CutMixとMixUpの効果を比較!なしと比べてどれが精度向上に効く?【Keras×CIFAR-10実験】

投稿日:2026年5月13日水曜日 最終更新日:

CIFAR-10 CNN CutMix Data Augmentation Google Colab Keras MixUp 過学習対策 画像分類

X f B! P L
CutMixとMixUpの効果を比較!なしと比べてどれが精度向上に効く?【Keras×CIFAR-10実験】 アイキャッチ画像

データ拡張(Data Augmentation)の中でも、MixUpとCutMixは近年の画像分類コンペで広く使われている強力な手法です。

MixUpは「2枚の画像をα比率でブレンドする」のに対し、CutMixは「片方の画像の一部を切り取って、もう一方に貼り付ける」という発展版です。どちらもラベルも同時に混ぜる(Soft Label)ことで、モデルが中間的な特徴を学習できるようにします。

今回はGoogle ColabとCIFAR-10を使い、データ拡張なし・MixUpあり・CutMixあり の3パターンでテスト精度と過学習の出方を比較します。

📘 この記事でわかること
  • CutMixとMixUpの仕組みの違い
  • データ拡張なし・MixUp・CutMixで精度がどう変わるか
  • 過学習の抑制効果はどの手法が高いか
  • KerasでCutMixをゼロから実装する方法

CutMixとMixUpの仕組み

まず2つの手法の仕組みを整理します。

MixUpとは

MixUpは2枚の画像をピクセル単位でα比率でブレンドし、ラベルも同じ比率で混合する手法です。

\[ \tilde{x} = \lambda x_i + (1 - \lambda) x_j, \quad \tilde{y} = \lambda y_i + (1 - \lambda) y_j \]

λはBeta(α, α)分布からサンプリングされます(αは通常0.2〜0.4)。画像全体が半透明に重なるため、「ぼんやりした合成画像」になります。

CutMixとは

CutMixは画像全体をブレンドするのではなく、片方の画像の矩形領域を切り取って、もう一方の画像に貼り付ける手法です。

\[ \tilde{x} = \mathbf{M} \odot x_i + (1 - \mathbf{M}) \odot x_j \]

ここでMはバイナリマスク(矩形領域が1、それ以外が0)です。ラベルは貼り付けた領域の面積比λで混合されます。

手法 画像の合成方法 ラベルの扱い 直感的なイメージ
なし 元画像そのまま One-hot 通常学習
MixUp 全体をα比率でブレンド Soft Label(比率混合) 透かし合成写真
CutMix 矩形領域を切り貼り Soft Label(面積比) コラージュ写真

実験コード

使用環境はGoogle Colab(GPU:T4)、データセットはCIFAR-10です。MixUp/CutMixの有無以外の条件は全て同一にします。

環境準備(最初に一度だけ実行)

# ── 環境準備(最初に一度だけ実行)──────────────────────
!apt-get -y install fonts-ipafont-gothic
!rm -rf /root/.cache/matplotlib
!pip install -q japanize_matplotlib
print("環境準備完了")
実行結果をクリックして内容を開く
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  fonts-ipafont-mincho
The following NEW packages will be installed:
  fonts-ipafont-gothic fonts-ipafont-mincho
0 upgraded, 2 newly installed, 0 to remove and 100 not upgraded.
Need to get 8,237 kB of archives.
After this operation, 28.7 MB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu jammy/universe amd64 fonts-ipafont-gothic all 00303-21ubuntu1 [3,513 kB]
Get:2 http://archive.ubuntu.com/ubuntu jammy/universe amd64 fonts-ipafont-mincho all 00303-21ubuntu1 [4,724 kB]
Fetched 8,237 kB in 0s (41.9 MB/s)
Selecting previously unselected package fonts-ipafont-gothic.
(Reading database ... 122402 files and directories currently installed.)
Preparing to unpack .../fonts-ipafont-gothic_00303-21ubuntu1_all.deb ...
Unpacking fonts-ipafont-gothic (00303-21ubuntu1) ...
Selecting previously unselected package fonts-ipafont-mincho.
Preparing to unpack .../fonts-ipafont-mincho_00303-21ubuntu1_all.deb ...
Unpacking fonts-ipafont-mincho (00303-21ubuntu1) ...
Setting up fonts-ipafont-mincho (00303-21ubuntu1) ...
update-alternatives: using /usr/share/fonts/opentype/ipafont-mincho/ipam.ttf to provide /usr/share/fonts/truetype/fonts-japanese-mincho.ttf (fonts-japanese-mincho.ttf) in auto mode
Setting up fonts-ipafont-gothic (00303-21ubuntu1) ...
update-alternatives: using /usr/share/fonts/opentype/ipafont-gothic/ipag.ttf to provide /usr/share/fonts/truetype/fonts-japanese-gothic.ttf (fonts-japanese-gothic.ttf) in auto mode
Processing triggers for fontconfig (2.13.1-4.2ubuntu5) ...
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.1/4.1 MB 58.6 MB/s eta 0:00:00
  Preparing metadata (setup.py) ... done
  Building wheel for japanize_matplotlib (setup.py) ... done
