【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别

时间:2022-12-27 10:51:29


本文主要通过卷积神经网络在图像识别上的应用来讲解卷积神经网络的基本原理以及如何使用Tensorflow2实现卷积神经网络。CIFAR数据集是一个影响力很大的图像分类数据集,其中的图片为32×32的彩色图片。

卷积神经网络

【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
卷积神经网络的特点

  • 局部感知:局部感知即卷积核的局部感受野,指的是卷积核所覆盖的像素面积,由于每个卷积核所覆盖的面积仅是很少的一部分,是局部特征,即为局部感知。CNN是一个从局部到整体的过程(局部到整体的实现是在全连通层)。下图是全连接层和卷积层的对比。
    【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
  • 权重共享:卷积神经网络的卷积层,不同神经元的权值是共享的,这使得整个神经网络的参数大大减小,提高了整个网络的训练性能。
  • 降采样:降采样是卷积神经网络的另一重要概念,通常也称之为池化(Pooling)。简单理解可以看作是对图像的一次“有损压缩”,因为在实际的训练中,我们并不需要对图像中的每一个细节都进行特征提取和训练,所以池化的作用就是更进一步的信息抽象和特征提取,当然也会减小数据的处理量。最常见的方式有最大值(Max)池化、最小值(Min)池化、平均值(Average)池化。池化的好处是降低了图像的分辨率,整个网络也不容易过拟合。最大值池化如图所示。
    【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
    卷积神经网络的层次结构
    CNN由输入和输出层以及多个隐藏层组成,隐藏层可分为卷积层,池化层、RELU层和全连通层。
  1. 输入层。是整个神经网络的输入,在处理图像的卷积神经网络中,输入为原始图像,三维(RGB)或二维的向量。
  2. 卷积层。CNN的核心,卷积层由一组可学习的滤波器(filter)或内核(kernels)组成,它们具有小的感受野,每个卷积核具有kernel size,padding,stride等参数。从图像的左上角依次做内积操作,提取出图片的高层次特征。一般来说,通过卷积层处理过的节点矩阵会变得更深。
  3. 池化层。对conv后输出的feature map进行下采样操作,这样的好处有降低参数的数量,防止过拟合等作用。不会改变三维矩阵的深度,但是它可以缩小矩阵的大小。
  4. 激活层:在CNN中使用relu激活函数,在网络中引入了非线性。通过relu激活函数传递卷积运算的结果。因此,最终特征映射中的值不是简单的线性关系。
  5. 全连接层:全连接层的输入是一维向量,需要将pooling 层的输出向量flatten成一个一维的向量,然后输入到全连接层中,最后送到softmax层进行类别的分类。

当然中间还可以使用一些其他的功能层:

  • 归一化层(Batch Normalization):在CNN中对特征的归一化

Batch Normalization:Batch Normalization(批量归一化)实现了在神经网络层的中间进行预处理的操作,即在上一层的输入归一化处理后再进入网络的下一层,这样可有效地防止“梯度弥散”,加速网络训练。Batch Normalization具体的算法如下图所示:
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
在每次训练时,取batch_size大小的样本进行训练,在BN层中,将一个神经元看作一个特征,batch_size个样本在某个特征维度会有batch_size个值,然后在每个神经元 x i x_i xi维度上的进行这些样本的均值和方差,通过公式得到 x ^ i \hat{x}_i x^i,再通过参数 γ γ γ β β β进行线性映射得到每个神经元对应的输出 y i y_i yi。在BN层中,可以看出每一个神经元维度上,都会有一个参数 γ γ γ β β β,它们同权重 w w w一样可以通过训练进行优化。
在卷积神经网络中进行批量归一化时,一般对未进行ReLu激活的feature map进行批量归一化,输出后再作为激励层的输入,可达到调整激励函数偏导的作用。
- 一种做法是将feature map中的神经元作为特征维度,参数 γ γ γ β β β的数量和则等于 2 × f m a p w i d t h × f m a p l e n g t h × f m a p n u m 2 × f m a p w i d t h × f m a p l e n g t h × f m a p n u m 2×fmapwidth×fmaplength×fmapnum2×fmapwidth×fmaplength×fmapnum 2×fmapwidth×fmaplength×fmapnum2×fmapwidth×fmaplength×fmapnum,这样做的话参数的数量会变得很多;
- 另一种做法是把一个feature map看做一个特征维度,一个feature map上的神经元共享这个feature map的参数 γ γ γ β β β,参数 γ γ γ β β β的数量和则等于 2 × f m a p n u m 2 × f m a p n u m 2×fmapnum2×fmapnum 2×fmapnum2×fmapnum,计算均值和方差则在batch_size个训练样本在每一个feature map维度上的均值和方差。注:fmapnum指的是一个样本的feature map数量,feature map 跟神经元一样也有一定的排列顺序。

