畳み込み層の仕組み
田中VPoE:「いよいよ画像処理の本丸、CNN(畳み込みニューラルネットワーク)に入ろう。NetShop の商品画像を自動分類するには、画像の空間的な特徴を捉える必要がある。全結合層だけでは画像のどこに何があるかという位置情報が失われてしまう。」
あなた:「全結合層だと画像を1次元に展開する必要がありますもんね。ピクセル間の位置関係が壊れてしまう。」
田中VPoE:「その通り。畳み込み層はフィルタを画像上でスライドさせて、局所的なパターンを検出する。エッジやテクスチャなど、画像認識の基本的な特徴を自動で学習してくれるんだ。」
畳み込み演算の仕組み
畳み込み(Convolution)は、小さなフィルタ(カーネル)を入力画像上でスライドさせながら、フィルタと画像の対応領域の内積を計算する操作です。
入力画像 (5x5) フィルタ (3x3) 出力(特徴マップ)(3x3)
┌─────────────┐ ┌───────┐
│ 1 0 1 0 1 │ │ 1 0 1 │ ┌───────┐
│ 0 1 0 1 0 │ * │ 0 1 0 │ = │ 4 3 4 │
│ 1 0 1 0 1 │ │ 1 0 1 │ │ 2 4 2 │
│ 0 1 0 1 0 │ └───────┘ │ 4 3 4 │
│ 1 0 1 0 1 │ └───────┘
└─────────────┘
PyTorch での畳み込み層
import torch
import torch.nn as nn
# 2D 畳み込み層
conv = nn.Conv2d(
in_channels=3, # 入力チャンネル数(RGB=3)
out_channels=16, # 出力チャンネル数(フィルタの数)
kernel_size=3, # フィルタサイズ(3x3)
stride=1, # スライド幅
padding=1 # パディング
)
# 入力: (バッチサイズ, チャンネル, 高さ, 幅)
x = torch.randn(1, 3, 32, 32) # 32x32 の RGB 画像
output = conv(x)
print(f"入力: {x.shape}")
print(f"出力: {output.shape}") # (1, 16, 32, 32)
ストライドとパディング
ストライド(Stride)
フィルタをスライドさせる間隔です。
stride=1: フィルタを1ピクセルずつ移動(デフォルト)
stride=2: フィルタを2ピクセルずつ移動(出力サイズが半分に)
パディング(Padding)
入力画像の周囲にピクセルを追加して、出力サイズを制御します。
| パディング | 出力サイズ | 用途 |
|---|---|---|
| padding=0 | 入力より小さくなる | サイズ削減が目的 |
| padding=‘same’ | 入力と同じ | サイズを維持したい場合 |
| padding=k//2 | 入力と同じ(kernel_size=k) | 手動で同サイズに |
出力サイズの計算式
出力サイズ = (入力サイズ - カーネルサイズ + 2 * パディング) / ストライド + 1
# 例: 入力 32x32, カーネル 3x3, stride=1, padding=1
# 出力 = (32 - 3 + 2*1) / 1 + 1 = 32
# 例: 入力 32x32, カーネル 3x3, stride=2, padding=1
# 出力 = (32 - 3 + 2*1) / 2 + 1 = 16
特徴マップ
畳み込み層の出力は「特徴マップ」と呼ばれます。各フィルタが異なるパターンを検出します。
フィルタ1: 横方向のエッジ → 特徴マップ1
フィルタ2: 縦方向のエッジ → 特徴マップ2
フィルタ3: 斜めのエッジ → 特徴マップ3
...
層が深くなるほど抽象的な特徴を学習
浅い層: エッジ、コーナー(低レベル特徴)
中間層: テクスチャ、パーツ(中レベル特徴)
深い層: 物体全体のパターン(高レベル特徴)
プーリング層
プーリング層は、特徴マップの空間サイズを縮小し、計算量を削減しつつ位置の微小なズレに対するロバスト性を高めます。
# Max Pooling: 各領域の最大値を取る
pool = nn.MaxPool2d(kernel_size=2, stride=2)
x = torch.randn(1, 16, 32, 32)
output = pool(x)
print(f"入力: {x.shape}") # (1, 16, 32, 32)
print(f"出力: {output.shape}") # (1, 16, 16, 16)
# Average Pooling: 各領域の平均値を取る
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
# Global Average Pooling: 特徴マップ全体の平均(分類の前段で使用)
gap = nn.AdaptiveAvgPool2d(1) # 出力サイズを 1x1 に
| プーリング | 特徴 | 主な用途 |
|---|---|---|
| Max Pooling | 最も強い特徴を保持 | CNN の中間層 |
| Average Pooling | 平均的な特徴を保持 | 情報の平滑化 |
| Global Average Pooling | パラメータ数を大幅削減 | 全結合層の代替 |
CNN の基本構造
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
# ブロック1: 3 → 32チャンネル
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2, 2), # 32x32 → 16x16
# ブロック2: 32 → 64チャンネル
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2, 2), # 16x16 → 8x8
# ブロック3: 64 → 128チャンネル
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.AdaptiveAvgPool2d(1), # 8x8 → 1x1
)
self.classifier = nn.Linear(128, num_classes)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1) # Flatten
x = self.classifier(x)
return x
model = SimpleCNN(num_classes=10)
x = torch.randn(4, 3, 32, 32)
output = model(x)
print(f"出力: {output.shape}") # (4, 10)
まとめ
- 畳み込み層はフィルタを使って画像の局所的パターンを検出する
- ストライドは出力サイズの縮小、パディングはサイズの維持に使う
- 特徴マップは各フィルタが検出したパターンを表す
- プーリング層は空間サイズを縮小し、計算量とロバスト性を改善する
- CNN は「畳み込み + 正規化 + 活性化 + プーリング」のブロックを積み重ねる
チェックリスト
- 畳み込み演算の仕組みを説明できる
- ストライドとパディングの役割を理解した
- 出力サイズの計算式を使える
- プーリング層の種類と役割を説明できる
推定読了時間: 30分