前言

  哈哈,重头戏终于来了,经过两天的服务器配置、模型训练,今天终于在微信公众号上部署了自己使用TensorFlow训练的聊天机器人。
  本篇博客主要介绍一下Seq2Seq模型,以及模型训练后的部署,使用的深度学习框架为TensorFlow2.1,GPU为Tesla P100(白嫖Kaggle的),由于网站有时间限制,只训练了两个epoch就先部署了哈,所以机器人目前还很沙雕。

  有关腾讯云服务器配置流程Django对接微信公众号以实现消息自动回复可以参考这两篇博客。

1. 模型介绍

   S e q 2 S e q Seq2Seq Seq2Seq的全称是 S e q u e n c e Sequence Sequence t o to to S e q u e n c e Sequence Sequence,也就是我们常说的序列到序列模型,它是基于 E n c o d e r − D e c o d e r Encoder-Decoder EncoderDecoder框架的 R N N ( R e c u r r e n t RNN(Recurrent RNN(Recurrent N e u r a l Neural Neural N e t w o r k , 循环神经网络 ) Network,循环神经网络) Network,循环神经网络)变种。 S e q 2 S e q Seq2Seq Seq2Seq引入 E n c o d e r − D e c o d e r Encoder-Decoder EncoderDecoder框架,提高了神经网络对长文本信息的提取能力,取得了比单纯使用 L S T M ( L o n g LSTM(Long LSTM(Long S h o r t − T e r m Short-Term ShortTerm M e m o r y , 长短期记忆神经网络 ) Memory,长短期记忆神经网络) Memory,长短期记忆神经网络)更好的效果。 S e q 2 S e q Seq2Seq Seq2Seq中有两个很重要的概念,一个就是上面提到的 E n c o d e r − D e c o d e r Encoder-Decoder EncoderDecoder框架,另一个就是 A t t e n t i o n Attention Attention机制。这里简单介绍一下这两个概念。

1.1 Encoder-Decoder框架

   E n c o d e r − D e c o d e r Encoder-Decoder EncoderDecoder又称为编码器-解码器模型,顾名思义,它有两部分组成,即编码器和解码器。它是一种处理输入、输出长短不一的多对多文本预测问题的框架,其提供了有效的文本特征提取、输出预测的机制。
  编码器的作用是对输入的文本信息进行有效的编码后,将其作为解码器的输入数据,其目的是对输入的文本信息进行特征提取,尽量准确高效地表征该文本的特征信息。
  解码器的作用是从上下文的文本信息中获取尽可能多的特征,然后输出预测文本。根据对文本信息的获取方式不同,解码器一般分为4种结构,分别是直译式解码、循环式解码、增强式解码和注意力机制解码。

  • 直译式解码:按照编码器的费那事进行逆操作得到的预测文本
  • 循环式解码:将编码器输出的编码向量作为第一时刻的输入,然后将得到的输出作为下一个时刻的输入,依次进行循环解码
  • 增强循环式解码:在循环式解码的基础上,每一时刻增加一个编码器输出的编码向量作为输入
  • 注意力机制解码:在增强式循环解码的基础上增加注意力机制,这样可以有效地训练解码器在繁多的输入中重点关注某些有效特征信息,以增加解码器的特征获取能力,进而得到更好的解码效果。

1.2 Attention机制

  虽然 E n c o d e r − D e c o d e r Encoder-Decoder EncoderDecoder结构的模型在机器翻译、语音识别以及文本生成等诸多领域均取得了非常不错的效果,但同时也存在着不足之处。编码器将输入的序列编码成一个固定长度的向量,再由解码器将其解码,得到输出序列。但个固定长度的向量所具有的表征能力是有限的,解码器又受限于这个固定长度的向量,当输入的文本序列较长时,编码器很难将所有的重要信息都编码到这个定长的向量中,从而使得模型的输出结果大大折扣。
   A t t e n t i o n Attention Attention机制有效解决了输入长序列信息时真实含义难以获取的问题。在进行长文本序列处理的任务中,影响当前时刻状态的信息可能隐藏在前面的时刻里,根据马尔可夫假设,这些信息有可能就会被忽略掉。比如,在“我快饿死了,今天搬了一天的砖,我要大吃一顿”这句话中,我们知道“我要大吃一顿”是因为“我快饿死了”,但是基于马尔可夫假设,“今天搬了一天的砖”“我要大吃一顿”在时序上离得更近,相比于“我快饿死了”“今天搬了一天的砖”“我要大吃一顿”的影响力更强,但是在真实的 N L P ( N a t u r a l NLP(Natural NLP(Natural L a n g u a g e Language Language P r o c e s s i n g , 自然语言处理 ) Processing,自然语言处理) Processing,自然语言处理)中不是这样的。从这个例子中可以看出,神经网络模型没有办法很好地准确获取倒装时序的语言信息,要解决这个问题就需要经过训练自动建立起“我要大吃一顿”“我快饿死了”的关联关系,这就是 A t t e n t i o n Attention Attention机制,即注意力机制。

