目录
1.深度学习关键组件
- 模型构建
- 参数访问与初始化
- 设计自定义层和块
- 模型读写到磁盘
- GPU加速
2.层和块
2.1层
- 上一层的输出为输入,下一层的输入为输出,并且具有一组可调参数, 这些参数根据从下一层反向传播的信号进行更新。(输入层输出层除外)
2.2块
- “比单个层大”但“比整个模型小”的组件。
- 描述单个层、由多个层组成的组件或整个模型本身。
- 从编程的角度来看,块由类(class)表示。
- 主打的就是一个多功能性。
2.2.1自定义块
每个块要实现的基本功能:
- 将输入数据作为其前向传播函数的参数。
- 通过前向传播函数来生成输出。
- 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。
- 存储和访问前向传播计算所需的参数。
- 根据需要初始化模型参数。
2.2.2顺序块
为了构建我们自己的简化的MySequential, 我们只需要定义两个关键函数:
- 一种将块逐个追加到列表中的函数;
- 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
import torch
from torch import nn
from torch.nn import functional as F
X = torch.rand(2, 20)
# 继承父类nn.Module
class MySequential(nn.Module):
def __init__(self, *args):
# *args 是一个特殊的语法,表示在函数定义时,可以传入任意数量的位置参数。
# 这些位置参数将被打包成一个 tuple 对象,并作为参数传递给函数。
# 在函数内部,可以通过遍历这个 tuple 来访问每个传入的位置参数。
# tuple 是 Python 中的一种内置数据类型,表示一个不可变的有序集合。
super().__init__()
# super().__init__() 是一个调用父类构造方法的语句,
# 用于在子类的构造方法中调用父类的构造方法并初始化父类的属性。
# 这个语句可以用于 Python 的类继承中。在 Python 中,
# 每个类都会默认继承 object 类,如果子类想要使用父类的构造方法,
# 就需要在子类的构造方法中使用 super().__init__() 调用父类的构造方法。
# 在调用时,需要传递所有必要的参数给父类的构造方法,
# 包括 self 对象。这样,父类的属性就会被正确地初始化,
# 而子类也能够继承父类的方法和属性。
# self.module = []
# 建立一个空列表
for idx, module in enumerate(args):
# enumerate 是 Python 内置函数之一,可以对一个可迭代对象进行遍历,
# 同时返回索引和元素。
# 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
# 变量_modules中。_module的类型是OrderedDict
print(f'idx:{idx}')
print(f'module:{module}')
self._modules[str(idx)] = module
# self.module.append(module)
# print(self.module)
def forward(self, X):
# OrderedDict保证了按照成员添加的顺序遍历它们
for block in self._modules.values():
# for block in self.module:
# 遍历列表的值
# values() 是 Python 字典(dictionary)类的一个方法,
# 用于返回字典中所有值构成的视图对象。
print(f'block:{block}')
X = block(X)
return X
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)
# 在 PyTorch 中,通过调用一个实例的方法 __call__ 来执行前向传播,
# 即 net(X) 相当于调用了 net.__call__(X)。而 __call__ 实际上是
# 调用了 forward 方法,这个方法定义了实例的前向传播计算。
# 因此,在 PyTorch 中我们可以通过自定义 forward 方法来实现自己的模型。
输出结果:
# 实例化时候的输出
idx:0
module:Linear(in_features=20, out_features=256, bias=True)
idx:1
module:ReLU()
idx:2
module:Linear(in_features=256, out_features=10, bias=True)
# 调用前向传播时候的输出
block:Linear(in_features=20, out_features=256, bias=True)
block:ReLU()
block:Linear(in_features=256, out_features=10, bias=True)
tensor([[-0.1272, -0.1378, -0.1972, 0.0640, -0.2142, 0.2393, -0.1388, 0.0457,
0.0226, -0.1721],
[-0.1543, -0.2248, -0.1773, -0.0157, -0.1861, 0.0347, -0.1544, -0.0136,
0.2051, -0.0957]], grad_fn=<AddmmBackward0>)
2.2小结
- 一个块可以由许多层组成;一个块可以由许多块组成。
- 块可以包含代码。
- 块负责大量的内部处理,包括参数初始化和反向传播。
- 层和块的顺序连接由Sequential块处理。
- 自定义块和神经网络都是继承nn.Module,所有nn.Module的子类可以相互调用。
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)
- 可以给内部变量定义块和层。前提是继承nn.Module并且super().init()。
- 子类主要对两个特别的方法进行定义__init__()和forward()。
3.参数管理
- 访问参数,用于调试、诊断和可视化
- 参数初始化
- 在不同模型组件间共享参数
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.0619],
[-0.0489]], grad_fn=<AddmmBackward0>)
3.1参数访问
print(net[2].state_dict())
输出结果:
OrderedDict([('weight', tensor([[ 0.3016, -0.1901, -0.1991, -0.1220, 0.1121, -0.1424, -0.3060, 0.3400]])), ('bias', tensor([-0.0291]))])
这个全连接层包含两个参数,分别是该层的权重和偏置。 两者都存储为单精度浮点数(float32)。 注意,参数名称允许唯一标识每个参数,即使在包含数百个层的网络中也是如此。
3.1.1目标参数
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
输出结果:
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([-0.0291], requires_grad=True)
tensor([-0.0291])
参数是复合的对象,包含值、梯度和额外信息。 这就是我们需要显式参数值的原因。 除了值之外,我们还可以访问每个参数的梯度。 在上面这个网络中,由于我们还没有调用反向传播,所以参数的梯度处于初始状态。
net[2].weight.grad == None
True
3.1.2一次性访问所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
#这行代码的作用是输出 net 中第一个 nn.Linear 层的所有参数的名称和形状。
# 具体来说,named_parameters() 是 nn.Module 类中的一个方法,
# 它会返回一个生成器对象,这个对象会按照深度优先的顺序遍历 nn.Module 对象中所有的子模块,
# 并生成每个子模块的参数名称和参数值。所以在这个例子中,net[0] 是第一个子模块,
# 它是一个 nn.Linear 对象,所以可以调用 named_parameters() 方法来获取该子模块的所有参数。
#[(name, param.shape) for name, param in ...] 是一个列表推导式,
# 它会把每个参数的名称和形状存储为一个元组,最后把所有元组组成一个列表。
# 最后使用 print() 函数打印输出这个列表。
# 总之,这行代码的作用是方便我们查看 nn.Module 对象中每个子模块的参数名称和形状。
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]))
在函数调用中,单个星号*表示将一个可迭代对象解包成多个参数,而双星号**表示将一个字典解包成多个关键字参数。
在列表或元组中,单个星号表示打散,可以将一个列表或元组解包成多个独立的元素。
例如:
def my_func(a, b, c):
print(a, b, c)
my_list = [1, 2, 3]
my_func(*my_list) # 等价于 my_func(1, 2, 3)
my_dict = {'a': 1, 'b': 2, 'c': 3}
my_func(**my_dict) # 等价于 my_func(a=1, b=2, c=3)
在上述代码中,*my_list将[1, 2, 3]解包成了1, 2, 3,作为my_func的位置参数传入。而**my_dict将{‘a’: 1, ‘b’: 2, ‘c’: 3}解包成了a=1, b=2, c=3,作为my_func的关键字参数传入。
一种访问网络参数的方式
net.state_dict()['2.bias'].data
输出:
tensor([-0.0291])
3.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 net
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)
输出:
tensor([[-0.3078],
[-0.3078]], grad_fn=<AddmmBackward0>)
设计了网络后,我们看看它是如何工作的。
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)
)
因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。 下面,我们访问第一个主要的块中、第二个子块的第一层的偏置项。
rgnet[0][1][0].bias.data
输出:
tensor([-0.2539, 0.4913, 0.3029, -0.4799, 0.2022, 0.3146, 0.0601, 0.3757])
3.2参数初始化
3.2.1内置初始化
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.0128, -0.0141, 0.0062, 0.0028]), 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.3809, 0.5354, -0.4686, -0.2376])
tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
3.2.2自定义初始化
深度学习框架没有提供我们需要的初始化方法。 在下面的例子中,我们使用以下的分布为任意权重参数 𝑤
定义初始化方法:
w
∼
{
U
(
5
,
10
)
可能性
1
4
0
可能性
1
2
U
(
−
10
,
−
5
)
可能性
1
4
\begin{aligned} w \sim \begin{cases} U(5, 10) & \text{ 可能性 } \frac{1}{4} \\ 0 & \text{ 可能性 } \frac{1}{2} \\ U(-10, -5) & \text{ 可能性 } \frac{1}{4} \end{cases} \end{aligned}
w∼⎩
⎨
⎧U(5,10)0U(−10,−5) 可能性 41 可能性 21 可能性 41
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)
# 这行代码使用了 PyTorch 的 nn.init 模块中的 uniform_ 方法来对参数进行初始化。
# uniform_ 方法会以均匀分布随机初始化张量中的每个元素,并将结果保存回参数张量中。
# 该方法的参数包括参数张量以及所需的均匀分布的下限和上限。
# 在这个例子中,-10 和 10 分别作为下限和上限,表示随机初始化的数值在
# [-10, 10] 的范围内。
m.weight.data *= m.weight.data.abs() >= 5
# 这一行代码实现的功能是将张量m.weight.data中绝对值大于等于5的元素乘以1,
# 其余元素不变。即对于满足条件的元素,将其变为原来的绝对值,
# 而对于不满足条件的元素,保持不变。
net.apply(my_init)
net[0].weight[:2]
输出:
Init weight torch.Size([8, 4])
Init weight torch.Size([1, 8])
tensor([[-0.0000, 0.0000, -0.0000, 0.0000],
[-0.0000, 9.3464, 5.5061, 6.8197]], grad_fn=<SliceBackward0>)
始终可以直接设置参数。
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
输出:
tensor([42., 1., 1., 1.])
3.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(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])
输出:
tensor([True, True, True, True, True, True, True, True])
tensor([True, True, True, True, True, True, True, True])
这个例子表明第三个和第五个神经网络层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 因此,如果我们改变其中一个参数,另一个参数也会改变。 这里有一个问题:当参数绑定时,梯度会发生什么情况? 答案是由于模型参数包含梯度,因此在反向传播期间第二个隐藏层 (即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。
3.3.1参数共享的好处
共享参数通常可以节省内存,并在以下方面具有特定的好处:
- 对于图像识别中的CNN,共享参数使网络能够在图像中的任何地方而不是仅在某个区域中查找给定的功能。
- 对于RNN,它在序列的各个时间步之间共享参数,因此可以很好地推广到不同序列长度的示例。
- 对于自动编码器,编码器和解码器共享参数。 在具有线性激活的单层自动编码器中,共享权重会在权重矩阵的不同隐藏层之间强制正交。
3.4小结
- 有几种方法可以访问、初始化和绑定模型参数。
- 可以使用自定义初始化方法。
4.自定义层
有时我们会遇到或要自己发明一个现在在深度学习框架中还不存在的层。 在这些情况下,必须构建自定义层。
4.1不带参数的层
例如:
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())
Y = net(torch.rand(4, 8))
Y.mean()
输出:
tensor(-1.3970e-09, grad_fn=<MeanBackward0>)
4.2带参数的层
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)
linear = MyLinear(5, 3)
linear.weight
输出:
Parameter containing:
tensor([[ 1.9094, -0.8244, -1.6846],
[ 0.6850, 0.8366, -1.3837],
[ 0.0289, 2.0976, 1.3855],
[-0.8574, -0.3557, -0.4109],
[ 2.2963, -1.3008, 1.2173]], requires_grad=True)
使用自定义层直接执行前向传播计算
linear(torch.rand(2, 5))
输出:
tensor([[0.0984, 0.5687, 2.8316],
[2.2558, 0.0000, 1.8880]])
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))
输出:
tensor([[7.5465],
[4.6817]])
4.3小结
- 我们可以通过基本层类设计自定义层。这允许我们定义灵活的新层,其行为与深度学习框架中的任何现有层不同。
- 在自定义层定义完成后,我们就可以在任意环境和网络架构中调用该自定义层。
- 层可以有局部参数,这些参数可以通过内置函数创建。
5.读写文件
- 希望保存训练的模型,比如将来用作部署。
- 在一个耗时长的训练中,做好的做法是定期保存中间结果。
5.1加载和保存张量
- 保存列表:
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)
输出:
tensor([0, 1, 2, 3])
(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.])}
5.2加载和保存模型参数
保存:
torch.save(net.state_dict(), 'mlp.params')
加载:
clone.load_state_dict(torch.load('mlp.params'))
例如:
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)
torch.save(net.state_dict(), 'mlp.params')
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()
Y_clone = clone(X)
Y_clone == Y
中间输出结果:
MLP(
(hidden): Linear(in_features=20, out_features=256, bias=True)
(output): Linear(in_features=256, out_features=10, bias=True)
)
tensor([[True, True, True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True, True, True]])
5.3小结
- save和load函数可以用于张量对象文件的读写
- 可以通过参数字典保存和加载网络的全部参数。
- 保存架构必须在代码中完成,而不是在参数中完成。