SIGMA-SE Tech Blog

SIGMA-SE Tech Blog


当サイトは、過去に運営していた別ドメイン(unisia-se.com)から sigma-se.com へ移行した技術ブログです。
旧サイトの記事をもとに、内容の精査・加筆・最新化を行い再構成しています。
正確で実用的な情報提供を目的としています。

Python - ニューラルネットワーク:13/14 重みに対する勾配とパラメータ更新

概要

ニューラルネットワークの重みに対して損失関数の勾配を求め、パラメータを更新する流れを整理する。

前の記事までで扱った損失関数、数値微分、勾配降下法を、ニューラルネットワークの重み更新に結び付ける段階となる。

ここでは、重みを少し変えたときに損失がどう変化するかを確認し、損失が小さくなる方向へ更新する考え方を実装する。

この記事で扱うこと

  • 重みに対する勾配の意味。
  • 損失関数と重み更新の関係。
  • 数値勾配を使った重み更新の流れ。
  • 学習率を掛けてパラメータを更新する方法。

作業前に確認すること

確認項目 内容
損失関数予測と正解のずれを数値化する考え方を確認しておく。
数値微分と勾配各パラメータに対する損失の変化を求める流れを確認しておく。
Pythonクラス、辞書、NumPy配列の基本を理解しておく。

概念の説明と実装サンプル

重みに対する勾配法とは

重みは、正解に対して入力値がどれだけ影響するかを示す重要度を表す。

重みという言葉からは全くイメージできない意味を持つが、これは英語のWeighを直訳し、重みとなっているからであり、本来は大切さ価値重要性という意味で命名されている。

そして、この重み(重要度)は、一般的にWeighの \(w\) を取り、\(w_{0}\)、\(w_{1}\)、\(w_{2}\) …で表現され、評価的に具体的な数値を入れてみて損失結果を計るためのパラメータとなる。

ニューラルネットワークでは、この重みに対して、Python - ニューラルネットワーク: 勾配降下法の実装サンプル で解説した勾配降下法を実施し、最適な重みを求めていく。

また、下記に示す行列のよう色々なパターンの重みを行列計算で一斉に偏微分し、最適な重みを求めていく。

\[ W = \begin{pmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \\ \end{pmatrix} \]
\[ \frac{∂L}{∂W} = \begin{pmatrix} \frac{∂L}{∂w_{11}} & \frac{∂L}{∂w_{12}} & \frac{∂L}{∂w_{13}} \\ \frac{∂L}{∂w_{21}} & \frac{∂L}{∂w_{22}} & \frac{∂L}{∂w_{23}} \\ \end{pmatrix} \]

\(W\) は、一斉に偏微分しようとしている2行3列の重み達。
\(L\) は、対象となる損失関数で \(\displaystyle \frac{∂L}{∂W}\) の各要素で偏微分し、損失関数\(L\) の勾配を求めている。

以降は、この勾配を求める実装サンプルを確認する。

重みに対する勾配のPython実装サンプル

以下、参考文献『ゼロから作るDeep Learning』から提供されている ch04/gradient_simplenet.py を用いたサンプル解説をしていく。

※ サンプルコードは、下記Gitからダウンロードする。
Git(deep-learning-from-scratch): https://github.com/oreilly-japan/deep-learning-from-scratch

ここでは、処理の流れを追いやすくするため、ch04/gradient_simplenet.pysimpleNetクラスで使われる下記3つの関数を、あえてPython対話モードで定義して確認する。

  • ソフトマックス関数:common/functions.pyのsoftmax関数
    ※ ソフトマックス関数の一般的な定義は下記ページを参考。
    Python - ニューラルネットワーク: 活性化関数の実装サンプルまとめ(ステップ、シグモイド、ReLU、恒等関数、ソフトマックス関数) > ソフトマックス関数

    $ python
    >>> import numpy as np
    >>>
    >>> def softmax(x):
    ...     if x.ndim == 2:
    ...         x = x.T
    ...         x = x - np.max(x, axis=0)
    ...         y = np.exp(x) / np.sum(np.exp(x), axis=0)
    ...         return y.T
    ...     x = x - np.max(x)
    ...     return np.exp(x) / np.sum(np.exp(x))
    ...
    >>>
    
  • 交差エントロピー誤差:common/functions.py の cross_entropy_error関数
    ※ 交差エントロピー誤差の処理内容については、下記ページを参考。
    Python - ニューラルネットワーク: 損失関数(2乗和誤差、交差エントロピー誤差)と実装サンプル > ソフトマックス関数 Python - ニューラルネットワーク: 交差エントロピー誤差のミニバッチ学習と実装サンプル

    >>> # 上記対話モードの続き
    >>> def cross_entropy_error(y, t):
    ...     if y.ndim == 1:
    ...         t = t.reshape(1, t.size)
    ...         y = y.reshape(1, y.size)
    ...
    ...     if t.size == y.size:
    ...         t = t.argmax(axis=1)
    ...
    ...     batch_size = y.shape[0]
    ...     return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
    ...
    >>>
    
  • 勾配処理:common/gradient.pyのnumerical_gradient関数
    ※ 勾配の処理内容は下記ページの勾配関数(num_gradient)を参考。
    Python - ニューラルネットワーク: 偏微分と勾配の実装サンプル

    >>> # 上記対話モードの続き
    >>> def numerical_gradient(f, x):
    ...     h = 1e-4
    ...     grad = np.zeros_like(x)
    ...
    ...     it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    ...
    ...     while not it.finished:
    ...         idx = it.multi_index
    ...         tmp_val = x[idx]
    ...         x[idx] = float(tmp_val) + h
    ...         fxh1 = f(x)
    ...
    ...         x[idx] = tmp_val - h
    ...         fxh2 = f(x)
    ...         grad[idx] = (fxh1 - fxh2) / (2*h)
    ...
    ...         x[idx] = tmp_val
    ...         it.iternext()
    ...
    ...     return grad
    ...
    >>>
    
  • 重みに対する勾配処理:3つの関数を呼び出した形でch04/gradient_simplenet.pysimpleNetクラスを実装

    >>> # 上記対話モードの続き
    >>> import sys, os
    >>>
    >>> class simpleNet:
    ...     def __init__(self):
    ...         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)
    ...         loss = cross_entropy_error(y, t)
    ...
    ...         return loss
    ...
    >>>
    

    \(x\) は、入力データで \(t\) が教師データ

    predict関数は、入力データ \(x\) と__init__で設定した仮(ランダム)の重みパラメータself.W評価結果(積)を返す。

    loss関数は、predict関数、softmax関数を実施した \(y\) と教師データ \(t\) の損失関数(交差エントロピー誤差:cross_entropy_error関数)を返す。

