【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挺好的,推荐,结合炮哥的视频会更好理解原理。

相关推荐
NAGNIP4 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab5 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab5 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP9 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年9 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼9 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS9 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区11 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈11 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang11 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx