Files
2026-07-13 12:49:29 +08:00

496 lines
16 KiB
JavaScript

// globals
var layer_defs, net, trainer;
var t = "\
layer_defs = [];\n\
layer_defs.push({type:'input', out_sx:28, out_sy:28, out_depth:1});\n\
layer_defs.push({type:'fc', num_neurons:50, activation:'tanh'});\n\
layer_defs.push({type:'fc', num_neurons:50, activation:'tanh'});\n\
layer_defs.push({type:'fc', num_neurons:2});\n\
layer_defs.push({type:'fc', num_neurons:50, activation:'tanh'});\n\
layer_defs.push({type:'fc', num_neurons:50, activation:'tanh'});\n\
layer_defs.push({type:'regression', num_neurons:28*28});\n\
\n\
net = new convnetjs.Net();\n\
net.makeLayers(layer_defs);\n\
\n\
trainer = new convnetjs.SGDTrainer(net, {learning_rate:1, method:'adadelta', batch_size:50, l2_decay:0.001, l1_decay:0.001});\n\
";
// ------------------------
// BEGIN MNIST SPECIFIC STUFF
// ------------------------
var sample_training_instance = function() {
// find an unloaded batch
var bi = Math.floor(Math.random()*loaded_train_batches.length);
var b = loaded_train_batches[bi];
var k = Math.floor(Math.random()*3000); // sample within the batch
var n = b*3000+k;
// load more batches over time
if(step_num%5000===0 && step_num>0) {
for(var i=0;i<num_batches;i++) {
if(!loaded[i]) {
// load it
load_data_batch(i);
break; // okay for now
}
}
}
// fetch the appropriate row of the training image and reshape into a Vol
var p = img_data[b].data;
var x = new convnetjs.Vol(28,28,1,0.0);
var W = 28*28;
for(var i=0;i<W;i++) {
var ix = ((W * k) + i) * 4;
x.w[i] = p[ix]/255.0;
}
return {x:x, label:labels[n]};
}
var num_batches = 21; // 20 training batches, 1 test
var data_img_elts = new Array(num_batches);
var img_data = new Array(num_batches);
var loaded = new Array(num_batches);
var loaded_train_batches = [];
// int main
$(window).load(function() {
$("#newnet").val(t);
change_net();
for(var k=0;k<loaded.length;k++) { loaded[k] = false; }
load_data_batch(0); // async load train set batch 0 (6 total train batches)
load_data_batch(20); // async load test set (batch 6)
start_fun();
});
var start_fun = function() {
if(loaded[0] && loaded[20]) {
console.log('starting!');
setInterval(load_and_step, 0); // lets go!
}
else { setTimeout(start_fun, 200); } // keep checking
}
var load_data_batch = function(batch_num) {
// Load the dataset with JS in background
data_img_elts[batch_num] = new Image();
var data_img_elt = data_img_elts[batch_num];
data_img_elt.onload = function() {
var data_canvas = document.createElement('canvas');
data_canvas.width = data_img_elt.width;
data_canvas.height = data_img_elt.height;
var data_ctx = data_canvas.getContext("2d");
data_ctx.drawImage(data_img_elt, 0, 0); // copy it over... bit wasteful :(
img_data[batch_num] = data_ctx.getImageData(0, 0, data_canvas.width, data_canvas.height);
loaded[batch_num] = true;
if(batch_num < 20) { loaded_train_batches.push(batch_num); }
console.log('finished loading data batch ' + batch_num);
};
data_img_elt.src = "mnist/mnist_batch_" + batch_num + ".png";
}
// ------------------------
// END MNIST SPECIFIC STUFF
// ------------------------
var maxmin = cnnutil.maxmin;
var f2t = cnnutil.f2t;
var render_act = function(A) {
var w = A.w;
var mm = maxmin(w);
var s = 1;
var canv = document.createElement('canvas');
canv.className = 'rendera';
var W = A.sx * s;
var H = A.sy * s;
canv.width = W;
canv.height = H;
var ctx = canv.getContext('2d');
var g = ctx.createImageData(W, H);
var d = 0;
for(var x=0;x<A.sx;x++) {
for(var y=0;y<A.sy;y++) {
var dval = Math.floor((A.get(x,y,d)-mm.minv)/mm.dv*255);
for(var dx=0;dx<s;dx++) {
for(var dy=0;dy<s;dy++) {
var pp = ((W * (y*s+dy)) + (dx + x*s)) * 4;
for(var i=0;i<3;i++) { g.data[pp + i] = dval; } // rgb
g.data[pp+3] = 255; // alpha channel
}
}
}
}
ctx.putImageData(g, 0, 0);
return canv;
}
// elt is the element to add all the canvas activation drawings into
// A is the Vol() to use
// scale is a multiplier to make the visualizations larger. Make higher for larger pictures
// if grads is true then gradients are used instead
var draw_activations = function(elt, A, scale, grads) {
var s = scale || 2; // scale
var draw_grads = false;
if(typeof(grads) !== 'undefined') draw_grads = grads;
// get max and min activation to scale the maps automatically
var w = draw_grads ? A.dw : A.w;
var mm = maxmin(w);
// create the canvas elements, draw and add to DOM
for(var d=0;d<A.depth;d++) {
var canv = document.createElement('canvas');
canv.className = 'actmap';
var W = A.sx * s;
var H = A.sy * s;
canv.width = W;
canv.height = H;
var ctx = canv.getContext('2d');
var g = ctx.createImageData(W, H);
for(var x=0;x<A.sx;x++) {
for(var y=0;y<A.sy;y++) {
if(draw_grads) {
var dval = Math.floor((A.get_grad(x,y,d)-mm.minv)/mm.dv*255);
} else {
var dval = Math.floor((A.get(x,y,d)-mm.minv)/mm.dv*255);
}
for(var dx=0;dx<s;dx++) {
for(var dy=0;dy<s;dy++) {
var pp = ((W * (y*s+dy)) + (dx + x*s)) * 4;
for(var i=0;i<3;i++) { g.data[pp + i] = dval; } // rgb
g.data[pp+3] = 255; // alpha channel
}
}
}
}
ctx.putImageData(g, 0, 0);
elt.appendChild(canv);
}
}
var visualize_activations = function(net, elt) {
// clear the element
elt.innerHTML = "";
// show activations in each layer
var N = net.layers.length;
for(var i=0;i<N;i++) {
var L = net.layers[i];
var layer_div = document.createElement('div');
// visualize activations
var activations_div = document.createElement('div');
activations_div.appendChild(document.createTextNode('Activations:'));
activations_div.appendChild(document.createElement('br'));
activations_div.className = 'layer_act';
var scale = 2;
if(L.layer_type==='fc') scale = 10; // for softmax
if(L.layer_type==='regression') {
var Vvis = L.out_act.clone();
Vvis.sx = 28;
Vvis.sy = 28;
Vvis.depth = 1;
draw_activations(activations_div, Vvis, scale);
} else {
draw_activations(activations_div, L.out_act, scale);
if(i===0) {
// also append the regression layer right nex tto input
// so that it's easy to compare
activations_div.appendChild(document.createElement('br'));
activations_div.appendChild(document.createTextNode('Predicted reconstruction:'));
activations_div.appendChild(document.createElement('br'));
var Vvis = net.layers[net.layers.length-1].out_act.clone();
Vvis.sx = 28;
Vvis.sy = 28;
Vvis.depth = 1;
draw_activations(activations_div, Vvis, scale);
}
}
if(L.layer_type === 'fc' && i===1) {
var filters_div = document.createElement('div');
filters_div.appendChild(document.createTextNode('Weights:'));
filters_div.appendChild(document.createElement('br'));
for(var j=0;j<L.filters.length;j++) {
var Lshow = L.filters[j].clone();
Lshow.sx = 28;
Lshow.sy = 28;
Lshow.depth = 1;
draw_activations(filters_div, Lshow, 2);
}
activations_div.appendChild(filters_div);
}
// visualize filters if they are of reasonable size
if(L.layer_type === 'conv') {
var filters_div = document.createElement('div');
if(L.filters[0].sx>3) {
// actual weights
filters_div.appendChild(document.createTextNode('Weights:'));
filters_div.appendChild(document.createElement('br'));
for(var j=0;j<L.filters.length;j++) {
draw_activations(filters_div, L.filters[j], 2);
}
// gradients
filters_div.appendChild(document.createElement('br'));
filters_div.appendChild(document.createTextNode('Gradients:'));
filters_div.appendChild(document.createElement('br'));
for(var j=0;j<L.filters.length;j++) {
draw_activations(filters_div, L.filters[j], 2, true);
}
} else {
filters_div.appendChild(document.createTextNode('Weights hidden, too small'));
}
activations_div.appendChild(filters_div);
}
layer_div.appendChild(activations_div);
// print some stats on left of the layer
layer_div.className = 'layer ' + 'lt' + L.layer_type;
var title_div = document.createElement('div');
title_div.className = 'ltitle'
var t = L.layer_type + ' (' + L.out_sx + 'x' + L.out_sy + 'x' + L.out_depth + ')';
title_div.appendChild(document.createTextNode(t));
layer_div.appendChild(title_div);
if(L.layer_type==='conv') {
var t = 'filter size ' + L.filters[0].sx + 'x' + L.filters[0].sy + 'x' + L.filters[0].depth + ', stride ' + L.stride;
layer_div.appendChild(document.createTextNode(t));
layer_div.appendChild(document.createElement('br'));
}
if(L.layer_type==='pool') {
var t = 'pooling size ' + L.sx + 'x' + L.sy + ', stride ' + L.stride;
layer_div.appendChild(document.createTextNode(t));
layer_div.appendChild(document.createElement('br'));
}
// find min, max activations and display them
var mma = maxmin(L.out_act.w);
var t = 'max activation: ' + f2t(mma.maxv) + ', min: ' + f2t(mma.minv);
layer_div.appendChild(document.createTextNode(t));
layer_div.appendChild(document.createElement('br'));
// number of parameters
if(L.layer_type==='conv') {
var tot_params = L.sx*L.sy*L.in_depth*L.filters.length + L.filters.length;
var t = 'parameters: ' + L.filters.length + 'x' + L.sx + 'x' + L.sy + 'x' + L.in_depth + '+' + L.filters.length + ' = ' + tot_params;
layer_div.appendChild(document.createTextNode(t));
layer_div.appendChild(document.createElement('br'));
}
if(L.layer_type==='fc') {
var tot_params = L.num_inputs*L.filters.length + L.filters.length;
var t = 'parameters: ' + L.filters.length + 'x' + L.num_inputs + '+' + L.filters.length + ' = ' + tot_params;
layer_div.appendChild(document.createTextNode(t));
layer_div.appendChild(document.createElement('br'));
}
// css madness needed here...
var clear = document.createElement('div');
clear.className = 'clear';
layer_div.appendChild(clear);
elt.appendChild(layer_div);
}
}
// loads a training image and trains on it with the network
var paused = false;
var embed_samples = [];
var embed_imgs = [];
var load_and_step = function() {
if(paused) return;
if(embed_samples.length === 0) { // happens once
for(var k=0;k<200;k++) {
var s = sample_training_instance();
embed_samples.push(s);
// render x and save it too
var I = render_act(s.x);
embed_imgs.push(I);
}
}
var sample = sample_training_instance();
step(sample); // process this image
}
var lix = 5;
var d0 = 0;
var d1 = 1;
function cycle() {
var selected_layer = net.layers[lix];
d0 += 1;
d1 += 1;
if(d1 >= selected_layer.out_depth) d1 = 0; // and wrap
if(d0 >= selected_layer.out_depth) d0 = 0; // and wrap
$("#cyclestatus").html('drawing neurons ' + d0 + ' and ' + d1 + ' of layer #' + lix + ' (' + net.layers[lix].layer_type + ')');
}
function updateLix(newlix) {
$("#button"+lix).css('background-color', ''); // erase highlight
lix = newlix;
d0 = 0;
d1 = 1; // reset these
$("#button"+lix).css('background-color', '#FFA');
$("#cyclestatus").html('drawing neurons ' + d0 + ' and ' + d1 + ' of layer with index ' + lix + ' (' + net.layers[lix].layer_type + ')');
}
var lossGraph = new cnnvis.Graph();
var xLossWindow = new cnnutil.Window(100);
var w2LossWindow = new cnnutil.Window(100);
var w1LossWindow = new cnnutil.Window(100);
var step_num = 0;
var colors = ["red", "blue", "green", "orange", "magenta", "cyan", "purple", "silver", "olive", "lime", "yellow"];
var step = function(sample) {
// train on it with network
var stats = trainer.train(sample.x, sample.x.w);
// keep track of stats such as the average training error and loss
xLossWindow.add(stats.cost_loss);
w1LossWindow.add(stats.l1_decay_loss);
w2LossWindow.add(stats.l2_decay_loss);
// visualize training status
var train_elt = document.getElementById("trainstats");
train_elt.innerHTML = '';
var t = 'Forward time per example: ' + stats.fwd_time + 'ms';
train_elt.appendChild(document.createTextNode(t));
train_elt.appendChild(document.createElement('br'));
var t = 'Backprop time per example: ' + stats.bwd_time + 'ms';
train_elt.appendChild(document.createTextNode(t));
train_elt.appendChild(document.createElement('br'));
var t = 'Regression loss: ' + f2t(xLossWindow.get_average());
train_elt.appendChild(document.createTextNode(t));
train_elt.appendChild(document.createElement('br'));
var t = 'L2 Weight decay loss: ' + f2t(w2LossWindow.get_average());
train_elt.appendChild(document.createTextNode(t));
train_elt.appendChild(document.createElement('br'));
var t = 'L1 Weight decay loss: ' + f2t(w1LossWindow.get_average());
train_elt.appendChild(document.createTextNode(t));
train_elt.appendChild(document.createElement('br'));
var t = 'Examples seen: ' + step_num;
train_elt.appendChild(document.createTextNode(t));
train_elt.appendChild(document.createElement('br'));
// visualize activations
if(step_num % 100 === 0) {
var vis_elt = document.getElementById("visnet");
visualize_activations(net, vis_elt);
}
// visualize embedding
if(step_num % 100 === 0) {
var embcanvas = document.getElementById('embedding');
var ctx = embcanvas.getContext("2d");
var EW = embcanvas.width;
var EH = embcanvas.height;
// propagate a few training examples through the network and grab codes
var xcodes = [];
var ycodes = [];
var ns = embed_samples.length; // number of samples
for(var k=0;k<ns;k++) {
var sample = embed_samples[k];
net.forward(sample.x);
var xcode = net.layers[lix].out_act.w[d0];
var ycode = net.layers[lix].out_act.w[d1];
xcodes.push(xcode);
ycodes.push(ycode);
}
var mmx = cnnutil.maxmin(xcodes);
var mmy = cnnutil.maxmin(ycodes);
// draw every example into the canvas
ctx.clearRect(0,0,EW,EH);
for(var k=0;k<ns;k++) {
var xpos = (EW-28*2)*(xcodes[k]-mmx.minv)/mmx.dv+28;
var ypos = (EH-28*2)*(ycodes[k]-mmy.minv)/mmy.dv+28;
// draw border according to class
ctx.fillStyle = colors[embed_samples[k].label];
ctx.fillRect(xpos-2,ypos-2,32,32);
ctx.drawImage(embed_imgs[k], xpos, ypos );
}
}
// log progress to graph, (full loss)
if(step_num % 200 === 0) {
var xa = xLossWindow.get_average();
var xw1 = w1LossWindow.get_average();
var xw2 = w2LossWindow.get_average();
if(xa >= 0 && xw1 >= 0 && xw2 >= 0) { // if they are -1 it means not enough data was accumulated yet for estimates
lossGraph.add(step_num, xa + xw1 + xw2);
lossGraph.drawSelf(document.getElementById("lossgraph"));
}
}
step_num++;
}
// user settings
var change_lr = function() {
trainer.learning_rate = parseFloat(document.getElementById("lr_input").value);
update_net_param_display();
}
var update_net_param_display = function() {
document.getElementById('lr_input').value = trainer.learning_rate;
}
var toggle_pause = function() {
paused = !paused;
var btn = document.getElementById('buttontp');
if(paused) { btn.value = 'resume' }
else { btn.value = 'pause'; }
}
var dump_json = function() {
document.getElementById("dumpjson").value = JSON.stringify(net.toJSON());
}
var clear_graph = function() {
lossGraph = new cnnvis.Graph(); // reinit graph too
}
var reset_all = function() {
update_net_param_display();
// reinit windows that keep track of val/train accuracies
lossGraph = new cnnvis.Graph(); // reinit graph too
step_num = 0;
// enter buttons for layers
var t = '';
for(var i=1;i<net.layers.length-1;i++) { // ignore input and regression layers (first and last)
var butid = "button" + i;
t += "<input id=\""+butid+"\" value=\"" + net.layers[i].layer_type + "(" + net.layers[i].out_depth + ")" +"\" type=\"submit\" onclick=\"updateLix("+i+")\" style=\"width:80px; height: 30px; margin:5px;\";>";
}
$("#layer_ixes").html(t);
$("#button"+lix).css('background-color', '#FFA');
$("#cyclestatus").html('drawing neurons ' + d0 + ' and ' + d1 + ' of layer with index ' + lix + ' (' + net.layers[lix].layer_type + ')');
}
var load_from_json = function() {
var jsonString = document.getElementById("dumpjson").value;
var json = JSON.parse(jsonString);
net = new convnetjs.Net();
net.fromJSON(json);
reset_all();
}
var change_net = function() {
eval($("#newnet").val());
reset_all();
}