Technologieaustausch

Entwerfen eines neuronalen Netzwerks von Grund auf: Realisieren der handschriftlichen Ziffernerkennung

2024-07-12

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

Vorwort

Um neuronale Netze besser zu verstehen, ist es sehr angebracht, das Funktionsprinzip und den allgemeinen Prozess neuronaler Netze Schicht für Schicht anhand der kleinen Aufgabe der handschriftlichen Ziffernerkennung zu verstehen.

In diesem Artikel wird eine handschriftliche Aufgabe zur Zahlenerkennung gelöst, um zu erklären, wie ein Standard-Feedforward-Neuronales Netzwerk entworfen, implementiert und trainiert wird, um ein spezifischeres und wahrnehmungsbezogeneres Verständnis neuronaler Netzwerke zu erhalten.

Überblick

Insbesondere müssen wir ein dreischichtiges neuronales Netzwerk entwerfen und trainieren. Dieses neuronale Netzwerk verwendet digitale Bilder als Eingabe. Nach der Berechnung durch das neuronale Netzwerk identifiziert es die Zahlen im Bild und realisiert so die Klassifizierung digitaler Bilder.

Fügen Sie hier eine Bildbeschreibung ein

Dabei werden vor allem drei Aspekte erläutert: der Entwurf und die Implementierung neuronaler Netze, die Aufbereitung und Verarbeitung von Trainingsdaten sowie der Modelltrainings- und Testprozess.

Fügen Sie hier eine Bildbeschreibung ein

Design und Implementierung neuronaler Netze

Um ein neuronales Netzwerk zur Verarbeitung von Bilddaten zu entwerfen, müssen zunächst die Größe und das Format der eingegebenen Bilddaten geklärt werden.

Fügen Sie hier eine Bildbeschreibung ein

Das Bild, das wir verarbeiten werden, ist ein Graukanalbild mit einer Größe von 28 × 28 Pixeln (das Format des MNIST-Datensatzes selbst).

Dieses graue Bild enthält 2828 = 784 Datenpunkte, wir müssen es zuerst auf 1 reduzierenVektor der Größe 784:

Fügen Sie hier eine Bildbeschreibung ein

Geben Sie diesen Vektor dann in das neuronale Netzwerk ein, um den dem Bild entsprechenden Vektor x zu verarbeiten. Die Eingabeschicht muss den 784-dimensionalen Bildvektor x empfangen Neuronale Netzwerke zum Empfangen, die Eingabeschicht enthält also 784 Neuronen:

Fügen Sie hier eine Bildbeschreibung ein

Die verborgene Ebene wird zur Merkmalsextraktion verwendet und verarbeitet die eingegebenen Merkmalsvektoren in Merkmalsvektoren höherer Ebene.

Da das handgeschriebene Ziffernbild nicht komplex ist, setzen wir hier die Anzahl der Neuronen in der verborgenen Schicht auf 256, sodass zwischen der Eingabeschicht und der verborgenen Schicht eine lineare Schicht mit der Größe 784 * 256 entsteht:

Fügen Sie hier eine Bildbeschreibung ein

Es kann einen 784-dimensionalen Eingabevektor in einen 256-dimensionalen Ausgabevektor umwandeln, der sich weiter vorwärts zur Ausgabeschicht ausbreitet.

Da das digitale Bild letztendlich als zehn mögliche Zahlen von 0 bis 9 erkannt wird, muss die Ausgabeschicht 10 Neuronen definieren, die diesen zehn Zahlen entsprechen:

Fügen Sie hier eine Bildbeschreibung ein

Nachdem der 256-dimensionale Vektor durch die lineare Schicht zwischen der verborgenen Schicht und der Ausgabeschicht berechnet wurde, wird ein 10-dimensionales Ausgabeergebnis erhalten. Dieser 10-dimensionale Vektor stellt die Vorhersagebewertung von 10 Zahlen dar:

Fügen Sie hier eine Bildbeschreibung ein

Um weiterhin die vorhergesagte Wahrscheinlichkeit von 10 Zahlen zu erhalten, müssen wir auch die Ausgabe der Ausgabeschicht in die Softmax-Schicht eingeben. Die Softmax-Schicht wandelt den 10-dimensionalen Vektor in jeweils 10 Wahrscheinlichkeitswerte P0 bis P9 um Der Wahrscheinlichkeitswert entspricht einer Zahl. Das heißt, die Möglichkeit, dass das Eingabebild eine bestimmte Zahl ist. Darüber hinaus beträgt die Summe der 10 Wahrscheinlichkeitswerte von P0 bis P9. Dies wird durch die Eigenschaften des bestimmt Softmax-Funktion:

Fügen Sie hier eine Bildbeschreibung ein

Das Obige ist die Designidee des neuronalen Netzwerks. Als nächstes verwenden wir das PyTorch-Framework, um es zu implementieren.

Implementieren Sie zunächst unser neuronales Netzwerk:

Fügen Sie hier eine Bildbeschreibung ein

Code wie folgt anzeigen:

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

Aufbereitung und Verarbeitung von Trainingsdaten

Bereiten Sie als Nächstes den Datensatz vor:

Fügen Sie hier eine Bildbeschreibung ein

Um den Datensatz zu erhalten, können Sie ihn auch mit der folgenden Methode von der offiziellen PyTorch-Website herunterladen:

# 准备数据集
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

Die auf diese Weise heruntergeladenen Daten werden automatisch in train_data und test_data gespeichert. Dementsprechend wird ein Toolpaket in das Verzeichnis heruntergeladen, das wir im Code angegeben haben:

Fügen Sie hier eine Bildbeschreibung ein

