想象一下你正在看 Netflix(一个国外的视频网站)上的电影。作为一个很棒的 Netflix 用户,你决定对每一部电影都给出评价。毕竟,一部好的电影值得好电影的称呼,而且你想看更多的好电影,对吧?事实证明,事情并不那么简单。随着时间的推移,人们对电影的看法会发生很大的变化。事实上,心理学家甚至对某些效应起了名字:
(Wu.Ahmed.Beutel.ea.2017)
. 简而言之,电影评分决不是固定不变的。因此,使用时间动力学可以得到更准确的电影推荐 :(Koren.2009
) 。当然,序列数据不仅仅是关于电影评分的。下面给出了更多的场景。
总结:
我们需要统计工具和新的深层神经网络结构来处理序列数据。为了简单起见,我们以下图所示的股票价格(富时100指数)为例。
让我们用 x t x_t xt 表示价格。即在 时间步(time step) t ∈ Z + t \in \mathbb{Z}^+ t∈Z+时,我们观察到的价格 x t x_t xt。请注意,对于本文中的序列, t t t 通常是离散的,并随整数或其子集而变化。假设一个交易员想在 t t t 日股市表现良好的,于是通过以下途径预测了 x t x_t xt:
x t ∼ P ( x t ∣ x t − 1 , … , x 1 ) . x_t \sim P(x_t \mid x_{t-1}, \ldots, x_1). xt∼P(xt∣xt−1,…,x1).
为了实现这一点,交易员可以使用回归模型,比如我们在 线性回归模型。只有一个主要问题:输入 x t − 1 , … , x 1 x_{t-1}, \ldots, x_1 xt−1,…,x1 的数量因 t t t 而异。也就是说,这个数字将会随着我们遇到的数据量的增加而增加,因此我们需要一个近似方法来使这个计算变得容易处理。本章后面的大部分内容将围绕着如何有效估计 P ( x t ∣ x t − 1 , … , x 1 ) P(x_t \mid x_{t-1}, \ldots, x_1) P(xt∣xt−1,…,x1) 展开。简单地说,它归结为以下两种策略。
第一种策略,假设在现实情况下相当长的序列 x t − 1 , … , x 1 x_{t-1}, \ldots, x_1 xt−1,…,x1 可能是不需要的,因此我们只使用观测序列 x t − 1 , … , x t − τ x_{t-1}, \ldots, x_{t-\tau} xt−1,…,xt−τ,并且满足于时间跨度为 τ \tau τ。现在,获得的最直接的好处就是对于 t > τ t > \tau t>τ 时参数的数量总是相同的,这就使我们能够训练一个上面提及的深层网络。这种模型被称为 自回归模型(autoregressive models),因为它们就是对自己执行回归。
第二种策略,如下图所示,是保留一些过去观测的总计 h t h_t ht,同时除了预测 x ^ t \hat{x}_t x^t 之外还更新 h t h_t ht。这就产生了估计 x t x_t xt 和 x ^ t = P ( x t ∣ h t ) \hat{x}_t = P(x_t \mid h_{t}) x^t=P(xt∣ht) 的模型,并且更新了 h t = g ( h t − 1 , x t − 1 ) h_t = g(h_{t-1}, x_{t-1}) ht=g(ht−1,xt−1)。由于 h t h_t ht 从未被观测到,这类模型也被称为 隐变量自回归模型(latent autoregressive models)。
这两种情况都有一个显而易见的问题,即如何生成训练数据。一个经典的方法是使用历史观测来预测下一次的观测。显然,我们并不指望时间会停滞不前。然而,一个常见的假设是序列本身的动力学不会改变,虽然特定值 x t x_t xt 可能会改变。这样的假设是合理的,因为新的动力学一定受新数据影响,而我们不可能用目前所掌握的数据来预测新的动力学。统计学家称不变的动力学为 静止的(stationary)。因此,无论我们做什么,整个序列的估计值都将通过以下的方式获得
P ( x 1 , … , x T ) = ∏ t = 1 T P ( x t ∣ x t − 1 , … , x 1 ) . P(x_1, \ldots, x_T) = \prod_{t=1}^T P(x_t \mid x_{t-1}, \ldots, x_1). P(x1,…,xT)=t=1∏TP(xt∣xt−1,…,x1).
注意,如果我们处理离散的对象(如单词),而不是连续的数字,则上述的考虑仍然有效。唯一的差别是,在这种情况下,我们需要使用分类器而不是回归模型来估计 P ( x t ∣ x t − 1 , … , x 1 ) P(x_t \mid x_{t-1}, \ldots, x_1) P(xt∣xt−1,…,x1)。
回想一下,在自回归模型的逼近方法中,我们使用 x t − 1 , … , x t − τ x_{t-1}, \ldots, x_{t-\tau} xt−1,…,xt−τ 而不是 x t − 1 , … , x 1 x_{t-1}, \ldots, x_1 xt−1,…,x1 来估计 x t x_t xt。只要这种近似是准确的,我们就说序列满足马尔可夫条件(Markov condition)。特别是,如果 τ = 1 \tau = 1 τ=1,得到一个一阶马尔可夫模型(first-order Markov model), P ( x ) P(x) P(x) 由下式给出:
P ( x 1 , … , x T ) = ∏ t = 1 T P ( x t ∣ x t − 1 ) where P ( x 1 ∣ x 0 ) = P ( x 1 ) . P(x_1, \ldots, x_T) = \prod_{t=1}^T P(x_t \mid x_{t-1}) \text{ where } P(x_1 \mid x_0) = P(x_1). P(x1,…,xT)=t=1∏TP(xt∣xt−1) where P(x1∣x0)=P(x1).
当 x t x_t xt 只假设离散值时,这样的模型特别棒,因为在这种情况下,使用动态规划可以沿着马尔可夫链精确地计算结果。例如,我们可以高效地计算 P ( x t + 1 ∣ x t − 1 ) P(x_{t+1} \mid x_{t-1}) P(xt+1∣xt−1):
P ( x t + 1 ∣ x t − 1 ) = ∑ x t P ( x t + 1 , x t , x t − 1 ) P ( x t − 1 ) = ∑ x t P ( x t + 1 ∣ x t , x t − 1 ) P ( x t , x t − 1 ) P ( x t − 1 ) = ∑ x t P ( x t + 1 ∣ x t ) P ( x t ∣ x t − 1 ) \begin{aligned} P(x_{t+1} \mid x_{t-1}) &= \frac{\sum_{x_t} P(x_{t+1}, x_t, x_{t-1})}{P(x_{t-1})}\\ &= \frac{\sum_{x_t} P(x_{t+1} \mid x_t, x_{t-1}) P(x_t, x_{t-1})}{P(x_{t-1})}\\ &= \sum_{x_t} P(x_{t+1} \mid x_t) P(x_t \mid x_{t-1}) \end{aligned} P(xt+1∣xt−1)=P(xt−1)∑xtP(xt+1,xt,xt−1)=P(xt−1)∑xtP(xt+1∣xt,xt−1)P(xt,xt−1)=xt∑P(xt+1∣xt)P(xt∣xt−1)
利用这一事实,我们只需要考虑过去观察到的非常短的历史: P ( x t + 1 ∣ x t , x t − 1 ) = P ( x t + 1 ∣ x t ) P(x_{t+1} \mid x_t, x_{t-1}) = P(x_{t+1} \mid x_t) P(xt+1∣xt,xt−1)=P(xt+1∣xt)。详细介绍动态规划超出了本节的范围。控制算法和强化学习算法广泛使用这些工具。
原则上,倒序展开 P ( x 1 , … , x T ) P(x_1, \ldots, x_T) P(x1,…,xT) 无可厚非。毕竟,基于条件概率公式,我们总是可以写出:
P ( x 1 , … , x T ) = ∏ t = T 1 P ( x t ∣ x t + 1 , … , x T ) . P(x_1, \ldots, x_T) = \prod_{t=T}^1 P(x_t \mid x_{t+1}, \ldots, x_T). P(x1,…,xT)=t=T∏1P(xt∣xt+1,…,xT).
事实上,如果基于一个马尔可夫模型,我们可以得到一个反向的条件概率分布。然而,在许多情况下,数据存在一个自然的方向,即在时间上是前进的。很明显,未来的事件不能影响过去。因此,如果我们改变 x t x_t xt,我们可能能够影响 x t + 1 x_{t+1} xt+1 未来发生的事情,但不能影响过去。也就是说,如果我们改变 x t x_t xt,基于过去事件的分布不会改变。因此,解释 P ( x t + 1 ∣ x t ) P(x_{t+1} \mid x_t) P(xt+1∣xt) 应该比解释 P ( x t ∣ x t + 1 ) P(x_t \mid x_{t+1}) P(xt∣xt+1) 更容易。例如,在某些情况下,对于某些可加性噪声 ϵ \epsilon ϵ,显然我们可以找到 x t + 1 = f ( x t ) + ϵ x_{t+1} = f(x_t) + \epsilon xt+1=f(xt)+ϵ,而反之则不行 (Hoyer.Janzing.Mooij.ea.2009
) 。这是个好消息,因为这通常是我们有兴趣估计的前进方向。彼得斯等人写的这本书。已经解释了关于这个主题的更多内容 (Peters.Janzing.Scholkopf.2017
) 。我们仅仅触及了它的皮毛。
在回顾了这么多统计工具之后,让我们在实践中尝试一下。首先,生成一些数据。为了简单起见,我们使用正弦函数和一些可加性噪声来生成序列数据,时间步为 1 , 2 , … , 1000 1, 2, \ldots, 1000 1,2,…,1000。
%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l
T = 1000
time = torch.arange(1,T+1,dtype=torch.float32)
x = torch.sin(0.01*time)+torch.normal(0,0.2,(T,))
d2l.plot(time,[x],'time','x',xlim=[1,1000],figsize=(6,3))
接下来,我们需要将这样的序列转换为我们的模型可以训练的特征和标签。基于嵌入维度 τ \tau τ,我们将数据映射为 y t = x t y_t = x_t yt=xt 和 x t = [ x t − τ , … , x t − 1 ] \mathbf{x}_t = [x_{t-\tau}, \ldots, x_{t-1}] xt=[xt−τ,…,xt−1]。精明的读者可能已经注意到,这比我们提供的数据样本少了 τ \tau τ 个,因为我们没有足够的历史记录来描述前 τ \tau τ 个数据样本。一个简单的解决办法,特别是序列如果够长就丢弃这几项,或者可以用零填充序列。在这里,我们仅使用前600个“特征-标签”对进行训练。
# 将数据映射为数据对,构造输入(996,4),将当前值作为标签,而前面四个值作为当前值的输入
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))
batch_size, n_train = 16, 600
# 只有前`n_train`个样本用于训练
train_iter = d2l.load_array((features[:n_train], labels[:n_train]),batch_size, is_train=True)
设计一个简单的多层感知机,有两个全连接层,ReLU激活函数和平方损失
# 初始化网络参数
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
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.backward()
trainer.step()
print(f'eopoch {epoch + 1},'
f'loss:{d2l.evaluate_loss(net,train_iter,loss):f}')
net = get_net()
loss = nn.MSELoss()
train(net,train_iter,loss,5,0.01)
eopoch 1,loss:0.064053
eopoch 2,loss:0.056895
eopoch 3,loss:0.056115
eopoch 4,loss:0.057444
eopoch 5,loss:0.056819
由于训练损失很小,我们希望模型能够很好地工作。让我们看看这在实践中意味着什么。首先是检查模型对发生在下一个时间步的事情的预测能力有多好,也就是 单步预测(one-step-ahead prediction)。
onestep_preds = net(features) # 将全部数据放进网络进行预测,但是实际上有很多时候我们是需要根据预测的结果来进行预测的
d2l.plot(
[time, time[tau:]],
[x.detach().numpy(), onestep_preds.detach().numpy()], 'time', 'x',
legend=['data', '1-step preds'], xlim=[1, 1000], figsize=(6, 3))
正如我们所料的单步预测效果不错。即使这些预测的时间步超过了 604 604 604(n_train + tau
),其结果看起来仍然是可信的。然而有一个小问题:如果数据观察序列的时间步只到 604 604 604,那么我们没有期望能够接收到所有提前一步预测的未来输入。相反,我们需要一步一步地向前迈进:
x ^ 605 = f ( x 601 , x 602 , x 603 , x 604 ) , x ^ 606 = f ( x 602 , x 603 , x 604 , x ^ 605 ) , x ^ 607 = f ( x 603 , x 604 , x ^ 605 , x ^ 606 ) , x ^ 608 = f ( x 604 , x ^ 605 , x ^ 606 , x ^ 607 ) , x ^ 609 = f ( x ^ 605 , x ^ 606 , x ^ 607 , x ^ 608 ) , … \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 x^605=f(x601,x602,x603,x604),x^606=f(x602,x603,x604,x^605),x^607=f(x603,x604,x^605,x^606),x^608=f(x604,x^605,x^606,x^607),x^609=f(x^605,x^606,x^607,x^608),…
通常,对于直到 x t x_t xt 的观测序列,其在时间步长 x ^ t + k \hat{x}_{t+k} x^t+k 处的预测输出 t + k t+k t+k 被称为 k k k 步预测( k k k-step-ahead-prediction)。由于我们已经观察到了 x 604 x_{604} x604,它领先 k k k 步的预测是 x ^ 604 + k \hat{x}_{604+k} x^604+k。换句话说,我们将不得不使用自己的预测来进行多步预测。让我们看看这件事进行的是否顺利。
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)))
d2l.plot([time, time[tau:], time[n_train + tau:]], [
x.detach().numpy(),
onestep_preds.detach().numpy(),
multistep_preds[n_train + tau:].detach().numpy()], 'time', 'x',
legend=['data', '1-step preds',
'multistep preds'], xlim=[1, 1000], figsize=(6, 3))
正如上面的例子所示,这是一个巨大的失败。在几个预测步骤之后,预测结果很快就会衰减到一个常数。为什么这个算法效果这么差呢?最终事实是由于错误的累积。
例如,未来24小时的天气预报往往相当准确,但超过这一点,准确率就会迅速下降。我们将在本章及以后讨论改进这一点的方法。让我们通过计算 k = 1 , 4 , 16 , 64 k = 1, 4, 16, 64 k=1,4,16,64 的整个序列的预测来更仔细地看一下 k k k 步预测的困难。
max_steps = 64
features = torch.zeros((T - tau - max_steps + 1, tau + max_steps))
# 列 `i` (`i` < `tau`) 是来自 `x` 的观测
# 其时间步从 `i + 1` 到 `i + T - tau - max_steps + 1`
for i in range(tau):
features[:, i] = x[i:i + T - tau - max_steps + 1]
# 列 `i` (`i` >= `tau`) 是 (`i - tau + 1`)步的预测
# 其时间步从 `i + 1` 到 `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)
d2l.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))
这清楚地说明了当我们试图进一步预测未来时,预测的质量是如何变化的。虽然“ 4 4 4 步预测”看起来仍然不错,但超过这个跨度的任何预测几乎都是无用的。