大纲如下:
- Tensorboard 的简介与安装
- Tensorboard 的基本使用(标量可视化和图像可视化)
- hook 函数与 CAM 可视化
Ok, let’s go!
2. Tensorboard 的简介与安装
首先,什么是 Tensorboard, 这是 TensorFlow 中强大的可视化工具,支持标量,图像,文本,音频,视频和 Embedding 等多种数据可视化。下面是它的一个界面:
我们先看一下它的一个运行机制:
上面就是 Tensorboard 的一个运行机制,先从 python 脚本中记录可视化的数据然后生成 eventfile 文件存储到硬盘, 然后从终端运行 Tensorboard,打开 web 页面, 然后读取存储的 eventfile 在 web 页面上进行数据的可视化。
下面我们通过代码来看看这个流程:
- 我们得编写 python 脚本文件记录数据可视化数据```python import numpy as np from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter(comment=’test_tensorboard’)
for x in range(100):
writer.add_scalar('y=2x', x * 2, x)writer.add_scalar('y=pow(2, x)', 2 ** x, x)writer.add_scalars('data/scalar_group', {"xsinx": x * np.sin(x),"xcosx": x * np.cos(x),"arctanx": np.arctan(x)}, x)
writer.close()
<br />上面要从 torch.utils.tensorboard 里面导入 SummaryWriter 这个类,这个类是我们最根本的类,用来创建一个 writer,这个 writer 可以记录我们想要可视化的那些数据,这里做的演示很简单,就是可视化几个函数图像。<br />我们运行代码,会发现报错,提示`ModuleNotFoundError: No module named 'tensorboard'`, 所以我们得先安装 Tensorboard, 这个`pip install tensorboard`进行安装即可,一定要安装到你运行的环境里面。然后运行,依然会报错,提示`ModuleNotFoundError: No module named 'past'`, 这个注意, 我们得`pip intall future`, 这里千万不要用 past。 这时候我们会发现当前文件夹下有个 runs:<br /><br />下面我们回到终端当中, 然后输入 tensorboard 读取这个 event files<br /><br />注意, 那个`--logdir=`后面不能加引号,上面的会提示找不到 eventsfile, 所以应该`tensorboard --logdir=./PytorchLearning/runs`, 这样我们点击下面的那个链接就可以看到我们创建的可视化事件了。 下面一个图介绍 Tensorboard 界面:<br />这就是 Tensorboard 的安装和简介的东西了。 下面我们看看这个东西到底应该如何使用了。<a name="e1bbff34"></a>## 3. Tensorboard 的基本使用Tensorbord 的基本使用包括准确率和损失的可视化,参数数据的分布及参数梯度的可视化等。 下面我们一一来学习。但是在正式学习之前,我们得先了解一些基础知识:<a name="5658f234"></a>### 3.1 SummaryWriter这个类的功能是提供创建 event file 的高级接口。<br />- log_dir: event file 输出文件夹, 如果不设置的话,就会创建一个 runs, 通常自己设置- comment:不指定 log_dir 时, 文件夹后缀- filename_suffix: event file 文件名后缀我们可以从代码里面看一下, 先看一下不用 log_dir 的效果:<br />下面我们指定 load_dir,就会发现此时 comment 就不起作用了:<br />上面就是如何创建一个 event file, 那么我们再看看一些方法:1. add_scaler()/add_scalers()<br />功能: 记录标量<br /><br />- tag 表示图像的标签名, 图的唯一标识,就是图的标题- scaler_value 表示要记录的标量,可以理解为 y 轴,- global_step 表示 x 轴。注意这个用起来的一个局限性就是它只能画一条线,但是往往模型训练的时候想监控训练集和验证集的曲线的对比情况,那时候这个不能使了。可以用 add_scalers(), 这里面两个参数:- main_tag: 该图的标签- tag_scaler_dict: key 是变量的 tag(类似于每条曲线的标签), value 是变量的值(等用于上面的 scaler_value, 只不过是可以多个线)这两个的使用我们前面的那个例子:```pythonwriter.add_scalar('y=2x', x * 2, x)writer.add_scalar('y=pow(2, x)', 2 ** x, x)writer.add_scalars('data/scalar_group', {"xsinx": x * np.sin(x),"xcosx": x * np.cos(x),"arctanx": np.arctan(x)}, x)
我们可以看一下这个的结果:
- add_histogram()
功能: 统计直方图与多分位数直线图, 这对参数的分布以及梯度的分布非常有用
tag 表示图像的标签名,图的唯一标识; values 表示要统计的参数, global_step 表示y 轴, bins 表示取直方图的 bins。 下面从代码中看看:
下面看看 Tensorboard 中的结果:
并且 Tensorboard 还能查看多分位折线图:
这个图的作用是观察每个数据方差的一个变化情况。这就是 add_histogram 的一个使用情况了
下面我们就拿人民币二分类的例子,采用上面的两个方法进行模型训练过程中 loss, acc 的一个监控和参数的分布以及参数对应的梯度的一个分布, 就是看看在具体的模型训练中应该怎么用?我们当前学的这个模块是模型训练中的第 5 个模块,所以直接从这里构建我们的 event file 就可以了。
我们首先在第五模块构建一个 SummaryWriter
下面我们看看如何绘制训练过程中的损失,正确率变化曲线以及参数的分布及它们权重的一个分布:
下面还可以绘制验证集上的损失和正确率的曲线变化图像:
我们看看最后的结果:
上面这是模型训练验证过程中学习曲线的可视化。下面我们看看参数的分布直方图,然后说个现象:
好了, 这就是 SummaryWriter 怎么去构建 event file 的一个路径,设置路径,然后就是 add_scaler 和 add_histogram 方法,采用这两个方法就可以监控我们模型训练过程中训练集和验证集 loss 曲线及准确率曲线对比,还有模型参数的数据分布以及每个 epoch 梯度更新的一个分布。 下面我们再学习 Tensorboard 的图像可视化方法。
3.2 Tensorboard 图像可视化方法
我们下面学习 Tensorboard 中图像可视化相关的两个方法:
- add_image()
功能: 记录图像
参数说明:- tag 表示图像的标签名, 图的唯一标识。
- img_tensor 这个要注意, 表示的我们图像数据,但是要注意尺度, 如果我们的图片像素值都是 0-1, 那么会默认在这个基础上 * 255 来可视化,毕竟我们的图片都是 0-255, 如果像素值有大于 1 的,那么机器就以为是 0-255 的范围了,不做任何改动。
- global_step: x 轴
- dataformats: 数据形式, 有 CHW, HWC, HW(灰度图)
下面用一组代码来演示一下这个 add_image() 方法的使用:
writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")fake_img = torch.randn(3, 512, 512)writer.add_image("fake_img", fake_img, 1)time.sleep(1)fake_img = torch.ones(3, 512, 512)time.sleep(1)writer.add_image("fake_img", fake_img, 2)fake_img = torch.ones(3, 512, 512) * 1.1time.sleep(1)writer.add_image("fake_img", fake_img, 3)fake_img = torch.rand(512, 512)writer.add_image("fake_img", fake_img, 4, dataformats="HW")fake_img = torch.rand(512, 512, 3)writer.add_image("fake_img", fake_img, 5, dataformats="HWC")writer.close()
下面看一下效果:
上面的图片中我们就可视化了 5 张图片, 但是发现显示的时候,还得拖动去一次次的显示每一张图片,这样就没法同时对比, 如果我们想从一个界面里面同时显示 5 张图片怎么办? 那就需要下面的方法:
- torchvision.utils.make_grid
功能: 制作网格图像
参数说明:- tensor: 图像数据, B_C_H*W 的形式, B 表示图片个数
- nrow: 行数(列数自动计算), 这个是根据上面指定的 B 来计算列数
- padding: 图像间距(像素单位)
- normalize: 是否将像素值标准化, 这里的标准化可不是平时那种,我们视觉像素一般是 0-255, 所以如果我们的像素值是 0-1 的数了,将这个设置为 True,就会把像素值映射到 0-255 之间,设置为 False,就不变。 所以这里的标准化是针对视觉像素正常范围来讲。
- range:标准化范围,有时候我们可以舍弃掉一些过大或者过小的像素。 比如我们一张图片像素值范围是[-1000,2000], 如果我们指定这里的标准化范围是[-600, 500], 那么就会先把图片像素值规范到这个指定区间,小于 - 600 的统一 - 600 表示,大于 500 的统一 500 表示。然后再进行标准化到 0-255.
- scale_each: 是否单张图维度标准化(因为有的图像可能尺度都不一样,如果设置 False,是从整个大张量上进行标准化)
- pad_value: padding 的像素值(网格线的颜色,通常默认 0)
下面演示一下这个函数的使用,我们用的是人民币二分类实验中的图片进行可视化,代码如下:
writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")split_dir = os.path.join("..", "05数据读取机制", "data", "rmb_split")train_dir = os.path.join(split_dir, "train")transform_compose = transforms.Compose([transforms.Resize((32, 64)), transforms.ToTensor()])train_data = RMBDataset(data_dir=train_dir, transform=transform_compose)train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True)data_batch, label_batch = next(iter(train_loader))img_grid = vutils.make_grid(data_batch, nrow=4, normalize=True, scale_each=True)writer.add_image("input img", img_grid, 0)writer.close()
我们可以看看效果:
add_image 结合 make_grid 的使用方法还是比较实用的,我们可以对我们的数据进行一个基本的审查,快速的检查训练数据样本之间是否有交叉,这些样本的标签是否是正确的。这样审查数据集就比较快了。
3.3 可视化模型计算图
add_graph()
功能: 可视化模型计算图
参数说明:
- model: 模型,必须时 nn.Module
- input_to_model: 输入给模型的数据
- verbose: 是否打印计算图结构信息
这个就先不进行演示了, 因为这个 Pytorch1.2 运行的时候存在 bug,显示不出计算图来。 版本不支持了,所以得装一个 Pytorch1.3 版本,这个最好是新构建虚拟环境,然后下载 Pytorch 和 torchvision 安装包进行安装即可。我们这里只看一下计算图的显示效果:
所以 Tensorboard 的可视化功能还是很强大的,竟然计算图也能可视化。用的时候这么用这个方法:
writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")fake_img = torch.randn(1, 3, 32, 32)lenet = LeNet(classes=2)writer.add_graph(lenet, fake_img)writer.close()
3.4 模型信息总结
当然,计算图一般用不到,我们平时更加关心的反而是数据经过各个模块之后的 shape 变化,所以下面介绍一个使用的工具:
- torchsummary
功能: 查看模型信息,便于调试, 打印模型输入输出的 shape 以及参数总量
参数说明:- model: pytorch 模型
- input_size: 模型输入 size
- batch_size: batch size
- device: “cuda” or “cpu”, 通常选 CPU
这个 torchsummary 需要自己安装 pip install 这个东西。安装完了之后我们导入,然后直接打印即可:
这个工具还是非常实用的。
4. hook 函数与 CAM 可视化
这些属于一些挺高级的一些知识了,作为知识框架内容,这里简单的进行整理, 这一块会分为三块,首先介绍一下什么是 hook 函数,然后 hook 函数怎么使用,最后介绍一个比较强大的可视化方法叫做 CAM。
4.1 hook 函数介绍
Hook 函数机制: 不改变模型的主体,实现额外功能,像一个挂件和挂钩。
为什么需要个这样的东西呢? 这个和 Pytorch 动态计算图的机制有关,我们知道在动态图的运算过程中,运算结束之后,一些中间变量就会被释放掉,比如特征图,非叶子节点的梯度。 但是往往我们想提取这些中间变量,这时候我们就可以用 hook 函数在前向传播,反向传播的时候,挂上一个额外的函数,通过这个额外的函数去获取这些可能被释放掉而我们后面又想用的这些中间变量, 甚至可以通过 hook 函数去改变中间变量的梯度。
Pytorch 提供了四种 hook 函数:
针对 tensor:
- torch.Tensor.register_hook(hook)
后面这三个针对 Module:
- torch.nn.Module.register_forward_hook
- torch.nn.Module.register_forward_pre_hook
- torch.nn.Module.register_backward_hook
4.2 hook 函数与特征图提取
下面我们一一来看一下:
- torch.Tensor.register_hook
这是一个张量的 hook 函数,作用就是注册一个反向传播的 hook 函数,为什么这里强调反向传播呢? 因为我们只有在反向传播的时候某些中间叶子节点的梯度才会被释放掉,才需要我们使用 hook 函数去保留一些中间变量的信息。 这个东西其实就可以理解成一个钩子就可以,我们用这个钩子挂一些函数到计算图上,然后去帮助我们完成一些额外的功能。既然是一个钩子,那么我们看看它上面允许挂什么样的函数:
可以发现这里允许挂的函数只有一个输入参数, 不返回或者返回张量。
这里的重点是看看应该怎么使用这个东西,这里的注册是个什么意思? 看到下面你会发现,与其说是注册,不如通俗点说叫 “挂上”, 下面详细说一说这个钩子到底怎么用: 我们从需求开始,还记得第二篇里面的那个计算图吗?
这个图我们当时说在反向传播结束之后,非叶子节点的梯度是会被释放掉的,也就是这里的 a, b 的梯度,那么我们如果想保留住呢? 在第二篇的时候我们讲了一个方法,叫做 retain_grad(), 这个可以保留住中间节点的梯度,其实这里的 hook 也可以保留住梯度,我们下面可以看看:
那你可能就要说了, 我既然有个 retain_grad() 方法了,我何必再多次一举弄个 hook 函数呢? 这个使用起来还那么麻烦, 当然如果只想保留梯度的话,hook 函数的作用可能不那么明显,但是 hook 函数可不只是能保留梯度那么简单,这个函数的功能就很大了,具体做什么可以自己去写的, 比如我想在反向传播的时候改变叶子节点 w 的梯度, 这个要怎么弄? 依然可以使用 hook 函数,参数都一样,只不过改改具体实现方法即可:
可以看到,通过钩子的方式在计算图上挂函数然后去完成一些功能还是很方便的。 这是针对张量的钩子,那么我们看看针对 Module 的钩子。 - Module.register_forward_hook
功能: 注册 module 的前向传播 hook 函数, 下面是它的 hook 函数的定义方法,也就是我们钩子上要挂的函数是什么样的,毕竟不同的钩子应该挂不同的 hook 函数。
这个钩子允许挂的函数有 3 个输入,module 表示当前网络层, input 表示当前网络层的输入数据, output 表示当前网络层的输出数据。通常使用这个函数在前向传播过程中获取卷积输出的一个特征图。 - Module.register_forward_pre_hook
功能: 注册 module 前向传播前的 hook 函数, 允许挂的函数结构:
因为它是挂在前向传播前的函数,所以这里的接收参数就没有 output 了。 这个功能可以查看我们网络之前的数据。 - Module.register_backward_hook
功能: 注册 module 反向传播的 hook 函数
由于是挂在了反向传播后,所以当前的输入有三个参数,并且后两个是 grad_input 和 grad_out, 当前网络层输入梯度数据和输出梯度数据。
好了,四个 hook 函数已经介绍完毕, 第一个是针对 tensor 的,而后面三个是针对 Module 的, 但是又根据钩子摆放的位置不同分成了前向传播之前,前向传播,反向传播后三个。下面我们先通过代码看看后面三个到底应该怎么用, 这里会通过调试的方式看看钩子函数调用的内容机制,然后再梳理一遍这几个函数的逻辑。
我们还是从需求出发:
假设我们有一张图片,首先会通过两个卷积核提取一些特征,然后这些特征再通过池化得到最后的值,我们知道如果是单纯的前向传播,那么传完了之后我们通过卷积之后的特征图就会被释放掉,那么我们如何进行保留这些特征图并且后期可以用 Tensorboard 进行可视化呢?
这时候我们就可以采用前向传播 hook 函数来获取中间的这个特征图, 看看如何操作:
class Net(nn.Module):def __init__(self):super(Net, self).__init__()self.conv1 = nn.Conv2d(1, 2, 3)self.pool1 = nn.MaxPool2d(2,2)def forward(self, x):x = self.conv1(x)x = self.pool1(x)return xdef forward_hook(module, data_input, data_output):fmap_block.append(data_output)input_block.append(data_input)net = Net()net.conv1.weight[0].detach().fill_(1)net.conv1.weight[1].detach().fill_(2)net.conv1.bias.data.detach().zero_()fmap_block = list()input_block = list()net.conv1.register_forward_hook(forward_hook)fake_img = torch.ones((1, 1, 4, 4))output = net(fake_img)print("output shape: {}\noutput value: {}\n".format(output.shape, output))print("feature maps shape: {}\noutput value: {}\n".format(fmap_block[0].shape, fmap_block[0]))print("input shape: {}\ninput value: {}".format(input_block[0][0].shape, input_block[0]))
我们看一下结果:
我们发现,通过这种方式就可以进行中间特征图的保存与可视化。但是这个机制是怎么运作的呢? 是什么时候调用的我们的钩子函数呢? 下面通过调试的方式看看背后的工作原理。
我们在output = net(fake_img)前面打上断点,然后 debug,步入。我们会进入 Module 的__call__函数, 在这里面会调用前向传播函数:
我们在前向传播这一行再次步入,就跳到了我们自己写的前向传播函数里面:
这里面第一个子模块就是卷积模块,也就是我们想放钩子的地方,我们再次步入, 这一次依然到了 Module 的__call__函数,因为子模块也是继承的 Module 了,那么这里就趁机说一说这个__call__了,因为这里不仅是只完成前向传播了,而且别忘了,我们在第一个子模块是放了一个钩子的, 所以得说一说这个__call__函数实际上不止完成 forward, 看下图:
所以在这里,前向传播之后,我们就获得了中间特征图,但是这一次我们是有一个钩子放这里的,那么获取了中间特征图之后,不会返回,而是去执行我们的钩子函数:
我们在hook_result = hook(self, input, result)这一行再次步入,就会发现跳到了我们自己定义的 hook 函数中来:
这样就完成了中间图的存储。
好了,上面的 hook 函数的运行机制差不多清楚了吧,其实都是在__call__函数中完成的,这也是 Pytorch 代码高级的一个地方了,它实现了一些 hook 机制,提供了一些额外的实现别的功能的一些接口。稍微梳理一下这个逻辑吧, 首先我们在定义网络的时候,这里会调用父类 Module 的init函数对模块进行初始化,当然这里的模块不仅指的最后的大网络,每个小的子模块也是如此,这个初始化的过程中是完成了 8 个参数字典的初始化。在模型调用的时候,其实是在执行 Module 的__call__函数,这个函数其实是完成 4 部分的工作,首先是前向传播之前的 hook 函数,然后才是前向传播的函数, 之后是 forward_hooks 函数, 最后是反向传播的 hooks 函数。这个就是 Pytorch 中 hook 函数的一个运行机制了。
下面我们看看再加上另外两个 hook 函数看看到底是不是这样的一个运行顺序:
class Net(nn.Module):def __init__(self):super(Net, self).__init__()self.conv1 = nn.Conv2d(1, 2, 3)self.pool1 = nn.MaxPool2d(2, 2)def forward(self, x):x = self.conv1(x)x = self.pool1(x)return xdef forward_hook(module, data_input, data_output):fmap_block.append(data_output)input_block.append(data_input)def forward_pre_hook(module, data_input):print("forward_pre_hook input:{}".format(data_input))def backward_hook(module, grad_input, grad_output):print("backward hook input:{}".format(grad_input))print("backward hook output:{}".format(grad_output))net = Net()net.conv1.weight[0].detach().fill_(1)net.conv1.weight[1].detach().fill_(2)net.conv1.bias.data.detach().zero_()fmap_block = list()input_block = list()net.conv1.register_forward_hook(forward_hook)net.conv1.register_forward_pre_hook(forward_pre_hook)net.conv1.register_backward_hook(backward_hook)fake_img = torch.ones((1, 1, 4, 4))output = net(fake_img)loss_fnc = nn.L1Loss()target = torch.randn_like(output)loss = loss_fnc(target, output)loss.backward()
这里加了另外两个 hook 函数, 然后把最后的输出注释掉,我们看看这三个 hook 函数的运行顺序:
好了,上面就基本上把 hook 函数给通了一遍,现在再来回想 hook 函数的作用: 不改变模型主体而可以增加额外的功能,应该知道是什么意思了吧,玄机都在这个__call__函数中。 下面梳理一下子这几个 hook 函数的关系吧:
hook 机制,是在计算图上挂一些钩子,然后钩子上挂一些函数,在不改变模型或者计算图的主体下,实现了一些额外的功能,比如保存一些中间变量等。 主要有四个 hook 函数,其中一个是针对 Tensor 的,这个挂在了反向传播之后,常用来保留中间节点的梯度或者改变一些梯度等。另外三个是针对 Module 的, 根据挂的位置不同分为三个, 有挂在前向传播前的,这个接收的参数没有输出,一般用来查看输入数据的信息,有挂在前向传播之后的,这个接收的参数就是输入和输出, 一般用来存储中间特征图的信息。 还有一个是挂在反向传播后面, 常用来查看梯度信息。 hook 机制的运行原理主要是在 Module 的
__call__函数中, 这里面完成四块功能, 先看看有没有前向传播前的钩子,然后前向传播,然后前向传播后的钩子,然后反向传播钩子。
4.3 CAM 可视化
CAM: 类激活图, class activation map。这个东西的功能就是分析卷积神经网络,当卷积神经网络得到了输出之后,可以分析我们的网络是关注图像的哪些部分而得到的这个结果。通过这个东西就可以分析出我们的网络是否学习到了图片中物体本身的特征信息, 可以看看下面的这个过程图:
网络最后的输出是澳大利亚犬种。 那么我们的网络从图像中看到了什么东西才确定是这一个类呢? 这里就可以通过 CAM 算法进行一个可视化,结果就是下面的图像。 红色的就是网络重点关注的, 在这个结果中看以发现,这个网络重点关注了狗的头部,最后判定是一个这样的犬种。
CAM 的基本思想: 它会对网络的最后一个特征图进行加权求和,就可以得到一个注意力机制,就是卷积神经网络更关注于什么地方。 这里有个比较有趣的小实验:
我们可以发现这个实验中的网络在预测是飞机的时候,其实关注的并不是飞机本身,而是飞机周围的天空, 发现一片蓝色,所以网络就预测是飞机。 预测汽车的时候,如果把汽车进行缩小, 周围出现了大片蓝色,就发现网络把车也预测成了飞机。 而最后一张图片,竟然还预测出了一个船。 这个可能是因为底部的左边是蓝色,右边不是蓝色,所以网络认为这个是个船。 这说明这个网络根本就没有在学习物体本身,而是光关注物体周围的环境了。
通过 Grad-CAM 可视化我们可以分析卷积神经网络学习到的特征是否真的是好的,是否真的在学习物体本身,是否真的在学习飞机本身,而不是因为看到了蓝天才判断是飞机。
关于详细的 CAM 的知识,这里不说了,可以参考CAM 分析与代码实现
5. 总结
这次的整理就到这里了,内容依然是挺多的,但是这次的这些内容好像在实战中并不是太常用,所以也类似于一个知识扩充吧, 简单的梳理一下:
首先我们学习了 Tensorboard 的安装与使用方法, 见识了 Tensorboard 的强大可视化功能,学习了对于标量的可视化,尤其是绘制模型的学习曲线,这个还是非常重要的,因为学习曲线这个东西可以看出模型究竟是过拟合还是欠拟合的问题。 当然参数的分布和梯度可视化也是挺重要的, 当发现参数的梯度非常小的时候,有时候也不能盲目的去判断是梯度消失,或许是因为 loss 本身就不大。 然后学习了图像的可视化方式,add_graph, add_image, 还有 make_grid 等 函数。add_image 结合 make_grid 的使用方法还是比较实用的,可以帮助我们更好的去审查输入数据。
第二块介绍了 hook 机制,这个是 Pytorch 中留给我们扩展功能的一些接口,在不改变网络的主体下额外增加功能。 主要有四种 hook 函数, 并且学习了内部的运行机制。 最后了解了一下 CAM 可视化的东西,可以帮助我们更好的理解网络的输出究竟是学习到了什么而让它做出的这个判断。
下面依然是一张导图把知识拎起来:
