pythonによる深層学習[CNNフルスクラッチ編]

この記事は、このブログ内の機械学習の記事を読んでいたり、機械学習の知識をある程度持っていると読みやすいかもしれません。

また、かなり長くなってしまうので、数式の証明や意味などは特に説明しません。

深層学習とは

深層学習とは、「ニューラルネットワーク」と呼ばれる人間の脳の仕組みをコンピュータで再現するモデルを用いた機械学習のことです。

英語ではディープラーニングと呼ばれていて、人工知能ブームの真っ只中ですし聞いたことがある方が多いのではないでしょうか。

今回は、ニューラルネットワークの仕組みについて説明し、画像識別などによく用いられるニューラルネットワークであるCNNというものを紹介しようと思います。また、それをpythonで実装をしてみたいと思います。

なお、分かりやすく読んでいただけるように詳しい説明を省いたり通常の定義を噛み砕いて説明したりするので、本来の意味とずれてしまう説明がある可能性があります。何卒ご了承ください。

概要

ニューラルネットワークは、脳の動きを真似た数学モデルです。

かなり雑ですが具体的な例で説明します。

以下の写真をみてください。

f:id:palloc:20160720162710j:plain

これは何の写真なんでしょうか?

…はい、答えは猫の写真ですw

私たちの脳は、この画像を目から認識し、脳で画像に写っている耳、目、足、もふもふな毛などの特徴を処理して、これは猫だという判断をします。

脳科学についての記事でもないのでざっくりと詳細を書くと、脳は目から受け取った情報をたくさんのニューロンという細胞を通すことによって、様々な処理をしています。

ニューロンは情報を前のニューロンから受け取り、その情報が一定以上の値だったら次のニューロンに伝える(これを発火といいます)と行った感じで脳内で情報が伝達されていきます。

このニューロンからなる神経回路をコンピュータで真似できるのではといって作られたのが,ニューラルネットワークです.

そして,そのニューラルネットワークを「深く」(具体的には,4層以上,深いもので100層を超えるものも)したものがディープラーニングで,コンピュータが画像から「これは猫だ!」と判断させることができます.

仕組み

ニューラルネットワークは、全体の処理を以下の3つに分けることができます。

  • 入力層…画像や音声などを入力する場所

  • 中間層(隠れ層)…特徴を抽出する場所

  • 出力層…結果を出力する場所

入力層は、人間でいう目や耳から情報を受け取る層になります。

中間層(隠れ層)は、入力されたデータから様々な特徴、画像でいうなら形や模様などを抽出する層になります。

出力層は、中間層(隠れ層)で抽出した特徴から入力データを識別する層です。

出力層では猫の確率、犬の確率などが出てきて、その確率から入力データが猫なのか犬なのかを判断します。

以下のような感じです。

f:id:palloc:20160721102002p:plain

四角形で囲ってあるのが層で、上の○は各層でのデータです。この記事ではノードと呼ぶことにします。

前後の層のノードは層ごとに様々な形で繋がっています。

まず、最も単純なものを紹介します。

全結合層

f:id:palloc:20160721111630p:plain

これは、全結合層(Full connected layer)と呼ばれている、すべてのノードが次の層のすべてのノードに繋がっている層の事です。(以下全結合層とします)

ここで、ノード同士を繋いでいる矢印のことをエッジと呼ぶことにします。

エッジには、重みというものが付いていて、ノードを次のノードにつなげる際に使用します。

適切な重みをかけることによって、重要なノードを強く次のノードに反映させることができます。

言葉で説明しても分かりづらいので、具体例で見ていきましょう。

f:id:palloc:20160802144417p:plain

上では、入力の値が { \displaystyle
\boldsymbol{X}=(X_1,X_2,X_3)
} で、エッジについている重みが { \displaystyle
\boldsymbol{W}=(W_{11},W_{21},W_{31})
} 、次のノードの値が { \displaystyle
\boldsymbol{Y}=(Y_1,Y_2,Y_3)
} となっています。

次のノードの{Y_1} の値は、以下のように求めます。

{ \displaystyle
Y_1=\boldsymbol{X}・\boldsymbol{W}+b
}

ここで、bはバイアスと呼ばれる値を調節するためのものです。

また、全結合層の重みは各出力ノードの値ごとにベクトル \boldsymbol{W}が存在しています。これ以降すべての重みを並べた行列を \boldsymbol{W}とします。

