从零到英雄:用PyTorch创建你的第一个机器学习模型

用PyTorch创建你的第一个机器学习模型

 

动机

 

PyTorch是最广泛使用的基于Python的深度学习框架。它为所有机器学习架构和数据流水线提供了巨大的支持。在本文中,我们将介绍所有框架基础知识,以帮助您开始实现自己的算法。

所有机器学习实现都有4个主要步骤:

  • 数据处理
  • 模型架构
  • 训练循环
  • 评估

我们在PyTorch中实现自己的MNIST图像分类模型时将经历所有这些步骤。这将使您熟悉机器学习项目的一般流程。

 

导入

 

import torch
import torch.nn as nn
import torch.optim as  optim

from torch.utils.data import DataLoader

# 使用PyTorch提供的MNIST数据集
from torchvision.datasets.mnist import MNIST
import torchvision.transforms as transforms

# 从另一个文件中导入实现的模型
from model import Classifier

import matplotlib.pyplot as plt

 

torch.nn模块提供了对神经网络架构的支持,并具有内置的常用层的实现,例如Dense Layers、卷积神经网络等等。

torch.optim提供了诸如随机梯度下降和Adam等优化器的实现。

其他实用模块可用于数据处理支持和转换。我们稍后将详细介绍每个模块。

 

声明超参数

 

每个超参数在适当的位置将进一步解释。然而,最佳实践是在文件顶部声明它们,以便易于更改和理解。

INPUT_SIZE = 784 # 展平的28x28图像
NUM_CLASSES = 10    # 0-9手写数字。
BATCH_SIZE = 128    # 使用小批量进行训练
LEARNING_RATE = 0.01    # 优化器步长
NUM_EPOCHS = 5      # 总训练轮数

 

数据加载和转换

 

data_transforms = transforms.Compose([
        transforms.ToTensor(),
        transforms.Lambda(lambda x: torch.flatten(x))
])

train_dataset = MNIST(root=".data/", train=True, download=True, transform=data_transforms)


test_dataset = MNIST(root=".data/", train=False, download=True, transform=data_transforms)

 

MNIST是一个流行的图像分类数据集,默认在PyTorch中提供。它包含10个手写数字的灰度图像,从0到9。每个图像的大小为28×28像素,数据集包含60000个训练图像和10000个测试图像。

我们分别加载训练和测试数据集,通过MNIST初始化函数中的train参数来指示。root参数声明要下载数据集的目录。

然而,我们还传递了一个额外的transform参数。对于PyTorch,所有输入和输出都应该是Torch.Tensor格式。这相当于numpy中的numpy.ndarray。该张量格式提供了对数据操作的额外支持。然而,我们从中加载的MNIST数据处于PIL.Image格式中。我们需要将图像转换为PyTorch兼容的张量。因此,我们传递以下转换:

data_transforms = transforms.Compose([
        transforms.ToTensor(),
        transforms.Lambda(lambda x: torch.flatten(x))
])

 

ToTensor()转换将图像转换为张量格式。接下来,我们传递了一个额外的Lambda转换。Lambda函数允许我们实现自定义转换。在这里,我们声明一个将输入展平的函数。图像的大小为28×28,但是我们将其展平,即将其转换为大小为28×28或784的一维数组。这在我们实现模型时将非常重要。

Compose函数按顺序组合所有转换。首先将数据转换为张量格式,然后将其展平为一维数组。

将数据分成批次

为了计算和训练的目的,我们不能一次将完整的数据集传入模型中。我们需要将数据集分成小批次,按顺序将其馈送给模型。这样可以加快训练速度,并为数据集添加随机性,有助于稳定的训练。

PyTorch提供了对数据进行分批处理的内置支持。来自torch.utils模块的DataLoader类可以根据torch数据集模块创建数据批次。正如上面所述,我们已经加载了数据集。

train_dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)

我们将数据集传递给数据加载器,并将批次大小超参数作为初始化参数。这将创建一个可迭代的数据加载器,因此我们可以使用简单的for循环轻松地迭代每个批次。

我们初始的图像大小为(784, ),带有一个关联的标签。分批处理将不同的图像和标签组合在一起。例如,如果我们的批次大小为64,则批次中的输入大小将变为(64, 784),并且我们将为每个批次有64个关联的标签。

我们还对训练批次进行了洗牌操作,这会在每个epoch内更改批次中的图像顺序。这样可以实现稳定的训练,并加快模型参数的收敛速度。

定义我们的分类模型

我们使用了一个由3个隐藏层组成的简单实现。尽管简单,但这可以让您对组合不同层以实现更复杂实现有一个基本的理解。

如上所述,我们有一个大小为(784, )的输入张量和10个不同的输出类别,分别对应数字0-9。

** 对于模型实现,我们可以忽略批处理维度。

import torch
import torch.nn as nn


class Classifier(nn.Module):
    def __init__(
            self,
            input_size:int,
            num_classes:int
        ) -> None:
        super().__init__()
        self.input_layer = nn.Linear(input_size, 512)
        self.hidden_1 = nn.Linear(512, 256)
        self.hidden_2 = nn.Linear(256, 128)
        self.output_layer = nn.Linear(128, num_classes)

        self.activation = nn.ReLU()
     
    def forward(self, x):
        # 通过每个全连接层和激活函数对输入进行顺序处理
        x = self.activation(self.input_layer(x))
        x = self.activation(self.hidden_1(x))
        x = self.activation(self.hidden_2(x))
        return self.output_layer(x)

首先,模型必须继承torch.nn.Module类。这提供了神经网络架构的基本功能。然后,我们必须实现两个方法,__init__和forward。

在__init__方法中,我们声明模型将使用的所有层。我们使用PyTorch提供的Linear(也称为Dense)层。第一层将输入映射到512个神经元。我们可以将input_size作为模型参数传递,这样我们以后可以将其用于不同大小的输入。第二层将512个神经元映射到256个神经元。第三个隐藏层将前一层的256个神经元映射到128个神经元。最后一层最终缩小到输出大小。我们的输出大小将是一个大小为(10, )的张量,因为我们要预测十个不同的数字。

此外,我们在模型中初始化了一个ReLU激活层,用于非线性。

forward函数接收图像,并提供处理输入的代码。我们使用声明的层,并顺序地将输入通过每个层,使用中间的ReLU激活层。

在我们的主要代码中,我们可以初始化模型,并为数据集提供输入和输出大小。

model = Classifier(input_size=784, num_classes=10)
model.to(DEVICE)

初始化后,我们更改模型设备(可以是CUDA GPU或CPU)。我们在初始化超参数时检查了设备。现在,我们必须手动更改张量和模型层的设备。

 

训练循环

 

首先,我们必须声明我们将用于优化模型参数的损失函数和优化器。

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)

 

首先,我们必须声明我们将用于优化模型参数的损失函数和优化器。

我们使用交叉熵损失函数,它主要用于多标签分类模型。它首先对预测结果应用softmax,并计算给定的目标标签和预测值之间的差异。

Adam优化器是最常用的优化器函数,它允许稳定的梯度下降以达到收敛。它是现在的默认优化器选择,并提供了令人满意的结果。 我们将模型参数作为参数传递,表示将要优化的权重。

对于我们的训练循环,我们逐步构建并填充缺失的部分,以便我们能够理解。

作为起点,我们多次迭代完整的数据集(称为epoch),并在每次迭代中优化模型。然而,我们将数据分成了批次。然后,对于每个epoch,我们还必须迭代每个批次。以下是此代码的样子:

for epoch in range(NUM_EPOCHS):
        for batch in iter(train_dataloader):
          # 对每个批次训练模型。

 

现在,我们可以训练模型,给定单个输入批次。我们的批次包含图像和标签。首先,我们必须分别提取它们。我们的模型只需要图像作为输入进行预测。然后,我们将预测结果与真实标签进行比较,以估计模型的性能。

for epoch in range(NUM_EPOCHS):
        for batch in iter(train_dataloader):
            images, labels = batch # 分离输入和标签
            # 将张量硬件设备转换为GPU或CPU
            images = images.to(DEVICE)
            labels = labels.to(DEVICE)
         
            # 调用model.forward()函数生成预测结果
            predictions = model(images)

 

我们将图像批次直接传递给模型,模型将通过其定义的前向函数进行处理。一旦我们有了预测结果,我们就可以优化模型的权重。

优化代码如下所示:

# 计算交叉熵损失
loss = criterion(predictions, labels)
# 清除先前批次的梯度值
optimizer.zero_grad()
# 根据损失计算反向传播梯度
loss.backward()
# 优化模型权重
optimizer.step()

 

使用以上代码,我们可以计算所有反向传播梯度,并使用Adam优化器优化模型权重。所有以上代码的组合可以使我们的模型训练达到收敛。

完整的训练循环如下所示:

for epoch in range(NUM_EPOCHS):
        total_epoch_loss = 0
        steps = 0
        for batch in iter(train_dataloader):
            images, labels = batch # 分离输入和标签
            # 将张量硬件设备转换为GPU或CPU
            images = images.to(DEVICE)
            labels = labels.to(DEVICE)
         
            # 调用model.forward()函数生成预测结果         
            predictions = model(images)
         
            # 计算交叉熵损失
            loss = criterion(predictions, labels)
            # 清除先前批次的梯度值
            optimizer.zero_grad()
            # 根据损失计算反向传播梯度
            loss.backward()
            # 优化模型权重
            optimizer.step()
         
            steps += 1
            total_epoch_loss += loss.item()
         
        print(f'Epoch: {epoch + 1} / {NUM_EPOCHS}: 平均损失: {total_epoch_loss / steps}')

 

损失逐渐减少并接近0。然后,我们可以在最初声明的测试数据集上评估模型。

 

评估模型性能

 

for batch in iter(test_dataloader):
        images, labels = batch
        images = images.to(DEVICE)
        labels = labels.to(DEVICE)
     
        predictions = model(images)
     
        # 取概率最高的预测标签
        predictions = torch.argmax(predictions, dim=1)
     
        correct_predictions += (predictions == labels).sum().item()
        total_predictions += labels.shape[0]
    
print(f"\n测试准确率: {((correct_predictions / total_predictions) * 100):.2f}")

 

与训练循环类似,我们迭代测试数据集中的每个批次进行评估。我们为输入生成预测。然而,对于评估,我们只需要具有最高概率的标签。argmax函数提供此功能,以获取预测数组中具有最高值的值的索引。

对于准确率分数,我们可以比较预测的标签是否与真实目标标签匹配。然后,我们计算正确标签数量除以总预测标签的准确率。

 

结果

 

我只对模型进行了五个时期的训练,并获得了超过96%的测试准确率,而在训练之前准确率为10%。下图显示了训练五个时期后的模型预测。

   

就是这样。您现在已经从头开始实现了一个模型,它可以根据图像像素值区分手写数字。

这绝不是PyTorch的全面指南,但它确实为您提供了机器学习项目中的结构和数据流的一般理解。这仍然是足够的知识,可以让您开始实现深度学习中的最先进架构。

 

完整代码

 

完整代码如下:

model.py:

import torch
import torch.nn as nn


class Classifier(nn.Module):
    def __init__(
            self,
            input_size:int,
            num_classes:int
        ) -> None:
        super().__init__()
        self.input_layer = nn.Linear(input_size, 512)
        self.hidden_1 = nn.Linear(512, 256)
        self.hidden_2 = nn.Linear(256, 128)
        self.output_layer = nn.Linear(128, num_classes)

        self.activation = nn.ReLU()
     
    def forward(self, x):
        # 通过每个密集层和激活依次传递输入
        x = self.activation(self.input_layer(x))
        x = self.activation(self.hidden_1(x))
        x = self.activation(self.hidden_2(x))
        return self.output_layer(x)

 

main.py

import torch
import torch.nn as nn
import torch.optim as  optim

from torch.utils.data import DataLoader

# 使用PyTorch提供的MNIST数据集
from torchvision.datasets.mnist import MNIST
import torchvision.transforms as transforms

# 从不同文件中导入实现的模型
from model import Classifier

import matplotlib.pyplot as plt

if __name__ == "__main__":
    
    INPUT_SIZE = 784    # 展平的28x28图像
    NUM_CLASSES = 10    # 0-9手写数字。
    BATCH_SIZE = 128    # 使用小批量进行训练
    LEARNING_RATE = 0.01    # 优化器步骤
    NUM_EPOCHS = 5      # 总训练时期数

    DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
    
    # 将图像转换为PyTorch张量
    data_transforms = transforms.Compose([
        transforms.ToTensor(),
        transforms.Lambda(lambda x: torch.flatten(x))
    ])
    
    
    train_dataset = MNIST(root=".data/", train=True, download=True, transform=data_transforms)
    test_dataset = MNIST(root=".data/", train=False, download=True, transform=data_transforms)    
    
    train_dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
    test_dataloader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)
    
    
    model = Classifier(input_size=784, num_classes=10)
    model.to(DEVICE)
    
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
    
    for epoch in range(NUM_EPOCHS):
        total_epoch_loss = 0
        steps = 0
        for batch in iter(train_dataloader):
            images, labels = batch # 分离输入和标签
            # 将张量硬件设备转换为GPU或CPU
            images = images.to(DEVICE)
            labels = labels.to(DEVICE)
         
            # 调用model.forward()函数生成预测         
            predictions = model(images)
         
            # 计算交叉熵损失
            loss = criterion(predictions, labels)
            # 清除先前批次的梯度值
            optimizer.zero_grad()
            # 基于损失计算反向传播梯度
            loss.backward()
            # 优化模型权重
            optimizer.step()
         
            steps += 1
            total_epoch_loss += loss.item()
         
        print(f'Epoch: {epoch + 1} / {NUM_EPOCHS}: Average Loss: {total_epoch_loss / steps}')
    # 保存训练好的模型
    torch.save(model.state_dict(), 'trained_model.pth')
    
    model.eval()
    correct_predictions = 0
    total_predictions = 0
    for batch in iter(test_dataloader):
        images, labels = batch
        images = images.to(DEVICE)
        labels = labels.to(DEVICE)
     
        predictions = model(images)
     
        # 取具有最高概率的预测标签
        predictions = torch.argmax(predictions, dim=1)
     
        correct_predictions += (predictions == labels).sum().item()
        total_predictions += labels.shape[0]
    
    print(f"\n测试准确率:{((correct_predictions / total_predictions) * 100):.2f}")
    
    
    
    # -- 绘制结果的代码 -- #
    
    batch = next(iter(test_dataloader))
    images, labels = batch
    
    fig, ax = plt.subplots(nrows=1, ncols=4, figsize=(16,8))
    for i in range(4):
        image = images[i]
        prediction = torch.softmax(model(image), dim=0)
        prediction = torch.argmax(prediction, dim=0)
        # print(type(prediction), type(prediction.item()))
        ax[i].imshow(image.view(28,28))
        ax[i].set_title(f'预测:{prediction.item()}')
    plt.show()

    Muhammad Arham(穆罕默德·阿尔汉姆) 是一名从事计算机视觉和自然语言处理的深度学习工程师。他曾在Vyro.AI参与了多个生成式人工智能应用的部署和优化工作,并在全球排行榜上取得了巨大成功。他对构建和优化智能系统的机器学习模型充满兴趣,并坚信不断改进。