主要参考学习资料:
《动手学深度学习》阿斯顿·张 等 著
【动手学深度学习 PyTorch版】哔哩哔哩@跟李牧学AI
概述
- 为了实现更复杂的网络,我们需要研究比层更高一级的单元块,在编程中由类表示。通过自定义层和块,我们能更灵活地搭建网络。
- 我们有多种方法可以访问、初始化和绑定模型参数。
- 为了加载和保存权重向量和整个模型,我们需要进行文件读写。
- 将计算设备指定为GPU而不是CPU,可以更高效地完成深度学习的计算任务。
目录
- 4.1 层和块
- 4.1.1 自定义块
- 4.1.2 顺序块
- 4.2 参数管理
- 4.2.1 参数访问
- 1.目标参数
- 2.一次性访问所有参数
- 3.从嵌套块收集参数
- 4.2.2 参数初始化
- 1.内置初始化
- 2.自定义初始化
- 4.2.3 参数绑定
- 4.3 自定义层
- 4.3.1 不带参数的层
- 4.3.2 带参数的层
- 4.4 读写文件
- 4.4.1 加载和保存张量
- 4.4.2 加载和保存模型参数
- 4.5 GPU
- 4.5.1 计算设备
- 4.5.2 张量与GPU
- 4.5.3 神经网络与GPU
4.1 层和块
事实证明,研究讨论“比单个层大”但“比整个模型小”的组件更有价值。计算机视觉中广泛流行的ResNet-152架构有数百层,这些层是由层组的重复模式组成。为了实现更复杂的网络,我们引入神经网络块的概念。
块可以描述单个层、由多个层组成的组件或整个模型本身。使用块的好处是可以将一些块组成更大的组件,这一过程通常是递归的。
在编程中,块由类表示,类的任何子类都必须定义一个将其输入转换为输出的前向传播函数,并且必须存储任何必需的参数。为了计算梯度,块必须具有反向传播函数。在自定义块时,由于自动微分提供了一些后端实现,我们只需要考虑前向传播函数和必须的参数。
4.1.1 自定义块
块必须提供的基本功能:
- 将输入数据作为其前向传播函数的参数。
- 通过前向传播函数来生成输出。
- 计算其输出关于输入的梯度,可通过其反向传播函数进行访问,通常是自动完成的。
- 存储和访问前向传播计算所需的参数。
- 根据需要初始化模型参数。
import torch
from torch import nn
#functional模块提供一些函数化的神经网络操作,可直接应用于张量
from torch.nn import functional as F #以Module为父类定义MLP类
class MLP(nn.Module): def __init__(self): #初始化继承自Module的数据属性super().__init__() #实例化两个全连接层self.hidden = nn.Linear(20, 256) self.out = nn.Linear(256, 10) #定义模型的前向传播,根据输入X返回模型输出def forward(self, X): return self.out(F.relu(self.hidden(X)))
下面这个例子展示了自定义块的灵活性:
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 /= 2return X.sum()
4.1.2 顺序块
Sequential类的设计目的是将其他模块串起来。构建自己的简化的MySequential只需定义如下函数:
- 将块逐个追加到列表中的函数。
- 前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
class MySequential(nn.Module): #args传入任意数量的模块def __init__(self, *args): super().__init__() #遍历模块并为每个模块分配一个字符串键存入_modules#_modules的变量类型是OrderedDictfor idx, module in enumerate(args): self._modules[str(idx)] = module def forward(self, X): #OrderedDict保证按照成员添加的顺序遍历它们for block in self._modules.values(): X = block(X) return X
4.2 参数管理
4.2.1 参数访问
import torch
from torch import nnnet = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
通过索引访问模型的任意层,state_dict返回所访问层的参数:
print(net[2].state_dict())
OrderedDict([('weight', tensor([[ 0.2889, -0.2738, -0.3096, -0.3151, 0.2594, 0.1743, -0.2288, -0.1173]])), ('bias', tensor([0.0921]))])
1.目标参数
每个参数都表示为参数类的一个实例:
print(type(net[2].bias))
<class 'torch.nn.parameter.Parameter'>
参数是复合的对象,如下访问参数实例,并进一步访问该参数的值(张量类型)和梯度:
print(net[2].bias)
print(net[2].bias.data)
net[2].weight.grad == True
Parameter containing:
tensor([0.0921], requires_grad=True)
tensor([0.0921])
False
另一种访问网络参数的方式:
net.state_dict()['2.bias'].data
tensor([0.0921])
2.一次性访问所有参数
访问一个全连接层的参数:
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
访问所有层的参数:
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]))
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))
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.1969, -0.0747, -0.1520, 0.0474, -0.2245, -0.2320, -0.2794, 0.3429])
4.2.2 参数初始化
1.内置初始化
normal_将权重参数初始化为标准差为0.01的高斯随机变量,zeros_将偏置参数设置为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)
constant_将所有参数初始化为给定的常量:
def init_constant(m):if type(m) == nn.Linear:nn.init.constant_(m.weight, 1)nn.init.zeros_(m.bias)
xavier_uniform_为上一章中介绍的Xavier均匀分布初始化:
def init_xavier(m):if type(m) == nn.Linear:nn.init.xavier_uniform_(m.weight)
2.自定义初始化
以如下分布为例为任意权重参数 w w w定义初始化方法:
w ∼ { U ( 5 , 10 ) , 可能性为 1 4 0 , 可能性为 1 2 U ( − 10 , − 5 ) , 可能性为 1 4 w\sim\left\{\begin{matrix}U(5,10),&\displaystyle可能性为\frac14\\0,&\displaystyle可能性为\frac12\\U(-10,-5),&\displaystyle可能性为\frac14\end{matrix}\right. w∼⎩ ⎨ ⎧U(5,10),0,U(−10,−5),可能性为41可能性为21可能性为41
def my_init(m):if type(m) == nn.Linear:#以(-10, 10)为边界的均匀分布nn.init.uniform_(m.weight, -10, 10)#将绝对值小于5的权重清零m.weight.data *= m.weight.data.abs() >= 5
4.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))
改变shared的参数也会同步改变net中两个对应层的参数。
4.3 自定义层
4.3.1 不带参数的层
构建一个从其输入中减去均值的层:
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()
将自定义层作为组件合并到更复杂的模型中:
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
该自定义层没有指定输入维度,但框架会在数据第一次通过模型传递时动态地推断出每个层的大小,即延迟初始化。
4.3.2 带参数的层
下面是自定义版本的全连接层:
class MyLinear(nn.Module):#传入输入数和输出数作为参数def __init__(self, in_units, units):super().__init__()#通过Parameter实例创建参数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)
4.4 读写文件
4.4.1 加载和保存张量
张量通过load和save函数保存到内存并分别读写它们。
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')#张量列表
y = torch.zeros(4)
torch.save([x, y], 'x-files')
x2, y2 = torch.load('x-files')#从字符串映射到张量的字典
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
4.4.2 加载和保存模型参数
深度学习框架提供了内置函数来保存和加载整个网络,但这只保存模型的参数而不是模型,因此架构需要自己生成。先定义一个多层感知机模型:
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()
将模型参数存储在文件mlp.params中:
torch.save(net.state_dict(), 'mlp.params')
实例化原多层感知机模型的一个备份,并通过直接读取文件中存储的参数来恢复模型:
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
4.5 GPU
GPU具有强大的并行处理能力、浮点运算速度和内存带宽,在处理人工智能这样的复杂计算任务上相较于CPU有无可比拟的优势。首先确保至少安装了一个NVIDIA GPU,然后下载NVIDIA驱动和CUDA,最后使用nvidia-smi命令查看显卡信息:
!nvidia-smi
在pytorch中,每个数组都有一个设备,我们通常称其为环境。默认情况下,所有变量和相关的计算都分配给CPU,有时环境可能是GPU。
4.5.1 计算设备
我们可以指定用于存储和计算的设备。在pytorch中,CPU和GPU可以使用torch.device(‘cpu’)和torch.device(‘cuda’))表示。如果有多个GPU,我们使用torch.device(f’cuda:{i}')来表示第i块GPU(i从0开始),以及cuda:0和cuda等价。
import torch
from torch import nntorch.device('cpu'), torch.device('cuda'), torch.device('cuda:0')
(device(type='cpu'), device(type='cuda'), device(type='cuda', index=0))
查询可用GPU的数量:
torch.cuda.device_count()
1
定义两个函数以允许在不存在所需GPU的情况下执行代码:
def try_gpu(i=0):"""如果存在,返回第i个GPU,否则返回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()
(device(type='cuda', index=0),device(type='cpu'),[device(type='cuda', index=0)])
4.5.2 张量与GPU
默认情况下,张量是在内存中创建并使用CPU进行计算的。
x = torch.tensor([1, 2, 3])
x.device
device(type='cpu')
在创建张量时指定存储设备来在GPU上存储张量:
X = torch.ones(2, 3, device=try_gpu())
X
tensor([[1., 1., 1.],[1., 1., 1.]], device='cuda:0')
假设我们有两个GPU(博主没有所以移用示例代码),如下在第二个GPU上创建一个随机张量:
Y = torch.rand(2, 3, device=try_gpu(1))
Y
tensor([[0.2506, 0.4556, 0.9745],[0.2359, 0.6094, 0.0410]] device='cuda:1')
如果要计算X+Y,我们需要决定在哪里执行这个操作。张量的计算只能在同一设备上运行,因此要将X复制到第二个GPU才能执行:
Z = X.cuda(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上将其相加:
Y + Z
tensor([[1.2506, 1.4556, 1.9745],[1.2359, 1.6094, 1.0410]] device='cuda:1')
4.5.3 神经网络与GPU
类似地,神经网络模型可以指定设备,将模型参数放在GPU上:
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu())