
简介面向 PyTorch 初学者的编程练习资料与 B 站线性模型课程配套核心目标是绕过 TensorBoard 等高阶工具用 matplotlib 手动绘制损失Loss曲线直观认识模型训练中的收敛过程。压缩包内只有 1 个 PDF 文件大小约 60KB内容围绕线性回归示例展开先导入 numpy 和 matplotlib.pyplot构造 x_data[1.0,2.0,3.0]、y_data[2.0,4.0,6.0] 两组数据再定义 forward(x)x*w 和均方误差损失函数随后遍历 0.0 至 4.1 的权重 w累计每个样本的 loss 并求平均值 MSE将权重与 MSE 存入列表后用 plt.plot 画出 Loss 随 w 变化的曲线同时给出每步输出结果。除了完整代码和运行效果外资料还解释了过拟合、欠拟合以及如何根据损失曲线判断模型收敛等调参要点能帮助读者将可视化结果与训练策略结合起来。目前已有 16272 人学习下载适合刚接触深度学习、希望动手实践并理解损失曲线原理的开发者。1. 为什么PyTorch练习里要先画Loss曲线训练脚本跑完看终端里最后一个 epoch 的 Loss 是 0.8好像降了一点但到底是从 1.5 平滑降下来还是中途跌倒 0.6 又弹回去完全看不出来。Loss 曲线解决的就是这个盲区把训练过程按 step 或 epoch 展开成折线梯度爆炸、学习率太大、过拟合这类事故在曲线形态上各有各的长相。这里直接围绕 PyTorch 基础框架的典型训练循环来讲覆盖 Loss 怎么记录、怎么用 Matplotlib 画出来、画完怎么读最后落到 Focal Loss 这类不均衡损失下的曲线诊断。刚配好 PyTorch 环境的人可以照着抄跑过几个模型的人也能在参数细节里找到自己踩过的坑。2. 在PyTorch训练循环里把Loss按粒度收集起来2.1 按step记录还是按epoch记录画曲线之前第一步是决定记录粒度。如果每个 step 都画曲线会密到完全看不出趋势如果每个 epoch 只记最后一个 batch 的 loss又容易被噪声带偏。下面这个表格是我平时选择的依据记录粒度优点缺陷适合场景每个 step能看到单个 batch 对梯度的冲击锯齿严重趋势不直观调试 NaN、梯度爆炸每个 epoch 均值平滑反映整体收敛趋势丢失 batch 细节日常训练监控与最终绘图每个 epoch 中位数抗离群 batch更稳计算稍麻烦数据里有脏标签时在常规 PyTorch 练习里我会同时留两种训练循环内用loss.item()收集 step 级数值每隔若干 step 打印一次方便中途发现异常 batchepoch 结束后再算均值存入 list用于最终绘图。均值计算有个细节不要用sum(losses) / len(losses)而要按样本数加权因为 DataLoader 的最后一个 batch 通常不满等权重平均会让最后几次更新的 loss 比真实水平偏轻或偏重。2.2 在训练循环里收集Loss的可复用代码import torch import torch.nn as nn from torch.utils.data import DataLoader # 模型与数据加载请替换为自己的实现 model ... train_loader DataLoader(...) val_loader DataLoader(...) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.9) epochs 30 train_losses [] # 每个 epoch 记录一个均值 val_losses [] global_step 0 for epoch in range(epochs): model.train() total_loss 0.0 total_samples 0 for x, y in train_loader: optimizer.zero_grad() out model(x) loss criterion(out, y) loss.backward() optimizer.step() global_step 1 if global_step % 100 0: print(fstep {global_step}, loss {loss.item():.4f}) # 按样本数累加而不是按 batch 数累加 total_loss loss.item() * x.size(0) total_samples x.size(0) train_losses.append(total_loss / total_samples) model.eval() val_loss_total, val_samples 0.0, 0 with torch.no_grad(): for x, y in val_loader: out model(x) val_loss_total criterion(out, y).item() * x.size(0) val_samples x.size(0) val_losses.append(val_loss_total / val_samples) print(fepoch {epoch 1}: train {train_losses[-1]:.4f}, fval {val_losses[-1]:.4f})这段代码的要点有三个。第一记录时统一用loss.item()拿到 Python float而不是把loss张量整体 append 到列表里否则列表会持续持有计算图节点显存只增不减。分布式或混合精度场景下如果你确实需要loss.detach()后再同步再改成loss.detach().cpu().item()那属于另一套写法。第二total_loss loss.item() * x.size(0)是样本数加权最后一个不完整 batch 不会让均值失真。第三验证阶段必须同时写model.eval()和with torch.no_grad():前者切换 BatchNorm 和 Dropout 的推理状态后者关闭自动求导既省显存又能让验证循环明显变快。参数说明lr0.01和momentum0.9是 SGD 的常见起步值手写练习阶段先用默认即可step_count % 100控制打印频率如果你的数据集很小、一个 epoch 只有二三十个 batch改成每次迭代都打印更合适。epochs30也不是固定值后文提到用曲线判断早停时就可以把它调大让训练自己决定什么时候停。2.3 收集阶段常见的两个错误第一个错误是忘记.item()。常见现象是训练跑到一半显存占用率持续爬升最后 OOM脚本报错后误以为模型太大。其实只要把 list 里 append 的张量全部释放掉就能缓解。第二个错误是把验证集 loss 也放进自动求导图里验证循环没有torch.no_grad()包住导致验证阶段不仅慢而且每个 epoch 结束还保留一份验证集计算图。这两种情况在绘制曲线时不会直接报错但都会让你误判训练是否健康。提示如果你在冻结主干、只训练分类头的迁移学习场景里记录 loss记得确认requires_grad状态否则optimizer认为没有参数需要更新曲线平坦得毫无信息量。2.4 用TensorBoard看实时曲线除了最终画图我建议在训练中途就瞄一眼曲线。用torch.utils.tensorboard可以做到from torch.utils.tensorboard import SummaryWriter writer SummaryWriter(runs/loss_demo) for epoch, (tl, vl) in enumerate(zip(train_losses, val_losses)): writer.add_scalar(Loss/train, tl, epoch) writer.add_scalar(Loss/val, vl, epoch)在终端执行tensorboard --logdir runs浏览器打开 http://localhost:6006 就能看到实时更新的两条 Loss 曲线。TensorBoard 适合观察 step 级锯齿与梯度直方图Matplotlib 适合最终出图两者用途不同可以同时落地。要注意SummaryWriter的 log_dir 不要给多个实验共用同一个子目录否则曲线会叠在一起建议每次跑实验单独建一个runs/exp_日期_时间目录。3. 用Matplotlib绘制Loss曲线的最小代码与参数调整3.1 把记录下来的Loss画成折线图训练结束后用 Matplotlib 画图的核心代码只需要十来行import matplotlib.pyplot as plt plt.figure(figsize(8, 5)) epochs_axis range(1, len(train_losses) 1) plt.plot(epochs_axis, train_losses, labeltrain loss, color#1f77b4, linewidth2) plt.plot(epochs_axis, val_losses, labelval loss, color#d62728, linewidth2) plt.xlabel(epoch) plt.ylabel(loss) plt.title(PyTorch Training Loss Curve) plt.legend() plt.grid(True, linestyle--, alpha0.5) plt.savefig(loss_curve.png, dpi150) plt.show()这里的几个参数值得说明。figsize(8, 5)控制画布宽高比8:5 是出图比较稳的比例若比例接近正方形早期下降段会被纵向拉伸显得训练一启动就收敛容易高估模型行为。color固定两条线的颜色比默认颜色稳定报告和文章里多次出图时也方便统一风格。grid用虚线加半透明目的是对齐横轴 epoch 刻度否则曲线某一点对应哪个 epoch 只能靠猜。savefig最好放在show之前。一部分后端在plt.show()之后会重绘或清空画布代码顺序反了保存出来的图片可能是空白或者没有图例。dpi150对屏幕足够论文投稿需要 300 以上可以改成dpi300。3.2 Loss曲线平滑EMA比移动平均更直观step 级 loss 直接画出来锯齿经常大到盖住整体趋势这时候要平滑。常见做法有两种滑窗平均和指数移动平均EMA。滑窗平均的边界处理比较烦人窗口结束阶段曲线会短一节还要额外处理 NaNEMA 用一行循环就能实现效果也更贴近工程师习惯看到的“手感”。ema_alpha曲线特征建议场景0.6贴近原始仍保留大部分抖动排查某个 epoch 出现的异常波动0.7 ~ 0.8平滑且滞后小日常监控默认值0.9 及以上非常平滑拐点滞后 2~3 个 epoch看长趋势判断整体收敛对应实现如下def smooth_curve(values, ema_alpha0.9): 指数移动平均alpha 越大曲线越平滑但滞后越明显 smoothed [] last values[0] for v in values: last ema_alpha * last (1 - ema_alpha) * v smoothed.append(last) return smoothedema_alpha是个需要动手调的参数。0.9 时当前值占比 10%曲线很顺滑但拐点会有两三个 epoch 的滞后0.6 到 0.7 更贴近原始曲线适合看训练中后段的细微抖动如果是验证集 loss 这种本来就平滑的序列0.95 也没问题。实际使用时我会同时保留原始曲线和 EMA 曲线而不是只画一条平滑后的线否则遇到 loss 突然跳变的 bug平滑会把事故抹成一次普通波动。3.3 同时展示原始曲线和平滑曲线的做法fig, ax plt.subplots(figsize(8, 5)) ax.plot(epochs_axis, train_losses, colorlightgray, linewidth1, labelraw train) ax.plot(epochs_axis, smooth_curve(train_losses, 0.7), colorblue, linewidth2, labelsmoothed train) ax.set_yscale(log) ax.set_xlabel(epoch) ax.set_ylabel(loss (log)) ax.legend() ax.grid(True, linestyle--, alpha0.5)这段代码演示了两件事一是原始曲线用浅灰色做底平滑线用亮色做前景视觉上信息量最大二是纵轴直接设成 log用于 loss 跨越数量级的场景。这里注意alpha这个单词在代码里出现过多次一次是ax.plot(colorlightgray, alpha0.6)里的透明度另一次是smooth_curve的 EMA 系数建议变量名区分成line_alpha与ema_alpha不然三个月后回来看代码容易混淆。3.4 多子图与绘图参数统一对比多组实验时不要开多个 figure 窗口用subplots排成一行或一列fig, axes plt.subplots(1, 2, figsize(12, 4), shareyTrue) axes[0].plot(train_losses) axes[0].set_title(train) axes[1].plot(val_losses) axes[1].set_title(val) for ax in axes: ax.set_xlabel(epoch) ax.grid(True)shareyTrue保证两张子图纵轴范围一致组间比较才公平。如果只保存不显示记得在plt.show()之前fig.savefig(compare.png, dpi150)。4. 读懂Loss曲线该关注的参数与常见掉坑点画出来只是开始真正帮你做决策的是曲线形态。4.1 纵轴用log还是线性这是一个很容易被忽略的绘图参数。很多人拿到 loss 就plt.plot()但 loss 的最小值如果比最大值小两个数量级以上线性纵轴会把小数值压成一条水平线训练后段到底还在不在下降完全看不清。损失范围任务示例推荐纵轴原因1 ~ 10 且跨度小分类 CrossEntropy线性变化集中在同量级线性更直观1e-3 ~ 1MSE 回归、Focal Losslog大动态范围log 能同时看到早晚期变化1e-5 ~ 1e-2生成模型、对比学习log需要观察小数点后四位的变化切 log 用一行代码ax.set_yscale(log)。注意如果 loss 记录里出现过 0 或负值log 轴会在这些点上直接不显示或报错先做max(1e-8, v)的截断再画。4.2 三种典型的坏曲线第一种loss 从第一个 epoch 开始就持续上升或先降后爆。十有八九是学习率过大把lr降到原来的 0.1 倍再看有时配合torch.nn.utils.clip_grad_norm_能把训练稳住但这只是止血根因还是初始学习率不合适。第二种train loss 一路下降val loss 降到某一点开始回升。这是标准过拟合曲线。别急着加 Dropout先确认 val_losses 那个回升点是真实存在还是验证集样本太少导致的抖动确认后优先考虑增大数据量或数据增强其次才是正则化。第三种曲线整体下降但抖幅越来越大。常见原因是一个 batch 里混进了异常样本或标签噪声也可能是 batch size 太小导致梯度方差大。可以跳步打印每个 step 的 loss找出那个特别高的点看看对应输入数据长什么样。4.3 曲线锯齿明显时先查四件事DataLoader 是否忘了shuffleTrue。顺序固定时每个 epoch 都按同一批难样本循环曲线会出现周期性尖峰。注意验证集不要 shuffle只有训练集需要。batch size 是否过小。从 16 调到 64 往往能让曲线平滑很多代价是收敛速度变慢训练稳定优先。学习率调度器是否在固定步长切换。StepLR每次降学习率loss 会出现台阶式下跌这是预期行为不是 bug。检查optimizer初始 lr 与 scheduler 的last_epoch是否对齐。梯度里是否出现 NaN 或 Inf。在loss.backward()后加一句for p in model.parameters(): torch.isnan(p.grad).any()即可快速定位曲线抖幅突然变成“跳崖”时优先查这个。4.4 根据val loss趋势调整早停条件曲线不只在训练结束后用于复盘也可以在训练中实时决策。常见做法是连续 N 个 epoch val loss 没有下降超过一个阈值就触发早停并恢复最优状态。N 通常 10 到 20阈值取当前最佳 val loss 的 0.1%。best_val float(inf) best_state None patience 15 wait 0 for epoch, vl in enumerate(val_losses, 1): if vl best_val * 0.999: best_val vl best_state {k: v.clone() for k, v in model.state_dict().items()} wait 0 else: wait 1 if wait patience: model.load_state_dict(best_state) break0.999是“下降 0.1% 才算改善”的阈值避免在极小波动中反复保存。patience越大训练越晚停但不容易漏掉后段缓降练习阶段用 15 是一个平衡点。注意保存的是state_dict的克隆而不是原引用否则后续训练会把最佳权重覆盖掉。5. 进阶在Focal Loss下用Loss曲线做诊断前面聊的都是 CrossEntropy 的曲线换到 Focal Loss 这类为类别不均衡设计的损失时曲线解读的重点会变。5.1 Focal Loss曲线和普通交叉熵曲线的差异Focal Loss 的示意公式是FL(p_t) -alpha_t * (1 - p_t) ** gamma * log(p_t)。gamma越大模型对易分类样本的 loss 打压得越狠。相同的模型和数据集上Focal Loss 曲线起始值通常明显低于 CrossEntropy因为大部分样本都是易分类样本它们原本贡献的 loss 被压低。这时候用线性纵轴很容易把曲线压成一条平线。我一般直接ax.set_yscale(log)只有当最小值与最大值在同一个数量级内才改回线性。同样处理类别不平衡的思路还有 logit adjustment loss它是从决策边界角度修正 logits曲线数值通常更接近 CE但val曲线会比 Focal 更容易出现过拟合信号可以自己跑一组对比观察两条曲线的“分离点”出现在第几个 epoch。5.2 把Loss曲线和准确率曲线放一起看Loss 下降但准确率不动说明模型只是对已有样本更自信分类边界没有真变化。光看 Loss 会误以为模型还在进步。建议在验证循环里同步统计 acc和 Loss 画在同一个画布上但用双纵轴num_correct (out.argmax(dim1) y).sum().item() val_acc.append(num_correct / len(y))然后fig, ax1 plt.subplots(figsize(8, 5)) ax1.plot(val_losses, colorred, labelval loss) ax1.set_ylabel(loss) ax2 ax1.twinx() ax2.plot(val_acc, colorblue, labelacc) ax2.set_ylabel(accuracy)如果 loss 和 acc 同时在动说明模型在“真学”如果 loss 降但 acc 平优先怀疑是样本均衡问题而不是单纯调参问题。5.3 把曲线数据落盘避免重复训练一个容易被忽视的小技巧训练结束后把train_losses和val_losses写成文本之后画图脚本单独读文件不用每次重跑训练。在训练脚本末尾加两行with open(loss_history.txt, w) as f: for ep, (tl, vl) in enumerate(zip(train_losses, val_losses), 1): f.write(f{ep}\t{tl:.6f}\t{vl:.6f}\n)后续想重绘或对比多组实验时只需要写一个读取loss_history.txt的小脚本把所有实验的曲线叠在一张图里横轴 epoch 对齐即可直接对比不同学习率下的收敛速度。本文还有配套的精品资源点击获取