Обмен технологиями

Проектирование нейронной сети с нуля: реализация распознавания рукописных цифр

2024-07-12

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

Предисловие

Чтобы лучше понять нейронные сети, очень полезно понять принцип работы и общий процесс нейронных сетей слой за слоем на небольшой задаче по распознаванию рукописных цифр.

В этой статье будет выполнена задача по распознаванию чисел по почерку, чтобы объяснить, как спроектировать, реализовать и обучить стандартную нейронную сеть прямого распространения, чтобы иметь более конкретное и перцептивное понимание нейронных сетей.

Обзор

В частности, нам нужно спроектировать и обучить трехслойную нейронную сеть. Эта нейронная сеть будет принимать на вход цифровые изображения. После расчета нейронная сеть будет идентифицировать числа на изображении, тем самым реализуя классификацию цифровых изображений:

Вставьте сюда описание изображения

В этом процессе в основном объясняются три аспекта: проектирование и реализация нейронных сетей, подготовка и обработка обучающих данных, а также процесс обучения и тестирования модели.

Вставьте сюда описание изображения

Проектирование и реализация нейронных сетей

Чтобы спроектировать нейронную сеть для обработки данных изображения, необходимо сначала уточнить размер и формат входных данных изображения.

Вставьте сюда описание изображения

Изображение, которое мы собираемся обработать, представляет собой изображение серого канала размером 28 × 28 пикселей (формат самого набора данных MNIST).

Это серое изображение включает в себя 2828 = 784 точки данных, нам нужно сначала сравнять их до 1Вектор размера 784:

Вставьте сюда описание изображения

Затем введите этот вектор в нейронную сеть. Мы будем использовать трехслойную нейронную сеть для обработки вектора x, соответствующего изображению. Входной слой должен получить 784-мерный вектор изображения x. Каждое измерение данных в x имеет. нейронной сети для получения, поэтому входной слой содержит 784 нейрона:

Вставьте сюда описание изображения

Скрытый слой используется для извлечения признаков, обработки входных векторов признаков в векторы признаков более высокого уровня.

Поскольку изображение рукописной цифры не является сложным, здесь мы устанавливаем количество нейронов в скрытом слое равным 256, чтобы между входным слоем и скрытым слоем был линейный слой размером 784*256:

Вставьте сюда описание изображения

Он может преобразовать 784-мерный входной вектор в 256-мерный выходной вектор, который будет продолжать распространяться вперед к выходному слою.

Поскольку цифровое изображение в конечном итоге будет распознаваться как десять возможных чисел от 0 до 9, выходному слою необходимо определить 10 нейронов, соответствующих этим десяти числам:

Вставьте сюда описание изображения

После вычисления 256-мерного вектора через линейный слой между скрытым слоем и выходным слоем получается 10-мерный выходной результат. Этот 10-мерный вектор представляет собой оценку прогноза из 10 чисел:

Вставьте сюда описание изображения

Чтобы продолжить получать прогнозируемую вероятность 10 чисел, нам также необходимо ввести выходные данные выходного слоя в слой softmax. Слой softmax преобразует 10-мерный вектор в 10 значений вероятности от P0 до P9. Значение вероятности соответствует числу. То есть вероятность того, что входное изображение представляет собой определенное число. Кроме того, сумма 10 значений вероятности от P0 до P9 равна 1. Это определяется свойствами. функция softmax:

Вставьте сюда описание изображения

Вышеизложенное представляет собой идею проектирования нейронной сети. Далее мы используем фреймворк PyTorch для ее реализации.

Сначала реализуем нашу нейронную сеть:

Вставьте сюда описание изображения

код показан ниже:

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

Подготовка и обработка обучающих данных

Затем подготовьте набор данных:

Вставьте сюда описание изображения

Для получения набора данных вы также можете использовать следующий метод, чтобы загрузить его с официального сайта 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

Загруженные таким образом данные будут автоматически сохраняться в train_data и test_data. Соответственно, пакет инструментов будет загружен в каталог, который мы указали в коде:

Вставьте сюда описание изображения

