NLP入门系列四:Tensorflow2.0+Keras实现seq2seq+Attention模型的对话系统

本文主要是利用Tensorflow中keras框架记录简单实现seq2seq+Attention模型的过程,seq2seq的应用主要有问答系统、人机对话、机器翻译等。代码中会用一个中文对话数据简单测试。

seq2seq模型介绍

seq2seq模型主要有两个部分Encoder和Decoder,Encoder负责将输入序列编码,Decoder负责解码输出序列。最简单的seq2seq模型图:
seq2seq简单结构
基于注意力机制的seq2seq模型。
seq2seq+attention模型结构

Keras实现seq2seq+Atttention模型

本文的实现是基于Tensorflow 2.0中的keras,也可以用原始的keras也可以,如果用原始的keras,需要自己实现Attention层。

详细代码和数据:https://github.com/huanghao128/zh-nlp-demo

Encoder部分

encoder部分就是一个标准的RNN/LSTM模型,取最后时刻的隐藏层作为输出。我们用tensorflow.keras.models定义Encoder为一个sub model。
先导入tensorflow.keras的常用包。

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import backend as K
from tensorflow.keras import activations
from tensorflow.keras.layers import Layer, Input, Embedding, LSTM, Dense, Attention
from tensorflow.keras.models import Model

encoder部分结构,主要就是一个Embedding层,加上LSTM层。

class Encoder(keras.Model):
    def __init__(self, vocab_size, embedding_dim, hidden_units):
        super(Encoder, self).__init__()
        # Embedding Layer
        self.embedding = Embedding(vocab_size, embedding_dim, mask_zero=True)
        # Encode LSTM Layer
        self.encoder_lstm = LSTM(hidden_units, return_sequences=True, return_state=True, name="encode_lstm")
        
    def call(self, inputs):
        encoder_embed = self.embedding(inputs)
        encoder_outputs, state_h, state_c = self.encoder_lstm(encoder_embed)
        return encoder_outputs, state_h, state_c

Decoder部分

decoder部分结构,有三部分输入,一是encoder部分的每个时刻输出,二是encoder的隐藏状态输出,三是decoder的目标输入。另外decoder还包含一个Attention层,计算decoder每个输入与encoder的注意力。

class Decoder(keras.Model):
    def __init__(self, vocab_size, embedding_dim, hidden_units):
        super(Decoder, self).__init__()
        # Embedding Layer
        self.embedding = Embedding(vocab_size, embedding_dim, mask_zero=True)
        # Decode LSTM Layer
        self.decoder_lstm = LSTM(hidden_units, return_sequences=True, return_state=True, name="decode_lstm")
        # Attention Layer
        self.attention = Attention()
    
    def call(self, enc_outputs, dec_inputs, states_inputs):
        decoder_embed = self.embedding(dec_inputs)
        dec_outputs, dec_state_h, dec_state_c = self.decoder_lstm(decoder_embed, initial_state=states_inputs)
        attention_output = self.attention([dec_outputs, enc_outputs])
        
        return attention_output, dec_state_h, dec_state_c

Encoder和Decoder合并

encoder和decoder模块合并,组成一个完整的seq2seq模型。

def Seq2Seq(maxlen, embedding_dim, hidden_units, vocab_size):
    """
    seq2seq model
    """
    # Input Layer
    encoder_inputs = Input(shape=(maxlen,), name="encode_input")
    decoder_inputs = Input(shape=(None,), name="decode_input")
    # Encoder Layer
    encoder = Encoder(vocab_size, embedding_dim, hidden_units)
    enc_outputs, enc_state_h, enc_state_c = encoder(encoder_inputs)
    dec_states_inputs = [enc_state_h, enc_state_c]
    # Decoder Layer
    decoder = Decoder(vocab_size, embedding_dim, hidden_units)
    attention_output, dec_state_h, dec_state_c = decoder(enc_outputs, decoder_inputs, dec_states_inputs)
    # Dense Layer
    dense_outputs = Dense(vocab_size, activation='softmax', name="dense")(attention_output)
    # seq2seq model
    model = Model(inputs=[encoder_inputs, decoder_inputs], outputs=dense_outputs)
    return model

模型详细结构

我们自定义一些参数,看看seq2seq模型的整个结构。

maxlen = 10
embedding_dim = 50
hidden_units = 128
vocab_size = 10000

