HELLO CYBERNETICS

深層学習、機械学習、強化学習、信号処理、制御工学、量子計算などをテーマに扱っていきます

活性化関数を特徴空間で見てみた【ニューラルネット基本の基本】

 

 

follow us in feedly

https://cdn-ak.f.st-hatena.com/images/fotolife/s/s0sem0y/20180604/20180604215508.png

はじめに

活性化関数のグラフを$y=f(x)$のグラフとして見るのはよくやることです。私のブログでも以下の記事でそれを取り扱っています。

   

www.hellocybernetics.tech

 

ところで、ニューラルネットワークと言えば、

$$ z = Wx $$ という線形変換と、適当な非線形の活性化関数を用いて $$ y = f(z) $$ を組み合わせて使います。もしも活性化関数を使わずに、単に線形変換を何度もするだけでは多層にする意味が無くなってしまうからです。

$$ y = W_5W_4W_3W_2W_1x $$

なんてものを考えてみたって、$W=W_5W_4W_3W_2W_1$と置いてしまえば

$$ y = Wx $$

にすぎません。線形変換を行う毎に、何らかの非線形関数が入っていることがニューラルネットワークの表現力を支えていると言えます。 ちなみに畳み込み層も見方を変えると(特殊な制限が施された)線形層を沢山並べているようなものです。

www.hellocybernetics.tech

 

なので、畳み込み層も同様に、非線形な活性化関数が間に入らなかったとすればほとんど深くする意味はなくなってしまうかもしれません。

 

今回はあまり目立たないけど、実はニューラルネットワークを支えている活性化関数を特徴空間で見てみます。

特徴空間でデータの変換を見る

データの準備

まずはデータを準備します。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets

で今回使うライブラリを揃えておきましょう。

次に、2つの2次元正規分布からデータを発生させ、それぞれラベルを付けておきます。

train_X, train_y = datasets.make_blobs(n_samples=1000,
                            n_features=2,
                            centers=[[-1,1], [1,-1]],
                            cluster_std=1,
                            )

train_Xには2次元のデータが1000個、train_yにはラベルが入ります。ちなみに今回は慣習でデータの変数名を決めていますが、別に学習を行うわけではありません。 コイツを試しに散布図で見てみましょう。

plt.scatter(train_X[:,0], train_X[:,1], cmap='bwr', c=train_y)
plt.show()

で見てみると、以下の図のようになりました。若干重なっている部分がありますね。 こうなっていると、訓練データでも正解率を100%にするのは困難かもしれません。

 

f:id:s0sem0y:20180604215508p:plain

 

今回はこのデータを使っていきます。

 

行列で変換してみる

さて、先程得られたtrain_Xを変換します。コードは簡単です。

W = np.random.randn(2,2)
trans1_X = np.dot(train_X, W)

これは、ランダムな$2\times 2$の行列でデータを変換しています。ちょうど

$$z=Wx$$

に相当する計算に成っています。こうして変換されたデータを再び見てみましょう。

plt.scatter(trans1_X[:,0], trans1_X[:,1], cmap='bwr', c=train_y)
plt.show()

f:id:s0sem0y:20180604215829p:plain

 

この変換が良い変換なのか悪い変換なのかは分かりません。直感的には重なりが増えてしまうように変換されているようにも見えます(悪かったかな?)。 ただ、ランダムで行列を決めているので仕方ありません。このまま進みましょう。

 

シグモイド活性化関数を通してみる

さて、早速活性化関数を通してみます。まずは基本的なシグモイド関数で行きましょう。

$$ y = \frac{1}{1+\exp(-z)} $$

という変換になります。この変換を行ってから特徴空間でデータがどのようになったのかを見てみます。

sigmoid_X = 1/(1 + np.exp(-trans1_X))
plt.scatter(sigmoid_X[:,0], sigmoid_X[:,1], cmap='bwr', c=train_y)
plt.show()

f:id:s0sem0y:20180604220312p:plain

さて、どうでしょうか。さっきよりも良くなったように見えますか、悪くなったように見えますか?

 

私は直感的には良くなったように見えます。

なぜなら、重なっていた部分をグッと拡大して、細かく見ているように感じるからです。 元々分離が容易だったようなデータ点は隅っこにギュッと圧縮されています。なぜこのようになったのでしょうか。活性化関数の形を是非、 s0sem0y.hatenablog.com で確認しながら考えてみてください。

ReLU活性化関数を通してみる

次はReLU関数です。これは大人気ですね。よく言われるのは成分を$0$で落としてスパースにするというものです。 さて本当でしょうか。

relu_X = np.maximum(0, trans1_X)
plt.scatter(relu_X[:,0], relu_X[:,1], cmap='bwr', c=train_y)
plt.show()

f:id:s0sem0y:20180604220756p:plain

