基于递归神经网络(RNN)的口语理解(SLU)

在之前的教程中,我们介绍了卷积神经网络(CNN)和keras深度学习框架。 我们用它们解决了一个计算机视觉(CV)问题:交通标志识别。 今天,我们将用keras解决一个自然语言处理(NLP)问题。

问题和数据集

我们要解决的问题是自然语言理解(Natural Language Understanding) 。 它旨在提取话语中的含义。 当然,这仍然是一个未解决的问题。 因此,我们把这个问题分解为一个可以实际解决的问题,即在限定语境中理解话语的含义。 在本教程中,我们要实现的,就是理解人们在询问航班信息时的意图(intent)。

我们要使用的是航空出行信息系统(ATIS)数据集。 这个数据集是由DARPA在90年代初收集的。 ATIS数据集中包括有关航班相关信息的口头查询。 一个样例是I want to go from Boston to Atlanta on Monday 。 口语理解(SLU)的目的就是理解这一意图(intent),然后确定相关参数,如目的地和出发日期 。 这个任务被称为槽填充(slot filling)。

这是一个样本例句以及对应的标注,你可以看到标签是以IOBIn Out Begin)方式进行编码的:

话语 show flight from Boston to New York today
标注 O O O B-dept O B-arr I-arr B-date

ATIS训练集和测试集分别包含4,978 / 893个句子,总共56,590 / 9,198个单词(平均句长为15)。 类(不同的槽)的数量是128,包括O标注(NULL)。 在测试集未出现的单词使用进行编码,每个数字被替换为字符串DIGIT ,即20被转换为DIGITDIGIT

