因果卷积金融预测:用膨胀卷积抓长程周期
普通 CNN 做序列预测有个死穴:卷积核宽锁死感受野,要看到 32 步前的周期相位得堆几十层。膨胀卷积(dilated convolution)用「每隔 d 个点采一个样」把感受野从线性拉成指数级——d 逐层翻倍 (1,2,4,8,16),5 层就能覆盖 1+(3−1)·(1+2+4+8+16)=63 步,整根周期纳进窗口;左填充 (k−1)·d 个零保证严格因果、不偷看未来。本文用纯 numpy 从零实现因果膨胀卷积 + 有限差分梯度校验,在多周期正弦叠加合成序列(周期 5/11/17/23/31 步)上实测:膨胀因果 CNN 测试集 R²=0.997、MSE=0.0029,相对普通 CNN(0.996/0.0041) 略有改进,但讽刺的是它仍输给 OLS 线性(0.999/0.0009)——因为任务本质是已知周期的线性叠加,OLS 在精确频率特征上近最优。文章诚实拆穿「膨胀救一切 / 膨胀 CNN 比线性强 / 感受野越大越好 / 因果填充零成本 / 真周期≠可预测」五类真实误区(中阶)。
你想从一段价格序列预测下一步,或者从过去 64 天的量价预测未来收益。普通 CNN 也能做序列:卷积核在时间里滑。但它有个死穴——卷积核宽锁死了感受野。输出位置 p 只看到 p 附近 k 个点,堆 N 层也只看到 N·(k−1)+1,线性增长。要看到 31 步前的一个周期相位,核宽 3 得堆十几层,参数量和内存爆炸,还梯度衰减。
膨胀卷积(dilated convolution) 是绕开这个死穴的标准解法:卷积核在输入上每隔 d 个点采一个样,输出 p 看的是 p, p−d, p−2d… 而不是相邻的。d 逐层翻倍 (1,2,4,8,…),感受野就指数级扩张——5 层核宽 3 就能覆盖 1+(3−1)·(1+2+4+8+16)=63 步,整根周期纳进一个窗口。再加左填充保证严格因果,就是「因果膨胀卷积」。
在我们的多周期正弦叠加合成序列(周期 ∈ {5,11,17,23,31} 步)上:膨胀因果 CNN 测试集 R²=0.997、MSE=0.0029,相对普通 CNN(0.996/0.0041) 略有改进;但讽刺的是它仍输给 OLS 线性(0.999/0.0009)——因为这个任务本质是「已知频率的线性叠加」,OLS 用精确的周期/相位特征几乎吃满 R²。膨胀 CNN 真正证明的是「膨胀 vs 不膨胀」的感受野差距,不是「CNN 比线性强」。后文五类陷阱把它讲透。

1. 因果膨胀卷积:把「不偷看未来」和「看得够远」同时塞进卷积#
普通卷积做序列预测有两个毛病:
- 不因果:输出位置 p 会用到输入 p 之后的点 → 未来泄漏,回测必假。
- 感受野短:输出 p 只看到就近 k 个点,N 层也只有 N·(k−1)+1,线性增长,要看到 31 步前的周期得堆十几层。
膨胀卷积用两招解决:
- 膨胀(dilation):卷积核在输入上每隔 d 个点采一个样。第 i 层膨胀 d_i,L 层堆叠后感受野 =
1 + (k−1)·Σd_i。d 逐层翻倍时感受野指数级扩张。 - 因果填充(causal padding):在序列左边填 (k−1)·d 个零,保证输出位置 p 只依赖输入 ≤ p,严格不泄漏未来。
import numpy as np
K = 3 # 卷积核宽(>1 才能表达相位)
C_CH = 8
L = 64 # 输入窗口长度
PERIODS = (5, 11, 17, 23, 31) # 多周期,最长 31 步
DILS = [1, 2, 4, 8, 16] # 逐层翻倍膨胀,5 层感受野 = 1 + 2*(1+2+4+8+16)=63python2. 从零实现:因果膨胀卷积(纯 numpy)#
def causal_dconv(x, W, b, d):
"""因果膨胀卷积:out[b,c,l] = b[c] + Σ_k W[c,cin,k]·x[b,cin, l−k·d]
x:(B,cin,Lx) W:(cout,cin,K) b:(cout,) d:膨胀 -> out:(B,cout,Lx)
越界用 0 填充,天然因果(只看 ≤ 当前位置)。"""
B, cin, Lx = x.shape
cout = W.shape[0]
out = np.zeros((B, cout, Lx))
for k in range(K):
idx = np.arange(Lx) - k * d
valid = idx >= 0 # 越界 = 0 = 未来不泄漏
taps = np.zeros_like(x)
taps[:, :, valid] = x[:, :, idx[valid]]
for c in range(cout):
out[:, c, :] += b[c]
out[:, c, :] += np.tensordot(taps, W[c, :, k], axes=([1], [0]))
return outpython注意 valid = idx >= 0:左填充的零让输出 p 永远看不到未来,这是因果性来源。反向传播我们用有限差分逐参数校验过(脚本内置 CHECK_CC_GRAD=1,最大相对误差 < 1e-3),保证梯度正确——下面预测对比图的曲线是真的从头训出来的,不是画图。
网络堆叠 5 层膨胀 + 残差 + 末位读出:
def forward(X, P):
acts = []
h = X
for i, d in enumerate(DILS):
conv = causal_dconv(h, P[f"W{i}"], P[f"b{i}"], d)
res = np.einsum("bcl,cj->bjl", h, P[f"Wr{i}"]) # 1x1 残差
a = np.maximum(0, conv + res) # ReLU
acts.append((h, conv, res, a))
h = a
last = h[:, :, -1]
yhat = last @ P["Wo"].T + P["bo"] # 末位读出
return yhat.ravel(), actspython3. 数据合成:多周期正弦叠加任务#
目标 y_t 是多个周期正弦/余弦的线性叠加,专门测「长程周期性」——普通 CNN 核宽锁死、看不到一个完整长周期,相位错乱;膨胀卷积把整根周期纳进窗口。
def make_data(N=8000, periods=PERIODS, noise=0.05):
t = np.arange(N)
y = np.zeros(N)
for f in periods:
A = rng.normal(0, 1) * 0.8
B = rng.normal(0, 1) * 0.8
y += A * np.sin(2 * np.pi * f * t / 64.0) + B * np.cos(2 * np.pi * f * t / 64.0)
y += noise * rng.standard_normal(N)
y = (y - y.mean()) / (y.std() + 1e-9) # 标准化
return y.copy(), ypython构造窗口 X_t = x[t−64:t],预测 y_t。训练 6400、测试 1600。最长周期 31 步、主周期 64 步,膨胀到 16 的 5 层感受野 63 步刚好覆盖。
4. 实测:膨胀救了感受野,但救不了「比线性强」#
朴素基线 MSE=1.0002 R²=-0.000 RMSE=1.0001
OLS 线性 MSE=0.0009 R²=0.999 RMSE=0.0294
普通CNN(非膨胀) MSE=0.0041 R²=0.996 RMSE=0.0642
膨胀因果CNN MSE=0.0029 R²=0.997 RMSE=0.0540
膨胀CNN 相对 OLS 改进: -237.9%plaintext三个诚实结论:
- 膨胀 CNN vs 普通 CNN:R² 0.997 vs 0.996,MSE 0.0029 vs 0.0041。 膨胀确实把感受野拉长、吃到完整周期,但在这类「周期不算太长」的任务上,普通 CNN 核宽 3 跑 200 epoch 也基本学到了——差距只有一点点。真正要拉满差距,得把周期拉到远超普通 CNN 核宽能及的长度(如 200+ 步)才有碾压级差别。
- 膨胀 CNN 碾压朴素基线:朴素(预测 0)R²=−0.000,说明任务确有强可学结构,CNN 抓到了。
- 但膨胀 CNN 仍输给 OLS(0.999)。 根因干净:这个任务的 y 是已知频率的正弦/余弦线性叠加,OLS 用
sin/cos(2πf·t)精确频率特征做线性回归,几乎吃满 R²=0.999(噪声项才 0.05)。膨胀 CNN 是带非线性的有限容量网络,在「已知周期的线性叠加」任务上赢不了最优线性解。「膨胀 CNN 相对 OLS 改进 −237.9%」这个负数,恰恰说明 OLS 比它好得多——别被标题的「膨胀」骗了。

5. 测试段逐点预测:相位锁得住吗#

逐点看,膨胀 CNN(红)和 OLS(蓝)都几乎贴着真实(黑)。放大相位漂移窗口(图里金色段)会发现:OLS 因为用了精确频率特征,相位锁得最死;膨胀 CNN 在长周期上偶尔有微小相位滞后。这进一步印证——当真实生成过程是「已知频率的线性叠加」,线性模型就是理论上界,CNN 只能逼近、不能超越。
6. 五个真实陷阱(必须说清)#
陷阱 1:膨胀解决「感受野」,不解决「比线性强」。 这是最易被营销话术骗的一点。「膨胀卷积感受野指数级」是事实,但「感受野大 = 预测准」是错觉。若任务本身可由线性模型近优表达(如本例的已知周期叠加),任何有限容量非线性网络都赢不了最优线性解——这是「数据处理的不等式」。膨胀 CNN 的卖点是「并行 + 长感受野 + 因果」,不是「非线性碾压线性」。
陷阱 2:感受野越大越好?错,会吃噪声 + 过拟合 + warmup 污染。 膨胀翻倍堆到 63 步听起来很美,但感受野越大,开头 (k−1)·d 段输出依赖的是「假的零」填充——它们不是真实历史,长膨胀下开头几十步预测不可靠(warmup 污染)。且过长感受野把无关历史卷进来,低频任务上反而引入噪声。实践里感受野应略大于任务相关周期,不是越大越好。
陷阱 3:因果填充不是零成本。 左填 (k−1)·d 个零,序列开头 d 段输出依赖假零。回测必须从 warmup 之后算指标,否则 RMSE / Sharpe 被虚假开头污染(和所有序列模型同理)。本文测试集从窗口滑完之后才开始算,已避开。
陷阱 4:通道数与容量陷阱。 CNN 容量随通道数 C_CH 涨,但金融弱信号任务上通道太多 → 过拟合 + 训练抖。我们 C_CH=8 已够;真实落地先用小通道 + dropout 起步,再按验证集加。
陷阱 5:真周期 ≠ 可预测。 金融价格里「看起来有周期」(如月度效应、季节性)往往是伪周期——幅度时变、相位漂移、还会被regime切换打断。膨胀 CNN 能把「真·稳定周期」学到,但对伪周期会过拟合历史周期、未来失效。别把「模型学会了历史周期」当「模型能预测未来周期」。
7. 它和 TCN / RNN / Transformer 怎么选#
| 结构 | 复杂度 | 长程记忆 | 可并行 | 因果性 |
|---|---|---|---|---|
| 普通 CNN | O(N·k) | 线性(核宽锁死) | 是 | 左填充因果 |
| 膨胀 CNN | O(N·k) | 指数(d 翻倍) | 是 | 左填充因果 |
| TCN(残差膨胀) | O(N·k) | 指数 | 是 | 左填充因果 |
| RNN/LSTM | O(N) | 有但梯度消失 | 否(串行) | 天然因果 |
| Transformer | O(N²) | 完美(全看) | 是 | 需因果掩码 |
- 序列中等、要并行 + 长感受野、结构偏局部周期:膨胀 CNN / TCN 顺手。
- 要省事、任务周期已知且线性可加:直接 OLS +
sin/cos频率特征,别上网络(本例 OLS 就是上界)。 - 序列极长、要线性复杂度:Mamba/SSM。
- 要全局依赖、数据够:Transformer(配因果掩码)。
一句话:膨胀 CNN 是「把 CNN 的感受野从线性拉成指数级、且保持因果」的序列模型,代价是感受野有限、容量靠堆层、且赢不了已知结构的线性解。
8. 落地路径#
- 多周期特征 backbone:用膨胀 CNN 处理分钟/tick 级局部量价形态,吐特征给下游信号模型;不同膨胀层天然捕获不同周期(2 步/4 步/…/32 步)。
- 周期因子:把各膨胀层输出当「多周期卷积因子」直接产出,比手工算 MACD/DMI 更自适应。
- 因果自回归预测:推理时维护滑动窗口,每步一次前向,比 RNN 快(并行)、比 Transformer 省显存。
- 真要做端到端:上 PyTorch
Conv1d(dilation=d, padding=(k−1)·d)+ 因果掩码,GPU 上快几个数量级,自动微分取代有限差分校验。
9. 结论#
因果膨胀卷积的精髓一句话:用「膨胀采样」把卷积核的感受野从线性拉成指数级,用「左填充」保证严格不偷看未来。
我们纯 numpy 从零实现 + 有限差分梯度校验证明:在多周期正弦叠加任务(周期 5/11/17/23/31 步)上,膨胀因果 CNN 测试集 R²=0.997、MSE=0.0029,相对普通 CNN(0.996/0.0041) 略有改进,证明「膨胀」确实拉长了被核宽锁死的感受野。但它仍输给 OLS 线性(0.999/0.0009)——因为任务本质是已知频率的线性叠加,OLS 在精确周期特征上近最优。膨胀 CNN 的威力来自「并行 + 长感受野 + 因果」,不是「非线性碾压线性」;膨胀解决感受野,不解决比线性强(陷阱 1)。warmup 污染、通道容量、因果填充代价、伪周期陷阱,是四个必须正视的约束。
周期不用每次重看,用膨胀卷积把它一次性卷进来就好——但别指望它比「已知周期的线性模型」更懂周期。