MXNet: A flexible and efficient library for deep learning.
這是MXNet的官網介紹,“MXNet是靈活且高效的深度學習庫”。
MXNet是主流的三大深度學習框架之一:
- TensorFlow:Google支援,其簡化版是Keras;
- PyTorch:Facebook支援,其工業版是Caffe2;
- MXNet:中立,Apache孵化器專案,也被AWS選為官方DL平臺;
MXNet的優勢是,其開發者之一李沐,是中國人??,在MXNet的推廣中具有語言優勢(漢語),有利於國內開發者的學習。同時,推薦李沐錄製的教學視訊,非常不錯。
MXNet的高層介面是Gluon,Gluon同時支援靈活的動態圖和高效的靜態圖,既保留動態圖的易用性,也具有靜態圖的高效能,這也是官網介紹的flexible和efficient的出處。同時,MXNet還具備大量學術界的前沿演算法,方便移植至工業界。希望MXNet團隊再接再勵,在深度學習框架的競賽中,位於前列。
因此,掌握 MXNet/Gluon 很有必要。
本文以深度學習的多層感知機(Multilayer Perceptrons)為演算法基礎,資料集選用MNIST,介紹MXNet的工程細節。
本文的原始碼:https://github.com/SpikeKing/gluon-tutorial
資料集
在虛擬環境(Virtual Env)中,直接使用pip安裝MXNet即可:
pip install mxnet
複製程式碼
如果下載速度較慢,推薦使用阿里雲的pypi源:
-i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com
複製程式碼
MNIST就是著名的手寫數字識別庫,其中包含0至9等10個數字的手寫體,圖片大小為28*28的灰度圖,目標是根據圖片識別正確的數字。
MNIST庫在MXNet中被封裝為MNIST類,資料儲存於.mxnet/datasets/mnist
中。如果下載MNIST資料較慢,可以選擇到MNIST官網下載,放入mnist資料夾中即可。在MNIST類中:
- 引數
train
:是否為訓練資料,其中true是訓練資料,false是測試資料; - 引數
transform
:資料的轉換函式,lambda表示式,轉換資料和標籤為指定的資料型別;
原始碼:
# 引數train
if self._train:
data, label = self._train_data, self._train_label
else:
data, label = self._test_data, self._test_label
# 引數transform
if self._transform is not None:
return self._transform(self._data[idx], self._label[idx])
return self._data[idx], self._label[idx]
複製程式碼
在MXNet中,資料載入類被封裝成DataLoader類,迭代器模式,迭代輸出與批次數相同的樣本集。在DataLoader中,
- 引數
dataset
:資料來源,如MNIST; - 引數
batch_size
:訓練中的批次數量,在迭代中輸出指定數量的樣本; - 引數
shuffle
:是否洗牌,即打亂資料,一般在訓練時需要此操作。
迭代器的測試,每次輸出樣本個數(第1維)與指定的批次數量相同:
for data, label in train_data:
print(data.shape) # (64L, 28L, 28L, 1L)
print(label.shape) # (64L,)
break
複製程式碼
在load_data()
方法中,輸出訓練和測試資料,資料型別是0~1(灰度值除以255)的浮點數,標籤型別也是浮點數。
具體實現:
def load_data(self):
def transform(data, label):
return data.astype(np.float32) / 255., label.astype(np.float32)
train_data = DataLoader(MNIST(train=True, transform=transform),
self.batch_size, shuffle=True)
test_data = DataLoader(MNIST(train=False, transform=transform),
self.batch_size, shuffle=False)
return train_data, test_data
複製程式碼
模型
網路模型使用MXNet中Gluon的樣式:
- 建立
Sequential()
序列,Sequential是全部操作單元的容器; - 新增全連線單元Dense,引數units是輸出單元的個數,引數activation是啟用函式;
- 初始化引數:
- init是資料來源,Normal類即正態分佈,sigma是正態分佈的標準差;
- ctx是上下文,表示訓練中引數更新使用CPU或GPU,如mx.cpu();
Gluon的Sequential類與其他的深度學習框架類似,通過有序地連線不同的操作單元,組成不同的網路結構,每一層只需設定輸出的維度,輸入維度通過上一層傳遞,轉換矩陣在內部自動計算。
實現:
def model(self):
num_hidden = 64
net = gluon.nn.Sequential()
with net.name_scope():
net.add(gluon.nn.Dense(units=num_hidden, activation="relu"))
net.add(gluon.nn.Dense(units=num_hidden, activation="relu"))
net.add(gluon.nn.Dense(units=self.num_outputs))
net.collect_params().initialize(init=mx.init.Normal(sigma=.1), ctx=self.model_ctx)
print(net) # 展示模型
return net
複製程式碼
其中,net.name_scope()
為Sequential中的操作單元自動新增名稱。
模型視覺化
直接使用print(),列印模型結構,如print(net)
:
Sequential(
(0): Dense(None -> 64, Activation(relu))
(1): Dense(None -> 64, Activation(relu))
(2): Dense(None -> 10, linear)
)
複製程式碼
或,使用稍複雜的jupyter繪製模型,安裝jupyter包(Python 2.x):
pip install ipython==5.3.0
pip install jupyter==1.0.0
複製程式碼
啟動jupyter服務,訪問http://localhost:8888/
:
jupyter notebook
複製程式碼
新建Python 2
檔案,編寫繪製網路的程式碼。程式碼的樣式是,在已有模型之後,新增“繪製邏輯”,呼叫plot_network()
即可繪圖。如果替換Sequential類為HybridSequential類,可以提升繪製效率,不替換也不會影響繪製效果
網路模型和繪製邏輯:
import mxnet as mx
from mxnet import gluon
num_hidden = 64
net = gluon.nn.HybridSequential()
with net.name_scope():
net.add(gluon.nn.Dense(num_hidden, activation="relu"))
net.add(gluon.nn.Dense(num_hidden, activation="relu"))
net.add(gluon.nn.Dense(10))
# 繪製邏輯
net.hybridize()
net.collect_params().initialize()
x = mx.sym.var('data')
sym = net(x)
mx.viz.plot_network(sym)
複製程式碼
效果圖:
訓練
在訓練前,載入資料,建立網路。
train_data, test_data = self.load_data() # 訓練和測試資料
net = self.model() # 模型
複製程式碼
接著,建立交叉熵的介面softmax_cross_entropy
,建立訓練器trainer
。
訓練器的引數包含:網路中引數、優化器、優化器的引數等。
epochs = 10
smoothing_constant = .01
num_examples = 60000
softmax_cross_entropy = gluon.loss.SoftmaxCrossEntropyLoss() # 交叉熵
trainer = gluon.Trainer(params=net.collect_params(),
optimizer='sgd',
optimizer_params={'learning_rate': smoothing_constant})
複製程式碼
迴圈epoch訓練網路模型:
- 從迭代器
train_data
源中,獲取批次資料和標籤: - 指定資料和標籤的執行環境ctx是CPU或GPU,同時展開資料為1行;
- 自動梯度計算
autograd.record()
,網路預測資料,輸出output,計算交叉熵loss; - 對於loss反向傳播求導,設定訓練器trainer的步驟為批次數;
- 在
cumulative_loss
中,累加每個批次的損失loss,計算全部損失; - 在訓練一次epoch之後,計算測試和訓練資料的準確率accuracy;
不斷迴圈,直至執行完成全部epochs為止。
訓練的實現:
for e in range(epochs):
cumulative_loss = 0 # 累積的
for i, (data, label) in enumerate(train_data):
data = data.as_in_context(self.model_ctx).reshape((-1, 784)) # 資料
label = label.as_in_context(self.model_ctx) # 標籤
with autograd.record(): # 梯度
output = net(data) # 輸出
loss = softmax_cross_entropy(output, label) # 輸入和輸出計算loss
loss.backward() # 反向傳播
trainer.step(data.shape[0]) # 設定trainer的step
cumulative_loss += nd.sum(loss).asscalar() # 計算全部損失
test_accuracy = self.__evaluate_accuracy(test_data, net)
train_accuracy = self.__evaluate_accuracy(train_data, net)
print("Epoch %s. Loss: %s, Train_acc %s, Test_acc %s" %
(e, cumulative_loss / num_examples, train_accuracy, test_accuracy))
複製程式碼
在預測介面evaluate_accuracy()
中:
- 建立準確率Accuracy類acc,用於統計準確率;
- 迭代輸出批次的資料和標籤;
- 預測資料不同類別的概率,選擇最大概率(argmax)做為類別;
- 通過
acc.update()
更新準確率;
最終返回準確率的值,即acc的第2維acc[1]
,而acc的第1維acc[0]
是acc的名稱。
def __evaluate_accuracy(self, data_itertor, net):
acc = mx.metric.Accuracy() # 準確率
for i, (data, label) in enumerate(data_iterator):
data = data.as_in_context(self.model_ctx).reshape((-1, 784))
label = label.as_in_context(self.model_ctx)
output = net(data) # 預測結果
predictions = nd.argmax(output, axis=1) # 類別
acc.update(preds=predictions, labels=label) # 更新概率和標籤
return acc.get()[1] # 第1維是資料名稱,第2維是概率
複製程式碼
效果:
Epoch 0. Loss: 1.2743850797812144, Train_acc 0.846283333333, Test_acc 0.8509
Epoch 1. Loss: 0.46071574948628746, Train_acc 0.884366666667, Test_acc 0.8892
Epoch 2. Loss: 0.37149955205917357, Train_acc 0.896466666667, Test_acc 0.9008
Epoch 3. Loss: 0.3313815038919449, Train_acc 0.908366666667, Test_acc 0.9099
Epoch 4. Loss: 0.30456133014361064, Train_acc 0.915966666667, Test_acc 0.9172
Epoch 5. Loss: 0.2827877395868301, Train_acc 0.919466666667, Test_acc 0.9214
Epoch 6. Loss: 0.2653073514064153, Train_acc 0.925433333333, Test_acc 0.9289
Epoch 7. Loss: 0.25018166739145914, Train_acc 0.92965, Test_acc 0.9313
Epoch 8. Loss: 0.23669789231618246, Train_acc 0.933816666667, Test_acc 0.9358
Epoch 9. Loss: 0.22473177655935286, Train_acc 0.934716666667, Test_acc 0.9337
複製程式碼
GPU
對於深度學習而言,使用GPU可以加速網路的訓練過程,MXNet同樣支援使用GPU訓練網路。
檢查伺服器的Cuda版本,命令:nvcc --version
,用於確定下載MXNet的GPU版本。
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2016 NVIDIA Corporation
Built on Sun_Sep__4_22:14:01_CDT_2016
Cuda compilation tools, release 8.0, V8.0.44
複製程式碼
則,當前伺服器的Cuda版本是8.0。
將MXNet由CPU版本轉為GPU版本,解除安裝mxnet
,安裝mxnet-cu80
。
pip uninstall mxnet
pip install mxnet-cu80
複製程式碼
當安裝完成GPU版本之後,在Python Console中,執行如下程式碼,確認MXNet的GPU庫可以使用。
>>> import mxnet as mx
>>> a = mx.nd.ones((2, 3), mx.gpu())
>>> b = a * 2 + 1
>>> b.asnumpy()
array([[ 3., 3., 3.],
[ 3., 3., 3.]], dtype=float32)
複製程式碼
檢查GPU數量,命令:nvidia-smi
:
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 375.26 Driver Version: 375.26 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|===============================+======================+======================|
| 0 TITAN X (Pascal) Off | 0000:02:00.0 Off | N/A |
| 28% 49C P2 84W / 250W | 12126MiB / 12189MiB | 25% Default |
+-------------------------------+----------------------+----------------------+
| 1 TITAN X (Pascal) Off | 0000:03:00.0 Off | N/A |
| 24% 39C P2 57W / 250W | 12126MiB / 12189MiB | 33% Default |
+-------------------------------+----------------------+----------------------+
| 2 TITAN X (Pascal) Off | 0000:83:00.0 Off | N/A |
| 25% 41C P2 58W / 250W | 12126MiB / 12189MiB | 37% Default |
+-------------------------------+----------------------+----------------------+
| 3 TITAN X (Pascal) Off | 0000:84:00.0 Off | N/A |
| 23% 31C P2 53W / 250W | 11952MiB / 12189MiB | 2% Default |
+-------------------------------+----------------------+----------------------+
複製程式碼
則,當前伺服器的GPU數量是4。
設定引數環境ctx為GPU的列表,即[mx.gpu(0), mx.gpu(1), ...]
。
GPU_COUNT = 4
ctx = [mx.gpu(i) for i in range(GPU_COUNT)]
複製程式碼
在網路net中使用GPU初始化initialize()引數params,然後建立trainer訓練器。
net = self.model() # 模型
net.collect_params().initialize(init=mx.init.Normal(sigma=.1), ctx=ctx)
smoothing_constant = .01
trainer = gluon.Trainer(params=net.collect_params(),
optimizer='sgd',
optimizer_params={'learning_rate': smoothing_constant})
複製程式碼
迴圈執行10個epoch訓練模型,train_data
和valid_data
是迭代器,每次輸出一個batch樣本集。在train_batch()
中,依次傳入批次資料batch、GPU環境列表ctx、網路net和訓練器trainer;在valid_batch()
中,與訓練類似,只是不傳訓練器trainer。
epochs = 10
for e in range(epochs):
start = time()
for batch in train_data:
self.train_batch(batch, ctx, net, trainer)
nd.waitall() # 等待所有非同步的任務都終止
print('Epoch %d, training time = %.1f sec' % (e, time() - start))
correct, num = 0.0, 0.0
for batch in valid_data:
correct += self.valid_batch(batch, ctx, net)
num += batch[0].shape[0]
print('\tvalidation accuracy = %.4f' % (correct / num))
複製程式碼
具體分析批次訓練方法train_batch()
:
- 輸入batch是資料和標籤的集合,索引0表示資料,索引1表示標籤。
- 根據GPU的數量,拆分資料data與標籤label,每個GPU對應不同的資料;
- 每組資料和標籤,分別反向傳播backward()更新網路net的引數;
- 設定訓練器trainer的步驟step為批次數
batch_size
;
多個GPU是相互獨立的,因此,當使用多個GPU訓練模型時,需要注意不同GPU之間的資料融合。
實現如下:
@staticmethod
def train_batch(batch, ctx, net, trainer):
# split the data batch and load them on GPUs
data = gluon.utils.split_and_load(batch[0], ctx) # 列表
label = gluon.utils.split_and_load(batch[1], ctx) # 列表
# compute gradient
GluonFirst.forward_backward(net, data, label)
# update parameters
trainer.step(batch[0].shape[0])
@staticmethod
def forward_backward(net, data, label):
loss = gluon.loss.SoftmaxCrossEntropyLoss()
with autograd.record():
losses = [loss(net(X), Y) for X, Y in zip(data, label)] # loss列表
for l in losses: # 每個loss反向傳播
l.backward()
複製程式碼
具體分析批次驗證方法valid_batch()
:
- 將全部驗證資料,都執行於一個GPU中,即ctx[0];
- 網路net預測資料data的類別概率,再轉換為具體類別argmax();
- 將全部預測正確的樣本進行彙總,獲得總的正確樣本數;
實現如下:
@staticmethod
def valid_batch(batch, ctx, net):
data = batch[0].as_in_context(ctx[0])
pred = nd.argmax(net(data), axis=1)
return nd.sum(pred == batch[1].as_in_context(ctx[0])).asscalar()
複製程式碼
除了訓練部分,GPU的資料載入和網路模型都與CPU一致。
訓練GPU模型,需要連線遠端伺服器,上傳工程。如果無法使用Git傳輸,則推薦使用RsyncOSX,非常便捷的檔案同步工具:
在遠端伺服器中,將工程的依賴庫安裝至虛擬環境中,注意需要使用MXNet的GPU版本mxnet-cu80
,接著,執行模型訓練。
以下是GPU版本的模型輸出結果:
Epoch 5, training time = 13.7 sec
validation accuracy = 0.9277
Epoch 6, training time = 13.9 sec
validation accuracy = 0.9284
Epoch 7, training time = 13.8 sec
validation accuracy = 0.9335
Epoch 8, training time = 13.7 sec
validation accuracy = 0.9379
Epoch 9, training time = 14.4 sec
validation accuracy = 0.9402
複製程式碼
當遇到如下警告⚠️時:
only 4 out of 12 GPU pairs are enabled direct access.
It may affect the performance. You can set MXNET_ENABLE_GPU_P2P=0 to turn it off
複製程式碼
關閉MXNET_ENABLE_GPU_P2P
即可,不影響正常的訓練過程。
export MXNET_ENABLE_GPU_P2P=0
複製程式碼
至此 MXNet/Gluon 的工程設計,已經全部完成,從資料集、模型、訓練、GPU四個部分剖析MXNet的實現細節,MXNet的各個環節設計的非常巧妙,也與其他框架類似,容易上手。例項雖小,“五臟俱全”,為繼續學習MXNet框架,起到拋磚引玉的作用。
OK, that's all! Enjoy it!