初級ディープラーニング
記事内に商品プロモーションを含む場合があります

手書き文字(MNIST)認識をCNNでやってみる【初級 深層学習講座】

tadanori

以前、手書き文字(MNIST)認識をPyTorchで記述してみました。今回は、ニューラルネットワークのモデルを畳み込みニューラルネットワーク(CNN)に変更してみます。画像のクラス分類で一般的に使われているCNNの簡単なモデルを実際につくることで、CNNの理解に役立ちます。

あわせて読みたい
手書き文字(MNIST)認識をPyTorchで記述する【初級 深層学習講座】
手書き文字(MNIST)認識をPyTorchで記述する【初級 深層学習講座】

はじめに

MNISTの手書き数字のデータセットは、「0」〜「9」の数字を手書きしたデータセットです。

ディープラーニングの学習用によく利用されているため、手軽にダウンロードしてデータセットとして利用する環境が整っています。

今回は、PyTorchで手書き文字を識別するCNNモデルを作成し、学習させてみたいと思います。

普段は、TIMM(PyTorch Image Models)などを使ってベースモデルを加工してモデルを作成しますが、今回は1からモデルを作成します。

なお、以下で紹介するコードはGoogle Colab or Jupyter notebookで動かす前提としています。

Google Colabで動作するコードはこちらに置いています

ライブラリのインストール

前半のコードは、「手書き文字(MNIST)認識をPyTorchで記述する【初級 深層学習講座】」と同じです。一応、こちらの記事でも改めて掲載しておきます。

あわせて読みたい
手書き文字(MNIST)認識をPyTorchで記述する【初級 深層学習講座】
手書き文字(MNIST)認識をPyTorchで記述する【初級 深層学習講座】

最初に、ライブラリをインポートします。

インポートするのは、torch関連、torchvision, matplotlib, numpy, tqdm, sklearnです。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import matplotlib.pyplot as plt
import numpy as np
from tqdm.notebook import tqdm
from sklearn.metrics import accuracy_score

Google Colab/Jupyterで動かさない場合は、tqdm.notebooktqdmに変えてください。コードは以下になります。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
from sklearn.metrics import accuracy_score

importでエラーが出た場合は、必要なライブラリをインストールしてください

MNIST(手書き数字データセット)

データセット概要

MNISTのデータセットは以下の構成です

  • 訓練データ(6万枚)、テストデータ(1万枚)の合計7万枚
  • 8bitグレースケール(0~255)、28×28画素

PyTorchの場合は、torchvision.datasets.MNIST()を使うことで簡単にダウンロードし、データセットとして利用するこtが可能です。

データセットのダウンロード

以下のコードでtrain_datasetに6万枚の訓練データが、valid_datasetに1万枚のテストデータがダウンロードされます。

ローカル環境の場合、初回はデータセットがダウンロードされるので、インターネットへの接続が必要です

train_dataset = torchvision.datasets.MNIST(root="data", 
                                           train=True, 
                                           transform=torchvision.transforms.ToTensor(), 
                                           download=True)
valid_dataset = torchvision.datasets.MNIST(root="data", 
                                           train=False, 
                                           transform=torchvision.transforms.ToTensor(), 
                                           download=True)

データの中身を確認してみます。以下のコードを実行すると、訓練データの25枚の内容が表示されます。

# データを確認
fig, ax = plt.subplots(5,5, figsize=(10,10))

for i in range(25) :
  img, label = train_dataset[i] 
  r, c = i//5, i%5
  ax[r, c].imshow(img.squeeze(), cmap="gray")
  ax[r, c].axis("off")
  ax[r, c].set_title(label)
MNISTデータセットの中身(一部)

データローダーの設定

PyTorchでは、データローダー(DataLoader)を使ってデータをロードするのが一般的です。データローダーは、バッチ単位でのデータのロード、データのシャッフル、マルチスレッドによるデータの読み込みなどをサポートしてくれるので、自身でコードを書かなくても、バッチ単位で画像をシャッフルしつつ読み込むことができます。

データセットを定義していれば、データローダーの設定は非常に簡単です。

batch_sizeはバッチサイズ、shuffleはシャッフルするかどうかのフラグです。訓練データは毎回順番が変化した方がよいので、shuffle=Trueと設定しています。

batch_size = 64
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
valid_loader = torch.utils.data.DataLoader(valid_dataset, batch_size=batch_size, shuffle=False)
PyTorchのデータセットとデータローダーの関係

PyTorchのデータセットは、トレーニングやテストに使用するデータを扱うためのもので、例えば画像やテキストなどのデータを1つずつ処理します。一方、データローダーは、データセットからバッチ単位でデータを取り出すためのものです。この2つを組み合わせることで、簡単に大量のデータを読み出すことができます

以上で、データセットを読み込む準備は完了です。

モデル

モデルを定義する

MNISTは簡単なモデルでも学習できるので、今回は全結合層3つからなるモデルとしてみます。

PyTorchでモデルを作成する場合は、nn.Moduleを派生したクラスを定義します。基本的には、以下の2つの関数を定義します

  • __init__()
    初期化を行う関数です。引数は、自身のモデルに合わせて変更します。今回は、input_size(=28)を引数として受け取る想定です。画像のサイズはinput_size*input_sizeとなります。また、3つのnn.Linearを定義しています
  • forward()
    モデルの流れ(処理)を記述します。xは入力です(model(x)で渡される引数)
class MyCNNModel(nn.Module):
  def __init__(self, input_size):
    super(MyCNNModel, self).__init__()
    self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1)
    self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
    self.pool = nn.MaxPool2d(2,2)
    self.act = nn.ReLU()
    self.pool2 = nn.AdaptiveAvgPool1d(1024)
    self.fc1 = nn.Linear(1024, 256)
    self.fc2 = nn.Linear(256, 10)
  def forward(self, x):
    x = self.pool(self.act(self.conv1(x)))
    x = self.pool(self.act(self.conv2(x)))
    x = torch.flatten(x, start_dim=1)
    x = self.pool2(x)
    x = self.act(self.fc1(x))
    x = self.fc2(x)
    return x

モデルを図示すると以下のようになります。

最初のconv1, conv2がCNN(畳み込みの部分です)。2回畳み込みを行い7×7, 64チャネルにした後に、Flattenで1次元に変換し、AdaptivePoolで1024個に平均プーリングしています。

AdaptivePoolは、入力のサイズに関わらず出力サイズに合わせてくれるので、入力サイズを気にすることなく構成できるのでここに挿入しました。

実際は28×28と決まっているので、AdaptivePoolをする必要はありませんが、とりあえず挿入しています。AdaptivePoolについては以下の記事を参考にしてください。

Adaptive Pooling層の動きをわかりやすく解説【初級 深層学習講座】
Adaptive Pooling層の動きをわかりやすく解説【初級 深層学習講座】

最後の出力の10がクラスに対応し、クラスに対応してどれか1つの値だけ大きな値となります。例えば、文字が4の場合は、4番目の値が最大になるように学習させます。

モデル

CNNを作る場合は、畳み込み層(conv)、活性化(ReLUなど)、プーリング(MaxPoolingなど)の3つを並べることが多いです。段が深くなった場合は、勾配消失を防ぐために、さらにバッチノーマライゼーションを並べます。

また、CNNの部分には、より複雑な構造を持つinceptionresidual blockなどいろいろな構成が論文発表されています。

モデルを生成する

モデルを定義したので、モデルを生成します。モデルの生成は以下のコードになります。

device = ... の行は、GPUがあるかどうかをチェックして、GPUがある場合はcudaを、無い場合は、cpuを設定します。

to(device)とすることで、GPUがある場合はGPUにモデルがロードされます。

Macの場合は、GPUはmpsとなります

device = "cuda" if torch.cuda.is_available() else "cpu"
model = MyCNNModel(28).to(device)
model

以下がモデルの内容を表示したものになります

