美文网首页
聊天机器人框架Chatterbot的使用与魔改(下)

聊天机器人框架Chatterbot的使用与魔改(下)

作者: pamisu | 来源:发表于2021-11-09 12:53 被阅读0次

    上一篇 我们安装好了Chatterbot并做了中文适配,然后发现它不太能满足问答型的需求,训练数据中的问句也可能作为回复输出(机器人:你问我,我还想问问你呢)。在Chatterbot的设计中,训练基于对话(Conversation),一个对话通常由一系列相关联的语句组成,比如:

    conversation = [
        '早上好,二队长。',
        '早上好呀。',
        '这么早上哪儿去啊?',
        '别提了,宿舍马桶又堵了,通马桶去呢。',
        '愿马桶精灵保佑你。',
    ]
    

    训练后,对话中的语句将由上而下两两关联存储在数据库中。只要用户输入的句子能与训练数据匹配,就会给出相关联的回复(绿字是用户输入):

    最后一句话“愿马桶精灵保佑你。”没有关联的语句,会给出相似或随机的回复,学习功能开启时,也可能回复用户说过的话。

    在这种场景下它的表现还是比较不错的,但在某些场景,比如我们的机器人就是二队长,她通常是回答的那一方,不可能说出“早上好,二队长”、“愿马桶精灵保佑你”之类的语句;另外我们还希望某些回答只与特定的问题关联,不进入随机回复的选择范围,比如—“今晚的星空格外地璀璨,你觉得呢?”—“就像你一样美丽”。针对这些问题我们需要修改Chatterbot的训练与回复逻辑。

    如果对上下文支持要求较高,比如客服机器人,主打任务型对话的RASA可能是更好的选择,或者直接使用一些现成的机器人API。

    接下来的修改可能偏离了Chatterbot的设计理念,现在看来也不是特别好,总之能解决上面提到的问题,并且能兼容默认的基于对话的训练。

    训练部分修改

    对于训练部分,要修改的有两点:

    1. 标记问句,并且在一组训练数据中,只有第一句为问句,其他句子是这句话的候选回答
    2. 标记特殊回答,特殊回答不会进入随机回复的选择范围

    比如下面的训练数据:

    [
        '这么早去哪儿啊?',
        '别提了,宿舍马桶又堵了,通马桶去呢。',
        '去执行任务。-s',
        '没事,我溜达。'
    ]
    

    我们定义第一句为问句,其他语句都是对“这么早去哪儿啊?”的可能回复,同时用“-s”结尾标记特殊回答。

    为了标记出问句与特殊回答,直接在数据库中加两个字段是最方便的,可以先看看目前训练后的数据在数据库中的存储格式:

    text: 语句
    search_text: 语句通过tagger.get_bigram_pair_string处理后的检索文本
    conversation: 所属对话
    persona: 所属机器人
    in_response_to: 关联的语句
    search_in_response_to: 关联语句的检索文本
    

    所以可以增加“is_question”字段来区别是否为问句,“is_special”字段来区别是否为特殊回答。
    顺带一提,分词逻辑非常完美地理解错了“这么早上哪儿去啊”的意思,但用户输入“去哪儿啊”时依然可以根据文本相似度匹配上。

    数据对应的实体类为Statement,位于源码的conversation.py文件中,如果要加字段,可能就不得不修改源码了,这非常不优雅。冷静下来之后想到,增加的字段目前只会在数据存取时用到,所以和实体类Statement没有多大关系,不修改它也是可以的。

    Trainer

    可以冻手了,先来写自定义的问答型Trainer类,训练逻辑与默认的ListTrainer没有太大区别。在mybot模块下新建trainers.py:

    编写类QATrainer继承ListTrainer,照抄并修改train方法:

    trainers.py

    import re
    from chatterbot.conversation import Statement
    from chatterbot.trainers import ListTrainer
    from chatterbot.utils import print_progress_bar
    
    
    class QATrainer(ListTrainer):
        def train(self, conversation):
            # 修改:问句的文本与检索文本
            question_statement_text = None
            question_statement_search_text = ''
            # 修改end
    
            statements_to_create = []
    
            conversation_len = len(conversation)
            for conversation_count, text in enumerate(conversation):
                if not text.strip():
                    continue
                if self.show_training_progress:
                    print_progress_bar(
                        'QA Trainer',
                        conversation_count + 1, conversation_len
                    )
    
                # 修改:-s结尾的语句标记为特殊语句,特殊语句不出现在随机回复中
                is_special = False
                if re.match(r'.*-s$', text):
                    text = re.sub(r'-s', '', text)
                    is_special = True
                    print('Special statement: ' + text)
    
                statement_search_text = self.chatbot.storage.tagger.get_bigram_pair_string(text)
                # 如果没有分词结果,是问句则结束并提示错误,是回答则跳过
                if not statement_search_text.strip():
                    if conversation_count == 0:
                        self.chatbot.logger.error(f'Question has no search text, abort: {text}')
                        return
                    else:
                        self.chatbot.logger.info(f'Answer has no search text, skip: {text}')
                        continue
                # 修改end
    
                # 修改:增加字段,回答与问句相关联
                statement = Statement(
                    text=text,
                    search_text=statement_search_text,
                    in_response_to=question_statement_text,
                    search_in_response_to=question_statement_search_text,
                    conversation='training')
                # statement序列化需要向extra_statement_field_names增加额外字段
                statement.extra_statement_field_names.append('is_question')
                statement.extra_statement_field_names.append('is_special')
                # 额外字段赋值
                statement.is_question = conversation_len > 1 and conversation_count == 0
                statement.is_special = is_special
                # 修改end
                statement = self.get_preprocessed_statement(statement)
    
                # 修改:记录问句的文本与检索文本
                if conversation_count == 0:
                    question_statement_text = statement.text
                    question_statement_search_text = statement_search_text
                # 修改end
    
                statements_to_create.append(statement)
    
            self.chatbot.storage.create_many(statements_to_create)
    

    在QATrainer的train方法中,我们完成了问句与特殊回答的标记,将标记作为额外字段添加到了statement中。此时如果调用train方法可以在数据库中看到记录中新增的字段:

    StorageAdapter

    顺便解决上一篇提到的问题三:重复运行训练代码,数据库中的训练数据会重复添加。在上面的train方法最后,调用了storage的create_many方法来将处理好的训练数据存储到数据库,修改这个create_many,在中间加入对重复数据的判断:

    storage_adapter.py

    ...
    class MyMongoDatabaseAdapter(MongoDatabaseAdapter):
        ...
        def create_many(self, statements):
            """
            存储多个实体
            """
            create_statements = []
    
            for statement in statements:
                statement_data = statement.serialize()
                tag_data = list(set(statement_data.pop('tags', [])))
                statement_data['tags'] = tag_data
    
                if not statement.search_text:
                    statement_data['search_text'] = self.tagger.get_bigram_pair_string(statement.text)
    
                if not statement.search_in_response_to and statement.in_response_to:
                    statement_data['search_in_response_to'] = self.tagger.get_bigram_pair_string(statement.in_response_to)
    
                # 修改: 为避免重复,判断是否已经存在,若存在则跳过
                exists = self.statements.find_one({
                    'text': statement_data['text'],
                    'search_text': statement_data['search_text'],
                    'conversation': statement_data['conversation'],
                    'persona': statement_data['persona'],
                    'in_response_to': statement_data['in_response_to'],
                    'search_in_response_to': statement_data['search_in_response_to'],
                })
                if exists:
                    print(f'Statement already exists, skip: {statement_data["text"]}')
                    continue
                # 修改end
                create_statements.append(statement_data)
    
            if len(create_statements) > 0:
                self.statements.insert_many(create_statements)
    

    回复部分修改

    对于回复部分,我们需要在回复的处理过程中,对训练部分添加的两个额外字段进行判断,将问句与特殊回答排除在随机回复的选择范围外。那么究竟要修改哪里呢?阅读源码后发现,只需要重写StorageAdapter的get_random方法就可以了:

    storage_adapter.py

    class MyMongoDatabaseAdapter(MongoDatabaseAdapter):
        ...
        def get_random(self):
            """
            从数据库中返回随机结果
            """
            # 剔除掉is_question为True的元素
            # 剔除掉is_special为True的元素
            result = self.statements.aggregate([
                {'$match': {'is_question': {'$ne': True}, 'is_special': {'$ne': True}}},
                {'$sample': {'size': 1}}
            ])
            r = self.mongo_to_object(list(result)[0])
            return r
        ...
    

    这里使用MongoDB的聚合操作,过滤掉is_question与is_special为true的记录,然后随机采样一个结果。

    重新训练

    至此我们对Chatterbot的扩展修改就已经完成了,接下来调整一下调用的部分。在根目录下新建一个bot.py,用来统一bot的初始化;新建一个train.py,用来训练;原有的main.py用来测试对话效果:

    把原来创建bot的部分放到bot.py中:

    bot.py

    from chatterbot import ChatBot
    from chatterbot.response_selection import get_random_response
    
    BOT_NAME = '二队长'
    MONGO_URI = 'mongodb://localhost:27017/chatterbot'
    
    
    def new_bot() -> ChatBot:
        return ChatBot(
            BOT_NAME,
            storage_adapter='mybot.MyMongoDatabaseAdapter',
            database_uri=MONGO_URI,
            response_selection_method=get_random_response,  # 存在匹配时从候选回复中随机选取
            read_only=True  # 不许学习
        )
    

    在train.py编写训练代码,改用QATrainer之后,一次train方法的调用训练一组问答。实际使用时建议从文件读取训练数据,维护起来方便一些。

    train.py

    from typing import List
    from bot import new_bot
    from mybot import QATrainer
    
    
    def train_qa(conversations: List[List[str]]):
        bot = new_bot()
        trainer = QATrainer(bot)
        for conversation in conversations:
            trainer.train(conversation)
        print('Training finished.')
    
    
    if __name__ == '__main__':
        conversations = [
            [
                '早上好,二队长。',
                '早上好呀。',
            ],
            [
                '这么早去哪儿啊?',
                '别提了,宿舍马桶又堵了,通马桶去呢。',
                '去执行任务。-s',
                '没事,我溜达。'
            ],
            [
                '愿马桶精灵保佑你。',
                '...不用了',
                '你说的这个马桶精灵...是不是穿着厚厚的装甲?-s'
            ],
            [
                '祝你一路顺利。',
                '谢谢。-s',
            ]
        ]
        train_qa(conversations)
    

    记得清除一下数据库,运行train.py,不出意外的话可以看到如下输出:

    第二条75%并不是没有执行完,而是中途输出调试信息把进度更新打断了。训练完成后数据库中的训练结果:

    调整main.py:

    main.py

    from bot import new_bot
    
    # 获取bot
    chatbot = new_bot()
    # 对话
    while True:
        try:
            user_input = input()
            bot_response = chatbot.get_response(user_input)
            print(bot_response)
        except (KeyboardInterrupt, EOFError, SystemExit):
            break
    

    运行main.py,可以测试对话效果了。

    运行结果:

    其他一些零碎的修改就不过多记录了,总的来说Chatterbot的功能虽然不是很强大,但源码比较容易理解与扩展,从中也能学习到NLP的一些基本流程,还是非常不错的。

    相关文章

      网友评论

          本文标题:聊天机器人框架Chatterbot的使用与魔改(下)

          本文链接:https://www.haomeiwen.com/subject/cxvmzltx.html