社畜エンジニア発掘戦線

駆けだしAIエンジニア

①線形回帰問題(データフィッティング)

第一問


とっつきやすいように高校数学のような形式の問題にしてみました。PYTHONISTA3でコードを書きながら解いていってみましょう。

設問1.ガウシアンノイズを付加したy = 3xに準ずるデータセット(インプット:x、正解ラベル:t)を作成せよ

ガウシアンノイズの乗せ方はいったん置いといて、y=5xのデータを作ってみましょう。まず、numpyとmatplotlibをインポートします。

import numpy as np
import matplotlib.pyplot as plt

次に、y = 3xを描くためのインプット:xをセットします。ランダムに0から2の範囲でサンプル数は20にしましょう。

num_of_sam = 20
x = np.random.uniform(0, 2, num_of_sam)

最後に、y = 3xを書いて終わりです。ただし、今回は正解ラベルをtとしているのでt = 3xとしましょう。

t = 3*x

では、この正解ラベルにガウシアンノイズを乗せます。正規(ガウス)分布に沿った「平均0、標準偏差0.2」のノイズをサンプル数だけ生成します。このノイズを先ほどのt = 3xに足しましょう。

noise = np.random.normal(0, 0.2, num_of_sam) 
t = 3*x+noise

これで設問1は終了です、グラフ化してみましょう。「'o'」はおっきい丸をプロット、という意味です。

plt.plot(x, t, 'o')
plt.show()

ノイズの平均や標準偏差を調整するとデータのばらつき具合が変わるので確認してみてください。ちなみに、xの値はプログラムを走らせるごとに与えられる乱数なので、毎回同じプロットにはなりません。
f:id:sutokun:20190203125702p:plain

設問2.初期パラメータ:重み(w)を1として、出力を計算せよ

さて、本題です。出力を求めるにはニューラルネットワークを設計しなければいけません。線形問題に関しては「入力層」と「出力層」のみで構築できます。これさえ分かっていれば、今回の対象となる関数はy=3x(線形)なのでこの設計像が浮かび上がります。
f:id:sutokun:20190126143346j:plain

非常にシンプルですね、今回の重み(w)は1です。この重みとは線形回帰では直線の「傾き」を表します。つまり、出力はこの重みと入力の積:y = wxとなります。この計算が「順伝搬」にあたります。

w = 1
y = w*x

では、x、t、yのデータ形状を確認してみましょう。

print(y.shape) #(20,)
print(x.shape) #(20,)
print(t.shape) #(20,)

すべて一致しているはずです。今回の問題はシンプルなので「あたりまえやん」となりますが、今後ニューラルネットワークが複雑化するとこれらのデータ形状が行列化します。きちんと入力、正解ラベル、出力の型を整理しておかないと、途中で型が合わずに計算ができなくなることが多々あります、こうなったら頭が爆発します。気にかけておいてください。

設問3.二乗和誤差関数を損失関数(E)として、Eの値を求めよ

損失関数とは、得られた出力yが正解ラベルtとどれくらい離れているか(誤差:損失量)を計算するための関数です。よく用いるものとして「二乗和誤差関数」、「クロスエントロピー」の2種類があります。今回は二乗和誤差関数を用いて損失量を計算します。ちなみに、広義で損失関数をコスト関数とも呼びます。コスト関数とは損失関数に過学習を防ぐ対策が施された関数を指します。過学習とは字の通り、勉強し過ぎてゴールを見失うことです、人間でも勉強しすぎてぶっ飛んじゃう方いますよね。。


二乗和誤差関数:
\displaystyle E = \sum_{i} (y_{i} -t_{i} )^2

シグマの前に1/2がつくこともありますが、これは微分したときに係数が消えてくれるようにするテクニックです。この関数の大事なところは極小となる位置なので、「前に係数が付いても位置は変わんないよね」って発想のテクニックです。この数式をコードで書いてみます。

E = np.sum((y-t)**2)

ちなみに、数式の通りですがEはスカラー値です。printで確認してみてください。

print(E)

確かに、ひとつの値のみが出力されます。

設問4.勾配降下法を用いて、パラメータ更新式を導出せよ

ここからがディープラーニングの真髄と言えるでしょうか。先ほど計算した損失関数の値が0に近づくように、パラメータ:重み(w)の値を更新していきます。それはつまり、出力yと正解ラベルtの値がどんどん近くなっていく、ということを意味します。では、どのように更新するのかというと、「微分値」を用います。損失関数(E)を更新したいパラメータ(w)で微分します。詳しい内容は難しい専門書に譲りますが、要はパラメータ(w)をちょっと変化させると損失関数(E)がどれくらい変化して、0に近づいてくれるかということを表しています。とりあえず、その通りに書いてみましょう。

{\displaystyle
\frac{\partial E}{\partial w}\  = \frac{\partial }{\partial w}\  \sum_{i} (t_{i} - y_{i})^2
}
「うお、Eの中にwがないから微分できねぇ!」となるんですが、ここで「連鎖律」というテクニックを使います。
{\displaystyle
\frac{\partial E}{\partial w}\ = \frac{\partial y}{\partial w}\   \frac{\partial E}{\partial y}\  = \frac{\partial }{\partial w}x_{i}w\   \frac{\partial }{\partial y}\  \sum_{i} (t_{i} - y_{i})^2
}
「約分できて元の形に戻るやん!」という感じで覚えてもらえばいいと思います。

微分を進めましょう。

{\displaystyle
\frac{\partial E}{\partial w}\ = \Bigl( \sum_{i} x_{i}\Bigr)  \Bigl(-2 \sum_{i} (t_{i} - y_{i})\Bigr)  = 2 \sum_{i} x_{i} (y_{i} -t_{i} )
}
「おいおい微分したらなんでシグマがでてくんねん!」とかいろいろツッコミがあるんですが、これであってるらしいです。数学的にややこしいところなんですが、こういうもんだと飲み込んでしまいましょう。

