📋 このステップで学ぶこと
- DenseNet(Densely Connected Convolutional Networks)
- Dense Connection(密な接続)の仕組み
- Growth RateとTransition Layer
- Inception(GoogLeNet)の設計思想
- Inception Module(複数サイズのフィルタ並列適用)
- 1×1 Convolutionの3つの役割
- ResNeXt(ResNetの進化版)
- 各アーキテクチャの比較と使い分け
🌲 1. DenseNet – 密な接続による特徴の再利用
DenseNet(Densely Connected Convolutional Networks)は、2017年にCVPRでBest Paper Awardを受賞したアーキテクチャです。ResNetのSkip Connectionをさらに発展させた「Dense Connection」が特徴です。
1-1. ResNetとDenseNetの違い
ResNetでは「加算(Addition)」を使いましたが、DenseNetでは「連結(Concatenation)」を使います。この違いが大きな効果を生みます。
💡 核心的な違い:加算 vs 連結
ResNet:H(x) = F(x) + x(特徴を加算)
DenseNet:H(x) = [x, F(x)](特徴を連結)
【ResNet(加算)】
入力 x (64ch) → [層] → F(x) (64ch)
↓
出力 = F(x) + x = 64チャンネル
※ 加算なのでチャンネル数は変わらない
※ 元の情報と新しい情報が「混ざる」
【DenseNet(連結)】
入力 x (64ch) → [層] → F(x) (32ch)
↓
出力 = [x, F(x)] = 64 + 32 = 96チャンネル
※ 連結なのでチャンネル数が増える
※ 元の情報と新しい情報が「両方残る」
1-2. Dense Connectionの仕組み
DenseNetでは、各層がそれ以前のすべての層の出力を入力として受け取ります。これを「Dense Connection(密な接続)」と呼びます。
【Dense Block内の接続パターン】
層0(入力): x₀ (64ch)
↓
層1: x₀ を入力 → 32ch出力
連結: [x₀, x₁] = 64 + 32 = 96ch
↓
層2: [x₀, x₁] を入力 → 32ch出力
連結: [x₀, x₁, x₂] = 96 + 32 = 128ch
↓
層3: [x₀, x₁, x₂] を入力 → 32ch出力
連結: [x₀, x₁, x₂, x₃] = 128 + 32 = 160ch
↓
層4: [x₀, x₁, x₂, x₃] を入力 → 32ch出力
連結: [x₀, x₁, x₂, x₃, x₄] = 160 + 32 = 192ch
→ 各層は、前のすべての層の特徴マップにアクセスできる!
💡 Dense Connectionの4つの利点
① 特徴の再利用:全ての層が前の層の特徴を直接使える。無駄な再学習が不要。
② 勾配の直接伝播:勾配が各層に直接伝わる。100層以上でも勾配消失しにくい。
③ パラメータ効率:各層は少ないチャンネル(32ch程度)を出力するだけ。ResNetの1/3のパラメータで同等以上の精度。
④ 正則化効果:特徴の再利用により過学習しにくい。
1-3. Growth Rate(成長率)
Growth Rate(k)は、DenseNet固有のハイパーパラメータです。各層が出力するチャンネル数を決定します。
【Growth Rate = 32 の場合】
Dense Block内で、各層は32チャンネルを出力する。
入力: 64チャンネル
層1: +32ch → 合計 64 + 32 = 96ch
層2: +32ch → 合計 96 + 32 = 128ch
層3: +32ch → 合計 128 + 32 = 160ch
層4: +32ch → 合計 160 + 32 = 192ch
層5: +32ch → 合計 192 + 32 = 224ch
層6: +32ch → 合計 224 + 32 = 256ch
→ チャンネル数が線形に増加していく
🎯 Growth Rateの選び方
k = 12〜16:軽量モデル。メモリ効率が良い。
k = 32:標準的な設定。DenseNet-121などで使用。
k = 48〜64:高精度モデル。計算量が多い。
小さいGrowth Rateでも高精度を達成できるのが、DenseNetの大きな特徴です。
1-4. Transition Layer(圧縮層)
Dense Block内ではチャンネル数がどんどん増えていきます。このままでは計算量が爆発してしまうため、Transition Layerでチャンネル数を圧縮します。
【Transition Layerの構造】
Dense Block 1の出力: 256チャンネル
↓
┌─────────────────────────┐
│ BatchNorm │
│ ReLU │
│ Conv 1×1 (256 → 128ch) │ ← チャンネル数を半分に
│ AvgPool 2×2 (stride 2) │ ← 空間サイズを半分に
└─────────────────────────┘
↓
Dense Block 2への入力: 128チャンネル
※ Compression Factor θ = 0.5(半分に圧縮)
⚠️ DenseNetの全体構造
DenseNetは「Dense Block」と「Transition Layer」が交互に繰り返される構造です。
【DenseNet-121の全体構造】
入力画像 (224×224×3)
↓
Conv 7×7 (stride 2) → 112×112×64
↓
MaxPool 3×3 (stride 2) → 56×56×64
↓
Dense Block 1 (6層, k=32) → 56×56×256
↓
Transition Layer 1 → 28×28×128
↓
Dense Block 2 (12層, k=32) → 28×28×512
↓
Transition Layer 2 → 14×14×256
↓
Dense Block 3 (24層, k=32) → 14×14×1024
↓
Transition Layer 3 → 7×7×512
↓
Dense Block 4 (16層, k=32) → 7×7×1024
↓
Global Average Pooling → 1×1×1024
↓
FC → 1000クラス
💻 2. DenseNetの実装
2-1. Dense Layerの実装
まず、Dense Block内の1層(Dense Layer)を実装します。DenseNetでは「BN-ReLU-Conv」の順序を使います(Pre-activation構造)。
ステップ1:クラスの定義
import torch
import torch.nn as nn
class DenseLayer(nn.Module):
“””
DenseNet内の1層(Bottleneck構造付き)
構造:BN → ReLU → Conv1×1 → BN → ReLU → Conv3×3
Args:
in_channels: 入力チャンネル数(前の全層の出力の合計)
growth_rate: この層が出力するチャンネル数(k)
“””
def __init__(self, in_channels, growth_rate):
super(DenseLayer, self).__init__()
ステップ2:Bottleneck部分(1×1畳み込み)
# Bottleneck部分
# 1×1畳み込みで次元を削減してから3×3畳み込みを行う
# 出力チャンネル数は通常 4 × growth_rate
self.bn1 = nn.BatchNorm2d(in_channels)
self.relu1 = nn.ReLU(inplace=True)
# 1×1畳み込み:チャンネル数を 4*k に削減
# なぜ4倍?→ 経験的にこの値が良いとされている
self.conv1 = nn.Conv2d(in_channels, 4 * growth_rate,
kernel_size=1, bias=False)
ステップ3:3×3畳み込み部分
# 3×3畳み込み部分
self.bn2 = nn.BatchNorm2d(4 * growth_rate)
self.relu2 = nn.ReLU(inplace=True)
# 3×3畳み込み:growth_rateチャンネルを出力
# padding=1で空間サイズを維持
self.conv2 = nn.Conv2d(4 * growth_rate, growth_rate,
kernel_size=3, padding=1, bias=False)
ステップ4:順伝播(連結処理)
def forward(self, x):
“””
順伝播:入力と出力を連結して返す
Args:
x: 入力テンソル(前の全層の出力が連結されたもの)
Returns:
入力と新しい特徴マップを連結したテンソル
“””
# Bottleneck + 3×3畳み込み
out = self.conv1(self.relu1(self.bn1(x)))
out = self.conv2(self.relu2(self.bn2(out)))
# 重要:入力と出力を連結(これがDenseNetの核心)
# dim=1 はチャンネル方向での連結
return torch.cat([x, out], dim=1)
完成コード(Dense Layer)
※ コードが横に長い場合は横スクロールできます
import torch
import torch.nn as nn
class DenseLayer(nn.Module):
“””DenseNet内の1層(Bottleneck構造付き)”””
def __init__(self, in_channels, growth_rate):
super(DenseLayer, self).__init__()
# Bottleneck: BN → ReLU → Conv1×1
self.bn1 = nn.BatchNorm2d(in_channels)
self.relu1 = nn.ReLU(inplace=True)
self.conv1 = nn.Conv2d(in_channels, 4 * growth_rate,
kernel_size=1, bias=False)
# 3×3畳み込み: BN → ReLU → Conv3×3
self.bn2 = nn.BatchNorm2d(4 * growth_rate)
self.relu2 = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(4 * growth_rate, growth_rate,
kernel_size=3, padding=1, bias=False)
def forward(self, x):
out = self.conv1(self.relu1(self.bn1(x)))
out = self.conv2(self.relu2(self.bn2(out)))
return torch.cat([x, out], dim=1) # 連結!
# テスト
layer = DenseLayer(in_channels=64, growth_rate=32)
x = torch.randn(1, 64, 56, 56)
output = layer(x)
print(f”入力: {x.shape}”)
print(f”出力: {output.shape}”)
実行結果:
入力: torch.Size([1, 64, 56, 56])
出力: torch.Size([1, 96, 56, 56]) ← 64 + 32 = 96チャンネル
2-2. Dense Blockの実装
class DenseBlock(nn.Module):
“””複数のDense Layerからなるブロック”””
def __init__(self, num_layers, in_channels, growth_rate):
“””
Args:
num_layers: ブロック内の層数
in_channels: 入力チャンネル数
growth_rate: 各層が出力するチャンネル数
“””
super(DenseBlock, self).__init__()
layers = []
for i in range(num_layers):
# 各層の入力チャンネル数 = 初期 + これまでの層の出力の合計
layer_in_channels = in_channels + i * growth_rate
layers.append(DenseLayer(layer_in_channels, growth_rate))
self.layers = nn.ModuleList(layers)
def forward(self, x):
for layer in self.layers:
x = layer(x) # 各層で連結が行われる
return x
# テスト:6層のDense Block
block = DenseBlock(num_layers=6, in_channels=64, growth_rate=32)
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]) ← 64 + 6×32 = 256チャンネル
2-3. Transition Layerの実装
class TransitionLayer(nn.Module):
“””Dense Block間の圧縮層”””
def __init__(self, in_channels, out_channels):
“””
Args:
in_channels: 入力チャンネル数
out_channels: 出力チャンネル数(通常は半分)
“””
super(TransitionLayer, self).__init__()
self.bn = nn.BatchNorm2d(in_channels)
self.relu = nn.ReLU(inplace=True)
# 1×1畳み込みでチャンネル数を削減
self.conv = nn.Conv2d(in_channels, out_channels,
kernel_size=1, bias=False)
# Average Poolingで空間サイズを半分に
self.pool = nn.AvgPool2d(kernel_size=2, stride=2)
def forward(self, x):
out = self.conv(self.relu(self.bn(x)))
out = self.pool(out)
return out
# テスト
transition = TransitionLayer(in_channels=256, out_channels=128)
x = torch.randn(1, 256, 56, 56)
output = transition(x)
print(f”入力: {x.shape}”)
print(f”出力: {output.shape}”)
実行結果:
入力: torch.Size([1, 256, 56, 56])
出力: torch.Size([1, 128, 28, 28]) ← チャンネル数もサイズも半分に
2-4. PyTorchの事前学習済みDenseNetを使う
# PyTorchで事前学習済みDenseNetを読み込む
import torch
import torchvision.models as models
from torchvision.models import DenseNet121_Weights
# DenseNet-121を読み込み(ImageNetで事前学習済み)
model = models.densenet121(weights=DenseNet121_Weights.IMAGENET1K_V1)
# モデル構造の確認
print(model)
# パラメータ数を確認
total_params = sum(p.numel() for p in model.parameters())
print(f”\n総パラメータ数: {total_params:,}”)
# 転移学習用にclassifierを置き換え
num_classes = 2 # 例:猫 vs 犬
num_features = model.classifier.in_features # 1024
model.classifier = nn.Linear(num_features, num_classes)
print(f”新しい分類器: {model.classifier}”)
実行結果:
DenseNet(
(features): Sequential(
(conv0): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
…
(denseblock4): _DenseBlock(…)
(norm5): BatchNorm2d(1024, …)
)
(classifier): Linear(in_features=1024, out_features=1000, bias=True)
)
総パラメータ数: 7,978,856
新しい分類器: Linear(in_features=1024, out_features=2, bias=True)
🏛️ 3. Inception(GoogLeNet)- マルチスケール処理
Inception(GoogLeNet)は、2014年にGoogleが発表したアーキテクチャです。「どのサイズのフィルタが最適かわからないなら、全部使えばいい」という大胆な発想が特徴です。
3-1. Inceptionの設計思想
💡 Inceptionの核心的アイデア
問題:画像内の物体サイズは様々。大きい物体には大きいフィルタ、小さい物体には小さいフィルタが有効。でも、どのサイズが最適かは事前にわからない。
解決:1×1、3×3、5×5のフィルタを並列に適用し、その結果を連結する。ネットワーク自身に最適な組み合わせを学習させる。
【Inception Moduleの基本構造】
入力
↓
┌────────┬────────┬────────┬────────┐
↓ ↓ ↓ ↓ ↓
1×1 3×3 5×5 MaxPool
Conv Conv Conv 3×3
↓ ↓ ↓ ↓
│ │ │ │
└────────┴────────┴────────┘
↓
Concatenate(連結)
↓
出力
※ 異なるサイズのフィルタで異なるスケールの特徴を捉える
※ 結果を連結してチャンネル方向にまとめる
3-2. 1×1畳み込みの3つの役割
1×1畳み込みはInceptionの重要な要素です。「1×1のフィルタで何ができるの?」と思うかもしれませんが、実は非常に強力です。
💡 1×1畳み込みの3つの役割
① 次元削減(Dimension Reduction)
チャンネル数を減らして計算量を削減。256ch → 64ch など。
② 非線形性の追加
ReLUと組み合わせることで、モデルの表現力を向上。
③ クロスチャンネル学習
チャンネル間の相関関係を学習。空間的な情報は変えずに、チャンネル方向の特徴を抽出。
なぜ次元削減が重要なのか?
【計算量の比較:192ch入力、5×5畳み込み、32ch出力】
■ 次元削減なしの場合
5×5×192×32 = 153,600 パラメータ
■ 1×1で次元削減してからの場合
1×1×192×16 = 3,072 パラメータ(192→16に削減)
5×5×16×32 = 12,800 パラメータ
合計 = 15,872 パラメータ
比較:153,600 ÷ 15,872 ≒ 9.7
→ 約10分の1の計算量で同等の特徴抽出!
3-3. Inception Module(Dimension Reduction付き)
【Inception Module with Dimension Reduction】
入力 (192ch)
↓
┌──────────┬──────────┬──────────┬──────────┐
↓ ↓ ↓ ↓ ↓
1×1 1×1 1×1 MaxPool
64ch 96ch 16ch 3×3
↓ ↓ ↓ ↓
│ 3×3 5×5 1×1
│ 128ch 32ch 32ch
↓ ↓ ↓ ↓
└──────────┴──────────┴──────────┘
↓
Concatenate
↓
出力 (256ch)
64+128+32+32=256
※ 3×3と5×5の前に1×1で次元削減
※ MaxPoolの後にも1×1で次元削減
3-4. Inception Moduleの実装
※ コードが横に長い場合は横スクロールできます
import torch
import torch.nn as nn
class InceptionModule(nn.Module):
“””Inception Module with Dimension Reduction”””
def __init__(self, in_channels, ch1x1, ch3x3_reduce, ch3x3,
ch5x5_reduce, ch5x5, pool_proj):
“””
Args:
in_channels: 入力チャンネル数
ch1x1: 1×1ブランチの出力チャンネル数
ch3x3_reduce: 3×3ブランチの1×1削減後のチャンネル数
ch3x3: 3×3ブランチの出力チャンネル数
ch5x5_reduce: 5×5ブランチの1×1削減後のチャンネル数
ch5x5: 5×5ブランチの出力チャンネル数
pool_proj: プーリングブランチの1×1後のチャンネル数
“””
super(InceptionModule, self).__init__()
# ブランチ1: 1×1畳み込み
self.branch1 = nn.Sequential(
nn.Conv2d(in_channels, ch1x1, kernel_size=1),
nn.BatchNorm2d(ch1x1),
nn.ReLU(inplace=True)
)
# ブランチ2: 1×1 → 3×3
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, ch3x3_reduce, kernel_size=1),
nn.BatchNorm2d(ch3x3_reduce),
nn.ReLU(inplace=True),
nn.Conv2d(ch3x3_reduce, ch3x3, kernel_size=3, padding=1),
nn.BatchNorm2d(ch3x3),
nn.ReLU(inplace=True)
)
# ブランチ3: 1×1 → 5×5
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, ch5x5_reduce, kernel_size=1),
nn.BatchNorm2d(ch5x5_reduce),
nn.ReLU(inplace=True),
nn.Conv2d(ch5x5_reduce, ch5x5, kernel_size=5, padding=2),
nn.BatchNorm2d(ch5x5),
nn.ReLU(inplace=True)
)
# ブランチ4: MaxPool → 1×1
self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels, pool_proj, kernel_size=1),
nn.BatchNorm2d(pool_proj),
nn.ReLU(inplace=True)
)
def forward(self, x):
# 各ブランチを並列に計算
branch1_out = self.branch1(x)
branch2_out = self.branch2(x)
branch3_out = self.branch3(x)
branch4_out = self.branch4(x)
# チャンネル方向に連結
return torch.cat([branch1_out, branch2_out,
branch3_out, branch4_out], dim=1)
# テスト
inception = InceptionModule(
in_channels=192,
ch1x1=64,
ch3x3_reduce=96, ch3x3=128,
ch5x5_reduce=16, ch5x5=32,
pool_proj=32
)
x = torch.randn(1, 192, 28, 28)
output = inception(x)
print(f”入力: {x.shape}”)
print(f”出力: {output.shape}”)
print(f”出力チャンネル数: 64+128+32+32 = {64+128+32+32}”)
実行結果:
入力: torch.Size([1, 192, 28, 28])
出力: torch.Size([1, 256, 28, 28])
出力チャンネル数: 64+128+32+32 = 256
3-5. PyTorchの事前学習済みInceptionを使う
# PyTorchで事前学習済みInception v3を読み込む
import torch
import torchvision.models as models
from torchvision.models import Inception_V3_Weights
# Inception v3を読み込み
# 注意:Inception v3は入力サイズが299×299
model = models.inception_v3(weights=Inception_V3_Weights.IMAGENET1K_V1)
# 訓練時には補助分類器も使われる(aux_logits=True)
# 推論時にはメイン分類器のみ使用
print(f”補助分類器の有無: {model.aux_logits}”)
# 転移学習用に分類器を置き換え
num_classes = 2
model.fc = nn.Linear(model.fc.in_features, num_classes)
# 補助分類器も置き換え(訓練時に使用)
if model.aux_logits:
model.AuxLogits.fc = nn.Linear(model.AuxLogits.fc.in_features, num_classes)
print(f”メイン分類器: {model.fc}”)
# 推論時の使用例
model.eval() # 評価モードに(補助分類器は使われない)
x = torch.randn(1, 3, 299, 299) # 299×299が必要!
with torch.no_grad():
output = model(x)
print(f”出力: {output.shape}”)
実行結果:
補助分類器の有無: True
メイン分類器: Linear(in_features=2048, out_features=2, bias=True)
出力: torch.Size([1, 2])
🔗 4. ResNeXt – ResNetとInceptionの融合
ResNeXtは、ResNetとInceptionのアイデアを組み合わせたアーキテクチャです。「Cardinality(並列経路の数)」という新しい次元を導入しました。
4-1. ResNeXtのアイデア
💡 ResNeXtの発想
ResNet:1つの経路 + Skip Connection
Inception:複数の異なる経路(1×1, 3×3, 5×5)
ResNeXt:複数の同じ経路(同じ構造を並列に)+ Skip Connection
【ResNeXtの構造(Cardinality = 32)】
入力 x
↓
├─────────────────────────────────────┐
↓ │
┌─────────────────────────────────────┐ │
│ 経路1: 1×1(4ch) → 3×3(4ch) → 1×1 │ │
│ 経路2: 1×1(4ch) → 3×3(4ch) → 1×1 │ │
│ 経路3: 1×1(4ch) → 3×3(4ch) → 1×1 │ │
│ … │ │
│ 経路32: 1×1(4ch) → 3×3(4ch) → 1×1 │ │
└─────────────────────────────────────┘ │
↓ (32経路の出力を加算) │
+ ←───────────────────────────────────┘
↓
出力
※ 32個の同じ構造の経路を並列に実行
※ Cardinality = 32(並列経路の数)
4-2. Cardinalityの重要性
【同じ計算量での比較】
■ ResNet-50(より広いチャンネル)
256チャンネル × 1経路
パラメータ数: 大
精度: ベースライン
■ ResNeXt-50 (32×4d)
4チャンネル × 32経路 = 128チャンネル相当
パラメータ数: 同程度
精度: ResNet-50より高い!
→ 「深さ」「幅」に加えて「Cardinality」が重要な次元
→ 同じ計算量なら、チャンネルを広げるより経路を増やす方が効果的
4-3. PyTorchでResNeXtを使う
# PyTorchで事前学習済みResNeXtを読み込む
import torch
import torchvision.models as models
from torchvision.models import ResNeXt50_32X4D_Weights
# ResNeXt-50 (32×4d) を読み込み
# 32 = Cardinality(並列経路の数)
# 4d = 各経路のチャンネル幅
model = models.resnext50_32x4d(weights=ResNeXt50_32X4D_Weights.IMAGENET1K_V2)
# パラメータ数を確認
total_params = sum(p.numel() for p in model.parameters())
print(f”ResNeXt-50 (32×4d) パラメータ数: {total_params:,}”)
# 転移学習用に分類器を置き換え
num_classes = 2
model.fc = nn.Linear(model.fc.in_features, num_classes)
# テスト
x = torch.randn(1, 3, 224, 224)
model.eval()
with torch.no_grad():
output = model(x)
print(f”出力: {output.shape}”)
実行結果:
ResNeXt-50 (32×4d) パラメータ数: 25,028,904
出力: torch.Size([1, 2])
📊 5. アーキテクチャの比較と選び方
5-1. 主要アーキテクチャの比較表
| 項目 |
ResNet |
DenseNet |
Inception |
ResNeXt |
| 核心アイデア |
Skip Connection(加算) |
Dense Connection(連結) |
マルチスケール(並列フィルタ) |
Cardinality(並列経路) |
| 発表年 |
2015 |
2017 |
2014 |
2017 |
| 代表モデル |
ResNet-50 |
DenseNet-121 |
Inception v3 |
ResNeXt-50 |
| パラメータ数 |
25.6M |
8.0M |
27.2M |
25.0M |
| Top-1精度 |
76.1% |
74.4% |
77.3% |
77.6% |
| 推論速度 |
速い |
やや遅い |
普通 |
速い |
| メモリ使用量 |
中程度 |
多い |
中程度 |
中程度 |
5-2. 用途別の選び方
🎯 シナリオ別のおすすめモデル
📱 モバイル・組み込み(速度・サイズ重視):
→ MobileNet、EfficientNet-B0(次のSTEPで学習)
💻 一般的な画像分類(バランス重視):
→ ResNet-50、ResNeXt-50
💾 パラメータ効率(メモリ制約あり):
→ DenseNet-121(ResNet-50の1/3のパラメータ)
🎯 最高精度(リソース潤沢):
→ ResNeXt-101、DenseNet-201、EfficientNet-B7
🏥 医療画像・細かい特徴が重要:
→ DenseNet(特徴の再利用で細かい特徴も保持)
📝 練習問題
問題1:DenseNetとResNetの比較(基礎)
DenseNetとResNetの主な違いを、以下の観点から説明してください。
- 接続方法(AdditionとConcatenation)の違い
- 特徴の流れ方の違い
- パラメータ効率の違い
解答:
1. 接続方法の違い
ResNet:Addition(加算)H(x) = F(x) + x
→ 特徴が「混ざる」。チャンネル数は変わらない。
DenseNet:Concatenation(連結)H(x) = [x, F(x)]
→ 特徴が「両方残る」。チャンネル数が増える。
2. 特徴の流れ方の違い
ResNet:各層は直前の層の出力(または2層前のSkip Connection)のみを受け取る。
DenseNet:各層は前のすべての層の出力を受け取る。Layer 4は、Layer 0, 1, 2, 3の出力すべてにアクセスできる。
3. パラメータ効率の違い
ResNet-50:25.6M パラメータ
DenseNet-121:8.0M パラメータ(ResNet-50の約1/3)
DenseNetは特徴の再利用により、少ないパラメータで同等以上の精度を達成できる。各層は少ないチャンネル(Growth Rate = 32程度)を出力するだけで済む。
問題2:Inception Moduleの実装と計算(中級)
192チャンネルの入力に対して、以下の構成のInception Moduleを実装し、出力チャンネル数と計算量を計算してください。
- 1×1 branch: 64チャンネル出力
- 3×3 branch: 1×1で96チャンネルに削減 → 3×3で128チャンネル出力
- 5×5 branch: 1×1で16チャンネルに削減 → 5×5で32チャンネル出力
- pool branch: 3×3 max pool → 1×1で32チャンネル出力
解答:
出力チャンネル数:
64 + 128 + 32 + 32 = 256チャンネル
パラメータ数の計算:
■ 1×1ブランチ
1×1×192×64 = 12,288
■ 3×3ブランチ
1×1×192×96 = 18,432
3×3×96×128 = 110,592
小計 = 129,024
■ 5×5ブランチ
1×1×192×16 = 3,072
5×5×16×32 = 12,800
小計 = 15,872
■ poolブランチ
1×1×192×32 = 6,144
合計 = 12,288 + 129,024 + 15,872 + 6,144 = 163,328
実装コード:
class InceptionModule(nn.Module):
def __init__(self):
super(InceptionModule, self).__init__()
# 1×1ブランチ
self.branch1 = nn.Sequential(
nn.Conv2d(192, 64, kernel_size=1),
nn.ReLU(inplace=True)
)
# 3×3ブランチ(1×1で次元削減)
self.branch2 = nn.Sequential(
nn.Conv2d(192, 96, kernel_size=1),
nn.ReLU(inplace=True),
nn.Conv2d(96, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True)
)
# 5×5ブランチ(1×1で次元削減)
self.branch3 = nn.Sequential(
nn.Conv2d(192, 16, kernel_size=1),
nn.ReLU(inplace=True),
nn.Conv2d(16, 32, kernel_size=5, padding=2),
nn.ReLU(inplace=True)
)
# poolブランチ
self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
nn.Conv2d(192, 32, kernel_size=1),
nn.ReLU(inplace=True)
)
def forward(self, x):
return torch.cat([
self.branch1(x), self.branch2(x),
self.branch3(x), self.branch4(x)
], dim=1)
問題3:Growth Rateの計算(応用)
DenseNet-121のDense Block 1(6層)について、以下を計算してください。
- 初期チャンネル数: 64
- Growth Rate: 32
- Dense Block 1の出力チャンネル数は?
- Transition Layer 1で半分に削減した後のチャンネル数は?
解答:
Dense Block 1の出力チャンネル数:
初期チャンネル数 + 層数 × Growth Rate
= 64 + 6 × 32
= 64 + 192
= 256チャンネル
【詳細】
入力: 64ch
層1後: 64 + 32 = 96ch
層2後: 96 + 32 = 128ch
層3後: 128 + 32 = 160ch
層4後: 160 + 32 = 192ch
層5後: 192 + 32 = 224ch
層6後: 224 + 32 = 256ch
Transition Layer 1後のチャンネル数:
256 ÷ 2 = 128チャンネル
※ Compression Factor θ = 0.5 なので半分に削減
問題4:モデルの選択(応用)
以下のシナリオでは、ResNet-50、DenseNet-121、Inception v3、ResNeXt-50のどれを選ぶべきですか?理由とともに答えてください。
- スマートフォンでリアルタイム画像認識(推論速度重視)
- 医療画像診断(精度最優先、細かい特徴が重要)
- 組み込みデバイス(メモリ8GBの制約)
- Kaggleコンペティション(精度最大化)
解答:
1. スマートフォン(推論速度重視)
推奨:ResNet-50
理由:推論速度が速く、精度とのバランスが良い。DenseNetは連結操作が多くメモリアクセスが多いため、モバイルではやや遅い。ただし、より軽量なMobileNetが最適(次のSTEPで学習)。
2. 医療画像診断(精度重視、細かい特徴)
推奨:DenseNet-121 または DenseNet-201
理由:Dense Connectionにより、初期層の細かい特徴が最終層まで保持される。医療画像では微細な病変を見逃さないことが重要なため、特徴の再利用が有効。
3. 組み込みデバイス(メモリ制約)
推奨:DenseNet-121
理由:パラメータ数が8.0Mと最も少ない(ResNet-50は25.6M)。ただし、DenseNetは中間特徴マップのメモリ使用量が多い点に注意。メモリが厳しい場合はMobileNetやEfficientNet-B0を検討。
4. Kaggleコンペティション(精度最大化)
推奨:ResNeXt-50 または アンサンブル
理由:ResNeXt-50は同程度のパラメータ数でResNet-50より高精度。さらに、複数のモデル(ResNeXt + DenseNet + EfficientNet)のアンサンブルで精度を最大化するのがKaggleの定石。
📝 STEP 6 のまとめ
✅ このステップで学んだこと
1. DenseNet
・Dense Connection:各層が前の全層の出力を受け取る
・Growth Rate:各層が出力するチャンネル数
・Transition Layer:チャンネル数と空間サイズを圧縮
・パラメータ効率が非常に良い(ResNet-50の1/3)
2. Inception
・マルチスケール:1×1, 3×3, 5×5のフィルタを並列適用
・1×1畳み込み:次元削減、非線形性追加、クロスチャンネル学習
・入力サイズ:Inception v3は299×299が必要
3. ResNeXt
・Cardinality:並列経路の数という新しい次元
・同じ計算量なら、チャンネルを広げるより経路を増やす方が効果的
4. 使い分け
・速度重視:ResNet-50
・パラメータ効率:DenseNet-121
・精度重視:ResNeXt-50
💡 重要ポイント
ResNet以降も、様々な設計思想のCNNアーキテクチャが登場しています。それぞれに長所と短所があり、タスクや制約に応じて使い分けることが重要です。
次のSTEP 7では、EfficientNetとMobileNetを学びます。これらはモバイル・エッジデバイス向けの軽量モデルで、実務で非常によく使われます。