迁移学习【翻译】
原文:https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html
使用很少的数据构建强大的图像分类模型
在本教程中,我们将介绍一些简单而有效的方法,您可以使用这些方法来构建强大的图像分类器,仅使用很少的训练数据 ——您希望识别的每个类中只需要几百或几千张图片。
我们将讨论以下内容:
- 从头开始培训小型网络(作为基础)
- 使用预先训练的网络的bottleneck features
- 微调预先训练过的网络的顶层
这将引导我们介绍以下 Keras 功能:
fit_generator
用于使用 Python 数据生成器训练 Keras 模型ImageDataGenerator
实时数据扩增- 层冻结和模型微调
- …更多
我们的设置:只有 2000 个训练数据(每类 1000 个)
我们将从以下设置开始:
-
安装了 Keras 、 SciPy 、 PIL 的机器。如果您有一个 NVIDIA GPU,您可以使用(和 cuDNN 安装),这不错,但由于我们处理的图像很少,这是没有必要的。
-
包含每个类的一个子目录的训练数据目录和验证数据目录,其中.png或.jpg图像:
data/ train/ dogs/ dog001.jpg dog002.jpg ... cats/ cat001.jpg cat002.jpg ... validation/ dogs/ dog001.jpg dog002.jpg ... cats/ cat001.jpg cat002.jpg ...
要获取属于您感兴趣的数百或数千个训练图像,可以使用 Flickr API 下载与给定标记匹配的图片。
在我们的示例中,我们将使用两组图片,我们从Kaggle获得:1000 只猫和 1000 只狗(虽然原始数据集有 12,500 只猫和 12,500 只狗,但我们只使用了每个类拍摄了的前 1000 张图片)。我们还使用每个类中的 400 个附加样本作为验证数据,以评估我们的模型。
对于一个远非简单的分类问题,只有很少的例子可以借鉴的。因此,这是一个具有挑战性的机器学习问题,但它也是一个现实问题:在很多实际用例中,即使是小规模的数据收集也可能非常昂贵,有时甚至是几乎不可能的(例如,在医学影像中)。能够充分利用很少的数据是有能力的数据科学家的关键技能。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Rr9FNNW-1613983786319)(C:%5CUsers%5Cxiaomi%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20210218013405834.png)]
这个问题有多困难?当Kaggle开始猫与狗比赛(总共有25,000张训练图像),两年前一点点,它附带了以下声明:
"在多年前进行的一次非正式民意调查中,计算机视觉专家认为,如果没有技术上的重大进步,一个准确率超过60%的分类器将很难实现。作为参考,60%的分类器可以将12张图像的髋关节猜测概率从1/4096提高到1/459。目前的文献表明,机器分类器可以在该任务上获得80%以上的准确率[ref]。“
在最终的比赛中,顶尖选手通过使用现代深度学习技术,准确率超过 98%。在我们的案例中,由于我们仅使用 8% 的数据集,因此问题要困难得多。
关于深度学习对小数据问题的相关性
我经常听到的信息是,“只有在您拥有大量数据时,深度学习才相关”。虽然并非完全不正确,但这有点误导人。当然,深度学习需要能够从数据中自动学习要素,这通常只有在大量训练数据可用时才可能实现,尤其是对于输入样本非常高维的问题(如图像)。然而,卷积神经网络(深度学习的支柱算法)是设计中可用于大多数"感知"问题(如图像分类)的最佳模型之一,即使几乎没有数据可学。在小型图像数据集上从头开始训练深度卷积神经网络仍会产生合理的结果,而无需任何自定义要素工程。深度卷积神经网络都不错。它们是适合这项工作的工具。
但更重要的是,深度学习模型本质上是高度可重用的:例如,你可以采取在大规模数据集上训练的图像分类或语音到文本模型,然后重用它,在一个明显不同的问题上,只需稍作更改,我们将在这篇文章中看到。特别是在计算机视觉方面,许多预先训练的模型(通常在 ImageNet 数据集上训练)现在可公开下载,并可用于从很少的数据中引导强大的视觉模型。
数据预处理和数据扩充
为了充分利用我们的几个训练数据,我们将通过一些随机转换来"增强"它们,以便我们的模型永远不会两次看到完全相同的图片。这有助于防止过度拟合,并有助于模型更好地通用。
在 Keras 中,这可以通过类完成。此类允许您:keras.preprocessing.image.ImageDataGenerator
- 配置在训练期间对图像数据进行随机转换和规范化操作
- 通过实例化增强批量图像(及其标签)的生成器。然后,这些生成器可以与接受数据生成器作为输入的 Keras 模型方法一起使用, 和
.flow(data, labels)``.flow_from_directory(directory)``fit_generator``evaluate_generator``predict_generator
让我们马上看一个例子:
from keras.preprocessing.image import ImageDataGenerator
datagen = ImageDataGenerator(
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
rescale=1./255,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode='nearest')
这些只是几个可用的选项(有关更多信息,请参阅文档)。让我们快速了解一下我们刚才写的东西:
rotation_range
是一个图片旋转度值 ,范围(0-180)width_shift_range
浮点数,图片宽度的某个比例,数据提升时图片随机水平偏移的幅度height_shift_range
浮点数,图片高度的某个比例,数据提升时图片随机竖直偏移的幅度。rescale
是一个值,将在执行其他处理前乘到整个图像上,我们的图像在RGB通道都是0255的整数,这样的操作可能使图像的值过高或过低,所以我们将这个值定为01之间的数。shear_range
用于随机应用剪切变换,浮点数,剪切强度(逆时针方向的剪切变换角度)。是用来进行剪切变换的程度。zoom_range
用于随机缩放图片内部,浮点数或形如[lower,upper]的列表,随机缩放的幅度,若为浮点数,则相当于[lower,upper] = [1 - zoom_range, 1+zoom_range]。用来进行随机的放大。horizontal_flip
布尔值,进行随机水平翻转。随机的对图片进行水平翻转,这个参数适用于水平翻转不影响图片语义的时候。fill_mode
是用于填充新创建的像素的策略,可以在旋转或宽度/高度移位后显示。‘constant’,‘nearest’,‘reflect’或‘wrap’之一,当进行变换时超出边界的点将根据本参数给定的方法进行处理
现在,让我们开始使用此工具生成一些图片,并将它们保存到临时目录中,以便我们了解我们的增强策略正在做什么 - 在这种情况下,我们禁用重新缩放以保持图像显示:
from keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img
datagen = ImageDataGenerator(
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode='nearest')
img = load_img('data/train/cats/cat.0.jpg') # this is a PIL image
x = img_to_array(img) # this is a Numpy array with shape (3, 150, 150)
x = x.reshape((1,) + x.shape) # this is a Numpy array with shape (1, 3, 150, 150)
# the .flow() command below generates batches of randomly transformed images
# and saves the results to the `preview/` directory
i = 0
for batch in datagen.flow(x, batch_size=1,
save_to_dir='preview', save_prefix='cat', save_format='jpeg'):
i += 1
if i > 20:
break # otherwise the generator would loop indefinitely
以下是我们得到的 - 这就是我们的数据增强策略的样子。
从零开始培训小型深度卷积神经网络:40行代码的80%精度
图像分类工作的正确工具是深度卷积神经网络,因此,让我们尝试以初始的baseline对数据进行培训。由于我们的数据很少,我们最关心的问题应该是过拟合。当一个模型接触到的数据太少,它学习的模式不能推广到新数据时,就会发生过拟合。例如,如果你作为一个人,只看到三张伐木工人的照片,三张是水手的照片,其中只有一张伐木工人戴着帽子,你可能会开始认为戴帽子是伐木工人的标志,而不是水手。然后,你会做一个相当糟糕的伐木工人/水手分类器。
数据增强是对抗过拟合的一种方法,但它还不够,因为我们的增强样本仍然高度相关。您对抗过拟合的主要关注点应该是模型的熵容量——您的模型允许存储多少信息。一个可以存储大量信息的模型可以通过利用更多的特性变得更加精确,但是它也有可能开始存储不相关的特性。与此同时,一个只能存储少数特征的模型必须专注于数据中最重要的特征,而这些特征更有可能是真正相关的,并更好地一般化。
有不同的方法来调节熵容量。主要一个是选择模型中的参数,即各层的数量和每层的大小。另一种方式是使用权重规范化,如L1或L2正规化,其中包括迫使模型权重采取较小的值。
在我们的情况下,我们将使用一个非常小的深度卷积神经网络,每层只有几层和很少的filter,以及数据增强和dropout。dropout还有助于减少过度拟合,防止一个层看到两个完全相同的模式,从而以类似于数据增强的方式行事(你可以说,dropout和数据增强往往会破坏数据中发生的随机相关性)。
下面的代码片段是我们的第一个模型,一个简单的堆栈 3 个卷积层与 ReLU 激活,然后是max-pooling层。这与 Yann LeCun 在 20 世纪 90 年代倡导的图像分类(ReLU 除外)的架构非常相似。
此实验的完整代码可在此处找到。
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
model = Sequential()
model.add(Conv2D(32, (3, 3), input_shape=(3, 150, 150)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
# the model so far outputs 3D feature maps (height, width, features)
在它上面,我们坚持两个全连接的层。我们以单个神经元和 sigmoid 激活结束模型,这是二元分类的完美之举。为了配合它,我们也将利用损失来训练我们的模型。binary_crossentropy
model.add(Flatten()) # this converts our 3D feature maps to 1D feature vectors
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.compile(loss='binary_crossentropy',
optimizer='rmsprop',
metrics=['accuracy'])
让我们准备我们的数据。我们将直接从各自文件夹中的 jpgs 生成成批的图像数据(及其标签)。.flow_from_directory()
batch_size = 16
# this is the augmentation configuration we will use for training
train_datagen = ImageDataGenerator(
rescale=1./255,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True)
# this is the augmentation configuration we will use for testing:
# only rescaling
test_datagen = ImageDataGenerator(rescale=1./255)
# this is a generator that will read pictures found in
# subfolers of 'data/train', and indefinitely generate
# batches of augmented image data
train_generator = train_datagen.flow_from_directory(
'data/train', # this is the target directory
target_size=(150, 150), # all images will be resized to 150x150
batch_size=batch_size,
class_mode='binary') # since we use binary_crossentropy loss, we need binary labels
# this is a similar generator, for validation data
validation_generator = test_datagen.flow_from_directory(
'data/validation',
target_size=(150, 150),
batch_size=batch_size,
class_mode='binary')
我们现在可以使用这些生成器来训练我们的模型。每个epoch在GPU上需要20-30s,在CPU上需要300-400s。所以如果你不着急的话,在CPU上运行这个模型绝对是可行的
model.fit_generator(
train_generator,
steps_per_epoch=2000 // batch_size,
epochs=50,
validation_data=validation_generator,
validation_steps=800 // batch_size)
model.save_weights('first_try.h5') # always save your weights after training or during training
这种方法使我们在 50 个epoch后验证精度为 0.79-0.81(这是任意选择的数字 - 因为模型很小,并且使用攻击性辍学,到那时它似乎并不太合适)。因此,在Kaggle竞赛启动时,我们已经是"最先进的"——拥有8%的数据,并且不努力优化我们的架构或超参数。事实上,在Kaggle竞赛中,这种模式会进入前100名(在215名参赛者中)。我想至少有115名参赛者没有使用深度学习;)
请注意,验证精度的方差相当高,这既是因为精度是一个高方差度度,也是因为我们只使用 800 个验证样本。在这种情况下,一个很好的验证策略是进行 k 折交叉验证,但这需要为每个评估轮培训 k 模型。
使用预先培训的网络的瓶颈功能:一分钟内准确率达 90%
更精细的方法是利用在大型数据集上预先训练的网络。这样一个网络已经学会了对大多数计算机视觉问题有用的功能,利用这些功能将使我们能够比任何只依赖现有数据的方法达到更好的准确性。
我们将使用 VGG16 架构,在 ImageNet 数据集上预先训练 - 此博客上以前采用的模型。由于 ImageNet 数据集包含多个"猫"类(波斯猫、暹罗猫…)和 1000 个类中的许多"狗"类,因此此模型已经具备了与我们的分类问题相关的功能。事实上,仅仅通过我们的数据记录模型的softmax预测,而不是bottleneck features,就足以很好地解决我们的狗与猫的分类问题。但是,我们在这里介绍的方法更有可能很好地概括到更广泛的问题,包括以ImageNet中没有的类为特征的问题。
以下是 VGG16 架构的外观:
我们的策略将如下所示:我们将只实例化模型的卷积部分,直到全连接层。然后我们将在训练和验证数据上运行这个模型一次,在两个numpy数组中记录输出(来自VGG16模型的“bottleneck features”:在全连接层之前的最后激活映射)。然后,我们将在存储的features之上训练一个小的全连接模型。
我们之所以离线存储这些features,而不是在冻结的卷积基础上直接添加完全连接的模型,然后运行整个程序,原因是计算效率。运行VGG16是很昂贵的,尤其是在CPU上运行时,而且我们只希望运行一次。注意,这阻止了我们使用数据增强。
你可以在这里找到这个实验的完整代码。你可以从github获得权重文件。我们不会审查模型是如何构建和加载的- 这已经包含在多个 Keras 示例中。但是,让我们来看看我们如何使用图像数据生成器记录bottleneck features:
batch_size = 16
generator = datagen.flow_from_directory(
'data/train',
target_size=(150, 150),
batch_size=batch_size,
class_mode=None, # this means our generator will only yield batches of data, no labels
shuffle=False) # our data will be in order, so all first 1000 images will be cats, then 1000 dogs
# the predict_generator method returns the output of a model, given
# a generator that yields batches of numpy data
bottleneck_features_train = model.predict_generator(generator, 2000)
# save the output as a Numpy array
np.save(open('bottleneck_features_train.npy', 'w'), bottleneck_features_train)
generator = datagen.flow_from_directory(
'data/validation',
target_size=(150, 150),
batch_size=batch_size,
class_mode=None,
shuffle=False)
bottleneck_features_validation = model.predict_generator(generator, 800)
np.save(open('bottleneck_features_validation.npy', 'w'), bottleneck_features_validation)
然后,我们可以加载我们保存的数据,并训练一个小型的完全连接的模型:
train_data = np.load(open('bottleneck_features_train.npy'))
# the features were saved in order, so recreating the labels is easy
train_labels = np.array([0] * 1000 + [1] * 1000)
validation_data = np.load(open('bottleneck_features_validation.npy'))
validation_labels = np.array([0] * 400 + [1] * 400)
model = Sequential()
model.add(Flatten(input_shape=train_data.shape[1:]))
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics=['accuracy'])
model.fit(train_data, train_labels,
epochs=50,
batch_size=batch_size,
validation_data=(validation_data, validation_labels))
model.save_weights('bottleneck_fc_model.h5')
由于其小尺寸,这种模型列车非常快,即使在CPU(每个epoch):
Train on 2000 samples, validate on 800 samples
Epoch 1/50
2000/2000 [==============================] - 1s - loss: 0.8932 - acc: 0.7345 - val_loss: 0.2664 - val_acc: 0.8862
Epoch 2/50
2000/2000 [==============================] - 1s - loss: 0.3556 - acc: 0.8460 - val_loss: 0.4704 - val_acc: 0.7725
...
Epoch 47/50
2000/2000 [==============================] - 1s - loss: 0.0063 - acc: 0.9990 - val_loss: 0.8230 - val_acc: 0.9125
Epoch 48/50
2000/2000 [==============================] - 1s - loss: 0.0144 - acc: 0.9960 - val_loss: 0.8204 - val_acc: 0.9075
Epoch 49/50
2000/2000 [==============================] - 1s - loss: 0.0102 - acc: 0.9960 - val_loss: 0.8334 - val_acc: 0.9038
Epoch 50/50
2000/2000 [==============================] - 1s - loss: 0.0040 - acc: 0.9985 - val_loss: 0.8556 - val_acc: 0.9075
我们的验证精度为 0.90-0.91:一点也不差。这肯定是由于基础模型是在已经以狗和猫为特色的数据集上训练的(在数百个其他类别中)。
微调预先训练的网络的顶层
为了进一步改进我们之前的结果,我们可以尝试与顶级分类器旁边"微调"VGG16 模型的最后一个卷积块。微调包括从训练过的网络开始,然后使用非常小的权重更新在新的数据集上重新训练它。在我们的情况下,这可以通过 3 个步骤完成:
- 实例化VGG16的卷积并加载其权重
- 在顶部添加我们以前定义的完全连接的模型,并加载其权重
- 将VGG16模型的各个层冻结直到最后一个卷积块
请注意:
- 为了执行微调,所有层都应从经过适当训练的权重开始:例如,您不应在预先训练的卷积基座上拍打随机初始化的完全连接网络。这是因为随机初始化权重触发的大梯度更新会破坏卷积基座中学到的权重。在我们的情况下,这就是为什么我们首先训练顶级分类器,然后才开始微调卷积。
- 为了防止过拟合,我们选择只微调最后一个卷积块,而不是微调整个网络,因为整个网络的熵容量非常大,因此过拟合的倾向很强。低级卷积块学习到的特性比高级块更通用,更不抽象,因此保持前几个块固定(更通用的特性),只微调最后一个块(更专门的特性)是明智的。
- 微调应该以非常慢的学习速度进行,通常使用SGD优化器,而不是像RMSProp这样的自适应学习速度优化器。这是为了确保更新的幅度非常小,以免破坏之前学到的功能
你可以在这里找到这个实验的完整代码。
在实例化 基础VGG 并加载其权重后,我们将我们以前训练的完全连接的分类器添加到顶部:
# build a classifier model to put on top of the convolutional model
top_model = Sequential()
top_model.add(Flatten(input_shape=model.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(1, activation='sigmoid'))
# note that it is necessary to start with a fully-trained
# classifier, including the top classifier,
# in order to successfully do fine-tuning
top_model.load_weights(top_model_weights_path)
# add the model on top of the convolutional base
model.add(top_model)
然后,我们继续冻结所有卷积层,直至最后一个卷积区块:
# set the first 25 layers (up to the last conv block)
# to non-trainable (weights will not be updated)
for layer in model.layers[:25]:
layer.trainable = False
# compile the model with a SGD/momentum optimizer
# and a very slow learning rate.
model.compile(loss='binary_crossentropy',
optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
metrics=['accuracy'])
最后,我们开始用非常慢的学习速度进行整个训练:
batch_size = 16
# prepare data augmentation configuration
train_datagen = ImageDataGenerator(
rescale=1./255,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True)
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
train_data_dir,
target_size=(img_height, img_width),
batch_size=batch_size,
class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
validation_data_dir,
target_size=(img_height, img_width),
batch_size=batch_size,
class_mode='binary')
# fine-tune the model
model.fit_generator(
train_generator,
steps_per_epoch=nb_train_samples // batch_size,
epochs=epochs,
validation_data=validation_generator,
validation_steps=nb_validation_samples // batch_size)
这种方法使我们在 50 个epoch后验证精度为 0.94。非常成功!
下面是一些更多的方法,您可以尝试到0.95以上:
- 更积极的数据增强
- 更积极的dropout
- 使用L1和L2正规化(也称为"权重衰减")
- 微调一个多一个卷积块(除了更大的规范化)
这篇文章在这里结束!回顾一下,您可以在这里找到我们三个实验的代码: