在学习多层网络之前,我们先把目光放回单层神经网络:
单层神经网络
下图是一个单层神经网络的示意图:

在每一个训练epoch中,我们不是凭感觉调整权重,而是通过计算代价函数对每个权重的偏导数,沿着梯度的方向迈出一小步,让模型的预测误差逐渐变小。这个更新规则看起来简单,但它背后有一个重要前提:所有权重必须同时更新,如果用"算一个改一个"的在线方式,梯度方向就会被污染,导致收敛不稳定。
而且看看上面的图,在单层神经网络中,我们首先拥有 $x_m$ 个输入参数,每个参数都通过 $w_m$ 连接到 Net Input 函数,随后通过激活函数得到输出 $y$。这个结构虽然简单,但它的表达能力有限,象形组合本身只能够捕捉并表达线性关系,而无法捕捉更复杂的非线性关系。
因此,我们想到了一个改进方式:使用一个非线性激活函数 $\phi(z)$ 来引入表达能力。
还记得具体每个epoch是怎么计算的吗?我们计算权重:
$$\mathbf{w := w + \delta w}$$
这个向量 $w$ 是所有权重的集合,而 $\delta w$ 是每个权重的更新量。这个更新量是通过计算代价函数 $J(w)$ 对每个权重的偏导数得到的:
$$\delta w = -\eta \nabla J(w)$$
在梯度下降优化中,我们会计算代价函数 $J(w)$ 对每个权重 $w$ 的偏导数 $\nabla J(w)$,然后沿着负梯度的方向更新权重。这里的 $\eta$ 是学习率,控制了每次更新的步长。
我们定义激活函数为:
$$\phi(z) = z = a$$
净输入 $z$ 是输入 $x$ 和权重 $w$ 的线性组合:
$$z = \sum_{j} w_j x_j = w^T x$$
所以,虽然我们在计算梯度更新的时候引入了非线性激活函数 $\phi(z)$,在输出层我们使用阈值函数,来将连续的数值输出转换为离散的类别标签:
$$\hat{y} = \begin{cases} 1 & \text{if } g(z) \geq 0 \ -1 & \text{otherwise} \end{cases}$$
最后在训练阶段,我们也会使用随机梯度下降 SGD 来更新权重。批量梯度下降每次计算的时候都会使用全量数据,而随机梯度下降则在每次计算梯度的时候只使用一个样本或者一小批样本。这样可以加速训练过程,并且由于引入了噪声,可能有助于跳出局部最优解。
这些内容之前应该都是提过的了,所以如果你觉得这方面的知识还是很抽象,可以回去看看之前的章节。
多层网络的结构
一个典型的 多层全连接网络 使用多重感知机(Multilayer Perceptron, MLP)作为基本构建块。MLP 由多个层次组成,每一层都包含若干个神经元,这些神经元通过权重连接到下一层的神经元。
整体来说,多层网络的结构可以看成由 输入层,隐藏层 和 输出层 堆叠而成。每一层都由多个神经元组成,层与层之间通过权重矩阵 $W$ 和偏置向量 $b$ 实现全连接,并通过非线性激活函数 $\sigma$ 引入非线性变换。
整个流程就差不多是:
输入 $x$ → [线性变换] → $z$ → [非线性激活函数] → $a$ → ... → 输出 $\hat{y}$
你可以对照下图:

