【TensorFlow深度学习】十三、卷积神经网络(卷积层、池化层、LeNet、d2l.train_ch6)

本专栏是记录作者学习TensorFlow深度学习的相关内容,参考教材:《动手学深度学习》第二版

前面的章节我们通过多层感知机模型实现了对Fashion-MNIST数据集的分类,我们的思路是将28*28的图像展平,然后使用全连接层进行处理。而现在,我们可以使用卷积层的方法,更好的学习到图像的空间结构,并且模型更加简单,所需参数更少。

本节介绍了卷积层与池化层运算的过程,以LeNet-5为例实践了卷积网络在Fashion-MINIST分类任务中的应用,并且详细分析了训练过程。

本节的 Jupyter 笔记本文件已上传至gitee以供大家学习交流:我的gitee仓库

1 图像卷积

二维卷积层的核心计算是二维互相关运算。最简单的形式是,对二维输入数据和卷积核执行互相关操作,然后添加一个偏置。卷积核用于提取图像的空间的信息,例如可以用卷积核来检测图像的边缘,另外卷积核是可学习的。

1.1 互相关运算

在卷积层中,输入张量和核张量通过互相关运算产生输出张量。

二维互相关运算。阴影部分是第一个输出元素,以及用于计算输出的输入张量元素和核张量元素

image.png

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 2 2、宽度为 2 2 2,如下所示:

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.

输出大小等于输入大小 n h × n w n_h \times n_w nh×nw减去卷积核大小 k h × k w k_h \times k_w kh×kw,即:

( n h − k h + 1 ) × ( n w − k w + 1 ) . (n_h-k_h+1) \times (n_w-k_w+1). (nhkh+1)×(nwkw+1).

互相关运算的实现:

import tensorflow as tf
from d2l import tensorflow as d2l


def corr2d(X, K):
    """计算二维互相关运算"""
    h, w = K.shape  # 获取K的形状,分别为高度h和宽度w
    Y = tf.Variable(tf.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)))  # 创建一个变量Y,形状为(X的行数 - h + 1, X的列数 - w + 1)的全零矩阵
    for i in range(Y.shape[0]):  # 遍历Y的行数
        for j in range(Y.shape[1]):  # 遍历Y的列数
            Y[i, j].assign(tf.reduce_sum(  # 将Y[i, j]的值赋为张量的元素相乘后的和
                X[i: i + h, j: j + w] * K))  # 从X中取出形状为(h, w)的子矩阵,与K相乘后求和

    return Y
# 定义输入张量X
X = tf.constant([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])

