PyTorch 深度学习框架快速入门 (小土堆)
PyTorch 深度学习框架快速入门
-
深度学习框架
-
常用模块
-
- 数据集存取
-
- 图片数据处理库 —— PIL
- OS 模块
- 实例
-
使用TensorBoard工具记录机器学习相关数据
-
执行图像转换操作的Transform模块
-
完整的数据集下载流程
-
用于批量加载数据的数据加载器(DataLoader)
-
创建自定义神经网络架构的Module类
-
在前向传播过程中处理输入信号并提取特征:
-
卷积层通过卷积核对输入信号进行特征提取
-
卷积操作在简单的应用中用于特征提取过程
- 最大池化
- 非线性层
- 线性层
-
-
基本整合
- 基于现有网络模型进行修改
- 模型的存储与加载过程
- 完整的训练流程
- 利用GPU设备对模型进行训练
- 进行模型验证过程
- Note
- Python语言在Windows系统下文件路径表示的三种方法
本文主要参考并汇编自B站知识区UP 我是土堆 发布的PyTorch视频教程以及PyTorch官方文档
关于环境配置教程的教程读者自己完成。
文章中不涉及环境配置相关内容。
本文不含深度学习理论知识,关于理论知识的入门教程推荐:
- 深度学习入门 (鱼书)

- 苹果书 (对应李宏毅老师的深度学习课程)

- 吴恩达老师开设的机器学习与深度学习公开课程
- 李飞飞老师的cs231n课程深入探讨了计算机视觉技术
- 邱锡鹏教授的深度学习系统性教学内容

深度学习框架
PyTorch起源:
- 在lua这门小众语言催生下,在2002年的时候 torch诞生。
- torch在这一年的正式版是version7.
- 到了2016年 facebook基于version7推出了pytorch系统。值得注意的是 pytorch系列早期版本(编号为v_5到v_5.x)存在兼容性问题无法直接运行后续版本的程序需对代码稍加修改方能正常使用。
- 当然这一项技术突破也正因如此 pytorch随后于经过完善并在version1.0的基础上与caffe系统实现了联合发布
Tensorflow起源:
- Theano是由加拿大人开发的一个框架,但现已退出历史舞台.在Theano的基础上,Google推出了TensorFlow,并收购了Keras.
- Keras是一个功能强大的高级API规范,本身无需进行具体实现,可以通过底层计算库完成.例如,TensorFlow框架中的tf.keras模块即可调用Keras的部分功能.
- Keras官方实现也可通过TensorFlow的后端以及MXNet的后端快速搭建神经网络.然而作为功能强大的高级接口,其效率存在较大问题.
其他:
- Facebook的caffe主要组件是caffe1和caffe2,并推出pytorch作为主推产品。
- mxnet及caffee均是由华人团队开发,并已成为亚马逊官方框架。
- 微软阵营采用cntk作为技术基础且较为小众。
- chainer是由日本公司研发的技术框架而pytorch借鉴了其API规范.
区别:
PyTorch和TensorFlow是当前主流的主要框架体系结构。
其中PyTorch采用的是基于节点链接(dynamic computation graph)的设计理念而TensorFlow则基于静态计算图(static computation graph)。
相比于静态计算架构PyTorch能够提供更加灵活便捷的操作方式它允许开发者在运行时就能够根据具体情况调整运算流程这与人们的思维习惯更为契合。
而在使用TensorFlow进行开发时由于其遵循固定的运算顺序因此必须提前规划好整个运算流程并在运行阶段才能逐步完成各节点之间的数据传递。
这种设计模式使得PyTorch在处理复杂且多变的任务场景中表现更为出色而TensorFlow则更适合那些需要预先确定好整个运算路径的应用场景。
常用模块
数据集存取
本部分涉及两个关键组件:
- dataset 表示 数据集,在 数据集中 会 包含 数据编号、 数据内容以及 数据标签。
- dataloader 则是 负责加载 数据集 的 工具 , 其中 数据 不一定是 逐个输入 的 , 可以 以 批次 输入 来 处理。
数据集有两种最简单的保存方式:
- 第一种:

通过不同的文件夹将数据的标签和数据本身分开储存。
- 第二种:

文件夹的名称即为 label,文件夹内是数据和编号。
图片数据处理库 —— PIL
PIL 全称是 Python Imaging Library(PIL),旨在为各种图像文件格式提供读取、操作和保存的功能。该库集成了包括裁剪、旋转、色彩调节和滤镜操作在内的多项图像处理功能。
需要注意的是,在当前版本中已经不再使用PIL库了。原来的分支项目——Pillow已经发展成熟并持续维护其开发工作。因此我们建议直接获取最新的版本——Pillow。具体的获取方法可以通过以下代码片段来实现:
pip install pillow
尽管在配置时选择了 pillow 库,在实际应用中仍然可以利用下面这段代码以 PIL 作为数据源,并导入其他相关模块;值得注意的是,在功能上 Pillow 和 PIL 是完全一致的。
from PIL import Image
该代码的作用是从库中导入模块,并将其中的关键组件命名为Image。该模块为库中最重要的组件,在图像处理方面具有核心功能。
下面是一个简单的例子,展示如何使用 PIL 打开和显示一张图像:
from PIL import Image
# 打开图像
img = Image.open("test.png")
img2 = Image.open(r"C:\Users\zdh\Downloads\test.png")
# 显示图像
img.show()
img2.show()
在这个例子中,在调用 Image.open("test.png") 时会打开名为 test.png 的图像文件,在调用 img.show() 后会呈现这张图像。
OS 模块
该代码指令用于在Python中引入os模块。
该模块提供了大量且多样的功能来操作文件及目录等操作系统相关的任务。
Python的标准库模块中包含着名为os的部分。该模块提供了多种与操作系统相关联的操作功能,并主要涉及文件处理、目录管理以及环境变量的配置等任务。例如以下是一些常用的函数或操作:
- 文件和目录操作:
os.getcwd():获取当前工作目录。 # get current work directory
os.chdir(path):改变当前工作目录。 # change directory
os.listdir(path):列出指定目录中的文件和子目录。 # list directory
os.mkdir(path):创建目录。 # make directory
os.makedirs(path):递归创建目录。 # make directories
os.remove(path):删除文件。 # remove (file)
os.rmdir(path):删除目录。 # remove directory
os.rename(src, dst):重命名文件或目录。 # rename (file)
- 路径操作:
os.path.basename(path):获取路径中的文件名部分。
os.path.dirname(path):获取路径中的目录部分。
os.path.join(path, *paths):将多个路径组合后返回。
os.path.split(path):将路径分割为目录和文件名。
os.path.exists(path):判断路径是否存在。
os.path.isabs(path):判断是否为绝对路径。
os.path.isfile(path):判断是否为文件。
os.path.isdir(path):判断是否为目录。
- 环境变量:
os.getenv(key):获取环境变量的值。
os.putenv(key, value):设置环境变量的值。
os.environ:获取所有环境变量的字典。
- 进程管理:
os.system(command):运行系统命令。
os.popen(command):执行命令并获取输出。
os.getpid():获取当前进程 ID。
os.getppid():获取父进程 ID。
下面是一些使用 os 模块的示例代码:
import os
# 获取当前工作目录
current_directory = os.getcwd()
print("当前工作目录:", current_directory)
# 列出当前目录中的文件和子目录
files = os.listdir(current_directory)
print("当前目录中的文件和子目录:", files)
# 创建一个新目录
new_directory = "new_dir"
os.mkdir(new_directory)
print(f"创建目录: {new_directory}")
# 重命名目录
new_directory_renamed = "new_dir_renamed"
os.rename(new_directory, new_directory_renamed)
print(f"重命名目录: {new_directory_renamed}")
# 删除目录
os.rmdir(new_directory_renamed)
print(f"删除目录: {new_directory_renamed}")
# 获取环境变量
home_directory = os.getenv("HOME")
print("主目录:", home_directory)
实例
- 情况一: 文件夹的名称即为 label,文件夹内是数据和编号。
from torch.utils.data import Dataset
from PIL import Image
import os
class MyDataset(Dataset):
def __init__(self, root_dir, label_dir):
self.root_dir = root_dir
self.label_dir = label_dir
# 根据当前操作系统的路径分隔符规则,将多个字符串组合成一个完整的路径。
# 在 Windows 系统中,路径分隔符通常是 \,而在 Unix/Linux 系统中,路径分隔符通常是 /。
# 使用 os.path.join() 函数可以使你的代码在不同操作系统上都能正确地拼接路径,避免了硬编码路径分隔符带来的跨平台问题。
self.path = os.path.join(self.root_dir, self.label_dir)
# 返回目录下的文件名列表,便于 getitem 函数调用
self.img_path = os.listdir(self.path)
def __getitem__(self, idx): # []运算符重载,使 MyDataset[idx] 返回对应序号 idx 下的 img 和 label
img_name = self.img_path[idx]
img_item_path = os.path.join(self.path, img_name)
img = Image.open(img_item_path) # 使 img 包含该图像的所有数据,如 长宽、图片显示数据
label = self.label_dir # 图片所在目录名即图片对应Label
return img, label
def __len__(self):
return len(self.img_path) # 返回数据集长度
root_dir = 'dataset/train'
ants_label_dir = 'ants'
bees_label_dir = 'bees'
ants_dataset = MyDataset(root_dir, ants_label_dir)
bees_dataset = MyDataset(root_dir, bees_label_dir)
# Dataset 类中对+运算符进行了重载,因此,在这里使用 + 相当于是将两个数据集进行拼接
train_dataset = ants_dataset + bees_dataset
print(len(train_dataset))
print(len(ants_dataset))
print(len(bees_dataset))
ant_img, ant_label = ants_dataset[0]
bee_img, bee_label = bees_dataset[0]
ant_img.show()
bee_img.show()
- 情况二: 通过不同的文件夹将数据的标签和数据本身分开储存。

