最近整理代码发现我们对聊天模块用到的Chatterbot框架做了不少修改与扩展,所以简单记录一下瞎改过程。
Chatterbot是一个开源的聊天机器人框架,使用它可以快速构建出一个简单的闲聊型机器人。但它也存在着一定的局限性,比如鸡肋的学习功能、只能回复固定句子等等。如果只是要实现简单的语句匹配并给出对应回复,那么Chatterbot是还算不错的选择;如果想要做出更具有智能的机器人,那么还是得自己建立模型。
初识
基本用法可以通过官方文档学习,但还是有一些坑,可能会导致入门即放弃。
先来安装:
pip install chatterbot
尽管用了国内源,安装过程依然十分漫长,过程中如果有报错可以暂时忽略,先去喝杯茶或吃个饭,回来说不定就装好了。
在1-7挖了好几吨土之后,总算是安装好了,用官方的示例代码试试看:
main.py
from chatterbot import ChatBot
from chatterbot.trainers import ListTrainer
# 创建你的机器人
chatbot = ChatBot('正义骑士号')
# 训练你的机器人
# 这是训练对话
conversation = [
"Hello",
"Hi there!",
"How are you doing?",
"I'm doing great.",
"That is good to hear",
"Thank you.",
"You're welcome.",
"Never gonna give you up",
"Never gonna let you down"
]
trainer = ListTrainer(chatbot)
trainer.train(conversation)
# 跟你的机器人对话
response = chatbot.get_response('Good morning!')
print(response)
从这段示例中可以看出Chatterbot的基本使用流程:定义机器人->训练->使用。
运行时可能会提示还缺少依赖,我这里提示少了pytz,将它装上就行。不出意外的话会输出训练进度与“Good morning!”的回复:
List Trainer: [####################] 100%
How are you doing?
出意外的话则可能会卡在这种地方,或者完全没有输出:
[nltk_data] Downloading package xxx to /Users/你的用户名/nltk_data...
这是由于用于语言处理的NLTK库正在龟速下载所需要的数据集,这种情况建议直接去github或gitee镜像手动下载,github地址,下载完后根据报错提示放到对应路径下即可,缺什么就放什么。这一步不做也没关系,之后需要对中文分词进行修改,暂时没有用到NLTK。
中文适配
如果现在就兴冲冲地投喂中文语料的话,会发现不仅训练时间长、回答相当耗时并且不准确,简直是鸡同鸭讲。这是因为Chatterbot的训练过程中,需要对语句进行分词、词性标注、转换上位词等处理,并存储为检索文本(search_text),但它默认没有实现对中文的分词,导致数据库中存储的都是完整句子而不是检索文本,这里需要自己改造。
训练后的中英文数据对比:
正确数据 错误数据
粗略阅读训练类ListTrainer的源码可以得知,我们的训练数据在这里被处理,并作为Statement被存储起来:
源码中的trainers.py
ListTrainer可以确定改造目标就是get_bigram_pair_string方法,这个方法属于PosHypernymTagger类,而包含tagger的self.chatbot.storage对象为StorageAdapter类型。
那么至少需要扩展两个类:负责分词与词性标注的Tagger、负责存储适配的StorageAdapter。新建mybot模块与类对应的py文件:
Tagger
这里直接参考(抄)了github上fg607的代码 ,感谢这位老哥。
使用结巴中文分词进行分词处理,安装:
pip install jieba
创建data文件夹,放入停用词表文件与用户词典文件:
停用词表包含一些价值较低的、对检索没有帮助的词,比如“的”、“上”、“这个”等等,分词处理时会去除这些词;用户词典包含用户自定义的一些词,会被视为一个完整的词处理。
Tagger类:
taggers.py
import codecs
import os
import re
import jieba
class ChineseTagger(object):
"""
Handling chinese text
"""
def __init__(self, language=None):
self.stopword = []
cfp = codecs.open(os.path.dirname(__file__) + '/data/cn_stopwords.txt', 'r+', 'utf-8') # 停用词的txt文件
for line in cfp:
for word in line.split():
self.stopword.append(word)
cfp.close()
jieba.load_userdict(os.path.dirname(__file__) + '/data/user_dict.txt')
def get_bigram_pair_string(self, text):
"""
Return a string of text containing part-of-speech, lemma pairs.
"""
bigram_pairs = []
# 利用正则表达式去掉一些一些标点符号之类的符号。
text = re.sub(r'\s+', ' ', str(text)) # trans 多空格 to空格
text = re.sub(r'\n+', ' ', str(text)) # trans 换行 to空格
text = re.sub(r'\t+', ' ', str(text)) # trans Tab to空格
text = re.sub("[\s+\.\!\/_,$%^*(+\"\']+|[+——;!,”。《》,。:“?、~@#¥%……&*()1234567①②③④)]+".\
encode().decode("utf8"), "".encode().decode("utf8"), text)
wordlist = list(jieba.cut(str(text))) # jieba.cut 把字符串切割成词并添加至一个列表
for word in wordlist:
if word not in self.stopword: # 词语的清洗:去停用词
if word != '\r\n' and word != ' ' and word != '\u3000'.encode().decode('unicode_escape') \
and word != '\xa0'.encode().decode('unicode_escape'): # 词语的清洗:去全角空格
bigram_pairs.append(word)
return ' '.join(bigram_pairs)
需要注意的是,这里并没有像PosHypernymTagger一样标注词性与转换上位词,不过影响似乎不大(也许)。
StorageAdapter
Tagger写好了,接下来要让它能被训练类ListTrainer引用到,在这二者之间的便是StorageAdapter。Chatterbot提供了设置项,让我们可以扩展自己的存储适配类。默认数据库是SQLite,但我们更习惯使用MongoDB,所以继承MongoDatabaseAdapter:
storage_adapter.py
from chatterbot import languages
from chatterbot.storage import MongoDatabaseAdapter
from mybot.tagging import ChineseTagger
class MyMongoDatabaseAdapter(MongoDatabaseAdapter):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# 设置tagger
self.tagger = ChineseTagger(language=kwargs.get(
'tagger_language', languages.CHI
))
创建时设置数据库地址:
main.py
...
MONGO_URI = 'mongodb://localhost:27017/chatterbot'
# 创建你的机器人
chatbot = ChatBot(
'正义骑士号',
storage_adapter='mybot.MyMongoDatabaseAdapter',
database_uri=MONGO_URI)
...
到这里中文适配就完成了,换上中文训练数据:
main.py
...
# 训练你的机器人
# 这是训练对话
conversation = [
'你好',
'您好,博士。',
'戳戳',
'“滴滴”',
'行动开始!',
'正义号,出发!',
'你会用洗衣机吗?',
'洗衣机也没有想象中那么难用。',
]
...
改成从终端获取用户输入:
main.py
...
# 获取回复
# response = chatbot.get_response('晚上好!')
# print(response)
# 对话
while True:
try:
user_input = input()
bot_response = chatbot.get_response(user_input)
print(bot_response)
except (KeyboardInterrupt, EOFError, SystemExit):
break
...
运行结果:
现在有正常的运行结果了,真是可喜可贺,不过部分对话还是有些奇怪。
一些问题
问题一:对于训练过的句子,正义骑士号能给出对应的答复;没有训练过的句子,则给出随机的答复,但“你会用洗衣机吗?”这样的问句也有可能被选中。
这个问题在了解get_response的整个处理过程后就能知晓原因,get_response内部大致分为以下几个步骤:
- 用户输入语句的预处理
- 用户输入语句处理,生成检索文本(search_text)
- 根据检索文本,在数据库中获取最佳匹配项,如果用户输入了没有训练过的句子,则为空
- 根据最佳匹配项的检索文本,搜索最佳匹配项对应的回复
- 如搜索到对应回复则返回回复内容,如果没有搜索到回复,此时返回预先设置的默认回复,如果没有默认回复,则从整个数据库中随机选取
- 对本次对话进行学习
所以问句也是可能会出现的,如果我们的机器人是偏向问答模式,希望问句仅作为模板,用来匹配用户输入语句,那么这里还需要修改。
问题二:发送“滴滴”,会收到“行动开始!”,发送“您好,博士。”,会收到“戳戳”。
根据训练类ListTrainer的逻辑,同一个列表中的句子会上下关联,也就是“你好”关联“您好,博士。”,“您好,博士。”关联“戳戳”,“戳戳”关联“滴滴”。如果不希望产生这样的关联关系,可以将问句与答句分在一组训练,即每次训练时列表中只保留两个句子。
问题三:重复运行训练代码,数据库中的训练数据会重复添加。
它大概希望我们自己做这个处理。
问题四:到底学了个啥?我也没看出来它哪里在学习。
对话产生后,Chatterbot会向数据库中添加这样的记录,与上一次的回复形成关联:
learn.png嗯,就这样...没有然后了。
问题五:希望某些句子只匹配固定问句,不出现在随机回复的选取范围内。
如果这些问题对于你来说没有什么大碍,那么经过本篇文章的修改后Chatterbot基本可以愉快地使用了,可以拿github上的一些中文语料库训练看看效果。
下一篇将针对这些问题对Chatterbot进行一些魔改,严格来说可能会偏离它原本的设计思想,不过做人嘛,最紧要就是开心。
网友评论