参考https://github.com/chenyuntc/pytorch-book/tree/v1.0

希望大家直接到上面的网址去查看代码,下面是本人的笔记

本章介绍的nn模块是构建与autograd之上的神经网络模块

除了nn外还会介绍神经网络中常用的工具,比如优化器optim、初始化init等

1.nn.Module

torch的核心数据结构是Module,它是一个抽象的概念,既可以表示神经网络中的某个层,也可以表示一个包含很多层的神经网络

在实际使用中,最常见的做法是继承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

定义函数:

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):
x = x.mm(self.w)
return x + self.b.expand_as(x)

运行:

layer = Linear(,)
input = V(t.randn(,))
output = layer(input)
output

返回:

tensor([[-0.4199,  3.7252,  1.9104],
[ 2.3267, 2.0576, -2.9361]], grad_fn=<AddBackward0>)

查看参数:

for name, parameter in layer.named_parameters():
print(name, parameter) #即w,b

返回:

w Parameter containing:
tensor([[ 1.1147, -0.8054, -0.7915],
[-0.3828, 0.1073, 2.0440],
[-0.3297, 0.0465, 0.0759],
[ 0.1022, 0.1638, 1.0872]], requires_grad=True)
b Parameter containing:
tensor([ 1.2872, 2.3990, -0.7711], requires_grad=True)

可见,全连接层的实现非常简单,其代码量不超过10行,但需注意以下几点:

  • 自定义层Linear必须继承nn.Module,并且在其构造函数中需调用nn.Module的构造函数,即super(Linear, self).__init__() 或nn.Module.__init__(self),推荐使用第一种用法,尽管第二种写法更直观。
  • 在构造函数__init__中必须自己定义可学习的参数,并封装成Parameter,如在本例中我们把wb封装成parameterparameter是一种特殊的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,并将其作为学习参数。除了parameter之外,Module还包含子Module,主Module能够递归查找子Module中的parameter。下面再来看看稍微复杂一点的网络,多层感知机。

多层感知机的网络结构如图4-1所示,它由两个全连接层组成,采用函数作为激活函数,图中没有画出。

class Perceptron(nn.Module):
def __init__(self, in_features, hidden_features, out_features):
super(Perceptron, self).__init__()
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) #激活函数
return self.layer2(x)

调用:

perceptron = Perceptron(,,)
for name, param in perceptron.named_parameters():
print(name, param.size())

返回:

layer1.w torch.Size([, ])
layer1.b torch.Size([])
layer2.w torch.Size([, ])
layer2.b torch.Size([])

可见,即使是稍复杂的多层感知机,其实现依旧很简单。这里新增两个知识点:

  • 构造函数__init__中,可利用前面自定义的Linear层(module),作为当前module对象的一个子module,它的可学习参数,也会成为当前module的可学习参数。
  • 在前向传播函数中,我们有意识地将输出变量都命名成x,是为了能让Python回收一些中间层的输出,从而节省内存。但并不是所有都会被回收,有些variable虽然名字被覆盖,但其在反向传播仍需要用到,此时Python的内存回收模块将通过检查引用计数,不会回收这一部分内存。

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函数,且很多都专门针对GPU运算进行了CuDNN优化,其速度和性能都十分优异。本书不准备对nn.Module中的所有层进行详细介绍,具体内容读者可参照官方文档。阅读文档时应主要关注以下几点:

  • 构造函数的参数,如nn.Linear(in_features, out_features, bias),需关注这三个参数的作用。
  • 属性,可学习参数,子module。如nn.Linear中有weightbias两个可学习参数,不包含子module。
  • 输入输出的形状,如nn.linear的输入形状是(N, input_features),输出为(N,output_features),N是batch_size。

这些自定义layer对输入形状都有假设:输入的不是单个数据,而是一个batch。

若想输入一个数据,则必须调用unsqueeze(0)函数将数据伪装成batch_size=1的batch

下面将从应用层面出发,对一些常用的layer做简单介绍,更详细的用法请查看文档,这里只作概览参考。

2.常用神经网络层

1.图像相关层

