TensorFlow创建LSTM,现在我们要在TensorFlow中创建一个LSTM网络。代码将松散地遵循这里找到的TensorFlow team教程,但是进行了更新和我自己的大量修改。将使用的文本数据集是Penn Tree Bank (PTB)数据集,它是常用的基准测试语料库。与往常一样,本文的所有代码都可以在AdventuresinML Github站点上找到。要运行这段代码,首先必须从这里下载并提取.tgz文件。首先,我们将介绍代码的数据准备部分。
准备数据
这段代码将逐字使用前面提到的TensorFlow教程中的以下函数:read_words、build_vocab和file_to_word_ids。我不会详细讨论这些函数,但基本上,它们首先将给定的文本文件分割为单独的单词和基于句子的字符(即句子末尾
下面的代码展示了这些函数在我的代码中是如何使用的:
def load_data():
# get the data paths
train_path = os.path.join(data_path, "ptb.train.txt")
valid_path = os.path.join(data_path, "ptb.valid.txt")
test_path = os.path.join(data_path, "ptb.test.txt")
# build the complete vocabulary, then convert text data to list of integers
word_to_id = build_vocab(train_path)
train_data = file_to_word_ids(train_path, word_to_id)
valid_data = file_to_word_ids(valid_path, word_to_id)
test_data = file_to_word_ids(test_path, word_to_id)
vocabulary = len(word_to_id)
reversed_dictionary = dict(zip(word_to_id.values(), word_to_id.keys()))
print(train_data[:5])
print(word_to_id)
print(vocabulary)
print(" ".join([reversed_dictionary[x] for x in train_data[:10]]))
return train_data, valid_data, test_data, vocabulary, reversed_dictionary
首先,我们分别为火车、验证和测试数据集设置目录路径。然后,对训练数据调用build_vocab(),创建一个字典,其中每个单词作为键,一个惟一的整数作为关联值。下面是word_to_id字典的示例:
{‘write-off’: 7229, ‘ports’: 8314, ‘fundamentals’: 4478, ‘toronto-based’: 5034, ‘head’: 638, ‘fairness’: 6417,…
接下来,我们使用word_to_id字典将每个文件的文本数据转换为整数列表。列表train_data的前5项看起来像:
[9970, 9971, 9972, 9974, 9975]
我还创建了一个反向字典,它允许您从唯一的整数标识符到对应的单词执行相反的操作。稍后,当我们将LSTM网络的输出重新构造为简单的英语句子时,将使用这种方法。
下一步是开发一个输入数据管道,允许以有效的方式提取成批数据。
创建输入数据管道
在训练期间使用feed字典向模型提供数据,虽然在教程中很常见,但并不有效——可以在TensorFlow站点上阅读。相反,使用TensorFlow队列和线程更有效。注意,有一种使用Dataset API的新方法,在本教程中不会用到,但我可能会在将来更新它,以包括这种新方法。我将这段代码打包到一个名为batch_producer的函数中——这个函数提取成批的x、y训练数据——x批数据被格式化为时间步长文本数据。y批处理是相同的数据,只是延迟了一个时间步长。例如,批次中的单个x, y样本,时间步长为8,看起来像:
- x = “A girl walked into a bar, and she”
- y = “girl walked into a bar, and she said”
请记住,x和y将是批量整数数据,大小(batch_size, num_steps),而不是如上所示的文本—但是,为了帮助理解,我以文本形式显示了上面的x和y示例。因此,正如上面的模型体系结构图所示,我们正在生成一个多对多LSTM模型,在这个模型中,我们将对模型进行训练,以预测每个单词在时间步长的顺序中的下一个单词。
代码是这样的:
def batch_producer(raw_data, batch_size, num_steps):
raw_data = tf.convert_to_tensor(raw_data, name="raw_data", dtype=tf.int32)
data_len = tf.size(raw_data)
batch_len = data_len // batch_size
data = tf.reshape(raw_data[0: batch_size * batch_len],
[batch_size, batch_len])
epoch_size = (batch_len - 1) // num_steps
i = tf.train.range_input_producer(epoch_size, shuffle=False).dequeue()
x = data[:, i * num_steps:(i + 1) * num_steps]
x.set_shape([batch_size, num_steps])
y = data[:, i * num_steps + 1: (i + 1) * num_steps + 1]
y.set_shape([batch_size, num_steps])
return x, y
在上面的代码中,首先,原始文本数据被转换成一个int32张量。接下来,计算完整数据集的长度并将其存储在data_len中,然后用整数除法(//)除以批大小,得到数据集中可用的完整批数据的数量。下一行将raw_data张量(大小限制为整批数据的数量,即0到batch_size * batch_len)重新塑造为(batch_size, batch_len)形状。下一行设置每个历元中的迭代次数——通常,设置这个次数是为了让每个epoch_中的所有训练数据都通过算法传递。这就是这里发生的事情——数据中的批数(batch_len)是整数除以时间步长——这给出了可以在单个epoch_中迭代的时间步长大小的批数。
下一行设置一个输入范围生成器队列——这是一个简单的队列,允许异步和线程从现有数据集中提取数据批。基本上,每次在模型的训练中需要更多的数据时,都会从0到epoch_size之间提取一个新的整数——然后在下面的行中使用这个整数异步地从数据张量中提取一批数据。当shuffle参数设置为False时,这个整数只是从0循环到epoch_size,然后重置为0再重复。
为了生成 x、y 批的数据,数据切片基于排队整数 i 从数据张量中提取。要了解其工作原理,可以更轻松地想象一个整数高达 20 的虚拟数据集 ,该数据集最多为 20 -[0,1,2,3,4,5,6,…,19,20]。假设我们将批处理大小设置为3,步骤数设置为2。因此,变量batch_len和epoch_size将分别等于6和2。虚拟的重塑数据将看起来像:
对于第一次数据批量提取,i = 0,因此我们的虚拟数据集提取的x为data[:, 0:2]:
提取的y的数据为[:,1:3]:
可以看出,提取的x和y张量的每一行都是长度num_steps的单独样本,行数是批处理长度。通过以这种方式组织数据,可以直接提取批处理数据,同时仍然在每个数据样本中维护正确的句子序列。
创建模型
在这个代码示例中,为了有更好的封装和更好的代码,我将在Python类中构建模型。第一个类是一个包含输入数据的简单类:
class Input(object):
def __init__(self, batch_size, num_steps, data):
self.batch_size = batch_size
self.num_steps = num_steps
self.epoch_size = ((len(data) // batch_size) - 1) // num_steps
self.input_data, self.targets = batch_producer(data, batch_size, num_steps)
我们将重要的输入数据信息传递给这个对象,例如批大小、重复的时间步长以及最后我们希望从中提取批数据的原始数据文件。前面解释的batch_producer函数在调用时将返回输入数据批处理x和相关的时间步骤+ 1目标数据批处理y。
下一步是创建LSTM模型。同样,我使用Python类来保存所有的信息和TensorFlow操作:
# create the main model
class Model(object):
def __init__(self, input, is_training, hidden_size, vocab_size, num_layers,
dropout=0.5, init_scale=0.05):
self.is_training = is_training
self.input_obj = input
self.batch_size = input.batch_size
self.num_steps = input.num_steps
初始化的第一部分非常容易理解,可以在input_obj中找到输入数据信息和批处理生成器操作。另一个重要的输入是boolean is_training——这允许将模型实例创建为用于训练的模型设置,或者只创建用于验证或测试的模型设置。
# create the word embeddings
with tf.device("/cpu:0"):
embedding = tf.Variable(tf.random_uniform([vocab_size, self.hidden_size], -init_scale, init_scale))
inputs = tf.nn.embedding_lookup(embedding, self.input_obj.input_data)
上面的代码块创建了单词embeddings。正如我在前面的教程中讨论和显示的,单词嵌入创建有意义的向量来表示每个单词。首先,我们使用size (vocab_size, hidden_size)初始化嵌入变量,该变量创建“查找表”,其中每一行表示数据集中的一个单词,列集是嵌入向量。在这种情况下,我们的嵌入向量长度等于我们的LSTM隐藏层的大小。
下一行对嵌入张量执行查找操作,其中输入数据集中的每个单词都与嵌入张量中的一行匹配,匹配的嵌入向量在输入中返回。
在这个模型中,嵌入层/向量将在模型训练中学习——然而,如果我们愿意,我们也可以使用另一个模型预先学习嵌入向量并将其上传到我们的模型中。
下一步是向输入数据添加一个下拉包装器——这有助于通过不断改变网络连接的结构来防止过度拟合:
if is_training and dropout < 1:
inputs = tf.nn.dropout(inputs, dropout)
创建LSTM网络
下一步是设置初始状态TensorFlow占位符。这个占位符将为每个训练批加载LSTM单元格的初始状态。在每个训练epoch开始时,输入数据将重置为文本数据集的开始,因此我们希望将状态变量重置为零。但是,在每个epoch中执行多个训练批期间,我们希望将前一个训练批的最终状态变量加载到当前训练批的LSTM单元中。这在我们的模型中保持了一定的状态连续性,因为我们线性地通过文本数据集进行处理。
# set up the state storage / extraction
self.init_state = tf.placeholder(tf.float32, [num_layers, 2, self.batch_size, self.hidden_size])
占位符函数的第二个参数是变量的大小(num_layers, 2, batch_size, hidden_size),需要一些解释。如果我们考虑一个单独的LSTM单元,对于它处理的每个训练样本,它有另外两个输入—单元格的前一个输出(h_{t-1})和前一个状态变量(s_{t-1})。这两个输入h和s是将完整状态数据加载到LSTM单元所需的。还要记住,每个样本的h和s实际上是大小等于隐藏层大小的向量。因此,对于批处理中的所有样本,对于单个LSTM单元格,我们需要shape (2, batch_size, hidden_size)的状态数据。最后,如果我们已经堆叠了LSTM单元格层,我们需要为每个层使用状态变量- num_layers。这给出了状态变量的最终形状:(num_layers, 2, batch_size, hidden_size)。
接下来的两个步骤涉及到设置这个状态数据变量,其格式需要将其输入TensorFlow LSTM数据结构:
state_per_layer_list = tf.unstack(self.init_state, axis=0)
rnn_tuple_state = tuple(
[tf.contrib.rnn.LSTMStateTuple(state_per_layer_list[idx][0], state_per_layer_list[idx][1])
for idx in range(num_layers)]
)
如果将一个标志设置为True, TensorFlow LSTM单元可以接受状态为一个元组(稍后将详细介绍)。tf.unstack命令从init_state张量创建许多张量,每个张量的形状(2,batch_size, hidden_size),每个张量对应一个堆叠的LSTM层(num_layer)。然后将这些张量加载到一个特定的TensorFlow数据结构LSTMStateTuple中,这是输入到LSTM单元格所必需的。
接下来,我们创建一个LSTM单元格,它将根据时间步长“展开”。接下来,我们应用一个下拉包装器来再次防止过度拟合。注意,我们将遗忘偏置值设置为1.0,这有助于防止重复的低遗忘门输出导致梯度消失,如上所述:
# create an LSTM cell to be unrolled
cell = tf.contrib.rnn.LSTMCell(hidden_size, forget_bias=1.0)
# add a dropout wrapper if training
if is_training and dropout < 1:
cell = tf.contrib.rnn.DropoutWrapper(cell, output_keep_prob=dropout)
接下来,如果我们在模型中包含多层堆叠的LSTM单元,我们需要使用另一个名为MultiRNNCell的TensorFlow对象,它执行必要的单元堆叠/分层:
if num_layers > 1:
cell = tf.contrib.rnn.MultiRNNCell([cell for _ in range(num_layers)], state_is_tuple=True)
注意,通过将state_is_tuple标志设置为True,我们告诉MultiRNNCell期望状态变量以LSTMStateTuple的形式出现。
创建LSTM网络结构的最后一步是在TensorFlow中创建一个动态RNN对象。该对象将在每个时间步上动态执行LSTM单元格的展开。
output, self.state = tf.nn.dynamic_rnn(cell, inputs, dtype=tf.float32, initial_state=rnn_tuple_state)
dynamic_rnn对象将我们定义的LSTM单元作为第一个参数,将嵌入的向量张量输入作为第二个参数。最后一个参数initial_state是我们将之前创建的时间步长为零的状态变量加载到展开的LSTM网络中的位置。
该操作创建两个输出,第一个输出来自所有展开的LSTM单元格,其形状为(batch_size、num_steps、hidden_size)。这些数据将在下一步被平铺到softmax分类层中。第二个输出state是从LSTM单元的最后一个时间步长中提取的(s, h) state元组。此状态操作/元组将在每个批处理训练操作期间提取,以作为输入(通过init_state)到下一个训练批处理中。
创建softmax、loss和优化器操作
接下来,我们必须使输出变平,这样我们就可以将它们输入到我们提出的softmax分类层中。我们可以使用-1符号来重塑我们的输出张量,第二轴设置为等于隐藏层的大小:
# reshape to (batch_size * num_steps, hidden_size)
output = tf.reshape(output, [-1, hidden_size])
接下来我们设置我们的softmax权重变量和标准的xw+b操作:
softmax_w = tf.Variable(tf.random_uniform([hidden_size, vocab_size], -init_scale, init_scale))
softmax_b = tf.Variable(tf.random_uniform([vocab_size], -init_scale, init_scale))
logits = tf.nn.xw_plus_b(output, softmax_w, softmax_b)
注意,logits操作只是张量乘法的输出——我们还没有添加softmax操作——这将在下面的损失计算中发生(以及在我们的辅助精度计算中)。
在此之后,我们必须设置我们的损失或成本函数,这将用于训练我们的LSTM网络。在本例中,我们将使用专门的TensorFlow序列对序列丢失函数进行排序。这个损失函数允许计算(潜在的)加权交叉熵损失的一系列值。这个损失函数的第一个参数是logits参数,它需要形状为(batch_size、num_steps、vocab_size)的张量——所以我们需要重新构造logits张量。损失函数的第二个参数是目标张量的形状(batch_size num_steps)每个值是一个整数(对应于一个独特的词在我们的例子中)——换句话说,这个张量包含这个词的真正值序列,我们希望我们的LSTM网络来预测。第三个重要的参数是权值张量,形状(batch_size, num_steps),它允许您对不同的样本或时间步长进行加权,以表示损失,也就是说,您可能希望损失更倾向于后一个时间步长,而不是更早的时间步长。这个模型没有加权,所以一个1的张量被传递给这个参数。
# Reshape logits to be a 3-D tensor for sequence loss
logits = tf.reshape(logits, [self.batch_size, self.num_steps, vocab_size])
# Use the contrib sequence loss and average over the batches
loss = tf.contrib.seq2seq.sequence_loss(
logits,
self.input_obj.targets,
tf.ones([self.batch_size, self.num_steps], dtype=tf.float32),
average_across_timesteps=False,
average_across_batch=True)
# Update the cost
self.cost = tf.reduce_sum(loss)
这个函数还有两个更重要的参数——average_across_timesteps和average_across_batch。如果average_across_timesteps被设置为True,那么成本将跨时间维度求和,如果average_across_batch为True,那么成本将跨批处理维度求和。在这种情况下,我们倾向于后一种选择。
最后,我们生成了cost操作,它将损失减少到单个标量值—我们也可以通过将average_across_timesteps设置为True来做类似的事情—但是,我要保持与TensorFlow教程的一致。
在接下来的几个步骤中,我们设置了一些操作来计算批量样本预测的准确性:
# get the prediction accuracy
self.softmax_out = tf.nn.softmax(tf.reshape(logits, [-1, vocab_size]))
self.predict = tf.cast(tf.argmax(self.softmax_out, axis=1), tf.int32)
correct_prediction = tf.equal(self.predict, tf.reshape(self.input_obj.targets, [-1]))
self.accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
首先,我们应用一个softmax操作来获得LSTM网络每个输出的每个单词的预测概率。然后,我们使用 argmax 函数使网络预测结果等于具有最高 softmax 概率的单词。然后将这些预测与实际的目标词进行比较,然后取平均值以获得准确性。
现在我们开始构建优化操作——在这种情况下,我们不是使用一个简单的“开箱即用”优化器——而是做一些操作来改进结果:
if not is_training:
return
self.learning_rate = tf.Variable(0.0, trainable=False)
tvars = tf.trainable_variables()
grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, tvars), 5)
optimizer = tf.train.GradientDescentOptimizer(self.learning_rate)
self.train_op = optimizer.apply_gradients(
zip(grads, tvars),
global_step=tf.contrib.framework.get_or_create_global_step())
首先,如果模型只用于预测、验证或测试,则不需要创建这些操作。如果模型用于训练,第一步是创建一个学习率变量。这样我们就可以在训练中降低学习速度,从而提高模型的最终结果。
接下来,我们希望在反向传播过程中缩减网络中的梯度大小——这在循环神经网络中推荐使用,以改善结果。通常使用1到5之间的剪切值。最后,我们使用learning_rate变量创建优化器操作,并应用剪切的梯度。然后执行梯度下降步骤——将该操作分配给train_op。每个培训批将调用这个操作train_op。
模型创建的最后两行涉及到learning_rate的更新:
self.new_lr = tf.placeholder(tf.float32, shape=[])
self.lr_update = tf.assign(self.learning_rate, self.new_lr)
首先,创建一个占位符,它将在运行训练new_lr时通过feed_dict参数输入。然后,通过tf将这个新的学习率分配给learning_rate。分配操作。这个操作lr_update将在每个epoch的开始运行。
现在模型结构已经完全创建好了,我们可以进入训练环节:
训练LSTM模型
训练功能将输入训练数据,以及各种模型参数(批量大小、步骤数等)。函数的第一部分看起来像:
def train(train_data, vocabulary, num_layers, num_epochs, batch_size, model_save_name,
learning_rate=1.0, max_lr_epoch=10, lr_decay=0.93):
# setup data and models
training_input = Input(batch_size=batch_size, num_steps=35, data=train_data)
m = Model(training_input, is_training=True, hidden_size=650, vocab_size=vocabulary,
num_layers=num_layers)
init_op = tf.global_variables_initializer()
首先,我们创建一个输入对象实例和一个模型对象实例,并传入必要的参数。因为TensorFlow图是在这些对象的初始化过程中创建的,所以TensorFlow全局变量初始化器操作只能在这些实例创建之后正确运行。
orig_decay = lr_decay
with tf.Session() as sess:
# start threads
sess.run([init_op])
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(coord=coord)
saver = tf.train.Saver()
接下来,我们启动会话,并运行变量初始化器操作。因为我们在输入对象中使用队列,所以还需要创建一个线程协调器并启动线程的运行。如果跳过这一步,或者将其放在training_input创建之前,程序将挂起。最后,创建一个保护程序实例,因为我们想要存储模型训练检查点和最终的训练模型。
然后进入划时代的训练循环:
for epoch in range(num_epochs):
new_lr_decay = orig_decay ** max(epoch + 1 - max_lr_epoch, 0.0)
m.assign_lr(sess, learning_rate * new_lr_decay)
current_state = np.zeros((num_layers, 2, batch_size, m.hidden_size))
for step in range(training_input.epoch_size):
if step % 50 != 0:
cost, _, current_state = sess.run([m.cost, m.train_op, m.state],
feed_dict={m.init_state: current_state})
else:
cost, _, current_state, acc = sess.run([m.cost, m.train_op, m.state, m.accuracy],
feed_dict={m.init_state: current_state})
print("Epoch {}, Step {}, cost: {:.3f}, accuracy: {:.3f}".format(epoch, step, cost, acc))
# save a model checkpoint
saver.save(sess, data_path + '\\' + model_save_name, global_step=epoch)
# do a final save
saver.save(sess, data_path + '\\' + model_save_name + '-final')
# close threads
coord.request_stop()
coord.join(threads)
每个epoch 的第一步是计算学习速率衰减因子,当达到最大epoch个数后,学习速率衰减因子逐渐减小。这个学习率衰减因子new_lr_衰减乘以学习率,并通过调用模型方法assign_lr分配给模型。这个方法看起来像:
def assign_lr(self, session, lr_value):
session.run(self.lr_update, feed_dict={self.new_lr: lr_value})
可以看到,这个函数只运行lr_update操作,这在前一节中已经解释过。
下一步是为LSTM模型创建一个零初始状态张量——我们将这个零初始状态张量分配给变量current_state。然后在指定的epoch大小内循环遍历每个训练操作。每次迭代我们都运行以下操作:m.train_op and m.state。如前所示,train_op操作计算模型的剪切梯度,并采取批处理步骤来最小化成本。状态操作返回最终展开的LSTM单元的状态,我们将需要将其作为下一个训练批处理的状态输入—注意,它替换了current_state变量的内容。这个current_state变量被插入到m中。通过feed_dict设置init_state占位符。
每50次迭代,我们还提取当前训练中模型的成本,以及针对当前训练批次的准确性,以便在训练期间提供打印反馈。输出如下:
Epoch 9, Step 1850, cost: 96.185, accuracy: 0.198
Epoch 9, Step 1900, cost: 94.755, accuracy: 0.235
最后,在每个epoch结束时,我们使用saver对象保存一个模型检查点,最后在训练结束时执行模型状态的最终保存。
预期的训练成果
预期的成本和在各个时期的精度进展取决于提供给模型的大量参数,以及变量随机初始化的结果。训练时间还取决于您是只使用cpu,还是也使用gpu(注意,我还没有使用gpu在Github存储库上测试代码)。
我的模型经过38个epoch,在以下参数的作用下,平均成本和训练批次准确率分别达到110-120和30%左右:
隐藏大小:650,步骤数:35,初始化规模:0.05,批次大小:20,堆叠LSTM层数:2,保持概率/退出:0.5
您可能认为精度不是很高,您是正确的,但是进一步的训练和更大的隐藏层将提供更好的最终精度值。要在更大的网络上执行进一步的训练,您确实需要使用gpu来加速训练——我将在以后的文章中进行此操作并给出结果。
测试模型
为了在测试或验证数据上测试模型,我创建了另一个名为test的函数,如下所示:
def test(model_path, test_data, reversed_dictionary):
test_input = Input(batch_size=20, num_steps=35, data=test_data)
m = Model(test_input, is_training=False, hidden_size=650, vocab_size=vocabulary,
num_layers=2)
saver = tf.train.Saver()
with tf.Session() as sess:
# start threads
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(coord=coord)
current_state = np.zeros((2, 2, m.batch_size, m.hidden_size))
# restore the trained model
saver.restore(sess, model_path)
# get an average accuracy over num_acc_batches
num_acc_batches = 30
check_batch_idx = 25
acc_check_thresh = 5
accuracy = 0
for batch in range(num_acc_batches):
if batch == check_batch_idx:
true_vals, pred, current_state, acc = sess.run([m.input_obj.targets, m.predict, m.state, m.accuracy],
feed_dict={m.init_state: current_state})
pred_string = [reversed_dictionary[x] for x in pred[:m.num_steps]]
true_vals_string = [reversed_dictionary[x] for x in true_vals[0]]
print("True values (1st line) vs predicted values (2nd line):")
print(" ".join(true_vals_string))
print(" ".join(pred_string))
else:
acc, current_state = sess.run([m.accuracy, m.state], feed_dict={m.init_state: current_state})
if batch >= acc_check_thresh:
accuracy += acc
print("Average accuracy: {:.3f}".format(accuracy / (num_acc_batches-acc_check_thresh)))
# close threads
coord.request_stop()
coord.join(threads)
我们首先创建一个输入和模型类,它匹配我们的训练输入和模型类。关键参数要与训练模型匹配,如隐藏尺寸、步骤数、批大小等。我们将把保存的模型变量加载到由测试模型实例创建的计算图中,如果维度不匹配TensorFlow将抛出一个错误。
接下来,我们创建一个tf.train.Saver()操作——当运行saver.restore(sess, model_path)时,它将把保存的所有模型变量加载到测试模型中。在处理完所有线程并创建一个零状态变量之后,我们设置了一些变量,这些变量与我们将如何评估精度以及查看预测字符串的一些特定实例有关。因为我们必须通过给模型输入一些数据来“预热”模型以获得良好的状态变量,所以我们只在一定数量的批次(即acc_check_thresh)之后测量精度。
当批号等于check_batch_idx时,代码运行m.predict操作提取特定批次数据的预测。批处理的第一个预测将通过反向字典将它们转换回实际单词(连同批处理的目标单词),然后与应该通过打印预测的单词进行比较。
使用训练后的模型,我们可以看到如下输出:
真值(第一行)与预测值(第二行):
平均精度:0.283
准确性不是很好,但是你可以看到网络正在匹配句子的“主旨”,也就是说,不是生成所有的准确单词,而是匹配一般的主题。
我希望你喜欢这篇文章——它已经很长了,但我希望这篇文章为你理解循环神经网络和LSTMs以及如何在TensorFlow中实现它们打下坚实的基础。如果您想学习如何在Keras中构建LSTM网络,请参阅本教程。