ML学习笔记 #07 神经网络:反向传播
本文最后更新于:2023年1月5日 凌晨
上一节介绍了神经网络模型可以利用前向传播做预测,并引入了一系列符号来描述神经网络模型:
- \(x_i\) 表示输入层的第 \(i\) 个输入特征,其中 \(x_0\) 偏置项省略;
- \(a^{(j)}_i\) 表示第 \(j\) 层的第 \(i\) 个激活单元,其中 \(a_0^{(j)}\) 偏置单元省略;
- \(s_j\) 表示第 \(j\) 层的神经元数量(不包含偏置项),\(\hat a^{(j)}\in\mathbb R^{s_j+1}\) 表示加上偏置项后的 \(a^{(j)}\);
- \(\Theta^{(j)}\in\mathbb R^{s_{j+1}\times(s_j+1)}\) 表示第 \(j\) 层到第 \(j+1\) 层的权重矩阵,\(\Theta\) 表示所有权重矩阵的集合。
其中的权重参数,与前面的回归问题类似,需要通过最小化代价函数来逼近。
代价函数
为了描述代价函数,我们引入新的符号:
- \(L\) 表示神经网络的层数,包含输入层、隐藏层、输出层,因此共有 \(L-1\) 次传播;
- \(K\geqslant 3\) 表示多分类问题的类别数,相应的有 \(s_L = K,\;\;h_{\Theta}(x)\in\mathbb R^{K}\);
- \(h_\Theta(x^{(i)})\in\mathbb R^K\),表示从第 \(i\) 个样本经过神经网络得到的输出结果,\(h_\Theta(x^{(i)})_k\) 表示其第 \(k\) 维。
回顾正则化逻辑回归中的交叉熵代价函数: $$ J()=-_{i=1}m{}+{j=1}^n{{j}{2}}\
$$
在神经网络中,我们沿用交叉熵代价函数而不是均方误差,原因如下:
- 我们使用了 \(\text{sigmoid}\) 函数作为激活函数引入非线性,其单增 \(S\) 形曲线使其在 \(y=0\) 和 \(1\) 附近导数较小;
- 通常我们会随机初始化参数后进行梯度下降,如果使用均方误差代价函数,对 \(\theta\) 求偏导后含有 \(\text{sigmoid}\) 的导数项,会使前期的梯度下降十分缓慢;
- 如果使用交叉熵代价函数,则求偏导后不含 \(\text{sigmoid}\) 的导数项,前期收敛更快。
具体地,对于一个 \(K\) 分类的神经网络而言,可以看作是 \(K\) 个二分类的交叉熵代价函数之和: \[ J(\Theta )=-\frac{1}{m}\left[ \sum_{i=1}^m{\sum_{k=1}^K{y_{k}^{(i)}}}\ln \left( h_{\Theta}(x^{(i)})_k \right) +(1-y_{k}^{(i)})\ln \left( 1-h_{\Theta}(x^{(i)})_k \right) \right] +\frac{\lambda}{2m}\sum_{l=1}^{L-1}{\sum_{i=1}^{s_l}{\sum_{j=1}^{s_{l+1}}{\left( \Theta _{ji}^{(l)} \right) ^2}}} \]
注意这里有一个符号混用的地方:上标 \(^{(i)}\) 在 \(x^{(i)}\) 和 \(y^{(i)}\) 中表示第 \(i\) 个数据的输入(\(\in\mathbb R^{n+1}\))和输出(\(\in\mathbb R^{K}\)),但是在 \(\Theta^{(l)}\) 中表示第 \(l\) 层的 \(\Theta\)(\(\in \mathbb R^{s_{l+1}\times (s_l+1)}\)).
反向传播算法 | Back Propagation
现在我们的目标就是最小化代价函数 \(J(\Theta)\),并找到使之最小的参数 \(\Theta\),我们仍使用梯度下降法解决最小化问题(尽管 \(J(\Theta)\) 可能为非凸函数)。因此需要计算梯度 \(\begin{aligned} \frac{\partial J}{\partial \Theta_{ji}^{(l)}} \end{aligned}\),而计算的关键就是「反向传播」:从输出层开始逐层向前计算。
为什么需要「反向传播」?我们知道梯度反映的是 \(J\) 在 \(\Theta_{ji}^{(l)}\) 方向的变化率,即:当参数发生微小增量时,代价函数的增量。对于前几层网络,参数的增量需要经过多层全连接传播,才会影响到输出(代价函数)。
如果我们从输入层开始计算每个参数的梯度,则每个参数都要依赖其后续路径的传播,每个参数的计算都要先算一遍后续路径的梯度。而如果从输出层开始逐层向前计算并存储了梯度,则解耦了依赖关系,前一层可以利用后一层的计算结果。有点类似「动态规划」中「递归求解子问题」的思想。
数学推导:问题转化
为了推导方便,先假设只有一组数据 \((x,y)\),这样可以省去上标和求和的麻烦。并且令 \(\lambda = 0\) 省略正则化项,等到最后的偏导结果再加上。
我们首先对第 \(l+1\) 层的第 \(j\) 个神经元(偏置单元除外)定义一个 \(\text{delta}\) 误差: \[ \delta_j^{(l+1)}=\frac{\partial J}{\partial z_j^{(l+1)}} \] 其中 \(z_j^{(l+1)}=\sum\limits_{k=0}^{s_{l}}\Theta^{(l)}_{jk}a^{(l)}_{k}\),即 \(a_j^{(l+1)}\) 神经元取 \(\text{sigmoid}\) 激活前的输出值。换句话说,\(a^{(l+1)}_j=g\left(z_j^{(l+1)}\right)\)。
于是根据链式求导法则,我们有: \[ \frac{\partial J}{\partial \Theta^{(l)}_{ji}}=\frac{\partial J}{\partial z_j^{(l+1)}}\cdot\frac{\partial z_j^{(l+1)}}{\partial\Theta_{ji}^{(l)}}=\delta_j^{(l+1)}a_i^{(l)} \] 写作矩阵形式: \[ \boxed{\Delta ^{\left( l \right)}=\delta ^{\left( l+1 \right)}\cdot \left( a^{\left( l \right)} \right) ^T}\tag{1} \] 显然,一旦求出当前层的 \(\text{delta}\) 误差,那么传入当前层的各参数的梯度也可求出。所以现在问题转化为求解 \(\delta^{(l+1)}\)。
数学推导:输出层误差
既然要递归求解子问题,首先就是要算输出层,即 \(\delta_j^{(L)}\): \[ \begin{aligned} \delta _{j}^{\left( L \right)}&=\frac{\partial J}{\partial z_{j}^{\left( L \right)}}=\frac{\partial J}{\partial a_{j}^{\left( L \right)}}\cdot \frac{\partial a_{j}^{\left( L \right)}}{\partial z_{j}^{\left( L \right)}}\\ &=\frac{\partial J}{\partial a_{j}^{\left( L \right)}}\cdot g'\left( z_{j}^{\left( L \right)} \right)\\ &=-\left( \frac{y_j}{a_{j}^{\left( L \right)}}-\frac{1-y_j}{1-a_{j}^{\left( L \right)}} \right) \cdot \left( a_{j}^{\left( L \right)}\left( 1-a_{j}^{\left( L \right)} \right) \right)\\ &=a_{j}^{\left( L \right)}-y_j\\ \end{aligned} \]
写作矩阵形式: \[ \boxed{\delta^{(L)}=a^{(L)}-y}\tag{2} \]
此处推导过程的一些注释:
关于第二行,请注意:$a^{( L )}=g( z^{( L )} ) $;
关于第三行,请注意 \(\text{sigmoid}\) 函数的性质:\(g'(z)=g(z)(1-g(z))\);
以及第三行偏导项的计算,请注意 \(a^{(L)}=h_\Theta(x)\),所以 \(J(\Theta)\) 在现在的假设条件下可以写作: \[ J(\Theta)=-\left[\sum_{k=1}^Ky_k\ln(a^{(L)}_k)+(1-y_k)\ln(1-a_k^{(L)})\right] \]
数学推导:隐藏层误差
下面计算第 \(l\) 层(\(2\leqslant l<L\))的 \(\delta_j^{(l)}\): \[ \begin{aligned} \delta _{j}^{\left( l \right)}&=\frac{\partial J}{\partial z_{j}^{\left( l \right)}}\\ &=\frac{\partial J}{\partial z^{\left( l+1 \right)}}\cdot \frac{\partial z^{\left( l+1 \right)}}{\partial a_{j}^{\left( l \right)}}\cdot \frac{\partial a_{j}^{\left( l \right)}}{\partial z_{j}^{\left( l \right)}}\\ &={\delta ^{\left( l+1 \right)}}^T\cdot \Theta _{\cdot ,j}^{\left( l \right)}\cdot g'\left( z_{j}^{\left( l \right)} \right)\\ &={\delta ^{\left( l+1 \right)}}^T\cdot \Theta _{\cdot ,j}^{\left( l \right)}\cdot \left( a_{j}^{\left( l \right)} \left( 1-a_{j}^{\left( l \right)} \right) \right)\\ \end{aligned} \] 观察式子的前半部分,可以看出下一层所有结点的 \(\text{delta}\) 误差均会影响当前层的任一结点,而影响则是通过该结点链出的权重参数反向传播。
转置后,写作矩阵形式: \[ \boxed{\delta ^{\left( l \right)}=\left( {\Theta ^{\left( l \right)}}^T\delta ^{\left( l+1 \right)} \right) \odot \left( a^{\left( l \right)}\odot \left( 1-a^{\left( l \right)} \right) \right) }\tag{3} \] 其中 \(\odot\) 表示 Hadmard 积,即两个向量对应位置相乘。
步骤总结
以上是对一组数据的推导,我们得到了三个重要的结果 \((1)(2)(3)\): \[ \boxed{ \begin{aligned} \Delta ^{\left( l \right)}&=\delta ^{\left( l+1 \right)}\cdot \left( a^{\left( l \right)} \right) ^T&&1\leqslant l<L\\ \delta ^{\left( L \right)}&=a^{\left( L \right)}-y\\ \delta ^{\left( l \right)}&=\left( {\Theta ^{\left( l \right)}}^T\delta ^{\left( l+1 \right)} \right) \odot \left( a^{\left( l \right)}\odot \left( 1-a^{\left( l \right)} \right) \right) &&2\leqslant l<L\\ \end{aligned} } \] 而 \(m\) 组数据只需要在一些地方进行累加即可。设数据集为 \(\left\{ \left( x^{\left( i \right)},y^{\left( i \right)} \right) \mid 1\leqslant i\leqslant m \right\}\),则反向传播算法的步骤如下:
所有 \(\Delta^{(l)}\) 置零;
遍历数据集,设当前数据为 \((x^{(i)},y^{(i)})\):
- 以 \(x^{(i)}\) 为输入做前向传播,得到输出 \(a^{(L)}\);
- 公式 \((2)\) 计算输出层误差,公式 \((3)\) 计算隐藏层误差;
- 公式 \((1)\) 更新各层的 \(\Delta^{(l)}\) 矩阵,进行累加:\(\Delta^{(l)}:=\Delta^{(l)}+\delta ^{\left( l+1 \right)}\cdot \left( a^{\left( l \right)} \right)^T\);
计算 \(D\) 矩阵,对偏置项以外的参数进行正则化: \[ D_{ij}^{(l)}:=\begin{cases} \frac{1}{m}\left( \Delta _{ij}^{(l)}+\lambda \Theta _{ij}^{(l)} \right)& \mathrm{if}\; j\ne 0\\ \frac{1}{m}\Delta _{ij}^{(l)}& \mathrm{if}\; j=0\\ \end{cases} \] 这就是最终的梯度矩阵:\(\begin{aligned}\frac{\partial J}{\partial \Theta_{ij}^{(l)}}=D^{(l)}_{ij}\end{aligned}\)。
现在,我们可以用 \(D_{ij}^{(l)}\) 做一次梯度下降了,整个步骤称为一代(epoch)。注意,我们的参数 \(\Theta^{(l)}\) 应该初始化为 \([-\epsilon,\epsilon]\) 中的随机值。
代码实现
参数展开 | Unrolling Parameters
实现过程中,由于每一层都有一个 \(\Theta^{(l)}\) 和 \(D^{(l)}\) 矩阵,总共有 \(2 \times L\) 个矩阵,为了方便存储和计算,我们最好都转换为向量,并将各层的向量拼接成一个长向量。这样做可以方便调用封装好的高级优化函数接口,如 fminunc
或 scipy.optimize.minimize
。
同样,函数输出的结果向量也可以通过 reshape
转为矩阵,再应用于前向传播。
梯度检验 | Gradient Checking
当我们为一个较为复杂的模型(例如神经网络)实现梯度下降算法时,可能会出现一些不易察觉的错误,导致虽然代价看上去在不断减小,但最终的结果可能并不是最优解,误差可能高出一个量级。
为了避免这样的问题,可采取梯度的数值检验方法(Numerical Gradient Checking):通过数值计算得到梯度的近似值,再和反向传播的结果比对,检验是否接近。
在任意一代计算后,得到一组 \(\theta\) 向量及 \(D\) 矩阵,通过差商近似 \(J(\theta)\) 的各偏导: \[ \frac{\partial}{\partial \theta_k}J(\theta)\approx\frac{J(\theta_1,\cdots,\theta_k+ \epsilon,\cdots,\theta_n)-J(\theta_1,\cdots,\theta_k- \epsilon,\cdots,\theta_n)}{2\epsilon} \]
其中,\(\epsilon\) 是非常小的常数,为了避免计算误差,通常选取 \(10^{-4}\)。现在我们可以比较这些偏导估计值与对应位置 \(D_{ij}^{(l)}\) 的值,它们应该非常接近。
注意:梯度检验耗时巨大,复杂度远大于反向传播,一旦验证了神经网络反向传播的代码正确后,不应进行梯度检验(删掉 or 注释掉)。
参数随机初始化
前文我们提到参数 \(\Theta^{(l)}\) 应该初始化为 \([-\epsilon,\epsilon]\) 中的随机值,而不是简单初始化为全零。如果将 \(\Theta^{(l)}\) 初始化为全零,则下一层的 \(z^{(l+1)}\) 也为全零,则 \(a^{(l+1)}\) 就全为 \(0.5\),且所有隐藏层都会得到这个结果;与此同时,反向传播公式 \((3)\) 中,由输出层到隐藏层时 \(\text{delta}\) 误差也会清零。这将导致网络无法传播!
那么,如果将参数 \(\Theta^{(l)}\) 全都初始化为同一个非零常量呢?由于全连接,同一层的 \(a^{(l+1)}\) 将为相同值;在反向传播公式 \((3)\) 中,由输出层到隐藏层时 \(\text{delta}\) 误差也会变成相同值,则所有的 \(D^{(l)}\) 矩阵也为常数矩阵,参数 \(\Theta^{(l)}\) 将再次被更新为新的相同常量。这样下去所有的激活单元都成了摆设,因为每一层都在重复计算着相同特征。
上述问题也被称为对称权重问题(Symmetric Weights),解决方法就是随机初始化,实现时可以将 \([0,1]\) 的随机数映射到 \([-\epsilon,\epsilon]\),这里的 \(\epsilon\) 与梯度检验中的无关。
实际应用中 \(\epsilon\) 的选取,通常与 \(\Theta^{(l)}\) 前后两层的神经元个数有关,一种常用的有效的取值是: \[ \epsilon ^{\left( l \right)}=\frac{\sqrt{6}}{\sqrt{s^{\left( l \right)}+s^{\left( l+1 \right)}}} \]
练习
下面以 Coursera 上的多分类数据集 ex4data1.mat
为例,这是一个手写数字的数据集,与上一节的数据集类似。共 \(5000\) 组数据,每组数据输入是一个由 \(20\times20\) 灰度矩阵压缩而来的 \(400\) 维向量,输出是 \(0\) 到 \(9\) 之间的整数。
题目还提供一组训练好的权重参数 ex4weight.mat
,用以检验前向传播和代价函数的正确性,此处跳过。采用题目推荐的网络层数 \(L=3\),则输入层有 \(401\) 个单元(含偏置项),隐藏层有 \(26\) 个单元(含偏置项) ,输出层有 \(10\) 个单元(独热编码)。
先进行预处理,包括数据预处理、随机初始化参数:
1 |
|
再实现前向传播与代价函数计算,代价函数这里用于绘图、梯度检验:
1 |
|
实现反向传播算法,注意这里偏置单元的 \(\text{delta}\) 误差没有意义,算出的值要忽略:
1 |
|
实现梯度下降算法并检验结果,大约耗时 20 分钟:
1 |
|
如果要进行梯度检验,只需要将下述函数插入到 D = backProp()
后即可,正式训练时删去:
1 |
|
5000 轮次后,最终的预测结果如下:
1 |
|