十、卷积神经网络
内容参考来自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%。我们这里的实战代码比较精简,在精挑超参数、数据增强等手段加持下,准确率可以达到更高。
欢迎关注我的微信公众号,同步更新,嘻嘻
文章图片
推荐阅读
- 计算机视觉|超越PVT!南大提出ResT(高效多尺度的视觉Transformer)
- pytorch|图像分类篇(实现pytorch官网demo(LeNet))
- Pytorch进阶|【Pytorch进阶一】基于LeNet的CIFAR10图像分类
- 动手学深度学习PyTorch版|《动手学深度学习PyTorch版》打卡_Task3,过拟合,欠拟合,梯度消失,梯度爆炸
- 分类|LeNet网络模型——CIFAR-10数据集进行分类
- 深度学习|【深度学习1】Anaconda3的安装和Jupyter的使用
- 自然语言处理|Python 文字转语音(TTS)
- 深度学习|【深度学习】吴恩达深度学习-Course1神经网络与深度学习-第四周深度神经网络的关键概念编程(上)——一步步建立深度神经网络
- 计算机视觉|10分钟学会使用YOLO及Opencv实现目标检测