深度学习|十、卷积神经网络

十、卷积神经网络

内容参考来自https://github.com/dragen1860/Deep-Learning-with-TensorFlow-book开源书籍《TensorFlow2深度学习》,这只是我做的简单的学习笔记,方便以后复习。
1.全连接网络的问题 问题就是全连接网络的参数量太过庞大,超过了当时计算机的内存容量,无法存储全部的参数。
局部相关性:基于距离的重要性分布假设特性,它只关注和自己距离较近的部分节点,而忽略距离较远的节点。
从局部相关性引出了权值共享的概念,即对于每个输出节点 ,均使用相同的权值矩阵W。
卷积运算:
卷积的“卷”是指翻转平移操作,“积”是指积分运算。
2D 离散卷积运算流程:每次通过移动卷积核,并与图片对应位置处的感受野像素相乘累加,得到此位置的输出值。卷积核即是行、列为大小的权值矩阵W,应到特征图上大小为的窗口即为感受野,感受野与权值矩阵W相乘累加,得到此位置的输出值。通过权值共享,我们从左上方逐步向右、向下移动卷积核,提取每个位置上的像素特征,直至最右下方,完成卷积运算。
深度学习|十、卷积神经网络
文章图片

2.卷积神经网络 卷积神经网络通过充分利用局部相关性和权值共享的思想,大大地减少了网络的参数量,从而提高训练效率,更容易实现超大规模的深层网络。
2.1单通道输入和单卷积核
深度学习|十、卷积神经网络
文章图片

对应位置相乘再求和:-1-1+0-1+2+6+0-2+4=7
深度学习|十、卷积神经网络
文章图片

计算完成效果如图。
2.2多通道输入和单卷积核
深度学习|十、卷积神经网络
文章图片

深度学习|十、卷积神经网络
文章图片

深度学习|十、卷积神经网络
文章图片

2.3 多通道输入和多卷积核
深度学习|十、卷积神经网络
文章图片

2.4步长
感受野密度的控制手段一般是通过移动步长(Strides)实现的。步长是指感受野窗口每次移动的长度单位,对于 2D 输入来说,分为沿(向右)方向和(向下)方向的移动长度。如下图,步长为2
深度学习|十、卷积神经网络
文章图片

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

深度学习|十、卷积神经网络
文章图片

可以看到通过填充,输入和输出大小一致。
深度学习|十、卷积神经网络
文章图片

深度学习|十、卷积神经网络
文章图片

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 # 输出张量的 shapeTensorShape([2, 5, 5, 4])


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

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

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

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'] = Falsenetwork = 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()

深度学习|十、卷积神经网络
文章图片

深度学习|十、卷积神经网络
文章图片

在数据集上面循环训练 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)

深度学习|十、卷积神经网络
文章图片

深度学习|十、卷积神经网络
文章图片

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 提高了模型的泛化能力,防止过拟合。
深度学习|十、卷积神经网络
文章图片

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的池化窗口
深度学习|十、卷积神经网络
文章图片

深度学习|十、卷积神经网络
文章图片

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 卷积层
深度学习|十、卷积神经网络
文章图片

深度学习|十、卷积神经网络
文章图片

8.CIFAR10和VGG13实战 CIFAR10 数据集由加拿大 Canadian Institute For Advanced Research 发布,它包含了飞机、汽车、鸟、猫等共 10 大类物体的彩色图片,每个种类收集了 6000 张32 × 32大小图片,共 6 万张图片。其中 5 万张作为训练数据集,1 万张作为测试数据集。每个种类样片如图
深度学习|十、卷积神经网络
文章图片

本节将基于表达能力更强的 VGG13 网络,根据我们的数据集特点修改部分网络结构,完成 CIFAR10 图片识别。
深度学习|十、卷积神经网络
文章图片

import tensorflow as tf from tensorflow.keras import layers, optimizers, datasets, Sequential import osos.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_variablesfor 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 个单元采样一个点,
深度学习|十、卷积神经网络
文章图片

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 来实现输出高宽大于输入高宽的效果,从而实现向上采样的目的。
深度学习|十、卷积神经网络
文章图片

# 创建 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)

深度学习|十、卷积神经网络
文章图片

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卷积核的普通卷积运算,得到多个高宽不变的输出,这些输出在通道轴上面进行拼接,从而产生最终的分离卷积层的输出。可以看到,分离卷积层包含了两步卷积运算,第一步卷积运算是单个卷积核,第二个卷积运算包含了多个卷积核。
深度学习|十、卷积神经网络
文章图片

深度学习|十、卷积神经网络
文章图片

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,故称为残差网络。
深度学习|十、卷积神经网络
文章图片

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: xdef 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 采用在通道轴维度进行拼接操作,聚合特征信息。
深度学习|十、卷积神经网络
文章图片

12. CIFAR100和ResNet18实战 【深度学习|十、卷积神经网络】本节我们将实现 18 层的深度残差网络 ResNet18,并在 CIFAR100 图片数据集上训练与测试。
深度学习|十、卷积神经网络
文章图片

# ResNet.py import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers, Sequentialclass 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: xdef 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 outputclass 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 xdef 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 resblocksdef 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 ResNet18gpu = 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-kerasdef 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%。我们这里的实战代码比较精简,在精挑超参数、数据增强等手段加持下,准确率可以达到更高。
欢迎关注我的微信公众号,同步更新,嘻嘻
深度学习|十、卷积神经网络
文章图片

    推荐阅读