集智专栏
资源加载中,请稍后...
集智专栏

数学不行还学AI - 第5话 - 神经网络平话演义(上)

Kaiser

原文:Learning AI if You Suck at Math — P5 — Deep Learning and Convolutional Neural Nets in Plain English!

作者:Daniel Jeffries

翻译:Kaiser(王司图


欢迎你来!这里是《数学不行还学AI》第5话,如果你还没有看过前面的章节,请点击这里

今天,我们要来写一个自己的Python图像识别程序。

为此我们要了解一个强大的深度学习架构——深度卷积神经网络(Deep Convolutional Neural Network, DCNN)。

卷积神经网络可谓计算机视觉界的劳模,从无人汽车到Google图片搜索,背后都有其功劳。在TensorFlow 2017 峰会上,一位研究者展示了用手机里的卷积神经网络诊断皮肤癌

为什么卷积神经网络如此强劲呢?一个关键原因在于:

自动模式识别

那模式识别又是什么?自动了又如何?

模式可能以很多种形式存在,这里只看两个最重要的例子:

  • 定义物理形式的特征

  • 完成特定任务的步骤


计算机视觉

在图像处理的模式识别中,又叫作特征提取

当你看一张照片或者实物的时候,你会选择性地拎出关键特征来进行认识,这是无意识中发生的。

当你看到我家猫Dove的照片,会想到“猫”或者“铲屎官”,但是你并不知道自己是如何想到的,而只是单纯地那样去做了,这都是自动而且无意识发生的

听起来非常简单,每时每刻都在经历,但这是因为真正的复杂性隐藏在深处。大脑是个黑盒,我们谁也没有说明书。哪怕只是一个微小的动作,也包含了巨量的步骤,表面上看似简单,实则无比复杂。

  • 转动眼珠。

  • 接收并分解光线。再传递信号给大脑。

  • 大脑开始工作,将光信号转换为电化学信号。

  • 信号在我们内置的神经网络中传播,激活不同的区块——记忆、联想、感觉等等。

  • 大脑首先感知了低阶模式(耳朵,胡须,尾巴),再组成高阶模式(动物)。

  • 最后,我们进行了分类,转换成词汇,也就是对真实事物的象征表达,这里就是“猫”。

以上种种,全部发生在一瞬之间。

如果你想要教电脑来执行这些,你会如何开始?

  • 如何找到耳朵?

  • 什么是耳朵?

  • 如何描述耳朵?

  • 为什么猫耳不同于人耳、蝙蝠耳(或蝙蝠侠的耳朵)?

  • 耳朵从不同角度看都是什么样?

  • 所有的猫耳朵都一样吗?(当然不,看看苏格兰折耳猫)

类似问题无穷无尽。如果你想不出如何用C++或Python教会电脑的好方法,也不要灰心,因为这已经困扰了计算机科学家们50多年了!

你自然而然完成的,正式深度学习神经网络的关键应用之一——分类器,这里是图像分类器。

起初,AI研究者想做的跟我们刚才一样。他们希望事无巨细,手动定义每一个步骤,比如对于自然语言处理(NLP),他们召集了最顶尖的语言学专家,让他们总结出语言的所有规律,这也是为什么早期的AI又叫“专家系统”。

语言学家坐成一圈开始琢磨了,然后一个接一个,目不暇接的判断语句冒了出来:

  • 鸟会飞吗?

会。否则:

  • 鸟死了
  • 鸟残了
  • 没翅膀
  • 企鹅

这样下去就没完了,而且还不一定靠谱,花很长时间创造这些判断,最后只剩下无尽的争论、表述的偏差、定义的模糊。

深度神经网络代表着真正的突破,因为你不再需要知晓所有细节,而是让机器自动提取出猫的特征

这里的关键是“自动”,因为每个复杂的行为背后都有数以百万计的隐藏步骤,是不可能去全部明确的,只能选择绕过,然后让电脑自己领悟。


万物的无尽步骤

来看第二个例子:计算任务的步骤。

今天我们手动为计算机定义好了每一个步骤,这就是编程。比如你想找到硬盘上所有的图片文件,然后移动到新文件夹。对绝大多任务而言,程序员就是神经网络,就是智能。他学习任务,分解成步骤,再用符号表示(编程语言)告诉计算机。这里是一个Python的小例子,来自Stack Exchange上的Jolly Jumper

示例代码
import glob import shutil import os src_dir = “your/source/dir” dst_dir = “your/destination/dir” for jpgfile in glob.iglob(os.path.join(src_dir, “*.jpg”)): shutil.move(jpgfile, dst_dir)

Jolly Jumper为计算机定义好了每一个步骤:

  • 我们需要知道源路径和目标路径
  • 需要分类方法选出目标文件格式,这里是"jpg"
  • 进入路径,搜索jpg并移动到目标路径

对于简单的,甚至一般复杂的问题,这都是可行的。操作系统由上亿行代码组成,可以算是地球上最复杂的软件了,每一行都在显式地知道计算机该做什么(绘图,存储,更新),也帮助人完成任务(复制文件,输入文本,收发邮件,浏览照片等)。

但是随着问题复杂度的增加,我们手动定义问题步骤的能力,也遇到了瓶颈。举个例子,如何开车?这种想想就很复杂的任务,包含数以百万计的小步骤:

  • 沿直线行驶
  • 知道什么是直线,并认出来
  • 从某地行驶到另一地
  • 识别障碍物如墙,人,渣渣
  • 区分有益物(交通号志)还是危险物(作死的人)
  • 实时掌握周边车辆状况
  • 决定下一个动作

在机器学习里,这就是决策制定问题,复杂的该类问题例如:

  • 机器人的运动与感知
  • 语言翻译系统
  • 自动驾驶汽车
  • 股票交易系统

神经网络的秘密花园

来看深度学习如何通过自动特征提取,来帮助我们解决那些复杂到令人发狂的问题。

如果读过V.Anton Spraul的经典书籍像程序员一样思考(强烈推荐阅读),就会知道编程是有关解决问题的。程序员化大为小,分而治之,临阵画策,写码执行

而深度学习是代替我们解决问题,但是目前AI还是需要人类(万幸)设计测试AI架构的。让我们对神经网络也分而治之,再创建程序认出我家Dove是只猫。


故能成其深

深度学习是机器学习的子学科,我们把许多不同的堆叠起来,学习数据中更有意义的表征,因而得名。其中每一个“层”就是神经网络,由人工神经元连接而成

在强大的GPU帮我们做计算之前,只能建立一些很小的“玩具”神经网络,也做不了多少事情。而现在我们可以堆叠多层,故能成其深。

“神经网络”是在1950年代受人脑研究启发而来,研究者们创造了神经元的数学表达如下(感谢斯坦福大学的优秀公开课件和维基百科):

生物的神经元生物的神经元

神经元的数学模型神经元的数学模型

忘掉所有复杂的数学符号,因为你不需要它们。

基础非常简单,X0代表数据,在神经元的连接当中流动,连接的强度由权重(W0X0, W1X1)代表。如果信号足够强,就会通过“激励函数”激活神经元。

这里是一个三层神经网络的例子:

有些神经元被激活,有些神经元之间的连接被增强,由此系统学习到了那些才是重要的。


建立并训练神经网络

接下来我们边写代码,边深入理解深度学习。系统的必要特性有:

  • 训练
  • 输入数据
  • 权重
  • 目标
  • 损失函数
  • 优化函数
  • 预测

训练

训练就是我们如何教会神经网络要学什么,分为五个步骤:

  1. 建立训练集, 记作x,并导入标签为目标y
  2. 前馈数据给网络,得到预测结果y'
  3. 定义网络的“损失”,即预测y'和真实目标y之差
  4. 计算损失的“梯度”,即我们接近或远离正确目标的速度
  5. 沿着梯度的反方向调整网络权重,并周而复始


输入数据

在本例中,DCNN的输入数据是一组图片,图片越多越好。与人类不同,计算机需要大量的图片才能学会分类。AI研究者正致力于用尽可能少的数据达到学习目的,但这仍是个前沿问题。

一个著名例子就是ImageNet数据集,由很多手动标注过的图片组成。换句话说就是预先让人类用他们内置的神经网络把图片全部看一遍,然后给数据赋予意义。人们上传照片,并打上标签,比如“狗”,或者某个品种的狗“猎兔犬”。标签代表了网络的准确预测,网络的预测输出(y')与手动标记数据(y)越接近,就说明其越准确。

数据被分为两部分,训练集和测试集。训练集就是我们给神经网络的输入,根据它们学习多种物体的关键特征,再与测试集中的随机数据相比较以衡量准确性。

在我们的程序中,将用到的是著名的CIFAR-10数据集,由加拿大高等研究所提供。CIFAR10有60000张32x32的彩色图片,共分为10类,每类6000个。其中50000个作为训练集,10000个充当测试集。

当我第一次使用CIFAR的时候,我误以为这会比ImageNet的大尺寸图片更简单。而事实却是,CIFAR10更具挑战性,因为图片尺寸小、数量少,可供神经网络识别的特征也少。

一些最大也是最差的DCNN架构如ResNet可以在ImageNet上达到97%的准确率,但在CIFAR 10却只有87%。根据我的经验,目前处理CIFAR 10的业界标杆是DenseNet,准确率可达95%。但是需要足足250层和1500万个参数!我把这些框架附在了文末,可供参考,但是开始阶段最好还是先关注些简单的问题。

理论已经讲的差不多了,是时候放码过来了。如果你对Python还不是很熟悉,我热烈,强烈,猛烈,剧烈地推荐Fabrizio Romano的Learning Python,此书把每个点都解释得特别好。我从未见过如此优秀的Python书,反而被很多耽误了不少时间。

代码基于Github上的Keras示例代码,我个人的修改可见这里

我已经调整了架构和参数,并加入了TensorBoard来辅助可视化。首先初始化我们的Python程序,导入数据集和建立DCNN所需的类。所幸,Keras已经集成了很多,所以十分方便。

程序说明
初始化Keras
示例代码
from __future__ import print_function import numpy as np from keras.datasets import cifar10 from keras.callbacks import TensorBoard from keras.models import Sequential from keras.layers import Dense, Dropout, Activation, Flatten from keras.layers import Convolution2D, MaxPooling2D from keras.utils import np_utils from keras import backend as K # 神经网络始自随机状态,虽然从什么状态开始都差不多,但是不能指望开局有多好。并且,有些随机状态会偶然得出特别极端的结果,所以我们要预设随机数种子,以免把希望寄托在运气上。 np.random.seed(114514) print("Initialized!")
程序验证过程
True

现在来添加层。大多数神经网络使用全连接层,也就是每个神经元都连接着其他所有神经元。全连接层对各种问题都表现良好,但不幸的是对图像识别的尺度缩放问题解决不到位。所以我们用卷积层来搭建系统,其独到之处就在于,不是所有神经元都相互连接

斯坦福计算机视觉课程谈卷积神经网络

“在CIFAR-10中,图片只有32x32x3(32像素宽,32像素高,3个色彩通道),所以常规神经网络的第一个隐藏层上的全连接神经元就有32323=3072个权重值。这个数量看起来还可以,但很明显全连接结构无法应对大图片。比如,生活中常见的图片尺寸,200x200x3,就会使神经元拥有120,000个权重值。不仅如此,我们还希望更多的神经元,所以参数的数量会迅速暴涨!显然,全连接层有些浪费,而大量的参数又很容易导致过拟合。”

过拟合就是把训练集学得炉火纯青,却对没见过的图片束手无措,派不上实际用场。就像你不断地玩着同一局棋,对棋谱都倒背如流,但是对手一旦变招,你就无计可施了,之后我们还会详谈过拟合。

下图表现了数据在DCNN里的流动情况,每次只关注很小一部分数据,探寻模式,基于观察建立高层认知。

卷积神经网络可视化,来自MIT计算机视觉组开发的mNeuron插件卷积神经网络可视化,来自MIT计算机视觉组开发的mNeuron插件

注意,前面的几层是简单的模式,比如边缘,颜色,基本形状。随着信息在层间流淌,系统逐渐摸清了更复杂的模式,比如纹理,最终导出物体的类别。

这一思路来自一个生物实验:研究表明猫对于不同的刺激(边缘,颜色),有不同的视觉细胞响应。

来自牛津大学深度学习公开课来自牛津大学深度学习公开课

人类也是一样,我们的每个视觉细胞只能感受特定的特征。

这一个典型的DCNN架构示意图:

你会注意到这里还有第三种层,池化层,在牛津课程斯坦福课程中可以获得更多细节。这里我们会跳过很多细节,因为大部分人难以理解,我第一次学的时候就是这样的体会。

对于池化层你需要了解如下内容:它的目的很简单,就是下采样,也就是压缩输入图片,从而降低计算负担和内存消耗。信息少了,工作起来就轻便了。池化同样有助于减少过拟合,就是网络聚焦在了训练集的个别现象上,而忽略了挑选出猫狗鸟。比如有的图片上可能会存在坏点或镜头光晕,网络可能会把光晕和狗当成一体的,而实际上根本风马牛不相及。

最后,多数DCNN会添加几个密集连层接,也即全连接层 来映射前面几层的特征并做出预测。所以现在给我们的卷积网络也加几层。

首先定义些许输入变量。

程序说明
添加变量
示例代码
# 定义每次处理的图片个数 batch_size = 128 # 定义数据集中分出的类别个数,因为CIFAR 10 只有10中物体,这里就设为0 nb_classes = 10 # epoch决定训练过程的长度,越长并非总是越好,一段时间后我们会经历收益递减,根据需要调整 nb_epoch = 45 # 这里设定图片维度,已知图片为32x32 img_rows, img_cols = 32, 32 # 卷积滤波器的个数 nb_filters = 32 # 最大池化的池化面积 pool_size = (2, 2) # 卷积核的尺寸 kernel_size = (3, 3) print("batch_size={}, nb_classes={}, nb_epoch={}, img_rows={}, img_cols={}, nb_filters={}, pool_size={}, kernel_size={}".format(batch_size, nb_classes, nb_epoch, img_rows, img_cols, nb_filters, pool_size, kernel_size))
程序验证过程
True

卷积核池化面积定义了卷积网络如何处理图片挖掘特征。最小的卷积核可以是1X1,也就是我们认为关键特征只有1像素大小。比较典型的核尺寸有3像素,再将特征池化为2X2网格。

2X2网格从图片中抽取特征,并像炉石卡组一样堆叠起来,这就把它们从原图的特定位置剥离了出来,以便让系统寻找各处的直线或圆圈,而不仅限于最早被发现的位置。

很多教程将这一过程描述为处理“平移不变性”。


什么是平移不变性?好问题。

再看下面这张图:

如层1和层2所示,系统并不是一下子就把特征的本质给抓出来了,而是可能觉得猫鼻子的圆圈只有在图像正中(第一次发现该圆圈处)时,才是重要特征。

以我家Dove为例,如果系统最开始在她的眼睛上发现了一个圈,那么系统可能会错误的认为,这个圈在图中的位置与辨认猫也是相关的。而实际上,系统应该同等重视任何可能出现圆圈的地方:

在给神经网络添加层之前,我们先载入并处理数据:

程序说明
Keras数据预处理
预处理代码
import os if not os.path.exists("/home/jovyan/.keras/datasets"): os.mkdir("/home/jovyan/.keras/datasets") os.system("ln -s /mnt/vol0/Funny_Pro/cifar-10-batches-py.tar.gz ~/.keras/datasets")
示例代码
# 将数据分为训练集和测试集并载入。CIFAR10是Keras的标准测试集,为了节省临时下载的时间,集智已经将数据预存在了后台,只需解压即可。 (X_train, y_train), (X_test, y_test) = cifar10.load_data() # 麻烦的是,TensorFlow和Theano对张量参数的顺序要求不同,所以我们要检查后台并做相应设置。 if K.image_dim_ordering() == 'th': X_train = X_train.reshape(X_train.shape[0], 3, img_rows, img_cols) X_test = X_test.reshape(X_test.shape[0], 3, img_rows, img_cols) input_shape = (3, img_rows, img_cols) else: X_train = X_train.reshape(X_train.shape[0], img_rows, img_cols, 3) X_test = X_test.reshape(X_test.shape[0], img_rows, img_cols, 3) input_shape = (img_rows, img_cols, 3) print('X_train shape:', X_train.shape) print(X_train.shape[0], 'train samples') print(X_test.shape[0], 'test samples') # 将类别向量转换为二进制矩阵(独热码) Y_train = np_utils.to_categorical(y_train, nb_classes) Y_test = np_utils.to_categorical(y_test, nb_classes)
程序验证过程
True

OK,现在我们终于准备好给神经网络添加一些层了:

程序说明
Keras添加层1
示例代码
model = Sequential() model.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1], border_mode='valid', input_shape=input_shape)) model.add(Activation('relu')) model.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1])) # 请在此处再添加一层激励层,函数使用"ReLU" # 代码补完 # 代码补完 model.add(MaxPooling2D(pool_size=pool_size)) model.add(Dropout(0.25)) model.summary()
正确答案
model.add(Activation('relu'))
程序验证过程
model.get_config()[3]['config']['activation'] == 'relu'
提示信息
明确激励函数的定义,可在已有代码中寻找答案。

神经网络中堆叠起来的层依次有:

  • 卷积
  • 激励
  • 卷积
  • 激励
  • 池化
  • Dropout

多数类型已经介绍过了,除了两个:dropout激励

Dropout更容易理解些,本质上就是模型会按比例丢弃一些信息,就像Netflix的Chaos Monkey,它会按照脚本随机关闭一些服务节点以保证鲁棒性和荣誉度。这里也是一样道理,我们希望神经网络不要过度依赖于某一个特征。

激励层就是决定神经元是否被“激活”的标准,可供选择的激励函数有很多,ReLU是其中最成功的一种,这得益于它较高的计算效率。这里有Keras中可选的不同激励函数列表

我们还添加了第二个卷积网络与第一个互成镜像,如果我们追求编程效率的话,可以创建一个模型生成器以循环叠加神经网络层。不过这里我们就简单地复制粘贴一下,为图个方便,其实是有违Python之禅的。

最后,我们再添加一些全连接层,已经另一个Dropout层,再把所有特征映射扁平化:

程序说明
Keras添加层2
预处理代码
model = Sequential() model.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1], border_mode='valid', input_shape=input_shape)) model.add(Activation('relu')) model.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1])) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=pool_size)) model.add(Dropout(0.25))
示例代码
model.add(Flatten()) model.add(Dense(256)) model.add(Activation('relu')) # 添加一个Dropout层,参数为0.5 # 代码补完 # 代码补完 model.add(Dense(nb_classes)) model.add(Activation('softmax')) model.summary()
正确答案
model.add(Dropout(0.5))
程序验证过程
model.get_config()[9]['config']['p'] == 0.5
提示信息
model.add(Dropout())

