首页 / 资讯中心 / 文章详情

PyTorch训练Loss曲线可视化:matplotlib从入门到避坑

PyTorch训练Loss曲线可视化:matplotlib从入门到避坑 ★ FEATURED ARTICLE
1. 为什么一张Loss曲线值得单独拿出来讲训练一个神经网络最怕的不是报错而是看起来在跑其实早就废了。终端里一行行刷过去的loss: 0.6931数字单看某一个值毫无意义只有把它们连成一条线你才能判断模型到底是在学、在瞎猜、还是在过拟合。pytorch负责算梯度、更新参数但它本身不负责画图matplotlib就是那个把训练日志变成心电图的工具。这个组合几乎是每个做深度学习的人绕不开的基本功。这篇内容解决的就是一个非常具体的问题在 PyTorch 训练循环里把每个 epoch或每个 step的 loss 收集起来用 matplotlib 画成一条清晰、可读、能直接放进报告或论文的曲线图。看起来简单但真正动手时坑一点都不少——loss 列表长度对不上、图画出来是空白、中文标签变成方块、多条曲线挤在一起分不清谁是谁、验证集曲线和训练集曲线画在同一张图上却忘了加图例。这些问题我在带新人和自己复现项目时见过太多次。适合谁看刚跑通第一个nn.Module、想把训练过程可视化的新手已经会画图但总在细节上翻车的同学以及需要把 loss 曲线整理成规范图表放进项目文档的从业者。下面我会从整体设计思路讲到逐行可抄的代码再到我实际踩过的坑和排查方法尽量让你看完就能直接用到自己的训练脚本里。2. 整体设计思路先想清楚画什么再动手怎么画2.1 数据从哪来训练循环里的记账环节画图的前提是有数据。PyTorch 的训练循环本质上是一个不断重复的过程前向传播算输出、算 loss、反向传播算梯度、优化器更新参数。loss 这个标量在每一步都会产生但它默认只存在于当前这一步的变量里下一步就被覆盖了。所以第一件事是在循环外准备容器在循环内把 loss 追加进去。这里有个容易被忽略的细节你到底要记录每个 batch 的 loss还是每个 epoch 的平均 loss两者画出来的曲线形态完全不同。batch 级 loss 抖动大、点密集能看出训练早期的剧烈震荡epoch 级 loss 平滑适合看整体趋势。我的习惯是两者都留batch 级用于观察稳定性epoch 级用于对外汇报。容器用最朴素的 Pythonlist就够了不需要 numpy 数组——因为训练过程中长度是动态增长的list 的append是 O(1)而反复拼接 numpy 数组反而低效。2.2 为什么选 matplotlib 而不是别的有人会问TensorBoard 不是更专业吗确实TensorBoard 能实时刷新、能对比多次实验但它的门槛在于要起一个本地服务、要在浏览器里看而且导出的图样式不够自由。matplotlib 的优势是完全离线、完全可控、导出即用一条曲线、一个坐标轴、几个标签几行代码就能生成 PNG 或 PDF直接贴进文档。对于我只要一张能说明训练收敛情况的图这个需求matplotlib 是性价比最高的选择。选型上还有个小决策用plt.plot的面向过程写法还是用fig, ax plt.subplots()的面向对象写法新手教程多用前者但只要你需要画多子图、或者要把图嵌进更大的画布面向对象写法更清晰。我下面统一用subplots风格因为它扩展性更好后面加验证集曲线、加平滑处理都不用重构。2.3 一张合格的 loss 曲线应该包含什么在动手写代码前先明确合格的标准否则画出来自己都不知道缺什么。我的检查清单是这样的横轴epoch 或 step 编号必须有轴标签单位明确。纵轴loss 值同样要有标签。如果训练集和验证集 loss 量纲差异大考虑是否要分开或做归一化。曲线至少一条训练 loss理想情况下加一条验证 loss 做对比。图例多条曲线时必须有且位置不能遮挡数据。标题说明这是哪个模型、哪次实验的曲线。网格轻量网格线能帮助读数但别太密。这六点看着琐碎但缺任何一项图的可读性都会打折。尤其是图例和轴标签很多人图省事不写结果过两周自己回看都分不清哪条是训练哪条是验证。3. 核心细节解析从 loss 收集到曲线成型的每一步3.1 训练循环中的 loss 收集写法先看最核心的收集逻辑。下面这段是一个最小可用的训练循环骨架重点看train_losses和val_losses两个列表怎么用import torch import torch.nn as nn from torch.utils.data import DataLoader train_losses [] val_losses [] for epoch in range(num_epochs): model.train() running_loss 0.0 for batch_idx, (inputs, targets) in enumerate(train_loader): optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, targets) loss.backward() optimizer.step() running_loss loss.item() * inputs.size(0) epoch_train_loss running_loss / len(train_loader.dataset) train_losses.append(epoch_train_loss) model.eval() running_val_loss 0.0 with torch.no_grad(): for inputs, targets in val_loader: outputs model(inputs) loss criterion(outputs, targets) running_val_loss loss.item() * inputs.size(0) epoch_val_loss running_val_loss / len(val_loader.dataset) val_losses.append(epoch_val_loss) print(fEpoch {epoch1}: train_loss{epoch_train_loss:.4f}, val_loss{epoch_val_loss:.4f})这里有几个关键点值得展开。第一loss.item()一定要调用它把只含一个元素的张量转成 Python 浮点数避免把计算图一起存进列表导致显存泄漏——这是新手最常犯的隐性错误训练久了显存莫名其妙爆掉往往就是忘了.item()。第二running_loss loss.item() * inputs.size(0)这种按样本数加权的写法比直接累加 batch loss 再除以 batch 数更准确因为最后一个 batch 的样本数可能不满。第三验证阶段务必model.eval()加torch.no_grad()前者影响 BatchNorm 和 Dropout 的行为后者省显存两者缺一不可。3.2 单条曲线的绘制与样式控制数据齐了画图本身其实很短。下面是最小绘图代码import matplotlib.pyplot as plt plt.figure(figsize(8, 5)) plt.plot(range(1, len(train_losses) 1), train_losses, labelTrain Loss, color#1f77b4, linewidth1.8) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Training Loss Curve) plt.legend() plt.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.savefig(loss_curve.png, dpi150) plt.show()figsize控制画布比例8:5 是比较通用的宽屏比例适合放进文档。linewidth我一般设 1.5 到 2 之间太细看不清太粗显得笨重。dpi150是导出清晰度的关键默认 100 在屏幕上够用但打印或放大就会糊150 到 200 是比较稳妥的区间。tight_layout()用来防止轴标签被裁掉这个函数在保存前调用能省掉很多图边缘文字被切的麻烦。3.3 训练集与验证集双曲线的对比画法单条曲线只能看降没降双曲线才能看过没过拟合。把两条画在同一张图上核心是颜色区分和图例plt.figure(figsize(8, 5)) epochs range(1, len(train_losses) 1) plt.plot(epochs, train_losses, labelTrain Loss, color#1f77b4, linewidth1.8) plt.plot(epochs, val_losses, labelVal Loss, color#d62728, linewidth1.8) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Train vs Validation Loss) plt.legend(locbest) plt.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.savefig(train_val_loss.png, dpi150) plt.show()判断过拟合的经典信号是训练 loss 持续下降验证 loss 先降后升两条曲线在某个 epoch 后开始分叉。这时候图例的位置就很重要locbest让 matplotlib 自动找空白区域放图例避免压住曲线。如果两条曲线量纲差太多比如训练 loss 是 0.01 级别验证 loss 是 1.0 级别可以考虑用双 y 轴但我不太推荐——双轴容易误导读者对差距的感知更稳妥的做法是分开画两个子图。3.4 中文标签与字体问题的处理如果你的标题或轴标签要写中文直接写会得到一堆方块。原因是 matplotlib 默认字体不含中文字形。解决办法是显式指定一个系统里存在的中文字体import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, DejaVu Sans] matplotlib.rcParams[axes.unicode_minus] False第一行按顺序尝试字体找不到就往后fallback第二行解决负号显示成方块的问题因为很多中文字体里没有正确的负号字形。这里要注意SimHei在部分系统上不一定装了所以给一个 fallback 列表比写死一个字体更稳。如果实在搞不定字体最省事的办法是标签全用英文反而不会有兼容问题。4. 实操过程从零跑通一张可发布的 loss 曲线4.1 环境准备与依赖确认动手前先确认环境。需要的东西不多torch、matplotlib如果要处理数据再加numpy。版本上没有特别苛刻的要求matplotlib3.x 以上都支持本文所有写法。检查方式很简单python -c import torch, matplotlib; print(torch.__version__, matplotlib.__version__)能打印出版本号就说明装好了。如果是在无图形界面的服务器上跑plt.show()会报错或卡住这时候要么改用matplotlib.use(Agg)后端只保存不显示要么干脆去掉show()只保留savefig()。这个细节在本地开发时不会暴露一上服务器就翻车提前知道能省不少时间。4.2 完整可复现脚本把前面的片段拼成一个能直接跑的完整脚本。为了不依赖真实数据集这里用一个极简的线性回归任务模拟训练过程loss 收集和绘图逻辑与真实项目完全一致import torch import torch.nn as nn import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt torch.manual_seed(42) X torch.randn(500, 10) true_w torch.randn(10, 1) y X true_w 0.1 * torch.randn(500, 1) model nn.Sequential(nn.Linear(10, 32), nn.ReLU(), nn.Linear(32, 1)) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.01) train_losses, val_losses [], [] num_epochs 50 for epoch in range(num_epochs): model.train() optimizer.zero_grad() pred model(X) loss criterion(pred, y) loss.backward() optimizer.step() train_losses.append(loss.item()) model.eval() with torch.no_grad(): val_pred model(X) val_loss criterion(val_pred, y) val_losses.append(val_loss.item()) plt.figure(figsize(8, 5)) epochs range(1, num_epochs 1) plt.plot(epochs, train_losses, labelTrain Loss, color#1f77b4, linewidth1.8) plt.plot(epochs, val_losses, labelVal Loss, color#d62728, linewidth1.8, linestyle--) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Loss Curve Demo) plt.legend(locbest) plt.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.savefig(loss_curve_demo.png, dpi150) print(saved:, train_losses[-1], val_losses[-1])跑完会在当前目录生成loss_curve_demo.png。这里验证集和训练集用的是同一批数据只是为了演示双曲线画法真实项目里当然要分开。注意验证曲线我用了linestyle--虚线这是个小技巧颜色加线型双重区分即使打印成黑白也能分清两条线。4.3 曲线平滑让趋势更易读当 batch 级 loss 抖动很大时原始曲线像一团毛线趋势反而看不清。这时候可以做滑动平均平滑。核心思路是用一个窗口在序列上滑动取均值def smooth(values, weight0.9): smoothed [] last values[0] for v in values: last last * weight (1 - weight) * v smoothed.append(last) return smoothed这是指数移动平均EMA的写法weight越接近 1 越平滑但滞后也越明显。我一般取 0.9 左右既能压住抖动又不至于把真实拐点抹平。画图时把原始曲线用浅色细线、平滑曲线用深色粗线叠在一起读者既能看到原始波动又能抓住趋势这个组合在论文里很常见。4.4 多组实验对比的画法调参时经常要对比不同学习率或不同结构的 loss 曲线。最直接的做法是把多组结果画在同一张图上用不同颜色区分configs { lr0.001: losses_lr1, lr0.01: losses_lr2, lr0.1: losses_lr3, } plt.figure(figsize(9, 5)) for name, curve in configs.items(): plt.plot(range(1, len(curve) 1), curve, labelname, linewidth1.6) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Loss under Different Learning Rates) plt.legend() plt.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.savefig(lr_compare.png, dpi150)这里有个经验曲线超过四条就别挤一张图了颜色会开始难分辨改用子图plt.subplots(2, 2)更清晰。另外不同实验的 epoch 数可能不同range要按各自长度生成别硬套同一个范围否则会报长度不匹配。5. 常见问题与排查技巧实录5.1 图画出来是空白或只有一条直线最常见的原因是数据没进列表。检查两点append是不是写在了循环外面那样只会存最后一个值或者loss.item()是不是漏了导致存进去的是张量。还有一种情况是plt.plot的 x 和 y 长度不一致matplotlib 会静默画错或报错。排查方法很简单画图前先print(len(train_losses))确认长度等于 epoch 数。5.2 中文显示成方块前面 3.4 节已经给了方案这里补充一个排查顺序先确认系统里到底有没有那个字体matplotlib.font_manager.findfont(SimHei)能查再确认rcParams设置是不是在import pyplot之后被覆盖了。有时候是代码里别处又改回了默认字体导致设置失效。5.3 保存的图边缘文字被裁savefig默认按画布大小裁切轴标签或标题超出边界就被切掉。解决办法是在savefig里加bbox_inchestight它会自动扩展边界包住所有元素。这个参数比tight_layout()更彻底两者可以一起用。5.4 显存越训越大如果 loss 列表里存的是张量而不是浮点数每个张量都带着计算图几十个 epoch 下来显存就被吃光了。确认每个append进去的都是loss.item()的结果。这个坑非常隐蔽因为训练能跑、图也能画只是显存悄悄涨。5.5 常见问题速查表现象可能原因解决方向图为空白列表为空或长度不匹配打印列表长度核对只有一条直线只存了最后一个值检查 append 位置中文变方块字体未指定设置 sans-serif 字体列表边缘文字被切边界未扩展加 bbox_inchestight显存持续增长存了张量未转浮点统一用 loss.item()曲线抖动看不清batch 级噪声大加 EMA 平滑或改画 epoch 均值多曲线颜色难分曲线过多改子图或加线型区分5.6 几个我踩过的实操心得第一个心得先存数据再画图别边训边画。有人喜欢在每个 epoch 结束时调用一次plt.plot刷新这在交互环境里看着爽但会拖慢训练而且容易因为图形对象没清理导致内存泄漏。正确做法是训练全程只收集训练结束后一次性画。第二个心得给图起个能自解释的文件名。loss_curve.png这种名字过一周就分不清是哪次实验了带上模型名、学习率、日期比如resnet_lr0.01_20240601.png回头找图能省大量时间。第三个心得验证 loss 的横轴要和训练 loss 对齐。如果训练按 step 记录、验证按 epoch 记录直接画在一张图上横轴含义就乱了。要么都按 epoch要么把 step 级数据按 epoch 聚合后再画。第四个心得导出格式按用途选。放网页或文档用 PNG放论文或需要无损缩放用 PDF 或 SVGsavefig会根据扩展名自动选格式不用额外配置。6. 让曲线真正服务于训练决策画图不是终点看懂图才是。一条健康的训练 loss 曲线早期应该快速下降中期趋于平缓末期在一个小范围内波动。如果它长时间不降先别急着改模型检查学习率是不是太大导致震荡、或者太小导致几乎不动。如果训练 loss 降但验证 loss 升那是过拟合的典型信号考虑加正则、加数据或早停。如果两条曲线都剧烈抖动多半是 batch size 太小或学习率偏高。我个人的习惯是每次实验都固定导出两张图一张原始 batch 级曲线看稳定性一张 epoch 级平滑曲线看趋势文件名带上关键超参。时间久了这些图本身就是一份实验记录比翻日志高效得多。这套流程不复杂但坚持下来你对模型行为的判断会越来越准调参也不再是盲目试错。
阅读完成 · 觉得有帮助?
咨询建站