构建编码器解码器模型实现 Seq2Seq 预测

使用 Keras 实现 Sequence-to-Sequence

Posted by Jason Brownlee on November 16, 2017

翻译自 MachineLearningMastery《How to Develop an Encoder-Decoder Model for Sequence-to-Sequence Prediction in Keras》,作者 Jason Brownlee。小昇翻译,部分内容有删改。

Encoder-Decoder 模型提供了一种使用循环神经网络来解决诸如机器翻译这样的 Sequence-to-Sequence 预测问题的方法。

Encoder-Decoder 模型可以用 Keras Python 深度学习库来进行开发,并且在 Keras 博客上已经描述了用这种模型开发的神经机器翻译系统的示例,示例代码与 Keras 项目一起分发。这个示例为用户开发解决自己 Sequence-to-Sequence 预测问题的 Encoder-Decoder LSTM 模型提供了基础。

在本教程中,你将学会如何使用 Keras 来构建一个用于解决 Sequence-to-Sequence 预测问题的复杂编码器-解码器循环神经网络。通过本教程,你将会了解到:

  • 如何在 Keras 中正确定义一个复杂的 Encoder-Decoder 模型来进行 Sequence-to-Sequence 预测。
  • 如何定义一个可用于评估 Encoder-Decoder LSTM 模型的可伸缩 Sequence-to-Sequence 预测问题。
  • 如何在 Keras 中应用 Encoder-Decoder LSTM 模型来解决可伸缩的整数 Sequence-to-Sequence 预测问题。

让我们开始吧。

1

Python 环境

本教程假设你已经安装了 Python SciPy 环境。本教程可以使用 Python 2 或 3。

你必须已经安装 Keras(2.0或更高版本),并且使用 TensorFlow 或 Theano 作为后端。

本教程也假设你已经安装了 scikit-learn、Pandas、NumPy 和 Matplotlib。

1. Keras 中的 Encoder-Decoder 模型

Encoder-Decoder 模型是对于 Sequence-to-Sequence 预测问题组织循环神经网络的一种方法。它最初是为机器翻译问题开发的,并且在相关的 Sequence-to-Sequence 预测问题(如文本摘要和问题回答)中已被证明是有效的。

该方法涉及两个循环神经网络,一个用于对源序列进行编码,称为编码器,另一个将编码的源序列解码成目标序列,称为解码器。

Keras 深度学习 Python 库提供了一个如何实现用于机器翻译的 Encoder-Decoder 模型的例子 (lstm_seq2seq.py),作者在文章“在 Keras 中进行 Sequence-to-Sequence 学习的 10 分钟简介”中进行了讲解。以该示例中的代码为基础,我们可以设计一个通用函数去定义一个 Encoder-Decoder 循环神经网络。

下面是这个名为 define_models() 的函数。

# returns train, inference_encoder and inference_decoder models
def define_models(n_input, n_output, n_units):
	# define training encoder
	encoder_inputs = Input(shape=(None, n_input))
	encoder = LSTM(n_units, return_state=True)
	encoder_outputs, state_h, state_c = encoder(encoder_inputs)
	encoder_states = [state_h, state_c]
	# define training decoder
	decoder_inputs = Input(shape=(None, n_output))
	decoder_lstm = LSTM(n_units, return_sequences=True, return_state=True)
	decoder_outputs, _, _ = decoder_lstm(decoder_inputs, initial_state=encoder_states)
	decoder_dense = Dense(n_output, activation='softmax')
	decoder_outputs = decoder_dense(decoder_outputs)
	model = Model(inputs=[encoder_inputs, decoder_inputs], outputs=decoder_outputs)
	# define inference encoder
	encoder_model = Model(inputs=encoder_inputs, outputs=encoder_states)
	# define inference decoder
	decoder_state_input_h = Input(shape=(n_units,))
	decoder_state_input_c = Input(shape=(n_units,))
	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(inputs=[decoder_inputs] + decoder_states_inputs, outputs=[decoder_outputs] + decoder_states)
	# return all models
	return model, encoder_model, decoder_model

该函数有 3 个参数:

  • n_input:输入序列的基数,例如每个时间步的特征数、词数或字符数。
  • n_output:输出序列的基数,例如每个时间步的特征数、词数或字符数。
  • n_units:在编码器和解码器模型中创建的神经元的数量,例如 128 或 256。

该函数创建并返回 3 个模型:

  • train:给定源、目标和偏移目标序列进行训练的模型。
  • inference_encoder:对新的源序列进行预测时使用的编码器模型。
  • inference_decoder:对新的源序列进行预测时使用的解码器模型。

该模型对给定的源序列和目标序列进行训练,其中模型以源序列和一个目标序列的偏移版本作为输入,对整个目标序列进行预测。

例如,源序列是 [1,2,3],目标序列是 [4,5,6]。那么训练期间模型的输入和输出将是:

Input1: ['1', '2', '3']
Input2: ['_', '4', '5']
Output: ['4', '5', '6']

当为新的源序列生成目标序列时,该模型将会被递归调用。

对源序列进行编码,并且一次一个元素地生成目标序列,使用一个“开始序列”字符例如(“_”)来启动这个过程。因此,在上述情况下,训练过程中会生成以下这样的输入-输出对:

t,	Input1,			Input2,		Output
1,	['1', '2', '3'],	'_',		'4'
2,	['1', '2', '3'],	'4',		'5'
3,	['1', '2', '3'],	'5',		'6'

你可以看到如何递归地使用模型来构建输出序列。

在预测过程中,使用 inference_encoder 模型对输入序列进行一次编码,并且返回用于初始化 inference_decoder 模型的状态值。inference_decoder模型用于逐步生成预测。然后,使用 inference_decoder 模型来逐步生成预测。

下面这个 predict_sequence() 函数可以在模型训练好后被使用:

# generate target given source sequence
def predict_sequence(infenc, infdec, source, n_steps, cardinality):
	# encode
	state = infenc.predict(source)
	# start of sequence input
	target_seq = array([0.0 for _ in range(cardinality)]).reshape(1, 1, cardinality)
	# collect predictions
	output = list()
	for t in range(n_steps):
		# predict next char
		yhat, h, c = infdec.predict([target_seq] + state)
		# store prediction
		output.append(yhat[0,0,:])
		# update state
		state = [h, c]
		# update target sequence
		target_seq = yhat
	return array(output)

此函数有 5 个参数:

  • infenc:对新的源序列进行预测时使用的编码器模型。
  • infdec:对新的源序列进行预测时使用的解码器模型。
  • source:编码后的源序列。
  • n_steps:目标序列中的时间步数量。
  • cardinality:输出序列的基数,例如每个时间步的特征数、词数或字符数。

该函数会返回包含目标序列的列表。

2. 可伸缩的 Sequence-to-Sequence 问题

在本节中,我们定义了一个人为的可伸缩的 Sequence-to-Sequence 预测问题。

源序列是一系列随机生成的整数值,例如 [20, 36, 40, 10, 34, 28],目标序列是输入序列的逆序预定义子集,例如前 3 个元素的逆序排列 [40, 36, 20]。

源序列的长度、输入和输出序列的基数以及目标序列的长度都是可配置的。我们将使用的源序列元素个数是 6,基数是 50,目标序列元素个数是 3。

下面是一些具体的例子。

Source,				Target
[13, 28, 18, 7, 9, 5]		[18, 28, 13]
[29, 44, 38, 15, 26, 22]	[38, 44, 29]
[27, 40, 31, 29, 32, 1]		[31, 40, 27]
...

我们首先定义一个函数来生成一个随机整数序列。我们将使用 0 值作为序列字符的填充或起始,因此 0 是保留字符,我们不能在源序列中使用。为了达到这个目的,我们在配置的基数上加 1,以确保 one-hot 编码足够大。例如:

n_features = 50 + 1

我们可以使用 python 函数 randint() 生成 1 到问题基数 -1 的随机整数。下面的 generate_sequence() 生成一个随机整数序列。

# generate a sequence of random integers
def generate_sequence(length, n_unique):
	return [randint(1, n_unique-1) for _ in range(length)]

接下来,我们需要根据源序列创建相应的输出序列。为了简单起见,我们将选择源序列的前 n 个元素作为目标序列,并将其逆序。

# define target sequence
target = source[:n_out]
target.reverse()

我们还需要一个输出序列向前移动一个时间步的版本,把它作为生成的模拟目标序列,包括序列的开始第一个时间步的值。我们可以直接从目标序列创建它。

# create padded input target sequence
target_in = [0] + target[:-1]

现在所有的序列都已经定义好了,下面可以对它们进行 one-hot 编码了,即将它们转换为二进制向量的序列。可以使用 Keras 内置的 to_categorical() 函数来实现。

我们可以将所有这些放到一个名为 get_dataset() 的函数中,该函数将生成一定数量的序列,我们可以使用这些序列来训练一个模型。

# prepare data for the LSTM
def get_dataset(n_in, n_out, cardinality, n_samples):
	X1, X2, y = list(), list(), list()
	for _ in range(n_samples):
		# generate source sequence
		source = generate_sequence(n_in, cardinality)
		# define target sequence
		target = source[:n_out]
		target.reverse()
		# create padded input target sequence
		target_in = [0] + target[:-1]
		# encode
		src_encoded = to_categorical([source], num_classes=cardinality)
		tar_encoded = to_categorical([target], num_classes=cardinality)
		tar2_encoded = to_categorical([target_in], num_classes=cardinality)
		# store
		X1.append(src_encoded)
		X2.append(tar2_encoded)
		y.append(tar_encoded)
	return array(X1), array(X2), array(y)

最后,我们需要能够解码一个 one-hot 的序列,使其再次可读。这不仅对于打印生成的目标序列是必需的,而且还可以容易地比较完整的预测目标序列是否匹配预期的目标序列。

# decode a one hot encoded string
def one_hot_decode(encoded_seq):
	return [argmax(vector) for vector in encoded_seq]

我们可以将所有这一切联系在一起并测试这些功能。下面列出了一个完整的代码示例。

from random import randint
from numpy import array
from numpy import argmax
from keras.utils import to_categorical

# generate a sequence of random integers
def generate_sequence(length, n_unique):
	return [randint(1, n_unique-1) for _ in range(length)]

# prepare data for the LSTM
def get_dataset(n_in, n_out, cardinality, n_samples):
	X1, X2, y = list(), list(), list()
	for _ in range(n_samples):
		# generate source sequence
		source = generate_sequence(n_in, cardinality)
		# define target sequence
		target = source[:n_out]
		target.reverse()
		# create padded input target sequence
		target_in = [0] + target[:-1]
		# encode
		src_encoded = to_categorical([source], num_classes=cardinality)
		tar_encoded = to_categorical([target], num_classes=cardinality)
		tar2_encoded = to_categorical([target_in], num_classes=cardinality)
		# store
		X1.append(src_encoded)
		X2.append(tar2_encoded)
		y.append(tar_encoded)
	return array(X1), array(X2), array(y)

# decode a one hot encoded string
def one_hot_decode(encoded_seq):
	return [argmax(vector) for vector in encoded_seq]

# configure problem
n_features = 50 + 1
n_steps_in = 6
n_steps_out = 3
# generate a single source and target sequence
X1, X2, y = get_dataset(n_steps_in, n_steps_out, n_features, 1)
print(X1.shape, X2.shape, y.shape)
print('X1=%s, X2=%s, y=%s' % (one_hot_decode(X1[0]), one_hot_decode(X2[0]), one_hot_decode(y[0])))

首先运行示例打印生成的数据集的 shape,确保训练模型所需的 3D shape 符合我们的期望。然后将生成的序列解码并打印到屏幕上,以显示源序列和目标序列符合我们的意图,并且解码操作正常。

(1, 6, 51) (1, 3, 51) (1, 3, 51)
X1=[32, 16, 12, 34, 25, 24], X2=[0, 12, 16], y=[12, 16, 32]

现在我们准备为这个 sequence-to-sequence 预测问题开发一个模型。

3. 用于序列预测的 Encoder-Decoder LSTM

在本节中,我们将把第一部分介绍的 encoder-decoder LSTM 模型应用于第二部分介绍的 sequence-to-sequence 预测问题。

第一步是配置这个问题。

# configure problem
n_features = 50 + 1
n_steps_in = 6
n_steps_out = 3

接下来,我们定义模型并编译训练模型。

# define model
train, infenc, infdec = define_models(n_features, n_features, 128)
train.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['acc'])

接下来,我们生成包含 10 万个样本的训练集并训练模型。

# generate training dataset
X1, X2, y = get_dataset(n_steps_in, n_steps_out, n_features, 100000)
print(X1.shape,X2.shape,y.shape)
# train model
train.fit([X1, X2], y, epochs=1)

模型训练完成之后,我们就可以对其进行评估了。评估的办法是对 100 个源序列进行预测并计算目标序列预测正确的个数。我们可以在解码的序列上使用 numpy 的 array_equal() 函数来检查是否相等。

# evaluate LSTM
total, correct = 100, 0
for _ in range(total):
	X1, X2, y = get_dataset(n_steps_in, n_steps_out, n_features, 1)
	target = predict_sequence(infenc, infdec, X1, n_steps_out, n_features)
	if array_equal(one_hot_decode(y[0]), one_hot_decode(target)):
		correct += 1
print('Accuracy: %.2f%%' % (float(correct)/float(total)*100.0))

最后,我们将生成一些预测并打印出解码的源序列、目标序列和预测目标序列,以检查模型是否按预期的那样运行。

将所有这些元素放在一起,下面列出了完整的代码示例。

from random import randint
from numpy import array
from numpy import argmax
from numpy import array_equal
from keras.utils import to_categorical
from keras.models import Model
from keras.layers import Input
from keras.layers import LSTM
from keras.layers import Dense

# generate a sequence of random integers
def generate_sequence(length, n_unique):
	return [randint(1, n_unique-1) for _ in range(length)]

# prepare data for the LSTM
def get_dataset(n_in, n_out, cardinality, n_samples):
	X1, X2, y = list(), list(), list()
	for _ in range(n_samples):
		# generate source sequence
		source = generate_sequence(n_in, cardinality)
		# define padded target sequence
		target = source[:n_out]
		target.reverse()
		# create padded input target sequence
		target_in = [0] + target[:-1]
		# encode
		src_encoded = to_categorical([source], num_classes=cardinality)
		tar_encoded = to_categorical([target], num_classes=cardinality)
		tar2_encoded = to_categorical([target_in], num_classes=cardinality)
		# store
		X1.append(src_encoded)
		X2.append(tar2_encoded)
		y.append(tar_encoded)
	return array(X1), array(X2), array(y)

# returns train, inference_encoder and inference_decoder models
def define_models(n_input, n_output, n_units):
	# define training encoder
	encoder_inputs = Input(shape=(None, n_input))
	encoder = LSTM(n_units, return_state=True)
	encoder_outputs, state_h, state_c = encoder(encoder_inputs)
	encoder_states = [state_h, state_c]
	# define training decoder
	decoder_inputs = Input(shape=(None, n_output))
	decoder_lstm = LSTM(n_units, return_sequences=True, return_state=True)
	decoder_outputs, _, _ = decoder_lstm(decoder_inputs, initial_state=encoder_states)
	decoder_dense = Dense(n_output, activation='softmax')
	decoder_outputs = decoder_dense(decoder_outputs)
	model = Model([encoder_inputs, decoder_inputs], decoder_outputs)
	# define inference encoder
	encoder_model = Model(encoder_inputs, encoder_states)
	# define inference decoder
	decoder_state_input_h = Input(shape=(n_units,))
	decoder_state_input_c = Input(shape=(n_units,))
	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)
	# return all models
	return model, encoder_model, decoder_model

