本文的作者Jacques Mattheij自小就是一名乐高粉。在接触乐高的过程中,他发现了这么一种现象:不同种类的乐高售价是不同的。比如精装乐高的售价大概是每公斤40欧元,散装的乐高只需要10欧元;而一些限量、稀有版本以及乐高机械组的售价能达到每公斤100欧元。
为此甚至有人专门去买那些散装和精装新品的乐高,然后把它们进行重新分类以获取更高的价值。
然而,手动给那些千奇百怪的乐高分类看上去并不是个好主意。于是Mattheij某日突发奇想,决定尝试用机器干这件事。他在各个拍卖网站上拍下了能装满一整车库的乐高(运回来途中还丢了辆卡车)来做这个实验。
这是Mattheij在个人网站上发布的第二篇帖子,讲的是他为给这堆乐高分类而在软件上尝试过的方法;在第一篇帖子里,他介绍了硬件方面的准备和面临的困难。
我们先跳过买几车乐高、安装摄像头、传送带等等过程,来看看他是怎么写这个分类程序的。如果你对硬件部分更有兴趣,请到这里围观:https://jacquesmattheij.com/sorting-two-metric-tons-of-lego
以下内容编译自Mattheij的第二篇帖子:
概述
全部的软件都是用Python写出来的。我本人并不是Python专家,不过好在我也不至于花一辈子才能把它弄会。Anaconda是一种非常好用的Python分发工具。原本,要解决各种关联性和版本问题,给Python设置一个虚拟环境这种事简直就是个噩梦。而对我来讲,Anaconda能帮上很大的忙。
关于乐高分类软件,有个主要部分。比如说,一个通过摄像头实现的图像采集系统:
扫描仪/“图像缝纫机”
采集器完成工作后,会将图像发送到“图像缝纫机”(把两张图接在一起)上,后者的主要任务是两件事:一是判定自从上一张图像之后带着某块乐高的传送带移动了多少( 看视频里的波浪线),二是更新一张新扫描进来的内存图像。在两块乐高中间隔开的部分“缝纫机”会剪一下,然后把下一张扫进来的乐高图像接上。
上述这些都是用OpenCV写出来的。
扫描器和“图像缝纫机”完成了自己的工作后,成果看起来是这样的:
分类
这是这件事真正有趣的部分。这块我弄过好多次,现在已经烦得不行了。
OpenCV基元
我第一次选择的方法是用OpenCV基元,特别是其中的轮廓匹配和圆检测。只要处理乐高的种类没那么多,用这种方式就还能保证一个相对不错的识别准确率。结合一部分简单的元数据(比一块乐高的长、宽、高),它就能分辨出所有基本型乐高积木块之间的区别,不过也不能再多了。
贝叶斯
换种方式,我们试试贝叶斯。贝叶斯分类器相当好理解:你先设计一大堆特征,然后依据这些特征构建检测器,之后再创建一个测试集以保证你的检测器运行得就像他们告诉你的那样好,都完成之后,你就尽己所能提高系统对那些特征的识别能力。你要把一个尽可能大的测试图像集扔到这个系统里去跑,以确定你所设定特征的优先级,进而确定每个特征所占的权重——如果某一特征出现就会被检测为“正确”,特征没有出现就会被检测为“错误”。
我用这种方法建立了一个基于如下特征的分类器:
- 交叉(两条线在中间某处相交)
- 圆(积木里包含比螺柱更大的圆形)
- 侧螺柱(侧面可见的螺柱)
- “饱满”(the part occupies a large fraction of its outer perimeter)(PS:这个表述量子位也不懂┑( ̄Д  ̄)┍)
- 高度
- 洞(积木上某处有个洞)
- 洞穿(这块积木被打通了)
- 长度
- 盘子(积木大概跟个盘子那么高)
- 矩形(积木大致是长方形)
- 斜坡(积木上有斜坡)
- “皮包骨”(the part occupies a small fraction of its outer perimeter)(PS:没怎么玩过乐高的量子位依旧是不懂┑( ̄Д  ̄)┍)
- 正方形(积木大概是正方形)
- 螺柱(积木上有可见的螺柱)
- “透”(积木是透明的)
- 体积(以立方毫米表示)
- 楔形(积木是楔形的)
- 宽度
可能还有其他的特征……这些我弄了好一阵子。做一个“螺柱检测器”看上去微不足道,但其实事情也没那么简单。你需要记着螺柱可能位于任何方向,还有一些可能被识别成螺柱但其实并非螺柱的细小部件,积木可能会是颠倒放置,还可能是背对摄像头的。类似的问题几乎在每种特征上都要出现一遍,最后你要花费大量的精力去调整,才能让系统达到一个令你满意的状态。不过一旦你把上面这些都搞定,你就会收获一个能够检测很多不同种类积木、也能保证不错准确率的分类器了。
即便如此,这套系统离尽善尽美也还差得很远:它太慢了。每一次你往里添加进全新类别的积木,你就要为搞明白一块积木到底属于哪个类别而做更多的工作。电脑在集合元素上花费了大量的时间形成了一个不断膨胀的积木形状库,最佳匹配结果就从库里得出。系统的准确率令人印象深刻,但最后因为速度太慢(跟不上传送机器的速度),我还是放弃了这种方案。
剔除法
剔除系统使用了和上一种方法相同的分类条件。按有效性递减规则进行分类可以快速地将不合规则的对象剔除出去,剩余部分就可以被高效处理。这是第一次,软件能跟上全速运行的机器。
不过这种方案同样存在几个问题:一旦某件积木被剔除掉了,它就再也回不来了——但这个剔除可能是错误的。事实是这种“二进制”的方式确实限制了系统的准确率,你需要一个非常庞大的数据集才能让这个系统跑起来,而这将在很大程度上降低整体效能。
这个系统经常到最后把所有东西都剔除掉了——这样它就毫无用处了。因此,为修正准确率而付出的成本很可能就把它在速度上的优势抵消了。
树形分类
这是个因吹斯汀的想法。我照着一个叫“猜动物”游戏里的台词简单做了棵小树,每次往里面加入新的东西时这棵树就会找出特征中不同的部分并在上面分出一个叉来装入新的积木。与剔除法相比,这种方法有两种非常重要的优势:一是一块积木能用树上的多个点表示,这回帮助提升准确率;二是与之前的方法相比,这个系统的速度简直就和闪电一样快。
但这种方法同样存在明显的弊端:起初的时候你需要手动去创造所有这些特征,而就算你能找到足够清晰的特征,只靠基本的OpenCV写一个特征检测器,这个过程也实在是太过冗长乏味了……很快,这个事还会变得更不好办,特别是Python属于那种相当慢的语言,如果你的问题不能用NumPy或OpenCV库调用来表示,在速度上就要吃大亏了。
机器学习
终于写到这了!被上面那些乱七八糟的方法折磨了差不多六个月后,我受够了。我意识到,要写一个能将所有乐高积木种类都完整包括在内的、能真正干起活来的分类器根本就是不可能的。当然,这让我沮丧了好一阵子。
我决定咬咬牙拼了。我把目光投向了机器学习,并且以一种更为严肃认真的方式来对待它:接下来的数周里我都在啃论文,学习各种与神经网络相关的有趣事情。
上世纪80年代,我曾经与神经网络有过短暂接触,而现在我发现,这一领域与当时相比,已经发生了很大变化。
经过不少研究,我最终决定选择谷歌大脑团队开发的TensorFlow。但要真正学会用这个也需要一个过程,一开始我就在上面卡住了好一阵子,不知道如何处理最好。
大概两个月前,一为叫greenpizza13的Hacker News用户给我推荐了Keras,让我能够直接使用TensorFlow而不至于再去兜个大圈子(Anaconda能帮上很大的忙),而这也直接把我领向了Jeremy Howard和Rachel Thomas棒极了的机器学习入门课(课程链接:http://course.fast.ai/)。
结果,在几个小时内(是的你没看错),我得到的结果就实现了对过去几个月里实践过的所有方案的超越;而在几天之内我就让分类系统实现了真正的实时工作,而不是智能简单地分个几类。再多吹一点:不管是在训练还是推理中,大概2000行特征检测代码以及另外2000行测试和胶水(glue)代码可以被少于200行的Keras代码代替了。
与其他手动对特征进行编码的方式相比,机器学习在速度与编码简易度上的优势真是简直了。虽然它不如树状机制那么快,准确率却比它不知道要高到哪里去了;与此同时,你还不用为那些千奇百怪的积木门类手写代码了,系统能自动搞定。
接下来的麻烦事在于,我要搞出一个足够大的训练数据集,来保证系统能进行1000种以上的分类。起初这看上去就是个不可能完成的任务,我不知道怎么样才能搞到足够的图像并且在可接受的时间内手动对它进行标注,即便按最乐观的情况计算,要搞出一个足够大的数据集,从而让这套系统按理想状态跑起来也要花上我6个月的时间。
最后我想通了:这事不重要。大部分时间里都可以让机器自己对自己的图像进行标注,而我所要做的就是修正它的错误。随着系统的运行,错误也变得越来越少。这种方式非常迅速地扩充了训练图像集。第一天,我手动标注了500块积木;第二天,机器把这个数字提高到了2000,当然,其中有大概一半都标错了——这2500件积木就成了接下来三天这轮训练的基础数据,而最后机器标注了超过4000块乐高,其中90%都是正确的!我只需要修正400块错误的就行了。在这两周的最后,我已经有了一个全部正确标注的20000张图像的数据集。
这还远远不够,其中的一些类别非常不具有代表性,因此我需要提高这些类别中的图像数量,我或许应该把这部分拉出来单独在机器上处理一遍——不需要再进行任何修正,它们将被同样地标注。
自上周发布第一篇帖子后我收获了很多帮助,这里我想特别感谢两个人。一是Jeremy Howard,他帮我补上了知识的空缺,没有他的帮助,我都开不了头;第二位是Francois Chollet,Keras的作者,他将自己自定义版本的Xception模型提供给了我,大大加速了训练的进程。
现在训练在速度上陷入了瓶颈,即使我的Nvidia GPU已经够快的了,我还是嫌它跑得慢。要生成一个新的网络需要花费几天时间,在一台有4个GPU的机器上,这速度真是不行……我是个没什么耐心的人,不过生让这个事给练出来了。
在某一时刻这些软件和数据都会被开源,但在此之前,我还有很长一段路要走。
什么时候软件真正具备给这一大堆散装乐高分类的能力了,翻身的日子就该到了。等我把这堆山一样的乐高收拾完,我就把它们廉价处理出去。
最后,这是一张呈现我起初构想的概念图,全是用乐高拼出来的: