卷积神经网络可以有效地处理空间信息, 循环神经网络(recurrent neural network,RNN)可以更好地处理序列信息。 循环神经网络通过引入状态变量存储过去的信息和当前的输入,从而确定当前的输出。
考虑一种情况,我们已经有了以往多天的股票数据,现在需要预测未来的股票走势,为了解决这个问题,可以使用回归模型,其中,用 表示价格,在时间步(time step) 时,观察到的价格 。假设一个交易员想在 日的股市中表现良好,于是通过以下途径预测 :
自回归模型 为了实现这个预测,交易员可以使用回归模型,只有一个主要问题:我们每次预测第 天的股票数据,依靠 天的数据,那么当 变化时,输入数据的数量也会变化;处理这种不同输入数量的问题,主要有以下两种策略:
第一种策略:认为使用全部 天的数据是没必要的,只需要前固定天数的数据进行预测,忽略更早的数据;使用观测序列 ,长度为 的时间跨度;好处是在 时,参数数量总量不变,这就使我们能够训练一个上面提及的深度网络。这种模型被称为自回归模型 (autoregressive models),因为它们是对自己执行回归。
自回归是指 只用 自身历史数据进行预测;
与自回归相对的概念有:外生回归:比如利用风扇转速、室外温度预测室内温度;
第二种策略,如图所示,通过总结过去的数据得到状态 ,然后通过过去状态和数据更新状态 ,并生成新的预测 。这就产生了基于$\hat{x}t = P(x_t \mid h {t})估 计 x_t, 以 及 公 式 h_t = g(h_{t-1}, x_{t-1})更 新 的 模 型 。 由 于 h_t$从未被观测到,这类模型也被称为隐变量自回归模型 (latent autoregressive models)。
马尔可夫模型 马尔科夫性 当一个随机过程在给定「当前状态」时,「未来状态」的概率分布,与「过去所有历史状态」完全无关,只由当前状态决定。「未来只依赖于现在,和过去无关」,这个性质就叫做马尔科夫性;
左边:用「从初始到当前的全部历史」预测未来 右边:只用「当前状态」预测未来 等式成立:说明历史信息对预测未来没有额外价值 ,当前状态已经包含了所有必要信息
回想一下,在自回归模型的近似法中,我们使用长度为 的序列 而不是 来估计 。如果这种近似截取不影响预测精度,我们就说序列满足马尔可夫条件 (Markov condition)。特别是,如果 ,得到一个一阶马尔可夫模型 (first-order Markov model), 由下式给出:
当
一阶马尔可夫性的核心简化 一阶马尔可夫性(无后效性)给出了一个强假设:
未来只依赖于上一时刻,与更早的历史完全无关
把这个假设代入链式法则,所有「依赖全部历史」的项,都被简化为「仅依赖上一时刻」的项:
当假设 仅是离散值时,这样的模型特别棒,因为在这种情况下,使用动态规划可以沿着马尔可夫链精确地计算结果。例如,我们可以高效地计算 :
下面的式子,前几步推导天然成立,只有最后一步,必须要求满足马尔科夫条件才成立;
根 据 一 阶 马 尔 可 夫 近 似
因果关系 原则上,将 倒序展开也没什么问题。毕竟,基于条件概率公式,我们总是可以写出:
这是正常的正向链式分解,同样,如果从时刻 开始向前分解,可以得到
事实上,如果基于一个马尔可夫模型, 我们还可以得到一个反向的条件概率分布。然而,在许多情况下,数据存在一个自然的方向,即在时间上是前进的。很明显,未来的事件不能影响过去。因此,如果我们改变 ,可能会影响未来发生的事情 ,但不能反过来。也就是说,如果我们改变 ,基于过去事件得到的分布不会改变。因此,解释 应该比解释 更容易。例如,在某些情况下,对于某些可加性噪声 ,显然我们可以找到 ,而反之则不行。
模型训练 1 2 3 4 5 6 7 8 9 10 import torchfrom torch import nnT = 1000 time = torch.arange(1 , T + 1 , dtype=torch.float32) x = torch.sin(0.01 * time) + torch.normal(0 , 0.2 , (T,)) plot(time, x, y_name='x' , x_name='time' , xlim=[1 , 1000 ], file_name="plot_series.png" )
接下来,我们将这个序列转换为模型的特征-标签 (feature-label)对。基于嵌入维度 ,我们[将数据映射为数据对 和$\mathbf{x}t = [x {t-\tau}, \ldots, x_{t-1}]。 这 比 我 们 提 供 的 数 据 样 本 少 了 \tau个 , 因 为 我 们 没 有 足 够 的 历 史 记 录 来 描 述 前 \tau$个数据样本。 一个简单的解决办法是:如果拥有足够长的序列就丢弃这几项;另一个方法是用零填充序列。在这里,我们仅使用前600个“特征-标签”对进行训练。
1 2 3 4 5 6 7 8 tau = 4 features = torch.zeros((T - tau, tau)) for i in range (tau): features[:, i] = x[i: T - tau + i] labels = x[tau:].reshape((-1 , 1 ))
1 2 3 4 batch_size, n_train = 16 , 600 train_iter = load_array((features[:n_train], labels[:n_train]), batch_size, is_train=True )
在这里,我们使用一个相当简单的架构训练模型:一个拥有两个全连接层的多层感知机,ReLU激活函数和平方损失。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def init_weights (m ): if type (m) == nn.Linear: nn.init.xavier_uniform_(m.weight) def get_net (): net = nn.Sequential(nn.Linear(4 , 10 ), nn.ReLU(), nn.Linear(10 , 1 )) net.apply(init_weights) return net loss = nn.MSELoss(reduction='none' )
现在,准备[训练模型]了。实现下面的训练代码的方式与前面几节中的循环训练基本相同。因此,我们不会深入探讨太多细节。
1 2 3 4 5 6 7 8 9 10 11 12 13 def train (net, train_iter, loss, epochs, lr ): trainer = torch.optim.Adam(net.parameters(), lr) for epoch in range (epochs): for X, y in train_iter: trainer.zero_grad() l = loss(net(X), y) l.sum ().backward() trainer.step() print (f'epoch {epoch + 1 } , ' f'loss: {evaluate_loss(net, train_iter, loss):f} ' ) net = get_net() train(net, train_iter, loss, 5 , 0.01 )
预测 由于训练损失很小,因此我们期望模型能有很好的工作效果。让我们看看这在实践中意味着什么。首先是检查模型预测下一个时间步的能力,也就是单步预测 (one-step-ahead prediction)。
1 2 3 4 5 6 7 8 9 10 net = get_net() train(net, train_iter, loss, 5 , 0.01 ) onestep_preds = net(features) plot([time, time[tau:]], [x.detach().numpy(), onestep_preds.detach().numpy()], legend=['data' , '1-step preds' ], x_name='time' , y_name='x' , xlim=[1 , 1000 ], figsize=(6 , 3 ), file_name="plot_series_1step.png" )
可以看出单步预测效果不错。即使这些预测的时间步超过了 (n_train + tau),其结果看起来仍然是可信的。然而有一个小问题:我们前面继续宁预测时,使用的历史数据都是观测得到的;如果数据观察序列的时间步只到 ,我们需要一步一步地向前迈进:
$$ \hat{x}{605} = f(x {601}, x_{602}, x_{603}, x_{604}), \ \hat{x}{606} = f(x {602}, x_{603}, x_{604}, \hat{x}{605}), \ \hat{x} {607} = f(x_{603}, x_{604}, \hat{x}{605}, \hat{x} {606}),\ \hat{x}{608} = f(x {604}, \hat{x}{605}, \hat{x} {606}, \hat{x}{607}),\ \hat{x} {609} = f(\hat{x}{605}, \hat{x} {606}, \hat{x}{607}, \hat{x} {608}),\ \ldots $$
通常,对于直到 的观测序列,其在时间步 处的预测输出$\hat{x}{t+k}称 为 k$步预测 ( -step-ahead-prediction)。由于我们的观察已经到了$x {604}, 它 的 k步 预 测 是 \hat{x}_{604+k}$。换句话说,我们必须使用曾经的预测数据而不是观测到的实际数据来进行多步预测。让我们看看效果如何。
1 2 3 4 5 6 7 8 9 10 11 12 multistep_preds = torch.zeros(T) multistep_preds[: n_train + tau] = x[: n_train + tau] for i in range (n_train + tau, T): multistep_preds[i] = net( multistep_preds[i - tau:i].reshape((1 , -1 ))) plot([time, time[tau:], time[n_train + tau:]], [x.detach().numpy(), onestep_preds.detach().numpy(), multistep_preds[n_train + tau:].detach().numpy()], legend=['data' , '1-step preds' , 'multistep preds' ], x_name='time' , y_name='x' , xlim=[1 , 1000 ], figsize=(6 , 3 ),file_name="plot_series_multi_step.png" )
如上面的例子所示,绿线的预测显然并不理想。经过几个预测步骤之后,预测的结果很快就会衰减到一个常数。为什么这个算法效果这么差呢?事实是由于错误的累积:假设在步骤 之后,我们积累了一些错误 。于是,步骤 的输入被扰动了 ,结果积累的误差是依照次序的 ,其中 为某个常数,后面的预测误差依此类推。因此误差可能会相当快地偏离真实的观测结果。例如,未来 小时的天气预报往往相当准确,但超过这一点,精度就会迅速下降。我们将在本章及后续章节中讨论如何改进这一点。
基于 ,通过对整个序列预测的计算,让我们更仔细地看一下 步预测的困难。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 max_steps = 64 features = torch.zeros((T - tau - max_steps + 1 , tau + max_steps)) for i in range (tau): features[:, i] = x[i: i + T - tau - max_steps + 1 ] for i in range (tau, tau + max_steps): features[:, i] = net(features[:, i - tau:i]).reshape(-1 ) steps = (1 , 4 , 16 , 64 ) plot([time[tau + i - 1 : T - max_steps + i] for i in steps], [features[:, (tau + i - 1 )].detach().numpy() for i in steps], 'time' , 'x' , legend=[f'{i} -step preds' for i in steps], xlim=[5 , 1000 ], figsize=(6 , 3 ),file_name="plot_series_multi_step_forward.png" )
以上例子清楚地说明了当我们试图预测更远的未来时,预测的质量是如何变化的。虽然“ 步预测”看起来仍然不错,但是更远的预测质量严重下降。
小结 内插法(在现有观测值之间进行估计)和外推法(对超出已知观测范围进行预测)在实践的难度上差别很大。因此,对于所拥有的序列数据,在训练时始终要尊重其时间顺序,即最好不要基于未来的数据进行训练。 序列模型的估计需要专门的统计工具,两种较流行的选择是自回归模型和隐变量自回归模型。 对于时间是向前推进的因果模型,正向估计通常比反向估计更容易。 对于直到时间步 的观测序列,其在时间步 的预测输出是“ 步预测”。随着我们对预测时间 值的增加,会造成误差的快速累积和预测质量的极速下降。