Compartilhamento de tecnologia

Projetando uma rede neural do zero: realizando o reconhecimento de dígitos manuscritos

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Prefácio

Para compreender melhor as redes neurais, é muito apropriado compreender o princípio de funcionamento e o processo geral das redes neurais, camada por camada, a partir da pequena tarefa de reconhecimento de dígitos manuscritos.

Este artigo irá completar uma tarefa de reconhecimento de números por escrita manual para explicar como projetar, implementar e treinar uma rede neural feedforward padrão, a fim de ter uma compreensão mais específica e perceptiva das redes neurais.

Visão geral

Especificamente, precisamos projetar e treinar uma rede neural de 3 camadas. Esta rede neural receberá imagens digitais como entrada. Após o cálculo pela rede neural, ela identificará os números na imagem, realizando assim a classificação das imagens digitais:

Insira a descrição da imagem aqui

Neste processo, três aspectos são explicados principalmente: o projeto e implementação de redes neurais, a preparação e processamento de dados de treinamento e o processo de treinamento e teste do modelo.

Insira a descrição da imagem aqui

Projeto e implementação de redes neurais

Para projetar uma rede neural para processar dados de imagem, é necessário primeiro esclarecer o tamanho e o formato dos dados de imagem de entrada.

Insira a descrição da imagem aqui

A imagem que vamos processar é uma imagem de canal cinza de tamanho 28 × 28 pixels (o formato do próprio conjunto de dados MNIST).

Esta imagem cinza inclui 2828 = 784 pontos de dados, precisamos nivelá-los para 1 primeiroVetor de tamanho 784:

Insira a descrição da imagem aqui

Em seguida, insira esse vetor na rede neural. Usaremos uma rede neural de três camadas para processar o vetor x correspondente à imagem. A camada de entrada precisa receber o vetor de imagem x de 784 dimensões. rede neural a receber, então a camada de entrada contém 784 neurônios:

Insira a descrição da imagem aqui

A camada oculta é usada para extração de recursos, processando os vetores de recursos de entrada em vetores de recursos de nível superior.

Como a imagem do dígito manuscrito não é complexa, aqui definimos o número de neurônios na camada oculta para 256, de modo que haja uma camada linear de tamanho 784*256 entre a camada de entrada e a camada oculta:

Insira a descrição da imagem aqui

Ele pode converter um vetor de entrada de 784 dimensões em um vetor de saída de 256 dimensões, que continuará a se propagar para a camada de saída.

Como a imagem digital será finalmente reconhecida como dez números possíveis de 0 a 9, a camada de saída precisa definir 10 neurônios para corresponder a estes dez números:

Insira a descrição da imagem aqui

Depois que o vetor de 256 dimensões é calculado através da camada linear entre a camada oculta e a camada de saída, um resultado de saída de 10 dimensões é obtido. Este vetor de 10 dimensões representa a pontuação de previsão de 10 números:

Insira a descrição da imagem aqui

Para continuar a obter a probabilidade prevista de 10 números, também precisamos inserir a saída da camada de saída na camada softmax. A camada softmax converterá o vetor de 10 dimensões em 10 valores de probabilidade P0 a P9. o valor de probabilidade corresponde a um número. Ou seja, a possibilidade de a imagem de entrada ser um determinado número. Além disso, a soma dos 10 valores de probabilidade de P0 a P9 é 1. Isso é determinado pelas propriedades do. Função softmax:

Insira a descrição da imagem aqui

A ideia acima é o design da rede neural. A seguir, usamos a estrutura PyTorch para implementá-la.

Primeiro implemente nossa rede neural:

Insira a descrição da imagem aqui

código mostrado abaixo:

import torch
from torch import nn


# 定义神经网络
class NetWork(nn.Module):
    def __init__(self):
        super().__init__()
        # 线性层1,输入层和隐藏层之间的线性层
        self.layer1 = nn.Linear(784, 256)
        # 线性层2,隐藏层和输出层之间的线性层
        self.layer2 = nn.Linear(256, 10)
        # 这里没有直接定义 softmax 层
        # 这是因为后面会使用 CrossEntropyLoss 损失函数
        # 在这个损失函数中会实现 softmax 的计算

    def forward(self, x):
        x = x.view(-1, 28 * 28)  # 使用view 函数将 x 展平
        x = self.layer1(x)  # 将 x 输入至 layer1
        x = torch.relu(x)  # 使用 ReLu 函数激活
        return self.layer2(x)  # 输入至 layer2 计算结果
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

Preparação e processamento de dados de treinamento

Em seguida, prepare o conjunto de dados:

Insira a descrição da imagem aqui

Para obter o conjunto de dados, você também pode usar o seguinte método para baixá-lo do site oficial do PyTorch:

# 准备数据集
train_data = torchvision.datasets.MNIST(root='data', train=True, download=True)
test_data = torchvision.datasets.MNIST(root='data', train=False, download=True)
  • 1
  • 2
  • 3

Os dados baixados desta forma serão armazenados automaticamente em train_data e test_data. Da mesma forma, um pacote de ferramentas será baixado no diretório que especificamos no código:

Insira a descrição da imagem aqui

Mas este não é um método universal. No trabalho e estudo futuros, haverá muitos conjuntos de dados que precisaremos para operar e processar nós mesmos, o que não será tão conveniente quanto o acima. Portanto, um método mais geral é introduzido aqui. que consiste em baixar os dados nativos do site oficial. Um conjunto de dados é, sim, um monte de imagens!

Salvamos os dados baixados nas duas pastas a seguir:

Insira a descrição da imagem aqui

Salvamos os dados em dois diretórios, train e test respectivamente, onde train possui 60.000 dados e test possui 10.000 dados, que são usados ​​para treinamento e teste de modelo respectivamente.

Os diretórios train e test incluem dez subdiretórios, e os nomes dos subdiretórios correspondem aos números na imagem. Por exemplo, uma imagem do número 3 é salva em uma pasta chamada 3:

Insira a descrição da imagem aqui

onde o nome da imagem é uma assinatura de string aleatória.

Após concluir a preparação dos dados, a função de leitura de dados é implementada. Os iniciantes só precisam conhecer o processo geral de processamento de dados ao aprender esta parte.

O código é implementado da seguinte forma:

# 首先实现图像的预处理pipeline
transform = torchvision.transforms.Compose([
    torchvision.transforms.Grayscale(num_output_channels=1),  # 转换为单通道灰度图像
    torchvision.transforms.ToTensor()  # 转换为PyTorch支持的张量
])

# 这是方式一准备数据集的方式嗷
train_data = torchvision.datasets.MNIST(root='data', train=True, download=True, transform=transform)
test_data = torchvision.datasets.MNIST(root='data', train=False, download=True, transform=transform)

# 下面这是方式二准备数据集的方式
# 使用 ImageFolder 函数读取数据文件夹,构建数据集 dataset
# 这个函数会将保存数据的文件夹的名字,作为数据的标签,组织数据
# 例如,对于名字为 3 的文件夹
# 就会将 3 作为文件夹中图像数据的标签,和图像配对用于后续的训练,使用起来非常方便
train_dataset = torchvision.datasets.ImageFolder(root='./mnist/train', transform=transform)
test_dataset = torchvision.datasets.ImageFolder(root='./mnist/test', transform=transform)

# 不管使用哪种准备数据集的方式,最后的效果都是一样的
# 打印它们的长度看一下
print(len(train_data))
print(len(test_data))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

Dê uma olhada nos resultados da corrida:

Insira a descrição da imagem aqui

Você pode ver que tanto os dados de treinamento quanto os dados de teste foram obtidos, o que é totalmente consistente com o que dissemos antes.

Em seguida, usamos train_loader para implementar a leitura de dados em pequenos lotes:

# 使用 train_loader 实现小批量的数据读取
# 这里设置小批量的大小,batch_size = 64。
# 也就是每个批次包括 64 个数据
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)
# 打印 train_loader 的长度
print(" train loader length: ", len(train_loader))
# 60000 个训练数据,如果每个小批量读入 64 个样本,那么 60000 个数据会被分为 938 组
# 计算 938 * 64 = 60032,这说明最后一组会不够 64 个数据
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Os resultados da execução são os seguintes:

Insira a descrição da imagem aqui

Podemos então percorrer train_loader para obter cada minilote de dados:

# 循环遍历 train_loader
# 每一次循环,都会取出 64 个图像数据,作为一个小批量 batch
for batch_idx, (data, label) in enumerate(train_loader):
    if batch_idx == 3:  # 打印前三个 batch 观察
        break
    print("batch_idx: ", batch_idx)
    print("data.shape: ", data.shape)  # 数据的尺寸
    print("label: ", label.shape)  # 图像中的数字
    print(label)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Aqui está uma explicação simples da instrução de loop acima que a tornará mais clara:

enumerar(): Esta é a função integrada do Python, que é usada para combinar um objeto de dados percorrível (como uma lista, tupla ou string) em uma sequência de índice e listar os dados e subscritos de dados ao mesmo tempo, ou seja, obter o índice e valor ao mesmo tempo. Aqui, ele é usado para iterar cada lote no train_loader, onde batch_idx é o índice do lote atual (começando em 0), (dados, rótulo) são os dados e o rótulo do lote atual.

para batch_idx, (dados, rótulo) em enumerate(train_loader):: O significado desta instrução de loop é que para cada lote em train_loader, o código no corpo do loop é executado.Em cada iteração do loop, batch_idx é o índice do lote atual, (dados, rótulo) são os dados e o rótulo do lote atual . os dados são geralmente um tensor contendo vários pontos de dados, cada ponto de dados é uma amostra é o tensor de rótulo correspondente a esses pontos de dados, que é usado para o valor alvo na aprendizagem supervisionada;

O efeito de execução é o seguinte:

Insira a descrição da imagem aqui

Isso pode ser visto nos resultados da execução:

1. batch_idx = 0 significa que é o primeiro lote de dados

2. data.shape indica que o tamanho dos dados é [64, 1, 28, 28]

O tamanho acima significa que cada lote de dados inclui 64 imagens, cada imagem possui 1 canal cinza e o tamanho da imagem é 28*28.

3. label.shape significa que existem 64 números no lote e o número total de rótulos correspondentes a eles é 64. Cada número possui um rótulo.

Preste atenção na diferença. Deve haver apenas 9 categorias de rótulos reais, porque os números são apenas de 1 a 9, e aqui significa que existem 64 valores de rótulos.

4. A matriz tensorial representa o valor do rótulo correspondente a cada uma dessas 64 imagens digitais.

Processo de treinamento e teste de modelo

Com os preparativos anteriores feitos, podemos começar a treinar e testar o modelo.

Aqui está o código de treinamento:

# 在使用 PyTorch 训练模型时,需要创建三个对象
model = NetWork()  # 1、模型本身,它就是我们设计的神经网络
optimizer = torch.optim.Adam(model.parameters())  # 2、优化器,优化模型中的参数
criterion = nn.CrossEntropyLoss()  # 3、损失函数,分类问题使用交叉熵损失误差

# 开始训练模型
for epoch in range(10):  # 外层循环,代表了整个训练数据集的遍历次数
    # 整个训练集要循环多少轮,是10次还是20次还是100次都是有可能的
    # 内层循环使用train_loader 进行小批量的数据读取
    for batch_idx, (data, label) in enumerate(train_loader):
        # 内层每循环一次,就会进行一次梯度下降算法
        # 包括五个步骤:
        output = model(data)  # 1、计算神经网络的前向传播结果
        loss = criterion(output, label)  # 2、计算 output 和标签 label 之间的误差损失 loss
        loss.backward()  # 3、使用 backward 计算梯度
        optimizer.step()  # 4、使用 optimizer.step 更新参数
        optimizer.zero_grad()  # 5、将梯度清零
        # 这五个步骤是使用 PyTorch 框架训练模型的定式,初学的时候记住就可以了
        # 每迭代 100 个小批量,就打印一次模型的损失,观察训练的过程
        if batch_idx % 100 == 0:
            print(f"Epoch {epoch + 1} / 10 "
                  f"| Batch {batch_idx} / {len(train_loader)}"
                  f"| Loss: {loss.item():.4f}")

torch.save(model.state_dict(), 'mnist.pth')  # 最后保存训练好的模型
  • 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

O efeito de execução é o seguinte:

Insira a descrição da imagem aqui
Omita o meio…
Insira a descrição da imagem aqui

Pode-se observar que o valor final da perda é muito, muito pequeno, 0,0239.

A última etapa é o teste. O processo de teste é basicamente igual ao treinamento.

model = NetWork()  # 定义神经网络模型
model.load_state_dict(torch.load('mnist.pth'))  # 加载刚刚训练好的模型文件

right = 0  # 保存正确识别的数量
for i, (x, y) in enumerate(test_data):
    output = model(x)  # 将其中的数据 x 输入到模型中
    predict = output.argmax(1).item()  # 选择概率最大的标签作为预测结果
    # 对比预测值 predict 和真实标签 y
    if predict == y:
        right += 1
    else:
        # 将识别错误的样例打印出来
        print(f"wrong case: predict = {predict}, but y = {y}")

# 计算出测试结果
sample_num = len(test_data)
accuracy = right * 1.0 / sample_num
print("test accuracy = %d / %d = %.3lf" % (right, sample_num, accuracy))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

Os resultados da execução são os seguintes:

Insira a descrição da imagem aqui

Percebe-se que a precisão do teste é de 98%, o que ainda é muito alto.

O texto acima é o processo de projetar e treinar uma rede neural do zero.

Resumo e encapsulamento de código

O acima é explicado e descrito de acordo com cada parte funcional, o que pode ser um pouco confuso. Encapsulamos o código acima da seguinte maneira.

código de treinamento

import torch
import torchvision.datasets
from torch import nn


# ----------------1、定义神经网络-------------------
class NetWork(nn.Module):
    def __init__(self):
        super().__init__()
        # 线性层1,输入层和隐藏层之间的线性层
        self.layer1 = nn.Linear(784, 256)
        # 线性层2,隐藏层和输出层之间的线性层
        self.layer2 = nn.Linear(256, 10)
        # 这里没有直接定义 softmax 层
        # 这是因为后面会使用 CrossEntropyLoss 损失函数
        # 在这个损失函数中会实现 softmax 的计算

    def forward(self, x):
        x = x.view(-1, 28 * 28)  # 使用view 函数将 x 展平
        x = self.layer1(x)  # 将 x 输入至 layer1
        x = torch.relu(x)  # 使用 ReLu 函数激活
        return self.layer2(x)  # 输入至 layer2 计算结果


# ----------------2、图像预处理-------------------
# 实现图像的预处理的pipeline
transform = torchvision.transforms.Compose([
    torchvision.transforms.Grayscale(num_output_channels=1),  # 转换为单通道灰度图像
    torchvision.transforms.ToTensor()  # 转换为PyTorch支持的张量
])

# ----------------3、数据集准备-------------------
# 准备训练数据集
train_data = torchvision.datasets.MNIST(root='data', train=True, download=True, transform=transform)

# ----------------4、数据集加载-------------------
# 使用 train_loader 实现小批量的数据读取
# 这里设置小批量的大小,batch_size = 64。
# 也就是每个批次包括 64 个数据
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)