では、具体的な微分式を得られたところで、実際にパラメータを更新します。パラメータをを更新する手法も何パターンかあるんですが、今回は「勾配降下法」を利用します、一番スタンダードな手法ですね。他にも「モーメンタム法」や「AdaGrad法」などいろいろあるんですが、これらは問題の複雑さによって使い分けられます。「降下法」がいちばんシンプルですが、欠点もあります。ただし、今回の線形回帰のような問題には適しています。降下法では次のような式で更新式が与えられます。

{\displaystyle
w_{new} = w_{old} -  \mu \frac{\partial E}{\partial w}\ 
}

損失関数をwで微分した式は先ほど求めたので、この式に代入します。

{\displaystyle
w_{new} = w_{old} -  \mu \Bigl(2 \sum_{i} x_{i} (y_{i} -t_{i} )\Bigr)
}

突然でてきたμという係数ですが、これは学習率と呼ばれ、0から1の間で調整します。μの値が大きければ「頭のいい(学習が早い)AI」となりますが、学習が早すぎるとおかしなところにゴールしてしまうことがあります(過学習)。逆にμの値が小さければ、着実に学習を進めていきますが、時間がかかる(なんとも人間らしい)。μはトレードオフの関係を作るので、適切な値を探す必要があります。残念ながら、これは実際の人間の経験と勘によるところがあります。

これで設問4は終了です。

設問5.学習率0.004でイテレーションを25回転させ、その後の出力を計算せよ

イテレーションとはパラメータの更新を指します。パラメータの更新式も出せたので、実際にコードに書いて更新してみます。

learning_rate = 0.004
w = w - learning_rate*2*np.sum(x*(y-t))

これでパラメータの更新が完了しました。この計算が「逆伝搬(バックプロップ)」にあたり、まさにプログラムが「学習」します。さて、パラメータの更新は完了しましたが、1回の更新で学習は終わりません。この問題では、ここまでの順伝搬と逆伝搬を25回繰り返さなければいけません。繰り返しと言えば「for文」ですね。

順伝搬から逆伝搬までの部分をインテンドさせて、forの中に入れてしまいます。learning_rateの定義は外に出して置きましょう。

w = 1
learning_rate = 0.004

num_of_itr = 25
for i in range(num_of_itr):
    y = w*x
    E = np.sum((y-t)**2)
    w = w - learning_rate*2*np.sum(x*(y-t))

損失関数だけ浮いてる感じがしますが、次の設問で使うのでそのままforの中に入れておきます。では、25回更新を終えたwを使って最初のグラフに直線を出力してみましょう。うまくいっていれば最初に設定したプロットデータに沿っていい感じに直線がフィッティングされているはずです。

X_line = np.linspace(0, 2, 5)
Y_line = w*X_line

plt.plot(x, t, 'o')
plt.plot(X_line, Y_line)
plt.show()

f:id:sutokun:20190203125837p:plain

いい感じですね、これで設問6は終了です。

設問6.損失関数のイテレーション回数による推移をグラフ化せよ

最終設問です、損失関数は出力と正解ラベルの誤差を表します。イテレーションを回すごとにこの誤差をできるだけ小さくするようにパラメータは更新されていきます。先ほど、forの中にEの計算を残しました。イテレーションが回るごとにEも再計算されていきますが、このままでは計算されっぱなしで上書きされ続けます。イテレーションごとにEをリスト保存できるようにコードを追記しましょう。

w = 1
learning_rate = 0.004
E_save = [] #空のリストを用意

num_of_itr = 25
for i in range(num_of_itr):
    y = w*x
    E = np.sum((y-t)**2)
    E_save = np.append(E_save, E) #E_saveにEの値を保存
    w = w - learning_rate*2*np.sum(x*(y-t))

では、最後に損失量を保存したリストをグラフ化します。

plt.plot(E_save)
plt.show()

f:id:sutokun:20190203125809p:plain


イテレーション25回でそれなりに収束していますね。損失量がどれくらいのイテレーション回数で収束するかを確認するために、よくこのグラフ描画をします。ニューラルネットワークがより複雑になってくると損失量の推移も複雑になります。これはまた追々問題を解きながら見ていきましょう。

まとめ

これで第一問が完了です。最後に直線がプロットにフィットしていく様子をGIFで作成しました。画像の保存までPYTHONISTA3で行い、GIFの作成は別のソフトを使いました。
f:id:sutokun:20190203130023g:plain

この問題がニューラルネットワークの入り口の入り口で、「機械学習がやりたくても何から始めりゃいいのか初学者」はまずはココから始めていけば理解が広がっていくのではないかと思います。今回の出力はy=xwなので切片がありません。つまり、どれだけデータにフィットしようと頑張っても原点に縛られています。このニューラルネットワークを原点から開放してあげるには傾き:重み(w)と同様に切片:バイアス(b)も設定してあげなければいけません。次はバイアスを付加した線形問題にチャレンジします。

[ note ]
グラフ出力のコードは最小限のものしか書いていません。写真のグラフは見やすいようにラベルなど加工しています。詳しくは下リンクのGithubに全体コードを載せています。PYTHONISTA3はグラフの描画にクセがあるかな〜と感じますが、今のところ問題はないですね。

全体コード
github.com

次の問題
②線形回帰問題(バイアス付加) - 社畜エンジニア発掘戦線

元の記事
PYTHONISTA3を使って機械学習(ディープラーニング) - 社畜エンジニア発掘戦線

Twitter
世界の社畜 (@sekai_syachiku) | Twitter