Conv2Dの層数を変えると精度はどう変わる?(1層 vs 2層 vs 3層)【Keras×CIFAR-10実験】

投稿日:2026年4月28日火曜日 最終更新日:

CIFAR-10 CNN Conv2D Google Colab Keras 画像分類 層数

X f B! P L
Conv2Dの層数を変えると精度はどう変わる?(1層 vs 2層 vs 3層)【Keras×CIFAR-10実験】 アイキャッチ画像

CNNを設計するとき、Conv2Dの層数をいくつにすればいいか迷ったことはありませんか?

「層を増やせば精度が上がるはず」——直感的にはそう思えますが、実際はどうでしょうか?今回はGoogle ColabとCIFAR-10を使い、Conv2D層を1層・2層・3層の3パターンで比較しました。

なお、MNISTでの層数比較は → CNNの層数で精度はどう変わる?1層・2層・3層モデルをKerasで比較検証 をご覧ください。本記事はCIFAR-10・GAP構成でより踏み込んだ比較を行います。

📘 この記事でわかること

  • Conv2D層を1層・2層・3層に変えると精度・パラメータ数・学習時間がどう変わるか
  • 層を増やすことで特徴抽出がどう変わるか
  • CIFAR-10+GAP構成での最適な層数の目安

層数を変えると何が起きるか

Conv2D層を増やすほど、モデルは段階的により複雑な特徴を抽出できるようになります。

層数 抽出できる特徴 パラメータ数 リスク
1層 エッジ・色などの低レベル特徴のみ 少ない 表現力不足で精度が低くなりやすい
2層 低レベル+中レベル特徴(テクスチャ・形状) 中程度 CIFAR-10ではバランスが良い
3層 低〜高レベル特徴(物体の部位・構造) 多い 過学習・特徴マップの縮小しすぎに注意

特にCIFAR-10は32×32と解像度が低いため、MaxPoolingを挟みながら層を増やすと特徴マップが急速に縮小します。3層構成では慎重な設計が必要です。

各モデルの特徴マップサイズの変化(padding='same'・MaxPooling=(2,2)の場合)

A:1層モデル B:2層モデル C:3層モデル
入力 32×32×3 32×32×3 32×32×3
Conv2D(1回目)+Pool後 16×16×64 16×16×64 16×16×64
Conv2D(2回目)+Pool後 8×8×128 8×8×128
Conv2D(3回目)+Pool後 4×4×256
GAP後 64次元 128次元 256次元

実験コード

使用環境はGoogle Colab(GPU:T4)、データセットはCIFAR-10です。Conv2D層数以外の条件(フィルター数・kernel_size・padding・Optimizer・エポック数・バッチサイズ)は全て同一にして、層数の影響だけを取り出します。

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

# ── 環境準備(最初に一度だけ実行)──────────────────────
!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 42 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 2s (3,661 kB/s)
Selecting previously unselected package fonts-ipafont-gothic.
(Reading database ... 122354 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 51.8 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 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

def build_model_1layer(name):
    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.GlobalAveragePooling2D(),
        keras.layers.Dense(128, activation='relu'),
        keras.layers.Dropout(0.2),
        keras.layers.Dense(10, activation='softmax'),
    ], name=name)

def build_model_2layer(name):
    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(128, activation='relu'),
        keras.layers.Dropout(0.2),
        keras.layers.Dense(10, activation='softmax'),
    ], name=name)

def build_model_3layer(name):
    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.Conv2D(256, (3, 3), activation='relu', padding='same'),
        keras.layers.MaxPooling2D((2, 2)),
        keras.layers.GlobalAveragePooling2D(),
        keras.layers.Dense(128, activation='relu'),
        keras.layers.Dropout(0.2),
        keras.layers.Dense(10, activation='softmax'),
    ], name=name)

def compile_and_fit(model):
    model.compile(optimizer='adam',
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    start = time.time()
    history = model.fit(x_train, y_train, epochs=30, batch_size=64,
                        validation_split=0.2, verbose=1)
    return history, time.time() - start
実行結果をクリックして内容を開く

3パターンの学習実行

configs = [
    (build_model_1layer, 'A_1layer'),
    (build_model_2layer, 'B_2layer'),
    (build_model_3layer, 'C_3layer'),
]
histories, times, scores, params = {}, {}, {}, {}

for build_fn, name in configs:
    print(f"\n=== {name} ===")
    model = build_fn(name)
    print(model.summary())
    h, t = compile_and_fit(model)
    s = model.evaluate(x_test, y_test, verbose=0)
    label = name.split('_')[1]
    histories[label] = h
    times[label] = t
    scores[label] = s
    params[label] = model.count_params()
    print(f"学習時間:{t:.1f}秒 パラメータ数:{model.count_params():,} test_accuracy:{s[1]:.4f}")
実行結果をクリックして内容を開く
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170498071/170498071 ━━━━━━━━━━━━━━━━━━━━ 14s 0us/step

グラフ+サマリー

fig, axes = plt.subplots(1, 2, figsize=(14, 5))
for label, h in histories.items():
    axes[0].plot(h.history['val_accuracy'], label=label)
    axes[1].plot(h.history['val_loss'],     label=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('conv2d_layers_comparison.png', dpi=150)
plt.show()

print("\n===== 最終結果サマリー =====")
print(f"{'Pattern':>8} | {'Val Acc':>8} | {'Test Acc':>9} | {'Time(s)':>8} | {'Params':>12}")
print("-" * 56)
for label in ['1layer', '2layer', '3layer']:
    val_acc  = histories[label].history['val_accuracy'][-1]
    test_acc = scores[label][1]
    t        = times[label]
    p        = params[label]
    print(f"{label:>8} | {val_acc:>8.4f} | {test_acc:>9.4f} | {t:>8.1f} | {'p':>12}")
print("-" * 56)

結果サマリ

===== 最終結果サマリー =====
 Pattern |  Val Acc |  Test Acc |  Time(s) |   Params
--------------------------------------------------------
  1layer |   0.4803 |    0.4761 |    106.0 |    11402
  2layer |   0.6531 |    0.6598 |    122.1 |    93450
  3layer |   0.7630 |    0.7610 |    152.5 |   405002
--------------------------------------------------------

実験結果

精度グラフ

精度グラフ

損失グラフ

損失グラフ
パターン 最終 val_accuracy 最終 test_accuracy パラメータ数 学習時間
A:Conv2D 1層 48.03% 47.61% 11,402 106.0秒
B:Conv2D 2層 65.31% 65.98% 93,450 122.1秒
C:Conv2D 3層 76.30% 76.10% 405,002 152.5秒

考察

① 層を増やすほど精度が一貫して上がった

今回の実験では層数を増やすほど精度が明確に向上するという、直感通りの結果になりました。

比較 精度の向上 パラメータ増加 学習時間増加
1層 → 2層 +18.37% 約8.2倍(11,402 → 93,450) +16.1秒(約15%)
2層 → 3層 +10.12% 約4.3倍(93,450 → 405,002) +30.4秒(約25%)

特に1層→2層の精度向上(+18.37%)が最も大きく、コストパフォーマンスも高いことがわかります。パラメータ数は8.2倍増えますが、学習時間はわずか15%増に留まっています。

② 1層では表現力が根本的に不足する

1層のtest_accuracy(47.61%)は、CIFAR-10の10クラスランダム分類(10%)よりは高いものの、実用的な水準には到達していません

1層のConv2Dが抽出できるのはエッジや色のグラデーションなど低レベルの特徴のみです。CIFAR-10のような「飛行機・自動車・鳥…」という10クラスを識別するには、テクスチャ・形状・物体の構造といった中〜高レベルの特徴が必要で、1層では根本的に表現力が足りません。パラメータ数が11,402と少ない(GAPが64次元しか出力しない)ことも精度の低さに直結しています。

③ 3層が最高精度——ただしパラメータ数が急増

3層のtest_accuracy(76.10%)は2層(65.98%)を約10%上回り、今回の実験での最高精度です。3層目のConv2D(256フィルター)が高レベルの特徴(物体の部位・構造など)を抽出することで、2層では捉えられなかったパターンを学習できた結果です。

ただしパラメータ数は405,002と2層の約4.3倍です。これはGAP後のベクトルが256次元(2層の128次元の2倍)になり、Dense(128)のパラメータが(256+1)×128 = 32,896から(256+1)×128を超える計算になるためです。過学習のリスクも高まるため、train_lossとval_lossの乖離をグラフで確認することが重要です。

④ 学習時間は層数に比例しない

パラメータ数は1層→3層で約35倍に増えていますが、学習時間は106.0秒→152.5秒と約1.4倍に留まっています。これはGPU(T4)の並列処理効率によるもので、パラメータ数が増えても行列演算がGPU上で効率よく処理されるためです。「パラメータが増えると学習時間も同じ割合で増える」という思い込みは修正が必要です。

⑤ val_lossの逆転——3層が2層を途中で追い抜く

損失グラフを見ると、学習の途中で3層のval_lossが2層のval_lossを下回る逆転が起きています。これは興味深い現象です。

序盤(Ep1〜10前後)では3層のval_lossは2層より高く推移します。これは3層モデルの方がパラメータが多く、収束に時間がかかるためです。しかし中盤以降(Ep15前後)から3層が急速に改善し、最終的に2層を大きく下回ります。

この逆転が示しているのは、「序盤の損失が高い=悪いモデル」ではないということです。複雑なモデルほど収束に時間がかかりますが、十分なエポック数を与えれば最終的にシンプルなモデルを上回ります。早い段階でEarlyStoppingのpatienceが短すぎると、3層モデルを途中で打ち切ってしまうリスクがあります。今回EarlyStopping無しで30エポック回しきったことで、3層モデルの本来の性能が引き出せた結果とも言えます。


まとめ

  • 3層が最高精度(76.10%)。層を増やすほど精度が一貫して向上する明快な結果に
  • 1層→2層の精度向上(+18.37%)が最大かつ最もコスパが良い。学習時間増加はわずか15%
  • 1層(47.61%)は表現力が根本的に不足。CIFAR-10レベルでは実用的でない
  • 3層はパラメータ数が2層の約4.3倍(405,002)まで増加。過学習対策(Dropout・EarlyStopping)との組み合わせが重要
  • 学習時間はパラメータ数に比例せず、GPUの並列処理で効率的に処理される(1層の1.4倍に留まった)
  • CIFAR-10+GAP構成では精度優先なら3層・バランス重視なら2層が実用的な選択
  • 損失グラフでは3層のval_lossが序盤は高く中盤以降に逆転。複雑なモデルは収束に時間がかかるためpatienceの設定に注意

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