正文

一个强大而流行的循环神经网络(RNN)的变种是长短期模型网络(LSTM)。

它使用广泛,因为它的架构克服了困扰着所有周期性的神经网络梯度消失梯度爆炸的问题,允许创建非常大的、非常深的网络。

与其他周期性的神经网络一样,LSTM网络保持状态,在keras框架中实现这一点的细节可能会令人困惑。

在这篇文章中,您将会确切地了解到在LSTM网络中,如何在LSTM深度学习库中维护状态。

 本文目标:

  1. 怎么在keras上实现一个普通的lstm循环神经网络
  2. 在lstm中怎样小心的利用好时间状态特征
  3. 怎样在lstm上实现状态的预测

本文在一个很简单的例子上说明lstm的使用和lstm的特点,通过对这个简化例子的理解,可以帮助我们对一般的序列预测问题和序列预测问题有更高的理解和使用。
用到的库:Keras 2.0.2,TensorFlow 1.0.1Theano 0.9.0.

问题描述:学习字母

在本教程中,我们将开发和对比许多不同的LSTM循环神经网络模型。

这些比较的背景是学习字母表的一个简单的序列预测问题。也就是说,根据字母表的字母,可以预测字母表的下一个字母。

这是一个简单的序列预测问题,一旦被理解,就可以被推广到其他的序列预测问题,如时间序列预测和序列分类。

让我们用一些python代码来准备这个问题,我们可以从示例中重用这些代码。

首先,让我们导入本教程中计划使用的所有类和函数。

  1. import numpy
  2. from keras.models import Sequential
  3. from keras.layers import Dense
  4. from keras.layers import LSTM
  5. from keras.utils import np_utils

接下来,我们可以对随机数生成器选定随机数种子,以确保每次执行代码时结果都是相同的。

  1. # fix random seed for reproducibility
  2. numpy.random.seed(7)

我们现在可以定义我们的数据集,字母表。为了便于阅读,我们用大写字母来定义字母表。

神经网络是对数字建模,因此我们需要将字母表中的字母映射到整数值(把字母映射为数字)。我们可以很容易地通过创建字母索引的字典(map)到字符。我们还可以创建一个反向查找,以便将预测转换回字符,以便稍后使用。

  1. # define the raw dataset
  2. alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  3. # create mapping of characters to integers (0-25) and the reverse
  4. char_to_int = dict((c, i) for i, c in enumerate(alphabet))
  5. int_to_char = dict((i, c) for i, c in enumerate(alphabet))

现在我们需要创建我们的输入和输出键值对来训练我们的神经网络。我们可以通过定义输入序列长度,然后从输入字母序列中读取序列来实现这一点。

例如,我们使用的输入长度是1。从原始输入数据的开始,我们可以读出第一个字母A和下一个字母“B”。我们沿着一个字符移动,直到我们到达一个“Z”的预测。

我们先创造这样一个数据集,用一个字母,来预测下一个字母是什么。

  1. # prepare the dataset of input to output pairs encoded as integers
  2. seq_length = 1
  3. dataX = []
  4. dataY = []
  5. for i in range(0, len(alphabet) - seq_length, 1):
  6. seq_in = alphabet[i:i + seq_length]
  7. seq_out = alphabet[i + seq_length]
  8. dataX.append([char_to_int[char] for char in seq_in])
  9. dataY.append(char_to_int[seq_out])
  10. print seq_in, '->', seq_out

我们运行上面的代码,来观察现在我们的input和output数据集是这样一种情况

  1. A -> B
  2. B -> C
  3. C -> D
  4. D -> E
  5. E -> F
  6. F -> G
  7. G -> H
  8. H -> I
  9. I -> J
  10. J -> K
  11. K -> L
  12. L -> M
  13. M -> N
  14. N -> O
  15. O -> P
  16. P -> Q
  17. Q -> R
  18. R -> S
  19. S -> T
  20. T -> U
  21. U -> V
  22. V -> W
  23. W -> X
  24. X -> Y
  25. Y -> Z

input是一个一个字母的序列,output是一个一个的序列。
ok,就在这样的数据集上来应用我们的lstm。看看会有什么结果?

这时候dataX是一个一个用字母组成的序列,但是还要转换一下格式,才能用到keras上。我们需要将NumPy数组重新构造为LSTM网络所期望的格式,即[samples示例, time steps时间步数, features特征]。

  1. # reshape X to be [samples, time steps, features]
  2. X = numpy.reshape(dataX, (len(dataX), seq_length, 1))

然后我们需要把我们的整数值归一化到0~1的区间上,这是LSTM网络使用的s形激活函数(sigmoid)的范围。

  1. # normalize
  2. X = X / float(len(alphabet))

最后,我们可以把这个问题看作是一个序列分类任务,其中26个字母代表一个不同的类。因此,我们用keras的内置的 to_categorical()函数把输出output(y)进行 one-hot编码(one-hot指n维单位向量a=(0,…,0,1,0,…,0))作为输出层的结果。

  1. # one hot encode the output variable
  2. y = np_utils.to_categorical(dataY)

现在我们已经准备好去训练不同的LSTM模型了。

 单字符——单字符的映射的简单LSTM

让我们从设计一个简单的LSTM开始,学习如何根据一个字符的上下文来预测字母表中的下一个字符。

我们将定义这个问题为:一些单字母的随机集合作为输入,另一些单字母作为输出,由输入输出对组成。正如我们所看到的,这对于LSTM来说是一个很难用来学习的结构。

让我们定义一个LSTM网络,它有32个单元(the LSTM units are the “memory units” or you can just call them the neurons.),一个输出层,其中有一个softmax的激活函数来进行预测。由于这是一个多类分类问题,所以我们可以使用在Keras中使用对数损失函数(称为“分类交叉熵”(categorical_crossentropy)),并使用ADAM优化函数对网络进行优化。

该模型以500批次(epochs),每批次数据输入大小(batch)为1的形式训练

我们通过lstm在这个问题上的预测,会发现这对lstm循环网络来说是很难解决的问题。

keras上LSTM用于上述问题的代码如下:

  1. # create and fit the model
  2. model = Sequential()
  3. model.add(LSTM(32, input_shape=(X.shape[1], X.shape[2])))
  4. model.add(Dense(y.shape[1], activation='softmax'))
  5. model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
  6. model.fit(X, y, nb_epoch=500, batch_size=1, verbose=2)

通过keras例子理解LSTM 循环神经网络(RNN)

在我们训练模型之后,我们可以对整个训练集的性能进行评估和总结。

  1. # summarize performance of the model
  2. scores = model.evaluate(X, y, verbose=0)
  3. print("Model Accuracy: %.2f%%" % (scores[1]*100))

然后,我们可以通过网络重新运行训练数据,并生成预测,将输入和输出对转换回原来的字符格式,以获得关于网络如何了解问题的视觉效果。

  1. # demonstrate some model predictions
  2. for pattern in dataX:
  3. x = numpy.reshape(pattern, (1, len(pattern), 1))
  4. x = x / float(len(alphabet))
  5. prediction = model.predict(x, verbose=0)
  6. index = numpy.argmax(prediction)
  7. result = int_to_char[index]
  8. seq_in = [int_to_char[value] for value in pattern]
  9. print seq_in, "->", result

通过keras例子理解LSTM 循环神经网络(RNN)

 

我们可以看到,这个问题对于网络来说确实是很困难的。
原因是可怜的lstm单元根本没有可以利用的上下文章信息。
每个输入输出模式都以随机的顺序显示在网络中,并且网络的状态在每个模式之后被重置(每个批处理的每个批次包含一个模式)。

 

这是对LSTM网络架构的滥用,因为我们把它当作了一个标准的多层感知器。

接下来,让我们尝试一个不同的问题框架,以便为网络提供更多的序列来学习。

 

三字符特征——单字符的映射的简单LSTM

在多层感知器中添加更多上下文最流行的方法是特征窗口方法(Feature Window method)。

即序列中的前面步骤的输出被作为附加的输入特性提供给网络。我们可以用相同的技巧,为LSTM网络提供更多的上下文。

在这里,我们将序列长度从1增加到3,例如:
我们把输入从一个字符升到三个字符。

  1. # prepare the dataset of input to output pairs encoded as integers
  2. seq_length = 3

就像这样:

  1. ABC -> D
  2. BCD -> E
  3. CDE -> F

然后将序列中的每个元素作为网络的一个新输入特性提供给它。这需要修改输入序列在数据准备步骤中的reshape:

  1. # reshape X to be [samples, time steps, features]
  2. X = numpy.reshape(dataX, (len(dataX), 1, seq_length))

还需要对示例模式的reshape进行修改,以展示模型的预测结果。

x = numpy.reshape(pattern, (1, 1, len(pattern)))

全部的代码如下:

  1. # Naive LSTM to learn three-char window to one-char mapping
  2. import numpy
  3. from keras.models import Sequential
  4. from keras.layers import Dense
  5. from keras.layers import LSTM
  6. from keras.utils import np_utils
  7. # fix random seed for reproducibility
  8. numpy.random.seed(7)
  9. # define the raw dataset
  10. alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  11. # create mapping of characters to integers (0-25) and the reverse
  12. char_to_int = dict((c, i) for i, c in enumerate(alphabet))
  13. int_to_char = dict((i, c) for i, c in enumerate(alphabet))
  14. # prepare the dataset of input to output pairs encoded as integers
  15. seq_length = 3
  16. dataX = []
  17. dataY = []
  18. for i in range(0, len(alphabet) - seq_length, 1):
  19. seq_in = alphabet[i:i + seq_length]
  20. seq_out = alphabet[i + seq_length]
  21. dataX.append([char_to_int[char] for char in seq_in])
  22. dataY.append(char_to_int[seq_out])
  23. print seq_in, '->', seq_out
  24. # reshape X to be [samples, time steps, features]
  25. X = numpy.reshape(dataX, (len(dataX), 1, seq_length))
  26. # normalize
  27. X = X / float(len(alphabet))
  28. # one hot encode the output variable
  29. y = np_utils.to_categorical(dataY)
  30. # create and fit the model
  31. model = Sequential()
  32. model.add(LSTM(32, input_shape=(X.shape[1], X.shape[2])))
  33. model.add(Dense(y.shape[1], activation='softmax'))
  34. model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
  35. model.fit(X, y, epochs=500, batch_size=1, verbose=2)
  36. # summarize performance of the model
  37. scores = model.evaluate(X, y, verbose=0)
  38. print("Model Accuracy: %.2f%%" % (scores[1]*100))
  39. # demonstrate some model predictions
  40. for pattern in dataX:
  41. x = numpy.reshape(pattern, (1, 1, len(pattern)))
  42. x = x / float(len(alphabet))
  43. prediction = model.predict(x, verbose=0)
  44. index = numpy.argmax(prediction)
  45. result = int_to_char[index]
  46. seq_in = [int_to_char[value] for value in pattern]
  47. print seq_in, "->", result

运行结果如下:

  1. Model Accuracy: 86.96%
  2. ['A', 'B', 'C'] -> D
  3. ['B', 'C', 'D'] -> E
  4. ['C', 'D', 'E'] -> F
  5. ['D', 'E', 'F'] -> G
  6. ['E', 'F', 'G'] -> H
  7. ['F', 'G', 'H'] -> I
  8. ['G', 'H', 'I'] -> J
  9. ['H', 'I', 'J'] -> K
  10. ['I', 'J', 'K'] -> L
  11. ['J', 'K', 'L'] -> M
  12. ['K', 'L', 'M'] -> N
  13. ['L', 'M', 'N'] -> O
  14. ['M', 'N', 'O'] -> P
  15. ['N', 'O', 'P'] -> Q
  16. ['O', 'P', 'Q'] -> R
  17. ['P', 'Q', 'R'] -> S
  18. ['Q', 'R', 'S'] -> T
  19. ['R', 'S', 'T'] -> U
  20. ['S', 'T', 'U'] -> V
  21. ['T', 'U', 'V'] -> Y
  22. ['U', 'V', 'W'] -> Z
  23. ['V', 'W', 'X'] -> Z
  24. ['W', 'X', 'Y'] -> Z

我们发现有了一点点的提升,但是这一点点的提升未必是真的,梯度下降算法本来就是具有随机性的。

也就是说我们再一次的错误使用了lstm循环神经网络。
我们确实给了上下文,但是并不是合适的方式,
实际上,字母序列A-B-C才是一个特征的timesteps,而不是单独ABC一个特征的timestep
我们已经给网络提供了更多的上下文,但并没有像预期的那样有更多的顺序。

在下一节中,我们将以timesteps的形式为网络提供更多的上下文。

keras实践循环的正确打开方式!

在keras中,利用lstm的关键是以时间序列(time steps)的方法来提供上下文,而不是像其他网络结构(CNN)一样,通过windowed features的方式。

这次我们还是采用这样的训练方式

seq_length = 3

输入输出对(input-output pairs)

  1. ABC -> D
  2. BCD -> E
  3. CDE -> F
  4. DEF -> G

我们这次唯一改变的地方是下面这里:

  1. # reshape X to be [samples, time steps, features]
  2. X = numpy.reshape(dataX, (len(dataX), seq_length, 1))

timesteps这个参数,我们设置了3,而不是前面的1。

不同之处是,对输入数据的reshape是将输入序列作为一个特性的time step序列,而不是多个特性的单一time step。
也就是说我们把ABC 看成独立的一个特征组成的多个时间序列,而不是把ABC看成一个多个特征组成一个时间序列。
 

这就是keras中LSTM循环神经网络的正确打开的方式。
我的理解是,这样在训练 ABC——D的时候,BCD,CDE,都可以发挥作用。而最开始那种使用方法,只是利用了ABC——D这样一个训练样本。

