第 11 章 用深度学习方法为图像中的物体进行分类

第8章用到了最基础的神经网络。最近几年,该领域兴起的研究热潮为其带来了一系列长足的进步。如今,神经网络的研究创造出了一些最为先进和精确的分类算法,在很多领域展露锋芒。

计算机性能的提升使得训练更大、更复杂的神经网络成为可能,但该领域之所以能够取得进步,主要不是因为计算性能的提升,而是因为采用了新的算法和神经网络层。

本章研究如何确定图像中的物体,我们使用像素值作为神经网络的输入值,自动找到有用的像素组合,形成更高层级的特征,然后将其用于实际的分类。本章主要内容如下:

  • 为图像中物体分类

  • 不同类型的深度神经网络

  • TheanoLasagneNolearn:用于创建和训练神经网络的库

  • 用GPU提升算法速度

11.1 物体分类

计算机视觉逐渐成为科技领域的明日之星。未来五年,我们也许就能坐上自动驾驶汽车(甚至可能比这还要提前,如果流传的消息是可信的)。要让计算机来开车,它得能看清周围的环境:障碍物、其他车辆和天气状况等。

虽然计算机借助雷达等技术很容易就能检测到是否有障碍物,但这还不够,它还需要知道障碍物是什么。假如是动物,它们可能很快就会跑开,但如果是建筑物,那就麻烦了,计算机需要控制车辆躲开。

11.2 应用场景和目标

本章将构建一个系统,它接收图像,给出图像里面是什么物体。该系统就好比是自动驾驶汽车的视觉系统,它能发现道路及两侧的任何障碍物。我们使用如下形式的图像。

{%}

图像数据来自于著名的CIFAR-10数据集,它含有6万张32像素见方的图像,每个像素都有一个红—绿—蓝(RGB)值。这个数据集已经分为训练集和测试集两部分,我们在训练结束后才会用到测试集,这里先提一下。

第 11 章 用深度学习方法为图像中的物体进行分类 - 图2 CIFAR10数据集的下载地址为http://www.cs.toronto.edu/~kriz/cifar.html。请下载Python版本,所有图像已经转换为numpy数组。

