赞
踩
哈哈,重头戏终于来了,经过两天的服务器配置、模型训练,今天终于在微信公众号上部署了自己使用TensorFlow训练的聊天机器人。
本篇博客主要介绍一下Seq2Seq模型,以及模型训练后的部署,使用的深度学习框架为TensorFlow2.1,GPU为Tesla P100(白嫖Kaggle的),由于网站有时间限制,只训练了两个epoch就先部署了哈,所以机器人目前还很沙雕。
有关腾讯云服务器配置流程和Django对接微信公众号以实现消息自动回复可以参考这两篇博客。
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 Encoder−Decoder框架的 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 Encoder−Decoder框架,提高了神经网络对长文本信息的提取能力,取得了比单纯使用 L S T M ( L o n g LSTM(Long LSTM(Long S h o r t − T e r m Short-Term Short−Term 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 Encoder−Decoder框架,另一个就是 A t t e n t i o n Attention Attention机制。这里简单介绍一下这两个概念。
E
n
c
o
d
e
r
−
D
e
c
o
d
e
r
Encoder-Decoder
Encoder−Decoder又称为编码器-解码器模型,顾名思义,它有两部分组成,即编码器和解码器。它是一种处理输入、输出长短不一的多对多文本预测问题的框架,其提供了有效的文本特征提取、输出预测的机制。
编码器的作用是对输入的文本信息进行有效的编码后,将其作为解码器的输入数据,其目的是对输入的文本信息进行特征提取,尽量准确高效地表征该文本的特征信息。
解码器的作用是从上下文的文本信息中获取尽可能多的特征,然后输出预测文本。根据对文本信息的获取方式不同,解码器一般分为4种结构,分别是直译式解码、循环式解码、增强式解码和注意力机制解码。
虽然
E
n
c
o
d
e
r
−
D
e
c
o
d
e
r
Encoder-Decoder
Encoder−Decoder结构的模型在机器翻译、语音识别以及文本生成等诸多领域均取得了非常不错的效果,但同时也存在着不足之处。编码器将输入的序列编码成一个固定长度的向量,再由解码器将其解码,得到输出序列。但个固定长度的向量所具有的表征能力是有限的,解码器又受限于这个固定长度的向量,当输入的文本序列较长时,编码器很难将所有的重要信息都编码到这个定长的向量中,从而使得模型的输出结果大大折扣。
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机制,即注意力机制。
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》文档。
pip3 install tensorflow==2.1.0
pip3 install jieba
腾讯云服务器用的是学生版的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)
首先要把微信公众号的基本配置改一下,把那个服务器地址更改成阿里云的公网IP,然后启动服务器就可以了(大致需要五六分钟)。
测试的结果如下:
目前来看,机器人还很沙雕,毕竟只训练了两个epoch,准备再多训练几次,不过整体来看还蛮好的,部署的流程成功的走了一下,接下来就开始继续训练模型了。
在阿里云后台看了一下服务器,模型确实比较吃内存,4G内存占用了近80%,怪不得2G内存不够用!
总的来说,很OK,很nice!!!!想体验的小伙伴们,欢迎来玩哦,关注微信公众号夏小悠
。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。