「PyTorch自然语言处理系列」3. 神经网络的基本组件(上)
来源 | Natural Language Processing with PyTorch
作者 | Rao,McMahan
译者 | Liangchu
校对 | gongyouliu
编辑 | auroral-L
上下拉动翻看整个目录
本章通过介绍构建神经网络的过程中涉及的基本思想(如激活函数、损失函数、优化器和监督训练设置)为后面的章节奠定基础。我们从感知器开始,它是一个将多种不同概念联系在一起成为一个单元的神经网络。感知器本身是更复杂的神经网络的一个组成部分。这是一种贯穿全书的常见模式——我们所讨论的每个架构或网络都可以单独使用,也可以作为其他复杂的网络的一部分来组合使用。当我们讨论计算图和本书的剩余部分时,这种组合性将变得更容易理解。
1. 感知机:最简单的神经网络
最简单的神经网络单元是感知器(perceptron)。在历史上,感知器曾用于非常松散地模仿生物神经元。像生物神经元一样,感知器有输入和输出,“信号(signal)”从输入流向输出,如下图(3-1)所示:
每个感知器单元都有一个输入input(x),一个输出output(y),和三个“旋钮”(knobs):一组权重weight(w)、偏差bias(b)和一个激活函数activation function(f)。权重和偏差都由数据习得,激活函数是基于神经网络设计师的直觉和目标输出而精心选择出来的。数学上,我们将之可以表示如下:y= f( wx+b )
一般情况下,感知器不止有一个输入。我们可以用向量(vector)表示这个一般情况,即X和W是向量,W和X的乘积替换为点积:y=f ( WX +b )
这里用f表示激活函数,它通常是一个非线性函数。线性函数的图是一条直线。这个例子中,WX +b 是一个线性函数。因此,一个感知器本质上是线性函数和非线性函数的组合。线性表达式 WX +b 也被称作仿射变换(affine transform),是一种二维坐标到二维坐标之间的线性变换。
下例(3-1)展示了 PyTorch 中的感知器实现,它接受任意数量的输入、执行仿射转换、应用激活函数并生成单个输出。
import torch
import torch.nn as nn
class Perceptron(nn.Module):
''' A Perceptron is one Linear layer '''
def __init__(self, input_dim):
'''
Args:
input_dim (int): size of the input features
'''
super(Perceptron, self).__init__()
self.fc1 = nn.Linear(input_dim, 1)
def forward(self, x_in):
'''The forward pass of the Perceptron
Args:
x_in (torch.Tensor): an input data tensor.
x_in.shape should be (batch, num_features)
Returns:
the resulting tensor. tensor.shape should be (batch,)
'''
return torch.sigmoid(self.fc1(x_in)).squeeze()
PyTorch 在torch.nn模块中提供了一个Linear()类,该模块用于做权值和偏差所需的簿记,并做所需的仿射变换。在“深入有监督训练”一节中,你将看到如何从数据中“学习”权重w和b的值。前面示例中使用的激活函数是 sigmoid 函数。在下一节中,我们将回顾包括 sigmoid 函数在内的一些常见激活函数。
2. 激活函数
激活函数是神经网络中引入的非线性函数,它用于捕获数据中的复杂关系。在“深入有监督训练”和“多层感知器”两节中,我们会深入了解为什么学习中需要非线性,但首先让我们了解一些常见的激活函数。
2.1 Sigmoid
sigmoid 是神经网络历史上最早被使用的激活函数之一,它将输入的任何实数压缩成0和1之间的一个值。数学上,sigmoid 的表达式如下:
从表达式中很容易看到sigmoid是一个光滑的可微分的函数。torch将 sigmoid 实现为Torch.sigmoid(),如下例(3-2)所示:
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 函数对于大多数输入饱和(也就是产生极值输出)得非常快,这可能成为一个问题——因为它可能导致梯度变为零或发散到溢出的浮点值,这些现象分别被称为梯度消失(vanishing gradient)和梯度爆炸(exploding gradient)问题。因此在神经网络中,除了在输出端使用 sigmoid 单元外,很少看到其他使用 sigmoid 单元的情况。在输出端,压缩属性允许将输出解释为概率。
2.2 Tanh
tanh 激活函数是 sigmoid 在外观上的不同变体。当你写下 tanh 的数学表达式时就能清楚地理解这点了:
通过一些争论(留作练习),你可以确信 tanh 只是 sigmoid 的一个线性变换,正如下例(3-3)所示。当你为tanh()写下 PyTorch 代码并绘制其曲线时,这一点也是十分明显的。注意tanh像 sigmoid一样,也是一个“压缩”函数,不同的是,tanh映射实值集合从(-∞,+∞)输出到范围(-1,+1)。
import torch
import matplotlib.pyplot as plt
x = torch.range(-5., 5., 0.1)
y = torch.tanh(x)
plt.plot(x.numpy(), y.numpy())
plt.show()
2.3 ReLU
ReLU(发音 ray-luh)代表线性整流函数(rectified linear unit),也称修正线性单元。ReLU可以说是最重要的激活函数。事实上,我们可以大胆地说:倘若没有ReLU,最近许多在深度学习方面的创新都是不可能实现的。对于我们所学的如此基础的内容来说,就神经网络激活函数而言,ReLU也是让人惊讶的新功能。它的形式也出奇的简单:
因此ReLU 单元所做的就是将负值裁剪为零,如下例(3-4)所示:
示例 3-4:ReLU 激活
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 死亡”问题。为了减轻这种影响,人们提出了 Leaky ReLU 和 Parametric ReLU (PReLU)等变体,其中泄漏系数a是一个可学习的参数:
下例(3-5)展示了结果:
示例3-5:PReLU激活
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()
2.4 Softmax
激活函数的另一个选择是 softmax。与 sigmoid 函数类似,softmax 函数将每个单元的输出压缩到 0 和 1 之间,正如下例(3-6)所示。然而softmax 操作还将每个输出除以所有输出的和,从而得到一个离散概率分布,它有k个可能的类:
产生的分布中的概率总和为 1,这对于解释分类任务的输出非常有用,因此这种转换通常与概率训练目标配对,例如分类交叉熵(将在“深入有监督训练”中介绍)。
Input[0]
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))
Output[0]
tensor([[ 0.5836, -1.3749, -1.1229]])
tensor([[ 0.7561, 0.1067, 0.1372]])
tensor([ 1.])
在本节中,我们学习了四种重要的激活函数:sigmoid、tanh、ReLU 和 softmax,但它们只是你在构建神经网络时可能用到的很多种激活方式中的四种。深入学习本书的过程中,我们将会清楚地知道要使用哪些激活函数以及在哪儿使用它们,但是一般的指南只是简单地遵循过去的工作原理。
3. 损失函数
在第一章中,我们了解了通用的监督机器学习架构,以及损失函数或目标函数如何通过查看数据来帮助指导训练算法选择正确的参数。回想一下,一个损失函数将实际值truth(y)和预测prediction(ŷ)作为输入,产生一个实值。这个值越高,模型的预测效果就越差。PyTorch 在其nn包中实现了许多损失函数,由于数量实在太多,因此我们就不一一介绍了,这里只介绍一些常用的损失函数。
3.1 均方误差损失
回归问题的网络输出output(ŷ)和目标target(y)是连续值,一个常用的损失函数是均方误差损失(mean squared error,MSE):
MSE 就是预测值与目标值之差的平方的平均值。还有其他一些损失函数可用于回归问题,例如平均绝对误差(mean absolute error,MAE)和均方根差(root mean squared error ,RMSE),它们都涉及计算输出和目标之间的实值距离。下例(3-7)展示了如何使用 PyTorch 实现 MSE 损失。
Input[0]
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)
Output[0]
tensor(3.8618)
3.2 分类交叉熵损失
分类交叉熵损失(categorical cross-entropy loss)通常用于多分类问题,其中输出被解释为类成员概率的预测。目标target(y)是n个元素组成的向量,表示所有类真正的多项分布。如果只有一个类是正确的,那么这个向量就是独热向量。网络的输出(ŷ)也是n个元素组成的一个向量,但它代表网络多项分布的预测。分类交叉熵将比较这两个向量(y, ŷ)来衡量损失:
交叉熵及其表达式起源于信息学,但是出于本节的目的,把它看作一种用于比较衡量两个分布的不同的方法会更好理解。我们希望正确的类的概率接近 1,而其他类的概率接近 0。
为了正确使用 PyTorch 的CrossEntropyLoss()函数,在一定程度上理解网络输出、损失函数的计算方法和来自真正表示浮点数的各种计算约束之间的关系是很重要的。具体来说,有四条信息决定了网络输出和损失函数之间微妙的关系。首先,一个数字的大小是有限的;第二,如果 softmax 公式中使用的指数函数的输入是负数,那么结果是一个指数小的数,反之如果是正数,则结果是一个指数大的数;第三,假定网络的输出是应用 softmax 函数之前的向量;最后,log函数是指数函数的倒数,而log(exp (x))就等于x。基于以上四条信息,数学简化假设指数函数是softmax函数的核心,log函数被用于交叉熵计算,以达到数值上更稳定和避免很小或很大数字的目的。这些简化的结果是:不使用 softmax 函数的网络输出可以与 PyTorch 的CrossEntropyLoss()一起使用,从而优化概率分布。接着,当网络经过训练后,可以使用 softmax 函数创建概率分布,如下例(3-8)所示。
Input[0]
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)
Output[0]
tensor(2.7256)
在这个代码示例中,随机值构成的一个向量首先被用于网络的输出,然后真实向量(也叫目标target)被创建为整数构成的一个向量,这是因为Pytorch的CrossEntropyLoss()实现假设每个输入都有一个特定的类,每个类都有一个唯一索引。这就是为什么target有三个元素:一个表示每个输入对应正确类的索引。根据假设,它执行索引到模型输出的计算效率更高的操作。
3.3 二元交叉熵损失
我们在上一节看到的分类交叉熵损失函数对于多分类问题非常有用。
有时,我们的任务包括区分两个类——也称为二元分类(binary classification)。在这种情况下,使用二元交叉熵(binary cross-entropy,BCE)损失是有效的。我们将在示例任务的“示例:分类餐馆评论的情感”一节中研究这个损失函数。
在下例(3-9)中,我们使用表示网络输出的随机向量上的 sigmoid 激活函数创建二元概率输出向量(probabilities)。接下来,真实情况被实例化为一个由0 和 1 组成的向量。最后,我们利用二元概率向量和真实向量计算二元交叉熵损失。
Input[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)
Output[0]
tensor([[ 0.1625],
[ 0.5546],
[ 0.6596],
[ 0.4284]])
tensor(0.9003)