人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

Chainerによる畳み込みニューラルネットワークの実装

Chainerによる多層パーセプトロンの実装(2015/10/5)のつづき。今回はChainerで畳み込みニューラルネットワーク(CNN:Convolutional Neural Network)を実装した。Theanoによる畳み込みニューラルネットワークの実装 (1)(2015/6/26)で書いたのと同じ構造をChainerで試しただけ。タスクは前回と同じくMNIST。

f:id:aidiary:20150626203849p:plain

今回は、MNISTデータの取得や訓練/テストの分割にscikit-learnの関数を使ってみた。

Chainerで畳み込みをするためには、訓練データの画像セットを(ミニバッチサイズ、チャンネル数、高さ、幅)の4次元テンソルに変換する必要があるここに書いてある)。今回はチャンネル数が1なので単純にreshapeで変形できる。

3チャンネルのカラー画像だとnumpyのtranspose()で4次元テンソルに変換できるみたい。transpose()は転置行列作るときに使うけどこのnumpyサンプルの3例目によるとndarrayの次元を入れ替えるときにも使えるようだ。あとで物体認識をやるときに確認しよう。

訓練時の誤差とテスト精度を描いてみると下のようになった。エポックが進むにつれて誤差が減り、学習が進んでいることがわかる。テスト精度は多少がたがたするが徐々に向上し、最大で99.3%くらいになる。今回はEarly-Stoppingのような高度な収束判定は使わず、単純に20エポック回しただけなので手を抜いている。GTX760で20エポックの学習に984秒かかった。

f:id:aidiary:20151007213443p:plain f:id:aidiary:20151007213449p:plain

学習したモデルはcPickleでファイルにダンプできる。このフォーラムの記事によると学習したモデルをファイルにダンプするときはmodel.to_cpu()でGPUからCPUに戻した方がよいとのこと。こうしておけばGPUがないマシンでも学習済みモデルを読み込める。

畳み込みニューラルネットは、学習対象の重みがフィルタに当たるので画像として描画できる。試しに学習したモデルの重みを可視化してみよう。下のようなコードで描ける。

f:id:aidiary:20151007220137p:plain

ちょっと解釈できない。ガボールフィルタみたいなのができるはずなのだけれど、ランダムだった初期状態からあまり変わらない気もする。もう少しエポック回せばよかったのかな?でも精度は十分上がったしなぁ。もっと別の例でも確認してみよう。

参考

Chainerによる多層パーセプトロンの実装

これまでDeep LearningのアルゴリズムをTheanoで実装してきた(2015/4/29)けれど、ここらで巷で大人気のライブラリChainerにも手を出してみた。Theanoの勉強を始めたあとすぐにChainerが公開された(2015/6/9)がユーザや情報が増えるまで待っていた感じ(笑)最近はコードや実験結果などを公開してくれる人が増えてきたので非常に参考になっている。目についたものはてぶに登録しているので、興味を持った手法はがしがし勉強して追試していきたい。

Chainerのバージョンは1.3.2をベースにしている。1.3からPyCUDA/scikit-cudaを独自ライブラリのCuPyに置き換えたとのことで、以前のコードは少し修正しないと動かないようだ。その分、1.3からはインストールがシンプルになっていてとてもうれしい。1.1のころは、Chainerと直接関係ないPyCUDAとscikit-cudaのところで意味不明なエラーが頻発してて泣きそうだった・・・

今回は、最初ということでChainerに付属している多層パーセプトロンのコードを動かしてみるところから始めた。そろそろ食傷気味ではあるが、MNISTの数字画像データを分類するというおなじみのタスク。

実装のデフォルトの設定は、

  • 隠れ層の数は2つ
  • 隠れ層のユニット数は1000(だけど何か気持ち悪い(笑)ので1024にした)
  • 活性化関数はReLU
  • Dropout使用
  • 最適化はAdam

となっている。自分の理解もかねてコメントを追加したコードがこちら。dataモジュールはChainer付属のものをそのままコピーした。このモジュールは自分で書かずにscikit-learnのfetch_mldata()を使うともっと簡単に書けそう。

コードはサンプルとほとんど同じだけれど、一部変えている。

  • forward()で訓練時は損失のみ返して、テスト時は精度のみを返すように変更
  • 各エポックのテスト精度をファイルに出力するコードを追加
  • 実行時間を出力

1つめの変更はIntroduction to Chainerのp.32を参考にした。前にTheanoで実装したとき(2015/6/18)も訓練時は損失のみ返し、テスト時は精度のみを返していたのでこっちのほうがしっくりくる。

CPUとGPUの切り替えはスクリプトのオプションに指定すればよい。うちのマシンはGPUが1枚しかないので0番目を使っている。最初、GPUを使うときは正の値を入れるのだと勘違いして--gpu 1としてよくわからないエラーが出てはまっていた。

python train_mnist.py          # for CPU
python train_mnist.py --gpu 0  # for GPU

CPUだと538秒、GPUだと90秒で実行できた。

ただ動かすだけだけだとつまらないので設定をいろいろ変えて収束がどのように変わるか調べてみよう。テスト精度をファイルに出力しているのでそれをプロットしてみた。すべてのグラフは横軸がエポック、縦軸がテスト精度にしている。一つだけ変えて残りはデフォルトの設定のまま。

隠れ層の数を変えたら?

f:id:aidiary:20151005220623p:plain

隠れ層の数が1のとき最大のように見える。4や5にすると明らかに(0.005なので誤差の範囲かな?)悪化している。隠れ層の数が多いとモデルが複雑化するため過学習しやすいのかもしれない。隠れ層の数が多ければ多いほどよいというものではないようだ。

隠れ層のユニット数を変えたら?

f:id:aidiary:20151005220632p:plain

隠れ層のユニット数が大きいほど収束が速い(エポックの最初の方で最高精度に達する)傾向にありそう。デフォルトの1024くらいから精度が飽和しているのでこれ以上増やしても意味がなさそうだ。

活性化関数を変えたら?

よく使われるsigmoid、tanh、ReLUの3つを比較してみた。マニュアルを見るともっとあるみたいだけど今回は省略。

f:id:aidiary:20151005220639p:plain

ReLUの収束が明らかに早い。でも秒で測った実行時間はほとんど変わらない。sigmoid(85秒)、tanh(84秒)、ReLU(89秒)。なぜか実装が単純なはずのReLUが遅かった。

Dropoutの有無

f:id:aidiary:20151005220646p:plain

Dropoutを使った方が最終的な精度が少しよいかな?という程度。もっと複雑なモデルだと効いてくるのかも。

Optimizerを変えたら?

最後にChainerで実装されているSGD(確率的勾配降下法)、AdaGrad、Adamの3種類のOptimizerを比較してみた。Optimizerのパラメータはすべてデフォルト。

f:id:aidiary:20151005220702p:plain

よく使われる確率的勾配降下法に比べてAdamは収束がすごく速い!これからはAdamを使おう。アルゴリズムはよくわかってないけど・・・

Theanoに比べるとネットワーク構造や設定が簡単に変えられるので非常に便利だと感じた。その分、更新するパラメータや更新式がブラックボックス化されていてもやもや感もある。Theanoの実装をもとにアルゴリズムをしっかり勉強して、その後にChainerで実装がよさそう。というわけでTheanoの実装もがんばって最後まで続けたい(笑)Autoencoderで止まっちゃってるな~。

参考

Machine Learning with Scikit Learn (Part IV)

Machine Learning with Scikit Learn (Part III)(2015/9/8)のつづき。5.2節ではSupport Vector Machine (SVM)が詳しく取り上げられている。

5.2 In Depth - Support Vector Machines

scikit-learnでは分類用のSupport Vector Classifierssklearn.svm.SVCと回帰用のSupport Vector Regressorsklearn.svm.SVRの2種類が用意されている。この節では主にSVCが取り上げられている。以後、SVMと言ったらSVCを指す。

SVMの識別関数

SVMの識別関数は下式のようになる。

 \displaystyle \hat{y} = sign(\alpha_0 + \sum_{j} \alpha_j y^{(j)} k(x^{(j)}, x))

ここで、(x^{(j)}, y^{(j)})はj番目の訓練データ、xはテストサンプル、kはカーネル関数を意味する。\alpha_jが訓練データから学習されるSVMのパラメータ。SVMのパラメータは訓練データと同じ数だけある。

SVMのパラメータを学習するといくつかのサンプルx^{(j)}のみ0ではない\alpha_jが得られて、その他は0になる。この0でないa_jを持つサンプルx^{(j)}サポートベクトルと呼ばれ、分類境界を決定する上で大きな役割を果たす。

SVMの識別関数の導出は以前PRMLを勉強したときにまとめた。下の記事の式(7.13)が上と同じ式。

カーネル関数

SVMはカーネル関数を変えることで挙動が変わる。scikit-learnには、

  • 線形カーネル(linear
  • 多項式カーネル(poly
  • ガウスカーネル(rbf
  • シグモイドカーネル(sigmoid

が用意されている。独自のカーネル関数を定義することもできる。デフォルトではガウスカーネルが使われる。

k(x, x') = \exp (-\gamma ||x - x'||^2)

ガウスカーネルはパラメータgammaを取り、分類境界の滑らかさを表す。gammaが小さいほど滑らかな境界が得られる。

SVMの正則化パラメータC

前回(2015/9/8)も取り上げたけどSVMではパラメータCを用いて正則化の強さを調整できる。このCはペナルティ項の重みを意味する。つまり、Cが大きいほど誤分類のペナルティが大きくなるため訓練データをなるべく分類しようとして過学習気味になる。一方、Cを小さくすると正則化が強まり、汎化性能が高まる。ただし、Cを小さくしすぎるとモデルが単純になりすぎて逆に性能が出なくなる。

ここら辺も前にPRMLでソフトマージンSVMを勉強したときにまとめた。

SVMのパラメータ

ここまでをまとめるとガウスカーネルを用いたSVMには、gammaCという2つの調整すべきパラメータがある。このチュートリアルのノートブックには、これらのパラメータによって分類曲線がどう変わるかをインタラクティブに体験できるデモがついている。

f:id:aidiary:20150909215651p:plain

上のつまみをいじるとリアルタイムにサポートベクトル(黒い枠付きの丸)と分類曲線が変わる(この記事のはスクリーンショットなのでできない)。iPython Notebookはこんなこともできるのか~面白いな。

練習問題

最後に手書き数字認識のデータを使ってSVMのパラメータをグリッドサーチで求めろという練習問題があったので解いてみた。下のような感じかな?SVCのカーネルはデフォルトがrbfなので書かなくてもOK。

実行結果は、

{'C': 10, 'gamma': 0.001} 0.991833704529
0.993333333333

となる。パラメータはC = 10 & gamma = 0.001のときが最適で、そのときのValidation Scoreは99.18%でTest Scoreは99.33%となる。SVMはニューラルネットが伸びてきたせいであまり注目されなくなった気がするけど分類精度はかなりのものだ。

Part Vにつづく。次はRandom Forest。