深度学习:神经网络-BP算法原理及利用python从零实现

摘要:给出反向传播算法的具体推导,并通过一个例子对反向传播的计算流程进行介绍。最后利用python从零实现一个神经网络。代码仅作为公式的理解,不具备重复使用能力。

目录

  1. BP算法原理
  2. 简单实例
  3. python从零实现
  4. 各资料中的BP算法公式

参考

【1】“Neural Networks and Deep Learning”. Michael A. Nielsen.

【2】“Pattern Classification”.

【3】“Neural Networks and Deep Learning”. 邱锡鹏.

1. BP算法原理

有关多层感知器、神经网络的具体内容在各参考文献中均有详细介绍。

对于前馈网络,前向传播接收输入数据 x x x,计算输出 y ^ \hat{y} y^。在训练时通过输出 y ^ \hat{y} y^计算损失 J ( θ ) J(\theta) J(θ)。反向传播算法通过计算 J ( θ ) J(\theta) J(θ)的梯度,允许信息从输出层反向传输,如图1。下面开始介绍反向传播的具体过程,并通过一个例子加深理解。

首先给出Hinton报告中的几点说明:

  1. 从输入数据,我们并不知道隐层单元时如何工作的;
  2. 但我们能知道当隐层激活改变时,损失减少的快慢程度(通过梯度);
  3. 每一个隐层单元能影响多个输出节点,并且对误差有不同的影响;
  4. 通过微分链式法则,损失函数每个权重的梯度很容易计算

接下去就重点分析一下这个很容易计算的过程。

上图即一个常见的全连接网络结构,输入向量 x x x d d d个维度,单隐层有 m 个 单 元 m个单元 m,单输出模型,并且包括下标为 0 0 0的偏置项。

根据前向操作,从输入 x x x到输出 y ^ \hat{y} y^,我们有如下公式:

y ^ ( x ; w ) = f ( ∑ j = 1 m w j ( 2 ) h ( ∑ i = 1 d w i j ( 1 ) x i + w 0 j ( 1 ) ) + w 0 ( 2 ) ) \hat{y}(x;w)=f\left(\sum_{j=1}^mw^{(2)}_jh\left(\sum_{i=1}^dw^{(1)}_{ij}x_i+w_{0j}^{(1)}\right)+w_{0}^{(2)}\right) y^(x;w)=f(j=1mwj(2)h(i=1dwij(1)xi+w0j(1))+w0(2))

公式即简单的线性操作加非线性激活,再进行线性操作和非线性激活,就得到估计的输出。

网络的误差同样很容易得到:

L ( X ; w ) = ∑ i = 1 M 1 2 ( y i − y ^ ( x ; w ) ) 2 L(X;w)=\sum_{i=1}^M\frac{1}{2}\left(y_i-\hat{y}(x;w)\right)^2 L(X;w)=i=1M21(yiy^(x;w))2

这里用的就是常见的平方损失。

通常,由于非线性激活的存在,最小化上述误差都是没有闭式解的,因此需要用到梯度下降等优化算法。并且对每个样本,我们都要计算其梯度。

这里给出一个简单的线性模型: y ^ = ∑ j w j x i j \hat{y}=\sum_jw_jx_{ij} y^=jwjxij,作为例子,观察梯度的特点。梯度计算如下:
∂ L ( x i ) ∂ w j = ( y ^ i − y ) x i j \frac{\partial L(x_i)}{\partial w_{j}}=(\hat{y}_{i}-y)x_{ij} wjL(xi)=(y^iy)xij
即,梯度为误差乘输入

对于如下图所示的多层神经网络的特定单元,其有 s s s个输入单元,我们分析它所连的第 t t t个输出单元。

其输出 z t z_t zt可以表示为

z t = h ( ∑ j w j t z j ) z_t=h\left(\sum_jw_{jt}z_j\right) zt=h(jwjtzj)