我们的解决思路是使用:

  • 单词嵌入(word embedding
  • 递归神经网络 (recurrent neural network

下面我将简单介绍这两方面的技术要点。

单词嵌入

单词嵌入将一个单词映射为高维空间中的向量(dense vector)。 如果以正确的方式进行训练,这些嵌入向量可以学习到单词的语义和句法信息,即类似的词在高维空间中彼此接近,不相似的词则彼此相距很远。

可以使用大量的文字如维基百科、或针对特定的问题领域来学习这些嵌入向量。 针对ATIS数据集,我们将采取第二个途径。

下面的示例显示了一些词(第一行)在嵌入空间中的最近邻居。 这个嵌入空间是由我们在后面定义的模型学习到的:

sunday delta california boston august time car
wednesday continental colorado nashville september schedule rental
saturday united florida toronto july times limousine
friday american ohio chicago june schedules rentals
monday eastern georgia phoenix december dinnertime cars
tuesday northwest pennsylvania cleveland november ord taxi
thursday us north atlanta april f28 train
wednesdays nationair tennessee milwaukee october limo limo
saturdays lufthansa minnesota columbus january departure ap
sundays midwest michigan minneapolis may sfo later

递归神经网络

卷积层(convolutional layers)是汇聚局部信息的好方法,但是它们并不能真正地捕获数据中包含的先后顺序信息。 递归神经网络(RNN)则可以帮助我们处理像自然语言这样的序列信息。

如果我们要预测当前单词的属性,最好还记得之前出现过的单词。 RNN使用内部隐藏状态(hidden state)来存储了历史序列的概要信息。 这使得我们可以使用RNN来解决复杂的词语标记问题,如词类(part of speech)标注或槽填充(slot filling)。

下图展示了RNN的内部机制:

基于递归神经网络(RNN)的口语理解(SLU)_第1张图片

让我们简要地梳理下关于RNN的技术要点:

  • x1,x2,...,xt−1,xt,xt+1...RNN的分时间步的序列输入。
  • st :第t步时RNN的隐藏状态 。 根据t−1步的隐藏状态和当前的输入来计算第t步的状态,即 st=f(Uxt+Wst−1) 。 这里的f是一个像tanhrelu之类的非线性激活函数。
  • ot: 第t步的输出 。 计算公式为ot=f(Vst)
  • U,V,WRNN要学习的参数。

在我们要解决的问题中,将使用单词嵌入向量的序列作为输入传递给RNN

整合在一起

我们已经定义好了要解决的问题,并且理解了这些基本组成部分,现在可以来编写实现代码了。

由于我们使用IOB方式进行序列标注,因此要计算模型的输出分值并不是简单的事情。 我们使用conlleval脚本来计算F1得分 。 我调整了这个代码,以便进行数据预处理和分值计算。 完整的代码在GitHub上。

$ git clone https://github.com/chsasank/ATIS.keras.git
$ cd ATIS.keras

我建议你使用jupyter notebook来运行并试用教程中的代码片段:

$ jupyter notebook 

加载数据

我们使用data.load.atisfull()来加载数据。 它会在第一次运行时下载数据。 单词和标注都使用其词汇表的索引进行编码。 词表保存在w2idxlabels2idx中 :

import numpy as np
import data.load

train_set, valid_set, dicts = data.load.atisfull()
w2idx, labels2idx = dicts['words2idx'], dicts['labels2idx']

train_x, _, train_label = train_set
val_x, _, val_label = valid_set

# Create index to word/label dicts
idx2w  = {w2idx[k]:k for k in w2idx}
idx2la = {labels2idx[k]:k for k in labels2idx}

# For conlleval script
words_train = [ list(map(lambda x: idx2w[x], w)) for w in train_x]
labels_train = [ list(map(lambda x: idx2la[x], y)) for y in train_label]
words_val = [ list(map(lambda x: idx2w[x], w)) for w in val_x]
labels_val = [ list(map(lambda x: idx2la[x], y)) for y in val_label]

n_classes = len(idx2la)
n_vocab = len(idx2w)

让我们打印输出一个例句和其对应的标注看看:

print("Example sentence : {}".format(words_train[0]))
print("Encoded form: {}".format(train_x[0]))
print()
print("It's label : {}".format(labels_train[0]))
print("Encoded form: {}".format(train_label[0]))

输出:

Example sentence : ['i', 'want', 'to', 'fly', 'from', 'boston', 'at', 'DIGITDIGITDIGIT', 'am', 'and', 'arrive', 'in', 'denver', 'at', 'DIGITDIGITDIGITDIGIT', 'in', 'the', 'morning']
Encoded form: [232 542 502 196 208  77  62  10  35  40  58 234 137  62  11 234 481 321]

It's label : ['O', 'O', 'O', 'O', 'O', 'B-fromloc.city_name', 'O', 'B-depart_time.time', 'I-depart_time.time', 'O', 'O', 'O', 'B-toloc.city_name', 'O', 'B-arrive_time.time', 'O', 'O', 'B-arrive_time.period_of_day']
Encoded form: [126 126 126 126 126  48 126  35  99 126 126 126  78 126  14 126 126  12]

Keras模型

接下来我们定义keras模型。 Keras有现成的用于单词嵌入的神经网络层(embedding layer)。 它需要整数索引。 SimpleRNN就是是前面提到的递归神经网络层。 我们必须使用TimeDistributed来把RNNt步的输出 ot传给一个全连接层(full connected layer)。 否则,只有最后那个时间步的输出被传递到下一层:

from keras.models import Sequential
from keras.layers.embeddings import Embedding
from keras.layers.recurrent import SimpleRNN
from keras.layers.core import Dense, Dropout
from keras.layers.wrappers import TimeDistributed
from keras.layers import Convolution1D

model = Sequential()
model.add(Embedding(n_vocab,100))
model.add(Dropout(0.25))
model.add(SimpleRNN(100,return_sequences=True))
model.add(TimeDistributed(Dense(n_classes, activation='softmax')))
model.compile('rmsprop', 'categorical_crossentropy')

训练

现在,让我们开始训练模型。 我们将把每个句子作为一个批次(batch)传递给模型。 注意,不能使用model.fit(),因为它要求所有的句子具有相同的大小。 因此,我们使用model.train_on_batch()

import progressbar
n_epochs = 30

for i in range(n_epochs):
    print("Training epoch {}".format(i))

    bar = progressbar.ProgressBar(max_value=len(train_x))
    for n_batch, sent in bar(enumerate(train_x)):
        label = train_label[n_batch]
        # Make labels one hot
        label = np.eye(n_classes)[label][np.newaxis,:] 
        # View each sentence as a batch
        sent = sent[np.newaxis,:]

        if sent.shape[1] > 1: #ignore 1 word sentences
            model.train_on_batch(sent, label)

评估模型

为了衡量模型的准确性,我们使用model.predict_on_batch()metrics.accuracy.conlleval()

from metrics.accuracy import conlleval

labels_pred_val = []

bar = progressbar.ProgressBar(max_value=len(val_x))
for n_batch, sent in bar(enumerate(val_x)):
    label = val_label[n_batch]
    label = np.eye(n_classes)[label][np.newaxis,:]
    sent = sent[np.newaxis,:]

    pred = model.predict_on_batch(sent)
    pred = np.argmax(pred,-1)[0]
    labels_pred_val.append(pred)

labels_pred_val = [ list(map(lambda x: idx2la[x], y)) \
                                    for y in labels_pred_val]
con_dict = conlleval(labels_pred_val, labels_val, 
                            words_val, 'measure.txt')

print('Precision = {}, Recall = {}, F1 = {}'.format(
            con_dict['r'], con_dict['p'], con_dict['f1']))

使用这个模型,我得到的F1分值:92.36:

Precision = 92.07, Recall = 92.66, F1 = 92.36 

请注意,为了简洁起见,我没有显示日志(logging)方面的代码。 损失和准确性日志是模型开发的重要部分。 在main.py中的改进模型使用了日志记录。你可以用下面的命令来执行它:

$ python main.py 

模型改进

我们目前的模型,有一个缺点是不能利用未来的信息。 即输出ot仅取决于当前和历史单词,而没有利用旁边的单词。 可以想象,使用序列中的下一个单词,也有助于为预测当前单词的属性提供更多线索。

在调用RNN之前、单词嵌入之后,使用一个卷积层就可以很容易地利用未来的信息:

model = Sequential()
model.add(Embedding(n_vocab,100))
model.add(Convolution1D(128, 5, border_mode='same', activation='relu'))
model.add(Dropout(0.25))
model.add(GRU(100,return_sequences=True))
model.add(TimeDistributed(Dense(n_classes, activation='softmax')))
model.compile('rmsprop', 'categorical_crossentropy')

使用这个改进的模型,我获得了94.90的 F1分值。

收尾

在本教程中,我们学习了有关单词嵌入和RNN的知识,并且将这些知识应用于解决一个具体的NLP问题:ATIS数据集的口语理解。 我们也尝试了使用卷积层来改进模型。

为了进一步改进模型,我们可以尝试使用基于大型语料库(例如维基百科)学习到的单词嵌入向量。 此外,还有像LSTMGRU这样的改进RNN模型,都可以尝试。

参考

  1. GrégoireMesnil,Xiaodong He,Li Deng和Yoshua Bengio。 递归神经网络结构及其在口语理解中的学习方法的研究 Interspeech,2013. pdf
  2. 使用单词嵌入的递归神经网络, theano教程

原文:Keras Tutorial - Spoken Language Understanding

你可能感兴趣的:(RNN,Keras,递归神经网络,口语理解,自然语言理解,机器学习)