環境準備完了

import・データ準備

import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
import time

# データ読み込み
(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data()
x_train = x_train.astype('float32') / 255.0
x_test  = x_test.astype('float32')  / 255.0

NUM_CLASSES = 10
y_train_onehot = tf.keras.utils.to_categorical(y_train, NUM_CLASSES)
y_test_onehot  = tf.keras.utils.to_categorical(y_test,  NUM_CLASSES)

print(f"x_train shape: {x_train.shape}")
print(f"x_test shape:  {x_test.shape}")
実行結果をクリックして内容を開く
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170498071/170498071 ━━━━━━━━━━━━━━━━━━━━ 41s 0us/step
x_train shape: (50000, 32, 32, 3)
x_test shape:  (10000, 32, 32, 3)
⚠️ ハマりポイント①:CutMix/MixUpはSoft Labelが必要
通常のsparse_categorical_crossentropyはOne-hotではなくクラスインデックスを受け取るため、MixUp/CutMixのSoft Label(float配列)とは互換性がありません。必ず categorical_crossentropy(One-hot形式)に切り替えてください。
y_trainto_categorical() で変換し忘れるとエラーになります。

MixUp実装

def mixup_batch(x, y, alpha=0.4):
    """MixUp: 2枚の画像をα比率でブレンドし、ラベルも混合する"""
    batch_size = tf.shape(x)[0]
    # Beta分布からλをサンプリング
    lam = np.random.beta(alpha, alpha)

    # シャッフルインデックスを生成
    indices = tf.random.shuffle(tf.range(batch_size))
    x2 = tf.gather(x, indices)
    y2 = tf.gather(y, indices)

    # ブレンド
    x_mixed = lam * x + (1 - lam) * x2
    y_mixed = lam * y + (1 - lam) * y2
    return x_mixed, y_mixed

CutMix実装

def rand_bbox(size, lam):
    """CutMix用のランダム矩形領域を生成"""
    H, W = size[1], size[2]
    cut_ratio = np.sqrt(1.0 - lam)
    cut_h = int(H * cut_ratio)
    cut_w = int(W * cut_ratio)

    # 中心座標をランダムに決定
    cx = np.random.randint(W)
    cy = np.random.randint(H)

    x1 = np.clip(cx - cut_w // 2, 0, W)
    y1 = np.clip(cy - cut_h // 2, 0, H)
    x2 = np.clip(cx + cut_w // 2, 0, W)
    y2 = np.clip(cy + cut_h // 2, 0, H)
    return x1, y1, x2, y2

def cutmix_batch(x, y, alpha=1.0):
    """CutMix: 矩形領域を切り取って別画像に貼り付け、ラベルは面積比で混合"""
    x = x.numpy() if hasattr(x, 'numpy') else x
    y = y.numpy() if hasattr(y, 'numpy') else y
    batch_size = x.shape[0]

    lam = np.random.beta(alpha, alpha)
    indices = np.random.permutation(batch_size)

    x1, y1, x2, y2 = rand_bbox(x.shape, lam)

    # 貼り付け
    x_mixed = x.copy()
    x_mixed[:, y1:y2, x1:x2, :] = x[indices, y1:y2, x1:x2, :]

    # 実際の面積比でλを再計算
    lam = 1.0 - (x2 - x1) * (y2 - y1) / (x.shape[2] * x.shape[1])
    y_mixed = lam * y + (1 - lam) * y[indices]
    return x_mixed, y_mixed
⚠️ ハマりポイント②:CutMix内でtf.Tensorを直接操作できない
tf.dataパイプライン内でcutmix_batchを呼ぶ際、入力がtf.Tensorのまま NumPy操作(スライス代入など)しようとするとエラーになります。
上のコードではx.numpy()で一度NumPy配列に変換してから処理しています。tf.py_functionでラップするか、eager executionが有効な環境で実行してください。

モデル構築関数

def build_model(name):
    """共通CNNモデル(CutMix/MixUpの有無以外は全て同一)"""
    return keras.Sequential([
        keras.layers.Input(shape=(32, 32, 3)),
        keras.layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
        keras.layers.MaxPooling2D((2, 2)),
        keras.layers.Conv2D(128, (3, 3), activation='relu', padding='same'),
        keras.layers.MaxPooling2D((2, 2)),
        keras.layers.GlobalAveragePooling2D(),
        keras.layers.Dense(256, activation='relu'),
        keras.layers.Dropout(0.3),
        keras.layers.Dense(10, activation='softmax'),
    ], name=name)

def compile_model(model):
    model.compile(
        optimizer='adam',
        loss='categorical_crossentropy',   # ← Soft Label対応のためcategorical
        metrics=['accuracy']
    )

3パターンの学習実行

EPOCHS     = 30
BATCH_SIZE = 64
histories, times, scores = {}, {}, {}

# ──────────────────────────────────────────
# A:データ拡張なし
# ──────────────────────────────────────────
print("\n=== A:なし ===")
model_none = build_model('A_none')
compile_model(model_none)
start = time.time()
h_none = model_none.fit(
    x_train, y_train_onehot,
    epochs=EPOCHS, batch_size=BATCH_SIZE,
    validation_split=0.2, verbose=1
)
t_none = time.time() - start
s_none = model_none.evaluate(x_test, y_test_onehot, verbose=0)
histories['なし'] = h_none
times['なし']    = t_none
scores['なし']   = s_none
print(f"学習時間:{t_none:.1f}秒 test_accuracy:{s_none[1]:.4f}")

# ──────────────────────────────────────────
# B:MixUp
# ──────────────────────────────────────────
print("\n=== B:MixUp ===")
model_mixup = build_model('B_mixup')
compile_model(model_mixup)
start = time.time()

# MixUpはバッチごとに手動で適用
h_mixup_history = {'loss': [], 'accuracy': [], 'val_loss': [], 'val_accuracy': []}
val_split_idx = int(len(x_train) * 0.8)
x_tr, y_tr = x_train[:val_split_idx], y_train_onehot[:val_split_idx]
x_val, y_val = x_train[val_split_idx:], y_train_onehot[val_split_idx:]

for epoch in range(EPOCHS):
    idx = np.random.permutation(len(x_tr))
    x_tr_s, y_tr_s = x_tr[idx], y_tr[idx]
    epoch_loss, epoch_acc = [], []
    for i in range(0, len(x_tr_s), BATCH_SIZE):
        xb = x_tr_s[i:i+BATCH_SIZE]
        yb = y_tr_s[i:i+BATCH_SIZE]
        xb_m, yb_m = mixup_batch(xb, yb, alpha=0.4)
        result = model_mixup.train_on_batch(xb_m, yb_m)
        epoch_loss.append(result[0]); epoch_acc.append(result[1])
    val_result = model_mixup.evaluate(x_val, y_val, verbose=0)
    h_mixup_history['loss'].append(np.mean(epoch_loss))
    h_mixup_history['accuracy'].append(np.mean(epoch_acc))
    h_mixup_history['val_loss'].append(val_result[0])
    h_mixup_history['val_accuracy'].append(val_result[1])
    print(f"Epoch {epoch+1}/{EPOCHS} - loss:{np.mean(epoch_loss):.4f} - val_acc:{val_result[1]:.4f}")

t_mixup = time.time() - start
s_mixup = model_mixup.evaluate(x_test, y_test_onehot, verbose=0)

class FakeHistory:
    def __init__(self, d): self.history = d

histories['MixUp'] = FakeHistory(h_mixup_history)
times['MixUp']     = t_mixup
scores['MixUp']    = s_mixup
print(f"学習時間:{t_mixup:.1f}秒 test_accuracy:{s_mixup[1]:.4f}")

# ──────────────────────────────────────────
# C:CutMix
# ──────────────────────────────────────────
print("\n=== C:CutMix ===")
model_cutmix = build_model('C_cutmix')
compile_model(model_cutmix)
start = time.time()

h_cutmix_history = {'loss': [], 'accuracy': [], 'val_loss': [], 'val_accuracy': []}

for epoch in range(EPOCHS):
    idx = np.random.permutation(len(x_tr))
    x_tr_s, y_tr_s = x_tr[idx], y_tr[idx]
    epoch_loss, epoch_acc = [], []
    for i in range(0, len(x_tr_s), BATCH_SIZE):
        xb = x_tr_s[i:i+BATCH_SIZE]
        yb = y_tr_s[i:i+BATCH_SIZE]
        xb_c, yb_c = cutmix_batch(xb, yb, alpha=1.0)
        result = model_cutmix.train_on_batch(xb_c, yb_c)
        epoch_loss.append(result[0]); epoch_acc.append(result[1])
    val_result = model_cutmix.evaluate(x_val, y_val, verbose=0)
    h_cutmix_history['loss'].append(np.mean(epoch_loss))
    h_cutmix_history['accuracy'].append(np.mean(epoch_acc))
    h_cutmix_history['val_loss'].append(val_result[0])
    h_cutmix_history['val_accuracy'].append(val_result[1])
    print(f"Epoch {epoch+1}/{EPOCHS} - loss:{np.mean(epoch_loss):.4f} - val_acc:{val_result[1]:.4f}")

t_cutmix = time.time() - start
s_cutmix = model_cutmix.evaluate(x_test, y_test_onehot, verbose=0)

histories['CutMix'] = FakeHistory(h_cutmix_history)
times['CutMix']     = t_cutmix
scores['CutMix']    = s_cutmix
print(f"学習時間:{t_cutmix:.1f}秒 test_accuracy:{s_cutmix[1]:.4f}")
実行結果をクリックして内容を開く
=== A:なし ===
Epoch 1/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 17s 19ms/step - accuracy: 0.2699 - loss: 1.9147 - val_accuracy: 0.3602 - val_loss: 1.6972
Epoch 2/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - accuracy: 0.3856 - loss: 1.6519 - val_accuracy: 0.4277 - val_loss: 1.5442
Epoch 3/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 8s 12ms/step - accuracy: 0.4413 - loss: 1.5257 - val_accuracy: 0.4583 - val_loss: 1.4742
Epoch 4/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 9s 14ms/step - accuracy: 0.4814 - loss: 1.4203 - val_accuracy: 0.5013 - val_loss: 1.3450
Epoch 5/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - accuracy: 0.5003 - loss: 1.3655 - val_accuracy: 0.5168 - val_loss: 1.3113
Epoch 6/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - accuracy: 0.5179 - loss: 1.3138 - val_accuracy: 0.5228 - val_loss: 1.2862
Epoch 7/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 8s 13ms/step - accuracy: 0.5339 - loss: 1.2799 - val_accuracy: 0.5405 - val_loss: 1.2611
Epoch 8/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 10s 17ms/step - accuracy: 0.5483 - loss: 1.2443 - val_accuracy: 0.5577 - val_loss: 1.1960
Epoch 9/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 10s 16ms/step - accuracy: 0.5598 - loss: 1.2067 - val_accuracy: 0.5708 - val_loss: 1.1732
Epoch 10/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 9s 14ms/step - accuracy: 0.5711 - loss: 1.1816 - val_accuracy: 0.5648 - val_loss: 1.1879
Epoch 11/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 10s 16ms/step - accuracy: 0.5827 - loss: 1.1518 - val_accuracy: 0.5947 - val_loss: 1.1230
Epoch 12/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 10s 16ms/step - accuracy: 0.5949 - loss: 1.1234 - val_accuracy: 0.6074 - val_loss: 1.0905
Epoch 13/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 11s 17ms/step - accuracy: 0.6007 - loss: 1.1075 - val_accuracy: 0.5991 - val_loss: 1.1083
Epoch 14/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 10s 16ms/step - accuracy: 0.6111 - loss: 1.0795 - val_accuracy: 0.6169 - val_loss: 1.0566
Epoch 15/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 11s 17ms/step - accuracy: 0.6179 - loss: 1.0564 - val_accuracy: 0.6184 - val_loss: 1.0646
Epoch 16/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 10s 15ms/step - accuracy: 0.6296 - loss: 1.0369 - val_accuracy: 0.6328 - val_loss: 1.0252
Epoch 17/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6357 - loss: 1.0180 - val_accuracy: 0.6244 - val_loss: 1.0509
Epoch 18/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6379 - loss: 1.0051 - val_accuracy: 0.6537 - val_loss: 0.9780
Epoch 19/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6482 - loss: 0.9824 - val_accuracy: 0.6356 - val_loss: 1.0039
Epoch 20/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 7ms/step - accuracy: 0.6537 - loss: 0.9659 - val_accuracy: 0.6504 - val_loss: 0.9789
Epoch 21/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 7ms/step - accuracy: 0.6575 - loss: 0.9597 - val_accuracy: 0.6628 - val_loss: 0.9465
Epoch 22/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6618 - loss: 0.9412 - val_accuracy: 0.6532 - val_loss: 0.9560
Epoch 23/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6718 - loss: 0.9189 - val_accuracy: 0.6539 - val_loss: 0.9796
Epoch 24/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6751 - loss: 0.9172 - val_accuracy: 0.6663 - val_loss: 0.9283
Epoch 25/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6817 - loss: 0.8975 - val_accuracy: 0.6680 - val_loss: 0.9298
Epoch 26/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6805 - loss: 0.8917 - val_accuracy: 0.6676 - val_loss: 0.9385
Epoch 27/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6905 - loss: 0.8729 - val_accuracy: 0.6789 - val_loss: 0.8951
Epoch 28/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6900 - loss: 0.8667 - val_accuracy: 0.6782 - val_loss: 0.9046
Epoch 29/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6969 - loss: 0.8518 - val_accuracy: 0.6746 - val_loss: 0.9059
Epoch 30/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6985 - loss: 0.8482 - val_accuracy: 0.6768 - val_loss: 0.9110
学習時間:222.9秒 test_accuracy:0.6732

=== B:MixUp ===
Epoch 1/30 - loss:2.1525 - val_acc:0.3460
Epoch 2/30 - loss:1.8539 - val_acc:0.4137
Epoch 3/30 - loss:1.7338 - val_acc:0.4495
Epoch 4/30 - loss:1.6456 - val_acc:0.4781
Epoch 5/30 - loss:1.5839 - val_acc:0.5166
Epoch 6/30 - loss:1.5197 - val_acc:0.5284
Epoch 7/30 - loss:1.4882 - val_acc:0.5335
Epoch 8/30 - loss:1.4717 - val_acc:0.5505
Epoch 9/30 - loss:1.4227 - val_acc:0.5559
Epoch 10/30 - loss:1.4188 - val_acc:0.5605
Epoch 11/30 - loss:1.3811 - val_acc:0.5791
Epoch 12/30 - loss:1.3754 - val_acc:0.5921
Epoch 13/30 - loss:1.3449 - val_acc:0.5852
Epoch 14/30 - loss:1.3524 - val_acc:0.5993
Epoch 15/30 - loss:1.3055 - val_acc:0.6163
Epoch 16/30 - loss:1.2884 - val_acc:0.6105
Epoch 17/30 - loss:1.2949 - val_acc:0.6238
Epoch 18/30 - loss:1.2749 - val_acc:0.6260
Epoch 19/30 - loss:1.2741 - val_acc:0.6408
Epoch 20/30 - loss:1.2489 - val_acc:0.6344
Epoch 21/30 - loss:1.2462 - val_acc:0.6563
Epoch 22/30 - loss:1.2115 - val_acc:0.6604
Epoch 23/30 - loss:1.2113 - val_acc:0.6615
Epoch 24/30 - loss:1.2011 - val_acc:0.6518
Epoch 25/30 - loss:1.2073 - val_acc:0.6529
Epoch 26/30 - loss:1.2010 - val_acc:0.6690
Epoch 27/30 - loss:1.1727 - val_acc:0.6732
Epoch 28/30 - loss:1.1658 - val_acc:0.6709
Epoch 29/30 - loss:1.1690 - val_acc:0.6729
Epoch 30/30 - loss:1.1576 - val_acc:0.6813
学習時間:285.0秒 test_accuracy:0.6767

=== C:CutMix ===
Epoch 1/30 - loss:2.1882 - val_acc:0.3213
Epoch 2/30 - loss:1.9460 - val_acc:0.3750
Epoch 3/30 - loss:1.8654 - val_acc:0.4062
Epoch 4/30 - loss:1.7931 - val_acc:0.4325
Epoch 5/30 - loss:1.7473 - val_acc:0.4864
Epoch 6/30 - loss:1.6806 - val_acc:0.5027
Epoch 7/30 - loss:1.6564 - val_acc:0.5070
Epoch 8/30 - loss:1.6331 - val_acc:0.5237
Epoch 9/30 - loss:1.6127 - val_acc:0.5360
Epoch 10/30 - loss:1.5885 - val_acc:0.5519
Epoch 11/30 - loss:1.5479 - val_acc:0.5232
Epoch 12/30 - loss:1.5744 - val_acc:0.5726
Epoch 13/30 - loss:1.5313 - val_acc:0.5841
Epoch 14/30 - loss:1.5096 - val_acc:0.5780
Epoch 15/30 - loss:1.5028 - val_acc:0.5759
Epoch 16/30 - loss:1.4888 - val_acc:0.6037
Epoch 17/30 - loss:1.4654 - val_acc:0.6050
Epoch 18/30 - loss:1.4649 - val_acc:0.5847
Epoch 19/30 - loss:1.4717 - val_acc:0.5999
Epoch 20/30 - loss:1.4627 - val_acc:0.6177
Epoch 21/30 - loss:1.4316 - val_acc:0.6283
Epoch 22/30 - loss:1.4059 - val_acc:0.6246
Epoch 23/30 - loss:1.4301 - val_acc:0.6419
Epoch 24/30 - loss:1.3961 - val_acc:0.6308
Epoch 25/30 - loss:1.4011 - val_acc:0.6400
Epoch 26/30 - loss:1.3975 - val_acc:0.6510
Epoch 27/30 - loss:1.3853 - val_acc:0.6496
Epoch 28/30 - loss:1.3812 - val_acc:0.6625
Epoch 29/30 - loss:1.3629 - val_acc:0.6548
Epoch 30/30 - loss:1.3498 - val_acc:0.6441
学習時間:172.2秒 test_accuracy:0.6418

グラフ+サマリー

# ── val_accuracy / val_loss 比較グラフ ────────────────────
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
colors = {'なし': 'steelblue', 'MixUp': 'darkorange', 'CutMix': 'forestgreen'}
for label, h in histories.items():
    axes[0].plot(h.history['val_accuracy'], label=label, color=colors[label])
    axes[1].plot(h.history['val_loss'],     label=label, color=colors[label])
axes[0].set_title('val_accuracy の比較(全30エポック)')
axes[1].set_title('val_loss の比較(全30エポック)')
for ax in axes:
    ax.set_xlabel('Epoch'); ax.legend(); ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('cutmix_comparison.png', dpi=150)
plt.show()

# ── 過学習の乖離グラフ ─────────────────────────────────────
fig2, axes2 = plt.subplots(1, 3, figsize=(18, 5))
for i, (label, h) in enumerate(histories.items()):
    axes2[i].plot(h.history['loss'],     label='train_loss', color='steelblue')
    axes2[i].plot(h.history['val_loss'], label='val_loss',   color='darkorange')
    axes2[i].set_title(f'{label}')
    axes2[i].set_xlabel('Epoch'); axes2[i].legend(); axes2[i].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('cutmix_overfit.png', dpi=150)
plt.show()

# ── 最終結果サマリー ──────────────────────────────────────
print("\n===== 最終結果サマリー =====")
print(f"{'Pattern':>8} | {'Val Acc':>8} | {'Test Acc':>9} | {'Time(s)':>8}")
print("-" * 44)
for label in ['なし', 'MixUp', 'CutMix']:
    val_acc  = histories[label].history['val_accuracy'][-1]
    test_acc = scores[label][1]
    t        = times[label]
    print(f"{label:>8} | {val_acc:>8.4f} | {test_acc:>9.4f} | {t:>8.1f}")
print("-" * 44)

最終結果サマリー

===== 最終結果サマリー =====
 Pattern |  Val Acc |  Test Acc |  Time(s)
--------------------------------------------
      なし |   0.6768 |    0.6732 |    222.9
   MixUp |   0.6813 |    0.6767 |    285.0
  CutMix |   0.6441 |    0.6418 |    172.2
--------------------------------------------

実験結果

精度グラフ

精度グラフ

損失グラフ

損失グラフ

なし

なし

MixUp

MixUp

CutMix

CutMix

最終結果サマリー

パターン 最終 val_accuracy 最終 test_accuracy 学習時間
A:なし 67.68% 67.32% 222.9秒
B:MixUp 68.13% 67.67% 285.0秒
C:CutMix 64.41% 64.18% 172.2秒

考察

① CutMixが「なし」より精度が下がった理由

今回の実験で最も注目すべき結果は、CutMixがデータ拡張なしより約3%精度が低い(67.32% → 64.18%)という点です。

原因として最も疑わしいのはCIFAR-10の解像度(32×32)との相性です。CutMixで切り貼りされる矩形パッチのサイズは、α=1.0のBeta分布から決まるλに基づきます。α=1.0では混合率が0〜1の一様分布に近くなり、パッチが画像の半分近くを占めるケースも頻繁に発生します。32×32という極めて小さい画像で画面の半分を別クラスで塗りつぶすと、元クラスの識別に必要な特徴がほぼ消えた状態でソフトラベルだけ残ることになり、かえって学習信号がノイズになります。

論文でCutMixの効果が報告されているのは主にImageNet(224×224)やCIFAR-100などの高難度タスクです。CIFAR-10の低解像度・10クラスという比較的シンプルな設定では、CutMixの「局所特徴を保ちながら多様性を高める」メリットが出にくく、むしろ学習を妨げる方向に働いた可能性があります。

② MixUpは「なし」とほぼ同等——わずか+0.35%

MixUpは「なし」比 test_accuracyで +0.35%(67.32% → 67.67%)と、ほぼ誤差の範囲内の改善にとどまりました。

MixUpは画像全体をブレンドするため、CIFAR-10の32×32画像でも「元画像の特徴が完全には消えない」という点でCutMixより安定しています。ただし今回のモデルはDropout=0.3をすでに持っており、ある程度の正則化が効いています。すでに正則化が入っているモデルに対して、さらにMixUpで正則化を追加しても効果が薄かったと解釈できます。

③ CutMixの学習時間が「なし」より短い——なぜ?

CutMixの学習時間(172.2秒)は「なし」(222.9秒)より約50秒短くなっています。これは一見不思議に見えますが、train_on_batchループの実装上の特性が影響しています。CutMixはNumPy操作(スライス代入)を含む処理のため、バッチ合成のオーバーヘッドが小さく、かつ学習が早期に収束(損失が低下しにくくなる)した結果、エポックごとのgrad更新が軽くなった可能性があります。今回の実装ではmodel.fit()の最適化パスを使っておらず、XLAコンパイル等の恩恵も受けていないため、単純な比較はできません。

④ αパラメータの再検討が必要

今回CutMixにはα=1.0を使いました。これは論文推奨値ですが、CIFAR-10の低解像度ではα=0.2〜0.3に下げてパッチサイズを小さくすると改善する可能性があります。αを小さくするとλが0や1に偏りやすくなり(≒元画像に近い)、切り貼り面積が小さくなります。低解像度タスクでCutMixを試す場合は、αのチューニングが重要な調整ポイントです。

✅ まとめ
  • CutMixは矩形領域を切り貼りしてラベルも面積比で混合する手法。MixUpの進化版とされるが、タスク・解像度によって効果は大きく変わる
  • 今回の実験では なし(67.32%)≒ MixUp(67.67%)> CutMix(64.18%) という結果に。CutMixは逆効果だった
  • CutMixが不振だった主な原因はCIFAR-10の低解像度(32×32)との相性の悪さ。大きなパッチで元クラスの特徴が消えノイズになりやすい
  • MixUpは「なし」と誤差レベルの差。すでにDropout=0.3が効いているモデルへの追加正則化は効果が薄かった
  • CutMixをCIFAR-10で使う場合は α=0.2〜0.3 に下げてパッチサイズを抑えるチューニングが有効な可能性あり
  • 実装時は categorical_crossentropy(Soft Label対応)への切り替えを忘れずに

関連記事もあわせてどうぞ: