6.3 TensorBoard:行军日志 本节摘要:靠终端 print 盯训练,等于靠嗓门传军情。TensorBoard 把损失曲线、参数直方图、多实验对比变成可交互的面板。本节接入训练循环完成三件事:记标量、记分布、对比两次实验,并说清日志记录本身的性能代价怎么控制。 终端打印为什么不够用 后勤第一站。5.1 节的循环每个 epoch print 一次,能用,但三个场景立刻露怯:曲线看不了趋势(数字列表不如折线直观);两次实验没法叠在同一张图上对比(你只能肉眼对两列数字);参数分布随训练的演化完全不可见(权重有没有炸成极端值,print 一个均值看不出来)。 TensorBoard 的解法是"先记录、后查看":训练代码里往日志目录写事件文件,浏览器面板实时读取渲染。
本节摘要:靠终端 print 盯训练,等于靠嗓门传军情。TensorBoard 把损失曲线、参数直方图、多实验对比变成可交互的面板。本节接入训练循环完成三件事:记标量、记分布、对比两次实验,并说清日志记录本身的性能代价怎么控制。
后勤第一站。5.1 节的循环每个 epoch print 一次,能用,但三个场景立刻露怯:曲线看不了趋势(数字列表不如折线直观);两次实验没法叠在同一张图上对比(你只能肉眼对两列数字);参数分布随训练的演化完全不可见(权重有没有炸成极端值,print 一个均值看不出来)。
TensorBoard 的解法是"先记录、后查看":训练代码里往日志目录写事件文件,浏览器面板实时读取渲染。写日志的 API 只有一个入口——SummaryWriter,掌握三个方法就够覆盖九成需求:add_scalar 记标量曲线,add_histogram 记张量分布,两者都靠 tag(标签)组织面板层级。

给 5.1 节的循环装上记录仪。改动只有三处:建 writer、每个 epoch 写两笔标量、结束时关闭:
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset from torch.utils.tensorboard import SummaryWriter torch.manual_seed(42) templates = torch.randn(10, 64) X = torch.cat([templates[d].repeat(80, 1) + 0.3 * torch.randn(80, 64) for d in range(10)]) Y = torch.cat([torch.full((80,), d) for d in range(10)]) loader = DataLoader(TensorDataset(X[:640], Y[:640]), batch_size=64, shuffle=True) loss_fn = nn.CrossEntropyLoss() writer = SummaryWriter() # 默认按当前时间生成实验目录 model = nn.Sequential(nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 10)) opt = torch.optim.SGD(model.parameters(), lr=0.5, momentum=0.9) for epoch in range(15): model.train() ep_loss = 0.0 for xb, yb in loader: opt.zero_grad() loss = loss_fn(model(xb), yb) loss.backward() opt.step() ep_loss += loss.item() model.eval() with torch.no_grad(): val_loss = loss_fn(model(X[640:]), Y[640:]).item() # 两笔标量:训练前缀与验证前缀分开,面板自动分组 writer.add_scalar("loss/train", ep_loss / len(loader), epoch) writer.add_scalar("loss/val", val_loss, epoch) writer.close() print("日志已写入,目录形如 runs/八月29_时间戳/")
输出:
日志已写入,目录形如 runs/八月29_时间戳/
打开面板的命令是命令行的 tensorboard --logdir runs,然后浏览器访问它提示的本机端口(面板是纯本机工具,不涉外网)。此刻 SCALARS 页里 train 与 val 两条曲线并排——训练降验证不降的过拟合、两者一起震荡的学习率问题,在曲线上都是一眼事。
背景:5.2 节争论过"动量到底值多少",这次用日志给出可回看的裁决。两组实验:SGD 纯净版与动量版,各自写日志,目录名直接写实验名。
操作:实验名编码超参(这是命名纪律),跑两组,同面板对比。
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset from torch.utils.tensorboard import SummaryWriter torch.manual_seed(42) templates = torch.randn(10, 64) X = torch.cat([templates[d].repeat(80, 1) + 0.3 * torch.randn(80, 64) for d in range(10)]) Y = torch.cat([torch.full((80,), d) for d in range(10)]) loader = DataLoader(TensorDataset(X[:640], Y[:640]), batch_size=64, shuffle=True) loss_fn = nn.CrossEntropyLoss() def train_with_log(run_name, momentum): torch.manual_seed(0) writer = SummaryWriter(run_name) # 目录名即实验名 model = nn.Sequential(nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 10)) opt = torch.optim.SGD(model.parameters(), lr=0.5, momentum=momentum) for epoch in range(15): model.train() ep_loss = 0.0 for xb, yb in loader: opt.zero_grad() loss = loss_fn(model(xb), yb) loss.backward() opt.step() ep_loss += loss.item() writer.add_scalar("loss/train", ep_loss / len(loader), epoch) writer.close() return run_name print(train_with_log("exp_sgd_plain", 0.0)) print(train_with_log("exp_sgd_momentum", 0.9)) print("两个实验目录就绪,面板里叠加对比曲线")
输出:
exp_sgd_plain exp_sgd_momentum 两个实验目录就绪,面板里叠加对比曲线
结果:面板中两条曲线同轴呈现,动量版的下降斜率肉眼可见地更陡——5.2 节的数字结论变成了随时可回看的视觉证据。
解读:实验名编码超参(exp_sgd_momentum 这种命名)是日志纪律的核心:三个月后翻到一张陡峭曲线,你需要能从名字重建实验配置。更讲究的团队会把配置写成日志目录名的一段固定格式(优化器、学习率、动量、日期)。这套习惯的成本是打字,收益是每一次实验都成为资产而不是消耗品。
变式:在循环里加一行 writer.add_histogram("weights/fc1", model[0].weight, epoch),翻到 HISTOGRAMS 页看参数分布随训练的演化——参数分布突然出现长尾或双峰,往往是学习率过大或梯度爆炸的前兆,比损失曲线更早报警。
下一节后勤第二站:Profiler 体检训练的时间都花在了哪里。