preface
深度学习框架学起来还是 pytorch 更舒服,简洁易懂,个人觉得比 tensorflow 学起来更轻松,并且目前学术界大多用的也都是 pytorch 来复现代码,所以这篇博客就记录一下我学习的过程中的笔记。
unsqueeze
tensor.unsqueeze(tensor, dim=x)
用来给 tensor 升维,也就是加上一个维数为 1 的维度(dim 参数不能超过 tensor 的维度),例如
import torch
a = torch.rand((1, 2, 3))
a.size()
# torch.Size([1, 2, 3])
b = torch.unsqueeze(a, 2)
b.size()
# torch.Size([1, 2, 1, 3])
unsqueeze 这个函数还是挺常用的,例如在处理逻辑回归时输入的点为一维的数据,我们就要用 unsqueeze 来升维使其变成二维的数据。同理,tensor.squeeze(tensor, dim=x)
就是用来降维的,如果不指定 dim 参数的话就默认将所有维数为 1 的维度都删除
import torch
a = torch.rand((1, 1, 2, 3))
a.size()
# torch.Size([1, 1, 2, 3])
b = torch.squeeze(a)
b.size()
# torch.Size([2, 3])
如果指定了 dim 参数的话,则将该 dim 上进行维度删除,如果该 dim 的维数不为 1 的话则 tensor 不变,为 1 的话则将该 dim 删除
import torch
a = torch.rand((1, 1, 2, 3))
a.size()
# torch.Size([1, 1, 2, 3])
b = torch.squeeze(a, 2)
b.size()
# torch.Size([1, 1, 2, 3])
c = torch.squeeze(a, 1)
c.size()
# torch.Size([1, 2, 3])
图像在通过网络的卷积层 forward 之后,出来的维度是 batch_size, chanels, height, width
,有四个维度,所以测试的时候要用 unsqueeze(0)
来将测试用的三维图像提升一个维度(在图像预处理时就已经用 transforms.ToTensor()
来将测试图像变成了 channels, height, width
格式)
并且如果在通过卷积层后还要继续接全连接层的话,一般用 tensor.view(tensor.size(0), -1)
来将卷积过后的所有特征都变成一个特征向量进行全连接
visualization
在用 matplotlib 进行画图可视化时要用 tensor.data.numpy()
将 tensor 转化为 numpy 的 ndarray 数据,不能用代表 Variable 的 tensor 来画图
用了 GPU 训练的数据不能用 matplotlib 进行可视化,要用 data.cpu()
将其转到 CPU 上
在 jupyter notebook 上用 OpenCV 的 cv2.imshow()
会使进程崩溃,可以用 matplotlib 来代替
import cv2
import matplotlib.pyplot as plt
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.show()
因为 OpenCV 储存的图像是 BGR 格式的,而 matplotlib 是 RGB 格式,所以要转换一下颜色空间再显示,否则颜色会有些奇怪
可视化训练集,用 DataLoader 的话可以先将其变成一个迭代器,然后用 next()
方法获取 batch_size 张图片,用 torchvision.utils.make_grid(img, padding=x)
可以将多张图片变成一张,padding 是图片之间的间隔。一般都用 matplotlib 来可视化,注意 DataLoader 中的图如果是 tensor 形式的话,要先转成 numpy 形式,此时它的通道是(channels,imgSize,imgSize),而 matplotlib 中 show 图的通道形式是(imgSize,imgSize,channels),因此还需要用 np.transpose(1, 2, 0)
来转置一下通道
def imshow(img):
npimg = img.numpy()
plt.axis('off')
plt.imshow(np.transpose(npimg, (1, 2, 0)))
dataiter = iter(trainset_loader)
images, labels = dataiter.next()
imshow(torchvision.utils.make_grid(images, padding=10))
用 torchvision.utils.save_image(tensor, fp, format, normalize=True)
可以将一个 batch 的图片给保存下来,因为这里面直接会调用 make_grid
函数,跟上面是一样的效果
- tensor (Tensor or list) – Image to be saved. If given a mini-batch tensor, saves the tensor as a grid of images by calling
make_grid
.- fp (string or file object) – A filename or a file object
- format (Optional) – If omitted, the format to use is determined from the filename extension. If a file object was used instead of a filename, this parameter should always be used.
- **kwargs – Other arguments are documented in
make_grid
.
import time
import torchvision.utils as vutils
vutils.save_image(data['img'].data, 'images/{}.png'.format(time.time()), nrow=4, padding=0, normalize=True)
GPU
将网络或数据从 CPU 转到 GPU 上可以用 data.cuda()
或者 data.to('cuda')
,不过一般都用后面这种形式,因为会在前面加一行代码
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
data.to(device)
这样就能使得没有 GPU 的情况下也需要改代码就能够跑模型了
网络结构较小的时候,CPU 和 GPU 之间进行数据传输耗时更高,这时用 CPU 更快,当网络庞大的时候,用 GPU 可以明显感觉到提速。我自己试了一个简单的回归网络,跑 200 个 epoch 在 CPU 上 2.5s ,GPU 要 6.6s
指定 GPU 进行训练:实验室 8 块卡,目前只有 cuda:6 是空闲的,但是默认情况下 pytorch 会找 cuda:0,上面是满的所以会报超内存错误。这时候就要用下面代码指定 cuda:6 进行训练,也就是说屏蔽了其他的卡,此时第六块卡也就变成了 cuda:0,这个要注意不要搞错了,如果后面再写 cuda:6 就会报错了
import os
#多块使用逗号隔开
os.environ['CUDA_VISIBLE_DEVICES'] = '6'
注意:这样写可能还不行,我今天试过了,不生效,保险一点的话还是在命令行里面写吧
$ CUDA_VISIBLE_DEVICES=6 python xx_train.py
loss_function
loss_function 有些在 torch.nn.functional
和 torch.nn
里面都有,但是调用起来的方法是不一样的,而且一个需要大写首字母,一个不需要。具体内容看以下代码
import torch.nn as nn
criterion = nn.CrossEntropyLoss()
loss = criterion(out, label)
loss = nn.CrossEntropyLoss()(out, label) # 或者直接这样写
import torch.nn.functional as F
loss = F.cross_entropy(out, label)
train&test
测试的时候要将 model 变成 eval 模式(net.eval()),一般是在 train 模式的,如果测试后还要接着训练的话在最后加上 net.train()
测试时用 with xx.no_grad()
不计算梯度,减小显存开销和算力。如果不在意显存开销的话用 model.eval() 就够了,这就可以改变 Batch Normalization 和 Dropout 的行为。eval() 模式的梯度计算与储存和 train() 一样,只不过不会反向传播更新参数罢了。
save&load
加载 load 训练好的模型的时候要将模型的定义代码一起包含进来,否则会报错说找不到网络结构
当不需要对变量进行梯度更新时,可以在后面加上 .detach()
,这样做相当于将 requre_grad
置为 False,在 GAN 中固定 Discriminator 时会用到这个
torch.max
torch.max(tensor, dim=x)
返回的是 tensor 中的最大值以及最大值的索引号,dim 参数表示取的是横向的还是竖向的最大值,0 代表每个纵向的最大值,1 代表每个横向的最大值
import torch
torch.manual_seed(1)
a = torch.rand(3, 4)
value, index = torch.max(a, dim=1)
print('{}\n{}\n{}'.format(a, value, index))
# tensor([[0.7576, 0.2793, 0.4031, 0.7347],
# [0.0293, 0.7999, 0.3971, 0.7544],
# [0.5695, 0.4388, 0.6387, 0.5247]])
# tensor([0.7576, 0.7999, 0.6387])
# tensor([0, 1, 2])
经常在神经网络的分类任务求准确率的时候用到这个函数,要记住的是 max 函数有两个返回值,并且也要知道 dim 代表的含义,在 torch.nn.functional.softmax(tensor, dim=x)
中的 dim 跟这里的 dim 也是有一样的含义
torch.manual_seed(x)
torch.manual_seed(x)
用来固定随机数,使得每次生成的随机数都是相同的,常用来复现他人结果
torch.cat & torch.stack
torch.cat 和 torch.stack 都可以将两个 tensor 连接成一个,但是用法有点不同,以 pytorch 经常处理的四维 tensor 来举例子
torch.cat
简单的说, cat 会将两个 tensor 中指定维度的数据堆在一起,扩充该维度的大小,要求两个 tensor 的维度必须一致,并且经过 cat 后的 tensor 的维度不会变,和之前一样。下面看看例子
import torch
a0 = torch.Tensor([[[[1,1,1,1],[2,2,2,2]]]])
a1 = torch.Tensor([[[[3,3,3,3],[4,4,4,4]]]])
l = []
l.append(torch.cat((a0,a1),dim=0))
l.append(torch.cat((a0,a1),dim=1))
l.append(torch.cat((a0,a1),dim=2))
l.append(torch.cat((a0,a1),dim=3))
for i in l:
print('{}\n{}\n{}\n\n\n'.format(i, a1.size(), i.size()))
分别在四个维度上做实验,得到的结果如下:
tensor([[[[1., 1., 1., 1.],
[2., 2., 2., 2.]]],
[[[3., 3., 3., 3.],
[4., 4., 4., 4.]]]])
torch.Size([1, 1, 2, 4])
torch.Size([2, 1, 2, 4])
tensor([[[[1., 1., 1., 1.],
[2., 2., 2., 2.]],
[[3., 3., 3., 3.],
[4., 4., 4., 4.]]]])
torch.Size([1, 1, 2, 4])
torch.Size([1, 2, 2, 4])
tensor([[[[1., 1., 1., 1.],
[2., 2., 2., 2.],
[3., 3., 3., 3.],
[4., 4., 4., 4.]]]])
torch.Size([1, 1, 2, 4])
torch.Size([1, 1, 4, 4])
tensor([[[[1., 1., 1., 1., 3., 3., 3., 3.],
[2., 2., 2., 2., 4., 4., 4., 4.]]]])
torch.Size([1, 1, 2, 4])
torch.Size([1, 1, 2, 8])
torch.stack
stack 会在拼接之前先将 tensor 给扩大一维,然后再将指定维度上的数据进行连接,也就相当于在这个例子中 dim 可以指定为 4(不过没人会这么用),如下:
import torch
a0 = torch.Tensor([[[[1,1,1,1],[2,2,2,2]]]])
a1 = torch.Tensor([[[[3,3,3,3],[4,4,4,4]]]])
l = []
l.append(torch.stack((a0,a1),dim=0))
l.append(torch.stack((a0,a1),dim=1))
l.append(torch.stack((a0,a1),dim=2))
l.append(torch.stack((a0,a1),dim=3))
l.append(torch.stack((a0,a1),dim=4))
for i in l:
print('{}\n{}\n{}\n\n\n'.format(i, a1.size(), i.size()))
实验的结果如下,细细品,dim 指定哪个维度就在哪个维度前面加一个维数为 2 的维度
tensor([[[[[1., 1., 1., 1.],
[2., 2., 2., 2.]]]],
[[[[3., 3., 3., 3.],
[4., 4., 4., 4.]]]]])
torch.Size([1, 1, 2, 4])
torch.Size([2, 1, 1, 2, 4])
tensor([[[[[1., 1., 1., 1.],
[2., 2., 2., 2.]]],
[[[3., 3., 3., 3.],
[4., 4., 4., 4.]]]]])
torch.Size([1, 1, 2, 4])
torch.Size([1, 2, 1, 2, 4])
tensor([[[[[1., 1., 1., 1.],
[2., 2., 2., 2.]],
[[3., 3., 3., 3.],
[4., 4., 4., 4.]]]]])
torch.Size([1, 1, 2, 4])
torch.Size([1, 1, 2, 2, 4])
tensor([[[[[1., 1., 1., 1.],
[3., 3., 3., 3.]],
[[2., 2., 2., 2.],
[4., 4., 4., 4.]]]]])
torch.Size([1, 1, 2, 4])
torch.Size([1, 1, 2, 2, 4])
tensor([[[[[1., 3.],
[1., 3.],
[1., 3.],
[1., 3.]],
[[2., 4.],
[2., 4.],
[2., 4.],
[2., 4.]]]]])
torch.Size([1, 1, 2, 4])
torch.Size([1, 1, 2, 4, 2])
reference: https://zhuanlan.zhihu.com/p/70035580
format_transform
使用 PIL 的 Image.fromarray 创建图象时,要求 numpy 数组的格式为 uint8 类型
to_tensor 是 pytorch 的 transforms 中的方法,将 PIL 格式的图片转化成 tensor 格式,原理是:PIL 储存图片的格式为(HWC),而 PIL 储存的是 (HWC),通过变换通道后,再将像素的值除以 255 得到 tensor。反过来,tensor 变成 PIL 格式的话就使用 transforms.ToPILImage() 方法
PIL 图像在转化成 numpy.ndarray 后,格式为(HWC),通道顺序是 RGB,用 Image.size 方法返回的是(W,H)。OpenCV 读入图片的格式就是 ndarray,格式为(HWC),通道顺序是 BGR
用 PIL 的 Image.Open(path) 读 png 图片只有一个通道,OpenCV 读到的是 3 个通道,不过会报错。所以如果拿 png 格式的图片训练的模型,在测试阶段用 jpg 格式去测试的话很可能会报错,因为 channel 数不对,解决办法就是判断图片的通道数(用 len(Image.split()) 来判断),如果 len 大于 1 的话说明不止一个通道,可以只取一个通道(如绿色通道)来进行测试。
有时候需要改变 tensor 的 type,可以用 tensor.dtype
来查看 tensor 的 type,例如 torch.int64
就是一种 type,然后如果要转化的话,用 tensor.to(type)
来进行,例如 tensor.to(torch.float32)
,而不是像 numpy 一样用 array.astype(type)
model_structure
torch.nn.AdaptiveAvgPool2d()
自适应的平均池化,即只需要给定最终想要获得的 feather map 的 size 就行了,不用管怎么实现,并且通道数前后不变
e.g. torch.nn.AdaptiveAvgPool2d((1, 1)) 无论给定的输入 feather map 是多少,最终都会变成 (1 x 1) 的 feather map
torch.nn.MaxPool2d()
参数列表为 kernel_size, *stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False,一般前两个参数都是 (2,2),这里说一下 ceil_mode 这个参数,默认情况下是 False,用的是 floor_mode,feather map 的 output 按照下面这个公式计算 \(output = floor((W-K+2P)/S) + 1\) 也就是向下取整,如果用了 ceil_mode 的话就是向上取整,公式如下 \(output = ceil((W-K+2P)/S) + 1\)
有什么不同呢,会影响最后生成的 feather map 的尺寸,通过一个例子看看
import torch.nn as nn
import torch
x = torch.tensor([
[-2, 1, 2, 6, 4],
[-3, 1, 7, 2, -2],
[-4, 2, 3, -1 , -3],
[-7, 1, 2, 3, 11],
[5, -7, 8, 12, -9] ]).float()
x = x.unsqueeze(0)
y_1 = nn.MaxPool2d(kernel_size=2,stride=2, padding=0)
y_2 = nn.MaxPool2d(kernel_size=2,stride=2, padding=0, ceil_mode=True)
print(y_1(x))
print(y_2(x))
最终结果如下,所以用了 ceil_mode 的话相当于 padding 了几个 0 进行池化操作
tensor([[[1., 7.], [2., 3.]]])
tensor([[[ 1., 7., 4.], [ 2., 3., 11.], [ 5., 12., -9.]]])
torch.meshgrid
在 Faster RCNN 中有用到这个 API 用来将 anchor 从 feature map 上偏移到原图上,其实这个函数就是用来生成两个矩阵,在 Faster RCNN 中表示 anchor 在 x 和 y 方向上的偏移量(注意,numpy 也有 meshgrid,但是两者的顺序有不一样,详见我的 numpy 参考手册)
shifts_x = torch.arange(0, 4, dtype=torch.float32) * 16
shifts_y = torch.arange(0, 4, dtype=torch.float32) * 16
shift_y, shift_x = torch.meshgrid(shifts_y, shifts_x)
shift_x
tensor([[ 0., 16., 32., 48.],
[ 0., 16., 32., 48.],
[ 0., 16., 32., 48.],
[ 0., 16., 32., 48.]])
shift_y
tensor([[ 0., 0., 0., 0.],
[16., 16., 16., 16.],
[32., 32., 32., 32.],
[48., 48., 48., 48.]])