from torch.utils.data import Dataset
from PIL import Image
import os
class MyDataset(Dataset):
def __init__(self, root_dir, img_dir, label_dir):
self.root_dir = root_dir
self.img_dir = img_dir
self.label_dir = label_dir
self.img_path = os.path.join(self.root_dir, self.img_dir)
self.label_path = os.path.join(self.root_dir, self.label_dir)
# 图片名列表
self.img_list = os.listdir(self.img_path)
# 图片标签txt文件名列表
self.label_list = os.listdir(self.label_path)
def __getitem__(self, idx):
img_item_path = os.path.join(self.img_path, self.img_list[idx])
# with类似Java JDK8之后推出的try(...) catch语法糖,作用大体都是可以在代码执行最后自动调用资源对象的close方法
with open(os.path.join(self.root_dir, self.label_dir, self.label_list[idx]), 'r') as f:
# 读取图片对应标签txt文件中保存的label名
label = f.readlines()
img = Image.open(img_item_path)
return img, label
root_dir = "test/train"
img_dir = "ants_image"
label_dir = "ants_label"
ants_dataset = MyDataset(root_dir, img_dir, label_dir)
ant_img, ant_label = ants_dataset[0]
ant_img.show()
小Tips:
# 如果希望显示两幅图像,可以采用 matplotlib 库
import matplotlib.pyplot as plt
fig1, ax1 = plt.subplots(figsize=(5, 5))
ax1.imshow(bee_img)
ax1.set_title("Bee")
ax1.axis("off")
# Create the second figure for the ant image
fig2, ax2 = plt.subplots(figsize=(5, 5))
ax2.imshow(ant_img)
ax2.set_title("Ant")
ax2.axis("off")
# Show both figures
plt.show()
Tensorboard 记录机器学习的过程
TensorBoard 属于 TensorFlow 的一个高级可视化工具包,并专为开发者设计。它为用户提供了一个直观的界面来辅助理解、排查错误以及提升性能。该工具集成了多种功能模块,并非仅限于简单的数据展示
- 损失函数与评估指标的行为特征及其演变模式。
- 模型构建中的计算架构及其拓扑组织形式。
- 训练过程中各参数的状态分布及其对应的频率直方图分析。
- 多模态数据(图像、音频及文本)的表现形式及其实时呈现能力。
常用的简单的方法介绍:
add_scalar(tag, scalar_value, global_step=None, walltime=None)
- tag(必填) : 一个字符串类型的唯一标识符。它将作为标签显示在TensorBoard界面中,并用于区分不同的标量数据序列。
- scalar_value(必填) :要记录的标量数据的具体数值。该值可以是Python内置的数据类型(如int或float),也可以是PyTorch中的标量张量(torch.Tensor且张量元素个数为1)。例如,在深度学习模型训练过程中,该值可能是损失函数值或准确率等指标。
- global_step(可选) :一个整数类型变量,默认为None。在模型训练过程中会随着时间逐步增加以区分不同迭代阶段的数据。
- walltime(可选) :一个浮点数类型变量,默认为None。如果未提供,则系统会自动记录当前时间戳。
1.代码中进行打点记录
from torch.utils.tensorboard import SummaryWriter
# SummaryWriter 主要用于将训练过程中的各种信息记录下来,以便后续使用 TensorBoard 进行可视化
# 'logs' 是一个字符串参数,指定了存储日志文件的目录路径。
# 当你创建了 SummaryWriter 对象后,在后续的代码中,可以使用该对象将训练过程中的标量、图像、直方图等信息写入到这个目录下的日志文件中。
writer = SummaryWriter('logs')
for i in range(100):
writer.add_scalar("y = x", i, i) # add_scalar 表示增加标量 第一个 i 表示 y,第二个 i 表示 x
writer.close()
2. 启动TensorBoard显示打点记录
在终端使用tensorboard --logdir=logs --port=6007 命令打开日志文件,端口可以设置为其他的。
add_image(tag, img_tensor, global_step=None, walltime=None, dataformats='CHW')
- tag(必需) :一个标识符字符串(Tag),它是唯一标识图像数据的关键信息,并在TensorBoard界面中显示以区分不同的图像数据集。
- img_tensor(必需) :表示图像的空间信息张量(Image Tensor),其形状取决于张量参数的数据格式设置。
- global_step(可选) :整数型全局计数器变量(Global Step),通常用于记录训练迭代次数,在时间序列排序中对图像进行排列,默认值为None。
- walltime(可选) :浮点型时间戳变量(Wall Time),记录数据采集的时间,默认值为None;若未提供,则系统自动生成当前时间戳。
- dataformats(可选) :字符串型参数设置选项(Data Formats),指定张量的空间维度顺序,默认值为'CHW'表示(C, H, W)通道-高度-宽度排列;也可设置为'HWC'表示(H, W, C)排列顺序。
# 除了通过记录点绘制上述的图像外,还可用于图像的显示。
import numpy as np # 导入 numpy 库是用于图像数据的转换。
img_path = "test/train/bees_image/16838648_415acd9e3f.jpg" # 此处使用的是相对地址
img = Image.open(img_path)
img_array = np.array(img) # 将图像数据转换为数组进行储存
# 第一个参数是日志文件中图像区间的 名字,第二个参数是 图像数据(必须是 array型、tensor型等),第三个参数是 步长
# 每个步长存储相应步长下的数据,最后一个参数是设置图像数据的格式,H → 高,W → 宽,C → 通道
# 当使用相同名字的图来记录数据时,步长是有用的,当使用不同名字时,可以不设置步长
writer.add_image("test", img_array, 0, dataformats = "HWC")
Tips:
在计算机领域中,图像通常采用 HWC 和 CHW 等多种格式进行存储。其中 H 代表图像的高度(Height),W 表示宽度(Width),C 则指通道数(Channel)。当采用 RGB 彩色格式时,则有三个通道分别对应红色(Red)、绿色(Green)和蓝色(Blue);而若为灰度图像,则仅有一个通道用于表示亮度信息。
通常情况下,“分辨率”一词指的是图片宽度与高度的乘积(即 W×H)。例如一张分辨率为 512×300 的图片其实际宽度为 512 个像素点、高度为 300 个像素点。
所有像素点的数量可以用 H 和 W 来表示,并且每个像素点都包含三个通道值的信息。只有当这三个通道值都被确定后才能计算出 R、G、B 的具体强度数值。
对于灰度图像而言,“其像素值数值大小则代表该像素点亮度的程度”,即数值越大则表示越亮。
Transform 进行图像变换
Transform 是一种主要处理图像相关操作的工具,在实际应用中需先将输入的图像转换为张量形式,并能执行裁剪和标准化等基本操作
Transform是属于torchvision这个库;它是专注于计算机视觉领域的库;所谓的是让计算机能够读取图像和视频数据并进行相关处理的技术;包括但不限于图像识别、图像分类等技术。
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
img_path = "test/train/bees_image/16838648_415acd9e3f.jpg"
img = Image.open(img_path)
# 0. ToTensor -- PIL转换为张量类型
trans_tensor = transforms.ToTensor()
# tensor 类型的数据和 PIL image的区别在于 前者是 (C, H, W) 的形式储存的,后者则是 (H, W, C) 的格式储存的,
# 前者像素值是在 0.0 ~ 1.0 之间,后者则是在 0 ~ 255 之间。转换成 tensor 类型的数据更适合用于图像处理的操作。
img_tensor = trans_tensor(img) # 能够这样使用的原因是 ToTensor 这个类已经重载了 __call__ 方法。
writer = SummaryWriter("logs")
writer.add_image("Tensor", img_tensor, 0) # 0 表示从第 0 个步长开始记录数据
# 1. Normalization -- 归一化
tensor_norm = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
# 用于对张量进行归一化的操作,第一个参数表示标准化的均值列表,表示 RGB 三个通道的均值。每个通道上的值会减去均值。
# 第二个参数是标准化的标准差列表,表示 RGB 三个通道的标准差。每个通道上的值会除以对应通道的标准差。
img_norm = tensor_norm(img_tensor) # 能够这样使用的原因是 Normalize 这个类已经改写了 __call__ 方法。
writer.add_image("Normalization", img_norm, 0)
# 2. Resize -- 转换图片大小
trans_resize = transforms.Resize((512, 512)) # 裁剪图像
img_resize = trans_resize(img_tensor)
writer.add_image("Resize", img_resize)
# 3. Compose -- 类似过滤器链模式,可以组合多个Transforms
trans_resize_2 = transforms.Resize((512, 512)) # 将裁剪图像和将图像转换成张量的步骤合并了
trans_comp = transforms.Compose([trans_resize_2, trans_tensor])
# 在最新版的 PyTorch 中,transforms.Resize 进行了优化,可以根据传入的输入类型自动进行适当的处理。
# 它可以直接处理 PIL 图像和 Tensor 图像,无需手动在 ToPILImage 和 ToTensor 之间进行转换。
img_comp = trans_comp(img)
writer.add_image("Resize", img_comp, 1)
# 4. RandomCrop -- 随机裁剪
trans_crop = transforms.RandomCrop((100,100), 50)
# RandomCrop 类可以随机从图像数据中截取一个 100*100 的数据,50 表示的是 padding,即图像周围的填充,默认填充 0(黑色)
for i in range(10):
img_crop = trans_crop(img_tensor)
writer.add_image("RandomCrop", img_crop, i)
writer.close()
数据集的下载

