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

Softmax 回归#

学习目标与记号#

  1. 准确解释输出层线性运算结果与 Softmax、多分类交叉熵的关系;

  2. 根据公式和张量维度分析平移不变性,并识别常见实现错误;

  3. Python 实现或验证输出层梯度;在其他条件相同的情况下,只改变一个需要研究的因素进行比较,并根据实验结果分析该因素可能带来的影响。

  本节沿用统一记号:普通小写字母表示标量,粗体小写字母表示向量,粗体大写字母表示矩阵或高阶张量;层编号写作上标 \([l]\)⁠,样本或时间编号写作下标;转置写作 \(\trans\)⁠。除非另有说明,批量样本按行存放。正文与练习中的程序都应同时检查数值结果和数组维度。本节练习的参考答案见 Softmax 多分类回归答案⁠。

  Softmax 回归是 逻辑回归 的多分类推广,其损失与 交叉熵 直接相关;作为神经网络输出层时,可对照 输出层激活选择⁠。

  前面主要讨论二分类问题。对于 \(K\geq2\) 个互斥类别,输出层需要给出 \(K\) 个非负且和为 1 的数,分别表示各类别的预测概率。Softmax 回归可以看作逻辑回归向多分类问题的推广,也常作为多分类神经网络的输出层。

  Softmax 函数可以将一个含任意实数的 \(K\) 维向量 \(\bz=(z_1,\ldots,z_K)\trans\) 转换成另一个 \(K\) 维实向量 \(\ba=(a_1,\ldots,a_K)\trans\)⁠,其中

(51)#\[a_k=\frac{\exp(z_k)}{\sum_{j=1}^K\exp(z_j)}, \qquad k=1,\ldots,K.\]

由定义可知 \(a_k\in(0,1)\)\(\sum_{k=1}^K a_k=1\)⁠。向量 \(\bz\) 由输出层的 \(K\) 个线性运算结果组成;这些结果尚未经过 Softmax 转换。给它们同时加上同一常数不会改变 Softmax 输出。利用这一平移不变性,程序中通常先令 \(c=\max_k z_k\)⁠,再计算

\[a_k=\frac{\exp(z_k-c)}{\sum_{j=1}^K\exp(z_j-c)},\]

从而避免直接计算较大指数时发生数值溢出。

  记 \(\by=(y_1,\ldots,y_K)\trans\) 为标签的独热编码(one-hot encoding)。若真实类别为 \(r\)⁠,则 \(y_r=1\)⁠,其余分量为 0。单个样本的交叉熵损失为

(52)#\[\mathcal{L}(\ba,\by)=-\sum_{k=1}^K y_k\log a_k=-\log a_r.\]

  对于第 \(i\) 个样本,记 \(\ba_i=(a_{i1},\ldots,a_{iK})\trans\) 为模型给出的预测概率向量,记 \(\by_i=(y_{i1},\ldots,y_{iK})\trans\) 为该样本标签的独热编码。该样本的交叉熵损失记为

\[\mathcal{L}_i =\mathcal{L}(\ba_i,\by_i) =-\sum_{k=1}^K y_{ik}\log a_{ik} =-\log a_{i,r_i},\]

其中,\(a_{ik}\) 表示模型将第 \(i\) 个样本判为第 \(k\) 类的预测概率,\(y_{ik}\) 表示独热编码标签的第 \(k\) 个分量,\(r_i\) 表示第 \(i\) 个样本的真实类别。因此,\(\mathcal{L}_i\) 就是第 \(i\) 个样本对应的交叉熵损失。

  对 \(n\) 个样本的 \(\mathcal{L}_1,\ldots,\mathcal{L}_n\) 取平均,即可得到训练目标 \(\mathcal{J}(\btheta)=n^{-1}\sum_{i=1}^n\mathcal{L}_i\)⁠,其中 \(\btheta\) 表示模型中需要通过训练确定的全部参数。实际实现中应优先使用框架提供的“log-softmax 加负对数似然”或交叉熵函数,以获得更好的数值稳定性。

