PyTorch03-PyTorch的主要组成模块

三、PyTorch的主要组成模块

3.1 深度学习流程及与机器学习差异

一项机器学习任务的整体流程如下:

  • 数据准备与划分:对原始数据进行清洗、格式统一与特征变换,并划分为训练集验证集测试集(如采用随机切分或 K 折交叉,可以使用sklearn带的test_train_split函数、kfold来实现)。
  • 模型与算法配置:选择合适的网络架构,指定衡量误差的损失函数、更新参数的优化器以及相关超参数(可以使用sklearn这样的机器学习库中模型自带的损失函数和优化器)。
  • 模型训练(拟合):将训练集输入模型,通过反向传播算法迭代更新权重以拟合数据。
  • 性能评估与调优:在验证集和测试集上评估模型的泛化能力,并根据表现进行超参数微调或最终效果验证。

深度学习与机器学习的差异如下:

对比维度 传统机器学习 深度学习
本质定义 数据驱动让程序从经验中提升性能,包含多种算法范式 机器学习的一个分支,特指用多层神经网络自动提取特征
特征提取方式 人工设计特征。依赖专家经验从原始数据中提取有用特征(如TF-IDF、HOG、SIFT等),特征的好坏直接影响模型上限 自动学习特征。网络底层自动学习边缘、纹理等低级特征,高层组合成语义特征,实现端到端学习,无需人工设计
数据需求量 较小。几百到几万样本即可取得不错效果,在小数据集上表现稳定,不易过拟合 较大。因参数量巨大,通常需要十万级以上样本才能充分训练,否则容易过拟合;可通过迁移学习/数据增强缓解
硬件依赖 CPU即可胜任。计算量相对较小,普通笔记本电脑即可运行大部分算法 重度依赖GPU。矩阵运算密集,需要GPU加速才能在大规模数据和深层网络上合理时间内完成训练
数据加载方式 通常可全量加载。数据量小,可一次性读入内存,训练过程简单直接 需mini-batch分批加载。数据量和模型参数量远超显存容量,必须分批次送入GPU训练,配合DataLoader、预读取等机制;批大小(batch size)本身也是影响模型收敛的重要超参数
模型构建方式 调用现成算法库。通常直接调用sklearn等库中的完整算法类,传入参数即可,无需逐层搭建网络结构 逐层搭建或模块组装。需手动定义网络各层(卷积层、池化层、批正则化层、LSTM层等),或预定义功能模块再组装,灵活性强但代码实现要求更高
损失函数与优化器 与深度学习类似,均需设定损失函数和优化器;但传统模型多为凸优化,调参相对简单 损失函数和优化器需保证反向传播能在用户自定义的复杂网络结构上正确实现;非凸优化对学习率、动量等超参数更敏感
GPU配置与操作 通常不需要。大部分传统ML算法在CPU上运行即可满足需求 必须显式管理。需将模型和数据手动“放到”GPU(.cuda().to(device)),损失函数和优化器也需在GPU上工作;多卡训练还需考虑模型并行/数据并行、梯度同步、结果汇总等问题;验证/评测时常需将数据“放回”CPU
训练与验证流程 全量数据一次性训练。通常一轮训练即可收敛(如线性回归闭式解),或少量迭代(如随机森林建树) 按批循环训练。每个epoch遍历所有batch,每批数据前向传播→计算损失→反向传播→优化器更新参数;需协调DataLoader、模型、损失函数、优化器、GPU之间的配合;验证时同样按批计算指标
可解释性 较强。树模型可直接输出特征重要性排序,线性模型系数有明确含义,易于理解和调试 较弱。黑盒特性显著,难以直观解释内部决策逻辑,需借助Grad-CAM、SHAP、注意力可视化等事后工具辅助理解
适用数据类型 结构化表格数据为主。在金融风控、电商推荐等表格数据场景中表现优异 非结构化数据为主。图像、语音、文本、视频等原始高维数据是深度学习的主场
表格数据表现 常胜。XGBoost、LightGBM等梯度提升树在表格数据上至今是工业界首选 未必占优。纯深度模型在表格数据上通常打不过调优后的GBDT,需结合TabNet等专门设计才有竞争力
图像/语音/NLP 基本退场。传统方法在这些领域已被深度学习全面超越 绝对主场。CNN统治图像,RNN/Transformer统治序列数据,大语言模型引领NLP前沿
典型算法举例 线性回归、逻辑回归、支持向量机(SVM)、决策树、随机森林、XGBoost、K近邻(KNN)、K均值聚类 卷积神经网络(CNN)、循环神经网络(RNN/LSTM)、Transformer、生成对抗网络(GAN)、扩散模型(Diffusion)、BERT/GPT

