18-训练可视化:日志、指标记录与 VisualDL 入门

概述

训练日志只打印在命令行里,很难长期观察趋势。尤其是训练轮数多、实验配置多时,只看几行输出很容易误判。

VisualDL 是飞桨生态中的可视化分析工具。它可以把训练过程中的 loss、accuracy、参数分布、图像样本等写入日志文件,再通过 Web 面板展示。

常见用途:

  • 观察训练 loss 是否下降。
  • 对比训练集和验证集 accuracy。
  • 查看学习率变化。
  • 查看参数或梯度分布。
  • 检查输入图像是否预处理正确。
  • 对比多个实验。

读完本文,你应该能安装 VisualDL,在训练循环中记录指标,并启动面板查看训练曲线。

VisualDL 的工作方式

官方文档把 VisualDL 使用过程分成"写"和"读"两部分:

text 复制代码
训练脚本写日志
    |
LogWriter 写入 runs 目录
    |
命令行启动 visualdl 服务
    |
浏览器查看曲线和图像

也就是说,VisualDL 不直接改变训练流程,它只是把训练过程中的关键数据记录下来。

安装与启动

安装:

bash 复制代码
python -m pip install visualdl

查看版本:

bash 复制代码
visualdl --version

启动面板:

bash 复制代码
visualdl --logdir ./runs/mlp_experiment --host 0.0.0.0 --port 8040

浏览器打开:

text 复制代码
http://localhost:8040

如果端口被占用,换一个端口:

bash 复制代码
visualdl --logdir ./runs/mlp_experiment --port 8041

LogWriter:训练脚本中的日志写入器

在 Python 中使用:

python 复制代码
from visualdl import LogWriter

writer = LogWriter(logdir="./runs/mlp_experiment")

更推荐使用上下文管理器:

python 复制代码
from visualdl import LogWriter

with LogWriter(logdir="./runs/mlp_experiment") as writer:
    writer.add_scalar(tag="train/loss", value=0.8, step=1)

常见标量写入:

python 复制代码
writer.add_scalar(tag="train/loss", value=train_loss, step=epoch)
writer.add_scalar(tag="val/acc", value=val_acc, step=epoch)

其中:

  • tag:曲线名称。
  • value:记录的数值。
  • step:横轴步数。

记录训练和验证指标

在训练循环中记录:

python 复制代码
with LogWriter(logdir="./runs/mlp_experiment") as writer:
    for epoch in range(num_epochs):
        train_loss, train_acc = train_one_epoch(model, train_loader, optimizer)
        val_loss, val_acc = evaluate(model, val_loader)

        writer.add_scalar("train/loss", train_loss, epoch)
        writer.add_scalar("train/acc", train_acc, epoch)
        writer.add_scalar("val/loss", val_loss, epoch)
        writer.add_scalar("val/acc", val_acc, epoch)

        print(epoch, train_loss, train_acc, val_loss, val_acc)

命名建议:

text 复制代码
train/loss
train/acc
val/loss
val/acc
lr

使用带斜杠的 tag,可以在面板里更清楚地区分训练和验证。

记录每个 batch 还是每个 epoch

两种粒度都可以:

粒度 优点 缺点
batch 更细,能看到短期波动 日志多,曲线噪声大
epoch 简洁,适合观察长期趋势 看不到 batch 内波动

入门建议:

text 复制代码
每个 epoch 记录 train_loss、val_loss、val_acc
需要排查训练不稳定时,再记录 batch loss

batch 记录示例:

python 复制代码
global_step = 0

for epoch in range(num_epochs):
    model.train()
    for batch_x, batch_y in train_loader:
        logits = model(batch_x)
        loss = F.cross_entropy(logits, batch_y)

        loss.backward()
        optimizer.step()
        optimizer.clear_grad()

        writer.add_scalar("batch/loss", float(loss.numpy()), global_step)
        global_step += 1

记录学习率

如果使用学习率调度器,建议记录当前学习率:

python 复制代码
current_lr = optimizer.get_lr()
writer.add_scalar("train/lr", current_lr, epoch)

这能帮助判断:

  • 调度器是否生效。
  • 学习率是否衰减过快。
  • loss 变化是否与学习率变化相关。

