环境:PyCharm + python3.8
1. 层和块
块(block)可以描述
- 单个层、
- 由多个层组成的组件
- 或整个模型本身。
使用块进行抽象的好处:
- 可将块组合成更大的组件(这一过程通常是递归) 如 图5.1.1所示。
- 通过定义代码来按需生成任意复杂度的块,可以通过简洁的代码实现复杂的神经网络。

从编程的角度来看,块由类(class)表示。
- 其子类都必须定义一个前向传播函数(将其输入转换为输出),且必须存储任何必需的参数(有些块不需要任何参数)。
- 最后,为了计算梯度,块必须具有反向传播函数。在定义自己的块时,由于自动微分(在 2.5节 中引入)提供了一些后端实现,我们只需要考虑前向传播函数和必需的参数。
下面代码生成一个网络,其中包含
- 具有 256个单元 和 ReLU激活函数 的全连接隐藏层,
- 具有 10个隐藏单元 且 不带激活函数 的全连接输出层。
import torch
from torch import nn
from torch.nn import functional as Fnet = nn.Sequential(nn.Linear(20, 256),nn.ReLU(),nn.Linear(256, 10))X = torch.rand(2, 20)
print(f"随机生成的原始输入:\n{X}")
print(f"模型输出:\n{net(X)}")
该例子通过实例化nn.Sequential
来构建模型,层的执行顺序是作为参数传递的。
nn.Sequential
定义了一种特殊的Module
,即在PyTorch中表示一个块的类,它维护了一个由Module
组成的有序列表。- 注意:两个全连接层都是
Linear
类的实例,Linear
类本身就是Module
的子类。 net(X)
实际上是net.__call__(X)
的简写 (我们前面一直在通过net(X)
调用我们的模型来获得模型的输出)。- 这个前向传播函数非常简单:它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。
1.1. 自定义块
简要总结每个块必须提供的基本功能:
- 将输入数据作为其前向传播函数的参数。
- 通过前向传播函数来生成输出。(注意:输出的形状可能与输入的形状不同。例如,上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出)。
- 计算其输出关于输入的梯度,可通过其反向传播函数进行访问(通常是自动发生)。
- 存储和访问前向传播计算所需的参数。
- 根据需要初始化模型参数。
下面的代码片段从零开始编写一个块。(包含一个多层感知机,其具有256个隐藏单元的隐藏层和一个10维输出层)。 注意,下面的MLP
类继承了表示块的类。我们的实现只需要提供我们自己的构造函数(Python中的__init__
函数)和前向传播函数。
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)))
前向传播函数:
- 以
X
作为输入, - 计算带有激活函数的隐藏表示,
- 并输出其未规范化的输出值。
在这个MLP
实现中,两个层都是实例变量。要了解这为什么是合理的,可以想象实例化两个多层感知机(net1
和net2
),并根据不同的数据对它们进行训练。
接着我们实例化多层感知机的层,然后在每次调用前向传播函数时调用这些层。注意一些关键细节:
- 首先,定制的
__init__
函数通过super().__init__()
调用父类的__init__
函数 (避免了重复编写模版代码)。 - 然后,实例化两个全连接层, 分别为
self.hidden
和self.out
。 - 注意,除非要实现一个新的运算符,否则不必担心反向传播函数或参数初始化,系统将自动生成这些。
net = MLP()
mlp_Out = net(X)
print(f"MLP模型输出:\n{mlp_Out}")
块的一个主要优点是它的多功能性。我们可以子类化块以创建
- 层(如全连接层的类)、
- 整个模型(如上面的
MLP
类) - 或具有中等复杂度的各种组件。
1.2. 顺序块
Sequential
的设计是为了把其他模块串起来。
构建自己的简化的MySequential
,只需定义两个关键函数:
- 一种将块逐个追加到列表中的函数;
- 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
下面的MySequential
类提供了与默认Sequential
类相同的功能。
# 直接将神经网络的每个层当作参数传进来
class MySequential(nn.Module):def __init__(self, *args):super().__init__() # 调用父类的构造函数for idx, module in enumerate(args):# 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员# 变量_modules中。_module的类型是OrderedDictself._modules[str(idx)] = moduledef forward(self, X):# OrderedDict保证了按照成员添加的顺序遍历它们for block in self._modules.values():X = block(X)return X
__init__
函数将每个模块逐个添加到有序字典_modules
中。_modules
的主要优点是:在模块的参数初始化过程中,系统知道在_modules
字典中查找需要初始化参数的子块。
当MySequential
的前向传播函数被调用时,每个添加的块都按照它们被添加的顺序执行。下面使用MySequential
类重新实现多层感知机。
net = MySequential(nn.Linear(20, 256),nn.ReLU(),nn.Linear(256, 10))
myModule_Out = net(X)
print(f"MySequential模型输出:\n{myModule_Out}")
1.3. 在前向传播函数中执行代码
当架构需要更强的灵活性时,则需要定义自己的块。
常数参数 (constant parameter):既不是上一层的结果 也不是可更新参数 的项。
- 例如,我们需要一个计算函数
的层,其中
- x 是 输入,
- w 是 参数,
- c 是 某个在优化过程中没有更新的指定常量。
因此下面实现一个FixedHiddenMLP
类,如下所示:
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 /= 2return X.sum()
在这个FixedHiddenMLP
模型中,
- 实现了一个隐藏层,其权重(
self.rand_weight
) 在实例化时被随机初始化,之后为常量。 - 这个权重不是一个模型参数,因此它永远不会被反向传播更新。
- 然后,神经网络将这个固定层的输出通过一个全连接层。
- 注意:在返回输出之前,模型做了一些不寻常的事情:
- 它运行了一个while循环,在 L1范数>1 的条件下,将输出向量除以2,直到它满足条件为止。
- 最后,模型返回了
X
中所有项的和。 - 注意:此操作可能不会常用于在任何实际任务中,这里只展示如何将任意代码集成到神经网络计算的流程中。
net = FixedHiddenMLP()
fixMLP_Out = net(X)
print(f"FixedHiddenMLP模型输出:\n{fixMLP_Out}")
也可以混合搭配各种组合块的方法。如下例子,我们以一些想到的方法嵌套块:(网络结构如下)
- NestMLP(),
- net()
- nn.Linear(20, 64), nn.ReLU()
- nn.Linear(64, 32), nn.ReLU()
- nn.Linear(32, 16)
- net()
- nn.Linear(16, 20),
- FixedHiddenMLP()
- nn.Linear(20, 20)
- x*w+1, nn.ReLU()
- nn.Linear(20, 20)
- x一直减半直到 其值的和>1。
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())
print(f"NestMLP模型输出:\n{chimera(X)}")
1.4. 效率
读者可能会开始担心操作效率的问题。毕竟,我们在一个高性能的深度学习库中进行了大量的字典查找、代码执行和许多其他的Python代码。Python的问题全局解释器锁 是众所周知的。在深度学习环境中,我们担心速度极快的GPU可能要等到CPU运行Python代码后才能运行另一个作业。
小结
一个块可以由许多层组成;一个块可以由许多块组成。
块可以包含代码。
块负责大量的内部处理,包括参数初始化和反向传播。
层和块的顺序连接由
Sequential
块处理。
2. 参数管理
选择好架构并设置好超参数后,下一阶段就是训练阶段。
训练阶段的目标:找到 使损失函数最小化 的模型参数值。(这些参数就是用于做出未来的预测)
本节内容:
- 访问参数,用于调试、诊断和可视化;
- 参数初始化;
- 在不同模型组件间共享参数。
首先看一下具有单隐藏层的多层感知机:
import torch
from torch import nnnet = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
print(f"随机生成的原始输入:\n{X}")
print(f"模型输出:\n{net(X)}")
2.1. 参数访问
从已有模型中访问参数:
- 当通过
Sequential
类定义模型时,可以通过索引来访问模型的任意层。 - 可以把模型当作一个列表,每层的参数都在其属性中。
- 如下所示,可以检查第二个全连接层的参数。
print(f"第二个全连接层的参数:\n{net[2].state_dict()}")
输出的结果显示:
- 该全连接层包含两个参数,分别是该层的 权重 和 偏置。
- 两者都存储为单精度浮点数(float32)。
- 注意,参数名称允许唯一标识每个参数,即使在包含数百个层的网络中也是如此。
2.1.1. 目标参数
每个参数都表示为参数类的一个实例。要对参数执行任何操作,首先需要访问底层的数值。有几种方法可以做到这一点。下面的代码从第二个全连接层(即第三个神经网络层)提取偏置, 提取后返回的是一个参数类实例,并进一步访问该参数的值。
# 从第二个全连接层(即第三个神经网络层)提取偏置:
# 参数是复合的对象,包含值、梯度和额外信息,因此需要显式参数值
print(f"类型:{type(net[2].bias)}")
print(f"值(包括其形状和数据类型):\n{net[2].bias}")
print(f"数据部分(偏置参数的底层数据张量):{net[2].bias.data}")
除了值之外,还可以访问每个参数的梯度。在上面这个网络中,由于还没有调用反向传播,所以参数的梯度处于初始状态。
print(f"参数的梯度:{net[2].weight.grad == None}")
2.1.2. 一次性访问所有参数
- 当需要对所有参数执行操作时,逐个访问会很麻烦。
- 当处理更复杂的块(例如,嵌套块)时,情况会变得特别复杂,因为需要递归整个树来提取每个子块的参数。
下面,将通过演示来比较 访问第一个全连接层的参数 和 访问所有层。
print(f"---访问第一个全连接层的参数:\n{[(name, param.shape) for name, param in net[0].named_parameters()]}")
print(f"---访问所有层:\n{[(name, param.shape) for name, param in net.named_parameters()]}")
# print(*[(name, param.shape) for name, param in net[0].named_parameters()])
# print(*[(name, param.shape) for name, param in net.named_parameters()])
这为我们提供了另一种访问网络参数的方式,如下所示
# net.state_dict() 返回模型的参数字典
# ['2.bias'] 从参数字典中 获取模型中第三个模块(索引从0开始)的偏置参数
print(f"另一种访问网络参数的方式:{net.state_dict()['2.bias'].data}")
2.1.3. 从嵌套块收集参数
若将多个块相互嵌套,参数命名约定是如何工作的。首先定义一个生成块的函数(可以说是“块工厂”),然后将这些块组合到更大的块中。
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 netrgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)
print(f"嵌套块 模型输出:\n{rgnet(X)}")
设计了网络后,接下来看看它是如何工作的。
print(f"嵌套块 模型结构:\n{rgnet}")
因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。
访问 第一个主要的块中、第二个子块的第一层的偏置项:
print(f"第一个主要的块中、第二个子块的第一层的偏置项:\n{rgnet[0][1][0].bias.data}")
2.2. 参数初始化
- 深度学习框架提供默认随机初始化,
- 也允许创建自定义初始化方法,满足我们通过其他规则实现初始化权重。
- 默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵,
- 这个范围是根据输入和输出维度计算出的。
- PyTorch的
nn.init
模块提供了多种预置初始化方法。
2.2.1. 内置初始化
首先调用内置的初始化器。
2.2.1.1 初始化为高斯随机变量
- 将所有权重参数初始化为标准差为0.01的高斯随机变量,且将偏置参数设置为0。
# 将所有权重参数初始化为 标准差=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)
print(f"权重初始化为 标准差=0.01的高斯随机变量:\n{net[0].weight.data[0]}")
print(f"偏置初始化为 常量0:\n{net[0].bias.data[0]}")
2.2.1.2 初始化为给定的常数
- 将所有参数初始化为给定的常数,比如初始化为1。
# 将所有参数初始化为给定的常数,比如初始化为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)
print(f"权重初始化为 常量1:\n{net[0].weight.data[0]}")
print(f"偏置初始化为 常量0:\n{net[0].bias.data[0]}")
2.2.1.3 对不同块应用不同的初始化方法
- 对不同块应用不同的初始化方法。
例如,使用Xavier初始化方法初始化第一个神经网络层,然后将第三个神经网络层初始化为常量值42。
'''
Xavier初始化:根据输入和输出的维度自动调整初始化的范围,使得每一层的输出的方差在训练初期保持一致。(助于缓解梯度消失和梯度爆炸)
均匀分布:xavier_uniform_()使用均匀分布来初始化权重,而不是正态分布。均匀分布的范围是[-limit, limit],其中limit是根据输入和输出维度计算得出的
'''
# 对某些块应用不同的初始化方法
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) # 使用Xavier初始化方法初始化第一个神经网络层
net[2].apply(init_42) # 将第三个神经网络层初始化为常量值42
print(f"第一层网络,使用Xavier初始化权重:\n{net[0].weight.data[0]}")
print(f"第三层网络,权重初始化为 常量42:\n{net[2].weight.data}")
2.2.2. 自定义初始化
有时,深度学习框架没有提供我们需要的初始化方法。在下例中,使用以下的分布为任意权重参数w定义初始化方法:
同样,实现一个my_init
函数来应用到net
。
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) # 使用均匀分布初始化权重,范围是[-10, 10]# 对权重进行阈值处理,将绝对值小于 5 的元素设置为 0。# 即 只有 绝对值>=5 的权重会被保留,其余的被置零m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
print(f"自定义初始化:\n{net[0].weight[:2]}")
注意,我们始终可以直接设置参数。
net[0].weight.data[:] += 1 # 将第一个模块的所有权重值加 1
net[0].weight.data[0, 0] = 42 # 将第一个模块的权重张量的 [0, 0] 位置的值设置为 42
print(f"对权重进行手动修改后:\n{net[0].weight[:2]}")
2.2.3. 参数绑定 (共享参数)
有时希望在多个层间共享参数:我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。
# 参数绑定
# 我们需要给共享层一个名称,以便可以引用它的参数
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(f"检查参数是否相同:\n"f"{net[2].weight.data[0] == net[4].weight.data[0]}")
net[2].weight.data[0, 0] = 100
print(f"确保它们实际上是同一个对象,而不只是有相同的值:\n"f"{net[2].weight.data[0] == net[4].weight.data[0]}")
这个例子表明第三个和第五个神经网络层的参数是绑定的。
- 它们不仅值相等,而且由相同的张量表示。
- 因此,若改变其中一个参数,另一个参数也会改变。
- 当参数绑定时,由于模型参数包含梯度,因此在反向传播期间第二个隐藏层 (即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。
共享参数通常可以节省内存,并在以下方面具有特定的好处:
- 对于图像识别中的CNN,共享参数使网络能够在图像中的任何地方而不是仅在某个区域中查找给定的功能。
- 对于RNN,它在序列的各个时间步之间共享参数,因此可以很好地推广到不同序列长度的示例。
- 对于自动编码器,编码器和解码器共享参数。 在具有线性激活的单层自动编码器中,共享权重会在权重矩阵的不同隐藏层之间强制正交。
小结
我们有几种方法可以访问、初始化和绑定模型参数。
可以使用自定义初始化方法。
3. 延后初始化
延后初始化 (defers initialization):直到数据第一次通过模型传递时,框架才会动态地推断出每个层的大小。
tensorflow 版本:
def delay_initialization_tf(): # tensorflow版本import tensorflow as tfnet = tf.keras.models.Sequential([tf.keras.layers.Dense(256, activation=tf.nn.relu),tf.keras.layers.Dense(10),])print(f"访问输入层的权重参数:\n{[net.layers[i].get_weights() for i in range(len(net.layers))]}")X = tf.random.uniform((2, 20))net(X)print(f"将数据通过网络,使框架初始化参数:\n{[w.shape for w in net.get_weights()]}") delay_initialization_tf()
def delay_initialization():net = nn.Sequential(nn.LazyLinear(64),nn.ReLU(),nn.LazyLinear(10))print(f"尚未初始化:\n{net}")print(net[0].weight) # 尚未初始化,会报错print(f"输入层的权重参数:\n{[net[i].state_dict() for i in range(len(net))]}")X = torch.rand(2, 20)net(X) # 数据第一次通过模型传递print(f"数据第一次通过模型传递后,完成初始化:{net}")print(f"输入层的权重参数:\n{net[0].weight}")
delay_initialization()
3.1. 实例化网络
让实例化一个多层感知机:
net = nn.Sequential(nn.LazyLinear(64),nn.ReLU(),nn.LazyLinear(10))
此时输入维数是未知的,因此网络不可能知道输入层权重的维数。因此,框架尚未初始化任何参数:
print(f"尚未初始化:\n{net}")print(net[0].weight) # 尚未初始化,会报错print(f"输入层的权重参数:\n{[net[i].state_dict() for i in range(len(net))]}")
注意:每个层对象都存在,但权重为空,参数显示未初始化。使用net.get_weights()
将抛出一个错误,因为权重尚未初始化。
接下来将数据通过网络,最终使框架初始化参数:
X = torch.rand(2, 20)net(X) # 数据第一次通过模型传递print(f"数据第一次通过模型传递后,完成初始化:{net}")print(f"输入层的权重参数:\n{net[0].weight}")
- 一旦知道输入维数是20,框架可以通过代入值20来识别第一层权重矩阵的形状。
- 识别出第一层的形状后,框架处理第二层,
- 依此类推,直到所有形状都已知为止。
- 注意:在这种情况下,只有第一层需要延迟初始化,但是框架仍是按顺序初始化的。等到知道了所有的参数形状,框架就可以初始化参数。
小结
延后初始化使框架能够自动推断参数形状,使修改模型架构变得容易,避免了一些常见的错误。
我们可以通过模型传递数据,使框架最终初始化参数。
练习
- Q1: 如果指定了第一层的输入尺寸,但没有指定后续层的尺寸,会发生什么?是否立即进行初始化?
- A1: 可以正常运行。第一层会立即初始化,但其他层同样是直到数据第一次通过模型传递才会初始化(不知道题目理解的对不对)
- Q2: 如果指定了不匹配的维度会发生什么?
- A2:
- 如果在定义模型时,模块的输出尺寸和下一个模块的输入尺寸不匹配,PyTorch会在第一次调用
forward
时抛出错误。 - 例如,如果你有一个
LazyLinear
层输出尺寸为10,但下一个LazyLinear
层期望输入尺寸为20,则在第一次forward
调用时会引发错误
- 如果在定义模型时,模块的输出尺寸和下一个模块的输入尺寸不匹配,PyTorch会在第一次调用
- A2:
- Q3: 如果输入具有不同的维度,你需要做什么?提示:查看参数绑定的相关内容。
- A3:
- 如果输入维度比指定维度小,可以考虑使用padding填充;
- 如果输入维度比指定维度大,可以考虑用pca等降维方法,将维度降至指定维度。
- A3:
4. 自定义层
4.1. 不带参数的层
下面的CenteredLayer
类要从其输入中减去均值。要构建它,只需继承基础层类并实现前向传播功能。
import torch
import torch.nn.functional as F
from torch import nnclass CenteredLayer(nn.Module): # 从输入中减去均值def __init__(self):super().__init__()def forward(self, X):return X - X.mean()
向该层提供一些数据,验证它是否能按预期工作:
layer = CenteredLayer()
X = torch.FloatTensor([1, 2, 3, 4, 5])
print(f"输入:{X}")
print(f"均值:{X.mean()}")
print(f"网络输出:{layer(X)}")
现在,可以将层作为组件合并到更复杂的模型中:
# 将层作为组件合并到更复杂的模型中
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
作为额外的健全性检查,我们可以在向该网络发送随机数据后,检查均值是否为0。由于处理的是浮点数,因为存储精度的原因,我们仍然可能会看到一个非常小的非零数。
Y = net(torch.rand(4, 8)) # 向该网络发送随机数据
# print(f"将层作为组件合并到更复杂的模型中:{Y}")
print(f"检查均值是否为0 Y.mean():{Y.mean()}")
4.2. 带参数的层
定义具有参数(参数可以通过训练进行调整)的层,这些参数可以使用内置函数来创建。
- 内置函数提供一些基本的管理功能 (比如管理访问、初始化、共享、保存和加载模型参数)。
- 如此可以不需要为每个自定义层编写自定义的序列化程序。
下面实现自定义版本的全连接层,该层需要权重和偏置项两个参数。 这里使用修正线性单元作为激活函数。 该层需要输入参数: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.datareturn F.relu(linear)
接下来,实例化MyLinear
类并访问其模型参数:
linear = MyLinear(5, 3)
print(f"自定义全连接层的权重:\n{linear.weight}")
可以使用自定义层直接执行前向传播计算:
print(f"使用自定义层直接执行前向传播计算:\n{linear(torch.rand(2, 5))}")
还可以使用自定义层构建模型,就像使用内置的全连接层一样使用自定义层:
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
print(f"使用自定义全连接层构建的复杂模型,预测结果:\n{net(torch.rand(2, 64))}")
小结
可以通过基本层类 设计自定义层。以此来定义 行为与深度学习框架中的任何现有层不同的 灵活的 新层。
在自定义层定义完成后,就可以在任意环境和网络架构中调用该自定义层。
层可以有局部参数,这些参数可以通过内置函数创建。
5. 读写文件
- 保存训练的模型,可以 以备将来在各种环境中使用(比如在部署中进行预测)。
- 当运行一个耗时较长的训练过程时,定期保存中间结果,可以防止在服务器电源被不小心断掉时,损失几天的计算结果。
5.1. 加载和保存张量
对于单个张量,可以直接调用load
和save
函数分别读写它们。这两个函数都要求提供一个名称,save
要求将要保存的变量作为输入。
import torch
from torch import nn
from torch.nn import functional as Fx = torch.arange(4)
torch.save(x, 'outputFile/x-file') # 将张量保存到文件中
然后可以将存储在文件中的数据读回内存:
x2 = torch.load('outputFile/x-file', weights_only=False) # 从文件中加载张量,并赋值
print(f"x2={x2}")
可以存储一个张量列表,然后把它们读回内存:
# 存储一个张量列表,然后把它们读回内存:
y = torch.zeros(4)
torch.save([x, y],'outputFile/x-files') # 将两个张量保存到文件中
x2, y2 = torch.load('outputFile/x-files', weights_only=False) # 加载列表中的张量,并将其解包到 x2 和 y2
print(f"x2, y2 = {x2, y2}")
也可以写入或读取从字符串映射到张量的字典。这利于读取或写入模型中的所有权重:
# 写入或读取从字符串映射到张量的字典
mydict = {'x': x, 'y': y} # 创建字典
torch.save(mydict, 'outputFile/mydict') # 将字典保存到文件 mydict 中
mydict2 = torch.load('outputFile/mydict', weights_only=False) # 加载字典,并将其赋值给 mydict2
print(f"mydict2={mydict2}")
5.2. 加载和保存模型参数
若想保存整个模型,并在以后加载它们,单独保存每个向量则会变得很麻烦。 毕竟,我们可能有数百个参数散布在各处。 因此,
- 深度学习框架提供了内置函数来保存和加载整个网络。
- 注意,保存的是 模型的参数,而不是保存整个模型。
例如,有一个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”的文件中:
torch.save(net.state_dict(), 'outputFile/mlp.params')
为了恢复模型,我们实例化了原始多层感知机模型的一个备份。这里不需要随机初始化模型参数,而是直接读取文件中存储的参数:
clone = MLP()
print(f"模型:\n{clone}")
# 直接读取文件中存储的参数
# clone.load_state_dict(...) 将加载的状态字典应用到clone中
clone.load_state_dict(torch.load('outputFile/mlp.params', weights_only=False))
clone.eval() # 设置模型为评估模式
由于两个实例具有相同的模型参数,在输入相同的X
时, 两个实例的计算结果应该相同。 下面进行验证:
Y_clone = clone(X)
print(f"副本与读上来版本的异同:\n{Y_clone == Y}")
小结
save
和load
函数可用于张量对象的文件读写。我们可以通过参数字典保存和加载网络的全部参数。
保存架构必须在代码中完成,而不是在参数中完成。
6. GPU
6.1. 计算设备
6.2. 张量与GPU
6.2.1. 存储在GPU上
6.2.2. 复制
6.2.3. 旁注
6.3. 神经网络与GPU
小结
我们可以指定用于存储和计算的设备,例如CPU或GPU。默认情况下,数据在主内存中创建,然后使用CPU进行计算。
深度学习框架要求计算的所有输入数据都在同一设备上,无论是CPU还是GPU。
不经意地移动数据可能会显著降低性能。一个典型的错误如下:计算GPU上每个小批量的损失,并在命令行中将其报告给用户(或将其记录在NumPy
ndarray
中)时,将触发全局解释器锁,从而使所有GPU阻塞。最好是为GPU内部的日志分配内存,并且只移动较大的日志。