第 10 章 2019—2020:规模化——EfficientNet 与缩放定律
学习目标
- 理解 EfficientNet(2019)的 MBConv 模块与复合缩放
- 理解「缩放定律」:数据、参数、计算量如何共同决定模型性能
- 用 PyTorch 实现 MBConv 风格网络
- 用实验观察「数据越多 loss 越低;数据不足时大模型过拟合」的规律
10.1 时代背景:大模型与大数据的正反馈
2018 年后,计算机视觉和 NLP 同时进入「规模化」时代。视觉这边,2019 年 Google 的 EfficientNet 用复合缩放(同时放大深度、宽度、分辨率)在 ImageNet 上以 1/10 的计算量达到 SOTA。语言这边,OpenAI 的 GPT-2(2019)、GPT-3(2020)以及 Scaling Laws 论文(2020)揭示了一个更深的规律:模型性能随参数量、数据量、计算量的幂律增长。
这两件事指向同一句话:规模本身是力量。本章用两个可运行实验,把「规模化」讲透。
10.2 MBConv:高效卷积的基本单元
EfficientNet 的核心模块是 MBConv(Mobile Inverted Bottleneck Convolution),由 MobileNetV2 的倒置残差 + 挤压激励(SE)注意力组成:
- 1×1 升维:先把通道数扩大(如 4 倍),增加表达能力;
- 深度可分离卷积:每个通道单独做 3×3 卷积(分组数 = 通道数),参数从
C×C×9降到C×9; - 挤压激励(SE):全局平均池化 → 两个全连接 → sigmoid,给每个通道一个「重要性权重」,按通道加权;
- 1×1 降维:把通道数压回目标值,再接残差捷径。
examples/ch10_mbconv.py 实现:
class MBConv(nn.Module):
def __init__(self, in_c, out_c, expand=4, stride=1):
super().__init__()
hidden = in_c * expand
self.expand_conv = nn.Sequential(
nn.Conv2d(in_c, hidden, 1), nn.BatchNorm2d(hidden), nn.SiLU(),
) if expand > 1 else nn.Identity()
self.dw = nn.Sequential(
nn.Conv2d(hidden, hidden, 3, stride, padding=1, groups=hidden),
nn.BatchNorm2d(hidden), nn.SiLU(),
)
self.se = nn.Sequential(
nn.AdaptiveAvgPool2d(1), nn.Flatten(),
nn.Linear(hidden, max(8, hidden // 4)), nn.SiLU(),
nn.Linear(max(8, hidden // 4), hidden), nn.Sigmoid(),
)
self.project = nn.Sequential(nn.Conv2d(hidden, out_c, 1), nn.BatchNorm2d(out_c))
self.shortcut = nn.Sequential()
if stride != 1 or in_c != out_c:
self.shortcut = nn.Sequential(
nn.Conv2d(in_c, out_c, 1, stride), nn.BatchNorm2d(out_c),
)
def forward(self, x):
h = self.expand_conv(x)
h = self.dw(h)
h = h * self.se(h).view(-1, h.shape[1], 1, 1) # SE:按通道加权
return self.project(h) + self.shortcut(x)运行结果:
EfficientNet 风格参数量:120,386
epoch 1:训练准确率 = 86.72%
epoch 2:训练准确率 = 89.76%
epoch 3:训练准确率 = 89.67%
测试准确率 = 87.50%12 万参数达到 87.5%——比第 5 章 292 万参数的 TinyVGG(91.8%)低一些,但效率高一个数量级。EfficientNet 的复合缩放是在这个骨干上同时放大三件事:层数(深度)、每层通道数(宽度)、输入分辨率。论文给出一个观察:三者的最优比例大致是固定的——这就是「缩放」的由来。
10.3 缩放定律:三个变量的幂律
2020 年,OpenAI 的 Kaplan 等人发表《Scaling Laws for Neural Language Models》,发现神经语言模型的测试 loss 与三个量近似成幂律关系:
loss ≈ a·N^(-α) + b·D^(-β) + c0其中 N 是参数量,D 是数据量。含义:每翻一倍参数或数据,loss 以稳定比例下降;想同时压榨两者,要按比例同步放大(Chinchilla 定律,2022 年进一步精确了「参数:数据 ≈ 1:20」的最优配比)。
GPT-2(15 亿参数,2019)、GPT-3(1750 亿参数,2020)就是这个规律的工程兑现:模型更大、数据更多、计算更多 → 语言能力更强。ChatGPT 之所以震撼世界,不是因为发明了新架构,而是把已有的 Transformer 放大到足够大。
10.4 实验:在迷你世界里观察缩放
examples/ch10_scaling.py 在一个人造语言上做微型缩放实验:20 个符号,按固定 bigram 概率表生成文本(保证「真实分布」已知且可无限采样)。然后:
- 用不同大小的训练数据(10% / 30% / 100%)训练迷你 GPT;
- 用固定测试段评估;对比小模型(d=32)与大模型(d=64)。
运行结果:
随机猜测基线 loss ≈ 2.996
数据 10%:小模型(d=32) loss = 3.278,大模型(d=64) loss = 5.631
数据 30%:小模型(d=32) loss = 2.891,大模型(d=64) loss = 3.044
数据 100%:小模型(d=32) loss = 2.834,大模型(d=64) loss = 2.846读这张表,两个规律清晰可见:
- 数据越多,测试 loss 越低:对两种规模都成立(小模型 3.28→2.89→2.83;大模型 5.63→3.04→2.85);
- 数据不足时,大模型反而更差:10% 数据下,d=64 的 loss(5.63)远高于 d=32(3.28)——大模型参数多,小数据下严重过拟合;数据充足后两者趋同。
这就是 Chinchilla 定律的直觉版本:模型规模要和数据量匹配。真实的 GPT-3 训练用了约 3000 亿 token,正是因为 1750 亿参数需要这么多数据才能「吃饱」。
完整代码
本章用到的完整示例代码:
examples/ch10_mbconv.py
"""第 10 章示例(2019):EfficientNet 的 MBConv 模块。
MBConv = 1×1 升维 + 深度可分离卷积 + 挤压激励(SE)注意力 + 1×1 降维,
用更少的计算量取得更好的精度。
运行方式:
uv run python examples/ch10_mbconv.py
"""
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
torch.manual_seed(0)
device = "cuda" if torch.cuda.is_available() else "cpu"
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.2860,), (0.3530,)),
])
train = datasets.FashionMNIST("data", train=True, download=True, transform=transform)
test = datasets.FashionMNIST("data", train=False, download=True, transform=transform)
train_loader = DataLoader(train, batch_size=256, shuffle=True)
test_loader = DataLoader(test, batch_size=1024)
class MBConv(nn.Module):
def __init__(self, in_c, out_c, expand=4, stride=1):
super().__init__()
hidden = in_c * expand
self.expand_conv = nn.Sequential(
nn.Conv2d(in_c, hidden, 1), nn.BatchNorm2d(hidden), nn.SiLU(),
) if expand > 1 else nn.Identity()
self.dw = nn.Sequential(
nn.Conv2d(hidden, hidden, 3, stride, padding=1, groups=hidden),
nn.BatchNorm2d(hidden), nn.SiLU(),
)
self.se = nn.Sequential(
nn.AdaptiveAvgPool2d(1), nn.Flatten(),
nn.Linear(hidden, max(8, hidden // 4)), nn.SiLU(),
nn.Linear(max(8, hidden // 4), hidden), nn.Sigmoid(),
)
self.project = nn.Sequential(nn.Conv2d(hidden, out_c, 1), nn.BatchNorm2d(out_c))
self.shortcut = nn.Sequential()
if stride != 1 or in_c != out_c:
self.shortcut = nn.Sequential(
nn.Conv2d(in_c, out_c, 1, stride), nn.BatchNorm2d(out_c),
)
def forward(self, x):
h = self.expand_conv(x)
h = self.dw(h)
h = h * self.se(h).view(-1, h.shape[1], 1, 1) # SE:按通道加权
return self.project(h) + self.shortcut(x)
class TinyEfficientNet(nn.Module):
def __init__(self):
super().__init__()
self.stem = nn.Sequential(nn.Conv2d(1, 16, 3, 1, 1), nn.BatchNorm2d(16), nn.SiLU())
self.stages = nn.Sequential(
MBConv(16, 24, expand=2, stride=1),
MBConv(24, 32, expand=4, stride=2), # 28→14
MBConv(32, 48, expand=4, stride=2), # 14→7
MBConv(48, 64, expand=6, stride=1),
)
self.head = nn.Sequential(
nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(64, 10),
)
def forward(self, x):
return self.head(self.stages(self.stem(x)))
model = TinyEfficientNet().to(device)
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
print(f"EfficientNet 风格参数量:{sum(p.numel() for p in model.parameters()):,}")
def evaluate(loader):
model.eval()
correct = total = 0
with torch.no_grad():
for xb, yb in loader:
pred = model(xb.to(device)).argmax(1)
correct += (pred == yb.to(device)).sum().item()
total += len(yb)
return correct / total
for epoch in range(3):
model.train()
for xb, yb in train_loader:
xb, yb = xb.to(device), yb.to(device)
opt.zero_grad()
loss = criterion(model(xb), yb)
loss.backward()
opt.step()
print(f"epoch {epoch + 1}:训练准确率 = {evaluate(train_loader) * 100:.2f}%")
print(f"测试准确率 = {evaluate(test_loader) * 100:.2f}%")examples/ch10_scaling.py
"""第 10 章示例(2019—2020):缩放定律——数据量与模型规模。
在一个人造"语言"(固定 bigram 分布生成)上训练迷你 GPT:
1. 数据越多,测试 loss 越低;
2. 数据不足时,更大的模型过拟合更严重(模型规模要和数据量匹配)。
运行方式:
uv run python examples/ch10_scaling.py
"""
import torch
from torch import nn
torch.manual_seed(0)
device = "cuda" if torch.cuda.is_available() else "cpu"
V, BLOCK = 20, 32
g = torch.Generator().manual_seed(0)
# 人造语言的"语法":20 个符号,每个符号后接什么由固定的 bigram 概率表决定
bigram = torch.rand(V, V, generator=g)
bigram = bigram / bigram.sum(1, keepdim=True)
def gen_text(n, seed=0):
gg = torch.Generator().manual_seed(seed)
out = torch.randint(0, V, (1,), generator=gg)
for _ in range(n - 1):
out = torch.cat([out, torch.multinomial(bigram[out[-1]], 1, generator=gg)])
return out
full = gen_text(20000)
train_full = full[:16000].to(device)
test = full[16000:16000 + 512].to(device)
class Block(nn.Module):
def __init__(self, d):
super().__init__()
self.ln1 = nn.LayerNorm(d)
self.attn = nn.MultiheadAttention(d, 2, batch_first=True)
self.ln2 = nn.LayerNorm(d)
self.mlp = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d))
def forward(self, x, mask):
x = x + self.attn(self.ln1(x), self.ln1(x), self.ln1(x), attn_mask=mask)[0]
return x + self.mlp(self.ln2(x))
class MiniGPT(nn.Module):
def __init__(self, d=32):
super().__init__()
self.tok = nn.Embedding(V, d)
self.pos = nn.Embedding(BLOCK, d)
self.blocks = nn.ModuleList([Block(d) for _ in range(2)])
self.head = nn.Linear(d, V)
def forward(self, x):
B, T = x.shape
h = self.tok(x) + self.pos(torch.arange(T, device=x.device))
mask = torch.triu(torch.full((T, T), float("-inf"), device=x.device), diagonal=1)
for block in self.blocks:
h = block(h, mask)
return self.head(h)
def train(subset, d=32, steps=400):
model = MiniGPT(d).to(device)
opt = torch.optim.AdamW(model.parameters(), lr=1e-3)
for _ in range(steps):
start = torch.randint(0, len(subset) - BLOCK, (64,), device=device)
xb = torch.stack([subset[s:s + BLOCK] for s in start])
yb = torch.stack([subset[s + 1:s + BLOCK + 1] for s in start])
opt.zero_grad()
loss = nn.functional.cross_entropy(model(xb).transpose(1, 2), yb)
loss.backward()
opt.step()
model.eval()
eval_losses = []
with torch.no_grad():
for i in range(0, len(test) - BLOCK, BLOCK):
xb = test[i:i + BLOCK].unsqueeze(0)
yb = test[i + 1:i + BLOCK + 1].unsqueeze(0)
eval_losses.append(nn.functional.cross_entropy(model(xb).transpose(1, 2), yb).item())
return sum(eval_losses) / len(eval_losses)
print(f"随机猜测基线 loss ≈ {torch.log(torch.tensor(V)).item():.3f}")
for frac, label in [(0.1, "10%"), (0.3, "30%"), (1.0, "100%")]:
subset = train_full[: int(len(train_full) * frac)]
loss_s = train(subset, d=32)
loss_m = train(subset, d=64)
print(f"数据 {label:>4}:小模型(d=32) loss = {loss_s:.3f},大模型(d=64) loss = {loss_m:.3f}")动手实践
- 运行
examples/ch10_mbconv.py,把 MBConv 的expand从 4 改成 1 和 8,对比参数量与准确率。 - 运行
examples/ch10_scaling.py,把测试数据比例从 20% 改成 50%,观察三个数据档位的 loss 变化。 - 在缩放实验中增加 d=128 档位,验证「数据越少,大模型越吃亏」是否更明显。
- 修改 MBConv 的 SE 模块:把两个全连接改成一个(直接 sigmoid),观察效果差异。
常见错误
| 错误写法 | 现象 | 原因 |
|---|---|---|
深度可分离卷积忘了 groups=hidden | 参数量暴涨 | 分组数必须等于通道数 |
SE 的 view(-1, C, 1, 1) 形状不对 | reshape 报错 | SE 输出是 (B, C),要扩展成 (B, C, 1, 1) |
| 用最后一个 batch 的 loss 当评估指标 | 结论噪声大 | 训练 loss 有波动,要用固定测试集 |
| 小数据 + 大模型直接对比 | 得出「大模型没用」的片面结论 | 要同时报告数据量维度 |
| 复合缩放只放大一个维度 | 收益递减 | EfficientNet 是同时缩放深度/宽度/分辨率 |
章末练习
基础
- MBConv 的四个步骤是什么?
- 深度可分离卷积为什么省参数?写出参数量公式。
- 缩放定律说的三个变量是什么?
提高
- 计算:输入 64 通道,expand=4,普通 3×3 卷积 vs 深度可分离 + 1×1 升维降维,参数量各是多少。
- 在
examples/ch10_scaling.py中把训练步数从 300 加到 1000,重新观察 10% 数据下大模型是否追上小模型。 - 用 log-log 坐标画出数据量 vs loss,观察是否接近直线(幂律)。
挑战
- 实现一个 5 个 MBConv 阶段的完整 EfficientNet-B0 简化版,在 Fashion-MNIST 上训练并报告参数与准确率。
- 复现 Chinchilla 思想:固定总计算量,比较「大模型少数据」与「小模型多数据」两种配置,哪个测试 loss 更低。
章末自测
- MBConv 中深度可分离卷积的 groups 参数应等于?
- A. 1
- B. 输入通道数
- C. 输出通道数
- D. 批大小
- SE 模块的作用是?
- A. 空间注意力
- B. 按通道加权
- C. 下采样
- D. 正则化
- EfficientNet 的复合缩放同时缩放?
- A. 深度、宽度、分辨率
- B. 学习率、批大小
- C. 数据、标签
- D. 卷积核、池化
- 缩放定律中,测试 loss 与参数量近似成?
- A. 线性关系
- B. 幂律关系
- C. 指数关系
- D. 无关
- 数据不足时,大模型通常?
- A. 表现更好
- B. 过拟合更严重
- C. 没有区别
- D. 无法训练
- Chinchilla 定律的直觉是?
- A. 模型越大越好
- B. 数据越多越好
- C. 模型规模与数据量要匹配
- D. 计算量无关
- GPT-3 的参数量约为?
- A. 1.5 亿
- B. 1750 亿
- C. 100 万
- D. 7000 亿
- 缩放实验中 10% 数据下大模型 loss 高于小模型的原因是?
- A. 大模型参数少
- B. 过拟合
- C. 学习率太小
- D. 数据错误
- MBConv 的倒置体现在?
- A. 先降维再升维
- B. 先升维再降维
- C. 无升维
- D. 只降维
- 2020 年《Scaling Laws for Neural Language Models》的作者机构是?
- A. Google
- B. OpenAI
- C. Meta
- D. DeepMind
