精华内容
下载资源
问答
  • keras seq2seqDo you want to try some other methods to solve your forecasting problem rather than traditional regression? There are many neural network architectures, which are frequently applied in NL...

    keras seq2seq

    Do you want to try some other methods to solve your forecasting problem rather than traditional regression? There are many neural network architectures, which are frequently applied in NLP field, can be used for time series as well. In this article, we are going to build two Seq2Seq Models in Keras, the simple Seq2Seq LSTM Model, and the Seq2Seq LSTM Model with Luong Attention, and compare their forecasting accuracy.

    您是否要尝试其他方法来解决预测问题,而不是传统回归方法? 神经网络架构有很多,它们经常用于NLP领域,也可以用于时间序列。 在本文中,我们将在Keras中构建两个Seq2Seq模型,即简单的Seq2Seq LSTM模型和带有Luong Attention的Seq2Seq LSTM模型,并比较它们的预测准确性。

    创建一些数据 (Create Some Data)

    First of all, let’s create some time series data.

    首先,让我们创建一些时间序列数据。

    Image for post

    We’ve just created two sequences, x1 and x2, by combining sin waves, trend, and random noise. Next we will preprocess x1 and x2.

    通过组合正弦波趋势随机噪声 ,我们刚刚创建了两个序列x1x2 。 接下来,我们将预处理x1x2

    预处理 (Preprocess)

    1.将序列拆分为80%的训练集和20%的测试集 (1. Split sequences to 80% train set and 20% test set)

    Image for post

    Since the sequence length is n_ = 1000, the first 800 data points will be used as our train data, while the rest will be used as our test data.

    由于序列长度为n_ = 1000 ,因此前800个数据点将用作我们的火车数据,而其余的数据点将用作我们的测试数据。

    2.去趋势 (2. Detrending)

    It is not a must to detrend time series. However stationary time series will make model training much easier. There are many ways to detrend time series, such as taking difference of sequence with its lag1. Here for the simplicity, we assume the order of trend is known and we are just going to simply fit separate trend lines to x1 and x2, and then subtract the trend from the corresponding original sequence.

    趋势曲线不是必须趋势。 但是,固定时间序列将使模型训练更加容易。 有许多种方法可以使时间序列趋于趋势,例如将序列与其lag1的差取值。 在这里,为简单起见,我们假定趋势的顺序是已知的,而我们只是简单地将单独的趋势线拟合到x1x2 ,然后从相应的原始序列中减去趋势。

    We will create index number of each position in the sequence, for easier detrending and trend recover.

    我们将在序列中创建每个位置的索引号,以便于趋势下降和趋势恢复。

    Here we will use np.polyfit to complete this small task. Note that only the first 800 data points are used to fit the trend lines, this is because we want to avoid data leak.

    在这里,我们将使用np.polyfit完成此小任务。 请注意,只有前800个数据点用于拟合趋势线,这是因为我们要避免数据泄漏。

    Image for post

    Based on the above values we got, we can now come up with the trend lines for x1 and x2.

    基于上面得到的值,我们现在可以得出x1x2的趋势线。

    Let’s plot the trend lines together with x1 and x2, and check if they look good.

    让我们将趋势线与x1x2一起绘制,并检查它们是否看起来不错。

    Image for post

    The above result looks fine, now we can deduct the trend.

    以上结果看起来不错,现在我们可以推断趋势了。

    Image for post

    After removing the trend, x1 and x2 become stationary.

    消除趋势后,x1和x2变得平稳。

    3.合并序列 (3. Combine sequences)

    For easier preprocessing in next several steps, we can combine the sequences and their relevant information together into one array.

    为了在接下来的几个步骤中更轻松地进行预处理,我们可以将序列及其相关信息组合到一个数组中。

    Image for post

    In the combined array we created x_lbl:

    在组合数组中,我们创建了x_lbl

    • the first column is the detrended x1

      第一列是去趋势 x1

    • the second column is the detrended x2

      第二列是去趋势 x2

    • the third column is the index

      第三列是索引

    • the fourth column is the label (1 for train set and 0 for test set)

      第四列是标签 (火车组为1,测试组为0)

    4.归一化 (4. Normalize)

    Normalisation can help model avoid favouring large features while ignoring very small features. Here we can just simply normalise x1 and x2 by dividing the corresponding maximum values in train set.

    规范化可以帮助模型避免偏爱大型功能,而忽略非常小的功能。 在这里,我们可以简单地通过除以火车集合中的相应最大值来标准化x1x2

    Image for post

    Note that the above code only calculates maximum value of column 1 (detrended x1) and column 2 (detrended x2), the denominator of column 3 (index) and column 4 (label) are set to 1. This is because we do not input column 3 and column 4 into neural network, and hence no need to normalise them.

    注意,上面的代码仅计算第1列(去趋势 x1 )和第2列(去趋势 x2 )的最大值,第3列( 索引 )和第4列( 标签 )的分母设置为1。这是因为我们没有输入第3列和第4列进入神经网络,因此无需对其进行标准化。

    Image for post
    Image for post

    After normalisation, all the values are more or less within range from -1 to 1.

    归一化后,所有值或多或少都在-1到1的范围内。

    5.截断 (5. Truncate)

    Next, we will cut sequence into smaller pieces by sliding an input window (length = 200 time steps) and an output window (length = 20 time steps), and put these samples in 3d numpy arrays.

    接下来,我们将通过滑动一个输入窗口(长度= 200个时间步长)和一个输出窗口(长度= 20个时间步长)将序列切成更小的片段,并将这些样本放入3d numpy数组中。

    Image for post
    Image for post

    The function truncate generates 3 arrays:

    函数truncate生成3个数组:

    • input to neural network X_in: it contains 781 samples, length of each sample is 200 time steps, and each sample contains 3 features: detrended and normalised x1, detrended and normalised x2, and original assigned data position index. Only the first 2 features will be used for training.

      输入到神经网络X_in 它包含781个样本,每个样本的长度为200个时间步长,并且每个样本都包含3个特征:去趋势化和归一化 x1 ,去趋势化和归一化 x2以及原始分配的数据位置索引 。 仅前两个功能将用于培训。

    • target in neural network X_out: it contains 781 samples, length of each sample is 20 time steps, and each sample contains the same 3 features as in X_in. Only the first 2 features will be used as target, and the third feature will only be used to recover trend of the prediction.

      神经网络X_out中的目标: 它包含781个样本,每个样本的长度为20个时间步长,每个样本包含与X_in相同的3个特征。 仅前两个特征将用作目标,而第三个特征将仅用于恢复预测趋势。

    • label lbl: 1 for train set and 0 for test set.

      标签lbl :1代表火车,0代表测试。

    Image for post

    Now the data is ready to be input into neural network!

    现在可以将数据输入神经网络了!

    模型1:简单的Seq2Seq LSTM模型 (Model 1: Simple Seq2Seq LSTM Model)

    Image for post

    The above figure represents unfolded single layer of Seq2Seq LSTM model:

    上图表示Seq2Seq LSTM模型的未折叠单层:

    • The encoder LSTM cell: The value of each time step is input into the encoder LSTM cell together with previous cell state c and hidden state h, the process repeats until the last cell state c and hidden state h are generated.

      编码器LSTM单元 :将每个时间步的值与先前的单元状态c和隐藏状态h一起输入到编码器LSTM单元中,重复此过程,直到生成最后的单元状态c和隐藏状态h

    • The decoder LSTM cell: We use the last cell state c and hidden state h from the encoder as the initial states of the decoder LSTM cell. The last hidden state of encoder is also copied 20 times, and each copy is input into the decoder LSTM cell together with previous cell state c and hidden state h. The decoder outputs hidden state for all the 20 time steps, and these hidden states are connected to a dense layer to output the final result.

      解码器LSTM单元 :我们使用编码器的最后一个单元状态c和隐藏状态h作为解码器LSTM单元的初始状态。 编码器的最后一个隐藏状态也被复制了20次,每个副本与先前的单元状态c和隐藏状态h一起输入到解码器LSTM单元中。 解码器输出所有20个时间步长的隐藏状态,并将这些隐藏状态连接到密集层以输出最终结果。

    Set number of hidden layers:

    设置隐藏层数:

    The input layer

    输入层

    Image for post

    The encoder LSTM

    编码器LSTM

    We need to pay attention to 2 import parameters return_sequences and return_state, because they decide what LSTM returns.

    我们需要注意2个导入参数return_sequencesreturn_state ,因为它们决定了LSTM返回什么。

    • return_sequences=False, return_state=False: return the last hidden state: state_h

      return_sequences = False,return_state = False :返回最后一个隐藏状态:state_h

    • return_sequences=True, return_state=False: return stacked hidden states (num_timesteps * num_cells): one hidden state output for each input time step

      return_sequences = True,return_state = False :返回堆叠的隐藏状态( num_timesteps * num_cells ):每个输入时间步长输出一个隐藏状态

    • return_sequences=False, return_state=True: return 3 arrays: state_h, state_h, state_c

      return_sequences = False,return_state = True :返回3个数组:state_h,state_h,state_c

    • return_sequences=True, return_state=True: return 3 arrays: stacked hidden states, last state_h, last state_c

      return_sequences = True,return_state = True :返回3个数组:堆叠的隐藏状态,最后一个state_h,最后一个state_c

    For simple Seq2Seq model, we only need last state_h and last state_c.

    对于简单的Seq2Seq模型,我们只需要last state_h和last state_c。

    Image for post

    Batch normalisation is added because we want to avoid gradient explosion caused by the activation function ELU in the encoder.

    添加批归一化是因为我们要避免编码器中激活函数ELU引起的梯度爆炸。

    Next, we make 20 copies of the last hidden state of encoder and use them as input to the decoder. The last cell state and the last hidden state of the encoder are also used as the initial states of decoder.

    接下来,我们复制编码器的最后一个隐藏状态的20个副本,并将其用作解码器的输入。 编码器的最后一个单元状态和最后一个隐藏状态也用作解码器的初始状态。

    Image for post
    Image for post

    Then we put everything into the model, and compile it. Here we simply use MSE as the loss function and MAE as the evaluation metric. Note that we set clipnorm=1 for Adam optimiser. This is to normalise the gradient, so as to avoid gradient explosion during back propagation.

    然后,我们将所有内容放入模型中并进行编译。 在这里,我们仅将MSE用作损失函数,将MAE用作评估指标。 请注意,我们为Adam optimiser设置了clipnorm = 1 。 这是为了规范化梯度,以避免在反向传播过程中梯度爆炸。

    Image for post

    We can also plot the model:

    我们还可以绘制模型:

    Image for post

    The next step is training:

    下一步是培训:

    Image for post
    Image for post

    预测 (Prediction)

    The model prediction as well as the true values are unnormalised:

    模型预测以及真实值未归一化:

    Image for post

    Then we combine the unnormalised outputs with their corresponding index, so that we can recover the trend.

    然后,我们将未标准化的输出与其相应的索引结合起来,以便我们可以恢复趋势。

    Image for post

    Next, we put all the outputs with recovered trend into a dictionary data_final.

    接下来,我们将所有具有恢复趋势的输出放入字典data_final中

    Image for post

    Just a quick check to see if the prediction value distribution is reasonable:

    只需快速检查一下预测值分布是否合理:

    Image for post
    Image for post

    The data distribution of prediction and true values are almost overlapped, so we are good.

    预测值和真实值的数据分布几乎重叠,所以我们很好。

    We can also plot MAE of all samples in time order, to see if there is clear pattern. The ideal situation is when line is random, otherwise it may indicate that the model is not sufficiently trained.

    我们还可以按时间顺序绘制所有样本的MAE,以查看是否有清晰的图案。 理想的情况是线是随机的,否则可能表明模型未得到充分训练。

    Image for post
    Image for post

    Based on the above plots, we can say that there are still certain periodical pattens in both train and test MAE. Training for more epochs may lead to better results.

    根据以上图,我们可以说在训练和测试MAE中仍然有一定的周期性模式。 训练更多的时代可能会导致更好的结果。

    Next we are going to check some random samples and see if the predicted lines and corresponding true lines are aligned.

    接下来,我们将检查一些随机样本,看看预测线和相应的真实线是否对齐。

    Image for post
    Image for post

    We can also check the nth prediction of each time step:

    我们还可以检查每个时间步长的第n个预测:

    Image for post

    Take a closer look at the prediction on test set:

    仔细查看测试集的预测:

    Image for post

    模型2:具有Luong注意的Seq2Seq LSTM模型 (Model 2: Seq2Seq LSTM Model with Luong Attention)

    Image for post
    https://blog.floydhub.com/attention-mechanism/ https://blog.floydhub.com/attention-mechanism/

    One of the limitations of simple Seq2Seq model is: only the last state of encoder RNN is used as input to decoder RNN. If the sequence is very long, the encoder will tend to have much weaker memory about earlier time steps. Attention mechanism can solve this problem. An attention layer is going to assign proper weight to each hidden state output from encoder, and map them to output sequence.

    简单Seq2Seq模型的局限性之一是:仅将编码器RNN的最后状态用作解码器RNN的输入。 如果序列很长,则编码器将在较早的时间步长上具有较弱的内存。 注意机制可以解决此问题。 注意层将为编码器输出的每个隐藏状态分配适当的权重,并将其映射到输出序列。

    Next we will build Luong Attention on top of Model 1, and use Dot method to calculate alignment score.

    接下来,我们将在模型1的顶部构建Luong Attention ,并使用Dot方法计算对齐分数。

    The Input layer

    输入

    It is the same as in Model 1:

    模型1相同:

    The encoder LSTM

    编码器LSTM

    This is slightly different from Model 1: besides returning the last hidden state and the last cell state, we also need to return the stacked hidden states for alignment score calculation.

    这与模型1稍有不同:除了返回最后一个隐藏状态和最后一个单元格状态,我们还需要返回堆叠的隐藏状态以计算比对分数

    Image for post

    Next, we apply batch normalisation to avoid gradient explosion.

    接下来,我们应用批量归一化以避免梯度爆炸。

    The Decoder LSTM

    解码器LSTM

    Next, we repeat the last hidden state of encoder 20 times, and use them as input to decoder LSTM.

    接下来,我们重复编码器的最后一个隐藏状态20次,并将其用作解码器LSTM的输入。

    Image for post

    We also need to get the stacked hidden state of de decoder for alignment score calculation.

    我们还需要获取解码器的堆叠隐藏状态以进行对齐分数计算。

    Image for post

    Attention Layer

    注意层

    To build the attention layer, the first thing to do is to calculate the alignment score, and apply softmax activation function over it:

    要构建关注层,首先要做的是计算对齐分数,并在其上应用softmax激活函数:

    Image for post

    Then we can calculate the context vector, and also apply batch normalisation on top of it:

    然后,我们可以计算上下文向量,并在其之上应用批处理规范化:

    Image for post

    Now we concat the context vector and stacked hidden states of decoder, and use it as input to the last dense layer.

    现在,我们结合上下文向量和解码器的堆叠隐藏状态,并将其用作最后一个密集层的输入。

    Image for post
    Image for post

    Then we can compile the model. The parameters are the same as those in Model 1, for the sake of comparing the performance of the 2 models.

    然后我们可以编译模型。 为了比较两个模型的性能,参数与模型1中的参数相同。

    Image for post
    Image for post

    How data flow through the model:

    数据如何流经模型:

    Image for post

    The training and evaluation process is the same as illustrated in Model 1. After training for 100 epochs (the same number of training epochs as Model 1), we can evaluate the result.

    培训和评估过程与模型1中所示的过程相同。 在训练了100个纪元后(与模型1相同的训练纪元数),我们可以评估结果。

    Image for post

    Below are the plots of sample MAE vs. sample order for train set and test set. Again, the model is not sufficiently trained since we can still see some periodical pattern. But for easier comparison of the 2 models, we are not going to train it more for now. Note that the overall MAE of both train set and test set are slightly improved compare to Model 1.

    以下是火车集和测试集的样本MAE与样本顺序 。 同样,由于我们仍然可以看到一些周期性的模式,因此模型的训练不足。 但是为了更轻松地比较这两种模型,我们暂时不对其进行更多的培训。 请注意,与模型1相比,训练集和测试集的总体MAE都有所改善。

    After adding the attention layer:

    添加关注层后:

    • MAE of train set decrease from 5.7851 to 5.7381

      火车的MAE从5.7851降低到5.7381
    • MAE of test set decrease from 6.1495 to 5.9392

      测试集的MAE从6.1495降低到5.9392
    Image for post
    Image for post

    Random sampling to check predicted line vs. true line:

    随机抽样以检查预测线与真实线:

    Image for post
    Image for post
    Image for post
    Image for post

    翻译自: https://levelup.gitconnected.com/building-seq2seq-lstm-with-luong-attention-in-keras-for-time-series-forecasting-1ee00958decb

    keras seq2seq

    展开全文
  • 时间序列深度学习:seq2seq 模型预测太阳黑子 学习路线 商业中的时间序列深度学习 商业中应用时间序列深度学习 深度学习时间序列预测:使用 keras 预测太阳黑子 递归神经网络 设置、预处理与探索 所用的.....

    时间序列深度学习:seq2seq 模型预测太阳黑子

    本文翻译自《Time Series Deep Learning, Part 2: Predicting Sunspot Frequency With Keras Lstm in R》,略有删减

    原文链接

    深度学习于商业的用途之一是提高时间序列预测的准确性。之前的教程显示了如何利用自相关性预测未来 10 年的月度太阳黑子数量。本教程将借助 RStudio 重新审视太阳黑子数据集,并且使用到 TensorFlow for R 中一些高级的深度学习功能,展示基于 keras 的深度学习教程遇到 tfruns(用于追踪、可视化和管理 TensorFlow 训练、实验的一整套工具)后产生出的有趣结果。

    学习路线

    深度学习教程将教会你:

    • 时间序列深度学习如何应用于商业
    • 深度学习预测太阳黑子
    • 如何建立 LSTM 模型
    • 如何回测 LSTM 模型

    事实上,最酷的事之一是你能画出 LSTM 预测的回测结果。

    232518-20180808234733752-1946083790.png

    商业中的时间序列深度学习

    时间序列预测是商业中实现投资回报率(ROI)的一个关键领域。想一想:预测准确度提高 10% 就可以为机构节省数百万美元。这怎么可能?下面让我们来看看。

    我们将以 NVIDIA 为例,一家为 Artificial IntelligenceDeep Learning 生产最先进芯片的半导体厂商。NVIDIA 生产的图形处理器或 GPU,这对于高性能深度学习所要求的大规模数值计算来说是必需的。芯片看起来像这样。

    232518-20180808234752808-1738570648.png

    与所有制造商一样,NVIDIA 需要预测其产品的需求。为什么? 因为他们据此可以为客户提供合适数量的芯片。这个预测很关键,需要很多技巧和一些运气才能做到这一点。

    我们所讨论的是销售预测,它推动了 NVIDIA 做出的所有生产制造决策。这包括购买多少原材料,有多少人来制造芯片,以及需要多少预算用于加工和装配操作。销售预测中的错误越多,NVIDIA 产生的成本就越大,因为所有这些活动(供应链、库存管理、财务规划等)都会变得没有意义!

    商业中应用时间序列深度学习

    时间序列深度学习对于预测具有高自相关性的数据非常准确,因为包括 LSTMGRU 在内的算法可以从序列中学习信息,无论模式何时发生。这些特殊的 RNN 旨在具有长期记忆性,这意味着它们善于在最近发生的观察和很久之前发生的观察之间学习模式。这使它们非常适合时间序列!但它们对销售数据有用吗?也许,来!我们讨论一下。

    销售数据混合了各种特征,但通常有季节性模式趋势趋势可以是平坦的、线性的、指数的等等。这通常不是 LSTM 擅长的地方,但其他传统的预测方法可以检测趋势。但是,季节性不同。销售数据中的季节性是一种可以在多个频率(年度、季度、月度、周度甚至每天)上出现的模式。LSTM 非常适合检测季节性,因为它通常具有自相关性。因此,LSTM 和 GRU 可以很好地帮助改进季节性检测,从而减少销售预测中的整体预测误差。

    深度学习时间序列预测:使用 keras 预测太阳黑子

    这一节我们将借助基本的 R 工具在太阳黑子数据集上做时间序列预测。太阳黑子是太阳表面的低温区域,这里有一张来自 NASA 的太阳黑子图片。

    232518-20180808235422130-2116645799.jpg

    我们使用月度数据集 sunspots.month(也有一个年度频率的版本),它包括 265 年间(1749 - 2013)的月度太阳黑子观测。

    232518-20180808234816677-1708636327.png

    预测该数据集具有相当的挑战性,因为短期内的高变异性以及长期内明显的不规则周期性。例如,低频周期达到的最大幅度差异很大,达到最大低频周期高度所需的高频周期步数也是如此。(译注:数据中的局部高点之间间隔大约为 11 年)

    我们的文章将重点关注两个主要方面:如何将深度学习应用于时间序列预测,以及如何在该领域中正确应用交叉验证。对于后者,我们将使用 rsample 包来允许对时间序列数据进行重新抽样。对于前者,我们的目标不是达到最佳表现,而是显示使用递归神经网络对此类数据进行建模时的一般操作过程。

    递归神经网络

    当我们的数据具有序列结构时,我们就用递归神经网络(RNN)进行建模。

    目前为止,在 RNN 中,建立的最佳架构是 GRU(门递归单元)和 LSTM(长短期记忆网络)。今天,我们不要放大它们自身独特的东西,而是集中于它们与最精简的 RNN 的共同点上:基本的递归结构。

    与通常称为多层感知器(MLP)的神经网络的原型相比,RNN 具有随时间推移的状态。来自 Goodfellow 等人的著作——“深度学习的圣经”,从这个图中可以很好地看出这一点:

    232518-20180808234829426-1800548337.png

    每次,状态是当前输入和先前隐含状态的组合。这让人联想到自回归模型,但是对于神经网络,我们必须在某种程度上停止依赖。

    因为那是为了确定权重,我们会不断计算输入变化后我们的损失如何变化。现在,如果我们必须考虑的输入在任意时间步无限地返回,那么我们将无法计算所有这些梯度。然而,在实践中,我们的隐含状态将在每次迭代中通过固定数量的步骤继续前进。

    一旦我们加载并预处理数据,我们就会回过头来。

    设置、预处理与探索

    所用的包

    这里是该教程所涉及到的包。

    # Core Tidyverse
    library(tidyverse)
    library(glue)
    library(forcats)
    
    # Time Series
    library(timetk)
    library(tidyquant)
    library(tibbletime)
    
    # Visualization
    library(cowplot)
    
    # Preprocessing
    library(recipes)
    
    # Sampling / Accuracy
    library(rsample)
    library(yardstick)
    
    # Modeling
    library(keras)
    library(tfruns)

    如果你之前没有在 R 中运行过 Keras,你需要用 install_keras() 函数安装 Keras。

    # Install Keras if you have not installed before
    install_keras()

    数据

    数据集 sunspot.month 是一个 ts 类对象(非 tidy 类),所以我们将使用 timetk 中的 tk_tbl() 函数转换为 tidy 数据集。我们使用这个函数而不是来自 tibbleas.tibble(),用来自动将时间序列索引保存为zoo yearmon 索引。最后,我们将使用 lubridate::as_date()(使用 tidyquant 时加载)将 zoo 索引转换为日期,然后转换为 tbl_time 对象以使时间序列操作起来更容易。

    sun_spots <- datasets::sunspot.month %>%
        tk_tbl() %>%
        mutate(index = as_date(index)) %>%
        as_tbl_time(index = index)
    
    sun_spots
    # A time tibble: 3,177 x 2
    # Index: index
       index      value
       <date>     <dbl>
     1 1749-01-01  58  
     2 1749-02-01  62.6
     3 1749-03-01  70  
     4 1749-04-01  55.7
     5 1749-05-01  85  
     6 1749-06-01  83.5
     7 1749-07-01  94.8
     8 1749-08-01  66.3
     9 1749-09-01  75.9
    10 1749-10-01  75.5
    # ... with 3,167 more rows

    探索性数据分析

    时间序列很长(有 265 年!)。我们可以将时间序列的全部(265 年)以及前 50 年的数据可视化,以获得该时间系列的直观感受。

    使用 cowplot 可视化太阳黑子数据

    我们将创建若干 ggplot 对象并借助 cowplot::plot_grid() 把这些对象组合起来。对于需要缩放的部分,我们使用 tibbletime::time_filter(),可以方便的实现基于时间的过滤。

    p1 <- sun_spots %>%
        ggplot(aes(index, value)) +
        geom_point(
            color = palette_light()[[1]],
            alpha = 0.5) +
        theme_tq() +
        labs(
            title = "From 1749 to 2013 (Full Data Set)")
    
    p2 <- sun_spots %>%
        filter_time("start" ~ "1800") %>%
        ggplot(aes(index, value)) +
        geom_line(
            color = palette_light()[[1]],
            alpha = 0.5) +
        geom_point(color = palette_light()[[1]]) +
        geom_smooth(
            method = "loess",
            span = 0.2, se = FALSE) +
        theme_tq() +
        labs(
            title = "1749 to 1759 (Zoomed In To Show Changes over the Year)",
            caption = "datasets::sunspot.month")
    
    p_title <- ggdraw() +
        draw_label(
            "Sunspots", size = 18,
            fontface = "bold",
            colour = palette_light()[[1]])
    
    plot_grid(
        p_title, p1, p2,
        ncol = 1, rel_heights = c(0.1, 1, 1))

    232518-20180808234908795-507566696.png

    回测:时间序列交叉验证

    在序列数据上执行交叉验证时,必须保留对以前时间样本的时间依赖性。我们可以通过平移窗口的方式选择连续子样本,进而创建交叉验证抽样计划。实质上,由于没有未来测试数据,我们需要创造若干人造“未来”数据来解决这个问题,在金融领域这通常被称为“回测”。

    之前的教程提到过,rsample包含回测功能。“Time Series Analysis Example”描述了一个使用 rolling_origin() 函数为时间序列交叉验证创建样本的过程。我们将使用这种方法。

    开发一个回测策略

    我们创建的抽样计划使用 100 年(initial = 12 x 100)的数据作为训练集,50 年(assess = 12 x 50)的数据用于测试集。我们选择大约 22 年的跳跃跨度(skip = 12 x 22 - 1),将样本均匀分布到 6 组中,跨越整个 265 年的太阳黑子历史。最后,我们选择 cumulative = FALSE 来允许平移起始点,这确保了较近期数据上的模型相较那些不太新近的数据没有不公平的优势(使用更多的观测数据)。rolling_origin_resamples 是一个 tibble 型的返回值。

    periods_train <- 12 * 100
    periods_test  <- 12 * 50
    skip_span     <- 12 * 22 - 1
    
    rolling_origin_resamples <- rolling_origin(
        sun_spots,
        initial    = periods_train,
        assess     = periods_test,
        cumulative = FALSE,
        skip       = skip_span)
    
    rolling_origin_resamples
    # Rolling origin forecast resampling
    # A tibble: 6 x 2
      splits       id    
      <list>       <chr>
    1 <S3: rsplit> Slice1
    2 <S3: rsplit> Slice2
    3 <S3: rsplit> Slice3
    4 <S3: rsplit> Slice4
    5 <S3: rsplit> Slice5
    6 <S3: rsplit> Slice6

    可视化回测策略

    我们可以用两个自定义函数来可视化再抽样。首先是 plot_split(),使用 ggplot2 绘制一个再抽样分割图。请注意,expand_y_axis 参数默认将日期范围扩展成整个 sun_spots 数据集的日期范围。当我们将所有的图形同时可视化时,这将变得有用。

    # Plotting function for a single split
    plot_split <- function(split,
                           expand_y_axis = TRUE,
                           alpha = 1,
                           size = 1,
                           base_size = 14)
    {
    
        # Manipulate data
        train_tbl <- training(split) %>%
            add_column(key = "training")
    
        test_tbl  <- testing(split) %>%
            add_column(key = "testing")
    
        data_manipulated <- bind_rows(
            train_tbl, test_tbl) %>%
            as_tbl_time(index = index) %>%
            mutate(
                key = fct_relevel(
                    key, "training", "testing"))
    
        # Collect attributes
        train_time_summary <- train_tbl %>%
            tk_index() %>%
            tk_get_timeseries_summary()
    
        test_time_summary <- test_tbl %>%
            tk_index() %>%
            tk_get_timeseries_summary()
    
        # Visualize
        g <- data_manipulated %>%
            ggplot(
                aes(x = index, y = value, color = key)) +
            geom_line(size = size, alpha = alpha) +
            theme_tq(base_size = base_size) +
            scale_color_tq() +
            labs(
                title    = glue("Split: {split$id}"),
                subtitle = glue(
                    "{train_time_summary$start} to {test_time_summary$end}"),
                y = "",
                x = "") +
            theme(legend.position = "none")
    
        if (expand_y_axis)
        {
    
            sun_spots_time_summary <- sun_spots %>%
                tk_index() %>%
                tk_get_timeseries_summary()
    
            g <- g +
                scale_x_date(
                    limits = c(
                        sun_spots_time_summary$start,
                        sun_spots_time_summary$end))
        }
    
        return(g)
    }

    plot_split() 函数接受一个分割(在本例中为 Slice01),并可视化抽样策略。我们使用 expand_y_axis = TRUE 将横坐标范围扩展到整个数据集的日期范围。

    rolling_origin_resamples$splits[[1]] %>%
        plot_split(expand_y_axis = TRUE) +
        theme(legend.position = "bottom")

    232518-20180808234922317-120584745.png

    第二个函数是 plot_sampling_plan(),使用 purrrcowplotplot_split() 函数应用到所有样本上。

    # Plotting function that scales to all splits
    plot_sampling_plan <- function(sampling_tbl,
                                   expand_y_axis = TRUE,
                                   ncol = 3,
                                   alpha = 1,
                                   size = 1,
                                   base_size = 14,
                                   title = "Sampling Plan")
    {
        # Map plot_split() to sampling_tbl
        sampling_tbl_with_plots <- sampling_tbl %>%
            mutate(
                gg_plots = map(
                    splits, plot_split,
                    expand_y_axis = expand_y_axis,
                    alpha = alpha, base_size = base_size))
    
        # Make plots with cowplot
        plot_list <- sampling_tbl_with_plots$gg_plots
    
        p_temp <- plot_list[[1]] +
            theme(legend.position = "bottom")
        legend <- get_legend(p_temp)
    
        p_body <- plot_grid(
            plotlist = plot_list, ncol = ncol)
    
        p_title <- ggdraw() +
            draw_label(
                title, size = 14,
                fontface = "bold",
                colour = palette_light()[[1]])
    
        g <- plot_grid(
            p_title, p_body,
            legend, ncol = 1,
            rel_heights = c(0.05, 1, 0.05))
    
        return(g)
    }

    现在我们可以使用 plot_sampling_plan() 可视化整个回测策略!我们可以看到抽样计划如何平移抽样窗口逐渐切分出训练和测试子样本。

    rolling_origin_resamples %>%
        plot_sampling_plan(
            expand_y_axis = T, ncol = 3,
            alpha = 1, size = 1, base_size = 10,
            title = "Backtesting Strategy: Rolling Origin Sampling Plan")

    232518-20180808234936560-127915781.png

    此外,我们可以让 expand_y_axis = FALSE,对每个样本进行缩放。

    rolling_origin_resamples %>%
        plot_sampling_plan(
            expand_y_axis = F, ncol = 3,
            alpha = 1, size = 1, base_size = 10,
            title = "Backtesting Strategy: Zoomed In")

    232518-20180808234948775-1260551024.png

    当在太阳黑子数据集上测试 LSTM 模型准确性时,我们将使用这种回测策略(来自一个时间序列的 6 个样本,每个时间序列分为 100/50 两部分,并且样本之间有大约 22 年的偏移)。

    LSTM 模型

    首先,我们将在回测策略的某个样本上用 Keras 开发一个状态 LSTM 模型,通常是最近的一个。然后,我们将模型套用到所有样本,以测试和验证模型性能。

    example_split    <- rolling_origin_resamples$splits[[6]]
    example_split_id <- rolling_origin_resamples$id[[6]]

    我么可以用 plot_split() 函数可视化该分割,设定 expand_y_axis = FALSE 以便将横坐标缩放到样本本身的范围。

    plot_split(
        example_split,
        expand_y_axis = FALSE,
        size = 0.5) +
        theme(legend.position = "bottom") +
        ggtitle(glue("Split: {example_split_id}"))

    232518-20180808235007964-267667013.png

    数据准备

    为了帮助进行超参数调整,除了训练集之外,我们还需要一个验证集。例如,我们将使用一个回调函数 callback_early_stopping,当验证集上没有显着的表现时,它会停止训练(多少算显著由你决定)。

    我们将分析集的三分之二用于训练,三分之一用于验证。

    df_trn <- analysis(example_split)[1:800, , drop = FALSE]
    df_val <- analysis(example_split)[801:1200, , drop = FALSE]
    df_tst <- assessment(example_split)

    首先,我们将训练和测试数据集合成一个数据集,并使用列 key 来标记它们来自哪个集合(trainingtesting)。请注意,tbl_time 对象需要在调用 bind_rows() 时重新指定索引,但是这个问题应该很快在 dplyr 包中得到纠正。

    df <- bind_rows(
        df_trn %>% add_column(key = "training"),
        df_val %>% add_column(key = "validation"),
        df_tst %>% add_column(key = "testing")) %>%
        as_tbl_time(index = index)
    
    df
    # A time tibble: 1,800 x 3
    # Index: index
       index      value key     
       <date>     <dbl> <chr>   
     1 1849-06-01  81.1 training
     2 1849-07-01  78   training
     3 1849-08-01  67.7 training
     4 1849-09-01  93.7 training
     5 1849-10-01  71.5 training
     6 1849-11-01  99   training
     7 1849-12-01  97   training
     8 1850-01-01  78   training
     9 1850-02-01  89.4 training
    10 1850-03-01  82.6 training
    # ... with 1,790 more rows

    recipe 做数据预处理

    LSTM 算法通常要求输入数据经过中心化并标度化。我们可以使用 recipe 包预处理数据。我们用 step_sqrt 来转换数据以减少异常值的影响,再结合 step_centerstep_scale 对数据进行中心化和标度化。最后,数据使用 bake() 函数实现处理转换。

    rec_obj <- recipe(
        value ~ ., df) %>%
        step_sqrt(value) %>%
        step_center(value) %>%
        step_scale(value) %>%
        prep()
    
    df_processed_tbl <- bake(rec_obj, df)
    
    df_processed_tbl
    # A tibble: 1,800 x 3
       index      value key     
       <date>     <dbl> <fct>   
     1 1849-06-01 0.714 training
     2 1849-07-01 0.660 training
     3 1849-08-01 0.473 training
     4 1849-09-01 0.922 training
     5 1849-10-01 0.544 training
     6 1849-11-01 1.01  training
     7 1849-12-01 0.974 training
     8 1850-01-01 0.660 training
     9 1850-02-01 0.852 training
    10 1850-03-01 0.739 training
    # ... with 1,790 more rows

    接着,记录中心化和标度化的信息,以便在建模完成之后可以将数据逆向转换回去。平方根转换可以通过乘方运算逆转回去,但要在逆转中心化和标度化之后。

    center_history <- rec_obj$steps[[2]]$means["value"]
    scale_history  <- rec_obj$steps[[3]]$sds["value"]
    
    c("center" = center_history, "scale" = scale_history)
    center.value  scale.value
        6.694468     3.238935

    调整数据形状

    Keras LSTM 希望输入和目标数据具有特定的形状。输入必须是 3 维数组,维度大小为 num_samplesnum_timestepsnum_features

    这里,num_samples 是集合中观测的数量。这将以每份 batch_size 大小的分量分批提供给模型。第二个维度 num_timesteps 是我们上面讨论的隐含状态的长度。最后,第三个维度是我们正在使用的预测变量的数量。对于单变量时间序列,这是 1。

    隐含状态的长度应该选择多少?这通常取决于数据集和我们的目标。如果我们提前一步预测,即仅预测下个月,我们主要关注的是选择一个状态长度,以便学习数据中存在的任何模式。

    现在说我们想要预测 12 个月的数据,就像 SILSO(世界数据中心,用于生产、保存和传播国际太阳黑子观测)所做的。我们通过 Keras 实现这一目标的方法是使 LSTM 隐藏状态与后续输出有相同的长度。因此,如果我们想要产生 12 个月的预测,我们的 LSTM 隐藏状态长度应该是 12。(译注:原始数据的周期大约是 10 到 11 年,使用相邻两年的数据不能涵盖一个完整的周期,致使模型无法学习到和周期有关的模式,最终表现可能不佳。)

    然后使用 time_distributed() 包装器将这 12 个时间步连接到 12 个线性预测器单元。该包装器的任务是将相同的计算(即,相同的权重矩阵)应用于它接收的每个状态输入。

    目标数组的格式应该是什么?由于我们在这里预测了几个时间步,目标数据也需要是 3 维的。维度 1 同样是批量维度,维度 2 同样对应于时间步(被预测的),维度 3 是包装层的大小。在我们的例子中,包装层是单个单元的 layer_dense(),因为我们希望每个时间点只有一个预测。

    所以,让我们调整数据形状。这里的主要动作是用滑动窗口创建长度 12 的输入,后续 12 个观测作为输出。举个更简单的例子,假设我们的输入是从 1 到 10 的数字,我们选择的序列长度(状态大小)是 4,下面就是我们希望我们的训练输入看起来的样子:

    1,2,3,4
    2,3,4,5
    3,4,5,6

    相应的目标数据:

    5,6,7,8
    6,7,8,9
    7,8,9,10

    我们将定义一个简短的函数,对给定的数据集进行调整。最后,我们形式上添加了需要的第三个维度(即使在我们的例子中该维度的大小为 1)。

    # these variables are being defined just because of the order in which
    # we present things in this post (first the data, then the model)
    # they will be superseded by FLAGS$n_timesteps, FLAGS$batch_size and n_predictions
    # in the following snippet
    n_timesteps <- 12
    n_predictions <- n_timesteps
    batch_size <- 10
    
    # functions used
    build_matrix <- function(tseries,
                             overall_timesteps)
    {
        t(sapply(
            1:(length(tseries) - overall_timesteps + 1),
            function(x) tseries[x:(x + overall_timesteps - 1)]))
    }
    
    reshape_X_3d <- function(X)
    {
        dim(X) <- c(dim(X)[1], dim(X)[2], 1)
        X
    }
    
    # extract values from data frame
    train_vals <- df_processed_tbl %>%
        filter(key == "training") %>%
        select(value) %>%
        pull()
    valid_vals <- df_processed_tbl %>%
        filter(key == "validation") %>%
        select(value) %>%
        pull()
    test_vals <- df_processed_tbl %>%
        filter(key == "testing") %>%
        select(value) %>%
        pull()
    
    # build the windowed matrices
    train_matrix <- build_matrix(
        train_vals, n_timesteps + n_predictions)
    valid_matrix <- build_matrix(
        valid_vals, n_timesteps + n_predictions)
    test_matrix <- build_matrix(
        test_vals, n_timesteps + n_predictions)
    
    # separate matrices into training and testing parts
    # also, discard last batch if there are fewer than batch_size samples
    # (a purely technical requirement)
    X_train <- train_matrix[, 1:n_timesteps]
    y_train <- train_matrix[, (n_timesteps + 1):(n_timesteps * 2)]
    X_train <- X_train[1:(nrow(X_train) %/% batch_size * batch_size), ]
    y_train <- y_train[1:(nrow(y_train) %/% batch_size * batch_size), ]
    
    X_valid <- valid_matrix[, 1:n_timesteps]
    y_valid <- valid_matrix[, (n_timesteps + 1):(n_timesteps * 2)]
    X_valid <- X_valid[1:(nrow(X_valid) %/% batch_size * batch_size), ]
    y_valid <- y_valid[1:(nrow(y_valid) %/% batch_size * batch_size), ]
    
    X_test <- test_matrix[, 1:n_timesteps]
    y_test <- test_matrix[, (n_timesteps + 1):(n_timesteps * 2)]
    X_test <- X_test[1:(nrow(X_test) %/% batch_size * batch_size), ]
    y_test <- y_test[1:(nrow(y_test) %/% batch_size * batch_size), ]
    # add on the required third axis
    X_train <- reshape_X_3d(X_train)
    X_valid <- reshape_X_3d(X_valid)
    X_test <- reshape_X_3d(X_test)
    
    y_train <- reshape_X_3d(y_train)
    y_valid <- reshape_X_3d(y_valid)
    y_test <- reshape_X_3d(y_test)

    构建 LSTM 模型

    现在我们的数据具有了所需的形式,让我们构建最终模型。与深度学习一样,一项重要且经常耗时的工作是调整超参数。为了使这篇文章保持独立,并且考虑到这主要是关于如何在 R 中使用 LSTM 的教程,让我们假设在经过大量实验后发现了以下设置(实际上实验确实发生了,但没有达到最佳的表现)。

    我们没有硬编码超参数,而是使用 tfruns 设置了一个环境,在环境中可以轻松地实现网格搜索。

    我们会注释这些参数的作用,但具体含义留到以后再解释。

    FLAGS <- flags(
        # There is a so-called "stateful LSTM" in Keras. While LSTM is stateful per se,
        # this adds a further tweak where the hidden states get initialized with values
        # from the item at same position in the previous batch.
        # This is helpful just under specific circumstances, or if you want to create an
        # "infinite stream" of states, in which case you'd use 1 as the batch size.
        # Below, we show how the code would have to be changed to use this, but it won't be further
        # discussed here.
        flag_boolean("stateful", FALSE),
        # Should we use several layers of LSTM?
        # Again, just included for completeness, it did not yield any superior performance on this task.
        # This will actually stack exactly one additional layer of LSTM units.
        flag_boolean("stack_layers", FALSE),
        # number of samples fed to the model in one go
        flag_integer("batch_size", 10),
        # size of the hidden state, equals size of predictions
        flag_integer("n_timesteps", 12),
        # how many epochs to train for
        flag_integer("n_epochs", 100),
        # fraction of the units to drop for the linear transformation of the inputs
        flag_numeric("dropout", 0.2),
        # fraction of the units to drop for the linear transformation of the recurrent state
        flag_numeric("recurrent_dropout", 0.2),
        # loss function. Found to work better for this specific case than mean squared error
        flag_string("loss", "logcosh"),
        # optimizer = stochastic gradient descent. Seemed to work better than adam or rmsprop here
        # (as indicated by limited testing)
        flag_string("optimizer_type", "sgd"),
        # size of the LSTM layer
        flag_integer("n_units", 128),
        # learning rate
        flag_numeric("lr", 0.003),
        # momentum, an additional parameter to the SGD optimizer
        flag_numeric("momentum", 0.9),
        # parameter to the early stopping callback
        flag_integer("patience", 10))
    
    # the number of predictions we'll make equals the length of the hidden state
    n_predictions <- FLAGS$n_timesteps
    # how many features = predictors we have
    n_features <- 1
    # just in case we wanted to try different optimizers, we could add here
    optimizer <- switch(
        FLAGS$optimizer_type,
        sgd = optimizer_sgd(
            lr = FLAGS$lr,
            momentum = FLAGS$momentum))
    
    # callbacks to be passed to the fit() function
    # We just use one here: we may stop before n_epochs if the loss on the validation set
    # does not decrease (by a configurable amount, over a configurable time)
    callbacks <- list(
        callback_early_stopping(
            patience = FLAGS$patience))

    经过所有这些准备工作,构建和训练模型的代码就相当简短!让我们首先快速查看“长版本”,这将允许你测试堆叠多个 LSTM 或使用状态 LSTM,然后通过最终的短版本(两者都不做)并对其进行评论。

    完整的代码,仅供参考。

    model <- keras_model_sequential()
    
    model %>%
        layer_lstm(
            units = FLAGS$n_units,
            batch_input_shape  = c(
                FLAGS$batch_size,
                FLAGS$n_timesteps,
                n_features),
            dropout = FLAGS$dropout,
            recurrent_dropout = FLAGS$recurrent_dropout,
            return_sequences = TRUE,
            stateful = FLAGS$stateful)
    
    if (FLAGS$stack_layers)
    {
        model %>%
            layer_lstm(
                units             = FLAGS$n_units,
                dropout           = FLAGS$dropout,
                recurrent_dropout = FLAGS$recurrent_dropout,
                return_sequences  = TRUE,
                stateful          = FLAGS$stateful)
    }
    
    model %>% time_distributed(
        layer_dense(units = 1))
    
    model %>%
        compile(
            loss      = FLAGS$loss,
            optimizer = optimizer,
            metrics   = list("mean_squared_error"))
    
    if (!FLAGS$stateful) {
        model %>% fit(
            x          = X_train,
            y          = y_train,
            validation_data = list(X_valid, y_valid),
            batch_size = FLAGS$batch_size,
            epochs     = FLAGS$n_epochs,
            callbacks  = callbacks)
    
    } else
    {
        for (i in 1:FLAGS$n_epochs)
        {
            model %>% fit(
                x          = X_train,
                y          = y_train,
                validation_data = list(X_valid, y_valid),
                callbacks  = callbacks,
                batch_size = FLAGS$batch_size,
                epochs     = 1,
                shuffle    = FALSE)
            model %>% reset_states()
        }
    }
    
    if (FLAGS$stateful)
        model %>% reset_states()

    现在让我们逐步完成下面更简单但更好(或同样)的配置。

    # create the model
    model <- keras_model_sequential()
    
    # add layers
    # we have just two, the LSTM and the time_distributed
    model %>%
        layer_lstm(
            units = FLAGS$n_units,
            # the first layer in a model needs to know the shape of the input data
            batch_input_shape  = c(
                FLAGS$batch_size, FLAGS$n_timesteps, n_features),
            dropout = FLAGS$dropout,
            recurrent_dropout = FLAGS$recurrent_dropout,
            # by default, an LSTM just returns the final state
            return_sequences = TRUE) %>%
        time_distributed(layer_dense(units = 1))
    
    model %>%
        compile(
            loss = FLAGS$loss,
            optimizer = optimizer,
            # in addition to the loss, Keras will inform us about current MSE while training
            metrics = list("mean_squared_error"))
    
    history <- model %>% fit(
        x          = X_train,
        y          = y_train,
        validation_data = list(X_valid, y_valid),
        batch_size = FLAGS$batch_size,
        epochs     = FLAGS$n_epochs,
        callbacks  = callbacks)

    正如我们所见,训练在约 55 个周期后停止,因为验证集损失不再减少。我们还发现验证集上的表现比训练集上的性能差——通常表明过度拟合。

    这个话题,我们将在另一个时间单独讨论,但有趣的是,使用更高 dropoutrecurrent_dropout(与增加的模型容量相结合)的正则化并没有产生更好的泛化表现。这可能与我们在介绍中提到的这个特定时间序列的特征有关。

    plot(history, metrics = "loss")

    232518-20180808235019859-644731064.png

    现在让我们看看该模型捕捉训练集特征的效果如何。

    pred_train <- model %>%
        predict(
            X_train,
            batch_size = FLAGS$batch_size) %>%
        .[, , 1]
    
    # Retransform values to original scale
    pred_train <- (pred_train * scale_history + center_history) ^2
    compare_train <- df %>%
        filter(key == "training")
    
    # build a dataframe that has both actual and predicted values
    for (i in 1:nrow(pred_train))
    {
        varname <- paste0("pred_train", i)
        compare_train <- mutate(
            compare_train,
            !!varname := c(
                rep(NA, FLAGS$n_timesteps + i - 1),
                pred_train[i,],
                rep(NA,
                    nrow(compare_train) - FLAGS$n_timesteps * 2 - i + 1)))
    }

    我们计算所有预测序列的平均 RSME。

    coln <- colnames(compare_train)[4:ncol(compare_train)]
    cols <- map(coln, quo(sym(.)))
    rsme_train <- map_dbl(
        cols,
        function(col)
        {
            rmse(
                compare_train,
                truth = value,
                estimate = !!col,
                na.rm = TRUE)
        }) %>%
        mean()
    
    rsme_train
    21.01495

    这些预测看起来如何?由于所有预测序列的可视化看起来会非常拥挤,我们会间隔地选择起始点。

    ggplot(
        compare_train,
        aes(x = index, y = value)) +
        geom_line() +
        geom_line(aes(y = pred_train1), color = "cyan") +
        geom_line(aes(y = pred_train50), color = "red") +
        geom_line(aes(y = pred_train100), color = "green") +
        geom_line(aes(y = pred_train150), color = "violet") +
        geom_line(aes(y = pred_train200), color = "cyan") +
        geom_line(aes(y = pred_train250), color = "red") +
        geom_line(aes(y = pred_train300), color = "red") +
        geom_line(aes(y = pred_train350), color = "green") +
        geom_line(aes(y = pred_train400), color = "cyan") +
        geom_line(aes(y = pred_train450), color = "red") +
        geom_line(aes(y = pred_train500), color = "green") +
        geom_line(aes(y = pred_train550), color = "violet") +
        geom_line(aes(y = pred_train600), color = "cyan") +
        geom_line(aes(y = pred_train650), color = "red") +
        geom_line(aes(y = pred_train700), color = "red") +
        geom_line(aes(y = pred_train750), color = "green") +
        ggtitle("Predictions on the training set")

    232518-20180808235030821-446336660.png

    看起来像当好。对于验证集,我们并不认为有同样好的结果。

    pred_test <- model %>%
        predict(
            X_test,
            batch_size = FLAGS$batch_size) %>%
        .[, , 1]
    
    # Retransform values to original scale
    pred_test <- (pred_test * scale_history + center_history) ^2
    pred_test[1:10, 1:5] %>% print()
    compare_test <- df %>% filter(key == "testing")
    
    # build a dataframe that has both actual and predicted values
    for (i in 1:nrow(pred_test))
    {
        varname <- paste0("pred_test", i)
        compare_test <-mutate(
            compare_test,
            !!varname := c(
                rep(NA, FLAGS$n_timesteps + i - 1),
                pred_test[i,],
                rep(NA,
                    nrow(compare_test) - FLAGS$n_timesteps * 2 - i + 1)))
    }
    
    compare_test %>% write_csv(
        str_replace(model_path, ".hdf5", ".test.csv"))
    compare_test[FLAGS$n_timesteps:(FLAGS$n_timesteps + 10), c(2, 4:8)] %>%
        print()
    
    coln <- colnames(compare_test)[4:ncol(compare_test)]
    cols <- map(coln, quo(sym(.)))
    rsme_test <- map_dbl(
        cols,
        function(col)
        {
            rmse(
                compare_test,
                truth = value,
                estimate = !!col,
                na.rm = TRUE)
        }) %>%
        mean()
    
    rsme_test
    31.31616
    ggplot(
        compare_test,
        aes(x = index, y = value)) +
        geom_line() +
        geom_line(aes(y = pred_test1), color = "cyan") +
        geom_line(aes(y = pred_test50), color = "red") +
        geom_line(aes(y = pred_test100), color = "green") +
        geom_line(aes(y = pred_test150), color = "violet") +
        geom_line(aes(y = pred_test200), color = "cyan") +
        geom_line(aes(y = pred_test250), color = "red") +
        geom_line(aes(y = pred_test300), color = "green") +
        geom_line(aes(y = pred_test350), color = "cyan") +
        geom_line(aes(y = pred_test400), color = "red") +
        geom_line(aes(y = pred_test450), color = "green") +  
        geom_line(aes(y = pred_test500), color = "cyan") +
        geom_line(aes(y = pred_test550), color = "violet") +
        ggtitle("Predictions on test set")

    232518-20180808235042037-2034187582.png

    这不如训练集那么好,但也不错,因为这个时间序列非常具有挑战性。

    在手动选择的示例分割中定义并运行我们的模型后,让我们现在回到我们的整体重新抽样框架。

    在所有分割上回测模型

    为了获得所有分割的预测,我们将上面的代码移动到一个函数中并将其应用于所有分割。它返回包含两个数据框的列表,分别对应训练和测试集,每个数据框包含模型的预测以及实际值。

    obtain_predictions <- function(split)
    {
        df_trn <- analysis(split)[1:800, , drop = FALSE]
        df_val <- analysis(split)[801:1200, , drop = FALSE]
        df_tst <- assessment(split)
    
        df <- bind_rows(
            df_trn %>% add_column(key = "training"),
            df_val %>% add_column(key = "validation"),
            df_tst %>% add_column(key = "testing")) %>%
            as_tbl_time(index = index)
    
        rec_obj <- recipe(
            value ~ ., df) %>%
            step_sqrt(value) %>%
            step_center(value) %>%
            step_scale(value) %>%
            prep()
    
        df_processed_tbl <- bake(rec_obj, df)
    
        center_history <- rec_obj$steps[[2]]$means["value"]
        scale_history  <- rec_obj$steps[[3]]$sds["value"]
    
        FLAGS <- flags(
            flag_boolean("stateful", FALSE),
            flag_boolean("stack_layers", FALSE),
            flag_integer("batch_size", 10),
            flag_integer("n_timesteps", 12),
            flag_integer("n_epochs", 100),
            flag_numeric("dropout", 0.2),
            flag_numeric("recurrent_dropout", 0.2),
            flag_string("loss", "logcosh"),
            flag_string("optimizer_type", "sgd"),
            flag_integer("n_units", 128),
            flag_numeric("lr", 0.003),
            flag_numeric("momentum", 0.9),
            flag_integer("patience", 10))
    
        n_predictions <- FLAGS$n_timesteps
        n_features <- 1
    
        optimizer <- switch(
            FLAGS$optimizer_type,
            sgd = optimizer_sgd(
                lr = FLAGS$lr, momentum = FLAGS$momentum))
        callbacks <- list(
            callback_early_stopping(patience = FLAGS$patience))
    
        train_vals <- df_processed_tbl %>%
            filter(key == "training") %>%
            select(value) %>%
            pull()
        valid_vals <- df_processed_tbl %>%
            filter(key == "validation") %>%
            select(value) %>%
            pull()
        test_vals <- df_processed_tbl %>%
            filter(key == "testing") %>%
            select(value) %>%
            pull()
    
        train_matrix <- build_matrix(
            train_vals, FLAGS$n_timesteps + n_predictions)
        valid_matrix <- build_matrix(
            valid_vals, FLAGS$n_timesteps + n_predictions)
        test_matrix <- build_matrix(
            test_vals, FLAGS$n_timesteps + n_predictions)
    
        X_train <- train_matrix[, 1:FLAGS$n_timesteps]
        y_train <- train_matrix[, (FLAGS$n_timesteps + 1):(FLAGS$n_timesteps * 2)]
        X_train <- X_train[1:(nrow(X_train) %/% FLAGS$batch_size * FLAGS$batch_size),]
        y_train <- y_train[1:(nrow(y_train) %/% FLAGS$batch_size * FLAGS$batch_size),]
    
        X_valid <- valid_matrix[, 1:FLAGS$n_timesteps]
        y_valid <- valid_matrix[, (FLAGS$n_timesteps + 1):(FLAGS$n_timesteps * 2)]
        X_valid <- X_valid[1:(nrow(X_valid) %/% FLAGS$batch_size * FLAGS$batch_size),]
        y_valid <- y_valid[1:(nrow(y_valid) %/% FLAGS$batch_size * FLAGS$batch_size),]
    
        X_test <- test_matrix[, 1:FLAGS$n_timesteps]
        y_test <- test_matrix[, (FLAGS$n_timesteps + 1):(FLAGS$n_timesteps * 2)]
        X_test <- X_test[1:(nrow(X_test) %/% FLAGS$batch_size * FLAGS$batch_size),]
        y_test <- y_test[1:(nrow(y_test) %/% FLAGS$batch_size * FLAGS$batch_size),]
    
        X_train <- reshape_X_3d(X_train)
        X_valid <- reshape_X_3d(X_valid)
        X_test <- reshape_X_3d(X_test)
    
        y_train <- reshape_X_3d(y_train)
        y_valid <- reshape_X_3d(y_valid)
        y_test <- reshape_X_3d(y_test)
    
        model <- keras_model_sequential()
    
        model %>%
            layer_lstm(
                units              = FLAGS$n_units,
                batch_input_shape  = c(
                    FLAGS$batch_size, FLAGS$n_timesteps, n_features),
                dropout            = FLAGS$dropout,
                recurrent_dropout  = FLAGS$recurrent_dropout,
                return_sequences   = TRUE) %>%
            time_distributed(layer_dense(units = 1))
    
        model %>%
            compile(
                loss = FLAGS$loss,
                optimizer = optimizer,
                metrics = list("mean_squared_error"))
    
        model %>% fit(
            x          = X_train,
            y          = y_train,
            validation_data = list(X_valid, y_valid),
            batch_size = FLAGS$batch_size,
            epochs     = FLAGS$n_epochs,
            callbacks  = callbacks)
    
        pred_train <- model %>%
            predict(
                X_train,
                batch_size = FLAGS$batch_size) %>%
            .[, , 1]
    
        # Retransform values
        pred_train <- (pred_train * scale_history + center_history) ^ 2
        compare_train <- df %>% filter(key == "training")
    
        for (i in 1:nrow(pred_train))
        {
            varname <- paste0("pred_train", i)
            compare_train <- mutate(
                compare_train,
                !!varname := c(
                    rep(NA, FLAGS$n_timesteps + i - 1),
                    pred_train[i, ],
                    rep(NA,
                        nrow(compare_train) - FLAGS$n_timesteps * 2 - i + 1)))
        }
    
        pred_test <- model %>%
            predict(
                X_test,
                batch_size = FLAGS$batch_size) %>%
            .[, , 1]
    
        # Retransform values
        pred_test <- (pred_test * scale_history + center_history) ^ 2
        compare_test <- df %>% filter(key == "testing")
    
        for (i in 1:nrow(pred_test))
        {
            varname <- paste0("pred_test", i)
            compare_test <- mutate(
                compare_test,
                !!varname := c(
                    rep(NA, FLAGS$n_timesteps + i - 1),
                    pred_test[i, ],
                    rep(NA,
                        nrow(compare_test) - FLAGS$n_timesteps * 2 - i + 1)))
        }
        
        list(
            train = compare_train,
            test = compare_test)
    }

    将函数应用到所有分割上,得到一系列预测。

    all_split_preds <- rolling_origin_resamples %>%
         mutate(predict = map(splits, obtain_predictions))

    计算所有分割的 RMSE:

    calc_rmse <- function(df)
    {
        coln <- colnames(df)[4:ncol(df)]
        cols <- map(coln, quo(sym(.)))
        map_dbl(
            cols,
            function(col)
            {
                rmse(
                    df, truth = value,
                    estimate = !!col, na.rm = TRUE)
            }) %>%
            mean()
    }
    
    all_split_preds <- all_split_preds %>% unnest(predict)
    all_split_preds_train <- all_split_preds[seq(1, 11, by = 2), ]
    all_split_preds_test <- all_split_preds[seq(2, 12, by = 2), ]
    
    all_split_rmses_train <- all_split_preds_train %>%
        mutate(rmse = map_dbl(predict, calc_rmse)) %>%
        select(id, rmse)
    
    all_split_rmses_test <- all_split_preds_test %>%
        mutate(rmse = map_dbl(predict, calc_rmse)) %>%
        select(id, rmse)

    这是 6 个分割的 RMSE。

    all_split_rmses_train
    # A tibble: 6 x 2
      id      rmse
      <chr>  <dbl>
    1 Slice1  22.2
    2 Slice2  20.9
    3 Slice3  18.8
    4 Slice4  23.5
    5 Slice5  22.1
    6 Slice6  21.1
    all_split_rmses_test
    # A tibble: 6 x 2
      id      rmse
      <chr>  <dbl>
    1 Slice1  21.6
    2 Slice2  20.6
    3 Slice3  21.3
    4 Slice4  31.4
    5 Slice5  35.2
    6 Slice6  31.4

    在这些数字中,我们看到了一些有趣的东西:时间序列的前三个分割上的泛化表现比后者更好。这证实了我们的印象,如上所述,数据似乎有一些隐藏的变化发展,这使预测更加困难。

    这里是相应训练和测试集的预测可视化。

    首先,训练集:

    plot_train <- function(slice,
                           name)
    {
        ggplot(
            slice,
            aes(x = index, y = value)) +
            geom_line() +
            geom_line(aes(y = pred_train1), color = "cyan") +
            geom_line(aes(y = pred_train50), color = "red") +
            geom_line(aes(y = pred_train100), color = "green") +
            geom_line(aes(y = pred_train150), color = "violet") +
            geom_line(aes(y = pred_train200), color = "cyan") +
            geom_line(aes(y = pred_train250), color = "red") +
            geom_line(aes(y = pred_train300), color = "red") +
            geom_line(aes(y = pred_train350), color = "green") +
            geom_line(aes(y = pred_train400), color = "cyan") +
            geom_line(aes(y = pred_train450), color = "red") +
            geom_line(aes(y = pred_train500), color = "green") +
            geom_line(aes(y = pred_train550), color = "violet") +
            geom_line(aes(y = pred_train600), color = "cyan") +
            geom_line(aes(y = pred_train650), color = "red") +
            geom_line(aes(y = pred_train700), color = "red") +
            geom_line(aes(y = pred_train750), color = "green") +
            ggtitle(name)
    }
    
    train_plots <- map2(
        all_split_preds_train$predict,
        all_split_preds_train$id, plot_train)
    p_body_train  <- plot_grid(
        plotlist = train_plots, ncol = 3)
    p_title_train <- ggdraw() +
        draw_label(
            "Backtested Predictions: Training Sets",
            size = 18, fontface = "bold")
    
    plot_grid(
        p_title_train, p_body_train,
        ncol = 1,
        rel_heights = c(0.05, 1, 0.05))

    232518-20180808235054428-1073822660.png

    接着是测试集:

    plot_test <- function(slice,
                          name)
    {
        ggplot(
            slice, aes(x = index, y = value)) +
            geom_line() +
            geom_line(aes(y = pred_test1), color = "cyan") +
            geom_line(aes(y = pred_test50), color = "red") +
            geom_line(aes(y = pred_test100), color = "green") +
            geom_line(aes(y = pred_test150), color = "violet") +
            geom_line(aes(y = pred_test200), color = "cyan") +
            geom_line(aes(y = pred_test250), color = "red") +
            geom_line(aes(y = pred_test300), color = "green") +
            geom_line(aes(y = pred_test350), color = "cyan") +
            geom_line(aes(y = pred_test400), color = "red") +
            geom_line(aes(y = pred_test450), color = "green") +
            geom_line(aes(y = pred_test500), color = "cyan") +
            geom_line(aes(y = pred_test550), color = "violet") +
            ggtitle(name)
    }
    
    test_plots <- map2(
        all_split_preds_test$predict,
        all_split_preds_test$id, plot_test)
    
    p_body_test  <- plot_grid(
        plotlist = test_plots, ncol = 3)
    p_title_test <- ggdraw() +
        draw_label(
            "Backtested Predictions: Test Sets",
            size = 18, fontface = "bold")
    
    plot_grid(
        p_title_test, p_body_test,
        ncol = 1, rel_heights = c(0.05, 1, 0.05))

    232518-20180808235106307-204259145.png

    这是一个很长的帖子,必然会留下很多问题,首先是我们如何获得好的超参数设置(学习率、周期数、dropout)? 我们如何选择隐含状态的长度?或者甚至,我们能否直观了解 LSTM 在给定数据集上的表现(具有其特定特征)?我们将在未来的文章中解决上述问题。

    转载于:https://www.cnblogs.com/xuruilong100/p/9446475.html

    展开全文
  • Seq2Seq 是一种循环神经网络的变种,包括编码器 (Encoder) 和解码器 (Decoder) 两部分。Seq2Seq 是自然语言处理中的一种重要模型,可以用于机器翻译、对话系统、自动文摘。 1. RNN 结构及使用RNN 模型在之前的文章...

    Seq2Seq 是一种循环神经网络的变种,包括编码器 (Encoder) 和解码器 (Decoder) 两部分。Seq2Seq 是自然语言处理中的一种重要模型,可以用于机器翻译、对话系统、自动文摘。

    1. RNN 结构及使用

    c63c0b191fc270d27213ff72de5742f1.png

    RNN 模型

    在之前的文章《循环神经网络 RNN、LSTM、GRU》中介绍了 RNN 模型,RNN 基本的模型如上图所示,每个神经元接受的输入包括:前一个神经元的隐藏层状态 h (用于记忆) 和当前的输入 x (当前信息)。神经元得到输入之后,会计算出新的隐藏状态 h 和输出 y,然后再传递到下一个神经元。因为隐藏状态 h 的存在,使得 RNN 具有一定的记忆功能。

    针对不同任务,通常要对 RNN 模型结构进行少量的调整,根据输入和输出的数量,分为三种比较常见的结构:N vs N、1 vs N、N vs 1。

    1.1 N vs N

    8ad359fbc48b6ffcefaa92742ae499ba.png

    N vs N 结构

    上图是RNN 模型的一种 N vs N 结构,包含 N 个输入 x1, x2, ..., xN,和 N 个输出 y1, y2, ..., yN。N vs N 的结构中,输入和输出序列的长度是相等的,通常适合用于以下任务:

    • 词性标注
    • 训练语言模型,使用之前的词预测下一个词等

    1.2 1 vs N

    2ce4c7397aa16797fa635e44843a3255.png

    1 vs N 结构(1)

    22610844646c039e76d0d02c8183a8df.png

    1 vs N 结构(2)

    在 1 vs N 结构中,我们只有一个输入 x,和 N 个输出 y1, y2, ..., yN。可以有两种方式使用 1 vs N,第一种只将输入 x 传入第一个 RNN 神经元,第二种是将输入 x 传入所有的 RNN 神经元。1 vs N 结构适合用于以下任务:

    • 图像生成文字,输入 x 就是一张图片,输出就是一段图片的描述文字
    • 根据音乐类别,生成对应的音乐
    • 根据小说类别,生成相应的小说

    1.3 N vs 1

    f20f845b212b61a0cb74ed0f71ed5194.png

    N vs 1 结构

    在 N vs 1 结构中,我们有 N 个输入 x1, x2, ..., xN,和一个输出 y。N vs 1 结构适合用于以下任务:

    • 序列分类任务,一段语音、一段文字的类别,句子的情感分析

    2. Seq2Seq 模型

    2.1 Seq2Seq 结构

    上面的三种结构对于 RNN 的输入和输出个数都有一定的限制,但实际中很多任务的序列的长度是不固定的,例如机器翻译中,源语言、目标语言的句子长度不一样;对话系统中,问句和答案的句子长度不一样。

    Seq2Seq 是一种重要的 RNN 模型,也称为 Encoder-Decoder 模型,可以理解为一种 N×M 的模型。模型包含两个部分:Encoder 用于编码序列的信息,将任意长度的序列信息编码到一个向量 c 里。而 Decoder 是解码器,解码器得到上下文信息向量 c 之后可以将信息解码,并输出为序列。Seq2Seq 模型结构有很多种,下面是几种比较常见的:

    1b2dc60f81577723712985cd20c33ea0.png

    第一种 Seq2Seq 结构

    033c8a49dd8d84478c35b63dde3e2f51.png

    第二种 Seq2Seq 结构

    e90caa1cafbe19fe1069fbd68eac5d16.png

    第三种 Seq2Seq 结构

    2.2 编码器 Encoder

    这三种 Seq2Seq 模型的主要区别在于 Decoder,他们的 Encoder 都是一样的。下图是 Encoder 部分,Encoder 的 RNN 接受输入 x,最终输出一个编码所有信息的上下文向量 c,中间的神经元没有输出。Decoder 主要传入的是上下文向量 c,然后解码出需要的信息。

    1dcbbe767244dc80d626dd62af944081.png

    Encoder

    从上图可以看到,Encoder 与一般的 RNN 区别不大,只是中间神经元没有输出。其中的上下文向量 c 可以采用多种方式进行计算。

    0f4a076fcaa551383ba9ba8795ad9d6c.png

    上下文向量 c 计算公式

    从公式可以看到,c 可以直接使用最后一个神经元的隐藏状态 hN 表示;也可以在最后一个神经元的隐藏状态上进行某种变换 hN 而得到,q 函数表示某种变换;也可以使用所有神经元的隐藏状态 h1, h2, ..., hN 计算得到。得到上下文向量 c 之后,需要传递到 Decoder。

    2.3 解码器 Decoder

    Decoder 有多种不同的结构,这里主要介绍三种。

    第一种

    1754e532df296bd6c8f57844f7859113.png

    第一种 Decoder 结构

    第一种 Decoder 结构比较简单,将上下文向量 c 当成是 RNN 的初始隐藏状态,输入到 RNN 中,后续只接受上一个神经元的隐藏层状态 h' 而不接收其他的输入 x。第一种 Decoder 结构的隐藏层及输出的计算公式:

    4f308fd36b8efcfe7e4a1374009edaab.png

    第一种 Decoder 隐藏层及输出的计算公式

    第二种

    2f6c459e0c3653a9ae8f04b0c86d87fb.png

    第二种 Decoder 结构

    第二种 Decoder 结构有了自己的初始隐藏层状态 h'0,不再把上下文向量 c 当成是 RNN 的初始隐藏状态,而是当成 RNN 每一个神经元的输入。可以看到在 Decoder 的每一个神经元都拥有相同的输入 c,这种 Decoder 的隐藏层及输出计算公式:

    014149087b34021e28eb8b84c348f19a.png

    第二种 Decoder 隐藏层及输出的计算公式

    第三种

    3dcaf89e6f6e3c3f326924ef4df9b9ad.png

    第三种 Decoder 结构

    第三种 Decoder 结构和第二种类似,但是在输入的部分多了上一个神经元的输出 y'。即每一个神经元的输入包括:上一个神经元的隐藏层向量 h',上一个神经元的输出 y',当前的输入 c (Encoder 编码的上下文向量)。对于第一个神经元的输入 y'0,通常是句子其实标志位的 embedding 向量。第三种 Decoder 的隐藏层及输出计算公式:

    5710491146b8cb6c905e582d69f86fe0.png

    第三种 Decoder 隐藏层及输出的计算公式

    3. Seq2Seq模型使用技巧

    3.1 Teacher Forcing

    Teacher Forcing 用于训练阶段,主要针对上面第三种 Decoder 模型来说的,第三种 Decoder 模型神经元的输入包括了上一个神经元的输出 y'。如果上一个神经元的输出是错误的,则下一个神经元的输出也很容易错误,导致错误会一直传递下去。

    而 Teacher Forcing 可以在一定程度上缓解上面的问题,在训练 Seq2Seq 模型时,Decoder 的每一个神经元并非一定使用上一个神经元的输出,而是有一定的比例采用正确的序列作为输入。

    举例说明,在翻译任务中,给定英文句子翻译为中文。"I have a cat" 翻译成 "我有一只猫",如果不使用 Teacher Forcing:

    27fc8e08decbf657420425ad41336008.png

    不使用 Teacher Forcing

    如果使用 Teacher Forcing,则神经元直接使用正确的输出作为当前神经元的输入。

    0387fdd946d02b6c6d29d43caa344820.png

    使用 Teacher Forcing

    3.2 Attention

    在 Seq2Seq 模型,Encoder 总是将源句子的所有信息编码到一个固定长度的上下文向量 c 中,然后在 Decoder 解码的过程中向量 c 都是不变的。这存在着不少缺陷:

    • 对于比较长的句子,很难用一个定长的向量 c 完全表示其意义。
    • RNN 存在长序列梯度消失的问题,只使用最后一个神经元得到的向量 c 效果不理想。
    • 与人类的注意力方式不同,即人类在阅读文章的时候,会把注意力放在当前的句子上。

    Attention 即注意力机制,是一种将模型的注意力放在当前翻译单词上的一种机制。例如翻译 "I have a cat",翻译到 "我" 时,要将注意力放在源句子的 "I" 上,翻译到 "猫" 时要将注意力放在源句子的 "cat" 上。

    使用了 Attention 后,Decoder 的输入就不是固定的上下文向量 c 了,而是会根据当前翻译的信息,计算当前的 c

    9a88ffc5b369361b338527f26b6d7f80.png

    Decoder 使用 Attention

    Attention 需要保留 Encoder 每一个神经元的隐藏层向量 h,然后 Decoder 的第 t 个神经元要根据上一个神经元的隐藏层向量 h't-1 计算出当前状态与 Encoder 每一个神经元的相关性 et。et 是一个 N 维的向量 (Encoder 神经元个数为 N),若 et 的第 i 维越大,则说明当前节点与 Encoder 第 i 个神经元的相关性越大。et 的计算方法有很多种,即相关性系数的计算函数 a 有很多种:

    3e579b4c29060e5850d5149d0e1ad49f.png

    Attention 相关性系数计算公式

    上面得到相关性向量 et 后,需要进行归一化,使用 softmax 归一化。然后用归一化后的系数融合 Encoder 的多个隐藏层向量得到 Decoder 当前神经元的上下文向量 ct:

    2d246f9ecef275f8f34528586d753f3f.png

    Attention 上下文向量的计算公式

    3.3 beam search

    beam search 方法不用于训练的过程,而是用在测试的。在每一个神经元中,我们都选取当前输出概率值最大的 top k 个输出传递到下一个神经元。下一个神经元分别用这 k 个输出,计算出 L 个单词的概率 (L 为词汇表大小),然后在 kL 个结果中得到 top k 个最大的输出,重复这一步骤。

    4. Seq2Seq 总结

    Seq2Seq 模型允许我们使用长度不同的输入和输出序列,适用范围相当广,可用于机器翻译,对话系统,阅读理解等场景。

    Seq2Seq 模型使用时可以利用 Teacher Forceing,Attention,beam search 等方法优化。

    参考文献

    博客园:RNN神经网络模型的不同结构

    知乎:Tensorflow中的Seq2Seq全家桶

    知乎:Attention机制详解(一)——Seq2Seq中的Attention

    展开全文
  • PyTorch: 序列序列模型(Seq2Seq)实现机器翻译实战

    万次阅读 热门讨论 2018-02-12 20:16:44
    与单个RNN的序列预测不同,每个输入对应于一个输出,seq2seq模型无需考虑序列长度和顺序,这使得它成为两种语言之间翻译的理想选择。使用seq2seq模型,编码器会创建一个单一的矢量,在理想的情况下,将输入序列的...

    简介

    在这个项目中,我们将使用PyTorch框架实现一个神经网络,这个网络实现法文翻译成英文。这个项目是Sean Robertson写的稍微复杂一点的教程,但对学习PyTorch还是有很大的帮助。

    本文通过序列网络的这种简单而强大的思想来实现的,其中包括两个循环神经网络一起工作以将一个序列转换为另一个序列。 编码器网络(Encode)将输入序列压缩成矢量,解码器网络(Decode)将该矢量展开为新的序列。为了改进这个模型,我们将使用一个注意机制,让解码器学习把注意力集中在输入序列的特定范围上。

    这里写图片描述

    关于这些技术,更多的学习资料可以在下面网址学习:http://pytorch.org/tutorials/intermediate/seq2seq_translation_tutorial.html

    数据集

    这个项目的数据是一组数以千计的英语到法语的翻译对。作者选取了其中部分数据构建本文的训练数据集(data / eng-fra.txt)。 该文件是一个制表符分隔的翻译对列表(下载地址:https://download.pytorch.org/tutorial/data.zip):

    这里写图片描述

    我们才有one-hot vector初始化词,与前面分类名词不同的是,这里把单词看作一个独立的语言粒度:
    这里写图片描述

    我们需要每个单词的唯一索引作为以后网络的输入(inputs)和目标(targets)。 为此,我们使用名为Lang的助手类,它具有词→索引(word2index)和索引→词(index2word)字典,以及每个单词word2count的计数以用于稍后替换罕见词语。

    SOS_token = 0
    EOS_token = 1
    
    
    class Lang:
        def __init__(self, name):
            self.name = name
            self.word2index = {}
            self.word2count = {}
            self.index2word = {0: "SOS", 1: "EOS"}
            self.n_words = 2  # Count SOS and EOS
    
        def addSentence(self, sentence):
            for word in sentence.split(' '):
                self.addWord(word)
    
        def addWord(self, word):
            if word not in self.word2index:
                self.word2index[word] = self.n_words
                self.word2count[word] = 1
                self.index2word[self.n_words] = word
                self.n_words += 1
            else:
                self.word2count[word] += 1
    

    要读取数据文件,我们将文件分割成几行,然后将行分成两部分。 这些文件都是英文→其他语言,所以如果我们想从其他语言翻译→英文,我添加了reverse标志来反转对。

    def readLangs(lang1, lang2, reverse=False):
        print("Reading lines...")
    
        # Read the file and split into lines
        lines = open('data/%s-%s.txt' % (lang1, lang2), encoding='utf-8').\
            read().strip().split('\n')
    
        # Split every line into pairs and normalize
        pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]
    
        # Reverse pairs, make Lang instances
        if reverse:
            pairs = [list(reversed(p)) for p in pairs]
            input_lang = Lang(lang2)
            output_lang = Lang(lang1)
        else:
            input_lang = Lang(lang1)
            output_lang = Lang(lang2)
    
        return input_lang, output_lang, pairs
    

    本文数据预处理过程是:
    1.读取文本文件并拆分成行,将行拆分成对
    2.使文本标准化,按照长度和内容进行过滤
    3.从成对的句子中构建单词列表

    Seq2Seq模型

    Seq2Seq(Sequence to Sequence network or Encoder Decoder network)是由两个称为编码器和解码器的RNN组成的模型。 编码器读取输入序列并输出单个矢量,解码器读取该矢量以产生输出序列。

    与单个RNN的序列预测不同,每个输入对应于一个输出,seq2seq模型无需考虑序列长度和顺序,这使得它成为两种语言之间翻译的理想选择。使用seq2seq模型,编码器会创建一个单一的矢量,在理想的情况下,将输入序列的“含义”编码为单个矢量 - 句子的N维空间中的单个点。

    The Encoder

    seq2seq网络的编码器是一个RNN,它为输入句子中的每个单词输出一些值。 对于每个输入单词,编码器输出一个向量和一个隐藏状态,这个隐藏状态和下一个单词构成下一步的输入。

    这里写图片描述

    class EncoderRNN(nn.Module):
        def  __init__(self, input_size, hidden_size):
            super(EncoderRNN, self).__init__()
            self.hidden_size = hidden_size
    
            self.embedding = nn.Embedding(input_size, hidden_size)
            self.gru = nn.GRU(hidden_size, hidden_size)
    
        def forward(self, input, hidden):
            embedded = self.embedding(input).view(1, 1, -1)
            output = embedded
            output, hidden = self.gru(output, hidden)
            return output, hidden
    
        def initHidden(self):
            result = Variable(torch.zeros(1, 1, self.hidden_size))
            return result
    
    

    The Decoder

    解码器是另一个RNN,它接收编码器输出向量并输出一个字序列来创建翻译。

    在最简单的seq2seq解码器中,我们只使用编码器的最后一个输出。 这个最后的输出有时被称为上下文向量,因为它从整个序列编码上下文。 该上下文向量被用作解码器的初始隐藏状态。如果仅在编码器和解码器之间传递上下文向量,则该单个向量承担编码整个句子的负担。注意力(Attention Decoder)允许解码器网络针对解码器自身输出的每一步“聚焦”编码器输出的不同部分。首先我们计算一组注意力权重。 这些将被乘以编码器输出矢量以创建加权组合。

    这里写图片描述

    这里写图片描述

    class AttnDecoderRNN(nn.Module):
        def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
            super(AttnDecoderRNN, self).__init__()
            self.hidden_size = hidden_size
            self.output_size = output_size
            self.dropout_p = dropout_p
            self.max_length = max_length
    
            self.embedding = nn.Embedding(self.output_size, self.hidden_size)
            self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
            self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
            self.dropout = nn.Dropout(self.dropout_p)
            self.gru = nn.GRU(self.hidden_size, self.hidden_size)
            self.out = nn.Linear(self.hidden_size, self.output_size)
    
        def forward(self, input, hidden, encoder_outputs):
            embedded = self.embedding(input).view(1, 1, -1)
            embedded = self.dropout(embedded)
            attn_weights = F.softmax(
                self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
            attn_applied = torch.bmm(attn_weights.unsqueeze(0),
                                     encoder_outputs.unsqueeze(0))
            output = torch.cat((embedded[0], attn_applied[0]), 1)
            output = self.attn_combine(output).unsqueeze(0)
            output = F.relu(output)
            output, hidden = self.gru(output, hidden)
            output = F.log_softmax(self.out(output[0]), dim=1)
            return output, hidden, attn_weights
    
        def initHidden(self):
            result = Variable(torch.zeros(1, 1, self.hidden_size))
            return result
    

    训练和测试模型

    loss 图:
    这里写图片描述

    评估与训练大部分相同,但没有目标(target),因此我们只是将解码器的预测反馈给每一步的自身。 每当它预测到一个单词时,我们就会将它添加到输出字符串中,并且当生成EOS字符就停止。 我们还存储解码器的注意力输出以供稍后显示。

    可视化Attention,这个机制的一个有用特性是其高度可解释的输出。 因为它用于对输入序列的特定编码器输出进行加权,所以我们可以想象在每个时间步骤中网络最集中的位置。这里将注意力输出显示为矩阵,其中列是输入步骤,行是输出步骤:
    这里写图片描述

    更好的观看体验,我们额外用了几个数据对:
    这里写图片描述

    注意:所以的代码基本上为教程上的,我跑通的代码稍微会上传到github上。

    参考:http://pytorch.org/tutorials/intermediate/seq2seq_translation_tutorial.html

    更多AI、ML、NLP干货资源请关注公众号:AI小白入门(ID: StudyForAI):
    在这里插入图片描述

    展开全文
  • 本篇文章内容:介绍数据准备和预处理长短期记忆(LSTM) - 背景知识编码器模型架构(Seq2Seq)编码器代码实现(Seq2Seq)解码器模型架构(Seq2Seq)解码器代码实现(Seq2Seq)Seq2Seq(编码器+解码器)接口Seq2Seq(编码器+解码器...
  • 在本文中,你将了解:为什么我们需要seq2seq模型的注意力机制?Bahdanua的注意力机制是如何运作的?Luong的注意力机制是如何运作的?什么是局部和全局注意力?Bahdanua和Luong注意力机制的关键区别什么是注意力,为...
  • 参考链接: Python机器学习中的seq2seq模型 简介 在这个项目中,我们将使用PyTorch框架实现一个神经网络,这个网络实现法文翻译成英文。这个项目是Sean Robertson写的稍微复杂一点的教程,但对学习PyTorch还是有很...
  • seq2seq与Attention机制

    千次阅读 2020-07-20 22:22:19
    学习目标 目标 掌握seq2seq模型特点 掌握集束搜索方式 掌握BLEU评估方法 掌握Attention机制 ...seq2seq是一个Encoder–Decoder 结构的网络,它的输入是一个序列,输出也是一个序列,Encoder 中将一个可变长度.
  • 使用 seq2seq 模型解决文本生成任务伴随着一些重大缺陷,...序列序列seq2seq)模型给机器翻译领域带来了巨大变革,并成为多种文本生成任务的首选工具,如文本摘要、句子融合和语法纠错。模型架构改进(如 Transf...
  • 本文和大家一起学习一个当下比较流行的网络,但是也是比较基础的-序列网络(SEQ2SEQ),为什么要引入这个网络呢?因为在很多序列处理的应用的场景中,比如机器翻译,文章摘要提取,评价数据分析等,在NLP中传统的rnn效率太...
  • seq2seq

    2020-01-14 13:34:05
    seq2seq 模型就像一个翻译模型,输入是一个序列(比如一个英文句子),输出也是一个序列(比如该英文句子所对应的法文翻译)。这种结构最重要的地方在于输入序列和输出序列的长度是可变的。 seq2seq原理图 算法原理...
  • 注意点: 1 数据预处理阶段(添加特殊...在训练过程中,我们需要将我们的target序列作为输入传给Decoder端RNN的每个阶段,而不是使用前一阶段预测输出,这样会使得模型更加准确。TrainingHelper用于训练阶段,Greedy
  • keras实现seq2seq做Chatbot

    2020-10-28 16:16:01
    今天在《自然语言处理实战》一书中看到一个seq2seq实现的Chatbot的例子,感觉可以很好地体现出seq2seq的架构,这里我们一起来实现如何用seq2seq实现Chatbot。 首先,seq2seq架构大家都比较熟悉,我们一起来看看本文...
  • Seq2Seq的PyTorch实现

    千次阅读 2020-07-02 09:11:11
    本文介绍一下如何使用 PyTorch 复现 Seq2Seq,实现简单的机器翻译应用,请先简单阅读论文...我看了很多Seq2Seq网络结构图,感觉PyTorch官方提供的这个图是最好理解的 首先,从上面的图可以很明显的看出,Seq2Se
  • Seq2Seq模型

    2021-08-28 10:27:13
    文章目录前言一、Seq2Seq模型1、Seq2Seq的介绍2、Seq2Seq模型的实现2.1、实现流程2.2、文本转化为序列、准备Dataloader二、模型的搭建1.准备编码器2. 准备解码器3. seq2seq模型搭建、训练和保存三、整体流程 前言 ...
  • 干货内容敬请关注「平安寿险PAI」(公众号ID:PAL-AI),文末有本期分享内容资料获取方式。人机对话作为人机交互系统的核心功能之一,发挥着十分重要的作用。目前,生成式的人机对话存在内容把控性较弱,生成...
  • 0、引言: 承接上一篇,现在继续对于seq2seq模型进行讲解,decoder部分是和encoder部分对应的,层数、隐藏层、单元数都要对应。 1、LSTM Seq2Seq Decoder ...接下来将隐藏状态传递给Linear层,预测目标序列下一个标记应
  • Keras-9 实现Seq2Seq

    万次阅读 热门讨论 2018-03-06 22:30:24
    A ten-minute introduction to sequence-to-...简单介绍如何用Keras实现Seq2Seq模型 原文链接 https://blog.keras.io/a-ten-minute-introduction-to-sequence-to-sequence-learning-in-keras.html 该博客的完整...
  • Seq2Seq 模型简介

    2020-10-10 12:03:51
    使用LSTM构建序列序列模型,介绍Seq2Seq。
  • 在这篇文章中,我们将构建一个基于LSTM的Seq2Seq模型,使用编码器-解码器架构进行机器翻译。 本篇文章内容: 介绍 数据准备和预处理 长短期记忆(LSTM) - 背景知识 编码器模型架构(Seq2Seq) 编码器代码实现(Seq2Seq)...
  • 在文章里会引用较的博客,文末会进行reference。 搜索Transformer机制,会发现高分结果基本上都源于一篇论文Jay Alammar的《The Illustrated Transformer》(图解Transformer),提到最多的Attention是Google的...
  • 从Encoder到Decoder实现Seq2Seq模型

    千次阅读 2019-04-06 10:24:55
    首发于机器不学习关注专栏写文章从Encoder到Decoder实现Seq2Seq模型天雨粟模型师傅 / 果粉​关注他300 人赞同了该文章更新:感谢@Gang He指出的代码错误。get_batches函数中第15行与第19行,代码已经重新修改,...
  • 如果要查看图文版教程,请到 http://studyai.com/pytorch-1.4/intermediate/seq2seq_translation_tutorial.html 在这个项目中,我们将教一个神经网络,把法语翻译成英语。 [KEY: > input, = target, < output]...
  • seq2seq 和 Encoder-Decoder基本相同,只不过后者是一种抽象概念,前者是具体的模型,seq2seq可以看做是一种结构,有很这种结构的模型。seq2seq有多种类型,N VS N,N vs 1, 1vs N, N vs M。最后一种应用最广。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,108
精华内容 1,643
关键字:

多步骤序列预测的seq2seq