Press "Enter" to skip to content

【神经网络】(9) 迁移学习(CNN-InceptionResNetV2),案例:交通标志4分类

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

各位同学好,今天和大家分享一下Tensorflow2.0中使用迁移学习的方法,搭载InceptionResNetV2网络进行交通标志分类识别。

 

1. 网络简介

 

网络复现代码见下文: https://blog.csdn.net/weixin_44791964/article/details/103732720 。 本节使用迁移学习,不复现网络

 

一、Inception

 

基本思想: 不需要人为决定使用哪个过滤器,或是否需要池化,而是由网络自行确定这些参数,你可以给网络添加这些参数的所有可能值,然后把这些输出连接起来,让网络自己学习它需要什幺样的参数,采用哪些过滤器组合。

 

细节: 网络中存在softmax分支,即便是隐藏单元和中间层也参与了特征计算,它们也能预测图片的分类,它在Inception网络中起到一种调整的效果,防止过拟合。

 

二、Resnet

 

残差网络就是残差块的堆叠,这样可以把网络设计的很深;残差网络和普通网络的差异是,在a进行非线性变化前,把a的数据拷贝了一份,a与b累加后的结果进行非线性变换。

 

对于普通的卷积网络,用梯度下降等常用的优化算法,随着网络深度的增加,训练误差会呈现出先降低后增加的趋势,而我们期望的理想结果是随着网络深度的增加训练误差逐渐减小,而 Resnet随着网络深度的增加训练误差会一直减小 。

 

三、1*1卷积的主要作用有以下几点:

 

1、降维 。比如,一张500 * 500且通道数为100 的图片在20个卷积核上做1*1的卷积,那幺结果的大小为500*500*20。

 

2、加入非线性 。卷积层之后经过激励层,1*1的卷积在前一层的学习表示上添加了非线性函数,提升网络的表达能力;可以在保持feature map尺度不变的(即不损失分辨率)的前提下大幅增加非线性特性(利用后接的非线性激活函数),把网络做的很深。

 

当1*1卷积出现时,在大多数情况下 它作用是升/降特征的维度,这里的维度指的是通道数(厚度),而不改变图片的宽和高。

 

InceptionResNetV2网络结构如下图所示:

 

 

2. 数据加载

 

import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import Model, layers, optimizers
#(1)数据获取
def get_data(height, width, batchsz):
    # 获取训练集
    filepath1 = 'C:/Users/admin/.spyder-py3/test/数据集/交通标志/new_data/train'
    train_ds = tf.keras.preprocessing.image_dataset_from_directory(
        filepath1, # 指定训练集数据路径
        label_mode = 'categorical',  # 导入的目标数据,进行onehot编码
        image_size = (height, width), # 对图像resize
        batch_size = batchsz, # 每次迭代取32个数据 
        )
    
    # 获取验证集数据
    filepath2 = 'C:/Users/admin/.spyder-py3/test/数据集/交通标志/new_data/val'
    val_ds = tf.keras.preprocessing.image_dataset_from_directory(
        filepath2, # 指定训练集数据路径
        label_mode = 'categorical',  
        image_size = (height, width), # 对图像resize
        batch_size = batchsz, # 每次迭代取32个数据 
        )  
    
    # 获取验证集数据
    filepath3 = 'C:/Users/admin/.spyder-py3/test/数据集/交通标志/new_data/test'
    test_ds = tf.keras.preprocessing.image_dataset_from_directory(
        filepath3, # 指定训练集数据路径
        label_mode = 'int',  # 不进行onehot编码
        image_size = (height, width), # 对图像resize
        batch_size = batchsz, # 每次迭代取32个数据 
        )     
    
    return(train_ds, val_ds, test_ds)
# 数据读取函数,返回训练集、验证集、测试集
train_ds, val_ds, test_ds = get_data(128,128,32)
# 查看分类名称
class_names = train_ds.class_names
print('分类名:', class_names)
# 分类名: ['禁令标志', '警示标志', '通行标志', '限速标志']
# 查看数据信息
sample = next(iter(train_ds))
print('x_batch.shape:', sample[0].shape, 'y_batch.shape:', sample[1].shape)
# x_batch.shape: (32, 128, 128, 3) y_batch.shape: (32, 4)
#显示图像
import matplotlib.pyplot as plt
for i in range(15):
    plt.subplot(3,5,i+1)
    plt.imshow(sample[0][i]/255.0)
    plt.xticks([])
    plt.yticks([])
plt.show()

 

展示交通标志图像如下,共有四分类:[‘禁令标志’, ‘警示标志’, ‘通行标志’, ‘限速标志’]

 

 

2. 数据预处理

在预处理函数中,需要将每张图像的像素值
从[0, 255]映射到[-1, 1]

,因为使用迁移学习

InceptionResNetV2时,官方规定了输入预处理要求。使用

.map()
方法对数据集中的所有元素执行函数内容。
iter()
构造迭代器,
next()
执行一次迭代器,每次运行都只会 取一个batch的数据 。

#(2)数据预处理
def processing(x,y):
    # 图像中的每个像素值映射到[-1,1]之间
    x = 2 * tf.cast(x, dtype=tf.float32) / 255.0 - 1
    y = tf.cast(y, dtype=tf.int32)
    # 返回处理后的结果
    return (x,y) 
# 构建数据集
train_ds = train_ds.map(processing).shuffle(10000)
val_ds = val_ds.map(processing)  #验证集和测试集不需要打乱顺序
test_ds = test_ds.map(processing)
# 查看数据信息
sample = next(iter(train_ds))
print('x_batch.shape:', sample[0].shape, 'y_batch.shape:', sample[1].shape)
# x_batch.shape: (32, 128, 128, 3) y_batch.shape: (32, 4)

 

3. 迁移学习

 

迁移学习函数可见: Module: tf.keras.applications  |  TensorFlow Core v2.7.0 (google.cn)

 

使用迁移学习,可以 直接获取官方已经构建好了的网络模型架构,以及训练好的权重参数 ,可能这些参数和我们处理的问题不太一样。但相比我们建模过程中采用随机初始化的权重参数,预训练的权重参数会让我们的模型训练速度更快,准确率提高,只要稍作调整就能达到很好的效果。

 

预训练模型是一个之前基于大型数据集(通常是大型图像分类任务)训练的已保存网络。您可以按原样使用预训练模型,也可以使用迁移学习针对给定任务自定义此模型。

 

迁移学习的理念 是,如果一个模型是基于足够大且通用的数据集训练的,那幺该模型将有效地充当视觉世界的通用模型。随后, 您可以利用这些学习到的特征映射,而不必通过基于大型数据集训练大型模型而从头开始。

 

在进行迁移学习时,我们一般有两种方法,法一:取卷积部分作为权重参数初始化,并继续进行训练;练法二:把别人训练好的层冻住,只用于特征提取,权重参数不更新。

 

一般全连接层按自己的方式重新定义,把前面的特征提取的部分微调

 

tf.keras.applications.inception_resnet_v2.InceptionResNetV2(
    include_top=True, weights='imagenet', input_tensor=None,
    input_shape=None, pooling=None, classes=1000,
    classifier_activation='softmax', **kwargs)

 

input_shape: 代表我们自己的输入图像的大小,某些模型对输入的大小有要求。 include_top: 是否要导入模型的全连接层部分,一般不需要,全连接层根据自己的任务来自定义,默认有1000个输出。 weights: 加载模型的权重参数,可以是: None 不加载; ‘imagenet’ 官方训练好的权重; path 自己找到的权重的文件路径。

 

实例化一个已预加载基于 ImageNet 训练的权重的 MobileNet V2 模型。通过指定 include_top=False 参数,可以加载不包括顶部分类层的网络,这对于特征提取十分理想。

 

layer遍历预训练模型的所有层,使所有层的 layer.trainable = False ,即 所有层在模型训练的正方向传播过程中,权重参数都不会发生变化,不进行更新。

 

4. 微调网络

 

首先我们通过迁移学习导入InceptionResNetV2网络,遍历所有层使所有权重参数冻结,在模型训练过程中不变,并且不要网络的全连接层部分,默认的全连接层是1000分类,不适用于我们这个案例,全连接层自己写。

 

#(3)迁移学习InceptionResNetV2
pre_model = keras.applications.inception_resnet_v2.InceptionResNetV2(
    # 不包括全连接层,导入预训练权重,自定义输入图片大小
    include_top=False, weights='imagenet',  input_shape=[128,128,3]
    )
# 冻住特征提取层
for layer in pre_model.layers:
    layer.trainable = False  #正反向传播过程中权重参数不更新
# 进行一次前向传播,看图像有何改变
imgs, labels = next(iter(train_ds)) # 获取训练集的某一个batch的图像及标签
res = pre_model(imgs)  
print('特征提取层输出结果为:', res.shape)
# [32,128,128,3] ==> [32,2,2,1536]

 

将网络的部分层解冻,即在网络的循环过程中,被解冻的层的权重参数可以随着网络迭代而不断被优化,不但减少了训练时间,还提高了网络精度。该网络一共有780层,冻结前500层,具体可根据自己的任务要求来定。

 

#(4)微调网络
# 解冻所有网络层
pre_model.trainable = True
# 查看一共有多少层:780
print('numbers of layers:', len(pre_model.layers))
find_tune_at = 500  # 指定冻结前500层
# 前500层在正方向传播过程中参数不能变,剩下的层权重参数可以调整
for layer in pre_model.layers[:find_tune_at]:  # 包括第500层
    layer.trainable = False
# 查看网络参数,Trainable params: 34,038,336
pre_model.summary()

 

我们可以看到,解冻部分层后可训练的权重参数明显增加,Trainable params: 34,038,336

 

-------------------------------------------------
省略 N 层
-------------------------------------------------- 
block8_10_mixed (Concatenate)  (None, 2, 2, 448)    0           ['activation_808[0][0]',         
                                                                  'activation_811[0][0]']         
                                                                                                  
 block8_10_conv (Conv2D)        (None, 2, 2, 2080)   933920      ['block8_10_mixed[0][0]']        
                                                                                                  
 block8_10 (Lambda)             (None, 2, 2, 2080)   0           ['block8_9_ac[0][0]',            
                                                                  'block8_10_conv[0][0]']         
                                                                                                  
 conv_7b (Conv2D)               (None, 2, 2, 1536)   3194880     ['block8_10[0][0]']              
                                                                                                  
 conv_7b_bn (BatchNormalization  (None, 2, 2, 1536)  4608        ['conv_7b[0][0]']                
 )                                                                                                
                                                                                                  
 conv_7b_ac (Activation)        (None, 2, 2, 1536)   0           ['conv_7b_bn[0][0]']             
                                                                                                  
==================================================================================================
Total params: 54,336,736
Trainable params: 34,038,336
Non-trainable params: 20,298,400
__________________________________________________________________________________________________

 

5. 构造输出层

 

从网络结构图中我们看到, 特征提取层 最后的输出维度是 [None,2,2,1536] ,我们需要将输出特征压平后送入到输出层,使用 全局平均池化 方法 GlobalAveragePooling2D() ,将输出维度从[None,2,2,1536] 变换到  [None,1536] ,送入全连接层,最后通过 softmax 函数得到 图片属于4个分类的概率。

 

#(4)构造输出层
# 对特征提取层的输出进行全局平均池化
# [None,2,2,1536] ==> [None,1536]
x = layers.GlobalAveragePooling2D()(pre_model.output)
# 输出层分7类
x = layers.Dense(4, activation='softmax')(x)
# 构建模型
model = Model(pre_model.input, x)

 

6. 网络训练

 

#(6)网络配置,调小学习率避免过拟合
opt = optimizers.Adam(learning_rate=1e-7)
# 编译
model.compile(optimizer=opt,  # 学习率
              loss = 'categorical_crossentropy',  # 对onehot后的y,计算交叉熵损失
              metrics = ['accuracy'])  # 评价指标
# 训练,在上一次训练的基础上继续训练10次
history_fine = model.fit(train_ds,  # 训练集
          validation_data = val_ds, # 验证集 
          epochs = 60, # 迭代次数
          )

 

网络运行结果如下:

 

Epoch 1/60
139/139 [==============================] - 34s 151ms/step - loss: 1.4587 - accuracy: 0.2529 - val_loss: 1.4768 - val_accuracy: 0.2797
Epoch 2/60
139/139 [==============================] - 19s 128ms/step - loss: 1.4268 - accuracy: 0.2747 - val_loss: 1.4078 - val_accuracy: 0.3061
Epoch 3/60
139/139 [==============================] - 19s 124ms/step - loss: 1.3889 - accuracy: 0.3055 - val_loss: 1.3761 - val_accuracy: 0.3217
----------------------------------------------------
省略 N 层
----------------------------------------------------
Epoch 58/60
139/139 [==============================] - 19s 127ms/step - loss: 0.3505 - accuracy: 0.9519 - val_loss: 0.2896 - val_accuracy: 0.9712
Epoch 59/60
139/139 [==============================] - 19s 126ms/step - loss: 0.3334 - accuracy: 0.9597 - val_loss: 0.2829 - val_accuracy: 0.9724
Epoch 60/60
139/139 [==============================] - 20s 131ms/step - loss: 0.3291 - accuracy: 0.9561 - val_loss: 0.2758 - val_accuracy: 0.9736

 

7. 网络评估

 

绘制训练过程中的准确率和损失曲线,如下图所示可见模型效果很好

 

#(7)网络评估
# 准确率
train_acc = history_fine.history['accuracy']
test_acc = history_fine.history['val_accuracy']
# 损失
train_loss = history_fine.history['loss']
test_loss = history_fine.history['val_loss']
# 绘图
# 准确率曲线
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.plot(train_acc, label='train_acc')
plt.plot(test_acc, label='test_acc')
plt.title('accuracy')
plt.legend() #显示图例label
# 损失曲线
plt.subplot(1, 2, 2)
plt.plot(train_loss, label='train_loss')
plt.plot(test_loss, label='test_loss')
plt.title('loss')
plt.legend()
plt.show()

 

 

8. 预测

 

从测试集中取一个batch用于网络的预测,通过 model.predict() 方法,返回每张图片属于4个分类的概率,通过 np.argmax() 找到概率最大的值的下标索引,该索引对应 class_names 列表中的标签名。

 

#(10)预测
test_true = []  # 存放真实值
test_pred = []  # 存放预测值
# 从测试集中取出一个batch用于预测
for imgs, labels in test_ds: # label没有经过onehot编码
    # 每次取出一组图像和标签
    for img, label in zip(imgs, labels):
        # 给图像增加一个维度
        image_array = tf.expand_dims(img, 0)
        # 预测某一张图片的类别
        prediction = model.predict(image_array)
        
        # 找到预测结果中元素值最大的下标,图像属于某一个类别概率最大的值对应的下标
        test_pred.append(class_names[np.argmax(prediction)])
        test_true.append(class_names[label])  # 真实值的标签名
# 展示结果
print('真实值:', test_true)
print('预测值:', test_pred)

 

真实值: ['限速标志', '限速标志', '限速标志', '警示标志', '禁令标志', '通行标志', '警示标志', '通行标志', '通行标志', '禁令标志']
预测值: ['限速标志', '限速标志', '限速标志', '警示标志', '禁令标志', '通行标志', '警示标志', '通行标志', '通行标志', '禁令标志']

 

为了更清晰的看出测试数据中是否所有的真实值都和预测值相同,需绘制一个混淆矩阵。

 

#(8)混淆矩阵
from sklearn.metrics import confusion_matrix
import seaborn as sns
import pandas as pd
plt.rcParams['font.sans-serif'] = ['SimSun']  #宋体
plt.rcParams['font.size'] = 15  #设置字体大小
 
# 生成混淆矩阵
conf_numpy = confusion_matrix(test_true, test_pred)
# 将矩阵转化为 DataFrame
conf_df = pd.DataFrame(conf_numpy, index=class_names ,columns=class_names)  
    
plt.figure(figsize=(8,7))
    
sns.heatmap(conf_df, annot=True, fmt="d", cmap="BuPu")
    
plt.title('混淆矩阵')
plt.ylabel('真实值')
plt.xlabel('预测值')
# 由于我中文显示乱码下图用英文注释代替

 

从图中可以看到测试集基本都预测对了

 

Be First to Comment

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注