【notes】pytorch学习笔记3-神经网络工具箱nn

神经网络工具箱nn

autograd实现了自动微分系统,然而对于深度学习来说过于底层。nn模块是构建与autograd之上的神经网络模块。除了nn之外,我们还会介绍神经网络中常用的工具,比如优化器optim,初始化init等。

nn.Module

torch.nn的核心数据结构是Module,它是一个抽象的概念,既可以表示神经网络中的某个层(layer),也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法是继承nn.Module,撰写自己的网络、层。

全连接层,又名仿射层,输出$\boldsymbol{x}$和输入$\boldsymbol{x}$满足$\boldsymbol{y=Wx+b}$,$\boldsymbol{W}$ 和 $\boldsymbol{b}$ 是可学习函数。

1
2
3
import torch as t
from torch import nn
from torch.autograd import Variable as V
1
2
3
4
5
6
7
8
9
10
class Linear(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):
x = x.mm(self.w)
return x + self.b.expand_as(x)
1
2
3
4
layer = Linear(4, 3)
input = V(t.randn(2, 4))
output = layer(input)
output
tensor([[ 2.0269,  5.1465,  1.5603],
        [-0.6868, -0.8096, -0.6427]], grad_fn=<AddBackward0>)
1
2
3
for name, parameter in layer.named_parameters():

print(name, parameter)
w Parameter containing:
tensor([[-0.0121, -0.2593, -0.5310],
        [ 0.2982, -0.2846, -0.0437],
        [ 0.6220,  1.7351,  0.8025],
        [ 1.0544,  2.3325,  0.6561]], requires_grad=True)
b Parameter containing:
tensor([0.2586, 2.3734, 0.5372], requires_grad=True)

但需要注意一下几点:

  • 自定义层Linear必须继承nn.Module,并且在其构造函数中需调用nn.Module的构造函数,即super(Linear, self).init()或nn.Module.__init__(self)。
  • 在构造函数__init__中必须自己定义可学习的参数,并封装成Parameter,如在本例中我们把w和b封装成Parameter。Parameter是一种特殊的Variable,但其默认需要求导(requires_grad=True)。
  • 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能够自动检测到自己的parameter,并将其作为学习参数。除了parameter,Module还包含子Module,主Module能够递归查找子Module中的parameter。

下面看一个稍微复杂的网络:多层感知机。

1
2
3
4
5
6
7
8
9
class Perceptron(nn.Module):
def __init__(self, in_features, hidden_features, out_features):
nn.Module.__init__(self)
self.layer1 = Linear(in_features, hidden_features)
self.layer2 = Linear(hidden_features, out_features)
def forward(self, x):
x = self.layer1(x)
x = t.sigmoid(x)
return self.layer2(x)
1
2
3
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的名字,就是sub_module.param_name。

为了方便用户使用,pytorch实现了神经网络中绝大多数的layer,这些layer都继承了nn.Module,封装了可学习参数parameter,并实现了forward函数,且专门针对GPU运算进行了CuDNN优化。具体内容可参考官方文档或在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。
    若想输入一个数据需要调用unsqueeze(0)函数将数据伪装成batch_size = 1的batch

常用的神经网络层

图像相关层

图像相关层主要包括卷积层(Conv)、池化层(Pool)等,这些层在实际使用中可分为一维、二维和三维,池化层又分为平均池化(AvgPool)、最大值池化(MaxPool)、自适应池化(AdaptiveAvgPool)等。卷积层除了常用的前向卷积外,还有逆卷积(TransposeConv)。

1
2
3
4
5
6
7
8
9
import torch as t
from torch import nn
from torch.autograd import Variable as V
from PIL import Image
from torchvision.transforms import ToTensor, ToPILImage
to_tensor = ToTensor()
to_pil = ToPILImage()
curry = Image.open('curry')
curry
1
2
3
4
5
6
7
8
9
10
11
# 输入一个batch,batch_size = 1
input = to_tensor(curry).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(V(input))
to_pil(out.data.squeeze(0))