如何从官网寻找数据集下载说明。
torchvision.datasets.CIFAR10 作为 PyTorch 中方便获取 CIFAR-10 数据集的工具,在设计上提供了丰富的配置选项。用户可以根据需求设置数据存储路径,并选择性地加载训练样本或测试样本;此外还支持预定义的数据预处理操作以及选择是否从官方资源库下载数据集合。
核心参数解释:
-
root 必填字段:字符串类型,请指定数据集合的存储位置。若数据文件夹不在指定位置,则可以根据 download 参数选择是否从该位置下载文件夹。例如:如 root = ./data,则将被存储在当前文件夹下的 data 文件夹中。
-
train 可选参数,默认设置为 True:布尔值,请指定是要加载训练集合还是测试集合。
- 当启用时(True),则会调用用于训练的 CIFAR-10 数据集,并拥有 5万张图片。
- 当关闭时(False),则会调用用于测试的 CIFAR-10 数据集合,并拥有 1万张图片。
-
transform(可选,默认设為 None) :可調用模块(通常來自 torchvision.transforms 或者自訂定義的函數集合),用於對數據庫中的圖片進行轉換操作。
-
比如說،我們可以用 transform=torchvision.transforms.ToTensor() 將圖片經過 PyTorch 的 Tensor 開 type 轉換,在此過程中將 PIL Image 的 (H, W, C) 形態轉為 (C, H, W) 形態並將像素值範圍從 [0, 255] 轉換至 [0, 1]。
-
target\_transform(可选,默认为 None) :可调用类型,用于将数据集中的标签转换为所需类型或形式。
-
比如说,默认情况下该参数设为None表示无需执行任何转换操作。
-
应用以下代码段将标签转换为PyTorch的Tensor类型:
target\_transform = \lambda x: \text{torch.tensor}(x)
- download(可选项,默认设置为False值):布尔类型变量,用以决定是否从网络下载数据集。
- 如果 download 标志变量被设置为 True 并且目标数据集未位于 root 目录下,则系统会自动下载该数据集。
- 当 download 标志变量被设置为 False 且目标数据集未位于指定目录下时,则会导致异常发生。
PyCharm编码下载CIFAR10训练集和测试集:
CIFAR-10 是一个广泛应用的图像分类数据集,并且它包含了 6 \times 10^{4} 张 32 \times 32 像素的彩色图像。
这些图像被划分为 10 个不同的类别,并且每个类别都提供了 6 \times 10^{3} 张图片。
这一系列具体包括:飞机类、汽车类、鸟类、猫类、鹿类、狗类、蛙类、马类、船类和卡车类。
import torchvision
from torch.utils.tensorboard import SummaryWriter
# 定义图像转换操作,将图像转换为 Tensor 类型
dataset_transforms = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])
# 加载 CIFAR-10 训练集
dataset_train = torchvision.datasets.CIFAR10(root="./Datasets", train=True, transform=dataset_transforms, download=True)
# 加载 CIFAR-10 测试集
dataset_test = torchvision.datasets.CIFAR10(root="./Datasets", train=False, transform=dataset_transforms, download=True)
# 创建一个 SummaryWriter 对象,将日志存储在 "dataset" 目录下
writer = SummaryWriter("dataset")
# 从测试集中选取前 10 张图像添加到 TensorBoard 中
for i in range(10):
# 返回值包括图像和标签,图像是 torch.Tensor 类型,标签是整数类型。
# 对于 CIFAR-10 数据集,标签的范围是 0 到 9,分别对应 10 个类别:
# 0: airplane
# 1: automobile
# 2: bird
# 3: cat
# 4: deer
# 5: dog
# 6: frog
# 7: horse
# 8: ship
# 9: truck
img, target = dataset_test[i]
writer.add_image("dataset", img, i)
# 关闭 SummaryWriter
writer.close()
DataLoader
DataLoader属于PyTorch库中的一个类。它主要功能包括将数据集打包成可迭代的数据加载器,并支持多线程的数据集遍历。该类支持批次读取样本进行预处理,并随机打乱顺序以提高训练效率。此外,在深度学习任务中扮演着关键角色。
DataLoader具有以下优势:
批数据加载:便于将数据以批次的形式传递给模型,并符合深度学习中基于批次的训练方式。该方法能够充分利用硬件资源的多线程特性,并显著提升了训练效率
-
数据将被随机排列:
-
当 shuffle=True 时,在每个 epoch 开始前对数据进行随机排列,
-
从而使模型不会过度依赖特定的数据顺序,
-
并增强其泛化能力。
-
多进程加载:
-
通过 num_workers 参数设置多进程数据加载。
-
显著提升数据加载速度。
-
尤其是针对大规模数据集及其复杂的预处理任务。
DataLoader参数详解:
- dataset(必要项):
- 表示要加载的数据集对象,请确保其为 torch.utils.data.Dataset 类或其子类(例如 torchvision.datasets.MNIST),或者支持自定义的其他数据集类型。
$batch_size$(常用)定义为变量名 batch_size 的值。
每个批次的数据量所包含的样本数量决定了每次迭代中所取样的样本数量,在训练深度学习模型时显著地影响模型训练的速度与稳定性。
-
shuffle(常用):
-
该参数决定了每个 epoch 开始前是否对数据进行重排。在训练阶段,默认设为 True 可以提高数据加载过程中的随机性,并使模型具有更强的泛化能力;而在评估或验证过程中,则通常设为 False 以避免引入额外的数据抖动带来的潜在偏差。
-
num_workers:
-
表示用于数据加载的子进程数量。
-
当设为0时,在单进程中完成数据加载。
-
若大于零,则由多进程共同负责数据加载。
-
这有助于提升整体性能。
-
但会带来额外的系统负载压力。
-
drop_last:
-
当数据集中的样本总数无法被batch_size整除时(即余数存在),是否舍弃最后一个包含有余数的不完整批次?若设置参数为True,则会舍弃该不完整批次;若设置参数为False,则会保留该批次。
- pin_memory:
- 若为 True,会将加载的数据 Tensor 存储在固定内存(pinned memory)中,加快数据传输到 GPU 的速度,对于 GPU 训练非常有用。
- pin_memory:
代码的简单使用演示:
import torchvision
from torch.utils.tensorboard import SummaryWriter
from torch.utils.data import DataLoader
test_set = torchvision.datasets.CIFAR10("./Datasets", train=False, transform=torchvision.transforms.ToTensor(), download=False)
test_load = DataLoader(dataset=test_set, batch_size=64, shuffle=True, num_workers=0, drop_last=False)
# 这里测试集的载入规则是每个批次载入 64 个图片,shuffle 为 True 表示每次重新使用这个测试集时都打乱顺序,
# num_workers 指定用于数据加载的工作线程数,Windows 中通常设为 0。drop_last 表示如果最后批次的图像数据不能凑齐 64 个,就舍弃
writer = SummaryWriter("dataloader")
for epoch in range(2): # 进行两个周期(epoch) 的训练 -- 每个epoch会分批次看完一轮所有训练集的数据
step = 0 # step 表示的是每个周期中训练的轮数,与 batch_size 的值有关。
for i in test_load:
imgs, targets = i # 参数的分配参照的是 CIFAR10 中的 __getitem__ 方法。
writer.add_images(f"Epoch:{epoch}", imgs, step) # 注意,这里是 add_images,适用于每个批次不止一张图片的情况
# 如果每个 step 只有一张图片显示,那么使用 add_image。
step = step + 1
writer.close()
Module 自定义网络
nn.Module 模块作用:
模型的模块化组织 :
-
nn.Module 为构建神经网络提供了一个核心框架,在其基础上可以通过继承实现多种功能。
-
基于其设计特性,默认情况下将网络组件如层、激活函数及池化结构整合为独立功能单元。
-
这种组织架构显著提高了代码可读性和可维护性。
-
例如,在实例化一个卷积神经网络 (CNN) 时,默认会将卷积操作、下采样及全连接计算作为独立功能模块。
-
在 init 方法中进行参数配置,在 forward 方法中定义数据流动路线。
参数管理 :
-
自动化管理网络中的可训练的参数(包括权重与偏置)。在定义子类神经网络模块时(如线性层、卷积层等),这些结构会自然纳入该模块的所有可学习参数集合中。
-
模型功能不仅限于获取和处理数据;它还能够对数据进行分类和预测,并通过深度学习算法提升其准确性。
-
例如,在图像分类任务中:
- 使用预定义的数据集
- 应用卷积神经网络模型
- 训练分类器以识别不同物体类别
- 评估模型性能并根据结果进行微调
前向传播的定义 :
-
必须在继承nn.Module的子类中定义forward函数以描述数据在网络中的传递过程。
-
正式地讲,在输入张量net(input_tensor)被传递时调用forward函数完成数据处理流程。
设备迁移 :
-
nn.Module 包含 to 方法,允许将整个网络及其参数转移至不同设备(如CPU或GPU)。
-
该功能对于在GPU上加速深度学习计算至关重要。
-
由于GPU能够显著提升训练与推理的速度。
训练和评估模式切换 :
-
nn.Module 包含 train() 和 eval() 方法。
-
这些方法允许将网络转换至训练模式或评估模式。
-
其中一些层(如 nn.Dropout 和 nn.BatchNorm2d)在不同工作状态下表现出不同的行为。
-
例如,在 training 模式下, dropout 层会在前向传播期间随机移除一定比例的神经元节点以防止过拟合, 而在 inference 模式下则不会执行此操作。
-
同样地, 批量归一化层 (nn.BatchNorm2d) 在 training 模式下利用当前批次数据计算并归一化特征, 以便更新运行均值和方差; 而在 evaluation 模式下则采用预先存储的统计数据参数。
模型保存和加载 :
可以用state_dict()函数调用获取该网络的状态表示。
其中包含了网络的参数数据。
方便保存和加载模型.
前向传播
import torch
import torch.nn as nn
class SimpleNet(nn.Module):
def __init__(self):
super(SimpleNet, self).__init__()
# 定义网络的层
self.fc1 = nn.Linear(10, 5) # 全连接层,输入维度为 10,输出维度为 5
self.relu = nn.ReLU() # ReLU 激活函数
self.fc2 = nn.Linear(5, 2) # 全连接层,输入维度为 5,输出维度为 2
def forward(self, x):
# 前向传播过程
x = self.fc1(x) # 输入 x 通过第一个全连接层
x = self.relu(x) # 使用 ReLU 激活函数
x = self.fc2(x) # 通过第二个全连接层
return x
# 创建一个 SimpleNet 实例
net = SimpleNet()
# 生成一个随机输入张量,形状为 (batch_size, input_size),这里 batch_size = 3,input_size = 10
input_tensor = torch.randn(3, 10)
# 进行前向传播 -- net()会调用__call__方法,__call__方法会调用forward
output_tensor = net(input_tensor)
print(f"Input tensor shape: {input_tensor.shape}")
print(f"Output tensor shape: {output_tensor.shape}")
输出:
Input tensor shape: torch.Size([3, 10])
Output tensor shape: torch.Size([3, 2])
卷积层
核心函数conv2d介绍:
torch.nn.functional.conv2d
conv2d 函数的输入通常是一个四维张量,其形状为 (batch_size, in_channels, height, width)。这四个维度分别代表:
- batch_size:指代输入数据的批量大小。
在深度学习体系中,默认情况下会设置一个合理的批量大小来平衡计算效率与内存占用。
例如,在训练阶段会利用批量加载技术一次性完成多个样本的数据前向传播。
通过批量加载技术一次性完成多个样本的数据前向传播。 - in_channels:代表输入数据所具有的色彩通道数量。
常见的彩色图像拥有三个独立的色彩通道(红、绿、蓝)。
单色灰度图像则仅包含一个单一的色彩通道。 - 在卷积层设计时需要特别注意的是:
因为卷积操作会对每一个独立的颜色通道分别进行处理,
所以必须确保每个卷积核都有与之匹配的颜色通道数量,
这样才能保证正确的特征提取和信号传递过程。
举例而言,
在一个典型的 RGB 图像分类任务中,
当 in_channels = 3 (即使用三个颜色维度)时,
每个二维滤波器都会对应于一个特定的颜色空间,
从而实现对多维信息的有效捕捉与整合。
height 和 width:描述输入数据的高度和宽度;即它们代表图像或特征图的空间尺寸。
import torch
import torch.nn.functional as F
# 定义输入张量,形状为 (5, 5)
input = 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]
])
# 定义卷积核张量,形状为 (3, 3)
kernel = torch.tensor([
[1, 2, 1],
[0, 1, 0],
[2, 1, 0]
])
# 将输入张量重塑为 (1, 1, 5, 5),符合 conv2d 函数的输入格式
# 1 表示批次数,1 表示输入通道数,5 表示高度,5 表示宽度
input = torch.reshape(input, [1, 1, 5, 5])
# 将卷积核张量重塑为 (1, 1, 3, 3)
# 1 表示输入通道数,1 表示输出通道数,3 表示卷积核高度,3 表示卷积核宽度
kernel = torch.reshape(kernel, [1, 1, 3, 3])
# 使用 conv2d 函数进行卷积操作,步长为 1
output = F.conv2d(input, kernel, stride=1)
# 打印卷积结果
print(output)
输出:
tensor([[[[10, 12, 12],
[18, 16, 16],
[13, 9, 3]]]])
conv2d 函数相关参数作用如下动图所示:

该页面托管于github.com/vdumoulin/conv_arithmetic中的README.md文件
Tips: Reshape方法介绍
-
函数功能 : reshape函数的核心作用是将输入的张量input重塑为其预设指定的新shape结构。通常情况下,输出会是输入的一个视图;但如果无法直接生成视图,则会生成一份拷贝。
-
参数: * input (Tensor): 受reshaped处理的目标张量。
-
shape (tuple of int): 新定义的形状结构.其中至少有一个维度大小为1,该维大小可通过计算其他所有维度元素总数来确定。
-
视图与拷贝: * 视图:当输入张量为连续时(contiguous),且其步幅(strides)兼容时(contiguous), reshape可能会返回一个共享数据的视图(shared memory),这意味着输出张量会共用内存。
- 拷贝:在特定情况下(certain circumstances), reshape必须复制数据以满足新的形状需求。
-
什么是"stride是compatible的"? * stride兼容:如果一个张量在内存中的排列方式(即stride)允许它被重新解释为另一个形状(如张量重塑),而无需对数据进行重新排列,则称其stride是兼容的。在这种情况下,重塑后的张量会形成原始张量的一个视图(view),这意味着实际数据并未被拷贝或复制,在这种操作下保持了高效性。
-
一种直观的理解方法:设想我们用拼图搭建了一个正方形相框,在正面观察时呈现方形结构;但若从侧面观察,则呈现出直线形态。这说明在不同视角下看待数据会有不同的表现形式。由于内存中的数据存储是连续分配的,在这种连续性基础上定义一个3x3矩阵时,在内存中的实际长度为9个字节(即9个元素)。因此我们也可以将其视为1x9或9x1矩阵的形式(因为它们在内存中的存在形式相同)。因此reshape操作就是在保证数据在内存中以原有形式存在的前提下改变我们观察的角度或形状。
卷积简单应用
import torchvision
import torch.nn as nn
import torch
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
# 加载 CIFAR-10 测试数据集,并将其转换为 Tensor 类型
dataset = torchvision.datasets.CIFAR10("./Datasets", train=False, transform=torchvision.transforms.ToTensor(), download=False)
# 使用 DataLoader 包装数据集,设置批次大小为 64
dataloader = DataLoader(dataset=dataset, batch_size=64)
# 定义一个自定义的卷积神经网络 Mynn,继承自 nn.Module
class Mynn(nn.Module):
def __init__(self):
super(Mynn, self).__init__()
# 定义一个卷积层,输入通道为 3,输出通道为 6,卷积核大小为 3x3,步长为 1
# 每个卷积核会对输入张量的所有通道进行卷积操作,并将结果相加得到一个输出通道。
# 因此,有多少个输出通道,就有多少个卷积核。
self.conv1 = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1)
def forward(self, x):
# 前向传播过程,将输入通过卷积层
output = self.conv1(x)
return output
# 创建 Mynn 网络实例
mynn = Mynn()
# 创建 SummaryWriter,将日志存储在 "./logs" 目录下
writer = SummaryWriter("./logs")
# 初始化步骤计数器
step = 0
# 遍历数据加载器
for data in dataloader:
# 从数据加载器中获取一个批次的数据,包括图像和标签
imgs, targets = data
# 将一个批次的图像输入到网络中进行前向传播
output = mynn(imgs)
# 打印输入和输出的形状
print(imgs.shape)
print(output.shape)
# 将输入图像添加到 TensorBoard 中
writer.add_images("input", imgs, step)
# 将输出的特征图进行形状重塑,以便添加到 TensorBoard 中
output = torch.reshape(output, (-1, 3, 30, 30))
# 将重塑后的输出添加到 TensorBoard 中
writer.add_images("output", output, step)
# 增加步骤计数器
step = step + 1
# 关闭 SummaryWriter
writer.close()
输出:
torch.Size([16, 3, 32, 32])
torch.Size([16, 6, 30, 30])

最大池化
最大池化(Max Pooling)是一种常见的下采样或降采样操作,在卷积神经网络(CNNs)中得到广泛应用。其主要作用是通过在局部区域中选取最大值来降低输出的空间维度,并有效保留关键特征信息的同时减少计算复杂度,并防止模型过度拟合。

类似于卷积操作一样,在池化操作中也是通过一个称为池化核的区域来进行扫描。值得注意的是,在进行扫描时,并不是从池化核内部执行相乘求和的操作(如卷积操作那样),而是从池化核内部提取该区域的最大值作为主要特征。
在最大池化过程中涉及的主要类型是 MaxPool2d(用于处理二维图像数据)。需要注意的是这两个参数分别是 MaxPool2d 中的关键参数:ceil_mode 和 dilation。
关于 ceil_mode 参数:
- ceil_mode=False(默认):该系统会采用向下取整的方法来确定输出特征图尺寸;当池化窗口滑动至输入特征图边缘时其舍弃不处理边缘数据。
- ceil_mode=True:该系统会采用向上取整的方法来确定输出特征图尺寸;即使当池化窗口滑动至输入特征图边缘时其也能被纳入考虑范围并参与池化。
关于 dilation 参数:

dilation 表示的是池化核内部各个元素之间的 stride,也就是 步幅。
import torchvision
import torch.nn as nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
# 加载 CIFAR-10 测试数据集,并将其转换为 Tensor 类型
dataset = torchvision.datasets.CIFAR10("./Datasets", train=False, transform=torchvision.transforms.ToTensor())
# 使用 DataLoader 包装数据集,设置批次大小为 64
dataloader = DataLoader(dataset=dataset, batch_size=64)
# 创建 SummaryWriter,将日志存储在 "logs" 目录下
writer = SummaryWriter("logs")
# 初始化步骤计数器
step = 0
# 定义一个自定义的神经网络模块,包含一个最大池化层
class My_nn(nn.Module):
def __init__(self):
super(My_nn, self).__init__()
# 定义一个最大池化层,核大小为 3x3,步长为 3,无填充,ceil_mode=True
self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=3, padding=0, ceil_mode=True)
def forward(self, x):
# 前向传播过程,将输入通过最大池化层
output = self.maxpool1(x)
return output
# 创建 My_nn 网络实例
my_nn = My_nn()
# 遍历数据加载器
for data in dataloader:
# 从数据加载器中获取一个批次的数据,包括图像和标签
imgs, targets = data
# 将一个批次的图像输入到网络中进行前向传播
output = my_nn(imgs)
# 将输入图像添加到 TensorBoard 中
writer.add_images("maxpool_input", imgs, step)
# 将最大池化层的输出添加到 TensorBoard 中
writer.add_images("maxpool_output", output, step)
# 增加步骤计数器
step = step + 1
# 关闭 SummaryWriter
writer.close()
输出:

非线性层
ReLU 全称为 Rectified Linear Unit(修正线性单元),是一种在神经网络中广泛应用的激活函数。其数学表达式为 f(x) = \max(0, x)
即当输入值小于 0 时输出为 0,当输入值大于等于0时输出为输入值本身。
ReLU 的名称源自其数学表达式中的"rectification"操作(即"修正")以及其作为"linear unit"的特点。具体而言,"rectification"指的是当输入数值低于零时会被设置为零,而"linear unit"则指当输入数值非负时会维持其原有的线性特性。
在神经网络领域中,ReLU这一函数的核心功能是主动地引入非线性特性,这是实现深度学习模型关键能力的重要基础之一
ReLU 具有以下优点:
运算简便:ReLU 的运算过程非常简单,只需判断输入是否大于0。 具有显著:相比于传统的 Sigmoid 和 Tanh 激活函数,ReLU 显著地具有梯度在正区间恒为1的特点。
注意
- 卷积层(Convolutional Layer):该层通过利用卷入操作将预设的滤波器作用于输入数据,并提取出所需的特征信息。
- 激活层(Activation Layer):经过该层处理后通常会采用 ReLU 激活函数来进行非线性变换运算。
- 池化层(Pooling Layer):通过下采样处理进一步精简数据量并提取主要特征信息,在实际应用中常采用的最大值池化和平均值池化两种基本实现方式。
这种模式一般采用卷积 -> 激活(ReLU)-> 池化的形式进行处理。该结构可反复进行多次操作以构建多层网络架构。在实际应用中也可以依据需求灵活配置各层之间的连接方式和参数设置以优化模型性能。
基于这种架构设计的深度学习模型中, 卷积神经网络利用卷积层提取图像特征. 接着, 在激活过程中引入了非线性特性. 然后, 在池化操作之后进行下采样处理以降低计算负担. 此外, 在整个设计中还考虑了减少计算开销以及优化模型复杂度的问题. 最终的结果是实现了对复杂任务的有效处理能力
import torchvision
from torch.utils.tensorboard import SummaryWriter
import torch.nn as nn
from torch.utils.data import DataLoader
# 加载 CIFAR-10 数据集,仅使用测试集,同时将数据转换为张量
dataset = torchvision.datasets.CIFAR10("./Datasets", train=False, transform=torchvision.transforms.ToTensor())
# 创建数据加载器,将数据集按批次加载,每个批次包含 64 个样本
dataloader = DataLoader(dataset=dataset, batch_size=64)
# 自定义神经网络类,继承自 nn.Module
class My_nn(nn.Module):
def __init__(self):
super(My_nn, self).__init__()
# 定义 ReLU 激活函数
self.relu1 = nn.ReLU()
# 定义 Sigmoid 激活函数
self.sigmoid = nn.Sigmoid()
def forward(self, x):
# 前向传播过程
# 将输入 x 通过 ReLU 激活函数得到输出 output_1
output_1 = self.relu1(x)
# 将输入 x 通过 Sigmoid 激活函数得到输出 output_2
output_2 = self.sigmoid(x)
# 返回两个激活函数的输出结果
return output_1, output_2
# 实例化自定义的神经网络
my_nn = My_nn()
# 创建 SummaryWriter 对象,用于将数据记录到 "./ReLU_logs" 目录下,以便使用 TensorBoard 进行可视化
writer = SummaryWriter("./ReLU_logs")
# 初始化步骤计数器
step = 0
# 遍历数据加载器,按批次处理数据
for data in dataloader:
# 从批次数据中分离出图像数据 imgs 和目标标签 targets
imgs, targets = data
# 将图像数据 imgs 输入到自定义神经网络中,得到 ReLU 和 Sigmoid 激活函数的输出
output1, output2 = my_nn(imgs)
# 将 ReLU 激活函数的输出添加到 TensorBoard 的 "ReLU" 部分,并使用 step 作为当前步骤的标识
writer.add_images("ReLU", output1, step)
# 将 Sigmoid 激活函数的输出添加到 TensorBoard 的 "Sigmoid" 部分,并使用 step 作为当前步骤的标识
writer.add_images("Sigmoid", output2, step)
# 步骤计数器加 1
step += 1
writer.close()
输出:

线性层
一般来说,在神经网络架构中,线性层(Linear Layer)被归类为全连接层(Fully Connected Layer)。它们的主要功能是通过对输入数据进行线性变换来处理信息。具体而言,在数学表达式中,给定输入向量x和权重矩阵W,在经过计算后得到的结果为W乘以x再加上偏置项b。
线性层的作用
- 特征提取 : 线性变换模块能够将输入信号投射至一个新的特征空间中,在提升数据表达能力的同时起到关键作用。
- 尺寸调整 : 通过调节输入与输出的维度配置,在不影响数据完整性的情况下实现数据的空间结构转换。
- 参数训练 : 线性层中的可调节参数能够通过反向传播机制进行精确优化以满足特定的任务需求。
卷积神经网络(CNN)中线性层和其他层的关系:
- 卷积神经网络中的卷积层 (Convolutional Layer): 通过卷积操作作用于输入数据以提取图像的局部特征信息,并实现空间位置上的局部感受野特性。例如:torch.nn.Conv2d
- 非线性激活函数 (Non-linear Activation Function): 引入非线性激活函数使网络能够学习和表示更为复杂的模式与关系。例如:torch.nn.ReLU
- 池化层 (Pooling Layer): 池化层用于减少计算量并增强模型鲁棒性。通过降采样处理特征图以降低空间维度并减少计算复杂度,并且有助于防止过拟合现象的发生。例如:torch.nn.MaxPool2d
重复上述三步多次,逐步提取高层次特征。
- 扩展层 (Flatten Layer): 将多维特征图展平为一维向量以供后续处理,并提供给全连接层使用(例如:
torch.flatten())。 - 线性层 (Linear Layer): 对输入特征进行线性变换作为后续操作的基础模块,并常用于分类或回归任务的最后一两层(例如:
torch.nn.Linear)。 - 输出层 (Output Layer): 最后一层通常是线性结构并配合激活函数完成最终输出(如 Softmax 函数在分类任务中使用),其示例代码为
torch.nn.Softmax。
import torch
import torchvision
from torch.utils.data import DataLoader
import torch.nn as nn
# 加载 CIFAR-10 数据集,使用测试集,将数据转换为张量
datasets = torchvision.datasets.CIFAR10("Datasets", train=False, transform=torchvision.transforms.ToTensor())
# 使用 DataLoader 将数据集按批次加载,批次大小为 64
dataloader = DataLoader(dataset=datasets, batch_size=64)
# 自定义神经网络类,继承自 nn.Module
class My_nn(nn.Module):
def __init__(self):
super(My_nn, self).__init__()
# 定义一个线性层,输入维度为 196608,输出维度为 10
self.linear = nn.Linear(196608, 10)
def forward(self, x):
# 前向传播过程,将输入通过线性层
output = self.linear(x)
return output
# 实例化自定义的神经网络
my_nn = My_nn()
# 遍历数据加载器
for data in dataloader:
# 获取一个批次的数据,包括图像和目标标签
imgs, targets = data
# 打印原始图像数据的形状
print(imgs.shape)
# 将图像数据展平为一维张量
output = torch.flatten(imgs)
# 打印展平后的张量形状
print(output.shape)
# 将展平后的张量输入到神经网络中
output = my_nn(output)
# 打印经过神经网络后的输出形状
print(output.shape)
输出:
torch.Size([64, 3, 32, 32])
torch.Size([196608])
torch.Size([10])
简单的整合
训练CIFAR10 数据集采用的网络结构如下图所示:

