神经网络是一门重要的机器学习技术。它是目前最为火热的研究方向–深度学习的基础。学习神经网络不仅可以让你掌握一门强大的机器学习方法,同时也可以更好地帮助你理解深度学习技术。
本文以一种简单的,循序的方式讲解神经网络。适合对神经网络了解不多的同学。本文对阅读没有一定的前提要求,但是懂一些机器学习基础会更好地帮助理解本文。
神经网络是一种模拟人脑的神经网络以期能够实现类人工智能的机器学习技术。人脑中的神经网络是一个非常复杂的组织。成人的大脑中估计有1000亿个神经元之多。 图1 人脑神经网络
一. 前言
让我们来看一个经典的神经网络。这是一个包含三个层次的神经网络。红色的是输入层,绿色的是输出层,紫色的是中间层(也叫隐藏层)。输入层有3个输入单元,隐藏层有4个单元,输出层有2个单元。后文中,我们统一使用这种颜色来表达神经网络的结构。 图2 神经网络结构图 在开始介绍前,有一些知识可以先记在心里:
设计一个神经网络时,输入层与输出层的节点数往往是固定的,中间层则可以自由指定; 神经网络结构图中的拓扑与箭头代表着预测过程时数据的流向,跟训练时的数据流有一定的区别; 结构图里的关键不是圆圈(代表“神经元”),而是连接线(代表“神经元”之间的连接)。每个连接线对应一个不同的权重(其值称为权值),这是需要训练得到的。 除了从左到右的形式表达的结构图,还有一种常见的表达形式是从下到上来表示一个神经网络。这时候,输入层在图的最下方。输出层则在图的最上方,如下图: 图3 从下到上的神经网络结构图 从左到右的表达形式以Andrew Ng和LeCun的文献使用较多,Caffe里使用的则是从下到上的表达。在本文中使用Andrew Ng代表的从左到右的表达形式。
下面从简单的神经元开始说起,一步一步介绍神经网络复杂结构的形成。
二. 神经元
1.引子
对于神经元的研究由来已久,1904年生物学家就已经知晓了神经元的组成结构。
一个神经元通常具有多个树突,主要用来接受传入信息;而轴突只有一条,轴突尾端有许多轴突末梢可以给其他多个神经元传递信息。轴突末梢跟其他神经元的树突产生连接,从而传递信号。这个连接的位置在生物学上叫做“突触”。
人脑中的神经元形状可以用下图做简单的说明: 图4 神经元 1943年,心理学家McCulloch和数学家Pitts参考了生物神经元的结构,发表了抽象的神经元模型MP。在下文中,我们会具体介绍神经元模型。 * 图5 Warren McCulloch(左)和 Walter Pitts(右) 2.结构 图6 神经元模型
连接是神经元中最重要的东西。每一个连接上都有一个权重。
一个神经网络的训练算法就是让权重的值调整到最佳,以使得整个网络的预测效果最好。 我们使用a来表示输入,用w来表示权值。一个表示连接的有向箭头可以这样理解:在初端,传递的信号大小仍然是a,端中间有加权参数w,经过这个加权后的信号会变成a*w,因此在连接的末端,信号的大小就变成了a*w。 在其他绘图模型里,有向箭头可能表示的是值的不变传递。而在神经元模型里,每个有向箭头表示的是值的加权传递。 神经元模型是一个包含输入,输出与计算功能的模型。输入可以类比为神经元的树突,而输出可以类比为神经元的轴突,计算则可以类比为细胞核。 下图是一个典型的神经元模型:包含有3个输入,1个输出,以及2个计算功能。 注意中间的箭头线。这些线称为“连接”。每个上有一个“权值”。 图7 连接(connection) 如果我们将神经元图中的所有变量用符号表示,并且写出输出的计算公式的话,就是下图。 图8 神经元计算 可见z是在输入和权值的线性加权和叠加了一个函数g的值。在MP模型里,函数g是sgn函数,也就是取符号函数。这个函数当输入大于0时,输出1,否则输出0。 下面对神经元模型的图进行一些扩展。首先将sum函数与sgn函数合并到一个圆圈里,代表神经元的内部计算。其次,把输入a与输出z写到连接线的左上方,便于后面画复杂的网络。最后说明,一个神经元可以引出多个代表输出的有向箭头,但值都是一样的。 神经元可以看作一个计算与存储单元。计算是神经元对其的输入进行计算功能。存储是神经元会暂存计算结果,并传递到下一层。 图9 神经元扩展 当我们用“神经元”组成网络以后,描述网络中的某个“神经元”时,我们更多地会用“单元”(unit)来指代。同时由于神经网络的表现形式是一个有向图,有时也会用“节点”(node)来表达同样的意思。
3.效果
神经元模型的使用可以这样理解:
我们有一个数据,称之为样本。样本有四个属性,其中三个属性已知,一个属性未知。我们需要做的就是通过三个已知属性预测未知属性。
具体办法就是使用神经元的公式进行计算。三个已知属性的值是a1,a2,a3,未知属性的值是z。z可以通过公式计算出来。
这里,已知的属性称之为特征,未知的属性称之为目标。假设特征与目标之间确实是线性关系,并且我们已经得到表示这个关系的权值w1,w2,w3。那么,我们就可以通过神经元模型预测新样本的目标。
假设,你有这样一个网络层: 第一层是输入层,包含两个神经元i1,i2,和截距项b1;第二层是隐含层,包含两个神经元h1,h2和截距项b2,第三层是输出o1,o2,每条线上标的wi是层与层之间连接的权重,激活函数我们默认为sigmoid函数。 现在对他们赋上初值,如下图:
其中,输入数据 i1=0.05,i2=0.10;
输出数据 o1=0.01,o2=0.99;
初始权重 w1=0.15,w2=0.20,w3=0.25,w4=0.30;
w5=0.40,w6=0.45,w7=0.50,w8=0.55 目标:给出输入数据i1,i2(0.05和0.10),使输出尽可能与原始输出o1,o2(0.01和0.99)接近。
Step 1 前向传播
1.输入层---->隐含层:
计算神经元h1的输入加权和: 神经元h1的输出o1:(此处用到激活函数为sigmoid函数): 同理,可计算出神经元h2的输出o2: 2.隐含层---->输出层:
计算输出层神经元o1和o2的值: 这样前向传播的过程就结束了,我们得到输出值为[0.75136079 , 0.772928465],与实际值[0.01 , 0.99]相差还很远,现在我们对误差进行反向传播,更新权值,重新计算输出。
Step 2 反向传播
1.计算总误差
总误差:(square error) 但是有两个输出,所以分别计算o1和o2的误差,总误差为两者之和:
2.隐含层---->输出层的权值更新:
以权重参数w5为例,如果我们想知道w5对整体误差产生了多少影响,可以用整体误差对w5求偏导求出:(链式法则) 下面的图可以更直观的看清楚误差是怎样反向传播的: 现在我们来分别计算每个式子的值:
计算: 计算: (这一步实际上就是对sigmoid函数求导,比较简单,可以自己推导一下) 计算: 最后三者相乘: 这样我们就计算出整体误差E(total)对w5的偏导值。
回过头来再看看上面的公式,我们发现:
为了表达方便,用来表示输出层的误差: 因此,整体误差E(total)对w5的偏导公式可以写成: 如果输出层误差计为负的话,也可以写成: 最后我们来更新w5的值: (其中,是学习速率,这里我们取0.5)
同理,可更新w6,w7,w8: 3.隐含层---->隐含层的权值更新:
方法其实与上面说的差不多,但是有个地方需要变一下,在上文计算总误差对w5的偏导时,是从out(o1)---->net(o1)---->w5,但是在隐含层之间的权值更新时,是out(h1)---->net(h1)---->w1,而out(h1)会接受E(o1)和E(o2)两个地方传来的误差,所以这个地方两个都要计算。 计算: 先计算: 同理,计算出: 两者相加得到总值: 再计算: 再计算: 最后,三者相乘: 为了简化公式,用sigma(h1)表示隐含层单元h1的误差: 最后,更新w1的权值: 同理,额可更新w2,w3,w4的权值: 这样误差反向传播法就完成了,最后我们再把更新的权值重新计算,不停地迭代,在这个例子中第一次迭代之后,总误差E(total)由0.298371109下降至0.291027924。迭代17126次后,总误差为9.999213376027056e-06,输出为0.0131328 0.98680876(原输入为[0.01,0.99]),证明效果还是不错。 代码如下:
import numpy as np import matplotlib.pyplot as plt import math x=np.array([0.05,0.10])#输入的数据 y=np.array([[0.15,0.25],[0.2,0.3]])#第一个隐含层的权重[[w1,w3],[w2,w4]] y1=np.array([[0.4,0.5],[0.45,0.55]])#第二个隐含层的权重[[w5,w7],[w6,w8]] r=np.array([0.01,0.99]) b=0.35#第一个隐含层的偏置 b1=0.6#第二个隐含层的偏置 q=0.5#学习率 def neth(x,y,b): return np.dot(x,y)+b def outh(): outh=[] for i in range(len(y[0])): a=1/(math.exp(-neth(x,y,b)[i])+1) outh.append(a) return np.array(outh) def neto(): return np.dot(outh(),y1)+b1 def outo(): outo=[] for i in range(len(y1[0])): a=1/(math.exp(-neto()[i])+1) outo.append(a) return np.array(outo) o=0#运行次数 O=[]#运行次数集合 result=[]#误差集合 #梯度下降 while True: o=o+1 dif=[[0,0],[0,0]] dif1=[[0,0], [0,0]] a=0 #对w5-w8进行求导 for i in range(len(y1)): for j in range(len(y1[i])): dif1[i][j]=(-(r[j]-outo()[j]))*(outo()[j]*(1-outo()[j]))*(outh()[i]) y1[i][j]=y1[i][j]-q*dif1[i][j] #对w1-w4进行求导 for i in range(len(y)): for j in range(len(outo())): a=a+(-(r[j]-outo()[j]))*outo()[j]*(1-outo()[j])*y1[i][j] for k in range(len(y[i])): dif[i][k]=a*(outh()[k]*(1-outh()[k]))*x[i] y[i][k]=y[i][k]-q*dif[i][k] e=np.sum((r-outo())**2/2)#总误差 result.append(e) O.append(o) if e<0.00001: break plt.plot(O,result) plt.show() print(outo()) print(o) print(e)结果图(y轴为误差,x轴为运行次数)