备注
点击 此处 下载完整示例代码
简介 || 张量 || Autograd || 构建模型 || TensorBoard支持 || 训练模型 || 模型理解
使用PyTorch构建模型¶
Created On: Nov 30, 2021 | Last Updated: Oct 15, 2024 | Last Verified: Nov 05, 2024
按照以下视频或在`YouTube <https://www.youtube.com/watch?v=OSqIP-mOWOI>`__上学习。
torch.nn.Module
和 torch.nn.Parameter
¶
在这段视频中,我们将讨论PyTorch提供的一些用于构建深度学习网络的工具。
除了``Parameter``外,在视频中讨论的类都是``torch.nn.Module``的子类。这是一个PyTorch基类,用于封装PyTorch模型及其组件的特定行为。
``torch.nn.Module``的重要行为之一是注册参数。如果一个特定``Module``子类有学习权重,这些权重将以``torch.nn.Parameter``实例的形式表达。``Parameter``类是``torch.Tensor``的子类,具有特殊行为:当它们被分配为``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.0754, -0.0260, -0.0680, ..., 0.0112, 0.0585, 0.0605],
[ 0.0465, -0.0836, -0.0761, ..., -0.0265, 0.0487, -0.0196],
[-0.0470, 0.0887, 0.0489, ..., -0.0659, -0.0635, 0.0243],
...,
[-0.0940, -0.0101, 0.0879, ..., -0.0223, 0.0540, -0.0323],
[-0.0004, 0.0558, 0.0434, ..., 0.0686, 0.0249, 0.0857],
[-0.0143, -0.0903, 0.0267, ..., 0.0203, 0.0928, -0.0193]],
requires_grad=True)
Parameter containing:
tensor([ 0.0034, -0.0795, 0.0841, -0.0040, -0.0421, 0.0144, 0.0526, 0.0692,
0.0579, 0.0194, 0.0359, -0.0268, -0.0277, -0.0990, 0.0293, 0.0783,
-0.0060, -0.0792, -0.0398, 0.0847, -0.0031, 0.0052, 0.0107, -0.0024,
-0.0235, -0.0219, 0.0832, 0.0355, 0.0602, -0.0553, -0.0055, -0.0333,
0.0673, -0.0866, -0.0785, 0.0697, 0.0520, 0.0678, -0.0426, -0.0207,
-0.0155, 0.0215, 0.0423, 0.0467, -0.0113, 0.0809, 0.0439, 0.0523,
-0.0758, -0.0708, -0.0289, 0.0880, -0.0172, -0.0770, 0.0336, -0.0162,
-0.0131, -0.0231, -0.0891, 0.0176, 0.0735, -0.0410, -0.0916, 0.0024,
0.0166, -0.0619, -0.0997, -0.0403, -0.0609, -0.0532, 0.0707, -0.0546,
0.0619, 0.0774, 0.0814, 0.0956, -0.0826, -0.0731, -0.0359, 0.0088,
-0.0292, 0.0952, 0.0737, -0.0263, -0.0239, 0.0401, -0.0811, -0.0135,
0.0820, 0.0442, 0.0363, -0.0820, -0.0102, 0.0630, -0.0101, 0.0515,
-0.0111, -0.0303, 0.0944, 0.0845, -0.0947, -0.0334, 0.0626, -0.0438,
0.0944, 0.0889, -0.0445, 0.0051, -0.0897, -0.0526, 0.0943, 0.0533,
-0.0448, -0.0502, -0.0767, -0.0349, 0.0894, 0.0640, -0.0331, 0.0720,
-0.0693, 0.0908, 0.0159, 0.0909, 0.0091, 0.0384, -0.0241, 0.0742,
-0.0511, 0.0092, -0.0477, 0.0139, 0.0103, -0.0334, -0.0405, 0.0648,
0.0796, -0.0451, 0.0266, -0.0016, -0.0747, -0.0822, -0.0890, -0.0067,
0.0182, -0.0705, 0.0386, -0.0670, -0.0664, -0.0656, -0.0885, -0.0706,
-0.0416, -0.0066, 0.0132, -0.0477, -0.0310, 0.0592, 0.0436, -0.0008,
-0.0487, -0.0904, 0.0722, -0.0199, -0.0222, 0.0326, 0.0610, 0.0947,
-0.0904, -0.0902, 0.0333, -0.0198, -0.0070, 0.0456, 0.0989, 0.0290,
0.0473, -0.0389, -0.0374, -0.0148, -0.0970, 0.0059, -0.0468, -0.0795,
-0.0287, 0.0425, -0.0120, 0.0445, 0.0793, -0.0949, 0.0508, -0.0737,
0.0319, 0.0610, 0.0762, 0.0533, -0.0183, -0.0955, 0.0204, 0.0579],
requires_grad=True)
Parameter containing:
tensor([[ 0.0528, 0.0093, -0.0454, ..., 0.0501, 0.0140, -0.0076],
[ 0.0107, 0.0555, -0.0466, ..., -0.0263, 0.0295, 0.0478],
[-0.0157, -0.0654, -0.0168, ..., -0.0163, 0.0209, 0.0033],
...,
[ 0.0012, -0.0508, 0.0189, ..., 0.0121, -0.0413, -0.0690],
[-0.0304, 0.0075, 0.0027, ..., -0.0158, 0.0483, -0.0156],
[ 0.0486, 0.0021, -0.0505, ..., 0.0594, 0.0689, 0.0534]],
requires_grad=True)
Parameter containing:
tensor([ 0.0404, 0.0616, -0.0645, -0.0092, 0.0600, -0.0520, 0.0380, 0.0333,
-0.0300, -0.0060], requires_grad=True)
Layer params:
Parameter containing:
tensor([[ 0.0528, 0.0093, -0.0454, ..., 0.0501, 0.0140, -0.0076],
[ 0.0107, 0.0555, -0.0466, ..., -0.0263, 0.0295, 0.0478],
[-0.0157, -0.0654, -0.0168, ..., -0.0163, 0.0209, 0.0033],
...,
[ 0.0012, -0.0508, 0.0189, ..., 0.0121, -0.0413, -0.0690],
[-0.0304, 0.0075, 0.0027, ..., -0.0158, 0.0483, -0.0156],
[ 0.0486, 0.0021, -0.0505, ..., 0.0594, 0.0689, 0.0534]],
requires_grad=True)
Parameter containing:
tensor([ 0.0404, 0.0616, -0.0645, -0.0092, 0.0600, -0.0520, 0.0380, 0.0333,
-0.0300, -0.0060], 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.8282, 0.6515, 0.7522]])
Weight and Bias parameters:
Parameter containing:
tensor([[ 0.2365, 0.2305, -0.5484],
[ 0.5066, -0.4887, -0.0207]], requires_grad=True)
Parameter containing:
tensor([-0.1213, 0.4951], requires_grad=True)
Output:
tensor([[-0.1877, 0.5807]], grad_fn=<AddmmBackward0>)
如果你对``x``与线性层的权重执行矩阵乘法,并加上偏置,你会发现得到的是输出向量``y``。
另一个需要注意的重要特性是:当我们通过``lin.weight``检查层的权重时,它将其自身报告为一个``Parameter``(这是``Tensor``的子类),并告诉我们它正在使用自动微分跟踪梯度。这是``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张量都有卷积层可用。构造函数还有许多可选参数,包括步幅(例如,仅每隔第二个或每第三个位置扫描输入)、填充(使你可以扫描到输入的边缘),等等。有关更多信息,请参阅`文档 <https://pytorch.org/docs/stable/nn.html#convolution-layers>`__。
循环层¶
*循环神经网络(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网络教程 <https://pytorch.org/tutorials/beginner/nlp/sequence_models_tutorial.html>`__。
Transformers(变换器)¶
*Transformers*是多用途网络,随着像BERT这样的模型,它们已成为NLP领域最新的技术标准。关于变换器架构的讨论超出了此视频范围,但PyTorch有一个``Transformer``类,可以定义变换器模型的总体参数——包括注意力头的数量、编码器和解码器层数、dropout和激活函数等。(使用正确的参数,甚至可以从此单分类构建BERT模型!)``torch.nn.Transformer``类还封装了子组件(如``TransformerEncoder``、TransformerDecoder``以及``TransformerEncoderLayer
、TransformerDecoderLayer
)。详情请参阅变换器类的`文档 <https://pytorch.org/docs/stable/nn.html#transformer-layers>`__。
其他层和函数¶
数据操作层¶
还有一些层类型执行模型中的重要功能,但它们本身不参与学习过程。
**最大池化**(以及它的孪生兄弟,最小池化)通过组合输入单元格来减少张量,并将输入单元格的最大值赋给输出单元格(我们已经看到过这一点)。例如:
my_tensor = torch.rand(1, 6, 6)
print(my_tensor)
maxpool_layer = torch.nn.MaxPool2d(3)
print(maxpool_layer(my_tensor))
tensor([[[0.4246, 0.9950, 0.1673, 0.8358, 0.0582, 0.1874],
[0.8761, 0.1085, 0.9898, 0.5233, 0.6777, 0.7067],
[0.7278, 0.0982, 0.0856, 0.7291, 0.3509, 0.6735],
[0.6529, 0.7919, 0.8538, 0.9308, 0.5194, 0.5882],
[0.5002, 0.4345, 0.9487, 0.1001, 0.7517, 0.5321],
[0.2296, 0.2472, 0.4831, 0.3386, 0.3483, 0.5019]]])
tensor([[[0.9950, 0.8358],
[0.9487, 0.9308]]])
仔细观察上述的数值,你会发现max pooled结果中的每个值都是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([[[ 5.8610, 23.8407, 21.0855, 15.3688],
[ 6.7692, 10.0197, 16.6761, 13.7145],
[17.3777, 23.0578, 17.8931, 7.3597],
[11.1264, 20.6643, 20.9893, 13.8442]]])
tensor(15.3530)
tensor([[[-1.5519, 1.0612, 0.6608, -0.1701],
[-1.3441, -0.4748, 1.3055, 0.5134],
[ 0.1681, 1.1674, 0.2588, -1.5943],
[-1.2915, 0.9362, 1.0121, -0.6567]]],
grad_fn=<NativeBatchNormBackward0>)
tensor(-5.9605e-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.0000, 0.0000, 0.5726, 0.0000],
[1.5877, 0.4433, 1.2624, 0.7835],
[0.0000, 1.5222, 1.3698, 0.2493],
[0.9970, 0.0000, 1.3737, 0.0000]]])
tensor([[[0.0000, 0.0861, 0.5726, 1.6432],
[0.0000, 0.0000, 1.2624, 0.7835],
[0.0000, 1.5222, 1.3698, 0.2493],
[0.9970, 0.0000, 1.3737, 0.4865]]])
上面展示了dropout对样本张量的影响。你可以使用可选的``p``参数设置单个权重丢弃的概率;如果不设置,它默认为0.5。
激活函数¶
激活函数使深度学习成为可能。神经网络实际上是一个程序——含有许多参数——其目的是*模拟数学函数*。如果我们只是重复通过层权重乘以张量,我们只能模拟*线性函数*;此外,拥有多个层也没有意义,因为整个网络可以简化为单个矩阵乘法。通过在层之间插入*非线性*激活函数,这使得深度学习模型可以模拟任何函数,而不仅仅是线性函数。
``torch.nn.Module``提供了封装所有主要激活函数的对象,包括ReLU及其许多变体、Tanh、Hardtanh、sigmoid以及更多函数。它还包括其他在模型输出阶段最有用的函数,比如Softmax。
损失函数¶
损失函数表明模型的预测与正确答案之间的偏差程度。PyTorch提供了多种损失函数,包括常用的MSE(均方误差 = L2范数)、交叉熵损失和负似然损失(对于分类器非常有用)以及其他函数。