其中需要特别提到的是,深度学习所需的样本量一般更大,所以相关数据(训练集/验证集/测试集)一般无法一次性被加载到内存或显存中,此时一般分批输入数据。因此深度学习在数据加载上需要有专门的设计。

3.2 基本配置

一些常用的包

在使用PyTorch中,需要导入一些常用的包来使用其提供的特定功能。常见的包有如下:os、sys、torch、torch.nn、torch.utils.data.Dataset、torch.utils.data.DataLoader、torch.optimizer、numpy、matplotlib、seaborn,如果涉及到表格读取与处理一般会用到pandas,下游分析和指标计算一般会用到sklearn。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import os 
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import torch.optim as optimizer
import matplotlib
import seaborn
import sklearn

print(f"numpy: {np.__version__}")
print(f"torch: {torch.__version__}")
print(f"matplotlib: {matplotlib.__version__}")
print(f"seaborn: {seaborn.__version__}")
print(f"sklearn: {sklearn.__version__}")

print("导包完成!")


# 输出如下:
numpy: 2.5.1
torch: 2.6.0+cu124
matplotlib: 3.11.1
seaborn: 0.13.2
sklearn: 1.9.0
导包完成!

一些常用的超参数:

可以通过变量的形式设置超参数,然后在代码中使用:

1
2
3
4
5
6
batch_size = 16
# 批次的大小
lr = 1e-4
# 优化器的学习率
max_epochs = 100
...

也可以使用yamljsondict等文件来存储超参数,这样可以方便后续的调试和修改,常见的深度学习库(mmdetection,Paddledetection,detectron2)和一些AI Lab里面比较常用这种方式。

指定GPU

如果未指明要使用GPU,程序中默认使用的是CPU。如果指明了使用GPU但未指定GPU编号,那么默认就一直使用第0个GPU,容易造成此GPU资源如显存耗尽,所以最好明确要使用的GPU的编号。一般有如下两个方法

1
2
3
4
5
6
7
8
9
10
11
12
# 方案一:过滤/屏蔽指定的 GPU。
# 1. 过滤/屏蔽指定的 GPU:仅将物理上的 1 号和 2 号卡暴露给当前程序。
# 2. PyTorch 内部会将这 2 张可见卡重新映射为逻辑上的 cuda:0 和 cuda:1。
# 3. 此时当前程序一共只能看到 2 张卡,只能使用cuda:0、cuda:1。如果尝试调用 cuda:2 或更高索引,会因索引越界而报错 (invalid device ordinal)。
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '1,2' # 程序只能看到服务器上的1,2号GPU,它们会被映射为0,1号GPU

# 方案二:代码中动态为device变量赋值,后续对要使用GPU的变量用.to(device)。其实就是动态指定要使用的GPU编号
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu") # 指明调用的GPU为1

# 方案三:在终端中运行,仅将系统的 0 号和 1 号 GPU 暴露给该 Python 程序
CUDA_VISIBLE_DEVICES=0,1 python train.py

3.3 数据读入

此小节学习如下内容:PyTorch常见的数据读取方式、构建自己的数据读取流程。


在PyTorch中,通过Dataset与DataLoader的配合使用,完成对的数据集的加载与批量读入。其中Dataset告诉 PyTorch 数据在哪里、怎么读取某一条具体的数据(比如读取一张图片、一段文本或一行表格数据)、读取出来后要做什么预处理(数据变换/Augmentation),DataLoader本身就是一个可迭代对象(Iterable)、以迭代的方式不断按批次(就是一次读入固定的N个单数据,最后一个批次可能小于N个单数据)读入数据。

总结起来这二者的作用如下:

  • Dataset:解决“单条数据的提取与转换”问题(个体逻辑)。

  • DataLoader:解决“批量化、打乱、多线程加速与内存流式供给”问题(工程效率)。

不管处理的是图像、语音还是文本,DataLoader 的批处理逻辑是通用的,只需要编写对应数据类型的 Dataset 即可。PyTorch官方也提供了一些现成的具体Dataset类实现。

可以自定义类来实现灵活的数据读取,自定义类需要继承PyTorch自身的torch.utils.data.Dataset类。一般只需要重写几个核心方法:

  • __init__:初始化,传入外部参数、配置数据集运行所需的基础资源与参数。
  • __getitem__:用于逐个读取样本集合中的元素,可以进行一定的变换,并将返回训练/验证所需的数据。比如输入一个索引 idx,返回第 idx 条处理好的数据(比如:读取第 50 张图,裁剪放缩后,转换为 Tensor 并与标签一起返回)。
  • __len__:用于返回数据集的样本数。即告诉 PyTorch 一共有多少条数据(比如数据集一共 10000 张图)。

PyTorch 官方提供的一些现成 Dataset 具体实现类,可以直接使用。比如下面的datasets.ImageFolder及大致使用:

1
2
3
4
5
import torch
from torchvision import datasets
# 分别创建训练与验证时所用到的Dataset类实例
train_data = datasets.ImageFolder(train_path, transform=data_transform)
val_data = datasets.ImageFolder(val_path, transform=data_transform)

此示例代码使用了PyTorch中自带的ImageFolder类读取按一定结构存储的图片数据。ImageFolder 会自动扫描 train_path 下的所有子文件夹名称作为类别(Class),并自动为每个类别分配一个整数标签(0, 1, 2...)。

1
2
3
4
5
6
7
# train_path/
# ├── cat/ --> 自动映射标签类别 0
# │ ├── 001.jpg
# │ └── 002.jpg
# └── dog/ --> 自动映射标签类别 1
# ├── 003.jpg
# └── 004.jpg

其中data_transform可以对图像进行一定的变换,如翻转、裁剪、缩放等操作,可自定义。

如下是一个自定义Dataset类的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import os
import pandas as pd
from torch.utils.data import Dataset
from torchvision.io import read_image

# 继承 PyTorch 的基类 Dataset 来构建自定义数据集
class MyDataset(Dataset):
def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
"""
Args:
annotations_file (string): 存储图像名称和标签信息的 CSV 文件路径。
img_dir (string): 存放所有图像文件的目录路径。
transform (callable, optional): 应用于图像特征(Input)的数据预处理/增强函数。
target_transform (callable, optional): 应用于标签(Label)的数据预处理/转换函数。
"""
# 读取 CSV 标注文件,保存为 DataFrame 格式(通常第0列为图片文件名,第1列为标签)
self.img_labels = pd.read_csv(annotations_file)
# 保存图片文件所在的根目录路径
self.img_dir = img_dir
# 保存传入的图像预处理/特征变换方法
self.transform = transform
# 保存传入的标签预处理/变换方法
self.target_transform = target_transform

def __len__(self):
# 返回数据集中的总样本数(即标注文件的行数)
return len(self.img_labels)

def __getitem__(self, idx):
"""
Args:
idx (int): 数据的索引位置
"""
# 根据索引获取第 idx 行、第 0 列的图片文件名,并拼接出该图片的完整磁盘路径
img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
# 使用 torchvision 的 read_image 函数直接从磁盘读取图片并转为 PyTorch Tensor 格式
image = read_image(img_path)
# 根据索引获取第 idx 行、第 1 列的标签值
label = self.img_labels.iloc[idx, 1]

