技術共有

ニューラルネットワークをゼロから設計:手書き数字認識を実現

2024-07-12

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

序文

ニューラル ネットワークをより深く理解するには、手書きの数字認識という小さなタスクから、ニューラル ネットワークの動作原理と一般的なプロセスを層ごとに理解することが非常に適切です。

この記事では、ニューラル ネットワークをより具体的かつ知覚的に理解するために、標準的なフィードフォワード ニューラル ネットワークを設計、実装、トレーニングする方法を説明する手書きによる数値認識タスクを完了します。

概要

具体的には、3 層のニューラル ネットワークを設計してトレーニングする必要があります。このニューラル ネットワークは、デジタル画像を入力として受け取り、ニューラル ネットワークによる計算の後、画像内の数値を識別し、それによってデジタル画像の分類を実現します。

ここに画像の説明を挿入します

このプロセスでは、ニューラル ネットワークの設計と実装、トレーニング データの準備と処理、モデルのトレーニングとテストのプロセスの 3 つの側面が主に説明されます。

ここに画像の説明を挿入します

ニューラルネットワークの設計と実装

画像データを処理するニューラルネットワークを設計するには、まず入力画像データのサイズと形式を明確にする必要があります。

ここに画像の説明を挿入します

私たちが処理しようとしている画像は、サイズ 28 × 28 ピクセル (MNIST データセット自体の形式) のグレー チャネル画像です。

このグレーの画像には 28 個の画像が含まれています28 = 784 データポイント、最初に 1 に平坦化する必要がありますサイズ 784 のベクトル:

ここに画像の説明を挿入します

次に、このベクトルをニューラル ネットワークに入力します。3 層のニューラル ネットワークを使用して、画像に対応するベクトル x を処理します。入力層は、x の各次元のデータを受け取る必要があります。ニューラル ネットワークを受信するため、入力層には 784 個のニューロンが含まれます。

ここに画像の説明を挿入します

隠れ層は特徴抽出に使用され、入力特徴ベクトルをより高いレベルの特徴ベクトルに処理します。

手書きの数字の画像は複雑ではないため、ここでは隠れ層のニューロンの数を 256 に設定し、入力層と隠れ層の間に 784*256 サイズの線形層が存在するようにします。

ここに画像の説明を挿入します

784 次元の入力ベクトルを 256 次元の出力ベクトルに変換でき、出力層に前方に伝播し続けます。

デジタル画像は最終的に 0 から 9 までの 10 個の数値として認識されるため、出力層はこれら 10 個の数値に対応する 10 個のニューロンを定義する必要があります。

ここに画像の説明を挿入します

隠れ層と出力層の間の線形層を通じて 256 次元のベクトルが計算された後、10 次元の出力結果が得られます。この 10 次元のベクトルは 10 個の数値の予測スコアを表します。

ここに画像の説明を挿入します

引き続き 10 個の数値の予測確率を取得するには、出力層の出力をソフトマックス層に入力する必要があります。ソフトマックス層は、10 次元ベクトルをそれぞれ 10 個の確率値 P0 から P9 に変換します。確率値は数値に対応します。また、P0 から P9 までの 10 個の確率値の合計は 1 になります。これは、入力画像の性質によって決まります。ソフトマックス関数:

ここに画像の説明を挿入します

以上がニューラルネットワークの設計アイデアです。次に、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 公式 Web サイトからダウンロードすることもできます。

# 准备数据集
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 に自動的に保存されます。これに応じて、コードで指定したディレクトリにツール パッケージがダウンロードされます。

ここに画像の説明を挿入します

しかし、これは普遍的な方法ではなく、将来の仕事や研究では、多くのデータセットを自分で操作して処理する必要があるため、ここではより一般的な方法を紹介します。これは、公式 Web サイトからネイティブ データをダウンロードすることです。データ セットは、はい、画像の束です。

ダウンロードしたデータは次の 2 つのフォルダーに保存されます。

ここに画像の説明を挿入します

データを 2 つのディレクトリ (それぞれ train と test) に保存します。train には 60,000 個のデータがあり、test には 10,000 個のデータがあり、それぞれモデルのトレーニングとテストに使用されます。

train ディレクトリと test ディレクトリには両方とも 10 個のサブディレクトリが含まれており、サブディレクトリの名前は画像内の番号に対応しています。たとえば、数字の 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 の各バッチを反復処理するために使用されます。ここで、batch_idx は現在のバッチのインデックス (0 から始まります)、(data, label) は現在のバッチのデータとラベルです。

batch_idx、(データ、ラベル) を enumerate(train_loader) で次のように記述します。 このループ ステートメントの意味は、train_loader のバッチごとに、ループ本体のコードが実行されることです。各ループ反復では、batch_idx は現在のバッチのインデックス、(data, label) は現在のバッチのデータとラベルです。 。 data は通常、複数のデータ ポイントを含むテンソルであり、各データ ポイントはサンプルです。label はこれらのデータ ポイントに対応するラベル テンソルであり、教師あり学習のターゲット値に使用されます。

ランニング効果は以下の通りです。

ここに画像の説明を挿入します

実行結果からわかります。

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