news 2026/9/27 20:46:55

Python实战:TCN时间卷积网络预测外汇价格,对比RNN与LSTM

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python实战:TCN时间卷积网络预测外汇价格,对比RNN与LSTM

简介:这份资源面向初级及以上水平的数据科学家、机器学习爱好者及时间序列分析从业者,以Python结合TensorFlow构建时间卷积神经网络(TCN)完成时间序列预测为主线,重点解决长序列建模中RNN易梯度消失、长程依赖捕捉不足的问题。项目以外汇市场历史数据为示范场景,完整覆盖数据生成与归一化、滑动窗口样本构造、因果卷积与膨胀卷积搭建、模型训练及预测结果可视化,并给出TCN与LSTM两类网络的预测效果对比图,便于直观理解精度差异。资源包为1个docx文档,约31KB,内含完整程序代码、执行结果截图与讲解说明,读者可据此复现实验、调整卷积层数、滤波器数量与时间步等参数,掌握从数据准备到模型评估的全流程实现思路。目前已有171人学习,适合希望将TCN落地于金融趋势等长期数值预测任务的学习者参考。

1. 从外汇价格到 TCN:这份 Python 项目到底能跑出什么

很多人第一次接触时间序列预测,都是从 LSTM 或者 RNN 入手的,结果训练半天 loss 不降、预测滞后、调参调到怀疑人生。这份项目实例换了个思路,用时间卷积神经网络(TCN)来做外汇价格走势预测,并且直接和 RNN 做了同台对比。TCN 的核心优势在于因果卷积加膨胀卷积的组合,既能捕捉长程依赖,又避开了 RNN 那种梯度消失的老毛病。项目里给了一套完整的 Python 代码,从模拟外汇数据生成、归一化预处理、模型搭建、训练到 RMSE 评估和可视化,全流程都能跑通。适合已经会 Python 基础语法、装过 TensorFlow、想找一个能直接复现的时序预测项目练手的人。下面我按实际拆包的顺序,把这份资源里的关键环节和容易翻车的地方一条条讲清楚。

2. TCN 模型结构拆解:因果卷积和膨胀卷积到底怎么配

2.1 为什么 TCN 比 RNN 更适合长序列

RNN 和 LSTM 处理序列的方式是逐步递归,每一步的输出依赖上一步的隐藏状态。序列一长,梯度在反向传播时反复相乘,要么爆炸要么消失,这是结构性的问题,不是调参能根治的。TCN 换了一种打法:用一维卷积沿时间轴滑动,配合 causal padding 保证第 t 步的输出只看到 t 步及之前的输入,不会偷看未来数据。再叠加膨胀卷积(dilated convolution),卷积核的感受野随层数指数级扩大。举个例子,kernel_size=3、膨胀系数按 1、2、4、8 递增时,四层之后感受野就能覆盖到 2^4 级别的时间步,长程依赖照样抓得住。

项目里的 TCN 实现用的是三层 Conv1D 堆叠,每层 filters=64、kernel_size=3、padding='causal',中间插了 Dropout(0.2) 做正则化。这个结构不算深,但对于 1000 条模拟外汇数据、time_step=10 的输入窗口来说够用了。实际生产中如果序列更长、模式更复杂,常见做法是把层数加到 4 到 6 层,同时按膨胀系数 1、2、4、8、16 递增来扩大感受野。

2.2 因果卷积的实现细节与参数含义

先看项目里 TCN 模型的核心代码:

import tensorflow as tf from tensorflow.keras.layers import Input, Conv1D, Dense, Dropout from tensorflow.keras.models import Model def build_tcn_model(input_shape): inputs = Input(shape=input_shape) # 第一层因果卷积:filters=64 表示输出 64 个特征图 # kernel_size=3 表示每次看 3 个时间步 # padding='causal' 保证不泄露未来信息 x = Conv1D(filters=64, kernel_size=3, padding='causal', activation='relu')(inputs) x = Dropout(0.2)(x) # 随机丢弃 20% 神经元,防止过拟合 # 第二层:同样的配置,进一步提取时序特征 x = Conv1D(filters=64, kernel_size=3, padding='causal', activation='relu')(x) x = Dropout(0.2)(x) # 第三层:不加 Dropout,直接输出给全连接层 x = Conv1D(filters=64, kernel_size=3, padding='causal', activation='relu')(x) # 输出层:Dense(1) 把 64 维特征映射成单个预测值 outputs = Dense(1)(x) model = Model(inputs, outputs) model.compile(optimizer='adam', loss='mean_squared_error') return model tcn_model = build_tcn_model((X_train.shape[1], 1)) tcn_model.summary() tcn_model.fit(X_train, y_train, epochs=50, batch_size=32, validation_split=0.1)

这段代码里几个参数值得单独说。filters=64决定了每层输出的特征通道数,太小会欠拟合,太大训练慢且容易过拟合,64 是一个比较稳的起点。kernel_size=3是每次卷积覆盖的时间步数,项目里没有用膨胀卷积,如果你要处理更长的序列,可以改成Conv1D(filters=64, kernel_size=3, dilation_rate=2, padding='causal')这样逐层递增膨胀率。padding='causal'是 TCN 区别于普通 CNN 的关键,它会在序列左侧补零,保证卷积只看历史不看未来。Dropout(0.2)的 0.2 是丢弃比例,数据量小的时候可以适当加大到 0.3,数据量大就降到 0.1。

注意,项目里第三层 Conv1D 后面没有接 Dropout,直接连到 Dense 输出层。这个设计是有意的——最后一层卷积的输出已经比较抽象了,再加 Dropout 可能丢太多信息。但如果你发现验证集 loss 波动很大,可以在第三层后面也补一个 Dropout(0.1) 试试。

2.3 RNN 对照组的搭建与公平比较

项目同时给了一个 LSTM 模型做对照,代码很简洁:

from tensorflow.keras.layers import LSTM def build_rnn_model(input_shape): model = tf.keras.Sequential() # LSTM(64):64 个隐藏单元,输入形状为 (time_step, 1) model.add(LSTM(64, input_shape=input_shape)) model.add(Dropout(0.2)) model.add(Dense(1)) # 输出单个预测值 model.compile(optimizer='adam', loss='mean_squared_error') return model rnn_model = build_rnn_model((X_train.shape[1], 1)) rnn_model.summary() rnn_model.fit(X_train, y_train, epochs=50, batch_size=32, validation_split=0.1)

LSTM 的隐藏单元数设成 64,和 TCN 的 filters 对齐,这样比较才公平。优化器、损失函数、epochs、batch_size 全部一致,唯一变量就是模型结构。这种对照实验的设计思路值得学——很多人做对比时这边用 Adam 那边用 SGD,或者 epochs 不一样,跑出来的结论根本站不住脚。

从实际运行结果看,TCN 在测试集上的 RMSE 通常会比 LSTM 低一些,尤其是在序列较长、模式变化较缓的场景下。但差距不会特别夸张,因为模拟数据本身噪声就大。真正拉开差距的是训练速度——TCN 的卷积可以并行计算,LSTM 必须逐步递归,同样 epochs 下 TCN 快不少。

3. 数据预处理流水线:从原始价格到模型可吃的 3D 张量

3.1 模拟外汇数据生成与归一化

项目用随机游走的方式生成了 1000 天的模拟外汇价格数据:

import numpy as np import pandas as pd np.random.seed(42) # 固定随机种子,保证每次生成的数据一样 dates = pd.date_range(start='2020-01-01', periods=1000, freq='D') # 用正态分布生成每日收益率,累加后加上基准价 100 prices = np.random.normal(loc=1.0, scale=0.01, size=len(dates)).cumsum() + 100 data = pd.DataFrame({'Date': dates, 'Price': prices}) data.set_index('Date', inplace=True) data.to_csv('forex_data.csv')

np.random.seed(42)这行很关键,不设种子的话每次跑出来的数据都不一样,调参时根本分不清是模型变了还是数据变了。loc=1.0, scale=0.01控制的是每日价格变动的均值和标准差,模拟的是波动比较温和的外汇品种。如果你要模拟波动更大的品种,把 scale 调到 0.02 甚至 0.05。

生成完数据后必须做归一化,否则模型会被大数值主导:

from sklearn.preprocessing import MinMaxScaler data = pd.read_csv('forex_data.csv', index_col=0) scaler = MinMaxScaler() scaled_data = scaler.fit_transform(data)

MinMaxScaler 把所有价格压缩到 [0, 1] 区间。这里有个容易翻车的地方:fit_transform是在全量数据上做的,严格来说应该只在训练集上 fit,然后 transform 测试集。项目为了简化流程没这么做,实际项目中要改过来,否则测试集的信息会泄露到归一化参数里。

3.2 滑动窗口构造与 3D 张量变形

时间序列预测的核心操作是把一维序列切成 (输入窗口, 预测目标) 的样本对:

def create_dataset(data, time_step=1): X, y = [], [] for i in range(len(data) - time_step): # X 取连续 time_step 个数据点作为输入 X.append(data[i:(i + time_step), 0]) # y 取紧接着的下一个数据点作为预测目标 y.append(data[i + time_step, 0]) return np.array(X), np.array(y) time_step = 10 # 用过去 10 天的价格预测第 11 天 X, y = create_dataset(scaled_data, time_step) train_size = int(len(X) * 0.8) # 80% 训练,20% 测试 X_train, X_test = X[:train_size], X[train_size:] y_train, y_test = y[:train_size], y[train_size:] # 变形为 [样本数, 时间步, 特征数],Conv1D 和 LSTM 都要求 3D 输入 X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], 1) X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], 1)

time_step=10意味着模型每次看过去 10 天的价格来预测下一天。这个值怎么选?太小了模型看不到趋势,太大了训练样本数减少且容易过拟合。对于日频外汇数据,10 到 30 是比较常见的范围。你可以写个循环试 [5, 10, 20, 30],看哪个 RMSE 最低。

reshape 那两行也是新手常卡住的地方。Conv1D 和 LSTM 的输入必须是 3D 的:第一维是样本数量,第二维是时间步数,第三维是每个时间步的特征数。这里只有价格一个特征,所以最后一维是 1。如果你加了成交量、利率等多个特征,最后一维就改成对应的特征数。

3.3 训练集与测试集的划分边界

项目用的是最简单的顺序切分:前 80% 做训练,后 20% 做测试。时间序列不能随机打乱,因为打乱会破坏时间依赖关系,造成未来数据泄露到训练集。这一点和普通分类任务完全不同,很多人从图像分类转过来做时序预测,习惯性用train_test_split加shuffle=True,结果模型在测试集上表现好得离谱,上线就崩。

另外注意,项目在create_dataset之后才切分训练测试,这意味着 X_train 的最后一个样本和 X_test 的第一个样本在原始序列上是相邻的。严格来说应该先切分原始序列,再分别做滑动窗口,避免边界重叠。不过对于 1000 条数据、10 步窗口来说,这点重叠影响很小,知道有这回事就行。

4. 训练、预测与 RMSE 评估:怎么判断 TCN 真的比 RNN 强

4.1 预测结果的反归一化与 RMSE 计算

模型训练完之后,预测出来的值还在 [0, 1] 区间,必须用 scaler 反归一化回原始价格尺度才能算有意义的误差:

from sklearn.metrics import mean_squared_error # TCN 预测并反归一化 tcn_predictions = tcn_model.predict(X_test) tcn_predictions = scaler.inverse_transform(tcn_predictions) # RNN 预测并反归一化 rnn_predictions = rnn_model.predict(X_test) rnn_predictions = scaler.inverse_transform(rnn_predictions) # 真实值也要反归一化才能对比 y_test_original = scaler.inverse_transform(y_test.reshape(-1, 1)) # 计算 RMSE tcn_rmse = np.sqrt(mean_squared_error(y_test_original, tcn_predictions)) rnn_rmse = np.sqrt(mean_squared_error(y_test_original, rnn_predictions)) print(f'TCN RMSE: {tcn_rmse}') print(f'RNN RMSE: {rnn_rmse}')

这里有个细节容易忽略:y_test.reshape(-1, 1)是必须的,因为 scaler 的inverse_transform要求输入是 2D 数组。不 reshape 会直接报错。另外,scaler.inverse_transform对预测值和真实值用的是同一套参数,这没问题,因为归一化时就是统一处理的。

RMSE 的单位和原始价格一致,比如 RMSE=0.35 意味着预测值平均偏离真实价格 0.35 个单位。但 RMSE 的绝对值好不好,取决于价格本身的波动范围。如果价格在 100 附近波动、日波动 0.5 左右,那 RMSE=0.35 算可以接受;如果日波动只有 0.1,那 0.35 就太大了。所以看 RMSE 的同时要结合数据本身的统计特征。

4.2 可视化对比与结果解读

项目最后用 Matplotlib 画了三条曲线做对比:

import matplotlib.pyplot as plt plt.figure(figsize=(14, 7)) plt.plot(y_test_original, label='真实价格', color='blue') plt.plot(tcn_predictions, label='TCN 预测', color='orange') plt.plot(rnn_predictions, label='RNN 预测', color='green') plt.title('外汇时间序列预测') plt.xlabel('时间步') plt.ylabel('价格') plt.legend() plt.show()

从图上能看出几个典型现象。TCN 的预测曲线通常更贴合真实曲线的局部波动,而 LSTM 的预测往往有滞后——真实价格拐头了,LSTM 还沿着原来的方向走。这是因为 LSTM 的递归结构对近期输入更敏感,对趋势变化的响应慢半拍。TCN 的卷积核直接覆盖多个时间步,对拐点的捕捉更及时。

但也要注意,模拟数据本身是随机游走生成的,没有真实的市场规律,所以两个模型的差距不会特别大。如果你换成真实外汇数据,TCN 的优势可能会更明显,也可能因为市场噪声太大而优势被淹没。这取决于数据本身的信噪比。

4.3 训练过程中的验证集监控

项目在 fit 里设了validation_split=0.1,也就是从训练集里再切 10% 做验证。这个验证集不参与梯度更新,只用来监控过拟合。训练时注意看 loss 和 val_loss 的曲线:如果 loss 持续下降但 val_loss 开始上升,说明模型开始死记训练数据了,该加 Dropout 或者减层数了。如果两个都降不下去,说明模型容量不够,该加 filters 或者加层。

常见做法是加一个 EarlyStopping 回调,val_loss 连续 5 个 epoch 不降就自动停:

from tensorflow.keras.callbacks import EarlyStopping early_stop = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True) tcn_model.fit(X_train, y_train, epochs=100, batch_size=32, validation_split=0.1, callbacks=[early_stop])

patience=5是容忍 5 个 epoch 没有改善,restore_best_weights=True是训练结束后恢复到验证集 loss 最低的那组权重。这两个参数能省不少手动调参的时间。

5. 避坑与排查:TCN 时序预测里最容易翻车的五个地方

5.1 归一化泄露:测试集信息污染训练过程

现象:模型在测试集上 RMSE 很低,看起来效果很好,但换一批新数据预测就完全不准。

原因:项目里用scaler.fit_transform(data)在全量数据上做了归一化,测试集的 min/max 信息被编码进了 scaler,相当于模型间接看到了测试集的范围。

解决:只在训练集上 fit scaler,然后分别 transform 训练集和测试集:

scaler = MinMaxScaler() train_scaled = scaler.fit_transform(data[:train_size_raw]) test_scaled = scaler.transform(data[train_size_raw:])

注意fit_transform和transform的区别——前者会重新计算 min/max,后者用已有的参数。

5.2 输入形状不匹配:Conv1D 和 LSTM 的 3D 要求

现象:ValueError: Input 0 of layer conv1d is incompatible with the layer: expected ndim=3, found ndim=2。

原因:忘了 reshape 成 3D 数组,或者 reshape 的维度顺序搞错了。

解决:确认输入形状是(样本数, 时间步, 特征数)。如果只有价格一个特征,最后一维是 1。用X_train.shape打印出来检查,确保是 3 个维度。另外,input_shape参数只写后两维(time_step, 1),不要带样本数。

5.3 预测结果全是同一个值:模型没学到东西

现象:TCN 或 RNN 的预测曲线是一条水平直线,不管输入怎么变输出都一样。

原因:学习率太大导致梯度爆炸后参数锁死,或者数据归一化后所有值挤在一起区分度太低,或者 epochs 太少模型还没收敛。

解决:先把 epochs 加到 100 看看 loss 有没有下降。如果 loss 一直不动,把 Adam 的学习率从默认的 0.001 降到 0.0001。如果还不行,检查归一化后的数据标准差是不是接近 0——如果是,说明原始数据本身变化太小,需要换数据或者换归一化方式。

5.4 训练集 loss 低但测试集 loss 高:过拟合

现象:训练集 RMSE 0.1,测试集 RMSE 1.5,差距巨大。

原因:模型参数太多、训练数据太少、训练轮数太多,模型把训练集的噪声也学进去了。

解决:三个方向同时下手——加大 Dropout 到 0.3 或 0.4,减少 Conv1D 的 filters 从 64 降到 32,加 EarlyStopping 提前停。如果数据量实在太小,考虑用数据增强(加噪声、时间窗口滑动)来扩充训练样本。

5.5 反归一化时报维度错误

现象:ValueError: Expected 2D array, got 1D array instead。

原因:scaler.inverse_transform要求输入是 2D 数组,但y_test或预测结果可能是 1D 的。

解决:统一 reshape 成(-1, 1):

y_test_original = scaler.inverse_transform(y_test.reshape(-1, 1)) tcn_predictions = scaler.inverse_transform(tcn_predictions.reshape(-1, 1))

-1表示这一维自动计算,1表示每个样本一个值。这个操作在时序预测里几乎每次都要做,记住就行。

6. 进阶技巧:把 TCN 从 demo 推到能用的状态

项目里的 TCN 是一个能跑通的最小实现,但离实际可用还有距离。我一般会在这几个地方做加强。

第一是加膨胀卷积。项目里的三层 Conv1D 膨胀率都是默认的 1,感受野只有 7 个时间步(3 层 × kernel_size 3)。改成dilation_rate=1, 2, 4之后,感受野扩展到 1+2+4 的覆盖范围,能捕捉更长的依赖。代码改动很小:

x = Conv1D(filters=64, kernel_size=3, dilation_rate=1, padding='causal', activation='relu')(inputs) x = Conv1D(filters=64, kernel_size=3, dilation_rate=2, padding='causal', activation='relu')(x) x = Conv1D(filters=64, kernel_size=3, dilation_rate=4, padding='causal', activation='relu')(x)

第二是加残差连接。TCN 原论文里每层卷积外面套了一个残差块,输入直接加到输出上。这样梯度能更顺畅地回传,深层网络也能训得动。Keras 里用Add()层实现:

from tensorflow.keras.layers import Add residual = inputs x = Conv1D(filters=64, kernel_size=3, padding='causal', activation='relu')(inputs) x = Dropout(0.2)(x) x = Conv1D(filters=64, kernel_size=3, padding='causal')(x) x = Add()([residual, x]) # 残差连接 x = tf.keras.layers.Activation('relu')(x)