1.3 代码实现

class Encoder(tf.keras.Model):
    """编码器"""
    def __init__(self, vocab_size, embedding_dim, enc_units, batch_size):
        super(Encoder, self).__init__()

        self.batch_size = batch_size
        self.enc_units = enc_units
        self.embedding = tf.keras.layers.Embedding(input_dim=vocab_size, output_dim=embedding_dim)
        self.gru = tf.keras.layers.GRU(units=self.enc_units, recurrent_initializer='glorot_uniform',
                                       return_sequences=True, return_state=True)

    def call(self, x, hidden):
        # 此处添加模型调用的代码(处理输入并返回输出)
        x = self.embedding(x)
        output, state = self.gru(inputs=x, initial_state=hidden)
        return output, state

    def initialize_hidden_state(self):
        return tf.zeros(shape=(self.batch_size, self.enc_units))


class BahdanauAttention(tf.keras.Model):
    """Bahdanau Attention"""
    def __init__(self, units):
        super(BahdanauAttention, self).__init__()
        self.W1 = tf.keras.layers.Dense(units=units)
        self.W2 = tf.keras.layers.Dense(units=units)
        self.V = tf.keras.layers.Dense(units=1)

    def call(self, query, values):
        # query为Encoder最后一个时间步的隐状态(hidden), shape为(batch_size, hidden_size)
        # values为Encoder部分的输出,即每个时间步的隐状态,shape为(batch_size, max_length, hidden_size)
        # 为方便后续计算,需将query的shape转为(batch_size, 1, hidden_size)
        # 给query增加一个维度
        query = tf.expand_dims(input=query, axis=1)

        # 计算score(相似度), 使用MLP网络,即再引入一个神经网络来专门计算score
        # score的shape为(batch_size, max_length, 1)
        score = self.V(
            inputs=tf.nn.tanh(self.W1(inputs=query) + self.W2(inputs=values))
        )

        # 计算attention_weights
        # 计算attention_weights的shape为(batch_size, max_length, 1)
        attention_weights = tf.nn.softmax(logits=score, axis=1)

        # 计算context vector
        # context vector的shape为(batch_size, max_length, hidden_size)
        context_vector = attention_weights * values
        # 加权求和
        # 求和之后的shape为(batch_size, hidden_size)
        context_vector = tf.reduce_sum(input_tensor=context_vector, axis=1)

        return context_vector, attention_weights


class Decoder(tf.keras.Model):
    """解码器"""
    def __init__(self, vocab_size, embedding_dim, dec_units, batch_size):
        super(Decoder, self).__init__()

        self.batch_size = batch_size
        self.dec_units = dec_units
        self.embedding = tf.keras.layers.Embedding(input_dim=vocab_size, output_dim=embedding_dim)
        self.gru = tf.keras.layers.GRU(units=self.dec_units, recurrent_initializer='glorot_uniform',
                                       return_sequences=True, return_state=True)
        self.fc = tf.keras.layers.Dense(units=vocab_size)
        self.attention = BahdanauAttention(units=self.dec_units)

    def call(self, x, hidden, enc_output):
        # 获取context vector和attention weights
        context_vector, attention_weights = self.attention(hidden, enc_output)

        # 编码之后x的shape为(batch_size, 1, embedding_dim)
        x = self.embedding(inputs=x)

        # 将context_vector与输入x进行拼接
        # 拼接后的shape为(batch_size, 1, embedding_dim + hidden_size)
        # 这里的hidden_size即context_vector向量的长度
        x = tf.concat(values=[tf.expand_dims(input=context_vector, axis=1), x], axis=-1)

        # 拼接后输入GRU网络
        output, state = self.gru(inputs=x)
        # print("Decoder output shape: {}".format(output.shape))
        # print("Decoder state shape: {}".format(state.shape))

        # (batch_size, 1, hidden_size) ==> (batch_size, hidden_size)
        output = tf.reshape(tensor=output, shape=(-1, output.shape[2]))

        # x的shape为(batch_size, vocab_size)
        x = self.fc(inputs=output)

        return x, state, attention_weights

  我也是这学期才开始入手TensorFlow2,以前用的都是TensorFlow 1.13.1,代码不明白的地方可以查看《简单粗暴 TensorFlow 2》文档

