一、图像卷积
1.互相关操作(卷积运算)
严格意义来说,输入与核的计算叫做互相关,是将滤波器进行镜像翻转再进行计算的。但习惯上称为卷积。
卷积的计算过程是,给定卷积核在输入图片上从左到右从上到下滑动,卷积核框取图片对应大小进行各元素相乘再相加得到输出特征图的一个元素。
输出特征图大小计算公式:
outputsize=([(IN_h- K_h+2padding)/step]+1,[(IN_w- K_w+2padding)/step]+1)
此处没有padding和step
代码实现:
import torch
from torch import nn
def corr2d(X, K): #@save
"""Compute 2D cross-correlation."""
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
先根据公式定义Y的大小及输出结果的函数。然后传入参数。
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]], dtype=torch.float32)
K = torch.tensor([[0, 1], [2, 3]], dtype=torch.float32)
corr2d(X, K)
输出:
tensor([[19., 25.],
[37., 43.]])
2.卷积层
二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常先对卷积核随机初始化,然后不断迭代卷积核和偏差。
下面基于corr2d函数来实现一个自定义的二维卷积层。在构造函数__init__里声明weight和bias这两个模型参数。前向计算函数forward则是直接调用corr2d函数再加上偏差。
代码实现:
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
3.图像中物体边缘检测
下面看一个卷积层的简单应用:检测图像中物体的边缘,即找到像素变化的位置。首先构造一张 6×8 的图像(即高和宽分别为6像素和8像素的图像)。它中间4列为黑(0),其余为白(1)。
代码:
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.]])
然后造一个高和宽分别为1和2的卷积核K。
代码:
K = torch.tensor([[1, -1]], dtype=torch.float32)
下面将输入X和设计的卷积核K做互相关运算。可以看出,我们将从白到黑的边缘和从黑到白的边缘分别检测成了1和-1。其余部分的输出全是0。
代码:
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.]])
如果将这个kernel与转置的图像运算会发现边缘消失,说明这个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.]])
4.通过数据学习核数组
使用物体边缘检测中的输入数据X和输出数据Y来学习构造的核数组K。我们首先构造一个卷积层,将其卷积核初始化成随机数组。接下来在每一次迭代中,使用平方误差来比较Y和卷积层的输出,然后计算梯度来更新权重。简单起见,这里的卷积层忽略了偏差。
虽然之前构造了Conv2D类,但由于corr2d使用了对单个元素赋值([i, j]=)的操作因而无法自动求梯度。
代码:
# Construct a convolutional layer with 1 input channel and 1 output channel
# (channels will be introduced in the following section)
# and a kernel array shape of (1, 2). For sake of simplicity we ignore bias
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)
# The two-dimensional convolutional layer uses four-dimensional input and
# output in the format of (example channel, height, width), where the batch
# size (number of examples in the batch) and the number of channels are both 1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'batch {i+1}, loss {l.sum():.3f}')
输出:
batch 2, loss 1.172
batch 4, loss 0.293
batch 6, loss 0.088
batch 8, loss 0.031
batch 10, loss 0.012
可以看到,10次迭代后误差已经降到了一个比较小的值。现在来看一下学习到的核数组。
conv2d.weight.data.reshape((1, 2))
tensor([[ 1.0063, -0.9845]])
手动@小宋是呢