10.2: Neural Networks: Perceptron Part 1 - The Nature of Code

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Hello welcome to Coding Challenge in this Coding Challenge I'm going to attempt to make a Perceptron this is part of a whole bunch of videos that I'm doing about neural-network-based learning and, but in this video I'm not kind of getting into too much about the whole broader landscape of everything history-wise and future wise about what's happening with neural network based learning in this example I just want to build with code the simplest model of an artificial neural network possible known as a Perceptron. Perceptron, uh, the concept of perceptron was invented by Frank Rosenblatt in 1957 at the Cornell Aeronautical Laboratory, there's a link here to the original paper which I will include in this video's description if you want to take a look at that, and of course you can always find more on the perceptron Wikipedia page. What is a Perceptron? Why am I even here talking about this? So ultimately I want to look at Artificial Intelligence, machine neural network based machine learning systems that are inspired by and modeled loosely off of the idea of the actual brain. The actual brain being this thing with like neurons and axons that connect other neurons and dendrites that receive input Actually, I don't know what I'm talking about just reading whatever is on this diagram Ultimately where I'm going to start building examples and show you examples of lots of these components that are all interconnected and inputs are coming in and and outputs are flowing out but I want to start with this idea of the perceptron being a model of a single neuron the simplest possible artificial neural network that we could build and this will serve as the building block for future examples that it will make in future videos. Okay, So let's come over to the white board for a second so if the idea of a perceptron is that there is a single neuron, call this a neuron. That neuron could have inputs let's say there are two inputs I'm going to call them X[0] X[1] so inputs zero and one. They come into the neuron some type of mathematical process happens in the neuron. And then, there is an output which I'll call Y or output. So these are inputs. And again this diagram it might looks familiar to you if you've watched some of my other videos because I often talk about this idea of. There is some amount of input so long list of inputs that go into some machine learning recipe that processes all those inputs and performs a task maybe it tries to classify or perform a regression but make some sort of output some sort of prediction so this is exactly what this Perceptron is designed to do. Okay here's the thing. In order for us to understand and look at all of the pieces of what's happening in here we need some scenario so let's come up with a scenario that we can use so let's say I have a two-dimensional space you could think of this this will be my canvas my window and what I'm going to do is I'm going to arbitrarily divide the space with some line. Some points will be on one side of the line and other points will be on another side of the line. So essentially what I want to do is use this perceptron for a classification problem. I want to say that these belong to you know Class A and these belong to Class B so and I'm going to use a supervised learning strategy. So for background not some of this you could go and watch session three of my intelligence and learning series where I do some other videos about classification and regression using a linear regression model there's a lot of crossover here but anyway you could stay here if you want I'm going to kind of talk through everything but the idea is I want to classify these so I want these inputs to be so x0 or any points right here like this X comma Y it's a little bit confusing the way I'm using it I don't I think I don't love this because I should really think it'd be the X is input zero the Y is input one right the X is input zero the Y is input 1 the output is Class A or Class B the Y so I'm using x and y in two different places and slightly different ways which is a bit confusing but hopefully we'll make sense as we continue to go along here okay so but I actually want to say instead of instead of A and B what I actually want to say is plus one and negative one so the idea is that my perceptron is going to output a plus one if the point belongs to group A and it's going to output a negative one if the point belongs to group B. So how does – and we're going to use the technique known as supervised learning and what supervised learning involves is I am going to ask the perceptron to say here is some input give me a guess but I know the correct answer I'm going to give the perceptron a point that I know should be in A and it's going to guess either A or B if it guesses A I'm going to say great job perceptron you keep on going! If it guesses B I'm going to say perceptron you made a mistake let's tweak something about your algorithm to try to get you to the correct answer and this tweaking is a process known as gradient descent and it's something I've also covered in a couple different videos that I will also link to down here where to go through it as I get through here okay so that's the supervised learning process. Okay so what is the actual algorithm here, what happens here inside the neuron? So here there's here's the missing piece there's a few different missing pieces that I'm get to over time I was stepping on some of these you can think of as connections the inputs flow into this neuron but our weighted weighted weighted as they flow in so each one of these connections has a weight let me say W0 W1 so these inputs are weighted and what the perceptron does its algorithm is to create a sum of all of the inputs multiplied by the weights that sum is X input times sorry input 0 times weight 0 plus input 1 times weight 1. Now in this case the perceptron only has two inputs so this is a very easy formula to write as you're going to see as I get into future videos you might realize like oh there's a hundred inputs or a thousand inputs or a hundred thousand inputs but this same formula is always going to apply. A weighted sum of all the inputs input 0 times weight 0 input 1 times weight 1 add them all up together so that step one is the sum. Step two before so you could say like okay we'll take that and that's the output but this isn't the output step 2 is something called an activation function. Activation function. And this is a key concept in neural network based machine learning as I get into future videos we're going to see there's a variety of different kinds of activation functions and why might you use this one or this one and what do they do and why – but typically what an activation function does is it allows you to conform the output to some desired range and do thing and another way actually another way to think about it is if you think about that idea of the brain like you can think it does the does the neuron fire and continue to send its data along, or does it not? So what happens as the data comes out of that neuron? We're going to use in this particular example we're going to use a very very simple activation function you could think ok well I only want two outputs I want a plus one or a negative one. How could I take any number I can take any number and convert that number into plus one or negative one? How would I do that? How about a function called sign? Take the sign of any number n. If that number n is positive then I get a plus one, if that number n is negative then I get a negative one, okay? So that's the activation function this is the whole process, it's often referred to as feed-forward. The inputs come in they get multiplied by the weights they get added together and then that weighted sum gets passed through an activation function and then that activation function gives us a plus one or negative one should the neuron fire or not fire and that gets sent out and that's the output ok I'm back from I was just checking a live chat that's going on if you're watching this in archives I live chat doesn't exist but there are two important questions before I move on number one is what do you do with 0? I don't know we could just pick we could just arbitrarily right now let's just say this is greater than or equal so 0 will will consider +1 I mean in the case this is just like a toy example just to demonstrate the idea it's a building block you know I don't know how meaningful it is to be able to like classify points into these state space or a line in your app but but so it just wanted to make an arbitrary determination for that if it's on the line you know is it above or below I'll pick one. The other question that was asked is well how do you pick these weights? So this is the essential question and this is where I have to get to. So the idea is that what we – through the supervised learning process we want to search for – we're basically doing the search to find the optimal weights the optimal weight values that will get the best results, the results with the least amount of errors and so to start we have to pick something to start. In this case we could pick random values we could just start with the weight at zero that could be problematic so there's different ways this is a big topic in the field of machine learning when you start a neural network based system how do you how do you initialize the weights? Randomly? What distribution of random numbers you do some other kind of like learning process that gets to like a good starting point for the weights? That's a big topic of discussion and research but for us I'm going to pick random weights and start to tweak them okay so there's a lot more pieces of this still but I think I'm going to go and start writing some code and we'll come back to pieces that we're missing I'm going to do this in Processing which is a Java based programming language and environment you can find out more about it at processing.org I will also release a JavaScript version of this that you can run in the browser so check this video description for links to both source codes after it's over. Okay so what I want to do, I want to create a perceptron class. So we can see here by the way I have this slide here this is the same algorithm I just talked through let's just make sure we have it right the algorithm is for every input multiply input by the corresponding weight. Sum all the weighting – weighted inputs and then compute the output based on that sum passed through an activation function, the sign of the sum. So we can see we can think of like this could be the point at 12 comma 4 and these could be the weights of the perceptron so what I'm going to do for this particular perceptron is I am going to create an array to store all of those weights and I'm going to say it's an array with two elements in it and in the perceptron constructor I'm going to loop through all of the weights and give them a random value between negative 1 and 1 so whoops you don't say void with a constructor I don't remember how to program in Java based languages okay so this is the constructor and what I want to do in the constructor is initialize the weights randomly. Okay now what are some things the perceptron should do? Well I – one of the things it should do is it should be able to receive inputs and then compute a guess, an output we'll call that a guess okay? So let's write a function I'm going to call it guess and it should return an integer plus one or negative one and it should receive inputs which could also be in the form of an array. Now I could if I wanted to because the simplicity of the example I could have done something like float w0 and float w1 I could just sort of have individual variables for the weights instead of an array but the nice thing about doing this way is this is more flexible that this we could happen if we reuse this code later with we can adjust the number of inputs and that sort of things within our array okay so first thing I need to do is compute that weighted sum so I'm going to create a variable called sum and initialize it at zero then I'm going to loop through all of the weights and I'm going to say sum plus equal – what do I want to do? The sum of all the inputs multiplied by their corresponding weights, so inputs index i times weights index i, so this is now that weighted sum I say that second step start with a summit zero loop through and multiply all the inputs by the weights. Then what I'm going to do is I am going to return – ah! I need to get the – so then I'm going to say the output is sign of the sum so it doesn't know what sign is there probably is like a Java based function I could call automatically but let's just write our own up at the top of this code here I'm going to write a function I'm going to say int sign and it gets I guess I could say it gets any float I'm going to say if n is greater than or equal to zero return a positive one otherwise return a negative one so this is just that this is the so I could I could write here as a comment this is the activation function I could call it activate or something instead the activation function is a function that receives some value if it's greater than zero positive one if it's zero negative one so no matter what number goes in to the what other inputs come in whatever that's weighted sum is no matter what the only thing this perceptron can ever output is a 1 or negative 1 so and then I can say return output so now I have basically if I kind of give myself – help! I really want, I guess this is it, I really want to see the whole thing this is if I have all the code for the almost all the code for the perceptron a perceptron has a bunch of weights it initializes the weights randomly and it can perform a guess by receiving all the inputs doing a weighted sum that's passing through the activation function. Ok so now if I were to just create something arbitrary just to sort of test if this is working I'm going to say float inputs equals I'm going to just create some random values like negative 1 0.5 and I'm going to say print line Oh first I'm going to say I'm going to have a perceptron I'll just call it P for perceptron P equals a new a new perceptron and then I can say P guest inputs and I can say output I can say guess and I can say what's wrong here what doesn't it not like oh so this should be sorry that should syntax wise that should these are the input that's an array and I should say sorry a print line guess so if I run this – how come I can't – here we go if I run this we should see, oh it outputted a 1, let's run it again I got a 1 run it again eventually I should be able to to run it a bunch of times and I got a negative 1 negative 1 okay so this I believe is working the system works I have a perceptron object I could feed-in inputs and I can get – make a guess. Okay so we have the overall structure now for the perceptron and it works but we need some we need to do more so here's the thing we need to create an if I had an actual data set if I were to try to classify flowers and these were sunflowers and these were days and this x-axis was like petal length and this was sepal length or something I could use a real data set here I'm going to do something really phony-baloney I think I'm going to like kind of almost be really ridiculous about it which is that I'm going to say I'm going to actually just say that anything do like do I really want to do this I'm going to do it this way let's do anything that is let's just consider the line y equals x anything that's above y equals x is a plus 1 anything that's below y equals x should be a negative 1 so I want to create a known data set a known data set that I could use to train the perceptron so let's do that really quickly what I'm going to do is, let me think about this, I'm going to – there's so many different ways I could do this whoops sorry it got stuck in my eye, uh, so many different ways I could do this I'm going to do I'm going to make a tab called training and I'm going to make a class called point and the point is actually just an input array that has three values in it no no no no let me think about this let me actually have the point have an X and a Y and also a I don't want to call it class a label we'll just call it a label okay so if I make a new point when I make a new point I'm going to say x equals a random with y equals random height – am I going to run into trouble without just using the pixel coordinates? Let's try just using the pixel coordinates, I don't know if that's going to be a problem. And then the label I could say if X is greater than Y the label is one else the label is negative one right that should give me that should give me everything about I don't know what's above what's below so let's do that and then let's do let's write a little function called like show where I'm going to say stroke zero and I'm going to say if label equals one fill 255 else fill a zero and then I'm going to draw a little ellipse at X comma Y a small ellipse okay so what I'm going to do is I'm going to make an array of points I'm going to make a hundred of them and I'm going to initialize them and I'm going to say points index i is a new point and then I let's make the size a little bit bigger 500 comma 300 then I'm going to do a background 255 and for every point in points this is an enhanced loop in Java for every point in points I'm going to say p.show() so if I run this we can see yeah I mean let's make this a square so it looks a little less weird and I can also draw just to sort of like see correctly I'll draw a line from 0 0 to width comma height so you can see I picked all these points half of them are on this side and half of them are on this side alright so now that I have all these points and I can see them correctly categorized this is my known training data so what I need to do process wise is I need to take all of my known training data one at a time I need to pass it in ask the perceptron to give me a guess. Is it in one is it in one or is it in negative one? And then I need to do something based on whether it's correct or incorrect. What is it that I need to do? Okay let's establish something there are still some missing pieces here I need to that I – that I need to add but I'm going to keep going let's talk about training let's talk about supervised learning. Here's what I need to do I need to take all of that known data. I'm going to take each and every piece of known data I'm going to take the X&Y point and I'm going to pass it in, again it's going to do the weighted sum it's going to it's going to pass through the activation function and it's going to guess plus 1 or negative fun – one. Plus fun or negative fun! Are you having we're having some negative fun right now I'm pretty sure. So this is going to give me a guess but I also have the answer, right? So I have both the perceptron's guess and I have the answer. If I have both of those things I can compute something known as the error. The error I can think of as let's let's say the answer, I always get this wrong backwards one way or the other but it's the difference between the correct value and the incorrect sorry – between the correct value and the guess right because if it's if the guess is a plus one and the answer is a plus one what's that error? 1 minus 1 that error is 0 and I actually have a little bit of a diagram here over here where you know you can sort of see these are the only possible correct answers negative 1 or plus 1 so there's only four possibilities if it's supposed to be a negative one I could guess a negative one or a plus one if it's supposed to be a plus one I could guess a negative one or a plus one so these are all the possible errors the error is either zero or the error is negative 2 or positive 2 or 0 so this is a start this is a good starting point I need to have this error the idea here oh come back let me come back to the whiteboard okay so remember what I'm trying to do is find the optimal weights so ultimately what I want to do is I want to figure out if I want to say well the weight should equal itself plus some change in weight. I want to adjust the weight if there's a mistake I want to make a tweak I want to like make the weight a little bit higher or a little bit lower right because maybe my weighted sum got me below negative 1 but it should be a plus 1 if I make that weight higher maybe that will push the output up to positive so the issue becomes how do I calculate that Delta weight? How should this weight change based on – how should the weight change for weight zero for weight one and if there were many many many more weights? So the way that this is calculated with it is with a process called gradient descent and I have a couple videos where I go through this in in pretty large detail one way of thinking about this which I'll kind of duplicate here in this video is this really relates back to a lot of my steering examples so I have all these steering examples where I have a vehicle that has a given velocity and it's seeking a given target so this vehicle has a velocity and it also has a desired velocity right because if it should be going towards that target its desired velocity is to go towards that target so you can think of just steering formula if you go back to that Craig Reynolds steering formula the steering formula equals the difference between the desired velocity the way that I want to go and the current velocity which is kind of like my guess and if I get this a steering formula if I get the steering function if I get this steering vector and I add it to the velocity it's going to cause me to turn and go towards that target so essentially that's what I want to do here this steering vector is the error the desired velocity is the answer that's where I want to go the velocity is my guess that's where I'm going right now I want to steer in the direction of the error so Delta weight the Delta weight is actually equal to the error multiplied by the input so it's filtered through the it but what's that error filtered through the input that's how I change the weight itself so that's the process that I'm going to do over and over again and I have a slide here that I think will walk through that and it looks I'm in the wrong keyboard here so this is the process this is the supervised learning algorithm. Provide the perceptron with inputs for which there is a known answer and ask the perceptron to guess. Okay the perceptron guessed what's the error? Is it right or wrong? Is the error zero is it, is it two, is it negative two? Adjust the weights according to the error and go back and do it again and again and again and this is the formula the weight is changed according to the air multiplied by the input and there something called learning rate which I'll get to in a second so let's see now I've kind of explained that in pieces let me see if I can now add that to the code so I'm going to here whoops I'm going to now create a function in the perceptron and I'm not going to call it guess I'm going to call it train. So this is going to receive some inputs and a target. Right the difference between the guess is the guess something like oh I just want to receive these inputs and provide a guess with training I want to receive the inputs and the known answer so I can adjust the weights accordingly okay so the first thing actually that I should do is just get the guess which is actually guess with those inputs so since I already have a function that does the guess I can ask for the guess from that guess then what I want to do is get the error the error equals the error equals the target minus the guess that's the error so now that I've done that what I can do is I can go through all of the weights and say each weight should change according to that error multiplied by its corresponding input so this is that particular algorithm this is tune all the weights. Okay so this is like basically supervised learning says put the data in, get the result, if the result is right just move on move along nothing to see here. If the result is wrong twist some dials in here to try to get it closer to the correct answer and do it again and again and over and over again here keep twisting dials eventually to find that optimal result now there's something important here though. If I go back to this steering example you could think about okay so this is the vehicle it's going in this direction it should seek the target it knows what the error is the error is the difference between the way it should go and its current velocity how much should it steer? If I steer a lot I could actually like overshoot and start going the wrong way in the other direction but if I steer just a little bit maybe I'm going closer – but I'm, but you realize that we're going to doing this with lots and lots of data so one thing that's actually optimal here is not to steer the full amount all the way according to the error but just some percentage of the way and that percentage is referred to it's a key concept here and it's called where's a learning rate. So what I would actually do here is say that Delta weight what's this plus 1 here is the error times the input multiplied also by a learning rate. So that's a key concept here so let's add that into our code if I come back over here I'm at the perceptron is going to have a variable called LR for learning rate I'm just going to say 0.1 so now I'm going to say also multiplied by learning rate. So there we go now this is going to adjust all of the weights so we should be able to if I go back to here I should be able to now train the perceptron. So let's go here and say for all of the points oh I did something terrible though I was doing some awful stuff here which is not I used P for perceptron and then I'm using a local variable P for point so let's call this PT for points let's actually just call this I'm going to call perceptron I'm going to call this like the brain probably like a bad variable name but at least so it says something more than P so for every point what I want to do is I want to say brain.train the point the inputs associated with that point, and the target pass in the target associated with that point right? The point oh it has an X and a Y and a label so uh-huh so the target is actually the label and can I do this I want to make something called inputs which is an array which just has point.x and point.y in it. Is Java going to let me write that? I think so so this is what I want to do I want to train the perceptron I want to send in every point as input so X the x and a y make the zero on the one input and then I want to send it into the train function with the label which is the known answer so if I do that okay so in theory it's training and it's doing this I can't see anything so now I need some sort of way of tracking how well it's doing. This is going to be tricky but I have an idea for how to do this. Somebody the chat just pointed out that you know why am I doing the uh, why am I doing the same loop twice yeah that's kind of unnecessary here but I'm really just trying to separate out different parts of the code ultimately I probably don't need this original loop but so there's a couple things I do one is I could actually calculate the overall error like I could actually look and this wouldn't be such a bad idea and just sort of print that out I could I could calculate the total squared error and kind of evaluate how well it's doing but I want to just actually look at it visually for a second so I think what I want to do is let me see here what's a good way so I'm going to just actually say guess equals inputs brain.guess(inputs) and then I'm going to say if guess equals point.label let's just call this looks like target equals points points label just to have these in separate variables if guess equals target what I'm going to do is I'm going to draw um, I'm going to say I'm going to draw something that's green I'm going to say no stroke I'm going to draw an ellipse at X – like I just keep typing p5 – Y that's like a smaller size else fill red. Well everything became green instantly that can't be right all right let's not train it. Okay so we can see I guess it's just working better than I had imagined I'm trying to think visually if there's a better this is like okay so let me let me make the window bigger and let me make all of the points let me make all the points much bigger so it's a little easier to see and let's run this again okay so you can see without any training everything is wrong so I'm not if I add the Train function back in everything is correct now I guess it just like this is such a simple scenario it's like trained and worked so so quickly in like a matter of like one or two iterations me I am so me in the chat had an excellent suggestion which I could demonstrate the training process with a mouse click so I'm going to quickly do that so what I'm actually going to do is I'm going to take this training out here I'm going to take this whole loop and they'll just delete this line of code that actually does the training and I'm going to run it so now it's not training and you can see it's got every time it you know when I run this it's all wrong right but what I can do now is I can say void mousepressed and I can just run the training algorithm so now what I did is only when I click the mouse is it going to run through all the data and actually adjust the weights so now if I run this you can see you can see look at this it's got most wrong but it's got some right, it's got most right but some wrong and now if I click the mouse ah a few more correct but some more wrong click the mouse again OOP click the mouse again click the mouse again click the mouse again so you can see it's like changing and eventually now it's got everything right and so you can see how that learning process happens over time it only took five or six cycles. One other thing that I could do is just train one point at a time so I would draw everything but I could here I could say I could say int training index equals zero so what I'm going to do in draw now is I'm going to say point training equals points training index so I'm just going to take one point and train it off at one point okay and then what I'm going to do is I'm going to say training index plus plus and if training training index equals points.length like if I get to the end of the array I will just reset training index back equal to zero. So this now in the draw loop what did I get wrong here I'm just a spelling training wrong Oh double equals sorry so now in the draw loop it should be training one point at a time and you can see like it's kind of weird all sorts of weird flashy stuff is happening but over time it should eventually settle into at getting everything correct but as it makes these little adjustments it's going to get some things wrong and some things right and you can see now so again there's so many other ways you can think about visualizing the training and you could actually visualize the one thing you might try is actually visualize the perceptron itself like visualize the weights visualize the connection visualize the data flowing through it so I'll leave that to you creative people watching this video so I'm coming towards the end of this particular video and I think I need to do I'm going to give you some exercises and I think I'm going to do a follow-up one that kind of adds I'm gonna give you some exercises and then I'm going to come back and do another video where I add this these answers in but there's a couple things here one is I've got a serious problem a very serious problem with everything that I've done here let's just say I'm going to let's say that this space that I'm in is actually this. 0 0 forget about pixel space or anything there's a coordinate space where zero zero is in the middle right here and I want to categorize us data as above or below this line or maybe you know I want to use the same system but I want to categorize data as above or below this other line so I want to use a perceptron to categorize the same exact code to categorize both of these two scenarios well in one case in the orange case zero zero should be a plus one in the black case zero zero should be a negative one but notice something here if I'm actually feeding in 0 0 into this perceptron system no matter what the weights are the only thing I can ever get out of here is a 0 this is a problem because sometimes 0 0 is going to be above or below, sometimes it's going to be in class A sometimes it's going to be in class B that can't possibly be right and this is where this idea of a third – I'm going to go and get another color here this perceptron will actually not work or perform correctly with this genetic generic scenario other than with having something called a bias so I need another input, a third input into the perceptron that is always going to be a 1 input 0 is X input 1 is y and the bias is 1 and actually if you think about this this is really I'm really working through the same problem that's from my linear regression with gradient descent videos or what I'm trying to do is solve the formula I'm trying – neural networks are designed to generalize a function to solve a function and in this case, this very simplistic scenario it's actually just looking to solve the formula for this line y equals MX plus B and M being the slope is kind of like rise over run and B being the y-intercept so this weight the biased weight is really there to solve that y-intercept and these weights are really solving the slope the rise over run so that's what we're doing we're essentially doing linear regression with gradient descent again but just through this perceptron model so if you didn't watch those other videos you could go back and watch them to some connect all these systems so what I want you to do is I have an example here from the nature of codebook this is essentially the same code that I've been writing but it does two different things one is it visualizes what the brain thinks the current line is and also it adds that bias so if you're watching this video I'm going to release the code for this video this I get to part one of a perceptron coding challenge if you're watching this video you could go right to the next one and I'm going to add the bias in and maybe do a bit more with sort of visualizing what's going on here and have a generic formula for a line but what I might suggest you do is see if you can add those things yourself to this particular perceptron and then when I get to the end of the next video I'm going to talk about why this is such a limited system that can barely actually do anything meaningful in machine learning but it could be a building block for a much more complex system that can do a lot more interesting and powerful things so I hope you've got a sense of what a perceptron is what the algorithm for how a perceptron works is and how the feed-forward supervised learning training process of a model perceptron works because this exact building block this scenario is what I'm going to use in the future videos restart to build more complex neural networks ok thanks for watching and I look forward to your feedback and thoughts in the comments you
Info
Channel: The Coding Train
Views: 418,726
Rating: undefined out of 5
Keywords: programming, daniel shiffman, creative coding, coding challenge, tutorial, coding, challenges, coding train, the coding train, nature of code, artificial intelligence, itp nyu, neural network, intelligence creative coding, neural network artist, intelligence and learning, machine learning, machine learning art, linear regression, gradient descent, perceptron, perceptron processing, processing(java), java perceptron, machine learning processing, machine learning java
Id: ntKn5TPHHAk
Channel Id: undefined
Length: 44min 39sec (2679 seconds)
Published: Thu Jun 08 2017
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.