注意
点击此处下载完整示例代码
简介 || 张量 || Autograd || 构建模型 || TensorBoard 支持 || 训练模型 || 理解模型
使用 PyTorch 构建模型¶
创建日期:2021 年 11 月 30 日 | 最后更新:2024 年 10 月 15 日 | 最后验证: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)
The model:
TinyModel(
(linear1): Linear(in_features=100, out_features=200, bias=True)
(activation): ReLU()
(linear2): Linear(in_features=200, out_features=10, bias=True)
(softmax): Softmax(dim=None)
)
Just one layer:
Linear(in_features=200, out_features=10, bias=True)
Model params:
Parameter containing:
tensor([[ 0.0765, 0.0830, -0.0234, ..., -0.0337, -0.0355, -0.0968],
[-0.0573, 0.0250, -0.0132, ..., -0.0060, 0.0240, 0.0280],
[-0.0908, -0.0369, 0.0842, ..., -0.0078, -0.0333, -0.0324],
...,
[-0.0273, -0.0162, -0.0878, ..., 0.0451, 0.0297, -0.0722],
[ 0.0833, -0.0874, -0.0020, ..., -0.0215, 0.0356, 0.0405],
[-0.0637, 0.0190, -0.0571, ..., -0.0874, 0.0176, 0.0712]],
requires_grad=True)
Parameter containing:
tensor([ 0.0304, -0.0758, -0.0549, -0.0893, -0.0809, -0.0804, -0.0079, -0.0413,
-0.0968, 0.0888, 0.0239, -0.0659, -0.0560, -0.0060, 0.0660, -0.0319,
-0.0370, 0.0633, -0.0143, -0.0360, 0.0670, -0.0804, 0.0265, -0.0870,
0.0039, -0.0174, -0.0680, -0.0531, 0.0643, 0.0794, 0.0209, 0.0419,
0.0562, -0.0173, -0.0055, 0.0813, 0.0613, -0.0379, 0.0228, 0.0304,
-0.0354, 0.0609, -0.0398, 0.0410, 0.0564, -0.0101, -0.0790, -0.0824,
-0.0126, 0.0557, 0.0900, 0.0597, 0.0062, -0.0108, 0.0112, -0.0358,
-0.0203, 0.0566, -0.0816, -0.0633, -0.0266, -0.0624, -0.0746, 0.0492,
0.0450, 0.0530, -0.0706, 0.0308, 0.0533, 0.0202, -0.0469, -0.0448,
0.0548, 0.0331, 0.0257, -0.0764, -0.0892, 0.0783, 0.0062, 0.0844,
-0.0959, -0.0468, -0.0926, 0.0925, 0.0147, 0.0391, 0.0765, 0.0059,
0.0216, -0.0724, 0.0108, 0.0701, -0.0147, -0.0693, -0.0517, 0.0029,
0.0661, 0.0086, -0.0574, 0.0084, -0.0324, 0.0056, 0.0626, -0.0833,
-0.0271, -0.0526, 0.0842, -0.0840, -0.0234, -0.0898, -0.0710, -0.0399,
0.0183, -0.0883, -0.0102, -0.0545, 0.0706, -0.0646, -0.0841, -0.0095,
-0.0823, -0.0385, 0.0327, -0.0810, -0.0404, 0.0570, 0.0740, 0.0829,
0.0845, 0.0817, -0.0239, -0.0444, -0.0221, 0.0216, 0.0103, -0.0631,
0.0831, -0.0273, 0.0756, 0.0022, 0.0407, 0.0072, 0.0374, -0.0608,
0.0424, -0.0585, 0.0505, -0.0455, 0.0268, -0.0950, -0.0642, 0.0843,
0.0760, -0.0889, -0.0617, -0.0916, 0.0102, -0.0269, -0.0011, 0.0318,
0.0278, -0.0160, 0.0159, -0.0817, 0.0768, -0.0876, -0.0524, -0.0332,
-0.0583, 0.0053, 0.0503, -0.0342, -0.0319, -0.0562, 0.0376, -0.0696,
0.0735, 0.0222, -0.0775, -0.0072, 0.0294, 0.0994, -0.0355, -0.0809,
-0.0539, 0.0245, 0.0670, 0.0032, 0.0891, -0.0694, -0.0994, 0.0126,
0.0629, 0.0936, 0.0058, -0.0073, 0.0498, 0.0616, -0.0912, -0.0490],
requires_grad=True)
Parameter containing:
tensor([[ 0.0504, -0.0203, -0.0573, ..., 0.0253, 0.0642, -0.0088],
[-0.0078, -0.0608, -0.0626, ..., -0.0350, -0.0028, -0.0634],
[-0.0317, -0.0202, -0.0593, ..., -0.0280, 0.0571, -0.0114],
...,
[ 0.0582, -0.0471, -0.0236, ..., 0.0273, 0.0673, 0.0555],
[ 0.0258, -0.0706, 0.0315, ..., -0.0663, -0.0133, 0.0078],
[-0.0062, 0.0544, -0.0280, ..., -0.0303, -0.0326, -0.0462]],
requires_grad=True)
Parameter containing:
tensor([ 0.0385, -0.0116, 0.0703, 0.0407, -0.0346, -0.0178, 0.0308, -0.0502,
0.0616, 0.0114], requires_grad=True)
Layer params:
Parameter containing:
tensor([[ 0.0504, -0.0203, -0.0573, ..., 0.0253, 0.0642, -0.0088],
[-0.0078, -0.0608, -0.0626, ..., -0.0350, -0.0028, -0.0634],
[-0.0317, -0.0202, -0.0593, ..., -0.0280, 0.0571, -0.0114],
...,
[ 0.0582, -0.0471, -0.0236, ..., 0.0273, 0.0673, 0.0555],
[ 0.0258, -0.0706, 0.0315, ..., -0.0663, -0.0133, 0.0078],
[-0.0062, 0.0544, -0.0280, ..., -0.0303, -0.0326, -0.0462]],
requires_grad=True)
Parameter containing:
tensor([ 0.0385, -0.0116, 0.0703, 0.0407, -0.0346, -0.0178, 0.0308, -0.0502,
0.0616, 0.0114], requires_grad=True)
这展示了 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)
Input:
tensor([[0.8790, 0.9774, 0.2547]])
Weight and Bias parameters:
Parameter containing:
tensor([[ 0.1656, 0.4969, -0.4972],
[-0.2035, -0.2579, -0.3780]], requires_grad=True)
Parameter containing:
tensor([0.3768, 0.3781], requires_grad=True)
Output:
tensor([[ 0.8814, -0.1492]], grad_fn=<AddmmBackward0>)
如果你将 x
与线性层的权重进行矩阵乘法,并加上偏置,你会发现得到输出向量 y
。
另一个需要注意的重要特性是:当我们使用 lin.weight
检查层的权重时,它报告自己是 Parameter
(它是 torch.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。在将此输出传递给线性层之前,它被重塑为 16 * 6 * 6 = 576 元素的向量,供下一层使用。
有用于处理一维、二维和三维张量的卷积层。卷积层构造函数还有更多可选参数,包括输入中的步长(例如,只扫描每第二个或每第三个位置)、填充(以便你可以扫描到输入的边缘)等等。请参阅文档以获取更多信息。
循环层¶
循环神经网络(或 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
维空间中是一个 one-hot 向量(或单位向量)。tagset_size
是输出集合中的标签数量。embedding_dim
是词汇表 嵌入 空间的大小。嵌入将词汇表映射到低维空间,在该空间中,含义相似的词彼此靠近。hidden_dim
是 LSTM 记忆的大小。
输入将是句子,单词表示为 one-hot 向量的索引。然后嵌入层将这些向量映射到 embedding_dim
维空间。LSTM 接收这个嵌入序列并对其进行迭代,生成长度为 hidden_dim
的输出向量。最后的线性层充当分类器;对最后一层的输出应用 log_softmax()
会将输出转换为一组归一化的估计概率,表示给定单词映射到给定标签的概率。
如果你想看到这个网络的实际应用,请查看 pytorch.org 上的序列模型和 LSTM 网络教程。
Transformer¶
Transformer 是多用途网络,凭借 BERT 等模型在自然语言处理领域取得了最先进的成果。Transformer 架构的讨论超出了本视频的范围,但 PyTorch 有一个 Transformer
类,允许你定义 Transformer 模型的整体参数——注意力头的数量、编码器和解码器层的数量、dropout 和激活函数等。(甚至可以使用这一个类构建 BERT 模型,只需提供正确的参数!)torch.nn.Transformer
类还包含封装各个组件(TransformerEncoder
、TransformerDecoder
)和子组件(TransformerEncoderLayer
、TransformerDecoderLayer
)的类。有关详细信息,请查阅 Transformer 类的文档。
其他层和函数¶
数据操作层¶
还有其他层类型在模型中执行重要功能,但不参与学习过程本身。
最大池化(及其孪生兄弟最小池化)通过组合单元格来减小张量,并将输入单元格的最大值赋给输出单元格(我们已经看到了这一点)。例如
my_tensor = torch.rand(1, 6, 6)
print(my_tensor)
maxpool_layer = torch.nn.MaxPool2d(3)
print(maxpool_layer(my_tensor))
tensor([[[0.5036, 0.6285, 0.3460, 0.7817, 0.9876, 0.0074],
[0.3969, 0.7950, 0.1449, 0.4110, 0.8216, 0.6235],
[0.2347, 0.3741, 0.4997, 0.9737, 0.1741, 0.4616],
[0.3962, 0.9970, 0.8778, 0.4292, 0.2772, 0.9926],
[0.4406, 0.3624, 0.8960, 0.6484, 0.5544, 0.9501],
[0.2489, 0.8971, 0.7499, 0.1803, 0.9571, 0.6733]]])
tensor([[[0.7950, 0.9876],
[0.9970, 0.9926]]])
如果你仔细观察上面的值,你会发现最大池化输出中的每个值都是 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())
tensor([[[ 7.7375, 23.5649, 6.8452, 16.3517],
[19.5792, 20.3254, 6.1930, 23.7576],
[23.7554, 20.8565, 18.4241, 8.5742],
[22.5100, 15.6154, 13.5698, 11.8411]]])
tensor(16.2188)
tensor([[[-0.8614, 1.4543, -0.9919, 0.3990],
[ 0.3160, 0.4274, -1.6834, 0.9400],
[ 1.0256, 0.5176, 0.0914, -1.6346],
[ 1.6352, -0.0663, -0.5711, -0.9978]]],
grad_fn=<NativeBatchNormBackward0>)
tensor(3.3528e-08, grad_fn=<MeanBackward0>)
运行上面的单元格,我们已经为输入张量添加了一个较大的缩放因子和偏移量;你应该会看到输入张量的 mean()
在 15 附近。通过归一化层运行后,你会看到值变小了,并且聚集在零附近——事实上,均值应该非常小(> 1e-8)。
这是有益的,因为许多激活函数(下面讨论)在接近 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))
tensor([[[0.0000, 0.0000, 0.0000, 0.2878],
[0.0000, 0.6824, 0.0000, 0.5920],
[0.0000, 0.0000, 1.3319, 0.5738],
[0.5676, 0.8335, 0.9647, 0.2928]]])
tensor([[[0.0000, 0.0000, 0.2098, 0.0000],
[0.0000, 0.6824, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.5738],
[0.0000, 0.8335, 0.0000, 0.2928]]])
上面,你可以看到 dropout 对示例张量的影响。你可以使用可选的 p
参数设置单个权重 dropout 的概率;如果不设置,默认为 0.5。
激活函数¶
激活函数使深度学习成为可能。神经网络实际上是一个程序——具有许多参数——它 模拟数学函数。如果所有我们做的就是重复地用层权重乘以张量,我们只能模拟 线性函数; 此外,拥有许多层就没有意义了,因为整个网络可以简化为单次矩阵乘法。在层之间插入 非线性 激活函数是深度学习模型能够模拟任何函数,而不仅仅是线性函数的原因。
torch.nn.Module
包含了封装所有主要激活函数(包括 ReLU 及其许多变体、Tanh、Hardtanh、sigmoid 等)的对象。它还包括其他函数,例如 Softmax,这些函数在模型的输出阶段最有用。
损失函数¶
损失函数告诉我们模型的预测与正确答案相差多远。PyTorch 包含多种损失函数,包括常见的 MSE(均方误差 = L2 范数)、交叉熵损失和负对数似然损失(对分类器有用)等。
脚本总运行时间: ( 0 分钟 0.021 秒)