conv2d 的 padding 参数值计算公式如下:

公式对应的官方文档具体页码
import torch
from torch import nn
from torch.nn import Sequential, Conv2d, MaxPool2d, Flatten, Linear
from torch.utils.tensorboard import SummaryWriter
class my_nn(nn.Module):
def __init__(self):
super(my_nn, self).__init__()
# 使用 Sequential 容器组合多个层
self.model = Sequential(
# 第一个卷积层:输入通道为 3,输出通道为 32,卷积核大小为 5,填充为 2
Conv2d(3, 32, 5, padding=2),
# 第一个最大池化层:核大小为 2
MaxPool2d(2),
# 第二个卷积层:输入通道为 32,输出通道为 32,卷积核大小为 5,填充为 2
Conv2d(32, 32, 5, padding=2),
# 第二个最大池化层:核大小为 2
MaxPool2d(2),
# 第三个卷积层:输入通道为 32,输出通道为 64,卷积核大小为 5,填充为 2
Conv2d(32, 64, 5, padding=2),
# 第三个最大池化层:核大小为 2
MaxPool2d(2),
# 将特征图展平
Flatten(),
# 第一个线性层:输入维度为 1024,输出维度为 64
Linear(1024, 64),
# 第二个线性层:输入维度为 64,输出维度为 10
Linear(64, 10)
)
def forward(self, x):
# 前向传播:将输入通过 Sequential 容器中的层
output = self.model(x)
return output
# 实例化自定义的神经网络
my = my_nn()
# 打印网络结构
print(my)
# 创建一个测试输入张量,形状为 (64, 3, 32, 32) , 值全部为1
input = torch.ones((64, 3, 32, 32))
# 将输入张量传入网络,得到输出
output = my(input)
# 打印输出形状
print(output.shape)
# 创建 SummaryWriter 对象,将日志存储在 "logs_seq" 目录下
writer = SummaryWriter("logs_seq")
# 将网络结构添加到 TensorBoard 中
writer.add_graph(my, input)
# 关闭 SummaryWriter
writer.close()
输出:
my_nn(
(model): Sequential(
(0): Conv2d(3, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(2): Conv2d(32, 32, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(4): Conv2d(32, 64, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
(5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(6): Flatten(start_dim=1, end_dim=-1)
(7): Linear(in_features=1024, out_features=64, bias=True)
(8): Linear(in_features=64, out_features=10, bias=True)
)
)
torch.Size([64, 10])

阐述反向传播与损失函数: 反向传播算法即通过损失函数来逆向更新训练参数的过程被称作反向传播。
# 实例化神经网络
my = my_nn()
# 定义交叉熵损失函数
loss = nn.CrossEntropyLoss()
# 遍历数据加载器
for data in dataloader:
# 获取图像和标签
imgs, targets = data
# 前向传播
output = my(imgs)
# 计算损失
result_loss = loss(output, targets)
# 反向传播
result_loss.backward()
引入优化器的方式是将其与反向传播算法结合在一起以优化每一步的梯度值。这一基本过程主要包括以下步骤:首先定义一个优化器,在循环内部清除上一次迭代中的梯度值;随后运用反向传播算法计算当前参数;最后通过预先设定好的优化策略更新下一步迭代中的参数值。
# 实例化神经网络
my = my_nn()
# 定义交叉熵损失函数,用于分类任务
loss = nn.CrossEntropyLoss()
# 定义随机梯度下降(SGD)优化器,学习率为 0.01
optim = torch.optim.SGD(my.parameters(), lr=0.01)
# 训练 20 个 epoch
for epoch in range(20):
# 初始化每轮的累积损失为 0.0
running_loss = 0.0
# 遍历数据加载器中的每个批次
for data in dataloader:
# 获取图像和目标标签
imgs, targets = data
# 前向传播,将图像输入网络得到输出
output = my(imgs)
# 计算损失
result_loss = loss(output, targets)
# 清空梯度,避免梯度累积
optim.zero_grad()
# 反向传播,计算梯度
result_loss.backward()
# 更新参数,根据梯度更新网络参数
optim.step()
# 累积每批次的损失
running_loss = running_loss + result_loss.item()
# 打印每轮的累积损失
print(running_loss)
Tips:
optim.zero_grad()是 PyTorch 优化器功能之一(如torch.optim.SGD和torch.optim.Adam等),其主要作用是清除当前所有优化器管理参数的梯度值。- 在 PyTorch 操作中,在执行反向传播(
backward())操作后会自动累加到每个参数对应的.grad属性中。为了确保每次新开始反向传播时能够正确初始化,请及时清除现有的梯度值。 - 比方说,在未调用
zero_grad()函数的情况下连续多次执行backward()操作时,“.grad”属性内的数值会被不断累加起来,在最终执行参数更新时会导致步长过大现象出现,并影响模型训练效果。
基于现有网络模型修改
import torchvision.models
from torch import nn
# 创建不带预训练权重的 VGG16 模型
vgg16_false = torchvision.models.vgg16(weights=None)
# 创建带预训练权重的 VGG16 模型
vgg16_true = torchvision.models.vgg16(weights='DEFAULT')
# 打印带预训练权重的 VGG16 模型结构
print(vgg16_true)
# 对带预训练权重的 VGG16 模型添加一个线性层,将输入特征从 4096 改为 1000,输出特征从 1000 改为 10
vgg16_true.add_module("add_Linear", nn.Linear(1000, 10))
# 再次打印修改后的 VGG16 模型结构
print(vgg16_true)
模型的保存和载入
- 模型的保存
import torch
import torchvision.models
vgg16 = torchvision.models.vgg16()
# 保存方式 1,模型结构和模型参数
torch.save(vgg16, "vgg16_method1.pth") # 引号内部是保存的文件名,最好使用 pth 结尾
# 保存方式 2, 只保存模型参数(官方推荐)
torch.save(vgg16.state_dict(), "vgg16_method2.pth")
- 模型的载入
# 由于 vgg16 是一个标准的 PyTorch 预训练模型,且这个模型类已经定义在 torchvision.models 模块中,
# 因此不需要再载入模型的 py 文件中重新定义类
# 但是对于自己定义的类,在其他 py 文件中导入时,一定要重新定义类,类似于 class My_nn(nn.Module):...
import torch
import torchvision.models
# 保存方式 1 载入模型的方式
model = torch.load("vgg16_method1.pth")
print(model)
# 保存方式 2 载入模型的方式
vgg16 = torchvision.models.vgg16()
vgg16.load_state_dict(torch.load("vgg16_method2.pth"))
print(vgg16)
# 如果方式 2 通过 方式 1 载入模型的代码载入,只会导入一堆模型参数的字典。
完整模型训练流程
还是基于 CIFAR10 训练集采用的网络结构,下面给出完整的模型训练流程代码:

- model.py 文件
import torch
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear
class My_nn(nn.Module):
def __init__(self):
super(My_nn, self).__init__()
self.model = nn.Sequential(
# 第一个卷积层:输入通道为 3,输出通道为 32,卷积核大小为 5,步长为 1,填充为 2
Conv2d(3, 32, 5, 1, 2),
# 最大池化层,池化核大小为 2
MaxPool2d(2),
# 第二个卷积层:输入通道为 32,输出通道为 32,卷积核大小为 5,步长为 1,填充为 2
Conv2d(32, 32, 5, 1, 2),
# 最大池化层,池化核大小为 2
MaxPool2d(2),
# 第三个卷积层:输入通道为 32,输出通道为 64,卷积核大小为 5,步长为 1,填充为 2
Conv2d(32, 64, 5, 1, 2),
# 最大池化层,池化核大小为 2
MaxPool2d(2),
# 展平层,将特征图展平为一维向量
Flatten(),
# 第一个线性层:输入维度为 64*4*4,输出维度为 64
Linear(64*4*4, 64),
# 第二个线性层:输入维度为 64,输出维度为 10
Linear(64, 10)
)
def forward(self, x):
# 前向传播函数,将输入 x 通过模型
output = self.model(x)
return output
if __name__ == '__main__':
# 实例化自定义的神经网络
my_nn = My_nn()
# 创建一个形状为 (64, 3, 32, 32) 的输入张量
input = torch.ones((64, 3, 32, 32))
# 将输入张量传递给网络进行前向传播
output = my_nn(input)
# 打印输出张量的形状
print(output.shape)
Tips:
- 此
if __name__ == '__main__'语句 是一个关键控制结构,在程序设计中具有重要地位。 - 当Python解释器启动一个
.py文件时, 会自动将其中定义的所有变量与函数绑定到当前程序环境中。 - 这种绑定机制确保了Python程序能够独立运行并与其他代码进行交互。
- train.py 文件
import torchvision
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from model import * # model.py 文件 与 train.py 文件要在同一个文件夹下
# 导入数据集
dataset_train = torchvision.datasets.CIFAR10("Datasets", train=True, transform=torchvision.transforms.ToTensor())
dataset_test = torchvision.datasets.CIFAR10("Datasets", train=True, transform=torchvision.transforms.ToTensor())
# 加载数据集
dataloader_train = DataLoader(dataset=dataset_train, batch_size=64, shuffle=True)
dataloader_test = DataLoader(dataset=dataset_test, batch_size=64, shuffle=True)
# 训练集的长度
len_train = len(dataset_train)
print(f"训练集的长度为 {len_train}")
# 测试集的长度
len_test = len(dataset_test)
print(f"测试集的长度为 {len_test}")
# 构造模型
model = My_nn()
# 设置训练和测试次数
train_step = 0
test_step = 0
# 设置训练轮数
epoch = 10
# 设置学习率
learning_rate = 1e-2
# 定义优化器
optimise = torch.optim.SGD(model.parameters(), lr=learning_rate)
# 定义损失函数
my_loss = nn.CrossEntropyLoss()
# 构造日志文件
writer = SummaryWriter("Train_Logs")
for i in range(epoch):
print(f"----开始第 {i+1} 轮训练----")
# 训练步骤开始
# 这一行并不是对于所有的网络都是必须的,
# 只有当网络中含有 dropout 层、BatchNorm或是其他一些特殊的层的时候需要使用这一层激活
# 关于这一点说明参见
# https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module:~:text=in%20evaluation%20mode.-,This%20has%20any%20effect%20only%20on%20certain%20modules.%20See%20documentations%20of%20particular%20modules%20for%20details%20of%20their%20behaviors%20in%20training/evaluation%20mode%2C%20if%20they%20are%20affected%2C%20e.g.%20Dropout%2C%20BatchNorm%2C%20etc.,-This%20is%20equivalent
model.train()
for data in dataloader_train:
imgs, targets = data
outputs = model(imgs)
loss = my_loss(outputs, targets)
optimise.zero_grad()
loss.backward()
optimise.step()
train_step += 1
if train_step % 100 == 0:
print(f"第 {train_step} 步 损失率:{loss.item()}")
writer.add_scalar("train_loss", loss.item(), train_step)
# 测试步骤开始
# 这一行也不是对所有网络都是必须的
model.eval()
total_test_loss = 0
total_accuracy = 0
with torch.no_grad():
for data in dataloader_test:
imgs, targets = data
outputs = model(imgs)
loss = my_loss(outputs, targets)
total_test_loss += loss.item()
# outputs.argmax(1) 表示的是在outputs每一行的所有类别预测中,找到
# 概率最大的一个并返回,返回的结果就是该 img 的标签,由于 out_feature = 10
# 因此标签值是 0 ~ 9 中的一个。outputs.argmax(1) 具有 batch_size 个标签值
# 将该标签列表中与 targets 进行元素级别的比较,若元素相同会返回 1,否则为 0
# 将所有返回 1 的值相加即得到正确的样本个数,除以测试集大小就是正确率
accuracy = (outputs.argmax(1) == targets).sum()
total_accuracy += accuracy
print(f"整体测试集的损失 Loss 为 {total_test_loss}")
print(f"整体测试集上的正确率为 {total_accuracy/len_test}")
writer.add_scalar("test_loss", total_test_loss, test_step)
writer.add_scalar("test_accuracy", total_accuracy/len_test, test_step)
test_step += 1
torch.save(model, "torch_{}.pth".format(i))
# 也可以使用官方推荐的方式进行保存,即 torch.save(model.state_dict(), "torch_{}.pth".format(i))
print("模型已保存")
writer.close()
Tips: python中的with关键字
with open("1.txt") as file:
data = file.read()
- 紧随with关键字后的语句执行完毕后, 返回对象的
–enter–()方法会被调用, 其返回值会被赋值给as后面的变量;- 一旦with关键字后的代码块执行完毕, 返回对象的
–exit–()方法会被调用。
- 一旦with关键字后的代码块执行完毕, 返回对象的
Tips:
with torch.no_grad()
- 在使用深度学习框架进行训练或者推理时,我们通常需要计算各个模型参数的梯度,以便进行反向传播和参数更新。然而,在某些情况下,我们并不需要计算梯度,例如在模型预测或评估时,只需要进行前向传播并获得输出结果。
- 在这些情况下,使用 “with torch.no_grad()” 可以帮助我们关闭梯度计算,从而同时减少内存的消耗,并提高程序的运行速度。因为不计算梯度,所以无需保存相应的中间结果,这可以显著减少内存的使用。
应用GPU对模型进行训练
- GPU 训练模式一:利用 .cuda() 方法
- 为了优化网络性能并使其能够高效运行在GPU上。
必须配置相应的环境变量以激活该方法。
为了确保所有组件都能够顺利运行在GPU上,并根据需求分别配置网络模型的数据类型、数据加载器的工作模式以及损失函数的计算方式。
import torch
import torchvision
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import time
# 导入数据集
dataset_train = torchvision.datasets.CIFAR10("Datasets", train=True, transform=torchvision.transforms.ToTensor())
dataset_test = torchvision.datasets.CIFAR10("Datasets", train=False, transform=torchvision.transforms.ToTensor())
# 加载数据集
dataloader_train = DataLoader(dataset=dataset_train, batch_size=64, shuffle=True)
dataloader_test = DataLoader(dataset=dataset_test, batch_size=64, shuffle=True)
# 训练集的长度
len_train = len(dataset_train)
print(f"训练集的长度为 {len_train}")
# 测试集的长度
len_test = len(dataset_test)
print(f"测试集的长度为 {len_test}")
# 模型创建
class My_nn(nn.Module):
def __init__(self):
super(My_nn, self).__init__()
self.model = nn.Sequential(
Conv2d(3, 32, 5, 1, 2),
MaxPool2d(2),
Conv2d(32, 32, 5, 1, 2),
MaxPool2d(2),
Conv2d(32, 64, 5, 1, 2),
MaxPool2d(2),
Flatten(),
Linear(64 * 4 * 4, 64),
Linear(64, 10)
)
def forward(self, x):
output = self.model(x)
return output
# 构造模型
model = My_nn()
if torch.cuda.is_available():
model = model.cuda()
# 设置训练和测试次数
train_step = 0
test_step = 0
# 设置训练轮数
epoch = 10
# 设置学习率
learning_rate = 1e-2
# 定义优化器
optimise = torch.optim.SGD(model.parameters(), lr=learning_rate)
# 定义损失函数
my_loss = nn.CrossEntropyLoss()
if torch.cuda.is_available():
my_loss = my_loss.cuda()
# 构造日志文件
writer = SummaryWriter("Train_Logs")
# 计时开始
start_time = time.process_time()
for i in range(epoch):
print(f"----开始第 {i + 1} 轮训练----")
# 训练步骤开始
# 这一行并不是对于所有的网络都是必须的,只有当网络中含有 dropout 层或是其他一些特殊的层的时候需要使用这一层激活
model.train()
for data in dataloader_train:
imgs, targets = data
if torch.cuda.is_available():
imgs = imgs.cuda()
targets = targets.cuda()
outputs = model(imgs)
loss = my_loss(outputs, targets)
optimise.zero_grad()
loss.backward()
optimise.step()
train_step += 1
if train_step % 100 == 0:
end_time = time.process_time()
print(f"第 {train_step} 消耗的时间为 {end_time - start_time} s")
print(f"第 {train_step} 步 损失率:{loss.item()}")
writer.add_scalar("train_loss", loss.item(), train_step)
# 测试步骤开始
# 这一行也不是对所有网络都是必须的
model.eval()
total_test_loss = 0
total_accuracy = 0
with torch.no_grad():
for data in dataloader_test:
imgs, targets = data
if torch.cuda.is_available():
imgs = imgs.cuda()
targets = targets.cuda()
outputs = model(imgs)
loss = my_loss(outputs, targets)
total_test_loss += loss.item()
# outputs.argmax(1) 表示的是在 outputs 每一行的所有类别预测中,找到
# 概率最大的一个并返回,返回的结果就是该 img 的标签,由于 out_feature = 10
# 因此标签值是 0 ~ 9 中的一个。outputs.argmax(1) 具有 batch_size 个标签值
# 将该标签列表中与 targets 进行元素级别的比较,若元素相同会返回 1,否则为 0
# 将所有返回 1 的值相加即得到正确的样本个数,除以测试集大小就是正确率
accuracy = (outputs.argmax(1) == targets).sum()
total_accuracy += accuracy
print(f"整体测试集的损失 Loss 为 {total_test_loss}")
print(f"整体测试集上的正确率为 {total_accuracy / len_test}")
writer.add_scalar("test_loss", total_test_loss, test_step)
writer.add_scalar("test_accuracy", total_accuracy / len_test, test_step)
test_step += 1
torch.save(model, "torch_{}.pth".format(i))
# 也可以使用官方推荐的方式进行保存,即 torch.save(model.state_dict(), "torch_{}.pth".format(i))
print("模型已保存")
writer.close()
- GPU 训练模式二:利用 .to(device) 方法
为了实现系统的正常运行,必须先明确所需的硬件配置.随后还需将网络模型以及相关的数据(包括输入端和标注端的数据)与损失函数一并连接至该设备上.
import torch
import torchvision
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import time
# 导入数据集
dataset_train = torchvision.datasets.CIFAR10("Datasets", train=True, transform=torchvision.transforms.ToTensor())
dataset_test = torchvision.datasets.CIFAR10("Datasets", train=False, transform=torchvision.transforms.ToTensor())
# 加载数据集
dataloader_train = DataLoader(dataset=dataset_train, batch_size=64, shuffle=True)
dataloader_test = DataLoader(dataset=dataset_test, batch_size=64, shuffle=True)
# 训练集的长度
len_train = len(dataset_train)
print(f"训练集的长度为 {len_train}")
# 测试集的长度
len_test = len(dataset_test)
print(f"测试集的长度为 {len_test}")
# 定义训练设备
device = torch.device("cpu")
# 如果需要使用 gpu 进行训练那么就定义为 device = torch.device("cuda") ,
# 或者使用 device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 模型创建
class My_nn(nn.Module):
def __init__(self):
super(My_nn, self).__init__()
self.model = nn.Sequential(
Conv2d(3, 32, 5, 1, 2),
MaxPool2d(2),
Conv2d(32, 32, 5, 1, 2),
MaxPool2d(2),
Conv2d(32, 64, 5, 1, 2),
MaxPool2d(2),
Flatten(),
Linear(64*4*4, 64),
Linear(64, 10)
)
def forward(self, x):
output = self.model(x)
return output
# 构造模型
model = My_nn()
model = model.to(device)
# 设置训练和测试次数
train_step = 0
test_step = 0
# 设置训练轮数
epoch = 10
# 设置学习率
learning_rate = 1e-2
# 定义优化器
optimise = torch.optim.SGD(model.parameters(), lr=learning_rate)
# 定义损失函数
my_loss = nn.CrossEntropyLoss()
my_loss = my_loss.to(device)
# 构造日志文件
writer = SummaryWriter("Train_Logs")
# 计时开始
start_time = time.process_time()
for i in range(epoch):
print(f"----开始第 {i+1} 轮训练----")
# 训练步骤开始
# 这一行并不是对于所有的网络都是必须的,只有当网络中含有 dropout 层或是其他一些特殊的层的时候需要使用这一层激活
model.train()
for data in dataloader_train:
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)
outputs = model(imgs)
loss = my_loss(outputs, targets)
optimise.zero_grad()
loss.backward()
optimise.step()
train_step += 1
if train_step % 100 == 0:
end_time = time.process_time()
print(f"第 {train_step} 消耗的时间为 {end_time - start_time} s")
print(f"第 {train_step} 步 损失率:{loss.item()}")
writer.add_scalar("train_loss", loss.item(), train_step)
# 测试步骤开始
# 这一行也不是对所有网络都是必须的
model.eval()
total_test_loss = 0
total_accuracy = 0
with torch.no_grad():
for data in dataloader_test:
imgs, targets = data
imgs = imgs.to(device)
targets = targets.to(device)
outputs = model(imgs)
loss = my_loss(outputs, targets)
total_test_loss += loss.item()
# outputs.argmax(1) 表示的是在outputs每一行的所有类别预测中,找到
# 概率最大的一个并返回,返回的结果就是该 img 的标签,由于 out_feature = 10
# 因此标签值是 0 ~ 9 中的一个。outputs.argmax(1) 具有 batch_size 个标签值
# 将该标签列表中与 targets 进行元素级别的比较,若元素相同会返回 1,否则为 0
# 将所有返回 1 的值相加即得到正确的样本个数,除以测试集大小就是正确率
accuracy = (outputs.argmax(1) == targets).sum()
total_accuracy += accuracy
print(f"整体测试集的损失 Loss 为 {total_test_loss}")
print(f"整体测试集上的正确率为 {total_accuracy/len_test}")
writer.add_scalar("test_loss", total_test_loss, test_step)
writer.add_scalar("test_accuracy", total_accuracy/len_test, test_step)
test_step += 1
torch.save(model, "torch_{}.pth".format(i))
# 也可以使用官方推荐的方式进行保存,即 torch.save(model.state_dict(), "torch_{}.pth".format(i))
print("模型已保存")
writer.close()
模型验证
准备一张狗狗的图片:

下面加载保存的模型尝试进行识别:
import torch
from PIL import Image
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear
from torchvision import transforms
# 图片路径
image_path = r"C:\Users\zdh\Downloads\dog.png"
# 打开图片并转换为 RGB 模式
image = Image.open(image_path)
image = image.convert(mode="RGB")
# 定义图片的预处理操作
transform = transforms.Compose([
transforms.Resize((32, 32)),
# 调整图片大小为 32x32,以匹配网络输入
transforms.ToTensor()
])
# 对图片进行预处理
image = transform(image)
class My_nn(nn.Module):
def __init__(self):
super(My_nn, self).__init__()
self.model = nn.Sequential(
# 第一个卷积层:输入通道 3,输出通道 32,卷积核 5,步长 1,填充 2
Conv2d(3, 32, 5, 1, 2),
# 最大池化层,池化核 2
MaxPool2d(2),
# 第二个卷积层:输入通道 32,输出通道 32,卷积核 5,步长 1,填充 2
Conv2d(32, 32, 5, 1, 2),
# 最大池化层,池化核 2
MaxPool2d(2),
# 第三个卷积层:输入通道 32,输出通道 64,卷积核 5,步长 1,填充 2
Conv2d(32, 64, 5, 1, 2),
# 最大池化层,池化核 2
MaxPool2d(2),
# 展平特征图
Flatten(),
# 第一个线性层:输入维度 64*4*4,输出维度 64
Linear(64 * 4 * 4, 64),
# 第二个线性层:输入维度 64,输出维度 10
Linear(64, 10)
)
def forward(self, x):
# 前向传播函数
output = self.model(x)
return output
# 加载预训练的模型
model = torch.load("torch_8.pth")
# 将图像重塑为 (1, 3, 32, 32) 的形状,以匹配模型输入
image = torch.reshape(image, (1, 3, 32, 32))
# 将图像输入模型进行前向传播
output = model(image)
# 打印输出结果
print(output)
# 打印预测的类别索引
print(output.argmax(1))
输出:
tensor([[-3.6053, -4.3679, 4.0489, 3.7961, -0.3184, 7.2941, -2.1973, 3.5614,
-7.7688, -1.5362]], grad_fn=<AddmmBackward0>)
tensor([5])
让我们回顾一下CIFAR10训练集中各个标签值对应的类型:
# 对于 CIFAR-10 数据集,标签的范围是 0 到 9,分别对应 10 个类别:
# 0: airplane
# 1: automobile
# 2: bird
# 3: cat
# 4: deer
# 5: dog
# 6: frog
# 7: horse
# 8: ship
# 9: truck
Note
Python对Windos下文件路径的三种表示方法
对于路径中的反斜杠问题,在代码中应连续使用两个反斜杠符号(\setminus \setminus),这样Python才会正确解析为一个单独的反斜杠。
path = "C:\ Users\ Username\ Documents"
2. 标记原始字符串:在字符串前面加上r时会被视为未经转义的文本。
path = r"C:\Users\Username\Documents"
第3点建议采用正斜杠符号(/):该方法不仅能够支持路径表示,在Windows系统或其他操作系统中也能确保正确性。
path = "C:/Users/Username/Documents"
总体而言,在处理转义字符时通常采用正斜杠 / 或原始字符串 r 这两种方法被视为推荐的做法,并可避免转义字符带来的潜在问题
