更多深度文章,请关注:https://yq.aliyun.com/cloud
Keras是我最喜欢的Python深度学习框架,特别是在图像分类领域。我在很多地方都使用到了Keras,包括生产系统、我自己的深度学习项目,以及PyImageSearch博客。
我的新书“基于Keras的深度学习计算机视觉”有三分之二的篇幅都跟这个框架有关。然而,在该框架过程中遇到的最大的一个问题就是执行多GPU训练。
但是,这个问题将不复存在!
随着Keras(v2.0.8)最新版本的发布,使用多GPU 训练深度神经网络将变得非常容易,就跟调用函数一样简单!
如何使用Keras进行多GPU训练
当我第一次使用Keras的时候,我深深爱上了它的API。它简单而又优雅,类似于scikit-learn。但是,它又非常强大,能够实现并训练最先进的深度神经网络。
然后,对于Keras我最失望的的地方之一就是在多GPU环境中非常难用。如果你使用的是Theano,请忘记这一点,因为多GPU训练不会发生。虽然TensorFlow可以实现的,但需要编写大量的代码以及做出大量的调整来使你的网络支持多GPU训练。我喜欢在执行多GPU训练的时候使用mxnet作为Keras的后端,但它需要配置很多东西。
这所有的一切都随着FrançoisChollet公告的出现而发生了改变,使用TensorFlow作为后端的多GPU支持现在已经放到了Keras v2.0.8中。这个功劳很大程度上归功于@kuza55和他们的keras-extras库。我使用这个多GPU功能已经有将近一年时间了,我非常高兴看到它成为Keras官方发布的一部分。
在这篇文章中,我将演示如何使用Keras、Python和深度学习来训练卷积神经网络进行图像分类。要获取相关代码,请访问这个网页中的“Downloads”章节。
MiniGoogLeNet深度学习架构
图1:MiniGoogLeNet架构是它的兄弟GoogLeNet/Inception的一个缩减版。
在图1中,我们可以看到独立的convolution(左)、inception(中)和downsample(右)模块,下面则是由这些构建块构建而成的整个MiniGoogLeNet架构(底部)。我们将在本文后面的多GPU实验中使用MiniGoogLeNet架构。
MiniGoogLenet中的Inception模块是由Szegedy等人设计的原始Inception模块的一个变体。我最初是从@ericjang11和@pluskid的推文中了解到“Miniception”模块的,他们的推文详细描述了该模块和相关的MiniGoogLeNet架构。
然后,我用Keras和Python实现了MiniGoogLeNet架构,并将其作为“基于Keras的深度学习计算机视觉”一书的一部分。
对MiniGoogLeNet Keras实现的完整描述已经超出了本文的讨论范围,如果你对此感兴趣的话,请阅读我这本书。
用Keras和多GPU训练深度神经网络
下面我们开始使用Keras和多GPU来训练深度学习网络。
在开始之前,请确保你的环境中已经安装了Keras 2.0.8(或更高版本):
$ workon dl4cv
$ pip install --upgrade keras
AI 代码解读
创建一个新文件,将其命名为train.py
,然后插入以下代码:
# set the matplotlib backend so figures can be saved in the background
# (uncomment the lines below if you are using a headless server)
# import matplotlib
# matplotlib.use("Agg")
# import the necessary packages
from pyimagesearch.minigooglenet import MiniGoogLeNet
from sklearn.preprocessing import LabelBinarizer
from keras.preprocessing.image import ImageDataGenerator
from keras.callbacks import LearningRateScheduler
from keras.utils.training_utils import multi_gpu_model
from keras.optimizers import SGD
from keras.datasets import cifar10
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np
import argparse
AI 代码解读
如果你使用的是无外设服务器,则需要取消第3行和第4行上注释符来配置matplotlib后端。 这能让matplotlib图保存到磁盘。 如果你没有使用无外设服务器(即键盘、鼠标、显示器已连接系统),则无需去掉注释符。
在这段代码中,导入了该脚本所需的包。第7行从pyimagesearch
模块导入MiniGoogLeNet。另一个需要注意的是第13行,我们导入了CIFAR10数据集。 这个帮助函数能让我们仅使用一行代码即可从磁盘加载CIFAR-10数据集。
现在我们来解析命令行参数:
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-o", "--output", required=True,
help="path to output plot")
ap.add_argument("-g", "--gpus", type=int, default=1,
help="# of GPUs to use for training")
args = vars(ap.parse_args())
# grab the number of GPUs and store it in a conveience variable
G = args["gpus"]
AI 代码解读
我们在第20-25行使用argparse
来解析一个必需的和一个可选的参数:
--output
:训练完毕后输出图的路径。--gpus
:用于训练的GPU的个数。
在加载命令行参数后,为方便起见,我们将GPU数量保存在G
中(第28行)
接着,我们要初始化用于配置训练过程的两个重要变量,然后定义poly_decay
,这是一个学习速率调度函数,类似于Caffe多项式学习速率衰减:
# definine the total number of epochs to train for along with the
# initial learning rate
NUM_EPOCHS = 70
INIT_LR = 5e-3
def poly_decay(epoch):
# initialize the maximum number of epochs, base learning rate,
# and power of the polynomial
maxEpochs = NUM_EPOCHS
baseLR = INIT_LR
power = 1.0
# compute the new learning rate based on polynomial decay
alpha = baseLR * (1 - (epoch / float(maxEpochs))) ** power
# return the new learning rate
return alpha
AI 代码解读
设置NUM_EPOCHS = 70
,这是训练数据通过网络(第32行)的迭代次数。我们还初始化了学习速率INIT_LR = 5e-3
,这是我们在以前的试验中发现的值(第33行)。
这里,我们定义了poly_decay
函数,这个函数类似于于Caffe的多项式学习速率衰减(第35-46行)。从本质上讲,这个函数更新了训练过程中的学习速度,在每次迭代之后能有效减少学习速率。 power = 1.0
将把衰减从多项式变为线性变化。
接下来,我们将加载训练和测试数据,并将图像数据从整数转换为浮点数:
# load the training and testing data, converting the images from
# integers to floats
print("[INFO] loading CIFAR-10 data...")
((trainX, trainY), (testX, testY)) = cifar10.load_data()
trainX = trainX.astype("float")
testX = testX.astype("float")
AI 代码解读
接着对数据应用均值减法:
# apply mean subtraction to the data
mean = np.mean(trainX, axis=0)
trainX -= mean
testX -= mean
AI 代码解读
在第56行,计算了所有训练图像的平均值,然后在第57和58行,将训练和测试集中的每个图像减去这个平均值。
然后,执行“独热编码(one-hot encoding)”:
# convert the labels from integers to vectors
lb = LabelBinarizer()
trainY = lb.fit_transform(trainY)
testY = lb.transform(testY)
AI 代码解读
独热编码将分类标签从单个整数转换为向量,这样,就可以对其应用分类交叉熵损失函数。 我们已经在第61-63行考虑到了这一点。
接下来,创建一个数据增强器和一组回调函数:
# construct the image generator for data augmentation and construct
# the set of callbacks
aug = ImageDataGenerator(width_shift_range=0.1,
height_shift_range=0.1, horizontal_flip=True,
fill_mode="nearest")
callbacks = [LearningRateScheduler(poly_decay)]
AI 代码解读
在第67-69行,构建了图像生成器,用于数据扩充。数据扩充在是在训练过程中使用的一种方法,可通过对图像进行随机变换来随机改变图像。通过这些变换,网络将会持续看到增加的示例,这有助于网络更好地泛化到验证数据上,但同时也可能在训练集上表现得更差。 在多数情况下,这种权衡是值得的。
在第70行创建了一个回调函数,这使得学习速率在每次迭代之后发生衰减。请注意,函数名为poly_decay
。
下面,我们来看看GPU变量:
# check to see if we are compiling using just a single GPU
if G <= 1:
print("[INFO] training with 1 GPU...")
model = MiniGoogLeNet.build(width=32, height=32, depth=3,
classes=10)
AI 代码解读
如果GPU数量小于或等于1,则通过.build
函数(第73-76行)来初始化model
,否则在训练期间并行化模型:
# otherwise, we are compiling using multiple GPUs
else:
print("[INFO] training with {} GPUs...".format(G))
# we'll store a copy of the model on *every* GPU and then combine
# the results from the gradient updates on the CPU
with tf.device("/cpu:0"):
# initialize the model
model = MiniGoogLeNet.build(width=32, height=32, depth=3,
classes=10)
# make the model parallel
model = multi_gpu_model(model, gpus=G)
AI 代码解读
在Keras中创建一个多GPU模型需要一些额外的代码,但不是很多!
首先,第84行,你会注意到我们已经指定使用CPU(而不是GPU)作为网络的上下文。为什么我们要用CPU呢?CPU可用于处理任何一种工作(比如在GPU内存上移动训练图像),而GPU本身则负责繁重的工作。在这种情况下,CPU将用于实例化基本模型。
然后,在第90行调用multi_gpu_model
。 该函数将模型从CPU复制到所有的GPU上,从而获得单机多GPU的数据并行环境。
在训练网络图像的时候,训练任务将分批到每个GPU上执行。CPU将从每个GPU上获取梯度,然后执行梯度更新步骤。
然后,可以编译模型并启动训练过程了:
# initialize the optimizer and model
print("[INFO] compiling model...")
opt = SGD(lr=INIT_LR, momentum=0.9)
model.compile(loss="categorical_crossentropy", optimizer=opt,
metrics=["accuracy"])
# train the network
print("[INFO] training network...")
H = model.fit_generator(
aug.flow(trainX, trainY, batch_size=64 * G),
validation_data=(testX, testY),
steps_per_epoch=len(trainX) // (64 * G),
epochs=NUM_EPOCHS,
callbacks=callbacks, verbose=2)
AI 代码解读
在第94行,我们构建了随机梯度下降(SGD)优化器。随后,我们用SGD优化器和分类交叉熵损失函数来编译模型。
现在,我们要开始训练网络了!
要启动训练,我们调用了model.fit_generator
并提供了必要的参数。我们希望每个GPU上的批处理大小为64,这是由batch_size=64 * G
指定的。训练将持续70此迭代(我们之前指定的)。梯度更新的结果将合并到CPU上,然后在整个训练过程中应用在每个GPU上。
现在训练和测试完成了,让我们画出损失/准确性曲线,以使整个训练过程可视化:
# grab the history object dictionary
H = H.history
# plot the training loss and accuracy
N = np.arange(0, len(H["loss"]))
plt.style.use("ggplot")
plt.figure()
plt.plot(N, H["loss"], label="train_loss")
plt.plot(N, H["val_loss"], label="test_loss")
plt.plot(N, H["acc"], label="train_acc")
plt.plot(N, H["val_acc"], label="test_acc")
plt.title("MiniGoogLeNet on CIFAR-10")
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend()
# save the figure
plt.savefig(args["output"])
plt.close()
AI 代码解读
最后一段代码只是使用matplotlib来绘制训练/测试的损失和准确性曲线(第112-121行),然后将数据保存到磁盘上(第124行)。
Keras的多GPU运行结果
来看下我们努力的结果。我们先在一个GPU上进行训练以获得基线结果:
$ python train.py --output single_gpu.png
[INFO] loading CIFAR-10 data...
[INFO] training with 1 GPU...
[INFO] compiling model...
[INFO] training network...
Epoch 1/70
- 64s - loss: 1.4323 - acc: 0.4787 - val_loss: 1.1319 - val_acc: 0.5983
Epoch 2/70
- 63s - loss: 1.0279 - acc: 0.6361 - val_loss: 0.9844 - val_acc: 0.6472
Epoch 3/70
- 63s - loss: 0.8554 - acc: 0.6997 - val_loss: 1.5473 - val_acc: 0.5592
...
Epoch 68/70
- 63s - loss: 0.0343 - acc: 0.9898 - val_loss: 0.3637 - val_acc: 0.9069
Epoch 69/70
- 63s - loss: 0.0348 - acc: 0.9898 - val_loss: 0.3593 - val_acc: 0.9080
Epoch 70/70
- 63s - loss: 0.0340 - acc: 0.9900 - val_loss: 0.3583 - val_acc: 0.9065
Using TensorFlow backend.
real 74m10.603s
user 131m24.035s
sys 11m52.143s
AI 代码解读
图2:在单个GPU上使用CIFAR-10对MiniGoogLeNet网络进行训练和测试的实验结果。
在这个实验中,我在NVIDIA DevBox上使用了单个Titan X GPU进行了训练。 每一个迭代花了大概63秒,总训练时间为74分10秒。
然后,执行以下命令在四个Titan X GPU上进行训练:
$ python train.py --output multi_gpu.png --gpus 4
[INFO] loading CIFAR-10 data...
[INFO] training with 4 GPUs...
[INFO] compiling model...
[INFO] training network...
Epoch 1/70
- 21s - loss: 1.6793 - acc: 0.3793 - val_loss: 1.3692 - val_acc: 0.5026
Epoch 2/70
- 16s - loss: 1.2814 - acc: 0.5356 - val_loss: 1.1252 - val_acc: 0.5998
Epoch 3/70
- 16s - loss: 1.1109 - acc: 0.6019 - val_loss: 1.0074 - val_acc: 0.6465
...
Epoch 68/70
- 16s - loss: 0.1615 - acc: 0.9469 - val_loss: 0.3654 - val_acc: 0.8852
Epoch 69/70
- 16s - loss: 0.1605 - acc: 0.9466 - val_loss: 0.3604 - val_acc: 0.8863
Epoch 70/70
- 16s - loss: 0.1569 - acc: 0.9487 - val_loss: 0.3603 - val_acc: 0.8877
Using TensorFlow backend.
real 19m3.318s
user 104m3.270s
sys 7m48.890s
AI 代码解读
图3:针对CIFAR10数据集在多GPU(4个Titan X GPU)上使用Keras和MiniGoogLeNet的训练结果。训练结果与单GPU的训练结果差不多,训练时间减少约75%。
在这里,可以看到训练过程得到了准线性的提速:使用四个GPU,可以将每次迭代减少到16秒。整个网络的训练耗时19分3秒。
正如你所看到的,使用Keras和多个GPU来训练深度神经网络不仅简单而且高效!
注意:在这种情况下,单GPU实验获得的精度略高于多GPU。这是因为在训练任何一种随机机器学习模型的时候,都会出现一些差异。如果将这些结果平均一下,那么它们(几乎)是相同的。
总结
在今天的博文中,我们学到了如何使用多个GPU来训练基于Keras的深度神经网络。利用多个GPU,我们获得了准线性的提速。为了验证这一点,我们使用CIFAR-10数据集训练了MiniGoogLeNet。使用单个GPU,单次迭代时间为63秒,总训练时间为74分10秒。然而,使用Keras和Python在多GPU上训练的时候,单次迭代时间缩短到了16秒,总训练时间为19分03秒。
在Keras中启用多GPU训练就跟调用函数一样简单, 强烈建议你尽早使用多GPU进行训练。我猜想,在将来multi_gpu_model
肯定会进一步得到改进,能让我们指定使用哪几个GPU进行训练,并最终实现多系统的训练。
文章原标题《How-To: Multi-GPU training with Keras, Python, and deep learning》,作者:Adrian Rosebrock,译者:夏天,审校:主题曲。
文章为简译,更为详细的内容,请查看原文
本文由北邮@爱可可-爱生活老师推荐,阿里云云栖社区组织翻译。