【Python时序预测系列】建立CNN与Transformer融合模型实现单变量时序预测(案例+源码)

这是我的第405篇原创文章。

一、引言

    单纯的 CNN 擅长提取局部特征(如局部趋势、周期性波动),而 Transformer 通过自注意力机制能够捕捉长距离依赖和全局关联。两者融合后,可以同时兼顾局部模式与全局结构,从而提升趋势预测的效果。

    在真实的应用场景领域,时间序列数据(例如股票价格、外汇汇率、商品期货等)通常具有非平稳、高噪声和多尺度依赖等特点。传统的统计方法在捕捉数据局部波动或全局趋势方面存在局限性。

  • CNN能够利用滑动窗口提取局部模式,如短期波动、周期性变化和局部异常值;

  • Transformer则通过自注意力机制捕捉序列中各时刻之间的全局依赖关系。

    因此,将两者进行融合可以在保持局部特征敏感性的同时捕获长距离依赖,提高趋势预测的精度。

    这里给大家一个案例来看看如何利用这两种模型的优势对单变量时间序列数据进行趋势预测。

二、实现过程

2.1 读取数据集

核心代码:

class VirtualTimeSeriesDataset(Dataset):
    def __init__(self, seq_len=12, num_samples=132):
        super(VirtualTimeSeriesDataset, self).__init__()
        self.seq_len = seq_len
        self.num_samples = num_samples
        self.data, self.targets = self.generate_data()
    def generate_data(self):
        window_size = self.seq_len
        data = []
        targets = []
        # 读取数据集
        df = pd.read_csv('data.csv')
        # 将日期列转换为日期时间类型
        df['Month'] = pd.to_datetime(df['Month'])
        # 将日期列设置为索引
        df.set_index('Month', inplace=True)
        scaler = MinMaxScaler()
        df = scaler.fit_transform(df.values.reshape(-1, 1))
        for i in range(len(df) - window_size):
            data.append(df[i:i + window_size, 0:df.shape[1]])
            targets.append(df[i + window_size, 0])
        data = np.array(data, dtype=np.float32)  # shape: (num_samples, seq_len, 1)
        targets = np.array(targets, dtype=np.float32).reshape(-1, 1)
        print(data.shape, targets.shape)
        return data, targets
    def __len__(self):
        return self.num_samples
    def __getitem__(self, index):
        return self.data[index], self.targets[index]

2.2 模型实例化

核心代码:

class FusedCNNTransformer(nn.Module):
    def __init__(self, input_dim=1, embed_dim=64, cnn_channels=64, kernel_size=3,
                 num_transformer_layers=2, nhead=4, dropout=0.1, fc_dim=32):
        super(FusedCNNTransformer, self).__init__()
        self.embed_dim = embed_dim
        # 线性嵌入层:将输入数据映射到 embed_dim 维度
        self.embedding = nn.Linear(input_dim, embed_dim)
        # CNN 分支:提取局部特征
        # 输入需变换为 (batch, channels, seq_len) 形式
        self.cnn = nn.Sequential(
            nn.Conv1d(in_channels=embed_dim, out_channels=cnn_channels, kernel_size=kernel_size,
                      padding=kernel_size // 2),
            nn.ReLU(),
            nn.Conv1d(in_channels=cnn_channels, out_channels=cnn_channels, kernel_size=kernel_size,
                      padding=kernel_size // 2),
            nn.ReLU(),
            nn.AdaptiveAvgPool1d(1)  # 压缩时间维度,输出 shape (batch, cnn_channels, 1)
        )
        # Transformer 分支:捕捉全局依赖
        self.pos_encoder = PositionalEncoding(d_model=embed_dim)
        encoder_layers = nn.TransformerEncoderLayer(d_model=embed_dim, nhead=nhead, dropout=dropout)
        self.transformer_encoder = nn.TransformerEncoder(encoder_layers, num_layers=num_transformer_layers)
        # 对 Transformer 输出进行池化:取最后时刻或均值池化
        self.transformer_pool = nn.AdaptiveAvgPool1d(1)  # 最终输出 (batch, embed_dim, 1)
        # 融合层(门控机制)
        # 将 CNN 与 Transformer 特征拼接后计算门控系数
        self.gate_fc = nn.Linear(cnn_channels + embed_dim, cnn_channels + embed_dim)
        # 最终预测层
        self.fc = nn.Sequential(
            nn.Linear(cnn_channels + embed_dim, fc_dim),
            nn.ReLU(),
            nn.Linear(fc_dim, 1)  # 输出回归值,预测下一个价格/趋势
        )
    def forward(self, x):
        # x: [batch, seq_len, input_dim]
        batch_size, seq_len, _ = x.size()
        # 线性嵌入
        x_embed = self.embedding(x)  # [batch, seq_len, embed_dim]
        # Transformer 分支:添加位置编码并转置为 [seq_len, batch, embed_dim] 供 Transformer 使用
        x_pos = self.pos_encoder(x_embed)
        x_trans_input = x_pos.transpose(0, 1)  # [seq_len, batch, embed_dim]
        transformer_output = self.transformer_encoder(x_trans_input)  # [seq_len, batch, embed_dim]
        transformer_output = transformer_output.transpose(0, 1)  # [batch, seq_len, embed_dim]
        # 对 Transformer 输出进行池化,提取全局特征
        transformer_pool = self.transformer_pool(transformer_output.transpose(1, 2))  # [batch, embed_dim, 1]
        transformer_feature = transformer_pool.squeeze(-1)  # [batch, embed_dim]
        # CNN 分支:将嵌入后的数据转置为 [batch, embed_dim, seq_len] 进行一维卷积
        cnn_input = x_embed.transpose(1, 2)  # [batch, embed_dim, seq_len]
        cnn_feature = self.cnn(cnn_input)  # [batch, cnn_channels, 1]
        cnn_feature = cnn_feature.squeeze(-1)  # [batch, cnn_channels]
        # 融合:拼接两个分支的特征,然后计算门控系数
        fused = torch.cat([cnn_feature, transformer_feature], dim=1)  # [batch, cnn_channels+embed_dim]
        gate = torch.sigmoid(self.gate_fc(fused))  # 门控系数,形状同 fused
        fused_feature = gate * fused  # 简单使用门控系数对特征加权
        # 最终预测
        output = self.fc(fused_feature)  # [batch, 1]
        # 为了展示部分中间结果,这里返回 cnn_feature 与 transformer_feature(用于可视化)
        return output, cnn_feature, transformer_feature

模型结构介绍:

  • 模型结构设计:模型通过嵌入层将原始输入映射到高维空间,随后分别进入 CNN 分支和 Transformer 分支:

    • CNN 分支:通过两层一维卷积及 ReLU 激活函数提取局部模式,并利用池化层将时序信息进行降维,得到固定长度的特征向量;

    • Transformer 分支:利用位置编码与多层 Transformer Encoder 捕捉全局依赖,最后通过均值池化提取全局特征;

    • 融合层:采用门控机制对两路特征进行融合,有效平衡局部信息和全局信息。

2.3 训练模型

核心代码:

def train_model(model, dataloader, num_epochs=50, learning_rate=1e-3, device='cpu'):
    criterion = nn.MSELoss()
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)
    model.train()
    loss_history = []
    for epoch in range(num_epochs):
        epoch_losses = []
        for batch_data, batch_targets in dataloader:
            batch_data = batch_data.to(device)
            batch_targets = batch_targets.to(device)
            optimizer.zero_grad()
            outputs, _, _ = model(batch_data)
            loss = criterion(outputs, batch_targets)
            loss.backward()
            optimizer.step()
            epoch_losses.append(loss.item())
        avg_loss = np.mean(epoch_losses)
        loss_history.append(avg_loss)
        if (epoch + 1) % 10 == 0:
            print(f"Epoch [{epoch + 1}/{num_epochs}], Loss: {avg_loss:.4f}")
    return loss_history

采用均方误差(MSE)作为损失函数,使用 Adam 优化器对模型参数进行更新。训练过程中,每个 epoch 记录平均损失并输出,直至模型收敛。

2.4 模型评估

核心代码:

def evaluate_model(model, dataloader, device='cpu'):
    model.eval()
    preds = []
    trues = []
    cnn_features = []
    trans_features = []
    with torch.no_grad():
        for batch_data, batch_targets in dataloader:
            batch_data = batch_data.to(device)
            outputs, cnn_f, trans_f = model(batch_data)
            preds.append(outputs.cpu().numpy())
            trues.append(batch_targets.cpu().numpy())
            cnn_features.append(cnn_f.cpu().numpy())
            trans_features.append(trans_f.cpu().numpy())
    preds = np.concatenate(preds, axis=0).squeeze()
    trues = np.concatenate(trues, axis=0).squeeze()
    cnn_features = np.concatenate(cnn_features, axis=0)
    trans_features = np.concatenate(trans_features, axis=0)
    return preds, trues, cnn_features, trans_features

2.5 可视化分析

训练损失曲线:反映模型在训练过程中损失的逐步下降,说明模型收敛情况;

plt.plot(loss_history, marker='o', color='dodgerblue', linestyle='-', linewidth=2)
plt.title("Training Loss Curve")
plt.xlabel("Epoch")
plt.ylabel("MSE Loss")
plt.tight_layout()
plt.show()

图片

真实值与预测值对比图:直观展示模型在测试集上的预测效果,真实数据与预测值的吻合程度决定了模型性能;

图片

CNN 局部特征图:展示了 CNN 分支提取到的局部特征,有助于理解模型如何捕捉数据中短期波动与局部模式;

图片

Transformer 全局特征图:展示了 Transformer 分支提取到的全局特征,体现了模型对长距离依赖关系的建模能力。

图片

作者简介:

读研期间发表6篇SCI数据挖掘相关论文,现在某研究院从事数据算法相关科研工作,结合自身科研实践经历不定期分享关于Python、机器学习、深度学习、人工智能系列基础知识与应用案例。致力于只做原创,以最简单的方式理解和学习,关注我一起交流成长。需要数据集和源码的小伙伴可以关注底部公众号添加作者微信。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据杂坛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值