俵言

しがない社会人が書く、勉強とかゆるふわプロコンのこと

【jupyterで学ぶ】 ゼロから作るDeep Learning - 第2回:3章(その1)パーセプトロンとニューラルネットワークの違い

はじめに

第1回を上げてから四日ぐらいで早くも第2回を上げられるのを嬉しく思います。まあ単に3章を分割することに決めたから早く上げれたってだけなのですが..

やっぱり「ある程度まとまった量で上げたい」とか考えるとズルズルとずれていって結局闇に飲まれていくと思うんですよね(下書きで残っている記事たちを見つつ..)。なので、章を一気にとかじゃなくてある程度切りのいい所で記事を書く形にしようと思います。そのほうが勉強の速度も上がるしね!

そんな感じで今回も「ゼロから作るDeep Learning」を勉強していきます。

3章 ニューラルネットワーク

2章にはパーセプトロンのことが載っていましたが、3章はニューラルネットワークのお話です。
個人的には「パーセプトロンの方が単純らしい」ぐらいの印象を持ってたんですが、よくよく考えると両者がどう違うのかは知りませんでした。(広義にはパーセプトロンニューラルネットワークに含まれそうですが。)この章ではその話が出てきます。

3章は「まとめ」を合わせて7節あるのですが、大体以下のような流れです。

というわけで、今回はその1の活性化関数のお話です。大げさに言えば「遂に明らかになる、パーセプトロンニューラルネットワークの違い!」って感じでしょうか。
因みに前回の終わりで言っていた様に、3章ではNN(※めんどくさいので以降略記します)の学習方法の話が出てきません。3章でNNの動作の仕方を学んだ後、4章で学習方法を学ぶとのことです。では本題に入りましょう。

(その1)パーセプトロンとNNの違い

2章ではパーセプトロンの層を増やすと表現力が増していくことを述べた。しかしそのためには、人間が適切な重みを選ばなければならなかった...。NNはこの問題を解決できる。すなわち、データから適切な重みを学習できるという重要な性質を持っているのだ..

みたいなこと(※表現はこの通りではありません)が3章の冒頭に書かれているのですが、3章では学習の話が出てきません。この章では、パーセプトロンとNNの大きな違いとして活性化関数について述べられています。これがどうやら学習に関わってくるらしいのですが、それは4章までのお楽しみみたいですね。

活性化関数とは?

ここまで何度も名前だけ出てきた活性化関数ですが、入力の(重みつき)総和を出力に変換する関数のことを言うそうです。wikipediaさん曰く伝達関数とも呼ばれるとか。入力信号に対してニューロンがどう活性化するか(発火する)かの度合いを表す関数ともいえます。

2章で出てきた2入力のパーセプトロン


\displaystyle y =
\begin{cases}
\displaystyle 0 \ (b + w_1 x_1 + w_2 x_2 \leq 0) \\
\displaystyle 1 \ (b + w_1 x_1 + w_2 x_2 \gt 0) \\
\end{cases}

という風に、入力の線形和が0より大きいかどうかで発火する(y=1になる)か決めていました。この機能を式で書くと、


\displaystyle h(x) =
\begin{cases}
\displaystyle 0 \ (x \leq 0) \\
\displaystyle 1 \ (x \gt 0) \\
\end{cases}

となります。この h(x)こそが、パーセプトロンの活性化関数です。ちなみにこの関数は、閾値(ここでは0)で出力が切り替わることからステップ関数とか階段関数と呼ばれます。
これを用いると、一番最初の式が

 \displaystyle y = h(b + w_1 x_1 + w_2 x_2)

と簡単に書けます。更に、入力の線形和を  a = b + w_1 x_1 + w_2 x_2 と置けば、

 \displaystyle y = h(a)

と更にシンプルになります。(「何でわざわざ置いたの?」って話ですが、詳しくは次回で。)

パーセプトロンとNNの違いは何なのか

活性化関数を説明したところで、本題であるパーセプトロンとNNの違いの話に入ります。

実のところ、両者の大きな違いは活性化関数の違いだけだそうです。正直意外だったのですが、つまるところノード(ニューロン)のつながり方や入力信号の伝わり方にほとんど違いはなくて、入力に対してどう出力(発火)するかだけが違いらしい。非常にシンプルですね。

というわけで、この後は両者の違いをもたらしている活性化関数の違いを見ていきます。
NNでよく用いられるのは以下に示す(標準)シグモイド関数 です。

 \displaystyle h(x) = \frac{1}{1 + \exp(-x)}

うん、これだけ見てもよくわかりません。というわけで実際に実装してプロットしてみます。
ステップ関数、シグモイド関数の実装は以下の通りです。

import numpy as np

## ステップ関数.入力xはベクトル
def step_function(x):
    y = x > 0
    return y.astype(np.int)

## シグモイド関数.入力xはベクトル
## a = 1 のときに標準シグモイド関数と呼ばれる
def sigmoid(x, a = 1):x
    y = 1 / (1 + np.exp(-a*x))
    return y

注目すべき点は、(これだとわかりにくいですが)入力  x はベクトル(numpy.ndarrayで表現)だということ。関数の中身だけ見ると、for文も見当たらないのでまるで xスカラー値の様に思えます。しかし実際は xはベクトルであり、step_function xの各要素に対して計算を行い、ベクトルである出力 y を返してくれます。いやーpython最高ですね!本でもこの点を強調していました。

ちなみに、>+などの二項演算子、よく使うexpsqrt,logなどの関数はnumpy.ndarrayに対して定義されているため、上の実装のようにかなり直感的に使うことができます。でも、「自作のややこしい関数を各要素に対して適用したい!」なんてときはどうするんでしょうか..?

この疑問は一旦置いておいて、とりあえずプロットをします。

# 必要なものをimport
import numpy as np
from matplotlib import pyplot as plt


# 作成した step_function, sigmoidをimport
from my_functions import step_function, sigmoid

# 図示して比較する
## 準備
plt.rcParams["font.size"] = 20 # ラベルの文字を大きく
fig, (ax_sf, ax_sigm) = plt.subplots(1,2, figsize = (20,10), sharex = True, sharey = True)
x = np.arange(-5,5,0.01)

## ステップ関数
### 描画
sf_x = step_function(x)
ax_sf.plot(x, sf_x) 

### ラベルなど
ax_sf.set_title("step function")
ax_sf.set_xlabel("x"); ax_sf.set_ylabel("step_function(x)")
ax_sf.set_xlim(-6,6); ax_sf.set_ylim(-0.1,1.1) 

## シグモイド関数
### 描画
sigm_x = sigmoid(x)
ax_sigm.plot(x, sigm_x) 

### ラベルなど
ax_sigm.set_title("sigmoid function")
ax_sigm.set_xlabel("x"); ax_sigm.set_ylabel("sigmoid(x)")

## 保存
fig.savefig("step_sigmoid.png")

f:id:Tawara:20161025231727p:plain

これでもいいんですが、違いを際立たせるために重ねて図示してみます。

## ちょっとわかりにくいので、同じ図に出す
### 図が一つなのでsubplotsを使う必要はないが、サイズを指定したいので使ってみる
plt.rcParams["font.size"] = 12
fig, ax = plt.subplots(1,1,figsize = (5.5,5.5))

### ステップ関数は破線、シグモイド関数は実線で表示
ax.plot(x,sf_x, color='#ff0000', linestyle='dashed', label = "step")
ax.plot(x,sigm_x, color='#0000ff', label = "sigmoid")

### ラベルとか
ax.set_title("step function and sigmoid function")
ax.set_xlim([-6,6]); ax.set_ylim([-0.1,1.1])
ax.set_xlabel("x"); ax.set_ylabel("y")
ax.legend(loc = "lower right")

## 保存
fig.savefig("step_sigmoid_overwrap.png")

f:id:Tawara:20161025232000p:plain

ぱっと見ると、「ステップ関数をとても滑らかにしたものがシグモイド関数」みたいな印象を受けます。
実際両者の大きな違いは、ステップ関数は連続関数でない(閾値を境に0から1に変化)がシグモイド関数連続に変化するところです。これが学習に大きく関わってくるらしい..。単調で連続 ... 逆関数でも作るんですかね?まあ4章を楽しみにしておきましょう。

連続性が大きな違いの両者ですが、見た目からもわかるように結構似ています。共通点として

  • どちらも入力が小さければ値が0に、大きければ値が1になる
  • ステップ関数(繋がっていない折れ線)もシグモイド関数(曲線)も非線形な関数である

が上げられます。

で、この非線形がめっちゃ重要だってことが本の中で語られてます。

非線形性の重要性

なぜ活性化関数が非線形なことが重要なのか?それは、もし活性化関数が線形関数だった場合、NNを多層にする意味が無くなってしまうからです。

例として、仮に活性化関数が

 h(x) = c x + d

のような線形関数であった場合を考えます。問題を単純にするために一層あたりのノードを一つとして層を三つ通ったときを考えると、出力は活性化関数を三回重ねがけした

 \displaystyle h \left( h \left( h \left(x \right) \right) \right)
= h \left( h \left( cx + d \right) \right)
= h \left( c^2 x + c d + d \right)
= c^3 x + c^2 d + c d + d

となります。一見複雑そうに見えるんですけど、 c' = c^3, d' = c^2 d + c d + d とおくと

 \displaystyle h \left( h \left( h \left(x \right) \right) \right) = c' x + d'

