介绍 || 张量 || 自动求导 || 构建模型 || 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 范数)、交叉熵损失和负对数似然损失(适用于分类器)等。