# 定义卷积核K
K = tf.constant([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)

结果:

<tf.Variable 'Variable:0' shape=(2, 2) dtype=float32, numpy=
array([[19., 25.],
       [37., 43.]], dtype=float32)>

1.2 卷积层

卷积层对输入与卷积核进行互相关运算,再加上偏置产生输出。和全连接层类似,卷积层中待训练的参数是卷积核和偏置。卷积核大小作为超参数。

我们使用上一章“自定义层”的方式自定义卷积层,通过自定义卷积层了解卷积层的执行过程。

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 corr2d(inputs, self.weight) + self.bias  # 执行二维卷积运算并在结果上加上偏置

Conv2D是一个继承自tf.keras.layers.Layer的类。它表示一个二维卷积层。
build方法用于构建卷积层的一些成员变量,在该方法中,使用了tf.random_normal_initializer作为初始化器,创建了一个形状为kernel_size的权重weight,以及一个形状为(1, )的偏置bias
call方法用于执行卷积运算,它接受一个输入inputs,并将输入与权重进行卷积运算,然后将结果与偏置相加,最终得到卷积层的输出。

1.3 卷积层的简单应用:边缘检测

构建一个黑白图像,0表示黑色,1表示白色

X = tf.Variable(tf.ones((10, 10)))
X[:, 3:7].assign(tf.zeros(X[:, 3:7].shape))

X

结果:

<tf.Variable 'Variable:0' shape=(10, 10) dtype=float32, numpy=
array([[1., 1., 1., 0., 0., 0., 0., 1., 1., 1.],
       [1., 1., 1., 0., 0., 0., 0., 1., 1., 1.],
       [1., 1., 1., 0., 0., 0., 0., 1., 1., 1.],
       [1., 1., 1., 0., 0., 0., 0., 1., 1., 1.],
       [1., 1., 1., 0., 0., 0., 0., 1., 1., 1.],
       [1., 1., 1., 0., 0., 0., 0., 1., 1., 1.],
       [1., 1., 1., 0., 0., 0., 0., 1., 1., 1.],
       [1., 1., 1., 0., 0., 0., 0., 1., 1., 1.],
       [1., 1., 1., 0., 0., 0., 0., 1., 1., 1.],
       [1., 1., 1., 0., 0., 0., 0., 1., 1., 1.]], dtype=float32)>

我们尝试使用一个高度为1,宽度为2的卷积核通过互相关计算进行边缘检测。

K = tf.constant([[1.0, -1.0]])
Y = corr2d(X, K)
Y

结果:

<tf.Variable 'Variable:0' shape=(10, 9) dtype=float32, numpy=
array([[ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.]], dtype=float32)>

输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘,其他情况的输出为0。所以其实学习到的卷积核可以获取图像的各种特征。

1.4 学习卷积核

我们可以通过比较卷积核的输出与我们期望输出的平方误差对卷积核进行训练,**注意我们只学习卷积核!**以下是对学习过程的手工实现,可以看到训练细节:

# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = tf.keras.layers.Conv2D(1, (1, 2), use_bias=False)

# 这个二维卷积层使用四维输入和输出格式(批量大小、高度、宽度、通道),
# 其中批量大小和通道数都为1
X = tf.reshape(X, (1, 10, 10, 1))
Y = tf.reshape(Y, (1, 10, 9, 1))
lr = 9e-3  # 学习率

# 使用卷积层对输入进行卷积操作
Y_hat = conv2d(X)

# 循环迭代20次
for i in range(20):
    # 创建GradientTape对象,设置不监听访问的变量
    with tf.GradientTape(watch_accessed_variables=False) as g:
        # 注册卷积核权重为可监听的变量,梯度带只会追踪卷积核权重的操作,而不会追踪其他变量的操作,例如偏置。
        g.watch(conv2d.weights[0])
        
        # 更新输入
        Y_hat = conv2d(X)
        
        # 计算损失函数,使用绝对值差的平方作为损失函数
        l = (abs(Y_hat - Y)) ** 2
        
        # 根据损失函数计算梯度
        # g.gradient(l, conv2d.weights[0]):这部分计算损失函数 l 相对于卷积核权重 conv2d.weights[0] 的梯度
        # tf.multiply(lr, ...):这部分将梯度与学习率 lr 相乘。
        # 最终得到的 update 是一个与梯度相同形状的张量,它表示了在梯度下降中应用的更新步骤。这个张量将被用于更新卷积核权重。
        update = tf.multiply(lr, g.gradient(l, conv2d.weights[0]))
        
        # 获取卷积层的权重
        weights = conv2d.get_weights()
        
        # 更新卷积核权重,(conv2d.weights[0])是卷积层中卷积核的权重。 
        # (conv2d.weights[1])是卷积层中的偏置项。
        weights[0] = conv2d.weights[0] - update
        
        # 设置卷积层的权重
        conv2d.set_weights(weights)
        
        # 每两次迭代输出一次训练结果
        if (i + 1) % 2 == 0:
            print(f'epoch {i+1}, loss {tf.reduce_sum(l):.3f}')

结果:

epoch 2, loss 22.822
epoch 4, loss 5.192
epoch 6, loss 1.590
epoch 8, loss 0.607
epoch 10, loss 0.258
epoch 12, loss 0.114
epoch 14, loss 0.051
epoch 16, loss 0.023
epoch 18, loss 0.010
epoch 20, loss 0.005

这样我们就训练出一个可以检测纵向边缘的卷积核

tf.reshape(conv2d.get_weights()[0], (1, 2))

结果:

<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[ 0.98743683, -0.98736733]], dtype=float32)>

2 填充与步幅

一个 240 × 240 240 \times 240 240×240像素的图像,经过 10 10 10 5 × 5 5 \times 5 5×5的卷积后,会减少到 200 × 200 200 \times 200 200×200像素。填充可以增加输出的高度和宽度,不使用填充(padding)可能导致输入特征图的边缘信息在卷积过程中逐渐丢失。步幅可以减小输出的高和宽,填充和步幅可用于有效地调整数据的维度

2.1 填充

填充是指在输入图像周围添加额外的像素值(通常为0)的操作。

通过填充,可以确保边缘的信息也能够参与卷积操作,有助于保留更多的空间信息。在需要的情况下,填充允许在输入特征图的周围添加像素,从而保持输出特征图的尺寸与输入特征图相同。

例如将 3 × 3 3 \times 3 3×3输入填充到 5 × 5 5 \times 5 5×5,那么它的输出就增加为 4 × 4 4 \times 4 4×4

