Pytorch

9 minute read

Published:

Pytorch学习笔记

本文主要记录pytorch中可能经常被遗忘的知识点,包括基本的神经网络设置。主要内容参考Dive-into-DL-PyTorch仓库

2-2. tensor

torch.empty(5, 3) #用于创建**未初始化**的Tensor

索引形式的结果与源数据共享内存,因此修改其中一个,另一个也会修改,例如

a = b.view(15)  # 修改a时,b也会改变

pytorch操作inplace版本都有后缀”_“,例如

x.t_()

重载运算符:

x += 1 # 在每个x的维度上都加1
z = x + y # 通过广播机制,使得x和y的维度扩充为相同的
y = x ** 2 #也可以写成 y = x * x; element-wise,哈达玛积(点乘)
y = torch.mm(x, x) #矩阵乘,也可以用torch.einsum()
"""
例如: x = [[1, 2]] 大小为(1, 2),把它扩充到(3, 2),则结果为
x = [
[1, 2],
[1, 2],
[1, 2]
]
"""

为张量创建一个复制而不共享内存:

x_cp = x.clone()

tensor与numpy的转换: numpy()和torch.from_numpy()共用内存; torch.tensor()不共用内存

b = a.numpy()   # a: tensor; b: numpy
c = torch.from_numpy(d) # c: tensor; d: numpy
b = torch.tensor(a) # a和b不共享内存

在GPU上创建张量:

