Condivisione della tecnologia

Progettare una rete neurale da zero: realizzare il riconoscimento delle cifre scritte a mano

2024-07-12

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

Prefazione

Per comprendere meglio le reti neurali, è molto opportuno comprendere il principio di funzionamento e il processo generale delle reti neurali strato dopo strato partendo dal piccolo compito di riconoscimento delle cifre scritte a mano.

Questo articolo completerà un'attività di riconoscimento dei numeri tramite scrittura per spiegare come progettare, implementare e addestrare una rete neurale feedforward standard, al fine di avere una comprensione più specifica e percettiva delle reti neurali.

Panoramica

Nello specifico, dobbiamo progettare e addestrare una rete neurale a 3 strati. Questa rete neurale prenderà le immagini digitali come input Dopo il calcolo da parte della rete neurale, identificherà i numeri nell'immagine, realizzando così la classificazione delle immagini digitali:

Inserisci qui la descrizione dell'immagine

In questo processo vengono spiegati principalmente tre aspetti: la progettazione e l'implementazione delle reti neurali, la preparazione e l'elaborazione dei dati di addestramento e il processo di addestramento e test del modello.

Inserisci qui la descrizione dell'immagine

Progettazione e realizzazione di reti neurali

Per progettare una rete neurale per l'elaborazione dei dati immagine, è necessario innanzitutto chiarire la dimensione e il formato dei dati immagine di input.

Inserisci qui la descrizione dell'immagine

L'immagine che elaboreremo è un'immagine del canale grigio di dimensioni 28 × 28 pixel (il formato del set di dati MNIST stesso).

Questa immagine grigia ne include 2828 = 784 punti dati, dobbiamo prima appiattirli a 1Vettore di dimensione 784:

Inserisci qui la descrizione dell'immagine

Quindi inserisci questo vettore nella rete neurale. Utilizzeremo una rete neurale a tre strati per elaborare il vettore x corrispondente all'immagine. Lo strato di input deve ricevere il vettore immagine a 784 dimensioni x rete neurale. neuroni da ricevere, quindi lo strato di input contiene 784 neuroni:

Inserisci qui la descrizione dell'immagine

Il livello nascosto viene utilizzato per l'estrazione delle caratteristiche, elaborando i vettori delle caratteristiche di input in vettori delle caratteristiche di livello superiore.

Poiché l'immagine della cifra scritta a mano non è complessa, qui impostiamo il numero di neuroni nello strato nascosto su 256, in modo che ci sia uno strato lineare di dimensione 784*256 tra lo strato di input e lo strato nascosto:

Inserisci qui la descrizione dell'immagine

Può convertire un vettore di input a 784 dimensioni in un vettore di output a 256 dimensioni, che continuerà a propagarsi in avanti verso lo strato di output.

Poiché l'immagine digitale verrà infine riconosciuta come dieci possibili numeri da 0 a 9, lo strato di output deve definire 10 neuroni che corrispondano a questi dieci numeri:

Inserisci qui la descrizione dell'immagine

Dopo che il vettore a 256 dimensioni è stato calcolato attraverso lo strato lineare tra lo strato nascosto e lo strato di output, si ottiene un risultato di output a 10 dimensioni. Questo vettore a 10 dimensioni rappresenta il punteggio di previsione di 10 numeri:

Inserisci qui la descrizione dell'immagine

Per continuare a ottenere la probabilità prevista di 10 numeri, dobbiamo anche inserire l'output del livello di output nel livello softmax. Il livello softmax convertirà il vettore a 10 dimensioni in 10 valori di probabilità da P0 a P9 ciascuno Il valore di probabilità corrisponde a un numero, ovvero la possibilità che l'immagine in input sia un certo numero. Inoltre, la somma dei 10 valori di probabilità da P0 a P9 è 1. Ciò è determinato dalle proprietà del valore. funzione softmax:

Inserisci qui la descrizione dell'immagine

Quanto sopra è l'idea progettuale della rete neurale. Successivamente, utilizziamo il framework PyTorch per implementarla.

Per prima cosa implementa la nostra rete neurale:

Inserisci qui la descrizione dell'immagine

codice mostrato come di seguito:

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

Preparazione ed elaborazione dei dati formativi

Quindi preparare il set di dati:

Inserisci qui la descrizione dell'immagine

Per ottenere il set di dati, puoi anche utilizzare il seguente metodo per scaricarlo dal sito Web ufficiale 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

I dati scaricati in questo modo verranno automaticamente archiviati in train_data e test_data. Di conseguenza, un pacchetto di strumenti verrà scaricato nella directory specificata nel codice:

Inserisci qui la descrizione dell'immagine

