Bottleneck構造あり vs なし|1×1 Convの役割をCIFAR-10で実験検証【Keras】

投稿日:2026年5月19日火曜日 最終更新日:

CIFAR-10 CNN Google Colab Keras ResNet 画像分類

X f B! P L
Bottleneck構造あり vs なし|1×1 Convの役割をCIFAR-10で実験検証【Keras】 アイキャッチ画像

Bottleneckブロックとは何か

ResNet50などの深いCNNで使われる Bottleneck Block(ボトルネックブロック)は、
1×1 Conv → 3×3 Conv → 1×1 Conv という3層構造が特徴です。

「なぜわざわざ1×1 Convで挟むのか?」——今回はその疑問にKerasとCIFAR-10の実験で答えます。

📘 この記事でわかること
  • Bottleneckブロックと通常ブロック(3×3→3×3)の精度・パラメータ数の違い
  • 1×1 Convがチャンネル数削減(次元圧縮)にどう機能するか
  • CIFAR-10での実験結果と、どちらを使うべきかの判断基準

Bottleneckブロックの仕組み

通常の 3×3 Conv × 2 ブロックと比べて、Bottleneckブロックでは1×1 Convでチャンネル数を一旦絞り(例:256→64)、3×3 Convで特徴を抽出し、また1×1 Convで元のチャンネル数に戻す(64→256)構造になっています。

ブロック種別 構成 特徴
通常ブロック(BasicBlock) 3×3 Conv → BN → ReLU → 3×3 Conv → BN → ReLU シンプル。ResNet18/34で使用
Bottleneckブロック 1×1 Conv → BN → ReLU → 3×3 Conv → BN → ReLU → 1×1 Conv → BN → ReLU パラメータ効率が高い。ResNet50以降で使用

パラメータ数の比較(入出力ともにチャンネル数=64の場合)

以下の数式で、どちらのブロックがパラメータ効率に優れているかを確認します。

通常ブロック:\( 3 \times 3 \times 64 \times 64 \times 2 = 73{,}728 \) パラメータ

Bottleneckブロック(中間ch=16): \( 1 \times 1 \times 64 \times 16 + 3 \times 3 \times 16 \times 16 + 1 \times 1 \times 16 \times 64 = 1{,}024 + 2{,}304 + 1{,}024 = 4{,}352 \) パラメータ

中間チャンネル数を元の1/4に絞るだけで、パラメータ数が 73,728 → 4,352(約17分の1)になります。これがBottleneckの「効率化」の正体です。


実験コード

使用環境はGoogle Colab(GPU:T4)、データセットはCIFAR-10です。
ブロックの種類(通常 vs Bottleneck)以外の条件はすべて同一にして、ブロック構造の影響だけを取り出します。

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

