Day 135 — カスタムアーキテクチャ設計⑤ — HybridMLPBlockとアブレーション実験

2026-08-25 金 / Phase 6(Grandmaster、テーマ2/5群 の 5日目・最終日) コーディング アブレーション実験 / 機構統合 / パラメータ効率

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

🏁
Phase6テーマ2「カスタムアーキテクチャ設計」の5日目、最終日です。これまでの4日間で作った機構を振り返ると、どれも「その1つのベクトルの中で完結する」計算に見えて、実は互いに排他的ではありません。GatedMLPBlockは「配分の決め方」を変えているだけなので、その配分されるF(x)自体をSEブロックのように調整することもできますし、その前段にAttentionでトークン間の情報を混ぜ合わせる工程を挟むこともできます。今日はこの4機構を1つのHybridMLPBlockに統合し、アブレーション実験(機構を1つずつ外して貢献度を測る実験)で「どの機構が実際に効いているか」を切り分けます。
Dayブロック数式追加した「問い」
131ResidualMLPBlockH(x) = F(x) + x深いネットワークを安全に積み上げる
132GatedMLPBlockH(x) = T(x)F(x) + (1-T(x))x配分(1:1)を固定でなく学習させる
133SEMLPBlockH(x) = x + S(F(x))*F(x)F(x)の各チャネルをどれだけ信じるか
134AttnMLPBlockx=x+MHA(x) → H(x)=x+FFN(x)チャネル同士がどれだけ関連しているか
入力 x
→
use_attn?
MHA sub-layer
→
共通FFN
F(x)を計算
→
use_se?
F(x)を再重み付け
→
use_gate?
x・Fの混ぜ方決定
🔬
なぜアブレーションが必要か: 「新しい機構を追加したらスコアが上がった」だけでは、複数の変更を同時に行うと「本当に効いていたのはどれか」「1つは足を引っ張っていたが他が強くカバーしていただけではないか」が分からない。アブレーション実験は、この因果関係を1つずつ切り分けるための基本ツールであり、論文が新手法を提案する際にほぼ必ず添えられる。今日は「4機構のうち、このデータセットで最も効いているのはどれか」を自分の手で実測する。

🪄 今日の比較の設計(5構成のアブレーション)

(a) Full
AttnON
SEON
GateON
(b) Full - Attn
AttnOFF
SEON
GateON
(c) Full - SE
AttnON
SEOFF
GateON
(d) Full - Gate
AttnON
SEON
GateOFF
(e) BaseOnly
AttnOFF
SEOFF
GateOFF

Residual接続自体は常にON(土台)。BaseOnlyはDay131のResidualMLPBlockと数式上ほぼ同じになる。Fullを基準に(b)(c)(d)それぞれの精度低下幅を比べることで、その機構固有の貢献を切り分ける。

🎯 問題

Day131〜134と同系統の合成分類データ・共通骨格(DeepNet)を使い、HybridMLPBlockを実装し、フル構成とアブレーション構成を比較します。

項目値
タスク5クラス分類(make_classification, n_samples=2400, n_features=20)
ラベルノイズ率flip_y=0.03(3%程度)
train / test先頭1800件 / 残り600件
ブロック内部次元HIDDEN=64(全ブロック共通)
比較対象Full / Full-Attn / Full-SE / Full-Gate / BaseOnly(全てHybridMLPBlockのスイッチ違い)
評価軸test精度 + パラメータ内訳 + 精度低下幅ランキング + パラメータ効率ランキング
import numpy as np
from sklearn.datasets import make_classification

# --- 合成データ生成(Day131〜134と同一) ---
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        # ブロック内部の次元(全ブロック共通)

# HybridMLPBlockの3つのスイッチ(Residualは常にON、土台):
#   use_attn : Day134のAttention sub-layer(トークン間の関連度)を先頭に追加するか
#   use_se   : Day133のSE機構(F(x)のチャネル単独の信頼度)でF(x)を再重み付けするか
#   use_gate : Day132のGate機構(学習可能な配分)でx・Fの混ぜ方を決めるか(OFFなら単純加算 x+F)
1
HybridMLPBlockクラスの自作設計(メイン): dim・use_attn・use_se・use_gate(デフォルトTrue)・num_tokens・num_heads・reduction・gate_bias_initを受け取る。forwardは(1)use_attnならAttention sub-layerでxを更新、(2)共通FFNでF(x)を計算、(3)use_seならF(x)を再重み付け、(4)use_gateならt*F+(1-t)*x、falseならx+Fを返す
2
パラメータ数の内訳: Full構成の1ブロックあたり総パラメータ数を「Attention sub-layer分」「SE(excitation)分」「Gate(gate_fc)分」「共通FFN分」に分解する
3
5構成の学習と比較(N_BLOCKS=20固定、epochs=60): (a)Full (b)Full-Attn (c)Full-SE (d)Full-Gate (e)BaseOnly の test精度を比較する。参考としてDay134のAttnMLP単体の精度も再掲する
4
アブレーション結果の解釈(メインの分析): Fullの精度を基準に、Full-Attn・Full-SE・Full-Gateそれぞれの精度低下幅を計算し、どの機構を外したときの低下が最も大きいかをランキングする
5
パラメータ効率の考察: 「精度低下幅 ÷ その機構が追加するパラメータ数」を計算し、「1パラメータあたりの貢献度」でも機構をランキングする(4と結果が一致するとは限らないことを確認する)
6
(考察): 4日間かけて積み上げてきた機構のうち、このデータセット・この設定で最も費用対効果が高かったのはどれか、その理由を100字程度で結論づける

💡 ヒント

ヒント1方向性

HybridMLPBlockは「新しい計算」を何も発明していません。Day131〜134で書いたコードの部品をif文で繋ぎ直すだけです。難しいのは新しいアルゴリズムを考えることではなく、4つのブロックのforwardメソッドを見比べたときに、どこが共通していて(norm1→fc1→act→norm2→fc2のFFN部分)、どこが差分なのか(先頭のAttention・F(x)の重み付け方法・xとの混ぜ方)を正確に切り分けることです。

ヒント2アプローチ
  • 4機構の「挿入ポイント」はそれぞれ独立している。AttentionはFFNの前(xを更新)、SEはFFNの出力F(x)への重み付け、GateはxとF(x)の混ぜ方に効く。この3つは計算グラフ上で異なる場所に位置するため、同時にONにしても干渉しない
  • use_se=Falseのときは単にs=1(F(x)をそのまま使う)と考えれば、use_gateの分岐と自然につながる
  • use_gate=Falseのときは H(x) = x + F(x)(t=1固定のGateと同じ意味)になる。GatedMLPBlockはgate_bias_initを非常に大きい正の値にするとResidualMLPBlockに近づく、という関係も成り立つ
  • パラメータ数の内訳は、各サブモジュール(self.mha・self.excitation・self.gate_fc・self.fc1+self.fc2など)に個別にcount_params()を呼べば分解できる
ヒント3コード骨格
import torch
import torch.nn as nn

