Day 133 — カスタムアーキテクチャ設計③ — Squeeze-and-Exciteブロックの自作設計

2026-08-23 金 / Phase 6(Grandmaster、テーマ2/5群 の 3日目) コーディング Squeeze-and-Excitation / ボトルネック構造 / チャネル重要度

📚 背景知識(読んでから問題へ)

🏁
Phase6テーマ2「カスタムアーキテクチャ設計」の3日目です。Day131ではH(x) = F(x) + xというResidual Connectionを、Day132では配分比率を学習可能にしたH(x) = T(x)F(x) + (1-T(x))xというGated MLPを実装しました。この2つはどちらも「F(x)をどれくらい足すか」という配分の話でした。今日はまた違う角度から、「変換結果F(x)の各チャネルをどれくらい信じるか」を学習するSqueeze-and-Excitation(SE)ブロック(Hu et al., 2018、CVPR 2018 Best Paper Award)を実装します。
🖼️
画像向けSEブロックの元々のアイデア: CNNでは画像が「チャネル×高さ×幅」のTensorとして扱われる。SEブロックはまず各チャネルを空間方向に平均するSqueeze(圧縮)で1つの数値にまとめ、その後ボトルネック構造の小さなネットワークとシグモイドで各チャネルの重要度スコア(Excitation)を計算し、元のチャネルに掛け算する。今日扱う表形式データには「空間方向」が存在しないため、Squeezeのステップは実質スキップし、Excitationだけを実装する(元論文の忠実な再現ではなく表形式向けの簡略化であることを明示)。
入力 x
→
変換 F(x)
→
Excitation
dim→dim/r→dim
+sigmoid
→
H(x)=x+s・F(x)

今日実装するSqueeze-and-Excitation(簡略版)の核心の数式:

H(x) = x + S(F(x)) ⊙ F(x)

ボトルネック構造とreduction比率: Excitationの2層ネットワークはdim → dim/r → dimという形で一度圧縮してから元の次元に戻す。Day132のGated MLPが独立したdim×dimのフル結合層でゲートを計算したのに対し、SEブロックはパラメータ数を絞りながらチャネル別重要度を計算する設計思想の違いがある。

🪄 今日の比較の設計(4種類のブロック × reduction 2パターン)

a

PlainMLP

Day131と同じ対照群。正規化なし・Skipなし(劣化問題の再現用)

b

ResMLP

Day131のResidualMLPBlock。H(x)=F(x)+x(配分は固定・1:1)

c

GatedMLP

Day132のGatedMLPBlock。H(x)=T(x)F(x)+(1-T(x))x(配分は学習可能)

d

SEMLP

今日新規実装。H(x)=x+S(F(x))*F(x)(チャネル別の信頼度を学習)

SEMLPBlockはResidualMLPBlockのF(x)部分をそのまま流用し、return x + hをreturn x + s * hに置き換えたものと捉えると理解しやすい。今日はさらにreduction(ボトルネックの圧縮率)を4と16の2パターンで比較し、パラメータ数と精度のトレードオフを見る。

🎯 問題

Day131・Day132と同系統の合成分類データ・共通骨格(DeepNet)を使い、これまでのResidualMLPBlock・GatedMLPBlockに加えて新しくSEMLPBlockを実装し、比較します。

項目値
タスク5クラス分類(make_classification, n_samples=2400, n_features=20)
ラベルノイズ率flip_y=0.03(3%程度)
train / test先頭1800件 / 残り600件
ブロック内部次元HIDDEN=64(全ブロック共通)
比較対象PlainMLP / ResMLP(Day131) / GatedMLP(Day132) / SEMLP(新規, reduction可変)
評価軸test精度 + 1ブロックあたりの追加パラメータ数 + チャネル別平均重要度
import numpy as np
from sklearn.datasets import make_classification

# --- 合成データ生成(Day131・Day132と同一) ---
X, y = make_classification(
    n_samples=2400, n_features=20, n_informative=12,
    n_classes=5, n_clusters_per_class=1, random_state=42,
    flip_y=0.03,
)

TRAIN_LEN = 1800   # 先頭1800件をtrain、残り600件をtest
HIDDEN = 64        # ブロック内部の次元(全ブロック共通)
# 今日比較する4種類:
#   (a) PlainMLP  : Day131と同じ(正規化・Skipなし、劣化問題の対照群)
#   (b) ResMLP    : Day131のResidualMLPBlock(H(x) = F(x) + x)
#   (c) GatedMLP  : Day132のGatedMLPBlock(H(x) = T(x)F(x) + (1-T(x))x、gate_bias_init=-3)
#   (d) SEMLP     : 今日新規実装。H(x) = x + S(F(x)) * F(x)(S(F(x))はチャネルごとの重要度スコア)
1
SEMLPBlockの自作設計(メイン): dim次元の入出力を持つブロックをnn.Moduleとして実装。F(x)はDay131/132と同構成、F(x)自体をチャネル記述子として使い(Squeeze省略)、dim→dim/r→dimのボトルネック構造(Linear→ReLU→Linear→sigmoid)で重要度スコアsを計算、x + s*F(x)を返す
2
reduction比率の指定: reduction引数でボトルネックの圧縮率を指定可能にする(reduced=max(1, dim // reduction))。reduction=4と16の2パターンを比較
3
パラメータ数の比較(メインの設計判断): ResMLP・GatedMLP・SEMLP(r=4)・SEMLP(r=16)について、1ブロックあたりの追加パラメータ数(ゲート/SE機構のみ)を計算し比較する
4
4種類のネットワークの学習と比較(N_BLOCKS=20固定): PlainMLP・ResMLP・GatedMLP(gate_bias_init=-3)・SEMLP(reduction=4)をepochs=60で学習し、test精度を比較する
5
reduction比率の比較実験: SEMLPについてreduction=4と16の2パターンで学習し、test精度とパラメータ数のトレードオフを確認する
6
チャネル重要度の可視化: 学習済みSEMLP(reduction=4)の最終ブロックにおける各チャネルの平均重要度スコアsを計算し、重要度が高い/低いチャネルの傾向を確認する
7
(考察): SEブロックが「配分」ではなく「チャネルごとの信頼度」を学習する違いが何をもたらしたか、ボトルネック構造のパラメータ削減がなぜ有効だったかを100字程度で結論づける

💡 ヒント

ヒント1方向性

SEMLPBlockは、Day131のResidualMLPBlockのreturn x + hという行を、return x + s * hに置き換えたものだと考えると理解しやすいです(hはF(x)の意味)。Day132のGatedMLPBlockが「xとF(x)のどちらをどれくらい使うか」という混合比率を学習したのに対し、SEMLPBlockは「F(x)の中の各チャネルを、そのままxに足す前にどれくらい弱める/強めるか」というF(x)自体へのフィルタを学習する点が違います。sはt(Day132のゲート)と似た0〜1のTensorですが、役割は「配分」ではなく「重要度によるスケーリング」です。

ヒント2アプローチ
  • Excitationのボトルネックはnn.Sequential(nn.Linear(dim, reduced), nn.ReLU(), nn.Linear(reduced, dim), nn.Sigmoid())のように書くと簡潔にまとまる
  • reduced = max(1, dim // reduction)のように、reductionが大きすぎて次元が0になることを防ぐmax(1, ...)を忘れないこと
  • パラメータ数を数えるにはsum(p.numel() for p in module.parameters())を使う。GatedMLPBlockのgate_fcはdim*dim + dim個、SEMLPBlockのExcitationは(dim*reduced+reduced) + (reduced*dim+dim)個
  • チャネル重要度を集計するには、SEMLPBlockに「直前のforwardで計算したsをチャネルごとに保持する属性(例: self.last_scale)」を持たせておくと集計しやすい
ヒント3コード骨格
import torch
import torch.nn as nn

class SEMLPBlock(nn.Module):
    def __init__(self, dim, reduction=4):
        super().__init__()
        self.norm1 = nn.LayerNorm(dim)
        self.fc1 = nn.Linear(dim, dim)
        self.act = nn.ReLU()
        self.norm2 = nn.LayerNorm(dim)
        self.fc2 = nn.Linear(dim, dim)
        reduced = max(1, dim // reduction)
        self.excitation = nn.Sequential(
            nn.Linear(dim, reduced),
            nn.ReLU(),
            nn.Linear(reduced, dim),
            nn.Sigmoid(),
        )
        self.last_scale = None   # 可視化用

    def forward(self, x):
        h = self.norm1(x)
        h = self.act(self.fc1(h))
        h = self.norm2(h)
        f = self.fc2(h)                       # F(x)
        s = self.excitation(f)                # チャネルごとの重要度スコア(0〜1)
        self.last_scale = s.mean(dim=0).detach()
        return x + s * f                      # Residual + チャネル別重み付けF(x)

✅ 模範解答

設計要素↔コード変数の対応表

設計上の要素役割コード上の変数
F(x)ブロック内部の変換(Day131/132と同じ構成)forward内のf
Squeeze画像ではGAPで圧縮するが、表形式ではf自体が既に1チャネル1値なので省略(実装不要)
Excitationボトルネック構造でチャネル別重要度を計算self.excitation
s=Excitation(F(x))各チャネルの重要度スコア(0〜1)forward内のs
H(x)=x+S(F(x))⊙F(x)ブロックの最終出力forwardの戻り値
reductionボトルネックの圧縮率(パラメータ数とのトレードオフ)reduced
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.datasets import make_classification
from torch.utils.data import TensorDataset, DataLoader

torch.manual_seed(0)

# --- サンプルデータ生成(Day131・Day132と同一) ---
X, y = make_classification(
    n_samples=2400, n_features=20, n_informative=12,
    n_classes=5, n_clusters_per_class=1, random_state=42, flip_y=0.03,
)
TRAIN_LEN = 1800
HIDDEN = 64

X_mean, X_std = X[:TRAIN_LEN].mean(axis=0), X[:TRAIN_LEN].std(axis=0)
X_scaled = (X - X_mean) / X_std

X_train = torch.tensor(X_scaled[:TRAIN_LEN], dtype=torch.float32)
y_train = torch.tensor(y[:TRAIN_LEN], dtype=torch.long)
X_test = torch.tensor(X_scaled[TRAIN_LEN:], dtype=torch.float32)
y_test = torch.tensor(y[TRAIN_LEN:], dtype=torch.long)

train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=32, shuffle=True)

# --- ステップ1&2: SEMLPBlockの自作設計(ボトルネックのreduction可変) ---
class SEMLPBlock(nn.Module):
    def __init__(self, dim, reduction=4):
        super().__init__()
        self.norm1 = nn.LayerNorm(dim)
        self.fc1 = nn.Linear(dim, dim)
        self.act = nn.ReLU()
        self.norm2 = nn.LayerNorm(dim)
        self.fc2 = nn.Linear(dim, dim)
        reduced = max(1, dim // reduction)
        self.excitation = nn.Sequential(
            nn.Linear(dim, reduced),
            nn.ReLU(),
            nn.Linear(reduced, dim),
            nn.Sigmoid(),
        )
        self.last_scale = None

    def forward(self, x):
        h = self.norm1(x)
        h = self.act(self.fc1(h))
        h = self.norm2(h)
        f = self.fc2(h)
        s = self.excitation(f)
        self.last_scale = s.mean(dim=0).detach()
        return x + s * f

class GatedMLPBlock(nn.Module):
    """Day132と同じ実装(比較対象)"""
    def __init__(self, dim, gate_bias_init=-3.0):
        super().__init__()
        self.norm1 = nn.LayerNorm(dim)
        self.fc1 = nn.Linear(dim, dim)
        self.act = nn.ReLU()
        self.norm2 = nn.LayerNorm(dim)
        self.fc2 = nn.Linear(dim, dim)
        self.gate_fc = nn.Linear(dim, dim)
        nn.init.constant_(self.gate_fc.bias, gate_bias_init)

    def forward(self, x):
        h = self.norm1(x)
        h = self.act(self.fc1(h))
        h = self.norm2(h)
        f = self.fc2(h)
        t = torch.sigmoid(self.gate_fc(x))
        return t * f + (1 - t) * x

class ResidualMLPBlock(nn.Module):
    """Day131と同じ実装(比較対象)"""
    def __init__(self, dim):
        super().__init__()
        self.norm1 = nn.LayerNorm(dim)
        self.fc1 = nn.Linear(dim, dim)
        self.act = nn.ReLU()
        self.norm2 = nn.LayerNorm(dim)
        self.fc2 = nn.Linear(dim, dim)

    def forward(self, x):
        h = self.norm1(x)
        h = self.act(self.fc1(h))
        h = self.norm2(h)
        h = self.fc2(h)
        return x + h

class PlainBlock(nn.Module):
    """Day131と同じ実装(劣化問題の対照群)"""
    def __init__(self, dim):
        super().__init__()
        self.fc1 = nn.Linear(dim, dim)
        self.act1 = nn.ReLU()
        self.fc2 = nn.Linear(dim, dim)
        self.act2 = nn.ReLU()

    def forward(self, x):
        h = self.act1(self.fc1(x))
        h = self.act2(self.fc2(h))
        return h

# --- ステップ3: 1ブロックあたりの追加パラメータ数(ゲート/SE機構のみ)比較 ---
def count_params(module):
    return sum(p.numel() for p in module.parameters())

dummy_gate = nn.Linear(HIDDEN, HIDDEN)
gate_extra = count_params(dummy_gate)

reduced4 = max(1, HIDDEN // 4)
reduced16 = max(1, HIDDEN // 16)
se4_extra = count_params(nn.Linear(HIDDEN, reduced4)) + count_params(nn.Linear(reduced4, HIDDEN))
se16_extra = count_params(nn.Linear(HIDDEN, reduced16)) + count_params(nn.Linear(reduced16, HIDDEN))
# 出力例:
# ResidualMLPBlock:        +0 個
# GatedMLPBlock:           +4160 個(64*64+64)
# SEMLPBlock(reduction=4): +2128 個(64*16+16 + 16*64+64)
# SEMLPBlock(reduction=16):+580 個(64*4+4 + 4*64+64)
# → SEブロックはボトルネックのおかげでGatedMLPより少ないパラメータで
#   チャネル別の重み付けを実現している

# --- ステップ4: 共通骨格を持つDeepNet ---
class DeepNet(nn.Module):
    def __init__(self, in_dim=20, hidden=HIDDEN, num_classes=5, n_blocks=20,
                 mode="se", gate_bias_init=-3.0, reduction=4):
        super().__init__()
        self.input_layer = nn.Linear(in_dim, hidden)
        if mode == "plain":
            self.blocks = nn.ModuleList([PlainBlock(hidden) for _ in range(n_blocks)])
        elif mode == "res":
            self.blocks = nn.ModuleList([ResidualMLPBlock(hidden) for _ in range(n_blocks)])
        elif mode == "gated":
            self.blocks = nn.ModuleList(
                [GatedMLPBlock(hidden, gate_bias_init=gate_bias_init) for _ in range(n_blocks)]
            )
        elif mode == "se":
            self.blocks = nn.ModuleList(
                [SEMLPBlock(hidden, reduction=reduction) for _ in range(n_blocks)]
            )
        else:
            raise ValueError(mode)
        self.output_layer = nn.Linear(hidden, num_classes)

    def forward(self, x):
        h = self.input_layer(x)
        for block in self.blocks:
            h = block(h)
        return self.output_layer(h)

def evaluate(model):
    model.eval()
    with torch.no_grad():
        preds = model(X_test).argmax(dim=1)
    return (preds == y_test).float().mean().item()

def train_and_measure(mode, n_blocks=20, epochs=60, lr=1e-3, gate_bias_init=-3.0, reduction=4):
    torch.manual_seed(0)
    model = DeepNet(n_blocks=n_blocks, mode=mode, gate_bias_init=gate_bias_init, reduction=reduction)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    for epoch in range(epochs):
        model.train()
        for xb, yb in train_loader:
            optimizer.zero_grad()
            loss = F.cross_entropy(model(xb), yb)
            loss.backward()
            optimizer.step()
    acc = evaluate(model)
    return model, acc

# --- ステップ4: PlainMLP・ResMLP・GatedMLP・SEMLP(r=4)の4者比較(N_BLOCKS=20) ---
for mode, kwargs in [("plain", {}), ("res", {}), ("gated", {"gate_bias_init": -3.0}), ("se", {"reduction": 4})]:
    model, acc = train_and_measure(mode, n_blocks=20, **kwargs)
    print(f"{mode:6s}: acc={acc:.4f}")
# 出力例(乱数・実行環境により変動):
# plain : acc=0.2117
# res   : acc=0.7217
# gated : acc=0.7350
# se    : acc=0.7417
# → SEMLPはGatedMLPよりわずかに精度が高く、しかもパラメータ数はGatedMLPの約半分(2128 vs 4160)

# --- ステップ5: reduction比率(4 vs 16)の比較 ---
for r in [4, 16]:
    _, acc = train_and_measure("se", n_blocks=20, reduction=r)
    extra = se4_extra if r == 4 else se16_extra
    print(f"reduction={r:2d}: acc={acc:.4f}  追加パラメータ数(1ブロック)={extra}")
# 出力例の傾向:
# reduction= 4: acc=0.7417  追加パラメータ数(1ブロック)=2128
# reduction=16: acc=0.7183  追加パラメータ数(1ブロック)=580
# → reduction=16はパラメータ数を4分の1近くまで削っても、resより高い精度を維持する

# --- ステップ6: チャネル重要度の可視化 ---
se_model, _ = train_and_measure("se", n_blocks=20, reduction=4)
se_model.eval()
with torch.no_grad():
    _ = se_model(X_test)
last_block_scale = se_model.blocks[-1].last_scale
sorted_idx = torch.argsort(last_block_scale, descending=True)
for rank, idx in enumerate(sorted_idx[:5], start=1):
    print(f"重要度上位{rank}: channel={idx.item():2d}  scale={last_block_scale[idx]:.4f}")
for rank, idx in enumerate(sorted_idx[-5:], start=1):
    print(f"重要度下位{rank}: channel={idx.item():2d}  scale={last_block_scale[idx]:.4f}")
# 出力例の傾向:
# 重要度上位1: channel=12  scale=0.8320
# 重要度下位1: channel= 3  scale=0.3512
# → 全チャネルが一様に0.5前後に収束するのではなく、明確な高低差(0.35〜0.83程度)が生じる
🧭
タスク7・考察: SEブロックは「入力を足すか(Residual)」「配分を混ぜるか(Gated)」ではなく「変換結果F(x)の各チャネルをどれだけ信じるか」を学習する点で異なり、Gated MLPよりわずかに高い精度をより少ないパラメータ(約半分)で達成した。ボトルネック構造は「重要度を計算するだけならdim×dimのフル結合は過剰」という発想に基づいており、reductionを16まで絞ってもresを上回る精度を維持できたことがそれを裏付けている。

📊 結果の可視化(パラメータ効率とチャネル重要度)

1ブロックあたりの追加パラメータ数(ゲート/SE機構のみ、F(x)本体は除く):

ブロック構成追加パラメータ数
ResidualMLPBlockなし0
GatedMLPBlockLinear(64,64)4,160
SEMLPBlock (reduction=4)64→16→642,128
SEMLPBlock (reduction=16)64→4→64580
4種のブロック比較: test精度(N_BLOCKS=20) Plain 0.212 ResMLP 0.722 GatedMLP 0.735 SEMLP 0.742 → SEMLPが最も高精度、かつGatedMLPの約半分のパラメータで実現
学習後: 最終ブロックのチャネル別重要度スコア(降順ソート・64チャネル中の抜粋) 0.0 1.0 上位1 下位1 ■ 明確な高低差(約0.35〜0.83)— 全チャネル一様(0.5前後)には収束していない → ネットワークが「常に信頼するチャネル」と「あまり信頼しないチャネル」を選択的に学習した結果
🔍
これらの図は模式的なイメージ(実際の数値は実行環境・乱数シードにより変動)。重要なのは「SEMLPがパラメータ効率よく最高精度を達成した」点と、「チャネル重要度が学習後に明確な高低差を持つ」という2つの構造である。

🪜 Step-by-Step 解説

1なぜ表形式データでは「Squeeze」のステップを省略できるのか

f = self.fc2(h)              # F(x)。shape: (batch, dim)
s = self.excitation(f)       # そのままExcitationへ。GAPなどの追加集約は不要
🔗
なぜこうするか: 元論文のSEブロックは、画像の「チャネル×高さ×幅」というTensorに対し、まず高さ・幅方向にGlobal Average Poolingをかけて「チャネル×1×1」に圧縮する(これがSqueeze)。この圧縮が必要なのは、画像1チャネルの中に何百もの画素値(空間的なばらつき)が存在し、それを1つの代表値にまとめる必要があるからである。一方、今日扱う表形式データのfは最初から(batch, dim)という形で、各チャネル(次元)はすでにちょうど1つの数値を持っている。つまり「圧縮する対象となる空間的な広がり」がそもそも存在しないため、Squeezeの作業自体が不要になる。これは「画像のためのテクニックを別のデータ構造に持ち込むときは、そのテクニックが解決していた問題が今のデータにも存在するかを確認すべき」という設計上の教訓の実例である。

2S(F(x)) * F(x)が「配分」ではなく「フィルタ」である理由

🧩
なぜこうするか: Day132のGatedMLPBlockのt * f + (1 - t) * xは、tが大きければf寄り、小さければx寄りという2つの経路の混合比率を表していた。一方、今日のs * fにはxとの混合という概念がなく、sは単に「fの各チャネルの値をどれだけ弱める(あるいはそのまま通す)か」というフィルタ・ゲインとして働く。数式上は似ているように見えるが、tが「xかf、どちらを選ぶか」を決める役割なのに対し、sは「f自体の中身をどれだけ信頼するか」を決める役割であり、両者は問いの立て方が異なる。この後+ xで無条件にxを足しているため、SEブロックは常にResidual Connectionの恩恵を保ちながら、F(x)の質だけを調整する設計になっている。

3ボトルネック構造がパラメータを削減しながら機能する理由

# GatedMLPBlockのgate_fc: dim -> dim(フル結合)           → dim*dim + dim 個
# SEMLPBlockのexcitation: dim -> dim/r -> dim(ボトルネック)→ 2*(dim*dim/r) + dim/r + dim 個
🎯
なぜこうするか: チャネルごとの重要度スコアという「dim個の数値」を出力するだけなら、理屈の上ではdim→dimのフル結合層1つで十分計算できる。しかしSEブロックは、その途中に一度dim/rという狭い層を挟むことで、「まずdim個のチャネル情報をdim/r個の要約された特徴に圧縮し、そこから再びdim個の重要度スコアを展開する」という構造を強制する。これは全結合層のパラメータを削減するだけでなく、「チャネルごとの重要度は、チャネル間の複雑な相互作用を要約した少数の潜在因子から決まるはずだ」という帰納バイアスを組み込んでいるとも解釈できる。reduction=16(ボトルネック幅わずか4)でもResidualより高い精度を維持できたのは、このボトルネックが極端に狭くても重要度スコアの計算には十分な情報量が残っていたことを示している。

4チャネル重要度の可視化が示す「学習された選択的注意」

⚖️
なぜこうするか: もしSEブロックが機能していなければ、last_scale(各チャネルの平均重要度)は全チャネルでほぼ同じ値(シグモイドの出力が偏りなく分布した状態)になるはずである。しかし実験では明確な高低差(例: 0.35〜0.83)が観察され、これは「一部のチャネルは常に強く通し、別の一部は弱めに抑える」という選択的な重み付けをネットワークが学習した証拠である。今日のデータは20次元のうち12次元だけが「informative」な特徴量であるように生成されているが、SEブロックの重要度スコアが完全にこの情報構造と一致するとは限らない(ブロックごとに中間表現の意味は変化するため)。それでも少なくとも「どのチャネルを信頼するか」という判断をネットワーク自身が特徴量ごとに下している、という点がAttention機構への橋渡しとして重要である。

🧮 数学・統計の補足(文系向け)

🎻
「Squeeze-and-Excitation」を、オーケストラの指揮者で理解する:

オーケストラには様々な楽器(チャネル)があり、演奏中どの楽器をどれくらいの音量で聞かせるかは、曲全体のバランスを見ながら指揮者が判断します。Excitation(励起)は、まさにこの指揮者の役割です。指揮者はまず全ての楽器の演奏(F(x)の各チャネル)を一度に見渡し(今日はSqueezeを省略しているので「見渡す」作業自体が既に完了している状態)、そこから「バイオリンはもう少し抑えめに、トランペットはもっと前に出して」という各楽器ごとの音量調整の指示(重要度スコアs)を出します。この指示に従って各楽器の音量を調整した演奏(s * F(x))を、常に流れている通奏低音(x、Residual接続)に重ねるのが、今日実装したx + s * F(x)という式のイメージです。
📝
「ボトルネック構造」を、要約メモの作成で理解する:

dim=64個のチャネルの情報を全部そのまま使って重要度を判断しようとすると(GatedMLPBlockのフル結合のように)、判断材料は多いですが計算コスト(パラメータ数)もかさみます。ボトルネック構造は、いったん64個の情報を「4個や16個の要約メモ」に凝縮してから、そのメモを見て64個それぞれへの評価を下す、というプロセスに近いです。優れた要約メモは、元の情報の要点を落とさずに圧縮できるため、判断の質はほとんど落ちません。今日の実験でreduction=16(要約メモがたった4行)でも十分な精度が出たのは、この「良い要約」がうまく機能した結果と言えます。

🏆 Kaggleでの実践的な使い方

よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 画像認識(CV) / ☐ 時系列(Time Series)

実例

🖼️ 画像コンペのバックボーンでの標準装備

EfficientNet等に組み込み済み

SEブロックはEfficientNetをはじめ多数のCNNアーキテクチャに組み込まれている定番の軽量モジュール。画像コンペでtimm系のモデルを使う際、内部で既にSE(-like)機構が動いていることが多い。

応用

🔍 特徴量重要度の「モデル内蔵版」として

SHAPとは別の観察軸

チャネル重要度の可視化は、SHAP値のような事後的な解釈手法とは別に、モデルが学習の過程で「どの特徴量由来のチャネルを信頼しているか」を直接観察できる。特徴量エンジニアリングの方向性を決める補助的な手がかりになりうる。

発展

🌉 Attention機構への自然な橋渡しとして

次回のAttentionへの導入

「重要度を動的に計算して掛け算する」という発想は、Self-Attentionが「どのトークンに注目するか」を動的に計算する仕組みと本質的に同じ系譜にある。次回学ぶAttentionブロックの前段階として理解しておくと導入がスムーズになる。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
SEブロックは常にGatedMLPより優れると思い込むパラメータが少なく精度も同等以上だった今日の結果から「効率的で万能」に見える今日の実験はこのデータ・設定における一例であり、タスクによってはGatedMLPの「配分の自由度」の方が有利な場合もある。SEブロックはxとF(x)の混合比率を制御できないため、恒等写像に近づきたい場合の柔軟性はGatedMLPより低い
Squeezeステップを省略したことを「実装のサボり」だと捉えてしまう元論文にあるステップを削っているため不完全な実装に見える画像のSqueezeは「空間方向の情報を1つに集約する」という明確な目的を持っており、その目的が表形式データには存在しない。目的のない工程を機械的にコピーせず、データの構造に応じて必要な部分だけを移植するのが正しい応用である
reductionを小さくする(ボトルネックを広くする)ほど必ず精度が上がると考えるパラメータが増えれば表現力も上がるはずという直感今日の実験でもreduction=4はreduction=16よりわずかに精度が高かったが、差はそれほど大きくない。ボトルネックが必要以上に広いと過学習のリスクやパラメータ効率の悪化につながる場合があり、単純な関係ではない
チャネル重要度スコアsを、元データとして重要かどうかの直接的な指標だと解釈してしまう重要度という名前からfeature importanceと同じ概念に思えるsはあくまでそのブロック内部の中間表現の各チャネルに対する重み付けであり、元の20個の入力特徴量との対応は入力層や前段のブロックの変換を経ているため単純ではない。「どの生の特徴量が重要か」を知りたい場合はSHAPなど別の解釈手法と併用すべき

🚀 次のステップ

  • 発展: 今日はSEMLPBlockのExcitationをF(x)自体から計算しましたが、元論文のように「バッチ内の複数サンプルにまたがる統計量(例: バッチ平均・分散)」を追加の入力として使うバリエーションも考えられます。SEMLPBlockを拡張し、fだけでなくfのバッチ内平均を連結した情報からExcitationを計算する版を実装し、精度がどう変わるか試してみましょう
  • 次回予告: Phase6テーマ2「カスタムアーキテクチャ設計」4日目。SEブロックは「重要度をチャネル自身から計算する」機構でしたが、次回はいよいよMulti-Head Self-Attentionブロックを表形式データに応用します。SEが「各チャネルをどれだけ信じるか」を計算したのに対し、Attentionは「他のサンプル(あるいは他の特徴量グループ)とどれだけ関連しているか」を動的に計算する、より一般化された注意機構です

Phase 6 の学習マップ(全20テーマ)

1-5
SOTA手法追跡・論文実装(完了)
6-10
カスタムアーキテクチャ設計(本日3/5)
11-15
高度なアンサンブル(Pseudo Labeling・TTA)
16-20
コンペ戦略・Gold 3枚達成プラン

Day131でResidual Connection、Day132でその配分比率を学習可能にするGated MLPを実装。今日はチャネル単位の再重み付けを行うSqueeze-and-Exciteブロックを実装した。次回はいよいよAttention機構へと進む。

📝 自己評価(解いた後に記入)

自分の回答・気づき・メモ: