第三章 神经网络基础

本章通过介绍构建神经网络所涉及的基本思想,如激活函数,损失函数,优化器和监督训练设置,为后面的章节打基础。我们从一个简单的神经网络Perceptron开始,将各种概念联系在一起。Perceptron本身是更复杂的神经网络中的基本模块。这是一个常见的模式,将在整本书中重复出现 – 我们讨论的每个架构或网络可以单独使用,也可以在其他复杂网络中使用。

1 感知器:最简单的神经网络

感知器在神经网络发展历史上占据特殊位置,他是第一个从算法上完整描述的神经网络,它的发明者Rosenblatt是一位心理学者,在20世纪60年代和70年代,受感知器的启发工程师和物理学家以及数学家纷纷投身神经网络各个不同的方面研究。感知器在今天依然有效。
最简单的神经网络单元是Perceptron。在生物神经元之后,感知器在历史上很有名,与生物神经元一样,有输入和输出,“信号”从输入流向输出,如图4-1所示。

《第三章 神经网络基础》 Perceptron

每个感知器单元具有输入(x),输出(y)和三个“旋钮”:一组权重(w),偏置(b)和激活函数(f)。从数据中学习权重和偏差,并根据网络设计者对网络及其目标输出的直觉来手工挑选激活功能。在数学上,我们可以表达如下:
y = f(wx + b)
通常情况下,Perceptron有多个输入。我们可以使用向量来表示这种一般情况; 也就是说,x和w是向量,w和x的乘积用点积替换:
y = f(wx + b)
这里用f表示的激活函数通常是非线性函数。给出了PyTorch中的Perceptron实现,它采用任意数量的输入,仿射变换,应用激活函数,并产生单个输出。
Example 3-1. Perceptron的pytorch实现

# Example 3-1. Perceptron PyTorch表示
import torch
import torch.nn as nn