多分类问题中的宏平均 F1

  前面介绍的 F1 分数 用于衡量某个类别的 Precision 与 Recall。对于包含 \(K\) 个类别的多分类问题,可以依次把每一个类别看作正类、把其余类别合在一起看作负类,并分别计算第 \(k\) 个类别的 F1 分数:

\[\operatorname{F1}_k =\frac{2\operatorname{TP}_k} {2\operatorname{TP}_k+\operatorname{FP}_k+\operatorname{FN}_k}, \qquad k=1,\ldots,K.\]

  再对这 \(K\) 个结果直接取平均,便得到宏平均 F1(macro-averaged F1,简称 macro-F1):

\[\operatorname{macro\text{-}F1} =\frac{1}{K}\sum_{k=1}^{K}\operatorname{F1}_k.\]

  这种平均方法给予每个类别相同的权重,因此在各类别样本数量相差较大时,比只看总体准确率更容易发现模型是否忽视了样本较少的类别。不过,单独一个平均值可能掩盖某些类别表现很差的情况,所以通常还应同时查看各类别的 F1 分数或召回率。

  Softmax 的雅可比矩阵满足 \(\partial a_k/\partial z_j=a_k(\delta_{kj}-a_j)\)⁠。与交叉熵组合后,链式法则给出特别简洁的损失对线性运算结果的梯度:

(53)#\[\frac{\partial\mathcal{L}}{\partial\bz}=\ba-\by.\]

该结果与“sigmoid 函数与二元交叉熵”的 \(a-y\) 形式一致。得到输出层梯度后,其余各层仍按照 多隐藏层神经网络 中给出的公式继续进行后向传播。

Shiny 交互演示:Softmax 概率

  交互页面可以改变三个类别的线性运算结果以及同时加到这些结果上的常数,观察稳定 Softmax 算法得到的类别概率和单样本交叉熵。页面会直接核对概率之和以及 Softmax 对共同平移的不变性。

点击打开“逻辑回归与 Softmax 回归”交互演示

核心推导与实现核验#

核心关系

\[a_k=\frac{e^{z_k}}{\sum_{j=1}^K e^{z_j}},\qquad\frac{\partial\mathcal{L}}{\partial\bz}=\ba-\by.\]

  推导路径。 先求 \(\partial a_i/\partial z_j=a_i(\mathbb 1\{i=j\}-a_j)\)⁠,再对 \(-\sum_i y_i\log a_i\) 使用链式法则并利用 one-hot 标签和为 1,得到 \(\ba-\by\)⁠。

关键条件

  对所有线性运算结果加常数 \(c\) 后,分子分母同时乘 \(e^c\) 并约去,所以 Softmax 输出不变;这证明减去最大的线性运算结果不改变概率。

数据规模

  若一次处理 \(n\) 个样本并区分 \(K\) 个类别,把第 \(i\) 个样本的线性运算结果、预测概率和 one-hot 标签列向量分别记为 \(\bz_i\)⁠、\(\ba_i\)\(\by_i\)⁠,则相应矩阵统一写为 \(\bZ=[\bz_1\trans;\ldots;\bz_n\trans]\)⁠、\(\bA=[\ba_1\trans;\ldots;\ba_n\trans]\)\(\bY=[\by_1\trans;\ldots;\by_n\trans]\)⁠。三个矩阵的大小均为 \(n\times K\)⁠,每一行对应一个样本,并且 \(\bA\) 每一行的 \(K\) 个预测概率之和必须等于 1。

常见误区

  先 Softmax 再取 log 会损失数值精度;标签若已是类别索引,就不应误当作概率矩阵参与逐元素乘法。

数值稳定性与规模

  对于每个样本,先找出 \(K\) 个线性运算结果中的最大值 \(m\)⁠,并从每个结果中减去 \(m\)⁠;然后对得到的数依次计算指数、求和并取对数,最后加回 \(m\)⁠。这个过程计算的是 \(\log\left(\sum_{k=1}^K e^{z_k}\right)\)⁠,通常称为 log-sum-exp。交叉熵函数应直接接收输出层的线性运算结果。使用很大或很小的输入进行测试时,应确认损失不是无穷大或无效数值、每个样本的预测概率之和接近 1,并避免先求概率再取对数。

