Tensorflow搞一个聊天机器人

catalogue

0. 前言
1. 训练语料库
2. 数据预处理
3. 词汇转向量
4. 训练
5. 聊天机器人 - 验证效果

 

0. 前言

不是搞机器学习算法专业的,3个月前开始补了一些神经网络,卷积,神经网络一大堆基础概念,尼玛,还真有点复杂,不过搞懂这些基本数学概念,再看tensorflow的api和python代码觉得跌跌撞撞竟然能看懂了,背后的意思也能明白一点点

0x1: 模型分类

1. 基于检索的模型 vs. 产生式模型

基于检索的模型(Retrieval-Based Models)有一个预先定义的"回答集(repository)",包含了许多回答(responses),还有一些根据输入的问句和上下文(context),以及用于挑选出合适的回答的启发式规则。这些启发式规则可能是简单的基于规则的表达式匹配,或是相对复杂的机器学习分类器的集成。基于检索的模型不会产生新的文字,它只能从预先定义的"回答集"中挑选出一个较为合适的回答。
产生式模型(Generative Models)不依赖于预先定义的回答集,它会产生一个新的回答。经典的产生式模型是基于机器翻译技术的,只不过不是将一种语言翻译成另一种语言,而是将问句"翻译"成回答(response)

2. 长对话模型 vs. 短对话模型

短对话(Short Conversation)指的是一问一答式的单轮(single turn)对话。举例来说,当机器收到用户的一个提问时,会返回一个合适的回答。对应地,长对话(Long Conversation)指的是你来我往的多轮(multi-turn)对话,例如两个朋友对某个话题交流意见的一段聊天。在这个场景中,需要谈话双方(聊天机器人可能是其中一方)记得双方曾经谈论过什么,这是和短对话的场景的区别之一。现下,机器人客服系统通常是长对话模型

3. 开放话题模型 vs. 封闭话题模型

开放话题(Open Domain)场景下,用户可以说任何内容,不需要是有特定的目的或是意图的询问。人们在Twitter、Reddit等社交网络上的对话形式就是典型的开放话题情景。由于该场景下,可谈论的主题的数量不限,而且需要一些常识作为聊天基础,使得搭建一个这样的聊天机器人变得相对困难。
封闭话题(Closed Domain)场景,又称为目标驱动型(goal-driven),系统致力于解决特定领域的问题,因此可能的询问和回答的数量相对有限。技术客服系统或是购物助手等应用就是封闭话题模型的例子。我们不要求这些系统能够谈论政治,只需要它们能够尽可能有效地解决我们的问题。虽然用户还是可以向这些系统问一些不着边际的问题,但是系统同样可以不着边际地给你回复 ;)

Relevant Link:

http://naturali.io/deeplearning/chatbot/introduction/2016/04/28/chatbot-part1.html
http://blog.topspeedsnail.com/archives/10735/comment-page-1#comment-1161
http://blog.csdn.net/malefactor/article/details/51901115

 

1. 训练语料库

wget https://raw.githubusercontent.com/rustch3n/dgk_lost_conv/master/dgk_shooter_min.conv.zip
解压
unzip dgk_shooter_min.conv.zip

Relevant Link:

https://github.com/rustch3n/dgk_lost_conv

 

2. 数据预处理

一般来说,我们拿到的基础语料库可能是一些电影台词对话,或者是UBUNTU对话语料库(Ubuntu Dialog Corpus),但基本上我们都要完成以下几大步骤

1. 分词(tokenized)
2. 英文单词取词根(stemmed)
3. 英文单词变形的归类(lemmatized)(例如单复数归类)等
4. 此外,例如人名、地名、组织名、URL链接、系统路径等专有名词,我们也可以统一用类型标识符来替代 

M 表示话语,E 表示分割,遇到M就吧当前对话片段加入临时对话集,遇到E就说明遇到一个中断或者交谈双方转换了,一口气吧临时对话集加入convs总对话集,一次加入一个对话集,可以理解为拍电影里面的一个"咔"

convs = []  # conversation set
with open(conv_path, encoding="utf8") as f:
    one_conv = []  # a complete conversation
    for line in f:
        line = line.strip('\n').replace('/', '')
        if line == '':
            continue
        if line[0] == 'E':
            if one_conv:
                convs.append(one_conv)
            one_conv = []
        elif line[0] == 'M':
            one_conv.append(line.split(' ')[1])