其中,记住我们的 j j j是上一层的神经元号, t t t是当前层的神经元号。令线性组合 ∑ j w j t z j = a t \sum_jw_{jt}z_j=a_t jwjtzj=at,则 z t = h ( a t ) z_t=h(a_t) zt=h(at) z t z_t zt为激活值, a t a_t at为线性组合值。

因此可以发现损失函数 L L L w j t w_{jt} wjt的依赖性全部都在 a t a_t at中,如下所示:

∂ L ∂ w j t = ∂ L ∂ a t ∂ a t ∂ w j t = ∂ L ∂ a t z j \frac{\partial L}{\partial w_{jt}}=\frac{\partial L}{\partial a_{t}}\frac{\partial a_t}{\partial w_{jt}}=\frac{\partial L}{\partial a_{t}}z_j wjtL=atLwjtat=atLzj

类比前面线性模型的例子, z j z_j zj就是上一层第 j j j个输出即当前层的第 j j j个输入。

然后定义

∂ L ∂ a t = δ t \frac{\partial L}{\partial a_{t}}=\delta_t atL=δt

δ t \delta_t δt可以理解为误差对神经元 t t t的敏感程度,反向传播过程就是该敏感程度沿着网络反向传播的过程。

对于当前神经元 t t t,我们已经分析了它的输入,接下去分析它的输出。

如图所示,神经元 t t t会对其下一层的多个单元传递输入,令所有接收 t t t输入的神经元所处的集合为 S S S,可以得到下式:

a s = ∑ j : j → s ∈ S w j s h ( a j ) a_s=\sum_{j:j\rightarrow s\in S}w_{js}h(a_j) as=j:jsSwjsh(aj)

即,对于所属集合 S S S的神经元 s s s,其输入为上一层所有指向它的神经元输出之和。

因此当 j = t j=t j=t时, a t a_t at就和 a s a_s as发生关系,而 a s a_s as更加接近输出,因而对 L L L的影响更靠前,如下式:

δ t = ∑ s ∈ S ∂ L ∂ a s ∂ a s ∂ a t = ∑ s ∈ S ∂ L ∂ a s w t s h ′ ( a t ) = h ′ ( a t ) ∑ s ∈ S w t s δ s \begin{aligned} \delta_t&=\sum_{s\in S}\frac{\partial L}{\partial a_{s}}\frac{\partial a_s}{\partial a_{t}}\\ &=\sum_{s\in S}\frac{\partial L}{\partial a_{s}}w_{ts}h'(a_t)\\ &=h'(a_t)\sum_{s\in S}w_{ts}\delta_s \end{aligned} δt=sSasLatas=sSasLwtsh(at)=h(at)sSwtsδs

到此,我们就将当前层的 δ t \delta_t δt和下一层的 δ s \delta_s δs联系在一起。

目前只是反向传播的过程,那在网络中反向传播具体是怎么表现的呢?又怎么和权值更新结合在一起呢?这两个问题将通过一个具体的例子进行解答。

2. 简单实例

简单算例,网络结构如上图,并假设:

  1. 输出: y ^ = f ( a ) = a \hat{y}=f(a)=a y^=f(a)=a
  2. 隐层激活函数: h ( a ) = tanh ⁡ ( a ) h(a)=\tanh(a) h(a)=tanh(a),因此 h ′ ( a ) = 1 − h ( a ) 2 h'(a)=1-h(a)^2 h(a)=1h(a)2

反向传播求解开始!

对于一个输入样本 x x x,首先是前向传播过程:

  1. 隐层输入: a j = ∑ i = 0 d w i j ( 1 ) x i a_j=\sum_{i=0}^dw^{(1)}_{ij}x_i aj=i=0dwij(1)xi
  2. 隐层输出: z j = tanh ⁡ ( a j ) z_j=\tanh(a_j) zj=tanh(aj)
  3. 网络输出: y ^ = a = ∑ j = 0 m w j ( 2 ) z j \hat{y}=a=\sum_{j=0}^mw^{(2)}_jz_j y^=a=j=0mwj(2)zj

因此对每一个样本的误差为:

