PyTorch 101, part 3:使用PyTorch,让网络模型更深
在这篇教程中,我们将深入挖掘PyTorch的函数,并会涉及到高级的任务,比如使用不同的学习率,学习率的策略和不同权重初始化等等。
读者们你们好,这是我们PyTorch教程的另外一篇文章。这篇文章适合那些有了PyTorch基础的、以及想要提高PyTorch水平的读者。尽管在前面的文章中我们已经涉及到如何实现一个基本的分类器了,但是,在这篇文章中,我们将会讨论如何使用PyTorch去实现更为复杂的深度学习功能。为了让你更好理解,下面列出了这篇文章的要达成的目的:
- PyTorch类和类之间有哪些不同,比如nn.Module, nn.Functional, nn.Parameter,以及什么时候该使用哪一个。
- 如何定制你自己的训练选项,比如不用网络层有不同的学习率,不同学习率的策略。
- 设置权重初始化
目录
PyTorch 101, part 3:使用PyTorch,让网络模型更深
4 nn.ModuleList和nn.ParameterList
1 nn.Module vs nn.Functional
这些类都是经常会遇到的,特别是当你读一些开源代码的时候。在PyTorch中,网络层经常作为torch.nn.Module实例对象或者是torch.nn.Funtional函数来实现。该使用哪一个呢?哪一个更好呢?
我们已经在第二部分讲过了,torch.nn.Module是PyTorch基础。当你第一次定义一个nn.Module实例对象的时候,之后它自己会调用forward()方法去运行。这是一种面向对象的思想。
另一方面,nn.functional以函数的形式,提供了很多网络层或者是激活函数,因此它可以直接在输入上调用,而不需要定义一个对象。举个例子,如果想要去调整一个图像tensor的尺寸,你可以调用在图像tensor上调用torch.nn.functional.interpolate方法。
所以,当我们使用它们的时候,我们该选择哪一个呢?什么时候我们实现的网络层、激活函数和损失函数含有损失呢?
2 理解网络的状态
一般来说,任何层都可以看做是一个函数。举例来说,一个卷积操作仅仅是一系列的乘法和加法运算。因此,对于我们来说,仅仅把它理解成是一个函数来实现吗?但是,网络层需要保存权重以及当我们训练的时候,需要进行更新。因此,从一个编程的角度来讲,网络层不仅仅是一个函数。它同样也需要保存数据,当我们训练网络的时候,它会随之发生变化。
现在,我希望你去重视这个事实:当我们卷积层变化的时候,数据会被保存。这意味着,当我们进行训练的时候,网络层发生了变化。对于我们来说去实现一个可以进行卷积操作的函数,我们还需要去额外定义一个数据结构来单独保存网络层的权重。然后让这个额外的数据结构作为我们函数的输入。
或者,为了避免麻烦,我们可以仅仅定义一个类去保存数据结构,并且让卷积运算作为类的成员函数。这确实会减轻我们的工作量,因为我们不需要去担心函数之外的变量状态。在这些情况下,我们更倾向于使用nn.Module实例对象,我们有权重或者有其他状态,它或许定义网络层的某些行为。具体来说,dropout网络层和BN网络层在训练和推理的时候有着不同的行为。
另一方面,没有权重或者状态的时候,我们可以使用nn.functional。举例来说,比如resizing(可以调用nn.functional.interpolate函数),平均池化(可以调用nn.functional.AvgPool2d函数)。
除了上面的原因,大部分的nn.Module类都有他们对应的nn.functional函数。但是,在实际工作中,应当遵循上面的原理。
综上所述,当我们需要保存网络层的权重和状态的时候,就需要使用nn.Module类;如果我们仅仅是进行一些变换的话,我们使用nn.functional类就可以了。
3 nn.Parameter
PyTorch中一个重要的类是nn.Parameter类,出乎我们意料,在PyTorch介绍的文章中却很少涉及。考虑一下下面的情况:
class net(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Linear(10,5)
def forward(self, x):
return self.linear(x)
myNet = net()
#prints the weights and bias of Linear Layer
print(list(myNet.parameters()))
每个nn.Module都有一个parameters()的函数,它返回的是可训练的参数。我们需要显示地定义参数。在定义nn.Conv2d的时候,PyTorch的创造者定义了权重和偏置为该网络层的参数。但是,需要注意一件事,当我们定义net的时候,我们并不需要将nn.Conv2d的parameters添加到net的parameters中。它天生就有将nn.Conv2d实例对象设置为net实例对象成员变量的优势(也就是说PyTorch自己会将nn.Conv2d的参数加载到net的参数中,并不需要程序员添加)。
这是nn.Parameter类内部的实现,nn.Parameter类是Tensor的子类。当我们调用nn.Module对象的parameters()函数的时候,它会返回nn.Parameter对象的成员变量。
事实上,所有的nn.Module类的训练权重都是作为nn.Parameter实例对象实现的。无论何时,一个nn.Module(在我们的例子中是nn.Conv2d)被分配为另一个nn.Module的成员变量,分配的实例对象的 "parameters"(也就是nn.Conv2d的权重)也要添加到被分配的那个实例对象的”parameters"中(也就是nn.Conv2d的参数要添加到net网络中)。这叫做是nn.Module的注册的"parameters"。
如果你试图去给nn.Module实例对象分配一个张量的,它不会在parameters()中显示,除非你把它定义为nn.Parameter的实例对象。你可以在你需要去保存一个不可微的张量的时候去这样做,比如在RNNs中保存上一层的输出。
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()))
4 nn.ModuleList和nn.ParameterList
我记得当我使用PyTorch实现YOLOV3的教程中,我已经使用了nn.ModuleList这个类。我通过解析包含YOLOV3网络架构的文本文件去构建了这个网络。我在PyTorch列表中保存所有的nn.Module实例对象,然后让这个一系列的nn.Module模块表示这个网络。
简化一下,就像这样:
layer_list = [nn.Conv2d(5,5,3), nn.BatchNorm2d(5), nn.Linear(5,2)]
class myNet(nn.Module):
def __init__(self):
super().__init__()
self.layers = layer_list
def forward(x):
for layer in self.layers:
x = layer(x)
net = myNet()
print(list(net.parameters())) # Parameters of modules in the layer_list don't show up.
# 输出结果就是: []
如你所见,这和我们单独注册模块的结果不一样,PyTorch并不会注册Python列表中的Modules参数(也就是说,PyTorch不会保存list中的Modules的parameters)。为了解决这个问题,我们用nn.ModuleList类包裹我们的列表,然后就可以将其变为网络类的成员。
layer_list = [nn.Conv2d(5,5,3), nn.BatchNorm2d(5), nn.Linear(5,2)]
class myNet(nn.Module):
def __init__(self):
super().__init__()
# 我们需要将list对象转换成ModuleList对象类型
# 这是因为我们PyTorch不会保存list对象的parameters
self.layers = nn.ModuleList(layer_list)
def forward(x):
for layer in self.layers:
x = layer(x)
net = myNet()
print(list(net.parameters())) # Parameters of modules in layer_list show up.
# 注意:如果输出的时候没有list,那么就不会将网络的参数打印出来,只会打印内存地址
# <generator object Module.parameters at 0x0000026581AA1830>
相似的,一系列的张量数据也可以通过nn.ParameterList类包裹进行注册。
补充一点:nn.ModuleList和nn.Sequential还是有区别的:
- nn.ModuleList类仅仅是保存module的容器,module在其中存放是没有顺序的,而且它并没有实现forward()函数。这意味着如果我们不在网络的forward()函数中指定数据在module中传递的顺序的话,将不会正确执行的。我们可以使用append()函数,将模块添加到nn.ModuleList容器中
- nn.Sequential类则是按照顺序存放的。我们只需要将数据输入到它的实例对象中,PyTorch会自动给定数据在Sequential容器中传递的先后顺序的。并且,我们可以使用add.module(name, module)函数来将模块添加到nn.Sequential容器中。
5 权重初始化
初始化权重可以影响训练的结果。再者,你可能要为不同类型的网络层分配不同的初始化策略。这可以通过modules和apply函数实现。modules是nn.Module类的成员函数,它返回一个包含nn.Module函数的所有的nn.Module成员变量的迭代器。之后可以在每个nn.Module上调用去使用apply函数,来进行初始化。
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
# reshape(-1)是将数据变成行向量
weights = weights.reshape(-1).detach().cpu().numpy()
print(module.bias) # Bias to zero
plt.hist(weights)
plt.show()
可以在torch.nn.init模块中找到很多的初始化函数。
6 modules() vs children()
modules和children函数非常相似。虽然它们之前的差异很小,但是很重要。众所周知,一个nn.Module实例对象可以包含其他的nn.Module实例对象作为它的成员。
当children调用的时候,children()只会返回一系列的nn.Module实例对象,它们只是实例对象的数据成员。
另一方面,nn.Modules递归地的进入每个nn.Module实例对象中,创建每个实例对象的列表,直到不存在nn.module实例对象为止。注意,modules()也会返回已经是列表一部分的nn.Module。
注意,上面的情况使用与所有继承nn.Module类的实例对象或者类。
class myNet(nn.Module):
def __init__(self):
super().__init__()
self.convBN = nn.Sequential(nn.Conv2d(10,10,3), nn.BatchNorm2d(10))
self.linear = nn.Linear(10,2)
def forward(self, x):
pass
Net = myNet()
print("Printing children\n------------------------------")
print(list(Net.children()))
print("\n\nPrinting Modules\n------------------------------")
print(list(Net.modules()))
# 上面的输出结果是:
Printing children
------------------------------
[Sequential(
(0): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
(1): BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
), Linear(in_features=10, out_features=2, bias=True)]
Printing Modules
------------------------------
[myNet(
(convBN): Sequential(
(0): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
(1): BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(linear): Linear(in_features=10, out_features=2, bias=True)
), Sequential(
(0): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
(1): BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
), Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1)), BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True), Linear(in_features=10, out_features=2, bias=True)]
从上面的输出结果我们可以看到:
- Net.children()函数只会返回这个网络架构的第一层成员变量。
- Net.modules()函数会递归进入到每层成员变量然后进行输出。第一层:它会先打印Sequential(convBN, linear);第二层:它会打印Sequential(Conv2d,BN);第三层:它最后会打印Conv2d这个网络层。
因此,当我们初始化权重的时候,我们或许会使用modules()函数,因为我们无法进入到nn.Sequential实例对象内容,以及为它的成员进行初始化权重。
7 打印网络的信息
我们可能需要去打印网络的信息,无论是给使用者或者是出于调试的目的。通过使用它的named_*函数,PyTorch给我们提供了非常简洁的方式去打印这个网络的信息,这里有四个函数:
- named_parameters 返回一个迭代器,他给出了包含参数名称的元组(如果一个卷积层分配为self.conv1,那么他的参数就是conv1.weight和conv1.bias)和nn.Parameter的__repr__函数返回的值。
- named_modules 和上面类似,但是迭代器返回的模型和modules()函数返回的一样。
- named_children 和上面类似,但是迭代器返回的模型和children()函数返回的一样。
- named_buffers 返回缓存张量,比如批归一化层的运行平均值。
# 等价于:for x in Net.modules():
for x in Net.named_modules():
print(x[0], x[1], "\n-------------------------------")
# 输出结果
myNet(
(convBN): Sequential(
(0): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
(1): BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(linear): Linear(in_features=10, out_features=2, bias=True)
)
convBN Sequential(
(0): Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
(1): BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
convBN.0 Conv2d(10, 10, kernel_size=(3, 3), stride=(1, 1))
convBN.1 BatchNorm2d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
linear Linear(in_features=10, out_features=2, bias=True)
8 不同的网络层,不同的学习率
在这个部分,我们将会学习如何给不同的网络层使用不同的学习率。通常来说,我们将会涉及如何让不同组的参数有着不同的超参数,不管是不同网络层的不同学习率,异或是偏置和权重有着不同的学习率。
实现这个相符是非常容易的一件事。在我们之前的文章中,我们实现了CIFAR分类器,我们将网络的所有参数作为一个整体,传给了优化器实例对象。
class myNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(10,5)
self.fc2 = nn.Linear(5,2)
def forward(self, x):
return self.fc2(self.fc1(x))
Net = myNet()
optimiser = torch.optim.SGD(Net.parameters(), lr = 0.5)
然而,torch.optim类允许我们用词典的形式提供不同学习率的不同参数集合。
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,momentum为0.99。如果没有为一组参数初始化超参数(比如fc2),那么它就会使用默认超参数的值,作为优化器函数的输入参数(此时因为没有为fc2初始化参数,那么它的就使用默认的数值)。你也可以创建不同网络层的偏置参数列表,或者是使用上面我们讲过的named_parameters()函数创建权重参数或者是偏置参数。
9 学习率策略
设计你学习率是你需要调整的一个重要的参数。PyTorch使用它的torch.optim.lr_scheduler模块来支持规划你的学习率,这个模块有各种不同的学习策略。下面的例子显示了这样的一个例子:
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimiser, milestones = [10,20], gamma = 0.1)
上面的规划器中,当我们到达milestones列表中的epochs的时候,学习率每次都会乘以gamma。在我们的案例中,在第10个epoch和第20个epoch的时候,学习率将会乘以0.1,。在代码中,你还要在每个epochs的循环中写上secheduler.step这行代码,好让学习率可以更新。
通常来说,训练循环由两个嵌套的循环构成,一个循环遍历epochs,另个在epoch中遍历批次。要确保在epoch循环的开始调用scheduler.step函数,好让你的学习率可以更新。小心不要把代码写在批次循环中,否则你的学习率可能会在第10个批次而不是第10个epoch中进行更新,你可以参考这样的布局:
for epoch,(data,label) in enumerate(trainSet):
Net.train()
# step1:
scheduler.step()
for batch in batches:
...
loss.backward()
# step2:
optimiser.step()
还需要注意,scheduler.step函数不能代替optim.step,并且你需要都需要在反向传播中调用optim.step函数(这个在批次循环中)。
10 保存你的模型
为了后面推理,你或许想要保存你的模型,或者仅仅想创建训练检查点。要在PyTorch重保存模型涉及到两个选项。
第一个是使用torch.save。这个等价于使用Pickle去序列化整个nn.Module实例对象。它将整个模型保存在磁盘中。你稍后可以使用torch.load去从内存中加载这个模型。
torch.save(Net, "net.pth")
Net = torch.load("net.pth")
print(Net)
上面的代码会保存整个模型的权重和架构。如果你仅仅需要保存权重而不是保存整个模型,你可以只使用模型的state_dict函数。这个static_dict是一歌词典,它将nn.Parameter一个网络的实例对象映射成它的值。
如上所述,可以将存在的state_dict加载到一个nn.Module实例对象中。注意,这不会保存整个模型而只会保存参数。在你加载这个state_dict之前,你需要创建这个网络模型。如果这个网络架构和你保存的state_dict的不同,PyTorch将会抛出异常。
torch.save(Net.state_dict(), "net_state_dict.pth")
Net.load_state_dict(torch.load("net_state_dict.pth"))
torch.optim的优化器也有一个state_dict实例对象,它用来保存优化算法的超参数。它也可以用上面我们在优化器实例对象调用的load_state_dict那样保存和加载。
11 总结
在这篇文章中,我们将重点放在了如何构建一个网络层数更深、更加复杂的网络架构。
- 在构建网络层的时候:你可以使用nn.Module或者使用nn.functional两这个类。
- 在保存网络层的时候:你可以使用nn.ModuleList或者nn.Sequential这两个容器。
- 网络层不同,学习率不同:你可以设定整个网络层的学习率是一样的,也可以设定不同的网络层拥有不同的学习率。
- epoch不同,学习率不同:你可以设定整个epochs的学习率是一样的,当然不同的epoch可以使用不同的学习率。
- 保存和加载:你可以将整个模型以及参数保存下来,当然你也可以值保存参数,然后构建网络模型实例对象,然后将参数加载到这个实例对象中。
我们完成了PyTorch的更高级的特征的讨论。我希望这篇文章能够帮助你实现你思考出的复杂的深度学习架构。