# ── 環境準備(最初に一度だけ実行)──────────────────────
!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 51 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 1s (14.5 MB/s)
Selecting previously unselected package fonts-ipafont-gothic.
(Reading database ... 122412 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 68.1 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
from tensorflow.keras import layers
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

# ── 通常ブロック(BasicBlock)────────────────────────
def basic_block(x, filters):
    """3×3 Conv → BN → ReLU → 3×3 Conv → BN → ReLU"""
    x = layers.Conv2D(filters, (3, 3), padding='same')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation('relu')(x)
    x = layers.Conv2D(filters, (3, 3), padding='same')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation('relu')(x)
    return x

# ── Bottleneckブロック ────────────────────────────────
def bottleneck_block(x, filters, bottleneck_ratio=4):
    """1×1 Conv(圧縮)→ 3×3 Conv → 1×1 Conv(復元)"""
    mid_filters = filters // bottleneck_ratio  # 中間チャンネル数を1/4に絞る
    x = layers.Conv2D(mid_filters, (1, 1), padding='same')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation('relu')(x)
    x = layers.Conv2D(mid_filters, (3, 3), padding='same')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation('relu')(x)
    x = layers.Conv2D(filters, (1, 1), padding='same')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation('relu')(x)
    return x
実行結果をクリックして内容を開く
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170498071/170498071 ━━━━━━━━━━━━━━━━━━━━ 19s 0us/step

モデル構築関数

def build_model(block_type, name):
    """
    block_type: 'basic' or 'bottleneck'
    基本構成:Conv2D(64) → block × 2 → GAP → Dense(128) → Dense(10)
    """
    inputs = keras.Input(shape=(32, 32, 3))

    # ── Stem ──────────────────────────────────────────
    x = layers.Conv2D(64, (3, 3), padding='same', activation='relu')(inputs)
    x = layers.MaxPooling2D((2, 2))(x)  # 32×32 → 16×16

    # ── ブロック1(チャンネル数=64)────────────────
    if block_type == 'basic':
        x = basic_block(x, filters=64)
    else:
        x = bottleneck_block(x, filters=64, bottleneck_ratio=4)
    x = layers.MaxPooling2D((2, 2))(x)  # 16×16 → 8×8

    # ── ブロック2(チャンネル数=128)───────────────
    x = layers.Conv2D(128, (1, 1), padding='same', activation='relu')(x)  # チャンネル調整
    if block_type == 'basic':
        x = basic_block(x, filters=128)
    else:
        x = bottleneck_block(x, filters=128, bottleneck_ratio=4)

    # ── 分類ヘッド ────────────────────────────────
    x = layers.GlobalAveragePooling2D()(x)
    x = layers.Dense(128, activation='relu')(x)
    x = layers.Dropout(0.3)(x)
    outputs = layers.Dense(10, activation='softmax')(x)

    return keras.Model(inputs, outputs, 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

2パターンの学習実行

configs = [('basic', 'A_BasicBlock'), ('bottleneck', 'B_Bottleneck')]
histories, times, scores, params = {}, {}, {}, {}

for block_type, name in configs:
    print(f"\n=== {name} ===")
    model = build_model(block_type, name)
    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}")
実行結果をクリックして内容を開く
=== A_BasicBlock ===
Model: "A_BasicBlock"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ input_layer (InputLayer)        │ (None, 32, 32, 3)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d (Conv2D)                 │ (None, 32, 32, 64)     │         1,792 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d (MaxPooling2D)    │ (None, 16, 16, 64)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_1 (Conv2D)               │ (None, 16, 16, 64)     │        36,928 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization             │ (None, 16, 16, 64)     │           256 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation (Activation)         │ (None, 16, 16, 64)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_2 (Conv2D)               │ (None, 16, 16, 64)     │        36,928 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_1           │ (None, 16, 16, 64)     │           256 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_1 (Activation)       │ (None, 16, 16, 64)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d_1 (MaxPooling2D)  │ (None, 8, 8, 64)       │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_3 (Conv2D)               │ (None, 8, 8, 128)      │         8,320 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_4 (Conv2D)               │ (None, 8, 8, 128)      │       147,584 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_2           │ (None, 8, 8, 128)      │           512 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_2 (Activation)       │ (None, 8, 8, 128)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_5 (Conv2D)               │ (None, 8, 8, 128)      │       147,584 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_3           │ (None, 8, 8, 128)      │           512 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_3 (Activation)       │ (None, 8, 8, 128)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ global_average_pooling2d        │ (None, 128)            │             0 │
│ (GlobalAveragePooling2D)        │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (Dense)                   │ (None, 128)            │        16,512 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout (Dropout)               │ (None, 128)            │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_1 (Dense)                 │ (None, 10)             │         1,290 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 398,474 (1.52 MB)
 Trainable params: 397,706 (1.52 MB)
 Non-trainable params: 768 (3.00 KB)
Epoch 1/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 18s 13ms/step - accuracy: 0.4863 - loss: 1.4094 - val_accuracy: 0.2659 - val_loss: 3.7784
Epoch 2/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.6327 - loss: 1.0355 - val_accuracy: 0.5171 - val_loss: 1.5101
Epoch 3/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.6935 - loss: 0.8702 - val_accuracy: 0.5230 - val_loss: 1.5263
Epoch 4/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 13s 13ms/step - accuracy: 0.7348 - loss: 0.7586 - val_accuracy: 0.5045 - val_loss: 1.6176
Epoch 5/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.7668 - loss: 0.6708 - val_accuracy: 0.7146 - val_loss: 0.8415
Epoch 6/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.7949 - loss: 0.5996 - val_accuracy: 0.7208 - val_loss: 0.8216
Epoch 7/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.8152 - loss: 0.5429 - val_accuracy: 0.4143 - val_loss: 3.2660
Epoch 8/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.8314 - loss: 0.4896 - val_accuracy: 0.6451 - val_loss: 1.2804
Epoch 9/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.8462 - loss: 0.4476 - val_accuracy: 0.7197 - val_loss: 0.8899
Epoch 10/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.8626 - loss: 0.4055 - val_accuracy: 0.6665 - val_loss: 1.2212
Epoch 11/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.8756 - loss: 0.3643 - val_accuracy: 0.7352 - val_loss: 0.8489
Epoch 12/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.8854 - loss: 0.3285 - val_accuracy: 0.7086 - val_loss: 0.9562
Epoch 13/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.8990 - loss: 0.2947 - val_accuracy: 0.6374 - val_loss: 1.4856
Epoch 14/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9053 - loss: 0.2742 - val_accuracy: 0.6918 - val_loss: 1.3367
Epoch 15/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 10ms/step - accuracy: 0.9161 - loss: 0.2439 - val_accuracy: 0.6992 - val_loss: 1.0969
Epoch 16/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9228 - loss: 0.2232 - val_accuracy: 0.7556 - val_loss: 0.9372
Epoch 17/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9296 - loss: 0.2042 - val_accuracy: 0.7368 - val_loss: 0.9605
Epoch 18/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9377 - loss: 0.1788 - val_accuracy: 0.7740 - val_loss: 0.9235
Epoch 19/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9431 - loss: 0.1616 - val_accuracy: 0.7700 - val_loss: 0.9609
Epoch 20/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9469 - loss: 0.1512 - val_accuracy: 0.7423 - val_loss: 1.0588
Epoch 21/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9516 - loss: 0.1383 - val_accuracy: 0.7651 - val_loss: 1.0501
Epoch 22/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9549 - loss: 0.1317 - val_accuracy: 0.7217 - val_loss: 1.3963
Epoch 23/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9600 - loss: 0.1167 - val_accuracy: 0.7964 - val_loss: 0.8658
Epoch 24/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9574 - loss: 0.1216 - val_accuracy: 0.7455 - val_loss: 1.1590
Epoch 25/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 11s 17ms/step - accuracy: 0.9639 - loss: 0.1029 - val_accuracy: 0.7546 - val_loss: 1.1165
Epoch 26/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9635 - loss: 0.1044 - val_accuracy: 0.7707 - val_loss: 1.0882
Epoch 27/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9664 - loss: 0.0966 - val_accuracy: 0.6994 - val_loss: 1.5352
Epoch 28/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9665 - loss: 0.0963 - val_accuracy: 0.6982 - val_loss: 1.6695
Epoch 29/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 9ms/step - accuracy: 0.9702 - loss: 0.0888 - val_accuracy: 0.7536 - val_loss: 1.2443
Epoch 30/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9687 - loss: 0.0902 - val_accuracy: 0.7818 - val_loss: 1.0543
学習時間:195.8秒 パラメータ数:398,474 test_accuracy:0.7785

=== B_Bottleneck ===
Model: "B_Bottleneck"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ input_layer_1 (InputLayer)      │ (None, 32, 32, 3)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_6 (Conv2D)               │ (None, 32, 32, 64)     │         1,792 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d_2 (MaxPooling2D)  │ (None, 16, 16, 64)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_7 (Conv2D)               │ (None, 16, 16, 16)     │         1,040 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_4           │ (None, 16, 16, 16)     │            64 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_4 (Activation)       │ (None, 16, 16, 16)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_8 (Conv2D)               │ (None, 16, 16, 16)     │         2,320 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_5           │ (None, 16, 16, 16)     │            64 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_5 (Activation)       │ (None, 16, 16, 16)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_9 (Conv2D)               │ (None, 16, 16, 64)     │         1,088 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_6           │ (None, 16, 16, 64)     │           256 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_6 (Activation)       │ (None, 16, 16, 64)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d_3 (MaxPooling2D)  │ (None, 8, 8, 64)       │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_10 (Conv2D)              │ (None, 8, 8, 128)      │         8,320 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_11 (Conv2D)              │ (None, 8, 8, 32)       │         4,128 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_7           │ (None, 8, 8, 32)       │           128 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_7 (Activation)       │ (None, 8, 8, 32)       │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_12 (Conv2D)              │ (None, 8, 8, 32)       │         9,248 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_8           │ (None, 8, 8, 32)       │           128 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_8 (Activation)       │ (None, 8, 8, 32)       │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_13 (Conv2D)              │ (None, 8, 8, 128)      │         4,224 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_9           │ (None, 8, 8, 128)      │           512 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_9 (Activation)       │ (None, 8, 8, 128)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ global_average_pooling2d_1      │ (None, 128)            │             0 │
│ (GlobalAveragePooling2D)        │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_2 (Dense)                 │ (None, 128)            │        16,512 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_1 (Dropout)             │ (None, 128)            │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_3 (Dense)                 │ (None, 10)             │         1,290 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 51,114 (199.66 KB)
 Trainable params: 50,538 (197.41 KB)
 Non-trainable params: 576 (2.25 KB)
Epoch 1/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 17s 10ms/step - accuracy: 0.4027 - loss: 1.6041 - val_accuracy: 0.3976 - val_loss: 1.7501
Epoch 2/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.5341 - loss: 1.2828 - val_accuracy: 0.4322 - val_loss: 1.6924
Epoch 3/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.5809 - loss: 1.1565 - val_accuracy: 0.5051 - val_loss: 1.3669
Epoch 4/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6130 - loss: 1.0786 - val_accuracy: 0.3781 - val_loss: 2.2135
Epoch 5/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6349 - loss: 1.0174 - val_accuracy: 0.6350 - val_loss: 1.0163
Epoch 6/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6500 - loss: 0.9808 - val_accuracy: 0.4637 - val_loss: 1.7326
Epoch 7/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6646 - loss: 0.9391 - val_accuracy: 0.5927 - val_loss: 1.1507
Epoch 8/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6761 - loss: 0.9093 - val_accuracy: 0.6290 - val_loss: 1.0843
Epoch 9/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.6866 - loss: 0.8858 - val_accuracy: 0.5321 - val_loss: 1.3941
Epoch 10/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.6970 - loss: 0.8552 - val_accuracy: 0.6704 - val_loss: 0.9351
Epoch 11/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.7042 - loss: 0.8367 - val_accuracy: 0.6025 - val_loss: 1.1794
Epoch 12/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.7089 - loss: 0.8180 - val_accuracy: 0.5437 - val_loss: 1.3317
Epoch 13/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 9s 7ms/step - accuracy: 0.7170 - loss: 0.7974 - val_accuracy: 0.6414 - val_loss: 1.0077
Epoch 14/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 7ms/step - accuracy: 0.7245 - loss: 0.7777 - val_accuracy: 0.6430 - val_loss: 1.0549
Epoch 15/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.7315 - loss: 0.7625 - val_accuracy: 0.6754 - val_loss: 0.9299
Epoch 16/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.7366 - loss: 0.7437 - val_accuracy: 0.6101 - val_loss: 1.1652
Epoch 17/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.7427 - loss: 0.7272 - val_accuracy: 0.4741 - val_loss: 1.7742
Epoch 18/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.7444 - loss: 0.7246 - val_accuracy: 0.7017 - val_loss: 0.8625
Epoch 19/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.7502 - loss: 0.7089 - val_accuracy: 0.6816 - val_loss: 0.9275
Epoch 20/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 8ms/step - accuracy: 0.7597 - loss: 0.6878 - val_accuracy: 0.5762 - val_loss: 1.3045
Epoch 21/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.7620 - loss: 0.6800 - val_accuracy: 0.6877 - val_loss: 0.9322
Epoch 22/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.7639 - loss: 0.6674 - val_accuracy: 0.5456 - val_loss: 1.6379
Epoch 23/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.7679 - loss: 0.6566 - val_accuracy: 0.6950 - val_loss: 0.9152
Epoch 24/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.7744 - loss: 0.6431 - val_accuracy: 0.6957 - val_loss: 0.8840
Epoch 25/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 7ms/step - accuracy: 0.7764 - loss: 0.6343 - val_accuracy: 0.6848 - val_loss: 0.9153
Epoch 26/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 7ms/step - accuracy: 0.7824 - loss: 0.6238 - val_accuracy: 0.7120 - val_loss: 0.8494
Epoch 27/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.7827 - loss: 0.6131 - val_accuracy: 0.6849 - val_loss: 0.9568
Epoch 28/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 7ms/step - accuracy: 0.7872 - loss: 0.6100 - val_accuracy: 0.6953 - val_loss: 0.9193
Epoch 29/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 5s 7ms/step - accuracy: 0.7883 - loss: 0.5940 - val_accuracy: 0.6975 - val_loss: 0.8833
Epoch 30/30
625/625 ━━━━━━━━━━━━━━━━━━━━ 4s 7ms/step - accuracy: 0.7944 - loss: 0.5860 - val_accuracy: 0.6823 - val_loss: 0.9614
学習時間:154.8秒 パラメータ数:51,114 test_accuracy:0.6784

グラフ+サマリー

# ── val_accuracy / val_loss 比較グラフ ───────────────
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('bottleneck_comparison.png', dpi=150)
plt.show()

# ── train_loss vs val_loss(過学習の乖離)────────────
fig2, axes2 = plt.subplots(2, 1, figsize=(7, 10))
for i, (label, h) in enumerate(histories.items()):
    axes2[i].plot(h.history['loss'],     label='train_loss')
    axes2[i].plot(h.history['val_loss'], label='val_loss')
    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('bottleneck_overfit.png', dpi=150)
plt.show()

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

最終結果サマリー

===== 最終結果サマリー =====
       Pattern |  Val Acc |  Test Acc |  Time(s) |       Params
--------------------------------------------------------------
    BasicBlock |   0.7818 |    0.7785 |    195.8 |      398,474
    Bottleneck |   0.6823 |    0.6784 |    154.8 |       51,114
--------------------------------------------------------------

実験結果

精度グラフ

精度グラフ

損失グラフ

損失グラフ

BasicBlock

BasicBlock

Bottleneck

Bottleneck
パターン 最終 val_accuracy 最終 test_accuracy パラメータ数 学習時間
A:BasicBlock(通常) 78.18% 77.85% 398,474 195.8秒
B:Bottleneck 68.23% 67.84% 51,114 154.8秒

精度は BasicBlock が +10.01% 上回った一方、パラメータ数は Bottleneck が約 1/8(51,114 vs 398,474)、学習時間も約 41秒短縮という結果になりました。


考察

① BasicBlockが+10%の大差をつけた——なぜか?

今回の実験では BasicBlock(77.85%)が Bottleneck(67.84%)を約10ポイント上回るという明確な差がつきました。

原因は 浅いネットワークでの情報圧縮のしすぎにあります。Bottleneckでは中間チャンネルを1/4(例:64→16)に絞っているため、3×3 Convが処理できる特徴の豊かさが大きく制限されます。ブロックが2つしかない今回のような浅い構成では、この圧縮がそのまま表現力の低下に直結してしまいます。

一方 BasicBlock は 3×3 Conv をフルチャンネルのまま2回かけるため、特徴抽出の能力がそのまま活かされます。パラメータ数は Bottleneck の約8倍(398,474 vs 51,114)ですが、それに見合う精度を出しているとも言えます。

② 1×1 Convは「チャンネル方向の全結合」——その限界も

1×1 Convは空間方向には何も処理せず、チャンネル方向だけを線形変換します。これはチャンネル間の情報を混合しつつ次元数を変えるため、「チャンネル方向の全結合層」と捉えると直感的です。

ただし、この圧縮・復元の操作は 情報のロスを伴う可能性があります。深いネットワークでは多段の圧縮・復元を繰り返すことで最終的に十分な表現が構築されますが、今回のような2ブロック構成では圧縮の恩恵よりもロスの影響の方が大きく出たと考えられます。

③ Bottleneckのパラメータ効率は本物——ただし「深さ」が前提

パラメータ数を見ると、Bottleneck(51,114)は BasicBlock(398,474)の約 1/8 です。学習時間も41秒短縮されています。このパラメータ効率の高さ自体は本物で、ResNet50が50層もの深さを実現できるのはBottleneckのおかげです。

ネットワーク ブロック種別 層数 なぜその選択か
ResNet18 / 34 BasicBlock 18〜34層 浅いのでフルチャンネルの表現力を活かす
ResNet50 / 101 / 152 Bottleneck 50〜152層 深くしてもパラメータ爆発を防ぐ

Bottleneckは「少ないパラメータで深さを稼ぐ」ための技術であり、今回のように「浅いまま使う」ことは本来の使い方ではありません。

④ 今回の実験で言えること・言えないこと

今回の実験は 「浅いネットワーク単体でどちらが精度が出るか」という比較です。スキップ接続(Residual接続)なし・ブロック数2という条件下では BasicBlock が有利という結果になりましたが、これは「Bottleneckは使えない」という意味ではありません。

実際の用途では以下の点が重要です。

  • 推論速度・モデルサイズを優先するなら:Bottleneck(組み込み・モバイル向け)
  • 精度優先で浅いネットワークを構築するなら:BasicBlock
  • 深いネットワークで精度とパラメータ効率を両立するなら:Bottleneck + スキップ接続
⚠️ ハマりポイント
  • bottleneck_ratio の設定:中間チャンネル数を元の1/4(ratio=4)にするのが ResNet の標準設定。ratio が大きすぎると情報が失われ、小さすぎるとパラメータ削減の効果が薄れます。
  • skip connection(残差接続)を入れていない:本記事の実験はBottleneckの構造そのものの効果を確認するため、あえてスキップ接続なしで比較しています。実際のResNetではスキップ接続が必須です。スキップ接続の効果は → Residual接続あり vs なし の比較 もご覧ください。
  • チャンネル数が少ない場合は効果が出にくい:チャンネル数が32以下だと1/4に絞っても8チャンネルになり、3×3 Convの表現力が著しく低下します。Bottleneckは64チャンネル以上での使用が目安です。
まとめ
  • Bottleneckブロックは 1×1→3×3→1×1 の3層構造で、チャンネルを一旦圧縮してから復元することでパラメータ数を大幅に削減する(今回の実験では BasicBlock の約1/8)
  • しかし 浅いネットワーク(今回は2ブロック)では Bottleneck の精度が BasicBlock を約10ポイント下回った——チャンネル圧縮による情報ロスが表現力の低下に直結するため
  • Bottleneckが真価を発揮するのは 50層以上の深いネットワーク。ResNet50以降がBottleneckを採用している理由はここにある
  • 「軽量・高速」を優先するならBottleneck、「浅いネットワークで精度」を優先するならBasicBlockが有利
  • 実運用では スキップ接続と組み合わせる(ResNet50スタイル)ことで、Bottleneckの圧縮による精度低下を補完できる

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