📋 このステップで学ぶこと
- 深いネットワークの課題(勾配消失問題、劣化問題)
- 残差学習(Residual Learning)の理論
- Skip Connection(ショートカット接続)の仕組み
- ResNetのアーキテクチャ(ResNet-18、34、50、101、152)
- Basic BlockとBottleneck Blockの違い
- PyTorchでのResNet実装
- 事前学習モデルを使った転移学習
🎯 1. 深いネットワークの課題
ResNetを理解するために、まず「なぜResNetが必要になったのか」という背景を知る必要があります。
1-1. 深層学習の素朴な疑問
深層学習では、層を増やすほど複雑なパターンを学習できるはずです。では、層を100層、200層と増やせば、精度はどんどん上がるのでしょうか?
🤔 直感と現実のギャップ
直感:層が多い = 表現力が高い = 精度が上がる
現実:ある程度深くなると、精度が下がる!
【実際の実験結果(ImageNet)】
VGG-16(16層): エラー率 7.3%
VGG-19(19層): エラー率 7.1% ← ここまでは改善
もっと深くしたら?
Plain-20(20層): エラー率 8.0%
Plain-56(56層): エラー率 9.5% ← 深くしたのに悪化!
なぜ?
1-2. 勾配消失問題(Vanishing Gradient Problem)
深いネットワークで最初に問題になるのが勾配消失問題です。
❌ 勾配消失問題とは?
誤差逆伝播(バックプロパゲーション)で、勾配が層を遡るたびに小さくなっていく現象です。
【勾配消失のメカニズム】
ニューラルネットワークの学習では、出力層から入力層に向かって
勾配(誤差の情報)を逆伝播させます。
例:100層のネットワーク
出力層(100層目): 勾配 = 1.0
↓ 逆伝播(各層で少しずつ小さくなる)
90層目: 勾配 = 0.5
80層目: 勾配 = 0.25
70層目: 勾配 = 0.125
60層目: 勾配 = 0.0625
…
10層目: 勾配 = 0.00001
1層目: 勾配 ≈ 0 ← ほぼゼロ!
→ 初期層のパラメータがほとんど更新されない
→ ネットワーク全体が学習できない
なぜ勾配が小さくなるのか?
【数学的な説明】
連鎖律(Chain Rule)により、勾配は各層の勾配の積になる:
∂L/∂W₁ = ∂L/∂y × ∂y/∂h₁₀₀ × ∂h₁₀₀/∂h₉₉ × … × ∂h₂/∂h₁ × ∂h₁/∂W₁
もし各層の勾配が 0.9 だとすると:
0.9¹⁰⁰ = 0.0000265… ← ほぼゼロ
逆に 1.1 だとすると:
1.1¹⁰⁰ = 13,780… ← 爆発(勾配爆発)
→ どちらも学習が不安定になる
1-3. 劣化問題(Degradation Problem)
勾配消失問題はBatch NormalizationやReLUである程度解決できます。しかし、それでも劣化問題が残ります。
💡 劣化問題とは?
深いネットワークが、浅いネットワークよりも訓練誤差すら高くなる現象です。これは過学習とは異なります。
【劣化問題の核心】
20層のネットワーク: 訓練誤差 10%
56層のネットワーク: 訓練誤差 20% ← 訓練データでも悪い!
これは過学習ではない。
過学習なら「訓練誤差は低いがテスト誤差が高い」はず。
理論的には、56層ネットワークは20層ネットワークを含んでいる。
(余分な36層を「何もしない層 = 恒等写像」にすればいい)
しかし実際には、最適化が難しくて
「何もしない」ことすら学習できない。
→ ResNetがこの問題を解決!
🔗 2. 残差学習(Residual Learning)の革命
2015年、Microsoftの研究者たちがResNet(Residual Network)を発表しました。これはコンピュータビジョンの歴史を変える革命的なアーキテクチャでした。
2-1. ResNetのアイデア
ResNetの核心は非常にシンプルです:「学習する目標を変える」というものです。
💡 ResNetの発想の転換
従来:目的の関数 H(x) を直接学習する
ResNet:残差 F(x) = H(x) – x を学習する
【従来のネットワーク】
入力 x → [層1] → [層2] → 出力 H(x)
目標:H(x) を直接学習する
【ResNet(残差学習)】
入力 x → [層1] → [層2] → F(x)
│ │
│ ↓
└─────────────────────→ (+) → 出力 H(x) = F(x) + x
ショートカット
目標:F(x) = H(x) – x(残差)を学習する
2-2. なぜ残差を学習する方が簡単なのか?
一見すると、H(x)を直接学習しても、F(x) = H(x) – x を学習しても同じに思えます。しかし、実は大きな違いがあります。
【恒等写像を学習したい場合を考える】
目標:入力 x をそのまま出力したい(y = x)
【従来の方法】
H(x) = x を学習する必要がある
→ 層の重みを「ちょうど入力をそのまま出力するように」調整
→ 実際には難しい!
【ResNetの方法】
H(x) = F(x) + x なので、
H(x) = x にしたいなら F(x) = 0 を学習すればいい
→ 重みをゼロに近づけるだけ
→ 簡単!
🎯 ResNetの利点
① 層が不要なら F(x) = 0 にできる
追加した層が不要な場合、F(x) = 0 を学習すれば入力がそのまま通る。
② 層が有益なら F(x) を学習する
層が有益な特徴を抽出できるなら、その分だけ F(x) を学習する。
③ 最悪でも「何もしない」ことができる
これにより、深くしても少なくとも浅いネットワークと同等の性能は保証される。
2-3. Skip Connection(ショートカット接続)
Skip Connectionは、入力を直接出力に加算する「近道」です。これがResNetの核心技術です。
【Skip Connectionの構造図】
入力 x
│
├────────────────┐
↓ │
┌───────────┐ │
│ Conv 3×3 │ │
│ BatchNorm │ │ メイン経路
│ ReLU │ │ F(x) を計算
│ Conv 3×3 │ │
│ BatchNorm │ │
└───────────┘ │
│ F(x) │
↓ │
(+) ←─────────────┘ ショートカット(x をそのまま加算)
│
↓
ReLU
│
↓
出力 H(x) = F(x) + x
💡 Skip Connectionが勾配消失を解決する理由
勾配がショートカットを通って直接伝播するため、勾配消失が起きにくくなります。
【勾配の流れ】
従来のネットワーク:
勾配は全ての層を順番に通る必要がある
→ 層が多いと勾配が小さくなる
ResNet:
勾配はショートカットを通って直接伝播できる
∂L/∂x = ∂L/∂H × ∂H/∂x
= ∂L/∂H × (∂F/∂x + 1) ← この「+1」が重要!
↑
ショートカットからの勾配(常に1)
→ 勾配が0になりにくい
→ 深いネットワークでも学習可能
2-4. ResNetの成果
🏆 ImageNet 2015での圧勝
ResNetは2015年のILSVRC(ImageNet Large Scale Visual Recognition Challenge)で圧勝しました。
| 年 |
モデル |
層数 |
Top-5エラー率 |
| 2012 |
AlexNet |
8 |
16.4% |
| 2014 |
VGGNet |
19 |
7.3% |
| 2014 |
GoogLeNet |
22 |
6.7% |
| 2015 |
ResNet |
152 |
3.57% |
| 参考 |
人間 |
– |
約5% |
人間の認識精度を超えたことは、AI史上の大きなマイルストーンでした。
🏛️ 3. ResNetのアーキテクチャ
ResNetには複数のバリエーションがあります。主な違いは「層の数」と「ブロックの種類」です。
3-1. Basic Block(ResNet-18、34用)
Basic Blockは、ResNet-18とResNet-34で使われる基本的なブロックです。2つの3×3畳み込み層で構成されます。
【Basic Blockの構造】
入力(Cチャンネル)
│
├─────────────────┐
↓ │
┌──────────────┐ │
│ Conv 3×3, C │ │ ← 3×3畳み込み(チャンネル数維持)
│ BatchNorm │ │
│ ReLU │ │
│ Conv 3×3, C │ │ ← 3×3畳み込み
│ BatchNorm │ │
└──────────────┘ │
│ F(x) │
↓ │
(+) ←──────────────┘ ← 加算
│
↓
ReLU
│
↓
出力(Cチャンネル)
※ 2層の畳み込み = 1つのBasic Block
3-2. Bottleneck Block(ResNet-50、101、152用)
Bottleneck Blockは、ResNet-50以上で使われる効率的なブロックです。「ボトルネック(くびれ)」という名前の通り、中間でチャンネル数を減らして計算量を削減します。
【Bottleneck Blockの構造】
入力(Cチャンネル)
│
├─────────────────────┐
↓ │
┌────────────────┐ │
│ Conv 1×1, C/4 │ ←──────── 次元削減(Bottleneck)
│ BatchNorm │ │
│ ReLU │ │
│ │ │
│ Conv 3×3, C/4 │ ←──────── メインの畳み込み(計算量が小さい)
│ BatchNorm │ │
│ ReLU │ │
│ │ │
│ Conv 1×1, C │ ←──────── 次元復元
│ BatchNorm │ │
└────────────────┘ │
│ F(x) │
↓ │
(+) ←──────────────────┘
│
↓
ReLU
│
↓
出力(Cチャンネル)
※ 3層の畳み込み = 1つのBottleneck Block
なぜBottleneckが効率的なのか?
【計算量の比較:256チャンネルの場合】
■ Basic Block(2つの3×3畳み込み)
3×3×256×256 + 3×3×256×256
= 589,824 + 589,824
= 1,179,648 パラメータ
■ Bottleneck Block(1×1 → 3×3 → 1×1)
1×1×256×64 = 16,384 ← チャンネル数を1/4に削減
3×3×64×64 = 36,864 ← 少ないチャンネル数で3×3畳み込み
1×1×64×256 = 16,384 ← チャンネル数を復元
合計 = 69,632 パラメータ
比較:1,179,648 ÷ 69,632 ≒ 17
→ Bottleneckは約17分の1の計算量!
→ 深いネットワーク(50層以上)で効率的
3-3. ResNetファミリーの比較
| モデル |
層数 |
ブロック |
パラメータ数 |
用途 |
| ResNet-18 |
18 |
Basic |
11.7M |
軽量、高速推論 |
| ResNet-34 |
34 |
Basic |
21.8M |
バランス型 |
| ResNet-50 |
50 |
Bottleneck |
25.6M |
標準的な選択 |
| ResNet-101 |
101 |
Bottleneck |
44.5M |
高精度 |
| ResNet-152 |
152 |
Bottleneck |
60.2M |
最高精度 |
🎯 どのResNetを選ぶべき?
ResNet-18/34:リソースが限られている場合、リアルタイム処理が必要な場合
ResNet-50:最も一般的な選択。精度と速度のバランスが良い
ResNet-101/152:精度を最大化したい場合。コンペティションなど
💻 4. PyTorchでのResNet実装
ResNetを理解するために、まずBasic Blockを自分で実装してみましょう。
4-1. Basic Blockの実装
ステップ1:必要なインポート
# PyTorchのニューラルネットワーク用モジュール
import torch
import torch.nn as nn
# nn.Module:全てのニューラルネットワーク層の基底クラス
# nn.Conv2d:2次元畳み込み層
# nn.BatchNorm2d:2次元バッチ正規化
# nn.ReLU:ReLU活性化関数
ステップ2:Basic Blockクラスの定義
class BasicBlock(nn.Module):
“””
ResNet-18, 34用のBasic Block
構造:
入力 → Conv3×3 → BN → ReLU → Conv3×3 → BN → (+) → ReLU → 出力
↑
入力(ショートカット)
“””
# expansion:出力チャンネル数の倍率
# Basic Blockでは入力と出力のチャンネル数が同じなので1
expansion = 1
def __init__(self, in_channels, out_channels, stride=1, downsample=None):
“””
Args:
in_channels: 入力のチャンネル数
out_channels: 出力のチャンネル数
stride: 畳み込みのストライド(2にするとサイズが半分に)
downsample: ショートカットの次元を合わせるための層
“””
super(BasicBlock, self).__init__()
# ここからメイン経路を定義
ステップ3:畳み込み層の定義
# 1つ目の畳み込み層
# kernel_size=3:3×3のフィルタ
# stride:最初の畳み込みでサイズを変更する場合に使用
# padding=1:出力サイズを維持するためのパディング
# bias=False:BatchNormを使うのでバイアスは不要
self.conv1 = nn.Conv2d(
in_channels, out_channels,
kernel_size=3, stride=stride, padding=1, bias=False
)
# バッチ正規化:学習を安定させる
self.bn1 = nn.BatchNorm2d(out_channels)
# ReLU活性化関数
# inplace=True:メモリ節約のため入力を直接書き換える
self.relu = nn.ReLU(inplace=True)
# 2つ目の畳み込み層
# stride=1:2つ目は常にサイズを維持
self.conv2 = nn.Conv2d(
out_channels, out_channels,
kernel_size=3, stride=1, padding=1, bias=False
)
self.bn2 = nn.BatchNorm2d(out_channels)
# ショートカット用のダウンサンプリング層
# 次元が合わない場合に使用
self.downsample = downsample
ステップ4:順伝播(forward)の実装
def forward(self, x):
“””
順伝播の処理
Args:
x: 入力テンソル
Returns:
出力テンソル
“””
# ショートカット用に入力を保存
identity = x
# メイン経路の処理
out = self.conv1(x) # 畳み込み1
out = self.bn1(out) # バッチ正規化1
out = self.relu(out) # ReLU
out = self.conv2(out) # 畳み込み2
out = self.bn2(out) # バッチ正規化2
# ※ここではまだReLUをかけない
# ショートカットの処理
# 次元が合わない場合(チャンネル数やサイズが異なる場合)
if self.downsample is not None:
identity = self.downsample(x)
# 残差接続:メイン経路 + ショートカット
out += identity
# 最後にReLU
out = self.relu(out)
return out
完成コード(Basic Block)
※ コードが横に長い場合は横スクロールできます
import torch
import torch.nn as nn
class BasicBlock(nn.Module):
“””ResNet-18, 34用のBasic Block”””
expansion = 1
def __init__(self, in_channels, out_channels, stride=1, downsample=None):
super(BasicBlock, self).__init__()
# メイン経路
self.conv1 = nn.Conv2d(in_channels, out_channels,
kernel_size=3, stride=stride,
padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(out_channels, out_channels,
kernel_size=3, stride=1,
padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# ショートカット
self.downsample = downsample
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
# テスト
block = BasicBlock(64, 64)
x = torch.randn(1, 64, 56, 56) # バッチサイズ1, 64チャンネル, 56×56
output = block(x)
print(f”入力: {x.shape}”)
print(f”出力: {output.shape}”)
実行結果:
入力: torch.Size([1, 64, 56, 56])
出力: torch.Size([1, 64, 56, 56])
4-2. Bottleneck Blockの実装
class BottleneckBlock(nn.Module):
“””ResNet-50, 101, 152用のBottleneck Block”””
expansion = 4 # 出力チャンネル数は入力の4倍
def __init__(self, in_channels, out_channels, stride=1, downsample=None):
super(BottleneckBlock, self).__init__()
# 1×1畳み込み(次元削減)
# 例:256ch → 64ch
self.conv1 = nn.Conv2d(in_channels, out_channels,
kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
# 3×3畳み込み(メインの特徴抽出)
# チャンネル数が少ないので計算量が少ない
self.conv2 = nn.Conv2d(out_channels, out_channels,
kernel_size=3, stride=stride,
padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# 1×1畳み込み(次元復元)
# 例:64ch → 256ch(expansion=4なので4倍)
self.conv3 = nn.Conv2d(out_channels, out_channels * self.expansion,
kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(out_channels * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
def forward(self, x):
identity = x
# 1×1 → 3×3 → 1×1
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
# テスト
# 入力256ch、中間64ch、出力256ch(64×4)
downsample = nn.Sequential(
nn.Conv2d(64, 256, kernel_size=1, bias=False),
nn.BatchNorm2d(256)
)
block = BottleneckBlock(64, 64, downsample=downsample)
x = torch.randn(1, 64, 56, 56)
output = block(x)
print(f”入力: {x.shape}”)
print(f”出力: {output.shape}”)
実行結果:
入力: torch.Size([1, 64, 56, 56])
出力: torch.Size([1, 256, 56, 56])
🎓 5. 事前学習モデルと転移学習
実際には、ResNetを最初から訓練することは稀です。事前学習済みモデル(ImageNetで学習済み)を使うのが一般的です。
5-1. 事前学習モデルの読み込み
# PyTorchで事前学習済みResNetを読み込む
import torch
import torchvision.models as models
# 方法1:古い書き方(まだ動くが警告が出る)
# model = models.resnet50(pretrained=True)
# 方法2:推奨される書き方(PyTorch 1.13以降)
from torchvision.models import ResNet50_Weights
# ImageNetで事前学習された重みを使用
model = models.resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
# モデル構造を確認
print(model)
実行結果(一部):
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, …)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, …)
(layer1): Sequential(…)
(layer2): Sequential(…)
(layer3): Sequential(…)
(layer4): Sequential(…)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=2048, out_features=1000, bias=True) ← 最後の全結合層
)
5-2. 転移学習の3つのアプローチ
転移学習には、データ量や目的に応じて3つのアプローチがあります。
| アプローチ |
学習する部分 |
データ量の目安 |
特徴 |
| Feature Extraction |
最後の層(fc)のみ |
〜1,000枚 |
過学習しにくい |
| Partial Fine-tuning |
後半の層 + fc |
1,000〜10,000枚 |
バランスが良い |
| Full Fine-tuning |
全ての層 |
10,000枚〜 |
最高精度の可能性 |
5-3. Feature Extraction(特徴抽出)の実装
最もシンプルな転移学習。事前学習部分を固定し、最後の全結合層だけを新しいタスク用に置き換えて学習します。
# Feature Extraction:最後の層だけ学習
import torch
import torch.nn as nn
import torchvision.models as models
from torchvision.models import ResNet50_Weights
# 1. 事前学習済みモデルを読み込み
model = models.resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
# 2. 事前学習部分のパラメータを固定(学習しない)
for param in model.parameters():
param.requires_grad = False
# 3. 最後の全結合層を新しいタスク用に置き換え
# 例:2クラス分類(猫 vs 犬)の場合
num_classes = 2
# model.fc:ResNetの最後の全結合層
# in_features:入力特徴量の次元(ResNet-50は2048)
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, num_classes)
# 4. 確認:学習可能なパラメータ
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())
print(f”学習可能なパラメータ: {trainable_params:,}”)
print(f”全パラメータ: {total_params:,}”)
print(f”学習率: {trainable_params / total_params * 100:.2f}%”)
実行結果:
学習可能なパラメータ: 4,098
全パラメータ: 23,512,130
学習率: 0.02%
5-4. Partial Fine-tuning(部分的ファインチューニング)の実装
後半の層だけ学習を許可し、初期層は固定します。
# Partial Fine-tuning:後半の層 + fcを学習
import torch
import torch.nn as nn
import torchvision.models as models
from torchvision.models import ResNet50_Weights
# 1. 事前学習済みモデルを読み込み
model = models.resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
# 2. 全てのパラメータを一旦固定
for param in model.parameters():
param.requires_grad = False
# 3. 後半の層(layer4とfc)を学習可能に
# layer4:ResNetの最後のブロック群
for param in model.layer4.parameters():
param.requires_grad = True
# 4. 最後の全結合層を置き換え
num_classes = 2
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, num_classes)
# 新しく作った層は自動的にrequires_grad=True
# 5. 学習率を層ごとに変える(重要!)
# 事前学習部分は小さい学習率、新しい層は大きい学習率
optimizer = torch.optim.Adam([
{‘params’: model.layer4.parameters(), ‘lr’: 1e-5}, # 後半層:小さい学習率
{‘params’: model.fc.parameters(), ‘lr’: 1e-3} # 新しい層:大きい学習率
])
# 確認
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f”学習可能なパラメータ: {trainable_params:,}”)
実行結果:
学習可能なパラメータ: 7,083,010
5-5. Full Fine-tuning(全体ファインチューニング)の実装
全ての層を学習します。データが十分にある場合に使用します。
# Full Fine-tuning:全ての層を学習
import torch
import torch.nn as nn
import torchvision.models as models
from torchvision.models import ResNet50_Weights
# 1. 事前学習済みモデルを読み込み
model = models.resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
# 2. 最後の全結合層を置き換え
num_classes = 2
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, num_classes)
# 3. 全パラメータが学習可能(デフォルト)
# requires_gradは変更しない
# 4. 学習率を層ごとに細かく設定(推奨)
optimizer = torch.optim.Adam([
# 初期層:最も小さい学習率(一般的な特徴を保持)
{‘params’: model.conv1.parameters(), ‘lr’: 1e-6},
{‘params’: model.layer1.parameters(), ‘lr’: 1e-6},
# 中間層:中程度の学習率
{‘params’: model.layer2.parameters(), ‘lr’: 1e-5},
{‘params’: model.layer3.parameters(), ‘lr’: 1e-5},
# 後半層:やや大きい学習率
{‘params’: model.layer4.parameters(), ‘lr’: 1e-4},
# 新しい層:最も大きい学習率
{‘params’: model.fc.parameters(), ‘lr’: 1e-3}
])
# 確認
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f”学習可能なパラメータ: {trainable_params:,}”)
実行結果:
学習可能なパラメータ: 23,512,130
5-6. 転移学習の完全な訓練コード
※ コードが横に長い場合は横スクロールできます
# 転移学習の完全な訓練コード
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models
from torchvision.models import ResNet50_Weights
# デバイス設定
device = torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’)
print(f”使用デバイス: {device}”)
# データ拡張と前処理
train_transform = transforms.Compose([
transforms.Resize((256, 256)),
transforms.RandomCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
test_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# データセット(例:ImageFolderを使用)
# train_dataset = datasets.ImageFolder(‘data/train’, transform=train_transform)
# test_dataset = datasets.ImageFolder(‘data/test’, transform=test_transform)
# DataLoader
# train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
# test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False, num_workers=4)
# モデル(Feature Extractionの場合)
model = models.resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
for param in model.parameters():
param.requires_grad = False
num_classes = 2 # 分類するクラス数
model.fc = nn.Linear(model.fc.in_features, num_classes)
model = model.to(device)
# 損失関数とオプティマイザ
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.fc.parameters(), lr=0.001)
# 学習率スケジューラ(オプション)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)
# 訓練関数
def train_one_epoch(model, loader, criterion, optimizer, device):
model.train()
running_loss = 0.0
correct = 0
total = 0
for images, labels in loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
epoch_loss = running_loss / len(loader)
epoch_acc = 100. * correct / total
return epoch_loss, epoch_acc
# 評価関数
def evaluate(model, loader, criterion, device):
model.eval()
running_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
running_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
epoch_loss = running_loss / len(loader)
epoch_acc = 100. * correct / total
return epoch_loss, epoch_acc
# 訓練ループ(実際に実行する場合はDataLoaderが必要)
# num_epochs = 10
# for epoch in range(num_epochs):
# train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device)
# test_loss, test_acc = evaluate(model, test_loader, criterion, device)
# scheduler.step()
#
# print(f”Epoch [{epoch+1}/{num_epochs}]”)
# print(f” Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%”)
# print(f” Test Loss: {test_loss:.4f}, Test Acc: {test_acc:.2f}%”)
print(“訓練コードの準備完了!”)
print(“実際のデータセットを用意して実行してください。”)
📝 練習問題
問題1:ResNetの理解(基礎)
以下の質問に答えてください。
- 従来のネットワークとResNetの最大の違いは何ですか?
- Skip Connectionが勾配消失問題を緩和する理由を説明してください。
- なぜ残差 F(x) = H(x) – x を学習する方が、H(x)を直接学習するより簡単なのですか?
解答:
1. 最大の違い
従来:目的の関数 H(x) を直接学習する
ResNet:Skip Connectionを使って、残差 F(x) = H(x) – x を学習する
2. Skip Connectionが勾配消失を緩和する理由
勾配がSkip Connectionを通って直接伝播するため。逆伝播時に、勾配は「メイン経路からの勾配」と「ショートカットからの勾配(常に1)」の和になります。ショートカットからの勾配が常に存在するため、全体の勾配がゼロになりにくくなります。
3. 残差の方が学習しやすい理由
恒等写像(y = x)を学習したい場合を考えます。
従来の方法:H(x) = x を学習する必要があり、層の重みを「入力をそのまま出力する」ように精密に調整する必要がある。これは難しい。
ResNetの方法:F(x) = 0 を学習すれば H(x) = F(x) + x = 0 + x = x となる。F(x) = 0 は重みをゼロに近づけるだけなので簡単。
問題2:Basic BlockとBottleneck Blockの比較(中級)
256チャンネルの入力に対して、Basic BlockとBottleneck Blockの計算量(パラメータ数)を計算してください。Bottleneckはどれくらい効率的ですか?
解答:
Basic Block(2つの3×3畳み込み):
第1層:3×3×256×256 = 589,824
第2層:3×3×256×256 = 589,824
合計:1,179,648 パラメータ
Bottleneck Block(1×1 → 3×3 → 1×1):
第1層(次元削減):1×1×256×64 = 16,384
第2層(3×3畳み込み):3×3×64×64 = 36,864
第3層(次元復元):1×1×64×256 = 16,384
合計:69,632 パラメータ
効率の比較:
1,179,648 ÷ 69,632 ≒ 16.9
→ Bottleneck Blockは約17分の1の計算量で、非常に効率的!
問題3:転移学習のアプローチ選択(応用)
以下のシナリオでは、どの転移学習アプローチを選ぶべきですか?理由とともに答えてください。
- 医療画像データ500枚で病気を分類
- 猫50品種の分類(各品種200枚、計10,000枚)
- 企業独自の製品画像50,000枚で不良品検出
解答:
1. 医療画像500枚 → Feature Extraction
理由:データが非常に少ないため、多くのパラメータを学習すると過学習する。事前学習部分を固定し、最後の全結合層のみを学習することで、少ないデータでも安定して学習できる。
2. 猫50品種10,000枚 → Partial Fine-tuning
理由:データが中程度あるため、後半の層も学習させることでタスクに適応させられる。初期層(エッジやテクスチャなどの一般的な特徴)は固定し、後半の層(より抽象的な特徴)と全結合層を学習する。
3. 製品画像50,000枚 → Full Fine-tuning
理由:データが十分にあるため、モデル全体をタスクに最適化できる。ただし、学習率は層ごとに調整し、初期層は小さい学習率、後半層は大きい学習率を設定する。
問題4:Skip Connectionの実装(応用)
次元が合わない場合のdownsampleの処理を含むBasic Blockを実装してください。入力が64チャンネル、出力が128チャンネル、stride=2の場合を想定してください。
解答:
import torch
import torch.nn as nn
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, in_channels, out_channels, stride=1):
super(BasicBlock, self).__init__()
# メイン経路
self.conv1 = nn.Conv2d(in_channels, out_channels,
kernel_size=3, stride=stride,
padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(out_channels, out_channels,
kernel_size=3, stride=1,
padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# 次元が合わない場合のダウンサンプリング
self.downsample = None
if stride != 1 or in_channels != out_channels:
self.downsample = nn.Sequential(
nn.Conv2d(in_channels, out_channels,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
# テスト:64ch→128ch, stride=2(サイズ半分)
block = BasicBlock(64, 128, stride=2)
x = torch.randn(1, 64, 56, 56)
output = block(x)
print(f”入力: {x.shape}”)
print(f”出力: {output.shape}”)
実行結果:
入力: torch.Size([1, 64, 56, 56])
出力: torch.Size([1, 128, 28, 28])
ポイント:stride=2でサイズが半分(56→28)、チャンネル数は64→128に増加。downsampleの1×1畳み込みで、ショートカットの次元も同様に変換している。
問題5:過学習対策(応用)
ResNet-50を使った転移学習で過学習が発生した場合、どのような対策を取るべきですか?少なくとも5つ挙げてください。
解答:
1. データ拡張を強化
RandomRotation、ColorJitter、GaussianBlur、RandomAffine等を追加して、訓練データの多様性を増やす。
2. Dropoutを追加
全結合層の前にDropout(0.5)を挿入して、過学習を防ぐ。
model.fc = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(num_features, num_classes)
)
3. 学習率を下げる
事前学習部分は1e-6〜1e-5程度の小さい学習率を使う。新しい層は1e-3〜1e-4程度。
4. Early Stoppingを実装
検証精度が改善しなくなったら訓練を停止する。
5. Weight Decay(L2正則化)を追加
optimizerにweight_decay=1e-4を設定して、重みの大きさを制限する。
optimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-4)
6. Feature Extractionに切り替え
データが少ない場合は、Full Fine-tuningではなくFeature Extractionに切り替えて、学習可能なパラメータ数を減らす。
7. バッチサイズを調整
バッチサイズを大きくすると学習が安定し、過学習が軽減される場合がある。
📝 STEP 5 のまとめ
✅ このステップで学んだこと
1. 深いネットワークの課題
・勾配消失問題:勾配が層を遡るごとに小さくなる
・劣化問題:深くしても訓練誤差すら改善しない
2. 残差学習(Residual Learning)
・H(x)を直接学習するのではなく、F(x) = H(x) – x を学習
・恒等写像の学習が容易になる(F(x) = 0 にするだけ)
3. Skip Connection
・勾配が直接伝播するため、勾配消失を緩和
・パラメータなしの単純な加算
4. ResNetのアーキテクチャ
・Basic Block(ResNet-18, 34):2つの3×3畳み込み
・Bottleneck Block(ResNet-50+):1×1 → 3×3 → 1×1で効率化
5. 転移学習
・Feature Extraction:最後の層のみ学習(データ少)
・Partial Fine-tuning:後半層 + fc(データ中)
・Full Fine-tuning:全体を学習(データ多)
💡 ResNetが重要な理由
ResNetは2015年にコンピュータビジョンの歴史を変えた革命的なアーキテクチャです。Skip Connectionという概念は、ResNetだけでなく、その後のほぼ全ての重要なアーキテクチャ(DenseNet、U-Net、Transformer等)に影響を与えています。
次のSTEP 6では、ResNetの考え方をさらに発展させたDenseNetと、別のアプローチを取るInceptionを学びます。