Press "Enter" to skip to content

代码详解:用卷积神经网络及迁移学习识别犬种

 

本文将演示如何使用keras和tensorflow来构建、训练、测试能够识别特定图像中犬种的卷积神经网络。验证及测试的高准确率则表明测试成功,并以不同的精确率和召回率来区分准确率相近的模型。

 

这是一个监督学习问题,尤指多类别分类问题,因此可以通过以下步骤予以解决:

 

1. 收集标签数据。在本文案例中即指用已知犬种编译图像库。

 

2. 构建模型。该模型能从训练图像中提取数据并输出可以辨别的犬种数据。

 

3. 在训练数据上训练模型;在验证数据上验证性能。

 

4. 评价性能指标。该步骤可能返回步骤2重新进行建模以提高性能。

 

5. 在测试数据上测试模型。

 

每步都有许多子步骤,具体详情将在下文予以说明。

 

 

 

前言

 

从计算成本来看,训练神经网络非常昂贵,哪怕是相对简单的神经网络也绝不便宜。许多公司使用专门处理此类任务的专用图形处理器(GPU);本文则使用配备GTX 1070显卡(专门为该实验而装配)的本地个人计算机进行测试。要在本地计算机上执行此任务,必须执行以下几个步骤才能定义正确的编程环境,我将在此处予以详细的说明。如若对后端设置不感兴趣,请略过该部分。

 

首先,在Anaconda中创建新环境,并安装以下软件包:

 

• tensorflow-gpu

 

• jupyter

 

• glob2

 

• scikit-learn

 

• keras

 

• matplotlib

 

• opencv (用于图像或视频流的人脸识别技术——并非必备,但在某些应用中非常实用)

 

• tqdm

 

• pillow

 

• seaborn

 

接下来,更新显卡驱动;这极其重要,因为驱动程序会定期推出更新,即使对于已经推出3年之久的显卡也是如此,如果使用的tensorflow是最新版本,则需要配备最新驱动以实现兼容。

 

最后,从新环境的anaconda prompt中打开Jupyter Notebook,以便执行工作,并确保Jupyter正在为内核使用的环境是正确的。如果不这样做,Tensorflow可能会遇到问题。

 

发布模块导入,调用以下命令来显示可用的CPU和GPU以作为完整性检查。

 

from tensorflow.python.client import device_libprint(device_lib.list_local_devices())

 

 

GTX 1070相关配置安装完成!

 

最后,执行以下代码块,用来编辑tensorflow后端的一些配置参数,并防止运行错误。

 

# tensorflow local GPU configurationgpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.8)config = tf.ConfigProto()config.gpu_options.allow_growth = Truesession = tf.Session(config=config)

 

 

 

第1步: 编译数据

 

 

在本文实验中,这一步无足轻重,因为Udacity已经提供了囊括133个品种的1.08Gb的犬类图像,并且将这些犬类图像放置于适当的文件结构之中。对于使用keras构建的分类卷积神经网络而言,适当的文件结构指文件根据训练、验证和测试进行划分并根据犬种进一步细分至文件夹。每个文件夹的名称都与计划识别的类别名称保持一致。

 

显然易见,世界上犬种远超133种;美国养狗协会(AKC)列出了190种,而世界犬类联盟(FCI)则列出了360种。如果想要增加训练数据集的容量以囊括更多犬种,或者囊括每个品种的更多图像,可以通过安装python Flickr API实现,而且还可以查询任何标记犬种名称的图像。但是就本实验目的而言,基础数据集已经完全足够。

 

首先,将所有文件名加载到内存中便于处理。

 

# define function to load train, test, and validation datasets

 

def load_dataset(path):

 

data = load_files(path)

 

dog_files = np.array(data[ filenames ])

 

dog_targets = np_utils.to_categorical(np.array(data[ target ]))#, 133)

 

return dog_files, dog_targets

 

# load train, test, and validation datasets

 

train_files, train_targets = load_dataset( dogImages/train )

 

valid_files, valid_targets = load_dataset( dogImages/valid )

 

test_files, test_targets = load_dataset( dogImages/test )

 

# load list of dog names

 

# the [20:-1] portion simply removes the filepath and folder number

 

