使用RNN生成莎士比亚文本(四)有状态RNN


该系列的Jupyter源码以及Char-RNN的模型权重&训练历史已放到附件以供下载:

源码和模型数据点击下载

使用RNN生成莎士比亚文本(四)有状态RNN

前面的Char-CNN中,模型使用的是无状态RNN,即每次训练迭代中,模型的隐藏状态都从0开始,隐藏状态在每个时间步长内都会更新一次,在最后一个时间步长结束后就重新置0。所以我们很容易想到优化方式:RNN在处理一个训练批次后,保留最终状态,作为下一训练批次的初始状态。

注意:只有当当前批次的输入序列从上一个批次中序列中断的精确位置快开始训练,有状态RNN才有意义。
  • 之前训练无状态RNN的混洗+重叠序列就不能用了,需要用顺序+不重合的输入序列。在使用window()方法分窗时,必须定shift=n_steps,这样才能保证没有重叠。
  • 不能使用shuffle了,因为不能混洗。
  • 批处理也变麻烦了,之前无状态是32个连续窗口放到同一个批处理里,下一个批处理不会从上一个批处理的每个中断处继续。且相邻的批处理的对应窗口也不是连续的。解决这个问题,最简单的方法是只使用包含单个窗口的“批(?)处理”
    批处理代码:

    tf.random.set_seed(42)
    dataset = tf.data.Dataset.from_tensor_slices(encoded[:train_size])
    dataset = dataset.window(window_length, shift=n_steps, drop_remainder=True)
    dataset = dataset.flat_map(lambda window: window.batch(window_length))
    dataset = dataset.batch(1)
    dataset = dataset.map(lambda windows: (windows[:, :-1], windows[:, 1:]))
    dataset = dataset.map(
      lambda X_batch, Y_batch: (tf.one_hot(X_batch, depth=max_id), Y_batch))
    dataset = dataset.prefetch(1)
    batch_size = 32
    encoded_parts = np.array_split(encoded[:train_size], batch_size)
    datasets = []
    for encoded_part in encoded_parts:
      dataset = tf.data.Dataset.from_tensor_slices(encoded_part)
      dataset = dataset.window(window_length, shift=n_steps, drop_remainder=True)
      dataset = dataset.flat_map(lambda window: window.batch(window_length))
      datasets.append(dataset)
    dataset = tf.data.Dataset.zip(tuple(datasets)).map(lambda *windows: tf.stack(windows))
    dataset = dataset.map(lambda windows: (windows[:, :-1], windows[:, 1:]))
    dataset = dataset.map(
      lambda X_batch, Y_batch: (tf.one_hot(X_batch, depth=max_id), Y_batch))
    dataset = dataset.prefetch(1)

然后搭建模型:

model = keras.models.Sequential([
    keras.layers.GRU(128, return_sequences=True, stateful=True,
                     #dropout=0.2, recurrent_dropout=0.2,
                     dropout=0.2,
                     batch_input_shape=[batch_size, None, max_id]),
    keras.layers.GRU(128, return_sequences=True, stateful=True,
                     #dropout=0.2, recurrent_dropout=0.2),
                     dropout=0.2),
    keras.layers.TimeDistributed(keras.layers.Dense(max_id,
                                                    activation="softmax"))
])

编写一个回调函数,使模型再每个训练轮次结束后,先重置隐藏状态,再返回到文本的开头

class ResetStatesCallback(keras.callbacks.Callback):
    def on_epoch_begin(self, epoch, logs):
        self.model.reset_states()

开始编译训练

这个模型收敛很快,因为批处理的量小。在自己的古董1050ti电脑平均8秒1轮,不到十分钟就跑完了。



model.compile(loss="sparse_categorical_crossentropy", optimizer="adam")
history = model.fit(dataset, epochs=50,
                    callbacks=[ResetStatesCallback()])

注意,我们上面训练的模型并不直接使用,而是使用他的权重。因为这个模型只能预测与训练期间使用的相同大小的批次。我们再创建一个相同的无状态模型,将有状态模型的权重复制到该模型中。

# 搭建一个与有状态RNN相同结构的无状态RNN
stateless_model = keras.models.Sequential([
    keras.layers.GRU(128, return_sequences=True, input_shape=[None, max_id]),
    keras.layers.GRU(128, return_sequences=True),
    keras.layers.TimeDistributed(keras.layers.Dense(max_id,
                                                    activation="softmax"))
])

建立模型以设置权重:

stateless_model.build(tf.TensorShape([None, None, max_id]))

将有状态RNN的权重导入到无状态RNN中:

stateless_model.set_weights(model.get_weights())
model = stateless_model

使用这个无状态RNN再进行文本生成:

tf.random.set_seed(42)

print(complete_text("t"))

输出:

thing idsumper your shint.
why, he has go too stone

结果还不错!

声明:奋斗小刘|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 使用RNN生成莎士比亚文本(四)有状态RNN


Make Everyday Count