class Perceptron(nn.Module):
    """ 感知器是一个线性模型 """
    def __init__(self, input_dim):
        """
        Args:
            input_dim (int): 输入特征的尺寸
        """
        super(Perceptron, self).__init__()
        self.fc1 = nn.Linear(input_dim, 1)
       
    def forward(self, x_in):
        """感知器前向传递函数"""
        
        Args:
             x_in (torch.Tensor): 输入数据tensor. 
             x_in.shape 应该是 (batch, num_features)
        Returns:
            结果的 tensor. tensor.shape 应该是 (batch,)
        """
        return torch.sigmoid(self.fc1(x_in)).squeeze()

线性运算wx + b称为仿射变换。PyTorch方便地使用在torch.nn模块中提供了一个类Linear(),它可以进行权重和偏差计算,并进行所需的仿射变换。在“深入监督训练”中,您将了解如何从数据中“学习”权重wb的值。这个例子中使用的激活函数是sigmoid函数。在下一节中,我们将回顾一些常见的激活函数。

2 激活函数

激活函数是在人工神经网络的神经元上运行的函数,负责将神经元的输入映射到输出端。是一个重要的非线性函数,负责提高神经网络的非线性因素,增加深度网络解决问题的能力。“激活函数”按照其可导的性能分成两类——“饱和激活函数”和“非饱和激活函数”。

《第三章 神经网络基础》 激活函数的分类

2.1 Sigmoid

神经网络历史中最早使用的激活函数之一。它输入可以是任意实数,把它压缩在0和1之间。sigmoid是“饱和激活函数”,在数学上,sigmoid表示如下:
σ(x) = 1 / (1 + exp(−x))
Sigmoid激活

import torch
import matplotlib.pyplot as plt

x = torch.range(-5, 5, 0.1)
y = torch.sigmoid(x)
plt.plot(x.numpy(), y.numpy())
plt.show()

《第三章 神经网络基础》 Sigmoid

从图中可以看出,Sigmoid函数对于大多数输入而言,非常快速地饱和。这可能会成为一个问题,因为它可能导致梯度变为零或发散到溢出的浮点值。这些现象也分别称为消失梯度问题和爆炸梯度问题。因此,sigmoid一般仅用于输出,其中压缩属性允许将输出解释为概率。

2.2 Tanh

tanh函数是Sigmoid不同的变体,是“饱和激活函数”。当你写下表达式时,这一点就变得清晰
tanh(x) = 2σ(2x) − 1
就像sigmoid一样,也是一个“压缩”函数,除了它将实际值集合从(-∞,+∞)映射到范围[-1, +1]。

import torch
import matplotlib.pyplot as plt

x = torch.range(-5., 5., 0.1)
y = torch.tanh(x)
plt.grid()
plt.plot(x.numpy(), y.numpy())
plt.show()

《第三章 神经网络基础》 Tanh

2.3 RELU

ReLU(发音为ray-luh)代表整流线性单元。这可以说是最重要的激活功能。事实上,如果没有使用ReLU,很可能会说很多最近的深度学习创新是不可能的。对于一些如此基础的东西,就神经网络激活功能而言,它也是一个令人惊讶的新东西。它的形式也非常简单:
f(x)=max(0,x)
因此,所有ReLU单元正在做的是将负值剪辑为零,如例3-4所示。

import torch
import matplotlib.pyplot as plt

relu = torch.nn.ReLU()
x = torch.range(-5., 5., 0.1)
y = relu(x)

plt.plot(x.numpy(), y.numpy())
plt.show()

《第三章 神经网络基础》 ReLU

ReLU的削波效应有助于消除梯度问题,随着时间的推移,网络中的某些输出可能会简单地变为零并且永远不会再次恢复。这被称为“垂死的ReLU”问题。为了改变这种影响,提出了诸如Leaky ReLU或Parametric ReLU(PReLU)之类的变体,其中泄漏系数a是学习参数:
f(x)=max(x,ax)

import torch
import matplotlib.pyplot as plt

prelu = torch.nn.PReLU(num_parameters=1)
x = torch.range(-5., 5., 0.1)
y = prelu(x)

plt.plot(x.numpy(), y.numpy())
plt.show()

《第三章 神经网络基础》 PReLU

2.4 SOFTMAX

激活功能的另一个选择是softmax。像Sigmoid形函数,该函数SOFTMAX的输出是01之间。然而,softmax操作还将每个输出除以所有输出的总和,这给出了k个可能类的离散概率分布。结果分布中的概率总和为1。这对于解释分类任务的输出非常有用,因此这种转换通常与概率训练目标配对,例如分类交叉熵。
softmax(xi)=exi/(Σexj)

import torch.nn as nn
import torch

softmax = nn.Softmax(dim=1)
x_input = torch.randn(1, 3)
y_output = softmax(x_input)
print(x_input)
print(y_output)
print(torch.sum(y_output, dim=1))
tensor([[-0.6618,  1.0094,  0.4215]])
tensor([[0.1078, 0.5736, 0.3186]])
tensor([1.])

在本节中,我们研究了四个重要的激活函数:Sigmoid,Tanh,ReLU和softmax。这些只是您可以用于构建神经网络的许多可能激活中的四个。随着我们逐步完成本书,将会清楚更多激活函数,但这些都是历史经验,更好的激活函数需要你自己动手去挖掘。

3 损失函数

在第1章中,我们看到了一般监督机器学习架构以及损失函数或目标函数如何帮助指导训练算法,通过查看数据来选择正确的参数。回想一下,损失函数将真值(y)和预测(ŷ)作为输入并得到分数。该分数越高,模型预测越差。PyTorch在其nn包中实现了许多损失函数,我们将学习一些常用的损失函数。

3.1 均方误差损失

对于网络输出(ŷ)和目标(y)是连续值的回归问题,一个常见的损​​失函数是均方误差(MSE)。

《第三章 神经网络基础》 MSE

MSE只是预测值和目标值之差的平方平均值。还有一些其他损失函数可用于回归问题,例如平均绝对误差(MAE)和均方根误差(RMSE),但它们都涉及计算输出和目标之间的实值距离。例3-6显示了如何使用PyTorch实现MSE损失。将计算多为变量之间的距离,要求类型相同。

import torch
import torch.nn as nn

mse_loss = nn.MSELoss()
outputs = torch.randn(3, 5, requires_grad=True)
targets = torch.randn(3, 5)
loss = mse_loss(outputs, targets)
print(loss)

3.2 分类交叉熵损失

分类交叉熵损失通常用于多类分类设置,其中输出被解释为类成员概率的预测。目标(y)是n个元素的向量,表示所有类的真正多项式分布。如果只有一个类是正确的,那么这个向量就是一个热点。网络的输出(ŷ)也是n个元素的向量,但表示网络对多项分布的预测。分类交叉熵将比较这两个向量(yŷ)来衡量损失:

《第三章 神经网络基础》 CrossEntropy

交叉熵及其表达式起源于信息论,但出于本节的目的,将其视为计算不同两种分布的方法是有帮助的。我们希望正确类的概率接近
1
,而其他类的概率接近于零。

要正确使用PyTorch CrossEntropyLoss,重要的是要有点理解网络输出之间的关系,如何计算损失函数,以及源于真正表示浮点数的计算约束的种类。具体而言,有四点确定网络输出和损失函数之间的细微差别关系。首先,数量大小是有限的。其次,如果对softmax公式中使用的指数函数的输入是负数,则结果是指数小的数,如果是正数,则结果是指数大数。接下来,假设网络的输出是应用softmax函数之前的向量。最后,logfunction是指数函数的倒数,和log(exp(x))恰好等于x。根据这四条信息,假设作为softmax函数核心的指数函数和交叉熵计算中使用的对数函数进行数学简化,以便在数值上更稳定并避免真正小或真的大数。这些简化的结果是不使用softmax函数的网络输出可以与PyTorch一起使用CrossEntropyLoss以优化概率分布。然后,当网络训练完成后,softmax函数可用于创建概率分布,如例3-7。

import torch
import torch.nn as nn

ce_loss = nn.CrossEntropyLoss()
outputs = torch.randn(3, 5, requires_grad=True)
targets = torch.tensor([1, 0, 3], dtype=torch.int64)
loss = ce_loss(outputs, targets)
print(loss)

tensor([[-0.5252,  1.3702,  0.6016, -0.9798,  0.2892],
        [ 0.1054, -0.5197, -0.3658, -0.5261, -1.2623],
        [-0.0698, -0.1942,  0.5676,  0.6920, -0.1870]], requires_grad=True)
tensor([1, 0, 3])
tensor(0.9845, grad_fn=<NllLossBackward>)

在此代码示例中,首先使用随机值向量来模拟网络输出。分类的目标值来指示变量的在该维度的概率分布是否合理,从而计算出一个值,要求输入的行数是目标的维度,目标的最大可能值指示了数据列的维度。因为PyTorch的实现CrossEntropyLoss假定每个输入都有一个特定的类,并且每个类都有一个唯一的索引。这就是为什么targets有三个元素:表示每个输入的正确类的索引。根据这个假设,它执行计算更高效的索引到模型输出的操作

3.3 二进制交叉熵

我们在上一节中看到的分类交叉熵损失函数在我们有多个分类问题中非常有用。有时,我们的任务涉及区分两个类 – 也称为二进制分类。对于这种情况,使用二进制交叉熵(BCE)损失是有效的。我们在“示例:餐厅评论的情感分类”中查看此损失功能,以用于我们的示例任务。
在例3-8中,我们使用sigmoid激活函数在表示网络输出的随机向量上创建二进制概率输出向量概率。接下来,将基础事实实例化为0和1的向量。最后,我们计算使用二进制概率向量和实况矢量二元交叉熵损失。输入向量和输出向量维度相同,这点和CrossEntropyLoss不同,但是将输入向量以0.5为界向[1, 0]转换,与模数转化器很类似,输入时模拟信号、输出是数字[1,0]信号,并得到置信值。

bce_loss = nn.BCELoss()
sigmoid = nn.Sigmoid()
probabilities = sigmoid(torch.randn(4, 1, requires_grad=True))
targets = torch.tensor([1, 0, 1, 0],  dtype=torch.float32).view(4, 1)
loss = bce_loss(probabilities, targets)
print(probabilities)
print(loss)

tensor([[0.5622],
        [0.2502],
        [0.5170],
        [0.4629]], grad_fn=<SigmoidBackward>)
tensor([[1.],
        [0.],
        [1.],
        [0.]])
tensor(0.5363, grad_fn=<BinaryCrossEntropyBackward>)

4 深入监督学习

监督学习是学习如何在给定标记示例的情况下将观察结果映射到指定目标的问题。在本节中,我们将详细介绍。具体来说,我们明确地描述了如何使用模型预测损失函数来对模型参数进行基于梯度的优化。这是一个重要的部分,因为本书的其余部分依赖于它,所以即使您对监督学习有点熟悉,也值得详细阅读。

回忆第1章,监督学习需要以下内容:模型,损失函数,训练数据和优化算法。用于监督学习的训练数据是成对的观察和目标,该模型根据观察结果计算预测,并且损失测量预测的误差与目标相比。培训的目标是使用基于梯度的优化算法来调整模型的参数,以使损失尽可能低。

在本节的其余部分,我们将讨论一个经典的玩具问题:将二维点分类为两个类中的一个。直观地说,这意味着学习一条线,称为决策边界或超平面,以区分一个类与另一个类的点。我们逐步介绍并描述数据结构,选择模型,选择损失,设置优化算法,最后将它们一起运行。

4.1 构建数据

在机器学习中,通常的做法是在尝试理解算法时创建具有良好理解属性的合成数据。对于本节,我们将合成数据用于“玩具”任务 – 将二维点分类为两个类中的一个。为了构建数据,我们从xy平面的两个不同部分采样个点,为模型创建一个易于学习的情况。样品显示在图3-2中所示的图中。该模型的目标是将恒星(⋆)分类为一类,(◯)作为另一类。这在图的右侧可视化,其中线上方的所有内容都不同于线下方的所有内容。生成数据的代码位于名为的函数中get_toy_data() 在本章附带的Python笔记本中。

《第三章 神经网络基础》 图3-2 创建可线性分离的数据集。数据集是来自两个正态分布的采样,每个类一个。分类任务成为区分数据点是属于一个分布还是属于另一个分布的任务之一

4.2 选择一个模型

我们在这里使用的模型是在本章开头介绍的模型:Perceptron。Perceptron的灵活性在于它允许任何大小的输入。在典型的建模情况下,输入大小由任务和数据确定。在这个玩具示例中,输入大小为2,因为我们明确地将数据构造在二维平面中。对于这个两类问题,我们为类分配一个数字索引:0和1。字符串标签⋆和○到类索引的映射是任意的,只要它在整个数据预处理,训练,评估和测试中是一致的。该模型的一个重要的附加属性是其输出的性质。由于Perceptron的激活函数是一个sigmoid,Perceptron的输出是数据点的概率(x)上课1; 也就是说,P(y=1|x)

4.3 将概率转换为离散类

对于二元分类问题,我们可以通过施加决策边界δ将输出概率转换为两个离散类。如果预测概率P(y = 1 | x) >δ,则预测类别为1,否则类别为0。通常,此决策边界设置为0.5,但实际上,您可能需要调整此超参数(使用评估数据集)以实现所需的分类精度。

4.4 选择一个损失函数

准备好数据并选择模型体系结构后,在监督培训中还有两个重要的组件可供选择:损失函数和优化器。对于模型输出是概率的情况,最合适的损失函数族是基于交叉熵的损失。对于这个玩具数据示例,因为模型正在产生二元结果,所以我们专门使用BCE Loss。

4.5 选择优化器

这个简化的监督训练示例中的最终选择点是优化器。当模型产生预测并且损失函数测量预测和目标之间的误差时,优化器使用误差信号更新模型的权重。在最简单的形式中,有一个超参数控制优化器的更新行为。这个称为学习率的超参数控制误差信号对更新权重的影响程度。学习率是一个关键的超参数,您应该尝试几种不同的学习率并进行比较。较大的学习速率将导致参数的更大变化,并可能影响收敛。学习率太低会导致培训期间进展甚微。
PyTorch库为优化器提供了多种选择。随机梯度下降(SGD)是一种经典的算法选择,但对于困难的优化问题,SGD存在收敛问题,往往导致较差的模型。当前优选的替代方案是自适应优化器,例如Adagrad或Adam,其使用关于随时间的更新的信息。在下面的示例中,我们使用Adam,但总是值得查看几个优化器。使用Adam,默认学习率为0.001。对于学习率等超参数,我们始终建议首先使用默认值,除非您从纸张中获取调用特定值的配方。

import torch.nn as nn
import torch.optim as optim

input_dim = 2
lr = 0.001

perceptron = Perceptron(input_dim=input_dim)
bce_loss = nn.BCELoss()
optimizer = optim.Adam(params=perceptron.parameters(), lr=lr)

4.6 基于梯度的监督学习

学习从计算损失开始; 也就是说,模型预测离目标有多远。反过来,损失函数的梯度成为参数应该“改变多少”的信号。每个参数的梯度表示给定参数的损耗值的瞬时变化率。实际上,这意味着您可以知道每个参数对损失函数的贡献程度。直观举个例子,这是一个斜坡,你可以想象每个参数都站在自己的山上,想要在山坡上向下走一步。与基于梯度的模型训练有关的所有内容都是使用关于该参数的损失函数的梯度来迭代地更新每个参数。

我们来看看这个算法。首先,perceptron使用zero_grad()函数清除当前存储在model()对象内的梯度信息。然后,模型计算y_pred输入数据(x_data)给出的output()。接下来,通过将模型输出(y_pred)与预期目标(y_target)进行比较来计算损失。这正是受监督训练信号的监督部分。PyTorch损失对象(criterion)具有一个名为backward()迭代地通过计算图向后传播损失并每个参数的函数梯度。最后,optimizer(opt)指示参数如何更新其值,实现更新函数为step()

整个训练数据集被分为若干批次。在文献和本书中,术语也常常指minibatch,并不严格区分。表示每个批次都明显小于整个训练数据的大小; 例如,训练数据可能是数百万,而小批量可能只有几百。梯度步骤的每次迭代都在一批数据上执行。使用超参数batch_size指定批次的大小。由于训练数据集是固定的,因此增加批量大小会减少批次数。在多个批次(通常是有限大小数据集中的批次数)之后,表示训练循环已完成一个epoch。一个时代是一个部分数据的训练迭代。如果每个epoch的批次数与数据集中的批次数相同,则epoch是对数据集的完整迭代。模型训练要指定训练的数量。训练的数量并不容易选择,但有一些方法可以确定何时停止,我们将在稍后讨论。如示例3-10所示,监督训练循环因此是嵌套循环:数据集上的内循环或批量的设定数量,以及外循环,其在固定数量的epoch或其他终止标准上重复内循环。

n_epochs = 10
n_batches = 10
batch_size = 32
def get_toy_data(batch_size):
    x = torch.rand(batch_size, input_dim)
    y = torch.zeros(batch_size)
    for i in range(batch_size):
        if x[i, :].sum() > 1:
            y[i] = 1
    return x,y 
#一个epoch是一个完整的数据集
for epoch_i in range(n_epochs):
    # 内部循环是在一个批次数据集上
    for batch_i in range(n_batches):

        # Step 0: 获取数据
        x_data, y_target = get_toy_data(batch_size)

        # Step 1: 清除梯度
        perceptron.zero_grad()

        # Step 2: 计算模型前向参数
        y_pred = perceptron(x_data)

        # Step 3: 计算损失
        loss = bce_loss(y_pred, y_target)

        # Step 4: 后向传递
        loss.backward()

        # Step 5: 触发优化,更新参数
        optimizer.step()

5.7 辅助训练概念

有监督的基于梯度的学习的核心概念很简单:定义模型,计算输出,使用损失函数来计算梯度,并应用优化算法来使用梯度更新模型参数。但是,训练过程中有几个重要但辅助的概念。我们将在本节中介绍其中的一些内容。

5.7.1 正确衡量模型绩效:评估指标

监督训练循环之外最重要的组成部分是使用模型从未训练过的数据的客观性能测量。使用一个或多个评估指标评估模型。在自然语言处理(NLP)中,存在多个评估度量。最常见的,也就是我们将在本章中使用的是准确性。准确性是指在训练期间看不到的数据集上正确的预测的准确率。

5.7.2 正确测量模型性能:拆分数据集

始终牢记最终目标是很好地描述数据的真实分布。那是什么意思?假设我们能够看到无限量的数据(“ 真实 / 看不见的 分布 ”),则存在全局存在的数据分布。显然,我们做不到。相反,我们使用训练数据的样本是有限的。我们观察到有限样本中的数据分布,它是真实分布的近似或不完整图像。一个模型更好地处理它不仅减少了训练数据中看到的样本的误差,而且还减少了看不见的分布的样本,我们称这个模型的泛型性能好。由于该模型致力于降低其在训练数据上的损失,因此它可能产生过度拟合并适应实际上不属于真实数据分布的特性。
为了避免此类问题,标准做法是将数据集拆分为三个随机采样部分,称为训练train,验证verify和测试test数据集,或进行k折交叉验证。拆分为三个集合是比较简单的方法,因为它只需要一次计算。您应该使用一些技巧,以确保三个数据集中的每个分类之间的类分布保持一致。换句话说,最好通过类标签拆分数据集,然后将按类标签拆分的每个集合随机分成训练,验证和测试数据集。常见的拆分百分比是训练70%,验证15%,测试15%。不过,这不是一成不变的。

在某些情况下,可能存在预定义的训练,验证和测试拆分; 这在基准测试任务的数据集中很常见。在这种情况下,重要的是仅使用训练数据来更新模型参数,在每个时期结束时使用验证数据来测量模型性能,并且在探索所有建模选择并且最终结果需要之后仅使用测试数据一次。最后一部分非常重要,因为机器学习工程师在测试数据集上模拟性能越多,他们就越偏向于在测试集上表现更好的选择。当发生这种情况时,如果不收集更多数据,就无法知道未见数据的模型性能。
  
  使用k- fold交叉验证的模型评估与使用预定义拆分的评估非常相似,但在此之前是将整个数据集拆分为k个相等大小的折叠的额外步骤。其中一个折叠用于评估,剩余的k-1折叠用于训练。通过交换评估中的哪个折叠来迭代地重复这一过程。因为存在k个折叠,所以每个折叠都有机会成为评估折叠并导致折叠特定的精度,从而产生k个精度值。最终报告的准确度只是标准偏差的平均值。ķ- 折叠评估在计算上是昂贵的,但对于较小的数据集来说非常必要,因为错误的分割会导致过于乐观(因为测试数据太容易)或者过于悲观(因为测试数据太难了)。

知道何时停止训练

前面的例子训练了模型的固定数量的时期。虽然这是最简单的方法,但它是任意的和不必要的。正确测量模型性能的一个关键功能是使用该测量来知道何时应停止训练。最常见的方法是使用称为提前停止的启发式方法。早期停止的工作原理是跟踪验证数据集从纪元到纪元的性能,并注意性能何时不再提高。然后,如果性能继续不提高,则终止训练。在终止训练之前等待的时期数被称为耐心patience。通常,模型在某些数据集上停止改进的点被称为模型收敛时的点。在实践中,我们很少等待模型完全收敛,因为收敛是耗时的,并且它可能导致过度拟合。

5.8 寻找合适的超参数

我们之前已经了解到,参数(或权重)采用优化器针对小批量的训练数据的固定子集调整的实际值。超参数影响由参数取入的模型参数和值数量的任何模型。确定模型的训练方式有很多不同的选择。这些选择包括选择损失函数、优化器、优化器的学习率,作为层大小(在第4章中介绍; 耐心提前停止; 和各种正规化决定(也在第4章中讨论)。重要的是要注意这些决定会对模型是否收敛及其性能产生很大影响,您应该系统地探索各种选择点。

5.9 正则

深度学习中最重要的概念之一是正规化。正则化的概念来自数值优化理论。回想一下,大多数机器学习算法都在优化损失函数,以找到解释观察结果的最可能的参数值(或“模型”)(即产生最少量的损失)。对于大多数数据集和任务,可能存在针对此优化问题的多个解决方案(可能的模型)。那么我们应该选择哪一个(或优化器)呢?为了形成直观的理解,请考虑图3-3,以便通过一组点拟合曲线。

《第三章 神经网络基础》 图3-3 两条曲线都“适合”这些点,但其中一条似乎比另一条更合理。正规化有助于我们选择这种更合理的解释

两条曲线都“适合”这些点,但哪一个是不太可能的解释?根据奥卡姆的剃刀原理,我们知道一个更简单的解释比复杂的解释更好。机器学习中的这种平滑约束称为
L2正则化。在PyTorch中,您可以通过
weight_decay在优化器中设置参数来控制它。较大的
weight_decay价值,更多的优化器将选择平滑解释; 也就是说,L2正则化越强。

除L2之外,另一种流行的正则化是L1正则化。L1通常用于鼓励更稀疏的解决方案; 换句话说,大多数模型参数值接近于零。在第4章中,您将看到一种称为“Dropout”的结构正则化技术。模型正则化的主题是一个活跃的研究领域,PyTorch是一个实现自定义正则化器的灵活框架。

6 示例:餐厅评论的情绪分类

在上一节中,我们深入探讨了有关玩具示例的监督训练,并说明了许多基本概念。在本节中,我们重复该练习,但这一次是使用真实世界的任务和数据集:使用感知器和监督训练来分类Yelp上的餐厅评论是正面还是负面。因为这是本书中第一个完整的NLP示例,我们将以极其详细的方式描述辅助数据结构和训练例程。后面章节中的示例将遵循非常相似的模式,因此我们建议您仔细学习本节,并根据需要重新参考它以进行修改。
在本书的每个示例的开头,我们描述了我们正在使用的数据集和任务。在此示例中,我们使用Yelp数据集将评论与其情感标签(正面或负面)配对。我们还描述了一些数据集操作步骤,我们采取这些步骤来清理并将其划分为训练,验证和测试集。

在理解了数据集之后,您将看到一个模式,它定义了本书中重复的三个辅助类,并用于将文本数据转换为矢量化形式:VocabularyVectorizer和PyTorch DataLoaderVocabulary表示单词到变量,我们在讨论“观察与目标编码”。我们使用Vocabulary两者来将文本标记映射到整数并将类标签映射到整数。接下来,Vectorizer封装词汇表并负责摄取字符串数据,如评论文本,并将其转换为将在训练例程中使用的数字向量。我们使用最终辅助类PyTorch DataLoader将各个矢量化数据点分组并整理成小批量。

在描述文本到矢量化小批量的数据集和辅助类之后,概述了Perceptron分类器及其训练例程。需要注意的一点是,本书中后续每个示例的训练程序样子差不多。我们在此示例中对此进行了更详细的讨论,因此,我们再次鼓励您将此示例用作未来培训例程的参考。我们通过讨论结果并总结一下这个例子,看看模型学到了什么。

6.1 Yelp评估数据集

2015年,Yelp举办了一场比赛,邀请参与者预测餐厅的评级。同年,Zhang,Zhao和Lecun(2015)通过将1星和2星评级转换为“负面”情绪类别,将3星和4星评级转化为“积极”情绪来简化数据集类。该数据集分为560,000个训练样本和38,000个测试样本。在本数据集部分的其余部分中,我们描述了最小化数据清理并派生最终数据集的过程。然后,我们概述了利用PyTorch Dataset类的实现。

在此示例中,我们使用简化的Yelp数据集,但有两个小的差异。第一个区别是我们使用数据集的“轻”版本,这是通过选择10%的训练样本作为完整数据集得出的。这有两个结果:首先,使用小型数据集可以使训练测试循环快速,因此我们可以快速进行实验。其次,它产生的模型精度低于使用所有数据。这种低精度通常不是主要问题,因为您可以使用从较小数据集子集获得的知识重新训练整个数据集。这是培训深度学习模型的一个非常有用的技巧,在许多情况下,训练数据的数量可能是巨大的。

从这个较小的子集中,我们将数据集分成三个分区:一个用于训练,一个用于验证,一个用于测试。虽然原始数据集只有两个分区,但拥有验证集很重要。在机器学习中,您通常会在数据集的训练集上训练模型,在测试集来评估模型的完成程度。如果模型决策基于训练集,则该模型现在不可避免地偏向于在保持部分上执行得更好。由于测量增量进度至关重要,因此解决此问题的方法是使用第三个分区,尽可能少地用于评估。

总而言之,您应该使用数据集的训练分区来导出模型参数,数据集的验证分区用于在超参数之间进行选择(制定建模决策),并且数据集的测试分区应该用于最终评估和报告。我们展示了如何拆分数据集。请注意,随机种子设置为静态数字,我们首先按类标签聚合,以保证类分布保持不变

# 拆分数据集 train, val, and test splits
by_rating = collections.defaultdict(list)
for _, row in review_subset.iterrows():
    by_rating[row.rating].append(row.to_dict())

# 分割数据
final_list = []
np.random.seed(args.seed)

for _, item_list in sorted(by_rating.items()):
    np.random.shuffle(item_list)
    
    n_total = len(item_list)
    n_train = int(args.train_proportion * n_total)
    n_val = int(args.val_proportion * n_total)
    n_test = int(args.test_proportion * n_total)
    
    # Give data point a split attribute
    for item in item_list[:n_train]:
        item['split'] = 'train'
    
    for item in item_list[n_train:n_train+n_val]:
        item['split'] = 'val'

    for item in item_list[n_train+n_val:n_train+n_val+n_test]:
        item['split'] = 'test'

    # Add to final list
    final_list.extend(item_list)

final_reviews = pd.DataFrame(final_list)

除了创建具有三个用于训练,验证和测试的分区的子集之外,我们还通过在标点符号周围添加空格,并删除所有分割中不是标点符号的无关符号来最小化数据,如示例3所示-12

def preprocess_text(text):
    text = text.lower()
    text = re.sub(r"([.,!?])", r" \1 ", text)
    text = re.sub(r"[^a-zA-Z.,!?]+", r" ", text)
    return text

final_reviews.review = final_reviews.review.apply(preprocess_text)

6.2 了解PyTorch的数据集表示

示例3-13中ReviewDataset显示的类假定已经过最小程度清理并分成三个分区的数据集。特别是,数据集假定它可以基于空格分割评论,以便在评论中获取标记列表。此外,它假定数据具有其所属分割的注释。请注意,我们使用Python的类方法指示此数据集类的入口点方法。我们在整本书中遵循这种模式。

PyTorch通过提供Dataset类来为数据集提供抽象。该Dataset是一个抽象的迭代器。使用PyTorch定义新数据集时,必须首先从Dataset类中继承(或继承)并实现__getitem____len__方法。对于这个例子,我们创建了一个ReviewDataset继承自PyTorch Dataset类的类,并实现了两个方法:__getitem____len__。通过实现这两种方法,允许各种PyTorch实用程序使用我们的数据集。我们将介绍其中一个实用程序,特别DataLoader是下一节中的实用程序。后面的实现很大程度上依赖于一个名为的类ReviewVectorizer。我们创设了ReviewVectorizer,可以将其描绘为一个类,用于处理从评论文本到表示评论的数字向量的转换。只有通过一些矢量化操作,神经网络才能与文本数据交互。总体设计模式是实现一个数据集类,它处理一个数据点的矢量化逻辑。然后,PyTorch DataLoader(也在下一节中描述)将通过从数据集中采样和整理来创建小批量。

from torch.utils.data import Dataset

class ReviewDataset(Dataset):
    def __init__(self, review_df, vectorizer):
        """
        Args:
            review_df (pandas.DataFrame): the dataset
            vectorizer (ReviewVectorizer): vectorizer instantiated from dataset
        """
        self.review_df = review_df
        self._vectorizer = vectorizer

        self.train_df = self.review_df[self.review_df.split=='train']
        self.train_size = len(self.train_df)

        self.val_df = self.review_df[self.review_df.split=='val']
        self.validation_size = len(self.val_df)

        self.test_df = self.review_df[self.review_df.split=='test']
        self.test_size = len(self.test_df)

        self._lookup_dict = {'train': (self.train_df, self.train_size),
                             'val': (self.val_df, self.validation_size),
                             'test': (self.test_df, self.test_size)}

        self.set_split('train')

    @classmethod
    def load_dataset_and_make_vectorizer(cls, review_csv):
        """Load dataset and make a new vectorizer from scratch
        
        Args:
            review_csv (str): location of the dataset
        Returns:
            an instance of ReviewDataset
        """
        review_df = pd.read_csv(review_csv)
        return cls(review_df, ReviewVectorizer.from_dataframe(review_df))

    def get_vectorizer(self):
        """ returns the vectorizer """
        return self._vectorizer

    def set_split(self, split="train"):
        """ selects the splits in the dataset using a column in the dataframe 
        
        Args:
            split (str): one of "train", "val", or "test"
        """
        self._target_split = split
        self._target_df, self._target_size = self._lookup_dict[split]

    def __len__(self):
        return self._target_size

    def __getitem__(self, index):
        """the primary entry point method for PyTorch datasets
        
        Args:
            index (int): the index to the data point 
        Returns:
            a dict of the data point's features (x_data) and label (y_target)
        """
        row = self._target_df.iloc[index]

        review_vector = \
            self._vectorizer.vectorize(row.review)

        rating_index = \
            self._vectorizer.rating_vocab.lookup_token(row.rating)

        return {'x_data': review_vector,
                'y_target': rating_index}

    def get_num_batches(self, batch_size):
        """Given a batch size, return the number of batches in the dataset
        
        Args:
            batch_size (int)
        Returns:
            number of batches in the dataset
        """
        return len(self) // batch_size

6.3 词汇表,Vectorizer和DataLoader

Vocabulary,在VectorizerDataLoader包含了处理数据的关键管道:转换的文本输入矢量minibatches。管道以预处理文本开始; 每个数据点都是令牌的集合。在这个例子中,令牌碰巧的话,但是你会看到第4和第6章,代币也可以是文字。以下小节中介绍的三个类负责将每个标记映射到整数,将此映射应用于每个数据点以创建矢量化形式,然后将矢量化数据分成小批处理。

6.3.1 词汇

从文本到矢量化小批量的第一个阶段是将每个token映射到数字。标准方法是具有双射 – 可以在标记和整数之间反转的映射。在Python中,这只是两个词典。我们将这个双射封装成一个Vocabulary类,如例3-14所示。该Vocabulary不仅管理这双射,允许用户添加新的令牌,并且让索引自动递增,同时也处理了一个名为UNK特殊的记号。UNK代表“未知”令牌。通过使用UNK令牌,得知不是词汇表中的单词。通常我们会控制UNK令牌个数,以便Vocabulary训练程序中有UNK令牌。这样的好处是可以减少Vocabulary类使用的内存。具体的函数有,add_token新的令牌添加到Vocabularylookup_token用于检索令牌,lookup_index检索对应于一个特定的索引令牌。

class Vocabulary(object):
    """处理文本,提取单词类"""

    def __init__(self, token_to_idx=None, add_unk=True, unk_token="<UNK>"):
        """
        Args:
            token_to_idx (dict): a pre-existing map of tokens to indices
            add_unk (bool): a flag that indicates whether to add the UNK token
            unk_token (str): the UNK token to add into the Vocabulary
        """

        if token_to_idx is None:
            token_to_idx = {}
        self._token_to_idx = token_to_idx

        self._idx_to_token = {idx: token 
                              for token, idx in self._token_to_idx.items()}

        self._add_unk = add_unk
        self._unk_token = unk_token
        
        self.unk_index = -1
        if add_unk:
            self.unk_index = self.add_token(unk_token) 
        
        
    def to_serializable(self):
        """ 返回一个序列化字典 """
        return {'token_to_idx': self._token_to_idx, 
                'add_unk': self._add_unk, 
                'unk_token': self._unk_token}

    @classmethod
    def from_serializable(cls, contents):
        """ 有序字典中初始化类 """
        return cls(**contents)

    def add_token(self, token):
        """更新映射字典.

        Args:
            token (str): the item to add into the Vocabulary
        Returns:
            index (int): the integer corresponding to the token
        """
        if token in self._token_to_idx:
            index = self._token_to_idx[token]
        else:
            index = len(self._token_to_idx)
            self._token_to_idx[token] = index
            self._idx_to_token[index] = token
        return index

    def lookup_token(self, token):
        """Retrieve the index associated with the token 
          or the UNK index if token isn't present.
        
        Args:
            token (str): the token to look up 
        Returns:
            index (int): the index corresponding to the token
        Notes:
            `unk_index` needs to be >=0 (having been added into the Vocabulary) 
              for the UNK functionality 
        """
        if self.add_unk:
            return self._token_to_idx.get(token, self.unk_index)
        else:
            return self._token_to_idx[token]

    def lookup_index(self, index):
        """Return the token associated with the index
        
        Args: 
            index (int): the index to look up
        Returns:
            token (str): the token corresponding to the index
        Raises:
            KeyError: if the index is not in the Vocabulary
        """
        if index not in self._idx_to_token:
            raise KeyError("the index (%d) is not in the Vocabulary" % index)
        return self._idx_to_token[index]

    def __str__(self):
        return "<Vocabulary(size=%d)>" % len(self)

    def __len__(self):
        return len(self._token_to_idx)
    原文作者:readilen
    原文地址: https://www.jianshu.com/p/e7431bcff3a6
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