【深度学习03】神经网络基本骨架、卷积、池化、非线性激活、线性层、搭建网络
文章目录
- 神经网络的基本骨架-nn.Module的使用
- Containers神经网络骨架
- `forward` 函数详解
- 代码实例
- 卷积操作
- 卷积层计算原理
- PyTorch中的`nn.Conv2d`
- 参数详解
- 代码实例
- 多通道卷积:`in_channels` 和 `out_channels`
- 在代码中应用卷积层
- 代码详解 (小白视角)
- 池化层 (Pooling Layers)
- 为什么要进行卷积和池化?
- 最大池化 (Max Pooling)
- `ceil_mode` 参数的影响
- PyTorch中的 `nn.MaxPool2d`
- 核心参数详解
- 代码实例与超详细分解
- 逐段讲解
- 1. "工具准备" - `import` 部分
- 2. "食材准备" - `dataset` 和 `dataloader`
- 3. "设计菜谱" - `class Tudui`
- 4. "开始烹饪" - 主流程部分
- 神经网络 - 非线性激活
- 为什么要使用非线性激活函数?
- ReLU 激活函数 (`torch.nn.ReLU`)
- 核心参数:`inplace`
- Sigmoid 激活函数
- 代码实例与超详细分解 (小白版)
- 逐段讲解
- 1 & 2. "工具准备" & "食材准备"
- 3. "设计菜谱" - `class Tudui`
- 4. "开始烹饪" - 主流程部分
- 线性层及其他层介绍
- Linear 层 (全连接层)
- PyTorch中的 `nn.Linear`
- 代码实例与超详细分解
- 逐段讲解
- 1 & 2. "工具准备" & "食材准备"
- 3. "设计菜谱" - `class Tudui`
- 4. "开始烹饪" - 主流程部分
- 其他重要层简介
- 正则化层 (Regularization Layers)
- Dropout 层 (Dropout Layer)
- Recurrent 层 (循环层)
- Transformer 层 (Transformer Layer)
- 搭建小型网络与Sequential的使用
- 手动搭建网络
- 尺寸变化计算
- 使用 `nn.Sequential` 简化搭建
- 代码实例与超详细分解
- 逐段讲解
- 1 & 2. "工具准备" & "设计菜谱"
- 4. "可视化菜谱" - TensorBoard
视频链接
【PyTorch深度学习快速入门教程(绝对通俗易懂!)【小土堆】】p16-p22
神经网络的基本骨架-nn.Module的使用
torch.nn
Containers神经网络骨架
containers
如上图所示,一个基本的神经网络模型接收一个输入(input),通过网络内部定义的forward(正向传播)方法进行计算,最终生成一个输出(output)。这个 forward 函数定义了数据从输入到输出的完整流动和计算过程。
forward
函数详解
这张图为我们展示了一个具体的 forward
函数代码,它清晰地定义了数据在网络中的计算流程。
def forward(self, x):# x代表输入数据x = F.relu(self.conv1(x))return F.relu(self.conv2(x))
如代码和右侧图示,整个数据处理流程如下:
- 输入 x:原始数据进入网络。
self.conv1(x)
:数据首先经过第一个卷积层。F.relu(...)
:卷积后的结果,通过一个非线性的激活函数relu
进行处理。self.conv2(x)
:接着,数据流向第二个卷积层。return F.relu(...)
:再次通过非线性激活函数,最终得到网络的输出。
可以看到,代码的执行顺序与图中的“卷积 → 非线性 → 卷积 → 非线性”流程完全对应,直观地展示了数据在网络中的计算路径。
代码实例
为了更具体地理解 nn.Module
的工作方式,我们可以看一个简单的代码实例。
假设我们定义一个名为 Tudui
的类,它继承自 nn.Module
。在这个类中,我们必须定义两个核心方法:__init__
构造函数和 forward
方法。
import torch
from torch import nnclass Tudui(nn.Module):def __init__(self):super().__init__()def forward(self, input):output = input + 1return output# 创建模型实例
tudui = Tudui()# 准备输入数据
x = torch.tensor(1.0)# 将数据传入模型,获取输出
output = tudui(x) # 这里会自动调用 forward 方法
print(output)
在这个例子中:
__init__
方法目前是空的,只调用了父类的构造函数,因为我们这个简单的网络不需要定义任何带有参数的层(比如卷积层或线性层)。forward
方法定义了我们网络要执行的计算:它接收一个input
,然后返回input + 1
的结果。- 当我们创建了
tudui = Tudui()
这个实例后,可以直接像函数一样调用它:output = tudui(x)
。
最关键的一点是:当执行 tudui(x)
时,PyTorch 在底层会自动调用我们事先定义好的 forward(self, input)
方法,并将 x
作为 input
参数传进去。我们不需要手动写成 tudui.forward(x)
。这是 nn.Module
提供的一个核心便利功能,它允许我们在后台处理一些钩子(hooks)的同时,保持代码的简洁和直观。
运行上面的代码,最终会打印出 tensor(2.)
,证明我们的输入 1.0
经过 forward
方法处理后,成功地加了1。
卷积操作
卷积是构建卷积神经网络(CNN)的核心。简单来说,卷积操作通过一个卷积核(Kernel)(也叫滤波器 Filter)在输入图像上滑动,来提取图像的特征,例如边缘、纹理等。
卷积层计算原理
让我们通过下面这张图来理解卷积的具体计算过程:
图中展示了卷积的三个关键部分:
- 输入图像 (Input Image):左侧是一个 5x5 的矩阵,代表了我们输入的原始数据。
- 卷积核 (Kernel):中间是一个 3x3 的矩阵。卷积核会学习去识别特定的模式或特征。
- 卷积后的输出 (Output):右侧是 3x3 的输出矩阵,也称为特征图 (Feature Map)。
计算过程是怎样的?
卷积核会在输入图像上从左到右、从上到下滑动。每移动到一个位置,卷积核会覆盖输入图像的一部分区域,然后将这个区域的数值与卷积核自身的数值进行对应元素相乘再求和,得到一个单一的输出值。
- 计算第一个输出值 (10):
- 卷积核覆盖在输入图像的左上角 3x3 区域。
- 计算:
(1*1) + (2*2) + (0*1) + (0*0) + (1*1) + (2*0) + (1*2) + (2*1) + (1*0) = 1+4+0+0+1+0+2+2+0 = 10
。
- 计算第二个输出值 (12):
- 图中定义了步长 (Stride) = 1,意味着卷积核每次向右移动一格。
- 计算:
(2*1) + (0*2) + (3*1) + (1*0) + (2*1) + (3*0) + (2*2) + (1*1) + (0*0) = 2+0+3+0+2+0+4+1+0 = 12
。
卷积核会以步长为1继续在整个输入图像上滑动,直到生成完整的 3x3 输出特征图。
PyTorch中的nn.Conv2d
在PyTorch中,我们通常使用 torch.nn.Conv2d
来创建二维卷积层。这个模块包含了卷积操作所需的所有参数。让我们看一下官方文档中对这些参数的定义。
Conv2d
参数详解
这里我们介绍几个最核心的参数:
in_channels
(int): 输入通道数。对于灰度图像,它是1;对于常见的RGB彩色图像,它是3。out_channels
(int): 输出通道数。它决定了将使用多少个卷积核。每个卷积核都会生成一个不同的特征图,因此输出通道数等于卷积核的数量。kernel_size
(int or tuple): 卷积核的尺寸。可以是一个数字(例如3
,代表3x3的卷积核),也可以是一个元组(例如(3, 5)
,代表3x5的卷积核)。stride
(int or tuple): 步长,即卷积核每次滑动的距离。默认值为1。如果设为2,卷积核每次会移动两个像素,输出的特征图尺寸会相应减小。padding
(int or str): 填充。它在输入图像的周围添加额外的像素(通常是0)。这有两个主要作用:- 让卷积核能够处理到图像边缘的像素。
- 控制输出特征图的尺寸。一个常见的设置是
padding=1
搭配kernel_size=3
和stride=1
,可以使输出和输入的尺寸保持不变。
代码实例
接下来,我们用代码来复现上面图中的卷积计算,并演示 stride
和 padding
参数的效果。
在PyTorch中,卷积操作的输入需要是一个4D张量,格式为 (N, C, H, W)
,分别代表:
- N: Batch size (批处理大小)
- C: Channel (通道数)
- H: Height (高度)
- W: Width (宽度)
import torch
import torch.nn.functional as F# 定义与图中完全一致的输入和卷积核
input_data = torch.tensor([[1, 2, 0, 3, 1],[0, 1, 2, 3, 1],[1, 2, 1, 0, 0],[5, 2, 3, 1, 1],[2, 1, 0, 1, 1]], dtype=torch.float32)kernel = torch.tensor([[1, 2, 1],[0, 1, 0],[2, 1, 0]], dtype=torch.float32)# 将输入和卷积核调整为 PyTorch 需要的4D格式
# (batch_size=1, in_channels=1, height=5, width=5)
input_data = torch.reshape(input_data, (1, 1, 5, 5))
# (out_channels=1, in_channels=1, height=3, width=3)
kernel = torch.reshape(kernel, (1, 1, 3, 3))# 1. 步长 Stride = 1 (默认情况)
# 我们使用 torch.nn.functional.conv2d 函数直接进行计算
output = F.conv2d(input_data, kernel, stride=1)
print("Stride=1的输出:\n", output)# 2. 步长 Stride = 2
output2 = F.conv2d(input_data, kernel, stride=2)
print("\nStride=2的输出:\n", output2)# 3. 步长 Stride = 1, 填充 Padding = 1
output3 = F.conv2d(input_data, kernel, stride=1, padding=1)
print("\nPadding=1的输出:\n", output3)
print("\nPadding=1后的输出尺寸:", output3.shape)
代码运行结果分析:
-
Stride=1的输出:
tensor([[[[10., 12., 12.],[18., 16., 16.],[13., 9., 3.]]]])
这个结果与我们最开始图中手动计算的“卷积后的输出”完全一致。
-
Stride=2的输出:
tensor([[[[10., 12.],[13., 3.]]]])
当步长变为2时,卷积核跳跃着在输入上滑动,因此采集到的特征更少,输出的特征图尺寸也从 3x3 减小到了 2x2。
-
Padding=1的输出:
tensor([[[[ 1., 3., 4., 10., 8.],[ 5., 10., 12., 12., 6.],[ 7., 18., 16., 16., 8.],[11., 13., 9., 3., 4.],[14., 13., 9., 7., 4.]]]])
当设置
padding=1
时,PyTorch会在原始5x5输入的四周各补上一圈0,使其变成一个7x7的输入。再用3x3的卷积核以步长1进行卷积,最终得到的输出尺寸是 5x5,与原始输入图像的尺寸相同。这在构建深度网络时非常有用,可以防止图像尺寸在逐层卷积后迅速缩小。
多通道卷积:in_channels
和 out_channels
在之前的例子中,我们的输入图像只有一个通道(可以理解为灰度图)。但在实际应用中,图像通常是彩色的,比如有RGB(红、绿、蓝)三个通道。卷积操作需要能处理这种多通道的输入。
上图是对之前例子的一个扩展,帮助我们理解 in_channels
和 out_channels
:
in_channels
(输入通道): 它定义了输入数据有多少个通道。对于上图左侧的输入图像,我们假设它是一个单通道的灰度图,所以in_channel=1
。如果是标准的彩色图片,那么in_channel
就应该是3。out_channels
(输出通道): 它决定了我们想要使用多少个不同的卷积核。每一个卷积核都会在输入上进行一次完整的卷积操作,并生成一个单通道的输出特征图。因此,out_channels
的数量就等于卷积核的数量。- 在上图中,
out_channel=2
意味着我们使用了两个不同的卷积核(图中为了简化,只画了一个新的卷积核[1, 3, 1; 0, 1, 0; 2, 1, 0]
,但实际上是两个独立的3x3x1的卷积核)。 - 这两个卷积核分别与输入图像进行卷积,从而得到了两个独立的输出特征图。这两个特征图堆叠在一起,就构成了我们最终的输出。
- 在上图中,
简单总结:in_channels
描述了输入数据的“深度”,而 out_channels
决定了卷积层将提取多少种不同的特征,也就是输出数据的“深度”。
在代码中应用卷积层
现在,让我们通过一个完整的代码示例,看看如何在PyTorch中定义和使用一个卷积层来处理真实的图像数据。
# 1. 导入必要的库
import torch
import torchvision
from torch import nn
from torch.nn import Conv2d
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter # 用于可视化# 2. 准备数据集
# 使用PyTorch内置的CIFAR10数据集
# transform 将图片数据转换为Tensor格式
dataset = torchvision.datasets.CIFAR10("./data", train=False, transform=torchvision.transforms.ToTensor(),download=True)# DataLoader负责加载数据,并按batch_size打包
dataloader = DataLoader(dataset, batch_size=64)# 3. 定义神经网络模型
class Tudui(nn.Module):def __init__(self):super(Tudui, self).__init__()# 定义一个卷积层self.conv1 = Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=0)def forward(self, x):# 将输入x传入卷积层x = self.conv1(x)return x# 4. 创建模型实例和可视化工具
tudui = Tudui()
writer = SummaryWriter("./logs") # 创建一个写入器,日志会保存在logs文件夹# 5. 遍历数据并进行卷积操作
step = 0
for data in dataloader:imgs, targets = data # 获取一个批次的图片和标签output = tudui(imgs) # 将图片输入到神经网络中# 打印输入和输出的形状,来观察变化print(f"输入的形状: {imgs.shape}")print(f"输出的形状: {output.shape}")# 将结果添加到TensorBoard中进行可视化# 输入图像writer.add_images("input", imgs, step)# 由于add_images不能直接显示6通道的输出,我们需要进行一个形状变换# 将6个通道分成2组,每组3个通道,这样就可以当成RGB图像来显示了output = torch.reshape(output, (-1, 3, 30, 30))writer.add_images("output", output, step)step = step + 1writer.close()
代码详解 (小白视角)
这段代码做了什么?我们一步步来看:
-
导入库:
torch
和torch.nn
: PyTorch的核心库,用于搭建网络。torchvision
: 包含了很多常用的数据集(比如CIFAR10)和图像处理方法。DataLoader
: 一个很方便的工具,可以帮我们把数据集分成一小批一小批的(batch),这样可以高效地训练模型。SummaryWriter
: 这是TensorBoard的可视化工具,可以把我们的输入、输出和模型结构等信息记录下来,方便观察。
-
准备数据集:
- 我们下载了
CIFAR10
数据集,这是一个包含6万张32x32像素彩色小图片的经典数据集。 transform=torchvision.transforms.ToTensor()
: 这是一个非常重要的步骤!它把原始的图片格式转换成了PyTorch能够处理的Tensor
格式。
- 我们下载了
-
定义神经网络 (
Tudui
类):-
__init__(self)
: 这是我们模型的“构造函数”。所有网络层都在这里“声明”。 -
self.conv1 = Conv2d(...)
: 这是本节的核心! 我们在这里定义了一个卷积层。in_channels=3
: 为什么是3?因为CIFAR10是彩色图片,有R、G、B三个通道。out_channels=6
: 我们希望这层网络能学习到6种不同的特征。所以我们定义了6个卷积核,输出的深度就是6。kernel_size=3
: 每个卷积核的大小是3x3。stride=1
,padding=0
: 步长为1,不进行任何填充。
-
forward(self, x)
: 这里定义了数据在网络中的“流动”方向。我们让输入x
直接通过我们刚刚定义的self.conv1
卷积层。
-
-
创建实例:
tudui = Tudui()
: 创建了一个我们刚刚设计的网络的实例。
-
遍历数据并计算:
for data in dataloader:
: 这个循环会一次从dataloader
中取出一批数据(我们设置了batch_size=64
,所以一次取64张图片)。output = tudui(imgs)
: 将这64张图片打包成一个imgs
张量,然后送入网络中进行卷积计算,得到输出output
。print(imgs.shape)
: 打印输入的形状。你会看到torch.Size([64, 3, 32, 32])
。64
: 批处理大小,一次处理64张图。3
: 输入通道数,代表RGB三通道。32, 32
: 图片的高度和宽度是32x32。
print(output.shape)
: 打印输出的形状。你会看到torch.Size([64, 6, 30, 30])
。64
: 批处理大小不变。6
: 通道数从3变成了6! 这正是因为我们设置了out_channels=6
。30, 30
: 图片尺寸从32x32缩小到了30x30。这是因为3x3的卷积核在32x32的图像上(步长1,无填充)进行卷积,其输出尺寸的计算公式为(输入尺寸 - 卷积核尺寸) + 1
,即(32 - 3) + 1 = 30
。
通过这个例子,我们清晰地看到了卷积层是如何改变输入数据的形状的,特别是它如何根据 in_channels
和 out_channels
参数来调整数据的“深度”。
池化层 (Pooling Layers)
在典型的卷积神经网络(CNN)结构中,我们经常在卷积层之后紧跟着一个池化层。要理解池化层,我们首先要明白它和卷积层是如何分工合作的。
为什么要进行卷积和池化?
我们可以把它们看作是一个特征处理的流水线:
-
卷积层 (Convolutional Layer):它的主要任务是特征提取。就像一个侦探拿着不同功能的放大镜(卷积核)去扫描一张图片,有的放大镜专门找边缘,有的专门找颜色块,有的专门找纹理。卷积层通过学习,自动制造出这些功能各异的“放大镜”,并提取出图片中最有用的初始特征。
-
池化层 (Pooling Layer):它的主要任务是特征压缩和降维,可以看作是特征提取后的“精炼”步骤。池化层没有需要学习的参数(它不像卷积层那样有需要学习的权重),它只根据一个固定的规则来工作。
我们为什么需要池化这个“精炼”步骤?
- 减小计算量,防止过拟合:卷积层提取的特征图可能非常大,包含了大量冗余信息。如果直接把这些庞大的特征图送到下一层,计算量会非常巨大,模型也容易“想太多”,把一些不重要的细节也学进去(这叫过拟合)。池化层通过减小特征图的尺寸,大大减少了后续层的计算负担和参数数量。
- 增强平移不变性 (Translation Invariance):想象一下,我们要识别一只猫。无论这只猫在图片的左上角还是稍微往右挪了一点,它都还是一只猫。池化操作(比如取一个区域内的最大值)可以保证,即使图片中的特征(比如猫的耳朵)发生了微小的移动,只要它还在那个小区域内,池化后的结果很可能是不变的。这使得我们的网络更加“稳健”,不会因为目标物体位置的轻微变化就认不出来。
池化操作示意图:一个窗口(核)在特征图上滑动,将每个窗口内的信息压缩成一个单一的值。
最大池化 (Max Pooling)
最大池化是池化操作中最常用的一种。它的规则极其简单:在一个区域内,谁最大就听谁的。它会用一个窗口(池化核)滑过特征图,在每个窗口覆盖的区域内,只保留那个最大的数值作为输出。
上图生动地展示了最大池化的计算过程:
- 输入图像: 左侧是一个 5x5 的矩阵。
- 池化核: 中间是一个 3x3 的窗口 (
kernel_size=3
)。它不像卷积核那样有权重,它只定义了一个“观察”区域。 - 输出: 右侧是池化后的结果。
ceil_mode
参数的影响
在移动池化窗口时,可能会遇到一个问题:走到边缘时,剩下的像素凑不够一个完整的窗口了,怎么办?ceil_mode
这个参数就是用来处理这种情况的。
-
ceil_mode=False
(默认): 这种模式比较“严格”,它采用向下取整(floor)的方式计算输出尺寸。如果最后剩余的像素不足以覆盖整个池化核,那么这部分数据就会被无情地丢弃。- 在上图中,输入尺寸为5,核大小为3,默认步长(stride)也为3。输出尺寸计算为
floor((5-3)/3) + 1 = 1
。所以我们只在左上角取了一次最大值(2
),右边和下边凑不够一个3x3窗口的数据就直接不要了,最终输出只有一个值。
- 在上图中,输入尺寸为5,核大小为3,默认步长(stride)也为3。输出尺寸计算为
-
ceil_mode=True
: 这种模式比较“宽容”,它采用向上取整(ceil)的方式计算输出尺寸。它会保留所有数据,即使边缘的像素不够一个完整的池化窗口,它也会在那个不完整的区域里取一个最大值。- 在上图中,输出尺寸计算为
ceil((5-3)/3) + 1 = 2
。因此它不仅处理了左上角,还把右边和下边的边角料也分别进行了池化,最终得到了一个 2x2 的结果。
- 在上图中,输出尺寸计算为
PyTorch中的 nn.MaxPool2d
在PyTorch中,我们使用 torch.nn.MaxPool2d
来创建一个最大池化层。
核心参数详解
kernel_size
: 池化窗口的大小。这是最重要的参数,定义了我们“观察”区域的尺寸。stride
: 池化窗口滑动的步长。如果这个参数不写,PyTorch会默认让它等于kernel_size
,这意味着池化窗口之间是紧挨着移动,没有重叠的。padding
: 在输入的周围添加填充。这在池化中不常用,一般在卷积中使用。ceil_mode
: 上文解释过的,决定是保留还是丢弃边缘数据。
代码实例与超详细分解
现在,让我们通过一个完整的代码示例,看看最大池化层是如何在实践中工作的。
# =================================================================
# 1. "工具准备" - 导入所有我们需要的工具包 (库)
# =================================================================
import torch
import torchvision
from torch import nn
from torch.nn import MaxPool2d
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter# =================================================================
# 2. "食材准备" - 加载我们的数据集
# =================================================================
# 下载并准备CIFAR10数据集
dataset = torchvision.datasets.CIFAR10("./data", train=False, download=True,transform=torchvision.transforms.ToTensor())# 创建一个数据加载器,它会像传送带一样把数据送给我们的网络
dataloader = DataLoader(dataset, batch_size=64)# =================================================================
# 3. "设计菜谱" - 定义我们的神经网络结构
# =================================================================
class Tudui(nn.Module):# 步骤 3.1: 定义网络的初始化设置 (这个函数只在开始时运行一次)def __init__(self):super(Tudui, self).__init__()# 在这里定义我们网络中需要用到的层# 我们给这个层起个名字叫 maxpool1self.maxpool1 = MaxPool2d(kernel_size=3, ceil_mode=False)# 步骤 3.2: 定义数据在网络中的“烹饪流程” (这个函数每次有数据进来都会运行)def forward(self, input):# 数据流的第一步:通过我们刚刚定义的最大池化层output = self.maxpool1(input)# 返回处理后的结果return output# =================================================================
# 4. "开始烹饪" - 创建网络实例并处理数据
# =================================================================
# 按照我们的 "Tudui" 菜谱,创建一个真正的神经网络
tudui = Tudui()# 准备一个“记录本”(TensorBoard),用来记录和观察我们的输入和输出图片
writer = SummaryWriter("./logs_maxpool")# 初始化一个计数器
step = 0
# 开始从“传送带”上一个一个地取数据
for data in dataloader:# 每一批数据都包含图片(imgs)和它们的标签(targets)imgs, targets = data# 在“记录本”里,记下原始图片的样子writer.add_images("input", imgs, step)# 把图片(imgs)送进我们创建的网络(tudui)里进行处理output = tudui(imgs)# 在“记录本”里,记下经过网络处理后图片的样子writer.add_images("output", output, step)# 在屏幕上打印出处理前后的尺寸,方便我们直观对比print(f"输入形状: {imgs.shape}")print(f"输出形状: {output.shape}")# 计数器加一,准备处理下一批数据step = step + 1# 所有数据都处理完了,合上我们的“记录本”
writer.close()
逐段讲解
1. “工具准备” - import
部分
import torch
: 这是PyTorch的核心,所有计算和张量操作都靠它。import torchvision
: 这是PyTorch的视觉工具箱,里面有很多现成的数据集(比如我们用的CIFAR10)、模型和图像处理方法。from torch.nn import MaxPool2d
: 我们要用“最大池化层”,所以就从torch.nn
(神经网络模块)里把它单独请出来。from torch.utils.data import DataLoader
: 我们要用“数据加载器”,它能帮我们把庞大的数据集一小批一小批地整理好。from torch.utils.tensorboard import SummaryWriter
: 我们要用“记录员”,它能帮我们把中间过程的图片保存下来,方便我们用一个叫TensorBoard的工具来查看。
2. “食材准备” - dataset
和 dataloader
dataset = torchvision.datasets.CIFAR10(...)
: 这一行做了两件事:- 从
torchvision
里找到CIFAR10
这个著名的数据集。 - 如果你的电脑里没有这个数据集(在
./data
文件夹里),download=True
会自动帮你下载。 transform=torchvision.transforms.ToTensor()
: 这是最关键的一步! 电脑里的图片(像.jpg)和神经网络能处理的数据格式不一样。这个transform
就像一个翻译官,把普通图片“翻译”成PyTorch能理解的Tensor格式。
- 从
dataloader = DataLoader(dataset, batch_size=64)
: 我们的数据集里有10000张测试图片,一次性全塞给电脑,内存可能会爆炸。DataLoader
就像一个传送带,它把dataset
里的图片每64张打包成一小份(这就是batch_size=64
的含义),然后分批次送给我们处理。
3. “设计菜谱” - class Tudui
class Tudui(nn.Module)
: 这是在PyTorch里创建网络的标准格式,你可以把它理解为“我们正在创建一个名叫Tudui的神经网络蓝图,这个蓝图是基于nn.Module
这个官方模板的”。def __init__(self)
: 这是“初始化”或“准备阶段”。在这里,我们要把这道菜(网络)需要用到的所有“锅碗瓢盆”(网络层)都先准备好。这个函数只在最开始创建网络时运行一次。self.maxpool1 = MaxPool2d(...)
: 我们在这里创建了一个最大池化层,并给它取名叫maxpool1
。kernel_size=3
: 设置池化的窗口大小是3x3,和我们图示中的一样。ceil_mode=False
: 当窗口移动到边缘,剩下的像素凑不够一个3x3的窗口时,False
表示“那就扔掉这些边角料吧”。
def forward(self, input)
: 这是“烹饪流程”。它定义了数据(食材input
)进入网络后,应该按什么顺序经过哪些层。每次我们把数据喂给网络,这个函数就会被自动调用。output = self.maxpool1(input)
: 这一行清晰地描述了流程:把进来的input
数据,放进我们之前准备好的self.maxpool1
层里进行处理,然后把结果存到output
里。
4. “开始烹饪” - 主流程部分
tudui = Tudui()
: 按照Tudui
这个蓝图,创建一个真实的网络实例,现在tudui
就是我们随时可以使用的神经网络了。writer = SummaryWriter(...)
: 创建一个记录员实例,它会把日志写到./logs_maxpool
文件夹里。for data in dataloader:
: 这是一个循环,意思是“传送带 (dataloader
),请开始工作!把你的数据一包一包 (data
) 地递给我,直到给完为止”。imgs, targets = data
: 每一包data
里都有两样东西:64张图片(imgs
)和这64张图片对应的正确答案(targets
)。writer.add_images("input", imgs, step)
: 用记录员把这批原始的imgs
保存下来,在TensorBoard里给它们起个名字叫"input"
。step
是批次的编号。output = tudui(imgs)
: 这是最核心的调用! 把64张图片imgs
直接塞进tudui
网络。PyTorch会自动调用Tudui
类里的forward
函数,完成池化计算,并返回结果output
。writer.add_images("output", output, step)
: 再次使用记录员,把处理后的结果output
也保存下来,起名叫"output"
。print(...)
: 在电脑屏幕上打出imgs
和output
的尺寸。你会看到:输入形状: torch.Size([64, 3, 32, 32])
输出形状: torch.Size([64, 3, 10, 10])
- 分析:输入是64张3通道的32x32图片。经过池化后,通道数
3
不变,但图片尺寸从32x32
缩小到了10x10
。这是因为floor((32 - 3) / 3) + 1 = 10
。这直观地展示了池化层的降维作用。
writer.close()
: 循环结束,所有数据都处理完了。告诉记录员可以收工了。
神经网络 - 非线性激活
在我们的神经网络“食谱”中,我们已经有了卷积层(负责提取特征)和池化层(负责精炼和压缩特征)。现在,我们需要加入一个关键的“调味品”——非线性激活函数。它通常紧跟在卷积层之后。
为什么要使用非线性激活函数?
想象一下,你正在用一堆直尺来画一个圆圈。无论你把这些直尺怎么拼接、怎么叠加,你最终得到的永远是一条更长的直线或者一个多边形,你永远画不出一条平滑的曲线。
在神经网络中,卷积操作和后续会学到的全连接层本质上都是线性的(就像那些直尺)。如果你只用这些线性层来搭建网络,那么无论你搭建多么深的网络,它最终的效果也只相当于一个简单的线性模型。这样的网络能力非常有限,无法学习和拟合现实世界中各种复杂的、非线性的数据分布(比如识别千姿百态的猫和狗)。
非线性激活函数的作用,就是给这个由“直尺”构成的系统,引入“掰弯”的能力。
它对上一层传来的数据进行一次非线性的变换,使得神经网络不再是一个简单的线性组合,从而具备了学习复杂模式的能力,能够真正地去拟“合”任意函数或曲线。可以说,没有非线性激活函数,深度学习的“深度”就失去了意义。
ReLU 激活函数 (torch.nn.ReLU
)
ReLU (Rectified Linear Unit, 修正线性单元) 是目前最受欢迎、最常用的激活函数之一。它的规则非常简单粗暴,但效果却出奇地好。
规则: 输入一个数,如果这个数大于0,就原样输出;如果这个数小于或等于0,就输出0。用数学公式表达就是 output = max(0, input)
。
优点:
- 计算速度快:只做一个简单的判断和置零操作,比后面要讲的Sigmoid函数计算快得多。
- 缓解梯度消失:在正数部分,它的导数恒为1,这有助于在反向传播时保持梯度的流动,让深层网络也能得到有效的训练。
核心参数:inplace
torch.nn.ReLU
有一个重要的参数 inplace
。
上图清晰地展示了 inplace
参数为 True
和 False
时的区别:
-
inplace=False
(默认值):这是更安全、更常见的用法。它会创建一个新的内存空间来存储计算结果,而原始的输入数据保持不变。- 比喻:就像你复印了一份文件,然后在复印件上做修改。原始文件安然无恙。
- 如上图右侧所示:
input
变量在经过Relu
计算后,它自身的值-1
并没有改变。计算结果0
被存放在了一个新的output
变量里。
-
inplace=True
:这个选项会直接在原始输入数据的内存上进行修改,从而节省内存空间。- 比喻:就像你直接在原始文件上做修改。这样虽然省了纸(内存),但原始文件也被永久改变了。
- 如上图左侧所示:
input
变量在经过Relu
计算后,它自身的值被直接从-1
修改成了0
。 - 注意:这种做法可能会导致问题,特别是当你网络中其他部分还需要使用原始数据时。除非你非常清楚自己在做什么并且需要极致地优化内存,否则建议保持默认的
inplace=False
。
Sigmoid 激活函数
Sigmoid 是一个经典的激活函数,它的主要作用是将任何实数压缩到 (0, 1) 这个区间内。
规则:它的数学函数图像是一个优美的"S"型曲线。非常大的正数会被压缩到接近1,非常大的负数会被压缩到接近0。
应用场景:由于它能将输出映射到 (0, 1) 区间,非常适合用在二分类任务的输出层,来表示一个样本属于某个类别的概率。但在隐藏层中,由于它存在梯度消失的问题(在输入值非常大或非常小时,函数曲线变得很平坦,梯度接近于0,导致网络难以训练),现在已经较少使用,通常被ReLU等函数替代。
代码实例与超详细分解 (小白版)
现在,我们通过代码来实际应用这些激活函数,并观察它们的效果。
# =================================================================
# 1. "工具准备" - 导入所有我们需要的工具包 (库)
# =================================================================
import torch
import torchvision
from torch import nn
from torch.nn import ReLU, Sigmoid # 把我们要用的激活函数请出来
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter# =================================================================
# 2. "食材准备" - 加载我们的数据集
# =================================================================
# 依然使用CIFAR10数据集
dataset = torchvision.datasets.CIFAR10("./data", train=False, download=True,transform=torchvision.transforms.ToTensor())
# 创建数据加载器
dataloader = DataLoader(dataset, batch_size=64)# =================================================================
# 3. "设计菜谱" - 定义我们的神经网络结构
# =================================================================
class Tudui(nn.Module):# 步骤 3.1: 定义网络的初始化设置def __init__(self):super(Tudui, self).__init__()# 准备好我们需要的激活函数层self.relu1 = ReLU(inplace=False) # 明确写出inplace=False,这是默认行为self.sigmoid1 = Sigmoid()# 步骤 3.2: 定义数据在网络中的“烹饪流程”def forward(self, input):# 让数据流过Sigmoid激活函数output = self.sigmoid1(input)# 你也可以换成ReLU来观察不同的效果:# output = self.relu1(input)return output# =================================================================
# 4. "开始烹饪" - 创建网络实例并处理数据
# =================================================================
# 按照 "Tudui" 菜谱,创建网络实例
tudui = Tudui()# 准备一个“记录本”(TensorBoard)
writer = SummaryWriter("./logs_relu") # 日志保存在logs_relu文件夹# 初始化计数器
step = 0
# 开始从“传送带”上取数据
for data in dataloader:# 获取图片和标签imgs, targets = data# 在“记录本”里记下原始图片writer.add_images("input", imgs, global_step=step)# 把图片送入网络,进行激活函数处理output = tudui(imgs)# 在“记录本”里记下处理后的图片writer.add_images("output", output, global_step=step)# 在屏幕上打印出处理前后的尺寸print(f"输入形状: {imgs.shape}")print(f"输出形状: {output.shape}")# 计数器加一step = step + 1# 合上“记录本”
writer.close()
逐段讲解
1 & 2. “工具准备” & “食材准备”
这部分和之前的例子完全一样。我们导入了必要的工具,并准备好了CIFAR10数据集的“传送带”dataloader
。
3. “设计菜谱” - class Tudui
from torch.nn import ReLU, Sigmoid
: 我们这次从torch.nn
中明确请出了ReLU
和Sigmoid
这两个激活函数。__init__(self)
: 在网络的准备阶段,我们创建了两个激活层的实例:self.relu1 = ReLU(inplace=False)
: 创建一个ReLU层,并把它存为self.relu1
。self.sigmoid1 = Sigmoid()
: 创建一个Sigmoid层,并把它存为self.sigmoid1
。- 现在,我们的网络里就有了这两个“工具”,随时可以在
forward
流程中调用。
forward(self, input)
: 在数据处理流程中,我们让输入数据input
流经self.sigmoid1
层,然后返回结果。你可以试着把self.sigmoid1
换成self.relu1
,然后去TensorBoard里观察两种不同激活函数处理后图像的视觉差异。
4. “开始烹饪” - 主流程部分
这部分的核心在于观察输入和输出的变化:
output = tudui(imgs)
: 将一批图片imgs
送入网络。PyTorch会自动调用forward
方法,对图片中的每一个像素值应用我们定义的Sigmoid函数。writer.add_images(...)
: 我们把原始图片imgs
和经过激活函数处理后的output
都保存到TensorBoard中。- 观察:打开TensorBoard后,你会发现,经过Sigmoid处理后的输出图片,整体色调会发生变化。因为所有像素值都被压缩到了0到1之间,负数像素值会变成接近0(黑色),正数像素值会被压缩。而如果使用ReLU,所有负数像素值会直接变成0(纯黑),正数值则保持不变,图像对比度会发生明显变化。
print(f"输入形状: ...")
和print(f"输出形状: ...")
: 这是本节一个非常重要的知识点!你会看到打印出的信息是:
注意:输入和输出的形状完全一样!输入形状: torch.Size([64, 3, 32, 32]) 输出形状: torch.Size([64, 3, 32, 32])
- 结论:非线性激活函数是对数据中的每一个元素独立进行操作的(element-wise)。它只改变元素的值,不会改变数据张量的形状(高度、宽度、通道数等)。这一点与会改变尺寸的池化层和某些卷积层有本质区别。
线性层及其他层介绍
我们已经学习了构成卷积神经网络(CNN)核心的卷积层、池化层和激活函数。这些层非常擅长从图像这样的网格数据中提取空间特征。但是,当特征提取完成后,我们如何根据这些提取到的特征(比如“有毛茸茸的耳朵”、“有胡须”、“有尖鼻子”)来做出最终的分类判断(“这是一只猫”)呢?
这就是线性层 (Linear Layer) 的用武之地。
Linear 层 (全连接层)
线性层,在很多地方也被称为全连接层 (Fully Connected Layer),是神经网络中一种非常基础和重要的层。
观察上图,我们可以看到:
- 输入层 (Input layer):接收原始数据。
- 隐藏层 (Hidden layer):进行中间的计算和特征转换。
- 输出层 (Output layer):产生最终的结果。
“全连接” 这个名字非常形象。它的意思是,前一层中的每一个神经元(节点),都与当前层中的每一个神经元相连接。如上图所示,隐藏层中的每一个g
节点,都连接着输出层中的每一个o
节点。
在卷积神经网络中,线性层通常出现在网络的末端。它的工作流程是:
- 接收从卷积层和池化层传来
的、已经被高度提炼的特征图 (Feature Map)。 - 由于这些特征图通常是二维或三维的,线性层无法直接处理。因此,在进入线性层之前,我们需要一个关键的“预处理”步骤:将这些多维的特征图“压平”或“拉直”,变成一个一维的长向量。
- 线性层接收这个一维向量,并通过一系列的线性变换(矩阵乘法和加法),最终将这些高级特征映射到我们想要的输出维度上(例如,在一个10分类任务中,就映射到10个输出值)。
PyTorch中的 nn.Linear
在PyTorch中,我们使用 torch.nn.Linear
来创建线性层。它的核心参数只有两个:
in_features
: 输入特征的数量。这个数字就是我们把特征图“压平”成一维向量后的长度。out_features
: 输出特征的数量。在分类任务中,这个数字通常等于我们任务的类别总数。例如,CIFAR10有10个类别,所以out_features
通常设为10。
代码实例与超详细分解
让我们通过代码来理解线性层,特别是那个关键的“压平”步骤。
# =================================================================
# 1. "工具准备" - 导入所有我们需要的工具包 (库)
# =================================================================
import torch
import torchvision
from torch import nn
from torch.nn import Linear # 把我们要用的线性层请出来
from torch.utils.data import DataLoader# =================================================================
# 2. "食材准备" - 加载我们的数据集
# =================================================================
dataset = torchvision.datasets.CIFAR10("./data", train=False, download=True,transform=torchvision.transforms.ToTensor())
dataloader = DataLoader(dataset, batch_size=64)# =================================================================
# 3. "设计菜谱" - 定义我们的神经网络结构
# =================================================================
class Tudui(nn.Module):# 步骤 3.1: 定义网络的初始化设置def __init__(self):super(Tudui, self).__init__()# 我们在这里定义一个线性层# CIFAR10图片尺寸是 3x32x32,压平后长度为 3*32*32 = 3072# 我们希望最终输出10个类别,所以 out_features=10self.linear1 = Linear(in_features=3072, out_features=10)# 步骤 3.2: 定义数据在网络中的“烹饪流程”def forward(self, input):# 线性层需要一维向量,所以我们在这里进行“压平”操作# input的原始形状是 [64, 3, 32, 32]# 我们使用 flatten,并指定从第1个维度开始压平(保留第0维,也就是batch_size)input_flattened = torch.flatten(input, start_dim=1)# 压平后的形状是 [64, 3072]# 将压平后的数据送入线性层output = self.linear1(input_flattened)return output# =================================================================
# 4. "开始烹饪" - 创建网络实例并处理数据
# =================================================================
# 按照 "Tudui" 菜谱,创建网络实例
tudui = Tudui()# 开始从“传送带”上取数据
for data in dataloader:# 获取一批图片imgs, targets = dataprint(f"原始图片的形状: {imgs.shape}")# 把图片送入网络,进行处理output = tudui(imgs)print(f"经过线性层后的输出形状: {output.shape}")break # 我们只处理一批数据就停下来,方便观察
逐段讲解
1 & 2. “工具准备” & “食材准备”
这部分和之前一样,我们准备好了工具和CIFAR10数据集的dataloader
。
3. “设计菜谱” - class Tudui
__init__(self)
: 在网络的准备阶段,我们创建了一个线性层实例:self.linear1 = Linear(in_features=3072, out_features=10)
: 这是本节的核心!out_features=10
: 这很容易理解,因为CIFAR10数据集有10个类别(飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车)。in_features=3072
: 这个数字是怎么来的?一张CIFAR10图片有3个颜色通道,每个通道的尺寸是32x32像素。所以,一张图片包含的总像素信息量就是3 * 32 * 32 = 3072
。这就是我们把它“压平”成一维向量后的长度。
forward(self, input)
: 在数据处理流程中,我们定义了两个步骤:input_flattened = torch.flatten(input, start_dim=1)
: 这是关键的**“压平”**操作。input
的形状是[64, 3, 32, 32]
,代表一个批次里有64张3通道的32x32图片。torch.flatten
的作用就是把张量拉直。start_dim=1
参数告诉函数:“请保留第0个维度(也就是64这个批次大小batch_size
)不动,从第1个维度(也就是通道channel
)开始,把后面的所有维度 (3
,32
,32
) 都合并成一个维度。”- 所以,
input_flattened
的形状就变成了[64, 3072]
。这完美地匹配了我们线性层的输入要求:N x in_features
,其中N
是批次大小。
output = self.linear1(input_flattened)
: 将这个形状为[64, 3072]
的张量送入我们定义的linear1
层。
4. “开始烹饪” - 主流程部分
output = tudui(imgs)
: 将一批原始图片imgs
送入网络。PyTorch会自动调用forward
方法,先进行压平,再进行线性变换。print(...)
: 当你运行这段代码,你会看到控制台打印出:原始图片的形状: torch.Size([64, 3, 32, 32]) 经过线性层后的输出形状: torch.Size([64, 10])
- 分析:这个输出形状
[64, 10]
清晰地告诉我们:对于我们输入的64张图片中的每一张,网络都给出了10个输出值。这10个值可以被看作是这张图片属于10个不同类别的“得分”。之后,我们通常会用一个Softmax
函数来将这些得分转换成概率。
- 分析:这个输出形状
其他重要层简介
正则化层 (Regularization Layers)
- 作用: 主要用来防止模型过拟合。过拟合就像一个学生只会死记硬背,考试时遇到一模一样的题就会做,题目稍微变一下就错了。正则化就是通过一些手段,强迫模型学习到更通用、更稳健的规律,而不是去记一些无关紧要的细节。
- 常见例子:
nn.BatchNorm2d
(批量归一化)。它在网络中间,对数据进行“标准化”处理,使其均值为0,方差为1。这不仅能防止过拟合,还能大大加快模型训练速度,使训练过程更稳定。
Dropout 层 (Dropout Layer)
- 作用: 也是一种非常强大和常用的防止过拟合的技术。
- 工作原理 (非常有趣): 在训练过程的每一步,Dropout层会随机地“丢弃”或“关闭”一部分神经元(让它们的输出变成0)。
- 比喻: 想象一个团队在做一个项目,如果每次开会都随机让几个人“缺席”,那么为了项目能继续下去,团队里的每个人都必须变得更强,不能过分依赖任何一个特定的队友。
- 对于神经网络来说,这强迫网络不能过度依赖少数几个神经元的激活,而是要学习到更加冗余和鲁棒的特征。
Recurrent 层 (循环层)
- 作用: 专门用来处理序列数据,比如一句话(单词序列)、一段音乐(音符序列)、或者股票价格(时间序列)。
- 核心思想: 循环层拥有“记忆”。当它处理序列中的当前元素时,它不仅会考虑当前的输入,还会考虑它从上一个元素那里继承来的“记忆”(隐藏状态)。这使得它能理解上下文关系。
- 常见例子:
nn.RNN
,nn.LSTM
,nn.GRU
。
Transformer 层 (Transformer Layer)
- 作用: 一种更现代、更强大的处理序列数据的架构,目前已经成为自然语言处理(NLP)领域的绝对主流。
- 核心思想: 它最关键的创新是**“自注意力机制” (Self-Attention)**。这种机制允许模型在处理序列中的一个元素时,能够直接计算出序列中所有其他元素对当前这个元素的重要性,并给予不同的“关注度”。它不再像RNN那样需要一个一个地传递“记忆”,而是可以一步到位地看到全局信息。
- 著名模型: 大名鼎鼎的GPT (ChatGPT的基础) 和 BERT都是基于Transformer架构构建的。
搭建小型网络与Sequential的使用
我们现在将前面学到的所有零散的“积木块”(卷积层、池化层、线性层等)组合起来,搭建一个完整的小型神经网络,并学习一个非常有用的工具——nn.Sequential
——来简化这个搭建过程。
到目前为止,我们已经了解了构建卷积神经网络(CNN)的各种核心组件。一个典型的CNN模型,如此下图所示的用于CIFAR10分类任务的结构,就是将这些组件按照特定顺序“串联”起来,形成一个完整的数据处理流水线。
这个流水线通常遵循一个模式:
- 输入 (Inputs):接收原始图像数据。
- 特征提取模块: 通过连续的 卷积层 (Convolution) → 池化层 (Max-pooling) 组合,逐步提取越来越抽象和高级的特征。在这个过程中,特征图的深度(通道数)通常会增加,而空间尺寸(高度和宽度)会减小。
- 压平 (Flatten):在特征提取完成后,将最终得到的多维特征图“拉直”成一个一维向量。
- 分类模块: 将这个一维向量送入一个或多个全连接层 (Fully connected / Linear),将高级特征映射到最终的分类得分上。
- 输出 (Outputs):得到每个类别的最终得分。
手动搭建网络
我们可以按照上面的流程,在 __init__
中定义好每一个层,然后在 forward
方法中,像接力赛一样,手动地将数据依次传递给每一层。
# =================================================================
# 1. 导入所有需要的层
# =================================================================
import torch
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear
from torch.utils.tensorboard import SummaryWriter# =================================================================
# 2. 手动定义网络结构
# =================================================================
class Tudui(nn.Module):def __init__(self):super(Tudui, self).__init__()# 定义特征提取部分self.conv1 = Conv2d(3, 32, 5, padding=2)self.maxpool1 = MaxPool2d(2)self.conv2 = Conv2d(32, 32, 5, padding=2)self.maxpool2 = MaxPool2d(2)self.conv3 = Conv2d(32, 64, 5, padding=2)self.maxpool3 = MaxPool2d(2)# 定义压平层self.flatten = Flatten()# 定义分类部分self.linear1 = Linear(1024, 64) # 注意这里的输入尺寸计算self.linear2 = Linear(64, 10)def forward(self, x):# 像接力赛一样,手动一步步传递数据x = self.conv1(x)x = self.maxpool1(x)x = self.conv2(x)x = self.maxpool2(x)x = self.conv3(x)x = self.maxpool3(x)x = self.flatten(x)x = self.linear1(x)x = self.linear2(x)return x# =================================================================
# 3. 验证网络
# =================================================================
tudui = Tudui()
print(tudui) # 打印网络结构
# 创建一个符合输入尺寸的假数据
input = torch.ones((64, 3, 32, 32))
output = tudui(input)
print(f"经过网络后的输出形状: {output.shape}")
尺寸变化计算
这里最关键的是计算 Linear
层的 in_features
。我们需要知道数据经过所有卷积和池化层后,尺寸变成了多少。我们可以使用官方文档提供的尺寸计算公式:
- 初始输入:
[64, 3, 32, 32]
- conv1:
padding=2
,kernel_size=5
,stride=1
(默认)。H_out = (32 + 2*2 - 5)/1 + 1 = 32
。尺寸不变,通道变为32。->[64, 32, 32, 32]
- maxpool1:
kernel_size=2
,stride=2
(默认)。H_out = (32 - 2)/2 + 1 = 16
。尺寸减半。->[64, 32, 16, 16]
- conv2: 尺寸不变,通道不变。->
[64, 32, 16, 16]
- maxpool2: 尺寸减半。->
[64, 32, 8, 8]
- conv3: 尺寸不变,通道变为64。->
[64, 64, 8, 8]
- maxpool3: 尺寸减半。->
[64, 64, 4, 4]
- Flatten: 压平后的长度为
64 * 4 * 4 = 1024
。这就是第一个Linear
层in_features
的由来。
这种手动搭建的方式非常灵活,但当网络结构是简单的线性堆叠时,forward
函数会显得很长很啰嗦。
使用 nn.Sequential
简化搭建
对于上面这种“一条路走到黑”的顺序结构,PyTorch提供了一个非常方便的容器:nn.Sequential
。你可以把它想象成一个管道,我们只需要把各种层按顺序放进去,数据就会自动地从管道的入口流到出口,依次通过每一个层,我们无需再手动编写forward
的每一步。
代码实例与超详细分解
# =================================================================
# 1. "工具准备" - 导入所有需要的工具包 (库)
# =================================================================
import torch
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential # 额外导入 Sequential
from torch.utils.tensorboard import SummaryWriter# =================================================================
# 2. "设计菜谱" - 使用 Sequential 定义网络
# =================================================================
class Tudui(nn.Module):# 步骤 2.1: 定义网络的初始化设置def __init__(self):super(Tudui, self).__init__()# 把所有的层按顺序放进 Sequential 这个“管道”里# 我们给这个管道起个名字叫 model1self.model1 = Sequential(Conv2d(3, 32, 5, padding=2),MaxPool2d(2),Conv2d(32, 32, 5, padding=2),MaxPool2d(2),Conv2d(32, 64, 5, padding=2),MaxPool2d(2),Flatten(),Linear(1024, 64),Linear(64, 10))# 步骤 2.2: 定义数据在网络中的“烹饪流程”def forward(self, x):# 整个流程简化为一步:把数据直接送进“管道”即可x = self.model1(x)return x# =================================================================
# 3. "开始烹饪" & "观察结果"
# =================================================================
# 按照 "Tudui" 菜谱,创建网络实例
tudui = Tudui()
# 打印网络结构,你会看到一个清晰的 Sequential 列表
print(tudui)# 准备一个符合输入尺寸的假数据 (64张3通道32x32的图片)
input = torch.ones((64, 3, 32, 32))
# 把数据送入网络处理
output = tudui(input)
# 打印输出形状,验证网络是否正确工作
print(f"经过 Sequential 网络后的输出形状: {output.shape}")# =================================================================
# 4. "可视化菜谱" - 使用 TensorBoard 查看网络图
# =================================================================
# 准备一个“记录本”(TensorBoard)
writer = SummaryWriter("./logs_seq")
# 调用 add_graph,把我们的网络(tudui)和一份输入数据(input)传进去
writer.add_graph(tudui, input)
# 合上“记录本”
writer.close()
逐段讲解
1 & 2. “工具准备” & “设计菜谱”
from torch.nn import Sequential
: 我们从torch.nn
中请出了Sequential
这个新工具。__init__(self)
:self.model1 = Sequential(...)
: 这是最核心的改变。我们创建了一个Sequential
的实例,并像写清单一样,按计算顺序把所有需要的网络层作为参数传了进去。PyTorch会自动为它们编号。
forward(self, x)
:x = self.model1(x)
: 代码变得极其简洁! 我们不再需要一行一行地调用conv1
,maxpool1
… 整个冗长的forward
函数被简化成了一行。数据x
被送入self.model1
这个管道,它会自动地、按顺序地流过管道中的每一个层,最后返回最终的计算结果。
4. “可视化菜谱” - TensorBoard
nn.Sequential
的另一个巨大好处是,它能让我们的网络结构在可视化工具中显得非常清晰。
writer.add_graph(tudui, input)
: 这个强大的函数能自动解析我们的网络结构,并生成一个可视化的计算图。- 观察结果: 当你打开TensorBoard (
logs_seq
文件夹) 并切换到 “GRAPHS” 标签页,你会看到:- 一个高层视图,显示数据从
input
节点流入我们的Tudui
模型,然后流向output
节点。 - 双击
Tudui
模块,你会看到它的内部结构,主要就是一个名叫Sequential[model1]
的方块。 - 再次双击这个
Sequential
方块,奇迹发生了! 整个网络的流水线被清晰地、自上而下地展示出来:Conv2d
->MaxPool2d
->Conv2d
…Linear
。 - 你甚至可以继续点击单个层,查看它的内部细节,比如权重
weight
和偏置bias
这些可学习的参数。
- 一个高层视图,显示数据从
这个可视化功能对于理解数据在网络中的流动路径、检查网络连接是否正确以及调试复杂模型非常有帮助。而nn.Sequential
的使用,让这个图的结构变得尤为整洁和易于理解。