图像相关层主要包括卷积层(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 =
print(lena.size) #(, , )
input = to_tensor(lena).unsqueeze() #变成(,,,)
print(input)
#锐化卷积核
kernel = t.ones( ,)/-
kernel[][] =
conv = nn.Conv2d(, , (,), , bias = False)
conv.weight.data = kernel.view(,,,) out = conv(V(input))
to_pil(out.data.squeeze())

返回:

(, )
tensor([[[[0.6353, 0.6314, 0.6314, ..., 0.6118, 0.6667, 0.5922],
[0.6353, 0.6314, 0.6314, ..., 0.6078, 0.6510, 0.5647],
[0.6275, 0.6235, 0.6235, ..., 0.4824, 0.4157, 0.3098],
...,
[0.1961, 0.2078, 0.2078, ..., 0.2510, 0.3098, 0.3412],
[0.1922, 0.2000, 0.2039, ..., 0.3098, 0.3686, 0.3804],
[0.1843, 0.2078, 0.1961, ..., 0.3569, 0.3961, 0.4078]]]])

图示:

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

pool = nn.AvgPool2d(,) #平均池化
list(pool.parameters()) #返回[],因为无参数
out = pool(input) #对数据进行池化
to_pil(out.data.squeeze()) #显示结果

图示:

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

  • Linear:全连接层。
  • BatchNorm:批规范化层,分为1D、2D和3D。除了标准的BatchNorm之外,还有在风格迁移中常用到的InstanceNorm层。
  • Dropout:dropout层,用来防止过拟合,同样分为1D、2D和3D。 下面通过例子来说明它们的使用。

1)全连接层

#输入batch_size = ,维度为3
input = t.randn(,)
linear = nn.Linear(,)
h = linear(input)
h

返回:

tensor([[ 0.5406, -0.0327,  0.7291,  0.5262],
[ 0.1471, -0.1924, 0.8960, 0.7801]], grad_fn=<AddmmBackward>)

2)批规范化,即归一化层

# channel,初始化标准差为4,均值为0
bn = nn.BatchNorm1d() #对小批量(mini-batch)的2d或3d输入进行批标准化(Batch Normalization)操作,即归一化
bn.weight.data = t.ones() *
bn.bias.data = t.zeros() bn_out = bn(h)
#注意输出的均值和方差
#方差是标准差的平方,计算无偏方差分母会减1
#使用unbiased=False 分母不减1
bn_out.mean(), bn_out.var(, unbiased=False) #归一化后平均值为0,方差为标准单位方差

返回:

(tensor([., ., ., .], grad_fn=<MeanBackward0>),
tensor([15.9959, 15.9749, 15.9771, 15.9901], grad_fn=<VarBackward1>))

Batch归一化使用在z上,下面激活函数处的例子可见,对其进行卷积 -> batch归一化 -> 激活函数

3)dropout正则化层

#每个元素以0.5的概率舍弃,实现dropout正则化,消除过拟合问题
dropout = nn.Dropout(0.5)
o = dropout(bn_out)
o #有一半左右的数变成0

返回:

tensor([[ 0.0000,  7.9937, -7.9943, -7.9975],
[-0.0000, -0.0000, 7.9943, 7.9975]], grad_fn=<MulBackward0>)

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

2.激活函数

1)ReLu

relu = nn.ReLU(inplace=True)
input = t.randn(,)
print(input)
output = relu(input)
print(output) #小于0的都被截断为0
#等价于input.clamp(min=)

返回:

tensor([[ 1.2619, -0.9128,  0.6259],
[-1.4834, 0.7297, -0.8562]])
tensor([[1.2619, 0.0000, 0.6259],
[0.0000, 0.7297, 0.0000]])

ReLU函数有个inplace参数,如果设为True,它会把输出直接覆盖到输入中,这样可以节省内存/显存。之所以可以覆盖是因为在计算ReLU的反向传播时,只需根据输出就能够推算出反向传播的梯度。

但是只有少数的autograd操作支持inplace操作(如tensor.sigmoid_()),除非你明确地知道自己在做什么,否则一般不要使用inplace操作。

在以上的例子中,基本上都是将每一层的输出直接作为下一层的输入,这种网络称为前馈传播网络(feedforward neural network)。

⚠️

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

1)Sequential