y = torch.ones_like(x, device = torch.device("cuda")    # 创建在GPU上的y
y.to("cpu", torch.double)   # 转移到cpu,保留double格式

2-3. autograd

每个tensor对象都拥有两个重要属性:

  1. .grad属性保存所有.backward()梯度计算之后更新的梯度结果。通过设置.requires_grad=True,后续所有计算都会被记录和追踪。为了防止将来的计算被追踪,可以调用.detach()或者torch.no_grad(),这样未来创建新变量的requires_grad为False
  2. .grad_fn为创建该Tensor对象时的function(这个对象可能没有显式地赋予名称)。因此,如果是显式地创建一个tensor,则他的grad_fn应当为None。直接创建的tensor成为梯度图中的叶子结点,可以通过对象的.is_leaf来查询。

注意:每次backward反向传播后,梯度都会累加之前保存的梯度。因此在反向传播前,需要将梯度清零。另外,调用.backward()一般都是标量!

在式子中,部分项detach:

y_1 = x ** 2
with torch.no_grad():
    y_2 = x ** 3    # y_2不记录梯度
y_3 = y_1 + y_2     # y_3包含了y_2项
y_3.backward()      # backward回传之后,x的梯度有何变化?

\(y_3 = y_1 + y_2 = x^2 + x^3\) \(\frac{\partial y_3}{\partial x} = \frac{\partial y_1}{\partial x} + \frac{\partial y_2}{\partial x} = 2x + 3x^2\) 如果$y_1$被torch.no_grad(),则它对应的梯度$\frac{\partial y_1}{\partial x}$不会回传,因此最终$x$的梯度为$2x$.

如果对象属性requires_grad为False,则不能调用backward()方法,会报错(因为没有grad_fn)

每个tensor都有一个属性.data。这个属性是不安全的,因为如果更改了data属性,则此次改变不会记录在原来的计算图中,而原来的tensor的值也改变了,因此导数值是错误的。例如:

x = torch.ones(1,requires_grad=True)
y = x ** 2
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播
print(y)        # 1
y.backward()
print(x) # x = 100; 更改data的值也会影响tensor的值
print(x.grad)   # x.grad = 200; x的梯度为2x,而x在这里已经被修改成100了

可见,梯度的计算调用了.data属性,而对data属性的修改是不在计算图内的,因此很危险。

3-1 ~ 3-3. linear regression线性回归

生成数据(符合normal分布):

torch.randn(bsz, feature_dim, dtype=torch.float32)  # 生成一个二维随机标准正态矩阵
np.random.normal(mean, variance, size=(2, 3))   # 生成一个2x3的随机矩阵,数据采样于均值mean,方差variance的正态分布

指定torch默认的数据类型:

torch.set_default_tensor_type('torch.FloatTensor')

定义Dataset的核心要点:使用Dataset这个子类

from torch.utils.data import Dataset, DataLoader

利用Dataset的一个实例dataset,我们可以构造一个DataLoader对象,后续可以通过循环迭代取出一个batch:

data_iter = Data.DataLoader(
    dataset=dataset,    
    batch_size=batch_size,      # mini batch size
    shuffle=True,               # 要不要打乱数据 (打乱比较好)
    num_workers=2,              # 多线程来读数据
)
for batch in data_iter:
    pass

我们可以根据自己的数据集格式,手动创建一个Dataset子类,一个简单的例子如下,必须包含__getitem__和__len__两个方法。

class MyDataset(Dataset):
    def __init__(self, myList):
        super(MyDataset, self).__init__()
        self.myList = myList
        self.length = len(myList)
    def __getitem__(self, idx):
        batch = {
            "input_ids": self.myList[idx]
        }
        return batch
    def __len__(self):
        return self.length

定义模型的核心要点,它的超集为nn.Module:

class LinearNet(nn.Module):
    def __init__(self, n_feature):
        super(LinearNet, self).__init__()
        self.linear = nn.Linear(n_feature, 1)   # 指定一些网络中的模块,比如线性层

    def forward(self, x):
        # x已经被batch包装好了,这里的大小为[bsz, n_feature],每个sample的维度即为feature size
        y = self.linear(x)  # 在前向过程中,初始的input作为叶子结点,调用网络模块来计算
        return y

也可以用nn.Sequential来定义有序网络与嵌套网络:

class SequentialNet(nn.Sequential):
    pass

创建一个模型的实例,例如

net = LinearNet(128)

可以通过net.parameters()来查看模型的各个层的参数细节。

3-6 ~ 3-7. softmax regression

在网络中套一层其他网络:

class FlattenLayer(nn.Module):
    # 后续可以通过FlattenLayer()来调用
    def __init__(self):
        super(FlattenLayer, self).__init__()
    def forward(self, x): # x shape: (batch, *, *, ...)
        # 将所有的feature拼成一维
        return x.view(x.shape[0], -1)

from collections import OrderedDict
net = nn.Sequential(
    # FlattenLayer(),
    # nn.Linear(num_inputs, num_outputs)
    OrderedDict([
      ('flatten', FlattenLayer()),
      ('linear', nn.Linear(num_inputs, num_outputs))])
    )

torch.nn.init可以初始化各层的参数(例如初始化线性层可以传入net.linear.weight或net.linear.bias)

for params in net.parameters():
    init.normal_(params, mean=0, std=0.01)

3-8 ~ 3-10. mlp

mlp的主要结构如下:

net = nn.Sequential(
    d2l.FlattenLayer(),
    nn.Linear(num_inputs, num_hiddens),
    nn.ReLU(),
    nn.Linear(num_hiddens, num_outputs), 
    )

注意,这里的Sequential本身就具有forward和backward功能,因为它继承了nn.Module父类。

3-11 ~ 3-13. 机器学习的其他常见问题

L_2范数惩罚项:
def l2_penalty(w):
    return (w**2).sum() / 2

dropout: 丢弃一部分元素,提升泛化能力 torch.zeros_like(X)将X全置0. (torch.rand(X.shape) < keep_prob).float()获得一个长度为X.shape, 掩码概率为keep_prob的0-1向量。 在实际使用中,调用nn.Dropout即可。 Dropout层输入的参数有一定概率被置零后输出。

net = nn.Sequential(
        d2l.FlattenLayer(),
        nn.Linear(num_inputs, num_hiddens1),
        nn.ReLU(),
        nn.Dropout(drop_prob1),
        nn.Linear(num_hiddens1, num_hiddens2), 
        nn.ReLU(),
        nn.Dropout(drop_prob2),
        nn.Linear(num_hiddens2, 10)
        )

4-1. 模型构造

isinstance(x, y)用于判断:对象x的类型是否为y。 继承nn.Module的网络类,可以调用self.add_module(k, v)方法,按顺序加入self._modules属性。 一个典型例子:

class MySequential(nn.Module):
    from collections import OrderedDict
    def __init__(self, *args):
        super(MySequential, self).__init__()
        if len(args) == 1 and isinstance(args[0], OrderedDict): # 如果传入的是一个OrderedDict
            for key, module in args[0].items():
                self.add_module(key, module)  # add_module方法会将module添加进self._modules(一个OrderedDict)
        else:  # 传入的是一些Module
            for idx, module in enumerate(args):
                self.add_module(str(idx), module)
    def forward(self, input):
        # self._modules返回一个 OrderedDict,保证会按照成员添加时的顺序遍历成
        for module in self._modules.values():
            input = module(input)
        return input

nn.ModuleList()参数为一个列表,列表元素为nn网络类,例如net = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])。这里不能使用list类来代替,虽然依旧可以在forward函数中计算出结果,但是list内的nn.Module参数不会被注册到网络中,也就是说,网络无法更新参数,无法训练。 我个人倾向于用nn.Sequential + OrderedDict来编写网络。

在网络中,可以手动设置一些不可学习(更改)的参数,

self.rand_weight = torch.rand((20, 20), requires_grad=False) # 不可训练参数(常数参数)

编写网络的两种方法:

  1. 自己定义一个nn.Module的子类,编写__init__和forward函数;
  2. 利用nn.Sequential传入一系列已有的模块(如nn.Linear等)作为参数,此时不需要写上述两个函数。 例如,
    net = nn.Sequential(NestMLP(), nn.Linear(30, 20), FancyMLP())
    X = torch.rand(2, 40)
    print(net)
    net(X)
    

4-2. 模型参数

可以通过net.named_parameters()来访问每个参数。

for name, param in net.named_parameters():
    print(name, param.size())

为了使模型注册到网络中,在编写网络时,每个初始化参数tensor需要用nn.Parameter包装:

self.weight1 = nn.Parameter(torch.rand(20, 20))

如果需要将参数加入一个列表,同样不能用list,而应当用ParameterList:

self.params = nn.ParameterList([nn.Parameter(torch.randn(4, 4)) for i in range(3)])

在forward中,可以用for in的方式抽取出self.params的每一层,例如:


此外,初始化参数,可以用init方法,例如将参数变为0,可以调用:

init.constant_(param, val=0)

注意:如果用自定义的方法初始化,则需要包装torch.no_grad() 共享参数的网络:只需重复引用一个模块实例即可:

linear = nn.Linear(1, 1, bias=False)
net = nn.Sequential(linear, linear) 

在Sequential模块中,两个linear实例共享同一套参数。 (共享参数的网络层梯度如何更新?)

x = torch.ones(1, 1)
y = net(x).sum()
print(y)
y.backward()
print(net[0].weight.grad)
## 输出
tensor(9., grad_fn=<SumBackward0>)
tensor([[6.]])

4-4. 模型层

通过nn.ParameterDict可以接受一个值为nn.Parameter的字典。这个ParameterDict对象也可以如正常字典一样update一个新的ParameterDict,例如:

self.params = nn.ParameterDict({
                'linear1': nn.Parameter(torch.randn(4, 4)),
                'linear2': nn.Parameter(torch.randn(4, 1))
        })
        self.params.update({'linear3': nn.Parameter(torch.randn(4, 2))}) # 新增

4-5. 读写

torch.save()和torch.load()可以将很多数据结构和存储关联起来,甚至可以操作字典:键依然作为字符串存储,而值转换为tensor。 net.state_dict()可以返回一个有序字典,存储所有层名和对应的参数列表。 注意,如果是用常规方法写,则有序字典的键对应着创建网络时的私有变量名,例如:

def __init__(self):
    super(MLP, self).__init__()
    self.hidden = nn.Linear(3, 2)
    self.act = nn.ReLU()
    self.output = nn.Linear(2, 1)

就对应着以下state_dict():


OrderedDict([('hidden.weight', tensor([[ 0.1836, -0.1812, -0.1681],
                      [ 0.0406,  0.3061,  0.4599]])),
             ('hidden.bias', tensor([-0.3384,  0.1910])),
             ('output.weight', tensor([[0.0380, 0.4919]])),
             ('output.bias', tensor([0.1451]))])

同理,optimizer也可以调用state_dict(),但返回值包含了更多信息,诸如learning rate和momentum等等。 存储模型,等同于存储模型的state_dict()。因此,存储模型应当这样使用:

torch.save(net.state_dict(), PATH)

4-6. 使用GPU

将一个tensor从cpu转换到cuda:0上:

x = x.cuda(0)

返回结果:tensor([1,2,3], device=’cuda:0’) 注意,这里创建一个GPU版本的x,需要利用返回值重新赋值。 相反,对于模型而言,

net.cuda()

直接调用.cuda()方法,net自身就迁移到GPU上了。

5-1 ~ 5-4. CNN基础知识

CNN就是通过一个固定的小核,将一个矩阵变换到另一个大小的矩阵。 输入为X(矩阵)和K(小核)。

def corr2d(X, K):  # 本函数已保存在d2lzh_pytorch包中方便以后使用
    h, w = K.shape  # Kernel的大小为h*w
    X, K = X.float(), K.float() 
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) # 最终的输出大小
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            Y[i, j] = (X[i: i + h, j: j + w] * K).sum() # 输出的每个元素为原始X的子矩阵
    return Y

CNN中,输入X的前两维分别为通道数批量大小。输入与输出的批量大小不变,而通道数可能发生变化,torch.nn.Conv2d的主要参数如下:

  • in_channels: 输入张量的通道数
  • out_channels: 输出张量的通道数
  • kernel_size: 卷积核的大小(二维),设大小为$(k_1, k_2)$
  • stride: 在卷积时,卷积核的移动步长,设大小为$(s_1, s_2)$
  • padding: 在卷积之前,原矩阵最后两维两侧填充边界,设大小为$(p_1, p_2)$

具体使用例子:

nn.Conv2d(1, 6, 5)  # in_channels = 1, out_channels = 6
# kernel_size被扩展到方阵(大小为5 * 5), 等价于
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))

设输入X的矩阵大小(不考虑批量和通道)为(8, 8),则

  1. 卷积之前,通过padding将矩阵扩充为$(8 + 2p_1, 8 + 2p_2)$
  2. 用卷积核卷积。若步长默认为1,则输出大小为$(8 + 2p_1 - (k_1-1), 8 + 2p_2 - (k_2-1))$
  3. 如果步长为$n\neq 1$,则最后的二维矩阵两条边各自减少为原先的$\frac{1}{n}$.

实际情况中,输入和输出通道数可能不止一个。设输入和输出的通道数分别为$(d_1, d_2)$. 每一个输入通道相应的矩阵$X_i$都对应着其各自的卷积核$K_{ij}$,经过单通道卷积运算后得到输出的张量$Y_{ij}$。如此操作每一个输入通道,将输出的张量$Y_{1j}, Y_{2j}, \cdots, Y_{d_1 j}$累加起来,即对应着最终第$j$个输出通道的张量$Y_j$。 依照上述步骤分别得到$Y_1, Y_2, \cdots, Y_{d_2}$,将他们拼在一起即可得到包含$d_2$维输出通道的结果。

可见,$d_1$个输入通道与$d_2$个输出通道之间是全连接的,也就是有$d_1\times d_2$个连接。每个连接的卷积核参数都是独立的,因此多通道卷积核的大小为$(d_1, d_2, k_1, k_2)$.

池化层和卷积核很像,都是在一个卷积核大小的窗口中,计算出一个标量存储到输出中。只不过池化层操作过后,计算得到的是窗口内原矩阵的最大值或平均值。 最大池化层和平均池化层可以分别通过nn.MaxPool2d()和nn.AvgPool2d()调用,参数为池化窗口的大小(同卷积核,都是二维),padding和stride。

5-5 ~ 5-7. 常见卷积网络

由CNN衍生出的网络有很多,例如lenet, alexnet和vgg。 lenet网络如下:

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(1, 6, 5), # in_channels, out_channels, kernel_size
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2), # kernel_size, stride
            nn.Conv2d(6, 16, 5),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2)
        )
        self.fc = nn.Sequential(
            nn.Linear(16*4*4, 120),
            nn.Sigmoid(),
            nn.Linear(120, 84),
            nn.Sigmoid(),
            nn.Linear(84, 10)
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output

可以注意到,网络在卷积之后留下的二维矩阵摊平(view)成一维向量,然后通过一个全连接MLP得到最终10分类结果。卷积和全连接网络均调用了nn.Sequential模块

AlexNet网络比LeNet更深,且激活函数使用了ReLU而不是Sigmoid,旨在提升网络的泛化性。

class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
            nn.ReLU(),
            nn.MaxPool2d(3, 2), # kernel_size, stride
            # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(3, 2),
            # 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
            # 前两个卷积层后不使用池化层来减小输入的高和宽
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(3, 2)
        )
         # 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
        self.fc = nn.Sequential(
            nn.Linear(256*5*5, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            # 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
            nn.Linear(4096, 10),
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output

VGG网络利用了重复的元素,引入了卷积核大小为3*3的卷积块vgg_block,并在最后添加了最大池化层,使输出大小减少为原先的$\frac{1}{4}$:

def vgg_block(num_convs, in_channels, out_channels):
    blk = []
    for i in range(num_convs):
        if i == 0:
            blk.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
        else:
            blk.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))
        blk.append(nn.ReLU())
    blk.append(nn.MaxPool2d(kernel_size=2, stride=2))
    return nn.Sequential(*blk)

上述代码创建一个列表,然后添加每个卷积层和池化层,最后用nn.Sequential()包装这个列表并返回。

接下来定义vgg网络。通过nn.Sequential()对象的.add_module()方法添加vgg块,以及最后的全连接层。注意,这里自定义了一个flatten层,此网络的作用仅仅是将输入变换成二维矩阵。

class FlattenLayer(torch.nn.Module):
    def __init__(self):
        super(FlattenLayer, self).__init__()
    def forward(self, x): # x shape: (batch, *, *, ...)
        return x.view(x.shape[0], -1)
def vgg(conv_arch, fc_features, fc_hidden_units=4096):
    net = nn.Sequential()
    # 卷积层部分
    for i, (num_convs, in_channels, out_channels) in enumerate(conv_arch):
        net.add_module("vgg_block_" + str(i+1), vgg_block(num_convs, in_channels, out_channels))
    # 全连接层部分
    net.add_module("fc", nn.Sequential(d2l.FlattenLayer(),
                                 nn.Linear(fc_features, fc_hidden_units),
                                 nn.ReLU(),
                                 nn.Dropout(0.5),
                                 nn.Linear(fc_hidden_units, fc_hidden_units),
                                 nn.ReLU(),
                                 nn.Dropout(0.5),
                                 nn.Linear(fc_hidden_units, 10)
                                ))
    return net

5-8 ~ 5-10. 其他网络结构

