原标题 | How to classify photos in 600 classes using nine million Open Images 作者 | Aleksey Bilogur 译者 | chesc、JadenNeal、小先生爱你 注:本文的相关链接请访问文末【阅读原文】
三明治,使用 Google Open Images Explorer可视化
如果你正在尝试构建一个图片分类器,但是需要训练集,你最好的选择是查看 Google Open Images 。
这个庞大的图像集包含了超过3000万张图片和1500万个边界框(标签),那是18TB的图像数据!
此外,Open Image相比其他同规模的图像数据集更加开放、更加容易获取。例如 ImageNet 具有限制性许可。
但是,对于开发者来说在单个机器上筛选这么多的数据是困难的。你需要下载和处理多个元数据的文件,遍历他们的存储空间(或申请访问Google Cloud bucket)。
另一方面,除此之外没有太多的开源的可用训练图像集,坦白地来说,创造和分享它们其实是一种痛苦的事情。
在这篇文章中,我们将会利用Open Images构建和分布一个简单的端到端的机器学习管道。
我们将看到如何利用Open Images边界框数据中包含的600个标签中的任何一个创建自己的数据集。
我们将通过建立“开放三明治”来展示我们的工作。这些都是简单、可重复的图像分类器,只为了回答一个古老的问题:汉堡包是三明治吗?
想看代码?你可以在GitHub上的存储库中进行操作。
在使用之前,我们需要下载相关数据。
在使用Google Open Images(或任何外部数据集)时,下载数据是核心挑战。没有简单的方法来下载数据的子集。我们需要编写一个脚本来为我们实现这份工作。
我写了一个Python脚本,用于在Open Images数据集中搜索你指定的关键字的元数据。它找到相应图像(在Flickr上)的原始URL,然后将它们下载到磁盘。
这也证明了Python的强大能力,只需50行的代码,即可完成所有这些工作:
import sys
import os
import pandas as pd
import requests
from tqdm import tqdm
import ratelim
from checkpoints import checkpoints
checkpoints.enable()
def download(categories):
# Download the metadata
kwargs = {'header': None, 'names': ['LabelID', 'LabelName']}
orig_url = "https://storage.googleapis.com/openimages/2018_04/class-descriptions-boxable.csv"
class_names = pd.read_csv(orig_url, **kwargs)
orig_url = "https://storage.googleapis.com/openimages/2018_04/train/train-annotations-bbox.csv"
train_boxed = pd.read_csv(orig_url)
orig_url = "https://storage.googleapis.com/openimages/2018_04/train/train-images-boxable-with-rotation.csv"
image_ids = pd.read_csv(orig_url)
# Get category IDs for the given categories and sub-select train_boxed with them.
label_map = dict(class_names.set_index('LabelName').loc[categories, 'LabelID']
.to_frame().reset_index().set_index('LabelID')['LabelName'])
label_values = set(label_map.keys())
relevant_training_images = train_boxed[train_boxed.LabelName.isin(label_values)]
# Start from prior results if they exist and are specified, otherwise start from scratch.
relevant_flickr_urls = (relevant_training_images.set_index('ImageID')
.join(image_ids.set_index('ImageID'))
.loc[:, 'OriginalURL'])
relevant_flickr_img_metadata = (relevant_training_images.set_index('ImageID').loc[relevant_flickr_urls.index]
.pipe(lambda df: df.assign(LabelValue=df.LabelName.map(lambda v: label_map[v]))))
remaining_todo = len(relevant_flickr_urls) if checkpoints.results is None else\
len(relevant_flickr_urls) - len(checkpoints.results)
# Download the images
with tqdm(total=remaining_todo) as progress_bar:
relevant_image_requests = relevant_flickr_urls.safe_map(lambda url: _download_image(url, progress_bar))
progress_bar.close()
# Write the images to files, adding them to the package as we go along.
if not os.path.isdir("temp/"):
os.mkdir("temp/")
for ((_, r), (_, url), (_, meta)) in zip(relevant_image_requests.iteritems(), relevant_flickr_urls.iteritems(),
relevant_flickr_img_metadata.iterrows()):
image_name = url.split("/")[-1]
image_label = meta['LabelValue']
_write_image_file(r, image_name)
@ratelim.patient(5, 5)
def _download_image(url, pbar):
"""Download a single image from a URL, rate-limited to once per second"""
r = requests.get(url)
r.raise_for_status()
pbar.update(1)
return r
def _write_image_file(r, image_name):
"""Write an image to a file"""
filename = f"temp/{image_name}"
with open(filename, "wb") as f:
f.write(r.content)
if __name__ == '__main__':
categories = sys.argv[1:]
download(categories)
有关代码的注释和使用说明,请参阅在原文的GitHub存储库。
该脚本可以下载原始图像的子集,其中包含我们选择类别的任意子集的边界框信息:
$ git clone https://github.com/quiltdata/open-images.git
$ cd open-images/
$ conda env create -f environment.yml
$ source activate quilt-open-images-dev
$ cd src/openimager/
$ python openimager.py "Sandwiches" "Hamburgers"
类别以分层的方式组织。
例如,三明治和汉堡都是食品的子标签(但汉堡不是三明治的子标签 )。
我们可以使用 Vega 将实体可视化为径向树:
你可以在原文查看此图表的交互式注释版本(并下载其代码)。
在Open Image中,并非所有类别都有与之关联的边界框数据。
但是这个脚本可以下载600个标签的任何子集。这是一种可能的情况:
足球、玩具、鸟、猫、花瓶、吹风机、袋鼠、刀子、公文包、铅笔盒、网球、钉子、高跟鞋、寿司、摩天大楼、树、卡车、小提琴、葡萄酒、轮子、鲸鱼、披萨刀、面包、直升机、柠檬、狗、大象、鲨鱼、花、家具、飞机、勺子、凳子、天鹅、花生、照相机、长笛、头盔、石榴、王冠……
正如本文的目的,我们将限制在两项上:汉堡包和三明治。
一旦我们运行脚本并对把图像下载到本地,我们就可以使用 matplotlib 检查它们,看看我们得到了什么:
import matplotlib.pyplot as plt
from matplotlib.image import imread
%matplotlib inline
import os
fig, axarr = plt.subplots(1, 5, figsize=(24, 4))
for i, img in enumerate(os.listdir('../data/images/')[:5]):
axarr[i].imshow(imread('../data/images/' + img))
来自Google Open Images V4的五个示例{汉堡包,三明治}图象。
这些图像不容易训练。使用公共网络的外部源构建的数据集拥有的所有问题,它们都会有。
这里的小样本示例展示了目标类中可能存在的不同大小、方向和遮挡。
在案例中,我们甚至没有成功下载实际图像。相反,我们有一个占位符告诉我们,我们想要的图像已被删除!
下载这些数据会让我们看到几千个像这样的样本图像。下一步是利用边界框信息将我们的图像剪切成只有三明治-y,汉堡包-y的部分。
下面是另一个图像数组,这次包含了边界框,来展示需要什么内容:
边界框。注意:(1)数据集包括“描述”(2)原始图像可以包含许多目标实例。
在GitHub存储库中的带注释的Jupyter notebook完成了这项工作。
我在这里不展示代码,因为它有点复杂。尤其是我们还需要(1)重构我们的图像元数据以匹配裁剪的图像输出,以及(2)提取已经删除的图像。如果您想查看代码,请务必查看notebook。
运行notebook代码后,我们在磁盘上将会有一个包含所有裁剪图像的images_cropped文件夹。
下载完数据,并且清洗数据之后,就可以准备训练模型了。
我们将在数据上训练一个卷积神经网路(CNN)。
CNN是一种特殊类型的神经网络,其在图像中常见的像素组中一步步构建更高级别的特征。
然后对这些不同特征上的图像分数进行加权以生成最终分类结果。
这种架构非常有效,因为它利用了局部性。这是因为任何一个像素与其附近的像素所拥有的共同点可能远远超过远处的像素。
CNN还具有其他吸引人的特性,如噪声容限和尺度不变性(在一定程度上)。这进一步改善了它们的分类属性。
如果你对CNN不熟悉,我建议先看看Brandon Rohrer的“神经网络是怎样工作的”,以了解它们。
我们将会训练一个非常简单的卷积神经网络,来看看在我们的问题上得到的不错的结果。我使用keras来定义和训练模型。
我们从将图片放到特定的路径结构下开始:
images_cropped/
sandwich/
some_image.jpg
some_other_image.jpg
...
hamburger/
yet_another_image.jpg
...
然后,我们使用以下代码将Keras指向此文件夹:
from keras.preprocessing.image import ImageDataGenerator
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,
fill_mode='nearest'
)
test_datagen = ImageDataGenerator(
rescale=1/255
)
train_generator = train_datagen.flow_from_directory(
'../data/images_cropped/quilt/open_images/',
target_size=(128, 128),
batch_size=16,
class_mode='binary'
)
validation_generator = test_datagen.flow_from_directory(
'../data/images_cropped/quilt/open_images/',
target_size=(128, 128),
batch_size=16,
class_mode='binary'
)
Keras将检查输入文件夹,并确定我们的分类问题中有两个类。它将根据子文件夹名称分配类名,并从这些文件夹中创建“图像生成器”。
但我们不只是返回图像本身。相反,我们返回经过随机二次采样、倾斜和缩放处理后的图像(通过 train_datagen.flow_from_directory)。
这是一个实际应用中数据增强的例子。
数据增强,是把经过随机裁剪和扭曲处理的输入数据集送入图像分类器。这有助于我们解决小规模数据集。我们可以在单个图像上多次训练我们的模型。每次我们以稍微不同的方式进行图像预处理,并使用一个稍微不同的图像片段。
在定义了数据输入后,下一步是定义模型本身:
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras.losses import binary_crossentropy
from keras.callbacks import EarlyStopping
from keras.optimizers import RMSprop
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), input_shape=(128, 128, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(32, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten()) # this converts our 3D feature maps to 1D feature vectors
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.compile(loss=binary_crossentropy,
optimizer=RMSprop(lr=0.0005), # half of the default lr
metrics=['accuracy'])
这是一个简单的卷积神经网络模型。它只包含三个卷积层:在输出层之前的单个密集连接的后处理层,强正则化形式的dropout层,和relu激活层。
所有这些因素共同作用,使得这个模型更难过拟合。这一点很重要,因为我们的输入数据集规模很小。
最后,最后一步实际上是拟合模型。
import pathlib
sample_size = len(list(pathlib.Path('../data/images_cropped/').rglob('./*')))
batch_size = 16
hist = model.fit_generator(
train_generator,
steps_per_epoch=sample_size // batch_size,
epochs=50,
validation_data=validation_generator,
validation_steps=round(sample_size * 0.2) // batch_size,
callbacks=[EarlyStopping(monitor='val_loss', min_delta=0, patience=4)]
)
model.save("clf.h5")
这个代码选择的迭代步数大小,是由我们的图像样本大小和所选的批次大小(16)决定的。然后它在该数据上训练50次迭代。
EarlyStopping 回调可能会提前暂停训练。如果在前四次迭代内没有看到验证分数的提高,则在50次迭代限制之前返回表现最佳的模型。
我们选择了如此大的 patience 值,因为在模型验证损失中存在大量的可变性。
这种简单的训练方案可以使模型具有约75%的准确度:
precision recall f1-score support
0 0.90 0.59 0.71 1399
1 0.64 0.92 0.75 1109
micro avg 0.73 0.73 0.73 2508
macro avg 0.77 0.75 0.73 2508
weighted avg 0.78 0.73 0.73 2508
有趣的是,我们的模型在对汉堡包(0类)进行分类时缺乏自信,但在对三明治(1类)进行分类时过于自信。
90%被归类为汉堡包的图片实际上是汉堡包。但只有59%的汉堡包是正确分类的。
另一方面,只有64%被归类为三明治的图像实际上是三明治。但92%的三明治是正确分类的。
Francois Chollet通过将一个非常相似的模型应用于一个同样大小的经典猫对狗数据集的子集,获得80%的准确率,我们的实验结果与这个准确率一致。
这种差异可能主要是由于Google Open Images v4数据集中的遮挡和噪声级别增加所致。
数据集还包括插图和摄影图像。这些有时会带来巨大的艺术自由,但也使得分类更加困难。您可以在构建模型时选择删除它们。
使用迁移学习技术可以进一步提高这种性能。要了解更多信息,请查看Keras作者Francois Chollet的博客文章“使用非常少的数据构建强大的图像分类模型”。
现在我们已经构建了一个自定义数据集并训练了一个模型,如果我们不共享它,那就太可惜了。
机器学习项目应该是可重复的。我在前一篇文章“用四行代码复现机器学习模型”中概述了以下策略。
将依赖项分离为数据,代码和环境三部分。
数据依赖项版本控制(1)模型定义和(2)训练数据。将这些保存到版本控制的blob存储,例如带有Quilt T4的Amazon S3。
代码依赖项版本控制用于训练模型的代码(使用git)。
环境依赖项版本控制用于训练模型的环境。在生产环境中,这可能是一个docker文件,但您可以在本地使用pip或conda。
为了向别人提供模型的可重新训练的副本,给他们相应的{data,code,environment}元组。
遵循这些原则,你就可以把所有需要用来训练自己的模型的东西,放进几行代码里:
git clone
conda env create -f open-images/environment.yml
source activate quilt-open-images-dev
python -c "import t4; t4.Package.install('quilt/open_images', dest='open-images/', registry='s3://quilt-example')"
要了解有关{data,code,environment}的更多信息,请参阅原文的GitHub存储库或相应的文章。
在本文中,我们演示了端到端图像分类的机器学习流程。我们介绍了从下载/转换数据集到训练模型的所有内容。然后我们以一种允许其他人在以后自行重建它的方式发布它。
由于自定义数据集很难生成和发布,随着时间的推移,出现了一系列示例数据集,可以在任何地方使用。这不是因为他们真的那么好(他们不好)。相反,这是因为它们很容易。
例如,谷歌最近发布的机器学习速成课程大量使用加州住房数据集。这些数据现在差不多已有二十年了!
考虑改为探索新的视野。使用来自互联网的真实图像进行有趣的分类细分。这与你仅仅想象相比会更加容易!
这篇文章有用吗?可以考虑关注AI研习社,并查看其他相关文章:
使用四行代码复现机器学习模型
利用机器学习和Quilt识别卫星图像中的建筑物
本文编辑:王立鱼
英语原文:https://blog.quiltdata.com/how-to-classify-photos-in-600-classes-using-nine-million-open-images-3cdb989ad1c2
想要继续查看该篇文章相关链接和参考文献?
点击底部【阅读原文】即可访问:
https://ai.yanxishe.com/page/TextTranslation/1624