Local Response Normalization: 近邻归一化(Local Response Normalization)的归一化方法主要发生在不同的相邻的卷积核(经过ReLu之后)的输出之间,即输入是发生在不同的经过ReLu之后的 feature map 中。LRN的公式如下:
b ( i , x , y ) = a ( i , x , y ) ( k + α ∑ j = m a x ( 0 , i − n 2 ) m i n ( N − 1 , i + n 2 ) a ( j , x , y ) 2 ) β b(i,x,y)=\frac{a(i,x,y)}{(k+\alpha\sum_{j=max(0,i-\frac{n}{2})}^{min(N-1, i+\frac{n}{2})} a(j,x,y)^2)^\beta} b(i,x,y)=(k+αj=max(0,i2n)min(N1,i+2n)a(j,x,y)2)βa(i,x,y)
其中:

  • a ( i , x , y ) a(i,x,y) a(i,x,y)表示第 i i i个卷积核的输出(经过RelU)层的feature map上的 ( x , y ) (x, y) (x,y)位置上的值。
  • b ( i , x , y ) b(i,x,y) b(i,x,y)表示 a ( i , x , y ) a(i,x,y) a(i,x,y)经LRN后的输出。
  • N是卷积核的数量,即输入的feature map的个数
  • n表示近邻的卷积核(或feature map)个数,由自己来决定
  • k , α , β k,\alpha, \beta k,α,β是超参数,由用户自己调整或决定

与BN的区别:BN依据mini batch的数据近邻归一仅需要自己来决定BN训练中有学习参数BN归一化主要发生在不同的样本之间,LRN归一化主要发生在不同的卷积核的输出之间

  • 切分层:对某些(图片)数据的进行分区域的单独学习
  • 融合层:对独立进行特征学习的分支进行融合,融合的方法有几种,一种是特征矩阵之间的拼接级联,另一种是在特征矩阵进行运算 (+,−,x,max,conv)。

经典卷积网络模型

CNN最早由LeCun 在1998年《Gradient-based learning applied to document recognition》中提出,并提出了一个目标检测的模型:LeNet-5,随后在2012年ImageNet竞赛上,基于CNN网络的AlexNet取得了第一,且正确率超出第二近10%,取得了历史性的突破。CNN开始大放异彩,VGG Net,Google Net,ResNet等,都是基于CNN网络的一些杰出的工作。
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别

LeNet5

由两个卷积层,两个池化层,两个全连接层组成。卷积核都是5×5,stride=1,池化层使用maxpooling
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别

  • 输入层,尺寸大于任何一个字母,以保证每个字母都会出现在第七层单元的感受野的中心。
  • 中间五层分别是:卷积层→降采样层→卷积层→降采样层→卷积层。
  • 第一个卷积层使用了六种滤波器,因此具有六个通道的 feature maps 。
  • 第二个卷积层上升到16个通道。每一个通道与前6个通道的关系都不一样,见上图,目的是破坏对称性,迫使每个通道学习不同的特征(理想情况是互补特征)。
  • 在全连接层,特征进行内积和非线性激活。
  • 最后是输出层,10种数字对应10个输出单元,分别计算输出向量和该分类参考向量的欧式距离。
  • loss 为 MSE loss,输出向量和分类参考向量最近则将其判为这一类。

AlexNet

2012 年 AlexNet 在 ImageNet 2012 图像识别挑战赛上一鸣惊人,证明了学习到的特征可以超越手工特征,结构如下:
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
该网络有以下的创新:

  • ReLU:之前使用的 tanh 和 sigmoid 激活函数都存在饱和区。改用无饱和的 ReLU ,收敛速度可以达到数倍于 tanh !
  • BTraining on Multiple GPU’s: 2个 GPU 协同,最直接的作用是加快了训练速度。作者尝试将网络改为单GPU,同时保证参数数量不变,速度略逊于双 GPUs 。
  • Overlapping Pooling: 重叠池化可以更好地抑制过拟合,使准确率提高约0.4%和0.3%。
  • Data Augmentation: 最简单的抑制过拟合技术,就是 label-preserving transformations 。简单来说,就是让图像进行各种不影响目标本质的变换,扩大数据量。
    • 镜像对称变换
    • 图像光照强度和色彩变换
      • 先提取 RGB 三通道分量;
      • 对每一个通道分别进行主成分分析,提取出主成分;
      • 然后再进行三通道的随机系数线性组合。
  • Dropout:dropout是一种防止过拟合的正则化技术,具体做法是,对每个隐藏层的输入进行一个概率判决,比如我们设置概率为0.5(通常命名为keep_prob),根据0.5,随机生成一个跟隐藏层神经元个数相同的向量,true:false的比例是1:1(因为keep_prob=0.5),与隐藏层的神经元进行相乘,那么会有一半隐藏层的神经元被舍弃,不参与训练。重复迭代上述操作。
    【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别

VGGNet

VGG提出可以通过重复使用简单的基础块来构建深度模型。VGG 块的组成规律是:连续使用多个相同的 padding 为 1,kernel size 为 3×3 的卷积层后,接上一个 stride 为 2,kernel 为 2×2 的最大池化层。卷积层保持输入的尺寸不变,而池化层则对其减半。
AlexNet和 VGGNet的对比如下图所示:
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别

NiN

NiN 是网络中的网络,使用的窗口形状为 11 × 11 11×11 11×11, 5 × 5 5×5 5×5, 3 × 3 3×3 3×3每个NiN 块后接一个 stride 2,窗口 3×3 的最大池化层。最大的不同是 NiN 去掉了 AlexNet 的最后 3 个全连接层,用输出通道数等于标签类别数的 NiN 块,然后用全局平均池化并直接用于分类,可以显著减小模型参数尺寸,缓解过拟合,但是训练时间一般要增加。
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别

GoogleNet(Inception v1)

GoogLeNet 在 2014 年的 ImageNet 图像识别挑战赛中大放异彩,吸收了 NiN 中网络串联网络的思想,并做了很大改进。这个model证明了一件事:用更多的卷积,更深的层次可以得到更好的结构。
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
GoogLeNet 中的基础卷积块叫做 Inception,结构如下图所示:
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
可以看到一个 Inception 块中有 4 条并行的线路,我们可以自定义每层输出的通道数,借此来控制模型复杂度。注:卷积部分使用了 5 个 block,每个 block 之间使用 stride 2 的 3×3 最大池化层来减小输出宽度

Inception-v2:在v1的基础上加入batch normalization技术,在tensorflow中,使用BN在激活函数之前效果更好;将5×5卷积替换成两个连续的3×3卷积,使网络更深,参数更少
Inception-v3:核心思想是将卷积核分解成更小的卷积,如将7×7分解成1×7和7×1两个卷积核,使网络参数减少,深度加深
Inception-v4结构:引入了ResNet,使训练加速,性能提升。但是当滤波器的数目过大(>1000)时,训练很不稳定,可以加入activate scaling因子来缓解

ResNet

VGG证明更深的网络层数是提高精度的有效手段,但是更深的网络极易导致梯度弥散,从而导致网络无法收敛。经测试,20层以上会随着层数增加收敛效果越来越差。ResNet可以很好的解决梯度消失的问题(其实是缓解,并不能真正解决),ResNet增加了shortcut连边。
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
先来看看残差块的设计:
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
左图:输入 x x x,希望虚线部分能够学习出 f ( x ) f(x) f(x)
右图:输入 x x x,希望虚线部分能够学习出 f ( x ) – x f(x) – x f(x)x,更容易学习, x x x也可以通过跨层的数据线路更快向前传播

