黄金期货预测实战:多元线性回归与多层感知机神经网络对比

发布时间:2026/10/30 0:47:07
黄金期货预测实战:多元线性回归与多层感知机神经网络对比

简介:这份资源面向量化金融方向的学生、研究者与投资分析人员,围绕中国黄金期货价格预测这一具体场景,系统对比多元线性回归与多层感知机(MLP)神经网络的预测准确性。项目先以多元线性回归刻画价格与影响因子间的线性关系,再引入MLP捕捉非线性模式,并配合时间序列平稳性检验与格兰杰因果检验筛选有效变量,帮助读者理解两类模型在不同情形下的优势与局限,为模型选择提供实证参考。压缩包共13个文件,约225KB,包含2个Python脚本(分别对应线性拟合预测与MLP精度分析)、8张png结果图、1份docx附赠资源、1份txt说明及README.md,覆盖从数据处理、建模到结果可视化的完整流程。目前已有79人学习下载,适合希望以小型案例入门金融时间序列预测、快速复现并对比线性与神经网络方法的读者。

1. 黄金期货预测的两种路线:线性回归与多层感知机到底差在哪

黄金期货价格预测这件事,做的人多,做对的少。我见过太多人拿一段收盘价序列直接丢进模型,跑出一个 R² 0.98 的结果就以为成了,结果一到实盘就翻车。问题往往不在模型本身,而在于没有先搞清楚:这段价格序列到底是不是平稳的,特征之间有没有因果关系,以及线性模型和神经网络各自适合捕捉什么形态的信号。这个项目标题里同时出现了多元线性回归和多层感知机神经网络,还带了时间序列分析、平稳性检验、格兰杰因果检验这几个关键词,说明作者想做的不是简单跑个模型对比,而是走一条完整的量化研究链路:先检验数据性质,再筛选因果特征,最后用两类模型做预测准确性的横向对比。适合谁看?如果你手上有黄金期货的日频或分钟频数据,想搞清楚线性模型和 MLP 在什么条件下谁更准、为什么准,以及整条链路里哪些步骤不能省,这篇就是写给你的。

2. 数据准备与平稳性检验:别拿非平稳序列直接喂模型

2.1 黄金期货价格序列的获取与清洗

做黄金期货预测,数据源常见的有上海期货交易所的黄金主力连续合约、伦敦金现货报价,以及各类数据终端导出的日频 OHLCV。我一般会先把数据整理成一张宽表,字段包括日期、开盘、最高、最低、收盘、成交量、持仓量,然后做三件事:对齐交易日历、处理缺失值、检查异常跳变。

import pandas as pd import numpy as np # 读取黄金期货日频数据,假设已有 CSV 文件 df = pd.read_csv("gold_futures_daily.csv", parse_dates=["date"]) df = df.sort_values("date").reset_index(drop=True) # 对齐交易日历:剔除周末与明显非交易日的空行 df = df[df["volume"] > 0].copy() # 缺失值处理:价格类用前向填充,成交量类用中位数填充 price_cols = ["open", "high", "low", "close"] df[price_cols] = df[price_cols].ffill() df["volume"] = df["volume"].fillna(df["volume"].median()) # 检查异常跳变:单日涨跌幅超过 8% 的标记出来人工确认 df["pct_change"] = df["close"].pct_change() outliers = df[df["pct_change"].abs() > 0.08] print(f"异常跳变记录数:{len(outliers)}")

这段代码的逻辑是先保证时间顺序正确,再处理缺失和异常。参数上,volume > 0这个过滤条件是为了剔除停牌或数据缺失导致的空行;前向填充只用于价格列,因为价格具有连续性,而成交量用中位数填充是为了避免极端值拉偏分布。异常跳变阈值设 8% 是黄金期货日频的常见经验值,超过这个幅度要么是真实极端行情,要么是数据错误,必须人工确认。

2.2 平稳性检验:ADF 与差分阶数的选择

时间序列分析里,平稳性检验是绕不过去的一步。黄金期货收盘价几乎肯定是非平稳的,直接拿去做回归或喂神经网络,模型学到的只是趋势,不是规律。常见做法是先做 ADF 检验,如果不通过就做一阶差分,再检验,直到通过为止。

from statsmodels.tsa.stattools import adfuller def adf_test(series, name): result = adfuller(series.dropna(), autolag="AIC") print(f"{name} ADF 统计量:{result[0]:.4f},p 值:{result[1]:.4f}") for key, value in result[4].items(): print(f" 临界值 {key}:{value:.4f}") return result[1] # 对收盘价做 ADF 检验 p_close = adf_test(df["close"], "收盘价") # 一阶差分 df["close_diff"] = df["close"].diff() p_diff = adf_test(df["close_diff"], "收盘价一阶差分")

