// Cloned by Jack O'Brien on 5 Dec 2020 from World "Character recognition neural network" by "Coding Train" project // Please leave this clone trail here.// Port of Character recognition neural network from here:// https://github.com/CodingTrain/Toy-Neural-Network-JS/tree/master/examples/mnist// with many modifications // --- defined by MNIST - do not change these ---------------------------------------const PIXELS =28;// images in data set are tiny const PIXELSSQUARED = PIXELS * PIXELS;// number of training and test exemplars in the data set:const NOTRAIN =60000;const NOTEST =10000;//--- can modify all these --------------------------------------------------// no of nodes in network const noinput = PIXELSSQUARED;const nohidden1 =64;const nohidden2 =64;const nooutput =10;const learningrate =0.01;// default 0.1 // should we train every timestep or not
let do_training =true;// how many to train and test per timestep const TRAINPERSTEP =32;const TESTPERSTEP =16;// multiply it by this to magnify for display const ZOOMFACTOR =10;const ZOOMPIXELS = ZOOMFACTOR * PIXELS;// 3 rows of// large image + 50 gap + small image // 50 gap between rows const canvaswidth =( PIXELS + ZOOMPIXELS )+415;const canvasheight =( ZOOMPIXELS *3)+100;const DOODLE_THICK =16;// thickness of doodle lines const DOODLE_BLUR =3;// blur factor applied to doodles
let mnist;// all data is loaded into this // mnist.train_images// mnist.train_labels// mnist.test_images// mnist.test_labels
let nn;
let trainrun =1;
let train_index =0;
let testrun =1;
let test_index =0;
let total_tests =0;
let total_correct =0;
let globalScore =0;
let learningRateFlag =false;// images in LHS:
let doodle, demo;
let doodle_exists =false;
let demo_exists =false;
let mousedrag =false;// are we in the middle of a mouse drag drawing? // ConfusionMatrix
let confusionM;
let canvas;// let draw;
let ctxdraw;var thedoodleimage;var doodleImageData;var convnet;
let trainer;const inputSize =24;// save inputs to global var to inspect// type these names in console var train_inputs, test_inputs, demo_inputs, doodle_inputs;// Matrix.randomize() is changed to point to this. Must be defined by user of Matrix. function randomWeight(){return( AB.randomFloatAtoB (-0.9,0.9));// Coding Train default is -1 to 1}// CSS trick // make run header bigger
$("#runheaderbox").css ({"max-height":"95vh"});//--- start of AB.msgs structure: ---------------------------------------------------------// We output a serious of AB.msgs to put data at various places in the run header var thehtml;// 1 Doodle header
thehtml ="<hr> <h1> 1. Doodle </h1> Top row: Doodle (left) and shrunk (right). <br> "+" Draw your doodle in top LHS. <button onclick='wipeDoodle();' class='normbutton' >Clear doodle</button> <br> "+"<div></div>";// <canvas id='doodleCanvas' style='width:358px;height:400px'></canvas>
AB.msg ( thehtml,1);// 2 Doodle variable data (guess)// 3 Training header
thehtml ="<hr> <h1> 2. Training </h1> Middle row: Training image magnified (left) and original (right). <br> "+" <button onclick='do_training == false?do_training = true: do_training = false;' class='normbutton' >Toggle training</button> "+" <button class='normbutton' onclick='saveModel()'>Save Model</button><button class='normbutton' onclick='loadModel()'>Load Model</button> <br> ";
AB.msg ( thehtml,3);// 4 variable training data // 5 Testing header
thehtml ="<h3> Hidden tests </h3> ";
AB.msg ( thehtml,5);// 6 variable testing data // 7 Demo header
thehtml ="<hr> <h1> 3. Demo </h1> Bottom row: Test image magnified (left) and original (right). <br>"+" The network is <i>not</i> trained on any of these images. <br> "+" <button onclick='makeDemo();' class='normbutton' >Demo test image</button> <br>";
AB.msg ( thehtml,7);// 8 Demo variable data (random demo ID)// 9 Demo variable data (changing guess)const greenspan ="<span style='font-weight:bold; font-size:x-large; color:darkgreen'> ";//--- end of AB.msgs structure: ---------------------------------------------------------function setup(){
createCanvas ( canvaswidth, canvasheight );
doodle = createGraphics ( ZOOMPIXELS, ZOOMPIXELS );// doodle on larger canvas
doodle.pixelDensity(1);// draw = document.getElementById("defaultCanvas0");// ctxdraw = draw.getContext("2d");// painting = false,// lastX = 0,// lastY = 0,// lineThickness = 1;// JS load other JS // maybe have a loading screen while loading the JS and the data set
AB.loadingScreen();
$.getScript ("/uploads/jobrien14/webcnn.js",function(){
$.getScript ("/uploads/jobrien14/mathutils.js",function(){
$.getScript ("/uploads/jobrien14/mnist.js",function(){
$.getScript ("/uploads/jobrien14/matrix.js",function(){
console.log ("All JS loaded");
createModel();
confusionM =newMatrix(10,10);
loadData();});});});});}//Code to create initial model architecturefunction createModel(){
convnet =newWebCNN;
convnet.newLayer({ name:"image", type: LAYER_TYPE_INPUT_IMAGE, width: inputSize, height: inputSize, depth:1});
convnet.newLayer({ name:"conv1", type: LAYER_TYPE_CONV, units:10, kernelWidth:5, kernelHeight:5, strideX:1, strideY:1, padding:false, activation: ACTIVATION_RELU });
convnet.newLayer({ name:"pool1", type: LAYER_TYPE_MAX_POOL, poolWidth:2, poolHeight:2, strideX:2, strideY:2});
convnet.newLayer({ name:"conv2", type: LAYER_TYPE_CONV, units:20, kernelWidth:5, kernelHeight:5, strideX:1, strideY:1, padding:false, activation: ACTIVATION_RELU });
convnet.newLayer({ name:"pool2", type: LAYER_TYPE_MAX_POOL, poolWidth:2, poolHeight:2, strideX:2, strideY:2});
convnet.newLayer({ name:"out", type: LAYER_TYPE_FULLY_CONNECTED, units:10, activation: ACTIVATION_SOFTMAX });
convnet.initialize();
convnet.setLearningRate(0.01);
convnet.setMomentum(0.9);
convnet.setLambda(0.0);return convnet;}function saveModel(){
console.log('Saving Model to server')
AB.saveData ( nn.serialize());}function loadModel(){
AB.restoreData (function( a ){
console.log('Restoring')// nn.deserialize(a);});}// load data set from local file (on this server)function loadData(){
loadMNIST (function(data){
mnist = data;
console.log ("All data loaded into mnist object:")
console.log(mnist);
AB.removeLoading();// if no loading screen exists, this does nothing });}function getImage ( img )// make a P5 image object from a raw data array {
let theimage = createImage (PIXELS, PIXELS);// make blank image, then populate it
theimage.loadPixels();for(let i =0; i < PIXELSSQUARED ; i++){
let bright = img[i];
let index = i *4;
theimage.pixels[index +0]= bright;
theimage.pixels[index +1]= bright;
theimage.pixels[index +2]= bright;
theimage.pixels[index +3]=255;}
theimage.updatePixels();return theimage;}function getInputs ( img )// convert img array into normalised input array {
let inputs =[];for(let i =0; i < PIXELSSQUARED ; i++){
let bright = img[i];
inputs[i]= bright /255;// normalise to 0 to 1}return( inputs );}// Image processing// returns the image in a suitable format that the model expects// expects a 3 dimensional object with a height, width and pixel data, must also match model inputfunction imagePreProcessing(img,Dimensions){return{
width:Dimensions,
height:Dimensions,
data: getImage(randomCrop(img,Dimensions),Dimensions).pixels
}}// Randomly crops the input image so as to reduce overfitting and imporve generalisation// Mentioned on the associate website and githubfunction randomCrop(img,Dimensions){const cropVal = PIXELS -Dimensions;var randVal1 =Math.floor(Math.random()* cropVal);var randVal2 =Math.floor(Math.random()* cropVal);
let newWidth = randVal1 +Dimensions;
let newHeight = randVal2 +Dimensions;
let outputImg =[];for(let i = randVal1; i < newWidth; i++)for(let j = randVal2; j < newHeight; j++)
outputImg.push(img[i * PIXELS + j]);return outputImg
}function trainit (show)// train the network with a single exemplar, from global var "train_index", show visual on or off {if(train_index % TRAINPERSTEP !==0)returnvoid train_index++;
let img = mnist.train_images[train_index];
let label = mnist.train_labels[train_index];
let trainImages =[];
let trainLabels =[];// For this implementation the model is trained in batches as opposed to on single images at a time like the original implementation// Starting from the current train_index, a batch of images are loaded into an array as this is what the model expects as an inputfor(var i =0;i< TRAINPERSTEP;i++){
trainImages.push(imagePreProcessing(mnist.train_images[train_index+i], inputSize));
trainLabels.push(mnist.train_labels[train_index+i]);}var theimage = getImage ( img );// get image from data array
image ( theimage,0, ZOOMPIXELS+50, ZOOMPIXELS, ZOOMPIXELS );// magnified
image ( theimage, ZOOMPIXELS+50, ZOOMPIXELS+50, PIXELS, PIXELS );// original// The model is trained on the current batch with the given labels
convnet.trainCNNClassifier(trainImages, trainLabels);
thehtml =" trainrun: "+ trainrun +"<br> no: "+ train_index ;
AB.msg ( thehtml,4);
train_index++;if( train_index == NOTRAIN ){
train_index =0;
console.log("finished trainrun: "+ trainrun );
trainrun++;}}
let img1;function testit()// test the network with a single exemplar, from global var "test_index"{
let img = mnist.test_images[test_index];
let label = mnist.test_labels[test_index];
img1=img;// set up the inputs
let inputs = getInputs ( img );
test_inputs = inputs;// can inspect in console // similar to above, the image needs to be put through the same process as the training data to have the best possibility for a match
let prediction = convnet.classifyImages([imagePreProcessing(img, inputSize)]);// array of outputs
let guess = findMax(prediction);// the top output // let confidence = 100; //
confusionM.data[guess][label]++;
total_tests++;if(guess == label) total_correct++;
let percent =(total_correct / total_tests)*100;
globalScore = percent
thehtml =" testrun: "+ testrun +"<br> no: "+ total_tests +" <br> "+" correct: "+ total_correct +"<br>"+" score: "+ greenspan + percent.toFixed(2)+"</span>";
AB.msg ( thehtml,6);
test_index++;if( test_index == NOTEST ){
console.log("finished testrun: "+ testrun +" score: "+ percent.toFixed(2));
confusionM.print();
confusionM =newMatrix(10,10);
testrun++;
test_index =0;
total_tests =0;
total_correct =0;}}//--- find no.1 (and maybe no.2) output nodes ---------------------------------------// (restriction) assumes array values start at 0 (which is true for output nodes)
let no1value;
let no2value;
let no1;
let no2;// function was editied to work with the convnet model and to find the top 2 predictionsfunction find12 (a)// return array showing indexes of no.1 and no.2 values in array {
no1 =0;
no2 =0;// Find no1for(var i=0;i<a[0].size;i++){
a[0].getValue(0,0, i)>a[0].getValue(0,0, no1)?no1=i:no1=no1;}// Find no2for(var j=0;j<a[0].size;j++){(a[0].getValue(0,0, j)>a[0].getValue(0,0, no2)&& a[0].getValue(0,0, j)<a[0].getValue(0,0, no1))?no2=j:no2=no2;}
no1value = a[0].getValue(0,0, no1);
no2value = a[0].getValue(0,0, no2);var b =[[ no1, no1value ],[no2, no2value]];return b;}// just get the maximum - separate function for speed - done many times // find our guess - the max of the output nodes array// Altered similar to above, uses built in .get Value to find the score for each of the nodes and returns the highest scorefunction findMax (a){
let test= a[0];var guess =0;for(var i=0;i<test.size;i++){
test.getValue(0,0, i)>test.getValue(0,0, guess)?guess=i:guess=guess
}return guess;}// --- the draw function -------------------------------------------------------------// every step:function draw(){// check if libraries and data loaded yet:if(typeof mnist =='undefined')return;// how can we get white doodle on black background on yellow canvas?// background('#ffffcc'); doodle.background('black');// background ('black');
stroke(0);
fill(0);
rect(0,0,( PIXELS + ZOOMPIXELS )+50,( ZOOMPIXELS *3)+100);
fill(255);
rect(( PIXELS + ZOOMPIXELS )+58,( PIXELS + ZOOMPIXELS )+58,( PIXELS + ZOOMPIXELS )+50,( ZOOMPIXELS *3)+100);if( do_training ){for(let i =0; i < TRAINPERSTEP; i++){if(i ==0){
trainit(true);}// show only one per step - still flashes by else
trainit(false);}// do some testing per step for(let i =0; i < TESTPERSTEP; i++)
testit();}// keep drawing demo and doodle images // and keep guessing - we will update our guess as time goes on if( demo_exists ){
drawDemo();
guessDemo();}if( doodle_exists ){
drawDoodle();
guessDoodle();}// detect doodle drawing // (restriction) the following assumes doodle starts at 0,0 if( mouseIsPressed )// gets called when we click buttons, as well as if in doodle corner {// console.log ( mouseX + " " + mouseY + " " + pmouseX + " " + pmouseY );var MAX = ZOOMPIXELS +20;// can draw up to this pixels in corner if((mouseX < MAX)&&(mouseY < MAX)&&(pmouseX < MAX)&&(pmouseY < MAX)){
mousedrag =true;// start a mouse drag
doodle_exists =true;
doodle.stroke('white');
doodle.strokeWeight( DOODLE_THICK );
doodle.line(mouseX, mouseY, pmouseX, pmouseY);}}else{// are we exiting a drawingif( mousedrag ){
mousedrag =false;// console.log ("Exiting draw. Now blurring.");
doodle.filter (BLUR, DOODLE_BLUR);// just blur once// doodle.filter (ERODE);// doodle.filter (ERODE);// doodle.filter (ERODE);// doodle.filter (ERODE);// doodle.filter (ERODE);
console.log (doodle);}}
textSize(22);
fill(255);
text("Confusion Matrix",( PIXELS + ZOOMPIXELS )+140,( PIXELS + ZOOMPIXELS)+50);for(var i =0;i<10;i++){for(var j =0;j<10;j++){var innerValue = confusionM.data[i][j];var x =(( PIXELS + ZOOMPIXELS )+58)+(i*35);var y =(( PIXELS + ZOOMPIXELS )+58)+(j*35);// stroke(0);
squareColor = color(80,20,255);
squareColor.setAlpha(255*(innerValue/1000));
fill(squareColor);
rect(x, y,35,35);
textSize(12);
fill(0);
text(innerValue, x+10, y+25);}}}//--- demo -------------------------------------------------------------// demo some test image and predict it// get it from test set so have not used it in trainingfunction makeDemo(){
demo_exists =true;var i = AB.randomIntAtoB (0, NOTEST -1);
demo = mnist.test_images[i];var label = mnist.test_labels[i];
thehtml ="Test image no: "+ i +"<br>"+"Classification: "+ label +"<br>";
AB.msg ( thehtml,8);// type "demo" in console to see raw data }function drawDemo(){var theimage = getImage ( demo );// console.log (theimage);
image ( theimage,0, canvasheight - ZOOMPIXELS, ZOOMPIXELS, ZOOMPIXELS );// magnified
image ( theimage, ZOOMPIXELS+50, canvasheight - ZOOMPIXELS, PIXELS, PIXELS );// original}function guessDemo(){
let inputs = getInputs ( demo );
demo_inputs = inputs;// can inspect in console
let prediction = convnet.classifyImages([imagePreProcessing(demo, inputSize)]);
let guess = findMax(prediction);
thehtml =" We classify it as: "+ greenspan + guess +"</span>";
AB.msg ( thehtml,9);}
let doodle1;//--- doodle -------------------------------------------------------------function drawDoodle(){// doodle is createGraphics not createImage
let theimage = doodle.get();// console.log (theimage);
image ( theimage,0,0, ZOOMPIXELS, ZOOMPIXELS );// original
image ( theimage, ZOOMPIXELS+50,0, PIXELS, PIXELS );// shrunk}function formatDoodle(img,Dimensions){
let outputImg =[];for(let i =0; i <Dimensions; i++)for(let j =0; j <Dimensions; j++)
outputImg.push(img[i * PIXELS + j]);// return outputImgreturn{
width:Dimensions,
height:Dimensions,
data: getImage(outputImg).pixels
}}function guessDoodle(){if(mousedrag===false)return;// doodle is createGraphics not createImage
let img = doodle.get();
img.resize ( PIXELS, PIXELS );
img.loadPixels();
doodle1=img;
let inputs =[];for(let j =0; j < PIXELS; j++){/** @type {!Array} */
inputs[j]=[];for(let i =0; i < PIXELS; i++){
inputs[j][i]= img.pixels[4*(j * PIXELS + i)];}}// doodle_inputs = inputs; // can inspect in console // thedoodleimage = getImage ( doodle_inputs );// doodleImageData = thedoodleimage.imageData;// feed forward to make prediction
let prediction = convnet.classifyImages([formatDoodle(inputs, inputSize)]);
let b = find12(prediction);
thehtml =" We classify it as: "+ greenspan + b[0][0]+"</span> with a confidence of: "+(b[0][1]*100).toFixed(2)+"%<br>"+" No.2 guess is: "+ greenspan + b[1][0]+"</span> with a confidence of: "+(b[1][1]*100).toFixed(2)+"%";
AB.msg ( thehtml,2);}function wipeDoodle(){
doodle_exists =false;
doodle.background('black');}// --- debugging --------------------------------------------------// in console// showInputs(demo_inputs);// showInputs(doodle_inputs);function showInputs ( inputs )// display inputs row by row, corresponding to square of pixels {var str ="";for(let i =0; i < inputs.length; i++){if( i % PIXELS ==0) str = str +"\n";// new line for each row of pixels var value = inputs[i];
str = str +" "+ value.toFixed(2);}
console.log (str);}// Penicl Drawing