我们来看下这些图像数据长什么样,打开一个IPython Notebook笔记本文件,设置好文件名。我们开始只用第一批文件,到后面再增加数量使用全部数据集。

  1. import os
  2. data_folder = os.path.join(os.path.expanduser("~"), "Data", "cifar-10-
  3. batches-py")
  4. batch1_filename = os.path.join(data_folder, "data_batch_1")

接着,创建一个函数,读取第一批图像文件数据。这些图像数据文件格式为pickle,pickle是Python用来保存对象的一个库。通常在pickle文件上调用pickle.load方法就能取得保存在里面的数据。但这里有个小问题:这些pickle文件是Python 2生成的,而我们要用Python3打开。所以在打开时需要把编码设置成latin(虽然我们是以字节模式打开)。

  1. import pickle
  2. # Bigfix thanks to: http://stackoverflow.com/questions/11305790/
  3. pickle-incompatability-of-numpy-arrays-between-python-2-and-3
  4. def unpickle(filename):
  5. with open(filename, 'rb') as fo:
  6. return pickle.load(fo, encoding='latin1')

使用上述函数加载数据集。

  1. batch1 = unpickle(batch1_filename)

这一批图像文件读进来后为一个字典结构,包含numpy数组形式的图像数据、图像类别、文件名以及该批文件的简短说明(例如,training batch 1 of 5表示训练集共五批,这是第一批)。

以data作为键,从字典batch1中取到存储这一批图像数据的列表后,再用图像索引号就能取到其中一张图像的数据。

  1. image_index = 100
  2. image = batch1['data'][image_index]

每一张图像数据为一个numpy数组,数组共有3072项,每一项为0到255之间的一个值。每个值代表图像某一位置的红、绿或蓝色的颜色强度。

图像数据格式与matplotlib(绘制图像)所使用的有所不同,因此,用它来显示图像前,需要改变数组的形状,对矩阵进行转换。这种数据格式不会影响神经网络训练(我们定义网络时会考虑支持这种数据格式),但是要用matplotlib绘制图像,就需要对数据格式进行转换。

  1. image = image.reshape((32,32, 3), order='F')
  2. import numpy as np
  3. image = np.rot90(image, -1)

然后,就可以用matplotlib绘制图像。

  1. %matplotlib inline
  2. from matplotlib import pyplot as plt
  3. plt.imshow(image)

这是张轮船图像。

{%}

图像分辨率很低——只有32像素见方。尽管如此,大部分人还是能看出图中是一艘船。我们能让计算机辨认出来吗?

你可以修改图像索引查看其他图像,了解下数据集的特点。

本章项目的目标是创建分类系统,预测像上面这样的图像的类别。

使用场景

计算机视觉技术应用范围很广。

在线地图网站,比如谷歌地图就使用计算机视觉技术,自动为街景中的人脸添加模糊效果,以保护被拍摄到的人们的隐私。除此之外,还有别的用途。

人脸识别技术在很多行业都有所应用。如今,照相机都能自动识别人脸,以提升拍摄质量(拍照时用户往往把相机聚焦到脸部)。人脸识别技术还可以用来识别照片中的人脸。Facebook就使用了这项技术,方便用户标记朋友。

我们前面也说过,自动驾驶汽车高度依赖计算机视觉来识别道路,躲避障碍物。计算机视觉是很多行业、应用都在努力解决的一个问题,不只是为了研制自动驾驶汽车,也不只是为了满足消费者的需求,采矿业等行业也在寻求攻克计算机视觉技术难关之路。

除此之外,其他行业也在使用计算机视觉技术,比如仓储业自动检测不合格产品就用到该项技术。

航天工业使用计算机视觉技术实现数据采集的自动化。这对于有效使用航天器来说至关重要,因为从地球上发送信号到火星上的探测车要花费很长时间,有时还会无法送达(例如,地球和火星不是面对面时)。人们跟太空探测器打交道越来越多,而从遥远的地球控制它们很不方便,增加这些设备的自主性就显得很有必要。

下图为美国国家航空和航天局(NASA)设计的火星车,它大量应用了计算机视觉技术。

第 11 章 用深度学习方法为图像中的物体进行分类 - 图4

11.3 深度神经网络

从理论角度看,第8章所使用的神经网络有几个显著的特点。例如,只需要一层隐含层来学习任何类型的映射(虽然隐含层可能规模很大)。神经网络在20世纪七八十年代很火,但随后人们不再使用它们,尤其是跟支持向量机等其他分类算法相比,神经网络被冷落了。首要问题是,运行多个神经网络比其他算法对计算能力有更高的要求,超出了当时计算机的能力范围。

其次是训练神经网络所需工作量很大。虽然那时反向传播算法已经研制出来,处理大型神经网络仍存在问题,需要大量的训练才能确定权重。

这两个问题在近些年都得到了解决,神经网络迎来了又一个春天。计算能力比起30年前更容易获得,训练算法的改进使得在现有计算能力的情况下,能够顺利完成大型神经网络的训练。

11.3.1 直观感受

深度神经网络和第8章中的基本神经网络的差别在于规模大小。至少包含两层隐含层的神经网络被称为深度神经网络。实际工作中遇到的深度神经网络通常规模很大,每层神经元数量和层次都非常多。2000年年中的一些研究,关注的是层次数量多的深度神经网络,而更巧妙的算法能够减少实际所需要的层数。

神经网络接收很基础的特征作为输入——就计算机视觉而言,输入为简单的像素值。 神经网络算法把这些数据整合起来向网络中传输,在这个过程中,基本的特征组合成复杂的特征。有时,这些组合特征对我们来说没有什么含义,但是它们表示个体某些方面的特征,计算机依靠它们进行分类。

11.3.2 实现

由于深度神经网络规模很大,实现起来很有挑战。实现不好,运行时间会很长,甚至还会出现由于内存不足,最后无法运行的情况。

神经网络的基本实现可以从创建神经元类开始,多个神经元组成层类,每一层的神经元使用边这个类的一个实例连接到另一层神经元。这种基于类的实现,有助于显示网络的工作方式,但是对于创建大型网络,效率较低。

神经网络的核心其实就是一系列矩阵运算。两个网络之间连接的权重可以用矩阵来表示,其中行表示第一层的神经元,列表示第二层神经元(有时会用到该矩阵的转置矩阵)。矩阵的每一个元素表示位于不同层的两个神经元之间连接的权重。一个神经网络就可以用一组这样的矩阵来表示。除了神经元外,每层增加一个偏置项,它是一个特殊的神经元,永远处于激活状态,并且跟下一层的每一个神经元都有连接。

对神经网络有了上述这般深入理解,我们就可以使用数学运算创建、训练和使用神经网络,比起前面用类来实现,效率要更高。就矩阵运算而言,有很多非常好用的库,其代码经过充分优化,借助它们可以高效完成矩阵运算。

第8章使用的PyBrain还实现了神经网络的卷积层。但是我们这里要用到的一些功能,它没有提供。对于规模更大,定制化程度更高的神经网络,我们需要更强大的库。因此,我们选用Lasagnenolearn两个库。这两个库依赖于擅长处理数学表达式的Theano库。

本章首先通过用Lasagne实现一个基础的神经网络,来介绍相关概念。然后,使用nolearn库重新做第8章中的字母识别实验。最后,使用更为复杂的卷积神经网络对CIFAR数据集进行图像分类,为了提升性能我们使用GPU而不是CPU运行算法。

11.3.3 Theano简介

Theano是用来创建和运行数学表达式的工具。它用起来乍一看跟编写程序没有差别,但是在Theano中,我们定义函数要做什么而不是怎么做,这样Theano就能以最佳的方式对表达式进行求值,还可以进行延迟计算——只在需要时,对表达式求值,而不是定义时。

多数程序员不会每天都使用这种类型的编程范式,但是他们几乎天天接触使用这种编程范式的系统。关系型数据库,尤其是SQL类,就用到了叫作声明型范式(declarative paradigm)的概念。比如程序员定义了含有WHERE子句的SELECT查询语句。数据库解释查询语句,并根据一系列因素创建优化过的查询语句,数据库要考虑的因素有WHERE子句是否建立在主键之上、数据存储格式等。程序员决定他们要什么,系统决定怎么做。

第 11 章 用深度学习方法为图像中的物体进行分类 - 图5 你可以使用pip安装Theanopip3 install Theano

我们可以用Theano来定义函数,处理标量(scalars)、数组和矩阵及其他数学表达式。例如,我们可以创建计算直角三角形斜边长度的函数。

  1. import theano
  2. from theano import tensor as T

首先,定义两个输入ab,它们为简单的数值类型,因此使用标量。

  1. a = T.dscalar()
  2. b = T.dscalar()

接着,定义输出c,它为由ab构成的一个表达式。

  1. c = T.sqrt(a ** 2 + b ** 2)

注意,上述c既不是函数,也不是一个数值——它是由ab组成的表达式。ab不是一个确切的值——这是一个代数式,最终结果不确定。为了计算这个表达式,我们来定义一个函数。

  1. f = theano.function([a,b], c)

上述语句告诉Theano创建一个函数,这个函数接收ab,返回经过计算得到的结果,也就是输出值c。例如,f(3, 4),返回5。

虽然上述例子看起来可能不比Python普通函数强大多少,但是我们现在就在代码其他部分和后面的映射关系中使用我们的函数或数学表达式c。此外,虽然我们在创建函数之前,就定义了表达式,其实直到调用函数时,才会对表达式进行计算。

11.3.4 Lasagne简介

Theano库不是用来创建神经网络的,就好比numpy库不是用来执行机器学习任务的,而是被别的库使用,来出卖苦力的。Lasagne库是专门用来构建神经网络的,它使用Theano进行计算。

Lasagne实现了几种比较新的神经网络层和组成这些层的模块,具体介绍如下。

  • 内置网络层(Network-in-network layers):这些小神经网络比传统的神经网络层更容易解释。

  • 删除层(Dropout layers):训练过程随机删除神经元,防止产生神经网络常见的过拟合问题。

  • 噪音层(Noise layers):为神经元引入噪音,也是为了解决过拟合问题。

本章使用卷积层(convolution layers,层级结构模拟人类视觉工作原理)。卷积层使用少量相互连接的神经元,分析一部分输入值(比如我们这里的一张图像),便于神经网络实现对数据的标准转换,比如对图像数据的转换。视觉分析实验,就是用卷积层对图像进行转换1。

1原文为translating the image。——译者注

比较而言,传统的神经网络内部连接十分紧密——一层的所有神经元全都连接到下一层所有神经元。

lasagne.layers.Conv1DLayerlasagne.layers.Conv2DLayer classes两个类实现了卷积网络。

第 11 章 用深度学习方法为图像中的物体进行分类 - 图6

写作本书时,Lasagne还没有正式版,资源没有上传到pip站点,因此无法用pip安装。但是可以从github安装。把Lasagne代码仓库克隆到本地新建的文件夹中:git clone https://github.com/Lasagne/Lasagne.git。进入到Lasagne文件夹,使用下面命令安装:

  1. sudo python3 setup.py install

安装指南请见http://lasagne.readthedocs.org/en/latest/user/installation.html

神经网络使用卷积层(一般而言,仅卷积神经网络包含该层)和池化层(pooling layer),池化层接收某个区域最大输出值,可以降低图像中的微小变动带来的噪音,减少(down-sample,降采样)信息量,这样后续各层所需工作量也会相应减少。

Lasagne还实现了池化层——比如lasagne.layers.MaxPool2DLayer类。再加上前面的卷积层,现在我们准备好了创建卷积神经网络所需的全部部件。

Lasagne中创建神经网络比起只用Theano更加容易。我们通过实现一个简单的卷积神经网络,介绍相关规则。我们再次使用第1章所用到的Iris数据集,该数据集很适合用来测试新算法,即使是复杂的深度神经网络也没问题。

首先,打开一个新的笔记本文件。前面加载CIFAR数据集所使用的笔记本文件,先搁一边,后面还会用。

加载Iris数据集。

  1. from sklearn.datasets import load_iris
  2. iris = load_iris()
  3. X = iris.data.astype(np.float32)
  4. y_true = iris.target.astype(np.int32)

Lasagne对数据类型有特殊要求,因此,需要把类别值转换为int32类型(在原始数据集中用int64类型存储)。

把数据集分为训练集和测试集两部分。

  1. from sklearn.cross_validation import train_test_split
  2. X_train, X_test, y_train, y_test = train_test_split(X, y_true, random_
  3. state=14)

接着,分别创建卷积神经网络各层。我们的数据集有四个特征、三个类别,这样第一层和最后一层神经元数量就确定了,但是中间层多大呢?中间层大小不同,最终结果也会不同,尝试使用不同的值,看看结果会有怎样的变化。

首先,创建输入层,其神经元数量跟数据集特征数量相同。可以指定每一批输入的数量(设置为10),这样Lasagne可以在训练阶段做些优化。

  1. import lasagne
  2. input_layer = lasagne.layers.InputLayer(shape=(10, X.shape[1]))

接着,创建隐含层。该层从输入层接收输入(由第一个参数指定),该层共有12个神经元,使用非线性的sigmoid函数,我们在第8章曾经介绍过。

  1. hidden_layer = lasagne.layers.DenseLayer(input_layer, num_units=12,
  2. nonlinearity=lasagne.nonlinearities.sigmoid)

接下来,创建输出层,它接收来自隐含层的输入,输出层共有三个神经元(跟类别的数量一致),使用非线性的softmax函数,该函数主要用于神经网络的最后一层。

  1. output_layer = lasagne.layers.DenseLayer(hidden_layer, num_units=3,
  2. nonlinearity=lasagne.
  3. nonlinearities.softmax)

依照Lasagne的习惯用法,输出层为我们的神经网络。当我们输入一条数据到神经网络时,它查看输出层,向上回溯找到向输出层提供输入的那一层(第一个参数)。这个过程重复进行直到到达输入层,因为输入层没有上一层,所以就把要处理的数据交给输入层处理。输入层的激活函数把接收到的数据处理后输出给调用它的层(我们这里是隐含层),然后再一步步在网络中传播直到输出层。

为了训练刚创建的网络,我们需要定义几个Theano训练函数。在这之前,需要定义一个Theano表达式和函数。我们先来为神经网络的输入数据、输出结果和实际输出结果声明变量。

  1. import theano.tensor as T
  2. net_input = T.matrix('net_input')
  3. net_output = output_layer.get_output(net_input)
  4. true_output = T.ivector('true_output')

接着,定义损失函数,训练函数如何提升网络效果需要参考它的返回值——训练神经网络时,以最小化损失函数的返回值为前提。我们用类别交叉熵(categorical cross entropy)表示损失,这是一种衡量分类数据(categorical data)分类效果好坏的标准。损失函数表示的是网络的期望输出和实际输出两者之间的差距。

  1. loss = T.mean(T.nnet.categorical_crossentropy(net_output,
  2. true_output))

接着,定义修改网络权重的函数。我们需要获取到网络的所有参数,创建调整权重的函数(使用Lasagne提供的工具),使得损失降到最小。

  1. all_params = lasagne.layers.get_all_params(output_layer)
  2. updates = lasagne.updates.sgd(loss, all_params, learning_rate=0.1)

最后,创建两个Theano函数,先是训练网络,然后获取网络的输出,以用于后续测试。

  1. import theano
  2. train = theano.function([net_input, true_output], loss,
  3. updates=updates)
  4. get_output = theano.function([net_input], net_output)

然后调用训练函数,在训练集上进行一轮迭代,接收训练数据,预测类别,与给定类别作比较,更新特征权重,以最小化损失。然后再进行1000次迭代,逐渐改进神经网络。

  1. for n in range(1000):
  2. train(X_train, y_train)

接着,对输出计算F值,以评估分类效果。首先获取到所有输出。

  1. y_output = get_output(X_test)

第 11 章 用深度学习方法为图像中的物体进行分类 - 图7 注意,get_outputTheano函数,是我们从神经网络中获取到的,因此,上述代码不用再把神经网络作为参数传进去。

y_output为输出层每个神经元的激励作用(activation)大小。找出激励作用最高的神经元,就能得到预测结果。

  1. import numpy as np
  2. y_pred = np.argmax(y_output, axis=1)

数组y_pred为预测结果数组,跟我们在前面分类任务中用到的相同。然后,我们就可以计算F1值。

  1. from sklearn.metrics import f1_score
  2. print(f1_score(y_test, y_pred))

结果异常完美——1.0!这表明我们的算法为测试集中所有数据正确分类,这个结果太棒了(虽然数据集有点小)。

从上面我们可以看到,仅用Lasagne库也能创建、训练一个神经网络,但是有点麻烦。我们可以使用nolearn库,对上述过程做进一步封装,使它很容易就能与scikitlearn接口对得上。

11.3.5 用nolearn实现神经网络

nolearnLasagne进行了封装,虽然比起用Lasagne创建神经网络,使用nolearn无法对一些细节进行调整,但是代码可读性更强,也更容易管理。

nolearn实现了几种常见的复杂程度很高的神经网络,应该能够满足你的需求。如果你想拥有更多的控制权,可以回头继续使用Lasagne,但是创建和训练过程需要你多花点心思。

为了快速了解nolearn,我们再次来实现第8章中识别图像中字母的实验。我们重建在第8章所创建的密集神经网络(dense neural network)。再次在笔记本文件输入创建数据集的代码。这些代码的具体含义,请参考第8章。

  1. import numpy as np
  2. from PIL import Image, ImageDraw, ImageFont
  3. from skimage.transform import resize
  4. from skimage import transform as tf
  5. from skimage.measure import label, regionprops
  6. from sklearn.utils import check_random_state
  7. from sklearn.preprocessing import OneHotEncoder
  8. from sklearn.cross_validation import train_test_split
  9. def create_captcha(text, shear=0, size=(100, 24)):
  10. im = Image.new("L", size, "black")
  11. draw = ImageDraw.Draw(im)
  12. font = ImageFont.truetype(r"Coval.otf", 22)
  13. draw.text((2, 2), text, fill=1, font=font)
  14. image = np.array(im)
  15. affine_tf = tf.AffineTransform(shear=shear)
  16. image = tf.warp(image, affine_tf)
  17. return image / image.max()
  18. def segment_image(image):
  19. labeled_image = label(image > 0)
  20. subimages = []
  21. for region in regionprops(labeled_image):
  22. start_x, start_y, end_x, end_y = region.bbox
  23. subimages.append(image[start_x:end_x,start_y:end_y])
  24. if len(subimages) == 0:
  25. return [image,]
  26. return subimages
  27. random_state = check_random_state(14)
  28. letters = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
  29. shear_values = np.arange(0, 0.5, 0.05)
  30. def generate_sample(random_state=None):
  31. random_state = check_random_state(random_state)
  32. letter = random_state.choice(letters)
  33. shear = random_state.choice(shear_values)
  34. return create_captcha(letter, shear=shear, size=(20, 20)),
  35. letters.index(letter)
  36. dataset, targets = zip(*(generate_sample(random_state) for i in
  37. range(3000)))
  38. dataset = np.array(dataset, dtype='float')
  39. targets = np.array(targets)
  40. onehot = OneHotEncoder()
  41. y = onehot.fit_transform(targets.reshape(targets.shape[0],1))
  42. y = y.todense().astype(np.float32)
  43. dataset = np.array([resize(segment_image(sample)[0], (20, 20)) for
  44. sample in dataset])
  45. X = dataset.reshape((dataset.shape[0], dataset.shape[1] * dataset.
  46. shape[2]))
  47. X = X / X.max()
  48. X = X.astype(np.float32)
  49. X_train, X_test, y_train, y_test = \
  50. train_test_split(X, y, train_size=0.9, random_state=14)

神经网络由一系列的层组成。在nolearn中实现神经网络,只需定义它由Lasagne哪几种类型的层组成就行,跟PyBrain做法大同小异。第8章所创建的神经网络使用的是全连通密集层。nolearn有相应的实现,这表明我们可以用nolearn实现的各层来复制第8章神经网络的基本结构。首先,创建由输入层、密集隐含层和密集输出层组成的层级结构。

  1. from lasagne import layers
  2. layers=[
  3. ('input', layers.InputLayer),
  4. ('hidden', layers.DenseLayer),
  5. ('output', layers.DenseLayer),
  6. ]

导入以下所需模块,用到时再解释它们的作用。

  1. from lasagne import updates
  2. from nolearn.lasagne import NeuralNet
  3. from lasagne.nonlinearities import sigmoid, softmax

接着,定义神经网络,它与scikitlearn估计器兼容。

  1. net1 = NeuralNet(layers=layers,

注意上面代码没加右半边括号——我们有意为之。输入神经网络的参数,先从每层的大小开始吧。

  1. input_shape=X.shape,
  2. hidden_num_units=100,
  3. output_num_units=26,

上述参数跟每一层相对应。换句话说,input_shape参数就会去查找名称为input的输入层,这跟在流水线中设置参数的工作原理很相似。

接着,定义非线性函数。跟之前一样,隐含层使用sigmoid函数,输出层使用softmax函数。

  1. hidden_nonlinearity=sigmoid,
  2. output_nonlinearity=softmax,

接下来,指定偏置神经元,它们位于隐含层,一直处于激活状态。偏置神经元对于网络的训练很重要,神经元激活后,可以对问题做更有针对性的训练,以消除训练中的偏差。举个简单的例子,如果预测结果总是偏差4,我们可以使用-4偏置值抵消偏差。我们设置的偏置神经元就能起到这样的作用,训练得到的权重决定了偏置值的大小。

偏置项为一组权重值,它所包含的元素数量应与它所连接的层的大小相同。

  1. hidden_b=np.zeros((100,), dtype=np.float32),

接下来,定义神经网络的训练方式。我们在第8章中用到的训练方法,nolearn没有与之完全相同的,因为nolearn没有削减权重的方法。然而,它的确有冲量(momentum),我们这里使用一个高学习速率和低冲量值。

  1. update=updates.momentum,
  2. update_learning_rate=0.9,
  3. update_momentum=0.1,

接下来,我们把分类问题定义为回归问题。这看起来有点奇怪,因为这是分类任务。然而,别忘了输出结果是一组数值,在训练阶段把它当成回归问题进行优化比起分类问题效果更好。

  1. regression=True,

最后,最大训练步数(epoch)设置为1000,这样既能保证训练效果,又不至于花太长时间(对于我们数据集很合适,但其他数据集训练步数会有所差异)。

  1. max_epochs=1000,

终于可以把创建神经网络这个函数的右半边括号加上了。

  1. )

接着,在训练集上训练网络。

  1. net1.fit(X_train, y_train)

现在,我们来评估训练得到的网络。首先,获取神经网络的输出,跟Iris分类一样,我们需要使用argmax方法找到输出值中最大的一项,然后找到它所对应的类别。

  1. y_pred = net1.predict(X_test)
  2. y_pred = y_pred.argmax(axis=1)
  3. assert len(y_pred) == len(X_test)
  4. if len(y_test.shape) > 1:
  5. y_test = y_test.argmax(axis=1)
  6. print(f1_score(y_test, y_pred))

结果同样振奋人心——在我的计算机上,再次全部预测正确。然而,你可能会得到不同的结果,因为nolearn库具有一定的随机性,眼下无法直接控制。

11.4 GPU优化

神经网络体积可能会增长得很大,这意味着需要更多内存;然而,使用稀疏矩阵这样高效的存储方式把整个神经网络装进内存一般不会遇到问题。

神经网络体积增长带来的主要问题是计算时间增加。此外,对于有些数据集,神经网络需要更多的训练步数才能较好地拟合数据集。本章所创建的神经网络在我性能还不错的计算机上,每完成一步训练需要八分多钟,而我们需要运行数十步甚至成百上千步训练。体积更大的神经网络,每一步训练需要几个小时,而为了取得最佳效果,可能需要成千上万步训练。

多长时间才能完成训练啊!

好在神经网络最核心的计算类型主要为浮点型运算。此外,由于神经网络训练过程主要为矩阵操作,大量运算都可以并行处理。这些因素表明用GPU进行计算,能提升训练速度。

11.4.1 什么时候使用GPU进行计算

GPU的设计初衷是用于图像渲染。这些图像用矩阵及由矩阵组成的数学方程式来表示,它们被转换为我们在屏幕上看到的像素。这个过程涉及大量并行计算。如今的CPU很可能是多核(你的计算机可能有2、4,甚至16个核——或更多!),GPU却拥有专门用于图像处理的成千上万个小核。

CPU每个核单独工作速度往往更快,访问计算机内存等操作效率更高,因而适合处理序列化任务。说实话,让CPU出苦力更容易,因为几乎所有的机器学习库默认使用CPU,如果要用GPU做并行计算,需要很多额外工作,但是它所带来的好处也是显而易见的。

有些任务,包含大量数字运算,但计算量都不大,可以同时处理,这样的任务最好交由GPU来处理。很多机器学习任务都具有类似的特点,因此使用GPU处理能提升算法运行速度。

让代码在GPU上跑起来可不简单,中间可能会遇到各种各样的挫折,具体与GPU类型、配置方式、操作系统相关,有时你可能还需要修改计算机底层设置。

使用GPU运算,主要方法有以下三种。

  • 第一种,使用现有计算机。了解你的计算机型号,查找GPU、操作系统相关的软件和驱动程序,寻找相关教程,软件、教程是否能用与操作系统相关。不过,现在使用GPU,比前几年容易多了,有更多更好的软件和驱动程序来启动GPU计算。

  • 第二种,选定计算机系统,找到关于如何设置系统的文档,购买一台装有这种系统的新计算机,买的系统很好用,但是可能相当昂贵——如今的计算机,GPU是价格最高的部件之一。尤其是你对性能有更高要求的话——你需要一个价格不菲的好GPU。

  • 第三种,使用专门为GPU计算而配置的虚拟机。例如,Markus Beissinger在亚马逊的AWS上搭建了一个这样的系统,该系统不是免费的,你得付费才能使用它提供的服务,但是比起购买一台新计算机要便宜得多。具体费用由你所在的区域、使用的系统类型和资源消耗数量来决定,你可能每小时花不到1美元,往往比这还要少得多。如果你使用AWS的竞价型实例(spot instance),每小时只需几美分(然而你需要单独开发在竞价型实例上运行的代码)。

如果你无法承担虚拟机开销,我建议你使用上面提到的第一种方法,即使用现有的计算机。你还可以试试看能不能从隔段时间就要换装备的家人或朋友那里找到二手GPU(爱玩游戏的朋友很可能有哦!)。

11.4.2 用GPU运行代码

我们这里将使用上述第三种方法,在Markus Beissinger系统的基础上创建一个虚拟机,该虚拟机运行在亚马逊的EC2云平台上。除了EC2,还有很多其他Web服务可供使用,但方法可能跟这里有所不同。我会讲下如何在亚马逊云平台上搭建虚拟机。

如果你打算使用自己的计算机,并且已经配置好,可以用GPU进行计算了,请跳过该节。

第 11 章 用深度学习方法为图像中的物体进行分类 - 图8 关于如何进行设置的更多信息,请见http://markus.com/installtheano-on-aws/,也许还提供在其他计算机上启用GPU计算的方法。

下面,看下如何在AWS上创建虚拟机。首先,访问AWS的登录界面。

https://console.aws.amazon.com/console/home?region=us-east-1

用你的AWS账号登录。如果没有,得先创建一个才能继续下面的操作。

接着,访问EC2云平台的控制台:https://console.aws.amazon.com/ec2/v2/home?region=us-east-1

点击Launch Instance,从右上方的下拉菜单中选择N. California作为目的地。

点击Community AMIs,搜索ami-b141a2f5,这就是Markus Beissinger创建的系统。然后,点击Select。下一屏中的机器类型选择g2.2xlarge,点击Review and Launch。在下一屏,点击Launch。

这个时候,AWS开始向你收费,所以使用完毕后请关机。从EC2云平台选择你刚创建的云主机,先停止运行。机器处于停机状态,不需要支付费用。

系统会给出如何连接虚拟机的相关信息。如果你之前没有使用过AWS,你可能需要创建密钥以安全连接虚拟机。创建时,需要指定密钥的名称,下载.pem格式的密钥文件,将其保存到一个安全地方——一旦丢失,你将无法登录自己的虚拟机!

点击Connect,了解如何使用密钥文件连接虚拟机。你很可能是用如下ssh命令登录。

  1. ssh -i <certificante_name>.pem ubuntu@<server_ip_address>

11.5 环境搭建

连接到虚拟主机后,你可以安装最新的Lasagnenolearn库。

首先,使用git命令克隆Lasagne代码仓库,本章前面用过。

  1. git clone https://github.com/Lasagne/Lasagne.git

在虚拟机上编译这个库,需要用到Python 3的setuptools,我们可以用Ubuntu常用的应用和包管理命令apt-get来安装;我们还需要numpy的开发版。

在虚拟机的命令行运行下述命令。

  1. sudo apt-get install python3-pip python3-numpy-dev

接着,安装Lasagne。首先,切换到源代码所在的目录,然后运行setup.py完成安装。

  1. cd Lasagne
  2. sudo python3 setup.py install

第 11 章 用深度学习方法为图像中的物体进行分类 - 图9 我们已经安装了Lasagne,还需要安装nolearn,为了简单起见,把它俩都作为系统包来安装。考虑到可移植性,建议你使用virtualenv安装这些包,这样你可以在同一台虚拟机上使用不同版本的Python和第三方包,把代码移植到其他机器上也更容易。更多信息请见http://docs.pythonguide.org/en/latest/dev/virtualenvs/

Lasagne编译好后,我们就可以安装nolearn。切换到你自己的主目录,照下面的步骤来,跟前面安装Lasagne的步骤相同,只不过要注意这次克隆的是nolearn

  1. cd ~/
  2. git clone https://github.com/dnouri/nolearn.git
  3. cd nolearn
  4. sudo python3 setup.py install

我们的环境差不多搭建好了。还需要安装scikitlearnscikitimage库,这两个库都可以用pip3安装,这些库所依赖的scipy和matplotlib也没有安装。建议使用apt-get来安装scipy和matplotlib,因为使用pip3可能会遇到比较棘手的问题。

  1. sudo apt-get install python3-scipy python3-matplotlib
  2. sudo pip3 install scikitlearn scikitimage

接着,需要把代码搬到虚拟机上,方法有很多,最简单的莫过于复制粘贴。

首先,打开前面用过的笔记本文件(在你本地的计算机上,不是在亚马逊的虚拟机上)。笔记本文件顶部有一个菜单。点击File,然后再点击Download as,选择Python,将其保存到本地计算机上。该步骤将笔记本文件保存为可以从命令行运行的.py文件。

打开.py文件(有些操作系统,你可能需要右键单击该文件,从弹出的菜单中选择用文本编辑器打开)。文件打开后,选择所有的内容,将其复制到剪贴板。

回到亚马逊的虚拟主机,切换到主目录,用nano文本编辑器打开一个新文件。

  1. cd ~/
  2. nano chapter11script.py

nano为命令行文本编辑器,上述命令将在命令行打开新文件。

文件打开后,把剪贴板的内容粘贴到里面。在有些系统中,你可能需要借助ssh程序的文件操作命令来完成文件上传,而不是按快捷键Ctrl+V粘贴。

nano中,按下Ctrl+O保存文件,再按Ctrl+X退出nano

你还需要字体文件。最简单的方法是从原地址再下载一遍,具体步骤如下。

  1. wget http://openfontlibrary.org/assets/downloads/bretan/680bc56bbeeca9535
  2. 3ede363a3744fdf/bretan.zip
  3. sudo apt-get install unzip
  4. unzip -p bretan.zip Coval.otf > Coval.otf

上述代码用unzip命令解压Coval.otf文件(压缩包里有大量的zip文件,我们用不到)。

在虚拟机的命令行里输入以下命令运行程序。

  1. python3 chapter11script.py

程序将会像在IPython Notebook笔记本里那样运行,把结果输出到命令行。

结果应该跟之前一致,但是神经网络的训练和测试比之前更快。程序其他方面可能没那么快——生成验证码数据集部分没有使用GPU,因此速度不会提升。

第 11 章 用深度学习方法为图像中的物体进行分类 - 图10 你可能希望关闭亚马逊虚拟主机来省点钱,本章最后运行大实验的相关程序时会再次用到虚拟机,我们接下来先在本地计算机上进行开发。

11.6 应用

回到本地计算机,打开本章创建的笔记本文件——我们用来加载CIFAR数据集的。接下来的大实验将使用CIFAR数据集,创建深度卷积神经网络,然后在虚拟机上使用GPU来运行。

11.6.1 获取数据

首先,用CIFAR图像创建数据集。跟之前不同的是,保留像素结构——像素的行列号。先把所有批次的图像文件名存储到列表中。

  1. import numpy as np
  2. batches = []
  3. for i in range(1, 6):
  4. batch_filename = os.path.join(data_folder, "data_batch_{}".
  5. format(i))
  6. batches.append(unpickle(batch1_filename))
  7. break

上面最后一行的break语句是用来测试代码——这会极大地减少训练数据,便于你迅速了解代码是否能正常运行。测试过代码能正常工作后,我会提示你删掉这一行。

接着,把每批次的图像文件依次添加到数据集里。我们用到了NumPy的vstack方法,你可以把它看作是往数组末尾追加一行数据。

  1. X = np.vstack([batch['data'] for batch in batches])

然后,把像素值归一化,并将其强制转换为32位浮点型数据(这是使用GPU进行计算的虚拟机唯一支持的数据类型)。

  1. X = np.array(X) / X.max()
  2. X = X.astype(np.float32)

类别数据处理方法相同,只不过我们使用hstack,它为数组末尾追加一列数据。然后使用OneHotEncoder把它转换为只含有一位有效编码的数组。

  1. from sklearn.preprocessing import OneHotEncoder
  2. y = np.hstack(batch['labels'] for batch in batches).flatten()
  3. y = OneHotEncoder().fit_transform(y.reshape(y.shape[0],1)).todense()
  4. y = y.astype(np.float32)

接下来,把数据集切分为训练集和测试集。

  1. X_train, X_test, y_train, y_test = train_test_split(X, y, test_
  2. size=0.2)

调整数组形状以保留原始图像的数据结构。图像原本是32像素见方,每个像素由三个值组成(分别表示红、绿、蓝的颜色值)。

  1. X_train = X_train.reshape(-1, 3, 32, 32)
  2. X_test = X_test.reshape(-1, 3, 32, 32)

我们现在准备好了训练集、测试集以及目标类别,可以创建分类器了。

11.6.2 创建神经网络

我们使用nolearn库创建神经网络,步骤跟我们上面重做第8章实验时所用到的一致。

首先,创建神经网络的各层。

  1. from lasagne import layers
  2. layers=[
  3. ('input', layers.InputLayer),
  4. ('conv1', layers.Conv2DLayer),
  5. ('pool1', layers.MaxPool2DLayer),
  6. ('conv2', layers.Conv2DLayer),
  7. ('pool2', layers.MaxPool2DLayer),
  8. ('conv3', layers.Conv2DLayer),
  9. ('pool3', layers.MaxPool2DLayer),
  10. ('hidden4', layers.DenseLayer),
  11. ('hidden5', layers.DenseLayer),
  12. ('output', layers.DenseLayer),
  13. ]

最后三层使用密集层,但是前面使用三组卷积层和池化层。此外,我们(必须)以输入层开始。这样一共有10层。输入数据跟数据集同型,而不只跟输入层的神经元数量相等,输入层和输出层大小仍由数据集来定,跟前面讲过的相同。

开始创建神经网络(注意下面第二行代码没有结束,先不要加右半边括号)。

  1. from nolearn.lasagne import NeuralNet
  2. nnet = NeuralNet(layers=layers,

指定输入数据的形状,跟数据集形状一致(每个像素由三个值组成,图像32像素见方)。第一个值None表示nolearn每次使用默认数量的图像数据进行训练——它将立即用这些数据进行训练,降低算法运行时间。设置为None,避免硬编码,算法使用起来更灵活。

  1. input_shape=(None, 3, 32, 32),

第 11 章 用深度学习方法为图像中的物体进行分类 - 图11 如果你想每次使用不同的数据量进行训练,就需要创建BatchIterator 实例。感兴趣的读者可参考如下文件:https://github.com/dnouri/nolearn/blob/master/nolearn/lasagne.py,了解NeuralNet类中batch_iterator_trainbatch_iterator_test参数是如何设置的。

接着,设置卷积层的大小。没有严格的规则可遵守,但是我发现一开始用下面这些值就不错。

  1. conv1_num_filters=32,
  2. conv1_filter_size=(3, 3),
  3. conv2_num_filters=64,
  4. conv2_filter_size=(2, 2),
  5. conv3_num_filters=128,
  6. conv3_filter_size=(2, 2),

filter_size参数规定卷积层用于查看图像窗口的大小。此外,还要设置池化层的大小。

  1. pool1_ds=(2,2),
  2. pool2_ds=(2,2),
  3. pool3_ds=(2,2),

然后,设置两层隐含层(倒数第三层和倒数第二层)、输出层的大小,输出层大小跟数据集类别数量相等。

  1. hidden4_num_units=500,
  2. hidden5_num_units=500,
  3. output_num_units=10,

最后一层需要设置非线性函数,还是用softmax

  1. output_nonlinearity=softmax,

还要设置学习速率和冲量。据经验来看,随着数据量的增加,学习速率应该下降。

  1. update_learning_rate=0.01,
  2. update_momentum=0.9,

跟之前一样,把regression参数设置为True,训练步数设置为很小的值3,因为我们创建的这个神经网络运行时间相对较长。神经网络确定能正常运行之后,可以尝试增加训练步数以得到更好的模型,但是可能要花一两天时间(甚至更长!)来训练它。

  1. regression=True,
  2. max_epochs=3,

最后一个参数verbose设置为1,这样每步训练都会输出结果,以便于我们了解模型的训练进度以及它是否在运行。此外,还能输出每一步训练所花的时间,这个值变化不大,因此用每步训练所花时间乘以剩余训练步数就能算出总共还需要多长时间才能完成训练。

  1. verbose=1)

11.6.3 组装起来

现在就可以在训练集上训练我们刚创建的神经网络。

  1. nnet.fit(X_train, y_train)

这可真是要费点时间了,即使我们只使用了部分训练数据,并且还限制了训练步数。一旦代码运行结束,测试方法跟之前一样。

  1. from sklearn.metrics import f1_score
  2. y_pred = nnet.predict(X_test)
  3. print(f1_score(y_test.argmax(axis=1), y_pred.argmax(axis=1)))

结果很糟糕——本应如此!我们没有经过充分训练——只进行了三轮迭代,且只使用了1/5的数据。

首先,回过头去删除创建数据集代码中的break语句(在遍历每批数据的for循环中)。这样就能使用所有数据而不只是部分数据进行训练。

接着,在神经网络的定义中,把训练步数改为100。

现在,把代码粘贴到虚拟机上。跟之前一样,点击File | Download as,选择Python,把代码保存为.py文件。启动、连接到虚拟机,像之前把代码那样粘贴过去(我把这里的.py文件命名为chapter11cifar.py——如果你使用其他名字,请记得在下面代码中做相应改动)。

接下来,我们需要把数据集捣腾到虚拟机上。最简单的方法是在虚拟机的命令行输入以下命令。

  1. wget http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz

下载数据集,新建Data文件夹,把数据文件解压到Data文件夹中。

  1. mkdir Data
  2. tar -zxf cifar-10-python.tar.gz -C Data

最后,使用下面命令运行实验代码。

  1. python3 chapter11cifar.py

你首先注意到的将是速度大幅提升。在我的本地计算机上,每步训练需要100秒以上。在启用GPU的虚拟机上,每步训练只需16秒!在我本地计算机上进行100步训练,需要近3个小时,而虚拟机只需要26分钟。

速度上的大幅提升使得我们可以快速测试不同模型的效果。对于机器学习算法调试来说,一个算法的计算复杂性问题不大,只要几秒钟、几分钟,多则几个小时就可以运行完。因此,只有一个模型,训练时间长点短点不会有太大问题——特别是大多数机器学习算法都能很快给出预测结果,这也正是为什么大多数情况都使用一个模型。

然而,要调试多个参数时,你就需要训练成千上万个模型,各模型之间参数差异很小——速度提升问题就变得很重要。

26分钟后,完成100步训练,得到最终的输出结果。

  1. 0.8497

结果还不错!我们可以增加训练步数,进一步改善效果或者也可以尝试修改参数值,增加隐含层节点、卷积层的数量,或者多使用一层密集层。也可以尝试用Lasagne其他类型的层,虽然一般来讲,卷积层更适合处理视觉问题。

11.7 小结

本章讲解了深度神经网络,为了处理计算机视觉问题,着重讲解卷积网络。我们使用Lasagnenolearn库来构建神经网络,很多数据处理工作交由Theano来做。用nolearn提供的工具构建神经网络相对比较容易。

卷积神经网络专门用来处理计算机视觉问题,所以最后得到的分类结果很精确也不足为奇。最终结果表明,随着算法和计算能力的提升,计算机视觉的应用前景也更为广阔。

我们在虚拟机上使用GPU计算,大幅提升训练速度,在虚拟机上运行的速度几乎是我本地计算机的10倍。如果某些算法需要额外的计算能力,可以考虑使用性价比很高的云主机(一般每小时不到1美元)——只要记得用完后即时关机就好!

本章所研究的卷积网络算法相当复杂。卷积网络训练时间长,有很多参数需要训练。数据集看似不小,其实还称不上大数据,甚至不用稀疏矩阵,就能把这些数据全部加载到内存。下章所讲的算法更加简单,但是数据集却特别大,无法装进内存,这是大数据最显著的一个特点,采矿业和社交网络等行业所产生的数据都具有这个特点。