ADF 检验的原假设是序列存在单位根,即非平稳。p 值小于 0.05 才能拒绝原假设,认为序列平稳。实际跑下来,收盘价的 p 值经常在 0.5 以上,一阶差分后通常能降到 0.01 以下。如果一阶差分还不平稳,就要考虑二阶差分或对数差分,但阶数越高,信息损失越大,我一般最多做到一阶差分,再不行就换特征构造方式。

提示:差分后的序列长度会少一个样本,做后续建模时要注意对齐索引,别让特征和标签错位。

2.3 格兰杰因果检验:哪些变量真的对预测有用

格兰杰因果检验回答的是一个很实际的问题:变量 X 的过去值能不能帮助预测变量 Y 的当前值。在黄金期货场景里,我通常会检验美元指数、原油价格、美债收益率、VIX 等外生变量对黄金收盘价的因果性。检验结果显著的变量才值得放进特征集,不显著的直接剔除,避免维度灾难。

from statsmodels.tsa.stattools import grangercausalitytests def granger_test(data, cause_col, effect_col, max_lag=5): test_data = data[[effect_col, cause_col]].dropna() results = grangercausalitytests(test_data, maxlag=max_lag, verbose=False) for lag in range(1, max_lag + 1): p_value = results[lag][0]["ssr_ftest"][1] print(f"滞后 {lag} 期:p 值 = {p_value:.4f}") # 假设 df 中已有美元指数列 dxy granger_test(df, "dxy", "close_diff", max_lag=5)

这段代码对每个滞后阶数做 F 检验,输出 p 值。我一般看滞后 1 到 5 期里有没有至少一个 p 值小于 0.05,有就保留该变量。注意,格兰杰因果检验要求序列平稳,所以要用差分后的数据做,否则结果不可信。另外,检验的是统计意义上的因果,不是物理因果,别过度解读。

3. 多元线性回归建模:基线模型怎么搭才靠谱

3.1 特征工程与滞后阶数的确定

线性回归做时间序列预测,核心是把时间序列问题转化成监督学习问题。具体做法是构造滞后特征:用 t-1、t-2、…、t-k 时刻的值预测 t 时刻的值。滞后阶数 k 怎么定?我一般先看自相关函数和偏自相关函数,再结合格兰杰检验的结果。

def create_lag_features(series, n_lags): df_lag = pd.DataFrame({"y": series}) for i in range(1, n_lags + 1): df_lag[f"lag_{i}"] = series.shift(i) return df_lag.dropna() # 用差分后的收盘价构造滞后特征 lag_df = create_lag_features(df["close_diff"], n_lags=5) print(lag_df.head()) print(f"样本数:{len(lag_df)}")

参数说明:n_lags=5表示用前 5 天的差分值预测当天。这个值不是拍脑袋定的,可以先跑一遍不同滞后阶数的回归,看调整后 R² 和 AIC 的变化。我一般会试 3、5、10、20 这几个档位,选验证集误差最小的。注意dropna()会损失前 k 个样本,样本量小的时候要权衡。

3.2 线性回归的训练与系数解读

特征构造好之后,线性回归的训练本身很简单,关键是解读系数和做残差诊断。

from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score X = lag_df.drop(columns=["y"]) y = lag_df["y"] # 按时间顺序切分,不能随机打乱 split_idx = int(len(X) * 0.8) X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:] model_lr = LinearRegression() model_lr.fit(X_train, y_train) y_pred_lr = model_lr.predict(X_test) print("系数:", dict(zip(X.columns, model_lr.coef_.round(4)))) print("截距:", round(model_lr.intercept_, 4)) print(f"MSE:{mean_squared_error(y_test, y_pred_lr):.6f}") print(f"MAE:{mean_absolute_error(y_test, y_pred_lr):.6f}") print(f"R²:{r2_score(y_test, y_pred_lr):.4f}")

系数解读上,如果 lag_1 的系数接近 1 且显著,说明差分序列有一阶自相关;如果多个滞后项系数都很小,说明线性模型可能抓不到什么规律。R² 在差分序列上通常不会很高,0.05 到 0.2 都算正常,别拿原始价格的 R² 来比。残差诊断要看残差是否白噪声,如果还有自相关,说明滞后阶数不够或者模型形式不对。

