Machine Learning: 畳み込みニューラルネットワーク — 画像分類と商品認識ガイド

最終更新:2026-08-26

CNNはコンピュータに「目」を与えます。畳み込みカーネルは視覚受容体のように働き、エッジからオブジェクトまで、層を重ねるごとに画像を認識していきます。

1. 学習内容


2. 実際のECサイトでの事例

(1) 課題:手動の商品分類は遅く、基準がばらつく

Bobのプラットフォームでは毎日1,000点の新商品が登録されますが、運営チームが商品画像を手動で正しいカテゴリに振り分けています。手動分類は遅く(1枚あたり5秒)、一貫性がなく(同じ商品でも人によって異なるカテゴリに分類される)、コストもかかります(人件費として月額3万米ドル)。画像分類はEC運営におけるボトルネックです。

(2) CNNによる解決策

CNNは商品画像から視覚的特徴を自動的に学習して分類を行います。ミリ秒単位の処理速度、高い一貫性、そして人件費ゼロを実現します。

PYTHON
import torchvision.models as models

# Transfer learning: use pre-trained ResNet
model = models.resnet18(pretrained=True)
model.fc = nn.Linear(model.fc.in_features, num_classes)  # Replace last layer

(3) 成果:分類精度95%、月額3万ドルのコスト削減

BobはCNN+転移学習を活用して商品分類を自動化し、95%以上の精度を達成しました。処理速度は1枚あたり5秒から0.01秒に向上し、月額3万米ドルの人件費を削減できました。


3. 畳み込みの仕組み

(1) カーネルと特徴抽出

100%
graph TB
    INPUT[Input Image<br/>H x W x C] --> CONV1[Conv Layer 1<br/>Edges & Lines]
    CONV1 --> POOL1[Pool Layer 1<br/>Reduce Spatial]
    POOL1 --> CONV2[Conv Layer 2<br/>Textures & Parts]
    CONV2 --> POOL2[Pool Layer 2]
    POOL2 --> CONV3[Conv Layer 3<br/>Objects & Shapes]
    CONV3 --> FLATTEN[Flatten]
    FLATTEN --> FC[Fully Connected]
    FC --> OUTPUT[Class Probabilities]

▶ サンプル:畳み込みの手動実装

PYTHON
import torch
import torch.nn as nn

# Input: 1 image, 1 channel, 5x5
input_img = torch.tensor([[
    [1, 2, 0, 1, 3],
    [0, 1, 2, 3, 1],
    [1, 3, 1, 0, 2],
    [2, 0, 3, 1, 1],
    [0, 1, 2, 0, 1],
]], dtype=torch.float32).unsqueeze(0)  # shape: (1, 1, 5, 5)

# Conv2d: 1 input channel, 1 output channel, 3x3 kernel
conv = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, stride=1, padding=0)
print(f"Kernel weights:\n{conv.weight.data.squeeze()}")
print(f"Bias: {conv.bias.data.item():.4f}")

output = conv(input_img)
print(f"\nInput shape: {input_img.shape}")
print(f"Output shape: {output.shape}")
print(f"Output:\n{output.squeeze().detach()}")

出力:

TEXT 📖 参照専用
# Execution successful

(2) 畳み込みのパラメータ

パラメータ 意味 効果
kernel_size カーネルサイズ 3x3(一般的)/ 5x5 / 7x7
stride ストライド(ステップ幅) 1(デフォルト)/ 2(ダウンサンプリング)
padding ゼロパディング 0(縮小する)/ 1(サイズを維持)
in_channels 入力チャネル数 RGB=3、グレースケール=1
out_channels 出力チャネル数 カーネル数と等しい

▶ サンプル:異なる畳み込み設定の比較

PYTHON
import torch.nn as nn

x = torch.randn(1, 3, 32, 32)  # 1 batch, 3 channels, 32x32

configs = {
    "3x3, stride=1, pad=1": nn.Conv2d(3, 16, 3, stride=1, padding=1),
    "3x3, stride=2, pad=1": nn.Conv2d(3, 16, 3, stride=2, padding=1),
    "5x5, stride=1, pad=2": nn.Conv2d(3, 16, 5, stride=1, padding=2),
    "5x5, stride=2, pad=0": nn.Conv2d(3, 16, 5, stride=2, padding=0),
}

for name, layer in configs.items():
    out = layer(x)
    print(f"{name:25s}: {x.shape} → {out.shape}")

出力:

TEXT 📖 参照専用
# Execution successful

