卷积神经网络(convolutional neural network)指带有卷积层(convolutional layer)的神经网络,虽然卷积层得名于卷积(convolution)运算,但是在实际应用中我们通常使用更加直观的互相关运算(cross-correlation)。在二维卷积层中,一个二维输入数组和一个二维核(kernel)数组通过互相关运算输出为一个二维数组,这个核数组也称之为卷积核或者过滤器(filter)。运算方式如下图所示:
在运算过程中,卷积核从输入数组最左上方开始,按顺序从左至右、从上至下依次在输入数组上滑动。上图的输出数组计算方式如下:
利用MXNet + Python实现二维互相关运算:
from mxnet import nd
# 定义二维互相关运算函数,接收输入数组x和核数组k,输出数组y
# 这里涉及到 MXNet中 NDArray的基本操作,不清楚的可参看笔者之前的文章或者 MXNet官方文档
def corr2d(x, k):
height, width = k.shape
y = nd.zeros((x.shape[0] - height + 1, x.shape[1] - width + 1))
for i in range(y.shape[0]):
for j in range(y.shape[1]):
y[i, j] = (x[i:i + height, j:j + width] * k).sum()
return y
二维卷积层将输入和卷积核做互相关运算,并加上一个偏差得到输出,卷积层的模型参数就包括卷积和核标量偏差。在训练模型的时候,首先对卷积随机核初始化,然后不断迭代更新卷积核和偏差。利用上述corr2d
函数实现一个自定义的二维卷积层:
from mxnet.gluon import nn
# 定义二维卷积层,参数包括卷积核和标量偏差
# 由于corr2d中直接使用索引对单个元素复制,所以无法自动求解梯度
# 这里涉及到 MXNet自定义模型及参数自定义,可参考 MXNet官方文档
class Convolution2d(nn.Block):
def __init__(self, kernel_size):
super(Convolution2d, self).__init__()
self.weight = self.params.get('weight', shape=kernel_size)
self.bias = self.params.get('bias', shape=(1,))
def forward(self, args):
这里直接引用《动手学深度学习》一书中关于“为什么可以使用更直观的互相关运算替代卷积运算”的解释:
实际上,卷积运算与互相关运算类似。为了得到卷积运算的输出,我们只需将核数组左右翻转并上下翻转,再与输入数组做互相关运算。可见,卷积运算和互相关运算虽然类似,但如果它们使用相同的核数组,对于同一个输入,输出往往并不相同。
那么,你也许会好奇卷积层为何能使用互相关运算替代卷积运算。其实,在深度学习中核数组都是学出来的:卷积层无论使用互相关运算或卷积运算都不影响模型预测时的输出。为了解释这一点,假设卷积层使用互相关运算学出图5.1(注:即本文互相关运算中的示例图)中的核数组。设其他条件不变,使用卷积运算学出的核数组即图5.1中的核数组按上下、左右翻转。也就是说,图5.1中的输入与学出的已翻转的核数组再做卷积运算时,依然得到图5.1中的输出。为了与大多数深度学习文献一致,如无特别说明,本书中提到的卷积运算均指互相关运算。
这里也直接引用《动手学深度学习》一书中的解释。
二维卷积层输出的二维数组可以看作是输入在空间维度(宽和高)上某一级的表征,也叫特征图(feature map)。影响元素 x 的前向计算的所有可能输入区域(可能大于输入的实际尺寸)叫做 x 的感受野(receptive field)。以图5.1为例,输入中阴影部分的四个元素是输出中阴影部分元素的感受野。我们将图5.1中形状为 2×2 的输出记为 Y ,并考虑一个更深的卷积神经网络:将 Y 与另一个形状为 2×2 的核数组做互相关运算,输出单个元素 z 。那么, z 在 Y 上的感受野包括 Y 的全部四个元素,在输入上的感受野包括其中全部9个元素。可见,我们可以通过更深的卷积神经网络使特征图中单个元素的感受野变得更加广阔,从而捕捉输入上更大尺寸的特征。
我们常使用“元素”一词来描述数组或矩阵中的成员。在神经网络的术语中,这些元素也可称为“单元”。当含义明确时,本书不对这两个术语做严格区分。
在上述互相关运算的基础上,做一个简单的边缘检测试验。代码如下:
# coding=utf-8
# author: BebDong
# 2019/1/21
# 二维互相关运算和二维卷积层
# 卷积运算和互相关运算类似,使用哪一个不影响模型的输出,因为卷积核都是学习出来的
from mxnet import nd, autograd
from mxnet.gluon import nn
import matplotlib.pyplot as plt
import pylab
# 定义二维互相关运算函数,接收输入数组x和核数组k,输出数组y
def corr2d(x, k):
height, width = k.shape
y = nd.zeros((x.shape[0] - height + 1, x.shape[1] - width + 1))
for i in range(y.shape[0]):
for j in range(y.shape[1]):
y[i, j] = (x[i:i + height, j:j + width] * k).sum()
return y
# 定义二维卷积层,参数包括卷积核和标量偏差
# 由于corr2d中直接使用索引对单个元素复制,所以无法自动求解梯度
class Convolution2d(nn.Block):
def __init__(self, kernel_size):
super(Convolution2d, self).__init__()
self.weight = self.params.get('weight', shape=kernel_size)
self.bias = self.params.get('bias', shape=(1,))
def forward(self, args):
return corr2d(args, self.weight.data()) + self.bias.data()
# 画图,将像素数组可视化展现
def draw(legend, pixels):
plt.figure(legend)
plt.imshow(pixels, cmap='gray', interpolation='None')
pylab.show()
# 测试互相关运算
x_example = nd.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
k_example = nd.array([[0, 1], [2, 3]])
print(corr2d(x_example, k_example))
# 卷积的简单应用:检测图像中物体的边缘,即找到像素变化的位置
x_image = nd.ones((6, 8)) # 构造一张6*8图像,中间四列黑(0),其余白(1)
x_image[:, 2:6] = 0
k_detect = nd.array([[1, -1]]) # 构造一个卷积核,如果输入横向相邻元素相同,则输出为0,否则为非0
detect_result = corr2d(x_image, k_detect)
print(detect_result)
# 画出原图和检测结果
draw('original', x_image.asnumpy())
draw('output', detect_result.asnumpy())
# 通过数据来学习核数组,即卷积层的模型参数
# 由于上述自定义卷积层无法自动求解梯度,这里使用Gluon预定义的卷积层,构造一个输出通道数为1,卷积核(1,2)的卷积层
# 同样使用上述的6*8图像例子
conv2d = nn.Conv2D(1, kernel_size=(1, 2))
conv2d.initialize()
x_image = x_image.reshape((1, 1, 6, 8)) # (样本数,通道数,高,宽)
detect_result = detect_result.reshape((1, 1, 6, 7))
for i in range(10): # 训练10次
with autograd.record():
y_hat = conv2d(x_image)
los = (y_hat - detect_result) ** 2
los.backward()
conv2d.weight.data()[:] -= 3e-2 * conv2d.weight.grad() # 这里忽略偏差参数,且更新参数不创建新的内存空间
print('epoch %d, loss %.3f' % (i + 1, los.sum().asscalar()))
# 输出学习到的卷积核,可以发现接近上述定义的[[1,-1]]
print(conv2d.weight.data())