# ----------------5、训练神经网络-------------------
# 在使用 PyTorch 训练模型时,需要创建三个对象
model = NetWork()  # 1、模型本身,它就是我们设计的神经网络
optimizer = torch.optim.Adam(model.parameters())  # 2、优化器,优化模型中的参数
criterion = nn.CrossEntropyLoss()  # 3、损失函数,分类问题使用交叉熵损失误差

# 开始训练模型
for epoch in range(10):  # 外层循环,代表了整个训练数据集的遍历次数
    # 整个训练集要循环多少轮,是10次还是20次还是100次都是有可能的
    # 内层循环使用train_loader 进行小批量的数据读取
    for batch_idx, (data, label) in enumerate(train_loader):
        # 内层每循环一次,就会进行一次梯度下降算法
        # 包括五个步骤:
        output = model(data)  # 1、计算神经网络的前向传播结果
        loss = criterion(output, label)  # 2、计算 output 和标签 label 之间的误差损失 loss
        loss.backward()  # 3、使用 backward 计算梯度
        optimizer.step()  # 4、使用 optimizer.step 更新参数
        optimizer.zero_grad()  # 5、将梯度清零
        # 这五个步骤是使用 PyTorch 框架训练模型的定式,初学的时候记住就可以了
        # 每迭代 100 个小批量,就打印一次模型的损失,观察训练的过程
        if batch_idx % 100 == 0:
            print(f"Epoch {epoch + 1} / 10 "
                  f"| Batch {batch_idx} / {len(train_loader)}"
                  f"| Loss: {loss.item():.4f}")

torch.save(model.state_dict(), 'mnist.pth')  # 最后保存训练好的模型

  • 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

código de teste

import torch
import torchvision.datasets
from torch import nn


# ----------------1、定义神经网络-------------------
class NetWork(nn.Module):
    def __init__(self):
        super().__init__()
        # 线性层1,输入层和隐藏层之间的线性层
        self.layer1 = nn.Linear(784, 256)
        # 线性层2,隐藏层和输出层之间的线性层
        self.layer2 = nn.Linear(256, 10)
        # 这里没有直接定义 softmax 层
        # 这是因为后面会使用 CrossEntropyLoss 损失函数
        # 在这个损失函数中会实现 softmax 的计算

    def forward(self, x):
        x = x.view(-1, 28 * 28)  # 使用view 函数将 x 展平
        x = self.layer1(x)  # 将 x 输入至 layer1
        x = torch.relu(x)  # 使用 ReLu 函数激活
        return self.layer2(x)  # 输入至 layer2 计算结果


# ----------------2、图像预处理-------------------
# 实现图像的预处理的pipeline
transform = torchvision.transforms.Compose([
    torchvision.transforms.Grayscale(num_output_channels=1),  # 转换为单通道灰度图像
    torchvision.transforms.ToTensor()  # 转换为PyTorch支持的张量
])

# ----------------3、数据集准备-------------------
# 准备测试数据集
test_data = torchvision.datasets.MNIST(root='data', train=False, download=True, transform=transform)

# ----------------4、测试神经网络-------------------
model = NetWork()  # 定义神经网络模型
model.load_state_dict(torch.load('mnist.pth'))  # 加载刚刚训练好的模型文件

right = 0  # 保存正确识别的数量
for i, (x, y) in enumerate(test_data):
    output = model(x)  # 将其中的数据 x 输入到模型中
    predict = output.argmax(1).item()  # 选择概率最大的标签作为预测结果
    # 对比预测值 predict 和真实标签 y
    if predict == y:
        right += 1
    else:
        # 将识别错误的样例打印出来
        print(f"wrong case: predict = {predict}, but y = {y}")

# 计算出测试结果
sample_num = len(test_data)
accuracy = right * 1.0 / sample_num
print("test accuracy = %d / %d = %.3lf" % (right, sample_num, accuracy))

  • 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