Lesson 6 / 12

最適化・正則化と学習ループ

このレッスンで学ぶこと

  • 学習ループの定石 zero_grad → backward → step を読める
  • SGD・Adam・weight decay(L2正則化)・初期化(Xavier/He)の役割が分かる
  • dropout と BatchNorm の train()/eval() での挙動差を説明できる
  • DataLoader でミニバッチ学習する流れを読める

1. 学習ループの全体像

モデルを「賢く」するには、データを何度も見せてパラメータを少しずつ更新します。その1ステップは必ず次の順序です。

  1. optimizer.zero_grad() … 前のステップの勾配を消す
  2. out = model(x) … 順伝播(予測)
  3. loss = loss_fn(out, y) … 損失を計算
  4. loss.backward() … 逆伝播(勾配を計算)
  5. optimizer.step() … パラメータを更新

この順序は実装で間違えやすい要点です。なぜこの順番なのかも含めて、実際のコードで確認します。

2. 学習ループのコード

合成データ(4特徴の2クラス分類)で5エポック学習します。損失が下がっていくのがポイントです。

train_loop.py(PyTorch・読むだけ)
import torch
import torch.nn as nn
torch.manual_seed(0)

X = torch.randn(200, 4)
y = (X.sum(dim=1) > 0).long()   # 合計が正なら1, それ以外0(2クラス)

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(4, 16)
        self.fc2 = nn.Linear(16, 2)
    def forward(self, x):
        return self.fc2(torch.relu(self.fc1(x)))

model = Net()
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

for epoch in range(5):
    optimizer.zero_grad()       # ① 勾配リセット
    out = model(X)              # ② 順伝播
    loss = loss_fn(out, y)     # ③ 損失
    loss.backward()            # ④ 逆伝播
    optimizer.step()           # ⑤ 更新
    print(f"epoch {epoch} loss {loss.item():.4f}")
▼ 出力
epoch 0 loss 0.6891
epoch 1 loss 0.6688
epoch 2 loss 0.6508
epoch 3 loss 0.6347
epoch 4 loss 0.6199

💡 上の y = (X.sum(dim=1) > 0).long() は何をしている? 正解ラベルが手元に無いので、入力 X から人工的に2クラスのラベルを作っています。3つの処理を順に追うと:

  • X.sum(dim=1)dim=1 は「特徴の軸」。各サンプル(行)の4特徴を足し合わせて1つの数にする。形は (200, 4)(200,)
  • > 0 … その合計が正かどうかを判定。True/False の真偽テンソルになる(例:合計2.3→True、合計−1.1→False)。
  • .long() … True/False を整数の 1/0 に変換CrossEntropyLoss の正解は「クラス番号の整数」(L05参照)なので、この型にそろえる。

結果として「4特徴の合計が正なら クラス1、そうでなければ クラス0」というルールのデータができます(このデータでは約56%がクラス1)。

⚠ なぜ zero_grad が先頭なのか(つまずきやすい点): PyTorch は backward() のたびに勾配を 足し算(累積)します(自動でリセットしない)。 zero_grad() を忘れると、前のステップの勾配が残ったまま更新され、学習が壊れます。 また backward()step() を呼ぶと、更新に使う勾配がまだ無いので正しく学習できません。

3. 最適化器(optimizer)と初期化

学習の「更新のしかた」を決めるのが最適化器(optimizer)、学習を始める前の「重みの初期値」を決めるのが初期化(initialization)です。 どちらも PyTorch では1〜2行で設定できます。「どう書くか」を順番に見ます。

① 最適化器(optimizer)をどう書くか

torch.optim.〇〇(model.parameters(), ...) の形で作ります。第1引数に更新したいパラメータ(=model.parameters())を渡し、 あとは lr(学習率)などを指定するだけ。代表は SGDmomentum を付けるのが定番)と Adam です。

optimizer.py(PyTorch・読むだけ)
import torch
import torch.nn as nn
torch.manual_seed(0)
lin = nn.Linear(4, 3)

# SGD:シンプル。momentum で「慣性」をつけ、収束を速める
sgd = torch.optim.SGD(lin.parameters(), lr=0.1, momentum=0.9)

# Adam:学習率を自動調整。weight_decay は L2正則化(重みを小さく保つ)
adam = torch.optim.Adam(lin.parameters(), lr=1e-3, weight_decay=1e-4)

print("SGD  lr:", sgd.param_groups[0]["lr"], "momentum:", sgd.param_groups[0]["momentum"])
print("Adam lr:", adam.param_groups[0]["lr"], "weight_decay:", adam.param_groups[0]["weight_decay"])
▼ 出力
SGD  lr: 0.1 momentum: 0.9
Adam lr: 0.001 weight_decay: 0.0001

💡 どちらを選ぶ? 迷ったら Adamlr=1e-3 あたり)から始めるのが無難。学習率を自動調整するので安定しやすいです。 よく調整した SGD + momentum は Adam より良い汎化を出すこともあり、画像分類などで定番。 weight_decayどちらの optimizer にも引数で渡せるL2正則化で、過学習を抑えたいときに足します。

② 初期化(initialization)をどう書くか

学習を始める前の重みの初期値を、活性化に合わせて設定します。PyTorch には初期化関数を集めた nn.init があり、 nn.init.kaiming_normal_(層.weight) のように呼ぶと、その層の weight を指定の方法のランダム値で「上書き」します。

init.py(PyTorch・読むだけ)
import torch
import torch.nn as nn
torch.manual_seed(0)

fc = nn.Linear(256, 256)              # 入力256・出力256の層で試す
print("デフォルト std:", round(fc.weight.std().item(), 4))

# He初期化(ReLU向け):weight をその場で上書き
nn.init.kaiming_normal_(fc.weight, nonlinearity="relu")
print("He後 std     :", round(fc.weight.std().item(), 4))

# Xavier初期化(tanh/シグモイド向け):weight をその場で上書き
nn.init.xavier_normal_(fc.weight)
print("Xavier後 std :", round(fc.weight.std().item(), 4))

# バイアスは0で初期化するのが定番
nn.init.zeros_(fc.bias)
print("bias[:3]     :", fc.bias[:3].tolist())
▼ 出力
デフォルト std: 0.036
He後 std     : 0.0888
Xavier後 std : 0.0623
bias[:3]     : [0.0, 0.0, 0.0]

💡 std(ばらつき)が方法ごとに変わるのがポイント: std() は重みのばらつき(標準偏差)。初期化とは「この最初のばらつきを、活性化に合わせて決める」操作です。

  • Hestd ≈ √(2 / fan_in)。入力数256なら √(2/256) ≈ 0.0884(出力 0.0888 とほぼ一致)。
  • Xavierstd ≈ √(2 / (fan_in + fan_out))√(2/512) ≈ 0.0625(出力 0.0623)。
  • He は Xavier の約1.41倍(√2倍)大きい。ReLU は入力の約半分を0にするので、He は重みを少し大きめにして信号の大きさを保ちます。

※ ここでは実測と理論がほぼ一致するよう大きめの層(256×256)で測っています(重みが少ないと実測値はばらつきます)。

📘 He / Xavier の数式の導出や「なぜその分散にするのか」という理論を復習したいときは、E資格対策ページ 「深層モデルのための最適化 ― パラメータの初期化戦略」 をどうぞ。

理論の確認: weight_decayL2正則化そのもの(重みを小さく保ち過学習を抑える)。 初期化は活性化に合わせて選び、ReLU系には He、tanh/シグモイド系には Xavierが定番。 Adam は学習率を自動調整するため、まず試す最適化器として人気です。

4. 正則化:dropout と BatchNorm(train/eval で挙動が変わる)

過学習を抑える代表が dropout(学習中だけランダムにニューロンを0にする)と BatchNorm(各層の出力を正規化)。 重要なのは、これらは model.train()model.eval() で挙動が変わること。 dropout で確かめます。

dropout_mode.py(PyTorch・読むだけ)
import torch
import torch.nn as nn
torch.manual_seed(0)

drop = nn.Dropout(p=0.5)   # 50%を0にする
x = torch.ones(2, 6)

drop.train()                # 学習モード → ランダムに0、生き残りは1/(1-p)=2倍
print("train:", drop(x))

drop.eval()                 # 評価モード → 何もしない(そのまま通す)
print("eval :", drop(x))
▼ 出力
train: tensor([[2., 0., 0., 2., 2., 0.],
        [0., 2., 2., 2., 2., 0.]])
eval : tensor([[1., 1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1., 1.]])

