📜  毫升 |使用卷积神经网络进行迁移学习

📅  最后修改于: 2022-05-13 01:55:32.531000             🧑  作者: Mango

毫升 |使用卷积神经网络进行迁移学习

迁移学习作为一个通用术语是指将从一项任务中学到的知识重用于另一项任务。特别是对于卷积神经网络 (CNN),许多图像特征对于各种数据集都是通用的(例如,几乎在每张图像中都可以看到线条、边缘)。正是出于这个原因,特别是对于大型结构,CNN 很少从头开始完全训练,因为很难获得大型数据集和大量计算资源。
常用的预训练数据集是ImageNet 数据集,由 120 万张图像组成。实际使用的模型因任务而异(很多时候,人们只是选择在 ImageNet 挑战中表现最好的模型),但本文使用的是ResNet50模型。预训练模型通常可以通过正在使用的任何库找到,在这种情况下,就是 Keras。

ResNet 简介
ResNet 最初被设计为一种解决梯度消失问题的方法。这是一个问题,反向传播的梯度在一遍又一遍地相乘时变得非常小,从而限制了神经网络的大小。 ResNet 架构试图通过使用跳过连接来解决这个问题,即添加允许数据跳过过去层的快捷方式。

该模型由一系列卷积层 + 跳过连接,然后是平均池化,然后是输出全连接(密集)层组成。对于迁移学习,我们只希望卷积层包含我们感兴趣的特征,因此我们希望在导入模型时省略它们。最后,因为我们要移除输出层,所以我们需要用我们自己的一系列层来替换它们。

问题陈述
为了展示迁移学习的过程,我将使用 Caltech-101 数据集,这是一个包含 101 个类别和每个类别大约 40-800 个图像的图像数据集。

数据处理

首先在此处下载并提取数据集。确保在提取后删除“BACKGROUND_Google”文件夹。

代码:为了正确评估,我们还需要将数据分成训练集和测试集。在这里,我们需要在每个类别中进行拆分,以确保在测试集中正确表示。

TEST_SPLIT = 0.2
VALIDATION_SPLIT = 0.2
  
import os
import math
  
# stores test data
os.mkdir("caltech_test") 
  
for cat in os.listdir("101_ObjectCategories/"):
  # moves x portion of images per category into test images
  # new category folder
  os.mkdir("caltech_test/"+cat) 
  imgs = os.listdir("101_ObjectCategories/"+cat) 
  # all image filenames
  split = math.floor(len(imgs)*TEST_SPLIT) 
  test_imgs = imgs[:split]
  # move test portion
  for t_img in test_imgs: 
    os.rename("101_ObjectCategories/"+cat+"/"+t_img, 
              "caltech_test/"+cat+"/"+t_img)

输出:

This above code creates the file structure:

101_ObjectCategories/
-- accordion
-- airplanes
-- anchor
-- ...
caltech_test/
-- accordion
-- airplanes
-- anchor
-- ...

第一个文件夹包含训练图像,第二个文件夹包含测试图像。每个子文件夹都包含属于该类别的图像。要输入数据,我们将使用 Keras 的 ImageDataGenerator 类。 ImageDataGenerator 允许轻松处理图像数据,还具有增强选项。

# make sure to match original model's preprocessing function
from keras.applications.resnet50 import preprocess_input 
from keras.preprocessing.image import ImageDataGenerator
  
train_gen = ImageDataGenerator(
        validation_split = 0.2, 
        preprocessing_function = preprocess_input)
train_flow = train_gen.flow_from_directory("101_ObjectCategories/", 
                                           target_size =(256, 256), 
                                           batch_size = 32, 
                                           subset ="training")
  
valid_flow = train_gen.flow_from_directory("101_ObjectCategories/", 
                                           target_size =(256, 256), 
                                           batch_size = 32, 
                                           subset ="validation")
  
test_gen = ImageDataGenerator(
        preprocessing_function = preprocess_input)
test_flow = test_gen.flow_from_directory("caltech_test", 
                                         target_size =(256, 256), 
                                         batch_size = 32)

上述代码获取图像目录的文件路径,并创建一个对象用于数据生成。

建筑模型
代码:添加基础预训练模型。

from keras.applications.resnet50 import ResNet50
from keras.layers import GlobalAveragePooling2D, Dense
from keras.layers import BatchNormalization, Dropout
from keras.models import Model
  
# by default, the loaded model will include the original CNN 
#classifier designed for the ImageNet dataset
# since we want to reuse this model for a different problem,
# we need to omit the original fully connected layers, and 
# replace them with our own setting include_top = False will
# load the model without the fully connected layer
  
# load resnet model, with pretrained imagenet weights.
res = ResNet50(weights ='imagenet', include_top = False, 
               input_shape =(256, 256, 3)) 

这个数据集相对较小,拆分后大约有 5628 张图像,大多数类别只有 50 张图像,因此微调卷积层可能会导致过度拟合。我们的新数据集与 ImageNet 数据集非常相似,因此我们可以确信许多预训练的权重也具有正确的特征。因此,我们可以冻结那些经过训练的卷积层,这样当我们训练分类器的其余部分时它们就不会改变。如果您有一个与原始数据显着不同的较小数据集,微调可能仍会导致过度拟合,但后面的层不会包含正确的特征。因此,您可以再次冻结卷积层,但仅使用早期层的输出,因为它们包含更一般的特征。使用大型数据集,您无需担心过度拟合,因此您通常可以对整个网络进行微调。

from keras.applications.resnet50 import ResNet50
from keras.layers import GlobalAveragePooling2D, Dense
from keras.layers import BatchNormalization, Dropout
from keras.models import Model
  
# by default, the loaded model will include the original CNN 
#classifier designed for the ImageNet dataset
# since we want to reuse this model for a different problem,
# we need to omit the original fully connected layers, and 
# replace them with our own setting include_top = False will
# load the model without the fully connected layer
  
# load resnet model, with pretrained imagenet weights.
res = ResNet50(weights ='imagenet', include_top = False, 
               input_shape =(256, 256, 3)) 

现在,我们可以添加分类器的其余部分。这从预训练的卷积层获取输出,并将其输入到一个单独的分类器中,该分类器在新数据集上进行训练。

# get the output from the loaded model
x = res.output 
  
# avg. pools across the spatial dimensions (rows, columns) 
# until it becomes zero. Reshapes data into a 1D, allowing 
# for proper input shape into Dense layers 
# (e.g. (8, 8, 2048) -> (2048)).
x = GlobalAveragePooling2D()(x) 
  
# subtracts batch mean and divides by batch standard deviation
# to reduce shift in input distributions between layers. 
x = BatchNormalization()(x) 
  
# dropout allows layers to be less dependent on
# certain features, reducing overfitting
x = Dropout(0.5)(x) 
x = Dense(512, activation ='relu')(x)
x = BatchNormalization()(x)
x = Dropout(0.5)(x)
  
# output classification layer, we have 101 classes, 
# so we need 101 output neurons
x = Dense(101, activation ='softmax')(x) 
  
# create the model, setting input / output
model = Model(res.input, x) 
  
# compile the model - we're training using the Adam Optimizer
# and Categorical Cross Entropy as the loss function
model.compile(optimizer ='Adam', 
              loss ='categorical_crossentropy', 
              metrics =['accuracy']) 
  
# structure of our model
model.summary() 

代码:训练模型

model.fit_generator(train_flow, epochs = 5, validation_data = valid_flow)


输出:

Epoch 1/5
176/176 [==============================] - 27s 156ms/step - loss: 1.6601 - acc: 0.6338 - val_loss: 0.3799 - val_acc: 0.8922
Epoch 2/5
176/176 [==============================] - 19s 107ms/step - loss: 0.4637 - acc: 0.8696 - val_loss: 0.2841 - val_acc: 0.9225
Epoch 3/5
176/176 [==============================] - 19s 107ms/step - loss: 0.2777 - acc: 0.9211 - val_loss: 0.2714 - val_acc: 0.9225
Epoch 4/5
176/176 [==============================] - 19s 107ms/step - loss: 0.2223 - acc: 0.9327 - val_loss: 0.2419 - val_acc: 0.9284
Epoch 5/5
176/176 [==============================] - 19s 106ms/step - loss: 0.1784 - acc: 0.9461 - val_loss: 0.2499 - val_acc: 0.9239

代码:评估测试集

result = model.evaluate(test_flow)
  
print('The model achieved a loss of %.2f and,'
      'accuracy of %.2f%%.' % (result[0], result[1]*100))

输出:

53/53 [==============================] - 5s 95ms/step
The model achieved a loss of 0.23 and accuracy of 92.80%.

对于 101 类数据集,我们仅在 5 个 epoch 后就达到了 92.8% 的准确率。从角度来看,原始的 ResNet 在大约 100 万个图像数据集上进行了 120 个 epoch 的训练。
有几件事可以改进。一方面,查看最后一个 epoch 中验证损失和训练损失之间的差异,您可以看到模型开始过度拟合。解决此问题的一种方法是添加图像增强。使用 ImageDataGenerator 类可以轻松实现简单的图像增强。您还可以添加/删除层或更改超参数,例如 dropout 或 Dense 层的大小。
使用 Google Colab 的免费 GPU 计算资源在此处运行此代码。