在之前的教程中,我们介绍了卷积神经网络(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.loadtrain_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)
【RNN|基于递归神经网络(RNN)的口语理解(SLU)】让我们打印输出一个例句和其对应的标注看看:
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 208776210354058 234 1376211 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 12648 1263599 126 126 12678 12614 126 12612]
Keras模型 接下来我们定义
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 Convolution1Dmodel = 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 = 30for i in range(n_epochs):
print("Training epoch {}".format(i))bar = progressbar.ProgressBar(max_value=https://www.it610.com/article/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 conllevallabels_pred_val = []bar = progressbar.ProgressBar(max_value=https://www.it610.com/article/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
模型,都可以尝试。参考
- GrégoireMesnil,Xiaodong He,Li Deng和Yoshua Bengio。 递归神经网络结构及其在口语理解中的学习方法的研究 Interspeech,2013. pdf
- 使用单词嵌入的递归神经网络, theano教程
推荐阅读
- paddle|动手从头实现LSTM
- 人工智能|干货!人体姿态估计与运动预测
- 推荐系统论文进阶|CTR预估 论文精读(十一)--Deep Interest Evolution Network(DIEN)
- Python专栏|数据分析的常规流程
- 读书笔记|《白话大数据和机器学习》学习笔记1
- Pytorch学习|sklearn-SVM 模型保存、交叉验证与网格搜索
- Python机器学习基础与进阶|Python机器学习--集成学习算法--XGBoost算法
- 深度学习|深度学习笔记总结
- 机器学习|机器学习Sklearn学习总结
- 机器学习|线性回归原理与python实现