十一行Python代码实现一个误差逆传播(BP)神经网络

十一行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 为各个节点的权值。
BP.png-5.2kB
根据《机器学习》(周志华的西瓜书)记述,可以设输出节点 y 的激励函数为Sigmoid函数:

sigmoid(x)=11+ex

输出节点的输出值是 y=f(i=3i=1ωixiθ) θ 是该节点的阈值,即大于阈值 θ 时,输出1,小于时输出0。

模型推导

因为BP神经网络是通过输出节点输出值与实际值比较,每次得到的误差推导出输入节点的权值 ωi ,即每次 ωi=ωi+Δωi
模型其他参数都是已知量(初始权重为任意假设给定值,阈值 θ 也可以看成是定值),所以,模型的关键是求出 ωi 每次需要的改变量 Δωi
根据周志华的《机器学习》P99页论述, Δωi=η(yy^)xi (后面将给出证明)。其中 η(0,1) 称为学习率(learning rate),用来调节 Δωi 的变化速度,可以看成是常值; y^ L2 层节点输出值。
Δωi 的设定和PID算法有点像
这个两层神经网络的均方误差为 E=12(y^kyk)2 ,用梯度下降法,求出 Δωi

Δωi=ηEωi

其中,设 ωixiθ=z
Eωi=Ey^ky^kzzωi

根据sigmoid函数的性质, f(z)=f(z)(1f(z)) ,可以得出
y^k(z)z=y^k(z)(1y^k(z))
所以,
Eωi=Ey^ky^kzzωi=(y^kyk)y^k(1y^k)xi

每次改变的 Δωi=η(y^kyk)(yk)xi=η(yky^k)y^k(1y^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(l0syn0)=sigmoid(xiωi)

l1 = nonlin(np.dot(l0,syn0))

计算误差 l1_error=yy^

l1_error = y - l1

利用梯度下降法,计算权重改变量。l1作为输出量,nonlin(l1,True)表示对输出量求导即 nonlin(l1,True)=f=f(1f)=(yk)
l1_delta=(yy^)(yk)

l1_delta = l1_error * nonlin(l1,True) 

对权重进行更新 np.dot(l0.T,l1_delta) 为 l0.Tl1_delta=(yy^)(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 的权重是 ωi ,输入层节点 xi 到隐层节点 bj 的权重为 vij
网络在输出节点的均方误差为:

E=12(y^y)2

对其求导,得到:
Eωi=Ey^y^zzωi

z=4j=1(ωibiθ) ,则均方误差中, Ey^=y^y y^z=y^(1y^) zωi=bi 。所以
Eωi=(y^y)y^(1y^)bi

因为是梯度下降,要减小误差,因此 Δωi 应该沿梯度方向相反方向前进。
所以, Δωi=η(y^y)y^(1y^)bi=η(yy^)y^(1y^)bi
我们求得隐层到输出层的权值,现在要求输入层到隐层的权值 vij 改变。
Evij=Ey^y^zzbjbjααvij

其中, bj=f(3i=1vijxiθj) α=3i=1vij(xiθj)
所以,
Evij=(y^y)y^(1y^)ωjbj(1bj)xi

第一层节点权重的该变量 Δvij=ηEvij=ηEvij
假设 η=1 ,则可以得到
Δvij=Δωiωj(1bj)xi

Δωi=(y^y)y^(1y^)bi=(yy^)y^(1y^)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=η(yy^)y^(1y^)bi ,假设学习率 η=1
这里公式符号跟编程代码的对应关系如下(因为编码中使用的是向量或者矩阵,这里只是简单的列出对应关系。)

公式符号 代码 公式符号 代码
yy^ l2_error bj l1
y^ l2 y^(1y^) nonlin(l2,deriv=True)
Δωi=(yy^)y^(1y^)bi l1.T.dot(l2_delta) (yy^)y^(1y^) l2_delta
(yy^)y^(1y^)ωj l1_error (yy^)y^(1y^)ωjbj(1bj) l1_delta
Δvij=(yy^)y^(1y^)ωjbj(1bj)xi l0.T.dot(l1_delta) xi l0.T

通过对应关系表,可以读懂程序中每一步代码的含义。

后记

神经网络是建立输入和输出之间的映射关系,如果输入是一张猫的照片,输出结果则为猫。
绝大多数时候,我们不能建立线性映射关系,建立映射关系需要复杂的函数,神经网络可以以任意精度逼近任一连续可微函数(这个是被证明过得),因此只要层数、神经元够多,我们就能建立关系,但是复杂性会随之增加。机器学习就是通过各种关系,来建立这一关系。

参考

  1. 神经网络之BP神经网络(Python实现):主要是公式推导
  2. 一个 11 行 Python 代码实现的神经网络:本文代码的主要参考
  3. 周志华的《机器学习》:本文公式推导的参考来源

猜你喜欢

转载自blog.csdn.net/jayandchuxu/article/details/78736832