目录
1. 层和块
1.1 自定义块
1.2 顺序块
1.3 一些灵活写法的例子(主要是展示写法自由度)
2. 参数管理
2.1 参数访问
2.1.1 目标函数
2.1.2 一次性访问所有参数
2.1.3 从嵌套块收集参数
2.2 参数初始化
2.2.1. 内置初始化
2.2.2 自定义初始化
2.2.3 参数绑定
3. 自定义层
3.1 不带参数的层
3.2 带参数的层
4. 读写文件
4.1 加载和保存张量
4.2 加载和保存模型参数
5. QA环节
5.1 如果类别转化采用独热编码,把类别变量转化为伪变量的时候内存炸了怎么办?
5.2 object数据该怎么处理,离散类别太多了,维度炸了?
5.3 我们创建好网络之后,torch是按声明规则给参数初始化的?
为了实现复杂的网络,我们引入了神经网络块的概念。 块(block)可以描述单个层、由多个层组成的组件或整个模型本身。 使用块进行抽象的一个好处是可以将一些块组合成更大的组件, 这一过程通常是递归的。 通过定义代码来按需生成任意复杂度的块, 我们可以通过简洁的代码实现复杂的神经网络。
在构造自定义块之前,我们先回顾一下第三章多层感知机的代码。 下面的代码生成一个网络,其中包含一个具有256个单元和ReLU激活函数的全连接隐藏层, 然后是一个具有10个隐藏单元且不带激活函数的全连接输出层。
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
的子类。 另外,到目前为止,我们一直在通过net(X)
调用我们的模型来获得模型的输出。 这实际上是net.__call__(X)
的简写。 这个前向传播函数非常简单: 它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。
在下面的代码片段中,我们从零开始编写一个块。 它包含一个多层感知机,其具有256个隐藏单元的隐藏层和一个10维输出层。 注意,下面的MLP
类继承了表示块的类。 我们的实现只需要提供我们自己的构造函数(Python中的__init__
函数)和前向传播函数。这里出现的F.relu函数可以通过输入的tensor进行相应激活函数的计算,而此前的nn.ReLU()是生成一个对应传入参数大小的ReLU层
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)))
块的一个主要优点是它的多功能性。 我们可以子类化块以创建层(如全连接层的类)、 整个模型(如上面的MLP
类)或具有中等复杂度的各种组件。 我们在接下来的章节中充分利用了这种多功能性, 比如在处理卷积神经网络时。
下面的MySequential
类提供了与默认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
_modules
的主要优点是: 在模块的参数初始化过程中, 系统知道在_modules
字典中查找需要初始化参数的子块。当MySequential
的前向传播函数被调用时, 每个添加的块都按照它们被添加的顺序执行。 现在可以使用我们的MySequential
类重新实现多层感知机。
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)
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()
net = FixedHiddenMLP()
net(X) ''' tensor(-0.0949, grad_fn=) '''
我们可以混合搭配各种组合块的方法。 在下面的例子中,我们以一些想到的方法嵌套块。
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)
有时我们希望提取参数,以便在其他环境中复用它们, 将模型保存下来,以便它可以在其他软件中执行, 或者为了获得科学的理解而进行检查。这里我们以一个单层感知机为例子。
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)
'''tensor([[0.2044],
[0.2064]], grad_fn=)'''
我们从已有模型中访问参数。 当通过Sequential
类定义模型时, 我们可以通过索引来访问模型的任意层。 这就像模型是一个列表一样,每层的参数都在其属性中。 如下所示,我们可以检查第二个全连接层的参数。
print(net[2].state_dict()) '''
OrderedDict([('weight', tensor([[ 0.0251, -0.2952, -0.1204,
0.3436, -0.3450, -0.0372, 0.0462, 0.2307]])),
('bias', tensor([0.2871]))])'''
输出的结果告诉我们一些重要的事情: 首先,这个全连接层包含两个参数,分别是该层的权重和偏置。 两者都存储为单精度浮点数(float32)。 注意,参数名称允许唯一标识每个参数,即使在包含数百个层的网络中也是如此。
每个参数都表示为参数类的一个实例。 要对参数执行任何操作,首先我们需要访问底层的数值。 有几种方法可以做到这一点。有些比较简单,而另一些则比较通用。 下面的代码从第二个全连接层(即第三个神经网络层)提取偏置, 提取后返回的是一个参数类实例,并进一步访问该参数的值。
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data) '''
Parameter containing:
tensor([0.2871], requires_grad=True)
tensor([0.2871]) '''
参数是复合的对象,包含值、梯度和额外信息。 这就是我们需要显式参数值的原因。 除了值之外,我们还可以访问每个参数的梯度。 在上面这个网络中,由于我们还没有调用反向传播,所以参数的梯度处于初始状态。
net[2].weight.grad == None '''
True '''
当我们需要对所有参数执行操作时,逐个访问它们可能会很麻烦。 当我们处理更复杂的块(例如,嵌套块)时,情况可能会变得特别复杂, 因为我们需要递归整个树来提取每个子块的参数。 下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层。
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
'''也可以直接用对应参数的名字访问到
tensor([0.2871]) '''
让我们看看,如果我们将多个块相互嵌套,参数命名约定是如何工作的。 我们首先定义一个生成块的函数(可以说是“块工厂”),然后将这些块组合到更大的块中。
def block1():
return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
nn.Linear(8, 4), nn.ReLU())
def block2():
net = nn.Sequential()
for i in range(4):
# 在这里嵌套
net.add_module(f'block {i}', block1())
return net
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X) '''
tensor([[0.1713],
[0.1713]], grad_fn=) '''
设计了网络后,我们看看它是如何工作的。
print(rgnet) '''
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)
) '''
因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。 下面,我们访问第一个主要的块中、第二个子块的第一层的偏置项。
我们在动手学习深度学习(总结梳理)——7.数值稳定性和模型初始化中讨论了良好初始化的必要性。深度学习框架提供默认随机初始化, 也允许我们创建自定义初始化方法, 满足我们通过其他规则实现初始化权重。
下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量, 且将偏置参数设置为0。
def init_normal(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=0, std=0.01)
nn.init.zeros_(m.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0] '''
(tensor([-0.0145, 0.0053, 0.0055, -0.0044]), tensor(0.))'''
我们还可以将所有参数初始化为给定的常数,比如初始化为1。但是千万别这样,会让神经网络训练失效。
def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1)
nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0] '''
(tensor([1., 1., 1., 1.]), tensor(0.))'''
我们还可以对某些块应用不同的初始化方法。 例如,下面我们使用第七章 数值稳定性和模型初始化所学的Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42。
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)
print(net[0].weight.data[0])
print(net[2].weight.data) '''
tensor([-0.4792, 0.4968, 0.6094, 0.3063])
tensor([[42., 42., 42., 42., 42., 42., 42., 42.]]) '''
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
有时我们希望在多个层间共享参数: 我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。
''' 我们需要给共享层一个名称,以便可以引用它的参数'''
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])
这个例子表明第三个和第五个神经网络层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 因此,如果我们改变其中一个参数,另一个参数也会改变。 你可能会思考:当参数绑定时,梯度会发生什么情况? 答案是由于模型参数包含梯度,因此在反向传播期间第二个隐藏层 (即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。
深度学习成功背后的一个因素是神经网络的灵活性: 我们可以用创造性的方式组合不同的层,从而设计出适用于各种任务的架构。 例如,研究人员发明了专门用于处理图像、文本、序列数据和执行动态规划的层。 未来,你会遇到或要自己发明一个现在在深度学习框架中还不存在的层。 在这些情况下,你必须构建自定义层。
下面的CenteredLayer
类要从其输入中减去均值。 要构建它,我们只需继承基础层类并实现前向传播功能。
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()
让我们向该层提供一些数据,验证它是否能按预期工作。
layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5])) '''
tensor([-2., -1., 0., 1., 2.])'''
现在,我们可以将层作为组件合并到更复杂的模型中。
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
作为额外的健全性检查,我们可以在向该网络发送随机数据后,检查均值是否为0。 由于我们处理的是浮点数,因为存储精度的原因,我们仍然可能会看到一个非常小的非零数。
Y = net(torch.rand(4, 8))
Y.mean() '''
tensor(0., grad_fn=)'''
以上我们知道了如何定义简单的层,下面我们继续定义具有参数的层, 这些参数可以通过训练进行调整。 我们可以使用内置函数来创建参数,这些函数提供一些基本的管理功能。 比如管理访问、初始化、共享、保存和加载模型参数。 这样做的好处之一是:我们不需要为每个自定义层编写自定义的序列化程序。
现在,让我们实现自定义版本的全连接层。 回想一下,该层需要两个参数,一个用于表示权重,另一个用于表示偏置项。 在此实现中,我们使用修正线性单元作为激活函数。 该层需要输入参数:in_units
和units
,分别表示输入数和输出数。
class MyLinear(nn.Module):
def __init__(self, in_units, units):
super().__init__()
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)
接下来,我们实例化MyLinear
类并访问其模型参数。
linear = MyLinear(5, 3)
linear.weight '''
Parameter containing:
tensor([[-1.4779, -0.6027, -0.2225],
[ 1.1270, -0.6127, -0.2008],
[-2.1864, -1.0548, 0.2558],
[ 0.0225, 0.0553, 0.4876],
[ 0.3558, 1.1427, 1.0245]], requires_grad=True) '''
我们可以使用自定义层直接执行前向传播计算。
linear(torch.rand(2, 5)) '''
tensor([[0.0000, 0.0000, 0.2187],
[0.0000, 0.0000, 0.0000]]) '''
我们还可以使用自定义层构建模型,就像使用内置的全连接层一样使用自定义层。
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64)) '''
tensor([[ 7.4571],
[12.7505]]) '''
对于单个张量,我们可以直接调用load
和save
函数分别读写它们。 这两个函数都要求我们提供一个名称,save
要求将要保存的变量作为输入。在名字前面可以写入路径,对路径加以控制。
import torch
from torch import nn
from torch import functional as F
x = torch.arange(4)
torch.save(x, 'x-file')
''' 我们可以通过load直接把文件中的数据读入内存 '''
x2 = torch.load('x-file')
x2 '''
tensor([0, 1, 2, 3]) '''
我们可以存储一个张量列表,然后把它们读回内存,读入的格式也是保存时的列表格式。
y = torch.zeros(4)
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')
(x2, y2) '''
(tensor([0, 1, 2, 3]), tensor([0., 0., 0., 0.])) '''
我们甚至可以写入或读取从字符串映射到张量的字典。 当我们要读取或写入模型中的所有权重时,这很方便。
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
mydict2 '''
{'x': tensor([0, 1, 2, 3]), 'y': tensor([0., 0., 0., 0.])} '''
保存单个权重向量(或其他张量)确实有用, 但是如果我们想保存整个模型,并在以后加载它们, 单独保存每个向量则会变得很麻烦。 毕竟,我们可能有数百个参数散布在各处。 因此,深度学习框架提供了内置函数来保存和加载整个网络。 需要注意的一个重要细节是,这将保存模型的参数而不是保存整个模型。 例如,如果我们有一个3层多层感知机,我们需要单独指定架构。 因为模型本身可以包含任意代码,所以模型本身难以序列化。 因此,为了恢复模型,我们需要用代码生成架构, 然后从磁盘加载参数。 让我们从熟悉的多层感知机开始尝试一下。
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.hidden = nn.Linear(20, 256)
self.output = nn.Linear(256, 10)
def forward(self, x):
return self.output(F.relu(self.hidden(x)))
net = MLP()
X = torch.randn(size=(2, 20))
Y = net(X)
接下来,我们将模型的参数存储在一个叫做“mlp.params”的文件中。这里我们可以通过这个帖子了解关于参数列表的三个函数net.parameters() & net.named_parameters() & net.state_dict()
torch.save(net.state_dict(), 'mlp.params')
为了恢复模型,我们实例化了原始多层感知机模型的一个备份。 这里我们不需要随机初始化模型参数,而是直接读取文件中存储的参数。
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval() ''' output:
MLP(
(hidden): Linear(in_features=20, out_features=256, bias=True)
(output): Linear(in_features=256, out_features=10, bias=True)
) '''
由于两个实例具有相同的模型参数,在输入相同的X
时, 两个实例的计算结果应该相同。 让我们来验证一下。
Y_clone = clone(X)
Y_clone == Y ''' output:
tensor([[True, True, True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True, True, True]]) '''
在特征特别多的情况下,确实容易出现这类问题,有两种解决办法,首先可以使用稀疏矩阵去存。其次真的特别多的情况下,像在竞赛里面可能会用一些别的办法,例如利用他们的地址去存,或者是不要取句子,拿出他们的词去处理,之后NLP也会讲很多这类特征处理方法。
实在不好处理的情况下,直接将它拿掉后去训练,未必精度会差特别多,后续还会讲很多别处理办法
默认kaiming初始化,其实数据初始化才用什么样是比较接近的,它只是想办法将参数控制在一个合理,不会出现梯度变化奇怪的地方