NLP:循环网络的记忆功能

异步社区官方博客

尽管一个循环神经网络需要学习的权重(参数)可能相对较少,但是从图8-12中可以看出,训练一个循环神经网络的代价高昂,尤其是对于较长的序列(如10个词条)。我们拥有的词条越多,每个时刻误差必须反向传播的时间越长。而对于每一时刻,都有更多的导数需要计算。虽然循环神经网络的效果并不比其他网络的效果差,但是请准备好用计算机的排气扇给房子供暖吧。

撇开新的供热能源不谈,我们已经给了神经网络一个基本的记忆能力,但是当它们(指网络时刻)变深,更多的麻烦也出现了(一个也可以在常规的前馈网络中看到的问题)。梯度消失问题(vanishing gradient problem)有一个推论:梯度爆炸问题(exploding gradient problem),它们的思想是,随着网络变得更深(更多层)时,误差信号会随着梯度的每一次计算消散或增长。

循环神经网络也面临着同样的问题,因为在数学上,时刻的每一次后退都相当于将一个误差反向传播到前馈网络的前一层。但是这里更糟!尽管由于这个原因,大多数前馈网络往往只有几层深,但是当我们要处理的是5个、10个,甚至数百个词条的序列时,要深入到100层网络的底层还是很困难的。不过,一个让我们可以继续工作、减轻压力的因素在于:尽管梯度可能会在计算最后一次权重集的过程中消失或爆炸,但是实际上我们只更新了一次权重集,并且每个时刻的权重集都是相同的。仍然有些信息会传递出去,虽然它可能不是我们认为所能创建的理想记忆状态,但是不必害怕,研究人员正在研究这个问题,对于这个挑战在下一章我们会有一些答案。

听了如此多令人郁闷的坏消息,现在我们来看一些魔法吧。

8.1.5 利用Keras实现循环神经网络

我们将从与上一章中所使用的相同的数据集和预处理开始。首先,加载数据集,获取标签并随机打乱样本,然后对文档分词并使用谷歌的Word2vec模型使其向量化,接下来,获取标签,最后我们按80/20的比例将原始数据分成训练集和测试集。

首先,我们需要导入数据处理和循环神经网络训练所需的所有模块,如代码清单8-1所示。

代码清单8-1 导入所有模块

>>> import glob
>>> import os
>>> from random import shuffle
>>> from nltk.tokenize import TreebankWordTokenizer
>>> from nlpia.loaders import get_data
>>> word_vectors = get_data('wv')

然后,我们可以构建数据预处理模块,它能对数据进行训练前的处理,如代码清单8-2所示。

代码清单8-2 数据预处理模块

>>> def pre_process_data(filepath):
...     """
...     Load pos and neg examples from separate dirs then shuffle them
...     together.
...     """
...     positive_path = os.path.join(filepath, 'pos')
...     negative_path = os.path.join(filepath, 'neg')
...     pos_label = 1
...     neg_label = 0
...     dataset = []
...     for filename in glob.glob(os.path.join(positive_path, '*.txt')):
...         with open(filename, 'r') as f:
...             dataset.append((pos_label, f.read()))
...     for filename in glob.glob(os.path.join(negative_path, '*.txt')):
...         with open(filename, 'r') as f:
...             dataset.append((neg_label, f.read()))
...     shuffle(dataset)
...     return dataset

与之前一样,我们可以将数据分词和向量化的方法写在一个函数中,如代码清单8-3所示。

代码清单8-3 数据分词和向量化

>>> def tokenize_and_vectorize(dataset):
...     tokenizer = TreebankWordTokenizer()
...     vectorized_data = []
...     for sample in dataset:
...         tokens = tokenizer.tokenize(sample[1])
...         sample_vecs = []
...         for token in tokens:
...             try:
...                 sample_vecs.append(word_vectors[token])
...             except KeyError:
...                 pass  ⇽--- 在谷歌w2v词汇表中没有匹配的词条
...         vectorized_data.append(sample_vecs)
...     return vectorized_data

