各位同学好,今天和大家分享一下Tensorflow2.0中使用迁移学习的方法,搭载InceptionResNetV2网络进行交通标志分类识别。
网络复现代码见下文:神经网络学习小记录35——Inception ResnetV2模型的复现详解_Bubbliiiing的学习小课堂-CSDN博客_inceptionresnetv2模型大小。本节使用迁移学习,不复现网络
基本思想:不需要人为决定使用哪个过滤器,或是否需要池化,而是由网络自行确定这些参数,你可以给网络添加这些参数的所有可能值,然后把这些输出连接起来,让网络自己学习它需要什么样的参数,采用哪些过滤器组合。
细节:网络中存在softmax分支,即便是隐藏单元和中间层也参与了特征计算,它们也能预测图片的分类,它在Inception网络中起到一种调整的效果,防止过拟合。
残差网络就是残差块的堆叠,这样可以把网络设计的很深;残差网络和普通网络的差异是,在a进行非线性变化前,把a的数据拷贝了一份,a与b累加后的结果进行非线性变换。
对于普通的卷积网络,用梯度下降等常用的优化算法,随着网络深度的增加,训练误差会呈现出先降低后增加的趋势,而我们期望的理想结果是随着网络深度的增加训练误差逐渐减小,而Resnet随着网络深度的增加训练误差会一直减小。
1、降维。比如,一张500 * 500且通道数为100 的图片在20个卷积核上做1*1的卷积,那么结果的大小为500*500*20。
2、加入非线性。卷积层之后经过激励层,1*1的卷积在前一层的学习表示上添加了非线性函数,提升网络的表达能力;可以在保持feature map尺度不变的(即不损失分辨率)的前提下大幅增加非线性特性(利用后接的非线性激活函数),把网络做的很深。
当1*1卷积出现时,在大多数情况下它作用是升/降特征的维度,这里的维度指的是通道数(厚度),而不改变图片的宽和高。
InceptionResNetV2网络结构如下图所示:
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()
展示交通标志图像如下,共有四分类:['禁令标志', '警示标志', '通行标志', '限速标志']
在预处理函数中,需要将每张图像的像素值从[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)
迁移学习函数可见: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,即所有层在模型训练的正方向传播过程中,权重参数都不会发生变化,不进行更新。
首先我们通过迁移学习导入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
__________________________________________________________________________________________________
从网络结构图中我们看到,特征提取层最后的输出维度是[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)网络配置,调小学习率避免过拟合
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)网络评估
# 准确率
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()
从测试集中取一个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('预测值')
# 由于我中文显示乱码下图用英文注释代替
从图中可以看到测试集基本都预测对了