image.png

通常,如果我们添加 p h p_h ph行填充(大约一半在顶部,一半在底部)和 p w p_w pw列填充(左侧大约一半,右侧一半),则输出形状将为

( n h − k h + p h + 1 ) × ( n w − k w + p w + 1 ) (n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1) (nhkh+ph+1)×(nwkw+pw+1)

实现上述过程,tf.keras.layers.Conv2D定义了一个卷积层conv2d,填充方式为same,表示使用“相同”填充,即在输入的每一侧都添加足够的零元素,使得卷积的输出与输入的形状相同

import tensorflow as tf
def comp_conv2d(conv2d, X):
    # 将输入张量X的形状重塑为(1, height, width, 1)
    X = tf.reshape(X, (1, ) + X.shape + (1, ))
    # 使用卷积层conv2d对输入进行卷积操作
    Y = conv2d(X)
    # 将输出张量Y的形状重塑为(height, width)
    return tf.reshape(Y, Y.shape[1:3])

# 创建一个包含一个过滤器的2D卷积层,卷积核大小为2x2,填充方式为'same'
conv2d = tf.keras.layers.Conv2D(1, kernel_size=2, padding='same')
# 生成一个形状为(4, 4)的随机张量X
X = tf.random.uniform(shape=(4, 4))
# 调用comp_conv2d函数,计算卷积结果
comp_conv2d(conv2d, X)

结果:

<tf.Tensor: shape=(4, 4), dtype=float32, numpy=
array([[-0.1953258 , -0.10850125, -0.11667946, -0.14603293],
       [-0.31800175, -0.10726026, -0.19865991, -0.16246773],
       [-0.3028457 , -0.15715508, -0.11268472, -0.22134446],
       [-0.11780988, -0.00540953, -0.1532639 , -0.15509766]],
      dtype=float32)>

如果卷积核高和宽不一样,那其会填充不一样的高宽,使得输出与输入形状一致

conv2d = tf.keras.layers.Conv2D(1, kernel_size=(1, 2), padding='same')
comp_conv2d(conv2d, X)

结果:

<tf.Tensor: shape=(4, 4), dtype=float32, numpy=
array([[-0.05135923, -0.06143384,  0.02822246, -0.10295077],
       [-0.1698699 ,  0.06940853, -0.12047672, -0.09347672],
       [-0.15873334, -0.12132923,  0.05193967, -0.15010415],
       [-0.10876597,  0.09020445, -0.06148071, -0.1546886 ]],
      dtype=float32)>

2.2 步幅

步幅(stride)是卷积神经网络中卷积核在输入数据上滑动的间隔大小。在卷积操作中,步幅的选择影响了输出特征图的尺寸。

下图是垂直步幅为 3 3 3,水平步幅为 2 2 2的二维互相关运算。着色部分是输出元素以及用于输出计算的输入和内核张量元素:
0 × 0 + 0 × 1 + 1 × 2 + 2 × 3 = 8 0\times0+0\times1+1\times2+2\times3=8 0×0+0×1+1×2+2×3=8

0 × 0 + 6 × 1 + 0 × 2 + 0 × 3 = 6 0\times0+6\times1+0\times2+0\times3=6 0×0+6×1+0×2+0×3=6

image.png

通常,当垂直步幅为 s h s_h sh、水平步幅为 s w s_w sw时,输出形状为

⌊ ( n h − k h + p h + s h ) / s h ⌋ × ⌊ ( n w − k w + p w + s w ) / 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. ⌊(nhkh+ph+sh)/sh×⌊(nwkw+pw+sw)/sw.

如果我们设置了 p h = k h − 1 p_h=k_h-1 ph=kh1 p w = k w − 1 p_w=k_w-1 pw=kw1,则输出形状将简化为 ⌊ ( n h + s h − 1 ) / s h ⌋ × ⌊ ( n w + s w − 1 ) / s w ⌋ \lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor ⌊(nh+sh1)/sh×⌊(nw+sw1)/sw
更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为 ( n h / s h ) × ( n w / s w ) (n_h/s_h) \times (n_w/s_w) (nh/sh)×(nw/sw)

conv2d = tf.keras.layers.Conv2D(1, kernel_size=2, padding='same', strides=2)
comp_conv2d(conv2d, X)

结果:

<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[ 0.06340596,  0.14556159],
       [-0.74849415, -0.0572098 ]], dtype=float32)>

对于垂直步幅和水平步幅可以不同

conv2d = tf.keras.layers.Conv2D(1, kernel_size=(2,1), padding='valid',
                                strides=(2, 1))
