注意
点击 这里 下载完整的示例代码
介绍 || 张量 || 自动求导 || 构建模型 || 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模型及其组件相关的特定行为。
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>)
如果你对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激活函数(稍后会详细介绍激活函数),然后通过一个最大池化层。最大池化层将激活图中彼此接近的特征分组在一起。它通过减少张量来实现这一点,将输出中的每个2x2单元格组合并为一个单元格,并将该单元格分配为进入其中的4个单元格的最大值。这为我们提供了一个较低分辨率的激活图版本,尺寸为6x14x14。
我们的下一个卷积层,conv2
,期望有6个输入通道
(对应于第一层寻找的6个特征),有16个
输出通道,和一个3x3的核。它输出一个16x12x12的激活
图,再次通过最大池化层减少到16x6x6。在
将此输出传递给线性层之前,它被重塑为一个16 * 6 *
6 = 576元素的向量,供下一层使用。
有用于处理1D、2D和3D张量的卷积层。卷积层构造函数还有许多可选参数,包括输入中的步长(例如,仅扫描每第二个或每第三个位置)、填充(以便您可以扫描到输入的边缘)等。有关更多信息,请参阅文档。
循环层
循环神经网络(或RNNs)用于处理序列数据——从科学仪器的时间序列测量到自然语言句子,再到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()
将输出转换为一个归一化的估计概率集,表示给定单词映射到给定标签的概率。
如果你想看到这个网络的运行情况,请查看Sequence Models and LSTM Networks教程,该教程位于pytorch.org上。
变压器
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]]])
如果你仔细观察上面的值,你会发现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([[[ 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
参数来设置单个权重被dropout的概率;如果你不设置,它默认为0.5。
激活函数
激活函数使得深度学习成为可能。神经网络实际上是一个程序——具有许多参数——它模拟了一个数学函数。如果我们所做的只是通过层权重重复乘以张量,我们只能模拟线性函数;此外,拥有许多层也没有意义,因为整个网络可以简化为一个单一的矩阵乘法。在层之间插入非线性激活函数是使深度学习模型能够模拟任何函数,而不仅仅是线性函数的原因。
torch.nn.Module
包含了封装所有主要激活函数的对象,包括ReLU及其多种变体、Tanh、Hardtanh、sigmoid等。它还包括其他函数,如Softmax,这些函数在模型的输出阶段非常有用。