深層学習④CNN II

Categories:

第4章 深層学習:CNN Ⅱ

📚 この章の学習目標

  • Convolution層のフィルタとカーネルの関係を深く理解する
  • CNNのパラメータ数を正確に計算する方法を習得する
  • モデルの性能を向上させる様々な技法を学ぶ
  • Data Augmentationによるデータ拡張の重要性を理解する
  • Batch NormalizationとDropoutの原理と実装を習得する
  • 実践的な犬・猫分類プロジェクトを完成させる

🔄 第3章の復習:Convolutionの基礎

💡 フィルタ(Filter)とカーネル(Kernel)の関係

第3章で学んだ内容を復習しましょう。CNNでは「フィルタ」と「カーネル」という用語がよく混用されますが、厳密には:

  • カーネル(Kernel): 重み行列そのもの
  • フィルタ(Filter): 1つまたは複数のカーネルで構成される特徴抽出器

📐 Conv2Dのパラメータ

Conv2D(filters=32, kernel_size=3)

パラメータの意味

  • filters=32: 32個の異なるフィルタを使用
  • kernel_size=3: 3×3のカーネル(重み行列)
  • 各フィルタは入力のチャンネル数と同じ深さを持つ

多チャンネル入力への適用例

例1:RGB画像への適用

入力: 32×32×3 (RGB 3チャンネル)
フィルタ: 3×3×3×16 (16個のフィルタ、各フィルタは3チャンネル)
       ↓
出力: 30×30×16 (padding='valid'の場合)

例2:特徴マップへの適用

入力: 14×14×32 (前層の出力32チャンネル)
フィルタ: 3×3×32×64 (64個のフィルタ、各フィルタは32チャンネル)
       ↓
出力: 12×12×64 (padding='valid'の場合)

⚠️ 重要な原則

  • Convolutionフィルタのチャンネル数は入力Feature Mapのチャンネル数と同じでなければならない
  • 適用するフィルタの個数が出力Feature Mapのチャンネル数になる

🔢 パラメータ数の詳細計算

基本的なパラメータ計算式

💡 Conv2D層のパラメータ数

パラメータ数 = (カーネル高さ × カーネル幅 × 入力チャンネル数 × フィルタ数) + バイアス数
           = (kernel_h × kernel_w × input_channels × filters) + filters

📊 計算例:段階的な理解

例1:最初のConv2D層

Conv2D(filters=16, kernel_size=3, input_shape=(28, 28, 1))

計算過程:

入力: 28×28×1 (グレースケール画像)
カーネル: 3×3×1 (入力チャンネル数1)
フィルタ数: 16個

パラメータ数 = (3 × 3 × 1) × 16 + 16
           = 9 × 16 + 16
           = 144 + 16
           = 160

詳細な内訳

  • 重み: 3×3×1×16 = 144個
  • バイアス: 16個(フィルタごとに1個)
  • 合計: 160個

例2:2番目のConv2D層

Conv2D(filters=32, kernel_size=3)  # 前層の出力は13×13×16

計算過程:

入力: 13×13×16 (前層の出力)
カーネル: 3×3×16 (入力チャンネル数16)
フィルタ数: 32個

パラメータ数 = (3 × 3 × 16) × 32 + 32
           = 144 × 32 + 32
           = 4,608 + 32
           = 4,640

例3:Dense(全結合)層

Flatten()  # 5×5×32 = 800
Dense(32)

計算過程:

入力: 800 (Flattenされた特徴)
出力: 32

パラメータ数 = 800 × 32 + 32
           = 25,600 + 32
           = 25,632

Dense層の特徴

  • すべての入力がすべての出力に接続される(全結合)
  • パラメータ数が非常に多くなりやすい
  • Conv層とPooling層で特徴を圧縮してからDense層に渡すのが一般的

🏗️ 完全なモデルのパラメータ計算例

model = Sequential([
    Conv2D(16, (3,3), activation='relu', input_shape=(28, 28, 1)),  # 160
    MaxPooling2D((2,2)),                                             # 0
    Conv2D(32, (3,3), activation='relu'),                            # 4,640
    MaxPooling2D((2,2)),                                             # 0
    Flatten(),                                                       # 0
    Dense(32, activation='relu'),                                    # 25,632
    Dense(10, activation='softmax')                                  # 330
])

