MXNetを中心としたCustom Loss functionの話

これはDeepLearning Advent Calendar 2016の12月24日分の記事です。

Deep Learning一般の内容ではないので申し訳ないですが、来年はMXNet Advent Calendarやりたいですねという願いもあり、空き枠に滑り込みました。

TensorFlowとChainerが強くてMXNetは肩身が狭いですが、AWSという錦の御旗も得たのでユーザ増加に期待したいです。 MXNetはドキュメントが弱いとよく言われますが、最近はドキュメントサンプルコードも充実してきています。 9月頃はドキュメントがリンク切れしておりサードパーティーのドキュメントを読んでいたくらいなので随分良くなりましたよ。

さて、今回はCustom Loss functionについて紹介しようと思います。機能自体は前からあったようですが今月になって簡単な資料が公開され、一般ユーザにも存在が知られるようになったものです。

ロス関数について

ここでは、モデル出力値とラベルの差を表現する関数で、勾配法によって最小化する対象をロス関数と呼びます。 どのライブラリでも回帰なら2乗誤差、分類ならクロスエントロピーが使われているはずです。

注意が必要なのは、モデル精度の最終的な評価をするための指標とロス関数は別になるケースもあるということです。 分類問題ではクロスエントロピーよりもAccuracyやAUCで精度評価することが多いと思いますが、そういった場合でもロス関数にはクロスエントロピーが選ばれてクロスエントロピーを最小化するのが通常です。

評価指標をAUCに変えたら、それを最適化してくれると思っているユーザは結構多く、私もそういった方への案内誘導をボランティアでやっています。*1

一方で、ロス関数に拘ったからといって報われるとは限らず、絶対誤差で評価されるタスク等でも2乗誤差を最小化する方が上手くいくケースも多いのが実際と思われます。

ごく限定的な状況ですが、標準のロス関数では対応できないような問題のためにロス関数をカスタムするインターフェースが各ライブラリから提供されています。

MXNetでの事情

MXNetのLinearRegressionOutputSoftmaxOutputといった出力層向けのシンボルは、出力とロス関数が一体化しておりパラメータでロス関数を変更できません。 ロス関数を変えたい場合はMakelossというシンボルを利用する必要があります。

回帰の例

LinearRegressionOutputを使った通常の方法は以下のようになります。これはMSEを最小化します。

data  = mx.sym.Variable('data')
label = mx.sym.Variable('label')
fc    = mx.sym.FullyConnected(data, num_hidden=3)
act   = mx.sym.Activation(fc, act_type='relu')
fco   = mx.sym.FullyConnected(act, num_hidden=1)
net1  = mx.sym.LinearRegressionOutput(fco, label)

同じようにMSEの最小化をCustom Loss functionでやると以下のようになります。

loss = mx.sym.square(
    mx.sym.Reshape(fco, shape=(-1,)) - label
)
net2 = mx.sym.MakeLoss(loss)

mx.sym.squareやelement-wise minusの導関数はMXNetが知っているので自動微分を使って上手く最小化してくれる訳です。 numpy等を使って好き勝手にロス関数を作ることはできません。

注意 MakeLossにmx.sym.squareした結果を渡すとpredictした際にも2乗した結果が出力されるそうです。

custom loss symbol in R/Python · Issue #3368 · dmlc/mxnet · GitHub

確かにどれが出力として求められているかはMXNetで判断しにくいかもしれません。これについては使い易いように修正がされるそうです。

分類の例

MXNetのLogisticRegressionOutputは名前のような挙動をしないことが知られています。

RPubs - MXnet LogisticRegressionOutput test

SoftmaxOutputを使うことが推奨されていますが、これは手前のレイヤーで隠れユニット数をカテゴリ数と同じだけ確保する必要があります。 2値分類のときに隠れユニットを2つ持っておくのは冗長な気がするので、Custom Loss functionでロジスティック回帰を組んでみようというのがこの試みです。

SoftmaxOutputを使った通常の方法は以下のようになります。これはLog Lossを最小化します。

data  = mx.sym.Variable('data')
label = mx.sym.Variable('sm_label')
fc    = mx.sym.FullyConnected(data, num_hidden=3)
act   = mx.sym.Activation(fc, act_type='relu')
fco   = mx.sym.FullyConnected(act, num_hidden=2)
net1  = mx.sym.SoftmaxOutput(fco, name='sm')

ロジスティック回帰らしく書いた場合は以下のようになります。

data  = mx.sym.Variable('data')
label = mx.sym.Variable('label')
fc    = mx.sym.FullyConnected(data, num_hidden=3)
act   = mx.sym.Activation(fc, act_type='relu')
fco   = mx.sym.FullyConnected(act, num_hidden=1)
p     = mx.sym.Activation(fco, act_type='sigmoid')
eps   = 1e-6
p     = mx.sym.minimum(mx.sym.maximum(p, eps), 1.0-eps)
q     = 1.0 - p
lp    = mx.sym.Reshape(mx.sym.log(p), shape=(-1,))
lq    = mx.sym.Reshape(mx.sym.log(q), shape=(-1,))
loss  = label * lp + (1.0 - label) * lq
net2  = mx.sym.MakeLoss(loss)

出力を確率に直すやり方がわからないので、ちゃんと動いているかわからないのですが、 方針自体はこれで良いはず…

中途半端になってしまいましたが、これ以上掘るのは効率が悪いので、本日はここまでとします。 手元のJupyter Notebookはこちらです。

github.com

やっぱり

以前に比べればドキュメントが強化されているとはいえ、最近どういう機能が追加されたか等を個人で調べるのは大変です。 ユーザが増えて互助していければ素敵ですね。

*1:こんなことは働き盛りの人間がやる仕事ではないので、ボット化したいなぁ