- AI ⚙️
勾配降下法とは|学習率・SGD・Adamを自作して収束を比較
結論
勾配降下法とは、誤差(損失)の「坂の傾き=勾配」を計算し、坂を下る向きへ少しずつパラメーターを動かして、誤差が最も小さくなる点を探す方法です。1回に動く幅を決めるのが学習率で、大きすぎると発散し、小さすぎるといつまでも終わりません。
この記事でわかること
- 学習率で発散・振動・収束が変わる境目を、式と実測で一致させた結果
- バッチ・ミニバッチ・SGD、モメンタム・RMSProp・Adam を numpy で自作した比較表
- 自作の SGD が scikit-learn の SGDClassifier と誤差 2.2e-16 で一致した手順
この記事で実際に動かしたもの:numpy で自作した勾配降下法(線形回帰・ロジスティック回帰)、バッチ・ミニバッチ・SGD、モメンタム・RMSProp・Adam と、scikit-learn の LinearRegression・LogisticRegression・SGDRegressor・SGDClassifier。
勾配降下法は、機械学習のモデルが「学習する」ときに裏で動いている計算です。目隠しをして山を下りるとき、足元の傾きだけを頼りに一歩ずつ下る様子にたとえられます。
flowchart TD
A[パラメーターを仮に決める] --> B[損失を計算する<br>予測と正解のずれ]
B --> C[勾配を計算する<br>どちらへ下れば減るか]
C --> D[学習率 × 勾配だけ動く]
D --> E{損失がほぼ<br>変わらなくなった?}
E -->|いいえ| B
E -->|はい| F[学習を終える]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| 勾配降下法とは | 勾配の逆向きへ少しずつ動き、損失の最小点を探す方法 | 勾配降下法とは |
| 更新式 | 新しい w = w − 学習率 × 勾配 | 勾配・偏微分と更新式 |
| 学習率とは | 1回に動く幅。大きすぎると発散、小さすぎると遅い | 学習率で発散・振動・収束が変わる |
| 学習率の上限 | 二乗誤差なら 2 ÷ 損失の曲がり具合の最大値(実測で一致) | 学習率で発散・振動・収束が変わる |
| 確率的勾配降下法(SGD)とは | データを1件(または少数)ずつ使って更新する方法 | バッチ・ミニバッチ・SGDの違い |
| Adam とは | モメンタム(勢い)と RMSProp(歩幅の自動調整)を合わせた方法 | モメンタム・RMSProp・Adam |
| どれが速いか | 問題による。斜めに細長い谷ではモメンタムが速かった | 最適化手法の収束の速さを比較 |
| 収束しない・nan になる | 学習率を下げる・特徴量を標準化する | よくあるエラーと判断表 |
確認環境:Ubuntu 24.04.5 LTS(CPU 2コア)の上で、Python 3.13.18、NumPy 2.5.3、SciPy 1.18.1、scikit-learn 1.9.1 を使い、numpy の計算は OPENBLAS_NUM_THREADS=1(1スレッド)で実行しました。データは scikit-learn 同梱の糖尿病データ(load_diabetes、442件・10特徴)と乳がんデータ(load_breast_cancer、569件・30特徴)、それに自作の5点のデータと2変数の関数です。勾配降下法・SGD・モメンタム・RMSProp・Adam はすべて numpy で自作し、scikit-learn の LinearRegression・LogisticRegression・SGDRegressor・SGDClassifier の結果と比べました。時間は同じマシンで他の処理と並行して測った値なので、桁の比較に使ってください。scikit-learn の設定の意味は公式ユーザーガイド(Stochastic Gradient Descent)、Adam の式は原論文(Kingma・Ba、arXiv:1412.6980)で確かめました。図は mermaid 11.16.0 で描画を確かめています(確認日 2026年11月5日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「勾配降下法とは|学習率・SGD・Adamを自作して収束を比較」
勾配降下法とは(意味と山下りのたとえ)
この節では、勾配降下法が何をする方法なのかと、「勾配」という言葉のふだんの意味と機械学習での意味の違いが分かります。
勾配降下法(こうばいこうかほう、Gradient Descent)は、損失関数(予測がどれだけ外れているかを1つの数にしたもの)を小さくするパラメーターを、繰り返し計算で探す方法です。線形回帰・ロジスティック回帰・ニューラルネットワークなど、多くのモデルの学習に使われます。
| 言葉 | ふだんの意味 | 機械学習での意味 |
|---|---|---|
| 勾配 | 坂や屋根の傾きの度合い | 損失を各パラメーターで偏微分した値を並べたもの(どちら向きにどれだけ急か) |
| 降下 | 高い所から下りること | 損失が減る向き(勾配と逆の向き)へパラメーターを動かすこと |
| 学習 | 知識や技術を身につけること | データに合うようにパラメーターの値を決めること |
| 収束 | 騒ぎなどが落ち着くこと | 繰り返しても損失やパラメーターがほとんど変わらなくなること |
| 発散 | 熱や光が外へ出ていくこと | 損失が繰り返すたびに大きくなり、最後は inf・nan になること |
霧の山で谷底を目指すとき、遠くは見えなくても、足元が下っている向きは分かります。その向きへ一歩進み、また足元を確かめる。これを繰り返すのが勾配降下法です。一歩の大きさが学習率、足元の傾きが勾配、谷底が損失が最小になるパラメーターにあたります。
1損失を決める
回帰なら平均二乗誤差(MSE)、分類ならロジスティック損失(交差エントロピー)。何を小さくしたいかを1つの数にする。
2勾配を計算する
損失をパラメーターごとに偏微分する。符号が「上る向き」、大きさが「坂の急さ」を表す。
3逆向きへ少し動く
学習率 × 勾配を引く。損失がほとんど変わらなくなるまで繰り返す。
機械学習の全体像(教師あり・教師なし・ディープラーニングとの関係)は、この記事とは役割を分けて「機械学習とは|種類・ディープラーニングとの違いを実行結果で解説」で扱っています。この記事は、その中の「学習の計算」の部分を掘り下げます。
最初の5分:10行で勾配降下法を動かす
この節では、numpy だけで勾配降下法を動かし、損失が下がっていく様子を数字で確かめられます。
5点のデータに直線 y = w × x + b を当てはめます。w と b を0から始め、学習率 0.05 で1,000回更新しました。
import numpy as np
x = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) # 入力
y = np.array([3.1, 4.9, 7.2, 8.8, 11.1]) # 正解
w, b, lr = 0.0, 0.0, 0.05 # 重み・切片・学習率
for t in range(1001):
e = (w * x + b) - y # 予測 - 正解
loss = np.mean(e ** 2) # 平均二乗誤差(MSE)
gw = 2 * np.mean(e * x) # loss を w で偏微分
gb = 2 * np.mean(e) # loss を b で偏微分
if t in (0, 1, 2, 5, 10, 100, 1000):
print(f"{t:>4}回目 w={w:.4f} b={b:.4f} loss={loss:.4f}")
w -= lr * gw # 坂を下る向きへ少し動く
b -= lr * gb
print("最小二乗法の答え:", np.polyfit(x, y, 1))
0回目 w=0.0000 b=0.0000 loss=57.2220
1回目 w=2.5040 b=0.7020 loss=1.9754
2回目 w=2.0430 b=0.5826 loss=0.1221
5回目 w=2.1083 b=0.6247 loss=0.0543
10回目 w=2.0982 b=0.6594 loss=0.0492
100回目 w=2.0133 b=0.9658 loss=0.0227
1000回目 w=1.9900 b=1.0500 loss=0.0214
最小二乗法の答え: [1.99 1.05]
最初の数回で損失は大きく減り、その後はゆっくり最小値へ近づきます。「最初は速く、終わりは遅い」のが勾配降下法の典型的な形です。谷底に近づくほど傾きが小さくなり、一歩も小さくなるためです。1,000回目の w=1.99・b=1.05 は、式で一度に解く最小二乗法(np.polyfit)と同じ値になりました。最小二乗法の式と手計算は、関連記事「最小二乗法とは|公式・手計算の例とPythonでの求め方を実例で解説」で扱っています。
勾配・偏微分と勾配降下法の更新式
この節では、勾配の意味、偏微分の計算、更新式の読み方が、さきほどのコードと対応させて分かります。
更新式は w ← w − η × ∂L/∂w の1行です。η(イータ)が学習率、∂L/∂w が「損失 L を w で偏微分した値」です。偏微分とは、ほかの変数を定数とみなして、1つの変数だけで微分することです。
なぜ「マイナス」なのか
微分の値(傾き)が正なら、w を増やすと損失が増えます。だから w を減らす。傾きが負なら逆に増やす。どちらの場合も「傾きの符号と逆へ動く」ので、引き算1つで書けます。傾きが急なほど大きく、緩やかなほど小さく動くのも、この式の性質です。
平均二乗誤差の偏微分
損失を L = (1/n) Σ (w·x_i + b − y_i)² とします。ずれを e_i = w·x_i + b − y_i と書くと、合成関数の微分から次の2つが出ます。
∂L/∂w = (2/n) Σ e_i · x_i(コードのgw = 2 * np.mean(e * x))∂L/∂b = (2/n) Σ e_i(コードのgb = 2 * np.mean(e))
特徴量が複数ある場合は、行列 X(行がデータ、列が特徴量)を使い 勾配 = (2/n) Xᵀ e とまとめて計算します。numpy なら 2 * X.T @ e / n の1行です。
自分で確かめる:数値微分と比べる
自作の勾配が正しいかは、w を少しだけ動かしたときの損失の変化(数値微分)と比べると分かります。0回目の値で比べると、式の勾配 gw = −50.08 に対し、(L(w+1e-6) − L(w−1e-6)) / 2e-6 も −50.08 になりました。式を書き間違えるとここで食い違うので、自作するときは最初に確かめる手順です。
学習率とは:発散・振動・収束が変わる境目を実測
この節では、学習率の大きさで結果がどう変わるかと、発散する学習率の境目を計算で予想できることが分かります。
学習率(learning rate)は、1回の更新でどれだけ動くかを決める数で、人が先に決めるハイパーパラメーターです。いちばん単純な f(w) = (w − 3)²(最小点は w=3)で、学習率だけを変えて50歩動かしました。
for lr in [0.01, 0.1, 0.5, 0.9, 1.0, 1.1]:
w = 0.0
path = [w]
for _ in range(50):
w = w - lr * 2 * (w - 3) # f(w)=(w-3)^2 の勾配は 2(w-3)
path.append(w)
print(f"lr={lr:<4} 最初の5歩={[round(v, 2) for v in path[1:6]]} 50歩後={path[50]:.5g}")
lr=0.01 最初の5歩=[0.06, 0.12, 0.18, 0.23, 0.29] 50歩後=1.9075
lr=0.1 最初の5歩=[0.6, 1.08, 1.46, 1.77, 2.02] 50歩後=3
lr=0.5 最初の5歩=[3.0, 3.0, 3.0, 3.0, 3.0] 50歩後=3
lr=0.9 最初の5歩=[5.4, 1.08, 4.54, 1.77, 3.98] 50歩後=3
lr=1.0 最初の5歩=[6.0, 0.0, 6.0, 0.0, 6.0] 50歩後=0
lr=1.1 最初の5歩=[6.6, -1.32, 8.18, -3.22, 10.46] 50歩後=-27298
| 学習率 | 動き方 | 50歩後 | 呼び方 |
|---|---|---|---|
| 0.01 | 同じ向きに少しずつ | 1.91(まだ途中) | 遅すぎる |
| 0.1 | 同じ向きに、だんだん小さく | 3(収束) | 収束 |
| 0.5 | 1歩で最小点に着く | 3 | この関数での最適値 |
| 0.9 | 左右に行き来しながら縮む | 3(収束) | 振動しながら収束 |
| 1.0 | 0 と 6 を永遠に往復 | 0 | 振動(境目) |
| 1.1 | 行き来しながら広がる | −27,298 | 発散 |
この関数は2階微分(曲がり具合)が2で、学習率 × 曲がり具合が2を超えると発散します。1.0 × 2 = 2 がちょうど境目で、表のとおり往復が止まりません。
本物のデータでも境目は計算で当たる
パラメーターが多いと、曲がり具合は方向ごとに違います。二乗誤差の場合、その最大値はヘッセ行列(2階微分を並べた行列)(2/n) XᵀX の最大固有値 λmax で、学習率の上限は 2 ÷ λmax です。標準化した糖尿病データ(切片の列を含む)では λmax = 8.048 で、上限は 0.2485 と計算できます。
| 学習率 | 結果 | 最小の損失+0.01%に届いた回数 | 10回目の損失 |
|---|---|---|---|
| 0.001 | 収束 | 127,332回(4.40秒) | 27,842.6 |
| 0.01 | 収束 | 12,733回 | 19,473.1 |
| 0.1 | 収束 | 1,273回 | 3,167.9 |
| 0.2 | 収束 | 636回 | 2,882.5 |
| 0.24 | 収束 | 530回 | — |
| 0.248 | 収束(上限の直前) | 1,097回 | — |
| 0.249 | 発散(上限の直後) | 損失が1e30を超えた | — |
| 0.3 | 発散 | 89回目で1e30超え | 1,883,990 |
最小の損失(MSE)は 2,859.70 です。計算で出した上限 0.2485 を挟んで、0.248 は収束し、0.249 は発散しました。上限の少し手前(0.24)が最も速く、上限に近づきすぎると往復が増えて遅くなります(0.248 は1,097回)。学習率を10分の1にすると、かかる回数はほぼ10倍になりました(0.01 → 12,733回、0.001 → 127,332回)。
学習率が大きすぎる
損失が増える・行ったり来たりする
最後は inf・nan になる
まず学習率を3分の1〜10分の1にする
学習率が小さすぎる
損失は減るが、いつまでも下がり続ける
回数の上限で止まり、途中の値になる
学習率を3倍ずつ上げて試す
線形回帰を勾配降下法で自作する(LinearRegressionと一致)
この節では、特徴量が10個ある線形回帰を勾配降下法で解き、scikit-learn の答えと一致することを確かめます。
LinearRegression は勾配降下法ではなく、最小二乗の式を直接解きます。同じ損失の最小点なので、勾配降下法を十分な回数まわせば同じ係数になるはずです。
import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
Xr, y = load_diabetes(return_X_y=True, scaled=False) # 元の単位のまま
X = StandardScaler().fit_transform(Xr) # 平均0・標準偏差1に
n, d = X.shape
w, b, lr = np.zeros(d), 0.0, 0.2
for t in range(30000):
e = X @ w + b - y
w -= lr * 2 * X.T @ e / n
b -= lr * 2 * e.mean()
ref = LinearRegression().fit(X, y)
print("差の最大:", np.max(np.abs(w - ref.coef_)))
print("sklearn", np.round(ref.coef_, 2))
print("自作 ", np.round(w, 2))
差の最大: 8.313350008393172e-13
sklearn [ -0.48 -11.41 24.73 15.43 -37.68 22.68 4.81 8.42 35.73 3.22]
自作 [ -0.48 -11.41 24.73 15.43 -37.68 22.68 4.81 8.42 35.73 3.22]
10個の係数の差は最大でも 8.3e-13 で、計算機の丸め誤差の範囲で一致しました。損失が最小値+1e-12(相対)に届いた時点(学習率 0.1・6,647回)では差は 4.1e-4 で、「損失がほぼ最小」と「係数がほぼ一致」は同じではないことも分かります。係数そのものを解釈したい場合は、止める条件を厳しめにするか、式で解く方法を使います。回帰分析の読み方(p値・VIF など)は「回帰分析とは|単回帰・重回帰・ロジスティック回帰の違いをp値・VIFで実演」で扱っています。
ロジスティック回帰を勾配降下法で自作する
この節では、分類のモデルでも勾配の形が同じになることと、scikit-learn の LogisticRegression と一致させる設定が分かります。
ロジスティック回帰の損失(交差エントロピー)を偏微分すると、勾配は (1/n) Xᵀ(p − y) になります。p はシグモイド関数で出した確率で、「予測 − 正解」に X を掛ける形は線形回帰と同じです。
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
X, y = load_breast_cancer(return_X_y=True)
X = StandardScaler().fit_transform(X); n, d = X.shape
lam = 1e-2 # L2 正則化の強さ
def grad(w, b):
p = 1 / (1 + np.exp(-(X @ w + b))) # シグモイド
e = p - y
return X.T @ e / n + lam * w, e.mean() # 切片には正則化をかけない
w, b = np.zeros(d), 0.0
for t in range(20000):
gw, gb = grad(w, b)
w -= 1.0 * gw; b -= 1.0 * gb
sk = LogisticRegression(C=1 / (n * lam), tol=1e-12, max_iter=10000).fit(X, y)
print(np.max(np.abs(w - sk.coef_[0])), abs(b - sk.intercept_[0]))
3.1107307268962714e-07 3.505831908112711e-08
scikit-learn の LogisticRegression は「C × 損失の合計 + ½‖w‖²」を最小にします。両辺を C × n で割ると「損失の平均 + 1/(2Cn) × ‖w‖²」になるので、C = 1/(n × λ) とすれば自作の損失と同じ問題になります。係数の差は 3.1e-7、正解率はどちらも 98.59% でした。
この問題では、理論上の安全な上限(損失の曲がり具合の上限から計算した 2/L)は 0.60 でしたが、実際には学習率 4.0 でも収束しました。上限の計算は「どんな点でも曲がり具合は最大 1/4」という最悪の場合を使うため、安全側に出ます。
| 学習率 | 最小値との差が0.001未満になる回数 | 1e-6未満になる回数 |
|---|---|---|
| 1.0 | 51回 | 272回 |
| 2.0 | 27回 | 141回 |
| 3.0 | 27回 | 98回 |
| 4.0 | 26回 | 77回 |
バッチ・ミニバッチ・確率的勾配降下法(SGD)の違い
この節では、1回の更新に使うデータの量で、速さと揺れ方がどう変わるかが実測で分かります。
確率的勾配降下法(SGD、Stochastic Gradient Descent)は、データを1件ずつランダムな順で使って勾配を計算し、そのたびに更新する方法です。全データで1回更新するのがバッチ勾配降下法(最急降下法とも呼ぶ)、数十件ずつ使うのがミニバッチ勾配降下法です。深層学習のライブラリでは、ミニバッチで更新する方法もまとめて SGD と呼ぶことが多いです。
| 方法 | 1回の更新に使う量 | 長所 | 短所 |
|---|---|---|---|
| バッチ | 全データ | 動きが滑らか。毎回損失が減る | データが多いと1回が重い。メモリに全部載せる |
| ミニバッチ | 数十〜数百件 | 行列計算の速さと更新回数の多さを両立 | バッチの大きさも調整が要る |
| SGD(1件ずつ) | 1件 | メモリが少ない。データを追加しながら学習できる | 揺れが大きく、学習率を一定にすると最小点の周りで揺れ続ける |
乳がんデータのロジスティック回帰(正則化 λ=0.01)で、全データを1周する単位(エポック)ごとに最小値との差を測りました。
| 方法(学習率) | 差が0.001未満になったエポック | 50エポック後の差 | 50エポックの時間 | 更新回数 |
|---|---|---|---|---|
| バッチ(1.0) | 51(50では届かず) | 0.00102 | 0.01秒 | 50 |
| ミニバッチ32件(0.1) | 29 | 0.000205 | 0.03秒 | 900 |
| ミニバッチ32件(0.5) | 6 | 0.0000137 | 0.05秒 | 900 |
| SGD 1件(0.01) | 10 | 0.0000130 | 1.16秒 | 28,450 |
| SGD 1件(0.05) | 5 | 0.000466(揺れ 0.00047〜0.0043) | 1.20秒 | 28,450 |
11周あたりはSGDが速い
同じ1周でも更新回数が569倍あるので、早いエポックで近くまで行く(5〜10エポック)。
2時間ではバッチが速い
numpy は1件ずつのループが遅く、SGD の50エポックは1.16秒、バッチは0.01秒だった。
3一定の学習率のSGDは揺れ続ける
学習率0.05では最後の5エポックの差が0.00047〜0.0043の間を上下した。学習率を下げると揺れも小さくなる。
ミニバッチは乱数の種で結果が変わります。学習率0.5で種を5通り変えると、50エポック後の差は 0.00001〜0.00033 に散らばりました。SGD で結果を比べるときは、乱数の種を固定するか、複数の種の平均で比べます。
特徴量のスケールで収束が遅れる(標準化の効果)
この節では、特徴量の単位がそろっていないと勾配降下法が極端に遅くなる理由と、その実測が分かります。
糖尿病データを元の単位のまま(年齢は歳、血圧は mmHg、血清の値など)使うと、特徴量の標準偏差は 0.5〜34.6 とばらばらです。この状態で同じ線形回帰を解きました。
| データ | 曲がり具合の最大÷最小(条件数) | 学習率の上限 | 結果 |
|---|---|---|---|
| 標準化した | 470 | 0.2485 | 学習率0.2で636回で収束 |
| 元の単位のまま | 52,365,337 | 0.0000136 | 学習率0.00001で20万回まわしても損失3,047.6(最小は2,859.7)。0.00005で16回目に発散 |
単位の大きい特徴量の方向は坂が急なので、学習率はそこに合わせて小さくするしかありません。すると、単位の小さい特徴量の方向は、ほとんど動かなくなります。谷が極端に細長い形になり、急な壁の間を行き来しながら、谷底に沿ってほとんど進まないのが原因です。標準化(平均0・標準偏差1にそろえる)で条件数は約11万分の1になりました。
scikit-learn の公式ユーザーガイドも「Stochastic Gradient Descent is sensitive to feature scaling, so it is highly recommended to scale your data.」と書き、make_pipeline(StandardScaler(), SGDClassifier()) の形を勧めています。手元でも、元の単位のまま SGDRegressor(既定の設定)に入れると、エラーも警告も出ないまま決定係数 R² が −4.7e22 になりました。標準化してから入れると R² は 0.514 で、LinearRegression(0.518)とほぼ同じです。
- 1標準化は訓練データだけで fit するテストデータの平均・標準偏差を使うと、評価が甘くなる。Pipeline に入れると自然に守られる
- 2予測するときも同じ変換を通す学習時の StandardScaler をそのまま使う。作り直さない
- 3エラーが出ないことを「正しい」と思わないSGDRegressor は R² が −4.7e22 でも黙って終わった。score で必ず確かめる
モメンタム・RMSProp・Adamの仕組み(numpyで自作)
この節では、勾配降下法を速く・安定させる3つの工夫が、それぞれ何を足したものかがコードで分かります。
モメンタムは「勢い」、RMSProp は「方向ごとの歩幅の自動調整」、Adam はその両方を合わせたものです。どれも勾配そのものは同じで、「勾配をどう使って動くか」だけが違います。
class GD:
def __init__(s, lr): s.lr = lr
def step(s, th, g): return th - s.lr * g
class Momentum: # 過去の勾配を勢いとしてためる
def __init__(s, lr, beta=0.9): s.lr, s.b, s.v = lr, beta, None
def step(s, th, g):
s.v = g if s.v is None else s.b * s.v + g
return th - s.lr * s.v
class RMSProp: # 勾配の2乗の移動平均で割る
def __init__(s, lr, rho=0.99, eps=1e-8): s.lr, s.r, s.e, s.s = lr, rho, eps, None
def step(s, th, g):
s.s = (1 - s.r) * g * g if s.s is None else s.r * s.s + (1 - s.r) * g * g
return th - s.lr * g / (np.sqrt(s.s) + s.e)
class Adam: # 勢い(m)と歩幅の調整(v)+偏りの補正
def __init__(s, lr, b1=0.9, b2=0.999, eps=1e-8):
s.lr, s.b1, s.b2, s.e, s.m, s.t = lr, b1, b2, eps, None, 0
def step(s, th, g):
if s.m is None: s.m = np.zeros_like(th); s.v = np.zeros_like(th)
s.t += 1
s.m = s.b1 * s.m + (1 - s.b1) * g
s.v = s.b2 * s.v + (1 - s.b2) * g * g
mh = s.m / (1 - s.b1 ** s.t) # 最初のうち 0 に寄る偏りを補正
vh = s.v / (1 - s.b2 ** s.t)
return th - s.lr * mh / (np.sqrt(vh) + s.e)
モメンタム
前の動きの0.9倍を足し続ける。同じ向きが続くと加速し、左右の往復は打ち消し合う。
細長い谷に強い
RMSProp
方向ごとに、最近の勾配の大きさ(2乗の平均の平方根)で割る。急な方向は小さく、緩い方向は大きく動く。
単位の違いに強い
Adam
モメンタムの勢い m と、RMSProp の大きさ v の両方を使い、最初の偏りを補正する。
既定値で動きやすい
素の勾配降下法
勾配に学習率を掛けて引くだけ。仕組みが単純で、上限の計算もしやすい。
比較の基準
Adam の β1=0.9・β2=0.999・ε=1e-8 は原論文(arXiv:1412.6980)で示された既定値です。m と v は0から始めるので、最初の数回は0に寄った値になります。1 − β^t で割る補正(バイアス補正)は、それを打ち消すためのものです。
自作の Adam を scikit-learn の内部クラス sklearn.neural_network._stochastic_optimizers.AdamOptimizer(MLPClassifier が使うもの)と同じ勾配で200回動かすと、差は最大 6.2e-8 でした。scikit-learn は補正を学習率の側にまとめて掛ける書き方で、ε を足す位置が少し違うためです(原論文の2節に、計算の順を入れ替えたこの書き方も載っています)。内部クラスは公開の API ではないので、ここでは確かめのためだけに使いました。
最適化手法の収束の速さを比較(SGD・モメンタム・RMSProp・Adam)
この節では、4つの方法を同じ問題で動かし、それぞれ最も速かった学習率で何回かかったかを比べます。
学習率は 0.0001〜1 を3倍刻みで試し(糖尿病データは 0.00001〜10)、目標に届いた中で最も少ない回数を載せました。「—」は試したどの学習率でも上限の回数内に届かなかったものです。
| 問題(目標) | GD | モメンタム | RMSProp | Adam |
|---|---|---|---|---|
| 斜めに細長い谷(条件数100、f<1e-6) | 857回(0.01) | 171回(0.03) | 801回(0.1) | 397回(1) |
| 軸に沿って細長い谷(同上) | 882回(0.01) | 141回(0.003) | 7回(1) | 151回(1) |
| ローゼンブロック関数(f<1e-6、上限2万回) | 15,681回(0.001) | 1,484回(0.001) | — | 983回(0.1) |
| 糖尿病・標準化(最小の損失+0.1%) | 601回(0.1) | 78回(0.1) | 244回(1) | 102回(10) |
| 糖尿病・元の単位(1万回後の損失÷最小) | 1.106 | 1.073 | 1.056 | 1.016 |
読み取れることは4つです。
- モメンタムはどの問題でも素の GD より大幅に速い。細長い谷の往復を打ち消すためです。素の GD が発散した学習率 0.03 でも、モメンタムは収束しました(勢いの分だけ安定な範囲が広い)。
- RMSProp の「7回」は、谷が座標軸に沿っていたための偶然に近い値です。同じ谷を45度回すと801回になり、GD とほとんど変わりませんでした。方向ごとの調整は「軸ごと」なので、斜めの谷には効きません。
- 学習率を一定にした RMSProp は、ローゼンブロック関数で目標に届きませんでした。最小点の近くで勾配が小さくなると、割る数も小さくなり、歩幅が縮まないためです。Adam は勢いの平均化で、この揺れが小さくなりました。
- 単位がそろっていないデータでは、Adam が最も残りが少なかった(1.6%)が、標準化した GD(601回で0.1%以内)には遠く及びません。手法を変えるより、先に標準化するほうが効きます。
この比較は、小さな凸の問題と2変数の関数での結果です。ニューラルネットワークのような大きな問題では順位が入れ替わることもあり、Adam が常に最良というわけではありません。ニューラルネットワークの学習の流れ(誤差逆伝播で勾配を出す部分)は、関連記事「ニューラルネットワークとは|仕組み・学習・AttentionをNumPyで実演」で扱っています。
scikit-learnのSGDClassifier・SGDRegressorと自作SGDの一致
この節では、scikit-learn の SGD が中で何をしているかを、自作のコードと完全に一致させて確かめます。
シャッフルなし・学習率一定・正則化なしにそろえると、自作の1件ずつの SGD と SGDClassifier の係数は 2.2e-16(倍精度の丸め誤差)で一致しました。
from sklearn.linear_model import SGDClassifier
ys = np.where(y == 1, 1.0, -1.0) # scikit-learn は内部で -1/+1 を使う
eta, E = 0.01, 5
sc = SGDClassifier(loss="log_loss", penalty=None, learning_rate="constant",
eta0=eta, max_iter=E, tol=None, shuffle=False).fit(X, y)
w, b = np.zeros(d), 0.0
for ep in range(E):
for i in range(n): # データを1件ずつ
z = X[i] @ w + b
dl = -ys[i] / (np.exp(ys[i] * z) + 1.0) # ロジスティック損失の微分
w -= eta * dl * X[i]; b -= eta * dl
print(np.max(np.abs(w - sc.coef_[0])), abs(b - sc.intercept_[0]))
2.220446049250313e-16 1.1102230246251565e-16
SGDRegressor も、損失を ½(予測 − 正解)² として同じ形で書くと 5.3e-15 で一致しました。t_ は「更新回数+1」で、442件 × 3周 + 1 = 1,327 です。
既定の設定は自作とどこが違うか
| 設定 | SGDClassifier の既定 | 意味 |
|---|---|---|
| learning_rate | "optimal"(回帰は "invscaling") | 回数が進むほど学習率を下げる。公式の式は η = 1/(α(t₀+t)) |
| alpha | 0.0001 | L2 正則化の強さ。optimal の学習率にも使われる |
| max_iter・tol | 1000・0.001 | 損失の改善が tol 未満の状態が続いたら止まる |
| shuffle | True | 毎周データの順を混ぜる。結果は random_state で固定する |
| average | False | True で更新の平均を係数にする(平均化SGD) |
既定の設定(loss="log_loss"・random_state=0)で乳がんデータを学習すると、23周で止まり正解率は 98.77% でした。max_iter=2 にすると次の警告が出ます。
ConvergenceWarning: Maximum number of iteration reached before convergence. Consider increasing max_iter to improve the fit.
公式ユーザーガイドは、経験的に約100万件分の更新で収束するとして、max_iter = np.ceil(10**6 / n) を最初の目安に挙げています。
局所解・鞍点・凸関数(勾配降下法の限界)
この節では、勾配降下法が「いちばん低い点」にたどり着けない場合と、この記事の線形モデルでは心配が要らない理由が分かります。
勾配降下法は、足元の傾きしか見ないので、着いた点が全体で最も低い点(大域最適解)とは限りません。周りより低いだけの点(局所解)や、ある方向には谷・別の方向には山になっている点(鞍点)でも勾配は0になり、そこで止まることがあります。
| 状況 | 何が起きるか | よく使われる対策 |
|---|---|---|
| 凸関数(線形回帰・正則化ありのロジスティック回帰) | 谷底が1つだけ。学習率が適切なら必ず最小点に着く | 対策は不要。学習率と標準化だけ見る |
| 局所解 | 近くの小さな谷で止まる | 初期値を変えて複数回学習する・SGD の揺れ |
| 鞍点・平らな所(プラトー) | 勾配がほぼ0で、長く進まない | モメンタム・Adam の勢い |
| 細長い谷 | 壁の間を往復して進まない | 標準化・モメンタム |
この記事で使った線形回帰と、L2 正則化をかけたロジスティック回帰の損失は凸関数です。そのため、どの方法でも最後は scikit-learn と同じ答えに着きました。一方、正則化なしのロジスティック回帰で、データがほぼ一直線で分けられる場合は、係数が無限に大きくなる方向へ進み続け、最小点がありません。scikit-learn の LogisticRegression が既定で L2 正則化(C=1.0)をかけているのは、このためでもあります。
よくあるエラー・つまずきと判断表
この節では、勾配降下法を自作・利用するときに出るエラーの原文と直し方、手法と学習率の選び方が分かります。
元の単位の糖尿病データで、学習率 0.001 のまま勾配降下法をまわすと、損失は次のように増え、71回目で inf になりました。
1 29074.481900452487
2 503585734.6648108
3 10761250618149.344
10 2.189849536826106e+43
50 3.4051582913661496e+216
/usr/local/lib/python3.13/dist-packages/numpy/_core/_methods.py:132: RuntimeWarning: overflow encountered in reduce
ret = umr_sum(arr, axis, dtype, out, keepdims, where=where)
71 inf
| 症状・原文 | 原因 | 直し方 |
|---|---|---|
損失が毎回何倍にも増える・RuntimeWarning: overflow encountered in reduce・inf・nan | 学習率が上限を超えている | 学習率を10分の1にする。特徴量を標準化する |
RuntimeWarning: overflow encountered in exp | シグモイドに大きな値が入った | 損失は np.logaddexp(0, z) で書く。学習率を下げる |
RuntimeWarning: divide by zero encountered in log | 確率がちょうど0か1になり log(0) | 上と同じく logaddexp で書く。確率を少しだけ内側に丸める |
ConvergenceWarning: Maximum number of iteration reached before convergence. | max_iter の回数内に止まる条件を満たさない | 標準化を確かめ、max_iter を増やす |
| エラーは無いが R² が大きく負(−4.7e22) | 単位のばらばらなデータを SGDRegressor に入れた | Pipeline で StandardScaler を前に入れる |
| 損失は下がるが、ずっと下がり続ける | 学習率が小さすぎる・回数が足りない | 学習率を3倍ずつ上げる |
| 実行のたびに結果が少し違う | SGD・ミニバッチの順番が乱数で決まる | random_state(乱数の種)を固定する |
発散したときに、先に手法を変える
元の単位のままでは Adam でも1万回で1.6%残り、標準化した GD は601回で0.1%以内だった
判断表:手法と学習率の選び方
| 場面 | 選ぶもの | 理由 |
|---|---|---|
| 表データの線形回帰・ロジスティック回帰(数万件まで) | LinearRegression・LogisticRegression(勾配降下法を自作しない) | 式やより速い解き方で、止める条件の心配が少ない |
| データが大きくメモリに載らない・少しずつ届く | SGDClassifier・SGDRegressor(partial_fit)+標準化 | 1件ずつ・少量ずつ学習できる |
| 仕組みを理解したい・試験の勉強 | numpy で GD を自作し、scikit-learn と一致させる | 一致すれば式とコードが正しいと分かる |
| ニューラルネットワークの学習 | まず Adam(学習率0.001前後)か、モメンタム付きSGD | 既定値で動きやすい。最終的な精度は問題ごとに比べる |
| 学習率を決める | 0.001〜1 を3倍刻みで試し、発散の手前を選ぶ | 回数は学習率にほぼ反比例。上限の少し手前が最速だった |
- 1特徴量を標準化する条件数が 5,236万 → 470 になった
- 2学習率を3倍刻みで試す発散する値の1つ手前を選ぶ
- 3損失を毎回(または毎周)記録する増えていたら発散、平らなら収束か小さすぎ
- 4遅ければモメンタム・Adam を試す細長い谷・平らな所で効く
- 5ライブラリの答えと比べる自作なら LinearRegression・SGDClassifier と一致するか
numpy の配列の作り方や行列の掛け算(@)は、関連記事「NumPyの使い方 基本一覧|配列の作成・演算・形状変更」で扱っています。勾配降下法は、標準化や固有値の考え方とつながっていて、固有値・分散の見方は関連記事「主成分分析(PCA)とは|手順・寄与率・負荷量の読み方をPythonで実演」で扱っています。
よくある質問
Q. 勾配降下法とは簡単に言うと何ですか? A. 誤差(損失)が減る向きを傾きから求め、その向きへ少しずつパラメーターを動かす作業を繰り返して、誤差が最も小さくなる値を探す方法です。霧の山を、足元の傾きだけを頼りに下りるのに似ています。
Q. 最急降下法と勾配降下法は同じですか? A. ほぼ同じ意味で使われます。全データで勾配を計算して更新する方法(バッチ勾配降下法)を、特に最急降下法と呼ぶことが多いです。
Q. 確率的勾配降下法(SGD)とは何ですか? A. データを1件(または少数)ずつランダムに選んで勾配を計算し、そのたびに更新する方法です。手元の実測では、1周あたりの進みはバッチより大きく、学習率を一定にすると最小点の周りで揺れ続けました。
Q. 学習率はいくつにすればよいですか? A. 問題によって違います。0.001〜1 程度を3倍刻みで試し、損失が増え始める値の1つ手前を選ぶのが手早い方法です。二乗誤差なら、上限は 2 ÷(ヘッセ行列の最大固有値)で計算でき、手元の実測とも一致しました。
Q. 学習率が大きすぎるとどうなりますか? A. 最小点を飛び越えて反対側へ行き、その幅がだんだん広がって損失が増え続けます(発散)。最後は inf や nan になり、numpy は overflow の RuntimeWarning を出します。
Q. Adam とは何ですか? A. 過去の勾配の平均(勢い)と、勾配の2乗の平均(方向ごとの歩幅の調整)を両方使う最適化手法です。最初の偏りを補正する計算が入っています。β1=0.9・β2=0.999 が原論文の既定値です。
Q. Adam を使えば SGD より必ず速いですか? A. いいえ。手元の比較では、斜めに細長い谷でもモメンタム(171回)が Adam(397回)より速く、標準化した線形回帰でもモメンタムが最も少ない回数でした。問題ごとに比べるのが確実です。
Q. なぜ特徴量を標準化すると速くなるのですか? A. 単位の違う特徴量があると、損失の谷が極端に細長くなり、学習率を最も急な方向に合わせて小さくするしかなくなるためです。糖尿病データでは、標準化で条件数が約11万分の1になり、20万回で終わらなかった計算が636回で終わりました。
Q. 勾配降下法はディープラーニングでも同じですか? A. 更新の考え方は同じです。違いは、パラメーターが非常に多いことと、勾配を誤差逆伝播法で計算すること、損失が凸ではないため局所解や鞍点がありうることです。
Q. 自作した勾配降下法が正しいか、どう確かめますか? A. 2つの方法があります。1つは数値微分(少し動かしたときの損失の変化)と勾配を比べること。もう1つは、同じ損失を scikit-learn で解き、係数が一致するかを見ることです。この記事では線形回帰で 8.3e-13、SGDClassifier で 2.2e-16 の差でした。
Q. G検定・E資格の勉強ではどこが大事ですか? A. 更新式、学習率の影響、バッチ・ミニバッチ・SGD の違い、モメンタム・AdaGrad・RMSProp・Adam の特徴がよく問われます。E資格の出題範囲と numpy での解き方は、関連記事「E資格とは|試験内容・合格率・G検定との違いとNumPyで解く要点」で扱っています。
まとめ
- 勾配降下法は「損失の傾きの逆へ少し動く」を繰り返す方法で、更新式は w ← w − 学習率 × 勾配の1行です。
- 学習率には上限があり、二乗誤差なら 2 ÷ λmax で計算できます。糖尿病データでは上限 0.2485 を挟み、0.248 は収束、0.249 は発散しました。
- バッチ・ミニバッチ・SGD は1回の更新に使うデータの量が違います。SGD は1周あたりの進みが大きい一方、揺れが残ります。
- モメンタム・RMSProp・Adam は「勾配の使い方」の工夫です。細長い谷ではモメンタムが GD の約5倍速く、ローゼンブロック関数では Adam が約16倍速く収束しました。
- 手法より先に標準化が効きます。自作の結果は LinearRegression・LogisticRegression・SGDClassifier と一致させて確かめると、式とコードの誤りに気づけます。
参考・出典
いずれも確認日は 2026年11月5日です。
- scikit-learn「1.5. Stochastic Gradient Descent」(User Guide):https://scikit-learn.org/stable/modules/sgd.html
- scikit-learn「SGDClassifier」:https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDClassifier.html
- scikit-learn「LogisticRegression」:https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html
- scikit-learn「StandardScaler」:https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html
- Diederik P. Kingma, Jimmy Ba「Adam: A Method for Stochastic Optimization」:https://arxiv.org/abs/1412.6980
- Sebastian Ruder「An overview of gradient descent optimization algorithms」:https://arxiv.org/abs/1609.04747
- Google for Developers「機械学習集中講座 線形回帰:勾配降下法」:https://developers.google.com/machine-learning/crash-course/linear-regression/gradient-descent?hl=ja
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「勾配降下法とは|学習率・SGD・Adamを自作して収束を比較」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。