Keras多GPU訓練指南
更多深度文章,請關注: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
創建一個新文件,將其命名為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
如果你使用的是無外設服務器,則需要取消第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"]
我們在第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
設置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")
接著對數據應用均值減法:
# apply mean subtraction to the data
mean = np.mean(trainX, axis=0)
trainX -= mean
testX -= mean
在第56行,計算了所有訓練圖像的平均值,然後在第57和58行,將訓練和測試集中的每個圖像減去這個平均值。
然後,執行“獨熱編碼(one-hot encoding)”:
# convert the labels from integers to vectors
lb = LabelBinarizer()
trainY = lb.fit_transform(trainY)
testY = lb.transform(testY)
獨熱編碼將分類標簽從單個整數轉換為向量,這樣,就可以對其應用分類交叉熵損失函數。 我們已經在第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)]
在第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)
如果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)
在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)
在第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()
最後一段代碼隻是使用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
圖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
圖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,譯者:夏天,審校:主題曲。
文章為簡譯,更為詳細的內容,請查看原文
本文由北郵@愛可可-愛生活老師推薦,阿裏雲雲棲社區組織翻譯。
最後更新:2017-11-02 10:33:43