跳转到内容
新建笔记

卷积网络:尺寸、通道、感受野与计算量

卷积层需要分别计算三件事:输出的空间尺寸、输出通道数、每个输出对应的输入范围。它们不能混为“卷积核大小”。本页按 PyTorch 的二维卷积参数记号推导,最后再计算参数量与计算量。

批量图像输入通常写成 (N,Cin,Hin,Win)(N,C_{\mathrm{in}},H_{\mathrm{in}},W_{\mathrm{in}}),输出为 (N,Cout,Hout,Wout)(N,C_{\mathrm{out}},H_{\mathrm{out}},W_{\mathrm{out}})。NN 是批量大小;CinC_{\mathrm{in}} 是输入通道数;CoutC_{\mathrm{out}} 由卷积层的 out_channels 指定,不要求等于输入通道数。

普通 groups=1 卷积中,每个输出通道都对所有输入通道的局部加权结果求和,再加一个偏置。PyTorch 的 Conv2d 实际使用互相关约定,即不先把核在空间方向翻转。PyTorch Conv2d

设分组数为 gg,则 CinC_{\mathrm{in}}、CoutC_{\mathrm{out}} 都应能被 gg 整除。Conv2d 权重张量形状为:

(Cout,Cing,Kh,Kw).\left(C_{\mathrm{out}},\frac{C_{\mathrm{in}}}{g},K_h,K_w\right).

因此“核的输入通道数等于输入通道数”只适用于不分组的情形。分组卷积每个输出通道只连接本组的 Cin/gC_{\mathrm{in}}/g 个输入通道;深度卷积是 g=Cing=C_{\mathrm{in}} 且输出通道为输入通道整数倍的情况。

2. 从一维窗口推导普通卷积尺寸

跳转到“2. 从一维窗口推导普通卷积尺寸”

高度和宽度分别计算。对其中任意一个方向,使用以下符号:

符号含义约束或常见默认值
LinL_{\mathrm{in}}输入长度正整数
KK核在该方向的采样点数正整数
pp该方向每一侧的对称填充量padding 默认 0
ss相邻窗口起点的间隔stride 默认 1
dd核内相邻采样点的间隔dilation 默认 1

没有空洞时 d=1d=1,不是 d=0d=0。核的采样位置为 0,d,2d,…,(K−1)d0,d,2d,\ldots,(K-1)d,因此它覆盖的有效长度是:

Keff=d(K−1)+1.K_{\mathrm{eff}}=d(K-1)+1.

例如 K=3,d=2K=3,d=2 时采样位置为 0,2,40,2,4,有效长度为 5,但可学习的核权重仍只有 3 个。

填充后长度为 Lin+2pL_{\mathrm{in}}+2p。若第 tt 个窗口起点为 tsts,其末端不得越过填充后区域,所以:

ts+Keff≤Lin+2p.ts+K_{\mathrm{eff}}\le L_{\mathrm{in}}+2p.

从 t=0t=0 开始数整数窗口,即得:

Lout=⌊Lin+2p−Keffs⌋+1=⌊Lin+2p−d(K−1)−1s⌋+1.L_{\mathrm{out}} =\left\lfloor\frac{L_{\mathrm{in}}+2p-K_{\mathrm{eff}}}{s}\right\rfloor+1 =\left\lfloor\frac{L_{\mathrm{in}}+2p-d(K-1)-1}{s}\right\rfloor+1.

这里假设至少能放下一个窗口,否则参数组合无效。分母是实际步长 ss,不能固定写成 2;取整必须保留。二维情形分别代入 (Hin,Kh,ph,sh,dh)(H_{\mathrm{in}},K_h,p_h,s_h,d_h) 和 (Win,Kw,pw,sw,dw)(W_{\mathrm{in}},K_w,p_w,s_w,d_w)。不对称填充时将 2p2p 换为该方向两侧填充之和。官方输出形状定义

输入长度KKppssdd有效核长输出长度
323121316
313121316
303021314
323131311
323212532

保持尺寸与整数倍下采样

跳转到“保持尺寸与整数倍下采样”

当 s=1s=1 且使用对称填充时,保持长度的条件是 2p=d(K−1)2p=d(K-1)。常见的 K=3,d=1,p=1K=3,d=1,p=1 只是其中一例。

若仍满足 2p=Keff−12p=K_{\mathrm{eff}}-1,但步长改为 s>1s>1,则:

