注意
点击 这里 下载完整示例代码
简介 || 张量 || 自动微分 || 构建模型 || 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基类。
One important behavior of torch.nn.Module 是注册参数。
如果某个特定的 Module 子类具有学习权重,这些权重则表示为 torch.nn.Parameter 的实例。Parameter 类是 torch.Tensor 的子类,具有特殊的行为,当它们被赋值为 Module 的属性时,会被添加到该模块的参数列表中。这些参数可以通过 parameters() 方法在 Module 类上访问。
作为一个简单的例子,这里有一个非常简单的模型,包含两个线性层和一个激活函数。我们将创建该模型的一个实例,并询问其参数情况。
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层(这是Tensor的一个子类),并告诉我们它正在使用autograd跟踪梯度。这是Parameter的默认行为,与Tensor不同。
线性层在深度学习模型中被广泛使用。你最常看到它们的地方是在分类器模型中,这些模型通常会在结尾处有一个或多个线性层,其中最后一层会有 n 个输出,n 是分类器需要处理的类别数量。
卷积层¶
卷积层用于处理具有高度空间相关性的数据。它们在计算机视觉中非常常用,可以检测紧密组合的特征并将其组成更高层次的特征。它们在其他上下文中也会出现——例如,在自然语言处理应用中,一个词的即时上下文(即序列中附近的其他词)可以影响句子的意义。
我们在之前的视频中看到了卷积层在LeNet-5中的应用:
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个元素的向量,供下一层使用。
有用于处理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维空间中是一个 one-hot 向量(或单位向量)。tagset_size是输出集中标签的数量。embedding_dim是词汇表的 嵌入 空间的大小。嵌入将词汇表映射到低维空间,在该空间中,具有相似含义的词彼此接近。hidden_dim是LSTM记忆体的大小。
输入将是一个句子,其中的单词表示为独热向量的索引。嵌入层随后会将这些映射到
embedding_dim维空间。LSTM 接受这个嵌入序列并对其进行迭代,生成长度为
hidden_dim的输出向量。最终的线性层充当分类器;对最后一层的输出应用
log_softmax() 将输出转换为归一化的估计概率集,表明给定单词映射到给定标签的可能性。
如果你想看看这个网络的实际效果,请查看 pytorch.org 上的 序列模型和 LSTM 网络 教程。
变换器¶
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输入中每个象限的最大值。
归一化层 在将一层的输出输入到另一层之前,重新居中并对其进行归一化。居中和缩放中间张量有许多有益的效果,例如让你可以使用更高的学习率而不会出现梯度爆炸/消失。
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.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 范数)、交叉熵损失和负对数似然损失(对分类器有用)等。
脚本的总运行时间: ( 0 分钟 0.032 秒)