在理解结构之前,我们需要了解描述网络内部结构的语言。我们记当前的层为 $l$,则:
$a^{(l-1)}$ 是第 $l-1$ 层的激活输出,也就是第 $l$ 层的输入。
$W^{(l)}$, $b^{(l)}$ 是第 $l$ 层的权重和偏置。
$z(l) = W^{(l)} a^{(l-1)} + b^{(l)}$ 是第 $l$ 层的线性变换结果。也就是喂给激活函数之前的输入。
$a^{(l)} = \sigma(z^{(l)})$ 是第 $l$ 层的激活输出,也就是第 $l+1$ 层的输入。
对于输入层,$a^{(0)} = x$;而对于输出层,$a^{(L)}$ 就等于预测值 $\hat{y}$。
另外:
- $m$ 是输入层的节点数,也就是输入特征的维度。
- $d$ 是隐藏层的节点数,也就是每一层的神经元数量。
- $t$ 是输出层的节点数,也就是输出类别的数量。
- $L$ 是除去输入层的层数,也就是隐藏层+输出层的数量。
你可以看到最左边是 1st layer,我们叫输入层。最右边是输出层。它们之间的层叫做隐藏层。每一层之间,我们使得所有神经元都与下一层的神经元全连接,也就是当前层每一个节点都会与下一层所有节点相连,除了偏置节点。
那么什么是偏置?你发现似乎每一层的第一个节点都是 1,这个节点叫做偏置节点(bias node),它的作用是引入一个常数项,帮助模型更好地拟合数据。这个偏置节点的权重就是我们之前提到的 $b$。之所以偏置能够帮助模型拟合数据,是因为它允许模型在没有输入的情况下也能产生输出,从而增加了模型的灵活性。
而深度人工神经网络的定义也是很有意思,这里深度不代表绝对层数,而是相对于传统浅层模型而言的。
一般来说,当网络 超过一个隐藏层 的时候,我们就可以称之为深度神经网络了。
多层网络的训练
多层网络的训练过程被分为三个阶段,分别是 前向传播 (Foward Propagate),计算误差 (Calculate Error) 反向传播 (Backpropagate)。
接下来我们一个一个来看,跟着MLP的训练流程走一遍:
前向传播
前向传播是网络逐层计算的过程,从输入层开始,通过每一层的线性变换和激活函数,最终得到输出层的预测结果。
为了你方便对照,我把上图贴下来:

假设我们有一个 $L$ 层的全连接网络,结构为:
- 输入层:$a^{(0)} = x$,维度为 $m$。
- 隐藏层:从第一层到第 $L-1$ 层,每层 $l$ 的激活输出为 $a^{(l)}$,维度为 $d$。
- 输出层:第 $L$ 层的激活输出为 $a^{(L)}$,维度为 $t$。
每一层会先做线性变换,然后通过激活函数做非线性映射。我们首先统一一下符号:
- $a^{(0)} = x$ 是原始输入向量,维度为 $(n_x, 1)$,或者是 $m$。
- $W^{(l)}$ 是第 $l$ 层的权重矩阵,维度为 $(n_l, n_{l-1})$,其中 $n_l$ 是第 $l$ 层的神经元数量。
- $b^{(l)}$ 是第 $l$ 层的偏置向量,维度为 $(n_l, 1)$。
- $z^{(l)}$ 是第 $l$ 层的净输入,也即是线性变换的结果,维度为 $(n_l, 1)$。
- $a^{(l)}$ 是第 $l$ 层的激活输出,维度为 $(n_l, 1)$。
- $\sigma^{(l)}(\cdot)$ 是第 $l$ 层的激活函数。
那么假设我们传入一个训练样本,前向传播的计算从输入层开始,逐层递推。
对于每一层 $l = 1, 2, ..., L$,我们首先计算线性变换
- 线性组合
$$z^{(l)} = W^{(l)}a^{(l-1)} + b^{(l)}$$
这里 $W^{(l)}a^{(l-1)}$ 表示矩阵乘法,将上一层的输出 $a^{(l-1)}$ 通过权重矩阵 $W^{(l)}$ 进行线性变换,得到一个新的向量。然后加上偏置向量 $b^{(l)}$,得到当前层的净输入 $z^{(l)}$。
- 非线性激活
算出来 $z^{(l)}$ 之后,按照流程,我们应该计算它的激活输出 $a^{(l)}$,也就是通过激活函数 $\sigma^{(l)}$ 进行非线性变换:
$$a^{(l)} = \sigma^{(l)}(z^{(l)})$$
激活函数 $\sigma^{(l)}$ 对 $z^{(l)}$ 的每一个元素单独作用,得到 $a^{(l)}$ 的每一个元素。
当 $l = L$ 的时候,我们之前也说到,最终的 $a^{(L)}$ 就是当前网络对于输入 $x$ 的预测输出,我们记作 $\hat{y} = a^{(L)}$。
在整个前向传播过程中,参数 $W$, $b$ 是固定的,而它们的值要么来自初始化的随机值,要么来自之前的训练更新。输入 $x$ 会沿着网络逐层传播,正向流动,最后产生预测值。
- 选择激活函数
不同层数的激活函数可以不同,这就是为什么我们在上面加了一个 $l$ 的下标。
对于隐藏层来说,常用的激活函数有:
ReLU(Rectified Linear Unit):$\sigma(z) = \max(0, z)$,在正数部分保持线性,在负数部分输出0,计算简单且能够缓解梯度消失问题,因此在深度网络中非常流行。
Leaky ReLU:$\sigma(z) = \max(0.01z, z)$,我们在负数部分引入一个小的斜率,避免了ReLU在负数部分完全没有梯度的问题。
tanh(双曲正切函数):$\sigma(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}}$,输出范围在 -1 到 1 之间,适用于需要输出负数的情况。
Sigmoid:$\sigma(z) = \frac{1}{1 + e^{-z}}$,输出范围在 0 到 1 之间,适用于二分类问题的输出层。但是现在较少用于隐藏层,因为它容易导致梯度消失问题。
至于梯度消失,我们一会会深度讨论。
而对于输出层来说,激活函数的选择通常取决于具体的任务:
例如如果我们选择二分类任务,那么输出层的激活函数通常是 Sigmoid,因为它能够将输出压缩到 0 和 1 之间,表示概率。
如果是多分类问题,那么输出层的激活函数通常是 Softmax:
$$a_i^{(L)} = \frac{e^{z_i^{(L)}}}{\sum_{j=1}^t e^{z_j^{(L)}}}$$
因为我们要输出一个概率分布向量,要求所有的输出值都在 0 和 1 之间,并且它们的和为 1。
如果是回归问题,我们需要输出的是连续的实数值。那么直接选择恒定函数 $\sigma(z) = z$ 作为输出层的激活函数就可以了。
矩阵维度
我们不妨追溯一下计算过程中的维度变化:
输入层:$a^{(0)} = x$,维度 $n_x \times 1$
权重 $W^{(1)}$维度: $n_1 \times n_x \rightarrow z^{(1)}$,维度 $n_1 \times 1$
权重 $W^{(2)}$维度: $n_2 \times n_1 \rightarrow z^{(2)}$,维度 $n_2 \times 1$
...
以此类推,最后一层的 $W^{(L)}$ 维度是 $n_L \times n_{L-1}$,输出层的 $z^{(L)}$ 维度是 $n_L \times 1$,也就是输出层的神经元数量。而 $a^{(l)}$ 的维度始终与 $z^{(l)}$ 相同。
而在过程中,我们运用得矩阵乘法规则能够始终保证维度匹配:
$$(n_l \times n_{l-1}) \cdot (n_{l-1} \times 1) = n_l \times 1$$
这也是为什么我们需要仔细设计每一层的神经元数量的原因。
批处理与向量化
但是在实际训练的时候,我们不会像刚才那样,一次处理一个样本,而是一次处理一个小批次 (batch) 的样本,将 $m$ 个样本堆叠成矩阵,这样就可以充分利用矩阵运算的并行性。
首先从输入开始。由于之前的输入 $x$ 是一个 $n_x \times 1$ 的向量,现在我们将 $m$ 个样本堆叠成一个矩阵 $X$,维度为 $n_x \times m$。每一列都代表一个样本。
而权重矩阵 $W^{(l)}$ 的维度仍然是 $n_l \times n_{l-1}$,偏置向量 $b^{(l)}$ 的维度是 $n_l \times 1$。
这样,线性输出的前向传播公式就变为:
$$Z^{(l)} = W^{(l)} A^{(l - 1)} + b^{(l)}$$
而激活输出就是套个壳:
$$A^{(l)} = \sigma^{(l)}(Z^{(l)})$$
这种向量化的写法与单样本形式完全一致,实际上就是小写换大写,维度也从 $n_l \times 1$ 变成了 $n_l \times m$。每一列仍然代表一个样本的输出.
咱们来做个例子?
一个二分类网络,输入两个特征,网络结构为:
输入层 (2) → 隐藏层 (2个神经元,Sigmoid激活) → 输出层 (1个神经元,Sigmoid激活)
给定参数:
$$W^{(1)} = \begin{bmatrix} 0.2 & 0.4 \\ 0.3 & -0.5 \end{bmatrix}$$
$$b^{(1)} = \begin{bmatrix} 0.1 \\ -0.2 \end{bmatrix}$$
$$W^{(2)} = \begin{bmatrix} 0.6 \\ -0.1 \end{bmatrix}$$
$$b^{(2)} = \begin{bmatrix} 0.3 \end{bmatrix}$$
输入样本:
$$x = a^{(0)} = \begin{bmatrix} 0.5 \\ 0.1 \end{bmatrix}$$
第一层计算:
线性组合:
$$z^{(1)} = W^{(1)}x + b^{(1)} = \begin{bmatrix} 0.2 & 0.4 \\ 0.3 & -0.5 \end{bmatrix} \begin{bmatrix} 0.5 \\ 0.1 \end{bmatrix} + \begin{bmatrix} 0.1 \\ -0.2 \end{bmatrix} = \begin{bmatrix} 0.24 \\ -0.10 \end{bmatrix}$$Sigmoid激活:
$$a_1^{(1)} = \frac{1}{1 + e^{-0.24}} \approx \frac{1}{1 + 0.7866} \approx 0.560$$$$a_2^{(1)} = \frac{1}{1 + e^{0.10}} \approx \frac{1}{1 + 1.1052} \approx 0.475$$
所以:
$$a^{(1)} = \begin{bmatrix} 0.560 \\ 0.475 \end{bmatrix}$$
第二层计算:
$$z^{(2)} = W^{(2)}a^{(1)} + b^{(2)} = \begin{bmatrix} 0.6 \\ -0.1 \end{bmatrix} \begin{bmatrix} 0.560 \\ 0.475 \end{bmatrix} + \begin{bmatrix} 0.3 \end{bmatrix} = \begin{bmatrix} 0.5885 \end{bmatrix}$$
- Sigmoid激活:
$$\hat{y} = a^{(2)} = \sigma(z^{(2)}) = \frac{1}{1 + e^{-0.5885}} \approx \frac{1}{1 + 0.555} \approx 0.643$$
因此,对于输入 $x$,网络给出的预测输出 $\hat{y}$ 约为 0.643,表示属于正类的概率。
在前向传播计算过程中,我们不仅要得到预测值,还必须缓存一些中间值,因为后面的反向传播会用到这些数据。我们通常需要缓存每一层的:
$z^{(l)}$,这是未被激活函数处理的线性输出,当我们求激活函数的导数时需要用到它。
$a^{(l)}$,这是每一层的激活输出,在反向传播中计算下一层误差以及权重梯度时需要用到它。
在上面的例子中,缓存的就是 $z^{(1)}$, $a^{(1)}$, $z^{(2)}$, $a^{(2)}$ 这些向量。
损失函数与误差计算
当我们前向传播得到预测值 $\hat{y}$ 之后,下一步需要进行误差计算。我们要找到,模型输出的预测与真实答案差了多少?一旦找到了这个差距,我们就可以通过反向传播来调整权重,减少这个差距。
误差计算通过定义一个 损失函数 (Loss Function) $J$ 来实现,它的值是一个标量,衡量了预测值 $$\hat{y}$$ 与真实标签 $y$ 之间的差距。损失函数的选择取决于具体的任务类型:
如果得出的损失越小,那么代表你模型预测的结果就越接近真实标签,模型的性能就越好。
损失对输出层输入的导数 $\frac{\partial J}{\partial z^{(L)}}$ 将会是反向传播的起始误差信号。通常,我们会根据之前整个训练集或者每一个batch的损失取平均,来得到一个更稳定的误差信号。但在数学推导中,我们往往会先分析单个样本,然后拓展到批量处理。
回归任务
对于回归任务,也就是像房价,温度等连续数值的预测,我们通常使用 均方误差 (Mean Squared Error, MSE) 作为损失函数:
对于一个样本,假设输出层没有激活函数,即是 $\hat{y} = z^{(L)}$,那么损失函数可以定义为:
$$J = \frac{1}{2}(y - \hat{y})^2$$
其中,$y$ 是真实标签,$\hat{y}$ 是预测值。系数 $\frac{1}{2}$ 是为了求导之后消去平方的2,让形式更简洁,不影响优化结果。
此时,输出层的误差信号可以通过对损失函数求导得到:
$$\partial^{(L)} = \frac{\partial J}{\partial z^{(L)}} = \hat{y} - y$$
直接就是预测值 - 真实值,非常的直观啊!
二分类任务
这种任务就是面向输出是否预测,非黑即白,标签是 $y \in {0, 1}$。
我们的输出层使用Sigmoid:$\hat{y} = \sigma(z^{(L)})$
对于二分类任务,我们通常使用 交叉熵损失 (Cross-Entropy Loss) 作为损失函数:
$$J = -[y \log(\hat{y}) + (1 - y) \log(1 - \hat{y})]$$
其中,$y$ 是真实标签,$\hat{y}$ 是预测值。
为什么我们不是用MSE,而是使用交叉熵呢?如果我们使用MSE,结合Sigmoid求导之后,误差信号会包含 $\sigma'(z)$,当 $\hat{y}$ 接近 0 或 1 时,$\sigma'(z)$ 会变得非常小,导致学习及其缓慢。这就是所谓的梯度消失问题。而交叉熵损失与Sigmoid激活函数配合时,求出来的 $\partial^{(L)}$ 十分简单,没有 $\sigma'$ 项,完美抵消,请看:
$$\partial^{(L)} = \frac{\partial J}{\partial z^{(L)}} = \frac{\partial J}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z^{(L)}}$$
首先计算 $\frac{\partial J}{\partial \hat{y}}$:
$$\frac{\partial J}{\partial \hat{y}} = -\left(\frac{y}{\hat{y}} - \frac{1 - y}{1 - \hat{y}}\right)$$
然后计算 $\frac{\partial \hat{y}}{\partial z^{(L)}}$:
$$\frac{\partial \hat{y}}{\partial z^{(L)}} = \hat{y}(1 - \hat{y})$$
我们已经找到链式法则的两个部分,现在将它们结合起来:
$$
\begin{aligned}
\partial^{(L)} &= (-\frac{y}{\hat{y}} + \frac{1 - y}{1 - \hat{y}}) \cdot \hat{y}(1 - \hat{y}) \\
&= -y(1 - \hat{y}) + (1 - y)\hat{y} \\
&= \hat{y} - y
\end{aligned}
$$
然后你发现所谓的梯度消失问题就没有了,误差信号 $\partial^{(L)}$ 就是预测值 $\hat{y}$ 减去真实标签 $y$,跟之前一样的简单和直接。
多分类任务
那么多分类呢?首先我们需要知道多分类任务中,标签是如何编码的。
对于多分类任务,标签通常使用 独热编码 (One-Hot Encoding),也就是将每个类别表示为一个向量,其中只有对应类别的位置是1,其余位置都是0。
例如说我们一共有类别集合 = {猫,狗,鸟},那么:
- 猫:$y = [1, 0, 0]$
- 狗:$y = [0, 1, 0]$
- 鸟:$y = [0, 0, 1]$
如果直接使用数字来表示类别,例如猫=0,狗=1,鸟=2,那么在计算损失函数时就会引入不必要的顺序关系,导致模型误以为类别之间存在大小关系,这显然是不合理的。
之前也讲过独热编码,所以你可以回去看看之前的章节。
至于使用独热编码的多分类任务,我们在输出层通常使用 Softmax 激活函数: $\hat{y} = \text{Softmax}(z^{(L)})$
Softmax对每个节点 $i$ 的输出计算如下:
$$\hat{y}_i = \frac{e^{z_i^{(L)}}}{\sum^K e^{z_j^{(L)}}}$$
这一步的作用是将输出层的线性输出 $z^{(L)}$ 转换为一个概率分布,使得每个 $\hat{y}_i$ 都在 0 和 1 之间,并且所有 $\hat{y}_i$ 的和为 1。
至于损失函数,我们依旧使用交叉熵损失函数,不过这次是针对多分类的版本:
$$J = -\sum_{i=1}^K y_i \log(\hat{y}_i)$$
由于 $y$ 是独热编码,因此实际上只有一个类别的对应的项非零,而损失就是那个类别的预测概率的负对数。
同样的如果我们推导损失对 $z^{(L))}$ 的梯度,结果依旧是:
$$\partial^{(L)} = \hat{y} - y$$
而每个分量的误差就是该类的预测概率减去真实标签。
总的来说,无论哪种任务,前向传播的重点是 $\hat{y}$,而误差计算除了给出标量损失值 $J$之外,最重要的产物是输出层的误差信号:
$$\partial^{(L)} = \frac{\partial J}{\partial z^{(L)}}$$
并且在合理的激活函数和损失函数的搭配下,它的形式都非常简洁,一般都是:
$$\partial^{(L)} = \hat{y} - y$$
这个误差向量会告诉输出层的每个神经元该如何调整。如果预测比真实值大,那么误差为正,后续会下调权重;如果预测比真实值小,那么误差为负,后续会上调权重。
正则化
深层网络的参数量大,非常容易过拟合。模型在训练集上的表现会很好,但是对新的数据泛化可能会很差。那么我们尝试请出我们的老朋友正则化,来迫使网络学习更简单,更平滑的映射,来提升泛化能力。
正则化可以加入到不同任务种类的损失函数上。例如,假如说我们之前交叉熵或MSE这种损失函数为 $J_{data}$,加入L2正则化:
$$J = J_{data} + \frac{\lambda}{2m} \sum_{l=1}^{L} ||W^{(l)}||_F^2$$
- $\lambda$ 是正则化超参数
- $||W^{(l)}||F^2 = \sum{i}\sum_j (W^{(l)}_{ij})^2$ , 是第 $l$ 层权重矩阵所有元素的平方和
- $m$ 是一次batch训练样本的数量,我们之所以要除以 $m$ 是为了让正则化项的规模与数据损失项保持一致,避免正则化项过大或过小对训练过程产生不平衡的影响。
而我们一般不对偏置 $b$ 进行正则化。这一项并不影响前向传播的预测值 $\hat{y}$,而他只在计算总损失和反向传播求得梯度的时候发挥作用。
反向传播
这一步是模型真正看到错误,尝试自己学习的阶段:反向传播 (Backpropagation)。我们在反向传播的目的是通过链式法则,算出损失函数对每一层参数的梯度。
在前向传播中,我们缓存了每一层的 $$z^{(l)}$$ 和 $$a^{(l)}$$。在误差计算中,我们也同时得到了输出层的误差信号 $$\partial^{(L)} = \frac{\partial J}{\partial z^{(L)}}$$。
反向传播的任务就是:
从输出层开始,将 $\partial^{(L)}$ 逐层往输入方向传递,计算出每一层的局部误差 $\partial^{(l)}$。
利用 $\partial^{(l)}$ 和缓存的 $a^{(l - 1)}$,计算出损失对该层参数 $W^{(l)}$ 和 $b^{(l)}$ 的梯度。
一旦有了梯度,我们就可以用梯度下降来更新参数了。
误差传播
首先我们要知道什么是局部误差。对第 $l$ 层,我们定义:
$$\partial^{(l)} = \frac{\partial J}{\partial z^{(l)}}$$
它的维度与 $z^{(l)}$ 相同,表示改变该层每个神经元的净输入对最终损失的影响。
我们之前推到过在误差传播的起始点,也就是输出层,误差信号 $\partial^{(L)}$ 对于回归,二分类和多分类任务都是 $\partial^{(L)} = \hat{y} - y$。那么在隐藏层中,误差该如何逆向传播?
对于 $l = L - 1, L - 2, \ldots, 1$,我们要用 $\partial^{(l + 1)}$ 来推导出 $\partial^{(l)}$,看看根据当前层,上一层的误差是如何传递过来的。
根据链式法则:
$$\partial^{(l)} = \frac{\partial J}{\partial z^{(l)}} = \frac{\partial J}{\partial z^{(l + 1)}} \cdot \frac{\partial z^{(l + 1)}}{\partial z^{(l)}}$$
我们已知:
$\frac{\partial J}{\partial z^{(l + 1)}} = \partial^{(l + 1)}$
$z^{(l + 1)} = W^{(l + 1)} a^{(l)} + b^{(l + 1)}$,所以 $\frac{\partial z^{(l + 1)}}{\partial a^{(l)}} = (W^{(l + 1)})^T$
$a^{(l)} = \sigma(z^{(l)})$,所以$\frac{\partial a^{(l)}}{\partial z^{(l)}} = \sigma'(z^{(l)})$。你发现这是一个对角矩阵,所以我们可以直接用元素乘的方式来表示。
于是我们就得到了关键的反向传播递推公式:
$$\partial^{(l)} = ((W^{(l + 1)})^T \partial^{(l + 1)}) \odot \sigma'(z^{(l)})$$
其中 $\odot$ 表示元素乘积,也就是逐个元素相乘。
这个公式看起来肯定难理解,但它的含义其实很直观:
$(W^{(l + 1)})^T \partial^{(l + 1)}$ 表示我们把 $l + 1$ 层的误差信号按照权重分配回给第 $l$ 层的每个神经元。一个神经元如果通过大权重连接到了一个误差很大的神经元,那么它自己也会得到一个较大的误差信号。
$\sigma'(z^{(l)})$ 而是根据当前神经元的饱和状态来调制误差。例如如果使用ReLU激活函数,则当输入 $z < 0$ 的时候导数为零,误差传递就被切断。
参数梯度计算
这样我们就能够得出每一层的 $\partial^{(l)}$,则计算参数梯度就水到渠成。
由于 $z^{(l)} = W^{(l)} a^{(l - 1)} + b^{(l)}$,我们可以计算损失函数对权重和偏置的梯度:
$$
\frac{\partial J}{\partial W^{(l)}} = \frac{\partial J}{\partial z^{(l)}} \cdot \frac{\partial z^{(l)}}{\partial W^{(l)}} = \partial^{(l)} (a^{(l - 1)})^T
$$
$$
\frac{\partial J}{\partial b^{(l)}} = \frac{\partial J}{\partial z^{(l)}} \cdot \frac{\partial z^{(l)}}{\partial b^{(l)}} = \partial^{(l)}
$$
正则化
正则化项只依赖于权重 $W^{(l)}$,因此只会改变 $\frac{\partial J}{\partial W^{(l)}}$,不会改变 $\frac{\partial J}{\partial b^{(l)}}$ 和 $\partial^{(l)}$ 的递推关系。
原来的梯度是:
$$\frac{\partial J_{data}}{\partial W^{(l)}} = \partial^{(l)} (a^{(l - 1)})^T$$
而加入正则化后,我们可得:
$$\frac{\partial J}{\partial W^{(l)}} = \frac{1}{m} (\partial^{(l)} (a^{(l - 1)})^T) + \lambda W^{(l)}$$
主要是看最后的 $\lambda W^{(l)}$。
这样一来,在参数更新时,我们就相当于在原本梯度方向的基础上,额外施加了一个朝向0点的拉力,迫使权重变得更小:
$$W^{(l)} \leftarrow W^{(l)} - \eta \left( \frac{\partial J_{data}}{\partial W^{(l)}} + \frac{\lambda}{m}W^{(l)} \right)$$
$$= (1 - \frac{\eta\lambda}{m})W^{(l)} - \eta \frac{\partial J_{data}}{\partial W^{(l)}}$$
权重就会自然地向0收缩。
来个例子吧。我们顺着刚才的那个例子来接着做。
Generated by DeepSeek-V4-Pro
网络回顾:
输入 $x = [0.5, 0.1]^T$,真实标签 $y=1$(正类)。
- 第一层:$W^{(1)} = \begin{pmatrix}0.2 & 0.4\\0.3 & -0.5\end{pmatrix}, b^{(1)} = \begin{pmatrix}0.1\\-0.2\end{pmatrix}$,激活 Sigmoid
- 第二层:$W^{(2)} = \begin{pmatrix}0.6 & -0.1\end{pmatrix}, b^{(2)} = [0.3]$,激活 Sigmoid
前向缓存:
$$
z^{(1)} = \begin{pmatrix}0.24\\-0.10\end{pmatrix}, \quad
a^{(1)} = \begin{pmatrix}0.560\\0.475\end{pmatrix}
$$
$$
z^{(2)} = 0.5885, \quad a^{(2)} = \hat{y} = 0.643
$$
输出层误差 $\delta^{(2)}$
二分类交叉熵 + Sigmoid, $\delta^{(2)} = \hat{y} - y = 0.643 - 1 = -0.357$反向递推隐藏层误差 $\delta^{(1)}$
公式:$\delta^{(1)} = \left( (W^{(2)})^T \delta^{(2)} \right) \odot \sigma'(z^{(1)})$
首先 $W^{(2)}$ 是 $1 \times 2$,其转置为 $2 \times 1$:
$$
(W^{(2)})^T = \begin{pmatrix}0.6\\-0.1\end{pmatrix}
$$
$$
(W^{(2)})^T \delta^{(2)} = \begin{pmatrix}0.6\\-0.1\end{pmatrix} \times (-0.357) = \begin{pmatrix}-0.2142\\0.0357\end{pmatrix}
$$
Sigmoid 导数:$\sigma'(z) = \sigma(z)(1-\sigma(z)) = a^{(1)} \odot (1-a^{(1)})$
$$
\sigma'(z^{(1)}) = \begin{pmatrix}0.560 \times 0.440\\0.475 \times 0.525\end{pmatrix} = \begin{pmatrix}0.2464\\0.2494\end{pmatrix}
$$
逐元素相乘得到 $\delta^{(1)}$:
$$
\delta^{(1)} = \begin{pmatrix}-0.2142 \times 0.2464\\0.0357 \times 0.2494\end{pmatrix} = \begin{pmatrix}-0.0528\\0.0089\end{pmatrix}
$$
- 计算参数梯度
对于第二层:
$$
\frac{\partial J}{\partial W^{(2)}} = \delta^{(2)} (a^{(1)})^T = -0.357 \times \begin{pmatrix}0.560 & 0.475\end{pmatrix} = \begin{pmatrix}-0.200 & -0.170\end{pmatrix}
$$
$$
\frac{\partial J}{\partial b^{(2)}} = \delta^{(2)} = -0.357
$$
对于第一层:
$$
\frac{\partial J}{\partial W^{(1)}} = \delta^{(1)} (a^{(0)})^T = \begin{pmatrix}-0.0528\\0.0089\end{pmatrix} \begin{pmatrix}0.5 & 0.1\end{pmatrix} = \begin{pmatrix}-0.0264 & -0.00528\\0.00445 & 0.00089\end{pmatrix}
$$
$$
\frac{\partial J}{\partial b^{(1)}} = \delta^{(1)} = \begin{pmatrix}-0.0528\\0.0089\end{pmatrix}
$$
- 梯度下降更新参数(假设学习率 $\eta = 0.1$)
我们梯度下降的公式是:
$$
\theta \leftarrow \theta - \eta \nabla_\theta J
$$
因此:
$$
W^{(2)} \leftarrow \begin{pmatrix}0.6 & -0.1\end{pmatrix} - 0.1 \times \begin{pmatrix}-0.200 & -0.170\end{pmatrix} = \begin{pmatrix}0.62 & -0.083\end{pmatrix}
$$
$$
b^{(2)} \leftarrow 0.3 - 0.1 \times (-0.357) = 0.3357
$$
$$
W^{(1)} \leftarrow \begin{pmatrix}0.2 & 0.4\\0.3 & -0.5\end{pmatrix} - 0.1 \times \begin{pmatrix}-0.0264 & -0.00528\\0.00445 & 0.00089\end{pmatrix} = \begin{pmatrix}0.2026 & 0.4005\\0.2996 & -0.5001\end{pmatrix}
$$
$$
b^{(1)} \leftarrow \begin{pmatrix}0.1\\-0.2\end{pmatrix} - 0.1 \times \begin{pmatrix}-0.0528\\0.0089\end{pmatrix} = \begin{pmatrix}0.1053\\-0.2009\end{pmatrix}
$$
如果我们此时再对这个样本做一次前向传播,会发现输出概率会向 1(正确标签)靠近——这就是学习在发生。
你看到误差信号从输出产生,通过权重矩阵的传递,将误差从最后一层传递到前一层,同时参数梯度是利用后一层的误差信号和前一层的激活输出计算得到的。
正因为这种梯度的计算是逆着网络结构的方向进行的:
$$\frac{\partial J}{\partial z^{(2)}} → \frac{\partial J}{\partial z^{(1)}} → \frac{\partial J}{\partial W} 和 \frac{\partial J}{\partial b}$$
我们叫反向传播。
我们不妨来看看这三步:正向传播,误差计算,反向传播,是如何形成一次完整的训练迭代的:
- 取一个训练批次 batch: $(X, Y)$
- 逐层计算 $Z^{(l)}$ , $A^{(l)}$,缓存中间值,得到预测 $\hat{Y}$
- 计算标量损失 $J = J_{data} + \frac{\lambda}{2m} \sum||W||^2$, 并求出输出层误差 $\partial^{(L)}$
- 从 $l = L - 1$ 到 1 递推 $\partial^{(l)}$,同时累积各层梯度 $\Delta_{W^{(l)}}J$, $\Delta_{b^{(l)}}J$
- 使用优化器(例如SGD)更新参数 $W^{(l)}$ 和 $b^{(l)}$
- 重复以上步骤,直到满足停止条件,例如损失收敛或者达到最大迭代次数。