1. 学習ループの全体像
モデルを「賢く」するには、データを何度も見せてパラメータを少しずつ更新します。その1ステップは必ず次の順序です。
optimizer.zero_grad()… 前のステップの勾配を消すout = model(x)… 順伝播(予測)loss = loss_fn(out, y)… 損失を計算loss.backward()… 逆伝播(勾配を計算)optimizer.step()… パラメータを更新
この順序は実装で間違えやすい要点です。なぜこの順番なのかも含めて、実際のコードで確認します。
2. 学習ループのコード
合成データ(4特徴の2クラス分類)で5エポック学習します。損失が下がっていくのがポイントです。
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(学習率)などを指定するだけ。代表は SGD(momentum を付けるのが定番)と Adam です。
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
💡 どちらを選ぶ?
迷ったら Adam(lr=1e-3 あたり)から始めるのが無難。学習率を自動調整するので安定しやすいです。
よく調整した SGD + momentum は Adam より良い汎化を出すこともあり、画像分類などで定番。
weight_decay はどちらの optimizer にも引数で渡せるL2正則化で、過学習を抑えたいときに足します。
② 初期化(initialization)をどう書くか
学習を始める前の重みの初期値を、活性化に合わせて設定します。PyTorch には初期化関数を集めた nn.init があり、
nn.init.kaiming_normal_(層.weight) のように呼ぶと、その層の weight を指定の方法のランダム値で「上書き」します。
- 関数名の末尾の
_(アンダースコア)は PyTorch の慣習で「その場で書き換える(in-place)」の意味。fc.weight = ...と代入し直す必要はなく、呼ぶだけでfc.weightの中身が変わります。 - 代表的な方法:He(kaiming)=ReLU系向け →
kaiming_normal_、Xavier(glorot)=tanh・シグモイド系向け →xavier_normal_。 - バイアスは 0 で初期化するのが定番 →
nn.init.zeros_(層.bias)。
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() は重みのばらつき(標準偏差)。初期化とは「この最初のばらつきを、活性化に合わせて決める」操作です。
- He:
std ≈ √(2 / fan_in)。入力数256なら√(2/256) ≈ 0.0884(出力 0.0888 とほぼ一致)。 - Xavier:
std ≈ √(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_decay はL2正則化そのもの(重みを小さく保ち過学習を抑える)。
初期化は活性化に合わせて選び、ReLU系には He、tanh/シグモイド系には Xavierが定番。
Adam は学習率を自動調整するため、まず試す最適化器として人気です。
4. 正則化:dropout と BatchNorm(train/eval で挙動が変わる)
過学習を抑える代表が dropout(学習中だけランダムにニューロンを0にする)と BatchNorm(各層の出力を正規化)。
重要なのは、これらは model.train() と model.eval() で挙動が変わること。
dropout で確かめます。
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() を呼ぶのか——骨格はこうです(中身は ... で省略)。
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 で バッチサイズとシャッフルを指定します。
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… データ読み込みを並列化するプロセス数(大きなデータで高速化)。
この loader を2.の学習ループに組み込むと、「外側=エポック/内側=ミニバッチ」の二重ループになります。これが実戦の基本形です。
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ステップの処理として正しい順序を選んでください。
- A. zero_grad → forward → loss → backward → step
- B. backward → zero_grad → step
- C. forward → step → backward → zero_grad
答えと解説を見る
正解:A
勾配をリセット → 予測 → 損失 → 勾配計算 → 更新。zero_grad を忘れると勾配が累積し、
backward 前に step しても更新する勾配がありません。
weight_decay は何の正則化?
optim.Adam(..., weight_decay=1e-4) の weight_decay に相当する正則化はどれですか。
- A. dropout
- B. L1正則化
- C. L2正則化
答えと解説を見る
正解:C. L2正則化
weight_decay は重みの大きさにペナルティをかけて小さく保つ=L2正則化。
過学習を抑える効果があります。
評価の前に呼ぶべきは?
dropout や BatchNorm を含むモデルで、検証・推論の前に呼ぶべきメソッドはどれですか。
- A. model.train()
- B. model.eval()
- C. model.zero_grad()
答えと解説を見る
正解:B. model.eval()
eval() で dropout は無効化され、BatchNorm は学習中に蓄えた統計を使います。
これを忘れると評価結果がブレます。学習に戻るときは model.train() を呼びます。
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(モメンタム)・Adam。
weight_decayは L2正則化 - 初期化は ReLU系→He、tanh/シグモイド系→Xavier
- dropout・BatchNorm は
train()/eval()で挙動が変わる。評価前は eval() - DataLoader でミニバッチ学習。外側=エポック、内側=バッチの二重ループ
腕試し:出力を予想してみよう
このレッスンの仕上げです。次のコードを頭の中で実行して、出力を予想してみましょう (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.75。drop_last は未指定(=端数を残す)なので 40, 40, 40, 30 の4バッチ。
② 最後のバッチは余った 30サンプルなので x の形は torch.Size([30, 4])。
shuffle=True は順序を入れ替えるだけで、最後のバッチのサイズは変わりません。
ここまでで「学習を回す」骨格がそろいました。次の Unit 3 では、画像で強力な CNN(畳み込みニューラルネット)の実装を読み解きます。
完了するとコース一覧に進捗が記録されます