# generate target given source sequence
def predict_sequence(infenc, infdec, source, n_steps, cardinality):
	# encode
	state = infenc.predict(source)
	# start of sequence input
	target_seq = array([0.0 for _ in range(cardinality)]).reshape(1, 1, cardinality)
	# collect predictions
	output = list()
	for t in range(n_steps):
		# predict next char
		yhat, h, c = infdec.predict([target_seq] + state)
		# store prediction
		output.append(yhat[0,0,:])
		# update state
		state = [h, c]
		# update target sequence
		target_seq = yhat
	return array(output)

# decode a one hot encoded string
def one_hot_decode(encoded_seq):
	return [argmax(vector) for vector in encoded_seq]

# configure problem
n_features = 50 + 1
n_steps_in = 6
n_steps_out = 3
# define model
train, infenc, infdec = define_models(n_features, n_features, 128)
train.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['acc'])
# generate training dataset
X1, X2, y = get_dataset(n_steps_in, n_steps_out, n_features, 100000)
print(X1.shape,X2.shape,y.shape)
# train model
train.fit([X1, X2], y, epochs=1)
# evaluate LSTM
total, correct = 100, 0
for _ in range(total):
	X1, X2, y = get_dataset(n_steps_in, n_steps_out, n_features, 1)
	target = predict_sequence(infenc, infdec, X1, n_steps_out, n_features)
	if array_equal(one_hot_decode(y[0]), one_hot_decode(target)):
		correct += 1
print('Accuracy: %.2f%%' % (float(correct)/float(total)*100.0))
# spot check some examples
for _ in range(10):
	X1, X2, y = get_dataset(n_steps_in, n_steps_out, n_features, 1)
	target = predict_sequence(infenc, infdec, X1, n_steps_out, n_features)
	print('X=%s y=%s, yhat=%s' % (one_hot_decode(X1[0]), one_hot_decode(y[0]), one_hot_decode(target)))

运行示例,首先打印出准备好的数据集的 shape。

(100000, 6, 51) (100000, 3, 51) (100000, 3, 51)

接下来,训练模型。 你应该看到一个进度条,在现代多核 CPU 上运行应该不到一分钟。

100000/100000 [==============================] - 50s - loss: 0.6344 - acc: 0.7968

再接下来,评估模型并打印准确度。 我们可以看到模型在新的随机生成的样本上实现了 100% 的准确度。

Accuracy: 100.00%

最后,生成 10 个新的例子,并预测目标序列。 再一次,我们可以看到,模型正确地预测了每种情况下的输出序列,并且期望值与源序列中逆序的前 3 个元素相匹配。

X=[22, 17, 23, 5, 29, 11] y=[23, 17, 22], yhat=[23, 17, 22]
X=[28, 2, 46, 12, 21, 6] y=[46, 2, 28], yhat=[46, 2, 28]
X=[12, 20, 45, 28, 18, 42] y=[45, 20, 12], yhat=[45, 20, 12]
X=[3, 43, 45, 4, 33, 27] y=[45, 43, 3], yhat=[45, 43, 3]
X=[34, 50, 21, 20, 11, 6] y=[21, 50, 34], yhat=[21, 50, 34]
X=[47, 42, 14, 2, 31, 6] y=[14, 42, 47], yhat=[14, 42, 47]
X=[20, 24, 34, 31, 37, 25] y=[34, 24, 20], yhat=[34, 24, 20]
X=[4, 35, 15, 14, 47, 33] y=[15, 35, 4], yhat=[15, 35, 4]
X=[20, 28, 21, 39, 5, 25] y=[21, 28, 20], yhat=[21, 28, 20]
X=[50, 38, 17, 25, 31, 48] y=[17, 38, 50], yhat=[17, 38, 50]

现在,你就拥有了一个 encoder-decoder LSTM 模型的模板,你可以将其应用到你自己的 sequence-to-sequence 预测问题上了。

4. 相关阅读

如果您想深入了解,可以查看下面更多有关该主题的资源。

翻译自 MachineLearningMastery《How to Develop an Encoder-Decoder Model for Sequence-to-Sequence Prediction in Keras》,作者 Jason Brownlee。小昇翻译,部分内容有删改。