📋 このステップで学ぶこと
- セグメンテーションの種類(セマンティック、インスタンス、パノプティック)
- FCN(Fully Convolutional Network)の革新
- U-Net(Encoder-Decoder構造、Skip Connection)
- DeepLab v3+(Atrous Convolution、ASPP)
- 評価指標(IoU、mIoU、Pixel Accuracy、Dice係数)
- 損失関数(Cross Entropy、Dice Loss)
- 実装:U-Netでのセグメンテーション
🎨 1. セグメンテーションとは
セグメンテーションは、画像の各ピクセルにクラスラベルを割り当てるタスクです。物体検出がBounding Boxで物体の位置を特定するのに対し、セグメンテーションはピクセル単位で物体の形状を正確に捉えます。
1-1. 画像分類 vs 物体検出 vs セグメンテーション
【3つのタスクの違い】
■ 画像分類(Image Classification)
入力: 画像全体
出力: 1つのクラスラベル
例: 「この画像は猫です」
┌─────────────┐
│ 🐱 │ → 出力: “cat”
│ │
└─────────────┘
─────────────────────────────────────────────────────
■ 物体検出(Object Detection)
入力: 画像
出力: [クラス + Bounding Box] × 複数
例: 「猫が(50,30,200,150)に、犬が(300,100,450,250)にいます」
┌─────────────┐
│ ┌───┐ │ → [(cat, box1), (dog, box2)]
│ │🐱│ ┌───┐ │
│ └───┘ │🐶│ │
│ └───┘ │
└─────────────┘
※物体の位置は分かるが、形状は分からない
─────────────────────────────────────────────────────
■ セグメンテーション(Segmentation)
入力: 画像
出力: ピクセル単位のクラスラベルマップ
例: 各ピクセルが猫/犬/背景のどれか
元画像 セグメンテーション結果
┌─────────────┐ ┌─────────────┐
│ 🐱 🐶 │ → │ 🟩🟥🟥🟦🟦🟩│
│ │ │ 🟩🟥🟥🟦🟦🟩│
└─────────────┘ └─────────────┘
赤=猫、青=犬、緑=背景
※物体の正確な形状が分かる
※ピクセルごとにクラスを予測
1-2. セグメンテーションの3つの種類
💡 セグメンテーションの3種類
1. セマンティックセグメンテーション
→ 「何クラスか」を予測(同じクラスは区別しない)
2. インスタンスセグメンテーション
→ 「何クラスか」+「どの個体か」を予測
3. パノプティックセグメンテーション
→ セマンティック+インスタンスの統合
【3種類の違い:具体例】
元画像: 2匹の猫と空(背景)
┌───────────────┐
│ 🐱 🐱 │
│ (猫A) (猫B) │
│ │
│ (空) │
└───────────────┘
─────────────────────────────────────────────────────
■ セマンティックセグメンテーション
出力: 各ピクセルのクラス
┌───────────────┐
│ 猫 猫 猫 猫 猫│ ← 猫Aと猫Bを区別しない
│ │ 同じ「猫」クラス
│ 空 空 空 空 空│
└───────────────┘
特徴:
・クラスの種類は分かる
・個体の区別はしない
・背景もクラスとして扱う
─────────────────────────────────────────────────────
■ インスタンスセグメンテーション
出力: 各物体のマスク
┌───────────────┐
│ 猫1猫1 猫2猫2│ ← 猫Aと猫Bを区別
│ │ 個別のインスタンス
│ (背景は無視) │
└───────────────┘
特徴:
・物体を個体ごとに識別
・同じクラスでも区別
・背景はラベル付けしない(無視)
─────────────────────────────────────────────────────
■ パノプティックセグメンテーション
出力: 全ピクセルのラベル(個体区別あり)
┌───────────────┐
│ 猫1猫1 猫2猫2│ ← 猫を個体区別
│ │
│ 空 空 空 空 空│ ← 背景もラベル付け
└───────────────┘
特徴:
・セマンティック + インスタンスの統合
・全ピクセルをカバー
・物体も背景も完全にラベル付け
| 種類 |
出力 |
個体区別 |
代表的なモデル |
| セマンティック |
クラスマップ |
なし |
FCN, U-Net, DeepLab |
| インスタンス |
物体ごとのマスク |
あり |
Mask R-CNN, YOLACT |
| パノプティック |
全ピクセル+個体 |
あり |
Panoptic FPN, MaskFormer |
1-3. セグメンテーションの応用分野
| 分野 |
用途 |
種類 |
具体例 |
| 自動運転 |
道路、車線、歩行者認識 |
セマンティック |
Tesla、Waymo |
| 医療画像 |
腫瘍、臓器の輪郭抽出 |
セマンティック |
CT/MRI解析 |
| 衛星画像 |
土地利用分類、建物検出 |
セマンティック |
都市計画、災害対応 |
| ロボティクス |
物体把持、障害物回避 |
インスタンス |
ピッキングロボット |
| 画像編集 |
背景除去、合成 |
セマンティック |
Photoshop、スマホアプリ |
🌐 2. FCN(Fully Convolutional Network)
FCN(2015年)は、セマンティックセグメンテーションに深層学習を適用した最初のモデルです。「全結合層を使わない」という革新的なアイデアで、任意サイズの画像に対応できます。
2-1. 従来の画像分類ネットワークの問題
【従来の画像分類ネットワーク(VGG、AlexNet)】
入力画像(224×224×3)
↓
┌─────────────────────────────────────────────────────┐
│ 畳み込み層(特徴抽出) │
│ │
│ Conv → Pool → Conv → Pool → … │
│ │
│ 224×224 → 112×112 → 56×56 → … → 7×7 │
│ ×3 ×64 ×128 ×512 │
└─────────────────────────────────────────────────────┘
↓
7×7×512 = 25,088個の特徴
↓
┌─────────────────────────────────────────────────────┐
│ 全結合層(Fully Connected) │
│ │
│ Flatten: 7×7×512 → 25,088次元ベクトル │
│ ↓ │
│ FC1: 25,088 → 4,096 │
│ ↓ │
│ FC2: 4,096 → 4,096 │
│ ↓ │
│ FC3: 4,096 → 1,000(クラス数) │
└─────────────────────────────────────────────────────┘
↓
出力: 1,000クラスの確率
─────────────────────────────────────────────────────
【問題点】
1. 空間情報の喪失
Flatten: 7×7の空間構造 → 1次元ベクトル
→ 「どこに何があるか」の情報が失われる
→ ピクセル単位の予測ができない
2. 固定サイズの入力しか受け付けない
全結合層は入力次元が固定
→ 224×224以外のサイズは処理できない
3. パラメータ数が膨大
FC1: 25,088 × 4,096 = 1億パラメータ
→ メモリ消費が大きい
2-2. FCNの革新的アイデア
💡 FCNの核心アイデア
「全結合層を1×1畳み込みに置き換える」
全結合層: 空間情報を捨てて1次元化
1×1畳み込み: 空間情報を保持したまま次元変換
【FCNの構造】
入力画像(任意サイズ H×W×3)
↓
┌─────────────────────────────────────────────────────┐
│ 畳み込み層(VGGベース) │
│ │
│ Conv → Pool → Conv → Pool → … │
│ │
│ H×W → H/2×W/2 → H/4×W/4 → … → H/32×W/32 │
│ ×3 ×64 ×128 ×512 │
└─────────────────────────────────────────────────────┘
↓
特徴マップ: H/32 × W/32 × 512
↓
┌─────────────────────────────────────────────────────┐
│ 1×1畳み込み(全結合層の代わり) │
│ │
│ 1×1 Conv: 512 → 4096 │
│ 1×1 Conv: 4096 → 4096 │
│ 1×1 Conv: 4096 → num_classes │
│ │
│ ※空間情報(H/32×W/32)を保持! │
└─────────────────────────────────────────────────────┘
↓
ヒートマップ: H/32 × W/32 × num_classes
↓
┌─────────────────────────────────────────────────────┐
│ アップサンプリング(元のサイズに戻す) │
│ │
│ H/32 × W/32 → H × W │
│ │
│ 方法: Transposed Convolution(逆畳み込み) │
│ または Bilinear Interpolation(双線形補間)│
└─────────────────────────────────────────────────────┘
↓
出力: H × W × num_classes(ピクセル単位の予測)
─────────────────────────────────────────────────────
【FCNの利点】
1. 空間情報を保持
1×1畳み込みは空間構造を壊さない
→ ピクセル単位の予測が可能
2. 任意サイズの入力に対応
畳み込みはサイズに依存しない
→ 様々なサイズの画像を処理可能
3. パラメータ効率
1×1畳み込みは全結合層より少ないパラメータ
2-3. FCNの3つのバージョン
【FCN-32s、FCN-16s、FCN-8s】
■ FCN-32s(最もシンプル)
最終層(pool5)から直接32倍アップサンプリング
pool5 (H/32)
↓
32倍アップサンプリング
↓
出力 (H)
問題: 解像度が低く、境界がぼやける
─────────────────────────────────────────────────────
■ FCN-16s(1段階のSkip)
pool5 + pool4を結合
pool5 (H/32) → 2倍アップ → (H/16)
↓
pool4 (H/16) ──────────────→ 結合
↓
16倍アップサンプリング
↓
出力 (H)
効果: pool4の細かい特徴を活用、境界が少し鮮明に
─────────────────────────────────────────────────────
■ FCN-8s(2段階のSkip)
pool5 + pool4 + pool3を結合
pool5 (H/32) → 2倍アップ → (H/16)
↓
pool4 (H/16) ──────────────→ 結合 → 2倍アップ → (H/8)
↓
pool3 (H/8) ─────────────────────────────────────→ 結合
↓
8倍アップサンプリング
↓
出力 (H)
効果: 浅い層の詳細な特徴も活用、境界が最も鮮明
─────────────────────────────────────────────────────
【性能比較(PASCAL VOC 2011)】
FCN-32s: 59.4% mIoU
FCN-16s: 62.4% mIoU (+3.0%)
FCN-8s: 62.7% mIoU (+0.3%)
→ Skip Connectionで精度向上!
⚠️ FCNの限界
1. 境界が不鮮明
32倍のダウンサンプリングで情報が大幅に失われる。アップサンプリングで完全には復元できない。
2. 小さい物体が苦手
ダウンサンプリングで小さい物体が消失する可能性がある。
3. 文脈情報が限定的
局所的な特徴のみ使用。画像全体の文脈を十分に活用できない。
🏗️ 3. U-Net(2015年)
U-Netは、医療画像セグメンテーションのために開発されました。Encoder-Decoder構造とSkip Connectionにより、少量のデータでも高精度を実現します。
3-1. U-Netの構造(U字型)
【U-Netのアーキテクチャ】
Encoder Decoder
(収縮パス) (拡張パス)
入力 (572×572) ↓ ↑ 出力 (388×388)
│ ┌───────┐ ┌───────┐ │
└────────→│Conv×2 │──Skip──────│Conv×2 │────────→┘
│64ch │ Connection │64ch │
└───┬───┘ └───┬───┘
│Pool UpConv│
┌───┴───┐ ┌───┴───┐
│Conv×2 │──Skip──────│Conv×2 │
│128ch │ Connection │128ch │
└───┬───┘ └───┬───┘
│Pool UpConv│
┌───┴───┐ ┌───┴───┐
│Conv×2 │──Skip──────│Conv×2 │
│256ch │ Connection │256ch │
└───┬───┘ └───┬───┘
│Pool UpConv│
┌───┴───┐ ┌───┴───┐
│Conv×2 │──Skip──────│Conv×2 │
│512ch │ Connection │512ch │
└───┬───┘ └───┬───┘
│Pool UpConv│
┌───┴───────────────────┴───┐
│ Bottleneck │
│ 1024ch │
└───────────────────────────┘
特徴:
1. 対称的なU字型構造
2. Encoder: ダウンサンプリングで特徴抽出
3. Decoder: アップサンプリングで解像度復元
4. Skip Connection: Encoderの特徴をDecoderに直接結合
3-2. Encoderの役割
【Encoder(収縮パス)】
役割: 画像から意味的な特徴を抽出
入力画像 (572×572×3)
↓
┌─────────────────────────────────────────────────────┐
│ Block 1 │
│ 3×3 Conv(64) → ReLU → 3×3 Conv(64) → ReLU │
│ 出力: 570×570×64(保存 → Skip Connection) │
│ ↓ MaxPool 2×2 │
│ 285×285×64 │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Block 2 │
│ 3×3 Conv(128) → ReLU → 3×3 Conv(128) → ReLU │
│ 出力: 282×282×128(保存 → Skip Connection) │
│ ↓ MaxPool 2×2 │
│ 141×141×128 │
└─────────────────────────────────────────────────────┘
↓
… (Block 3, 4 も同様)
↓
┌─────────────────────────────────────────────────────┐
│ Bottleneck │
│ 3×3 Conv(1024) → ReLU → 3×3 Conv(1024) → ReLU │
│ 出力: 28×28×1024 │
│ → 最も抽象的な特徴を持つ │
└─────────────────────────────────────────────────────┘
各段階で:
・解像度が半分に(ダウンサンプリング)
・チャンネル数が2倍に
・より抽象的な特徴を学習
3-3. Decoderの役割
【Decoder(拡張パス)】
役割: 解像度を復元し、ピクセル単位の予測を行う
Bottleneck (28×28×1024)
↓
┌─────────────────────────────────────────────────────┐
│ Up Block 1 │
│ 2×2 UpConv(512): 28×28 → 56×56×512 │
│ ↓ │
│ Concatenate with Encoder Block 4 (56×56×512) │
│ → 56×56×1024(Skip Connection!) │
│ ↓ │
│ 3×3 Conv(512) → ReLU → 3×3 Conv(512) → ReLU │
│ 出力: 52×52×512 │
└─────────────────────────────────────────────────────┘
↓
… (Up Block 2, 3, 4 も同様)
↓
┌─────────────────────────────────────────────────────┐
│ 最終出力 │
│ 1×1 Conv: 64 → num_classes │
│ 出力: 388×388×num_classes │
└─────────────────────────────────────────────────────┘
各段階で:
・解像度が2倍に(アップサンプリング)
・Skip Connectionで細かい特徴を復元
・より詳細な予測が可能に
3-4. Skip Connectionの重要性
💡 Skip Connectionはなぜ重要か
問題(Skip Connectionなし):
Encoderでダウンサンプリング → 細かい情報が失われる
Decoderでアップサンプリング → ぼやけた結果
解決(Skip Connectionあり):
Encoderの各段階の特徴を保存
Decoderの対応する段階に直接結合
→ 細かい境界情報を保持!
【Skip Connectionの効果】
■ Skip Connectionなしの場合
Encoder: 572×572 → 286×286 → … → 28×28
(細かい情報が徐々に失われる)
Decoder: 28×28 → … → 286×286 → 572×572
(失われた情報は復元できない)
結果: ぼやけた境界、細部が不正確
■ Skip Connectionありの場合
Encoder: 572×572の特徴を保存
286×286の特徴を保存
…
Decoder: 保存した特徴と結合して復元
結果: 鮮明な境界、細部も正確
─────────────────────────────────────────────────────
【具体的な処理】
Encoder Block 4 の出力: 56×56×512(細かい特徴あり)
↓ 保存
Decoder Up Block 1:
UpConv: 28×28×1024 → 56×56×512(粗い特徴)
↓
Concatenate(結合):
56×56×512(Encoder、細かい特徴)
56×56×512(Decoder、粗い特徴)
→ 56×56×1024(両方の特徴を持つ)
↓
Conv処理で融合
↓
細かい特徴と粗い特徴の両方を活用!
3-5. U-Netの実装
U-Netの実装を段階的に説明します。
※ コードが横に長い場合は横スクロールできます
import torch
import torch.nn as nn
# ===================================================
# U-Net の実装
# ===================================================
class DoubleConv(nn.Module):
“””
畳み込みブロック: Conv → BN → ReLU → Conv → BN → ReLU
U-Netの基本ブロック
“””
def __init__(self, in_channels, out_channels):
super(DoubleConv, self).__init__()
# Sequential: 複数の層を順番に適用
self.double_conv = nn.Sequential(
# 1つ目の畳み込み
# kernel_size=3: 3×3フィルタ
# padding=1: 出力サイズを維持
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
# Batch Normalization: 学習を安定化
nn.BatchNorm2d(out_channels),
# ReLU活性化関数
# inplace=True: メモリ効率のためその場で計算
nn.ReLU(inplace=True),
# 2つ目の畳み込み
nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
def forward(self, x):
return self.double_conv(x)
class UNet(nn.Module):
“””
U-Net アーキテクチャ
Args:
in_channels: 入力チャンネル数(RGB=3、グレースケール=1)
num_classes: 出力クラス数
features: 最初の畳み込み層のフィルタ数(デフォルト64)
“””
def __init__(self, in_channels=3, num_classes=2, features=[64, 128, 256, 512]):
super(UNet, self).__init__()
# ダウンサンプリング用のモジュールリスト
self.downs = nn.ModuleList()
# アップサンプリング用のモジュールリスト
self.ups = nn.ModuleList()
# MaxPooling: 解像度を半分に
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
# ===================================
# Encoder(ダウンサンプリング)部分
# ===================================
for feature in features:
# 各段階でDoubleConvを追加
self.downs.append(DoubleConv(in_channels, feature))
in_channels = feature
# ===================================
# Bottleneck(最も深い層)
# ===================================
# 最後の特徴数の2倍(512→1024)
self.bottleneck = DoubleConv(features[-1], features[-1] * 2)
# ===================================
# Decoder(アップサンプリング)部分
# ===================================
# 逆順に処理
for feature in reversed(features):
# アップサンプリング: 解像度を2倍に
# ConvTranspose2d: 転置畳み込み(逆畳み込み)
self.ups.append(
nn.ConvTranspose2d(
feature * 2, # 入力チャンネル(Skip Connectionで2倍)
feature, # 出力チャンネル
kernel_size=2,
stride=2
)
)
# Skip Connectionで結合後のDoubleConv
# feature * 2: アップサンプリング後 + Encoderからの特徴
self.ups.append(DoubleConv(feature * 2, feature))
# ===================================
# 最終出力層
# ===================================
# 1×1畳み込みでクラス数に変換
self.final_conv = nn.Conv2d(features[0], num_classes, kernel_size=1)
def forward(self, x):
# Skip Connection用の特徴を保存するリスト
skip_connections = []
# ===================================
# Encoder(ダウンサンプリング)
# ===================================
for down in self.downs:
x = down(x)
# Skip Connection用に保存
skip_connections.append(x)
# プーリングでダウンサンプリング
x = self.pool(x)
# ===================================
# Bottleneck
# ===================================
x = self.bottleneck(x)
# Skip Connectionは逆順で使用
skip_connections = skip_connections[::-1]
# ===================================
# Decoder(アップサンプリング)
# ===================================
# ups は [UpConv, DoubleConv, UpConv, DoubleConv, …] の順
for idx in range(0, len(self.ups), 2):
# アップサンプリング
x = self.ups[idx](x)
# Skip Connectionから特徴を取得
skip = skip_connections[idx // 2]
# サイズが異なる場合は調整(paddingの影響)
if x.shape != skip.shape:
x = nn.functional.interpolate(
x, size=skip.shape[2:], mode=’bilinear’, align_corners=True
)
# Concatenate(チャンネル方向に結合)
# dim=1: チャンネル次元で結合
x = torch.cat([skip, x], dim=1)
# DoubleConvで処理
x = self.ups[idx + 1](x)
# ===================================
# 最終出力
# ===================================
return self.final_conv(x)
使用例:
# モデルの作成
model = UNet(in_channels=3, num_classes=2)
# テスト用の入力(バッチサイズ1、RGB、256×256)
x = torch.randn(1, 3, 256, 256)
# 順伝播
output = model(x)
print(f”入力サイズ: {x.shape}”)
print(f”出力サイズ: {output.shape}”)
実行結果:
入力サイズ: torch.Size([1, 3, 256, 256])
出力サイズ: torch.Size([1, 2, 256, 256])
🎯 U-Netの特徴まとめ
1. 対称的なU字型構造:
Encoder(特徴抽出)とDecoder(解像度復元)が対称
2. Skip Connection:
Encoderの特徴をDecoderに直接結合 → 境界が鮮明
3. 少量データで高精度:
医療画像で数百枚のデータでも学習可能
4. 応用範囲が広い:
医療、衛星画像、一般画像すべてに有効
🌊 4. DeepLab v3+(2018年)
DeepLab v3+は、Atrous Convolution(拡張畳み込み)で受容野を効率的に拡大し、ASPP(Atrous Spatial Pyramid Pooling)でマルチスケールの文脈情報を活用します。
4-1. Atrous Convolution(拡張畳み込み)
💡 Atrous Convolutionの核心アイデア
「フィルタの間隔を空けて、パラメータ数を増やさずに受容野を拡大する」
Atrous = フランス語で「穴」の意味
Dilated Convolution(拡張畳み込み)とも呼ばれる
【通常の3×3畳み込み vs Atrous Convolution】
■ 通常の3×3畳み込み(rate=1)
フィルタが連続した9ピクセルを見る:
入力画像:
[1][2][3][4][5]
[6][7][8][9][10]
[11][12][13][14][15]
[16][17][18][19][20]
[21][22][23][24][25]
3×3フィルタ:
[×][×][×]
[×][×][×]
[×][×][×]
→ 見る範囲(受容野): 3×3 = 9ピクセル
─────────────────────────────────────────────────────
■ Atrous Convolution(rate=2)
フィルタの間隔を2に(1ピクセル空ける):
入力画像:
[1] ・ [3] ・ [5]
・ ・ ・ ・ ・
[11] ・ [13] ・ [15]
・ ・ ・ ・ ・
[21] ・ [23] ・ [25]
実際に見るピクセル:
[×] ・ [×] ・ [×]
・ ・ ・ ・ ・
[×] ・ [×] ・ [×]
・ ・ ・ ・ ・
[×] ・ [×] ・ [×]
→ 見る範囲(受容野): 5×5 = 25ピクセル
→ でもパラメータ数は3×3のまま!
─────────────────────────────────────────────────────
■ Atrous Convolution(rate=4)
間隔を4に:
[×] ・ ・ ・ [×] ・ ・ ・ [×]
・ ・ ・ ・ ・ ・ ・ ・ ・
・ ・ ・ ・ ・ ・ ・ ・ ・
・ ・ ・ ・ ・ ・ ・ ・ ・
[×] ・ ・ ・ [×] ・ ・ ・ [×]
…
→ 見る範囲(受容野): 9×9 = 81ピクセル
→ パラメータ数は変わらず!
─────────────────────────────────────────────────────
【受容野の計算式】
受容野 = k + (k – 1) × (r – 1)
k: カーネルサイズ(例: 3)
r: dilation rate
例:
・rate=1: 3 + 2×0 = 3
・rate=2: 3 + 2×1 = 5
・rate=4: 3 + 2×3 = 9
・rate=12: 3 + 2×11 = 25
4-2. ASPP(Atrous Spatial Pyramid Pooling)
【ASPPの構造】
入力特徴マップ(H×W×C)
│
├──────────────────────────────────────────┐
│ │
↓ │
┌─────────────────────────────────────────┐ │
│ 並列に5つのブランチで処理 │ │
│ │ │
│ ① 1×1 Conv │ │
│ → 局所的な情報 │ │
│ │ │
│ ② 3×3 Atrous Conv (rate=6) │ │
│ → やや広い範囲 │ │
│ │ │
│ ③ 3×3 Atrous Conv (rate=12) │ │
│ → 広い範囲 │ │
│ │ │
│ ④ 3×3 Atrous Conv (rate=18) │ │
│ → 非常に広い範囲 │ │
│ │ │
│ ⑤ Global Average Pooling + 1×1 Conv │ │
│ → 画像全体の情報 │ │
└─────────────────────────────────────────┘ │
│ │
↓ │
Concatenate(チャンネル方向に結合) │
│ │
↓ │
1×1 Conv(チャンネル数を削減) │
│ │
↓ │
出力特徴マップ │
─────────────────────────────────────────────────────
【各ブランチの役割】
① 1×1 Conv:
最も局所的な特徴(点)
② rate=6:
受容野 = 3 + 2×5 = 13ピクセル
小さい物体の文脈
③ rate=12:
受容野 = 3 + 2×11 = 25ピクセル
中程度の物体の文脈
④ rate=18:
受容野 = 3 + 2×17 = 37ピクセル
大きい物体の文脈
⑤ Global Average Pooling:
画像全体の文脈
→ 1×1に圧縮してからアップサンプリング
─────────────────────────────────────────────────────
【ASPPの効果】
・複数のスケールの情報を同時に抽出
・小さい物体から大きい物体まで対応
・画像全体の文脈も活用
→ より正確なセグメンテーション!
4-3. DeepLab v3+のアーキテクチャ
【DeepLab v3+の全体構造】
入力画像(H×W×3)
│
↓
┌─────────────────────────────────────────────────────┐
│ バックボーン(ResNet-101 or MobileNet-v2) │
│ │
│ 特徴抽出 │
│ ├─ 低レベル特徴(浅い層): H/4 × W/4 │
│ │ → 細かいエッジ、テクスチャ │
│ │ → Skip Connectionで保存 │
│ │ │
│ └─ 高レベル特徴(深い層): H/16 × W/16 │
│ → 意味的な特徴 │
└─────────────────────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────────┐
│ ASPP(Atrous Spatial Pyramid Pooling) │
│ │
│ マルチスケールの文脈情報を抽出 │
│ 出力: H/16 × W/16 × 256 │
└─────────────────────────────────────────────────────┘
│
↓
4倍アップサンプリング
│
↓
┌─────────────────────────────────────────────────────┐
│ Decoder │
│ │
│ ① 低レベル特徴を1×1 Convで48chに削減 │
│ ② ASPP出力と結合(Concatenate) │
│ ③ 3×3 Conv × 2 │
│ ④ 4倍アップサンプリング │
└─────────────────────────────────────────────────────┘
│
↓
出力(H×W×num_classes)
─────────────────────────────────────────────────────
【DeepLab v3 vs DeepLab v3+】
DeepLab v3:
バックボーン → ASPP → 16倍アップサンプリング
→ シンプルだが境界がぼやけやすい
DeepLab v3+(改良):
バックボーン → ASPP → Decoder(低レベル特徴と結合)
→ U-Netのような構造で境界が鮮明に
| モデル |
バックボーン |
mIoU |
特徴 |
| FCN-8s |
VGG-16 |
62.7% |
初の深層学習セグメンテーション |
| U-Net |
独自 |
〜75% |
医療画像で人気、少量データ |
| DeepLab v3 |
ResNet-101 |
79.3% |
ASPP導入 |
| DeepLab v3+ |
ResNet-101 |
82.1% |
Decoder追加、境界鮮明 |
📊 5. 評価指標
5-1. IoU(Intersection over Union)
【セグメンテーションのIoU】
IoU = 交差部分(Intersection)/ 和集合(Union)
予測マスク 正解マスク
┌─────────┐ ┌─────────┐
│ A │ │ B │
│ ┌────┼────┼────┐ │
│ │ A∩B│ │ │ │
└────┼────┘ └────┼────┘
└──────────────┘
IoU = |A ∩ B| / |A ∪ B|
= 交差ピクセル数 / (予測 + 正解 – 交差)
─────────────────────────────────────────────────────
【計算例】
あるクラス(例: 猫)について:
・予測で「猫」とラベル付けされたピクセル: 150個
・正解で「猫」のピクセル: 180個
・予測と正解が重なるピクセル: 120個
IoU = 120 / (150 + 180 – 120)
= 120 / 210
= 0.571
= 57.1%
─────────────────────────────────────────────────────
【IoU値の解釈】
IoU = 100%: 完全一致
IoU ≥ 80%: 非常に良い
IoU ≥ 50%: まあまあ
IoU < 50%: 改善が必要
5-2. mIoU(mean IoU)
【mIoUの計算】
mIoU = 各クラスのIoUの平均
例(4クラス: 背景、道路、車、人):
IoU(背景) = 0.95
IoU(道路) = 0.88
IoU(車) = 0.72
IoU(人) = 0.65
mIoU = (0.95 + 0.88 + 0.72 + 0.65) / 4
= 3.20 / 4
= 0.80
= 80.0%
─────────────────────────────────────────────────────
【注意点】
・背景クラスを含めるかどうかはデータセットによる
・PASCAL VOC: 21クラス(背景含む)
・Cityscapes: 19クラス(背景含まず)
・ADE20K: 150クラス
・クラスが存在しない画像では、そのクラスのIoUは計算から除外
(0にすると不公平なため)
5-3. Pixel Accuracy
【Pixel Accuracy(ピクセル精度)】
Pixel Accuracy = 正しく分類されたピクセル数 / 全ピクセル数
例:
画像サイズ: 640×480 = 307,200ピクセル
正しく分類: 276,480ピクセル
Pixel Accuracy = 276,480 / 307,200
= 0.90
= 90%
─────────────────────────────────────────────────────
【問題点】
背景が多い場合、高い値になりやすい:
例: 画像の95%が背景
背景だけ正解すれば → 95%の精度
でも物体は全く検出できていない!
→ mIoUの方が信頼できる指標
→ Pixel Accuracyは補助的に使用
5-4. Dice係数(F1スコア)
【Dice係数】
Dice = 2 × |A ∩ B| / (|A| + |B|)
= 2 × 交差 / (予測 + 正解)
─────────────────────────────────────────────────────
【IoUとの関係】
Dice = 2 × IoU / (IoU + 1)
IoU = Dice / (2 – Dice)
例:
IoU = 0.571 のとき
Dice = 2 × 0.571 / (0.571 + 1)
= 1.142 / 1.571
= 0.727 = 72.7%
→ Diceの方が常にIoUより高い値
─────────────────────────────────────────────────────
【計算例】
予測ピクセル数: 150
正解ピクセル数: 180
交差ピクセル数: 120
Dice = 2 × 120 / (150 + 180)
= 240 / 330
= 0.727
= 72.7%
(同じ例でIoUは57.1%だった)
─────────────────────────────────────────────────────
【Dice Lossとして使用】
Dice Loss = 1 – Dice
・損失関数として使用可能
・クラス不均衡に強い
・医療画像でよく使われる
| 指標 |
定義 |
特徴 |
| mIoU |
各クラスのIoUの平均 |
最も一般的、標準的な評価指標 |
| Pixel Accuracy |
正しいピクセルの割合 |
シンプルだがクラス不均衡に弱い |
| Dice係数 |
2×交差/(予測+正解) |
医療画像で人気、IoUより高値 |
📉 6. 損失関数
6-1. Cross Entropy Loss
【セグメンテーションのCross Entropy Loss】
各ピクセルで分類問題として損失を計算:
L_CE = -(1/N) × Σ Σ y_c × log(p_c)
N: 全ピクセル数
y_c: 正解ラベル(one-hot)
p_c: 予測確率
─────────────────────────────────────────────────────
【PyTorchでの実装】
criterion = nn.CrossEntropyLoss()
# output: [batch, num_classes, H, W]
# target: [batch, H, W](クラスID)
loss = criterion(output, target)
─────────────────────────────────────────────────────
【クラス重み付け】
クラス不均衡がある場合、重みを付ける:
# 背景が多い場合、背景の重みを下げる
weights = torch.tensor([0.1, 1.0, 1.0]) # [背景, クラス1, クラス2]
criterion = nn.CrossEntropyLoss(weight=weights)
6-2. Dice Loss
import torch
import torch.nn as nn
class DiceLoss(nn.Module):
“””
Dice Loss: 1 – Dice係数
クラス不均衡に強い損失関数
“””
def __init__(self, smooth=1.0):
super(DiceLoss, self).__init__()
# smooth: ゼロ除算を防ぐための小さな値
self.smooth = smooth
def forward(self, pred, target):
“””
Args:
pred: 予測 [batch, num_classes, H, W]
target: 正解 [batch, H, W]
“””
# Softmaxで確率に変換
pred = torch.softmax(pred, dim=1)
# targetをone-hotに変換
num_classes = pred.shape[1]
target_one_hot = torch.zeros_like(pred)
target_one_hot.scatter_(1, target.unsqueeze(1), 1)
# Dice係数を計算
# 各クラスについて計算
intersection = (pred * target_one_hot).sum(dim=(2, 3))
union = pred.sum(dim=(2, 3)) + target_one_hot.sum(dim=(2, 3))
dice = (2.0 * intersection + self.smooth) / (union + self.smooth)
# 全クラスの平均を取り、1から引いて損失に
dice_loss = 1.0 – dice.mean()
return dice_loss
# 使用例
criterion = DiceLoss()
loss = criterion(output, target)
6-3. Combined Loss
class CombinedLoss(nn.Module):
“””
Cross Entropy Loss + Dice Loss の組み合わせ
両方の利点を活用
“””
def __init__(self, ce_weight=1.0, dice_weight=1.0):
super(CombinedLoss, self).__init__()
self.ce_loss = nn.CrossEntropyLoss()
self.dice_loss = DiceLoss()
self.ce_weight = ce_weight
self.dice_weight = dice_weight
def forward(self, pred, target):
ce = self.ce_loss(pred, target)
dice = self.dice_loss(pred, target)
return self.ce_weight * ce + self.dice_weight * dice
# 使用例
criterion = CombinedLoss(ce_weight=1.0, dice_weight=1.0)
loss = criterion(output, target)
💻 7. 実装:U-Netでのセグメンテーション
7-1. データセットの準備
import torch
from torch.utils.data import Dataset, DataLoader
import torchvision.transforms as transforms
from PIL import Image
import numpy as np
import os
class SegmentationDataset(Dataset):
“””
セグメンテーション用データセット
ディレクトリ構造:
dataset/
├── images/
│ ├── train/
│ │ ├── img001.jpg
│ │ └── …
│ └── val/
├── masks/
│ ├── train/
│ │ ├── img001.png (グレースケール、ピクセル値=クラスID)
│ │ └── …
│ └── val/
“””
def __init__(self, image_dir, mask_dir, transform=None, target_size=(256, 256)):
“””
Args:
image_dir: 画像ディレクトリのパス
mask_dir: マスクディレクトリのパス
transform: 画像の変換(Data Augmentation含む)
target_size: リサイズ後のサイズ
“””
self.image_dir = image_dir
self.mask_dir = mask_dir
self.transform = transform
self.target_size = target_size
# 画像ファイルのリストを取得
self.images = sorted(os.listdir(image_dir))
def __len__(self):
return len(self.images)
def __getitem__(self, idx):
# 画像を読み込み
img_name = self.images[idx]
img_path = os.path.join(self.image_dir, img_name)
image = Image.open(img_path).convert(‘RGB’)
# マスクを読み込み(拡張子を変更)
mask_name = img_name.replace(‘.jpg’, ‘.png’).replace(‘.jpeg’, ‘.png’)
mask_path = os.path.join(self.mask_dir, mask_name)
mask = Image.open(mask_path).convert(‘L’) # グレースケール
# リサイズ
image = image.resize(self.target_size, Image.BILINEAR)
mask = mask.resize(self.target_size, Image.NEAREST) # NEARESTでクラスIDを保持
# NumPy配列に変換
image = np.array(image)
mask = np.array(mask)
# Data Augmentation(画像とマスクに同じ変換を適用)
if self.transform:
# Albumentationsを使用する場合
augmented = self.transform(image=image, mask=mask)
image = augmented[‘image’]
mask = augmented[‘mask’]
# テンソルに変換
image = torch.from_numpy(image).permute(2, 0, 1).float() / 255.0
mask = torch.from_numpy(mask).long()
# 正規化
normalize = transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
image = normalize(image)
return image, mask
7-2. 訓練ループ
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
def train_model(model, train_loader, val_loader, num_epochs=50, device=’cuda’):
“””
U-Netの訓練
Args:
model: U-Netモデル
train_loader: 訓練データローダー
val_loader: 検証データローダー
num_epochs: エポック数
device: ‘cuda’ or ‘cpu’
“””
model = model.to(device)
# 損失関数(Cross EntropyとDiceの組み合わせ)
criterion = CombinedLoss(ce_weight=1.0, dice_weight=1.0)
# オプティマイザ
optimizer = optim.Adam(model.parameters(), lr=1e-4)
# 学習率スケジューラ
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode=’min’, factor=0.5, patience=5
)
best_val_iou = 0.0
for epoch in range(num_epochs):
# ===================================
# 訓練フェーズ
# ===================================
model.train()
train_loss = 0.0
for images, masks in train_loader:
images = images.to(device)
masks = masks.to(device)
# 順伝播
outputs = model(images)
loss = criterion(outputs, masks)
# 逆伝播
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
train_loss /= len(train_loader)
# ===================================
# 検証フェーズ
# ===================================
model.eval()
val_loss = 0.0
val_iou = 0.0
with torch.no_grad():
for images, masks in val_loader:
images = images.to(device)
masks = masks.to(device)
outputs = model(images)
loss = criterion(outputs, masks)
val_loss += loss.item()
# IoU計算
preds = torch.argmax(outputs, dim=1)
val_iou += compute_miou(preds, masks, num_classes=2)
val_loss /= len(val_loader)
val_iou /= len(val_loader)
# 学習率の調整
scheduler.step(val_loss)
# ベストモデルの保存
if val_iou > best_val_iou:
best_val_iou = val_iou
torch.save(model.state_dict(), ‘best_unet.pth’)
print(f’Epoch [{epoch+1}/{num_epochs}]’)
print(f’ Train Loss: {train_loss:.4f}’)
print(f’ Val Loss: {val_loss:.4f}, Val mIoU: {val_iou:.4f}’)
return model
def compute_miou(pred, target, num_classes):
“””mIoUを計算”””
ious = []
for cls in range(num_classes):
pred_cls = (pred == cls)
target_cls = (target == cls)
intersection = (pred_cls & target_cls).sum().item()
union = (pred_cls | target_cls).sum().item()
if union > 0:
ious.append(intersection / union)
return np.mean(ious) if ious else 0.0
7-3. 推論と可視化
import matplotlib.pyplot as plt
def predict_and_visualize(model, image_path, device=’cuda’):
“””
画像を入力して予測結果を可視化
“””
model.eval()
model = model.to(device)
# 画像の読み込みと前処理
image = Image.open(image_path).convert(‘RGB’)
original_size = image.size
# リサイズ
image_resized = image.resize((256, 256), Image.BILINEAR)
# テンソルに変換
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
input_tensor = transform(image_resized).unsqueeze(0).to(device)
# 推論
with torch.no_grad():
output = model(input_tensor)
pred = torch.argmax(output, dim=1).squeeze().cpu().numpy()
# 元のサイズにリサイズ
pred_resized = Image.fromarray(pred.astype(np.uint8))
pred_resized = pred_resized.resize(original_size, Image.NEAREST)
pred_resized = np.array(pred_resized)
# 可視化
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# 元画像
axes[0].imshow(image)
axes[0].set_title(‘Original Image’)
axes[0].axis(‘off’)
# 予測マスク
axes[1].imshow(pred_resized, cmap=’jet’)
axes[1].set_title(‘Prediction Mask’)
axes[1].axis(‘off’)
# オーバーレイ
axes[2].imshow(image)
axes[2].imshow(pred_resized, alpha=0.5, cmap=’jet’)
axes[2].set_title(‘Overlay’)
axes[2].axis(‘off’)
plt.tight_layout()
plt.savefig(‘segmentation_result.png’)
plt.show()
return pred_resized
# 使用例
# model = UNet(in_channels=3, num_classes=2)
# model.load_state_dict(torch.load(‘best_unet.pth’))
# result = predict_and_visualize(model, ‘test_image.jpg’)
📝 練習問題
問題1:セグメンテーションの種類(基礎)
セマンティック、インスタンス、パノプティックセグメンテーションの違いを、「2台の車と道路」がある画像を例に説明してください。
解答:
元画像:2台の車(車A、車B)と道路(背景)
セマンティックセグメンテーション:
出力:
・車Aのピクセル → 「車」
・車Bのピクセル → 「車」
・道路のピクセル → 「道路」
特徴:
・車Aと車Bを区別しない(同じ「車」クラス)
・全ピクセルにクラスラベル
インスタンスセグメンテーション:
出力:
・車Aのピクセル → 「車_インスタンス1」
・車Bのピクセル → 「車_インスタンス2」
・道路のピクセル → (ラベルなし)
特徴:
・車Aと車Bを個別に識別
・背景(道路)はラベル付けしない
パノプティックセグメンテーション:
出力:
・車Aのピクセル → 「車_インスタンス1」
・車Bのピクセル → 「車_インスタンス2」
・道路のピクセル → 「道路」
特徴:
・車を個別に識別(インスタンス)
・背景もラベル付け(セマンティック)
・両方の情報を統合
問題2:IoUとDiceの計算(中級)
あるクラスについて、以下の情報が与えられています。IoUとDice係数を計算してください。
予測で「物体」とラベル付けされたピクセル: 200個
正解で「物体」のピクセル: 250個
予測と正解が重なるピクセル: 160個
解答:
与えられた情報:
・予測ピクセル数(A)= 200個
・正解ピクセル数(B)= 250個
・交差ピクセル数(A ∩ B)= 160個
IoUの計算:
IoU = |A ∩ B| / |A ∪ B|
= |A ∩ B| / (|A| + |B| – |A ∩ B|)
= 160 / (200 + 250 – 160)
= 160 / 290
= 0.552
= 55.2%
Dice係数の計算:
Dice = 2 × |A ∩ B| / (|A| + |B|)
= 2 × 160 / (200 + 250)
= 320 / 450
= 0.711
= 71.1%
検証(IoUとDiceの関係):
Dice = 2 × IoU / (IoU + 1)
= 2 × 0.552 / (0.552 + 1)
= 1.104 / 1.552
= 0.711 ✓
問題3:U-Netの構造理解(中級)
U-NetのSkip Connectionがなぜ重要なのか、具体的な例を挙げて説明してください。
解答:
Skip Connectionの役割:
問題(Skip Connectionなし):
Encoderでダウンサンプリング:
256×256 → 128×128 → 64×64 → … → 16×16
この過程で細かい情報が失われる:
・エッジの位置
・テクスチャの詳細
・小さな物体
Decoderでアップサンプリング:
16×16 → 32×32 → … → 256×256
失われた情報は復元できない:
→ 境界がぼやける
→ 小さな物体が消える
解決(Skip Connectionあり):
Encoder 128×128層の特徴:
・エッジ情報を保持
・テクスチャ情報を保持
→ Skip Connectionで保存
Decoder 128×128層:
・アップサンプリングした粗い特徴
・Skip Connectionからの細かい特徴
→ 結合(Concatenate)
→ 両方の情報を活用
結果:
・境界が鮮明
・細部も正確
・小さな物体も検出可能
具体例(医療画像):
腫瘍のセグメンテーション:
Skip Connectionなし:
腫瘍の大まかな位置は分かる
でも境界がぼやけて不正確
→ 手術計画に使えない
Skip Connectionあり:
腫瘍の正確な輪郭を検出
境界が鮮明
→ 手術計画に使用可能
問題4:Atrous Convolutionの理解(応用)
3×3カーネルでrate=6のAtrous Convolutionを使用した場合、受容野のサイズはいくつになりますか?計算式を示して答えてください。
解答:
受容野の計算式:
受容野 = k + (k – 1) × (r – 1)
k: カーネルサイズ
r: dilation rate(拡張率)
計算:
k = 3(3×3カーネル)
r = 6(rate=6)
受容野 = 3 + (3 – 1) × (6 – 1)
= 3 + 2 × 5
= 3 + 10
= 13ピクセル
→ 13×13 = 169ピクセルの範囲を見ることができる
→ パラメータ数は3×3 = 9のまま!
比較:
rate=1(通常の畳み込み): 受容野 = 3×3 = 9ピクセル
rate=6: 受容野 = 13×13 = 169ピクセル
rate=12: 受容野 = 25×25 = 625ピクセル
rate=18: 受容野 = 37×37 = 1369ピクセル
→ rateを上げるだけで、パラメータ数を増やさずに
より広い範囲の情報を活用できる
問題5:mIoUの計算(総合)
3クラス(背景、車、人)のセグメンテーション結果が以下の場合、mIoUを計算してください。
クラス 予測ピクセル 正解ピクセル 交差ピクセル
背景 80,000 75,000 70,000
車 5,000 8,000 4,000
人 2,000 4,000 1,500
解答:
各クラスのIoUを計算:
■ 背景:
IoU = 70,000 / (80,000 + 75,000 – 70,000)
= 70,000 / 85,000
= 0.824 = 82.4%
■ 車:
IoU = 4,000 / (5,000 + 8,000 – 4,000)
= 4,000 / 9,000
= 0.444 = 44.4%
■ 人:
IoU = 1,500 / (2,000 + 4,000 – 1,500)
= 1,500 / 4,500
= 0.333 = 33.3%
mIoUの計算:
mIoU = (IoU_背景 + IoU_車 + IoU_人) / 3
= (0.824 + 0.444 + 0.333) / 3
= 1.601 / 3
= 0.534 = 53.4%
解釈:
・背景(82.4%): 良好
・車(44.4%): 改善の余地あり(予測が少ない)
・人(33.3%): 要改善(予測が少なく、見逃しが多い)
改善策:
・データ拡張で車・人のサンプルを増やす
・クラス重み付けで車・人を重視
・より深いネットワークを使用
📝 STEP 13 のまとめ
✅ このステップで学んだこと
1. セグメンテーションの種類
・セマンティック: クラス分類
・インスタンス: 個体識別
・パノプティック: 両方の統合
2. FCN
・全結合層を1×1畳み込みに置き換え
・任意サイズの入力に対応
・Skip Connectionで精度向上
3. U-Net
・Encoder-Decoder構造
・Skip Connectionで境界鮮明化
・少量データでも高精度
4. DeepLab v3+
・Atrous Convolutionで受容野拡大
・ASPPでマルチスケール情報抽出
・高精度なセグメンテーション
5. 評価指標
・mIoU: 最も一般的
・Dice係数: 医療画像で人気
💡 重要ポイント
セマンティックセグメンテーションは、ピクセル単位でクラスを予測する重要なタスクです。U-NetのSkip Connectionは境界を鮮明にする重要な技術であり、DeepLabのAtrous Convolutionは効率的に受容野を拡大します。
次のSTEP 14では、「インスタンスセグメンテーション」を学びます。Mask R-CNNなど、物体を個別に識別する技術を習得します。