MGLによる2クラスロジスティック回帰

人工知能に関する断創録 - Kerasによる2クラスロジスティック回帰

音声でもとても参考にさせていただいたこちらのブログで今度からKerasの連載をするらしく、まず最初に最もシンプルな例としてロジスティック回帰の例が紹介されていたので、これをMGLでもやってみる。とはいえMGLに二値のクロスエントロピーがなかったのでマルチクラスのロジスティック回帰を2クラスに適用しているところがちょっと違う。

コード(Gist) mgl-logistic-regression.lisp

必要ライブラリの読み込み

mgl-userはMGLを使うためのユーティリティ集というか、サンプルコードを集めたもの。データの正規化などもここで定義してある。
データがCSVなのでfare-csvで読み込んでparse-numberでパースする。clgplotgnuplotのフロント。mgl-userとclgplotはQuicklispから入らないのでlocal-projects以下に置く必要がある。

(ql:quickload :mgl-user) ; https://github.com/masatoi/mgl-user
(ql:quickload :fare-csv)
(ql:quickload :parse-number)
(ql:quickload :clgplot) ; https://github.com/masatoi/clgplot

データの読み込みと正規化

データ: https://raw.githubusercontent.com/sylvan5/PRML/master/ch4/ex2data1.txt
参考エントリと同様にPRMLのデータを読み込む。fare-csv:read-csv-fileで読み込むと文字列のリストになっているのでparse-numberで数値に直す。それからデータ点の構造体(datum)のベクタにする。

(in-package :mgl-user)

;;; データの読み込み
(defparameter data-list
  (mapcar (lambda (line)
            (mapcar #'parse-number:parse-number line))
          (fare-csv:read-csv-file "/home/wiz/tmp/ex2data1.txt")))

(defparameter dataset
  (let ((dataset (make-array (length data-list))))
    (loop for i from 0 to (1- (length data-list))
          for line in data-list
          do
       (setf (aref dataset i)
             (make-datum
                  :id i :label (nth 2 line)
                  :array (make-mat 2 :initial-contents (subseq line 0 2)))))
    dataset))

データの各次元の平均が0、標準偏差が1になるようにデータを正規化する。まずdatasetのコピーを作って、そのコピーを破壊的に正規化する関数dataset-normalize!を呼ぶ。

;;; データの正規化
(defparameter dataset-normal (copy-dataset dataset))
(dataset-normalize! dataset-normal)

データの可視化

正規化したデータをプロットする。

(defun plot-dataset (dataset)
  (let ((positive-data (remove-if-not (lambda (datum) (= (datum-label datum) 1)) dataset))
        (negative-data (remove-if-not (lambda (datum) (= (datum-label datum) 0)) dataset)))

    (clgp:plot-lists
     (list (loop for datum across positive-data collect (mref (datum-array datum) 1))
           (loop for datum across negative-data collect (mref (datum-array datum) 1)))
     :x-lists (list (loop for datum across positive-data collect (mref (datum-array datum) 0))
                    (loop for datum across negative-data collect (mref (datum-array datum) 0)))
     :style 'points)))

(plot-dataset dataset-normal)


ロジスティック回帰モデルの定義

隠れ層なしで入力層から直接出力層に接続するようなネットワークになる。活性化関数はシグモイド関数とする。

;;; モデル定義
(defparameter fnn-sigmoid
  (build-fnn (:class 'fnn :max-n-stripes 100)
    (inputs (->input :size 2))
    (f1 (->sigmoid inputs))
    (prediction (->softmax-xe-loss (->activation f1 :name 'prediction :size 2) :name 'prediction))))

出力層の活性化関数はソフトマックス関数で、出力次元が2次元になるところが元エントリとは異なる。

図にするとこんな感じで、2つのクラスへの所属確率というか、信頼度みたいなものを出力する。この信頼度は常に合計1になり、この値が高い方のクラスに分類されたと解釈する。

訓練

次のようにすると訓練が始まる。モデルが破壊的に変更され、学習済のモデルが返るが、表示は何もない。

(train-fnn-process fnn-sigmoid dataset-normal :n-epochs 100)

途中経過を出力したい場合はこのようにする。第三引数は本当ならテストデータを指定して、エポックが変わるタイミングで訓練データとテストデータでの正答率が表示される。

(train-fnn-process-with-monitor fnn-sigmoid dataset-normal dataset-normal :n-epochs 100)

出力はこのようになって、100エポック回すと最終的に91%くらいの正答率になる。

(中略)
2016-10-31 18:35:40: ---------------------------------------------------
2016-10-31 18:35:40: training at n-instances: 10000
2016-10-31 18:35:40: test at n-instances: 10000
2016-10-31 18:35:40: pred. train bpn PREDICTION acc.: 91.00% (10000)
2016-10-31 18:35:40: pred. train bpn PREDICTION xent: 3.456e-3 (10000)
2016-10-31 18:35:40: pred. test  bpn PREDICTION acc.: 91.00% (100)
2016-10-31 18:35:40: pred. test  bpn PREDICTION xent: 3.456e-3 (100)
2016-10-31 18:35:40: Foreign memory usage:
foreign arrays: 0 (used bytes: 0)
CUDA memory usage:
device arrays: 114 (used bytes: 400,112, pooled bytes: 0)
host arrays: 0 (used bytes: 0)
host->device copies: 202, device->host copies: 20,602

最適化はモーメンタムSGDでやっているが、他にADAMなども指定できる。その辺の指定はtrain-fnn-processなどの定義の中でやっているので、細かな調整をしたいときはこれらの定義をいじる必要がある。

重みを見てみる

clumpsなどでモデルの中身を覗くことができる。例えばこのようにしてバイアスと重みを表示してみる。

(let* ((f1-activation (aref (clumps fnn-sigmoid) 2))
       (bias (aref (clumps f1-activation) 0))
       (weight (aref (clumps f1-activation) 2)))
  (describe bias)
  (describe weight)
  (list bias weight))
#<->WEIGHT (:BIAS PREDICTION) :SIZE 2 1/1 :NORM 2.76652>
  [standard-object]

Slots with :INSTANCE allocation:
  NAME               = (:BIAS PREDICTION)
  SIZE               = 2
  NODES              = #<MAT 1x2 AF #2A((1.956225 -1.9562262))>
  DERIVATIVES        = #<MAT 1x2 A #2A((0.0 0.0))>
  DEFAULT-VALUE      = 0
  SHARED-WITH-CLUMP  = NIL
  DIMENSIONS         = (1 2)
#<->WEIGHT (F1 PREDICTION) :SIZE 4 1/1 :NORM 4.55551>
  [standard-object]

Slots with :INSTANCE allocation:
  NAME               = (F1 PREDICTION)
  SIZE               = 4
  NODES              = #<MAT 2x2 AF #2A((-2.4052894 2.4102905) (-2.1375985 2.1420684))>
  DERIVATIVES        = #<MAT 2x2 A #2A((0.0 0.0) (0.0 0.0))>
  DEFAULT-VALUE      = 0
  SHARED-WITH-CLUMP  = NIL
  DIMENSIONS         = (2 2)

(#<->WEIGHT (:BIAS PREDICTION) :SIZE 2 1/1 :NORM 2.76652>
 #<->WEIGHT (F1 PREDICTION) :SIZE 4 1/1 :NORM 4.55551>)

これらの中のNODESが上の図で示したユニット間の矢印に対応している。

予測をプロット

次のようにしてネットワークの出力を可視化してみる。

(defun plot-prediction (dataset fnn class)
  (let* ((min-x1 (loop for x across dataset minimize (mref (datum-array x) 0)))
         (max-x1 (loop for x across dataset maximize (mref (datum-array x) 0)))
         (min-x2 (loop for x across dataset minimize (mref (datum-array x) 1)))
         (max-x2 (loop for x across dataset maximize (mref (datum-array x) 1)))
         (x1-list (loop for x from min-x1 to max-x1 by 0.1 collect x))
         (x2-list (loop for x from min-x2 to max-x2 by 0.1 collect x)))
    (clgp:splot-list
     (lambda (x1 x2)
       (mref (predict-datum fnn
                            (make-datum :id 0 :label 0d0
                                        :array (make-mat 2 :initial-contents (list x1 x2))))
             class))
     x1-list x2-list :map t)))

(plot-prediction dataset-normal fnn-sigmoid 0)
(plot-prediction dataset-normal fnn-sigmoid 1)

クラス毎の信頼度を出してプロットしてみたのが次の図になる。


データのプロットと見比べてみると、決定境界のあたりに信頼度0.5が来ていることが分かる。
同じ方法でマルチクラスへ拡張できる。