Simulator Screen Shot - iPhone 8 Plus - 2018-07-23 at 01.00.43

前情提要:教你的 iPhone 认识 Gogoro 换电站(Part 1)- 用黑苹果电脑玩转最夯的机械学习

前一篇文章中,第一次尝试从无到有完成整个机械学习影像辨识的练习过程,最终得到一个「必须以半作弊的方式得到貌似可用的训练模型」这种不太满意的结果,后续也留下许多问题留待后续探讨。接下来这段时间里,我一直不断尝试各种手段,增加辨识成功的机率,不过终究效果有限,误判的原因也越来越难深究,以至于教学课程训练好的范例资料可用,自己训练出来资料却不能用。才正要开心的向前踏出一步,接著又卡关了,实在很灰心!

于是又再度上谷歌大海中漂流,反复尝试搜寻自己可能遗漏了什么关键字,一次又一次的搜寻,一次又一次的触碰艰深难懂也始终搞不懂的演算法议题。在某一次的资料搜寻中,搜到一张图片:

dogs

一目了然...这不就是我想要的辨识结果吗?在上次的操作练习中,是针对整张图的辨识,也导致「Gogoro 换电站」的训练资料中,周遭伴随一堆地景地物而干扰学习结果,我总算意识到自己忽略的地方。顺著这张图的线索,找到图片的来源:

Turi Create - Object detection

在这里出现了新的关键字「Object Detection」(物体侦测),我终于搞懂卡关的原因了!原来我一直把「影像辨识」(Image Classification)与「物体侦测」(Object Detection)两者混为一谈了,机械学习的应用方式错误,所以始终摆脱不了辨识结果遭误判的命运。来源网址给出另一个关键字「Turi Create」,也找到了「Turi Create」专案的 Github 主页:

GitHub - apple/turicreate

蛇嘛?!虾毁?这是苹果在 GitHub 上的专案?什么时候苹果给出这玩意但我不知道,我意识到自己肯定又忽略了什么,再度回到苹果的开发者网页,重新浏览了一遍,才发现苹果早在「Machine Learning - Working with Core ML Models」这个专题页面中,就提过这玩意儿。这网页我不知刷了多少回都没注意到,正所谓「直觉往往是盲点」,以为又是难搞的第三方学习套件,所以就自动忽略。

好吧!就承认自己没把书读好,绕了太多冤枉路,「尝试错误是学习的方法之一」,能发现自己在学习之路上犯下的错误,表示自己又有向前跨步的可能。

既然「Turi Create」是苹果在 GitHub 上发表的专案,应该要好好搞懂「Turi Create」是怎么一回事,希望这玩意儿如同苹果自己在专案页上宣称「 You don’t have to be a machine learning expert to...」(您不需具备机械学习专家知识就能 ooxx ...)。

终于,透过 Turi Create 的 用户指南网页(User Guide),我总算完成第一个,从无到有的资料收集、整理,最终自己也觉得满意的训练模型,让 iPhone 能「真正认识」 Gogoro 换电站惹!

这篇文章是简单纪录我的「Turi Create - Object Detection」学习过程。先说结论:与前一篇文章中使用  Xcode 内建的「Create ML」的无脑训练方式相比, Turi Create 还是有一定程度的难度,不能像 Create ML 滑鼠拖拉简单操作的方式。难度主要有两个:

  • 具备基本的 Python 语言基础:毫无疑问,目前 Python 语言是机械学习入门基础。过程中无法避免需要撰写 Python 程式。
  • 资料标注(Annotations)作业:这是真正的苦工,也直接影响模型的学习成果。

首先,当然是先把 Turi Create 装起来,并顺著 Turi Create 的 Object-Detection User Guide 文件,一步一步的做下去...

准备工作:

  • 一台运作 macOS 10.13 以上,或 Ubuntu 16.04 的电脑。强烈推荐 macOS 10.14,因为 Turi Create 在 5 版以后,在 macOS 10.14 会自动以可用的 GPU 加速运算,训练出来的模型也能直接用 iOS App 测试,本文以 macOS 10.14 为例。
  • 安装 Xcode 10 (beta) 。

这两个条件和前一篇文章几乎一样,如何安装 macOS 10.14 Mojave 与 Xcode,请参阅前一篇文章「教你的 iPhone 认识 Gogoro 换电站(Part 1) - 用黑苹果电脑玩转最夯的机械学习」。

另外,强烈推荐安装一张 macOS 原生支援的独立显卡,经实测运行 Object Detect 模型所消耗的时间与资源,比 Image Classification 还要高出许多,若只用 CPU 运算,真的会跑到地老天荒。

安装 Turi Create

Turi Create 是 Python 的模组,所以只需把 Python 跟 pip 装起来之后,用 pip 来安装即可。

macOS 安装 Python:

虽然 macOS 已经内建 Python,但内建的是给 macOS 系统用的,也不能任意更动,否则 macOS 系统会出现一堆不能执行的内建程式。用户使用的 Python 通常会另外安装,安装方式有两种:Python 官网与 HomeBrew 安装,择一即可,我用的是官网安装方式。

Python Download

安装方式很简单,下载后执行它的 .pkg 依指示下一步就行了。安装 3.6.x 或 2.7 皆可,也可两种版本都安装。不要安装 3.7 版,因为目前 Turi Create 尚未支援。

安装官网版之后,需再手动执行 Python 安装目录下的两个 Command ,免得后面一堆问题。

萤幕快照 2018-07-22 上午11.04.52

pip 安装 Turi Create

终端机视窗,一行就搞定:

$ pip install turicreate

不过目前这行指令下载的是 4 版,并不支援 macOS 10.14 环境下的 GPU 加速运算,所以建议安装 5.0 beta 版:

$ pip install "turicreate==5.0b2"

第一次大概跑个几分钟之后,TuriCreate 就安装完成了。若是使用 Python 3.6 的话,要用 pip3 指令:

$ pip3 install "turicreate==5.0b2"

这样就完成了安装,简单执行一下看有没有安装完成:

萤幕快照 2018-07-22 上午11.15.46

没什么问题之后,顺著「Object Detection」章节的教学范例操作一次。

使用 Turi Create 进行 物体识别(Object Detection)项目

(1) 首先,当然是下载训练机器的范例资料。依照 Object Detect 教学文件(网址)中「Data Prepare」这节内容的指示,下载范例档

萤幕快照 2018-07-22 上午11.23.13

教学文件中要我们选择 ig02-v1.0-bikes.zip 和 ig02-v1.0-cars 两个就行了,目的很明显的是要训练出能识别「脚踏车(bikes)」和「汽车(cars)」主体的模型。下载后解压缩,建一个目录 ig02 后,把下载档案连同目录放进去,整理如下:

萤幕快照 2018-07-22 上午11.28.32

(2) 产生训练模型

开启一终端机视窗,先移到与 ig02 同一层的目录下,再执行 python:

萤幕快照 2018-07-22 上午11.48.51

接著就依照教学文件中的指令照 Key 就行了。要特别注意每一行前面是否有空格,这个是撰写 python 的特例,空格代表程式可执行的区段,空格没对齐,后续程式就会出问题。

import turicreate as tc
import os

# Change if applicable
ig02_path = '~/Desktop/ig02'

# Load all images in random order
raw_sf = tc.image_analysis.load_images(ig02_path, recursive=True,
                                       random_order=True)

# Split file names so that we can determine what kind of image each row is
# E.g. bike_005.mask.0.png -> ['bike_005', 'mask']
info = raw_sf['path'].apply(lambda path: os.path.basename(path).split('.')[:2])

# Rename columns to 'name' and 'type'
info = info.unpack().rename({'X.0': 'name', 'X.1': 'type'})

# Add to our main SFrame
raw_sf = raw_sf.add_columns(info)

# Extract label (e.g. 'bike') from name (e.g. 'bike_003')
raw_sf['label'] = raw_sf['name'].apply(lambda name: name.split('_')[0])

# Original path no longer needed
del raw_sf['path']

# Split into images and masks
sf_images = raw_sf[raw_sf['type'] == 'image']
sf_masks = raw_sf[raw_sf['type'] == 'mask']

def mask_to_bbox_coordinates(img):
    """
    Takes a tc.Image of a mask and returns a dictionary representing bounding
    box coordinates: e.g. {'x': 100, 'y': 120, 'width': 80, 'height': 120}
    """
    import numpy as np
    mask = img.pixel_data
    if mask.max() == 0:
        return None
    # Take max along both x and y axis, and find first and last non-zero value
    x0, x1 = np.where(mask.max(0))[0][[0, -1]]
    y0, y1 = np.where(mask.max(1))[0][[0, -1]]
    return {'x': (x0 + x1) / 2, 'width': (x1 - x0),
            'y': (y0 + y1) / 2, 'height': (y1 - y0)}

# Convert masks to bounding boxes (drop masks that did not contain bounding box)
sf_masks['coordinates'] = sf_masks['image'].apply(mask_to_bbox_coordinates)

