深度框架 MXNet/Gluon 初體驗

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同時支持靈活的動態圖和高效的靜態圖,既保留動態圖的易用性,也具有靜態圖的高性能,這也是官網介紹的flexibleefficient的出處。同時,MXNet還具備大量學術界的前沿算法,方便移植至工業界。希望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的樣式:

  1. 創建Sequential()序列,Sequential是全部操作單元的容器;
  2. 添加全連接單元Dense,參數units是輸出單元的個數,參數activation是激活函數;
  3. 初始化參數:
    • 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訓練網絡模型:

  1. 從迭代器train_data源中,獲取批次數據和標籤:
  2. 指定數據和標籤的執行環境ctx是CPU或GPU,同時展開數據爲1行;
  3. 自動梯度計算autograd.record(),網絡預測數據,輸出output,計算交叉熵loss;
  4. 對於loss反向傳播求導,設置訓練器trainer的步驟爲批次數;
  5. cumulative_loss中,累加每個批次的損失loss,計算全部損失;
  6. 在訓練一次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()中:

  1. 創建準確率Accuracy類acc,用於統計準確率;
  2. 迭代輸出批次的數據和標籤;
  3. 預測數據不同類別的概率,選擇最大概率(argmax)做爲類別;
  4. 通過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_datavalid_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()

  1. 輸入batch是數據和標籤的集合,索引0表示數據,索引1表示標籤。
  2. 根據GPU的數量,拆分數據data與標籤label,每個GPU對應不同的數據;
  3. 每組數據和標籤,分別反向傳播backward()更新網絡net的參數;
  4. 設置訓練器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()

  1. 將全部驗證數據,都運行於一個GPU中,即ctx[0];
  2. 網絡net預測數據data的類別概率,再轉換爲具體類別argmax();
  3. 將全部預測正確的樣本進行彙總,獲得總的正確樣本數;

實現如下:

@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,非常便捷的文件同步工具:

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!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章