実装サンプルの実行確認

以降、上記simpleNetの実行例を基に解説する。

  • インスタンスの結果確認

    >>> # 上記対話モードの続き
    >>> net = simpleNet()
    >>> print(net.W)
    [[ 0.49236891 -1.2239298  -1.13722119]
    [ 0.05405365 -0.79897152 -0.08066587]]
    >>>
    

    simpleNet()により、2x3行列のランダムな数値が生成される。

  • 評価結果(積)の確認

    >>> # ↑↑↑ 上記対話モードの続き
    >>> x = np.array([0.6, 0.9])
    >>> p = net.predict(x)
    >>> print(p)
    [ 1.05414809 0.63071653 1.1328074  ]
    >>> np.argmax(p)
    2
    >>>
    

    predict(x)により、入力データ \([0.6, 0.9]\) と重みパラメータnet.Wの評価結果(積)を算出している。 (最大インデックスは、2)

  • 損失関数の結果確認

    >>> # 上記対話モードの続き
    >>> t = np.array([0, 0, 1])
    >>> net.loss(x, t)
    0.92806853663411326
    >>>
    

    上記で最大インデックスとなった\(2\)が正解ラベルとなる状態で損失関数の結果は、約\(0.93\)

  • 勾配の結果確認

    >>> # 上記対話モードの続き
    >>> def f(W):
    ...     return net.loss(x, t)
    ...
    >>> dW = numerical_gradient(f, net.W)
    >>> print(dW)
    [[ 0.21924763  0.14356247 -0.36281009 ]
    [ 0.32887144  0.2153437 -0.544211514]]
    >>>
    

    net.loss(x, t)f(W)とし、勾配処理(numerical_gradient)を実施している。

    f(W)Wは、勾配処理:common/gradient.pyのnumerical_gradient関数の\((A)\)、\((B)\)と整合性が取れるように定義したもの。

  • 結果から見る結論

    重みパラメータ \(w_{11}\) と重みパラメータ \(w_{23}\) にスポットを当てた結果を見る。

    \(w_{11}\) が \(h\)分増加すると、\(\displaystyle \frac{∂L}{∂W}\) の \(\displaystyle \frac{∂L}{∂w_{11}}\) は、約\(0.2\)となり、\(0.2\)増加している。

    一方、\(w_{23}\) は \(h\) 分増加すると、\(\displaystyle \frac{∂L}{∂W}\) の \(\displaystyle \frac{∂L}{∂w_{23}}\) が約\(-0.5\)となり、\(0.5\)減少している。

    よって、 重みパラメータ \(w_{11}\) は、マイナス方向に。
    重みパラメータ \(w_{23}\) は、プラス方向更新すべきという結論となる。

    ※ 以上の要領で重みパラメータをより損失が少ない重みパラメータへ更新していくことが目的。

違いを整理する

比較する項目 整理するポイント
重みのshape入力数、出力数に合わせて重み行列の形が決まる。
勾配の向き勾配は損失が増える方向なので、更新ではマイナス方向へ動かす。
数値勾配の計算量数値微分は理解しやすいが、大きなネットワークでは計算量が大きい。

実務とのつながり

  • 学習処理の理解
    フレームワークが自動で行う重み更新の意味を、手元の実装で確認できる。
  • トラブルシュート
    損失が下がらないときは、勾配、学習率、初期値、データのshapeを順番に確認する。

まとめ

  • 重みに対する勾配は、重みを変えたときの損失の変化を表す。
  • 学習では、勾配の逆方向に重みを更新して損失を下げる。
  • 数値勾配は学習の仕組みを理解するのに役立つが、計算量には注意が必要。

参考文献

  • 斎藤 康毅(\(2018\))『ゼロから作るDeep Learning - Pythonで学ぶディープラーニングの理論と実装』株式会社オライリー・ジャパン


Copyright SIGMA-SE All Rights Reserved.
s-hama@sigma-se.jp