0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

技術書一冊やり込もうAdvent Calendar 2024

Day 20

ゼロから作るDeepLearning #6 -勾配法むず-

Last updated at Posted at 2024-12-19

※以下の企画です

前回に続いてゼロつくやっていきます。
今回も4章 -ニューラルネットワークの学習-の続きです。
前回学んだ勾配法を深堀って"学習"をしていく過程の内容っぽいです。
アウトプット難しそ〜〜〜〜

それでは頑張っていきます〜

4章 ニューラルネットワークの学習

勾配法

前回学んだ勾配を用いて、関数が最小値(必ずしも最小ではないらしいが)となるパラメータを見つけようというのが勾配法の考え方である。

数式で表すと以下になる。

x_{t+1} = x_t - \eta \nabla f(x_t)

ここで、

  • $x_t$ : 現在のパラメータ
  • $\eta$ : 学習率(イータ)
  • $\nabla f(x_t)$ : $x_t$における関数$f$の勾配

学習率$\eta$は重要な役割を果たし、高ければ高いほどパラメータを大胆に変更するが、最小値を通り越してしまったりするリスクがある。逆に低すぎると収束に時間がかかる可能性がある。この学習率はハイパーパラメータと呼ばれるもので、機械学習の過程で自動的に導かれるものではなく、操作者が設定する必要がある。

以下はPythonで単純な勾配法を実装する例である。

import numpy as np

def function_2(x):
    return x[0]**2 + x[1]**2

def numerical_gradient(f, x):
    h = 1e-4  # 微小値
    grad = np.zeros_like(x)

    for idx in range(x.size):
        tmp_val = x[idx]
        
        # f(x + h)
        x[idx] = tmp_val + h
        fxh1 = f(x)
        
        # f(x - h)
        x[idx] = tmp_val - h
        fxh2 = f(x)
        
        # 勾配計算
        grad[idx] = (fxh1 - fxh2) / (2 * h)
        
        # 値を元に戻す
        x[idx] = tmp_val

    return grad

def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x

    for _ in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x

# テスト実行
init_x = np.array([-3.0, 4.0])  # 初期値
result = gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100)
print("最適化結果:", result)
実行結果
最適化結果: [-6.11110793e-10  8.14814391e-10]

上記コードでは2次関数$f(x) = x_0^2 + x_1^2$の最小値を求めている。

  • numerical_gradient関数は、入力関数$f$に対して勾配を数値的に計算する
  • gradient_descent関数は勾配降下法を適用してパラメータを更新し、最小値に収束させる

というもの。
結果を見てみると、初期値(-3,4)から始めて、計算をstep_num分繰り返すと非常に小さい値が出力された。
これは最小値(十分に小さい値)を見つけられたということになる。

ニューラルネットワークでの勾配法

ニューラルネットワークでは、勾配法を用いて誤差関数の最小化を目指す。具体的には、以下のような流れで学習を進める:

  1. 誤差関数(損失関数)の定義:
    • ニューラルネットワークの出力と正解ラベルとの誤差を定義する
  2. 誤差関数の勾配を計算:
    • 各パラメータ(重みやバイアス)に関する勾配を計算する
  3. パラメータの更新:
    • 勾配降下法を用いて、パラメータを更新する

以下に、簡単な実装例(ほぼ写経だけど)を示す。

class SimpleNet:
    def __init__(self):
        np.random.seed(0)
        self.W = np.random.randn(2, 3)  # 重み(ランダム初期化)

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        return cross_entropy_error(y, t)

def softmax(x):
    if x.ndim == 2:
        x = x - np.max(x, axis=1, keepdims=True)  # オーバーフロー防止
        x = np.exp(x)
        return x / np.sum(x, axis=1, keepdims=True)
    
    x = x - np.max(x)  # オーバーフロー防止
    return np.exp(x) / np.sum(np.exp(x))

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + 1e-7)) / batch_size

def f(W):
    return net.loss(x, t)

# 2次元配列対応版
def numerical_gradient(f, x):
    reshape_flg = False
    
    # 2次元配列に変換
    if x.ndim == 1: # 1次元配列の場合
        x = x.reshape(1, x.size)
        reshape_flg = True # Trueに変更
    
    h = 1e-4
 
    grad = np.zeros_like(x)
    
    # 勾配を計算
    for i in range(x.shape[0]): # 行
        for j in range(x.shape[1]): # 列
            
            tmp_val = x[i, j]
            
            # f(x+h)を計算
            x[i, j] = tmp_val + h
            fxh1 = f(x[i, :])
            
            # f(x-h)を計算
            x[i, j] = tmp_val - h
            fxh2 = f(x[i, :])
            
            grad[i, j] = (fxh1 - fxh2) / (2 * h)
            
            # 元の値に戻す
            x[i, j] = tmp_val
    
    # 勾配を出力
    if reshape_flg:
        return grad.reshape(grad.size)
    else:
        return grad

上記では、numerical_gradient関数がやや変更されている。
というのも、前回学んだままの形式だと、サイズが合わずにエラーを吐き出すためだ。

実行結果は以下。

テスト実行
# 使用例
net = SimpleNet()
print(net.W) #重みパラメータ

x = np.array([0.6, 0.9])  # 入力
p = net.predict(x)
print(p)

t = np.array([0, 0, 1])  # 正解ラベル(one-hot)
net.loss(x,t)

# 勾配を計算
dW = numerical_gradient(f, net.W)
print(dW)
実行結果
※改行を意図的に入れています※

[[-0.63432209 -0.36274117 -0.67246045]
 [-0.35955316 -0.81314628 -1.7262826 ]]
 
[-0.7041911  -0.94947635 -1.95713061]

[[ 0.29011482  0.22700942 -0.51712425]
 [ 0.43517224  0.34051414 -0.77568637]]

一応できてるっぽい。

ニューラルネットワークの具体的な重み更新は、より複雑なネットワークでも基本は同じ手法に基づくので、ココらへんが使えれば基本はできてるって言える…かも?

まとめ

今回は勾配を用いてパラメータを更新する方法を学んだ。
データ量やパラメータ数が増えるとさらにわけわからなくなっていくと思うので、しっかり今回の内容を押さえておきたい…!
また次回も頑張ります〜

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?