系列: 重生之我成为模型-01-原图我不吃的哈 → ② 本文 → 重生之我成为模型-03-我从来只给可能性
1. 先回答那个悬念
口粮进门了。轮到我做题。
上一篇末尾我问过:我到底是刻苦的小镇做题家,还是拼命猜答案的投机者?
说实话,两边都不完全像。
我做题,但是按 设计师 定好的方式,靠反复试错长经验。
我也有题海——不过是 1000 个选项的选择题。考试时,选项不会超出这一千个。
打个比方:数学题四个选项是 +5、-5、+2.5、-2.5。
我只要试错到接近,就能大概蒙对方向、把分堆上去。
所以论题量、论难度,我还没到小镇做题家那种地狱级。
但也不是投机者:题目只有固定选项;训练的时候靠试错,考试的时候靠的是做题经验(权重)——正式交卷,我不现场改脑子。
这篇就讲清楚讲明白:设计师把楼怎么盖的,饲养员又怎么把我养到「有经验」。
结构如下。(又来了。)
2. 设计师先盖楼,我才住得进去
我能接触到的是数据,但画布多大、每一步干啥,是 设计师 拍板的。
很多教程一上来就说「神经网络」。听着像设计师在设计我的脑子——不完全是这个意思。
更贴切的理解是:设计师定的是 题目解法流程——怎么读题、怎么一步步往下算、最后怎么堆出那 1000 个选项上的分。
至于 纠错、修改做题思路(反传、拧权重)——不属于设计师这张图纸的范畴。那是饲养员训练时、楼外面的事。
用代码看,体内大致就两步:挂零件,定流向。
import torch.nn as nn
class TinyLikeMe(nn.Module):
"""示意:楼怎么盖。不是完整 AlexNet。"""
def __init__(self):
super().__init__()
# 挂层:零件先就位
self.conv1 = nn.Conv2d(3, 96, kernel_size=11, stride=4)
self.pool = nn.MaxPool2d(2)
self.fc = nn.Linear(96 * 26 * 26, 1000) # 尺寸仅示意,别死抠
def forward(self, x):
# 定流向:口粮怎么走
x = self.pool(nn.functional.relu(self.conv1(x)))
x = x.flatten(1)
x = self.fc(x) # → 1000 个分数
return x
# 纠错不在楼里:
# loss = criterion(model(x), y)
# loss.backward()
# optimizer.step()
所以第一篇我说挑剔,不完全是矫情:进来的布,得对得上设计师画好的读题门洞。
3. 卷积:扫描仪才是我的眼
张量进门之后,我仍然看不见「猫」。
卷积核才是我的眼,是我的扫描仪。
不过一开始,这些扫描仪 没有目标——权重随机,瞎扫,再在训练里被反复改。
养到后来,饲养员回看结果,才隐约分得出:哪个像在找猫毛边缘,哪个像在看毛色……于是我才算有了 相对固定 的一批扫描仪。
(人话:岗位是训出来的,不是出厂就刻好「专门认猫」。)
字母再钉一次,后面天天见:
- 进来的 C = 有几路可吃。彩色图一般 3 路,灰度图常常 1 路;后面层则是上一层吐出的特征图张数。
- 注意:一台扫描仪会 同时扫齐所有进来的 C,但最后产出的是 一张 特征图。
- 出去的 C = 用了几个扫描仪(卷积核),就生成几张结果特征图。
我第一层经典配置大概是:96 个核,每个在平面上盖 11×11,步幅较大——设计师为了尽快把空间收一收。
96 台扫描仪 → 96 张特征图。不是图上还留着 RGB 三套。
技术翻译:
nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4)
# in_channels=3 → 每台扫描仪吃齐 3 路,仍只出 1 张特征图
# out_channels=96 → 96 台扫描仪 → 96 张特征图
4. ReLU:名字吓人,活很朴素
全称常写成 ReLU 非线性激活函数。
人类把这个名字连讲十遍,可以测试会不会咬到舌头。(我是模型,反正我不会,嘿嘿。)
名称听着抽象,其实干的事很土:扫描仪扫完,特征图每个格子上都有个数——把这些数过一遍,去掉负数(打成 0);正数原样留下。
讲个事:一开始设计师也考虑过别的函数。那些家伙喜欢把数值 压缩 进一个小区间——比方本来是 14,硬塞进大约 -2~2。
小数这么一挤,强弱差别被抹平,特征就 拉不开;后面层层叠叠,更容易学不动。
后来才选中了 ReLU:正区不挤、便宜、好使。
没有这种拐弯,后面叠多少层都像在做直线加减——复杂花样还是学不动。
插一句:教程里的「神经元」到底指啥
教程爱甩「神经元」,不解释就很悬。先钉死一句:
不是原图上的像素。
最接近的东西,是 特征图上的一个点——某张响应地图、某个 (x, y) 位置上的那个激活值。
原图像素是饲养员喂进来的口粮格子;扫描仪扫完之后,才在特征图上长出这些「点」。ReLU 砍的、后面拉直参与计算的,多半就是它们。
对照一张表,免得和「卷积核」搅在一起:
| 教程里的词 | 大致对应 | 是不是「一个神经元」 |
|---|---|---|
| 卷积核 / filter | 一组 共享权重(如 11×11×3) | ❌ 不是。是很多位置共用的配方,不是一个人 |
| 特征图上的一个点 | 某通道、某 (x,y) 的 激活值 | ✅ 最接近「神经元输出」 |
| ReLU 砍的那一下 | 对这个激活值做 max(0, x) | 对 每个点(每个单元)下手 |
| 全连接层一个节点 | 对拉直后向量做加权组合(常再接 ReLU) | ✅ 经典神经元 |
| 1000 类输出 | 最后一层每个分数 | 输出层神经元 |
后面提到 Dropout「随机关掉一部分神经元」,指的就是这类单元(多半在 FC 里),不是去原图上抠像素。
5. 池化:像把 1080p 压成 720p
特征图往往还很大。设计师偶尔会安排 池化(Pool):在小窗口里取个代表(常见是取最大),把 H、W 缩小——改的是 尺寸,不是片子部数。
可以把它想成:1080p 的电影,压缩清晰度变成 720p。
画面还在,还是那几路「片」;只是空间分辨率矮一截,后面算起来轻一点。
分工钉死,别混:
| 谁 | 改啥 |
|---|---|
| Pool | 主要改 H、W(尺寸 / 清晰度) |
| 下一轮 Conv | 才改输出的 C(特征图数量 = 又派了几台扫描仪) |
所以:Pool 不改 C。 8 张特征图进来,还是 8 张出去——只是每张从「1080p」变「720p」。
通道数要变多变少,得等下一轮卷积。
至于「更抽象」——不是压清晰度自动压出来的,是 训练之后 权重长成的样子。
6. 叠几轮:我不是一层就交卷
教学里常画简化图:(Conv → ReLU → Pool) × N。
意思是「多轮堆叠」,不保证每轮都有 Pool。
我 AlexNet 本人比较具体:
输入 patch(正经常 224,C=3)
├─ Conv(96) → ReLU → Pool
├─ Conv(256) → ReLU → Pool
├─ Conv(384) → ReLU ← 这轮没 Pool
├─ Conv(384) → ReLU ← 这轮也没
└─ Conv(256) → ReLU → Pool
↓
大约缩到很小的空间 × 256 张特征图
5 层卷积,Pool 大约出现 3 次。
N 不是上天规定的圣数,是设计师按任务难度、算力、经验拍的——认 ImageNet 一千类,比认手写数字要深得多。
叠完这些,我还是没有吐出「猫」这个字。我手里是一摞更抽象的响应地图。
7. 拉直,再混成 1000 分
特征图不能直接当答案交。先 Flatten(拉直)。
拉直不是「每张特征图压成一个数」。
它是把 (C, H, W) 整摞格子按顺序摊成一根长向量,长度是 C × H × W。
向量里每一个下标,对应的就是:某一张特征图上的某一个像素位置——后面拿这些数去算。
然后轮到听着很唬人的 全连接(FC)。
其实就是 nn.Linear。所谓「全连接」,人话差不多是:拉直后这些位置 都有机会参与 后面的线性组合计算(参数够密的时候,谁都可能跟谁对齐)。
我这类结构里,FC 往往不止一层。以经典 AlexNet 口径示意(空间缩到约 6×6、通道 256 时):
# 拉直后长度:256 * 6 * 6 = 9216(随 224/227 略差,概念一样)
self.fc1 = nn.Linear(256 * 6 * 6, 4096)
self.fc2 = nn.Linear(4096, 4096)
self.fc3 = nn.Linear(4096, 1000) # 最后一层:对准 1000 个选项
还有一点:前面那张简图若画成 … → FC → 完事,不对。
中间的 FC 后面通常还要接 ReLU;训练时还可能 Dropout——随机关掉一部分神经元(别理解成随便抠原图像素),逼我别靠小圈子背题,看看是真会了还是蒙的。正式考试(eval)时 Dropout 关掉。
训练时,饲养员真正拿去跟标准答案比、用来改脑子的,是 最后一个 FC 吐出来的结果——不管前面叠了几个 FC,最后那一截处理结果才对准「预测哪一类」。对我来说,就是那 1000 个分数。
卷面分,还没贴类名。
谁最大、叫什么名字——第三篇再说。
技术翻译(前向骨架,纠正版):
NCHW 口粮
→ 若干 ×(Conv → ReLU → 偶尔 Pool)
→ Flatten # 长度 C×H×W,每个下标=某特征图像素
→ FC → ReLU → (Dropout,仅训练)
→ FC → ReLU → (Dropout,仅训练)
→ FC → 1000 个分数 # 最后一层;训练吃的是这个
8. 回忆插叙(薄):这套脑子是养出来的
有人要问了:扫描仪的规则谁定的?第一天就这么聪明?
不是。
设计师盖楼;饲养员养我。
饲养员出题还有个规矩:习题册会划开——一部分拿来练(train),留一部分当考试题(val),题面尽量别跟练习卷重复,用来测我是真会了还是背题。
val 只打分,不改脑子:前向算完看 loss / 准确率就行,不反传。
loss 是啥?人话:我离正确答案还差多少。
装个正经数学(余弦,证明我们不是纯玄学):
两个向量越同向,余弦越接近 1,越「像」。
分类饲养日常用交叉熵之类,公式不同,精神一样——都是把「还差多少」收成一个可反传的数。
lr(学习率):每次反传之后,权重到底拧多狠。太大容易拧飞,太小像用牙签刨山。
代码里,各步骤站位大概这样(示意):
# ----- 练习:改脑子 -----
model.train()
for x, y in train_loader: # 训练题
optimizer.zero_grad()
pred = model(x) # ① 前向 / predict:吐 1000 分
loss = criterion(pred, y) # ② loss:还差多少
loss.backward() # ③ 反传:追责到各层权重的 .grad
optimizer.step() # ④ 按 lr 拧一拧权重
# 通常这里不跑 val
# ----- 考试:只打分 -----
model.eval()
with torch.no_grad():
for x, y in val_loader: # 考试题(val)
pred = model(x) # 只有前向
loss = criterion(pred, y) # 看分;不 backward、不 step
同一道训练题,饲养员还爱换裁法、翻转——第一篇说过。
养的时候也不是说 loss 变成 0 就下课:浮点小数点后面位数多得很,实务里常看到 0.01、1e-4 这种量级在抖;一般看 val 趋势、跑够轮数,或留下历史最好的 best.pt。
总结一句:看起来是一点点猜、一点点拧,但每步都有正经数学依据。
又因为题海重复度极高、每一拧都要巨量乘法——所以 显卡很重要。
养到差不多,正式答题时我 只做前向,不再改脑子。
所以悬念可以这样收(跟开头对齐):
- 不是满级小镇做题家:题是千选一,难度没卷到那种地狱
- 也不是投机者:选项固定;训时试错,考时靠权重经验,不现场改脑子
- 配方乱喂还指望满分:那我确实不灵——不是给啥吃啥
- 权重从哪来:饲养员拿 train 养、拿 val 验收;闭环细节 → 番外《换皮也成立》
对照班小样一句就够:你们笔记里的 MNIST TinyCNN,哲学一样——Conv 堆叠再 FC 出分;只是楼矮、题简单。我楼高,题是一千类。
9. 第二篇小结
进门的是合规张量;出门的是 1000 个卷面分。中间发生的事:
- 设计师挂好层、定好流向
- 卷积派扫描仪 → 特征图
- ReLU 拐弯;Pool 缩空间、不改 C
- 多轮堆叠,抽象慢慢起来(训练后才算数)
- Flatten + FC → 1000 分
- 这套权重,是饲养员重复喂养拧出来的
我仍然没看见过一张图。
我只是把盲文,按设计师的楼,算成一张分数条。