4. プーリング層とCNNアーキテクチャ

(1) プーリング演算

▶ サンプル:MaxPoolとAvgPoolの比較

PYTHON
import torch
import torch.nn as nn

x = torch.tensor([[
    [1, 3, 2, 4],
    [5, 6, 7, 8],
    [9, 2, 1, 3],
    [4, 5, 6, 7],
]], dtype=torch.float32).unsqueeze(0)  # (1, 1, 4, 4)

maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
avgpool = nn.AvgPool2d(kernel_size=2, stride=2)

print(f"Input:\n{x.squeeze()}")
print(f"\nMaxPool (2x2):\n{maxpool(x).squeeze()}")
print(f"\nAvgPool (2x2):\n{avgpool(x).squeeze()}")

出力:

TEXT 📖 参照専用
# Execution successful

(2) 古典的アーキテクチャの進化

アーキテクチャ イノベーション 深さ パラメータ数
LeNet 1998 最初のCNN 5 60K
AlexNet 2012 ReLU + Dropout + GPU 8 60M
VGG 2014 小さなカーネル(3x3)の積み重ね 16-19 138M
ResNet 2015 残差接続(スキップ接続) 18-152 11-60M

▶ サンプル:PyTorchでシンプルなCNNを構築

PYTHON
import torch
import torch.nn as nn

class SimpleCNN(nn.Module):
    def __init__(self, num_classes=5):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1),   # 3→32 channels, 32x32
            nn.ReLU(),
            nn.MaxPool2d(2, 2),               # 32→16x16
            nn.Conv2d(32, 64, 3, padding=1),  # 32→64 channels, 16x16
            nn.ReLU(),
            nn.MaxPool2d(2, 2),               # 16→8x8
            nn.Conv2d(64, 128, 3, padding=1), # 64→128 channels, 8x8
            nn.ReLU(),
            nn.MaxPool2d(2, 2),               # 8→4x4
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(128 * 4 * 4, 256),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(256, num_classes),
        )

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x

model = SimpleCNN(num_classes=5)
print(model)
print(f"Parameters: {sum(p.numel() for p in model.parameters()):,}")

出力:

TEXT 📖 参照専用
# Function defined successfully

5. 転移学習

(1) 転移学習が機能する理由

ImageNetで事前学習されたモデルは、汎用的な視覚的特徴(エッジ → テクスチャ → オブジェクトの一部)をすでに学習しています。最後の層を置き換えるだけで、新しいタスクに適応させることができます。

▶ サンプル:ResNet18による転移学習

PYTHON
import torch
import torch.nn as nn
import torchvision.models as models
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import numpy as np

# Load pre-trained ResNet18
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Freeze all layers (feature extractor mode)
for param in model.parameters():
    param.requires_grad = False

# Replace the last fully connected layer
num_classes = 5  # Electronics, Clothing, Food, Books, Home
model.fc = nn.Linear(model.fc.in_features, num_classes)

# Only the new fc layer has trainable parameters
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"Trainable: {trainable:,} / Total: {total:,} ({trainable/total*100:.1f}%)")

# Data transforms for pre-trained model
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

出力:

TEXT 📖 参照専用
# Execution successful

(2) ファインチューニングの戦略

戦略 凍結する層 学習データサイズ 学習時間 最適なケース
Feature Extractor すべて(fcのみ学習) 小(<1k) 高速 データが非常に少ない場合
Partial Fine-tune 最後の数層 中(1-10k) 中程度 データが適度にある場合
Full Fine-tune なし 大(>10k) 低速 データが豊富な場合

▶ サンプル:最後の数層のファインチューニング

PYTHON
import torchvision.models as models
import torch.nn as nn

model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Freeze early layers, unfreeze layer4
for name, param in model.named_parameters():
    if "layer4" in name or "fc" in name:
        param.requires_grad = True
    else:
        param.requires_grad = False

model.fc = nn.Linear(model.fc.in_features, 5)

trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Fine-tune trainable params: {trainable:,}")

出力:

TEXT 📖 参照専用
# Execution successful

6. SalesPredictの商品画像分類

▶ サンプル:転移学習の完全な学習パイプライン

PYTHON
import torch
import torch.nn as nn
import torchvision.models as models
from torch.utils.data import DataLoader, TensorDataset
from sklearn.model_selection import train_test_split
import numpy as np

