图像卷积

一、互相关运算

严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算(cross-correlation),而不是卷积运算。在卷积层中,输入张量和核张量通过(互相关运算)产生输出张量。

在下图中,输入是高度为333、宽度为333的二维张量(即形状为3×33 \times 33×3)。卷积核的高度和宽度都是222,而卷积核窗口(或卷积窗口)的形状由内核的高度和宽度决定(即2×22 \times 22×2)。

image-20251017121056823

在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。
当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。
在如上例子中,输出张量的四个元素由二维互相关运算得到,这个输出高度为222、宽度为222,如下所示:
0×0+1×1+3×2+4×3=19,1×0+2×1+4×2+5×3=25,3×0+4×1+6×2+7×3=37,4×0+5×1+7×2+8×3=43. 0\times0+1\times1+3\times2+4\times3=19,\\ 1\times0+2\times1+4\times2+5\times3=25,\\ 3\times0+4\times1+6\times2+7\times3=37,\\ 4\times0+5\times1+7\times2+8\times3=43. 0×0+1×1+3×2+4×3=19,1×0+2×1+4×2+5×3=25,3×0+4×1+6×2+7×3=37,4×0+5×1+7×2+8×3=43.

注意,输出大小略小于输入大小。这是因为卷积核的宽度和高度大于1,
而卷积核只与图像中每个大小完全适合的位置进行互相关运算。
所以,输出大小等于输入大小nh×nwn_h \times n_wnh×nw减去卷积核大小kh×kwk_h \times k_wkh×kw,即:

(nh−kh+1)×(nw−kw+1).(n_h-k_h+1) \times (n_w-k_w+1).(nhkh+1)×(nwkw+1).

import torch
from torch import nn
from d2l import torch as d2l
# 实现2D卷积过程
# 函数接受输入张量X和卷积核张量K,并返回输出张量Y
def corr2d(X, K):  #@save
    """计算二维互相关运算"""
    h, w = K.shape
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
    return Y

通过上图中的输入张量X和卷积核张量K,我们来[验证上述二维互相关运算的输出]。

X = 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]])
corr2d(X, K)
tensor([[19., 25.],
        [37., 43.]])

二、卷积层

卷积层中的两个被训练的参数是卷积核权重和标量偏置。
在训练基于卷积层的模型时,需要随机初始化卷积核权重。

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

高度和宽度分别为hhhwww的卷积核可以被称为h×wh \times wh×w卷积或h×wh \times wh×w卷积核。
我们也将带有h×wh \times wh×w卷积核的卷积层称为h×wh \times wh×w卷积层。

三、图像中目标的边缘检测

如下是[卷积层的一个简单应用:]通过找到像素变化的位置,来(检测图像中不同颜色的边缘)。
首先,我们构造一个6×86\times 86×8像素的黑白图像。中间四列为黑色(000),其余像素为白色(111)。

X = torch.ones((6, 8))
X[:, 2:6] = 0
X
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.]])

接下来,我们构造一个高度为111、宽度为222的卷积核K。当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零。

K = torch.tensor([[1.0, -1.0]])

现在,我们对参数X(输入)和K(卷积核)执行互相关运算。
如下所示,[输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘],其他情况的输出为000

Y = corr2d(X, K)
Y
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.]])

现在我们将输入的二维图像转置,再进行如上的互相关运算。
其输出如下,之前检测到的垂直边缘消失了。
不出所料,这个[卷积核K只可以检测垂直边缘],无法检测水平边缘。

corr2d(X.t(), K)
tensor([[0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.]])

四、学习卷积核

我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较Y与卷积层输出的平方误差,然后计算梯度来更新卷积核。为了简单起见,我们在此使用内置的二维卷积层,并忽略偏置。

# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)

# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2  # 学习率

for i in range(10):
    Y_hat = conv2d(X)
    l = (Y_hat - Y) ** 2
    conv2d.zero_grad()
    l.sum().backward()
    # 迭代卷积核
    conv2d.weight.data[:] -= lr * conv2d.weight.grad
    if (i + 1) % 2 == 0:
        print(f'epoch {i+1}, loss {l.sum():.3f}')
epoch 2, loss 6.422
epoch 4, loss 1.225
epoch 6, loss 0.266
epoch 8, loss 0.070
epoch 10, loss 0.022