NiN模型:将多个卷积层和激活层组合在一起,同时保留Conv2d的语法。

def nin_block(in_channels, out_channels, kernel_size, stride, padding):
    blk = nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding),
                        nn.ReLU(),
                        nn.Conv2d(out_channels, out_channels, kernel_size=1),
                        nn.ReLU(),
                        nn.Conv2d(out_channels, out_channels, kernel_size=1),
                        nn.ReLU())
    return blk

实际中,多次调用NiN block,组合成新的网络。

net = nn.Sequential(
    nin_block(1, 96, kernel_size=11, stride=4, padding=0),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nin_block(96, 256, kernel_size=5, stride=1, padding=2),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nin_block(256, 384, kernel_size=3, stride=1, padding=1),
    nn.MaxPool2d(kernel_size=3, stride=2), 
    nn.Dropout(0.5),
    # 标签类别数是10
    nin_block(384, 10, kernel_size=3, stride=1, padding=1),
    # 全局平均池化层可通过将窗口形状设置成输入的高和宽实现
    nn.AvgPool2d(kernel_size=5),
    # 将四维的输出转成二维的输出,其形状为(批量大小, 10)
    d2l.FlattenLayer())

小技巧:如果需要得到每一层的输出大小,可以调用网络的.named_children()方法(和named_parameters很相似):

X = torch.rand(1, 1, 224, 224)
for name, blk in net.named_children(): 
    X = blk(X)
    print(name, 'output shape: ', X.shape)

GoogLeNet: 包含并行链接的网络Inception block,前向计算时,将每一部分的输出拼在一起并最终返回

class Inception(nn.Module):
    # c1 - c4为每条线路里的层的输出通道数
    def __init__(self, in_c, c1, c2, c3, c4):
        super(Inception, self).__init__()
        # 线路1,单1 x 1卷积层
        self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
        # 线路2,1 x 1卷积层后接3 x 3卷积层
        self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1 x 1卷积层后接5 x 5卷积层
        self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3 x 3最大池化层后接1 x 1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_c, c4, kernel_size=1)

    def forward(self, x):
        p1 = F.relu(self.p1_1(x))
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        return torch.cat((p1, p2, p3, p4), dim=1)  # 在通道维上连结输出

使用torch.cat需要注意,传入的第一个参数是一个张量元组,且除了指定维度参数之外,这些张量在其他维度的张量大小必须相同。 例如上述函数的例子,一个合理的设置为:

p1.shape = (3, 2, 4)
p2.shape = (3, 5, 4)
p3.shape = (3, 1, 4)
p4.shape = (3, 2, 4)

在dim=1拼接后,返回值的大小为

ret.shape = (3, 10, 4)

batchNorm即为批量归一化。它也是一个nn.Module子类:

def batch_norm(is_training, X, gamma, beta, moving_mean, moving_var, eps, momentum):
    # 判断当前模式是训练模式还是预测模式
    if not is_training:
        # 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        assert len(X.shape) in (2, 4)
        if len(X.shape) == 2:
            # 使用全连接层的情况,计算特征维上的均值和方差
            mean = X.mean(dim=0)
            var = ((X - mean) ** 2).mean(dim=0)
        else:
            # 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。这里我们需要保持
            # X的形状以便后面可以做广播运算
            mean = X.mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
            var = ((X - mean) ** 2).mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
        # 训练模式下用当前的均值和方差做标准化
        X_hat = (X - mean) / torch.sqrt(var + eps)
        # 更新移动平均的均值和方差
        moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
        moving_var = momentum * moving_var + (1.0 - momentum) * var
    Y = gamma * X_hat + beta  # 拉伸和偏移
    return Y, moving_mean, moving_var
