2016年10月18日火曜日

数学の素養がなくてもできるディープラーニング


◆ 目的
ディープラーニング(深層学習)とは、多層構造のニューラルネットワークを用いた機械学習である。ディープラーニングの代表例である、畳み込みニューラルネットワーク、CNN(Convolutional Neural Network)を使い、10種類の手書き数字を認識させる処理フローを追いかけてみる。また、TensorFlowのコードを実際にどう書いて組むかも理解する。数学に対する抵抗があったとしても難しい部分はTensorFlowが対応してくれるため、誰でもディープラーニングを試すことはできる。脱苦手意識!



◆ CNNの全体像
大きな流れは、特徴を抽出(①)し、それを元に分類(②)することである。①→②のフローが基本である。

CNNでは畳み込みフィルタとプーリング層の組み合わせで特徴を引き出す。
またこのセットを何回繰り返すかは目的によりけりである。また目的が違えば抽出方法も異なる。

識別部での分類処理では、隠れ層を例として2段にして書いているが、単層でも目的に叶う場合もあれば、より多層にする必要も出てくる。


                                 ↗︎
       畳み込みフィルタ → プーリング層 →
   ↗︎                            ↘︎
              ・               ・
input         ・               ・
              ・               ・
   ↘︎                            ↗︎
       畳み込みフィルタ → プーリング層 →
                                 ↘︎

|---------------特徴量の抽出部---------------|



      全結合層
  隠れ層   隠れ層
         ↗︎
     ○  →  ○
↗︎       ↘︎    ↘︎
→ 
↘︎                出力
         ↗︎      ↗︎
     ○  →  ○ → □ →
         ↘︎      ↘︎

↗︎       ↗︎    ↗︎
→   ○  →  ○
↘︎       ↘︎

|----------識別部----------|


◆ 識別部
先に②の識別部から覗いてみる。識別部は、入力される特徴量に基づいて分類処理を行う部分であった。識別部の役割を具体的なノード数(縦列)を当て込んで見ていくことにする。

・入力層
入力値として2つの値のペア(x0, x1)を受け取る。
このペアがデータとして複数入力されてくるわけである。
正確に書けば、n番目のデータとして、(x1n, x2n)としたほうがいいだろうか。

・全結合層
隠れ層が2つの多段ニューラルネットワーク。3ノードから成るとする。

・出力層
2次元だった入力データ(x0, x1)が3つの領域へ分けられる(3分割)。

簡易図での示しやすさを優先しているだけだり、それぞれの数に必然性はない。


入力層         結合層     出力層
          隠れ層0 隠れ層1

                 ↗︎
            ○z0 → ○z'0
      ↗︎         ↘︎     ↘︎
  x0  →                       P0
      ↘︎          ↗︎         ↗︎
            ○z1 → ○z'1 →  □ → P1
      ↗︎          ↘︎         ↘︎
  x1  →                       P2
      ↘︎          ↗︎    ↗︎
            ○z2 → ○z'2
                  ↘︎


(入力層から隠れ層0まで)
未知データを予測する数式は、与えられるデータをX、各項の係数をW、定数項をBの行列とし計算すればいいだろう。

Z = h(XW1 + B1)
z0 = h(x0w00 + x1w10 + b1)
z1 = h(x0w01 + x1w11 + b1)
z2 = h(x0w02 + x1w12 + b1)


実計算では行列でまとめる。行と列の縦横を混乱しがちになるが落ち着いて考えれば難しくない。

X =              W1 =                B1 =
[[x0, x1],       [[w00, w01, w02]     [b1, b2]
    ・             [w10, w11, w12]]
    ・
    ・    ]]

|--入力の次元数--| |--隠れ層のノード数--| |--隠れ層のノード数--|

Z =
[[z0, z1, z2],
      ・
      ・
      ・     ]]

X
入力データである。Placeholderと呼ばれる。
トレーニングやテスト用のデータとして(x0, x1)のペアが複数ある。
この入力データが最終的に3つのデータ(z0, z1, z2)に拡張される。

W
最適化するパラメータ(Weight)変数である。Variableと呼ばれる。

B
定数項(Bias)である

・h
x軸の0を境に値が1へ増加する、活性化関数である。
入力信号の変化に応じて出力が活性化するニューロンのような模式である。
以下のようなものがある。
  σ(x): シグモイド関数
  tanhx: 双曲線関数(hyperbolic function)
  relu(x): ReLU(Rectified Linear Unit, Rectifier)

