Pythonで勾配降下法。実装と理論

このチュートリアルは勾配降下法と呼ばれる簡単な最適化手法の入門書であり、最新の機械学習モデルにおいて大きな応用が見られます。

このチュートリアルでは、勾配降下を実装するための汎用ルーチンを開発し、教師あり学習による分類を含む様々な問題の解決に適用します。

この過程で、このアルゴリズムの動作を理解し、様々なハイパーパラメータがその性能に及ぼす影響を研究する。また、バッチ型と確率的勾配降下の変種を例として取り上げる。

グラディエント・デセント(Gradient Descent)とは?

勾配降下法とは、目的関数の最小値を求めることができる最適化手法の一つです。関数の減少率が最大となる方向に一歩ずつ進むことで最適解を求める貪欲な手法である。

これに対して、勾配降下法は、関数の増加率が最大になる方向に沿って関数の最大値を求めるという、近い対極にある手法である。

勾配降下の仕組みを理解するために、多変数関数 f(w)f(w)f(textbf{w}), where w=[w1,w2,…,wn]Tw=[w1,w2,…,wn]Ttextbf w = [w_1, wttp_2, ldots, wCOPY_n]^T で考えて見ます。この関数が最小となるww ∕textbf{w}を求めるために、勾配降下法では次のステップを踏みます。

    1. wwの初期値としてランダムな値を選択する。
    1. 最大反復回数 T を選択する。
    1. 学習率η∈[a,b]η∈[a,b]η[a,b]の値を選択する。
  1. fffが変化しないか、反復回数がTを超えるまで次の2ステップを繰り返す。

a.Compute:Δw=-η∇wf(w)Δw=-η∇wf(w) Delta textbf{w} = – eta nabla_textbf{w} f(textbf{w})

b. update ww.textbf{w} as: w←w+Δww←w+Δw.Textbf{w} として更新します。+ ⑭delta ⑯textbf{w}.

左矢印の記号は、数学的に正しい代入文の書き方です。

ここで、∇wf∇wf nabla_textbf{w} fは、次式で与えられるffの勾配を表す。

∇wf(w)=[∂f(w)∂w2 ∂f(w)∂wn]∇wf(w)=[∂w1 ∂w2 ∂f(w)∂wn] ∇f(w)∂w2 ∂f(w)∂wn] ∇f (w) ∂m1 ∂w2(sm_22)Ȃf(sm_202)とする。
nabla_textbf{w} f(textbf{w}) = (ウィキメディア・コモンズ)
begin{bmatrix}
frac{partial f(textbf{w})}{partial w_1}
frac{partial f(textbf{w})}{partial w_2} ←クリックすると拡大します。
vdots
frac{partial f(textbf{w})}{partial w_n} ↘↘↘↘↘不要
୧⃛(๑⃙⃘◡̈๑⃙⃘)

2変数の関数 f(w1,w2)=w21+w22f(w1,w2)=w12+w22 f(w1,w2) = wả_1^2+wả_2^2 とすると、各反復で (w1,w2)(w1,w2) (wả_1,wả_2) は以下のように更新されます。

[w1 w2]←[w1 w2]-η[2w1 2w2][w1 w2]←[w1 w2]-η[2w1 2w2] のように更新される。
begin {bmatrix}
w/1 w_2
end {bmatrix} Ίτ
begin {bmatrix}
w_1 w_2
end {bmatrix} – Ίταν
begin {bmatrix}
2wmatrix} 2w_1 ¦ 2w_2
୧end {bmatrix}

この関数に対して勾配降下がどのように働くかを示したのが下図である。

円はこの関数の輪郭である。輪郭に沿って移動しても、関数の値は変化せず一定である。

これは勾配の方向とは逆で、関数は最大速度で変化する。したがって、任意の点での関数の勾配の方向は、その点での輪郭の接線に垂直である。

簡単に言うと、勾配は関数が最も変化する方向を指す矢印と捉えることができる。

負の勾配をたどると、関数の値が最大に減少する地点に行き着く。学習速度はステップサイズとも呼ばれ、勾配の方向に沿ってどれだけ速く、または遅く移動するかを決定するものである。

モメンタム(勢い)を加える

勾配降下を用いる場合、以下のような問題にぶつかる。

    1. ローカルミニマムに陥る。これは、このアルゴリズムが貪欲であることの直接的な結果である。
    1. オーバーシュートして大域的最適解を逃す。これは勾配方向に沿って速く移動しすぎた直接の結果である。
  1. 振動、これは関数の値がどの方向に進んでも大きく変化しない場合に起こる現象です。これは、台地を進むようなもので、どこに行っても同じ高さにいるようなものです。

このような問題に対して、ΔwΔwDelta textbf{w} の式に運動量項ααを追加し、大域的最適値へ向かう際の学習速度を安定化させることができます。

以下、上付き添え字の iii は反復回数を表す。

Δwi=-η∇wf(wi)+αwi-1Δwi=-η∇wf(wi)+αwi-1
Delta textbf{w}^i = – eta nabla_Textbf{w} f(textbf{w}^i) + alpha textbf{w}^{i-1}

Pythonによる勾配降下法の実装

勾配降下の実際のコードを書き始める前に、これから利用するいくつかのライブラリをインポートしておきましょう。

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import sklearn.datasets as dt
from sklearn.model_selection import train_test_split


さて、それでは gradient_descent() 関数を定義してみましょう。この関数では、ループは次のどちらかのときに終了します。

    1. 反復回数が最大値を超えたとき
    1. 連続する2つの反復計算の間の関数値の差がある閾値以下になったとき

パラメータは,反復毎に目的関数の勾配に従って更新されます.

この関数は,以下のパラメータを受け付けます.

  • max_iterations: 実行する反復計算の最大回数
  • threshold: 連続する2つの反復処理の間で、関数値の差がこの閾値を下回ったら停止します。
  • w_init: 勾配降下を開始する初期点
  • obj_func: 目的関数を計算する関数への参照 * grad_func: 目的関数を計算する関数への参照
  • grad_func: 目的関数の勾配を計算する関数への参照 * grad_func: 目的関数の勾配を計算する関数への参照
  • extra_param: obj_func と grad_func の追加パラメータ (必要な場合)
  • learning_rate: gradient descent のステップサイズ。0,1]である必要がある。
  • momentum: 使用する運動量。0,1] の範囲でなければなりません。

また、この関数は以下を返します。

  • w_history: 勾配降下法によって訪問され、目的関数が評価された空間上のすべての点。
  • f_history: 各点で計算された目的関数の対応する値
# Make threshold a -ve value if you want to run exactly
# max_iterations.
def gradient_descent(max_iterations,threshold,w_init,
                     obj_func,grad_func,extra_param = [],
                     learning_rate=0.05,momentum=0.8):

    w = w_init
    w_history = w
    f_history = obj_func(w,extra_param)
    delta_w = np.zeros(w.shape)
    i = 0
    diff = 1.0e10

    while  i<max_iterations and="" diff=""threshold:
        delta_w = -learning_rate*grad_func(w,extra_param) + momentum*delta_w
        w = w+delta_w

        # store the history of w and f
        w_history = np.vstack((w_history,w))
        f_history = np.vstack((f_history,obj_func(w,extra_param)))

        # update iteration number and diff between successive values
        # of objective function
        i+=1
        diff = np.absolute(f_history[-1]-f_history[-2])

    return w_history,f_history


勾配降下法による関数の最適化

勾配降下の汎用実装ができたので、例の2次元関数 f(w1,w2)=w21+w22f(w1,w2)=w12+w22 f(w1,w_2) = wenta_1^2+wenta_2^2 with circular contours で動かしてみよう。

この関数は原点で最小値0となる。まず、関数を可視化し、その最小値を求めよう。

目的関数f(x)の可視化

以下の visualize_fw() 関数は、2500 個の等間隔な点を格子上に生成し、各点における関数値を計算します。

関数 function_plot() はその点での f(w)f(w)f(textbf w) の値に応じてすべての点を異なる色で表示します。関数の値が同じ点はすべて同じ色で表示されます。

def visualize_fw():
    xcoord = np.linspace(-10.0,10.0,50)
    ycoord = np.linspace(-10.0,10.0,50)
    w1,w2 = np.meshgrid(xcoord,ycoord)
    pts = np.vstack((w1.flatten(),w2.flatten()))

    # All 2D points on the grid
    pts = pts.transpose()

    # Function value at each point
    f_vals = np.sum(pts*pts,axis=1)
    function_plot(pts,f_vals)
    plt.title('Objective Function Shown in Color')
    plt.show()
    return pts,f_vals


