1. 程式人生 > >mxnet試用體驗

mxnet試用體驗

mxnet 是深度學習領域的主流框架之一,近段時間還成為了Amazon的AWS預設深度學習引擎(知乎上還說是Amazon的宮鬥,然而吃瓜群眾只關心實用性不關心背後的故事)。由於本人以前一直使用的都是caffe,故而本文或多或少會對caffemxnet進行一定程度的比較。

安裝

mxnet常常被吐槽的是文件欠缺和社群稍弱。但是在安裝這一項上,mxnet對於新手是非常友好的,僅需要進行

git clone https://github.com/dmlc/mxnet.git --recursive
cd mxnet/setup-utils
bash install-mxnet-ubuntu-python.sh

官方文件還加了幾個步驟,是修改編譯選項的操作,主要是新增cuda編譯和cudnn編譯。install的安裝腳本里面還有幾個依賴項的安裝,例如jupyter notebook用於畫網路結構圖(吐槽一下,這個圖挺醜的,根本不實用..)

一般情況下,我們都只需要使用python介面,所以第一次編譯專案的話,直接使用install-mxnet-ubuntu-python.sh或者根據裡面的內容自己敲進命令列都可以。如果後續還需要重新編譯專案,那就直接make就可以了。編譯的過程相對來說比較漫長,即使用上了8執行緒也等了頗為漫長的一段時間,所以如果沒有什麼異常情況的話,就不要make clean

mnist實驗

編譯完成之後,我們一般就會跑一個mnist實驗。在深度學習裡面,跑一個mnist就像寫一個hello world一樣。

mxnet根目錄下有很多個資料夾,其中有一個叫example,裡面有多個不同的CV或者NLP任務的例子。進入其中的image-classification子目錄,然後找到train_mnist.py,執行

python train_mnist.py -h

在mxnet中如果對於高層API的選項有疑問,可以使用-h來檢視。example裡面的train_*.py類指令碼都可以如此使用。如果不在指令碼後加入arg,訓練過程會預設使用cpu,不存模型,僅進行預設的資料增強(data augmentation)。一般做實驗我們需要用到的選項是如下兩個:

python train_something.py \
	--gpus 0	\
	--model-prefix snapshot/something		\

分別是指定使用GPU進行計算,還有定時模型存檔。懶得敲就寫個指令碼來使用。更高階一點的功能就是資料增強的部分。caffe原生只支援mirror操作和crop操作,mxnet提供了更加豐富的功能,包括:

  1. --random-crop 常規的切圖
  2. --random-mirror 常規的映象
  3. --max-random-h 影象H通道的抖動,預設開啟,抖動範圍36
  4. --max-random-s 影象S通道的抖動,預設開啟,抖動範圍50
  5. --max-random-aspect-ratio 長寬比抖動
  6. --max-random-rotate-angle 旋轉
  7. --max-random-shear-ratio (待查明,暫時理解為切掉影象的一部分)
  8. --max-random-scale--min-random-scale兩個值配合使用控制影象尺寸的抖動,但是由於CNN網路一旦有全連線層,則要求影象輸入一致,所以一般只能固定為1

通過觀察系統資源排程,發現mxnet的augmentation是多執行緒的CPU操作,效率比較高。以前在caffe中想要實現類似的訓練實時資料增強,一般都只能在data_layer中修改程式碼,不太容易實現多執行緒,估計是我太弱[攤手]。所以在mxnet資料增強的部分,加一秒,啊不,加一分。

回到正題,本節內容主要是想講述如何跑起來一個mnist實驗。好,找到train_mnist.py指令碼,執行,講完…

腳本里面有下載資料的程式碼,example裡面有幾個候選網路,一切都準備好了,完全就是傻瓜式操作…我不甘心!怎麼能把本少爺當傻瓜!接下來,我要用自己的資料跑一個實驗!

資料準備

為了完整跑出來一個實驗,我決定用本人的研究課題“第一視角手勢互動”的資料進行一個簡單的手勢分類實驗。

首先是資料準備的流程。在caffe裡面做分類任務,一般需要準備兩個裝圖片的資料夾和兩個文字用來標明圖片對應的類別編號,如

img1.jpg 0
img2.jpg 1
img3.jpg 0
img4.jpg 2

通過呼叫convert_imageset可執行檔案生成lmdb。而mxnet則是類似的呼叫一個工具,生成字尾為.rec的檔案。

caffe原始碼裡面對於讀入label文字的部分邏輯比較生硬,例如直接用boost庫的split來分割一個空格’ ‘,例如僅讀如一個int型的label,對multi-label的任務而言,使用者必須修改程式碼,這一點也是比較坑。mxnet讀如文字的部分支援multi-label,資料格式大概是

id label label label ... label path/to/image

例如單label的話就是

3123 0 img1.jpg
4322 1 img2.jpg
5123 2 img3.jpg

第一個id我暫時不清楚用途,後面就是直接是label和圖片路徑。

#####更妙的是 對於簡單的分類任務,mxnet提供了工具能夠直接遍歷一個資料夾,然後生成label文字。所以,使用者只需要按照類別把圖片存在不同的子目錄下面就可以了。

python im2rec.py -h

工具im2rec.py存與/mxnet/tools/下,同樣可以檢視選項。

python im2rec.py prefixname /path/to/your/data --list prefixname.lst --recursive --train-ratio 0.8 --test-ratio 0.2

上述的命令意思是遞迴地檢視/path/to/your/data目錄下面的所有子目錄和圖片,每個字目錄分別給予一個label的編碼,然後按照一定的比例劃分,生成一個*.lst的列表。這個列表就可以用來生成我們的需要的.rec檔案用於訓練。

python im2rec.py prefixname /path/to/your/data --resize 224 --num-thread 8

第二個命令依然是im2rec.py,只是在這個時刻,.lst檔案也已經生成好了,所以高階選項不同了。通過resize可以控制資料的短邊尺寸(會保持長寬比的),numthread選項可以多執行緒讀圖加快rec檔案的生成速度。這一點,還是mxnet加一秒。

網路訓練

進行CNN訓練需要兩個部分,一個是定義訓練過程的指令碼train_*.py,一個是定義網路的symbol指令碼some_network_name.py。思想上和caffe裡面的solver.prototxt結合train_val.prototxt一致,只是換了語言…

隨手抓一個train_cifar10.py,對程式碼做一定的修改。

import os
import argparse
import logging
logging.basicConfig(level=logging.DEBUG)
from common import find_mxnet, data, fit
from common.util import download_file
import mxnet as mx

def use_dataset():
    data_dir="data"
    fnames = (os.path.join(data_dir, "train.rec"),
              os.path.join(data_dir, "test.rec"))
    return fnames

if __name__ == '__main__':

    (train_fname, val_fname) = use_dataset()

    # parse args
    parser = argparse.ArgumentParser(description="train",
                                     formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    fit.add_fit_args(parser)
    data.add_data_args(parser)
    data.add_data_aug_args(parser)
    data.set_data_aug_level(parser, 2)
    parser.set_defaults(
        # network
        network        = 'tinynet',
        # data
        data_train     = train_fname,
        data_val       = val_fname,
        num_classes    = 12,
        num_examples   = 40675,
        image_shape    = '3,160,160',
        random_crop    = 0,
        # train
        batch_size     = 256,
        num_epochs     = 100,
        lr             = .01,
        lr_factor      = .1,
        lr_step_epochs = '50',
        # display
        disp_batches   = 50,
    )
    args = parser.parse_args()

    # load network
    from importlib import import_module
    net = import_module('symbol.'+args.network)
    print(net)

    sym = net.get_symbol(**vars(args))

    # train
    fit.fit(args, sym, data.get_rec_iter)

實際上需要修改的只有dataset的名字(或者是路徑,如果不在data/下的話),以及parser.set_defaults裡面的內容。學習率依然是一個比較難設定的引數。一開始做實驗以為是哪裡程式碼不對,一個簡單的二分類問題都不收斂,後來發現是預設學習率太大。如果是caffe的話,可以看到loss值非常大,甚至會出現nan或者inf,但是在mxnet中只打印使用者設定的一個衡量指標,例如accuracy,而不會計算loss值,所以只能看到accuracy一直不變,無從判斷髮生了什麼。接下來看看修改程式碼列印一下loss值會更加便於使用。這個點上,caffe加一秒。

定義網路的py指令碼放在每個example的symbol資料夾裡面,非常簡單。假如需要自己定義一個網路,可以隨手扒一個alex或者vgg然後修改修改,例如:

import mxnet as mx

def get_symbol(num_classes, **kwargs):
    input_data = mx.symbol.Variable(name="data")
    input_data = mx.symbol.BatchNorm(data=input_data, fix_gamma=True, eps=2e-5, momentum=0.9, name='bn_data')
    # stage 1
    conv1 = mx.symbol.Convolution(data=input_data, kernel=(3, 3), stride=(1, 1), pad=(1, 1), num_filter=32)
    relu1 = mx.symbol.Activation(data=conv1, act_type="relu")
    pool1 = mx.symbol.Pooling(data=relu1, pool_type="max", kernel=(2, 2), stride=(2,2))
    # stage 2
    conv2 = mx.symbol.Convolution(data=pool1, kernel=(3, 3), pad=(1, 1), num_filter=32)
    relu2 = mx.symbol.Activation(data=conv2, act_type="relu")
    pool2 = mx.symbol.Pooling(data=relu2, kernel=(2, 2), stride=(2, 2), pool_type="max")
    # stage 3
    conv3 = mx.symbol.Convolution(data=pool2, kernel=(3, 3), pad=(1, 1), num_filter=64)
    relu3 = mx.symbol.Activation(data=conv3, act_type="relu")
    pool3 = mx.symbol.Pooling(data=relu3, kernel=(2, 2), stride=(2, 2), pool_type="max") 
    # stage 4
    conv4 = mx.symbol.Convolution(data=pool3, kernel=(3, 3), pad=(1, 1), num_filter=64)
    relu4 = mx.symbol.Activation(data=conv4, act_type="relu")
    pool4 = mx.symbol.Pooling(data=relu4, kernel=(2, 2), stride=(2, 2), pool_type="max")        
    # stage 5 & 6
    conv5 = mx.symbol.Convolution(data=pool4, kernel=(3, 3), pad=(1, 1), num_filter=96)
    relu5 = mx.symbol.Activation(data=conv5, act_type="relu")
    conv6 = mx.symbol.Convolution(data=relu5, kernel=(3, 3), pad=(1, 1), num_filter=96)
    relu6 = mx.symbol.Activation(data=conv6, act_type="relu")
    # stage 7
    flatten = mx.symbol.Flatten(data=relu6)
    fc1 = mx.symbol.FullyConnected(data=flatten, num_hidden=256)
    relu7 = mx.symbol.Activation(data=fc1, act_type="relu")
    dropout1 = mx.symbol.Dropout(data=relu7, p=0.5)
    # stage 8
    fc2 = mx.symbol.FullyConnected(data=dropout1, num_hidden=32)
    relu8 = mx.symbol.Activation(data=fc2, act_type="relu")
    dropout2 = mx.symbol.Dropout(data=relu8, p=0.5)
    # stage 9
    fc3 = mx.symbol.FullyConnected(data=dropout2, num_hidden=num_classes)
    softmax = mx.symbol.SoftmaxOutput(data=fc3, name='softmax')

    return softmax

細心的讀者會可能會發現,這個網路在data後面加了BatchNorm。關於BatchNorm,我一開始沒有太多關注,僅知道大概是把輸入屬於變換為均值為0的高斯分佈,從而減少數值溢位的風險。

仔細思考了一下整個數值計算過程 > 在大多數CNN設計中,輸入都直接是圖片畫素值的範圍(0,255)。網路初始化的時候,一般權重也是一個均值為0的高斯分佈。這樣的情況下,卷積過程中,某幾個權重和某幾個對應畫素值相乘之和可能比較大,然後後面的卷積層池化層等等有可能“滾雪球”一樣在前向過程中越滾越大(因為池化一般取max-pooling求最大值,啟用一般取relu正數為線性,所以均不存在數值上界;假如存在起到歸一化作用的層會停止這個“滾雪球”,例如啟用取sigmoid,但是sigmoid啟用會引起梯度彌散問題)。假如必須要在這種輸入條件下進行訓練,只能通過設定一個非常小的學習率來保護反向傳播的梯度。這樣整個收斂速度都非常非常慢。

那麼,為了解決這個“數值爆炸”的問題,處理手段有好些。在caffe我一般的做法是在data層加上mean引數和scale引數,讓輸入的數值範圍從(0,255)變換到(-1,+1)。假設輸入數值本身就是一個均值為128的高斯分佈,那麼(沒理解錯的話)加上mean和scale就等價於BatchNorm起到的作用,能夠保證數值處於合適的範圍。mxnet中不支援對資料進行scale操作,假如通過修改程式碼實現的話,需要同時對data_augmentation的程式碼進行修改,否則隨機噪聲會成為巨大的錯誤。所以,在data後面加上BatchNorm是一個相對簡單的實現。

關於BatchNorm除了閱讀論文以外,還可以簡單看一下這篇部落格,文風比較優雅。有一些部落格提到BN一般使用在每個convolution層或fc層後面,我目前沒有驗證是否有這個必要性。也許大資料集上或者極深網路中需要吧。在小規模網路中我個人認為只放一個在data層後面基本足夠了。

網路預測

預測過程就是一個單純的前向過程。caffe進行前向就是在extract_feature.cpp的程式碼中扒出前向的部分,然後補上適合自己的輸入和輸出。根據經驗,mxnet一定也存在類似的程式碼。簡單搜尋後發現這個程式碼名為mxnet_predict_example.py,放在/mxnet/python/mxnet目錄下。修改輸入影象的尺寸,修改資料的預處理部分,修改輸出,指定定義網