因为场景是聊天机器人,影视剧的台词也是一人一句对答的,所以这里需要忽略2种特殊情况,只有一问或者只有一答,以及问和答的数量不一致,即最后一个人问完了没有得到回答

# Grasping calligraphy answer answer
ask = []  # ask
response = []  # answers
for conv in convs:
    if len(conv) == 1:
        continue
    if len(conv) % 2 != 0:
        conv = conv[:-1]
    for i in range(len(conv)):
        if i % 2 == 0:
            ask.append(conv[i])
        else:
            response.append(conv[i])

Relevant Link:

 

3. 词汇转向量

我们知道图像识别、语音识别之所以能率先在深度学习领域取得较大成就,其中一个原因在于这2个领域的原始输入数据本身就带有很强的样本关联性,例如像素权重分布在同一类物体的不同图像中,表现是基本一致的,这本质上也人脑识别同类物体的机制是一样的,即我们常说的"举一反三"能力,我们学过的文字越多,就越可能驾驭甚至能创造组合出新的文字用法,写出华丽的文章

但是NPL或者语义识别领域的输入数据,对话或者叫语料往往是不具备这种强关联性的,为此,就需要引入一个概念模型,叫词向量(word2vec)或短语向量(seq2seq),简单来说就是将语料库中的词汇抽象映射到一个向量空间中,向量的排布是根据预发和词义语境决定的,例如,"中国->人"(中国后面紧跟着一个人字的可能性是极大的)、"你今年几岁了->我 ** 岁了"

0x1: Token化处理、词编码

将训练集中的对话的每个文件拆分成单独的一个个文字,形成一个词表(word table)

def gen_vocabulary_file(input_file, output_file):
    vocabulary = {}
    with open(input_file) as f:
        counter = 0
        for line in f:
            counter += 1
            tokens = [word for word in line.strip()]
            for word in tokens:
                if word in vocabulary:
                    vocabulary[word] += 1
                else:
                    vocabulary[word] = 1
        vocabulary_list = START_VOCABULART + sorted(vocabulary, key=vocabulary.get, reverse=True)
        # For taking 10000 custom character kanji
        if len(vocabulary_list) > 10000:
            vocabulary_list = vocabulary_list[:10000]
        print(input_file + " phrase table size:", len(vocabulary_list))
        with open(output_file, "w") as ff:
            for word in vocabulary_list:
                ff.write(word + "\n")

完成了Token化之后,需要对单词进行数字编码,方便后续的向量空间处理,这里依据的核心思想是这样的

我们的训练语料库的对话之间都是有强关联的,基于这份有关联的对话集获得的词表的词之间也有逻辑关联性,那么我们只要按照此表原生的顺序对词进行编码,这个编码后的[work, id]就是一个有向量空间关联性的词表

def convert_conversation_to_vector(input_file, vocabulary_file, output_file):
    tmp_vocab = []
    with open(vocabulary_file, "r") as f:
        tmp_vocab.extend(f.readlines())
    tmp_vocab = [line.strip() for line in tmp_vocab]
    vocab = dict([(x, y) for (y, x) in enumerate(tmp_vocab)])
    for item in vocab:
        print item.encode('utf-8')

所以我们根据训练预料集得到的此表可以作为对话训练集和对话测试机进行向量化的依据,我们的目的是将对话(包括训练集和测试集)的问和答都转化映射到向量空间

968
""字在训练集词汇表中的位置是968,我们就给该字设置一个编码968

0x2: 对话转为向量

原作者在词表的选取上作了裁剪,只选取前5000个词汇,但是仔细思考了一下,感觉问题源头还是在训练语料库不够丰富,不能完全覆盖所有的对话语言场景

这一步得到一个ask/answer的语句seq向量空间集,对于训练集,我们将ask和answer建立映射关系

Relevant Link:

 

4. 训练

0x1: Sequence-to-sequence basics

A basic sequence-to-sequence model, as introduced in Cho et al., 2014, consists of two recurrent neural networks (RNNs): an encoder that processes the input and a decoder that generates the output. This basic architecture is depicted below.

Each box in the picture above represents a cell of the RNN, most commonly a GRU cell or an LSTM cell. Encoder and decoder can share weights or, as is more common, use a different set of parameters. Multi-layer cells have been successfully used in sequence-to-sequence models too
In the basic model depicted above, every input has to be encoded into a fixed-size state vector, as that is the only thing passed to the decoder. To allow the decoder more direct access to the input, an attention mechanism was introduced in Bahdanau et al., 2014.; suffice it to say that it allows the decoder to peek into the input at every decoding step. A multi-layer sequence-to-sequence network with LSTM cells and attention mechanism in the decoder looks like this.

0x2: 训练过程

利用ask/answer的训练集输入神经网络,并使用ask/answer测试向量映射集实现BP反馈与,使用一个三层神经网络,让tensorflow自动调整权重参数,获得一个ask-?的模型

# -*- coding: utf-8 -*-

import tensorflow as tf  # 0.12
from tensorflow.models.rnn.translate import seq2seq_model
import os
import numpy as np
import math

PAD_ID = 0
GO_ID = 1
EOS_ID = 2
UNK_ID = 3

# ask/answer conversation vector file
train_ask_vec_file = 'train_ask.vec'
train_answer_vec_file = 'train_answer.vec'
test_ask_vec_file = 'test_ask.vec'
test_answer_vec_file = 'test_answer.vec'

# word table 6000
vocabulary_ask_size = 6000
vocabulary_answer_size = 6000

buckets = [(5, 10), (10, 15), (20, 25), (40, 50)]
layer_size = 256
num_layers = 3
batch_size = 64


# read *dencode.vec和*decode.vec data into memory
def read_data(source_path, target_path, max_size=None):
    data_set = [[] for _ in buckets]
    with tf.gfile.GFile(source_path, mode="r") as source_file:
        with tf.gfile.GFile(target_path, mode="r") as target_file:
            source, target = source_file.readline(), target_file.readline()
            counter = 0
            while source and target and (not max_size or counter < max_size):
                counter += 1
                source_ids = [int(x) for x in source.split()]
                target_ids = [int(x) for x in target.split()]
                target_ids.append(EOS_ID)
                for bucket_id, (source_size, target_size) in enumerate(buckets):
                    if len(source_ids) < source_size and len(target_ids) < target_size:
                        data_set[bucket_id].append([source_ids, target_ids])
                        break
                source, target = source_file.readline(), target_file.readline()
    return data_set

if __name__ == '__main__':
    model = seq2seq_model.Seq2SeqModel(source_vocab_size=vocabulary_ask_size,
                                       target_vocab_size=vocabulary_answer_size,
                                       buckets=buckets, size=layer_size, num_layers=num_layers, max_gradient_norm=5.0,
                                       batch_size=batch_size, learning_rate=0.5, learning_rate_decay_factor=0.97,
                                       forward_only=False)

    config = tf.ConfigProto()
    config.gpu_options.allocator_type = 'BFC'  # forbidden out of memory

    with tf.Session(config=config) as sess:
        # 恢复前一次训练
        ckpt = tf.train.get_checkpoint_state('.')
        if ckpt != None:
            print(ckpt.model_checkpoint_path)
            model.saver.restore(sess, ckpt.model_checkpoint_path)
        else:
            sess.run(tf.global_variables_initializer())

        train_set = read_data(train_ask_vec_file, train_answer_vec_file)
        test_set = read_data(test_ask_vec_file, test_answer_vec_file)

        train_bucket_sizes = [len(train_set[b]) for b in range(len(buckets))]
        train_total_size = float(sum(train_bucket_sizes))
        train_buckets_scale = [sum(train_bucket_sizes[:i + 1]) / train_total_size for i in range(len(train_bucket_sizes))]

        loss = 0.0
        total_step = 0
        previous_losses = []
        # continue train,save modle after a decade of time
        while True:
            random_number_01 = np.random.random_sample()
            bucket_id = min([i for i in range(len(train_buckets_scale)) if train_buckets_scale[i] > random_number_01])

            encoder_inputs, decoder_inputs, target_weights = model.get_batch(train_set, bucket_id)
            _, step_loss, _ = model.step(sess, encoder_inputs, decoder_inputs, target_weights, bucket_id, False)

            loss += step_loss / 500
            total_step += 1

            print(total_step)
            if total_step % 500 == 0:
                print(model.global_step.eval(), model.learning_rate.eval(), loss)

                # if model has't not improve,decrese the learning rate
                if len(previous_losses) > 2 and loss > max(previous_losses[-3:]):
                    sess.run(model.learning_rate_decay_op)
                previous_losses.append(loss)
                # save model
                checkpoint_path = "chatbot_seq2seq.ckpt"
                model.saver.save(sess, checkpoint_path, global_step=model.global_step)
                loss = 0.0
                # evaluation the model by test dataset
                for bucket_id in range(len(buckets)):
                    if len(test_set[bucket_id]) == 0:
                        continue
                    encoder_inputs, decoder_inputs, target_weights = model.get_batch(test_set, bucket_id)
                    _, eval_loss, _ = model.step(sess, encoder_inputs, decoder_inputs, target_weights, bucket_id, True)
                    eval_ppx = math.exp(eval_loss) if eval_loss < 300 else float('inf')
                    print(bucket_id, eval_ppx)

Relevant Link:

https://www.tensorflow.org/tutorials/seq2seq
http://suriyadeepan.github.io/2016-06-28-easy-seq2seq/

 

5. 聊天机器人 - 验证效果

# -*- coding: utf-8 -*-

import tensorflow as tf  # 0.12
from tensorflow.models.rnn.translate import seq2seq_model
import os
import sys
import locale
import numpy as np

PAD_ID = 0
GO_ID = 1
EOS_ID = 2
UNK_ID = 3

train_ask_vocabulary_file = "train_ask_vocabulary.vec"
train_answer_vocabulary_file = "train_answer_vocabulary.vec"


def read_vocabulary(input_file):
    tmp_vocab = []
    with open(input_file, "r") as f:
        tmp_vocab.extend(f.readlines())
    tmp_vocab = [line.strip() for line in tmp_vocab]
    vocab = dict([(x, y) for (y, x) in enumerate(tmp_vocab)])
    return vocab, tmp_vocab


if __name__ == '__main__':
    vocab_en, _, = read_vocabulary(train_ask_vocabulary_file)
    _, vocab_de, = read_vocabulary(train_answer_vocabulary_file)

    # word table 6000
    vocabulary_ask_size = 6000
    vocabulary_answer_size = 6000

    buckets = [(5, 10), (10, 15), (20, 25), (40, 50)]
    layer_size = 256
    num_layers = 3
    batch_size = 1

    model = seq2seq_model.Seq2SeqModel(source_vocab_size=vocabulary_ask_size,
                                       target_vocab_size=vocabulary_answer_size,
                                       buckets=buckets, size=layer_size, num_layers=num_layers, max_gradient_norm=5.0,
                                       batch_size=batch_size, learning_rate=0.5, learning_rate_decay_factor=0.99,
                                       forward_only=True)
    model.batch_size = 1

    with tf.Session() as sess:
        # restore last train
        ckpt = tf.train.get_checkpoint_state('.')
        if ckpt != None:
            print(ckpt.model_checkpoint_path)
            model.saver.restore(sess, ckpt.model_checkpoint_path)
        else:
            print("model not found")

        while True:
            input_string = raw_input('me > ').decode(sys.stdin.encoding or locale.getpreferredencoding(True)).strip()
            # 退出
            if input_string == 'quit':
                exit()

            # convert the user's input to vector
            input_string_vec = []
            for words in input_string.strip():
                input_string_vec.append(vocab_en.get(words, UNK_ID))
            bucket_id = min([b for b in range(len(buckets)) if buckets[b][0] > len(input_string_vec)])
            encoder_inputs, decoder_inputs, target_weights = model.get_batch({bucket_id: [(input_string_vec, [])]},
                                                                             bucket_id)
            _, _, output_logits = model.step(sess, encoder_inputs, decoder_inputs, target_weights, bucket_id, True)
            outputs = [int(np.argmax(logit, axis=1)) for logit in output_logits]
            if EOS_ID in outputs:
                outputs = outputs[:outputs.index(EOS_ID)]

            response = "".join([tf.compat.as_str(vocab_de[output]) for output in outputs])
            print('AI > ' + response)

神经网络还是很依赖样本的训练的,我在实验的过程中发现,用GPU跑到20000 step之后,模型的效果才逐渐显现出来,才开始逐渐像正常的人机对话了

Relevant Link:

Copyright (c) 2017 LittleHann All rights reserved

posted @ 2017-02-22 14:20  郑瀚Andrew  阅读(18362)  评论(0编辑  收藏  举报