# Helper function to annotate a single point
def annotate_pt(text,xy,xytext,color):
    plt.plot(xy[0],xy[1],marker='P',markersize=10,c=color)
    plt.annotate(text,xy=xy,xytext=xytext,
                 # color=color,
                 arrowprops=dict(arrowstyle="-&gt;",
                 color = color,
                 connectionstyle='arc3'))


# Plot the function
# Pts are 2D points and f_val is the corresponding function value
def function_plot(pts,f_val):
    f_plot = plt.scatter(pts[:,0],pts[:,1],
                         c=f_val,vmin=min(f_val),vmax=max(f_val),
                         cmap='RdBu_r')
    plt.colorbar(f_plot)
    # Show the optimal point
    annotate_pt('global minimum',(0,0),(-5,-7),'yellow')


pts,f_vals = visualize_fw()


異なるハイパーパラメータで勾配降下を実行する

さて、いよいよ目的関数を最小化するために勾配降下法を実行するときです。gradient_descent()` を呼び出すために、2つの関数を定義します。

  • f(): 任意の点 w における目的関数を計算します。
  • grad(): 任意の点 w における勾配を計算する。

勾配降下法における様々なハイパーパラメータの効果を理解するために、関数 solve_fw()gradient_descent() を呼び出し、異なる学習率と運動量の値で5回反復させます。

関数 visualize_learning() は、 (w1,w2)(w1,w2)(w_1,w_2) の値をプロットし、関数値を異なる色で表示します。プロット中の矢印は、どの点が前回から更新されたかを追跡しやすくするためのものである。

# Objective function
def f(w,extra=[]):
    return np.sum(w*w)


# Function to compute the gradient
def grad(w,extra=[]):
    return 2*w


# Function to plot the objective function
# and learning history annotated by arrows
# to show how learning proceeded
def visualize_learning(w_history):  

    # Make the function plot
    function_plot(pts,f_vals)

    # Plot the history
    plt.plot(w_history[:,0],w_history[:,1],marker='o',c='magenta') 

    # Annotate the point found at last iteration
    annotate_pt('minimum found',
                (w_history[-1,0],w_history[-1,1]),
                (-1,7),'green')
    iter = w_history.shape[0]
    for w,i in zip(w_history,range(iter-1)):
        # Annotate with arrows to show history
        plt.annotate("",
                    xy=w, xycoords='data',
                    xytext=w_history[i+1,:], textcoords='data',
                    arrowprops=dict(arrowstyle='&lt;-',
                            connectionstyle='angle3'))     

def solve_fw():
    # Setting up
    rand = np.random.RandomState(19)
    w_init = rand.uniform(-10,10,2)
    fig, ax = plt.subplots(nrows=4, ncols=4, figsize=(18, 12))
    learning_rates = [0.05,0.2,0.5,0.8]
    momentum = [0,0.5,0.9]
    ind = 1

    # Iteration through all possible parameter combinations
    for alpha in momentum:
        for eta,col in zip(learning_rates,[0,1,2,3]):
            plt.subplot(3,4,ind)        
            w_history,f_history = gradient_descent(5,-1,w_init, f,grad,[],eta,alpha)

            visualize_learning(w_history)
            ind = ind+1
            plt.text(-9, 12,'Learning Rate = '+str(eta),fontsize=13)
            if col==1:
                plt.text(10,15,'momentum = ' + str(alpha),fontsize=20)


fig.subplots_adjust(hspace=0.5, wspace=.3)
    plt.show()


solve_fw()`を実行して、学習率と運動量が勾配降下にどのように影響するかを見てみましょう。

solve_fw()


この例では、運動量と学習速度の両方の役割を明らかにしています。

最初のプロットでは、運動量ゼロ、学習率0.05で、学習は遅く、アルゴリズムはグローバルミニマムに到達しません。運動量を増加させると、最初の列のプロットからわかるように、学習が加速される。もう一つの極端な例は最後の列で、ここでは学習率を高くしている。これは振動を引き起こすが、運動量を増やすことである程度制御することができる。

