Created
November 29, 2017 12:50
-
-
Save Ni55aN/53e412fb9d5f7607dc8e1b1d22ef456e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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)") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$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