Skip to content

Instantly share code, notes, and snippets.

@Ni55aN
Created November 29, 2017 12:50
Show Gist options
  • Save Ni55aN/53e412fb9d5f7607dc8e1b1d22ef456e to your computer and use it in GitHub Desktop.
Save Ni55aN/53e412fb9d5f7607dc8e1b1d22ef456e to your computer and use it in GitHub Desktop.
class Perceptron {
constructor(layers, inputs) {
if (layers[0] !== inputs.length)
throw new Error("Invalid number of inputs");
this.l = layers;
this.activation = val => (val >= 0.5 ? 1 : 0);
this.learningRate = 0.1;
this.initWeights();
this.clearValues(inputs);
}
initWeights() {
var layersCount = this.l.length - 1;
var additional = 1;
this.w = new Array(layersCount).fill(null).map((_, i) => {
return new Array(this.l[i] + additional).fill(null).map((_, j) => {
return new Array(this.l[i + 1])
.fill(0)
.map(() => Math.random().toFixed(3));
});
});
}
clearValues(inputs) {
this.values = this.l.map(n => new Array(n).fill(0));
this.values[0] = inputs;
}
run(inputs) {
this.clearValues(inputs);
var additional = 1;
for (var l = 0; l < this.w.length; l++) {
for (var i = 0; i < this.values[l].length; i++)
for (var n = 0; n < this.w[l][i].length; n++)
this.values[l + 1][n] += this.values[l][i] * +this.w[l][i][n];
var i = this.values[l].length;
for (var n = 0; n < this.w[l][i].length; n++)
this.values[l + 1][n] += +this.w[l][i][n];
for (var n = 0; n < this.w[l][0].length; n++)
this.values[l + 1][n] = this.activation(this.values[l + 1][n]);
}
return this.values;
}
*train(inputs, expected) {
var self = this;
var sigmoid = v => 1 / (1 + Math.exp(-v));
var dx = x => sigmoid(x) * (1 - sigmoid(x)); //sigmoid
this.run(inputs);
var weights_delta = 0;
function *correctWeights(layer) {
var actual = self.values[layer];
var error =
layer == self.values.length - 1 // last layer
? actual.map((val, i) => val - expected[i])
: actual.map((_, i) =>
self.values[layer+1].map((_, j) => {
console.log(actual, self.w[layer][i][j], layer, i,j );
return self.w[layer][i][j] * weights_delta[j];
})
.reduce((a, b) => a + b)
);
weights_delta = error.map((e, i) => e * dx(actual[i]));
for(var i=0;i<error.length;i++)
yield {layer, i, error:error[i]};
var prevVal = [...self.values[layer - 1], 1];
var lastWeights = self.w[layer - 1];
lastWeights.forEach((v, i) =>
v.forEach((k, j) => {
lastWeights[i][j] -= prevVal[i] * weights_delta[j] * self.learningRate;
})
);
for(var i=0;i<weights_delta.length;i++)
yield {layer, i, weights_delta:weights_delta[i]};
};
for (var i = this.values.length - 1; i > 0; i--)
for (var value of correctWeights(i))
yield value;
this.run(inputs);
}
}
Vue.component("plot", {
props: ["f", "name", "highlight"],
template: '<svg :class="{selected: highlight}"></svg>',
mounted() {
var data = [];
for (var i = -1; i < 1; i += 0.01) data.push([i, this.f(i)]);
var lineFunction = d3.svg
.line()
.x(function(d) {
return 50 + 50 * d[0];
})
.y(function(d) {
return 50 - 50 * d[1];
})
.interpolate("linear");
var cont = d3.select(this.$el);
var lineGraph = cont.append("path").attr("d", lineFunction(data));
}
});
var layers = [2, 2, 1];
var inputs = [0, 1];
var app = new Vue({
el: "#app",
data: {
layers: layers,
perceptron: new Perceptron(layers, inputs, 0.1),
trainGenerator: null,
mounted: false,
compact: false,
learningRate: 0.1,
errors: [],
weights_delta: [],
selectedActivation: null,
activations: {
linear: {
f: v => Math.min(1, Math.max(0, v+0.5)),
name: "Linear"
},
step: {
f: v => (v >= 0 ? 1 : 0),
name: "Step"
},
sigmoid: {
f: v => 1 / (1 + Math.exp(-3*v)),
name: "Sigmoid"
}
}
},
computed: {
canTrain() {
return (
!this.selectedActivation ||
(this.selectedActivation && this.selectedActivation.name !== "Step")
);
},
weights() {
return this.perceptron.w;
},
layerValues() {
return this.perceptron.values;
},
neuronIndices() {
var acc = 0;
return this.perceptron.l.map(a => (acc += a) - a);
},
connections() {
if (!this.mounted) return [];
var additional = 1; // or 0
var neurons = this.$refs.neuron;
neurons = neurons.sort((a, b) => {
var l = a.dataset.layer - b.dataset.layer;
if (l == 0) return a.dataset.index - b.dataset.index;
return l;
});
var positions = neurons.map(n => this.neuronPosition(n));
var lines = [];
var offset = 0;
for (var l = 0; l < this.perceptron.l.length - 1; l++) {
//for each layer
let count = this.perceptron.l[l] + additional;
offset += count;
for (
var j = 0;
j < this.perceptron.l[l + 1];
j++ // next layer neurons
)
for (var i = 0; i < this.perceptron.l[l] + additional; i++) {
// current layer neurons
var coords = [
...positions[i + offset - count],
...positions[j + offset]
];
var t = j % 2 == 0 ? 0.3 : 0.6;
var center = [];
center[0] = coords[0] + t * (coords[2] - coords[0]);
center[1] = coords[1] + t * (coords[3] - coords[1]);
lines.push({ center: center, path: coords });
}
}
return lines;
}
},
watch: {
compact() {
this.update();
this.run();
}
},
methods: {
update() {
this.mounted = false;
requestAnimationFrame(() => {
this.mounted = true;
this.$forceUpdate();
});
},
setLayers(val) {
this.layers = val
.split(",")
.map(v => parseInt(v))
.filter(v => Number.isInteger(v))
.filter(v => v > 0);
this.recreate();
},
setLearningRate(val) {
this.learningRate = val;
this.perceptron.learningRate = val;
},
recreate() {
var inputs = new Array(this.layers[0]).fill(0);
this.perceptron = new Perceptron(this.layers, inputs, this.learningRate);
this.update();
},
insertLayout(i) {
this.perceptron.l.splice(i + 1, 0, 2);
this.perceptron.initWeights();
this.update();
},
removeLayout(i) {
this.perceptron.l.splice(i, 1);
this.perceptron.initWeights();
this.update();
},
detachNeuron(i) {
this.layers[i]--;
this.perceptron.initWeights();
this.recreate();
},
appendNeuron(i) {
this.layers[i]++;
this.perceptron.initWeights();
this.recreate();
},
setActive(act) {
this.selectedActivation = act;
this.perceptron.activation = act.f;
this.run();
},
neuronPosition(neuron) {
var n = $(neuron);
var p1 = n.position();
var p2 = n.parent().position();
return [
p1.left + p2.left + n.width() / 2,
p1.top + p2.top + n.height() / 2
];
},
wInputPosition(a, b, c) {
var index = 0;
var additional = 1;
for (var i = 0; i < a; i++)
index += (this.perceptron.l[i] + additional) * this.perceptron.l[i + 1];
for (var i = 0; i < c; i++) index += this.perceptron.l[a] + additional;
index += b;
if (this.connections[index]) {
var c = this.connections[index].center;
return { left: c[0] + "px", top: c[1] + "px" };
}
},
pathData(path) {
var p = path;
return `M ${p[0]} ${p[1]} L ${p[2]} ${p[3]}`;
},
run() {
this.layerValues = this.perceptron.run(this.layerValues[0]);
},
cancelTrain(){
this.trainGenerator = null;
this.errors = [];
this.weights_delta = [];
},
stepTrain(){
var value = this.trainGenerator.next().value;
if(value == null)
this.cancelTrain();
else if(value.error !== undefined)
this.errors[value.layer+'_'+value.i] = value.error;
else if(value.weights_delta !== undefined)
this.weights_delta[value.layer+'_'+value.i] = value.weights_delta;
this.$forceUpdate();
},
trainStepByStep(){
var inputs = this.layerValues[0];
var expected = this.layerValues[this.layerValues.length - 1];
this.trainGenerator = this.perceptron.train(inputs, expected);
},
train() {
var inputs = this.layerValues[0];
var expected = this.layerValues[this.layerValues.length - 1];
var gen = this.perceptron.train(inputs, expected);
for (let state; state = gen.next().value;) {
// console.log('>>>', state);
}
}
},
mounted() {
this.setActive(this.activations.linear);
this.mounted = true;
}
});
$(window).on("resize", e => {
app.update();
});
app.run();
#app
header.pure-g
.activations.pure-u-2-5
plot(v-for="act in activations", @click.native="setActive(act)", :highlight="act==selectedActivation", :title="act.name", :f="act.f", :name="act.name")
.controls.pure-form.pure-u-3-5
label(for="compact") Compact
input#compact(type="checkbox", v-model="compact")
label
input.pure-u-1(title="Layers", :value="layers" @change="setLayers($event.target.value)")
label
input.pure-u-1(title="Learning rate", :value="learningRate", @change="setLearningRate($event.target.value)")
label
button.pure-button(@click="train") Train
button.pure-button(@click="trainStepByStep") Train step-by-step
button.pure-button(@click="stepTrain", :disabled="trainGenerator==null") Step
.net
.layer(v-for="(nums,layer) in perceptron.l")
//layer-handlers(v-if="i<(perceptron.l.length-1)")
//button(@click="removeLayout(i)", v-if="i>0") -
//button(@click="insertLayout(i)") +
.neuron(v-for="n in nums", ref="neuron", :data-layer="layer", :data-index="n", :class="{compact:compact}")
.hint(v-if="!compact")
.right(title="Error", v-if="errors[layer+'_'+(n-1)] !== undefined") {{errors[layer+'_'+(n-1)].toFixed(3)}}
.left(title="Weights delta", v-if="weights_delta[layer+'_'+(n-1)] !== undefined") {{weights_delta[layer+'_'+(n-1)].toFixed(3)}}
input(v-if="layer==0 || layer==(perceptron.l.length-1)",
v-model="layerValues[layer][n-1]"
@input="layer<(perceptron.l.length-1)?run():null")
span(v-else) {{layerValues[layer]?layerValues[layer][n-1].toFixed(3):null}}
.neuron.additional(v-if="layer<perceptron.l.length-1", ref="neuron", :data-layer="layer", :data-index="nums+1", :class="{compact:compact}")
span +1
//.layer-handlers
//button(@click="detachNeuron(i)", v-if="nums>1") -
//button(@click="appendNeuron(i)") +
.weights(v-for="(layer,i) in weights", v-if="!compact")
.neur(v-for="(neur,j) in layer")
input(v-for="(w,k) in neur", v-model.number="neur[k]", @input="run", :style="wInputPosition(i,j,k)")
svg
g(v-for="connect in connections")
path(:d="pathData(connect.path)")
$layer-width: 200px
$neuron-size: 60px
$sh: 1px 1px 25px white
$header-height: 80px
=inputNum()
text-align: center
z-index: 4
border: 0
background: radial-gradient(ellipse at center, white 20%, transparent 70%)
color: #4c4
header
height: 100px
z-index: 6
position: fixed
top: 0
.activations
display: inline-block
vertical-align: top
svg
position: relative
width: 100px
height: $header-height*0.7
margin: $header-height*0.1
path
stroke: rgb(48, 136, 30)
stroke-width: 2
fill: none
&.selected
background: rgb(216, 255, 124)
.controls
vertical-align: top
display: flex
height: $header-height
box-sizing: border-box
text-align: center
white-space: nowrap
& > *
height: $header-height*0.4
flex: 1
.net
margin: auto
display: table
position: relative
.layer
display: table-cell
vertical-align: middle
width: $layer-width
min-width: $layer-width
text-align: center
position: relative
z-index: 2
padding: 30px 0
.neuron
display: block
margin: 12vh 0
margin-left: ($layer-width - $neuron-size)/2
border: 1px solid gray
width: $neuron-size
height: $neuron-size
border-radius: $neuron-size
line-height: $neuron-size
background: white
cursor: default
position: relative
&.additional
line-height: $neuron-size*0.7
width: $neuron-size*0.7
height: $neuron-size*0.7
position: relative
left: $layer-width*0.2
top: $layer-width*0.2
&.compact
margin-top: 4vh
margin-bottom: 4vh
.hint
position: absolute
top: -30px
left: 50%
color: #ff6e62
.right
position: absolute
left: 20px
.left
position: absolute
right: 20px
input
width: $neuron-size
height: $neuron-size
+inputNum()
font-size: 16px
span
color: #999
&:before
content: ''
.layer-handlers
position: absolute
left: 0
right: 0
margin: auto
&:first-child
top: 0
&:last-child
bottom: 0
button
border: 0
font-size: 18px
padding: 4px 8px
background: linear-gradient(to bottom, #ecff81, #aac29f)
color: white
.weights
input
position: absolute
+inputNum()
width: 50px
font-size: 15px
padding: 4px 6px
box-sizing: border-box
transform: translate(-50%,-50%)
svg
position: absolute
left: 0
top: 0
width: 100%
height: 100%
z-index: 1
path
stroke: #c2a6cf
stroke-width: 1px
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment