深層学習(Deep Learning)で手書き数字認識を行うプログラム(AI)をなるべくわかりやすく解説

深層学習(Deep Learning)で手書き数字認識を行うプログラム(AI)をなるべくわかりやすく解説

今回の記事では実際に深層学習のプログラムを書く方法について解説しようと思います.

作成するプログラムは,深層学習によって手書きの数字の「0」から「9」までを自動認識するニューラルネットワーク(AI)です.

プログラミング言語はPython,深層学習用ライブラリはPyTorchを使用します.まだこれらのインストールや設定が完了していない方はこちらの記事を参考に,設定をしてみて下さい.

手書きの数字を認識するニューラルネットワーク

使用するデータセット: MNIST

今回はWeb上で公開されているMNISTと呼ばれる手書き数字認識用のデータセットを使用します.

MNISTは「0」から「9」までの手書き数字の画像で構成されており,合計で60,000枚あります.MNISTは人工知能に関する学会でもよく使用されているデータセットです.

MNISTデータセットの例

上図のように,各画像には正解ラベル(数字の名称)が割り当てられています.上記のような手書き数字の画像をニューラルネットワークに入力して,各正解ラベルを出力するようにニューラルネットワークの学習を行います.

各画像は縦横28画素のグレースケール画像になります.つまり,各画像は\((28, 28, 1)\)の3次元データとして表現されます(以下に例を示します).この3次元データをもとにニューラルネットワークの学習を行います.

各数値が画素値を表しています.
引用[https://ml4a.github.io/ml4a/jp/neural_networks/]

PythonとPyTorchによるプログラミング

それでは実際にPythonとPyTorchを用いて,手書き数字認識を行うニューラルネットワークのプログラムを書いてみましょう.

Pythonは広く使用されているプログラミング言語の1つです.PyTorchは深層学習を簡単に実装可能にするPython用のライブラリになります.

スポンサーリンク

1. エディタの起動

まず,プログラムを書くためのエディタを起動しましょう.私は普段はVisual studio codeを使用していますが,説明の利便性のため今回はJupyter notebookを使用します.Jupyter notebookも幅広く利用されている便利なツールなので,扱いに慣れておきましょう.

Anacondaを利用してPython環境を構築された方は,すでにJupyter notebookはインストールされていますので,特別に何かをする必要はありません.もし,まだJupyter notebookのインストールをされていない方は,以下の記事を参考に,インストールしましょう.

それでは,ターミナル(コマンドプロンプト)で以下のコマンドを実行して,Jupyter notebookを起動しましょう.

$ jupyter notebook

2. Jupyter notebookの使い方

Jupyter Notebook を起動後,プログラムを管理したいフォルダに移動し,「New」ボタン => 「Python3」を選択し,ipynbファイルを作成します.

そうすると以下のような画面になると思います.このセルと呼ばれる部分にプログラムを書いていきます.

Jupyter notebookでよく使用するコマンド

  • セル内のPythonプログラムを実行:Ctrl + Enter
  • 下にセルを追加: Shift + Enter
  • セルの削除: escDを2回クリック
スポンサーリンク

3. プログラミング開始

それではJupyter notebookを使用して,実際にプログラミングしていきましょう.

必要なライブラリのインポート

まずは,今回使用するプログラムで必要なライブラリをインポートします.ライブラリを使用することで,ゼロからすべてのプログラムを書かなくても,色々便利な機能を簡単に実行することができます.ライブラリのインポートはPythonコードの一番上で行うことが一般的です.

# データセットに関するライブラリ
import numpy as np
import torch
import torchvision
import torchvision.transforms as transforms

# ニューラルネットワークに関するライブラリ
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

一番上のnumpyは,多次元データを扱うためのライブラリです.先に述べたように,今回は\((28, 28, 1)\)の画像データを扱うため,numpyライブラリを使用します.numpy以下のライブラリはPyTorchに関するライブラリで,深層学習を効率良くプログラミングするためのライブラリになります.

Jupyter notebook上では,以下のような感じになります.

MNISTデータセットの入手

続いて,先ほどインポートしたPyTorchのライブラリを利用してMNISTデータセットをダウンロードします.

Python上で多次元データを扱うにはnumpyが必要ですが,PyTorch上で多次元データを扱うには,numpy形式のデータをTensorと呼ばれる形式に変換する必要があります.その変換を行うための関数が transform = transforms.Compose([transforms.ToTensor()]) の部分になります.この関数をtransformとして定義し,PyTorchが提供している関数 torchvision.datasets.MNISTに渡すことで,データを変換してくれます.

# numpy形式のデータをPyTorch形式のデータに変換するためのツール
transform = transforms.Compose([transforms.ToTensor()])

# MNISTデータセット(学習用)を入手 
trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)

# MNISTデータセット(テスト用)を入手 
testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)

深層学習では,学習用データでニューラルネットワークを訓練し,テスト用データ上でどれだけの性能を示せるのか,でニューラルネットワークの良し悪しを判断します.そのため,上記のプログラムでは学習用データとテスト用データを分けて入手しています.

batch_size(バッチサイズ)というのは,ニューラルネットワークの重みを1回更新するのに何個のデータを用いるのか,を表すハイパーパラメータになります.1つずつのデータを用いて重みを更新することもできますが,バッチサイズ分のデータから算出した損失を用いて一気に更新することで,高速に学習を行うことができます.

ニューラルネットワークの定義

今回は,2つの畳み込み層,2つのMax pooling層,2つの全結合層のCNNを用いて,手書き数字の認識を行いたいと思います.

上記のCNNをPyTorchを使用すると,以下のように記述できます.

# 6層のCNN
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=20, kernel_size=5, stride=1, padding=0)
        self.conv2 = nn.Conv2d(in_channels=20, out_channels=50, kernel_size=5, stride=1, padding=0)
        self.mp1 = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
        self.mp2 = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
        self.fc1 = nn.Linear(in_features=50*4*4, out_features=500, bias=False)
        self.fc2 = nn.Linear(in_features=500, out_features=10, bias=False)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = self.mp1(x)
        x = F.relu(self.conv2(x))
        x = self.mp2(x)
        x = x.view(-1, 50*4*4)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)
   

畳み込み層は,nn.Conv2d()の関数で定義することができます.関数に与える情報としましては,入力画像のチャネル数,この畳み込み層で出力する画像のチャネル数(変換画像枚数),フィルタのサイズ,ストライド,パディング数になります.

プーリング層は,nn.MaxPool2d()の関数で定義可能です.フィルタのサイズ,ストライド,パディング数を情報として与えます.

全結合層は,nn.Linear()の関数で表現することができます.入力ノード数と出力ノードを指定します.なお,今回のプログラムではバイアスのパラメータを除外しています.

init()関数でニューラルネットワーク全体の要素を定義し,forward()関数で,具体的にどのような順序で入力データを処理するかを定義します.

view()関数を用いることで,データの次元(形状)を変更することができます.PyTorchの畳み込み層の関数は(バッチサイズ,チャネル数,縦,横)の4次元データを入力として扱いますが,全結合層は,(バッチサイズ,チャネル数*縦*横)の2次元データを入力として扱います.そのため,上記のプログラムでは,view()関数で4次元データ→2次元データの変換を行ってから,全結合層に入力をしています.

上記で定義したCNNは以下のように関数を呼び出すことで作成することができます.

# CUDA(GPU)が使用可能か確認
use_cuda = torch.cuda.is_available()
device = torch.device("cuda" if use_cuda else "cpu")
# CNNの作成
model = CNN().to(device)
# SGDの定義
optimizer = optim.SGD(model.parameters(), lr=0.01)

ニューラルネットワークの学習

続いて,ニューラルネットワークの学習部分のプログラムを作成します.

CNNにMNISTのデータを入力し,誤差を計算し,CNNの重みを更新するプログラムは以下のようになります.このように,PyTorchを利用することで,誤差逆伝播法やCNNの重み更新のようなやや複雑な計算処理をたったの数行で実行することが可能です.

def train(model, train_loader, optimizer, epoch, device):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data   = data.to(device)        # 画像データをGPU or CPUに転送
        target = target.to(device)      # ラベルデータをGPU or CPUに転送 
        optimizer.zero_grad()           # オプティマイザを初期化
        output = model(data)            # CNNにデータを入力し,出力計算
        loss = F.nll_loss(output, target)     # 誤差の計算
        loss.backward()                       # 誤差逆伝播法により,CNN内の各重みの勾配を計算
        optimizer.step()                      # 誤差逆伝播法で求めた勾配を用いて,各重みの値を更新
        print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))

