Python深度学习实例--利用循环神经网络进行IMDB电影评论分类(二分类问题)

Python深度学习实例--利用循环神经网络进行IMDB电影评论分类(二分类问题)

1. 循环神经网络的初步用法

与其他所有神经网络一样,深度学习模型不会接收原始文本作为输入,它只能处理数值张量。
文本向量化(vectorize)是指将文本转换为数值张量的过程。它有多种实现方法,本文将介绍两种主要方法:one-hot编码word embedding本节将介绍如何使用这些方法,将原始文本转化为可以输入到Keras网络中的Numpy张量。

1.1 单词的one-hot编码

在之前的例子中,使用的是编写的代码对电影评论进行one-hot,这次我们使用Keras的内置函数对原始文本数据进行单词级的one-hot编码。内置函数可以实现许多功能,比如从文本中去除特殊字符、只考虑数据中前N个最常见的单词,下面通过一个简短的例子来进行操作。

from keras.preprocessing.text import Tokenizer     #创建一个分词器
samples = ['The cat sat on the mat.', 'The dog ate my homework.']     #定义两句话
tokenizer = Tokenizer(num_words=1000)     #设置仅考虑前1000个最常见的单词
tokenizer.fit_on_texts(samples)     #构建单词索引
sequences = tokenizer.texts_to_sequences(samples)     #将字符串转换为整数索引组成的列表
sequences

输出:

[[1, 2, 3, 4, 1, 5], [1, 6, 7, 8, 9]]
one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary')
one_hot_results    #结果
one_hot_results.shape
word_index = tokenizer.word_index
print('Found %s unique tokens.' % len(word_index))   #单词索引的长度

输出:

array([[0., 1., 1., ..., 0., 0., 0.],
       [0., 1., 0., ..., 0., 0., 0.]])
(2, 1000)
Found 9 unique tokens.

上面这种做法有一个缺点:如果一个句子中相同单词的数量太多会无法处理,可以使用one-hot编码的一种变体one-hot hashing trick。这种方法没有为每个单词分配一个索引并将这些索引保存在一个字典中,而是将单词散列编码为固定长度的向量,通常用一个非常简单的散列函数来实现。这种方法的主要优点在于,它避免了维护一个显式的单词索引,从而节省内存并允许数据的在线编码。

samples = ['The cat sat on the mat.', 'The dog ate my homework.']
dimensionality = 1000    #将单词保存为长度为 1000 的向量。如果单词数量接近 1000 个,那么会遇到很多散列冲突,这会降低这种编码方法的准确性。
max_length = 10
results = np.zeros((len(samples), max_length, dimensionality))
for i, sample in enumerate(samples):
    for j, word in list(enumerate(sample.split()))[:max_length]:
        index = abs(hash(word)) % dimensionality 
        results[i, j, index] = 1.
results.shape

输出:

(2, 10, 1000)

解释一下这部分代码

sample.split()
list(enumerate(sample.split()))
abs(hash(word))    #可以自己查看一下hash函数的作用

输出:

['The', 'dog', 'ate', 'my', 'homework.']
[(0, 'The'), (1, 'dog'), (2, 'ate'), (3, 'my'), (4, 'homework.')]
1054555716951036781

1.2 word embedding

one-hot 编码得到的向量是二进制的、稀疏的(绝大部分元素都是 0)、维度很高的(维度大小等于词表中的单词个数),而词嵌入是低维的浮点数向量(即密集向量,与稀疏向量相对),如下图所示。与 one-hot 编码得到的词向量不同,词嵌入是从数据中学习得到的。因此,在任务中偏向于使用word embedding。
在这里插入图片描述
获取词嵌入有两种方法,一种是利用embedding层学习,一种是利用预先训练的数据库,然后将其加载到模型中,下面主要介绍一下第一种方法。
要将一个词与一个密集向量相关联,最简单的方法就是随机选择向量。这种方法的问题在于,得到的嵌入空间没有任何结构。例如,accurate 和 exact 两个词的嵌入可能完全不同,尽管它们在大多数句子里都是可以互换的 a。深度神经网络很难对这种杂乱的、非结构化的嵌入空间进行学习。一般来说,任意两个词向量之间的几何距离(比如 L2 距离)应该和这两个词的语义距离有关(表示不同事物的词被嵌入到相隔很远的点,而相关的词则更加靠近)。除了距离,你可能还希望嵌入空间中的特定方向也是有意义的。为了更清楚地说明这一点,我们来看一个具体示例。在下图中,四个词被嵌入在二维平面上,这四个词分别是 cat(猫)、dog(狗)、wolf(狼)和 tiger(虎)。对于我们这里选择的向量表示,这些词之间的某些语义关系可以被编码为几何变换。例如,从 cat 到 tiger 的向量与从 dog 到 wolf 的向量相等,这个向量可以被解释为“从宠物到野生动物”向量。同样,从 dog 到 cat 的向量与从 wolf 到 tiger 的向量也相等,它可以被解释为“从犬科到猫科”向量。
在这里插入图片描述
由于某些语义关系的重要性因任务而异。因此,合理的做法是对每个新任务都学习一个新的嵌入空间。我们要做的就是学习一个层的权重,这个层就是Embedding。

from keras.layers import Embedding
#embedding_layer = Embedding(1000, 64)  #Embedding 层至少需要两个参数:标记的个数(这里是 1000,即最大单词索引 +1)和嵌入的维度(这里是 64)

Embedding 层的输入是一个二维整数张量,其形状为 (samples, sequence_length),每个元素是一个整数序列。它能够嵌入长度可变的序列,例如,对于前一个例子中的Embedding 层,你可以输入形状为 (32, 10)(32 个长度为 10 的序列组成的批量)。Embedding 层返回一个形状为 (samples, sequence_length, embedding_dimensionality) 的三维浮点数张量。然后可以用 RNN 层或一维卷积层来处理这个三维张量。将一个 Embedding 层实例化时,它的权重(即标记向量的内部字典)最开始是随机的,与其他层一样。在训练过程中,利用反向传播来逐渐调节这些词向量,改变空间结构以便下游模型可以利用。下面我们在IMDB数据中,实践一下这个做法。

#加载 IMDB 数据,准备用于 Embedding 层
from keras.datasets import imdb
from keras.layers import preprocessing 
max_features = 10000    #作为特征的单词个数
maxlen = 20    #在这么多单词后截断文本(这些单词都属于前 max_features 个最常见的单词)
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)  #将数据加载为整数列表
x_train = preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)  #将整数列表转换成形状为(samples, maxlen) 的二维整数张量
x_test = preprocessing.sequence.pad_sequences(x_test, maxlen=maxlen) #x_train和x_test的形状为(25000,20),其为embedding的输入
#在 IMDB 数据上使用 Embedding 层和分类器
from keras.models import Sequential 
from keras.layers import Flatten, Dense, Embedding
model = Sequential()
model.add(Embedding(10000, 8, input_length=maxlen)) #Embedding 层输出的形状为 (25000, 20, 8),网络将对每个词都学习一个8维embedding
model.add(Flatten()) 
model.add(Dense(1, activation='sigmoid')) 
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc']) 
model.summary()
history = model.fit(x_train, y_train, 
                    epochs=10,
                    batch_size=32, 
                    validation_split=0.2)

得到的验证精度约为 76%左右,考虑到仅查看每条评论的前 20 个单词,这个结果还是相当不错的。但请注意,仅仅将嵌入序列展开并在上面训练一个 Dense 层,会导致模型对输入序列中的每个单词单独处理,而没有考虑单词之间的关系和句子结构。更好的做法是在嵌入序列上添加循环层或一维卷积层,将每个序列作为整体来学习特征。这也是接下来的重点。

1.3 理解并初步使用循环神经网络

卷积神经网络有一个缺点,没有记忆历史信息的能力。卷积神经网络单独处理每个输入,输入与输入之间并没有保存任何历史信息。对于这样的网络处理如果要时间序列数据,例如人体行为识别任务,需要将人体行为识别任务的数据集转换为向量数据并一次性输入卷积神经网络中,因此该类网络称为前馈网络(Feedforward Network)。
为了更好的处理序列性数据,研究人员仿照人类阅读文本时逐步的向后阅读,同时记住之前的内容的模式,设计出了循环神经网络。人类通过此种形式以渐进的模式处理信息,从而达到动态理解文本含义的目的。循环神经网络(Recurrent Neural Networks,RNN)采用同样的原理,其处理序列性数据的模式是遍历所有序列数据,并保存一个状态(State),状态中包含与已查看内容相关的历史信息。目前循环神经网络在深度学习领域已经成为了处理时间序列问题的常用方法,主要用来解决机器翻译、智能问答等序列性的问题。在1997年,Hochreiter[37]等人提出长短时记忆神经网络(Long Short Term Memory Networks,LSTM)来解决梯度消失的问题,LSTM比SimpleRNN有着更强大的功能,其作为循环神经网络的变体,引入了输入门、遗忘门和输出门,通过强大的反馈机制避免了梯度消失和梯度爆炸问题。LSTM既能够处理类似于图像的这类数据,又能对视频和语音这类时间序列数据进行处理。

重新准备IMDB数据

from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 10000  # 作为特征的单词个数
maxlen = 500  # 在这么多单词后截断文本(这些单词都属于前 max_features 个最常见的单词)
batch_size = 32

print('Loading data...')
(input_train, y_train), (input_test, y_test) = imdb.load_data(num_words=max_features)
print(len(input_train), 'train sequences')
print(len(input_test), 'test sequences')

print('Pad sequences (samples x time)')
input_train = sequence.pad_sequences(input_train, maxlen=maxlen)
input_test = sequence.pad_sequences(input_test, maxlen=maxlen)
print('input_train shape:', input_train.shape)
print('input_test shape:', input_test.shape)

输出:

25000 train sequences
25000 test sequences
Pad sequences (samples x time)
input_train shape: (25000, 500)
input_test shape: (25000, 500)

使用 LSTM 层来创建一个模型,然后在 IMDB 数据上训练模型。只需指定LSTM 层的输出维度,其他所有参数(有很多)都使用 Keras 默认值。Keras 具有很好的默认值,无须手动调参,模型通常也能正常运行。

from keras.layers import LSTM

model = Sequential()
model.add(Embedding(max_features, 32))
model.add(LSTM(32))
model.add(Dense(1, activation='sigmoid'))
model.summary()
model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['acc'])
history = model.fit(input_train, y_train,
                    epochs=10,
                    batch_size=128,
                    validation_split=0.2)

输出:
在这里插入图片描述

#画出训练结果图
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(len(acc))
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()

在这里插入图片描述
验证精度达到了 89%,但对于一种计算量如此之大的方法而言,这个结果也说不上是突破性的。主要原因在于,适用于评论分析全局的长期性结构(这正是 LSTM 所擅长的),对情感分析问题帮助不大。对于这样的基本问题,观察每条评论中出现了哪些词及其出现频率就可以很好地解决。这也正是第一个全连接方法的做法。但还有更加困难的自然语言处理问题,特别是问答和机器翻译,这时 LSTM 的优势就明显了,下一节将介绍循环神经网络的高级用法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值