⚠ train()/eval() の切り替え忘れ(つまずきやすい点):

  • 学習前は model.train()検証・推論前は model.eval() を呼ぶ。
  • eval() を忘れると、評価中も dropout が効いたり BatchNorm がバッチ統計を使ったりして、結果がブレる
  • 学習中の dropout は、生き残ったニューロンを 1/(1-p) 倍にスケール(上の出力で「2倍」になっているのがそれ)。

では実際の学習ループのどこで train()/eval() を呼ぶのか——骨格はこうです(中身は ... で省略)。

train_eval_skeleton.py(PyTorch・読むだけ)
for epoch in range(epochs):
    model.train()                      # 学習モード(dropout/BatchNorm が有効)
    for xb, yb in train_loader:
        ...                            # zero_grad → 順伝播 → 損失 → backward → step

    model.eval()                       # 評価モード(dropout 無効・BNは蓄積統計)
    with torch.no_grad():             # 検証は勾配不要
        for xb, yb in val_loader:
            ...                        # 予測して正解率や損失を集計

学習・検証をエポックごとに行き来しながら、その都度モードを切り替えるのがポイントです。 この「学習ループへの組み込み」と評価の実コードは、レッスン8「CNNの学習・評価・正則化」で詳しく扱います。

5. DataLoader でミニバッチ学習

実際の学習では、全データを一度に流すのではなく小分け(ミニバッチ)にして回します。 TensorDataset でデータを束ね、DataLoaderバッチサイズシャッフルを指定します。

dataloader.py(PyTorch・読むだけ)
import torch
from torch.utils.data import TensorDataset, DataLoader
torch.manual_seed(0)

X = torch.randn(200, 4)
y = (X.sum(dim=1) > 0).long()

dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=64, shuffle=True)

for xb, yb in loader:        # 1バッチずつ取り出す
    print("バッチ x:", xb.shape, " y:", yb.shape)
    break
print("バッチ数:", len(loader))   # 200 / 64 → 4バッチ(64,64,64,8)
▼ 出力
バッチ x: torch.Size([64, 4])  y: torch.Size([64])
バッチ数: 4

💡 DataLoader を使う3ステップ:

  • TensorDataset(X, y) で束ねる:入力と正解をペアで対応づけ、「(x, y) の組」として取り出せるようにする(シャッフルしても対応がズレない)。
  • DataLoader(dataset, ...) でバッチ化:データセットをバッチ単位で小分けにし、繰り返し取り出せるようにする。
  • for xb, yb in loader: で回す:1回のループで (xb, yb) が1バッチぶん渡ってくる。

💡 DataLoader の主な引数:

  • batch_size … 1バッチのサンプル数(例:64)。大きいほど学習は安定するがメモリを使う。
  • shuffle=True毎エポックでデータの順序をシャッフルする。順番の丸暗記を防ぎ学習が安定する(学習用は True、検証用は False が基本)。
  • drop_last … 割り切れない最後の端数バッチを捨てるか。Trueで捨て、デフォルトは False=端数も1バッチとして残す。
  • num_workers … データ読み込みを並列化するプロセス数(大きなデータで高速化)。

この loader2.の学習ループに組み込むと、「外側=エポック/内側=ミニバッチ」の二重ループになります。これが実戦の基本形です。

train_with_loader.py(PyTorch・読むだけ)
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
torch.manual_seed(0)

X = torch.randn(200, 4)
y = (X.sum(dim=1) > 0).long()
loader = DataLoader(TensorDataset(X, y), batch_size=64, shuffle=True)

class Net(nn.Module):           # 2. と同じ2層ネット
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(4, 16)
        self.fc2 = nn.Linear(16, 2)
    def forward(self, x):
        return self.fc2(torch.relu(self.fc1(x)))

model = Net()
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

for epoch in range(3):              # 外側=エポック(全データを何周するか)
    model.train()
    running = 0.0
    for xb, yb in loader:           # 内側=ミニバッチ
        optimizer.zero_grad()
        loss = loss_fn(model(xb), yb)
        loss.backward()
        optimizer.step()
        running += loss.item()
    print(f"epoch {epoch} avg_loss {running/len(loader):.4f}")
▼ 出力
epoch 0 avg_loss 0.6550
epoch 1 avg_loss 0.6016
epoch 2 avg_loss 0.5509

