greataptic
Version:
A simplistic neural network library.
314 lines (247 loc) • 9.4 kB
Markdown
# Greataptic
**Just like life evolves, neural networks do too!**
**...and neural network libraries too!**
Greataptic is a minimalistic, sequential-only, simple and flexible
Node.JS library that was created after the abandoning of
[Neataptic](https://github.com/wagenaartje/neataptic). It is probably
extensible (but lacks a proper extension/plugin system), and it does
have a simple and efficient genetic algorithm.
## Tutorials
### Quickstart
First, you create a simple neural network.
```js
const greataptic = require('greataptic');
let net = greataptic.sequential(3, [ {size: 9, post: 'sigmoid'}, {size: 6, post: 'sigmoid'}, {size: 2, post: 'sigmoid'} ]);
```
In this example, this network will return whether we should
go to the park, given whether it is raining, and the temperature
in Celsius.
To make our life easier, we can just define a function that returns
whether we should go to the park, given these parameters:
```js
function goToThePark(bRain, temperature, raw = false) {
let l = net.compute([+!bRain, +bRain, temperature]).data;
if (raw) return l;
return l[0] > l[1];
}
```
Now, all we have to do is to train it.
```js
net = net.train({
inputSet: [[1, 0, 20], [0, 1, 18], [1, 0, 40], [0, 1, 32], [0, 1, 42], [0, 1, -20], [1, 0, -10]],
expectedSet: [[1, 0], [0, 1], [0, 1], [1, 0], [0, 1], [0, 1], [0, 1]],
debug: true,
mutation: 20,
maxGens: 300,
elitism: 0.04
});
```
Once we have done training it, we can use our function
to our heart's desire!
```js
> goToThePark(false, 20)
true
> goToThePark(false, -20)
false
> goToThePark(true, -20)
false
> goToThePark(true, 50)
false
> goToThePark(true, 15)
false
> goToThePark(true, 35)
false
> goToThePark(true, 32)
false
> goToThePark(false, 26)
true
> goToThePark(false, 31)
false
```
### Spiking Neural Networks
A _digital spiking_ neural network is a concept that is similar to the
ordinary _linear_ neural network, but with the added nuance that it can
vary **temporally**. It sacrifices the bloody scalarity into 1's and 0's,
but in turn it will likely not have the same result for the same input on
every single activation.
Basically, every neuron has a threshold and an accumulator. If this accumulator
reaches a specific threshold, it can release a specified output signal, that
will add to (or subtract from) the accumulator of other neurons.
For example, let's say we want to create a random word from noise: the
stronger the signal from a specific letter, the earlier it comes in this word,
and if this signal is too weak, it will not appear at all.
First, we have to initialize our spiking neural network. To use spiking layers,
we must provide the `type: 'spiking'` to the object describing the layer in the network
construction utility `sequential`.
```js
const greataptic = require('greataptic');
// A few adjustable constants to make sure our word is the right size.
const signalThreshold = 0.6;
const noiseSize = 9;
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
let net = greataptic.sequential(5, [ {size: noiseSize, type: 'spiking'}, {size: 23, type: 'spiking'}, {size: alphabet.length, type: 'spiking', post: 'sigmoid'} ]);
```
Now we can make ourselves an utility function to make our lives easier - _"do what you want 'cause
a 'coder' is free! You're the programmer!"_
```js
function inputNoise(size) {
return new Array(size).fill(0).map(() => Math.random() * 4 - 2);
}
function genWord(input = inputNoise(noiseSize)) {
let letters = net.compute(input).data;
return letters
.map((l, i) => [alphabet[i], l])
.filter((e) => e[1] >= signalThreshold)
.sort((a, b) => b[1] - a[1])
.map((e) => e[0])
.join('');
}
```
At first, it will be fully random:
```js
> genWord()
'oxjkmie'
> genWord()
'boxjkmz'
> genWord()
'poxjkmiywe'
> genWord()
'plxjckmie'
> genWord()
'oxjkmiz'
> genWord()
'poxjkmfiye'
> genWord()
'oxjkmie'
> genWord()
'oxjkmiz'
> genWord()
'boxjkmz'
> genWord()
'lxkmie'
> genWord()
'plxjckmie'
```
Since it is a completely generative approach, at this point, our best way to train this network
is most likely through a simple but efficient technique called...
### Generative Adversarial Network (GAN)
Remember our previous code? We can upgrade it using GANs.
In a GAN, there are two neural networks opposing each other:
the **generator**, which takes in a random noise vector and
outputs what we want (well, should), and a **discriminator**,
that will tell the generator how 'true' its output looks. The
discriminator learns to differentiate between real and false
data using two sets (a set of actual, true data, and a set
of fake data generated by the generator in a previous iteration),
and the generator learns simultaneously given the rating the
discriminator gives to the generator.
Unlike other GAN libraries, this rating is computed through
a fitness callback function and fed into a genetic algorithm.
This way, the truer a generator network is, the better it will
rank out, which can end up breeding (and mutating) better,
robuster networks, a la Darwian.
So, how can we use this powerful technology?
First, scrap that `greataptic.sequential` function. We're
going to take a look at a _class_ called `greataptic.GAN`!
(And we can also use the English word array library to get
the real data our GAN will require.)
```js
// @sequential GET OFF MY SWAMP
// let net = greataptic.sequential(5, [ {size: noiseSize, type: 'spiking'}, {size: 23, type: 'spiking'}, {size: alphabet.length, type: 'spiking', post: 'sigmoid'} ]);
```
Did that? Good. Now we can begin writing the file.
Begin with the useful constants:
```js
const greataptic = require('greataptic');
const shuffle = require('shuffle-array');
const realWords = shuffle(require('an-array-of-english-words')).slice(0, 10000);
// A few adjustable constants to make sure our word is the right size.
const signalThreshold = 0.6;
const noiseSize = 15;
const netType = 'linear';
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
```
Take note of the fact we're back to good old linear neural networks. Spiking
neurons and generativity are concepts that don't seem to ring well together.
Now, declare some functions so you can encode and decode words into arrays:
```js
function encode(word) {
let bad = Math.min(-1, signalThreshold - 0.5);
let d = greataptic.layerTypes.sigmoid.process(greataptic.$vec(Array.from(alphabet).map((l) =>
word.indexOf(l) !== -1 ? (word.length - word.indexOf(word)) : bad
)));
return d;
}
function decode(letters) {
return letters
.map((l, i) => [alphabet[i], l])
.filter((e) => e[1] >= signalThreshold)
.sort((a, b) => b[1] - a[1])
.map((e) => e[0])
.join('');
}
```
Good! NOW comes the interesting part.
Initialize the GAN thingie, providing the right arguments. We'll look into those
once I finish the API documentation.
```js
console.log(' * [INFO] Encoding English word subset.');
const realEncodedWords = realWords.map(encode);
let gan = new greataptic.GAN({
size: {
output: alphabet.length,
noise: noiseSize
},
outputType: netType
});
```
I guess this is a bit more code than our previous examples, but hey,
I'm sure it's totally worth it!
Now you can begin training! Hooray!
```js
console.log(' * [INFO] Training.');
gan.trainAsync(realEncodedWords, {
maxGens: 80,
population: 100,
maxComparisonSize: 150,
fitnessQuota: 0.95,
mutation: 0.6,
elitism: 0.0625,
survivalRate: 0.15,
debug: true,
discriminatorTrainOptions: { fitnessQuota: 0.9 },
genCallback: (data) => {
console.log(`(${data.fake.map(decode).slice(0, 8).concat((data.fake.length <= 8) ? [] : ['...']).join(', ')})`);
}
}).then(() => {
function genWord() {
let letters = gan.generate();
return decode(letters);
}
for (let i = 1; i <= (isNaN(+process.argv[2]) ? 50 : +process.argv[2]); i++) {
console.log('>', genWord());
}
});
```
Also take note of the fact we are using the `.trainAsync` function, which is
supposedly faster.
This _might_ take a long time. We have profiled the application, but if it takes
too long for you to train the GAN, there is probably not a lot that can be done
at that point. I'm sorry.
```log
...
[Generation #1] Best fitness: 0 | Worst fitness: 0
(hvwycfixnzl, nhvrwikmc, ivowycxjgn, tzfeigmknv, fgheovkiwxc, hwznflkgto, vhegyksq, yfhwesgvkci, ...)
[Generation #2] Best fitness: 0.6310722878538669 | Worst fitness: 0.06214089501871067
(hoefclxv, vhyfwglnm, ivofwyqgxlth, ztfegvim, xkfzvohnjei, hozqnfwal, vgfhkei, hyesfvwicgqlpk, ...)
[Generation #3] Best fitness: 0.7720685810330974 | Worst fitness: 0.02534786537134085
(fvoehzcxukn, efmvdzcn, oigvyftxwnqjk, hxfeyloqz, mqhwfoisenz, hzftogpume, hcesfdt, exzbfmgwov, ...)
[Generation #4] Best fitness: 0.9099892193256698 | Worst fitness: 0.011222608262300893
(hcdest, tzmfgavdqne, fizvetgkmb, joemitwafzguh, uefxvckniat, efszwkoh, feucvzhdtonwxslq, fokezcjqtm, ...)
```
At first it won't seem to improve much, but hey, it's a GAN.
Once you're done, go Wild Words, cowboy! _.......yes? no? But, come on, that was a good pun!_
You know what else is good? This beautiful output:
```log
(duatzrjpvmyfe, uzdmaprtjlyc, jtudzmoyralpwb, uajtoylrdpzbwq, jdrpxyltfnusvz, jdaoupyftvlx, urtdzyapmjf, udjtaylrpmoz, ...)
```