# There can be empty masks (which returns None), so let's get rid of those
sf_masks = sf_masks.dropna('coordinates')

# Combine label and coordinates into a bounding box dictionary
sf_masks = sf_masks.pack_columns(['label', 'coordinates'],
                                 new_column_name='bbox', dtype=dict)

# Combine bounding boxes of the same 'name' into lists
sf_annotations = sf_masks.groupby('name',
                                 {'annotations': tc.aggregate.CONCAT('bbox')})

# Join annotations with the images. Note, some images do not have annotations,
# but we still want to keep them in the dataset. This is why it is important to
# a LEFT join.
sf = sf_images.join(sf_annotations, on='name', how='left')

# The LEFT join fills missing matches with None, so we replace these with empty
# lists instead using fillna.
sf['annotations'] = sf['annotations'].fillna([])

# Remove unnecessary columns
del sf['type']

# Save SFrame
sf.save('ig02.sframe')

然后另外开一个终端机视窗,一样是移到与 ig02 同一层的目录,执行 python:

萤幕快照 2018-07-22 上午11.48.51

开始训练模型:

import turicreate as tc

# Load the data
data =  tc.SFrame('ig02.sframe')

# Make a train-test split
train_data, test_data = data.random_split(0.8)

# Create a model, 这里加上 max_iterations = 500 缩短训练时间,先把流程跑完
# 加上 _advanced_parameters={'batch_size':24} 则是解决 GPU 记忆体 < 4GB 导致 Loss 出现 nan 的问题
model = tc.object_detector.create(train_data,max_iterations = 500, _advanced_parameters={'batch_size':24}) # Save predictions to an SArray predictions = model.predict(test_data) # Evaluate the model and save the results into a dictionary metrics = model.evaluate(test_data) # Save the model for later use in Turi Create model.save('mymodel.model') # Export for use in Core ML model.export_coreml('MyCustomObjectDetector.mlmodel')

这时候可听到显示卡风扇起飞的声音,R9-280X 的温度大约在 79~81 度。如果再跑久一点(max_iterations 数字加大),还可以隐约闻到房间空气中一股清淡的塑胶味...

萤幕快照 2018-07-22 下午4.35.43

若 Loss 的值随著 Iteration 的数字越来越小,表示训练有正常进行。若 Loss 值越来越大,或是出现 nan 值,则表示训练失败,有很多种可能,得上网查一下原因。这次训练在 Iterations = 500 的参数下,耗时大约六分钟(如果是 CPU 大概要 1~2 个小时,你不会想等的),最后我们得到一个「MyCustomObjectDetector.mlmodel」的 Core ML 模型。可以将 python 命令中的「predictions」与「metrics」的内容印出来看看,评估这组模型的可信度如何。

如果把显卡换成 GTX-780 进行训练, 得到差异很大的结果:

batch_size = 24

萤幕快照 2018-07-27 下午11.58.53

batch_size = 16

萤幕快照 2018-07-28 上午12.17.07

相同训练资料与参数下(batch_size = 24, max_iterations = 500),GTX-780 使用了比 R9-280X 更多的时间(557 秒 vs 342 秒),模型信任度也比较低。将 batch_size 下修到 16 时训练时间有加快,但训练出来的信任度更低,模型可用性就呵呵了。

在 Turi Create - Object Detector 的案例中,R9-280X 显卡比 GTX-780 更适合拿来训练,与前一篇 Xcode 10 的影像辨识是完全相反的结果。至于纯 CPU 训练所花的时间,初估约 R9-280X 的 9 ~ 10 倍,没有等它跑完,因为实在太久了,相同训瑱参数下大概要一个多小时。

发布至 iOS App

接著用 iOS 程式去实际验证模型的使用方式和「信任度」(confidence)。

