【Pytorch】(一)使用 PyTorch 进行深度学习:60 分钟速成

前言:本篇文章是跟随 Pytorch官网60分钟速成教程 来学习。来看看今天能不能速成哈哈哈。

首先要安装pytorch:

javascript 复制代码
pip3 install torch torchvision

当然首先得有 pip3 和 python 环境,前置的安装在这里就不赘述了,这篇文章还是专注于 PyTorch 入门

一、张量

张量是一种数据结构,类似(多维)数组和矩阵,它将模型的输入、参数、输出的数据结构统一化。

pytorch 中的张量类似于 numpy 中的 narray,两者密不可分,可以相互转化,所以我们在程序一开始可以将这两个包引进来,方便调试

py 复制代码
import torch
import numpy as np

(一)张量初始化

① torch.tensor(数据)

直接来自数据,数据类型自动推断

javascript 复制代码
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)
print(x_data)

② torch.from_numpy(narray)

numpy 数组创建张量

javascript 复制代码
data = [[1, 2], [3, 4]]
np_array = np.array(data)
x_np = torch.from_numpy(np_array)
print(x_data)

③ torch.ones_like(x_data)

创建一个数据全是1的张量,会继承参数张量x_data的数据类型和形状。

javascript 复制代码
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)
y_data = torch.ones_like(x_data)
print(y_data)

④ torch.rand_like(x_data, dtype=torch.float)

创建一个随机数张量,随机数范围是0~1,形状继承参数张量 x_data ,数据类型需要通过第二个参数指定为浮点数 dtype=torch.float

javascript 复制代码
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)
y_data = torch.rand_like(x_data, dtype=torch.float)
print(y_data)

⑤ 通过shape

shape 用来指定张量的形状,直接 shape = (2, 3,) 定义,不得不说,python比js还灵活。shape = (2, 3,) 表示定义一个2行3列的二维数组,后面用逗号,表示后面还可以任意增加维度

下面的方法分别表示创建指定形状的随机数、数据全为1的张量、数据全为0的张量,第二个参数可以用 dtype=int 指定数据类型为整型

py 复制代码
shape = (2, 3, )
# 创建随机数张量
rand_tensor = torch.rand(shape)
# 创建数据全为1的张量,数据类型为int
ones_tensor = torch.ones(shape, dtype=int)
# 创建数据全为0的张量
zeros_tensor = torch.zeros(shape)

print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")

(二)张量属性

形状、数据类型、设备

javascript 复制代码
tensor = torch.rand(3, 4)

print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")

(三)张量运算

上述网页包含一百多种张量运算,下面介绍几种常见的张量运算

① 张量切片

张量切片是用于选中张量的一部分的语法。

python 复制代码
# 创建一个形状为4*4,数值全是1的张量
tensor = torch.ones(4, 4)
# 全部行的第一列设置为0
tensor[:,1] = 0
print(tensor)

② torch.cat

连接两个张量,不会升高张量的维度

python 复制代码
tensor1 = torch.ones(4, 4)
tensor2 = torch.zeros(4, 4)
print(torch.cat([tensor1,tensor2]))

③ tensor.mul

张量乘法,就可以看成矩阵乘法。二维还能手算,再往上就难手动模拟了

python 复制代码
tensor1 = torch.tensor([[1,2],[3,4]])
tensor2 = torch.tensor([[2,1],[2,3]])
print(tensor1.mul(tensor2))

④ 原地操作

带有 _ 的是原地操作,就是直接修改当前的张量

python 复制代码
tensor = torch.tensor([[1,2],[3,4]])
print(tensor, "\n")
tensor.add_(5)
print(tensor)

⑤ 与NumPy互相转化

数据层面相当于浅拷贝,修改一个会影响另一个

1️⃣ 张量转NumPy数组
python 复制代码
t = torch.ones(5, dtype=int)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")

张量的变化会反映在 NumPy 数组中。

python 复制代码
t.add_(8)
print(f"t: {t}")
print(f"n: {n}")
2️⃣ NumPy数组转张量
python 复制代码
n = np.ones(5)
t = torch.from_numpy(n)
print(f"n: {n}")
print(f"t: {t}")

NumPy数组的变化会反映在张量中。

python 复制代码
np.add(n, 1, out=n)
print(n)
print(t)

二、torch.autograd

torch.autograd 是 PyTorch 的自动微分引擎,为神经网络训练提供支持。本节从概念上解释 autograd 如何帮助神经网络进行训练。

(一)背景

神经网络是一系列对输入数据执行的网状的函数的集合。这些函数由参数(由权重和偏置组成)定义,这些参数在 PyTorch 中存储在张量中。

训练神经网络主要有两个步骤:

✬✬✬ ① 正向传播:在正向传播中,神经网络会对于正确的输出数据进行最佳猜测。输入数据经过所有的函数来获得最终猜测。