完整代码如下:

  1. # Naive LSTM to learn three-char time steps to one-char mapping
  2. import numpy
  3. from keras.models import Sequential
  4. from keras.layers import Dense
  5. from keras.layers import LSTM
  6. from keras.utils import np_utils
  7. # fix random seed for reproducibility
  8. numpy.random.seed(7)
  9. # define the raw dataset
  10. alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  11. # create mapping of characters to integers (0-25) and the reverse
  12. char_to_int = dict((c, i) for i, c in enumerate(alphabet))
  13. int_to_char = dict((i, c) for i, c in enumerate(alphabet))
  14. # prepare the dataset of input to output pairs encoded as integers
  15. seq_length = 3
  16. dataX = []
  17. dataY = []
  18. for i in range(0, len(alphabet) - seq_length, 1):
  19. seq_in = alphabet[i:i + seq_length]
  20. seq_out = alphabet[i + seq_length]
  21. dataX.append([char_to_int[char] for char in seq_in])
  22. dataY.append(char_to_int[seq_out])
  23. print seq_in, '->', seq_out
  24. # reshape X to be [samples, time steps, features]
  25. X = numpy.reshape(dataX, (len(dataX), seq_length, 1))
  26. # normalize
  27. X = X / float(len(alphabet))
  28. # one hot encode the output variable
  29. y = np_utils.to_categorical(dataY)
  30. # create and fit the model
  31. model = Sequential()
  32. model.add(LSTM(32, input_shape=(X.shape[1], X.shape[2])))
  33. model.add(Dense(y.shape[1], activation='softmax'))
  34. model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
  35. model.fit(X, y, nb_epoch=500, batch_size=1, verbose=2)
  36. # summarize performance of the model
  37. scores = model.evaluate(X, y, verbose=0)
  38. print("Model Accuracy: %.2f%%" % (scores[1]*100))
  39. # demonstrate some model predictions
  40. for pattern in dataX:
  41. x = numpy.reshape(pattern, (1, len(pattern), 1))
  42. x = x / float(len(alphabet))
  43. prediction = model.predict(x, verbose=0)
  44. index = numpy.argmax(prediction)
  45. result = int_to_char[index]
  46. seq_in = [int_to_char[value] for value in pattern]
  47. print seq_in, "->", result

最终的训练结果是

  1. Model Accuracy: 100.00%
  2. ['A', 'B', 'C'] -> D
  3. ['B', 'C', 'D'] -> E
  4. ['C', 'D', 'E'] -> F
  5. ['D', 'E', 'F'] -> G
  6. ['E', 'F', 'G'] -> H
  7. ['F', 'G', 'H'] -> I
  8. ['G', 'H', 'I'] -> J
  9. ['H', 'I', 'J'] -> K
  10. ['I', 'J', 'K'] -> L
  11. ['J', 'K', 'L'] -> M
  12. ['K', 'L', 'M'] -> N
  13. ['L', 'M', 'N'] -> O
  14. ['M', 'N', 'O'] -> P
  15. ['N', 'O', 'P'] -> Q
  16. ['O', 'P', 'Q'] -> R
  17. ['P', 'Q', 'R'] -> S
  18. ['Q', 'R', 'S'] -> T
  19. ['R', 'S', 'T'] -> U
  20. ['S', 'T', 'U'] -> V
  21. ['T', 'U', 'V'] -> W
  22. ['U', 'V', 'W'] -> X
  23. ['V', 'W', 'X'] -> Y
  24. ['W', 'X', 'Y'] -> Z

它已经学会了用字母表中的三个字母来预测下一个字母的顺序。它可以显示字母表中的任意三个字母的随机序列,并预测下一个字母。

我们还没有展示出循环神经网络的强大之处,因为上面这个问题我们用多层感知器,足够多的神经元,足够多的迭代次数也可以很好的解决。(三层神经网络拟合任意可以表示的函数)

 

LSTM网络是有状态的。它们应该能够学习整个字母表序列,但是在默认情况下,keras在每次训练之后重新设置网络状态。

 

那么接下来就是展示循环神经网络的独到之处!!

 

一个批处理中的LSTM状态

keras实现的LSTM在每一个batch以后,都重置了LSTM的状态。

这表明,如果我们的批处理大小足够容纳所有输入模式,如果所有输入模式都按顺序排序,LSTM就可以使用序列中的序列上下文来更好地学习序列。

通过修改第一个示例来学习一对一映射,并将批处理大小从1增加到训练数据集的大小,我们可以很容易地演示这一点。

此外,在每个epoch前,keras都重置了训练数据集。为了确保训练数据模式保持顺序,我们可以禁用这种洗牌。

model.fit(X, y, epochs=5000, batch_size=len(dataX), verbose=2, shuffle=False)

该网络将使用 within-batch批序列来学习字符的映射,但在进行预测时,这个上下文将无法用于网络。我们可以对网络进行评估,以确定网络在随机序列和顺序序列的预测能力。

完整代码如下:

  1. Naive LSTM to learn one-char to one-char mapping with all data in each batch
  2. import numpy
  3. from keras.models import Sequential
  4. from keras.layers import Dense
  5. from keras.layers import LSTM
  6. from keras.utils import np_utils
  7. from keras.preprocessing.sequence import pad_sequences
  8. # fix random seed for reproducibility
  9. numpy.random.seed(7)
  10. # define the raw dataset
  11. alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  12. # create mapping of characters to integers (0-25) and the reverse
  13. char_to_int = dict((c, i) for i, c in enumerate(alphabet))
  14. int_to_char = dict((i, c) for i, c in enumerate(alphabet))
  15. # prepare the dataset of input to output pairs encoded as integers
  16. seq_length = 1
  17. dataX = []
  18. dataY = []
  19. for i in range(0, len(alphabet) - seq_length, 1):
  20. seq_in = alphabet[i:i + seq_length]
  21. seq_out = alphabet[i + seq_length]
  22. dataX.append([char_to_int[char] for char in seq_in])
  23. dataY.append(char_to_int[seq_out])
  24. print seq_in, '->', seq_out
  25. # convert list of lists to array and pad sequences if needed
  26. X = pad_sequences(dataX, maxlen=seq_length, dtype='float32')
  27. # reshape X to be [samples, time steps, features]
  28. X = numpy.reshape(dataX, (X.shape[0], seq_length, 1))
  29. # normalize
  30. X = X / float(len(alphabet))
  31. # one hot encode the output variable
  32. y = np_utils.to_categorical(dataY)
  33. # create and fit the model
  34. model = Sequential()
  35. model.add(LSTM(16, input_shape=(X.shape[1], X.shape[2])))
  36. model.add(Dense(y.shape[1], activation='softmax'))
  37. model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
  38. model.fit(X, y, epochs=5000, batch_size=len(dataX), verbose=2, shuffle=False)
  39. # summarize performance of the model
  40. scores = model.evaluate(X, y, verbose=0)
  41. print("Model Accuracy: %.2f%%" % (scores[1]*100))
  42. # demonstrate some model predictions
  43. for pattern in dataX:
  44. x = numpy.reshape(pattern, (1, len(pattern), 1))
  45. x = x / float(len(alphabet))
  46. prediction = model.predict(x, verbose=0)
  47. index = numpy.argmax(prediction)
  48. result = int_to_char[index]
  49. seq_in = [int_to_char[value] for value in pattern]
  50. print seq_in, "->", result
  51. # demonstrate predicting random patterns
  52. print "Test a Random Pattern:"
  53. for i in range(0,20):
  54. pattern_index = numpy.random.randint(len(dataX))
  55. pattern = dataX[pattern_index]
  56. x = numpy.reshape(pattern, (1, len(pattern), 1))
  57. x = x / float(len(alphabet))
  58. prediction = model.predict(x, verbose=0)
  59. index = numpy.argmax(prediction)
  60. result = int_to_char[index]
  61. seq_in = [int_to_char[value] for value in pattern]
  62. print seq_in, "->", result

结果:

  1. Model Accuracy: 100.00%
  2. ['A'] -> B
  3. ['B'] -> C
  4. ['C'] -> D
  5. ['D'] -> E
  6. ['E'] -> F
  7. ['F'] -> G
  8. ['G'] -> H
  9. ['H'] -> I
  10. ['I'] -> J
  11. ['J'] -> K
  12. ['K'] -> L
  13. ['L'] -> M
  14. ['M'] -> N
  15. ['N'] -> O
  16. ['O'] -> P
  17. ['P'] -> Q
  18. ['Q'] -> R
  19. ['R'] -> S
  20. ['S'] -> T
  21. ['T'] -> U
  22. ['U'] -> V
  23. ['V'] -> W
  24. ['W'] -> X
  25. ['X'] -> Y
  26. ['Y'] -> Z
  27. Test a Random Pattern:
  28. ['T'] -> U
  29. ['V'] -> W
  30. ['M'] -> N
  31. ['Q'] -> R
  32. ['D'] -> E
  33. ['V'] -> W
  34. ['T'] -> U
  35. ['U'] -> V
  36. ['J'] -> K
  37. ['F'] -> G
  38. ['N'] -> O
  39. ['B'] -> C
  40. ['M'] -> N
  41. ['F'] -> G
  42. ['F'] -> G
  43. ['P'] -> Q
  44. ['A'] -> B
  45. ['K'] -> L
  46. ['W'] -> X
  47. ['E'] -> F

正如我们所期望的那样,网络能够使用 within-sequence的上下文来学习字母表,在训练数据上达到100%的准确率。

重要的是,该网络可以对随机选择的字符的下一个字母进行准确的预测。非常令人印象深刻。

单字符——单字符的映射的有状态LSTM

我们已经看到,我们可以将原始数据拆分为固定大小的序列,并且这种表示可以由LSTM来学习,且只需要学习3个字符到1个字符的随机映射。

我们也看到,我们可以对批量的大小进行限制,为网络提供更多的序列,但只有在训练期间才行。