教学文件也提供了 swift 版的 iOS 程式范例,所幸对应 iOS 12 的程式不复杂,可以很快的改写成熟悉的 Object-C 程式。取得的结果中,座标系统中的 Y 轴必须经过转换,程式细节不多谈,程式的重点如下: 

    MyDetector *mlmodel = [[MyDetector alloc] init];

    // featureImage 是被侦测的图片,UIImage 物件

    CGSize featureImageSize = featureImage.size;

    VNCoreMLModel *visionModel = [VNCoreMLModel modelForMLModel:mlmodel.model error:nil];

    VNCoreMLRequest *objectRecognition = [[VNCoreMLRequest alloc] initWithModel:visionModel completionHandler:^(VNRequest * _Nonnull request, NSError * _Nullable error) {

        if (request.results == nil) {

            return;

        }

        if (request.results.count <= 0 ) {

            return;

        }

        for (VNRecognizedObjectObservation *foundObject in request.results) {

            VNClassificationObservation *bestLabel = [foundObject.labels firstObject];

            CGRect objectBounds = foundObject.boundingBox;

            //发现 Y 轴怪怪的,做修正

            CGRect fixBouns = CGRectMake(objectBounds.origin.x,

                                         1.0f - (objectBounds.origin.y + objectBounds.size.height),

                                         objectBounds.size.width, objectBounds.size.height);

            CGRect rect = CGRectMake(fixBouns.origin.x * featureImageSize.width,

                                           fixBouns.origin.y * featureImageSize.height,

                                           fixBouns.size.width * featureImageSize.width,

                                           fixBouns.size.height * featureImageSize.height);

            NSLog(@"%@ (%.2f %%) ==> %f,%f,%f,%f",bestLabel.identifier, bestLabel.confidence * 100 , rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);

        }

    }];

    objectRecognition.imageCropAndScaleOption = VNImageCropAndScaleOptionScaleFill;

    VNImageRequestHandler *requestHandler = [[VNImageRequestHandleralloc] initWithCGImage:featureImage.CGImageoptions:@{}];

    NSError *error = nil;

    [requestHandler performRequests:@[objectRecognition] error:&error];

    if (error) {

        NSLog(@"VNImageRequestHandler:performRequests Error: %@",error.localizedDescription);

    }

 

 

 

如果产生的模型要给 iOS 11 使用的话,model.export_coreml 需加上 include_non_maximum_suppression = False 参数:

model.export_coreml('MyCustomObjectDetector.mlmodel', include_non_maximum_suppression=False)

不过对应 iOS11 的程式会有个麻烦。必须经过一堆阙值过滤、抑制过滤、矩阵处理与转换,这个部分教学指南仍然是 swift 版的范例,里面有一段矩阵的写法实在是看不太懂。还好有个对岸的同胞写出对应 Object-C 的程式,两相对照之下也弄懂了。而对应 iOS11 的 Core ML 模型,输出的座标 Y 轴并不需要转换,检测的信任度值也比较低。

刚好搜寻到一张车子载著脚踏车的图片,结果:

萤幕快照 2018-07-22 下午5.26.00

这张明确标示出汽车与单车。

萤幕快照 2018-07-22 下午5.24.49

这张只标示出单车,汽车的标示应该是被抑制掉了。

随机上网搜寻脚踏车或汽车的图片:

萤幕快照 2018-07-22 下午12.31.23

萤幕快照 2018-07-22 下午12.35.00

辨识度相当的不错,不过对数量的判定有时会有误差,会把一个物体标示成两个以上。 iOS 12 的程式还没研究如何手动调整「抑制度」,它是根据模型内的 Metadata 自动设置(预设值为 0.45,也就是两块区域交叠超过 45% 时会抑制其中一块不显示)。

从结果看来,范例的操作与训练是成功的,不过训练模型的资料是经过人为介入挖掘汇整,并经历各种 AI 演算法的千锤百炼之后,才成为教学范例。然而如同上一篇文章中所言,机械学习真正的价值,在于如何针对特定需求,去收集、挖掘出可用的训练资料,否则即使会写演算、精通程式,却挖掘不到可用的训练资料,一切也都是枉然。

所以再次用上次不算成功的「认识 Gogoro 换电站」为例,借由这次学到的新训练方式,尝试看看结果如何。

再一次模拟情境:训练辨识新的事物 - 「Gogoro 换电站」

初始资料收集就如同上次提到的,从官网的网页中,捞取所有的换电站资料。这段时间又有几个新开的站点,所以捞到的换电站图片来到 955 张。然而当我拿著这些资料,试著依照上面「cars & bikes」逻辑整理时,目睹的这一幕,让全国两千三百万人都惊呆了...

萤幕快照 2018-07-22 下午5.54.18

范例使用的资料集,是将训练资料标注为「image」与「mask」两种类型,换言之,每一张拿来训练的 image 原始资料,都得对应一张以上的 mask.(x) 图片。 从训练资料网站上的这段说明:

萤幕快照 2018-07-22 下午6.03.25

Details

Our team re-annotated carsbicycles and people on the original set of images. Only some...

标注资料的工作是一整个团队在执行,并非只有一人,每一张的 mask 都是经过人为标注与检验,仔细的把每一张训练图片,将物体的轮廓区域(红色)和被遮蔽区(绿色)画出来。

看到这个情况,再看到抓取的 955 张 Gogoro 换电站的原始图片,我的眼眶湿了,两行清泪不禁潸然落下....

如果依照上面的成功范例,那我得逐一标注出每张焕电站的 mask 图片,对个人来说,这简直是巨大的工程。难道我...又再度卡关了吗?接下来的几天,再度陷入一连串的苦思:

「人为作业下,如何用最快速、最间单的方法,正确的标注训练资料?」

为了寻找问题的答案,线索再回到练习的第一段 Python 程式,去研读每一段 Python 到底在做什么事情,是如何汇整 cars 和 bikes 当中个别标示为 image 与 mask 资料,最后又传了哪些资料给机械去学习。经过很多天的摸索,研读 Turi Create API 文件后,答案就在 python 程式的最后,我加上一行指令:

# 浏览训练资料集内容
sf.explore()

萤幕快照 2018-07-22 下午6.30.03

终于又弄懂了,第一段 python 程式中,mark 图片经过一连串高深莫测的 python 程序后,最终也只是产生 annonation 标注。每一个标注区域,也只是描述物体最大轮廓的矩形范围和中心点座标罢了。大概像下图这样:

xywh

换言之,只要找出一个快速标注图片的方法,产生对应且正确的 annonations 文字资料就行了,并不需要每张训练图片都得弄一张标注物体完整轮廓边缘的 mask 图片,毕竟那是很庞大的工程。

所以,与其要一张一张标出 mask 消耗大量时间的工作,不如再花个几天时间,自行开发出一支可直觉、快速标注训练图库的工具程式,对我反而还更容易些。

萤幕快照 2018-07-24 上午12.51.45

萤幕快照 2018-07-22 下午6.51.55

萤幕快照 2018-07-24 上午12.53.47

这支程式经过不断的优化操作介面之后,标注一张换电站照片的时间,缩短到 10 ~ 20 秒。标注完成后自动生成 Python 程式码,内容包含训练与产生 .mlmodel 模型的指令,以及「一键执行训练(Run Command)」功能。以后每次的训练时,不需再写任何 Python 程式了。

模型验证

从 955 张 Gogoro 换电站图片中快速挑了约 30 张,大约只花了十分钟标注完成,加上 R9-280X 显卡,只需五分钟的训练时间。

萤幕快照 2018-07-22 下午7.21.15

接下来是验收成果的时候了。

萤幕快照 2018-07-22 下午7.24.18

萤幕快照 2018-07-22 下午7.25.51

底下这张也能辨识成功!

萤幕快照 2018-07-24 上午12.46.45

萤幕快照 2018-07-22 下午7.27.10 

萤幕快照 2018-07-22 下午7.29.38

萤幕快照 2018-07-22 下午7.31.12

Simulator Screen Shot - iPhone 5s - 2018-07-25 at 00.16.37

当然也找来竞争对手的假换电站,模型没有被骗倒。

萤幕快照 2018-07-22 下午7.32.50

这个模型的信任度,达到了预期的目标。这还是只有 30 张图的训练成绩,若能继续加入更多种换电站外观,以及各种情况下拍摄的训练素材,相信会更加准确。

后记:

回想起这趟 Machine Learning 自学之路走得跌跌撞撞,前前后后也花费巨大的学习时间,也多亏有苹果另外提供的机械学习工具 Turi Create,大幅降低了门槛,如今总算是达标了,也学到了如何快速的挖掘与标注资料。从标注 Gogoro 换电站图片开始,到模型的产出,前后只花了半个小时就有满意的结果,对于不需特别严谨的应用,已经是足够了。背后所代表的一股脑傻劲与心力付出,和学习过程中不断的自我反省,可说是获得难能宝贵的一课。

相信「黑苹果」在这个议题中是很有利的。安装更高性能显卡的黑苹果电脑,在训练模型这个部分,应该会比使用 Macbook 笔电有更好的效率与更低的采购成本。Turi Create 适合在 macOS 环境中运行的特性,目前又是门槛最低的机械学习套件,让安装黑苹果的理由又多了这一项。

随著 iOS 12 与 macOS 10.14 Mojave 正式版即将于九月中旬释出,除了更完整支援 Core ML 训练与开发环境,尤其是 iOS 12 号称可大幅改善目前所使用的手机 iPhone 5s 效能,使得今年的九月,更加令人期待。

相关文章