注意
点击 这里 下载完整的示例代码
介绍 || 张量 || 自动微分 || 构建模型 || 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()
方法访问。
举个简单的例子,这里有一个非常简单的模型,包含两个线性层和一个激活函数。我们将创建一个实例并让它报告其参数。
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>)
如果你对线性层的权重进行矩阵乘法,并加上偏置,你就会发现得到了输出向量 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。
卷积层就像一个窗口,它扫描图像,寻找它识别的模式。这些模式被称为特征,卷积层的一个参数是我们希望它学习的特征数量。这是构造函数的第二个参数,它表示输出特征的数量。在这里,我们要求我们的层学习 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 张量的卷积层。卷积层构造函数还有许多其他可选参数,包括输入中的步长长度(例如,只扫描每个第二个或第三个位置)、填充(以便你可以扫描到输入的边缘)等等。有关更多信息,请查看文档。
循环层¶
循环神经网络(或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 网络教程。
Transformers¶
Transformers 是多用途网络,它们凭借 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 输入中每个象限的最大值。
规范化层在将一层输出馈送到另一层之前,重新居中并规范化该层的输出。对中间张量进行居中和缩放具有许多有益效果,例如让你能够使用更高的学习率而不出现梯度爆炸/消失。
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 的地方具有最强的梯度,但有时会遇到输入将它们拉离零很远而导致的梯度消失或梯度爆炸问题。保持数据集中在最陡梯度区域附近通常意味着更快的、更好的学习以及更高的可行学习率。
丢弃层 是一种鼓励模型中稀疏表示 的工具,即推动模型使用更少的数据进行推断。
丢弃层通过在训练期间随机设置输入张量的一部分来工作 - 丢弃层在推断时始终关闭。这迫使模型针对此掩盖或减少的数据集进行学习。例如
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]]])
在上面,您可以看到丢弃对样本张量的影响。您可以使用可选的 p
参数来设置单个权重丢弃的概率;如果您不设置,它将默认为 0.5。
激活函数¶
激活函数使深度学习成为可能。神经网络实际上是一个具有许多参数的程序,它模拟数学函数。如果我们只反复地将张量乘以层权重,那么我们只能模拟线性函数;此外,拥有许多层就没有意义,因为整个网络可以简化为单个矩阵乘法。在层之间插入非线性 激活函数是允许深度学习模型模拟任何函数(而不仅仅是线性函数)的原因。
torch.nn.Module
包含封装所有主要激活函数(包括 ReLU 及其许多变体、Tanh、Hardtanh、sigmoid 等)的对象。它还包括其他函数,例如 Softmax,这些函数在模型的输出阶段最有用。
损失函数¶
损失函数告诉我们模型的预测与正确答案的距离有多远。PyTorch 包含各种损失函数,包括常见的 MSE(均方误差 = L2 范数)、交叉熵损失和负似然损失(对分类器有用)等。
脚本的总运行时间:(0 分钟 0.031 秒)