本文将介绍和实现一个基于检索模型的聊天机器人。主要创新点在于采用LSTM方式对问题和答案进行文本表示,并计算相似度得分,根据分数的高低进行排序并得到我们选择的最佳回复。检索模型所使用的回复数据通常是预先存储且知道(或定义)的数据,而不像生成式模型那样可以创造出崭新的、未知的回复内容(模型没有见过)。准确来讲,检索式模型的输入是一段上下文内容 C (会话到目前未知的内容信息) 和一个可能作为回复的候选答案;模型的输出是对这个候选答案的打分。寻找最合适的回复内容的过程是:先对一堆候选答案进行打分及排序,最后选出分值最高的那个最为回复。
以下内容会介绍到基于检索的聊天机器人原理,并实现一个基于检索的模型,使用了双层Decoder的LSTM模型,通过这个模型可以实现聊天机器人。
本部分英文原文见deep-learning-for-chatbots-2-retrieval-based-model-tensorflow,本文涉及到的数据和代码见Github仓库地址。
一、技术框架
1、相似度匹配技术点:两层Encoder的LSTM模型的结构图
2、原理解析
(1)将训练集问题输入和回答输入文本进行分词,对每个词进行embedding。初始的词向量使用GloVe vectors,之后词向量随着模型的训练会进行fine-tuned(实验发现,初始的词向量使用GloVe并没有在性能上带来显著的提升)。
(2)问题文本embedding后的向量输入到rnn模型中;初始记忆单元
h
0
h_0
h0可以设为0;将最后输出的记忆单元向量
c
=
h
t
c=h_t
c=ht作为问题文本表示
(3)回答文本embedding后的向量输入到与(2)相同的rnn模型中;初始记忆单元
h
0
h_0
h0可以设为0;将最后输出的记忆单元向量
r
=
h
t
′
r=h_t'
r=ht′作为问题文本表示
(4)将向量c乘以矩阵M,生成一个向量表示,捕捉了Query和Response之间的[语义联系]。如果c为一个256维的向量,M维256*256的矩阵,两者相乘的结果为另一个256维的向量。我们可以将其解释为[一个生成式的回复向量]。矩阵M是需要训练的参数。
(5)将预测生成的回复r’和候选的回复r进行点乘,来表示两者之间的相似程度,点乘结果越大表示候选回复作为回复的可信度越高;之后通过sigmoid函数归一化,转成概率形式。
(5)使用预测概率和真实label进行交叉熵损失计算。采用随机梯度下降对模型进行优化
二、代码解析
1、数据预处理prepare_date.py
prepare_date.py文件主要实现如下操作:
(1)将.csv格式转化为TensorFlow专有的格式。这种格式的好处在于能够直接从输入文件中load tensors,并让TensorFlow来处理洗牌(shuffling)、批量(batching)和队列化(queuing)等操作。
预处理中还包括,TFRecord。
(2)构建词表。创建一个字典库,将词进行标号。
保存文件:vocab_processor.bin\vocabulary.txt
(3)将训练集、评估集、测试集单词转化为词表中对应的标号。文件将直接存储这些词的标号。
保存文件为:train.tfrecords\validation.tfrecordstest.tfrecords
每个实例包括如下几个字段:
- Query:表示为一串词标号的序列,如[231, 2190, 737, 0, 912];
- Query的长度;
- Response:同样是一串词标号的序列;
- Response的长度;
- Label;
- Distractor_[N]:表示负例干扰数据,仅在验证集和测试集中有,N的取值为0-8;
- Distractor_[N]的长度;
2、创建输入函数udc_inputs.py
为了使用TensoFlow内置的训练和评测模块,我们需要创建一个输入函数:这个函数返回输入数据的batch。因为训练数据和测试数据的格式不同,我们需要创建不同的输入函数。为了防止重复书写代码。在工程中会创建统一的函数接口,进行封装,方便调用。函数只需要多输入一个model参数即可。我们创建一个包装器(wrapper),名称为create_input_fn,针对不同的mode使用相应的code,如下
def create_input_fn(mode, input_files, batch_size, num_epochs=None):
def input_fn():
# TODO Load and preprocess data here
return batched_features, labels
return input_fn
udc_inputs.py文件主要实现如下操作:
(1) 定义了示例文件中的feature字段;
(2) 使用tf.TFRecordReader来读取input_files中的数据;
(3) 根据feature字段的定义对数据进行解析;
(4) 提取训练数据的标签;
(5) 产生批量化的训练数据;
(6) 返回批量的特征数据及对应标签(input_fn_train\input_fn_eval);
3、定义评估指标udc_metrics
tf分装好了评估指标
streaming_sparse_recall_at_k增量的计算recall@k。为了使用这些指标,需要创建一个字典,key为指标名称,value为对应的计算函数。如下:
def create_evaluation_metrics():
eval_metrics = {}
for k in [1, 2, 5, 10]:
eval_metrics["recall_at_%d" % k] = functools.partial(
tf.contrib.metrics.streaming_sparse_recall_at_k,
k=k)
return eval_metrics
如上,使用了functools.partial函数,这个函数的输入参数有两个。不要被streaming_sparse_recall_at_k所困惑,其中的streaming的含义是表示指标的计算是增量式的。
训练和测试所使用的评测方式是不一样的,训练过程中我们对每个case可能作为正确回复的概率进行预测,而测试过程中我们对每组数据(包含10个case,其中1个是正确的,另外9个是生成的负例/噪音数据)中的case进行逐条概率预测,得到例如[0.34, 0.11, 0.22, 0.45, 0.01, 0.02, 0.03, 0.08, 0.33, 0.11]这样格式的输出,这些输出值的和并不要求为1(因为是逐条预测的,有单独的预测概率值,在0到1之间)。而对于这组数据而言,因为数据index=0对应的为正确答案,这里recall@1为0(返回最大概率0.45,其对应的index=3,是错误样本);因为0.34是其中第二大的值,所以recall@2是1(表示这组数据中预测概率值在前二的中有一个是正确的)。
4、加载词表和预训练词向量helpers.py
(1)加载词表
(2)加载glove词向量
(3)构建初始embedding矩阵
5、设置超参数udc_hparams.py
封装所有的超参数
6、封装模型udc_model.py
create_model_fn函数用以处理不同格式的训练和测试数据,是对最外层model的封装;它的输入参数为model_impl,这个函数表示实际作出预测的模型。实际的模型构建在dual_encoder里面
7、模型构建-创建两层LSTM:dual_encoder.py
(1)生成embedding矩阵
(2)使用查表的方式将输入单词进行embedding
(3)使用查表的方式将输出单词进行embedding
(4)使用LSTMCell创建lstm基本单元
(5)先将问题输入和答案输入单元进行拼接;过同一个rnn;再将其输出进行拆分。(因为如果分别对问题输入进行rnn,答案输入进行rnn,这样就跑了两遍,需要时间花费的;高效的做法是先拼接)
(6)将问题输出C乘以矩阵M,得到预测答案R’。R’乘以答案输出R,得到预测答案与正确答案之间的相似度。值越大,相似度越高。
(7)sigmoid后得到一个概率;与标准答案进行交叉熵损失计算
(8)所有样本的平均Loss
这里就是用的LSTM,当然你可以替换成任意的其他模型。程序如下:
def dual_encoder_model(
hparams,
mode,
context,
context_len,
utterance,
utterance_len,
targets):
# Initialize embedidngs randomly or with pre-trained vectors if available
embeddings_W = get_embeddings(hparams)
# Embed the context and the utterance
context_embedded = tf.nn.embedding_lookup(
embeddings_W, context, name="embed_context")
utterance_embedded = tf.nn.embedding_lookup(
embeddings_W, utterance, name="embed_utterance")
# Build the RNN
with tf.variable_scope("rnn") as vs:
# We use an LSTM Cell
cell = tf.nn.rnn_cell.LSTMCell(
hparams.rnn_dim,
forget_bias=2.0,
use_peepholes=True,
state_is_tuple=True)
# Run the utterance and context through the RNN
rnn_outputs, rnn_states = tf.nn.dynamic_rnn(
cell,
tf.concat(0, [context_embedded, utterance_embedded]),
sequence_length=tf.concat(0, [context_len, utterance_len]),
dtype=tf.float32)
encoding_context, encoding_utterance = tf.split(0, 2, rnn_states.h)
with tf.variable_scope("prediction") as vs:
M = tf.get_variable("M",
shape=[hparams.rnn_dim, hparams.rnn_dim],
initializer=tf.truncated_normal_initializer())
# "Predict" a response: c * M
generated_response = tf.matmul(encoding_context, M)
generated_response = tf.expand_dims(generated_response, 2)
encoding_utterance = tf.expand_dims(encoding_utterance, 2)
# Dot product between generated response and actual response
# (c * M) * r
logits = tf.batch_matmul(generated_response, encoding_utterance, True)
logits = tf.squeeze(logits, [2])
# Apply sigmoid to convert logits to probabilities
probs = tf.sigmoid(logits)
# Calculate the binary cross-entropy loss
losses = tf.nn.sigmoid_cross_entropy_with_logits(logits, tf.to_float(targets))
# Mean loss across the batch of examples
mean_loss = tf.reduce_mean(losses, name="mean_loss")
return probs, mean_loss
实例化模型,可以用于下文的模型训练中udc_train.py
model_fn = udc_model.create_model_fn(
hparams=hparams,
model_impl=dual_encoder_model)
这样我们就可以直接运行udc_train.py文件,来开始模型的训练和评测了。可以设定–eval_every参数来控制模型在验证集上的评测频率。更多的命令行参数信息可见tf.flags和hparams,你也可以运行python udc_train.py --help来查看。
8、训练程序样例:详见udc_train.py
首先,给一个模型训练和测试的程序样例,这之后你可以参照程序中所用到的标准函数,来快速切换和使用其他的网络模型。假设我们有一个函数model_fn,函数的输入参数有batched features,label和mode(train/evaluation),函数的输出为预测值。
这里创建了一个model_fn的estimator(评估函数);两个输入函数,input_fn_train和input_fn_eval,以及计算评测指标的函数。程序样例如下:
estimator = tf.contrib.learn.Estimator(
model_fn=model_fn,
model_dir=MODEL_DIR,
config=tf.contrib.learn.RunConfig())
#生成训练数据
input_fn_train = udc_inputs.create_input_fn(
mode=tf.contrib.learn.ModeKeys.TRAIN,
input_files=[TRAIN_FILE],
batch_size=hparams.batch_size)
#生成评估数据
input_fn_eval = udc_inputs.create_input_fn(
mode=tf.contrib.learn.ModeKeys.EVAL,
input_files=[VALIDATION_FILE],
batch_size=hparams.eval_batch_size,
num_epochs=1)
#生成评估指标
eval_metrics = udc_metrics.create_evaluation_metrics()
# We need to subclass theis manually for now. The next TF version will
# have support ValidationMonitors with metrics built-in.
# It's already on the master branch.
#模型评估
class EvaluationMonitor(tf.contrib.learn.monitors.EveryN):
def every_n_step_end(self, step, outputs):
self._estimator.evaluate(
input_fn=input_fn_eval,
metrics=eval_metrics,
steps=None)
eval_monitor = EvaluationMonitor(every_n_steps=FLAGS.eval_every)
#模型训练
estimator.fit(input_fn=input_fn_train, steps=None, monitors=[eval_monitor])
训练结果:
INFO:tensorflow:training step 20200, loss = 0.36895 (0.330 sec/batch).
INFO:tensorflow:Step 20201: mean_loss:0 = 0.385877
INFO:tensorflow:training step 20300, loss = 0.25251 (0.338 sec/batch).
INFO:tensorflow:Step 20301: mean_loss:0 = 0.405653
…
INFO:tensorflow:Results after 270 steps (0.248 sec/batch): recall_at_1 = 0.507581018519, recall_at_2 = 0.689699074074, recall_at_5 = 0.913020833333, recall_at_10 = 1.0, loss = 0.5383
…
9、模型测试udc_test
在训练完模型后,可以将其应用在测试集上。得到模型在测试集上的recall@k的结果,注意在使用udc_test.py文件时,需要使用与训练时相同的参数。
在训练模型的次数大约2w次时(在GPU上大约花费1小时),模型在测试集上得到如下的结果:
recall_at_1 = 0.507581018519
recall_at_2 = 0.689699074074
recall_at_5 = 0.913020833333
其中,recall@1的值与tfidf模型的差不多,但是recall@2和recall@5的值则比tfidf模型的结果好太多。原论文中的结果依次是0.55,0.72和0.92,可能通过模型调参或者预处理能够达到这个结果。
10、模型预测udc_predict
# Load your own data here
INPUT_CONTEXT = "Example context"
POTENTIAL_RESPONSES = ["Response 1", "Response 2"]
对于新的数据,可以使用udc_predict.py来进行预测;返回的是一串概率,可以从从候选的回复中,选择预测概率分值最高的那个作为回复。
(1)提取问题特征
(2)提取答案特征
(3)使用Estimator对模型进行封装
(4)estimator.predict输出预测概率
三、项目总结
以上,我们实现了一个基于检索的深度学习模型,它能够对候选的回复进行预测和打分,通过输出分值最高(或者满足一定阈值)的候选回复已完成聊天的过程。后续可以尝试其他更好的模型,或者通过调参来取得更好的实验结果。
参考文献:本文为网易云课堂《自然语言处理》课程笔记