前六关,我们依次学习了:
Tensor → Conv2d → Pooling → Flatten → GAP → Residual
如果只是一直往后学,很容易出现一种情况:
每一个知识点单独看好像都懂,但突然让自己解释“这些东西到底是怎么串起来的”,又说不太清楚。
所以这一篇不继续增加新的知识点。
我们停下来,打开第一张:
秘境地图
一、前六关,我们到底走过了什么?
先把六关放在一起。
| 关卡 | 核心内容 | 真正解决的问题 |
|---|---|---|
| 第一关 | Tensor | 神经网络里的数据到底长什么样 |
| 第二关 | Conv2d | 卷积为什么会改变 Channel 和空间尺寸 |
| 第三关 | Pooling | 为什么 Feature Map 会越来越小 |
| 第四关 | Flatten | 多维 Tensor 怎么展开 |
| 第五关 | GAP | 如何把每个 Channel 压缩成一个全局结果 |
| 第六关 | Residual | 两条特征路径为什么能够重新连接 |
【秘境地图01:以 Tensor 为起点,依次经过 Conv2d、Pooling、Flatten/GAP、Residual。每一关旁边标注主要关注的 Tensor Shape 变化。】
这六关看起来是在学习六个不同的知识点。
但回头看会发现:
我们一直在追踪同一个东西——Tensor。
二、第一关:Tensor——先看懂神经网络里的数据
最开始,我们遇到:
[8, 3, 256, 256]
后来知道它其实对应:
[B, C, H, W]
其中:
-
B:Batch(批次,一次送入网络的样本数量) -
C:Channel(通道,一张图像或特征中的不同信息层) -
H:Height(高度) -
W:Width(宽度)
所以:
[8, 3, 256, 256]
表示:
一次输入 8 张图像,每张图有 3 个 Channel,空间尺寸为
256×256。
这是后面所有内容的起点。
因为 Conv、Pooling、Flatten、GAP 和 Residual,本质上都在操作 Tensor。
三、第二关:Conv2d——第一次真正追踪 Shape
Conv2d(二维卷积,对图像或特征进行局部特征提取的网络层)让我们第一次真正开始观察 Tensor Shape 的变化。
例如:
nn.Conv2d(
in_channels=3,
out_channels=64,
kernel_size=3,
stride=2,
padding=1
)
输入:
[8,3,256,256]
可能得到:
[8,64,128,128]
这时候已经开始学会分别观察:
B:8 → 8
C:3 → 64
H:256 → 128
W:256 → 128
到这一关,我们第一次形成了一个非常重要的习惯:
看到一个网络层,先问输入 Shape 是什么,再问输出 Shape 会变成什么。
四、第三关:Pooling——Channel 和空间尺寸不是一回事
Pooling(池化,对局部区域进行压缩的操作)进一步让我们发现:
Channel 和 H、W 是两类不同的东西。
例如:
[8,64,128,128]
经过:
nn.MaxPool2d(
kernel_size=2,
stride=2
)
得到:
[8,64,64,64]
这里:
C:64 → 64
H:128 → 64
W:128 → 64
Channel 没有改变。
变化的是空间尺寸:
H、W
这时候才真正开始理解:
Feature Map(特征图,卷积网络内部提取出来的特征表示)的“数量”和“大小”并不是同一回事。
五、第四关:Flatten——Shape 变了,数值不一定减少
Flatten(展平,把多个维度重新整理成一个维度)让我们遇到了一种完全不同的 Shape 变化。
例如:
[8,128,8,8]
执行:
torch.flatten(x, 1)
得到:
[8,8192]
因为:
128 × 8 × 8 = 8192
这里最重要的不是记住这一行代码,而是真正理解:
1表示从第 1 个维度开始展开。
所以:
dim=0:Batch 保留
dim=1、2、3:
128 × 8 × 8
→
8192
Flatten 并没有把这些数值求平均。
它主要改变的是:
数据原来的维度组织方式。
六、第五关:GAP——每个 Channel 最终只留下一个结果
GAP(Global Average Pooling,全局平均池化)和 Flatten 都能让 Tensor 变得更简单,但两者做的事情完全不同。
输入:
[8,128,8,8]
经过:
nn.AdaptiveAvgPool2d((1,1))
得到:
[8,128,1,1]
每一个:
8×8 Feature Map
都会变成:
1×1
也就是说:
每个 Channel 的整个空间区域最终求出一个平均值。
128 个 Channel:
→ 128 个结果
如果继续:
torch.flatten(x, 1)
就得到:
[8,128]
到这里,我们第一次真正区分:
改变 Shape
和:
压缩信息
并不是同一件事。
七、第六关:Residual——开始从“网络层”走向“网络结构”
Residual Connection(残差连接,让输入通过 Shortcut 与主分支重新相加)是一个明显的转折点。
前面主要研究:
一个操作对 Tensor 做了什么?
到了这里,开始思考:
为什么网络要这样连接?
最经典的一行:
out = out + identity
可以理解为:
F(x) + x
这里出现了几个新的结构概念:
-
Main Path(主分支,输入经过卷积等操作的路径)
-
Shortcut(跳跃连接,让输入绕过部分网络层直接向后传递)
-
Add(逐元素相加,把两个对应位置的特征进行融合)
我们开始知道:
[8,64,32,32]
+
[8,64,32,32]
可以正常进行 Residual Add。
但是:
[8,64,32,32]
+
[8,128,32,32]
不能直接按照普通 Residual 的方式相加。
于是又认识了:
1×1 Conv
它可以帮助两条路径调整 Channel,使 Shape 对齐。
到这里,我们已经开始从:
看懂一个网络层
进入:
看懂一个网络模块。
八、前六关真正串起来以后,我们会什么了?
如果现在只让我总结一种最重要的能力,我不会说:
我学会了六个 PyTorch API。
而是:
我开始能够追踪 Tensor 在网络中的流动。
例如看到:
x = torch.randn(8, 3, 256, 256)
conv = nn.Conv2d(
3,
64,
kernel_size=3,
padding=1
)
pool = nn.MaxPool2d(
kernel_size=2,
stride=2
)
x = conv(x)
x = pool(x)
现在应该已经能够自己判断:
输入:
[8,3,256,256]
Conv2d:
[8,64,256,256]
Pooling:
[8,64,128,128]
如果继续:
x = torch.flatten(x, 1)
也能够推算:
[8,64,128,128]
→
[8,1048576]
因为:
64 × 128 × 128
=
1048576
这就是前六关最核心的能力变化。
九、现在已经能够看懂哪些代码?
至少下面这些代码,已经不再只是“见过”。
torch.randn(...)
nn.Conv2d(...)
nn.MaxPool2d(...)
torch.flatten(x, 1)
nn.AdaptiveAvgPool2d((1,1))
以及:
out = out + identity
现在已经开始能够回答:
这行代码为什么放在这里?
以及:
执行以后 Tensor Shape 会发生什么变化?
这和单纯“知道代码怎么抄”已经不是同一个阶段了。
十、现在能自己写到什么程度?
现在已经可以尝试自己组合一个最基础的特征处理流程:
import torch
import torch.nn as nn
x = torch.randn(
8,
3,
256,
256
)
conv1 = nn.Conv2d(
in_channels=3,
out_channels=64,
kernel_size=3,
padding=1
)
pool = nn.MaxPool2d(
kernel_size=2,
stride=2
)
conv2 = nn.Conv2d(
in_channels=64,
out_channels=128,
kernel_size=3,
padding=1
)
gap = nn.AdaptiveAvgPool2d((1,1))
print("Input:", x.shape)
x = conv1(x)
print("Conv1:", x.shape)
x = pool(x)
print("Pooling:", x.shape)
x = conv2(x)
print("Conv2:", x.shape)
x = gap(x)
print("GAP:", x.shape)
x = torch.flatten(x, 1)
print("Flatten:", x.shape)
输出:
Input:
[8,3,256,256]
Conv1:
[8,64,256,256]
Pooling:
[8,64,128,128]
Conv2:
[8,128,128,128]
GAP:
[8,128,1,1]
Flatten:
[8,128]
如果现在已经能够不运行代码,就大致判断出这些 Shape 为什么变化:
前六关最重要的基础就开始真正连起来了。
十一、前六关,其实一直在学同一件事
现在回头看:
第一关问:
Tensor 有什么维度?
第二关问:
Conv 为什么改变这些维度?
第三关问:
Pooling 为什么只改变部分维度?
第四关问:
Flatten 为什么把几个维度合并?
第五关问:
GAP 为什么让 H、W 变成
1×1?
第六关问:
Residual 为什么要求两条路径的 Tensor 能够对应?
看起来是在学习六个不同知识点。
实际上一直围绕:
Tensor Shape
【示意图2:以 [B,C,H,W] 为中心,连接 Conv2d、Pooling、Flatten、GAP、Residual。分别标记:改变 C/H/W、缩小 H/W、合并 C/H/W、H/W→1×1、检查两条路径 Shape。】
所以真正获得的不是:
“记住了六个函数。”
而是:
开始能够顺着一个 Tensor,看它怎么穿过网络。
十二、秘境隐藏支线01:Batch 从 8 变成 16,网络还需要修改吗?
前面经常使用:
[8,64,32,32]
如果现在变成:
[16,64,32,32]
改变的是:
Batch:8 → 16
但:
C、H、W
没有变化。
那么:
nn.Conv2d(
64,
128,
kernel_size=3,
padding=1
)
需要因为 Batch 变化而修改吗?
不需要。
因为这里的:
64
表示的是:
in_channels
关注的是 Channel,而不是 Batch。
所以:
[8,64,32,32]
和:
[16,64,32,32]
都可以进入这个 Conv2d。
十三、秘境隐藏支线02:上一层输出 64 Channel,下一层为什么必须接 64?
假设:
conv1 = nn.Conv2d(
3,
64,
kernel_size=3,
padding=1
)
输出:
[8,64,256,256]
那么下一层可以写:
conv2 = nn.Conv2d(
64,
128,
kernel_size=3,
padding=1
)
这里第一个:
64
不是随便写的。
因为:
上一层的
out_channels,要能够接上下一层的in_channels。
所以:
Conv1:
3 → 64
Conv2:
64 → 128
如果第二层错误地写成:
nn.Conv2d(
3,
128,
kernel_size=3
)
它等待的是:
3 Channel
但实际传进来的却是:
64 Channel
自然就接不上。
这说明:
网络中的层并不是孤立的,它们通过 Tensor Shape 一层一层连接。
十四、秘境隐藏支线03:为什么先 Pooling,会让后面的 Flatten 短很多?
假设:
[8,128,32,32]
直接 Flatten:
[8,131072]
因为:
128 × 32 × 32
=
131072
如果先 Pooling:
[8,128,32,32]
→
[8,128,16,16]
再 Flatten:
[8,32768]
因为:
128 × 16 × 16
=
32768
所以虽然 Pooling 并没有直接操作 Flatten:
但是它改变了前面的 H、W,后面的 Flatten 长度自然也会跟着改变。
这就是网络层之间的连锁关系。
十五、秘境隐藏支线04:GAP 已经变成 1×1,为什么还要 Flatten?
GAP 后:
[8,128,8,8]
→
[8,128,1,1]
看起来每个 Channel 已经只剩一个值。
为什么还经常继续:
x = torch.flatten(x, 1)
得到:
[8,128]
因为:
[8,128,1,1]
仍然是四维 Tensor。
而:
[8,128]
是二维 Tensor。
两者元素数量相同:
8 × 128 × 1 × 1
=
8 × 128
但:
Shape 的组织形式不同。
所以这里的 Flatten 不是继续压缩数据。
而是在:
整理 Tensor 的维度结构。
十六、秘境隐藏支线05:元素数量一样,就一定能做 Residual Add 吗?
看两个 Tensor:
A = [8,64,32,32]
另一个:
B = [8,128,16,32]
每个样本的元素数量:
64 × 32 × 32
=
65536
而:
128 × 16 × 32
=
65536
元素数量完全相同。
那能不能直接:
A + B
作为普通 Residual Add?
不能。
因为 Residual Add 需要的是:
对应位置能够逐元素相加。
而:
[64,32,32]
和:
[128,16,32]
虽然总数一样,但是 Shape 的组织方式不同。
所以一定要记住:
元素总数一样,不等于 Shape 一样。
这其实正好把前面的 Flatten 和 Residual 两关联系起来了。
十七、秘境隐藏支线06:Flatten 没丢数值,为什么还是改变了特征结构?
输入:
[8,128,8,8]
执行:
torch.flatten(x, 1)
得到:
[8,8192]
前后的元素总数没有发生变化。
也没有做平均。
那到底改变了什么?
原来:
128
单独表示 Channel。
而:
8×8
表示每个 Channel 中的空间位置。
Flatten 之后:
8192
直接把:
C、H、W
合并到了一个长维度中。
所以:
Flatten 没有删除这些数值,但改变了这些数值原本的维度组织形式。
这也是为什么:
[8,128,8,8]
和:
[8,8192]
虽然拥有相同数量的元素,却不能直接认为它们是同一种 Tensor 结构。
十八、现在我们的地图坐标在哪里?
走完前六关,目前已经探索的区域可以暂时定义为:
基础 Tensor 与网络结构区域
现在已经能够:
-
看懂
[B,C,H,W] -
追踪 Conv2d 的 Shape
-
追踪 Pooling 的 Shape
-
使用 Flatten
-
理解 GAP
-
区分
[B,C,1,1]和[B,C] -
理解 Residual Add
-
判断为什么需要 Shape 对齐
-
看懂基础
1×1 Conv -
理解简单 Residual Block
但其中最重要的一项能力是:
已经开始从“看一行代码”,变成“看一条 Tensor 流”。
【秘境地图03:地图当前位置。左侧已探索区域包含 Tensor、Conv2d、Pooling、Flatten、GAP、Residual;当前位置标记为“能够追踪 Tensor Shape 与基础网络模块”;后续区域只显示模糊轮廓,不标注新知识点。】
十九、秘境地图不是终点
前六关结束以后,我最大的感受并不是:
“终于记住了六个 PyTorch API。”
而是:
以前看网络代码时,我看到的是一行一行陌生的代码;现在开始能看到一个 Tensor 在里面不断变化。
输入是什么 Shape。
经过 Conv 后为什么变。
经过 Pooling 后哪里缩小。
Flatten 为什么突然变成二维。
GAP 为什么每个 Channel 最终只剩一个值。
Residual 的两条路径为什么需要重新对齐。
当这些问题真正串起来以后:
CNN(Convolutional Neural Network,卷积神经网络)的轮廓才第一次开始变得清楚。
所以这张秘境地图记录的并不是:
“我看完了六篇文章。”
而是:
“我第一次开始真正看见 Tensor 是怎么穿过神经网络的。”
下一次再次打开秘境地图时,我们应该已经走得更远了。
从入门,到入神。

293

被折叠的 条评论
为什么被折叠?



