十一行Python代码实现一个误差逆传播(BP)神经网络
标签(空格分隔): BP 神经网络 机器学习
通过一个例子,来学习BP神经网络。这个例子来源于十一行Python代码实现一个神经网络(第一部分),好像也是翻译别人的博客。算法的推导来自周志华的《机器学习》。
样本定义
假设训练样本如下:
输入1 | 输入2 | 输入3 | 输出 |
---|---|---|---|
0 | 0 | 1 | 0 |
1 | 1 | 1 | 1 |
1 | 0 | 1 | 1 |
0 | 1 | 1 | 0 |
这是一个拥有3个输入节点1个输出节点,总共4个样例的训练样本。所以,可以画出下图这样的两层神经网络图,y点为输出节点,
x1
,
x2
,
x3
为输入节点。
ω1
,
ω2
,
ω3
为各个节点的权值。
根据《机器学习》(周志华的西瓜书)记述,可以设输出节点
y
的激励函数为Sigmoid函数:
输出节点的输出值是 y=f(∑i=3i=1ωixi−θ) 。 θ 是该节点的阈值,即大于阈值 θ 时,输出1,小于时输出0。
模型推导
因为BP神经网络是通过输出节点输出值与实际值比较,每次得到的误差推导出输入节点的权值
ωi
,即每次
ωi=ωi+Δωi
模型其他参数都是已知量(初始权重为任意假设给定值,阈值
θ
也可以看成是定值),所以,模型的关键是求出
ωi
每次需要的改变量
Δωi
根据周志华的《机器学习》P99页论述,
Δωi=η(y−y^)xi
(后面将给出证明)。其中
η∈(0,1)
称为学习率(learning rate),用来调节
Δωi
的变化速度,可以看成是常值;
y^
是
L2
层节点输出值。
Δωi
的设定和PID算法有点像
这个两层神经网络的均方误差为
E=12(y^k−yk)2
,用梯度下降法,求出
Δωi
其中,设 ∑ωixi−θ=z
根据sigmoid函数的性质, f′(z)=f(z)(1−f(z)) ,可以得出
每次改变的 Δωi=−η⋅(y^k−yk)⋅(yk)′⋅xi=η⋅(yk−y^k)⋅y^k⋅(1−y^k)⋅xi ,因为要求误差最小值,所以选用梯度的反方向。
python编程
import numpy as np
# sigmoid function 定义激活函数sigmoid(x),nonlin(x,True)就是计算sigmoid的导数。
def nonlin(x,deriv=False):
if(deriv==True):
return x*(1-x)
return 1/(1+np.exp(-x))
#生成样本训练模型,输入4*3矩阵,4个样本,每个样本3个输入值。
X = np.array([ [0,0,1],
[1,1,1],
[1,0,1],
[0,1,1] ])
#生成输出节点数值,输入4*1矩阵,4个样本,每个样本1个输出值。
y = np.array([[0,1,1,0]]).T
#设置随机种子,这个主要是为了每次生成的随机量一样,可以使程序重复试验。
np.random.seed(1)
#设置第一层各输入点权重,权重是随机生成的,均值是0
syn0 = 2*np.random.random((3,1)) - 1
for iter in xrange(10000):
# forward propagation l0是第一层
l0 = X
# l1是输出量\hat{y},l0是输入量,与权值相乘作为输出节点激励函数的自变量
l1 = nonlin(np.dot(l0,syn0))
# how much did we miss?
l1_error = y - l1
l1_delta = l1_error * nonlin(l1,True)
# update weights
syn0 += np.dot(l0.T,l1_delta)
print "Output After Training:"
print l1
程序解读
l0是神经网络第一层,即输入层
l0 = X
l1是输出量 y^ ,nonlin(np.dot(l0,syn0))是输入量l0与权值syn0相乘作为输出节点激励函数的自变量,即 y^=l1=sigmoid(l0∗syn0)=sigmoid(∑xi⋅ωi)
l1 = nonlin(np.dot(l0,syn0))
计算误差 l1_error=y−y^
l1_error = y - l1
利用梯度下降法,计算权重改变量。l1作为输出量,nonlin(l1,True)表示对输出量求导即
nonlin(l1,True)=f′=f(1−f)=(yk)′
l1_delta=(y−y^)⋅(yk)′
l1_delta = l1_error * nonlin(l1,True)
对权重进行更新 np.dot(l0.T,l1_delta) 为 l0.T⋅l1_delta=(y−y^)⋅(yk)′⋅x=Δω ,通过 syn0+=Δω 完成权重的更新。
syn0 += np.dot(l0.T,l1_delta)
在10000次训练之后,syn0为更新后的权重,则算出的l1为训练后输出值。
运行结果
通过10000次训练,获得权重syn0,在此权重下,输出节点的输出值:
[[ 0.00966449]
[ 0.99211957]
[ 0.99358898]
[ 0.00786506]]
误差为:
[[-0.00966449]
[ 0.00788043]
[ 0.00641102]
[-0.00786506]]
通过100000次训练,获得权重syn0,在此权重下,输出节点的输出值:
[[ 0.00301758]
[ 0.99753723]
[ 0.99799161]
[ 0.00246109]]
误差为:
[[-0.00301758]
[ 0.00246277]
[ 0.00200839]
[-0.00246109]]
可以发现,随着训练步骤的增多,误差越来越小。
三层神经网络
还是刚才的例子,我们这次假定网络有三层,即输入层l0,隐藏层l1,输出层l2,建立如下图的神经网络结构:
图中隐层节点
bi
到输出点
y
的权重是
网络在输出节点的均方误差为:
对其求导,得到:
设 z=∑4j=1(ωi⋅bi−θ) ,则均方误差中, ∂E∂y^=y^−y , ∂y^∂z=y^⋅(1−y^) , ∂z∂ωi=bi 。所以
因为是梯度下降,要减小误差,因此 Δωi 应该沿梯度方向相反方向前进。
所以, Δωi=−η⋅(y^−y)⋅y^⋅(1−y^)⋅bi=η⋅(y−y^)⋅y^⋅(1−y^)⋅bi
我们求得隐层到输出层的权值,现在要求输入层到隐层的权值 vij 改变。
其中, bj=f(∑3i=1vijxi−θj) , α=∑3i=1vij(xi−θj)
所以,
第一层节点权重的该变量 Δvij=−η∂E∂vij=−η∂E∂vij
假设 η=1 ,则可以得到
Δωi=−⋅(y^−y)⋅y^⋅(1−y^)⋅bi=(y−y^)⋅y^⋅(1−y^)⋅bi
import numpy as np
def nonlin(x,deriv=False):
if(deriv==True):
return x*(1-x)
return 1/(1+np.exp(-x))
X = np.array([[0,0,1],
[0,1,1],
[1,0,1],
[1,1,1]])
y = np.array([[0],
[1],
[1],
[0]])
np.random.seed(1)
# randomly initialize our weights with mean 0
syn0 = 2*np.random.random((3,4)) - 1
syn1 = 2*np.random.random((4,1)) - 1
for j in range(60000):
# Feed forward through layers 0, 1, and 2
l0 = X
l1 = nonlin(np.dot(l0,syn0))#计算隐层节点的输出值
l2 = nonlin(np.dot(l1,syn1))#计算输出节点输出值
# how much did we miss the target value?
l2_error = y - l2 #计算输出与实际值误差
if (j% 10000) == 0:
print("Error:" + str(np.mean(np.abs(l2_error))))
# in what direction is the target value?
# were we really sure? if so, don't change too much.
l2_delta = l2_error*nonlin(l2,deriv=True)
# how much did each l1 value contribute to the l2 error (according to the weights)?
l1_error = l2_delta.dot(syn1.T)
# in what direction is the target l1?
# were we really sure? if so, don't change too much.
l1_delta = l1_error * nonlin(l1,deriv=True)
syn1 += l1.T.dot(l2_delta)
syn0 += l0.T.dot(l1_delta)
程序解读
三层神经网络与二层相似,增加了隐层后需要计算隐层节点的误差,即程序35行
l1_error = l2_delta.dot(syn1.T)
l2_delta=l2_error*nonlin(l2,deriv=True),这步计算了隐层到输出层节点的权重调节值。即理论推导中的
Δωi=η⋅(y−y^)⋅y^⋅(1−y^)⋅bi
,假设学习率
η=1
。
这里公式符号跟编程代码的对应关系如下(因为编码中使用的是向量或者矩阵,这里只是简单的列出对应关系。)
公式符号 | 代码 | 公式符号 | 代码 |
---|---|---|---|
y−y^ | l2_error | bj | l1 |
y^ | l2 | y^(1−y^) | nonlin(l2,deriv=True) |
Δωi=(y−y^)y^(1−y^)bi | l1.T.dot(l2_delta) | (y−y^)y^(1−y^) | l2_delta |
(y−y^)y^(1−y^)ωj | l1_error | (y−y^)y^(1−y^)ωjbj(1−bj) | l1_delta |
Δvij=(y−y^)y^(1−y^)ωjbj(1−bj)xi | l0.T.dot(l1_delta) | xi | l0.T |
通过对应关系表,可以读懂程序中每一步代码的含义。
后记
神经网络是建立输入和输出之间的映射关系,如果输入是一张猫的照片,输出结果则为猫。
绝大多数时候,我们不能建立线性映射关系,建立映射关系需要复杂的函数,神经网络可以以任意精度逼近任一连续可微函数(这个是被证明过得),因此只要层数、神经元够多,我们就能建立关系,但是复杂性会随之增加。机器学习就是通过各种关系,来建立这一关系。
参考
- 神经网络之BP神经网络(Python实现):主要是公式推导
- 一个 11 行 Python 代码实现的神经网络:本文代码的主要参考
- 周志华的《机器学习》:本文公式推导的参考来源