
前六关我们依次学习了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]其中BBatch批次一次送入网络的样本数量CChannel通道一张图像或特征中的不同信息层HHeight高度WWidth宽度所以[8, 3, 256, 256]表示一次输入 8 张图像每张图有 3 个 Channel空间尺寸为256×256。这是后面所有内容的起点。因为 Conv、Pooling、Flatten、GAP 和 Residual本质上都在操作 Tensor。三、第二关Conv2d——第一次真正追踪 ShapeConv2d二维卷积对图像或特征进行局部特征提取的网络层让我们第一次真正开始观察 Tensor Shape 的变化。例如nn.Conv2d( in_channels3, out_channels64, kernel_size3, stride2, padding1 )输入[8,3,256,256]可能得到[8,64,128,128]这时候已经开始学会分别观察B8 → 8 C3 → 64 H256 → 128 W256 → 128到这一关我们第一次形成了一个非常重要的习惯看到一个网络层先问输入 Shape 是什么再问输出 Shape 会变成什么。四、第三关Pooling——Channel 和空间尺寸不是一回事Pooling池化对局部区域进行压缩的操作进一步让我们发现Channel 和 H、W 是两类不同的东西。例如[8,64,128,128]经过nn.MaxPool2d( kernel_size2, stride2 )得到[8,64,64,64]这里C64 → 64 H128 → 64 W128 → 64Channel 没有改变。变化的是空间尺寸H、W这时候才真正开始理解Feature Map特征图卷积网络内部提取出来的特征表示的“数量”和“大小”并不是同一回事。五、第四关Flatten——Shape 变了数值不一定减少Flatten展平把多个维度重新整理成一个维度让我们遇到了一种完全不同的 Shape 变化。例如[8,128,8,8]执行torch.flatten(x, 1)得到[8,8192]因为128 × 8 × 8 8192这里最重要的不是记住这一行代码而是真正理解1表示从第 1 个维度开始展开。所以dim0Batch 保留 dim1、2、3 128 × 8 × 8 → 8192Flatten 并没有把这些数值求平均。它主要改变的是数据原来的维度组织方式。六、第五关GAP——每个 Channel 最终只留下一个结果GAPGlobal 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_size3, padding1 ) pool nn.MaxPool2d( kernel_size2, stride2 ) 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_channels3, out_channels64, kernel_size3, padding1 ) pool nn.MaxPool2d( kernel_size2, stride2 ) conv2 nn.Conv2d( in_channels64, out_channels128, kernel_size3, padding1 ) 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看它怎么穿过网络。十二、秘境隐藏支线01Batch 从 8 变成 16网络还需要修改吗前面经常使用[8,64,32,32]如果现在变成[16,64,32,32]改变的是Batch8 → 16但C、H、W没有变化。那么nn.Conv2d( 64, 128, kernel_size3, padding1 )需要因为 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_size3, padding1 )输出[8,64,256,256]那么下一层可以写conv2 nn.Conv2d( 64, 128, kernel_size3, padding1 )这里第一个64不是随便写的。因为上一层的out_channels要能够接上下一层的in_channels。所以Conv1 3 → 64 Conv2 64 → 128如果第二层错误地写成nn.Conv2d( 3, 128, kernel_size3 )它等待的是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 长度自然也会跟着改变。这就是网络层之间的连锁关系。十五、秘境隐藏支线04GAP 已经变成1×1为什么还要 FlattenGAP 后[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 吗看两个 TensorA [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 两关联系起来了。十七、秘境隐藏支线06Flatten 没丢数值为什么还是改变了特征结构输入[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 的两条路径为什么需要重新对齐。当这些问题真正串起来以后CNNConvolutional Neural Network卷积神经网络的轮廓才第一次开始变得清楚。所以这张秘境地图记录的并不是“我看完了六篇文章。”而是“我第一次开始真正看见 Tensor 是怎么穿过神经网络的。”下一次再次打开秘境地图时我们应该已经走得更远了。从入门到入神。