>
Home

登龙(DLonng)

选择大于努力

从 0 开始机器学习 - 神经网络反向 BP 算法!


版权声明:本文为 DLonng 原创文章,可以随意转载,但必须在明确位置注明出处!

最近一个月项目好忙,终于挤出时间把这篇 BP 算法基本思想写完了,公式的推导放到下一篇讲吧。

一、神经网络的代价函数

神经网络可以看做是复杂逻辑回归的组合,因此与其类似,我们训练神经网络也要定义代价函数,之后再使用梯度下降法来最小化代价函数,以此来训练最优的权重矩阵。

1.1 从逻辑回归出发

我们从经典的逻辑回归代价函数引出,先来复习下:

\[J(\theta) = \frac{1}{m}\sum\limits_{i = 1}^{m}{[-{y^{(i)}}\log ({h_\theta}({x^{(i)}}))-( 1-{y^{(i)}})\log ( 1 - h_\theta({x^{(i)}}))]} + \frac{\lambda}{2m} \sum\limits_{j=1}^{n}{\theta_j^2}\]

逻辑回归代价函数计算每个样本的输入与输出的误差,然后累加起来除以样本数,再加上正则化项,这个我之前的博客已经写过了:

这里补充一点对单变量逻辑回归代价函数的理解,虽然这一行代价公式很长:

\[cost(i) = -{y^{(i)}}\log ({h_\theta}({x^{(i)}}))-( 1-{y^{(i)}})\log ( 1 - h_\theta({x^{(i)}}))\]

但是其实可以把它简单的理解为输出与输入的方差,虽然形式上差别很大,但是可以帮助我们理解上面这个公式到底在计算什么,就是计算输出与输入的方差,这样理解就可以:

\[cost(i) = h_{\theta}(x^{(i)} - y^{(i)})^2\]

1.2 一步步写出神经网络代价函数

前面讲的简单逻辑回归的只有一个输出变量,但是在神经网络中输出层可以有多个神经元,所以可以有很多种的输出,比如 K 分类问题,神经元的输出是一个 K 维的向量:

因此我们需要对每个维度计算预测输出与真实标签值的误差,即对 K 个维度的误差做一次求和:

\[\sum\limits_{i = 1}^{k}{[-{y_k^{(i)}}\log ({h_\theta}({x^{(i)}}))_k-( 1-{y_k^{(i)}})\log ( 1 - h_\theta({x^{(i)}})_k)]}\]

然后累加训练集的 m 个样本:

\[-\frac{1}{m}[\sum\limits_{i = 1}^{m}\sum\limits_{k = 1}^{k}{[-{y_k^{(i)}}\log ({h_\theta}({x^{(i)}}))_k-( 1-{y_k^{(i)}})\log ( 1 - h_\theta({x^{(i)}})_k)]}]\]

再加上所有权重矩阵元素的正则化项,注意 $i, j$ 都是从 1 开始的,因为每一层的 $\theta_0$ 是偏置单元,不需要对其进行正则化:

\[\frac{\lambda}{2m}\sum\limits_{i = l}^{L - 1}\sum\limits_{i = 1}^{S_l}\sum\limits_{j = 1}^{S_l + 1}(\theta_{ji}^{(l)})^2\]
  • 最内层求和:循环一个权重矩阵所有的行,行数是 $S_l + 1$ 层激活单元数
  • 中间层求和:循环一个权重矩阵所有的列,列数是 $S_l$ 层激活单元数
  • 最外层求和:循环所有的权重矩阵

这就得到了输出层为 K 个单元神经网络最终的代价函数:

\[J(\theta) = -\frac{1}{m}[\sum\limits_{i = 1}^{m}\sum\limits_{k = 1}^{k}{[-{y_k^{(i)}}\log ({h_\theta}({x^{(i)}}))_k-( 1-{y_k^{(i)}})\log ( 1 - h_\theta({x^{(i)}})_k)]}] + \frac{\lambda}{2m}\sum\limits_{i = l}^{L - 1}\sum\limits_{i = 1}^{S_l}\sum\limits_{j = 1}^{S_l + 1}(\theta_{ji}^{(l)})^2\]

有了代价函数后,就可以通过反向传播算法来训练一个神经网络啦!

二、神经网络反向 BP(Back Propagation) 算法

2.1 BP 算法简介

之前写神经网络基础的时候,跟大家分享了如何用训练好的神经网络来预测手写字符:从 0 开始机器学习 - 神经网络识别手写字符!,只不过当时我们没有训练网络,而是使用已经训练好的神经网络的权重矩阵来进行前馈预测,那么我们如何自己训练神经网络呢?

这就需要学习反向 BP 算法,这个算法可以帮助我们求出神经网络权重矩阵中每个元素的偏导数,进而利用梯度下降法来最小化上面的代价函数,你可以联想简单的线性回归算法:从 0 开始机器学习 - 一文入门多维特征梯度下降法!,也是先求每个参数的偏导数,然后在梯度下降算法中使用求出的偏导数来迭代下降。

因此训练神经网络的关键就是:如何求出每个权重系数的偏导数?,反向 BP 就可以解决这个问题!这里强烈建议你学习的时候完全搞懂 BP 算法的原理,最好自己独立推导一遍公式,因为你以后学习深度学习那些复杂的网络,不管是哪种,最终都要使用反向 BP 来训练,这个 BP 算法是最核心的东西,面试也逃不过的,所以既然要学,就要学懂,不然就是在浪费时间。

2.2 BP 算法基本原理

我先用个例子简单介绍下 BP 算法的基本原理和步骤,公式的推导放到下一节,反向 BP 算法顾名思义,与前馈预测方向相反:

  • 计算最后一层输出与实际标签值的误差,反向传播到倒数第二层
  • 计算倒数第二层的传播误差,反向传播到倒数第三层
  • 以此类推,一层一层地求出各层的误差
  • 直到第二层结束,因为第一层是输入特征,不是我们计算的,所以不需要求误差

以下面这个 4 层的神经网络为例:

假如我们的训练集只有 1 个样本 $(x^{(1)}, y^{(1)})$,每层所有激活单元的输出用 $a^{(i)}$ 向量表示,每层所有激活单元的误差用 $\delta^{(i)}$ 向量表示,来看下反向传播的计算步骤(公式的原理下一节讲):

  1. 输出层的误差为预测值减去真实值:$\delta^{(4)} = a^{(4)} - y^{(1)}$
  2. 倒数第二层的误差为:$\delta^{(3)} = (W^{(3)})^T \delta^{(4)} * g’(z^{(3)})$
  3. 倒数第三层的误差为:$\delta^{(2)} = (W^{(2)})^T \delta^{(3)} * g’(z^{(2)})$
  4. 第一层是输入变量,不需要计算误差

有了每层所有激活单元的误差后,就可以计算代价函数对每个权重参数的偏导数,即每个激活单元的输出乘以对应的误差,这里不考虑正则化:

\[\frac {\partial}{\partial W_{ij}^{(l)}} J (W) = a_{j}^{(l)} \delta_{i}^{(l+1)}\]

解释下这个偏导数的计算:

  • $l$ 表示目前计算的是第几层
  • $j$ 表示当前层中正在计算的激活单元下标($j$ 作为列)
  • $i$ 表示下一层误差单元的下标($i$ 作为行)

这个计算过程是对一个样本进行的,网络的输入是一个特征向量,所以每层计算的误差也是向量,但是我们的网络输入是特征矩阵的话,就不能用一个个向量来表示误差了,而是应该也将误差向量组成误差矩阵,因为特征矩阵就是多个样本,每个样本都做一个反向传播,就会计算误差,所以我们每次都把一个样本计算的误差累加到误差矩阵中:

\[\Delta_{ij}^{(l)} = \Delta_{ij}^{(l)} + a_{j}^{(l)} \delta_{i}^{(l+1)}\]

然后,我们需要除以样本总数 $m$,因为上面的误差是累加了所有 $m$ 个训练样本得到的,并且我们还需要考虑加上正则化防止过拟合,注意对偏置单元不需要正则化,这点已经提过好多次了:

  • 非偏置单元正则化后的偏导数 $j \neq 0$:
\[D_{ij}^{(l)} = \frac {1}{m}\Delta_{ij}^{(l)}+\lambda W_{ij}^{(l)}\]
  • 偏置单元正则化后的偏导数 $j = 0$:
\[D_{ij}^{(l)} = \frac{1}{m}\Delta_{ij}^{(l)}\]

最后计算的所有偏导数就放在误差矩阵中:

\[\frac {\partial}{\partial W_{ij}^{(l)}} J (W) = D_{ij}^{(l)}\]

这样我们就求出了每个权重参数的偏导数,再回想之前的梯度下降法,我们有了偏导数计算方法后,直接送到梯度下降法中进行迭代就可以最小化代价函数了,比如我在 Python 中把上面的逻辑写成一个正则化梯度计算的函数 regularized_gradient,然后再用 scipy.optimize 等优化库直接最小化文章开头提出的神经网络代价函数,以此来使用反向 BP 算法训练一个神经网络:

import scipy.optimize as opt

res = opt.minimize(fun = 神经网络代价函数,
                       x0 = init_theta,
                       args = (X, y, 1),
                       method = 'TNC',
                       jac = regularized_gradient,
                       options = {'maxiter': 400})

所以神经网络反向 BP 算法关键就是理解每个权重参数偏导数的计算步骤和方法!关于偏导数计算公式的详细推导过程,我打算在下一篇文章中单独分享,本次就不带大家一步步推导了,否则内容太多,先把基本步骤搞清楚,后面推导公式就容易理解。

2.3 反向 BP 算法的直观理解

之前学习前馈预测时,我们知道一个激活单元是输入是上一层所有激活单元的输出与权重的加权和(包含偏置),计算方向从左到右,计算的是每个激活单元的输出,看图:

其实反向 BP 算法也是做类似的计算,一个激活单元误差的输入是后一层所有误差与权重的加权和(可能不包含偏置),只不过这里计算的反向是从右向左,计算的是每个激活单元的误差,对比看图:

你只需要把单个神经元的前馈预测和反向 BP 的计算步骤搞清楚就可以基本理解反向 BP 的基本过程,因为所有的参数都是这样做的。

三、神经网络编程细节

3.1 随机初始化

每种优化算法都需要初始化参数,之前的线性回归初始化参数为 0 是没问题的,但是如果把神经网络的初始参数都设置为 0,就会有问题,因为第二层的输入是要用到权重与激活单元输出的乘积:

  • 如果权重都是 0,则每层网络的输出都是 0
  • 如果权重都是相同的常数 $a$,则每层网络的输出也都相同,只是不为 0

所以为了在神经网络中避免以上的问题,我们采用随机初始化,把所有的参数初始化为 $[-\epsilon, \epsilon]$ 之间的随机值,比如初始化一个 10 X 11 的权重参数矩阵:

\[initheta = rand(10, 11) * (2 * \epsilon) - \epsilon\]

3.2 矩阵 <-> 向量

注意上面优化库的输入 X0 = init_theta 是一个向量,而我们的神经网络每 2 层之间就有一个权重矩阵,所以为了把权重矩阵作为优化库的输入,我们必须要把所有的权重参数都组合到一个向量中,也就是实现一个把矩阵组合到向量的功能,但是优化库的输出也是一个包含所有权重参数的向量,我们拿到向量后还需要把它转换为每 2 层之间的权重矩阵,这样才能进行前馈预测:

  • 训练前:初始多个权重矩阵 -> 一个初始向量
  • 训练后:一个最优向量 -> 多个最优权重矩阵

3.3 梯度校验

梯度校验是用来检验我们的 BP 算法计算的偏导数是否和真实的偏导数存在较大误差,计算以下 2 个偏导数向量的误差:

  • 反向 BP 算法计算的偏导数
  • 利用导数定义计算的偏导数

对于单个参数,在一点 $\theta$ 处的导数可由 $[\theta - \epsilon, \theta + \epsilon]$ 表示,这也是导数定义的一种:

\[grad = \frac{J(\theta + \epsilon) - J(\theta - \epsilon)}{2 \epsilon}\]

如图:

但是我们的神经网络代价函数有很多参数,当我们把参数矩阵转为向量后,可以对向量里的每个参数进行梯度检验,只需要分别用定义求偏导数即可,比如检验 $\theta_1$:

\[\frac {\partial J}{\partial \theta_1} = \frac {J (\theta_1 + \varepsilon_1, \theta_2, \theta_3 ... \theta_n ) - J(\theta_1 - \varepsilon_1, \theta_2, \theta_3 ... \theta_n)}{2 \varepsilon}\]

以此类推,检验 $\theta_n$:

\[\frac {\partial J}{\partial \theta_n} = \frac {J (\theta_1, \theta_2, \theta_3 ... \theta_n + \varepsilon_n) - J(\theta_1, \theta_2, \theta_3 ... \theta_n - \varepsilon_n)}{2 \varepsilon}\]

求出导数定义的偏导数后,与 BP 算法计算的偏导数计算误差,在误差范围内认为 BP 算法计算的偏导数(D_vec)是正确的,梯度检验的伪代码如下:

for i = 1 : n
  theta_plus = theta
  theta_plus[i] = theta_plus + epsilon
  
  theta_minu = theta
  theta_minu[i] = theta_minu - epsilon
  
  grad = (J(theta_plus) - J(theta_minu)) / (2 * epsilon)
end

check 误差: grad 是否约等于 D_vec

注意一点:梯度检验通常速度很慢,在训练神经网络前先别进行检验!

今天就到这,溜了溜了,下篇文章见:)

本文原创首发于微信公号「登龙」,分享机器学习、算法编程、Python、机器人技术等原创文章,扫码即可关注

DLonng at 06/17/20