diff --git a/index.html b/index.html index 74a47cb..ca24a58 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,25 @@ -
- +
+
+ Total number of neurons: + + + +
+
+ + +
+ +
+ + + +
+ - + + + + diff --git a/js/Layer.js b/js/Layer.js new file mode 100644 index 0000000..7c7098a --- /dev/null +++ b/js/Layer.js @@ -0,0 +1,200 @@ +class Layer extends Draggable { + constructor(neuronNum, x, y, label, cnv, act_func = ActivationFunction.TANH) { + super("layer"); + this.canvas = cnv; + this.act_func = act_func; + this.nextLayer = null; + this.prevLayer = null; + this.label = label; + this.x = x; + this.w = 50; + this.yGap = 40; + this.h = this.yGap * (neuronNum - 1) + 50; + this.neurons = Array.from( + { length: neuronNum }, + () => new Neuron(x, y, false, cnv), + ); + this.shrinked = false; + this.shownNeuronNum = this.getNeuronNum(); + + let middleYPoint = 350; + this.y = y + middleYPoint - this.h / 2; + + this.updateNeuronsCoordinates(); + // this.neurons.length > 4 && this.shrink(); + } + + addNeuron(neuron) { + this.neurons.push(neuron); + } + + removeNeuron() { + this.neurons.pop(); + } + + getNeuronNum() { + return this.neurons.length; + } + + getShownNeuronNum() { + return this.shownNeuronNum; + } + + setShownNeuronNum(shownNeuronNum) { + this.shownNeuronNum = shownNeuronNum; + } + + shrink() { + const { neurons, yGap } = this; + + if ( + this.getShownNeuronNum() === this.getNeuronNum() || + this.getNeuronNum() < 4 + ) { + this.expand(); + return; + } + + const mid = this.getShownNeuronNum() / 2; + for (let i = 0; i < this.getNeuronNum(); i++) { + if (!(i < mid || i >= this.getNeuronNum() - mid)) { + neurons[i].hide(); + } + } + + this.h = yGap * (this.getShownNeuronNum() - 1) + 50; + this.shrinked = true; + this.updateNeuronsCoordinates(); + } + + expand() { + this.neurons.forEach((neuron) => neuron.visible()); + this.shrinked = false; + + this.updateNeuronsCoordinates(); + } + + isShrinked() { + return this.shrinked; + } + + resetP5Settings() { + const commands = [ + { func: "textSize", args: [12] }, + { func: "textAlign", args: [LEFT, BASELINE] }, + { func: "textLeading", args: [15] }, + { func: "fill", args: [255] }, + ]; + + executeDrawingCommands(this.canvas, commands); + } + + showInfoBox() { + const commands = [ + { func: "fill", args: [0] }, + { func: "textSize", args: [18] }, + { func: "textAlign", args: [CENTER, TOP] }, + { func: "textLeading", args: [7] }, + { func: "text", args: [`.\n.\n.`, this.x, this.infoBoxY + 10, 50, 100] }, + { func: "textAlign", args: [CENTER, CENTER] }, + { + func: "text", + args: [ + this.getNeuronNum() - this.getShownNeuronNum(), + this.x, + this.infoBoxY + 15, + 50, + 100, + ], + }, + { func: "textAlign", args: [CENTER, BOTTOM] }, + { func: "textLeading", args: [7] }, + { func: "text", args: [`.\n.\n.`, this.x, this.infoBoxY, 50, 110] }, + ]; + + executeDrawingCommands(this.canvas, commands); + this.resetP5Settings(); + } + + updateNeuronsCoordinates() { + const isShrinked = this.isShrinked(); + const neuronNum = isShrinked + ? this.getShownNeuronNum() + : this.getNeuronNum(); + const infoBoxH = 90; + + this.h = this.yGap * (neuronNum - 1) + 50; + + let index = 0; + this.neurons.forEach((neuron) => { + if (!neuron.isHidden()) { + const externalHeight = + index > neuronNum / 2 && isShrinked ? infoBoxH : 0; + const x = this.x + this.w / 2; + const y = + this.y + + this.h / 2 + + externalHeight + + this.yGap * (index - (neuronNum - 1) / 2); + + if (index == Math.floor(neuronNum / 2)) { + this.infoBoxY = y; + } + neuron.updateCoordinates(x, y); + index++; + } + }); + + this.h += isShrinked ? infoBoxH : 0; + } + + call(x) { + let outs = this.neurons.map((neuron) => neuron.call(x)); + return outs.length === 1 ? outs[0] : outs; + } + + parameters() { + return this.neurons.flatMap((neuron) => neuron.parameters()); + } + + change_act_func(act_func) { + this.act_func = act_func; + this.neurons.forEach((neuron) => neuron.change_act_func(this.act_func)); + } + + setNextLayer(layer) { + this.nextLayer = layer; + + this.neurons.forEach((neuron) => { + let lines = []; + this.nextLayer.neurons.forEach((toNeuron) => { + lines.push(new Line(neuron, toNeuron)); + }); + neuron.setLines(lines); + }); + } + + setPrevLayer(layer) { + this.prevLayer = layer; + } + + show() { + let commands = [ + { func: "noFill", args: [] }, + { func: "rect", args: [this.x, this.y, 50, this.h] }, + { func: "fill", args: [0] }, + { func: "text", args: [this.label, this.x, this.y - 10] }, + { func: "fill", args: [255] }, + ]; + executeDrawingCommands(this.canvas, commands); + this.isShrinked() && this.showInfoBox(); + } + + draw() { + this.show(); + this.neurons.forEach((neuron) => neuron.draw()); + + !organizer.getDragActive() && this.over(); + (organizer.getDragActive() || this.dragging) && this.updateCoordinates(); + } +} diff --git a/js/Line.js b/js/Line.js new file mode 100644 index 0000000..813ab48 --- /dev/null +++ b/js/Line.js @@ -0,0 +1,12 @@ +class Line { + constructor(from, to) { + this.from = from; + this.to = to; + this.w = new Value(Math.random() * 2 - 1); + } + + draw() { + !(this.from.isHidden() || this.to.isHidden()) && + line(this.from.x, this.from.y, this.to.x, this.to.y); + } +} diff --git a/js/MLP.js b/js/MLP.js index 0ed809f..5bec895 100644 --- a/js/MLP.js +++ b/js/MLP.js @@ -1,257 +1,3 @@ -class Neuron { - constructor(x, y, hidden, cnv) { - this.w = []; - // for (let i = 0; i < nin; i++) { - // this.w.push(new Value(Math.random() * 2 - 1)); - // } - this.canvas = cnv; - this.b = new Value(Math.random() * 2 - 1); - this.act_func = ActivationFunction.TANH; - this.output = null; - this.lines = []; - this.x = x; - this.y = y; - this.hidden = hidden; - } - - hide() { - this.hidden = true; - } - - visible() { - this.hidden = false; - } - - call(x) { - let act = this.b; - for (let i = 0; i < this.w.length; i++) { - act = act.add(this.w[i].mul(x[i])); - } - - this.output = activation_functions[this.act_func](act); - return this.output; - } - - parameters() { - return [...this.w, this.b]; - } - - change_act_func(act_func) { - this.act_func = act_func; - } - - setLines(lines) { - this.lines = lines; - } - - updateCoordinates(x, y) { - this.x = x; - this.y = y; - } - - show() { - const commands = [ - { func: "circle", args: [this.x, this.y, 25, 25] }, - { func: "fill", args: [0] }, - { - func: "text", - args: [this.output?.data.toFixed(2), this.x + 30, this.y], - }, - { - func: "text", - args: [this.output?.grad.toFixed(2), this.x + 30, this.y + 25], - }, - { func: "fill", args: [255] }, - ]; - executeDrawingCommands(this.canvas, commands); - } - - draw() { - if (this.hidden) return; - this.lines.forEach((line) => line.draw()); - this.show(); - } -} - -class Line { - constructor(from, to) { - this.from = from; - this.to = to; - this.w = new Value(Math.random() * 2 - 1); - } - - draw() { - !(this.from.hidden || this.to.hidden) && - line(this.from.x, this.from.y, this.to.x, this.to.y); - } -} - -class Layer extends Draggable { - constructor(neuronNum, x, y, label, cnv, act_func = ActivationFunction.TANH) { - super("layer"); - this.canvas = cnv; - this.act_func = act_func; - this.nextLayer = null; - this.prevLayer = null; - this.label = label; - this.x = x; - this.w = 50; - this.yGap = 40; - this.h = this.yGap * (neuronNum - 1) + 50; - this.neurons = Array.from( - { length: neuronNum }, - () => new Neuron(x, y, false, cnv), - ); - this.shrinked = false; - this.shownNeuronsNum = 3; - - let middleYPoint = 350; - this.y = y + middleYPoint - this.h / 2; - - this.updateNeuronsCoordinates(); - this.neurons.length > 4 && this.shrink(); - } - - shrink() { - let mid = this.shownNeuronsNum / 2; - for (let i = 0; i < this.neurons.length; i++) { - if (!(i < mid || i >= this.neurons.length - mid)) { - this.neurons[i].hide(); - } - } - - this.h = this.yGap * (this.shownNeuronsNum - 1) + 50; - - this.shrinked = true; - this.updateNeuronsCoordinates(); - } - - expand() { - this.neurons.forEach((neuron) => neuron.visible()); - this.shrinked = false; - - this.updateNeuronsCoordinates(); - } - - resetP5Settings() { - const commands = [ - { func: "textSize", args: [12] }, - { func: "textAlign", args: [LEFT, BASELINE] }, - { func: "textLeading", args: [15] }, - { func: "fill", args: [255] }, - ]; - - executeDrawingCommands(this.canvas, commands); - } - - showInfoBox() { - const commands = [ - { func: "fill", args: [0] }, - { func: "textSize", args: [18] }, - { func: "textAlign", args: [CENTER, TOP] }, - { func: "textLeading", args: [7] }, - { func: "text", args: [`.\n.\n.`, this.x, this.infoBoxY + 10, 50, 100] }, - { func: "textAlign", args: [CENTER, CENTER] }, - { - func: "text", - args: [ - this.neurons.length - this.shownNeuronsNum, - this.x, - this.infoBoxY + 15, - 50, - 100, - ], - }, - { func: "textAlign", args: [CENTER, BOTTOM] }, - { func: "textLeading", args: [7] }, - { func: "text", args: [`.\n.\n.`, this.x, this.infoBoxY, 50, 110] }, - ]; - - executeDrawingCommands(this.canvas, commands); - this.resetP5Settings(); - } - - updateNeuronsCoordinates() { - const neuronNum = this.shrinked - ? this.shownNeuronsNum - : this.neurons.length; - const infoBoxH = 90; - - this.h = this.yGap * (neuronNum - 1) + 50; - - let index = 0; - this.neurons.forEach((neuron) => { - if (!neuron.hidden) { - const externalHeight = - index >= neuronNum / 2 && this.shrinked ? infoBoxH : 0; - const x = this.x + this.w / 2; - const y = - this.y + - this.h / 2 + - externalHeight + - this.yGap * (index - (neuronNum - 1) / 2); - - if (index == Math.floor(neuronNum / 2)) { - this.infoBoxY = y; - } - neuron.updateCoordinates(x, y); - index++; - } - }); - - this.h += this.shrinked ? infoBoxH : 0; - } - - call(x) { - let outs = this.neurons.map((neuron) => neuron.call(x)); - return outs.length === 1 ? outs[0] : outs; - } - - parameters() { - return this.neurons.flatMap((neuron) => neuron.parameters()); - } - - change_act_func(act_func) { - this.act_func = act_func; - this.neurons.forEach((neuron) => neuron.change_act_func(this.act_func)); - } - - setNextLayer(layer) { - this.nextLayer = layer; - - this.neurons.forEach((neuron) => { - let lines = []; - this.nextLayer.neurons.forEach((toNeuron) => { - lines.push(new Line(neuron, toNeuron)); - }); - neuron.setLines(lines); - }); - } - - setPrevLayer(layer) { - this.prevLayer = layer; - } - - show() { - let commands = [ - { func: "rect", args: [this.x, this.y, 50, this.h] }, - { func: "fill", args: [0] }, - { func: "text", args: [this.label, this.x, this.y - 10] }, - { func: "fill", args: [255] }, - ]; - executeDrawingCommands(this.canvas, commands); - this.shrinked && this.showInfoBox(); - } - - draw() { - this.show(); - this.neurons.forEach((neuron) => neuron.draw()); - - !organizer.getDragActive() && this.over(); - (organizer.getDragActive() || this.dragging) && this.updateCoordinates(); - } -} - class MLP extends Draggable { constructor(nin, nouts, x, y, cnv) { super("mlp"); diff --git a/js/Neuron.js b/js/Neuron.js new file mode 100644 index 0000000..96c9aee --- /dev/null +++ b/js/Neuron.js @@ -0,0 +1,78 @@ +class Neuron { + constructor(x, y, hidden, cnv) { + this.w = []; + // for (let i = 0; i < nin; i++) { + // this.w.push(new Value(Math.random() * 2 - 1)); + // } + this.canvas = cnv; + this.b = new Value(Math.random() * 2 - 1); + this.act_func = ActivationFunction.TANH; + this.output = null; + this.lines = []; + this.x = x; + this.y = y; + this.hidden = hidden; + } + + hide() { + this.hidden = true; + } + + visible() { + this.hidden = false; + } + + isHidden() { + return this.hidden; + } + + call(x) { + let act = this.b; + for (let i = 0; i < this.w.length; i++) { + act = act.add(this.w[i].mul(x[i])); + } + + this.output = activation_functions[this.act_func](act); + return this.output; + } + + parameters() { + return [...this.w, this.b]; + } + + change_act_func(act_func) { + this.act_func = act_func; + } + + setLines(lines) { + this.lines = lines; + } + + updateCoordinates(x, y) { + this.x = x; + this.y = y; + } + + show() { + const commands = [ + { func: "circle", args: [this.x, this.y, 25, 25] }, + { func: "fill", args: [0] }, + { + func: "text", + args: [this.output?.data.toFixed(2), this.x + 30, this.y], + }, + { + func: "text", + args: [this.output?.grad.toFixed(2), this.x + 30, this.y + 25], + }, + { func: "fill", args: [255] }, + ]; + executeDrawingCommands(this.canvas, commands); + } + + draw() { + if (this.isHidden()) return; + this.lines.forEach((line) => line.draw()); + this.show(); + } +} diff --git a/js/canvas.js b/js/canvas.js index f53cb3f..06152da 100644 --- a/js/canvas.js +++ b/js/canvas.js @@ -3,18 +3,14 @@ var organizer; var editOrganizer; function setup() { - const mainCanvas = createCanvas(1920, 1080); - const editCanvas = createGraphics(1920, 1080); + const mainCanvas = createCanvas(windowWidth, windowHeight); + const editCanvas = createGraphics(windowWidth, windowHeight); organizer = new Organizer(); editOrganizer = new EditOrganizer(editCanvas); + mlps.push(new MLP(4, [3, 15, 1], 600, 100, mainCanvas)); mlps.push(new MLP(4, [3, 5, 1], 600, 100, mainCanvas)); - mlps.push(new MLP(4, [3, 5, 1], 600, 100, mainCanvas)); -} - -function setupPopup() { - organizer.disable(); } function draw() { @@ -38,6 +34,11 @@ function doubleClicked() { mlps.forEach((mlp) => mlp.handleDoubleClicked()); } +function windowResized() { + resizeCanvas(windowWidth, windowHeight); + editOrganizer.resize(); +} + // mlps.push(new MLP(3, [3, 2, 1], 500, 300)); // // xs = [ @@ -65,15 +66,3 @@ function doubleClicked() { // clearInterval(intervalId); // } // }, 500); -function executeDrawingCommands(cnv, arr) { - const parent = cnv instanceof p5.Graphics ? cnv : window; - - for (let i = 0; i < arr.length; i++) { - let { func, args } = arr[i]; - if (typeof parent[func] === "function") { - parent[func](...args); - } else { - console.error(`Function '${func}' does not exist on canvas`); - } - } -} diff --git a/js/draggable.js b/js/draggable.js index 498fe7c..4e6d9ea 100644 --- a/js/draggable.js +++ b/js/draggable.js @@ -31,8 +31,9 @@ class Draggable { doubleClicked() { if (this.rollover && !editOrganizer.getSelected()) { - editOrganizer.enable(); editOrganizer.setSelected(this); + + editOrganizer.enable(); } } diff --git a/js/drawable.js b/js/drawable.js deleted file mode 100644 index 6072806..0000000 --- a/js/drawable.js +++ /dev/null @@ -1,5 +0,0 @@ -class Showable { - constructor() { - this.canvas; - } -} diff --git a/js/editOrganizer.js b/js/editOrganizer.js index 863ada2..3f05012 100644 --- a/js/editOrganizer.js +++ b/js/editOrganizer.js @@ -4,11 +4,30 @@ class EditOrganizer { this.enabled = false; this.selected = null; this.selectedCopy = null; - this.originX = (width - 500) / 2; - this.originY = 150; + this.originX; + this.originY; this.w = 500; this.h = 500; - this.setup(); + this.editPanel = getElementById("edit-panel"); + this.resize(); + this.setShownNeuronContainer = getElementById("shown-neuron-container"); + this.buttonsContainer = getElementById("buttons-container"); + this.neuronNumContainer = getElementById("neuron-container"); + } + + getCanvas() { + return this.canvas; + } + + setLayout() { + this.setShownNeuronContainer.style.left = this.originX + "px"; + this.setShownNeuronContainer.style.top = this.originY + "px"; + + this.buttonsContainer.style.left = this.originX + "px"; + this.buttonsContainer.style.top = this.originY + 100 + "px"; + + this.neuronNumContainer.style.left = this.originX + "px"; + this.neuronNumContainer.style.top = this.originY + this.h - 50 + "px"; } setup() { @@ -17,25 +36,66 @@ class EditOrganizer { button.mousePressed(() => { this.disable(); }); - let button1 = createButton("Add neuron"); - button1.position(100, 100); - button1.mousePressed(() => { - this.selectedCopy.neurons.push( - new Neuron(5, 0, this.selectedCopy.shrinked, this.canvas), - ); - this.selectedCopy.updateNeuronsCoordinates(); - }); + const layer = this.selectedCopy; + this.setLayout(); + + const properties = { + value: layer.getShownNeuronNum(), + max: layer.getNeuronNum().toString(), + min: "3", + }; - let button2 = createButton("shrink"); - button2.position(200, 200); - button2.mousePressed(() => { - this.selectedCopy.shrink(); + setElementProperties("set-shown-neuron", properties); + + addEventToElement("set-shown-neuron", "input", (e) => { + const val = e.target.value; + + layer.setShownNeuronNum(val); + setElementProperties("shown-neuron-label", { innerText: val }); + + layer.expand(); + layer.shrink(); }); - let button3 = createButton("expand"); - button3.position(200, 300); - button3.mousePressed(() => { - this.selectedCopy.expand(); + + addEventToElement("shrink-btn", "click", () => layer.shrink()); + addEventToElement("expand-btn", "click", () => layer.expand()); + + setElementProperties("set-neuron-num", { value: layer.getNeuronNum() }); + + addEventToElement("set-neuron-num", "change", (e) => { + const diff = e.target.value - layer.getNeuronNum(); + + if (diff > 0) { + for (let i = 0; i < diff; i++) { + layer.addNeuron( + new Neuron(5, 0, this.selectedCopy.isShrinked(), this.canvas), + ); + } + } else { + for (let i = 0; i < -diff; i++) { + layer.removeNeuron(); + } + } + + layer.setShownNeuronNum( + Math.min(layer.getNeuronNum(), layer.getShownNeuronNum()), + ); + + layer.updateNeuronsCoordinates(); + const newProperties = { + max: layer.getNeuronNum().toString(), + min: "3", + value: layer.getShownNeuronNum(), + }; + + setElementProperties("set-shown-neuron", newProperties); + setElementProperties("shown-neuron-label", { + innerText: newProperties.value, + }); + + layer.expand(); + layer.shrink(); }); } @@ -63,7 +123,7 @@ class EditOrganizer { this.selected = layer; this.selectedCopy = new Layer( - layer.neurons.length, + layer.getNeuronNum(), x, y - 100, layer.label, @@ -76,13 +136,24 @@ class EditOrganizer { this.enabled = false; this.selected = null; this.selectedCopy = null; + this.editPanel.style.display = "none"; } enable() { this.enabled = true; + this.editPanel.style.display = "block"; + this.setup(); } isEnabled() { return this.enabled; } + + resize() { + this.getCanvas().resizeCanvas(windowWidth, windowHeight); + + this.originX = (width - 500) / 2; + + this.originY = 150; + } } diff --git a/js/script.js b/js/script.js new file mode 100644 index 0000000..dc1b8cd --- /dev/null +++ b/js/script.js @@ -0,0 +1,27 @@ +function executeDrawingCommands(cnv, arr) { + const parent = cnv instanceof p5.Graphics ? cnv : window; + + for (let i = 0; i < arr.length; i++) { + let { func, args } = arr[i]; + if (typeof parent[func] === "function") { + parent[func](...args); + } else { + console.error(`Function '${func}' does not exist on canvas`); + } + } +} + +function getElementById(el) { + return document.getElementById(el); +} + +function setElementProperties(elId, properties) { + const el = getElementById(elId); + for (let prop in properties) { + el[prop] = properties[prop]; + } +} + +function addEventToElement(elId, eventName, func) { + getElementById(elId).addEventListener(eventName, func); +} diff --git a/js/showable.js b/js/showable.js deleted file mode 100644 index d325f0a..0000000 --- a/js/showable.js +++ /dev/null @@ -1,10 +0,0 @@ -let Showable = (Base) => - class extends Base { - constructor(label, canvas) { - super(label); - this.canvas = canvas; - } - show() { - throw new Error("Method 'show' must be implemented."); - } - }; diff --git a/style.css b/style.css index 6dd492f..51d1f6a 100644 --- a/style.css +++ b/style.css @@ -3,13 +3,29 @@ body { padding: 0; } -#disabled-background { - position: fixed; - width: 100%; - height: 100%; - z-index: 1; - background: rgba(0, 0, 0, 0.2); +#edit-panel { display: none; +} + +.edit-container { + position: absolute; + display: flex; justify-content: center; align-items: center; + +} + +.inp { + background: white; + width: 50px; + height: 30px; + border: none; + border-bottom: 1px lightgray solid; + border-radius: 5px; + text-align: center; + font-size: 18px; + transition: background-color 0.3s; + cursor: pointer; + padding: 0; + margin: 5px }