理想情况下,我们希望将网络公开给整个序列,并让它学习相互依赖关系,而不是在问题的框架中明确地定义这些依赖关系。

我们可以在keras中做到这一点,通过使LSTM层拥有状态,并在epoch结束时手动重新设置网络的状态,这时也结束了训练整个序列的过程。

这才是LSTM网络的真正用途。我们发现,如果允许网络本身学习字符之间的依赖关系,我们只需要一个更小的网络(一半的单位数量)和更少的训练期(几乎是一半)。

首先我们需要将LSTM层定义为有状态的。这样做的话,我们必须显式地指定批大小作为输入形状的一个维度。这也意味着当我们评估网络或用网络进行预测时,我们也必须指定并遵守相同的批大小。现在这不是问题,因为我们使用的是批大小的1。这可能会在预测的时候带来困难,因为当批大小不是1时,预测需要按批进行和按顺序进行。

  1. batch_size = 1
  2. model.add(LSTM(16, batch_input_shape=(batch_size, X.shape[1], X.shape[2]), stateful=True))

训练有状态的LSTM的一个重要区别是,我们每次都手动地训练它,并且在每个时代之后重新设置状态。我们可以在for循环中这样做。同样,我们不会对输入进行洗牌,保留输入训练数据创建的顺序。

  1. for i in range(300):
  2. model.fit(X, y, epochs=1, batch_size=batch_size, verbose=2, shuffle=False)
  3. model.reset_states()

如前所述,在评估整个培训数据集的网络性能时,我们指定批处理大小。

  1. # summarize performance of the model
  2. scores = model.evaluate(X, y, batch_size=batch_size, verbose=0)
  3. model.reset_states()
  4. print("Model Accuracy: %.2f%%" % (scores[1]*100))

最后,我们可以证明网络确实学会了整个字母表。我们可以用第一个字母A“A”来做输入,获得一个预测,把预测作为输入反馈给它,然后把这个过程一直重复到“Z”。

  1. # demonstrate some model predictions
  2. seed = [char_to_int[alphabet[0]]]
  3. for i in range(0, len(alphabet)-1):
  4. x = numpy.reshape(seed, (1, len(seed), 1))
  5. x = x / float(len(alphabet))
  6. prediction = model.predict(x, verbose=0)
  7. index = numpy.argmax(prediction)
  8. print int_to_char[seed[0]], "->", int_to_char[index]
  9. seed = [index]
  10. model.reset_states()

我们也可以看看这个网络是否可以从任意的字母开始预测

  1. # demonstrate a random starting point
  2. letter = "K"
  3. seed = [char_to_int[letter]]
  4. print "New start: ", letter
  5. for i in range(0, 5):
  6. x = numpy.reshape(seed, (1, len(seed), 1))
  7. x = x / float(len(alphabet))
  8. prediction = model.predict(x, verbose=0)
  9. index = numpy.argmax(prediction)
  10. print int_to_char[seed[0]], "->", int_to_char[index]
  11. seed = [index]
  12. model.reset_states()

完整代码如下:

  1. # Stateful LSTM to learn one-char to one-char mapping
  2. import numpy
  3. from keras.models import Sequential
  4. from keras.layers import Dense
  5. from keras.layers import LSTM
  6. from keras.utils import np_utils
  7. # fix random seed for reproducibility
  8. numpy.random.seed(7)
  9. # define the raw dataset
  10. alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  11. # create mapping of characters to integers (0-25) and the reverse
  12. char_to_int = dict((c, i) for i, c in enumerate(alphabet))
  13. int_to_char = dict((i, c) for i, c in enumerate(alphabet))
  14. # prepare the dataset of input to output pairs encoded as integers
  15. seq_length = 1
  16. dataX = []
  17. dataY = []
  18. for i in range(0, len(alphabet) - seq_length, 1):
  19. seq_in = alphabet[i:i + seq_length]
  20. seq_out = alphabet[i + seq_length]
  21. dataX.append([char_to_int[char] for char in seq_in])
  22. dataY.append(char_to_int[seq_out])
  23. print seq_in, '->', seq_out
  24. # reshape X to be [samples, time steps, features]
  25. X = numpy.reshape(dataX, (len(dataX), seq_length, 1))
  26. # normalize
  27. X = X / float(len(alphabet))
  28. # one hot encode the output variable
  29. y = np_utils.to_categorical(dataY)
  30. # create and fit the model
  31. batch_size = 1
  32. model = Sequential()
  33. model.add(LSTM(16, batch_input_shape=(batch_size, X.shape[1], X.shape[2]), stateful=True))
  34. model.add(Dense(y.shape[1], activation='softmax'))
  35. model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
  36. for i in range(300):
  37. model.fit(X, y, epochs=1, batch_size=batch_size, verbose=2, shuffle=False)
  38. model.reset_states()
  39. # summarize performance of the model
  40. scores = model.evaluate(X, y, batch_size=batch_size, verbose=0)
  41. model.reset_states()
  42. print("Model Accuracy: %.2f%%" % (scores[1]*100))
  43. # demonstrate some model predictions
  44. seed = [char_to_int[alphabet[0]]]
  45. for i in range(0, len(alphabet)-1):
  46. x = numpy.reshape(seed, (1, len(seed), 1))
  47. x = x / float(len(alphabet))
  48. prediction = model.predict(x, verbose=0)
  49. index = numpy.argmax(prediction)
  50. print int_to_char[seed[0]], "->", int_to_char[index]
  51. seed = [index]
  52. model.reset_states()
  53. # demonstrate a random starting point
  54. letter = "K"
  55. seed = [char_to_int[letter]]
  56. print "New start: ", letter
  57. for i in range(0, 5):
  58. x = numpy.reshape(seed, (1, len(seed), 1))
  59. x = x / float(len(alphabet))
  60. prediction = model.predict(x, verbose=0)
  61. index = numpy.argmax(prediction)
  62. print int_to_char[seed[0]], "->", int_to_char[index]
  63. seed = [index]
  64. model.reset_states()

output

  1. Model Accuracy: 100.00%
  2. A -> B
  3. B -> C
  4. C -> D
  5. D -> E
  6. E -> F
  7. F -> G
  8. G -> H
  9. H -> I
  10. I -> J
  11. J -> K
  12. K -> L
  13. L -> M
  14. M -> N
  15. N -> O
  16. O -> P
  17. P -> Q
  18. Q -> R
  19. R -> S
  20. S -> T
  21. T -> U
  22. U -> V
  23. V -> W
  24. W -> X
  25. X -> Y
  26. Y -> Z
  27. New start: K
  28. K -> B
  29. B -> C
  30. C -> D
  31. D -> E
  32. E -> F

我们可以看到,网络已经完美地记住了整个字母表。它使用了样本的上下文,并学习了预测序列中下一个字符所需要的依赖关系。

我们还可以看到,如果我们用第一个字母输入网络,它就能正确地对字母表的其他部分进行正确的理解。

我们还可以看到,它只是从一个冷启动开始,就学会了完整的字母表顺序。当要求预测“K”的下一个字母时,它会预测“B”,然后返回到整个字母表中。

为了真正地预测“K”,网络的状态需要被反复地从“A”到“J”的字母“加热”。这告诉我们,我们也可以达到“无状态”LSTM的效果,如果我们通过准备形如下面的训练数据:

  1. ---a -> b
  2. --ab -> c
  3. -abc -> d
  4. abcd -> e

输入序列固定在25(a-y,以预测z)的位置,并且模式以 zero-padding为前缀。

最后,这提出了另一个问题,即是否可以使用可变长度的输入序列来训练LSTM网络,以预测下一个字符。

可变长度输入——单字符输出的LSTM

在上一节中,我们发现keras的“有状态的”LSTM实际上只是重新播放第一个n序列的一个快捷方式,并没有真正学习一个通用的字母表模型。

在这一节中,我们将探索一个“无状态”LSTM的变体,它学习了字母表中的随机子序列,并可以根据任意字母或字母序列去预测字母表中的下一个字母。

首先,我们改变问题的框架。为了简化,我们定义一个最大的输入序列长度(maximum input sequence length),并将其设置为5这样的小值来加速训练。这就定义了(用于训练的字母表的)子序列的最大长度。在扩展中,如果我们允许循环回到序列的开始,这就可以设置为完整的字母表(26)或更长。

我们还需要定义要创建的随机序列的数量,在本例中为1000。这也可能是更多或更少。我希望实际需要的模式更少。

  1. # prepare the dataset of input to output pairs encoded as integers
  2. num_inputs = 1000
  3. max_len = 5
  4. dataX = []
  5. dataY = []
  6. for i in range(num_inputs):
  7. start = numpy.random.randint(len(alphabet)-2)
  8. end = numpy.random.randint(start, min(start+max_len,len(alphabet)-1))
  9. sequence_in = alphabet[start:end+1]
  10. sequence_out = alphabet[end + 1]
  11. dataX.append([char_to_int[char] for char in sequence_in])
  12. dataY.append(char_to_int[sequence_out])
  13. print sequence_in, '->', sequence_out

输入大概像这样

  1. PQRST -> U
  2. W -> X
  3. O -> P
  4. OPQ -> R
  5. IJKLM -> N
  6. QRSTU -> V
  7. ABCD -> E
  8. X -> Y
  9. GHIJ -> K

输入序列的长度在1和maxlen之间变化,因此需要zero padding(零填充)。在这里,我们使用了left-hand-side (prefix) padding和 keras自带的pad_sequences()函数。

