注意
转到末尾 下载完整的示例代码。
简介 || 张量 || 自动求导 || 构建模型 || TensorBoard 支持 || 训练模型 || 模型理解
使用 PyTorch 构建模型#
创建日期:2021 年 11 月 30 日 | 最后更新:2024 年 10 月 15 日 | 最后验证:2024 年 11 月 05 日
请观看下方视频或在 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.0322, 0.0357, 0.0205, ..., -0.0918, 0.0783, -0.0910],
[ 0.0551, 0.0805, -0.0808, ..., 0.0964, 0.0559, 0.0171],
[-0.0272, -0.0063, -0.0203, ..., 0.0708, 0.0556, 0.0738],
...,
[-0.0247, 0.0970, 0.0810, ..., 0.0028, 0.0097, -0.0533],
[ 0.0591, -0.0928, -0.0171, ..., -0.0478, 0.0716, 0.0060],
[ 0.0325, -0.0762, 0.0917, ..., -0.0725, 0.0174, -0.0642]],
requires_grad=True)
Parameter containing:
tensor([ 0.0832, 0.0245, -0.0256, 0.0963, -0.0722, 0.0082, -0.0308, -0.0903,
-0.0517, -0.0809, -0.0333, 0.0772, 0.0192, -0.0217, 0.0241, 0.0958,
0.0708, -0.0342, 0.0012, -0.0615, 0.0853, -0.0097, -0.0771, -0.0326,
0.0484, -0.0936, -0.0539, 0.0341, 0.0452, -0.0608, 0.0880, -0.0819,
-0.0311, 0.0185, -0.0020, -0.0035, 0.0834, 0.0660, 0.0357, 0.0797,
-0.0767, -0.0189, -0.0143, 0.0389, 0.0425, 0.0646, -0.0851, -0.0610,
-0.0258, -0.0071, -0.0992, 0.0287, 0.0782, -0.0280, -0.0663, -0.0801,
0.0084, -0.0914, 0.0750, 0.0497, -0.0233, -0.0847, -0.0906, -0.0064,
0.0117, 0.0040, -0.0204, -0.0995, -0.0529, 0.0539, 0.0925, 0.0380,
0.0212, 0.0818, 0.0238, 0.0553, -0.0124, 0.0651, -0.0550, 0.0988,
0.0944, 0.0165, -0.0174, 0.0324, -0.0791, 0.0048, -0.0797, 0.0416,
-0.0387, 0.0883, -0.0372, 0.0802, 0.0631, -0.0751, -0.0307, 0.0933,
0.0518, 0.0364, -0.0344, 0.0450, -0.0931, 0.0540, -0.0152, -0.0095,
-0.0012, -0.0997, 0.0772, -0.0586, -0.0623, -0.0367, -0.0064, 0.0835,
-0.0292, -0.0359, 0.0593, -0.0392, -0.0899, 0.0665, 0.0020, 0.0133,
0.0056, 0.0605, 0.0892, 0.0271, 0.0801, 0.0595, -0.0282, -0.0067,
0.0685, -0.0417, -0.0756, 0.0227, 0.0449, 0.0852, -0.0568, 0.0770,
-0.0828, 0.0612, 0.0121, -0.0957, 0.0353, -0.0523, -0.0345, 0.0652,
0.0510, 0.0827, 0.0203, 0.0707, 0.0430, -0.0859, -0.0670, 0.0558,
-0.0208, -0.0407, 0.0307, 0.0536, -0.0888, 0.0401, -0.0411, -0.0826,
-0.0107, 0.0685, -0.0369, -0.0942, 0.0556, 0.0349, -0.0037, -0.0430,
0.0368, -0.0654, 0.0337, -0.0398, 0.0590, 0.0062, 0.0720, 0.0362,
-0.0966, -0.0802, 0.0774, -0.0064, 0.0847, -0.0376, -0.0572, 0.0246,
-0.0215, 0.0795, -0.0788, 0.0258, 0.0096, 0.0698, 0.0558, 0.0120,
0.0156, -0.0307, -0.0586, 0.0917, 0.0010, -0.0518, 0.0185, -0.0388],
requires_grad=True)
Parameter containing:
tensor([[ 0.0454, 0.0397, 0.0131, ..., 0.0285, -0.0571, -0.0669],
[ 0.0431, -0.0305, -0.0683, ..., 0.0385, -0.0166, 0.0369],
[-0.0072, -0.0054, 0.0396, ..., 0.0572, 0.0202, 0.0327],
...,
[-0.0547, 0.0300, 0.0339, ..., -0.0336, -0.0609, -0.0679],
[ 0.0491, -0.0294, 0.0139, ..., -0.0624, -0.0104, -0.0122],
[-0.0389, -0.0282, 0.0544, ..., -0.0204, 0.0204, -0.0024]],
requires_grad=True)
Parameter containing:
tensor([ 0.0526, -0.0480, 0.0021, -0.0189, -0.0574, 0.0064, 0.0061, 0.0203,
0.0539, -0.0285], requires_grad=True)
Layer params:
Parameter containing:
tensor([[ 0.0454, 0.0397, 0.0131, ..., 0.0285, -0.0571, -0.0669],
[ 0.0431, -0.0305, -0.0683, ..., 0.0385, -0.0166, 0.0369],
[-0.0072, -0.0054, 0.0396, ..., 0.0572, 0.0202, 0.0327],
...,
[-0.0547, 0.0300, 0.0339, ..., -0.0336, -0.0609, -0.0679],
[ 0.0491, -0.0294, 0.0139, ..., -0.0624, -0.0104, -0.0122],
[-0.0389, -0.0282, 0.0544, ..., -0.0204, 0.0204, -0.0024]],
requires_grad=True)
Parameter containing:
tensor([ 0.0526, -0.0480, 0.0021, -0.0189, -0.0574, 0.0064, 0.0061, 0.0203,
0.0539, -0.0285], 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.9138, 0.7448, 0.7237]])
Weight and Bias parameters:
Parameter containing:
tensor([[-0.3847, -0.1010, 0.2433],
[-0.4489, 0.4438, -0.1772]], requires_grad=True)
Parameter containing:
tensor([ 0.2610, -0.2792], requires_grad=True)
Output:
tensor([[ 0.0103, -0.4872]], grad_fn=<AddmmBackward0>)
如果您将 x
与线性层的权重进行矩阵乘法,然后加上偏置项,您会发现您得到了输出向量 y
。
另外一个需要注意的重要特性是:当我们用 lin.weight
检查我们的层的权重时,它将自己报告为 Parameter
(它是 Tensor
的子类),并告知我们它正在通过 autograd 跟踪梯度。这是 Parameter
的默认行为,与 Tensor
不同。
线性层在深度学习模型中被广泛使用。您最常看到它们的地方之一是分类器模型,这些模型通常会在末尾有一个或多个线性层,最后一个层将有 n 个输出,其中 n 是分类器处理的类别数。
卷积层#
卷积层是为处理具有高度空间相关性的数据而构建的。它们在计算机视觉中非常常用,在那里它们可以检测特征的紧密分组,并将这些分组组合成更高级别的特征。它们也出现在其他上下文中——例如,在 NLP 应用中,一个词的即时上下文(即,序列中附近的其他词)会影响句子的含义。
我们在之前的视频中在 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 激活函数(稍后将详细介绍激活函数),然后通过最大池化层。最大池化层将激活图上彼此靠近的特征分组。它通过减小张量来实现这一点,将 6x14x14 输出中的每 2x2 单元格组合成一个单元格,并将该单元格分配给构成它的 4 个单元格中的最大值。这给了我们一个较低分辨率的激活图,尺寸为 6x14x14。
我们的下一个卷积层,conv2
,期望 6 个输入通道(对应于第一层寻找的 6 个特征),具有 16 个输出通道,和一个 3x3 的核。它输出一个 16x12x12 的激活图,该激活图再次通过最大池化层减小到 16x6x6。在将此输出传递给线性层之前,它被重塑为 16 * 6 * 6 = 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 网络 教程。
Transformer#
Transformer 是多用途网络,它们已通过 BERT 等模型在 NLP 领域占据主导地位。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.9985, 0.6072, 0.9054, 0.0122, 0.0316, 0.6101],
[0.8788, 0.8865, 0.3571, 0.9121, 0.1494, 0.9066],
[0.0532, 0.0904, 0.9366, 0.5915, 0.3682, 0.1288],
[0.3470, 0.8063, 0.9123, 0.9545, 0.5613, 0.6272],
[0.6433, 0.1906, 0.1796, 0.1235, 0.8213, 0.7162],
[0.2726, 0.7960, 0.4821, 0.9708, 0.9662, 0.2448]]])
tensor([[[0.9985, 0.9121],
[0.9123, 0.9708]]])
如果您仔细查看上面的值,您会发现 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())
tensor([[[19.9866, 17.6844, 13.1704, 22.0329],
[17.2141, 20.5732, 13.6163, 14.3268],
[ 7.9626, 21.5659, 9.6751, 24.4932],
[16.5678, 24.9935, 6.4362, 6.8885]]])
tensor(16.0742)
tensor([[[ 0.5365, -0.1621, -1.5318, 1.1574],
[ 0.2848, 1.5089, -1.0263, -0.7674],
[-1.1049, 0.7829, -0.8672, 1.1892],
[ 0.3714, 1.4708, -0.9506, -0.8916]]],
grad_fn=<NativeBatchNormBackward0>)
tensor(-1.1176e-07, 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.0000e+00, 2.2911e-01, 8.0319e-01, 0.0000e+00],
[1.2981e+00, 8.9656e-01, 1.3806e+00, 3.0707e-01],
[0.0000e+00, 1.8874e-01, 0.0000e+00, 2.9187e-01],
[1.5137e+00, 9.8765e-04, 1.0918e+00, 2.9889e-01]]])
tensor([[[0.1878, 0.2291, 0.8032, 1.4431],
[0.0000, 0.0000, 1.3806, 0.3071],
[0.0000, 0.1887, 0.8674, 0.0000],
[0.0000, 0.0000, 1.0918, 0.0000]]])
上面,您可以看到 dropout 对样本张量的影响。您可以使用可选的 p
参数来设置单个权重 dropout 的概率;如果您不设置,则默认为 0.5。
激活函数#
激活函数使深度学习成为可能。神经网络实际上是一个程序——带有很多参数——它模拟一个数学函数。如果我们只是反复用层权重乘以张量,我们只能模拟线性函数;此外,多层就没有意义了,因为整个网络可以被简化为单个矩阵乘法。在层之间插入非线性激活函数是允许深度学习模型模拟任何函数,而不仅仅是线性函数。
torch.nn.Module
包含封装所有主要激活函数的对象,包括 ReLU 及其许多变体、Tanh、Hardtanh、sigmoid 等等。它还包括其他函数,如 Softmax,这些函数在模型的输出阶段最有用。
损失函数#
损失函数告诉我们模型的预测与正确答案的差距有多大。PyTorch 包含各种损失函数,包括常见的 MSE(均方误差 = L2 范数)、交叉熵损失和负对数似然损失(对分类器有用)等。
脚本的总运行时间:(0 分钟 0.024 秒)