LK 博客
Conv2d
大数据
约 1 分钟阅读 0 赞 0 条评论 鸿蒙黑体

Conv2d

zyd
zyd @zyd
累计点赞 0 登录后每个账号只能点一次
内容长度 0 正文词元数
正文
目录会跟随阅读位置移动。
阅读进度

手写 Conv2d 笔记

这份笔记对应代码文件:

目标是把下面几件事串起来理解:

  1. Conv2d 到底在算什么
  2. 为什么深度学习里常用 B, C, H, W
  3. 1x1 Conv2d 为什么能做通道变换
  4. 通道注意力为什么经常会用到 1x1 Conv2d
  5. 卷积算子.pycv通道注意力.py 的区别是什么

1. Conv2d 的本质

卷积的核心可以概括成一句话:

取输入中的一小块区域,和卷积核逐元素相乘,再把结果加起来,得到一个输出值。

如果是二维卷积,那么输出特征图上的每一个位置,都是这样算出来的。

例如最普通的单通道 3x3 卷积:

输入局部区域:
1 2 3
4 5 6
7 8 9

卷积核:
1 0 1
0 1 0
1 0 1

那么当前位置的卷积结果就是:

1*1 + 2*0 + 3*1 +
4*0 + 5*1 + 6*0 +
7*1 + 8*0 + 9*1

2. PyTorch 常见的张量格式

在图像处理中,OpenCV 读出来的图片通常是:

H, W, C

含义是:

  • H: 高度
  • W: 宽度
  • C: 通道数

例如一张彩色图,形状可能是:

(480, 640, 3)

但是在深度学习里,尤其是 PyTorch,常见格式是:

B, C, H, W

含义是:

  • B: batch size,一次输入多少张图
  • C: 通道数
  • H: 高度
  • W: 宽度

例如:

(1, 3, 480, 640)

表示:

  • 1 张图
  • 3 个通道
  • 高 480
  • 宽 640

在 这 里,做了下面这个变换:

img.astype(np.float32).transpose(2, 0, 1)[np.newaxis, ...]

它的作用就是把:

HWC -> CHW -> BCHW

3. 标准 Conv2d 中卷积核的形状

在深度学习里,卷积核不是简单的二维矩阵,而是四维:

(C_out, C_in, K_h, K_w)

其中:

  • C_out: 输出通道数
  • C_in: 输入通道数
  • K_h: 卷积核高度
  • K_w: 卷积核宽度

比如:

weight.shape = (8, 3, 3, 3)

意思是:

  • 有 8 个输出通道
  • 每个输出通道都要同时看 3 个输入通道
  • 每个卷积核大小是 3x3

所以,一个输出通道不是只看一个输入通道,而是会把所有输入通道的信息融合起来。

4. 手写 Conv2d 的核心逻辑

