autograd实现了自动微分系统,然而对深度学习来说过于底层,本章将介绍的nn模块,是构建于autograd之上的神经网络模块。除了nn之外,我们还会介绍神经网络中常用的工具,比如优化器optim、初始化init等。
第3章中提到,使用autograd可实现深度学习模型,但其抽象程度较低,如果用其来实现深度学习模型,则需要编写的代码量极大。在这种情况下,torch.nn应运而生,其是专门为深度学习设计的模块。torch.nn的核心数据结构是Module,它是一个抽象的概念,既可以表示神经网络中的某个层(layer),也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法继承nn.Module,撰写自己的网络/层。下面先来看看如何使用nn.Module实现自己的全连接层。全连接层,又名仿射层,输入y和输入x满足y=Wx+b,W和b是可学习的参数。
import torch as t from torch import nn from torch.autograd import Variable as V # 定义线性模型:y = w * x + b class Linear(nn.Module): # 继承nn.Module def __init__(self,in_features,out_features): super(Linear,self).__init__() # 等价于nn.Module.__init__(self) self.w = nn.Parameter(t.randn(in_features,out_features)) self.b = nn.Parameter(t.randn(out_features)) def forward(self,x): xw = x.mm(self.w) y = xw + self.b.expand_as(xw) return y net = Linear(4,3) x = V(t.randn(2,4)) y = net(x) y输出如下:
tensor([[ 0.2732, 1.8660, 1.3620], [ 0.6374, -0.3646, 1.0089]], grad_fn=<AddBackward0>) for name,parameter in layer.named_parameters(): print(name,parameter) # w and b输出如下:
w Parameter containing: tensor([[-0.3146, 2.5440, 0.0063], [ 0.6632, -1.5358, 0.1820], [-1.5990, 0.7136, 0.2463], [-1.8826, 1.4418, 0.7892]], requires_grad=True) b Parameter containing: tensor([ 0.1779, 0.2043, -0.3796], requires_grad=True)可见,全连接层的实现非常简单,其代码量不超过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简单许多。使用时,直观上可将net看成数学概念中的函数,调用net(x)即可得到x对应的结果。它等价于net.call(x),在__call__函数中,主要调用的是net.forward(x),另外还对钩子做了一些处理。所以在实际使用中应尽量使用net(x)而不是使用net.forward(x),关于钩子技术的具体内容将在下文讲到。Module中的可学习参数可以通过named_parameters()或者parameters()返回迭代器,前者会给每个parameter附上名字,使其更具有辨识度。可见,利用Module实现的全连接层,比利用Function实现的更简单,因其不再需要写反向传播函数。
Module能够自动检测到自己的parameter,并将其作为学习参数。除了parameter,Module还包含子Module,主Module能够递归查找子Module中的parameter。下面再来看看稍微复杂一点的网络:多层感知机。
多层感知机的网络结构如图所示。它由两个全连接层组成,采用sigmoid函数作为激活函数(图中没有画出)。
class Perceptron(nn.Module): def __init__(self,in_features,hidden_features,out_features): nn.Module.__init__(self) self.layer1 = Linear(in_features,hidden_features) # 此处的Linear是前面自定义的全连接层 self.layer2 = Linear(hidden_features,out_features) def forward(self,x): x = self.layer1(x) x = t.sigmoid(x) x = self.layer2(x) return x perceptron = Perceptron(3,4,1) for name,param in perceptron.named_parameters(): print(name,param.size())输出如下:
layer1.w torch.Size([3, 4]) layer1.b torch.Size([4]) layer2.w torch.Size([4, 1]) layer2.b torch.Size([1])可见,即使是稍复杂的多层感知机,其实现依旧很简单。这里需要注意以下两个知识点。
构造函数__init__中,可利用前面自定义的Linear层(Module)作为当前Module对象的一个子Module,它的可学习参数,也会成为当前Module的可学习参数。在前向传播函数中,我们有意识地将输出变量都命名为x,是为了能让Python回收一些中间层的输出,从而节省内存。但并不是所有的中间结果都会被回收,有些variable虽然名字被覆盖,但其在反向传播时仍需要用到,此时Python的内存回收模块将通过检查引用计数,不会回收这一部分内存。Module中parameter的全局命名规范如下:
Parameter直接命名。例如self.param_name = nn.Parameter(t.randn(3,4)),命名为param_name。子Module中的parameter,会在其名字之前加上当前Module的名字。例如self.sub_module = SubModule(),SubModule中有个parameter的名字也叫作param_name,那么二者拼接而成的parameter name就是sub_module.param_name。为了方便用户使用,PyTorch实现了神经网络中绝大多数的layer,这些layer都继承于nn.Module,封装了可学习参数parameter,并实现了forward函数,且专门针对GPU运算进行了CuDNN优化,其速度和性能都十分优异。本书不准备对nn.Module中的所有层进行详细介绍,具体内容读者可参照官方文档或在IPython/Jupyter中使用nn.layer?查看。阅读文档时应主要关注以下几点。
构造函数的参数,如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。
下面将从应用层面出发,对一些常用的layer做简单介绍,更详细的用法请查看官方文档。
图像相关层主要包括卷积层(Conv)、池化层(Pool)等,这些层在实际使用中分为一维(1D)、二维(2D)和三维(3D),池化方式又分为平均池化(AvgPool)、最大值池化(MaxPool)、自适应池化(AdaptiveAvgPool)等。卷积层除了常用的前向卷积外,还有逆卷积(TransposeConv)。下面举例说明。
from PIL import Image from torchvision.transforms import ToTensor, ToPILImage to_tensor = ToTensor() # img -> tensor to_pil = ToPILImage() lena = Image.open('imgs/lena.png') lena输出如下:
# 输入是一个batch,batch_size=1 input = to_tensor(lena).unsqueeze(0) # 锐化卷积核 kernel = t.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(input) to_pil(out.data.squeeze(0))处理后的Lena图如下:
图像的卷积操作还有各种变体,有关各种变体的介绍具体可以参照此处的介绍。
池化层可以看成是一种特殊的卷积层,用来下采样。但池化层没有可学习的参数,其weight是固定的。
pool = nn.AvgPool2d(2,2) list(pool.parameters())输出如下:
[] out = pool(input) to_pil(out.data.squeeze(0))处理后的Lena图如下:
除了卷积层和池化层,深度学习中还将常用到以下几个层。
Linear:全连接层。BatchNorm:批规范化层,分为1D、2D和3D。除了标准的BatchNorm之外,还有在风格迁移中常用到的InstanceNorm层。Dropout:dropout层,用于防止过拟合,同样分为1D、2D和3D。下面通过例子讲解它们的使用方法。
# 输入 batch_size=2,维度3 input = t.randn(2, 3) linear = nn.Linear(3, 4) h = linear(input) h输出:
tensor([[-0.3437, 0.3086, 0.3261, -1.3908], [ 0.3508, -0.7137, 0.8659, -0.5121]], grad_fn=<AddmmBackward>) # 4 channel,初始化标准差为4,均值为0 bn = nn.BatchNorm1d(4) bn.weight.data = t.ones(4) * 4 bn.bias.data = t.zeros(4) bn_out = bn(h) # 注意输出的均值和方差 # 方差是标准差的平方,计算无偏方差分母会减1 # 使用unbiased=False 分母不减1 bn_out.mean(0), bn_out.var(0, unbiased=False)输出:
(tensor([ 0.0000e+00, 0.0000e+00, -2.3842e-07, 0.0000e+00], grad_fn=<MeanBackward2>), tensor([15.9987, 15.9994, 15.9978, 15.9992], grad_fn=<VarBackward1>)) # 每个元素以0.5的概率舍弃 dropout = nn.Dropout(0.5) o = dropout(bn_out) o # 有一半左右的数变为0输出:
tensor([[-7.9997, 0.0000, -0.0000, -7.9998], [ 0.0000, -0.0000, 7.9995, 7.9998]], grad_fn=<MulBackward0>)以上很多例子中都对Module的属性直接操作,其大多数是可学习参数,一般会随着学习的进行而不断改变。实际使用中除非需要使用特殊的初始化,否则应尽量不要直接修改这些参数。
PyTorch实现了常见的激活函数,其具体的接口信息可参见官方文档。这些激活函数可作为独立的layer使用。这里将介绍最常用的激活函数ReLU,其数学表达式为:
R e L U ( x ) = m a x ( 0 , x ) ReLU(x) = max(0,x) ReLU(x)=max(0,x)
relu = nn.ReLU(inplace=True) input = t.randn(2, 3) print(input) output = relu(input) print(output) # 小于0的都被截断为0 # 等价于input.clamp(min=0)输出:
tensor([[-1.1303, 0.9884, 1.8299], [-1.0804, 1.4700, -1.0847]]) tensor([[0.0000, 0.9884, 1.8299], [0.0000, 1.4700, 0.0000]])ReLU函数有个inplace参数,如果设为True,它会把输出直接覆盖到输入中,这样可以节省内存/显存。之所以可以覆盖是因为在计算ReLU的反向传播时,只需根据输出就能推算出反向传播的梯度。但是只有少数的autograd操作支持inplace操作(如variable.sigmoid_()),除非你明确地知道自己在做什么,否则一般不要使用inplace操作。在以上例子中,都是将每一层的输出直接作为下一层的输入,这种网络称为前馈传播网络(Feedforward Neural Network)。对于此类网络,如果每次都写复杂的forward函数会有些麻烦,在此就有两种简化方式,ModuleList和Sequential。其中Sequential是一个特殊的Module,它包含几个子Module,前向传播时会将输入一层接一层地传递下去。ModuleList也是一个特殊的Module,可以包含几个子Module,可以像用list一样使用它,但不能直接把输入传给ModuleList。下面我们举例说明。
# Sequential的三种写法 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 OrderedDict net3= 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)输出:
net1: Sequential( (conv): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)) (batchnorm): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=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, track_running_stats=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, track_running_stats=True) (relu1): ReLU() ) # 可根据名字或序号取出子module net1.conv, net2[0], net3.conv1输出:
(Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)), Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)), Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))) input = t.rand(1, 3, 4, 4) output = net1(input) output = net2(input) output = net3(input) output = net3.relu1(net1.batchnorm(net1.conv(input))) modellist = nn.ModuleList([nn.Linear(3,4), nn.ReLU(), nn.Linear(4,2)]) input = t.randn(1, 3) for model in modellist: input = model(input) # 下面会报错,因为modellist没有实现forward方法 # output = modelist(input)看到这里,读者可能会问,为何不直接使用Python中自带的list,而非要多此一举呢?这是因为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输出:
MyModule( (module_list): ModuleList( (0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)) (1): ReLU() ) ) for name, param in model.named_parameters(): print(name, param.size())输出:
module_list.0.weight torch.Size([3, 3, 3, 3]) module_list.0.bias torch.Size([3])可见,list中的子Module并不能被主Module识别,而ModuleList中的子Module能够被主Module识别。这意味着如果用list保存子Module,将无法调整其参数,因其未加入到主Module的参数中。
除ModuleList之外还有ParameterList,它是一个可以包含多个parameter的类list对象。在实际使用中,使用方式与ModuleList类似。在构造函数__init__中用到list、tuple、dict等对象时,一定要思考是否应该用ModuleList或ParameterList代替。
近些年,随着深度学习和自然语言处理的结合加深,循环神经网络(RNN)的使用也越来越多,关于RNN的基础知识,推荐阅读colah的文章 入门。PyTorch中实现了如今最常用的三种RNN:RNN(vanilla RNN)、LSTM和GRU。此外还有对应的三种RNNCell。
RNN和RNNCell层的区别在于前者能够处理整个序列,而后者一次只处理序列中一个时间点的数据,前者封装更完备更易于使用,后者更具灵活性。RNN层可以通过组合调用RNNCell来实现。
t.manual_seed(1000) # 输入:batch_size=3,序列长度都为2,序列中每个元素占4维 input = t.randn(2, 3, 4) # lstm输入向量4维,隐藏元3,1层 lstm = nn.LSTM(4, 3, 1) # 初始状态:1层,batch_size=3,3个隐藏元 h0 = t.randn(1, 3, 3) c0 = t.randn(1, 3, 3) out, hn = lstm(input, (h0, c0)) out输出:
tensor([[[-0.3610, -0.1643, 0.1631], [-0.0613, -0.4937, -0.1642], [ 0.5080, -0.4175, 0.2502]], [[-0.0703, -0.0393, -0.0429], [ 0.2085, -0.3005, -0.2686], [ 0.1482, -0.4728, 0.1425]]], grad_fn=<StackBackward>) t.manual_seed(1000) input = t.randn(2, 3, 4) # 一个LSTMCell对应的层数只能是一层 lstm = nn.LSTMCell(4, 3) hx = t.randn(3, 3) cx = t.randn(3, 3) out = [] for i_ in input: hx, cx=lstm(i_, (hx, cx)) out.append(hx) t.stack(out)输出:
tensor([[[-0.3610, -0.1643, 0.1631], [-0.0613, -0.4937, -0.1642], [ 0.5080, -0.4175, 0.2502]], [[-0.0703, -0.0393, -0.0429], [ 0.2085, -0.3005, -0.2686], [ 0.1482, -0.4728, 0.1425]]], grad_fn=<StackBackward>)词向量在自然语言中应用十分广泛,PyTorch同样提供了Embedding层。
# 有4个词,每个词用5维的向量表示 embedding = nn.Embedding(4, 5) # 可以用预训练好的词向量初始化embedding embedding.weight.data = t.arange(0,20).view(4,5) input = t.arange(3, 0, -1).long() output = embedding(input) output输出:
tensor([[15, 16, 17, 18, 19], [10, 11, 12, 13, 14], [ 5, 6, 7, 8, 9]], grad_fn=<EmbeddingBackward>)在深度学习中药用到各种各样的损失函数(Loss Function),这些损失函数可看作是一种特殊的layer,PyTorch也将这些损失函数实现为nn.Module的子类。然而在实际使用中通常将这些孙淑函数专门提取出来,作为独立的一部分。详细的loss使用请参考官方文档。这里以分类中最常用的交叉熵损失CrocsEntropyLoss为例讲解。
# batch_size=3,计算对应每个类别的分数(只有两个类别) score = t.randn(3, 2) # 三个样本分别属于1,0,1类,label必须是LongTensor label = t.Tensor([1, 0, 1]).long() # loss与普通的layer无差异 criterion = nn.CrossEntropyLoss() loss = criterion(score, label) loss输出:
tensor(0.5944)PyTorch将深度学习中常用的优化方法全部封装在torch.optim中,其设计十分灵活,能够很方便地扩展城自定义的优化方法。
所有的优化方法都是继承自类optim.Optimizer,并实现了自己的优化步骤。下面就以最基本的优化方法——随机梯度下降法(SGD)举例说明。这里需要重点掌握:
优化方法的基本使用方法。如何对模型的不同部分设置不同的学习率。如何调整学习率。 # 首先定义一个LeNet网络 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 optim optimizer = optim.SGD(params=net.parameters(), lr=1) optimizer.zero_grad() # 梯度清零,等价于net.zero_grad() input = t.randn(1, 3, 32, 32) output = net(input) output.backward(output) # fake backward optimizer.step() # 执行优化 # 为不同子网络设置不同的学习率,在finetune中经常用到 # 如果对某个参数不指定学习率,就使用最外层的默认学习率 optimizer =optim.SGD([ {'params': net.features.parameters()}, # 学习率为1e-5 {'params': net.classifier.parameters(), 'lr': 1e-2} ], lr=1e-5) optimizer输出:
SGD ( Parameter Group 0 dampening: 0 lr: 1e-05 momentum: 0 nesterov: False weight_decay: 0 Parameter Group 1 dampening: 0 lr: 0.01 momentum: 0 nesterov: False weight_decay: 0 ) # 只为两个全连接层设置较大的学习率,其余层的学习率较小 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 = t.optim.SGD([ {'params': base_params}, {'params': special_layers.parameters(), 'lr': 0.01} ], lr=0.001 ) optimizer输出:
SGD ( Parameter Group 0 dampening: 0 lr: 0.001 momentum: 0 nesterov: False weight_decay: 0 Parameter Group 1 dampening: 0 lr: 0.01 momentum: 0 nesterov: False weight_decay: 0 )调整学习率主要有两种做法。一种是修改optimizer.param_groups中对应的学习率,另一种是新建优化器(更简单也是更推荐的做法),由于optimize十分轻量级,构建开销很小,故可以构建新的optimize。但是新建优化器会重新初始化动量等状态信息,这对使用动量的优化器来说(如带momentum的sgd),可能会造成损失函数在收敛过程中出现震荡。
# 方法1: 调整学习率,新建一个optimizer old_lr = 0.1 optimizer1 =optim.SGD([ {'params': net.features.parameters()}, {'params': net.classifier.parameters(), 'lr': old_lr*0.1} ], lr=1e-5) optimizer1输出:
SGD ( Parameter Group 0 dampening: 0 lr: 1e-05 momentum: 0 nesterov: False weight_decay: 0 Parameter Group 1 dampening: 0 lr: 0.010000000000000002 momentum: 0 nesterov: False weight_decay: 0 ) # 方法2: 调整学习率, 手动decay, 保存动量 for param_group in optimizer.param_groups: param_group['lr'] *= 0.1 # 学习率为之前的0.1倍 optimizer输出:
SGD ( Parameter Group 0 dampening: 0 lr: 0.0001 momentum: 0 nesterov: False weight_decay: 0 Parameter Group 1 dampening: 0 lr: 0.001 momentum: 0 nesterov: False weight_decay: 0 )nn中还有一个很常用的模块:nn.functional。nn中的大多数layer在functional中都有一个与之对应的函数。nn.functional中的函数和nn.Module主要区别在于,用nn.Module实现的layers是一个特殊的类,都是由class Layer(nn.Module)定义,会自动提取科学系参数;而nn.functional中的函数更像是纯函数,由def function(input)定义。下面举例说明functional的使用,并对比二者的不同。
input = t.randn(2, 3) model = nn.Linear(3, 4) output1 = model(input) output2 = nn.functional.linear(input, model.weight, model.bias) output1 == output2输出:
tensor([[1, 1, 1, 1], [1, 1, 1, 1]], dtype=torch.uint8) b = nn.functional.relu(input) b2 = nn.ReLU()(input) b == b2输出:
tensor([[1, 1, 1], [1, 1, 1]], dtype=torch.uint8)此时读者可能会问,应该什么时候使用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 F class 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)关于nn.functional的设计初衷,以及它和nn.Module更多的比较说明,可参看论坛的讨论和作者说明。
在深度学习中参数的初始化十分重要,良好的初始化能让模型更快收敛,并达到更高水平,而糟糕的初始化则可能使得模型迅速瘫痪。PyTorch中nn.Module的模块参数都采取了较为合理的初始化策略,因此一般不用我们考虑,当然我们也可以用自定义初始化去代替系统的默认初始化。而当我们在使用Parameter时,自定义初始化则尤为重要,因t.Tensor()返回的是内存中的随机数,很可能会有极大值,这在实际训练网络中会造成溢出或者梯度消失。PyTorch中nn.init模块就是专门为初始化而设计,如果某种初始化策略nn.init不提供,用户也可以自己直接初始化。
# 利用nn.init初始化 from torch.nn import init linear = nn.Linear(3, 4) t.manual_seed(1) # 等价于 linear.weight.data.normal_(0, std) init.xavier_normal_(linear.weight)输出:
Parameter containing: tensor([[ 0.3535, 0.1427, 0.0330], [ 0.3321, -0.2416, -0.0888], [-0.8140, 0.2040, -0.5493], [-0.3010, -0.4769, -0.0311]], requires_grad=True) # 直接初始化 import math t.manual_seed(1) # xavier初始化的计算公式 std = math.sqrt(2)/math.sqrt(7.) linear.weight.data.normal_(0,std)输出:
tensor([[ 0.3535, 0.1427, 0.0330], [ 0.3321, -0.2416, -0.0888], [-0.8140, 0.2040, -0.5493], [-0.3010, -0.4769, -0.0311]]) # 对模型的所有参数进行初始化 for name, params in net.named_parameters(): if name.find('linear') != -1: # init linear params[0] # weight params[1] # bias elif name.find('conv') != -1: pass elif name.find('norm') != -1: pass如果想要更深入地理解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’].
下面举例说明。
class Net(nn.Module): def __init__(self): super(Net, self).__init__() # 等价与self.register_parameter('param1' ,nn.Parameter(t.randn(3, 3))) self.param1 = nn.Parameter(t.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输出:
Net( (submodel1): Linear(in_features=3, out_features=4, bias=True) ) net._modules输出:
OrderedDict([('submodel1', Linear(in_features=3, out_features=4, bias=True))]) net._parameters输出:
OrderedDict([('param1', Parameter containing: tensor([[0.3398, 0.5239, 0.7981], [0.7718, 0.0112, 0.8100], [0.6397, 0.9743, 0.8300]], requires_grad=True))]) net.param1 # 等价于net._parameters['param1']输出:
Parameter containing: tensor([[0.3398, 0.5239, 0.7981], [0.7718, 0.0112, 0.8100], [0.6397, 0.9743, 0.8300]], requires_grad=True) for name, param in net.named_parameters(): print(name, param.size())输出:
param1 torch.Size([3, 3]) submodel1.weight torch.Size([4, 3]) submodel1.bias torch.Size([4]) for name, submodel in net.named_modules(): print(name, submodel)输出:
Net( (submodel1): Linear(in_features=3, out_features=4, bias=True) ) submodel1 Linear(in_features=3, out_features=4, bias=True) bn = nn.BatchNorm1d(2) input = t.rand(3, 2) output = bn(input) bn._buffers输出:
OrderedDict([('running_mean', tensor([0.0514, 0.0749])), ('running_var', tensor([0.9116, 0.9068])), ('num_batches_tracked', tensor(1))])nn.Module在实际使用中可能层层嵌套,一个module包含若干个子module,每一个子module又包含了更多的子module。为方便用户访问各个子module,nn.Module实现了很多方法,如函数children可以查看直接子module,函数module可以查看所有的子module(包括当前module)。与之相对应的还有函数named_childen和named_modules,其能够在返回module列表的同时返回它们的名字。
x = t.arange(0, 12).view(3, 4).float() model = nn.Dropout() # 在训练阶段,会有一半左右的数被随机置为0 model(x)输出:
tensor([[ 0., 0., 0., 0.], [ 8., 0., 0., 14.], [ 0., 18., 20., 0.]]) model.training = False # 在测试阶段,dropout什么都不做 model(x)输出:
tensor([[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.]])对于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输出:
True True (False, False) list(net.named_modules())输出:
[('', Net( (submodel1): Linear(in_features=3, out_features=4, bias=True) )), ('submodel1', Linear(in_features=3, out_features=4, bias=True))]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) # 用完hook后删除 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(t.ones(2, 2)) module._parameters输出:
OrderedDict([('param', Parameter containing: tensor([[1., 1.], [1., 1.]], requires_grad=True))]) submodule1 = nn.Linear(2, 2) submodule2 = nn.Linear(2, 2) module_list = [submodule1, submodule2] # 对于list对象,调用buildin函数,保存在__dict__中 module.submodules = module_list print('_modules: ', module._modules) print("__dict__['submodules']:",module.__dict__.get('submodules'))输出:
_modules: OrderedDict() __dict__['submodules']: [Linear(in_features=2, out_features=2, bias=True), Linear(in_features=2, out_features=2, bias=True)] 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'))输出:
ModuleList is instance of nn.Module: True _modules: OrderedDict([('submodules', ModuleList( (0): Linear(in_features=2, out_features=2, bias=True) (1): Linear(in_features=2, out_features=2, bias=True) ))]) __dict__['submodules']: None因_modules和_parameters中的item未保存在__dict__中,所以默认的getattr方法无法获取它,因而nn.Module实现了自定义的__getattr__方法,如果默认的getattr无法处理,就调用自定义的__getattr__方法,尝试从_modules、_parameters和_buffers这三个字典中获取。
getattr(module, 'training') # 等价于module.training # error # module.__getattr__('training')输出:
True module.attr1 = 2 getattr(module, 'attr1') # 报错 # module.__getattr__('attr1')输出:
2 # 即module.param, 会调用module.__getattr__('param') getattr(module, 'param')输出:
Parameter containing: tensor([[1., 1.], [1., 1.]], requires_grad=True)在PyTorch中保存模型十分简单,所有的Module对象都具有state_dict()函数,返回当前Module所有的状态数据。将这些状态数据保存后,下次使用模型时即可利用model.load_state_dict()函数将状态加载进来。优化器(optimizer)也有类似的机制,不过一般并不需要保存优化器的运行状态。
# 保存模型 t.save(net.state_dict(), 'net.pth') # 加载已保存的模型 net2 = Net() net2.load_state_dict(t.load('net.pth'))输出:
IncompatibleKeys(missing_keys=[], unexpected_keys=[])实际上还有另外一种保存方法,但因其严重依赖模型定义方式及文件路径结构等,很容易出问题,因而不建议使用。
t.save(net, 'net_all.pth') net2 = t.load('net_all.pth') net2输出:
Net( (submodel1): Linear(in_features=3, out_features=4, bias=True) )将Module放在GPU上运行也十分简单,只需两步:
model = model.cuda():将模型的所有参数转存到GPUinput.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.Module利用的也是autograd技术,其主要工作是实现前向传播。在forward函数中,nn.Module对输入的tensor进行的各种操作,本质上都是用到了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进行封装则更为简单高效。
Kaiming He的深度残差网络(ResNet)在深度学习的发展中起到了很重要的作用,ResNet不仅一举拿下了当年CV下多个比赛项目的冠军,更重要的是这一结构解决了训练极深网络时的梯度消失问题。
首先来看看ResNet的网络结构,这里选取的是ResNet的一个变种:ResNet34。ResNet的网络结构如图4-2所示,可见除了最开始的卷积池化和最后的池化全连接之外,网络中有很多结构相似的单元,这些重复单元的共同点就是有个跨层直连的shortcut。ResNet中将一个跨层直连的单元称为Residual block,其结构如图4-3所示,左边部分是普通的卷积网络结构,右边是直连,但如果输入和输出的通道数不一致,或其步长不为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。 class 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)) # 重复的layer,分别有3,4,6,3个residual block 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 = t.randn(1, 3, 224, 224) o = model(input)感兴趣的读者可以尝试实现Google的Inception网络结构或ResNet的其它变体,看看如何能够简洁明了地实现它,实现代码尽量控制在80行以内(本例去掉空行和注释总共不超过50行)。另外,与PyTorch配套的图像工具包torchvision已经实现了深度学习中大多数经典的模型,其中就包括ResNet34,读者可以通过下面两行代码使用:
from torchvision import models model = models.resnet34()本例中ResNet34的实现就是参考了torchvision中的实现并做了简化,感兴趣的读者可以阅读相应的源码,比较这里的实现和torchvision中实现的不同。
通过本章的学习,读者可以掌握PyTorch中神经网络工具箱中大部分类和函数的用法。关于这部分的更多内容,读者可以参考官方文档,文档中有更多详细的说明。