halo 的技术博客

返回

你想从一段价格序列预测下一步,或者从过去 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 比线性强」。后文五类陷阱把它讲透。

长程周期:普通 CNN 核宽锁死看不全一个周期,膨胀卷积逐层吃到整周期


1. 因果膨胀卷积:把「不偷看未来」和「看得够远」同时塞进卷积#

普通卷积做序列预测有两个毛病:

  1. 不因果:输出位置 p 会用到输入 p 之后的点 → 未来泄漏,回测必假。
  2. 感受野短:输出 p 只看到就近 k 个点,N 层也只有 N·(k−1)+1,线性增长,要看到 31 步前的周期得堆十几层。

膨胀卷积用两招解决:

  • 膨胀(dilation):卷积核在输入上每隔 d 个点采一个样。第 i 层膨胀 d_i,L 层堆叠后感受野 = 1 + (k−1)·Σd_id 逐层翻倍时感受野指数级扩张
  • 因果填充(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)=63
python

2. 从零实现:因果膨胀卷积(纯 numpy)#

注意 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(), acts
python

3. 数据合成:多周期正弦叠加任务#

目标 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(), y
python

构造窗口 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 比它好得多——别被标题的「膨胀」骗了。

四模型测试集指标:MSE / R² / RMSE 对比


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

测试段逐点预测:膨胀 CNN(红) vs OLS(蓝) vs 真实(黑)

逐点看,膨胀 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 怎么选#

结构复杂度长程记忆可并行因果性
普通 CNNO(N·k)线性(核宽锁死)左填充因果
膨胀 CNNO(N·k)指数(d 翻倍)左填充因果
TCN(残差膨胀)O(N·k)指数左填充因果
RNN/LSTMO(N)有但梯度消失否(串行)天然因果
TransformerO(N²)完美(全看)需因果掩码
  • 序列中等、要并行 + 长感受野、结构偏局部周期:膨胀 CNN / TCN 顺手。
  • 要省事、任务周期已知且线性可加:直接 OLS + sin/cos 频率特征,别上网络(本例 OLS 就是上界)。
  • 序列极长、要线性复杂度:Mamba/SSM。
  • 要全局依赖、数据够:Transformer(配因果掩码)。

一句话:膨胀 CNN 是「把 CNN 的感受野从线性拉成指数级、且保持因果」的序列模型,代价是感受野有限、容量靠堆层、且赢不了已知结构的线性解。


8. 落地路径#

  1. 多周期特征 backbone:用膨胀 CNN 处理分钟/tick 级局部量价形态,吐特征给下游信号模型;不同膨胀层天然捕获不同周期(2 步/4 步/…/32 步)。
  2. 周期因子:把各膨胀层输出当「多周期卷积因子」直接产出,比手工算 MACD/DMI 更自适应。
  3. 因果自回归预测:推理时维护滑动窗口,每步一次前向,比 RNN 快(并行)、比 Transformer 省显存。
  4. 真要做端到端:上 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 污染、通道容量、因果填充代价、伪周期陷阱,是四个必须正视的约束。

周期不用每次重看,用膨胀卷积把它一次性卷进来就好——但别指望它比「已知周期的线性模型」更懂周期。

因果卷积金融预测:用膨胀卷积抓长程周期
https://blog.halo26812.eu.org/blog/causal-conv-financial
Author halo
Published at 2026年7月23日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