Shape:

  • Input: $(N, C_{in}, H_{in}, W_{in})$
  • Output: $(N, C_{out}, H_{out}, W_{out})$ where
    $H_{out} = \left\lfloor\frac{H_{in} + 2 \times \text{padding}[0] - \text{dilation}[0]\times (\text{kernel_size}[0] - 1) - 1}{\text{stride}[0]} + 1\right\rfloor$

$W_{out} = \left\lfloor\frac{W_{in} + 2 \times \text{padding}[1] - \text{dilation}[1]\times (\text{kernel_size}[1] - 1) - 1}{\text{stride}[1]} + 1\right\rfloor$

图像的卷积操作还有各种变体,有关各种变体的介绍可以参照此处的介绍https://github.com/vdumoulin/conv_arithmetic/blob/master/README.md

池化层可以看作是一种特殊的卷积层,用来下采样。但池化层没有可学习参数,其weight是固定的。

1
2
pool = nn.AvgPool2d(2, 2)
list(pool.parameters())
[]
1
2
out = pool(V(input))
to_pil(out.data.squeeze(0))

除了卷积层和池化层,深度学习中还将常用到一下几个层

  • Linear:全连接层
  • BatchNorm:批规范化层,分为1D、2D和3D。除了标准的BatchNorm之外,还有在风格迁移中常用到的InstanceNorm层。
  • Dropout:dropout层,用来防止过拟合,同样分为1D、2D和3D。
1
2
3
4
# 输入batch_size=2,维度3
input = V(t.randn(2, 3))
linear = nn.Linear(3, 4)
h = linear(input);h
tensor([[-0.4360,  0.3433, -0.1978, -0.3128],
        [-0.9655,  0.6278,  0.2510,  0.1256]], grad_fn=<AddmmBackward>)
1
2
3
4
5
6
7
8
9
10
# 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([-1.1921e-07,  0.0000e+00,  0.0000e+00,  0.0000e+00],
        grad_fn=<MeanBackward1>),
 tensor([15.9977, 15.9921, 15.9968, 15.9967], grad_fn=<VarBackward1>))
1
2
3
4
5
6
# 每个元素以0.5的概率舍弃
dropout = nn.Dropout(0.5)
o = dropout(bn_out)

# 有一半的概率会变成0
o
tensor([[7.9994, -0.0000, -0.0000, -0.0000],
        [-0.0000, 7.9980, 7.9992, 7.9992]], grad_fn=<MulBackward0>)

以上很多例子中都对module的属性直接操作,其大多数是可学习参数,一般会随着学习的进行而不断改变。实际使用中除非需要使用特殊的初始化,否则尽量不要直接改变参数。

激活函数

pytorch实现了常见的激活函数。其他具体的接口信息可参见官方文档,这些激活函数可以作为独立的layer使用。这里介绍最常用的激活函数ReLU,其数学表达式为:
$$ReLU(x)=max(0,x)$$

1
2
3
4
5
6
relu = nn.ReLU(inplace = True)
input = V(t.randn(2, 3))
print(input)
output = relu(input)
print(output) # 小于0的都被截断为0
id(input) == id(output)
tensor([[ 0.5049,  0.6093, -0.1565],
        [-0.9114, -0.9594,  1.0539]])
tensor([[0.5049, 0.6093, 0.0000],
        [0.0000, 0.0000, 1.0539]])





True

ReLU函数有个inplace参数,如果设为True,如果设为True,它会把输出直接覆盖到输入中,这样可以节省内存/显存。之所以可以覆盖是因为在计算ReLU的反向传播时,只需根据输出就能够推算出反向传播的梯度。但是只有少数的autograd操作支持inplace操作(如variable.sigmoid_()),除非你明确知道自己在做什么,否则一般不要使用inplace操作。

在以上例子里,都是将每一层的输出直接作为下一层的输入,这种网络成为前馈传播网络。对于此种网络,如果每次都写复杂的forward函数会有些麻烦,在此就有两种简化方式,ModuleList和Sequential。其中Sequential是一个特殊的Module,它包含几个子module,前向传播时会将输入一层接一层第传递下去。ModuleList也是一个特殊的Module,可以包含几个子Module,可以像用list一样使用它,但不能直接把输入传给ModuleList。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Sequential的三种写法

