"""
=============================================================================
CS230 · 第 13 讲工具模块：经典 CNN 架构的"积木"与计数工具
=============================================================================

对应讲义：lectures/lecture13_deep_cnn_architectures.md（§2 经典网络、§3 ResNet、§4 1×1 与 Inception、
         §5 MobileNet 与 EfficientNet、§6 DenseNet）

-----------------------------------------------------------------------------
【这个模块里有什么】
-----------------------------------------------------------------------------
第 12 讲讲了卷积层、池化层本身；本讲关心的是**怎样把它们搭成一个好的网络**。这个模块把本讲出现的
所有"积木"都用 PyTorch 写出来，每一块都很短，可以逐行对照讲义的公式：

    0. 计数工具
        conv_output_size     输出尺寸公式 ⌊(n + 2p − f)/s⌋ + 1（第 12 讲）
        layer_report         让一个网络"空跑"一遍，记下每一层的输出形状、参数个数、乘加次数（MACs）
        print_layer_table    把 layer_report 的结果打印成表格
    1. 经典网络（§2）
        lenet5、alexnet、vgg16
    2. 残差网络（§3）
        BasicBlock           两个 3×3 卷积；residual=True 时加一条"捷径"（shortcut）—— 同一个类同时表示 plain 和 residual
        cifar_resnet         He et al. 2016 在 CIFAR-10 上用的 6n + 2 层结构（缩小版），代码 02 用它比较 plain 与 residual
        IdentityBlock        ResNet-50 的 bottleneck 恒等块（输入输出形状相同，捷径直接相加）
        ConvolutionalBlock   ResNet-50 的 bottleneck 卷积块（形状变了，捷径上用 1×1 卷积投影）
        resnet50             用上面两种块搭出 ResNet-50（和 Coursera 作业 "Residual Networks" 同构）
    3. 1×1 卷积与 Inception（§4）
        InceptionModule      四条并行分支（1×1、1×1→3×3、1×1→5×5、池化→1×1），在通道维上拼接
    4. 高效网络（§5）
        DepthwiseSeparable   MobileNet v1：depthwise 3×3 + pointwise 1×1
        InvertedResidual     MobileNet v2：1×1 扩张 → depthwise 3×3 → 1×1 线性投影 + 残差
        mobilenet_v1、mobilenet_v2、tiny_cnn、tiny_mobilenet
    5. DenseNet（§6）
        DenseBlock           每一层的输入 = 前面所有层输出的**拼接**

-----------------------------------------------------------------------------
【参数量和乘加次数（MACs）怎么数】
-----------------------------------------------------------------------------
    卷积层 Conv2d(C_in → C_out, f×f, groups=g)：
        参数   = C_out · (C_in / g) · f · f  （+ C_out 个偏置）
        MACs   = H_out · W_out · C_out · (C_in / g) · f · f      （每个输出数要做 (C_in/g)·f·f 次乘加）
    全连接层 Linear(n_in → n_out)：参数 = n_in · n_out + n_out，MACs = n_in · n_out
    BN、ReLU、池化、残差相加：参数很少或没有，乘加次数和卷积比可以忽略，这里记作 0。
"乘加"（multiply–accumulate）= 一次乘法加一次加法。论文里常说的 "FLOPs" 有时指 MACs，有时指 2 × MACs，
读论文时要看清楚；本讲一律用 MACs（Ng 在 Inception 那个例子里数的"乘法次数"就是它）。

-----------------------------------------------------------------------------
【meta 设备：不分配内存地"搭"一个大网络】
-----------------------------------------------------------------------------
VGG-16 有 1.38 亿个参数，真的建出来要占 0.55 GB 内存，而我们只想数一数。PyTorch 的 "meta" 设备上的张量
**只有形状、没有数据**：

    with torch.device("meta"):
        model = vgg16()                       # 瞬间建好，不占内存
    rows = layer_report(model, (3, 224, 224)) # 用 meta 张量"空跑"一遍，照样能拿到每一层的输出形状

【注释标签】【思路】【参数】【返回】【边界】【DL 联系】【PyTorch 小知识】【Python 小知识】

直接运行本文件做一个自检（约 2 秒）：各个块的输出形状、几个著名网络的参数总数是否和公认的数字一致。
    python3 arch_blocks.py
"""

import os
import sys

import torch
from torch import nn

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "lecture01"))
from console import pad  # noqa: E402  （第 1 讲的小工具：按显示宽度补空格）


# =============================================================================
# 0. 计数工具
# =============================================================================
def conv_output_size(n, f, p=0, s=1):
    """
    卷积 / 池化的输出边长：⌊(n + 2p − f) / s⌋ + 1（第 12 讲的公式）。

    【参数】n：输入边长；f：核大小；p：每边补零的圈数；s：步长
    【返回】int
    【Python 小知识】// 是整除（向下取整）：7 // 2 == 3。
    """
    return (n + 2 * p - f) // s + 1


def _macs(module, out):
    """
    一层的乘加次数（只算 Conv2d 和 Linear，其他层记 0）。out 是这一层的输出张量（batch 维 = 1）。

    【思路】见模块开头的公式。groups 是"分组卷积"的组数：depthwise 卷积是 groups = C_in 的特例。
    """
    if isinstance(module, nn.Conv2d):
        _, c_out, h, w = out.shape
        kh, kw = module.kernel_size
        return h * w * c_out * (module.in_channels // module.groups) * kh * kw
    if isinstance(module, nn.Linear):
        return module.in_features * module.out_features
    return 0


def layer_report(model, input_shape):
    """
    让模型"空跑"一个样本，记下每个**叶子层**（不再包含子层的层）的输出形状、参数个数和乘加次数。

    【思路】PyTorch 允许给每一层注册一个 forward hook（钩子）：这一层每算完一次前向，就调用一次我们的函数，
            把 (层, 输入, 输出) 传进来。我们在钩子里记一行；钩子被调用的顺序就是前向的执行顺序。
            参数只在一层**第一次**被调用时计入（同一个 ReLU 对象在一个块里用两次时，不重复计数）。
    【参数】
        model       : nn.Module。可以在 CPU 上，也可以在 meta 设备上（见模块开头）
        input_shape : 一个样本的形状，例如 (3, 224, 224)；函数自己补上 batch 维 1
    【返回】list[dict]，每个 dict：{"name", "type", "out_shape"（不含 batch 维的 tuple）, "params", "macs"}
    【边界】残差相加、torch.cat 这类写在 forward 里的运算不是"层"，不会出现在表里（它们没有参数、乘加可以忽略）。
            函数结束前会把钩子全部移除，模型不受影响；会临时切到 eval 模式（BN 用 running 统计量），最后恢复原模式。
    【PyTorch 小知识】
        model.named_modules()        ：递归地列出所有子层 (名字, 层)，名字像 "features.3.conv1"
        list(m.children())           ：直接子层；为空说明是叶子层
        h = m.register_forward_hook(f)：注册钩子；h.remove() 移除
        next(model.parameters())     ：取第一个参数张量（用来知道模型在哪个设备上）
    """
    rows, seen = [], set()

    def hook(module, inputs, out):
        first = id(module) not in seen
        seen.add(id(module))
        rows.append({
            "name": names[id(module)],
            "type": type(module).__name__,
            "out_shape": tuple(out.shape[1:]),
            "params": sum(p.numel() for p in module.parameters(recurse=False)) if first else 0,
            "macs": _macs(module, out),
        })

    names, handles = {}, []
    for name, module in model.named_modules():
        if len(list(module.children())) == 0:
            names[id(module)] = name
            handles.append(module.register_forward_hook(hook))
    device = next(model.parameters()).device
    was_training = model.training
    model.eval()
    try:
        with torch.no_grad():
            model(torch.zeros((1,) + tuple(input_shape), device=device))
    finally:
        for h in handles:
            h.remove()
        model.train(was_training)
    return rows


def totals(rows):
    """layer_report 结果的总参数个数和总乘加次数：返回 (params, macs)。"""
    return sum(r["params"] for r in rows), sum(r["macs"] for r in rows)


def human(n):
    """把大数字写成 "1.38 亿" / "1244 万" 这样好读的形式（中文习惯：万 = 1e4，亿 = 1e8）。返回字符串。"""
    if n >= 1e8:
        return f"{n / 1e8:.2f} 亿"
    if n >= 1e4:
        return f"{n / 1e4:.0f} 万"
    return f"{n:,}"


def print_layer_table(rows, keep=("Conv2d", "Linear", "MaxPool2d", "AvgPool2d", "AdaptiveAvgPool2d",
                                  "LocalResponseNorm")):
    """
    把 layer_report 的结果打印成表格（只列出 keep 里的层类型，BN / ReLU 之类一笔带过）。

    【参数】rows：layer_report 的返回值；keep：要列出的层类型名
    【返回】None（直接打印）
    【Python 小知识】f"{x:>12,}" ：右对齐、宽 12、千位加逗号。
    """
    print(pad("层（模块名）", 26) + pad("类型", 18) + pad("输出形状 (C, H, W)", 20) + pad("参数", 14, ">")
          + pad("乘加 MACs", 16, ">"))
    for r in rows:
        if r["type"] in keep:
            shape = "×".join(str(d) for d in r["out_shape"])
            print(pad(r["name"], 26) + pad(r["type"], 18) + pad(shape, 20) + f"{r['params']:>14,}{r['macs']:>16,}")
    p, m = totals(rows)
    print(pad("合计（含 BN 等所有层）", 64) + f"{p:>14,}{m:>16,}")


# =============================================================================
# 1. 经典网络（§2）
# =============================================================================
def lenet5(n_classes=10, act=nn.Tanh):
    """
    LeNet-5（LeCun et al. 1998），按 Ng 课上的简化版：输入 32×32×1。

        conv 5×5, 6 → 28×28×6 → 平均池化 2×2 s2 → 14×14×6
        conv 5×5, 16 → 10×10×16 → 平均池化 → 5×5×16 → 拉平 400 → FC 120 → FC 84 → 输出 10

    【参数】n_classes：类别数；act：激活函数类（原论文的年代用 sigmoid / tanh 一类的 S 形函数，这里默认 tanh）
    【返回】nn.Sequential
    【边界】和原论文的差别（讲义 §2.1）：原论文第二个卷积层只连接部分通道、池化层有可训练的系数、
            输出层是 RBF 单元而不是 softmax。这里按今天的写法简化，参数个数因此略多（61,706 vs 约 6 万）。
    """
    return nn.Sequential(
        nn.Conv2d(1, 6, 5), act(), nn.AvgPool2d(2, 2),
        nn.Conv2d(6, 16, 5), act(), nn.AvgPool2d(2, 2),
        nn.Flatten(),
        nn.Linear(400, 120), act(), nn.Linear(120, 84), act(), nn.Linear(84, n_classes),
    )


def alexnet(n_classes=1000, two_gpu=False):
    """
    AlexNet（Krizhevsky, Sutskever & Hinton 2012），输入 227×227×3（论文写 224，但只有 227 才能让第一层输出 55×55）。

        conv 11×11 s4, 96 → 55×55 → LRN → 最大池化 3×3 s2 → 27×27
        conv 5×5 same, 256 → LRN → 池化 → 13×13
        conv 3×3 same, 384 → conv 3×3, 384 → conv 3×3, 256 → 池化 → 6×6×256 = 9216
        FC 4096 → FC 4096 → 1000

    【参数】
        n_classes : 类别数
        two_gpu   : True 时模仿原论文的"两块 GPU"：第 2、4、5 个卷积层分成两组，每组只看自己那块 GPU 上的一半通道
                    （groups=2）。参数和乘加次数因此少一些（约 6100 万 vs 约 6240 万参数）。
    【返回】nn.Sequential
    【DL 联系】LocalResponseNorm（LRN）是 AlexNet 用的一种"相邻通道之间互相抑制"的归一化，后来的研究发现它
            作用不大，今天基本没人用（被 Batch Norm 取代，第 9 讲）。Dropout 用在前两个全连接层（第 6 讲）。
    """
    g = 2 if two_gpu else 1
    return nn.Sequential(
        nn.Conv2d(3, 96, 11, stride=4), nn.ReLU(), nn.LocalResponseNorm(5), nn.MaxPool2d(3, 2),
        nn.Conv2d(96, 256, 5, padding=2, groups=g), nn.ReLU(), nn.LocalResponseNorm(5), nn.MaxPool2d(3, 2),
        nn.Conv2d(256, 384, 3, padding=1), nn.ReLU(),
        nn.Conv2d(384, 384, 3, padding=1, groups=g), nn.ReLU(),
        nn.Conv2d(384, 256, 3, padding=1, groups=g), nn.ReLU(), nn.MaxPool2d(3, 2),
        nn.Flatten(),
        nn.Dropout(0.5), nn.Linear(9216, 4096), nn.ReLU(),
        nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(),
        nn.Linear(4096, n_classes),
    )


def vgg16(n_classes=1000):
    """
    VGG-16（Simonyan & Zisserman 2014），输入 224×224×3。

    【思路】整个网络只有两种"零件"：**3×3、same、stride 1 的卷积** 和 **2×2、stride 2 的最大池化**。
            五个阶段的通道数 64 → 128 → 256 → 512 → 512（前四次每次翻倍），每个阶段结束时池化让边长减半：
            224 → 112 → 56 → 28 → 14 → 7。13 个卷积层 + 3 个全连接层 = 16 个有权重的层。
    【参数】n_classes：类别数
    【返回】nn.Sequential
    """
    cfg = [(64, 2), (128, 2), (256, 3), (512, 3), (512, 3)]       # (通道数, 这个阶段有几个卷积层)
    layers, c_in = [], 3
    for c_out, n_conv in cfg:
        for _ in range(n_conv):
            layers += [nn.Conv2d(c_in, c_out, 3, padding=1), nn.ReLU()]
            c_in = c_out
        layers.append(nn.MaxPool2d(2, 2))
    layers += [nn.Flatten(),
               nn.Linear(512 * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
               nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
               nn.Linear(4096, n_classes)]
    return nn.Sequential(*layers)


# =============================================================================
# 2. 残差网络（§3）
# =============================================================================
class BasicBlock(nn.Module):
    """
    两个 3×3 卷积组成的块。residual=True 时是 ResNet 的残差块，False 时是同样结构的 "plain" 块。

        plain   ：a → conv → (BN) → ReLU → conv → (BN) → ReLU                     = g(z[l+2])
        residual：a → conv → (BN) → ReLU → conv → (BN) → (+ shortcut(a)) → ReLU   = g(z[l+2] + a[l])

    【思路】对应讲义 §3.3 的公式 a[l+2] = g(z[l+2] + a[l])：残差块只比 plain 块多了"加上输入"这一步。
            如果这一块的输出和输入形状不同（stride = 2 让边长减半、或者通道数变了），a[l] 没法直接相加，
            就在捷径上放一个 1×1 卷积（+ BN）把它"投影"成同样的形状 —— 这就是讲义里的 W_s。
    【参数】
        c_in, c_out : 输入 / 输出通道数
        stride      : 第一个卷积的步长（1 或 2）
        residual    : 是否加捷径
        bn          : 是否用 BatchNorm2d（用 BN 时卷积不要偏置，第 9 讲 §2.3）
        zero_init   : True 时把**这一块最后一层**初始化为 0（有 BN 时是第二个 BN 的 γ，没有 BN 时是第二个卷积的权重）。
                      于是刚初始化时残差分支输出 0，整块就是恒等映射（讲义 §3.4、§3.6）。plain 块不要用（输出会恒为 0）。
    【边界】所有卷积（包括捷径上的 1×1 卷积）的权重都用 He 初始化（第 7 讲；torchvision 的 ResNet 也是这样），偏置初始化为 0。
    【PyTorch 小知识】nn.Identity() 什么都不做，原样返回输入；用它占位，forward 里就不用写 if。
    """

    def __init__(self, c_in, c_out, stride=1, residual=True, bn=True, zero_init=False):
        super().__init__()
        self.residual = residual
        self.conv1 = nn.Conv2d(c_in, c_out, 3, stride, 1, bias=not bn)
        self.bn1 = nn.BatchNorm2d(c_out) if bn else nn.Identity()
        self.conv2 = nn.Conv2d(c_out, c_out, 3, 1, 1, bias=not bn)
        self.bn2 = nn.BatchNorm2d(c_out) if bn else nn.Identity()
        self.relu = nn.ReLU()
        self.shortcut = nn.Identity()
        if residual and (stride != 1 or c_in != c_out):
            layers = [nn.Conv2d(c_in, c_out, 1, stride, bias=not bn)]
            if bn:
                layers.append(nn.BatchNorm2d(c_out))
            self.shortcut = nn.Sequential(*layers)
        for conv in [self.conv1, self.conv2] + [m for m in self.shortcut.modules() if isinstance(m, nn.Conv2d)]:
            nn.init.kaiming_normal_(conv.weight, nonlinearity="relu")
            if conv.bias is not None:
                nn.init.zeros_(conv.bias)
        if zero_init:
            target = self.bn2.weight if bn else self.conv2.weight
            nn.init.zeros_(target)

    def forward(self, a):
        z = self.bn1(self.conv1(a))
        z = self.bn2(self.conv2(self.relu(z)))          # z[l+2]（还没有激活）
        if self.residual:
            z = z + self.shortcut(a)                    # z[l+2] + a[l]：残差连接就是这一行
        return self.relu(z)


def cifar_resnet(n, residual=True, bn=True, widths=(8, 16, 32), stem_stride=2, n_classes=8, in_ch=3,
                 zero_init=False):
    """
    He et al. (2016) 在 CIFAR-10 上比较 plain / residual 时用的 6n + 2 层网络（这里是缩小版）。

        stem：3×3 卷积（stride = stem_stride）→ (BN) → ReLU
        三个阶段，每个阶段 n 个 BasicBlock（每块 2 个卷积层），通道数 widths[0] → widths[1] → widths[2]；
        第 2、3 阶段的第一块 stride = 2，边长减半、通道翻倍
        全局平均池化（global average pooling）→ 全连接层输出 logits
    数层数：1（stem）+ 6n（块里的卷积）+ 1（全连接）= 6n + 2。n = 1, 3, 5 → 8, 20, 32 层。

    【参数】
        n           : 每个阶段的块数
        residual    : True 是 ResNet，False 是同样深、同样宽的 plain 网络（逐层一一对应，只差捷径）
        bn          : 是否用 BN
        widths      : 三个阶段的通道数（原论文 16, 32, 64；我们用更窄的 8, 16, 32 让 CPU 跑得动）
        stem_stride : 输入 16×16 时用 2，三个阶段的边长是 8 → 4 → 2（同样是为了省计算）
        n_classes   : 类别数；in_ch：输入通道数；zero_init：见 BasicBlock
    【返回】nn.Sequential
    【边界】residual 网络在通道数变化的两处有 1×1 投影捷径（原论文的 CIFAR 实验用的是补零的捷径"option A"，
            我们用投影"option B"，多了几百个参数，讲义 §3.5 里有说明）。
    """
    layers = [nn.Conv2d(in_ch, widths[0], 3, stem_stride, 1, bias=not bn)]
    nn.init.kaiming_normal_(layers[0].weight, nonlinearity="relu")
    if not bn:
        nn.init.zeros_(layers[0].bias)
    layers += [nn.BatchNorm2d(widths[0]) if bn else nn.Identity(), nn.ReLU()]
    c_in = widths[0]
    for stage, c_out in enumerate(widths):
        for b in range(n):
            stride = 2 if (stage > 0 and b == 0) else 1
            layers.append(BasicBlock(c_in, c_out, stride, residual, bn, zero_init))
            c_in = c_out
    layers += [nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(c_in, n_classes)]
    return nn.Sequential(*layers)


class IdentityBlock(nn.Module):
    """
    ResNet-50 的"恒等块"（identity block，Coursera 作业 "Residual Networks" 里的 identity_block）：

        a → 1×1 卷积 F1 → BN → ReLU → 3×3 卷积 F2（same）→ BN → ReLU → 1×1 卷积 F3 → BN → (+ a) → ReLU

    【思路】这是 **bottleneck**（瓶颈）结构：先用 1×1 卷积把通道压窄（比如 256 → 64），在窄的通道上做昂贵的 3×3，
            再用 1×1 恢复（64 → 256）。和"两个 3×3"相比，用更少的计算换来更多的层（讲义 §3.5、§4）。
            输入输出形状完全相同（要求 c_in == F3），所以捷径就是 a 本身。
    【参数】c_in：输入通道数（必须等于 filters[2]）；filters：(F1, F2, F3)
    【边界】c_in != F3 时直接报错 —— 这时应该用 ConvolutionalBlock。
    """

    def __init__(self, c_in, filters):
        super().__init__()
        f1, f2, f3 = filters
        if c_in != f3:
            raise ValueError(f"恒等块要求输入通道 {c_in} == F3 = {f3}；形状变了请用 ConvolutionalBlock")
        self.main = nn.Sequential(
            nn.Conv2d(c_in, f1, 1, bias=False), nn.BatchNorm2d(f1), nn.ReLU(),
            nn.Conv2d(f1, f2, 3, padding=1, bias=False), nn.BatchNorm2d(f2), nn.ReLU(),
            nn.Conv2d(f2, f3, 1, bias=False), nn.BatchNorm2d(f3),
        )
        self.relu = nn.ReLU()

    def forward(self, a):
        return self.relu(self.main(a) + a)


class ConvolutionalBlock(nn.Module):
    """
    ResNet-50 的"卷积块"（convolutional block，作业里的 convolutional_block）：主路径和恒等块一样，
    但第一个 1×1 卷积的步长是 s（边长缩小 s 倍），而且输出通道 F3 ≠ 输入通道，所以捷径上要放
    一个 1×1、步长 s 的卷积 + BN，把 a 投影成同样的形状（这就是讲义里的 W_s）。

    【参数】c_in：输入通道数；filters：(F1, F2, F3)；s：步长
    【边界】步长放在第一个 1×1 卷积上，和原论文、Coursera 作业一致；torchvision 的 "ResNet v1.5" 把步长放在
            3×3 卷积上（准确率略高、计算量略大），参数个数完全相同。
    """

    def __init__(self, c_in, filters, s=2):
        super().__init__()
        f1, f2, f3 = filters
        self.main = nn.Sequential(
            nn.Conv2d(c_in, f1, 1, stride=s, bias=False), nn.BatchNorm2d(f1), nn.ReLU(),
            nn.Conv2d(f1, f2, 3, padding=1, bias=False), nn.BatchNorm2d(f2), nn.ReLU(),
            nn.Conv2d(f2, f3, 1, bias=False), nn.BatchNorm2d(f3),
        )
        self.shortcut = nn.Sequential(nn.Conv2d(c_in, f3, 1, stride=s, bias=False), nn.BatchNorm2d(f3))
        self.relu = nn.ReLU()

    def forward(self, a):
        return self.relu(self.main(a) + self.shortcut(a))


def resnet50(n_classes=1000):
    """
    ResNet-50（He et al. 2016）：stem + 4 个阶段（3、4、6、3 个 bottleneck 块）+ 全局平均池化 + 全连接。

        stem   ：7×7 卷积 64, s2 → BN → ReLU → 3×3 最大池化 s2
        阶段 2 ：卷积块 [64, 64, 256], s = 1 + 2 个恒等块
        阶段 3 ：卷积块 [128, 128, 512], s = 2 + 3 个恒等块
        阶段 4 ：卷积块 [256, 256, 1024], s = 2 + 5 个恒等块
        阶段 5 ：卷积块 [512, 512, 2048], s = 2 + 2 个恒等块
        全局平均池化 → FC n_classes
    数层数：1 + 3·(3 + 4 + 6 + 3) + 1 = 50。

    【参数】n_classes：类别数（Coursera 作业用 SIGNS 手势，是 6）
    【返回】nn.Sequential
    【边界】作业里输入是 64×64、最后用 2×2 平均池化；这里用 AdaptiveAvgPool2d(1)，任何输入尺寸都能用。
    """
    layers = [nn.Conv2d(3, 64, 7, stride=2, padding=3, bias=False), nn.BatchNorm2d(64), nn.ReLU(),
              nn.MaxPool2d(3, stride=2, padding=1)]
    c_in = 64
    for f1, n_blocks, s in [(64, 3, 1), (128, 4, 2), (256, 6, 2), (512, 3, 2)]:
        filters = (f1, f1, 4 * f1)
        layers.append(ConvolutionalBlock(c_in, filters, s))
        c_in = 4 * f1
        for _ in range(n_blocks - 1):
            layers.append(IdentityBlock(c_in, filters))
    layers += [nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(c_in, n_classes)]
    return nn.Sequential(*layers)


# =============================================================================
# 3. 1×1 卷积与 Inception（§4）
# =============================================================================
class InceptionModule(nn.Module):
    """
    Inception 模块（Szegedy et al. 2015，GoogLeNet）：同一个输入同时走四条分支，输出在通道维上拼接。

        分支 1：1×1 卷积 c1
        分支 2：1×1 卷积 c3r（压缩通道）→ 3×3 卷积 c3
        分支 3：1×1 卷积 c5r（压缩通道）→ 5×5 卷积 c5
        分支 4：3×3 最大池化（stride 1, same）→ 1×1 卷积 pool_proj
        输出通道 = c1 + c3 + c5 + pool_proj，边长和输入相同

    【思路】"不知道用多大的卷积核？那就都用上，让网络自己决定每条分支有多重要"（讲义 §4.2）。
            1×1 的"压缩"卷积（bottleneck）让 3×3、5×5 在窄的通道上计算，省下大量乘法（讲义 §4.3，Ng 的例子：约 1.2 亿 → 约 1240 万）。
            reduce=False 时去掉所有压缩（c3r、c5r、pool_proj 那三个 1×1），得到"朴素版" Inception 作对照。
    【参数】c_in；c1, c3r, c3, c5r, c5, pool_proj：各分支通道数（GoogLeNet 论文 Table 1 的列名）；reduce：见上
    【返回】forward 的输出形状 (batch, 输出通道, H, W)
    【PyTorch 小知识】torch.cat([t1, t2, ...], dim=1)：沿第 1 维（通道维）把几个张量拼起来，其他维必须相同。
    """

    def __init__(self, c_in, c1, c3r, c3, c5r, c5, pool_proj, reduce=True):
        super().__init__()

        def cr(ci, co, f):
            return nn.Sequential(nn.Conv2d(ci, co, f, padding=f // 2), nn.ReLU())

        self.b1 = cr(c_in, c1, 1)
        if reduce:
            self.b2 = nn.Sequential(cr(c_in, c3r, 1), cr(c3r, c3, 3))
            self.b3 = nn.Sequential(cr(c_in, c5r, 1), cr(c5r, c5, 5))
            self.b4 = nn.Sequential(nn.MaxPool2d(3, 1, 1), cr(c_in, pool_proj, 1))
        else:
            self.b2 = cr(c_in, c3, 3)
            self.b3 = cr(c_in, c5, 5)
            self.b4 = nn.MaxPool2d(3, 1, 1)                 # 朴素版：池化分支原样输出 c_in 个通道

    def forward(self, x):
        return torch.cat([self.b1(x), self.b2(x), self.b3(x), self.b4(x)], dim=1)


# =============================================================================
# 4. 高效网络：MobileNet v1 / v2（§5）
# =============================================================================
def conv_bn_act(c_in, c_out, f=3, stride=1, groups=1, act=nn.ReLU6):
    """
    卷积 → BN → 激活，MobileNet 里反复出现的三件套。act=None 表示不加激活（"线性"瓶颈）。

    【参数】f：核大小（padding 自动取 f // 2，即 same）；groups：分组数（depthwise 时 = c_in）；act：激活函数类
    【DL 联系】ReLU6(z) = min(max(0, z), 6)。MobileNet 用它，是因为输出被限制在 [0, 6]，低精度（8 位）部署时更稳。
    """
    layers = [nn.Conv2d(c_in, c_out, f, stride, f // 2, groups=groups, bias=False), nn.BatchNorm2d(c_out)]
    if act is not None:
        layers.append(act())
    return nn.Sequential(*layers)


class DepthwiseSeparable(nn.Module):
    """
    MobileNet v1 的 depthwise separable 卷积 = depthwise 3×3 + pointwise 1×1。

        depthwise：每个输入通道**各用一个自己的** 3×3 核，只在空间上滤波，通道之间不混合（groups = C_in）
        pointwise：1×1 卷积，在每个像素上把 C_in 个通道线性组合成 C_out 个通道（只混合通道，不看邻居）

    【思路】普通卷积一步同时做"空间滤波"和"通道混合"；这里拆成两步，乘法次数从
            H·W·C_out·C_in·f² 降到 H·W·C_in·f² + H·W·C_in·C_out，比例 = 1/C_out + 1/f²（讲义 §5.2）。
    【参数】c_in, c_out：通道数；stride：depthwise 那一步的步长
    """

    def __init__(self, c_in, c_out, stride=1):
        super().__init__()
        self.depthwise = conv_bn_act(c_in, c_in, 3, stride, groups=c_in)
        self.pointwise = conv_bn_act(c_in, c_out, 1)

    def forward(self, x):
        return self.pointwise(self.depthwise(x))


class InvertedResidual(nn.Module):
    """
    MobileNet v2 的 bottleneck 块（"倒残差" inverted residual）：

        x → 1×1 扩张（C_in → t·C_in）→ BN → ReLU6 → depthwise 3×3 → BN → ReLU6 → 1×1 投影（→ C_out）→ BN → (+ x)

    【思路】
        · 扩张（expansion）：depthwise 卷积很便宜，所以先把通道"撑宽" t 倍（默认 6），让它在宽的空间里提特征；
        · 投影（projection）：再用 1×1 压回窄的通道。投影之后**不加 ReLU**（线性瓶颈）：Sandler et al. 认为
          在很窄的通道上做 ReLU 会丢信息；
        · 残差：stride = 1 且 C_in == C_out 时，把输入直接加到输出上 —— 捷径连接的是两头"窄"的部分。
          ResNet 的 bottleneck 是"宽 → 窄 → 宽"、捷径连宽的两头；这里反过来是"窄 → 宽 → 窄"，所以叫 inverted。
    【参数】c_in, c_out；stride：depthwise 的步长；t：扩张倍数（t = 1 时省掉扩张那一层）
    """

    def __init__(self, c_in, c_out, stride=1, t=6):
        super().__init__()
        hidden = c_in * t
        layers = [] if t == 1 else [conv_bn_act(c_in, hidden, 1)]
        layers += [conv_bn_act(hidden, hidden, 3, stride, groups=hidden),
                   conv_bn_act(hidden, c_out, 1, act=None)]          # 线性瓶颈：没有激活
        self.block = nn.Sequential(*layers)
        self.use_res = stride == 1 and c_in == c_out

    def forward(self, x):
        out = self.block(x)
        return x + out if self.use_res else out


def mobilenet_v1(n_classes=1000, alpha=1.0):
    """
    MobileNet v1（Howard et al. 2017）：一个普通 3×3 卷积 + 13 个 depthwise separable 块 + 平均池化 + FC。

    【参数】n_classes；alpha：宽度乘子（width multiplier），每层通道数乘以 alpha（论文的 α，1.0 是标准版）
    【返回】nn.Sequential
    """
    def c(ch):
        return int(ch * alpha)

    cfg = [(64, 1), (128, 2), (128, 1), (256, 2), (256, 1), (512, 2)] + [(512, 1)] * 5 + [(1024, 2), (1024, 1)]
    layers, c_in = [conv_bn_act(3, c(32), 3, 2, act=nn.ReLU)], c(32)
    for ch, s in cfg:
        layers.append(DepthwiseSeparable(c_in, c(ch), s))
        c_in = c(ch)
    layers += [nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(c_in, n_classes)]
    return nn.Sequential(*layers)


def mobilenet_v2(n_classes=1000):
    """
    MobileNet v2（Sandler et al. 2018），结构和 torchvision 的 mobilenet_v2 一致（宽度 1.0）。

        3×3 卷积 32, s2 → 17 个 InvertedResidual（论文 Table 2 的 (t, c, n, s)）→ 1×1 卷积 1280 → 平均池化 → FC

    【返回】nn.Sequential
    """
    cfg = [(1, 16, 1, 1), (6, 24, 2, 2), (6, 32, 3, 2), (6, 64, 4, 2), (6, 96, 3, 1), (6, 160, 3, 2), (6, 320, 1, 1)]
    layers, c_in = [conv_bn_act(3, 32, 3, 2)], 32
    for t, ch, n, s in cfg:
        for i in range(n):
            layers.append(InvertedResidual(c_in, ch, s if i == 0 else 1, t))
            c_in = ch
    layers += [conv_bn_act(c_in, 1280, 1), nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.2),
               nn.Linear(1280, n_classes)]
    return nn.Sequential(*layers)


def tiny_cnn(n_classes, widths=(16, 32, 64), mode="standard", depth_mult=1, expansion=4):
    """
    代码 03 用的三个"同骨架"小网络：只换卷积块的类型，比较参数量、计算量和准确率。

        stem   ：普通 3×3 卷积 3 → widths[0]（24×24）
        阶段 1 ：块(widths[0] → widths[1], stride 2) + (depth_mult − 1) 个块(widths[1] → widths[1])（12×12）
        阶段 2 ：块(widths[1] → widths[2], stride 2) + (depth_mult − 1) 个块（6×6）
        全局平均池化 → FC
    mode：
        "standard"  ：块 = 两个普通 3×3 卷积（conv-BN-ReLU × 2），第一个带 stride
        "separable" ：块 = 两个 DepthwiseSeparable（MobileNet v1 风格）
        "inverted"  ：块 = 两个 InvertedResidual（MobileNet v2 风格，扩张倍数 expansion）

    【参数】n_classes；widths；mode；depth_mult：每个阶段重复几个块（EfficientNet 的"深度"）；expansion
    【返回】nn.Sequential
    【边界】三种 mode 的"层数"一样（每块两层），通道数一样，只是每层的卷积方式不同。
    """
    def block(ci, co, s):
        if mode == "standard":
            return nn.Sequential(conv_bn_act(ci, co, 3, s, act=nn.ReLU), conv_bn_act(co, co, 3, 1, act=nn.ReLU))
        if mode == "separable":
            return nn.Sequential(DepthwiseSeparable(ci, co, s), DepthwiseSeparable(co, co, 1))
        if mode == "inverted":
            return nn.Sequential(InvertedResidual(ci, co, s, expansion), InvertedResidual(co, co, 1, expansion))
        raise ValueError(mode)

    layers = [conv_bn_act(3, widths[0], 3, 1, act=nn.ReLU)]
    c_in = widths[0]
    for c_out in widths[1:]:
        layers.append(block(c_in, c_out, 2))
        for _ in range(depth_mult - 1):
            layers.append(block(c_out, c_out, 1))
        c_in = c_out
    layers += [nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(c_in, n_classes)]
    return nn.Sequential(*layers)


# =============================================================================
# 5. DenseNet（§6）
# =============================================================================
class DenseBlock(nn.Module):
    """
    DenseNet（Huang et al. 2017）的 dense block：第 l 层的输入是**前面所有层输出的拼接**。

        x0 → H1(x0) = x1 → H2([x0, x1]) = x2 → H3([x0, x1, x2]) = x3 → … → 输出 [x0, x1, …, xL]
        每个 H 是 BN → ReLU → 3×3 卷积，输出 growth 个通道（"增长率" k）

    【思路】ResNet 用**相加**把前一层的信息传下去：x + F(x)；DenseNet 用**拼接**：[x, F(x)]。
            拼接不会把信息"混"在一起，后面的层能直接看到前面每一层的特征，所以每层只需要产生很少的新通道（k 很小）。
            代价：通道数随层数线性增长（c_in + L·k），所以 DenseNet 在块之间用 1×1 卷积 + 池化的 transition 层压缩。
    【参数】c_in：输入通道；growth：每层新增的通道数 k；n_layers：层数 L
    【返回】forward 的输出形状 (batch, c_in + n_layers · growth, H, W)
    """

    def __init__(self, c_in, growth, n_layers):
        super().__init__()
        self.layers = nn.ModuleList()
        for i in range(n_layers):
            ci = c_in + i * growth
            self.layers.append(nn.Sequential(nn.BatchNorm2d(ci), nn.ReLU(), nn.Conv2d(ci, growth, 3, padding=1,
                                                                                         bias=False)))

    def forward(self, x):
        feats = [x]
        for layer in self.layers:
            feats.append(layer(torch.cat(feats, dim=1)))    # 输入 = 前面所有输出的拼接
        return torch.cat(feats, dim=1)


# =============================================================================
# 自检
# =============================================================================
if __name__ == "__main__":
    torch.manual_seed(0)
    x = torch.randn(2, 16, 8, 8)
    print("各个块的输出形状（输入 (2, 16, 8, 8)）：")
    for name, blk in [("BasicBlock(16→16)", BasicBlock(16, 16)), ("BasicBlock(16→32, s2)", BasicBlock(16, 32, 2)),
                      ("IdentityBlock(16, [4,4,16])", IdentityBlock(16, (4, 4, 16))),
                      ("ConvolutionalBlock(16, [8,8,32], s2)", ConvolutionalBlock(16, (8, 8, 32), 2)),
                      ("InceptionModule(16 → 8+8+4+4)", InceptionModule(16, 8, 4, 8, 2, 4, 4)),
                      ("DepthwiseSeparable(16→32)", DepthwiseSeparable(16, 32)),
                      ("InvertedResidual(16→16, t=6)", InvertedResidual(16, 16)),
                      ("DenseBlock(16, k=4, L=3)", DenseBlock(16, 4, 3))]:
        print(f"    {pad(name, 40)}→ {tuple(blk(x).shape)}")
    # 参照值：LeNet-5、AlexNet 是按模块开头的公式手算的；VGG-16、ResNet-50、MobileNet v2 和 torchvision 里同名模型的参数个数一致
    known = {"LeNet-5（Ng 简化版）": (lenet5, (1, 32, 32), 61706, "手算"),
             "AlexNet（单 GPU）": (alexnet, (3, 227, 227), 62378344, "手算"),
             "VGG-16": (vgg16, (3, 224, 224), 138357544, "torchvision"),
             "ResNet-50": (resnet50, (3, 224, 224), 25557032, "torchvision"),
             "MobileNet v2": (mobilenet_v2, (3, 224, 224), 3504872, "torchvision")}
    print("\n参数总数和参照值对照（在 meta 设备上建模型，不占内存）：")
    for name, (fn, shape, ref, src) in known.items():
        with torch.device("meta"):
            model = fn()
        p, m = totals(layer_report(model, shape))
        print(f"    {pad(name, 22)} 参数 {p:>12,}（参照 {ref:>12,}，{pad(src, 12)}{'一致' if p == ref else '不一致'}）   乘加 {m:>15,}")
