如何合并带不同填充与步幅的二维卷积(PyTorch实现)
注:下文用*代表卷积操作。
如何合并带有不同填充和步幅的连续卷积核
核心结论
完全可以合并带有不同填充、步幅的连续卷积,但不能仅靠单一卷积核实现——需要结合等效的填充/步幅配置,同时调整核的合并逻辑,来匹配两次连续卷积的输出尺寸和数值结果。
关键思路拆解
连续卷积的等效转换需要同时处理三个核心要素:
- 步幅等效:两次卷积的步幅
s1和s2,等效为单次卷积的步幅s = s1 * s2。因为第一次步幅缩小特征图后,第二次步幅是在缩小后的图上采样,相当于原图上间隔s1*s2采样。 - 填充等效:需要计算全局等效填充
p_total,使得单次卷积的输出尺寸和两次卷积的输出尺寸一致。输出尺寸公式为:输出尺寸 = ⌊(输入尺寸 + 2*填充 - 核尺寸)/步幅 + 1⌋
先算出第二次卷积的输入尺寸(即第一次的输出尺寸),再反推单次卷积需要的填充值。 - 核的合并:无步幅时核的合并是
k3 = k1 * k2,但存在步幅时,需要先把k1按照步幅s1的采样规则筛选有效区域,再和k2完成卷积合并。
适配不同填充/步幅的PyTorch实现
以下代码支持合并任意两个2D卷积的核、填充、步幅,返回等效的核、填充、步幅,确保单次卷积的结果和两次连续卷积完全一致:
import torch import torch.nn.functional as F def merge_two_convs(k1, p1, s1, k2, p2, s2): """ 合并两个连续的2D卷积操作 参数: k1: 第一个卷积核,形状 (out1, in1, k1_h, k1_w) p1: 第一个卷积的填充 (pad_h, pad_w) 或 int s1: 第一个卷积的步幅 (stride_h, stride_w) 或 int k2: 第二个卷积核,形状 (out2, in2, k2_h, k2_w) p2: 第二个卷积的填充 (pad_h, pad_w) 或 int s2: 第二个卷积的步幅 (stride_h, stride_w) 或 int 返回: (k3, p3, s3): 等效的卷积核、填充、步幅 """ # 统一参数为tuple格式 p1 = (p1, p1) if isinstance(p1, int) else p1 s1 = (s1, s1) if isinstance(s1, int) else s1 p2 = (p2, p2) if isinstance(p2, int) else p2 s2 = (s2, s2) if isinstance(s2, int) else s2 # 通道合法性检查 assert k1.shape[0] == k2.shape[1], "通道不匹配:k1的输出通道数必须等于k2的输入通道数" # 计算等效步幅 s3 = (s1[0] * s2[0], s1[1] * s2[1]) # 第一步:合并核(处理PyTorch互相关与卷积的差异) k1_permuted = k1.permute(1, 0, 2, 3) k2_flipped = k2.flip(-1, -2) pad_k = (k2.shape[-1]-1, k2.shape[-1]-1, k2.shape[-2]-1, k2.shape[-2]-1) k3_raw = F.conv2d(k1_permuted, k2_flipped, padding=pad_k) k3_raw = k3_raw.permute(1, 0, 2, 3) # 第二步:处理步幅s1的采样偏移,筛选核的有效区域 merged_h, merged_w = k3_raw.shape[-2], k3_raw.shape[-1] if s1[0] > 1 or s1[1] > 1: mask_h = torch.zeros(merged_h, dtype=torch.bool) mask_w = torch.zeros(merged_w, dtype=torch.bool) mask_h[::s1[0]] = True mask_w[::s1[1]] = True k3 = k3_raw[:, :, mask_h, :][:, :, :, mask_w] else: k3 = k3_raw # 第三步:计算等效填充p3,确保输出尺寸一致 def calc_out_size(in_size, pad, kernel_size, stride): return (in_size + 2*pad - kernel_size) // stride + 1 def calc_pad(out_size, in_size, kernel_size, stride): pad = ((out_size -1)*stride + kernel_size - in_size) / 2 assert pad.is_integer(), "无法实现对称填充的等效转换,需改用不对称填充逻辑" return int(pad) # 用用户示例的20x20输入尺寸计算,也可替换为通用输入尺寸 h1 = calc_out_size(20, p1[0], k1.shape[-2], s1[0]) w1 = calc_out_size(20, p1[1], k1.shape[-1], s1[1]) h2 = calc_out_size(h1, p2[0], k2.shape[-2], s2[0]) w2 = calc_out_size(w1, p2[1], k2.shape[-1], s2[1]) p3_h = calc_pad(h2, 20, k3.shape[-2], s3[0]) p3_w = calc_pad(w2, 20, k3.shape[-1], s3[1]) p3 = (p3_h, p3_w) return k3, p3, s3 # 验证示例 if __name__ == "__main__": img = torch.randn(1, 1, 20, 20) k1 = torch.randn(1, 1, 3, 3) k2 = torch.randn(1, 1, 3, 3) p1, s1 = 1, 2 p2, s2 = 0, 1 # 两次连续卷积结果 out1 = F.conv2d(img, k1, padding=p1, stride=s1) out2 = F.conv2d(out1, k2, padding=p2, stride=s2) # 合并后的等效卷积结果 k3, p3, s3 = merge_two_convs(k1, p1, s1, k2, p2, s2) out3 = F.conv2d(img, k3, padding=p3, stride=s3) # 输出误差(应接近0) print(f"两次卷积与等效卷积的误差:{torch.max(torch.abs(out2 - out3))}")
注意事项
- 代码仅支持对称填充和整数步幅,若需处理不对称填充或非整数步幅,需调整填充计算和核掩码逻辑。
- 当步幅大于1时,合并后的核尺寸会缩小,仅保留对应采样位置的有效权重。
- 必须保证第一个卷积的输出通道数等于第二个卷积的输入通道数,否则无法直接合并。
内容的提问来源于stack exchange,提问作者Matteo
相关产品推荐
相关产品推荐

