当前位置: 首页 > news >正文

动手学深度学习pytorch学习笔记 —— 第五章

目   录

第五章 — 深度学习计算

1 层和块

1.1 层

1.2 块

1.3 自定义块

1.4 顺序块

2 参数管理

2.1 参数访问 

2.2 参数初始化

2.2.1 内置初始化

2.2.2 自定义初始化

2.2.3 参数绑定

2.3 延后初始化

3 自定义层

3.1 不带参数的层

3.2 带参数的层

4 读写文件(加载和保存)

4.1 加载和保存张量

4.2 模型参数

5 GPU

6 结语


第五章 — 深度学习计算

章节导航:除了庞大的数据集和强大的硬件, 优秀的软件工具在深度学习的快速发展中发挥了不可或缺的作用。本章重点学习深度学习计算的关键组件, 即模型构建、参数访问与初始化、设计自定义层和块、将模型读写到磁盘, 以及利用GPU实现显著的加速。虽然本章不介绍任何新的模型或数据集, 但后面的高级模型章节在很大程度上依赖于本章的知识。

1 层和块

回顾一下单个神经网络完成的几项工作:

  • 接受一些输入
  • 生成相应的标量输出
  • 具有一组相关参数,更新这些参数可以优化某目标函数。

当考虑具有多个输出的网络时,我们利用矢量化算法来描述整层神经元。

1.1 层

像单个神经元一样,完成下面工作:

  • 接受一组输入
  • 生成相应的输出
  • 由一组可调整参数描述

当我们使用softmax回归时,一个单层本身就是模型。 然而,即使我们随后引入了多层感知机,我们仍然可以认为该模型保留了上面所说的基本架构。对于多层感知机而言,整个模型及其组成层都是这种架构。 整个模型接受原始输入(特征)生成输出(预测), 并包含一些参数(所有组成层的参数集合)。同样,每个单独的层接收输入(由前一层提供), 生成输出(到下一层的输入),并且具有一组可调参数,这些参数会根据从下一层反向传播的结果进行更新。

事实证明,研究讨论“比单个层大”但“比整个模型小”的组件更有价值。在计算机视觉中广泛流行的ResNet-152架构就有数百层, 这些层是由层组的重复模式组成。在其他的领域,如自然语言处理和语音,层组以各种重复模式排列的类似架构现在也是普遍存在

1.2 块

为了实现复杂的网络,需要引入了神经网络块的概念。块可以描述单个层、由多个层组成的组件或整个模型本身。 使用块进行抽象的一个好处是可以将一些块组合成更大的组件,这一过程通常是递归的。 通过定义代码来按需生成任意复杂度的块,我们可以通过简洁的代码实现复杂的神经网络。

从编程的角度来看,块由类表示。它的任何子类都必须定义一个将其输入转换为输出的前向传播函数, 并且必须存储任何必需的参数(有些块不需要任何参数)。 最后,为了计算梯度,块必须具有反向传播函数

在构造自定义块之前,我们先回顾一下多层感知机的代码。先 生成一个网络,其中包含一个具有256个单元和ReLU激活函数的全连接隐藏层,然后是一个具有10个隐藏单元且不带激活函数的全连接输出层。

import torch
from torch import nn
from torch.nn import functional as F# 实例化nn.Sequential
# 层的执行顺序是作为参数传递的
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))X = torch.rand(2, 20)
net(X)

在PyTorch中表示一个块的类, 它维护了一个由Module组成的有序列表。 注意,两个全连接层都是Linear类的实例, Linear类本身就是Module的子类。

1.3 自定义块

首先,块必须提供的基本功能:

  • 将输入数据作为其前向传播函数的参数。

  • 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出。

  • 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。

  • 存储和访问前向传播计算所需的参数。

  • 根据需要初始化模型参数。

书中开始教学从0开始编写一个块,该块包含一个多层感知机,具有256个隐藏单元的隐藏层和一个10维的输出层。MLP类继承了表示块的类。我们只需要提供我们自己的构造函数(Python中的__init__函数)和前向传播函数。

class MLP(nn.Module):# 用模型参数声明层。这里,我们声明两个全连接的层def __init__(self):# 调用MLP的父类Module的构造函数来执行必要的初始化。# 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)super().__init__()self.hidden = nn.Linear(20, 256)  # 隐藏层self.out = nn.Linear(256, 10)  # 输出层# 定义模型的前向传播,即如何根据输入X返回所需的模型输出def forward(self, X):# 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。return self.out(F.relu(self.hidden(X)))

前向传播函数:以X作为输入, 计算带有激活函数的隐藏表示,并输出其未规范化的输出值。

MLP实现:两个层都是实例变量。 要了解这为什么是合理的,可以想象实例化两个多层感知机(net1net2), 并根据不同的数据对它们进行训练。 当然,我们希望它们学到两种不同的模型。

实例化多层感知机的层,在每次调用前向传播函数时调用这些层。首先,调用父类的__init__函数,实例化两个全连接层,分别为self.hiddenself.out(隐藏层和输出层)。除非我们实现一个新的运算符(自定义), 否则我们不必担心反向传播函数或参数初始化,由系统自动生成。

块的主要优点是它的多功能性:可以子类化块以创建层整个模型(如上MLP类)或各种组件

1.4 顺序块

之前学习的Sequential,它的的设计是为了把其他模块串起来。 为了能够构建出我们自己的简化的MySequential,需要定义两个关键函数:

  • 一种将块逐个追加到列表中的函数

  • 前向传播函数:将输入按追加块的顺序传递给块组成的“链条”

class MySequential(nn.Module):def __init__(self, *args):super().__init__()for idx, module in enumerate(args):# 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员# 变量_modules中。_module的类型是OrderedDictself._modules[str(idx)] = moduledef forward(self, X):# OrderedDict保证了按照成员添加的顺序遍历它们for block in self._modules.values():X = block(X)return X

上述的MySequential类提供了与默认Sequential类相同的功能

__init__函数将每个模块逐个添加到有序字典_modules

_modules的主要优点: 在模块的参数初始化过程中,系统知道在_modules字典中查找需要初始化参数的子块。

MySequential的前向传播函数被调用时,每个添加的块都按照它们被添加的顺序执行。

  • 一个块可以由许多层组成;一个块可以由许多块组成。

  • 块可以包含代码。

  • 块负责大量的内部处理,包括参数初始化和反向传播。

  • 层和块的顺序连接由Sequential块处理。

2 参数管理

选择了架构并设置了超参数后,就进入到了训练阶段。此时的目标:找到使损失函数最小化的模型参数值。训练后,需要使用这些参数来做出预测。或者我们希望你能做到提取参数,以便在其他环境中实现复。

之前的学习中,我们只依靠深度学习框架来完成训练的工作,并没有细细琢磨参数的具体细节。因此,书中在本节介绍了以下内容:

  • 访问参数:用于调试、诊断和可视化

  • 参数初始化

  • 在不同模型组件间共享参数

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))
net(X)# 访问参数,通过索引来访问模型的任意层
print(net[2].state_dict())

上述代码可以在单隐藏层的多层感知机中检查第二个全连接层的参数。首先,这个全连接层包含两个参数,分别是该层的权重和偏置。两者都存储为单精度浮点数float32。参数名称允许唯一标识每个参数。

1)目标参数:

每个参数都表示为参数类的一个实例。 

参数是复合的对象,包含值、梯度和额外信息。 这就是我们需要显式参数值的原因。 除了值之外,我们还可以访问每个参数的梯度。

print(type(net[2].bias))         <class 'torch.nn.parameter.Parameter'>
print(net[2].bias)                  Parameter containing:tensor([0.0887], requires_grad=True)
print(net[2].bias.data)          tensor([0.0887])

2)访问所有参数:

需要递归整个树来提取每个子块的参数。

print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])

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 netrgnet = nn.Sequential(block2(), nn.Linear(4, 1))

打印rgnet的结果如下图所示

 层是分层嵌套的,所以可以像通过嵌套列表索引一样访问它们。例如rgnet[0][1][0].bias.data就代表访问的是第一个顺序组合中的第二个块的第一个线性层的偏置项的数据。

2.2 参数初始化

默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。也就是会提供内置初始化。

2.2.1 内置初始化

让我们首先调用内置的初始化器。 下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量, 且将偏置参数设置为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)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]# 还可以将所有参数初始化为给定的常数,比如初始化为1
# 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]# Xavier初始化
# 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)

2.2.2 自定义初始化

书中想用以下分部为参数w定义初始化方法

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() >= 5net.apply(my_init)
net[0].weight[:2]

2.2.3 参数绑定

如果希望在多个层间共享参数,可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。

