ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

深度学习入门:Softmax分类器损失函数与梯度推导实战

深度学习入门:Softmax分类器损失函数与梯度推导实战 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(yk|x) \frac{e^{s_k}}{\sum_j e^{s_j}} ]其中 ( s_k ) 是第k类的得分。指数运算保证了所有输出为正除以总和保证了加起来等于1。这样我们就得到了一个合法的概率分布可以套用交叉熵损失来衡量预测和真实标签的差距。交叉熵损失的形式是 ( L_i -\log P(yy_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, axis1, keepdimsTrue) # 数值稳定 exp_scores np.exp(scores) probs exp_scores / np.sum(exp_scores, axis1, keepdimsTrue) # (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 ) 完成对所有样本的累加。注意keepdimsTrue这个参数很容易漏。如果不加np.max(scores, axis1)返回的是形状(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, axis1, keepdimsTrue)这一行。还有一种隐蔽的情况学习率太大权重更新后得分变得极大即使做了稳定化exp之后仍然可能溢出。这时候要降低学习率或者在权重更新后加一个裁剪。5.2 准确率卡在10%梯度方向反了CIFAR-10有10个类别随机猜测的准确率就是10%。如果训练半天准确率一直在10%附近说明模型根本没学到东西。排查顺序检查梯度是否取反了。dscores应该是probs - 1在正确类别上如果你写成了1 - probs梯度方向就反了参数会往错误方向更新。检查损失函数是否正确。打印几个样本的loss正确类别的概率应该随着训练增大。检查数据预处理。如果忘了减均值或归一化训练会非常慢甚至不收敛。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白化或者用动量更新代替朴素的梯度下降。这些在作业里没有要求但能帮你更直观地感受不同优化策略对线性模型的影响。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表