本文へスキップ
ひもとくAI

ミニバッチと、学習の進め方の工夫

レッスン 4/7

ミニバッチと確率的勾配降下法(SGD)

第4章の勾配降下法では、損失の坂を下る向き(勾配)を求め、次のように重み ww を少しずつ動かしました。

w←w−η×勾配w \leftarrow w - \eta \times \text{勾配}

η\eta(イータ)は 学習率 で、1回にどれだけ動かすかを決める数です。←\leftarrow は「右の値で、左を置きかえる」という意味です。

勾配を、何件のデータで計算するか

勾配は、データの損失の平均から計算します。何件のデータを使うかで、3つのやり方があります。

やり方1回の更新に使うデータよいところ困るところ
バッチ学習全部(例えば1347件)勾配が正確1回の更新が重い。データが多いと、計算しきれない
1件ずつ1件1回の更新が軽い勾配がばらつき、進む向きがふらふらする
ミニバッチ学習少しずつ(例えば32件)軽さと正確さのバランスがよいミニバッチの大きさを決める必要がある

全部のデータで正確な勾配を求め、その向きに下っていく方法(バッチ学習の勾配降下法)は、最急降下法 とも呼ばれます。実務ではほとんどの場合、ミニバッチ学習 を使います。第7章のミニプロジェクトでも、訓練データを32枚ずつに分けて学習させていました。ミニバッチは、毎周、訓練データの順番をでたらめに並べ替えてから作ります。でたらめに選んだデータで勾配を計算するので、この方法を 確率的勾配降下法(SGD、Stochastic Gradient Descent)と呼びます。

訓練データが1347件、ミニバッチが32件なら、1周で 1347÷32=42.09…1347 \div 32 = 42.09\ldots なので、43回重みを更新します(最後の1回は、残りの3件で計算します)。

学習率の大きさで、進み方が大きく変わる

手書き数字のデータで、ニューラルネットワーク(64 → 32 → 10)をSGDで20周学習させました。

学習率5周後の損失20周後の損失テストの正解率
0.011.9690.9220.820
0.10.3750.1090.949

学習率0.01では、坂をほんの少しずつしか下れず、20周しても損失がなかなか下がりません。学習率を大きくすれば速く進みますが、大きすぎると、谷を飛び越えて行ったり来たりし、かえって学習が進まなくなります(第4章)。ちょうどよい学習率を見つけるのは、意外に難しい のです。次のスライドから、この問題を和らげる工夫を見ていきます。

モメンタムとAdam:坂の下り方を工夫する

モメンタム:転がるボールのように、勢いをつける

SGDは、毎回その場の勾配だけを見て進みます。坂道を転がるボールを想像してください。ボールは、今の坂の向きだけでなく、それまでの 勢い を持って進みます。同じ向きの坂が続けば、どんどん速くなります。

この勢いを取り入れたのが モメンタム です。勢い vv を、次のように更新します。

v←β×v−η×勾配,w←w+vv \leftarrow \beta \times v - \eta \times \text{勾配}, \qquad w \leftarrow w + v

β\beta(ベータ)は、前の勢いをどれだけ残すかで、0.9がよく使われます。同じ向きの勾配が続くと、勢いが積み重なって速く進みます。勾配の向きが毎回逆になる(谷を行ったり来たりする)ときは、勢いが打ち消し合って、振れ幅が小さくなります。細長い谷のように、ある向きには急で、別の向きにはゆるやかな形の損失(学習が進みにくい形)で、特に効果があります。勢いで進んだ 先の位置 で勾配を計算する改良版を、Nesterovのモメンタム(ネステロフ)と呼びます。

Adam:重みごとに、進む幅を自動で調整する

重みによって、勾配がいつも大きいものと、いつも小さいものがあります。全部の重みを同じ学習率で動かすと、大きい方に合わせれば小さい方が進まず、小さい方に合わせれば大きい方が暴れます。

Adam(アダム)は、重みごとに、

  • 勾配の平均(勢い、モメンタムと同じもの)
  • 勾配の2乗の平均(その重みの勾配が、ふだんどれくらい大きいか)

を記録し、「勾配の平均 ÷ ふだんの大きさ」の向きに進みます。ふだん勾配が大きい重みは控えめに、小さい重みは大きめに動くので、重みごとに学習率を自動で調整しているようなものです。同じ考え方の前の世代の方法に、AdaGradやRMSPropがあります。

実験:4つの方法を比べる

前のスライドと同じ条件で、20周学習させました(モメンタムの β\beta は0.9)。

周損失05101520012
  • SGD(学習率0.01)
  • SGD(学習率0.1)
  • モメンタム(学習率0.01)
  • Adam(学習率0.01)
周ごとの訓練データの損失。SGD(学習率0.01)は1周目の約2.2からゆっくり下がり、20周目でも約0.92。SGD(学習率0.1)は速く下がるが、9周目(約0.37)や15周目(約0.28)のように、ところどころで損失が跳ね上がる。モメンタム(学習率0.01)は、跳ね上がらずになめらかに下がる。どちらも20周目に約0.11。Adam(学習率0.01)は最も速く、5周目に約0.10、20周目に約0.01まで下がる
方法学習率5周後の損失20周後の損失テストの正解率
SGD0.011.9690.9220.820
SGD0.10.3750.1090.949
モメンタム0.010.3700.1080.956
Adam0.010.0950.0130.969
Adam0.0010.8250.1720.949
  • モメンタムは、SGDの 10分の1の学習率 で、SGD(学習率0.1)とほぼ同じ速さで進みました。勢いが積み重なったためです。しかも、SGD(学習率0.1)のように損失が跳ね上がることがなく、なめらかに下がりました。勢いが、毎回の勾配のばらつきをならしているのです
  • Adam(学習率0.01)が最も速く下がりました。ただし、学習率0.001にすると、モメンタムより遅く なりました。Adamでも、学習率の選び方は大切です

Pythonでモメンタムを書く、よくある誤解と振り返り

モメンタムの更新は、勢い v を1つ増やすだけで書けます。

import numpy as np

def momentum_step(w, v, grad, lr=0.01, beta=0.9):
    v = beta * v - lr * grad      # 前の勢いを少し残し、今の勾配の分を足す
    w = w + v                     # 勢いの分だけ重みを動かす
    return w, v

w = np.array([1.0, -2.0])         # 重み
v = np.zeros_like(w)              # 勢い(最初は0)
for grad in [np.array([0.5, -0.1]), np.array([0.5, -0.1]), np.array([0.5, -0.1])]:
    w, v = momentum_step(w, v, grad)
    print(v)                      # 同じ勾配が続くと、勢いがだんだん大きくなる

同じ勾配 (0.5,−0.1)(0.5, -0.1) が3回続くと、1つ目の重みの勢いは −0.005→−0.0095→−0.01355-0.005 \to -0.0095 \to -0.01355 と、だんだん大きくなります。SGDなら、毎回 −0.005-0.005 ずつしか動きません。

PyTorchでは、torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9) や torch.optim.Adam(model.parameters(), lr=0.001) のように、1行で方法を選べます(第7章の対応表の optimizer です)。

よくある誤解

  • 「Adamを使えば、学習率を気にしなくてよい」: 実験では、学習率0.001のAdamは、学習率0.01のモメンタムより遅くなりました。Adamでも学習率は大切な設定です(ただし、よく使われる0.001前後で、まずまずうまくいくことが多い)
  • 「ミニバッチは大きいほどよい」: 大きいほど勾配は正確になりますが、1回の更新が重くなり、メモリも多く使います。また、ばらつきが少なすぎると、過学習しやすくなる場合があることも知られています。32〜256件がよく使われます
  • 「速く損失が下がる方法が、いつも一番よい」: 見るべきは、訓練データの損失の速さだけではなく、検証データでの結果です(前のレッスン)

振り返り

  • ミニバッチ学習: データを少しずつに分けて、勾配を計算し重みを更新する。でたらめに選ぶので、確率的勾配降下法(SGD)と呼ぶ
  • モメンタム: 前の勢いを残して進む。同じ向きなら速く、行ったり来たりは打ち消し合う
  • Adam: 重みごとに、勾配の大きさに合わせて進む幅を調整する。速いが、学習率の選び方は大切

演習

演習を読み込んでいます…