【神经网络】(7) 迁移学习(CNN-MobileNetV2),案例:乳腺癌二分类

各位同学好,今天和大家分享一下Tensorflow2.0中如何使用迁移学习的方法构造神经网络。需要数据集的在评论区留个言。

1. 迁移学习

官方文档:Module: tf.keras.applications  |  TensorFlow Core v2.7.0 (google.cn)

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

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

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

官方提供了许多经典的网络结构,在上面的文档中可仔细研究。

tf.keras.applications.resnet50.ResNet50
tf.keras.applications.resnet.ResNet101
tf.keras.applications.inception_v3.InceptionV3
tf.keras.applications.inception_resnet_v2.InceptionResNetV2
。。。。。。。

本节我使用迁移学习导入MobileNetV2网络,进行乳腺癌图像二分类的研究。下面介绍两种方法,一是将所有特征提取层的权重冻住,训练过程中不进行更新参数;二是冻住部分层,解冻的层可以更新权重参数。


2. 数据加载

使用tf.keras.preprocessing.image_dataset_from_directory()方法,在指定的文件夹中获取图像数据。其中label_model参数中,'int'表示标签被编码成整数(例如:sparse_categorical_crossentropy loss)。'categorical':多分类,指标签被编码为分类向量(例如:categorical_crossentropy loss)。'binary':二分类,意味着标签(只能有2个)被编码为值为0或1的float32标量(例如:binary_crossentropy)。None(无标签)。

import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import Sequential, optimizers, layers, Model
import matplotlib.pyplot as plt

def get_data(height, width, batchsz):
    # 获取训练集
    filepath1 = '/home/featurize/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 = '/home/featurize/data/val'
    test_ds = tf.keras.preprocessing.image_dataset_from_directory(
        filepath2, # 指定训练集数据路径
        label_mode = 'categorical',  
        image_size = (height, width), # 对图像resize
        batch_size = batchsz, # 每次迭代取32个数据 
        )  
    
    return(train_ds, test_ds)

# 数据读取函数,返回训练集、验证集、测试集
train_ds, test_ds = get_data(height=50, width=50, batchsz=32)

3. 数据可视化

iter()生成一个迭代器,配合next()使用,每运行一次,就从指定数据集中取出一个batch的数据,返回图像数据sample[0]标签数据sample[1]

# 查看导入数据的信息
sample = next(iter(train_ds))
print('x_batch.shape:', sample[0].shape, 'y_batch.shape:', sample[1].shape)
# x_batch.shape: (32, 50, 50, 3) y_batch.shape: (32, 2)
print('y[:5]:', sample[1][:5])
# [[1. 0.] [1. 0.] [1. 0.] [0. 1.] [0. 1.]]

# 绘图查看信息,1代表得了乳腺癌,0代表没得乳腺癌
import matplotlib.pyplot as plt
for i in range(15):
    plt.subplot(3,5,i+1)
    plt.imshow(sample[0][i]/255.0) #把图片像素压缩到0-1之间显示图像,不压缩matpolt好像画不了
    plt.xticks([]) # 不显示坐标轴
    plt.yticks([])
plt.show()

【神经网络】(7) 迁移学习(CNN-MobileNetV2),案例:乳腺癌二分类_第1张图片


4. 数据预处理

对训练集和测试集的数据集改变数据类型,并且将图像数据从[0,255]映射到[-1,1]之间,因为我们使用的预训练的MobileNetV2模型对输入有要求是在[-1,1]之间,我们需要和它保持一致,具体可以看官方的文档。tf.keras.applications.mobilenet_v2.MobileNetV2  |  TensorFlow Core v2.7.0 (google.cn)

# 数据预处理
def processing(x,y):
    x = 2*tf.cast(x, dtype=tf.float32)/255.0-1  #映射到-1-1之间
    y = tf.cast(y, tf.int32)
    return(x,y)

# 预处理训练集和验证集
train_ds = train_ds.map(processing).shuffle(10000)
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, 50, 50, 3) y_batch.shape: (32, 2)

5. 迁移学习--冻结所有特征提取层

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

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

当数据量小的时候(<1W),越需要借助别人训练好的模型,需要冻住的层也就越多。数据量大且任务不一样时只进行权重参数初始化,而不冻住这些层。

官方文档:迁移学习和微调  |  TensorFlow Core (google.cn)

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

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

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

# 迁移学习
pre_model = keras.applications.mobilenet_v2.MobileNetV2(
    input_shape=[50, 50, 3], include_top=False, weights='imagenet')

# 冻住特征提取层
for layer in pre_model.layers:
    layer.trainable = False

# 前向传播,看网络输出什么
imgs, labels = next(iter(train_ds))  #img的shape为[32,224,224,3]
outputs = pre_model(imgs) 
print('outputs.shape:', outputs.shape)
# [32,224,224,3] ==> [32,7,7,1280]

5.1 模型构建

我们获取的pre_model预训练模型已经包含了输入层、特征提取层,可以通过pre_model.input得到模型的输入,pre_model.output得到模型的特征提取层的输出结果。输出结果是一个形状为[b, h, w ,c]的tensor变量,将它输入全连接层之前需要把这个tensor压平,变成二维的[b, n]类型。

加入Flatten()层将输出结果压平,设计一个全连接层,进一步特征提取,输出1024个特征,加入Dropout层防止过拟合,最后由于本问题是二分类,采用sigmoid函数来判断预测结果。也可以使用softmax函数。

keras.Model容器中只需指定输入层和输出层就可以构建网络,中间层通过函数的方式连接在一起。

# 自定义顶层
# 将特征提取层的输出结果压平后再传入全连接层
x = layers.Flatten()(pre_model.output) 
# 加入全连接层
x = layers.Dense(1024, activation='relu')(x)
x = layers.Dropout(0.4)(x) #0.4的几率杀死神经元
# 输出层,输出图像属于10分类的概率
x = layers.Dense(2, activation='sigmoid')(x)

# 模型构建
model = Model(pre_model.input, x) # 给出输入层和输出层

# 查看网络结构
pre_model.summary()

预训练网络模型结构如下,我们可以看到,Trainable params: 0,我们冻住了所有的特征提取层的权重,特征提取层不会再进行更新了,只有全连接层的权重会更新。我们先使用这个模型来训练一下,看一下结果。

Model: "mobilenetv2_1.00_224"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_1 (InputLayer)           [(None, 50, 50, 3)]  0           []                               
                                                                                                  
 Conv1 (Conv2D)                 (None, 25, 25, 32)   864         ['input_1[0][0]']                
                                                                                                  
 bn_Conv1 (BatchNormalization)  (None, 25, 25, 32)   128         ['Conv1[0][0]']                  
                                                                                                  
 Conv1_relu (ReLU)              (None, 25, 25, 32)   0           ['bn_Conv1[0][0]']               
                                                                                                  
 expanded_conv_depthwise (Depth  (None, 25, 25, 32)  288         ['Conv1_relu[0][0]']             
 wiseConv2D)                                                                                      

----------------------
省略 N 层
----------------------
                                                                                                  
 block_16_project (Conv2D)      (None, 2, 2, 320)    307200      ['block_16_depthwise_relu[0][0]']
                                                                                                  
 block_16_project_BN (BatchNorm  (None, 2, 2, 320)   1280        ['block_16_project[0][0]']       
 alization)                                                                                       
                                                                                                  
 Conv_1 (Conv2D)                (None, 2, 2, 1280)   409600      ['block_16_project_BN[0][0]']    
                                                                                                  
 Conv_1_bn (BatchNormalization)  (None, 2, 2, 1280)  5120        ['Conv_1[0][0]']                 
                                                                                                  
 out_relu (ReLU)                (None, 2, 2, 1280)   0           ['Conv_1_bn[0][0]']              
                                                                                                  
==================================================================================================
Total params: 2,257,984
Trainable params: 0
Non-trainable params: 2,257,984
__________________________________________________________________________________________________

5.2 网络配置

# ==1== 网络配置
# 编译
model.compile(optimizer = optimizers.Adam(learning_rate=0.00001), #指定学习率,即梯度下降速率
            loss = 'binary_crossentropy', #二分类交叉熵损失
            metrics = ['acc']  # 网络评价指标
             )

# ==2== 模型训练
# model.fit_generator 用于大型数据集,有时直接读图片会导致内存占用过高
history = model.fit(
    train_ds, #训练集
    validation_data = test_ds, #测试集
    epochs = 10, #迭代10次
) 

模型训练结果如下,经过10次迭代后,测试集准确率上升了5%,损失变化较为平缓

Epoch 1/10
329/329 [==============================] - 11s 23ms/step - loss: 0.5772 - acc: 0.7344 - val_loss: 0.4576 - val_acc: 0.7870
Epoch 2/10
329/329 [==============================] - 7s 21ms/step - loss: 0.4784 - acc: 0.7915 - val_loss: 0.4353 - val_acc: 0.8011
------------------------------------------
省略 N 层
------------------------------------------
Epoch 10/10
329/329 [==============================] - 7s 21ms/step - loss: 0.3424 - acc: 0.8560 - val_loss: 0.3976 - val_acc: 0.8258

5.3 模型评估可视化

通过.history获取模型训练后保存的准确率和损失信息,绘制准确率和损失曲线,如下图。

# 模型评估
# 准确率
train_acc = history.history['acc']
test_acc = history.history['val_acc']
# 损失
train_loss = history.history['loss']
test_loss = history.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()

【神经网络】(7) 迁移学习(CNN-MobileNetV2),案例:乳腺癌二分类_第2张图片


6. 迁移学习--微调网络

上面介绍了冻住所有的特征提取层的迁移学习方法,现在来介绍只冻住部分特征提取层。解冻的层layer.trainable = True,可以在正反向传播的过程中更新权重参数,而被冻住的层layer.trainable = False不能更新权重参数。

微调网络可以提高我们的模型的精度,由于我们自己的任务和预训练模型解决的任务不相同,因此用一样的权重参数肯定达不到理想的效果。具体解封多少层需要按照自己的数据量来定,如果数据量很少,那就越需要依靠预训练模型的权重参数。如果数据量大,或任务相差很大的话,也需要解封很多层。

在大多数卷积网络中,层越高,它的专门程度就越高。前几层学习非常简单且通用的特征,这些特征可以泛化到几乎所有类型的图像。随着您向上层移动,这些特征越来越特定于训练模型所使用的数据集。微调的目标是使这些专用特征适应新的数据集,而不是覆盖通用学习。


6.1 选择部分层冻结

首先我们需要将之前冻结的层全部解开pre_model.trainable = True,在这个网络中有154层,那我们就冻结到第120层(包括第120层)。具体冻结到几层,自己调控。使用一个for循环遍历前120层,指定layer.trainable = False就可以将它们的权重参数都冻住了,正方向传播都不会更新了。而解冻的就可以改变权重参数优化我们的网络了。

# 将所有层解冻
pre_model.trainable = True
# 查看一共有多少层
print('numbers of layers:', len(pre_model.layers))
# numbers of layers: 154

# 指定冻结120层之前的层数
find_tune_at = 120

# 冻结到第120层
for layer in pre_model.layers[:find_tune_at]:
    layer.trainable = False

6.2 模型构建

采用和上面相同的方法构建网络,如下

# 自定义顶层
# 将特征提取层的输出结果压平后再传入全连接层
x = layers.Flatten()(pre_model.output) 
# 加入全连接层
x = layers.Dense(1024, activation='relu')(x)
x = layers.Dropout(0.4)(x) #0.4的几率杀死神经元
# 输出层,输出图像属于10分类的概率
x = layers.Dense(2, activation='sigmoid')(x)

# 模型构建
model = Model(pre_model.input, x) # 给出输入层和输出层

# 查看预训练网络结构
pre_model.summary()

网络结构如下,我们看到预训练模型中,可训练参数的数量明显增加,Trainable params: 1,624,896,因此也有更多的参数可以更新权重,提高我们网络的精度。

