第 6 / 30 课。 前置:上一课。本篇的运行范围以实验说明和验证记录为准,构造样例不代表真实任务精度或板端性能。
先把 BatchNorm 写成仿射变换
推理模式使用固定 running_mean、running_var。令 a=gamma/sqrt(var+eps),则 BN(z)=a×z+beta−a×mean。对于 z=Wx+b,融合权重为 aW,偏置为 a(b−mean)+beta,a 按输出通道广播。
训练模式依赖当前批次统计,不能用固定融合权重代替。eval() 后进行融合,并核对统计量是否确实经过训练;未训练的统计量也能证明代数等价,但不能证明任务质量。
多分支为什么可以相加
本课包含 3×3+BN、1×1+BN 和 identity+BN 三支,先相加再激活。把 1×1 核放到 3×3 核中心;把 identity 表示为中心位置的通道单位矩阵;再把各支融合后的核和偏置相加。
这个推导依赖分支之间没有非线性。如果每个分支先做 ReLU,通常不能把它们合成一个线性卷积。各支的 stride、padding、分组和输出尺寸也必须一致。
实验的数值对照
脚本先运行几批数据更新 BN 统计,再切换 eval,融合成一个带 bias 的 3×3 卷积。对多个固定随机输入比较完整输出,使用绝对加相对容差而不是要求浮点逐位一致。
修改任务与验收
故意在某一支相加前加入 ReLU,观察误差;恢复后把输入宽高设为奇数,再验证。交付融合前后结构、参数文件、最大误差和实际耗时。
参数文件可能变大或变小,取决于训练结构;主要收益往往是执行图更简单。是否变快需在目标设备测量,不能仅凭分支减少给出 FPS。
配套实验与运行记录
实验:结构重参数化:训练分支怎样合为一个卷积
查看完整代码:main.py
"""推导 Conv-BN 融合,并验证三分支结构与单卷积的数值一致性。"""
import torch
from torch import nn
import torch.nn.functional as F
def fuse(conv, bn):
scale = bn.weight / torch.sqrt(bn.running_var + bn.eps)
bias = conv.bias if conv.bias is not None else torch.zeros_like(scale)
return (
conv.weight * scale[:, None, None, None],
bn.bias + (bias - bn.running_mean) * scale,
)
def main():
torch.manual_seed(7)
torch.set_num_threads(2)
c = 4
c3 = nn.Conv2d(c, c, 3, padding=1, bias=False)
b3 = nn.BatchNorm2d(c)
c1 = nn.Conv2d(c, c, 1, bias=False)
b1 = nn.BatchNorm2d(c)
bi = nn.BatchNorm2d(c)
for _ in range(5):
x = torch.randn(4, c, 12, 12)
b3(c3(x))
b1(c1(x))
bi(x)
for b in (b3, b1, bi):
b.eval()
identity = nn.Conv2d(c, c, 1, bias=False)
with torch.no_grad():
identity.weight.copy_(torch.eye(c).reshape(c, c, 1, 1))
w3, p3 = fuse(c3, b3)
w1, p1 = fuse(c1, b1)
wi, pi = fuse(identity, bi)
merged = nn.Conv2d(c, c, 3, padding=1)
with torch.no_grad():
merged.weight.copy_(w3 + F.pad(w1, (1, 1, 1, 1)) + F.pad(wi, (1, 1, 1, 1)))
merged.bias.copy_(p3 + p1 + pi)
for size in [12, 17, 23]:
x = torch.randn(2, c, size, size)
expected = torch.relu(b3(c3(x)) + b1(c1(x)) + bi(x))
actual = torch.relu(merged(x))
torch.testing.assert_close(actual, expected, rtol=1e-5, atol=2e-6)
print(size, "max error", float((actual - expected).abs().max().detach()))
if __name__ == "__main__":
main()
在项目根目录执行;下载包解压后,在解压目录执行 python3 main.py。
cd 'content/posts/AI-深度学习/嵌入式视觉模型进阶/06-结构重参数化/code' && python3 main.py
在本篇代码目录执行 python3 main.py。依赖安装、可选入口见包内 README.md;实际运行结果见 verification.md。下载包包含本课使用的辅助模块与样例。
参考与进一步阅读
原始论文或官方文档用于核对结构、接口和适用范围。本篇代码是围绕教学目标编写的最小实验;除明确注明的完整入口外,不视为对应论文模型的完整复现。