如果我们实现了一个 CNN 网络,在 mnist 上通过两个卷积层完成分类识别。但是在我们调试代码的过程中,其实往往会想要知道我们的网络训练过程中的效果变化,比如 loss 和 accuracy 的变化曲线。
当然,我们可以将训练过程中的数据数据打印出来,但是一个是不够直观,另外一个是没有图形的表现力强。所以本篇笔记介绍了 tensorboard 来完成可视化的操作。
按照我们的习惯,先找一个简单的例子 run 起来,再一步步去学习其中的原理吧。首先我们看一个官方文档的示例:
from torch.utils.tensorboard import SummaryWriterwriter = SummaryWriter()x = range(100)for i in x: writer.add_scalar('y=2x', i * 2, i)writer.close()
执行这一段代码后,tensorboard 会在 './runs' 文件夹下保存训练的日志。 接下来我们仔细的分析一下这个例子,来帮助理解这里的具体执行过程:
首先导入 SummaryWriter,这是一个类,需要实例化出一个对象,这里我们命名为 writer。初始化的参数我们最关心的一个就是 log 文件的地址。如果没有输入就默认为当前文件中的 runs 文件夹,如果没有 runs 文件夹则创建一个。
接下来,我们看到在 for 循环中调用了 writer 的一个方法:add_scalar(),这个函数的参数定义如下:
def add_scalar(self, tag, scalar_value, global_step=None, walltime=None):
我们介绍一下这个方法比较重要的几个参数。这个方法的作用是将我们想要的数据存入 log 文件中,那么这段数据的标签(也就是我们画图时的 title)就是参数 tag,而 scalar_value 就是当前存入的数值(也就是画图时的 y 轴),而 global_step 就是我们在哪一步存入了一次数据(也就是画图时的 x 轴)。
那么在这个循环中,我们可以看到存入的数据标签是 'y=2x',而在第 i 步时存入的数值是 2*i,通过循环相当于将每一步时刻的 y 值都存了进去,也可以理解为将一幅图的每个点坐标存入了 log 文件中。
最后使用 write.close() 终止掉这个对象。
writer = SummaryWriter('tb_mnist')for epoch in range(EPOCH): for step, (b_x, b_y) in enumerate(train_loader): # print(b_x.shape); break if cuda_gpu: b_x = b_x.cuda() b_y = b_y.cuda() output = cnn(b_x) loss = loss_func(output, b_y) optimizer.zero_grad() loss.backward() optimizer.step() if step % 50 == 0: test_output = cnn(test_x) pred_y = torch.max(test_output, 1)[1].data if cuda_gpu: pred_y = pred_y.cpu().numpy() else: pred_y = pred_y.numpy() accuracy = float((pred_y == test_y.data.numpy()).astype(int).sum()) / float(test_y.size(0)) print('Epoch: ', epoch, '| train loss: %.4f' % loss.data, '| test accuracy: %.2f' % accuracy) writer.add_scalar("Train/Accuracy", accuracy, step) writer.add_scalar("Train/Loss", loss.item(), step)
可以看到第一行同样是实例化了一个 writer,其余的都一样,直到下面最后两行代码做了修改。第一个是在每次打印的时候,将对应 step 的 accuracy 添加进了 Train/Accuracy 中;第二个是在这个打印的 for 循环的外面,也就是对每一个 step 都保存了当前的 loss 信息。 和上面一样,我们在命令行中执行 tensorboard 命令,指定好 log 文件的地址,在目标端口中就可以看到如下信息:
可以看到这样的两幅图,左边的 accuracy,右边的是 loss。而且可以看到左边的 accuracy 因为我们是每过 50 个 step 保存一次,所以数据明显有折痕。而 loss 是每个 step 都在打印,所以可以看到非常详细的描述了训练过程中 loss 的变化曲线。 在我们的模型训练过程中,就可以通过 add_scalar() 来描绘一些目标参数的变化过程。
import torchimport torchvisionfrom torch.utils.tensorboard import SummaryWriterfrom torchvision import datasets, transforms# Writer will output to ./runs/ directory by defaultwriter = SummaryWriter()transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])trainset = datasets.MNIST('mnist_train', train=True, download=True, transform=transform)trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)model = torchvision.models.resnet50(False)# Have ResNet model take in grayscale rather than RGBmodel.conv1 = torch.nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)images, labels = next(iter(trainloader))grid = torchvision.utils.make_grid(images)writer.add_image('images', grid, 0)writer.add_graph(model, images)writer.close()
有了前面的例子,我们就可以比较轻松的理解这段代码的意思了。首先数据选择了我们前面用的数据集 mnist,网络结构直接调用了 pytorch 的自带模型 resnet,通过这条命令调取:torchvision.models.resnet50()。 重点是最后一段的代码,grid 是用 torchvision 中集成的工具,将数据集中的一批照片读取进来,按网格状排列。照片的数量我们可以看到,前面 trainloader 设置了 batch_size = 64,也就是说 64 张照片。 然后开始用 writer 去调用对应的方法,图片用 add_image,模型图用 add_graph。接下来让我们去看一下效果:
image 示例
graph 示例
这两张图分别展示了 image 和 graph 的效果,一个是 64 张训练时的照片进行展示,拼接成 8*8 的网格状;一个是 resnet50 的网络结构,这个网络结构还支持继续展开,只需要鼠标在上面双击即可,图中的情况就是我进行了适当的展开后的样子。