# 総パラメータ数 = 160 + 4,640 + 25,632 + 330 = 30,762

💡 Pooling層のパラメータ

MaxPooling2D層はパラメータを持ちません。
単に各領域の最大値を選択するだけで、学習可能な重みがないためです。

📈 出力サイズの計算

💡 Convolution後の出力サイズ

出力サイズ = (入力サイズ - カーネルサイズ + 2×Padding) / Stride + 1

例:

入力: 28×28
カーネル: 3×3
Padding: 0 (valid)
Stride: 1

出力サイズ = (28 - 3 + 0) / 1 + 1 = 26
→ 出力: 26×26

💡 Pooling後の出力サイズ

出力サイズ = 入力サイズ / プールサイズ

例:

入力: 26×26
プールサイズ: 2×2

出力サイズ = 26 / 2 = 13
→ 出力: 13×13

🔍 実習:パラメータ数の検証

from tensorflow import keras
from tensorflow.keras import layers

# モデルの構築
model = keras.Sequential([
    layers.Conv2D(16, (3,3), activation='relu', input_shape=(28, 28, 1)),
    layers.MaxPooling2D((2,2)),
    layers.Conv2D(32, (3,3), activation='relu'),
    layers.MaxPooling2D((2,2)),
    layers.Flatten(),
    layers.Dense(32, activation='relu'),
    layers.Dense(10, activation='softmax')
])

# モデルのサマリー表示
model.summary()
# 出力結果
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 26, 26, 16)        160     
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 13, 13, 16)        0       
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 11, 11, 32)        4640    
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 5, 5, 32)          0       
_________________________________________________________________
flatten (Flatten)            (None, 800)               0       
_________________________________________________________________
dense (Dense)                (None, 32)                25632   
_________________________________________________________________
dense_1 (Dense)              (None, 10)                330     
=================================================================
Total params: 30,762
Trainable params: 30,762
Non-trainable params: 0
_________________________________________________________________

🚀 CNNの性能向上技法

💡 モデル性能を向上させる4つの主要技法

  1. 層の追加(Deeper Network)
  2. Data Augmentation(データ拡張)
  3. Batch Normalization(バッチ正規化)
  4. Dropout(ドロップアウト)

1️⃣ 層の追加(Deeper Network)

💡 深いネットワークの利点

  • より複雑な特徴を階層的に学習できる
  • 低レベル特徴(エッジ、テクスチャ)から高レベル特徴(形状、物体)へ
  • 表現力が向上する

浅いネットワーク vs 深いネットワーク

浅いネットワーク(2層):

model = Sequential([
    Conv2D(32, (3,3), activation='relu', input_shape=(32, 32, 3)),
    MaxPooling2D((2,2)),
    Flatten(),
    Dense(10, activation='softmax')
])

深いネットワーク(6層):

model = Sequential([
    # ブロック1
    Conv2D(32, (3,3), activation='relu', input_shape=(32, 32, 3)),
    Conv2D(32, (3,3), activation='relu'),
    MaxPooling2D((2,2)),

    # ブロック2
    Conv2D(64, (3,3), activation='relu'),
    Conv2D(64, (3,3), activation='relu'),
    MaxPooling2D((2,2)),

    # ブロック3
    Conv2D(128, (3,3), activation='relu'),
    MaxPooling2D((2,2)),

    # 分類器
    Flatten(),
    Dense(128, activation='relu'),
    Dense(10, activation='softmax')
])

⚠️ 深いネットワークの注意点

  • 計算コスト増加: 学習時間が長くなる
  • 過学習のリスク: 正則化技法が必要
  • 勾配消失問題: 活性化関数やBatch Normalizationで対処

2️⃣ Data Augmentation(データ拡張)

💡 Data Augmentationとは?

既存の訓練データに対して様々な変換を適用し、人工的にデータセットを拡大する技法です。

  • 過学習を防止
  • モデルの汎化性能を向上
  • 少ないデータでも効果的に学習

主要なData Augmentation技法

技法説明効果
Rotation画像を回転向きの変化に頑健
Width/Height Shift水平・垂直移動位置の変化に頑健
Zoom拡大・縮小スケールの変化に頑健
Horizontal Flip左右反転左右対称性を学習
Brightness Adjustment明るさ調整照明条件の変化に頑健
Shearせん断変換変形に頑健