TensorFlowの実コードは後半改めて見る。


(隠れ層1そして出力)
入力層から隠れ層0までと基本同じである。

違いは、隠れ層0の出力が隠れ層1の入力となり、
最後に、ソフトマックス関数を適用させる点である。
ソフトマックス関数は出力をK個に、総和は1となる、ある分類領域に所属する確率を求める関数である。
境界を境にハードに領域が変化するのではなく、ソフトに確率が変化していく。

隠れ層から出力層へはこの式で計算する。
Z' = ZW0 + b
※'はただの別識別の記号として使っているだけである。

P = softmax(Z')
z'0 = z0w00 + z1w10 + z2w20 + b0
z'1 = z0w01 + z1w11 + z2w21 + b0
z'2 = z0w02 + z1w12 + z2w22 + b0

p0 = softmax(z'0)
p1 = softmax(z'1)
p2 = softmax(z'2)


Z =              W0 =              B0 =
[[z0, z1, z2],   [[w00, w01, w02]  [b0, b1, b2]
      ・           [w10, w11, w12]
      ・           [w20, w21, w22]]
      ・    ]]
                |---出力の分割数---|

Z' =
[[z'0, z'1, z'2],
       ・
       ・
       ・       ]]

softmax関数を適用し、
P =
[[p0, p1, p2],
      ・
      ・
      ・     ]]



◆ 特徴量の抽出部
入力データから畳み込みフィルタ、プーリング層を経由させ特徴量の取り出しを行う。

・入力データ
画像ファイルとする。

・畳み込みフィルタ
画像のエッジ抽出などを行うフィルタである。
ディープラーニング専用のファンクションではない。

・プーリング層
解像度を落とす役割を担う。

早速tensolflowのコードを見ながら処理を追いかけたい。
こちらのコードを参考にさせてもらう。

先に書いたフロー図と異なり、畳み込みフィルターとプーリング層の処理セットを2つ並べている。

(1段目の畳み込みフィルターとプーリング層)
# 入力の次元数が784(画像サイズ28x28ピクセル)。
# Noneとしているのは入力数は状況に合わせて動的対応させるため。
x = tf.placeholder(tf.float32, [None, 784])

# 配列内は、[画像枚数, 画像サイズ(縦x横), レイヤ数]。
# 画像枚数はplaceholderに格納したデータ数に任せるよう-1で指示。
# レイヤ数は、複数レイヤを重ね合わせて一つの画像にしている場合に増える。
x_image = tf.reshape(x, [-1,28,28,1])

# フィルタのノード数(縦列)は32とする。
num_filters1 = 32

# 事前に抽出したい特徴が分かっていればそのフィルタを適用すればよい。
# フィルタ配列は、[フィルタサイズ(縦x横), 入力レイヤ数, フィルタ数]である。 
# ただし、手書き文字ではどういった特徴を抽出するか、それ自体が分からないため、
# フィルタ配列をVariableとし、それ自体を勾配降下法による最適化の対象にしてしまう。
# 今回はフィルタサイズを5x5とし、それを当てはめる入力画像はグレースケールの1レイヤとする。
W_conv1 = tf.Variable(tf.truncated_normal([5, 5, 1, num_filters1], stddev=0.1))

# 畳み込みフィルタの計算はconv2d関数を適用するだけである。
h_conv1 = tf.nn.conv2d(x_image, W_conv1, strides=[1,1,1,1], padding='SAME')

# 画像濃度が、ある値b_conv1より小さい場合は0として扱いたい。e.g) b_conv1 = 0.1
# 活性化関数ReLUはx軸が負の値を0にする。この性質を利用する。
# 定数項b_conv1の初期値を0ではなく0.1とするのは、誤差関数の停留値を避けるためである。
# ただし、0.1も最適かどうか分からない。よってこの閾値も最適化対象のパラーメータとするVariableとする。
b_conv1 = tf.Variable(tf.constant(0.1, shape=[num_filters1]))
h_conv1_cutoff = tf.nn.relu(h_conv1 + b_conv1)

# プーリング層である。
# 28x28ピクッッセルの画像を2x2のピクセルブロックに分解する。
# それぞれを1つのピクセルにすることで14x14ピクセルになる。
h_pool1 =tf.nn.max_pool(h_conv1_cutoff, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')


(2段目の畳み込みフィルターとプーリング層)
1段目と基本変わらない。入力と出力、ノード数が変わっているだけである。

1段目は32このフィルタがあった。2段目は64個のフィルタを使っている。
1段目でフィルタ適用され出力された32個の画像データに2段目のフィルタを当てはめ、それをレイヤ合成して1つにして完成である。2段目フィルタは64個あるため、出力も64個である。

num_filters2 = 64

W_conv2 = tf.Variable(tf.truncated_normal([5, 5, num_filters1, num_filters2], stddev=0.1))

h_conv2 = tf.nn.conv2d(h_pool1, W_conv2, strides=[1,1,1,1], padding='SAME')

b_conv2 = tf.Variable(tf.constant(0.1, shape=[num_filters2]))

h_conv2_cutoff = tf.nn.relu(h_conv2 + b_conv2)

h_pool2 =tf.nn.max_pool(h_conv2_cutoff, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')



◆ 識別部
説明が前後するが、上の特徴量の抽出ができたので、省略していた識別部のコードを見てみる。

注意点が2つ。
サンプルでは、隠れ層は1層である(最初のフロー図では2段だった)。
また、全結合層から出力時に適合させるソフトマックス関数の間にドロップアウト層を設けている。過学習(オーバーフッティング)を防止するため、一部のノードを切断することが目的である。トレーニングセットの正解率は高いが未知データへの精度の悪さを回避する処理である。


# 入力次元数(7 x 7ピクセル x フィルタ数) ※28⇒14⇒7
num_units1 = 7*7*num_filters2

# h_pool2をnum_units1個のピクセル値を一列に並べた1次元リストへ変換する
h_pool2_flat = tf.reshape(h_pool2, [-1, num_units1])

# 隠れ層のノード数
num_units2 = 1024

# 縦、横が紛らわいい。XWの行列計算思い浮かべること。
#                                     入力の次元数, 層のノード数
w1 = tf.Variable(tf.truncated_normal([num_units1, num_units2]))

b1 = tf.Variable(tf.constant(0.1, shape=[num_units2]))
hidden1 = tf.nn.relu(tf.matmul(h_pool2_flat, w1) + b1)

# ドロップアウト層である。
keep_prob = tf.placeholder(tf.float32)
hidden1_drop = tf.nn.dropout(hidden1, keep_prob)

# 最後にソフトマックス関数を適用する。
w2 = tf.Variable(tf.zeros([num_units2, 10]))
b2 = tf.Variable(tf.zeros([10]))
p = tf.nn.softmax(tf.matmul(hidden2_drop, w2) + b2)



◆誤差関数、トレーニングアルゴリズム、正解率の定義
定義した数式のパラメータの良し悪しを判断する誤差関数と、それを最小にするパラメータを決める。

コードを見てから説明する。

t = tf.placeholder(tf.float32, [None, 10])

loss = -tf.reduce_sum(t * tf.log(p))

train_step = tf.train.AdamOptimizer(0.0001).minimize(loss)

correct_prediction = tf.equal(tf.argmax(p, 1), tf.argmax(t, 1))

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))


tは正解部分にビットが立っている10次元のデータである。
下の例の一行目であれば、答えは手書きの2という数字を表す。
t =
[[0, 0, 1, 〜, 0],
       ・
       ・
       ・        ]]

次に誤差を求める。誤差関数として有名かつ直感的に理解が容易なのは最小二乗法だろうか。TensorFlowでももちろん利用できる。
loss = tf.reduce_sum(tf.square(y-t))
しかし最小二乗法では推定した関数から値を予測する確率を最大化する情報を得られない。そこで最尤(さいゆう)推定法を使っている。

誤差を最小にするパラメータを決定する処理には、勾配降下法によるトレーニングアルゴリズムを使う。学習率のディフォルト値は明示的に0.0001を使っている(AdamOptimizer部分)。
トレーニングアルゴリズムにより学習率は動的に調整されるが、ディフォルトの初期値(0.001)がふさわしいとも限らない。学習率が大きいと、パラメータの最適化処理にかかる時間が短くなるが、パラメータが収束せず発散することがある。

argmaxは配列の行、または列内での最大値を取り出す関数である。
1を指定すると横方向、つまり行単位での検索となる。
そして予測と実データの比較をしている。

最終行は、castでbool値を1、0に変換して、reduce_meanによる平均の計算で正解率を求めている。


0 件のコメント:

コメントを投稿