原文: https://machinelearningmastery.com/memory-in-a-long-short-term-memory-network/
长短期记忆(LSTM)网络是一种能够学习长序列的循环神经网络。
这使它们与没有记忆的常规多层神经网络区分开来,并且只能学习输入和输出模式之间的映射。
重要的是要理解像 LSTM 这样的复杂神经网络在小型设计问题上的能力,因为这种理解将帮助您将网络扩展到大型甚至是非常大的问题。
在本教程中,您将发现 LSTM 记忆和回忆的功能。
完成本教程后,您将了解:
- 如何定义一个小序列预测问题,只有像 LSTM 这样的 RNN 可以使用记忆来解决。
- 如何转换问题表示,使其适合 LSTM 学习。
- 如何设计 LSTM 来正确解决问题。
让我们开始吧。
在长期短期记忆网络中的记忆演示 照片由 crazlei ,保留一些权利。
本教程假设您使用带有 TensorFlow 或 Theano 后端的 SciPy,Keras 2.0 或更高版本的 Python 2 或 3 环境。
有关设置 Python 环境的帮助,请参阅帖子:
问题是一次预测一个序列的值。
给定序列中的一个值,模型必须预测序列中的下一个值。例如,给定值“0”作为输入,模型必须预测值“1”。
模型必须学习和正确预测有两种不同的序列。
皱纹是两个序列之间存在冲突的信息,并且模型必须知道每个一步预测的上下文(例如,它当前正在预测的序列),以便正确地预测每个完整序列。
这种皱纹对于防止模型记忆每个序列中的每个单步输入 - 输出值对非常重要,因为序列未知模型可能倾向于这样做。
要学习的两个序列如下:
- 3,0,1,2,3
- 4,0,1,2,4
我们可以看到序列的第一个值重复作为序列的最后一个值。这是指示器为模型提供关于它正在处理的序列的上下文。
冲突是从每个序列中的第二个项目到最后一个项目的过渡。在序列 1 中,给出“2”作为输入并且必须预测“3”,而在序列 2 中,给出“2”作为输入并且必须预测“4”。
这是多层感知机和其他非循环神经网络无法学习的问题。
这是“_ 实验 2_ ”的简化版本,用于证明 Hochreiter 和 Schmidhuber 1997 年论文长期短期记忆( PDF )中的 LSTM 长期记忆能力。
本节分为 3 部分;他们是:
- 单热编码
- 输入输出对
- 重塑数据
我们将使用单热编码来表示 LSTM 的学习问题。
也就是说,每个输入和输出值将表示为具有 5 个元素的二进制向量,因为问题的字母表是 5 个唯一值。
例如,[0,1,2,3,4]的 5 个值表示为以下 5 个二进制向量:
0: [1, 0, 0, 0, 0]
1: [0, 1, 0, 0, 0]
2: [0, 0, 1, 0, 0]
3: [0, 0, 0, 1, 0]
4: [0, 0, 0, 0, 1]
我们可以使用一个简单的函数来执行此操作,该函数将获取序列并返回序列中每个值的二进制向量列表。下面的函数 encode() 实现了这种行为。
# binary encode an input pattern, return a list of binary vectors
def encode(pattern, n_unique):
encoded = list()
for value in pattern:
row = [0.0 for x in range(n_unique)]
row[value] = 1.0
encoded.append(row)
return encoded
我们可以在第一个序列上测试它并打印结果的二进制向量列表。下面列出了完整的示例。
# binary encode an input pattern, return a list of binary vectors
def encode(pattern, n_unique):
encoded = list()
for value in pattern:
row = [0.0 for x in range(n_unique)]
row[value] = 1.0
encoded.append(row)
return encoded
seq1 = [3, 0, 1, 2, 3]
encoded = encode(seq1, 5)
for vector in encoded:
print(vector)
运行该示例打印每个二进制向量。请注意,我们使用浮点值 0.0 和 1.0,因为它们将用作模型的输入和输出。
[0.0, 0.0, 0.0, 1.0, 0.0]
[1.0, 0.0, 0.0, 0.0, 0.0]
[0.0, 1.0, 0.0, 0.0, 0.0]
[0.0, 0.0, 1.0, 0.0, 0.0]
[0.0, 0.0, 0.0, 1.0, 0.0]
下一步是将一系列编码值拆分为输入 - 输出对。
这是问题的监督学习表示,使得机器学习问题可以学习如何将输入模式(X
)映射到输出模式(y
)。
例如,第一个序列具有以下要学习的输入 - 输出对:
X, y
3, 0
0, 1
1, 2
2, 3
我们必须从单热编码的二进制向量中创建这些映射对,而不是原始数字。
例如,3-> 0 的第一输入 - 输出对将是:
X, y
[0, 0, 0, 1, 0] [1, 0, 0, 0, 0]
下面是一个名为 to_xy_pairs() 的函数,它将在给定编码二进制向量列表的情况下创建X
和y
模式的列表。
# create input/output pairs of encoded vectors, returns X, y
def to_xy_pairs(encoded):
X,y = list(),list()
for i in range(1, len(encoded)):
X.append(encoded[i-1])
y.append(encoded[i])
return X, y
我们可以将它与上面的单热编码函数放在一起,并打印第一个序列的编码输入和输出对。
# binary encode an input pattern, return a list of binary vectors
def encode(pattern, n_unique):
encoded = list()
for value in pattern:
row = [0.0 for x in range(n_unique)]
row[value] = 1.0
encoded.append(row)
return encoded
# create input/output pairs of encoded vectors, returns X, y
def to_xy_pairs(encoded):
X,y = list(),list()
for i in range(1, len(encoded)):
X.append(encoded[i-1])
y.append(encoded[i])
return X, y
seq1 = [3, 0, 1, 2, 3]
encoded = encode(seq1, 5)
X, y = to_xy_pairs(encoded)
for i in range(len(X)):
print(X[i], y[i])
运行该示例将打印序列中每个步骤的输入和输出对。
[0.0, 0.0, 0.0, 1.0, 0.0] [1.0, 0.0, 0.0, 0.0, 0.0]
[1.0, 0.0, 0.0, 0.0, 0.0] [0.0, 1.0, 0.0, 0.0, 0.0]
[0.0, 1.0, 0.0, 0.0, 0.0] [0.0, 0.0, 1.0, 0.0, 0.0]
[0.0, 0.0, 1.0, 0.0, 0.0] [0.0, 0.0, 0.0, 1.0, 0.0]
最后一步是重新整形数据,以便 LSTM 网络可以直接使用它。
Keras LSTM 期望输入模式(X)为具有[_ 样本,时间步长,特征 _]维度的三维 NumPy 数组。
在一个输入数据序列的情况下,维度将是[4,1,5],因为我们有 4 行数据,每行有 1 个时间步长,每行有 5 列。
我们可以从 X 模式列表中创建 2D NumPy 数组,然后将其重新整形为所需的 3D 格式。例如:
df = DataFrame(X)
values = df.values
array = values.reshape(4, 1, 5)
我们还必须将输出模式列表(y)转换为 2D NumPy 数组。
下面是一个名为 to_lstm_dataset() 的函数,它将序列作为输入和序列字母表的大小,并返回准备使用的X
和y
数据集与 LSTM。它在重新整形数据之前执行所需的序列转换为单热编码和输入输出对。
# convert sequence to x/y pairs ready for use with an LSTM
def to_lstm_dataset(sequence, n_unique):
# one hot encode
encoded = encode(sequence, n_unique)
# convert to in/out patterns
X,y = to_xy_pairs(encoded)
# convert to LSTM friendly format
dfX, dfy = DataFrame(X), DataFrame(y)
lstmX = dfX.values
lstmX = lstmX.reshape(lstmX.shape[0], 1, lstmX.shape[1])
lstmY = dfy.values
return lstmX, lstmY
可以使用以下每个序列调用此函数:
seq1 = [3, 0, 1, 2, 3]
seq2 = [4, 0, 1, 2, 4]
n_unique = len(set(seq1 + seq2))
seq1X, seq1Y = to_lstm_dataset(seq1, n_unique)
seq2X, seq2Y = to_lstm_dataset(seq2, n_unique)
我们现在拥有为 LSTM 准备数据的所有部分。
在本节中,我们将定义 LSTM 以学习输入序列。
本节分为 4 个部分:
- LSTM 配置
- LSTM 训练
- LSTM 评估
- LSTM 完整示例
我们希望 LSTM 进行一步预测,我们已经在数据集的格式和形状中定义了这些预测。我们还希望在每个时间步之后更新 LSTM 错误,这意味着我们需要使用批量大小的一个。
默认情况下,Keras LSTM 在批次之间不具有状态。我们可以通过将 LSTM 层上的 _ 有状态 _ 参数设置为True
并手动管理训练时期来确保 LSTM 的内部状态在每个序列之后被重置,从而使它们成为有状态。
我们必须使用batch_input_shape
参数定义批次的形状,其中 3 维[_ 批量大小,时间步长和特征 _]分别为 1,1 和 5。
网络拓扑将配置一个具有 20 个单元的隐藏 LSTM 层和一个具有 5 个输出的普通密集层,用于输出模式中的每个 5 列。由于二进制输出,将在输出层上使用 sigmoid(逻辑)激活函数,并且将在 LSTM 层上使用默认的 tanh(双曲正切)激活函数。
由于二进制输出,因此在拟合网络时将优化对数(交叉熵)损失函数,并且将使用有效的 ADAM 优化算法与所有默认参数。
下面列出了为此问题定义 LSTM 网络的 Keras 代码。
model = Sequential()
model.add(LSTM(20, batch_input_shape=(1, 1, 5), stateful=True))
model.add(Dense(5, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam')
我们必须一次手动拟合模型一个时期。
在一个时期内,我们可以在每个序列上拟合模型,确保在每个序列之后重置状态。
鉴于问题的简单性,该模型不需要长时间训练;在这种情况下,只需要 250 个迭代。
下面是一个示例,说明模型如何适应所有时期的每个序列。
# train LSTM
for i in range(250):
model.fit(seq1X, seq1Y, epochs=1, batch_size=1, verbose=1, shuffle=False)
model.reset_states()
model.fit(seq2X, seq2Y, epochs=1, batch_size=1, verbose=0, shuffle=False)
model.reset_states()
我希望在安装网络时能看到关于损失函数的一些反馈,因此从其中一个序列开启详细输出,而不是另一个序列。
接下来,我们可以通过预测学习序列的每个步骤来评估拟合模型。
我们可以通过预测每个序列的输出来做到这一点。
predict_classes() 函数可以用于直接预测类的 LSTM 模型。它通过在输出二进制向量上执行 argmax() 并返回具有最大输出的预测列的索引来完成此操作。输出索引完美地映射到序列中使用的整数(通过上面的仔细设计)。下面列出了做出预测的示例:
result = model.predict_classes(seq1X, batch_size=1, verbose=0)
我们可以做出预测,然后在输入模式的上下文和序列的每个步骤的预期输出模式中打印结果。
我们现在可以将整个教程结合在一起。
完整的代码清单如下。
首先,准备数据,然后拟合模型并打印两个序列的预测。
from pandas import DataFrame
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
# binary encode an input pattern, return a list of binary vectors
def encode(pattern, n_unique):
encoded = list()
for value in pattern:
row = [0.0 for x in range(n_unique)]
row[value] = 1.0
encoded.append(row)
return encoded
# create input/output pairs of encoded vectors, returns X, y
def to_xy_pairs(encoded):
X,y = list(),list()
for i in range(1, len(encoded)):
X.append(encoded[i-1])
y.append(encoded[i])
return X, y
# convert sequence to x/y pairs ready for use with an LSTM
def to_lstm_dataset(sequence, n_unique):
# one hot encode
encoded = encode(sequence, n_unique)
# convert to in/out patterns
X,y = to_xy_pairs(encoded)
# convert to LSTM friendly format
dfX, dfy = DataFrame(X), DataFrame(y)
lstmX = dfX.values
lstmX = lstmX.reshape(lstmX.shape[0], 1, lstmX.shape[1])
lstmY = dfy.values
return lstmX, lstmY
# define sequences
seq1 = [3, 0, 1, 2, 3]
seq2 = [4, 0, 1, 2, 4]
# convert sequences into required data format
n_unique = len(set(seq1 + seq2))
seq1X, seq1Y = to_lstm_dataset(seq1, n_unique)
seq2X, seq2Y = to_lstm_dataset(seq2, n_unique)
# define LSTM configuration
n_neurons = 20
n_batch = 1
n_epoch = 250
n_features = n_unique
# create LSTM
model = Sequential()
model.add(LSTM(n_neurons, batch_input_shape=(n_batch, 1, n_features), stateful=True))
model.add(Dense(n_unique, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam')
# train LSTM
for i in range(n_epoch):
model.fit(seq1X, seq1Y, epochs=1, batch_size=n_batch, verbose=1, shuffle=False)
model.reset_states()
model.fit(seq2X, seq2Y, epochs=1, batch_size=n_batch, verbose=0, shuffle=False)
model.reset_states()
# test LSTM on sequence 1
print('Sequence 1')
result = model.predict_classes(seq1X, batch_size=n_batch, verbose=0)
model.reset_states()
for i in range(len(result)):
print('X=%.1f y=%.1f, yhat=%.1f' % (seq1[i], seq1[i+1], result[i]))
# test LSTM on sequence 2
print('Sequence 2')
result = model.predict_classes(seq2X, batch_size=n_batch, verbose=0)
model.reset_states()
for i in range(len(result)):
print('X=%.1f y=%.1f, yhat=%.1f' % (seq2[i], seq2[i+1], result[i]))
运行该示例提供关于模型在每个时期的第一序列上的损失的反馈。
在运行结束时,每个序列都在预测的上下文中打印。
...
4/4 [==============================] - 0s - loss: 0.0930
Epoch 1/1
4/4 [==============================] - 0s - loss: 0.0927
Epoch 1/1
4/4 [==============================] - 0s - loss: 0.0925
Sequence 1
X=3.0 y=0.0, yhat=0.0
X=0.0 y=1.0, yhat=1.0
X=1.0 y=2.0, yhat=2.0
X=2.0 y=3.0, yhat=3.0
Sequence 2
X=4.0 y=0.0, yhat=0.0
X=0.0 y=1.0, yhat=1.0
X=1.0 y=2.0, yhat=2.0
X=2.0 y=4.0, yhat=4.0
结果显示了两件重要的事情:
- LSTM 一次一步地正确学习每个序列。
- LSTM 使用每个序列的上下文来正确地解析冲突的输入对。
本质上,LSTM 能够记住 3 个时间步前序列开始处的输入模式,以正确预测序列中的最后一个值。
这种记忆和 LSTM 能够及时关联观测的能力是使 LSTM 如此强大以及它们如此广泛使用的关键能力。
虽然这个例子很简单,但 LSTM 能够在 100 秒甚至 1000 秒的时间步长中展示出同样的能力。
本节列出了本教程中示例扩展的思路。
- 调整。经过一些试验和错误后,选择了 LSTM(时期,单位等)的配置。更简单的配置可能会在此问题上获得相同的结果。需要搜索一些参数。
- 任意字母。 5 个整数的字母表是任意选择的。这可以更改为其他符号和更大的字母。
- 长序列。本例中使用的序列非常短。 LSTM 能够在更长的 100s 和 1000s 时间步长序列上展示相同的能力。
- 随机序列。本教程中使用的序列呈线性增长。可以创建新的随机值序列,允许 LSTM 设计一个通用解决方案,而不是专门用于本教程中使用的两个序列的解决方案。
- 批量学习。每个时间步后都对 LSTM 进行了更新。探索使用批量更新,看看这是否会改善学习。
- Shuffle Epoch 。序列在训练期间的每个时期以相同的顺序显示,并且在评估期间再次显示。随机化序列的顺序,使得序列 1 和 2 适合一个时期,这可以改善模型对具有相同字母表的新看不见的序列的概括。
你有没有探索过这些扩展? 在下面的评论中分享您的结果。我很想看看你想出了什么。
我强烈建议阅读 Hochreiter 和 Schmidhuber 的 1997 年原始 LSTM 论文;这很棒。
在本教程中,您发现了 LSTM 能够记住多个时间步的关键功能。
具体来说,你学到了:
- 如何定义一个小序列预测问题,只有像 LSTM 这样的 RNN 可以使用记忆来解决。
- 如何转换问题表示,使其适合 LSTM 学习。
- 如何设计 LSTM 来正确解决问题。
你有任何问题吗? 在下面的评论中发表您的问题,我会尽力回答。