文章来源:NeurIPS 2023
推荐指数:⭐⭐⭐⭐⭐
评价:很让人耳目一新的文章,想法易懂,逻辑清晰
关键词:时间序列预测、概念漂移、离线强化学习
Concept Drift,Test-Time adaptive methods,Time Series Modeling,Offline reinforcement learning.
好综合的一篇文章,好难理解的一篇文章
目录
摘要
时间序列预测模型的在线更新旨在通过有效地更新基于流媒体数据的预测模型来解决概念漂移问题。
许多算法都是为在线时间序列预测而设计的,一些算法利用跨变量依赖性,而另一些算法则假设变量之间的独立性。考虑到每个数据假设在在线时间序列建模中都有自己的优缺点,我们提出了在线集成网络(OneNet)。它动态地更新并结合了两个模型,一个关注于跨时间维度的依赖关系建模,另一个关注于跨变量依赖关系建模。
我们的方法将基于强化学习的方法合并到传统的在线凸规划框架中,允许两个模型与动态调整权重的线性组合。
OneNet解决了经典在线学习方法的主要缺点,这些方法在适应概念漂移方面往往比较缓慢。实证结果表明,与最先进的(SOTA)方法相比,OneNet的在线预测误差降低了50%以上。
代码在https://github.com/yfzhang114/OneNet.获得
介绍
可视化时间步损失,观察到了概念偏移。有种损失分阶段了的感觉,比如一个阶段损失比较大,下一个阶段损失整体又变小,可能说明数据分布在不同的阶段发生了变化,如果是看不出局部趋势和明显的不同阶段的,说明没有发生概念偏移,反之,则说明发生了概念偏移。
分析之前的实验结果,证明变量独立性确实能使得模型更加稳健,但是变量相关性对于预测也必不可少,如何解决这个矛盾的问题呢?图一中可以看出两个模型的表现是“此起彼伏”的,那就想办法利用两种思路来互补,然而单一模型如Crossformer无法同时兼顾变量依赖性和时间依赖性,甚至表现还会下降,因此文章提出了在线集成网络(OneNet)。它动态地更新并结合了两个模型,一个关注于跨时间维度的依赖关系建模,另一个关注于跨变量依赖关系建模。并且在测试阶段,还使用强化学习方法动态调整用于结合两个模型预测结果的权重。
贡献:
- 我们介绍了OneNet,一种用于在线时间序列预测的双流架构,它使用在线凸优化集成了两个模型的输出。OneNet在处理概念漂移时利用了变量独立模型的鲁棒性,同时也捕获了不同变量之间的相互依赖性,以提高预测精度。此外,我们提出了一种基于强化的在线学习方法,以减轻传统的OCP算法的局限性,并通过实证和理论分析证明了它的有效性。
- 我们对四个数据集的实证研究表明,与最先进的方法相比,OneNet将平均累积均方误差(MSE)降低了53.1%,将平均绝对误差(MAE)降低了34.5%。特别是,在具有挑战性的数据集ECL上
的性能增益更优越,其中MSE降低了59.2%,MAE降低了63.0%。 - 我们进行了全面的实证研究,以调查预测模型中常用的设计选择,如实例归一化、变量独立性、季节趋势分解和频域增强,如何影响模型的鲁棒性。此外,我们系统地比较了现有的基于Transformer的模型、基于TCN的模型和基于MLP的模型在面对概念漂移时的鲁棒性。
Onenet
双流预测器
双预测器
如图所示,输入序列被送入两个预测器,cross-time 预测器f1,cross-variable 预测器 f2,每个预测器都包括一个encoder 和prediction head。
假设模型的隐藏层维度都是 d m d_{m} dm,那么corss-time 预测器 f1的encoder将会把输入序列映射成 z 1 ∈ R M × d m z_{1} \in \mathbb{R}^{M \times d_{m}} z1∈RM×dm, 然后prediction head会生成最终的预测结果 y 1 ∈ R M × H y_{1} \in \mathbb{R}^{M \times H} y1∈RM×H.
对于cross-variable 预测器 f2,encoder将会把输入序列映射成 z 2 ∈ R L × d m z_{2} \in \mathbb{R}^{L \times d_{m}} z2∈RL×dm。 然后选择最后一个时间步的表示 z 2 , L ∈ R d m z_{2,L} \in \mathbb{R}^{d_{m}} z2,L∈Rdm送入prediction head ,生成最终的预测结果 y 1 ∈ R M × H y_{1} \in \mathbb{R}^{M \times H} y1∈RM×H.
f1 :的参数
d
m
×
H
d_{m} \times H
dm×H
f2 :的参数
d
m
×
M
×
H
d_{m} \times M \times H
dm×M×H
f1 是直接忽略变量相关性 ,而 f2 则通过选择最后一个时间步的表示来排除时间依赖性的影响。
OCP
OCP被用于学习最佳的权重组合,使用EGD(指数梯度下降)来更新每个预测器的权重 w i w_{i} wi ,使用离线强化学习来学习附加的short-term权重 b i b_{i} bi,然后更新预测器的权重 w i ← w i + b i w_{i} \leftarrow w_{i}+b_{i} wi←wi+bi。考虑到变量之间的差异性,作者为每个变量构建了不同的权重,也就是说 组合权重 w ∈ R M × 2 w \in \mathbb{R}^{M \times 2} w∈RM×2 ,M是变量的数量。
解耦训练策略
一句话,模型权重组合和模型参数各学各的。
源码(fsnet)
双流架构
变量独立性和变量依赖性
也就是一个的输入沿着序列方向(model_time),另一个的输入沿着变量方向(model_var)
def __init__(self, args, device):
super().__init__()
self.device = device
depth = 10
encoder = TSEncoder(input_dims=args.seq_len,
output_dims=320, # standard ts2vec backbone value
hidden_dims=64, # standard ts2vec backbone value
depth=depth)
self.encoder_time = TS2VecEncoderWrapper(encoder, mask='all_true').to(self.device)
self.regressor_time = nn.Linear(320, args.pred_len).to(self.device)
encoder = TSEncoder(input_dims=args.enc_in + 7,
output_dims=320, # standard ts2vec backbone value
hidden_dims=64, # standard ts2vec backbone value
depth=depth)
self.encoder = TS2VecEncoderWrapper(encoder, mask='all_true').to(self.device)
self.dim = args.c_out * args.pred_len
self.regressor = nn.Linear(320, self.dim).to(self.device)
上面代码的self.encoder_time和self.encoder都是借助TS2VecEncoderWrapper类生成的,输入参数的差别就是两个encoder了,这两个encoder的差别就是input_dims的差别,model_time的输入维度是args.seq_len,model_var的输入维度是args.enc_in+7
为什么是enc_in+7? 加7加了x_mark,7个时序特征。
x = torch.cat([x, x_mark], dim=-1)
rep2 = self.encoder(x)[:, -1]
TSEncoder
class TSEncoder(nn.Module):
def __init__(self, input_dims, output_dims, hidden_dims=64, depth=10, mask_mode='binomial', gamma=0.9):
super().__init__()
self.input_dims = input_dims
self.output_dims = output_dims
self.hidden_dims = hidden_dims
self.mask_mode = mask_mode
self.input_fc = nn.Linear(input_dims, hidden_dims)
self.feature_extractor = DilatedConvEncoder(
hidden_dims,
[hidden_dims] * depth + [output_dims],
kernel_size=3, gamma=gamma
)
self.repr_dropout = nn.Dropout(p=0.1)
feature_extractor 堆叠了十个(depth=10)空洞卷积编码器 for i in range(len(channels))
这里还使用了一个二项分布掩码mask_mode='binomial'
,按照二项分布生成遮挡掩码,遮挡输入的部分元素,增强模型的推断和泛化能力。(某种程度上是数据层面的dropout)
DiatedConvEncoder
class DilatedConvEncoder(nn.Module):
def __init__(self, in_channels, channels, kernel_size, gamma=0.9):
super().__init__()
self.net = nn.Sequential(*[
ConvBlock(
channels[i-1] if i > 0 else in_channels,
channels[i],
kernel_size=kernel_size,
dilation=2**i,
final=(i == len(channels)-1), gamma=gamma
)
for i in range(len(channels))
])
模型
居然不是transformer而是卷积,10个卷积层加一个预测头
self.conv = nn.Conv1d(
in_channels, out_channels, kernel_size,
padding=padding,
dilation=dilation,
groups=groups, bias=False
)
for p in self.conv.parameters():
self.grad_dim.append(p.numel())
self.shape.append(p.size())
self.grads = torch.Tensor(sum(self.grad_dim)).fill_(0).cuda()
训练参数
可学习的参数可以分为两个部分,第一部分是针对模型本身的,第二部分是针对两个模型之间的参数的 w是权重,使用EGD方法来更新,opt_bias是短期权重,使用强化学习方法来更新
self.opt = self._select_optimizer()
self.opt_w = optim.Adam([self.weight], lr=self.args.learning_rate_w)#创建对weight的优化器
self.opt_bias = optim.Adam(self.decision.parameters(), lr=self.args.learning_rate_bias)
Loss更新model参数,在train函数里面
w和b的更新是在pred, true, loss_w, loss_bias = self._process_one_batch( train_data, batch_x, batch_y, batch_x_mark, batch_y_mark)
的_process_one_batch函数里面
OCP
主要是对w的更新,就挺复杂的,先把源代码放在这里
def _process_one_batch(self, dataset_object, batch_x, batch_y, batch_x_mark, batch_y_mark, mode='train'):
# print(self.weight[0], self.bias[0])
if mode =='test' and self.online != 'none':
return self._ol_one_batch(dataset_object, batch_x, batch_y, batch_x_mark, batch_y_mark)
#准备数据
x = batch_x.float().to(self.device) #torch.cat([batch_x.float(), batch_x_mark.float()], dim=-1).to(self.device)
batch_x_mark = batch_x_mark.float().to(self.device)
batch_y = batch_y.float()
b, t, d = batch_y.shape
'''更新w,即self.weight'''
if self.individual:
loss1 = F.sigmoid(self.weight).view(1, 1, -1)
loss1 = loss1.repeat(b, t, 1)#可以不用repeat函数改成广播机制么?
loss1 = rearrange(loss1, 'b t d -> b (t d)')
outputs, y1, y2 = self.model.forward_weight(x, batch_x_mark, loss1, 1 - loss1)#模型输出,f1输出,f2输出
f_dim = -1 if self.args.features=='MS' else 0
batch_y = batch_y[:,-self.args.pred_len:,f_dim:].to(self.device)
b, t, d = batch_y.shape
criterion = self._select_criterion()
l1, l2 = criterion(y1, rearrange(batch_y, 'b t d -> b (t d)')), criterion(y2, rearrange(batch_y, 'b t d -> b (t d)'))#f1和f2的损失
loss_w = criterion(outputs, rearrange(batch_y, 'b t d -> b (t d)'))
loss_w.backward()
self.opt_w.step()
self.opt_w.zero_grad()
'''更新b,即self.bias,需要用到self.weight'''
if self.individual:
y1_w, y2_w = y1.view(b, t, d).detach(), y2.view(b, t, d).detach()
true_w = batch_y.view(b, t, d).detach()
loss1 = F.sigmoid(self.weight).view(1, 1, -1)
loss1 = loss1.repeat(b, t, 1)
inputs_decision = torch.cat([loss1*y1_w, (1-loss1)*y2_w, true_w], dim=1)
self.bias = self.decision(inputs_decision.permute(0,2,1))
weight = self.weight.view(1, 1, -1)
weight = weight.repeat(b, t, 1)
bias = self.bias.view(b, 1, -1)
loss1 = F.sigmoid(weight + bias.repeat(1, t, 1))
loss1 = rearrange(loss1, 'b t d -> b (t d)')
loss2 = 1 - loss1
y1_w = rearrange(y1_w, 'b t d -> b (t d)')
y2_w = rearrange(y2_w, 'b t d -> b (t d)')
true_w = rearrange(true_w, 'b t d -> b (t d)')
loss_bias = criterion(loss1 * y1_w + loss2 * y2_w, true_w)
loss_bias.backward()
self.opt_bias.step()
self.opt_bias.zero_grad()
return [y1, y2], rearrange(batch_y, 'b t d -> b (t d)'), loss_w.detach().cpu().item(), loss_bias.detach
weight 先简单更新w
将weight repeat 之后得到loss1,传入forward_weight
计算output
outputs, y1, y2 = self.model.forward_weight(x, batch_x_mark, loss1, 1 - loss1)
def forward_weight(self, x, x_mark, g1, g2):
rep = self.encoder_time.encoder.forward_time(x)
y = self.regressor_time(rep).transpose(1, 2)
y1 = rearrange(y, 'b t d -> b (t d)')
x = torch.cat([x, x_mark], dim=-1)
rep2 = self.encoder(x)[:, -1]
y2 = self.regressor(rep2)
return y1.detach() * g1 + y2.detach() * g2, y1, y2
然后更新计算损失,更新weight
loss_w = criterion(outputs, rearrange(batch_y, 'b t d -> b (t d)'))
loss_w.backward()
self.opt_w.step()
self.opt_w.zero_grad()
bias,用b去更新w
更新b的目的:作为一个影响因子去影响w
w
i
←
w
i
+
b
i
w_{i} \leftarrow w_{i}+b_{i}
wi←wi+bi
包括测试的时候去计算输出也是只用到了组合权重weight
关键
inputs_decision = torch.cat([loss1*y1_w, (1-loss1)*y2_w, true_w], dim=1) self.bias = self.decision(inputs_decision)
使用更新后的weight乘以两个模型分别预测的结果做为decision的输入参数,
decision长这样
MLP(
(input): Linear(in_features=3, out_features=32, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
(hiddens): ModuleList(
(0): Linear(in_features=32, out_features=32, bias=True)
)
(output): Linear(in_features=32, out_features=1, bias=True)
(act): Tanh()
)
更新bias相关的参数
loss1 = F.sigmoid(weight + bias.repeat(1, t, 1))
loss1 = rearrange(loss1, 'b t d -> b (t d)')
loss2 = 1 - loss1
loss_bias = criterion(loss1 * y1_w + loss2 * y2_w, true_w)
loss_bias.backward()
self.opt_bias.step()
self.opt_bias.zero_grad()
预测结果
if mode =='test' and self.online != 'none':
return self._ol_one_batch(dataset_object, batch_x, batch_y, batch_x_mark, batch_y_mark)
最后预测结果长这个样子,在这个函数里面
def _ol_one_batch(self,dataset_object, batch_x, batch_y, batch_x_mark, batch_y_mark, return_loss=False):
if self.individual:
weight = self.weight.view(1, 1, -1)
weight = weight.repeat(b, t, 1)
bias = self.bias.view(-1, 1, d)
loss1 = F.sigmoid(weight + bias.repeat(1, t, 1)).view(b, t, d)
loss1 = rearrange(loss1, 'b t d -> b (t d)')
else:
loss1 = F.sigmoid(self.weight + self.bias)
outputs, y1, y2 = self.model.forward_weight(x, batch_x_mark, loss1, 1-loss1)
forward_weight
def forward_weight(self, x, x_mark, g1, g2):
rep = self.encoder_time.encoder.forward_time(x)
y = self.regressor_time(rep).transpose(1, 2)
y1 = rearrange(y, 'b t d -> b (t d)')
x = torch.cat([x, x_mark], dim=-1)
rep2 = self.encoder(x)[:, -1]
y2 = self.regressor(rep2)
return y1.detach() * g1 + y2.detach() * g2, y1, y2
解耦训练
注意更新w和b的时候需要从计算图中分离出模型参数,模型参数和模型权重是分别去更新的
模型权重前向传播时,将模型预测结果从计算图中分离出来,避免更新模型参数
def forward_weight(self, x, x_mark, g1, g2):
rep = self.encoder_time.encoder.forward_time(x)
y = self.regressor_time(rep).transpose(1, 2)
y1 = rearrange(y, 'b t d -> b (t d)')
x = torch.cat([x, x_mark], dim=-1)
rep2 = self.encoder(x)[:, -1]
y2 = self.regressor(rep2)
return y1.detach() * g1 + y2.detach() * g2, y1, y2
一些问题
在线学习体现在哪里?
warm_up:online trainging=1:3
train:val:test=4:1:15
test的时候仍然在更新参数
def test(self, setting):
if self.individual:
self.weight = torch.zeros(self.args.enc_in, device = self.device)
self.bias = torch.zeros(self.args.enc_in, device = self.device)
else:
self.weight = torch.zeros(1, device = self.device)
self.bias = torch.zeros(1, device = self.device)
self.weight.requires_grad = True
self.opt_w = optim.Adam([self.weight], lr=self.args.learning_rate_w)
test_data, test_loader = self._get_data(flag='test')
强化学习体现在哪里?
backbone为什么用TCN
为什么不使用transformer backbone呢?
对于Transformer,发现大量的参数并不有益于泛化结果,并且总是选择超参数,使得Transformer基线的参数数量少于FSNet的参数数量。