おっと、赤いデータはy軸に、青いデータはx軸に張り付いています。分離が怪しそうなのがバラーっと散らばっていますが、上手くWを選んでいたらピシッと上手くワケられたかもしれません。

多層のパーセプトロン

ReLUを用いた2層のパーセプトロンにしてみる

以下のように新たに$2\times2$の行列を作成し、線形変換、活性化関数を介してプロットしてみましょう。

W2 = np.random.randn(2,2)
trans2_X = np.dot(relu_X, W2)
relu2_X = np.maximum(0, trans2_X)
plt.scatter(relu2_X[:,0], relu2_X[:,1], cmap='bwr', c=train_y)
plt.show()

f:id:s0sem0y:20180604221532p:plain

んーなんでしょうかこれは!?

上手くいっているのか行っていないのか分かりません。とりあえず解釈するとしたら、$x$軸に張り付いているデータたちは2個めの成分が$0$以下の値になっているためReLUで$0$に調整されたのでしょう。 一方で青いデータは何か直線的に並んでいます。こいつらは、どちらの成分も正の値を取っていたためReLUの影響を全く受けなかったデータたちです。

 

そもそも前の層で既にある程度データが直線的に並べられていた(relu_X = np.maximum(0, trans1_X))ため、それをtrans2_X = np.dot(relu_X, W2)で線形変換したところで回転拡大の具合が変わるだけです。

 

もしも都合良くW2が選ばれており、青いデータはy軸上に張り付き赤いデータがx軸上に張り付くようなことが起こったとすれば、それはW2によって、青いデータの第一成分のみが0未満に、赤いデータの第二成分のみが0未満になったということです。 上手く選べばそのような変換は可能です。試しに別のW2でもう一度やったら以下のようになりました。

f:id:s0sem0y:20180604221316p:plain

なんてことでしょうか!極めてスパースです。もやは軸にほとんど全てのデータが張り付いています。しかも、分離も上手くいきそうです!!!

 

と思いたいのですが、実際はどうか分かりません。軸にデータが張り付いてしまっており、x軸には赤いデータがたくさんありますが、よく見ると青いデータも混じっています。逆も同様です。 この部分を解決するにはもっと前の変換Wまで遡って修正を加えなければなりません。Wの時点で重なりを少なくできていれば、このようなことは起こらず済んだかもしれないからです。

 

気になる方は、WW2を何度も変えてみて調整してみてください。

もしかしたら、以下のような結果が得られることがあるかもしれません。

f:id:s0sem0y:20180604222354p:plain

これは一体何が起こったのでしょう。考えてみればすぐに分かりません。全てのデータの全ての成分が$0$未満になったため、ReLU関数で全てが原点に移動されてしまったのです。 このときのReLU関数を通す前のtrans2_Xの特徴空間は以下のようになっていました。

f:id:s0sem0y:20180604222639p:plain

 

誤差逆伝搬法

もしも上手くW_2を調整してみて分離が上手くいかなさそうなら、Wまで遡って修正を施す必要があります。 これをランダムで適当に、上手くいったらラッキーくらいで探していたらきりがありません。

 

そこで分離が上手くいっているのかを損失関数で(代理的に)評価しつつ、それを元に修正を僅かに施すことを繰り返すのが勾配法になります。

  そして、W_2に僅かに修正を施してもすぐにはうまくいかないので、その上手くいかない分を更に遡ってWに修正してもらう手続きを誤差逆伝播法と言います。

 

その手続きを数式で書くのは結構ややこしいです。以下の記事で一通り述べてはいます。

www.hellocybernetics.tech

もっとたくさん変換を続けても理屈は同じです。とりあえず変換を続けていってみて、損失関数で評価して、ダメそうならダメそうな具合を評価して修正量を後ろの方から前へ渡していきます。  

 

上記の記事の式を真面目にnumpyなどで拡張性も踏まえて実装するのはかなり難しいでしょう。 しかし一度、その一端でも触れることができればニューラルネットへの理解はかなり深まるはずです(というかココのロジックがほぼすべて、あとの細かい工夫はヒューリスティクスみたいなもの)。

ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装

ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装

はnumpyを使ってニューラルネットワークを実装していくことで勉強を進める素晴らしい書籍です。

 

また、近年話題となっているTensorFlowであるとか、PyTorch、Chainerなどのライブラリというのは、 要するにここらへんを隠蔽して、順伝播(先程記事内でnumpyで書いてた程度のこと)を記述するだけでニューラルネットワークを簡単に実装できるということで脚光を浴びています(逆に言えば、他の数値計算が得意な言語やライブラリと比べて違うのは誤差逆伝播の仕組みが予め備わっていることと、GPUが使いやすいという点が主である。これを使う=AIではない。AIになりうる可能性はあるかもしれないが)。

www.hellocybernetics.tech

www.hellocybernetics.tech

www.hellocybernetics.tech