尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

时间序列预测首选LSTM?TCN用膨胀卷积与残差连接实现更快训练

时间序列预测首选LSTM?TCN用膨胀卷积与残差连接实现更快训练 简介压缩包内是基于Python的TCN时间卷积神经网络时间序列预测完整工程核心任务是利用多个输入信号预测外汇中间价走势并对比了该网络与长短期记忆网络的效果适合有一定深度学习基础、从事时序数据建模的研究者或开发者。全部内容压缩为zip格式共5个文件大小仅1.04MB包括2个说明文档、2个Jupyter交互式笔记本和1个Python脚本其中交互式笔记本分别承担数据预处理与模型训练预测两个环节Python脚本实现网络结构配合数据目录和模型目录可直接运行学习也便于调整输入特征后迁移至其他序列预测任务。当前已有2392人学习热度较高。作者在实际外汇数据上验证了模型表现指出前期中间价预测一般但后期走势拟合较好同时提醒使用时应选择存在因果关系的输入和输出避免将相关性误当因果性并建议准备充足训练样本、采用提前停止等方法抑制过拟合这些要点对将时间卷积网络应用到工业预测场景很有参考价值。1. 从LSTM到TCN时间序列预测为什么需要换一种网络结构做时间序列预测的人十有八九第一反应是LSTM或者GRU。我自己的项目里也踩过这个惯性——直到有一次用TCN时间卷积神经网络跑同样的航班客流数据训练时间只有LSTM的三分之一验证集误差反而低了近10%。TCN不是什么黑匣子它的核心就是因果卷积、膨胀卷积和残差连接三件事因果卷积保证只用过去预测未来膨胀卷积在不加深网络的前提下成倍扩大感受野残差连接让几十层的卷积网络也能稳定收敛。它能把一个长序列切成固定窗口直接出预测值不需要像RNN那样逐步推进天然适合并行计算。这篇笔记我会按自己复现和调参的完整路径来讲TCN的结构为什么能打、数据怎么进模型、PyTorch里如何手写实现、训练和预测的参数怎么设以及那些让我翻过车的坑。2. TCN的结构拆解膨胀卷积和残差为什么能替代RNN2.1 因果卷积如何保证只用历史数据预测未来时间序列预测最基础的一条铁律是测试阶段不能用未来信息。RNN通过循环结构天然满足这一点——t时刻的输出只依赖t-1时刻的隐状态。但普通卷积不行一个kernel_size为3的卷积核在计算t时刻输出时会同时看到t-1、t、t1三个时刻的输入这就是“泄漏未来”。TCN用的因果卷积解决方式非常直接对输入做左侧padding把卷积核的覆盖范围整体向左偏移。例如kernel_size3时普通卷积是在输入两侧各pad 1位而因果卷积只在左侧pad 2位这样t时刻的输出实际只看到t-2、t-1、t三个时刻。在PyTorch里实现因果卷积不需要特殊的conv1d层import torch import torch.nn.functional as F from torch import nn def causal_pad(x, padding): # 只对时间维度左侧做padding右侧不补 return F.pad(x, (padding, 0)) x torch.randn(32, 8, 144) # batch32, channels8, 时间步144 x_padded causal_pad(x, padding4) print(x_padded.shape) # torch.Size([32, 8, 148])这里padding参数的含义是“向左补多少个零”。补pad位以后卷积输出序列长度会变成输入长度padding-kernel_size1要让输入输出等长需要配合Select操作把末尾多出来的部分裁掉。实际项目中我一般会封装一个CausalConv1d类把pad、conv和裁剪都包进去这样在堆叠多层时不会反复出错。提示因果卷积的padding计算必须和卷积核尺寸联动。kernel_size3、dilationd时左侧padding通常取(kernel_size - 1) * dilation这样输出序列正好和输入序列等长。2.2 膨胀卷积感受野公式与kernel_size、dilation的配比回到我开头提到的那个预测结果像白噪声的案例。那是一个逐小时的电力负荷数据有明显的24小时周期。当时我用kernel_size3、只堆了2层TCN感受野只有7个时间步别说周期连趋势都学不出来。TCN解决这个问题靠的是膨胀卷积dilated convolution。膨胀卷积的意思很简单卷积核相邻参数之间插入空洞。dilation1是普通卷积dilation2意味着卷积核每次跳过一个输入位置。它的价值在于感受野随层数指数增长但参数量不变。TCN层的感受野计算公式如下感受野 1 sum( (kernel_size - 1) * dilation_i )对第i层累加如果每一层的dilation按2的指数递增即(dilation1, 2, 4, 8...)那么6层、kernel_size4的TCN感受野是1 (4-1) * (12481632) 190个时间步这意味着一根6层的TCN就可以覆盖接近200个历史时刻而LSTM要想看到同样长的窗口需要自己维护一个200维的隐状态训练成本完全不同。感受野不能凭感觉定我一般先在纸上算一遍序列最长周期是多少、需要依赖多远的历史然后反推层数。例如月度数据有12个月的周期至少要让感受野超过12小时数据有24小时周期就按24起步。2.3 残差连接与归一化几十层网络不崩的秘密膨胀卷积解决了视野问题但网络一旦堆到十几层梯度消失就会找上门。TCN沿用ResNet的残差连接做法把每一层的输入和输出相加再进入下一层。这样梯度可以跳过中间层直接回传网络深度从几层扩展到几十层都没有问题。我在实际搭建模型时会在每个TCN残差块内部做两次因果卷积、加ReLU和Dropout并且加入权值归一化WeightNorm这一套组合在电力和金融数据上的稳定性都验证过。class TCNBlock(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, dilation, dropout): super().__init__() self.conv1 nn.Conv1d(in_channels, out_channels, kernel_size, dilationdilation) self.conv2 nn.Conv1d(out_channels, out_channels, kernel_size, dilationdilation) self.dropout nn.Dropout(dropout) self.relu nn.ReLU() # 输入输出通道不一致时用1x1卷积对齐维度 self.resample nn.Conv1d(in_channels, out_channels, 1) if in_channels ! out_channels else None def forward(self, x): # 第一层因果卷积只做左侧padding pad (self.conv1.kernel_size[0] - 1) * self.conv1.dilation[0] out F.pad(x, (pad, 0)) out self.relu(self.conv1(out)) out self.dropout(out) # 第二层因果卷积 out F.pad(out, (pad, 0)) out self.dropout(self.relu(self.conv2(out))) if self.resample is not None: x self.resample(x) return out x # 残差连接逐元素相加这段代码的参数要细致解释dilation是膨胀系数决定卷积核取输入的间隔kernel_size控制单个卷积核的覆盖范围两个一起决定这个block的感受野贡献。resample层的作用是当输入通道数不等于输出通道数时用1x1卷积把维度对齐否则残差相加会报形状错误。整个TCN网络就是把这个block按膨胀系数递增的方式串起来。2.4 TCN和LSTM的选型边界哪些场景适合换用TCN别看TCN在很多基准上超过了LSTM它不是万金油。从我的实际使用经验来说TCN的强项在于中长窗口、有周期性、数据量充足的任务比如负荷预测、交通流量预测和销量预测。训练过程可以整段并行GPU利用率明显高。LSTM适合的状态是序列本身很长且依赖关系是“记住某个遥远事件的影响”例如异常检测里需要记住几个月前的某个模式另外在线上推理时LSTM的递推式结构更省内存TCN需要缓存整个感受野窗口。判断标准我一般用一条如果序列里的规律主要靠“局部形状”判断比如双十一前几天的销量爬坡TCN更顺手如果规律藏在“某个全局状态”里LSTM更可靠。这也解释了为什么很多时间序列竞赛里TCN和LSTM的融合模型很常见——先把序列交给TCN提取局部特征再把特征交给LSTM处理长期依赖各干各的擅长部分。3. 数据准备与窗口构造滑窗参数如何决定预测效果3.1 数据集加载与训练测试切分航班客流示例我复现TCN时最常用的是经典的AirPassengers月度航班客流数据144条样本有明确的趋势和12个月周期特别适合验证模型有没有学到周期性。在本地直接用seaborn加载即可不需要额外找数据源import numpy as np import pandas as pd import seaborn as sns df sns.load_dataset(flights) series df[passengers].values.astype(float) print(f样本量: {len(series)}, 前5个值: {series[:5]}) # 按8:2切分训练/测试注意不能打乱顺序 train_len int(len(series) * 0.8) train, test series[:train_len], series[train_len:] print(f训练集: {len(train)}, 测试集: {len(test)})这里最需要提醒的是切分方式时间序列永远不能用随机打乱切分否则模型在训练时偷看到了测试段的时间模式评估分数全是假的。train_len按比例取前80%的连续样本测试段取最后20%这种切分方式保证测试段在时间上严格晚于训练段。3.2 归一化的正确姿势MinMaxScaler必须只fit训练集TCN用ReLU作为激活函数对输入尺度很敏感。时间序列的值如果从几十到几千波动卷积计算出的特征图数值会非常大梯度更新也跟着震荡。常见的做法是把数据缩放到[0, 1]区间。这里有一个我见过无数人翻车的细节归一化时必须只用训练集去fit得到min和max再把这个scaler同时应用到训练集和测试集。如果对整个序列包含测试段一起做MinMaxScaler测试段的最大值会被提前“剧透”训练时模型相当于看到了未来的尺度这种数据泄漏会让测试误差偏低线上部署后高得离谱。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) # 先切分、再fit训练集 train_scaled scaler.fit_transform(train.reshape(-1, 1)).flatten() # 把scaler用在测试集上不重新fit test_scaled scaler.transform(test.reshape(-1, 1)).flatten() print(f训练集范围: [{train_scaled.min():.3f}, {train_scaled.max():.3f}]) print(f测试集范围: [{test_scaled.min():.3f}, {test_scaled.max():.3f}])预测完成之后要用scaler.inverse_transform把归一化后的预测值还原成真实量纲这样才能和原始数据做误差对比。fit_transform和transform的区别是前者计算min/max并应用后者只应用已保存的min/max不重新计算。这是数据管线里最小的一个坑却能造成最致命的误差失真。3.3 滑动窗口数据集input_window和output_window怎么选TCN不能像LSTM那样一次吃一条任意长的序列它输入的是一个固定长度的窗口。我将原始序列切成一批(X, y)样本X是长度为input_window的历史窗口y是未来output_window步的预测目标。窗口长度直接影响感受野的利用效率input_window别超过感受野太多否则多余的部分模型根本没看到也别小于周期长度否则周期性学不全。def make_dataset(series, input_window24, output_window1): X, y [], [] for i in range(len(series) - input_window - output_window 1): X.append(series[i:i input_window]) y.append(series[i input_window:i input_window output_window]) return np.array(X), np.array(y) input_window, output_window 24, 1 X_train, y_train make_dataset(train_scaled, input_window, output_window) X_test, y_test make_dataset(test_scaled, input_window, output_window) # 转成PyTorch需要的形状: (batch, channels, sequence_len) X_train torch.from_numpy(X_train).float().unsqueeze(1) y_train torch.from_numpy(y_train).float().squeeze(-1) print(f训练样本形状: {X_train.shape}, 标签形状: {y_train.shape})output_window1是单步预测训练简单、误差较小如果做多步预测output_window设成目标步数即可但误差会随步数增加而放大。input_window的经验值是至少覆盖两个完整周期。航班数据周期是12个月我一般取24刚好两年窗口逐小时电力数据周期是24小时窗口通常会取168一周或336两周。滑动窗口构造时还有个容易被忽略的点相邻窗口的数据高度重叠这会让训练样本之间不独立影响模型泛化但样本量不足时还是要靠这种重叠来扩充数据。4. 从零搭建TCN模型PyTorch手写完整训练闭环4.1 完整TCN网络实现残差块堆叠与全连接输出在真实项目里我不想每次都重复写残差块所以会把上一章的TCNBlock组装成完整的TCN类。核心参数有四个num_channels定义每一层的通道数、kernel_size定义卷积核大小、dropout控制随机失活比例、num_levels控制层数进而控制感受野。每个残差块之间膨胀系数按2**level递增class TCN(nn.Module): def __init__(self, input_channels, num_channels, kernel_size, dropout, num_levels, output_steps): super().__init__() self.blocks nn.ModuleList() for level in range(num_levels): dilation 2 ** level in_ch input_channels if level 0 else num_channels[level - 1] out_ch num_channels[level] block TCNBlock(in_ch, out_ch, kernel_size, dilation, dropout) self.blocks.append(block) self.fc nn.Linear(num_channels[-1], output_steps) def forward(self, x): # x shape: (batch, input_channels, seq_len) for block in self.blocks: x block(x) # 取最后一个时间步的特征 x x[:, :, -1] return self.fc(x)参数说明num_levels不宜一次加太大层数越多感受野越大但训练越慢。航班数据用num_levels6、kernel_size4感受野约190覆盖24个月窗口绰绰有余如果数据量不足层数再高就会过拟合。output_steps是预测未来的步数单步预测时设为112步预测就设为12。4.2 训练配置损失函数、优化器和早停的取舍训练TCN和训练LSTM很像但有几个关键差异。第一是loss选择单步预测用MSE没问题但有明显尖峰的数据集比如节假日销量激增用HuberLoss更好它在大误差处的梯度是恒定的不会因为一个极端值把整个模型学歪。第二是优化器我用Adam的标配是lr0.001、weight_decay1e-4RNN在这配置下容易梯度爆炸但TCN因为残差连接的存在很少出现爆炸问题。第三是BatchSizeTCN是卷积结构同样的batch下内存占用比LSTM小可以适当调大我在航班数据上直接用64。from torch.optim import Adam from torch.nn import MSELoss, HuberLoss model TCN( input_channels1, num_channels[16, 32, 64, 64, 32, 16], kernel_size4, dropout0.2, num_levels6, output_steps1 ) optimizer Adam(model.parameters(), lr0.001, weight_decay1e-4) criterion HuberLoss(delta1.0) # 对尖峰数据更稳 # 训练循环简写 for epoch in range(200): model.train() optimizer.zero_grad() pred model(X_train) loss criterion(pred, y_train) loss.backward() optimizer.step() if epoch % 20 0: print(fepoch {epoch}, loss {loss.item():.6f})num_channels从16开始逐层增加再逐层减少形成一个对称的“梭形”结构前几层提取局部细节中间层扩大感受野后几层把高维特征压缩。dropout0.2是经验值不建议一开始就用0.5窗口序列本身信息密度不高dropout太狠容易欠拟合。4.3 测试集评估与真实值还原预测曲线为什么能对上周期训练结束后最关键的一步是检查模型在测试段的表现。测试段是模型从未见过的后20%数据能直接反映模型真实泛化能力。评估分两步先把归一化的预测结果还原成真实客流人数再和真实值计算MAE和MAPE。model.eval() with torch.no_grad(): y_pred_scaled model(X_test).numpy().flatten() # 还原真实尺度 y_pred scaler.inverse_transform(y_pred_scaled.reshape(-1, 1)).flatten() y_true test_scaled[input_window:] # 注意对齐窗口偏移 y_true scaler.inverse_transform(y_true.reshape(-1, 1)).flatten() mae np.mean(np.abs(y_true - y_pred)) mape np.mean(np.abs((y_true - y_pred) / y_true)) * 100 print(fMAE: {mae:.2f} 人, MAPE: {mape:.2f}%)航班数据在这组参数下MAPE通常在3%到6%之间。如果MAPE超过10%优先检查感受野是不是覆盖了周期其次是通道数是否太小。还原真实量纲这一步经常有人漏掉直接拿归一化的误差去报告导致误差数值小得好看却没有业务意义。5. TCN时间序列预测的5个高频坑从数据泄漏到递归误差累积5.1 对称padding导致未来信息泄漏测试集loss低到不真实现象训练和测试loss都低到0.001级别预测曲线和真实曲线几乎完全重合稍微平移一点。这时候别高兴太早大概率是泄漏了。原因实现时用了PyTorch内置的nn.Conv1d默认行为或者没有做左侧padding而是在序列两端都补零。TCN变成了普通卷积t时刻的输出混入了t1时刻的未来信息。序列预测本质上变成了“抄近道”测试评估失真。解决所有卷积层前强制用F.pad(x, (pad, 0))做左填充并写成独立的CausalConv1d类不允许普通Conv1d直接出现。然后做一个“戳穿测试”把输入序列的最后一个时刻改成极大值看预测结果变不变——TCN的预测应该完全响应这个变化而泄漏模型会提前在t-1时刻就出现异常。5.2 归一化时把测试集混进去fit回测漂亮上线崩盘现象线下回测误差MAPE只有2%上线后第一周误差飙到20%以上预测曲线整体比真实值高一大截。原因MinMaxScaler.fit时用的是全部序列的min和max测试段的最大值被模型提前看到了。测试段的取值范围被人为压缩到和训练段一致真实上线时新数据的max不在训练范围内scaler把新值映射到区间外预测自然全面偏移。解决雇一个严格的管线顺序先train_test_split再fit_transform(train)最后transform(test)。并且保存scaler时用joblib.dump单独存文件预测服务加载时只能加载训练阶段存下的scaler任何重新fit都不允许。5.3 感受野小于周期长度模型把周期学成了噪声现象预测曲线平坦几乎看不出周期性误差集中在波峰和波谷但平均值不高。原因感受野是190个步长但序列周期是365天或者12个月TCN根本看不到一个完整的周期只能把局部波动当噪声拟合。解决先用代码打印感受野rf 1 sum((kernel_size-1) * 2**level for level in range(num_levels))再对比输入序列的自相关图找到最显著的周期长度。感受野至少要达到周期长度的2倍否则就增加num_levels或kernel_size。航班数据周期12配置6层、kernel_size4已经到190绰绰有余逐小时负荷数据周期24推荐8层、kernel_size5。5.4 多步递归预测误差滚雪球越远的预测越接近均值现象做未来12个月预测时第3个月之后预测曲线开始趋于平缓最终变成一条接近历史均值的直线。原因用的是“递归多步预测”——把第1步的预测值当成输入再预测第2步误差逐步累积并放大。TCN的卷积结构本身没有不确定性建模能力每一步的误差被当作真实值送进下一个窗口最后模型倾向于输出保守的均值。解决多步预测时改成“直接多步输出”把output_steps设为12模型一次输出12个预测值误差不传递。如果坚持递归预测可以在训练时给输入加少量高斯噪声噪声尺度取训练集标准差的5%模拟预测误差的累积环境让模型对输入扰动更鲁棒。5.5 损失函数被尖峰数据带崩HuberLoss替代MSE现象训练loss出现一个突然的尖峰之后模型输出恒为历史均值。回看数据训练集里有一个远高于其他值的异常样本。原因MSE对大误差样本的梯度是线性的一个极大值会把梯度推得非常大Adam虽然能调整步长但网络权重被一次性推到远离最优解的区域。TCN的共享卷积核让这种破坏被放大到所有时间步。解决把MSELoss换成HuberLoss(delta1.0)。delta以内的误差按平方处理delta之外按线性处理梯度不会超过delta这个阈值单个异常样本的影响被天然压制。工业场景我几乎默认用HuberLoss除非数据确认没有异常值才用MSE。6. 验证多步预测的最后一公里滚动回测与残差分析多步预测的误差评估和单步完全不同。很多人在单步预测上效果不错就以为多步也稳了——真做12步预测时滚动回测才是最接近线上场景的验证方式。所谓滚动回测是从测试集起始点开始每次用历史窗口预测未来12步然后把窗口向后滚动一步重复这个过程。这比一次性在测试集上预测更能暴露误差累积问题。滚动回测的实现技巧是每滚动一步把窗口的真实值移动一个位置而不是把上一轮的预测值塞进窗口。这样评估的是“在每一步都能拿到真实历史的情况下模型每一步的预测能力”和线上真正面临的情况略有区别但它能帮你分离“模型拟合能力差”和“递归误差累积”两个问题。做完滚动回测还要看残差序列是否还有模式。把y_true - y_pred按时间顺序画出来如果残差在某个时间段系统性为正、另一个时间段系统性为负说明模型漏掉了某个周期性因子如果残差里还有明显的周期振荡说明感受野或通道数不够。残差应当是围绕0随机波动没有明显结构才算模型把时序规律榨干了。这一步排查完再朝着加大感受野的方向调参通常不会白费力气。我个人的习惯是每次调参先打印感受野、序列周期、损失函数类型这三个信息。感受野不够就加层数周期没抓到就加kernel_size或input_window损失被尖峰带崩就用HuberLoss。这套排查流程在负荷预测和销量预测上反复验证过最后补一句时间序列预测没有通用银弹但把TCN的这三个关键维度先摸透至少能让你少走我走过的弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表