各位同学好,今天和大家分享一下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