卷积神经网络(CNN)是深度学习中的一项革命性技术,它在计算机视觉任务中表现出了极高的效率。在这篇文章中,我们将通过一系列简单易懂的例子,介绍图像卷积的基本概念和技术实现。
1. 互相关运算:卷积的本质
1.1 卷积与互相关
首先需要澄清一个常见的误解。很多时候,我们提到卷积神经网络(CNN)时,都会说“卷积层”,但实际上,它们使用的运算并不是严格意义上的卷积,而是 互相关运算(cross-correlation)。虽然这两个运算非常相似,但它们在计算方式上有一点差别。在卷积神经网络中,输入张量与卷积核张量通过互相关运算进行处理。
1.2 互相关运算过程
考虑一个二维图像和一个卷积核。输入图像的大小为 ,卷积核的大小为 ,其中 和 分别是卷积核的高度和宽度。我们的任务是通过互相关运算,得到一个新的输出张量。
互相关运算的核心思想是:卷积核窗口从输入张量的左上角开始,依次从左到右、从上到下滑动,每次滑动时,卷积核和对应区域的图像进行元素相乘并求和,得到一个输出值。假设输入图像 和卷积核 为:
则通过互相关运算得到的输出张量 为:
假设输入张量的高度和宽度分别为 和 ,卷积核的高度和宽度分别为 和 ,则输出张量的高度和宽度分别为:
- 由于卷积核需要与输入张量的每个位置对齐进行互相关运算,输出的尺寸会比输入小。
- 输入张量的每一维(高度或宽度)会减去卷积核的对应维度大小,并加上1,这是因为卷积核会在输入张量上滑动的次数等于(输入大小 - 卷积核大小 + 1)。
因此,输出张量的尺寸比输入张量要小,除非采取 填充(padding) 等技术来调整输出的大小。
1.3 实现互相关运算
我们可以通过 Python 和 PyTorch 实现这一操作。假设我们有如下输入张量 和卷积核 :
import torch
def corr2d(X, K):
"""
互相关运算实现
"""
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 = 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]
])
print(corr2d(X, K))
输出将是:
tensor([[19., 25.],
[37., 43.]])
通过这个例子,我们可以看到,卷积核通过与图像的局部区域进行元素级的乘法并求和,得到了新的输出。
2. 卷积层:实现卷积操作
在卷积神经网络中,卷积层的功能是对输入张量执行卷积运算并生成输出。这个过程通常包括两个训练参数:卷积核的权重和偏置。
我们可以通过定义一个卷积层,来执行二维卷积操作。以下是一个简单的卷积层实现:
from torch import nn
class Conv2D(nn.Module):
def __init__(self, kernel_size):
"""
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
net = Conv2D((2, 2))
print(net(X))
这个卷积层有两个主要部分:卷积核权重(self.weight
)和偏置(self.bias
)。卷积操作本身由 corr2d
函数完成。
输出如下:
tensor([[2.6450, 3.6992],
[5.8076, 6.8617]], grad_fn=<AddBackward0>)
3. 边缘检测:卷积层的实际应用
一个经典的卷积层应用是边缘检测。我们可以设计一个简单的卷积核来检测图像中的边缘。例如,考虑一个简单的二值图像,其中中间的四列为黑色,其他部分为白色:
X = torch.ones((6, 8))
X[:, 2:6] = 0
print(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.]])
接下来,我们设计一个简单的卷积核,用于检测水平边缘:
K = torch.tensor([[1.0, -1.0]])
执行互相关运算后,我们得到的输出张量显示了图像中的边缘:
Y = corr2d(X, K)
print(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.]])
可以看到,1表示从白色到黑色的边缘,-1表示从黑色到白色的边缘。
4. 学习卷积核:通过训练优化卷积核
在实际应用中,我们通常无法手动设计有效的卷积核。相反,我们希望通过数据来学习卷积核的参数。例如,通过最小化损失函数,我们可以更新卷积核的权重。
我们可以通过构造一个简单的卷积层,随机初始化卷积核,并通过反向传播算法学习最优的卷积核。以下是一个例子:
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(1, 2), bias=False)
print("学习前的卷积核权重为:", conv2d.weight.data)
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
print(f'epoch {i + 1}, loss {l.sum():.3f}')
print("学习到的卷积核权重为:", conv2d.weight.data)
通过多次迭代,我们将看到误差逐渐减小,最终学到的卷积核与我们最初设计的卷积核非常相似:
学习前的卷积核权重为: tensor([[[[-0.3466, 0.6975]]]])
epoch 1, loss 29.647
epoch 2, loss 12.569
epoch 3, loss 5.421
epoch 4, loss 2.395
epoch 5, loss 1.092
epoch 6, loss 0.519
epoch 7, loss 0.258
epoch 8, loss 0.135
epoch 9, loss 0.074
epoch 10, loss 0.042
学习到的卷积核权重为: tensor([[[[ 1.0013, -0.9636]]]])
5. 互相关与卷积:两者的区别
尽管我们通常使用“卷积”这一术语来描述 CNN 中的运算,但实际上,这个运算是互相关运算。为了得到严格的卷积输出,我们需要对卷积核进行水平和垂直翻转。但在卷积神经网络的训练中,由于我们是从数据中学习卷积核,互相关和卷积的差异对输出结果的影响非常小。
6. 特征映射与感受野
在卷积神经网络中,特征映射(feature map)是卷积层的输出。它表示输入图像经过卷积层处理后得到的结果。每个特征映射的元素都有一个 感受野(receptive field),即 该元素在前向传播过程中所依赖的输入区域。通过增加卷积层的深度,我们可以扩大感受野,捕捉更广泛的图像特征。
7. 小结
- 二维卷积层的核心运算是 二维互相关运算,通过卷积核对输入数据进行卷积处理。
- 卷积层 的任务是将卷积核和输入数据进行卷积运算并加上偏置。
- 我们可以通过卷积核来 检测图像的边缘 等特征。
- 学习卷积核 可以通过梯度下降等优化方法,从数据中自动调整卷积核的权重。
- 互相关与卷积在卷积神经网络中的输出几乎相同,因此我们通常将其统称为卷积操作。
- 感受野 是指在卷积操作中,某一元素可能受到的输入区域的影响,深层网络能够捕捉更广泛的图像特征。
通过理解图像卷积的这些基本概念,我们能够更好地理解卷积神经网络的运作原理,并为构建更复杂的深度学习模型打下基础。