十、卷积神经网络

十、卷积神经网络

内容参考来自https://github.com/dragen1860/Deep-Learning-with-TensorFlow-book开源书籍《TensorFlow2深度学习》,这只是我做的简单的学习笔记,方便以后复习。

1.全连接网络的问题

问题就是全连接网络的参数量太过庞大,超过了当时计算机的内存容量,无法存储全部的参数。

局部相关性:基于距离的重要性分布假设特性,它只关注和自己距离较近的部分节点,而忽略距离较远的节点。

从局部相关性引出了权值共享的概念,即对于每个输出节点 ,均使用相同的权值矩阵W。

卷积运算:

卷积的“卷”是指翻转平移操作,“积”是指积分运算。

2D 离散卷积运算流程:每次通过移动卷积核,并与图片对应位置处的感受野像素相乘累加,得到此位置的输出值。卷积核即是行、列为大小的权值矩阵W,应到特征图上大小为的窗口即为感受野,感受野与权值矩阵W相乘累加,得到此位置的输出值。通过权值共享,我们从左上方逐步向右、向下移动卷积核,提取每个位置上的像素特征,直至最右下方,完成卷积运算。

十、卷积神经网络_第1张图片

2.卷积神经网络

卷积神经网络通过充分利用局部相关性和权值共享的思想,大大地减少了网络的参数量,从而提高训练效率,更容易实现超大规模的深层网络。

2.1单通道输入和单卷积核

十、卷积神经网络_第2张图片

对应位置相乘再求和:-1-1+0-1+2+6+0-2+4=7

十、卷积神经网络_第3张图片

计算完成效果如图。

2.2多通道输入和单卷积核

十、卷积神经网络_第4张图片
十、卷积神经网络_第5张图片
十、卷积神经网络_第6张图片

2.3 多通道输入和多卷积核

十、卷积神经网络_第7张图片

2.4步长

感受野密度的控制手段一般是通过移动步长(Strides)实现的。步长是指感受野窗口每次移动的长度单位,对于 2D 输入来说,分为沿(向右)方向和(向下)方向的移动长度。如下图,步长为2

十、卷积神经网络_第8张图片

2.5填充

为了让输出的高宽能够与输入X的相等,一般通过在原输入X的高和宽维度上面进行填充(Padding)若干无效元素操作,得到增大的输入X′。通过精心设计填充单元的数量,在X′上面进行卷积运算得到输出的高宽可以和原输入X相等,甚至更大。

十、卷积神经网络_第9张图片
十、卷积神经网络_第10张图片

可以看到通过填充,输入和输出大小一致。
十、卷积神经网络_第11张图片
十、卷积神经网络_第12张图片

3.卷积层实现

3.1自定义权值

在 TensorFlow 中,通过 tf.nn.conv2d 函数可以方便地实现 2D 卷积运算。tf.nn.conv2d基于输入X:[b, h, w, c_in] 和卷积核W: [k, k, c_in, cout]进行卷积运算,得到输出 ℎ′ ′ ,其中 表示输入通道数, 表示卷积核的数量,也是输出特征图的通道数。

x = tf.random.normal([2,5,5,3]) # 模拟输入,3 通道,高宽为 5
# 需要根据[k,k,cin,cout]格式创建 W 张量,4 个 3x3 大小卷积核
w = tf.random.normal([3,3,3,4])
# 步长为 1, padding 为 0,
out = tf.nn.conv2d(x,w,strides=1,padding=[[0,0],[0,0],[0,0],[0,0]])
out.shape  # TensorShape([2, 3, 3, 4])

上下左右各填充一个单位,则 padding 参数设置为[[0,0],[1,1],[1,1],[0,0]]

x = tf.random.normal([2,5,5,3]) # 模拟输入,3 通道,高宽为 5
# 需要根据[k,k,cin,cout]格式创建,4 个 3x3 大小卷积核
w = tf.random.normal([3,3,3,4])
# 步长为 1, padding 为 1,
out = tf.nn.conv2d(x,w,strides=1,padding=[[0,0],[1,1],[1,1],[0,0]])
out.shape # TensorShape([2, 5, 5, 4])

特别地,通过设置参数 padding=‘SAME’、strides=1 可以直接得到输入、输出同大小的卷积层,其中 padding 的具体数量由 TensorFlow 自动计算并完成填充操作。