class BatchNorm(nn.Module):
    def __init__(self, num_features, num_dims):
        super(BatchNorm, self).__init__()
        if num_dims == 2:
            shape = (1, num_features)
        else:
            shape = (1, num_features, 1, 1)
        # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成0和1
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        # 不参与求梯度和迭代的变量,全在内存上初始化成0
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.zeros(shape)

    def forward(self, X):
        # 如果X不在内存上,将moving_mean和moving_var复制到X所在显存上
        if self.moving_mean.device != X.device:
            self.moving_mean = self.moving_mean.to(X.device)
            self.moving_var = self.moving_var.to(X.device)
        # 保存更新过的moving_mean和moving_var, Module实例的traning属性默认为true, 调用.eval()后设成false
        Y, self.moving_mean, self.moving_var = batch_norm(self.training, 
            X, self.gamma, self.beta, self.moving_mean,
            self.moving_var, eps=1e-5, momentum=0.9)
        return Y

可以通过nn.BatchNorm1d和nn.BatchNorm2d来简洁调用。 一般而言他们只有一个参数num_features,例如:

nn.Linear(16*4*4, 120),
nn.BatchNorm1d(120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.BatchNorm1d(84),
nn.Sigmoid(),
nn.Linear(84, 10)

他们的不同点在于,nn.BatchNorm1d()接在一个三维张量输出$(N, C, L)$后面,而nn.BatchNorm2d()传入张量大小为$(N, C, H, W)$。 BatchNorm的大体步骤:(以BatchNorm2d为例)

  1. 循环C次,每个循环中取出一个通道$i$
  2. 得到一个切片$(N, H, W)$
  3. 将这个切片中的所有元素高斯标准化,大致转化到(0, 1)分布;实际上平均值和方差的确切值不一定为0和1,请看文档提及的技术细节
  4. 将这个标准化后的切片作为输出张量的第$i$个通道
  5. 最终输出的张量大小为$(N, C, H, W)$,每个通道对应的张量$(N, H, W)$的平均值和方差大约为0和1

用代码验证如下:

import torch.nn as nn
import torch
net = nn.BatchNorm2d(5)
t = torch.rand(3, 5, 2, 3)
output = net(t)
for i in range(5):
    print(torch.mean(output[:, i, :, :]), torch.var(output[:, i, :, :]))

输出结果:

tensor(6.6227e-08, grad_fn=<MeanBackward0>) tensor(1.0587, grad_fn=<VarBackward0>)
tensor(2.6491e-08, grad_fn=<MeanBackward0>) tensor(1.0587, grad_fn=<VarBackward0>)
tensor(-6.9539e-08, grad_fn=<MeanBackward0>) tensor(1.0587, grad_fn=<VarBackward0>)
tensor(6.6227e-09, grad_fn=<MeanBackward0>) tensor(1.0587, grad_fn=<VarBackward0>)
tensor(3.3114e-08, grad_fn=<MeanBackward0>) tensor(1.0587, grad_fn=<VarBackward0>)

具体细节参考Pytorch文档

5-11 ~ 5-12. 残差网络

接下来主要介绍ResNet和DenseNet。 ResNet主要解决了深度神经网络不能很好建模恒等变换的问题,ResNet Block网络在返回的时候,在最终的激活层前额外加上了一个原来的输入张量X。

class Residual(nn.Module):  # 本类已保存在d2lzh_pytorch包中方便以后使用
    def __init__(self, in_channels, out_channels, use_1x1conv=False, stride=1):
        super(Residual, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, stride=stride)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)

    def forward(self, X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        return F.relu(Y + X)

Densenet与Resnet的一个不同点在于,它的输出和输入并不是求和,而是在通道维度上拼接。也就是将第N层的输出和第N层的输入沿通道维度connect.

6. 循环神经网络: rnn, gru与lstm

用pytorch定义RNN layer:

rnn_layer = nn.RNN(input_size = vocab_size, hidden_size = num_hiddens)

RNN的结构可以从横向和纵向来看,横向是隐藏层的传递,纵向是输入的传递。输入为词表大小,隐藏层大小为num_hiddens。