conv2d_numpy(x, weight, bias=None, stride=1, padding=0),本质是在手写 PyTorch 的 nn.Conv2d` 前向过程。

它的流程可以拆成下面几步。

4.1 先确定输入和卷积核形状

batch_size, in_channels, in_h, in_w = x.shape
out_channels, weight_channels, kernel_h, kernel_w = weight.shape

这一步是在把每个维度拆出来,方便后面算输出尺寸和遍历。

4.2 计算输出尺寸

公式是:

out_h = (in_h + 2 * pad_h - kernel_h) // stride_h + 1
out_w = (in_w + 2 * pad_w - kernel_w) // stride_w + 1

例如:

in_h = 8
kernel_h = 3
pad_h = 1
stride_h = 1

那么:

out_h = (8 + 2*1 - 3) // 1 + 1 = 8

4.3 对输入补零

x_pad = np.pad(...)

如果卷积核是 3x3,常见会设置 padding=1,这样输出高宽通常和输入一致。

4.4 在输出特征图上逐点计算

最核心的部分就是 4 层循环:

for b in range(batch_size):
    for oc in range(out_channels):
        for i in range(out_h):
            for j in range(out_w):

含义分别是:

  • 遍历第几张图
  • 遍历第几个输出通道
  • 遍历输出高方向的位置
  • 遍历输出宽方向的位置

然后取出当前位置对应的输入局部区域:

region = x_pad[
    b,
    :,
    row_start : row_start + kernel_h,
    col_start : col_start + kernel_w,
]

这个 region 的形状是:

(C_in, K_h, K_w)

然后和第 oc 个卷积核做逐元素乘法,再求和:

value = np.sum(region * weight[oc])

最后得到当前输出位置的值:

out[b, oc, i, j] = value

所以公式可以写成:

out[b, oc, i, j] = sum(region * weight[oc]) + bias[oc]

如果没有 bias,那就不加偏置。

5. 什么是 1x1 Conv2d

如果卷积核大小是:

kernel_size = 1

那么卷积核形状就变成:

(C_out, C_in, 1, 1)

这时候它不会去看周围邻域,因为窗口大小只有 1x1

它只会在当前位置上,把不同通道做线性组合。

公式会变成:

out[b, oc, i, j] = sum(x[b, ic, i, j] * weight[oc, ic, 0, 0])

注意这里没有看周围的 i-1, i+1, j-1, j+1,所以:

  • 3x3 卷积: 既看空间邻域,也混合通道
  • 1x1 卷积: 不看空间邻域,只混合通道

6. 为什么 1x1 Conv2d 很适合做通道注意力

通道注意力关心的问题是:

哪些通道更重要,哪些通道应该被压低?

它关注的是“通道关系”,不是“空间邻域关系”。

所以经常会先把空间维度压掉,比如:

  • 全局平均池化
  • 全局最大池化

这样原来:

(B, C, H, W)

会被压成:

(B, C, 1, 1)

这时候再用 1x1 Conv2d,就很自然,因为它本质上就是在做通道变换。

通道注意力.py 里这部分写成:

self.fc1 = nn.Conv2d(in_channels, hidden_channels, kernel_size=1, bias=False)
self.fc2 = nn.Conv2d(hidden_channels, in_channels, kernel_size=1, bias=False)

虽然变量名叫 fc1fc2,但实际实现用的是 1x1 Conv2d

原因是:

  • 输入仍然保持四维张量格式
  • 写法更统一
  • 和全连接层在 1x1 情况下本质等价

7. 通道注意力的整体逻辑

标准通道注意力可以写成:

avg_out = MLP(AvgPool(x))
max_out = MLP(MaxPool(x))
attention = sigmoid(avg_out + max_out)
out = x * attention

含义是:

  1. 先对每个通道做全局池化,提取每个通道的整体响应
  2. 再经过一个小型 MLP,学习通道之间的关系
  3. sigmoid 生成 0~1 的权重
  4. 再把这些权重乘回原特征图

这样:

  • 重要通道权重大
  • 不重要通道权重小

8. 卷积算子.pycv通道注意力.py 的区别

这两个文件都在做卷积相关的事情,但目标不一样。

8. 读这几份代码时的建议顺序

如果你现在正在学这一块,建议按这个顺序看:

  1. 先看 卷积算子.py 先理解“卷积就是局部乘加”
  2. 再看 cv通道注意力.py 理解标准 Conv2d 的输入输出格式和多通道卷积
  3. 最后看 通道注意力.py 理解为什么 1x1 Conv2d 可以拿来做通道注意力

这样会比较顺。

9. 一句话总结

  • 普通卷积:提取局部空间特征
  • 1x1 卷积:做通道之间的线性组合
  • 通道注意力:给每个通道分配不同的重要性权重

如果把三者连起来理解,可以记成:

普通卷积负责“看哪里”,1x1 卷积负责“调哪些通道更重要”。

作者名片

zyd
zyd
@zyd

这个作者暂时还没有填写个人简介。

评论区
文章作者和管理员都可以管理这里的评论。
0 条评论
登录后即可参与评论。 去登录
还没有评论,欢迎留下第一条交流内容。