MyCNNModel(
  (conv1): Conv2d(1, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (act): ReLU()
  (pool2): AdaptiveAvgPool1d(output_size=1024)
  (dropout): Dropout(p=0.5, inplace=False)
  (fc1): Linear(in_features=1024, out_features=256, bias=True)
  (fc2): Linear(in_features=256, out_features=10, bias=True)
)

学習

損失関数と最適化手法を設定

損失関数と最適化手法を設定します。

クラス分類なので、損失関数はCrossEntropyLossを利用します。最適化手法は、SGDやAdamなどありますが、ここではAdamを選びました。

最適化関数の引数は、最適化するパラメータのリストと最適化のためのハイパーパラメータとなります。今回は、lrだけ設定し、他はデフォルトを利用しています。

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

最適化のパラメータを調整することで、精度や学習速度が変化します。ここは、何度か実験して決めていくのが一般的です

訓練用と評価用関数の定義

1エポック分の訓練用と評価用の関数を用意します。これを用意しておくと、学習コードが簡単になりますので、作成するように習慣づけしておくと良いと思います。

訓練用関数

学習では、model.train()でモデルを学習できる状態に切り替えます。

for文で、データローダーからデータを読み込み終わるまで繰り返します。処理の内容は以下の通りです

  • 画像とラベルを読み出す
  • model(images)で、推論結果を出力
  • lossを計算
  • loss.backward()を実行して誤差を逆伝播
  • optimizer.step()を実行
  • 各種結果集計

どのような学習でも基本の流れはほぼ同じです。

なお、modelを呼び出す前に、optimizer.zero_grad()を呼び出すのを忘れないようにしましょう。

def do_train(model, device, loader, criterion, optimizer):
  model.train()
  tot_loss = 0.0
  tot_score = 0.0
  for images, labels in tqdm(loader, desc="train"):
    images, labels = images.to(device), labels.to(device)

    optimizer.zero_grad()
    outputs = model(images)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()
    tot_loss += loss.detach().item()
    tot_score += accuracy_score(labels.cpu(), outputs.argmax(dim=1).cpu())

  tot_loss /= len(loader)
  tot_score /= len(loader)
  return tot_loss, tot_score

評価用関数

評価では、学習を行わないのでmodel.eval()で評価モードに切り替えます。

また、with torch.no_grad()で勾配計算を無効化しておきます。

no_grad()としておくことで推論時に不必要なメモリ使用量を減らすことができます

学習を行わないので、optimizerloss.backwordは必要ありません。

def do_valid(model, device, loader, criterion):
  model.eval()
  tot_loss = 0.0
  tot_score = 0.0
  with torch.no_grad():
    for images, labels in tqdm(loader, desc="valid"):
      images, labels = images.to(device), labels.to(device)
      outputs = model(images)
      loss = criterion(outputs, labels)
      tot_loss += loss.detach().item()
      tot_score += accuracy_score(labels.cpu(), outputs.argmax(dim=1).cpu())
  tot_loss /= len(loader)
  tot_score /= len(loader)
  return tot_loss, tot_score

学習ループ

学習ループです。エポック数は10回としました。

処理としては、訓練→評価を繰り返して実行しているだけです。

trainvalidで異なるデータを与えています。validのデータは学習に含まれていないので、validに対する結果は未知のデータに対する結果と考えることができます

validの結果が良いモデルを保存などすると、学習には使っていないですが、間接的にvalidがリークするので注意が必要です。

num_epochs = 10
for epoch in range(num_epochs):
  print(f'[EPOCH {epoch+1}]')
  train_loss, train_acc = do_train(model, device, train_loader, criterion, optimizer)
  valid_loss, valid_acc = do_valid(model, device, valid_loader, criterion)
  print(f"--> train loss {train_loss}, train accuracy {train_acc}, valid loss {valid_loss} valid accuracy {valid_acc}")

最終的に、以下のような結果となりました。

--> train loss 0.013030382923994666, train accuracy 0.9957689232409381, valid loss 0.027705744465230404 valid accuracy 0.9917396496815286

評価データで正解率99.17%とかなり高い精度で数字が認識できていることが分かります。

下記の記事で作成したニューラルネットワークでは、正解率は97.76%でしたので畳み込みネットワークに変更することで性能アップしたことがわかります。

なお、resnet18というより複雑なCNNモデルを利用した場合(下記記事参照)は、99.19%と精度がさらに高くなっています。

とりあえず、CNNを利用することで性能向上することが確認できました。

あわせて読みたい
手書き文字(MNIST)認識をPyTorchで記述する【初級 深層学習講座】
手書き文字(MNIST)認識をPyTorchで記述する【初級 深層学習講座】

まとめ

MNISTの手書き文字の識別を畳み込みネットワークで作成してみました。timmなどを利用し出すとモデルを自作することは減ってしまいますが、自力でモデルを作成できることは必要です。

簡単なモデルは自作できるようにしておいた方がよいです。

おすすめ書籍

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

記事URLをコピーしました