神经网络学习笔记(5)——BP算法代码讲解

22 篇文章 5 订阅
11 篇文章 0 订阅

其实我很不愿意写这篇文章的,主要是我代码没跑通!没跑通!没跑通!对于我一个多月没敲过代码后敲的第一份代码来说打击感巨大。但是想了想之前两篇文章都说了国庆要写一篇……然后我的完美主义犯了……
代码没跑通其实真不是我的原因,因为书上代码是错的……一方面是书上用python2写的,我是python3环境,第二方面是代码中的公式错了……这个代码我用了3天时间,推了整整3页草稿纸,又向师兄请教了两天,最后发现,好像代码真的错了……

嗯……开始正文前我还是放出我公式推导的草稿纸……
草稿纸
这是其中一页,然后原谅我计算机蚯蚓体的字体……

代码

我参考的代码的书籍是《python自然语言处理实战核心技术与算法》,由于代码有错,然后我也没跑通,所以就把我觉得最复杂,而且错了的这一坨代码拿出来说,先上代码:

    def backprop(self, x, y):
        """
        返回元祖

        :param x: 输入值, 784 * 1
        :param y: 真实值, 10 * 1
        :return nabla_b: bias的梯度
        :return nabla_w: weight的梯度
        """
        # 创建大小为100 * 1和10 * 1的零矩阵
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        # 创建大小为100 * 784和10 * 100的零矩阵
        nabla_w = [np.zeros(w.shape) for w in self.weights]

        # 前馈, feedforward
        activation = x
        activations = [x]  # 存放激活值
        zs = []  # 存放z向量

        # 前向传递
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation) + b  # z = wx + b
            zs.append(z)
            activation = self.sigmoid(z)  # σ(wx + b)
            activations.append(activation)

        # 后向传递, 取最新一次的最后一层的激活值
        # [σ(wx + b) - y] * σ(wx + b), 阈值(偏置项), 前者是代价的导数
        # 即偏导中c对a求偏导乘激活函数
        delta = self.cost_derivative(activations[-1], y) * self.sigmoid(zs[-1])

        # [σ(wx + b) - y] * σ(wx + b)作为这一层的偏置项???
        nabla_b[-1] = delta
        # {[σ(wx + b) - y] * σ(wx + b)} * 倒数第二层激活值的转置作为这一层的权重???
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())

        for l in range(2, self.num_layers):
            z = zs[-1]
            sp = self.sigmoid_prime(z)  # 对sigmoid求导

            delta = np.dot(self.weights[-l + 1].transpose(), delta) * sp
            nabla_b[-1] = delta
            nabla_w[-1] = np.dot(delta, activations[-l - 1].transpose())

        return nabla_b, nabla_w
  def sigmoid(self, z):
        """
        激活函数sigmoid

        :param z: 激活值
        :return: sigmoid(z)
        """
        return 1.0 / (1.0 + np.exp(-z))

    def sigmoid_prime(self, z):
        """
        对sigmoid求导

        :param z: 激活值
        :return: sigmoid'(z)
        """
        return self.sigmoid(z) * (1 - self.sigmoid(z))

    def cost_derivative(self, output_activations, y):
        """
        代价的导数

        :param output_activations: 输出的激活值向量
        :param y: 真实值向量
        :return: output_activations - y
        """
        return output_activations - y

嗯……大家看到我注释里面打的问号,估计也懂了吧,反正我看代码的时候也是一头黑人问号……

我们这篇文章的目标是,结合上代码把整个BP算法的过程与代码出错的地方讲清楚。

前向传递

前向传递的代码挺容易让人理解的,就是我的输入值,乘上权重,加上偏置,再激活一下,欧拉。

# 前馈, feedforward
activation = x
activations = [x]  # 存放激活值
zs = []  # 存放z向量

# 前向传递
for b, w in zip(self.biases, self.weights):
    z = np.dot(w, activation) + b  # z = wx + b
    zs.append(z)
    activation = self.sigmoid(z)  # σ(wx + b)
    activations.append(activation)

这里唯一要注意的是numpy的dot,如果是两个向量,那么是点积,如果是涉及到高维的话,那么就是矩阵相乘,或者说矩阵与向量的乘法。

后向传递

后向传递这一坨要重新补充一下数学知识,3B1B也没讲细致的推导过程,我这里参考的是这篇文章的推导过程。因为书上又是上标又是下标的,看的贼头大。我这里把那位大佬的公式抽一部分出来讲,主要是怎样进行的传递。

