~ $ cd projects/first-network && cat README.md
My first neural network
In 1994, in my second year of BUP, I wrote a neural network in C that told two letters apart, drawn on a small grid. To learn how, I went to the university and, with help, searched for papers over Gopher. It was a school project, and I presented it to the whole class. To do it I carried the PC from home: the tower and its CRT screen.
This is the same idea again, in this site's own code: a grid of five cells by five, ten hidden nodes, one output for each letter, and backpropagation. Press a cell to ink it or clear it, and the network reads the drawing again.
It reads A, after 200 rounds of lessons.
| A | 97% |
|---|---|
| B | 3% |
It has been taught A and B, two hundred rounds. Each round shows it every letter twice, once as drawn and once with one cell wrong, so that it learns the letter and not the drawing. Tick more letters and it starts again with all of them. O, C and D are there to be confused. Forget everything leaves it guessing; teach 100 more rounds and it comes back.
Or teach it a letter of your own. Draw it, give it a name of up to three characters, and press remember this drawing. It joins the letters taught, and the network learns them all again from the start. Your letters stay in your browser, and × forgets one.
The code
This is the whole of the learning, as it runs on this page:
learn(input, target, rate) {
const values = this.forward(input);
const output = values[values.length - 1];
// Each output is to blame for its error,
// times the slope of the squash where it stands.
let blame = output.map((value, n) => {
return (value - target[n]) * value * (1 - value);
});
for (let layer = this.weights.length - 1; layer >= 0; layer -= 1) {
const below = [...values[layer], 1];
const nodes = this.weights[layer];
// A node below takes the blame of the nodes it feeds,
// by its weight on each: before those weights move.
const passed = values[layer].map((value, i) => {
let sum = 0;
for (let n = 0; n < nodes.length; n += 1) {
sum += nodes[n][i] * blame[n];
}
return sum * value * (1 - value);
});
for (let n = 0; n < nodes.length; n += 1) {
for (let i = 0; i < below.length; i += 1) {
nodes[n][i] -= rate * blame[n] * below[i];
}
}
blame = passed;
}
let error = 0;
for (let n = 0; n < output.length; n += 1) {
error += (output[n] - target[n]) ** 2;
}
return error / 2;
}
forward works the other way: every node adds up the values below it by its weights, plus a bias, and squashes the sum between 0 and 1. Then learn starts from the answer. Each output is to blame for how far it missed. The blame goes down a layer, shared out by the weights. Every weight moves a little against its share.
A robot, the same year
That same school year I did a robotics workshop at the Museu de la Ciència. We had to move a small robot, and in theory in plain C, with the moves written out by hand. I used what I knew about networks to simulate one instead, with the weights set by hand and no backpropagation, and it worked at once. They changed the problem: first find a light, then find it avoiding obstacles. It was enough to give the robot a feeling: aversion to obstacles. Then they asked for a maze, and told us the trick of keeping a hand on the right-hand wall. The robot got the feeling of needing to touch something on its right, and the change was almost instant. This part is memory. None of that code is here.