# 如果指定了图像变换逻辑(如缩放、归一化、数据增强等),则对图像应用变换
if self.transform:
image = self.transform(image)
# 如果指定了标签变换逻辑(如 One-Hot 编码、类型转换等),则对标签应用变换
if self.target_transform:
label = self.target_transform(label)

# 返回处理好的图像 Tensor 以及对应的标签
return image, label

构建好Dataset后,就可以使用DataLoader来按批次读入数据了,实现代码如下:

1
2
3
4
from torch.utils.data import DataLoader
# 如下两行代码,分别创建训练数据、验证数据的DataLoader
train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, num_workers=4, shuffle=True, drop_last=True)
val_loader = torch.utils.data.DataLoader(val_data, batch_size=batch_size, num_workers=4, shuffle=False)

其中相关参数的作用:

  • train_data / val_data:输入的数据集实例(即继承并实现了 Dataset 的对象),告诉 DataLoader 从哪里提取数据。
  • batch_size:样本是按批次(Batch)读入的,指定每个批次包含的样本数量。
  • num_workers:指定用于数据加载的多进程 CPU 线程/进程数。数值越大,后台并行读取数据的速度越快,可有效防止 GPU 等待数据(0 表示仅用主进程)。
  • shuffle:指定是否在每个 Epoch 开始时打乱数据顺序。训练集设置为 True 可提高模型泛化能力;验证集通常设置为 False 以保持评估指标的可比性。
  • drop_last:指定当总样本数不能被 batch_size 整除时,是否丢弃最后一个样本量不足的 Batch(设置为 True 可避免最后一个小批次影响 Batch Normalization 或计算图的形状)。

可以通过如下方式查看加载的数据(又用到了matplotlib):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import matplotlib.pyplot as plt

# 以下代码的作用是:从验证集中提取“第一个批次”(对应代码:next(iter(val_loader)) )的数据,
# 并使用 Matplotlib 将该批次中的“第一张图片”(images[0])绘制展示出来。
# val_loader 是一个可迭代对象,可使用next和iter来迭代获取
images, labels = next(iter(val_loader))
print(images.shape) # 输出格式通常为: torch.Size([Batch, C, H, W])

# 1. images[0] 取出批次中的第一张图片
# 2. .detach() 切断可能的梯度追踪
# 3. .cpu() 确保张量在 CPU 内存中(如果是 GPU 张量直接 .numpy() 会报错)
# 4. .permute(1, 2, 0) 将 [C, H, W] 调整为 Matplotlib 要求的 [H, W, C]
# 5. .numpy() 显式转换为 NumPy 数组
img = images[0].detach().cpu().permute(1, 2, 0).numpy()

plt.imshow(img)
plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 模拟从Dataloader中按批次获取所有数据并处理
import matplotlib.pyplot as plt

# 遍历每一个 Batch
for images, labels in val_loader:
print(images.shape) # 输出当前 Batch 的形状,如 torch.Size([32, 3, 224, 224])

# 遍历当前 Batch 中的每一张图片
for i in range(images.shape[0]):
# numpy() 或 permute 转换维度:(C, H, W) -> (H, W, C)
img = images[i].permute(1, 2, 0).numpy()
plt.imshow(img)
plt.title(f"Label: {labels[i].item()}")
plt.show()

3.4 模型构建

本节学习内容:PyTorch中神经网络的构造方法、PyTorch中特殊层的构建、LeNet的PyTorch实现。


人工智能的第三次浪潮主要受益于深度学习的突破,其成功得益于海量数据、GPU 算力飞跃以及深度神经网络算法(如结合了 BP 算法和 GPU 加速的卷积神经网络 AlexNet)的融合应用。

PyTorch中,自定义模型一般是基于继承nn.Module类来完成的,我们只需要重写其中一些方法(如__init__、forward)即可。这给灵活构造自定义模型提供方便捷。

