人工知能に関する断創録

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

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。

Machine Learning with Scikit Learn (Part III)

Machine Learning with Scikit Learn (Part II)(2015/8/26)のつづき。今回は、Part IIの動画の5.1節の内容を簡単にまとめた。書いてたら長くなったので5.1節だけ。

5.1 In Depth - Linear Models

回帰のための線形モデルの詳細が取り上げられている。線形回帰は過学習(Overfitting)に弱い。過学習を回避するために正則化を導入したリッジ回帰LassoElasticNetの使い方が事例をもとに説明されている。また、線形分類器の正則化の例としてSVMとロジスティック回帰のパラメータCが説明されている。

線形回帰

まずは、単純な線形回帰から。線形回帰は、特徴量の線形結合で表されるモデル。

 \hat{y} (w, x) = w_0 + w_1 x_1 + \cdots + w_p x_p

ここで、(w_1, \cdots, w_p)が係数(coef_)でw_0が切片(intercept_)と呼ばれる。線形回帰では、教師データ (x, y) から係数と切片を学習するのが目的。線形回帰はPart I(2015/8/10)の動画でも取り上げられていたけど使うのは簡単

線形回帰は特徴量に比べてサンプル数が少ないと過学習しやすく、精度が大幅に悪化するという欠点がある。横軸にサンプル数、縦軸に精度が来る学習曲線を描いてみるとよくわかる。

f:id:aidiary:20150907205903p:plain

訓練データ数が特徴量の数(50)より少ないとき、訓練データのスコア(点線)に比べてテストデータのスコア(実線)が非常に低く、明らかに過学習していることがわかる。訓練データ数が十分あればテストスコアはどんどん改善して過学習は解消されていることもわかる。訓練データ数が少なくて過学習するときは以下に述べる様々な正則化(regularization)テクニックが使える。

リッジ回帰(ridge regression)

線形回帰にL2正則化 (L2 regularization)を加えたのがリッジ回帰。線形回帰のコスト関数に係数の2乗和の項が加わることで係数が小さくなるようなバイアスがかかる。正則化の強さは複雑度パラメータalphaで変えられる。最適なalphaはCross Validationで求められる。これを自動的にやってくれるRidgeCVが用意されている。

先と同様に学習曲線を描いてみる。

plot_learning_curve(linear_model.LinearRegression(), X, y)
plot_learning_curve(linear_model.Ridge(alpha=1.0), X, y)
plot_learning_curve(linear_model.RidgeCV(), X, y)

f:id:aidiary:20150907211415p:plain

とりあえずテストスコアだけ見てみる。訓練データ数が少ないところでは線形回帰(LinearRegression)に比べてリッジ回帰(Ridge)の精度がずっとよいことがわかる。さらにCross Validationで最適なalphaを求めたRidgeCVはさらにスコアがよくなっている。

Lasso

線形回帰にL1正則化(L1 regularization)を加えたのがLasso。線形回帰のコスト関数に係数の絶対値の和の項が加わる。Lassoは多くの係数が0になる疎な解が得られやすいという特徴がある

本当にそうなるのかやったことなかったので試してみよう。

出力は、

[ 1.22   2.363 -2.554 -0.972 -0.596 -0.334 -1.1    0.615 -1.215 -0.895
 -0.911  1.076  2.087 -1.524  0.333 -0.301 -1.239 -1.167 -2.765 -0.917
  0.734  0.709 -0.362 -0.84  -0.779 -0.78  -0.425 -0.029 -0.317  0.903
 -0.793 -0.221  1.691  0.329 -0.689 -1.755  0.302  0.232  1.095  2.925
  0.702 -1.634  0.643 -0.976 -0.38   0.144 -0.734  0.861 -0.173 -0.238]

[ 0.     1.235 -1.587 -0.049 -0.    -0.    -0.     0.    -0.    -0.    -0.
  0.     0.922 -0.275  0.    -0.    -0.066 -0.    -1.755 -0.     0.     0.
 -0.    -0.    -0.    -0.    -0.    -0.    -0.     0.153 -0.    -0.     0.248
  0.    -0.    -0.532  0.     0.     0.     1.859  0.    -0.125  0.    -0.
 -0.    -0.    -0.     0.    -0.    -0.   ]

上がリッジ回帰の係数で下がLassoの係数。確かにLassoを使うと多くの係数が0になることがわかる。どうしてこうなるかはPRMLにも書かれていたけどいまいち納得できていない・・・

LassoにもRidgeCVと同じくパラメータのCross ValidationまでやってくれるLassoCVが用意されている。

plot_learning_curve(linear_model.RidgeCV(), X, y)
plot_learning_curve(linear_model.LassoCV(n_alphas=20), X, y)

結果は、

f:id:aidiary:20150907213405p:plain

となる。テストスコアを比べるとリッジ回帰よりLassoの方がよりスコアが高くなることがわかる。これはたまたまかも。何回か試すとスコアが逆転することもある。リッジ回帰とLassoはどうやって使い分ければよいのだろう?

ElasticNet

最後にリッジ回帰とLassoの中間に位置するElasticNet。コスト関数にL1正則化とL2正則化の項を重みづけして加えただけの手法。

ElasticNetはl1_ratioというパラメータを取る。l1_ratio = 0.0にするとL2ペナルティのみになりRidgeと等価になる。逆にl1_ratio = 1.0にするとL1ペナルティのみになりLassoと等価になる。

同様にElasticNetCVでCross Validationできる。ただし、最適化されるパラメータはl1_ratioではなくalphaのようだ。l1_ratioの最適化はまた別にCross Validationを回す必要がありそう。

plot_learning_curve(linear_model.RidgeCV(), X, y)
plot_learning_curve(linear_model.ElasticNetCV(l1_ratio=0.6, n_alphas=20), X, y)
plot_learning_curve(linear_model.LassoCV(n_alphas=20), X, y)

f:id:aidiary:20150907214446p:plain

上の例ではl1_ratioを0.6で固定している。スコアもRidgeとLassoの中間くらいになる。

RidgeとかLassoとかElasticNetって名前わかりにくい感じがした。名前を見てもどんな手法かまったく想像がつかない・・・ElasticNetはニューラルネットの仲間かと思ってたし。名前の由来は何だろうね?

余談だけどビッグデータ時代でもこういう正則化テクニックはまだ重要なのだろうか?先にも書いたけど訓練データが山ほどあれば正則化を使わなくても過学習は解消される。大量のデータがあっても教師あり学習のラベル付けの手間はかかるからまだこういう技術が必要なのだろうか。

線形分類器の正則化

線形回帰ではなく、線形分類器の正則化の例としてSVMとロジスティック回帰が取り上げられている。これらのアルゴリズムでは、パラメータCによって正則化を調整できる

svm = SVC(kernel='linear', C=C).fit(X_train, y_train)

Cを変えたときの分類境界を描画すると下のようになる。

f:id:aidiary:20150908221042p:plain

Cが小さいほど正則化が強まり、過学習が解消され、汎化性能が高まる。逆にCを大きくすると正則化が弱まり、過学習気味になっていく。上の例では左上の赤丸の扱いがわかりやすい。Cを大きくしていくとこの赤丸を無理やり分類しようとする様子が見て取れる。すべての訓練データをきちんと分類する傾向が高まるため過学習気味と判断できる(過学習の特徴は訓練データの精度が高く、テストデータの精度が低くなる)。

線形SVMだとあまり面白くないが、5.2節でカーネルを使った非線形SVMが取り上げられている。非線形SVMだとパラメータCによる正則化の効果が非常によくわかる。

ロジスティック回帰も正則化を制御するのにCパラメータがある。共役勾配法によるロジスティック回帰のパラメータ推定(2014/4/15)でやった。このときはパラメータCと呼んでなかったが、正則化項の重みがCにあたる。

ロジスティック回帰の最適なCをグリッドサーチで探索してみよう。

出力は

0.966666666667
{'C': 0.01}

となる。最適なCは0.01でそのときの分類精度は96.7%になる。

Part IVにつづく。