【フレームワークを使用せず】ゼロから作る物体検出AIモデル
【No.9】ニューラルネットワークの実装(重みの更新)
1. 本記事について
本記事は、これまでの内容(ニューラルネットワークの理解その1〜4)を踏まえ、ニューラルネットワークをpythonプログラムに落とし込んでいきます。
本記事は重みの更新と、その実装を解説していきます。
(前回は、ロス関数について解説しました。)
ニューラルネットワークの理解その1~4を見てから、本記事を見ることをお勧めします(特にその3とその4)。
注意事項として、本シリーズではpythonの基礎的な解説、numpyの基礎的な解説は割愛させていただきます。特に、numpy配列やshape等頻出なので、その辺の知識が不十分の場合は、そちらの習得を先に行うことをお勧めします。
本シリーズを進めていくにあたり、参考にさせていただく書籍があります。オライリー・ジャパンから出版されている「ゼロから作るDeep Learning ーPythonで学ぶディープラーニングの理論と実装」です。
本記事は、上記書籍を参考にさせていただいております。
2. 前回迄のあらすじ
重みの更新に入る前に、少しだけ振り返りをしようと思います。興味ない方は飛ばしてください。
前回迄でaffine層・活性化関数・ロス関数を実装してきました。それぞれ順伝播と逆伝播の関数を含んでいます。
ニューラルネットワークの流れを簡単に説明すると、以下となります。
まずネットワークに値を入力します。この値は順伝播していきます。順伝播によって最終的に出力される値が推論値です。推論値はロス関数でどの程度正解値と差があるのか数値化されます。
その後、ロス関数の値が元になり、逆伝播されます。逆伝播によって各ノードの重み変化量が求めれられます。各backward関数の「self.dW」がそれに当たります。
今回は、それら逆伝播によって求められた重み変化量を使って、実際に重みを更新する部分を実装していきたいと思います
(重み更新の概要についてはこちらをご覧ください)。
3. 重みの更新
重みの更新は何種類か存在します。ここでは一番シンプルな方法とYOLOv1で使用されている方法を解説していきます。
SGD
SGDStochastic Gradient Descent : 確率的勾配降下法)と呼ばれる手法があります。これが一番シンプルな方法です。「\(W\)から\(dW\)を引く」だけです。
「\(\leftarrow\)」は「更新」を表していて、矢印の右側が更新方法、左側が更新後を表しています。
\(η\)は学習率です。ここでは一旦\(η = lr = 0.01\)としておきます。
学習率の値が大きいと、更新量が増えるので、その分学習が早く進む傾向にあります。但し、大きすぎると学習が収束せず、逆に増加してしまうこともあります。
学習率の値が小さいと、更新量が減るので、その分学習にかかる時間が増加する傾向にあります。とはいえ、着実にlossは減るので、着実に学習は進んでいきます。
学習率の最適値はネットワークによって様々で、最適な値を人間が探すことになります。こういった、深層学習の外側で人が設定するパラメータをハイパーパラメータと呼んだりします。
ハイパーパラメータのチューニングは結構大変で、YOLOv1に関しても、学習の進行によって学習率を変化させる工夫をしています。
SGDをプログラムに落とし込みます。以下となります。
lr = 0.01
W -= lr * dW
シンプルで良いですね。
クラスにまとめます。
class SGD:
# 学習率の初期化
def __init__(self, lr=0.01):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]
更新の処理をupdate(params, grads)関数としました。paramsとgradsを引数にとっています。
※ params、gradsについてはネットワーク構築の際に本格的に実装します。
paramsは各層の重みを格納した辞書型の変数です。例えば、params['W1']を指定すると1層目の重みの配列がvalueとして返ってきます。
gradsは格納の重みに対応する重みの変化量(dW)を格納した辞書型の変数です。例えば、grads['W1']を指定すると1層目の重み変化量の配列がvalueとして返ってきます。
実際に数値を入れて動作を確認します。
import numpy as np
class SGD:
# 学習率の初期化
def __init__(self, lr=0.01):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]
### 実際に数値を入力してみる ###
lr = 0.01
optimizer = SGD(lr=lr)# インスタンス化
params = {}# 重み
params['W1'] = np.array([
-1.953,
-0.008,
-0.236,
0.491,
-2.137
])
print(params['W1'])
print()
grads = {}# 勾配(適当な値)
grads['W1'] = np.array([
0.428,
2.229,
-1.067,
-1.456,
0.602
])
print(grads['W1'])
print()
optimizer.update(params, grads)# パラメータ更新
print(params['W1'])
[-1.953 -0.008 -0.236 0.491 -2.137]
[ 0.428 2.229 -1.067 -1.456 0.602]
[-1.95728 -0.03029 -0.22533 0.50556 -2.14302]
重みの更新ができました。ニューラルネットワークの学習は、基本的に以下を1サイクルで処理していきます。
入力→順伝播(推論)→loss計算→逆伝播(重みの勾配計算)→重みの更新
重みの更新が終わったら、今度は別の入力値を流してあげて学習を行います。この様にして重みを最適化=学習していきます。
Momentum
Momentum、若しくはMomentum SGDと呼ばれる手法があります。SGDが「\(W\)から\(dW\)を引く」手法だったのに対し、MomentumはSGDに一手間加えた手法です。式は以下となります。
SGDと異なる点は、\(v\)と\(α\)が存在している点です。
\(v\)はよく「速度」に例えられます。\(α\)は「速度に対する摩擦」です。
\(α\)は0から1の値を取り、\(α=0\)の時\(v = - η\frac{∂E}{∂W}\)となります。
これはつまり以下となります。
SGDの式となりました。つまり\(α=0\)の時は摩擦が0なので、過去の勾配結果が更新に全く影響を与えません。逆に\(α=1\)の時は摩擦が最大なので、過去の勾配結果がもろに更新に影響を与えるということになります。
momentumをプログラムに落とし込みます。内容的にSGDとあまり差がないので、初めからクラスの実装をします。
import numpy as np
class Momentum:
# 各値の初期化
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum# 摩擦
self.v = None# 次の更新時に使う
def update(self, params, grads):
# 初回時はv = 0に指定する
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)# 重みの配列と同一形状
# パラメータごとに値を更新
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
実際に数値を入れて動作を確認します。
import numpy as np
class Momentum:
# 各値の初期化
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum# 摩擦
self.v = None# 次の更新時に使う
def update(self, params, grads):
# 初回時はv = 0に指定する
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)# 重みの配列と同一形状
# パラメータごとに値を更新
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
### 実際に数値を入力してみる ###
lr = 0.01
momentum = 0.9
optimizer = Momentum(lr=lr, momentum=momentum)# インスタンス化
params = {}# 重み
params['W1'] = np.array([
-1.953,
-0.008,
-0.236,
0.491,
-2.137
])
print(params['W1'])
print()
grads = {}# 勾配(適当な値)
grads['W1'] = np.array([
0.428,
2.229,
-1.067,
-1.456,
0.602
])
print(grads['W1'])
print()
optimizer.update(params, grads)# パラメータ更新
print(params['W1'])
[-1.953 -0.008 -0.236 0.491 -2.137]
[ 0.428 2.229 -1.067 -1.456 0.602]
[-1.95728 -0.03029 -0.22533 0.50556 -2.14302]
YOLOv1は、momentumを使用しているので、上記クラスを後に使用して重み更新を行っていく予定です。
因みに、論文だとmomentum = 0.9の設定です。本シリーズもこれで行きます。
余談ですが、ここではSGDとmomentumはそれぞれの最適化手法を分けて解説しましたが、PytorchではSGDの引数としてmomentumが設定されています。つまり「SGDのオプションとしてmomentum設定可能です」という意味合いですね。
ここは解釈の違いという感じだと思います。
4. まとめ
今回はニューラルネットワークの実装(重みの更新)について解説しました。
次回も是非みてみてください!
5. 参考文献
斎藤康毅 著 「ゼロから作るDeep Learning ーPythonで学ぶディープラーニングの理論と実装」
Link
Search