✬✬✬ ② 反向传播:在反向传播中,神经网络根据其猜测的误差成比例地调整其参数。它通过从输出端向后遍历来实现这一点,收集误差相对于函数参数的导数(梯度),并使用梯度下降法优化参数。

(二)在 PyTorch 中使用

让我们来看一个单独的训练步骤。在这个例子中,我们会加载一个 torchvision 库提供的预训练模型 resnet18。我们会创建一个随机数据张量来展示一个单独的三通道(rgb)、宽高都为64的图片,将其对应的标签初始化为一些随机值。预训练模型中的标签形状为(1,1000)。

python 复制代码
import torch
from torchvision.models import resnet18, ResNet18_Weights
model = resnet18(weights=ResNet18_Weights.DEFAULT)
data = torch.rand(1, 3, 64, 64)
labels = torch.rand(1, 1000)

下面详细解释一下上述代码

1️⃣ 导入相关的库

python 复制代码
import torch
from torchvision.models import resnet18, ResNet18_Weights
  • torch: PyTorch 深度学习框架核心库
  • torchvision.models: PyTorch的计算机视觉模型库
  • resnet18: ResNet-18 网络结构(18层深度)
  • ResNet18_Weights: ResNet-18 的预训练权重枚举类

2️⃣ 加载预训练的 ResNet-18 模型

python 复制代码
model = resnet18(weights=ResNet18_Weights.DEFAULT)

weights=ResNet18_Weights.DEFAULT: 使用最新的官方预训练权重
模型特性:

  • 输入尺寸: 224×224 的 RGB 图像
  • 输出维度: 1000 类(ImageNet 数据集分类)
  • 预训练: 在 ImageNet 数据集上训练过

3️⃣ 创建模拟输入数据

python 复制代码
data = torch.rand(1, 3, 64, 64)

形状: (1, 3, 64, 64)

1: 批次大小(batch size)- 1张图像

3: 通道数(RGB)- 彩色图像

64: 高度(height)- 64像素

64: 宽度(width)- 64像素
数值: [0, 1) 范围内的随机浮点数

4️⃣ 创建模拟标签

python 复制代码
labels = torch.rand(1, 1000)

形状: (1, 1000)

1: 批次大小(对应1个样本)

1000: 分类数(ImageNet的1000个类别)
数值: [0, 1) 范围内的随机数

5️⃣ 前向传播

接下来,我们将输入数据通过模型的每一层进行运算,以做出预测。这被称为前向传播。

python 复制代码
prediction = model(data) # forward pass

6️⃣ 反向传播

然后需要用预测值和标签来计算损失,通过网络反向传播损失。我们通过损失张量调用 .backward() 来进行反向传播,Autograd 随后计算并存储每个模型参数的梯度,并将这些梯度保存在参数的 .grad 属性中。

python 复制代码
# 计算损失
loss = (prediction - labels).sum()
# 反向传播
loss.backward()

7️⃣ 加载优化器

下一步,我们会加载一个优化器。接下来,我们加载一个优化器,这里选择的是SGD,学习率为0.01,动量为0.9。

我们需要在优化器里注册模型所有的参数。

python 复制代码
optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)

优化器

在机器学习和深度学习中,优化器(Optimizer) 扮演着"导航员"的角色。它的核心作用是通过调整模型的参数,来最小化损失函数(Loss Function)的值,从而让模型的预测结果越来越接近真实值。

简单来说,如果把训练模型比作在深夜下山,损失函数就是"山的高度",而优化器就是带你寻找"山谷(最低点)"的方法和步伐。

核心作用与目标优化器的本质是执行 梯度下降(Gradient Descent) 的变体。其具体工作包括:

更新参数:根据计算出的梯度(Gradient),决定如何修改模型权重 w w w 和偏置 b b b。

控制学习速率:决定每一步走多远(学习率 η \eta η)。寻找最优解:在复杂的参数空间中避开鞍点或局部最小值,寻找全局最优解(或足够好的局部最优解)。

动量借用了物理学的概念,简单来说,动量的作用就是让损失函数更加平滑,在稳定的方向上速度更快。

8️⃣ 启动梯度下降

最后,我们调用 .step() 来启动梯度下降。优化器会根据存储在 .grad 中的梯度来调整每个参数。

python 复制代码
optim.step() #梯度下降

上述就是训练神经网络的整体流程。

(三)Autograd中的自微分

接下来看一下 autograd 如何收集梯度。下面的代码创建了两个张量 ab,通过 requires_grad=True 表示这两个张量需要计算梯度,这表示这两个张量上的每一步操作都会被追踪。

python 复制代码
import torch
a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)

接下来通过对 a 和 b 的运算式创建另一个张量 Q

python 复制代码
Q = 3*a**3 - b**2

假设 a 和 b 是神经网络的参数,Q 是误差。在神经网络训练中,我们想要误差 Q 关于参数的梯度,即:

当我们在 Q 上调用 .backward() 时,autograd 会计算这些梯度,并将它们存储在相应张量的 .grad 属性中。

我们需要在 Q.backward() 中显式传递一个梯度参数,它是一个向量。梯度是一个与 Q 形状相同的张量,它表示 Q 相对于自身的梯度,即:

等价地,我们也可以将 Q 聚合为一个标量,并隐式地调用 backward,例如 Q.sum().backward()

python 复制代码
external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)

external_grad = torch.tensor([1., 1.])

这一行创建了一个权重张量(通常称为v)。

它的维度必须与 Q 的维度完全一致。

这里设为 [1., 1.],意味着要将Q 中每个元素的梯度等权重地传递回去。
backward(gradient=external_grad)

计算Q关于网络参数的梯度。gradient 参数指定了 Q 中每个元素在反向传播时的"权重"或"初始变化率"。

梯度现在保存在 a.gradb.grad 中,两者权重相同。

python 复制代码
# 检查梯度是否正确
print(9*a**2 == a.grad)
print(-2*b == b.grad)

在数学中,如果 y = f(x),矩阵y是矩阵x的函数,那么y对x的导数是一个雅各比矩阵。

一般来说,torch.autograd 用于计算向量-雅可比积。也就是说,给定任意向量 𝑣,计算乘积 𝐽转置 ⋅ 𝑣

如果 v 是一个标量函数 l=g(y) 的梯度

那么根据链式法则,向量雅可比矩阵乘积将是 𝑙 对 𝑥⃗ 的梯度:

也就是说,矩阵y是矩阵x的函数,l是矩阵y的标量函数(即输入一个矩阵y,输出l是一个数值),torch.autograd 计算的是l 对x`的梯度。

(四)计算图

从概念上讲,autograd 会在一个有向无环图 (DAG) 中记录数据(张量)以及所有已执行的操作(以及产生的新张量),该图由 Function 对象组成。在这个DAG中,叶节点是输入张量,根节点是输出张量。通过从根节点到叶节点追踪这个图,你可以使用链式法则自动计算梯度。

在前向传播过程中,autograd 同时做两件事:

  • 执行请求的操作以计算结果张量
  • 在DAG中维护操作的梯度函数。
    反向传播从在DAG根节点上调用.backward()时开始。然后,autograd会:
  • 从每个梯度函数.grad_fn计算梯度
  • 将其累积到相应张量的.grad属性中
  • 使用链式法则,一直传播到叶张量
    以下是我们示例中DAG的可视化表示。在图中,箭头的指向是前向传播的方向,节点表示正向传播中每个操作的反向函数。蓝色的叶结点表示叶张量ab

    在PyTorch中,DAG(有向无环图)是动态的。需要注意的一个重要事项是,该图是从头开始重新创建的;每次调用.backward()后,autograd都会开始填充一个新图。这正是允许你在模型中使用控制流语句的原因;如果需要,你可以在每次迭代中更改形状、大小和操作。

从有向无环图中移除某些节点或元素的过程torch.autograd 会追踪所有配置项requires_grad为True的张量,requires_gradFalse的张量,表示不需要梯度,会将其从梯度计算DAG中排除。

python 复制代码
x = torch.rand(5, 5)
y = torch.rand(5, 5)
z = torch.rand((5, 5), requires_grad=True)

a = x + y
print(f"Does `a` require gradients?: {a.requires_grad}")
b = x + z
print(f"Does `b` require gradients?: {b.requires_grad}")

在神经网络中,不计算梯度的参数通常被称为冻结参数。冻结不需要梯度的参数可以减少自动梯度计算,提高模型的效率。

在微调中,我们通常会冻结模型的大部分参数,而只修改分类器层,以便对新的标签进行预测。让我们通过一个小例子来演示这一点。和之前一样,我们加载一个预训练的resnet18模型,并冻结所有的参数。

python 复制代码
from torch import nn, optim

model = resnet18(weights=ResNet18_Weights.DEFAULT)

# 冻结所有参数
for param in model.parameters():
    param.requires_grad = False

假设我们想在一个包含10个标签的新数据集上微调模型。在 ResNet 中,分类器是最后一个线性层 model.fc。下面创建一个简单的线性层作为我们的分类器。

python 复制代码
model.fc = nn.Linear(512, 10)

现在模型中除了model.fc,其他的参数都是冻结的,计算梯度全部根据model.fc的权重和偏置来计算

python 复制代码
# 优化分类器
optimizer = optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)

上面代码尽管我们在优化器中注册了所有参数,但只有分类器的权重和偏置是计算梯度(因此在梯度下降中更新)的参数。

三、神经网络

(一)简介

神经网络可以使用 torch.nn 包构建。
torch.nn 包依赖于 autograd 来定义模型并对其进行微分。一个 nn.Module 包含多个层,以及一个返回输出结果的 forward(input) 方法。

下面是一个对数字图片进行分类的网络:

这是一个简单的前馈网络。它接收输入,将其依次传递通过多个层,然后最终给出输出。

前馈神经网络(Feedforward Neural Network, FNN) 是深度学习中最基础、最古老的结构。它的名字"前馈"形象地描述了数据的流动方向:信号只从输入层流向输出层,没有任何反馈或循环。

神经网路典型的训练过程如下:

  • 定义一个包含一些可学习参数(或权重)的神经网络。可学习就是有规律,可以学习其中的规律。
  • 遍历输入数据集
  • 通过网络处理输入
  • 计算损失(输出结果与正确答案的差距有多大)
  • 将梯度反向传播到网络的参数中
  • 更新网络权重,通常使用一个简单的更新规则:weight = weight - learning_rate * gradient

(二)定义

python 复制代码
import torch
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        # 1 个输入图像通道(黑白图像), 6 个输出通道, 5x5 平方卷积核
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        # 仿射操作(线性变换): y = Wx + b
        # 输入维度为 16 * 5 * 5,输出维度为 120
        self.fc1 = nn.Linear(16 * 5 * 5, 120)  # 其中 5*5 是来自图像处理后的空间维度
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, input):
        # 卷积层 C1: 1 个输入图像通道, 6 个输出通道, 5x5 平方卷积。
        # 使用 ReLU 激活函数,并输出大小为 (N, 6, 28, 28) 的张量,
        # 其中 N 是批处理大小(Batch Size)。
        c1 = F.relu(self.conv1(input))
        
        # 下采样层 S2: 2x2 网格,纯函数式操作。
        # 该层没有任何参数,输出一个 (N, 6, 14, 14) 的张量。
        s2 = F.max_pool2d(c1, (2, 2))
        
        # 卷积层 C3: 6 个输入通道, 16 个输出通道, 5x5 平方卷积。
        # 使用 ReLU 激活函数,并输出一个 (N, 16, 10, 10) 的张量。
        c3 = F.relu(self.conv2(s2))
        
        # 下采样层 S4: 2x2 网格,纯函数式操作。
        # 该层没有参数,输出一个 (N, 16, 5, 5) 的张量。
        s4 = F.max_pool2d(c3, 2)
        
        # 展平操作(Flatten): 纯函数式操作,将多维张量拉平。
        # 输出一个 (N, 400) 的张量(16*5*5 = 400)。
        s4 = torch.flatten(s4, 1)
        
        # 全连接层 F5: 输入 (N, 400) 张量,
        # 输出 (N, 120) 张量,使用 ReLU 激活函数。
        f5 = F.relu(self.fc1(s4))
        
        # 全连接层 F6: 输入 (N, 120) 张量,
        # 输出 (N, 84) 张量,使用 ReLU 激活函数。
        f6 = F.relu(self.fc2(f5))
        
        # 全连接输出层: 输入 (N, 84) 张量,
        # 输出 (N, 10) 张量(对应 10 个数字类别)。
        output = self.fc3(f6)
        return output


net = Net()
print(net)

你只需要定义 forward 函数,反向传播函数(即计算梯度的部分)会自动通过 autograd 为你定义。你可以在 forward 函数中使用任何 Tensor 操作。

模型的可学习的参数会由 net.parameters() 方法返回。

python 复制代码
params = list(net.parameters())
print(len(params))
print(params[0].size())  # conv1's .weight
  • net.parameters(): 这是一个生成器(Generator),它会遍历模型中所有的参数。
  • list(...): 将生成器转换为列表,方便我们通过索引(如 [0], [1])直接访问特定的参数层。
    包含内容: 每一层通常包含两个参数:权重 (weight) 和 偏置 (bias)。

让我们尝试一个随机的32x32输入。注意:该网络 (LeNet) 的预期输入大小为32x32。 要在MNIST数据集上使用此网络,请将数据集中的图像调整大小为32x32。

python 复制代码
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
  • torch.randn: 创建一个张量,其元素服从均值为 0、方差为 1 的标准正态分布。
  • (1, 1, 32, 32): 这是张量的维度(Shape),对于处理图像的卷积神经网络,通常遵循 (N, C, H, W) 格式:
    • 1 (N): 批处理大小 (Batch Size)。即便只有一张图片,也需要这个维度。
    • 1 ©: 通道数 (Channels)。对应 conv1 定义的输入通道(黑白图像)。
    • 32, 32 (H, W): 图像的高度和宽度。LeNet-5 最初设计的输入尺寸就是 32×32。

将所有参数的梯度缓冲区清零,并使用随机梯度进行反向传播:

python 复制代码
net.zero_grad()
out.backward(torch.randn(1, 10))

在进一步进行之前,让我们回顾一下你目前为止见过的所有类。

  • torch.Tensor - 一个支持自动求导操作(如 backward())的多维数组。同时也会保存相对于该张量的梯度。
  • nn.Module - 神经网络模块。封装参数的便捷方式,并提供将其移动到 GPU、导出、加载等辅助功能。
  • nn.Parameter - 一种特殊的 Tensor,当其作为属性分配给一个 Module 时,会自动注册为参数。
  • autograd.Function - 实现自动求导操作的前向和反向定义。每个 Tensor 操作都会创建至少一个 Function 节点,该节点连接到创建 Tensor 的函数并编码其历史记录。
    至此已经实现了定义神经网络、处理输入以及调用反向传播。
    下面来看一下如何计算损失和更新网络的权重。

(三)损失函数

损失函数接收输入对,计算输出的预测值和真实值的之间的差距。nn包提供了若干个损失函数,其中最简单的是 nn.MSELoss ,这个函数计算的是预测值和真实值之间的均方误差。

例如:

python 复制代码
# 获取模型的预测结果
output = net(input)
# 创建一个含有10个随机数的向量
target = torch.randn(10)  
# 将其形状改为 [1, 10] 计算损失必须保证output和target形状一致 1表示批次,10表示特征维度
target = target.view(1, -1)
# 实例化"均方误差"准则
criterion = nn.MSELoss()
# 将预测值和目标值传入,返回一个包含损失值的张量
loss = criterion(output, target)
print(loss)

loss损失长下面这样

此时得到的 loss 是一个带有梯度信息的 Tensor。在后续的训练步骤中,会调用 loss.backward() 来根据这个误差计算梯度,进而更新网络权重。

现在,如果你沿着反向传播方向追踪损失,使用它的 .grad_fn 属性,你将会看到一个如下的计算图:

python 复制代码
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
      -> flatten -> linear -> relu -> linear -> relu -> linear
      -> MSELoss
      -> loss

所以,当我们调用loss.backward()时,整个计算图会相对于神经网络的参数进行微分,并且图中所有配置了requires_grad=True的张量的.grad属性所指向的张量都会累积梯度。

为了便于说明,让我们逐步反向传播几个步骤:

python 复制代码
print(loss.grad_fn)  # MSELoss
print(loss.grad_fn.next_functions[0][0])  # Linear 
print(loss.grad_fn.next_functions[0][0].next_functions[0][0])  # ReLU

(四)反向传播

反向传播误差需要使用 loss.backward() 方法。不过你需要清除现有的梯度,否则梯度会被累加到现有的梯度上。现在我们将调用loss.backward(),并查看反向传播前后conv1的偏置梯度。

python 复制代码
net.zero_grad()     # 将所有参数的梯度缓冲区清零

print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)

loss.backward() # 自动求导 从loss损失函数值开始,根据链式法则逆向遍历计算图,计算出的每个参数的梯度,会被存储到参数对应的.grad属性中

print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

到目前为止,我们已经了解了如何使用损失函数。下面来看一下如何更新神经网络中的权重。

(五)更新权重

实践中最简单的更新规则是随机梯度下降法(SGD):

python 复制代码
weight = weight - learning_rate * gradient

我们可以用简单的Python代码实现这一点:

python 复制代码
learning_rate = 0.01 # 学习率
for f in net.parameters():
    f.data.sub_(f.grad.data * learning_rate)
  • learning_rate = 0.01
    • 作用:定义学习率。它决定了模型在优化过程中沿着梯度反方向"迈出的步子"有多大。
    • 意义:太大可能导致错过最优解(在谷底左右跳动),太小则导致收敛过慢。
  • for f in net.parameters():
    • net.parameters():这是一个生成器,它会遍历神经网络 net 中所有需要训练的参数(包括权重 weights 和偏置 biases)。
  • f:代表当前的某一个参数张量(Tensor)。
  • f.data.sub_(f.grad.data * learning_rate)
    这是最关键的一行,执行了实际的数学运算:
    • f.grad.data:获取该参数在反向传播(loss.backward())中计算出来的梯度。梯度指明了函数值上升最快的方向。
    • learning_rate:将梯度乘以学习率,得到更新的幅度。
    • .sub_():这是 PyTorch 中的一个原地(in-place)减法操作(带有下划线后缀的方法通常表示直接修改原数据)。
    • 整体含义:将参数 f 减去"学习率 × 梯度"。因为梯度是上升方向,减去它就是向下降的方向移动,从而降低损失函数的值。
      请注意,梯度缓冲区必须使用 optimizer.zero_grad() 手动设置为零。这是因为梯度会像反向传播部分解释的那样累积。

四、训练分类器

(一)如何处理数据

通常,处理图片、文本、音频和视频数据可以使用标准的python包,可以将数据加载成 numpy 数组。然后你可以把 npmpy 数组转化为 torch.*Tensor

  • 图像使用Pillow, OpenCV包
  • 音频使用scipy 和 librosa 的包
  • 对于文本,原始 Python 还是 Cython ,或者 NLTK 和 SpaCy 都可以。
    特别针对视觉,我们创建了一个名为 torchvision 的软件包,它包含用于常见数据集(如 ImageNet、CIFAR10、MNIST 等)的数据加载器以及用于图像的数据转换器,即 torchvision.datasetstorch.utils.data.DataLoader
    这提供了极大的便利,并避免重复造轮子。
    下面我们将使用CIFAR10数据集。它包含以下类别:'飞机'、'汽车'、'鸟'、'猫'、'鹿'、'狗'、'青蛙'、'马'、'船'、'卡车'。CIFAR-10中的图像大小为3x32x32,即大小为32x32像素的3通道彩色图像。

(一)训练一个图像分类器

我们将按以下顺序执行步骤:

  1. 使用 torchvision 加载并标准化 CIFAR10 训练和测试数据集。
  2. 定义一个卷积神经网络。
  3. 定义一个损失函数。
  4. 在训练数据上训练网络。
  5. 在测试数据上测试网络。

① 加载和归一化CIFAR10

使用 torchvision,可以非常容易地加载 CIFAR10 数据集。

python 复制代码
import torch
import torchvision
import torchvision.transforms as transforms

torchvision 数据集输出的是范围在 [0, 1] 的 PILImage 图像。我们将它们转换为归一化范围在 [-1, 1] 的 Tensor。

python 复制代码
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

batch_size = 4

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size,
                                          shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size,
                                         shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

这段代码是使用 PyTorch 进行深度学习开发时非常典型的数据准备阶段。它主要完成了数据的下载、预处理(标准化)以及加载器的构建。

可以将其分为四个核心部分来讲解:

  1. 图像预处理 (Transforms)
python 复制代码
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

这部分定义了对原始图像进行的"流水线"操作:
transforms.ToTensor():

作用一:将 PIL 图像或 NumPy 数组转换为 FloatTensor。

作用二:将像素值从 [0,255] 归一化到 [0.0,1.0] 之间。
transforms.Normalize(...):

执行减均值、除以标准差的操作:output=(input−mean)/std

这里传入两个元组 (0.5, 0.5, 0.5) 分别对应 RGB 三个通道。

效果:将图像像素值从 [0,1] 范围转换到 [−1,1] 范围。这有助于神经网络在训练时更快收敛。

  1. 定义批次大小 (Batch Size)
python 复制代码
batch_size = 4
  • 这表示每次网络前向传播和反向传播时,会同时处理 4 张图片。
  • 在实际训练中,如果 GPU 显存很大,可以调高这个值(如 32, 64 或 128)提高处理效率。
  1. 加载数据集 (Datasets)
python 复制代码
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)

这里使用了 PyTorch 内置的 CIFAR10 数据集:

  • root='./data': 指定数据下载和存放的路径。
  • train=True/False: 指定是加载训练集(50,000张)还是测试集(10,000张)。
  • download=True: 如果本地没有数据,程序会自动从互联网下载。
  • transform=transform: 应用我们第一步定义的预处理逻辑。
  1. 数据加载器 (DataLoader)
python 复制代码
trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size,
                                          shuffle=True, num_workers=2)
testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size,
                                         shuffle=False, num_workers=2)

DataLoader 是 PyTorch 中非常强大的工具,负责实际给模型"喂"数据:

  • shuffle=True: 在每个训练周期(Epoch)开始时打乱数据。这对于训练非常重要,可以防止模型产生"位置依赖"或过拟合特定顺序。
  • num_workers=2: 使用 2 个子进程来并行加载数据。这可以加快数据读取速度,防止 CPU 读取数据成为模型训练的瓶颈。
  1. 类别定义
python 复制代码
classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

CIFAR10 数据集的标签是 0-9 的整数。这个元组的作用是建立一个索引到名字的映射,方便后续我们将预测结果(数字)转换成人类可读的名称(如"猫"、"狗")。

总结

这段代码执行后,你就拥有了两个迭代器 trainloader 和 testloader。你可以直接通过循环来获取数据:

python 复制代码
for images, labels in trainloader:
    # images 的形状是 [4, 3, 32, 32] -> (batch_size, channels, height, width)
    # labels 的内容是 4 个对应的类别索引
    ...

下面就可以看几个训练图像的效果

python 复制代码
import matplotlib.pyplot as plt
import numpy as np

# functions to show an image


def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()


# get some random training images
dataiter = iter(trainloader)
images, labels = next(dataiter)

# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(' '.join(f'{classes[labels[j]]:5s}' for j in range(batch_size)))

这段代码是使用 PyTorch 进行深度学习(通常是处理 CIFAR-10 等数据集)时,非常典型的数据可视化步骤。它的核心作用是:从训练数据集中随机抽取一组图片,将它们拼接成一张大图展示出来,并打印出对应的类别标签。

  1. 定义显示函数 imshow
python 复制代码
def imshow(img):
    img = img / 2 + 0.5     # 反归一化 (Unnormalize)
    npimg = img.numpy()
    # 将 [C, H, W] 转换为 [H, W, C]
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()
  • 反归一化 (img / 2 + 0.5):在预处理数据时,通常会将图片像素值从 [0,1] 归一化到 [−1,1](均值为 0.5,标准差为 0.5)。为了让图片能正常显示,需要将其还原回 [0,1] 范围。
  • img.numpy():将 PyTorch 的 Tensor(张量)转换为 NumPy 数组,因为 Matplotlib 绘图库使用的是 NumPy。
  • np.transpose(..., (1, 2, 0)):PyTorch 的图片格式是 [通道数 C, 高度 H, 宽度 W]。Matplotlib 的要求是 [高度 H, 宽度 W, 通道数 C]。此操作将维度顺序重新排列,以便正常显示颜色。
  1. 获取随机训练图像
python 复制代码
dataiter = iter(trainloader)
images, labels = next(dataiter)
  • iter(trainloader):将 trainloader(数据加载器)包装成一个迭代器。
  • next(dataiter):从中抓取"一批"(Batch)数据。images 包含了这批图片的像素数据,labels 包含了这些图片对应的类别索引。
  1. 展示图片
python 复制代码
# 将这一批图片拼接成网格
imshow(torchvision.utils.make_grid(images))
  • torchvision.utils.make_grid(images):这是一个方便的函数,它把 images 里的多张图片(通常是一个 batch,比如 4 张或 64 张)像贴瓷砖一样拼成一张长方形的大图。
  • 调用前面定义的 imshow 函数进行显示。
  1. 打印标签
python 复制代码
print(' '.join(f'{classes[labels[j]]:5s}' for j in range(batch_size)))
  • classes[labels[j]]labels[j] 是类别的数字索引(如 0, 1, 2),通过 classes 列表找到它对应的名字(如 'cat', 'dog', 'ship')。
  • f'...:5s':格式化字符串,保证每个单词占用 5 个字符宽度,使打印出来的文字能和上面的图片对齐。
  • ' '.join(...):将这些名字用空格连接成一行字符串并打印。

② 定义卷积神经网络

复制之前"神经网络"章节中的神经网络,并对其进行修改,使其能够处理3通道图像(而不是像之前定义的那样处理1通道图像)。

python 复制代码
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 6, 5) # 定义卷积层
        self.pool = nn.MaxPool2d(2, 2) # 最大池化
        self.conv2 = nn.Conv2d(6, 16, 5) # 卷积层
        self.fc1 = nn.Linear(16 * 5 * 5, 120) # 全连接层
        self.fc2 = nn.Linear(120, 84) # 全连接层
        self.fc3 = nn.Linear(84, 10) # 全连接层

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x))) # 池化层
        x = self.pool(F.relu(self.conv2(x))) # 池化层
        x = torch.flatten(x, 1) # 展平
        x = F.relu(self.fc1(x)) # 激活函数
        x = F.relu(self.fc2(x)) # 激活函数
        x = self.fc3(x) # 全连接
        return x # 返回结果


net = Net()

最上面三行是定义网络的固定写法。

③ 定义损失函数和优化器

使用分类交叉熵损失和带momentum的SGD。

  1. 分类交叉熵损失 (Categorical Cross-Entropy Loss)
    它是什么:
    这是一种衡量预测概率分布与真实标签概率分布之间"距离"的函数。主要用于多分类任务(如识别手写数字 0-9)。
    核心原理:
    输入: 神经网络最后一层通过 Softmax 激活函数输出的概率值(所有类别概率之和为 1)。
    目标: 拉近模型输出概率与真实 One-hot 标签(正确类别为1,其余为0)的距离。
  2. 带动量的 SGD (SGD with Momentum)
    它是什么:
    普通的 SGD(随机梯度下降)在更新参数时只看当前的梯度,容易在沟壑中震荡,或者陷入局部最小值。Momentum(动量) 借鉴了物理学中的惯性概念。
    核心原理:
    它记录了之前的更新方向。如果当前的梯度方向与之前的更新方向一致,则加大步长;如果不一致,则起到平滑作用。
python 复制代码
import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

④ 训练网络

事情开始变得有趣起来了。我们只需要遍历我们的数据迭代器,并将输入提供给网络并进行优化。

python 复制代码
for epoch in range(2):  # loop over the dataset multiple times

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # get the inputs; data is a list of [inputs, labels]
        inputs, labels = data

        # zero the parameter gradients
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # print statistics
        running_loss += loss.item()
        if i % 2000 == 1999:    # print every 2000 mini-batches
            print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
            running_loss = 0.0

print('Finished Training')

这段代码是使用 PyTorch 框架训练神经网络的核心循环(Training Loop)。它展示了模型如何通过接触数据、计算误差并调整参数来学习。

  1. 外层循环:Epoch(轮次)
python 复制代码
for epoch in range(2):  # loop over the dataset multiple times
  • Epoch 代表将整个训练数据集完整地送入神经网络训练一次的过程。
  • range(2) 表示这段代码会将整个数据集训练 2 轮。通常在实际项目中需要更多轮次(如 10、50 或 100),直到模型收敛。
  1. 内层循环:Batch(批次)
python 复制代码
for i, data in enumerate(trainloader, 0):
    # get the inputs; data is a list of [inputs, labels]
    inputs, labels = data
  • trainloader 是一个迭代器,它将数据集切分成许多小份(称为 Mini-batches)。
  • 直接训练整个数据集会消耗巨大内存,因此我们分批处理。
  • inputs 是图像或特征数据,labels 是这些数据对应的正确标签(真实答案)。
  1. 核心训练四部曲
    这是深度学习最关键的步骤:

第一步:梯度清零

python 复制代码
optimizer.zero_grad()
  • 在 PyTorch 中,梯度是累加的。如果不清零,当前批次的梯度会和上一个批次的梯度叠加。为了正确计算当前批次的更新量,必须先清零。

第二步:前向传播 (Forward Pass)

python 复制代码
outputs = net(inputs)
  • 将数据 inputs 输入模型 net,得到模型的预测结果 outputs

第三步:计算损失 (Loss) 与反向传播 (Backward Pass)

python 复制代码
loss = criterion(outputs, labels)
loss.backward()
  • criterion(损失函数)衡量模型预测值与真实标签之间的差距(误差)。
  • loss.backward() 利用链式法则计算损失函数对模型每个参数的梯度(即确定参数该往哪个方向调整,调整多少)。

第四步:更新参数 (Optimize)

python 复制代码
optimizer.step()
  • 优化器(如 SGD 或 Adam)根据刚才计算出的梯度,实际动手修改模型内部的参数(权重),使下一次预测更准确。
  1. 统计与打印
python 复制代码
running_loss += loss.item()
if i % 2000 == 1999:    # 每 2000 个 mini-batches 打印一次
    print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
    running_loss = 0.0
  • loss.item() 获取当前批次的损失数值。
  • 为了观察模型是否在进步,代码每处理 2000 个批次就打印一次平均损失。
  • 注意:如果损失值(loss)在不断下降,说明模型正在有效学习。

    保存训练好的模型:
python 复制代码
PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)

会在根目录中多出一个这个文件

  1. 测试网络
    我们已经对训练数据集进行了两次训练迭代。但是我们需要检查网络是否真正学习到了东西。
    我们将通过预测神经网络输出的类别标签,并将其与真实标签进行比对来检查这一点。如果预测正确,我们将把该样本添加到正确预测的列表中。
    好的,第一步。让我们先展示测试集中的一张图像,以便熟悉一下。
python 复制代码
dataiter = iter(testloader)
images, labels = next(dataiter)

# print images
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join(f'{classes[labels[j]]:5s}' for j in range(4)))

接下来,让我们重新加载我们保存的模型(注意:在这里保存和重新加载模型并不是必要的,这样做只是为了演示如何操作):

python 复制代码
net = Net()
net.load_state_dict(torch.load(PATH, weights_only=True))

好的,现在让我们看看神经网络认为以上这些例子是什么:

python 复制代码
outputs = net(images)

输出是10个类别的能量值。一个类别的能量值越高,网络就越认为图像属于该特定类别。所以,让我们获取能量最高的索引:

python 复制代码
_, predicted = torch.max(outputs, 1)

print('Predicted: ', ' '.join(f'{classes[predicted[j]]:5s}'
                              for j in range(4)))

下面的内容是在GPU上训练,由于俺是小辣鸡macbook就不操作了。

这一篇可太硬核了,虽然说感觉很多云里雾里,但只是不熟悉,多来几遍就好了。B站上炮哥讲的pytorch挺好的,推荐,结合炮哥的视频会更好理解原理。

相关推荐
墨染天姬17 小时前
【AI】OCR开源模型排行
人工智能·开源·ocr
幻云201017 小时前
Python机器学习:从入门到资深
人工智能·python
泰迪智能科技17 小时前
分享|企业数据挖掘平台产品功能
人工智能·数据挖掘
散峰而望17 小时前
【算法竞赛】顺序表和vector
c语言·开发语言·数据结构·c++·人工智能·算法·github
FL1717131417 小时前
Geometric Control
人工智能·算法
郑州光合科技余经理17 小时前
架构解析:同城本地生活服务o2o平台海外版
大数据·开发语言·前端·人工智能·架构·php·生活
小小工匠17 小时前
LLM - 将业务 SOP 变成 AI 能力:用 Skill + MCP 驱动 Spring AI 应用落地不完全指南
人工智能·skill·spring ai·mcp
一条咸鱼_SaltyFish17 小时前
[Day12] 合同审查引擎开发中的技术挑战与解决之道 contract-review-engine
开发语言·人工智能·程序人生·开源软件·ddd·个人开发·ai编程
百***243717 小时前
GPT-5.2国内稳定调用指南:API中转适配与成本管控实操
大数据·人工智能