深度学习入门到入神·秘境地图01:从 Tensor 到 Residual,前六关知识路线图

前六关,我们依次学习了:

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 是怎么穿过神经网络的。”

下一次再次打开秘境地图时,我们应该已经走得更远了。

从入门,到入神。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值