comp_conv2d(conv2d, X).shape

结果:

TensorShape([2, 4])

总之,填充和步幅可用于有效地调整数据的维度。

3 输入与输出通道

3.1 输入通道

彩色图像可能有RGB三个通道,例如200*200的图像会表示会3*200*200的张量

**每一个通道都有一个卷积核,每个通道与卷积核进行卷积操作后,所有通道的卷积结果的和就是输出。**阴影部分是第一个输出元素以及用于计算这个输出的输入和核张量元素: ( 1 × 1 + 2 × 2 + 4 × 3 + 5 × 4 ) + ( 0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 ) = 56 (1\times1+2\times2+4\times3+5\times4)+(0\times0+1\times1+3\times2+4\times3)=56 (1×1+2×2+4×3+5×4)+(0×0+1×1+3×2+4×3)=56

image.png

整个过程的代码如下,输出为单通道

import tensorflow as tf
from d2l import tensorflow as d2l


def corr2d_multi_in(X, K):
    """
    对多个输入的二维互相关函数进行计算

    参数:
    X:输入的二维张量列表
    K:卷积核的二维张量

    返回值:
    一个二维张量,表示多个输入的二维互相关函数的结果
    """

    # 使用 tf.reduce_sum 函数在列表中的所有互相关结果上沿着通道维度(axis=0)进行求和。
    # 调用 d2l.corr2d 函数进行二维互相关计算。
    return tf.reduce_sum([d2l.corr2d(x, k) for x, k in zip(X, K)], axis=0)