x = tf.random.normal([2,5,5,3]) # 模拟输入,3 通道,高宽为 5
w = tf.random.normal([3,3,3,4]) # 4 个 3x3 大小的卷积核
# 步长为,padding 设置为输出、输入同大小
# 需要注意的是, padding=same 只有在 strides=1 时才是同大小
out = tf.nn.conv2d(x,w,strides=1,padding='SAME')
out.shape # TensorShape([2, 5, 5, 4])

当 > 时,设置 padding='SAME’将使得输出高、宽将成 1/s 倍地减少

x = tf.random.normal([2,5,5,3])
w = tf.random.normal([3,3,3,4])
# 高宽先 padding 成可以整除 3 的最小整数 6,然后 6 按 3 倍减少,得到 2x2
out = tf.nn.conv2d(x,w,strides=3,padding='SAME')
out.shape # TensorShape([2, 2, 2, 4])

卷积神经网络层与全连接层一样,可以设置网络带偏置向量。tf.nn.conv2d 函数是没有实现偏置向量计算的,添加偏置只需要手动累加偏置张量即可。

# 根据[cout]格式创建偏置向量
b = tf.zeros([4])
# 在卷积输出上叠加偏置向量,它会自动 broadcasting 为[b,h',w',cout]
out = out + b

3.2卷积层类

在新建卷积层类时,只需要指定卷积核数量参数 filters,卷积核大小 kernel_size,步长strides,填充 padding 等即可。如下创建了 4 个3 × 3大小的卷积核的卷积层,步长为 1,padding 方案为’SAME’

layer = layers.Conv2D(4,kernel_size=3,strides=1,padding='SAME')
# layer = layers.Conv2D(4,kernel_size=(3,4),strides=(2,1),padding='SAME')

x = tf.random.normal([2,5,5,3]) # 模拟输入,3 通道,高宽为 5
layer = layers.Conv2D(4,kernel_size=3,strides=1,padding='SAME')
out = layer(x) # 前向计算
out.shape # 输出张量的 shape   TensorShape([2, 5, 5, 4])

# 返回所有待优化张量列表
layer.trainable_variables

4.Le-Net5实战

1990 年代,Yann LeCun 等人提出了用于手写数字和机器打印字符图片识别的神经网络,被命名为 LeNet-5。LeNet-5 的提出,使得卷积神经网络在当时能够成功被商用,广泛应用在邮政编码、支票号码识别等任务中。

十、卷积神经网络_第13张图片

我们在 LeNet-5 的基础上进行了少许调整,使得它更容易在现代深度学习框架上实现。首先我们将输入形状由32 × 32调整为28 × 28,然后将 2 个下采样层实现为最大池化层(降低特征图的高、宽,后续会介绍),最后利用全连接层替换掉 Gaussian connections层。下文统一称修改的网络也为 LeNet-5 网络。

十、卷积神经网络_第14张图片

import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential, losses
from matplotlib import pyplot as plt
import matplotlib

# Default parameters for plots
matplotlib.rcParams['font.size'] = 20
matplotlib.rcParams['figure.titlesize'] = 20
matplotlib.rcParams['figure.figsize'] = [9, 7]
matplotlib.rcParams['font.family'] = ['STKaiTi']
matplotlib.rcParams['axes.unicode_minus'] = False

network = Sequential([  # 网络容器
    layers.Conv2D(6, kernel_size=3, strides=1),  # 第一个卷积层, 6 个 3x3 卷积核
    layers.MaxPooling2D(pool_size=2, strides=2),  # 高宽各减半的池化层
    layers.ReLU(),  # 激活函数
    layers.Conv2D(16, kernel_size=3, strides=1),  # 第二个卷积层, 16 个 3x3 卷积核
    layers.MaxPooling2D(pool_size=2, strides=2),  # 高宽各减半的池化层
    layers.ReLU(),  # 激活函数
    layers.Flatten(),  # 打平层,方便全连接层处理
    layers.Dense(120, activation='relu'),  # 全连接层,120 个节点
    layers.Dense(84, activation='relu'),  # 全连接层,84 节点
    layers.Dense(10)  # 全连接层,10 个节点
])


def preprocess(x, y):
    # [0~1]
    x = 2 * tf.cast(x, dtype=tf.float32) / 255. - 1
    y = tf.cast(y, dtype=tf.int32)
    return x, y


(x, y), (x_test, y_test) = datasets.mnist.load_data()
print(x.shape, y.shape, x_test.shape, y_test.shape)

train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.shuffle(1000).map(preprocess).batch(128)

test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.map(preprocess).batch(64)

sample = next(iter(train_db))
print('sample:', sample[0].shape, sample[1].shape,
      tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))

# 创建损失函数的类,在实际计算时直接调用类实例即可
criteon = losses.CategoricalCrossentropy(from_logits=True)

lossArr = []  # 记录loss的变化
accArr = []  # 记录accuracy的变化


def main():
    # build 一次网络模型,给输入 X 的形状,其中 4 为随意给的 batchsz
    network.build(input_shape=(4, 28, 28, 1))

    # 统计网络信息
    # network.summary()
    optimizer = optimizers.Adam(learning_rate=1e-4)

    for epoch in range(20):
        for step, (x, y) in enumerate(train_db):
            # 构建梯度记录环境
            with tf.GradientTape() as tape:
                # 插入通道维度,=>[b,28,28,1]
                x = tf.expand_dims(x, axis=3)
                # 前向计算,获得10类别的预测分布,[b, 784] => [b, 10]
                out = network(x)
                # 真实标签one-hot编码,[b] => [b, 10]
                y_onehot = tf.one_hot(y, depth=10)
                # 计算交叉熵损失函数,标量
                loss = criteon(y_onehot, out)
            # 自动计算梯度
            grads = tape.gradient(loss, network.trainable_variables)
            # 自动更新参数
            optimizer.apply_gradients(zip(grads, network.trainable_variables))
            if step % 100 == 0:
                print(epoch, step, 'loss:', float(loss))

        # 记录预测正确的数量,总样本数量
        lossArr.append(loss)
        correct, total = 0, 0
        for x, y in test_db:  # 遍历所有训练集样本
            # 插入通道维度,=>[b,28,28,1]
            x = tf.expand_dims(x, axis=3)
            # 前向计算,获得 10 类别的预测分布,[b, 784] => [b, 10]
            out = network(x)
            # 真实的流程时先经过 softmax,再 argmax
            # 但是由于 softmax 不改变元素的大小相对关系,故省去
            pred = tf.argmax(out, axis=-1)
            y = tf.cast(y, tf.int64)
            # 统计预测正确数量
            correct += float(tf.reduce_sum(tf.cast(tf.equal(pred, y), tf.float32)))
            # 统计预测样本总数
            total += x.shape[0]
        # 计算准确率
        acc = correct / total
        print('test acc:', acc)
        accArr.append(acc)

    plt.figure()
    x = [i * 80 for i in range(len(lossArr))]
    plt.plot(x, lossArr, color='C0', marker='s', label='训练')
    plt.ylabel('交叉熵损失')
    plt.xlabel('epoch')
    plt.legend()
    # plt.savefig('train.svg')
    plt.show()

    plt.figure()
    plt.plot(x, accArr, color='C1', marker='s', label='测试')
    plt.ylabel('准确率')
    plt.xlabel('epoch')
    plt.legend()
    # plt.savefig('test.svg')
    plt.show()


if __name__ == '__main__':
    main()

十、卷积神经网络_第15张图片
十、卷积神经网络_第16张图片

在数据集上面循环训练 30 个 Epoch 后,网络的训练准确度达到了 98.1%,测试准确度也达到了 97.7%。对于非常简单的手写数字图片识别任务,古老的 LeNet-5 网络已经可以取得很好的效果,但是稍复杂一点的任务,比如彩色动物图片识别,LeNet-5 性能就会急剧下降。

5.池化层

池化层同样基于局部相关性的思想,通过从局部相关的一组元素中进行采样或信息聚合,从而得到新的元素值。特别地,最大池化层(Max Pooling)从局部相关元素集中选取最大的一个元素值,平均池化层(Average Pooling)从局部相关元素集中计算平均值并返回。

6.BatchNorm层

