1 层(Layer)
1.1 定义
层是深度学习模型中的基本构建单元,它由一组神经元组成,负责对输入数据进行特定的数学运算和变换,以提取数据的某种特征或表示。每一层可以看作是一个函数,它接收输入数据,并通过该层的权重和偏置等参数以及激活函数等操作,产生输出。
1.2 作用
不同的层可以实现不同的功能,比如卷积层可以用于提取图像的空间特征,池化层可以用于下采样以减少数据的维度和计算量,全连接层可以用于对全局特征进行综合和分类等。通过堆叠多层,模型可以逐步学习数据的复杂模式和层次化特征,从而对输入数据进行更深入的理解和处理,以完成如分类、回归、生成等任务。
2 块(Block)
2.1 定义
块是比层更高层次的构建单元,它通常由多个层按照某种特定的方式组合而成,形成一个相对独立的功能模块。块可以看作是一个封装好的子网络,具有一致的输入输出接口,可以在不同的位置重复使用。
块(block)可以描述单个层、由多个层组成的组件或整个模型本身
块由类(class)表示。 它的任何子类都必须定义一个将其输入转换为输出的前向传播函数, 并且必须存储任何必需的参数(有些块不需要任何参数)。
块必须具有反向传播函数
2.2 作用
块的设计主要是为了提高模型的性能和效率,以及增强模型的可扩展性和可重用性。例如,ResBlock(残差块)通过引入残差连接,解决了深层网络训练时梯度消失和梯度爆炸的问题,使得网络可以更有效地训练;Inception Block(Inception块)通过多尺度卷积操作,能够同时捕捉不同尺度的特征,提高模型对特征的表达能力。还有像Transformer中的Encoder Block和Decoder Block,它们分别负责编码和解码序列信息,通过堆叠多个这样的块可以构建强大的序列处理模型。
3 两者的区别与联系
区别
(1)粒度不同
层是模型的微观构成部分,是最基本的计算单元;块则是由多个层组成的宏观模块,是对多个层的进一步抽象和封装。
(2)功能侧重不同
层主要关注于实现具体的数学运算和特征提取;块更侧重于组合多个层以实现某种特定的结构或功能,解决某些特定的问题或满足特定的性能要求。
联系
(1)块是由层构成的,多个层按照一定的规则组合在一起就形成了一个块。
(2)层和块在构建深度学习模型时是相互配合的,层是块的基础,块则是构建更复杂模型结构的重要单元。通过对不同层和块的组合和堆叠,可以构建出各种各样功能强大的深度学习模型,以应对不同的任务和数据特点。
4 多层感知机代码
MLP: 包含256个隐藏单元和ReLU激活函数的全连接隐藏层, 具有10个隐藏单元且不带激活函数的全连接输出层
import torch
from torch import nn
from torch.nn import functional as F
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
X = torch.rand(2, 20)
net(X)
4.1 自定义块
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)))
net = MLP()
net(X)
4.2 顺序块
4.2.1 代码示例
import torch
import torch.nn as nn
# 定义 MySequential 类
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for idx, module in enumerate(args):
self._modules[str(idx)] = module
def forward(self, X):
for block in self._modules.values():
X = block(X)
return X
定义一个简单的神经网络模型,使用 MySequential 组合多个层
model = MySequential(
nn.Linear(4, 8), # 第一层:输入维度4,输出维度8的全连接层
nn.ReLU(), # 第二层:ReLU 激活函数
nn.Linear(8, 10), # 第三层:输入维度8,输出维度10的全连接层
nn.Sigmoid() # 第四层:Sigmoid 激活函数
)
# 输入张量,假设 batch_size 为 2,输入特征维度为4
X = torch.randn(2, 4)
print("输入 X 的形状:", X.shape)
# 前向传播
output = model(X)
print("输出 output 的形状:", output.shape)
4.2.2 MySequential调用过程
在上述案例中,for idx, module in enumerate(args): self._modules[str(idx)] = module
这段代码在实例化 MySequential
模型时执行,具体来说,当创建 model
实例时,传入的 args
包含四个模块:nn.Linear(4, 8)
、nn.ReLU()
、nn.Linear(8, 10)
和 nn.Sigmoid()
。以下是这段代码在该案例中的具体执行过程:
调用
MySequential
的构造函数- 当执行
model = MySequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 10), nn.Sigmoid())
时,会调用MySequential
类的__init__
方法,传入的args
是一个包含四个模块的元组。
- 当执行
初始化父类
- 执行
super().__init__()
,完成nn.Module
父类的初始化,这包括初始化_modules
这个有序字典,用于存储子模块。
- 执行
遍历
args
中的模块- 使用
enumerate(args)
遍历传入的模块。enumerate
会返回每个模块及其对应的索引:- 第一次迭代:
idx = 0
,module = nn.Linear(4, 8)
- 第二次迭代:
idx = 1
,module = nn.ReLU()
- 第三次迭代:
idx = 2
,module = nn.Linear(8, 10)
- 第四次迭代:
idx = 3
,module = nn.Sigmoid()
- 第一次迭代:
- 使用
将模块添加到
_modules
- 对于每次迭代,将当前模块添加到
_modules
有序字典中,键为索引的字符串形式,值为对应的模块:- 第一次迭代后:
self._modules["0"] = nn.Linear(4, 8)
- 第二次迭代后:
self._modules["1"] = nn.ReLU()
- 第三次迭代后:
self._modules["2"] = nn.Linear(8, 10)
- 第四次迭代后:
self._modules["3"] = nn.Sigmoid()
- 第一次迭代后:
- 对于每次迭代,将当前模块添加到
最终结果
_modules
有序字典中按顺序存储了传入的四个模块,键分别为"0"
、"1"
、"2"
、"3"
,对应的值分别是nn.Linear(4, 8)
、nn.ReLU()
、nn.Linear(8, 10)
和nn.Sigmoid()
。这些模块被正确地注册为
MySequential
实例的子模块,这样在后续的前向传播过程中,可以通过_modules.values()
按顺序获取并执行这些模块,确保输入数据依次经过每个模块的处理。
这段代码在实例化 MySequential
时,将传入的模块按照顺序添加到 _modules
中,为后续的前向传播做好准备。
4.2.3 forward方法调用过程
在上面的例子中,forward
方法的执行过程如下:
- 调用
forward
方法
当执行 output = model(X)
时,会自动调用 MySequential
类的 forward
方法,将输入张量 X
传递进去。
- 遍历
_modules.values()
forward
方法中通过 self._modules.values()
获取按顺序排列的子模块集合:
for block in self._modules.values():
- 依次执行每个模块的前向传播
对于每个模块(block
),将当前的输入 X
传递给该模块进行前向传播:
X = block(X)
4.2.4 forward方法具体执行过程示例
假设输入张量 X
的形状为 (2, 4)
:
第一个模块:
block = nn.Linear(4, 8)
- 输入:
X
(形状(2, 4)
) - 进行线性变换,权重矩阵
W
的形状为(8, 4)
,偏置项b
的形状为(8,)
- 输出:经过线性变换后的张量(形状
(2, 8)
)
- 输入:
第二个模块:
block = nn.ReLU()
- 输入:上一步得到的张量(形状
(2, 8)
) - 应用 ReLU 激活函数,将负值变为 0,正值保持不变
- 输出:ReLU 激活后的张量(形状
(2, 8)
)
- 输入:上一步得到的张量(形状
第三个模块:
block = nn.Linear(8, 10)
- 输入:ReLU 激活后的张量(形状
(2, 8)
) - 进行线性变换,权重矩阵
W
的形状为(10, 8)
,偏置项b
的形状为(10,)
- 输出:经过线性变换后的张量(形状
(2, 10)
)
- 输入:ReLU 激活后的张量(形状
第四个模块:
block = nn.Sigmoid()
- 输入:上一步得到的张量(形状
(2, 10)
) - 应用 Sigmoid 激活函数,将每个元素压缩到
(0, 1)
的范围内 - 输出:Sigmoid 激活后的张量(形状
(2, 10)
)
- 输入:上一步得到的张量(形状
返回最终输出
所有模块执行完毕后,将最后得到的张量作为整个模型的输出返回:
return X
在上面的例子中,最终输出张量的形状为 (2, 10)
4.3 在前向传播中自定义过程
在前向传播函数中执行Python的控制流,如计算函数 f ( x , w ) = c ∗ w T x f(x,w)=c*w^Tx f(x,w)=c∗wTx的层, 其中x是输入, w是参数,c是某个在优化过程中没有更新的指定常量
4.3.1 FixedHiddenMLP类示例
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)
# 控制流
while X.abs().sum() > 1:
X /= 2
return X.sum()
4.3.2 FixedHiddenMLP 类解析
这个类定义了一个包含特定隐藏层结构的多层感知机(MLP),它具有以下特点:
__init__
方法 :用于初始化模型的参数和层。forward
方法 :定义了模型的前向传播逻辑,包括线性变换、激活函数、矩阵乘法、控制流语句等。
- 初始化过程
self.rand_weight = torch.rand((20, 20), requires_grad=False)
- 随机权重参数
self.rand_weight
:创建一个形状为(20, 20)
的随机权重矩阵,并设置requires_grad=False
,表示这个参数在训练过程中不计算梯度,即保持不变。
self.linear = nn.Linear(20, 20)
- 全连接层
self.linear
:定义一个输入和输出维度都为 20 的全连接层,这个层的权重和偏置会在训练过程中更新。
- 前向传播过程
X = self.linear(X)
- 第一步:全连接层 :将输入
X
传递给全连接层self.linear
,进行线性变换,输出结果的形状与输入相同,为(batch_size, 20)
。
X = F.relu(torch.mm(X, self.rand_weight) + 1)
- 第二步:矩阵乘法和激活函数 :将上一步的输出与随机权重矩阵
self.rand_weight
进行矩阵乘法操作,然后加上 1,再应用 ReLU 激活函数。这一步的计算可以表示为X = F.relu(torch.mm(X, self.rand_weight) + 1)
。矩阵乘法操作将输入特征与随机权重矩阵相乘,加上 1 后应用 ReLU 激活函数,引入非线性。
X = self.linear(X)
- 第三步:复用全连接层 :再次将上一步的输出传递给全连接层
self.linear
,进行线性变换。这相当于两个全连接层共享相同的参数。
while X.abs().sum() > 1:
X /= 2
- 第四步:控制流语句 :使用一个
while
循环检查张量X
的绝对值之和是否大于 1。如果是,则将X
除以 2,直到其绝对值之和小于或等于 1。这个控制流语句用于对输出进行归一化,确保其值不会过大。
return X.sum()
- 第五步:返回结果 :最后返回张量
X
的所有元素之和。
- 总结
这个 FixedHiddenMLP
类定义了一个具有特定隐藏层结构的多层感知机。它的前向传播过程包括两次全连接层的线性变换,中间插入了矩阵乘法和 ReLU 激活函数,并使用了控制流语句对输出进行归一化。这种结构展示了如何在深度学习模型中使用控制流语句和共享参数的层。
在实际使用中,这个模型的输入需要是一个二维张量,形状为 (batch_size, 20)
,其中 batch_size
是批量大小。模型的输出是一个标量,即张量 X
的所有元素之和。