动手深度学习note-3(卷积层)

从全连接层Fully connected layers到卷积Convolution

  • 对全连接层使用平移不变性局部性可以得到卷积层

    index

回忆一下全连接层

  1. 回到最开始的地方:

    还是以一个28x28像素的图片黑白为例,图片可以表示为一个28x28的矩阵,矩阵的每个元素的取值介于0~255之间,将矩阵展平并转置,得到了一个包含784行的列向量

  2. 现在构建10个神经元,每个神经元与784个输入连接,构成全连接神经网络

    全连接层

  3. 学习过程可做如下解释:

    该网络可学习的权重矩阵为V,包含两个维度\(v_i\)\(v_j\),即 \(i\)\(j\) 列,输入的第\(j\)个元素\(x_j\)与第\(j\)个权重\(v_j\)分别相乘并求和得到输入经过任意一个神经元后的输出,将该输出与第 \(i\) 个神经元的权重\(v_i\)相乘,得到经过所有神经元后输入\(x_j\)对应的输出\(h_j\) \[ h_j=\sum_jv_{i,j}x_j \]

  4. 反思该过程,由于输入像素矩阵被展开为了一个一维向量,损失了部分相对位置信息,存在不足

  5. 现在将输入信息以一个二维的张量进行储存,那么推广可知对应的可学习的权重矩阵应该为一个四维的张量\(v_{i,j,k,l}\),对应的输出张量为\(|\mathbf{H}|_{i,j}\) \[ \mathbf{H}_{i, j}=\mathbf{U}_{i, j} + \sum_k \sum_l\mathsf{W}_{i, j, k, l} \mathbf{X}_{k, l}\tag{1} \]

  6. 为了引出卷积,将下标进行一下代换 \[ \mathbf{H}_{i,j}=\mathbf{U}_{i, j} + \sum_a \sum_b [\mathbf{V}]_{i, j, a, b} [\mathbf{X}]_{i+a, j+b} \] 其中,从\(\mathsf{W}\)\(\mathsf{V}\)的转换只是形式上的转换,因为在这两个四阶张量的元素之间存在一一对应的关系。我们只需重新索引下标\((k, l)\),使\(k = i+a\)\(l = j+b\),由此可得\([\mathsf{V}]_{i, j, a, b} = [\mathsf{W}]_{i, j, i+a, j+b}\)。索引\(a\)\(b\)通过在正偏移和负偏移之间移动覆盖了整个图像。对于隐藏表示中任意给定位置(\(i\),\(j\))处的像素值\([\mathbf{H}]*_{i, j}\),可以通过在\(x\)中以\((i, j)\)为中心对像素进行加权求和得到,加权使用的权重为\([\mathsf{V}]_{i, j, a, b}\)

过渡到卷积

  • 引入两个基本概念
    1. 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。
    2. 局部性(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。

应用平移不变性:

重新来考虑一下\(|\mathbf{V}|_{i,j,a,b}\),我的目的是提取特定的信息,因此我需要检测对象在输入\(\mathbf{X}\)中的平移,应该仅导致输出表示\(\mathbf{H}\)中的平移。也就是说,权重矩阵\(\mathsf{V}\)实际上不依赖于\((i, j)\)的值,即\([\mathbf{V}]_{i, j, a, b} = [\mathbf{V}]_{a, b}\)。我们可以简化\(\mathbf{H}\)定义为:(\(u\)为偏置项) \[ [\mathbf{H}]_{i, j} = u + \sum_a\sum_b [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b} \]

  • 应用到卷积层中,即卷积核共享权重矩阵

应用局部性

首先,之所以需要应用卷积,其中一个非常重要的原因就是全连接神经网络在提取信息时需要很多的参数,效率不高,会带来很多的内存和计算开销。而通过卷积,我可以先提取局部的信息,将局部的信息传给下一层的神经网络,经过提取,获得更加抽象的信息,从而具备识别图像整体的能力\(_{所谓整体与部分,整体是由部分构成的}\)

因此,我在进行这种特殊的全连接时,我只需要“全连接”相当小的一部分元素,故而对\(|\mathbf{H}|\)的定义做出如下限制,使得每次的“全连接”仅仅只提取当前区域的邻域\(\Delta\)的信息 \[ [\mathbf{H}]_{i, j} = u + \sum_{a = -\Delta}^{\Delta} \sum_{b = -\Delta}^{\Delta} [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b} \] 这样,就实现了从全连接层到卷积层的过渡

卷积层

卷积过程 卷积可视化

代码实现

卷积实现

1
2
3
import tensorflow as tf
import matplotlib.pyplot as plt
%matplotlib inline
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def coor2d(X, K):  # X为输入,K为卷积核
h, w = K.shape

Y = tf.Variable(
tf.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].assign(
tf.reduce_sum(X[i:i+h, j: j+w]*K)
)

return Y

测试

例1:本文可视化图像

1
2
3
X = tf.constant([ [3.,3.,2.,1.,0.], [0.,0.,1.,3.,1.], [3.,1.,2.,2.,3.], [2.,0.,0.,2.,2.], [2.,0.,0.,0.,1.] ])
K = tf.constant([ [0.,1.,2.],[2.,2.,0.],[0.,1.,2.] ])
coor2d(X, K)

输出:

1
2
3
4
<tf.Variable 'Variable:0' shape=(3, 3) dtype=float32, numpy=
array([[12., 12., 17.],
[10., 17., 19.],
[ 9., 6., 14.]], dtype=float32)>

例2:边缘检测

1
2
3
4
5
X = tf.Variable(tf.ones((8,8)))
X[:,2:6].assign(tf.zeros(X[:,2:6].shape))
K = tf.constant([ [1.,-1.] ])
Y = coor2d(X, K)
Y

输出:

1
2
3
4
5
6
7
8
9
<tf.Variable 'Variable:0' shape=(8, 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.],
[ 0., 1., 0., 0., 0., -1., 0.],
[ 0., 1., 0., 0., 0., -1., 0.]], dtype=float32)>

假设1为黑色,0为白色在水平方向上,输出为1的部分颜色由黑色变为了白色,输出为-1的地方颜色由白色变成了黑色,从而完成了水平方向的边界检测

卷积层实现

1
2
3
4
5
6
7
8
9
10
11
class Conv2D(tf.keras.layers.Layer):
def _init_(self):
super()._init_()

def build(self, kernel_size):
initializer = tf.random_normal_initializer()
self.weight = self.add_weight(name='w', shape=kernel_size, initializer=initializer)
self.bias = self.add_weight(name='b', shape=(1, ),initializer=initializer)

def call(self, inputs):
return coor2d(inputs, self.weight) + self.bias

学习卷积核参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
conv2d = tf.keras.layers.Conv2D(1, (1,2), use_bias=False)
X = tf.reshape(X,(1,6,8,1)) # reshape为一个张量,形状为(6,8),一个通道(黑白颜色通道)
Y = tf.reshape(Y,(1,6,7,1))
lr = 0.03

Y_hat = conv2d(X)
l_history = []
for i in range(10):
with tf.GradientTape(watch_accessed_variables=False) as g:
g.watch(conv2d.weights[0])
Y_hat = conv2d(X)
l = (abs(Y_hat-Y))**2
# 梯度下降
update = tf.multiply(lr, g.gradient(l, conv2d.weights[0])) # 学习率乘以l相对于权重的梯度得到步长
weights = conv2d.get_weights()
weights[0] = conv2d.weights[0] - update
conv2d.set_weights(weights)
l_history.append(tf.reduce_sum(l))
print(f'epoch {i+1}, loss {tf.reduce_sum(l):.3f}')

epoch = [x + 1 for x in range(10)]
plt.plot(epoch, l_history)
plt.xlabel('epoch')
plt.ylabel('loss')

输出:

1
2
3
4
5
6
7
8
9
10
11
epoch 1, loss 30.012
epoch 2, loss 14.123
epoch 3, loss 6.956
epoch 4, loss 3.599
epoch 5, loss 1.954
epoch 6, loss 1.107
epoch 7, loss 0.650
epoch 8, loss 0.392
epoch 9, loss 0.241
epoch 10, loss 0.150
<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[ 1.0234355, -0.9452952]], dtype=float32)>

loss

填充Padding

推导

重新回忆卷积的输入与输出:

假设给定的输入为一个\((n_h \times n_w)\)的矩阵,使用了\((k_h \times k_w)\)的卷积核,那么,对应的输出矩阵为\((n_h - k_h + 1 \times n_w - k_w +1)\)

对输入矩阵进行填充:

现在,对于深度学习,为了使模型做到更深的层数,我们对边缘进行填充,假设行数填充\(p_h\),列数填充\(p_w\),那么,对应的输出应该为一个\((n_h - k_h +p_h + 1 \times n_w - k_w + p_w +1)\)的矩阵

现在,令\(p_h = k_h - 1\)\(p_w = k_w - 1\),那么,输入矩阵经过卷积操作后与输出矩阵的形状相同,这样可以搭建根据需要任意层的卷积层,如下图:

填充可视化

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import tensorflow as tf

def comp_conv2d(conv2d, X):
X = tf.reshape(X, (1, ) + X.shape + (1, )) # X的形状由(h, w)变为(1, h, w, 1)
Y = conv2d(X)
return tf.reshape(Y, Y.shape[1:3]) # 忽略批量大小和通道的输出返回

conv2d = tf.keras.layers.Conv2D(1, kernel_size=3, padding='same')

'''
"valid":不进行填充。
"same":使用零填充来确保输出形状与输入形状相同。
"causal":只在输入序列的左侧进行填充,常用于因果卷积。
'''

X = tf.random.uniform(shape=(8,8))
comp_conv2d(conv2d, X).shape

输出:

1
TensorShape([8, 8])

步幅Strides

strides

推导

还是一个输入为\(n_h \times n_w\)的矩阵,设置卷积核的形状为\(k_h \times k_w\),设置矩阵沿着列方向上的步幅为\(s_h\),设置矩阵沿着行方向上的矩阵为\(s_w\),这样对应输出矩阵的形状为: \[ \lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor \] 同样,令\(p_h = k_h - 1\)\(p_w = k_w - 1\),输出矩阵的形状可简化为: \[ \lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor \] 进一步简化,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为: \[ \lfloor (n_h/s_h) \rfloor \times \lfloor (n_w/s_w) \rfloor \]

代码实现

使用上期代码:

1
2
3
4
5
6
7
8
import tensorflow as tf

def comp_conv2d(conv2d, X):
X = tf.reshape(X, (1, ) + X.shape + (1, )) # X的形状由(h, w)变为(1, h, w, 1)
Y = conv2d(X)
return tf.reshape(Y, Y.shape[1:3]) # 忽略批量大小和通道的输出返回

X = tf.random.uniform(shape=(8,8))
1
2
conv2d = tf.keras.layers.Conv2D(1, kernel_size=3, padding='same', strides=2)
comp_conv2d(conv2d, X).shape

输出:

1
TensorShape([4, 4])

多通道Channels

多输入通道

原理

  • 包含多通道的信息,例如:在图片识别中包含3个通道的输入表示图片的颜色信息

假设通道数为\(c_i\),那么,输入矩阵的形状为: \[ c_i \times n_h \times n_w \] 卷积核的形状为: \[ c_i \times k_h \times k_w \] 经过计算,在对\(c_i\)个通道学到的信息进行加和,从而便识别到了多通道信息

conv-multi-in.svg

代码实现

使用之前的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import tensorflow as tf

def coor2d(X, K): # X为输入,K为卷积核
h, w = K.shape

Y = tf.Variable(
tf.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].assign(
tf.reduce_sum(X[i:i+h, j: j+w]*K)
)

return Y
1
2
def corr2d_multi_in(X, K):
return tf.reduce_sum([coor2d(x, k) for x, k in zip(X, K)],axis=0)

验证输出:

1
2
3
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[ 56., 72.],
[104., 120.]], dtype=float32)>

多输出通道

multi_channel_in_out

原理

  • 回到关于卷积核的介绍:

    调整不同的卷积核的权重参数可以得到不同性质特征的提取,为了使提取的特征更多提取到更多性质的特征,对卷积实现多通道的输出

    例:图片识别中,一个维度用于学习纹理,一个维度用于学习图像边缘信息

现在,假设输出的通道数为\(c_o\)

卷积核的性质为: \[ c_o\times c_i\times k_h\times k_w \]

代码实现

1
2
def corr2d_multi_in_out(X, K):
return tf.stack([corr2d_multi_in(X, k) for k in K], axis=0)
1
2
K = tf.stack((K, K + 1, K + 2), 0)
corr2d_multi_in_out(X, K)

验证输出:

1
2
3
4
5
6
7
8
9
<tf.Tensor: shape=(3, 2, 2), dtype=float32, numpy=
array([[[ 56., 72.],
[104., 120.]],

[[ 76., 100.],
[148., 172.]],

[[ 96., 128.],
[192., 224.]]], dtype=float32)>

总结

  • 卷积输入\(\mathbf{X}\)\(c_i \times n_h \times n_w\)
  • 卷积核\(\mathbf{W}\)\(c_o\times c_i\times k_h\times k_w\)
  • 偏置项\(\mathbf{B}\)\(c_o \times c_i\)
  • 输出\(\mathbf{Y}\)\(c_o \times m_h \times m_w\)

池化层

  • 对于现实中要识别的图片,图片的像素特征可能存在一定的抖动,而卷积层对像素值的位置信息过于敏感,因此引入池化层

  • 池化层与卷积层的实现类似,同样具有相同的输入、输出以及核,只不过,池化层的核参数不可学习,主要运用最大池化和平均池化两种方法

    pooling
  • 池化层也具有卷积层类似的填充、步幅

  • 池化层的输出性质与卷积层相同,只不过池化层对输出不进行加权求和,将求和操作传入到下一层的卷积中进行


动手深度学习note-3(卷积层)
https://blog.potential.icu/2024/01/31/2024-1-31-动手深度学习note-3(卷积层)/
Author
Xt-Zhu
Posted on
January 31, 2024
Licensed under