循环神经网络
Vanilla RNN
循环神经网络是一个有向循环的过程,“有向”是因为朝着序列方依次输入各序列成分以及上一步的输出成分,“循环”是因为每个序列成分进行运算的参数是一致的,因为它对数据的每个输入执行相同的功能,而当前输入的输出取决于上一步的计算。
与前馈神经网络不同, 循环神经网络可以使用其内部状态(隐状态)来处理输入序列。这使它们适用于诸如语音、文本等序列数据。而在其他神经网络中,所有输入都是彼此独立的。但是在循环神经网络中,所有输入都是相互关联的,如下图所示为基本的循环神经网络模型结构: Vanilla RNN。
循环神经网络每一步的输出都包含了前面步骤的信息,因此具备记忆功能,而记忆功能是解读语境的关键。比如,对于“小梅很喜欢吃桔子,她不喜欢吃苹果”这句话,如果逐词输入输出但是缺少记忆性,我们只能解析出一个个独立词所表达的意思,反之,在具备记忆的情况下,当看到桔子时,可知其不仅仅指“水果的概念”,而是“一个人喜欢的食物对象”。因此循环神经网络很适合处理序列间存在联系的场景。
上图中展示了一个最基本最简单的单向循环神经网络,实际上根据需求可以在此基础上有所改进。如可以将单向序列行进的网络改为双向循环神经网络,因为很多时候,对于一个序列,元素之间的影响可以是双向的,即从前往后以及从后往前。还是以“小梅很喜欢吃桔子,她不喜欢吃苹果”为例,如果从后往前看,先看过“她不喜欢吃苹果”,再看到“桔子”,也能大概知道“桔子”可能和“一个人的喜好”相关。所以,双向循环神经网络能够提供更丰富的信息。
而在事实应用中,Vanilla RNN 并不常用,这是因为其在梯度下降过程中,存在累乘项及激活函数的值域导致的梯度消失和爆炸问题,也就是说,训练 Vanilla RNN 是一项非常困难的任务,无法处理很长的序列,获取不到远距离的信息。
LSTM 与 GRU
长短期记忆(LSTM)网络是 Vanilla RNN 的修改版,该网络由 Hochreiter & Schmidhuber (1997) 引入,并有许多人对其进行了改进和普及,可以更轻松地记住序列中的更长距离的过去数据,通过特制的门控结构改变了梯度更新的表达式,从而缓解了梯度消失问题(梯度爆炸可通过梯度裁剪解决)。LSTM 非常适合对序列数据进行分类,标注和预测。 LSTM 基本结构如下:
LSTM 的核心是细胞状态,用贯穿细胞的水平线表示。细胞状态像传送带一样,贯穿整个细胞却只有很少的分支,这样能保证信息稳定地流过整个网络,就好比人的记忆状态能够贯穿人的一生。
LSTM 网络能通过一种被称为门的结构对细胞状态进行删除或者添加信息。门能够有选择性地决定让哪些信息通过。其实门的结构很简单,即 Sigmoid 层和点乘操作的组合。因为 Sigmoid 层的输出是在 0-1 之间,这代表有多少概率的信息保留下来,0 表示都不能通过,1 表示都能通过。LSTM 里面包含三个门:忘记门、输入门和输出门。
以上便是 LSTM 的内部结构,通过门控状态来控制传输状态,记住对任务关键的信息,忘记不重要的信息;而不像普通的 RNN 那样只仅有一种记忆叠加的简单方式,可针对更长的文本。但同时也因为引入了很多内容,导致参数变多,也使得训练难度加大了很多。因此很多时候我们往往会使用效果和 LSTM 相当,但参数更少的 GRU 来构建大训练量的模型。
GRU 作为 LSTM 的一种变体,将忘记门和输入门合成了一个单一的更新门。同样还混合了细胞状态和隐藏状态,加诸其他一些改动。最终的模型比标准的 LSTM 模型要简单,也是非常流行的变体。
基于 PyTorch 搭建 LSTM
在 PyTorch 中直接调用 nn.LSTM() 便能获取已构建好的 LSTM 层结构,首先介绍其参数。
input_size: 表示的是输入的数据维数。
hidden_size: 表示的是输出维数。
num_layers: 表示堆叠几层的 LSTM,默认是 1。
bias: True 或者 False,决定是否使用 bias,默认为 True。
batch_first: 如果为 True, 接受的数据输入是 (batch_size,seq_len,input_size),如果为 False,则为 (seq_len,batch_size,input_size),默认为 False。
dropout: 表示除了最后一层之外都引入一个 dropout。
bidirectional: 表示双向 LSTM,默认为 False。
接下来介绍 LSTM 的输入与输出。
输入包括:
input: 表示输入数据,其维度为 (seq_len,batch_size,input_size)。
h_0: 初始隐状态,维度为 (num_layers*num_directions,batch_size,hidden_size),num_layers 表示 LSTM 的层数,num_directions 在 LSTM 为单向时为 1,双向时为 2,非必须输入,网络会提供默认初始状态。
c_0: 初始的细胞状态,维度与 h_0 相同,非必须,网络会提供默认初始状态。
输出包括:
output: 最后输出,维度为 (seq_len, batch_size, num_directions * hidden_size)。
h_n: 最后时刻的输出隐藏状态,维度为 (num_layers * num_directions, batch_size, hidden_size)。
c_n: 最后时刻的输出单元状态,维度与 h_n 相同。
定义 LSTM 层:
import torch.nn as nn
lstm = nn.LSTM(input_size=10, hidden_size=30)
lstm
接下来定义输入,输入数据大小应为 (seq_len,batch_size,input_size):
import torch
seq_len = 15
batch_size = 100
input_size = 10
# 模拟数据
x = torch.randn((seq_len, batch_size, input_size))
在 LSTM 层中输入 x,观察输出:
y, (h, c) = lstm(x)
print(y.shape)
print(h.shape)
print(c.shape)
由于 num_layers 和 num_directions 均为 1,因此 h, c 的第一维度为 num_layers*num_directions = 1。
在处理文本数据进行诸如文本分类等任务时,一般在循环神经网络的基础上加词向量层以及最后的输出层作为整体的神经网络模型,典型构架如下:
class LSTM(nn.Module):
def __init__(self, embedding_dim, hidden_dim, vocab_size, output_size):
super(LSTM, self).__init__()
self.embed = nn.Embedding(vocab_size, embedding_dim) # 词向量层
self.lstm = nn.LSTM(embedding_dim, hidden_dim) # LSTM 层
# 输出分类层, output_size 为类别大小
self.fc = nn.Linear(hidden_dim, output_size)
def forward(self, x):
# x: [seq_len, batch_size]
embeds = self.embed(x) # 经由词向量层
# embeds: [seq_len, batch_size, embedding_dim]
lstm_out, _ = self.lstm(embeds) # 经同由 LSTM 层
# lstm_out: [seq_len, batch_size, hidden_dim]
y = self.fc(lstm_out[-1]) # 取最后一步的输出进入最后的分类层
# y: [batch_size, output_size]
return y
初始化 LSTM 模型:
EMBEDDING_DIM = 128 # 词向量的大小为 128
HIDDEN_DIM = 216 # 隐层大小为 216
VOCAB_DIM = 1000 # 词典大小为 1000
OUTPUT_SIZE = 3 # 输出类别为 3 类
my_lstm = LSTM(EMBEDDING_DIM, HIDDEN_DIM, VOCAB_DIM, OUTPUT_SIZE)
my_lstm
定义输入,查看输出:
# seq_len 为 3,batch_size 为 2,各数字表示某单词的 id
x = torch.tensor([[1, 4, 5, 5], [3, 4, 9, 5]])
# x 大小为 [batch_size, seq_len],需要转置
y = my_lstm(x.permute(1, 0))
y.shape
输出大小为 2*3,即 batch_size * output_size。