勾配降下の一般的なガイドラインは、学習率を小さく、運動量を大きくすることである。

平均二乗誤差を最小化する勾配降下法

勾配降下法は,教師あり分類や回帰問題において平均二乗誤差を最小化するための簡単で優れた手法です.

i=1…mi=1…mi=1ldots m の学習例 [xij][xij][x_{ij}] が mmm 個与えられたとすると、各例は nnn 個の特徴、すなわち j=1…nj=1…nj=1ldots n を持っていることになります。各例に対応する目標値を titit_i 、出力値を oioio_i とすると、平均二乗誤差関数 EEE(この場合は目的関数)は次のように定義される。

E=1mΣmi=1(ti−oi)2E=1mΣi=1m(ti−oi)2
E = frac{1}{m} E = Sigma_{i=1}^m (t_i – o_i)^2

ここで、出力oioio_iは、次のように与えられる入力の重み付き線形結合によって決定される。

oi=w0+w1xi1+w2xi2+…+wnxinoi=w0+w1xi1+w2xi2+…+wnxin
o_i = wh_0 + wh_1 x_{i1} + w_2 x_{i2} + ldots + w_n x_{in}

上式の未知パラメータは、重みベクトルw=[w0,w1,…,wn]Tw=[w0,w1,…,wn]Textbf w = [w_0,wh_1,ldots,wh_n]^T とする。

この場合の目的関数は平均二乗誤差であり、勾配は次式で与えられる。