Ma questo non è un metodo universale. Nel lavoro e nello studio futuri, ci saranno molti set di dati di cui avremo bisogno per operare ed elaborare noi stessi, il che non sarà conveniente come quello sopra. Pertanto, qui viene introdotto un metodo più generale. ovvero scaricare i dati nativi dal sito Web ufficiale Un set di dati è, sì, un mucchio di immagini!

Salviamo i dati scaricati nelle seguenti due cartelle:

Inserisci qui la descrizione dell'immagine

Salviamo i dati in due directory, rispettivamente train e test, dove train ha 60.000 dati e test ha 10.000 dati, che vengono utilizzati rispettivamente per l'addestramento e il test del modello.

Sia la directory train che quella test includono dieci sottodirectory e i nomi delle sottodirectory corrispondono ai numeri nell'immagine. Ad esempio, un'immagine del numero 3 viene salvata in una cartella denominata 3:

Inserisci qui la descrizione dell'immagine

dove il nome dell'immagine è una firma di stringa casuale.

Dopo aver completato la preparazione dei dati, viene implementata la funzione di lettura dei dati. I principianti devono solo conoscere il processo generale di elaborazione dei dati quando imparano questa parte.

Il codice è implementato come segue:

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

Dai un'occhiata ai risultati della corsa:

Inserisci qui la descrizione dell'immagine

Puoi vedere che sono stati ottenuti sia i dati di allenamento che quelli di test, il che è completamente coerente con quanto detto prima.

Quindi utilizziamo train_loader per implementare la lettura di dati in piccoli batch:

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

I risultati in esecuzione sono i seguenti:

Inserisci qui la descrizione dell'immagine

Possiamo quindi eseguire il loop train_loader per ottenere ogni mini-batch di dati:

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

Ecco una semplice spiegazione dell'istruzione del ciclo precedente che la renderà più chiara:

enumerare(): Questa è la funzione incorporata di Python, che viene utilizzata per combinare un oggetto dati attraversabile (come una lista, una tupla o una stringa) in una sequenza di indici ed elencare i dati e i pedici dei dati allo stesso tempo, ovvero ottenere il indice e valore allo stesso tempo. Qui viene utilizzato per scorrere ogni batch in train_loader, dove batch_idx è l'indice del batch corrente (a partire da 0), (data, label) sono i dati e l'etichetta del batch corrente.

per batch_idx, (dati, etichetta) in enumerate(train_loader) :: Il significato di questa istruzione del ciclo è che per ogni batch in train_loader viene eseguito il codice nel corpo del ciclo.In ogni iterazione del ciclo, batch_idx è l'indice del batch corrente, (dati, etichetta) sono i dati e l'etichetta del batch corrente . i dati sono solitamente un tensore contenente più punti dati, ciascun punto dati è un'etichetta campione è il tensore dell'etichetta corrispondente a questi punti dati, che viene utilizzato per il valore target nell'apprendimento supervisionato;

L'effetto di corsa è il seguente:

Inserisci qui la descrizione dell'immagine

Si può vedere dai risultati correnti:

1. batch_idx = 0 significa che è il primo batch di dati

2. data.shape indica che la dimensione dei dati è [64, 1, 28, 28]

La dimensione sopra indicata significa che ogni batch di dati include 64 immagini, ogni immagine ha 1 canale di grigio e la dimensione dell'immagine è 28*28.

3. label.shape significa che ci sono 64 numeri nel lotto e il numero totale di etichette ad essi corrispondenti è 64. Ogni numero ha un'etichetta.

Presta attenzione alla differenza. Devono esserci solo 9 categorie di etichette reali, perché i numeri sono solo da 1 a 9, e qui significa che ci sono 64 valori di etichetta.

4. La matrice tensore rappresenta il valore dell'etichetta corrispondente a ciascuna di queste 64 immagini digitali.

Processo di formazione e test del modello

Una volta messi in atto i preparativi precedenti, possiamo iniziare ad addestrare e testare il modello.

Ecco il codice della formazione:

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

L'effetto di corsa è il seguente:

Inserisci qui la descrizione dell'immagine
Tralascia la parte centrale...
Inserisci qui la descrizione dell'immagine

Si può vedere che il valore della perdita finale è molto, molto piccolo, 0,0239.

L'ultimo passaggio è il test. Il processo di test è sostanzialmente lo stesso dell'addestramento. Il codice è il seguente:

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

I risultati in esecuzione sono i seguenti:

Inserisci qui la descrizione dell'immagine

Si può vedere che la precisione del test è del 98%, che è ancora molto elevata.

Quanto sopra è il processo di progettazione e addestramento di una rete neurale da zero.

Riepilogo e incapsulamento del codice

Quanto sopra è spiegato e descritto in base a ciascuna parte funzionale, il che può creare un po' di confusione. Incapsuliamo il codice sopra riportato come segue.

codice di formazione

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

codice di prova

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