net1 = nn.Sequential()
net1.add_module('conv', nn.Conv2d(3, 3, 3)) # 输入为(N, C_{in}, H_{in}, W_{in}),参数为
net1.add_module('batchnorm', nn.BatchNorm2d(3)) # 3为(N, C, H, W)中的C
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()
)
1
2
# 可根据名字或序号取出子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)))
1
2
input = V(t.rand(1, 3, 4, 4))
net1(input), net2(input), net3(input), net3.relu1(net2[1](net1.conv(input)))
(tensor([[[[0.0000, 1.4727],
           [0.1600, 0.0000]],

          [[0.0000, 0.0000],
           [0.7015, 1.1069]],

          [[1.7189, 0.0000],
           [0.0000, 0.0000]]]], grad_fn=<ReluBackward0>),
 tensor([[[[0.0000, 1.6957],
           [0.0000, 0.0000]],

          [[1.2454, 0.6350],
           [0.0000, 0.0000]],

          [[1.0204, 0.4811],
           [0.1430, 0.0000]]]], grad_fn=<ReluBackward0>),
 tensor([[[[0.0000, 1.5585],
           [0.1751, 0.0000]],

          [[0.0000, 1.4177],
           [0.1846, 0.0000]],

          [[0.0000, 0.0000],
           [1.3537, 0.2417]]]], grad_fn=<ReluBackward0>),
 tensor([[[[0.0000, 1.4727],
           [0.1600, 0.0000]],

          [[0.0000, 0.0000],
           [0.7015, 1.1069]],

          [[1.7189, 0.0000],
           [0.0000, 0.0000]]]], grad_fn=<ReluBackward0>))
1
2
3
4
5
6
7
modulelist = nn.ModuleList([nn.Linear(3, 4), nn.ReLU(), nn.Linear(4, 2)])
input = V(t.rand(1, 3))
for model in modulelist:
input = model(input)

# 下面会报错,因为modellist没有实现forward方法
# output = modellist(input)

为何不直接使用python中自带的list,而非要多次一举呢?
这是因为ModuleList是Module的子类,当在Module中使用它时,就能自动识别为子module。

1
2
3
4
5
6
7
8
9
10
11
class MyModule(nn.Module):
def __init__(self):
super(MyModule, self).__init__()
self.list = [nn.Linear(3,4), nn.ReLU()] # 直接用list
self.module_list = nn.ModuleList([nn.Conv2d(3, 3, 3), nn.ReLU()]) # 用nn.ModuleList

def forward(self):
pass

model = MyModule()
model
MyModule(
  (module_list): ModuleList(
    (0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
    (1): ReLU()
  )
)
1
2
3
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识别。

除ModuleList之外还有ParameterList,它是一个可以包含多个parameter的类list对象。在实际应用中,使用方式和ModuleList类似。在构造函数__init__中用到list、tuple、dict等对象,一定要思考是否应该用ModuleList或ParameterList代替。

损失函数

在深度学习中会用到各种各样的损失函数,这些损失函数可看作是一种特殊的layer,pytorch也将这些损失函数实现为nn.Module的子类。然而在实际使用中通常将这些损失函数专门提取出来,作为独立的一部分。

1
2
3
4
5
6
7
8
9
#batch_size = 3, 计算对应每个类别的分数
score = V(t.randn(3, 10)) # (N, C) N是batch_size,C是class的个数
# 三个样本分别属于1, 0, 1类,label必须是LongTensor
label = V(t.Tensor([1, 0, 9])).long()

# loss与普通的layer无差异
criterion = nn.CrossEntropyLoss()
loss = criterion(score, label)
loss
tensor(2.8392)

优化器

pytorch将深度学习中常用的优化方法全部封装到torch.optim中,其设计十分灵活,能够很方便地扩展自定义的优化方法。
所有的优化方法都是继承基类optim.Optimizer,并实现了自己的优化步骤。下面就以最基本的优化方法————随机梯度下降法(SGD)举例说明。这里需要重点掌握:

  • 优化方法的基本使用方法
  • 如何对模型的不同部分设置不同的学习率
  • 如何调整学习率
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
# 首先定义一个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()
1
2
3
4
5
6
7
8
9
from torch import optim
optimizer = optim.SGD(params = net.parameters(), lr = 1)
optimizer.zero_grad() # 梯度清零,等价于net.zero_grad()

input = V(t.randn(1, 3, 32, 32))
output = net(input)
output.backward(output) # fake backward

optimizer.step() # 执行优化
1
2
3
# 为不同子网络参数设置不同的学习率,在finetune中经常用到
# 如果对某个参数不指定学习率,就是用默认学习率
optimizer = optim.SGD([{'params':net.features.parameters()},{'params':net.classifier.parameters(), 'lr':1e-2}], lr = 1e-5)
1
2
3
4
5
6
7
8
9
#  只为两个全连接层设置较大的学习率,其余层的学习率较小
special_layers = nn.ModuleList([net.classifier[0],net.classifier[2]])
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)

调整学习率主要有两种做法。一种是修改optimmizer.param_groups中对应的学习率。另一种是新建优化器(更简单也是更推荐的做法),由于optimizer十分轻量级,构建开销很小,故可以构建新的optimizer。但是新建优化器会重新初始化动量等状态信息,这对使用动量的优化器来说(如带momentum的sgd),可能会造成损失函数在收敛过程中出震荡。

1
2
3
4
5
6
# 调整学习率,新建一个optimizer
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)定义。

1
2
3
4
5
input = V(t.randn(2, 3))
model = nn.Linear(3, 4)
output1 = model(input)
output2 = nn.functional.linear(input, model.weight, model.bias)
output1 == output2
tensor([[True, True, True, True],
        [True, True, True, True]])
1
2
3
b = nn.functional.relu(input)
b2 = nn.ReLU()(input)
b == b2
tensor([[True, True, True],
        [True, True, True]])

应该什么时候使用nn.Module,什么时候使用nn.functional?
如果模型有可学习的参数,最好用nn.Module,否则既可以使用nn.functional也可以使用nn.Module,二者在性能上没有太大差异,具体的使用方式取决于个人喜好。
但建议还是使用nn.Dropout而不是nn.functional.dropout,因为dropout在训练和测试两个阶段的行为有所差别,使用nn.Module对象能够通过model.eval操作以一区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.max_pool2d(F.relu(self.conv1(x)), 2)
x = F.max_pool2d(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
1
2
3
net = Net()
x = t.randn(1, 3, 32, 32)
net(x)
tensor([[-0.0539,  0.0145,  0.0214,  0.0474, -0.0516,  0.0890,  0.0539,  0.0805,
          0.0785, -0.1043]], grad_fn=<AddmmBackward>)

不具备可学习参数的层(激活层、池化层等),将它们用函数代替,这样可以不用放置在构造函数__init__中。有可学习参数的模块,也可以用functional代替,只不过实现起来较繁琐,需要手动定义参数parameter。

1
2
3
4
5
6
7
8
9
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,input):

return F.linear(input, self.weight, self.bias)
1
2
3
x = t.randn(1, 4)
linear = MyLinear()
linear(x)
tensor([[-0.0678,  2.5530,  0.8512]], grad_fn=<AddmmBackward>)

初始化策略

在深度学习中参数的初始化十分重要,良好的初始化能使模型收敛更快,并达到更高水平,而糟糕的初始化可能使模型迅速崩溃。pytorch中nn.Module的模块参数都采取了较合理的初始化策略,因此一般不用我们考虑。当然我们可以用自定义的初始化代替系统的默认初始化。自定义初始化尤为重要,因为t.Tensor()返回的是内存中的随机数,很可能会有极大值,这在实际训练网络中会造成溢出或者梯度消失。pytorch中的nn.init模块专门为初始化设计,实现了常用的初始化策略。如果某种初始化策略nn.init不提供,用户也可以自己直接初始化。

1
2
3
4
5
6
# 利用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)
1
2
3
4
5
import math
t.manual_seed(1)

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]])
1
2
3
4
5
6
7
8
9
10
# 对模型的所有参数进行初始化
for name, params in net.named_parameters():

if name.find('linear') != -1:
print(params[0]) # weight
print(params[1]) # bias
elif name.find('conv') != -1:
pass
elif name.find('norm') != -1:
pass

nn.Module深入分析

如果想深入地理解nn.Module,研究其原理是很有必要的。首先来看看nn.Module基类的构造函数的源代码:

1
2
3
4
5
6
7
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.submodule = 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’]

1
2
3
4
5
6
7
8
9
10
11
12
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 foward(self, input):
x = self.param1 * input
x = self.submodel1(x)
return x
net = Net()
net
Net(
  (submodel1): Linear(in_features=3, out_features=4, bias=True)
)
1
net._modules
OrderedDict([('submodel1', Linear(in_features=3, out_features=4, bias=True))])
1
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))])
1
net.param1 == net._parameters['param1']
tensor([[True, True, True],
        [True, True, True],
        [True, True, True]])
1
2
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])
1
2
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)
1
2
3
4
bn = nn.BatchNorm1d(2)
input = V(t.rand(3,2), requires_grad = True)
output = bn(input)
bn._buffers
OrderedDict([('running_mean', tensor([0.0362, 0.0596])),
             ('running_var', tensor([0.9009, 0.9262])),
             ('num_batches_tracked', tensor(1))])

nn.Module在实际使用中可能层层嵌套,一个module包含若干个子module,每一个子module又包含了更多的子module。为了方便用户访问各个子module,nn.Module实现了很多方法,如函数children可以查看直接子module,函数modules可以查看所有的子module(包括当前module)。与之相对应的还有函数named_children和named_modules,其能够在返回module列表的同时返回它们的名字。

1
2
3
input = V(t.arange(0, 12).view(3, 4).float())
model = nn.Dropout()
model(input)
tensor([[ 0.,  0.,  0.,  0.],
        [ 0.,  0., 12.,  0.],
        [ 0., 18.,  0.,  0.]])
1
2
model.training = False
model(input)
tensor([[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.]])

对batchnorm、dropout、instancenorm等在训练和测试阶段行为差距较大的层,如果在测试时不将其training值设为False,则可能会有很大影响,这在实际使用中千万注意。虽然可通过直接设置training属性将子module设为train和eval模式,但是这种方式比较繁琐。推荐的做法是调用model.train()函数,它会将当前module及其子module中的所有training属性都设为True。model.eval()函数会把training属性都设为False。

1
2
3
print(net.training, net.submodel1.training)
net.eval()
net.training, net.submodel1.training
True True





(False, False)
1
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_ouput) -> Tensor or None。钩子函数不应修改输入和输出,并且在使用后应及时删除,以避免每次都运行钩子增加运行负载。钩子函数主要用在获取某些中间结果的情景,如中间某一层的输出或某一层的梯度。这些结果本应写在forward函数中,但如果在forward函数中加上这些处理,可能会使处理逻辑比较复杂,这时使用钩子技术就更合适。下面考虑一种场景:有一个预训练的模型,需要提取模型的某一层(不是最后一层)的输出作为特征进行分类,希望不修改其原有的模型定义文件,这时就可以利用钩子函数。

1
2
3
4
5
6
7
8
9
model = VGG()
features = t.Tensor()
def hook(module, input, output):
features.copy_(output.data)

handle = model.layer8.register_forward_hook(hook)
_ = model(input)
# 用完hook后删除
handle.remove()