Dies ist jedoch keine universelle Methode. In der Zukunft müssen wir viele Datensätze selbst bedienen und verarbeiten. Daher wird hier eine allgemeinere Methode vorgestellt. Das heißt, die nativen Daten von der offiziellen Website herunterzuladen. Ein Datensatz besteht, ja, aus einer Menge Bildern!

Wir speichern die heruntergeladenen Daten in den folgenden zwei Ordnern:

Fügen Sie hier eine Bildbeschreibung ein

Wir speichern die Daten in zwei Verzeichnissen, train und test, wobei train 60.000 Daten und test 10.000 Daten enthält, die jeweils für das Modelltraining und -tests verwendet werden.

Sowohl das Trainings- als auch das Testverzeichnis umfassen zehn Unterverzeichnisse, und die Namen der Unterverzeichnisse entsprechen den Nummern im Bild. Beispielsweise wird ein Bild der Nummer 3 in einem Ordner mit dem Namen 3 gespeichert:

Fügen Sie hier eine Bildbeschreibung ein

Dabei ist der Name des Bildes eine zufällige Zeichenfolgensignatur.

Nach Abschluss der Datenvorbereitung ist die Datenlesefunktion implementiert. Anfänger müssen beim Erlernen dieses Teils nur den allgemeinen Datenverarbeitungsprozess kennen.

Der Code ist wie folgt implementiert:

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

Werfen Sie einen Blick auf die Laufergebnisse:

Fügen Sie hier eine Bildbeschreibung ein

Sie können sehen, dass sowohl die Trainingsdaten als auch die Testdaten erhalten wurden, was vollständig mit dem übereinstimmt, was wir zuvor gesagt haben.

Dann verwenden wir train_loader, um das Lesen kleiner Batch-Daten zu implementieren:

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

Die Laufergebnisse sind wie folgt:

Fügen Sie hier eine Bildbeschreibung ein

Wir können dann train_loader durchlaufen, um jeden Mini-Datenstapel zu erhalten:

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

Hier ist eine einfache Erklärung der obigen Schleifenanweisung, die sie klarer macht:

aufzählen(): Dies ist die in Python integrierte Funktion, mit der ein durchlauffähiges Datenobjekt (z. B. eine Liste, ein Tupel oder eine Zeichenfolge) zu einer Indexsequenz kombiniert und gleichzeitig die Daten und Datenindizes aufgelistet, dh abgerufen werden Index und Wert zugleich. Hier wird es verwendet, um jeden Stapel in train_loader zu durchlaufen, wobei „batch_idx“ der Index des aktuellen Stapels ist (beginnend bei 0) und (Daten, Label) die Daten und das Label des aktuellen Stapels sind.

für batch_idx, (Daten, Label) in enumerate(train_loader):: Die Bedeutung dieser Schleifenanweisung besteht darin, dass für jeden Stapel in train_loader der Code im Schleifenkörper ausgeführt wird.In jeder Schleifeniteration ist „batch_idx“ der Index des aktuellen Batches, (data, label) sind die Daten und das Label des aktuellen Batches . Daten sind normalerweise Tensoren, die mehrere Datenpunkte enthalten. Jeder Datenpunkt ist eine Stichprobe. Die Beschriftung ist der diesen Datenpunkten entsprechende Beschriftungstensor, der beim überwachten Lernen als Zielwert verwendet wird.

Der Laufeffekt ist wie folgt:

Fügen Sie hier eine Bildbeschreibung ein

Aus den Laufergebnissen geht hervor:

1. batch_idx = 0 bedeutet, dass es sich um den ersten Datenstapel handelt

2. data.shape gibt an, dass die Datengröße [64, 1, 28, 28] beträgt.

Die obige Größe bedeutet, dass jeder Datenstapel 64 Bilder enthält, jedes Bild einen Graukanal hat und die Bildgröße 28 * 28 beträgt.

3. label.shape bedeutet, dass der Stapel 64 Nummern enthält und die Gesamtzahl der entsprechenden Etiketten 64 beträgt. Jede Nummer hat eine Beschriftung.

Beachten Sie den Unterschied. Es dürfen nur 9 echte Etikettenkategorien vorhanden sein, da die Zahlen nur 1 bis 9 betragen. Dies bedeutet, dass es 64 Etikettenwerte gibt.

4. Das Tensor-Array stellt den Beschriftungswert dar, der jedem dieser 64 digitalen Bilder entspricht.

Modellschulungs- und Testprozess

Wenn die vorherigen Vorbereitungen getroffen sind, können wir mit dem Training und Testen des Modells beginnen.

Hier ist der Trainingscode:

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

Der Laufeffekt ist wie folgt:

Fügen Sie hier eine Bildbeschreibung ein
Lassen Sie die Mitte weg ...
Fügen Sie hier eine Bildbeschreibung ein

Es ist ersichtlich, dass der endgültige Verlustwert sehr, sehr klein ist und 0,0239 beträgt.

Der letzte Schritt ist das Testen. Der Testvorgang ist im Wesentlichen derselbe wie beim Training.

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

Die Laufergebnisse sind wie folgt:

Fügen Sie hier eine Bildbeschreibung ein

Man erkennt, dass die Genauigkeit des Tests bei 98 % liegt, was immer noch sehr hoch ist.

Das Obige ist der Prozess des Entwurfs und Trainings eines neuronalen Netzwerks von Grund auf.

Zusammenfassung und Codekapselung

Das Obige wird für jeden Funktionsteil erklärt und beschrieben, was etwas verwirrend sein kann. Wir kapseln den obigen Code wie folgt.

Trainingscode

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

Testcode

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