Press "Enter" to skip to content

代码详解:用深度学习在Keras中对蝴蝶进行分类

 

荷兰一个组织Vlinderstichting每年都会收集大量的蝴蝶。一些志愿者会帮忙分辨花园中蝴蝶的类别,Vlinderstichting则负责收集信息并分析结果。

 

由于是志愿者确定蝴蝶种类,所以不可避免会发生错误,进而导致Vlinderstichting工作人员要亲自检查提交的信息,这非常浪费时间。

 

具体一点,有三种蝴蝶的类别许多人都判断有误。这些是

 

•  Meadow brown or Maniola jurtina

 

• Gatekeeper or Pyronia Tithonus

 

• Small heath or Coenonympha pamphilus

 

本文会讲述利用深度学习模型来区分前两种蝴蝶的步骤。

 

 

 

使用Flickr API下载图像

 

为了训练卷积神经网络,需要找正确分类的蝴蝶图像。为了提高效率,我们需要一种自动化方式获取图像——通过Python使用Flickr API。

 

设置Flickr API

 

首先,用pip下载flickrapi安装包(https://pypi.org/project/flickrapi/2.3/)。然后在Flickr网页(https://www.flickr.com/services/api/misc.api_keys.html)上创建API密钥来连接Flickr API。

 

除了flickrapi包以外,还需要导入用于下载图像和设置目录的os及urllib包。

 

from flickrapi import FlickrAPI

 

import urllib

 

import os

 

import config

 

在配置模板中,为Flickr API定义公钥和密钥。这只是有着如下编码的Python脚本( config.py ):

 

API_KEY = 'XXXXXXXXXXXXXXXXX'  // replace with your key

 

API_SECRET = ‘XXXXXXXXXXXXXXXXX’  // replace with your secret

 

IMG_FOLDER = ‘XXXXXXXXXXXXXXXXX’  // replace with your folder to store the images

 

出于安全考虑,把这些钥匙都单独列在一个文件当中。这样就可以将代码保存在像GitHub或BitBucket这样的公共存储库中,将config.py放在.gitignore中。代码也就可以实现共享,不必担心他人访问你的凭证。

 

为了将不同类别的蝴蝶图像分开,我们编写了一个函数download_flickr_photos,对此将会展开详细解释(https://github.com/bertcarremans/Vlindervinder/tree/master/flickr)

 

输入参数

 

首先,检查输入的参数类型和值是否正确。如果没错,就提出一个问题。参数的说明可以在函数的docstring中找到。

 

if not (isinstance(keywords, str) or isinstance(keywords, list)):

 

raise AttributeError(‘keywords must be a string or a list of strings’)

 

if not (size in [‘thumbnail’, ‘square’, ‘medium’, ‘original’]):

 

raise AttributeError(‘size must be “thumbnail”, “square”, “medium” or “original”‘)

 

if not (max_nb_img == -1 or (max_nb_img > 0 and isinstance(max_nb_img, int))):

 

raise AttributeError(‘max_nb_img must be an integer greater than zero or equal to -1’)

 

其次,定义后面walk方法中会用到的一些参数。创建关键字的列表,选定从哪个URL下载图像。

 

if isinstance(keywords, str):

 

keywords_list = []

 

keywords_list.append(keywords)

 

else:

 

keywords_list = keywords

 

if size == ‘thumbnail’:

 

size_url = ‘url_t’

 

elif size == ‘square’:

 

size_url = ‘url_q’

 

elif size == ‘medium’:

 

size_url = ‘url_c’

 

elif size == ‘original’:

 

size_url = ‘url_o’

 

连接到Flickr API

 

调用Flickr API时,使用配置模块中定义的API键。

 

flickr = FlickrAPI(config.API_KEY, config.API_SECRET)

 

给每个蝴蝶类别创建子文件夹

 

将不同种别的蝴蝶图像存放在不同的子文件夹中。以蝴蝶名字命名每个文件夹,由关键字提供。若子文件夹尚不存在,就创建它。

 

results_folder = config.IMG_FOLDER + keyword.replace(" ", "_") + "/"

 

if not os.path.exists(results_folder):

 

os.makedirs(results_folder)

 

浏览Flickr库

 

photos = flickr.walk(

 

text=keyword,

 

extras=’url_m’,

 

license=’1,2,4,5′,

 

per_page=50)

 

用Flickr API的walk方法搜索指定关键字的图像。此法与Flickr API中搜索法用到的参数相同。

 

在文本参数中,利用关键字搜索相关图像。然后在附加参数中,为中小型图像制定url_m。更多关于图像大小和URL的说明请见Flickcurl C库(http://librdf.org/flickcurl/api/flickcurl-searching-search-extras.html)。

 

接着,在许可参数中选择非商业许可的图像。有关许可编码及其含义的更多信息可参阅Flickr API平台。最后,per_page参数会指定每页的图像个数。

 

因此,便得到一个名为photos的生成器来下载图像。

 

下载Flickr图像

 

有了photos生成器,就可以下载所有搜索查询中的图像。先找到专门下载图像的URL,再增加count变量,用此counter创建图像的文件名。

 

用urlretrieve方法下载图像,保存在蝴蝶种类文件中。发生错误的话,就打印出错误信息。

 

for photo in photos:

 

try:

 

url=photo.get(‘url_m’)

 

print(url)

 

count += 1

 

urllib.request.urlretrieve(url,  results_folder + str(count) +”.jpg”)

 

except Exception as e:

 

print(e, ‘Download failure’)

 

为了下载多种蝴蝶图像,创建一个列表,在for循环中调用download_flickr_photos函数。为了省事,我们只下载了上述三种蝴蝶的其中两种图像。

 

butterflies = ['meadow brown butterfly', 'gatekeeper butterfly']

 

for butterfly in butterflies:

 

download_flickr_photos(butterfly)

 

 

 

图像数据扩充

 

用少量图像训练convnet会导致过度拟合。结果会造成模型在分类未见过的新图像时产生错误。而数据扩充可以避免这一点。幸运的是,Keras中有一些不错的工具可以轻松地转换图像。

 

训练规模更大,分类越不容易出错。所以我们需要给convnet提供比目前掌握的还要多的蝴蝶图像。一种简单的解决方案就是数据扩充。简单来讲,就是对Flickr图像应用一组转换。

 

Keras提供了广泛的图像转换(https://keras.io/preprocessing/image/)。但首先,必须转换图像,Keras才能处理它们。

 

将图像转换成数字

 

首先导入Keras模块。用一个图像的例子来解释图像转换。再次用load_img方法。

 

from keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img

 

i = load_img(‘data/train/maniola_jurtina/1.jpg’ )

 

x = img_to_array(i)

 

x = x.reshape((1,) + x.shape)

 

load_img的方法是创建一个Python图像库文件。需要将其转换为Numpy数组以便在后期ImageDataGenerator方法中使用,会通过img_to_array法实现。最后会得到75x75x3大小的数组。这些维度反映了宽度、高度和RGB值。

 

实际上,图像的每个像素都有3个RGB值。范围介于0到255之间,反映了红色、绿色和蓝色的强度。值的高低与强度大小成反比。比如,一个像素用[ 78, 136, 60]这三个值表示。黑色就是[0, 0, 0]。

 

最后,在转换时需要添加一个额外维度来避免数值错误的发生。这一步用到的是重塑功能。

 

继续进行转换。

 

旋转

 

在0到180之间指定一个值,Keras会随机挑选一个角度旋转图像。要幺顺时针要幺逆时针。在本文例子中,图像最多旋转90°。

 

图像数据生成器也有fill_mode参数。默认值是最接近的。通过在原始图像宽度和高度内旋转图像,最后会得到“空”像素。fill_mode便会用最接近的像素填充空白部分。

 

imgGen = ImageDataGenerator(rotation_range = 90)

 

i = 1

 

for batch in imgGen.flow(x, batch_size=1, save_to_dir=’example_transformations’, save_format=’jpeg’, save_prefix=’trsf’):

 

i += 1

 

if i > 3:

 

break

 

在flow法中会确定转换图像的地方。确保该目录存在!同时为了方便也会给新建图像名称加前缀。flow方法可以无限运用,但本例中生成3张图像就行。所以counter达到这一值时,打破for循环。结果如下:

 

 

宽度偏移

 

在width_shift_range参数中,指定允许图像左右移动的原始宽度比率大小。fill_mode会再一次填充像素新产生的空缺部分。对于剩下的示例,介绍如何用不同参数将图像生成器实例化。用于生成图像的编码与旋转例子中的一样。

 

imgGen = ImageDataGenerator(width_shift_range = 90)

 

在转换后的图像中会看到图像右移了。空像素一经填充就有种向外延伸到的感觉。

 

上移或下移也可以通过给height_shift_range参数指定一个值完成。

 

 

重新调节

 

重新调节图像会在其他任何一道程序前选一个值来处理每个像素的多个RGB值。本例用了最小-最大缩放法处理值。结果是这些值会介于0到1之间。这让值变得更小,模型处理起来也更简单。

 

imgGen = ImageDataGenerator(rescale = 1./255)

 

 

剪切

 

用shear_range参数,可以指定剪切转换的应用方式。该转换在值过高的情况下也不会生成太奇怪的图像。不过不要设置太高的值。

 

imgGen = ImageDataGenerator(shear_range = 0.2)

 

 

缩放

 

这个转换会在图片内部进行缩放。就像裁剪参数一样,为了保持图像真实度,值不应过大。

 

imgGen = ImageDataGenerator(zoom_range = 0.2)

 

水平翻转

 

该转换会水平翻转图像。生活有时会很简单……

 

imgGen = ImageDataGenerator(horizontal_flip = True)

 

 

结合所有转换

 

既然已经见过每个转换的效果了,就一起运用所有转换。

 

imgGen = 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)

 

i = 1

 

for batch in imgGen.flow(x, batch_size=1, save_to_dir=’example_transformations’, save_format=’jpeg’, save_prefix=’all’):

 

i += 1

 

if i > 3:

 

break

 

 

设置文件夹结构

 

将这些图像保存在特定的文件夹结构中。因此可以使用flow_from_directory法扩充图像,创建相应标签。该文件夹结构需要像这个样子:

 

•  train

 

•   maniola_jurtina

 

•  0. jpg

 

•  1. jpg

 

•   ……

 

•   pyronia_tithonus

 

•  0. jpg

 

•  1. jpg

 

•   ……

 

•   validation

 

•  maniola_jurtina

 

•  0. jpg

 

•   1.jpg

 

•   ……

 

•   pyronia_tithonus

 

•  0. jpg

 

•  1. jpg

 

•   ……

 

为了建立该结构,我们创建了gist img_train_test_split.py(https://gist.github.com/bertcarremans/679624f369ed9270472e37f8333244f5)。

 

创建生成器

 

正如之前所讲,给训练生成器指定结构参数。验证图像不会转换成训练图像。只是划分RGB值使其更小。

 

flow_from_directory法从训练或验证文件夹中获取图像,生成32批转换后的图像。通过设置class_mode为“二进制”,就可以在图像文件名的基础上创建一维标签。

 

train_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)

 

validation_datagen = ImageDataGenerator(rescale=1./255)

 

train_generator = train_datagen.flow_from_directory(

 

‘data/train’,

 

batch_size=32,

 

class_mode=’binary’)

 

validation_generator = validation_datagen.flow_from_directory(

 

‘data/validation’,

 

batch_size=32,

 

class_mode=’binary’)

 

不同尺寸图像如何处理?

 

Flickr API会下载特定尺寸大小的图像。然而在现实应用程序中,图像的尺寸不是一直不变的。假如纵横比相同,只需重新调整图像大小即可,要幺就是裁剪图像。不巧的是,裁剪图像的同时保证目标物像完整很难。

 

Keras可以处理不同大小的图像。配置模型时,可以在input_shape给宽度和高度输入“空值”。

 

input_shape=(3, None, None)  # Theano

 

input_shape=(None, None, 3)  # Tensorflow

 

我们想展示处理不同大小的图像是可行的,不过有一些缺点。

 

•   不是所有图层(如拼合)会用“空值”作为输入维度。

 

•   运行计算量可能很大

 

 

 

建立深度学习模型

 

接下来探讨卷积神经结构,并通过蝴蝶项目中的一些实例进行说明。文末会有首次分类的结果。

 

卷积神经网络由哪些层组成?

 

当然了,可以选择层数和类型添加到卷积神经网络中(也称CNN或convnet)。在这个项目中可以从以下结构入手:

 

 

来了解一下每一层都是干什幺的以及如何用Keras创建它们。

 

输入层

 

这些不同版本的图像通过几个转换进行了修饰。然后将图像转换成数字表示或矩阵。

 

该矩阵的维数是宽×高×(颜色)通道数。对于RGB图像而言通道数为三。对于灰度图像而言为一。下面可以看到7×7图像的数字表示。

 

 

若图像大小为75×75,需要在添加第一个卷积层时在input_shape参数中指定。

 

cnn = Sequential()

 

cnn.add(Conv2D(32,(3,3), input_shape = (3 ,75 ,75)))

 

卷积层

 

在第一层,卷积神经网络会寻找低层特征,像水平或垂直边缘。随着对神经网络的深入,会发现它在寻求高层特征,比如蝴蝶翅膀。但它又是如何在只有数字作为输入时探寻特征呢?

 

过滤器(或内核)

 

你可以将过滤器视为扫描图像特定大小的探照灯。下面过滤器这个例子的尺寸是3×3×3,含有检测垂直边缘的权值。对于灰度图像,尺寸是3x3x1。通常情况下,过滤器的尺寸比想要分类的图像小。3×3、 5×5或7×7是常用的。第三维度应该总是等于通道数。

 

 

在扫描图像时,RGB值就会进行转换,通过RGB值与过滤器权值相乘来实现。最后,对所有通道上的值求和。在7x7x3实例图像和3x3x3过滤器中,相乘后的结果会是5x5x1的尺寸。

 

下图动画演示了此种卷积操作。为了方便起见,只在红色通道内寻找垂直边缘。所以绿色和蓝色通道权值都为0。但要记住这些通道上相乘得到的结果要添加到红色通道结果中。

 

如同下图所示,卷积层会产生数字。数字越高,就意味着过滤器寻找到了特征。本例就是垂直边缘。

 

 

我们可以指定需要更多的过滤器。这些过滤器会利用各自的特征来寻找图像。假设用32个尺寸为3x3x3的过滤器,所有过滤器的结果堆在一起,就本例来说,最终得到5x5x32的大小。在上面代码片段中,加入了32个3x3x3的过滤器。

 

行跨度

 

上面的例子中,可以看到过滤器每次会提升一个像素级别。这就叫行跨度。可以提高过滤器来提升的像素数。增大行跨度会更快地减少原始图像的大小。在下例中会看到过滤器如何在跨度为2时移动,最后3x3x3的过滤器和7x7x3的图像会得到3x3x1的结果。

 

 

填充

 

利用过滤器,原始图像的大小会快速减小。特别是图像边缘的像素只会在卷积操作过程中使用一次,这样就会导致信息流失。想要避免这种情况,可以指定填充。填充就是在图像周围加“额外像素”。

 

假设在7x7x3大小的图像周围填充一个像素,结果会得到9x9x3的图像。应用3x3x3的过滤器和1个行跨度,最后会是7x7x1的结果。所以在那种情况下,保留原始图像的大小,不止一次地使用外部像素。

 

可以用以下填充和行跨度来计算卷积操作的结果:

 

1+[(原始大小+填充量x 2-过滤器大小)/行跨度大小]

 

举个例子,假设有这个向量卷积运算的设置:

 

•   7x7x3的图像

 

•   3x3x3的过滤器

 

•   填充1个像素

 

•   行跨度为2个像素

 

给出的运算为1 + [(7 + 1 x 2–3) / 2] = 4

 

为何需要卷积层

 

使用conv层的一个好处是估测的参数数量会少得多。比正常情况下隐藏层的数量少很多。假设继续在无填充、跨度为1的条件下对7x7x3的图像和3x3x3的过滤器进行卷积操作。卷积层会有5x5x1 + 1的偏差=26个权值要估算。在输入为7x7x3、隐藏层神经元为5x5x1的神经网络中,需要估测3,675个权值。想象一下图像更大的时候这个数字会是多少……

 

激活函数层

 

或者整流线形单位图层。此图层给网络增添了非线性。卷积层是直线层,它会把过滤权值与RGB值的乘积加到一起。

 

对于所有值为x <= 0,激活函数后结果等于0。或者就等于x值。Keras中添加的激活函数层代码为:

 

cnn.add(Activation(‘relu’))

 

池化层

 

池化将输入值聚到一起,从而进一步减小大小。因为估测的参数数量减少了,计算时间就会加快。除此之外,通过加固网络会帮助避免过度拟合。下面会用大小为2×2跨度为2来阐述最大池化。

 

 

Keras中用于添加池化层的大小为2×2的代码是:

 

cnn.add(MaxPooling2D(pool_size = (2 ,2)))

 

最后,convnet就可以在输入的图像中探测到更高层特征。这个接着可以作为完全连接层的输入。在能进行此操作之前,平铺最后的激活函数层。平铺意味着把它转换成一个向量。向量值就会与完全连接层中的所有神经元连接上。为了在Python中进行此操作,利用下述Keras功能:

 

cnn.add(Flatten())

 

cnn.add(Dense(64))

 

随机失活

 

就像池化一样,随机失活可以帮助规避过度拟合。在模型的训练过程中,随机设定输入的指定部分为0。随机失活率在20%到50%之间,说明工作良好。

 

cnn.add(Dropout(0.2))

 

Sigmoid 激活

 

想要制造一种可能性——图像是两类蝴蝶之一(即二进制分类),可以借助sigmoid激活函数层。

 

cnn.add(Activation('relu'))

 

cnn.add(Dense(1))

 

cnn.add(Activation( ‘sigmoid’))

 

对蝴蝶图像应用卷积神经网络

 

现在可以将完整的卷积神经网络结构定义为本文一开始展示的那种。第一,需要导入必要Keras模块。然后可以开始添加之前讲过的图层。

 

from keras.models import Sequential

 

from keras.layers import Conv2D, MaxPooling2D

 

from keras.layers import Activation, Flatten, Dense, Dropout

 

from keras.preprocessing.image import ImageDataGenerator

 

import time

 

IMG_SIZE = # Replace with the size of your images

 

NB_CHANNELS = # 3 for RGB images or 1 for grayscale images

 

BATCH_SIZE = # Typical values are 8, 16 or 32

 

NB_TRAIN_IMG = # Replace with the total number training images

 

NB_VALID_IMG = # Replace with the total number validation images

 

我们为conv层清晰地列了一些附加参数。这是个简单的介绍:

 

•   kernel_size指定过滤器大小。所以对于第一个conv图层,大小为2×2

 

•   池化= ‘相同’意味着在保留原始图像的基础上应用零池化

 

•   池化= ‘有效’意味着不需要任何池化

 

•  只是用于指定颜色通道数在input_shape中是最后确立的

 

cnn = Sequential()

 

cnn.add(Conv2D(filters=32,

 

kernel_size=(2,2),

 

strides=(1,1),

 

padding=’same’,

 

input_shape=(IMG_SIZE,IMG_SIZE,NB_CHANNELS),

 

data_format=’channels_last’))

 

cnn.add(Activation(‘relu’))

 

cnn.add(MaxPooling2D(pool_size=(2,2),

 

strides=2))

 

cnn.add(Conv2D(filters=64,

 

kernel_size=(2,2),

 

strides=(1,1),

 

padding=’valid’))

 

cnn.add(Activation(‘relu’))

 

cnn.add(MaxPooling2D(pool_size=(2,2),

 

strides=2))

 

cnn.add(Flatten())

 

cnn.add(Dense(64))

 

cnn.add(Activation(‘relu’))

 

cnn.add(Dropout(0.25))

 

cnn.add(Dense(1))

 

cnn.add(Activation(‘sigmoid’))

 

cnn.compile(loss=’binary_crossentropy’, optimizer=’rmsprop’, metrics=[‘accuracy’])

 

最后,编制该网络结构,将损失的参数设为二元交叉熵( binary_crossentropy ),这对二进制目标有作用,将精确度作为评估指标。

 

在确立了神经网络结构后,创建用于训练和验证样本的生成器。在训练样本时,应用上述的数据扩充法。验证时,不进行任何扩充,因为扩充只是为了评估模型的性能。

 

train_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)

 

validation_datagen = ImageDataGenerator(rescale = 1./255)

 

train_generator = train_datagen.flow_from_directory(

 

‘../flickr/img/train’,

 

target_size=(IMG_SIZE,IMG_SIZE),

 

class_mode=’binary’,

 

batch_size = BATCH_SIZE)

 

validation_generator = validation_datagen.flow_from_directory(

 

‘../flickr/img/validation’,

 

target_size=(IMG_SIZE,IMG_SIZE),

 

class_mode=’binary’,

 

batch_size = BATCH_SIZE)

 

有了生成器上的flow_from_directory,可以在指定目录中轻松浏览所有图像。

 

最后,可以用卷积神经网络训练数据,评估验证数据。模型的权值结果可以保存下来,后期也能使用。

 

start = time.time()

 

cnn.fit_generator(

 

train_generator,

 

steps_per_epoch=NB_TRAIN_IMG//BATCH_SIZE,

 

epochs=50,

 

validation_data=validation_generator,

 

validation_steps=NB_VALID_IMG//BATCH_SIZE)

 

end = time.time()

 

print(‘Processing time:’,(end – start)/60)

 

cnn.save_weights(‘cnn_baseline.h5’)

 

任意设置纪元为50。一纪元就是前向传播的周期,会检查错误,然后在后向传播时调整权值。

 

参数steps_per_epoch设为训练图像的数量除以批次大小(顺便说一下,双除法符号会确保结果是整数而不是浮点数)。指定一个比笔者还大的批次大小会加速该过程。Idem代表validation_steps参数。

 

结果

 

运算50个纪元后,训练的精确度是0.8091,验证的精确度是0.7359.所以卷积神经网络还存在一些过度拟合的情况。同时可以看出验证精确度有着很大差异。这是因为验证样本数组较小。针对每一轮评估最好都用k-fold进行交叉验证。

 

为了解决过度拟合现象,我们可以:

 

•   提升随机失活率

 

•   在每一图层都运用随机失活

 

•   寻找更多的训练数据

 

我们着眼于前两个操作,检测了结果。第一个模型的结果会作为基线。额外应用随机失活函数层并提升随机失活率后,模型的过度拟合状况有所好转。

 

 

所有的代码都可以在Github上找到:https://github.com/bertcarremans/Vlindervinder

 

Be First to Comment

发表回复

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