X = pad_sequences(dataX, maxlen=max_len, dtype='float32')

训练模型在随机选择的输入模式下进行评估。这可以很容易地成为新的随机生成的字符序列。我认为,这也可以是一个线性序列,用“A”作为单个字符输入的输出。

  1. # LSTM with Variable Length Input Sequences to One Character Output
  2. import numpy
  3. from keras.models import Sequential
  4. from keras.layers import Dense
  5. from keras.layers import LSTM
  6. from keras.utils import np_utils
  7. from keras.preprocessing.sequence import pad_sequences
  8. from theano.tensor.shared_randomstreams import RandomStreams
  9. # fix random seed for reproducibility
  10. numpy.random.seed(7)
  11. # define the raw dataset
  12. alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  13. # create mapping of characters to integers (0-25) and the reverse
  14. char_to_int = dict((c, i) for i, c in enumerate(alphabet))
  15. int_to_char = dict((i, c) for i, c in enumerate(alphabet))
  16. # prepare the dataset of input to output pairs encoded as integers
  17. num_inputs = 1000
  18. max_len = 5
  19. dataX = []
  20. dataY = []
  21. for i in range(num_inputs):
  22. start = numpy.random.randint(len(alphabet)-2)
  23. end = numpy.random.randint(start, min(start+max_len,len(alphabet)-1))
  24. sequence_in = alphabet[start:end+1]
  25. sequence_out = alphabet[end + 1]
  26. dataX.append([char_to_int[char] for char in sequence_in])
  27. dataY.append(char_to_int[sequence_out])
  28. print sequence_in, '->', sequence_out
  29. # convert list of lists to array and pad sequences if needed
  30. X = pad_sequences(dataX, maxlen=max_len, dtype='float32')
  31. # reshape X to be [samples, time steps, features]
  32. X = numpy.reshape(X, (X.shape[0], max_len, 1))
  33. # normalize
  34. X = X / float(len(alphabet))
  35. # one hot encode the output variable
  36. y = np_utils.to_categorical(dataY)
  37. # create and fit the model
  38. batch_size = 1
  39. model = Sequential()
  40. model.add(LSTM(32, input_shape=(X.shape[1], 1)))
  41. model.add(Dense(y.shape[1], activation='softmax'))
  42. model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
  43. model.fit(X, y, epochs=500, batch_size=batch_size, verbose=2)
  44. # summarize performance of the model
  45. scores = model.evaluate(X, y, verbose=0)
  46. print("Model Accuracy: %.2f%%" % (scores[1]*100))
  47. # demonstrate some model predictions
  48. for i in range(20):
  49. pattern_index = numpy.random.randint(len(dataX))
  50. pattern = dataX[pattern_index]
  51. x = pad_sequences([pattern], maxlen=max_len, dtype='float32')
  52. x = numpy.reshape(x, (1, max_len, 1))
  53. x = x / float(len(alphabet))
  54. prediction = model.predict(x, verbose=0)
  55. index = numpy.argmax(prediction)
  56. result = int_to_char[index]
  57. seq_in = [int_to_char[value] for value in pattern]
  58. print seq_in, "->", result

output

  1. Model Accuracy: 98.90%
  2. ['Q', 'R'] -> S
  3. ['W', 'X'] -> Y
  4. ['W', 'X'] -> Y
  5. ['C', 'D'] -> E
  6. ['E'] -> F
  7. ['S', 'T', 'U'] -> V
  8. ['G', 'H', 'I', 'J', 'K'] -> L
  9. ['O', 'P', 'Q', 'R', 'S'] -> T
  10. ['C', 'D'] -> E
  11. ['O'] -> P
  12. ['N', 'O', 'P'] -> Q
  13. ['D', 'E', 'F', 'G', 'H'] -> I
  14. ['X'] -> Y
  15. ['K'] -> L
  16. ['M'] -> N
  17. ['R'] -> T
  18. ['K'] -> L
  19. ['E', 'F', 'G'] -> H
  20. ['Q'] -> R
  21. ['Q', 'R', 'S'] -> T

我们可以看到,尽管这个模型没有从随机生成的子序列中完美地学习字母表,但它做得很好。该模型没有进行调整,可能需要更多的训练或更大的网络,或者两者都需要(为读者提供一个练习)。

这是一个很好的自然扩展,对于“每个批处理中的所有顺序输入示例”,都可以在上面学到,它可以处理特殊的查询,但是这一次是任意的序列长度(最多的是最大长度)。

总结

这篇文章你应该学会了:

  • 如何开发一个简单的LSTM网络,一个字符到一个字符的预测。
  • 如何配置一个简单的LSTM,以在一个示例中跨时间步骤学习一个序列。
  • 如何配置LSTM来通过手动管理状态来学习跨示例的序列。