深度学习笔记(三)pytorch基础

本文最后更新于 2024年6月25日 早上

记录pytorch的安装和一个训练例子

0 安装pytorch

  • 进入官网,根据上面安装的CUDA版本选择一个对应的。

  • 进入conda环境进行安装

    1
    2
    3
    4
    如果之前安装过则先删除
    pip uninstall torch torchvision torchaudio
    进行安装
    pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
  • 测试

    1
    2
    import torch
    torch.cuda.is_available()

1 导入数据集

TorchText、TorchVision和TorchAudio是三个常用的扩展库,它们分别用于文本处理、计算机视觉和音频处理。

  1. TorchText:TorchText是一个用于自然语言处理(NLP)的库,提供了一些常用的数据集和预处理工具,例如文本分类、序列标注、机器翻译等。使用TorchText可以方便地加载和处理文本数据,并将其转换为PyTorch张量,以便进行深度学习模型的训练和评估。

  2. TorchVision:TorchVision是一个用于计算机视觉的库,提供了一些常用的数据集和预处理工具,例如图像分类、目标检测、图像分割等。使用TorchVision可以方便地加载和处理图像数据,并将其转换为PyTorch张量,以便进行深度学习模型的训练和评估。

  3. TorchAudio:TorchAudio是一个用于音频处理的库,提供了一些常用的数据集和预处理工具,例如音频分类、语音识别、音频增强等。使用TorchAudio可以方便地加载和处理音频数据,并将其转换为PyTorch张量,以便进行深度学习模型的训练和评估。

这里使用torchvision库的FashionMNIST数据集,它是一个图像分类的数据集,包含了10个类别的灰度图像,每个类别有6000张训练图像和1000张测试图像,图像大小为28x28像素。

引入相关包

1
2
3
4
5
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor

下载数据集

1
2
3
4
5
6
7
8
9
10
11
12
training_data = datasets.FashionMNIST(
    root="./data",
    train=True,
    download=True,
    transform=ToTensor(),
)
test_data = datasets.FashionMNIST(
    root="./data",
    train=False,
    download=True,
    transform=ToTensor(),
)

加载

1
2
3
batch_size = 64 # 每次提取64个特征和标签
train_dataloader = DataLoader(training_data, batch_size=batch_size)
test_dataloader = DataLoader(test_data, batch_size=batch_size)

打印数据集的信息

1
2
3
4
5
6
7
8
9
10
# X.shape : 数据的形状,N 表示 batch size,C 表示通道数,H 和 W 分别表示图像的高度和宽度
# y.shape 表示测试数据集中一个 batch 的标签的形状,其中 y.dtype 表示标签的数据类型
for X, y in test_dataloader:
    print(f"Shape of X [N, C, H, W]: {X.shape}")
    print(f"Shape of y: {y.shape} {y.dtype}")
    break

Shape of X [N, C, H, W]: torch.Size([64, 1, 28, 28])
Shape of y: torch.Size([64]) torch.int64

2 创建模型

2.1 选择训练的设备

1
2
3
4
5
6
7
8
9
10
device = (
    "cuda"
    if torch.cuda.is_available()
    else "mps"
    if torch.backends.mps.is_available()
    else "cpu"
)
print(f"Using {device} device")

Using cuda:0 device

2.2 定义模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class NeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        # 将输入的二维图像数据平成一维向量
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            # 第一个全连接层,输入大小为28*28,输出大小为512
            nn.Linear(28*28, 512),
            # 激活函数
            nn.ReLU(),
            # 第二个全连接层,输入大小为512,输出大小为512
            nn.Linear(512, 512),
            # 激活函数
            nn.ReLU(),
            # 第三个全连接层,输入大小为512,输出大小为10(对应10个类别)
            nn.Linear(512, 10)
        )
    # 向前传播
    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits
       
model = NeuralNetwork().to(device)
print(model)

NeuralNetwork(
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear_relu_stack): Sequential(
(0): Linear(in_features=784, out_features=512, bias=True)
(1): ReLU()
(2): Linear(in_features=512, out_features=512, bias=True)
(3): ReLU()
(4): Linear(in_features=512, out_features=10, bias=True)
)
)

对上述代码的一些说明:

  • nn.Module
    nn.Module是PyTorch中的一个基类,用于定义神经网络模型。所有的神经网络模型都应该继承自nn.Module,并实现其构造函数__init__()和向前传播函数forward()。通过继承nn.Module,我们可以方便地使用PyTorch提供的各种优化器、损失函数等工具来训练和优化我们的神经网络模型

  • nn.Flatten()
    nn.Flatten()是 PyTorch 中的一个层,用于将输入数据展平成一维向量。它没有任何可学习的参数,只是对输入数据进行形状变换。

    具体来说,如果输入数据的形状为 (N, C, H, W),其中 N 表示 batch size,C 表示通道数,HW 分别表示图像的高度和宽度,那么经过 nn.Flatten() 层后,输出数据的形状为 (N, C*H*W),即将每个样本的所有特征都展开成了一维向量。

    例如,对于一个大小为 (64, 1, 28, 28) 的输入数据,经过 nn.Flatten() 层后,输出数据的形状为 (64, 784),即将每个样本的 28x28=784 个像素值展开成了一维向量。这样做的目的是为了方便后续的全连接层处理,因为全连接层需要将输入数据展平成一维向量才能进行计算。

  • 全连接层
    全连接层(Fully Connected Layer),也称为稠密层(Dense Layer),是神经网络中最基本的一种层类型。它的作用是将输入数据的所有特征都连接到输出数据的每个神经元上,从而实现特征的组合和变换。

    具体来说,全连接层的输入数据通常是一个一维向量,每个元素对应着输入数据的一个特征。全连接层的输出数据也是一个一维向量,其中每个元素对应着输出数据的一个神经元。全连接层中的每个神经元都与输入数据的所有特征相连,因此可以学习到输入数据中不同特征之间的关系,从而实现更复杂的特征表示和分类决策。

    在深度神经网络中,通常会使用多个全连接层来构建模型。例如,在图像分类任务中,可以先使用卷积层提取图像的局部特征,然后再通过若干个全连接层将这些特征进行组合和变换,最终得到分类结果。

  • 为什么需要多个全连接层

    原因是为了增加模型的表达能力和拟合能力。

    具体来说,如果只使用一个全连接层将输入数据映射到输出数据,那么模型的表达能力会受到限制,可能无法学习到复杂的特征表示和分类决策。相反,使用多个全连接层可以让模型更加灵活地组合和变换输入数据的特征,从而提高模型的表达能力和拟合能力。

    此外,使用多个全连接层还可以使得模型的训练更加稳定和高效。通过逐层地进行特征变换,可以逐步地减少输入数据的维度,从而降低模型的复杂度和计算量,同时也可以避免梯度消失或爆炸等问题,提高模型的训练效率和收敛速度。

    因此,在实际应用中,通常会使用多个全连接层来构建深度神经网络模型,以提高模型的表达能力、拟合能力和泛化能力。

  • 激活函数作用

    在神经网络中,激活函数的作用是引入非线性变换,从而使得模型可以学习到更加复杂的特征表示和分类决策。如果没有激活函数,那么多个全连接层的组合就相当于一个单独的全连接层,无法实现非线性变换。

    ReLU(Rectified Linear Unit)是一种常用的激活函数。它的优点是计算简单、收敛速度快、不会出现梯度消失等问题,并且在实际应用中表现良好。

    在上述代码中,使用了多个 ReLU 激活函数来增加模型的非线性能力。具体来说,每个全连接层的输出都先经过一个 ReLU 激活函数,再传递给下一层全连接层进行处理。这样做的目的是为了让模型可以学习到更加复杂的特征表示和分类决策,从而提高模型的表达能力和拟合能力。

    同时,使用多个 ReLU 激活函数还可以避免梯度消失或爆炸等问题,从而提高模型的训练效率和收敛速度。因此,在实际应用中,ReLU 是一种常用的激活函数,尤其是在深度神经网络中。

  • nn.Linear(28*28, 512)中为什么输出是512

    在神经网络中,每个全连接层的输出大小是由设计者自行决定的。在上述代码中,第一个全连接层的输入大小为 28x28=784,输出大小为 512,这是一种常见的设置。

    具体来说,输出大小的选择通常取决于以下几个因素:

    1. 数据集的大小和复杂度:如果数据集比较大或者比较复杂,那么需要更多的参数来拟合数据集的特征,从而提高模型的表达能力和拟合能力。

    2. 模型的深度和宽度:如果模型比较深或者比较宽,那么需要更多的参数来组合和变换输入数据的特征,从而提高模型的表达能力和拟合能力。

    3. 计算资源的限制:如果计算资源有限,那么需要控制模型的参数数量,以避免过拟合或者训练时间过长等问题。

    因此,在实际应用中,输出大小的选择需要根据具体情况进行调整,以达到最佳的性能和效果。

3 损失函数和优化器

3.1 损失函数

1
loss_fn = nn.CrossEntropyLoss()

这里定义了一个交叉熵损失函数,用于计算模型预测结果与真实标签之间的差异。
交叉熵损失函数通常用于多分类问题中,它的计算方式是将模型预测的概率分布与真实标签的概率分布进行比较,并计算它们之间的交叉熵。交叉熵损失函数的值越小,说明模型预测的概率分布与真实标签的概率分布越接近,模型的性能也就越好。

3.2 优化器

1
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)

这里定义了一个随机梯度下降(SGD)优化器,用于更新神经网络模型的参数。
SGD是一种常用的优化算法,它的基本思想是通过不断地迭代来更新模型参数,使得损失函数的值逐渐减小。具体来说,在每次迭代中,SGD会计算当前样本的梯度,并根据学习率lr和梯度大小来更新模型参数。由于SGD只考虑当前样本的梯度,因此其计算速度较快,但可能会受到噪声的影响,导致收敛速度较慢或者陷入局部最优解。

在这行代码中,optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)表示我们将模型的所有可训练参数(即权重和偏置)传递给SGD优化器,并设置学习率为1e-3。在训练过程中,我们可以调用optimizer.step()方法来更新模型参数,从而使得模型的预测结果更加准确。

4 训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def train(dataloader, model, loss_fn, optimizer):
    #数据集的大小
    size = len(dataloader.dataset)
    #将模型设置为训练模式
    model.train()
    # 遍历每个批次数据
    for batch, (X, y) in enumerate(dataloader):
        # 将X和y传递给显卡
        X, y = X.to(device), y.to(device)
        # 对X进行预测
        pred = model(X)
        # 计算预测的误差值
        loss = loss_fn(pred, y)
        # 通过反向传递计算梯度
        loss.backward()
        # 使用优化器更新模型参数
        optimizer.step()
        # 清空梯度
        optimizer.zero_grad()
        # 每处理100个批次数据,打印一次损失值和已经处理的样本数量
        # .item()是将张量转换为标量
        if batch % 100 == 0:
            loss, current = loss.item(), (batch + 1) * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")

其中的参数dataloader, model, loss_fn, optimizer就是前面的数据集、模型、损失函数和优化器。
size表示的是数据总数量,而batch表示的是每次从数据集中取出用来训练的数量
使用enumerate(dataloader)是为了方便记录当前处理到了那个批次,不使用enumerate则只能得到每个批次,而无法得到当前处理到了第几个批次,这样就无法方便地进行日志输出和进度跟踪。
model.train()切换训练模式后,能够计算梯度进行反向传播更新模型参数,而评估模式model.eval()则不会更新模型参数。

5 评估

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    # 批次数
    num_batches = len(dataloader)
    # 模型设置为评估模式
    model.eval()
    # 测试的损失和正确预测数
    test_loss, correct = 0, 0
    with torch.no_grad():
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            # 统计正确预测样本数
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    # 平均损失和准确率
    test_loss /= num_batches
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")

该函数用于输出基于测试样本得出地平均损失和准确率。参数对比训练缺少了优化器,因为这里不需要修改模型参数了。

  • torch.no_grad()
    一个上下文管理器,用于禁用 PyTorch 张量(tensor)的梯度计算。在使用该上下文管理器时,所有在其内部进行的操作都不会被记录到计算图中,也不会影响模型参数的更新。
    通常情况下,当我们对模型进行评估或测试时,不需要计算梯度,因为这会浪费计算资源并且可能导致模型出现过拟合等问题。因此,在这些场景下,可以使用 torch.no_grad() 上下文管理器来禁用梯度计算,以便加快模型的前向计算速度。

6 进行训练和评估

1
2
3
4
5
6
epochs = 5
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model, loss_fn)
print("Done!")

执行5轮,日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
Epoch 1
-------------------------------
loss: 2.306058 [ 64/60000]
loss: 2.292524 [ 6464/60000]
loss: 2.277905 [12864/60000]
loss: 2.278586 [19264/60000]
loss: 2.252084 [25664/60000]
loss: 2.238573 [32064/60000]
loss: 2.238545 [38464/60000]
loss: 2.213803 [44864/60000]
loss: 2.210443 [51264/60000]
loss: 2.174720 [57664/60000]
Test Error:
Accuracy: 43.2%, Avg loss: 2.174277

Epoch 2
-------------------------------
loss: 2.187514 [ 64/60000]
loss: 2.177330 [ 6464/60000]
loss: 2.125688 [12864/60000]
loss: 2.145923 [19264/60000]
loss: 2.095594 [25664/60000]
loss: 2.049253 [32064/60000]
loss: 2.071624 [38464/60000]
loss: 2.007783 [44864/60000]
loss: 2.011179 [51264/60000]
loss: 1.931144 [57664/60000]
Test Error:
Accuracy: 52.8%, Avg loss: 1.938487

Epoch 3
-------------------------------
loss: 1.972887 [ 64/60000]
loss: 1.945989 [ 6464/60000]
loss: 1.835424 [12864/60000]
loss: 1.872588 [19264/60000]
loss: 1.765438 [25664/60000]
loss: 1.719593 [32064/60000]
loss: 1.729420 [38464/60000]
loss: 1.640577 [44864/60000]
loss: 1.657937 [51264/60000]
loss: 1.536771 [57664/60000]
Test Error:
Accuracy: 59.6%, Avg loss: 1.563752

Epoch 4
-------------------------------
loss: 1.633100 [ 64/60000]
loss: 1.595681 [ 6464/60000]
loss: 1.445400 [12864/60000]
loss: 1.511869 [19264/60000]
loss: 1.390925 [25664/60000]
loss: 1.384651 [32064/60000]
loss: 1.391175 [38464/60000]
loss: 1.321024 [44864/60000]
loss: 1.352417 [51264/60000]
loss: 1.240162 [57664/60000]
Test Error:
Accuracy: 63.0%, Avg loss: 1.273700

Epoch 5
-------------------------------
loss: 1.355194 [ 64/60000]
loss: 1.333768 [ 6464/60000]
loss: 1.167527 [12864/60000]
loss: 1.274431 [19264/60000]
loss: 1.145151 [25664/60000]
loss: 1.169585 [32064/60000]
loss: 1.190854 [38464/60000]
loss: 1.127306 [44864/60000]
loss: 1.165686 [51264/60000]
loss: 1.075500 [57664/60000]
Test Error:
Accuracy: 64.7%, Avg loss: 1.099728

7 保存模型和加载模型

1
2
3
4
5
6
7
# 保存模型
torch.save(model.state_dict(), "./model/model.pth")
print("Saved PyTorch Model State to model.pth")

# 加载模型
model = NeuralNetwork().to(device)
model.load_state_dict(torch.load("./model/model.pth"))

model.pth保存了模型的所有参数和权重,所有在加载的时候需要先定义一摸一样的模型才能导入参数和权重。

另一种是保存整个模型和参数

1
2
3
4
torch.save(model, "./model/model2.pth")
print("Saved PyTorch Model to model.pth")  

model = torch.load("./model/model2.pth")

8 使用模型进行预测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 类别,来源于训练的数据集
classes = [
    "T-shirt/top",
    "Trouser",
    "Pullover",
    "Dress",
    "Coat",
    "Sandal",
    "Shirt",
    "Sneaker",
    "Bag",
    "Ankle boot",
]
# 仍然要先切换到预测模式
model.eval()
# 从测试数据集中取出第一张,x是图,y是标签
x, y = test_data[0][0], test_data[0][1]
with torch.no_grad():
    x = x.to(device)
    pred = model(x)
    predicted, actual = classes[pred[0].argmax(0)], classes[y]
    print(f'Predicted: "{predicted}", Actual: "{actual}"')
  • pred[0].argmax(0)
    pred[0].argmax(0)的意思是在模型的输出张量pred中,找到最大值所在的索引。这个索引对应了预测结果中概率最高的类别。

    具体来说,pred是一个形状为(batch_size, num_classes)的张量,其中batch_size表示输入数据的批次大小,num_classes表示分类问题中的类别数。在这里,batch_size为1,因为我们只取出了测试数据集中的一张图片进行预测。

    pred[0]表示取出第一张图片的预测结果,它是一个形状为(num_classes,)的张量。.argmax(0)方法返回该张量中最大值所在的索引,也就是预测结果中概率最高的类别的索引。

    最后,将这个索引作为下标,从classes列表中取出对应的类别名称,即可得到模型的预测结果

9 链接


深度学习笔记(三)pytorch基础
https://blog.kala.love/posts/356d96d2/
作者
久远·卡拉
发布于
2023年6月6日
许可协议