当参数绑定时,会由于模型参数包含梯度,因此在反向传播期间第二个隐藏层 (即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。

2.3 延后初始化

目前忽略了建立网络时需要做的事情:

  • 定义了网络架构,但没有指定输入维度

  • 添加层时未指定前一层的输出维度

  • 初始化参数时,没有足够的信息确定模型应包含多少参数

深度学习框架无法判断网络的输入维度是什么。因此需要框架的延后初始化,即直到数据第一次通过模型传递时,框架才会动态地推断出每个层的大小。

在使用卷积神经网络时, 由于输入维度(图像分辨率)将影响每个后续层的维数,会更加需要延后初始化。

  • 延后初始化使框架能自动推断参数形状,使修改模型架构更容易,避免一些常见错误。

  • 我们可以通过模型传递数据,使框架最终初始化参数。

3 自定义层

深度学习成功背后的一个因素是神经网络的灵活性: 我们可以用创造性的方式组合不同的层,从而设计出适用于各种任务的架构。 例如专门用于处理图像、文本、序列数据和执行动态规划的层。有时我们需要自己发明一个现在在深度学习框架中还不存在的层。须构建自定义层。

3.1 不带参数的层

下面的CenteredLayer类要从其输入中减去均值。 要构建它,我们只需继承基础层类并实现前向传播功能。同样的调用父类的__init__(),并1且完成自己所需要的前向传播

import torch
import torch.nn.functional as F
from torch import nnclass CenteredLayer(nn.Module):def __init__(self):super().__init__()# 返回X-X的平均值def forward(self, X):return X - X.mean()

如果我们  输入:[1,2,3,4,5]  输出[-2. ,-1. ,0. ,1. , 2.]

3.2 带参数的层

继续定义具有参数的层, 这些参数可以通过训练进行调整。使用内置函数来创建参数,可以不需要为每个自定义层编写自定义的序列化程序。

现在实现自定义版本的全连接层。需要两个参数,一个用于表示权重,另一个用于表示偏置项。 使用修正线性单元作为激活函数。需要输入参数:in_unitsunits,分别表示输入数和输出数。

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.datareturn F.relu(linear)# 使用自定义层直接执行前向传播计算
linear(torch.rand(2, 5))# 使用自定义层构建模型
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))

4 读写文件(加载和保存)

4.1 加载和保存张量

先导入

import torch
from torch import nn
from torch.nn import functional as F
  • 对于单个张量或张量列表,可以直接调用load和save读写
  • 对于模型中所有权重,可以写入或读取从字符串映射到张量的字典

 

# 单个张量
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')
mydict2

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()
X = torch.randn(size=(2, 20))
Y = net(X)

接下来:

  • 将模型参数保存到文件中
  • 然后直接读取文件中存储的参数恢复模型
  • 判断相同模型参数,相同输入的计算结果是否相同
torch.save(net.state_dict(), 'mlp.params')clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()Y_clone = clone(X)
Y_clone == Y

结果全为True。

5 GPU

应为书中扩展介绍的多个GPU不太适用,因此不过多介绍

感兴趣可以了解:5.6. GPU — 动手学深度学习 2.0.0 documentation

这里有个问题比较重要:

无论何时我们要对多个项进行操作, 它们都必须在同一个设备上。 例如,如果我们对两个张量求和, 我们需要确保两个张量都位于同一个设备上, 否则框架将不知道在哪里存储结果,甚至不知道在哪里执行计算。

神经网络与GPU:神经网络模型可以指定设备,下面的代码将模型参数放在GPU上。

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

其中的try_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')

6 结语

本章链接:5. 深度学习计算 — 动手学深度学习 2.0.0 documentation

学习链接:【视频+教材】原著大佬李沐带你读《动手学习深度学习》真的通俗易懂!深度学习入门必看!(人工智能、机器学习、神经网络、计算机视觉、图像处理、AI)_哔哩哔哩_bilibili

如果你有任何建议或疑问,欢迎留言讨论,最近每天基本上都会看留言,看到会及时回复。

相关文章:

  • 【瑶池数据库训练营及解决方案本周精选(探索PolarDB,参与RDS迁移、连接训练营)】
  • [IMX] 10.串行外围设备接口 - SPI
  • 抢占先机!品牌如何利用软文营销领跑内容营销赛道?
  • Wayland模式X11模式LinuxFB​​模式,Linux图形显示系统三大模式深度解析
  • 如何做好一份技术文档:构建知识传递的精准航海图
  • 【原理扫描】不安全的crossdomain.xml文件和CORS(跨站资源共享)原始验证失败验证与彻底方案
  • CATIA高效工作指南——测量分析篇(一)
  • 算法题(159):快速幂
  • 换行符在markdown格式时异常
  • StringBulder的底层原理?
  • 半导体厂房设计建造流程、方案和技术要点-江苏泊苏系统集成有限公司
  • 语音通信接通率、应答率和转化率有什么区别?
  • spring openfeign
  • Java中hashCode()与equals()的常见错误及解决方案
  • JS入门——三种输入方式
  • 超低延迟与高稳定性的行业领先直播解决方案
  • python里的Matplotlib库
  • 亚马逊商品评论爬取与情感分析:Python+BeautifulSoup实战(含防封策略)
  • 智绅科技——科技赋能健康养老,构建智慧晚年新生态
  • SpringAI系列 - 升级1.0.0
  • 中文网页模板免费下载/长沙网站seo分析
  • 做企业官网需要多少钱/如何做一个网站的seo
  • wordpress是pass么/宁波seo推广费用
  • 达州做网站的公司有哪些/二维码推广赚佣金平台
  • 2016手机网站制作规范/客服系统网页源码2022免费
  • 网站外链/鸡西网站seo