101010次迭代之后,误差已经降到足够低。现在我们来看看我们[所学的卷积核的权重张量]。

conv2d.weight.data.reshape((1, 2))
tensor([[ 1.0010, -0.9739]])

学习到的卷积核权重非常接近我们之前定义的卷积核K

五、互相关和卷积


1. 数学上的区别

  • 卷积(Convolution)
    数学里定义卷积的时候,卷积核要翻转 180°(先左右,再上下),然后再和输入做滑动相乘求和。
    公式大概长这样:
    (f∗g)(t)=∑τf(τ)g(t−τ) (f * g)(t) = \sum_\tau f(\tau) g(t - \tau) (fg)(t)=τf(τ)g(tτ)
    👉 注意这里的 (g) 被翻转了。

  • 互相关(Cross-correlation)
    不做翻转,直接拿卷积核在输入上“平移”,点乘求和。
    (f⋆g)(t)=∑τf(τ)g(t+τ) (f \star g)(t) = \sum_\tau f(\tau) g(t + \tau) (fg)(t)=τf(τ)g(t+τ)

所以从定义上看,差别就一个字:卷积要翻核,互相关不翻核


2. 在深度学习中的实际用法

  • 框架里(PyTorch、TensorFlow 等)卷积层的实现,其实用的是互相关,不是严格的数学卷积!
    原因很简单:
    • 翻转没啥必要,
    • 卷积核是学出来的,你让它翻还是不翻,最后都能学到合适的模式。
  • 所以大家口头上都叫“卷积层”,其实背后计算的是“互相关”。

3. 直观理解

  • 可以把卷积核看成一副“探测眼镜”:比如一个 3×3 的边缘检测核。
  • 卷积(数学意义):先把眼镜翻过来再看。
  • 互相关(深度学习实际操作):直接戴上眼镜就看。
  • 最终效果差别不大,因为“眼镜”是训练出来的,模型自己会调整成能看的样子。

六、特征映射和感受野


1. 特征映射(Feature Map)

  • 是什么?
    卷积层的输出结果,就是一张张“特征图”。
    每个卷积核都会扫描整张输入图片,然后生成一张新的二维图,这张图里每个像素值就代表了“这个位置对某种特征的响应强度”。
    • 比如一个卷积核可能专门学会检测“水平边缘”,那它生成的特征映射里,凡是有横线的地方就会亮起来。
  • 直观理解
    想象你在看一张风景图:
    • 一个卷积核像是戴了“找树的眼镜”,它会输出一张“树的特征图”;
    • 另一个卷积核像是戴了“找房子的眼镜”,它就输出一张“房子的特征图”。
      这些不同的特征图堆叠在一起,就构成了这一层的“输出通道”。

2. 感受野(Receptive Field)

  • 是什么?
    就是卷积神经网络里某一层的一个神经元,能看到的输入区域大小
    • 比如在输入层(原图像),一个神经元只“盯着”一个像素点;
    • 经过卷积层后,感受野就扩大了,一个神经元可能代表输入图像中的一个小方块区域;
    • 越往深层走,感受野越大,最终可能覆盖整张图像。
  • 直观理解
    就像你用相机拍照:
    • 一开始你只能盯着一块草地(小感受野);
    • 拉远镜头,能看到整片草原(大感受野);
    • 再拉远,整个风景尽收眼底(全局感受野)。
      网络训练也是类似过程,从低层的小细节到高层的大局。

小结

  • 二维卷积层的核心计算是二维互相关运算。最简单的形式是,对二维输入数据和卷积核执行互相关操作,然后添加一个偏置。
  • 我们可以设计一个卷积核来检测图像的边缘。
  • 我们可以从数据中学习卷积核的参数。
  • 学习卷积核时,无论用严格卷积运算或互相关运算,卷积层的输出不会受太大影响。
  • 当需要检测输入特征中更广区域时,我们可以构建一个更深的卷积网络。

内容声明
本文基于开源教材《动手学深度学习》(Dive into Deep Learning, 作者:Aston Zhang、Zachary C. Lipton、Mu Li、Alexander J. Smola 等)整理,原始项目地址:https://github.com/d2l-ai/d2l-zh。
在整理过程中对部分内容进行了删改和补充,仅用于个人学习与交流,版权归原作者所有。


更多推荐