📚 背景知識(読んでから問題へ)
F(x)自体をSEブロックのように調整することもできますし、その前段にAttentionでトークン間の情報を混ぜ合わせる工程を挟むこともできます。今日はこの4機構を1つのHybridMLPBlockに統合し、アブレーション実験(機構を1つずつ外して貢献度を測る実験)で「どの機構が実際に効いているか」を切り分けます。| Day | ブロック | 数式 | 追加した「問い」 |
|---|---|---|---|
| 131 | ResidualMLPBlock | H(x) = F(x) + x | 深いネットワークを安全に積み上げる |
| 132 | GatedMLPBlock | H(x) = T(x)F(x) + (1-T(x))x | 配分(1:1)を固定でなく学習させる |
| 133 | SEMLPBlock | H(x) = x + S(F(x))*F(x) | F(x)の各チャネルをどれだけ信じるか |
| 134 | AttnMLPBlock | x=x+MHA(x) → H(x)=x+FFN(x) | チャネル同士がどれだけ関連しているか |
MHA sub-layer
F(x)を計算
F(x)を再重み付け
x・Fの混ぜ方決定
🪄 今日の比較の設計(5構成のアブレーション)
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)
💡 ヒント
HybridMLPBlockは「新しい計算」を何も発明していません。Day131〜134で書いたコードの部品をif文で繋ぎ直すだけです。難しいのは新しいアルゴリズムを考えることではなく、4つのブロックのforwardメソッドを見比べたときに、どこが共通していて(norm1→fc1→act→norm2→fc2のFFN部分)、どこが差分なのか(先頭のAttention・F(x)の重み付け方法・xとの混ぜ方)を正確に切り分けることです。
- 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()を呼べば分解できる
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_attn | use_se | use_gate | 意味 |
|---|---|---|---|---|
| Full | True | True | True | 4機構すべて乗せた完全版 |
| Full - Attn | False | True | True | Attentionだけ外す |
| Full - SE | True | False | True | SEだけ外す |
| Full - Gate | True | True | False | Gateだけ外す(単純加算に戻る) |
| BaseOnly | False | False | False | Day131の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位の順序は入れ替わる可能性がある
📊 結果の可視化(パラメータ内訳・精度比較・2つのランキング)
Full構成: 1ブロックあたりのパラメータ内訳:
| 要素 | 構成 | パラメータ数 |
|---|---|---|
| Attention sub-layer | attn_norm + mha(embed_dim=8) | 352 |
| 共通FFN | norm1+fc1+norm2+fc2 | 8,320 |
| SE(excitation) | 64→16→64, reduction=4 | 2,128 |
| Gate(gate_fc) | Linear(64,64) | 4,160 |
| 合計(Full) | - | 14,960 |
① 精度低下幅ランキング(貢献度の絶対値、大きいほど重要):
② パラメータ効率ランキング(1000個あたりの精度向上、Attnが突出):
■ ①では 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
2アブレーション実験が「単なる比較」と違う点
3精度低下幅とパラメータ効率が一致しないことの意味
# 精度低下幅ランキング: Attn > Gate > SE
# パラメータ効率ランキング: Attn >> SE ≈ Gate
4BaseOnly(Day131のResidualMLPBlock)が比較の「原点」である理由
🧮 数学・統計の補足(文系向け)
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テーマ)
Day131でResidual Connection、Day132でGated MLP、Day133でSqueeze-and-Excite、Day134でMulti-Head Self-Attentionを実装。今日はこの4機構を統合したHybridMLPBlockを作り、アブレーション実験で貢献度を実測した。次回からはPhase6テーマ3「高度なアンサンブル」へ進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: