STEP 13:セマンティックセグメンテーション

🎨 STEP 13: セマンティックセグメンテーション

FCN、U-Net、DeepLab v3+の仕組み、
Encoder-Decoder構造、Skip Connection、Atrous Convolutionを学びます

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

  • セグメンテーションの種類(セマンティック、インスタンス、パノプティック)
  • 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など、物体を個別に識別する技術を習得します。

📝

学習メモ

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

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