图像卷积

深入理解图像卷积的数学原理和PyTorch实现,掌握卷积层的核心概念和应用技巧

发布于 2025-08-07 更新于 2025-08-071,217 字 3 分钟
图像卷积的文章封面
展开文章目录
  1. 1. 由互相关运算实现的卷积层
  2. 1.1 互相关运算
  3. 1.2 实现
  4. 1.3 在边界检测中的应用
  5. 1.4 卷积层
  6. 2. 学习卷积核
  7. 总结

1. 由互相关运算实现的卷积层

1.1 互相关运算

数学上的卷积被定义为,卷积核在水平和垂直方向翻转后,与输入数据滑动求和的过程。而卷积层的设计并没有严格遵循数学上卷积的定义,采用的是其简化形式,没有卷积核翻转的步骤——互相关运算 (cross-correlation)。

I(x,y)I(x,y) 为输入,K(x,y)K(x,y) 为卷积核。经典卷积运算 IKI*K 的定义如下:

(IK)(x,y)=mnI(m,n)K(xm,yn)(I*K)(x,y)=\sum_m\sum_nI(m,n)\cdot K(x-m,y-n)

而互相关运算 IKI*K 的定义如下:

(IK)(x,y)=mnI(m,n)K(x+m,y+n)(I*K)(x,y)=\sum_m\sum_nI(m,n)\cdot K(x+m,y+n)

xmx-myny-n 描述了卷积运算中的翻转后滑动;xmx-myny-n 描述了互相关运算中未经过翻转的滑动。

只考虑二维的视角。

有填充边界的互相关运算保证了输出的大小与输入的大小不变

1.2 实现

import torch


def corr2d(input2d: torch.Tensor, kernel: torch.Tensor) -> torch.Tensor:
    """二维的互相关运算"""

    h_input, w_input = input2d.shape
    h_kernel, w_kernel = kernel.shape

    # 不使用填充 (padding) 时的输出尺寸
    h_output = h_input - h_kernel + 1
    w_output = w_input - w_kernel + 1
    output = torch.empty(h_output, w_output)

    # 互相关运算
    for i in range(h_output):  # 遍历输出张量的每一行
        for j in range(w_output):  # 遍历输出张量的每一列
            # 输入的局部区域与卷积核的逐元素相乘,并求和
            output[i, j] = (input2d[i:i + h_kernel, j:j + w_kernel] * kernel).sum()

    return output
if __name__ == '__main__':
    i = torch.tensor([[0.0, 1.0, 2.0],
                      [3.0, 4.0, 5.0],
                      [6.0, 7.0, 8.0]])
    k = torch.tensor([[0.0, 1.0],
                      [2.0, 3.0]])
    o = corr2d(i, k)
    print(o)
tensor([[19., 25.],
        [37., 43.]])

1.3 在边界检测中的应用

图像边缘本质上是图像像素值发生变化的位置,可以用互相关运算检测。

这里给出一个示例:对于一幅像素尺寸为 6×8 的黑白图像,用 1 表示白色,用 0 表示黑色,使用水平差分算子检测水平方向的边缘(亦可选择其他类型的算子检测其他方向的边缘)。

pic = torch.ones(6, 8)
pic[:, 2:6] = 0
k = torch.tensor([[1, -1]])
boundary = corr2d(pic, k)

print(f'{pic.shape = }\n{pic}')
print(f'{boundary.shape = }\n{boundary}')
pic.shape = torch.Size([6, 8]):
tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.]])
boundary.shape = torch.Size([6, 7]):
tensor([[ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.]])

输出的结果中,1表示从亮到暗的边缘,-1表示从暗到亮的边缘,0表示非边缘处的像素。

1.4 卷积层

虽然执行的不是经典的卷积运算,但习惯上仍称这个结构为卷积层。

卷积层对输入数据与卷积核(滤波器或权重)执行互相关运算并滑动,添加偏置项后生成特征图 (feature map)(也称为特征映射)。其中,特征图的维数等于卷积层的输出通道数out_channels。卷积核权重的学习过程,就是从输入数据提取边缘、纹理或形状等特征的过程。

在实现卷积层时,与全连接层的实现类似,同样需要定义权重weight和偏置bias 2 个参数:

class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        super().__init__()
        self.weight = nn.Parameter(torch.rand(kernel_size))
        self.bias = nn.Parameter(torch.zeros(1))

    def forward(self, x):
        return corr2d(x, self.weight) * self.bias

可以使用卷积核大小img(用img表示)、步幅 (stride)img、填充 (padding) img及网络的层数img,以递推公式的形式量化感受野的大小。第img层的感受野img为:

img

其中:

  • img:前一层的感受野;
  • img:第img层卷积核的大小;
  • img:第img层的步幅。

单卷积层的感受野的大小等于卷积核的尺寸。

img为输入、img为卷积核时,步幅为 1 且无填充的单卷积层输出为:

img

2. 学习卷积核

互相关运算在边界检测中的应用中,直接指定了用于检测水平黑白边缘的卷积核,并很有效。但在更复杂的场景下,希望能“学习到”适合特定模式的卷积核。

于是,在卷积层中将卷积核初始化为随机张量,用深度学习的思想更新卷积核(忽略偏置):

import torch
from torch import nn

# 卷积层输入数据形状应为 (N, C_in, H_in, W_in),需要执行 reshape 操作
pic = torch.tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
                    [1., 1., 0., 0., 0., 0., 1., 1.],
                    [1., 1., 0., 0., 0., 0., 1., 1.],
                    [1., 1., 0., 0., 0., 0., 1., 1.],
                    [1., 1., 0., 0., 0., 0., 1., 1.],
                    [1., 1., 0., 0., 0., 0., 1., 1.]]).reshape(1, 1, 6, 8)
boundary = torch.tensor([[0., 1., 0., 0., 0., -1., 0.],
                         [0., 1., 0., 0., 0., -1., 0.],
                         [0., 1., 0., 0., 0., -1., 0.],
                         [0., 1., 0., 0., 0., -1., 0.],
                         [0., 1., 0., 0., 0., -1., 0.],
                         [0., 1., 0., 0., 0., -1., 0.]]).reshape(1, 1, 6, 7)

net = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)  # 单通道输入输出的卷积层,卷积核形状为 (1, 2)
LR = 3e-2
EPOCH = 15

for i in range(EPOCH):
    boundary_hat = net(pic)
    l = ((boundary_hat - boundary) ** 2).sum()
    net.zero_grad()
    l.backward()
    net.weight.data[:] -= LR * net.weight.grad
    print(f'EPOCH: {i + 1:02}/{EPOCH}, LOSS: {l:.3f}')

print(f'\nkernel = {net.weight.data.view(1, 2)}')
EPOCH: 01/15, LOSS: 14.776
EPOCH: 02/15, LOSS: 6.882
EPOCH: 03/15, LOSS: 3.350
EPOCH: 04/15, LOSS: 1.712
EPOCH: 05/15, LOSS: 0.919
EPOCH: 06/15, LOSS: 0.516
EPOCH: 07/15, LOSS: 0.300
EPOCH: 08/15, LOSS: 0.180
EPOCH: 09/15, LOSS: 0.110
EPOCH: 10/15, LOSS: 0.069
EPOCH: 11/15, LOSS: 0.043
EPOCH: 12/15, LOSS: 0.027
EPOCH: 13/15, LOSS: 0.017
EPOCH: 14/15, LOSS: 0.011
EPOCH: 15/15, LOSS: 0.007

kernel = tensor([[ 1.0074, -0.9902]])

根据结果,这种方法可以有效地”学习”到目标卷积核[[1, -1]]


总结

本文介绍了图像卷积的核心概念:

  • 互相关运算:卷积层实际执行的计算过程
  • 边界检测:卷积在特征提取中的典型应用
  • 卷积核学习:通过梯度下降自动学习最优的卷积核参数
  • 感受野:网络能够”看到”的输入区域范围

如果这篇记录对你有帮助,可以留下一句回应。

前往留言

DISCUSSION

讨论与回应

欢迎补充细节、指出问题,或分享与这篇文章有关的经验。

昵称与邮箱为必填项,邮箱仅用于头像和回复通知,不会公开。网址可以留空。

正在准备留言区…

演示赞赏界面

谢谢你愿意支持长期写作。

第一阶段不会发起支付或收集任何信息。接入真实赞赏渠道后,这里会展示清楚的金额、渠道和完成状态。

输入关键词开始搜索 · 按 Esc 关闭
打开完整搜索页 →