Skip to content

使用 PyTorch 构建模型

译者:Fadegentle

项目地址:https://pytorch.apachecn.org/2.0/tutorials/beginner/introyt/modelsyt_tutorial

原始地址:https://pytorch.org/tutorials/beginner/introyt/modelsyt_tutorial.html

请跟随下面的视频或在 youtube 上观看。

torch.nn.Moduletorch.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 的子类),并让我们知道它正在使用自动微分跟踪梯度。与 Tensor 不同,这是 Parameter 的默认行为。

线性层广泛应用于深度学习模型中。在分类器模型中最常见到的就是线性层,分类器模型的最后通常会有一个或多个线性层,最后一层会有 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 的卷积内核)。

卷积层的输出是 激活图,即输入tensor中存在特征的空间表示。conv1 将为我们提供一个 6x28x28 的输出tensor,6 是特征的数量,28 是图的宽高。(28 是指在 32 像素行上扫描 5 像素窗口时,只有 28 个有效位置)。

然后,我们将卷积输出通过 ReLU 激活函数(激活函数稍后详述),再通过最大池化层。最大池化层将激活图中相互靠近的特征集中在一起。具体做法是缩小tensor,将输出中的每组 2x2 单元合并为一个单元,并为该单元分配 4 个单元中的最大值。这样,我们就得到了激活图的低分辨率版本,尺寸为 6x14x14。

下一个卷积层 conv2 需要 6 个输入通道(与第一层的 6 个特征相对应),16 个输出通道和一个 3x3 内核。它输出一个 16x12x12 的激活图,通过最大池化层再次将激活图缩小为 16x6x6。在将输出传递给线性层之前,它被重塑为一个 16 * 6 * 6 = 576 元素的向量,供下一层使用。

卷积层可用于处理一维、二维和三维tensor。卷积层构造函数还有更多可选参数,包括输入中的跨距长度(例如,每次只扫描第二或第三个位置)、填充(以便扫描到输入的边缘)等。更多信息请参阅文档

循环层

RNN(Recurrent neural network,递归神经网络) 用于处理序列数据——从科学仪器的时间序列测量到自然语言句子再到 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_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 是词汇 嵌入(embedding) 空间的大小。嵌入空间将词汇映射到一个低维空间中,在这个空间中,词义相近的词会靠得很近。
  • hidden_dim 是 LSTM 内存的大小。

输入将是一个句子,其中的单词表示为独热(one-hot)向量的索引。然后,嵌入层会将这些词映射到一个 embedding_dim 维空间。LSTM 获取嵌入序列并对其进行迭代,得到长度为 hidden_dim 的输出向量。最后的线性层充当分类器,对最后一层的输出应用 log_softmax(),可将输出转换为一组归一化的估计概率,表示指定单词映射到指定标签的概率。

如果您想看看这个网络的实际应用,请查看 pytorch.org 上的序列模型和 LSTM 网络教程

变换器

变换器(Transformer) 是一种多用途网络,它与 BERT 等模型一起,称霸了 NLP 技术领域。讨论变换器架构超出了本视频的范围,但 PyTorch 有一个 Transformer 类,可以定义变换器模型的整体参数——注意力头的数量、编码器和解码器层的数量、随机失活(dropout)和激活函数等(甚至可以通过正确的参数用这个类来构建 BERT 模型!)。torch.nn.Transformer 类还封装了各个组件(TransformerEncoderTransformerDecoder)和子组件(TransformerEncoderLayerTransformerDecoderLayer)的类。有关详细信息,请查阅有关变换器类的文档以及 pytorch.org 上的相关教程

其他层和函数

数据操作层

还有其他一些类型的层在模型中起重要作用,但它们本身并不参与学习过程。

最大池化(和它兄弟,最小池化)通过合并单元来减少tensor,并将输入单元的最大值分配给输出单元(我们看到过这个)。例如

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 输入的各象限最大值。

归一化层会将一层的输出重新居中并归一化,然后再输入到另一层。将中间tensor居中和缩放有很多好处,比如可以让您使用更高的学习率,而不会导致梯度爆炸/消失。

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>)

运行上面的单元格时,我们为输入tensor添加了一个较大的缩放因子和偏移量,您应该看到输入tensor的 mean() 在 15 左右。在通过归一化层运行后,您可以看到数值变小了,并聚集在零附近——事实上,平均值应该非常小(> 1e-8)。

这样做是有好处的,因为许多激活函数(下面会讨论)在 0 附近的梯度最大,但有时把输入推远离 0 会出现梯度消失或爆炸。将数据保持在梯度最陡峭区域的中心,往往意味着更快更好的学习和更高的可行学习率。

随机失活(Dropout)层是模型中一种鼓励 稀疏表示 的工具,即让模型用更少的数据进行推理。

随机失活层的工作原理是在 训练过程中 随机设置输入tensor的一部分——随机失活层在推理时始终处于关闭状态。这就迫使模型根据这种被屏蔽或减少的数据集进行学习。例如:

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]]])

从上面,您可以看到样本tensor应用随机失活的效果。您可以使用可选的 p 参数设置单个权重失活的概率,如果不使用,默认值为 0.5。

激活函数

激活函数使得深度学习成为可能。神经网络实际上是一个带有许多参数的程序,用于 模拟数学函数。如果我们只是反复将tensor乘以层权重,那么我们只能模拟 线性函数,另外没有必要使用多层,因为整个网络可以简化为一次矩阵乘法。在层之间插入非线性激活函数允许深度学习模型模拟任何函数,而不仅仅是线性函数。

torch.nn.Module 中包含封装了所有主要激活函数的对象,包括 ReLU 及其许多变种、Tanh、Hardtanh、sigmoid 等。它还包括其他在模型输出阶段最有用的函数,例如 Softmax。

损失函数

损失函数告诉我们一个模型的预测离正确答案有多远。PyTorch 包含多种损失函数,包括常见的 MSE(均方误差 = L2 范数)、交叉熵损失和负对数似然损失(适用于分类器)等。



回到顶部