
交叉熵损失函数是我这几年做深度学习项目绕不开的一个老朋友。从最早跑图像分类到后来调目标检测、再到现在折腾大语言模型的预训练和微调几乎每个任务里都能看到它的身影而且往往还是决定模型能不能收敛、收敛到什么程度的关键角色。很多人对交叉熵的理解停留在“分类任务用它就对了”这个层面但真到换业务场景、改模型结构、调loss曲线的时候就会发现只知其然远远不够。这篇博文我打算从信息论原理解剖、分类任务的选型逻辑、手推导公式与代码实现到YOLO、LLM预训练这类主流场景下的实际用法和画loss曲线的调试技巧完整过一遍交叉熵损失函数。适合正在学深度学习的学生、刚入门CV/NLP的工程师以及那些被val loss曲线折磨得想摔键盘的调参选手。看完你会明白交叉熵背后到底在算什么、为什么分类任务几乎离不开它以及实际工程里那些踩坑经验。1. 从信息论视角理解交叉熵它到底在衡量什么1.1 信息量、熵与真实分布的关系要理解交叉熵得先从“信息量”这个概念说起。信息论里一个事件发生的概率越低它携带的信息量越大。比如“明天太阳从东边升起”这件事概率接近1说了等于没说信息量趋近于0而“明天会下暴雨导致航班取消”概率很低这个信息才值得关注。信息量用公式表示就是 -log(p)p是事件发生的概率底数通常取2或自然常数e对应单位分别是bit和nat。把所有可能事件的信息量按概率加权求平均就得到了熵Entropy。熵衡量的是一个概率分布自身的不确定程度分布越均匀熵越大分布越集中熵越小。拿抛硬币来说如果是公平硬币正面和反面各0.5熵就是1 bit如果硬币被做了手脚正面概率0.9反面0.1熵就只有大约0.47 bit因为结果相对可预测了。在机器学习场景下我们通常把真实数据的分布记为P模型预测出来的分布记为Q。比如一张图片真实标签是“猫”在one-hot编码下P就是猫的位置是1、其他位置是0的确定分布。此时这个真实分布P的熵是多少是0因为概率全部集中在一个类别上没有任何不确定性。但模型预测出来的Q往往是一组概率值比如猫0.7、狗0.2、鸟0.1。这个时候就有意思了真实分布P是确定的但我们要衡量“用模型分布Q去描述真实分布P”时平均需要多少信息量。这个概念就叫交叉熵H(P, Q) -∑P(x)logQ(x)。1.2 KL散度与交叉熵的本质联系交叉熵和KL散度是一对形影不离的概念。KL散度衡量的是两个分布之间的差异公式是D_KL(P‖Q) ∑P(x)log(P(x)/Q(x))。稍微变形一下D_KL(P‖Q) ∑P(x)logP(x) - ∑P(x)logQ(x)右边第一项是真实分布P的熵的负值也就是 -H(P)第二项就是交叉熵H(P, Q)。于是有了一个极其重要的关系式H(P, Q) H(P) D_KL(P‖Q)这个公式解释了为什么在机器学习里我们常常只优化交叉熵就够了。因为在训练时真实分布P是固定的所以H(P)是一个常数不随模型参数变化。此时最小化交叉熵等价于最小化KL散度也就是让预测分布Q尽可能逼近真实分布P。很多教材在讲到这里时会强调分类任务中真实标签通常是one-hot分布因此H(P)0交叉熵就等于负的对数似然也就是公式 -logQ(真实类别概率)。这也是为什么交叉熵损失可以理解为“鼓励模型给正确类别的预测概率尽量大”。1.3 为什么实际训练用的是交叉熵而不是KL散度你可能会有个疑问既然KL散度更直观地表示“两个分布的距离”那为什么训练时不用KL散度当loss原因是KL散度的表达式里包含 -H(P)这一项也就是真实分布的熵。真实分布P在训练过程中是固定的锚点它不随模型更新而改变所以减去H(P)相当于减去一个常数对梯度的计算没有影响却多了一次运算。更重要的是从代码和框架的视角看交叉熵损失的表示更加简洁。PyTorch的nn.CrossEntropyLoss、TensorFlow的tf.keras.losses.CategoricalCrossentropy底层直接实现的就是H(P, Q)的计算逻辑极少有人直接去调KL散度做分类训练。只有当我们需要做分布蒸馏、变分推断这类任务需要显式监控两个分布之间的差异时KL散度才会单独上场。所以我的建议是在做最常规的分类、分割、检测、序列生成任务时你不需要纠结KL散度和交叉熵哪个“更正确”直接用交叉熵就好。但理解了这层关系你就能明白为什么模型训练过程其实是“用预测分布去逼近真实分布”的过程。2. 为什么分类任务几乎离不开交叉熵和MSE的对比2.1 分类任务的概率输出要求分类任务的输出层和回归任务有本质区别。回归任务预测连续数值比如房价、温度输出层可以不加激活函数直接用线性输出。分类任务则要求模型输出的是“样本属于每个类别的概率”所以最后一层几乎总是搭配Softmax多分类或Sigmoid二分类或多标签。Softmax做的事情是把一组实数映射成一组和为1的正数。它天然适合作为概率输出。而交叉熵损失衡量的是“预测概率分布”和“真实标签分布”之间的差异它和概率输出是天生的一对交叉熵的公式里关注的只是真实标签对应位置上的预测概率。这决定了它和Softmax/Sigmoid搭配时具备非常优雅的数学性质。2.2 从梯度推导看MSE在分类任务中的致命弱点假设我们做一个二分类任务模型输出经过Sigmoid得到预测概率p真实标签是y取0或1。如果用均方误差Mean Squared Error简称MSE作为损失函数L (p - y)²对sigmoid之前的输入z求梯度需要用链式法则∂L/∂z ∂L/∂p · ∂p/∂z。其中∂L/∂p 2(p - y)而sigmoid的导数∂p/∂z p(1-p)。当预测非常错误时比如真实标签y1但模型预测p0.1此时∂L/∂p 2(0.1-1) -1.8∂p/∂z 0.1 × 0.9 0.09最终梯度约为0.162。如果预测更差p0.01∂L/∂p约等于-1.98∂p/∂z 0.01 × 0.99 ≈ 0.0099梯度只剩约0.0196。看见问题了吗模型越是自信地预测错误sigmoid的导数就越接近于0梯度反而越小参数更新越慢。这就是所谓的梯度饱和问题。直观理解就是模型已经在错误的方向上“非常确信”了用MSE却告诉它“虽然你错得离谱但我给你的修正信号很弱”。这在训练初期会严重拖慢收敛速度。再看交叉熵。二分类交叉熵损失为 L -[y·log(p) (1-y)·log(1-p)]同样对z求导∂L/∂z p - y这是个极其简洁的结果。当真实标签y1而模型预测p0.01时梯度是0.01 - 1 -0.99模型会得到一个很强的“向正确方向更新”的信号。换句话说交叉熵的梯度大小与“预测和真实之间的差异”成正比预测越错梯度越大修正力度越强。这是MSE完全不具备的优势。2.3 交叉熵损失的收敛特性与优化友好度除了梯度饱和问题交叉熵损失函数的收敛特性也更适合分类任务。因为交叉熵等价于最大化正确类别的对数似然它的优化目标是明确的把正确类别的概率推向1把错误类别的概率压向0。MSE则是在拟合一个数值上的距离关系。它对输出概率的数值差异敏感但从概率分布角度看它没有充分利用“类别”这个离散信息的结构。实际训练中你会发现用MSE做分类任务不是不能收敛而是收敛慢、容易卡在局部最小值附近最终的准确率往往不如交叉熵。从损失函数的几何形态来看交叉熵在概率单纯形上对“错误分类”的惩罚是非线性的越错惩罚越重MSE则近似二次惩罚对严重错误的惩罚力度不够。这也是为什么你在任何一本深度学习教材里分类任务默认的损失函数几乎都是交叉熵。我自己刚工作那会儿曾试着在一个多标签分类项目里对比MSE和交叉熵同样训练100个epoch交叉熵收敛到90%以上的准确率时MSE还在85%附近反复横跳后来果断放弃了这种“学术实验”。3. 由浅入深从公式推导到代码实现3.1 二分类交叉熵公式展开与Sigmoid搭配二分类交叉熵的公式看起来有两个分支原因是真实标签y只有两种取值。当y1时损失为 -log(p)当y0时损失为 -log(1-p)。合在一起就是L -[y·log(p) (1-y)·log(1-p)]这里的p是经过Sigmoid后得到的正类预测概率。很多初学者会困惑为什么二分类也要用这种“两段式”写法直接取正确类别的负对数不行吗其实这个公式的本质就是取“正确类别对应预测概率的负对数”。y1时正确类别是正类y0时正确类别是负类只是用y做了个选择。在PyTorch中二分类交叉熵有几个不同层级的接口。如果你已经得到Sigmoid后的概率p可以用F.binary_cross_entropy如果模型最后没有加Sigmoid直接用原始logits更推荐用F.binary_cross_entropy_with_logits它在内部融合了Sigmoid和损失计算数值稳定性更好。import torch import torch.nn.functional as F # 方式一先过Sigmoid再算BCE pred_sigmoid torch.sigmoid(logits) loss1 F.binary_cross_entropy(pred_sigmoid, target) # 方式二直接用logits算推荐 loss2 F.binary_cross_entropy_with_logits(logits, target)3.2 多分类交叉熵Softmax与One-hot标签多分类任务的交叉熵公式写成L -∑(y_i · log(q_i))其中 y_i 是真实标签的第i个分量通常one-hot编码q_i 是模型预测的第i个类别的概率。由于one-hot向量只有一个位置是1其余都是0这个求和实际上只留下了一个有效项正确类别对应位置的 -log(q_c)。PyTorch的nn.CrossEntropyLoss和F.cross_entropy有一点极其容易踩坑它们并不是“先Softmax再算对数”这么简单而是在内部先把输入logits过LogSoftmax再结合负对数似然计算。这意味着你输入的是模型的原始输出logits而不是经过Softmax之后的值。import torch import torch.nn as nn loss_fn nn.CrossEntropyLoss() # logits: (batch_size, num_classes)target: (batch_size,) 存类别索引 loss loss_fn(logits, target)这里target不需要转成one-hot编码直接传类别索引就行。如果你手贱先对logits做了Softmax再传给CrossEntropyLoss会发现损失值完全不对而且数值还会出现NaN的风险。这个坑我见过无数新人踩过。3.3 数值稳定性为什么框架都推荐用logits直接算Softmax的计算过程涉及指数运算。假设某个logits向量是 [1000, 990, 980]直接计算exp(1000)在float32精度下会直接溢出成inf后续的除法就会出现NaN。而正确的Softmax结果应该是 [1, 0, 0]最大值对应的类别概率应该接近1其余接近0。框架们普遍采用的解决方案是把每个logits都减去该样本中logits的最大值再做指数运算。因为Softmax的分子分母都除以一个常数后结果不变减去最大值后指数里的最大值变成0其余变成负数即便再大的数值也能安全计算。这个技巧通常被称为LogSumExp稳定化。nn.CrossEntropyLoss内部把LogSoftmax和负对数似然合在一起计算本质上就是在做这类稳定化处理。它不是在“优化速度”而是在“防止数值爆炸”。所以实践中千万不要自己手动实现一个Softmax再套个log再算loss除非你有把握把数值稳定性处理干净。我见过某些线上推理代码为了“灵活调温度系数”自己手写了Softmax再算交叉熵结果batch稍大、logits动态范围稍高loss就直接飞了。3.4 从零实现一个数值稳定的交叉熵损失为了让你彻底搞明白框架背后的逻辑我写个简短的PyTorch代码演示一个数值稳定的交叉熵实现。这个例子不是让你在实际项目里替换框架接口而是帮助你理解内部原理。import torch import torch.nn.functional as F def stable_cross_entropy(logits, target): # logits: (batch_size, num_classes) # target: (batch_size,) 类别索引 # 1. 减去每行最大值防止exp溢出 logits_max torch.max(logits, dim-1, keepdimTrue).values logits_stable logits - logits_max # 2. 计算LogSoftmax exp_logits torch.exp(logits_stable) sum_exp torch.sum(exp_logits, dim-1, keepdimTrue) log_probs logits_stable - torch.log(sum_exp) # 3. 取出正确类别的log概率求负均值 batch_size logits.size(0) loss -log_probs[torch.arange(batch_size), target].mean() return loss这个实现和PyTorch内置的CrossEntropyLoss在数学上等价忽略一些边界细节。你可以在小规模tensor上验证random logits下自己实现的loss和nn.CrossEntropyLoss()的结果几乎一致。亲手写一遍之后你对“为什么logits能直接算”“为什么框架要减去最大值”这些问题的理解会彻底打通。4. 热词背后的真实应用YOLO、LLM预训练与loss曲线绘制4.1 YOLO系列目标检测中的交叉熵面貌YOLO系列的目标检测损失函数是一个多任务加权组合主要包含三个分支边界框回归损失、目标置信度损失、类别分类损失。其中类别分类损失和部分置信度损失使用的正是交叉熵。以前几代YOLO为例置信度损失用二分类交叉熵衡量“框中是否有目标”的预测准确度类别损失用多分类交叉熵判断每个框内目标的具体类别。到了YOLOv8作者把分类分支换成了BCEWithLogitsLoss本质就是二分类交叉熵回归分支则换成了CIoU或DFL等损失。很多人在读YOLOv8源码时会发现一个有意思的细节虽然多标签分类的概率输出在推理时也会走Sigmoid但在训练时它用的是二分类交叉熵的逐类别形式而不是nn.CrossEntropyLoss那种多分类Softmax形式。原因在于目标检测任务中一个目标框可能同时对应多个标签比如“人”和“成年人”用Softmax会强制所有类别概率之和为1这反而限制了模型的表达能力。改用逐类别的Sigmoid 二分类交叉熵每个类别独立判断互不干扰更贴合多标签检测场景。如果你自己魔改YOLO训练自己的数据集建议保留它的原始loss组合方式不要轻易把分类分支改成多分类Softmax交叉熵。我一开始手痒改过一版mAP直接掉了两三个点后来查资料才知道多标签和单标签在loss设计上是有本质区别的。4.2 LLM预训练和微调中的交叉熵用法大语言模型的预训练和微调核心任务就是“给定前文预测下一个token”。这个任务本质上是一个词表级别的超大规模分类问题。假设词表大小是50万那么模型最后一层输出的logits维度就是50万然后用交叉熵去计算“真实下一个token”对应的损失。在实际训练中LLM的交叉熵损失通常从左到右逐token计算再取平均。以GPT系列为例每个位置的输入都对应一个预测概率分布标签是下一位置的token。损失函数会把这个序列级别的交叉熵平均成一个标量用来反传梯度。这也是为什么LLM训练时经常看到loss从5.x一路下降到1.x甚至0.x因为这个loss的本质是负对数似然可以直观理解为“平均对每个token的预测置信度”。LLM训练里还有几个和交叉熵强相关的实践细节。第一是损失会除以序列长度再除以batch大小因为不同长度的序列总token数不同直接求和会导致长序列主导梯度。第二是微调时经常在交叉熵之外加上KL散度项比如RLHF里的PPO阶段用参考模型的输出分布约束正在训练的模型防止跑偏。第三是预训练中word级别的交叉熵通常不会刻意做类别平衡因为自然语言里token频率天然服从长尾分布刻意平衡反而会破坏语言的统计规律。4.3 如何绘制YOLOv8损失函数曲线图现在很多人在跑YOLOv8训练时会特别关注loss曲线是不是平滑下降。YOLOv8训练过程中会在终端打印train/box_loss、train/cls_loss、train/dfl_loss以及对应的val损失。默认情况下Ultralytics会在训练结束后自动生成results.png里面包含loss曲线、精度曲线和召回率曲线。但如果你想自己监控实时曲线或者想画出更定制化的图有几种方式。一种方式是在训练回调里把每个epoch的loss值记录下来。Ultralytics提供了on_train_epoch_end回调你可以继承Trainer类在里面把self.loss存成历史列表。另一种方式更简单训练日志会输出到runs/detect/train/目录里面有一个results.csv文件每一行是每个epoch的loss值和指标值。直接读这个CSV用Matplotlib画图就行。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) epochs df[epoch] plt.plot(epochs, df[train/cls_loss], labeltrain_cls_loss) plt.plot(epochs, df[val/cls_loss], labelval_cls_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.grid(True) plt.show()这里有几个经验谈。第一loss曲线有轻微震荡是正常的不必每次看到抖动就慌但如果val loss持续上升而train loss还在下降那是过拟合的信号需要提前停止或加正则化。第二交叉熵损失在训练初期下降很快是正常的因为模型从“均匀瞎猜”到“有点谱”的过程信息增益最大反而是后期loss下降变慢时需要调整学习率或优化器。第三不同分支的loss量级不同画图时建议分开画不要放在同一个坐标轴里否则dfl_loss可能会把cls_loss那条线压成一条直线看起来像完全没有在下降。5. 实战中的避坑经验与可复用的调参技巧5.1 标签平滑从“过度自信”到“放松约束”交叉熵损失在训练后期会把正确类别的预测概率推向无限接近1。这在某些场景下会让模型变得过度自信产生过拟合和泛化能力下降的问题。标签平滑Label Smoothing的解决办法很简单不再用硬标签1表示真实类别而是用一个略小于1的值比如0.9把剩下的0.1均匀分配给其他类别。从信息论角度解释标签平滑相当于在真实分布P上混入了一个均匀分布让目标分布不再是“完全确定”的零熵分布而是带有一定不确定性。这样做的好处是模型不会为了把某个样本的正确类别概率推到极限而不惜代价减少过拟合。PyTorch的CrossEntropyLoss直接支持label_smoothing参数loss_fn nn.CrossEntropyLoss(label_smoothing0.1)我自己的经验是在图像分类和文本分类任务里label_smoothing取0.1是一个很通用的默认值。但要在剪枝、蒸馏场景下标签平滑的效果需要单独评估因为它会影响蒸馏时教师模型输出的soft label分布。5.2 类别不均衡与Focal Loss登场现实业务里类别不均衡是常态。比如工业质检中“正常”样本可能有几万个“缺陷”样本只有几百个。直接用交叉熵训练模型会倾向于把所有样本都预测成多数类因为这样loss最低。此时硬怼交叉熵的收敛结果往往非常差。常见对策有几种。最简单的是给不同类别设置不同的权重PyTorch的CrossEntropyLoss支持传入weight参数。这是一种“代价敏感”的处理方法让少数类的错误被放大从而诱导模型更重视它们。更进阶的做法是Focal Loss它在交叉熵的基础上引入一个调制因子(1 - p)^γ其中γ是调节参数一般取2。这个因子的作用是降低“已经分类正确”样本的loss权重让模型把注意力集中在难分类的样本上。Focal Loss在目标检测尤其是RetinaNet里是标配在长尾分类任务里实践效果也很明显。它不是替代交叉熵而是交叉熵在类别不均衡场景下的一种变体理解这一点比死记公式更重要。5.3 若干容易被忽视的工程细节交叉熵损失所处的位置决定了它只是整个训练闭环中的一个环节但很多问题最后都会在loss上暴露出来。第一个细节是损失函数的dtype。混合精度训练时logits可能是float16target通常是int64。如果手写loss函数时做索引操作要确保索引张量的类型是int64否则某些GPU算子上会直接报错。第二个细节是“除以0”的问题。虽然框架做了数值稳定但在手动实现交叉熵时如果某个类别在batch中完全没有出现对应的log概率可能因为取了对数变成很大的负值但不会变成无穷大所以一般的常规操作还没事。但如果是自定义的Focal Loss变体在p接近0或1时要小心(1-p)^γ这个项的浮点精度问题极端情况下会造成梯度爆炸。这时候加一个epsilon做下剪裁是稳妥的。第三个细节是梯度累积和loss缩放之间的关系。在分布式训练或大batch训练时如果采用梯度累积策略loss需要除以累积步数如果是混合精度训练loss缩放因子会动态调整。这些和交叉熵本身无关但会在曲线监控时表现得像“loss突然跳了一下”。看到loss出现异常跳变时先检查学习率调度、数据顺序是否shuffle、以及不同卡上的batch分布不要第一反应就是loss函数写错了。我自己在调试时有个习惯训练刚开始的前几个step打印一下loss的初始值。如果初始loss和理论值差很远说明大概率数据标签对不齐或者模型输出层维度配错了。多分类任务初始loss约为log(num_classes)二分类任务初始loss约为log(2)这个数值检查在排查问题时非常高效。交叉熵损失函数看着简单但它和模型结构、任务类型、数据分布、优化策略之间有着千丝万缕的关系。很多人在训练卡住时习惯性地换优化器、调学习率我却建议先回头检查loss设计和实现的细节。把交叉熵背后的概率和信息论逻辑吃透你会发现自己定位问题的速度快了不止一倍。