My Avatar

Shadow

I love bleak day, like something will happen

Chat Robot

2017年02月08日 星期三, 发表于 北京

如果你对本文有任何的建议或者疑问, 可以在 这里给我提 Issues, 谢谢! :)

  最近在写聊天机器人,就记录下开发聊天机器人的一些基本知识等等的,首先给出一些我看过的不错的文章,我的记录也是在这些前人的基础上丰富起来的。

  自己动手做聊天机器人 这是一个国人自己搭建的blog,这一系列记录了他自己学习搭建聊天机器人的过程,技术栈是python,不过看了下虽然有很多干货,包括代码什么,但是总觉得不够连贯。

  聊天機器人的開發思路 这是一个湾湾程序猿的github博客,在github上他自己有个chatbot的工程,因此他记了些博客,对我也是很有帮助的。

  AIML 一种基于规则的人工智能标记语言

  结巴分词结巴分词

  opencc中文简繁转换(巨难编译,最后搞不定只能用yum install 了老版的opencc)


机器人模型

  聊天机器人大体上分为三种模型:

  下面我们一一来介绍

规则式模型

  所谓规则式模型,是最简单的一种,就是通过设计规则,来让机器人知道,遇到什么词该给出什么样的回答,例如:

1
2
if "天氣" in user.query:
chatbot.say("今天天氣真好")

  而提到规则式模型,就不得不提AIML(Artificial Intelligence Mark-up Language),它是一种人工智能标记语言,最简单的形式如下:

1
2
3
4
5
6
7
8
9
	<aiml version = "1.0.1" encoding = "UTF-8"?>
   		<category>
      		<pattern> HELLO </pattern>
      		<template>
         		WORLD !
      		</template>
   		</category>
	</aiml>

  按照上述规则,机器人在遇见HELLO后,就会回复WORLD。

  AIML以XML文件来定义规则,最重要的几个标签,在上面都有展示:

  当然,AIML还有20多种其余的标签,都有各自的作用,具体可以参考如下文档

  http://www.alicebot.org/TR/2005/WD-aiml/WD-aiml-1.0.1-008.html#section-introduction

  http://www.pandorabots.com/pandora/pics/wallaceaimltutorial.html

  当然,这个AIML只是一种语言,那么如果用它来构建一个机器人,需要一种该语言的解释器,在ALICE的官网上提供了各种语言的AIML解释器,我试用了一下其中的chatterbean,是java版本的。

chatterbean

  按照chatterbean官网介绍,有两个版本下载,一个是binary,一个是source,binary就是两个jar包,而source则包含了所有的java源文件以及alice的aiml文件。

  我首先下了binary版本,先试用一下,打开压缩包后,就两个jar包,一个bsh.jar,一个chatterbean.jar。 官网说我们只要执行:

1
java -cp bsh.jar -jar chatterbean.jar path_to/properties.xml

  其中,path_to/properties.xml是机器人的配置文件,但是官网居然没说这配置文件是干嘛的,需要配什么,只给了个示例,是一个机器人Ifurita的下载,我又把这个机器人下载下来,发现里面确实有properties.xml,内容如下:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  		<comment>Ifurita's configuration properties</comment>
  		<entry key="context">context.xml</entry>
  		<entry key="splitters">splitters.xml</entry>
  		<entry key="substitutions">substitutions.xml</entry>
  		<entry key="categories">./</entry>
</properties>

  Ifurita里面确实也有context.xml、splitters.xml、substitutions.xml文件,那么我们把这些都复制到之前的binary目录下,然后再执行

1
java -cp bsh.jar -jar chatterbean.jar properties.xml

  这时候,发现并没有报错,但是,我们输入任何文字后,会报一个空指针,想了想,我们最关键的aiml文件并没有指定啊,所以肯定报空指针,那么怎么指定我们所要使用的AIML文件呢,其实在properties.xml里面最后不是有一个categories条目吗,这里指定一个目录路径,那么chatterbean就会在该目录下寻找以aiml为文件后缀的文件做为机器人的AIML文件,那么我们将Ifurita机器人的ifurita.xml文件拷贝过来,改为ifurita.aiml,然后再执行上面的java命令,就可以愉快地聊天了,如下:

  那如果我自己再添加一个我自己的AIML文件呢? 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="ISO-8859-1"?>

