以下内容是翻译自papersapce上的Pytorch教程系列,目的是为了自己学习时候记录进度督促自己,另一方面也可供需要的人汲取。
PyTorch 101 第三部分:深入了解Pytorch
这篇文章是针对熟悉PyTorch基础并且想要继续学习的人。在上一篇文章里我们已经介绍了如何实现一个最基本的分类网络,接下来我们将讨论如何用pytorch实现更复杂的深度学习功能。这篇文章的目标是让你明白:
- nn.Module,nn.Functional,nn.Parameter之间的区别以及什么时候去使用哪一个。
- 如何写自己的训练机制,如不同层配置不同学习率,学习率调节策略。
- 权重初始化。
开始之前,请记住这是PyTorch系列的第三篇。
可以在这里获取所有代码。
nn.Module VS nn.Functional
这是非常有用的东西,尤其是阅读源码时。在pytorch中,经常使用torch.nn.Module类或者torch.nn.Functional函数来实现layers,我们应该用哪一个?哪一个更好呢?
正如我们在第二部分所讲,torch.nn.Module是pytorch的基石,使用它的方法就是先定义一个nn.Module类,然后调用它的forward方法,这是面向对象的方法。
nn.Functional以函数的形式提供了一些层/激活函数,可以被直接调用不需要定义类。例如,想要调整图像大小,可以直接调用torch.nn.functional.interpolate。
因此,我们该如何选择使用哪一个?
理解状态
通常,所有的层都可以认为是一个函数,例如卷积操作就是一堆乘法和加法操作,因此可以将其认为是一个函数,但是卷积层包含了在训练过程中需要保存和更新的权重,所以从程序角度来看,这个层不仅仅是一个函数,它需要保存数据,在训练网络时需要改变。
我希望你能明白的是,卷积层保存的数据需要改变,这意味着在训练时,这个层有状态改变。对于我们而言,如果实现一个完成卷积操作的函数时,我们还需要定义一个数据结构来分别保存这一层的权重信息。
为了避免麻烦,我们可以只定义一个类来保存数据结构,卷积操作是这个类的成员方法,这将会简化我们的工作,因为我们不需要担心函数外部存在的有状态变量。因此,如果一层有权重或者定义了其他的状态,我们建议使用nn.Module类,例如dropout/Batch Norm层在训练和测试是状态不同。
另一方面,如果没有状态或者权重,可以使用nn.functional,例如nn.functional.interpolate,nn.functional.AvgPool2d。
尽管有上述的推理,但是大多数nn.Module类都有其对应的nn.functional,然而,在实际工作中依然应该遵循上述推理。
nn.Parameter
PyTorch中另一个重要的类就是nn.Parameter,但是在PyTorch文档中鲜有介绍。
每一个nn.Module都有一个parameters()函数返回,也是可训练参数。我们需要隐式地定义这些参数。在nn.Conv2d的定义中,作者定义了相应的权重和偏置参数,然而当我们定义net时,我们不需要把nn.Conv2d的参数加到net的参数中,这一切通过将nn.Conv2d对象设置成net类的成员完成。
这是由nn.Parameter类在内部促成的,它是Tensor类的子类。当我们调用一个nn.Module对象的parameters()函数时,它返回nn.Parameters对象的成员。
事实上,nn.Module类的所有权重都是实现nn.Parameter对象,无论何时,nn.Module(在我们的例子中为nn.Conv2d)被指定为另一个nn.Module的成员,被指定对象的“参数”(即nn.Conv2d的权重)也被添加到被分配给的对象(网络对象的参数)“参数”中。这称为nn.Module的“参数”注入。
如果尝试将张量分配给nn.Module对象,除非将其定义为nn.Parameter对象,否则它不会显示在parameters()中。这样做是为了便于可能需要缓存不可微分张量的情况,例如,在RNN的情况下缓存先前的输出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class net1(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Linear(10,5)
self.tens = torch.ones(3,4)
# This won't show up in a parameter list
def forward(self, x):
return self.linear(x)
myNet = net1()
print(list(myNet.parameters()))
##########################################################
class net2(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Linear(10,5)
self.tens = nn.Parameter(torch.ones(3,4))
# This will show up in a parameter list
def forward(self, x):
return self.linear(x)
myNet = net2()
print(list(myNet.parameters()))
##########################################################
class net3(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Linear(10,5)
self.net = net2()
# Parameters of net2 will show up in list of parameters of net3
def forward(self, x):
return self.linear(x)
myNet = net3()
print(list(myNet.parameters()))
nn.ModuleList 和 nn.ParameterList()
当用PyTorch实现YOLO v3时必须使用nn.ModuleList,因为需要解析一个包含网络结构的文本文档来搭建网络,将所有的nn.Module对象存在一个列表中,然后使这个列表成为nn.Module的成员。简化起来就像下面这样。
如你所见,这与我们注入单个模块不同,分配Python列表不会注入参数。为了解决这个问题,我们用nn.ModuleList类来包裹列表,然后作为一个成员分配到网络中。
类似的,Tensors列表也可以通过包裹nn.ParameterList类来注入。
权重初始化
权重初始化会影响训练的结果,此外,不同层可能需要不同的初始化策略,这些都可以通过modules和apply函数实现,modules是nn.Module类的成员函数,返回一个包含nn.Module函数所有的nn.Module成员的迭代器,然后使用apply函数调用每个nn.Module来初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import matplotlib.pyplot as plt
%matplotlib inline
class myNet(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv2d(10,10,3)
self.bn = nn.BatchNorm2d(10)
def weights_init(self):
for module in self.modules():
if isinstance(module, nn.Conv2d):
nn.init.normal_(module.weight, mean = 0, std = 1)
nn.init.constant_(module.bias, 0)
Net = myNet()
Net.weights_init()
for module in Net.modules():
if isinstance(module, nn.Conv2d):
weights = module.weight
weights = weights.reshape(-1).detach().cpu().numpy()
print(module.bias) # Bias to zero
plt.hist(weights)
plt.show()
在torch.nn.init模块中可以找到大量的初始化函数。
modules() VS children()
与modules非常相似的函数是children,它们的差别很小但是很重要。我们知道,一个nn.Module对象可以包含其他nn.Module对象做为成员。
当调用children时,children()只会返回nn.Module对象的数据成员列表。
另一方面,nn.Modules在每一个nn.Module对象里递归,创建每个nn.Module对象的列表,直到没有nn.Module为止。注意,modules()还返回已作为列表一部分调用的nn.Module
注意,对于从nn.Module类继承的所有对象/类,上述语句仍然适用。
因此,当我们初始化权重时使用modules()函数,因为我们无法进入nn.Sequential对象内部并初始化其成员的权重。
打印网络信息
无论是处于用户的目的还是调试,我们可能需要打印网络信息,PyTorch提供了一个非常巧妙的方式来打印网络的信息通过使用named_*函数。
- named_parameters,返回一个元组迭代器,包含参数的名字(如果一个卷积层被定义为self.conv1,参数名字就会是conv1.weight和conv1.bias),参数值通过nn.Parameter的__repr__函数返回。
- named_modules,和上一个一样,但是返回modules,和modules()函数一样。
- named_children,和上一个一样,但是返回modules,和children()函数一样。
- named_buffers,返回缓冲区张量,例如BN层的平均值。
为不同层设置不同学习率
在本节中,我们将学习如何为不同的层使用不同的学习率。一般而言,我们将介绍如何针对不同的参数组设置不同的超参数,要么是不同层的学习率不同,要么是偏差和权重的学习率不同。
实现这样的想法相当简单。在我们之前的文章中,我们实现了CIFAR分类器,我们将网络的所有参数作为一个整体传递给了优化器对象。
然而,torch.optim类允许我们以字典的形式为不同的参数集设置不同学习率。
1
2
optimiser = torch.optim.SGD([{"params": Net.fc1.parameters(), 'lr' : 0.001, "momentum" : 0.99},
{"params": Net.fc2.parameters()}], lr = 0.01, momentum = 0.9)
在上面的列子中,fc1的参数使用0.01的学习率和0.99的动量,如果没有为一组参数设置超参数,它们就会使用默认值。可以使用上面介绍的named_parameters()函数,根据不同的层创建参数列表,或者权重和偏差。
学习率调节策略
学习率是主要调节的参数,PyTorch提供学习率调节机制torch.optim.lr_scheduler模块,有许多不同的调节方法。
1
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimiser, milestones = [10,20], gamma = 0.1)
当我们达到epochs列表中包含的时期时,上述程序将学习率乘以$gamma$。在我们的例子中,学习率在第10和第20个时期乘以0.1。您还必须在代码中循环执行scheduler.step,以便更新学习速率。
通常,训练循环由两个嵌套循环组成,其中一个循环遍历epochs,嵌套的循环遍历该epochs的所有batch。确保你在epoch循环开始时调用scheduler.step,这样你的学习率会更新,小心不要写进batch循环中,否则你的学习率会在第10个batch更新,而不是第10个epoch。
还需要知道scheduler.step不能替代optim.step,需要在反向传播时调用optim.step。
保存模型
您可能希望保存模型以便用于推理。在PyTorch中保存模型时,有两种选择。
- 使用torch.save,这相当于使用Pickle序列化整个nn.Module对象,这会将整个模型保存到磁盘,你可以使用torch.load稍后在内存中加载这个模型。
上面的语句将会保存整个模型的权重和结构,如果只需要保存权重而不是整个网络,你可以保存模型的state_dict。state_dict是一个字典,它将网络的nn.Parameter对象映射到它们的值。
如上所示,可以将现有的state_dict加载到nn.Module对象中。注意,这不保存整个模型而只保存参数。在加载state_dict之前,您必须使用层创建网络。如果网络结构与我们保存的state_dict的网络结构不完全相同,PyTorch将抛出错误。
来自torch.optim的优化器对象还有一个state_dict对象,用于存储优化算法的超参数。它可以通过在优化器对象上调用load_state_dict以与上面类似的方式保存和加载。
总结
这完成了我们对PyTorch的一些更高级功能的讨论。我希望你在这篇文章中看到的内容可以帮助你实现你可能想出的复杂深层学习的想法。