えんじにあのじゆうちょう

勉強したことを中心にアウトプットしていきます。

単回帰/勾配降下法をコードで徹底的に理解する

はじめに

結局、使ってみたとしても、数式だけ読んだとしても、案外実際の動きを理解できないことは多いと思います。
これから、色々な機械学習モデルの学習についてコードをイチから書きながら考えてみようと思います。
(以前も少しやってますが、ちょっと細かめに書きます)

まずは第1段として、線形回帰の中でも最もシンプルな単回帰を例にやってみます。
ロジスティック回帰、決定木くらいまではとりあえずやってしまう予定です。

実装

ゴール

最終的にはSimpleLinearRegressionクラスを実装します。
できるだけ、scikit-learnなどのインタフェースに合わせたふうにします。

雛形

class SimpleLinearRegression:
    # パラメータを諸々定義
    def __init__(self):
        pass
        
    # 学習の開始関数
    def fit(self, X, Y):
        pass

    # 誤差の計算を実装
    def error(self, y, y_hat):
        pass

    # パラメータのアップデートを実装
    def update(self, y, y_hat, x):
        pass
    
    # 現在のパラメータに従った予測値を計算
    def predict(self, x):
       pass

初期化

解説はこれからなものもありますが、初期化ブロック内は以下のようにします。

    def __init__(self, eta=0.01, max_iteration=100):
        self.a = 0
        self.b = 0
        self.eta = eta
        self.max_iteration = max_iteration

パラメータとなるa, bに加え、学習率etaと最大の繰り返し回数を定義しています。

予測値を返す関数を定義

今回は単回帰なので y = ax + bを実装します。
以下のようになります。特に解説は必要ありませんね。

    def predict(self, x):
        return self.a * x + self.b

誤差関数を定義

誤差関数として二乗和誤差を定義していきます。
二乗和誤差はL(w) = \frac{1}{2} \sum (y_i-\hat{y_i})^2として定義されますので、素直にそれを実装します。

    def error(self, y, y_hat):
        return np.sum((y-y_hat) ** 2) / 2

パラメータの最適化:勾配降下法の実装

勾配降下法はモデルの予測値と真の値の誤差関数を設定し、その誤差を最小化することで最適なパラメータを求めようとする動きです。
基本的に、誤差関数をパラメータについて微分して、そのパラメータが最小化する方向へパラメータを動かしていきます。
上で記載したとおり、誤差関数には二乗和誤差が設定されます。

L(w) = \frac{1}{2} \sum (y_i-\hat{y_i})^2

yが真の値(教師データ)で\hat{y}が現在のパラメータを元にモデルが出した予測値です。

いま、モデルのパラメータをa,bとすると、\hat{y} = ax + bと表現されますので、改めて代入してみると、

L(w) = \frac{1}{2} \sum (y_i-ax + b)^2

のようになります。

なんとなくxyが変数のように見えてしまいますが、今回xyはデータとして与えられますのでa, bがパラメータです。

この式ですが、a, bに関する二次関数になっています。
つまり、傾きがゼロとなる点が存在していて、そこが最小値です。

それを求めるために偏微分します。

\frac{\partial{L}}{\partial{a}} = \sum xy - ax^2 - bx
\frac{\partial{L}}{\partial{b}} = \sum y - ax - b

このように各a, bにおける傾きが算出できます。
つまり、傾きとは符号が逆の方向にa, bを修正していけばいいのですが、一度にやると行ったり来たりしてしまい、中々収束しないという自体に陥ります。
そこで、学習率\etaを設定し、それを乗じた値でパラメータを更新します。

a = a - \eta \frac{\partial{L}}{\partial{a}}
b = b - \eta \frac{\partial{L}}{\partial{b}}

基本的に\etaは0.01など小さい値になります。

結果的に、コードは以下のようになります。

    def update(self, y, y_hat, x):
        self.a = self.a + self.eta * np.sum(np.dot(y - y_hat, x)) / len(x)
        self.b = self.b + self.eta * np.sum(y - y_hat) / len(x)

呼び出し元

ここは基本的に決めた回数分、学習をループするだけになります。(本来は学習の打ち切り、など考えるべきです)

    def fit(self, X, Y):
        for i in range(self.max_iteration):
            y_hat = self.predict(X)
            print("iteration: {} / Average Error: {}".format(i, self.error(Y, y_hat)))
            self.update(Y, y_hat, X)

全体

それでは最後に全体をすべて通しで記載しておきます。

class SimpleLinearRegression:
    def __init__(self, eta=0.01, max_iteration=100):
        self.a = 0
        self.b = 0
        self.eta = eta
        self.max_iteration = max_iteration
        
    def fit(self, X, Y):
        for i in range(self.max_iteration):
            y_hat = self.predict(X)
            print("iteration: {} / Average Error: {}".format(i, self.error(Y, y_hat)))
            self.update(Y, y_hat, X)
        
    def error(self, y, y_hat):
        return np.sum((y-y_hat) ** 2) / len(y)

    def update(self, y, y_hat, x):
        self.a = self.a + self.eta * np.sum(np.dot(y - y_hat, x)) / len(x)
        self.b = self.b + self.eta * np.sum(y - y_hat) / len(x)
    
    def predict(self, x):
        return self.a * x + self.b

実際に予測してプロットしてみる

今回は適当にirisのデータセットからそれらしく使えそうなところだけ抜き出して使います。

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

dataset = datasets.load_iris()
cond = (dataset.target == 0)
X = dataset.data[cond][:,0]
Y = dataset.data[cond][:,1]

plt.scatter(X, Y, color="blue")

f:id:marufeuillex:20191106233145p:plain

こんなデータです。実際に適用してみましょう。

reg  = SimpleLinearRegression()
reg.fit(X, Y)

plt.scatter(X, Y, color="blue")
x_range = np.arange(4.2, 6.0, 0.1)
plt.plot(x_range, reg.predict(x_range))

f:id:marufeuillex:20191106233302p:plain

それらしい線を引くことができました!
正解が気になる方は、scikit-learnなどのLinearRegressionを実行して確かめてみてもいいかもしれません。

まとめ

今回は単回帰を勾配降下法を用いて実現する方法について、Pythonコードを交えながら解説しました。
非常に単純ですが、勾配降下法は他のモデルでもよく出てくるので、意味をしっかり理解しておくと良いように思いました。