STEP 5:ResNetの理論と実装

🏗️ STEP 5: ResNetの理論と実装

残差学習とSkip Connectionの仕組みを理解し、
ResNetの実装と転移学習を実践します

📋 このステップで学ぶこと

  • 深いネットワークの課題(勾配消失問題、劣化問題)
  • 残差学習(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の理解(基礎)

以下の質問に答えてください。

  1. 従来のネットワークとResNetの最大の違いは何ですか?
  2. Skip Connectionが勾配消失問題を緩和する理由を説明してください。
  3. なぜ残差 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:転移学習のアプローチ選択(応用)

以下のシナリオでは、どの転移学習アプローチを選ぶべきですか?理由とともに答えてください。

  1. 医療画像データ500枚で病気を分類
  2. 猫50品種の分類(各品種200枚、計10,000枚)
  3. 企業独自の製品画像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を学びます。

📝

学習メモ

コンピュータビジョン(CV) - Step 5

📋 過去のメモ一覧
#artnasekai #学習メモ
LINE