深度学习框架PyTorch——从入门到精通(YouTube系列 - 4)——使用PyTorch构建模型
这部分是 PyTorch介绍——YouTube系列的内容,每一节都对应一个youtube视频。(可能跟之前的有一定的重复)
- torch.nn.Module(PyTorch神经网络模块)和torch.nn.Parameter(PyTorch神经网络参数)
- 常见的层类型
- 线性层
- 卷积层
- 循环层
- Transformer(变换器)
- 其他层和函数
- 数据处理层
- 最大池化(Max pooling)
- 归一层(Normalization layers)
- 随机失活层(Dropout)
- 激活函数
- 损失函数
本节YouTube视频地址:点击这里
torch.nn.Module(PyTorch神经网络模块)和torch.nn.Parameter(PyTorch神经网络参数)
在上面的YouTube视频中,我们将讨论一些PyTorch提供的用于构建深度学习网络的工具。
除了Parameter
之外,我们在本视频中讨论的类都是torch.nn.Module
的子类。torch.nn.Module
是PyTorch的基类,旨在封装特定于PyTorch模型及其组件的行为。
torch.nn.Module
的一个重要功能或者说用处(原文直译是重要行为
)是注册参数。如果某个特定的Module
子类具有可学习的权重,这些权重会表示为torch.nn.Parameter
的实例。Parameter
类是Tensor
(张量)类的子类,它具有特殊的行为:当它们被赋值为一个Module
的属性时,就会被添加到该Module
的参数列表中。这些参数可以通过Module
类的parameters()
方法来访问。
举个简单的例子,这里有一个非常简单的模型,它包含两个线性层和一个激活函数。我们将创建该模型的一个实例,并让它报告其参数:
import torchclass 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 xtinymodel = 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×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
(Parameter
是Tensor
的子类),并且告知我们它正在通过自动求导机制(autograd)跟踪梯度。这是Parameter
不同于Tensor
的默认行为。
线性层在深度学习模型中应用广泛。最常见的应用场景之一是分类器模型,这类模型通常在末尾会有一个或多个线性层,其中最后一层会有n个输出,这里的n是分类器所处理的类别数量。
卷积层
卷积层是为处理具有高度空间相关性的数据而设计的。它们在计算机视觉领域应用极为普遍,在该领域中,卷积层会检测特征的紧密组合,并将其组合成更高级别的特征。它们也会出现在其他场景中,例如在自然语言处理应用里,一个单词的紧邻上下文(即序列中相邻的其他单词)会影响句子的含义。
在之前的视频中,我们在LeNet5模型里见识过卷积层的实际应用:
import torch.functional as Fclass LeNet(torch.nn.Module):def __init__(self):super(LeNet, self).__init__()# 1 input image channel (black & white), 6 output channels, 5x5 square convolution# kernelself.conv1 = torch.nn.Conv2d(1, 6, 5)self.conv2 = torch.nn.Conv2d(6, 16, 3)# an affine operation: y = Wx + bself.fc1 = torch.nn.Linear(16 * 6 * 6, 120) # 6*6 from image dimensionself.fc2 = torch.nn.Linear(120, 84)self.fc3 = torch.nn.Linear(84, 10)def forward(self, x):# Max pooling over a (2, 2) windowx = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))# If the size is a square you can only specify a single numberx = 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 xdef num_flat_features(self, x):size = x.size()[1:] # all dimensions except the batch dimensionnum_features = 1for s in size:num_features *= sreturn 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
个元素的向量,以便下一层使用。
存在用于处理一维、二维和三维张量的卷积层。而且卷积层构造函数还有许多更多的可选参数,包括步长(例如,在输入中只每隔一个位置或每隔两个位置进行扫描)、填充(这样你就可以扫描到输入的边缘)等等。更多信息请查看相关文档。
循环层
循环神经网络(简称RNN)用于处理序列数据,这些数据涵盖范围广泛,从科学仪器记录的时间序列测量数据,到自然语言句子,再到DNA核苷酸序列等。RNN通过维护一个隐藏状态来实现对序列数据的处理,这个隐藏状态就像是一种记忆,存储着到目前为止它在序列中所“看到”的信息。
RNN层的内部结构,或者其变体,如长短期记忆网络(LSTM,即Long Short-Term Memory)和门控循环单元(GRU,即Gated Recurrent Unit),结构相对复杂,超出了本视频的讨论范围。不过,我们将通过一个基于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_dimself.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 spaceself.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网络”教程。
Transformer(变换器)
Transformer 是一种多用途的网络架构,像 BERT 这样的模型凭借它在自然语言处理(NLP)领域占据了领先地位。
关于 Transformer 架构的讨论超出了本视频的范畴,不过 PyTorch 提供了一个 Transformer
类,借助这个类你能够定义一个 Transformer 模型的整体参数,比如注意力头的数量、编码器和解码器的层数、 dropout(随机失活)参数以及激活函数等等。(只要参数设置得当,你甚至可以仅通过这一个类构建出 BERT 模型!)
torch.nn.Transformer
类还包含了用于封装各个独立组件的类(TransformerEncoder
编码器类、TransformerDecoder
解码器类)以及子组件类(TransformerEncoderLayer
编码器层类、TransformerDecoderLayer
解码器层类)。想要了解详细信息,可以查阅关于 Transformer 类的文档。
其他层和函数
数据处理层
在模型中还有其他类型的层,它们执行着重要的功能,但自身并不参与学习过程。
最大池化(Max pooling)
最大池化(以及与之相对的最小池化)通过合并单元格并将输入单元格中的最大值赋给输出单元格来对张量进行降维处理(我们之前已经见识过了)。例如:
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 输入中每个象限的最大值。
归一层(Normalization layers)
归一化层会在将一层的输出传递到下一层之前,对该输出重新进行中心化处理并归一化。对中间张量进行中心化和缩放有许多好处,例如,它能让你在使用较高学习率的情况下,避免梯度爆炸或梯度消失的问题。
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]]])
运行上面的代码单元,我们给一个输入张量添加了一个较大的缩放因子和偏移量;你会发现输入张量的均值(mean()
)大概在 15 左右。在将其通过归一化层后,你可以看到数值变小了,并且集中在零附近——实际上,均值应该非常小(大于 1 × 1 0 − 8 1\times10^{-8} 1×10−8)。
这是很有益处的,因为许多激活函数(下面会讨论)在接近 0 的地方具有最强的梯度,但对于那些使输入值远离 0 的情况,有时会出现梯度消失或梯度爆炸的问题。让数据集中在梯度最陡的区域附近,往往意味着可以实现更快、更好的学习效果,并且能够使用更高的可行学习率。
随机失活层(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.0000, 0.2878],[0.0000, 0.6824, 0.0000, 0.5920],[0.0000, 0.0000, 1.3319, 0.5738],[0.5676, 0.8335, 0.9647, 0.2928]]])
tensor([[[0.0000, 0.0000, 0.2098, 0.0000],[0.0000, 0.6824, 0.0000, 0.0000],[0.0000, 0.0000, 0.0000, 0.5738],[0.0000, 0.8335, 0.0000, 0.2928]]])
在上面的内容里,你可以看到随机失活对一个样本张量产生的影响。你可以使用可选参数p
来设置单个权重被随机置零的概率;若不设置,该概率默认是 0.5。
激活函数
激活函数让深度学习成为可能。神经网络本质上是一个拥有众多参数的程序,它模拟的是一个数学函数。要是我们仅仅反复用各层的权重去乘张量,那么就只能模拟线性函数;而且,设置多个层也毫无意义,因为整个网络最终会简化为一次矩阵乘法。而在各层之间插入非线性激活函数,能让深度学习模型模拟任意函数,而非仅仅是线性函数。
torch.nn.Module
封装了所有主要的激活函数,其中有 ReLU 及其众多变体、Tanh、Hardtanh、Sigmoid 等等。它还包含了其他函数,像 Softmax 这类在模型输出阶段极为有用的函数。
损失函数
损失函数能告诉我们模型的预测结果与正确答案之间的偏差程度。PyTorch 涵盖了各种各样的损失函数,常见的有均方误差(MSE,即 Mean Squared Error,也就是 L2 范数)、交叉熵损失以及负对数似然损失(在分类器中很有用)等。