CNN(Convolutional Neural Network, 畳み込みニューラルネットワーク)は、画像認識を中心に広く利用されている深層学習モデルです。畳み込み演算により、画像の局所的な特徴(エッジ、テクスチャなど)を効率的に抽出し、それらを階層的に組み合わせて高次の特徴を認識します。
本記事では、CNNの各構成要素の仕組みを数学的に解説し、PyTorchでMNIST手書き数字分類を実装します。
本記事の内容
- CNNのアーキテクチャの全体像
- 畳み込み層、プーリング層の数学的定義
- パラメータ数の計算
- PyTorchでのMNIST分類の実装
前提知識
この記事を読む前に、ニューラルネットワークの基礎(全結合層、活性化関数、誤差逆伝播法)を押さえておくと理解が深まります。
CNNのアーキテクチャ
CNNは主に以下の3種類の層で構成されます。
- 畳み込み層(Convolution Layer): カーネル(フィルタ)を入力にスライドさせて特徴マップを生成
- プーリング層(Pooling Layer): 特徴マップのサイズを縮小し、位置不変性を獲得
- 全結合層(Fully Connected Layer): 抽出した特徴を基に分類を行う
典型的な構成は、
$$ \text{入力} \to [\text{Conv} \to \text{ReLU} \to \text{Pool}] \times L \to \text{Flatten} \to \text{FC} \to \text{出力} $$
畳み込み層
2次元畳み込みの定義
入力画像 $\bm{X} \in \mathbb{R}^{H \times W}$ とカーネル $\bm{K} \in \mathbb{R}^{k_h \times k_w}$ の畳み込みは、
$$ (\bm{X} * \bm{K})_{ij} = \sum_{m=0}^{k_h-1}\sum_{n=0}^{k_w-1} X_{i+m, j+n} \cdot K_{m,n} $$
出力のサイズは、
$$ H_{\text{out}} = \frac{H – k_h + 2p}{s} + 1, \quad W_{\text{out}} = \frac{W – k_w + 2p}{s} + 1 $$
ここで $p$ はパディング、$s$ はストライドです。
多チャネルの畳み込み
入力が $C_{\text{in}}$ チャネルの場合、カーネルは $\bm{K} \in \mathbb{R}^{C_{\text{in}} \times k_h \times k_w}$ となり、
$$ (\bm{X} * \bm{K})_{ij} = \sum_{c=1}^{C_{\text{in}}}\sum_{m=0}^{k_h-1}\sum_{n=0}^{k_w-1} X_{c, i+m, j+n} \cdot K_{c, m, n} + b $$
$C_{\text{out}}$ 個のカーネルを用いると、$C_{\text{out}}$ チャネルの出力が得られます。
パラメータ数
畳み込み層のパラメータ数は、
$$ \text{パラメータ数} = C_{\text{out}} \times (C_{\text{in}} \times k_h \times k_w + 1) $$
最後の +1 はバイアス項です。全結合層と比較して大幅にパラメータ数が少なく、これがCNNの効率性の源泉です。
プーリング層
プーリング層は特徴マップのサイズを縮小します。最もよく使われるのはMaxプーリングです。
$$ \text{MaxPool}_{ij} = \max_{(m, n) \in R_{ij}} X_{m,n} $$
$R_{ij}$ はプーリングウィンドウの領域です。
プーリングにより、
- 計算量の削減
- 微小な位置変化に対する不変性
- 過学習の抑制
が得られます。
PyTorchでの実装
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# デバイス設定
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Device: {device}")
# --- データの準備 ---
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST('./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)
# --- CNNモデルの定義 ---
class CNN(nn.Module):
def __init__(self):
super().__init__()
# 畳み込み層
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1) # 28x28 -> 28x28
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) # 14x14 -> 14x14
self.pool = nn.MaxPool2d(2, 2) # サイズを半分に
self.dropout1 = nn.Dropout2d(0.25)
self.dropout2 = nn.Dropout(0.5)
# 全結合層
self.fc1 = nn.Linear(64 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
# Conv1 -> ReLU -> Pool: (1, 28, 28) -> (32, 14, 14)
x = self.pool(torch.relu(self.conv1(x)))
# Conv2 -> ReLU -> Pool: (32, 14, 14) -> (64, 7, 7)
x = self.pool(torch.relu(self.conv2(x)))
x = self.dropout1(x)
# Flatten
x = x.view(-1, 64 * 7 * 7)
# FC1 -> ReLU -> Dropout
x = torch.relu(self.fc1(x))
x = self.dropout2(x)
# FC2 (出力)
x = self.fc2(x)
return x
model = CNN().to(device)
# パラメータ数の確認
total_params = sum(p.numel() for p in model.parameters())
print(f"総パラメータ数: {total_params:,}")
# --- 学習 ---
optimizer = optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
n_epochs = 10
train_losses = []
test_accuracies = []
for epoch in range(n_epochs):
model.train()
epoch_loss = 0
for batch_x, batch_y in train_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
optimizer.zero_grad()
output = model(batch_x)
loss = criterion(output, batch_y)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
avg_loss = epoch_loss / len(train_loader)
train_losses.append(avg_loss)
# テスト精度
model.eval()
correct = 0
total = 0
with torch.no_grad():
for batch_x, batch_y in test_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
output = model(batch_x)
pred = output.argmax(dim=1)
correct += (pred == batch_y).sum().item()
total += batch_y.size(0)
acc = correct / total
test_accuracies.append(acc)
print(f"Epoch [{epoch+1}/{n_epochs}] Loss: {avg_loss:.4f}, Test Acc: {acc:.4f}")
# --- 可視化 ---
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
ax1 = axes[0]
ax1.plot(train_losses, 'b-o', markersize=4)
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Loss')
ax1.set_title('Training Loss')
ax1.grid(True, alpha=0.3)
ax2 = axes[1]
ax2.plot(test_accuracies, 'r-o', markersize=4)
ax2.set_xlabel('Epoch')
ax2.set_ylabel('Accuracy')
ax2.set_title('Test Accuracy')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# --- 予測結果の表示 ---
model.eval()
test_x, test_y = next(iter(test_loader))
with torch.no_grad():
output = model(test_x.to(device))
preds = output.argmax(dim=1).cpu()
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for i in range(10):
ax = axes[i // 5, i % 5]
ax.imshow(test_x[i].squeeze(), cmap='gray')
color = 'green' if preds[i] == test_y[i] else 'red'
ax.set_title(f'Pred: {preds[i].item()} (True: {test_y[i].item()})', color=color)
ax.axis('off')
plt.tight_layout()
plt.show()
まとめ
本記事では、CNNの仕組みとPyTorchでの実装について解説しました。
- CNNは畳み込み層・プーリング層・全結合層で構成され、局所的な特徴を階層的に抽出する
- 畳み込み層はカーネルをスライドさせて特徴マップを生成し、パラメータ共有により効率的
- プーリング層は特徴マップを縮小し、位置不変性と過学習抑制の効果がある
- MNISTの手書き数字分類では、シンプルなCNNでも99%近い精度が得られる
次のステップとして、以下の記事も参考にしてください。