1. 二维卷积层
卷积神经网络(convolutional neural network)是含有卷积层(convolutional layer)的神经网络。
本章中,将介绍其中最常见的二维卷积层,包含高和宽两个空间维度,常用来处理图像数据。
本节中,将介绍简单形式的二维卷积层的工作原理。
1.1 two dimentional cross-correlation
1.1.1 概念
虽然卷积层得名于卷积(convolution)运算,但通常在卷积层中使用更加直观的互相关(cross-correlation)运算。
在二维卷积层中,一个二维输入数组和一个二维核(kernel)数组通过互相关运算输出一个二维数组。
二维互相关运算的含义,可以结合下图进行较为直观的理解:
![](https://img-blog.csdnimg.cn/20200614161826915.png)
如上图所示,二维输入数组的高和宽均为3,其形状记为
3
×
3
3 \times 3
3×3或(3,3);
二维核(kernel)数组的高和宽分别为2,其形状记为
2
×
2
2 \times 2
2×2。
其中,核数组在卷积计算中,又称卷积核或过滤器(filter)。
卷积窗口(又称卷积核窗口)的形状,取决于卷积核的高和宽,即
2
×
2
2 \times 2
2×2。
p
×
q
p \times q
p×q卷积或
p
×
q
p \times q
p×q卷积核,说明卷积核的高和宽分别为
p
p
p和
q
q
q。
卷积窗口形状为
p
×
q
p \times q
p×q的卷积层,称为
p
×
q
p \times q
p×q卷积层。
上图中的阴影部分,分别为输入的第一个计算区域、核数组以及对应的输出。有输出的计算公式为:
0
×
0
+
1
×
1
+
3
×
2
+
4
×
3
=
19
0\times0+1\times1+3\times2+4\times3=19
0×0+1×1+3×2+4×3=19
在二维互相关运算中,卷积窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输入数组上滑动。
当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。
上图中的二维输出数组的高和宽分别为2,其中的4个元素由二维互相关运算得出:
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.1.2 代码示例
import tensorflow as tf
print(tf.__version__)
2.0.0
示例1:corr2d
# 二维互相关运算
def corr2d(X, K):
h, w = K.shape
Y = tf.Variable(tf.zeros(shape=(X.shape[0]-h+1, X.shape[1]-w+1)))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
# tf.Variable.assign: Assigns a new value to the variable.
# X[i:i+h, j:j+w]: Left to right, up to down.
Y[i,j].assign(tf.cast(tf.reduce_sum(X[i:i+h, j:j+w] * K), dtype=tf.float32))
return Y
X = tf.constant([[0,1,2],[3,4,5],[6,7,8]])
K = tf.constant([[0,1],[2,3]])
Y = corr2d(X, K)
Y
输出:
<tf.Variable 'Variable:0' shape=(2, 2) dtype=float32, numpy=
array([[19., 25.],
[37., 43.]], dtype=float32)>
示例2:Conv2D
二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。
卷积层的模型参数包括了卷积核和标量偏差。在训练模型时,通常先对卷积核进行随机初始化,然后不断迭代卷积核和偏差。
下面基于上文定义的corr2d
函数,来实现一个自定义的二维卷积层:
class Conv2D(tf.keras.layers.Layer):
def __init__(self, units):
super().__init__()
self.units = units
def build(self, kernel_size):
self.w = self.add_weight(name='w',
shape=kernel_size,
initializer=tf.random_normal_initializer())
self.b = self.add_weight(name='b',
shape=(1,),
initializer=tf.random_normal_initializer())
def call(self, inputs):
return corr2d(inputs, self.w) + self.b
其中,在构造函数__init__
里,声明weight
和bias
这两个模型参数。前向计算函数forward
则是直接调用corr2d
函数再加上偏差。
示例3:edge detection
卷积层的简单应用:检测图像中物体的边缘,即找到像素变化的位置。
a. 构造一张
6
×
8
6\times 8
6×8的图像(即高和宽分别为6像素和8像素):
中间4列为黑(0),其余为白(1)。
X = tf.Variable(tf.ones((6,8)))
X[:,2:6].assign(tf.zeros(X[:,2:6].shape))
X
输出:
<tf.Variable 'Variable:0' shape=(6, 8) dtype=float32, numpy=
array([[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.]], dtype=float32)>
b. 构造一个
1
×
2
1 \times 2
1×2的卷积核K:
当它与输入做互相关运算时,若横向相邻元素相同,输出为0;否则输出为非0。
K = tf.constant([[1,-1]], dtype=tf.float32)
c. 将输入X和设计的卷积核K进行互相关运算。
Y = corr2d(X, K)
Y
输出:
<tf.Variable 'Variable:0' shape=(6, 7) dtype=float32, numpy=
array([[ 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.]], dtype=float32)>
结论:将从白到黑的边缘和从黑到白的边缘分别检测成了1和-1,其余部分的输出全为0。
示例4:learn kernel by data
使用1.3小节中物体边缘检测的输入数据X
和输出数据Y
,来学习构造的核数组K
,简要步骤包括:
先构造一个卷积层,将其卷积核初始化成随机数组。
之后,在每一次迭代中,使用平方误差来比较Y
和卷积层的输出,然后计算梯度来更新权重。
简单起见,这里的卷积层忽略了偏差。
由于corr2d
使用了对单个元素赋值([i, j]=
)的操作,无法自动求梯度,因而使用tf.keras.layers
提供的Conv2D
类来进行实现(而非上文自行构造的Conv2D
类)。
构造二维卷积层:
X = tf.reshape(X, (1,6,8,1))
Y = tf.reshape(Y, (1,6,7,1))
conv2d = tf.keras.layers.Conv2D(filters=1, kernel_size=(1,2))
Y_hat = conv2d(X)
Y_hat.shape
TensorShape([1, 6, 7, 1])
迭代10次:
for i in range(10):
with tf.GradientTape(watch_accessed_variables=False) as tape:
# 仅对权重w进行追踪
tape.watch(conv2d.weights[0])
Y_hat = conv2d(X)
# 损失函数为平方误差
l = (abs(Y_hat - Y)) ** 2
grads = tape.gradient(l, conv2d.weights[0])
# 元素对应相乘
lr = 3e-2
update = tf.multiply(lr, grads)
# 更新权重w
updated_weights = conv2d.get_weights()
updated_weights[0] = conv2d.weights[0] - update
conv2d.set_weights(updated_weights)
if (i + 1)% 2 == 0:
print('batch %d, loss %.3f' % (i + 1, tf.reduce_sum(l)))
batch 2, loss 8.014
batch 4, loss 2.823
batch 6, loss 1.079
batch 8, loss 0.429
batch 10, loss 0.174
查看学习到的核数组(即,权重w):
tf.reshape(conv2d.get_weights()[0],(1,2))
<tf.Tensor: id=1012, shape=(1, 2), dtype=float32, numpy=array([[ 0.94999015, -1.035664 ]], dtype=float32)>
结论:结果与之前定义的核数组K较接近。