∇wE(w)=-Σmi=1(ti-oi)xi∇wE(w)=-Σi=1m(ti-oi)xi
E(nabla_{textbf w}E(textbf w) = -┣Σ_{i=1}^{m} (tenta_i – oenta_i) textbf{x}┣i

ここで、xixix_Θはi番目の例、またはサイズnの特徴量の配列である。

あとは勾配を計算する関数と平均二乗誤差を計算する関数が必要である。

gradient_descent()` 関数は,そのまま利用することができます.勾配を計算する際には、すべての学習例が一緒に処理されることに注意してください。したがって,この重みの更新のための勾配降下法のバージョンは,バッチ更新またはバッチ学習と呼ばれます.

# Input argument is weight and a tuple (train_data, target)
def grad_mse(w,xy):
    (x,y) = xy
    (rows,cols) = x.shape

    # Compute the output
    o = np.sum(x*w,axis=1)
    diff = y-o
    diff = diff.reshape((rows,1))    
    diff = np.tile(diff, (1, cols))
    grad = diff*x
    grad = -np.sum(grad,axis=0)
    return grad


# Input argument is weight and a tuple (train_data, target)
def mse(w,xy):
    (x,y) = xy

    # Compute output
    # keep in mind that wer're using mse and not mse/m
    # because it would be relevant to the end result
    o = np.sum(x*w,axis=1)
    mse = np.sum((y-o)*(y-o))
    mse = mse/2
    return mse


OCRでの勾配降下法の実行

分類問題での勾配降下を説明するために、sklearn.datasetsに含まれるdigitsデータセットを選択しました。

シンプルにするために、2クラス問題(digit 0 vs. digit 1)で勾配降下をテストしてみましょう。以下のコードは数字をロードし、最初の10桁を表示します。これにより、学習ポイントの性質を知ることができます。

# Load the digits dataset with two classes
digits,target = dt.load_digits(n_class=2,return_X_y=True)
fig,ax = plt.subplots(nrows=1, ncols=10,figsize=(12,4),subplot_kw=dict(xticks=[], yticks=[]))


# Plot some images of digits
for i in np.arange(10):
    ax[i].imshow(digits[i,:].reshape(8,8),cmap=plt.cm.gray)   
plt.show()


また、訓練データを訓練セットとテストセットに分割するために、 sklearn.model_selectiontrain_test_split メソッドが必要です。以下のコードでは、トレーニングセットに対して勾配降下を実行し、重みを学習し、異なる反復における平均二乗誤差をプロットしています。

勾配降下を実行する際、入力が正規化・標準化されていないため、学習速度と運動量は非常に小さくします。また、バッチ版の勾配降下法では、学習率を小さくする必要がある。

# Split into train and test set
x_train, x_test, y_train, y_test = train_test_split(
                        digits, target, test_size=0.2, random_state=10)


# Add a column of ones to account for bias in train and test
x_train = np.hstack((np.ones((y_train.size,1)),x_train))
x_test  = np.hstack((np.ones((y_test.size,1)),x_test))


# Initialize the weights and call gradient descent
rand = np.random.RandomState(19)
w_init = rand.uniform(-1,1,x_train.shape[1])*.000001
w_history,mse_history = gradient_descent(100,0.1,w_init,
                              mse,grad_mse,(x_train,y_train),
                             learning_rate=1e-6,momentum=0.7)


# Plot the MSE
plt.plot(np.arange(mse_history.size),mse_history)
plt.xlabel('Iteration No.')
plt.ylabel('Mean Square Error')
plt.title('Gradient Descent on Digits Data (Batch Version)')
plt.show()


これはいい感じです。それでは、学習データとテストデータでOCRのエラー率を調べてみましょう。以下は、分類のエラー率を計算する小さな関数で、トレーニングセットとテストセットに対して呼び出されます。

# Returns error rate of classifier
# total miclassifications/total*100
def error(w,xy):
    (x,y) = xy
    o = np.sum(x*w,axis=1)

    #map the output values to 0/1 class labels
    ind_1 = np.where(o&gt;0.5)
    ind_0 = np.where(o&lt;=0.5)
    o[ind_1] = 1
    o[ind_0] = 0
    return np.sum((o-y)*(o-y))/y.size*100

train_error = error(w_history[-1],(x_train,y_train))
test_error = error(w_history[-1],(x_test,y_test))


print("Train Error Rate: " + "{:.2f}".format(train_error))
print("Test Error Rate: " + "{:.2f}".format(test_error))


Train Error Rate: 0.69
Test Error Rate: 1.39


Python で確率的勾配降下法(STG)

前節では、gradient descent の更新方法としてバッチ更新を使いました。

勾配降下の別のバージョンとして、オンラインまたは確率的な更新スキームがあります。これは、各トレーニング例を一回ずつ取り出して重みを更新します。

すべての学習例が終了すると、1つのエポックが終了したとみなされます。より良い結果を得るために、各エポックの前に学習例をシャッフルします。

以下のコードは、 gradient_descent() 関数を少し修正し、確率的な対応する関数を組み込んだものです。この関数は、追加パラメータの代わりに (training set, target) をパラメータとして受け取ります。また、’iterations’ という用語は ‘epochs’ に改名されました。

# (xy) is the (training_set,target) pair
def stochastic_gradient_descent(max_epochs,threshold,w_init,
                                obj_func,grad_func,xy,
                                learning_rate=0.05,momentum=0.8):
    (x_train,y_train) = xy
    w = w_init
    w_history = w
    f_history = obj_func(w,xy)
    delta_w = np.zeros(w.shape)
    i = 0
    diff = 1.0e10
    rows = x_train.shape[0]

    # Run epochs
    while  i<max_epochs and="" diff=""threshold:
        # Shuffle rows using a fixed seed to reproduce the results
        np.random.seed(i)
        p = np.random.permutation(rows)

        # Run for each instance/example in training set
        for x,y in zip(x_train[p,:],y_train[p]):
            delta_w = -learning_rate*grad_func(w,(np.array([x]),y)) + momentum*delta_w
            w = w+delta_w

        i+=1
        w_history = np.vstack((w_history,w))
        f_history = np.vstack((f_history,obj_func(w,xy)))
        diff = np.absolute(f_history[-1]-f_history[-2])

    return w_history,f_history


それでは、確率的勾配降下法の結果を確認するために、コードを実行してみましょう。

rand = np.random.RandomState(19)
w_init = rand.uniform(-1,1,x_train.shape[1])*.000001
w_history_stoch,mse_history_stoch = stochastic_gradient_descent(
                                100,0.1,w_init,
                              mse,grad_mse,(x_train,y_train),
                             learning_rate=1e-6,momentum=0.7)


# Plot the MSE
plt.plot(np.arange(mse_history_stoch.size),mse_history_stoch)
plt.xlabel('Iteration No.')
plt.ylabel('Mean Square Error')
plt.title('Gradient Descent on Digits Data (Stochastic Version)')


plt.show()


また、エラー率も確認してみましょう。

train_error_stochastic = error(w_history_stoch[-1],(x_train,y_train))
test_error_stochastic = error(w_history_stoch[-1],(x_test,y_test))


print("Train Error rate with Stochastic Gradient Descent: " + 
      "{:.2f}".format(train_error_stochastic))
print("Test Error rate with Stochastic Gradient Descent: "  
      + "{:.2f}".format(test_error_stochastic))


Train Error rate with Stochastic Gradient Descent: 0.35
Test Error rate with Stochastic Gradient Descent: 1.39


バッチ版とストキャスティック版の比較

バッチ版と確率版の勾配降下法を比較してみましょう。

両者の学習率を同じ値に固定し、運動量を変化させ、両者の収束の速さを見ることにします。両アルゴリズムの初期重みと停止基準は同じです。

fig, ax = plt.subplots(nrows=3, ncols=1, figsize=(10,3))


rand = np.random.RandomState(11)
w_init = rand.uniform(-1,1,x_train.shape[1])*.000001
eta = 1e-6
for alpha,ind in zip([0,0.5,0.9],[1,2,3]):
    w_history,mse_history = gradient_descent(
                                100,0.01,w_init,
                              mse,grad_mse,(x_train,y_train),
                             learning_rate=eta,momentum=alpha)


w_history_stoch,mse_history_stoch = stochastic_gradient_descent(
                                100,0.01,w_init,
                              mse,grad_mse,(x_train,y_train),
                             learning_rate=eta,momentum=alpha)

    # Plot the MSE
    plt.subplot(130+ind)
    plt.plot(np.arange(mse_history.size),mse_history,color='green')
    plt.plot(np.arange(mse_history_stoch.size),mse_history_stoch,color='blue')
    plt.legend(['batch','stochastic'])

    # Display total iterations
    plt.text(3,-30,'Batch: Iterations='+
             str(mse_history.size) )
    plt.text(3,-45,'Stochastic: Iterations='+
             str(mse_history_stoch.size))
    plt.title('Momentum = ' + str(alpha))   

    # Display the error rates
    train_error = error(w_history[-1],(x_train,y_train))
    test_error = error(w_history[-1],(x_test,y_test))

    train_error_stochastic = error(w_history_stoch[-1],(x_train,y_train))
    test_error_stochastic = error(w_history_stoch[-1],(x_test,y_test))

    print ('Momentum = '+str(alpha))

    print ('    Batch:')
    print ('        Train error: ' + "{:.2f}".format(train_error) )
    print ('        Test error: ' + "{:.2f}".format(test_error) )

    print ('    Stochastic:')
    print ('        Train error: ' + "{:.2f}".format(train_error_stochastic) )
    print ('        Test error: ' + "{:.2f}".format(test_error_stochastic) )


plt.show()


Momentum = 0
    Batch:
        Train error: 0.35
        Test error: 1.39
    Stochastic:
        Train error: 0.35
        Test error: 1.39
Momentum = 0.5
    Batch:
        Train error: 0.00
        Test error: 1.39
    Stochastic:
        Train error: 0.35
        Test error: 1.39
Momentum = 0.9
    Batch:
        Train error: 0.00
        Test error: 1.39
    Stochastic:
        Train error: 0.00
        Test error: 1.39


2つのバージョンの分類器の間に精度の大きな差はありませんが、収束の速さに関してはストキャスティックバージョンが明らかに勝者であると言えます。バッチ版と同じ結果を得るために必要な反復回数は少なくなります。

結論

勾配降下法は、シンプルで実装が簡単な手法です。

このチュートリアルでは、円形の輪郭を持つ2変数の関数に対する勾配降下法を説明しました。次に、この例を分類問題における平均二乗誤差を最小化するように拡張し、簡単な OCR システムを構築しました。また、確率的な勾配降下法についても説明した。

このチュートリアルでは、勾配降下を実装するための汎用的な関数を開発しました。この関数の動作をよりよく理解するために、読者は異なるハイパーパラメータで、異なる回帰問題や分類問題でこの関数を使用することをお勧めします。

を参照してください。

タイトルとURLをコピーしました