损失函数这东西,每个做深度学习的都绕不开。无论是训练一个图像分类模型,还是调一个目标检测的工程,你最先盯着的就是那张 loss 曲线。它升了你会慌,它降不下来你会焦虑,它变成 NaN 你就知道今晚又得加班了。但很多人对损失函数的态度是“能用就行”:框架里默认的交叉熵用到底,模型训不动就调学习率、换优化器,很少有人认真想过——我选的这个损失函数,真的在衡量我真正关心的东西吗?
这篇文章就把损失函数从原理到实践彻底讲透。我会先用一个特别常见的类比把它讲明白,然后逐类拆解回归、分类、对比学习等场景下常用损失函数的数学原理、梯度行为和适用边界,最后给出我在实际项目中踩过坑之后总结出来的选型方法、组合策略和排查技巧。内容不适合零基础的小白,但只要你已经跑通过一个简单的训练脚本,这篇文章能帮你把损失函数这层窗户纸彻底捅破。
1. 损失函数的本质与设计思路
1.1 为什么模型训练离不开损失函数
先抛一个问题:假设你手里有一堆猫狗照片,要训练一个模型区分它们。模型的输出是一个概率分布,比如 [0.8, 0.2],意思是“80% 是猫,20% 是狗”。真实标签是“猫”,也就是 [1, 0]。模型答对了,但还不够自信,怎么量化这个“不够自信”?靠的就是损失函数。
损失函数做的事情本质上只有一件:计算模型预测和真实标签之间的距离。这个“距离”在数学上有多种定义方式,不同的定义方式对应不同的损失函数,也就对应着不同的优化目标。
我习惯把损失函数比作“考试评分标准”。你告诉模型“按这个标准答题”,它就会不惜一切代价去迎合这个标准。如果评分标准里计算题分值特别高,学生就会花大量时间练计算;如果作文分值特别高,学生就会猛练写作。深度学习模型的参数更新完全由损失函数的梯度驱动,所以损失函数决定了模型会“拼命学好什么”。这是一种根本性的设计决策,而不是一个简单的“选 A 还是选 B”的问题。
很多人有个思维误区:觉得损失函数只是一个评估工具,训练结束后看一眼“效果好不好”。事实恰恰相反,在训练过程中,损失函数是模型唯一的信息来源。反向传播算法通过链式法则计算损失对每个参数的偏导数,然后沿着梯度方向更新参数。没有损失函数,就没有梯度;没有梯度,模型没有任何方式知道自己该往哪个方向调整。从这个意义上说,选损失函数就等于在定义“什么样的模型是一个好模型”。
1.2 损失函数与梯度下降的关系
要真正理解损失函数,必须把它放进梯度下降的框架里看。
假设我们有一个参数为 θ 的模型,输入样本 x,输出预测值 ŷ,真实值是 y。损失函数记为 L(θ),它衡量的是模型在整个训练集上的平均预测误差:
θ* = argmin_θ L(θ)
梯度下降的做法是,每一步都沿着梯度 ∇L(θ) 的反方向更新参数:
θ_{t+1} = θ_t - η · ∇L(θ_t)
这个式子看起来简单,但信息量很大:学习率 η 控制步长,梯度 ∇L 控制方向。而不同的损失函数,会对这个方向产生完全不同的影响。
举一个具体的对比:平方损失(MSE)和绝对值损失(MAE)。平方损失对“大误差”的惩罚是二次增长,梯度随误差线性增长;绝对值损失对“大误差”的惩罚是线性增长,梯度是常数。这意味着在训练初期,如果预测值和真实值偏差很大,MSE 会产生很大的梯度,模型会非常激进地修正参数;而 MAE 的梯度始终是 ±1,无论误差多大,修正力度都一样。
我在一次回归任务里对比过这两种损失。数据里有少量离群点,用 MSE 训练时那些离群点把模型拽得东倒西歪,损失曲线一直在高位震荡。换成 MAE 之后,模型稳定了很多,但收敛速度明显变慢,原因是 MAE 在误差很小时梯度仍然是 ±1,模型很难精确收敛到最优值。后来换成 Huber Loss 兼顾两者,才同时解决了鲁棒性和收敛速度的问题。这个案例充分说明,损失函数的数学特性会直接影响训练动态。
1.3 一个容易被忽视的观念:损失函数不等于评估指标
还有一个特别常见的坑:把损失函数和业务评估指标混为一谈。
业务指标是什么?是“精确率”“召回率”“F1 分数”“IoU”这些。它们通常很直观,能反映业务效果,但一般都是不可微的。你说“模型错分了 3 张猫,5 张狗”,这句话没法对模型参数求导。所以我们需要损失函数这个“替身”,用它作为可微的代理目标,间接优化我们真正关心的评估指标。
这个替身和原身之间的差距,就是理论和实践之间最微妙的缝隙。实践中经常出现损失函数不断下降、但验证集指标却没怎么涨的情况。这时候不要急着调学习率,先想想:我这个损失函数是评估指标的合格替身吗?
举个例子。做目标检测的时候,评估指标是 IoU,也就是预测框和真实框的重叠程度。但早期检测模型直接用 L1 或 L2 损失回归框坐标,因为 IoU 本身不可微。L1 损失把四个坐标点(x, y, w, h)当作独立变量来优化,但 IoU 关注的是框的整体重叠关系。这就是为什么后来出现了 IoU Loss、GIoU Loss、DIoU Loss、CIoU Loss 这一系列把“框的对齐程度”直接建模为可微函数的损失函数。它们和评估指标更对齐,所以实践中效果普遍更好。
悟到这一层,你就明白为什么有的人调参调得飞起,因为他改的不只是损失函数本身,而是“评分标准”。
2. 常见损失函数的工作原理与选择逻辑
2.1 回归问题:MSE、MAE 与 Huber Loss
回归任务的损失函数选择,核心是在“对异常值的鲁棒性”和“收敛精度”之间做权衡。
先看最简单的均方误差(MSE):
L_MSE = (1/N) Σ (y_i - ŷ_i)²
它的导数是 2(y - ŷ),误差越大,梯度越大。好处是收敛快,尤其是训练后期,误差较小时梯度也很小,可以实现精确逼近。代价是平方项会让离群点产生极大的损失值,模型被迫优先“讨好”这些异常样本,导致整体预测偏差。
再看平均绝对误差(MAE):
L_MAE = (1/N) Σ |y_i - ŷ_i|
它的梯度是 ±1,对离群点完全不敏感,非常稳健。问题在于零点处不可导,而且无论误差多小,梯度都恒定不变,模型难以精细收敛,训练后期容易在最优值附近来回震荡。
Huber Loss 是两者的折中方案:
L_Huber =
- 0.5(y-ŷ)², 当 |y-ŷ| ≤ δ
- δ|y-ŷ| - 0.5δ², 当 |y-ŷ| > δ
当误差小于阈值 δ 时,它等价于 MSE,保证精确收敛;当误差大于 δ 时,它退化为线性惩罚,限制离群点的影响。PyTorch 里的nn.SmoothL1Loss就是 δ=1 的 Huber Loss,我用它替代 MSE 做边界框回归后,训练稳定性提升非常明显。
从实践经验来看,如果数据干净无异常,MSE 是一个安全的起点;如果数据里有明显的离群点,直接上 Huber 或者分段考虑。还有一个进阶选择是分位数损失(Quantile Loss),它的好处是能控制预测的“倾向性”,比如设置分位数为 0.9,模型会更倾向于预测较高值,适合库存预测这类“宁可多备不可缺货”的业务。
2.2 分类问题:交叉熵为什么是默认选项
分类任务里,交叉熵基本是统治级的默认选项。它的表达式很多人背过:
L_CE = -Σ y_i · log(p_i)
其中 p_i 是模型对第 i 个类别的预测概率,y_i 是 one-hot 标签。之所以交叉熵效果远好于“用 MSE 配合 softmax 做分类”,关键在于梯度行为。
如果你用 MSE 配合 softmax 输出做分类,会发现训练特别慢。数学原因很简单:softmax 把输出值压到 [0,1] 后,MSE 的梯度会乘上一个 softmax 导数的因子 p(1-p)。当 p 接近 0 或 1(模型已经比较自信)时,梯度几乎消失。预测越错,梯度越小,模型越学不动。而交叉熵的梯度是 (p - y),预测错得很离谱时梯度也很大,错得越离谱,修正力度越大。这种“错得多改得多”的特性,天然适合分类问题。
但在实际应用中,直接调nn.CrossEntropyLoss只是起步,有几个关键细节会显著影响效果:
第一,是否合并 softmax。PyTorch 的CrossEntropyLoss默认把 softmax 和交叉熵合并在一起,数值上更稳定,如果手动先 softmax 再取 log 再用nn.NLLLoss,容易出现数值下溢。我见过有新手直接对 softmax 输出做torch.log,预测值接近 0 时 log 直接变成负无穷,loss 直接 NaN。框架帮你合并,不只是为了写起来方便,更是为了数值稳定性。
第二,类别权重。当数据类别不平衡时,可以给CrossEntropyLoss传一个weight向量,让少数类的损失权重更大。这个操作简单,但很多人忽略了权重向量的归一化逻辑,导致学习率需要重新调整。我的一般做法是:权重与类别样本数的倒数成正比,同时整体缩放使权重均值为 1,这样能保持损失尺度和加权前大致相当。
第三,标签平滑。真实标签是 one-hot,但模型如果被训练到对某个类别极其自信,容易过拟合,还会导致特征空间非常尖锐。标签平滑的做法是把硬标签替换成软标签,比如把目标类别概率从 1.0 降到 0.9,其余类别从 0 升到 0.1/(C-1)。这个东西我一开始觉得“不就是加了点噪声嘛”,直到有一次分类任务里验证集准确率一直上不去,加上标签平滑之后曲线立刻变得健康了。它本质上是给模型留了点“不确定性余地”。
2.3 特殊场景:对比损失与温度系数
如果做度量学习、人脸验证、自监督表征学习,或者 Siamese 风格的模型,你一定会接触到对比损失(Contrastive Loss)。它的核心思想不是“让预测贴近标签”,而是“让同类样本在特征空间更近、异类样本更远”。
最经典的公式是 InfoNCE:
L_InfoNCE = -log [ exp(sim(z_i, z_j)/τ) / Σ_k exp(sim(z_i, z_k)/τ) ]
这里 sim 通常是余弦相似度,z_i 是锚样本的特征,z_j 是正样本,z_k 包括所有负样本,τ 是温度系数。
这里面有一个特别有意思的参数:温度系数 τ。它的作用是对相似度分布做缩放。τ 越小,logits 之间的差异被放大,模型对负样本的“排斥力”更强,训练更激进;τ 越大,分布越平滑,训练更温和但可能学不到足够的区分度。
我记得第一次自己复现对比学习时,τ 设成了 0.5,训练了 50 个 epoch 表征没什么区分度,之后把 τ 降到 0.07,效果立刻不同了。这是因为 0.5 的温度把负样本的贡献摊得太均匀,模型失去了对“难负样本”的关注度。这种玄学参数在网上讨论很多,但真正动手试过的人才明白它的敏感程度。
对比损失的核心逻辑和交叉熵有一个本质区别:它的“标签”是样本对之间的关系,而不是样本本身的类别。这让它非常适合没有标注数据的场景。但也要注意,负样本的选择策略对效果的影响可能比损失函数本身更大。一个常见的错误是对比损失中“假阴性”问题:数据里存在很多语义相似但没有被标为正对的样本,如果强行把它们当负样本,模型会被教坏。实践中往往需要大 batch size 或者动量队列来提供足够多样的负样本。
2.4 损失函数横向对比速查
| 损失函数 | 适用场景 | 梯度特点 | 关键注意点 |
|---|---|---|---|
| MSE | 回归、特征重建 | 随误差线性增长 | 对离群点敏感 |
| MAE | 含离群点的回归 | 恒定 ±1 | 收敛精度差 |
| Huber | 大多数回归任务 | 小误差线形,大误差常数 | δ 需要调参 |
| 交叉熵 | 单标签分类 | 错得越多改得越多 | 需数值稳定实现 |
| Focal Loss | 极端类别不平衡 | 难易样本动态加权 | γ 敏感 |
| Dice Loss | 图像分割 | 区域重叠相关 | 训练早期不稳定 |
| InfoNCE | 对比学习/表征 | 依赖于温度系数 | 负样本质量决定上限 |
这张表是我自己平时对照着用的。选型的时候先确定“任务属于哪一类”,再看“数据分布有什么特点”,两张表一交叉,基本能锁定候选范围。
3. 从原理到实践的损失函数选型
3.1 三步完成损失函数选型
第一次独立负责模型训练的同学,面对十几个损失函数经常手足无措。我建议按照下面三步走,比到处搜“哪个损失函数最好”要靠谱得多。
第一步,确认任务的输出形式和优化目标。回归输出是连续值,分类输出是概率分布,检测输出是框坐标加类别,分割输出是逐像素概率。输出形式决定了损失函数的数学结构必须匹配:你不可能拿交叉熵去算连续值的差异,也不应该拿 MSE 去约束概率分布。
第二步,检查数据分布中的“稀有情况”。离群点多吗?类别均衡吗?难样本多吗?矩阵样本多吗?这些决定了是否需要换掉默认损失函数。数据不均衡的分 类任务,默认交叉熵会偏向多数类,这时候就要考虑加权交叉熵或者 Focal Loss。
第三步,和业务方对齐“什么样的错不可容忍”。这一步最容易被忽略。有些业务场景宁可漏报也不可误报,比如疾病筛查;有些场景相反,宁可误报也不可漏报,比如垃圾邮件拦截。不同的容忍度可以通过调整损失函数里不同样本类型的权重来实现。这个是通过纯技术手段解决不了的,必须由业务输入来修订设计。
这三步走完,你大概率不会选错损失函数。剩下的事情就交给实验去验证。
3.2 数据不平衡时如何改造损失函数
类别不平衡是分类任务中最常见的问题之一。最朴素的做法是给少数类加权重,但权重加多少、怎么加,是有讲究的。
常用的加权公式是:w_c = N / (C · N_c),其中 N 是总样本数,C 是类别数,N_c 是第 c 类的样本数。这个公式让各类别的总权重贡献大致均衡,比较适合多数类和少数类数量差距在 10 倍以内的情况。如果差距超过 100 倍,就需要更激进的方案。
Focal Loss 是我在目标检测和极端不平衡分割任务里用过的效果最明显的方法之一。它的表达式是:
L_FL = -α(1-p)^γ · log(p)
和交叉熵相比,它多了两个因子:(1-p)^γ 是调制因子,当样本已经被正确分类且 p 接近 1 时,调制因子趋近于 0,这种“简单样本”的损失被大幅压低,模型把注意力集中到难样本上;α 是类别权重,用来解决正负样本本身的失衡。
但 Focal Loss 有它自己的脾气。γ 一般从 0.5 试到 2,我见过有人直接设 γ=2,结果简单样本权重被压得太低,模型在易分类的类别上反而出现退化。而且 Focal Loss 对超参数极其敏感,运行前最好做个小规模网格搜索。Focal Loss 出来之后有不少变体,比如梯度均衡机制,思路是动态调整不同梯度的样本权重,但工程复杂度也上去了,不是所有场景都值得。
对于图像分割任务,还有一个特别常见的选择是 Dice Loss:
L_Dice = 1 - (2|P ∩ G|) / (|P| + |G|)
它直接优化区域重叠程度,在类别极度不平衡的分割任务里表现得比交叉熵稳定。但它在训练初期容易产生梯度噪声,我实践下来最好的做法是把 Dice Loss 和交叉熵按一定比例组合,比如 0.5 的 Dice 加 0.5 的交叉熵,而不是单独用某一种。
3.3 损失函数组合与多任务平衡
现实中很少有一个任务只需要单一损失函数。图像分割模型往往输出多个头,目标检测模型要同时回归框坐标和分类类别,多任务学习更是直接把好几路损失加在一起。
组合损失函数的核心问题是:权重怎么定?最粗暴的做法是直接把几个损失相加,但每个损失函数的数值尺度不同。分类交叉熵通常是个位数级别的损失,而检测框回归的 SmoothL1 可能只有 0.1 的量级,把它们直接相加,分类损失会主导梯度,回归分支训练不充分;反过来,如果分割掩码的 Loss 是 0.001 级别,而分类 Loss 是 5.0 级别,分割分支形同虚设。
工程上常用的对策有三个:
一是固定权重加归一化。把每个损失除以其初始值,让它们从同一量级开始训练。这个方法简单,但在训练过程中各部分损失下降速度不同,会导致后期权重实际在漂移。
二是不确定性加权。这个思路来自多任务学习的经典工作:把每个任务的损失看作带噪声的高斯分布,用可学习的方差 σ 来调节权重。损失大的任务会自动获得更低的权重,反之亦然。这个方法实测下来在多任务场景里比手动调权重要稳,但需要注意数值稳定性,σ 初始值不能太大。
三是最朴素也最有效的:手动调,加早停。多个损失函数组合时,先固定一部分,只让另一个参与训练,观察各项指标的变化,再逐步放开。这种方式粗糙,但在大多数场景里能找到一个够用的配置。
我之前做一个多任务文本模型时,同时要预测情感分数和关键词位置,两个损失函数的尺度差了大约 50 倍。手动把回归分支的权重调到 0.02 之后,两个任务的表现都平衡了。这个“约 50 倍”的数字就是在小规模验证集上反复试验得出来的。说白了,组合损失函数是一门“实验调参”的手艺,但关键是要理解每个损失对梯度的贡献尺度。
3.4 代码示例:在 PyTorch 中实现自定义损失函数
框架自带的损失函数覆盖大部分场景,但等你真正遇到“需要修改损失函数逻辑”的时候,学会自定义是必须的。PyTorch 里实现一个损失函数最简单的方式是继承nn.Module,在forward里完成计算。
以 Focal Loss 为例,一个可用实现如下:
import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'): super().__init__() self.alpha = alpha self.gamma = gamma self.reduction = reduction def forward(self, logits, targets): ce_loss = F.cross_entropy(logits, targets, reduction='none') pt = torch.exp(-ce_loss) # 预测正确的概率 focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss if self.reduction == 'mean': return focal_loss.mean() elif self.reduction == 'sum': return focal_loss.sum() return focal_loss这里有个细节值得说明:pt = torch.exp(-ce_loss)这个操作是通过交叉熵损失反推“模型对正确类别的预测概率”。因为ce_loss = -log(pt),所以exp(-ce_loss) = pt。这种方式避开了手动获取 softmax 概率再取索引的繁琐操作,同时数值上也更稳定。这个技巧我在很多自定义损失函数的实现里都会用到。
验证自定义损失函数是否正确的最快方法,是用torch.autograd.gradcheck。它会用数值微分和你写的解析导数做对比,如果差异过大就会报错。新写的损失函数先跑一遍 gradcheck,能省非常多调 bug 的时间。
4. 训练中的典型问题与排查技巧
4.1 损失不下降、NaN 与梯度异常
先说 NaN。损失函数变成 NaN 的原因有很多,但最常见的是对数运算的输入越界。交叉熵里有个 log,如果预测概率 p 因为数值原因变成了 0,log(0) 就是负无穷,再加个负号就是正无穷,稍微计算一下就 NaN 了。这就是为什么 PyTorch 要专门提供把 softmax 和 log 合并的CrossEntropyLoss,也是为什么如果你自己实现交叉熵,一定要用log_softmax而不要先softmax再log。
第二个常见原因是学习率过大。损失函数的梯度是“错得越多,改得越多”,一旦某一步梯度乘以学习率后参数飞出了合理范围,模型输出变成极端值,下一轮损失直接爆掉。我一般的排查顺序是:先看是不是数据里有 NaN 或无穷值,再看学习率,最后检查损失函数本身是否有数值不稳定的操作。诊断的时候不要猜,直接把每个中间变量的min和max打印出来,一眼就能定位在哪一步炸的。
损失完全不下降,但数值是有限值,这种情况常见于“梯度太小”。注意,不是所有梯度消失都是网络结构的问题,有些是损失函数设计造成的。比如你用 MSE 做分类任务,softmax 输出饱和后梯度接近 0,模型就会停滞。这时候换成交叉熵,模型立刻被“激活”。这类问题的本质就是损失函数和任务结构不匹配。
4.2 损失函数与过拟合的关系
很多人对损失函数和过拟合的关系理解比较浅,觉得过拟合就是“模型参数太多”。实际上,损失函数也直接影响过拟合程度。
举一个最直接的例子:标签平滑。前面提过它会把 one-hot 标签变成软标签,迫使模型不要过度自信。模型预测概率不会冲到 1.0,特征空间中相同类别的样本也不会被压缩成无限小的点,泛化性随之提升。这就是通过修改损失函数来抑制过拟合的典型操作。
另一个角度是损失函数对“难样本”的关注度。Focal Loss 压制简单样本,让模型花更多精力在难样本上,这会显著增加模型对训练集的拟合能力,但也更容易在噪声标注上过拟合。如果训练集里本身有标签噪声,Focal Loss 会把模型往噪声样本上带,这时候 Focal Loss 反而是毒药。
更普遍的现象是:损失函数下降得很漂亮,训练集表现很好,但验证集指标一直不涨。这种情况我建议先看验证集损失曲线的形状。如果验证损失先降后升,那就是典型的过拟合,需要考虑正则化手段;如果验证损失从一开始就不降,那问题通常出在“训练分布和验证分布不一致”,跟损失函数关系不大,要回头检查数据划分。
4.3 调试损失函数的实用技巧合集
做深度学习这几年,我踩过的损失函数相关的坑不少,总结成一份速查表,新项目启动的时候我都会对照着过一遍。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| Loss 先降后突然 NaN | 学习率过大 | 降低学习率,或加梯度裁剪 |
| Loss 完全不降 | 损失函数与任务不匹配 | 检查是否用错了任务类型 |
| 训练正常,验证集指标不涨 | 损失函数与评估指标不对齐 | 换更接近指标的代理损失 |
| 分类训练极慢 | 用了 MSE 配合 softmax | 换成交叉熵 |
| 回归结果整体偏移 | 损失函数没有约束全局偏差 | 尝试在损失里加入 L1 正则项 |
| 多任务中某一任务不学习 | 损失尺度差异过大 | 按初始值归一化或不确定性加权 |
| 训练初期 loss 震荡 | Focal/Dice 等加剧早期梯度 | 先用基础损失训练若干轮再切换 |
还有一个我特别想强调的调试技巧:第一个 epoch 的损失值要和初始预期对上。比如随机初始化的分类模型,对于 10 类平均分布的数据,交叉熵的初始损失应该在 -log(1/10) ≈ 2.3 左右。如果初始损失远大于或远小于这个值,说明模型初始化或者数据预处理有问题,应该先解决这个问题,而不是急着训练然后找为什么学不动。
另外,损失曲线的形状也藏着信息。健康的回归 loss 曲线应该是平滑下降,后期贴地;不健康的曲线经常会出现“台阶式”下降,说明某个特定类别的样本在一批一批地被攻克,这往往是类别不平衡的表现。遇到这种情况,盯着 loss 本身是没用的,要按类别拆开算损失,看看是哪个类别贡献了主要误差,再有针对性地调损失权重。
4.4 一个实战案例:分割任务的损失函数改造
去年我做一个医学影像分割的模拟项目,病灶区域在整张图里只占极小比例,可能 2% 都不到。一开始直接用交叉熵,模型学到的全是背景,输出基本是全黑图,但损失还降得挺漂亮,因为背景像素占了 98%,全预测成背景的准确率本来就是 98%。这就是“损失函数下降但模型完全没学到东西”的典型表现。
我先把交叉熵改成了带类别权重的版本,给病灶像素更大的权重。效果有提升,但训练过程非常震荡,稍不留神就崩。后面查资料发现 Dice Loss 对这类任务更合适,就换成了 Dice + 交叉熵的组合,比例设为 1:1。训练前期用交叉熵主导,帮助模型快速学到粗略的病灶位置,后期 Dice 主导,精细优化区域重叠度。这个组合做下来终于让模型开始真正关注病灶区域。
这次经历让我对损失函数有两个非常深刻的体会。第一,损失函数的选择不能独立于数据分布和评估指标,它是一个系统设计问题。第二,真实任务的损失函数几乎永远是组合的,不要指望一个现成的函数直接解决所有问题。最有效的路径是理解每个损失函数的梯度行为,然后按照训练阶段和数据特点把它们组合起来。
最后再分享一个小技巧:自定义损失函数时,我会刻意在forward里把中间结果分离出来,比如把每个类别的平均损失存到字典里,便于训练时可视化。别人看到的是总损失,而你通过拆解类别的损失变化,往往能在第一轮训练就发现问题。这个习惯帮我避过不少坑,也建议你试试。