在最后一层,我们使用了不同的激励函数:softmax,因为这定义了每个类的概率分布。

预知以上代码究竟几个意思,且听下回分解。


讨论组:

QQ群:557373801

微信群:请添加客服申请加入,客服ID: jizhi_im

您也许喜欢这些文章

集智专栏

深度学习环境搭建手册——安装Nvidia、Cuda、CuDNN、TensorFlow以及Keras

发表至数据科学
本文简要介绍了在Ubuntu系统上搭建基于GPU的深度学习运行环境所需要的各类深度学习驱动、工具以及软件包的安装流程和注意事项。
集智专栏

人工智能揭秘,带你了解AI的前世今生

发表至数据科学
我们已经看过太多的媒体和人们在讨论人工智能,在讨论人类将会被AI统治。毫无疑问,人工智能已经成为了这个时代的主题和趋势,那么,到底什么是人工智能,人工智能又包含哪些部分,它的未来又是怎样的呢?本文将带你一一揭开人工智能的神秘面纱。
集智专栏

[Scikit-learn教程] 02.04 无监督学习:追寻数据表征

发表至系列教程
此前所有的算法,不论回归(Regression)还是分类(Classification),都是根据已知标签训练模型再应用到新数据上。而如果一开始就没有标签可供参考呢?就需要用到新的机器学习方法:聚类(Clustering)。

文章评论(4)

Kaiser集智 站长 发表于 1年前回复
回复a-will:感谢K神。请问里面的数字是随机设置的吗?或者有木有一些关于随机数的链接,学习下~~
可以看下这篇文章:http://blog.csdn.net/vicdd/article/details/52667709,详细解释了一下numpy里random函数
a-will 发表于 1年前回复
回复Kaiser:有啊,设置随机数种子,才能保证每次初始化的结果是一样的,不会被随机结果所干扰。
感谢K神。请问里面的数字是随机设置的吗?或者有木有一些关于随机数的链接,学习下~~
Kaiser集智 站长 发表于 1年前回复
回复a-will:对随机数种子有些疑惑,它和权值初始化有关系吗?看到蛮多例程并没有设置随机数种子……
有啊,设置随机数种子,才能保证每次初始化的结果是一样的,不会被随机结果所干扰。
a-will 发表于 1年前回复
对随机数种子有些疑惑,它和权值初始化有关系吗?看到蛮多例程并没有设置随机数种子……