邵洲作者

开发Encoder-Decoder LSTM模型的简单教程(附代码)

LSTM是一种时间递归神经网络,适合于处理和预测时间序列中间隔和延迟相对较长的重要事件。在自然语言处理语言识别等一系列的应用上都取得了很好的效果。

《Long Short Term Memory Networks with Python》是澳大利亚机器学习专家Jason Brownlee的著作,里面详细介绍了LSTM模型的原理和使用。

该书总共分为十四个章节,具体如下:

第一章:什么是LSTMs?

第二章:怎么样训练LSTMs?

第三章:怎么样准备LSTMs的数据?

第四章:怎么样在Keras中开发LSTMs?

第五章:序列预测建模

第六章:如何开发一个Vanilla LSTM模型?

第七章:怎么样开发Stacked LSTMs?

第八章:开发CNN LSTM模型(本期内容)

第九章:开发Encoder-Decoder LSTMs(本期内容)

第十章:开发Bidirectional LSTMs(下周一发布)

第十一章:开发生成LSTMs

第十二章:诊断和调试LSTMs

第十三章:怎么样用LSTMs做预测?

第十四章:更新LSTMs模型

本文的作者对此书进行了翻译整理之后,分享给大家,本文是第九期内容。

第一期内容为:一万字纯干货|机器学习博士手把手教你入门LSTM(附代码资料)

第二期内容为:干货推荐|如何基于时间的反向传播算法来训练LSTMs?

第三期内容为:干货推荐|如何准备用于LSTM模型的数据并进行序列预测?(附代码)

第四期内容为:机器学习博士带你入门|一文学会如何在Keras中开发LSTMs(附代码)

第五期内容为:初学者如何避免在序列预测问题中遇到的陷阱?

第六期内容为:如何开发和评估Vanilla LSTM模型?

第七期内容为:博士带你学LSTM|怎么样开发Stacked LSTMs?(附代码)

第八期内容为:博士带你学LSTM|手把手教你开发CNN LSTM模型,并应用在Keras中(附代码)

我们还将继续推出一系列的文章来介绍里面的详细内容,和大家一起来共同学习。

9.0 前言

9.0.1 课程目标

本课程的目标是学习怎么样开发Encoder-Decoder LSTM模型。完成本课程之后,你将会学习到:

  • Encoder-Decoder LSTM的结构以及怎么样在Keras中实现它;

  • 加法序列到序列的预测问题;

  • 怎么样开发一个Encoder-Decoder LSTM模型用来解决加法seq2seq预测问题。

9.1 课程概览

本课程被分为7个部分,它们是:

  1. Encoder-Decoder LSTM;

  2. 加法预测问题;

  3. 定义并编译模型;

  4. 拟合模型;

  5. 评估模型;

  6. 用模型做预测;

  7. 完成例子

让我们开始吧!

9.2 Encoder-Decoder LSTM模型

9.2.1 序列到序列预测问题

序列预测问题通常涉及预测真实序列中的下一个值或者输出输入序列的类标签。这通常被构造为一个输入时间步长序列到一个输出时间步长(例如,one-to-one)或者多个输入时间步长到一个输出时间步长(many-to-many)类型的序列预测问题。

有一种更具挑战性的序列预测问题,它以序列作为输入,需要序列预测作为输出。这些被称为序列到序列预测问题,或者简称为seq2seq问题。使这些问题具有挑战性的一个建模问题是输入和输出序列的长度可能变化。由于存在多个输入时间步长和多个输出时间步长,这种形式的问题被称为many-to-many序列预测问题。

9.2.2 结构

seq2seq预测问题的一种被证明是非常有效的方法被称为Encoder-Decoder LSTM。该体系结构包括两个模型:一个用于读取输入序列并将其编码成一个固定长度的向量,另一个用于解码固定长度的向量并输出预测序列。模型的使用相应地给出了该体系结构的名字——Encoder-Decoder LSTM,专门针对seq2seq问题而设计。

... RNN Encoder-Decoder由两个循环神经元网络(RNN)所组成,它们作为编码和解码对存在。编码器可将可变长度的源序列映射到一个固定长度的向量,同时解码器将矢量使用回可变长度的目标序列。

— Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation, 2014

Encoder-Decoder LSTM是为处理自然语言处理问题而开发的,它显示了-of-the-art的性能,特别是在文本翻译领域称为统计机器翻译。这种体系结构的创新是在模型的最核心的部分使用了固定大小的内部表示,这里输入序列被读取并且输出序列从中被读取。由于这个原因,该方法被称为序列嵌入。

在英语与法语翻译的体系结构的一个应用中,编码的英语短语的内部表示被可视化。输出的图像解释了翻译任务中短语管理的一个定性的有意义的学习结构。

提出的RNN Encoder-Decoder自然地生成一个短语的连续空间表示。[...]从可视化角度,很明显地RNN Encoder-Decoder捕获语义和句法结构的短语。

— Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation, 2014.

在翻译任务上,该模型在输入顺序颠倒时更有效。此外,即使在很长的输入序列上,该模型也被证明是有效的。

我们能够很好地完成长句,因为我们颠倒了原来句子的词序,而不是训练和测试集中的目标句子。通过这样做,我们引入了许多短期的依赖关系,是的优化问题变得简单多了。...源句倒换的简单技巧是这项工作的主要技术贡献之一。

— Sequence to Sequence Learning with Neural Networks, 2014.

这种方法也被用于图像输入,其中卷积神经网络被用作输入图像上的特征提取器,然后由解码器LSTM读取。

...我们建议遵循这个优雅的配方,有一个深度卷积神经网络(CNN)来取代encoder RNN。[...]使用CNN作为图像编码器是很自然的,首先对图像分类任务进行预训练,最后使用隐藏层作为RNN解码器输出句子的输入。

— Show and Tell: A Neural Image Caption Generator, 2014.

图 9.1 Encoder-decoder LSTM结构

9.2.3 应用

下面的列表突出了Encoder-Decoder LSTM结构的一些有趣的应用。

  • 机器翻译,如短语的英译法语。

  • 学习执行,例如小程序的计算结果;

  • 图像标题,例如用于生成图像;

  • 对话建模,例如对语篇产生的答案的问题;

  • 运动序列的分类,例如对一系列的手势生成一系列的命令;

9.2.4 实现

Encoder-Decoder LSTM可以直接在Keras中实现。我们可以认为模型由两个关键部分组成:编码器和解码器。首先,输入序列一次向网络显示一个编码字符。我们需要一个编码水平来学习输入序列中的步骤之间的关系,并开发这些关系的内部表示。

一个或多个LSTM层可用于显示编码器模型。这个模型的输出时一个固定大小的向量,表示输入许的内部表示。这个层中的存储单元的数目与这个大小的向量长度无关。

  1. model = Sequential()

  2. model.add(LSTM(..., input_shape=(...)))

表 9.1 Vanilla LSTM模型的例子

解码器必须将所学习的输入序列的内部表示转换成正确的输出序列。还可以使用一个或多个LSTM层来实现编解码模型。这个模型是从编码器模型的大小输出中读取的。与Vanilla LSTM一样,一个Dense层可以被用做网络的输出。通过将Dense层包裹在TimeDistributed层中,同样的权重可以被用来在每个输出序列中输出每个时间步长。

  1. model.add(LSTM(..., return_sequences=True))

  2. model.add(TimeDistributed(Dense(...)))

表 9.2 用TimeDistributed包裹Dense层的LSTM模型的例子

但是有一个问题。我们必须把编码器和解码器连接起来,但是它们不适合。也就是说,编码器将产生输出的二维矩阵,其中长度由层中的存储单元的数目决定。解码器是一个LSTM层,它期望3D输入(样本、时间步长、特征),以产生由该问题产生的不同长度的解码序列。

如果你试图强迫这些碎片在一起,你会得到一个错误,表明解码器的输出是2D,需要3D解码器。我们可以用重复向量层来解决这个问题。该层简单地多次重复所提出的2D输入以创建3D输出。

RepeatVector层可以像适配器一样使用,以将网络的编码器和解码器部分适配在一起。我们可以配置重复向量以在输出序列中的每个时间步长中重复一个固定长度向量。

  1. model.add(RepeatVector(...))

表 9.3 一个RepeatVector层的例子

把它们放在一起,我们得到:

  1. model = Sequential()

  2. model.add(LSTM(..., input_shape=(...)))

  3. model.add(RepeatVector(...))

  4. model.add(LSTM(..., return_sequences=True))

  5. model.add(TimeDistributed(Dense(...)))

表 9.4 Encoder-Decoder模型的例子

总的来说,使用RepeatVector作为编码器的固定大小的2D输出,以适应解码器期望的不同长度和3D输入。TimeDistributed wrapper允许相同的输出层用于输出序列中的每个元素。

9.3 加法预测问题

加法问题是一个序列到序列,或者seq2seq的预测问题。它被用于 Wojciech Zaremba和Ilya Sutskever2014年名为《Learning to Execute》的论文来探索Encoder-Decoder LSTM的能力,其中的体系结构被证明学习计算小程序的输出。

该问题被定义为计算两个输入数的和的输出。这是具有挑战性的,因为每个数字和数学符号被提供为字符类型的,并且预期输出也被预期为字符。例如,输入10+6与输出16将由序列表示:

  1. Input: [ 1 , 0 , + , 6 ]

  2. Output: [ 1 , 6 ]

表 9.5 加法问题中输入和输出序列的例子

该模型不仅要学习字符的整数性质,还要学习要执行的数学运算的性质。注意序列是如何重要的,并且随机地拖动输入将创建与输出序列无关的无意义序列。还是要注意序列在输入和输出序列中如何变化。在技术上,这使得加法预测问题是一个序列到序列的问题,需要many-to-many的模型来求解。

 图 9.2 用many-to-many预测模型构造加法预测问题

我们可以通过添加两个数字来保持事物的简单性,但是我们可以看到如何将其缩放成可变数量的术语和数学运算,这些数学运算可以作为模型的输入来学习和推广。这个问题可以用Python来实现。我们可以把它们分成以下步骤:

  1. 生成加法对;

  2. 填充字符串的整数;

  3. 整数编码序列;

  4. one hot编码序列;

  5. 序列生成流水线;

  6. 解码序列。

9.3.1 生成加法对

第一步是生成随机整数序列及其总和。我们可以把它放在一个名为randomsumpairs()的函数中,如下所示:

  1. from random import seed

  2. from random import randint

  3. # generate lists of random integers and their sum

  4. def random_sum_pairs(n_examples, n_numbers, largest):

  5.    X, y = list(), list()

  6.    for i in range(n_examples):

  7.        in_pattern = [randint(1,largest) for _ in range(n_numbers)]

  8.        out_pattern = sum(in_pattern)

  9.        X.append(in_pattern)

  10.        y.append(out_pattern)

  11.    return X, y

  12. seed(1)

  13. n_samples = 1

  14. n_numbers = 2

  15. largest = 10

  16. # generate pairs

  17. X, y = random_sum_pairs(n_samples, n_numbers, largest)

  18. print(X, y)

表 9.6 生成随机序列对的例子

运行这个函数只打印一个在1到10之间添加两个随机整数的例子。

  1. [[3, 10]] [13]

表 9.7 输出生成一个随机序列对的例子

9.3.2 填充字符串的整数

下一步是将整数转换为字符串。输入字符串将是“10+10”格式,输出字符串将是“20”格式。这个函数的关键是填充数字,以确保每个输出和输出序列具有相同的字符数。填充字符应该与数据无关,因此模型可以学会忽略它们。在这种情况下,我们使用空格字符串(“ ”)填充,并在左侧填充字符串,保持最右边的信息。