L = 1 2 ( y − y ^ ) 2 L=\frac{1}{2}\left(y-\hat{y}\right)^2 L=21(yy^)2

对于输出单元,直接计算其 δ \delta δ

δ = ∂ L ∂ a = y − a = y − y ^ \delta=\frac{\partial L}{\partial a}=y-a=y-\hat{y} δ=aL=ya=yy^

然后计算隐层单元的 δ \delta δ

δ j = h ′ ( a j ) w j ( 2 ) δ = ( 1 − z j ) 2 w j ( 2 ) δ \delta_j=h'(a_j)w^{(2)}_j\delta=(1-z_j)^2w^{(2)}_j\delta δj=h(aj)wj(2)δ=(1zj)2wj(2)δ

就是上面的传播公式,这里的集合 S S S只有一个输出神经元。

下面就可以直接写出梯度:

∂ L ∂ w i j ( 1 ) = δ j x i ∂ L ∂ w j ( 2 ) = δ z j \begin{aligned} &\frac{\partial L}{\partial w^{(1)}_{ij}}=\delta_jx_i\\ &\frac{\partial L}{\partial w^{(2)}_{j}}=\delta z_j \end{aligned} wij(1)L=δjxiwj(2)L=δzj

带入梯度下降法的权值更新公式:

w i j ( 1 ) : = w i j ( 1 ) − η δ j x i w j ( 2 ) : = w j ( 2 ) − η δ z j \begin{aligned} &w^{(1)}_{ij}:=w^{(1)}_{ij}-\eta\delta_jx_i\\ &w^{(2)}_{j}:=w^{(2)}_{j}-\eta\delta z_j \end{aligned} wij(1):=wij(1)ηδjxiwj(2):=wj(2)ηδzj


通过上面的例子,大家都能自己编程推导反向传播算法了嘛,哪里有疑问或者没说清楚的欢迎评论~~

下面就按照上述例子直观的编写反向传播算法。

3. python从零实现BP网络

【1】中有比较详细的DNN从零搭建过程,这里没有按照那么复杂的方式来些,纯粹是为了配合公式,使代码和公式更好理解。

首先准备数据集,人工生成一个回归任务的数据。

import numpy as np
import matplotlib.pyplot as plt

num_train, num_test = 100, 100
num_features = 10

true_w, true_b = np.ones((num_features, 1)) * 5, 2

features = np.random.normal(0, 1, (num_train + num_test, num_features))
noises = np.random.normal(0, 1, (num_train + num_test, 1)) * 0.0
labels = np.dot(features, true_w) + true_b + noises

x_train, x_test = features[:num_train, :], features[num_train:, :]
y_train, y_test = labels[:num_train], labels[num_train:]

再选择模型,进行训练。代码比较简单,结合注释以及第二节的公式阅读。基本和公式是一样的。

# 选择模型类型
def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))

def sigmoid_prime(x):
    return sigmoid(x) * (1 - sigmoid(x))


class Network(object):
    
    def __init__(self, sizes):
        self.num_layers = len(sizes)
        self.sizes = sizes
        self.biases = [np.random.randn(y) for y in self.sizes[1:]]
        self.weights = [np.random.randn(x, y) / np.sqrt(x)
                        for (x, y) in zip(self.sizes[:-1], self.sizes[1:])]
        self.delta = []
        
    def feedforward(self, activation):
        # 公式前向传播
        a_hidden = np.dot(activation, self.weights[0]) + self.biases[0]
        z = sigmoid(a_hidden)
        a_out = np.dot(z, self.weights[1]) + self.biases[1]
        y_hat = a_out
        return y_hat
    
    def backpropagation(self, x, y):
        
        # -----------------------------前向传播开始-----------------------------------
        # 依次计算隐层输入,隐层输出,网络输出
        a_hidden = np.dot(x, self.weights[0]) + self.biases[0]
        z = sigmoid(a_hidden)
        a_out = np.dot(z, self.weights[1]) + self.biases[1]
        y_hat = a_out
        assert y_hat.all() == self.feedforward(x).all()
        # -----------------------------前向传播完成-----------------------------------
        
        # -----------------------------反向传播开始-----------------------------------
        # 依次计算误差,输出单元delta和隐层delta
        L = (y_hat - y) ** 2 / 2
        delta_output = (y_hat - y)
        delta_hidden = sigmoid_prime(a_hidden).reshape((-1,1)) * (self.weights[1] * delta_output)
        # -----------------------------反向传播结束-----------------------------------
        
        # -----------------------------权重更新开始-----------------------------------
        # 更新输入-隐层的权重
        delta1 = np.zeros_like(self.weights[0])
        for j in range(self.weights[0].shape[1]):
            for i in range(self.weights[0].shape[0]):
                delta1[i, j] = delta_hidden[j] * x[i]