nn.Module对象在构造函数中的行为看起来有些诡异,想要理解就需要看两个魔法方法__getattr__和__setattr__。在python中有两个常用的builtin方法:getattr和setattr。getattr(obj, ‘attr1’)等价于obj.attr,setattr(obj, ‘name’, value)等价于obj.name = value。

  • result = obj.name会调用builtin函数getattr(obj, ‘name’),如果该属性找不到,会调用obj.__getattr__(‘name’)
  • obj.name = value会调用builtin函数setattr(obj, ‘name’, value),如果obj对象实现了__setattr__方法,setattr会直接调用obj.__setattr__(‘name’, value)。
1
2
3
4
5
6
7
8
9
10
11
class person():
dict = {'name':'xxx','sex':'boy','age':18}

def __getattr__(self,name):

return self.dict[name]
def __setattr__(self, name , value):

self.dict[name] = value

return value
1
2
one = person()
one.name, one.sex, one.age
('xxx', 'boy', 18)
1
one.name = '吴彦祖';one.name
'吴彦祖'

nn.Module实现了自定义的__setattr__函数,当执行module.name=value时,会在__setattr__中判断value是否为Parameter或nn.Module对象,如果是则将这些对象加到_parameters和_modules两个字典中;如果是其他类型的对象,如Variable、list、dict等,则调用默认的操作,将这个值保存在__dict__中。

1
2
3
module = nn.Module()
module.param = nn.Parameter(t.ones(2, 2))
module._parameters,module.param
(OrderedDict([('param', Parameter containing:
               tensor([[1., 1.],
                       [1., 1.]], requires_grad=True))]), Parameter containing:
 tensor([[1., 1.],
         [1., 1.]], requires_grad=True))
1
2
3
4
5
6
7
submodule1 = nn.Linear(2, 2)
submodule2 = nn.Linear(2, 2)
module_list = [submodule1, submodule2]
# 对于list对象,调用builtin函数,保存在__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)]
1
2
3
module.submodules = nn.ModuleList(module_list)
print('_modules:',module._modules)
print("__dict__['submodules']:",module.__dict__.get('submodules'))
_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三个字典中获取。

1
2
3
getattr(module, 'training') # 等价于module.training
#error
#module.__getattr__('training')
True
1
2
3
4
module.attr1 = 2
getattr(module, 'attr1')
# 报错
# module.__getattr__('attr1')
2
1
getattr(module, 'param')
Parameter containing:
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)

在pytorch中保存模型十分简单,所有的module对象都具有state_dict()函数,返回当前Module所有的状态数据。将这些状态数据保存后,下次使用模型时即可利用model.load_state_dict()函数将状态加载进来。优化器(optimizer)也有类似机制,不过一般并不需要保存优化器的运行状态。

1
2
3
4
5
6
# 保存模型
t.save(net.state_dict(), 'net.pth')

# 加载已保存的模型
net2 = Net()
net2.load_state_dict(t.load('net.pth'))
<All keys matched successfully>
1
2
3
t.save(net, 'net_all.pth')
net2 = t.load('net_all.pth')
net2
/usr/local/lib/python3.6/dist-packages/torch/serialization.py:256: UserWarning: Couldn't retrieve source code for container of type Net. It won't be checked for correctness upon loading.
  "type " + obj.__name__ + ". It won't be checked "





Net(
  (submodel1): Linear(in_features=3, out_features=4, bias=True)
)

将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上进行并行加速。

1
2
3
4
5
6
# method1
new_net = nn.DataParallel(net, device_ids = [0, 1])
output = new_net(input)

# method 2
output = nn.parallel.data_parallel(net, input, device_ids = [0, 1])

DataParallel并行的方式,是将输入一个batch的数据均分成多份,分别送到对应的GPU进行计算,然后将各个GPU得到的梯度相加。与Module相关的所有数据也会以浅复制的方式复制多份。

nn和aautograd的关系

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将多个操作聚合,实现优化,比直接利用autograd低级别的操作要快。如果只是想在深度学习中怎加某一层,使用nn.Module进行封装则更简单高效。

———————————————感谢阅读———————————————

欢迎收藏访问我的博客 知乎 掘金 简书 知乎

贰三 wechat
欢迎扫描二维码订阅我的公众号!