2. 安装依赖库

  • 安装TensorFlow 2.1
	pip3 install tensorflow==2.1.0
  • 安装jieba
	pip3 install jieba

在这里插入图片描述
在这里插入图片描述

3. 模型部署

  腾讯云服务器用的是学生版的1核2G,感觉不一定能够支撑模型运行,先尝试一下吧。在此之前还是在本地通过Postman进行一下测试:

在这里插入图片描述
  还是OK的,就是模型加载的较慢,下面把模型文件以及相关代码上传到服务器的项目目录,目录内容更新为如下:

在这里插入图片描述
  上传到服务器之后,大致等到模型差不多加载好就可以准备测试了,测试结果如下:

在这里插入图片描述
  查看一下日志文件,发现了一些端倪:
在这里插入图片描述
  进程被杀死了,查了一下相关文件,说是超时了,enmmmmm,貌似有些道理【虽然不是很确定,但是模型确实是被重新加载了,更改了相关uwsgi的参数之后依旧是这个结果】,于是我直接上传了一个更改后的测试模型文件CR.py,直接在环境中运行,果不其然:

在这里插入图片描述
在这里插入图片描述
  这应该是内存不够吧~OK,暂时到此结束。



  昨天出了一点意外,1核2G的腾讯云服务器运行不了这个模型,所以今天换成了2核4G的阿里云服务器【有一说一,阿里云的这个学生套餐还是挺实惠的,又成功白嫖】,阿里云的配置过程同腾讯云的一样,可参考我的这篇博客
  服务器配置完成之后,把项目文件上传到阿里云服务器的wwwroot文件夹下,然后进入pyweb虚拟环境,再次运行一下CR.py文件,看看模型能不能运行起来。结果如下:

在这里插入图片描述
  还是很nice的,模型能够运行,OK,接入到微信公众号上,配置代码很简单,只需要把微信公众号发送过来的消息送入到模型即可,代码如下:

# views.py
# 导入模型的接口
from tencent.chatRobot import predict

input_info = recMsg.Content.decode('utf-8')
try:
	content = predict(sentence=input_info)
except Exception as err:
	content = '小悠没理解主银的意思~'
replyMsg = TextMsg(toUser, fromUser, content)

  当时,还考虑了很久,模型如何先被加载,因为模型加载的时间稍长,不能等到微信公众号消息来了再加载模型,那肯定会超时的,而且每次都加载,肯定还很麻烦。当时还考虑到用线程等方法来加载,enmmmmm,后来嘛,就突然想到,为何不用全局变量的形式来加载,就是Python执行的时候是顺序执行嘛,像函数、类之类的这种对象,虽然定义了,但只要不被调用,这些代码就不会被运行,而函数、类之外的代码会正常按顺序执行,相当于就是全局变量了嘛。

# chatRobot.py
# -*- coding: utf-8 -*-
# @Time    : 2021/1/4 22:47
# @Author  : XiaYouRan
# @Email   : youran.xia@foxmail.com
# @File    : chatRobot.py
# @Software: PyCharm


import tensorflow as tf
import jieba
import os


def preprocess_sentence(sentence):
    """
    给句子添加开始和结束标记
    :param sentence:
    :return:
    """
    sentence = '<start> ' + sentence + ' <end>'
    return sentence


def max_length(tensor):
    """
    计算数据集中问句和答句中最长的句子长度
    :param tensor:
    :return:
    """
    return max([len(t) for t in tensor])


def tokenize(sentences):
    """
    分词器函数
    :param sentence:
    :return:
    """
    # 初始化分词器,并生成词典
    sentence_tokenizer = tf.keras.preprocessing.text.Tokenizer(filters='')
    sentence_tokenizer.fit_on_texts(sentences)

    # 利用字典将文本数据转为id
    # 也是二维的
    tensor = sentence_tokenizer.texts_to_sequences(texts=sentences)

    # 将数据填充成统一长度
    # 默认统一为最长句子长度
    # 将长为nb_samples的序列(标量序列)转化为形如(nb_samples,nb_timesteps) 2D numpy array
    tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor, maxlen=30, padding='post')

    return tensor, sentence_tokenizer


