简介:Visdom 是一款面向深度学习和 PyTorch 实验的可视化工具,这份 visdom-master.zip 源码包适合希望理解其内部机制、进行二次开发或将其嵌入训练流程的算法工程师与研究人员。压缩包共 45 个文件,大小约 715KB,内部以 Python 后端代码(7 个 py)和 JavaScript 交互界面(12 个 js)为主体,辅以 CSS 样式、HTML 页面、Markdown 文档以及配置文件,目录结构清晰,便于按模块学习与复用。已有 2200 人学习下载。通过学习源码,读者能掌握 Visdom 的窗口创建、事件回调、RESTful API 通信等核心机制,理解服务器与浏览器之间的实时数据交换,并可将损失曲线、准确率、图像、文本等多种数据类型动态展示在 PyTorch 训练过程中。借助示例脚本和构建配置,还能自定义可视化面板、扩展实验管理功能,或部署到远程服务器实现团队协作,从而提升深度学习实验的分析效率。
1. 可视化工具 visdom:PyTorch 炼丹时最该先跑起来的观察台
训练一个 PyTorch 模型,最烦的不是反向传播报错,而是 loss 曲线“黑匣子化”:你只能等终端里刷出一串数字,然后脑补收敛趋势。visdom 解决的就是这个问题——它是一个浏览器里实时刷新的大屏,把 loss、acc、图像重建结果、特征分布全部变成图表,训练过程中随时瞄一眼就知道模型是活了还是死了。有人在终端里盯数字,有人用 visdom 盯曲线,训练体验完全两个等级。
这个资源是 visdom 的完整源码包,解压后你能直接启动服务,也可以顺手改它的前端页面和 Python 客户端。适合刚接触 PyTorch、想搭一套可视化训练面板的开发者,也适合想把这套服务端/客户端架构拿来改造成自己实验平台的人。我拆解这份源码时最深的感受是:它的核心其实不是画图,而是一套基于 Socket.IO 的实时数据链路,理解了这条链路,画什么图都是顺手的事。
2. 环境搭建与启动:把 visdom 当成一个本地服务跑起来
2.1 服务端加客户端:visdom 的真实架构
很多人以为 visdom 是 matplotlib 那样的绘图库,调用完就出图。其实它是典型的 C/S 架构,拆开目录就能看明白:
visdom-master/ ├── visdom/ # Python 客户端包 │ ├── __init__.py │ ├── Visdom.py # 客户端主类,所有绘图 API 都在这里 │ └── utils.py # JSON 序列化、参数校验工具 ├── server/ │ ├── app.js # Node.js 服务端主程序 │ └── static/ # 前端页面,浏览器渲染就用这里面的 HTML/JS ├── example/ │ ├── basic.py # 官方示例,覆盖 line/scatter/image 等 │ └── mnist.py # 训练循环里嵌可视化的示例 ├── py/ │ ├── __init__.py │ └── visdom.py # 历史版本兼容层 ├── requirements.txt └── setup.pyPython 端负责把你要画的点序列化成 JSON,通过 WebSocket 推给 Node.js 服务端;服务端再把数据广播到所有连接着的浏览器页面。换句话说,训练脚本和服务端是“生产数据”,浏览器只是“消费数据”。所以 visdom 才能做到训练和看图分离:服务器上跑训练,本地浏览器打开 8097 端口就能看曲线。
这也解释了为什么用pip install visdom装完还要单独启动一个服务进程。我用一句话总结这份源码的数据链路:PyTorch tensor -> numpy/list -> JSON -> Socket.IO -> 浏览器渲染。
2.2 安装和启动命令,踩过一遍就顺了
解压后先装依赖。源码包里有 setup.py,直接以可编辑方式安装:
cd visdom-master pip install -e . pip install -r requirements.txt-e是 editable 模式,装完可以随意改visdom/目录下的源码,改动即时生效,不用反复重装。对准备二次开发的场景,这一步能省很多时间。requirements.txt 里的核心依赖是numpy、requests、tornado和websocket-client,前两个管数据格式,后两个管网络传输。
装完启动服务端:
python -m visdom.server --port 8097看到终端输出It's Alive!就说明服务起来了。浏览器访问http://localhost:8097,会看到一个深色背景的空白面板,左边有main这个默认环境名。接着跑一个官方示例验证全链路:
python example/basic.py脚本跑完后,浏览器里会自动出现折线图、散点图和文本卡片。这一步能跑通,说明你本地的 Python 客户端、Node.js 服务端、浏览器三端已经打通。注意example/basic.py里的示例会同时用到viz.line、viz.scatter、viz.text这几个 API,正好把最常用的几条链路全验了一遍。
提示:如果你在服务器上训练,想从本地观察,启动参数要加
--hostname 0.0.0.0,然后用 SSH 隧道转发端口,别把服务裸奔到公网。
3. 核心模块拆解:line/image/text 的实现链路与参数
3.1 客户端连接与探活
visdom 的 Python 客户端全部收敛在Visdom.py的Visdom类里。初始化连接是第一步:
from visdom import Visdom import numpy as np # 连接本地 8097 服务端,指定环境名 viz = Visdom(port=8097, env='train_exp1') # 探活:检查服务端是否真的连上了 if not viz.check_connection(): raise RuntimeError('visdom server is not reachable, run: python -m visdom.server')Visdom(port, env, base_url)三个参数里,port是服务端口,env是环境隔离名(后面再说它的作用),base_url默认是http://localhost。连接成功后,所有绘图调用都通过这个viz对象发出。我一般会在训练脚本开头强制做一次check_connection(),因为它返回布尔值,能第一时间暴露“服务没起”这个最常见的问题,而不是在训练跑到第 10 个 epoch 时才因为画图失败而整段崩掉。
3.2 折线图、散点图、图片和文本的完整参数解读
折线图是训练监控的核心,对应底层是viz.line:
# 单条曲线:每个 epoch 记录一个 loss 值 viz.line( Y=[0.82, 0.61, 0.45], X=[1, 2, 3], win='train_loss', opts={ 'title': 'Training Loss', 'xlabel': 'epoch', 'ylabel': 'loss', 'legend': ['loss_v1'], 'width': 600, 'height': 400, } )win是窗口的唯一标识,同一个名字会复用同一个图表;不传win每次调用都会新建一个窗口,后期会漫天都是图。opts里的legend是图例名,多条曲线时传一个和 Y 行数对齐的列表。width和height控制画布尺寸,单位是像素。
散点图适合看特征分布、PCA 降维后的聚类效果:
# features 是形状为 (N, 2) 的 numpy 数组,代表降维后的坐标 viz.scatter( Y=features, X=labels, win='feature_scatter', opts={ 'title': 'Feature Distribution', 'markersize': 6, 'colormap': 'Viridis', } )这里的X是类别标签,类别不同点会染上不同颜色。colormap支持Viridis、Jet、Portland等 matplotlib 风格色图,选色时注意对比度,训 GAN 看真假样本分布时特别有用。
图片可视化在生成模型里是刚需,重建效果、生成样本都要靠它看:
# batch 是形状为 (B, C, H, W) 的 numpy 数组 viz.images( batch, nrow=4, win='recon_images', opts={'title': 'Reconstruction Samples'} )nrow=4表示每行摆 4 张图。像素值是 0~1 浮点还是 0~255 整型,visdom 会自动归一化。我见过的最典型翻车是:用 PyTorch 的 tensor 直接传进来,不转.cpu().numpy(),结果 TypeError 和 Shape 报错换着来。
文本卡片适合打印每个 epoch 的详细跑分,支持 Markdown 语法:
viz.text( '## Epoch %d\n' 'train loss: **%.4f** \n' 'val acc: **%.2f%%**' % (epoch, train_loss, val_acc), win='run_log', opts={'title': 'Experiment Log'} )同一张文本卡会被后续内容覆盖,相当于一个不断更新的实验日记。调参时把超参数、当前最优 acc、学习率变化都塞进去,翻记录非常方便。
3.3 曲线续画:append 模式与增量更新
训练是迭代的,每个 epoch 都要往同一条曲线加一个点。如果每次调用viz.line传全量历史数据,图照样能画,但数据量一大网络负载就上去了。visdom 提供了增量更新模式:
# 第一次调用,创建新窗口并画初始点 viz.line( Y=[loss.item()], X=[epoch], win='train_loss', opts={ 'title': 'Training Loss', 'xlabel': 'epoch', 'ylabel': 'loss', } ) # 后续每个 epoch,用 update='append' 追加点 viz.line( Y=[loss.item()], X=[epoch], win='train_loss', update='append' )注意两点:追加调用时不用再传opts,窗口样式保留第一次的设定;append的前提是 X 要在已有区间上递增,乱序追加会在前端渲染出乱线。还有一种update='insert'模式,用于在指定 X 位置插入历史点,比如补画某几个缺失的验证点。
4. 训练监控落地:从单次画点到完整记录与离线回放
4.1 用 env 做多组实验隔离
同时跑多个实验是常态,env就是 visdom 里用来隔离不同实验组的机制。前端页面左侧的下拉框对应不同的 env,切换环境时图表也会跟着切。最自然的用法是一个实验一个 env:
# 训练脚本 A viz_a = Visdom(port=8097, env='resnet_lr1e3') # 训练脚本 B viz_b = Visdom(port=8097, env='resnet_lr3e4')两个脚本各画各的,浏览器里来回切换就能对比学习率对收敛的影响。env参数不仅可以写在Visdom()初始化里,还能在每次绘图调用时传env='xxx'临时覆盖。我习惯把env命名为“模型_超参数”格式,比如resnet50_bs128_lr1e3,保证只看 env 名就能分辨出是哪组实验。
官方示例example/mnist.py里还有个值得抄的模式:动态更新viz.text维护的实验标签页,把 env 名、启动时间、当前轮数写进同一张卡,开多个实验时不至于混淆哪条曲线属于哪次运行。
4.2 启动参数与日志回放
训练跑完人走了,曲线想复盘怎么办?visdom 内置了日志记录,启动时指定日志文件:
python -m visdom.server --port 8097 --logfile /tmp/run1.json之后所有绘图操作会实时追加到这个 JSON 文件里。复盘时用回放模式重新启动服务:
python -m visdom.server --port 8097 --replay /tmp/run1.json加--replay后,服务端只读日志文件,不再接收新的写操作。回放的曲线是“逐步重现”的,会像直播一样把当时画图的过程演一遍,很适合用来还原某个实验当时的收敛节奏。日志文件格式是 JSON Lines,每行一个事件对象,包含type、payload、opts等字段。这意味着你可以写脚本解析日志,把 loss 序列提取出来做成对比图,或者自动生成实验报表。
4.3 把可视化嵌进训练主循环
把前面的 API 绑到一起,就是一份可以直接抄进训练脚本的监控代码:
import torch from visdom import Visdom class LiveMonitor: def __init__(self, env='train', port=8097): self.viz = Visdom(port=port, env=env) if not self.viz.check_connection(): raise ConnectionError('visdom server unavailable') self.windows = {} self.viz.text('## Experiment Started\nenv: %s' % env, win='summary', opts={'title': 'Summary'}) def track_metric(self, name, phase, epoch, value): win_key = '%s_%s' % (phase, name) win = self.windows.get(win_key) self.viz.line( X=[epoch], Y=[value], win=win, update='append' if win is not None else None, opts={'title': '%s / %s' % (phase, name), 'xlabel': 'epoch', 'ylabel': name} ) self.windows[win_key] = win_key def track_image(self, name, image_np): self.viz.image(image_np, win='img_%s' % name, opts={'title': name})用法是在每个 epoch 结束的地方调用:
monitor = LiveMonitor(env='unet_road') for epoch in range(30): train_loss = train_one_epoch() val_miou = validate() monitor.track_metric('loss', 'train', epoch, train_loss) monitor.track_metric('miou', 'val', epoch, val_miou)这个LiveMonitor类把win的管理收拢在一个字典里,第一次调用创建窗口,后续自动走append。参数说明就两点:win_key用phase_name拼出来保证 train/val 曲线分窗口;第一次调用时update参数传None,等价于创建新窗口,之后必须传'append'。把这种模块化监控单独放一个文件,训练脚本里只留两行调用,非常干净。
5. 常见问题与排障:起不来、连不上、曲线断了的血泪清单
5.1 服务起不来,卡在 node 下载
现象:执行python -m visdom.server后终端一直停在Downloading状态,几分钟后报错退出,8097 端口没有服务。
原因:visdom 的 Node.js 服务端依赖一份内置的 node 二进制,安装或首次启动时要联网下载。网络环境受限时这个下载会一直失败,而服务端没有优雅降级逻辑。
解决:给系统装好 node,并确保node命令在 PATH 里;visdom 启动时会优先复用系统 node。装好后重新执行启动命令。如果还是不行,手动把 node 二进制下载后放到 visdom 安装目录下的server/node_modules对应位置,再重启服务。
5.2 浏览器打开白屏,一直显示 Connecting
现象:服务端进程活着,终端也没有报错,但浏览器访问http://localhost:8097白屏,页面左下角连接状态一直转圈。
原因:大多是端口冲突——之前有过一个残留的 visdom 进程占着 8097,新进程虽然起来了,但注册不上;或者 torch 版本和 websocket 客户端库版本不匹配,导致 Socket.IO 握手失败。
解决:先查端口占用情况,杀掉旧进程再重启。
lsof -i :8097 kill -9 <pid>然后重新启动服务端,再用python -c "from visdom import Visdom; print(Visdom().check_connection())"验证连通性。返回True再去刷新浏览器。
5.3 GPU tensor 直接画图报 TypeError
现象:训练脚本里viz.line(Y=loss)直接传了torch.Tensor,控制台报TypeError: Object of type Tensor is not JSON serializable。
原因:visdom 客户端只接收 Python 原生类型、numpy 数组或 list,对它来说 GPU tensor 是不认识的类型。这个错误在 PyTorch 训练里简直每天都能碰到。
解决:统一在画图前做一次转换。
# 错误写法 viz.line(Y=loss, X=[epoch], win='loss') # 正确写法 viz.line(Y=[loss.item()], X=[epoch], win='loss')loss.item()是最省事的,数字型标量直接取 Python float;如果是向量或矩阵,记得先.cpu().detach().numpy()。
5.4 曲线不续画,反而越开越多窗口
现象:训练 loop 里反复调用viz.line,图上没有一条完整曲线,而是铺满了十几张只有两三个点的碎图。
原因:没有传win参数,或者win每次都不一样,导致 visdom 认为每次都是新窗口。这是新手最容易犯的使用错误,因为单次画图不传win也能正常出图。
解决:给每一条曲线固定一个全局唯一的字符串窗口名,第一次创建,后续update='append'。记住一条法则:窗口属于曲线,不属于调用点。
5.5 远程机器上的服务,本地浏览器打不开
现象:训练跑在服务器上,服务端也启动了,但本地浏览器访问http://服务器IP:8097超时。
原因:visdom 默认只允许本地连接,服务绑定在 loopback 接口上;即便改成公网监听,防火墙和浏览器跨域策略也可能拦一道。这里别图省事把端口直接暴露公网,安全风险太大。
解决:最稳妥的是 SSH 隧道方式,本地执行:
ssh -L 8097:localhost:8097 user@train_server然后浏览器访问http://localhost:8097。这个方式把加密隧道建立在 SSH 通道里,数据不会裸奔。如果一定要用--hostname 0.0.0.0,配合防火墙白名单,只允许特定 IP 段访问。
6. 二次开发方向:做一个训练指标聚合小面板
拿到这份源码,最能发挥改造空间的是前端页面和客户端封装。我的习惯是不动 Socket.IO 链路,改 Python 客户端那层。基于前面的LiveMonitor,可以再进一步,做一个“多实验指标聚合器”:
class ExperimentBoard: """多实验指标聚合:同一窗口画出不同超参的曲线对比""" def __init__(self, env='board', port=8097): self.viz = Visdom(port=port, env=env) self.runs = {} # run_name -> (epoch, value) def add_point(self, run_name, metric, epoch, value): key = (run_name, metric) self.runs.setdefault(key, ([], [])) self.runs[key][0].append(epoch) self.runs[key][1].append(value) # 每次重画全量曲线,但只画到当前 epoch self.viz.line( Y=np.array(self.runs[key][1]), X=np.array(self.runs[key][0]), win=metric, update='append' if len(self.runs[key][0]) > 1 else None, opts={'title': metric, 'legend': [run_name]} )这段代码的改写逻辑很直白:以metric为窗口,以run_name为图例维度。跑三组学习率实验,每个实验用同一个ExperimentBoard上报 loss,最终在浏览器里一张图上看到三条曲线,横轴 epoch,纵轴 loss。需要对比时一眼看趋势,不需要记日志文件。
再往深走一步,visdom 的server/static目录里是完整的前端源码,Vue 风格的传统 JS 组件结构。你可以加载一份自己的 HTML 页面,在里面写一个参数面板,把location.href.split('env=')[1]解析出来,周报式汇总显示当前实验的最佳 acc。这个改造对源码不动筋骨,只是新增一个静态页面入口。
我见过一个比较聪明的做法:在visdom/Visdom.py的line方法外面包一层装饰器,每次画图自动把数据同时写进一份csv,这样训练结束后拿 pandas 一读就能做论文实验对比表。实现成本极低,却彻底摆脱了对前端回放的依赖。
踩过这么多次坑后,我现在每搭一个新的训练流程,都会强制先跑一遍check_connection()确认链路,再动手写训练循环;每个可视化调用都会提前问自己一句“这个 win 会不会变”。这套习惯帮我少看了很多莫名其妙的空白页,希望帮到你。
本文还有配套的精品资源,点击获取