李沐动手学深度学习(PyTorch)课程学习笔记第四章:PyTorch 神经网络基础。
1. 层和块
在构造自定义块之前,我们先回顾一下多层感知机(第三章第二节)的代码,下面的代码生成一个网络,其中包含一个具有256个单元和 ReLU 激活函数的全连接隐藏层,然后是一个具有10个隐藏单元且不带激活函数的全连接输出层:
1 2 3 4 5 6 7 8
| 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) print(net(X).shape)
|
在这个例子中,我们通过实例化 nn.Sequential
来构建我们的模型,层的执行顺序是作为参数传递的。简而言之,nn.Sequential
定义了一种特殊的 Module
,即在 PyTorch 中表示一个块的类,它维护了一个由 Module
组成的有序列表。注意,两个全连接层都是 Linear
类的实例,Linear
类本身就是 Module
的子类。另外,到目前为止,我们一直在通过 net(X)
调用我们的模型来获得模型的输出。这实际上是 net.__call__(X)
的简写。这个前向传播函数非常简单:它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。
在下面的代码片段中,我们从零开始编写一个块。它包含一个多层感知机,其具有256个隐藏单元的隐藏层和一个10维输出层。注意,下面的 MLP
类继承了表示块的类。我们的实现只需要提供我们自己的构造函数(Python 中的 __init__
函数)和前向传播函数:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class MLP(nn.Module): def __init__(self): super().__init__() self.hidden = nn.Linear(20, 256) self.out = nn.Linear(256, 10)
def forward(self, X): return self.out(F.relu(self.hidden(X)))
|
接着我们实例化多层感知机的层,然后在每次调用前向传播函数时调用这些层:
1 2
| net = MLP() print(net(X).shape)
|
现在我们可以更仔细地看看 Sequential
类是如何工作的,回想一下 Sequential
的设计是为了把其他模块串起来。为了构建我们自己的简化的 MySequential
,我们只需要定义两个关键函数:
- 一种将块逐个追加到列表中的函数;
- 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
下面的 MySequential
类提供了与默认 Sequential
类相同的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class MySequential(nn.Module): def __init__(self, *args): super().__init__() for idx, module in enumerate(args): self._modules[str(idx)] = module
def forward(self, X): for block in self._modules.values(): X = block(X) return X
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)) print(net(X).shape)
|
Sequential
类使模型构造变得简单,允许我们组合新的架构,而不必定义自己的类。然而,并不是所有的架构都是简单的顺序架构。当需要更强的灵活性时,我们需要定义自己的块。例如,我们可能希望在前向传播函数中执行 Python 的控制流。此外,我们可能希望执行任意的数学运算,而不是简单地依赖预定义的神经网络层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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) X = F.relu(torch.mm(X, self.rand_weight) + 1) X = self.linear(X) while X.abs().sum() > 1: X /= 2 return X.sum()
|
我们可以混合搭配各种组合块的方法。在下面的例子中,我们以一些想到的方法嵌套块:
1 2 3 4 5 6 7 8 9 10 11
| 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(chimera(X))
|
2. 参数管理
我们首先看一下具有单隐藏层的多层感知机:
1 2 3 4 5 6
| 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)) print(net(X).shape)
|
我们从已有模型中访问参数。当通过 Sequential
类定义模型时,我们可以通过索引来访问模型的任意层。这就像模型是一个列表一样,每层的参数都在其属性中。如下所示,我们可以检查第二个全连接层的参数:
1 2
| print(net[2].state_dict())
|
这个全连接层包含两个参数,分别是该层的权重和偏置,每个参数都表示为参数类的一个实例。要对参数执行任何操作,首先我们需要访问底层的数值:
1 2 3
| print(type(net[2].bias)) print(net[2].bias) print(net[2].bias.data)
|
参数是复合的对象,包含值、梯度和额外信息。这就是我们需要显式参数值的原因。除了值之外,我们还可以访问每个参数的梯度。在上面这个网络中,由于我们还没有调用反向传播,所以参数的梯度处于初始状态:
1
| print(net[2].weight.grad == None)
|
当我们需要对所有参数执行操作时,逐个访问它们可能会很麻烦。下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层:
1 2 3 4
| print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
|
这为我们提供了另一种访问网络参数的方式,如下所示:
1
| print(net.state_dict()['2.bias'].data)
|
现在让我们看看如果我们将多个块相互嵌套,参数命名约定是如何工作的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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)) print(rgnet)
|
因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们:
1
| print(rgnet[0][1][0].bias.data)
|
知道了如何访问参数后,现在我们看看如何正确地初始化参数。深度学习框架提供默认随机初始化,也允许我们创建自定义初始化方法。
让我们首先调用内置的初始化器。下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量,且将偏置参数设置为0:
1 2 3 4 5 6 7
| 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(net[0].weight.data[0], net[0].bias.data[0])
|
我们还可以将所有参数初始化为给定的常数,比如初始化为1:
1
| nn.init.constant_(m.weight, 1)
|
我们还可以对某些块应用不同的初始化方法。例如,下面我们使用 Xavier 初始化方法初始化第一个神经网络层,然后将第三个神经网络层初始化为常量值42:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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)
|
有时,深度学习框架没有提供我们需要的初始化方法。在下面的例子中,我们先初始化为-10~10的均匀分布,然后将绝对值小于5的参数置零:
1 2 3 4 5 6 7 8 9 10
| 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) print(net[0].weight[:2])
|
有时我们希望在多个层间共享参数:我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数:
1 2 3 4 5 6 7 8
| shared = nn.Linear(8, 8) net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared, nn.ReLU(), nn.Linear(8, 1))
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])
|
这个例子表明第三个和第五个神经网络层的参数是绑定的。它们不仅值相等,而且由相同的张量表示。因此,如果我们改变其中一个参数,另一个参数也会改变。这里有一个问题:当参数绑定时,梯度会发生什么情况?答案是由于模型参数包含梯度,因此在反向传播期间第二个隐藏层(即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。
3. 自定义层
我们构造一个没有任何参数的自定义层,下面的 CenteredLayer
类要从其输入中减去均值。要构建它,我们只需继承基础层类并实现前向传播功能:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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() print(layer(torch.FloatTensor([1, 2, 3, 4, 5])))
|
下面我们继续定义具有参数的层,这些参数可以通过训练进行调整。让我们实现自定义版本的全连接层。回想一下,该层需要两个参数,一个用于表示权重,另一个用于表示偏置项。在此实现中,我们使用 ReLU 作为激活函数。该层需要输入参数:in_units
和 units
,分别表示输入数和输出数:
1 2 3 4 5 6 7 8 9 10 11 12
| 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) print(linear.weight.shape)
|
4. 读写文件
加载和保存张量:
1 2 3 4 5 6
| import torch from torch import nn from torch.nn import functional as F
x = torch.arange(4) torch.save(x, '../save/x-file')
|
将存储在文件中的数据读回内存:
1 2
| x2 = torch.load('../save/x-file') print(x2)
|
存储一个张量列表,然后把它们读回内存:
1 2 3 4
| y = torch.zeros(4) torch.save([x, y],'../save/x-files') x2, y2 = torch.load('../save/x-files') print((x2, y2))
|
我们可以写入或读取从字符串映射到张量的字典。当我们要读取或写入模型中的所有权重时,这很方便:
1 2 3 4
| mydict = {'x': x, 'y': y} torch.save(mydict, '../save/mydict') mydict2 = torch.load('../save/mydict') print(mydict2)
|
深度学习框架提供了内置函数来保存和加载整个网络。需要注意的一个重要细节是,这将保存模型的参数而不是保存整个模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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(), '../save/mlp.params')
clone = MLP() clone.load_state_dict(torch.load('../save/mlp.params')) clone.eval() Y_clone = clone(X) print(Y_clone == Y)
|
5. GPU
CUDA 的安装配置教程:Anaconda与PyTorch安装教程
在 PyTorch 中,CPU 和 GPU 可以用 torch.device('cpu')
和 torch.device('cuda')
表示:
1 2 3 4
| import torch from torch import nn
print(torch.device('cpu'), torch.device('cuda'), torch.device('cuda:0'))
|
我们可以查询可用 GPU 的数量:
1
| print(torch.cuda.device_count())
|
我们可以查询张量所在的设备。默认情况下,张量是在 CPU 上创建的:
1 2
| x = torch.tensor([1, 2, 3]) print(x.device)
|
需要注意的是,无论何时我们要对多个项进行操作,它们都必须在同一个设备上。例如,如果我们对两个张量求和,我们需要确保两个张量都位于同一个设备上,否则框架将不知道在哪里存储结果,甚至不知道在哪里执行计算。
有几种方法可以在 GPU 上存储张量。例如,我们可以在创建张量时指定存储设备:
1 2
| X = torch.ones(2, 3, device='cuda') print(X.device)
|
数据在同一个 GPU 上我们才可以将它们相加:
1 2 3
| Y = X.cuda(0) print(Y.device) print((X + Y).device)
|
类似地,神经网络模型可以指定设备。下面的代码将模型参数放在 GPU 上:
1 2 3 4 5
| net = nn.Sequential(nn.Linear(3, 1)) net = net.to(device='cuda')
print(net(X)) print(net[0].weight.data.device)
|
总之,只要所有的数据和参数都在同一个设备上,我们就可以有效地学习模型。
下一章:卷积神经网络。