CNN(畳み込みニューラルネットワーク)

2024/10/29に公開

CNNとは

CNN(畳み込みニューラルネットワーク)は,ディープラーニングの一種であり,特に画像や音声などのデータから特徴を自動的に抽出して解析するために用いられます.従来のニューラルネットワークとは異なり,CNNは空間的な情報(例えば画像内のピクセルの位置関係)を活用することができます.これにより,画像認識や物体検出などのタスクで高い精度を実現します.記事の後半にはPythonによる実装例も紹介しています.

CNNは主に以下の層構成をもち,CNNならではの特徴としては「畳み込み層」と「プーリング層」が存在します.

  • 畳み込み層
  • プーリング層
  • 全結合層

畳み込み層

畳み込み層では,「特徴マップ」を生成することを目標に,入力データ全体にフィルタ(カーネル)を作用させます.フィルタの重みと入力データの対応する部分を要素ごとに乗算し,その結果を合計するという操作を入力データ全体に対してスライドさせながらおこいます.

畳み込み演算の数式

S(i, j) = (X * K)(i, j) = \sum_{m}\sum_{n} X(i + m, j + n) \cdot K(m, n)
  • X:入力データ(画像など)
  • K:フィルタ(カーネル)
  • S:出力の特徴マップ
  • (i,j):出力位置
  • (m,n):フィルタ内の位置

「畳み込み演算」に関する用語

  • カーネルサイズ:フィルタの高さと幅
  • フィルタ数:各畳み込み層で使用するフィルタの数
  • ストライド(Stride):フィルタを適用する際の移動ステップの大きさ.ストライドが大きいと出力サイズが小さくなります.
  • パディング(Padding):入力データの周囲にゼロなどの値を追加して,出力サイズを調整します.

畳み込み層の出力サイズ

\text{出力の高さ} = \left\lfloor \frac{\text{入力の高さ} + 2 \times \text{パディング} - \text{カーネルサイズ}}{\text{ストライド}} \right\rfloor + 1
\text{出力の幅} = \left\lfloor \frac{\text{入力の幅} + 2 \times \text{パディング} - \text{カーネルサイズ}}{\text{ストライド}} \right\rfloor + 1

プーリング層

プーリング層は,特徴マップの空間的なサイズを縮小し,(1)計算効率を向上させること,(2)過学習を防ぐこと,を理由に使用されます.この処理自体に数学的な意味があるというよりは,サイズを縮小することで後の作業を効果的にするという印象です.

プーリングの種類

最大値プーリング(Max Pooling):プーリングウィンドウ内の最大値を取得します.

S(i, j) = \max_{(m, n) \in \text{カーネルサイズ}} X(i \times s + m, j \times s + n)

平均値プーリング(Average Pooling):プーリングウィンドウ内の平均値を計算します.

S(i, j) = \frac{1}{|\text{カーネルサイズ}|} \sum_{(m, n) \in \text{カーネルサイズ}} X(i \times s + m, j \times s + n)

CNNの具体的な流れ

  1. 畳み込み層:入力データにフィルタを適用し,初期的な特徴を抽出します.
  2. 活性化関数:非線形性を導入するために,ReLUなどの活性化関数を適用します.
  3. プーリング層:特徴マップの空間的なサイズを縮小し,重要な情報を凝縮します.
  4. これらのステップを繰り返す:必要な深さまで繰り返し,より高次の特徴を学習します.

実装例

以下では,PyTorchを使用してシンプルなCNNを実装し,MNISTデータセットで手書き数字の分類を行う例を示します.

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms

# データの前処理と読み込み
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

# 訓練データとテストデータのダウンロードと準備
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset  = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

train_loader  = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
test_loader   = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=1000, shuffle=False)

# CNNモデルの定義
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # 畳み込み層1:入力チャンネル1、出力チャンネル32、カーネルサイズ3
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
        # 畳み込み層2:入力チャンネル32、出力チャンネル64、カーネルサイズ3
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        # プーリング層:最大値プーリング
        self.pool = nn.MaxPool2d(2, 2)
        # 全結合層:入力特徴数7*7*64、出力特徴数128
        self.fc1 = nn.Linear(7 * 7 * 64, 128)
        # 出力層:入力特徴数128、出力特徴数10(クラス数)
        self.fc2 = nn.Linear(128, 10)
        # 活性化関数:ReLU
        self.relu = nn.ReLU()

    def forward(self, x):
        # 畳み込み層1 + ReLU + プーリング
        x = self.pool(self.relu(self.conv1(x)))
        # 畳み込み層2 + ReLU + プーリング
        x = self.pool(self.relu(self.conv2(x)))
        # フラット化
        x = x.view(-1, 7 * 7 * 64)
        # 全結合層1 + ReLU
        x = self.relu(self.fc1(x))
        # 出力層
        x = self.fc2(x)
        return x

# デバイスの設定(GPUが利用可能ならGPUを使用)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleCNN().to(device)

# 損失関数と最適化アルゴリズムの定義
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# モデルの訓練
num_epochs = 5
for epoch in range(num_epochs):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        # 勾配の初期化
        optimizer.zero_grad()
        # 順伝播
        output = model(data)
        # 損失の計算
        loss = criterion(output, target)
        # 逆伝播
        loss.backward()
        # パラメータの更新
        optimizer.step()
        # ログの出力
        if batch_idx % 100 == 0:
            print(f'Epoch: {epoch+1} [{batch_idx * len(data)}/{len(train_loader.dataset)}] Loss: {loss.item():.6f}')

# モデルの評価
model.eval()
correct = 0
with torch.no_grad():
    for data, target in test_loader:
        data, target = data.to(device), target.to(device)
        output = model(data)
        # 最も高い確率を持つクラスを選択
        pred = output.argmax(dim=1, keepdim=True)
        # 正解数を累計
        correct += pred.eq(target.view_as(pred)).sum().item()

print(f'Test Accuracy: {correct}/{len(test_loader.dataset)} ({100. * correct / len(test_loader.dataset):.2f}%)')

「畳み込み演算」って良い響きですよね,,,

LiFe is LiKe a BoAt

Discussion