halo 的技术博客

返回

你想从一段价格序列预测下一步,或者从过去 48 天的量价预测未来收益。RNN/LSTM 是经典选项:状态 h_t 沿时间递推,能记历史。但它有两个老毛病——梯度消失记不住太长、且必须严格串行、没法并行训练,慢。Transformer 一把梭:注意力能看全序列,但矩阵是 O(N²),序列一长显存就爆。

结论先放这:时序卷积网络(TCN, Bai et al. 2018)走中间路线——它用「因果膨胀卷积」让卷积网络也能处理序列:感受野随层数指数级扩张(用线性层数换到极长历史),训练时全并行(像 CNN 一样一次性算完),且通过左填充保证输出位置 p 只看输入 ≤ p,严格不泄漏未来。

在我们的多尺度滞后合成序列(目标依赖 lag ∈ {1,2,4,8,16,32} 处的 tanh 项)上:TCN 测试集 R²=0.669,相对普通 CNN(0.436) 提升 53.4%、相对朴素基线(−0.009) 碾压;但诚实地说,它对标 OLS 线性(0.957) 仍明显落后——因为任务本身是可加线性的,OLS 在已知 lag 上几乎是最优解。 TCN 真正证明的是「膨胀卷积 vs 普通卷积」的差距:普通 CNN 感受野被核宽锁死、几乎学不到长滞后,R² 只有 0.436,TCN 把它拉到 0.669。膨胀解决的是「感受野够不够长」,不是「模型比线性万能」。后文五类陷阱把它讲透。

TCN 感受野:膨胀卷积指数级扩张,普通 CNN 线性,RNN 常数级


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

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

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

TCN 用两招解决:

  • 因果填充(causal padding):在序列左边填 (k−1)·d 个零,保证卷积后位置 p 只依赖输入 ≤ p。d 是膨胀因子。
  • 膨胀(dilation):卷积核在输入上每隔 d 个点采一个样。第 i 层膨胀 d_i,L 层堆叠后感受野 = 1 + (k−1)·Σd_id 逐层翻倍 (1,2,4,8,…) 时,感受野指数级扩张——6 层 k=2 就能覆盖 1+1·(1+2+4+8+16+32)=64 步。

TCN vs 普通CNN vs OLS:测试集 R² 与 MSE 对比


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

注意 valid = pos >= 0:左填充的零让输出 p 永远看不到未来,这是 TCN 的因果性来源。反向传播我们用有限差分逐参数校验过(脚本内置 CHECK_TCN_GRAD=1,最大相对误差 < 1e-3),保证梯度正确——下面预测对比图的曲线是真的从头训出来的。


3. 残差 + 末位读出:让深层也能训#

TCN 原文用残差块(每个块 = 膨胀卷积 + 权重归一 + ReLU + dropout + 1×1 跳跃)。我们简化但保留「残差」精神:每层输入输出通道相同,加跳跃连接避免深层梯度消失。最后一层用一个 1×1 卷积把通道压成 1,读成预测值。

def tcn_forward(X, P):
    """X:(B,1,L) -> (B,) 预测。P 为逐层参数 dict。"""
    h = X
    for i, d in enumerate(DILS):
        z = causal_dconv(h, P[f"W{i}"], P[f"b{i}"], d)
        z = np.maximum(0, z)                 # ReLU
        h = z + h if h.shape == z.shape else z   # 残差/下采样
    # 末位读出:对最后时间步取平均池化后线性映射
    out = h[:, :, -1].mean(axis=-1)          # (B,)
    return out[None, :] if out.ndim == 1 else out
python

4. 数据合成:多尺度滞后 tanh 任务#

目标依赖 x 在 lag ∈ {1,2,4,8,16,32} 处的 tanh 非线性项,专门测「多尺度长程滞后」——普通 CNN 核宽 2、感受野只有就近几步,根本学不到 32 步前的项;TCN 膨胀到 32,能覆盖全部。

def make_data(N=6000, lags=(1, 2, 4, 8, 16, 32)):
    x = rng.standard_normal(N)
    y = np.zeros(N)
    for lag in lags:
        y[lag:] += np.tanh(0.7 * x[:-lag])   # 多尺度滞后,可加
    y += 0.05 * rng.standard_normal(N)
    return x, (y - y.mean()) / y.std()
python

构造窗口 X_t = x[t−48:t],预测 y_t。训练 4500、测试 1500。


5. 实测:膨胀救了「感受野」,但救不了「比线性强」#

朴素基线        MSE=0.9578  R²=-0.009
OLS 线性        MSE=0.0409  R²=0.957
普通CNN(非膨胀) MSE=0.5353  R²=0.436
TCN(膨胀因果)    MSE=0.3137  R²=0.669
plaintext