3.4.1 神经网络的构造

PyTorch 中,torch.nn.Module是一个基本构造类,所有神经网络层(如 nn.Linearnn.Conv2d)以及自定义的大模型,本质上都是它的子类。

以下示例通过继承 Module 类构造多层感知机。这里定义的 MLP 类重载了 Module 类的 __init__ 函数和 forward 函数。它们分别用于创建模型参数和定义前向计算(正向传播)。下面的 MLP 类定义了一个具有两个隐藏层的多层感知机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
from torch import nn

class MLP(nn.Module):
# 声明带有模型参数的层,这里声明了两个全连接层
def __init__(self, **kwargs):
# 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
super(MLP, self).__init__(**kwargs)
self.hidden = nn.Linear(784, 256)
self.act = nn.ReLU()
self.output = nn.Linear(256,10)

# 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
def forward(self, x):
o = self.act(self.hidden(x))
return self.output(o)

上述执行过程大致如下:

  • 这个MLP的__init__方法中定义了两个线性函数,一个激活函数。然后在forward方法中使用了它们
  • self.hidden(x):输入特征数是 784(例如将 \(28 \times 28\) 的图像展平后的向量),通过矩阵乘法 \(xW^T + b\) 转换后,输出 256 个特征数。
  • self.act(...):ReLU 是一个激活函数,计算公式为 \(\max(0, x)\)。它不会改变张量的形状或特征数量,只是把这 256 个特征值中小于 0 的部分变成 0,大于 0 的保持不变,因此最终输出是 256 个激活后的特征值。
  • self.output(o):将这 256 个特征值传递到输出层,最终映射转换为 10 个特征值(比如对应 10 个类别的预测 logits)。

以上的 MLP 类中⽆须定义反向传播函数。系统将通过⾃动求梯度⽽自动⽣成反向传播所需的 backward 函数。

nn.Module类中包含__call__属性且是一个可执行方法,按照Python中的 __call__ 协议,nn.Module类实例就可以当成一个方法来调用,且调用时实际上执行的就是__call__属性对应的方法。以下代码首先将MLP实例化为net,通过调用net(x)最终将导致调用MLP类中重写的forward方法(而这也反过来要求我们在继承nn.Module类自定义子类时,一定要重写实现forward方法)。

1
2
3
4
X = torch.rand(2,784) # 设置一个随机的输入张量
net = MLP() # 实例化模型
print(net) # 打印模型
net(X) # 前向计算
1
2
3
4
5
6
7
8
9
10
# 输出如下
MLP(
(hidden): Linear(in_features=784, out_features=256, bias=True)
(act): ReLU()
(output): Linear(in_features=256, out_features=10, bias=True)
)
tensor([[ 0.0057, 0.0504, -0.1897, 0.2306, 0.0876, 0.2021, 0.1165, 0.1699,
-0.0005, -0.1344],
[-0.0716, -0.0220, -0.1374, 0.1645, 0.1267, 0.0325, 0.1621, 0.2814,
-0.1388, -0.2205]], grad_fn=<AddmmBackward0>)

注意,此示例中nn.Module的命名特征,翻译过来叫“模块”,而不是叫Layer层或Model模型,因为该类被定义成一个可供⾃由组建的部件。Module 里可以包含 Module,任意多个 Module 可以组合成更大、更复杂的 Module。

继承nn.Module的子类即可以是某某层(如池化层、线性化层等),也可以是一个模型(如上述MLP),甚至一个模型的一部分(在中大型复杂模型如 ResNet、Transformer、YOLO 等中,“模型的一个部分”被经常单独封装为一个 nn.Module )。

3.4.2 神经网络中常见的层

深度学习中提供了或可实现神经网络中各式各样的层,如全连接层、卷积层、池化层与循环层等等。虽然PyTorch中已经预置了大量的层,但我们在实际开发中仍可能需要自定义层,此时就是一个继承 nn.Module类实现子类的过程。