model = Seq2Seq(maxlen, embedding_dim, hidden_units, vocab_size)
print(model.summary())

seq2seq的层结构和参数,由于上面我们把encoder和decoder都封装在一起,所以这里看起来只有一个层,当然也可以展开的。
seq2seq模型结构图
从上面seq2seq模型中获取Encoder子模块:

def encoder_infer(model):
    encoder_model = Model(inputs=model.get_layer('encoder').input, 
                        outputs=model.get_layer('encoder').output)
    return encoder_model

encoder_model = encoder_infer(model)
print(encoder_model.summary())

Encoder内部的层结构和参数:
Encoder模块结构图
从上面seq2seq模型中获取Decoder子模块,这里没有直接从decoder层取,方便后续decoder的预测推断。

def decoder_infer(model, encoder_model):
    encoder_output = encoder_model.get_layer('encoder').output[0]
    maxlen, hidden_units = encoder_output.shape[1:]
    
    dec_input = model.get_layer('decode_input').input
    enc_output = Input(shape=(maxlen, hidden_units), name='enc_output')
    enc_input_state_h = Input(shape=(hidden_units,), name='input_state_h')
    enc_input_state_c = Input(shape=(hidden_units,), name='input_state_c')
    dec_input_states = [enc_input_state_h, enc_input_state_c]

    decoder = model.get_layer('decoder')
    dec_outputs, out_state_h, out_state_c = decoder(enc_output, dec_input, dec_input_states)
    dec_output_states = [out_state_h, out_state_c]

    decoder_dense = model.get_layer('dense')
    dense_output = decoder_dense(dec_outputs)

    decoder_model = Model(inputs=[enc_output, dec_input, dec_input_states], 
                          outputs=[dense_output]+dec_output_states)
    return decoder_model

decoder_model = decoder_infer(model, encoder_model)
print(decoder_model.summary())

Decoder内部的层结构和参数:
Decoder模块结构图

seq2seq模型训练

这里我们以一个中文的聊天对话数据作为训练语料,训练一个简单的对话系统demo。语料已经分过词,首先对数据做一下预处理,转成one-hot表示,并添加一些开始结束符。

读取数据以及词典的方法

def read_vocab(vocab_path):
    vocab_words = []
    with open(vocab_path, "r", encoding="utf8") as f:
        for line in f:
            vocab_words.append(line.strip())
    return vocab_words

def read_data(data_path):
    datas = []
    with open(data_path, "r", encoding="utf8") as f:
        for line in f:
            words = line.strip().split()
            datas.append(words)
    return datas

def process_data_index(datas, vocab2id):
    data_indexs = []
    for words in datas:
        line_index = [vocab2id[w] if w in vocab2id else vocab2id["<UNK>"] for w in words]
        data_indexs.append(line_index)
    return data_indexs

预处理数据并生成词典

vocab_words = read_vocab("data/ch_word_vocab.txt")
special_words = ["<PAD>", "<UNK>", "<GO>", "<EOS>"]
vocab_words = special_words + vocab_words
vocab2id = {word: i for i, word in enumerate(vocab_words)}
id2vocab = {i: word for i, word in enumerate(vocab_words)}

num_sample = 10000
source_data = read_data("data/ch_source_data_seg.txt")[:num_sample]
source_data_ids = process_data_index(source_data, vocab2id)
target_data = read_data("data/ch_target_data_seg.txt")[:num_sample]
target_data_ids = process_data_index(target_data, vocab2id)

print("vocab test: ", [id2vocab[i] for i in range(10)])
print("source test: ", source_data[10])
print("source index: ", source_data_ids[10])
print("target test: ", target_data[10])
print("target index: ", target_data_ids[10])

test data
Decoder部分输入输出加上开始结束标识
decoder的输入前面加上开始"“表示,输出后面加上结束”"标识。

def process_decoder_input_output(target_indexs, vocab2id):
    decoder_inputs, decoder_outputs = [], []
    for target in target_indexs:
        decoder_inputs.append([vocab2id["<GO>"]] + target)
        decoder_outputs.append(target + [vocab2id["<EOS>"]])
    return decoder_inputs, decoder_outputs

target_input_ids, target_output_ids = process_decoder_input_output(target_data_ids, vocab2id)
print("decoder inputs: ", target_input_ids[:2])
print("decoder outputs: ", target_output_ids[:2])

数据pad填充

maxlen = 10
source_input_ids = keras.preprocessing.sequence.pad_sequences(source_data_ids, padding='post', maxlen=maxlen)
target_input_ids = keras.preprocessing.sequence.pad_sequences(target_input_ids, padding='post',  maxlen=maxlen)
target_output_ids = keras.preprocessing.sequence.pad_sequences(target_output_ids, padding='post',  maxlen=maxlen)
print(source_data_ids[:5])
print(target_input_ids[:5])
print(target_output_ids[:5])

构建模型

K.clear_session()

maxlen = 10
embedding_dim = 50
hidden_units = 128
vocab_size = len(vocab2id)

model = Seq2Seq(maxlen, embedding_dim, hidden_units, vocab_size)
print(model.summary())

训练模型

epochs = 10
batch_size = 32
val_rate = 0.2

loss_fn = keras.losses.SparseCategoricalCrossentropy()
model.compile(loss=loss_fn, optimizer='adam')
model.fit([source_input_ids, target_input_ids], target_output_ids, 
          batch_size=batch_size, epochs=epochs, validation_split=val_rate)

保存和加载模型
由于上面构建模型的写法原因,这里不能直接model.save()同时保存模型图和权重,这里只保存了保存模型的权重,所以新加载时,需要先重新定义模型,然后load_weights加载权重。

# save model weights
model.save_weights("data/seq2seq_attention_weights.h5")
del model
# load model weights
model = Seq2Seq(maxlen, embedding_dim, hidden_units, vocab_size)
model.load_weights("data/seq2seq_attention_weights.h5")
print(model.summary())

模型预测

seq2seq模型预测时,与一般的模型是有区别的,这里Encoder部分直接预测得到输出,Decoder部分需要一次预测一个位置的输出,并把当前位置的输出作为下一时刻的输入。所以decoder部分需要自己写一个循环,预测每个时刻输出,最后拼接到一起。结束的判断要么达到最大长度,要么预测出结束符""。
预测时用到了上面从seq2seq总的model中提取出的encoder_model和decoder_model部分,encoder_model用于对输入编码,decoder_model用于对输出按每个时刻解码。

import numpy as np

maxlen = 10

def infer_predict(input_text, encoder_model, decoder_model):
	"""
	预测部分
	"""
    text_words = input_text.split()[:maxlen]
    input_id = [vocab2id[w] if w in vocab2id else vocab2id["<UNK>"] for w in text_words]
    # input_id = [vocab2id["<START>"]] + input_id + [vocab2id["<END>"]]
    if len(input_id) < maxlen:
        input_id = input_id + [vocab2id["<PAD>"]] * (maxlen-len(input_id))

    input_source = np.array([input_id])
    input_target = np.array([vocab2id["<GO>"]])
    
    # 编码器encoder预测输出
    enc_outputs, enc_state_h, enc_state_c = encoder_model.predict([input_source])
    dec_inputs = input_target
    dec_states_inputs = [enc_state_h, enc_state_c]

    result_id = []
    result_text = []
    for i in range(maxlen):
        # 解码器decoder预测输出
        dense_outputs, dec_state_h, dec_state_c = decoder_model.predict([enc_outputs, dec_inputs]+dec_states_inputs)
        pred_id = np.argmax(dense_outputs[0][0])
        result_id.append(pred_id)
        result_text.append(id2vocab[pred_id])
        if id2vocab[pred_id] == "<EOS>":
            break
        dec_inputs = np.array([[pred_id]])
        dec_states_inputs = [dec_state_h, dec_state_c]
    return result_id, result_text

测试一下输入一个句子,看看输出结果。

input_sent = "你 在 干 什么 呢"
result_id, result_text = infer_predict(input_text, encoder_model, decoder_model)

print("Input: ", input_text)
print("Output: ", result_text, result_id)

注意:这里作为例子,预测每个时刻的输出都只取了概率最大的那一个,这样的输出序列其实不是最优的,还有一种beam search的方法输出效果会更好,有机会在加上。


参考文档

[1]. https://machinelearningmastery.com/develop-encoder-decoder-model-sequence-sequence-prediction-keras/
[2]. https://www.tensorflow.org/guide/keras/functional

posted @ 2020-05-17 00:15  黄然小悟  阅读(1101)  评论(0编辑  收藏  举报