简介:这是一份基于LSTM网络的外汇预测模型学习资源,面向计算机相关专业学生、教师及企业员工,可用于深度学习与时间序列分析的入门实践,也可作为课程设计、毕业设计或作业的参考案例。资源包共31个文件,约13.41MB,包含Python脚本、Jupyter Notebook、CSV汇率数据、模型权重与配置文件等,覆盖数据准备、模型训练、测试与预测的完整流程。其中Notebook便于分步阅读与调试,脚本文件封装了通用变量与时间序列处理逻辑,CSV数据可直接用于复现实验。已有48人学习下载。通过阅读README可快速了解依赖安装与使用方法,借助训练与测试代码,读者能掌握LSTM门控机制、数据标准化与划分、模型保存与加载等关键环节,并理解汇率预测中非线性与不确定性的处理思路,适合作为深度学习实战的练手项目。
1. 从一份汇率 CSV 到能跑起来的 LSTM 外汇预测模型
手里只有一份 EUR/USD 的日线 CSV,想用 LSTM 做一个能滚动预测下一根 K 线的模型,这是很多做量化、做金融数据分析的工程师真正会遇到的起点。标题里说的「基于 LSTM 网络的外汇预测模型 Python 源码 数据 模型」,拆开就是三件事:一份时间序列数据、一套 PyTorch 或 Keras 写的 LSTM 源码、一个能保存下来复用的权重文件。它解决的不是「预测明天涨跌发财」这种问题,而是把外汇价格序列喂进 LSTM、跑通训练、拿到可评估的预测输出这条完整链路。适合已经会一点 Python、装过 PyTorch 或 TensorFlow,但没系统做过时间序列预测的人;也适合做过股票预测、想换到外汇数据上验证的熟手。下面按数据、模型、训练、评估、避坑的顺序,把这条链路讲透。
2. 外汇时间序列的数据准备:从原始 CSV 到 LSTM 能吃的张量
2.1 外汇数据和普通 CSV 的差别在哪
外汇数据看起来就是时间加价格,但真正喂模型前有几个绕不开的特点。第一是时间不连续:周末休市,周五收盘到周一开盘之间没有数据,如果直接按行号做滑动窗口,窗口里会混进跨周末的跳跃,模型学到的「相邻两根」其实隔了两天。第二是量纲问题:EUR/USD 在 1.08 附近波动,USD/JPY 在 150 附近波动,如果混着训练,不归一化的话梯度会被大数值主导。第三是缺失和重复:不同数据源对同一分钟的报价可能重复,或者节假日出现空行。
常见做法是只保留收盘价(close)做单变量预测,或者加上最高、最低、开盘做多变量。我一般先用单变量把链路跑通,再考虑加特征。数据来源上,很多公开数据集和券商导出的 CSV 都能用,格式无非是datetime, open, high, low, close, volume这几列,重点是确认时间列能被正确解析成时间戳。
2.2 用 pandas 清洗并构造滑动窗口
下面这段代码做三件事:读 CSV、按时间排序去重、用滑动窗口把序列切成(样本数, 时间步, 特征数)的三维张量。这是 LSTM 输入的标准形状。
import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler # 读取外汇数据,parse_dates 把时间列转成 Timestamp df = pd.read_csv("eurusd_daily.csv", parse_dates=["datetime"]) df = df.sort_values("datetime").drop_duplicates("datetime") df = df.dropna(subset=["close"]) # 收盘价缺失的行直接丢 # 只取收盘价,reshape 成 (n, 1) 供 scaler 使用 close = df["close"].values.reshape(-1, 1) # 归一化到 [0,1],注意 scaler 要保存,预测时要反变换 scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(close) def make_windows(series, lookback=60): """把一维序列切成 (样本, lookback, 1) 的监督学习样本""" X, y = [], [] for i in range(lookback, len(series)): X.append(series[i - lookback:i, 0]) # 过去 lookback 天 y.append(series[i, 0]) # 预测下一天 X = np.array(X).reshape(-1, lookback, 1) y = np.array(y) return X, y LOOKBACK = 60 X, y = make_windows(scaled, LOOKBACK) # 按时间顺序切分,绝不能随机打乱,否则未来信息泄漏 split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] print(X_train.shape, X_test.shape) # 例如 (1200, 60, 1) (300, 60, 1)逻辑说明:make_windows里lookback=60表示用过去 60 个交易日预测第 61 天,这个值不是随便定的,外汇日线常用 20 到 120 之间,太短学不到趋势,太长训练慢且容易过拟合。MinMaxScaler必须fit在训练集上再transform测试集,我这里为了简洁先对全序列 fit,严谨做法是只用训练段 fit,否则测试集的极值会泄漏进归一化参数。切分用X[:split]而不是train_test_split(shuffle=True),时间序列一旦打乱,模型就是在用未来预测过去,评估结果会虚高得离谱。
2.3 归一化和反归一化的坑
归一化本身简单,坑在反变换。模型输出的是 [0,1] 区间的值,你要拿它和真实价格比,必须用同一个 scaler 做inverse_transform。很多人训练时归一化、评估时忘了反变换,算出来的 MAE 是 0.01 这种「看起来很好」的数字,实际对应价格误差是几百点。记住一条:scaler 是模型的一部分,训练完要和权重一起存下来。
3. LSTM 模型结构:PyTorch 源码逐层拆解与参数设置
3.1 为什么外汇预测常用 LSTM 而不是普通 RNN
普通 RNN 在反向传播时梯度会指数衰减,序列一长(比如 60 步)前面的信息基本传不到后面,这就是梯度消失。LSTM 靠输入门、遗忘门、输出门三个门控结构,把「记住什么、忘掉什么」变成可学习的参数,长序列上的表现稳定得多。外汇价格这种带趋势和噪声的序列,LSTM 能捕捉到一定程度的滞后相关性,这也是它在这个场景里被反复使用的原因。GRU 是 LSTM 的简化版,参数少、训练快,效果常常接近,如果算力紧张可以先试 GRU。
3.2 一个最小可用的 LSTM 网络定义
下面用 PyTorch 定义一个单层 LSTM 加全连接输出的模型,输入维度 1,隐藏层 64,输出 1 个预测值。
import torch import torch.nn as nn class LSTMForecaster(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=1, dropout=0.0): super().__init__() self.hidden_size = hidden_size self.num_layers = num_layers # batch_first=True 让输入形状为 (batch, seq, feature) self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout, # num_layers=1 时 dropout 无效 ) self.fc = nn.Linear(hidden_size, 1) # 把隐藏状态映射成单值预测 def forward(self, x): # h0, c0 默认全零,也可显式初始化 out, (h_n, c_n) = self.lstm(x) last = out[:, -1, :] # 取最后一个时间步的隐藏输出 return self.fc(last).squeeze(-1) model = LSTMForecaster(input_size=1, hidden_size=64, num_layers=1) print(sum(p.numel() for p in model.parameters())) # 参数量,约 1.7 万逻辑说明:batch_first=True是关键,PyTorch 的 LSTM 默认输入是(seq, batch, feature),设成 True 后才是我们习惯的(batch, seq, feature),忘了设会导致维度对不上或者结果错乱。out[:, -1, :]取的是序列最后一个时间步的输出,因为我们要预测的是「看完过去 60 天后」的下一天。self.fc把 64 维隐藏状态压成 1 维预测值。
参数说明:hidden_size控制记忆容量,外汇日线数据量通常几千条,64 到 128 够用,再大容易过拟合;num_layers超过 2 层收益递减且训练变慢,一般 1 到 2 层;dropout只在多层时生效,单层想正则化得在 LSTM 外面单独加nn.Dropout。
3.3 训练循环和损失函数怎么选
import torch.optim as optim from torch.utils.data import TensorDataset, DataLoader # 转成 tensor X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32) loader = DataLoader(TensorDataset(X_train_t, y_train_t), batch_size=32, shuffle=True) criterion = nn.MSELoss() # 回归任务用 MSE optimizer = optim.Adam(model.parameters(), lr=1e-3) for epoch in range(50): model.train() total_loss = 0 for xb, yb in loader: optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() # 梯度裁剪,防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_loss += loss.item() if (epoch + 1) % 10 == 0: print(f"epoch {epoch+1}, loss {total_loss/len(loader):.6f}")逻辑说明:shuffle=True在训练集内部打乱 batch 是可以的,因为每个样本内部的时间顺序没被破坏,打乱的是样本之间的顺序,不影响时序建模。clip_grad_norm_是 LSTM 训练的标配,梯度爆炸在长序列上很常见,不裁剪的话 loss 会突然变成 nan。学习率1e-3是 Adam 的常用起点,loss 不降就降到1e-4。
参数说明:batch_size32 或 64 都行,数据量小就用小 batch;epoch看 loss 曲线,通常 30 到 100 之间,配合早停更好。MSE 对异常值敏感,如果数据里有极端跳空,可以换nn.L1Loss()或 HuberLoss。
4. 训练完怎么评估:反归一化、指标和滚动预测
4.1 反归一化后算真实误差
model.eval() with torch.no_grad(): X_test_t = torch.tensor(X_test, dtype=torch.float32) pred_scaled = model(X_test_t).numpy().reshape(-1, 1) # 关键一步:反归一化回真实价格 pred_price = scaler.inverse_transform(pred_scaled).flatten() true_price = scaler.inverse_transform(y_test.reshape(-1, 1)).flatten() from sklearn.metrics import mean_absolute_error, mean_squared_error mae = mean_absolute_error(true_price, pred_price) rmse = np.sqrt(mean_squared_error(true_price, pred_price)) print(f"MAE={mae:.5f}, RMSE={rmse:.5f}")逻辑说明:model.eval()关掉 dropout 等训练态行为,torch.no_grad()省显存。反归一化必须用训练时那个 scaler,这里为了演示用了全序列 fit 的 scaler,严谨项目里要单独保存训练段 scaler。MAE 和 RMSE 是回归常用指标,外汇日线上 MAE 能到 0.002 到 0.005 已经算不错,具体看货币对波动率。
4.2 别被「预测得很像」骗了
把预测曲线和真实曲线画在一起,经常看起来贴合得很好,但这是滞后效应造成的假象:模型输出的往往是前一天的平滑值,曲线整体右移一点就能「像」。真正要看的是方向准确率,也就是预测的涨跌方向和真实涨跌方向一致的比例。
# 用相邻真实值算真实方向,用相邻预测值算预测方向 true_dir = np.sign(np.diff(true_price)) pred_dir = np.sign(np.diff(pred_price)) hit = (true_dir == pred_dir).mean() print(f"方向准确率: {hit:.3f}")如果方向准确率在 0.5 附近,说明模型没学到方向信息,只是拟合了水平。这时候加特征(成交量、其他货币对)、换 lookback、调网络结构都比盲目加 epoch 有用。
4.3 保存模型和 scaler
torch.save({ "model_state": model.state_dict(), "lookback": LOOKBACK, "scaler_min": scaler.data_min_, "scaler_max": scaler.data_max_, }, "lstm_fx.pth")把 scaler 的 min/max 一起存,推理时重建 scaler,避免「训练归一化、推理没归一化」这种低级但高频的翻车。
5. 避坑与排查:外汇 LSTM 训练里最容易翻车的 5 个点
5.1 现象:loss 一直不降,停在某个值不动
原因通常是学习率太大导致震荡,或者输入没归一化,数值范围差几个数量级。解决:先把学习率降到1e-4,确认输入已经过 scaler;再检查batch_first是否设对,维度错了模型其实在学噪声。
5.2 现象:训练 loss 很低,测试 loss 高得离谱
典型过拟合。外汇数据噪声大、有效信号少,模型很容易记住训练段的波动。解决:减小hidden_size、加 dropout、减少num_layers,或者增加训练数据量。别指望靠调大模型解决,方向反了。
5.3 现象:预测曲线整体平移,方向准确率约 0.5
这是滞后预测,模型学的是「下一天约等于今天」。原因可能是 lookback 太长、特征太单一。解决:缩短 lookback 到 20 试试,加入收益率(diff)而不是原始价格作为目标,让模型学变化量而不是水平值。
5.4 现象:评估指标好得不像话,MAE 接近 0
先怀疑数据泄漏。检查是不是用了train_test_split(shuffle=True),是不是在全序列上 fit 了 scaler,是不是把测试段混进了训练。时间序列里这三条任意一条都会让指标虚高。
5.5 现象:换一个货币对,模型完全失效
不同货币对的波动率、趋势性差别很大,在 EUR/USD 上调好的超参不一定适用 USD/JPY。解决:每个货币对单独归一化、单独调 lookback,或者做多货币对联合训练时对每个序列分别标准化。
6. 让预测更靠谱的两个进阶技巧:差分建模与多步滚动
单变量直接预测价格,模型很容易退化成「复制上一天」。我后来习惯改成预测收益率,也就是(price_t - price_{t-1}) / price_{t-1},这样目标序列围绕 0 波动,模型必须真的学方向才能降低 loss。做法很简单,在构造窗口前先算 diff:
ret = df["close"].pct_change().dropna().values.reshape(-1, 1) scaled_ret = MinMaxScaler(feature_range=(-1, 1)).fit_transform(ret) X, y = make_windows(scaled_ret, LOOKBACK)注意这里 scaler 范围改成(-1, 1),因为收益率有正有负。预测出收益率后,用price_t = price_{t-1} * (1 + pred_ret)还原价格。这个改动通常能把方向准确率从 0.5 拉到 0.52 到 0.55,别小看这几个点,外汇上已经有意义。
第二个技巧是多步滚动预测。上面都是单步预测,实际用的时候你想要未来 5 天。做法是把模型预测出的值填回输入序列末尾,再预测下一步,循环 5 次。代价是误差会累积,5 步之后基本只能看趋势不能看点位。我的习惯是:单步预测用来验证模型有没有学到东西,多步预测只用来画趋势参考,绝不拿第 5 步的点位去做决策。
还有一个验证习惯值得养成:把测试集按时间切成 3 段,分别算方向准确率,如果某一段特别差,说明模型对某种市场状态(比如横盘或急涨)不适应,这比看一个总体指标有用得多。做外汇预测这几年,我最大的教训就是别被一条贴合的可视化曲线骗了,方向准确率和分段稳定性才是能拿来做判断的东西。希望帮到你。
本文还有配套的精品资源,点击获取