1. 学習と評価の作法
CNN の学習ループ自体は L06 と同じ(zero_grad → 順伝播 → 損失 → backward → step)。
違いは評価フェーズです。評価では model.eval() に切り替え、with torch.no_grad(): で囲むのが定石。
dropout や BatchNorm の挙動を評価モードにし、勾配計算を止めて速く・正確に測ります。
2. 正解率(accuracy)の計算
分類の評価でよく出るのが正解率。モデルの出力(ロジット)から argmax で予測クラスを取り、正解と比べて一致率を出します。
import torch
# 3サンプル・3クラスのロジット(モデル出力の例)
logits = torch.tensor([[2., 1., 0.],
[0., 3., 1.],
[1., 0., 2.]])
y = torch.tensor([0, 1, 1]) # 正解クラス
pred = logits.argmax(dim=1) # 各行で最大のクラス → [0, 1, 2]
acc = (pred == y).float().mean() # 一致率
print("pred:", pred.tolist())
print("acc :", round(acc.item(), 4)) # 3つ中2つ正解 → 0.6667
pred: [0, 1, 2]
acc : 0.6667
実際の評価では、これをバッチごとに繰り返して全体で集計します。
model.eval() と torch.no_grad() を付けた評価関数にまとめておくと、学習ループから何度でも呼べて便利です。
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
torch.manual_seed(0)
# 評価用データ(120枚・2クラス)と未学習モデル
X = torch.randn(120, 1, 8, 8)
y = (X.mean(dim=(1, 2, 3)) > 0).long()
loader = DataLoader(TensorDataset(X, y), batch_size=32)
model = nn.Sequential(nn.Flatten(), nn.Linear(64, 2))
def evaluate(model, loader):
model.eval() # ① 評価モード(dropout/BNを評価用に)
correct, total = 0, 0
with torch.no_grad(): # ② 勾配を計算しない(速い・省メモリ)
for xb, yb in loader:
pred = model(xb).argmax(dim=1) # ③ バッチごとの予測クラス
correct += (pred == yb).sum().item() # ④ 正解数を加算
total += yb.size(0)
return correct / total # ⑤ 最後に全体で割る
print("acc:", round(evaluate(model, loader), 4))
acc: 0.4333
💡 評価ループの定石(上の evaluate() がこれ):
① model.eval() → ② with torch.no_grad(): の中で、③ argmax で予測 → ④ 正解数を加算 → ⑤ 最後に全体で割る。
argmax(dim=1) の dim=1 は「クラス方向で最大」の意味です(softmax をかけても順位は変わらないので、ロジットのまま argmax してOK)。
上の出力 0.4333 は未学習モデルなので偶然レベル。学習が進むと、この同じ関数で測る精度が上がっていきます(§6で確認)。
3. 正則化を組み込んだ畳み込みブロック
過学習を抑える手段は L06 で見た通り(dropout・BatchNorm・weight decay・early stopping)。
CNN では BatchNorm2d を畳み込みの後に挟むのが定番です。典型的なブロックは
Conv2d → BatchNorm2d → ReLU → MaxPool2d の並びです。
import torch
import torch.nn as nn
torch.manual_seed(0)
block = nn.Sequential(
nn.Conv2d(1, 16, 3, padding=1), # 28 -> 28, 16ch
nn.BatchNorm2d(16), # チャネルごとに正規化(学習を安定化)
nn.ReLU(),
nn.MaxPool2d(2), # 28 -> 14
)
x = torch.randn(8, 1, 28, 28)
print("block出力:", block(x).shape) # (8, 16, 14, 14)
block出力: torch.Size([8, 16, 14, 14])
BatchNorm が train() と eval() で挙動を変える正体は、内部に持つ running統計(runningの平均 running_mean・分散 running_var)です。処理手順はこう分かれます。
- 学習時(
train()):① その入力バッチの平均・分散で正規化する。② それとは別に、running統計を移動平均で少しずつ更新していく(後の評価で使うために貯めていくイメージ)。 - 評価時(
eval()):バッチの統計は使わず、学習中に貯めた running統計で正規化する。だから評価バッチの中身に左右されない。
下のコードで、train() のまま入力を1回通すと、running統計が 0 から動くことを確認します。
import torch
import torch.nn as nn
torch.manual_seed(0)
bn = nn.BatchNorm2d(3) # 3チャネル分のrunning統計を持つ
print("初期 running_mean:", bn.running_mean.tolist()) # 最初は全部0
bn.train() # ① 学習モードにする
x = torch.randn(16, 3, 8, 8) * 2 + 5 # 平均5・ばらつき2あたりの入力
_ = bn(x) # ② xを通す。出力は使わないので _ に捨てる
# ねらいは「running統計を更新させる」副作用のほう
print("更新後 running_mean:", [round(v, 3) for v in bn.running_mean.tolist()])
print("更新後 running_var :", [round(v, 3) for v in bn.running_var.tolist()])
bn.eval() # ③ 評価モード。これ以降は上のrunning統計で正規化(バッチ統計は使わない)
初期 running_mean: [0.0, 0.0, 0.0]
更新後 running_mean: [0.504, 0.493, 0.501]
更新後 running_var : [1.324, 1.293, 1.289]
💡 なぜ「平均5の入力」なのに running_mean は約0.5? running統計は一気に置き換えず、移動平均で目標に少しずつ近づけます。更新式はこうです:
新しい running_mean = (1 − momentum) × 旧 + momentum × バッチ平均
既定の momentum=0.1 なので、1バッチ通しただけだと 0.9 × 0 + 0.1 × 5 = 0.5。
つまり「目標(このバッチの平均=約5)に10%だけ近づいた」状態で 0 → 約0.5 です。これを何バッチも繰り返すうちに、だんだん5へ近づいていきます(running_var も同様)。
💡 だから評価の前に eval() を呼ぶ:
eval() を忘れると、BatchNorm はその評価バッチの統計で正規化してしまい、同じ画像でも一緒に入れた他の画像しだいで結果が変わります。
eval() にすれば学習で貯めた running統計を使うので、いつ・どんなバッチで評価しても安定します。
BatchNorm2d(C) の引数はチャネル数。各チャネルについて、ミニバッチ内で平均0・分散1になるよう正規化します。
学習を安定させ、より大きな学習率を使えるようにする効果があります。評価時は eval() で学習中に蓄えた統計を使う点に注意(L06)。
4. 転移学習(凍結と付け替え)
ゼロから学習する代わりに、大規模データ(ImageNet等)で学習済みのモデルを再利用するのが転移学習です。 学習済みの畳み込み部分は「エッジ・形・模様」といった汎用的な特徴をすでに獲得しているので、手元のデータが少なくても高精度を狙えます。
💡 2つの戦略:
- ① 特徴抽出(feature extraction):畳み込み部分を凍結し、最後の分類層だけ付け替えて学習。データが少ないとき向き。
- ② ファインチューニング(fine-tuning):分類層を付け替えたうえで、一部〜全体も小さい学習率で再学習。データがそこそこあるとき向き。
凍結は requires_grad = False、付け替えはその層を新しい層で上書きするだけです。
実務では torchvision の学習済みモデルを使うのが定番です。型はこうです(重みのダウンロードが必要なので、ここでは構造だけ確認)。
import torch
import torch.nn as nn
from torchvision import models
net = models.resnet18(weights="IMAGENET1K_V1") # 学習済みResNet18をロード
# ① backbone を凍結
for p in net.parameters():
p.requires_grad = False
# ② 最終の全結合(fc)を自分のクラス数(=3)に付け替え(新しい層はrequires_grad=True)
net.fc = nn.Linear(net.fc.in_features, 3)
# ③ optimizer には「学習する層」だけ渡す
opt = torch.optim.Adam(net.fc.parameters(), lr=1e-3)
仕組みを小さなモデルで実際に確認します(凍結 → 付け替え → 学習対象のパラメータ数を数える)。
import torch
import torch.nn as nn
torch.manual_seed(0)
# 学習済みモデルに見立てた「特徴抽出器+分類層」
model = nn.Sequential(
nn.Conv2d(3, 8, 3, padding=1), nn.ReLU(), nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(8, 1000), # 元の分類層(1000クラス)
)
# ① すべて凍結
for p in model.parameters():
p.requires_grad = False
# ② 最後の層を3クラス用に付け替え(新しい層は学習対象)
model[-1] = nn.Linear(8, 3)
total = sum(p.numel() for p in model.parameters())
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
print("総パラメータ :", total) # 251
print("学習するパラメータ:", trainable) # 27(付け替えた層だけ)
総パラメータ : 251
学習するパラメータ: 27
💡 効く理由と実装のコツ:
- 少データ・短時間で高精度:汎用特徴は再利用し、最後の層だけ学習すればよい。新しく作った層は
requires_grad=Trueが既定なので自動で学習対象。 - optimizer には学習する層だけ渡すのが安全(凍結分は勾配が無いので渡しても更新されませんが、明示すると意図が明確)。
- 凍結した BatchNorm は
eval()に:backbone を凍結するなら統計も更新しないよう評価モードにするのが定石。 - fine-tuning するなら学習率を小さめに(学習済みの重みを壊さないため)。まず分類層だけ学習 → その後 backbone も小さい lr で、という二段構えも一般的。
5. ResNet と skip connection(残差)
層を深くするほど表現力は上がりますが、勾配が消えて(小さくなって)学習しにくくなる問題が起きます。
ResNet はこれを skip connection(残差接続)で解決しました。
ブロックの出力に入力をそのまま足す(out + x)ことで、勾配が「近道」を通って流れやすくなります。
import torch
import torch.nn as nn
torch.manual_seed(0)
class Residual(nn.Module):
def __init__(self, ch):
super().__init__()
self.conv1 = nn.Conv2d(ch, ch, 3, padding=1)
self.bn1 = nn.BatchNorm2d(ch)
self.conv2 = nn.Conv2d(ch, ch, 3, padding=1)
self.bn2 = nn.BatchNorm2d(ch)
def forward(self, x):
out = torch.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
return torch.relu(out + x) # ← 入力xを足す(skip connection)
x = torch.randn(4, 16, 8, 8)
print("残差ブロック出力:", Residual(16)(x).shape) # 形は変わらない
残差ブロック出力: torch.Size([4, 16, 8, 8])
⚠ skip connection の条件:
out + x ができるのは out と x の形が同じのときだけ。だから残差ブロックは
入力と同じチャネル数・同じ H×W を保つように作ります(padding=1, K=3 でサイズ維持)。
形が変わる場合は x 側にも 1×1 畳み込みを入れて形を合わせます。
実際の ResNet には、途中でチャネルを増やし・サイズを半分にするブロックがあります。そのときは x 側に 1×1 畳み込み(downsample)を通して形をそろえてから足します。
import torch
import torch.nn as nn
torch.manual_seed(0)
class ResidualDown(nn.Module):
def __init__(self, in_ch, out_ch, stride=2):
super().__init__()
self.conv1 = nn.Conv2d(in_ch, out_ch, 3, stride=stride, padding=1)
self.bn1 = nn.BatchNorm2d(out_ch)
self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1)
self.bn2 = nn.BatchNorm2d(out_ch)
# x 側を out と同じ形にそろえる 1×1 畳み込み
self.down = nn.Conv2d(in_ch, out_ch, 1, stride=stride)
def forward(self, x):
out = torch.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
return torch.relu(out + self.down(x)) # 形をそろえてから足す
x = torch.randn(4, 16, 8, 8)
print("出力:", ResidualDown(16, 32, stride=2)(x).shape) # ch16→32, 8→4
出力: torch.Size([4, 32, 4, 4])
6. 総合演習:CNN学習パイプライン全体
このレッスンの仕上げです。これまでの部品——Conv–BatchNorm–ReLU・残差ブロック・Global Average Pooling・学習ループ・評価関数(eval/no_grad)——を1つにまとめた、 最小だが本格的な学習パイプラインを読み解きます。各部分がどの回の内容かを意識しながら追ってみましょう。
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
torch.manual_seed(0)
# --- データ:合成画像(1ch・16×16)を2クラスに ---
X = torch.randn(256, 1, 16, 16)
y = (X.mean(dim=(1, 2, 3)) > 0).long()
train_loader = DataLoader(TensorDataset(X[:200], y[:200]), batch_size=32, shuffle=True)
test_loader = DataLoader(TensorDataset(X[200:], y[200:]), batch_size=32)
# --- 残差ブロック(このレッスンの5.)---
class Residual(nn.Module):
def __init__(self, ch):
super().__init__()
self.conv1 = nn.Conv2d(ch, ch, 3, padding=1); self.bn1 = nn.BatchNorm2d(ch)
self.conv2 = nn.Conv2d(ch, ch, 3, padding=1); self.bn2 = nn.BatchNorm2d(ch)
def forward(self, x):
out = torch.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
return torch.relu(out + x) # skip connection
# --- モデル:Conv-BN-ReLU → 残差 → GAP → 全結合 ---
model = nn.Sequential(
nn.Conv2d(1, 8, 3, padding=1), nn.BatchNorm2d(8), nn.ReLU(),
Residual(8),
nn.AdaptiveAvgPool2d(1), nn.Flatten(), # GAP → (N, 8)
nn.Linear(8, 2),
)
loss_fn = nn.CrossEntropyLoss()
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
# --- 評価関数(このレッスンの2.)---
def evaluate(model, loader):
model.eval()
correct, total = 0, 0
with torch.no_grad():
for xb, yb in loader:
correct += (model(xb).argmax(1) == yb).sum().item()
total += yb.size(0)
return correct / total
# --- 学習ループ(L6):train()で学習 → evaluate()で測る ---
for epoch in range(3):
model.train()
for xb, yb in train_loader:
opt.zero_grad()
loss = loss_fn(model(xb), yb)
loss.backward()
opt.step()
print(f"epoch {epoch} test_acc {evaluate(model, test_loader):.3f}")
epoch 0 test_acc 0.554
epoch 1 test_acc 0.554
epoch 2 test_acc 0.661
💡 ここを変えて試してみよう(まず出力を予想 → 手元で確かめる):
- エポック数を
3 → 10に増やすとtest_accはどう動く? Residual(8)を外すと(残差なし)学習の進みはどう変わる?nn.BatchNorm2d(8)を抜くと収束は速い?遅い?- 評価で
model.eval()を呼ばないと、結果はどうブレる?
※ PyTorchはこのページでは動かせないので、まず頭の中で予想してから、手元の環境で実際に動かして確かめるのがおすすめです(合成データなので精度は参考値)。
7. 練習問題
正解率はいくつ?
予測 pred = [1, 0, 1, 1]、正解 y = [1, 1, 1, 0] のとき、正解率はいくつですか。
- A. 0.25
- B. 0.50
- C. 0.75
答えと解説を見る
正解:B. 0.50
一致しているのは1番目(1=1)と3番目(1=1)の2つ。4つ中2つで 2/4 = 0.5。
層を凍結するには?
転移学習で、ある層を「学習しない(凍結)」にするにはどうしますか。
- A. その層のパラメータの
requires_gradをFalseにする - B. その層を
eval()にする - C. その層を削除する
答えと解説を見る
正解:A
p.requires_grad = False にすると勾配が計算されず、step() で更新されません=凍結。
eval() は dropout/BatchNorm のモード切り替えで、凍結とは別物です。
skip connection の役割は?
ResNet の skip connection(out + x)の主な目的はどれですか。
- A. 画像のサイズを小さくする
- B. クラス数を増やす
- C. 深いネットワークでも勾配が流れやすくし、学習を可能にする
答えと解説を見る
正解:C
入力を足すことで勾配が「近道」を通れるようになり、勾配消失を緩和。これにより非常に深いネットワークでも学習できます。
BatchNorm2d の引数は?
nn.BatchNorm2d(?) に渡すのは何の数ですか。
- A. バッチサイズ
- B. チャネル数
- C. 画像の高さ
答えと解説を見る
正解:B. チャネル数
BatchNorm2d(C) はチャネルごとに正規化するので、引数はチャネル数。直前の畳み込みの out_ch に合わせます。
畳み込みブロックの出力 shape は?
次のブロックに (4, 3, 32, 32) を通すと、出力 shape はどれですか。
block = nn.Sequential(
nn.Conv2d(3, 32, 3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2),
)
out = block(torch.randn(4, 3, 32, 32))
- A. (4, 32, 16, 16)
- B. (4, 32, 32, 32)
- C. (4, 3, 16, 16)
答えと解説を見る
正解:A. (4, 32, 16, 16)
チャネルは 3→32。Conv2d(pad=1, K=3) は 32×32 を保ち、BatchNorm2d・ReLU は形を変えず、MaxPool2d(2) が 32→16 にします。
学習するパラメータ数は?
次の転移学習コードのあと、trainable(学習対象のパラメータ数)はいくつですか。
model = nn.Sequential(
nn.Linear(10, 20), nn.ReLU(), nn.Linear(20, 5)
)
for p in model.parameters():
p.requires_grad = False
model[2] = nn.Linear(20, 4) # 付け替え(新しい層は学習対象)
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
- A. 304
- B. 220
- C. 84
答えと解説を見る
正解:C. 84
凍結後に付け替えた Linear(20, 4) だけが学習対象。20×4 + 4 = 84。
元の層は requires_grad=False のままなので数えません(総数は 304)。
8. まとめ
このレッスンのポイント
- 評価は
model.eval()+with torch.no_grad():の評価ループ。バッチごとにargmax→ 正解数を加算 → 全体で割る BatchNorm2d(C)の引数はチャネル数。train()はバッチ統計+running更新/eval()はrunning統計を使う(だから評価前にeval())- CNN の正則化は BatchNorm・dropout・weight decay・early stopping
- 転移学習=「特徴抽出部を凍結(
requires_grad=False)+最後の層を付け替え」。少データは特徴抽出、データがあれば fine-tuning(小さいlr) - ResNet の skip connection(
out + x)は勾配消失を緩和。形が変わる所は 1×1 畳み込み(downsample)でxをそろえる - これらを統合した学習パイプライン=データ → モデル(Conv-BN-残差-GAP) →
train()で学習 →eval()で評価
次は、画像以外の系列データ(RNN/LSTM)と、いま主流の Transformer・Self-Attention のコードを読み解きます。
完了するとコース一覧に進捗が記録されます