def load_dataset(file_path):
    with open(file_path, 'r', encoding='utf-8') as f:
        lines = f.readlines()
        q = ''
        a = ''
        qa_pairs = []
        # len(lines) 总行数
        for i in range(len(lines)):
            if i % 3 == 0:
                q = ' '.join(jieba.cut(lines[i].strip()))
            elif i % 3 == 1:
                a = ' '.join(jieba.cut(lines[i].strip()))
            else:
                # 问句与答句进行组合
                pair = [preprocess_sentence(q), preprocess_sentence(a)]
                qa_pairs.append(pair)

    # zip 拆解
    q_sentences, a_sentences = zip(*qa_pairs)

    # question数据集(id)及其分类器词汇表
    q_tensor, q_tokenizer = tokenize(q_sentences)
    # answer数据集(id)及其分类器词汇表
    a_tensor, a_tokenizer = tokenize(a_sentences)

    return q_tensor, a_tensor, q_tokenizer, a_tokenizer


class Encoder(tf.keras.Model):
    """编码器"""


class BahdanauAttention(tf.keras.Model):
    """Bahdanau Attention"""


class Decoder(tf.keras.Model):
    """解码器"""


# 使用Adam优化器
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)


def predict(sentence):
    """模型测试"""
    # 加载模型
    checkpoint = tf.train.Checkpoint(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
                                     encoder=encoder,
                                     decoder=decoder)
    checkpoint.restore(save_path=tf.train.latest_checkpoint(checkpoint_dir=checkpoint_dir))

    sentence = ' '.join(jieba.cut(sentence.strip()))
    sentence = preprocess_sentence(sentence=sentence)

    inputs = [q_tokenizer.word_index[i] for i in sentence.split(' ')]
    inputs = tf.keras.preprocessing.sequence.pad_sequences(sequences=[inputs], maxlen=30, padding='post')
    inputs = tf.convert_to_tensor(value=inputs)

    result = ''

    hidden = [tf.zeros(shape=(1, units))]
    enc_out, enc_hidden = encoder(inputs, hidden)

    dec_hidden = enc_hidden
    dec_input = tf.expand_dims(input=[a_tokenizer.word_index['<start>']], axis=0)

    for t in range(q_tesor_length):
        predictions, dec_hidden, attention_weights = decoder(dec_input, dec_hidden, enc_out)

        predicted_id = tf.argmax(predictions[0]).numpy()
        result += a_tokenizer.index_word[predicted_id] + ' '

        if a_tokenizer.index_word[predicted_id] == '<end>':
            break

        dec_input = tf.expand_dims(input=[predicted_id], axis=0)

    # print("Q: %s" % sentence[8:-6].replace(' ', ''))
    # print("A: {}".format(result[:-6].replace(' ', '')))
    # print("A: {}".format(result.replace(' ', '')))

    return result[:-6].replace(' ', '')


file_path = os.path.dirname(__file__)
corpus_path = os.path.join(file_path, 'dataset/corpus.txt')

checkpoint_dir = os.path.join(file_path, 'model/train_checkpoints')

q_tensor, a_tensor, q_tokenizer, a_tokenizer = load_dataset(file_path=corpus_path)

q_tesor_length = max_length(q_tensor)
a_tesor_length = max_length(a_tensor)

buffer_size = len(q_tensor)
batch_size = 32
steps_per_epoch = len(q_tensor) // batch_size
embedding_dim = 128
units = 256

# q_tokenizer.word_index 字典类型(word, id)
vocab_q_size = len(q_tokenizer.word_index) + 1
vocab_a_size = len(a_tokenizer.word_index) + 1

# 模型初始化
encoder = Encoder(vocab_size=vocab_q_size, embedding_dim=embedding_dim, enc_units=units, batch_size=batch_size)
attention_layer = BahdanauAttention(units=10)
decoder = Decoder(vocab_size=vocab_a_size, embedding_dim=embedding_dim, dec_units=units, batch_size=batch_size)


if __name__ == '__main__':
    input_sentence = "Start chatting..."
    while input_sentence != "stop":
        print("请输入:")
        input_sentence = input()
        try:
            predict(input_sentence)
            print("----------------------")
        except Exception as err:
            print('Test model error info: ', err)

4. 测试

  首先要把微信公众号的基本配置改一下,把那个服务器地址更改成阿里云的公网IP,然后启动服务器就可以了(大致需要五六分钟)。
  测试的结果如下:

在这里插入图片描述
  目前来看,机器人还很沙雕,毕竟只训练了两个epoch,准备再多训练几次,不过整体来看还蛮好的,部署的流程成功的走了一下,接下来就开始继续训练模型了。
  在阿里云后台看了一下服务器,模型确实比较吃内存,4G内存占用了近80%,怪不得2G内存不够用!

在这里插入图片描述
  总的来说,很OK,很nice!!!!想体验的小伙伴们,欢迎来玩哦,关注微信公众号夏小悠

在这里插入图片描述

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