不含模型参数的层

先介绍如何定义一个不含模型参数的自定义层。以下示例中定义了一个层:将输入减掉均值后输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import torch
from torch import nn

class MyLayer(nn.Module):
def __init__(self, **kwargs):
super(MyLayer, self).__init__(**kwargs)
"""
只有当计算逻辑涉及固定常数、预设权重或带有参数的子层时,才会在 __init__ 中提前声明,然后在 forward 中使用。
数据的实际计算(如切片、加减乘除、均值计算、矩阵乘法等)必须写在 forward 中,因为这些操作需要针对每次传入的具体张量 x 实时执行。
"""

def forward(self, x):
return x - x.mean()

layer = MyLayer()
out = layer(torch.tensor([1, 2, 3, 4, 5], dtype=torch.float))
print(out)

# 输出如下:
tensor([-2., -1., 0., 1., 2.])

__init__ 只负责“搭建舞台”(初始化网络结构与权重),forward 才是真正的“演出过程”(数据流转与计算)。

含模型参数的层

还可以自定义包含模型参数的层。其中的模型参数可以通过多轮训练反向传播更新学习。

torch.nn.Parameter类是 torch.Tensor 的子类。当我们在自定义层或模型时,如果某个张量是实际上是torch.nn.Parameter实例,那么它将被自动添加到模型的参数列表中,以便于后续跟踪、求导。所以在⾃定义含模型参数的层时,应该将这些模型参数定义成 Parameter类型。除了直接定义成 Parameter 类外,还可以使⽤ ParameterListParameterDict 分别定义参数的列表和字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyListDense(nn.Module):
def __init__(self):
super(MyListDense, self).__init__()
self.params = nn.ParameterList([nn.Parameter(torch.randn(4, 4)) for i in range(3)])
self.params.append(nn.Parameter(torch.randn(4, 1)))

def forward(self, x):
for i in range(len(self.params)):
x = torch.mm(x, self.params[i])
return x
net = MyListDense()
print(net)

# 输出如下:
MyListDense(
(params): ParameterList(
(0): Parameter containing: [torch.float32 of size 4x4]
(1): Parameter containing: [torch.float32 of size 4x4]
(2): Parameter containing: [torch.float32 of size 4x4]
(3): Parameter containing: [torch.float32 of size 4x1]
)
)
tensor([[512.]], grad_fn=<MmBackward0>)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyDictDense(nn.Module):
def __init__(self):
super(MyDictDense, self).__init__()
self.params = nn.ParameterDict({
'linear1': nn.Parameter(torch.randn(4, 4)),
'linear2': nn.Parameter(torch.randn(4, 1))
})
self.params.update({'linear3': nn.Parameter(torch.randn(4, 2))}) # 新增

def forward(self, x, choice='linear1'):
return torch.mm(x, self.params[choice])

net = MyDictDense()
print(net)

# 输出如下:
MyDictDense(
(params): ParameterDict(
(linear1): Parameter containing: [torch.FloatTensor of size 4x4]
(linear2): Parameter containing: [torch.FloatTensor of size 4x1]
(linear3): Parameter containing: [torch.FloatTensor of size 4x2]
)
)

二维卷积层

虽然在深度学习中我们习惯叫“卷积”运算,但严格从数学定义上讲,神经网络里的卷积层实际上执行的是“互相关运算(Cross-correlation)”

  • 互相关运算:用一个较小的矩阵(卷积核 \(K\))在较大的输入矩阵(输入 \(X\))上从左到右、从上到下按步长滑动。每移动到一个位置,就将卷积核与输入对应的子区域按元素相乘再求和,得到输出矩阵 \(Y\) 中的一个点。
  • 标量偏差(Bias \(b\):一个可学习的标量,加到互相关运算结果的每一个元素上。
  • 参数更新:卷积核权重 \(W\) 和偏置 \(b\) 就是模型要学习的“参数”。它们初始时是随机给出的,在训练过程中通过反向传播(Backpropagation)和梯度下降逐步优化。

卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常先对卷积核随机初始化,然后不断迭代更新卷积核和偏差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch
from torch import nn

# 卷积运算(二维互相关)
def corr2d(X, K):
h, w = K.shape
X, K = X.float(), K.float()
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i: i + h, j: j + w] * K).sum()
return Y