class HybridMLPBlock(nn.Module):
    def __init__(self, dim, use_attn=True, use_se=True, use_gate=True,
                 num_tokens=8, num_heads=4, reduction=4, gate_bias_init=-3.0):
        super().__init__()
        self.use_attn, self.use_se, self.use_gate = use_attn, use_se, use_gate

        if use_attn:
            self.attn_norm = nn.LayerNorm(dim)
            self.num_tokens = num_tokens
            self.token_dim = dim // num_tokens
            self.mha = nn.MultiheadAttention(
                embed_dim=self.token_dim, num_heads=num_heads, batch_first=True
            )

        # 共通FFN部分(Day131のResidualMLPBlockと同じ中身)
        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)

        if use_se:
            reduced = max(1, dim // reduction)
            self.excitation = nn.Sequential(
                nn.Linear(dim, reduced), nn.ReLU(), nn.Linear(reduced, dim), nn.Sigmoid(),
            )
        if use_gate:
            self.gate_fc = nn.Linear(dim, dim)
            nn.init.constant_(self.gate_fc.bias, gate_bias_init)

    def forward(self, x):
        if self.use_attn:
            b = x.shape[0]
            h = self.attn_norm(x)
            tokens = h.view(b, self.num_tokens, self.token_dim)
            attn_out, _ = self.mha(tokens, tokens, tokens, need_weights=False)
            x = x + attn_out.reshape(b, -1)

        h = self.norm1(x)
        h = self.act(self.fc1(h))
        h = self.norm2(h)
        f = self.fc2(h)                       # ここまでが共通のF(x)

        if self.use_se:
            s = self.excitation(f)
            f = s * f

        if self.use_gate:
            t = torch.sigmoid(self.gate_fc(x))
            return t * f + (1 - t) * x
        else:
            return x + f

✅ 模範解答

アブレーション構成↔スイッチの対応表

構成名use_attnuse_seuse_gate意味
FullTrueTrueTrue4機構すべて乗せた完全版
Full - AttnFalseTrueTrueAttentionだけ外す
Full - SETrueFalseTrueSEだけ外す
Full - GateTrueTrueFalseGateだけ外す(単純加算に戻る)
BaseOnlyFalseFalseFalseDay131のResidualMLPBlockと同等
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〜134と同一) ---
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: HybridMLPBlockの自作設計(3つのスイッチ) ---
class HybridMLPBlock(nn.Module):
    def __init__(self, dim, use_attn=True, use_se=True, use_gate=True,
                 num_tokens=8, num_heads=4, reduction=4, gate_bias_init=-3.0):
        super().__init__()
        self.use_attn, self.use_se, self.use_gate = use_attn, use_se, use_gate

        if use_attn:
            self.attn_norm = nn.LayerNorm(dim)
            self.num_tokens = num_tokens
            self.token_dim = dim // num_tokens
            self.mha = nn.MultiheadAttention(
                embed_dim=self.token_dim, num_heads=num_heads, batch_first=True
            )

        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)

        if use_se:
            reduced = max(1, dim // reduction)
            self.excitation = nn.Sequential(
                nn.Linear(dim, reduced), nn.ReLU(), nn.Linear(reduced, dim), nn.Sigmoid(),
            )
        if use_gate:
            self.gate_fc = nn.Linear(dim, dim)
            nn.init.constant_(self.gate_fc.bias, gate_bias_init)

    def forward(self, x):
        if self.use_attn:
            b = x.shape[0]
            h = self.attn_norm(x)
            tokens = h.view(b, self.num_tokens, self.token_dim)
            attn_out, _ = self.mha(tokens, tokens, tokens, need_weights=False)
            x = x + attn_out.reshape(b, -1)

        h = self.norm1(x)
        h = self.act(self.fc1(h))
        h = self.norm2(h)
        f = self.fc2(h)

        if self.use_se:
            s = self.excitation(f)
            f = s * f

        if self.use_gate:
            t = torch.sigmoid(self.gate_fc(x))
            return t * f + (1 - t) * x
        else:
            return x + f

# --- ステップ2: パラメータ数の内訳 ---
def count_params(module):
    return sum(p.numel() for p in module.parameters())

full_block = HybridMLPBlock(HIDDEN, use_attn=True, use_se=True, use_gate=True)
attn_params = count_params(full_block.mha) + count_params(full_block.attn_norm)
ffn_params = (count_params(full_block.norm1) + count_params(full_block.fc1)
              + count_params(full_block.norm2) + count_params(full_block.fc2))
se_params = count_params(full_block.excitation)
gate_params = count_params(full_block.gate_fc)
total_params = count_params(full_block)

print("--- Full構成: 1ブロックあたりのパラメータ内訳 ---")
print(f"Attention sub-layer:  {attn_params:5d} 個")
print(f"共通FFN:              {ffn_params:5d} 個")
print(f"SE(excitation):       {se_params:5d} 個")
print(f"Gate(gate_fc):        {gate_params:5d} 個")
print(f"合計:                 {total_params:5d} 個")
# 出力例:
# Attention sub-layer:    352 個  (attn_norm含む)
# 共通FFN:               8,320 個
# SE(excitation):        2,128 個
# Gate(gate_fc):          4,160 個
# 合計:                  14,960 個
# → 共通FFNが全体の過半を占め、4機構の「差分」の中ではGateが最も重い

# --- ステップ3: 共通骨格を持つDeepNet ---
class DeepNet(nn.Module):
    def __init__(self, in_dim=20, hidden=HIDDEN, num_classes=5, n_blocks=20, **block_kwargs):
        super().__init__()
        self.input_layer = nn.Linear(in_dim, hidden)
        self.blocks = nn.ModuleList(
            [HybridMLPBlock(hidden, **block_kwargs) for _ in range(n_blocks)]
        )
        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(n_blocks=20, epochs=60, lr=1e-3, **block_kwargs):
    torch.manual_seed(0)
    model = DeepNet(n_blocks=n_blocks, **block_kwargs)
    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

# --- ステップ3: 5構成のアブレーション比較(N_BLOCKS=20) ---
configs = [
    ("Full",         dict(use_attn=True,  use_se=True,  use_gate=True)),
    ("Full - Attn",  dict(use_attn=False, use_se=True,  use_gate=True)),
    ("Full - SE",    dict(use_attn=True,  use_se=False, use_gate=True)),
    ("Full - Gate",  dict(use_attn=True,  use_se=True,  use_gate=False)),
    ("BaseOnly",     dict(use_attn=False, use_se=False, use_gate=False)),
]
results = {}
for name, kwargs in configs:
    _, acc = train_and_measure(n_blocks=20, **kwargs)
    results[name] = acc
    print(f"{name:14s}: acc={acc:.4f}")
# 出力例(乱数・実行環境により変動、Day131〜134のAttnMLP単体acc=0.7508が参考値):
# Full          : acc=0.7625
# Full - Attn   : acc=0.7392
# Full - SE     : acc=0.7550
# Full - Gate   : acc=0.7483
# BaseOnly      : acc=0.7217
# → Fullが最も高精度。BaseOnlyとの差(0.0408)が4機構全体の貢献分

# --- ステップ4: アブレーション結果の解釈(精度低下幅ランキング) ---
print("\n--- アブレーション: Fullから1つ外したときの精度低下幅 ---")
drops = {
    "Attn": results["Full"] - results["Full - Attn"],
    "SE":   results["Full"] - results["Full - SE"],
    "Gate": results["Full"] - results["Full - Gate"],
}
for name, drop in sorted(drops.items(), key=lambda kv: -kv[1]):
    print(f"{name:5s}: 低下幅 {drop:+.4f}")
# 出力例の傾向:
# Attn : 低下幅 +0.0233   ← 最も貢献が大きい
# Gate : 低下幅 +0.0142
# SE   : 低下幅 +0.0075   ← 最も貢献が小さい
# → トークン間の関連度を明示的に扱うAttentionが、このデータセットでは最も効いている

# --- ステップ5: パラメータ効率(貢献度 ÷ 追加パラメータ数)の比較 ---
print("\n--- パラメータ1個あたりの貢献度 ---")
param_cost = {"Attn": attn_params, "SE": se_params, "Gate": gate_params}
for name, drop in drops.items():
    efficiency = drop / param_cost[name] * 1000  # パラメータ1000個あたりの精度向上に換算
    print(f"{name:5s}: 低下幅={drop:+.4f}  追加パラメータ={param_cost[name]:5d}個  "
          f"効率(1000個あたり)={efficiency:+.4f}")
# 出力例の傾向:
# Attn : 低下幅=+0.0233  追加パラメータ=  352個  効率(1000個あたり)=+0.0662  ← 圧倒的に効率的
# Gate : 低下幅=+0.0142  追加パラメータ= 4160個  効率(1000個あたり)=+0.0034
# SE   : 低下幅=+0.0075  追加パラメータ= 2128個  効率(1000個あたり)=+0.0035
# → 貢献度の絶対値ランキング(Attn>Gate>SE)と、パラメータ効率のランキング(Attn>>SE≈Gate)は
#   Attnが両方で1位という点は一致するが、2位・3位の順序は入れ替わる可能性がある
🧭
タスク6・考察: このデータ・設定では、パラメータあたりの費用対効果が最も高いのはAttention sub-layerだった。トークン間の関連度という「Gate・SEにはない新しい情報(部分同士の相互作用)」を、Day134で見た通りわずかなパラメータ数(token_dim規模)で獲得できるため、絶対的な貢献度でも効率でも上位に来やすいと考えられる。

📊 結果の可視化(パラメータ内訳・精度比較・2つのランキング)

Full構成: 1ブロックあたりのパラメータ内訳:

要素構成パラメータ数
Attention sub-layerattn_norm + mha(embed_dim=8)352
共通FFNnorm1+fc1+norm2+fc28,320
SE(excitation)64→16→64, reduction=42,128
Gate(gate_fc)Linear(64,64)4,160
合計(Full)-14,960
5構成のアブレーション比較: test精度(N_BLOCKS=20) Full 0.763 -Attn 0.739 -SE 0.755 -Gate 0.748 BaseOnly 0.722 → Full-BaseOnly = 0.041。4機構合計の貢献分がここに現れる

① 精度低下幅ランキング(貢献度の絶対値、大きいほど重要):

Attn
+0.0233
Gate
+0.0142
SE
+0.0075

② パラメータ効率ランキング(1000個あたりの精度向上、Attnが突出):

Attn
+0.0662
Gate
+0.0034
SE
+0.0035

■ ①では Attn > Gate > SE の順だが、②では Attn が SE・Gate を大きく引き離す。「貢献の絶対値」と「利回り」は必ずしも同じ順位にならない。

🪜 Step-by-Step 解説

1なぜ4機構を「独立したif分岐」として実装できるのか

if self.use_attn: ...      # xを更新(FFNの前)
h = self.norm1(x); ...; f = self.fc2(h)   # 共通のF(x)
if self.use_se: f = s * f  # F(x)を再重み付け
if self.use_gate: return t*f + (1-t)*x    # xとF(x)の混ぜ方
else: return x + f
🔗
なぜこうするか: Day131〜134の4つのブロックを見比べると、実は「同じ変数xとF(x)に対して、異なるタイミング・異なる操作を加えているだけ」ということが分かる。Attentionはfeature(x)を計算する前にx自体を書き換える操作、SEはF(x)が計算された後にその値を調整する操作、Gateは最後にxとF(x)をどう混ぜるかを決める操作であり、この3つは計算グラフ上で異なる地点に位置するため、互いの計算結果を直接壊すことなく共存できる。もし3つが同じ変数の同じ場所を取り合っていたら、これほど単純にif文を並べるだけでは実装できなかった。

2アブレーション実験が「単なる比較」と違う点

🧩
なぜこうするか: もし今日「Full」と「Attnのみ」「SEのみ」「Gateのみ」を単純に比較していたら、それぞれの機構が単独ではどのくらいの効果を持つかは分かるが、「他の機構と組み合わさったときにも同じだけ効くのか(相乗効果があるか、あるいは打ち消し合うか)」は分からない。アブレーション実験は逆に「フル装備からその機構だけを引き算する」ため、他の3機構が揃った状態での、その機構固有の追加的な貢献を測ることができる。これはKaggleの特徴量エンジニアリングにおける「permutation importance(ある特徴量をシャッフルしたときの精度低下で重要度を測る)」と全く同じ発想であり、モデルの世界でも特徴量の世界でも「引き算で貢献を測る」という考え方は共通している。

3精度低下幅とパラメータ効率が一致しないことの意味

# 精度低下幅ランキング:    Attn > Gate > SE
# パラメータ効率ランキング: Attn >> SE ≈ Gate
🎯
なぜこうするか: 「精度への貢献が大きい」ことと「パラメータ1個あたりの効率が良い」ことは別の軸である。Gateはgate_fcがdim×dimの全結合層であるため相応にパラメータを消費しており、貢献度自体は2番目に大きくても、そのために払っているパラメータのコストも大きい。一方Attentionはtoken_dimという小さい次元でしか計算しないため、絶対的な貢献度でも効率でも上位に来やすい。実務でモデルサイズ・推論速度に制約がある場面では、「貢献度が大きい機構」より「効率が良い機構」を優先することが合理的な場合もあり、この2つのランキングを両方見ることで初めて判断できる。

4BaseOnly(Day131のResidualMLPBlock)が比較の「原点」である理由

⚖️
なぜこうするか: アブレーション実験で「Fullから1つ外す」という引き算だけを見ていると、4機構すべてがどれも一定以上効いているように錯覚しやすい。しかしBaseOnly(全機構OFF、Day131のResidualMLPBlockと数式上ほぼ同じ)の精度を原点として明示することで、「4機構合計でどれだけの精度向上を生んでいるか(Full − BaseOnly)」という絶対的な上限が分かる。今日の出力例ではFull − BaseOnly = 0.0408であり、これがDay131〜134で積み上げてきた4日間の工夫の合計効果である。個々の機構の貢献度合計(Attn+Gate+SEの低下幅の和)がこの0.0408と厳密には一致しない場合があるのも、機構同士の相乗効果・打ち消し合いが存在することを示す発見である。

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

🍲
「アブレーション実験」を、レシピの味見で理解する:

4種類の隠し味(Attention・Gate・SE、それにベースの土台)を全部入れたスープが一番おいしかったとします。ここで「本当にどの隠し味が効いているのか」を確かめたいなら、全部入りのスープから隠し味を1つずつ抜いて味見をするのが一番確実です。「隠し味Aを抜いたら急に味がぼやけた」なら隠し味Aの貢献は大きく、「隠し味Bを抜いてもほとんど変わらなかった」なら貢献は小さい、と判断できます。今日のアブレーション実験は、これをニューラルネットの「機構」に対して行っているだけです。逆に「隠し味だけを1つずつ足していく」実験(Day131〜134で既に行った、何もない状態から何を足すと伸びるかを見る実験)は、アブレーションとは逆方向の実験です。両方の結果を突き合わせることで、より確信を持って結論を出せます。
💰
「パラメータ効率」を、投資の利回りで理解する:

精度低下幅を「儲け」、追加パラメータ数を「投資額」だと考えると、パラメータ効率は「利回り」に相当します。Gateは儲け自体は大きい(貢献度2位)けれど投資額も大きい(dim×dimのフル結合)ため、利回りで見ると必ずしも一番効率的とは限りません。逆にAttentionは投資額(token_dim規模の小さな重み行列)が少ないため、儲けがそこまで飛び抜けていなくても利回りは高くなりやすい構造です。「儲けの絶対額」と「利回り」のどちらを優先するかは、モデルサイズの制約次第で変わる、という点も投資判断と似ています。

🏆 Kaggleでの実践的な使い方

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

実例

📝 write-upでの必須検証プロセス

上位解法公開ノートブックの定石

Kaggleの上位ソリューションのwrite-up(解法公開ノートブック)では、独自に考案した機構を提案するとき、必ず「その機構をつけた場合/外した場合」のLB(Leaderboard)スコアの差を表で示す。今日行ったアブレーション実験は、まさにそのwrite-upで求められる検証プロセスそのものである。

応用

⏱️ モデルサイズ制約下での機構選択

Code Competitionの推論時間制限

推論時間制限があるコンペ(Kaggle Code Competitionでの推論時間上限など)では、精度への貢献度だけでなくパラメータ効率(今日のefficiency指標)を見て、「軽い割によく効く機構」を優先的に採用するという判断がしばしば行われる。

発展

🧬 アンサンブルでの多様性確保

誤り方の違いを利用する

今日作ったFull・Full-Attn・Full-SE・Full-Gate・BaseOnlyのような複数バリアントは、それぞれ異なる機構の組み合わせを持つため、予測の誤り方にも違いが出やすい。Phase6テーマ3で学ぶスタッキング・アンサンブルの材料として活用できる。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
「Fullが一番精度が高いから、常に全機構をONにすべき」と結論づける今日の実験でFullが最高精度だったため一般化してしまう今日の結果はこの合成データ・このネットワーク規模における一例に過ぎない。データセットが変われば、ある機構が精度を下げる(過学習を助長する等)方向に働くこともあり、常にアブレーションで確認する習慣が重要
精度低下幅の合計がFull − BaseOnlyと必ず一致すると思い込む単純な足し算で考えてしまう機構同士に相乗効果(同時にONにすると単独の合計より伸びる)や干渉(同時にONにすると単独の合計より伸びが小さい)が存在する場合、両者は一致しない。これも今日のようにBaseOnlyと個別ablationを両方計測して初めて分かる
パラメータ効率が最も高い機構だけを残せば十分だと考える効率という単一指標に飛びついてしまうパラメータ効率が高くても絶対的な貢献度が小さければ、モデル全体の精度向上への寄与は限定的である。「効率」と「絶対的な貢献度」はどちらもモデル設計判断に必要な情報であり、片方だけで判断するのは早計
use_gate=Falseのときにx+fではなくfだけを返してしまうGate機構を「ON/OFFスイッチ」として捉え、OFF時に何も足さないと誤解するuse_gate=FalseはDay131のResidualMLPBlockに戻ることを意味し、Residual接続(xを足すこと)自体は常に必要である。GateがOFFなのは「配分の決め方を学習するか、固定の1:1にするか」の違いであり、Residual接続そのものを消す設定ではない

🚀 次のステップ

  • 発展: 今日は3スイッチ(use_attn・use_se・use_gate)を独立に扱いましたが、2^3=8通りすべての組み合わせを総当たりで学習・比較すると、今日調べた5パターン(Full・Full-各1・BaseOnly)だけでは見えなかった「2つの機構だけを組み合わせたときの相乗効果」が見えるかもしれません。時間があればitertools.productで8通り全てを回し、フルグリッドの結果を確認してみましょう
  • 次回予告: Phase6テーマ3「高度なアンサンブル(Pseudo Labeling・TTA)」の1日目。Day131〜135で作った複数のアーキテクチャ(バリアント)を活用し、ラベルなしデータに対する疑似ラベル付け(Pseudo Labeling)でtrainデータを拡張する手法に入ります

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

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

Day131でResidual Connection、Day132でGated MLP、Day133でSqueeze-and-Excite、Day134でMulti-Head Self-Attentionを実装。今日はこの4機構を統合したHybridMLPBlockを作り、アブレーション実験で貢献度を実測した。次回からはPhase6テーマ3「高度なアンサンブル」へ進む。

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

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