5.1 层和块

首先回顾以下多层感知机(MLP)

  • nn.Sequential()
  • net = nn.Sequential():实质上定义了一种很特殊的Module(顺序Module)
import torch
from torch import nn
from torch import functional as F
net = nn.Sequential(
    nn.Linear(20, 256),
    nn.ReLU(),
    nn.Linear(256, 10))
X = torch.rand(2, 20)
net(X), X.shape

线性层的权重矩阵_线性层的权重矩阵

  1. 使用自定义块定义net
class MLP(nn.Module):
    def __init__(self):
        super(MLP, self).__init__()
        # super().__init__() 当该类指定继承父类是,可省略参数
        self.hidden = nn.Linear(20, 256)
        self.out = nn.Linear(256, 10)

    def forward(self, X):
        # 方式1:常规写法:执行每个层都写出来
        # X = nn.ReLU(self.hidden(X))
        # X = self.out(X)
        # return X
        
        # 方式2:对于简单的网络层,直接使用简洁写法
        return self.out(F.relu(self.hidden(X)))

实例化多层感知机的层, 然后在每次调用正向传播函数时会调用这些层

X = torch.rand(2, 20)
net = MLP()  # 调用类函数,得先实例化类,不要忘记哦
net(X), X.shape

线性层的权重矩阵_人工智能_02

  1. 手动实现顺序块
# 手动实现顺序块
class MySequential(nn.Module):
    def __init__(self, *args):
        super().__init__()
        for idx, module in enumerate(args):
            # 这里,module是Module子类的一个实例,我们把它保存在Module类的成员
            # 变量_modules中:_module的类型是OrderedDict
            self._module[str(idx)] = module

    def forward(self, X):
        # OrderedDict保证了按照成员添加的顺序遍历它们
        for block in self._module.values():
            X = block(X)
        return X
X = torch.rand(2, 20)
net = MySequential(nn.Linear(20, 256), nn.ReLU(), 
                             nn.Linear(256, 10))
net(X)

线性层的权重矩阵_深度学习_03


3. 使用控制流定义块

  • 这种方式在正向传播中执行代码
  • (可能没有实际意义,但是能体现继承nn.Module的灵活多样性)
# 继承nn.Module的灵活多样性
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)
        # python控制流
        while X.abs().sum() > 1 :
            X /= 2
        return X.sum()

net = FixedHiddenMLP()
net(X)

线性层的权重矩阵_学习_04

  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):
        X = self.net(X)
        X = self.linear(X)
        return X
chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20))  # chimera:虚构的
chimera(X), X.shape

线性层的权重矩阵_学习_05

5.2 参数管理

5.2.1 参数访问

  • 首先关注具有单隐藏层的MLP
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), net(X).shape

线性层的权重矩阵_深度学习_06


查看网络结构

net

线性层的权重矩阵_自定义_07


参数访问

  • 使用nn.Module创建的网络块,默认会自动创建权重W, B,并且以类型为OrderedDict顺序存储。
print(net[2].state_dict())  # 为什么权重矩阵是转置的: 默认的Linear层:y = X*w^T

线性层的权重矩阵_学习_08


目标参数: 其实参数Parameter中含有两种数据:1. 数值(data) 2.梯度(grad)

print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)

线性层的权重矩阵_深度学习_09

print(net[2].weight.grad == None)

线性层的权重矩阵_学习_10


一次性访问所有参数

print(*[(name, param.shape) for name, param in net[0].named_parameters()])  # 访问第0层的参数
print() 
print(*[(name, param.shape) for name, param in net.named_parameters()])  # 访问所有参数

线性层的权重矩阵_学习_11

next(net.named_parameters())

线性层的权重矩阵_人工智能_12

print(net.state_dict()['0.bias'].data)

线性层的权重矩阵_人工智能_13


这两种访问参数的方式都一样,后者比较常用

print(net.state_dict()['0.bias'].data == net[0].bias.data)

线性层的权重矩阵_人工智能_14

5.2.2 参数初始化

使用嵌套块收集函数

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

X = torch.rand(size=(2, 4))
rgnet = nn.Sequential(
    block2(), nn.Linear(4, 1))
rgnet(X)

线性层的权重矩阵_深度学习_15

print(rgnet)

线性层的权重矩阵_人工智能_16

  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)
print(net[0].weight.data[0], net[0].bias.data[0])

线性层的权重矩阵_自定义_17

def init_constant(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 1)  # 功能:使用val的值来填充输入的Tensor
        nn.init.zeros_(m.bias)

net.apply(init_constant)
print(net[0].weight.data[0], net[0].bias.data[0])

