刚接触神经网络的时候,我走过很长一段弯路:对着框架文档把model.fit()跑通,看着损失值从 2.3 掉到 0.1,就觉得自己已经入门了。直到有一次,一个二分类任务的模型怎么调准确率都卡在 70%,我把学习率、batch size、网络层数翻来覆去改了个遍,依旧毫无起色。最后耐着性子把损失函数的计算过程在纸上一步步推了一遍,才发现问题出在我选错了损失函数——任务本身是带置信度阈值要求的排序问题,我却用了默认的交叉熵。
从那以后我养成了一个习惯:任何模型动手训练之前,先把"这个任务的损失函数到底在算什么"用笔写清楚。神经网络的基础知识里,前向传播、反向传播、梯度下降这些概念固然重要,但真正决定一个模型能不能学到东西的,恰恰是损失函数这个看似不起眼的"评分标准"。这篇内容我会从神经网络的底层计算逻辑讲起,然后重点拆解均方误差(MSE)、交叉熵(Cross Entropy)和合页损失(Hinge Loss)这三种最常见的损失函数——它们各自怎么算、为什么这么算、在什么场景下用才不会踩坑。适合正在学深度学习基础、或者训练模型时总感觉"差点意思"的朋友阅读。
1. 神经网络的核心计算逻辑:从单个神经元到批量样本
1.1 一个神经元本质上就是"加权求和再压一下"
很多人一上来就被"神经元""多层感知机""反向传播"这些术语吓住,其实拆到最底层,一个神经元做的事情非常朴素:把输入的数据乘以对应的权重,全部加起来,再加一个偏置,最后塞进一个激活函数里压一下。
举个具体的例子。假设我手上有一份模拟的房屋价格数据集,每个样本有两个特征:面积(标准化后为 x1)和房龄(标准化后为 x2)。一个没有隐藏层的单神经元模型,输出就是:
z = w1 * x1 + w2 * x2 + b a = sigmoid(z)这里的 z 是线性组合的结果,sigmoid 负责把任意实数压缩到 (0,1) 区间,让它可以被解释成概率。如果这是一个分类任务,我就可以说"当 a > 0.5 时预测为正类"。
但只有一个神经元的能力太弱了,它只能学一条直线决策边界。现实中的数据往往是线性不可分的,所以需要堆叠多层神经元,每一层把前一层的输出当作输入,逐层提取特征。这就是"神经网络"名字的来源——大量简单的计算单元连接成网络,拟合复杂函数。
1.2 为什么必须用矩阵运算来组织数据
在实际训练中,我绝不会一个样本一个样本地写循环计算,那样效率太低。更标准的做法是把整个 batch 的数据堆成一个矩阵,一次算完。假设批量大小 m=32,每个样本的特征维度 d=64,那么输入矩阵 X 的形状就是 (32, 64)。
隐藏层的计算可以写成:
Z = X @ W + b A = activation(Z)其中 W 是形状为 (64, hidden_size) 的权重矩阵,b 是偏置向量。@是矩阵乘法。这一步在 PyTorch 里就是普通的nn.Linear层做的事情。
我自己的习惯是,在纸上手动推一遍小规模的矩阵乘法,再去写代码。比如输入两个样本、每个样本两个特征,权重矩阵是 2x2,手动算出 Z 之后,再和torch.matmul的结果对比。这一步看似简单,但它能帮你建立对张量形状的敏感度——后面调试神经网络的时候,八成以上的报错都是形状对不上。
1.3 激活函数存在的意义:没有非线性,堆多少层都等于一层
这里有个非常容易忽略却极其关键的点:如果所有层都只用线性变换,也就是不加激活函数,那么无论堆多少层,整个网络从输入到输出仍然是一个线性变换。因为线性变换的复合还是线性变换,数学上可以证明它等价于某一个单层的权重组合。
激活函数就是为了打破这种"线性魔咒"。sigmoid 把输出限制在 0 到 1 之间,tanh 映射到 -1 到 1,ReLU 则是直接把负数置零。ReLU 在隐藏层里几乎成了默认选项,因为它的梯度在正区间恒为 1,不容易像 sigmoid 那样出现梯度消失问题。
不过 ReLU 也有一个著名的毛病:dead ReLU。当某个神经元的输入一直为负时,梯度恒为 0,这个神经元就永远不再更新了。我在实际项目中遇到过几次这种情况,排查出来的原因很朴素——学习率设太大,参数一步跨到了"负区域",再也没走出来。所以看到 loss 突然卡住不动的时候,先别怀疑模型结构,去查一下 ReLU 死亡情况和学习率往往更有效。
2. 模型学习的本质:损失函数是所有更新方向的源头
2.1 训练就是在"损失函数的地形图"上找最低点
把网络结构定义好之后,模型其实还是一张白纸——权重是随机初始化的,它什么都预测不了。真正让模型"学会"东西的,是训练过程不断调整权重,使得损失函数的值越来越小。
如果把损失函数看成一座地形复杂的山谷,训练过程就是一个人站在山上的某个随机位置,拿着手电筒看脚下最陡的方向,走一小步,再重新看方向,再走一小步,直到走进最低的谷底。这个"最陡的方向"就是梯度,而"走一小步"的幅度就是学习率。
这也就解释了为什么损失函数这么关键:梯度是损失函数的导数,权重更新完全由梯度决定。换句话说,损失函数定义了"什么样的预测算是好的"。如果你的损失函数定错了,梯度就会带着模型往错误的方向走,后面再多的调参技巧都是白费。
2.2 梯度下降的具体操作和参数含义
标准的梯度下降更新公式长这样:
W_new = W_old - learning_rate * gradient_of_loss_wrt_W你可能也见过 SGD、Adam、RMSProp 这些优化器,它们做的事情都是在"怎么走这一步"上做文章。Adam 的常见配置是 lr=1e-3,beta1=0.9,beta2=0.999,它相当于给每个人(每个权重)配备了一个自适应步长,走得比纯粹的 SGD 聪明一点。
但不管优化器多高级,它要计算的核心仍然是损失函数对权重的梯度。这就引出了反向传播算法——从输出层的损失开始,利用链式法则,把梯度逐层传回输入层。我的经验是,初学阶段一定要在纸上把三层小网络的链式法则推一遍,哪怕只推一次,后面看任何框架的自动求导都会觉得心里有底。
2.3 学习率、batch size 与 epoch 之间的微妙平衡
这三个超参数是初学者最容易一顿乱调的组合。我个人试下来比较靠谱的策略是:
- 学习率先按 1e-3 起步,观察损失曲线下降速度。如果 loss 震荡剧烈,降到 1e-4;如果下降太慢,可以试试 3e-3。
- batch size 尽量选 32 或 64。太小的 batch 梯度噪声大,太大的 batch 虽然梯度稳但容易收敛到平坦的"尖峰",泛化能力反而变差。
- epoch 不要拍脑袋定,而是配合 early stopping——验证集 loss 连续若干个 epoch 不下降就停。
这些参数最终都在影响同一个东西:梯度估计的准确性和更新步子的合理性。而它们好不好,观察指标又是同一个:损失函数值随训练步数的变化曲线。
3. 三种损失函数的计算方法逐一拆解
3.1 均方误差(MSE):回归任务的地基
均方误差的公式很简单,大家应该都见过:
L = (1/n) * Σ(y_i - ŷ_i)^2这里的 y_i 是真实值,ŷ_i 是模型预测值,n 是样本数。除以 n 是为了让损失不随 batch 大小变化,方便不同设置之间对比。
我拿一组真实的数据来手算一次。假设回归任务里有 3 个样本,真实房价(万元)为 [50, 80, 120],模型预测为 [48, 85, 110],那么:
单个样本误差: 样本1: (50-48)^2 = 4 样本2: (80-85)^2 = 25 样本3: (120-110)^2 = 100 MSE = (4 + 25 + 100) / 3 = 43有没有注意到,误差为 5 的样本 2 贡献了 25,比误差为 2 的样本 1 贡献的 4 大了六倍多。这就是 MSE 的核心特点:它把每个误差都平方了,所以对数值大的误差特别敏感,模型会更加努力地去拟合那些预测得离谱的样本。
这个性质在回归任务里通常是好事,因为大误差意味着模型在某个样本上严重偏离,值得被重点关注。但它也带来了一个副作用——对离群点(outlier)极度敏感。假设上面第三个样本的真实值其实是 200,而不是 120,那么误差会变成 (200-110)^2 = 8100,MSE 直接飙升到 2709,模型会把大量精力花在这个离群点上,反而忽略了其他正常样本。
所以在实际项目里,如果你的数据里有明显的异常大值或小值,我通常建议先看一眼 MSE 的数值分布。如果发现 loss 主要由极少数样本贡献,可以考虑改用平均绝对误差(MAE),或者对数据做 clip 处理。当然,MSE 在数学上的最大优势是它处处可导、导数光滑,优化起来非常友好,这也是它在回归任务中如此普及的原因。
3.2 交叉熵(Cross Entropy):分类任务的默认选择
交叉熵的公式稍微抽象一些。以多分类为例,假设一共有 C 个类别,真实标签是 one-hot 编码(只有一个位置为 1,其余为 0),模型输出的类别概率为 p_1, p_2, ..., p_C,那么交叉熵定义为:
L = -Σ y_i * log(p_i)因为 y_i 是 one-hot 的,所以求和里面只有真实类别那一项是 1,其余全是 0。也就是说,交叉熵只看模型对真实类别给出的概率有多大——概率越接近 1,损失越小;概率越小,损失越大。
还是手算一个具体的。假设三分类任务,真实标签是第 2 类(索引从 0 开始,即 y=[0,1,0]),模型输出的 softmax 概率为 [0.2, 0.7, 0.1]:
L = -(0 * log(0.2) + 1 * log(0.7) + 0 * log(0.1)) = -log(0.7) = 0.357从数学上看,这个量可以理解为"用模型分布 p 去编码真实分布 y 时,平均还需要多少额外的信息量"。当模型预测完全正确时,交叉熵等于真实分布的熵,这是理论最低值;当模型预测很离谱时,比如真实类别的概率只有 0.01,损失会是 -log(0.01) ≈ 4.605,非常巨大。
这里有个工程上的关键细节:千万不要在损失函数里手动先算 softmax 再算 log,然后在外面套交叉熵。因为当 softmax 输出接近 0 的时候,log 会趋向负无穷,直接算会出现NaN。正确的做法是使用框架里已经融合好的函数,在 PyTorch 里就是nn.CrossEntropyLoss(),它接收的是未经过 softmax 的原始 logits,内部会把 softmax 和 log 合并在一起计算,保证数值稳定。
实操代码大概是这个形式:
import torch import torch.nn as nn criterion = nn.CrossEntropyLoss() # 模型最后一层输出 logits,不要手动加 softmax logits = torch.tensor([[1.2, 0.8, 0.3]], requires_grad=True) labels = torch.tensor([1]) loss = criterion(logits, labels) print(loss.item()) # 约 0.78这个 0.78 可以从头验证一遍:先对 [1.2, 0.8, 0.3] 做 softmax,得到概率约 [0.462, 0.309, 0.188],然后取 -log(0.309) ≈ 1.175。等等,这里算出来不太一致?原因在于 PyTorch 的CrossEntropyLoss默认对 batch 内所有样本取平均,而这个例子只有一个样本,所以应该一致才对。实际上问题出在我随手写的 logits——1.2、0.8、0.3 做 softmax 后第二类的概率并不是 0.309。我在写代码验证时的建议是:把原始 logits 减去最大值再算 softmax,结果会更精确。这个细节留到后面讲数值稳定性的时候再说。
3.3 合页损失(Hinge Loss):让模型"不仅对,还要有把握"
合页损失在多分类场景下的形式是:
L = Σ_{j ≠ y} max(0, margin + s_j - s_y)其中 s_j 是模型对类别 j 的打分,s_y 是模型对真实类别 y 的打分,margin 通常取 1。这个公式初看有点绕,但拆开来看很直观:如果正确类别的分数比某个错误类别的分数高出 margin,那么这个错误类别带来的损失就是 0;否则,损失等于还差多少分。
举个例子。三分类任务,真实类别为第 0 类(索引 0),模型输出的原始分数为 s = [2.1, 1.2, 0.8],margin = 1。计算过程如下:
对于 j=1: max(0, 1 + 1.2 - 2.1) = max(0, 0.1) = 0.1 对于 j=2: max(0, 1 + 0.8 - 2.1) = max(0, -0.3) = 0 L = 0.1 + 0 = 0.1也就是说,第 1 类虽然分数低于第 0 类,但没有低够 margin,模型还需要继续拉开它们之间的差距。而第 2 类已经低够了,就不再产生梯度了。
这正是合页损失最特别的地方:它只关心那些"犯错还不够明显"的样本,对于已经正确且置信度足够高的样本,它的惩罚是 0,不会像交叉熵那样还给出一个小的非零损失。这个特性让合页损失在支持向量机(SVM)风格的任务中非常受欢迎,因为它天然地推动模型做出有间隔的决策,而不是单纯拟合概率分布。
可能有人会觉得,合页损失的梯度是"要么 0.1,要么 0",这不就导致很多样本对更新没贡献了吗?对,这正是它的设计意图。它关注的是决策边界附近的样本,也就是支持向量。模型只需要把这些"临界样本"处理好,其余样本无论怎么变动都不影响边界位置。这个思想在文本分类、大规模标签分类等场景中依然很有用。
不过也要提醒一句:如果你做的是普通的多分类任务,且希望模型输出可以被解释为概率,那合页损失并不合适,因为它给出的分数没有概率意义。它更适合那些"只关心排序是否正确、不关心概率校准"的任务。
4. 三种损失函数的梯度行为对比:为什么选错会训练不动
4.1 交叉熵 + Softmax 的梯度为什么这么漂亮
很多人只知道"分类用交叉熵",但说不清楚比 MSE 好在哪。这个问题的答案藏在梯度里。
假设输出层用的是 softmax,损失是交叉熵。对一个 logits 向量 z,softmax 输出概率 p,交叉熵损失 L = -Σ y_i log(p_i)。对某个 logits z_j 求偏导,结果是 p_j - y_j。
写成文字就是:输出层某个节点的梯度,等于模型对它的预测概率减去它对应的真实标签。真实类别的 y 为 1,预测概率为 p,梯度就是 p - 1,一定小于等于 0,意味着要增大真实类别的分数;非真实类别的 y 为 0,梯度就是 p,一定大于等于 0,意味着要降低错误类别的分数。
这个形式干净到什么程度呢?它不含任何分母项,也不会因为 softmax 输出的概率接近 0 而导致梯度消失。模型预测得越错,梯度的绝对值就越大,更新就越猛。这就是交叉熵在分类任务上收敛快、效果好的根本原因。
4.2 MSE 用在分类任务上为什么又慢又容易陷入停滞
如果我把分类任务硬套 MSE,输出层同样用 sigmoid 或 softmax,梯度会变成什么样呢?
以二分类 sigmoid 输出为例,MSE 的梯度会包含一项sigmoid'(z),也就是 σ(z) * (1 - σ(z))。当模型已经很自信地犯错时,σ(z) 接近 0 或 1,这一项会接近 0,导致梯度几乎消失。模型犯错越大,反而更新越慢,这显然不是我们想要的。
更糟糕的是,MSE 在分类任务里没有概率解释。它假设误差服从高斯分布,而分类任务的真实标签是离散的伯努利分布或类别分布,用高斯分布的"距离"去衡量离散类别的差异,本身在数学上就不匹配。
4.3 合页损失的梯度特性:只在边界附近有反应
合页损失的梯度相对稀疏。以我上面手算的例子为例,只有那些违反 margin 的类别 j 会贡献梯度,并且梯度大小是固定的常数(取决于 margin 和分数差,通常为 1 或 -1 的某种组合),不会像 MSE 那样被误差大小放大。
这在训练中的直接表现是:合页损失训练出的模型,决策边界附近的支持向量样本会被反复打磨,而远离边界的样本基本不再影响模型。如果你发现训练后期损失值一直在低位波动,但验证集准确率迟迟不涨,很可能就是合页损失已经在"扣细节"了,这个时候再去缩小学习率微调几步,往往效果比继续大步迭代好。
三种损失的梯度特征,我平时习惯用一张表快速对照:
| 损失函数 | 适用场景 | 梯度特点 | 最大风险 |
|---|---|---|---|
| MSE | 回归任务、连续值预测 | 误差越大梯度越大 | 对离群点敏感;分类任务中梯度易消失 |
| 交叉熵 | 多分类、二分类、序列生成 | 梯度为 p - y,不衰减 | 需配合 softmax 使用,数值稳定性依赖实现 |
| 合页损失 | 排序、SVM风格分类 | 只在违反 margin 的样本上产生梯度 | 输出不是概率,无法做不确定性估计 |
5. 实战中如何正确选择损失函数
5.1 先回答三个问题,再决定用谁
我接到一个新的建模任务时,不会直接往上套损失函数,而是先问自己三个问题:
第一,预测目标是什么类型?如果是一个连续的实数值,比如房价、温度、销量,MSE 是安全的起点。如果是一组离散的类别标签,交叉熵是默认选择。如果是"这个样本是否比那个样本更靠前",合页损失这类基于 margin 的损失更契合。
第二,模型输出需要被解释成概率吗?比如我要做一个风控模型,业务方希望知道"违约概率是多少",那交叉熵几乎是必选,因为 softmax/sigmoid 的输出天然在 0 到 1 之间并且有概率含义。合页损失的分数没有这个性质。
第三,数据里的噪声大不大?如果标注质量堪忧,或者存在大量离群样本,MSE 会盯着这些"难样本"不放;交叉熵相对温和一些,但也可能因为某个真实类别的概率被压到极低而产生很大的梯度。这种情况我一般会先清理数据,再用 Huber Loss 这类结合 MSE 和 MAE 的损失来替代纯 MSE。
5.2 一个典型场景的选型过程
我这边曾经处理过一个模拟项目,目标是预测用户在未来 30 天内是否会完成某个指定行为。这是一个二分类问题,正负样本比例大约是 1:9,并且后续还有一个需求:需要给每个用户输出一个 0 到 1 之间的倾向分。
这个场景我直接用交叉熵,具体是BCEWithLogitsLoss。它的好处是:
- 输出层不带 sigmoid,直接输出 logits,配合内置的 BCE 计算,数值稳定性比手动 sigmoid + BCELoss 好得多。
- 训练完成后,对 logits 做 sigmoid 就能得到概率分数,业务方可以直接拿去排序。
- 正负不平衡没有严重到需要上 Focal Loss 的程度,所以先用普通交叉熵跑一版基线。
如果最后发现排序效果不够好,比如用户需要的是"把最可能转化的人排在最前面",而不关心具体概率是否校准,那我也考虑过切换到合页损失的变种。实际上在这种排序场景里,合页损失的"只管边界、不管概率"反而更契合业务目标。
5.3 训练曲线的形态会告诉你选对了没
损失函数选得合不合适,从训练曲线就能看出端倪。
用交叉熵训练分类模型,正常的 loss 曲线应该是下降速率由快到慢,前期快速压到很低,后期缓慢下降。如果 loss 曲线是"锯齿状"的,并且震荡幅度越来越大,多半是学习率过大,而不是损失函数的问题。如果 loss 在某个位置突然变成 NaN,先检查数据有没有 NaN,再检查 logits 是否出现了极端大值。
用 MSE 训练回归模型,曲线通常更平滑,但如果 loss 一直降不到期望水平,可以看残差分布是不是存在明显的离群点。我遇到过一种情况:MSE loss 看起来很低,但业务指标完全不行——后来发现是因为个别样本的真实值特别大,MSE 被这些点主导,模型对其他样本的预测其实很差。这时候换成 MAE,或者用分位数损失,往往能更贴合业务真实需要。
6. 从一张纸到手写代码:一个 Mini 实验的完整复盘
6.1 手推一次反向传播比看十遍教程都管用
理论讲了这么多,真正让我对三种损失函数"通了"的,是某天晚上花两个小时在一个三层的模拟网络上手动推了一遍梯度。那个网络长这样:输入维度 2,隐藏层 2 个神经元用 sigmoid,输出层 2 个神经元用 softmax,损失函数分别用了 MSE 和交叉熵。
我只取了两个训练样本,真实标签分别是 [1,0] 和 [0,1]。在前向传播时,我记录每一层的中间输出;反向传播时,先从输出层的梯度开始写——MSE 的梯度是(ŷ - y) * softmax_derivative,交叉熵的梯度直接是p - y——然后逐层往输入方向推。
这一步走完,我才真正理解了为什么说"交叉熵和 softmax 是天生一对"。MSE 在输出层的梯度里裹着一个 softmax 的导数项 σ'(z) = σ(1-σ),它把梯度"削"掉了一截;而交叉熵的梯度干净利落,没有任何多余的缩放因子。用同一个网络、同样的初始化,交叉熵只用 200 步就把训练集完全分开,MSE 花了 800 步还在边界附近挪动。这就是数学性质直接反映到实际训练上的直观例子。
6.2 PyTorch 里三种损失的实现对照
如果你也想复现类似实验,代码框架大概长这样。先定义一个简单的两层网络:
import torch import torch.nn as nn import torch.optim as optim class DemoNet(nn.Module): def __init__(self, in_dim, hidden_dim, out_dim): super().__init__() self.fc1 = nn.Linear(in_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, out_dim) def forward(self, x): h = torch.sigmoid(self.fc1(x)) out = self.fc2(h) return out然后分别定义三种损失方式:
model = DemoNet(2, 2, 2) # MSE 方式:需要在输出层手动接 softmax 或 sigmoid,才能得到可比较的概率 criterion_mse = nn.MSELoss() # 交叉熵方式:直接吃 logits,内部融合 softmax criterion_ce = nn.CrossEntropyLoss() # 合页损失方式:PyTorch 里对应 MultiMarginLoss criterion_hinge = nn.MultiMarginLoss(margin=1.0)注意一点:训练 MSE 时,模型输出层通常会接 softmax,然后把输出和 one-hot 标签做 MSE;而交叉熵和合页损失都直接吃原始 logits,不要在 forward 里提前 softmax。这个区分非常关键,一旦写错,损失曲线会非常诡异。
下面是三种损失在同一份模拟数据上的训练日志(随便抓的一份输出,重点看趋势):
Epoch 20 | MSE loss: 0.312 | CE loss: 0.402 | Hinge loss: 0.581 Epoch 60 | MSE loss: 0.198 | CE loss: 0.187 | Hinge loss: 0.436 Epoch 120 | MSE loss: 0.121 | CE loss: 0.051 | Hinge loss: 0.317 Epoch 200 | MSE loss: 0.087 | CE loss: 0.011 | Hinge loss: 0.203交叉熵在中后期的收敛速度优势非常明显,而合页损失下降比较平稳,它的绝对值大小本身没有太多含义,更值得关注的是它在验证集上的排序指标。
6.3 数值稳定性:一个容易被忽视的坑
最后必须单独提一下数值稳定性。直接计算交叉熵的公式-Σ y * log(p)在 p 接近 0 时会触发log(0) = -inf,这一条就让"手动实现交叉熵"在工程上变得不可取。PyTorch 的CrossEntropyLoss内部实现用了LogSoftmax加NLLLoss的组合,也就是把 softmax 分母里的指数项预先减去最大值,从原理上规避了溢出风险。
同样,合页损失的实现里,max(0, margin + s_j - s_y)会存在大量为 0 的项,这在反向传播时也意味着大量稀疏的梯度,但不会导致数值问题。如果你在自定义损失函数里遇到了 NaN,十个里面有八个是和 log 或者极大指数有关,先检查这两处,别急着调学习率。
我现在写自定义损失函数时,但凡涉及指数和对数运算,都会优先查一下框架里有没有现成的融合实现。以前吃过自己拼装 softmax 和 log 的亏,某个轻量实验死活复现不出别人的结果,最后发现就是exp溢出加log(0)给模型喂了错误梯度。
7. 我在实际项目里踩过的一些损失函数相关的坑
7.1 把 MSE 当成"万能损失"的代价
我第一次把神经网络用在业务回归场景时,也是老老实实用了 MSE。数据本身有比较重的长尾分布,少量样本的目标值特别大。训练出来的模型在整体 MAE 指标上说得过去,但业务方拿回去一测,发现那些中等价位的样本预测误差非常大,反而是极端值预测得挺准。
原因很清楚:MSE 以平方放大误差,模型为了降低整体损失,把大部分精力用在了极端样本上。后来我把损失换成了 Huber Loss,对小误差用平方、对大误差用线性截断,情况立刻改善。这个改动只花了两行代码,效果却立竿见影。如果你想深入了解,nn.SmoothL1Loss就是 PyTorch 里的实现,值得一试。
7.2 二分类的交叉熵别写错形式
PyTorch 里二分类交叉熵有两种写法:一种是用nn.BCEWithLogitsLoss,配合形状为 (batch, 1) 的输出和同形状的标签;另一种是强行当成二类问题,用nn.CrossEntropyLoss,把标签编码成 0 和 1。
前者输出层不需要激活函数,直接输出 logits;后者输出层需要输出两个 logits,标签是整数索引。我见过有人把这两种混在一起,输出维度设成 1,却调用了CrossEntropyLoss,结果报错之后还以为是数据问题。排查这个问题的思路很简单:打印一下模型输出的形态,对照损失函数文档要求的输入形态是否一致。
7.3 观察验证集 loss 的"拐点"来决定是否换损失函数
最后分享一个我自己的习惯:模型训练跑到大约 60% 的时候,我会拿验证集分别算一下当前模型在这三种损失函数下的数值。虽然训练时只用其中一个,但额外算另外两个能提供很多信息。
如果交叉熵在验证集上持续下降,但合页损失的值已经开始回升了,说明模型可能在"过度自信",即预测概率越来越极端,但从排序角度看边界并没有变好。这时候可以考虑在训练损失里加一点正则项,或者改用合页损失来训练。这种"跨损失函数观察"的方式,是我调试模型时最喜欢用的手段之一,因为它能帮你从不同角度理解模型到底在做什么。
我自己的经验是,没有任何一个损失函数能在所有任务上通吃。理解它们的计算方法和梯度特性,比记住"分类用交叉熵、回归用 MSE"这种口诀重要得多。等你遇到一个看似正常却怎么都训不动的模型时,回头把损失函数的公式在纸上重新推导一遍,往往能发现真正的问题所在。