You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何合并带不同填充与步幅的二维卷积(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 17:55:21