背景
MXNet とは
Deep learning のフレームワークで、Kaggler の間で流行っている(流行っていた?)らしい。対応している言語が豊富で、その中に R も含まれているので、R 使いの人でも deep learning できます。分散処理前提の h2o と比べてローカルマシンで走らせても速く実行できるようなので、R でお手軽に deep learning したい場合にはいいんじゃないかなあと思っています。
以下のページを読んでもらえれば雰囲気は掴めると思います。
- Deep Learningライブラリ{mxnet}のR版でConvolutional Neural Networkをサクッと試してみた(追記3件あり)
- Deep Learningライブラリ「MXNet」のR版をKaggle Otto Challengeで実践してみた
DMLC によるチュートリアル記事もあります。
この記事のモチベーション
一応、GitHub に autoencoder の公式サンプルがあるのですが、
Python な上に、汎用性を持たせようとした結果なのかたくさんクラス作っていたり自前で学習処理を書いていたりして読むの辛い……もっと限定的でよいからサクッと動かせるコードないのか。
(見つから)ないので自分で書きました。
# Autoencoder もうあんまり使わないんですかね……
ここで扱う課題と方針
課題
MNIST のデータセットを使って、手書き数字の判別問題を解きます。
ここでは MXNet のチュートリアルに合わせて Kaggle からダウンロードしたデータを使います。
方針
以下のように、次元を半分ずつ減らしていって、最後に10クラス分類を行います。
784次元(28px x 28px)
--[全結合, ReLU]-> 392次元
--[全結合, ReLU]-> 196次元
--[全結合, ReLU]-> 98次元
--[全結合, ReLU]-> 49次元
--[全結合, softmax]-> 10次元(10クラス)
次元を半分に減らす部分を autoencoder で作成し、その結果を結合して最後に fine-tuning します。
MXNet の使い方のポイント
以下の2点がわからなくて個人的に苦戦しました。
- 部分的なネットワークをどう取り出すのか?
- # autoencoder からエンコーダを取り出すところ
- 取り出したネットワークを組み合わせるにはどうすればよいのか?
- # エンコーダを積み上げて10クラス分類器を作るところ
MXNet で Autoencoder を作ってみる
パッケージの読み込みとシード固定
R のシード固定とは別に、MXNet 用のシード固定があります。おそらく MXNet 用で固定しないと、C で書かれている部分のシードが固定されないのではないかと思います(要確認)。
library(mxnet)
# mxnet関連のシードを固定する
mx.set.seed(0)
データセットを読み込む
Kaggle のデータセットは rowmajor 形式(行がサンプルで、列が変数)になっていますが、MXNet では colmajor 形式(行が変数で、列がサンプル)が一般的らしいので、そちらに合わせて転置しています。RDBMS 的には rowmajor が普通だと思うので、ちょっと違和感……(他のフレームワークをよく知らないので、deep learning 界隈では colmajor が当たり前なのか MXNet が特殊なのかわかりませんが)
# KaggleからダウンロードしたMNISTデータを読み込む
rawdata <- read.csv("train.csv", header = T)
rawdata <- as.matrix(rawdata)
# 訓練データとテストデータに分ける
train.index <- sample(x = 1:nrow(rawdata), size = 30000)
train <- rawdata[train.index, ]
test <- rawdata[-train.index, ]
# データとラベルに分ける。転置しているのは、colmajor形式にするため
train.x <- t(train[, -1]/ 255)
train.y <- train[, 1]
test.x <- t(test[, -1]/ 255)
test.y <- test[, 1]
Autoencoder を作る
Autoencoder を定義して、学習を行います。ここはほぼチュートリアルのままです。
# nrow(input)次元のデータをhidden_size次元に圧縮するautoencoderを作る
pretrain <- function(input, hidden_size)
{
input_size <- nrow(input) # colmajor
# autoencoderを定義する
symbol <- mx.symbol.Variable("data")
symbol <- mx.symbol.FullyConnected(data = symbol, name = "encoder", num_hidden = hidden_size)
symbol <- mx.symbol.Activation(data = symbol, name = "encoder_act", act_type = "relu")
symbol <- mx.symbol.FullyConnected(data = symbol, name = "decoder", num_hidden = input_size)
symbol <- mx.symbol.LinearRegressionOutput(data = symbol, name = "output")
# 学習する
model <- mx.model.FeedForward.create(
symbol = symbol,
X = input, y = input,
ctx = mx.cpu(),
num.round = 10, array.batch.size = 100,
optimizer = "sgd", learning.rate = 0.01, momentum = 0.9,
initializer = mx.init.Xavier(rnd_type = "uniform", factor_type = "in", magnitude = 2),
eval.metric = mx.metric.rmse,
batch.end.callback = mx.callback.log.train.metric(10),
array.layout = "colmajor")
return(model)
}
エンコードする
次の層の学習を行うために、今の層の出力値を計算します。pretrain
で定義したネットワークからデコーダを削除し、エンコーダだけからなるネットワークを再定義します。そこに学習済みパラメータを適用して、エンコーダを作成します。MXNet のモデルはネットワーク構造と学習済みパラメータのリストとして表されるので、学習を行わない場合は mx.model.FeedForward.create
を使わなくてもリストを作るだけで作成できます。
# 次の層の学習を行うために、今の層の出力値を計算する
encode <- function(input, model)
{
# 学習済みパラメータを取り出す
arg.params = model$arg.params[c("encoder_weight", "encoder_bias")]
# エンコーダを定義する
symbol <- mx.symbol.Variable("data")
symbol <- mx.symbol.FullyConnected(data = symbol, name = "encoder", num_hidden = ncol(arg.params$encoder_weight))
symbol <- mx.symbol.Activation(data = symbol, name = "encoder_act", act_type = "relu")
# 学習済みパラメータを適用し、エンコーダを作成する
model <- list(symbol = symbol, arg.params = arg.params, aux.params = list())
class(model) <- "MXFeedForwardModel"
# エンコードする
output <- predict(model, input, array.layout = "colmajor")
return(output)
}
各層の学習を行う
上で定義した pretrain
と encode
を使って、各層の学習を行います。
# 各層の学習を行う
input.1 <- train.x
model.1 <- pretrain(input = input.1, hidden_size = 392)
input.2 <- encode(input = input.1, model = model.1)
model.2 <- pretrain(input = input.2, hidden_size = 196)
input.3 <- encode(input = input.2, model = model.2)
model.3 <- pretrain(input = input.3, hidden_size = 98)
input.4 <- encode(input = input.3, model = model.3)
model.4 <- pretrain(input = input.4, hidden_size = 49)
10クラス分類器を定義する
各 autoencoder からデコーダを取り除いたネットワークを積み上げて、10クラス分類器を定義します。
# 各エンコーダを結合し、10クラス分類器を定義する
symbol <- mx.symbol.Variable("data")
symbol <- mx.symbol.FullyConnected(data = symbol, name = "encoder_1", num_hidden = 392)
symbol <- mx.symbol.Activation(data = symbol, name = "encoder_act_1", act_type = "relu")
symbol <- mx.symbol.FullyConnected(data = symbol, name = "encoder_2", num_hidden = 196)
symbol <- mx.symbol.Activation(data = symbol, name = "encoder_act_2", act_type = "relu")
symbol <- mx.symbol.FullyConnected(data = symbol, name = "encoder_3", num_hidden = 98)
symbol <- mx.symbol.Activation(data = symbol, name = "encoder_act_3", act_type = "relu")
symbol <- mx.symbol.FullyConnected(data = symbol, name = "encoder_4", num_hidden = 49)
symbol <- mx.symbol.Activation(data = symbol, name = "encoder_act_4", act_type = "relu")
symbol <- mx.symbol.FullyConnected(data = symbol, name = "affine", num_hidden = 10)
symbol <- mx.symbol.SoftmaxOutput(data = symbol, name = "output")
次に、各 autoencoder の学習済みパラメータを取り出し、10クラス分類器用に名前を付け替えます。名前は、重みは <レイヤ名>_weight
、バイアスは <レイヤ名>_bias
になるみたいです。名前でレイヤとパラメータを紐づけているようで、名前が違うと「パラメータが見つかりません」的なエラーが出るので注意してください。
# 学習済みパラメータを取り出す
arg.params <- list()
arg.params <- c(arg.params, "encoder_1_weight" = model.1$arg.params$encoder_weight)
arg.params <- c(arg.params, "encoder_1_bias" = model.1$arg.params$encoder_bias)
arg.params <- c(arg.params, "encoder_2_weight" = model.2$arg.params$encoder_weight)
arg.params <- c(arg.params, "encoder_2_bias" = model.2$arg.params$encoder_bias)
arg.params <- c(arg.params, "encoder_3_weight" = model.3$arg.params$encoder_weight)
arg.params <- c(arg.params, "encoder_3_bias" = model.3$arg.params$encoder_bias)
arg.params <- c(arg.params, "encoder_4_weight" = model.4$arg.params$encoder_weight)
arg.params <- c(arg.params, "encoder_4_bias" = model.4$arg.params$encoder_bias)
新たに追加したレイヤのパラメータについては、自前で初期化を行います。すべてのパラメータを初期化する場合には mx.model.FeedForward.create
の引数に指定した initializer
が初期化してくれるのですが、今回のように部分的に初期化を行いたい場合には自前で初期化を行う必要があるようです(mx.model.FeedForward.create
の実装を見ると、一応 initializer
で初期化するのですが、arg.params
が指定されていた場合にはそのオブジェクトで丸ごと置き換えるという処理になっていました。そのため、arg.params
には必要なすべてのパラメータを初期化した状態で格納しておかなければならないようです)。
# 新たに追加したパラメータについては、初期化を行う
arg.params <- c(arg.params, "affine_weight" = mx.rnorm(c(49, 10), 0, sqrt(2 / 49), mx.cpu()))
arg.params <- c(arg.params, "affine_bias" = mx.nd.zeros(10, mx.cpu()))
# なんとなくバイアスは0で初期化しましたが、普通どうやるんでしょうか……?
Fine-tuning を行う
最後に、通しで改めて学習を行います。
# fine-tuningを行う
model <- mx.model.FeedForward.create(
symbol = symbol,
X = train.x, y = train.y,
ctx = mx.cpu(),
num.round = 10, array.batch.size = 100,
optimizer = "sgd", learning.rate = 0.01, momentum = 0.9,
eval.metric = mx.metric.accuracy,
batch.end.callback = mx.callback.log.train.metric(10),
array.layout = "colmajor",
arg.params = arg.params, aux.params = NULL)
予測結果を確認する
# 予測結果を確認する
score <- predict(model, test.x, array.layout = "colmajor")
label <- max.col(t(score)) - 1
table(test.y, label)
label
test.y 0 1 2 3 4 5 6 7 8 9
0 1165 0 10 4 0 3 3 2 1 0
1 0 1373 3 2 0 0 1 1 0 0
2 4 4 1180 6 5 0 0 3 0 1
3 0 2 11 1212 0 2 0 5 0 4
4 1 5 6 1 1105 0 5 3 0 3
5 5 3 1 76 1 944 5 2 4 6
6 14 9 12 0 9 9 1150 1 2 0
7 1 7 15 0 0 2 0 1214 0 0
8 2 23 30 66 0 13 3 8 1008 4
9 6 11 1 21 24 3 0 41 2 1106