X = tf.constant([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
               [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = tf.constant([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])

corr2d_multi_in(X, K)

结果:

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

3.2 输出通道

上面的输出通道为单输出通道,同样,输出通道也可以变为多个输出通道,此时卷积核形状变为4维。每个输出通道可以识别特定的模式,可以匹配不同的模式,识别不同的特征(输出通道是卷积层的超参数。)

c i c_i ci c o c_o co分别表示输入和输出通道的数目,并让 k h k_h kh k w k_w kw为卷积核的高度和宽度。参数的shape如下:

  • 输入 X : c i × k h × k w X:c_i\times k_h\times k_w X:ci×kh×kw
  • W : c o × c i × k h × k w W:c_o\times c_i\times k_h\times k_w W:co×ci×kh×kw
  • 偏差 B : c o × c i B:c_o\times c_i B:co×ci
  • 输出 Y : c o × m h × m w Y:c_o\times m_h\times m_w Y:co×mh×mw
def corr2d_multi_in_out(X, K):
    # 对输入的张量`X`和卷积核`K`执行多输入多输出的二维互相关运算。
    # 遍历卷积核`K`的第0个维度,每次都对输入`X`执行互相关运算。
    # 最后将所有结果都叠加在一起,并用`tf.stack`函数将结果组合为一个张量。
    return tf.stack([corr2d_multi_in(X, k) for k in K], 0)
X = tf.constant([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
               [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = tf.constant([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]]) #单通道卷积核

print(K.shape)
K = tf.stack((K, K + 1, K + 2), 0) #构造具有三个输出通道的卷积核
print(K.shape)

结果:卷积核的形状变化

(2, 2, 2)
(3, 2, 2, 2)

进行卷积运算

corr2d_multi_in_out(X, K)

结果:

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

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

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

总结:

  • 每个输入通道有独立的二维卷积核,所有通道结果相加得到一个输出通道的结果。
  • 每个输出通道对应一个三维的卷积核。
  • 每个输出对应一个四维的卷积核。

3.3 1*1卷积层

卷积核输出是对应输入通道的加权和。不做空间的匹配,等价于一个全连接层,为网络做reshape。

1 × 1 1\times 1 1×1卷积层通常用于调整网络层的通道数量和控制模型复杂性。

image.png

所以1*1卷积核可以通过全连接层实现

def corr2d_multi_in_out_1x1(X, K):
    """
    对输入数据进行多输入多输出的1x1二维互相关运算。
    
    参数:
    X:输入数据,形状为 (c_i, h, w)。
    K:卷积核,形状为 (c_o, c_i)。
    
    返回:
    相关运算结果,形状为 (c_o, h, w)。
    """
    c_i, h, w = X.shape
    c_o = K.shape[0]
    X = tf.reshape(X, (c_i, h * w))
    K = tf.reshape(K, (c_o, c_i))
    # 全连接层中的矩阵乘法
    Y = tf.matmul(K, X)
    return tf.reshape(Y, (c_o, h, w))

当执行 1 × 1 1\times 1 1×1卷积运算时,上述函数相当于先前实现的互相关函数corr2d_multi_in_out

X = tf.random.normal((3, 3, 3), 0, 1)
K = tf.random.normal((2, 3, 1, 1), 0, 1)
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
float(tf.reduce_sum(tf.abs(Y1 - Y2))) < 1e-2

结果:Y1与Y2的差距很小

True

4 池化层

卷积层运算对位置非常敏感,例如垂直边缘检测。我们需要池化层缓解卷积层对位置的敏感性。最大池化层会输出该窗口内的最大值,平均池化层会输出该窗口内的平均值。池化层的填充和步幅可以人工指定。

4.1 最大池化层和平均池化层

最大池化层:返回滑动窗口中的最大值。平均池化层:返回滑动窗口中的平均值。

池化层特点:没有可学习的参数。不会改变通道数,输入通道数=输出通道数。

超参数:窗口大小、填充、步幅

例子:着色部分是第一个输出元素,以及用于计算这个输出的输入元素: max ⁡ ( 0 , 1 , 3 , 4 ) = 4 \max(0, 1, 3, 4)=4 max(0,1,3,4)=4

image.png

汇聚层的前向传播过程:

import tensorflow as tf


def pool2d(X, pool_size, mode='max'):
    """
    输入:
    X: 输入的二维张量,shape为(X.shape[0], X.shape[1])
    pool_size: 池化窗口的大小,为一个包含两个元素的元组(p_h, p_w)
    mode: 池化方式,可选值为'max'或'avg',默认为'max'

    输出:
    Y: 池化后的二维张量,shape为(Y.shape[0], Y.shape[1])

    """
    p_h, p_w = pool_size
    Y = tf.Variable(tf.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w +1)))
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            if mode == 'max':
                Y[i, j].assign(tf.reduce_max(X[i: i + p_h, j: j + p_w]))
            elif mode =='avg':
                Y[i, j].assign(tf.reduce_mean(X[i: i + p_h, j: j + p_w]))
    return Y

我们使用1.3 边缘检测的输出Y作为池化层输入

X = tf.Variable(tf.ones((10, 10)))
X[:, 3:7].assign(tf.zeros(X[:, 3:7].shape))
K = tf.constant([[1.0, -1.0]])
Y = corr2d(X, K)
Y

结果:输出检测结果

<tf.Variable 'Variable:0' shape=(10, 9) dtype=float32, numpy=
array([[ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.,  0., -1.,  0.,  0.]], dtype=float32)>

平均池化

pool2d(Y, (2, 2), 'avg')

结果:

<tf.Variable 'Variable:0' shape=(9, 8) dtype=float32, numpy=
array([[ 0. ,  0.5,  0.5,  0. ,  0. , -0.5, -0.5,  0. ],
       [ 0. ,  0.5,  0.5,  0. ,  0. , -0.5, -0.5,  0. ],
       [ 0. ,  0.5,  0.5,  0. ,  0. , -0.5, -0.5,  0. ],
       [ 0. ,  0.5,  0.5,  0. ,  0. , -0.5, -0.5,  0. ],
       [ 0. ,  0.5,  0.5,  0. ,  0. , -0.5, -0.5,  0. ],
       [ 0. ,  0.5,  0.5,  0. ,  0. , -0.5, -0.5,  0. ],
       [ 0. ,  0.5,  0.5,  0. ,  0. , -0.5, -0.5,  0. ],
       [ 0. ,  0.5,  0.5,  0. ,  0. , -0.5, -0.5,  0. ],
       [ 0. ,  0.5,  0.5,  0. ,  0. , -0.5, -0.5,  0. ]], dtype=float32)>

最大池化

pool2d(Y, (2, 2), 'max')

结果:

<tf.Variable 'Variable:0' shape=(9, 8) dtype=float32, numpy=
array([[0., 1., 1., 0., 0., 0., 0., 0.],
       [0., 1., 1., 0., 0., 0., 0., 0.],
       [0., 1., 1., 0., 0., 0., 0., 0.],
       [0., 1., 1., 0., 0., 0., 0., 0.],
       [0., 1., 1., 0., 0., 0., 0., 0.],
       [0., 1., 1., 0., 0., 0., 0., 0.],
       [0., 1., 1., 0., 0., 0., 0., 0.],
       [0., 1., 1., 0., 0., 0., 0., 0.],
       [0., 1., 1., 0., 0., 0., 0., 0.]], dtype=float32)>

由此看出,池化层缓解了卷积层对位置的敏感性。

4.2 填充与步幅

TensorFlow中的输入,最后一个维度是通道,第一个维度是样本。

X = tf.reshape(tf.range(16, dtype=tf.float32), (1, 4, 4, 1))
X

结果:

<tf.Tensor: shape=(1, 4, 4, 1), dtype=float32, numpy=
array([[[[ 0.],
         [ 1.],
         [ 2.],
         [ 3.]],

        [[ 4.],
         [ 5.],
         [ 6.],
         [ 7.]],

        [[ 8.],
         [ 9.],
         [10.],
         [11.]],

        [[12.],
         [13.],
         [14.],
         [15.]]]], dtype=float32)>

默认情况下,TensorFlow框架中的步幅与汇聚窗口的大小相同

因此,如果使用形状为(3, 3)的汇聚窗口,那么默认情况下,我们得到的步幅形状为(3, 3)

pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3])
pool2d(X)

结果:

<tf.Tensor: shape=(1, 1, 1, 1), dtype=float32, numpy=array([[[[10.]]]], dtype=float32)>

得到4个维度均为1的张量,值为10。

填充和步幅可以手动设定:
tf.constant
[0, 0]表示在第一个维度(batch 维度)上不进行填充,
[1, 0]表示在第二个维度上的前面填充一个元素,
[1, 0]表示在第三个维度上的前面填充一个元素,
[0, 0]表示在最后一个维度(通道维度)上不进行填充。

tf.padCONSTANT是填充模式,表示使用常数值填充。

即在输入张量的高和宽的两侧各填充一个零

# 定义填充常量paddings
paddings = tf.constant([[0, 0], [1,0], [1,0], [0,0]])  
# 对输入张量X进行填充,填充常量为paddings
X_padded = tf.pad(X, paddings, "CONSTANT")  
X_padded

结果:

<tf.Tensor: shape=(1, 5, 5, 1), dtype=float32, numpy=
array([[[[ 0.],
         [ 0.],
         [ 0.],
         [ 0.],
         [ 0.]],

        [[ 0.],
         [ 0.],
         [ 1.],
         [ 2.],
         [ 3.]],

        [[ 0.],
         [ 4.],
         [ 5.],
         [ 6.],
         [ 7.]],

        [[ 0.],
         [ 8.],
         [ 9.],
         [10.],
         [11.]],

        [[ 0.],
         [12.],
         [13.],
         [14.],
         [15.]]]], dtype=float32)>

配置池化层

# 创建最大池化层,池化窗口大小为3x3,不填充边界,步长为2
pool2d = tf.keras.layers.MaxPool2D(pool_size=[3, 3], padding='valid', strides=2)  
# 对填充后的张量X进行最大池化操作
pool2d(X_padded)  

结果

<tf.Tensor: shape=(1, 2, 2, 1), dtype=float32, numpy=
array([[[[ 5.],
         [ 7.]],

        [[13.],
         [15.]]]], dtype=float32)>

4.3 池化层处理多通道输入

在处理多通道输入数据时,池化层只会在每个通道上进行单独运算,而不像卷积层一样进行求和汇总。这意味着池化层运算前后的通道数不会发生变化。

X = tf.reshape(tf.range(16, dtype=tf.float32), (1, 4, 4, 1))
X = tf.concat([X, X + 1, X + 2], 3)
X

结果:

<tf.Tensor: shape=(1, 4, 4, 3), dtype=float32, numpy=
array([[[[ 0.,  1.,  2.],
         [ 1.,  2.,  3.],
         [ 2.,  3.,  4.],
         [ 3.,  4.,  5.]],

        [[ 4.,  5.,  6.],
         [ 5.,  6.,  7.],
         [ 6.,  7.,  8.],
         [ 7.,  8.,  9.]],

        [[ 8.,  9., 10.],
         [ 9., 10., 11.],
         [10., 11., 12.],
         [11., 12., 13.]],

        [[12., 13., 14.],
         [13., 14., 15.],
         [14., 15., 16.],
         [15., 16., 17.]]]], dtype=float32)>

经过池化层运算后,输出通道仍然为3

pool2d = tf.keras.layers.MaxPool2D(pool_size=2)
pool2d(X)

结果:

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

        [[13., 14., 15.],
         [15., 16., 17.]]]], dtype=float32)>