# 二维卷积层
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super(Conv2D, self).__init__()
self.weight = nn.Parameter(torch.randn(kernel_size)) # 卷积核
self.bias = nn.Parameter(torch.randn(1)) # 偏差

def forward(self, x):
return corr2d(x, self.weight) + self.bias

卷积窗口形状为\(p×q\)的卷积层称为\(p×q\)卷积层。同样,\(p×q\)卷积或\(p×q\)卷积核说明卷积核的高和宽分别为p和q。一般情况下,p与q相等。

为了防止图像尺寸过快收缩(如果不加 Padding,每经过一层 \(3 \times 3\) 卷积,图像高宽就会减小 \(2\)。经过十几层卷积后,图像就会缩小为 \(1 \times 1\),无法构建很深的网络结构。)、保护边缘信息(位于输入图像边缘和角落的像素点,如果不加 Padding,被卷积核扫描到的次数远远少于中心区域的像素;加了 Padding 之后,边缘像素也能被充分提取特征),我们在实际应用中会对输入数据上下左右加上填充(padding)。

填充的元素值一般都是 0,PyTorch 中 nn.Conv2dpadding 参数默认采用的就是 零填充。加上填充后,我们可以使输入数据与输出数据有相同高度、宽度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch
from torch import nn

# 定义一个函数来计算卷积层。它对输入和输出做相应的升维和降维
def comp_conv2d(conv2d, X):
# (1, 1)代表批量大小和通道数
X = X.view((1, 1) + X.shape)
print(f"升维后,X的形状:{X.shape}")
Y = conv2d(X)
return Y.view(Y.shape[2:]) # 排除不关心的前两维:批量和通道


# 注意这里是两侧分别填充1⾏或列,所以在两侧一共填充2⾏或列
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3,padding=1)

X = torch.rand(8, 8)
out = comp_conv2d(conv2d, X)
print(f"卷积结果out的形状 = {out.shape}")

# 输出如下:
升维后,X的形状:torch.Size([1, 1, 8, 8])
卷积结果out的形状 = torch.Size([8, 8])

还可以为卷积核窗口设置不同的高与宽。如果卷积核的高和宽不同时,可以通过在高和宽上设置不同的填充数,最后使得输出和输入具有相同的高和宽。

1
2
3
4
5
6
7
8
9
# 假设输入数据形状还是 (1,1,8,8)
# 使用高为5、宽为3的卷积核。在⾼和宽两侧的填充数分别为2和1
# 这样经过卷积运算后,输出结果的形状也是(1,1,8,8)
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

# 输出如下:
升维后,X的形状:torch.Size([1, 1, 8, 8])
torch.Size([8, 8])

填充可以增加输出的高和宽。这常用来使输出与输入具有相同的高和宽。

做卷积过程中,卷积窗口在输入数据上按照从上到下、从左到右的规则滑动,每次滑动的行数或列数称为步幅(stride),行步幅与列步幅可以不同。

1
2
3
4
5
6
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape

# 输出如下:
升维后,X的形状:torch.Size([1, 1, 8, 8])
torch.Size([2, 2])

步幅可以减小输出的高和宽,例如输出的高和宽变为指定的大小。

池化层

3.4.3 模型示例

3.5 模型初始化

3.6 损失函数

3.7 训练与评估

3.8 可视化

3.9 PyTorch优化器


PyTorch03-PyTorch的主要组成模块
https://jiangsanyin.github.io/2026/08/06/PyTorch03-PyTorch的主要组成模块/
作者
sanyinjiang
发布于
2026年8月6日
许可协议