Machine Learning: 畳み込みニューラルネットワーク — 画像分類と商品認識ガイド
最終更新:2026-08-26
CNNはコンピュータに「目」を与えます。畳み込みカーネルは視覚受容体のように働き、エッジからオブジェクトまで、層を重ねるごとに画像を認識していきます。
1. 学習内容
- 畳み込み演算:カーネル/フィルタ、ストライド、パディング、マルチチャネル畳み込み
- プーリング層:MaxPool/AvgPool、空間的ダウンサンプリングと平行移動不変性
- 古典的アーキテクチャ:LeNet → AlexNet → VGG → ResNetへの進化と残差接続
- 転移学習:事前学習済みモデル(ImageNet)+ファインチューニングで少量データでも高精度を実現
- SalesPredictの商品画像分類:商品カテゴリの自動認識
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) カーネルと特徴抽出
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) データ拡張(ランダムな反転・回転・クロップ)。
📖 まとめ
- 畳み込みカーネルは視覚受容体のように機能し、層ごとに特徴を抽出します:エッジ → テクスチャ → 部位 → オブジェクト
- プーリング層は空間次元を縮小し、平行移動不変性を高め、計算コストを削減します
- CNNアーキテクチャはLeNetからResNetへ進化し、残差接続が深いネットワークの学習問題を解決しました
- 転移学習:事前学習済み層の凍結+分類ヘッドの置き換えで、少量データでも高精度を実現できます
- 商品分類の3つの戦略:Feature Extractor(データ少量)、Partial Fine-tune(データ中程度)、Full Fine-tune(データ豊富)
- 画像の前処理は事前学習済みモデルに合わせる必要があります(Resize + Normalize)
📝 練習問題
- 基礎(難易度 ⭐):nn.Conv2d + nn.MaxPool2dを使って2層のCNNを構築してください。入力は3x32x32、出力は16x8x8とします。各層の後のshapeを出力してください。ヒント:Conv2d(3,8,3,pad=1) → Pool → Conv2d(8,16,3,pad=1) → Pool。
- 応用(難易度 ⭐⭐):転移学習(ResNet18)を使ってCIFAR-10(10クラス)を分類してください。fc以外のすべての層を凍結し、5エポック学習させてください。ヒント:torchvision.datasets.CIFAR10 + transforms。
- 発展(難易度 ⭐⭐⭐):CIFAR-10でFeature Extractor・Partial Fine-tune・Full Fine-tuneを比較し、精度と学習時間を評価してください。3つの戦略すべての精度対エポックの曲線をプロットしてください。ヒント:凍結する層の数を変え、各エポックのテスト精度を記録します。