#Sequential的三种写法
#第一种
net1 = nn.Sequential()
net1.add_module('conv', nn.Conv2d(,,))
net1.add_module('batchnorm', nn.BatchNorm2d())
net1.add_module('activation_layer', nn.ReLU()) #第二种
net2 = nn.Sequential(
nn.Conv2d(,,),
nn.BatchNorm2d(),
nn.ReLU()
) #第三种
from collections import OrderedDict
net3 = nn.Sequential(OrderedDict([
('conv1', nn.Conv2d(,,)),
('bn1', nn.BatchNorm2d()),
('relu1',nn.ReLU())
]))
print('net1:', net1)
print('net2:', net2)
print('net3:', net3)

返回:

net1: Sequential(
(conv): Conv2d(, , kernel_size=(, ), stride=(, ))
(batchnorm): BatchNorm2d(, eps=1e-, momentum=0.1, affine=True, track_running_stats=True)
(activation_layer): ReLU()
)
net2: Sequential(
(): Conv2d(, , kernel_size=(, ), stride=(, ))
(): BatchNorm2d(, eps=1e-, momentum=0.1, affine=True, track_running_stats=True)
(): ReLU()
)
net3: Sequential(
(conv1): Conv2d(, , kernel_size=(, ), stride=(, ))
(bn1): BatchNorm2d(, eps=1e-, momentum=0.1, affine=True, track_running_stats=True)
(relu1): ReLU()
)
#可根据名字后序号取出子module
net1.conv, net2[], net3.conv1

返回:

(Conv2d(, , kernel_size=(, ), stride=(, )),
Conv2d(, , kernel_size=(, ), stride=(, )),
Conv2d(, , kernel_size=(, ), stride=(, )))

调用方式为:

input = t.rand(,,,)
output1 = net1(input)
output2 = net2(input)
output3 = net3(input)
output4 = net3.relu1(net1.batchnorm(net1.conv(input)))

2)ModuleList

modellist = nn.ModuleList([nn.Linear(,), nn.ReLU(), nn.Linear(,)])
input = t.randn(,)
for model in modellist:
input = model(input)
print(input)

返回:

tensor([[-0.6547,  0.8027]], grad_fn=<AddmmBackward>)
# 下面会报错,因为modellist没有实现forward方法
output = modellist(input)

看到这里,读者可能会问,为何不直接使用Python中自带的list,而非要多此一举呢?这是因为ModuleListModule的子类,当在Module中使用它的时候,就能自动识别为子module。

下面举一个实现forward的例子进行说明:

class MyModule(nn.Module):
def __init__(self):
super(MyModule, self).__init__()
self.list = [nn.Linear(,), nn.ReLU()]
self.module_list = nn.ModuleList([nn.Conv2d(,,), nn.ReLU()]) def forward(self):
pass
model = MyModule()
model

返回:

MyModule(
(module_list): ModuleList(
(): Conv2d(, , kernel_size=(, ), stride=(, ))
(): ReLU()
)
)

查看参数:

for name, param in model.named_parameters():
print(name, param.size())

返回:

module_list..weight torch.Size([, , , ])
module_list..bias torch.Size([])

可见,list中的子module并不能被主module所识别,而ModuleList中的子module能够被主module所识别。这意味着如果用list保存子module,将无法调整其参数,因其未加入到主module的参数中。

除ModuleList之外还有ParameterList,其是一个可以包含多个parameter的类list对象。在实际应用中,使用方式与ModuleList类似。

如果在构造函数__init__中用到list、tuple、dict等对象时,一定要思考是否应该用ModuleList或ParameterList代替。

3.循环神经网络层(后面好好看看)

近些年随着深度学习和自然语言处理的结合加深,RNN的使用也越来越多,关于RNN的基础知识,推荐阅读colah的文章1入门。PyTorch中实现了如今最常用的三种RNN:RNN(vanilla RNN)、LSTM和GRU。此外还有对应的三种RNNCell。

RNN和RNNCell层的区别在于前者一次能够处理整个序列,而后者一次只处理序列中一个时间点的数据,前者封装更完备更易于使用,后者更具灵活性。实际上RNN层的一种后端实现方式就是调用RNNCell来实现的。

