torch.nn专门为深度学习而设计的模块。
torch.nn的核心数据结构是Module
,它是一个抽象概念,既可以表示神经网络中的某个层(layer),也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法是继承nn.Module
,撰写自己的网络层.
使用torch.nn实现全连接层
import torch from torch import nnfrom torch.autograd import Variable class Linear (nn.Module ): def __init__ (self, in_features, out_features ): super (Linear, self).__init__() self.w = nn.Parameter(torch.randn(in_features, out_features)) self.b = nn.Parameter(torch.randn(out_features)) def forward (self, x ): x = x.mm(self.w) return x + self.b.expand_as(x) layer = Linear(4 ,3 ) input = Variable(torch.randn(2 ,4 ))output = layer(input ) output
可见,全连接层的实现非常简单,其代码量不超过10行,但需注意以下几点:
自定义层Linear
必须继承nn.Module
,并且在其构造函数中需调用nn.Module
的构造函数,即super(Linear, self).__init__()
或nn.Module.__init__(self)
,推荐使用第一种用法,尽管第二种写法更直观。
在构造函数__init__
中必须自己定义可学习的参数,并封装成Parameter
,如在本例中我们把w
和b
封装成parameter
。parameter
是一种特殊的Variable
,但其默认需要求导(requires_grad = True),感兴趣的读者可以通过nn.Parameter??
,查看Parameter
类的源代码。
forward
函数实现前向传播过程,其输入可以是一个或多个variable,对x的任何操作也必须是variable支持的操作。
无需写反向传播函数,因其前向传播都是对variable进行操作,nn.Module能够利用autograd自动实现反向传播,这点比Function简单许多。
使用时,直观上可将layer看成数学概念中的函数,调用layer(input)即可得到input对应的结果。它等价于layers.__call__(input)
,在__call__
函数中,主要调用的是 layer.forward(x)
,另外还对钩子做了一些处理。所以在实际使用中应尽量使用layer(x)
而不是使用layer.forward(x)
,关于钩子技术将在下文讲解。
Module
中的可学习参数可以通过named_parameters()
或者parameters()
返回迭代器,前者会给每个parameter都附上名字,使其更具有辨识度。
可见利用Module实现的全连接层,比利用Function
实现的更为简单,因其不再需要写反向传播函数。
module中parameter的命名规范:
对于类似self.param_name = nn.Parameter(t.randn(3, 4))
,命名为param_name
对于子Module中的parameter,会其名字之前加上当前Module的名字。如对于self.sub_module = SubModel()
,SubModel中有个parameter的名字叫做param_name,那么二者拼接而成的parameter name 就是sub_module.param_name
。
为方便用户使用,PyTorch实现了神经网络中绝大多数的layer,这些layer都继承于nn.Module,封装了可学习参数parameter
,并实现了forward函数,阅读文档时应主要关注以下几点:
构造函数的参数,如nn.Linear(in_features, out_features, bias),需关注这三个参数的作用。
属性,可学习参数,子module。如nn.Linear中有weight
和bias
两个可学习参数,不包含子module。
输入输出的形状,如nn.linear的输入形状是(N, input_features),输出为(N,output_features),N是batch_size。
这些自定义layer对输入形状都有假设:输入的不是单个数据,而是一个batch。若想输入一个数据,则必须调用unsqueeze(0)
函数将数据伪装成batch_size=1的batch
常用神经网络层
图像相关层
图像相关层主要包括卷积层(Conv)、池化层(Pool)等,这些层在实际使用中可分为一维(1D)、二维(2D)、三维(3D),池化方式又分为平均池化(AvgPool)、最大值池化(MaxPool)、自适应池化(AdaptiveAvgPool)等。而卷积层除了常用的前向卷积之外,还有逆卷积(TransposeConv)。下面举例说明一些基础的使用。
from PIL import Imagefrom torchvision.transforms import ToTensor, ToPILImageto_tensor = ToTensor() to_pil = ToPILImage() lena = Image.open ('imgs/lena.png' ) input = to_tensor(lena).unsqueeze(0 ) kernel = torch.ones(3 , 3 )/-9. kernel[1 ][1 ] = 1 conv = nn.Conv2d(1 , 1 , (3 , 3 ), 1 , bias=False ) conv.weight.data = kernel.view(1 , 1 , 3 , 3 ) out = conv(Variable(input )) to_pil(out.data.squeeze(0 ))
池化层可以看作是一种特殊的卷积层,用来下采样。但池化层没有可学习参数,其weight是固定的。
pool = nn.AvgPool2d(2 ,2 ) list (pool.parameters())out = pool(Variable(input )) to_pil(out.data.squeeze(0 ))
除了卷积层和池化层,深度学习中还将常用到以下几个层:
Linear:全连接层。
BatchNorm:批规范化层,分为1D、2D和3D。除了标准的BatchNorm之外,还有在风格迁移中常用到的InstanceNorm层。
Dropout:dropout层,用来防止过拟合,同样分为1D、2D和3D。
input = Variable(torch.randn(2 , 3 ))linear = nn.Linear(3 , 4 ) h = linear(input )
bn = nn.BatchNorm1d(4 ) bn.weight.data = torch.ones(4 ) * 4 bn.bias.data = torch.zeros(4 ) bn_out = bn(h) bn_out.mean(0 ), bn_out.var(0 , unbiased=False )
dropout = nn.Dropout(0.5 ) o = dropout(bn_out)
以上很多例子中都对module的属性直接操作,其大多数是可学习参数,一般会随着学习的进行而不断改变。实际使用中除非需要使用特殊的初始化,应尽量不要直接修改这些参数。
激活函数
PyTorch实现了常见的激活函数,其具体的接口信息可参见官方文档,这些激活函数可作为独立的layer使用。这里将介绍最常用的激活函数ReLU
relu = nn.ReLU(inplace=True ) input = Variable(torch.randn(2 , 3 ))print(input ) output = relu(input ) print(output)
ReLU函数有个inplace参数,如果设为True,它会把输出直接覆盖到输入中,这样可以节省内存/显存。之所以可以覆盖是因为在计算ReLU的反向传播时,只需根据输出就能够推算出反向传播的梯度。但是只有少数的autograd操作支持inplace操作(如variable.sigmoid_()),除非你明确地知道自己在做什么,否则一般不要使用inplace操作。
在以上的例子中,基本上都是将每一层的输出直接作为下一层的输入,这种网络称为前馈传播网络(feedforward neural network)。对于此类网络如果每次都写复杂的forward函数会有些麻烦,在此就有两种简化方式,ModuleList和Sequential。其中Sequential是一个特殊的module,它包含几个子Module,前向传播时会将输入一层接一层的传递下去。ModuleList也是一个特殊的module,可以包含几个子module,可以像用list一样使用它,但不能直接把输入传给ModuleList。下面举例说明。
net1 = nn.Sequential() net1.add_module('conv' , nn.Conv2d(3 , 3 , 3 )) net1.add_module('batchnorm' , nn.BatchNorm2d(3 )) net1.add_module('activation_layer' , nn.ReLU()) net2 = nn.Sequential( nn.Conv2d(3 , 3 , 3 ), nn.BatchNorm2d(3 ), nn.ReLU() ) from collections import OrderedDictnet3= nn.Sequential(OrderedDict([ ('conv1' , nn.Conv2d(3 , 3 , 3 )), ('bn1' , nn.BatchNorm2d(3 )), ('relu1' , nn.ReLU()) ])) print('net1:' , net1) print('net2:' , net2) print('net3:' , net3) output: net1: Sequential( (conv): Conv2d (3 , 3 , kernel_size=(3 , 3 ), stride=(1 , 1 )) (batchnorm): BatchNorm2d(3 , eps=1e-05 , momentum=0.1 , affine=True ) (activation_layer): ReLU() ) net2: Sequential( (0 ): Conv2d (3 , 3 , kernel_size=(3 , 3 ), stride=(1 , 1 )) (1 ): BatchNorm2d(3 , eps=1e-05 , momentum=0.1 , affine=True ) (2 ): ReLU() ) net3: Sequential( (conv1): Conv2d (3 , 3 , kernel_size=(3 , 3 ), stride=(1 , 1 )) (bn1): BatchNorm2d(3 , eps=1e-05 , momentum=0.1 , affine=True ) (relu1): ReLU() )
net1.conv, net2[0 ], net3.conv1
input = Variable(torch.rand(1 , 3 , 4 , 4 ))output = net1(input ) output = net2(input ) output = net3(input ) output = net3.relu1(net1.batchnorm(net1.conv(input )))
ModuleList
是Module
的子类,当在Module
中使用它的时候,就能自动识别为子module。
class MyModule (nn.Module ): def __init__ (self ): super (MyModule, self).__init__() self.list = [nn.Linear(3 , 4 ), nn.ReLU()] self.module_list = nn.ModuleList([nn.Conv2d(3 , 3 , 3 ), nn.ReLU()]) def forward (self ): pass model = MyModule() model output: MyModule( (module_list): ModuleList( (0 ): Conv2d (3 , 3 , kernel_size=(3 , 3 ), stride=(1 , 1 )) (1 ): ReLU() ) )
list中的子module并不能被主module所识别,而ModuleList中的子module能够被主module所识别。这意味着如果用list保存子module,将无法调整其参数,因其未加入到主module的参数中。
除ModuleList之外还有ParameterList,其是一个可以包含多个parameter的类list对象。在实际应用中,使用方式与ModuleList类似。如果在构造函数__init__
中用到list、tuple、dict等对象时,一定要思考是否应该用ModuleList或ParameterList代替。
循环神经网络层(RNN)
近些年随着深度学习和自然语言处理的结合加深,RNN的使用也越来越多,关于RNN的基础知识,推荐阅读colah的文章[^4]入门。PyTorch中实现了如今最常用的三种RNN:RNN(vanilla RNN)、LSTM和GRU。此外还有对应的三种RNNCell。
RNN和RNNCell层的区别在于前者一次能够处理整个序列,而后者一次只处理序列中一个时间点的数据,前者封装更完备更易于使用,后者更具灵活性。实际上RNN层的一种后端实现方式就是调用RNNCell来实现的。
torch.manual_seed(1000 ) input = Variable(torch.randn(2 , 3 , 4 ))lstm = nn.LSTM(4 , 3 , 1 ) h0 = Variable(torch.randn(1 , 3 , 3 )) c0 = Variable(torch.randn(1 , 3 , 3 )) out, hn = lstm(input , (h0, c0)) out
torch.manual_seed(1000 ) input = Variable(torch.randn(2 , 3 , 4 ))lstm = nn.LSTMCell(4 , 3 ) hx = Variable(torch.randn(3 , 3 )) cx = Variable(torch.randn(3 , 3 )) out = [] for i_ in input : hx, cx=lstm(i_, (hx, cx)) out.append(hx) torch.stack(out)
词向量在自然语言中应用十分普及,PyTorch同样提供了Embedding层。
embedding = nn.Embedding(4 , 5 ) embedding.weight.data = torch.arange(0 ,20 ).view(4 ,5 )
input =Variable(torch.arange(3 , 0 , -1 )).long()output = embedding(input ) output
损失函数
在深度学习中要用到各种各样的损失函数(loss function),这些损失函数可看作是一种特殊的layer,PyTorch也将这些损失函数实现为nn.Module
的子类。然而在实际使用中通常将这些loss function专门提取出来,和主模型互相独立。详细的loss使用请参照文档,这里以分类中最常用的交叉熵损失CrossEntropyloss为例说明。
score = Variable(torch.randn(3 , 2 )) label = Variable(torch.Tensor([1 , 0 , 1 ])).long() criterion = nn.CrossEntropyLoss() loss = criterion(score, label) loss
优化器
PyTorch将深度学习中常用的优化方法全部封装在torch.optim
中,其设计十分灵活,能够很方便的扩展成自定义的优化方法。
所有的优化方法都是继承基类optim.Optimizer
,并实现了自己的优化步骤。下面就以最基本的优化方法——随机梯度下降法(SGD)举例说明。这里需重点掌握:
优化方法的基本使用方法
如何对模型的不同部分设置不同的学习率
如何调整学习率
class Net (nn.Module ): def __init__ (self ): super (Net, self).__init__() self.features = nn.Sequential( nn.Conv2d(3 , 6 , 5 ), nn.ReLU(), nn.MaxPool2d(2 ,2 ), nn.Conv2d(6 , 16 , 5 ), nn.ReLU(), nn.MaxPool2d(2 ,2 ) ) self.classifier = nn.Sequential( nn.Linear(16 * 5 * 5 , 120 ), nn.ReLU(), nn.Linear(120 , 84 ), nn.ReLU(), nn.Linear(84 , 10 ) ) def forward (self, x ): x = self.features(x) x = x.view(-1 , 16 * 5 * 5 ) x = self.classifier(x) return x net = Net()
from torch import optimoptimizer = optim.SGD(params=net.parameters(), lr=1 ) optimizer.zero_grad() input = Variable(torch.randn(1 , 3 , 32 , 32 ))output = net(input ) output.backward(output) optimizer.step()
optimizer =optim.SGD([ {'params' : net.features.parameters()}, {'params' : net.classifier.parameters(), 'lr' : 1e-2 } ], lr=1e-5 )
special_layers = nn.ModuleList([net.classifier[0 ], net.classifier[3 ]]) special_layers_params = list (map (id , special_layers.parameters())) base_params = filter (lambda p: id (p) not in special_layers_params, net.parameters()) optimizer = torch.optim.SGD([ {'params' : base_params}, {'params' : special_layers.parameters(), 'lr' : 0.01 } ], lr=0.001 )
对于如何调整学习率,主要有两种做法。一种是修改optimizer.param_groups中对应的学习率,另一种是更简单也是较为推荐的做法——新建优化器,由于optimizer十分轻量级,构建开销很小,故而可以构建新的optimizer。但是后者对于使用动量的优化器(如Adam),会丢失动量等状态信息,可能会造成损失函数的收敛出现震荡等情况。
old_lr = 0.1 optimizer =optim.SGD([ {'params' : net.features.parameters()}, {'params' : net.classifier.parameters(), 'lr' : old_lr*0.1 } ], lr=1e-5 )
nn.functional
nn中还有一个很常用的模块:nn.functional
,nn中的大多数layer,在functional
中都有一个与之相对应的函数。nn.functional
中的函数和nn.Module
的主要区别在于,用nn.Module实现的layers是一个特殊的类,都是由class layer(nn.Module)
定义,会自动提取可学习的参数。而nn.functional
中的函数更像是纯函数,由def function(input)
定义。下面举例说明functional的使用,并指出二者的不同之处。
input = Variable(torch.randn(2 , 3 ))model = nn.Linear(3 , 4 ) output1 = model(input ) output2 = nn.functional.linear(input , model.weight, model.bias) output1 == output2 output: tensor([[True , True , True , True ], [True , True , True , True ]])
b = nn.functional.relu(input ) b2 = nn.ReLU()(input ) b == b2 output: tensor([[True , True , True ], [True , True , True ]])
应该什么时候使用nn.Module,什么时候使用nn.functional呢?答案很简单,如果模型有可学习的参数,最好用nn.Module,否则既可以使用nn.functional也可以使用nn.Module,二者在性能上没有太大差异,具体的使用取决于个人的喜好。如激活函数(ReLU、sigmoid、tanh),池化(MaxPool)等层由于没有可学习参数,则可以使用对应的functional函数代替,而对于卷积、全连接等具有可学习参数的网络建议使用nn.Module。下面举例说明,如何在模型中搭配使用nn.Module和nn.functional。另外虽然dropout操作也没有可学习操作,但建议还是使用nn.Dropout
而不是nn.functional.dropout
,因为dropout在训练和测试两个阶段的行为有所差别,使用nn.Module
对象能够通过model.eval
操作加以区分。
from torch.nn import functional as Fclass Net (nn.Module ): def __init__ (self ): super (Net, self).__init__() self.conv1 = nn.Conv2d(3 , 6 , 5 ) self.conv2 = nn.Conv2d(6 , 16 , 5 ) self.fc1 = nn.Linear(16 * 5 * 5 , 120 ) self.fc2 = nn.Linear(120 , 84 ) self.fc3 = nn.Linear(84 , 10 ) def forward (self, x ): x = F.pool(F.relu(self.conv1(x)), 2 ) x = F.pool(F.relu(self.conv2(x)), 2 ) x = x.view(-1 , 16 * 5 * 5 ) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x
对于不具备可学习参数的层(激活层、池化层等),将它们用函数代替,这样则可以不用放置在构造函数__init__
中。对于有可学习参数的模块,也可以用functional来代替,只不过实现起来较为繁琐,需要手动定义参数parameter,如前面实现自定义的全连接层,就可将weight和bias两个参数单独拿出来,在构造函数中初始化为parameter。
class MyLinear (nn.Module ): def __init__ (self ): super (MyLinear, self).__init__() self.weight = nn.Parameter(t.randn(3 , 4 )) self.bias = nn.Parameter(t.zeros(3 )) def forward (self ): return F.linear(input , weight, bias)
初始化策略
在深度学习中参数的初始化十分重要,良好的初始化能让模型更快收敛,并达到更高水平,而糟糕的初始化则可能使得模型迅速瘫痪。PyTorch中nn.Module的模块参数都采取了较为合理的初始化策略,因此一般不用我们考虑,当然我们也可以用自定义初始化去代替系统的默认初始化。而当我们在使用Parameter时,自定义初始化则尤为重要,因t.Tensor()返回的是内存中的随机数,很可能会有极大值,这在实际训练网络中会造成溢出或者梯度消失。PyTorch中nn.init
模块就是专门为初始化而设计 ,如果某种初始化策略nn.init
不提供,用户也可以自己直接初始化。
from torch.nn import initlinear = nn.Linear(3 , 4 ) torch.manual_seed(1 ) init.xavier_normal(linear.weight)
import mathtorch.manual_seed(1 ) std = math.sqrt(2 )/math.sqrt(7. ) linear.weight.data.normal_(0 ,std)
for name, params in net.named_parameters(): if name.find('linear' ) != -1 : params[0 ] params[1 ] elif name.find('conv' ) != -1 : pass elif name.find('norm' ) != -1 : pass
nn.Module深入分析
如果想要更深入地理解nn.Module,究其原理是很有必要的。首先来看看nn.Module基类的构造函数:
def __init__ (self ): self._parameters = OrderedDict() self._modules = OrderedDict() self._buffers = OrderedDict() self._backward_hooks = OrderedDict() self._forward_hooks = OrderedDict() self.training = True
其中每个属性的解释如下:
_parameters
:字典,保存用户直接设置的parameter,self.param1 = nn.Parameter(t.randn(3, 3))
会被检测到,在字典中加入一个key为’param’,value为对应parameter的item。而self.submodule = nn.Linear(3, 4)中的parameter则不会存于此。
_modules
:子module,通过self.submodel = nn.Linear(3, 4)
指定的子module会保存于此。
_buffers
:缓存。如batchnorm使用momentum机制,每次前向传播需用到上一次前向传播的结果。
_backward_hooks
与_forward_hooks
:钩子技术,用来提取中间变量,类似variable的hook。
training
:BatchNorm与Dropout层在训练阶段和测试阶段中采取的策略不同,通过判断training值来决定前向传播策略。
上述几个属性中,_parameters
、_modules
和_buffers
这三个字典中的键值,都可以通过self.key
方式获得,效果等价于self._parameters['key']
.
import torchclass Net (nn.Module ): def __init__ (self ): super (Net, self).__init__() self.param1 = nn.Parameter(torch.rand(3 , 3 )) self.submodel1 = nn.Linear(3 , 4 ) def forward (self, input ): x = self.param1.mm(input ) x = self.submodel1(x) return x net = Net() net output: Net( (submodel1): Linear(in_features=3 , out_features=4 , bias=True ) ) net._modules output: OrderedDict([('submodel1' , Linear(in_features=3 , out_features=4 , bias=True ))]) net._parameters output: OrderedDict([('param1' , Parameter containing: 0.3398 0.5239 0.7981 0.7718 0.0112 0.8100 0.6397 0.9743 0.8300 [torch.FloatTensor of size 3x3])]) net.param1 output: Parameter containing: 0.3398 0.5239 0.7981 0.7718 0.0112 0.8100 0.6397 0.9743 0.8300 [torch.FloatTensor of size 3x3]
nn.Module在实际使用中可能层层嵌套,一个module包含若干个子module,每一个子module又包含了更多的子module。为方便用户访问各个子module,nn.Module实现了很多方法,如函数children
可以查看直接子module,函数module
可以查看所有的子module(包括当前module)。与之相对应的还有函数named_childen
和named_modules
,其能够在返回module列表的同时返回它们的名字。
input = Variable(torch.arange(0 , 12 ).view(3 , 4 ).float ())model = nn.Dropout() model(input )
model.training = False model(input )
对于batchnorm、dropout、instancenorm等在训练和测试阶段行为差距巨大的层,如果在测试时不将其training值设为True,则可能会有很大影响,这在实际使用中要千万注意。虽然可通过直接设置training
属性,来将子module设为train和eval模式,但这种方式较为繁琐,因如果一个模型具有多个dropout层,就需要为每个dropout层指定training属性。更为推荐的做法是调用model.train()
函数,它会将当前module及其子module中的所有training属性都设为True,相应的,model.eval()
函数会把training属性都设为False。
print(net.training, net.submodel1.training) net.eval () net.training, net.submodel1.training
list (net.named_modules())
register_forward_hook
与register_backward_hook
,这两个函数的功能类似于variable函数的register_hook
,可在module前向传播或反向传播时注册钩子。每次前向传播执行结束后会执行钩子函数(hook)。前向传播的钩子函数具有如下形式:hook(module, input, output) -> None
,而反向传播则具有如下形式:hook(module, grad_input, grad_output) -> Tensor or None
。钩子函数不应修改输入和输出,并且在使用后应及时删除,以避免每次都运行钩子增加运行负载。钩子函数主要用在获取某些中间结果的情景,如中间某一层的输出或某一层的梯度。这些结果本应写在forward函数中,但如果在forward函数中专门加上这些处理,可能会使处理逻辑比较复杂,这时候使用钩子技术就更合适一些。下面考虑一种场景,有一个预训练好的模型,需要提取模型的某一层(不是最后一层)的输出作为特征进行分类,但又不希望修改其原有的模型定义文件,这时就可以利用钩子函数。下面给出实现的伪代码。
model = VGG() features = t.Tensor() def hook (module, input , output ): '''把这层的输出拷贝到features中''' features.copy_(output.data) handle = model.layer8.register_forward_hook(hook) _ = model(input ) handle.remove()
nn.Module
对象在构造函数中的行为看起来有些怪异,如果想要真正掌握其原理,就需要看两个魔法方法__getattr__
和__setattr__
。在Python中有两个常用的buildin方法getattr
和setattr
,getattr(obj, 'attr1')
等价于obj.attr
,如果getattr
函数无法找到所需属性,Python会转而调用obj.__getattr__('attr1')
方法,即getattr
函数无法找到的交给__getattr__
函数处理,没有实现__getattr__
或者__getattr__
也无法处理的就会raise AttributeError。setattr(obj, 'name', value)
等价于obj.name=value
,如果obj对象实现了__setattr__
方法,setattr会直接调用obj.__setattr__('name', value)
,否则调用buildin方法。总结一下:
result = obj.name会调用buildin函数getattr(obj, 'name')
,如果该属性找不到,会调用obj.__getattr__('name')
obj.name = value会调用buildin函数setattr(obj, 'name', value)
,如果obj对象实现了__setattr__
方法,setattr
会直接调用obj.__setattr__('name', value')
nn.Module实现了自定义的__setattr__
函数,当执行module.name=value
时,会在__setattr__
中判断value是否为Parameter
或nn.Module
对象,如果是则将这些对象加到_parameters
和_modules
两个字典中,而如果是其它类型的对象,如Variable
、list
、dict
等,则调用默认的操作,将这个值保存在__dict__
中。
module = nn.Module() module.param = nn.Parameter(torch.ones(2 , 2 )) module._parameters
submodule1 = nn.Linear(2 , 2 ) submodule2 = nn.Linear(2 , 2 ) module_list = [submodule1, submodule2] module.submodules = module_list print('_modules: ' , module._modules) print("__dict__['submodules']:" ,module.__dict__.get('submodules' ))
module_list = nn.ModuleList(module_list) module.submodules = module_list print('ModuleList is instance of nn.Module: ' , isinstance (module_list, nn.Module)) print('_modules: ' , module._modules) print("__dict__['submodules']:" , module.__dict__.get('submodules' ))
因_modules
和_parameters
中的item未保存在__dict__
中,所以默认的getattr方法无法获取它,因而nn.Module
实现了自定义的__getattr__
方法,如果默认的getattr
无法处理,就调用自定义的__getattr__
方法,尝试从_modules
、_parameters
和_buffers
这三个字典中获取。
getattr (module, 'training' )
module.attr1 = 2 getattr (module, 'attr1' )
模型的保存
在PyTorch中保存模型十分简单,所有的Module对象都具有state_dict()函数,返回当前Module所有的状态数据。将这些状态数据保存后,下次使用模型时即可利用model.load_state_dict()
函数将状态加载进来。优化器(optimizer)也有类似的机制,不过一般并不需要保存优化器的运行状态。
torch.save(net.state_dict(), 'net.pth' ) net2 = Net() net2.load_state_dict(torch.load('net.pth' ))
实际上还有另外一种保存方法,但因其严重依赖模型定义方式及文件路径结构等,很容易出问题,因而不建议使用。
torch.save(net, 'net_all.pth' ) net2 = torch.load('net_all.pth' ) net2
使用GPU运算
将Module放在GPU上运行也十分简单,只需两步:
model = model.cuda():将模型的所有参数转存到GPU
input.cuda():将输入数据也放置到GPU上
至于如何在多个GPU上并行计算,PyTorch也提供了两个函数,可实现简单高效的并行GPU计算
nn.parallel.data_parallel(module, inputs, device_ids=None, output_device=None, dim=0, module_kwargs=None)
class torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
可见二者的参数十分相似,通过device_ids
参数可以指定在哪些GPU上进行优化,output_device指定输出到哪个GPU上。唯一的不同就在于前者直接利用多GPU并行计算得出结果,而后者则返回一个新的module,能够自动在多GPU上进行并行加速。
# method 1 new_net = nn.DataParallel(net, device_ids=[0, 1]) output = new_net(input) # method 2 output = nn.parallel.data_parallel(new_net, input, device_ids=[0, 1])
DataParallel并行的方式,是将输入一个batch的数据均分成多份,分别送到对应的GPU进行计算,各个GPU得到的梯度累加。与Module相关的所有数据也都会以浅复制的方式复制多份,在此需要注意,在module中属性应该是只读的。
nn和autograd的关系
nn.Module利用的也是autograd技术,其主要工作是实现前向传播。在forward函数中,nn.Module对输入的Variable进行的各种操作,本质上都是用到了autograd技术。这里需要对比autograd.Function和nn.Module之间的区别:
autograd.Function利用了Tensor对autograd技术的扩展,为autograd实现了新的运算op,不仅要实现前向传播还要手动实现反向传播
nn.Module利用了autograd技术,对nn的功能进行扩展,实现了深度学习中更多的层。只需实现前向传播功能,autograd即会自动实现反向传播
nn.functional是一些autograd操作的集合,是经过封装的函数
作为两大类扩充PyTorch接口的方法,我们在实际使用中应该如何选择呢?如果某一个操作,在autograd中尚未支持,那么只能实现Function接口对应的前向传播和反向传播。如果某些时候利用autograd接口比较复杂,则可以利用Function将多个操作聚合,实现优化,正如Sigmoid
一样,比直接利用autograd低级别的操作要快。而如果只是想在深度学习中增加某一层,使用nn.Module进行封装则更为简单高效。
搭建ResNet
Kaiming He的深度残差网络(ResNet)[^7]在深度学习的发展中起到了很重要的作用,ResNet不仅一举拿下了当年CV下多个比赛项目的冠军,更重要的是这一结构解决了训练极深网络时的梯度消失问题。
首先来看看ResNet的网络结构,这里选取的是ResNet的一个变种:ResNet34。ResNet的网络结构如图所示,可见除了最开始的卷积池化和最后的池化全连接之外,网络中有很多结构相似的单元,这些重复单元的共同点就是有个跨层直连的shortcut。ResNet中将一个跨层直连的单元称为Residual block,其结构如图所示,左边部分是普通的卷积网络结构,右边是直连,但如果输入和输出的通道数不一致,或其步长不为1,那么就需要有一个专门的单元将二者转成一致,使其可以相加。
另外我们可以发现Residual block的大小也是有规律的,在最开始的pool之后有连续的几个一模一样的Residual block单元,这些单元的通道数一样,在这里我们将这几个拥有多个Residual block单元的结构称之为layer,注意和之前讲的layer区分开来,这里的layer是几个层的集合。
考虑到Residual block和layer出现了多次,我们可以把它们实现为一个子Module或函数。这里我们将Residual block实现为一个子moduke,而将layer实现为一个函数。下面是实现代码,规律总结如下:
对于模型中的重复部分,实现为子module或用函数生成相应的modulemake_layer
nn.Module和nn.Functional结合使用
尽量使用nn.Seqential
from torch import nnimport torch from torch.nn import functional as Fclass ResidualBlock (nn.Module ): ''' 实现子module: Residual Block ''' def __init__ (self, inchannel, outchannel, stride=1 , shortcut=None ): super (ResidualBlock, self).__init__() self.left = nn.Sequential( nn.Conv2d(inchannel,outchannel,3 ,stride, 1 ,bias=False ), nn.BatchNorm2d(outchannel), nn.ReLU(inplace=True ), nn.Conv2d(outchannel,outchannel,3 ,1 ,1 ,bias=False ), nn.BatchNorm2d(outchannel) ) self.right = shortcut def forward (self, x ): out = self.left(x) residual = x if self.right is None else self.right(x) out += residual return F.relu(out) class ResNet (nn.Module ): ''' 实现主module:ResNet34 ResNet34 包含多个layer,每个layer又包含多个residual block 用子module来实现residual block,用_make_layer函数来实现layer ''' def __init__ (self, num_classes=1000 ): super (ResNet, self).__init__() self.pre = nn.Sequential( nn.Conv2d(3 , 64 , 7 , 2 , 3 , bias=False ), nn.BatchNorm2d(64 ), nn.ReLU(inplace=True ), nn.MaxPool2d(3 , 2 , 1 )) self.layer1 = self._make_layer( 64 , 64 , 3 ) self.layer2 = self._make_layer( 64 , 128 , 4 , stride=2 ) self.layer3 = self._make_layer( 128 , 256 , 6 , stride=2 ) self.layer4 = self._make_layer( 256 , 512 , 3 , stride=2 ) self.fc = nn.Linear(512 , num_classes) def _make_layer (self, inchannel, outchannel, block_num, stride=1 ): ''' 构建layer,包含多个residual block ''' shortcut = nn.Sequential( nn.Conv2d(inchannel,outchannel,1 ,stride, bias=False ), nn.BatchNorm2d(outchannel)) layers = [] layers.append(ResidualBlock(inchannel, outchannel, stride, shortcut)) for i in range (1 , block_num): layers.append(ResidualBlock(outchannel, outchannel)) return nn.Sequential(*layers) def forward (self, x ): x = self.pre(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = F.avg_pool2d(x, 7 ) x = x.view(x.size(0 ), -1 ) return self.fc(x) model = ResNet() input = torch.autograd.Variable(torch.randn(1 , 3 , 224 , 224 ))o = model(input )
from torchvision import modelsmodel = models.resnet34()