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

自动问答简介

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

enter image description here

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

  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 群技术对话,也看得出程序员们都很努力,整体想的都是学习。

enter image description here

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

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

前面,我们在第09课《一网打尽神经序列模型之 RNN 及其变种 LSTM、GRU》中讲了序列数据处理模型,从 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 负责将输入序列压缩成指定长度的向量,这个向量就可以看成是这个序列的语义,这个过程称为编码,如下图,获取语义向量最简单的方式就是直接将最后一个输入的隐状态作为语义向量。也可以对最后一个隐含状态做一个变换得到语义向量,还可以将输入序列的所有隐含状态做一个变换得到语义变量。

enter image description here

具体理论知识这里不再赘述,下面重点看看,如何通过 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

3. 模型预测。

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

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 例子

如有侵权请联系QQ:758230255删除

你可能感兴趣的:(第14课:动手制作自己的简易聊天机器人)