这个思路非常深刻影响了未来的神经网络设计,为了能够让 f ( x ) − x f(x)-x f(x)x x x x相加,可能需要引入 1 × 1 1×1 1×1的卷积层来变化尺寸。

DenseNet

DenseNet 可以看作是 ResNet 的发展创新,对比如下:
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别

  • 左边:ResNet,A 的输出和 B 的输出相加(这就需要维度一致)
  • 右边:DenseNet,A 的输出和 B 的输出拼起来
    DenseNet 主要通过 dense block(确定输入和输入如何连接)和 transititon layer(控制通道数量)构成。
    DenseNet通过特征重用来大幅减少网络的参数量,又在一定程度上缓解了梯度消失问题

卷积神经网络迁移学习

所谓迁移学习,就是将一个问题上训练好的模型通过简单的调整使其适用于一个新的问题。

一般来说,在数据量足够的情况下,迁移学习的效果不如完全重新训练。但是迁移学习所需要的训练事件和训练样本数要远远小于训练完整的模型。

建模流程实战

数据准备

cifar2数据集为cifar10数据集的子集,只包括前两种类别airplane和automobile。训练集有airplane和automobile图片各5000张,测试集有airplane和automobile图片各1000张。
cifar2任务的目标是训练一个模型来对飞机airplane和机动车automobile两种图片进行分类
Cifar2数据集的文件结构如下所示:
【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
在Tensorflow中准备图片数据的常用方案有两种:

  • 第一种是使用tf.keras中的ImageDataGenerator工具构建图片数据生成器。
  • 第二种是使用tf.data.Dataset搭配tf.image中的一些图片处理方法构建数据管道。

第二种方法是TensorFlow的原生方法,更加灵活,使用得当的话也可以获得更好的性能。

import tensorflow as tf
from tensorflow.keras import datasets, layers, models

BATCH_SIZE = 100

def load_image(img_path, size=(32,32)):
    label = tf.constant(1, tf.int8) if tf.strings.regex_full_match(img_path,".*automobile.*") else tf.constant(0, tf.int8)
    img = tf.io.read_file(img_path)
    img = tf.image.decode_jpeg(img)
    img = tf.image.resize(img, size)/255.0
    return img, label
ds_train = tf.data.Dataset.list_files("../DemoData/cifar2/train/*/*.jpg").map(load_image, num_parallel_calls=tf.data.experimental.AUTOTUNE).shuffle(buffer_size=1000).batch(BATCH_SIZE).prefetch(tf.data.experimental.AUTOTUNE)
ds_test = tf.data.Dataset.list_files("../DemoData/cifar2/test/*/*.jpg").map(load_image, num_parallel_calls=tf.data.experimental.AUTOTUNE).batch(BATCH_SIZE).prefetch(tf.data.experimental.AUTOTUNE)
%matplotlib inline
%config InlineBackend.figure_format = 'png'

# 查看部分样本
from matplotlib import pyplot as plt

plt.figure(figsize=(8, 8))
for i, (img, label) in enumerate(ds_train.unbatch().take(9)):
    ax = plt.subplot(3,3,i+1)
    ax.imshow(img.numpy())
    ax.set_title("label= %d"%label)
    ax.set_xticks([])
    ax.set_yticks([])
plt.show()

【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别

for x, y in ds_train.take(1):
    print(x.shape, y.shape) # (100, 32, 32, 3) (100,)

定义模型

使用Keras接口有以下3种方式构建模型:

  1. 使用Sequential按层顺序构建模型,
  2. 使用函数式API构建任意结构模型,
  3. 继承Model基类构建自定义模型。

这里选择使用函数式API构建模型

# 定义模型
tf.keras.backend.clear_session() # 清空会话

inputs = layers.Input(shape=(32,32,3))
x = layers.Conv2D(32, kernel_size=(3,3))(inputs)
x = layers.MaxPool2D()(x)
x = layers.Conv2D(64, kernel_size=(5,5))(x)
x = layers.MaxPool2D()(x)
x = layers.Dropout(rate=0.1)(x)
x = layers.Flatten()(x)
x = layers.Dense(32, activation='relu')(x)
outputs = layers.Dense(1, activation='sigmoid')(x)

model = models.Model(inputs=inputs, outputs=outputs)
model.summary()
Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 32, 32, 3)]       0                 
 conv2d (Conv2D)             (None, 30, 30, 32)        896       
 max_pooling2d (MaxPooling2D)  (None, 15, 15, 32)       0                                                           
 conv2d_1 (Conv2D)           (None, 11, 11, 64)        51264     
 max_pooling2d_1 (MaxPooling2D)  (None, 5, 5, 64)         0         
 dropout (Dropout)           (None, 5, 5, 64)          0          
 flatten (Flatten)           (None, 1600)              0         
 dense (Dense)               (None, 32)                51232       
 dense_1 (Dense)             (None, 1)                 33        
=================================================================
Total params: 103,425
Trainable params: 103,425
Non-trainable params: 0
_________________________________________________________________

训练模型

训练模型通常有3种方法:①内置fit方法,②内置train_on_batch方法,以及③自定义训练循环。这里选择最常用也最简单的内置fit方法。

import datetime
import os

stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
logdir = os.path.join('data', 'autograph', stamp)

## 在 Python3 下建议使用 pathlib 修正各操作系统的路径
# from pathlib import Path
# stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
# logdir = str(Path('../../data/autograph/' + stamp))

tensorboard_callback = tf.keras.callbacks.TensorBoard(logdir, histogram_freq=1)

model.compile(
        optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
        loss=tf.keras.losses.binary_crossentropy,
        metrics=["accuracy"]
    )

history = model.fit(ds_train,epochs= 10,validation_data=ds_test,
                    callbacks = [tensorboard_callback],workers = 4)

这里出现问题:因为采用的是jupyter notebook server运行方式,在执行cell之后,kernel重新启动了,没有执行成功。建议使用本地jupyter notebook 运行方式。

评估模型

%load_ext tensorboard
#%tensorboard --logdir ../../data/keras_model
from tensorboard import notebook
notebook.list() 
#在tensorboard中查看模型
notebook.start("--logdir ../../data/keras_model")

【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
打印运行记录:

import pandas as pd 
dfhistory = pd.DataFrame(history.history)
dfhistory.index = range(1,len(dfhistory) + 1)
dfhistory.index.name = 'epoch'

dfhistory

绘制训练损失和验证损失:

%matplotlib inline
%config InlineBackend.figure_format = 'svg'

import matplotlib.pyplot as plt

def plot_metric(history, metric):
    train_metrics = history.history[metric]
    val_metrics = history.history['val_'+metric]
    epochs = range(1, len(train_metrics) + 1)
    plt.plot(epochs, train_metrics, 'bo--')
    plt.plot(epochs, val_metrics, 'ro-')
    plt.title('Training and validation '+ metric)
    plt.xlabel("Epochs")
    plt.ylabel(metric)
    plt.legend(["train_"+metric, 'val_'+metric])
    plt.show()

plot_metric(history,"loss")

【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
绘制精确度变化曲线:

plot_metric(history,"accuracy")

【再学Tensorflow2】TensorFlow2的建模流程:CIFAR2图像识别
使用evaluate对数据进行评估

#可以使用evaluate对数据进行评估
val_loss,val_accuracy = model.evaluate(ds_test,workers=4)
print(val_loss,val_accuracy)

使用模型

可以使用model.predict(ds_test)进行预测:

model.predict(ds_test)

或者也可以使用model.predict_on_batch(x_test)对一个批量进行预测。

for x,y in ds_test.take(1):
    print(model.predict_on_batch(x[0:20]))

保存模型

推荐使用TensorFlow原生方式保存模型:

# 保存权重,该方式仅仅保存权重张量
model.save_weights('../../data/tf_model_weights.ckpt',save_format = "tf")

保存模型结构与模型参数到文件,该方式保存的模型具有跨平台性便于部署:

model.save('../../data/tf_model_savedmodel', save_format="tf")
print('export saved model.')

model_loaded = tf.keras.models.load_model('../../data/tf_model_savedmodel')
model_loaded.evaluate(ds_test)

参考资料

[1] 《30天吃掉那只Tensorflow2》
[2] CNN网络结构的发展
[3] 深度学习算法之卷积神经网络(CNN)