Model: "mobilenetv2_1.00_224"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_2 (InputLayer)           [(None, 50, 50, 3)]  0           []                               
                                                                                                  
 Conv1 (Conv2D)                 (None, 25, 25, 32)   864         ['input_2[0][0]']                
                                                                                                  
 bn_Conv1 (BatchNormalization)  (None, 25, 25, 32)   128         ['Conv1[0][0]']                  
                                                                                                  
 Conv1_relu (ReLU)              (None, 25, 25, 32)   0           ['bn_Conv1[0][0]']               
                                                                                                  
 expanded_conv_depthwise (Depth  (None, 25, 25, 32)  288         ['Conv1_relu[0][0]']             
 wiseConv2D)                                                                                        
                                                                                                  
-----------------------------------------------
省略 N 层
-----------------------------------------------                                                                              
                                                                                                  
 block_16_depthwise_relu (ReLU)  (None, 2, 2, 960)   0           ['block_16_depthwise_BN[0][0]']  
                                                                                                  
 block_16_project (Conv2D)      (None, 2, 2, 320)    307200      ['block_16_depthwise_relu[0][0]']
                                                                                                  
 block_16_project_BN (BatchNorm  (None, 2, 2, 320)   1280        ['block_16_project[0][0]']       
 alization)                                                                                       
                                                                                                  
 Conv_1 (Conv2D)                (None, 2, 2, 1280)   409600      ['block_16_project_BN[0][0]']    
                                                                                                  
 Conv_1_bn (BatchNormalization)  (None, 2, 2, 1280)  5120        ['Conv_1[0][0]']                 
                                                                                                  
 out_relu (ReLU)                (None, 2, 2, 1280)   0           ['Conv_1_bn[0][0]']              
                                                                                                  
==================================================================================================
Total params: 2,257,984
Trainable params: 1,624,896
Non-trainable params: 633,088
__________________________________________________________________________________________________

6.3 网络配置

网络编译和配置的方法和前面相同,经过10次迭代后,可见微调后的准确率有所上升。如6.4的损失和准确率图所示,损失和准确率的变化更加平稳。

当您正在训练一个大得多的模型并且想要重新调整预训练权重时,请务必在此阶段使用较低的学习率。否则,您的模型可能会很快过拟合。

# 编译,调小学习率防止过拟合
model.compile(optimizer=optimizers.Adam(learning_rate=1e-6),  # 学习率,调小一些防止过拟合
              loss = 'binary_crossentropy',  # 二分类损失,放入sigmoid中再计算损失
              metrics=['acc'])  # 评价指标 

# 网络训练
history_fine = model.fit(train_ds,  #训练数据
                    validation_data=test_ds,  # 验证数据
                    epochs=10,  # 迭代次数
                    ) 
Epoch 1/10
329/329 [==============================] - 14s 31ms/step - loss: 0.8064 - acc: 0.5974 - val_loss: 0.6245 - val_acc: 0.6898
Epoch 2/10
329/329 [==============================] - 10s 28ms/step - loss: 0.6037 - acc: 0.7447 - val_loss: 0.5144 - val_acc: 0.7782
-------------------------------------------------------------------------------------
省略N层
-------------------------------------------------------------------------------------
Epoch 9/10
329/329 [==============================] - 9s 27ms/step - loss: 0.3105 - acc: 0.8850 - val_loss: 0.4067 - val_acc: 0.8399
Epoch 10/10
329/329 [==============================] - 10s 28ms/step - loss: 0.2881 - acc: 0.8927 - val_loss: 0.4056 - val_acc: 0.8407

6.4 网络评估可视化

# 准确率
train_acc_fine = history_fine.history['acc']
test_acc_fine = history_fine.history['val_acc']
# 损失
train_loss_fine = history_fine.history['loss']
test_loss_fine = history_fine.history['val_loss']
# 绘图
plt.figure(figsize=(20, 8))
plt.subplot(1, 2, 1)
plt.plot(train_acc_fine, label='Train_Acc')
plt.plot(test_acc_fine, label='Val_Acc')
plt.legend()
plt.title('Accuracy')

plt.subplot(1, 2, 2)
plt.plot(train_loss_fine, label='Training Loss')
plt.plot(test_loss_fine, label='Validation Loss')
plt.legend()
plt.title('Loss')
plt.show()

【神经网络】(7) 迁移学习(CNN-MobileNetV2),案例:乳腺癌二分类_第3张图片

你可能感兴趣的:(TensorFlow神经网络,迁移学习,神经网络,python,深度学习,tensorflow)