并且我们需要将目标变量提取(解压)到单独的(但对应的)样本中,如代码清单8-4所示。

代码清单8-4 目标变量解压缩

>>> def collect_expected(dataset):
...      """ Peel off the target values from the dataset """
...      expected = []
...      for sample in dataset:
...          expected.append(sample[0])
...      return expected

既然我们已经写好了所有的预处理函数,就需要在数据上运行它们,如代码清单8-5所示。

代码清单8-5 加载和准备数据

>>> dataset = pre_process_data('./aclimdb/train')
>>> vectorized_data = tokenize_and_vectorize(dataset)
>>> expected = collect_expected(dataset)
>>> split_point = int(len(vectorized_data) * .8)   ⇽--- 按80/20的比例划分为训练集和测试集(不用混洗)
>>> x_train = vectorized_data[:split_point]
>>> y_train = expected[:split_point]
>>> x_test = vectorized_data[split_point:]
>>> y_test = expected[split_point:]

我们将为这个模型使用相同的超参数:每个样本使用400个词条,批大小为32。词向量是300维,我们将让它运行2个周期。具体做法参见代码清单8-6。

代码清单8-6 初始化网络参数

>>> maxlen = 400
>>> batch_size = 32
>>> embedding_dims = 300
>>> epochs = 2

接下来,我们需要再次填充和截断样本。通常我们不需要对循环神经网络使用填充或截断,因为它们可以处理任意长度的输入序列。但是,在接下来的几个步骤中,我们将看到所使用的模型要求输入指定长度的序列。具体做法参见代码清单8-7。

代码清单8-7 加载测试数据和训练数据

>>> import numpy as np

>>> x_train = pad_trunc(x_train, maxlen)
>>> x_test = pad_trunc(x_test, maxlen)

>>> x_train = np.reshape(x_train, (len(x_train), maxlen, embedding_dims)) 
>>> y_train = np.array(y_train)
>>> x_test = np.reshape(x_test, (len(x_test), maxlen, embedding_dims)) 
>>> y_test = np.array(y_test)

现在我们已经获得了数据,是时候构建模型了。我们将再次从Keras的一个标准的分层模型Sequential()(分层的)模型开始,如代码清单8-8所示。

代码清单8-8 初始化一个空的Keras网络

>>> from keras.models import Sequential
>>> from keras.layers import Dense, Dropout, Flatten, SimpleRNN
>>> num_neurons = 50
>>> model = Sequential()

然后,和之前一样,神奇的Keras处理了组装神经网络的各个复杂环节:我们只需要将想要的循环层添加到我们的网络中,如代码清单8-9所示。

代码清单8-9 添加一个循环层

>>> model.add(SimpleRNN(
...    num_neurons, return_sequences=True,
...    input_shape=(maxlen, embedding_dims)))

现在,基础模块已经搭建完毕,可以接收各个输入并将其传递到一个简单的循环神经网络中(不简单的版本将在下一章介绍),对于每个词条,将它们的输出集合到一个向量中。因为我们的序列有400个词条长,并且使用了50个隐藏层神经元,所以这一层的输出将是一个400个元素的向量,其中每个元素都是一个50个元素的向量,每个神经元对应着一个输出。

注意这里的关键字参数return_sequences。它会告诉网络每个时刻都要返回网络输出,因此有400个向量,每个向量为50维。如果return_sequences被设置为False(Keras的默认行为),那么只会返回最后一个时刻的50维向量。

在本例中,50个神经元的选择是任意的,主要是为了减少计算时间。我们用这个数字做实验,来看看它是如何影响计算时间和模型精确率的。

提示  

一个好的经验法则是尽量使模型不要比训练的数据更复杂。说起来容易做起来难,但是这个想法为我们在数据集上做实验时的调参提供了一个基本法则。较复杂的模型对训练数据过拟合,泛化效果不佳;过于简单的模型对训练数据欠拟合,而且对于新数据也没有太多有意义的内容。我们会看到这个讨论被称为偏差与方差的权衡。对数据过拟合的模型具有高方差和低偏差,而欠拟合的模型恰恰相反:低方差和高偏差;它会用一致的方式给出答案,结果把一切都搞错了。

注意,我们再次截断并填充了数据,这样做是为了与上一章的CNN例子作比较。但是当使用循环神经网络时,通常不需要使用截断和填充。我们可以提供不同长度的训练数据,并展开网络,直到输入结束,Keras对此会自动处理。问题是,循环层的输出长度会随着输入时刻的变化而变化。4个词条的输入将输出4个元素长的序列。100个词条的序列将产生100个元素长的序列。如果我们需要把它传递到另一个层,即一个期望输入的维度统一的层,那么上述不等长的结果就会出现问题。但在某些情况下,这种不等长的序列输入也是可以接受的,甚至本来就期望如此。但是还是先回到我们这里的分类器,参见代码清单8-10。

代码清单8-10 添加一个dropout层

>>> model.add(Dropout(.2))

>>> model.add(Flatten())
>>> model.add(Dense(1, activation='sigmoid'))

我们要求上述简单的RNN返回完整的序列,但是为了防止过拟合,我们添加了一个Dropout层,在每个输入样本上随机选择输入,使这些输入有20%的概率为零,最后再添加一个分类器。在这种情况下,我们只有一个类:“Yes - Positive Sentiment - 1”或“No - Negative Sentiment - 0”,所以我们选择只有单个神经元(Dense(1))的层并使用sigmoid激活函数。但是该稠密层需要输入一个由n个元素组成的扁平的向量(每个元素都是一个浮点数)。SimpleRNN输出的是一个400个元素长的张量,张量中的每个元素都是50个元素长。但是前馈网络并不关心元素的顺序而只关心输入是否符合网络的需要,所以我们使用Keras提供的一个非常方便的网络层Flatten()将输入从400 × 50的张量扁平化为一个长度为20 000个元素的向量。这就是我们要传递到最后一层用来做分类的向量。实际上,Flatten层是一个映射。这意味着误差将从最后一层反向传播回RNN层的输出,而如前所述,这些反向传播的误差之后将在输出的合适点随时间反向传播。

将循环神经网络层生成的“思想向量”传递到前馈网络中,将不再保留我们努力试图想要包含的输入顺序的关系。但重要的是我们注意到,与词条的序列相关的“学习”发生在RNN层本身;通过随时间反向传播过程中误差的聚合将这种关系编码进了网络中,并将其表示为“思想向量”本身。我们基于思想向量作出的决策,通过分类器,就特定的分类问题向思想向量的“质量”提供反馈。我们可以“评估”我们的思想向量,并以其他方式使用RNN层,更多内容将在下一章讨论。(大家能感觉到我们提到下一章时的兴奋吗?)坚持下去,这些知识对于理解下一部分是至关重要的。

本文摘自《自然语言处理实战》

语言是人类建立共识的基础。人们之间交流的不仅有事实,还有情感。通过语言,人们获得了经验领域之外的知识,并通过分享这些经验来构建理解的过程。通过本书,大家将会深入理解自然语言处理技术的原理,有朝一日可能创建出能通过语言来了解人类的系统。

本书是介绍自然语言处理(NLP)和深度学习的实战书。NLP已成为深度学习的核心应用领域,而深度学习是NLP研究和应用中的必要工具。本书分为3部分:第一部分介绍NLP基础,包括分词、TF-IDF向量化以及从词频向量到语义向量的转换;第二部分讲述深度学习,包含神经网络、词向量、卷积神经网络(CNN)、循环神经网络(RNN)、长短期记忆(LSTM)网络、序列到序列建模和注意力机制等基本的深度学习模型和方法;第三部分介绍实战方面的内容,包括信息提取、问答系统、人机对话等真实世界系统的模型构建、性能挑战以及应对方法。

本书面向中高级Python开发人员,兼具基础理论与编程实战,是现代NLP领域从业者的实用参考书。