资讯动态

深度学习分类任务核心:Softmax与交叉熵损失原理、推导与实现

发布时间:2026/8/12 15:01:22 来源:尧图企业网站定制
1. 从分类任务到Softmax为什么我们需要它如果你做过图像分类、文本情感分析或者任何需要模型输出“属于哪个类别”的任务你肯定遇到过一个问题神经网络的最后一层那个叫“全连接层”的家伙它输出的是一堆毫无约束的实数我们称之为“logits”。这些数字可能很大可能很小可能有正有负它们直接代表了模型对每个类别的“原始打分”。但打分本身不是概率。我们没法告诉用户“模型认为这张图片有120.5分是猫-30.2分是狗。” 用户和后续的计算比如计算损失需要的是一个清晰的、符合概率公理的解释“模型有95%的把握认为这是猫5%认为是狗。” 这个将原始打分转化为合法概率分布的过程就是Softmax干的事情。Softmax的公式看起来挺简单对于第i个类别的logit值z_i其Softmax概率s_i为s_i exp(z_i) / Σ_j exp(z_j)这个公式妙在哪儿首先exp()指数函数确保了所有输出都是正数这是概率的基本要求。其次分母是所有类别指数值的和这保证了所有输出概率之和严格等于1构成了一个完美的概率分布。最后指数函数具有“放大”效应它会拉大logits之间的差距。假设两个logits分别是2.0和1.0经过Softmax后对应的概率大约是0.73和0.27如果差距扩大到3.0和1.0概率就变成了0.88和0.12。这让模型“有信心”的预测更加突出。在实际项目中比如用PyTorch或TensorFlow你几乎不用手写Softmax框架已经提供了torch.nn.Softmax(dim1)或tf.nn.softmax。但这里有个新手常踩的坑维度dim参数。如果你的输入张量形状是[batch_size, num_classes]那么dim1表示沿着类别维度进行Softmax为每个样本独立计算一个概率分布。如果设错成dim0就变成了跨样本计算结果完全错误。我早期就因为这个bug导致模型损失不下降排查了半天。注意在训练阶段我们通常不显式调用Softmax层而是将Softmax的计算与Cross-entropy Loss合并使用nn.CrossEntropyLoss()。这个Loss函数内部会先算Softmax再算交叉熵。这样做在数值上更稳定框架有优化代码也更简洁。只有在模型推理预测时为了得到可解释的概率值我们才需要显式地加上Softmax。2. 交叉熵损失衡量概率距离的“尺子”现在我们有了模型预测的概率分布s比如[0.9, 0.1]表示猫和狗以及真实的标签。对于分类任务真实标签通常用**独热编码one-hot**表示例如猫是[1, 0]狗是[0, 1]。这个真实分布我们记作y。我们需要一把“尺子”来衡量预测分布s和真实分布y之间的差距。这把尺子就是交叉熵损失Cross-entropy Loss。它的直观理解是用预测分布s去编码真实分布y所需要的平均信息量比特数。当预测完全正确时s和y一模一样这个信息量最小损失为0预测越离谱损失越大。对于单个样本交叉熵损失的公式是L - Σ_j y_j * log(s_j)由于y是独热编码只有真实类别t的位置是1其他都是0。所以这个求和公式瞬间简化了L - log(s_t)看交叉熵损失在分类任务中本质上就是真实类别对应预测概率的负对数这个结论极其重要。它意味着我们只关心模型对真实类别的预测概率有多大。损失L随着s_t的增大而减小。s_t越接近1模型越自信且正确-log(s_t)越接近0。函数-log(x)在x接近0时值会急剧增大。这给了模型一个“严厉的惩罚”如果你把真实类别的概率预测得非常低比如0.001损失会变得非常大约等于6.9梯度也会很大迫使模型在下次更新时猛烈调整参数。在代码里我们这样使用它PyTorch示例import torch import torch.nn as nn # 假设一个batch有2个样本3个类别 logits torch.tensor([[2.0, 1.0, 0.1], # 样本1的logits [0.5, 2.0, -1.0]]) # 样本2的logits # 真实标签样本1是第0类样本2是第1类 labels torch.tensor([0, 1]) # 方法1使用组合的CrossEntropyLoss推荐 criterion nn.CrossEntropyLoss() loss criterion(logits, labels) # 内部含Softmax print(f组合Loss: {loss.item()}) # 方法2手动分解步骤用于理解 softmax nn.Softmax(dim1) probs softmax(logits) print(f预测概率:\n{probs}) # 根据公式 L -log(s_t) 手动计算 manual_loss -torch.log(probs[torch.arange(2), labels]).mean() print(f手动计算Loss: {manual_loss.item()})你会发现两种方法计算出的损失值是一样的。但务必记住nn.CrossEntropyLoss的输入是logits而不是已经过Softmax的概率。这是初学者最容易混淆的地方之一。3. 梯度的推导反向传播的核心引擎理解了前向传播如何从logits得到损失下一步就是重头戏反向传播。模型要学习就必须知道每个参数比如权重W和偏置b对总损失L的“贡献”有多大是正是负以便沿着减少损失的方向更新它们。这个“贡献”就是损失L对参数θ的偏导数也就是梯度∂L/∂θ。我们的计算图是θ - z (logits) - s (Softmax probs) - L (Cross-entropy Loss)。根据链式法则∂L/∂θ (∂L/∂z) * (∂z/∂θ)。∂z/∂θ相对简单就是全连接层本身的梯度。关键在于∂L/∂z即损失对logits的梯度。这个梯度将指导logits应该如何变化才能降低损失。我们来手推一下∂L/∂z。设总共有C个类别。已知L -log(s_t)其中t是真实类别的索引。Softmax函数s_i exp(z_i) / Σ_k exp(z_k)记S Σ_k exp(z_k)。求导我们需要∂L/∂z_j对于任意一个logitz_jj可以是真实类别t也可以是其他类别。首先∂L/∂s_i -1/s_t当i t否则为0。因为L只直接依赖于s_t。然后需要∂s_i/∂z_j。这是Softmax的雅可比矩阵需要分情况讨论当i j时∂s_i/∂z_j s_i * (1 - s_j)当i ≠ j时∂s_i/∂z_j -s_i * s_j应用链式法则∂L/∂z_j Σ_i (∂L/∂s_i) * (∂s_i/∂z_j)。由于∂L/∂s_i仅在it时非零这个求和大大简化了。对于真实类别j t∂L/∂z_t (∂L/∂s_t) * (∂s_t/∂z_t) (-1/s_t) * [s_t * (1 - s_t)] s_t - 1对于非真实类别j ≠ t∂L/∂z_j (∂L/∂s_t) * (∂s_t/∂z_j) (-1/s_t) * [-s_t * s_j] s_j推导结果令人惊喜地简洁∂L/∂z_j s_j - y_j其中y_j是独热编码真实标签的第j位对于真实类别ty_t1对于其他类别y_j0。这个结果太优美了损失对logits的梯度就等于模型的预测概率分布s减去真实的标签分布y。对于真实类别梯度是(s_t - 1)一个负数意味着需要增大z_t对于其他类别梯度是s_j一个正数因为概率为正意味着需要减小z_j。这完全符合直觉模型应该增强对正确类别的“信心”削弱对错误类别的“信心”。这个简洁的梯度形式是Softmax配合交叉熵损失被称为“黄金搭档”的主要原因之一它使得反向传播非常高效和稳定。4. 从理论到代码完整的实现与数值稳定性陷阱理解了原理和梯度我们现在可以尝试脱离深度学习框架用纯Python和NumPy实现一个完整的、包含前向传播和反向传播的Softmax分类层。这能让你彻底吃透每一个计算步骤。我们先来实现前向传播并重点解决数值稳定性问题。import numpy as np def softmax_forward(logits): 计算Softmax概率。 参数: logits: 形状为 (N, C) 的numpy数组N是样本数C是类别数。 返回: probs: Softmax概率形状同logits。 # 关键步骤减去最大值防止指数爆炸 # logits中的每个样本独立处理 max_vals np.max(logits, axis1, keepdimsTrue) shifted_logits logits - max_vals # 现在最大值是0 exp_vals np.exp(shifted_logits) sum_exp np.sum(exp_vals, axis1, keepdimsTrue) probs exp_vals / sum_exp return probs def cross_entropy_forward(probs, labels): 计算交叉熵损失。 参数: probs: Softmax概率形状 (N, C)。 labels: 真实类别索引形状 (N,)。 返回: loss: 标量平均损失。 cache: 存储反向传播需要的中间变量 (probs, labels)。 N probs.shape[0] # 获取每个样本真实类别对应的概率 correct_class_probs probs[np.arange(N), labels] # 计算损失L -log(s_t) losses -np.log(correct_class_probs) loss np.mean(losses) cache (probs, labels) return loss, cache这里有个至关重要的技巧logits - max(logits)。因为指数函数exp(x)增长极快如果logits数值较大比如几百直接计算exp会导致溢出得到inf。通过减去最大值使得最大值为0我们保证了exp的输入全部 ≤ 0结果在 (0, 1] 之间彻底避免了溢出风险同时不改变Softmax的结果因为分子分母同除以exp(max)。接下来是实现反向传播也就是计算梯度。def softmax_cross_entropy_backward(cache): 计算损失对输入logits的梯度。 参数: cache: 前向传播保存的 (probs, labels)。 返回: d_logits: 梯度形状同输入logits。 probs, labels cache N probs.shape[0] # 初始化梯度矩阵 d_logits probs.copy() # 形状 (N, C) # 根据公式 ∂L/∂z_j s_j - y_j # 对于每个样本将其真实类别位置的梯度减1 d_logits[np.arange(N), labels] - 1 # 因为前向传播计算了平均损失所以这里的梯度也要除以N d_logits / N return d_logits # 整合测试 def test_implementation(): np.random.seed(42) N, C 3, 5 # 随机生成logits和标签 logits np.random.randn(N, C) * 2 labels np.random.randint(0, C, size(N,)) # 前向传播 probs softmax_forward(logits) loss, cache cross_entropy_forward(probs, labels) print(f预测概率 (每行和为1):\n{probs}) print(f真实标签: {labels}) print(f计算得到的损失: {loss:.4f}) # 反向传播 d_logits softmax_cross_entropy_backward(cache) print(f\n损失对logits的梯度形状: {d_logits.shape}) print(f梯度示例 (第一个样本): {d_logits[0]}) # 梯度检查使用数值梯度近似验证我们解析梯度的正确性 def loss_function(logits_flat): logits_reshaped logits_flat.reshape(N, C) probs softmax_forward(logits_reshaped) loss, _ cross_entropy_forward(probs, labels) return loss from scipy.optimize import approx_fprime # 将logits展平以进行梯度检查 logits_flat logits.flatten() numerical_grad approx_fprime(logits_flat, loss_function, epsilon1e-7) numerical_grad numerical_grad.reshape(N, C) analytical_grad d_logits # 比较数值梯度和解析梯度 grad_diff np.abs(numerical_grad - analytical_grad).max() print(f\n梯度检查 - 最大差异: {grad_diff:.10f}) if grad_diff 1e-6: print(✅ 梯度计算正确) else: print(❌ 梯度计算可能有误。) if __name__ __main__: test_implementation()这个手动实现清晰地展示了整个流程。softmax_cross_entropy_backward函数的核心就是一行代码d_logits[np.arange(N), labels] - 1它完美地体现了我们推导出的梯度公式s - y。梯度检查Gradient Check是验证自定义层实现是否正确的重要手段通过比较解析梯度和数值梯度可以确保你的推导和代码没有错误。5. 框架中的高效实现与高级话题在实际的深度学习框架中实现远比我们的教学版本复杂和高效。以PyTorch的nn.CrossEntropyLoss为例它做了几件重要的事情数值稳定性优化它使用了我们提到的“减去最大值”技巧并且可能结合了Log-Sum-Exp (LSE) 的稳定算法在计算log(softmax)时一步到位避免中间数值问题。类权重与忽略索引支持weight参数给不同类别设置不同的损失权重用于处理类别不平衡问题。也支持ignore_index来忽略某些特定标签如填充符。标签平滑Label Smoothing这是一个非常重要的正则化技术。传统的独热编码过于“绝对”正确类为1其他为0可能导致模型过度自信和过拟合。标签平滑将真实标签分布改为y [0.9, 0.1]对于二分类正确类0.9错误类0.1这相当于在训练中加入了噪声鼓励模型不要给出过于极端的概率提升了泛化能力。PyTorch的CrossEntropyLoss通过label_smoothing参数直接支持。# 使用标签平滑的交叉熵损失 criterion nn.CrossEntropyLoss(label_smoothing0.1)与优化器的配合计算出的梯度∂L/∂z s - y会继续反向传播到更早的网络层。优化器如SGD, Adam根据这些梯度更新权重W和偏置b。对于全连接层z XW b其梯度∂L/∂W X^T * (∂L/∂z)∂L/∂b sum(∂L/∂z, axis0)。框架自动完成了所有这些链式法则的计算。理解这些底层细节能让你在遇到问题时不再像个黑盒用户。例如当你的模型损失出现NaN时你可能会怀疑是梯度爆炸。但如果你知道Softmax的数值稳定性处理你就会先检查输入logits是否过大或者考虑在损失函数中加入微小的epsilon防止log(0)。当模型在验证集上表现不佳时你可能会想到尝试标签平滑来缓解过拟合。6. 超越分类Softmax与交叉熵的变体与应用Softmax和交叉熵的组合并不仅限于多分类。理解其本质后你可以将其应用到许多变体任务中。1. 多标签分类Multi-label Classification多分类是“单选”一个样本只属于一个类多标签是“多选”一个样本可以同时属于多个类比如一张图片包含“天空”和“云”两个标签。此时真实标签y不再是独热编码而是多个位置为1的向量如[1, 0, 1, 0]。我们不能再用原始的Softmax因为它强制输出和为1。解决方案是将每个类别视为独立的二分类问题对每个logit使用Sigmoid函数输出一个独立的概率然后用二元交叉熵损失Binary Cross-Entropy Loss, BCE Loss。# PyTorch 多标签分类示例 bce_loss nn.BCEWithLogitsLoss() # 内置Sigmoid # logits 和 targets 形状相同targets中每个位置是0或1 loss bce_loss(logits, targets)2. 蒸馏Knowledge Distillation在模型压缩中我们用一个笨重但性能好的“教师模型”去教一个轻量级的“学生模型”。这里Softmax被改进为带温度参数T的Softmaxs_i exp(z_i / T) / Σ_j exp(z_j / T)当T1时是标准SoftmaxT1时概率分布变得更“平滑”揭示了教师模型学到的类间相似性等暗知识dark knowledge。学生模型不仅学习真实标签还学习教师模型的软标签soft labels通常使用两个交叉熵损失的加权和。3. 注意力机制Attention Mechanism这是Transformer架构的核心。在自注意力中Q和K的点积得分logits经过Softmax后得到的就是注意力权重表示在生成当前词时应该“注意”序列中其他词的强度。这里的Softmax将注意力分数归一化为一个权重分布其梯度形式同样简洁使得Transformer能够高效训练。从最基本的分类任务到这些前沿的应用Softmax和交叉熵损失作为深度学习的基石其简洁的形式、优雅的梯度以及稳定的数值性质使其成为了不可或缺的工具。亲手推导一遍实现一遍再去看框架的源码你会对正在运行的模型有完全不同的、更深层次的控制感和理解。下次当你的分类模型训练出现问题时你不再只是盲目地调整超参而是可以有理有据地分析数据流、梯度值真正地开始“调试”你的模型。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价