5 经典卷积神经网络 LeNet

LeNet是早期成功的神经网络。思路是,先试用卷积层学习图片空间信息,然后使用全连接层来转换到类别空间。

为了构造高性能的卷积神经网络,我们通常对卷积层进行排列,逐渐降低其表示的空间分辨率,同时增加通道数。

5.1 LeNet-5

LeNet由两个卷积层,三个全连接层组成,结构如下:

image.png

具体每一层的信息如下:

image.png

所以我们定义我们的网络模型如下:

import tensorflow as tf
from d2l import tensorflow as d2l

def net():
    """
    创建一个卷积神经网络模型

    Returns:
        tf.keras.models.Sequential: 返回一个包含多个卷积神经网络层的模型
    """
    return tf.keras.models.Sequential([
        # 二维卷积层,使用6个滤波器,每个滤波器的大小为5x5,激活函数为sigmoid
        # 填充方式为在输入的边界处添加0,使得输出的尺寸与输入相同。
        tf.keras.layers.Conv2D(filters=6, kernel_size=5, activation='sigmoid',
                               padding='same'),  
        
        # 平均池化层,池化窗口大小为2x2,步长为2
        tf.keras.layers.AvgPool2D(pool_size=2, strides=2),  
        
        # 二维卷积层,使用16个滤波器,每个滤波器的大小为5x5,激活函数为sigmoid
        tf.keras.layers.Conv2D(filters=16, kernel_size=5,
                              activation='sigmoid'),  
        
        # 平均池化层,池化窗口大小为2x2,步长为2
        tf.keras.layers.AvgPool2D(pool_size=2, strides=2),  

        # 展平层,将输入的多维数组展平为一维数组,以便进行全连接层的处理。
        tf.keras.layers.Flatten(),  

        # 全连接层,神经元个数为120,激活函数为sigmoid
        tf.keras.layers.Dense(120, activation='sigmoid'),  

        # 全连接层,神经元个数为84,激活函数为sigmoid
        tf.keras.layers.Dense(84, activation='sigmoid'),  

        # 全连接层,神经元个数为10
        tf.keras.layers.Dense(10)])  

