機械学習入門(ニューラルネットワークモデル)

schedule 2022/04/07  refresh 2023/11/08

 

 

いよいよ今回で機械学習入門も最終回、ニューラルネットワークモデルです。

 

ニューラルネットワークと聞いて難しそうだなと思うかもしれませんが今までやってきたことを少し拡張するだけです。今までは元の値(X)とそれにかける重み行列(W)と確率関数(sigmoid, softmax)などと結果(Y)だけでしたがYの前に、さらに別の重み行列をかけて活性化関数(今までは確率関数を使ってた)での処理を複数追加することで数式の表現力(複雑さ)を増やしより精緻な最適化ができるようにしようというコンセプトです。

簡単に書くと今までは

 

Y = f(W・X)

 

 

例えば隠れ層が1つあると仮定すると同じようなことを2回繰り返しているだけです。(基本的に隠れ層の数を増やせば精度が上がっていくのでなるべく増やすのがいいですが計算量の費用対効果が下がっていくので計算能力と相談です。)

 

 

u = f1(W1・X)
Y = f2(W2・u) 

 

ニューラルネットワークモデル作成のステップ

 

  1. 1. モデルを定義する。
  2. 2. モデルを最適化する。
  3. 3. Pythonで実装する。

 

1.モデルを定義する。

 

入力層 x
隠れ層 u = f(a)
出力層 y = f(b)
重み行列 v, w 隠れ層の数だけ増える。
中間データ a = x @ v
b = u @ w
活性化関数 f(x), 隠れ層の数だけ増える。今回は同じもの。

 

入力層をx,隠れ層u、出力層yとしx => uの重み行列、活性化関数をV, f(x)、u => yはW, f(u)とする。
実際は隠れ層の数や層のつなげ方、どの活性化関数にするかなどはデータにより答えが変わるので試行錯誤してデータにあったモデルを探すしかありません。今回は適当

 

計算の流れはこんな感じです。

 

 

a = V・x
u = f(a)
b = W・u
y = f(b)

 

2.モデルを最適化する。

いつものように残差平方和で計算していけば楽勝…ではないです。
ここで隠れ層uの誤差ベクトルudの問題です。正解ベクトルutが与えられていないため計算ができません。
そこで学習時は通常と逆方向に計算していく誤差逆伝播とよばれるやり方でやります。ここでは導出方法は割愛しますが興味のある人は最後の補足を読んでください。

 

 

ΘL/Θwij = uj * ΘL/Θbi
ΘL/Θbi = ydi
ΘL/Θvij = xj * ΘL/Θai
ΘL/Θai = f'(ai) * Σ(ydl*wli)

 

この式を眺めてf'(ai) * Σ(ydl*wli)がudiだったらキレイだなぁと思いませんか。


そうなんです。いつものことですがなぜイコールなの?という疑問はナンセンスです。矛盾がなく計算しやすさで大きな恩恵があるから置き換えるだけです。このことにより層毎の計算方法が同じになりモデルをどんなに複雑に組んでもそれぞれの計算は比較的シンプルになります。

 

 

udi = f'(ai) * Σ(ydl*wli)

 

なので学習時の計算の流れは以下の通りです。

 

  1. 普通に計算した予測ベクトルypと正解ベクトルytから誤差ベクトルydを出す。
  2. ydがわかったのでwを使ってudを出す。ud = f'(a) * Σ(ydl*wl)
  3. ydとuからLのwの偏微分を計算する。 ΘL/Θwij = uj * ΘL/Θbi = uj * ydi
  4. udとxからLのvの偏微分を計算する。

あとはPythonでの実装です。

 

 

3.Pythonで実装

 

1. 普通に計算した予測ベクトルypと正解ベクトルytから誤差ベクトルydを出す。

活性化関数について全く説明をしておりませんでしたがこの計算方法ではシグモイドなどを使うと大きな不都合がでてきます。出力層に一番近い誤差以外では毎回活性化関数の微分をかけることになります。シグモイドの微分は過去の記事のようにf'(x) = f(x)(1 - f(x))でf(x)の最大値は1なのでその微分のf'(x)は最大で0.5 * 0.5 = 0.25となり減少のバイアスが隠れ層が多く入力層に向かうにつれて大きくなり誤差が伝わらないため学習が意味のないものになっていってしまいます。udi = f'(ai) * Σ(ydl*wli)と定義したためです。いいことばかりじゃないですね。そこで登場したのがrelu関数というもので今回はこれを使います。relu関数とはx <= 0のとき0, x > 0のときxとなる関数でその微分は傾きなのでx <= 0のとき0, x > 0のとき1になります。


もう一つ問題があります。データと中間層が同じ次元数だと単に出力層のコピーに近いものになってしまうため表現力の向上にあまり寄与しなくなってしまいます。そこで中間層の次元数は異なるものを使ってみます。

(*現在ではアルゴリズムの進化により重みパラメータにランダム要素を加えることで誤差が伝わらない問題や次元圧縮・拡張がなくても普通に特徴を表現できるようになっています。)

 

relu関数は

x * (x > 0.0)

 

reluの微分関数は

1.0 * (x > 0.0)

 

x => a、u => bの計算は

a = x @ v
b = u @ w

 

よって誤差ベクトルydは

yd = yp - yt

 

 

2.ydがわかったのでwを使ってudを出す。

ud = relu_diff(u) * (yd @ w.T)

 

3.ydとuからLのwの偏微分を計算する。

前の記事のように微分計算して誤差を反映します。

w -= l_rate * u.T @ yd / M

 

4.udとxからLのvの偏微分を計算する。

v -= l_rate * x.T @ ud / M

 

もう一つ忘れてはいけないことがあります。今までのように次元数が2や3と少ないときはいいのですが今回のように128などとすると重み行列の初期値を上手く設定しないと収束してくれません。reluを使うときにはHeの初期値というものを使うのが一般的です。以下のように正規分布のランダムな値を√{次元数}/2で割ります。


v = np.random.randn(D, H)/np.sqrt(D/2)
w = np.random.randn(H, C)/np.sqrt(H/2)

あとは全部まとめるだけです。

 

 

import numpy as np
from numpy.core.fromnumeric import argmax
from sklearn import datasets
from sklearn.model_selection import train_test_split

def onehot(y):
    return np.eye(3)[y]

def relu(u):
    return x * (x > 0.0)

def diff_relu(u)
    return 1.0 * (x > 0.0)

def pred(x,w):
    return x @ w

def accuracy_rate(v, w, x, y):
    a = pred(x, v)
    u = relu(a)
    b = pred(u, w)
    yp = relu(b)
    yp = np.eye(3)[argmax(yp, axis=1)]
    diff = (y == yp)
    correct = diff[np.where(diff == True)]
    return (correct.size / diff.size) * 100

iris = datasets.load_iris()
x = iris.data
y = iris.target
y2 = onehot(y)
train_x, test_x, train_y, test_y = train_test_split(x, y2, test_size=0.3)

# データ数
M = train_x.shape[0]
# データ次元数
D = train_x.shape[1]
# 隠れ層次元数
H = 128
v = np.random.randn(D, H)/np.sqrt(D/2)
w = np.random.randn(H, C)/np.sqrt(H/2)

l_rate = 0.01
max_run = 100

rate = accuracy_rate(w, test_x, test_y)
print(rate)

x = train_x
yt = train_y

for _ in range(max_run):
    a = pred(x, v)
    u = relu(a)
    b = pred(u, w)
    yp = relu(b)
    yd = yp - yt
    ud = diff_relu(u) * (yd @ w.T)
    w -= l_rate * u.T @ yd / M
    v -= l_rate * x.T @ ud / M

rate = accuracy_rate(v, w, test_x, test_y)
print(rate)

 

いかがでしたでしょうか。

 

機械学習がどのように発展してきたかを感じていただければ幸いです。被っているところも多く思ったよりシンプルだと感想を持たれる方も多いのではないでしょうか。

 

 

補足

本編で使った損失関数の微分結果を導出します。

 

まず以下の2式は 前の記事 を参照ください。

 

ΘL/Θwij = uj * ΘL/Θbi
ΘL/Θbi = ydi

 

次の2式がどこから来たのか考えます。

ΘL/Θvij = xj * ΘL/Θai
ΘL/Θai = f'(ai) * Σ(ydl*wli)

 

まずΘL/Θvij = xj * ΘL/Θaiを導出します。

 

ΘL/Θvij を分解してみます。

ΘL/Θvij = ΘL/Θai * Θai/Θvij

 

Θai/Θvijを考える。
Θai/Θvijのaはxベクトルとv行列の積でxを(1,2)のベクトル、vを(2,2)の行列で考えると

 

a = (x1 * (v11 + v12),x2 * (v21 + v22))

 

これをv11で微分するとx1そのほかも同様なので

 

Θai/Θvij = xj

 

代入して

 

ΘL/Θvij = xj * ΘL/Θai

 

次にΘL/Θai = f'(ai) * Σ(ydl*wli) を導出します。

 

ΘL/Θai = ΘL/Θui * Θui/Θai

 

まずΘL/Θuiを考えて
全微分という公式がありまして
例えばL(x,y,z)を全微分すると

 

dL = ΘL/Θx * dx + ΘL/Θy * dy + ΘL/Θz * dz

 

それを例えばΘuとかでわると

ΘL/Θui = ΘL/Θx * dx/Θui + ΘL/Θy * dy/Θui + ΘL/Θz * dz/Θui
         = Σ(ΘL/Θbl * Θbl/Θui)

 

ここでΘL/Θblを考えると前の記事より以下の通り。

 

ΘL/Θbl = ydl

 

またΘbl/Θuiを考えると Θai/Θvij の例と似たような感じでwliとなる。

 

Θbl/Θui = wli

 

Θui/Θaiはu = f(a)なので微分はもちろんf'(a)
それぞれ代入して

 

ΘL/Θai = f'(ai) * Σ(ydl*wli)