NLP第13课:动手制作自己的简易聊天机器人

自动问答简介

自动聊天机器人,也称为自动问答系统,由于所使用的场景不同,叫法也不一样。自动问答(Question Answering,QA)是指利用计算机自动回答用户所提出的问题以满足用户知识需求的任务。不同于现有搜索引擎,问答系统是信息服务的一种高级形式,系统返回用户的不再是基于关键词匹配排序的文档列表,而是精准的自然语言答案。近年来,随着人工智能的飞速发展,自动问答已经成为倍受关注且发展前景广泛的研究方向。


v2-825997cb3b5c8a587066b2430a7b9a77_b.jpg


自动问答主要研究的内容和关键科学问题如下:

  1. 问句理解:给定用户问题,自动问答首先需要理解用户所提问题。用户问句的语义理解包含词法分析、句法分析、语义分析等多项关键技术,需要从文本的多个维度理解其中包含的语义内容。
  2. 文本信息抽取:自动问答系统需要在已有语料库、知识库或问答库中匹配相关的信息,并抽取出相应的答案。
  3. 知识推理:自动问答中,由于语料库、知识库和问答库本身的覆盖度有限,并不是所有问题都能直接找到答案。这就需要在已有的知识体系中,通过知识推理的手段获取这些隐含的答案。

纵观自动问答研究的发展态势和技术现状,以下研究方向或问题将可能成为未来整个领域和行业重点关注的方向:基于深度学习的端到端自动问答,多领域、多语言的自动问答,面向问答的深度推理,篇章阅读理解、对话等。

基于 Chatterbot 制作中文聊天机器人

ChatterBot 是一个构建在 Python 上,基于一系列规则和机器学习算法完成的聊天机器人,具有结构清晰,可扩展性好,简单实用的特点。

Chatterbot 安装有两种方式:

  • 使用 pip install chatterbot 安装;
  • 直接在 Github Chatterbot 下载这个项目,通过 python setup.py install 安装,其中 examples 文件夹中包含几个例子,可以根据例子加深自己的理解。

安装过程如果出现错误,主要是需要安装这些依赖库:

        chatterbot-corpus>=1.1,<1.2
mathparse>=0.1,<0.2
nltk>=3.2,<4.0
pymongo>=3.3,<4.0
python-dateutil>=2.6,<2.7
python-twitter>=3.0,<4.0
sqlalchemy>=1.2,<1.3
pint>=0.8.1
      

1. 手动设置一点语料,体验基于规则的聊天机器人回答。

        from chatterbot import ChatBot
from chatterbot.trainers import ListTrainer
Chinese_bot = ChatBot("Training demo") #创建一个新的实例
Chinese_bot.set_trainer(ListTrainer)
Chinese_bot.train([
    '亲,在吗?',
    '亲,在呢',
    '这件衣服的号码大小标准吗?',
    '亲,标准呢,请放心下单吧。',
    '有红色的吗?',
    '有呢,目前有白红蓝3种色调。',
])
      

下面进行测试:

        # 测试一下
question = '亲,在吗'
print(question)
response = Chinese_bot.get_response(question)
print(response)
print("\n")
question = '有红色的吗?'
print(question)
response = Chinese_bot.get_response(question)
print(response)
      

从得到的结果可以看出,这应该完全是基于规则的判断:

亲,在吗

亲,在呢

有红色的吗?

有呢,目前有白红蓝3种色调。 2. 训练自己的语料。

本次使用的语料来自 QQ 群的聊天记录,导出的 QQ 聊天记录稍微处理一下即可使用,整个过程如下。

(1)首先载入语料,第二行代码主要是想把每句话后面的换行 \n 去掉。

        lines = open("QQ.txt","r",encoding='gbk').readlines()
sec = [ line.strip() for line in lines]
      

(2)接下来就可以训练模型了,由于整个语料比较大,训练过程也比较耗时。

        from chatterbot import ChatBot
from chatterbot.trainers import ListTrainer
Chinese_bot = ChatBot("Training")
Chinese_bot.set_trainer(ListTrainer)
Chinese_bot.train(sec)
      

这里需要注意,如果训练过程很慢,可以在第一步中加入如下代码,即只取前1000条进行训练:

        sec = sec[0:1000]
      

(3)最后,对训练好的模型进行测试,可见训练数据是 QQ 群技术对话,也看得出程序员们都很努力,整体想的都是学习。

v2-60729856babb7f1acd68ee3a70cc8304_b.jpg


以上只是简单的 Chatterbot 演示,如果想看更好的应用,推荐看官方文档。

基于 Seq2Seq 制作中文聊天机器人

前面,我们讲了序列数据处理模型,从 N-gram 语言模型到 RNN 及其变种。这里我们讲另外一个基于深度学习的 Seq2Seq 模型。

从 RNN 结构说起,根据输出和输入序列不同数量 RNN ,可以有多种不同的结构,不同结构自然就有不同的引用场合。

  • One To One 结构,仅仅只是简单的给一个输入得到一个输出,此处并未体现序列的特征,例如图像分类场景。
  • One To Many 结构,给一个输入得到一系列输出,这种结构可用于生产图片描述的场景。
  • Many To One 结构,给一系列输入得到一个输出,这种结构可用于文本情感分析,对一些列的文本输入进行分类,看是消极还是积极情感。
  • Many To Many 结构,给一系列输入得到一系列输出,这种结构可用于翻译或聊天对话场景,将输入的文本转换成另外一系列文本。
  • 同步 Many To Many 结构,它是经典的 RNN 结构,前一输入的状态会带到下一个状态中,而且每个输入都会对应一个输出,我们最熟悉的应用场景是字符预测,同样也可以用于视频分类,对视频的帧打标签。

在 Many To Many 的两种模型中,第四和第五种是有差异的,经典 RNN 结构的输入和输出序列必须要等长,它的应用场景也比较有限。而第四种,输入和输出序列可以不等长,这种模型便是 Seq2Seq 模型,即 Sequence to Sequence。它实现了从一个序列到另外一个序列的转换,比如 Google 曾用 Seq2Seq 模型加 Attention 模型实现了翻译功能,类似的还可以实现聊天机器人对话模型。经典的 RNN 模型固定了输入序列和输出序列的大小,而 Seq2Seq 模型则突破了该限制。

Seq2Seq 属于 Encoder-Decoder 结构,这里看看常见的 Encoder-Decoder 结构。基本思想就是利用两个 RNN,一个 RNN 作为 Encoder,另一个 RNN 作为 Decoder。Encoder 负责将输入序列压缩成指定长度的向量,这个向量就可以看成是这个序列的语义,这个过程称为编码,如下图,获取语义向量最简单的方式就是直接将最后一个输入的隐状态作为语义向量。也可以对最后一个隐含状态做一个变换得到语义向量,还可以将输入序列的所有隐含状态做一个变换得到语义变量。


v2-4b1152a70e23d9a43f8e33010d55dc69_b.jpg


具体理论知识这里不再赘述,下面重点看看,如何通过 Keras 实现一个 LSTM_Seq2Seq 自动问答机器人。

1. 语料准备。

语料我们使用 Tab 键 \t 把问题和答案区分,每一对为一行。其中,语料为爬虫爬取的工程机械网站的问答。

2. 模型构建和训练。

第一步,引入需要的包:

        from keras.models import Model
from keras.layers import Input, LSTM, Dense
import numpy as np
import pandas as pd
      

第二步,定义模型超参数、迭代次数、语料路径:

        #Batch size 的大小
batch_size = 32  
# 迭代次数epochs
epochs = 100
# 编码空间的维度Latent dimensionality 
latent_dim = 256  
# 要训练的样本数
num_samples = 5000 
#设置语料的路径
data_path = 'D://nlp//ch13//files.txt'
      

第三步,把语料向量化:

        #把数据向量话
input_texts = []
target_texts = []
input_characters = set()
target_characters = set()

with open(data_path, 'r', encoding='utf-8') as f:
    lines = f.read().split('\n')
for line in lines[: min(num_samples, len(lines) - 1)]:
    #print(line)
    input_text, target_text = line.split('\t')
    # We use "tab" as the "start sequence" character
    # for the targets, and "\n" as "end sequence" character.
    target_text = target_text[0:100]
    target_text = '\t' + target_text + '\n'
    input_texts.append(input_text)
    target_texts.append(target_text)

    for char in input_text:
        if char not in input_characters:
            input_characters.add(char)
    for char in target_text:
        if char not in target_characters:
            target_characters.add(char)

input_characters = sorted(list(input_characters))
target_characters = sorted(list(target_characters))
num_encoder_tokens = len(input_characters)
num_decoder_tokens = len(target_characters)
max_encoder_seq_length = max([len(txt) for txt in input_texts])
max_decoder_seq_length = max([len(txt) for txt in target_texts])

print('Number of samples:', len(input_texts))
print('Number of unique input tokens:', num_encoder_tokens)
print('Number of unique output tokens:', num_decoder_tokens)
print('Max sequence length for inputs:', max_encoder_seq_length)
print('Max sequence length for outputs:', max_decoder_seq_length)

input_token_index = dict(
    [(char, i) for i, char in enumerate(input_characters)])
target_token_index = dict(
    [(char, i) for i, char in enumerate(target_characters)])

encoder_input_data = np.zeros(
    (len(input_texts), max_encoder_seq_length, num_encoder_tokens),dtype='float32')
decoder_input_data = np.zeros(
    (len(input_texts), max_decoder_seq_length, num_decoder_tokens),dtype='float32')
decoder_target_data = np.zeros(
    (len(input_texts), max_decoder_seq_length, num_decoder_tokens),dtype='float32')

for i, (input_text, target_text) in enumerate(zip(input_texts, target_texts)):
    for t, char in enumerate(input_text):
        encoder_input_data[i, t, input_token_index[char]] = 1.
    for t, char in enumerate(target_text):
        # decoder_target_data is ahead of decoder_input_data by one timestep
        decoder_input_data[i, t, target_token_index[char]] = 1.
        if t > 0:
            # decoder_target_data will be ahead by one timestep
            # and will not include the start character.
            decoder_target_data[i, t - 1, target_token_index[char]] = 1.
      

第四步,LSTM_Seq2Seq 模型定义、训练和保存:

        encoder_inputs = Input(shape=(None, num_encoder_tokens))
encoder = LSTM(latent_dim, return_state=True)
encoder_outputs, state_h, state_c = encoder(encoder_inputs)
# 输出 `encoder_outputs` 
encoder_states = [state_h, state_c]

# 状态 `encoder_states` 
decoder_inputs = Input(shape=(None, num_decoder_tokens))
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs,
                       initial_state=encoder_states)
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)

# 定义模型
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)

# 训练
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')
model.fit([encoder_input_data, decoder_input_data], decoder_target_data,
          batch_size=batch_size,
          epochs=epochs,
          validation_split=0.2)
# 保存模型
model.save('s2s.h5')
      

第五步,Seq2Seq 的 Encoder 操作:

        encoder_model = Model(encoder_inputs, encoder_states)

decoder_state_input_h = Input(shape=(latent_dim,))
decoder_state_input_c = Input(shape=(latent_dim,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]
decoder_outputs, state_h, state_c = decoder_lstm(
    decoder_inputs, initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)
decoder_model = Model(
    [decoder_inputs] + decoder_states_inputs,
    [decoder_outputs] + decoder_states)
      

第六步,把索引和分词转成序列:

        reverse_input_char_index = dict(
    (i, char) for char, i in input_token_index.items())
reverse_target_char_index = dict(
    (i, char) for char, i in target_token_index.items())
      

第七步,定义预测函数,先使用预模型预测,然后编码成汉字结果:

        def decode_sequence(input_seq):
    # Encode the input as state vectors.
    states_value = encoder_model.predict(input_seq)
    #print(states_value)

    # Generate empty target sequence of length 1.
    target_seq = np.zeros((1, 1, num_decoder_tokens))
    # Populate the first character of target sequence with the start character.
    target_seq[0, 0, target_token_index['\t']] = 1.

    # Sampling loop for a batch of sequences
    # (to simplify, here we assume a batch of size 1).
    stop_condition = False
    decoded_sentence = ''
    while not stop_condition:
        output_tokens, h, c = decoder_model.predict(
            [target_seq] + states_value)

        # Sample a token
        sampled_token_index = np.argmax(output_tokens[0, -1, :])
        sampled_char = reverse_target_char_index[sampled_token_index]
        decoded_sentence += sampled_char
        if (sampled_char == '\n' or
           len(decoded_sentence) > max_decoder_seq_length):
            stop_condition = True

        # Update the target sequence (of length 1).
        target_seq = np.zeros((1, 1, num_decoder_tokens))
        target_seq[0, 0, sampled_token_index] = 1.
        # 更新状态
        states_value = [h, c]
    return decoded_sentence
      
  1. 模型预测。

首先,定义一个预测函数:

        def predict_ans(question):
        inseq = np.zeros((len(question), max_encoder_seq_length, num_encoder_tokens),dtype='float16')
        decoded_sentence = decode_sequence(inseq)
        return decoded_sentence
      

然后就可以预测了:

        print('Decoded sentence:', predict_ans("挖机履带掉了怎么装上去"))
      

总结

本文我们首先基于 Chatterbot 制作了中文聊天机器人,并用 QQ 群对话语料自己尝试训练。然后通过 LSTM 和 Seq2Seq 模型,根据爬取的语料,训练了一个自动问答的模型,通过以上两种方式,我们们对自动问答有了一个简单的入门。

参考文献及推荐阅读:

  1. 《中文信息处理发展报告(2016)》
  2. ChatterBot 文档
  3. ChatterBot 的 GitHub
  4. Sutskever, Vinyals and Le (2014)
  5. 漫谈四种神经网络序列解码模型
  6. 怎样导出 QQ 群里的所有聊天记录?
  7. Keras 中的 LSTM_Seq2Seq 例子
版权声明:本文为liujian197905187511原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/liujian197905187511/article/details/104815544

智能推荐

【机器学习基础】线性回归

                                                        &nbs...

08-Vue实现书籍购物车案例

书籍购物车案例 index.html main.js style.css 1.内容讲解 写一个table和thead,tbody中每一个tr都用来遍历data变量中的books列表。 结果如下: 在thead中加上购买数量和操作,并在对应的tbody中加入对应的按钮。结果如下: 为每个+和-按钮添加事件,将index作为参数传入,并判断当数量为1时,按钮-不可点击。 结果如下: 为每个移除按钮添加...

堆排序

堆排序就是利用堆进行排序的方法,基本思想是,将代排序列构造成一个大根堆,此时整个序列的最大值就是堆顶的根节点。将它与堆数组的末尾元素交换,此时末尾元素就是最大值,移除末尾元素,然后将剩余n-1个元素重新构造成一个大根堆,堆顶元素为次大元素,再次与末尾元素交换,再移除,如此反复进行,便得到一个有序序列。 (大根堆为每一个父节点都大于两个子节点的堆) 上面思想的实现还要解决两个问题: 1.如何由一个无...

基础知识(变量类型和计算)

一、值类型 常见的有:number、string、Boolean、undefined、Symbol 二、引用类型 常用的有:object、Array、null(指针指向为空)、function 两者的区别: 值类型暂用空间小,所以存放在栈中,赋值时互不干扰,所以b还是100 引用类型暂用空间大,所以存放在堆中,赋值的时候b是引用了和a一样的内存地址,所以a改变了b也跟着改变,b和a相等 如图: 值...

猜你喜欢

Codeforces 1342 C. Yet Another Counting Problem(找规律)

题意: [l,r][l,r][l,r] 范围内多少个数满足 (x%b)%a!=(x%a)%b(x \% b) \% a != (x \% a) \% b(x%b)%a!=(x%a)%b。 一般这种题没什么思路就打表找一下规律。 7 8 9 10 11 12 13 14 15 16 17 18 19 20 28 29 30 31 32 33 34 35 36 37 38 39 40 41 49 50...

[笔记]飞浆PaddlePaddle-百度架构师手把手带你零基础实践深度学习-21日学习打卡(Day 3)

[笔记]飞浆PaddlePaddle-百度架构师手把手带你零基础实践深度学习-21日学习打卡(Day 3) (Credit: https://gitee.com/paddlepaddle/Paddle/raw/develop/doc/imgs/logo.png) MNIST数据集 MNIST数据集可以认为是学习机器学习的“hello world”。最早出现在1998年LeC...

哈希数据结构和代码实现

主要结构体: 实现插入、删除、查找、扩容、冲突解决等接口,用于理解哈希这种数据结构 完整代码参见github: https://github.com/jinxiang1224/cpp/tree/master/DataStruct_Algorithm/hash...

解决Ubuntu中解压zip文件(提取到此处)中文乱码问题

在Ubuntu系统下,解压zip文件时,使用右键--提取到此处,得到的文件内部文件名中文出现乱码。 导致此问题出现的原因一般为未下载相应的字体。 解决方案: 在终端中使用unar命令。 需要注意的是系统需要包含unar命令,如果没有,采用如下的方式解决: 实例效果展示: 直接提取到此处: 使用 unar filename.zip得到的文件...

centos7安装mysql8.0.20单机版详细教程

mysql8.0之后与5.7存在着很大的差异,这些差异不仅仅表现在功能和性能上,还表现在基础操作和设置上。这给一些熟悉mysql5.7的小伙伴带来了很多困扰,下面我们就来详细介绍下8.0的安装和配置过程。 mysql在linux上的多种安装方式: 1.yum安装 由于centos默认的yum源中没有mysql,所以我们要使用yum安装mysql就必须自己指定mysql的yum源。在官网下载mysq...