t.manual_seed()
# 输入:batch_size=,序列长度都为2,序列中每个元素占4维
input = t.randn(, , )
# lstm输入向量4维,隐藏元3,1层
lstm = nn.LSTM(, , )
# 初始状态:1层,batch_size=,3个隐藏元
h0 = t.randn(, , )
c0 = t.randn(, , )
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()
input = t.randn(, , )
# 一个LSTMCell对应的层数只能是一层
lstm = nn.LSTMCell(, )
hx = t.randn(, )
cx = t.randn(, )
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>)
# 有4个词,每个词用5维的向量表示
embedding = nn.Embedding(, )
# 可以用预训练好的词向量初始化embedding
embedding.weight.data = t.arange(,).view(,)
input = t.arange(, , -).long()
output = embedding(input)
output

返回:

tensor([[, , , , ],
[, , , , ],
[ , , , , ]], grad_fn=<EmbeddingBackward>)

4.损失函数

这里以分类中最常用的交叉熵损失CrossEntropyloss为例说明:

#batch_size = ,计算对应每个类别的分数(只有两个类别)
score = t.randn(,)
#三个样本分别属于1,,1类,label必须是LongTensor
label = t.Tensor([,,]).long() #loss与普通的layer无差异
criterion = nn.CrossEntropyLoss()
loss = criterion(score, label)
loss #返回tensor(0.5944)

5.优化器

PyTorch将深度学习中常用的优化方法全部封装在torch.optim中,其设计十分灵活,能够很方便的扩展成自定义的优化方法。

所有的优化方法都是继承基类optim.Optimizer,并实现了自己的优化步骤。下面就以最基本的优化方法——随机梯度下降法(SGD)举例说明。这里需重点掌握:

  • 优化方法的基本使用方法
  • 如何对模型的不同部分设置不同的学习率
  • 如何调整学习率
#首先定义一个LeNet网络
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(,,),
nn.ReLU(),
nn.MaxPool2d(,),
nn.Conv2d(,,),
nn.ReLU(),
nn.MaxPool2d(,)
)
self.classifier = nn.Sequential( #全连接层
nn.Linear(**, ),
nn.ReLU(),
nn.Linear(, ),
nn.ReLU(),
nn.Linear(, )
)
def forward(self, x):
x = self.features(x)
x = x.view(-,**) #将数据扁平化处理用传入全连接层
x = self.classifier(x)
return x
net = Net()
from torch import optim
optimizer = optim.SGD(params=net.parameters(), lr=)
optimizer.zero_grad() #梯度清零,等价于net.zero_grad() input = t.randn(,,,)
output = net(input)
output.backward(output) #fake backward,后向传播,计算梯度 optimizer.step() #执行优化
# 为不同子网络设置不同的学习率,在finetune中经常用到
# 如果对某个参数不指定学习率,就使用最外层的默认学习率
optimizer = optim.SGD([
{'params' : net.features.parameters()}, #学习率为1e-
{'params' : net.classifier.parameters(), 'lr':1e-}
], lr=1e-)
optimizer

返回:

SGD (
Parameter Group
dampening:
lr: 1e-
momentum:
nesterov: False
weight_decay: Parameter Group
dampening:
lr: 0.01
momentum:
nesterov: False
weight_decay:
)
# 只为两个全连接层设置较大的学习率,其余层的学习率较小
special_layers = nn.ModuleList([net.classifier[], net.classifier[]])
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
dampening:
lr: 0.001
momentum:
nesterov: False
weight_decay: Parameter Group
dampening:
lr: 0.01
momentum:
nesterov: False
weight_decay:
)

对于如何调整学习率,主要有两种做法:

  • 一种是更简单也是较为推荐的做法——新建优化器,由于optimizer十分轻量级,构建开销很小,故而可以构建新的optimizer。但是后者对于使用动量的优化器(如Adam),会丢失动量等状态信息,可能会造成损失函数的收敛出现震荡等情况。
  • 一种是修改optimizer.param_groups中对应的学习率

1)新建优化器

#方法1:调整学习率,新建一个optimizer
old_lr = 0.1
optimizer1 = optim.SGD([
{'params': net.features.parameters()},
{'params': net.classifier.parameters(), 'lr':old_lr *0.1}
], lr = 1e-)
optimizer1

