Pytorch
Published:
Pytorch学习笔记
本文主要记录pytorch中可能经常被遗忘的知识点,包括基本的神经网络设置。主要内容参考Dive-into-DL-PyTorch仓库
2-2. numpy与tensor
numpy的常见用法:
np.zeros([10, 10]) # 浮点0矩阵
np.ones([10, 10]) # 浮点1矩阵
np.arange(3, 6) # 创建[3, 4, 5]的array
np.random.rand(10, 10) # 大小为10*10的包含0~1之间的均匀数的矩阵
np.random.uniform(0, 100) # 0~100均匀分布的一个数
np.random.randint(0, 100) # 0~100均匀分布的一个整数
np.random.normal(100, 1, (2, 3)) # 给定均值标准差和维度的正态分布
a.size; a.shape; a.ndim; a.dtype # 元素个数/数组形状/数组维度/元素类型
a.sum(); a.mean(); a.std(); a.var(); a.min(); a.max(); a.argmin(); a.argmax(); a.cumsum(); a.cumprod() # 顾名思义,最后两个为所有元素累加和累积,但会化为dim=1数组
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对象都拥有两个重要属性:
- .grad属性保存所有.backward()梯度计算之后更新的梯度结果。通过设置.requires_grad=True,后续所有计算都会被记录和追踪。为了防止将来的计算被追踪,可以调用.detach()或者torch.no_grad(),这样未来创建新变量的requires_grad为False
- .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) # 不可训练参数(常数参数)
编写网络的两种方法:
- 自己定义一个nn.Module的子类,编写__init__和forward函数;
- 利用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),则
- 卷积之前,通过padding将矩阵扩充为$(8 + 2p_1, 8 + 2p_2)$
- 用卷积核卷积。若步长默认为1,则输出大小为$(8 + 2p_1 - (k_1-1), 8 + 2p_2 - (k_2-1))$
- 如果步长为$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为例)
- 循环C次,每个循环中取出一个通道$i$
- 得到一个切片$(N, H, W)$
- 将这个切片中的所有元素高斯标准化,大致转化到(0, 1)分布;实际上平均值和方差的确切值不一定为0和1,请看文档提及的技术细节。
- 将这个标准化后的切片作为输出张量的第$i$个通道
- 最终输出的张量大小为$(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。