还有其他的方法来填充,比如单个填充每个术语。试试看它是否会带来更好的性能。填充需要我们知道最长序列的长度。我们可以通过计算我们可以生成的最大整数的 $log_{10}()$和这个数字的上线来计算每个数字需要多少字符。我们增加了1个最大的数字,以确保我们期望3个字符而不是2个字符,对于一个圆最大的数字,比如200个取结果的上限(例如 $ceil(log10(largest+1)$)。然后,我们需要添加正确数目的加符号(例如, n numbers-1)。

  1. max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

表 9.8 计算输入序列最大长度的例子

我们可以用一个实际的例子来做这个具体的例子,其中总的数量(n个数)是3,最大的值(最大)是10。

  1. max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

  2. max_length = 3 * ceil(log10(10+1)) + 3 - 1

  3. max_length = 3 * ceil(1.0413926851582251) + 3 - 1

  4. max_length = 3 * 2 + 3 - 1

  5. max_length = 6 + 3 - 1

  6. max_length = 8

表 9.9 最大输入序列长度工作实例

直观来说,我们期望每个词两个空间(例如['1', '0'])乘以3个词,或者最大长度为6个空间的输入序列,如果有加法符号的话就再加两位(例如:[‘1’,‘0’,‘+’,‘1’,‘0’,‘+’,‘1’,‘0’])使得最大的可能序列长度为8个字符。这就是我们在实际例子中看到的。

在输出序列上重复一个类似的过程,当然没有加号。

  1. max_length = ceil(log10(n_numbers * (largest+1)))

表 9.10 计算输出序列长度的例子

再次,我们可以通过计算期望的最大输出序列长度来具体实现,上面的例子的总数量(n个数)是3,最大值(最大)是10。

  1. max_length = ceil(log10(n_numbers * (largest+1)))

  2. max_length = ceil(log10(3 * (10+1)))

  3. max_length = ceil(log10(33))

  4. max_length = ceil(1.5185139398778875)

  5. max_length = 2

表 9.11 最大输出序列长度的工作实例

同样的,直观的,我们期望最大可能的加法是10+10+10或者30的 值。这将需要最大长度为2,这就是我们在工作示例中所看到的。下面的示例添加了string()函数,并用一个输入/输出对来演示它的用法。

  1. from random import seed

  2. from random import randint

  3. from math import ceil

  4. from math import log10

  5. # generate lists of random integers and their sum

  6. def random_sum_pairs(n_examples, n_numbers, largest):

  7.    X, y = list(), list()

  8.    for i in range(n_examples):

  9.        in_pattern = [randint(1,largest) for _ in range(n_numbers)]

  10.        out_pattern = sum(in_pattern)

  11.        X.append(in_pattern)

  12.        y.append(out_pattern)

  13.    return X, y

  14. # convert data to strings

  15. def to_string(X, y, n_numbers, largest):

  16.    max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

  17.    Xstr = list()

  18.    for pattern in X:

  19.        strp = '+'.join([str(n) for n in pattern])

  20.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  21.        Xstr.append(strp)

  22.    max_length = ceil(log10(n_numbers * (largest+1)))

  23.    ystr = list()

  24.    for pattern in y:

  25.        strp = str(pattern)

  26.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  27.        ystr.append(strp)

  28.    return Xstr, ystr

  29. seed(1)

  30. n_samples = 1

  31. n_numbers = 2

  32. largest = 10

  33. # generate pairs

  34. X, y = random_sum_pairs(n_samples, n_numbers, largest)

  35. print(X, y)

  36. # convert to strings

  37. X, y = to_string(X, y, n_numbers, largest)

  38. print(X, y)

表 9.12 将一个序列对转换成插补字符的例子

运行例子首先输出整数序列,并插补同样序列的字符串表达。

  1. [[3, 10]] [13]

  2. ['3+10'] ['13']

表 9.13 将一个序列对转换为插补字符的输出的例子

9.3.3 整数编码序列

接下来,我们需要将字符串中的每个字符编码为整数值。在神经网络中我们必须用数字进行工作,而不是字符。整数编码将问题转化为一个分类问题,其中输出序列可以被认为是具有11个可能值的类输出。这恰好是具有一些序数关系的整数(前10类值)。为了执行此编码,我们必须确定字符串编码中可能出现的符号的完整字母表,如下:

  1. alphabet = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , + , ]

表 9.14 定义一个字符表的例子

然后,整数编码成为一个简单的过程,构建一个字符串到整数偏移的查找表,并逐个转换每个字符串的每个字符。下面的示例提供整数编码的integer_encode()函数,并演示它如何使用。

  1. from random import seed

  2. from random import randint

  3. from math import ceil

  4. from math import log10

  5. # generate lists of random integers and their sum

  6. def random_sum_pairs(n_examples, n_numbers, largest):

  7.    X, y = list(), list()

  8.    for i in range(n_examples):

  9.        in_pattern = [randint(1,largest) for _ in range(n_numbers)]

  10.        out_pattern = sum(in_pattern)

  11.        X.append(in_pattern)

  12.        y.append(out_pattern)

  13.    return X, y

  14. # convert data to strings

  15. def to_string(X, y, n_numbers, largest):

  16.    max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

  17.    Xstr = list()

  18.    for pattern in X:

  19.        strp = '+ '.join([str(n) for n in pattern])

  20.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  21.        Xstr.append(strp)

  22.    max_length = ceil(log10(n_numbers * (largest+1)))

  23.    ystr = list()

  24.    for pattern in y:

  25.        strp = str(pattern)

  26.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  27.        ystr.append(strp)

  28.    return Xstr, ystr

  29. # integer encode strings

  30. def integer_encode(X, y, alphabet):

  31.    char_to_int = dict((c, i) for i, c in enumerate(alphabet))

  32.    Xenc = list()

  33.    for pattern in X:

  34.        integer_encoded = [char_to_int[char] for char in pattern]

  35.        Xenc.append(integer_encoded)

  36.    yenc = list()

  37.    for pattern in y:

  38.        integer_encoded = [char_to_int[char] for char in pattern]

  39.        yenc.append(integer_encoded)

  40.    return Xenc, yenc

  41. seed(1)

  42. n_samples = 1

  43. n_numbers = 2

  44. largest = 10

  45. # generate pairs

  46. X, y = random_sum_pairs(n_samples, n_numbers, largest)

  47. print(X, y)

  48. # convert to strings

  49. X, y = to_string(X, y, n_numbers, largest)

  50. print(X, y)

  51. # integer encode

  52. alphabet = [ '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , '+' , ' ']

  53. X, y = integer_encode(X, y, alphabet)

  54. print(X, y)

表 9.15 整数编码插补序列的例子

运行例子打印每个字符串编码模式的整数编码版本。我们可以看到空字符串(“ ”)被编码成了11,字符三(“3”)被编码成了3,等等。

  1. [[3, 10]] [13]

  2. ['3+ 10'] ['13']

  3. [[3, 10, 11, 1, 0]] [[1, 3]]

表 9.16 从整数编码输入和输出序列输出的例子

9.3.4 one hot编码序列

下一步是对整数编码序列进行二进制编码。这涉及到将每个整数转换成与字母相同长度的二进制向量,并用1标记特定的整数。例如,0个整数表示“0”字符,并将其编码为11个向量元素的第0位位1的二进制向量: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]。下面的例子为二进制编码定义了onehotencode() 函数,并演示了如何使用它。

  1. from random import seed

  2. from random import randint

  3. from math import ceil

  4. from math import log10

  5. # generate lists of random integers and their sum

  6. def random_sum_pairs(n_examples, n_numbers, largest):

  7.    X, y = list(), list()

  8.    for i in range(n_examples):

  9.        in_pattern = [randint(1,largest) for _ in range(n_numbers)]

  10.        out_pattern = sum(in_pattern)

  11.        X.append(in_pattern)

  12.        y.append(out_pattern)

  13.    return X, y

  14. # convert data to strings

  15. def to_string(X, y, n_numbers, largest):

  16.    max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

  17.    Xstr = list()

  18.    for pattern in X:

  19.        strp = '+'.join([str(n) for n in pattern])

  20.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  21.        Xstr.append(strp)

  22.    max_length = ceil(log10(n_numbers * (largest+1)))

  23.    ystr = list()

  24.    for pattern in y:

  25.        strp = str(pattern)

  26.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  27.        ystr.append(strp)

  28.    return Xstr, ystr

  29. # integer encode strings

  30. def integer_encode(X, y, alphabet):

  31.    char_to_int = dict((c, i) for i, c in enumerate(alphabet))

  32.    Xenc = list()

  33.    for pattern in X:

  34.        integer_encoded = [char_to_int[char] for char in pattern]

  35.        Xenc.append(integer_encoded)

  36.    yenc = list()

  37.    for pattern in y:

  38.        integer_encoded = [char_to_int[char] for char in pattern]

  39.        yenc.append(integer_encoded)

  40.    return Xenc, yenc

  41. # one hot encode

  42. def one_hot_encode(X, y, max_int):

  43.    Xenc = list()

  44.    for seq in X:

  45.        pattern = list()

  46.        for index in seq:

  47.            vector = [0 for _ in range(max_int)]

  48.            vector[index] = 1

  49.            pattern.append(vector)

  50.        Xenc.append(pattern)

  51.    yenc = list()

  52.    for seq in y:

  53.        pattern = list()

  54.        for index in seq:

  55.            vector = [0 for _ in range(max_int)]

  56.            vector[index] = 1

  57.            pattern.append(vector)

  58.        yenc.append(pattern)

  59.    return Xenc, yenc

  60. seed(1)

  61. n_samples = 1

  62. n_numbers = 2

  63. largest = 10

  64. # generate pairs

  65. X, y = random_sum_pairs(n_samples, n_numbers, largest)

  66. print(X, y)

  67. # convert to strings

  68. X, y = to_string(X, y, n_numbers, largest)

  69. print(X, y)

  70. # integer encode

  71. alphabet = [ '0' , '1' , '2' , '3', '4' , '5' , '6' , '7' , '8' , '9' , '+' ,' ']

  72. X, y = integer_encode(X, y, alphabet)

  73. print(X, y)

  74. # one hot encode

  75. X, y = one_hot_encode(X, y, len(alphabet))

  76. print(X, y)

表 9.17 one hot编码一个整数编码序列的例子

运行示例为每个整数编码打印二进制编码序列。我添加了一些新行,使输入和输出的二进制编码更加清晰。可以看到,一个和模式变成5个二进制编码向量的序列,每一个都有11个元素。输出或者和成为2个二进制编码向量的序列,每一个都具有11个元素。

  1. [[3, 10]] [13]

  2. ['3+10'] ['13']

  3. [[3, 10, 1, 0]] [[1, 3]]

  4. [[[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]] [[[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]]]

表 9.18 one hot编码一个整数编码序列的输出的例子

9.3.5 序列生成流水线

我们可以将所有这些步骤结合到一个名为generate_data()的函数中,如下所示。给定设计的样本数量、术语数量、每个术语的最大值和可能字符的字母表,函数将生成一组输入和输出序列。

  1. # generate an encoded dataset

  2. def generate_data(n_samples, n_numbers, largest, alphabet):

  3.    # generate pairs

  4.    X, y = random_sum_pairs(n_samples, n_numbers, largest)

  5.    # convert to strings

  6.    X, y = to_string(X, y, n_numbers, largest)

  7.    # integer encode

  8.    X, y = integer_encode(X, y, alphabet)

  9.    # one hot encode

  10.    X, y = one_hot_encode(X, y, len(alphabet))

  11.    # return as NumPy arrays

  12.    X, y = array(X), array(y)

  13.    return X, y

表 9.19 生成一个序列、编码和对其进行变型以适应LSTM模型的例子

9.3.6 解码序列

最后,我们需要反转编码来将输出向量转换成数字,这样我们就可以将预期输出整数有预测整数进行比较。下面的invert()函数执行此操作。关键是使用argmax()函数将二进制编码转回到整数,然后将整数转换成字符,使用整数的反向映射到字母表中的字符。

  1. # invert encoding

  2. def invert(seq, alphabet):

  3.    int_to_char = dict((i, c) for i, c in enumerate(alphabet))

  4.    strings = list()

  5.    for pattern in seq:

  6.        string = int_to_char[argmax(pattern)]

  7.        strings.append(string)

  8.    return ''.join(strings)

表 9.20 决定一个编码输入或者输出序列的例子

现在我们为这个例子准备好了所有的事情了。

9.4 定义并编译模型

第一步是定义一个特定的序列预测问题。我们必须指定3个参数作为generate_data()函数(如上)的输入来生成输入-输出序列的样本:

  • n_term:等式中单词的数目(例如,2则为10+10)。

  • largest:每个单词的最大数(例如,10则为值在0-10之间)。

  • alphabet:用于编码输入和输出序列的字母表(例如,0-9,+和“ ”)。

我们将使用具有适度复杂性的问题的配置。每个实例由3个术语组成,每个术语的最大值为10.不管值为0-9,+,还是“0”,字母表保持不变。

  1. # number of math terms

  2. n_terms = 3

  3. # largest value for any single input digit

  4. largest = 10

  5. # scope of possible symbols for each input or output time step

  6. alphabet = [str(x) for x in range(10)] + [ '+' , ' ']

表 9.21 配置问题实例的例子

由于加法问题的特殊性,网络需要三个配置值。

  • n_chars:一个时间步长的字母表的大小(例如,12对应0,9,'+'和’ ‘)。

  • ninseq_length:编码输入序列的时间步长(例如,8的时候对应'10+10+10')

  • noutseq_length:编码输出序列的时间步长(例如,2时候对应'30')。

nchars变量用于对输入层中的特征数目和输出层中的每个输入和输出时间步长的特征数进行分解。使用ninseqlength变量来定义时间步长的数量以在RepeatVector中重复编码输入,这反过来定义了序列喂入用于产生输出序列的解码器中的长度。ninseqlength和noutseqlength的定义使用了来自tostring()函数相同的代码,tostring()函数是用作将整数序列映射为字符串的。

  1. # size of alphabet: (12 for 0-9, + and )

  2. n_chars = len(alphabet)

  3. # length of encoded input sequence (8 for 10+10+10)

  4. n_in_seq_length = n_terms * ceil(log10(largest+1)) + n_terms - 1

  5. # length of encoded output sequence (2 for 30 )

  6. n_out_seq_length = ceil(log10(n_terms * (largest+1)))

表 9.22 在问题实例的基础上定义网络配置的例子

现在我们准备好定义Encoder-Decoder LSTM了。我们将使用一个单一的LSTM层的编码器和另一个单一层的解码器。编码器具有75个存储单元和50个存储单元的解码器。记忆细胞的数量是通过一次次的实验和错误确定的。由于输入序列相对输出序列较长,所以编码器和解码器中的层的大小不对称似乎是一种自然的组织。

输出层使用可预测的12个可能类别的分类log损失。使用了有效的Adam算法实现梯度下降法,并且在训练和模型评估期间计算精度。

  1. # define LSTM

  2. model = Sequential()

  3. model.add(LSTM(75, input_shape=(n_in_seq_length, n_chars)))

  4. model.add(RepeatVector(n_out_seq_length))

  5. model.add(LSTM(50, return_sequences=True))

  6. model.add(TimeDistributed(Dense(n_chars, activation= 'softmax' )))

  7. model.compile(loss= categorical_crossentropy , optimizer= 'adam' , metrics=[ 'accuracy' ])

  8. print(model.summary())

表 9.23 定义并编译Encoder-Decoder LSTM的例子

运行示例打印网络结构的摘要。我们可以看到,编码器将输出一个固定大小的向量,对于给定的输入序列长度为75。该序列被重复2次,以提供75个特征的2个时间步长序列到解码器。解码器将50个特征的两个时间步长输入到Dense输出层,通过一个TimeDistributed wrapper一次处理这些输出,以每次输出一个编码字符。

  1. _________________________________________________________________

  2. Layer (type)                 Output Shape              Param #  

  3. =================================================================

  4. lstm_1 (LSTM)                (None, 75)                26400    

  5. _________________________________________________________________

  6. repeat_vector_1 (RepeatVecto (None, 2, 75)             0        

  7. _________________________________________________________________

  8. lstm_2 (LSTM)                (None, 2, 50)             25200    

  9. _________________________________________________________________

  10. time_distributed_1 (TimeDist (None, 2, 12)             612      

  11. =================================================================

  12. Total params: 52,212

  13. Trainable params: 52,212

  14. Non-trainable params: 0

  15. _________________________________________________________________

  16. None

表 9.24 定义和编译Encoder-Decoder LSTM输出的例子

9.5 拟合模型

该模型适合于75000个随机生成的输入输出对实例的单个周期(epoch)。序列的数目是训练周期(epoch)的代理。总共有75000个数,选定批次大小(batch size)为32个是通过一次次尝试和错误得来的,并不是一个最佳的配置。

  1. # fit LSTM

  2. X, y = generate_data(75000, n_terms, largest, alphabet)

  3. model.fit(X, y, epochs=1, batch_size=32)

表 9.25 拟合定义的Encoder-Decoder LSTM的例子

拟合提供进度条,显示模型在每个批次结束时的损失和准确性。该模型不需要很长的时间就可以安装在CPU上。如果进度条干扰您的开发环境,您可以通过在fit()函数中设置verbose=0来关闭它。

  1. 75000/75000 [==============================] - 37s - loss: 0.6982 - acc: 0.7943

表 9.26 拟合定义的Encoder-Decoder LSTM的输出的例子

9.6 评价模型

我们可以通过在100个不同的随机产生的输入-输出对上生成预测来评估模型。结果将给出一般随机生成示例的模型学习能力的估计。

  1. # evaluate LSTM

  2. X, y = generate_data(100, n_terms, largest, alphabet)

  3. loss, acc = model.evaluate(X, y, verbose=0)

  4. print( 'Loss: %f, Accuracy: %f' % (loss, acc*100))

表 9.27 评价拟合Encoder-Decoder LSTM拟合的例子

运行该示例同时打印模型的log损失和准确性。由于神经网络的随机性,您的特定值可能有所不同,但是模型的精度应该是在90%以内的。

  1. Loss: 0.128379, Accuracy: 100.000000

表 9.28 评估拟合Encoder-Decoder LSTM输出的例子

9.7 用模型做预测

我们可以使用拟合模型进行预测。我们将演示一次做出一个预测,并提供解码输入、预期输出和预测输出的摘要。打印解码输出使我们对问题和模型能力有了更具体的联系。在这里,我们生成10个新的随机输入-输出序列对,使用每一个拟合模型进行预测,解码所涉及的所有序列,并将它们打印到屏幕上。

  1. # predict

  2. for _ in range(10):

  3.    # generate an input-output pair

  4.    X, y = generate_data(1, n_terms, largest, alphabet)

  5.    # make prediction yhat = model.predict(X, verbose=0)

  6.    # decode input, expected and predicted

  7.    in_seq = invert(X[0], alphabet)

  8.    out_seq = invert(y[0], alphabet)

  9.    predicted = invert(yhat[0], alphabet)

  10.    print( '%s = %s (expect %s)' % (in_seq, predicted, out_seq))

表 9.29 使用Encoder-Decoder LSTM做预测的例子

运行该示例表明,模型使大部分序列正确。你生成的叶鼎序列和模型的学习能力在10个例子中会有所不同。尝试运行预测几次,以获得良好的模型行为的感觉。

  1. 9+10+9 = 27 (expect 28)

  2. 9+6+9 = 24 (expect 24)

  3. 8+9+10 = 27 (expect 27)

  4. 9+9+10 = 28 (expect 28)

  5. 2+4+5 = 11 (expect 11)

  6. 2+9+7 = 18 (expect 18)

  7. 7+3+2 = 12 (expect 12)

  8. 4+1+4 = 9 (expect 9)

  9. 8+6+7 = 21 (expect 21)

  10. 5+2+7 = 14 (expect 14)

表 9.30 用拟合Encoder-Decoder LSTM做预测输出的例子

9.8 完整例子

为了完整性,我们将全部的代码列表提供如下供你参考。

  1. from random import seed

  2. from random import randint

  3. from numpy import array

  4. from math import ceil

  5. from math import log10

  6. from math import sqrt

  7. from numpy import argmax

  8. from keras.models import Sequential

  9. from keras.layers import Dense

  10. from keras.layers import LSTM

  11. from keras.layers import TimeDistributed

  12. from keras.layers import RepeatVector

  13. # generate lists of random integers and their sum

  14. def random_sum_pairs(n_examples, n_numbers, largest):

  15.    X, y = list(), list()

  16.    for i in range(n_examples):

  17.        in_pattern = [randint(1,largest) for _ in range(n_numbers)]

  18.        out_pattern = sum(in_pattern)

  19.        X.append(in_pattern)

  20.        y.append(out_pattern)

  21.    return X, y

  22. # convert data to strings

  23. def to_string(X, y, n_numbers, largest):

  24.    max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

  25.    Xstr = list()

  26.    for pattern in X:

  27.        strp = '+' .join([str(n) for n in pattern])

  28.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  29.        Xstr.append(strp)

  30.    max_length = ceil(log10(n_numbers * (largest+1)))

  31.    ystr = list()

  32.    for pattern in y:

  33.        strp = str(pattern)

  34.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  35.        ystr.append(strp)

  36.    return Xstr, ystr

  37. # integer encode strings

  38. def integer_encode(X, y, alphabet):

  39.    char_to_int = dict((c, i) for i, c in enumerate(alphabet))

  40.    Xenc = list()

  41.    for pattern in X:

  42.        integer_encoded = [char_to_int[char] for char in pattern]

  43.        Xenc.append(integer_encoded)

  44.    yenc = list()

  45.    for pattern in y:

  46.        integer_encoded = [char_to_int[char] for char in pattern]

  47.        yenc.append(integer_encoded)

  48.    return Xenc, yenc

  49. # one hot encode

  50. def one_hot_encode(X, y, max_int):

  51.    Xenc = list()

  52.    for seq in X: pattern = list()

  53.    for index in seq:

  54.        vector = [0 for _ in range(max_int)]

  55.        vector[index] = 1

  56.        pattern.append(vector)

  57.        Xenc.append(pattern)

  58.    yenc = list()

  59.    for seq in y:

  60.        pattern = list()

  61.        for index in seq:

  62.            vector = [0 for _ in range(max_int)]

  63.            vector[index] = 1

  64.            pattern.append(vector)

  65.        yenc.append(pattern)

  66.    return Xenc, yenc

  67. # generate an encoded dataset

  68. def generate_data(n_samples, n_numbers, largest, alphabet):

  69.    # generate pairs

  70.    X, y = random_sum_pairs(n_samples, n_numbers, largest)

  71.    # convert to strings

  72.    X, y = to_string(X, y, n_numbers, largest)

  73.    # integer encode

  74.    X, y = integer_encode(X, y, alphabet)

  75.    # one hot encode

  76.    X, y = one_hot_encode(X, y, len(alphabet))

  77.    # return as numpy arrays

  78.    X, y = array(X), array(y)

  79.    return X, y

  80. # invert encoding

  81. def invert(seq, alphabet):

  82.    int_to_char = dict((i, c) for i, c in enumerate(alphabet))

  83.    strings = list()

  84.    for pattern in seq:

  85.        string = int_to_char[argmax(pattern)]

  86.        strings.append(string)

  87.    return ''.join(strings)

  88. # configure problem

  89. # number of math terms

  90. n_terms = 3

  91. # largest value for any single input digit

  92. largest = 10

  93. # scope of possible symbols for each input or output time step

  94. alphabet = [str(x) for x in range(10)] + [ '+' , ' ']

  95. # size of alphabet: (12 for 0-9, + and )

  96. n_chars = len(alphabet)

  97. # length of encoded input sequence (8 for 10+10+10)

  98. n_in_seq_length = n_terms * ceil(log10(largest+1)) + n_terms - 1

  99. # length of encoded output sequence (2 for 30 )

  100. n_out_seq_length = ceil(log10(n_terms * (largest+1)))

  101. # define LSTM

  102. model = Sequential()

  103. model.add(LSTM(75, input_shape=(n_in_seq_length, n_chars)))

  104. model.add(RepeatVector(n_out_seq_length))

  105. model.add(LSTM(50, return_sequences=True))

  106. model.add(TimeDistributed(Dense(n_chars, activation= 'softmax' )))

  107. model.compile(loss= 'categorical_crossentropy' , optimizer= 'adam' , metrics=[ 'accuracy' ])

  108. print(model.summary())

  109. # fit LSTM

  110. X, y = generate_data(75000, n_terms, largest, alphabet)

  111. model.fit(X, y, epochs=1, batch_size=32)

  112. # evaluate LSTM

  113. X, y = generate_data(100, n_terms, largest, alphabet)

  114. loss, acc = model.evaluate(X, y, verbose=0)

  115. print('Loss: %f, Accuracy: %f' % (loss, acc*100))

  116. # predict

  117. for _ in range(10):

  118.    # generate an input-output pair

  119.    X, y = generate_data(1, n_terms, largest, alphabet)

  120.    # make prediction

  121.    yhat = model.predict(X, verbose=0)

  122.    # decode input, expected and predicted

  123.    in_seq = invert(X[0], alphabet)

  124.    out_seq = invert(y[0], alphabet)

  125.    predicted = invert(yhat[0], alphabet)

  126.    print('%s = %s (expect %s)' % (in_seq, predicted, out_seq))

表 9.31 Encoder-Decoder LSTM在加法预测问题上的完整例子

9.9 扩展阅读

本章节提供了一些扩展阅读的资料。

9.9.1 Encoder-Decoder LSTM论文

  • Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation, 2014.

  • [Sequence to Sequence Learning with Neural Networks, 2014. https://arxiv.org/abs/1409.3215

  • Show and Tell: A Neural Image Caption Generator, 2014.

  • Learning to Execute, 2015.

  • {A Neural Conversational Model, 2015.](https://arxiv.org/abs/1506.05869)

9.9.2 Keras API

  • RepeatVector Keras API.

  • TimeDistributed Keras API.

9.10 扩展

你想更深度地了解Encoder-Decoder LSTMs吗?本章节列出了本课程的一些具有挑战性的扩展。

  • 列出10个可以从Encoder-Decoder LSTM结构中获益的序列到序列预测问题;

  • 增加terms的数量或者数字的数量,并调整模型以获得100%的准确度;

  • 设计一个比较模型大小与问题序列问题复杂度(term和/或数字)的研究;

  • 更新示例以支持给定实例中的可变数量的术语,并调整模型以获得100%的准确度。

  • 增加对其他数学运算的支持,例如减法、除法和乘法。

9.11 总结

在本课程中,你学习到了怎么样开发一个Encoder-Decoder LSTM模型。特别地,你学习到了:

  • Encoder-Decoder LSTM的结构以及怎么样在Keras中实现它;

  • 加法序列到序列的预测问题;

  • 怎么样开发一个Encoder-Decoder LSTM模型用来解决加法seq2seq预测问题。

在下一个章节中,我们将会学习到怎么样开发并评估一个Bidirectional LSTM模型。

AMiner学术头条
AMiner学术头条

AMiner平台由清华大学计算机系研发,拥有我国完全自主知识产权。系统2006年上线,吸引了全球220个国家/地区800多万独立IP访问,数据下载量230万次,年度访问量1000万,成为学术搜索和社会网络挖掘研究的重要数据和实验平台。

https://www.aminer.cn/
专栏二维码
工程Seq2SeqKeras模型LSTMEncoder-Decoder
5
相关数据
权重技术

线性模型中特征的系数,或深度网络中的边。训练线性模型的目标是确定每个特征的理想权重。如果权重为 0,则相应的特征对模型来说没有任何贡献。

机器学习技术

机器学习是人工智能的一个分支,是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、计算复杂性理论等多门学科。机器学习理论主要是设计和分析一些让计算机可以自动“学习”的算法。因为学习算法中涉及了大量的统计学理论,机器学习与推断统计学联系尤为密切,也被称为统计学习理论。算法设计方面,机器学习理论关注可以实现的,行之有效的学习算法。

参数技术

在数学和统计学裡,参数(英语:parameter)是使用通用变量来建立函数和变量之间关系(当这种关系很难用方程来阐述时)的一个数量。

时间递归神经网络技术

时间递归神经网络 (aka.循环神经网络, RNN) 是一类擅长处理序列数据的神经网络,其单元连接形成一个有向环。一般人工神经网络(ANN)由多层神经元组成,典型的连接方式是在前馈神经网络中,仅存在层与层之间的互相连接,而同层神经元之间没有连接。RNN在此基础上结合了隐藏层的循环连接,从而能从序列或时序数据中学习特征和长期依赖关系。RNN隐藏层的每一单独计算单元对应了数据中某个时间节点的状态,它可以是简单神经元、神经元层或各式的门控系统。 每一单元通过参数共享的层间顺序连接,并随着数据序列传播。这一特性使得RNN中每一单元的状态都取决于它的过去状态,从而具有类似“记忆”的功能,可以储存并处理长时期的数据信号。 大多数RNN能处理可变长度的序列,理论上也可以建模任何动态系统。

机器翻译技术

机器翻译(MT)是利用机器的力量「自动将一种自然语言(源语言)的文本翻译成另一种语言(目标语言)」。机器翻译方法通常可分成三大类:基于规则的机器翻译(RBMT)、统计机器翻译(SMT)和神经机器翻译(NMT)。

神经网络技术

(人工)神经网络是一种起源于 20 世纪 50 年代的监督式机器学习模型,那时候研究者构想了「感知器(perceptron)」的想法。这一领域的研究者通常被称为「联结主义者(Connectionist)」,因为这种模型模拟了人脑的功能。神经网络模型通常是通过反向传播算法应用梯度下降训练的。目前神经网络有两大主要类型,它们都是前馈神经网络:卷积神经网络(CNN)和循环神经网络(RNN),其中 RNN 又包含长短期记忆(LSTM)、门控循环单元(GRU)等等。深度学习是一种主要应用于神经网络帮助其取得更好结果的技术。尽管神经网络主要用于监督学习,但也有一些为无监督学习设计的变体,比如自动编码器和生成对抗网络(GAN)。

反向传播算法技术

反向传播(英语:Backpropagation,缩写为BP)是“误差反向传播”的简称,是一种与最优化方法(如梯度下降法)结合使用的,用来训练人工神经网络的常见方法。该方法计算对网络中所有权重计算损失函数的梯度。这个梯度会反馈给最优化方法,用来更新权值以最小化损失函数。 在神经网络上执行梯度下降法的主要算法。该算法会先按前向传播方式计算(并缓存)每个节点的输出值,然后再按反向传播遍历图的方式计算损失函数值相对于每个参数的偏导数。

梯度下降技术

梯度下降是用于查找函数最小值的一阶迭代优化算法。 要使用梯度下降找到函数的局部最小值,可以采用与当前点的函数梯度(或近似梯度)的负值成比例的步骤。 如果采取的步骤与梯度的正值成比例,则接近该函数的局部最大值,被称为梯度上升。

卷积神经网络技术

卷积神经网路(Convolutional Neural Network, CNN)是一种前馈神经网络,它的人工神经元可以响应一部分覆盖范围内的周围单元,对于大型图像处理有出色表现。卷积神经网路由一个或多个卷积层和顶端的全连通层(对应经典的神经网路)组成,同时也包括关联权重和池化层(pooling layer)。这一结构使得卷积神经网路能够利用输入数据的二维结构。与其他深度学习结构相比,卷积神经网路在图像和语音识别方面能够给出更好的结果。这一模型也可以使用反向传播算法进行训练。相比较其他深度、前馈神经网路,卷积神经网路需要考量的参数更少,使之成为一种颇具吸引力的深度学习结构。 卷积网络是一种专门用于处理具有已知的、网格状拓扑的数据的神经网络。例如时间序列数据,它可以被认为是以一定时间间隔采样的一维网格,又如图像数据,其可以被认为是二维像素网格。

映射技术

映射指的是具有某种特殊结构的函数,或泛指类函数思想的范畴论中的态射。 逻辑和图论中也有一些不太常规的用法。其数学定义为:两个非空集合A与B间存在着对应关系f,而且对于A中的每一个元素x,B中总有有唯一的一个元素y与它对应,就这种对应为从A到B的映射,记作f:A→B。其中,y称为元素x在映射f下的象,记作:y=f(x)。x称为y关于映射f的原象*。*集合A中所有元素的象的集合称为映射f的值域,记作f(A)。同样的,在机器学习中,映射就是输入与输出之间的对应关系。

统计机器翻译技术

随着统计学的发展,研究者开始将统计模型应用于机器翻译,这种方法是基于对双语文本语料库的分析来生成翻译结果。这种方法被称为统计机器翻译(SMT)

分类问题技术

分类问题是数据挖掘处理的一个重要组成部分,在机器学习领域,分类问题通常被认为属于监督式学习(supervised learning),也就是说,分类问题的目标是根据已知样本的某些特征,判断一个新的样本属于哪种已知的样本类。根据类别的数量还可以进一步将分类问题划分为二元分类(binary classification)和多元分类(multiclass classification)。

语言识别技术

在自然语言处理中,语言识别或语言猜测是确定给定内容所使用的自然语言的问题。针对该问题的计算方法被视为文本分类的特例,并用各种统计方法解决。

自然语言处理技术

自然语言处理(英语:natural language processing,缩写作 NLP)是人工智能和语言学领域的分支学科。此领域探讨如何处理及运用自然语言;自然语言认知则是指让电脑“懂”人类的语言。自然语言生成系统把计算机数据转化为自然语言。自然语言理解系统把自然语言转化为计算机程序更易于处理的形式。

序列到序列技术

批次技术

模型训练的一次迭代(即一次梯度更新)中使用的样本集。

图像分类技术

图像分类,根据各自在图像信息中所反映的不同特征,把不同类别的目标区分开来的图像处理方法。它利用计算机对图像进行定量分析,把图像或图像中的每个像元或区域划归为若干个类别中的某一种,以代替人的视觉判读。

推荐文章
暂无评论
暂无评论~