返回:

SGD (
Parameter Group
dampening:
lr: 1e-
momentum:
nesterov: False
weight_decay: Parameter Group
dampening:
lr: 0.010000000000000002
momentum:
nesterov: False
weight_decay:
)

2)调整学习率

#方法2:调整学习率,手动衰减,保存动量
for param_group in optimizer.param_groups:
param_group['lr'] *= 0.1
optimizer

返回:

SGD (
Parameter Group
dampening:
lr: 0.0001
momentum:
nesterov: False
weight_decay: Parameter Group
dampening:
lr: 0.001
momentum:
nesterov: False
weight_decay:
)

3.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 = t.randn(,)
model = nn.Linear(,)
output1 = model(input)
#使用上面使用的w,b,两种写法返回的结果是相同的
output2 = nn.functional.linear(input, model.weight, model.bias)
output1 == output2

返回:

tensor([[, , , ],
[, , , ]], dtype=torch.uint8)
b = nn.functional.relu(input)
b2 = nn.ReLU()(input)
b == b2

返回:

tensor([[, , ],
[, , ]], 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(,,)
self.conv2 = nn.Conv2d(,,)
self.fc1 = nn.Linear(**, )
self.fc2 = nn.Linear(,)
self.fc3 = nn.Linear(, ) def forward(self,x):
x = F.pool(F.relu(self.conv1(x)),)
x = F.pool(F.relu(self.conv2(x)),)
x = x.view(-, **)
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(,))
self.bias = nn.Parameter(t.zeros())
def forward(self):
return F.linear(input, weight, bias)

4.初始化策略

在深度学习中参数的初始化十分重要,良好的初始化能让模型更快收敛,并达到更高水平,而糟糕的初始化则可能使得模型迅速瘫痪。

PyTorch中nn.Module的模块参数都采取了较为合理的初始化策略,因此一般不用我们考虑,当然我们也可以用自定义初始化去代替系统的默认初始化。而当我们在使用Parameter时,自定义初始化则尤为重要,因t.Tensor()返回的是内存中的随机数,很可能会有极大值,这在实际训练网络中会造成溢出或者梯度消失。

PyTorch中nn.init模块就是专门为初始化而设计,如果某种初始化策略nn.init不提供,用户也可以自己直接初始化。

使用的初始化策略是:

torch.nn.init.xavier_normal_(tensor, gain=)

参数:

  • tensor – n维的torch.Tensor
  • gain - 可选的缩放因子

用一个正态分布生成值,填充输入的张量或变量。结果张量中的值采样自均值为0,标准差为gain * sqrt(2/(fan_in + fan_out))的正态分布。也被称为Glorot initialisation.

1)直接初始化:

#利用nn.init初始化
from torch.nn import init
linear = nn.Linear(,) t.manual_seed()
#等价于linear.weight.data.normal_(, std),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)

2)手动初始化

#手动初始化
import math
t.manual_seed() #xavier初始化的计算公式
std = math.sqrt()/math.sqrt(.)#+ =
linear.weight.data.normal_(, 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') != -:
#init linear
params[] #weight
params[] #bias
elif name.find('conv') != -:
pass
elif name.find('norm') != -:
pass

5.nn.Module深入分析

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

其中每个属性的解释如下:

  • _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'].

下面举例说明:

1)定义网络:

class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
#等价于self.register_parameter('param1', nn.Parameter(t.randn(,)))
self.param1 = nn.Parameter(t.rand(,))
self.submodel1 = nn.Linear(,)
def forward(self, input):
x = self.param1.mm(input)
x = self.submodel1(x)
return x
net = Net()
net

返回:

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

2)

net._modules #查看设置的子模块

返回:

OrderedDict([('submodel1', Linear(in_features=, out_features=, bias=True))])

另一种查看子模块方法:

for name, submodel in net.named_modules():
print(name, submodel)

返回:

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

nn.Module在实际使用中可能层层嵌套,一个module包含若干个子module,每一个子module又包含了更多的子module。

为方便用户访问各个子module,nn.Module实现了很多方法,如函数children可以查看直接子module,函数module可以查看所有的子module(包括当前module)。

与之相对应的还有函数named_childennamed_modules,其能够在返回module列表的同时返回它们的名字。

