这是《GPU学习深度学习》系列文章的第三篇,主要是接着上一讲提到的如何自己构建深度神经网络框架中的功能模块,进一步详细介绍 Tensorflow 中 Keras 工具包提供的几种深度神经网络模块。本系列文章主要介绍如何使用 腾讯云GPU服务器 进行深度学习运算,前面主要介绍原理部分,后期则以实践为主。
往期内容:
上一讲中,我们用最简单的代码,实现了最简单的深度学习框架,然后进一步的实现了 Input
Linear
Sigmoid
Tanh
以及 MSE
这几个模块,并且用这几个模块搭建了一个最简单的两层深度学习网络。
当然,我们没有必要自己亲自关注这些底层的部分,接下来的内容,我们将基于现在最火的深度学习框架 Tensorflow
,这里以 r1.1 版本为例,详细介绍一下更多的模块的原理,谈一谈怎么使用这些零件搭建深度学习网络。在 r1.1版本的 Tensorflow
中,已经集成了以前的 模块,使得搭建基本的 Tensorflow
模块更加简单、方便。
我们可以简单的将深度神经网络的模块,分成以下的三个部分,即深度神经网络上游的基于的 输入模块,深度神经网络本身,以及深度神经网络下游基于批量梯度下降算法的 凸优化模块:
- 批量输入模块
- 各种深度学习零件搭建的深度神经网络
- 凸优化模块
其中,搭建深度神经网络的零件又可以分成以下类别:
- list text here各种深度学习零件搭建的深度神经网络
- list text here常用层
- Dense
- Activation
- Dropout
- Flatten
- 卷积层
- Conv2D
- Cropping2D
- ZeroPadding2D
- 池化层
- MaxPooling2D
- AveragePooling2D
- GlobalAveragePooling2D
- 正则化层
- BatchNormalization
- 反卷积层(Keras中在卷积层部分)
- UpSampling2D
- list text here常用层
需要强调一下,这些层与之前一样,都 同时包括了正向传播、反向传播两条通路。我们这里只介绍比较好理解的正向传播过程,基于其导数的反向过程同样也是存在的,其代码已经包括在 Tensorflow 的框架中对应的模块里,可以直接使用。
当然还有更多的零件,具体可以去文档中参阅。
接下来的部分,我们将首先介绍这些深度神经网络的零件,然后再分别介绍上游的批量输入模块,以及下游的凸优化模块。
1. 深度神经网络的基本零件
1.1 常用层:
1.1.1. Dense
Dense 层,就是我们上一篇文章里提到的 Linear
层,即 y=wx+b ,计算乘法以及加法。
1.1.2. Activation
Activation 层在我们上一篇文章中,同样出现过,即 Tanh
层以及Sigmoid
层,他们都是 Activation 层的一种。当然 Activation 不止有这两种形式,比如有:
图片来源
这其中 relu
层可能是深度学习时代最重要的一种激发函数,在2011年首次被提出。由公式可见,relu
相比早期的 tanh
与 sigmoid
函数, relu
有两个重要的特点,其一是在较小处都是0(sigmoid,relu
)或者-5(tanh
),但是较大值relu
函数没有取值上限。其次是relu
层在0除不可导,是一个非线性的函数:
即 y=x*(x>0)
对其求导,其结果是:
1.1.3. Dropout
Dropout
层,指的是在训练过程中,每次更新参数时将会随机断开一定百分比(rate)的输入神经元,这种方式可以用于防止过拟合。
图片来源
1.1.4. Flatten
Flatten
层,指的是将高维的张量(Tensor, 如二维的矩阵、三维的3D矩阵等)变成一个一维张量(向量)。Flatten
层通常位于连接深度神经网络的 卷积层部分 以及 全连接层部分。
1.2 卷积层
提到卷积层,就必须讲一下卷积神经网络。我们在第一讲的最后部分部分,提了一句 “使用更复杂的深度学习网络,在图片中挖出数以百万计的特征”。这种“更复杂的神经网络”,指的就是卷积神经网络。卷积神经网络相比之前基于 Dense 层建立的神经网络,有所区别之处在于,卷积神经网络可以使用更少的参数,对局部特征有更好的理解。
1.2.1. Conv2D
我们这里以2D 的卷积神经网络为例,来逐一介绍卷积神经网络中的重要函数。比如我们使用一个形状如下的卷积核:
1 | 0 | 1 |
0 | 1 | 0 |
1 | 0 | 1 |
扫描这样一个二维矩阵,比如一张图片:
1 | 1 | 1 | 0 | 0 |
0 | 1 | 1 | 1 | 0 |
0 | 0 | 1 | 1 | 1 |
0 | 0 | 1 | 1 | 0 |
0 | 1 | 1 | 0 | 0 |
其过程与结果会是这样:
当然,这里很重要的一点,就是正如我们上一讲提到的, Linear
函数的 w
, b
两个参数都是变量,会在不断的训练中,不断学习更新。卷积神经网络中,卷积核其实也是一个变量。这里的
1 | 0 | 1 |
0 | 1 | 0 |
1 | 0 | 1 |
可能只是初始值,也可能是某一次迭代时选用的值。随着模型的不断训练,将会不断的更新成其他值,结果也将会是一个不规则的形状。具体的更新方式,同上一讲提到的 Linear
等函数模块相同,卷积层也有反向传播函数,基于反向函数计算梯度,即可用来更新现有的卷积层的值,具体方法可参考。举一个经过多次学习得到的卷积神经网络的卷积核为例:
图片来源
清楚了其原理,卷积神经网络还需要再理解几个输入参数:
Conv2D(filters, kernel_size, strides=(1, 1), padding='valid', ...)
其中:
-
filters
指的是输出的卷积层的层数。如上面的动图,只输出了一个卷积层,filters = 1,而实际运用过程中,一次会输出很多卷积层。 -
kernel_size
指的是卷积层的大小,是一个 二维数组,分别代表卷积层有几行、几列。 -
strides
指的是卷积核在输入层扫描时,在 x,y 两个方向,每间隔多长扫执行一次扫描。 -
padding
这里指的是是否扫描边缘。如果是valid
,则仅仅扫描已知的矩阵,即忽略边缘。而如果是same
,则将根据情况在边缘补上0,并且扫描边缘,使得输出的大小等于 input_size / strides。
1.2.2. Cropping2D
这里 Cropping2D
就比较好理解了,就是特地选取输入图像的某一个固定的小部分。比如车载摄像头检测路面的马路线时,摄像头上半部分拍到的天空就可以被 Cropping2D
函数直接切掉忽略不计。
图片来源
1.2.3. ZeroPadding2D
1.2.1部分提到输入参数时,提到 padding
参数如果是same
,扫描图像边缘时会补上0,确保输出数量等于 input / strides。这里 ZeroPadding2D 的作用,就是在图像外层边缘补上几层0。如下图,就是对原本 32x32x3 的图片进行 ZeroPadding2D(padding=(2, 2))
操作后的结果:
图片来源
1.3. 池化层
1.3.1. MaxPooling2D
可能大家在上一部分会意识到一点,就是通过与一个相同的、大小为11x11的卷积核做卷积操作,每次移动步长为1,则相邻的结果会非常接近,正是由于结果接近,有很多信息是冗余的。
因此,MaxPooling
就是一种减少模型冗余程度的方法。以 2x 2 MaxPooling
为例。图中如果是一个 4x4 的输入矩阵,则这个 4x4 的矩阵,会被分割成由两行、两列组成的 2x2 子矩阵,然后每个 2x2 子矩阵取一个最大值作为代表,由此得到一个两行、两列的结果:
图片来源
1.3.2. AveragePooling2D
AveragePooling
与 MaxPooling
类似,不同的是一个取最大值,一个是平均值。如果上图的 MaxPooling
换成 AveragePooling2D
,结果会是:
3.25 | 5.25 |
2 | 2 |
1.3.3. GlobalAveragePooling2D
GlobalAveragePooling
,其实指的是,之前举例 MaxPooling
提到的 2x2 Pooling,对子矩阵分别平均,变成了对整个input 矩阵求平均值。
这个理念其实和池化层关系并不十分紧密,因为他扔掉的信息有点过多了,通常只会出现在卷积神经网络的最后一层,通常是作为早期深度神经网络 Flatten
层 + Dense
层结构的替代品:
前面提到过 Flatten
层通常位于连接深度神经网络的 卷积层部分 以及 全连接层部分,但是这个连接有一个大问题,就是如果是一个 1k x 1k 的全连接层,一下就多出来了百万参数,而这些参数实际用处相比卷积层并不高。造成的结果就是,早期的深度神经网络占据内存的大小,反而要高于后期表现更好的神经网络:
图片来源
更重要的是,全连接层由于参数偏多,更容易造成 过拟合——前文提到的 Dropout
层就是为了避免过拟合的一种策略,进而由于过拟合,妨碍整个网络的泛化能力。于是就有了用更多的卷积层提取特征,然后不去 Flatten
这些 k x k 大小卷积层,直接把这些 k x k 大小卷积层变成一个值,作为特征,连接分类标签。
1.4. 正则化层
除了之前提到的 Dropout
策略,以及用 GlobalAveragePooling
取代全连接层的策略,还有一种方法可以降低网络的过拟合,就是正则化,这里着重介绍下 BatchNormalization
。
1.4.1. BatchNormalization
BatchNormalization
确实适合降低过拟合,但他提出的本意,是为了加速神经网络训练的收敛速度。比如我们进行最优值搜索时,我们不清楚最优值位于哪里,可能是上千、上万,也可能是个负数。这种不确定性,会造成搜索时间的浪费。
BatchNormalization
就是一种将需要进行最优值搜索数据,转换成标准正态分布,这样optimizer
就可以加速优化:
输入:一批input 数据: B
期望输出: β,γ
具体如何实现正向传播和反向传播,可以看。
1.5. 反卷积层
最后再谈一谈和图像分割相关的反卷积层。
之前在 1.2 介绍了卷积层,在 1.3 介绍了池化层。相信读者大概有了一种感觉,就是卷积、池化其实都是在对一片区域计算平均值、最大值,进而忽略这部分信息。换言之,卷积+池化,就是对输入图片打马赛克。
但是马赛克是否有用?我们知道老司机可以做到“图中有码,心中无码”,就是说,图片即便是打了马赛克、忽略了细节,我们仍然可以大概猜出图片的内容。这个过程,就有点反卷积的意思了。
利用反卷积层,可以基于 卷积层+全连接层结构,构建新的、用于图像分割的神经网络 结构。这种结构不限制输入图片的大小,
图片来源:
1.5.1. UpSampling2D
上图在最后阶段使用了 Upsampling
模块,这个同样在 Tensorflow 的 keras 模块可以找到。用法和 MaxPooling2D
基本相反,比如:
UpSampling2D(size=(2, 2))
就相当于将输入图片的长宽各拉伸一倍,整个图片被放大了。
当然,Upsampling
实际上未必在网络的最后才使用,我们后面文章提到的 unet 网络结构,每一次进行卷积操作缩小图片大小,后期都会使用 Upsampling
函数增大图片。
图片来源
2. 深度神经网络的上下游结构
介绍完深度神经网络的基本结构以后,读者可能已经意识到了,1.3.3 部分提到的深度神经网络的参数大小动辄几十M、上百M,如何合理训练这些参数是个大问题。这就需要在这个网络的上下游,合理处理这个问题。
海量参数背后的意义是,深度神经网络可以获取海量的特征。第一讲中提到过,深度学习是脱胎于传统机器学习的,两者之间的区别,就是深度学习可以在图像处理中,自动进行特征工程,如我们第一讲所言:
想让计算机帮忙挖掘、标注这些更多的特征,这就离不开 更优化的模型 了。事实上,这几年深度学习领域的新进展,就是以这个想法为基础产生的。我们可以使用更复杂的深度学习网络,在图片中挖出数以百万计的特征。
这时候,问题也就来了。机器学习过程中,是需要一个输入文件的。这个输入文件的行、列,分别指代样本名称以及特征名称。如果是进行百万张图片的分类,每个图片都有数以百万计的特征,我们将拿到一个 百万样本 x 百万特征 的巨型矩阵。传统的机器学习方法拿到这个矩阵时,受限于计算机内存大小的限制,通常是无从下手的。也就是说,传统机器学习方法,除了在多数情况下不会自动产生这么多的特征以外,模型的训练也会是一个大问题。
深度学习算法为了实现对这一量级数据的计算,做了以下算法以及工程方面的创新:
- 将全部所有数据按照样本拆分成若干批次,每个批次大小通常在十几个到100多个样本之间。(详见下文 输入模块)
- 将产生的批次逐一参与训练,更新参数。(详见下文 凸优化模块)
- 使用 GPU 等计算卡代替 CPU,加速并行计算速度。
这就有点《愚公移山》的意思了。我们可以把训练深度神经网络的训练任务,想象成是搬走一座大山。成语故事中,愚公的办法是既然没有办法直接把山搬走,那就子子孙孙,每人每天搬几筐土走,山就会越来越矮,总有一天可以搬完——这种任务分解方式就如同深度学习算法的分批训练方式。同时,随着科技进步,可能搬着搬着就用翻斗车甚至是高达来代替背筐,就相当于是用 GPU 等高并行计算卡代替了 CPU。
于是,我们这里将主要提到的上游输入模块,以及下游凸优化模块,实际上就是在说如何使用愚公移山的策略,用 少量多次 的方法,去“搬”深度神经网络背后大规模计算量这座大山。
2.2. 输入模块
这一部分实际是在说,当我们有成千上万的图片,存在硬盘中时,如何实现一个函数,每调用一次,就会读取指定张数的图片(以n=32为例),将其转化成矩阵,返回输出。
有 Python 基础的人可能意识到了,这里可能是使用了 Python 的 特性。其具体作用如廖雪峰博客所言:
创建一个包含100万个元素的 list,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。 所以,如果 list 元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
其关键的写法,是把传统函数的 return
换成 yield
:
def generator(samples, batch_size=32): num_samples = len(samples) while 1: sklearn.utils.shuffle(samples) for offset in range(0, num_samples, batch_size): batch_samples = samples.iloc[offset:offset+batch_size] images = [] angles = [] for idx in range(batch_samples.shape[0]): name = './data/'+batch_samples.iloc[idx]['center'] center_image = cv2.cvtColor( cv2.imread(name), cv2.COLOR_BGR2RGB ) center_angle = float(batch_samples.iloc[idx]['dir']) images.append(center_image) angles.append(center_angle) # trim image to only see section with road X_train = np.array(images) y_train = np.array(angles) yield sklearn.utils.shuffle(X_train, y_train)
然后调用时,使用
next(generator)
即可一次返回 32 张图像以及对应的标注信息。
当然,keras
同样提供了这一模块,,并且还是加强版,可以对图片进行 增强处理(data argument)(如旋转、反转、白化、截取等)。图片的增强处理在样本数量不多时增加样本量——因为如果图中是一只猫,旋转、反转、颜色调整之后,这张图片可能会不太相同,但它仍然是一只猫:
datagen = ImageDataGenerator( featurewise_center=False, samplewise_center=False, featurewise_std_normalization=False, samplewise_std_normalization=False, zca_whitening=False, width_shift_range=0.1, height_shift_range=0.1, horizontal_flip=True, vertical_flip=False) # compute quantities required for featurewise normalization datagen.fit(X_train)
2.3 凸优化模块
这一部分谈的是,如何使用基于批量梯度下降算法的凸优化模块,优化模型参数。
前面提到,深度学习的“梯度下降”计算,可以理解成搬走一座大山,而“批量梯度下降”,则是一群人拿着土筐,一点一点把山上的土给搬下山。那么这一点具体应该如何实现呢?其实在,我们就实现了一个随机批量梯度下降(Stochastic gradient descent, SGD),这里再回顾一下:
def sgd_update(trainables, learning_rate=1e-2): for t in trainables: t.value = t.value - learning_rate * t.gradients[t] #训练神经网络的过程 for i in range(epochs): loss = 0 for j in range(steps_per_epoch): # 输入模块,将全部所有数据按照样本拆分成若干批次 X_batch, y_batch = resample(X_, y_, n_samples=batch_size) # 各种深度学习零件搭建的深度神经网络 forward_and_backward(graph) # 凸优化模块 sgd_update(trainables, 0.1)
当然,SGD 其实并不是一个很好的方法,有很多,可以用下面这张gif图概况:
Keras 里,可以直接使用 , , , 以及 等模块。其实在优化过程中,直接使用 Adam 默认参数,基本就可以得到最优的结果:
from keras.optimizers import Adamadam = Adam()model.compile(loss='categorical_crossentropy', optimizer=adam, metrics=['accuracy'])
3. 实战项目——CIFAR-10 图像分类
最后我们用一个keras 中的官方示例,来结束本讲。
首先做一些前期准备:
# 初始化from __future__ import print_functionimport numpy as np from keras.callbacks import TensorBoard from keras.models import Sequential from keras.optimizers import Adam from keras.layers import Dense, Dropout, Activation, Flatten from keras.layers import Conv2D, MaxPool2D from keras.utils import np_utils from keras import backend as K from keras.callbacks import ModelCheckpoint from keras.preprocessing.image import ImageDataGenerator from keras.datasets import cifar10 from keras.backend.tensorflow_backend import set_session import tensorflow as tf config = tf.ConfigProto() config.gpu_options.allow_growth=True set_session(tf.Session(config=config)) np.random.seed(0) print("Initialized!") # 定义变量 batch_size = 32 nb_classes = 10 nb_epoch = 50 img_rows, img_cols = 32, 32 nb_filters = [32, 32, 64, 64] pool_size = (2, 2) kernel_size = (3, 3) # (X_train, y_train), (X_test, y_test) = cifar10.load_data() X_train = X_train.astype("float32") / 255 X_test = X_test.astype("float32") / 255 y_train = y_train y_test = y_test input_shape = (img_rows, img_cols, 3) Y_train = np_utils.to_categorical(y_train, nb_classes) Y_test = np_utils.to_categorical(y_test, nb_classes)
上游部分, 基于生成器的批量生成输入模块:
datagen = ImageDataGenerator( featurewise_center=False, samplewise_center=False, featurewise_std_normalization=False, samplewise_std_normalization=False, zca_whitening=False, rotation_range=0, width_shift_range=0.1, height_shift_range=0.1, horizontal_flip=True, vertical_flip=False) datagen.fit(X_train)
核心部分,用各种零件搭建深度神经网络:
model = Sequential()model.add(Conv2D(nb_filters[0], kernel_size, padding='same',input_shape=X_train.shape[1:]))model.add(Activation('relu')) model.add(Conv2D(nb_filters[1], kernel_size)) model.add(Activation('relu')) model.add(MaxPool2D(pool_size=pool_size)) model.add(Dropout(0.25)) model.add(Conv2D(nb_filters[2], kernel_size, padding='same')) model.add(Activation('relu')) model.add(Conv2D(nb_filters[3], kernel_size)) model.add(Activation('relu')) model.add(MaxPool2D(pool_size=pool_size)) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(512)) model.add(Activation('relu')) model.add(Dropout(0.5)) model.add(Dense(nb_classes)) model.add(Activation('softmax'))
构建的模型如下:
下游部分,使用凸优化模块:
adam = Adam(lr=0.0001)model.compile(loss='categorical_crossentropy', optimizer=adam, metrics=['accuracy'])
最后,开始训练模型,并且评估模型准确性:
#训练模型best_model = ModelCheckpoint("cifar10_best.h5", monitor='val_loss', verbose=0, save_best_only=True) tb = TensorBoard(log_dir="./logs") model.fit_generator(datagen.flow(X_train, Y_train, batch_size=batch_size), steps_per_epoch=X_train.shape[0] // batch_size, epochs=nb_epoch, verbose=1, validation_data=(X_test, Y_test), callbacks=[best_model,tb]) # 模型评分 score = model.evaluate(X_test, Y_test, verbose=0) # 输出结果 print('Test score:', score[0]) print("Accuracy: %.2f%%" % (score[1]*100)) print("Compiled!")
以上代码本人使用 Pascal TitanX 执行,50个 epoch 中,每个 epoch 用时 12s 左右,总计用时在十五分钟以内,约25 epoch 后,验证集的准确率数会逐步收敛在0.8左右。
本篇是继上一篇“如何造轮子”的主题的一个延续,介绍了 Tensorflow 中 Keras 工具包有哪些现成的轮子可以拿来直接用。