3、标量可视化:Scalar组件详解、训练曲线绘制、多曲线对比、平滑处理

好,咱们直接进入正题。Scalar 组件,说白了就是 TensorBoard 里最常用的那个看板。你训练模型时,那些 loss、accuracy 怎么变化的?全靠它来展示。

我个人习惯,每次开始一个新实验,第一件事就是打开 Scalar 面板。为什么?因为一眼就能看出模型有没有在学,有没有出问题。比看一堆数字直观多了。

3.1 Scalar 组件到底能干啥?

Scalar 组件,就是用来记录和展示单个数值随时间变化的曲线。比如:

  • 训练 loss 曲线
  • 验证 accuracy 曲线
  • 学习率衰减曲线
  • 梯度范数变化曲线

你想想看,一个模型训练十几个小时,你不可能一直盯着终端。有了 Scalar 曲线,你只需要每隔一段时间扫一眼,就知道模型状态了。

核心要点:Scalar 记录的是「标量」,也就是单个数值。每次记录一个点,连起来就是曲线。

3.2 怎么往 Scalar 里写数据?

在 TensorFlow 里,用 tf.summary.scalar 这个 API。我刚开始用的时候,总搞不清该放哪里。后来发现,最常用的地方就是训练循环里。

看个例子:

import tensorflow as tf

# 创建文件写入器
writer = tf.summary.create_file_writer('logs/train')

# 假设这是你的训练循环
for epoch in range(100):
    loss = train_one_epoch()  # 你的训练函数
    
    with writer.as_default():
        tf.summary.scalar('training_loss', loss, step=epoch)

嗯,这里要注意:step 参数很重要。它决定了曲线横轴的位置。我一般用 epoch 或者 global step。

我的小技巧:如果你记录的是每个 batch 的 loss,建议用 global step 作为 step。这样曲线更精细,能看出每个 batch 的波动。

3.3 训练曲线绘制:从零到一

光写数据还不够,你得知道怎么在 TensorBoard 里看。启动 TensorBoard 的命令很简单:

tensorboard --logdir logs/train

然后打开浏览器,输入 localhost:6006。默认就会显示 Scalar 面板。

你会看到一条曲线。如果 loss 在下降,说明模型在学。如果 loss 震荡剧烈或者不下降,那就有问题了。

我曾经遇到过一个情况:loss 曲线一开始下降很快,然后突然变成一条直线。我以为是模型收敛了,结果发现是学习率设得太小,模型卡在局部最优了。所以,曲线不仅要看趋势,还要看形态。

3.4 多曲线对比:一眼看出谁好谁坏

做实验最怕什么?怕记不清哪个参数组合效果最好。多曲线对比就是来解决这个问题的。

在 TensorBoard 的 Scalar 面板里,你可以同时显示多条曲线。比如:

  • 不同学习率的 loss 曲线
  • 不同优化器的 accuracy 曲线
  • 有正则化和无正则化的对比

怎么实现?很简单,把不同实验的日志写到不同的目录下:

logs/
├── exp_lr_0.001/
├── exp_lr_0.01/
└── exp_lr_0.1/

然后启动 TensorBoard 时指定父目录:

tensorboard --logdir logs

这样,所有实验的曲线都会显示在同一个图表里。你可以用鼠标框选、缩放,还能单独隐藏某条曲线。

避坑指南:我曾经把不同实验的日志都写到了同一个目录下,结果曲线全混在一起,根本分不清谁是谁。所以,一定要为每个实验单独建一个子目录。

3.5 平滑处理:让曲线更「顺眼」

原始的训练曲线,尤其是 batch 级别的 loss,往往锯齿很严重。看起来像心电图一样,根本看不清趋势。

这时候就需要平滑处理。TensorBoard 自带平滑功能,在 Scalar 面板的左侧,有一个「Smoothing」滑块。

默认值是 0.6,你可以调到 0.9 甚至 0.99。数值越大,曲线越平滑,但也会丢失一些细节。

我个人习惯:

  • 看整体趋势时,平滑度调到 0.8-0.9
  • 看细节波动时,平滑度调到 0.2-0.4

平滑的原理其实很简单,就是指数移动平均(EMA)。每个点的值,由当前值和之前的值加权平均得到。

我的经验:平滑只是辅助观察的工具,不要过度依赖。有时候,锯齿本身也包含信息——比如 loss 突然跳高,可能意味着梯度爆炸了。

3.6 实战:一个完整的例子

咱们把上面讲的串起来,写一个完整的训练脚本:

import tensorflow as tf
from tensorflow.keras import layers

# 构建一个简单的模型
model = tf.keras.Sequential([
    layers.Dense(64, activation='relu'),
    layers.Dense(10, activation='softmax')
])

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

# 创建日志目录
train_log_dir = 'logs/fit/train'
val_log_dir = 'logs/fit/val'
train_summary_writer = tf.summary.create_file_writer(train_log_dir)
val_summary_writer = tf.summary.create_file_writer(val_log_dir)

# 模拟训练数据
(x_train, y_train), (x_val, y_val) = tf.keras.datasets.mnist.load_data()
x_train = x_train.reshape(-1, 784).astype('float32') / 255.0
x_val = x_val.reshape(-1, 784).astype('float32') / 255.0

# 训练循环
for epoch in range(10):
    # 训练一个 epoch
    history = model.fit(x_train, y_train, 
                        batch_size=32, 
                        epochs=1, 
                        verbose=0)
    
    # 记录训练指标
    with train_summary_writer.as_default():
        tf.summary.scalar('loss', history.history['loss'][0], step=epoch)
        tf.summary.scalar('accuracy', history.history['accuracy'][0], step=epoch)
    
    # 验证
    val_loss, val_acc = model.evaluate(x_val, y_val, verbose=0)
    
    # 记录验证指标
    with val_summary_writer.as_default():
        tf.summary.scalar('loss', val_loss, step=epoch)
        tf.summary.scalar('accuracy', val_acc, step=epoch)
    
    print(f'Epoch {epoch+1}: train_loss={history.history["loss"][0]:.4f}, val_loss={val_loss:.4f}')

运行后,启动 TensorBoard:

tensorboard --logdir logs/fit

你就能看到训练和验证的 loss、accuracy 曲线了。两条曲线一对比,过拟合还是欠拟合,一目了然。

3.7 常见问题与避坑

问题 原因 解决方法
曲线是空的 日志目录没写对,或者 step 没递增 检查 logdir 路径,确保 step 是递增的整数
曲线突然中断 训练过程中断,或者日志文件损坏 重新训练,或者检查磁盘空间
多条曲线颜色一样 不同实验的标签名相同 给每个实验的 scalar 名称加上前缀,比如 'lr_0.001_loss'
曲线太密看不清 记录的 step 太多 每隔 N 个 step 记录一次,或者使用平滑功能

重要提醒:不要等到训练结束才去看 TensorBoard。我建议每训练 10-20 个 epoch 就刷新一下页面,及时发现问题。我曾经有一次训练了 500 个 epoch,结果第 100 个 epoch 就开始过拟合了,白白浪费了 400 个 epoch 的时间。

好了,Scalar 组件的内容就这些。说白了,它就是帮你把枯燥的数字变成直观的曲线。多练几次,你就能从曲线里读出很多信息。下一章咱们聊聊 Histogram 组件,看看权重和偏置的分布是怎么变化的。