尤其使用 StepDecayCosineAnnealingDecay 时,学习率曲线很有价值。

记录参数直方图

VisualDL 可以记录参数分布:

python 复制代码
for name, param in model.named_parameters():
    writer.add_histogram(tag=f"params/{name}", values=param.numpy(), step=epoch)

用途:

  • 查看参数是否异常变大。
  • 观察参数分布是否集中。
  • 排查训练中参数是否出现 NaN

不要每个 batch 都记录所有参数直方图,日志会变大。通常每个 epoch 记录一次即可。

记录输入图像

图像任务中,最容易出现的问题是预处理错误。可以把输入图片写入 VisualDL。

示例思路:

python 复制代码
images, labels = next(iter(train_loader))
writer.add_image(tag="sample/image_0", img=images[0], step=0)

如果输入是 [C, H, W] 格式,确保 VisualDL 接口接收的格式与数据一致。图像记录最重要的目的不是美观,而是确认:

  • 图片是否归一化过度。
  • 通道顺序是否正确。
  • Resize 或 Crop 是否裁错。
  • 标签是否对应。

完整示例:MLP 训练接入 VisualDL

python 复制代码
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
from visualdl import LogWriter


class CircleDataset(paddle.io.Dataset):
    def __init__(self, num_samples, seed):
        super().__init__()
        paddle.seed(seed)
        self.x = paddle.rand([num_samples, 2], dtype="float32") * 4.0 - 2.0
        radius_square = paddle.sum(self.x ** 2, axis=1)
        self.y = (radius_square > 1.0).astype("int64")

    def __len__(self):
        return self.y.shape[0]

    def __getitem__(self, index):
        return self.x[index], self.y[index]


class MLP(nn.Layer):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(2, 32),
            nn.ReLU(),
            nn.Linear(32, 2),
        )

    def forward(self, x):
        return self.net(x)


def accuracy(logits, labels):
    pred = paddle.argmax(logits, axis=1)
    return paddle.mean((pred == labels).astype("float32"))


def train_one_epoch(model, loader, optimizer):
    model.train()
    total_loss = 0.0
    total_acc = 0.0
    count = 0

    for batch_x, batch_y in loader:
        logits = model(batch_x)
        loss = F.cross_entropy(logits, batch_y)
        acc = accuracy(logits, batch_y)

        loss.backward()
        optimizer.step()
        optimizer.clear_grad()

        total_loss += float(loss.numpy())
        total_acc += float(acc.numpy())
        count += 1

    return total_loss / count, total_acc / count


def evaluate(model, loader):
    model.eval()
    total_loss = 0.0
    total_acc = 0.0
    count = 0

    with paddle.no_grad():
        for batch_x, batch_y in loader:
            logits = model(batch_x)
            loss = F.cross_entropy(logits, batch_y)
            acc = accuracy(logits, batch_y)
            total_loss += float(loss.numpy())
            total_acc += float(acc.numpy())
            count += 1

    return total_loss / count, total_acc / count


def main():
    train_loader = paddle.io.DataLoader(CircleDataset(2048, 2026), batch_size=64, shuffle=True)
    val_loader = paddle.io.DataLoader(CircleDataset(512, 2027), batch_size=128, shuffle=False)

    model = MLP()
    optimizer = paddle.optimizer.Adam(learning_rate=0.001, parameters=model.parameters())

    with LogWriter(logdir="./runs/mlp_experiment") as writer:
        for epoch in range(20):
            train_loss, train_acc = train_one_epoch(model, train_loader, optimizer)
            val_loss, val_acc = evaluate(model, val_loader)

            writer.add_scalar("train/loss", train_loss, epoch)
            writer.add_scalar("train/acc", train_acc, epoch)
            writer.add_scalar("val/loss", val_loss, epoch)
            writer.add_scalar("val/acc", val_acc, epoch)
            writer.add_scalar("train/lr", optimizer.get_lr(), epoch)

            for name, param in model.named_parameters():
                writer.add_histogram(f"params/{name}", param.numpy(), epoch)

            print("epoch:", epoch, "train_loss:", train_loss, "val_acc:", val_acc)


if __name__ == "__main__":
    main()

运行训练脚本后启动:

bash 复制代码
visualdl --logdir ./runs/mlp_experiment --host 0.0.0.0 --port 8040

如何阅读可视化曲线

看 loss:

text 复制代码
train/loss 下降,val/loss 下降:正常学习
train/loss 下降,val/loss 上升:可能过拟合
loss 剧烈震荡:学习率可能过大
loss 几乎不变:学习率太小、模型太弱或数据错误

看 accuracy:

text 复制代码
train/acc 和 val/acc 同步提升:正常
train/acc 高,val/acc 低:过拟合
train/acc 低,val/acc 低:欠拟合或训练错误

看学习率:

text 复制代码
确认 scheduler 是否按预期变化
观察 lr 下降后 loss 是否更稳定

多实验对比

建议每次实验使用不同日志目录:

text 复制代码
runs/mlp_adam_lr001
runs/mlp_adam_lr003
runs/mlp_sgd_lr01
runs/mlp_dropout03

启动时可以指定上级目录:

bash 复制代码
visualdl --logdir ./runs --port 8040

这样可以在同一个面板里对比多个实验。

记录实验配置:

python 复制代码
writer.add_text("config", "optimizer=Adam, lr=0.001, hidden_dim=32", step=0)

常见错误:VisualDL 排查清单

错误一:logdir 写错

训练时:

python 复制代码
LogWriter(logdir="./runs/mlp_experiment")

启动时也要指向同一个目录:

bash 复制代码
visualdl --logdir ./runs/mlp_experiment

错误二:step 一直相同

如果所有记录都使用 step=0,曲线不会正常展开。应使用 epoch 或 global_step。

错误三:value 不是可序列化数值

建议写入 Python float:

python 复制代码
writer.add_scalar("train/loss", float(loss.numpy()), step)

错误四:日志太大

不要每个 batch 都记录所有参数直方图。图像和 histogram 记录频率要控制。

建议练习:把训练过程看见

  1. 记录 train/lossval/loss
  2. 记录 train/accval/acc
  3. 记录 optimizer.get_lr()
  4. 对比两个不同学习率实验。
  5. 每个 epoch 记录一次参数直方图。
  6. 故意设置过大学习率,观察 loss 曲线震荡。

总结:可视化让训练过程可诊断

VisualDL 的价值不是让日志更好看,而是让训练过程更容易诊断:

  • 用 Scalar 看 loss、accuracy、learning rate。
  • 用 Histogram 看参数和梯度分布。
  • 用 Image 检查图像输入是否正确。
  • 用多实验目录对比不同配置。
  • 用曲线趋势判断过拟合、欠拟合和学习率问题。

如果只能记住一句话,那就是:

训练可视化的核心价值,是把模型是否正常学习从猜测变成可观察证据。

相关推荐
科技新资讯2 小时前
国内AI影视制作服务商哪家口碑好
运维·人工智能·devops
2501_941601862 小时前
标注员标注徐州话时声调调值标的完全不一样
人工智能·语音识别
weixin_446260852 小时前
超越Top-K:用可解释智能体操作替代黑盒检索
大数据·人工智能·机器学习
东离与糖宝2 小时前
语音Agent全双工误区:WebRTC双向不等于真全双工
人工智能
BIGmustang2 小时前
千帆模型微调及部署
人工智能
神奇霸王龙2 小时前
CC Switch 配置 Claude Desktop+ selltoken 中转 API 实测教程(2026 年 8 月更新)
人工智能·ai·aigc·agent·ai编程·claude
朱涛的自习室2 小时前
从 Prompt 到 Graph:AI 工程的进化史
android·前端·人工智能
程序员cxuan2 小时前
Pi + DeepSeek-v4-Flash,这用着也太爽了。
人工智能·后端·程序员
Lee_jerome2 小时前
python神经网络编程入门(三十)——Transformer 从 RNN 到注意力:模型为什么需要“瞄一眼“
rnn·深度学习·nlp·transformer·attention·注意力机制·序列建模
stormzhangV2 小时前
2026 年 8 月,我的最新 AI 装机单
人工智能·openai·ai编程