3)

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)

另一种查看参数方法,上面的方法没办法查看到层中使用的w,b参数,下面的方法可以查看w,b参数:

for name, param in net.named_parameters():
print(name, param.size())

返回:

param1 torch.Size([, ])
submodel1.weight torch.Size([, ])
submodel1.bias torch.Size([])

4)_buffers

bn = nn.BatchNorm1d()
input = t.rand(,)
output = bn(input)
bn._buffers #上一次前向传播结果

返回:

OrderedDict([('running_mean', tensor([0.0514, 0.0749])),
('running_var', tensor([0.9116, 0.9068])),
('num_batches_tracked', tensor())])

5)training

input = t.arange(, ).float().view(,)
model = nn.Dropout()
#在训练阶段,会有一半的值被设置为0
model(input)

返回:

tensor([[ .,  .,  .,  .],
[ ., ., ., .],
[ ., ., ., .]])
#如果将training设置为False,那么dropout在测试阶段将什么都不做
model.training = False
model(input)

返回:

tensor([[ .,  .,  .,  .],
[ ., ., ., .],
[ ., ., ., .]])

对于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)

6)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()

7)__getattr__ / __setattr__
nn.Module
对象在构造函数中的行为看起来有些怪异,如果想要真正掌握其原理,就需要看两个魔法方法__getattr____setattr__

在Python中有两个常用的buildin方法getattrsetattrgetattr(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是否为Parameternn.Module对象,如果是则将这些对象加到_parameters_modules两个字典中,而如果是其它类型的对象,如Variablelistdict等,则调用默认的操作,将这个值保存在__dict__中。

1》

module = nn.Module() #直接使用nn.Module()对象
module.param = nn.Parameter(t.ones(,)) #设置参数
module._parameters

返回:

OrderedDict([('param', Parameter containing:
tensor([[., .],
[., .]], requires_grad=True))])

2》

submodule1 = nn.Linear(,)
submodule2 = nn.Linear(,)
module_list = [submodule1, submodule2]
#对于list对象,调用buildin函数,保存在__dict__中
module.submodules = module_list #设置module,因为这里使用的是list,所以会存放在__dict__
print('_modules:', module._modules)
print("__dict__['submodules']:", module.__dict__.get('submodules'))

返回:

_modules: OrderedDict()
__dict__['submodules']: [Linear(in_features=, out_features=, bias=True), Linear(in_features=, out_features=, bias=True)]

3》

module_list = nn.ModuleList(module_list)#将上面的list类型转成nn.Module对象类型
module.submodules = module_list
#判断是否为nn.Module对象类型
print('ModuleList is instance of nn.Module: ', isinstance(module_list, nn.Module))
print('_modules: ', module._modules) #这样值就会存储在这里,而不是__dict__
print("__dict__['submodules']:", module.__dict__.get('submodules'))

返回:

ModuleList is instance of nn.Module:  True
_modules: OrderedDict([('submodules', ModuleList(
(): Linear(in_features=, out_features=, bias=True)
(): Linear(in_features=, out_features=, bias=True)
))])
__dict__['submodules']: None

4》

_modules_parameters中的item未保存在__dict__中,所以默认的getattr方法无法获取它,因而nn.Module实现了自定义的__getattr__方法,如果默认的getattr无法处理,就调用自定义的__getattr__方法,尝试从_modules_parameters_buffers这三个字典中获取。

getattr(module, 'training')#等价于module.training
#如果没有得到值,就会调用module.__getattr__('training')

返回:

True
module.attr1 =
getattr(module, 'attr1') #返回2
getattr(module, 'param')

返回:

Parameter containing:
tensor([[., .],
[., .]], requires_grad=True)

8)state_dict()/load_state_dict()

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

#保存模型
t.save(net.state_dict(), 'net.pth') #然后就会在本地文件夹中生成一个net.pth文件 #加载已经保存的模型
net2 = Net()
net2.load_state_dict(t.load('net.pth'))

9)运行在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
new_net = nn.DataParallel(net, device_ids=[, ])
output = new_net(input) # method
output = nn.parallel.data_parallel(new_net, input, device_ids=[, ])

DataParallel并行的方式,是将输入一个batch的数据均分成多份,分别送到对应的GPU进行计算,各个GPU得到的梯度累加。与Module相关的所有数据也都会以浅复制的方式复制多份,在此需要注意,在module中属性应该是只读的。

6.nn和autograd的关系

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进行封装则更为简单高效。

7.小试牛刀:搭建ResNet

Kaiming He的深度残差网络(ResNet)[^7]在深度学习的发展中起到了很重要的作用,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或用函数生成相应的module make_layer
  • nn.Module和nn.Functional结合使用
  • 尽量使用nn.Seqential

[^7]: He K, Zhang X, Ren S, et al. Deep residual learning for image recognition[C]//Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2016: 770-778.

from torch import nn
import torch as t
from torch.nn import functional as F

定义网络:

class ResidualBlock(nn.Module):
'''
实现子module: Residual Block
'''
def __init__(self, inchannel, outchannel, stride=, shortcut=None):
super(ResidualBlock, self).__init__()
self.left = nn.Sequential(
nn.Conv2d(inchannel,outchannel,,stride, ,bias=False),
nn.BatchNorm2d(outchannel),
nn.ReLU(inplace=True),
nn.Conv2d(outchannel,outchannel,,,,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=):
super(ResNet, self).__init__()
# 前几层图像转换
self.pre = nn.Sequential(
nn.Conv2d(, , , , , bias=False),
nn.BatchNorm2d(),
nn.ReLU(inplace=True),
nn.MaxPool2d(, , )) # 重复的layer,分别有3,,,3个residual block
self.layer1 = self._make_layer( , , )
self.layer2 = self._make_layer( , , , stride=)
self.layer3 = self._make_layer( , , , stride=)
self.layer4 = self._make_layer( , , , stride=) #分类用的全连接
self.fc = nn.Linear(, num_classes) def _make_layer(self, inchannel, outchannel, block_num, stride=):
'''
构建layer,包含多个residual block
'''
shortcut = nn.Sequential(
nn.Conv2d(inchannel,outchannel,,stride, bias=False),
nn.BatchNorm2d(outchannel)) layers = []
layers.append(ResidualBlock(inchannel, outchannel, stride, shortcut)) for i in range(, 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, )
x = x.view(x.size(), -)
return self.fc(x)

调用:

model = ResNet()
input = t.randn(, , , )
o = model(input)

感兴趣的读者可以尝试实现Google的Inception网络结构或ResNet的其它变体,看看如何能够简洁明了地实现它,实现代码尽量控制在80行以内(本例去掉空行和注释总共不超过50行)。

另外,与PyTorch配套的图像工具包torchvision已经实现了深度学习中大多数经典的模型,其中就包括ResNet34,读者可以通过下面两行代码使用:

from torchvision import models
model = models.resnet34()

本例中ResNet34的实现就是参考了torchvision中的实现并做了简化,感兴趣的读者可以阅读相应的源码,比较这里的实现和torchvision中实现的不同。

深度学习框架PyTorch一书的学习-第四章-神经网络工具箱nn的更多相关文章

  1. 深度学习框架PyTorch一书的学习-第五章-常用工具模块

    https://github.com/chenyuntc/pytorch-book/blob/v1.0/chapter5-常用工具/chapter5.ipynb 希望大家直接到上面的网址去查看代码,下 ...

  2. 深度学习框架PyTorch一书的学习-第一/二章

    参考https://github.com/chenyuntc/pytorch-book/tree/v1.0 希望大家直接到上面的网址去查看代码,下面是本人的笔记 pytorch的设计遵循tensor- ...

  3. 深度学习框架PyTorch一书的学习-第六章-实战指南

    参考:https://github.com/chenyuntc/pytorch-book/tree/v1.0/chapter6-实战指南 希望大家直接到上面的网址去查看代码,下面是本人的笔记 将上面地 ...

  4. 深度学习框架PyTorch一书的学习-第三章-Tensor和autograd-2-autograd

    参考https://github.com/chenyuntc/pytorch-book/tree/v1.0 希望大家直接到上面的网址去查看代码,下面是本人的笔记 torch.autograd就是为了方 ...

  5. 深度学习框架PyTorch一书的学习-第三章-Tensor和autograd-1-Tensor

    参考https://github.com/chenyuntc/pytorch-book/tree/v1.0 希望大家直接到上面的网址去查看代码,下面是本人的笔记 Tensor Tensor可以是一个数 ...

  6. 深度学习框架PyTorch一书的学习-第七章-生成对抗网络(GAN)

    参考:https://github.com/chenyuntc/pytorch-book/tree/v1.0/chapter7-GAN生成动漫头像 GAN解决了非监督学习中的著名问题:给定一批样本,训 ...

  7. 《深度学习框架PyTorch:入门与实践》读书笔记

    https://github.com/chenyuntc/pytorch-book Chapter2 :PyTorch快速入门 + Chapter3: Tensor和Autograd + Chapte ...

  8. 《深度学习框架PyTorch:入门与实践》的Loss函数构建代码运行问题

    在学习陈云的教程<深度学习框架PyTorch:入门与实践>的损失函数构建时代码如下: 可我运行如下代码: output = net(input) target = Variable(t.a ...

  9. 神工鬼斧惟肖惟妙,M1 mac系统深度学习框架Pytorch的二次元动漫动画风格迁移滤镜AnimeGANv2+Ffmpeg(图片+视频)快速实践

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_201 前段时间,业界鼎鼎有名的动漫风格转化滤镜库AnimeGAN发布了最新的v2版本,一时间街谈巷议,风头无两.提起二次元,目前国 ...

随机推荐

  1. 关于TensorFlow你需要了解的9件事

    关于TensorFlow你需要了解的9件事 https://mp.weixin.qq.com/s/cEQAdLnueMEj0OQZtYvcuw 摘要:本文对近期在旧金山举办的谷歌 Cloud Next ...

  2. 28.Odoo产品分析 (四) – 工具板块(1) – 项目(1)

    查看Odoo产品分析系列--目录 "项目管理"是一个用于管理你的项目,且将它们与其他应用关联起来的非常灵活的模块,他允许您的公司管理项目阶段,分配团队,甚至跟踪与项目相关的时间和工 ...

  3. Linux 线程实现模型

    1.Linux 线程的调度实现可以有两种模型, 一种是完全由进程负责,进程内启动一个线程调度器,由进程内的线程调度器完成调度. 缺点是:(1)各个线程自己加主动释放cpu的流程 (2)进程可能阻塞,达 ...

  4. Python_基于Python同Linux进行交互式操作实现通过堡垒机访问目标机

    基于Python同Linux进行交互式操作实现通过堡垒机访问目标机   by:授客 QQ:1033553122 欢迎加入全国软件测试交流群:7156436 实现功能 1 测试环境 1 代码实践 2 注 ...

  5. web自动化 基于python+Selenium+PHP+Ftp实现的轻量级web自动化测试框架

    基于python+Selenium+PHP+Ftp实现的轻量级web自动化测试框架   by:授客 QQ:1033553122     博客:http://blog.sina.com.cn/ishou ...

  6. EasyUI动画效果

    1.jQuery动画效果 a)基本效果 >show(speed),显示 >hide(speed),隐藏 >toggle(speed),切换 b)滑动的效果 >slideUp(s ...

  7. coTurn测试程序之 turnutils_uclient

    接着对使用coTurn搭建的STUN/TURN服务使用turnutils_uclient程序测试其TURN服务是否正常. 直接连接服务测试服务是否正常.为保证测试使用的服务是TURN服务,在TURN服 ...

  8. java中利用dom4j解析XML文件

    官网下载Dom4j地址:https://dom4j.github.io/ 注意:使用Dom4j开发,需下载dom4j相应的jar文件 题目:后台利用dom4j解析student.xml文件,并返回Li ...

  9. .gho文件检查

    虽然目前windows10的接受程度越来越广泛,但我接触到的一些非IT人士还是钟爱于windows7系统,本文记录一下在使用ghost还原系统遇到的问题. gho还原失败 在还原ghost系统过程中, ...

  10. LeetCode算法题-Factorial Trailing Zeroes(Java实现)

    这是悦乐书的第183次更新,第185篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第42题(顺位题号是172).给定一个整数n,返回n!中的尾随零数.例如: 输入:3 输 ...