☰
深度学习入门:Softmax分类器损失函数与梯度推导实战
2026/10/17 5:52:56 网站建设 项目流程

1. 从Softmax分类器说起:这个作业到底在练什么

如果你正在啃深度学习入门的第一块硬骨头,大概率绕不开图像分类这条主线。Assignment1里从KNN到SVM再到Softmax,本质上是在用四种不同的视角回答同一个问题:怎么让机器把一张图分到正确的类别里。Softmax这个环节特别关键,因为它是从传统线性分类器过渡到神经网络损失函数的桥梁——你现在手写的这个Softmax损失,后面在卷积网络里会以几乎一模一样的形态出现,只是输入从原始像素变成了特征图。

这个作业的核心任务其实就三件事:实现一个数值稳定的Softmax损失函数、推导并实现它的解析梯度、用向量化方式完成参数更新。听起来简单,但真正动手写的时候,新手最容易在三个地方翻车:指数运算的数值溢出、梯度推导的链式法则、以及循环改向量化时的维度对齐。我见过太多人损失函数写对了但梯度算错,结果训练半天准确率卡在10%不动,排查一晚上才发现是某个转置写反了。

这篇文章我会把整个Softmax作业拆成可复现的步骤,从数学推导到代码实现,从朴素循环到全向量化,每一步都讲清楚为什么这么做。适合正在做这个作业但卡住的同学,也适合想重新梳理线性分类器底层逻辑的从业者。你不需要有很强的数学背景,但需要会一点Python和NumPy的基本操作。

2. 核心原理拆解:Softmax到底在算什么

2.1 从得分到概率:为什么需要Softmax

线性分类器的基本形式是 ( f(x, W) = Wx + b ),输入一张图片拉平成向量 ( x ),乘以权重矩阵 ( W ),得到每个类别的得分(score)。这个得分是实数,可正可负,大小关系代表了模型对类别的偏好,但它不是概率——你没法说“这张图有3.2分是猫”。

Softmax的作用就是把这个得分向量变成概率分布。公式很简洁:

[ P(y=k|x) = \frac{e^{s_k}}{\sum_j e^{s_j}} ]

其中 ( s_k ) 是第k类的得分。指数运算保证了所有输出为正,除以总和保证了加起来等于1。这样我们就得到了一个合法的概率分布,可以套用交叉熵损失来衡量预测和真实标签的差距。

交叉熵损失的形式是 ( L_i = -\log P(y=y_i|x_i) ),也就是取正确类别概率的负对数。直觉上很好理解:正确类别的概率越接近1,负对数越接近0,损失越小;概率越接近0,负对数越大,损失越大。这个损失函数是凸的(对于线性模型而言),而且梯度形式非常优雅,这也是它成为分类任务标配的原因。

2.2 数值稳定:那个让新手翻车的指数溢出

直接按公式实现Softmax,第一版代码大概率会炸。原因在于指数函数增长极快,得分稍微大一点,比如100,( e^{100} ) 就溢出了,NumPy会返回inf,然后inf除以inf得到nan,整个损失变成nan,训练直接崩掉。

解决办法是利用Softmax的一个不变性:给所有得分同时减去一个常数,结果不变。数学上:

[ \frac{e^{s_k}}{\sum_j e^{s_j}} = \frac{e^{s_k - C}}{\sum_j e^{s_j - C}} ]

证明很简单,分子分母同乘 ( e^{-C} ) 即可。实践中我们取 ( C = \max(s) ),这样最大的指数变成 ( e^0 = 1 ),其余都是小于1的正数,完全避免了溢出。这个技巧叫“数值稳定化”,是所有深度学习框架内部都在做的事,你手写一遍就理解为什么PyTorch的CrossEntropyLoss不需要你手动做这个操作了。

注意:减去最大值之后,分母至少包含一项 ( e^0 = 1 ),所以分母永远不会是0,不会出现除零错误。这个细节在推导梯度时也要保持一致。

2.3 梯度推导:链式法则的完整展开

Softmax的梯度推导是Assignment1里数学味最浓的部分。设得分 ( s = Wx ),损失 ( L_i = -\log(\frac{e^{s_{y_i}}}{\sum_j e^{s_j}}) = -s_{y_i} + \log\sum_j e^{s_j} )。

对得分 ( s_k ) 求偏导,分两种情况:

当 ( k = y_i )(正确类别)时: [ \frac{\partial L_i}{\partial s_k} = -1 + \frac{e^{s_k}}{\sum_j e^{s_j}} = P_k - 1 ]

当 ( k eq y_i )(错误类别)时: [ \frac{\partial L_i}{\partial s_k} = \frac{e^{s_k}}{\sum_j e^{s_j}} = P_k ]

合并起来就是 ( \frac{\partial L_i}{\partial s_k} = P_k - \mathbb{1}(k = y_i) )。这个形式非常简洁:预测概率减去真实标签的one-hot编码。

再对 ( W ) 求导,根据链式法则 ( \frac{\partial L_i}{\partial W} = \frac{\partial L_i}{\partial s} \cdot \frac{\partial s}{\partial W} = (P - \mathbb{1}) \cdot x^T )。这就是解析梯度的最终形式,也是我们代码里要实现的。

3. 代码实现:从朴素循环到全向量化

3.1 损失函数的朴素实现

先写一个最直白的版本,用循环逐样本计算,方便理解每一步在做什么:

import numpy as np def softmax_loss_naive(W, X, y, reg): loss = 0.0 dW = np.zeros_like(W) num_train = X.shape[0] num_classes = W.shape[1] for i in range(num_train): scores = X[i].dot(W) # (C,) # 数值稳定化 scores -= np.max(scores) exp_scores = np.exp(scores) probs = exp_scores / np.sum(exp_scores) # 交叉熵损失 loss += -np.log(probs[y[i]]) # 梯度计算 for j in range(num_classes): dW[:, j] += (probs[j] - (j == y[i])) * X[i] loss /= num_train dW /= num_train loss += reg * np.sum(W * W) dW += 2 * reg * W return loss, dW

这段代码逻辑清晰,但有两个问题:一是双重循环在CIFAR-10的50000张训练集上跑一次要几十秒,二是内层循环可以合并。不过作为理解梯度的第一步,这个版本值得亲手敲一遍。

实操心得:写朴素版本时,建议先用一个极小的数据集(比如5张图、3个类别)手动验证。打印出scores、probs、loss的中间值,拿计算器核对一两个数字,确认无误后再上完整数据集。我当初就是靠这个笨办法发现梯度里少乘了一个X[i]。

3.2 向量化损失函数:一次算完所有样本

向量化的核心思想是把循环维度变成矩阵运算。对于损失部分:

def softmax_loss_vectorized(W, X, y, reg): num_train = X.shape[0] scores = X.dot(W) # (N, C) scores -= np.max(scores, axis=1, keepdims=True) # 数值稳定 exp_scores = np.exp(scores) probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) # (N, C) # 损失:取正确类别的概率,取负对数,求平均 correct_logprobs = -np.log(probs[np.arange(num_train), y]) loss = np.sum(correct_logprobs) / num_train loss += reg * np.sum(W * W) # 梯度 dscores = probs.copy() # (N, C) dscores[np.arange(num_train), y] -= 1 dW = X.T.dot(dscores) / num_train dW += 2 * reg * W return loss, dW

关键点在于probs[np.arange(num_train), y]这个高级索引,它一次性取出每个样本正确类别的概率,避免了循环。梯度部分dscores先复制概率矩阵,然后在正确类别位置减1,得到的就是 ( P - \mathbb{1} ),再左乘 ( X^T ) 完成对所有样本的累加。

注意:keepdims=True这个参数很容易漏。如果不加,np.max(scores, axis=1)返回的是形状(N,)的数组,广播时会出现维度不匹配或者错误的对齐。加上之后保持(N, 1),才能正确地和(N, C)的scores做减法。

3.3 梯度检查:别跳过这一步

作业里提供了grad_check_sparse函数,用数值梯度验证解析梯度。数值梯度的原理是中心差分:

[ \frac{\partial L}{\partial W_{ij}} \approx \frac{L(W_{ij} + h) - L(W_{ij} - h)}{2h} ]

取 ( h = 10^{-5} ) 左右,相对误差应该在 ( 10^{-8} ) 量级。如果误差大于 ( 10^{-4} ),说明解析梯度有bug。

我踩过的坑是:梯度检查通过不代表代码完全正确,因为数值梯度只采样了部分维度。有一次我的梯度在正确类别上少减了1,但采样点恰好没覆盖到,检查通过了,训练时准确率却上不去。所以梯度检查通过后,还要看训练曲线是否正常收敛。

4. 训练与调参:让模型真正跑起来

4.1 超参数搜索:学习率和正则化强度

作业要求用验证集调学习率和正则化强度。我的经验是先用对数尺度粗搜,再在最优附近细搜。比如学习率试[1e-7, 5e-7, 1e-6, 5e-6, 1e-5],正则化强度试[1e-5, 1e-4, 1e-3, 1e-2],组合起来跑一轮,看验证集准确率。

CIFAR-10上,Softmax分类器大概能到35%-38%的准确率。如果低于30%,大概率是学习率太大导致震荡,或者正则化太强导致欠拟合。如果训练准确率远高于验证准确率,说明过拟合了,该加大正则化。

学习率正则化强度训练准确率验证准确率
1e-71e-428%27%
1e-61e-436%35%
1e-51e-438%36%
1e-61e-233%32%

这张表是我当时跑出来的典型结果,可以看到学习率在1e-6到1e-5之间比较合适,正则化1e-4是个不错的起点。

4.2 权重可视化:看看模型学到了什么

训练完之后,把权重矩阵 ( W ) 的每一列还原成32x32x3的图像,可以直观看到每个类别的模板。你会发现Softmax学到的模板有点像类别的平均图像——比如猫的模板隐约有猫脸的轮廓,车的模板有车身的形状。这是因为线性分类器本质上是在找每个类别的“原型”,通过比较输入和原型的相似度来分类。

这个可视化不仅是作业要求,也是排查问题的好工具。如果某个类别的模板看起来像噪声,说明这个类别的样本太少或者学习率有问题。如果所有模板长得差不多,说明模型没学到东西,可能权重初始化太小或者学习率太低。

4.3 与SVM的对比:为什么Softmax更常用

Assignment1里同时实现了SVM和Softmax,两者都是线性分类器,但损失函数不同。SVM用的是合页损失(hinge loss),只关心正确类别的得分是否比错误类别高出至少一个边界值;Softmax用的是交叉熵,关心的是整个概率分布。

实际效果上,两者在CIFAR-10上的准确率差不多,但Softmax有两个优势:一是输出是概率,可以给出置信度;二是损失函数光滑可导,优化起来更稳定。这也是为什么后续的神经网络几乎都用Softmax加交叉熵的组合。

实操心得:如果你在Softmax上调参调不动了,可以回头看看SVM的实现,对比两者的梯度形式。SVM的梯度只在边界内非零,Softmax的梯度处处非零,这个差异在训练初期表现得特别明显——Softmax收敛更平滑,但每轮计算量稍大。

5. 常见报错与排查实录

5.1 损失变成nan:指数溢出的典型症状

这是最常见的报错,没有之一。症状是第一次迭代loss就是nan,或者训练几轮后突然变nan。原因几乎总是忘了做数值稳定化。检查你的代码里有没有scores -= np.max(scores, axis=1, keepdims=True)这一行。

还有一种隐蔽的情况:学习率太大,权重更新后得分变得极大,即使做了稳定化,exp之后仍然可能溢出。这时候要降低学习率,或者在权重更新后加一个裁剪。

5.2 准确率卡在10%:梯度方向反了

CIFAR-10有10个类别,随机猜测的准确率就是10%。如果训练半天准确率一直在10%附近,说明模型根本没学到东西。排查顺序:

  1. 检查梯度是否取反了。dscores应该是probs - 1在正确类别上,如果你写成了1 - probs,梯度方向就反了,参数会往错误方向更新。
  2. 检查损失函数是否正确。打印几个样本的loss,正确类别的概率应该随着训练增大。
  3. 检查数据预处理。如果忘了减均值或归一化,训练会非常慢甚至不收敛。

5.3 向量化后维度不匹配

从循环改向量化时,最常见的错误是维度对不上。比如X.T.dot(dscores)要求X.T的列数等于dscores的行数。X是(N, D),X.T是(D, N),dscores是(N, C),乘出来是(D, C),正好和W的形状一致。

如果报错说维度不匹配,先打印每个变量的shape,对照上面的维度关系逐个检查。我习惯在向量化代码里加一行assert来验证形状,比如assert dW.shape == W.shape,这样出错时能立刻定位。

报错信息可能原因解决方法
loss为nan指数溢出加数值稳定化
准确率10%梯度方向反检查dscores符号
维度不匹配转置或广播错误打印shape逐行核对
梯度检查失败链式法则漏项重新推导并对照公式
训练震荡学习率过大降低学习率一个量级

5.4 梯度检查的相对误差偏大

如果梯度检查的相对误差在1e-4到1e-2之间,说明梯度基本正确但有数值精度问题。常见原因是数值稳定化时减去的最大值在解析梯度里没有对应处理。实际上减去常数不影响梯度,因为常数项求导为0,但如果你在计算概率时用了不同的稳定化方式,可能导致微小差异。

另一个原因是正则化项的梯度。reg * np.sum(W * W)的梯度是2 * reg * W,别忘了乘2。我见过有人写成reg * W,梯度检查误差不大但训练效果差一截。

6. 从Softmax到神经网络:这个作业的后续价值

Softmax作业看起来只是Assignment1的一个小环节,但它埋下了后面所有内容的种子。你在这里手写的损失函数和梯度,在Assignment2的多层神经网络里会以模块化的形式复用;你在这里理解的数值稳定化,在实现Batch Normalization和Layer Normalization时会再次遇到;你在这里踩过的向量化坑,在写卷积层的前向传播时会以更复杂的形式重现。

我个人建议是,做完Softmax之后不要急着往下走,花半小时把梯度推导在纸上重新写一遍,不看代码,纯手推。推到 ( P - \mathbb{1} ) 这一步的时候,你会对“预测减真实”这个形式有肌肉记忆,后面看到任何分类损失都能快速反应出它的梯度长什么样。

最后分享一个小技巧:如果你想让Softmax分类器的准确率再往上提一两个点,可以试试在训练前对数据做PCA白化,或者用动量更新代替朴素的梯度下降。这些在作业里没有要求,但能帮你更直观地感受不同优化策略对线性模型的影响。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询