线性层的权重矩阵_深度学习_18

  1. 对某些块应用不同的初始化方法
    初始化函数 uniform, normal, const, Xavier, He initialization
# 对某些块应用不同的初始化层
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)
print(net[0].weight.data[0])
print(net[2].weight.data[0])

线性层的权重矩阵_深度学习_19


3. 自定义初始化

# 自定义初始化
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 
        # a = a * (abs(a)>=5): 绝对值>=5的:相乘, 否则,权重对应元素变为0
net.apply(my_init)
print(net[0].weight[:2])

线性层的权重矩阵_自定义_20

net[0].weight.data[:] += 1  # weight所有值+1
net[0].weight.data[0, 0] = 42  # weight第一个值=42
print(net[0].weight.data[0])

线性层的权重矩阵_自定义_21

  1. 参数绑定(层之间共享参数)
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[4].weight.data[0, 0])

线性层的权重矩阵_深度学习_22

print(net[4].weight.data)

线性层的权重矩阵_学习_23

5.3 延后初始化

pytorch很少用此操作,因为pytorch定义网络结构时,必明确前后的维度,并且保证前后维度一致;MXnet、tensorflow用的较多

到目前为止,我们忽略了建立网络时需要做的以下这些事情:

  • 我们定义了网络架构,但没有指定输入维度。
  • 我们添加层时没有指定前一层的输出维度。
  • 我们在初始化参数时,甚至没有足够的信息来确定模型应该包含多少参数。

有些读者可能会对我们的代码能运行感到惊讶。 毕竟,深度学习框架无法判断网络的输入维度是什么。 这里的诀窍是框架的延后初始化(defers initialization), 即直到数据第一次通过模型传递时,框架才会动态地推断出每个层的大小。

5.4 自定义层

  1. 构建一个没有任何参数的自定义层
%%time
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]))

线性层的权重矩阵_自定义_24


2. 将层作为组件合并到构建更复杂的模型中

%%time
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())

Y = net(torch.rand(4, 8))
Y.mean()

线性层的权重矩阵_学习_25

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)

dense = MyLinear(5, 3)
print(dense.weight)
print(dense.bias)

线性层的权重矩阵_人工智能_26


使用自定义层直接执行正向传播计算

# 使用自定义层直接执行正向传播计算
dense(torch.rand(2, 5))

线性层的权重矩阵_人工智能_27

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

线性层的权重矩阵_线性层的权重矩阵_28

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')  # x-fil:是一个含有archive的压缩文件 ,pytorch的专属二进制文件格式

x2 = torch.load("x-file")
x2

线性层的权重矩阵_学习_29


查看文件保存位置

import os
file_path = os.path.abspath('x-file')
print(file_path)

线性层的权重矩阵_线性层的权重矩阵_30

  1. 存储一个张量列表,然后把他们读回内存
# 存储一个张量列表,然后把他们读回内存
y = torch.zeros(4)
torch.save([x, y], 'x-file')
x2, y2 = torch.load('x-file')
x2, y2

线性层的权重矩阵_自定义_31

  1. 写入或读取从字符串映射到张量的字典
# 写入或读取从字符串映射到张量的字典
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
print(mydict2)

线性层的权重矩阵_人工智能_32

  1. 加载和保存模型参数
# 加载和保存模型参数
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(), 'mlp.params')
  • 实例化了原始多层感知机模型的一个备份。直接读取文件中存储的参数
# 实例化
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()

线性层的权重矩阵_人工智能_33

  • 可以观察到使用之前的参数来评估模型,与训练完成的参数是一样的
y_clone = clone(X)
print(y_clone == y)

线性层的权重矩阵_自定义_34

5.6 GPU

5.6.1 计算机设备

在PyTorch中,每个数组都有一个设备(device), 我们通常将其称为环境(context)。

  • 默认情况下,所有变量和相关的计算都分配给CPU。 有时环境可能是GPU。 当我们跨多个服务器部署作业时,事情会变得更加棘手。
  • 通过智能地将数组分配给环境, 我们可以最大限度地减少在设备之间传输数据的时间。 例如,当在带有GPU的服务器上训练神经网络时,
  • 我们通常希望模型的参数在GPU上。

使用nvidia-smi命令来查看显卡信息。

!nvidia-smi

线性层的权重矩阵_自定义_35

在PyTorch中,CPU和GPU可以用torch.device(‘cpu’) 和torch.device(‘cuda’)表示。

  • 在PyTorch中,CPU和GPU可以用torch.device(‘cpu’) 和torch.device(‘cuda’)表示。
  • gpu设备只代表一个卡和相应的显存。
  • 如果有多个GPU,我们使用torch.device(f’cuda:{i}') 来表示第i块GPU(从0开始)。
  • 另外,cuda:0和cuda是等价的。
import torch
from torch import nn

torch.device('cpu'), torch.device('cuda'), torch.device('cuda:1')

线性层的权重矩阵_人工智能_36

查询可用GPU的数量

torch.cuda.device_count()

定义使用GPU的两个函数:

  • try_gpu():是否使用1个GPU
  • try_all_gpus(): 是否使用所有的GPU
# 使用GPU
def try_gpu(i=0):  
    """如果存在,则返回gpu(i),否则返回cpu()"""
    if torch.cuda.device_count() >= i + 1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')
def try_all_gpus():  
    """返回所有可用的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()

线性层的权重矩阵_深度学习_37

5.6.2 张量与GPU

  • 默认情况下, 张量实在cpu上创建的
X = torch.tensor([1, 2, 3])
X.device

线性层的权重矩阵_深度学习_38


将张量从CPU转移到GPU

device = try_gpu()
X = X.to(device)
X.device

线性层的权重矩阵_学习_39

  • 也可以在创建张量时指定存储设备
X = torch.ones(2, 3, device=try_gpu())
# 如果至少了两个GPU,也可指定在第几个GPU上创建
#  Y = torch.ones(2, 3, device=try_gpu(1))  

X

线性层的权重矩阵_线性层的权重矩阵_40

5.6.3 不同设备上张量的计算

如果我们要计算X + Y,我们需要决定在哪里执行这个操作。

  • 例如,有两个张量X,Y, 其中:X:存储在第一个GPU, Y: 存储在第二个GPU。
  • 我们可以将X传输到第二个GPU并在那里执行操作。
  • 不要简单地X加上Y,因为这会导致异常, 运行时引擎不知道该怎么做:它在同一设备上找不到数据会导致失败。
  • 由于Y位于第二个GPU上,所以我们需要将X移到那里, 然后才能执行相加运算
  1. 先查看设备上是否有GPU
# 使用GPU训练
if not torch.cuda.is_available():
    print('CUDA is not available.  Training on CPU ...')
else:
    print('CUDA is available.  Training on GPU ...')

线性层的权重矩阵_学习_41

  1. 定义张量:
  • 例如,有两个张量X、Y, 其中:X:存储在GPU上, Y: 存储在GPU上。
X = torch.ones((2, 3))
Y = torch.randn((2, 3), device = try_gpu())
X, Y

线性层的权重矩阵_深度学习_42


如果我们要计算X + Y,我们需要决定在哪里执行这个操作。 (若直接计算则会出错)

RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!

# X + Y
  1. 声明设备环境
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
device

线性层的权重矩阵_学习_43

  1. 将X传输到GPU上
X = X.to(device)
X

线性层的权重矩阵_深度学习_44


5. 执行X + Y。(此时X,Y均在同一个设备上,不会出错

X + Y

线性层的权重矩阵_学习_45

注意
人们使用GPU来进行机器学习,因为单个GPU相对运行速度快。但是在设备(CPU、GPU和其他机器)之间传输数据比计算慢得多。这也使得并行化变得更加困难,因为我们必须等待数据被发送(或者接收), 然后才能继续进行更多的操作
这就是为什么拷贝操作要格外小心。 根据经验,多个小操作比一个大操作糟糕得多。 此外,一次执行几个操作比代码中散布的许多单个操作要好得多。如果一个设备必须等待另一个设备才能执行其他操作, 那么这样的操作可能会阻塞。 这有点像排队订购咖啡,而不像通过电话预先订购:当客人到店的时候,咖啡已经准备好了。
最后,当我们打印张量或将张量转换为NumPy格式时, 如果数据不在内存中,框架会首先将其复制到内存中, 这会导致额外的传输开销
更糟糕的是,它现在受制于全局解释器锁,使得一切都得等待Python完成。

5.6.4 神经网络与GPU

神经网络模型也可以指定设备

net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu())

确认模型参数存储在同一个GPU上。

net[0].weight.data.device

线性层的权重矩阵_自定义_46


只要所有的数据和参数都在同一个设备上, 我们就可以有效地学习模型

总结

  • 我们可以指定用于存储和计算的设备,例如CPU或GPU。默认情况下,数据在主内存中创建,然后使用CPU进行计算。
  • 深度学习框架要求计算的所有设备都在同一设备上,无论是CPU还是GPU,否则会报错。
  • 不经意地移动数据可能会显著降低性能。
  • 一个典型的错误:计算GPU上每个小批量的损失,并命令将其报告给用户(或将其记录在Numpy ndarray中)时,将触发全局解释锁,从而使所有GPU堵塞。最好是为GPU内部的日志,并且只移动较大的日志。