Day 131 — カスタムアーキテクチャ設計① — Residual接続を持つMLPブロックの自作設計

2026-08-21 金 / Phase 6(Grandmaster、テーマ2/5群 の 1日目) コーディング Skip Connection / 劣化問題 / 勾配ノルムの可視化

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

🏁
Phase6テーマ2「カスタムアーキテクチャ設計」の初日です。テーマ1(Day126〜130)は「論文の数式→PyTorch実装」という翻訳作業を5回反復しました。今日からのテーマ2は少し性格が変わり、既存の部品(Linear・ReLU・LayerNormなど)を組み合わせて、自分で「ブロック」を設計するという一段階抽象度の高い作業に入ります。今日の題材はディープラーニングの歴史上もっとも影響力の大きいアイデアの1つ、Residual Connection(Skip Connection、He et al., 2015)です。
🧱
なぜ「ただ層を増やす」だけではモデルが壊れるのか: 直感的には「層を増やすほど賢くなるはず」と考えたくなりますが、2015年以前、この直感は実験的に裏切られていました。普通のMLP(Linear→活性化関数の単純な積み重ね)はある程度より深くするとtrain誤差自体が浅いモデルより高くなるという奇妙な現象が起きます。過学習ではありません。He et al.はこれをdegradation problem(劣化問題)と名付けました。原因は、何層もの重み行列が「掛け合わせると単位行列になる(=何もしない)」ように調整されるのが、勾配降下法にとって不自然に難しいタスクだからです。
入力 x
→
変換 F(x)
(Norm→Linear→ReLU→Norm→Linear)
+x →
出力 H(x)=F(x)+x

Residual Connectionの核心の数式:

H(x) = F(x) + x

ブロックが「何もしない(恒等写像)」を学習したいなら、F(x)がただゼロを出力すればよいだけになる。エスカレーターに例えると、エスカレーター(x、恒等写像という無料の移動手段)に乗ったまま、必要な分だけ自分の足でさらに歩く(F(x)、追加の変換)というイメージ。歩かなくても目的階に着けるなら「F(x)=0」と学習すればよく、これは一番簡単な選択。

🪄 今日の比較の設計(3種類のブロック × 4段階の深さ)

a

PlainBlock

正規化なし・Skipなし。Linear→ReLUの単純な積み重ね(劣化問題の再現用)

b

NormBlock

LayerNormはあるがSkipなし(アブレーション: 正規化だけの効果を見る)

c

ResidualMLPBlock

LayerNorm + Skip接続(Pre-Norm方式)。今日のメイン設計対象

3つのブロックはパラメータ数・層構成をほぼ揃え、return文が「hのみ」か「x + h」かという1点だけを変える。この差がN_BLOCKS(深さ)を増やしたときにどう拡大するかを観察する。

🎯 問題

Day126〜130と同系統の合成分類データに対し、同じ深さ(ブロック数)を持つ3種類のネットワークを構築し、深さを変えながら比較します。

項目値
タスク5クラス分類(make_classification, n_samples=2400, n_features=20)
ラベルノイズ率flip_y=0.03(3%程度)
train / test先頭1800件 / 残り600件
ブロック内部次元HIDDEN=64(全ブロック共通)
比較対象PlainMLP / NormMLP / ResMLP(骨格は共通、ブロックだけ差し替え)
評価軸test精度 + 入力層の勾配ノルム(1バッチ目) + 深さN_BLOCKS=[5,10,20,40]の感度
import numpy as np
from sklearn.datasets import make_classification

# --- 合成データ生成(Day126〜130と同系統) ---
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        # ブロック内部の次元(全ブロック共通)
# 3種類のネットワークを、ブロック数 N_BLOCKS を変えながら比較する:
#   (a) PlainMLP  : Linear→ReLU の単純な積み重ね(正規化なし・Skip接続なし)
#   (b) NormMLP   : LayerNormはあるがSkip接続はない(アブレーション用)
#   (c) ResMLP    : LayerNorm + Skip接続を持つ ResidualMLPBlock の積み重ね
1
ResidualMLPBlockの自作設計(メイン): 入出力次元が同じdimのMLPブロックをnn.Moduleとして実装する。「LayerNorm→Linear→活性化関数→LayerNorm→Linear」の変換F(x)を行い、最後に入力xを足し込んでx+F(x)を返す(Pre-Norm方式)
2
3種類のネットワークの構築: 入力層(Linear)→ ブロックをN_BLOCKS個スタック → 出力層(Linear)という共通骨格で(a)PlainMLP・(b)NormMLP・(c)ResMLPを実装する
3
学習と比較(N_BLOCKS=20固定): 3モデルをそれぞれepochs=60で学習させ、test精度を比較する
4
勾配の伝わりやすさの可視化: 学習開始直後(1バッチ目のbackward()直後)に、入力層の重みに対する勾配のL2ノルムを3モデルで計測し比較する
5
深さの感度確認: N_BLOCKSを[5, 10, 20, 40]の4通りで変化させ、3モデルそれぞれのtest精度がどう変化するかを確認する
6
(考察): Skip接続・LayerNormの有無が、深さに対する頑健性にどう影響したかを100字程度で結論づける

💡 ヒント

ヒント1方向性

ResidualMLPBlockのforwardメソッドで一番大事な行は、最後にreturn x + hのように入力そのものを足し算することです。逆に言えばreturn h(足さない)と書いた瞬間、それはただの「LayerNorm付きMLP」(今日の比較対象(b) NormMLP)になります。3つのモデルの実装上の違いは実は「足すか足さないか」と「LayerNormを挟むか挟まないか」の2点だけであり、それ以外のパラメータ数・層の数はできるだけ揃えて「公平な比較」にすることが今日の実験の肝です。

ヒント2アプローチ
  • Skip接続でx + F(x)を計算するには、F(x)の出力次元がxと完全に同じでなければならない。ResidualMLPBlockは「入力次元=出力次元=dim」という制約を持つ設計にするのが最も簡単
  • 「Pre-Norm」とは、Linearの前にLayerNormを置く設計(LayerNorm→Linear→活性化関数→LayerNorm→Linear)。層を非常に深く積む場合、Pre-Normの方が学習が安定しやすいことが知られている
  • 勾配のL2ノルムは、loss.backward()の直後にmodel.input_layer.weight.grad.norm().item()のようにして取得できる。必ずbackward()の直後・optimizer.step()の前に取得すること
  • N_BLOCKS=40のような深いPlainMLPは、精度がほぼランダム推測(5クラス分類なら約20%)近くまで落ち込むことがある。これは実装のバグではなく劣化問題そのものが再現されている結果
ヒント3コード骨格
import torch
import torch.nn as nn

class ResidualMLPBlock(nn.Module):
    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   # ← Skip接続(これがないとただのNormMLPになる)

class DeepNet(nn.Module):
    def __init__(self, in_dim, hidden, num_classes, n_blocks, mode="res"):
        # mode: "plain"(正規化・Skipなし)/ "norm"(正規化のみ)/ "res"(正規化+Skip)
        ...

✅ 模範解答

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

設計上の要素役割コード上の変数
F(x)ブロック内部の変換(LayerNorm→Linear→活性化→LayerNorm→Linear)forward内のh
x恒等写像。ブロックへの入力をそのまま足す部分return x + h
H(x)=F(x)+xブロックの最終出力forwardの戻り値
Pre-NormLinearの前にLayerNormを置く配置方針norm1→fc1, norm2→fc2の順序
N_BLOCKSネットワークの深さ(ブロック数)n_blocks
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)

# --- サンプルデータ生成(問題文と同じ) ---
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: ResidualMLPBlockの自作設計 ---
class ResidualMLPBlock(nn.Module):
    """Pre-Norm方式: LayerNorm -> Linear -> ReLU -> LayerNorm -> Linear のあとに入力xを足す"""
    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   # Skip接続: F(x) + x

class PlainBlock(nn.Module):
    """正規化もSkip接続もない、素朴なLinear->ReLUの繰り返し(劣化問題の再現用)"""
    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        # Skip接続なし。そのままhを返す

class NormBlock(nn.Module):
    """LayerNormはあるがSkip接続はない(アブレーション: 正規化だけの効果を見る)"""
    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 h        # Skip接続なし(ResidualMLPBlockとの唯一の違いは最後の "x +" の有無)

# --- ステップ2: 共通骨格を持つ3種類のDeepNet ---
BLOCK_CLS = {"plain": PlainBlock, "norm": NormBlock, "res": ResidualMLPBlock}

class DeepNet(nn.Module):
    def __init__(self, in_dim=20, hidden=HIDDEN, num_classes=5, n_blocks=20, mode="res"):
        super().__init__()
        self.input_layer = nn.Linear(in_dim, hidden)   # 勾配ノルム計測の対象
        block_cls = BLOCK_CLS[mode]
        self.blocks = nn.ModuleList([block_cls(hidden) 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()

# --- ステップ3: 学習ループ + 1バッチ目の入力層の勾配ノルム計測 ---
def train_and_measure(mode, n_blocks=20, epochs=60, lr=1e-3):
    torch.manual_seed(0)
    model = DeepNet(n_blocks=n_blocks, mode=mode)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)

    first_batch_grad_norm = None
    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()
            if first_batch_grad_norm is None:   # 学習開始直後の1回だけ記録
                first_batch_grad_norm = model.input_layer.weight.grad.norm().item()
            optimizer.step()

    acc = evaluate(model)
    return acc, first_batch_grad_norm

# --- ステップ4: N_BLOCKS=20固定で3モデルを比較 ---
for mode in ["plain", "norm", "res"]:
    acc, grad_norm = train_and_measure(mode, n_blocks=20)
    print(f"{mode:5s} (N=20): acc={acc:.4f}  input_layer勾配ノルム(1バッチ目)={grad_norm:.6f}")
# 出力例(乱数・実行環境により変動):
# plain (N=20): acc=0.2117  input_layer勾配ノルム(1バッチ目)=0.000031
# norm  (N=20): acc=0.6883  input_layer勾配ノルム(1バッチ目)=0.041520
# res   (N=20): acc=0.7217  input_layer勾配ノルム(1バッチ目)=0.183760
# → plainは勾配がほぼ消滅し(0.000031)、精度もランダム推測(5クラスなら約20%)に近い
# → normは正規化だけでもある程度救われるが、resが最も勾配が伝わりやすく精度も最良

# --- ステップ5: 深さ(N_BLOCKS)の感度確認 ---
for n_blocks in [5, 10, 20, 40]:
    for mode in ["plain", "norm", "res"]:
        acc, _ = train_and_measure(mode, n_blocks=n_blocks)
        print(f"N_BLOCKS={n_blocks} {mode}={acc:.3f}")
# 出力例の傾向:
# N_BLOCKS=5   plain=0.701  norm=0.708  res=0.715   (浅いうちは3者ほぼ差がない)
# N_BLOCKS=10  plain=0.583  norm=0.702  res=0.719   (plainだけ劣化が始まる)
# N_BLOCKS=20  plain=0.212  norm=0.688  res=0.722   (plainはほぼ崩壊、res/normは維持)
# N_BLOCKS=40  plain=0.198  norm=0.641  res=0.708   (plainは崩壊したまま、normもやや悪化、resが最も頑健)
🧭
タスク6・考察: Skip接続のないPlainMLPはN_BLOCKSを増やすほどtest精度が急激に悪化し、入力層への勾配ノルムもほぼゼロに近づく劣化問題が再現された。LayerNormを加えるだけでも劣化はかなり緩和されるが、N_BLOCKS=40では精度がやや低下し始める。Skip接続を持つResMLPは全ての深さで最も高い精度と最も大きい勾配ノルムを維持し、深さに対して最も頑健だった。

📊 結果の可視化(深さ別のtest精度と勾配ノルム)

深さ(N_BLOCKS)別: test精度の推移(劣化問題の再現) 0.0 0.8 N=5 N=10 N=20 N=40 ■ ResMLP(緑)はほぼ横ばい ■ NormMLP(青)はゆるやかに低下 ■ PlainMLP(赤)はN=10から急降下 → 劣化問題は「浅いうちは差がないが、深くするとPlain(Skipなし)だけ崩壊する」という形で現れる
N_BLOCKS=20: 入力層の勾配ノルム(1バッチ目・対数的な差) Plain 0.00003 Norm 0.0415 Res 0.1838 → Skip接続がある方が入力層まで勾配が桁違いに伝わりやすい
🔍
これらの図は模式的なイメージ(実際の数値は実行環境により変動)。重要なのは「PlainMLPだけがN_BLOCKSに対して非線形に崩壊する」点と、「勾配ノルムの差はtest精度の差よりもさらに劇的(桁が違う)」という2つの構造である。

🪜 Step-by-Step 解説

1なぜ「入出力の次元を揃える」という制約が生まれるのか

def forward(self, x):
    h = self.norm1(x)
    ...
    return x + h
🔗
なぜこうするか: x + hというTensorの足し算は、2つのTensorの形(shape)が完全に一致していなければ実行できない(あるいは意図しないブロードキャストが起きてバグになる)。したがってResidualMLPBlockの設計は自動的に「入力の次元=出力の次元=dim」という制約を持つことになる。これはSkip接続を成立させるための構造的な要請であり、カスタムアーキテクチャを設計する際は「このブロックの入出力の形は一致しているか」を常に意識する必要がある(次元を変えたい場合はx側にも小さなLinearを挟む「projection shortcut」という追加の工夫が必要になる)。

2PlainBlock・NormBlock・ResidualMLPBlockの「1行だけの違い」が生む大差

# NormBlock:            return h       # Skip接続なし
# ResidualMLPBlock:      return x + h   # Skip接続あり
🧩
なぜこうするか: 3つのブロックはほぼ同じ層構成(Linear・ReLU・LayerNormの数もパラメータ数もほぼ同じ)でありながら、return文の1行だけが違う。それにもかかわらずN_BLOCKS=20での精度はplain=0.21・norm=0.69・res=0.72と大きく異なった。これは「パラメータ数が同じでも、勾配が実際に伝わって学習に使われるかどうかは全く別の問題である」ことを示している。ネットワークの表現力(理論上何を表現できるか)と学習可能性(実際に勾配降下法でそこに到達できるか)は別物であり、Residual接続は後者を改善する仕組みである。

3なぜ「1バッチ目の入力層の勾配ノルム」を測るのか

if first_batch_grad_norm is None:
    first_batch_grad_norm = model.input_layer.weight.grad.norm().item()
🎯
なぜこうするか: 深いネットワークで劣化問題や勾配消失が起きると、その影響は出力に近い層よりも、入力に近い層で最も顕著に現れる。誤差逆伝播は出力側から入力側へ勾配を伝えていくため、途中の何層ものReLUや小さい重みを通過するうちに、入力に近い層に届くころには勾配の大きさが指数的に小さくなってしまうことがある(今回のplainモデルの0.000031という値がまさにこれ)。この「入力層まで勾配がどれだけ届いているか」を数値で確認することで、「学習が進まない」という現象をテスト精度という間接的な結果だけでなく勾配という直接の原因からも観察できる。

4Pre-Norm方式を選んだ理由

⚖️
なぜこうするか: LayerNormをLinearの前に置く(Pre-Norm)か後に置く(Post-Norm)かは、Skip接続を使う設計では地味だが重要な選択である。Post-Norm(Linear→LayerNormの順で、最後に正規化された値に対して足し算する)は層を非常に深く積んだときに学習が不安定になりやすい。一方Pre-Norm(今日採用した方式)はreturn x + hのx(恒等写像の経路)が正規化を経由せずそのままの値で伝わるため、Skip接続本来の「勾配の高速道路」としての役割がより素直に機能しやすい。Transformer系のアーキテクチャ(GPTなど)が広く採用しているのもPre-Norm方式である。

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

🛗
「Skip接続」を、エスカレーターと階段の関係で理解する:

ビルの1階から2階に上がる方法が2つあるとします。1つはエスカレーター(何もしなくても勝手に運んでくれる、恒等写像x)。もう1つはエスカレーターの脇にある階段(自分の意志で追加の移動をする、変換F(x))。この2つを合体させた乗り物を想像してください——エスカレーターに立ったまま、必要なら追加で数段だけ自分の足で歩くこともできる乗り物です。もし2階までちょうどエスカレーターだけで着くなら、階段部分(F(x))は「何もしない(0段だけ歩く)」と学習すればよく、これは「複雑な階段の上り方を学習する」よりもはるかに簡単です。H(x) = F(x) + xという式は、まさに「エスカレーター(x)+追加の階段(F(x))」という2つの経路を足し合わせたものだと考えると直感的です。
📢
「勾配消失」を、伝言ゲームの劣化で理解する:

20人が一列に並んで、最後尾の人(出力層)から先頭の人(入力層)へ「どれくらい・どちら向きに声を大きくすべきか」という指示(勾配)を、1人ずつ伝言していく状況を想像してください。各人が指示を伝えるたびに内容の一部を弱めてしまう(例えば毎回90%に減衰させる)としたら、20人を経由するころには指示の強さは0.920≈0.12、つまりほぼ聞こえないくらい弱まってしまいます。Skip接続は、この伝言ゲームの列の脇に「先頭まで直接届く伝声管」を1本追加するようなものです。伝言(勾配)は弱まった経路だけでなく、この伝声管を通っても先頭に届くため、途中で情報が消えてしまうリスクが大幅に下がります。

🏆 Kaggleでの実践的な使い方

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

実例

🧱 表形式データ向けDeep Learningアーキテクチャ

「Revisiting Deep Learning for Tabular Data」系の設計

表形式データ用ResNet風アーキテクチャは、今日作ったResidualMLPBlockとほぼ同じ発想(Pre-Norm + Skip接続のMLPブロックを何層も積む)を採用している。

応用

🎯 GBDTアンサンブルへの多様性の追加

似た予測傾向のGBDTを崩す

GBDT系のモデルは似た予測傾向を持ちやすい。Skip接続を持つ深いMLPはGBDTとは異なる「間違え方」をする傾向があり、アンサンブルに加えるとLBが改善する事例がよくある。

発展

🔧 ドメイン知識に合わせたブロック自作

上位解法の差別化ポイント

「カテゴリ変数のEmbeddingを合流させるブロック」「時系列特徴量専用のブロック」など、データの構造に合わせてブロックを自作することが差別化になる。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
Skip接続さえあれば正規化(LayerNorm/BatchNorm)は不要だと考えてしまう「足し算するだけ」というSkip接続のシンプルさから他の工夫が不要に思える今日の実験でもnorm(正規化のみ)はplainより大きく改善しており、正規化とSkip接続はそれぞれ別の理由で学習を助ける独立した工夫。両方を組み合わせるのが最も頑健
ResidualMLPBlockの入力次元と出力次元を自由に変えられると思い込む通常のLinear層は入出力の次元を自由に設定できるためSkip接続でx + F(x)を計算するには次元が完全に一致している必要がある。次元を変えたい場合はx側にもLinearによる射影(projection shortcut)を追加しないと単純な足し算ができずエラーになる
深いネットワークの学習がうまくいかない原因を「過学習」だと誤診してしまう深いモデル=表現力が高い=過学習しやすい、という一般論からの類推劣化問題はtrain誤差自体が高いまま下がらない現象であり、過学習(trainは良いがtestが悪い)とは全く別の症状。trainのlossカーブがそもそも下がっているかを必ず確認すべき
Pre-NormとPost-Normの違いを「どちらでも大差ない些細な実装の好み」だと考えてしまうどちらもLayerNormを使う点では同じに見える層を深く積むほど、LayerNormをSkip接続の外側に置くか内側に置くかで、恒等写像の経路が正規化の影響を受けるかどうかが変わり、学習の安定性に無視できない差が出る

🚀 次のステップ

  • 発展: 今日はdimを固定したResidualMLPBlockのみを扱いましたが、実際のResNet論文では「次元を変える箇所だけprojection shortcut(x側にも1層Linearを挟む)を使う」という設計がされています。ResidualMLPBlockを拡張し、in_dim != out_dimのときは自動的にx側に射影用のLinearを追加するin_dim・out_dim可変版を実装し、次元を徐々に変化させる(例: 64→32→16)深いネットワークを組んでみましょう
  • 次回予告: Phase6テーマ2「カスタムアーキテクチャ設計」2日目。今日作ったResidualMLPBlockをベースに、Gated(ゲート機構付き)MLPブロックを自作し、「足すか足さないかを0/1で決める」のではなく「どれくらいの割合で足すか」を学習可能なゲート(シグモイド関数で0〜1に制限した重み)で制御する設計に挑みます

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

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

Day126〜130でLabel Smoothing・SAM・EMA・Mixup・Knowledge Distillationと「論文→実装」ワークフローを完走。今日からはブロックを自分で設計する「カスタムアーキテクチャ設計」フェーズに移行し、Residual接続を皮切りに5回のブロック設計を反復する。

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

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