近年來(lái),深度學(xué)習(xí)可謂是機(jī)器學(xué)習(xí)方向的明星概念,不同的模型分別在圖像處理與自然語(yǔ)言處理等任務(wù)中取得了前所未有的好成績(jī)。在實(shí)際的應(yīng)用中,大家除了關(guān)心模型的準(zhǔn)確度,還常常希望能比較快速地完成模型的訓(xùn)練。一個(gè)常用的加速手段便是將模型放在GPU上進(jìn)行訓(xùn)練。然而由于種種原因,R語(yǔ)言似乎缺少一個(gè)能夠在GPU上訓(xùn)練深度學(xué)習(xí)模型的程序包。

DMLC(Distributed (Deep) Machine Learning Community)是由一群極客發(fā)起的組織,主要目標(biāo)是提供快速高質(zhì)量的開(kāi)源機(jī)器學(xué)習(xí)工具。近來(lái)流行的boosting模型xgboost便是出自這個(gè)組織。最近DMLC開(kāi)源了一個(gè)深度學(xué)習(xí)工具mxnet,這個(gè)工具含有R,python,julia等語(yǔ)言的接口。本文以R接口為主,向大家介紹這個(gè)工具的性能與使用方法。
一、五分鐘入門指南
在這一節(jié)里,我們?cè)谝粋€(gè)樣例數(shù)據(jù)上介紹mxnet的基本使用方法。目前mxnet還沒(méi)有登錄CRAN的計(jì)劃,所以安裝方法要稍微復(fù)雜一些。
- 如果你是Windows/Mac用戶,那么可以通過(guò)下面的代碼安裝預(yù)編譯的版本。這個(gè)版本會(huì)每周進(jìn)行預(yù)編譯,不過(guò)為了保證兼容性,只能使用CPU訓(xùn)練模型。
install.packages("drat", repos="https://cran.")
drat:::addRepo("dmlc")
install.packages("mxnet")
- 如果你是Linux用戶或者想嘗試GPU版本,請(qǐng)參考這個(gè)鏈接里的詳細(xì)編譯教程在本地進(jìn)行編譯。
安裝完畢之后,我們就可以開(kāi)始訓(xùn)練模型了,下面兩個(gè)小節(jié)分別介紹兩種不同的訓(xùn)練神經(jīng)網(wǎng)絡(luò)的方法。
二分類模型與mx.mlp
首先,我們準(zhǔn)備一份數(shù)據(jù),并進(jìn)行簡(jiǎn)單的預(yù)處理:
require(mlbench)
require(mxnet)
data(Sonar, package="mlbench")
Sonar[,61] = as.numeric(Sonar[,61])-1
train.ind = c(1:50, 100:150)
train.x = data.matrix(Sonar[train.ind, 1:60])
train.y = Sonar[train.ind, 61]
test.x = data.matrix(Sonar[-train.ind, 1:60])
test.y = Sonar[-train.ind, 61]
我們借用mlbench 包中的一個(gè)二分類數(shù)據(jù),并且將它分成訓(xùn)練集和測(cè)試集。mxnet 提供了一個(gè)訓(xùn)練多層神經(jīng)網(wǎng)絡(luò)的函數(shù)mx.mlp ,我們額可以通過(guò)它來(lái)訓(xùn)練一個(gè)神經(jīng)網(wǎng)絡(luò)模型。下面是mx.mlp 中的部分參數(shù):
- 訓(xùn)練數(shù)據(jù)與預(yù)測(cè)變量
- 每個(gè)隱藏層的大小
- 輸出層的結(jié)點(diǎn)數(shù)
- 激活函數(shù)類型
- 損失函數(shù)類型
- 進(jìn)行訓(xùn)練的硬件(CPU還是GPU)
- 其他傳給mx.model.FeedForward.create的高級(jí)參數(shù)
了解了大致參數(shù)后,我們就可以理解并讓R運(yùn)行下面的代碼進(jìn)行訓(xùn)練了。
mx.set.seed(0)
model <- mx.mlp(train.x, train.y, hidden_node=10, out_node=2, out_activation="softmax", num.round=20, array.batch.size=15, learning.rate=0.07, momentum=0.9, eval.metric=mx.metric.accuracy)
## Auto detect layout of input matrix, use rowmajor..
## Start training with 1 devices
## [1] Train-accuracy=0.488888888888889
## [2] Train-accuracy=0.514285714285714
## [3] Train-accuracy=0.514285714285714
...
## [18] Train-accuracy=0.838095238095238
## [19] Train-accuracy=0.838095238095238
## [20] Train-accuracy=0.838095238095238
這里要注意使用mx.set.seed而不是R自帶的set.seed函數(shù)來(lái)控制隨機(jī)數(shù)。因?yàn)閙xnet的訓(xùn)練過(guò)程可能會(huì)運(yùn)行在不同的運(yùn)算硬件上,我們需要一個(gè)足夠快的隨機(jī)數(shù)生成器來(lái)管理整個(gè)隨機(jī)數(shù)生成的過(guò)程。模型訓(xùn)練好之后,我們可以很簡(jiǎn)單地進(jìn)行預(yù)測(cè):
preds = predict(model, test.x)
## Auto detect layout of input matrix, use rowmajor..
pred.label = max.col(t(preds))-1
table(pred.label, test.y)
## test.y
## pred.label 0 1
## 0 24 14
## 1 36 33
如果進(jìn)行的是多分類預(yù)測(cè),mxnet的輸出格式是類數(shù)X樣本數(shù)。
回歸模型與自定義神經(jīng)網(wǎng)絡(luò)
mx.mlp接口固然很方便,但是神經(jīng)網(wǎng)絡(luò)的一大特點(diǎn)便是它的靈活性,不同的結(jié)構(gòu)可能有著完全不同的特性。mxnet的亮點(diǎn)之一便是它賦予了用戶極大的自由度,從而可以任意定義需要的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)。我們?cè)谶@一節(jié)用一個(gè)簡(jiǎn)單的回歸任務(wù)介紹相關(guān)的語(yǔ)法。
首先,我們?nèi)匀灰獪?zhǔn)備好一份數(shù)據(jù)。
data(BostonHousing, package="mlbench")
train.ind = seq(1, 506, 3)
train.x = data.matrix(BostonHousing[train.ind, -14])
train.y = BostonHousing[train.ind, 14]
test.x = data.matrix(BostonHousing[-train.ind, -14])
test.y = BostonHousing[-train.ind, 14]
mxnet提供了一個(gè)叫做“Symbol”的系統(tǒng),從而使我們可以定義結(jié)點(diǎn)之間的連接方式與激活函數(shù)等參數(shù)。下面是一個(gè)定義沒(méi)有隱藏層神經(jīng)網(wǎng)絡(luò)的簡(jiǎn)單例子:
# 定義輸入數(shù)據(jù)
data <- mx.symbol.Variable("data")
# 完整連接的隱藏層
# data: 輸入源
# num_hidden: 該層的節(jié)點(diǎn)數(shù)
fc1 <- mx.symbol.FullyConnected(data, num_hidden=1)
# 針對(duì)回歸任務(wù),定義損失函數(shù)
lro <- mx.symbol.LinearRegressionOutput(fc1)
在神經(jīng)網(wǎng)絡(luò)中,回歸與分類的差別主要在于輸出層的損失函數(shù)。這里我們使用了平方誤差來(lái)訓(xùn)練模型。希望能更進(jìn)一步了解Symbol的讀者可以繼續(xù)閱讀這份以代碼為主的文檔。
定義了神經(jīng)網(wǎng)絡(luò)之后,我們便可以使用mx.model.FeedForward.create進(jìn)行訓(xùn)練了。
mx.set.seed(0)
model <- mx.model.FeedForward.create(lro, X=train.x, y=train.y, ctx=mx.cpu(), num.round=50, array.batch.size=20, learning.rate=2e-6, momentum=0.9, eval.metric=mx.metric.rmse)
## Auto detect layout of input matrix, use rowmajor..
## Start training with 1 devices
## [1] Train-rmse=16.063282524034
## [2] Train-rmse=12.2792375712573
## [3] Train-rmse=11.1984634005885
...
## [48] Train-rmse=8.26890902770415
## [49] Train-rmse=8.25728089053853
## [50] Train-rmse=8.24580511500735
這里我們還針對(duì)回歸任務(wù)修改了eval.metric參數(shù)。目前我們提供的評(píng)價(jià)函數(shù)包括”accuracy”,”rmse”,”mae” 和 “rmsle”,用戶也可以針對(duì)需要自定義評(píng)價(jià)函數(shù),例如:
demo.metric.mae <- mx.metric.custom("mae", function(label, pred) {
res <- mean(abs(label-pred))
return(res)
})
mx.set.seed(0)
model <- mx.model.FeedForward.create(lro, X=train.x, y=train.y, ctx=mx.cpu(), num.round=50, array.batch.size=20, learning.rate=2e-6, momentum=0.9, eval.metric=demo.metric.mae)
## Auto detect layout of input matrix, use rowmajor..
## Start training with 1 devices
## [1] Train-mae=13.1889538083225
## [2] Train-mae=9.81431959337658
## [3] Train-mae=9.21576419870059
...
## [48] Train-mae=6.41731406417158
## [49] Train-mae=6.41011292926139
## [50] Train-mae=6.40312503493494
至此,你已經(jīng)掌握了基本的mxnet使用方法。接下來(lái),我們將介紹更好玩的應(yīng)用。
二、手寫數(shù)字競(jìng)賽
在這一節(jié)里,我們以Kaggle上的手寫數(shù)字?jǐn)?shù)據(jù)集(MNIST)競(jìng)賽為例子,介紹如何通過(guò)mxnet定義一個(gè)強(qiáng)大的神經(jīng)網(wǎng)絡(luò),并在GPU上快速訓(xùn)練模型。
第一步,我們從Kaggle上下載數(shù)據(jù),并將它們放入data/文件夾中。然后我們讀入數(shù)據(jù),并做一些預(yù)處理工作。
require(mxnet)
train <- read.csv('data/train.csv', header=TRUE)
test <- read.csv('data/test.csv', header=TRUE)
train <- data.matrix(train)
test <- data.matrix(test)
train.x <- train[,-1]
train.y <- train[,1]
train.x <- t(train.x/255)
test <- t(test/255)
最后兩行預(yù)處理的作用有兩個(gè):
- 原始灰度圖片數(shù)值處在[0,255]之間,我們將其變換到[0,1]之間。
- mxnet接受 像素X圖片 的輸入格式,所以我們對(duì)輸入矩陣進(jìn)行了轉(zhuǎn)置。
接下來(lái)我們定義一個(gè)特別的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu):LeNet。這是Yann LeCun提出用于識(shí)別手寫數(shù)字的結(jié)構(gòu),也是最早的卷積神經(jīng)網(wǎng)絡(luò)之一。同樣的,我們使用Symbol語(yǔ)法來(lái)定義,不過(guò)這次結(jié)構(gòu)會(huì)比較復(fù)雜。
# input
data <- mx.symbol.Variable('data')
# first conv
conv1 <- mx.symbol.Convolution(data=data, kernel=c(5,5), num_filter=20)
tanh1 <- mx.symbol.Activation(data=conv1, act_type="tanh")
pool1 <- mx.symbol.Pooling(data=tanh1, pool_type="max",
kernel=c(2,2), stride=c(2,2))
# second conv
conv2 <- mx.symbol.Convolution(data=pool1, kernel=c(5,5), num_filter=50)
tanh2 <- mx.symbol.Activation(data=conv2, act_type="tanh")
pool2 <- mx.symbol.Pooling(data=tanh2, pool_type="max",
kernel=c(2,2), stride=c(2,2))
# first fullc
flatten <- mx.symbol.Flatten(data=pool2)
fc1 <- mx.symbol.FullyConnected(data=flatten, num_hidden=500)
tanh3 <- mx.symbol.Activation(data=fc1, act_type="tanh")
# second fullc
fc2 <- mx.symbol.FullyConnected(data=tanh3, num_hidden=10)
# loss
lenet <- mx.symbol.SoftmaxOutput(data=fc2)
為了讓輸入數(shù)據(jù)的格式能對(duì)應(yīng)LeNet,我們要將數(shù)據(jù)變成R中的array格式:
train.array <- train.x
dim(train.array) <- c(28, 28, 1, ncol(train.x))
test.array <- test
dim(test.array) <- c(28, 28, 1, ncol(test))
接下來(lái)我們將要分別使用CPU和GPU來(lái)訓(xùn)練這個(gè)模型,從而展現(xiàn)不同的訓(xùn)練效率。
n.gpu <- 1
device.cpu <- mx.cpu()
device.gpu <- lapply(0:(n.gpu-1), function(i) {
mx.gpu(i)
})
我們可以將GPU的每個(gè)核以list的格式傳遞進(jìn)去,如果有BLAS等自帶矩陣運(yùn)算并行的庫(kù)存在,則沒(méi)必要對(duì)CPU這么做了。
我們先在CPU上進(jìn)行訓(xùn)練,這次我們只進(jìn)行一次迭代:
mx.set.seed(0)
tic <- proc.time()
model <- mx.model.FeedForward.create(lenet, X=train.array, y=train.y, ctx=device.cpu, num.round=1, array.batch.size=100, learning.rate=0.05, momentum=0.9, wd=0.00001, eval.metric=mx.metric.accuracy, epoch.end.callback=mx.callback.log.train.metric(100))
## Start training with 1 devices
## Batch [100] Train-accuracy=0.1066
## Batch [200] Train-accuracy=0.16495
## Batch [300] Train-accuracy=0.401766666666667
## Batch [400] Train-accuracy=0.537675
## [1] Train-accuracy=0.557136038186157
## user system elapsed
## 130.030 204.976 83.821
在CPU上訓(xùn)練一次迭代一共花了83秒。接下來(lái)我們?cè)贕PU上訓(xùn)練5次迭代:
mx.set.seed(0)
tic <- proc.time()
model <- mx.model.FeedForward.create(lenet, X=train.array, y=train.y, ctx=device.gpu, num.round=5, array.batch.size=100, learning.rate=0.05, momentum=0.9, wd=0.00001, eval.metric=mx.metric.accuracy, epoch.end.callback=mx.callback.log.train.metric(100))
## Start training with 1 devices
## Batch [100] Train-accuracy=0.1066
## Batch [200] Train-accuracy=0.1596
## Batch [300] Train-accuracy=0.3983
## Batch [400] Train-accuracy=0.533975
## [1] Train-accuracy=0.553532219570405
## Batch [100] Train-accuracy=0.958
## Batch [200] Train-accuracy=0.96155
## Batch [300] Train-accuracy=0.966100000000001
## Batch [400] Train-accuracy=0.968550000000003
## [2] Train-accuracy=0.969071428571432
## Batch [100] Train-accuracy=0.977
## Batch [200] Train-accuracy=0.97715
## Batch [300] Train-accuracy=0.979566666666668
## Batch [400] Train-accuracy=0.980900000000003
## [3] Train-accuracy=0.981309523809527
## Batch [100] Train-accuracy=0.9853
## Batch [200] Train-accuracy=0.985899999999999
## Batch [300] Train-accuracy=0.986966666666668
## Batch [400] Train-accuracy=0.988150000000002
## [4] Train-accuracy=0.988452380952384
## Batch [100] Train-accuracy=0.990199999999999
## Batch [200] Train-accuracy=0.98995
## Batch [300] Train-accuracy=0.990600000000001
## Batch [400] Train-accuracy=0.991325000000002
## [5] Train-accuracy=0.991523809523812
## user system elapsed
## 9.288 1.680 6.889
在GPU上訓(xùn)練5輪迭代只花了不到7秒,快了數(shù)十倍!可以看出,對(duì)于這樣的網(wǎng)絡(luò)結(jié)構(gòu),GPU的加速效果是非常顯著的。有了快速訓(xùn)練的辦法,我們便可以很快的做預(yù)測(cè),并且提交到Kaggle上了:
preds <- predict(model, test.array)
pred.label <- max.col(t(preds)) - 1
submission <- data.frame(ImageId=1:ncol(test), Label=pred.label)
write.csv(submission, file='submission.csv', row.names=FALSE, quote=FALSE)
三、圖像識(shí)別應(yīng)用
其實(shí)對(duì)于神經(jīng)網(wǎng)絡(luò)當(dāng)前的應(yīng)用場(chǎng)景而言,識(shí)別手寫數(shù)字已經(jīng)不夠看了。早些時(shí)候,Google公開(kāi)了一個(gè)云API,讓用戶能夠檢測(cè)一幅圖像里面的內(nèi)容。現(xiàn)在我們提供一個(gè)教程,讓大家能夠自制一個(gè)圖像識(shí)別的在線應(yīng)用。
DMLC用在ImageNet數(shù)據(jù)集上訓(xùn)練了一個(gè)模型,能夠直接拿來(lái)對(duì)真實(shí)圖片進(jìn)行分類。同時(shí),我們搭建了一個(gè)Shiny應(yīng)用,只需要不超過(guò)150行R代碼就能夠自己在瀏覽器中進(jìn)行圖像中的物體識(shí)別。
為了搭建這個(gè)應(yīng)用,我們要安裝shiny和imager兩個(gè)R包:
install.packages("shiny", repos="https://cran.")
install.packages("imager", repos="https://cran.")
現(xiàn)在你已經(jīng)配置好了mxnet, shiny和imager三個(gè)R包,最困難的部分已經(jīng)完成了!下一步則是讓shiny直接下載并運(yùn)行我們準(zhǔn)備好的代碼:
shiny::runGitHub("thirdwing/mxnet_shiny")
第一次運(yùn)行這個(gè)命令會(huì)花上幾分鐘時(shí)間下載預(yù)先訓(xùn)練好的模型。訓(xùn)練的模型是Inception-BatchNorm Network,如果讀者對(duì)它感興趣,可以閱讀這篇文章。準(zhǔn)備就緒之后,你的瀏覽器中會(huì)出現(xiàn)一個(gè)網(wǎng)頁(yè)應(yīng)用,就用本地或在線圖片來(lái)挑戰(zhàn)它吧!
如果你只需要一個(gè)圖像識(shí)別的模塊,那么我們下面給出最簡(jiǎn)單的一段R代碼讓你能進(jìn)行圖像識(shí)別。首先,我們要導(dǎo)入預(yù)訓(xùn)練過(guò)的模型文件:
model <<- mx.model.load("Inception/Inception_BN", iteration = 39)
synsets <<- readLines("Inception/synset.txt")
mean.img <<- as.array(mx.nd.load("Inception/mean_224.nd")[["mean_img"]])
接下來(lái)我們使用一個(gè)函數(shù)對(duì)圖像進(jìn)行預(yù)處理,這個(gè)步驟對(duì)于神經(jīng)網(wǎng)絡(luò)模型而言至關(guān)重要。
preproc.image <- function(im, mean.image) {
# crop the image
shape <- dim(im)
short.edge <- min(shape[1:2])
yy <- floor((shape[1] - short.edge) / 2) + 1
yend <- yy + short.edge - 1
xx <- floor((shape[2] - short.edge) / 2) + 1
xend <- xx + short.edge - 1
croped <- im[yy:yend, xx:xend,,]
# resize to 224 x 224, needed by input of the model.
resized <- resize(croped, 224, 224)
# convert to array (x, y, channel)
arr <- as.array(resized)
dim(arr) = c(224, 224, 3)
# substract the mean
normed <- arr - mean.img
# Reshape to format needed by mxnet (width, height, channel, num)
dim(normed) <- c(224, 224, 3, 1)
return(normed)
}
最后我們讀入圖像,預(yù)處理與預(yù)測(cè)就可以了。
im <- load.image(src)
normed <- preproc.image(im, mean.img)
prob <- predict(model, X = normed)
max.idx <- order(prob[,1], decreasing = TRUE)[1:5]
result <- synsets[max.idx]
四、參考資料
MXNet是一個(gè)在底層與接口都有著豐富功能的軟件,如果讀者對(duì)它感興趣,可以參考一些額外的材料來(lái)進(jìn)一步了解MXNet,或者是深度學(xué)習(xí)這個(gè)領(lǐng)域。
|