最小神经网络如何构建
结合涂鸦与代码来教你实现
闲来无事,想尝试做一个简单的最小神经网络,通过训练来让其识别一些东西。在这篇文章里,我将试着通过结合涂鸦与代码的方式来更为直观地向大家解释这一最小神经网络的构建过程。好了,让我们开始吧!
01
很多神经网络的术语都是从生物学的角度出发并赋予其意义的,所以让我们从最顶层的一番小科普开始:
大脑是十分复杂的,但总的来说可以将之分为几个基本的运作部分:刺激 - 输入 - 处理 - 输出
当然,刺激也可以通过内在产生(如感知或想法):
我们来看一下基本的、简化的大脑构造。下图右侧为一个大脑的横切面,其中四周灰色的部分为神经元层,白色的部分为轴突:
大脑令人惊讶之处其实就是“布线”
神经元是大脑计算的基本单位,它接收和整合来自其它神经元的化学信号。取决于多种因素,它要么什么都不做,要么产生电信号或动作电位,其反过来通过触发信号通知其它相连接的神经元:
做梦,记忆,想法,自我调节运动,反射以及你所想所做的一切都是通过这个过程产生的:数以百万计甚至数十亿的神经元以不同的速率发射并建立连接,从而产生不同的并行运行子系统, 并基于此构建了一个生物意义上的神经网络。
当然,上述是一种泛化和简化的解释。但现在我们可以描述一个最小的生物神经网络是什么了,以手指被图钉扎到而产生应激反应为例:皮肤被图钉扎到 - 感觉神经元 - 中间神经元 - 运动神经元 - 肌肉产生应激反应:
用图形的方式可以正式地描述为:
为了在当前这一阶段尽可能解释地简单,圆圈代表神经元,它们之间存在连接,连接线表示信息从左到右的向前移动。 第一个神经元目前正在发射,在此通过阴影来指代。 它会被赋予一个数字(当它正在发射时为1,否则为0)。 神经元之间的数字表示连接权重。
如果说上一张图代表了神经网络的一个时刻,而对神经网络更准确的描述将被分成时间段:
02
在构建神经网络之前,我们需要了解权重是如何影响神经元以及神经元是如何学习的。让我们从一只兔子(一只真实的实验兔子)和一个经典的调节实验来说起:
当受到无害的吹气时,像人类一样兔子通常会眨眼:
我们可以用一个简单的图表来建模这个行为:
就像之前的图表一样,上图显示的只是当兔子感应到空气的那一刻,在此我们将它编码为一个布尔值。在此基础上,现在我们计算第二个神经元是否基于权重值进行触发,如果权重为1,感觉神经元正在触发我们眨眼,如果低于1,则不眨眼。换句话说,我们的第二个神经元有一个阈值1。
让我们来引入另一个元素,当兔子接收到一个无害的听觉声音:
我们可以用以下方式模拟兔子缺乏兴趣:
可以看出,唯一不同的是权重现在是,所以兔子不会有眨眼,至少现在还没有。那么现在让我们训练我们的兔子根据混合刺激命令来眨眼(混合听觉声音与吹气):
重要的一点是,每个事件都发生在不同的时间点,因此图表看起来会是这样:
声音并没有产生任何影响,但是吹气仍然会引起兔子眨眼的反应,这里要注意的要点是我们通过权重乘以刺激(上图中红色)来表示这一点。
到这里,我们可以将在一串复杂行为中的“学习”,简化定义为神经网络中连接的神经元之间时间权重的递增变化。
为了训练我们的兔子,我们必须得重复这个过程:
下图对应的是初始状态和前3次实验:
请注意,在每次实验之后,声音的权重会增加(红色),这个数字在这一点上是任意的 - 我选择了0.30,但它可以是任何数字,甚至是负数。直到第三次实验,兔子都有着相同的行为。但是在第四次实验之后,一些新的事情发生了......我们发现了新的行为。
这次实验中,我们暂停了吹气,但兔子在听到声音后仍然眨了眼睛!对这一新行为的解释如下图:
也就是说,现在我们的兔子已经被训练出可以在听到声音后就眨眼了。
注:真实实验中,通过在几个星期或更长时间内进行大约60次实验即可引起兔子的上述反应
03
好了,现在让我们离开大脑和兔子的“生物世界”,而是将上述我们已经学到的一些知识应用于人工神经网络的构建。 我们要做的第一件事是去尝试定义一个简单的问题。
假设有一个4个按键的机器,如果你按下正确的按键则会给你提供食物。为此,目标对象需要学习去按哪个按键才能够获取食物:
我们可以通过以下图形方式来表示按下按键:
这一问题很容易在整体上理解,所以让我们看看所有可能的结果:
现实中,按下按键3就能获得食物
为了用代码创建一个神经网络,我们首先需要一个模型或图形来匹配它。对于目前的这一问题,我们可以用下图来对其生物表象进行松散地模拟:
这一神经网络所做的可以简单理解为接收一个输入(在此例中即为感知哪个按键被按下),然后通过权重修改上述输入,最后根据所添加的图层返回输出。这听起来可能有点复杂,那么就让我们来看看我们的模型是如何来表示按钮被按下的:
注意所有权重都是零,所以这一神经网络处于空白状态,但是其完全连接,就像新生儿一样
因此,我们将外部事件映射到神经网络输入层并计算输出,这个输出可能符合也可能不符合实际结果(此处即,能“吃鸡”的正确按钮)。但现在我们先忽略这一点,并开始用计算机友好的方式来编写这个问题的代码。
04
让我们从输入和权重开始(这里我将使用Javascript):
var inputs = [0,1,0,0];
var weights = [0,0,0,0];
// we can call these vectors for convenience.
下一步是创建一个函数,它接收这些输入和权重并计算输出;会是这样一个功能:
functionevaluateNeuralNetwork(inputVector, weightVector){
var result = 0;
inputVector.forEach(function(inputValue, weightIndex) {
layerValue = inputValue*weightVector[weightIndex];
result += layerValue;
});
return (result.toFixed(2));
}
// Might look complex, but all it does is multiply weight/input pairs and adds the result.
正如所料,如果我们运行上述代码,我们会得到与我们上图模型相同的结果...
evaluateNeuralNetwork(inputs, weights); // 0.00
代码实例可参考:https://codepen.io/k3no/pen/GWyJgP
下一步对我们神经网络的升级则是让其将自己的输出结果与真实情况进行比对。让我们先把这个特定的真实情况编码成一个变量:
为了检测是否不匹配(以及有多少不匹配),我们将添加一个错误函数:
Error = Reality - Neural Net Output
有了这两个部分之后,我们现在可以来判断我们的神经网络了,比对结果错误如下图:
以及更为重要的,比对结果正确图(可以吃鸡):
到这里,现在我们可以明确知道我们的神经网络模型在何种情况下输出的结果会不work(并且在数值上会差多少),这很好! 很好的原因是因为现在我们可以使用错误的反馈结果来指导我们的学习。这里,如果我们对我们的“错误函数”做一些小的改变,会让这一切显得更有意义,如下所示:
Error =Desired Output- Neural Net Output
这一微小但重要的改变在这里可以默认理解为:我们将使用以前观察到的结果与未来的表现来进行比对(换句话说,与“学习”结果进行比对,后面很快会讲到)。这一逻辑同样适用于现实生活,因为真理(正确的结果)往往是会重复显现的。所以,我们可以将上述策略理解为一种自我进化策略。
理解了以上逻辑之后,下面对于示例代码的修改就变得简单了,我们只需要添加一个新变量:
Error = var input = [0,0,1,0];
var weights = [0,0,0,0];
var desiredResult = 1;
以及新的函数:
function evaluateNeuralNetError(desired,actual) {
return (desired — actual);
}
// After evaluating both the Network and the Error we would get:
// "Neural Net output: 0.00 Error: 1"
代码实例可参考:https://codepen.io/k3no/pen/dvaGpX
好了,到这里,对于我们之前的讲解做一下友情回顾:我们从定义一个问题开始,然后针对其以神经元生物网络的形式建立一个简单的模型。接着,我们将这一模型的输出结果与真实结果/期望结果做比对,并通过某种方式来修正错误的比对结果。这一过程对于计算机和人类来说即为学习。
05
那么如何来训练一个神经网络呢?
无论是对于生物神经网络还是人工智能来说,“学习”的基本原则都可以理解为基于重复实验与学习算法。对于这两个关键词,我们来一一解决。让我们先从学习算法开始。
本质上,学习算法可以理解为在一次经历之后神经元的物理和化学特性所发生的变化:
在代码和我们的模型中,学习算法可以简单理解为我们将随着时间的推移而改变某些东西。秉承着简单易懂的原则,让我们先添加一个变量:
var learningRate = 0.20;
// bigger rate,bigger faster learnings : )
我们将改变什么?与之前兔子的例子一样,我们将改变权重,特别是我们想要的结果的权重:
如何对这样一个算法进行编程不同人会有不同的选择。为简单起见,我只是增加了学习速度的权重,在这里它是一个函数:
functionlearn(inputVector, weightVector) {
weightVector.forEach(function(weight, index, weights) {
if (inputVector[index] > 0) {
weights[index] = weight + learningRate;
}
});
}
使用这个学习函数,在每个学习周期(或实验)前后,都会将我们的学习速率添加到活动神经元的权重向量中,其结果如下:
// Original weight vector: [0,0,0,0]
// Neural Net output: 0.00 Error: 1
learn(input, weights);
// New Weight vector: [0,0.20,0,0]
// Neural Net output: 0.20 Error: 0.8
// If it is not apparently obvious, a Neural Net output closer to 1
// (chicken dinner) is what we want, so we are heading in the right
// direction
代码实例可参考:http://codepen.io/k3no/pen/qrJoXO
好了,我们正朝着正确的方向前进,现在我们需要做的最后一步就是重复实验。
这不难,本质上我们只是去一遍又一遍地做一些事情,而在代码中我们只是指定了实验的次数:
var trials = 6;
并根据我们所定义的实验次数,将我们的学习函数应用于我们的神经网络中,即这一训练函数为:
functiontrain(trials) {
for (i = 0; i < trials; i++) {
neuralNetResult = evaluateNeuralNetwork(input, weights);
learn(input, weights);
}
}
我们的最终读数为:
Neural Net output: 0.00 Error: 1.00 Weight Vector: [0,0,0,0]
Neural Net output: 0.20 Error: 0.80 Weight Vector: [0,0,0.2,0]
Neural Net output: 0.40 Error: 0.60 Weight Vector: [0,0,0.4,0]
Neural Net output: 0.60 Error: 0.40 Weight Vector: [0,0,0.6,0]
Neural Net output: 0.80 Error: 0.20 Weight Vector: [0,0,0.8,0]
Neural Net output: 1.00 Error: 0.00 Weight Vector: [0,0,1,0]
// Chicken Dinner !
代码实例可参考:https://codepen.io/k3no/pen/dvBZLe?editors=0012
我们现在得到一个权重向量,只有当输入向量与真实结果(第三个按钮向下)一致时,才会导致输出结果1(吃鸡)的产生。
06
我们刚刚建立的这个东西能带给我们什么?
在这一特定的案例中,我们的神经网络(在被训练之后)可以识别或区分输入之间的差异,并告诉你哪一个输入会产生一个所希望的输出结果(当然,我们仍需要为特定的情况进行编程):
除此之外,我们得到了一个比例模型,玩具或者说是学习工具,可以让我们进一步了解机器学习,神经网络和人工智能。
EECS圈
新工科
不只是口号
欢迎勾搭
领取专属 10元无门槛券
私享最新 技术干货