1 概述¶
训练过程的可视化在深度学习模型训练中扮演着重要的角色。谷歌为Tensorflow打造可视化工具Tensorboard,而Facebook为PyTorch开发了一款可视化工具,名为Visdom。Visdom十分轻量级,却支持非常丰富的功能,能胜任绝大多数的科学运算可视化任务,其可视化结果如下图所示。
我们可以看到Visdom可以帮助我们展示数据的分布,模型的训练、模型结构、参数分布等,这些对于我们在debug中查找问题来源非常重要。更多的介绍我们可以参考下方的两个链接:
经过本节课的学习,你将收获:
- 如何安装和使用Visdom
- 了解Visdom基本知识
- 使用Visdom进行绘图操作
- 利用Visdom可视化训练过程
准备好了吗?按照以下步骤开始吧!
2 安装和使用Visdom¶
我们可以通过该pip命令来安装visdom pip install visdom
安装后,我们该如何启动Visdom呢?
可以通过下方命令来启动,第一次启动时会下载一些相关文件。初次启动后最好在终端重新输入一次指令看看能否正常启动。
python -m visdom.server # 或直接输入 visdom
nohup python -m visdom.server & # 还可以使用该命令将服务放到后台运行
如果能正常启动,终端将会显示如下信息:
复制http://localhost:8097
到浏览器后,发现此时的界面并没有显示任何信息,如下图所示。
没关系,在随后的内容中,我们将学习如何使用多个Panes(窗格)来填充它,并且这些窗格可以进行缩放、移动、删除等操作。
顶部的按钮含义如下图所示:
- 注意clear操作需双击。
- 在状态为“offline”时,无法保存/删除/清空环境。只能进行过滤筛选操作。
- 点击管理(外观为文件夹的icon)按钮后,弹出以下页面,可保存或删除当前环境的视图内容。
3 Visdom基本知识¶
Visdom可以创建,共享多种数据形式的可视化,包括数值,图像,文本和视频,支持PyTorch,Numpy等接口。Visdom中主要有以下几个重要概念。
3.1 env(环境)¶
Environment 对可视化的区域进行分区,这样使得不同环境的可视化结果相互隔离,互不影响,在使用时如果不指定特定的env,默认将会使用main默认环境.
我们可以通过编程或UI创建新的env。不同用户、不同程序一般使用不同env。
这样做可以让我们通过分享url: http://localhost.com:8097/env/env_name 让其他人访问特定的env。
我们的 envs 默认通过$HOME/.visdom/
加载。也可以将自定义的路径当作命令行参数传入。
不要轻易移除目录下的env_name.json
文件,这将导致相应的环境也会被删除。
可以通过以下代码建立新的environment。
import visdom
vis = visdom.Visdom(env='pytorchenv')
# vis = visdom.Visdom(env=env_name)的作用是构建一个客户端
# env_name 是指定的环境的名称(字符串),也可以指定host,port等其他参数
3.2 pane(窗格)¶
Pane可以理解为用于可视化图表、图片、文本、视频的容器。一个环境里可以使用不同的窗格来可视化或记录某一信息。
我们可以对pane进行拖放,删除,调整大小和销毁等操作。
一个程序既可以使用同一个env中的不同pane,也可以通过指定win
来使用同一个env中的pane。
4 使用Visdom绘图¶
4.1 基本绘制¶
在接下来的示例中,我们将围绕常见的line、image、text等操作进行介绍。
还需要注意的是visdom仅仅支持PyTorch的tensor和numpy的ndarray的数据结构,不支持Python中的int、float等类型,因此在传入前应该确保我们的数据格式是tensor或numpy。
下面我们来运行一下最基础的示例吧~!
import visdom
import numpy as np
vis = visdom.Visdom(env = 'pytorchenv') # 指定使用的环境,若不指定将默认使用main
vis.text('Hello, world!') # 输出文本字符
vis.image(np.zeros((3, 224, 224))) # 输出大小为3*224*224(CxHxW)大小的黑色图片
我们发现原来的界面出现了一个文本和一幅图像
如下图所示,左上角的图标从左到右分别代表“关闭”、“下载”和“刷新”。点击右下角按钮可进行拖拽操作。
除此以外,我们通常会传入win和opt来进行设置。
win
:用于指定pane的名字,若不指定,visdom会自动分配给我们一个新的pane。但是我们一般需要在原始图片上修改。因此建议每次操作都指定win。opts
:用来可视化配置,接收一个字典,常见的option包括title,xlabel,ylabel,width等,用来设置pane的显示格式。append
:在visdom中,每次操作都会覆盖前面的值,但在可视化损失函数时往往需要不断更新数值且不覆盖前面的数值,这时,只需要传入update = 'append'
这个参数来避免覆盖之前的数值即可。
再来尝试一下其他案例吧~!
import torch
import visdom as vis
vis = vis.Visdom(env='pytorchenv')
x = torch.arange(1,30,0.01)
y = torch.sin(x)
vis.line(X=x,Y=y,win='sinx',opts={'title':'y.sin(x)'})
结果如下所示:
import visdom as vis
vis = vis.Visdom(env='pytorchenv')
vis.image(torch.randn(64,64),win='rand1') #可视化一张随机的黑白图片
vis.image(torch.randn(3,64,64),win='rand2') #可视化一张随机的彩色图片
vis.images(torch.randn(36,3,64,64).numpy(), nrow=6, win='rand3', opts={'title':'demo'}) #可视化36张随机彩色图片,每一张6行
结果如下所示:
除了text、line、image、images以外,Visdom还支持以下基本可视化函数:
- vis.image : 图片
- vis.line: 曲线
- vis.images : 图片列表
- vis.text : 抽象HTML 输出文字
- vis.properties : 属性网格
- vis.audio : 音频
- vis.video : 视频
- vis.svg : SVG对象
- vis.matplot : matplotlib图
- vis.save : 序列化状态服务端
上述函数可传入的参数:
- opts.title : 图标题
- win : 窗口名称
- opts.width : 图宽
- opts.height : 图高
- opts.showlegend : 显示图例 (true or false)
- opts.xtype : x轴的类型 ('linear' or 'log')
- opts.xlabel : x轴的标签
- opts.xtick : 显示x轴上的刻度 (boolean)
- opts.xtickmin : 指定x轴上的第一个刻度 (number)
- opts.xtickmax : 指定x轴上的最后一个刻度 (number)
- opts.xtickvals : x轴上刻度的位置(table of numbers)
- opts.xticklabels : 在x轴上标记标签 (table of strings)
- opts.xtickstep : x轴上刻度之间的距离 (number)
- opts.xtickfont :x轴标签的字体 (dict of font information)
- 有关y轴的参数只需将上述x换成y即可
- opts.marginleft : 左边框 (in pixels)
- opts.marginright :右边框 (in pixels)
- opts.margintop : 上边框 (in pixels)
- opts.marginbottom : 下边框 (in pixels)
- opts.lagent=[''] : 显示图标
4.2 其他图表绘制¶
我们在第一步创建的Visdom
的实例vis
支持以下画图函数,这些函数接口由Plotly
所提供。
vis.scatter
: 绘制2D 或 3D 散点图vis.line
: 线形图vis.stem
: 茎状图vis.heatmap
: 热力图vis.bar
: 柱状图vis.histogram
: 直方图vis.boxplot
: 箱线图vis.surf
: 曲面图vis.contour
: 等高线图vis.quiver
: 折线图vis.mesh
: 网格图vis.dual_axis_lines
: 双 y 轴线图
下面将以柱状图的绘制为例。
from visdom import Visdom
import numpy as np
vis = Visdom(env='pytorchenv')
vis.bar(X=np.random.rand(4, 2),
win='test1',
opts=dict(
stacked=False, # 是否堆叠柱形(若为False,效果如下图demo1所示;若为True,效果如下图demo2所示
legend=['A', 'B'], # 图例标签名称
rownames=['top1', 'top5', 'top10', 'top20'], # 列名称
title='demo1', # 图表标题
ylabel='rank-k Error Rate', # y轴名称
xtickmin=0.4, # x轴左端点起始位置
xtickstep=0.4 # 每个柱形间隔距离
))
from visdom import Visdom
import numpy as np
vis = Visdom(env='pytorchenv')
vis.bar(X=np.random.rand(4, 2),
win='test2',
opts=dict(
stacked=True, # 是否堆叠柱形(若为False,效果如下图demoA所示;若为True,效果如下图demoB所示
legend=['A', 'B'], # 图例标签名称
rownames=['top1', 'top5', 'top10', 'top20'], # 列名称
title='demo2', # 图表标题
ylabel='rank-k Error Rate', # y轴名称
xtickmin=0.4, # x轴左端点起始位置
xtickstep=0.4 # 每个柱形间隔距离
))
输出结果如下图所示:
4.3 可视化图片¶
在处理图像数据时,可以使用Visdom对图像进行可视化
import visdom
from PIL import Image
import torchvision.transforms.functional as F
vis = visdom.Visdom(env='pytorchenv')
img = Image.open('img/Lenna.jpg')
img_tensor = F.to_tensor(img) # 将图像转为tensor类型
print(img_tensor.shape) # 输出图片大小,可省略
vis.image(img_tensor, win='photo')
输出结果如下图所示:
看完以上内容,你是否依然摸不着头脑,还是不知该如何运用这些函数与参数?没关系,在接下来的学习中我们会用一些实例帮助大家掌握。
5 利用Visdom可视化训练过程¶
经过上述学习,相信大家已经对Visdom有了一个初步的了解,在接下来的这部分中,我们将通过具体地案例来帮助大家通过Visdom更好地查看损失函数变化。
5.1 绘制实时曲线¶
# 单条曲线绘制
import visdom
vis = visdom.Visdom(env="pytorchenv")
'''起点'''
vis.line([0.], #第一个点的Y坐标
[0.], #第一个点的X坐标
win='train loss', # 窗口名称
opts=dict(title = 'train_loss',xlabel='episodes',ylabel='loss') #图标题、x轴和Y轴标签
) #设置起点
'''模型数据'''
vis.line([1.],[1.], #下一点的Y坐标及X坐标
win='train loss', # 窗口名称 与上个窗口同名表示显示在同一个表格里
update='append') # 添加到上一个点的后面
结果如下图所示:
- 点击右上角的标签按钮,出现详细的属性信息。
- 鼠标悬浮在图片上方,可以进行更多操作,如放大、缩小、下载为png图片等。
- 点击右下角的“edit”,还可对页面进行编辑操作。
# 多条曲线绘制 实际上就是传入y值时为一个向量
vis = visdom.Visdom(env="pytorchenv")
'''起点'''
vis.line([[0.0,0.0]], # Y的起始点
[0.], # X的起始点
win="test loss", #窗口名称
opts=dict(title='test_loss') # 图像标例
)
'''模型数据'''
vis.line([[1.1,1.5]], # Y的下一个点
[1.], # X的下一个点
win="test loss", # 窗口名称
update='append') # 添加到上一个点后面
输出结果如下图所示:
5.2 初识可视化训练过程¶
为方便学习,我们使用自带的MNIST数据进行可视化训练过程的展示。
'''
导入库文件
'''
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import visdom
import numpy as np
'''
构建简单的模型:简单线性层+Relu函数的多层感知机
'''
class MLP(nn.Module):
def __init__(self):
super(MLP, self).__init__()
self.model = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(inplace=True),
nn.Linear(200, 200),
nn.ReLU(inplace=True),
nn.Linear(200, 10),
nn.ReLU(inplace=True))
def forward(self, x):
x = self.model(x)
return x
'''
设置超参数
'''
batch_size = 128
learning_rate = 0.01
epochs = 10
'''
加载数据
'''
train_loader = torch.utils.data.DataLoader(datasets.MNIST(
'data', #
train=True,
download=True,
transform=transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.1307, ), (0.3081, ))])),
batch_size=batch_size,
shuffle=True)
test_loader = torch.utils.data.DataLoader(datasets.MNIST(
'data',
train=False,
transform=transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.1307, ), (0.3081, ))])),
batch_size=batch_size,
shuffle=True)
# 注意此处初始化visdom类
vis = visdom.Visdom(env="pytorchenv")
# 绘制起点
vis.line([0.], [0.], win="train loss", opts=dict(title='train_loss'))
device = torch.device('cuda:0') # 指定GPU
net = MLP().to(device) # 初始化网络
optimizer = optim.SGD(net.parameters(), lr=learning_rate)
criteon = nn.CrossEntropyLoss().to(device)
for epoch in range(epochs):
for batch_idx, (data, target) in enumerate(train_loader):
data = data.view(-1, 28 * 28)
data, target = data.to(device), target.cuda()
logits = net(data)
loss = criteon(logits, target)
optimizer.zero_grad()
loss.backward()
# print(w1.grad.norm(), w2.grad.norm())
optimizer.step()
if batch_idx % 100 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
test_loss = 0
correct = 0
for data, target in test_loader:
data = data.view(-1, 28 * 28)
data, target = data.to(device), target.cuda()
logits = net(data)
test_loss += criteon(logits, target).item()
pred = logits.argmax(dim=1)
correct += pred.eq(target).float().sum().item()
test_loss /= len(test_loader.dataset)
# 绘制epoch以及对应的测试集损失loss
vis.line([test_loss], [epoch], win="train loss", update='append') # win是必须的
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset), correct / len(test_loader.dataset)))
得到输出如下图所示:
以上内容只是Visdom工具的初步知识,更多有意思的操作等待大家在实际中探索~
创建日期: November 30, 2023