本节介绍卷积神经网络中最为常见的二维卷积层。二维卷积层常用来处理图像数据,它具有两个空间维度(高和宽)。
在二维互相关运算中, 卷积窗口从左上角开始,每次向右滑动一列,直至到达最右边,然后回到最左边的列并向下滑动一行,继续重复上面的动作,直至到达右下角。当卷积窗口滑动到某一位置时,窗口中的输入子数组与卷积窗口数组按元素相乘并求和,得到输出数组中相应位置的元素。
听着实在是复杂,看个实例吧。
我们对大小为
的输入二维数组和大小为
的二维核数组(卷积窗口大小)进行二维互相关运算。
根据互相关运算的过程,首先核数组会和
进行运算
然后向右滑动一列,核数组与
进行运算
上一次卷积窗口已经滑到了最右列,所以现在卷积窗口回到最左侧列并向下滑动一行,核数组与
进行运算
现在卷积窗口滑动到了右下角,核数组与
进行运算
最后将四次运算的数,与窗口滑动同顺序排列在输出二维数组中,得到输出二维数组。
分析上述过程可知,输出数组的大小与输入数组、核数组存在关系:
式中,
分别为输出数组、输入数组和核数组的行数;
分别为输出数组、输入数组和核数组的列数。
并且横向滑动次数和
相同,纵向滑动次数与
相同。
根据数组行列数之间关系和运算方法,最终程序实现如下。
def cross(X,K):
H_i = X.shape[0]
W_i = X.shape[1]
h = K.shape[0]
w = K.shape[1]
Y = torch.zeros((H_i-h+1,W_i-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, 1, 2], [3, 4, 5], [6, 7, 8]])
K = torch.tensor([[0, 1], [2, 3]])
cross(X, K)
得到输出数组为
tensor([[19., 25.],
[37., 43.]])
窗口形状为
的卷积层称为
卷积层。
二维卷积层的模型参数为卷积核(weight)和标量偏差(bias)。训练模型时,同样是先随机初始化模型参数,然后不断更新迭代参数。二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。
class Conv(nn.Module):
def __init__(self, kernel_size):
super(Conv, self).__init__()
#类型为Parameter的tensor自动添加到参数列表
self.weight = nn.Parameter(torch.randn(kernel_size))
self.bias = nn.Parameter(torch.randn(1)) #只有一组输出,所以只需要一个偏差参数
def forward(self, x):
#正向传播:互相关运算之后加上偏差
return cross(x, self.weight) + self.bias
下面使用二维卷积层检测图像中物体的边缘(像素发生变化的位置)。
首先初始化一张
的图像,令它的中间四列为黑(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.]])
然后构造一个大小为
的卷积核K,当它与输入做互相关运算时,如果横向相邻元素相同,输出为0;否则输出为非0。
K = torch.tensor([[1, -1]])
最后使用互相关运算,计算得到输出值。
Y = cross(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.]])
可以看出, 我们将从白到黑的边缘和从黑到白的边缘分别检测成了1和-1。其余部分的输出全是0。使用卷积核可以有效地表征局部空间。
这一部分将使用1.3中的输入数组X和输出数组Y来训练卷积神经网络,最终得到卷积核。
net = Conv(kernel_size = (1,2))
step =20#训练周期
lr = 0.01#学习率
for i in range(step):
Y_hat = net(X)
l = ((Y_hat - Y) ** 2).sum()
l.backward()
net.weight.grad.fill_(0)
net.bias.grad.fill_(0)
if (i+1)%5 ==0:
print('Step %d, loss %.3f' % (i + 1, l.item()))
print("weight:",net.weight.data)
print("bias:",net.bias.data)
各个学习周期的损失为
Step 5, loss 7.531
Step 10, loss 1.380
Step 15, loss 0.304
Step 20, loss 0.076
训练结束后模型参数为
weight: tensor([[ 0.8960, -0.9054]])
bias: tensor([0.0053])
训练得到的参数与真实参数[1,-1]
还是比较接近的。
为了得到卷积运算的输出,我们只需将核数组左右翻转并上下翻转,再与输入数组做互相关运算。 虽然卷积运算与互相关运算类似,但如果它们使用相同的核数组,对于同一个输入,输出往往并不相同。
二维卷积层输出的二维数组可以被看作输入数组在空间维度上某一级的表征,也就是特征图。
输入数组的感受野决定输出数组中对应元素值。如1.1中输入数组中的
是输出数组中
的感受野。事实上,整个输入数组都是输出数组的感受野。