把训练过程做成前端能实时刷新的可视化

举报
哦啦啦啦啦 发表于 2026/09/29 14:12:45 2026/09/29
【摘要】 先澄清:项目里其实有两条"可视化"读代码时最容易踩的坑是把 legacy 的 visualization.py 当成"前端实时刷新"的实现。实际上它是离线出图;真正支撑前端实时曲线的是另一条 API 链路。两条链路职责不同,拆开讲才看得清。链路一:legacy 离线可视化(visualization.py)visualization.py 461 行,技术栈是 matplotlib + Ag...

先澄清:项目里其实有两条"可视化"

读代码时最容易踩的坑是把 legacy 的 visualization.py 当成"前端实时刷新"的实现。实际上它是离线出图;真正支撑前端实时曲线的是另一条 API 链路。两条链路职责不同,拆开讲才看得清。

链路一:legacy 离线可视化(visualization.py)

visualization.py 461 行,技术栈是 matplotlib + Agg 非交互后端——即"无头环境出图",不弹窗口、直接画 PNG 存盘。它产出的图表覆盖训练复盘的全部需要:

函数 图表
plot_training_loss 步级 loss 细线 + 移动平均粗线 + 学习率双轴
plot_evaluation_results 2×2 四联图(成败柱状/成功率曲线/耗时直方/饼图)
plot_success_rate_comparison 三任务成功率对比 + 加权总分
plot_action_sequence 每关节一条折线,双臂拆上下两半
plot_action_heatmap (T, action_dim) 热力图
save_evaluation_report 汇总 PNG + 文本报告

loss 曲线那幅图值得细看,它把"实验复盘"的体验做得很完整:

if len(losses) > 50:
    window = min(100, len(losses) // 10)
    moving_avg = _moving_average(losses, window)
    ax1.plot(avg_steps, moving_avg, color="#F44336", linewidth=2.0, ...)
...
if lr_history and len(lr_history) == len(losses):
    ax2 = ax1.twinx()
    ax2.plot(steps, lr_history, color="#4CAF50", linewidth=1.5,
             linestyle="--", alpha=0.7, label="Learning Rate")
  • 原始 loss 画细浅线,移动平均画粗红主线——原始曲线噪声大,肉眼很难判断趋势;窗口 min(100, len//10) 让窗口随数据量自适应(数据少时窗口也小,避免过度平滑)。
  • 学习率用 twinx() 画在次坐标轴——训练中"loss 不降"最常被问的问题是"是不是学习率出了问题",把学习率曲线和 loss 叠在一张图上,一眼就能对照"loss 拐点"和"lr 变化"的关系。这是实验工具里性价比极高的设计。

离线链路的价值不在"实时",在"复盘与留存":训练结束后把整套图存盘,形成可回溯的实验记录。它和实时链路是互补关系,不是替代关系。

链路二:真实的前端实时刷新(API + 轮询)

这才是"前端能实时刷新"的真相,由三部分组成。

数据管道(data_loader.py)

CloudRoboDataset 是对 LeRobot 数据集的薄封装,几个决策值得注意:

  • 样本 = 单帧(frame),不是整条 episode——训练时按帧采样更灵活;
  • 子集选择用等距取步长(step = total_frames / num_data),不是随机采样——保证取到的帧均匀覆盖整条轨迹,避免随机采样集中在某段;这对模仿学习的"轨迹分布均匀"很重要;
  • 先增强、后归一化:
if self.image_aug is not None:
    img = self.image_aug(img)     # 先增强(0-255 域)
img = img.float() / 255.0         # 再归一化到 [0,1]

增强在整数像素域做,归一化在增强之后——因为裁剪、抖动、加噪声这些操作对 0-255 的整数域语义更直观,归一化后再做增强会破坏统计分布。顺序错了,增强效果和归一化语义都会变味。

实时采集(训练 API + 回调)

后端训练用 threading.Thread(daemon=True) 在后台跑,指标采集靠 Ultralytics 的回调钩子:

def on_fit_epoch_end(trainer):
    record = {
        "epoch": trainer.epoch + 1,
        "loss": float(loss) if loss else 0,
        "precision": float(metrics.get("metrics/precision(B)", 0)),
        "recall": float(metrics.get("metrics/recall(B)", 0)),
        "mAP50": float(metrics.get("metrics/mAP50(B)", 0)),
        "mAP50_95": float(metrics.get("metrics/mAP50-95(B)", 0)),
    }
    TRAINING_STATUS["metrics_history"].append(record)

model.add_callback("on_fit_epoch_end", on_fit_epoch_end)

关键决策是把指标写进全局状态字典 TRAINING_STATUS,而不是直接推给前端。这样训练线程和 API 线程通过共享状态解耦:训练只负责"写",查询接口只负责"读",谁也不用等谁。

前端轮询

前端 ModelTraining.vue 里 pollTimer = setInterval(fetchStatus, 2000)——每 2 秒拉一次 /api/training/status 和 /api/training/metrics,拿到 metrics_history 后用 ECharts 画 loss / precision / recall / mAP50 / mAP50-95 五条曲线。

轮询 vs WebSocket:现在的取舍是合理的

全项目 grep websocket|SSE|EventSource,代码里没有任何 WebSocket/SSE,只有 blog 里写着"未来可用 WebSocket 推送指标替代轮询"。为什么现在用轮询?

  1. 2 秒间隔对训练场景足够——一个 epoch 通常几十秒,2 秒拉一次已经能给出"实时感",不存在聊天/游戏那种毫秒级诉求;
  2. 实现复杂度差一个量级——轮询 = 一个定时器 + 一个 GET;WebSocket 需要管理连接生命周期、断线重连、心跳,还要防训练线程和推送通道的耦合;
  3. 容错简单——浏览器刷新页面后轮询自动恢复,WebSocket 却要处理"服务端在训练中途重启"的僵死连接。

真实场景里,在数据变化慢、查询成本低的场景,轮询是比推送更工程正确的选择。什么时候该升级?当指标采集频率上升到"每步"级别、页面上需要毫秒级反馈、或者多个客户端要共享同一推送通道时,再上 WebSocket 不迟——而且升级路径是清晰的(回调里 push 而不是 append 到状态字典)。

总结

这条"实时可视化"的真相是:轮询是更朴素、更可靠的实时。离线 matplotlib 出图管复盘留存,API 链路用"回调写全局状态 + 2 秒轮询 + ECharts"管实时展示——两条链路各司其职,没有一处用到复杂的推送协议,却完整覆盖了"训练可视化"这个需求。它提醒我:实时不一定等于推送,先算清"我真需要多快",再决定用什么协议。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。