本文首发于集智专栏:https://jizhi.im/blog/post/ml_is_fun_03
你是否厌倦了每天被深度学习相关的新闻轰炸却不明所以?此诚求变之机。
这一次我们将学习如何用深度学习来写程序识别图像中的物体。也可以说我们是要解释Google图片搜索背后的黑科技:
就像Part 1和Part 2一样,本指南仍然面向所有对机器学习感兴趣却不知如何开始的朋友们。我们的目标是所有人都读得懂——因而势必无法照顾到每个细节。但那又如何呢?只要能让一位读者对ML感兴趣,那就是功德一件了!
产品:每当一名用户拍了照片,APP应该检测他们是否在国家公园……
开发:当然了,不过是简单的GIS查询而已,给我几个小时。
产品:……以及拍的是不是一只鸟。
开发:那我需要一个研究小组和五年时间。
旁白:在计算机科学中,有时很难解释“简单”和“根本不可能”之间的区别。
你可能已经在xkcd系列漫画中看到过了。
这里的笑点在于,三岁小孩儿都能认出鸟的照片,但是教会计算机识别物体,已经让最优秀的计算机科学家耗费了50年。
在过去的几年里,我们终于找到了物体识别的好路子,那就是深度卷积神经网络。这听起来像是从威廉·吉布森的科幻小说里捡了几个词拼起来的,不过只要分解开来细看,其原理真的很简单。
开始吧——现在就写一个能认识鸟的程序!
在我们学习如何辨别鸟的照片之前,先来学个简单得多的例子——手写数字"8"。
在Part 2中,我们已经学习了神经网络如何链接大量的神经元来解决复杂问题。我们搭建了一个迷你神经网络来基于房间数、面积、周边环境预测房价:
我们还知道了机器学习的思想就是重复利用相同的普适算法,根据不同的数据,解决不同的问题。下面我们修改这个神经网络来识别手写文本,并且进一步简化任务——只识别数字"8"。
机器学习只有当你有数据的时候才好使——数据越多越好,所以我们需要大量的手写"8"来开始。所幸,研究者们为了这一目的已经建立了MNIST手写数字数据集。MNIST提供了60,000张手写数字的图片,每个尺寸都是18x18,以下是数据集里部分的"8":
Part 2里搭建的神经网络只读取3个输入("3"间卧室,"2000"平方米等)。但是现在我们要用神经网络处理图片,不是数字怎么传入神经网络呢?
答案无比的简单。神经网络以数字为输入,对于计算机来说,图片也不过只是代表了像素明暗的数字:
只要把18x18像素的图片当作324个数字组成的数组,就可以把图片输入给神经网络了。
要处理324个输入,我们只需让神经网络具有324个输入节点:
注意我们的神经网络现在有两个输出(而非之前的一个)。第一个输出是预测此图为"8"的概率,第二个为不是"8"的概率。每种类型的目标物体有了分别的输出,就可以让神经网络对物体进行分类了。
这次的神经网络比之前要大得多了(从3个输入到324),但是现代的计算机处理几百个节点的神经网络,连眼都不带眨的,甚至连手机都能轻松满足。
剩下的就是用"8"和非"8"的数字去训练神经网络以使其能够区分两者了,当我们输入一个"8",我们告诉神经网络,此图为"8"的概率是100%,非"8"的概率是0%,反之亦反。
这是我们的训练数据:
现在随便一个笔记本电脑,也能在几分钟之内训练完这样一个网络,训练完之后我们就拥有了能够高精度识别数字"8"的神经网络。欢迎来到(1980年代的)图像识别世界!
把像素简单地导入神经网络里一训练,就能识别图像了,感觉无比清爽。机器学习真乃魔法……也?
事情并不简单。
首先,一个好消息是,我们的识8器对于图片正中的数字,效果还是很喜人的:
现在坏消息来了:
当数字不是恰好居中的时候,识8器完全失效了,哪怕一丁点的位置偏差也不行:
这是因为,神经网络只学到了恰好居中"8"的模式,但是对于离心的"8"却一无所知,它知道且仅知道一种模式。
在现实中这就实用价值不大了,因为实际问题不可能总是那么干净简约。所以我们必须让神经网络能够处理离心的"8"。
我们已经有了一个好方法,能够找到图片中心的"8",那可不可以在图片上扫描寻找子区域里的"8",直至找到呢?
这就是滑动窗口法,一种非常暴力的解决方案。在很有限的某些例子中可行,但效率也很低。你必须一遍一遍地寻找不同尺寸的物体,我们可以比这更好。
当我们在训练网络的时候,只用到了完美居中的"8"。如果我们在训练过程中就引入不同位置、不同大小的"8",那会怎么样呢?
不需要收集更多的训练数据,我们可以写个脚本生成新的图片,图中的"8"具有不同的位置和大小:
应用这一技术,我们可以轻易创造出无穷多的训练数据。
更多的数据让问题变得更加复杂了,但是我们可以用更大的神经网络,那样就能学习更复杂的模式。为了扩大网络,我们简单地把节点层堆叠起来:
这就是“深度神经网络”,因为比传统神经网络的层数更多。这个想法在1960年代便有了,不过直至最近,训练这么大的神经网络训练还是慢得无法接受。但是自从我们发现了用3d显卡替代传统CPU来加速矩阵计算,大规模神经网络就变得可行了。你用来玩守望先锋的NVIDIA GeForce GTX 1080显卡,也可以用来训练神经网络。
然而即便我们可以用显卡很快地训练出大规模神经网络,这仍然不是解决方案的全部,我们还需要在处理图片上更加机智才行。
在暴力方法#2中,把图片顶端的"8"和图片底部的"8"当作完全不同的物体,这其实是没有意义的。应该存在一种方案,让神经网络不经额外训练即知:图片不管哪个位置的"8"都是一个东西。万幸,确实是存在的!
生而为人,你可以直观地看出照片中含有层次或概念结构。考虑这张照片:
作为一个人类,你立即就能识别出照片里的层次:
地面覆盖了草坪
图里有个小孩儿
小孩儿骑在弹跳小马上
弹跳小马在草坪上
更重要的是,无论小孩在什么样的表面上,我们都能认出那是个小孩儿。我们并不需要重新学习各种表面上的小孩儿。但是目前,神经网络还做不到这些,它会把不同位置的"8"当作完全不同的东西,而并不知道在图片上移动物体不会改变其实质。这意味着网络必须重新学习每个位置上的物体,太悲催了。
我们需要让神经网络明白平移不变形——"8"还是"8",不论出现在图片的哪里。这一过程可由卷积实现,卷积的想法部分来自计算机科学,还有部分是受生物学启发(比如,疯狂科学家们真的在猫脑子里插管,以研究猫是怎么处理图像的人)。
这次我们不再把整个图片当成一个数字网格输入到神经网络,而是利用物体与位置的独立性,做得更聪明些。以下就是卷积的作用机理,一步一步来——
与之前的滑动窗口法类似,这里也用一个窗口在整张原图上滑动截图,并且把截到的每一小部分单独存成一个图片:
如此我们便把一张大图拆成了77张等尺寸的小图。
曾经我们把单张图片输入神经网络来判断其是否为"8",这里再重复一样的工作,不过是针对每一个单独的小图片:
这里还有一大诀窍:我们将相同的神经网络权重应用在每一块小图上。换言之,我们平等地对待所有小图,如果某个小图里出现了我们感兴趣的内容,我们就将其标记为有趣。
我们并不想丢失原始小图的排列信息,所以我们要把每个小图的结果按照相同的排列重新构建原图,形式如下:
也可以说,我们从一张大图片开始,到小阵列结束,阵列记录了原图当中哪一部分有我们最关心的内容。
第3步的结果是一个阵列,反映了原图的哪一部分是我们最感兴趣的,不过这个阵列仍然很庞大:
为了减小阵列的尺寸,我们用最大池化算法进行下采样。听着很稀奇,其实完全不!
看阵列中的每个2x2方块,并且只留下最大的数字:
这里的主要思路就是,当我们在组成2x2方块的四个输入小图中的任意一个发现了关心的内容,就只保留我们最感兴趣的信息,这可以减小阵列规模并保留最重要的信息。
至此,我们已经把一个巨大的图片消减成了相对较小的阵列,阵列就是一串数,所以我们可以把小阵列输入另一个神经网络,最后的这个神经网络会决定图片是否匹配。为了与卷积层相区分,我们称之为“全连接层”。从头到尾,我们的五步流水线如下图所示:
我们的图像处理流水线有一系列步骤:卷积,最大池化,全连接网络。
而在解决实际问题的时候,这些步骤可以组合起来并堆叠多次。你可以加两个,三个甚至十个卷积层,也可以在任何时候插入一个最大池化层。总之基本思路就是把一张大图,逐步分解为小图,直至我们能够获得结果。卷积层越多,网络就能学习识别越复杂的特征。
比如说,第一个卷积层可能学会了辨认锐利边缘,第二个卷积层可能根据锐边的知识学会了识别鸟喙,第三个卷积层又基于鸟喙识别出了整只鸟。以下是更贴近实践的深度卷积网络结构(学术论文里常见):
在这一实例中,他们从224x224像素的图片开始,应用了:卷积,两次最大池化,三次卷积,又是两次最大池化,最后是两个全连接层。最终的结果可以将图片从1000个分类中识别出来。
那么我们要怎么知道什么时候该用什么层呢?诚然,你得经过大量的实践和测试才能回答,可能训练100个网络才能找到最佳的结构和参数。机器学习就是包含了大量的尝试和错误!
如今我们已经足以写出一个程序来判断一张图上是不是鸟类。一如既往,我们需要训练数据来开始。免费的CIFAR10数据集包括了60,000张鸟类图片和52,000张非鸟图片。如果要更多的数据,我们还需要加入Caltech-UCSD Birds-200-2011数据集,内含12,000张鸟类图片。
这是组合数据集的一部分鸟图:
以及一部分非鸟图:
这一数据集可以很好地满足我们的需求,不过相对于真实世界的应用而言,72,000张低分辨率图片还是太少,如果你想要达到Google级的水平,你需要上百万张高清无码大图。在机器学习中,数据多总是比算法好更重要。现在你就知道Google为什么那么乐意提供无限照片存储了,他们要的是你的数据啊,数据!
原作者使用的是TFLearn,译注者改用了国内更流行的Keras。TFLearn代码可见作者原文。
Keras封装了TensorFlow和Theano两个深度学习库并提供了简单易用的API,这使得开发者可以仅用几行就搭建一个卷积神经网络。
以下是定义并训练网络的代码:
Keras内置了CIFAR10数据集,但是需要从网络上下载。为了方便读者朋友们体验,我们已经事先下载好了数据集并存在了后台,您只需点击运行即可导入CIFAR10。(注:若重复运行“Keras导入CIFAR10数据集”模块可能会报错,这是因为已经数据已经导入的原因,不影响之后的程序运行。)
以下代码可以直接在线运行和修改在集智的Amalthea上:https://jizhi.im/blog/post/ml_is_fun_03
# 导入相关库
from matplotlib import pyplot
from scipy.misc import toimage
import numpy
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Flatten
from keras.constraints import maxnorm
from keras.optimizers import SGD
from keras.layers.convolutional import Convolution2D
from keras.layers.convolutional import MaxPooling2D
from keras.utils import np_utils
from keras import backend as K
# 定义训练集、测试集
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
print("Datasets is ready!")
# 预览部分图片
for i in range(9):
pyplot.subplot(330 + 1 + i)
pyplot.imshow(toimage(X_train[i]))
pyplot.show()
下面导入其他将会用到的Keras相关模块并对数据进行预处理。Keras搭建神经网络的过程其实非常简单直观,就像搭积木一样。
首先确定网络的基本结构,这里就用最基本Sequential()
串型拓扑,并建立网络模型对象model
。
之后便是给model
中一层层地添加网络,添加的方式也很简单,就是使用model.add()
方法,逐个地把卷积层(Convolution2D
)、池化层(MaxPooling2D
)、全连接层(Dense
)等插入到model
里,形成整个神经网络。
# 对标签进行独热码编码
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
num_classes = y_test.shape[1]
# 设置神经网络对象model
model = Sequential()
model.add(Convolution2D(32,3,3,input_shape=(32,32,3),
border_mode='same',activation='relu',
W_constraint=maxnorm(3)))
model.add(Dropout(0.2))
model.add(Convolution2D(32,3,3,activation='relu',
border_mode='same',W_constraint=maxnorm(3)))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Flatten())
model.add(Dense(512,activation='relu',W_constraint=maxnorm(3)))
model.add(Dropout(0.5))
model.add(Dense(num_classes,activation='softmax'))
print("Neural Network built!")
经过以上过程,我们已经有了一个神经网络的外壳,但也仅仅是外壳,你会注意到上面这个代码模块运行的很快,因为这只是根据神经网络的设置形成了一个容器,内部还是空空如也。还要经过“编译”并且导入数据进行“训练”,我们才真正拥有了“有血有肉”的神经网络。最后通过与测试集的对比,衡量神经网络的表现。
虽然数据量并不大,但是训练神经网络对于普通CPU来说,还是个比较艰巨的任务,因此训练速度可能堪忧。我们的网站后台暂时还未提供GPU接口,所以这里只能先将训练迭代次数设为1
,以后我们会争取提供更强大的计算资源。
因后台计算资源所限,神经网络训练可能失败,如果您感兴趣可以复制代码到本地开发环境。
# 设定训练过程
epoches = 1 # 对训练集的迭代次数(这里设为最小的1)
lrate = 0.01 # 初始学习率
decay = lrate/epoches
# 优化器设置与编译
sgd = SGD(lr=lrate, momentum=0.9, decay=decay, nesterov=False)
model.compile(loss='categorical_crossentropy',optimizer=sgd,metrics=['accuracy'])
print(model.summary())
# 用训练集数据训练网络
model.fit(X_train, y_train, validation_data=(X_test, y_test), nb_epoch=epoches,
batch_size=32, verbose=1)
# 输出神经网络的表现
scores = model.evaluate(X_test, y_test, verbose=0)
print("Accuracy: %.2f%%") % (scores[1]*100)
如果你有个不错的显卡和足够的显存(比如Nvidia GeForce GTX 980 Ti或以上),大概一个小时可以训练完,如果用CPU的话可能就要久的多了。
精度随着训练进程儿上升。第一轮过后,我的精度只有75.4%;经过十轮,精度已经提高到91.7%;在50轮以后,精度攀升到95.5%,再训练已经收效甚微了,因此我就此打住。
Congrats!现在我们的程序可以识别图片里的鸟了!
有了训练过的神经网络,是骡子是马该拉出来遛遛。这一简单的脚本读取一个图片文件并判断是否是鸟。
为了真正检验我们的神经网络是否有效,需要测试大批的图片。我这里用了15,000张图片作为验证集,当我把这15,000张图传递给神经网络,95%都得到了正确答案。
听起来很好,对吧?其实这可未必!
我们的神经网络号称有95%的准确度,但是细微之处见真章,面子上95%,里子可能千差万别。比如说,如果我们的训练图片里5%是鸟,而另外95%不是鸟,然后一个程序只输出“不是鸟”就可以达到95%的精度,但这毫无意义。
我们需要进一步细究这个数字,而不是满足于一个含糊的95%。为了判断一个分类系统的作用究竟几何,我们需要考察它是如何失效的,而不是失效的百分比。抛掉单纯的“对/错”标准,我们来把问题细分为四种分别的情况——
根据我们这15,000张图片,以下为各种情况的统计数据:
为什么要这样细分结果呢?因为并非每种错误都是均等的。想象一下,如果我们在写一个根据MRI图像诊断癌症的程序,这时候宁可引入False Positive也不要带有False Negative。前者只是让患者虚惊一场,后者可就是延误治疗了。
所以出了宽泛的准确度,我们还计算精确率和召回率。精确率和召回率更加清晰地定义了分类起的效用。
由上表可知,当我们给出“是鸟”的猜测时,其中97%是对的。但是我们却只找出了90%的鸟,这意味着我们可能不是所有鸟都认得,但是认鸟却很准!