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

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

1、神经网络的代价函数

神经网络能够看作是复杂逻辑回归的组合,所以与其相似,咱们训练神经网络也要定义代价函数,以后再使用梯度降低法来最小化代价函数,以此来训练最优的权重矩阵。git

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 个样本:post

\[-\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 \]

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

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、神经网络编程细节

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

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

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

相关文章
相关标签/搜索