Compartir tecnología

Diseñar una red neuronal desde cero: realizar el reconocimiento de dígitos escritos a mano

2024-07-12

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

Prefacio

Para comprender mejor las redes neuronales, es muy apropiado comprender el principio de funcionamiento y el proceso general de las redes neuronales capa por capa a partir de la pequeña tarea de reconocimiento de dígitos escritos a mano.

Este artículo completará una tarea de reconocimiento de números a mano para explicar cómo diseñar, implementar y entrenar una red neuronal de avance estándar, con el fin de tener una comprensión más específica y perceptiva de las redes neuronales.

Descripción general

Específicamente, necesitamos diseñar y entrenar una red neuronal de 3 capas. Esta red neuronal tomará imágenes digitales como entrada. Después del cálculo de la red neuronal, identificará los números en la imagen, realizando así la clasificación de las imágenes digitales:

Insertar descripción de la imagen aquí

En este proceso se explican principalmente tres aspectos: el diseño e implementación de redes neuronales, la preparación y procesamiento de datos de entrenamiento y el proceso de entrenamiento y prueba del modelo.

Insertar descripción de la imagen aquí

Diseño e implementación de redes neuronales.

Para diseñar una red neuronal para procesar datos de imágenes, primero es necesario aclarar el tamaño y el formato de los datos de la imagen de entrada.

Insertar descripción de la imagen aquí

La imagen que vamos a procesar es una imagen de canal gris de tamaño 28 × 28 píxeles (el formato del propio conjunto de datos MNIST).

Esta imagen gris incluye 2828 = 784 puntos de datos, primero debemos aplanarlos a 1Vector de tamaño 784:

Insertar descripción de la imagen aquí

Luego ingrese este vector en la red neuronal. Usaremos una red neuronal de tres capas para procesar el vector x correspondiente a la imagen. La capa de entrada necesita recibir el vector de imagen de 784 dimensiones x. red neuronal para recibir, por lo que la capa de entrada contiene 784 neuronas:

Insertar descripción de la imagen aquí

La capa oculta se utiliza para la extracción de características, procesando los vectores de características de entrada en vectores de características de nivel superior.

Dado que la imagen de dígitos escrita a mano no es compleja, aquí configuramos el número de neuronas en la capa oculta en 256, de modo que habrá una capa lineal de tamaño 784 * 256 entre la capa de entrada y la capa oculta:

Insertar descripción de la imagen aquí

Puede convertir un vector de entrada de 784 dimensiones en un vector de salida de 256 dimensiones, que continuará propagándose hacia la capa de salida.

Dado que la imagen digital finalmente será reconocida como diez números posibles del 0 al 9, la capa de salida necesita definir 10 neuronas para corresponder a estos diez números:

Insertar descripción de la imagen aquí

Después de calcular el vector de 256 dimensiones a través de la capa lineal entre la capa oculta y la capa de salida, se obtiene un resultado de salida de 10 dimensiones. Este vector de 10 dimensiones representa la puntuación de predicción de 10 números:

Insertar descripción de la imagen aquí

Para continuar obteniendo la probabilidad predicha de 10 números, también necesitamos ingresar la salida de la capa de salida en la capa softmax. La capa softmax convertirá el vector de 10 dimensiones en 10 valores de probabilidad P0 a P9 cada uno. El valor de probabilidad corresponde a un número, es decir, la posibilidad de que la imagen de entrada sea un número determinado. Además, la suma de los 10 valores de probabilidad de P0 a P9 es 1. Esto está determinado por las propiedades del. función softmax:

Insertar descripción de la imagen aquí

Lo anterior es la idea de diseño de la red neuronal. A continuación, utilizamos el marco PyTorch para implementarla.

Primero implemente nuestra red neuronal:

Insertar descripción de la imagen aquí

El código se muestra a continuación:

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

Preparación y procesamiento de datos de entrenamiento.

Luego prepare el conjunto de datos:

Insertar descripción de la imagen aquí

Para obtener el conjunto de datos, también puede utilizar el siguiente método para descargarlo del sitio web oficial de 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

Los datos descargados de esta manera se almacenarán automáticamente en train_data y test_data. En consecuencia, se descargará un paquete de herramientas en el directorio que especificamos en el código:

Insertar descripción de la imagen aquí

Pero este no es un método universal. En el trabajo y estudio futuro, habrá muchos conjuntos de datos que necesitaremos operar y procesar nosotros mismos, lo que no será tan conveniente como el anterior, por lo que aquí se presenta un método más general. que consiste en descargar los datos nativos del sitio web oficial. Un conjunto de datos es, sí, un montón de imágenes.

