尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

交叉熵损失函数全解析:从信息论到PyTorch实战与YOLO/LLM应用

交叉熵损失函数全解析:从信息论到PyTorch实战与YOLO/LLM应用 做深度学习这几年交叉熵损失函数CrossEntropy Loss大概是写代码时出现频率最高的三个词之一。随便打开一个图像分类、目标检测甚至如今大模型预训练的脚本训练日志里那个不断下降的 loss绝大多数都是交叉熵。可我见过太多初学者把loss F.cross_entropy(...)当成黑盒咒语来用换数据集不知道该不该换损失函数画出来的损失曲线看不懂模型不收敛也分不清是学习率问题还是损失函数配错了。这篇文章我打算把交叉熵这件事从头到尾拆开讲一遍包括它的信息论源头、二分类和多分类的落地写法、训练曲线的正确打开方式以及它在 YOLO、LLM 预训练这些热点场景里的变体用法最后附上我的踩坑笔记。1. 先搞懂它到底在衡量什么交叉熵不是惩罚函数是信息差度量1.1 从熵说起一个事件的信息量怎么算很多人一看到熵这个字就头大其实它最初的想法特别朴素。一个事件发生的概率越低它带来的信息量就越大。比如说明天太阳从东边升起这句话信息量约等于零因为概率是 1人人都知道但如果有人告诉你明天要降温 15 度这个事件概率低信息量就大。信息论里把某个事件的自信息量定义为I(x) -log(P(x))概率越小-log 越大信息量越大。而熵就是所有可能事件信息量的期望也就是平均信息量H(p) -Σ p(i) log(p(i))它衡量的是一个概率分布本身的不确定程度。均匀分布熵最大因为完全猜不到下一个样本是什么一个几乎确定的事件分布熵接近 0。1.2 交叉熵和 KL 散度到底什么关系当我们训练一个分类模型比如手写数字识别模型对一张图片会输出一组概率比如 [0.1, 0.6, 0.3]表示是 0 的概率 10%是 1 的概率 60%是 2 的概率 30%。而真实标签是 one-hot 形式比如 [0, 1, 0]。我们当然希望模型的输出 q 尽量接近真实分布 p。交叉熵的公式是H(p, q) -Σ p(i) log(q(i))它可以拆成两部分H(p, q) H(p) KL(p || q)其中 H(p) 是真实分布本身的熵在数据集固定时是个常数KL 散度衡量的是用 q 去近似 p 时多出来的信息量。所以最小化交叉熵本质上就是在最小化 KL 散度也就是把预测分布 q 往真实分布 p 上推。这里有个关键细节为什么是交叉熵而不是直接用真实分布 p 去做因为真实分布我们并不知道我们只有样本和标签。one-hot 标签可以看作对真实分布 p 的一个采样近似。交叉熵既衡量了模型输出和样本标签之间的差距又不要求我们预先知道 p 的具体形状这是它能成为分类任务默认损失的根本原因。1.3 为什么不直接用 MSE 做分类一个新手常问的问题回归用 MSE均方误差分类为什么不能用表面上也可以但实际效果差很多我实测下来主要有两个原因。第一是梯度问题。分类任务最后一层几乎都是 softmax把输出变成概率。如果接 MSE梯度里会多乘一项 softmax 导数 p(1-p)当模型的预测已经很自信p 接近 0 或 1时这项趋近于 0梯度消失模型学不动。而交叉熵加 softmax 的梯度是 (p - y)也就是预测概率减真实标签这个误差信号非常直接预测错了多少就回传多少不会因为置信度饱和而消失。第二是语义问题。MSE 是把概率当成普通数值去拟合比如真实标签是 1模型输出 0.7MSE 认为是 0.09 的误差但交叉熵会认为这个误差更大因为模型对应该是 1的事情只有 70% 把握信息损失是 log(1/0.7)。分类任务的评价标准是概率校准不是数值逼近交叉熵的语义更贴合。2. 二分类和多分类的公式与 PyTorch 落地写法2.1 二分类交叉熵BCE的两种等价写法二分类的交叉熵长这样L -[ y * log(p) (1 - y) * log(1 - p) ]其中 y 是真实标签 0 或 1p 是模型预测的正类概率。这个式子很直观当 y1 时只看 log(p)模型预测越接近 1 loss 越小当 y0 时只看 log(1-p)。PyTorch 里有两个容易混淆的接口。nn.BCELoss要求输入已经是经过 sigmoid 的概率而nn.BCEWithLogitsLoss直接吃 logits内部把 sigmoid 和 BCE 合成一步。我强烈建议用后一个原因很简单数值稳定。直接先算 sigmoid 再算 log当 logits 绝对值很大时sigmoid 结果可能被舍入成 0 或 1导致 log(0) 得到无穷大。PyTorch 的BCEWithLogitsLoss用了 log-sum-exp 的技巧把这两步合并计算避免了中间精度丢失。2.2 多分类交叉熵softmax、log、NLLLoss 的黄金组合多分类的交叉熵是二分类的自然推广对所有类别求和L -Σ y_i * log(q_i)其中 y 是 one-hot 标签q 是模型输出的各类概率。因为 y 只有一个位置是 1所以实际只计算真实类别那一项的 -log(q_correct)。这就是为什么多分类交叉熵经常被简化成正确类别的概率取负对数。在 PyTorch 里nn.CrossEntropyLoss接收的是原始 logits它会依次做三件事softmax 把 logits 变成概率分布取 log用 NLLLoss 取出真实类别对应的负对数所以下面三行代码在数学上是等价的import torch import torch.nn as nn import torch.nn.functional as F logits torch.randn(8, 10) # 8个样本10类 labels torch.randint(0, 10, (8,)) # 方式一直接用 CrossEntropyLoss loss1 nn.CrossEntropyLoss()(logits, labels) # 方式二手动 LogSoftmax NLLLoss loss2 nn.NLLLoss()(F.log_softmax(logits, dim1), labels) # 方式三分开算 probs F.softmax(logits, dim1) log_probs torch.log(probs 1e-10) # 自己加 epsilon 防止 log(0) loss3 F.nll_loss(log_probs, labels)三种方式结果几乎一样但方式一的底层实现最稳不要自己手写 log(softmax(x)) 的展开式。2.3 为什么 CrossEntropyLoss 自带 Softmax 却不接受概率输入这是个高频困惑点既然交叉熵需要概率为什么接口不直接收 softmax 之后的概率核心还是数值稳定性。softmax 的分子是 e^x当 logits 很大时 e^x 直接溢出变成 inf当 logits 很小时又可能下溢成 0。PyTorch 内部会在计算 log_softmax 时先减去最大值做平移log_softmax(x_i) x_i - max(x) - log(Σ e^(x_j - max(x)))这样所有指数运算的输入都被限制在一个安全范围内计算出的结果也是精确的 log 概率。如果你自己在外面先 softmax 再 log很难避免中间溢出尤其是训练初期 logits 可能非常大。所以记住一个原则nn.CrossEntropyLoss的输入是 logits不是概率。有次我同事写了loss criterion(softmax(logits), labels)训练一直不收敛排查半天发现模型输出分布被二次 softmax 压得太平梯度的方向已经不对了。2.4 处理不规则标签ignore_index、类别权重和输出形状实际项目里标签很少是规规矩矩的 0 到 C-1。做语义分割时很多数据集把边界、模糊区域标成 255 表示忽略做文本分类时padding 位置也要屏蔽。nn.CrossEntropyLoss的ignore_index参数就是干这个的criterion nn.CrossEntropyLoss(ignore_index255)它会自动把标签为 255 的位置排除在 loss 计算之外同时也不计入分母。类别不平衡时可以传weight参数给少数类更高权重。这个权重直接乘在每一项 loss 上等价于对每个样本的贡献做缩放。需要注意weight和ignore_index可以同时用但二者叠加时少数类的梯度会被放大得比较厉害我一般建议先用weight的平方根再配合学习率下调避免少数类过拟合。输出形状也要留意。图像分类时 logits 是 (N, C)标签是 (N,)语义分割时 logits 是 (N, C, H, W)标签是 (N, H, W)Channel 维必须放在第 1 维。有些框架用 (N, H, W, C) 布局转到 PyTorch 时忘了permuteloss 直接形状报错这类问题我看过太多次了。3. 训练曲线图画出来不等于看懂损失曲线怎么看才有信息量3.1 一套能直接用的 loss 记录与绘制代码热搜里很多人搜yolov8 画损失函数曲线图说明大家训练时都有看曲线的需求但很多人的画法是记录每个 epoch 的 loss最后连个线看完啥也判断不出来。我分享一套稍微讲究点的做法。import matplotlib.pyplot as plt def record_training(train_losses, val_losses, save_pathloss_curve.png): epochs range(1, len(train_losses) 1) plt.figure(figsize(10, 5)) plt.plot(epochs, train_losses, b-, labeltrain loss, linewidth2) plt.plot(epochs, val_losses, r--, labelval loss, linewidth2) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.grid(True, alpha0.3) plt.savefig(save_path, dpi150, bbox_inchestight) plt.show()有个细节很多人忽略横轴用 epoch 还是 step 差别很大。训练前期一个 epoch 内 loss 波动很大按 step 画会看到一条剧烈抖动的曲线容易让人误判不收敛。我习惯在同一个图里画两条线一条是每个 step 的原始 loss浅色细线一条是滑动平均深色粗线这样既能看出震荡幅度也能看清趋势。3.2 三条曲线状态判断模型健康度看 loss 曲线的核心不是看它降不降而是看训练 loss和验证 loss的间距。训练 loss 下降、验证 loss 也同步下降并趋于平稳正常。验证 loss 比训练 loss 高一点是正常的高出的部分就是泛化差距。训练 loss 持续下降、验证 loss 先降后反弹过拟合信号。此时不要盲目调学习率先考虑加正则、加数据增强或者提前停止。训练 loss 和验证 loss 都高且几乎不降欠拟合或模型结构问题。常见原因是学习率太小、特征提取能力不足或者损失函数配错了。loss 在某个值附近长期震荡不降可能是学习率偏大也可能是 batch size 太小导致梯度噪声过大。损失曲线还能用来判断学习率是否合理。如果 loss 一开始就飙到无穷大NaN大概率是学习率过大如果 loss 平滑得像一条直线说明学习率可能太小模型几乎没在学。我在实际工作中经常用学习率预热 余弦退火组合曲线会很漂亮但漂亮的曲线只说明训练过程稳定不代表模型一定好用还得看验证集指标。3.3 YOLOv8 输出的那几张 loss 曲线到底在画什么YOLOv8 训练结束后会在runs/detect/train*/results.png生成一组曲线里面包含train/box_loss、train/cls_loss、train/dfl_loss和对应的验证集曲线。很多同学盯着 cls_loss 看其实未必理解它的含义。box_loss边界框回归损失YOLOv8 用的是 CIoU 和 DFLDistribution Focal Loss的组合衡量预测框和真实框的重合程度。cls_loss分类损失就是交叉熵的变体。YOLO 的本质是多标签分类一个 anchor 可能同时属于多个类别所以它用的是 BCE 而不是多分类 CE对每个类别独立做二分类。dfl_loss分布焦点损失是 YOLOv8 对边界框坐标做离散分布建模时用的。看这组曲线的正确姿势是先确认三个 loss 都在稳定下降再看 val 曲线和 train 曲线的差距是否随训练拉大如果 val 曲线在第 100 个 epoch 附近掉头上扬说明过拟合可能 token 增强、数据增强没跟上。另外YOLOv8 默认训练 300 个 epoch但如果 cls_loss 在 150 epoch 后基本走平再加训练轮次收益很小我一般会提前用patience参数让它停止。4. 从分类到目标检测和 LLM 预训练交叉熵在真实项目里的变体用法4.1 YOLO 损失函数为什么不是一个交叉熵如果你搜yolo 损失函数会发现 YOLO 的损失是好几项的加和交叉熵只是其中的分类分支。YOLO 把目标检测拆成三个子任务物体在哪框回归、有没有物体置信度、是什么物体分类。对应的 loss 分别是L λ_box * L_box λ_obj * L_obj λ_cls * L_cls其中 L_cls 就是分类交叉熵L_obj 是置信度分支的 BCEL_box 是框回归的 CIoU 等。为什么框回归不用交叉熵因为框坐标是连续值交叉熵天生处理离散类别用 MSE/CIoU 这类回归损失更自然。这里有个容易踩的认知误区把yolo 损失函数当成一个固定公式去背。实际上不同版本的 YOLO 用的回归损失一直在换但分类分支始终是 BCE原因是目标检测的类别设计是多标签 独立二分类一张图里有猫又有狗用多分类 softmax 交叉熵就无法表达这种同时存在的语义。所以当你需要处理多标签图像分类任务时正确的选择是 BCE而不是 CrossEntropyLoss。4.2 LLM 预训练下一个词预测本质上就是超大规模交叉熵现在最火的llm 预训练 损失函数热搜背后其实就是交叉熵的极致放大版。GPT 系列的目标是给定前面的 token预测下一个 token 的概率分布然后取真实下一个 token 位置的负对数作为 lossL -log P(token_t | token_1, ..., token_{t-1})这个过程对序列里的每个位置求交叉熵然后对所有位置取平均。可以说一次 forward 做了几十万次交叉熵计算只是每次的类别数是词表大小几万到十几万。有个判断训练是否有问题的技巧LLM 预训练刚开始时模型几乎随机输出loss 应该在 log(V) 附近V 是词表大小。比如词表 50k初始 loss 应该在 log(50000) ≈ 10.8 左右。如果你看到的初始 loss 远小于这个值说明数据和 tokenizer 可能有问题模型还没学就先猜中了很多位置。训练后期 loss 降到 2 到 3 时perplexity 大约是 exp(2.5) ≈ 12这个解释方式在做模型评估时经常用到。4.3 标签平滑Label Smoothing在分类和 LLM 里的作用交叉熵有个不太招人喜欢的特性它对正确类别概率接近 1的样本给极小 loss于是模型会被鼓励做得过度自信甚至去死记硬背训练集。标签平滑的做法是把 one-hot 标签改造成软标签y_smooth (1 - ε) * y_onehot ε / CC 是类别数ε 一般取 0.1。比如 10 分类的 one-hot 是 [0,0,1,0,...]平滑后变成 [0.01, 0.01, 0.91, 0.01, ...]。这样模型不需要把正确类别的概率推到无穷接近 1缓解过拟合也让输出分布更接近真实世界的模糊性。在 LLM 预训练里标签平滑同样有效但不建议用太大。语言本身有很强的确定性一个词在给定上下文下如果不是唯一选择过大的平滑会抹掉那些天然的多样性导致生成质量下降。我见过的做法是 ε 取 0.01 到 0.1 之间拿小规模数据先对比实验再决定。5. 我从实际项目中踩过的交叉熵相关的坑5.1 类别不平衡交叉熵被多数类带偏分类任务里最常见的坑是类别不均衡。比如缺陷检测99.9% 的像素是正常背景0.1% 是缺陷直接上交叉熵模型只要全部预测成背景就能拿到 0.001 的 loss训练出来就是个全预测背景的废物模型。我的处理顺序是先试weight参数给少数类加权不够再用分段损失比如 Dice Loss 或 Focal Loss。Focal Loss 是在交叉熵前面乘一个调制因子 (1-p)^γγ 取 2让模型把注意力从易分样本转移到难分样本上。注意 Focal Loss 不是交叉熵的替代品它只是改变了每个样本的权重底层用的还是交叉熵的 log 项。还有一个细节做二分类不均衡时BCEWithLogitsLoss有个pos_weight参数可以直接调整正类的权重比自定义 weight 向量更精细也不需要改网络结构。实测下来pos_weight 设为正负样本比值的平方根往往比直接设成比值更稳。5.2 噪声标签下交叉熵的过拟合问题交叉熵对错误标签非常敏感因为正确类别的概率被强制推到 1模型会主动记忆那些标错的样本。我自己做过一个数据清洗实验给 CIFAR-10 的训练集随机注入 20% 的错误标签用标准交叉熵训练测试准确率掉 15 个百分点以上换成标签平滑后只掉 8 个点左右。另一个思路是置信度惩罚或提前停止。因为模型是先学简单干净样本后记忆噪声样本所以早停能避免记忆阶段。更复杂的做法是动态调整样本权重比如让模型对每个样本的预测置信度反过来决定它的 loss 权重这个思路衍生出了 Co-teaching、DivideMix 等一系列方法但工程复杂度高一般项目不推荐一上来就上这种方案。5.3 最后一层的激活函数和损失函数配对问题这是一个特别低级但极其常见的坑。nn.CrossEntropyLoss自带 softmax所以模型最后一层直接输出 logits 即可不要再手动加 softmax而nn.BCEWithLogitsLoss自带 sigmoid同样不要在最后一层加 sigmoid。反过来如果你用nn.BCELoss或nn.NLLLoss就必须在模型里手动加对应的激活。有次我帮人排查一个二分类模型不收敛发现他在模型定义里已经加了一层 sigmoid训练时又用BCEWithLogitsLoss等于对 sigmoid 输出又做了一次 logits 解释梯度方向完全错了。这种问题日志里通常不会报错loss 甚至会缓慢下降但最终 AUC 上不去特别迷惑人。诊断方法很简单打印模型最后一层的输出范围。如果输出落在 [0, 1] 之间说明已经过了 sigmoid损失函数要用BCELoss如果输出范围是 [-∞, ∞]说明是 logits用BCEWithLogitsLoss。5.4 训练早期 loss 是 NaN 的排查链路NaN 是交叉熵训练中最让人头疼的问题我总结了一个固定排查顺序第一步看学习率太大会导致 loss 爆炸成 NaN先降到原来的 1/10 试试第二步看输入数据有没有 NaN 或无穷大混进特征第三步看模型最后一层的 logits 有没有异常大值可以用 hook 打印出来第四步看标签有没有超出类别范围比如 10 分类任务标签里混进了 11这个在CrossEntropyLoss里会直接报 index out of bounds但某些数据增强库切图时很容易引入这种错误报错还不明显。另外一个容易忽略的点混合精度训练AMP下交叉熵偶尔会出现梯度下溢导致的 NaN特别是分类数特别多的任务。这时候可以给 logits 乘一个缩放系数或者把关键层保持 float32不要全部半精度。最后说点实在的。交叉熵看似只是一个公式但它是分类任务、目标检测、语义分割、生成模型、大模型预训练这些领域的公共底座。什么时候该用它、什么时候该换 BCE、什么时候该加标签平滑这些判断能力是在一次次看曲线、排查 NaN、处理不均衡数据中练出来的。如果你刚接触我建议别急着背公式先拿一个公开数据集跑通训练然后把损失函数换着试观察曲线和指标的变化比看十篇教程都有用。
返回列表