<aiml>
  	<!-- Categories for saying goodbye. -->
  		<category>
		<pattern>HI</pattern>
		<template>hi there</template>
  		</category>
  		<category>
		<pattern>WHO ARE YOU</pattern>
		<template>I'm shadow</template>
  		</category>
  		<category>
		<pattern>WHERE DO YOU WORK</pattern>
		<template>I work in YonYou</template>
  		</category> 
</aiml>

  其实最开始,我试了中文,发现不生效,原因以及如何让AIML支持中文参考http://blog.csdn.net/zhang_hui_cs/article/details/22686951

  而且,AIML文件中的pattern标签里的英文必须是大写。。。。不然也不生效,这样之后,我们再问它这些问题,就会给出相应回答了

  AIML有一些还是比较有用的标签的,想srai、think、that等等,帮助进行同义转换、上下文理解等等有帮助,这里不详解,自己去上面提供的网址看吧。

检索式模型

  所谓检索式模型,也挺好理解的,它跟搜索引擎挺像的,它有一个问题库,每个问题都有一个回答,这样,当用户输入对话后,就用该句子去问题库中寻找最相似的问题,以此来获得回答。

  所以这种模型的关键在于,如何计算句子之间的相似度。常用的文本相似度计算方法如下:

TF/IDF

  TF/IDF就是词频、逆文档频率,是计算文档相似度的常用算法,当然也有其改进算法OKapi BM25(考虑了文本长度的改进算法),不过在我开发机器人的过程中,并没有使用这种算法,机器人场景下,待计算相似度的文本都是短文本,这种计算方法的准确率并不高。

编辑距离

  编辑距离就是要将句子A改成B,至少需要进行几步操作,这里的操作包括:替换、删除、添加,不过这种方法并没办法考虑到语义的情况,因此在做聊天机器人时,这种算法也不太适用

向量相似度

  向量相似度的计算方法有很多,例如:余弦相似度、jaccard相似性、欧几里得距离、曼哈顿距离等等、那么我们的重点在于如何将文本转为向量,在这方面的研究,提的比较多的就是word2vec以及sentence2vec(doc2vec),这两种算法在gensim的python库中都有实现,原理比较复杂,后面会继续研究,其实单独看一个词,你也许不能知道他们是否相似,所以我们可以结合上下文,来分析单词的语义,上下文语境越相似的词相似度应该是越高的,这就是word2vec的思想,同理doc2vec也一样。

  不过,word2vec和doc2vec都是无监督学习,因此需要大量的语料,才能训练出来比较好的结果。

  关于word2vec的使用,最上面的两篇分享里面都有

  其实,短句子的相似度计算,用word2vec就能达到较好的效果,我们只要获得句子所有词的向量,然后对所有词向量做一个平均,就生成了句向量,然后利用余弦相似度计算句子间的相似度即可,但是这只适用于短句子,长的文档则还是用doc2vec来计算比较合适,因为如果用word2vec会丢失掉很多上下文信息。

  下面就介绍下我用word2vec来计算句子间相似度的过程(基于python):

  1 下载语料库(wiki)

  word2vec是无监督训练,需要大量语料来训练,因此我们选择从wiki上下载需要的wiki语料库,其中有各种语言的,我们选择中文的,然后我下载了20170201的备份,记得下载zhwiki-20170201-pages-articles.xml.bz2,但是这个比较大,我用来做实验,所以下载了下面的那个分段后的第一部分即zhwiki-20170201-pages-articles1.xml.bz2

  2 安装gensim

  gensim是一个很有名的主题模型函数库,里面提供了各种有用的工具,像word2vec、doc2vec、lda等等,有了python环境,直接执行pip3 install –upgrade gensim即可安装,但是后面你会发现有坑的,那是因为通过这种方式安装的gensim,虽然装了numpy库,但是在windows上装这个有个坑,就是你没有装mkl库,因此后面会报错,所以我们需要从下面这个地址去下载numpy‑1.12.0+mkl‑cp34‑cp34m‑win_amd64.whl,然后执行pip3 install numpy‑1.12.0+mkl‑cp34‑cp34m‑win_amd64.whl来完成对numpy+mkl的安装(linux安装numpy和scipy也是各种坑,这两个都是安装gensim包必备的 = =后面详解)

  3 wiki语料xml转为txt文本

  第一步下载下来的wiki语料是xml文档,里面有很多无用的标签,我们需要对它进行处理,来得到纯粹的文本数据,好在gensim已经提前为我们准备了这样的工具,因此我们只要按照下述程序代码,去执行一遍,即可得到处理好的文本文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
	# -*- coding: utf-8 -*-
	import logging
	import sys

	from gensim.corpora import WikiCorpus

	def main():

    	if len(sys.argv) != 2:
        	print("Usage: python " + sys.argv[0] + " wiki_data_path")
        	exit()

    	logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
    	wiki_corpus = WikiCorpus(sys.argv[1], dictionary={})
    
    	texts_num = 0
    
    	with open("wiki_texts.txt",'w',encoding='utf-8') as output:
        	for text in wiki_corpus.get_texts():
            	output.write(b' '.join(text).decode('utf-8') + '\n')
            	texts_num += 1
            	if texts_num % 10000 == 0:
                	logging.info("已处理 %d 篇文章" % texts_num)
	if __name__ == "__main__":
    	main()

  转换后的文本如下:

  4 繁简转换

  从上图可以看出,大量的繁体字出现,如果不处理,那么繁体和简体相同的词会被当做两个词,因此,我们还需要对这个文档进行繁体转简体的处理,繁简转换大家都推荐用opencc,然而,我上主页看了之后,下载了源码,怎么编译都不通过,不管是在window上(我尝试了用MSYS和Visual Studio去编译)还是linux上编译,全都报各种错,试错了一天还是没搞定,后来发现,yum源search了一下opencc,有如下结果:

  虽然是0.4版本的,属于老版,但是能用就好,直接yum install opencc之后,还不行,还得装一个opencc-tools,因此在yum install opencc-tools.x86_64,这样之后,在/usr/share/opencc下会有opencc需要用到的转换的配置文件,我们只要执行:

1
opencc -i input_file -o output_file -c mix2zhs.ini

  就可以将文本转换为简体了,注意配置文件必须是mix2zhs.ini,代表将混合有简体和繁体的文本转换为简体,而不能用zht2zhs.ini(繁体转换为简体),我试了并没有转换成功。这也是老版的问题吧,我觉得,没关系,用mix2zhs就行。

  5 分词

  英文用空格就可以标识每个词,但是中文必须得借助分词工具,来分词,现在开源的分词工具有很多,结巴、word、hanlp等等,这里为了方便,直接使用结巴分词,非常简单,首先pip3 install jieba安装下载结巴分词,然后执行如下python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
	# -*- coding: utf-8 -*-

	import jieba
	import logging

	def main():

    	logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

    	# load stopwords set
    	stopwordset = set()
    	with open('stopwords.txt','r',encoding='utf-8') as sw:
        	for line in sw:
            	stopwordset.add(line.strip('\n'))

    	output = open('wiki_seg.txt','w',encoding='utf-8')
    
    	texts_num = 0
    
    	with open('wiki_texts_sim.txt','r',encoding='utf-8') as content :
        	for line in content:
            	words = jieba.cut(line, cut_all=False)
            	for word in words:
                	if word not in stopwordset:
                    	output.write(word +' ')
            	texts_num += 1
            	if texts_num % 10000 == 0:
                	logging.info("已完成前 %d 行分词" % texts_num)
    	output.close()
    
	if __name__ == '__main__':
		main()

  这段代码,需要注意的是,stopwords.txt是停用词,直接去结巴分词github上就能找到,或者用自己的,无非就是你我他之类的这些词,然后还需要注意的是,在open()文件时,一定要加上encoding=”utf-8”,不然就会报编码之类的错误,所以这点需要注意。分词完后,文件如下:

  6 训练词向量

  这是最关键的一步了,我们需要对文本进行训练,获得每个词的向量表示,然而代码其实很简单,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	# -*- coding: utf-8 -*-

	from gensim.models import word2vec
	import logging

	def main():

    	logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
    
    	sentences = word2vec.Text8Corpus("wiki_seg.txt")
    	model = word2vec.Word2Vec(sentences, size=250)
    	model.save_word2vec_format(u"med250.model.bin", binary=True)

    	# how to load a model ?
    	# model = word2vec.Word2Vec.load_word2vec_format("your_model.bin", binary=True)

	if __name__ == "__main__":
    	main()

  虽然代码很简单,但是内容其实很多,最关键的一行代码是:

1
model = word2vec.Word2Vec(sentences, size=250)

  参数的含义如下:

1
2
3
4
5
6
7
sentences:待训练的句子集
size:這表示的是訓練出的詞向量會有幾維
alpha:機器學習中的學習率,這東西會逐漸收斂到 min_alpha
sg:這個不是三言兩語能說完的,sg=1表示採用skip-gram,sg=0 表示採用cbow
window:窗口参数,往左往右看幾個字的意思,印象中原作者論文裡寫 cbow 採用 5 是不錯的選擇
workers:執行緒數目,除非電腦不錯,不然建議別超過 4
min_count:若這個詞出現的次數小於min_count,那他就不會被視為訓練對象

  但是在训练过程中,说我的电脑上没有c编译器,因此采用的是slow training,如果要用fast training,需要安装c编译器后再重新安装gensim,一开始我以为忍忍吧,就让它慢慢跑得了,结果发现,在windows,没有c编译器的slow training真心太slow,顾及得跑三四天,好吧,还是去linux上再重新来一遍吧。

  首先,安装python3.4,虽然linux上已经有python2.7了,gensim也支持python2.7,但是为了和windows上版本一致,因此还是装一个python3.4吧。不用删除原来的2.7,直接在官网下载一个python的source包,然后解压,之后执行:

1
2
3
./configure
make
make install

  装完后,接着就要进入艰难的安装gensim的步骤了,因为它依赖numpy和scipy,这两个东西安装起来各种坑。。

  按照gensim官方安装指导,我们其实可以直接:

1
easy_install-3.4 -U gensim

  来安装,注意这里是easy_install_3.4,因为我们之前有2.7版本,所以easy_install是2.7版本的。但是这样安装,会一下把gensim依赖的东西一起安装,出错了信息会非常多,不好排查,因此,我们还是按照后面提供的方法,先装numpy和scipy,最后再装gensim

  执行:

1
easy_install-3.4 numpy

  理所当然的报了一堆错。。。不要慌,打开报错的信息一步步排查,最后发现,就是缺少了一些依赖,首先是需要装subversion

1
yum install subversion

  然后,还需要gfortran的编译环境,所以

1
yum install gcc-gfortran

  然而,天不助我,我们内网的源没有这个,好吧,直接去网上搜rpm包,有个网站很赞http://rpm.pbone.net/,直接搜你要的即可,注意gfortran的版本要和你的gcc版本一致。我们下载了:

1
2
gcc-gfortran-4.8.5-4.el7.x86_64.rpm
libquadmath-devel-4.8.5-4.el7.x86_64.rpm

  后面那个是gfortran依赖的包,因此也下载了,然后执行:

1
rpm -i xx.rpm

  安装完毕,基本上就可以正常安装numpy了,如果再报错,你就去看看报的啥错,缺啥装啥吧 = =,然后安装scipy

1
easy_install-3.4 scipy

  scipy安装过程中可能会需要装一个atlas、blas的东西,直接yum源安装即可,需要注意的是,在安装过程中,可能会报很多的warning,我一开始以为出错,后来发现只是warning而已,不要在意。。。。

1
yum install lapack lapack-devel blas blas-devel 

  最后,终于可以装gensim了

1
easy_install-3.4 --upgrade gensim

  如果numpy和scipy装成功了,这一步基本上不会有问题了,然后装个结巴分词

1
pip3 install jieba

  然后,将我们的train.py和分好词的语料,拷贝到linux上,执行

1
python3 train.py

  发现,又出了个之前没出现过的错,ImportError: No module named bz2,google之百度之,解决方法为,首先,安装bzip2-devel

1
yum install bzip2-devel

  然后,对python进行重新编译,cd到python source目录,然后执行:

1
2
make
make install

  然后,终于可以执行train.py了,这时发现,我去,速度快得一比啊,几分钟就训练完了!!训练完得到模型文件:med250.model.bin

  7 计算句子间的相似度

  有了训练好的模型,我们就可以开始计算句子间的相似度,其实就是计算向量间的相似度,向量间的相似度计算方法有很多,我们采用最常见最简单的余弦相似度,执行下面的python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
	# -*- coding: utf-8 -*-

	from gensim.models import word2vec
	import jieba
	import logging
	import sys

	def get_sentence_vector(sentence,model,stopwordset):
		word_vector_list = []
	
		words = jieba.cut(sentence, cut_all=False)
	
		for word in words:
			if word not in stopwordset:
				word_vector_list.append(model[word])
	
		result_vector = [0] * 250
	
		for i in range(250):
			for vector in word_vector_list:
				result_vector[i] += vector[i]
			if(len(word_vector_list)!=0):
				result_vector[i] /= len(word_vector_list)
	
		return result_vector	

	def calc_sim(vector1,vector2):
		dot_product = 0.0  
		normA = 0.0  
		normB = 0.0  
		for a,b in zip(vector1,vector2):  
			dot_product += a*b  
			normA += a**2  
			normB += b**2  
		if normA == 0.0 or normB==0.0:  
			return None  
		else:  
			return dot_product / ((normA*normB)**0.5)  

	def main():

		logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
	
		if(len(sys.argv)!=3):
			logging.info("usage: python " + sys.argv[0] + " sentence1 sentence2")
			exit()

		# load stopwords set
		stopwordset = set()
		with open('stopwords.txt','r',encoding='utf-8') as sw:
			for line in sw:
				stopwordset.add(line.strip('\n'))
			
		model = word2vec.Word2Vec.load_word2vec_format("med250.model.bin", binary=True)
		
		sent1 = sys.argv[1]
		sent2 = sys.argv[2]
	
		vector1 = get_sentence_vector(sent1,model,stopwordset)
		vector2 = get_sentence_vector(sent2,model,stopwordset)

		similarity = calc_sim(vector1,vector2)
	
		print(similarity)

	if __name__ == "__main__":
		main()

  上面的代码主要干了这些事:首先,从传入的参数中读入待比较的两个句子,然后,获取停用词,这里和之前分词用到的停用词一样,然后利用word2vec.Word2Vec.load_word2vec_format来加载模型到内存,这一步如果模型很大会比较慢且占较多内存,不过我们测试的数据量并不大,所以很快,然后通过get_sentence_vector函数,将句子转化为向量,具体做法就是对句子分词,然后去掉停用词,对每个词再获取其词向量,最后对所有这些词向量做个平均。获得两个句子的词向量后,我们就可以通过余弦相似度算法计算他们之间的相似度了

  下面是一些结果展示:

1
2
3
4
5
6
我爱读书 我爱游泳 0.912990954057
我爱读书 你是傻子 0.769530908508
我爱读书 我爱看书 0.954195134156
我爸爸是天底下最好的人 我超级喜欢我爸爸 0.823780526446
北京公积金查询 工作居住证办理 0.439870274941
北京公积金查询 上海公积金提取 0.662318980781

  如果输入句子的分词结果中有模型中不包含的词会报错,当然如果你的训练语料足够丰富,一般是不会出现这种情况的。通过上述结果,我们发现,虽然我们的语料很有限,只用了wiki语料的很少一部分,但训练出的结果还是比较令人满意的。可以预见,如果针对特定场景或领域,我们的训练语料足够丰富的情况下,这样的相似度计算效果是很好的。

主题模型

  每个句子都有自己的主题,利用主题模型来对句子进行分类,获取句子的类型后,在抽取句子特征,最后根据特征,获取回答,这是主题模型的处理流程,这其中涉及到的关键技术在于分类和特征抽取,分类算法有很多,例如最常见的朴素贝叶斯等等,而特征抽取则涉及到实体识别。

  现在很多开源的NLP工具都已经实现了一部分的文本相似度计算算法, 例如word分词、Hanlp等等,我之前就用的hanlp的文本推荐功能来计算句子之间的相似度,hanlp的文本推荐计算相似度是结合了三种相似度算法来计算的,一是语义相似度、一是编辑距离、一是拼音相似度,后来我对这个算法进行了一些修改,以让它来适应机器人短文本相似度计算的特点。

  现在我们的智能聊天机器人,就是一个检索式模型的机器人。它由这样一些组件组成:

  而其中大脑是最核心最重要的组件,它由如下模块组成:

  其中知识模块,就涉及到利用检索模型,去知识库中检索相关知识,并给出回答。

生成式模型

  这种应该是技术含量最高的模型了,但是这种一般是用于日常生活对话的聊天型机器人,如果是针对特定领域的商务型机器人则不会使用这种模型。

  生成式模型我也不太了解,简单谈一谈,貌似自从google发表了Sequence to Sequence Learning with Neural Networks的论文后,大家都开始用这种模型去实现机器人了,像github上就有很多高完成度应用,如DeepQA,这也是图灵后台使用的关键技术之一。Sequence to Sequence 的基本概念是串接兩個 RNN/LSTM,一個當作編碼器,把句子轉換成隱含表示式,另一個當作解碼器,將記憶與目前的輸入做某種處理後再輸出,不過這只是最直觀的方式,其實解碼器還有很多種作法,如果想了解細節與效能上的差異,我推薦這篇文章

  

三种模型就介绍到这里吧,后续再继续研究关于机器人上下文处理、自学习相关方面的内容