#                 print(delta1[i, j])
                self.weights[0][i, j] -= 1 * delta1[i, j]
            self.biases[0][j] -= 1 * delta_hidden[j]
        # 更新隐层-输出的权重
        delta2 = np.zeros_like(self.weights[1])
        for j in range(self.weights[1].shape[1]):
            for i in range(self.weights[1].shape[0]):
                delta2[i, j] = delta_output[j] * z[i]
                self.weights[1][i, j] -= 0.001 * delta2[i, j]
            self.biases[1][j] -= 0.001 * delta_output[j]
    
def myloss(y_hat, y):
    return (y_hat - y) ** 2
    
net = Network([10, 20, 1])

all_loss = []
for epoch in range(100):
    loss = 0
    for isample in range(x_train.shape[0]):
        loss += myloss(net.feedforward(x_train[isample]), y_train[isample])
    all_loss.append(loss)
    for isample in range(x_train.shape[0]):
        net.backpropagation(x_train[isample], y_train[isample])

plt.plot(range(100), all_loss)

观察一下测试结果,基本拟合在一起了。表明上述推导以及比较低级的代码是有效的。
在这里插入图片描述

4. 总结BP算法公式

各资料的推导过程大同小异,很多都只有符号上的区别。

本文仅针对单个神经元进行推导,最终得到的几个公式如下, a a a为线性组合, z z z为激活值。

  1. δ \delta δ法则:其中 t t t为当前层的某个神经元, s s s为下一层的某个神经元。

δ t = h ′ ( a t ) ∑ s ∈ S w t s δ s \delta_t=h'(a_t)\sum_{s\in S}w_{ts}\delta_s δt=h(at)sSwtsδs

  1. 权重更新:其中 t t t为当前层的某个神经元, j j j为输入层的某个神经元。

∂ L ∂ w j t = ∂ L ∂ a t ∂ a t ∂ w j t = ∂ L ∂ a t z j = δ t z j \frac{\partial L}{\partial w_{jt}}=\frac{\partial L}{\partial a_{t}}\frac{\partial a_t}{\partial w_{jt}}=\frac{\partial L}{\partial a_{t}}z_j=\delta_tz_j wjtL=atLwjtat=atLzj=δtzj

后来发现这样的推导和《模式分类》【2】中的推导基本相同。

邱锡鹏老师的《神经网络与深度学习》【3】中对某层的神经元进行推导,最终得到的公式如下, a a a为激活值。

  1. δ \delta δ法则:

δ ( l ) = f l ′ ( z ( l ) ) ∘ ( ( w ( l + 1 ) ) T δ ( l + 1 ) ) \delta^{(l)}=f_l^{'}(z^{(l)})\circ(\left(w^{(l+1)})^T\delta^{(l+1)}\right) δ(l)=fl(z(l))((w(l+1))Tδ(l+1))

  1. 权重更新:

∂ L ∂ W l = δ ( l ) ( a ( l − 1 ) ) T \frac{\partial L}{\partial W^{l}}=\delta^{(l)}\left(a^{(l-1)}\right)^T WlL=δ(l)(a(l1))T

统一符号,并矩阵化以后,两者是完全相同的。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值