
简介visdom-master.zip内含Visdom可视化工具的完整源代码适合深度学习研究者和PyTorch用户用于训练过程可视化与实验管理。压缩包共45个文件约715KB主要包含7个Python文件、12个JavaScript文件、7个Markdown文档以及CSS、HTML、配置脚本等分别承担服务端绘图、前端交互、使用说明与安装配置功能。核心的py/visdom目录提供服务器与绘图接口js目录实现各类可视化窗口组件便于理解Visdom的内部机制并进行二次开发。目前已有2200人学习下载。通过研读源码和示例可掌握与PyTorch配合时实时绘制损失曲线、展示图像与文本、管理多窗口布局等技能还可利用回调机制定制实验交互流程提升模型调优效率。1. visdom 到底解决了什么问题训练可视化不再是玄学visdom 是我在 pytorch 训练项目里用得最顺手的可视化工具没有之一。很多人训练时习惯用 matplotlib 画 loss 曲线但真到了 100 个 epoch 的模型上你会发现 plt.show() 会卡住 GPU 流水线而且画出来的图没法实时刷新。visdom 走的是另一条路它在本地起一个网页服务python 端只管把数据推过去浏览器端自动渲染训练和看图完全解耦。拆 visdom-master 这个源码包时我最大的感受是它的结构比想象中简单——核心就是一个 Tornado 服务加一个 socket 客户端。这份资源适合两类人被训练可视化搞得焦头烂额的 pytorch 新手以及想给自己的实验平台扩展自定义可视化面板的熟手。2. 架构设计Tornado 服务端与 Socket.IO 客户端的双向通道2.1 三个关键角色env、win 和 plotly_msgvisdom 的所有操作都建立在三个基本概念上。env 是环境你可以把它理解成一个独立的命名空间比如把一次实验的所有曲线放在 envexp1 下面和另一次实验互不干扰。win 是窗口每个图表、每张图片都是环境里的一个窗口它有唯一的名字。第三个角色是 plotly_msg这是客户端发给服务端的消息类型所有可视化数据的载体。一个典型的 plotly_msg 长这样{ data: [ {x: [1, 2, 3], y: [0.9, 0.5, 0.2], type: scatter, mode: lines} ], win: loss_curve, env: main, opts: { title: 训练损失, xlabel: epoch, ylabel: loss } }msg 里 data 字段是 plotly 格式的绘图数据win 和 env 决定这条消息落到哪个窗口opts 是绘图样式参数。整个 visdom 生态就是围绕这种消息结构展开的。客户端构造消息服务端存储并转发消息前端解析消息并渲染图表。想二次开发第一件事就是吃透这个 JSON 结构。2.2 端到端链路一次 line() 调用走了哪几步我从源码层面把一次 line() 调用的完整链路拆给你们看。首先是 Visdom 类的方法入口它负责把 python 侧的数据整理成 plotly_msg然后通过 socketio 客户端发出去。中间不经过 HTTP 轮询而是走 WebSocket 长连接所以数据能瞬时到达前端。# visdom/client.py 或 visdom/__init__.py 中的核心发送逻辑简化 def _send(self, msg, reasonClient send): # 如果 socket 还没连上先初始化连接 if not self.socket.connected: self._init_socket() # 如果 msg 里没指定 env用客户端默认的 env if env not in msg or msg[env] is None: msg[env] self.env # 通过 socketio 发出事件服务端监听 plotly_msg self.socket.emit(plotly_msg, msg, callbackself._handle_callback)这里 _send 是所有可视化方法的公共出口。line、scatter、text、image 这些方法最后都会调到它。回调函数 _handle_callback 会拿到服务端的处理结果如果服务端返回报错客户端的日志里会打出来这是排查问题的重要抓手。服务端收到消息后做两件事把消息存进内存里的数据结构再把消息广播给所有打开了同一 env 的前端页面。新打开页面的浏览器会先向服务端拉取历史数据所以即使训练脚本先跑、浏览器后打开之前的曲线也能全部恢复出来这个设计对远程实验尤其实用。2.3 目录结构源码包里每个目录是干什么的拿到 visdom-master.zip解压后第一件事是看它的目录骨架。visdom-master/ ├── visdom/ # python 源码主包 │ ├── __init__.py # Visdom 类入口所有可视化 API 都在这 │ ├── client.py # socket 客户端封装连接管理与消息发送 │ ├── server.py # Tornado 服务端消息路由与窗口管理 │ ├── api.py # 后端的 REST API 与静态资源路由 │ ├── static/ # 前端资源目录 │ │ ├── js/ │ │ │ ├── main.js # 前端入口监听 socket 事件并渲染 │ │ │ └── ... │ │ └── css/ │ ├── requirements.txt # 服务端依赖清单 │ ├── setup.py # 打包安装脚本 │ └── README.mdvisdom/static 是整个项目的重头戏它占了源码包体积的大头。服务端会把这个目录作为静态资源直接托管浏览器访问 http://localhost:8097 时加载的就是这里面的文件。注意看 main.js 里对 socket 事件的监听逻辑前端的行为全在这个文件里。setup.py 和 requirements.txt 用来安装依赖跑起来之前一定先看这个文件。2.4 服务端结构WindowStorage 与广播机制服务端并不复杂核心是一个 Tornado WebSocket 处理器加一个内存存储。我扒代码时最关注的是窗口数据是怎么管理的因为这决定了我后面改扩展的难度。# visdom/server.py 中窗口存储的核心逻辑简化 class WindowStorage: def __init__(self): # 两层字典env - win - 最近一次消息 self.data {} def save(self, msg): env msg.get(env, main) win msg.get(win) # 按 env 分组保存win 是窗口唯一标识 self.data.setdefault(env, {})[win] msg def load(self, env, winNone): env_data self.data.get(env, {}) if win is None: # 不指定 win 时返回该 env 下所有窗口的消息 return list(env_data.values()) return env_data.get(win)这个存储结构决定了 visdom 的一个行为特点每个 win 只有最新的消息如果前端刷新页面只能恢复出最近一次的曲线状态而不是全量历史点。所以你要想保留完整训练历史得自己定期用 save 方法做快照这个后面我细说。广播逻辑在 PlotlyHandler 里它拿到消息后调 save 存一份再对所有连着同一个 env 的连接逐个推送。3. 安装与运行从源码包到浏览器出现第一条曲线的完整过程3.1 三种安装方式按你的场景选第一种是直接 pip 安装整包最快但不适合改源码。第二种是从源码包安装进到 visdom-master 目录执行cd visdom-master pip install -r requirements.txt python setup.py install第三种是免安装运行把 visdom 目录直接放到你项目的 site-packages 里或者干脆在项目根目录建个软链接。我从源码包拆项目时更喜欢这种方式因为可以直接改源码里的 client.py改完重启训练脚本就能生效不用反复 install。三种方式的服务启动命令完全一样都是 python -m visdom.server。3.2 启动服务与参数说明启动 visdom 服务只需要一条命令python -m visdom.server --port 8097 --host 0.0.0.0 --base_url /这里几个参数决定你后续能不能顺利连上。port 是服务端口默认 8097host 默认是 localhost如果你在本地调试可以不改但如果是远程服务器上跑训练、本地浏览器看图必须改成 0.0.0.0base_url 是给反向代理场景用的比如你通过 nginx 把某个路径转发到 visdom就要配置这个参数。启动成功后终端会输出一段提示然后浏览器打开 http://localhost:8097 就能看到界面。启动过程中如果卡在 Downloading scripts 这样的进度条上说明前端静态资源没有在本地找到完整副本服务进程会尝试从远端拉取这是后面避坑章节的重点之一这里先按下不表。3.3 跑通第一个 demo验证连接再画图服务起来之后我习惯先写一个最简脚本验证通不通import visdom # 初始化客户端server 和 port 要和启动参数对上 vis visdom.Visdom(serverhttp://localhost, port8097) # check_connection 会实际发一个探活请求返回 True 才说明连接可用 assert vis.check_connection(), visdom 服务连不上检查服务是否启动 # text 是最简单的可视化类型画一段文字到网页上 vis.text(hello visdom, envdemo)check_connection 这个方法很关键它避免了训练脚本跑到一半才发现连接断了的情况。我一般在每个训练脚本开头都加这么一段断言属于常规保命操作。text 方法会在 envdemo 这个环境里创建一个窗口里面显示这行文字。如果这一步能在浏览器里看到窗口说明整条链路——客户端、socket、服务端、前端——全部是通的。3.4 与 pytorch 训练循环集成一个能用的 loss 曲线实测下来visdom 最常见的用法还是画训练损失曲线。下面这个模板我用了很久import visdom vis visdom.Visdom(port8097) # 先用一个空线图占住 win 窗口为后续 append 做准备 vis.line(X[0], Y[0], winloss, envtrain, opts{title: 训练损失, xlabel: epoch, ylabel: loss}) for epoch in range(100): loss train_one_epoch() # 你自己的训练函数 # updateappend 表示把新点追加到已有的 loss 窗口末尾 vis.line(X[epoch 1], Y[loss], winloss, envtrain, updateappend)第一次调用 line 时win 参数指定为 loss这个窗口就固定下来了。之后每次循环都要带上 updateappend并把 win 写成同一个名字。这一步是新手最容易翻车的地方update 不写 append而是每次新建一个窗口浏览器里就会冒出几十个空白窗口曲线永远画不完整。后面避坑章节我会专门展开。3.5 认识界面操作习惯决定了数据会不会丢浏览器打开 visdom 界面后左侧是窗口列表下面是环境切换的下拉框。每个窗口右上角有 Clear 和 Update 两个按钮。Clear 是清空窗口数据Update 是从服务端重新拉取。这里要养成一个习惯实验结束时先用 python 端调用 vis.save 把当前环境的所有窗口配置落盘再关服务。因为服务端内存数据一重启就没了不主动保存的话前一天晚上跑出来的曲线第二天全部消失这件事我踩过一次就再也没忘过。4. 核心模块拆解一次 plotly_msg 从进入到渲染的完整链路4.1 client.pyVisdom 类的方法分发与参数合并Visdom 类的方法很多但套路高度一致。我拿 line 方法做模板讲搞懂这一个scatter、bar、histogram 全都能自己看明白。# visdom/client.py 中 line 方法的实现按源码结构简化 def line(self, X, Y, winNone, envNone, optsNone, updateNone): opts {} if opts is None else opts # 追加模式下必须明确指定 win否则服务端不知道往哪里追加 if update append and win is None: raise ValueError(updateappend 时必须指定 win) # 如果 opts 里有 legend说明要画多条曲线需要把 data 拆开 if legend in opts: data [] legend_list opts.pop(legend) for name, x, y in zip(legend_list, X, Y): data.append({x: x, y: y, type: scatter, mode: lines, name: name}) else: # 单条曲线的情况X、Y 直接作为一组数据 data [{x: X, y: Y, type: scatter, mode: lines}] # 构造消息并发送这一步才是真正落地 self._send({data: data, win: win, env: env, opts: opts, update: update})这里暴露了 visdom 设计上几个值得注意的地方。第一legend 参数会在方法内部被 pop 掉所以调用方的 opts 字典会被原地修改如果你在循环里复用同一个 opts 字典第二次调用时 legend 就已经不存在了。第二update 一共有三种取值——None 表示新建窗口append 表示追加数据insert 表示插入数据——但是要注意insert 主要用于表格类窗口scatter 图不支持。第三win 参数在第一次创建窗口后必须固定否则前后数据对不上。4.2 server.py消息存储、窗口注册与广播服务端的 PlotlyHandler 是整套系统的心脏。看这段代码之前先记住一个前提服务端收到 plotly_msg 后不是直接往前端扔而是先存一份再广播。这是 visdom 支持后打开页面也能看到历史图的根本原因。# visdom/server.py 中的 WebSocket 消息处理器简化 class PlotlyHandler(tornado.websocket.WebSocketHandler): def on_message(self, message): # 前端发来的是 JSON 字符串先解析成字典 msg json.loads(message) # 把消息保存到内存存储按 env - win 组织 self.application.window_storage.save(msg) # 广播给所有订阅了同一 env 的前端连接 env msg.get(env, main) for client in self.application.subscribers.get(env, []): client.write_message(json.dumps(msg))这段代码揭示了 visdom 的性能上限它是单进程内存存储没有落盘、没有数据库。如果单次广播的连接数很大比如几十个浏览器同时开着同一个 env服务端的延迟会明显上升。我们做二次开发时如果遇到这个问题第一反应不该是去优化 Tornado而是减少打开的窗口页面数量。on_message 里还有一个隐藏细节广播时没有过滤 win也就是说同一个 env 下所有窗口的数据会把全部订阅者都刷一遍前端拿到消息后靠自己的 win 判断要不要更新。这个设计在窗口多了以后会有无谓的网络流量属于可以优化的点。4.3 前端渲染socket 事件到 plotly 图表的映射前端目录 visdom/static/js/main.js 里有一段和上面服务端对应的监听逻辑。它通过 socketio 的 on 方法注册了 plotly_msg 事件的回调每次收到消息就调用 Plotly.react 重新画图。// visdom/static/js/main.js 中 socket 事件处理简化 socket.on(plotly_msg, function (msg) { // msg 里的 data 是 plotly 格式的 traces var traces msg.data; var layout buildLayout(msg.opts); // 如果这个窗口还没在页面上先创建一个容器 div var container getOrCreateWindow(msg.env, msg.win); // Plotly.react 会智能地做增量更新而不是全量重绘 Plotly.react(container, traces, layout); });这段代码解释了一个现象为什么 visdom 在数据量大时还能保持流畅。Plotly.react 会对比新旧 traces只更新变化的部分而不是每次把整个图表销毁重建。前端页面还维护着一个 window 列表用来支撑左侧的窗口导航栏。如果你想给 visdom 加一个完全自定义的图表类型最省力的方式是在 main.js 里新增一个逻辑分支判断 msg 里某个自定义字段后调用别的渲染函数。4.4 api.py静态资源托管与 REST 接口最后提一下 api.py它承担了所有非 WebSocket 的 HTTP 请求。浏览器打开 http://localhost:8097 时是它返回了 index.html前端加载 js、css 文件也是走这里的静态资源路由。除此之外它还提供了几个有用的 REST 接口比如 /envs 能列出当前所有环境/win 能拿到某个窗口的 JSON 数据。我有时候排诡异问题会在浏览器地址栏直接访问这些接口用肉眼检查服务端内存里到底存了什么比自己一遍遍翻日志快得多。5. visdom 使用排查五个高频踩坑场景与解决方案5.1 启动卡在 Downloading scripts 不动了现象输入 python -m visdom.server 之后终端一直停在 Downloading scripts 的进度条几分钟都不进下一步。原因visdom 的静态资源比较大服务进程启动时会检查 visdom/static 目录里的前端脚本是否完整。如果是从源码包第一次启动或者 static 目录被清理过它就会启动一个下载流程从远端拉资源。这个下载过程受网络环境影响极大卡住是常事。解决不要干等。我一般是直接结束进程用 python 命令手动执行一次完整下载python -c from visdom.server import download_scripts_and_run; download_scripts_and_run()。如果这个也卡就换一个网络环境或者从其他已装好 visdom 的机器上把 static 目录整个拷贝过来。在服务器上离线跑实验时血泪经验是提前把 static 目录备好。5.2 远程服务器上训练本地浏览器看不到任何图现象训练脚本在远程服务器上跑visdom 服务也起了本地浏览器打开 http://服务器ip:8097 能看到页面但页面里一个窗口都没有。原因visdom 服务默认只绑定 localhost远程访问进来被拒。而且 python 客户端在初始化 Visdom() 时server 参数如果没显式传服务器地址它默认连 http://localhost在服务器环境里会连到自己身上去。解决启动服务时加 --host 0.0.0.0训练脚本里初始化客户端时写 Visdom(serverhttp://服务器ip, port8097)。这里建议用环境变量来管理服务器地址训练脚本不要写死 IP。5.3 loss 曲线出现了一大堆空窗口曲线断断续续现象训练跑完浏览器里开了几十个窗口每个窗口里只有一两个点曲线完全连不起来。原因训练循环里每次调用 vis.line 时没有固定 win 名字。第一次传 winloss1第二次传 winloss2visdom 把每次调用都当成新建窗口处理数据被分散到了不同窗口。这是 visdom 新手最容易踩的坑。解决把 win 名字作为一个常量写在训练脚本顶部后续所有 update 调用都引用这个常量。调用 updateappend 时必须带上同一个 win我在前面的代码示例里已经演示过正确写法。5.4 用 vis.image 显示 numpy 图像结果输出一片花屏现象把训练中的生成图像或分割掩码传给 vis.image网页上显示的是一堆噪点或者颜色完全错乱的色块。原因visdom 要求传入的 numpy 数组是 CHW 格式即 shape 为 (3, H, W)且数值范围是 [0, 255] 的 uint8。很多人习惯性地把 HWC 格式的图像直接传进去导致通道被错误解读。解决传之前先检查 shape 和 dtype。HWC 转 CHW 用 np.transpose(img, (2, 0, 1))float 转 uint8 用 img.astype(np.uint8)。灰度图要转成三通道才能显示正常否则 visdom 会把它当成单通道数据来布色。5.5 opts 里的 xlabel、ylabel 设置了但不生效现象窗口标题显示正常但坐标轴标签始终是默认的 x 和 y。原因不同版本的 plotly.js 对 layout 配置字段的兼容性有差异。老版本 visdom 内置的 plotly 不认识新版 layout 里的部分字段导致 label 静默失效。解决升级 visdom 到新版或者在 opts 里同时传一个完整的 axis 配置比如 opts{xlabel: epoch, ylabel: loss, layoutopts: {xaxis: {title: epoch}, yaxis: {title: loss}}}。我不建议长期停留在老版本因为新版本还修了不少 WebSocket 重连的稳定性问题。6. 二次开发把一个自定义训练看板接进 visdom6.1 先想清楚要扩展什么我接手训练平台时最烦的是每个实验要打开四五个窗口分别看 loss、准确率、学习率。visdom 虽然支持 env 分组但默认没提供一屏多图的聚合视图。二次开发的第一类需求就是把多个 win 组织成一个看板。visdom 本身提供了 layout 布局功能可以用 grid 语法把多个窗口排列在同一个页面里不需要改源码也能实现。# 用 layout 做训练看板把 loss 和 acc 排在同一屏 layout { grid: [ [loss, acc] ], xgap: 20, ygap: 20 } vis visdom.Visdom() vis.layout(layout, envtrain_dashboard)调用 layout 后浏览器里会出现一个两列的面板左边 loss 窗口右边 acc 窗口。这样训练时一眼扫过去就能看到全部关键指标不用来回切 env。这个能力是 visdom 自带但不被文档重点介绍的属于实际跑起来才能发现的实用点。6.2 给 Visdom 类追加一个 log_scalar 方法第二步是封装一个自己的指标记录函数。训练循环里的调用点实在太多不封装的话每个 epoch 要写十行重复代码。我一般会写一个这样的包装函数# 二次开发统一的指标记录入口 def log_scalar(vis, tag, value, step, envtrain): 把标量指标按 tag 分类记录自动创建窗口并追加数据 win f{env}_{tag} vis.line( X[step], Y[value], winwin, envenv, updateappend, opts{title: tag, xlabel: step, ylabel: tag} )这个版本默认 updateappend第一次调用时如果 win 不存在visdom 会自动创建窗口并把这个点作为初始数据之后的调用自动追加完全不需要事前初始化。我后来把 env 从 win 名字里拆出来正是一个教训结果——一开始我把 env 写在函数体外面结果多个实验共用同一个服务图表串到一块去了。从那以后我每次封装记录函数都强制把 env 作为参数传入绝不写死。6.3 验证扩展是否可靠改完代码别急着上大训练写一个几十行的冒烟测试脚本起服务、建窗口、造 5 个虚拟点、断言窗口个数和数据条数符合预期。验证通过后再并进正式训练流程。这套封装入口加冒烟测试加统一 env 管理的习惯让我后面做实验管理省了很多事每次新实验都先从 visdom 看板搭起希望帮到你。本文还有配套的精品资源点击获取