通过代码检查每一层的输出维度

# 生成一个形状为 (1, 28, 28, 1) 的随机张量 X
X = tf.random.uniform((1, 28, 28, 1))

# 遍历 net() 函数返回的 layers 列表
# tf.keras.models.Sequential.layers是TensorFlow中Sequential模型的一个属性,它返回该模型的层列表。
for layer in net().layers:
    # 将 X 通过当前层处理
    X = layer(X)
    
    # 打印当前层的类名以及输出形状
    print(layer.__class__.__name__, ': \t', X.shape)

结果:

Conv2D : 	 (1, 28, 28, 6)
AveragePooling2D : 	 (1, 14, 14, 6)
Conv2D : 	 (1, 10, 10, 16)
AveragePooling2D : 	 (1, 5, 5, 16)
Flatten : 	 (1, 400)
Dense : 	 (1, 120)
Dense : 	 (1, 84)
Dense : 	 (1, 10)

5.2 训练

卷积网络虽然参数较少,但仍然有很高的计算成本,以下我们可以通过GPU训练,使用d2l.train_ch6方法。

不同于【TensorFlow深度学习】七、Softmax回归(独热编码、交叉熵、Fashion-MNIST分类模型)3.6 训练讲解的d2l.tain_ch3d2l.train_ch6使用了GPU加快训练。

# 批大小,用于指定每个批次的样本数量
batch_size = 256  

# 从Fashion-MNIST数据集中加载训练集和测试集,并指定批次大小
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)  

# 学习率和训练轮数
lr, num_epochs = 0.9, 20  

# 使用给定的网络、训练集、测试集、训练轮数和学习率进行训练,并尝试使用GPU进行计算
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())  

结果:

loss 0.353, train acc 0.870, test acc 0.862
101400.5 examples/sec on /GPU:0

结果:

<keras.engine.sequential.Sequential at 0x14da3220610>

svg

5.3 d2l.train_ch6

下面详细介绍d2l.train_ch6