2015 年,Google 研究人员 Sergey Ioffe 等提出了一种参数标准化(Normalize)的手段,并基于参数标准化设计了 Batch Nomalization(简写为 BatchNorm,或 BN)层 [6]。BN 层的提出,使得网络的超参数的设定更加自由,比如更大的学习率、更随意的网络初始化等,同时网络的收敛速度更快,性能也更好。BN 层提出后便广泛地应用在各种深度网络模型
上,卷积层、BN 层、ReLU 层、池化层一度成为网络模型的标配单元块,通过堆叠 Conv-BN-ReLU-Pooling 方式往往可以获得不错的模型性能。

BN 层实现: 以 LeNet-5 的网络模型为例,在卷积层后添加 BN 层

# 第1步修改
network = Sequential([  # 网络容器
    layers.Conv2D(6, kernel_size=3, strides=1),  # 第一个卷积层, 6 个 3x3 卷积核
    # 插入 BN 层
    layers.BatchNormalization(),
    layers.MaxPooling2D(pool_size=2, strides=2),  # 高宽各减半的池化层
    layers.ReLU(),  # 激活函数
    layers.Conv2D(16, kernel_size=3, strides=1),  # 第二个卷积层, 16 个 3x3 卷积核
    # 插入 BN 层
    layers.BatchNormalization(),
    layers.MaxPooling2D(pool_size=2, strides=2),  # 高宽各减半的池化层
    layers.ReLU(),  # 激活函数
    layers.Flatten(),  # 打平层,方便全连接层处理
    layers.Dense(120, activation='relu'),  # 全连接层,120 个节点
    # 插入 BN 层
    layers.BatchNormalization(),
    layers.Dense(84, activation='relu'),  # 全连接层,84 节点
    # 插入 BN 层
    layers.BatchNormalization(),
    layers.Dense(10)  # 全连接层,10 个节点
])
# 第2步修改 在训练阶段,需要设置网络的参数 training=True 以区分 BN 层是训练还是测试模型
out = network(x, training=True)
# 第3步修改 在测试阶段,需要设置 training=False ,避免 BN 层采用错误的行为
out = network(x, training=False)

十、卷积神经网络_第17张图片
十、卷积神经网络_第18张图片

7.经典卷积网络

7.1AlexNet

2012 年,ILSVRC12 挑战赛 ImageNet 数据集分类任务的冠军 Alex Krizhevsky 提出了 8层的深度神经网络模型 AlexNet,它接收输入为22 × 22 大小的彩色图片数据,经过五个卷积层和三个全连接层后得到样本属于 1000 个类别的概率分布。为了降低特征图的维度,AlexNet 在第 1、2、5 个卷积层后添加了 Max Pooling 层

AlexNet 的创新之处在于

  • 层数达到了较深的 8 层
  • 采用了 ReLU 激活函数,过去的神经网络大多采用 Sigmoid 激活函数,计算相对复杂,容易出现梯度弥散现象。
  • 引入 Dropout 层。Dropout 提高了模型的泛化能力,防止过拟合。

十、卷积神经网络_第19张图片

7.2VGG 系列

2014 年,
ILSVRC14 挑战赛 ImageNet 分类任务的亚军牛津大学 VGG 实验室提出了 VGG11、VGG13、VGG16、VGG19 等一系列的网络模型(图 10.45),并将网络深度最高提升至 19层 [8]。以 VGG16 为例,它接受22 × 22 大小的彩色图片数据,经过 2 个 Conv-Conv-Pooling 单元,和 3 个 Conv-Conv-Conv-Pooling 单元的堆叠,最后通过 3 层全连接层输出当
前图片分别属于 1000 类别的概率分布,如图 10.44 所示。VGG16 在 ImageNet 取得了7.4%的 Top-5 错误率,比 AlexNet 在错误率上降低了 7.9%。

VGG 系列网络的创新之处在于:

  • 层数提升至 19 层。
  • 全部采用更小的3 × 3卷积核,相对于 AlexNet 中 × 的卷积核,参数量更少,计算代价更低。
  • 采用更小的池化层2 × 2窗口和步长 = 2,而 AlexNet 中是步长 = 2、3 × 3的池化窗口

十、卷积神经网络_第20张图片
十、卷积神经网络_第21张图片

7.3 GoogLeNet

