
简介《基于图深度学习的大宗商品价格预测研究》是一篇发表于《计算机应用与软件》2021年第7期的学术论文PDF适合从事深度学习、数据分析、量化研究的学生、研究员与从业者阅读也可作为论文写作或课题立项的参考文献。文章针对产业链上多种大宗商品价格间存在非线性相关性与上下游联动效应、经典预测算法难以有效建模的问题提出一种基于图深度学习的价格预测方法。该方法以产品现货价、期货价、产量等作为节点属性以上下游关系经产品结构熵量化后的结果为边属性并以CCPI、PPI作为全局属性对产业链产品价格进行预测。实验结果表明所提模型可行有效相比单变量LSTM与多变量LSTM预测准确度更高。资源包内仅有1个PDF文件约2.21MB总学习人数已达200人内容完整呈现了研究思路、模型设计与实验对比是了解图神经网络在大宗商品价格预测中应用的实用资料。1. 当“价格预测”的邻居不再只是时间戳一个做原油贸易的朋友曾向我诉苦他同时跟踪 WTI 原油、沪铜、大豆到岸价发现单看每个品种的时间序列模型都很难获得超额收益但在一次原油主产国港口调度出问题后同一条产业链上的沥青、燃料油、PTA 几乎同步异动而且是先于新闻标题的。这就是大宗商品价格预测里最常见的“关联先行”现象。传统 LSTM、Transformer 这类序列模型默认把各商品当作独立通道实际上断裂了产业链、库存区域、宏观因子之间的结构信息。基于图深度学习的价格预测就是把“相关商品和上下游关系”显式建模成图用 GNN 的邻居聚合机制把联动信息编码进预测。它适合已经跑通过时间序列基线、想要提升多商品联合预测精度和可解释性的研究者和工程师。2. 构建价格预测图节点、边和特征设计2.1 为什么商品价格适合用图建模大宗商品价格有两个固有属性一是强周期性二是强传导性。传导性源于产业链铁矿石 - 螺纹钢 - 热卷 - 汽车用钢原油 - 石脑油 - 乙烯 - PE/PP。传统滑动窗口模型把每个品种的滞后价格作为特征只能捕捉到“品种 A 过去 5 天涨跌对品种 B 今天的影响”却丢失了“传导路径上中间环节的库存堆积”这类高阶信息。图神经网络的核心是消息传递每一层卷积都在做“把自己的一阶邻居信息聚合到自身”两层 GCN 实际上就能覆盖二阶传导路径。这就把产业链结构直接嵌入了学习目标。另外宏观因子美元指数、海运费、港口库存天然适合作为全局节点或环境节点连接所有商品节点而不是把它们作为拼接到每个商品向量里的标量。这样 GNN 可以学习到“宏观因子对不同商品的影响强度不同”。从选型角度讲只要你有 10 个以上存在已知关联关系的商品且这些关联不是单纯的时间相关性图建模就值得尝试。你可能已经画过滚动相关矩阵热力图但热力图的问题是不可学习、不可扩展它只能给出一对品种的相关性不能把“A 通过 B 影响 C”的传递关系显式输入模型。而图神经网络把结构学习和节点表示学习放在同一个优化目标里这才是它和传统“用相关性做特征”的本质区别。2.2 三种可落地的构图方式下面给出三种经过验证的构图方案。注意这个“经过验证”指的是常见做法不是某个特定项目的结论。构图方式节点定义边定义适用场景产业链静态图每种商品上下游流向关系边权为投入产出系数或 Hodrick-Prescott 相关系数有明确产业链图谱的农产品、金属、能化品种动态相关图每种商品 关键宏观变量边由滚动相关性计算每 T 天重建一次图品种之间关联不稳定、需要频繁调整持仓的量化场景因子超图商品、产业、宏观因子三类节点超边连接“商品-产业-宏观”三元组数据来源多但关系稀疏需要显式建模高维交互的场景动态相关图是最常见的起步方案。边权可以用滚动 60 日 Spearman 相关系数再做硬阈值比如绝对值小于 0.3 的边直接删掉或软阈值保留但降低权重。静态产业链图的好处是稳定、可解释但面临“产业链关系是定性知识难以数字化”的问题实践中可以用投入产出表或者行业调研数据。在构造图之前先想清楚一个问题你希望 GNN 学到的“邻居”是价格同步性还是因果传导链同步性适合做相关性套利因果传导链适合做基本面驱动的事件策略。这两种意图对应的边权算法完全不同。我在一开始做的时候直接一股脑把所有高相关的边都加上结果模型严重过度平滑后来改用阈值 供应商集中度加权才把噪声压下来。构图规模也需要控制。节点数量在 10-30 个之间每个商品最好都有 3-8 条有效边。边太密GCN 的每一层输出都等于对所有邻居做平均节点表示趋同这就是过平滑边太稀则消息传递不到远端的传导节点。一个实用做法是先按静态产业链建主干边再动态添加滚动相关性超过阈值的辅助边最后用图稀疏化如 k-最近邻剪枝把每个节点的度限制到 15 以内。2.3 用 Python 构造一张产业链图的代码示例2.3.1 节点特征构造下面给出一段构筑图的代码。我们在做多商品预测时常用的特征是“收益率 波动率 成交量变化率”而不是绝对价格因为绝对价格的量纲差异会主导 GNN 的注意力分配。import pandas as pd import numpy as np # 假设 prices 是 (N, T) 的 DataFrameN 个商品T 个交易日收盘价 returns prices.pct_change().iloc[1:] window 20 features {} for col in prices.columns: # 滚动收益率均值反映近期趋势 mom returns[col].rolling(window).mean().values # 滚动波动率反映不确定性 vol returns[col].rolling(window).std().values # 5 日与 20 日动量差反映短期加速 accel (returns[col].rolling(5).mean() - returns[col].rolling(20).mean()).values features[col] np.stack([mom, vol, accel], axis1) # 对齐时间戳去掉 NaN ts prices.index[window:] feature_matrix np.stack([features[c][window-1:] for c in prices.columns], axis1) # shape: (T, N, 3)这里我们构造了“趋势、波动、加速度”三个维度。注意vol和accel都是滚动窗口计算必须保证在时序上只使用历史数据否则会产生前视偏差。feature_matrix的最终形状是(T, N, 3)其中 T 是有效交易日数N 是商品数。这一设计比把绝对价格直接输入要稳健原因在于价格的量纲差异铜价和豆粕价格相差两个数量级会主导第一层图卷积的梯度。2.3.2 边权计算和 Data 对象构造接下来把滚动相关性转成边索引和边权。为了避免每次重新计算相关矩阵可以按交易日窗口滑动并缓存。这里给出核心构造逻辑from torch_geometric.data import Data import torch def build_temporal_graph(returns, feature_matrix, corr_window20, threshold0.3): T, N, F feature_matrix.shape data_list [] for t in range(T): if t corr_window: # 冷启动阶段用自环图兜底 src torch.arange(N).repeat(N) dst torch.arange(N).unsqueeze(1).repeat(1, N).flatten() weights torch.ones(N * N, dtypetorch.float) else: # 只用 t-corr_window 到 t 的数据计算 Spearman 相关 hist returns.iloc[t-corr_window1:t1] corr hist.corr(methodspearman).values mask np.abs(corr) threshold src, dst np.nonzero(mask) weights torch.tensor(corr[src, dst], dtypetorch.float) edge_index torch.tensor(np.stack([src, dst], axis0), dtypetorch.long) x torch.tensor(feature_matrix[t], dtypetorch.float) data Data(xx, edge_indexedge_index, edge_attrweights.unsqueeze(1)) data_list.append(data) return data_list这段代码的关键在于hist returns.iloc[t-corr_window1:t1]矩阵的每一行是日期每列是商品所以corr的索引必须严格对应“截至当前时刻 t 的数据”。许多初用者在写动态图时容易把整个样本期里的相关矩阵直接提取出来那就算纯粹的前视。threshold0.3是一个经验值如果你发现图太稀疏可以降到 0.2如果过度平滑则升到 0.5。每次循环生成一个PyG Data 对象后面既可以逐批喂给模型也可以合并成批量图。3. 模型选型从 GCN 到时空图网络的实现路径3.1 GNN 家族选型对比图深度学习用于价格预测最常用的是 GCN、GAT、GraphSAGE、GIN。我们用表格对比模型聚合方式优势价格预测中的典型短板GCN度归一化均值参数少、训练快、强基线无法区分邻居重要性GAT注意力加权可解释性好能输出边权重在大图/动态图上注意力易过平滑GraphSAGE采样聚合适合大规模动态图需要额外调采样参数GIN求和聚合 MLP理论表达能力最强在价格这种连续目标上容易过拟合从工程角度我会先上 GCN 作为基线再换 GAT 来做可解释性分析。如果你做的商品数量在 10-30 个之间整图计算完全没有压力不需要上 GraphSAGE 的采样。这里的关键是不要盲目堆模型复杂度价格数据噪声极大复杂模型常常把噪声也学进去。GIN 的表达能力虽然理论更强但在日频价格数据上并没有稳定优势反而需要更多的正则化。如果你的任务里只有 5-8 个商品那么 GCN 和 GAT 的差距通常很小当节点超过 20 后GAT 的注意力机制才值得。3.2 将时间序列喂进 GNN 的三种方案图神经网络本身不感知时间顺序。价格预测需要时间建模常见做法有三种第一种是时间快照法Snapshot每个时间步的图独立过 GNN再把每个节点的嵌入序列送入 LSTM/GRU/Transformer。优点是思路简单缺点是无法建模边随时间的变化。第二种是动态图法Temporal GNN边的时间权重直接作为 GNN 的边权节点特征包含动量、波动等历史统计量GNN 输出直接是下一期收益。我们前面构造的动态相关图就是这种输入。缺点是存在重复计算滚动相关性的成本。第三种是时空融合法如 STGCN 思路图卷积和时序卷积交替堆叠能同时捕捉空间和时间局部性。但实现复杂需要更多调参。对于大宗商品日度数据我认为方案 2 的性价比最高。原因是商品价格传导往往在 3-5 日内完成收益率的滚动统计量已经包含了大多数时间信息不需要复杂的时序编码器。不过如果换到 tick 级或日内分钟级数据则方案 3 更合适。在你的实验里方案 1 可以用作消融对照组把动态图换成静态图再比较验证集 IC能直观看出“动态边权”的增量到底有多大。另外无论选哪个方案都要警惕“时间建模被 GNN 的空间聚合淹没”的现象GNN 层数增多后节点表示会趋于同质化最终时间编码器拿到的输入几乎一样。所以 GNN 层数一般控制在 2-3 层。3.3 一个基于 PyTorch Geometric 的可运行模型骨架3.3.1 图卷积层定义这里给出一个简单但有效的模型GCN GRU 融合。下面是模型的核心代码import torch import torch.nn as nn from torch_geometric.nn import GCNConv class GCNGRU(nn.Module): def __init__(self, in_features, hidden_size64, num_layers2, num_steps5, dropout0.2): super().__init__() self.num_steps num_steps self.gcn nn.ModuleList() for i in range(num_layers): in_ch in_features if i 0 else hidden_size self.gcn.append(GCNConv(in_ch, hidden_size)) self.gru nn.GRU(hidden_size, hidden_size, batch_firstTrue) self.head nn.Linear(hidden_size, 1) self.dropout nn.Dropout(dropout) def forward(self, x_list, edge_index_list, edge_attr_list): # x_list: num_steps 个节点的张量列表每个 (N, F) emb_list [] for t in range(self.num_steps): x x_list[t] ei edge_index_list[t] ew edge_attr_list[t] for conv in self.gcn: x torch.relu(conv(x, ei, ew)) x self.dropout(x) emb_list.append(x) # 每个 (N, H) # 时间维堆叠: (N, steps, H) seq torch.stack(emb_list, dim1) out, _ self.gru(seq) # 取最后一步 last out[:, -1, :] return self.head(last)代码的逻辑是先对每个时间步独立做两层图卷积这样每个节点就聚合了图上邻居和二阶邻居的商品信息随后把同节点的多步嵌入组成序列送入 GRU 捕捉时序最后用线性层预测下一期收益率。参数上num_steps一般取 5 或 10对应一周或两周的交易日窗口hidden_size64 在商品数量不超过 30 时足够。注意edge_attr如果不是 NoneGCNConv会将其作为消息权重在我们的构图方式下就等于把滚动相关性直接作用到消息传递上。3.3.2 时序编码器上面已经用 GRU 作为时序编码器。若想进一步引入 Transformer 作为时序编码器只需把 GRU 换成 TransformerEncoder 的自回归输入。但根据我的经验日频价格数据样本量通常只有几千个交易日Transformer 的自注意力容易过拟合除非加入时序位置编码和足够强的 dropout。因此在实际落地时我一般会先保留 GRU 或一维 CNN。如果你面对的品种流动性很高、数据量达到分钟级那 Transformer 的优势才能发挥出来。还有一种混合方案用 TEMPORAL GNN 的思路在 GNN 内部并行引入一个门控时序单元让每个节点在聚合邻居之前先对自己的历史做一次门控这种设计在能化品种上的表现比 GCN-GRU 更稳定但实现成本更高。4. 训练、评估与回测配置4.1 数据切分防止图泄漏和时序泄漏时间序列预测中最常见的错误是随机切分训练集和测试集。即使按时间切分如果构图时用了整段数据的统计值比如用全样本均值做归一化预测结果仍然乐观得离谱。对于动态相关图还需要额外注意某一时刻 t 的边权重如果用了 t 之后的收益率数据计算周相关性那就等于把未来信息漏给了模型。正确的做法是构建一个滚动切分器保证特征、边和图三者都只使用截止到 t 的数据。下面给出一个 Walk-forward 切分的伪代码实现def walk_forward_split(dates, train_days1250, val_days250, step50): splits [] i 0 while i train_days val_days len(dates): train_end i train_days val_end train_end val_days splits.append({ train: dates[i:train_end], valid: dates[train_end:val_end], test: dates[val_end:val_endstep] }) i step return splits这里train_days建议至少 1250即约 5 年交易日val_days用 250 天做早停step50每次前滚 50 天。注意每个 split 里训练数据的图要重算一次不能复用之前 split 的结果因为边权是基于滚动窗口的窗口内容变了。频繁重建图会带来计算开销但对 20 个节点以内的图这种开销在可接受范围内。如果商品数量到了 100就要考虑用增量式相关矩阵或直接切换到 DGL 的采样邻居避免每步全量重建。4.2 归一化与特征滞后GNN 对特征尺度非常敏感。商品价格从每吨 3000 元到 30 万美元不等如果不做标准化GCN 的度归一化会掩盖商品自身量纲的影响GAT 的注意力也会被大数值特征主导。常见做法是每个品种各自做 z-score 归一化均值和标准差只用训练段的数据计算并保存下来供验证和测试段使用。具体到代码实现通常是在预处理阶段固化标量而不是在训练循环里动态计算。很多人在这里偷懒直接用全样本均值结果就是回测超额收益高到离谱实盘一塌糊涂。特征滞后方面需要特别注意滚动均值、波动率这些统计量已经引入了滞后因此模型的预测目标通常是“下一期收益率”而不是“未来 N 天价格”。如果你想预测 5 日后的价格建议把目标变成“未来 5 日累计收益”同时把特征窗口也平移 5 天而不是直接把 5 日后的绝对价格作为回归目标。这样做还有一个好处避免模型依赖连续价格里的单位根性质减少伪回归。你可以计算验证集上的预测残差与滞后 1 期残差的自相关如果自相关很高说明模型还没吃透时间结构这时可以增加num_steps或加入一个差异项。4.3 训练循环和评估指标4.3.1 自定义损失和评估代码价格预测方向一般用 MSE 或 Huber Loss。MSE 对异常跳空行情敏感而大宗商品动不动就出现单日 5% 以上的跳空Huber Loss 更稳健。这里给出训练循环的关键部分from sklearn.metrics import mean_absolute_error import torch.nn.functional as F def train_one_epoch(model, loader, optimizer): model.train() total_loss 0 for batch in loader: x_list, edge_list, w_list, target batch optimizer.zero_grad() pred model(x_list, edge_list, w_list) loss F.huber_loss(pred, target, delta1.0) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() return total_loss / max(len(loader), 1)这个循环里做两件容易忽略的事一是delta1.0的 Huber 损失在收益率为 1% 量级的数据上delta 设为 1 通常对应 1% 误差以内用平方误差、以上用线性误差二是梯度裁剪max_norm1.0防止个别交易日异常值把参数推飞。注意PyG 的批处理要把多个时间快照拼成一个 batch实际工程中常用Batch.from_data_list但这里为了可读性我用列表传参。评估指标方面除了 MSE、MAE还要看方向准确率Directional Accuracy。价格预测的盈利根本不在于预测值精确到第三位小数而在于涨跌方向是否判对。方向准确率的实现def directional_accuracy(y_true, y_pred): true_dir (y_true 0).astype(int) pred_dir (y_pred 0).astype(int) return (true_dir pred_dir).mean()4.3.2 参数表学习率、dropout、早停参数推荐值说明学习率1e-3Adam预训练可以 3e-3验证集损失停滞时衰减到 1e-4hidden_size64商品节点超过 30 时可以用 128num_steps5一周交易日窗口5 日滚动收益的主要周期dropout0.2对日频数据0.3 以上会明显降低泛化早停 patience15在验证集上连续 15 轮没有下降就停止批次大小32时间快照数量不是节点数量这些参数来自多个项目的共同经验。hidden_size不建议一开始就上 256因为节点数就这么多隐藏单元再大也只能让模型记住训练集里的异常跳空。dropout要放在每层图卷积之后不要只在最后的全连接层放。如果你用 GAT还需要为注意力层单独设置dropout否则注意力权重会集中于少数几个边上。5. 用图注意力权重做价格驱动因子归因的进阶技巧5.1 从 GAT 的 attention 系数提取边重要性当模型训练完成后GAT 的注意力系数可以把“黑盒预测”转成一张可读的产业链热力图。PyG 的GATConv在 forward 时只要设置return_attention_weightsTrue就能拿到(边索引, 注意力权重)权重形状是(边数, 头数)。我们对多头取平均得到每条边在当前时刻的重要性。代码如下class GATPricePredictor(nn.Module): def __init__(self, in_features, hidden64, heads4): super().__init__() self.gat GATConv(in_features, hidden, headsheads) self.reg nn.Linear(hidden * heads, 1) def forward(self, x, edge_index): x, (attn_edge_index, attn_weight) self.gat( x, edge_index, return_attention_weightsTrue) x torch.relu(x) return self.reg(x), attn_edge_index, attn_weight拿到attn_weight后可以按商品维度做边的重要性聚合比如统计“所有下游商品对原油的注意力均值”或者把某个特定边如铜精矿对精炼铜的注意力单独抽出来与库存、加工费的时间序列对齐。这个技巧最大的价值是它把模型对“谁在驱动谁”的判断变成了可追踪的时序信号。5.2 稳定性检验与落地注意事项不要直接用单日注意力做交易信号因为它对噪声的敏感性不亚于价格本身。一个更稳的做法是计算注意力权重的 20 日滚动排名然后看排名序列的稳定性。具体可以用 Spearman 相关性衡量两个相邻窗口的边重要性排名只有稳定超过 0.6 的边才值得进入信号组合。另一个有效做法是把注意力权重与同期库存周环比、近月价差做回归看注意力是否在统计上解释了价差变化。如果注意力与基本面指标的解释方向不一致优先检查构图是不是用了未来信息、边权有没有做对称化、是否把相关性误当成了因果。这里有一个真实可复用的技巧用 GAT 训练好模型后固定住它的参数然后把验证集里的每一个时间步都跑一遍得到一条“边重要性时间序列”。再对这条序列做一次滑窗比较当某条边的注意力权重连续 3 天净上升同时模型的预测误差也开始收敛说明这条产业链的传导关系正在被市场定价。把它作为事件驱动策略的过滤器比单独使用注意力阈值更稳健。如果你需要把预测能力晋升为决策能力还可以把图卷积输出的节点嵌入作为状态表征接一层策略网络使用图强化学习与深度强化学习的框架做仓位控制。那是一个更大的工程但对于已经跑通图价格预测的团队来说下一步的瓶颈往往不在于模型而在于如何把边注意力转化为可执行的风险约束。本文还有配套的精品资源点击获取