Но это не универсальный метод. В дальнейшей работе и учебе нам придется оперировать и обрабатывать множество наборов данных, что будет не так удобно, как описано выше. Поэтому здесь представлен более общий метод. то есть скачать родные данные с официального сайта. Набор данных - это да, куча изображений!

Сохраняем загруженные данные в следующие две папки:

Вставьте сюда описание изображения

Мы сохраняем данные в два каталога: train и test соответственно, где train содержит 60 000 данных, а test — 10 000 данных, которые используются для обучения и тестирования модели соответственно.

Каталоги train и test включают по десять подкаталогов, имена которых соответствуют числам на изображении. Например, изображение цифры 3 сохраняется в папке с именем 3:

Вставьте сюда описание изображения

где имя изображения представляет собой случайную строковую подпись.

После завершения подготовки данных реализуется функция чтения данных. При изучении этой части новичкам необходимо знать только общий процесс обработки данных.

Код реализован следующим образом:

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

Взгляните на текущие результаты:

Вставьте сюда описание изображения

Вы можете видеть, что были получены как обучающие, так и тестовые данные, что полностью соответствует тому, что мы говорили ранее.

Затем мы используем train_loader для реализации небольшого пакетного чтения данных:

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

Результаты бега следующие:

Вставьте сюда описание изображения

Затем мы можем пройти через train_loader, чтобы получить каждый мини-пакет данных:

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

Вот простое объяснение приведенного выше оператора цикла, которое сделает его более понятным:

перечислить(): Это встроенная функция Python, которая используется для объединения проходимого объекта данных (например, списка, кортежа или строки) в индексную последовательность и одновременного вывода списка данных и индексов данных, то есть получения индекс и значение одновременно. Здесь он используется для перебора каждого пакета в train_loader, где пакет_idx — индекс текущего пакета (начиная с 0), (данные, метка) — данные и метка текущего пакета.

для batch_idx, (данные, метка) в enumerate(train_loader):: Смысл этого оператора цикла заключается в том, что для каждого пакета в train_loader выполняется код тела цикла.На каждой итерации цикла пакет_idx — это индекс текущего пакета, (данные, метка) — данные и метка текущего пакета. . данные обычно представляют собой тензор, содержащий несколько точек данных, каждая точка данных представляет собой образец метки, соответствующий этим точкам данных, который используется для целевого значения в контролируемом обучении;

Эффект от бега следующий:

Вставьте сюда описание изображения

Это видно по результатам бега:

1. Batch_idx = 0 означает, что это первый пакет данных.

2. data.shape указывает, что размер данных равен [64, 1, 28, 28]

Вышеуказанный размер означает, что каждый пакет данных включает 64 изображения, каждое изображение имеет 1 канал серого, а размер изображения составляет 28*28.

3. label.shape означает, что в пакете 64 числа и общее количество соответствующих им меток равно 64. У каждого числа есть метка.

Обратите внимание на разницу: реальных категорий меток должно быть всего 9, потому что цифры только от 1 до 9, и здесь это означает, что значений меток 64.

4. Тензорный массив представляет значение метки, соответствующее каждому из этих 64 цифровых изображений.

Процесс обучения и тестирования модели

Сделав предварительные приготовления, мы можем приступить к обучению и тестированию модели.

Вот код обучения:

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

Эффект от бега следующий:

Вставьте сюда описание изображения
Опустить середину…
Вставьте сюда описание изображения

Видно, что итоговое значение потерь очень и очень маленькое, 0,0239.

Последний шаг — тестирование. Процесс тестирования в основном такой же, как и обучение. Код выглядит следующим образом:

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

Результаты бега следующие:

Вставьте сюда описание изображения

Видно, что точность теста составляет 98%, что по-прежнему очень высоко.

Выше описан процесс проектирования и обучения нейронной сети с нуля.

Резюме и инкапсуляция кода

Вышеупомянутое объяснено и описано для каждой функциональной части, что может немного сбить с толку. Мы инкапсулируем приведенный выше код следующим образом.

код обучения

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

тестовый код

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