目录
层和块
一个简单的多层感知机
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是PyTorch框架中的一个特殊模块。 这个模块允许你以线性顺序堆叠一系列的层(例如全连接层、卷积层、激活函数等),而无需显式定义一个独立的模型类。这种方式特别适合构造那些前向传播过程是简单的输入到输出直接流动,没有分支或者跳连的网络结构。nn.Sequantial模块中将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。
自定义模块
每个块都要事先如下基本功能:
-
将输入数据作为其前向传播函数的参数。
-
通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出。
-
计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。
-
存储和访问前向传播计算所需的参数。
-
根据需要初始化模型参数。
如下是上述MLP的自定义模块实现:
class MLP(nn.Module):
# 用模型参数声明层。这里,我们声明两个全连接的层
def __init__(self):
# 调用MLP的父类Module的构造函数来执行必要的初始化。
# 这样,在类实例化时也可以指定其他函数参数,例如模型参数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)))
forward函数以X
作为输入, 计算带有激活函数的隐藏表示,并输出其未规范化的输出值。
__init__函数通过调用父类的init函数省去了代码的重复编写。
框架将自动实现反向传播函数和参数初始化
自己实现一个Sequential类
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for idx, module in enumerate(args):
# 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
# 变量_modules中。_module的类型是OrderedDict
self._modules[str(idx)] = module
def forward(self, X):
# OrderedDict保证了按照成员添加的顺序遍历它们
for block in self._modules.values():
X = block(X)
return X
MySequantial可以传入多个层作为参数,__init__
函数将每个模块逐个添加到有序字典_modules
中。当MySequential
的前向传播函数被调用时, 每个添加的块都按照它们被添加的顺序执行。
在forward函数中执行代码
当模型需要更高的灵活性时,我们不能简单地依赖预定义的层,可以自己定义forward函数。
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和mm函数
X = F.relu(torch.mm(X, self.rand_weight) + 1)
# 复用全连接层。这相当于两个全连接层共享参数
X = self.linear(X)
# 控制流
while X.abs().sum() > 1:
X /= 2
return X.sum()
在这个FixedHiddenMLP
模型中,我们实现了一个隐藏层, 其权重(self.rand_weight
)在实例化时被随机初始化,之后为常量。 这个权重不是一个模型参数,因此它永远不会被反向传播更新。 然后,神经网络将这个固定层的输出通过一个全连接层。
嵌套块
上述各种模块定义方式可以嵌套使用,提升模型的灵活性。
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
块处理。
参数管理
首先关注具有单隐藏层的多层感知机,模型如下:
import torch
from torch import nn
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
net(X)
可以通过索引方式从已有模型中访问参数。
print(net[2].state_dict())
# OrderedDict([('weight', tensor([[-0.0427, -0.2939, -0.1894, 0.0220, -0.1709, -0.1522, -0.0334, -0.2263]])), ('bias', tensor([0.0887]))])
每层包含两个参数,分别是该层的权重weight和偏置bias,两者都存储为单精度浮点数float32。
目标参数
可以直接访问具体参数
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
'''
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([0.0887], requires_grad=True)
tensor([0.0887])
'''
参数是复合的对象,包含值、梯度和额外信息。 这就是我们需要显式参数值的原因。 除了值之外,我们还可以访问每个参数的梯度。
net[2].weight.grad == None
# True
因为这里还没有反向传播,所以梯度为None。
一次性访问所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
'''
('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))
'''
也可以通过名称访问权重。
net.state_dict()['2.bias'].data
从嵌套块收集参数
一个嵌套模块
Sequential(
(0): Sequential(
(block 0): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 1): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 2): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 3): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
)
(1): Linear(in_features=4, out_features=1, bias=True)
)
因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。
rgnet[0][1][0].bias.data
参数初始化
深度学习框架提供默认随机初始化, 也允许我们创建自定义初始化方法, 满足我们通过其他规则实现初始化权重。默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。 PyTorch的nn.init
模块提供了多种预置初始化方法。
内置初始化
def init_normal(m):
if type(m) == nn.Linear:
# init中定义了很多初始化方法,此处为正态分布随机数初始化
nn.init.normal_(m.weight, mean=0, std=0.01)
nn.init.zeros_(m.bias)
# 使用apply函数传入初始化的参数
net.apply(init_normal)
常量初始化
def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1)
nn.init.zeros_(m.bias)
net.apply(init_constant)
Xavier初始化
def init_xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
net[0].apply(init_xavier)
net[2].apply(init_42)
自定义初始化
def my_init(m):
if type(m) == nn.Linear:
print("Init", *[(name, param.shape)
for name, param in m.named_parameters()][0])
nn.init.uniform_(m.weight, -10, 10)
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
参数绑定
# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])
第三个和第五个神经网络层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 因此,如果我们改变其中一个参数,另一个参数也会改变。
自定义层
构造一个没有任何参数的自定义层。构建它,只需继承基础层类并实现前向传播功能。
import torch
import torch.nn.functional as F
from torch import nn
class CenteredLayer(nn.Module):
def __init__(self):
super().__init__()
def forward(self, X):
return X - X.mean()
可以将层作为组件合并到更复杂的模型中。
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
带参数的层
自定义版本的全连接层。 该层需要两个参数,一个用于表示权重,另一个用于表示偏置项。
class MyLinear(nn.Module):
def __init__(self, in_units, units):
super().__init__()
# 输入维度 x 输出维度的矩阵
self.weight = nn.Parameter(torch.randn(in_units, units))
# 初始化偏置量
self.bias = nn.Parameter(torch.randn(units,))
def forward(self, X):
linear = torch.matmul(X, self.weight.data) + self.bias.data
return F.relu(linear)
读写文件
加载和保存张量
对于单个张量,我们可以直接调用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')
x2
我们可以存储一个张量列表,然后把它们读回内存。
y = torch.zeros(4)
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')
(x2, y2)
我们甚至可以写入或读取从字符串映射到张量的字典。 方便读取和写入模型中的所有权重。
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
mydict2
加载和保存模型参数
对于一个神经网络,pytorch框架只保存权重参数,不保存模型的整个架构,即保存net.state_dict()。
# save(模型参数,文件名)
torch.save(net.state_dict(), 'mlp.params')
回复模型,将保存的模型参数传入新的模型实例即可。
#新的模型实例
clone = MLP()
# 加载模型参数
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()