いぬおさんのおもしろ数学実験室

おいしい紅茶でも飲みながら数学、物理、工学、プログラミング、そして読書を楽しみましょう

画像認識でトラブった話。出力層のユニットの値があまり変化しない

 (Pythonのライブラリなどを一切使わない)画像認識の実験の中間報告です。

www.omoshiro-suugaku.com

CIFAR-10を使って画像認識の実験をしていましたが、途中までは快調だったんですがつまずきました。いろいろがんばって光明が見えたので報告です。1人で書籍だけに頼って勉強していて、ぼくと同じ状況に陥って困っている人がいるかも知れません。ネットで調べようとしても似たケースが見つからず、「これは難しいかも……」と思っていました。ライブラリを使うならこんな苦労はないのかも知れませんが、ハマったところをある程度書いておきます。備忘録でもありますし、フルスクラッチでコーディングしている誰か、同じ目に遭っている人が助かるかも!! なお、ぼくはC#でやっています。

 CIFAR-10は10種類の画像(車、トラック、蛙、鳥、……)の画像認識の実験用セットです。車や鳥など、それぞれ様々の画像、合計5万枚がトレーニング用に、学習がうまくいったかのテスト用に1万枚が含まれています。ぼくは畳み込み層、プーリング層の組を2回、その後全結合を2回使ったネットワークで実験していました。大量のデータを扱うプログラムで、デバッグがしづらい印象です。正しくコーディングできているのか不安なんですね。「今まで正答率が90%だったのが50%に下がった」とかなら「あ、どこかでミスしたかな」と分かるんですが、試しに動かしても10%くらいだと「コーディングは正しいが学習の仕方が間違っている」のか「ロジックやコーディングに誤りがあって、このままではいくらがんばってもダメ」なのかどっちだろう、などということになります。これについてはうまい方法があり、詳細は省略しますが誤差逆伝播法と(偏微分の)定義通りの計算方法で得られる数値に違いがあるかないか見てやればよいのです。この方法を「勾配確認」と表現しているサイトがありました。今回の実験では、これによるとコーディング自体には間違いはなさそうでした。

 CIFAR-10の実験は写真をパソコンに見せて「これは鳥」などと答えさせるのが目的ですから、写真のデータをネットワークに流して、写真に応じて10通りの正しい答えが得られればよいのです。ぼくがハマったのは「写真によらず、常に『鳥』という答えが出る」という現象です。ネットワークのパラメータ(重み、バイアス)の初期値によって答えは変化しますが、不思議なことに初期値を変えなければどんな写真を使っても同じ答えが出るのです! ぼくはとりあえず写真のデータ(各点が0~255の整数値)を0~1の実数値に変換し、これをネットワークに入力しています(データの白色化など、やるべきことはあるのでしょうが最初の実験なので省略!!)。ネットワークの出力層では10個のユニットがあり、写真に応じて10個のユニットが様々な値になります。これが、写真を変えると変化はするのですがその変化が例えば小数第4位以降くらいだけなのです。つまり「3番目のユニットが常に最大値」などとなるのです!! その結果、どんな写真を見せても「鳥」などと同じ答えが返ります。そもそもこういう現象が起こること自体が不思議です。ネットワークの構造からして、重みやバイアスが一定でも写真が変われば出力はもっと変わりそうな気がするんですが……。しかもこの現象は中間層が2層(全結合を2回)だけでも起こります。層が多いとおかしな現象が起こったときに原因の切り分けがしにくいですから、こうして小規模のネットワークで試すのがよいですよね。数学的に一種の「収束」に近いことが起こってこうなるのかも……みたいなことも考えました。CIFARでうまくいかないのなら……と、前に成功しているMNISTでも試しましたが、結果は変わりませんでした。前にはうまくいったのに!!

 ぼくはプログラムを書くとき、開発日記をつけることにしています。日付と時刻、その日にどこまで進んだ、何を勉強した、……と開発作業に関わることを何でも書きます。ふと思いつき、前にMNISTで実験したときの開発日記を見てみたら……何と!! 同じ現象が起こっていました。やはり「勾配確認」は無事にパス、しかし写真によらず同じ答えが出てしまう、という状況だったのです。すっかり忘れていました。幸いにしてうまくいったときのネットワークの構造や重み、バイアスの初期値、学習率などが記録してあり、その値を使ってみました。中間層は2層(全結合を2回)のネットワークです。そのままではやはりうまくいかなかったのですが(当時と細かな部分で構造などが違うからでしょう……)、少し試行錯誤した結果、認識率が68%になりました。パラメータを書いておきます。学習率は0.05、重みとバイアスの初期値は-0.1~0.1の乱数を与えました(本には「バイアスの初期値は0か1の定数を」のように書いてあったんですが……)。活性化関数はLReLU(x > 0 ならxを、そうでないなら0.01xを返す)を使いました。前回の実験ではその後、畳み込み層やプーリング層を入れて、最終的に96%とかの認識率になりましたから、多分今回も大丈夫でしょう!! ……と信じたい……。これでうまくいったら、データをCIFARに切り替えて試します。

 そうそう、MNISTは28×28ドットの数字の画像で、CIFAR-10は32×32ドットです。CIFARでやっていたときには32×32ドット固定でコーディングしていて(^_^;)、MNISTに切り替えたときにはちょうど真ん中にデータを置きました(32×32ドットの画像の一番外側2ドット分は0で埋める)。どうかな……と思ったんですが、認識率68%なので多分問題ないのでしょう。ここで活性化関数にsigmoid関数を使うと同じ現象が起こるので、結局パラメータと活性化関数の選び方の問題だった、ということになりそうです。

 結構時間もエネルギーも費やしていたので、「うまくいかなかた」という結論だったらショックも大きかったんですが、よかった……。

 

追記:

その後、[畳み込み層、プーリング層]×2、全結合層2、1万データで学習させたら84.62%になりました。チャンネル数はどの層も1。やはり方向としては誤っていないと思います。しかし5万データで学習させても85.63%でした。実は1データ毎に重みとバイアスを更新しています。開発日記では、前はバッチサイズ50でやっていたことになっています。そうすればもっとよくなるでしょう。

 

追記2:

MNISTのデータはもともと1チャンネルです。これを1回目の畳み込みで3チャンネルに、2回目の畳み込みで6チャンネルにしてみました。1万データの学習で、正答率は93.86%に。やはり1データ毎のパラメータ更新なのですが、結構うまくいくんですね。

 

追記3:

追記2で4万データにしてみても95.5%。読み損ないの数字を見ていませんが、よほど判別しづらい字なのかも。それとも1データ毎の更新だとこの辺が限界なのでしょうか。