\[ \begin{align}\begin{aligned}\newcommand{\ba}{\boldsymbol{a}}
\newcommand{\bb}{\boldsymbol{b}}
\newcommand{\be}{\boldsymbol{e}}
\newcommand{\bq}{\boldsymbol{q}}
\newcommand{\bk}{\boldsymbol{k}}
\newcommand{\bw}{\boldsymbol{w}}
\newcommand{\bx}{\boldsymbol{x}}
\newcommand{\by}{\boldsymbol{y}}
\newcommand{\bz}{\boldsymbol{z}}
\newcommand{\bd}{\boldsymbol{d}}
\newcommand{\bv}{\boldsymbol{v}}
\newcommand{\bs}{\boldsymbol{s}}\\\newcommand{\btheta}{\boldsymbol{\theta}}
\newcommand{\bbeta}{\boldsymbol{\beta}}
\newcommand{\bgamma}{\boldsymbol{\gamma}}
\newcommand{\bsigma}{\boldsymbol{\sigma}}
\newcommand{\md}{\mbox{d}}
\newcommand{\bmu}{\boldsymbol{\mu}}
\newcommand{\bone}{\boldsymbol{1}}
\newcommand{\bzero}{\boldsymbol{0}}
\newcommand{\bepsilon}{\boldsymbol{\epsilon}}
\newcommand{\bphi}{\boldsymbol{\phi}}
\newcommand{\bh}{\boldsymbol{h}}
\newcommand{\bc}{\boldsymbol{c}}
\newcommand{\br}{\boldsymbol{r}}
\newcommand{\bQ}{\boldsymbol{Q}}
\newcommand{\bK}{\boldsymbol{K}}
\newcommand{\bV}{\boldsymbol{V}}
\newcommand{\bSigma}{\boldsymbol{\Sigma}}
\newcommand{\bg}{\boldsymbol{g}}
\newcommand{\bxi}{\boldsymbol{\xi}}
\newcommand{\bvarepsilon}{\boldsymbol{\varepsilon}}
\newcommand{\bdelta}{\boldsymbol{\delta}}
\newcommand{\bq}{\boldsymbol{q}}
\newcommand{\bk}{\boldsymbol{k}}
\newcommand{\bJ}{\boldsymbol{J}}
\newcommand{\bp}{\boldsymbol{p}}
\newcommand{\bi}{\boldsymbol{i}}
\newcommand{\bo}{\boldsymbol{o}}
\newcommand{\bE}{\boldsymbol{E}}
\newcommand{\bH}{\boldsymbol{H}}
\newcommand{\bL}{\boldsymbol{L}}
\newcommand{\bu}{\boldsymbol{u}}
\newcommand{\bLambda}{\boldsymbol{\Lambda}}
\newcommand{\trans}{^{\rm\scriptsize T}}
\newcommand{\var}{\mathrm{var}}\\\newcommand{\bA}{\boldsymbol{A}}
\newcommand{\bB}{\boldsymbol{B}}
\newcommand{\bC}{\boldsymbol{C}}
\newcommand{\bD}{\boldsymbol{D}}
\newcommand{\bG}{\boldsymbol{G}}
\newcommand{\bI}{\boldsymbol{I}}
\newcommand{\bM}{\boldsymbol{M}}
\newcommand{\bP}{\boldsymbol{P}}
\newcommand{\bS}{\boldsymbol{S}}
\newcommand{\bU}{\boldsymbol{U}}
\newcommand{\bW}{\boldsymbol{W}}
\newcommand{\bX}{\boldsymbol{X}}
\newcommand{\bY}{\boldsymbol{Y}}
\newcommand{\bZ}{\boldsymbol{Z}}
\newcommand{\cotp}{\textcolor[RGB]{48,209,88}{TP}}
\newcommand{\cotn}{\textcolor[RGB]{100,210,255}{TN}}
\newcommand{\cofp}{\textcolor[RGB]{94,92,230}{FP}}
\newcommand{\cofn}{\textcolor[RGB]{191,90,242}{FN}}\\\newcommand{\numcotp}{\textcolor[RGB]{48,209,88}{50}}
\newcommand{\numcotn}{\textcolor[RGB]{100,210,255}{30}}
\newcommand{\numcofp}{\textcolor[RGB]{94,92,230}{10}}
\newcommand{\numcofn}{\textcolor[RGB]{191,90,242}{10}}
\DeclareMathOperator*{\argmin}{arg\,min}\end{aligned}\end{align} \]
单隐藏层神经网络——多样本向量化
学习目标与记号
准确解释样本按行排列的默认规则、批量前向传播与平均损失;
根据公式和张量维度分析批量后向传播,并识别常见实现错误;
用 Python 实现或验证广播偏置;在其他条件相同的情况下,只改变一个需要研究的因素进行比较,并根据实验结果分析该因素可能带来的影响。
本节沿用统一记号:普通小写字母表示标量,粗体小写字母表示向量,粗体大写字母表示矩阵或高阶张量;层编号写作上标 \([l]\),样本或时间编号写作下标;转置写作 \(\trans\)。除非另有说明,批量样本按行存放。正文与练习中的程序都应同时检查数值结果和数组维度。本节练习的参考答案见 多样本向量化前向与后向传播答案。
本节是 单样本前向传播 与 单样本后向传播 的批量化推广;所涉及的广播机制运算规则请参见 广播机制。
上一节从单个样本推导了前向传播和后向传播。本节把 \(n\) 个样本按行堆叠,以一次矩阵运算完成整个训练集或一个小批量的计算。记 \(\bx_i\in\mathbb{R}^{d\times 1}\) 和 \(y_i\in\{0,1\}\),并令 \(\bX=[\bx_1\trans;\ldots;\bx_n\trans]\in\mathbb{R}^{n\times d}\) 和 \(\by=(y_1,\ldots,y_n)\trans\in\mathbb{R}^{n\times1}\)。
模型参数
\(\bb^{[1]}\in\mathbb{R}^{d^{[1]}\times 1}\) 为隐藏层中每个神经元对应的偏置项,每一行对应一个神经元,该层共有 \(d^{[1]}\) 个神经元。
\(\bW^{[1]}\in\mathbb{R}^{d^{[1]}\times d}\) 为隐藏层中每个神经元对应的权重项,每一行对应一个神经元。
\(\bb^{[2]}\in\mathbb{R}^{1\times1}\) 为输出层神经元对应的偏置项。
\(\bW^{[2]}\in\mathbb{R}^{1\times d^{[1]}}\) 为输出层神经元对应的权重矩阵。
需要指出的是,我们将神经网络中的每个参数都表达成了二维矩阵。这样做有助于借助于 广播机制 对本节所涉及到的向量化进行讨论。
前向传播
与上一节的式 (17) 类似,在给定当前模型参数时,我们需要 针对每个训练样本 计算前向传播各个“神经元”的函数值。即对于 \(i=1,\ldots,n\),计算
\[\begin{split}\begin{aligned}
\bz_i^{[1]} &= \bb^{[1]}+\bW^{[1]}\cdot\bx_i, \\
\ba_i^{[1]} &= \sigma(\bz_i^{[1]}), \\
z_i^{[2]} &= \bb^{[2]}+\bW^{[2]}\cdot\ba_i^{[1]},\\
a_i^{[2]} &=\sigma(z_i^{[2]}).
\end{aligned}\end{split}\]
对应的损失函数为
(24)\[\mathcal{J}(\btheta)=-\frac{1}{n}\sum_{i=1}^n
\left\{y_i\log a_i^{[2]}+(1-y_i)\log\left(1-a_i^{[2]}\right)\right\},\]
其中,\(a_i^{[2]}\) 为模型参数 \(\btheta\) 的函数,对应于第 \(i\) 个训练样本的输出,即给定特征 \(\bx_i\) 时,对应标签属于类别 1 的条件概率。与单训练样本的情况不同,式 (24) 使用平均损失。
记 \(\bX=[\bx_1\trans;\ldots;\bx_n\trans]\in\mathbb{R}^{n\times d}\) 和 \(\by=(y_1,\ldots,y_n)\trans\in\mathbb{R}^{n\times1}\),则前向传播可写为
(25)\[\begin{split}\begin{aligned}
\bZ^{[1]} &= \bX\cdot(\bW^{[1]})\trans+\bone\cdot(\bb^{[1]})\trans, \\
\bA^{[1]} &= \sigma(\bZ^{[1]}), \\
\bZ^{[2]} &= \bA^{[1]}\cdot(\bW^{[2]})\trans+\bone\cdot(\bb^{[2]})\trans,\\
\bA^{[2]} &=\sigma(\bZ^{[2]}).
\end{aligned}\end{split}\]
式 (25) 显式使用 \(\bone\in\mathbb{R}^{n\times1}\) 将偏置复制到每一行,激活函数 \(\sigma(\cdot)\) 逐元素作用于 \(\bZ^{[l]}\)。在 NumPy 中可以依靠 广播机制 省略这个乘法,但偏置应存为 \(d^{[l]}\) 或 \(1\times d^{[l]}\),而不是 \(d^{[l]}\times1\),否则可能沿错误的轴广播。也就是说,如果使用 Python 的广播机制,对应的计算结果如下:
\[\begin{split}\begin{aligned}
\bZ^{[1]} &= \bX\cdot(\bW^{[1]})\trans+(\bb^{[1]})\trans, \\
\bA^{[1]} &= \sigma(\bZ^{[1]}), \\
\bZ^{[2]} &= \bA^{[1]}\cdot(\bW^{[2]})\trans+(\bb^{[2]})\trans,\\
\bA^{[2]} &=\sigma(\bZ^{[2]}).
\end{aligned}\end{split}\]
于是,平均损失(或者代价函数)可写成
\[\mathcal{J}(\btheta)=-\frac{1}{n}\bone\trans\cdot
\left[\by\odot\log\bA^{[2]}
+(\bone-\by)\odot\log(\bone-\bA^{[2]})\right],\]
其中,\(\bone\in\mathbb{R}^{n\times1}\) 为全 1 向量,\(\odot\) 表示逐元素乘积,对数运算是逐元素进行的。这个写法把“逐样本计算损失”和“对样本求平均”明确区分开。
后向传播
为了简化记号,我们记
\[\mathrm{d}\cdot=\frac{\partial\mathcal{J}}{\partial\cdot}.\]
基于向量化以及上一节中式 (22) 的计算过程,我们可以得到如下后向传播结果。
(26)\[\begin{split}\begin{aligned}
\mathrm{d}\bZ^{[2]} &=n^{-1}(\bA^{[2]}-\by),\\
\mathrm{d}\bb^{[2]} &= (\mathrm{d}\bZ^{[2]})\trans\cdot\bone,\\
\mathrm{d}\bW^{[2]} &= (\mathrm{d}\bZ^{[2]})\trans\cdot\bA^{[1]},\\
\mathrm{d}\bZ^{[1]} &= \mathrm{d}\bZ^{[2]}\cdot\bW^{[2]}\odot\sigma'(\bZ^{[1]}),\\
\mathrm{d}\bb^{[1]} &= (\mathrm{d}\bZ^{[1]})\trans\cdot\bone,\\
\mathrm{d}\bW^{[1]} &= (\mathrm{d}\bZ^{[1]})\trans\cdot\bX,
\end{aligned}\end{split}\]
其中,\(\mathrm{d}\bZ^{[2]}\in\mathbb{R}^{n\times1}\) 已包含因子 \(1/n\),因此其余梯度不再重复除以 \(n\)。请大家自行验证:\(\mathrm{d}\bW^{[l]}\) 与 \(\bW^{[l]}\) 同规模,\(\mathrm{d}\bb^{[l]}\) 与 \(\bb^{[l]}\) 同规模。
当我们得到损失函数对模型参数的导数后,我们仍然用式 (23) 对模型参数进行更新。
核心推导与实现核验
核心关系
(27)\[\bZ^{[l]}=\bA^{[l-1]}\cdot(\bW^{[l]})\trans+\bone\cdot(\bb^{[l]})\trans.\]
推导路径。 将 \(n\) 个样本按行组成输入矩阵后,可以用一次矩阵乘法同时计算所有样本的结果。偏置向量会加到每个样本的计算结果上。由于损失函数是所有样本损失的平均值,因此参数梯度等于各样本梯度之和除以样本数。
关键条件
在式 (27) 中,矩阵乘法结果的第 \((i,j)\) 个元素恰好是第 \(i\) 个样本与第 \(j\) 个神经元权重的内积,因此该式与逐样本计算的结果逐元素相等。
数据规模
若一次处理 \(n\) 个样本,第 \(l-1\) 层每个样本有 \(d^{[l-1]}\) 个特征,第 \(l\) 层有 \(d^{[l]}\) 个神经元,则输入 \(\bA^{[l-1]}\) 的规模为 \(n\times d^{[l-1]}\),权重 \(\bW^{[l]}\) 的规模为 \(d^{[l]}\times d^{[l-1]}\),输出 \(\bZ^{[l]}\) 的规模为 \(n\times d^{[l]}\)。
常见误区
式 (26) 中的 \(\mathrm{d}\bZ^{[2]}\) 已经包含平均因子 \(1/n\);如果在计算后续权重梯度或偏置梯度时再次除以 \(n\),最终梯度会额外缩小 \(n\) 倍。式 (27) 使用 \(\bone\cdot(\bb^{[l]})\trans\) 将同一个偏置向量加到每个样本的计算结果上;若将偏置存成一维数组,虽然 NumPy 可以自动完成广播,但数组不再明确保留行轴和列轴,后续进行转置或求和时更难发现轴使用错误。从这个意义上讲,我们建议将偏置项 \(\bb^{[1]}\) 等表示成二维数组。
数值稳定性与规模
对于二分类问题,应采用数值稳定的方法计算损失函数。初始化权重时,应根据该层的输入神经元数量确定权重的初始大小。训练过程中,应记录各层激活值和梯度的大小。如果梯度过大,应先检查后向传播公式是否正确以及学习率是否过大;确认计算无误后,再考虑限制所有参数梯度的整体大小。
本节小结
本材料的样本按行进行堆叠,形成设计矩阵。
偏置广播必须与目标轴一致。
乘子 \(n^{-1}\) 只在计算输出层时使用,批量和逐样本结果应可互相验证。
综合练习
本组练习围绕批量矩阵公式、逐样本计算与批量计算的等价性以及向量化实现展开。程序题应固定数据划分、随机种子集合和训练预算,写出维度断言并报告运行环境。全部参考答案见 多样本向量化前向与后向传播答案。
批量前向传播。 从第 \(i\) 个样本的逐样本公式出发,推导式 (25)。写出 \(\bX\)、\(\bW^{[l]}\)、\(\bb^{[l]}\)、\(\bZ^{[l]}\) 和 \(\bA^{[l]}\) 的规模,并说明全 1 向量在偏置复制中的作用。
批量后向传播。 从式 (24) 给出的平均损失出发,推导式 (26)。说明因子 \(1/n\) 应在哪一步引入,并证明 \(\mathrm d\bW^{[l]}\) 与 \(\bW^{[l]}\) 以及 \(\mathrm d\bb^{[l]}\) 与 \(\bb^{[l]}\) 具有相同的规模。
逐样本与批量计算的等价性。 按矩阵元素展开,证明批量前向传播的每一行等于对应样本的前向结果,并证明批量参数梯度等于逐样本参数梯度的平均值。
矩阵手算。 给定
\(\bA=\begin{pmatrix}1&2\\-1&0\end{pmatrix}\)、
\(\bW=\begin{pmatrix}1&-1\\2&1\end{pmatrix}\)、
\(\bb=(0.5,-0.5)\trans\) 和
\(\mathrm d\bZ=\begin{pmatrix}1&2\\3&4\end{pmatrix}\),手算 \(\bZ=\bA\cdot\bW\trans+\bone\cdot\bb\trans\)、\(\mathrm d\bW=(\mathrm d\bZ)\trans\cdot\bA\)、\(\mathrm d\bb=(\mathrm d\bZ)\trans\cdot\bone\) 与 \(\mathrm d\bA=\mathrm d\bZ\cdot\bW\),并核对对应结果的规模。
向量化实现。 仅使用 NumPy 实现单隐藏层网络的批量 forward_backward。样本按行存放,偏置显式保存为一维数组或 \(1\times d^{[l]}\) 行向量;损失按样本取平均,并确保平均因子只出现一次。返回全部中间量和梯度,用第 4 题核验仿射层实现。
循环与向量化结果核验。 编写逐样本循环版和批量矩阵版前向与后向传播。在多个随机维度上比较损失函数值和参数梯度,并构造一个能够发现“重复除以 \(n\)”以及“偏置沿错误轴广播”的测试。
循环与向量化效率。 对不同的 \(n\)、\(d\) 和隐藏神经元个数,比较逐样本循环版与批量矩阵版的前向时间、前向加后向时间、固定批量预测时间和峰值内存。预热后重复计时,核验结果一致后再讨论效率,并解释为什么小规模问题上计时差异可能不稳定。