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

自动问答简介

自动聊天机器人,也称为自动问答系统,由于所使用的场景不同,叫法也不一样。自动问答(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种色调。

  1. 训练自己的语料。

本次使用的语料来自 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 制作中文聊天机器人

前面,我们讲了序列数据处理模型,从 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
  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 例子
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容