Lout=⌊Lin−1s⌋+1=⌈Lins⌉.L_{\mathrm{out}}=\left\lfloor\frac{L_{\mathrm{in}}-1}{s}\right\rfloor+1 =\left\lceil\frac{L_{\mathrm{in}}}{s}\right\rceil.

只有输入长度能被步长整除时,输出才恰好是 Lin/sL_{\mathrm{in}}/s;不需要把步长限定为偶数。若对称整数 padding 无法满足条件,需考虑不对称填充。PyTorch padding='same' 的 Conv2d 目前只支持步长 1,应区分 API 选项和手工尺寸推导。

3. 转置卷积与 output_padding

跳转到“3. 转置卷积与 output_padding”

转置卷积常用于可学习上采样。它对应卷积线性映射的转置关系,不是一般意义上能恢复原图的逆运算;输出尺寸恢复也不代表数值信息恢复。

在一个方向上,设 output_padding 为 oo,输出长度为:

Lout=(Lin−1)s−2p+d(K−1)+o+1.L_{\mathrm{out}}=(L_{\mathrm{in}}-1)s-2p+d(K-1)+o+1.

其中默认 d=1,o=0d=1,o=0。只有在 p=0,d=1,o=0p=0,d=1,o=0 时,才化简为 s(Lin−1)+Ks(L_{\mathrm{in}}-1)+K。PyTorch ConvTranspose2d

可以从正向卷积的取整看出 oo 的作用。设正向输入长度为 LL、输出长度为 MM,余数为 rr:

L+2p−Keff=(M−1)s+r,0≤r<s.L+2p-K_{\mathrm{eff}}=(M-1)s+r,\qquad 0\le r<s.

反向选择原长度时便有:

L=(M−1)s−2p+Keff+r.L=(M-1)s-2p+K_{\mathrm{eff}}+r.

由正向步长造成的多种可能尺寸,可用这里的 o=ro=r 来区分。比如 K=3,s=2,p=1,d=1K=3,s=2,p=1,d=1 时,正向输入 27 和 28 都得到 14;转置输入 14 时,o=0o=0 得到 27,o=1o=1 得到 28。

output_padding 用于消除尺寸歧义,不是在已经计算出的输出后面简单补一圈零。它不能任意取值;上面推导针对 0≤o<s0\le o<s 的尺寸选择。官方 output_padding 说明

4. 多层感受野与累积步长

跳转到“4. 多层感受野与累积步长”

感受野描述一个输出单元可能依赖的原始输入范围。令 FlF_l 是第 ll 层的理论感受野边长,JlJ_l 是该层相邻单元在原始输入上的间隔;初始值为 F0=1,J0=1F_0=1,J_0=1。

前一层相邻单元已经相隔 Jl−1J_{l-1} 个输入位置。当前核在前一层覆盖 dl(Kl−1)d_l(K_l-1) 个这样的间隔,所以:

Fl=Fl−1+dl(Kl−1)Jl−1,Jl=slJl−1.\begin{aligned} F_l&=F_{l-1}+d_l(K_l-1)J_{l-1},\\ J_l&=s_lJ_{l-1}. \end{aligned}

应先使用上一层的 Jl−1J_{l-1} 扩展感受野,再更新累积步长。原笔记的三层例子为:

层KlK_lsls_lplp_ldld_lFlF_l 的计算JlJ_l
输入————11
第 1 层31011+2×1=31+2\times1=31
第 2 层32013+2×1=53+2\times1=52
第 3 层31015+2×2=95+2\times2=92

如果只把第 3 层 dilation 改为 2,感受野变成 5+2×2×2=135+2\times2\times2=13,累积步长仍为 2。这个例子保留了原稿的 3,5,93,5,9 推导,并补齐空洞卷积情形。

以上递推针对串联的卷积/局部池化路径。padding 改变边界与中心位置,但不直接改变此处理论跨度递推;边界范围可能包含填充值。空洞可能导致范围内某些位置没有连接,理论感受野跨度也不同于训练后各像素实际影响强弱的“有效感受野”。有分支、跳连或全局注意力时,应分析各路径依赖再合并,不能直接套单路径表。

设 b=1b=1 表示使用偏置,b=0b=0 表示无偏置。分组 Conv2d 参数量为:

P=CoutCingKhKw+bCout.P=C_{\mathrm{out}}\frac{C_{\mathrm{in}}}{g}K_hK_w+bC_{\mathrm{out}}.

stride、padding、dilation 不直接增加核权重个数;但它们可能改变输出尺寸,因此影响计算量。参数量不随 batch 大小或输入图像面积成倍增加。

令每个输出单元参与点积的项数为 Q=(Cin/g)KhKwQ=(C_{\mathrm{in}}/g)K_hK_w,单张图像输出单元数为 T=HoutWoutCoutT=H_{\mathrm{out}}W_{\mathrm{out}}C_{\mathrm{out}}。按直接点积计数:

Nmul=TQ,Nadd=T(Q−1)+bT,NMAC=TQ.\begin{aligned} N_{\mathrm{mul}}&=TQ,\\ N_{\mathrm{add}}&=T(Q-1)+bT,\\ N_{\mathrm{MAC}}&=TQ. \end{aligned}

这些是普通卷积的理论运算计数,包含对 padding 位置执行的常规乘加,不计激活、归一化和内存访问;实际算法可能不同。MAC 常按一次乘累加记 1,FLOPs 常把乘和加各记 1,故常用 2NMAC2N_{\mathrm{MAC}} 估计。报告性能时应注明口径,不能混用 MACs 和 FLOPs;批量 NN 张再乘 NN。

例如输入 1×3×32×321\times3\times32\times32,Conv2d 输出通道 16,Kh=Kw=3,s=2,p=1,d=1,g=1K_h=K_w=3,s=2,p=1,d=1,g=1 且有偏置:输出为 1×16×16×161\times16\times16\times16,参数量为 16×3×3×3+16=44816\times3\times3\times3+16=448,单张图像 MACs 为 16×16×16×27=11059216\times16\times16\times27=110592。

下面仅需 Python 标准库。// 在这些整数表达式中实现向下取整;窗口枚举从另一个角度核对尺寸公式。

def conv_size(length, kernel, stride=1, padding=0, dilation=1):
if min(length, kernel, stride, dilation) < 1 or padding < 0:
raise ValueError("invalid convolution parameters")
effective = dilation * (kernel - 1) + 1
if length + 2 * padding < effective:
raise ValueError("kernel does not fit")
return (length + 2 * padding - effective) // stride + 1
def enumerate_windows(length, kernel, stride=1, padding=0, dilation=1):
positions = range(0, length + 2 * padding, stride)
return sum(start + dilation * (kernel - 1) < length + 2 * padding
for start in positions)
cases = [
(32, 3, 2, 1, 1, 16),
(31, 3, 2, 1, 1, 16),
(30, 3, 2, 0, 1, 14),
(32, 3, 3, 1, 1, 11),
(32, 3, 1, 2, 2, 32),
]
for length, kernel, stride, padding, dilation, expected in cases:
arguments = (length, kernel, stride, padding, dilation)
assert conv_size(*arguments) == expected
assert enumerate_windows(*arguments) == expected
assert (14 - 1) * 2 - 2 + (3 - 1) + 0 + 1 == 27
assert (14 - 1) * 2 - 2 + (3 - 1) + 1 + 1 == 28
field, jump = 1, 1
for kernel, stride, dilation in [(3, 1, 1), (3, 2, 1), (3, 1, 1)]:
field += dilation * (kernel - 1) * jump
jump *= stride
assert (field, jump) == (9, 2)
assert 16 * 3 * 3 * 3 + 16 == 448
assert 16 * 16 * 16 * 27 == 110592
print("all examples passed")

在已经安装 PyTorch 的环境中,还可用张量直接复核形状及参数量:

import torch
from torch import nn
with torch.no_grad():
x = torch.zeros(1, 3, 32, 32)
down = nn.Conv2d(3, 16, 3, stride=2, padding=1)
y = down(x)
assert tuple(y.shape) == (1, 16, 16, 16)
assert sum(parameter.numel() for parameter in down.parameters()) == 448
up = nn.ConvTranspose2d(16, 3, 3, stride=2, padding=1,
output_padding=1)
assert tuple(up(y).shape) == tuple(x.shape) # 只验证尺寸,不表示恢复原值
grouped = nn.Conv2d(8, 12, 3, padding=1, groups=4)
assert tuple(grouped.weight.shape) == (12, 2, 3, 3)
assert sum(parameter.numel() for parameter in grouped.parameters()) == 228

本次修订已执行标准库示例与窗口枚举核验;所检查的本机 Python 环境未安装 PyTorch,因此第二段保留为框架复核代码,不宣称已在本机运行。