Guardamos los datos descargados en las siguientes dos carpetas:

Insertar descripción de la imagen aquí

Guardamos los datos en dos directorios, train y test respectivamente, donde train tiene 60.000 datos y test tiene 10.000 datos, que se utilizan para el entrenamiento y prueba del modelo respectivamente.

Tanto el directorio train como el test incluyen diez subdirectorios y los nombres de los subdirectorios corresponden a los números de la imagen. Por ejemplo, una imagen del número 3 se guarda en una carpeta llamada 3:

Insertar descripción de la imagen aquí

donde el nombre de la imagen es una firma de cadena aleatoria.

Después de completar la preparación de datos, se implementa la función de lectura de datos. Los principiantes solo necesitan conocer el proceso general de procesamiento de datos cuando aprenden esta parte.

El código se implementa de la siguiente manera:

# 首先实现图像的预处理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

Eche un vistazo a los resultados de ejecución:

Insertar descripción de la imagen aquí

Puedes ver que se han obtenido tanto los datos de entrenamiento como los de prueba, lo cual es totalmente consistente con lo que dijimos antes.

Luego usamos train_loader para implementar la lectura de datos por lotes pequeños:

# 使用 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

Los resultados de ejecución son los siguientes:

Insertar descripción de la imagen aquí

Luego podemos recorrer train_loader para obtener cada mini lote de datos:

# 循环遍历 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

Aquí hay una explicación simple de la declaración de bucle anterior que la hará más clara:

enumerar(): Esta es la función incorporada de Python, que se utiliza para combinar un objeto de datos transitable (como una lista, tupla o cadena) en una secuencia de índice y enumerar los datos y los subíndices de datos al mismo tiempo, es decir, obtener el índice y valor al mismo tiempo. Aquí, se utiliza para iterar a través de cada lote en train_loader, donde lote_idx es el índice del lote actual (comenzando desde 0), (datos, etiqueta) son los datos y la etiqueta del lote actual.

para batch_idx, (datos, etiqueta) en enumerate(train_loader):: El significado de esta declaración de bucle es que para cada lote en train_loader, se ejecuta el código en el cuerpo del bucle.En cada iteración del bucle, lote_idx es el índice del lote actual, (datos, etiqueta) son los datos y la etiqueta del lote actual . los datos suelen ser un tensor que contiene varios puntos de datos, y cada punto de datos es una muestra. La etiqueta es el tensor de etiqueta correspondiente a estos puntos de datos, que se utiliza para el valor objetivo en el aprendizaje supervisado.

El efecto de ejecución es el siguiente:

Insertar descripción de la imagen aquí

Se puede ver en los resultados de ejecución:

1. lote_idx = 0 significa que es el primer lote de datos

2. data.shape indica que el tamaño de los datos es [64, 1, 28, 28]

El tamaño anterior significa que cada lote de datos incluye 64 imágenes, cada imagen tiene 1 canal gris y el tamaño de la imagen es 28*28.

3. label.shape significa que hay 64 números en el lote y el número total de etiquetas correspondientes a ellos es 64. Cada número tiene una etiqueta.

Preste atención a la diferencia. Debe haber solo 9 categorías de etiquetas reales, porque los números son solo del 1 al 9, y aquí significa que hay 64 valores de etiquetas.

4. La matriz de tensores representa el valor de etiqueta correspondiente a cada una de estas 64 imágenes digitales.

Modelo de proceso de entrenamiento y prueba.

Con los preparativos previos realizados, podemos comenzar a entrenar y probar el modelo.

Aquí está el código de entrenamiento:

# 在使用 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

El efecto de ejecución es el siguiente:

Insertar descripción de la imagen aquí
Omitir el medio...
Insertar descripción de la imagen aquí

Se puede ver que el valor de pérdida final es muy, muy pequeño, 0,0239.

El último paso es la prueba. El proceso de prueba es básicamente el mismo que el de capacitación. El código es el siguiente:

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

Los resultados de ejecución son los siguientes:

Insertar descripción de la imagen aquí

Se puede ver que la precisión de la prueba es del 98%, que sigue siendo muy alta.

Lo anterior es el proceso de diseñar y entrenar una red neuronal desde cero.

Resumen y encapsulación de código.

Lo anterior se explica y describe de acuerdo con cada parte funcional, lo que puede resultar un poco confuso. Encapsulamos el código anterior de la siguiente manera.

código de entrenamiento

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 prueba

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