在之前的教程中,我们介绍了卷积神经网络(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
)。
这是一个样本例句以及对应的标注,你可以看到标签是以IOB
(In 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
的技术要点:
x1,x2,...,xt−1,xt,xt+1...
:RNN
的分时间步的序列输入。st
:第t
步时RNN
的隐藏状态 。 根据t−1
步的隐藏状态和当前的输入来计算第t
步的状态,即 st=f(Uxt+Wst−1)
。 这里的f
是一个像tanh
或relu
之类的非线性激活函数。ot
: 第t
步的输出 。 计算公式为ot=f(Vst)
U,V,W
: RNN
要学习的参数。在我们要解决的问题中,将使用单词嵌入向量的序列作为输入传递给RNN
。
我们已经定义好了要解决的问题,并且理解了这些基本组成部分,现在可以来编写实现代码了。
由于我们使用IOB
方式进行序列标注,因此要计算模型的输出分值并不是简单的事情。 我们使用conlleval脚本来计算F1得分 。 我调整了这个代码,以便进行数据预处理和分值计算。 完整的代码在GitHub上。
$ git clone https://github.com/chsasank/ATIS.keras.git
$ cd ATIS.keras
我建议你使用jupyter notebook
来运行并试用教程中的代码片段:
$ jupyter notebook
我们使用data.load.atisfull()
来加载数据。 它会在第一次运行时下载数据。 单词和标注都使用其词汇表的索引进行编码。 词表保存在w2idx
和labels2idx
中 :
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
有现成的用于单词嵌入的神经网络层(embedding layer
)。 它需要整数索引。 SimpleRNN
就是是前面提到的递归神经网络层。 我们必须使用TimeDistributed
来把RNN
第t
步的输出 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
数据集的口语理解。 我们也尝试了使用卷积层来改进模型。
为了进一步改进模型,我们可以尝试使用基于大型语料库(例如维基百科)学习到的单词嵌入向量。 此外,还有像LSTM
或GRU
这样的改进RNN
模型,都可以尝试。
原文:Keras Tutorial - Spoken Language Understanding