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

1 篇文章 0 订阅
1 篇文章 0 订阅

在之前的教程中,我们介绍了卷积神经网络(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)方式进行编码的:

话语showflightfromBostontoNewYorktoday
标注OOOB-deptOB-arrI-arrB-date

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

我们的解决思路是使用:

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

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

单词嵌入

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

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

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

sundaydeltacaliforniabostonaugusttimecar
wednesdaycontinentalcoloradonashvilleseptemberschedulerental
saturdayunitedfloridatorontojulytimeslimousine
fridayamericanohiochicagojuneschedulesrentals
mondayeasterngeorgiaphoenixdecemberdinnertimecars
tuesdaynorthwestpennsylvaniaclevelandnovemberordtaxi
thursdayusnorthatlantaaprilf28train
wednesdaysnationairtennesseemilwaukeeoctoberlimolimo
saturdayslufthansaminnesotacolumbusjanuarydepartureap
sundaysmidwestmichiganminneapolismaysfolater

递归神经网络

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

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

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

rnn unfold

让我们简要地梳理下关于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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值