关于pytorch的一些笔记

Posted by kevin on March 20, 2020

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) 来将卷积过后的所有特征都变成一个特征向量进行全连接

x.view

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))


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.functionaltorch.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 的话说明不止一个通道,可以只取一个通道(如绿色通道)来进行测试。

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.]])