Skip to content

第 16 章 从树到卷积:感受野、因果性与可靠迭代 ​

树形模型每次读八个字符,输出下一个字符的分布。假设要对一句文本的相邻八个窗口都预测一遍,最直接的办法是循环调用模型八次。窗口大量重叠,里面的局部组合也会重复计算。课程第 6 讲最后用这个现象引出卷积,但尚未完整实现扩张因果卷积。

本章前半延续课程的“卷积是在多个位置复用同一种局部计算”这一线索;带有“补充”的段落给出本书新增的实际卷积代码、等价性与因果测试。树形教学网络和 2016 年完整 WaveNet 的区别始终保留,不用同一个名字抹掉它们。

16.1 一组 Linear 就能写成一个局部卷积 ​

先看相邻两个位置,每个位置有 C 个通道。拼接后乘权重 W∈R2C×H,可把矩阵按行分成两块 W(0),W(1)∈RC×H:

[xt;xt+1]W=xtW(0)+xt+1W(1).

这里的分号表示拼接,不是求和。对每一组相邻位置重复同一表达式,就是局部共享权重计算。若各组互不重叠,起点为 0、2、4、6,对应核宽 2、stride 2 的一维卷积。stride 是相邻输出在输入中前进几格。

比如一维输入 [1,2,3,4],权重两项为 [10,1],结果是 [1×10+2,3×10+4]=[12,34]。不能把输入分组解释成平均池化,因为输出数值取决于两个有顺序的权重。

补充:实际库接口。 PyTorch 的 conv1d 接收 B×C×T,卷积核为 H×C×K,而本书层接口是 B×T×C。这里 K=2。代码需要转置输入,并把矩阵权重按“核位置、输入通道、输出通道”拆开:

python
# x: B,T,C; W: 2*C,H
by_groups = x.reshape(B, T//2, 2*C) @ W
kernel = W.reshape(2, C, H).permute(2, 1, 0)
by_conv = F.conv1d(x.transpose(1, 2), kernel, stride=2).transpose(1, 2)

配套实验随机生成 B=2,T=8,C=3,H=4 的例子,两个结果最大误差为 8.88×10−16。它验证了分组矩阵乘法与 stride-2 卷积的局部等价性,还没有说明整个相邻窗口预测都完成了。

严格说,深度学习库常用的是互相关形式:核不做数学卷积中常见的翻转。这里只要两边采用同样的权重顺序,等价关系就成立。实际移植时不能一边按“最早到最近”展开,一边按“最近到最早”放核。PyTorch v2.6.0 卷积源码与定义

16.2 因果性由输入依赖决定,不由名称决定 ​

预测位置 t+1 时,模型只能使用到 xt 为止的信息。我们采用一个明确的对齐约定:输出 yt 根据 xt 及更早位置计算,用来预测下一个 token。若你的输出 yt 要预测同编号的 xt,输入还需整体右移一位,不能沿用同一解释。

补充实现。 核宽为 K、扩张率为 d 时,左侧补 d(K−1) 个零,不在右侧补零;然后执行普通 stride-1 卷积。这样输出长度保持不变,时间 t 只读取 t,t−d,…,t−(K−1)d 的输入。

python
def causal_conv(x, w, dilation):
    # x: B,T,Cin; w: K,Cin,Cout,w[-1] 对应最近位置
    K = w.shape[0]
    padded = F.pad(x.transpose(1, 2), (dilation * (K - 1), 0))
    kernel = w.permute(2, 1, 0)
    return F.conv1d(padded, kernel, dilation=dilation).transpose(1, 2)

其中 F.pad 的二元组按最后一维的左、右填充值解释。若用对称 padding,当前位置很可能读到右边的未来输入;输出形状仍然“正确”,所以 shape 测试抓不住泄漏。

配套正例将时间 10 及之后的输入增加 100,检查时间 0–9 的输出完全不变,最大差值为 0。负例改用核宽 3、对称 padding 1 的全一卷积,时间 9 的输出变化了 100,证明未来已经漏进来。因果性测试应直接干预未来输入,不能只看训练 loss 是否合理。

16.3 扩张率如何让感受野长得更快 ​

感受野是一个输出在结构上可能依赖的输入位置范围。普通核宽 2、stride 1 的卷积,每叠一层只向过去多看一个位置。扩张卷积让相邻核采样点隔开 d 格,但不因此减少输出位置。

设所有层 stride 为 1,第 ℓ 层核宽为 Kℓ、扩张率为 dℓ。从一个输入位置开始,感受野范围的宽度为

R=1+∑ℓ=1L(Kℓ−1)dℓ.

若核宽始终为 2,三层扩张率为 1、2、4,就有 R=1+1+2+4=8。第一层联合两个位置;第二层结合相隔 2 的两组;第三层再结合相隔 4 的两组。这里恰好连续覆盖八个位置。对任意扩张率组合,公式给的是最左到最右跨度,内部可能留洞,不能不加检查便声称每一个中间点都被覆盖。

扩张率 1、2、4 的三层依赖树,末端输出连接到连续八个原输入
可切换原始尺寸,滚动查看细节

查看大图

尺寸读取中

适合窗口:完整显示图片,不放大超过原始尺寸。 按 Esc 关闭。

考虑输出时间 9,三层应依赖时间 2–9。本书把三个两项核都设成 1,避免随机抵消,反传单个输出。输入梯度非零的位置恰好为 [2,3,4,5,6,7,8,9]。这个检查证明的是本次结构与非退化权重下的依赖范围;若某权重为零或路径相互抵消,梯度为零不一定代表结构上不可能依赖该位置。

若还有 stride,计算需同时追踪相邻输出对应输入的间隔 J。初始 R0=1,J0=1,每层更新

Rℓ=Rℓ−1+(Kℓ−1)dℓJℓ−1,Jℓ=Jℓ−1sℓ.

三层核宽 2、stride 2、无扩张时,R 依次为 2、4、8,输出间隔最终为 8。三层 stride 1、扩张 1、2、4 同样有八字符感受野,但每个时间位置都有输出。这正是“树的单窗口计算”与“整段密集预测”的重要差别。

16.4 一棵滑动的树,怎样变成扩张卷积堆栈 ​

把八字符树应用到每一个滑动窗口。对以时间 t 结束的窗口,第一层相邻对分别结束于 t−6,t−4,t−2,t。第二层把两对合成一组,末端位于 t−4,t;第三层合成整个窗口。

现在不按窗口重复做,而先在整段序列每个位置计算第一层的相邻对。第二层在位置 t 读取第一层的 t−2 与 t,所以用扩张 2;第三层读取第二层的 t−4 与 t,所以用扩张 4。相同的局部函数被复用,相同位置的中间结果只需算一次。

配套 conv_tests() 用三层随机权重和 tanh,在长度 11 的序列上比较:一边逐个提取四个完整八字符窗口运行树,另一边在整段运行扩张率 1、2、4 的堆栈,再取时间 7–10 的输出。最大误差为 2.22×10−16。

这个等价测试有明确前提:相同权重、相同非线性、相同位置顺序;比较完整窗口避免边界填充差异;测试中不加入训练 BN。若 BN 处于推理模式,可以先折叠为固定仿射变换再比较。训练 BN 的统计集合随窗口组织方式变化,不能期待逐滑窗树与整段卷积仍给完全相同的值。

更重要的是,整段密集自回归训练若在时间轴上计算 BN 统计量,早期输出可能通过统计量间接依赖后面的真实 token。即使每个卷积都只向左看,也不能再直接宣称整个训练函数严格因果。第 15 章一次输入历史窗口、只预测窗口后一个标签,与在整段每个时间位置同时预测,是不同的使用场景。这里的因果测试有意用不跨时间汇总的操作,避免这个混淆。

16.5 与完整 WaveNet 的边界 ​

WaveNet 原论文针对原始音频的自回归分布,使用扩张因果卷积、门控单元、残差与跳跃连接,并讨论条件信息。教学树形模型只保留局部组合逐层扩大的思路,使用字符 embedding、普通 tanh 和 BN;它没有复现音频量化、条件语音生成或论文的主观听音实验。WaveNet 原论文,2016

比较维度本课树形字符模型论文中的关键设计
目标下一个字符的分类分布下一个音频采样值的分布
上下文计算分组拼接与共享 Linear扩张因果卷积
非线性tanhtanh 分支与 sigmoid 门逐元素相乘
深层路径顺序层残差与通往输出的跳跃连接
输出位置每个窗口一个可对整段各时间位置并行训练

理解门控不需要把它想成额外的控制程序。两条同形状卷积结果分别经过 tanh 与 sigmoid,再逐元素相乘;sigmoid 给每个位置一个 0 到 1 的倍率。残差相加要求两侧形状相同,必要时用投影匹配。它们都属于后续可独立学习的结构,本书此处只标清差别,不声称小实验复现完整 WaveNet。

16.6 课程的成绩日志应该怎样读 ​

第 6 讲 notebook 保留了一条开发记录。下面按原来源重列数值,不是本书本机复现,也不是每一步都只改一个变量的严格消融实验。

上游记录的配置变化train lossval loss
3 字符上下文、200 隐藏单元,约 12K 参数2.0582.105
上下文扩为 8,约 22K 参数1.9182.027
扁平改为层级结构,约 22K 参数1.9412.029
修复 BN 归约轴1.9122.022
embedding 24、hidden 128,约 76K 参数1.7691.993

从这组记录至少可以看出:树形结构不会凭名字自动战胜同规模扁平模型;一个“程序可以跑”的 BN 轴错误也可能影响最终结果。至于结构、容量、训练随机性各贡献多少,得通过新的控制实验判断,不能仅凭五行历史日志分配因果。

课程还把每步 loss 分组平均,减少曲线的抖动。如果原记录存的是 log10(loss),先平均再画显示的是平均对数损失,也就是原 loss 几何平均的对数,不等于先平均原 loss 再取对数。比如两步 loss 为 1 和 9,平均 log10 为约 0.4771,而平均 loss 的 log10 为 log10⁡5≈0.6990。两者都能用来观察趋势,但轴标签与处理方式要说清。

原代码 view(-1,1000).mean(1) 还要求长度能被 1000 整除。缩小实验只有 400 步时,应该改变分组大小或处理尾块,而不是复制后让 reshape 报错。每次重构训练器,都要留一个几秒能跑完的烟雾测试。

16.7 把“改进 loss”变成能核对的实验 ​

视频结尾邀请读者继续改进模型,但没有提供一条必胜配置。可以先建立实验表,每行记录一个明确改动及其证据。固定按单词划分的数据、字符映射、训练预算、评估模式和 seed;只在 dev 上选择候选,最后一次用 test 报告选定模型。

先试上下文与隐藏宽度时,要同时记录参数量与计算成本。更长上下文可能补充有用信息,也可能增加学习难度;更大隐藏宽度可能降低训练损失,也可能扩大泛化差距。比较“相同参数量”和“相同训练时长”回答的是不同问题,应明确选择哪一个。

本书的完整执行顺序是:检查分组顺序和参数数目,验证 BN 轴和推理状态,用微型数据确认损失下降,验证卷积等价与未来干预测试,再考虑长训练。换成整段卷积实现后,优先比较同一组冻结参数的输出;只有这项通过,才比较重新训练后的 dev loss。否则性能差异可能只是端点对齐或核顺序错误。

练习与答案 ​

  1. 补充。 核宽 3,扩张率依次为 1、2、4,stride 都是 1,感受野跨度是多少?最左、最右位置怎样确定?
  2. 补充。 把两两拼接的 Linear 改成三位置拼接,如何构造对应 stride-3 的卷积核?
  3. 课程开放挑战。 在完整姓名数据上尝试一个更好的 dev loss,但给出配置、预算、重复 seed 和测试集使用规则;不以生成的几个好看名字代替指标。
  4. 补充。 若只检查输出长度相同,能否证明因果卷积正确?设计一个反例。

第一题 R=1+2(1+2+4)=15,对齐到输出时间 t 时最左为 t−14、最右为 t;这一组合还需按路径确认内部覆盖。第二题将矩阵 W∈R3C×H reshape 为 (3,C,H),再 permute 成 (H,C,3),stride 取 3,并确认时间长度可分组。第三题没有预先给定的最优答案;合格答案必须能重跑且不泄漏 test。第四题的答案就是本章对称 padding 的负例,配套脚本已经实测未来扰动造成 100 的输出变化。

所有补充卷积检查都在 examples/part2/experiments.py:conv_tests(),图由 draw_diagrams.py 生成。至此,我们有一条清楚的升级路线:先保证每个局部操作的梯度与形状正确,再让共享计算覆盖更长上下文,而不是依靠更大的模型掩盖基础错误。

来源与范围 ​

保留各书来源与许可 · 示例运行条件以正文为准