OneNet:OneNet: Enhancing Time Series Forecasting Models under Concept Drift by Online Ensembling

在这里插入图片描述

文章来源: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)。它动态地更新并结合了两个模型,一个关注于跨时间维度的依赖关系建模,另一个关注于跨变量依赖关系建模。并且在测试阶段,还使用强化学习方法动态调整用于结合两个模型预测结果的权重。

贡献:

  1. 我们介绍了OneNet,一种用于在线时间序列预测的双流架构,它使用在线凸优化集成了两个模型的输出。OneNet在处理概念漂移时利用了变量独立模型的鲁棒性,同时也捕获了不同变量之间的相互依赖性,以提高预测精度。此外,我们提出了一种基于强化的在线学习方法,以减轻传统的OCP算法的局限性,并通过实证和理论分析证明了它的有效性。
  2. 我们对四个数据集的实证研究表明,与最先进的方法相比,OneNet将平均累积均方误差(MSE)降低了53.1%,将平均绝对误差(MAE)降低了34.5%。特别是,在具有挑战性的数据集ECL上
    的性能增益更优越,其中MSE降低了59.2%,MAE降低了63.0%。
  3. 我们进行了全面的实证研究,以调查预测模型中常用的设计选择,如实例归一化、变量独立性、季节趋势分解和频域增强,如何影响模型的鲁棒性。此外,我们系统地比较了现有的基于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}} z1RM×dm, 然后prediction head会生成最终的预测结果 y 1 ∈ R M × H y_{1} \in \mathbb{R}^{M \times H} y1RM×H.

对于cross-variable 预测器 f2,encoder将会把输入序列映射成 z 2 ∈ R L × d m z_{2} \in \mathbb{R}^{L \times d_{m}} z2RL×dm。 然后选择最后一个时间步的表示 z 2 , L ∈ R d m z_{2,L} \in \mathbb{R}^{d_{m}} z2,LRdm送入prediction head ,生成最终的预测结果 y 1 ∈ R M × H y_{1} \in \mathbb{R}^{M \times H} y1RM×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} wiwi+bi。考虑到变量之间的差异性,作者为每个变量构建了不同的权重,也就是说 组合权重 w ∈ R M × 2 w \in \mathbb{R}^{M \times 2} wRM×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} wiwi+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的参数数量。

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
基于STM32 OneNet设计的应用是指使用STM32微控制器来连接到OneNet云平台进行数据传输和控制的设计方案。STM32是一种高性能、低功耗的微控制器,具有强大的处理能力和丰富的外设接口,非常适合用于物联网应用的开发。 在这个设计方案中,首先需要将STM32与OneNet云平台进行连接。可以通过使用Wi-Fi、以太网或者GSM等通信方式来实现与云平台的通信。通过在STM32上编写相应的固件程序,可以将采集的传感器数据或者控制指令发送到OneNet云平台。 其次,需要在OneNet云平台上创建设备和数据流,并将其与STM32的固件程序相对应。设备表示实际的物理设备,数据流表示设备采集的数据或者控制指令。通过设备和数据流的创建,可以实现云端和设备端的数据交互。 在STM32的固件程序中,需要实现数据采集、数据处理和数据传输等功能。通过使用STM32的外设接口,可以连接各种类型的传感器,例如温度传感器、湿度传感器、光线传感器等,采集环境数据。对于采集到的数据,可以进行处理、存储或者发送到云平台。同时,也可以接收云端下发的命令,对设备进行控制。 最后,借助OneNet云平台丰富的功能,可以实现数据的可视化和远程控制。通过OneNet的数据分析和展示功能,可以对采集的数据进行展示和分析,实时监控设备状态。同时,通过OneNet的指令下发功能,可以远程控制设备的运行状态,实现远程操控。 总之,基于STM32 OneNet设计的应用可以实现物联网设备与云平台的连接与数据交互,为物联网应用的开发提供了强大的平台和工具。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值