从头实现深度学习的对话系统–简单chatbot代码实现
本文的代码都可以到我的github中下载:https://github.com/lc222/seq2seq_chatbot
预训练好的模型可以到我的百度云网盘中下载:
链接:https://pan.baidu.com/s/1hrNxaSk 密码:d2sn
前面几篇文章我们已经介绍了seq2seq模型的理论知识,并且从tensorflow源码层面解析了其实现原理,本篇文章我们会聚焦于如何调用tf提供的seq2seq的API,实现一个简单的chatbot对话系统。这里先给出几个参考的博客和代码:
- tensorflow官网API指导
- Chatbots with Seq2Seq Learn to build a chatbot using TensorFlow
- DeepQA
- Neural_Conversation_Models
经过一番调查发现网上现在大部分chatbot的代码都是基于1.0版本之前的tf实现的,而且都是从tf官方指导文档nmt上进行迁移和改进,所以基本上大同小异,但是在实际使用过程中会发现一个问题,由于tf版本之间的兼容问题导致这些代码在新版本的tf中无法正常运行,常见的几个问题主要是:
- seq2seq API从tf.nn迁移到了tf.contrib.legacy_seq2seq;
- rnn目前也大都使用tf.contrib.rnn下面的RNNCell;
- embedding_attention_seq2seq函数中调用deepcopy(cell)这个函数经常会爆出
TypeError: can't pickle _thread.lock objects
关于上面第三个错误这里多说几句,因为确实困扰了我很久,基本上我在网上找到的每一份代码都会有这个错(DeepQA除外)。首先来讲一种最简单的方法是将tf版本换成1.0.0,这样问题就解决了。
然后说下不想改tf版本的办法,我在网上找了很久,自己也尝试着去找bug所在,错误定位在embedding_attention_seq2seq函数中调用deepcopy函数,于是就有人尝试着把deepcopy改成copy,或者干脆不进行copy直接让encoder和decoder使用相同参数的RNNcell,但这明显是不正确的做法。我先想出了一种解决方案就是将embedding_attention_seq2seq的传入参数中的cell改成两个,分别是encoder_cell和decoder_cell,然后这两个cell分别使用下面代码进行初始化:
encoCell = tf.contrib.rnn.MultiRNNCell([create_rnn_cell() for _ in range(num_layers)],)
decoCell = tf.contrib.rnn.MultiRNNCell([create_rnn_cell() for _ in range(num_layers)],)
这样做不需要调用deepcopy函数对cell进行复制了,问题也就解决了,但是在模型构建的时候速度会比较慢,我猜测是因为需要构造两份RNN模型,但是最后训练的时候发现速度也很慢,就先放弃了这种做法。
然后我又分析了一下代码,发现问题并不是单纯的出现在embedding_attention_seq2seq这个函数,而是在调用module_with_buckets的时候会构建很多个不同bucket的seq2seq模型,这就导致了embedding_attention_seq2seq会被重复调用很多次,后来经过测试发现确实是这里出现的问题,因为即便不使用model_with_buckets函数,我们自己为每个bucket构建模型时同样也会报错,但是如果只有一个bucket也就是只调用一次embedding_attention_seq2seq函数时就不会报错,其具体的内部原理我现在还没有搞清楚,就看两个最简单的例子:
import tensorflow as tf
import copy
cell = tf.contrib.rnn.BasicLSTMCell(10)
cell1 = copy.deepcopy(cell)#这句代码不会报错,可以正常执行
a = tf.constant([1,2,3,4,5])
b = copy.deepcopy(a)#这句代码会报错,就是can't pickle _thread.lock objects。可以理解为a已经有值了,而且是tf内部类型,导致运行时出错???还是不太理解tf内部运行机制,为什么cell没有线程锁,但是a有呢==
所以先忽视原因,只看解决方案的话就是,不适用buckets构建模型,而是简单的将所有序列都padding到统一长度,然后直接调用一次embedding_attention_seq2seq函数构建模型即可,这样是不会抱错的。(希望看到这的同学如果对这里比较理解可以指点一二,或者互相探讨一下)
最后我也是采用的这种方案,综合了别人的代码实现了一个embedding+attention+beam_search等多种功能的seq2seq模型,训练一个基础版本的chatbot对话机器人,tf的版本是1.4。写这份代码的目的一方面是为了让自己对tf的API接口的使用方法更熟悉,另一方面是因为网上的一些代码都很繁杂,想DeepQA这种,里面会有很多个文件还实现了前端,然后各种封装,显得很复杂,不适合新手入门,所以就想写一个跟textcnn相似风格的代码,只包含四个文件,代码读起来也比较友好。接下来就让我们看一下具体的代码实现吧。最终的代码我会放在github上
数据处理
这里我们借用DeepQA里面数据处理部分的代码,省去从原始本文文件构造对话的过程直接使用其生成的dataset-cornell-length10-filter1-vocabSize40000.pkl文件。有了该文件之后数据处理的代码就精简了很多,主要包括:
- 读取数据的函数loadDataset()
- 根据数据创建batches的函数getBatches()和createBatch()
- 预测时将用户输入的句子转化成batch的函数sentence2enco()
具体的代码含义在注释中都有详细的介绍,这里就不赘述了,见下面的代码:
padToken, goToken, eosToken, unknownToken = 0, 1, 2, 3
class Batch:
#batch类,里面包含了encoder输入,decoder输入,decoder标签,decoder样本长度mask
def __init__(self):
self.encoderSeqs = []
self.decoderSeqs = []
self.targetSeqs = []
self.weights = []
def loadDataset(filename):
'