快捷搜索:

想正确识别猫狗?理论+实例+代码一文搞定

 

深度学习正迅速成为人工智能应用的关键工具。例如,在计算机视觉、自然语言处理和语音识别等领域,深度学习已经取得显著的成果。因此,人们对深度学习的兴趣也越来越浓厚。

深度学习中最突出的问题之一是图像分类。图像分类的目的是根据潜在的类别对特定的图像进行分类。图像分类的一个经典示例是在一组图像中识别猫和狗。

从深度学习的角度来看,图像分类问题可以通过迁移学习来解决。实际上,图像分类中几项最新的研究成果都是基于迁移学习方案得出的。

本文将介绍如何在图像分类问题中实施迁移学习解决方案。本文提供的实施方案基于使用Python编程语言的Keras(Chollet 2015)。通过实施此方案,你将能够快速轻松地解决任何图像分类问题。

目录

1. 迁移学习

2. 卷积神经网络

3. 再利用预训练模型

4. 迁移学习过程

5. 深度卷积神经网络上的分类器

6. 实例

7. 总结

1. 迁移学习

迁移学习是计算机视觉中一种备受欢迎的学习方法,通过迁移学习,我们可以在省时的基础上建立准确的模型。进行迁移学习时,你可以从解决不同问题时遇到的已知的模式开始学习,而不需要从头开始。这样你可以利用以前学到的知识,避免从头开始。迁移学习可视作沙特尔所说的“站在巨人的肩膀上”的深度学习版本。

在计算机视觉中,迁移学习通常通过使用预训练模型来实现。预训练模型指在大型基准数据集上进行问题解决训练的模型,而用于训练的问题与我们实际想要解决的问题类似。由于训练这类模型的计算量与计算成本低,更常见的应用是在已出版的文献中导入和使用这类模型。

2. 卷积神经网络

用于迁移学习的几种预训练模型都是基于大型卷积神经网络(CNN)的。总的来说,CNN在各种计算机视觉任务中表现出色。其具备的高性能且易于训练是近年来CNN流行的两个主要因素。

一个典型的CNN 由两部分组成:

1. 卷积基底,由一堆卷积层和池化层组成。卷积基的主要目的是从图像中生成特征。

2. 分类器,通常由全连接层组成。分类器的主要作用是根据检测到的特征对图像进行分类。全连接层指神经元与前一层中的所有激活完全连接的层。

图1是基于CNN的模型结构。注意,这是一个简化的版本,它符合本文的目的。实际中,这类模型结构比我们这里列举的更复杂。

图1 基于CNN的模型结构

这些深度学习模型的一个重要特征是它们可以自动学习分层特征表示。这意味着由第一层计算的特征是通用的,并且可以在不同的问题域中再次使用,而由最后一层计算的特征是特定的,并且取决于所选择的数据集和任务。有专家认为:“如果第一层特征是通用的,而最后一层特征是特定的,那么在网络的某个地方必须要从一般特征过渡到特定特征。”

因此,CNN的卷积基,尤其是其较低的层(更接近输入的层),指的是一般特征;而CNN的分类器部分和卷积基中的一些较高层则指特殊特征。

3. 再利用预训练模型

当你根据自己的需要再利用预训练模型时,你首先要删除原始的分类器,然后添加符合你需要的新分类器,最后必须根据以下三种策略中的一种对模型进行微调:

1. 训练整个模型。在这种情况下,你将用到预训练模型的体系结构,并根据你的数据集对其进行训练。由于你将从头开始学习模型,因此你需要一个大数据集(以及大量的计算能力)。

2. 训练一部分层,其他层保持冻结状态。如前面所说,较低层指的是一般特性(与问题无关),而较高层指的是特定特性(与问题有关)。这里,我们通过需要调整的网络权重(冻结层在训练期间不会改变)来实现这种二分法。通常,如果你有一个小的数据集和大量的参数,你需要留下更多的冻结层以避免过度拟合。相反,如果数据集很大,参数的数量很小,你可以对更多的层进行新任务训练来改进模型,因为这种情况下不会出现过度拟合。

3. 冻结卷积基。这种情况与训练权衡或冻结权衡的极端情况相对应。其主要思想是使卷积基保持其原始形式,然后将其输出提供给分类器。当你将预训练模型作为固定的特征提取机制时,如果你缺乏计算能力、数据集很小,和/或预训练模型解决了你想要解决的问题,那么此方法将很有用。

图2是这三种策略的流程图:

图2 微调策略

不同于操作直接的策略3,使用策略1和策略2时,你需要注意卷积部分中使用的学习率。学习率是一个超参数,它控制你对网络权重的调整程度。当你使用基于CNN的预训练模型时,最好保持较低学习率,因为高学习率会增加失去之前获取的知识的风险。假设预训练模型经过了良好的训练(这样假设是完全合理的),保持低学习率将确保你不会过早和过量地扭曲CNN权重。

4.迁移学习过程

从实际角度来看,整个迁移过程可归纳如下:

1. 选择一个预训练模型。你可以从各种可用的预训练模型中选择一种适合你的模型。例如,如果你使用的是Keras,那么你可以立即访问如VGG、InceptionV3 , 以及 ResNet5 这类模型。

2. 根据大小-相似性矩阵对问题进行分类。图3中的“矩阵”影响着你的选择。这个矩阵根据数据集的大小以及与训练预训练模型时所用的的数据集的相似性对计算机视觉问题进行分类。根据经验法则,如果一个数据集的每个类中图像少于1000个,则认为该数据集很小。而数据集的相似性可以根据常识判断出。例如,如果你的任务是识别猫和狗,则ImageNet是一个相似的数据集,因为它能识别猫和狗的图像。然而,如果你的任务是识别癌细胞,则ImageNet不能被视为相似的数据集。

3. 微调模型。在这里,你可以使用大小-相似性矩阵来辅助选择,然后参考前面提到的关于再利用预训练模型的三种选择。图4是以下文本的图像总结。

象限1:大数据集,但与预训练模型的数据集不同。在这种情况下,需要采取策略1。由于你有一个大的数据集,所以可以从头开始训练模型,并执行你想要的任何操作。尽管数据集有所不同,但在实践中,通过使用体系结构和权重,初始化一个预训练模型的方法仍然是有用的。

象限2:大数据集并类似于预训练模型的数据集。这时候任何选项都适用,但很有可能最有效的选择是策略2。由于数据集很大,因而不会出现过度拟合,所以我们想学多少就学多少。然而,由于数据集是相似的,我们可以通过利用以前的知识省去大量的工作。因此,我们只需要训练分类器和卷积基顶层就足够了。

象限3:小数据集并不同于预训练模型的数据集。这类数据集必将造成计算机视觉问题。一切都对你不利。此时抱怨没有用,唯一的希望就是选择策略2。你很难平衡需要训练和冻结的层数。如果你研究得过于深入,你的模型可能会发生过拟合现象,如果你停留在模型的浅层,你又学不到任何有用的东西。也许,与象限2相比,你需要更进一步,并且需要运用数据增强技术。

象限4:小型数据集,但类似于预训练模型的数据集。当我问尤达大师对此的看法时,他告诉我:“对于这类数据集,策略3是最好的选择”。我虽不了解他,但我不会低估他的“原力”。因此,我选择策略3。你只需要移除最后一个全连接层(输出层),将预训练模型作为固定的特征提取器运行,然后使用先前所得的特征来训练新的分类器。

图3和图4 大小-相似性矩阵(左)和用于微调预训练模型的决策图(右)

5.深度卷积神经网络上的分类器

如前所述,基于预训练卷积神经网络的迁移学习方法产生的图像分类模型通常由两部分组成:

1. 卷积基,用于执行特征提取。

2. 分类器,根据卷积基提取的特征对输入图像进行分类。

由于在本节中我们主要关注分类器部分,所以首先我们必须说明构建分类器的方法有多种,其中最常用的是:

1. 全连接层。处理图像分类问题,最佳方法是使用一堆全连接层,然后再用Softmax激活层。Softmax层输出在每个可能的类标签上的概率分布,然后我们只需要根据最可能的类对图像进行分类。

2. 全局平均池化。有人提出了一种基于全局平均池化的方法。使用这种方法,我们不需要在卷积基上添加全连接层,而是添加全局平均池层并将其输出直接馈送到softmax激活层。

3. 线性支持向量机。线性支持向量机(SVM)是另一种可用于构建分类器的方法。 我们可以通过训练线性SVM分类器对卷积基提取的特征进行分类来提高分类精度。本文也会进一步详细介绍SVM方法的优缺点。

6.实例

在本例中,我们将探究如何将每个分类器应用于图像分类的迁移学习解决方案中。“在深度卷积神经网络上比较不同分类器的性能仍然需要进一步研究,从而形成一个有趣的研究方向”。因此,观察每个分类器在标准图像分类问题中的表现将会很有趣。

6.1.准备数据

在本例中,我们将使用一个较小版本的原始数据集。这将利于我们更快地运行模型,且对于计算能力有限的人(比如我)来说也是一件好事。

为了构建较小版本的数据集,我们可以对以前的代码进行改编,如代码1所示:

# Create smaller dataset for Dogs vs. Cats

import os, shutil

original_dataset_dir = '/Users/macbook/dogs_cats_dataset/train/'

base_dir = '/Users/macbook/book/dogs_cats/data'

if not os.path.exists(base_dir):

os.mkdir(base_dir)

# Create directories

train_dir = os.path.join(base_dir,'train')

if not os.path.exists(train_dir):

os.mkdir(train_dir)

validation_dir = os.path.join(base_dir,'validation')

if not os.path.exists(validation_dir):

os.mkdir(validation_dir)

test_dir = os.path.join(base_dir,'test')

if not os.path.exists(test_dir):

os.mkdir(test_dir)

train_cats_dir = os.path.join(train_dir,'cats')

if not os.path.exists(train_cats_dir):

os.mkdir(train_cats_dir)

train_dogs_dir = os.path.join(train_dir,'dogs')

if not os.path.exists(train_dogs_dir):

os.mkdir(train_dogs_dir)

validation_cats_dir = os.path.join(validation_dir,'cats')

if not os.path.exists(validation_cats_dir):

os.mkdir(validation_cats_dir)

validation_dogs_dir = os.path.join(validation_dir, 'dogs')

if not os.path.exists(validation_dogs_dir):

os.mkdir(validation_dogs_dir)

test_cats_dir = os.path.join(test_dir, 'cats')

if not os.path.exists(test_cats_dir):

os.mkdir(test_cats_dir)

test_dogs_dir = os.path.join(test_dir, 'dogs')

if not os.path.exists(test_dogs_dir):

os.mkdir(test_dogs_dir)

# Copy first 1000 cat images to train_cats_dir

fnames = ['cat.{}.jpg'.format(i) for i in range(100)]

for fname in fnames:

src = os.path.join(original_dataset_dir, fname)

dst = os.path.join(train_cats_dir, fname)

shutil.copyfile(src, dst)

# Copy next 500 cat images to validation_cats_dir

fnames = ['cat.{}.jpg'.format(i) for i in range(200, 250)]

for fname in fnames:

src = os.path.join(original_dataset_dir, fname)

dst = os.path.join(validation_cats_dir, fname)

shutil.copyfile(src, dst)

# Copy next 500 cat images to test_cats_dir

fnames = ['cat.{}.jpg'.format(i) for i in range(250,300)]

for fname in fnames:

src = os.path.join(original_dataset_dir, fname)

dst = os.path.join(test_cats_dir, fname)

shutil.copyfile(src, dst)

# Copy first 1000 dog images to train_dogs_dir

fnames = ['dog.{}.jpg'.format(i) for i in range(100)]

for fname in fnames:

src = os.path.join(original_dataset_dir, fname)

dst = os.path.join(train_dogs_dir, fname)

shutil.copyfile(src, dst)

# Copy next 500 dog images to validation_dogs_dir

fnames = ['dog.{}.jpg'.format(i) for i in range(200,250)]

for fname in fnames:

src = os.path.join(original_dataset_dir, fname)

dst = os.path.join(validation_dogs_dir, fname)

shutil.copyfile(src, dst)

# Copy next 500 dog images to test_dogs_dir

fnames = ['dog.{}.jpg'.format(i) for i in range(250,300)]

for fname in fnames:

src = os.path.join(original_dataset_dir, fname)

dst = os.path.join(test_dogs_dir, fname)

shutil.copyfile(src, dst)

# Sanity checks

print('total training cat images:',len(os.listdir(train_cats_dir)))

print('total training dog images:',len(os.listdir(train_dogs_dir)))

print('total validation cat images:',len(os.listdir(validation_cats_dir)))

print('total validation dog images:',len(os.listdir(validation_dogs_dir)))

print('total test cat images:', len(os.listdir(test_cats_dir)))

print('total test dog images:', len(os.listdir(test_dogs_dir)))

代码1 给猫狗大战构建较小数据集

6.2.从卷积基中提取特征

卷积基将用于提取特征。这些特征将被输入到我们想要训练的分类器中,以便我们识别图像中是否有狗或猫。

同样,我们再次对代码进行改编。详情请看代码2:

# Extract features

import os, shutil

from keras.preprocessing.image import ImageDataGenerator

datagen = ImageDataGenerator(rescale=1./255)

batch_size = 32

def extract_features(directory, sample_count):

features = np.zeros(shape=(sample_count, 7, 7, 512)) # Must be equal to the output of the convolutional base

labels = np.zeros(shape=(sample_count))

# Preprocess data

generator = datagen.flow_from_directory(directory,

target_size=(img_width,img_height),

batch_size = batch_size,

class_mode='binary')

# Pass data through convolutional base

i = 0

for inputs_batch, labels_batch in generator:

features_batch = conv_base.predict(inputs_batch)

features[i * batch_size: (i + 1) * batch_size] = features_batch

labels[i * batch_size: (i + 1) * batch_size] = labels_batch

i += 1

if i * batch_size >= sample_count:

break

return features, labels

train_features, train_labels = extract_features(train_dir, train_size) # Agree with our small dataset size

validation_features, validation_labels = extract_features(validation_dir, validation_size)

test_features, test_labels = extract_features(test_dir, test_size)

代码2 卷积基中提取的特征

6.3.分类器

6.3.1.全连接层

我们提出的第一个解决方案基于全连接层。该分类器中添加了一堆全连接层,这些层由从卷积基础中提取的特征提给。

为了简单(和快速),我们将使用Chollet(2018)提出的解决方案,并对其稍加修改。

代码3是此方案使用的代码,图5和图6是其学习曲线。

# Define model

from keras import models

from keras import layers

from keras import optimizers

epochs = 100

model = models.Sequential()

model.add(layers.Flatten(input_shape=(7,7,512)))

model.add(layers.Dense(256, activation='relu', input_dim=(7*7*512)))

model.add(layers.Dropout(0.5))

model.add(layers.Dense(1, activation='sigmoid'))

model.summary()

# Compile model

model.compile(optimizer=optimizers.Adam(),

loss='binary_crossentropy',

metrics=['acc'])

# Train model

history = model.fit(train_features, train_labels,

epochs=epochs,

batch_size=batch_size,

validation_data=(validation_features, validation_labels))

代码3 全连接层解决方案

图5 全连接层解决方案的精确度

图6 全连接层解决方案的损失函数

对结果的简要概述:

1. 验证精度约为0.85,考虑到数据集的大小,此结果还算不错。

2. 模型过度拟合。训练曲线和验证曲线之间有很大的间隙。

3. 由于我们已经使用了dropout层,我们应该增加数据集的大小以改善结果。

6.3.2.全局平均池化

与前一种情况不同的是,这里我们将添加一个全局平均池层,并将其输出馈送到一个sigmoid激活层,而不是添加一堆全连接层。