{\displaystyle
\begin{equation}
\boldsymbol{W}=\left(
\begin{array}{cccc}
W_{11} & W_{12} & \cdots & W_{1n} \\
W_{21} & W_{22} & \cdots & W_{2n} \\
\vdots & \vdots & \ddots & \vdots \\
W_{m1} & W_{m2} & \cdots & W_{mn} 
\end{array}
\right)
\end{equation}
}

この後、この Y_1が発火するかどうかを確認するため活性化関数というものに値を通します。

活性化関数には様々な関数を用いますが、このブログでは、活性化関数をシグモイド関数とします。

シグモイド関数は、

{ \displaystyle
f(x)=\frac{1}{1+e^{\left(-x\right)}}
}

という形の関数で、一定以上の値が入力された時に一気に値が上昇します。

このシグモイド関数を通して、 Y_1が求められます。

以上が全結合層の仕組みとなります。

CNN の概要

CNNとは、Convolutional Neural Networkの略で、ニューラルネットワークの一つです。日本語では畳み込みニューラルネットワークと呼ばれています。

これは、先ほど紹介した全結合層とは別の「畳み込み層」という名前の層を用いたニューラルネットワークです。

一般的な形としては、CNNは畳み込み層、プーリング層と呼ばれる2つの層をセットで用いて、最後の出力層には全結合層を用います。

f:id:palloc:20160723175613p:plain

このブログでは、出力層に全結合層を用いる時、活性化関数はソフトマックス関数と呼ばれるものを用いることとします。

畳み込み層、プーリング層の動きについて説明していきます。

畳み込み層

畳み込み層は、カーネルと呼ばれるフィルタを端から動かしていき、カーネル内のノードが次の1つのノードにエッジで結びついています。

図にすると、以下のような感じです。

f:id:palloc:20160722000205p:plain

これは、ノードの数が4つでカーネルの大きさが3の時の例です。

式で表すと、以下のようになります。

{ \displaystyle
Y_1 = X_1 \times W_1 + X_2 \times W_2 + X_3 \times W_3 + b
}

{ \displaystyle
Y_2 = X_2 \times W_1 + X_3 \times W_2 + X_4 \times W_3 + b
}

そして、値がもとまったらそれを活性化関数であるシグモイド関数に通せば畳み込み層の処理完了です。

この層は、入力されたデータの特徴を抽出する役割を果たしています。

プーリング層

プーリング層は、プーリングサイズ(畳み込み層でいうカーネルサイズ)内のノードから最大値や平均値といった値を次のノードに渡す層です。

このブログでは、最大値を取り出す「Max Pooling」を利用することとします。

プーリング層を図で表すと、以下のようになります。

f:id:palloc:20160722012744p:plain

これは、ノード数が4でプーリングサイズが2で、2つずつずらす時の1例です。

2つの中で大きい方を採用し、次のノードとしています。

式でそれっぽく表すと、以下のようになります。

{ \displaystyle
Y_1=Max(X_1, X_2)
}

{ \displaystyle
Y_2=Max(X_3, X_4)
}

この層は、畳み込み層の後によく用いられていて、畳み込み層で抽出した特徴から強く現れている特徴を取り出す役割をします。

CNNの学習

いままで、CNNの仕組みについて説明しました。

説明の中で、最適な重みを用いることで重要なノードの情報を強く次のノードに反映させることができると言いました。

では、その最適な重みというのはどうやって決めるのか?という疑問が生まれると思います。

最適な重みを求めることを、ニューラルネットワークを学習させるといい、Backpropagation(バックプロパゲーション)という手法で行います。

Backpropagation

日本語では、誤差逆伝播法と呼ばれています。(以下BPとします)

先ほど説明したCNNは、入力から順番に計算していくので「順伝播」と呼ばれていて、誤差逆伝播法は誤差を出力層から逆に戻っていきます。

まず、適当な重みの値を設定して、学習データを入れて順伝播します。

そして、出力層で出た値と、本来欲しかった出力値のクロスエントロピー(要するに誤差)を求めます。

BPでは、この誤差を最小にするため勾配降下法という手法を用います。詳しくは最急降下法 - Wikipediaを参照して下さい。

簡単に言うと、誤差の勾配、すなわち関数の傾きがマイナスの方向に点を動かしていくことで、極小値を出そうっていう手法です。

出力層のノードの値を\boldsymbol{X},本来欲しいノードの値(確率)を\boldsymbol{Y}とすると、クロスエントロピーの勾配\boldsymbol{\Delta}は以下のような式となります。

{ \displaystyle
\boldsymbol{\Delta}=\boldsymbol{X}-\boldsymbol{Y}
}

この値を、逆伝播させていきます。(以降、勾配を \boldsymbol{\Delta}と書きます)

全結合層のBP

全結合層のBP後の新しい重み \boldsymbol{W}_{new}は、前の層の誤差の勾配 \boldsymbol{\Delta}と今までの重み \boldsymbol{W}_{old}と各ノードの値 \boldsymbol{X}を用いて、以下のように計算できます。

 {\displaystyle
\boldsymbol{W}_{new} = \boldsymbol{W}_{old} - \boldsymbol{\epsilon}・\boldsymbol{\Delta}・\boldsymbol{X}
}

\epsilonは学習係数と呼ばれているもので、一回の学習でどれくらい重みを更新するかを調整する役割があります。こいつを最適化する手法もあるのですが、それはまた別の機会に…(このブログでは0~1の固定値とします)

重みを更新したら、次の層が使うここまでの層の誤差の勾配 \boldsymbol{\Delta}_{FC}を求めます。 ノード\boldsymbol{Y}シグモイド関数微分に通し、更新前の重み \boldsymbol{W}_{old}と、\boldsymbol{Y}シグモイド関数微分に通した後のノード\boldsymbol{Y}_{d}を用いて、以下のように誤差の勾配の式が立てられます。

 \boldsymbol{\Delta}_{FC} = \boldsymbol{\Delta}・\boldsymbol{W}_{old}^T・\boldsymbol{Y}_{d}

これにより、次の層で使う誤差の勾配が用意できました。

プーリング層のBP

プーリング層のBPといっても、プーリング層自体は重みを持たないため重みの更新はありません。

次の層で用いるプーリング層での誤差の勾配は、順伝播の際に用いらなかったノードの箇所を0にして伝播してきた勾配のサイズを合わせるだけです。

畳み込み層のBP

畳み込み層のBPでは、更新後の重み \boldsymbol{W}_{new}=(W_{new}^{(1)},W_{new}^{(2)},...,W_{new}^{(n)})は、ノードの値 \boldsymbol{X}=(X_1,X_2,...,X_n)以下の式で求めます。

 {\displaystyle
\boldsymbol{W}_{new}^{(i)} =\boldsymbol{W}_{old}^{(i)} - \sum_{j=1}^{n} \Delta_j \times X_{i+j}
}

また、今回の例のニューラルネットワークでは畳み込み層が最も最初の層なので、誤差の勾配の逆伝播はこれで終了です。

一通り順伝播、逆伝播を紹介しましたが、ややこしいですよね…後でわかりやすい文献を紹介しますw

pythonによる実装

現在、ディープラーニングをするためのフレームワークがたくさん出ていて、pythonでもchainerというフレームワークなどがあり、それを用いるととても簡単にニューラルネットワークを組むことができます。

とりあえずディープラーニングフレームワークで有名なものを紹介します。

だいたいディープラーニング界隈の人は上記のフレームワークを使っています。これ以外にいいフレームワークがあれば教えて下さい。

まぁでも、せっかく仕組みが分かっていたらフルスクラッチで書きたくなりますよね。

ということでフルスクラッチで実装しました。

github.com

1*N次元のデータを扱えるCNNライブラリです。

速度出したいわけでもないのでnumpyなどの外部のライブラリを何一つ使っていません。なのでnumpyをあまり使い慣れていない方も調べながら読む必要がなく、ほとんどこのブログで紹介した数式をそのままプログラムに落とし込んだ形で実装しているのである程度読みやすいかなと思います。

READMEに各関数の説明を書いていますのでよければ参照してください。

サンプルでは、jpg形式のファイルとexeファイルの8バイト(ビットで学習させているので1*64次元)で識別器を作成しています(7/26現在)。超適当にデータは用意したので良質ではありませんが精度は92%くらいだった気がします。

おすすめ文献

Spcial Thanks

・島田さん (https://twitter.com/sheema_sheema)

この記事のレビューをしてくださって本当に有難うございました!

ではでは〜ノ