本节小结#

  1. Softmax 把线性运算结果之间的相对大小变成和为 1 的概率。

  2. 减最大值利用平移不变性保证数值稳定。

  3. Softmax 与交叉熵组合后的梯度为预测减标签。

综合练习#

  程序题应固定随机种子、写出维度断言并报告运行环境。全部参考答案见 Softmax 多分类回归答案⁠。

  1. Softmax 与交叉熵计算。\(\bz=(2,1,0)\trans\)⁠,计算三个 Softmax 概率。若真实类别为第 2 类,计算单样本交叉熵以及 \(\partial\mathcal L/\partial\bz\)⁠,保留至少四位小数并验证梯度分量之和为 0。

  2. 平移不变性证明。 证明对任意常数 \(c\)⁠,有 \(\operatorname{softmax}(\bz+c\bone)=\operatorname{softmax}(\bz)\)⁠;据此说明减去 \(\max_k z_k\) 为什么不改变概率。再证明 Softmax 只能由线性运算结果之间的差确定,并解释共同偏移方向为何不可识别。

  3. 雅可比与交叉熵梯度。 从定义推导 \(\partial a_k/\partial z_j=a_k(\delta_{kj}-a_j)\)⁠,并证明 \(\partial\mathcal L/\partial\bz=\ba-\by\)⁠。

  4. 凸性证明。 把单样本损失写成 \(\mathcal L(\bz)=\operatorname{logsumexp}(\bz)-\by\trans\cdot\bz\)⁠,其中 \(\operatorname{logsumexp}(\bz)=\log\left(\sum_{k=1}^K e^{z_k}\right)\)⁠。推导 Hessian 矩阵,并证明它半正定。说明损失关于 \(\bz\) 为什么是凸函数但沿共同平移方向不是严格凸函数。

  5. 稳定实现。 用 NumPy 实现批量 log_softmax 和接受类别索引标签的交叉熵。对于每个样本,先从所有类别的线性运算结果中减去其中的最大值,再对得到的数依次计算指数、求和并取对数,最后加回刚才减去的最大值;检查输入为二维有限数组、标签为一维合法整数,并返回每个样本损失、平均损失和概率。

  6. 极端输入与库函数测试。 使用含 \(1000\)⁠、\(-1000\)⁠、全部相等值、单类别和大量类别的输入,按照 手算结果与可信实现对照 的原则,比较手写稳定实现与可信库交叉熵。朴素实现应作为预期失败对照;检查损失与梯度有限、类别轴正确、索引越界报错,并明确 \(K=1\) 的退化含义。

  7. 朴素与稳定算法效率。 在相同的随机线性运算结果上比较三种算法:直接计算 exp 后取对数、先减去最大值再计算 Softmax 后取对数,以及直接计算稳定的 log-softmax。各算法使用相同的随机种子、批量大小、类别数、数值范围和数据类型;程序先运行相同次数,再重复运行相同次数进行计时。报告结果为有限数值的比例、相对或绝对误差、固定批量计算时间、峰值内存和指数函数与对数函数的运行次数。

  8. 类别数扩展。\(K\in\{10,100,1000,10000\}\) 比较 Softmax 输出层的训练与预测成本。固定数据生成规则与训练/验证/测试划分、输入维度、批量、随机种子集合、优化器、训练更新次数、每次更新样本数和评价代码;报告交叉熵或 top-1 准确率、训练时间、固定批量预测时间、峰值内存、输出层参数量和前向/后向调用次数。

  9. Softmax 回归与神经网络。 在同一多分类数据上比较 Softmax 回归、单隐藏层神经网络和参数量更大的网络。固定训练/验证/测试划分、随机种子集合、输入预处理、最大训练更新次数、早停规则和评价程序;报告测试准确率、macro-F1⁠、交叉熵、训练时间、固定批量预测时间、峰值内存、参数量和模型前向调用次数。