KerasのImageDataGenerator

from tensorflow.keras.preprocessing.image import ImageDataGenerator

# Data Augmentationの設定
datagen = ImageDataGenerator(
    rotation_range=20,          # ±20度回転
    width_shift_range=0.2,      # 水平方向に±20%移動
    height_shift_range=0.2,     # 垂直方向に±20%移動
    horizontal_flip=True,       # 左右反転
    zoom_range=0.2,             # ±20%ズーム
    shear_range=0.2,            # せん断変換
    fill_mode='nearest'         # 空白領域の埋め方
)

# 訓練データに適用
datagen.fit(X_train)

Data Augmentationの可視化

import matplotlib.pyplot as plt
import numpy as np

# 1枚の画像を選択
sample_image = X_train[0:1]  # shape: (1, 32, 32, 3)

# 拡張された画像を生成
plt.figure(figsize=(15, 5))
plt.suptitle('Data Augmentation Examples', fontsize=16)

# オリジナル画像
plt.subplot(2, 5, 1)
plt.imshow(sample_image[0])
plt.title('Original')
plt.axis('off')

# 拡張された画像を9枚生成
aug_iter = datagen.flow(sample_image, batch_size=1)
for i in range(9):
    plt.subplot(2, 5, i+2)
    aug_image = next(aug_iter)[0].astype('uint8')
    plt.imshow(aug_image)
    plt.title(f'Augmented {i+1}')
    plt.axis('off')

plt.tight_layout()
plt.show()

モデル訓練でのData Augmentation使用

# Data Augmentationを使った訓練
history = model.fit(
    datagen.flow(X_train, y_train, batch_size=32),
    steps_per_epoch=len(X_train) // 32,
    epochs=50,
    validation_data=(X_test, y_test)
)

steps_per_epochの計算

steps_per_epoch = 訓練データ数 / バッチサイズ

Data Augmentationを使う場合、各エポックで異なる変換が適用されます。

3️⃣ Batch Normalization(バッチ正規化)

💡 Batch Normalizationとは?

各ミニバッチごとに、層の入力を平均0、分散1に正規化する技法です。

  • 学習の安定化: 学習率を大きくできる
  • 高速な収束: 学習が速くなる
  • 正則化効果: 過学習を軽減
  • 勾配消失問題の緩和

Batch Normalizationの動作原理

入力バッチ → [平均と分散を計算] → [正規化] → [スケールとシフト] → 出力

数式:

1. バッチの平均: μ = (1/m) Σ xᵢ
2. バッチの分散: σ² = (1/m) Σ (xᵢ - μ)²
3. 正規化: x̂ᵢ = (xᵢ - μ) / √(σ² + ε)
4. スケールとシフト: yᵢ = γ·x̂ᵢ + β

学習可能なパラメータ

  • γ(ガンマ): スケールパラメータ
  • β(ベータ): シフトパラメータ

これらはバックプロパゲーションで学習されます。

Batch Normalizationの実装

from tensorflow.keras.layers import BatchNormalization

model = Sequential([
    Conv2D(32, (3,3), input_shape=(32, 32, 3)),
    BatchNormalization(),          # Conv2Dの後に追加
    Activation('relu'),
    MaxPooling2D((2,2)),

    Conv2D(64, (3,3)),
    BatchNormalization(),          # Conv2Dの後に追加
    Activation('relu'),
    MaxPooling2D((2,2)),

    Flatten(),
    Dense(128),
    BatchNormalization(),          # Denseの後に追加
    Activation('relu'),
    Dense(10, activation='softmax')
])

⚠️ 配置の順序

一般的には:

Conv2D → BatchNormalization → Activation → Pooling

または:

Conv2D → Activation → BatchNormalization → Pooling

どちらも使われますが、前者が最近では一般的です。

Batch Normalizationの効果比較

# Batch Normalizationなし
model_without_bn = Sequential([
    Conv2D(64, (3,3), activation='relu', input_shape=(32, 32, 3)),
    MaxPooling2D((2,2)),
    Flatten(),
    Dense(10, activation='softmax')
])

# Batch Normalizationあり
model_with_bn = Sequential([
    Conv2D(64, (3,3), input_shape=(32, 32, 3)),
    BatchNormalization(),
    Activation('relu'),
    MaxPooling2D((2,2)),
    Flatten(),
    Dense(128),
    BatchNormalization(),
    Activation('relu'),
    Dense(10, activation='softmax')
])

4️⃣ Dropout(ドロップアウト)

💡 Dropoutとは?

訓練時にランダムにニューロンの一部を一時的に無効化する正則化技法です。

  • 過学習を防止
  • アンサンブル効果:複数のサブネットワークを学習
  • 頑健なモデル:特定のニューロンへの依存を減らす

Dropoutの動作

通常の層:
[●][●][●][●][●][●][●][●]
 ↓  ↓  ↓  ↓  ↓  ↓  ↓  ↓
すべてのニューロンが活性

Dropout適用(rate=0.5):
[●][○][●][○][●][●][○][●]
 ↓     ↓     ↓  ↓     ↓
50%がランダムにドロップ

訓練時と推論時の違い

  • 訓練時: ランダムにニューロンをドロップ
  • 推論時: すべてのニューロンを使用(自動調整)

Dropoutの実装

from tensorflow.keras.layers import Dropout

model = Sequential([
    Conv2D(32, (3,3), activation='relu', input_shape=(32, 32, 3)),
    MaxPooling2D((2,2)),

    Conv2D(64, (3,3), activation='relu'),
    MaxPooling2D((2,2)),

    Flatten(),
    Dense(128, activation='relu'),
    Dropout(0.5),              # 50%のニューロンをドロップ

    Dense(64, activation='relu'),
    Dropout(0.3),              # 30%のニューロンをドロップ

    Dense(10, activation='softmax')
])

Dropout rateの選択

  • 0.2-0.3: 軽度の正則化
  • 0.5: 標準的な設定(最も一般的)
  • 0.7-0.8: 強い正則化(過学習が深刻な場合)

すべての技法を組み合わせた最適モデル

from tensorflow.keras.layers import Conv2D, MaxPooling2D, Dense
from tensorflow.keras.layers import Flatten, Dropout, BatchNormalization, Activation