《python自然语言处理实战核心技术与算法》这本书的残差项公式如下:
δ i ( n l ) = ∂ ∂ z i ( n l ) 1 2 ∣ ∣ y − h w , b ( x ) ∣ ∣ 2 \delta_i^{(n_l)}=\frac{\partial}{\partial z_i^{(n_l)}}\frac{1}{2}||y-h_{w,b}(x)||^2 δi(nl)=zi(nl)21yhw,b(x)2
这里的 1 2 \frac{1}{2} 21是方便求导,因为大家看后面这一坨求导出来是不是有个2,方便把这个2约掉。而 y y y是真实值, h x , b ( x ) h_{x,b}(x) hx,b(x)是输出值,只不过这里我们要注意的一点是,作者在这里求残差项,是对 z = w x + b z=wx+b z=wx+b求的偏导,并不是一般书和网上教材上分开求导的,所以可能有的同学疑惑疑惑在这里。

然后书上公式10.21可能许多同学是有疑惑的,公式10.21在没有任何说明激活函数的情况下,直接给出如下形式:
δ i ( l ) = ( ∑ j = 0 s l + 1 W j i 1 δ j ( l + 1 ) ) f ′ ( z i ( 1 ) ) \delta_i^{(l)}=(\sum_{j=0}^{s_{l+1}}W_{ji}^{1}\delta_j^{(l+1)})f'(z_i^{(1)}) δi(l)=(j=0sl+1Wji1δj(l+1))f(zi(1))
那么我们结合上我上上篇文章,我们应该知道,这一步残差是从输出层到第一层隐藏层一层层求偏导过来的,但是书本中并没有提到采用的什么激活函数,所以这个公式大家可以第一疑惑是,为什么激活函数只是一阶导数,而非 l l l阶?即是 f ′ ( z i ( 1 ) ) f'(z_i^{(1)}) f(zi(1))而非 f n ( z i ( 1 ) ) f^n(z_i^{(1)}) fn(zi(1)),其实这里是在求每一层的偏导的时候,将这一层的对激活函数的偏导放到了这一层的残差里面,详细的可以看下面的推导过程:

注:这里我采用刚刚分享的链接的大佬的推导过程,我只说一下很关键的几个步骤,详细的推导过程可以看大佬的博客。所以我这里沿用大佬的,对 w w w求偏导,而非对 z z z

大佬采用了2层隐藏层,所以我这里沿用大佬的神经网络,只不过大佬的网络图略显复杂,我这里精简下:

网络结构
大佬那个是将隐藏层1,2和输出层分别命名为的i、j、k,然后我把每一层下面的节点去掉了,看上去要简单点,不过大家要记住,隐藏层1有i个节点,2有j个节点,输出层有k个节点,同理,真实值也有k个。
然后前置的几个公式我补充下(作者使用的sigmoid激活函数):
σ ′ ( x ) = σ ( x ) ( 1 − σ ( x ) ) \sigma'(x)=\sigma(x)(1-\sigma(x)) σ(x)=σ(x)(1σ(x))
a k = σ ( z k ) a_k=\sigma(z_k) ak=σ(zk)
z k = a j w j k + b k z_k=a_jw_{jk}+b_k zk=ajwjk+bk

这里要注意,在z这里,就已经与上一层神经元有关联了,这也是链式法则很核心的部分。

公式关键部分推导开始:

我们要明确我们的目标,是寻找最开始的权重 w i j w_{ij} wij对损失 L L L的影响,所以是 L L L w i j w_{ij} wij求偏导:
∂ L ∂ w i j = ∂ ∂ w i j 1 2 ∑ k ( a k − y k ) 2 \frac{\partial L}{\partial w_{ij}}=\frac{\partial}{\partial w_{ij}}\frac{1}{2}\sum_k(a_k-y_k)^2 wijL=wij21k(akyk)2
剩下的链式法则咱跳过,我也不好一直剽窃别人的成果对不对,这里直接到我觉得很重要的一步,此时公式如下:
∂ L ∂ w i j = ∑ k ( a k − y k ) a k ( 1 − a k ) ∂ z k ∂ w i j \frac{\partial L}{\partial w_{ij}}=\sum_k(a_k-y_k)a_k(1-a_k)\frac{\partial z_k}{\partial w_{ij}} wijL=k(akyk)ak(1ak)wijzk

大家很快就发现了, ( a k − y k ) a k ( 1 − a k ) (a_k-y_k)a_k(1-a_k) (akyk)ak(1ak)就是输出层残差项 δ k \delta_k δk,这里后面的 a k ( 1 − a k ) a_k(1-a_k) ak(1ak)就是sigmoid函数求导的内容,这一步很关键,我们后面会提到。

接着大家看到这个 ∂ z k ∂ w i j \frac{\partial z_k}{\partial w_{ij}} wijzk,这里已经不是同一层的东西了, w i j w_{ij} wij是第一层隐藏层到第二层隐藏层的权重,而 z k z_k zk是输出层的值,所以我们这里要根据 z k = a j w j k + b k z_k=a_jw_{jk}+b_k zk=ajwjk+bk这个来与第二层隐藏层做关联,由于 z k z_k zk a j a_j aj有关,而 a j = σ ( z j ) = σ ( a i w i j + b j ) a_j=\sigma(z_j)=\sigma(a_iw_{ij}+b_j) aj=σ(zj)=σ(aiwij+bj),所以 a j a_j aj w i j w_{ij} wij有关,于是转换为如下形式:
∂ z k ∂ w i j = ∂ z k ∂ a j ∂ a j ∂ w i j \frac{\partial z_k}{\partial w_{ij}}=\frac{\partial z_k}{\partial a_j}\frac{\partial a_j}{\partial w_{ij}} wijzk=ajzkwijaj
公式就变为:
∂ L ∂ w i j = ∂ a j ∂ w i j ∑ k δ k w j k \frac{\partial L}{\partial w_{ij}}=\frac{\partial a_j}{\partial w_{ij}}\sum_k\delta_kw_{jk} wijL=wijajkδkwjk

这一部分的层次很重要,尤其是怎样把k层的内容转换到对j层求偏导上面,所以我认为这是这个公式推导的最核心的内容。

通过这样一步一步转换下去,就会又获得一个残差的形状的公式:
∂ L ∂ w i j = a j ( 1 − a j ) a i ∑ k δ k w j k \frac{\partial L}{\partial w_{ij}}=a_j(1-a_j)a_i\sum_k\delta_kw_{jk} wijL=aj(1aj)aikδkwjk
这里令
δ j = a j ( 1 − a j ) ∑ k δ k w j k \delta_j=a_j(1-a_j)\sum_k\delta_kw_{jk} δj=aj(1aj)kδkwjk
公式转换为:
∂ L ∂ w i j = δ j a i \frac{\partial L}{\partial w_{ij}}=\delta_ja_i wijL=δjai

嗯,大家看到这里估计也很不容易,但是我要说这么多,尤其是残差,估计大家也懂了,代码错就是错在残差上,我们现在来看代码:

# 后向传递, 取最新一次的最后一层的激活值
# [σ(wx + b) - y] * σ(wx + b), 阈值(偏置项), 前者是代价的导数
# 即偏导中c对a求偏导乘激活函数
delta = self.cost_derivative(activations[-1], y) * self.sigmoid(zs[-1])

# [σ(wx + b) - y] * σ(wx + b)作为这一层的偏置项???
nabla_b[-1] = delta
# {[σ(wx + b) - y] * σ(wx + b)} * 倒数第二层激活值的转置作为这一层的权重???
nabla_w[-1] = np.dot(delta, activations[-2].transpose())

for l in range(2, self.num_layers):
    z = zs[-1]
    sp = self.sigmoid_prime(z)  # 对sigmoid求导

    # delta = (最后一层权重的转置 点积 (C对a的偏导 * 激活函数)) * 激活函数的导数
    delta = np.dot(self.weights[-l + 1].transpose(), delta) * sp
    nabla_b[-1] = delta
    nabla_w[-1] = np.dot(delta, activations[-l - 1].transpose())

这个for循环里面就是对隐藏层求残差,对应于公式中的 ∂ L ∂ w i j = ∂ a j ∂ w i j ∑ k δ k w j k \frac{\partial L}{\partial w_{ij}}=\frac{\partial a_j}{\partial w_{ij}}\sum_k\delta_kw_{jk} wijL=wijajkδkwjk之后的内容,最后一层隐藏层的残差delta来源于输出层的残差delta(for外面那个delta,也就是公式中的 δ k \delta_k δk)。

这个cost_derivative()方法用于求“真实值与输出值的差的平方的导数”,即 ( 1 2 ( a k − y k ) 2 ) ′ (\frac{1}{2}(a_k-y_k)^2)^{'} (21(akyk)2)

那么对比上面输出层的残差公式,大家是不是一目了然,这里应该是乘self.sigmoid_prime(zs[-1]),而非self.sigmoid(zs[-1])

所以我和师兄商量的结果是,代码应该改为:

delta = self.cost_derivative(activations[-1], y) * self.sigmoid_prime(zs[-1])

写在最后

嗯,当然很有可能代码是对的,我和师兄看错了,如果我写的有错误的话,麻烦告知,我进行更正,谢谢~

参考

[1]涂铭,刘祥,刘树春.python自然语言处理实战核心技术与算法[M].机械工业出版社:北京,2018-4:204-216.
[2]周志华.机器学习[M].清华大学出版社:北京,2016:97-115.
[3]jsfantasy.深度学习之反向传播算法(BP)代码实现[EB/OL].https://www.cnblogs.com/jsfantasy/p/12177216.html,2020-1-10.
[4]jsfantasy.神经网络之反向传播算法(BP)公式推导(超详细)[EB/OL].https://www.cnblogs.com/jsfantasy/p/12177275.html,2020-11-10.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值