3.3 线性模型的边界在哪里

线性回归的优势是可解释性强、训练快、不容易过拟合。但它的边界也很明显:它只能捕捉线性关系,对非线性交互、 regime switching、波动率聚集这些黄金期货常见的形态无能为力。我一般把线性回归当基线,如果 MLP 连这个基线都跑不过,那说明网络结构或特征有问题,得回去查。

4. 多层感知机建模:网络结构、训练与调参

4.1 MLP 的网络结构与激活函数选择

多层感知机做时间序列预测,输入层维度等于特征数,输出层一个神经元做回归。隐藏层怎么设?常见做法是两到三层,每层 32 到 128 个神经元。激活函数我一般用 ReLU,输出层不加激活函数。

import torch import torch.nn as nn class GoldMLP(nn.Module): def __init__(self, input_dim, hidden_dims=[64, 32]): super().__init__() layers = [] prev_dim = input_dim for h_dim in hidden_dims: layers.append(nn.Linear(prev_dim, h_dim)) layers.append(nn.ReLU()) layers.append(nn.Dropout(0.2)) prev_dim = h_dim layers.append(nn.Linear(prev_dim, 1)) self.net = nn.Sequential(*layers) def forward(self, x): return self.net(x) input_dim = X_train.shape[1] model_mlp = GoldMLP(input_dim=input_dim, hidden_dims=[64, 32]) print(model_mlp)

结构说明:两层隐藏层,第一层 64 个神经元,第二层 32 个,每层后面接 ReLU 和 Dropout(0.2)。Dropout 是为了防止过拟合,时间序列样本量通常不大,这个参数很关键。输入维度由特征数决定,输出维度是 1,因为做的是回归预测。

4.2 训练循环与早停策略

MLP 训练最容易翻车的地方是过拟合和训练不稳定。我一般用 Adam 优化器,学习率设 1e-3,配合早停。

from torch.utils.data import DataLoader, TensorDataset # 转成张量 X_train_t = torch.tensor(X_train.values, dtype=torch.float32) y_train_t = torch.tensor(y_train.values, dtype=torch.float32).view(-1, 1) X_test_t = torch.tensor(X_test.values, dtype=torch.float32) y_test_t = torch.tensor(y_test.values, dtype=torch.float32).view(-1, 1) train_ds = TensorDataset(X_train_t, y_train_t) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True) optimizer = torch.optim.Adam(model_mlp.parameters(), lr=1e-3) criterion = nn.MSELoss() best_loss = float("inf") patience = 20 counter = 0 for epoch in range(500): model_mlp.train() epoch_loss = 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred = model_mlp(xb) loss = criterion(pred, yb) loss.backward() optimizer.step() epoch_loss += loss.item() * len(xb) epoch_loss /= len(train_ds) if epoch_loss < best_loss: best_loss = epoch_loss counter = 0 torch.save(model_mlp.state_dict(), "best_mlp.pth") else: counter += 1 if counter >= patience: print(f"早停于 epoch {epoch}") break

参数说明:batch_size 设 32 是常见起点,样本少可以降到 16;学习率 1e-3 是 Adam 的默认值,如果 loss 震荡明显可以降到 1e-4;patience 设 20 表示连续 20 个 epoch 验证损失不降就停。注意这里用的是训练损失做早停,严格来说应该划一个验证集,但样本量小的时候可以先用训练损失观察。

4.3 预测与评估:MLP 和线性回归怎么比才公平

评估的时候,两个模型必须用同一份测试集、同一个评估指标。我一般看 MSE、MAE 和方向准确率三个指标。

model_mlp.load_state_dict(torch.load("best_mlp.pth")) model_mlp.eval() with torch.no_grad(): y_pred_mlp = model_mlp(X_test_t).numpy().flatten() mse_mlp = mean_squared_error(y_test, y_pred_mlp) mae_mlp = mean_absolute_error(y_test, y_pred_mlp) # 方向准确率:预测涨跌方向与实际一致的比例 dir_acc_lr = np.mean(np.sign(y_pred_lr) == np.sign(y_test)) dir_acc_mlp = np.mean(np.sign(y_pred_mlp) == np.sign(y_test)) print(f"MLP MSE:{mse_mlp:.6f},MAE:{mae_mlp:.6f}") print(f"线性回归方向准确率:{dir_acc_lr:.4f}") print(f"MLP 方向准确率:{dir_acc_mlp:.4f}")