注意残差连接要求输入和输出的维度一致,如果 filters 变了需要加一个 1×1 卷积做投影。

第三是换真实数据。项目用的是随机生成的模拟数据,练手可以,但真要评估 TCN 的效果得用真实外汇历史数据。常见做法是从公开数据源拉取日频 OHLC 数据,取收盘价作为预测目标,再加一些技术指标(移动平均、RSI、布林带宽度)作为额外特征。特征多了之后,输入形状从(time_step, 1)变成(time_step, N),模型结构不用大改,Conv1D 会自动适配。

第四是评估指标别只看 RMSE。RMSE 对大误差敏感,但外汇预测里方向对不对往往比数值准不准更重要。加一个方向准确率:

direction_true = np.sign(np.diff(y_test_original.flatten())) direction_pred = np.sign(np.diff(tcn_predictions.flatten())) direction_acc = np.mean(direction_true == direction_pred) print(f'方向准确率: {direction_acc:.2%}')

这个指标算的是涨跌方向判断对了多少,50% 是瞎猜,60% 以上就有参考价值了。

最后说一个我自己的习惯:每次跑完模型,先把预测值和真实值的前 20 个点打印出来肉眼扫一遍,再画图。因为 RMSE 有时候会骗人——两个模型 RMSE 差不多,但一个是在拐点处错、一个是在平稳段错,实际使用价值完全不同。从那以后我每次做时序预测都强制走一遍「打印前 20 个点 → 画全量对比图 → 算方向准确率」这三步,再下结论。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 20:46:24

IEC104规约遥控遥调全解析:报文结构、选择执行机制与调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 20:46:04

网站建设和网袷宣传从零搭建实战避坑指南

网站建设和网袷宣传从零搭建实战避坑指南 网站上线三个月,后台流量个位数,转化率几乎为零? 这是很多老板和项目经理最头疼的噩梦。 别再怪推广费烧得慢,问题往往出在 从零搭建 的底层逻辑上。 很多人以为, 网站建设和网袷宣传 就是找个模板,把 Logo 换掉,发几篇新闻就完事了。 大错特错。…

作者头像 李华
网站建设 2026/9/27 20:45:58

1.6T光模块量产前夜:上游产业链卡位与价值量拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 20:45:52

张家港网站建设培训班:从零搭建避坑指南

张家港网站建设培训班:从零搭建避坑指南 域名买好了,服务器租了,结果网站打不开,或者后台乱码,这是很多想学建站的朋友遇到的第一道坎。 域名服务器搞不懂 ,就像买了食材却不会开火,全白搭。在张家港,想 从零搭建 一个能跑、能搜、能转化的网站,光靠死磕技术文档太慢,找个靠谱的培训班确实能省不少弯路。…

作者头像 李华
网站建设 2026/9/27 20:45:45

3个建站避坑点:搞懂网站宣传工作别瞎下载源码

3个建站避坑点:搞懂网站宣传工作别瞎下载源码 找建站公司最怕什么?不是技术不行,是报价离谱,最后发现核心功能还要加钱。很多SEO从业者自己搞网站宣传工作时,直接去搜“源码下载”,结果装了一堆没用的模板,后台改个颜色都得看半天文档。其实,网站宣传工作不只是把页面做出来,更得考虑后续的维护成本、SEO友…

作者头像 李华
网站建设 2026/9/27 20:45:20

虚拟主机建wordpress避坑指南:3个档位报价拆解

虚拟主机建wordpress避坑指南:3个档位报价拆解 改个需求建站公司拖一周,这种憋屈事不少中小企业主都经历过。其实,很多时候不是对方技术不行,而是你没搞懂背后的成本结构。今天这篇 避坑指南 ,不聊虚的,直接拆解用 虚拟主机建wordpress…

作者头像 李华