目录
4.1 模型构造
在线性回归和softmax回归中已经陆续用到了这些方法,这里系统回顾。
4.1.2 最简单的开始分析
import torch
from torch import nn
from torch.nn import functional as F
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
X = torch.rand(2, 20)
net(X)
在这个例子中,我们通过实例化nn.Sequential来构建我们的模型,层的执行顺序是作为参数传递的。简而言之,nn.Sequential定义了一种特殊的Module,即在PyTorch中表示一个块的类。它维护了一个由Module组成的有序列表,注意,两个全连接层都是Linear类的实例,Linear类本身就是Module的子类。正向传播(forward)函数也非常简单:它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。注意,到目前为止,我们一直在通过net(X)调用我们的模型来获得模型的输出。这实际上是net.call(X)的简写。
4.1.2 继承MODULE类来构造模型(自定义块)
Module类是nn模块里提供的一个模型构造类,是所有神经网络模块的基类。此处用继承Module类来构造本节开头提到的MLP,这里重载了Module类的__init__函数和forward函数,分别用于创建模型参数和定义前向计算。
class MLP(nn.Module):
# 用模型参数声明层。这里,我们声明两个全连接的层
def __init__(self):
# 调用`MLP`的父类`Block`的构造函数来执行必要的初始化。
# 这样,在类实例化时也可以指定其他函数参数,例如模型参数`params`(稍后将介绍)
super().__init__()
self.hidden = nn.Linear(20, 256) # 隐藏层
self.out = nn.Linear(256, 10) # 输出层
# 定义模型的正向传播,即如何根据输入`X`返回所需的模型输出
def forward(self, X):
# 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
return self.out(F.relu(self.hidden(X)))
以上的MLP类无需定义反向传播,pytorch中会自动求梯度而自动生成反向传播所需要的forward函数来完成前向计算
我们还可以实例化MLP类得到模型变量net, 下面的代码初始化net并传入输入数据x做一次前向计算。 其中,net(X)会调用MLP继承自Module类的_call_函数,此函数将调用MLP类定义的forward函数完成前向计算
我们在构造函数中实例化多层感知机的层,然后在每次调用正向传播函数时调用这些层。注意一些关键细节。首先,我们定制的__init__函数通过super().init()调用父类的__init__函数,省去了重复编写适用于大多数块的模版代码的痛苦。然后我们实例化两个全连接层,分别为self.hidden和self.out。注意,除非我们实现一个新的运算符,否则我们不必担心反向传播函数或参数初始化,系统将自动生成这些。
net = MLP()
net(X)
output:
tensor([[-0.2297, -0.0396, -0.3351, 0.0677, 0.2896, -0.0685, -0.1796, -0.2028,
0.1920, -0.2273],
[-0.1520, -0.0249, -0.3276, -0.0050, 0.1500, -0.0456, -0.0526, -0.1227,
0.1207, -0.1618]], grad_fn=<AddmmBackward>)
4.1.3 顺序块
为了构建我们自己的简化的MySequential,我们只需要定义两个关键函数: 1. 一种将块逐个追加到列表中的函数。 2. 一种正向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
下面的MySequential类提供了与默认Sequential类相同的功能。
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for block in args:
# 这里,`block`是`Module`子类的一个实例。我们把它保存在'Module'类的成员变量
# `_children` 中。`block`的类型是OrderedDict。
self._modules[block] = block
def forward(self, X):
# OrderedDict保证了按照成员添加的顺序遍历它们
for block in self._modules.values():
X = block(X)
return X
我们在之前也学习过
net = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10),
)
print(net)
output:
Sequential(
(0): Linear(in_features=784, out_features=256, bias=True)
(1): ReLU()
(2): Linear(in_features=256, out_features=10, bias=True)
)
4.1.4 嵌套调用
'''第一个块'''
class FixedHiddenMLP(nn.Module):
def __init__(self):
super().__init__()
# 不计算梯度的随机权重参数。因此其在训练期间保持不变。
self.rand_weight = torch.rand((20, 20), requires_grad=False)
self.linear = nn.Linear(20, 20)
def forward(self, X):
X = self.linear(X)
# 使用创建的常量参数以及`relu`和`dot`函数。
X = F.relu(torch.mm(X, self.rand_weight) + 1)
# 复用全连接层。这相当于两个全连接层共享参数。
X = self.linear(X)
# 控制流
while X.abs().sum() > 1:
X /= 2
return X.sum()
'''第二个块'''
class NestMLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
nn.Linear(64, 32), nn.ReLU())
self.linear = nn.Linear(32, 16)
def forward(self, X):
return self.linear(self.net(X))
'''嵌套调用两个块'''
chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
chimera(X)
做个总结,
层也是块。
一个块可以由许多层组成。
一个块可以由许多块组成。
块可以包含代码。
块负责大量的内部处理,包括参数初始化和反向传播。
层和块的顺序连接由Sequential块处理。
虽然Sequential可以使得模型构造简单,但是直接继承Module类可以扩展模型构造的灵活性。
4.2 模型参数的访问、初始化和共享
我们先定义一个含单隐藏层的多层感知机,依旧使用默认方式初始化参数,并做一次前向计算;我们从nn导入了init模块,包含多种模型初始化方法
import torch
from torch import nn
from torch.nn import init
net = nn.Sequential(nn.Linear(4, 3), nn.ReLU(), nn.Linear(3, 1)) #已经默认初始化
print(net)
X = torch.rand(2, 4)
Y = net(X).sum()
print(Y)
output:
Sequential(
(0): Linear(in_features=4, out_features=3, bias=True)
(1): ReLU()
(2): Linear(in_features=3, out_features=1, bias=True)
)
tensor(0.9349, grad_fn=<SumBackward0>)
4.2.1 访问模型参数
我们可以通过Module类的parameters()或者named_parameters方法来访问所有参数,后者除了返回参数还会返回其名字
print(type(net.named_parameters()))
for name, param in net.named_parameters():
print(name, param.size())
output:
<class 'generator'>
0.weight torch.Size([3, 4])
0.bias torch.Size([3])
2.weight torch.Size([1, 3])
2.bias torch.Size([1])
可以看出,返回的名字自动加上了层数的索引作为前缀;对于使用Sequential类构造的神经网络,可以用方括号[]访问任一层
for name, param in net[0].named_parameters():
print(name, param.size(), type(param))
output:
weight torch.Size([3, 4]) <class 'torch.nn.parameter.Parameter'>
bias torch.Size([3]) <class 'torch.nn.parameter.Parameter'>
param返回的类型是torch.nn.parameter.Paremeter,这是Tensor的子类;如果一个Tensor是Parameter,将会自动被添加到模型的参数列表里
4.2.2 初始化参数
如果在init模块中没有想要的初始化方法,就要自己实现,这一节内容书上较为详尽,用时看书即可
4.2.3 共享模型参数
方法:1.Module类的forward函数里多次调用同一个层
2.如果传入Sequential的模块是同一个Module实例,参数也是共享的
4.2.4 自定义层
可以利用Module类自定义层,从而被反复调用
这一节建议参考第二版书
4.2.5 读写文件
4.2.5.1 加载和保存张量
对于单个张量,我们可以直接调用load和save函数分别读写它们。这两个函数都要求我们提供一个名称,save要求将要保存的变量作为输入。
import torch
from torch import nn
from torch.nn import functional as F
x = torch.arange(4)
torch.save(x, 'x-file')
我们现在可以将存储在文件中的数据读回内存。
x2 = torch.load("x-file")
print(x2)
output:
tensor([0, 1, 2, 3])
我们可以存储一个张量列表,然后把它们读回内存。
x = torch.arange(4)
y = torch.zeros(4)
torch.save([x, y], 'x-files')
x2, y2 = torch.load('x-files')
print((x2, y2))
ouput:
(tensor([0, 1, 2, 3]), tensor([0., 0., 0., 0.]))
甚至可以写入或读取从字符串映射到张量的字典。当我们要读取或写入模型中的所有权重时,这很方便。
x = torch.arange(4)
y = torch.zeros(4)
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
print(mydict2['x'])
print(mydict2['y'])
print(mydict2)
output:
tensor([0, 1, 2, 3])
tensor([0., 0., 0., 0.])
{'x': tensor([0, 1, 2, 3]), 'y': tensor([0., 0., 0., 0.])}
4.2.5.2 加载和保存模型参数
深度学习框架提供了内置函数来保存和加载整个网络。需要注意的一个重要细节是,这将保存模型的参数而不是保存整个模型。例如,如果我们有一个3层多层感知机,我们需要单独指定结构。因为模型本身可以包含任意代码,所以模型本身难以序列化。因此,为了恢复模型,我们需要用代码生成结构,然后从磁盘加载参数。让我们从熟悉的多层感知机开始尝试一下。
在pytorch中,Module的可学习参数, 模块模型包含在参数中。state_dict是一个从参数名称映射到参数Tensor的字典对象
class MLP(nn.Module):
def __init__(self):
super(MLP, self).__init__()
self.hidden = nn.Linear(3, 2)
self.act = nn.ReLU()
self.output = nn.Linear(2, 1)
def forward(self, x):
a = self.act(self.hidden(x))
return self.output(a)
net = MLP()
print(net.state_dict())
output:
OrderedDict([('hidden.weight', tensor([[-0.3529, 0.4916, 0.5747],
[ 0.2375, -0.1873, 0.4313]])), ('hidden.bias', tensor([ 0.3667, -0.4421])), ('output.weight', tensor([[0.0303, 0.2033]])), ('output.bias', tensor([-0.7012]))])
只有具有可学习参数的层(卷积层,线性层)才有state_dict中的条目;优化器(optim)也有一个state_dict,其中包含关于优化器状态以及所使用的超参数信息
optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
output:
<bound method Optimizer.state_dict of SGD (
Parameter Group 0
dampening: 0
lr: 0.001
momentum: 0.9
nesterov: False
weight_decay: 0
)>
4.2.5.3 保存和加载模型
有两种方法:
1.仅保存和加载模型参数(state_dict);
2.保存和加载整个模型
1.保存和加载state_dict (这是较为推荐的方法)
'''保存'''
torch.save(model.state_dict(), PATH) #推荐后缀名是pt或者pth
'''加载'''
model = TheModelClass(*args, **kwargs)
model.load_state_dict(torch.load(PATH))
2.保存和加载整个模型
'''保存'''
torch.save(model, PATH)
'''加载'''
model = torch.load(PATH)
实例展示:
X = torch.randn(2, 3)
Y = net(X)
PATH = "./net.pt"
torch.save(net.state_dict(), PATH)
net2 = MLP()
net2.load_state_dict(torch.load(PATH))
Y2 = net2(X)
Y2 == Y
OUT:
tensor([[True],
[True]])
由于net和net2有一样的模型参数,故对于一个x进行计算,结果是相同的。
4.3 GPU计算
由于本人电脑是CPU型,故等用到GPU时,再来更新这部分内容。