dog_names = [item[20:-1] for item in sorted(glob(“dogImages/train/*/”))]

 

# print statistics about the dataset

 

print( There are %d total dog categories. % len(dog_names))print( There are %s total dog images.

% len(np.hstack([train_files, valid_files, test_files])))

 

print( There are %d training dog images. % len(train_files))

 

print( There are %d validation dog images. % len(valid_files))

 

print( There are %d test dog images. % len(test_files)

 

输出以下统计数据:

 

There are 133 total dog categories.

 

There are 8351 total dog images.

 

There are 6680 training dog images.

 

There are 835 validation dog images.

 

There are 836 test dog images.

 

接下来,将图像的每个像素除以255来执行数据标准化步骤,并将输出格式化为张量(可以由keras使用的向量)。注意:以下代码将成千上万个文件作为张量加载到内存中。尽管使用相对较小的数据集就可以实现,但是最好还是使用一次仅能加载少量张量的批量加载系统。至于最后一个设计模型,将在后面的步骤中予以执行。

 

# define functions for reading in image files as tensors

 

def path_to_tensor(img_path, target_size=(224, 224)):

 

# loads RGB image as PIL.Image.Image type

 

# 299 is for xception, 224 for the other models

 

img = image.load_img(img_path, target_size=target_size)

 

# convert PIL.Image.Image type to 3D tensor with shape (224, 224, 3)

 

x = image.img_to_array(img)

 

# convert 3D tensor to 4D tensor with shape (1, (target_size,) 3) and return 4D tensor

 

return np.expand_dims(x, axis=0)

 

def paths_to_tensor(img_paths, target_size = (224, 224)):

 

list_of_tensors = [path_to_tensor(img_path, target_size) for img_path in tqdm(img_paths)]

 

return np.vstack(list_of_tensors)

 

# run above functions

 

from PIL import ImageFile

 

ImageFile.LOAD_TRUNCATED_IMAGES = True

 

# pre-process the data for Keras

 

train_tensors = paths_to_tensor(train_files).astype( float32 )/255

 

valid_tensors = paths_to_tensor(valid_files).astype( float32 )/255

 

test_tensors = paths_to_tensor(test_files).astype( float32 )/255

 

 

 

构建、训练、测试、评估

 

这一步可以采取许多种办法,其中一些要比另外一些效果更好。本文使用3种特殊的方法,并按照这三种方法进行构建、测试与评估。采取的方法如下:

 

1. 平凡解。在数据集上构建并训练一个非常简单的卷积神经网络,对其性能进行评估。

 

2. 进行迁移学习。利用经大规模图像库训练过的现有卷积神经网络将输入图像转换为“瓶颈特征”(表示图像的抽象特征)以适应应用程序。

 

3. 通过图像增强进行迁移学习。该方法类似于瓶颈特征方法,但是将尝试为应用程序创建模型来获得更优的模型泛化能力;创建的模型是一个预先训练且具有瓶颈特征并携带自定义输出层的卷积神经网络堆叠,其输入为经过象形转换随机增强的图像。

 

首先,将演示创建基本卷积神经网络以及在数据集上进行训练的简单方法。

 

步骤2a:构建平凡模型

 

使用带有tensorflow后端的keras创建一个带有以下代码的简易卷积神经网络。

 

from keras.layers import Conv2D, MaxPooling2D, GlobalAveragePooling2D

 

from keras.layers import Dropout, Flatten, Dense

 

from keras.models import Sequential

 

model = Sequential()

 

# Define model architecture.

 

model.add(Conv2D(16, kernel_size=2, activation= relu , input_shape=(224,224,3)))

 

# activation nonlinearity typically performed before pooling

 

model.add(MaxPooling2D()) # defaults to pool_size = (2,2), stride = None = pool_size

 

model.add(Conv2D(32, kernel_size=2, activation= relu ))

 

model.add(MaxPooling2D())

 

model.add(Conv2D(64, kernel_size=2, activation= relu ))

 

model.add(MaxPooling2D())

 

model.add(GlobalAveragePooling2D())

 

model.add(Dense(133, activation= softmax ))

 

model.summary()

 

使用model.summary()方法打印输出以下模型结构:

 

 

此处,利用3个与最大池化配对并终止与133个节点全连接层的卷积层创建一个8层顺序神经网络,其每一层对应一种预测类别。注意:密集层使用了归一化指数激活函数;原因是它的范围是0-1而且强制输出层中所有节点的总和为1。这就可以将单个节点的输出解释为模型预测概率,即输入是该节点所对应的类别。换句话说,如果图层中的第二个节点对于特定图像来说具有0.8的激活值,就可以说模型预测输入来自第二类的概率是80%。注意:19,000个模型参数指的是网络将尝试优化的权重、偏差以及卷积核(卷积滤波器)。现在应该明白为什幺这个过程就计算方面来说要求非常高。

 

最后,对该模型进行编译以便于训练。注意:这一步可以使用多种损失函数和优化器,但目前对多类别图像标签预测通常使用Adam作为优化器,使用分类交叉熵作为损失函数。我对Adam与SGD和RMSProp进行测试比较,发现Adam训练速度更快。

 

model.compile(optimizer=’adam’, loss=’categorical_crossentropy’, metrics=[‘accuracy’])

 

步骤3a:训练平凡模型

 

现在有大量应用于训练、验证和测试模型的张量,也有完全编译的卷积神经网络。训练之前,定义一个ModelCheckpoint对象,作为挂钩用来保存模型权重,以便将来进行简易加载而无需进行重新训练。使用关键字参数调用模型的.fit()方法来训练这个模型。

 

checkpointer = ModelCheckpoint(filepath= saved_models/weights.best.from_scratch.hdf5 , verbose=1, save_best_only=True)

 

model.fit(train_tensors, train_targets,

 

validation_data=(valid_tensors, valid_targets),

 

epochs=3, batch_size=20, callbacks=[checkpointer], verbose=2)

 

正如所见,因为由于它过于简单而不会具备高性能,所以只运行这个模型3个回合(1个回合等于使用训练集中的全部样本训练一次);这个模型纯粹只是为了演示。模型训练输出如下:

 

 

该模型以训练准确率1.77%,验证准确率1.68%结束训练。虽然该模型比随机猜测表现要好,但是也没有什幺值得详述的。

 

 

附注:训练此模型时,可以看到GPU使用率的跃升!这个情况十分令人喜闻乐见,表明着tensorflow后端确确实实使用了显卡。

 

步骤4a:评估平凡模型

 

 

该模型在训练或者验证数据方面没有达到合理的准确性,证明它与数据欠拟合。下图展示了模型预测的混淆矩阵以及分类报告。

 

该模型对于几乎每个输入图像都预测了两个类别之一。该模型似乎偏爱巴塞特猎犬和边境牧羊犬。但不幸的是,这一结果并不是因为创建了一个有情感的人工智能,有其偏爱的犬种,而仅仅是因为训练集中,所以巴塞特猎犬和边境牧羊犬的图片比其他犬种的图片要多一些。因为该模型严重欠拟合,所以不值得进一步探讨精确率和召回率。

 

步骤5a:测试平凡模型

 

最后,在测试数据集上测试模型。

 

# get index of predicted dog breed for each image in test set

 

dog_breed_predictions = [np.argmax(model.predict(np.expand_dims(tensor, axis=0))) for tensor in test_tensors]

 

# report test accuracy

 

test_accuracy = 100*np.sum(np.array(dog_breed_predictions)==np.argmax(test_targets, axis=1))/len(dog_breed_predictions)

 

print( Test accuracy: %.4f%% % test_accuracy)

 

这样可以获得符合预期的1.6746%测试准确率。如果对模型进行更多回合的训练,可能会获得更高的准确率,但是这只是一个极度简单的模型,修改模型架构也许是一个更好的选择。下文将演示使用迁移学习构建性能更佳、准确率更高的的模型。

 

步骤2b:构建瓶颈特征模型

 

利用迁移学习方法可显着提高性能,即利用已经预训练的现有卷积神经网络来识别一般图像数据的特征,并根据目的进行调整。Keras有许多这样的预训练模型可供下载使用。每个训练模型都在一个名为imagenet的图像库中进行训练的,该图像库包含数百万个图像,涵盖1000个类别。在imagenet上训练的模型通常是具有多个全连接输出层的深层卷积神经网络,通过训练可以将被卷积层暴露的隐藏特征分类为这1000个类别中的1个。选择其中一个预训练模型,然后将输出层简单替换为全连接层,可以对其进行训练,然后将每个输入图像分类为133个犬种之一。注意:这一步不再训练卷积神经网络;而是仅训练自定义输出网络并将卷积层的权重和内核冻结,至于这些卷积层,它们通过训练已经能够识别图像的抽象特征。这就节省了大量的时间。

 

至少有两种方法可用于这一步骤。一种是如上所述的方法,将预训练网络和自定义网络拼接在一起。另一种方法则更加简单,将数据集中的所有图片输入预训练网络,并将输出保存为数组并输入自定义网络。后一种方法的好处是可以节省计算时间,因为每个训练回合只是在自定义模型中进行前向传递和反向传递,并未在图像网模型和自定义模型中共同进行。方便的是,Udacity已经将他们提供的所有训练图像输入到一些内置的卷积神经网络中,并提供了原始输出或瓶颈特征,用户只需简单地输入数据即可。

 

此处定义了全连接网络,以接受瓶颈特征并输出133个节点,每个节点对应一个品种。该全连接网络将作为VGG16网络的示例。下一节将会介绍实际训练中使用的各种网络。

 

VGG16_model = Sequential()

 

VGG16_model.add(GlobalAveragePooling2D(input_shape=train_VGG16.shape[1:]))

 

VGG16_model.add(Dense(133, activation= softmax ))

 

注意事项:

 

• 以GlobalAveragePooling层开始,是因为VGG16的最后一层是卷积/池化序列,而事实上,测试的所有imagenet模型都是如此。全局池化层减少了输出的维度,并大大减少了进入密集层的训练时间。

 

• 自定义网络中第一层的输入形状必须根据其设计的模型进行定制。可以通过简单地获取瓶颈数据的形状以达成。第一个维度就是切除瓶颈特征形状允许keras添加一个维度用于批量处理。

 

• 再次使用归一化指数激活函数,原因与上文的平凡模型相同。

 

步骤3b:训练瓶颈特征模型

 

Udacity为4个网络提供了瓶颈功能,分别是VGG19、ResNet50、InceptionV3以及Xception。以下代码块会读取每个模型的瓶颈特征,并创建一个全连接输出网络,而且会对该全连接输出网络进行超过20个回合的训练。最后,它会输出每个模型的准确率。

 

 

从最后一行可以看出,4个模型都比上文的平凡卷积神经网络表现得更好,其中Xception模型的验证准确率甚至达到了85%!

 

步骤4b:评估瓶颈特征模型

 

Xception和ResNet50表现最佳,都具有卓越的验证准确率,但是通过日志挖掘可以发现,两者对训练数据的准确率接近100%。这是过度拟合的标志,但是这并不令人惊讶,因为Xception有2200万个参数,ResNet50有2300万个参数,也就意味着这两个模型都具有巨大的熵能力,并且能够记忆训练数据图像。为了解决这个问题,将对全连接模型做出调整并且重新训练。

 

 

我们已经添加了第二个密集层,希望模型能够减少对预训练参数的依赖,并且还增加了L2范数正则化和范数丢弃法以增强两个完全密集层。L2范数正则化惩罚个体参数权重过高的网络,并且L2范数丢弃法在训练期间随机丢弃网络节点。两者通过要求网络在训练期间更加泛化来避免过拟合。还要注意的是优化策略发生改变; 在真实的研究环境中,这将由能够接收大量超参(如具有大量超参的优化器)的GridSearch完成,但是出于时间考量,这里手动进行了尝试。注意:已经重新使用SGD,而且通过实验发现尽管Adam训练速度极快,可是在训练回合足够多的前提之下,SDG则会持续超越Adam。

 

100个回合(5分钟)的训练后:

 

 

该模型获得了与之前相当的验证准确率,但训练准确率却低得多。低训练准确率是由丢弃法引起的,因为丢弃法从不使用完整的模型来评估训练输入。我对这个模型非常满意,它不再像之前的模型那样过拟合;而且验证准确率和损失大致保持平衡。如果继续100个回合,准确率可能会减少1-2%,但是可以预先采用的训练技术却会更多。

 

步骤5b:测试瓶颈特征模型

 

 

测试数据集的准确率接近83%,与期望的验证集准确率非常相似。参见以下混淆矩阵:

 

 

该混淆矩阵比前一个表现更加优秀。可以看到,对于一些犬种,模型表现良好,但是对于另外一些犬种,模型表现得并不尽如人意。原因请参照下例。

 

放大y轴1/2处以及x轴1/4处左右的离群值。

 

 

该模型一直把第66类实际上辨认为第35类。也就是说,把田野小猎犬实际上当作博伊金猎犬。以下是两个犬种的图片。

 

 

田野小猎犬(左侧) 博伊金猎犬(右侧)

 

能够注意到任何相似之处吗?显然,两个犬种很难区分。对此,我认为哪怕调整模型参数,在这种情况下也不会取得建设性的改进;在实际场景中,可以通过训练二元分类器来区分两个犬种,并且如果基本模型能够预测出其中的一类,可以将其应用于分类预测传输中。然而,就目前而言,我比较感兴趣的是通过增加训练数据以获得更好的模型性能。

 

步骤2c:编译增强输入模型

 

在图像数据训练的模型中,存在一种被称为图像增强的训练数据重采样方法,可以在训练期间将随机旋转、变焦以及平移应用于训练图像。通过改变训练图像的像素来人为地增加训练数据大小,还可以保持内容的完整性;例如,我将一只柯基的图像旋转15度并且进行水平翻转,那幺图像应该仍然可以识别为柯基,但是模型在训练期间却不会看到同样的图像。这种技术能够在多回合训练中提高模型的准确率,还可以防止过拟合。为了应用这一方法,不能再使用之前用过的瓶颈特征,而且还要编译整个模型,使前向传播以及反向传播在两个模型(imagenet模型和自定义模型)中共同进行。另注意:因为我仍然不想编辑imagenet预训练模型的参数,所以在训练前会冻结这些图层。

 

首先,定义数据加载途径:

 

 

接下来,加载imagenet模型,定义一个自定义的全连接输出模型,将其组合成顺序模型。由于时间限制,此处切回Adam作为优化器。但是,如有更多的计算资源,那幺像之前那样使用SGD可能也是值得的。

 

 

步骤3c:训练增强输入模型

 

由于每个前向传播必须遍历imagenet模型的所有节点,所以该模型的训练时间比以前的任何模型需要花费的时间都要长。在GPU上,该模型每个回合的训练大约需要3.5分钟,这与瓶颈特征模型所需的仅仅数秒的时间大相径庭。这也暗示了之前使用瓶颈特征模型所获得的计算增益。

 

 

该模型以极快的速度在训练和验证数据集上实现了相对较高的准确率,这要归功于Adam优化器。 注意: 因为仍然使用了丢弃法,该模型的训练准确率仍然低于验证准确率。 另注意: 验证准确率发生很大变化,该变化可能是高学习率(Adam)或高熵能力(太多参数)的表征。 但是该模型似乎随着时间的推移而变得平稳,所以对此我并非十分担心。

 

步骤4c: 评估增强输入模型

 

通过审度该模型和前一个模型的分类报告,可以发现验证过程中两者的精确率以及召回率得分均为80。而两者最终的验证损失都在1左右,进一步表明模型性能没有任何改进。因为训练数据得以增强,所以希望看到的是准确率能够实现跃升,但是同时我认为它只需要一个数量级的更多的训练回合就能使改进变得明显。使用SGD分类器并施以更多回合的训练会有所帮助。

 

步骤5c:测试增强输入模型

 

在增强模型中输入测试数据和先前输入训练和训练数据的方法相同,都使用了keras ImageDataGenerator:

 

 

虽然该模型的测试准确率比瓶颈特征模型的测试准确率低了几个百分点,但是基本上差不多。

 

为了深入研究精确率和召回率,使用混淆矩阵进行与之前相同的分析:

 

 

真正有趣的是,我们发现异常值与之前相同,虽然异常值表现得更加不明显,但是这也表明这个模型在区分猎犬犬种方面要性能略佳。可是,在矩阵中心附近还有另一个明亮的异常值,经过放大发现这个模型无法区分德国杜宾犬和德国宾莎犬。

 

 

德国杜宾犬(左侧) 德国宾莎犬(右侧)

 

细细揣摩吧。

 

 

 

最终结果

 

最后一步就是编写一个函数,这个函数从头开始加载一个给定模型并接受图像数据作为输入,最后输出品种预测。我将继续增强图像模型,因为我相信未来可以通过更多的训练来提升其性能。

 

在测试文中每个模型的训练时,将模型的参数保存到.hdf5文件中。因此,一旦训练结束,鉴于知道如何编译想要使用的模型,就可以输入命令从上一次训练中加载最佳权重。然后,在预测函数中,只需要重新创建训练期间执行的图像处理步骤即可。

 

 

因为我们已将模型权重存储在外部文件中,也知道如何重新创建模型架构,所以,将所述的模型打包以供任何地方使用(包括网端或移动应用程序)。

 

代码传送门: https://github.com/jfreds91/DSND_t2_capstone

 

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注