三个诚实结论:

  • 膨胀卷积 vs 普通卷积:TCN(0.669) 把普通 CNN(0.436) 的 R² 拉高 53.4%。 普通 CNN 核宽 2、膨胀全 1,感受野锁死在就近几步,32 步滞后学不到;TCN 膨胀到 32,把所有滞后都纳入感受野。
  • TCN 碾压朴素基线: 朴素(预测 0)R²=−0.009,说明任务确有可学结构,TCN 抓到了。
  • 但 TCN 仍输给 OLS(0.957)。 根因干净:这个任务的 y 是各 lag 上 tanh(0.7·x)线性相加,OLS 用精确 lag 特征做线性回归,几乎吃满 R²=0.957(tanh 近似线性 + 噪声项)。TCN 是带非线性的有限容量网络,在「可加线性」任务上赢不了最优线性解。

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


6. 五个真实陷阱(必须说清)#

陷阱 1:膨胀解决「感受野」,不解决「比线性强」。 这是最易被营销话术骗的一点。「TCN 超越 RNN/Transformer」的论文结论,是在特定序列任务(如多变量长程依赖、需要局部卷积归纳偏置)上成立的。但若任务本身可由线性模型近优表达(如本例的可加滞后),任何有限容量非线性网络都赢不了最优线性解——这是「数据处理的不等式」。TCN 的卖点是「并行 + 长感受野 + 因果」,不是「非线性碾压线性」。

陷阱 2:感受野越大越好?错,会吃噪声 + 过拟合。 膨胀翻倍堆到 64 步听起来很美,但感受野越大,每层参数和序列长度相关的内存越大;且过长感受野会把无关历史卷进来,低频任务上反而引入噪声。实践里感受野应略大于任务相关滞后(如本例 32 步,给到 64 足够),不是越大越好。

陷阱 3:因果填充不是零成本。 左填 (k−1)·d 个零,序列开头 d 段输出依赖的是「假的零」——它们不是真实历史。长膨胀下,开头几十步预测不可靠(warmup 污染)。回测必须从 warmup 之后算指标,否则 Sharpe 被虚假开头污染(和所有序列模型同理)。

陷阱 4:通道数与容量陷阱。 TCN 容量随通道数 C_CH 线性涨,但金融弱信号任务上通道太多 → 过拟合 + 训练抖。我们 C_CH=8 已够;真实落地先用小通道 + dropout 起步,再按验证集加。

陷阱 5:纯 numpy 递归卷积慢,生产要用框架卷积。 我们的 causal_dconv 是三重 Python 循环,只为可读性 + 可校验梯度。生产用 PyTorch Conv1d(dilation=d, padding=(k−1)·d) + 因果掩码,GPU 上快几个数量级,且自动微分取代有限差分校验。


7. 它和 RNN / Transformer / Mamba 怎么选#

结构复杂度长程记忆可并行因果性
RNN/LSTMO(N)有但梯度消失否(串行)天然因果
TransformerO(N²)完美(全看)需因果掩码
TCNO(N·k)膨胀到指数级左填充因果
Mamba/SSMO(N)有,连续态递归因果
  • 序列中等、要并行 + 长感受野、结构偏局部:TCN 很顺手(音频、局部形态、tick 级短模式)。
  • 序列极长、要线性复杂度 + 无限记忆:Mamba/SSM。
  • 要全局依赖、数据够:Transformer(配因果掩码做自回归)。
  • 要省事、任务线性可加:直接 OLS / 线性模型,别上网络。

一句话:TCN 是「把 CNN 的并行 + 卷积归纳偏置,改造成因果且长感受野的序列模型」,代价是感受野有限、容量靠堆层。


8. 落地路径#

  1. 特征提取 backbone:用 TCN 处理分钟/tick 级局部量价模式,吐特征给下游信号模型。
  2. 多尺度信号:不同膨胀层天然捕获不同周期(2 步/4 步/…/32 步),可当「多周期卷积因子」直接产出。
  3. 因果自回归预测:推理时维护滑动窗口,每步一次前向,比 RNN 快(并行)、比 Transformer 省显存。
  4. 真要做端到端:上 PyTorch Conv1d + 因果填充 + 残差,别用 numpy 递归。

9. 结论#

时序卷积网络的精髓一句话:用「因果膨胀卷积」把卷积网络改成序列模型——左填充保证不偷看未来,膨胀因子翻倍让感受野指数级扩张,训练全程并行。

我们纯 numpy 从零实现证明:在多尺度滞后 tanh 任务上,TCN 测试集 R²=0.669,相对普通 CNN(0.436) 提升 53.4%,证明「膨胀」确实救了被核宽锁死的感受野。但它仍输给 OLS 线性(0.957)——因为这个任务可加线性、OLS 在已知 lag 上近最优。TCN 的威力来自「并行 + 长感受野 + 因果」,不是「非线性碾压线性」;膨胀解决感受野,不解决比线性强(陷阱 1)。warmup 污染、通道容量、因果填充代价、生产换框架,是四个必须正视的约束。

历史不用每次重看,用膨胀卷积把它一次性卷进来就好——但别指望它比线性更懂线性。

TCN 时序卷积交易:用膨胀因果卷积替代 RNN
https://blog.halo26812.eu.org/blog/temporal-conv-trading
Author halo
Published at 2026年7月23日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