【聊天机器人】Tensorflow构建探索式聊天机器人项目——Ubuntu数据集

本文将介绍和实现一个基于检索模型的聊天机器人。主要创新点在于采用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输出预测概率

三、项目总结

以上,我们实现了一个基于检索的深度学习模型,它能够对候选的回复进行预测和打分,通过输出分值最高(或者满足一定阈值)的候选回复已完成聊天的过程。后续可以尝试其他更好的模型,或者通过调参来取得更好的实验结果。

参考文献:本文为网易云课堂《自然语言处理》课程笔记

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值