💡 ポイント: len(loader)1エポックあたりのバッチの個数です。これは イテレーション数ともパラメータ更新の回数とも呼ばれ、3つとも同じ値を指します(1バッチ=1イテレーション=1更新)。 内側ループの1回ごとに zero_grad → backward → step が走るので、1エポックで len(loader) 回だけ重みが更新されます。 §2のように全データを一度に流す(バッチ=全体)のは特別な場合で、通常はこのミニバッチの二重ループが基本形です。

6. 練習問題

問題 1

学習ループの正しい順序は?

1ステップの処理として正しい順序を選んでください。

  • A. zero_grad → forward → loss → backward → step
  • B. backward → zero_grad → step
  • C. forward → step → backward → zero_grad
答えと解説を見る

正解:A

勾配をリセット → 予測 → 損失 → 勾配計算 → 更新。zero_grad を忘れると勾配が累積し、 backward 前に step しても更新する勾配がありません。

問題 2

weight_decay は何の正則化?

optim.Adam(..., weight_decay=1e-4)weight_decay に相当する正則化はどれですか。

  • A. dropout
  • B. L1正則化
  • C. L2正則化
答えと解説を見る

正解:C. L2正則化

weight_decay は重みの大きさにペナルティをかけて小さく保つ=L2正則化。 過学習を抑える効果があります。

問題 3

評価の前に呼ぶべきは?

dropout や BatchNorm を含むモデルで、検証・推論の前に呼ぶべきメソッドはどれですか。

  • A. model.train()
  • B. model.eval()
  • C. model.zero_grad()
答えと解説を見る

正解:B. model.eval()

eval() で dropout は無効化され、BatchNorm は学習中に蓄えた統計を使います。 これを忘れると評価結果がブレます。学習に戻るときは model.train() を呼びます。

問題 4

1エポックでの更新回数は?

サンプル数100 を DataLoader(batch_size=32)drop_last は指定しない)で1エポック学習します。パラメータは何回更新されますか(=1エポックのイテレーション数)。

  • A. 4
  • B. 3
  • C. 32
答えと解説を見る

正解:A. 4

1バッチ=1更新です。100 / 32 = 3.125 で、drop_last 未指定なら端数も1バッチになる(32, 32, 32, 4)ので4バッチ=4回更新。 もし drop_last=True なら端数を捨てて3回です。なお batch_size=32 は「1バッチのサンプル数」、エポックは「全データを1周すること」で、別の概念です。

7. まとめ

このレッスンのポイント

  • 学習1ステップは zero_grad → 順伝播 → 損失 → backward → step
  • zero_grad を忘れると勾配が累積する(PyTorchは自動リセットしない)
  • 最適化器は SGD(モメンタム)Adamweight_decayL2正則化
  • 初期化は ReLU系→He、tanh/シグモイド系→Xavier
  • dropout・BatchNorm は train()/eval() で挙動が変わる。評価前は eval()
  • DataLoader でミニバッチ学習。外側=エポック、内側=バッチの二重ループ

腕試し:出力を予想してみよう

このレッスンの仕上げです。次のコードを頭の中で実行して、出力を予想してみましょう (PyTorchはブラウザでは動かせないので、メモに書き出してから答えで確認するのがおすすめ)。

challenge.py(PyTorch・予想してみよう)
import torch
from torch.utils.data import TensorDataset, DataLoader
torch.manual_seed(0)

X = torch.randn(150, 4)
y = torch.randint(0, 2, (150,))
loader = DataLoader(TensorDataset(X, y), batch_size=40, shuffle=True)

print(len(loader))                 # ① バッチ(イテレーション)数は?
print(list(loader)[-1][0].shape)   # ② 最後のバッチの x の形は?
答えと解説を見る
4
torch.Size([30, 4])

150 / 40 = 3.75drop_last は未指定(=端数を残す)なので 40, 40, 40, 304バッチ
② 最後のバッチは余った 30サンプルなので x の形は torch.Size([30, 4])shuffle=True は順序を入れ替えるだけで、最後のバッチのサイズは変わりません。

ここまでで「学習を回す」骨格がそろいました。次の Unit 3 では、画像で強力な CNN(畳み込みニューラルネット)の実装を読み解きます。

完了するとコース一覧に進捗が記録されます