介绍 || 张量 || 自动求导 || 构建模型 || TensorBoard 支持 || 训练模型 || 模型理解
使用 PyTorch 构建模型
请跟随以下视频或访问 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() 方法进行访问。
作为一个简单的示例,这里有一个包含两个线性层和一个激活函数的非常简单的模型。我们将创建它的一个实例,并要求它报告其参数:
importtorch
classTinyModel(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()
defforward(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(它是 Tensor 的子类),并告诉我们它正在使用 autograd 跟踪梯度。这是 Parameter 的默认行为,与 Tensor 不同。
线性层在深度学习模型中广泛使用。您最常见到它们的地方之一是在分类器模型中,这些模型通常在末尾有一个或多个线性层,其中最后一层会有 n 个输出,n 是分类器处理的类别数量。
卷积层
卷积层专门用于处理具有高度空间相关性的数据。它们在计算机视觉中非常常见,用于检测紧密组合的特征,进而将其组合成更高层次的特征。它们也出现在其他场景中——例如,在自然语言处理(NLP)应用中,一个词的上下文(即序列中附近的其他词)可以影响句子的含义。
我们在之前的视频中看到卷积层在LeNet5中的应用:
importtorch.functionalasF
classLeNet(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)
defforward(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
defnum_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个元素的向量,供下一层使用。
卷积层可以处理1D、2D和3D张量。卷积层构造函数还有许多可选参数,包括步幅长度(例如,只扫描每隔一个或每隔两个位置)、填充(以便可以扫描到输入的边缘)等。更多信息请参阅文档。
循环层
循环神经网络(或RNNs)用于处理序列数据——从科学仪器的时间序列测量到自然语言句子,再到DNA核苷酸序列。RNN通过维护一个隐藏状态来实现这一点,该状态充当一种记忆,记录它到目前为止在序列中所看到的内容。
RNN层的内部结构——或其变体LSTM(长短期记忆)和GRU(门控循环单元)——相对复杂,超出了本视频的范围,但我们将通过一个基于LSTM的词性标注器(一种分类器,用于判断一个词是名词、动词等)来展示其实际应用:
classLSTMTagger(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)
defforward(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 内存的大小。
输入将是一个句子,其中的单词表示为 one-hot 向量的索引。嵌入层随后会将这些索引映射到一个 embedding_dim 维的空间中。LSTM 接收这个嵌入序列并对其进行迭代,生成一个长度为 hidden_dim 的输出向量。最后的线性层充当分类器;对最后一层的输出应用 log_softmax() 将输出转换为一组归一化的估计概率,表示给定单词映射到给定标签的概率。
如果您想了解这个网络的实际应用,请查看 pytorch.org 上的 序列模型和 LSTM 网络 教程。
Transformers
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.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输入中每个象限的最大值。
归一化层在将一层的输出传递到下一层之前,对其进行重新居中(re-center)和归一化(normalize)。对中间张量进行居中和缩放有许多好处,例如允许您使用更高的学习率而不会出现梯度爆炸或消失的问题。
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)。
这是有益的,因为许多激活函数(将在下面讨论)在接近零时具有最强的梯度,但有时对于使它们远离零的输入,可能会遇到梯度消失或爆炸的问题。将数据保持在梯度最陡峭的区域附近,通常意味着更快、更好的学习以及更高的可行学习率。
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.8869, 0.6595, 0.2098, 0.0000],
[0.5379, 0.0000, 0.0000, 0.0000],
[0.1950, 0.2424, 1.3319, 0.5738],
[0.5676, 0.8335, 0.0000, 0.2928]]])
tensor([[[0.8869, 0.6595, 0.2098, 0.2878],
[0.5379, 0.0000, 0.4029, 0.0000],
[0.0000, 0.2424, 1.3319, 0.5738],
[0.0000, 0.8335, 0.9647, 0.0000]]])
如上所示,您可以看到 dropout 对一个示例张量的影响。您可以使用可选的 p 参数来设置单个权重被丢弃的概率;如果不设置,默认值为 0.5。
激活函数
激活函数使得深度学习成为可能。神经网络本质上是一个程序——具有许多参数——它模拟了一个数学函数。如果我们所做的只是通过层权重重复地乘以张量,那么我们只能模拟线性函数;此外,拥有多个层也没有意义,因为整个网络可以被简化为一个单一的矩阵乘法。在层之间插入非线性激活函数,使得深度学习模型能够模拟任何函数,而不仅仅是线性函数。
torch.nn.Module 包含了封装所有主要激活函数的对象,包括 ReLU 及其多种变体、Tanh、Hardtanh、sigmoid 等。它还包含其他在模型输出阶段非常实用的函数,例如 Softmax。
损失函数
损失函数告诉我们模型的预测与正确答案之间的差距。PyTorch 提供了多种损失函数,包括常见的均方误差(MSE = L2 范数)、交叉熵损失和负对数似然损失(适用于分类器)等。