注意,我们讨论的是sigmoid激活层而不是softmax激活层。我们之所以选择sigmoid作为激活函数,是因为在Keras中,为了执行二进制分类,应该将sigmoid作为激活函数,binary_crossentropy 作为损失函数。

代码4是构建分类器的代码,图7和8是结果学习曲线。

# Define model

from keras import models

from keras import layers

from keras import optimizers

epochs = 100

model = models.Sequential()

model.add(layers.GlobalAveragePooling2D(input_shape=(7,7,512)))

model.add(layers.Dense(1, activation='sigmoid'))

model.summary()

# Compile model

model.compile(optimizer=optimizers.Adam(),

loss='binary_crossentropy',

metrics=['acc'])

# Train model

history = model.fit(train_features, train_labels,

epochs=epochs,

batch_size=batch_size,

validation_data=(validation_features, validation_labels))

代码4 全局平均池化解决方案

图7 全局平均池化解决方案的精确性

图8 全局平均池化解决方案的损失函数

对结果的简要概述:

1. 验证精度与由全连接层解决方案得到的验证精度相似。

2. 此模型不发生之前出现的过度拟合现象。

3. 当模型停止训练时,损失函数仍然在减小。也许可以通过增加epoch的数量来改进模型。

6.3.3.线性支持向量机

这里,我们将根据卷积基提取的特征训练一个线性支持向量机(SVM)分类器。

对于此分类器的训练,传统的机器学习方法是首选。因此,我们将使用K-折交叉验证法来估计分类器的误差。使用K-折交叉验证法时,我们可以将训练集和验证集连接起来,以扩展训练数据(与前面的情况一样,测试集保持不动)。代码5显示了连接数据的过程。

# Concatenate training and validation sets

svm_features = np.concatenate((train_features, validation_features))

svm_labels = np.concatenate((train_labels, validation_labels))

代码5 数据连接

最后,我们必须知道SVM分类器有一个超参数。这个超参数是误差项的惩罚参数C。为了优化这个超参数的选择,我们将使用穷举网格搜索。代码6是用于构建这个分类器的代码,图9是其学习曲线。

# Build model

import sklearn

from sklearn.cross_validation import train_test_split

from sklearn.grid_search import GridSearchCV

from sklearn.svm import LinearSVC

X_train, y_train = svm_features.reshape(300,7*7*512), svm_labels

param = [{

"C": [0.01, 0.1, 1, 10, 100]

}]

svm = LinearSVC(penalty='l2', loss='squared_hinge') # As in Tang (2013)

clf = GridSearchCV(svm, param, cv=10)

clf.fit(X_train, y_train)

代码6 线性SVM解决方案

图9 线性SVM解决方案的精确性

对结果的简要概述:

1. 模型的精度为0.86左右,与之前的解决方案相近。

2. 角落处产生过度拟合。此外,训练精度总是1.0,这不常见,可视为过度拟合的标志。

3. 模型的精度应随着训练样本数量的增加而增加。然而,事实并非如此,这可能是因为过度拟合。当数据集增加时,模型将如何反应呢?这是一个有趣的问题。

7. 总结

本文中,我们:

1. 介绍了迁移学习、卷积神经网络和预训练模型的概念。

2. 定义了重新调整预训练模型的基本微调策略。

3. 描述了基于数据集的大小和相似性来决定应该使用哪种微调策略的结构化方法。

4. 列举了三种可用于从卷积基提取的特征顶部的分类器。

5. 为本文列举的三个分类器分别提供端到端的分类示例图。

编译组:王玲、韦振琛

相关链接:

https://www.kdnuggets.com/2018/12/solve-image-classification-problem-quickly-easily.html

如需转载,请后台留言,遵守转载规范

[注:本文部分图片来自互联网!未经授权,不得转载!每天跟着我们读更多的书]


互推传媒文章转载自第三方或本站原创生产,如需转载,请联系版权方授权,如有内容如侵犯了你的权益,请联系我们进行删除!

如若转载,请注明出处:http://www.hfwlcm.com/info/257686.html