分类 TensorFlow & Keras 下的文章

注意力机制

这是Dzmitry Bahdanai等人在2014年的突破性论文中的核心思想。他们介绍了一种技术,该技术允许编码器在每个时间步长中专注于适当的单词(由编码器编码)。例如,在编码器需要输出单词'lait'的时间步长上,它会把注意力集中在单词'milk'上。这意味着从输入单词到其翻译的路径变短了,因此RNN的短期记忆限制的影响变小了。注意力机制彻底改变了神经机器翻译(一般来说是NLP),极大地改善了现有技术,特别是对于长句子。

现在,不是把编码器的最终隐藏状态发送给解码器,而是把其所有输出发送给解码器。在每个时间步长,解码器的记忆单元会计算这些编码器输出的加权总和:这确定了该步长会把重点关注在哪个单词。权重$\alpha_{(t,i)}$是在第t个解码器时间步长处的第$i$个编码器输出的权重。例如,如果权重$\alpha_{(3,2)}$远大于权重$\alpha_{(3,0)}$和权重$\alpha_{(3,1)}$,则解码器将更注意第二个单词,而不是其他两个单词,至少在这个时间步长。解码器的其余部分的工作方式与之前类似:在每个时间步长,记忆单元都会接收输入,再加上前一个时间步长的隐藏状态,最后,它接收前一个时间步长中的目标单词。

这些$\alpha_{(t,i)}$权重从何而来:它们是由一种对齐模型(或注意力层)的小型神经网络生成,该网络与其余的编码器-解码器一起进行训练。它始于有单个神经元的时间分布Dense层,该层接受所有编码器输出作为输入,与解码器先前的隐藏状态合并。该层为每个编码器的输出而输出一个分数:该分数用于衡量每个输出与解码器先前的隐藏状态对齐程度。最后,所有分数都通过softmax层,以获取每个编码器输出的最终权重。给定编码器时间步长的所有权重加起来为1(因为softmax层未按时间分布)。这种特殊的注意力机制称为Bahdanau注意力。由于它将编码器的输出和解码器的先前隐藏状态合并在一起,因此有时称为合并注意力(或加法注意力)

如果输入句子的长度为n个单词,并且假设输出句子的长度大致相同,则此模型需要计算大约$n^2$个权重。幸运的是,这种二次计算复杂度仍然是很容易处理的,因为即使长句子也没有成千上万个单词。

之后不久,Minh-Thang Luong等人在2015年发表的论文中提出了另一种常见的注意力机制。由于注意力机制的目的是测量编码器的输出之一与解码器的先前隐藏状态之间的相似性,因此作者提出了简单计算这两个向量的点积。因为这通常是一个相当好的相似度度量,当前的硬件可以更快地进行计算。为此,两个向量必须具有相同的维度。这被称为Luong注意力,有时也称为乘法注意力。点积给出一个分数,所有的分数都经过softmax层以给出最终的权重,就像在Bahdanau注意力中一样。他们提出的另一种简化方法是在每个时间步长而不是在前一个时间步长使用解码器的隐藏状态,然后使用注意力机制的输出直接计算解码器的预测(而不是使用它计算解码器的当前隐藏状态)。他们还提出了一种点积机制的变体,其中编码器的输出在计算点积之前先经过线性变换(即没有偏置项地时间分布Dense层)。这成为“通用”点积方法。他们将两种点积方法与合并注意力机制进行了对比(添加了一个重新缩放参数向量$v$),他们观察到,点积变体的效果要好于合并注意力。因此,现在很少使用合并注意力

注意力机制:

$$ \tilde{h}_{(t)}=\sum_i\alpha_{(t,i)}y_{(i)} $$

$$ \text{with }\alpha_{(t,i)}=\frac{\exp(e_{(t,i)})}{\sum_{i'}\exp(e_{(t,i')})} $$

$$ \text{and } e_{(t,i)}= \begin{cases} h_{(t)}^T & \text{点} \\ h_{(t)}^TWy_{(i)} & \text{通用} \\ v^T\tanh(W[h_{(t)};y_{(i)}]) & \text{合并} \end{cases} $$

# 使用TensorFlow Addons将Lulong注意力添加到编码器-解码器模型的方法
import tensorflow_addons as tfa
from tensorflow import keras
import tensorflow as tf

attention_mechanism = tfa.seq2seq.attention_wrapper.LuongAttention(
    units, encoder_state, memory_sequence_length=encoder_sequence_length
)
attention_decoder_cell = tfa.seq2seq.attention_wrapper.AttentionWrapper(
    decoder_cell, attention_mechanism, attention_layer_size=n_units
)
# 简单地将解码器单元包在AttentionWrapper中,提供了所需的注意力机制

视觉注意力

注意力机制可以用于多种目的。它们在NMT(神经网络机器翻译)之外的第一个应用之一是使用视觉注意力生成图像标题:卷积神经网络首先处理图像并输出一些特征图,然后具有注意力机制的解码器RNN生成标题,一次生成一个单词。在每个解码器时间步长(每个单词),解码器使用注意力模型将注意力集中在图像的正确部分

可解释性

注意力机制的另一个好处是,它们使人们更容易理解导致模型产生其输出的原因。这成为可解释性。当模型出错时,它特别有用:例如,如果在雪地里行走的狗的图片被标记为“在雪地里行走的狼”,那么可以返回并检查模型输出单词:狼“时所关注的是什么。可能会发现它不仅关注狗,还关注大雪,还暗示了可能的解释:也许许多模型学会区分狼和狗的方式是通过检查周围是否有大雪。然后,可以通过使用更多没有雪的狼和有雪的狗的图像训练模型来解决此问题。它对可解释性采用了不同的方法:在分类器的预测局部周围学习一个可解释的模型

在某些应用程序中,可解释性不仅仅是调试模型的工具,还可能是一项法律要求

Transformer架构

在2017年的一篇开创性论文中,一个Google研究团队提出了”注意力就是你所需要的一切。他们设法创建了一个名为Transformer的架构,该架构显著改善了NMT的现有水平,没有使用任何循环层或卷积层,只有注意力机制(加上嵌入层、密集层、归一化层以及其它一些小东西)。额外的好处是该架构的训练速度更快且更易于并行化,因此他们使用了比以前最先进的模型更少的时间和成本来训练。

  • 左边是编码器。就像前面一样,它以一批表示为单词ID序列的句子作为输入(输如形状为[批处理大小,最大输出句子长度]),并将每个单词编码为512维的表征(因此编码器的输出形状为[批处理大小,最大输出句子长度,512])。编码器的顶部堆叠了N次
  • 右侧是解码器。在训练期间,它以目标句子作为输入(也表示为单词ID的序列),向右移动一个时间步长(即在序列的开头插入一个序列开始令牌)。它也接收编码器的输出(即来自左侧的箭头)。解码器顶部也堆叠了N次,编码器堆的最终输出在N层的每层馈送到解码器。解码器在每个时间步长输出每个可能的下一个单词的概率(其输出形状为[批处理大小,最大输出句子长度,词汇表长度])
  • 在训练后,无法向编码器提供目标值,因此需要向其提供先前输出的单词(从序列开始令牌开始),因此,需要反复调用该模型,并在每个回合中预测下一个字
  • 有两个嵌入层;5XN个跳过连接,每个后面都有一个归一化层;2XN个前馈模块,每个模块由两个密集层组成(第一个使用ReLU激活函数,第二个没有激活函数),最后输出层是使用softmax激活函数的密集层,如何一次只看一个单词就能翻译一个句子,这就需要新组件出现:

    • 编码器的多头注意力(Multi-Head Attention)层对同一句子中每个单词与其他单词之间的关系进行编码,更关注最相关的单词。例如,句子“They welcomed the Queen of the United Kingdom”中的“Queen”单词的这一层的输出取决于句子中的所有单词,它可能会更关注“United”和“Kingdom”,而不是“They”或“Welcomed”。这个注意力机制被称为“自我注意力”(句子关注自身)。解码器的掩码多头注意力(Masked Multi-Head Attention)层执行相同的操作,但是每个单词只能被允许关注位于其前面的单词。最后,解码器的多头注意力层上部是解码器关注输入句子中单词的地方。例如,当解码器要输出这个单词的翻译时,解码器可能会密切注意输入句子中的单词“Queen”
    • 位置嵌入只是表示单词在句子中的位置的密集向量(就像词嵌入)。第n个位置嵌入被添加到每个句子中的第n个单词的词嵌入中。这是的模型可以访问每个单词的位置,这是必须的,因为“多头注意力”层不考虑单词的顺序或位置,只看它们的关系。由于所有其他层都是由于时间分布的,它们无法直到每个单词的位置(无论是相对还是绝对)。显然,相对和绝对的词位置很重要,所以需要将此信息提供给Transformer,而位置嵌入是实现此目的的好方法

    位置嵌入

    位置嵌入是对单词在句子中的位置进行编码的密集向量:第i个位置嵌入被添加到句子中第i个单词的词嵌入中。这些位置嵌入可以通过模型学习,但是在本论文中,作者更喜欢使用固定的位置嵌入,该固定位置嵌入是使用不同频率的正弦和余弦函数定义的。

正弦/余弦位置嵌入:

$$ \begin{cases} P_{p,2i}=\sin(p/10000^{2i/d}) \\ P_{p,2i+1}=\cos(p/10000^{2i/d}) \end{cases} $$

其中,$P_{p,i}$ 是位于句子中第 $p$ 个位置的单词嵌入的第 $i$ 个分量。

该方法具有与学习的位置相同的性能,但可以拓展到任意长的句子,因此受到青睐。在将位置嵌入添加到单词嵌入之后,模型的其余部分可以访问句子中每个单词的绝对位置,因为每个位置都有唯一的位置嵌入。此外,震荡函数(正弦和余弦)的选择使模型也可以学习相对位置。例如,相隔38个单词的单词在嵌入维度i=100和i=101中始终具有相同的位置嵌入值

TensorFlow中没有PositionalEmbedding层,但是创建起来很容易。处于效率原因,预先计算了构造函数中的位置嵌入矩阵(因此需要知道最大句子长度max_steps和每个单词表示的维度max_dims)。然后call()方法将该嵌入矩阵裁剪为输入的大小,将其添加到输入中。由于在创建位置嵌入矩阵时增加了一个额外的大小为1的维度,因此广播法则确保把矩阵添加到输入的每个句子中:

import tensorflow as tf
from tensorflow import keras
import numpy as np


class PositionalEncoding(keras.layers.Layer):
    def __init__(self, max_steps, max_dims, dtype=tf.float32, **kwargs):
        super().__init__(dtype=dtype, **kwargs)
        if max_dims % 2 == 1: max_dims += 1
        p, i = np.meshgrid(np.arange(max_steps), np.arange(max_dims // 2))
        pos_emb = np.empty((1, max_steps, max_dims))
        pos_emb[0, :, ::2] = np.sin(p / 10000 ** (2 * i / max_dims)).T
        pos_emb[0, :, 1::2] = np.cos(p / 10000 ** (2 * i / max_dims)).T
        self.positional_embedding = tf.constant(pos_emb.astype(self.dtype))

    def call(self, inputs):
        shape = tf.shape(inputs)
        return inputs + self.positional_embedding[:, :shape[-2], :shape[-1]]


# 然后可以创建Transformer的第一层:
embed_size = 512
max_steps = 500
vocab_size = 10000
encoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32)
decoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32)
embeddings = keras.layers.Embedding(vocab_size, embed_size)
encoder_embeddings = embeddings(encoder_inputs)
decoder_embeddings = embeddings(decoder_inputs)
positional_encoding = PositionalEncoding(max_steps, max_dims=embed_size)
encoder_in = positional_encoding(encoder_embeddings)
decoder_in = positional_encoding(decoder_embeddings)

多头注意力

要理解“多头注意力”层如何工作,必须理解“缩放点积注意力”层。假设编码器分析了输入句子‘The played chess’,并设法理解单词‘They’是主语,单词‘played’是动词,因此用这些词的表征来编码这些信息。现在假设解码器已经翻译了主语,认为接下来应该翻译动词。为此,它需要从输入句子中获取动词。这类似于字典查找:好像编码器创建了一个字典{‘subject’:‘They’,‘verb’:‘played’,···},解码器想要查找与键‘verb’相对应的值。但是,该模型没有具体的令牌来表示键;它具有这些概念的向量化表示(在训练期间学到的),因此用于查找的键不完全匹配字典中的任何键。解决方法是计算查询和字典中每个键的相似度,然后使用softmax函数将这些相似度分数转换为加起来为1的权重。如果表示动词的键到目前为止是最相似的查询,那么该键的权重将接近1。然后,该模型可以计算相应值的加权和,因此,如果‘verb’键的权重接近于1,则该加权和将非常接近单词‘played‘的表征。简而言之,可以将整个过程视为可区分的字典查找。像Luong注意力一样,Transformer使用的相似性度量只是点积。实际上,除了比例因子外,该公式与Luong注意力的相同

缩放点积注意力:$$Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_{keys}}})V$$
在此等式中:

  • $Q$是一个矩阵,每个查询包含一行。其形状为$[n_{queries},d_{keys}]$,其中$n_{queries}$是查询数,而$d_{keys}$是每个查询和键的维度
  • $K$是一个矩阵,每个键包含一行。其形状为$[n_{keys},d_{keys}]$,其中$n_{keys}$是键和值的数量
  • $V$是一个矩阵,每个值包含一行。其形状为$[n_{keys},d_{values}]$,其中$d_{values}$是每个值的数量
  • $QK^T$的形状是$[n_{queries},n_{keys}]$:每一对查询/键有一个相似性分数。$softmax$函数的输出具有相同的形状,但所有行的总和为1。最终输出的形状为$[n_{queries},d_{values}]$:每个查询有一行,其中每一行代表了查询结果(值的加权和)
  • 比例因子会缩小相似度分数,以避免softmax函数饱和,这会导致很小的梯度
  • 可以在计算softmax之前,通过一个非常大的负值加到相应的相似性分数中来屏蔽一些键/值对。这在”掩码多头注意力“层中很有用

在编码器中,这个等式应用于批处理中的每个输入句子,其中$Q,K,V$均等于输入句子中的单词列表(因此,这个句子里的每个单词都会和同一个句子里的每个单词进行比较,包括它自己)。类似地,在解码器的掩码注意力层中,该等式会应用于批处理中的每个目标句子,其中$Q,K,V$都等于目标句子中的单词列表,但这次使用掩码来防止任何单词将自己与其后面的单词进行比较(在推理时,解码器只能访问它已经输出的单词,而不能访问将来的单词,因此在训练期间,必须屏蔽掉以后输出的令牌)。在解码器的注意力层上部,键$K$和值$V$仅是编码器生成的单词编码的列表,而查询$Q$是解码器生成的单词编码的列表

keras.layers.Attention层实现了缩放点积注意力,将等式有效地应用于一个批量中的多个句子。它的输入就像$Q,K,V$一样,除了有额外的批处理维度

如果忽略跳过连接、归一化层、前馈块,那事实是这是缩放点积注意力而不是多头注意力,可以像下面这样实现Transformer模型的其余部分:

z = encoder_in
for N in range(6):
    Z = keras.layers.Attention(use_scale=True)([Z, Z])
encoder_outputs = Z
Z = decoder_in
for N in range(6):
    Z = keras.layers.Attention(use_scale=True, causal=True)([Z, Z])
    Z = keras.layers.Attention(use_scale=True, causal=True)([Z, encoder_outputs])
outputs = keras.layers.TimeDistributed(
    keras.layers.Dense(vocab_size, activation='softmax'))(Z)

use_scale=True参数创建了一个附加参数,该参数可以让层来学习如何适当降低相似性分数。这个Transformer模型有所不同,后者始终使用相同的因子$\sqrt{d_{keys}}$来降低相似度分数。创建第二个注意力层时,causal=True参数可确保每个输出令牌仅注意先前的输出令牌,而不是将来的

情感分析

如果MNIST是计算机视觉的“hello world”,那么IMDB评论数据集就是自然语言处理的“hello world”:它提取了来自著名的互联网电影数据库的50000条英文电影评论(其中25000条用于训练,25000条用于测试),每条评论的简单二元目标值表明了该评论是负面(0)还是正面(1)。就像MNIST一样,IMDB评论数据集也很受欢迎,这有充分的理由:它足够简单,可以在合理的时间内利用笔记本电脑来处理,但又具有挑战性。Keras提供了一个简单的函数来加载它:

from tensorflow import keras

(X_train, y_train), (X_test, y_test) = keras.datasets.imdb.load_data()
X_train[0][:10]
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb.npz
17465344/17464789 [==============================] - 1s 0us/step
17473536/17464789 [==============================] - 1s 0us/step





[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65]

数据集已经进行了预处理:X_train由一个评论列表组成,每条评论都由一个NumPy整数数组表示,其中每个整数代表一个单词。删除了所有标点符号,然后将单词转换为小写字母,用空格分隔,最后按频率索引(因此,小的整数对应于常用单词),整数0、1和2是特殊的:它们分别表示填充令牌、序列开始(SSS)令牌和位置单词。如果想可视化评论可以按以下方式对其进行解码:

word_index = keras.datasets.imdb.get_word_index()
id_to_word = {id_ + 3: word for word, id_ in word_index.items()}
for id_, token in enumerate(("<pad>", "<sos>", "<unk>")):  # 将整数0、1和2可视化为pad、sos和unk
    id_to_word[id_] = token
' '.join([id_to_word[id_] for id_ in X_train[0][:10]])
'<sos> this film was just brilliant casting location scenery story'

在实际的项目中,不得不自己处理文本。但是可以使用与之前使用的相同的Tokenizer类来执行此操作,但是这次要设置char_level=False(这是默认值)。在对单词进行编码时,它滤掉很多字符,包括大多数标点符号、换行符和制表符(但是可以通过filter参数来更改)。最重要的是,它使用空格来表示单词边界。对于英文和许多其他在单词之间使用空格的脚本(书面语言)这是可以的,但并非所有脚本都用此方式。中文在单词之间不使用空格,越南语甚至在单词中也使用空格,例如德语之类的语言经常将多个单词附加在一起,而没有空格。即使在英文中,空格也不总是分词的最佳方法:例如“San Francisco”
还有更好的选择,Taku Kudo在20118年发表的论文引入了一种无监督学习技术,用一种独立于语言的方式在子单词级别对文本进行分词和组词,像对待其他字符一样对待空格。使用这种方法,即使模型遇到一个从未见过的单词,也仍然可以合理地猜出其含义。例如,它可能在训练期间从未见过“smartest”一词,但可能学会了“smart”一词,并且还学习到后缀“est”的意思是“the most”,因此它可以推断出“smartest”的意思。Google的SentencePiece项目提供了一个开源实现,Takku Kudo和John Rihardson在论文中对此进行了描述
Rico Sennrich等人在较早的论文中提出了另一种选择,探索了创建子单词编码的其他方式(例如使用字节对编码)。TensorFlow团队于2019年6月发布了TF.Text库,该库实现了各种分词策略,包括WordPiece(字节对编码的一种变体)
如果想将模型部署到移动设备或Web浏览器上,而又不想每次都编写不同的预处理函数,那么可以使用TensorFlow操作来做预处理,因此其包含在模型本身中。首先使用TensorFlow数据集以文本(字节字符串)的形式加载原始的IMDB评论:

import tensorflow as tf
import tensorflow_datasets as tfds

datasets, info = tfds.load('imdb_reviews', as_supervised=True, with_info=True)
train_size = info.splits['train'].num_examples


# 编写预处理函数
def preprocess(X_batch, y_batch):
    X_batch = tf.strings.substr(X_batch, 0, 300)
    X_batch = tf.strings.regex_replace(X_batch, b'<br\\s*/?>', b' ')
    X_batch = tf.strings.regex_replace(X_batch, b"[^a-zA-z']", b' ')
    X_batch = tf.strings.split(X_batch)
    return X_batch.to_tensor(default_value=b'<pad>'), y_batch

它从阶段评论开始,每条评论仅保留前300个字符:这会加快训练速度,并且不会对性能产生太大的影响,因此通常可以在第一句话或第二句话中判断出评论是正面还是负面的。它使用正则表达式用空格来替换
"来填充所有评论,以使它们都具有相同的长度
接下来,需要构建词汇表。这需要依次遍历整个数据集,应用preprocess()函数,并使用Counter来对每个单词出现的次数进行计数:

from collections import Counter

vocabulary = Counter()
for X_batch, y_batch in datasets['train'].batch(32).map(preprocess):
    for review in X_batch:
        vocabulary.update(list(review.numpy()))
# 最常见的三个单词
vocabulary.most_common()[:3]
[(b'<pad>', 224533), (b'the', 61156), (b'a', 38567)]

但是,为了良好的性能,可能不需要模型知道字典中的所有单词,因此截断词汇表,只保留10000个最常见的单词:

vocab_size = 10000
truncated_vocabulary = [word for word, count in vocabulary.most_common()[:vocab_size]]

现在需要添加一个预处理步骤,以便把每个单词替换为其ID(即其在词汇表中的索引),使用out-of-vocabulary(oov)存储桶来创建一个查找表

words = tf.constant(truncated_vocabulary)
word_ids = tf.range(len(truncated_vocabulary), dtype=tf.int64)
vocab_init = tf.lookup.KeyValueTensorInitializer(words, word_ids)
num_oov_buckets = 1000
table = tf.lookup.StaticVocabularyTable(vocab_init, num_oov_buckets)
# 然后可以使用此表来查找几个单词的ID:
table.lookup(tf.constant([b'This movie was faaaaaantastic'.split()]))
<tf.Tensor: shape=(1, 4), dtype=int64, numpy=array([[   24,    12,    13, 10053]], dtype=int64)>

在表中可以找到单词’this‘、’movie‘和’was‘,因此它们的ID低于10000,而单词“faaaaaantastic”没找到,因此被映射的ID大于或等于10000的一个ovv桶中
TF Transform提供了一些有用的函数来处理此类词汇表。例如,查看tft.compute_and_apply_vocabulary()函数:它会遍历数据集来查早所有不同的的单词并构建词汇表,并将生成对使用此词汇表的单词进行编码所需的TF操作
现在,准备创建最终的训练集,对评论进行批处理,然后使用preprocess()函数将它们转换为单词的短序列,然后使用简单的encode_words()函数来对这些单词进行编码,该函数会使用刚才构建的单词表,最后预取一下批次:

def encode_words(X_batch, y_batch):
    return table.lookup(X_batch), y_batch


train_set = datasets['train'].batch(32).map(preprocess)
train_set = train_set.map(encode_words).prefetch(1)
# 最后创建模型并对其进行训练
embed_size = 128
model = keras.models.Sequential([
    keras.layers.Embedding(vocab_size + num_oov_buckets, embed_size, input_shape=[None]),
    keras.layers.GRU(128, return_sequences=True),
    keras.layers.GRU(128),
    keras.layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
history = model.fit(train_set, epochs=5)
Epoch 1/5
782/782 [==============================] - 9s 10ms/step - loss: 0.6192 - accuracy: 0.6206
Epoch 2/5
782/782 [==============================] - 8s 10ms/step - loss: 0.3875 - accuracy: 0.8297
Epoch 3/5
782/782 [==============================] - 8s 10ms/step - loss: 0.2304 - accuracy: 0.9146
Epoch 4/5
782/782 [==============================] - 8s 10ms/step - loss: 0.1442 - accuracy: 0.9513
Epoch 5/5
782/782 [==============================] - 8s 10ms/step - loss: 0.1087 - accuracy: 0.9624

第一层是嵌入层,它将单词ID转换为嵌入。嵌入矩阵需要每个ID(vocab_size+num_oov_buckets)一行,每个嵌入维度一列(这里使用128维,这是可以调整的超参数)。模型的输入使形状为[批处理大小,时间步长]的2D张量,而嵌入层的输出为[批处理大小,时间步长,嵌入大小]的3D张量。
该模型其余部分先当简单直接:它由两个GRU层组成,第二个GRU层仅返回最后一个时间步长的输出。输出层只是使用sigmoid激活函数来输出估计概率的单个神经元。该概率反映了评论表达了与电影有关的正面情绪。然后可以简单地编译模型,并将其拟合到之前准备的数据集中,进行几个轮次的训练。

创建训练数据集

首先,使用Keras的get_file()函数来下载莎士比亚的所有作品,并从Andrej Karpathy的Char-RNN项目中下载数据:

import tensorflow as tf
from tensorflow import keras

shakespeare_url = 'https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare'
filepath = keras.utils.get_file('input.txt', shakespeare_url)
with open(filepath) as f:
    shakespeare_text = f.read()

接下来,必须将每个字符编码为整数。一种选择是创建自定义的预处理层。在这种情况下,使用Keras的Tokenizer类会更简单。首先,为文本添加一个分词器:它会找到文本中使用的所有字符,并将它们映射到不同的字符ID,从1到不同字符的数量(它不从0开始,所以可以使用该值进行屏蔽)

tokenizer = keras.preprocessing.text.Tokenizer(char_level=True)
tokenizer.fit_on_texts(shakespeare_text)

设置char_level=True来得到字符级编码,而不是默认的单词级编码。默认情况下,该分词器将文本转换为小写(但是,如果不希望这样,可以将其设置为lower=False)。现在,分词器可以将一个句子(或句子列表)编码为字符ID列表并返回,并告诉有多少个不同的字符以及文本中的字符总数

tokenizer.texts_to_sequences(['First'])
[[20, 6, 9, 8, 3]]



tokenizer.sequences_to_texts([[20, 6, 9, 8, 3]])
['f i r s t']



max_id = len(tokenizer.word_index)
dataset_size = tokenizer.document_count
print(max_id)
print(dataset_size)
39
1115394

对全文进行编码,以便每个字符都由其ID表示(减去1即可得到从0到38的ID,而不是1到39的ID):

import numpy as np

[encoded] = np.array(tokenizer.texts_to_sequences([shakespeare_text])) - 1

在继续之前,需要将训练集分为训练集、验证集和测试集。不能只是将文本中的所有字符进行混洗

如何拆分数据集

避免训练集、验证集和测试集之间的任何重合都是非常重要的。例如,可以将文本的前90%用作训练集,其后的5%用作验证集,最后的5%作为测试集。在两个集合之前留一个空袭也是一个好主意,可以避免出现两个集合中的段落重合。

在处理时间序列时,通常会跨时间划分:例如,使用2000年到2012年之间的作为训练集,在2013年到2015年之间的作为验证集,在2016年到2018年之间的作为测试集。但是,在某些情况下,可以沿着其他维度来拆分,这将使得拥有更长的的训练时间。如果有2000年到2018年间10000家公司的财务状况数据,可以按照不同的公司将这些数据拆分。但是,其中许多公司很有可能会高度相关(例如,整个经济部门可能会一起上升或下降),如果在训练集和测试集中有相关联的公司,那测试集不会很有用,因为其对泛化误差的度量偏向于乐观

因此,跨时间划分通常更安全。但这隐含地假设RNN过去(在训练集中)学习到的模式将来仍会存在。换句话说,假设时间序列是稳定的(至少在广义上是这样)。对于许多时间序列,此假设是合理的(例如,化学反应就很好,因为化学定律不会每天改变),但是对于其他的并非如此(例如,金融市场并非一成不变,因为一旦交易者发现并开始利用弄个它们,模式就会消失)。为了确保时间序列足够稳定,可以按时间在验证集上绘制模型的误差:如果模型在验证集地第一部分比最后一部分表现得更好,则时间序列可能不过稳定,因此最好在较短的时间范围内训练模型

简而言之,将时间序列分为训练集、验证集和测试集不是一件容易的事,而如何划分将在很大程度上取决于手头的任务

回到莎士比亚,使用文本的前90%作为训练集(其余部分保留为验证集和测试集),并创建一个tf.data.Dataset,它将从该集合中逐个返回每个字符:

train_size = dataset_size * 90 // 100
dataset = tf.data.Dataset.from_tensor_slices(encoded[:train_size])

将顺序数据集切成多个窗口

训练集现在由超过一百万个字符的单个序列组成,所以不能直接在其上训练神经网络:RNN相当于一个超过一百万层的深度网络,会有一个很长的实例来训练它。取而代之的是,使用数据集的window()方法将这个长字符串序列转换为许多较小的文本窗口,数据集中的每个实例是将整个文本的很短的子字符串,并且RNN仅在这些子字符串的长度上展开。这称为时间截断反向传播。调用window()方法来创建短文本窗口数据集:

n_steps = 100
window_length = n_steps + 1
dataset = dataset.window(size=window_length, shift=1,
                         drop_remainder=True)  # 可以调整n_steps:在较短的输入序列上训练RNN会更容易,但是RNN不能学习比n_steps更长的模式,因此不要使其太短

默认情况下,window()方法会创建不重叠的窗口,但是为了获得最大可能地数据集,使用shift=1,以便使得第一个窗口包含0到100的字符,第二个窗口包含1到101的字符,依次类推。为确保所有窗口正好是101个字符长度(这使得不需要进行任何填充就可以创建批处理),设置drop_remainder=True(否则最后100个窗口将包含100个字符,99个字符,以此类推直至1个字符)。

window()方法创建一个包含窗口的数据集,每个窗口也表示为一个数据集。它是一个嵌套的数据集,类似列表的列表。想通过调用窗口数据集方法来转换每个窗口时(例如对它们进行混洗和批处理),此功能非常有用。但是,不能直接使用嵌套数据集进行训练,因为模型希望输入张量,而不是数据集。因此,必须调用flat_map()方法:它将嵌套的数据集转换为一个展平的数据集(一个不包含数据集的数据集)。例如,假设{1,2,3}表示一个包含张量1、2、3的序列的数据集。如果展平嵌套的数据集{{1,2},{3,4,5,6}},将获得展平的数据集{1,2,3,4,5,6}。此外,flat_map()方法将一个函数用作参数,它允许在展平前变换嵌套数据集中的每个数据集。例如,如果将函数lambda ds:ds.batch(2)传递给flat_map(),则它将嵌套数据集{{1,2},{3,4,5,6}}转换为平数据集{[1,2],[3,4],[5,6]}:它是大小为2的张量的数据集。考虑到这一点,这里需要将数据展平:

dataset = dataset.flat_map(lambda window: window.batch(window_length))

在每个窗口上调用batch(window_length):由于所有窗口的长度都恰好相同,因此每个窗口都获得一个张量。现在数据集包含101个字符的连续窗口。由于当训练集中的实例独立且分布相同时,梯度下降效果最好,因此需要对这些窗口进行混洗,然后,可以批量处理这些窗口并将输入(前100个字符)与目标(最后一个字符)分开:

batch_size = 32
dataset = dataset.shuffle(10000).batch(batch_size)
dataset = dataset.map(lambda windows: (windows[:, :-1], windows[:, 1:]))  # X从0到100,Y从1到101

将类别输入特征编码为独热向量或嵌入。在这里,将使用独热向量对每个字符进行编码,因为只有很少的不同字符:

dataset = dataset.map(lambda X_batch, Y_batch: (tf.one_hot(X_batch, depth=max_id), Y_batch))
# 添加预取
dataset = dataset.prefetch(1)

创建和训练Char-RNN模型

要基于前100个字符来预测下一个字符,可以使用有2个GRU层的RNN,每个GRU层有128个单元,输入(dropout)和隐藏状态(recurrent_dropout)的dropout率均为20%。如果需要,可以稍后调整这些超参数。输出层是一个时间分布的Dense层,这一层必须有39个单元(max_id),因为文本中有39个不同的字符,并且想为每个可能的字符输出一个概率(在每个时间步长)。每个时间步长的输出概率总和为1,因此将softmax激活函数应用于Dense层的输出。然后,使用‘sparse_categorical_crossentropy’损失和Adam优化器来编译此模型:

model = keras.models.Sequential([
    keras.layers.GRU(128, return_sequences=True, input_shape=[None, max_id], dropout=0, recurrent_dropout=0),
    keras.layers.GRU(128, return_sequences=True, dropout=0, recurrent_dropout=0),
    # 设置了dropout不为0,发现不能在GPU上运行,在CPU上太慢了,就改成了0
    keras.layers.TimeDistributed(keras.layers.Dense(max_id, activation='softmax'))
])
model.compile(loss='sparse_categorical_crossentropy', optimizer='adam')
history = model.fit(dataset, epochs=1)
31368/31368 [==============================] - 343s 11ms/step - loss: 0.9550

使用Char-RNN模型

现在有了一个可以预测莎士比亚写的文本中下一个字符的模型。为了提供一些文本,需要向之前一样对它进行预处理:

def preprocess(texts):
    X = np.array(tokenizer.texts_to_sequences(texts)) - 1
    return tf.one_hot(X, max_id)


X_new = preprocess(['How are yo'])
Y_pred = np.argmax(model.predict(X_new), axis=2)
tokenizer.sequences_to_texts(Y_pred + 1)[0][-1]  # 输出是0到38索引表示,输出+1为ID表示从1到39,第一句最后一个字母
'u'


生成假莎士比亚文本

要使用Char-RNN模型来生成新文本,可以向其提供一些文本,使模型预测最可能的下一个字母,把它添加在文本末尾,然后将拓展的文本提供给模型来预测下一个字母,以此类推。但是实际上,这经常导致相同的单词一遍又一遍重复。相反,可以使用tf.random.categorical()函数估计出来的概率随机选择下一个字符。这将产生更多不同和有趣的文本。对给定类对数概率(logits),categorical()函数会对随机索引进行采样。为了更好地控制生成的文本的多样性,可以把logits除以一个称为温度的数字,这样可以根据需要进行调整:接近0的温度倾向于高概率的字符,而非常高的温度会给予所有的字符相同的概率。下面的next_char()函数使用这种方法来选择要添加到输入文本的下一个字符:

def next_char(text, temperature=1):
    X_new = preprocess([text])
    y_proba = model.predict(X_new)[0, -1:, :]
    rescaled_logits = tf.math.log(y_proba) / temperature
    char_id = tf.random.categorical(rescaled_logits, num_samples=1) + 1
    return tokenizer.sequences_to_texts(char_id.numpy())[0]


# 编写函数,反复调用next_char()来获得下一个字符并将其添加到给定的文本中
def complete_text(text, n_chars=50, temperature=1):
    for _ in range(n_chars):
        text += next_char(text, temperature)
    return text

有状态RNN

早目前为止,仅仅使用了无状态RNN:在每次训练迭代中,模型都从一个充满零的隐藏状态开始,然后在每个时间步长更新状态,在最后一个时间步长之后将其丢弃,因为不再需要了。如果告诉RNN在处理一个训练批次后保留此最终状态并将其作为下一个训练批次的初始状态。这样,尽管反向传播只是通过短序列,模型仍可以学习长期模式。这称为有状态RNN。

首先,只有当批次中的每个输入序列均从上一批次中对应序列中断的确切位置开始,有状态RNN才有意义。因此,创建有状态RNN所需要做的第一件事时使用顺序和非重合的输入序列(而不是用来训练无状态RNN的混洗和重叠的序列)。因此,在创建Dataset时,在调用window()方法时必须使用shift=n_steps(而不是shift=1)。而且,不能调用shuffle方法。在为有状态RNN准备数据集时,批处理要比无状态RNN困难得多。如果要调用batch(32),那32个连续的窗口应该放入同一批处理中,而下一批处理不会在这些窗口中的每个中断处继续。第一批包含窗口1至32,第二批包含窗口33至64,因此,如果考虑每个批次中的第一个窗口(即窗口1和窗口33),可以看到它们是不连续的。解决此问题的最简单方法是只是用包含单个窗口的“批处理”:

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 windows: windows.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)

批处理比较困难,但并非不可能。例如,可以将莎士比亚的文本切成等长的32个文本,为每个文本创建一个连续输入序列的数据集,最后使用tf.train.Dataset.zip(dataset).map(lambda *windows:tf.stack(windows))来创建合适的连续批处理,其中批处理中的第n个输入序列恰好是从上一个批处理中第n个输入序列结束的位置开始

创建有状态RNN,在创建每个循环层时设置stateful=True,其次,有状态RNN需要知道批处理大小(因为它会为批处理中每个输入序列保留一个状态),因此必须在第一层中设置batch_input_shape参数,可以不指定第二个维度,因为输入可以有任意长度:

model = keras.models.Sequential([
    keras.layers.GRU(128, return_sequences=True, state_ful=True, dropout=0.2, recurrent_dropout=0.2,
                     batch_input_shape=[batch_size, None, max_id]),
    keras.layers.GRU(128, return_sequences=True, state_ful=True, dropout=0.2, recurrent_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()


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

处理长序列

在长序列上训练一个RNN,必须运行很多时间步长,从而展开的RNN成为一个非常深的网络。就像任何深度神经网络一样,它可能会遇到不稳定的梯度问题:它可能永远在训练,或者训练可能会不稳定。此外,当RNN处理一个长序列时,它会逐渐忘记序列中的第一个输入。

应对不稳定梯度问题

在深度网络中用于应对不稳定梯度问题的许多技巧也可以用于RNN:良好的参数初始化、更快的优化器、dropout,等等。但是,非饱和激活函数(例如ReLU)在这里可能没有太大的帮助。实际上,它可能导致RNN在训练过程中变得更加不稳定。假设梯度下降以一种在第一个时间步长稍微增加输出的方式来更新权重。由于每个时间步长都使用相同的权重,因此第二个时间步长的输出也可能会略有增加,第三个时间步长的输出也会稍有增加,以此类推,直到输出爆炸为止,而非饱和激活函数不能阻止这种情况。可以使用较小的学习率来降低这种风险,但也可以使用饱和激活函数(例如双曲正切)(这解释了为什么将其设定为默认值)。同样的方式,梯度本身也会爆炸。

此外,批量归一化不能像深度前馈网络那样高效地用于RNN。实际上,不能在时间步长之间使用它,而只能在递归层之间使用,更准确地说,从技术上讲,可以在记忆单元中添加一个BN层,以便将其应用于每个时间步长(在该时间步长和前一个时间步长的隐藏状态上)。但是,在每个时间步长都是用相同的BN层,并使用相同的参数,而不管输入的实际比例和偏移以及隐藏状态如何。在实践中,这不能产生良好的效果,BN仅在将BN应用于输入而非隐藏状态时才稍微收益。当应用于循环层之间时,它总比没有好,但不是在递归层中。在Keras中,可以简单地在每个递归层之前添加一个BatchNormalization层来完成此操作

归一化的另一种形式通常与RNN一起使用会更好:层归一化:它和批量归一化非常相似,但是它不是跨批量维度进行归一化,而是在特征维度上进行归一化。它的一个优点是可以在每个时间步长上针对每个实例独立地即时计算所需的统计信息。这也意味着它在训练和测试期间的行为方式相同(与BN相反),并且不需要使用指数移动平均值来估计样本中所有实例的特征统计信息。像BN一样,层归一化学习每个输入的比例和偏移参数。在RNN中,通常在输入和隐藏状态的线性组合后立即使用它

使用tf.keras在一个简单的记忆单元中实现层归一化。为此需要自定义个自定义记忆单元。就像常规层一样,不同处在于其call()方法采用两个参数:当前时间步长的inputs和上一个时间步长的隐藏states。states参数是包含一个或多个张量的列表。对于简单的RNN单元,它包含的单个张量等于上一个时间步长的输出,但是其他单元可能具有多个状态张量(例如,LSTMCell具有长期状态和短期状态)。一个单元必须具有state_size属性和output_size属性。在简单的RNN中,两者都等于单元数量。以下代码实现了一个简单的记忆单元,该单元的行为类似于SimpleRNNCell,不同之处在于它还在每个时间步长应用“层归一化”

import tensorflow as tf
from tensorflow import keras


class LNSimpleRNNCell(keras.layers.Layer):
    def __init__(self, units, activation='tanh', **kwargs):
        super().__init__(**kwargs)
        self.state_size = units
        self.output_size = units
        self.simple_rnn_cell = keras.layers.SimpleRNNCell(units, activation=None)
        self.layer_norm = keras.layers.LayerNormalization()
        self.activation = keras.activation.get(activation)

    def call(self, inputs, states):
        outputs, new_states = self.simple_rnn_cell(inputs, states)
        norm_outputs = self.activation(self.layer_norm(outputs))
        return norm_outputs, [norm_outputs]

LNSimpleRNNCell类继承自keras.layers.Layer类,就像任何自定义层一样。构造函数采用单元数量和所需激活函数,并设置state_size和output_size属性,然后创建一个没有激活函数的SimpleRNNCell(因为要在线性运算之后但在激活函数之前执行“层归一化”),然后,构造函数创建LayerNormalization层,最后获取需要的激活函数。call()函数通过应用于简单的RNN单元开始,该单元计算当前输入和先前输出的隐藏状态的线性组合,并返回两个结果(实际上,在SimpleRNNCell中,输出等于隐藏状态:也就是new_states[0]=outputs,因此可以在其他的call()方法中安全的忽略new_states)。接下来call()方法应用“层归一化”,然后跟随一个激活函数。最后,它返回两个输出(一个作为输出,另一个作为新的隐藏状态)。要使用此自定义单元,需要做的就是创建一个keras.layers.RNN层,并向其传递一个单元实例:

model = keras.models.Sequential([
    keras.layers.RNN(LNSimpleRNNCell(20), return_sequences=True, input_shape=[None, 1]),
    keras.layers.RNN(LNSimpleRNNCell(20), return_sequences=True),
    keras.layers.TimeDistributed(keras.layers.Dense(10))
])

同样可以创建一个自定义单元在每个时间步长之间应用dropout。但是有一种更简单的方法:所有循环层(除kears.layers.RNN之外)和Keras提供的所有单元都有一个超参数dropout和一个超参数recurrent_dropout:前者定义了应用于步长的dropout率(在每个时间步长),后者定义隐藏状态的dropout率(也在每个时间步长)。

在RNN中使用这些技术,可以缓解不稳定的梯度问题,更有效地训练RNN

解决短期记忆问题

由于数据在遍历RNN时会经过转换,因此在每个时间步长都会丢失一些信息。一段时间后,RNN的状态几乎没有任何最初输入的痕迹。这是一个热门问题,想象一下试图翻译一个长句子,当读完句子后,不知道如何开始。为了解决这个问题,引入了具有长期记忆的各种类型的单元。它们被证明非常成功,以至于不再使用基本的单元了

LSTM单元

长短期记忆(LSTM)单元由Sepp Hochriter和Jurgen Schmidhuber于1997年提出,并在随后的几年中由Alex Graves、Hasim Sak和Wojciech Zaremba等几位研究者逐步改进。如果把LSTM单元视作黑盒子,则它可以像基本单元一样使用,组它的性能会更好:训练会收敛得更快,它会检测数据中的长期依赖性。在Keras中,可以简单地使用LSTM层:

model = keras.models.Sequential([
    keras.layers.LSTM(20, return_sequences=True, input_shape=[None, 1]),
    keras.layers.LSTM(20, return_sequences=True),
    keras.layers.TimeDistributed(keras.layers.Dense(10))
])

或者,可以使用通用的keras.layers.RNN层,为其提供LSTMCell作为参数:

model = keras.models.Sequential([
    keras.layers.RNN(keras.layers.LSTMCell(20), return_sequences=True, input_shape=[None, 1]),
    keras.layers.RNN(keras.layers.LSTMCell(20), return_sequences=True),
    keras.layers.TimeDistributed(keras.layers.Dense(10))
])

LSTM层在GPU上运行时会使用优化过的实现,因此通常最好使用它

不过不查看框内的内容,则LSTM单元看起来与常规单元完全一样,除了它的状态被分为两个向量:$h_{(t)}$和$c_{(t)}$(‘c’代表“单元cell“。$h_{(t)}$视为短期状态,$c_{(t)}$为长期状态

LSTM单元关键的思想是网络可以学习长期状态下存储的内容、丢弃的内容以及从中读取的内容。当长期状态$c_{(t-1)}$从左到右遍历网络时,可以看到它首先经过了一个遗忘门,丢掉了一些记忆,然后通过加法操作添加了一些新的记忆(由输入门选择的记忆)。结果$c_{(t)}$直接送出来,无需任何进一步的转换。因此,在每个时间步长中,都会丢掉一些记忆,并添加一些记忆。此外,在加法运算之后,长期状态被复制并通过tanh函数传输,然后结果被输出门滤波。这将产生短期状态$h_{(t)}$(等于该时间步长的单元输出$y_{(t)}$)

首先,将当前输入向量$x_{(t)}$和先前的短期状态$h_{(t-1)}$馈入四个不同的全连接层。它们都有不同的目的:

  • 主要层是输出$g_{(t)}$的层。它通常的作用是分析当前输入$x_{(t)}$和先前(短期)状态$h_{(t-1)}$。在基本单元中,除了这一层,没有其他的东西,它的输出直接到$y_{(t)}$和$h_{(t)}$。相比之下,在LSTM单元中,该层的输出并非直接输出,而是将其最重要的部分存储在长期状态中(其余部分则丢弃)
  • 其他三层是门控制器。由于它们使用逻辑激活函数,因此它们的输出范围是0到1。它们的输出被馈送到逐元素乘法运算,因此如果输出0则关闭门,输出1则将门打开。特别地:

    • 遗忘门(由$f_{(t)}$控制)控制长期状态的哪些部分应当被删除
    • 遗忘门(由$i_{(t)}$控制)控制应将$g_{(t)}$的哪些部分添加到长期状态
    • 最后,输出门(由$o_{(t)}$控制)控制应在此时间步长读取长期状态的哪些部分并输出到$h_{(t)}$和$y_{(t)}$
      简而言之,LSTM单元可以学会识别重要的输入(这是输入门的作用),将其存储在长期状态中,只要需要就保留它(即遗忘门的作用),并在需要时将其提取出来。这就解释了为什么这些单元在识别时间序列、长文本、录音等长期模式方面取得了惊人的成功

    下列公式总结了如果计算单个实例在每个时间步长的单元的长期状态、短期状态以及其输出(与整个小批量的方程非常类似):

    $$ \begin{align*}i(t)=&\sigma(W_{xi}^Tx_{(t)}+W_{hi}^Th_{(t-1)}+b_i)\\ f_{(t)}=&\sigma(W_{xf}^Tx_{(t)}+W_{hf}h_{(t-1)}+b_f)\\ o_{(t)}=&\sigma(W_{xo}^T+W_{ho}^Th_{(t-1)}+b_o)\\ g_{(t)}=&tanh(W_{xg}^Tx_{(t)}+W_{hg}^Th_{(t-1)}+b_g)\\ c_{(t)}=&f_{(t)}\otimes c_{(t-1)}+i_{(t)}\otimes g_{(t)}\\ y_{(t)}=&h_{(t)}=o_{(t)}\otimes tanh(c_{(t)}\\ \end{align*} $$

    在此等式中:

    • $W_{xi},W_{xf},W_{xo},W_{xg}$是四层中每层与输入向量$x_{(t)}$连接的权重矩阵
    • $W_{hi},W_{hf},W_{ho},W_{hg}$是四层中的每层与先前的短期状态$h_{(t-1)}$连接的权重矩阵
    • $b_i,b_f,b_o,b_g$是四层中每层的偏置项。TensorFlow将$b_f$初始化为一个全是1不是0的向量。这样可以防止在训练开始时忘记一切

窥视孔连接

在常规LSTM单元中,门控制器只能查看输入$x_{(t)}$和先前的短期状态$h_{(t-1)}$。通过让它们也查看长期状态来给它们更多的功能,这可能是一个好主意。Felix Gers和Jergen Schmidhuber于2020年提出了这个想法。他们提出了一种带有额外连接的LSTM变体,称为窥视孔连接:先前的连接状态$c_{(t-1)}$作为输入添加到输出门的控制器。这通常会提高性能,但并非总是如此,并且没有明确的模式说明哪个任务更好。

在Keras中,LSTM层基于不支持窥视孔的keras.layers.LSTMCell单元。实验性的tf.keras.experimental.PeepholeLSTMCell可以,因此可以创建keras.layers.RNN层,并将PeepholeLSTMCell传递给其构造函数。LSTM单元还有许多其他变体。一种特别流行的变体是GRU单元

GRU单元

门控循环单元(Gated Recurrent Unit,GRU)是由Kyunghyun Cho等人在2014年的论文中提出的,该论文还介绍了Encoder-Decoder网络

GRU单元式LSTM单元的简化版,但它的性能也不错。以下是主要的简化:

  • 两个状态向量合并为一个向量$h_{(t)}$
  • 单个门控制器$z_{(t)}$控制遗忘门和输入门。如果门控制器输出1,则遗忘门打开,输入门关闭。如果输出0,则相反。无论何时记忆需要被存储,其存储位置需要首先被删除。实际上,这本身就是LSTM单元的常见变体
  • 没有输出门,在每个时间步长都输出完整的状态向量。但是,有一个新的门控制器$r_{(t)}$控制先前状态的哪一部分将显示给主要层$g_{(t)}$

GRU计算公式

$$ \begin{align*}z_{(t)}=&\sigma(W_{xz}^Tx_{(t)}+W_{hz}^Th_{(t-1)}+b_z)\\ r_{(t)}=&\sigma(W_{xr}^Tx_{(t)}+W_{hr}^Th_{(t-1)}+b_r)\\ g_{(t)}=&tanh(W_{xg}^Tx_{(t)}+W_{hg}^T(r_{(t)}\otimes h_{(t-1)})+b_g)\\ h_{(t)}=&z_{(t)}\otimes h_{(t-1)}+(1-z_{(t)})\otimes g_{(t)} \end{align*} $$

Keras提供了一个keras.layers.GRU层(基于keras.layers.GRUCell记忆单元),使用它只是用GRU来替换SimpleRNN或LSTM的问题。

LSTM和GRU单元是RNN成功的主要原因之一。尽管它们可以处理比RNN更长的序列,但它们的短期记忆仍然非常有限,而且很难学习100个或更多时间步长时间序列的长期模式,例如音频样本、长时间序列或长句子。解决这个问题的一种方法是缩短输入序列,例如使用一位卷积层。

使用一维卷积层处理序列

2D卷积层的工作原理是在图像上滑动几个相当小的内核(或过滤器),生成多个2D特征图(每个内核一个)。类似地,一维卷积层在一个序列上滑动多个内核,为每个内核生成一维卷积图。每个内核将学习检测单个非常短的顺序模式(不长于内核大小)。如果使用10个内核,则该层的输出将由10个一维的序列(所有长度相同)组成,或者等效地,可以将此输出视为单个10维的序列。这意味着可以构建由循环层和一维卷积层(甚至一维池化层)混合而成的神经网络。如果使用步幅为1且填充为'same'的一维卷积层,则输出序列的长度和输入序列的长度相同。但是,如果使用1填充为’valid‘或步幅大于1,则输出序列比输入序列短,因此需要确保相应地调整目标。

例如,一下模型与前面模型相同,不同之处在于它从一维卷积层开始,将输入序列进行2倍下采样,使用步幅为2。内核大小大于步幅,因此所有的输入将被用来计算层的输出,因此该模型可以学习保留有用的信息,而只删除不重要的细节。通过缩短序列,卷积层可以帮助GRU检测更长的模式。需要注意的是,还必须裁剪掉目标中的前三个时间步长(因为内核大小为4,所有卷积层的第一个输出将基于0至3的输入是时间步长),并对目标进行2倍的下采样

import numpy as np
import tensorflow as tf
from tensorflow import keras


def generate_time_series(batch_size, n_steps):
    freq1, freq2, offsets1, offsets2 = np.random.rand(
        4, batch_size, 1)  # 生成4个,(batch_size,1)形状的矩阵
    time = np.linspace(0, 1, n_steps)  # 时间为将0-1划分为n_steps个时间步长
    series = .5*np.sin((time-offsets1)*(freq1*10+10))  # 创建时间序列数值:sin(时间-相位)*频率)
    series += .2*np.sin((time-offsets2)*(freq2*20+20))
    series += .1*(np.random.rand(batch_size, n_steps)-.5) # 添加噪声
    return series[..., np.newaxis].astype(np.float32)

n_steps = 50
series = generate_time_series(10000, n_steps+10)
X_train, Y_train = series[:7000, :n_steps], series[:7000, -10:,0]
X_valid, Y_valid = series[7000:9000, :n_steps], series[7000:9000, -10:,0]
X_test, Y_test = series[9000:, :n_steps], series[9000:, -10:,0]
Y = np.empty((10000, n_steps, 10))
for step_ahead in range(1, 10+1):
    Y[:, :, step_ahead-1] = series[:, step_ahead:step_ahead +
                                   n_steps, 0]  # 创建时间序列,将预测目标设置为X后n_steps个时间步的数据作为序列中的一个数据,时间步与X相同但是是一个拥有10个数值的序列
Y_train = Y[:7000]
Y_valid = Y[7000:9000]
Y_test = Y[9000:]
print(series.shape)
print(X_train.shape)
print(Y_train.shape)


def last_time_step_mse(Y_true, Y_pred):
    return keras.metrics.mean_squared_error(Y_true[:, -1], Y_pred[:, -1])



model=keras.models.Sequential([
    keras.layers.Conv1D(filters=20,kernel_size=4,strides=2,padding='valid',input_shape=[None,1]),
    keras.layers.GRU(20,return_sequences=True),
    keras.layers.GRU(20,return_sequences=True),
    keras.layers.TimeDistributed(keras.layers.Dense(10))
])
model.compile(loss='mse',optimizer='adam',metrics=[last_time_step_mse])
history=model.fit(X_train,y_train[:,3::2],epochs=20,
                 validtion_data=(X_valid,Y_valid[:,3::2]))
(10000, 60, 1)
(7000, 50, 1)
(7000, 50, 10)
Epoch 1/20
219/219 [==============================] - 4s 11ms/step - loss: 0.0705 - last_time_step_mse: 0.0629 - val_loss: 0.0464 - val_last_time_step_mse: 0.0399
Epoch 2/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0403 - last_time_step_mse: 0.0328 - val_loss: 0.0358 - val_last_time_step_mse: 0.0284
Epoch 3/20
219/219 [==============================] - 2s 9ms/step - loss: 0.0312 - last_time_step_mse: 0.0215 - val_loss: 0.0282 - val_last_time_step_mse: 0.0176
Epoch 4/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0260 - last_time_step_mse: 0.0150 - val_loss: 0.0251 - val_last_time_step_mse: 0.0137
Epoch 5/20
219/219 [==============================] - 2s 9ms/step - loss: 0.0240 - last_time_step_mse: 0.0130 - val_loss: 0.0238 - val_last_time_step_mse: 0.0125
Epoch 6/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0231 - last_time_step_mse: 0.0123 - val_loss: 0.0231 - val_last_time_step_mse: 0.0122
Epoch 7/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0222 - last_time_step_mse: 0.0116 - val_loss: 0.0225 - val_last_time_step_mse: 0.0117
Epoch 8/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0218 - last_time_step_mse: 0.0113 - val_loss: 0.0218 - val_last_time_step_mse: 0.0109
Epoch 9/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0212 - last_time_step_mse: 0.0108 - val_loss: 0.0216 - val_last_time_step_mse: 0.0108
Epoch 10/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0207 - last_time_step_mse: 0.0103 - val_loss: 0.0209 - val_last_time_step_mse: 0.0101
Epoch 11/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0202 - last_time_step_mse: 0.0099 - val_loss: 0.0207 - val_last_time_step_mse: 0.0100
Epoch 12/20
219/219 [==============================] - 2s 7ms/step - loss: 0.0198 - last_time_step_mse: 0.0095 - val_loss: 0.0200 - val_last_time_step_mse: 0.0093
Epoch 13/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0192 - last_time_step_mse: 0.0090 - val_loss: 0.0194 - val_last_time_step_mse: 0.0088
Epoch 14/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0185 - last_time_step_mse: 0.0083 - val_loss: 0.0187 - val_last_time_step_mse: 0.0079
Epoch 15/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0178 - last_time_step_mse: 0.0075 - val_loss: 0.0181 - val_last_time_step_mse: 0.0076
Epoch 16/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0170 - last_time_step_mse: 0.0066 - val_loss: 0.0173 - val_last_time_step_mse: 0.0064
Epoch 17/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0164 - last_time_step_mse: 0.0060 - val_loss: 0.0165 - val_last_time_step_mse: 0.0057
Epoch 18/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0159 - last_time_step_mse: 0.0054 - val_loss: 0.0162 - val_last_time_step_mse: 0.0055
Epoch 19/20
219/219 [==============================] - 2s 9ms/step - loss: 0.0154 - last_time_step_mse: 0.0050 - val_loss: 0.0157 - val_last_time_step_mse: 0.0049
Epoch 20/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0151 - last_time_step_mse: 0.0047 - val_loss: 0.0153 - val_last_time_step_mse: 0.0046
32/32 [==============================] - 0s 3ms/step - loss: 0.0149 - last_time_step_mse: 0.0044





[0.014898738823831081, 0.0043519060127437115]

精度为0.004是目前为止最好的模型,卷积层确实有帮助。

WaveNet

在2016年的一篇论文中,介绍了一种称为WaveNet的架构。他们堆叠了一维卷积层,使每一层的扩散(dilation)率(每个神经元输入的分散程度)加倍:第一个卷积层一次只看到两个时间步长,而下一个卷积层一次看到四个时间步长(其接受视野为四个时间步长),下一次看到8个时间步长,以此类推。这样,较低的层学习短期模式,而较高的曾学习长期模式。由于扩散率的加倍,网络可以非常有效地处理非常大的序列。

在WaveNet论文中,作者实际上堆叠了10个卷积层,其扩散率为1、2、4、8,···,256,512,然后又堆叠了另一组10个相同的层(扩散率也分别为1、2、4、8,···,256,512),然后又是另一组相同的10层。它们通过指出具有这些扩散率的10个卷积层的单个堆栈1来证明该结构的合理性,就像内核大小为1024的超高效卷积层一样工作(除了更快、更强大且使用更少的参数之外),这就是为什么它们堆叠了3个这样的块。它们还对输入序列进行了左填充,这些零与每个层之前的扩散率相等,以保证整个网络中相同的序列长度,以下实现了简化的WaveNet来处理和之前相同的序列:

model=keras.models.Sequential()
model.add(keras.layers.InputLayer(input_shape=[None,1]))
for rate in (1,2,4,8)*2:
    model.add(keras.layers.Conv1D(filters=20,kernel_size=2,padding='causal',activation='relu',dilation_rate=rate))
model.add(keras.layers.Conv1D(fliters=10,kernel_size=1))
model.compile(loss='mse',optimizer='adam',metrics=[last_time_step_mse],validation_data=(X_valid,Y_valid))
history=model.fit(X_train,Y_train,epochs=20,validation_data=(X_valid,Y_valid))
model.evaluate(X_test,Y_test)
Epoch 1/20
219/219 [==============================] - 3s 9ms/step - loss: 0.0700 - last_time_step_mse: 0.0577 - val_loss: 0.0377 - val_last_time_step_mse: 0.0237
Epoch 2/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0318 - last_time_step_mse: 0.0185 - val_loss: 0.0299 - val_last_time_step_mse: 0.0170
Epoch 3/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0274 - last_time_step_mse: 0.0150 - val_loss: 0.0264 - val_last_time_step_mse: 0.0138
Epoch 4/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0258 - last_time_step_mse: 0.0137 - val_loss: 0.0255 - val_last_time_step_mse: 0.0133
Epoch 5/20
219/219 [==============================] - 2s 7ms/step - loss: 0.0247 - last_time_step_mse: 0.0128 - val_loss: 0.0246 - val_last_time_step_mse: 0.0125
Epoch 6/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0239 - last_time_step_mse: 0.0121 - val_loss: 0.0236 - val_last_time_step_mse: 0.0116
Epoch 7/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0232 - last_time_step_mse: 0.0114 - val_loss: 0.0231 - val_last_time_step_mse: 0.0110
Epoch 8/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0227 - last_time_step_mse: 0.0110 - val_loss: 0.0225 - val_last_time_step_mse: 0.0106
Epoch 9/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0221 - last_time_step_mse: 0.0105 - val_loss: 0.0222 - val_last_time_step_mse: 0.0102
Epoch 10/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0216 - last_time_step_mse: 0.0099 - val_loss: 0.0217 - val_last_time_step_mse: 0.0097
Epoch 11/20
219/219 [==============================] - 2s 7ms/step - loss: 0.0212 - last_time_step_mse: 0.0096 - val_loss: 0.0214 - val_last_time_step_mse: 0.0093
Epoch 12/20
219/219 [==============================] - 2s 7ms/step - loss: 0.0207 - last_time_step_mse: 0.0089 - val_loss: 0.0211 - val_last_time_step_mse: 0.0087
Epoch 13/20
219/219 [==============================] - 2s 7ms/step - loss: 0.0203 - last_time_step_mse: 0.0085 - val_loss: 0.0203 - val_last_time_step_mse: 0.0080
Epoch 14/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0198 - last_time_step_mse: 0.0079 - val_loss: 0.0213 - val_last_time_step_mse: 0.0092
Epoch 15/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0196 - last_time_step_mse: 0.0078 - val_loss: 0.0202 - val_last_time_step_mse: 0.0077
Epoch 16/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0193 - last_time_step_mse: 0.0075 - val_loss: 0.0199 - val_last_time_step_mse: 0.0077
Epoch 17/20
219/219 [==============================] - 2s 7ms/step - loss: 0.0190 - last_time_step_mse: 0.0073 - val_loss: 0.0200 - val_last_time_step_mse: 0.0076
Epoch 18/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0188 - last_time_step_mse: 0.0069 - val_loss: 0.0193 - val_last_time_step_mse: 0.0070
Epoch 19/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0187 - last_time_step_mse: 0.0069 - val_loss: 0.0195 - val_last_time_step_mse: 0.0073
Epoch 20/20
219/219 [==============================] - 2s 8ms/step - loss: 0.0183 - last_time_step_mse: 0.0067 - val_loss: 0.0188 - val_last_time_step_mse: 0.0066
32/32 [==============================] - 0s 8ms/step - loss: 0.0183 - last_time_step_mse: 0.0063

[0.018330231308937073, 0.006296257022768259]

MSE到达了0.0063

该Sequential模型从显示输入层开始,然后继续使用填充为“causal”的一维卷积层:这确保卷积层在进行预测时不会窥视未来(它等效于在输入的左侧填充正确数量的零且使用’valid‘填充)。然后添加类似的成对层,使用不断扩大的扩散率:1、2、4、8,还是1、2、4、8.最后添加输出层:一个具有10个大小为1的滤波器且没有任何激活函数的卷积层。多亏了填充层,每个卷积层都输出和输入序列长度相同的序列,因此在训练过程中使用的目标是完整序列,无需将裁减或对它们进行下采样。

到目前为止,最后两个模型在预测时间序列方面提供了最佳性能。在WaveNet论文中,作者在各种音频任务(架构的名称)上获得了最先进的性能,包括从文本到语音的任务,在多种语言中产生了令人难以置信的逼真的声音。他们还是用该模型来生成音乐,一次生成一个音频样本。当意识到一秒钟的音频可以包含数万个时间步长时,这一成就会更加令人印象深刻,甚至LSTM和GRU都无法处理如此长的序列。

循环神经元和层

到目前为止,我们只关注前馈神经网络,其中激活仅在一个方向上流动,从输入层流向输出层。循环神经网络看起来非常像前馈神经网络,除了它还具有指向反向的连接。最简单的RNN由一个神经元接收输入,产生输出并将该输出返送回自身组成。在每个时间步长$t$(也称为帧),该循环神经网络接收输入$x_{(t)}$和前一个时间步长$y_{(t-1)}$的输出。由于在第一个时间步长没有先前的输出,因此通常将其设置为0。

一层递归神经元。在每个时间步长$t$,每个神经元接收输入向量$x_{(t)}$和前一个时间步长的输出向量$y_{(t-1)}$。输入和输出都是向量

每个循环神经元都有两组权重:一组用于输入$x_{(t)}$,另一组用于前一个时间步长$y_{(t-1)}$的输出。称这些权重向量为$w_x$和$w_y$。如果考虑整个循环曾而不仅仅一个神经元,则可以将所有的权重向量放在两个权重矩阵$W_x$和$W_y$中。然后可以如预期的那样计算整个循环层的输出向量,如公式所示:$b$是偏置向量,$\phi(\cdot)$是激活函数(例如ReLU)

单个实例的循环层的输出:
$$y_{(t)}=\phi(W_x^Tx_{(t)}+W_y^Ty_{(t-1)}+b)$$
正如前馈神经网络一样,可以通过将时间步长$t$处的所有输入都放在输入矩阵$X_{(t)}$中,来一次性地计算整个小批量中的递归层的输出

小批量中的所有实例的循环神经元层的输出:
$$Y_{(t)}=\phi(X_{(t)}W_x+Y_{(t-1)}W_y+b)=\phi([X_{(t)}\,Y_{(t-1)}]W+b)$$
其中:
$$W=\begin{bmatrix}W_x \\ W_y\end{bmatrix}$$
在此等式中:
$Y_{(t)}$是一个$m\times n{neurons}$矩阵,包括小批量处理中每个实例在时间步长t时该层的输出($m$是小批量处理中的实例数量,$n_{neurons}$是神经元数量)。

$X_{(t)}$是一个$m\times n_{inputs}$矩阵,包括所有实例的输出($n_{inputs}$是输入特征的数量)

$W_x$是一个$n_{inputs}\times n_{neurons}$矩阵,包含当前时间步长的输入的连接权重

$W_y$是一个$n_{neurons}\times n_{neurons}$矩阵,包含前一个时间步长的输出的连接权重

$b$是大小为$n_{neurons}$的向量,包含每个神经元的偏置项

  • 权重矩阵$W_x$和$W_y$经常垂直合并成形状为$(n_{inputs}+n_{neurons})\times n_{neurons}$的单个权重矩阵$W$
  • 符号$[X_{(t)}\,Y_{(t-1)}]$表示矩阵$X_{(t)}$和$Y_{(t-1)}$的水平合并

$Y_{(t)}$是$X_{(t)}$和$Y_{(t-1)}$的函数,$Y_{(t-1)}$是$X_{(t-1)}$和$Y_{(t-2)}$的函数,以此类推。自时间$t=0$以来,这使$Y_{(t)}$成为所有输入的函数。在第一个时间步长$t=0$时,没有先前的输出,因此通常假定它们均为零

记忆单元

由于在时间步长$t$时递归神经元的输出是先前时间步长中所有输入的函数,因此可以说它具有记忆的形式。在时间步长上保留某些状态的神经网络的一部分称为记忆单元(或简称为单元)。单个循环神经元或一层循环神经元是一个非常基本的单元,它只能学习短模式(通常约为10个步长,但这取决于任务)。

通常,在时间步长$t$的单元状态,表示为$h_{(t)}$(“h”代表“隐藏”),是该时间步长的某些输入和其前一个时间步长状态的函数:$h_{(t)}=f(h_{(t-1)},x_{(t)})$。它在时间步长$t$处的输出表示为$y_{(t)}$,也是先前状态和当前输入的函数。

输入和输出序列

RNN可以同时接收输入序列并产生输出序列。这种类型的序列到序列的网络可用于预测注入股票价格之类的时间序列:输入过去N天的价格作为输入,它必须输出未来偏移一天的价格(即从前N-1天到明天)。

或者,可以向网络提供一个输入序列,并忽略除了最后一个输出外的所有输入。换句话说,这是一个序列到向量的网络。例如,可以向网络提供与电影评论相对应的单词序列,然后网络将输出一个情感的分(例如从-1[恨]到+1[爱])

相反,可以在每个时间步长中一次又一次地向网络提供相同的输入向量,并让其输出一个序列。这是一个向量到序列的网络。例如,输入可以是图像(或CNN地输出),而输出可以是图像的描述。

最后,可能有一个成为编码器的序列到向量的网络,然后是一个成为解码器地向量到序列的网络。例如,这可以用于将句子从一种语言翻译成另一种语言。向网络提供一种语言的句子,编码器会将其转换为单个向量的表征,然后解码器会将此向量解码成另一种语言的句子。这种称为“编码器-解码器(Encoder-Decoder)”的两步模型比使用单个序列到序列的RNN进行即时翻译要好得多:句子的最后一个单词会影响翻译的第一个词,因此在翻译之前需要等待直到看完整个句子。

训练RNN

要训练RNN,就要将其按时间逐步展开,然后简单的使用常规的反向传播。这种策略称为“时间反向传播”(Back Propagation Through Time,BPTT)。

就像在常规反向传播中一样,这里有一个通过展开网络的第一次前向通路。然后使用成本函数$C(Y_{(0)},Y_{(1)},\cdots,Y_{(T)}$(其中$T$是最大时间步长)来评估输出序列。此成本函数可能会忽略某些输出,在序列到向量RNN中,除最后一个输出外,所有的输出都将被忽略。然后,该成本函数的梯度通过展开的网络反向传播。最后,使用在BPTT期间计算的梯度来更新模型参数。梯度反向通过成本函数使用的所有输出,而不是最终输出。而且,由于在每个时间步长上使用相同的参数$W$和$b$,所以反向传播会做正确的事情,在所有时间步长上求和

预测时间序列

假设你正在研究网站上每小时的活跃用户数、城市的每日温度或使用多个指标来评估每个季度公司的财务状况。在所有这些情况下,数据是每个时间步长一个或多个值的序列。这称为时间序列。在前两个示例中,每个时间步长只有一个值,因此它们是单变量时间序列,而在财务示例中,每个时间步长有多个值(例如,公司的收入、债务等),因此它是一个多变量时间序列。典型的任务是预测未来值,这称为预测。另一个常见的任务是填补空白:预测过去的缺失值。这称为插补。

使用由generate_time_series()函数生成的时间序列来研究:

import numpy as np
import tensorflow as tf
from tensorflow import keras


def generate_time_series(batch_size, n_steps):
    freq1, freq2, offsets1, offsets2 = np.random.rand(
        4, batch_size, 1)  # 生成4个,(batch_size,1)形状的矩阵
    time = np.linspace(0, 1, n_steps)  # 时间为将0-1划分为n_steps个时间步长
    series = .5*np.sin((time-offsets1)*(freq1*10+10))  # 创建时间序列数值:sin(时间-相位)*频率)
    series += .2*np.sin((time-offsets2)*(freq2*20+20))
    series += .1*(np.random.rand(batch_size, n_steps)-.5) # 添加噪声
    return series[..., np.newaxis].astype(np.float32)
C:\ProgramData\Miniconda3\envs\tf2\lib\site-packages\numpy\_distributor_init.py:30: UserWarning: loaded more than 1 DLL from .libs:
C:\ProgramData\Miniconda3\envs\tf2\lib\site-packages\numpy\.libs\libopenblas.GK7GX5KEQ4F6UYO3P26ULGBQYHGQO7J4.gfortran-win_amd64.dll
C:\ProgramData\Miniconda3\envs\tf2\lib\site-packages\numpy\.libs\libopenblas.WCDJNK7YVMPZQ2ME2ZZHJJRJ3JIKNDB7.gfortran-win_amd64.dll
  warnings.warn("loaded more than 1 DLL from .libs:"

此函数根据请求(通过batch_size)参数创建任意数量的时间序列,每个序列的长度为n_steps,并且每个序列中每个时间步长只有一个值(即所有序列都是单变量的)。该函数返回一个形状为[批处理大小,时间步长,1]的NumPy数组,其中每个序列是两个固定振幅但频率和相位随机的正弦波的总和,再加上一点噪声。

在处理时间序列(以及其中类型的序列,例如句子)时,输入特征通常表示为形状为[批处理大小,时间步长,维度]的3D数组,其中单变量时间序列的维度为1,多变量时间序列的维度更多。

现在使用此函数创建训练集、验证集和测试集:

n_steps = 50
series = generate_time_series(10000, n_steps+1)  # 生成10000个时间步长为51的单变量时间序列
# 取前7000个时间步长为50作为输入,最后一个时间步长作为输出。通过前50个时间步长来预测后一个时间步长
X_train, y_train = series[:7000, :n_steps], series[:7000, -1]
X_valid, y_valid = series[7000:9000, :n_steps], series[7000:9000, -1]
X_test, y_test = series[9000:, :n_steps], series[9000:, -1]

X_train包含7000个时间序列(即形状为[7000, 50, 1]),而X_valid包含2000个(从第7000个时间序列到8999个),X_test包含1000个(从9000个时间序列到9999个)。由于要为每个序列预测一个值,因此目标是列向量(例如,y_train的形状为[7000, 1])。

基准指标

在开始使用RNN之前,通常最好有一些基准指标,否则可能会认为模型工作得很哈,但是实际上它的表现要比基本模型差。例如,最简单的方法是预测每个序列中的最后一个值。这被称为单纯预测,有时会令人惊讶地表现不好。在这种情况下,它们给了约0.020的均方误差:

y_pred = X_valid[:, -1]
np.mean(keras.losses.mean_squared_error(y_valid, y_pred))
0.020261222


另一种简单的方法是使用全连接的网络。由于它希望每个输入都有一个平的特征列表,因此需要添加一个Flatten层。只是使用一个简单的线性回归模型,是每个预测是时间序列中值的线性组合:

model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[50, 1]),
    keras.layers.Dense(1)
])
model.compile(optimizer='adam', loss='mse')
model.fit(X_train, y_train, epochs=20)
model.evaluate(X_test, y_test)
Epoch 1/20
219/219 [==============================] - 0s 1ms/step - loss: 0.2783
Epoch 2/20
219/219 [==============================] - 0s 977us/step - loss: 0.0724
Epoch 3/20
219/219 [==============================] - 0s 863us/step - loss: 0.0486
Epoch 4/20
219/219 [==============================] - 0s 1ms/step - loss: 0.0354
Epoch 5/20
219/219 [==============================] - 0s 867us/step - loss: 0.0275
Epoch 6/20
219/219 [==============================] - 0s 872us/step - loss: 0.0225
Epoch 7/20
219/219 [==============================] - 0s 849us/step - loss: 0.0192
Epoch 8/20
219/219 [==============================] - 0s 863us/step - loss: 0.0168
Epoch 9/20
219/219 [==============================] - 0s 918us/step - loss: 0.0147
Epoch 10/20
219/219 [==============================] - 0s 881us/step - loss: 0.0130
Epoch 11/20
219/219 [==============================] - 0s 890us/step - loss: 0.0114
Epoch 12/20
219/219 [==============================] - 0s 950us/step - loss: 0.0101
Epoch 13/20
219/219 [==============================] - 0s 872us/step - loss: 0.0090
Epoch 14/20
219/219 [==============================] - 0s 838us/step - loss: 0.0080
Epoch 15/20
219/219 [==============================] - 0s 844us/step - loss: 0.0072
Epoch 16/20
219/219 [==============================] - 0s 991us/step - loss: 0.0066
Epoch 17/20
219/219 [==============================] - 0s 867us/step - loss: 0.0060
Epoch 18/20
219/219 [==============================] - 0s 844us/step - loss: 0.0056
Epoch 19/20
219/219 [==============================] - 0s 876us/step - loss: 0.0053
Epoch 20/20
219/219 [==============================] - 0s 867us/step - loss: 0.0050
32/32 [==============================] - 0s 581us/step - loss: 0.0047





0.00474193412810564


如果使用MSE损失和默认的Adam优化器来编译此模型,然后在训练集上训练20个轮次并在验证集上评估,得出的MSE约为0.005,这比单纯预测的方法好得多

实现一个简单的RNN

model = keras.models.Sequential([
    keras.layers.SimpleRNN(1, input_shape=[None, 1])
])

它仅包含有一个有单个神经元的单层。不需要指定输入序列的长度(与之前的模型不同),因为循环神经网络可以处理任意数量的时间步长(这就是将第一个输入的维度设置为None的原因)。默认情况下,SimpleRNN层使用双曲正切激活函数。它的工作原理:初始状态$h_{(init)}$设置为0,并将其与第一个时间步长的值$x_{(0)}$一起传递给单个循环神经元。神经元计算这些值的加权总和,并将双曲正切激活函数应用于其结果,这得出了第一个输出$y_0$。在简单的RNN中,此输出也是新状态$h_0$。这个新的状态与西安一个输入值$x_{(1)}$一起传递给相同的循环神经网络,并重复该过程直到最后一个时间步长。然后,该层仅仅输出最后一个值$y_{49}$。对于每个时间序列,所有这些都是同时执行的。

默认情况下,Keras中的循环层仅返回最终输出。要使它们每个时间步长返回一个输出,必须设置return_sequences=True。

model.compile(optimizer='adam', loss='mse')
model.fit(X_train, y_train, epochs=20)
model.evaluate(X_test, y_test)
Epoch 1/20
219/219 [==============================] - 7s 29ms/step - loss: 0.0126
Epoch 2/20
219/219 [==============================] - 6s 30ms/step - loss: 0.0112
Epoch 3/20
219/219 [==============================] - 7s 31ms/step - loss: 0.0111
Epoch 4/20
219/219 [==============================] - 7s 32ms/step - loss: 0.0110
Epoch 5/20
219/219 [==============================] - 7s 31ms/step - loss: 0.0110
Epoch 6/20
219/219 [==============================] - 7s 31ms/step - loss: 0.0110
Epoch 7/20
219/219 [==============================] - 7s 32ms/step - loss: 0.0110
Epoch 8/20
219/219 [==============================] - 7s 30ms/step - loss: 0.0110
Epoch 9/20
219/219 [==============================] - 6s 28ms/step - loss: 0.0110
Epoch 10/20
219/219 [==============================] - 6s 29ms/step - loss: 0.0110
Epoch 11/20
219/219 [==============================] - 7s 30ms/step - loss: 0.0110
Epoch 12/20
219/219 [==============================] - 7s 31ms/step - loss: 0.0109
Epoch 13/20
219/219 [==============================] - 7s 31ms/step - loss: 0.0110
Epoch 14/20
219/219 [==============================] - 7s 32ms/step - loss: 0.0110
Epoch 15/20
219/219 [==============================] - 7s 31ms/step - loss: 0.0110
Epoch 16/20
219/219 [==============================] - 7s 32ms/step - loss: 0.0110
Epoch 17/20
219/219 [==============================] - 7s 32ms/step - loss: 0.0110
Epoch 18/20
219/219 [==============================] - 6s 29ms/step - loss: 0.0110
Epoch 19/20
219/219 [==============================] - 6s 29ms/step - loss: 0.0110
Epoch 20/20
219/219 [==============================] - 7s 31ms/step - loss: 0.0109
32/32 [==============================] - 0s 6ms/step - loss: 0.0109





0.010947210714221


MSE只有0.011,因此它比单纯方法好,但它还不能击败简单的线性模型。对于每个神经元,线性模型的每个输入和每个时间步长都有一个参数,加上一个偏置项。相反,对于简单RNN中的每个循环神经元,每个输入和每个隐藏状态维度只有一个参数(在简单RNN中,这只是该层中循环神经元的数量),加上一个偏置项。在这个简单的RNN中,总共只有三个参数。

趋势和季节性

还有许多其他模型可以预测时间序列,例如加权移动平均模型或自回归集成移动平均(ARIMA)模型。其中有些要求你首先删除趋势和季节性。例如,如果正在研究网站上的活动用户数,并且该数字每月以10%的速度增长,则需要从时间序列中消除这种趋势。一旦训练完模型并开始进行预测,你不得不将趋势添加回去来得到最终的预测。同样,如果试图预测每个月出售的防晒乳液的数量,可能会观察到强烈的季节性:因为它在夏季会销售得非常好,而且每年都会重复类似的模式。你必须从时间序列中删除此季节性因素,例如,通过计算每个时间步长的值与一年前的值的差(这种技术称为差分)。同样,在对模型进行训练并做出预测之后,将不得不重新添加季节性模式来得到最终的预测。

使用RNN时,通常不需要执行所有这些操作,但是在某些情况下可能会提高性能,因为该模型不需要学习趋势或季节性。

深度RNN

把多层单元堆叠起来是非常普遍的。这就可以实现深度RNN。

使用tf.keras实现深度RNN非常简单:只需要堆叠循环层。在此示例中,使用了三个SimpleRNN层(也可以添加任何其他类型的循环层,例如LSTM层或GRU层):

model = keras.models.Sequential([
    keras.layers.SimpleRNN(20, return_sequences=True, input_shape=[None, 1]),
    keras.layers.SimpleRNN(20, return_sequences=True),
    keras.layers.SimpleRNN(1)
])
model.compile(optimizer='adam', loss='mse')
model.fit(X_train, y_train, epochs=20)
model.evaluate(X_test, y_test)
Epoch 1/20
219/219 [==============================] - 23s 100ms/step - loss: 0.0234
Epoch 2/20
219/219 [==============================] - 23s 105ms/step - loss: 0.0041
Epoch 3/20
219/219 [==============================] - 24s 108ms/step - loss: 0.0036
Epoch 4/20
219/219 [==============================] - 23s 104ms/step - loss: 0.0034
Epoch 5/20
219/219 [==============================] - 24s 108ms/step - loss: 0.0033
Epoch 6/20
219/219 [==============================] - 24s 110ms/step - loss: 0.0033
Epoch 7/20
219/219 [==============================] - 23s 105ms/step - loss: 0.0032
Epoch 8/20
219/219 [==============================] - 22s 100ms/step - loss: 0.0031
Epoch 9/20
219/219 [==============================] - 24s 112ms/step - loss: 0.0031
Epoch 10/20
219/219 [==============================] - 24s 108ms/step - loss: 0.0030
Epoch 11/20
219/219 [==============================] - 23s 103ms/step - loss: 0.0030
Epoch 12/20
219/219 [==============================] - 24s 107ms/step - loss: 0.0030
Epoch 13/20
219/219 [==============================] - 24s 109ms/step - loss: 0.0030
Epoch 14/20
219/219 [==============================] - 23s 103ms/step - loss: 0.0030
Epoch 15/20
219/219 [==============================] - 23s 103ms/step - loss: 0.0029
Epoch 16/20
219/219 [==============================] - 25s 113ms/step - loss: 0.0029
Epoch 17/20
219/219 [==============================] - 23s 103ms/step - loss: 0.0028
Epoch 18/20
219/219 [==============================] - 23s 105ms/step - loss: 0.0028
Epoch 19/20
219/219 [==============================] - 24s 112ms/step - loss: 0.0029
Epoch 20/20
219/219 [==============================] - 23s 104ms/step - loss: 0.0029
32/32 [==============================] - 1s 20ms/step - loss: 0.0028





0.0028372975066304207


MSE为0.0028,终于设法击败了线性模型

最后一层不是理想的:它必须有一个单元,因此要预测一个单变量时间序列,这意味着每个时间步长必须有一个输出值。但是,只有一个单元意味着隐藏状态只是一个数字。

RNN主要使用其他循环层的隐藏状态来传递其所需的所有信息,而不会使用最终层的隐藏状态。此外,在默认情况下SimpleRNN层使用tanh激活函数,因此预测值必须在-1到1的范围内。但是,如果要使用其他激活函数,最好把输出层替换为Dense层:运行速度稍快,精度大致相同,并且可以选择所需的任何输出激活函数。如果做了更改,在最后一个循环层中删除return_sequences=True:

model = keras.models.Sequential([
    keras.layers.SimpleRNN(20, return_sequences=True, input_shape=[None, 1]),
    keras.layers.SimpleRNN(20),
    keras.layers.Dense(1)
])
model.compile(optimizer='adam', loss='mse')
model.fit(X_train, y_train, epochs=20)
model.evaluate(X_test, y_test)
Epoch 1/20
219/219 [==============================] - 14s 63ms/step - loss: 0.0340
Epoch 2/20
219/219 [==============================] - 15s 66ms/step - loss: 0.0041
Epoch 3/20
219/219 [==============================] - 16s 72ms/step - loss: 0.0033
Epoch 4/20
219/219 [==============================] - 15s 70ms/step - loss: 0.0032
Epoch 5/20
219/219 [==============================] - 14s 65ms/step - loss: 0.0031
Epoch 6/20
219/219 [==============================] - 15s 68ms/step - loss: 0.0031
Epoch 7/20
219/219 [==============================] - 15s 70ms/step - loss: 0.0031
Epoch 8/20
219/219 [==============================] - 15s 68ms/step - loss: 0.0031
Epoch 9/20
219/219 [==============================] - 15s 66ms/step - loss: 0.0030
Epoch 10/20
219/219 [==============================] - 15s 68ms/step - loss: 0.0030
Epoch 11/20
219/219 [==============================] - 16s 71ms/step - loss: 0.0030
Epoch 12/20
219/219 [==============================] - 16s 72ms/step - loss: 0.0029
Epoch 13/20
219/219 [==============================] - 15s 69ms/step - loss: 0.0030
Epoch 14/20
219/219 [==============================] - 15s 69ms/step - loss: 0.0030
Epoch 15/20
219/219 [==============================] - 14s 65ms/step - loss: 0.0029
Epoch 16/20
219/219 [==============================] - 14s 66ms/step - loss: 0.0029
Epoch 17/20
219/219 [==============================] - 14s 65ms/step - loss: 0.0028
Epoch 18/20
219/219 [==============================] - 15s 69ms/step - loss: 0.0028
Epoch 19/20
219/219 [==============================] - 15s 70ms/step - loss: 0.0029
Epoch 20/20
219/219 [==============================] - 15s 67ms/step - loss: 0.0028
32/32 [==============================] - 1s 14ms/step - loss: 0.0028





0.002804815536364913


输出层换成Dense后,收敛得更快并且性能也一样好。另外,可以根据需要更改输出激活函数。

预测未来几个时间步长

到目前为止,仅仅预测了下一个时间步长的值,但是可以很容易地通过适当地更改目标来预测未来几步的值(例如,要预测下十步,只需将目标更改为未来10步,而不是未来1步)。但是,如何预测接下来的10个值

第一种选择是使用已经训练好的模型,使其预测下一个值,然后将该值加到输入中(就像这个预测值实际上已经有了),然后再次使用该模型预测后面的值,以此类推,如以下代码所示:

series = generate_time_series(1, n_steps+10)
X_new, Y_new = series[:, :n_steps], series[:, n_steps:]
X = X_new
for step_ahead in range(10):
    y_pred_one = model.predict(X[:, step_ahead:])[:, np.newaxis, :]
    X = np.concatenate([X, y_pred_one], axis=1)
Y_pred = X[:, n_steps:]
np.mean(keras.losses.mean_squared_error(Y_new, Y_pred))
0.055745773


由于误差可能会累积,因此对下一个步长的预测通产会被对未来几个时间步长的预测更为准确。

第二种选择是训练RNN一次预测所有10个值,仍然可以使用一个序列到向量的模型,但是它输出10个值而不是1个值。但是,首先需要将目标更改为包含接下来10个值的向量:

series = generate_time_series(10000, n_steps+10)
X_train, Y_train = series[:7000, :n_steps], series[:7000, -10:,0]
X_valid, Y_valid = series[7000:9000, :n_steps], series[7000:9000, -10:,0]
X_test, Y_test = series[9000:, :n_steps], series[9000:, -10:,0]
model = keras.models.Sequential([
    keras.layers.SimpleRNN(20, return_sequences=True, input_shape=[None, 1]),
    keras.layers.SimpleRNN(20),
    keras.layers.Dense(10)
])
model.compile(optimizer='adam', loss='mse')
model.fit(X_train, Y_train, epochs=20, validation_data=(X_valid, Y_valid))
model.evaluate(X_test, Y_test)
Epoch 1/20
219/219 [==============================] - 16s 69ms/step - loss: 0.0700 - val_loss: 0.0371
Epoch 2/20
219/219 [==============================] - 17s 76ms/step - loss: 0.0272 - val_loss: 0.0201
Epoch 3/20
219/219 [==============================] - 15s 70ms/step - loss: 0.0168 - val_loss: 0.0138
Epoch 4/20
219/219 [==============================] - 15s 67ms/step - loss: 0.0133 - val_loss: 0.0114
Epoch 5/20
219/219 [==============================] - 16s 72ms/step - loss: 0.0118 - val_loss: 0.0103
Epoch 6/20
219/219 [==============================] - 16s 72ms/step - loss: 0.0109 - val_loss: 0.0115
Epoch 7/20
219/219 [==============================] - 16s 75ms/step - loss: 0.0105 - val_loss: 0.0110
Epoch 8/20
219/219 [==============================] - 16s 74ms/step - loss: 0.0101 - val_loss: 0.0099
Epoch 9/20
219/219 [==============================] - 14s 65ms/step - loss: 0.0101 - val_loss: 0.0094
Epoch 10/20
219/219 [==============================] - 15s 69ms/step - loss: 0.0096 - val_loss: 0.0092
Epoch 11/20
219/219 [==============================] - 16s 73ms/step - loss: 0.0095 - val_loss: 0.0086
Epoch 12/20
219/219 [==============================] - 17s 76ms/step - loss: 0.0094 - val_loss: 0.0089
Epoch 13/20
219/219 [==============================] - 15s 68ms/step - loss: 0.0095 - val_loss: 0.0091
Epoch 14/20
219/219 [==============================] - 15s 68ms/step - loss: 0.0090 - val_loss: 0.0092
Epoch 15/20
219/219 [==============================] - 16s 73ms/step - loss: 0.0090 - val_loss: 0.0085
Epoch 16/20
219/219 [==============================] - 16s 74ms/step - loss: 0.0089 - val_loss: 0.0097
Epoch 17/20
219/219 [==============================] - 16s 73ms/step - loss: 0.0091 - val_loss: 0.0099
Epoch 18/20
219/219 [==============================] - 15s 68ms/step - loss: 0.0090 - val_loss: 0.0111
Epoch 19/20
219/219 [==============================] - 16s 71ms/step - loss: 0.0088 - val_loss: 0.0089
Epoch 20/20
219/219 [==============================] - 16s 73ms/step - loss: 0.0089 - val_loss: 0.0081
32/32 [==============================] - 0s 15ms/step - loss: 0.0082





0.00820199865847826


该模型运行良好:接下里10个时间步长的MSE约为0.0082。这比线性模型好得多。但是仍然可以做得更好:实际上,与其训练模型在最后一个时间步长预测下一个10个值,不如训练模型在每个时间步长来预测下一个10个值。可以将这个序列到向量的RNN转换为序列到序列的RNN。这种技术的优势在于,损失将包含每个时间步长的RNN输出项,而不仅仅是最后一个时间步长的输出项。这意味着将有更多的误差梯度流过模型,它们不需要随时间而“流淌”。它们从每个时间步长的输出中流出。这会稳定和加速训练。

为了清楚期间,模型会在时间步长0时输出一个向量,其中包含时间步长1到10的预测,然后在时间步长1时模型会预测时间步长2到11,以此类推。因此,每个目标必须是与输入序列长度相同的序列,在每个步长都必须包含10维向量:

Y = np.empty((10000, n_steps, 10))
for step_ahead in range(1, 10+1):
    Y[:, :, step_ahead-1] = series[:, step_ahead:step_ahead +
                                   n_steps, 0]  # 创建时间序列,将预测目标设置为X后n_steps个时间步的数据作为序列中的一个数据,时间步与X相同但是是一个拥有10个数值的序列
Y_train = Y[:7000]
Y_valid = Y[7000:9000]
Y_test = Y[9000:]
print(series.shape)
print(X_train.shape)
print(Y_train.shape)
(10000, 60, 1)
(7000, 50, 1)
(7000, 50, 10)

目标中包含了在输入中出现的值X_train和Y_train之间有很多重叠。那不是作弊吗?幸运的是,一点也不是:在每个时间步长上,该模型仅知道过去的时间步长,因此不能向前看,这是一种因果模型。

要将模型转换为序列到序列的模型,必须在所有循环层(甚至最后一层)中设置return_sequences=True,必须在每个时间步长都应用输出Dense层。Keras为此提供了一个TimeDistributed层:它包装了任何层(例如Dense层),在输入序列的每个时间步长应用这个层。它通过重构输入的形状来有效地做到这一点,以便每个时间步长都被视为一个单独的实例(将输入的形状从[批量大小,时间不长,输入维度]调整为[批量大小X时间步长,输出维度]。在此示例中,输入维度为20,因为前一个SimpleRNN有20个单元),然后运行Dense层,最后将输出重构为序列(将输出从[批量大小X时间步长。输出维度]重构为[批量大小,时间步长,输出维度]。在此示例中,由于Dense层有10个单元,因此输出维度为10)。这是更新的模型:

model=keras.models.Sequential([
    keras.layers.SimpleRNN(20,return_sequences=True,input_shape=[None,1]),
    keras.layers.SimpleRNN(20,return_sequences=True),
    keras.layers.TimeDistributed(keras.layers.Dense(10))
])

Dense层实际上支持序列作为输入(甚至是更高维的输入):它像TimeDistributed(Dense(...))一样处理它们,这意味着它仅仅应用于最后一个输出维度(独立于所有的时间步长)。因此,可以使用Dense(10)代替最后一层。但是为了清楚起见,继续使用TimeDistributed(Dense(10)),因为它清楚地表明在每个时间步长都独立应用了Dense层,并且将模型输出一个序列,而不仅仅是一个向量。

训练期间需要所有的输出,但是只有最后一个时间步长的输出才对预测和评估有用。因此,尽管要依赖所有输出的MSE进行计算,但是需要使用自定义指标进行评估,以便在最后一个时间步长来计算输出的MSE:

def last_time_step_mse(Y_true, Y_pred):
    return keras.metrics.mean_squared_error(Y_true[:, -1], Y_pred[:, -1])


optimizer = keras.optimizers.Adam(lr=.01)
model.compile(loss='mse', optimizer=optimizer, metrics=[last_time_step_mse])
model.fit(X_train, Y_train, epochs=20, validation_data=(X_valid, Y_valid))
model.evaluate(X_test, Y_test)
C:\ProgramData\Miniconda3\envs\tf2\lib\site-packages\keras\optimizer_v2\optimizer_v2.py:355: UserWarning: The `lr` argument is deprecated, use `learning_rate` instead.
  warnings.warn(


Epoch 1/20
219/219 [==============================] - 17s 75ms/step - loss: 0.0469 - last_time_step_mse: 0.0365 - val_loss: 0.0343 - val_last_time_step_mse: 0.0202
Epoch 2/20
219/219 [==============================] - 16s 72ms/step - loss: 0.0331 - last_time_step_mse: 0.0200 - val_loss: 0.0311 - val_last_time_step_mse: 0.0194
Epoch 3/20
219/219 [==============================] - 15s 70ms/step - loss: 0.0288 - last_time_step_mse: 0.0156 - val_loss: 0.0264 - val_last_time_step_mse: 0.0120
Epoch 4/20
219/219 [==============================] - 16s 74ms/step - loss: 0.0260 - last_time_step_mse: 0.0129 - val_loss: 0.0247 - val_last_time_step_mse: 0.0107
Epoch 5/20
219/219 [==============================] - 16s 72ms/step - loss: 0.0239 - last_time_step_mse: 0.0113 - val_loss: 0.0237 - val_last_time_step_mse: 0.0127
Epoch 6/20
219/219 [==============================] - 15s 69ms/step - loss: 0.0221 - last_time_step_mse: 0.0097 - val_loss: 0.0209 - val_last_time_step_mse: 0.0084
Epoch 7/20
219/219 [==============================] - 15s 69ms/step - loss: 0.0210 - last_time_step_mse: 0.0087 - val_loss: 0.0216 - val_last_time_step_mse: 0.0079
Epoch 8/20
219/219 [==============================] - 16s 71ms/step - loss: 0.0205 - last_time_step_mse: 0.0085 - val_loss: 0.0196 - val_last_time_step_mse: 0.0078
Epoch 9/20
219/219 [==============================] - 16s 74ms/step - loss: 0.0200 - last_time_step_mse: 0.0079 - val_loss: 0.0214 - val_last_time_step_mse: 0.0103
Epoch 10/20
219/219 [==============================] - 15s 69ms/step - loss: 0.0201 - last_time_step_mse: 0.0083 - val_loss: 0.0209 - val_last_time_step_mse: 0.0088
Epoch 11/20
219/219 [==============================] - 15s 68ms/step - loss: 0.0195 - last_time_step_mse: 0.0076 - val_loss: 0.0191 - val_last_time_step_mse: 0.0069
Epoch 12/20
219/219 [==============================] - 16s 74ms/step - loss: 0.0193 - last_time_step_mse: 0.0074 - val_loss: 0.0205 - val_last_time_step_mse: 0.0080
Epoch 13/20
219/219 [==============================] - 15s 69ms/step - loss: 0.0191 - last_time_step_mse: 0.0074 - val_loss: 0.0206 - val_last_time_step_mse: 0.0087
Epoch 14/20
219/219 [==============================] - 16s 71ms/step - loss: 0.0203 - last_time_step_mse: 0.0090 - val_loss: 0.0199 - val_last_time_step_mse: 0.0095
Epoch 15/20
219/219 [==============================] - 16s 74ms/step - loss: 0.0190 - last_time_step_mse: 0.0073 - val_loss: 0.0190 - val_last_time_step_mse: 0.0068
Epoch 16/20
219/219 [==============================] - 15s 70ms/step - loss: 0.0192 - last_time_step_mse: 0.0076 - val_loss: 0.0191 - val_last_time_step_mse: 0.0070
Epoch 17/20
219/219 [==============================] - 16s 71ms/step - loss: 0.0294 - last_time_step_mse: 0.0209 - val_loss: 0.0383 - val_last_time_step_mse: 0.0335
Epoch 18/20
219/219 [==============================] - 16s 72ms/step - loss: 0.0347 - last_time_step_mse: 0.0283 - val_loss: 0.0269 - val_last_time_step_mse: 0.0177
Epoch 19/20
219/219 [==============================] - 16s 72ms/step - loss: 0.0258 - last_time_step_mse: 0.0158 - val_loss: 0.0232 - val_last_time_step_mse: 0.0127
Epoch 20/20
219/219 [==============================] - 16s 75ms/step - loss: 0.0225 - last_time_step_mse: 0.0118 - val_loss: 0.0210 - val_last_time_step_mse: 0.0102
32/32 [==============================] - 0s 15ms/step - loss: 0.0208 - last_time_step_mse: 0.0100





[0.020847154781222343, 0.010038050822913647]


得到的验证MSE约为,可以将此方法与第一个方法结合使用:使用这个RNN来预测接下来的10个值,然后将这些值合并到输入时间序列,然后再次使用该模型预测接下来的10个值。对于长期预测而言,它可能不太准确,但是如果目标是生成原始音乐或文本,则可能会很好。

预测时间序列是,将一些误差与预测一起使用通常很有用。为此,在每个记忆单元内添加一个MC Dropout层,删除部分输入和隐藏状态。训练后,为了预测新的时间序列,多次使用模型,并在每个时间步长计算预测的均值和标准差

简单的RNN可以很好的预测时间序列或处理其他类型的序列,但是在长序列上表现不佳。