资讯详情

Pytorch训练中Loss曲线绘制全攻略:从记录到可视化与调参

📅 2026/10/4 19:21:06 | 华诺云谱 👁 阅读
Pytorch训练中Loss曲线绘制全攻略:从记录到可视化与调参
简介一份面向PyTorch初学者的练习笔记聚焦模型训练中Loss曲线的绘制方法。内容以线性回归为例逐步演示numpy与matplotlib的导入、样本数据设置、前向传播与均方误差损失函数定义并通过遍历权重w计算MSE、最终绘制Loss曲线。适合正在学习深度学习基础、希望理解损失函数与参数关系的读者也可作为课程配套的简明实验参考。资源为1个PDF文件体积约60KB内容精炼便于快速查阅。目前已有16273人学习下载。通过这份材料读者可以掌握在不依赖TensorBoard等高级工具的情况下用原生PyTorch思路手写可视化流程并了解如何通过Loss曲线判断模型收敛状态为后续训练更复杂网络打下基础。1. 绘制Loss曲线之前先想清楚曲线要告诉你什么一个常见误区是等训练全部结束才想起来画Loss曲线。训练跑了一夜图一画出来训练损失一路走低验证损失却从第20个epoch开始反弹整晚白跑。其实Loss曲线在Pytorch里不是什么复杂功能它就是从训练循环里记录的一组标量再用matplotlib画两条线但恰恰是“记录”这一步决定了曲线能不能反映真实情况。作为一线工程师我见过太多人把重心放在画图样式上结果数据记录得稀碎最后曲线完全不能用于判断拟合状态。这篇文章把Pytorch练习里最常用的Loss曲线绘制过程拆开先讲训练循环里怎么记录train/val损失和学习率再给最小绘图脚本按“平滑、对比、排错、沉淀”的顺序讲透适合刚跑通分类或目标检测任务、想快速看懂训练发生了什么的新手也适合需要反复对比不同超参数的熟练工程师。2. 训练循环先记录Loss决定曲线成败的不是绘图而是数据2.1 为什么按batch记录loss会把曲线画成锯齿很多人习惯在训练循环里打印每个step的loss然后把所有数值收集起来画图。这样做的直接后果是曲线变成一根密密麻麻的锯齿完全看不出趋势。原因在于相邻batch的数据分布不一样尤其用了BatchNorm之后每个step的loss波动会被放大训练初期loss下降很快后期变化很慢原始点会把这两种节奏混在一起。更麻烦的是如果你把每个step的loss都存进列表几千个点里真正有信息量的没几个画出来之后你自己都不知道该信哪里。我一般会按epoch做平均每个epoch只产生一个点。这样保存下来的曲线天然平滑而且能和其他实验的曲线对比。你不需要额外引入TensorBoard这类重工具一个列表就够了。关键操作是loss要调用.item()取出Python标量而不是保留整个张量——如果直接把loss加进list整个计算图都不会释放内存越积越多训练到一半就可能被OOM干掉。Pytorch基础框架里每个epoch的loss平均值差不多就够画趋势了。如果你在跑分割或检测任务一个batch里样本尺度差异大平均值的波动也会大这时候更不应该保存单步数值。先按epoch把数据点压缩后续才谈得上平滑和对比。2.2 一个能同时记录train/val的最小训练循环直接给一个能跑的最小代码块这段代码会把训练集和验证集的loss都按epoch存下来同时记录学习率。验证集必须使用torch.no_grad()否则不仅浪费显存还可能因为验证阶段仍然构建计算图让后续的绘图脚本出现莫名其妙的显存溢出。# train_losses 和 val_losses 会在整个训练过程中不断被追加 train_losses, val_losses, lr_records [], [], [] for epoch in range(epochs): model.train() running_loss 0.0 for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() out model(x) loss criterion(out, y) loss.backward() optimizer.step() # 用 .item() 拿标量不保留计算图避免内存累积 running_loss loss.item() epoch_train_loss running_loss / len(train_loader) train_losses.append(epoch_train_loss) model.eval() val_running 0.0 with torch.no_grad(): for x, y in val_loader: x, y x.to(device), y.to(device) out model(x) val_running criterion(out, y).item() epoch_val_loss val_running / len(val_loader) val_losses.append(epoch_val_loss) # 记录当前学习率后面很多对比实验要靠它归因 lr_records.append(optimizer.param_groups[0][lr]) print(fepoch {epoch1:03d} | train {epoch_train_loss:.4f} | val {epoch_val_loss:.4f})这段代码的逻辑不复杂但有三个点必须说清楚。第一len(train_loader)是batch的个数不是样本数所以算出来的是一个batch的平均loss这个值和你loss函数里设置的reduction保持一致就行。第二如果换用BCE Loss、Focal Loss这类不同损失函数曲线的绝对数值区间会不同但记录方式完全一样不需要改结构。第三验证loss的数值通常和训练loss有差距这是正常的关键是看两条线之间的距离怎么变化而不是追求它们完全相等。2.3 学习率也要留下来不然对比实验没法归因只记loss不记学习率是很多人后期做对比实验时最后悔的一件事。Pytorch里常见的学习率调度器比如StepLR、CosineAnnealingLR每个epoch的学习率都在变。如果某一次实验loss掉得特别快你可能说不清是模型结构变好了还是恰好撞上了学习率的高位区间。所以我建议在训练循环里顺手把optimizer.param_groups[0][lr]存下来。这样画Loss曲线的时候可以把学习率曲线放在同一张图的次坐标轴上一眼就能看出loss的拐点是不是由学习率变化引起的。这种做法在调warmup和CosineAnnealing时尤其好用因为曲线形状经常和学习率曲线强相关。另外一个容易被忽略的点是列表只存在内存里脚本一退出就没了。我建议每个epoch结束后把本轮的数字追加到CSV里具体做法在后面的避坑章节会给出。先把损失记录这一环做扎实绘图的输入才不会出问题。3. 用matplotlib把Loss曲线画出来最小脚本与三个必调参数3.1 训练线和验证线为什么要分开画很多人第一次画Loss曲线时只画训练loss画完看不出问题。训练loss低只能说明模型在训练集上拟合得好无法判断泛化情况。真正有价值的信息来自训练线和验证线之间的距离和走向。如果训练loss持续下降验证loss在第20个epoch之后开始反弹说明模型开始过拟合如果两条线都高居不下说明欠拟合或学习率设置不对如果两条线同步下降且间距稳定说明训练状态比较健康。所以从记录数据那一刻起就要把train loss和val loss分开存。画图时也分开用不同线型和颜色画。有人会问验证集每轮只有一批数据计算出来会不会波动大确实会尤其是验证集样本少的时候。这时候可以在验证集上多做几次dropout采样或者直接对验证loss用指数移动平均处理这一点我会在下一章展开。3.2 最小可用的绘图脚本下面是一段可以直接复制的matplotlib绘图代码。它没有任何花哨功能就是画出两条曲线、加上图例和网格、保存成PNG。这样做的好处是无论你在本机还是服务器上跑输出都是同一张图不需要额外配置图形界面。import matplotlib.pyplot as plt # 如果标题里要显示中文需要指定中文字体不想折腾就直接用英文标题 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False epochs range(1, len(train_losses) 1) fig, ax plt.subplots(figsize(10, 6), dpi120) ax.plot(epochs, train_losses, o-, labeltrain loss, linewidth1.8, markersize4) ax.plot(epochs, val_losses, s--, labelval loss, linewidth1.8, markersize4) ax.set_xlabel(epoch) ax.set_ylabel(loss) ax.set_title(Pytorch 训练 Loss 曲线) ax.legend() ax.grid(True, linestyle--, alpha0.4) fig.tight_layout() fig.savefig(loss_curve.png)这里有个小习惯训练线用实线加圆点o-验证线用虚线加方块s--。这样即使打印成黑白图或者读者有色盲问题依然能区分两条线。linewidth1.8保证线条清晰markersize4让每个epoch点不会太大、也不会小到看不清。fig.tight_layout()这行不能省它会把坐标轴标签和图例重新排版避免保存出来的图片四周留白不均匀或者标签被裁剪。3.3 三个必调参数figsize、dpi、平滑窗口每次画Loss曲线都反复调样式是浪费时间先把下面这几个参数定好后面基本不用动。参数典型值作用figsize(10, 6)画布尺寸太小看不清拐点太大存图浪费空间dpi120像素密度120用于屏幕看图足够论文插图用到200以上平滑窗口见第四章对验证线做平滑避免单点噪声干扰判断figsize和dpi其实是一对配合的参数同样一张图figsize(10,6), dpi120和figsize(6,4), dpi300的最终像素数量差不多但文字和线条的相对大小完全不同。我一般习惯屏幕预览用figsize(10,6), dpi120要放进报告里再单独用大尺寸重新渲染一次。平滑窗口不应该是每次画图时顺手改的数字而应该是一个固定的函数参数。你只需要记住训练loss的epoch平均已经比较光滑了平滑主要用在验证loss或那些带噪声的原始记录上。具体算法我推荐指数移动平均下一章详细说。4. 让曲线更有信息量平滑、对比与视觉秩序4.1 指数移动平均比滑动窗口更好用的平滑方式很多教程会用np.convolve做滑动窗口平均但我更推荐指数移动平均EMA。滑动窗口平均有一个问题窗口大小需要自己试太小压不住噪声太大拐点会被抹平。用EMA的时候你只需要控制一个alpha参数它对近期的数据赋予更高的权重实际效果更直观。下面这个函数可以直接贴进你的绘图脚本里def ema_smooth(values, alpha0.95, warmup5): smoothed [] last values[0] for i, v in enumerate(values): if i warmup: # warmup阶段保留原始点避免曲线头部变形 smoothed.append(v) last v else: last alpha * last (1 - alpha) * v smoothed.append(last) return smoothed这个函数有两次调整空间。alpha越大曲线越平滑但对真实拐点反应越迟钝alpha越小曲线越贴近原始数据噪声也会保留更多。我常用的区间在0.9到0.99之间。warmup的作用是防止前面的点被平滑得过早因为训练前几个epoch的loss往往变化剧烈如果一上来就用EMA开头一段会明显失真。经验做法是warmup设为5左右前5个epoch保留原始值。EMA还有一个隐藏好处你可以把平滑后的线和原始线叠在一张图上原始线用很浅的颜色画平滑线用深色画。这样既能展示真实波动范围又不丢失趋势。这个技巧在验证集数据量少、曲线噪声大的时候特别管用。4.2 两代模型的对比图横向比较才更能说明问题如果你只画一次训练的Loss曲线其实看不出太多东西。真正有价值的是把修改前后的两条曲线放在同一张图里对比比如换了一个数据增强策略、调整了损失函数、或者改了网络结构。画对比图有个细节两次训练的epoch总数可能不一样直接画在一起会导致两条曲线的x轴长度不一致视觉上容易误判。我的做法是在代码里先做一次对齐比如都以较短实验的epoch数为准去做截断或者用numpy.interp插值到统一长度。截断更简单也不会引入虚拟数据点。import numpy as np min_epochs min(len(losses_a), len(losses_b)) epochs_aligned range(1, min_epochs 1) fig, ax plt.subplots(figsize(10, 6), dpi120) ax.plot(epochs_aligned, losses_a[:min_epochs], o-, labelbaseline) ax.plot(epochs_aligned, losses_b[:min_epochs], s-, labelmodified) ax.set_xlabel(epoch) ax.set_ylabel(loss) ax.legend() ax.grid(alpha0.4) fig.savefig(compare.png)对比时不要只看训练loss。如果baseline的验证loss比改进方案的更低说明这次修改大概率是负优化如果训练loss差不多但验证loss明显更低说明泛化能力确实提升了。一次完整的对比实验应该同时包含train和val四条线但为了视觉清晰我建议分两张图画一张训练loss对比一张验证loss对比共享相同的y轴范围会更直观。4.3 颜色、线型、图例的视觉约定多条线放在同一张图的时候最容易翻车的不是代码而是视觉秩序。如果每次画图都是随机颜色看三分钟眼睛就花了。我给自己定的约定是同一个模型的训练线和验证线用同一色系的颜色训练用实线验证用虚线不同模型之间用不同颜色区分。这样横向对比时先看颜色定位模型再看线型区分train/val效率会高很多。图例的位置也有讲究。默认的ax.legend()一般放在右上角但如果你的验证loss曲线正好在右上角区域图例就会挡住关键拐点。我一般用ax.legend(locupper right)或bbox_to_anchor(1.02, 1)手动调节。如果你画的是多标签任务的多个loss建议用subplots每个子图放一条曲线而不是把所有线都叠在一起。多条线叠一张图的做法我真的踩过坑最后除了自己谁也看不懂。网格线alpha0.4是一个比较舒服的值太深会抢走数据线条的注意力太浅在投影仪上看不清。这些参数不需要记记住“每条曲线都要能被单独辨认”这个原则就够了。5. 绘制Loss曲线常见的6个翻车点与排查思路5.1 锯齿严重趋势完全看不清现象画出来的曲线像心电图每一个点都剧烈跳动看不出loss是升还是降。原因你保存的是每个step的原始loss值而不是每个epoch的平均值。相邻batch的差异在视觉上被放大了。解决改成按epoch平均后再画这是最省事也最有效的办法。如果平均之后还是抖得厉害再对验证loss用上一章的EMA平滑处理。5.2 val loss比train loss高一个量级现象验证loss大概是训练loss的5到10倍曲线整体被托上去两条线几乎分不开。原因最常见的是训练时模型处于train模式有dropout和BatchNorm而验证阶段忘了调用model.eval()导致验证时网络仍然在做不稳定的采样另一种可能是训练和验证用了不同的loss计算方式比如训练用reductionmean验证时误用了reductionsum。解决先把验证阶段加model.eval()再用同一个criterion分别计算两个集合的loss。如果确认无误后差距还是很大说明模型泛化确实有问题这时候曲线变差反而是真相。5.3 训练loss在降但精度或mAP不涨现象loss曲线所有都在往下走看起来一切正常但accuracy指标原地不动。原因损失函数和评估指标并不总是一致的。比如用一个边界框回归loss它的下降不代表IoU在上升又比如类别极度不均衡时模型学会把所有样本预测为多数类loss可能仍然在下降但目标类的精度是0。解决不要把Loss曲线当唯一依据应该把accuracy、precision或mAP曲线和Loss曲线上下拼在同一张图里共享epoch轴一起观察。画两个独立的子图即可代码不复杂但能让你立刻发现“loss在骗人”的情况。5.4 训练中断所有记录在内存里全丢现象训练到第80个epoch时服务器OOM或者代码报错退出你发现train_losses这个列表只存在于刚才的进程里什么都没留下。原因你只在内存里保存了记录没有落盘。解决每个epoch结束后把当前数字追加到CSV文件。只需要几行代码import csv with open(train_log.csv, a, newline) as f: writer csv.writer(f) writer.writerow([epoch 1, epoch_train_loss, epoch_val_loss, optimizer.param_groups[0][lr]])这样就算训练中断重新写一个脚本读CSV也能把已经完成的曲线画出来不用重新训练。这个习惯我一直保留着它相当于给训练过程吃了颗后悔药。5.5 loss突然变成NaN曲线断档现象训练到某个epoch之后loss变成nan曲线出现断崖或者消失后面再也画不出来。原因学习率设置过高导致梯度爆炸或者数据里出现inf/nan值或者在混合精度训练下loss缩放异常。解决先把学习率降一个量级试试这是成本最低的排查方式。然后在训练循环里加一个检查如果loss不是有限数值跳过当前step的更新if not torch.isfinite(loss): print(fepoch {epoch1} step {step}: loss is nan, skip update) optimizer.zero_grad() continue注意要放在loss.backward()之前判断因为loss已经算出来了直接检查它本身。不会损失太多训练进度但能防止整个训练进程因为一个坏样本挂掉。6. 把画图嵌进训练流程用CSV兜底让曲线随时可复现6.1 用CSV兜底训练中断也能把曲线重新画出来现在建议你把训练和绘图彻底解耦。训练时只负责往CSV里追加数据绘图时单独写一个脚本从CSV读数据。这样做有两个好处训练过程中崩溃不会丢数据同一份CSV可以随时重新绘制方便不同阶段调整画图参数后反复出图。一个简单的离线绘图脚本可以是这样的import csv import matplotlib.pyplot as plt epochs, train_losses, val_losses [], [], [] with open(train_log.csv, r) as f: reader csv.reader(f) header next(reader) # 跳过表头 for row in reader: epochs.append(int(row[0])) train_losses.append(float(row[1])) val_losses.append(float(row[2])) fig, ax plt.subplots(figsize(10, 6), dpi120) ax.plot(epochs, train_losses, o-, labeltrain loss, linewidth1.8) ax.plot(epochs, val_losses, s--, labelval loss, linewidth1.8) ax.set_xlabel(epoch) ax.set_ylabel(loss) ax.legend() ax.grid(alpha0.4) fig.savefig(loss_curve_from_csv.png)从CSV重新画出来的曲线和你训练结束时画的那张完全一致这也方便你在不同机器之间迁移实验记录。把上一次实验的CSV和训练脚本一起归档下次想要复现的时候根本不用重新训练。6.2 曲线画完之后怎样验证这次训练是否正常很多人画完曲线看一眼就关了缺少一次正式的“验收”。我自己的习惯是固定检查四件事训练loss最终值是否落在合理范围验证loss和训练loss之间的距离是否稳定两条线有没有在最后阶段出现明显分叉学习率曲线是否和loss曲线匹配。这四条看完再决定是继续训练、提前停止还是调整超参。如果你训练的是分类任务还可以额外注意一点初始loss是否符合预期。比如用CrossEntropyLoss做10分类随机初始化的模型loss应该在log(10)≈2.3附近如果差很多大概率是权重初始化或数据处理出了问题。用BCE Loss做多标签分类时初始值会根据正负样本比例变化不能直接套经验值。这些数值层面的细节比曲线形状更能说明问题。说句实在话我现在开新项目的第一件事不是搭模型而是先把CSV记录和画图脚本写好。训练再久也不慌因为随时可以重新画图、随时可以对比实验、随时能回答“上一次模型到底在哪一步开始过拟合”这个最容易被忽略的问题。希望这些做法能帮你也少走一次弯路画出的每条Loss曲线都能真正指导你调参。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