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

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

1. 网络简介

网络复现代码见下文:神经网络学习小记录35——Inception ResnetV2模型的复现详解_Bubbliiiing的学习小课堂-CSDN博客_inceptionresnetv2模型大小本节使用迁移学习,不复现网络

一、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('预测值')

# 由于我中文显示乱码下图用英文注释代替

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

  • 5
    点赞
  • 78
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

立Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值