class TrainCallback(tf.keras.callbacks.Callback):  
    """一个以可视化方式展示训练进展的回调类"""

    def __init__(self, net, train_iter, test_iter, num_epochs, device_name):
        self.timer = d2l.Timer()  # 创建一个计时器
        self.animator = d2l.Animator(
            xlabel='epoch', xlim=[1, num_epochs], legend=[
                'train loss', 'train acc', 'test acc'])  # 创建一个动画演示器
        self.net = net  # 存储网络模型
        self.train_iter = train_iter  # 存储训练数据迭代器
        self.test_iter = test_iter  # 存储测试数据迭代器
        self.num_epochs = num_epochs  # 存储训练轮数
        self.device_name = device_name  # 存储设备名称

    def on_epoch_begin(self, epoch, logs=None):
        self.timer.start()  # 开始计时

    def on_epoch_end(self, epoch, logs):
        self.timer.stop()  # 停止计时
        test_acc = self.net.evaluate(
            self.test_iter, verbose=0, return_dict=True)['accuracy']  # 计算测试集准确率
        metrics = (logs['loss'], logs['accuracy'], test_acc)  # 按照指定顺序存储损失和准确率
        self.animator.add(epoch + 1, metrics)  # 将训练结果添加到动画演示器中
        if epoch == self.num_epochs - 1:
            batch_size = next(iter(self.train_iter))[0].shape[0]  # 获取批次大小
            num_examples = batch_size * tf.data.experimental.cardinality(
                self.train_iter).numpy()  # 计算训练集样本数量
            print(f'loss {metrics[0]:.3f}, train acc {metrics[1]:.3f}, '
                  f'test acc {metrics[2]:.3f}')  # 打印训练结果
            print(f'{num_examples / self.timer.avg():.1f} examples/sec on '
                  f'{str(self.device_name)}')  # 打印设备的示例数和平均计时


def train_ch6(net_fn, train_iter, test_iter, num_epochs, lr, device):
    """使用GPU训练模型"""
    device_name = device._device_name  # 获取GPU设备名称,例如:'/GPU:0'
    strategy = tf.distribute.OneDeviceStrategy(device_name)  # 使用单设备策略将计算任务放置在该 GPU 上
    # 使用该策略进行模型训练
    with strategy.scope():  # 在设备上创建子字典
        optimizer = tf.keras.optimizers.SGD(learning_rate=lr)  # 创建SGD优化器
        loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)  # 创建稀疏分类交叉熵损失函数
        net = net_fn()  # 创建模型实例
        net.compile(optimizer=optimizer, loss=loss, metrics=['accuracy'])  # 编译网络
    callback = TrainCallback(net, train_iter, test_iter, num_epochs, device_name)  # 创建训练回调函数
    net.fit(train_iter, epochs=num_epochs, verbose=0, callbacks=[callback])  # 在训练集上进行模型训练
    return net  # 返回训练好的模型

训练主要由TensorFlow框架内compile和fit两个方法构成,compile用于编译网络,fit用于训练模型。分类准确度。

5.3.1 net.compile

net.compile(optimizer=optimizer, loss=loss, metrics=['accuracy'])

net是一个 TensorFlow 模型,通常是由tf.keras.Model派生而来的。

compile方法用于配置模型的训练设置,包括选择优化器 (optimizer)、损失函数 (loss) 和评估指标 (metrics)。

optimizer参数指定训练过程中使用的优化器,例如tf.keras.optimizers.Adamtf.keras.optimizers.SGD。优化器决定了模型参数的更新方式,以最小化损失函数。

loss参数指定了用于训练的损失函数。损失函数度量模型输出与实际标签之间的差异,训练的目标是最小化损失。

metrics参数是一个列表,包含在训练过程中监控的指标。在这个例子中,['accuracy']表示在每个训练周期结束时,会计算并显示模型在训练数据上的分类准确度。

5.3.2 net.fit

net.fit(train_iter, epochs=num_epochs, verbose=0, callbacks=[callback])

fit方法用于训练模型。

train_iter 是训练数据集的迭代器,包含了输入特征和相应的标签。

epochs=num_epochs 指定了训练的周期数,即模型将遍历整个训练数据的次数。

verbose=0表示训练过程中不显示详细的日志信息。设置为 1 或其他值时,会显示更多训练过程中的信息。

callbacks=[callback]允许在训练过程中应用回调函数。回调函数可以在每个训练周期结束时执行一些操作,例如模型保存、学习率调整等。在这里,callback 是一个回调函数的列表。

5.3.3 class TrainCallback(tf.keras.callbacks.Callback)

TrainCallback是 d2l 深度学习库中提供的一个回调类,用于在训练过程中展示训练进展的动画。

__init__(self, net, train_iter, test_iter, num_epochs, device_name):初始化方法。接收神经网络模型net、训练数据迭代器train_iter、测试数据迭代器test_iter、训练轮数num_epochs和设备名称device_name

on_epoch_begin(self, epoch, logs=None)在每个训练轮开始时调用。 在这里,回调类开始计时。

on_epoch_end(self, epoch, logs)在每个训练轮结束时调用。 在这里,回调类停止计时,计算测试集准确率,并将训练结果添加到动画演示器中。如果是最后一轮训练,还会打印训练结果和设备的示例数和平均计时。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雯雅千鶴子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值