本章代码:
TensorBoard 是 TensorFlow 中强大的可视化工具,支持标量、文本、图像、音频、视频和 Embedding 等多种数据可视化。
在 PyTorch 中也可以使用 TensorBoard,具体是使用 TensorboardX 来调用 TensorBoard。除了安装 TensorboardX,还要安装 TensorFlow 和 TensorBoard,其中 TensorFlow 和 TensorBoard 需要一致。
TensorBoardX 可视化的流程需要首先编写 Python 代码把需要可视化的数据保存到 event file 文件中,然后再使用 TensorBoardX 读取 event file 展示到网页中。
下面的代码是一个保存 event file 的例子:
复制 import numpy as np
import matplotlib.pyplot as plt
from tensorboardX import SummaryWriter
from common_tools import set_seed
max_epoch = 100
writer = SummaryWriter(comment='test_comment', filename_suffix="test_suffix")
for x in range(max_epoch):
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)}, x)
writer.close()
上面具体保存的数据,我们先不关注,主要关注的是保存 event file 需要用到 SummaryWriter 类,这个类是用于保存数据的最重要的类,执行完后,会在当前文件夹生成一个runs
的文件夹,里面保存的就是数据的 event file。
然后在命令行中输入tensorboard --logdir=lesson5/runs
启动 tensorboard 服务,其中lesson5/runs
是runs
文件夹的路径。然后命令行会显示 tensorboard 的访问地址:
复制 TensorBoard 1.9.0 at http://LAPTOP-DPDNNJSU:6006 (Press CTRL+C to quit)
在浏览器中打开,显示如下:
右上角有一些功能设置
左边的菜单栏如下,点击Show data download links
可以展示每个图的下载按钮,如果一个图中有多个数据,需要选中需要下载的曲线,然后下载,格式有 csv
和json
可选。
runs
显示所有的 event file,可以选择展示某些 event file 的图像,其中正方形按钮是多选,圆形按钮是单选。
optimizer 的属性
PyTorch 中提供了 Optimizer 类,定义如下:
复制 class Optimizer(object):
def __init__(self, params, defaults):
self.defaults = defaults
self.state = defaultdict(dict)
self.param_groups = []
主要有 3 个属性
defaults:优化器的超参数,如 weight_decay,momentum
state:参数的缓存,如 momentum 中需要用到前几次的梯度,就缓存在这个变量中
param_groups:管理的参数组,是一个 list,其中每个元素是字典,包括 momentum、lr、weight_decay、params 等。
_step_count:记录更新 次数,在学习率调整中使用
SummaryWriter
复制 torch.utils.tensorboard.writer.SummaryWriter(log_dir=None, comment='', purge_step=None, max_queue=10, flush_secs=120, filename_suffix='')
功能:提供创建 event file 的高级接口
主要功能:
log_dir:event file 输出文件夹,默认为runs
文件夹
comment:不指定 log_dir 时,runs
文件夹里的子文件夹后缀
filename_suffix:event_file 文件名后缀
代码如下:
复制 log_dir = "./train_log/test_log_dir"
writer = SummaryWriter(log_dir=log_dir, comment='_scalars', filename_suffix="12345678")
# writer = SummaryWriter(comment='_scalars', filename_suffix="12345678")
for x in range(100):
writer.add_scalar('y=pow_2_x', 2 ** x, x)
writer.close()
运行后会生成train_log/test_log_dir
文件夹,里面的 event file 文件名后缀是12345678
。
add_scalar
复制 add_scalar(tag, scalar_value, global_step=None, walltime=None)
功能:记录标量
scalar_value:要记录的标量,y 轴的数据
add_scalars
上面的add_scalar()
只能记录一条曲线的数据。但是我们在实际中可能需要在一张图中同时展示多条曲线,比如在训练模型时,经常需要同时查看训练集和测试集的 loss。这时我们可以使用add_scalars()
方法
复制 add_scalars(main_tag, tag_scalar_dict, global_step=None, walltime=None)
tag_scalar_dict:用字典的形式记录多个曲线。key 是变量的 tag,value 是变量的值
代码如下:
复制 max_epoch = 100
writer = SummaryWriter(comment='test_comment', filename_suffix="test_suffix")
for x in range(max_epoch):
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)}, x)
writer.close()
运行后生成 event file,然后使用 TensorBoard 来查看如下:
add_histogram
复制 add_histogram(tag, values, global_step=None, bins='tensorflow', walltime=None, max_bins=None)
功能:统计直方图与多分位折线图
values:要统计的参数,通常统计权值、偏置或者梯度
下面的代码构造了均匀分布和正态分布,循环生成了 2 次,分别用matplotlib
和 TensorBoard 进行画图。
复制 writer = SummaryWriter(comment='test_comment', filename_suffix="test_suffix")
for x in range(2):
np.random.seed(x)
data_union = np.arange(100)
data_normal = np.random.normal(size=1000)
writer.add_histogram('distribution union', data_union, x)
writer.add_histogram('distribution normal', data_normal, x)
plt.subplot(121).hist(data_union, label="union")
plt.subplot(122).hist(data_normal, label="normal")
plt.legend()
plt.show()
writer.close()
matplotlib
画图显示如下:
正态分布显示如下,每个子图分别对应一个 global_step:
模型指标监控
下面使用 TensorBoard 来监控人民币二分类实验训练过程中的 loss、accuracy、weights 和 gradients 的变化情况。
首先定义一个SummaryWriter
。
复制 writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")
然后在每次训练中记录 loss 和 accuracy 的值
复制 # 记录数据,保存于event file
writer.add_scalars("Loss", {"Train": loss.item()}, iter_count)
writer.add_scalars("Accuracy", {"Train": correct / total}, iter_count)
并且在验证时记录所有验证集样本的 loss 和 accuracy 的均值
复制 # 记录数据,保存于event file
writer.add_scalars("Loss", {"Valid": np.mean(valid_curve)}, iter_count)
writer.add_scalars("Accuracy", {"Valid": correct / total}, iter_count)
并且在每个 epoch 中记录每一层权值以及权值的梯度。
复制 # 每个epoch,记录梯度,权值
for name, param in net.named_parameters():
writer.add_histogram(name + '_grad', param.grad, epoch)
writer.add_histogram(name + '_data', param, epoch)
在训练还没结束时,就可以启动 TensorBoard 可视化,Accuracy 的可视化如下,颜色较深的是训练集的 Accuracy,颜色较浅的是 验证集的样本:
add_image
复制 add_image(tag, img_tensor, global_step=None, walltime=None, dataformats='CHW')
功能:记录图像
dataformats:数据形式,取值有'CHW','HWC','HW'。如果像素值在 [0, 1] 之间,那么默认会乘以 255,放大到 [0, 255] 范围之间。如果有大于 1 的像素值,认为已经是 [0, 255] 范围,那么就不会放大。
代码如下:
复制 writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")
# img 1 random
# 随机噪声的图片
fake_img = torch.randn(3, 512, 512)
writer.add_image("fake_img", fake_img, 1)
time.sleep(1)
# img 2 ones
# 像素值全为 1 的图片,会乘以 255,所以是白色的图片
fake_img = torch.ones(3, 512, 512)
time.sleep(1)
writer.add_image("fake_img", fake_img, 2)
# img 3 1.1
# 像素值全为 1.1 的图片,不会乘以 255,所以是黑色的图片
fake_img = torch.ones(3, 512, 512) * 1.1
time.sleep(1)
writer.add_image("fake_img", fake_img, 3)
# img 4 HW
fake_img = torch.rand(512, 512)
writer.add_image("fake_img", fake_img, 4, dataformats="HW")
# img 5 HWC
fake_img = torch.rand(512, 512, 3)
writer.add_image("fake_img", fake_img, 5, dataformats="HWC")
writer.close()
使用 TensorBoard 可视化如下:
torchvision.utils.make_grid
上面虽然可以通过拖动显示每张图片,但实际中我们希望在网格中同时展示多张图片,可以用到make_grid()
函数。
复制 torchvision.utils.make_grid(tensor: Union[torch.Tensor, List[torch.Tensor]], nrow: int = 8, padding: int = 2, normalize: bool = False, range: Optional[Tuple[int, int]] = None, scale_each: bool = False, pad_value: int = 0)
功能:制作网格图像
tensor:图像数据,$B \times C \times H \times W$的形状
nrow:行数(列数是自动计算的,为:$\frac{B}{nrow}$)
normalize:是否将像素值标准化到 [0, 255] 之间
range:标准化范围,例如原图的像素值范围是 [-1000, 2000],设置 range 为 [-600, 500],那么会把小于 -600 的像素值变为 -600,那么会把大于 500 的像素值变为 500,然后标准化到 [0, 255] 之间
下面的代码是人民币图片的网络可视化,batch_size 设置为 16,nrow 设置为 4,得到 4 行 4 列的网络图像
复制 writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")
split_dir = os.path.join(enviroments.project_dir, "data", "rmb_split")
train_dir = os.path.join(split_dir, "train")
# train_dir = "path to your training data"
# 先把宽高缩放到 [32, 64] 之间,然后使用 toTensor 把 Image 转化为 tensor,并把像素值缩放到 [0, 1] 之间
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)
# img_grid = vutils.make_grid(data_batch, nrow=4, normalize=False, scale_each=False)
writer.add_image("input img", img_grid, 0)
writer.close()
TensorBoard 显示如下:
AlexNet 卷积核与特征图可视化
使用 TensorBoard 可视化 AlexNet 网络的前两层卷积核。其中每一层的卷积核都把输出的维度作为 global_step,包括两种可视化方式:一种是每个 (w, h) 维度作为灰度图,添加一个 c 的维度,形成 (b, c, h, w),其中 b 是 输入的维度;另一种是把整个卷积核 reshape 到 c 是 3 的形状,再进行可视化。详细见如下代码:
复制 writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")
alexnet = models.alexnet(pretrained=True)
# 当前遍历到第几层网络的卷积核了
kernel_num = -1
# 最多显示两层网络的卷积核:第 0 层和第 1 层
vis_max = 1
# 获取网络的每一层
for sub_module in alexnet.modules():
# 判断这一层是否为 2 维卷积层
if isinstance(sub_module, nn.Conv2d):
kernel_num += 1
# 如果当前层大于1,则停止记录权值
if kernel_num > vis_max:
break
# 获取这一层的权值
kernels = sub_module.weight
# 权值的形状是 [c_out, c_int, k_w, k_h]
c_out, c_int, k_w, k_h = tuple(kernels.shape)
# 根据输出的每个维度进行可视化
for o_idx in range(c_out):
# 取出的数据形状是 (c_int, k_w, k_h),对应 BHW; 需要扩展为 (c_int, 1, k_w, k_h),对应 BCHW
kernel_idx = kernels[o_idx, :, :, :].unsqueeze(1) # make_grid需要 BCHW,这里拓展C维度
# 注意 nrow 设置为 c_int,所以行数为 1。在 for 循环中每 添加一个,就会多一个 global_step
kernel_grid = vutils.make_grid(kernel_idx, normalize=True, scale_each=True, nrow=c_int)
writer.add_image('{}_Convlayer_split_in_channel'.format(kernel_num), kernel_grid, global_step=o_idx)
# 因为 channe 为 3 时才能进行可视化,所以这里 reshape
kernel_all = kernels.view(-1, 3, k_h, k_w) #b, 3, h, w
kernel_grid = vutils.make_grid(kernel_all, normalize=True, scale_each=True, nrow=8) # c, h, w
writer.add_image('{}_all'.format(kernel_num), kernel_grid, global_step=kernel_num+1)
print("{}_convlayer shape:{}".format(kernel_num, tuple(kernels.shape)))
writer.close()
使用 TensorBoard 可视化如下。
这是根据输出的维度分批展示第一层卷积核的可视化
复制 writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")
# 数据
path_img = "./lena.png" # your path to image
normMean = [0.49139968, 0.48215827, 0.44653124]
normStd = [0.24703233, 0.24348505, 0.26158768]
norm_transform = transforms.Normalize(normMean, normStd)
img_transforms = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
norm_transform
])
img_pil = Image.open(path_img).convert('RGB')
if img_transforms is not None:
img_tensor = img_transforms(img_pil)
img_tensor.unsqueeze_(0) # chw --> bchw
# 模型
alexnet = models.alexnet(pretrained=True)
# forward
# 由于在定义模型时,网络层通过nn.Sequential() 堆叠,保存在 features 变量中。因此通过 features 获取第一个卷积层
convlayer1 = alexnet.features[0]
# 把图片输入第一个卷积层
fmap_1 = convlayer1(img_tensor)
# 预处理
fmap_1.transpose_(0, 1) # bchw=(1, 64, 55, 55) --> (64, 1, 55, 55)
fmap_1_grid = vutils.make_grid(fmap_1, normalize=True, scale_each=True, nrow=8)
writer.add_image('feature map in conv1', fmap_1_grid, global_step=322)
writer.close()
使用 TensorBoard 可视化如下:
add_graph
复制 add_graph(model, input_to_model=None, verbose=False)
功能:可视化模型计算图
input_to_model:输入给模型的数据,形状为 BCHW
查看 LeNet 的计算图代码如下:
复制 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()
使用 TensorBoard 可视化如下:
torchsummary
模型计算图的可视化还是比较复杂,不够清晰。而torchsummary
能够查看模型的输入和输出的形状,可以更加清楚地输出模型的结构。
复制 torchsummary.summary(model, input_size, batch_size=-1, device="cuda")
功能:查看模型的信息,便于调试
model:pytorch 模型,必须继承自 nn.Module
input_size:模型输入 size,形状为 CHW
batch_size:batch_size,默认为 -1,在展示模型每层输出的形状时显示的 batch_size
查看 LeNet 的模型信息代码如下:
复制 # 模型
lenet = LeNet(classes=2)
print(summary(lenet, (3, 32, 32), device="cpu"))
输出如下:
复制 ----------------------------------------------------------------
Layer (type) Output Shape Param #
================================================================
Conv2d-1 [-1, 6, 28, 28] 456
Conv2d-2 [-1, 16, 10, 10] 2,416
Linear-3 [-1, 120] 48,120
Linear-4 [-1, 84] 10,164
Linear-5 [-1, 2] 170
================================================================
Total params: 61,326
Trainable params: 61,326
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.01
Forward/backward pass size (MB): 0.05
Params size (MB): 0.23
Estimated Total Size (MB): 0.30
----------------------------------------------------------------
None
上述信息分别有模型每层的输出形状,每层的参数数量,总的参数数量,以及模型大小等信息。
我们以第一层为例,第一层卷积核大小是 (6, 3, 5, 5),每个卷积核还有一个偏置,因此$6 \times 3 \times 5 \times 5+6=456$。
参考资料
如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。
我的文章会首发在公众号上,欢迎扫码关注我的公众号张贤同学 。