と書けてしまう。三回重ねがけしても関数の形が一回の場合とまったく同じなので、多層にする(隠れ層を用意する)意味が無くなっちゃうんですね。だって係数(c,d)を変えちゃえば一層で表現できるってことですし。これにはなるほどなあーと思いました。この記事でもっとも大事なとこ上げろって言われたら間違いなくここだと思います。

ReLU関数

この章では、NNでよく使う活性化関数はシグモイド関数だという話が出ました。シグモイド関数は古くからNNで用いられてきたそうです。
しかしながら最近は、ReLURectified Linear Unit)関数(日本語で正規化線形関数)がよく用いられるそうです。定義は以下の通り。


\displaystyle y =
\begin{cases}
0 \ (x \leq 0) \\
x \ (x \gt 0) \\
\end{cases}

つまり、0以下なら0を、そうでなければ xをそのまま返す関数です。こいつが使われる理由も多分後々語られる..はず..。

せっかくなのでステップ関数、シグモイド関数、ReLU関数を図示してみましょう。

ReLU関数の実装。

import numpy as np
def relu(x):
    y = np.maximum(x, 0)
    return y

例によってnumpy.maximumもベクトル xの各要素に対して作用しています。さて、三者をプロットしましょう。

# 必要なものをimport
import numpy as np
from matplotlib import pyplot as plt

# 作成した step_function, sigmoid, reluをimport
from my_functions import step_function, sigmoid, relu

sf_x = step_function(x)
sigm_x = sigmoid(x)
relu_x = relu(x)

plt.rcParams["font.size"] = 12
fig, ax = plt.subplots(1,1,figsize = (10,5))

### ステップ関数は破線、シグモイド関数は実線、ReLU関数はドット+破線で描画
ax.plot(x,sf_x, color='#ff0000', linestyle='dashed', label = "step")
ax.plot(x,sigm_x, color='#0000ff', label = "sigmoid")
ax.plot(x,relu_x, color='#00ff00',  linestyle= "dashdot",label = "ReLU")
### ラベルとか
ax.set_title("step, sigmoid and ReLU")
ax.set_xlim([-6,6]); ax.set_ylim([-0.1,6])
ax.set_xlabel("x"); ax.set_ylabel("y")
ax.legend(loc = "upper left")

## 図を保存
fig.savefig("step_sigmoid_ReLU.png")

f:id:Tawara:20161026004655p:plain

思ったより見にくくなってしまった..。はっきりわかるのは、ReLUだけ値がどんどん大きくなるってことですね。これが多分よい効果をもたらすから使われていると思いますが、それについてはどこかで説明があるでしょう。

今回のまとめ

こんなところで3章(その1)は終了です。「いやまだ何も始まってないやん!」って感じが正直ありますが、まあ仕方ない(^^;)

今回のポイントは

  • パーセプトロンとNNの大きな違いは活性化関数の違いである
    • 活性化関数の連続性が鍵
  • 活性化関数が非線形だからこそ、NNを多層にする意味がある

といったところ。途中でも言いましたが、最後の話はいわれたらわかる話ですけどとっても重要だと思ってます。

さて次回は多層NNを実際に実装してみるお話です。python(numpy)の便利さが遺憾なく発揮される回になると思います。そしてコードの占める量も増えそう笑
いつになるかはわかりませんが、ペース落としたくないので出来るだけ今週中に上げるつもりで頑張ろうと思います。ではでは。

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

おまけ

ステップ関数とかの実装のときに「自作の関数をベクトルの各要素に適用したいときにどうすればいいの?」って話をしたんですが、調べたらやっぱり同じ要望を持つ人はいるみたいです。

stackoverflow.com

この記事の中では「結局順番に関数適用するしかないけど、どの方法が一番コスト(時間など)が低いのか」って話してました。方法としては

  • 単純にfor文でnumpy.ndarrayから要素を順番に取り出して関数適用
  • numpy.fromiterを用いる
  • numpy.factorizedを用いる

の3つが上げられていました。「データ量によって効率性が違う」とか「この方法が一番いい」とか意見がいくつかあって、結局どれ使っていいのかはいまいちわかんないですが。 それと、if-elseを組み込まないといけない関数はnumpy.whereを使うとよいとの話がありました。実際にこれらを使って実装してみると、

import numpy as np
import math

def step_function(x):
    y = np.where(x > 0, 1, 0)
    return y

def sigm(xi, a):
    return 1 / (1 + math.exp(-a*x))

def sigmoid(x, a = 1):
    y = np.fromiter((sigm(xi,a) for xi in x), x.type)
    return y

def relu(x):
    y = x.where(x > 0, x, 0)
    return y

このように書けます。関数をモジュール化して見やすくしたいならfromiterのようなものを使うのはありですね、関数が複雑すぎる場合は特に。whereもかなり便利だと思います。

今回は一次元の配列(ベクトル)に対して関数を適用することを考えたんですが、行列も似たような要望があると思います。まあそのうち調べようかな。