# Simulate image data (in practice, load real images with ImageFolder)
rng = np.random.default_rng(42)
n = 2000
X = rng.standard_normal((n, 3, 224, 224)).astype(np.float32)  # 3-channel 224x224
y = rng.integers(0, 5, n)  # 5 product categories

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

train_ds = TensorDataset(torch.FloatTensor(X_train), torch.LongTensor(y_train))
test_ds = TensorDataset(torch.FloatTensor(X_test), torch.LongTensor(y_test))
train_loader = DataLoader(train_ds, batch_size=32, shuffle=True)
test_loader = DataLoader(test_ds, batch_size=64)

# Pre-trained ResNet18 with custom head
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
for param in model.parameters():
    param.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, 5)

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)

# Training
for epoch in range(10):
    model.train()
    correct, total = 0, 0
    for X_batch, y_batch in train_loader:
        y_pred = model(X_batch)
        loss = criterion(y_pred, y_batch)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        correct += (y_pred.argmax(1) == y_batch).sum().item()
        total += y_batch.size(0)

    if epoch % 2 == 0:
        print(f"Epoch {epoch}: loss={loss.item():.4f}, acc={correct/total:.3f}")

# Evaluate
model.eval()
correct, total = 0, 0
with torch.no_grad():
    for X_batch, y_batch in test_loader:
        y_pred = model(X_batch)
        correct += (y_pred.argmax(1) == y_batch).sum().item()
        total += y_batch.size(0)

print(f"\nTest Accuracy: {correct/total:.3f}")

出力:

TEXT 📖 参照専用
# Execution successful

❓ よくある質問

Q なぜCNNはMLPより画像処理に適しているのですか?
A 理由は3つあります。1) パラメータ共有(同じカーネルが画像全体を走査するためパラメータ数が削減される);2) 局所結合(各ニューロンは局所的な領域のみを参照する);3) 平行移動不変性(位置に関係なくオブジェクトを認識できる)。MLPは画像をフラット化するため空間構造が失われます。
Q 転移学習にはどのくらいのデータが必要ですか?
A Feature Extractorモード(fc層のみ学習)なら数百枚の画像で十分です。最後の数層をファインチューニングする場合は1〜10k枚、スクラッチから学習する場合は10k枚以上が必要です。データが少ないほど、多くの層を凍結すべきです。
Q なぜ3x3カーネルが最もよく使われるのですか?
A 3x3畳み込みを2層重ねると、5x5カーネル1層と同じ受容野が得られますが、パラメータ数は少なく(18対25)、非線形性も強くなります(ReLU活性化が2回)。VGGは、小さなカーネルの積み重ねが大きなカーネルより優れていることを実証しました。
Q ResNetの残差接続とは何ですか?
A y = F(x) + x という式で表され、ネットワークは完全なマッピングではなく残差(増分)を学習します。利点としては、深いネットワークにおける勾配消失問題を解決し、100層以上のネットワークの学習を可能にすることが挙げられます。
Q 画像の前処理でNormalizeが必要な理由は何ですか?
A 事前学習済みモデルはImageNetの平均と標準偏差で正規化されています。新しいデータにも同じ正規化を適用しなければ、特徴の分布が一致せず、転移学習の性能が低下します。
Q クラスの不均衡にはどう対処すればよいですか?
A 3つのアプローチがあります。1) 重み付き損失関数(nn.CrossEntropyLoss(重み=class_weights));2) 少数クラスのオーバーサンプリング;3) データ拡張(ランダムな反転・回転・クロップ)。

📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐):nn.Conv2d + nn.MaxPool2dを使って2層のCNNを構築してください。入力は3x32x32、出力は16x8x8とします。各層の後のshapeを出力してください。ヒント:Conv2d(3,8,3,pad=1) → Pool → Conv2d(8,16,3,pad=1) → Pool。
  2. 応用(難易度 ⭐⭐):転移学習(ResNet18)を使ってCIFAR-10(10クラス)を分類してください。fc以外のすべての層を凍結し、5エポック学習させてください。ヒント:torchvision.datasets.CIFAR10 + transforms。
  3. 発展(難易度 ⭐⭐⭐):CIFAR-10でFeature Extractor・Partial Fine-tune・Full Fine-tuneを比較し、精度と学習時間を評価してください。3つの戦略すべての精度対エポックの曲線をプロットしてください。ヒント:凍結する層の数を変え、各エポックのテスト精度を記録します。

← 前のレッスン:ディープラーニング入門 | 次のレッスン:モデルの評価とチューニング →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%