多層パーセプトロンを書いてみる

前回は単純パーセプトロン(SLP)でAND, ORを学習し、XORは学習に失敗することを確認しました。今回は多層パーセプトロン(Multi Layer Perceptron, MLP)を使ってXORも含めて学習してみます。内容的には高卒でもわかる機械学習第3回以降に相当します。

相変わらず定義がいいかげんなまま記号を使ったりするのでお察しください^^;

今回は入力層、出力層に加えて1つの隠れ層を設けます。バイアス抜きで入力層のユニット数2, 隠れ層のユニット数2, 出力層のユニット数1とします*1。今回も出力側のみ値を {-1,1} の形式にします。

         in  hidden  out
bias      o     o

{0,1} --> o     o

{0,1} --> o     o     o --> {-1,1}

言い換えると、出力層の活性化関数をこのように定義します:

{ \displaystyle
f^{(2)}(u) = \begin{cases}
   1 & (u^{(2)} \ge 0) \\
  -1 & (u^{(2)} < 0)
\end{cases}
}

今回は隠れ層もあるのでここにも活性化関数を定義する必要があります。これを  f^{(1)}(u) と書くことにします。この関数の定義は任意ですが、XORを学習するためには非線形関数でなければなりません。線形関数だと出力層のユニット値は結局入力の線型結合にしかならないため、線形分離不能なXORは学習できないためです。このことは後に実際に試してみます。

今回も訓練データ1件ごとのオンライン学習とし、誤差関数を以下のように定義します(SLPのときと同様。微分ができるならこれで問題ないはず):

{ \displaystyle
E = max(0, -yu^{(2)})
}

u(2) は出力層のユニット値(w(2)*o(1)), y は教師データです。重みの更新は不正解時のみ行うので、実質的には

{ \displaystyle
E = -yu^{(2)}
}

と考えて問題ありません。これが全ての重みについて微分できるかどうか見ていきます。

まず重み行列の形を確認しておきます。入力層から隠れ層への重みを W(1), 隠れ層から出力層への重みを W(2) とすると、以下のようになります:

{ \displaystyle
W^{(1)} = \begin{bmatrix}
  w^{(1)}_{1,0} & w^{(1)}_{1,1} & w^{(1)}_{1,2} \\
  w^{(1)}_{2,0} & w^{(1)}_{2,1} & w^{(1)}_{2,2}
\end{bmatrix} \\
W^{(2)} = \begin{bmatrix}
  w^{(2)}_{1,0} & w^{(2)}_{1,1} & w^{(2)}_{1,2}
\end{bmatrix}
}

出力側の W(2) から考えてみます。簡単のため1要素についてのみ考えます:

{ \displaystyle
\frac{\partial E}{\partial w^{(2)}_{1,0}} =
\frac{\partial E}{\partial u^{(2)}} \frac{\partial u^{(2)}}{\partial w^{(2)}_{1,0}} =
-yo^{(1)}_0
}

よって出力層の重みについては微分可能です。次は隠れ層の W(1) について考えます。やはり1要素についてのみ考えると:

{ \displaystyle
\frac{\partial E}{\partial w^{(1)}_{1,0}} =
\frac{\partial E}{\partial o^{(1)}_1} \frac{\partial o^{(1)}_1}{\partial u^{(1)}_1} \frac{\partial u^{(1)}_1}{\partial w^{(1)}_{1,0}}
}

この3項を個別に求めると:

{ \displaystyle
\frac{\partial E}{\partial o^{(1)}_1} =
\frac{\partial E}{\partial u^{(2)}} \frac{\partial u^{(2)}}{\partial o^{(1)}_1} =
-yw^{(2)}_{1,1} \\
\frac{\partial o^{(1)}_1}{\partial u^{(1)}_1} = f'^{(1)}(u^{(1)}_1) \\
\frac{\partial u^{(1)}_1}{\partial w^{(1)}_{1,0}} = x_0
}

よって、全て代入して

{ \displaystyle
\frac{\partial E}{\partial w^{(1)}_{1,0}} = -yw^{(2)}_{1,1}f'^{(1)}(u^{(1)}_1)x_0
}

となり、隠れ層の重みについても微分可能です。これで勾配がわかるので、後は勾配降下法により学習可能です。

ということでPythonコードを書いてみました。今回は重みを乱数で初期化し、隠れ層の活性化関数と学習率をコマンドラインから与える方式にしています。シグモイド関数を指定し、学習率を 0.1 としたときの出力例を示します:

#------------------------------------------------------------
# Learning "AND" (eta=0.1)
w1 = [[-0.11631637 -0.32964966 -0.76916355]
 [ 0.05914025 -0.94074692  0.49658873]]
w2 = [ 0.4131751  -0.8584504  -0.27154784]

# Test "AND"
[1 0 0] -> -1
[1 0 1] -> -1
[1 1 0] -> -1
[1 1 1] -> 1

#------------------------------------------------------------
# Learning "OR" (eta=0.1)
w1 = [[-0.12387904  0.52193623 -0.35688955]
 [-0.25589149 -1.01165379 -0.52477719]]
w2 = [ 0.15433217  0.06677338 -0.56990398]

# Test "OR"
[1 0 0] -> -1
[1 0 1] -> 1
[1 1 0] -> 1
[1 1 1] -> 1

#------------------------------------------------------------
# Learning "XOR" (eta=0.1)
w1 = [[ 0.26898265  0.64685031 -1.14389473]
 [-2.09612791 -1.51971218  1.74802215]]
w2 = [-0.69779446  0.9496701   1.01464362]

# Test "XOR"
[1 0 0] -> -1
[1 0 1] -> 1
[1 1 0] -> 1
[1 1 1] -> -1

今回はXORも正しく学習できています。なお、隠れ層の活性化関数として恒等関数(identity)も選べますが、これは線形関数なのでXORは正しく学習できません。前述の記事の第3回でXORはOR, NAND, ANDの組み合わせで実現できると述べられていますが、これはORとNANDの出力がステップ関数などの非線形関数を通したものだからということです。

実は今回はSLPのときと違って色々ハマり所があります。まず、全ての重みを0初期化すると隠れ層の活性化関数が何であってもXORは正しく学習できなくなります。これは全ての重みが0だと実質SLPと変わらなくなるためだと思われます。隠れ層の重みが全て0なので隠れ層のユニット値は全て0になり、出力層の重みも全て0なので、隠れ層の2ユニットの重みが毎回同じように更新されてしまい、実質1ユニットと変わらなくなってしまいます。ということで今回は重みを乱数で初期化しています。

これに伴い、学習成功率が100%ではなくなっています。実験した結果シグモイド関数で学習率0.1なら安定して学習できるように見えますが、活性化関数や学習率を変えると結構学習失敗が起こります(AND, ORすら学習できないこともあります)。そもそも収束判定を行っていないのと、重みを初期化する際の乱数の範囲が -1.0 から 1.0 なので場合によっては非常に0に近い値で初期化されることなどが原因なのかなぁと思いますが、正直よくわかってません。ちゃんと理論を勉強しなさいというのが結論かなと思いますが、ひとまずハマり所はある程度実感できたということで。

*1:前述の記事の第3回にある通り、XORはOR, NAND, ANDの組み合わせで実現できるので、隠れ層のユニット数は2で足りるはず。