Neural Networks in your browser

A non-scary way to jump in on machine learning

Diego Pablos

Diego Pablos

Senior Software Engineer at Softvision
Diego Pablos has been tinkering with full stack development for over seven years and currently works as a Senior Software Engineer for Softvision's Web Community, since joining in 2018. At the time, he is part of a leading cosmetic retail brand’s maintenance team.
Diego Pablos

Latest posts by Diego Pablos

Machine Learning is a topic often brought up tied in with advanced research and with good reason: Back in the day when the first papers on automated learning were presented, it was only feasible to test them out with specialized equipment.

In fact, even after all the time that has passed since the fifties and a slight peak of research material in the early nineties, things were not much different when the web 2.0 came about.

It was not until a few years ago when the average computing power of everyday devices became capable of carrying out practical tests of machine learning, thus allowing for people outside of the realms of scientific or business research to dabble in and boost the potential uses.

With all this baggage and a few-too-many frameworks to keep up with, it is no wonder why for a certain profile of front-end oriented developers immersing themselves into machine learning concepts might sound at first a task too daunting to take on. I know because I am one of them. Let us work around it.

Before we start, I would like to say even though this article focuses on Neural Networks, it is important to keep in mind that Machine Learning encompasses a lot more. Neural Networks (NN) are just one of the many well-refined tools of the Deep Learning toolset, whereas Deep Learning is a subfamily of Machine Learning algorithms that rely on interpreting input data and “black-boxing” out the result as a group of probabilities. So, the onus of the programmer befalls mostly on how to turn an initial set of training data into something that our NN can interpret and give us an answer for, and to make sure those answers fit the things we know. Basically, we will be the training wheels keeping the course steady until the NN is able to ride on its own.

How does a Neural Network learn?

Neural Networks are, unsurprisingly, made out of Artificial Neurons (AN) which somewhat mimic the biological ones. A NN is going to be comprised of layers of ANs, whereas every AN from each single layer is going to be connected to every AN of the subsequent layer.


(A sample Neural Network)


Every single Artificial Neuron is comprised of:
– As many inputs as neurons in the previous layer.
– A single activator function whose sole role is to verify if the combination of input values are enough to let it “activate” and pass a value through.
– As many outputs as neurons in the following layer: these are going to be the inputs from their perspective.

– Each neuronal input has a variable weight value associated for which it multiplies the input value that was entered. These weights are initially set at random, but are subsequently adjusted by the learning algorithm. This is on WHERE the learning process is reflected.

Once a set of values are entered into the NN, the first layer is triggered, and the activator function for each one of the neurons runs using the inputted values. For those neurons whose final values clear the threshold, that value is sent through the outputs to the next layer. The process repeats until the final output is reached.

(An Artificial Neuron at work, source: https://becominghuman.ai/what-is-an-artificial-neuron-8b2e421ce42e)


Here is where the learning gets started:
Out of the several learning algorithms available for Neural Networks, Ours is going to use back-propagation, which is a supervised learning algorithm. This means we are going to need to feed it with training data first. That is, to say, a group of inputs with their corresponding correct outputs so it can adjust itself to produce those results.

Let’s build a simple, mostly pointless NN using JS.

Say we want to build a cheating NN that learns how to beat you at Rock-Paper-Scissors.

That’s right: Instead of just writing a simple conditional that returns which value beats which value, we are going to leave the “conditional” aspect to a full-blown NN. To illustrate what we are talking about, the following would be a terrible program that achieves the same results:

 

const ruleset = {‘scissors’: ‘rock’, ‘paper’:’scissors’, ‘rock’:’paper’},

playRPS = (humanPlay)=> ruleset[humanPlay];

playRPS(‘rock’)// the computer outputs ‘paper’ and beats you.

However, using a NN will help us see how the learning process works.
Where do we start from?

 

Defining the input and output layer

Neural Networks work with binary sets, so our inputs and outputs are going to be binary arrays, whose dimensions will match the amount of nodes in the input and output layer respectively.

RPS has three exclusive unique conditions for each player, one for each possible play. So, both the input layer -the player’s play of choice- and the output layer -the AI’s choice- are going to have three nodes, and the following combinations of the three nodes each one is going to represent a possible play, say:
Rock [1,0,0] // First node activated, Seconde node not activated, Third node not activated.
Paper [0,1,0] // First node not activated, Seconde node activated, Third node not activated.
Scissors [0,0,1] // First node not activated, Seconde not node activated, Third node activated.

// Little side note: The conditions don’t have to be exclusive. IF in order to decide whether to play [1] or not [0] a football match we have the initial possible conditions Day/Night [x,0,0], Dry/Wet[0,x,0], Summer/Winter[0,0,x], the input might end up looking like [1,1,1], and the most possible output would be [0], which is to say: Better not play the match if it’s on a wet winter night.

So, for example, if the NN is triggered with an input value of “Rock” -[1,0,0]-, we want the most statistically significant response to be [0,1,0]; “Paper”.

 

Backpropagation

Here is a not-entirely-accurate but easily understandable version of how backpropagation works:
With our training set, we tell the NN that when the input is [1,0,0], the answer should be [0,1,0].
When the unlearned NN does a trial run for that input value for the first time, the output layer returns an object that looks like this: [0.5489, 0.5781, 0.544].
These numbers represent how “positive” the NN is that each node from the three we have in the output layer should activate.

From our training set, it “knows” that the middle node should be active, while the other two nodes are not, because that is the answer that represents “paper.”

So what it does now knowing how “off” it was, is to take what the proper answer [0,1,0] and run it backwards from the output layer to the input layer, adjusting the weight values ever so slightly in a way that paves the road so next time it runs the final result is closer to this value. This is actually achieved through minimizing the error cost value, a concept you can find in a more in-depth article about NNs.

There is a reason why these adjustments are very subtle for each training pass: since there are multiple possible inputs and outputs but only one instance of weight for each input/output pair, adjusting all the weights for one particular desired answer will negatively affect the others. Ultimately, after multiple training runs, the NN should be able to adjust all the weights in a way that the NN returns a result that closely resembles beyond statistical insignificance the correct results.

Also, as you might reckon from this, the more layers and neurons there are, the more subtle nuances the NN is able to pick up.


Getting back to the code

Now that we have the ruleset and how to represent in binary sets the input/output values, we are going to need a library that allows us to create a NN. There are several options, but we are going with Synaptic.js, since it is the most simple one to use out of the box. [http://caza.la/synaptic]. The quickest way to use it in a browser is to just add their CDN script tag.


<script src=“https://cdnjs.cloudflare.com/ajax/libs/synaptic/1.1.4/synaptic.js”></script>


Here is what we can build with what we know so far. There are much better ways to write this, but I tried to go for understandability:


// What beats what

const ruleset = {‘scissors’: ‘rock’, ‘paper’:’scissors’, ‘rock’:’paper’};

// How the RPS values can represented as active/inactive nodes.
const binaryset = {‘rock’: [1,0,0], ‘paper’:[0,1,0], ‘scissors’:[0,0,1]};

 

Now, let’s create the actual NN with Synaptic:

const inputLayer = new synaptic.Layer(3);  // input layer with 3 neurons
const outputLayer = new synaptic.Layer(3); // output layer with 3 neurons

// We now indicate synaptic that these layers are connected in succession.
inputLayer.project(outputLayer);
// We create a new NN instance and indicate which layer is which.

const NN = new synaptic.Network({ input: inputLayer, output: outputLayer });

That is all it takes. Now, we are going to need a way to train it obviously.


Let’s build a function:

const train = (trainingset) => {
   for (let i = 0; i < trainingset.length; i++) {
       inputLayer.activate(trainingset[i][‘input’]);
       outputLayer.activate();
       outputLayer.propagate(0.4, trainingset[i][‘output’]);

}
}

 

Breaking down the training function:

For each input of the training set, we activate the NN through the input layer. We then activate the output layer to get the results. This is how you normally use an NN to produce results.

Now that we have its answer, we use the training set matching index output value to back-propagate the correct solution. This causes the weight values to be slightly adjusted to reduce the differences encountered from the original output. The more we run this process, the better adjusted to fit our expected solutions it becomes.

As an additional note, the first parameter of outputLayer.propagate function, that 0.4 value, is the learning rate we are choosing for the back-propagation.
It basically affects how much each training run affects the overall weights.
It is defined between 0 and 1 and the higher it is the fewer training runs it takes to see the effects of it, but you are risking losing nuance and precision. An overall value of 0.4 is good enough for most cases, but you’re free to experiment with it.

 

The training wheels

This last “train” function needs a training input to learn from. There aren’t many things to learn from rock paper scissors, so let’s build the full set from the get-go:

const trainingset = [
{‘input’: binaryset[‘rock’], ‘output’: binaryset[ruleset[‘rock’]]},
{‘input’: binaryset[‘paper’], ‘output’:binaryset[ruleset[‘paper’]]},
{‘input’: binaryset[‘scissors’], ‘output’: binaryset[ruleset[‘scissors’]]}

];

 

And, of course, let’s write our “play” function. As you might notice, there is barely any difference with the training one. The only thing that is missing is the back-propagation part.

const playRPS = (humanPlay) => {

inputLayer.activate(binaryset[humanPlay]);

return outputLayer.activate();

}

 

Putting the NN in motion

We can now take it for a test drive and have it learn from its mistakes. We’ll do an activation-training loop: that is, for every time we ask it to give us an output, we run one session with the training set and repeat the process multiple times while monitoring the results. Let’s see how it learns to respond to a human playin “rock” throughout 15 training sessions.

console.log(playRPS(‘rock’));
for(let i = 0; i < 15; i++){
train(trainingset);

console.log(playRPS(‘rock’));

}

 

These are the output values for every run:

 

As mentioned earlier, the answers come in the form of an array with the confidence values for which of the nodes should activate. At first, it’s only about .52 sure that the central node should be 1, without significant statistical difference towards the rest of the nodes being active as well. The more it learnt, the more significant the difference became between the confidence for the middle node to activate against the other ones.

Conclusion

It goes without saying that this barely scratches the surface on what can be achieved with an NN, in fact, it doesn’t even go as far as using a hidden layer in between, something almost all of the simplest NN do use. My hope was for this to be simple enough so anyone without a background in math, statistics or programming beyond the front-end can begin to grasp the usefulness and relative ease-of-use of Neural Networks, and then move onto better written and more insightful tutorials.

This has been loosely based on a tech talk I gave about a similar program I made that can be accessed here: [https://pedantic-sammet-892cfd.netlify.com/], a Rock-Paper-Scissors-Lizard-Spock playing NN which has more adaptive approach at learning and spreads the confidence values into die roll tiers, so it is not always impossible to win, but it becomes increasingly difficult. The code is available at [https://github.com/thelowend/rpsls-single_layer_NN/].

Share This Article


Diego Pablos

Diego Pablos

Senior Software Engineer at Softvision
Diego Pablos has been tinkering with full stack development for over seven years and currently works as a Senior Software Engineer for Softvision's Web Community, since joining in 2018. At the time, he is part of a leading cosmetic retail brand’s maintenance team.
Diego Pablos

Latest posts by Diego Pablos

No Comments

Post A Comment