【动手学深度学习】#4 深度学习计算

主要参考学习资料:

《动手学深度学习》阿斯顿·张 等 著

【动手学深度学习 PyTorch版】哔哩哔哩@跟李牧学AI

概述

  • 为了实现更复杂的网络,我们需要研究比层更高一级的单元,在编程中由表示。通过自定义层和块,我们能更灵活地搭建网络。
  • 我们有多种方法可以访问、初始化和绑定模型参数。
  • 为了加载和保存权重向量和整个模型,我们需要进行文件读写
  • 将计算设备指定为GPU而不是CPU,可以更高效地完成深度学习的计算任务。

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 /= 2
		return X.sum()

4.1.2 顺序块

Sequential类的设计目的是将其他模块串起来。构建自己的简化的MySequential只需定义如下函数:

  • 将块逐个追加到列表中的函数。
  • 前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
class MySequential(nn.Module):  
	#args传入任意数量的模块
    def __init__(self, *args):  
        super().__init__()  
        #遍历模块并为每个模块分配一个字符串键存入_modules
        #_modules的变量类型是OrderedDict
        for 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 nn

net = 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 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())

该自定义层没有指定输入维度,但框架会在数据第一次通过模型传递时动态地推断出每个层的大小,即延迟初始化

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.data
		return 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

4

在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 nn

torch.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())
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值