简介一套面向初级及以上数据科学家与机器学习爱好者的时间序列预测项目实例聚焦用 Python 与 TensorFlow 构建时间卷积神经网络TCN并以模拟外汇行情数据演示从数据生成、归一化、滑动窗口构造到训练与预测可视化的完整流程。包体仅含 1 个 docx 文档约 31KB以图文教程形式组织便于按步骤阅读和复现代码文档内配有大量示例代码、执行结果截图以及两种神经网络预测效果对比图。资源重点讲解 TCN 的因果卷积与膨胀卷积如何捕捉长程依赖、缓解 RNN 常见梯度消失问题并给出 TCN 与 LSTM/RNN 两类模型的实现、训练参数和评估思路。已有 173 人学习适合希望把序列建模理论落到代码实践、进一步调参比较模型表现或需要金融市场趋势预测案例参考的读者。1. 拿到一份 Python TensorFlow 的 TCN 时间序列预测实例先别急着粘贴多数人拿到这类项目包第一反应是把模型那几十行整段粘进 notebook看 loss 曲线掉下去然后截图收工。真换成自己的数据、真要跟 LSTM 比一比、真被问“为什么后期走势 TCN 跟得更紧”时问题就全冒出来了因果卷积补在哪一侧、膨胀率怎么排、Dense 接在三维张量上出来什么形状、反归一化该在哪一步做。这份实例的价值不在那段 Conv1D 堆叠而在于它把“造数据 → 切窗口 → 建模 → 对照循环网络 → 画图”这条链路完整走了一遍缺的是每一步为什么这么设。下面按我自己复现的顺序拆先把感受野和环境立住再把数据管道和两个模型的同口径比较做实最后把几个一照抄就翻车的点摊开讲。适合已经跑通过 Keras 序列模型、想搞清楚 TCN 边界在哪的人。2. 因果卷积与膨胀卷积TCN 在长序列上压过 RNN 的两条理由循环网络处理序列的方式是“记住上一步的状态再往前走”好处是参数量与序列长度无关坏处是所有历史信息必须挤在一个隐藏状态向量里传递序列一长梯度在时间维度上连乘衰减和爆炸几乎躲不掉。TCN 换了个思路不用循环用一维卷积在时间轴上滑动靠“因果”保证不看未来靠“膨胀”把感受野指数级撑开。理解这两点后面所有参数设置才有依据否则调参就是玄学。2.1 因果卷积补的是哪一侧paddingcausal 的真实语义普通 Conv1D 做 same padding 时左右各补一部分第 t 个输出会同时看到 t-1 和 t1 的输入这在预测任务里是致命的数据泄漏——你等于把答案提前喂给了模型。因果卷积要求输出第 t 步只能依赖输入的第 1 到第 t 步实现上就是在序列左端补(kernel_size - 1) * dilation个零右端不补。Keras 里写paddingcausal就是这个行为。import tensorflow as tf from tensorflow.keras.layers import Input, Conv1D, Lambda from tensorflow.keras.models import Model # 造一个长度为 6 的序列单通道值就是位置编号 x tf.reshape(tf.range(1, 7, dtypetf.float32), (1, 6, 1)) inp Input(shape(6, 1)) # kernel_size3, dilation1, 因果卷积 out Conv1D(filters1, kernel_size3, paddingcausal, dilation_rate1, use_biasFalse, kernel_initializerones)(inp) model Model(inp, out) y model(x) print(y.numpy().reshape(-1)) # 输出约等于 [1, 3, 6, 9, 12, 15] # 第 t 个值只由前 t 个输入累加而来没有任何未来信息参与这段代码用一个全 1 的卷积核把因果性“可视化”了第一个输出只等于第一个输入第二个等于前两个之和越往后累加的历史越长。逻辑说明——卷积核权重固定为 1输出就是窗口内输入的求和从求和覆盖的元素范围能直接读出“这一步看到了几步历史”。参数说明——paddingcausal决定补零方向dilation_rate决定窗口内采样点的间隔kernel_size决定采样点个数。三者共同决定单个输出能看到多远的历史也就是下一节要算的感受野。2.2 感受野算一遍三层 dilation1 的卷积盖不住 10 个时间步感受野receptive field是判断一个卷积序列模型“能看多长的历史”的唯一硬指标。单层卷积的感受野增量是(kernel_size - 1) * dilation多层串联时累加起始值为 1R 1 Σ (k_i - 1) * d_i按这个式子把项目里的结构和标准 TCN 结构各算一遍差距一下就出来了。结构卷积层配置感受野能否覆盖 time_step10项目原始写法3 层 Conv1Dk3d1,1,11 222 7否最后 3 步历史看不到加膨胀后的写法3 层 Conv1Dk3d1,2,41 248 15是且有余量标准 TCN 残差堆叠6 个残差块每块 2 层d1,2,4,8,16,321 2×(12481632) 127远超适合长序列原始代码里三层的dilation_rate都取了默认值 1感受野只有 7而输入窗口time_step10。这意味着每个预测点其实只用了最近 7 个历史值最早那 3 个时间步被卷积核“够不着”——它们参与了张量形状却没参与信息聚合。这是很多人复现后觉得“效果一般、跟 LSTM 拉不开差距”的直接原因之一。改法很简单把膨胀率按 2 的幂次铺开from tensorflow.keras.layers import Input, Conv1D, Dropout, Dense, Lambda, Add, Activation from tensorflow.keras.models import Model def build_tcn_dilated(input_shape, filters64, kernel_size3, dilations(1, 2, 4)): 带膨胀的三层因果卷积感受野 1 (k-1) * sum(dilations) inputs Input(shapeinput_shape) x inputs for d in dilations: x Conv1D(filtersfilters, kernel_sizekernel_size, paddingcausal, dilation_rated, # 关键膨胀率逐层翻倍 activationrelu)(x) x Dropout(0.2)(x) # 只取最后一个时间步的输出做单点预测 x Lambda(lambda t: t[:, -1, :])(x) outputs Dense(1)(x) model Model(inputs, outputs) model.compile(optimizeradam, lossmean_squared_error) return model # 感受野自检 k, dil 3, (1, 2, 4) rf 1 (k - 1) * sum(dil) print(f感受野 {rf}) # 15逻辑说明——循环体按dilations依次堆卷积层activationrelu放在卷积里省一行末尾用Lambda切片把(batch, time_step, filters)压成(batch, filters)再进Dense(1)输出才是(batch, 1)而不是三维。参数说明——dilations的每一项是这一层的采样间隔必须满足dilation_rate 1时kernel_size 2且步长为 1filters保持三层一致是为了后面残差相加时形状对得上否则得配Conv1D(filters, 1)做通道投影。2.3 环境与最小可运行骨架依赖不需要多一条命令的事但版本组合值得说一句TensorFlow 2.x 下paddingcausal与dilation_rate同时使用时低版本对strides 1的因果卷积支持不全复现前先确认自己的版本能正常构图。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install numpy pandas scikit-learn tensorflow matplotlib python -c import tensorflow as tf; print(tf.__version__)逻辑说明——独立虚拟环境避免和系统里已有版本打架这在 TensorFlow 上不是洁癖而是刚需最后一行打印版本方便后面出问题时定位。参数说明——tensorflow装 CPU 版即可跑通全流程数据量在千级、模型参数量在十万级没有 GPU 也能几分钟训完matplotlib注意在无图形界面的服务器上要设matplotlib.use(Agg)否则plt.show()会静默卡住。骨架搭好后建议先跑一次model.summary()把每层输出形状抄下来后面遇到形状相关的报错对照这张表比盲目 print 快得多。3. 数据管道从单列价格到 (样本, 时间步, 1) 的三维张量时间序列预测里模型结构带来的差异往往小于数据管道带来的差异。同一份 TCN 代码归一化区间选错、窗口切片差一位、训练测试划分方式不对都能让指标的波动盖过模型本身的效果。这一章把这条管道拆成三个可验证的动作。3.1 MinMaxScaler 该在哪个区间 fit最常见的错误是先对全体数据fit_transform再切训练测试。这样测试段的极值信息提前进入了 scaler 的min_和max_训练阶段的指标会好看得不真实一换新数据就露馅。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler # 读入单列价格序列 data pd.read_csv(forex_data.csv, index_col0) values data[[Price]].values # 保持 (N, 1) 形状scaler 要求二维输入 # 按时间顺序切分先切再 fit split int(len(values) * 0.8) train_raw, test_raw values[:split], values[split:] scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_raw) # 只在训练段学习 min/max test_scaled scaler.transform(test_raw) # 测试段只做变换不参与统计 print(scaler.data_min_, scaler.data_max_) # 记下这两个值反归一化要用逻辑说明——fit_transform只在训练段调用transform用于测试段和后续新数据这是时序任务里防止信息泄漏的最低要求。参数说明——feature_range(0, 1)是默认值价格序列有趋势时也可以试(-1, 1)对带tanh激活的结构更友好values必须保持二维因为MinMaxScaler按列统计传一维数组会被当成单样本多特征结果完全错位。scaler.data_min_和data_max_建议打印出来存着。反归一化时如果发现预测值整体偏移十有八九是这两处和当初 fit 的不是同一个对象。3.2 create_dataset 的切片边界与时间步选择滑动窗口切片的逻辑本身不复杂坑在边界。窗口长度time_step、循环上界、标签位置三者差一位样本数就对不上或者最后一个样本的标签越界。import numpy as np def create_dataset(series, time_step10, horizon1): series: 归一化后的一维或二维序列 time_step: 输入窗口长度 horizon: 预测第几步之后的值1 表示预测下一个点 X, y [], [] n len(series) for i in range(n - time_step - horizon 1): X.append(series[i:(i time_step), 0]) # 取窗口内的历史 y.append(series[i time_step horizon - 1, 0]) # 取窗口之后的第一个标签 X np.array(X).reshape(-1, time_step, 1) # 直接成型为 3D y np.array(y) return X, y X, y create_dataset(np.vstack([train_scaled, test_scaled]), time_step10, horizon1) print(X.shape, y.shape) # 形如 (989, 10, 1) (989,)逻辑说明——循环上界写成n - time_step - horizon 1保证i time_step horizon - 1最大取到n - 1不越界X在函数内就 reshape 成三维省得外面再补一步忘了。参数说明——time_step要和上一节算出的感受野对照感受野远小于time_step时窗口后半段是浪费远大于时窗口不够用horizon改成 3 就是预测三天后的值此时标签位置随之后移样本数相应减少horizon - 1个。一个容易被忽略的点先整体切窗口再划分训练测试和先划分再各自切窗口结果不一样。前者会在边界处产生跨段的样本只有一两个但足以让指标小幅漂移。严谨做法是先按时间切原始序列再分别切窗口train_X, train_y create_dataset(train_scaled, time_step10) test_X, test_y create_dataset(test_scaled, time_step10)代价是测试集丢掉前time_step个点换来的是边界干净。3.3 用生成序列先把链路跑通再换真实行情真实行情数据带缺口、带节假日、带异常跳空一上手就调模型很容易把数据问题误判成模型问题。稳妥的顺序是先造一条统计性质可控的序列把整条链路跑通。import numpy as np import pandas as pd np.random.seed(42) dates pd.date_range(start2020-01-01, periods1000, freqD) # 随机游走日增量服从正态分布累积后加基准价 prices np.random.normal(loc1.0, scale0.01, sizelen(dates)).cumsum() 100 data pd.DataFrame({Date: dates, Price: prices}).set_index(Date) data.to_csv(forex_data.csv) print(data.describe())逻辑说明——cumsum()把独立增量变成随机游走这条序列没有可学的确定性模式正好用来验证“模型能不能过拟合到噪声”如果一个模型在这种数据上 RMSE 低得离谱说明数据泄漏了。参数说明——scale0.01控制日波动幅度loc1.0是漂移项改大一点序列趋势更明显模型之间的差距也更容易看出来periods1000保证切完窗口还有近千个样本训练测试比例 8:2 时测试集不至于太小。换成真实数据时先做三件事按日期排序、检查缺失值处理方式、确认没有重复时间戳。这三步不做后面所有指标都不可信。4. 模型实现与同口径对照TCN 与 LSTM 怎么比才不算耍赖对照实验最容易做假的地方是“两个模型不在同一条件下比”。参数量差三倍、训练轮数不同、评估用的测试集不一样得出的结论没有意义。这一章把 TCN 的手写实现、LSTM 基线的控制变量、反归一化顺序三件事讲清楚。4.1 手写 build_tcn_model卷积层、Dropout 与输出头先从原始写法说起。三层Conv1D(filters64, kernel_size3, paddingcausal)叠 Dropout末尾直接Dense(1)。这段代码有两处需要动手改。第一处是输出头。Conv1D的输出是(batch, time_step, 64)Dense(1)作用在最后一维得到(batch, time_step, 1)而标签y是(batch,)。多数 TensorFlow 版本会在fit阶段直接抛形状不匹配。第二处是膨胀率2.2 节已经算过。from tensorflow.keras.layers import (Input, Conv1D, Dropout, Dense, Lambda, Add, Activation, LayerNormalization) from tensorflow.keras.models import Model def build_tcn(input_shape, filters64, kernel_size3, dilations(1, 2, 4), dropout0.2): inputs Input(shapeinput_shape) x inputs for d in dilations: h Conv1D(filtersfilters, kernel_sizekernel_size, paddingcausal, dilation_rated, activationrelu)(x) h Dropout(dropout)(h) # 残差连接形状不一致时用 1x1 卷积投影 if x.shape[-1] ! filters: x Conv1D(filtersfilters, kernel_size1, paddingsame)(x) x Add()([x, h]) x Activation(relu)(x) x Lambda(lambda t: t[:, -1, :])(x) # 取最后时间步 outputs Dense(1)(x) model Model(inputs, outputs) model.compile(optimizeradam, lossmean_squared_error, metrics[mae]) return model model build_tcn((10, 1)) model.summary()逻辑说明——每个膨胀层外面包一层残差Add之前检查通道数不一致就补一个 1×1 卷积对齐这是标准 TCN 残差块的最小实现比单纯堆三层卷积更稳定Lambda切片把时序维压掉输出头才是标量。参数说明——filters建议 32 到 64再大在千级样本上容易过拟合dropout0.2是经验值样本少于 500 时可以提到 0.3dilations的长度决定层数和感受野按(1, 2, 4)起步time_step加到 30 以上时换成(1, 2, 4, 8)。如果不想手写Keras 生态里也有现成的 TCN 实现但在自己项目里手写一遍的好处是感受野、残差、输出头这三处都能自己控出问题时知道该翻哪一行。4.2 LSTM 基线要控住的三个变量要让“TCN 比 RNN 好”这个结论站得住LSTM 基线至少控住三点参数量、训练轮数、评估数据。import tensorflow as tf from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.models import Sequential def build_lstm(input_shape, units64, dropout0.2): model Sequential([ LSTM(units, input_shapeinput_shape), # 默认只返回最后一步 Dropout(dropout), Dense(1) ]) model.compile(optimizeradam, lossmean_squared_error, metrics[mae]) return model lstm_model build_lstm((10, 1)) lstm_model.summary() # 训练时两组用同样的 epochs 和 batch_size history_tcn model.fit(X_train, y_train, epochs50, batch_size32, validation_split0.1, verbose1) history_lstm lstm_model.fit(X_train, y_train, epochs50, batch_size32, validation_split0.1, verbose1)逻辑说明——LSTM默认return_sequencesFalse只输出最后一步的隐藏状态形状(batch, units)后面接Dense(1)正好这一点和 TCN 里手动切片等价。参数说明——units64是为了和 TCN 的filters64对齐量级严格对齐参数量比较麻烦实践里对齐“隐藏维度”已经够用epochs和batch_size必须两边一致否则一轮训练量不同曲线没可比性。还有一点validation_split0.1是从训练集末尾切 10%隐含了顺序信息这点对两个模型一样可以接受但如果数据本身波动剧烈最后 10% 可能刚好是极端行情导致两个模型的验证曲线一起崩这时候换成固定比例的验证集更稳。4.3 训练、预测、反归一化的正确顺序原始代码里tcn_predictions scaler.inverse_transform(tcn_predictions)这一步前提是预测结果的形状是(N, 1)且 scaler 是在同一列上 fit 的。如果前面输出头没改预测出来是(N, 10, 1)这里直接报错如果 scaler 是在全体数据上 fit 的反归一化的基准又不对。# 预测 tcn_pred model.predict(X_test).reshape(-1, 1) # 确保二维 lstm_pred lstm_model.predict(X_test).reshape(-1, 1) # 反归一化用当初只在训练段 fit 的 scaler tcn_pred_inv scaler.inverse_transform(tcn_pred) lstm_pred_inv scaler.inverse_transform(lstm_pred) y_test_inv scaler.inverse_transform(y_test.reshape(-1, 1)) # 统一在原始量纲上算指标 from sklearn.metrics import mean_squared_error, mean_absolute_error import numpy as np for name, p in [(TCN, tcn_pred_inv), (LSTM, lstm_pred_inv)]: rmse np.sqrt(mean_squared_error(y_test_inv, p)) mae mean_absolute_error(y_test_inv, p) print(f{name} RMSE{rmse:.4f} MAE{mae:.4f})逻辑说明——先 reshape 成二维再反归一化避免(N, 1, 1)这类形状进inverse_transform报错三个序列统一反归一化到原始量纲后再算指标这样 RMSE 的单位和价格单位一致才能横向比较。参数说明——reshape(-1, 1)里的 1 对应单特征多变量预测时改成对应列数且 scaler 也要在多列上一起 fit。指标算出来后除了比大小还要看量级是否合理。价格序列在 100 附近、日波动 0.1 的量级下RMSE 应该是个小数如果算出 0.0001 或者 5000先怀疑反归一化别怀疑模型。4.4 RMSE 与预测曲线的读法数字之外图能看出更多东西。import matplotlib matplotlib.use(Agg) # 无图形界面环境加这一行 import matplotlib.pyplot as plt plt.figure(figsize(14, 6)) plt.plot(y_test_inv, labelTrue, color#1f77b4, linewidth1.2) plt.plot(tcn_pred_inv, labelTCN, color#ff7f0e, linewidth1.0, alpha0.85) plt.plot(lstm_pred_inv, labelLSTM, color#2ca02c, linewidth1.0, alpha0.85) plt.title(Time Series Forecast Comparison) plt.xlabel(Time Step) plt.ylabel(Price) plt.legend() plt.tight_layout() plt.savefig(prediction_compare.png, dpi150)逻辑说明——三条线画在同一坐标系里看的是“谁的滞后更小、谁对拐点反应更快”。TCN 的优势通常体现在长序列后段预测曲线不会整体平移而 LSTM 在窗口变长时容易出现一到两步的延迟。参数说明——alpha降低一点避免三条线互相遮挡dpi150保证保存出来能看清细节。看图时重点盯三个位置突变的拐点、波动率放大的区段、序列的末端。拐点看滞后波动率区段看模型是不是只会输出均值末端看有没有系统性的偏移。5. 复现排查五个一照抄就翻车的地方这一章全是我自己和周围人踩过的记录每条按现象、原因、解决写。5.1 现象fit 阶段报形状不匹配或者 predict 出来是三维现象照抄原始代码model.fit(X_train, y_train, ...)直接抛Dimensions must be equal之类的错误或者勉强训完predict返回形状是(N, 10, 1)反归一化那步崩掉。原因Conv1D保留了时间维Dense(1)只改最后一维输出是(batch, time_step, 1)而标签是(batch,)。Keras 对广播的容忍度在不同版本间不一致有时报错有时静默算出一个错误的 loss。解决在输出头前把时间维压掉。三种写法任选Lambda(lambda t: t[:, -1, :])取最后一步适合单点预测GlobalAveragePooling1D()对时间维取均值适合关注整体趋势Flatten()后接Dense参数量会涨但保留全部信息。单步预测优先第一种语义最清楚。5.2 现象预测曲线整体滞后一两步拐点跟不上现象训练 loss 降得很好画图看预测线和真实线形状相似但永远慢半拍涨价的时候预测值偏低跌价的时候偏高。原因两个可能。一是感受野不够模型只能看到最近几步的信息对趋势的判断天然滞后原始代码三层dilation1感受野只有 7就是这个情况。二是数据划分时先整体切窗口再切训练测试边界样本混入了跨段信息掩盖了真实的泛化表现。解决先按 2.2 节把膨胀率铺开把感受野抬到time_step的 1.5 倍以上再把数据划分改成先切原始序列、再各自切窗口。改完重训滞后通常会缩小如果还滞后试试把horizon往前挪一步用当前点预测下一步而不是下两步。5.3 现象RMSE 小得离谱或者大得离谱现象跑出RMSE 0.0003或者RMSE 4821跟数据的量级完全不搭。原因反归一化的基准错位。可能是预测值反归一化了但真实值没反也可能是 scaler 在全体数据上 fit、而预测用的是另一套参数还可能y_test在切窗口时被当成了已归一化数据处理了两次。解决在算指标前把三方预测值、真实值、scaler 参数都打印出来看量级。统一规则是所有进入mean_squared_error的数组必须处于同一量纲且inverse_transform用的 scaler 和当初fit的是同一个对象。养成习惯把 scaler 连同模型一起存下来joblib.dump(scaler, scaler.pkl)下次加载时用同一个。5.4 现象验证集 loss 异常低换新数据就废现象训练时val_loss比train_loss还低看着很美好把模型拿去预测新一段数据误差大得没法看。原因时序数据被随机打乱了。如果中间用了train_test_split的默认参数或者手动shuffle训练集里混入了时间上靠后的样本模型等于提前见过未来。另一种可能是validation_split切出来的验证段刚好落在平稳区间而新数据是剧烈波动段。解决所有划分一律按时间顺序切禁用 shuffle。验证集尽量用滚动前向的方式生成也就是每次用前 N 天训练、预测紧接着的 M 天滚动下去这样得到的评估更接近真实使用场景具体做法放到最后一章。5.5 现象同样代码换台机器结果差很多现象本地跑出 TCN 优于 LSTM同事机器上跑出来反过来了参数完全一样。原因随机初始化、Dropout 掩码、数据打乱如果有都受随机种子影响此外 TensorFlow 在不同版本、不同硬件上卷积和矩阵乘的底层实现可能有细微数值差异在千级样本上足以让两个接近的模型互换位置。解决开头统一设种子并在结论里注明跑了几个种子取平均。import os, random import numpy as np import tensorflow as tf def set_seed(seed42): os.environ[PYTHONHASHSEED] str(seed) random.seed(seed) np.random.seed(seed) tf.random.set_seed(seed) # 强制确定性算子牺牲一点速度换可复现 os.environ[TF_DETERMINISTIC_OPS] 1 set_seed(42)逻辑说明——四个种子分别管 Python 哈希、Python 随机、NumPy 随机和张量随机TF_DETERMINISTIC_OPS让部分算子在固定顺序下执行。参数说明——seed取值无所谓关键是全流程统一开了确定性算子后训练会慢一些做对比实验时值得日常调试可以关掉。如果两个模型差距本来就很小建议至少跑 3 个种子取均值再下结论单次结果说服力不够。6. 把感受野和回测做实两个能让结论站得住的动作前面把链路和坑都过了一遍最后补两个能明显提升结论可信度的动作。第一件事把感受野固化成代码里的自检而不是脑算。每次改dilations或time_step先跑一遍这个函数不匹配就直接报错退出比训完才发现问题省时间。def check_receptive_field(time_step, kernel_size, dilations): rf 1 (kernel_size - 1) * sum(dilations) status OK if rf time_step else WARN: 感受野不足 print(ftime_step{time_step} receptive_field{rf} {status}) assert rf time_step, 窗口长度超过感受野最早的时间步不会被任何输出用到 return rf check_receptive_field(time_step10, kernel_size3, dilations(1, 2, 4)) check_receptive_field(time_step10, kernel_size3, dilations(1, 1, 1)) # 这一行会断言失败逻辑说明——把 2.2 节的公式封成函数assert保证窗口长度不超过感受野改配置时立刻暴露问题。参数说明——dilations传入的是各层膨胀率元组长度即层数如果项目里用了残差块每块两层卷积sum(dilations)要按实际层数展开。第二件事用滚动前向回测替代一次性切分。一次性切分只在一个时间点上评估运气成分太大滚动回测把整个测试段切成若干折逐折训练预测最后把各折误差汇总。回测方式训练段测试段优点代价一次性切分前 80%后 20%快一次训练单点评估波动大扩展窗口滚动逐步扩大紧随其后的一段接近真实使用训练次数多固定窗口滚动固定长度滑动紧随其后的一段适应分布漂移早期样本被丢弃def walk_forward(model_fn, series, time_step10, n_splits5, horizon1): 扩展窗口滚动回测返回每折的 RMSE from sklearn.metrics import mean_squared_error fold len(series) // (n_splits 1) errors [] for i in range(1, n_splits 1): train_end fold * (i 1) train_seg series[:train_end] test_seg series[train_end:train_end fold] if len(test_seg) time_step horizon: continue # 每一折重新 fit scaler避免用到未来的极值 sc MinMaxScaler() tr sc.fit_transform(train_seg) te sc.transform(test_seg) X_tr, y_tr create_dataset(tr, time_step, horizon) X_te, y_te create_dataset(te, time_step, horizon) m model_fn((time_step, 1)) m.fit(X_tr, y_tr, epochs30, batch_size32, verbose0) pred sc.inverse_transform(m.predict(X_te).reshape(-1, 1)) true sc.inverse_transform(y_te.reshape(-1, 1)) errors.append(np.sqrt(mean_squared_error(true, pred))) return errors # 用法把 TCN 的构建函数传进去 errs walk_forward(build_tcn, values.ravel(), time_step10, n_splits5) print(各折 RMSE:, [round(e, 4) for e in errs]) print(均值:, round(np.mean(errs), 4), 标准差:, round(np.std(errs), 4))逻辑说明——每一折都重新fitscaler只用当前折之前的训练段这是滚动回测里最容易漏掉的细节n_splits5把数据后段切成五块轮流当测试段误差取均值和标准差标准差大说明模型对区段敏感比单个 RMSE 更有信息量。参数说明——epochs降到 30 是为了控制总训练时间折数多的时候可以再降fold用整除尾部余数直接丢弃不影响结论。真要拿这份项目去做实际对比我会至少跑三组原始三层dilation1、改成(1,2,4)的膨胀版、以及同维度的 LSTM每组跑三个种子再用滚动回测汇总。那次我在一个小样本上只跑了一次切分就下结论说 TCN 更好后来换了个种子结果反过来被追问的时候很被动。从那以后每次做时序模型对比感受野自检和滚动回测都强制走一遍哪怕只是验证一句“这个结构更适合长序列”也不省这两步。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?