2014 年,ILSVRC14 挑战赛的冠军 Google 提出了大量采用3 × 3和 × 卷积核的网络模型:GoogLeNet,网络层数达到了 22 层 [9]。虽然 GoogLeNet 的层数远大于 AlexNet,但是它的参数量却只有 AlexNet 的1/12 ,同时性能也远好于 AlexNet。在 ImageNet 数据集分类任务上,GoogLeNet 取得了 6.7%的 Top-5 错误率,比 VGG16 在错误率上降低了 0.7%。

GoogLeNet 网络采用模块化设计的思想,通过大量堆叠 Inception 模块,形成了复杂的网络结构。如下图 10.47 所示,Inception 模块的输入为X,通过 4 个子网络得到 4 个网络输出,在通道轴上面进行拼接合并,形成Inception 模块的输出。这 4 个子网络是

  • 1× 1卷积层
  • 1× 1 卷积层,再通过一个 3×3卷积层
  • 1× 1 卷积层,再通过一个 5×5 卷积层
  • 3 × 3最大池化层,再通过 1×1 卷积层

十、卷积神经网络_第22张图片

十、卷积神经网络_第23张图片

8.CIFAR10和VGG13实战

CIFAR10 数据集由加拿大 Canadian Institute For Advanced Research 发布,它包含了飞机、汽车、鸟、猫等共 10 大类物体的彩色图片,每个种类收集了 6000 张32 × 32大小图片,共 6 万张图片。其中 5 万张作为训练数据集,1 万张作为测试数据集。每个种类样片如图

十、卷积神经网络_第24张图片

本节将基于表达能力更强的 VGG13 网络,根据我们的数据集特点修改部分网络结构,完成 CIFAR10 图片识别。

十、卷积神经网络_第25张图片

import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
tf.random.set_seed(2345)
# VGG13的结构
conv_layers = [  # 5 units of conv + max pooling
    # unit 1
    layers.Conv2D(64, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.Conv2D(64, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

    # unit 2
    layers.Conv2D(128, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.Conv2D(128, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

    # unit 3
    layers.Conv2D(256, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.Conv2D(256, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

    # unit 4
    layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

    # unit 5
    layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same')

]


def preprocess(x, y):
    # [0~1]
    x = 2 * tf.cast(x, dtype=tf.float32) / 255. - 1
    y = tf.cast(y, dtype=tf.int32)
    return x, y


(x, y), (x_test, y_test) = datasets.cifar10.load_data()
y = tf.squeeze(y, axis=1)
y_test = tf.squeeze(y_test, axis=1)
print(x.shape, y.shape, x_test.shape, y_test.shape)

train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.shuffle(1000).map(preprocess).batch(128)

test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.map(preprocess).batch(64)

sample = next(iter(train_db))
print('sample:', sample[0].shape, sample[1].shape,
      tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))


def main():
    # [b, 32, 32, 3] => [b, 1, 1, 512]
    conv_net = Sequential(conv_layers)

    fc_net = Sequential([
        layers.Dense(256, activation=tf.nn.relu),
        layers.Dense(128, activation=tf.nn.relu),
        layers.Dense(10, activation=None),
    ])

    conv_net.build(input_shape=[None, 32, 32, 3])
    fc_net.build(input_shape=[None, 512])
    conv_net.summary()
    fc_net.summary()
    optimizer = optimizers.Adam(lr=1e-4)

    # [1, 2] + [3, 4] => [1, 2, 3, 4]
    variables = conv_net.trainable_variables + fc_net.trainable_variables

    for epoch in range(50):

        for step, (x, y) in enumerate(train_db):

            with tf.GradientTape() as tape:
                # [b, 32, 32, 3] => [b, 1, 1, 512]
                out = conv_net(x)
                # flatten, => [b, 512]
                out = tf.reshape(out, [-1, 512])
                # [b, 512] => [b, 10]
                logits = fc_net(out)
                # [b] => [b, 10]
                y_onehot = tf.one_hot(y, depth=10)
                # compute loss
                loss = tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True)
                loss = tf.reduce_mean(loss)

            grads = tape.gradient(loss, variables)
            optimizer.apply_gradients(zip(grads, variables))

            if step % 100 == 0:
                print(epoch, step, 'loss:', float(loss))

        total_num = 0
        total_correct = 0
        for x, y in test_db:
            out = conv_net(x)
            out = tf.reshape(out, [-1, 512])
            logits = fc_net(out)
            prob = tf.nn.softmax(logits, axis=1)
            pred = tf.argmax(prob, axis=1)
            pred = tf.cast(pred, dtype=tf.int32)

            correct = tf.cast(tf.equal(pred, y), dtype=tf.int32)
            correct = tf.reduce_sum(correct)

            total_num += x.shape[0]
            total_correct += int(correct)

        acc = total_correct / total_num
        print(epoch, 'acc:', acc)


if __name__ == '__main__':
    main()

话说这个段代码对电脑的计算能力要求很高,我电脑上安装的是CPU版本的,运行不出结果(太慢了),需要使用GPU运算,有条件的可以试试。

9.卷积层变种

9.1 空洞卷积

空洞卷积(Dilated/Atrous Convolution)的提出较好地解决这个问题,空洞卷积在普通卷积的感受野上增加一个 Dilation Rate 参数,用于控制感受野区域的采样步长,当感受野的采样步长 Dilation Rate 为 1 时,每个感受野采样点之间的距离为1,此时的空洞卷积退化为普通的卷积;当 Dilation Rate 为 2 时,感受野每 2 个单元采样一个点,

十、卷积神经网络_第26张图片

x = tf.random.normal([1,7,7,1]) # 模拟输入
# 空洞卷积,1 个 3x3 的卷积核
layer = layers.Conv2D(1,kernel_size=3,strides=1,dilation_rate=2)
out = layer(x) # 前向计算
out.shape #  TensorShape([1, 3, 3, 1])

9.2 转置卷积

转置卷积(Transposed Convolution,或 Fractionally Strided Convolution,部分资料也称之为反卷积/Deconvolution,实际上反卷积在数学上定义为卷积的逆过程,但转置卷积并不能恢复出原卷积的输入,因此称为反卷积并不妥当)通过在输入之间填充大量的 padding 来实现输出高宽大于输入高宽的效果,从而实现向上采样的目的。

十、卷积神经网络_第27张图片

# 创建 X 矩阵,高宽为 5x5
x = tf.range(25)+1
# Reshape 为合法维度的张量
x = tf.reshape(x,[1,5,5,1])
x = tf.cast(x, tf.float32)
# 创建固定内容的卷积核矩阵
w = tf.constant([[-1,2,-3.],[4,-5,6],[-7,8,-9]])
# 调整为合法维度的张量
w = tf.expand_dims(w,axis=2)
w = tf.expand_dims(w,axis=3)

# 进行普通卷积运算
out = tf.nn.conv2d(x,w,strides=2,padding='VALID')
out.shape # TensorShape([1, 2, 2, 1])

# 普通卷积的输出作为转置卷积的输入,进行转置卷积运算
xx = tf.nn.conv2d_transpose(out, w, strides=2,padding='VALID',output_shape=[1,5,5,1])
xx.shape # TensorShape([1, 5, 5, 1])

x = tf.random.normal([1,6,6,1])
# 6x6 的输入经过普通卷积
out = tf.nn.conv2d(x,w,strides=2,padding='VALID')
out.shape # TensorShape([1, 2, 2, 1])

# 恢复出 6x6 大小
xx = tf.nn.conv2d_transpose(out, w, strides=2,padding='VALID',output_shape=[1,6,6,1])
xx.shape # TensorShape([1, 6, 6, 1])

# 创建 4x4 大小的输入
x = tf.range(16)+1
x = tf.reshape(x,[1,4,4,1])
x = tf.cast(x, tf.float32)
# 创建 3x3 卷积核
w = tf.constant([[-1,2,-3.],[4,-5,6],[-7,8,-9]])
w = tf.expand_dims(w,axis=2)
w = tf.expand_dims(w,axis=3)
# 普通卷积运算
out = tf.nn.conv2d(x,w,strides=1,padding='VALID')
out.shape # TensorShape([1, 2, 2, 1])

xx = tf.nn.conv2d_transpose(out, w, strides=1, padding='VALID',output_shape=[1,4,4,1])
tf.squeeze(xx)  ## shape=(4, 4)

十、卷积神经网络_第28张图片

layer = layers.Conv2DTranspose(1,kernel_size=3,strides=1,padding='VALID')
xx2 = layer(out) # 通过转置卷积层
xx2.shape # TensorShape([1, 4, 4, 1])

9.3分离卷积

分离卷积的计算流程则不同,卷积核的每个通道与输入的每个通道进行卷积运算,得到多个通道的中间特征,如图 10.61 所示。这个多通道的中间特征张量接下来进行多个1× 1卷积核的普通卷积运算,得到多个高宽不变的输出,这些输出在通道轴上面进行拼接,从而产生最终的分离卷积层的输出。可以看到,分离卷积层包含了两步卷积运算,第一步卷积运算是单个卷积核,第二个卷积运算包含了多个卷积核。

十、卷积神经网络_第29张图片
十、卷积神经网络_第30张图片

10.深度残差网络(ResNet)

2015 年,微软亚洲研究院何凯明等人发表了基于 Skip Connection 的深度残差网络(Residual Neural Network,简称 ResNet)算法 [10],并提出了 18 层、34 层、50 层、101层、152 层的 ResNet-18、ResNet-34、ResNet-50、ResNet-101 和 ResNet-152 等模型,甚至成功训练出层数达到 1202 层的极深层神经网络。ResNet 在 ILSVRC 2015 挑战赛 ImageNet数据集上的分类、检测等任务上面均获得了最好性能。

ResNet 通过在卷积层的输入和输出之间添加 Skip Connection 实现层数回退机制,如10.63 所示,输入x通过两个卷积层,得到特征变换后的输出ℱ(x),与输入x进行对应元素的相加运算,得到最终输出ℋ(x):

ℋ(x) = x + ℱ(x)

ℋ(x)叫作残差模块(Residual Block,简称 ResBlock)。由于被 Skip Connection 包围的卷积神经网络需要学习映射ℱ(x) = ℋ(x) − x,故称为残差网络。

十、卷积神经网络_第31张图片

ResBlock 实现

class BasicBlock(layers.Layer):

    def __init__(self, filter_num,stride=1):
        super(BasicBlock, self).__init__()
        self.conv1 = layers.Conv2D(filter_num, (3,3), strides=stride, padding='same')
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation('relu')

        self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')
        self.bn2 = layers.BatchNormalization()

        if stride != 1:
            self.downsample = Sequential()
            self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
        else:
            self.downsample = lambda x: x

    def call(self, inputs, training=True):
        # [b, h, w, c]
        out = self.conv1(inputs)
        out = self.bn1(out)
        out = self.relu(out)

        identity = self.downsample(inputs)
        output = layers.add([out, identity])
        output = tf.nn.relu(output)
        return output

11.DenseNet

Skip Connection 的思想在 ResNet 上面获得了巨大的成功,研究人员开始尝试不同的Skip Connection 方案,其中比较流行的就是 DenseNet 。DenseNet 将前面所有层的特征图信息通过 Skip Connection 与当前层输出进行聚合,与 ResNet 的对应位置相加方式不同,DenseNet 采用在通道轴维度进行拼接操作,聚合特征信息。

十、卷积神经网络_第32张图片

12. CIFAR100和ResNet18实战

本节我们将实现 18 层的深度残差网络 ResNet18,并在 CIFAR100 图片数据集上训练与测试。

十、卷积神经网络_第33张图片

# ResNet.py
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Sequential

class BasicBlock(layers.Layer):

    def __init__(self, filter_num,stride=1):
        super(BasicBlock, self).__init__()
        self.conv1 = layers.Conv2D(filter_num, (3,3), strides=stride, padding='same')
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation('relu')

        self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')
        self.bn2 = layers.BatchNormalization()

        if stride != 1:
            self.downsample = Sequential()
            self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
        else:
            self.downsample = lambda x: x

    def call(self, inputs, training=True):
        # [b, h, w, c]
        out = self.conv1(inputs)
        out = self.bn1(out)
        out = self.relu(out)

        identity = self.downsample(inputs)
        output = layers.add([out, identity])
        output = tf.nn.relu(output)
        return output


class ResNet(keras.Model):
    def __init__(self, layer_dims, num_classes=100):  # [2,2,2,2]
        super(ResNet, self).__init__()
        self.stem = Sequential([layers.Conv2D(64, (3, 3),strides=(1, 1)),
                                layers.BatchNormalization(),
                                layers.Activation('relu'),
                                layers.MaxPool2D(pool_size=(2, 2), strides=(1, 1), padding='same')])
        self.layer1 = self.build_resblack(64,  layer_dims[0])
        self.layer2 = self.build_resblack(128, layer_dims[1], stride=2)
        self.layer3 = self.build_resblack(256, layer_dims[2], stride=2)
        self.layer4 = self.build_resblack(512, layer_dims[3], stride=2)

        # output: [b, 512, h, w]
        self.avgpoll = layers.GlobalAveragePooling2D()
        self.fc = layers.Dense(num_classes)

    def call(self, inputs, training=None):
        x = self.stem(inputs)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        # [b, c]
        x = self.avgpoll(x)
        # [b, 100]
        x = self.fc(x)
        return x

    def build_resblack(self, filter_num, blocks, stride=1):
        resblocks = Sequential()
        resblocks.add(BasicBlock(filter_num, stride))
        for _ in range(1, blocks):
            resblocks.add(BasicBlock(filter_num, stride=1))
        return resblocks


def ResNet18():
    return ResNet([2, 2, 2, 2])


def ResNet34():
    return ResNet([3, 4, 6, 3])


import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
from ResNet import ResNet18

gpu = tf.config.list_physical_devices('GPU')
if len(gpu) > 0:
    tf.config.experimental.set_memory_growth(gpu[0], True)


# 加载数据
def preprocess(x, y):
    [-1,1]
    x = 2 * tf.cast(x, dtype=tf.float32) / 255. -1
    y = tf.cast(y, dtype=tf.int32)
    return x, y


(x, y), (x_test, y_test) = datasets.cifar100.load_data()
y = tf.squeeze(y, axis=1) # [n, 1] => [n]
y_test = tf.squeeze(y_test, axis=1) # [n, 1] => [n]

train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.shuffle(1000).map(preprocess).batch(256)

test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.map(preprocess).batch(256)

# 老师为了讲解算法,这里分成了两个网络,把reshape的过程手动计算了,其实完全可以用一个reshape层就行了,直接调用keras API训练,参见另一个文件 cifar100-keras


def main():
    # [b, 32, 32, 3] => [b, 1, 1, 512]
    model = ResNet18()
    model.build(input_shape=(None, 32, 32, 3)) # 这里input_shape 用 [] 就会报错,不知道为啥
    model.summary()
    optimizer = optimizers.Adam(1e-3)

    for epoch in range(50):
        for step, (x, y) in enumerate(train_db):
            with tf.GradientTape() as tape:
                # [b, 32, 32, 3] => [b, 100]
                logits = model(x, training=True)
                y_onehot = tf.one_hot(y, depth=100)
                loss = tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True)
                loss = tf.reduce_mean(loss)

            grads = tape.gradient(loss, model.trainable_variables)
            optimizer.apply_gradients(zip(grads, model.trainable_variables))
            if step%100 == 0:
                print(epoch, step, 'loss:', float(loss))
        total_num = 0
        total_correct = 0
        for x, y in test_db:
            logits = model(x, training=False)
            prob = tf.nn.softmax(logits, axis=1)
            pred = tf.argmax(prob, axis=1)
            pred = tf.cast(pred, dtype=tf.int32)

            correct = tf.cast(tf.equal(pred, y), dtype=tf.int32)
            correct = tf.reduce_sum(correct)

            total_num += x.shape[0]
            total_correct += int(correct)

        acc = total_correct / total_num
        print(epoch, 'acc:', acc)


if __name__ == '__main__':
    main()

同样,这个例子我自己的电脑跑不了,哭…

ResNet18 的网络参数量共 1100 万个,经过 50 个 Epoch 后,网络的准确率达到了79.3%。我们这里的实战代码比较精简,在精挑超参数、数据增强等手段加持下,准确率可以达到更高。

欢迎关注我的微信公众号,同步更新,嘻嘻
十、卷积神经网络_第34张图片

你可能感兴趣的:(深度学习,神经网络,深度学习,tensorflow)