5. 深度学习计算

5.1 层和块

研究“比单个层大”但“比整个模型小”的块更有价值(block)可以描述单个层或多个层组成的组件或整个模型本身。可以将一些块以递归的方式组合成更大的组件。
blocks.svg
块由类(class)表示,它的任何子类都必须实现如下功能:

  1. 接收输入数据。
  2. 通过前向传播生成输出。
  3. 通过反向传播计算输出关于输入的梯度。
  4. 存储和访问模型参数。
  5. 根据需要初始化模型参数。

    1. class FixedHiddenMLP(nn.Module): # 继承Module
    2. def __init__(self):
    3. super().__init__()
    4. # 不计算梯度的随机权重参数。因此其在训练期间保持不变
    5. self.rand_weight = torch.rand((20, 20), requires_grad=False)
    6. self.linear = nn.Linear(20, 20) # 定义一个输出层,反向传播和参数初始化由系统自动完成
    7. # 定义前向传播
    8. def forward(self, X):
    9. X = self.linear(X)
    10. # 使用创建的常量参数以及relu
    11. X = F.relu(torch.mm(X, self.rand_weight) + 1)
    12. # 复用全连接层。这相当于两个全连接层共享参数
    13. X = self.linear(X)
    14. # 将控制流集成到前向传播
    15. while X.abs().sum() > 1:
    16. X /= 2
    17. return X.sum()

    可以混合搭配各种组合块。下面的例子进行了一些块的嵌套。 ```python class NestMLP(nn.Module): def init(self):

    1. super().__init__()
    2. self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
    3. nn.Linear(64, 32), nn.ReLU())
    4. 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) # tensor(-0.1137, grad_fn=)

<a name="bfe3dd27"></a>
## 5.2. 模型参数管理
<a name="f01c2ecb"></a>
### 5.2.1. 参数访问
通过`Sequential`类定义的模型可通过索引来访问任意层参数。
```python
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)
# 检查第二个全连接层的参数(即第三个层,索引从0开始)
print(net[2].state_dict())    # 第二层为激活函数
"""
OrderedDict([('weight', tensor([[-0.2496, -0.0703, -0.2378,  0.1302, -0.2831,  0.2991,  0.2335,  0.1590]])), ('bias', tensor([-0.0820]))])
"""
print(net[2].bias)    # Parameter containing:tensor([-0.0820], requires_grad=True)
print(net[2].bias.data)    # tensor([-0.0820])
# 除了值之外还可以访问每个参数的梯度。 由于还没有调用反向传播,所以参数的梯度处于初始状态。
net[2].weight.grad == None # True
# 递归整个树来提取每个子块的参数。
print([(name, param.shape) for name, param in net.named_parameters()])
"""
[('0.weight', torch.Size([8, 4])), ('0.bias', torch.Size([8])), ('2.weight', torch.Size([1, 8])), ('2.bias', torch.Size([1]))]
"""

下面探究嵌套块中的参数命名约定是如何工作的。

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)
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    # 访问第一个主要的块中、第二个子块的第一层的偏置项

5.2.2. 参数初始化

5.2.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.0091,  0.0023, -0.0125,  0.0040]), tensor(0.))

# 将所有参数初始化为给定的常数
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.))

# 对不同块执行不同的初始化方式
def 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(xavier)
net[2].apply(init_42)

5.2.2.2. 自定义初始化

使用以下分布为任意权重参数w定义初始化方法:
5. 深度学习计算 - 图2%20%26%20%5Ctext%20%7B%20%E5%8F%AF%E8%83%BD%E6%80%A7%20%7D%20%5Cfrac%7B1%7D%7B4%7D%20%5C%5C%0A0%20%26%20%5Ctext%20%7B%20%E5%8F%AF%E8%83%BD%E6%80%A7%20%7D%20%5Cfrac%7B1%7D%7B2%7D%20%5C%5C%0AU(-10%2C-5)%20%26%20%5Ctext%20%7B%20%E5%8F%AF%E8%83%BD%E6%80%A7%20%7D%20%5Cfrac%7B1%7D%7B4%7D%0A%5Cend%7Barray%7D%5Cright.%0A#card=math&code=w%20%5Csim%5Cleft%5C%7B%5Cbegin%7Barray%7D%7Bll%7D%0AU%285%2C10%29%20%26%20%5Ctext%20%7B%20%E5%8F%AF%E8%83%BD%E6%80%A7%20%7D%20%5Cfrac%7B1%7D%7B4%7D%20%5C%5C%0A0%20%26%20%5Ctext%20%7B%20%E5%8F%AF%E8%83%BD%E6%80%A7%20%7D%20%5Cfrac%7B1%7D%7B2%7D%20%5C%5C%0AU%28-10%2C-5%29%20%26%20%5Ctext%20%7B%20%E5%8F%AF%E8%83%BD%E6%80%A7%20%7D%20%5Cfrac%7B1%7D%7B4%7D%0A%5Cend%7Barray%7D%5Cright.%0A&id=ZITKU)

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)
net[0].weight[:2]
# Init weight torch.Size([8, 4])
# Init weight torch.Size([1, 8])
# tensor([[-8.4986,  0.0000, -0.0000, -0.0000],[ 6.7884,  0.0000, -9.8570, -6.8247]], grad_fn=<SliceBackward>)

# 直接设置参数
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
# tensor([42.,  1.,  1.,  1.])

5.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(net[2].weight.data[0] == net[4].weight.data[0])
#  tensor([True, True, True, True, True, True, True, True])
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])

由于模型参数包含梯度,因此在反向传播期间第二个隐藏层和第三个隐藏层的梯度会加在一起。共享参数通常可以节省内存,并在以下方面具有特定的好处:

  • 对于CNN,共享参数使网络能够在图像中的任何地方而不是仅在某个区域中查找给定的功能。
  • 对于RNN,它在序列的各个时间步之间共享参数,可以很好地推广到不同序列长度的示例。
  • 对于自动编码器,编码器和解码器共享参数。 在具有线性激活的单层自动编码器中,共享权重会在权重矩阵的不同隐藏层之间强制正交。

    5.3 自定义层

    5.3.1 不带参数的层

    ```python 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())

<a name="6c49fc29"></a>
### 5.3.2 带参数的层
```python
class MyLinear(nn.Module):
    # in_units:输入数,units:输出数
    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(torch.rand(2, 5))

# 使用自定义层构建更复杂的模型
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))

5.4 读写文件

5.4.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
# 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.])}

5.4.2. 加载和保存模型参数

需要注意的一个重要细节是,这将保存模型的参数而不是保存整个模型。

torch.save(net.state_dict(), 'mlp.params')
mlp = MLP()    # 初始化模型参数
mlp.load_state_dict(torch.load('mlp.params'))

5.5 GPU

跨多个服务器部署作业时,通过智能地将数组分配给设备可以最大限度地减少在设备之间传输数据的时间。本节展示数据如何在不同设备之间传递。

5.5.1. 计算设备

PyTorch默认情况下,张量在内存中创建并使用CPU计算。

  • CPU:torch.device('cpu'),意味着PyTorch的计算将尝试使用所有CPU核心。
  • GPU:torch.device('cuda')。 如果有多个GPU,使用torch.device(f'cuda:{i}') 来表示第i块GPU(i从0开始),如torch.device('cuda:1')。 另外,cuda:0cuda是等价的。查询GPU的数量可以使用torch.cuda.device_count()。 ```python def try_gpu(i=0): #@save “””如果存在第i块GPU,则返回gpu(i),否则返回cpu()””” if torch.cuda.device_count() >= i + 1:
      return torch.device(f'cuda:{i}')
    
    return torch.device(‘cpu’)

def try_all_gpus(): #@save “””返回所有可用的GPU,如果没有GPU,则返回[cpu(),]””” devices = [torch.device(f’cuda:{i}’) for i in range(torch.cuda.device_count())] return devices if devices else [torch.device(‘cpu’)]

try_gpu(), try_gpu(10), try_all_gpus()

(device(type=’cuda’, index=0),device(type=’cpu’), [device(type=’cuda’, index=0), device(type=’cuda’, index=1)])

<a name="09e28626"></a>
### 5.5.2 张量与GPU
```python
# 查询张量所在的设备
x = torch.tensor([1, 2, 3])
x.device    # device(type='cpu')

在GPU上创建的张量只消耗该GPU的显存。

X = torch.ones(2, 3, device=try_gpu())
X
# tensor([[1., 1., 1.],[1., 1., 1.]], device='cuda:0')

计算时在同一设备上找不到数据会导致异常。,必须将计算所需的所有数据存放到一个设备上。
copyto.svg

Z = X.cuda(1)    # 将数据复制到显卡1
print(X)
print(Z)
"""
tensor([[1., 1., 1.],
        [1., 1., 1.]], device='cuda:0')
tensor([[1., 1., 1.],
        [1., 1., 1.]], device='cuda:1')
"""

单个GPU相对运行速度快。 但是在设备(CPU、GPU和其他机器)之间传输数据比计算慢得多。 这也使得并行化变得更加困难,因为我们必须等待数据被发送(或者接收), 然后才能继续进行更多的操作。 最后,当打印张量或将张量转换为NumPy格式时, 如果数据不在内存中,框架会首先将其复制到内存中, 这会导致额外的传输开销。 更糟糕的是,它现在受制于全局解释器锁,使得一切都得等待Python完成。
一个典型的错误如下:计算GPU上每个小批量的损失,并在命令行中将其报告给用户(或将其记录在NumPy ndarray中)时,将触发全局解释器锁,从而使所有GPU阻塞。

5.5.3 神经网络和GPU

# 将模型放到GPU上训练
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu())
# 当输入为GPU上的张量时,模型将在同一GPU上计算结果。
net(X)# tensor([[1.2194], [1.2194]], device='cuda:0', grad_fn=<AddmmBackward>)
# 确认模型参数存储在同一GPU
net[0].weight.data.device # device(type='cuda', index=0)