方向准确率在金融预测里比 MSE 更实用,因为交易看的是方向。如果 MLP 的 MSE 更低但方向准确率没提升,说明它只是拟合了幅度,没抓到方向。我见过不少案例,MLP 在 MSE 上略胜,但方向准确率和线性回归差不多,这时候就要考虑是不是特征里根本没有非线性信号。

5. 避坑与排查:黄金期货预测里最容易翻车的五件事

5.1 用原始价格做回归,R² 虚高

现象:线性回归 R² 跑到 0.95 以上,看起来效果极好。原因:原始价格序列非平稳,模型学到的只是趋势,测试集和训练集共享同一个趋势,R² 自然高。解决:先做平稳性检验,用差分后的序列建模,R² 降到 0.1 左右才是正常水平。

5.2 随机切分时间序列,造成未来信息泄露

现象:测试集效果远好于预期,实盘一塌糊涂。原因:用train_test_split默认随机打乱,导致训练集里混入了未来数据。解决:按时间顺序切分,前 80% 做训练,后 20% 做测试,绝对不能 shuffle。

5.3 格兰杰检验用了非平稳序列

现象:检验结果一大堆显著,但模型效果没提升。原因:格兰杰因果检验要求序列平稳,用原始价格做检验,F 统计量分布不对,p 值不可信。解决:先差分再做检验,或者用协整检验处理非平稳序列的长期关系。

5.4 MLP 训练 loss 不降或震荡

现象:训练 loss 一直在高位震荡,或者降到某个值就不动了。原因:学习率太大、特征没标准化、或者网络太深。解决:先做特征标准化,学习率降到 1e-4 试试,隐藏层从两层减到一层,观察 loss 曲线。

5.5 用 MSE 选模型,忽略方向准确率

现象:MLP 的 MSE 比线性回归低,但实盘收益没改善。原因:MSE 衡量的是幅度误差,金融预测更看重方向。解决:把方向准确率作为主要评估指标,MSE 作为辅助,两个模型在同一测试集上对比。

6. 进阶技巧:用滚动窗口验证和特征重要性分析提升可信度

走到这里,模型已经能跑通了,但离可信还差一步。我一般会做两件事:滚动窗口验证和特征重要性分析。

滚动窗口验证的做法是:用前 N 天训练,预测第 N+1 天,然后窗口向前滑动一天,重新训练,再预测。这样能模拟实盘的滚动预测场景,比单次切分更可靠。

def rolling_window_eval(X, y, model_fn, window_size=200, step=1): preds, actuals = [], [] for start in range(0, len(X) - window_size - step, step): end = start + window_size X_train_rw = X.iloc[start:end] y_train_rw = y.iloc[start:end] X_test_rw = X.iloc[end:end + step] y_test_rw = y.iloc[end:end + step] model = model_fn() model.fit(X_train_rw, y_train_rw) pred = model.predict(X_test_rw) preds.extend(pred) actuals.extend(y_test_rw) return np.array(preds), np.array(actuals) preds_lr, actuals_lr = rolling_window_eval(X, y, lambda: LinearRegression(), window_size=200) print(f"滚动窗口线性回归方向准确率:{np.mean(np.sign(preds_lr) == np.sign(actuals_lr)):.4f}")

参数说明:window_size=200表示每次用 200 个样本训练,这个值可以根据数据量调整,一般不少于特征数的 10 倍;step=1表示每天滚动一次,计算量大但最贴近实盘。如果数据量很大,可以设 step=5 或 10 降低计算成本。

特征重要性分析方面,线性回归看系数绝对值,MLP 可以用 permutation importance:把某一列特征随机打乱,看模型误差增加多少,增加越多说明该特征越重要。

from sklearn.inspection import permutation_importance def mlp_predict(X_np): model_mlp.eval() with torch.no_grad(): return model_mlp(torch.tensor(X_np, dtype=torch.float32)).numpy().flatten() result = permutation_importance( model_mlp, X_test.values, y_test.values, n_repeats=10, random_state=42, scoring="neg_mean_squared_error" ) for i, col in enumerate(X.columns): print(f"{col}:重要性均值 {result.importances_mean[i]:.6f}")

我自己的习惯是,每次做完一轮模型对比,先把滚动窗口验证跑一遍,如果滚动窗口的方向准确率能稳定在 52% 以上,再考虑进一步调参或加特征。低于这个数,说明特征里没有可用的信号,换模型也是白搭。黄金期货预测这件事,模型只是最后一步,前面的平稳性检验和因果筛选才是决定上限的地方。希望帮到你。

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