备注
点击此处下载完整示例代码
简介 || 张量 || Autograd || 模型构建 || TensorBoard 支持 || 训练模型 || 模型理解
使用 PyTorch 构建模型
创建于:2025 年 4 月 1 日 | 最后更新:2025 年 4 月 1 日 | 最后验证:2024 年 11 月 5 日
按照下面的视频或 YouTube 上的视频进行操作。
torch.nn.Module
和 torch.nn.Parameter
¶
在这个视频中,我们将讨论 PyTorch 为构建深度学习网络提供的部分工具。
除了 Parameter
之外,本视频中讨论的类都是 torch.nn.Module
的子类。这是 PyTorch 的基础类,用于封装 PyTorch 模型及其组件的特定行为。
torch.nn.Module
的重要行为之一是注册参数。如果特定的 Module
子类有学习权重,这些权重将表示为 torch.nn.Parameter
的实例。 Parameter
类是 torch.Tensor
的子类,具有特殊行为,即当它们被分配为 Module
的属性时,它们会被添加到该模块参数列表中。这些参数可以通过 Module
类上的 parameters()
方法访问。
以下是一个简单的例子,这是一个包含两个线性层和一个激活函数的非常简单的模型。我们将创建其实例,并要求它报告其参数:
import torch
class TinyModel(torch.nn.Module):
def __init__(self):
super(TinyModel, self).__init__()
self.linear1 = torch.nn.Linear(100, 200)
self.activation = torch.nn.ReLU()
self.linear2 = torch.nn.Linear(200, 10)
self.softmax = torch.nn.Softmax()
def forward(self, x):
x = self.linear1(x)
x = self.activation(x)
x = self.linear2(x)
x = self.softmax(x)
return x
tinymodel = TinyModel()
print('The model:')
print(tinymodel)
print('\n\nJust one layer:')
print(tinymodel.linear2)
print('\n\nModel params:')
for param in tinymodel.parameters():
print(param)
print('\n\nLayer params:')
for param in tinymodel.linear2.parameters():
print(param)
这显示了 PyTorch 模型的基本结构:有一个 __init__()
方法定义模型的层和其他组件,还有一个 forward()
方法在这里执行计算。请注意,我们可以打印模型或其任何子模块,以了解其结构。
常见层类型
线性层
神经网络中最基本的层是线性层或全连接层。这是一个每一输入都影响层中每一个输出的层,影响的程度由层的权重决定。如果一个模型有 m 个输入和 n 个输出,那么权重将是一个 m x n 的矩阵。例如:
lin = torch.nn.Linear(3, 2)
x = torch.rand(1, 3)
print('Input:')
print(x)
print('\n\nWeight and Bias parameters:')
for param in lin.parameters():
print(param)
y = lin(x)
print('\n\nOutput:')
print(y)
如果你对 x
进行矩阵乘法,并与线性层的权重相乘,然后加上偏置,你会得到输出向量 y
。
另一个需要注意的重要特性是:当我们检查 lin.weight
层的权重时,它报告自己为 Parameter
(这是 Tensor
的子类),并告诉我们它正在使用 autograd 跟踪梯度。这是 Parameter
的默认行为,与 Tensor
不同。
线性层在深度学习模型中得到了广泛的应用。你最常见的它们出现在分类器模型中,这些模型通常在末尾有一个或多个线性层,最后一个层将有 n 个输出,其中 n 是分类器所处理的类别数量。
卷积层
卷积层是为了处理具有高度空间相关性的数据而构建的。它们在计算机视觉中非常常见,可以检测特征之间的紧密分组,并将这些特征组合成更高级别的特征。它们在其他环境中也很常见——例如,在自然语言处理应用中,一个词的即时上下文(即序列中附近的其他词)可以影响句子的含义。
我们在之前的视频中看到了 LeNet5 中的卷积层应用:
import torch.functional as F
class LeNet(torch.nn.Module):
def __init__(self):
super(LeNet, self).__init__()
# 1 input image channel (black & white), 6 output channels, 5x5 square convolution
# kernel
self.conv1 = torch.nn.Conv2d(1, 6, 5)
self.conv2 = torch.nn.Conv2d(6, 16, 3)
# an affine operation: y = Wx + b
self.fc1 = torch.nn.Linear(16 * 6 * 6, 120) # 6*6 from image dimension
self.fc2 = torch.nn.Linear(120, 84)
self.fc3 = torch.nn.Linear(84, 10)
def forward(self, x):
# Max pooling over a (2, 2) window
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# If the size is a square you can only specify a single number
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
x = x.view(-1, self.num_flat_features(x))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
def num_flat_features(self, x):
size = x.size()[1:] # all dimensions except the batch dimension
num_features = 1
for s in size:
num_features *= s
return num_features
让我们来分析这个模型卷积层中正在发生的事情。从 conv1
开始:
LeNet5 旨在接收一个 1x32x32 的黑白图像。卷积层构造函数的第一个参数是输入通道数。在这里,它是 1。如果我们构建这个模型来查看 3 个颜色通道,它将是 3。
卷积层就像一个在图像上扫描的窗口,寻找它所识别的图案。这些模式被称为特征,卷积层的一个参数是我们希望它学习的特征数量。这是构造函数的第二个参数,即输出特征的数量。在这里,我们要求我们的层学习 6 个特征。
在上面,我把卷积层比作一个窗口 - 但窗口有多大?第三个参数是窗口或核大小。在这里,“5”表示我们选择了一个 5x5 的核。如果你想要高度和宽度不同的核,你可以为这个参数指定一个元组 - 例如,
(3, 5)
来得到一个 3x5 的卷积核。
卷积层的输出是一个激活图——输入张量中特征存在性的空间表示。 conv1
将为我们提供一个 6x28x28 的输出张量;6 是特征的数量,28 是地图的高度和宽度。28 来自这样一个事实:当在 32 像素的行上扫描一个 5 像素的窗口时,只有 28 个有效的位置。
然后,我们将卷积的输出通过 ReLU 激活函数(关于激活函数的更多内容将在后面介绍),然后通过最大池化层。最大池化层将激活图中彼此靠近的特征分组在一起。它是通过减少张量,将输出中的每个 2x2 单元格组合并为一个单元格,并将该单元格分配给这 4 个单元格中的最大值来实现的。这给我们提供了一个分辨率较低的激活图版本,其尺寸为 6x14x14。
我们下一个卷积层, conv2
,期望有 6 个输入通道(对应于第一层寻求的 6 个特征),有 16 个输出通道,并且使用 3x3 的核。它输出一个 16x12x12 的激活图,然后通过最大池化层再次减少到 16x6x6。在将此输出传递给线性层之前,它被重塑为一个 576 个元素的向量,以便下一层消费。
存在用于处理 1D、2D 和 3D 张量的卷积层。还有许多可选参数用于卷积层构造函数,包括步长长度(例如,只扫描每个第二个或第三个位置)在输入中,填充(以便可以扫描到输入的边缘),等等。请参阅文档以获取更多信息。
循环层
递归神经网络(或 RNN)用于序列数据 - 从科学仪器的时序测量到自然语言句子再到 DNA 核苷酸。RNN 通过维护一个隐藏状态来实现这一点,该状态充当一种记忆,记录了到目前为止它已经看到的序列内容。
RNN 层(或其变体,如 LSTM(长短期记忆)和 GRU(门控循环单元))的内部结构相对复杂,超出了本视频的范围,但我们将向您展示一个基于 LSTM 的词性标注器(一种分类器,可以告诉您一个词是名词、动词等)的工作原理:
class LSTMTagger(torch.nn.Module):
def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size):
super(LSTMTagger, self).__init__()
self.hidden_dim = hidden_dim
self.word_embeddings = torch.nn.Embedding(vocab_size, embedding_dim)
# The LSTM takes word embeddings as inputs, and outputs hidden states
# with dimensionality hidden_dim.
self.lstm = torch.nn.LSTM(embedding_dim, hidden_dim)
# The linear layer that maps from hidden state space to tag space
self.hidden2tag = torch.nn.Linear(hidden_dim, tagset_size)
def forward(self, sentence):
embeds = self.word_embeddings(sentence)
lstm_out, _ = self.lstm(embeds.view(len(sentence), 1, -1))
tag_space = self.hidden2tag(lstm_out.view(len(sentence), -1))
tag_scores = F.log_softmax(tag_space, dim=1)
return tag_scores
构造函数有四个参数:
vocab_size
是输入词汇表中的单词数量。每个单词都是一个在vocab_size
维空间中的一维向量(或单位向量)。tagset_size
是输出集合中的标签数量。embedding_dim
是词汇表的嵌入空间大小。嵌入将词汇映射到低维空间,其中具有相似意义的词语在该空间中彼此靠近。hidden_dim
是 LSTM 的内存大小。
输入将是一个句子,其中的词语以一热向量的索引表示。然后嵌入层将这些映射到 embedding_dim
维度的空间。LSTM 将迭代这个嵌入序列,生成一个长度为 hidden_dim
的输出向量。最终的线性层充当分类器;将 log_softmax()
应用到最终层的输出,将输出转换为估计概率的归一化集合,这些概率表示给定的词映射到给定的标签。
如果你想看到这个网络的实际运行情况,请查看 pytorch.org 上的序列模型和 LSTM 网络教程。
生成器
变换器是多用途网络,BERT 等模型已经使它们在 NLP 领域达到了最先进的状态。关于变换器架构的讨论超出了本视频的范围,但 PyTorch 有一个 Transformer
类,允许您定义变换器模型的整体参数——注意力头的数量、编码器和解码器层的数量、dropout 和激活函数等。(您甚至可以使用这个单个类构建 BERT 模型,只要设置正确的参数!) torch.nn.Transformer
类还有类来封装单个组件( TransformerEncoder
, TransformerDecoder
)和子组件( TransformerEncoderLayer
, TransformerDecoderLayer
)。有关详细信息,请参阅变换器类的文档。
其他层和函数
数据操作层
模型中还有其他类型的层执行重要的功能,但它们自身并不参与学习过程。
最大池化(及其孪生兄弟最小池化)通过合并单元格并将输入单元格的最大值分配给输出单元格来减少张量(我们之前看到过)。例如:
my_tensor = torch.rand(1, 6, 6)
print(my_tensor)
maxpool_layer = torch.nn.MaxPool2d(3)
print(maxpool_layer(my_tensor))
如果您仔细观察上面的值,您会看到每个 maxpooled 输出中的每个值都是 6x6 输入的每个象限的最大值。
归一化层在将输出馈送到另一个层之前重新居中和归一化一个层的输出。对中间张量进行居中和缩放具有许多有益的效果,例如让您可以使用更高的学习率而不会出现梯度爆炸/消失。
my_tensor = torch.rand(1, 4, 4) * 20 + 5
print(my_tensor)
print(my_tensor.mean())
norm_layer = torch.nn.BatchNorm1d(4)
normed_tensor = norm_layer(my_tensor)
print(normed_tensor)
print(normed_tensor.mean())
运行上面的单元格,我们给输入张量添加了一个大的缩放因子和偏移量;你应该能在大约 15 的附近看到输入张量的 mean()
。在通过归一化层运行后,你可以看到值更小,并且围绕零分组——实际上,平均值应该非常小(> 1e-8)。
这是有益的,因为许多激活函数(下文将讨论)在 0 附近的梯度最强,但有时对于远离 0 的输入会遭受梯度消失或爆炸。保持数据围绕最陡梯度区域将倾向于意味着更快的、更好的学习和更高的可行学习率。
Dropout 层是鼓励模型中稀疏表示的工具——也就是说,推动它在更少的数据上进行推理。
Dropout 层通过在训练过程中随机设置输入张量的部分来工作——推理时总是关闭 dropout 层。这迫使模型在掩码或减少的数据集上学习。例如:
my_tensor = torch.rand(1, 4, 4)
dropout = torch.nn.Dropout(p=0.4)
print(dropout(my_tensor))
print(dropout(my_tensor))
在上方,您可以看到 dropout 在样本张量上的效果。您可以使用可选的 p
参数来设置单个权重掉出的概率;如果不设置,则默认为 0.5。
激活函数 ¶
激活函数使得深度学习成为可能。神经网络实际上是一个程序——具有许多参数——它模拟一个数学函数。如果我们只是反复将张量与层权重相乘,我们只能模拟线性函数;更进一步,拥有许多层也就没有意义,因为整个网络可以被简化为一次矩阵乘法。在层之间插入非线性激活函数,这使得深度学习模型能够模拟任何函数,而不仅仅是线性函数。
torch.nn.Module
包含封装了所有主要激活函数的对象,包括 ReLU 及其许多变体、Tanh、Hardtanh、sigmoid 等。它还包括其他函数,如 Softmax,这些函数在模型的输出阶段非常有用。
损失函数 ¶
损失函数告诉我们模型预测与正确答案之间的距离。PyTorch 包含多种损失函数,包括常见的均方误差(MSE = L2 范数)、交叉熵损失和负似然损失(对分类器很有用),以及其他一些。
脚本总运行时间:(0 分钟 0.000 秒)