model = Sequential([
    # ブロック1
    Conv2D(32, (3,3), padding='same', input_shape=(32, 32, 3)),
    BatchNormalization(),
    Activation('relu'),
    Conv2D(32, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    MaxPooling2D((2,2)),
    Dropout(0.2),

    # ブロック2
    Conv2D(64, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    Conv2D(64, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    MaxPooling2D((2,2)),
    Dropout(0.3),

    # ブロック3
    Conv2D(128, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    MaxPooling2D((2,2)),
    Dropout(0.4),

    # 分類器
    Flatten(),
    Dense(128),
    BatchNormalization(),
    Activation('relu'),
    Dropout(0.5),
    Dense(10, activation='softmax')
])

model.summary()

🔬 実習:CIFAR-10での性能比較

CIFAR-10データセットの準備

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.utils import to_categorical
import numpy as np

# データセットの読み込み
(X_train, y_train), (X_test, y_test) = cifar10.load_data()

print(f'訓練データ: {X_train.shape}')
print(f'テストデータ: {X_test.shape}')

# クラス名
class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer',
               'dog', 'frog', 'horse', 'ship', 'truck']

# データの正規化
X_train = X_train.astype('float32') / 255.0
X_test = X_test.astype('float32') / 255.0

# ラベルのOne-hot encoding
y_train = to_categorical(y_train, 10)
y_test = to_categorical(y_test, 10)

モデル1:基本CNN(ベースライン)

model_basic = Sequential([
    Conv2D(32, (3,3), activation='relu', input_shape=(32, 32, 3)),
    MaxPooling2D((2,2)),
    Conv2D(64, (3,3), activation='relu'),
    MaxPooling2D((2,2)),
    Flatten(),
    Dense(64, activation='relu'),
    Dense(10, activation='softmax')
])

model_basic.compile(
    optimizer='adam',
    loss='categorical_crossentropy',
    metrics=['accuracy']
)

print("=== 基本CNNモデル ===")
model_basic.summary()

# 訓練
history_basic = model_basic.fit(
    X_train, y_train,
    batch_size=64,
    epochs=30,
    validation_data=(X_test, y_test),
    verbose=1
)

モデル2:Data Augmentation追加

from tensorflow.keras.preprocessing.image import ImageDataGenerator

# Data Augmentationの設定
datagen = ImageDataGenerator(
    rotation_range=15,
    width_shift_range=0.1,
    height_shift_range=0.1,
    horizontal_flip=True
)
datagen.fit(X_train)

model_augmented = Sequential([
    Conv2D(32, (3,3), activation='relu', input_shape=(32, 32, 3)),
    MaxPooling2D((2,2)),
    Conv2D(64, (3,3), activation='relu'),
    MaxPooling2D((2,2)),
    Flatten(),
    Dense(64, activation='relu'),
    Dense(10, activation='softmax')
])

model_augmented.compile(
    optimizer='adam',
    loss='categorical_crossentropy',
    metrics=['accuracy']
)

print("=== Data Augmentation適用モデル ===")

# 訓練
history_augmented = model_augmented.fit(
    datagen.flow(X_train, y_train, batch_size=64),
    steps_per_epoch=len(X_train) // 64,
    epochs=30,
    validation_data=(X_test, y_test),
    verbose=1
)

モデル3:すべての技法を適用

model_optimized = Sequential([
    # ブロック1
    Conv2D(32, (3,3), padding='same', input_shape=(32, 32, 3)),
    BatchNormalization(),
    Activation('relu'),
    Conv2D(32, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    MaxPooling2D((2,2)),
    Dropout(0.2),

    # ブロック2
    Conv2D(64, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    Conv2D(64, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    MaxPooling2D((2,2)),
    Dropout(0.3),

    # ブロック3
    Conv2D(128, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    MaxPooling2D((2,2)),
    Dropout(0.4),

    # 分類器
    Flatten(),
    Dense(128),
    BatchNormalization(),
    Activation('relu'),
    Dropout(0.5),
    Dense(10, activation='softmax')
])

model_optimized.compile(
    optimizer='adam',
    loss='categorical_crossentropy',
    metrics=['accuracy']
)

print("=== 最適化モデル(すべての技法適用) ===")
model_optimized.summary()

# 訓練
history_optimized = model_optimized.fit(
    datagen.flow(X_train, y_train, batch_size=64),
    steps_per_epoch=len(X_train) // 64,
    epochs=50,
    validation_data=(X_test, y_test),
    verbose=1
)

📊 結果の比較と可視化

import matplotlib.pyplot as plt

# 訓練履歴の可視化
fig, axes = plt.subplots(1, 2, figsize=(15, 5))

# Accuracy
axes[0].plot(history_basic.history['accuracy'], label='Basic - Train')
axes[0].plot(history_basic.history['val_accuracy'], label='Basic - Val')
axes[0].plot(history_augmented.history['val_accuracy'], label='Augmented - Val')
axes[0].plot(history_optimized.history['val_accuracy'], label='Optimized - Val')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Accuracy')
axes[0].set_title('Model Accuracy Comparison')
axes[0].legend()
axes[0].grid(True)

# Loss
axes[1].plot(history_basic.history['loss'], label='Basic - Train')
axes[1].plot(history_basic.history['val_loss'], label='Basic - Val')
axes[1].plot(history_augmented.history['val_loss'], label='Augmented - Val')
axes[1].plot(history_optimized.history['val_loss'], label='Optimized - Val')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Loss')
axes[1].set_title('Model Loss Comparison')
axes[1].legend()
axes[1].grid(True)

plt.tight_layout()
plt.show()

# 最終結果の比較
print("\n=== 最終テスト精度の比較 ===")
print(f"基本CNN: {history_basic.history['val_accuracy'][-1]:.4f}")
print(f"Data Augmentation: {history_augmented.history['val_accuracy'][-1]:.4f}")
print(f"最適化モデル: {history_optimized.history['val_accuracy'][-1]:.4f}")

予測結果の確認

# 予測
predictions = model_optimized.predict(X_test)
predicted_labels = np.argmax(predictions, axis=1)
true_labels = np.argmax(y_test, axis=1)

# ランダムに9枚表示
plt.figure(figsize=(12, 12))
for i in range(9):
    idx = np.random.randint(0, len(X_test))
    plt.subplot(3, 3, i+1)
    plt.imshow(X_test[idx])

    pred_label = class_names[predicted_labels[idx]]
    true_label = class_names[true_labels[idx]]

    color = 'green' if pred_label == true_label else 'red'
    plt.title(f'Pred: {pred_label}\nTrue: {true_label}', color=color)
    plt.axis('off')

plt.tight_layout()
plt.show()

🐕🐈 実習プロジェクト:犬と猫の分類

🎯 プロジェクト概要

実世界のデータを使って、犬と猫の画像を分類するCNNモデルを構築します。

  • データセット: Dogs vs Cats(Kaggle)
  • クラス数: 2(犬、猫)
  • 画像サイズ: 様々(リサイズが必要)
  • 環境: Google Colab推奨

Google Colabでの環境設定

# Google Driveのマウント
from google.colab import drive
drive.mount('/content/drive')

# 必要なライブラリのインポート
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.layers import *
from tensorflow.keras.models import Sequential
import matplotlib.pyplot as plt
import numpy as np
import os

データセットの準備

# データセットのダウンロード(Kaggle API使用)
!pip install kaggle

# Kaggle APIキーの設定(kaggle.jsonをアップロード)
!mkdir -p ~/.kaggle
!cp /content/drive/MyDrive/kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json

# データセットのダウンロード
!kaggle competitions download -c dogs-vs-cats-redux-kernels-edition
!unzip -q dogs-vs-cats-redux-kernels-edition.zip
!unzip -q train.zip

データ構造

train/
  ├── cat.0.jpg
  ├── cat.1.jpg
  ├── ...
  ├── dog.0.jpg
  ├── dog.1.jpg
  └── ...

データの整理

import shutil
import os

# ディレクトリ構造の作成
base_dir = '/content/data'
os.makedirs(base_dir, exist_ok=True)

# 訓練・検証・テスト用ディレクトリ
for split in ['train', 'validation', 'test']:
    for category in ['cats', 'dogs']:
        dir_path = os.path.join(base_dir, split, category)
        os.makedirs(dir_path, exist_ok=True)

# データの分割(猫:12500枚、犬:12500枚)
# 訓練:各8000枚、検証:各2000枚、テスト:各2500枚

# 猫の画像を移動
fnames = [f'cat.{i}.jpg' for i in range(10000)]
for i, fname in enumerate(fnames):
    src = os.path.join('/content/train', fname)
    if i < 8000:
        dst = os.path.join(base_dir, 'train/cats', fname)
    elif i < 10000:
        dst = os.path.join(base_dir, 'validation/cats', fname)
    shutil.copyfile(src, dst)

fnames = [f'cat.{i}.jpg' for i in range(10000, 12500)]
for fname in fnames:
    src = os.path.join('/content/train', fname)
    dst = os.path.join(base_dir, 'test/cats', fname)
    shutil.copyfile(src, dst)

# 犬の画像を移動
fnames = [f'dog.{i}.jpg' for i in range(10000)]
for i, fname in enumerate(fnames):
    src = os.path.join('/content/train', fname)
    if i < 8000:
        dst = os.path.join(base_dir, 'train/dogs', fname)
    elif i < 10000:
        dst = os.path.join(base_dir, 'validation/dogs', fname)
    shutil.copyfile(src, dst)

fnames = [f'dog.{i}.jpg' for i in range(10000, 12500)]
for fname in fnames:
    src = os.path.join('/content/train', fname)
    dst = os.path.join(base_dir, 'test/dogs', fname)
    shutil.copyfile(src, dst)

print("データの整理完了!")
print(f"訓練データ(猫): {len(os.listdir(os.path.join(base_dir, 'train/cats')))}枚")
print(f"訓練データ(犬): {len(os.listdir(os.path.join(base_dir, 'train/dogs')))}枚")
print(f"検証データ(猫): {len(os.listdir(os.path.join(base_dir, 'validation/cats')))}枚")
print(f"検証データ(犬): {len(os.listdir(os.path.join(base_dir, 'validation/dogs')))}枚")

ImageDataGeneratorの設定

# Data Augmentationを含む訓練データジェネレータ
train_datagen = ImageDataGenerator(
    rescale=1./255,              # 正規化
    rotation_range=40,           # ±40度回転
    width_shift_range=0.2,       # 水平移動
    height_shift_range=0.2,      # 垂直移動
    shear_range=0.2,             # せん断変換
    zoom_range=0.2,              # ズーム
    horizontal_flip=True,        # 左右反転
    fill_mode='nearest'
)

# 検証・テストデータは正規化のみ
test_datagen = ImageDataGenerator(rescale=1./255)

# ジェネレータの作成
train_generator = train_datagen.flow_from_directory(
    os.path.join(base_dir, 'train'),
    target_size=(150, 150),      # すべて150×150にリサイズ
    batch_size=32,
    class_mode='binary'          # 二値分類
)

validation_generator = test_datagen.flow_from_directory(
    os.path.join(base_dir, 'validation'),
    target_size=(150, 150),
    batch_size=32,
    class_mode='binary'
)

test_generator = test_datagen.flow_from_directory(
    os.path.join(base_dir, 'test'),
    target_size=(150, 150),
    batch_size=32,
    class_mode='binary',
    shuffle=False                # テストデータは順序を保持
)

モデルの構築

model = Sequential([
    # ブロック1
    Conv2D(32, (3,3), padding='same', input_shape=(150, 150, 3)),
    BatchNormalization(),
    Activation('relu'),
    Conv2D(32, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    MaxPooling2D((2,2)),
    Dropout(0.25),

    # ブロック2
    Conv2D(64, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    Conv2D(64, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    MaxPooling2D((2,2)),
    Dropout(0.25),

    # ブロック3
    Conv2D(128, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    Conv2D(128, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    MaxPooling2D((2,2)),
    Dropout(0.25),

    # ブロック4
    Conv2D(256, (3,3), padding='same'),
    BatchNormalization(),
    Activation('relu'),
    MaxPooling2D((2,2)),
    Dropout(0.25),

    # 分類器
    Flatten(),
    Dense(512),
    BatchNormalization(),
    Activation('relu'),
    Dropout(0.5),
    Dense(1, activation='sigmoid')  # 二値分類
])

model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.0001),
    loss='binary_crossentropy',
    metrics=['accuracy']
)

model.summary()

モデルの訓練

# Early Stoppingとモデルチェックポイント
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

callbacks = [
    EarlyStopping(
        monitor='val_loss',
        patience=10,
        restore_best_weights=True
    ),
    ModelCheckpoint(
        'best_model.h5',
        monitor='val_accuracy',
        save_best_only=True
    )
]

# 訓練
history = model.fit(
    train_generator,
    steps_per_epoch=train_generator.samples // train_generator.batch_size,
    epochs=50,
    validation_data=validation_generator,
    validation_steps=validation_generator.samples // validation_generator.batch_size,
    callbacks=callbacks,
    verbose=1
)

学習曲線の可視化

# Accuracy
plt.figure(figsize=(15, 5))

plt.subplot(1, 2, 1)
plt.plot(history.history['accuracy'], label='Training Accuracy')
plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Training and Validation Accuracy')
plt.legend()
plt.grid(True)

# Loss
plt.subplot(1, 2, 2)
plt.plot(history.history['loss'], label='Training Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training and Validation Loss')
plt.legend()
plt.grid(True)

plt.tight_layout()
plt.show()

テストデータでの評価

# テストデータでの評価
test_loss, test_accuracy = model.evaluate(test_generator)
print(f'\nTest Accuracy: {test_accuracy:.4f}')
print(f'Test Loss: {test_loss:.4f}')

# 予測
predictions = model.predict(test_generator)
predicted_classes = (predictions > 0.5).astype(int).flatten()

# 実際のラベル
true_classes = test_generator.classes
class_labels = list(test_generator.class_indices.keys())

# 混同行列
from sklearn.metrics import confusion_matrix, classification_report
import seaborn as sns

cm = confusion_matrix(true_classes, predicted_classes)

plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', 
            xticklabels=class_labels, yticklabels=class_labels)
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.title('Confusion Matrix')
plt.show()

# 詳細なレポート
print("\n=== Classification Report ===")
print(classification_report(true_classes, predicted_classes, 
                          target_names=class_labels))

予測結果の可視化

# ランダムに予測結果を表示
import random

plt.figure(figsize=(15, 10))

# ランダムに20枚選択
random_indices = random.sample(range(len(test_generator.filenames)), 20)

for i, idx in enumerate(random_indices):
    plt.subplot(4, 5, i+1)

    # 画像の読み込み
    img_path = os.path.join(base_dir, 'test', test_generator.filenames[idx])
    img = keras.preprocessing.image.load_img(img_path, target_size=(150, 150))
    plt.imshow(img)

    # 予測と実際のラベル
    pred_class = 'dog' if predicted_classes[idx] == 1 else 'cat'
    true_class = 'dog' if true_classes[idx] == 1 else 'cat'
    confidence = predictions[idx][0] if predicted_classes[idx] == 1 else 1 - predictions[idx][0]

    # タイトル(正解なら緑、不正解なら赤)
    color = 'green' if pred_class == true_class else 'red'
    plt.title(f'Pred: {pred_class} ({confidence:.2f})\nTrue: {true_class}', 
             color=color, fontsize=10)
    plt.axis('off')

plt.tight_layout()
plt.show()

誤分類の分析

# 誤分類されたサンプルの抽出
misclassified_indices = np.where(predicted_classes != true_classes)[0]

print(f"\n誤分類されたサンプル数: {len(misclassified_indices)}")
print(f"全体の精度: {test_accuracy:.4f}")

# 誤分類されたサンプルの中で、特に確信度が高かったものを表示
confidences = np.abs(predictions.flatten() - 0.5)
misclassified_confidences = confidences[misclassified_indices]
top_misclassified = misclassified_indices[np.argsort(misclassified_confidences)[-12:]]

plt.figure(figsize=(15, 8))
for i, idx in enumerate(top_misclassified):
    plt.subplot(3, 4, i+1)

    img_path = os.path.join(base_dir, 'test', test_generator.filenames[idx])
    img = keras.preprocessing.image.load_img(img_path, target_size=(150, 150))
    plt.imshow(img)

    pred_class = 'dog' if predicted_classes[idx] == 1 else 'cat'
    true_class = 'dog' if true_classes[idx] == 1 else 'cat'
    confidence = predictions[idx][0] if predicted_classes[idx] == 1 else 1 - predictions[idx][0]

    plt.title(f'Pred: {pred_class} ({confidence:.2f})\nTrue: {true_class}', 
             color='red', fontsize=10)
    plt.axis('off')

plt.suptitle('High Confidence Misclassifications', fontsize=16)
plt.tight_layout()
plt.show()

🎯 実習課題

🎯 課題:犬と猫の分類モデルの改善

課題内容:

犬と猫の分類プロジェクトをベースに、さらに性能を向上させてください。

要件:

  1. 異なるモデルアーキテクチャを最低3つ試す
  • 層の数を変える
  • フィルタ数を調整する
  • 異なる正則化技法を試す
  1. Data Augmentationのパラメータを最適化する
  • 異なる変換の組み合わせを試す
  • 過度な変換が悪影響を与えないか確認する
  1. ハイパーパラメータの調整
  • 学習率の調整
  • バッチサイズの変更
  • Dropoutレートの最適化
  1. 結果の詳細な分析
  • 各モデルの学習曲線を比較
  • 混同行列で誤分類のパターンを分析
  • 誤分類された画像の特徴を考察

期待される成果物:

  • 3つ以上のモデルの実装コード
  • 各モデルの性能比較表
  • 学習曲線のグラフ
  • 混同行列と分類レポート
  • 誤分類の分析レポート
  • 最終的な考察と改善提案

評価基準:

  • テスト精度85%以上を目標
  • 過学習を適切に抑制できているか
  • 実験の体系的な記録と分析

📚 第4章のまとめ

🎯 第4章の重要ポイント

  • パラメータ数の計算は、モデル設計と最適化に不可欠
  • 深いネットワークは複雑な特徴を階層的に学習できる
  • Data Augmentationはデータを人工的に拡張し、過学習を防ぐ
  • Batch Normalizationは学習を安定化し、高速化する
  • Dropoutはランダムにニューロンを無効化し、汎化性能を向上させる
  • 複数の技法を組み合わせることで、最高の性能が得られる
  • 実践的なプロジェクトを通じて、実世界のデータに対応する力を身につける

💡 次章への繋がり

第4章ではCNNの性能向上技法を学びました。次の第5章では、転移学習(Transfer Learning)を学びます。事前学習された強力なモデル(VGG、ResNet、Inception等)を活用することで、少ないデータでも高精度なモデルを構築できる技術を習得します。