上記のtrain()関数を以下のようにあらかじめ決めたepoch数だけまわすことで,ニューラルネットワークの学習を行います.

epoch_num = 100
for epoch in range(1, epoch_num + 1):
        train(model, trainloader, optimizer, epoch, device)

ここまでのまとめ

ここまでのプログラムを整理してまとめると以下のようになります.

import numpy as np
import torch
import torchvision
import torchvision.transforms as transforms

import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

# CNNの定義
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=20, kernel_size=5, stride=1, padding=0)
        self.conv2 = nn.Conv2d(in_channels=20, out_channels=50, kernel_size=5, stride=1, padding=0)
        self.mp1 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.mp2 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.fc1 = nn.Linear(in_features=4*4*50, out_features=500, bias=False)
        self.fc2 = nn.Linear(in_features=500, out_features=10, bias=False)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = self.mp1(x)
        x = F.relu(self.conv2(x))
        x = self.mp2(x)
        x = x.view(-1, 4*4*50)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)

# CNNの学習部分
def train(model, train_loader, optimizer, epoch, device):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data   = data.to(device)
        target = target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))


# numpy形式のデータをPyTorch形式のデータに変換するためのツール
transform = transforms.Compose([transforms.ToTensor()])
# 学習データ
trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True, num_workers=2)
#  テストデータ
testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False, num_workers=2)
# CNNの作成
use_cuda = torch.cuda.is_available()
device = torch.device("cuda" if use_cuda else "cpu")
model = CNN().to(device)
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 学習開始
epoch_num = 100
for epoch in range(1, epoch_num + 1):
        train(model, trainloader, optimizer, epoch, device)

        
スポンサーリンク

テスト検証用のプログラム追加

最後に,学習途中のCNNがテストデータ上でどの程度性能を発揮しているか確認するコードを追加したいと思います.

# テスト
def test(args, model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in testloader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction='sum').item() # 損失の加算
            pred = output.argmax(dim=1, keepdim=True) # 最も確率が高いインデックス(クラスID)を取得
            correct += pred.eq(target.view_as(pred)).sum().item() # ラベルと一致する出力結果数をカウント

    test_loss /= len(test_loader.dataset)

    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))

全体のプログラム

まとめとして全体のプログラムを以下に示したいと思います.

import numpy as np
import torch
import torchvision
import torchvision.transforms as transforms

import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

# CNNの定義
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=20, kernel_size=5, stride=1, padding=0)
        self.conv2 = nn.Conv2d(in_channels=20, out_channels=50, kernel_size=5, stride=1, padding=0)
        self.mp1 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.mp2 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.fc1 = nn.Linear(in_features=4*4*50, out_features=500, bias=False)
        self.fc2 = nn.Linear(in_features=500, out_features=10, bias=False)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = self.mp1(x)
        x = F.relu(self.conv2(x))
        x = self.mp2(x)
        x = x.view(-1, 4*4*50)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)

# CNNの学習部分
def train(model, train_loader, optimizer, epoch, device):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data   = data.to(device)
        target = target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))

# CNNのテスト
def test(model, test_loader, epoch, device):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss
            pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))

# numpy形式のデータをPyTorch形式のデータに変換するためのツール
transform = transforms.Compose([transforms.ToTensor()])
# 学習データ
trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True, num_workers=2)
#  テストデータ
testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False, num_workers=2)
# CNNの作成
use_cuda = torch.cuda.is_available()
device = torch.device("cuda" if use_cuda else "cpu")
model = CNN().to(device)
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 学習開始
epoch_num = 100
for epoch in range(1, epoch_num + 1):
     train(model, trainloader, optimizer, epoch, device) # CNNの学習
     test(model, testloader, epoch, device) # CNNのテスト

今回作成したプログラムはこちらで公開していますので,適宜ダウンロードして使用してみて下さい.

まとめ

今回は手書き数字をCNNで認識するためのプログラムを実際に作成してみましたが,いかがだったでしょうか.PythonとPyTorchを利用することで,簡単にプログラムを作成できることがわかったかと思います.PyTorchの細かい関数の説明は省きましたが,Web上で簡単に情報が見つかりますので,わからない関数に出会ったらGoogleで検索してみて下さい.

今後も今回のようなプログラムを通した情報発信をしていきたいと思っています.

タイトルとURLをコピーしました