手書き数字の画像認識 深層学習モデルの実装例

[2025/04/16] チュートリアルを更新しました.
本チュートリアルでは【練習問題】手書き数字の画像認識コンペの簡単な深層学習モデルの実装例を示します.
本コンペは0~9の手書き数字が写った画像に対して0~9の数字を割り当てる問題です. 学習用として与えられる画像データは60,000枚で
評価用として与えられる画像データは10,000枚です. 評価指標は正解率(Accuracy)です.
7層の比較的浅いモデル(LeNet[1])に対して実験した結果、約90%の精度を出すことが可能であることがわかりました。
分析環境はGoogle Colablatoryを想定します.
実行環境・ライブラリバージョンに応じては、コードが動作しない可能性もありますのでご了承ください.
from google.colab import drive
drive.mount('/content/drive')
データの読み込み¶
解凍したファイルをDriveにアップロードすると時間がかかるため、zipファイルをDrive上で解凍し、直接Colabで利用する
!cp "/content/drive/MyDrive/Colab Notebooks/手書き文字認識/data/train.zip" "."
!cp "/content/drive/MyDrive/Colab Notebooks/手書き文字認識/data/test.zip" "."
!unzip -qu "train.zip"
!unzip -qu "test.zip"
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import tqdm
import random
import cv2
import sys
import gc
import os
import time
import warnings
from sklearn.metrics import accuracy_score, confusion_matrix
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.optim import lr_scheduler
from torch.autograd import Variable, detect_anomaly
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms
from torchvision.utils import make_grid
from PIL import Image, ImageOps, ImageEnhance
from sklearn.model_selection import train_test_split
train_size = len(os.listdir("train/"))
test_size = len(os.listdir("test/"))
print(f"学習データの枚数: {train_size}")
print(f"テストデータの枚数: {test_size}")
学習データの枚数: 60000 テストデータの枚数: 10000
seeds = 777
torch.manual_seed(seeds)
random.seed(seeds)
np.random.seed(seeds)
torch.cuda.manual_seed(seeds)
if torch.cuda.is_available():
torch.backends.cudnn.deterministic = True
適当な画像を確認¶
image = Image.open(os.path.join('train', 'train_0.jpg'))
print('width, height:', image.size)
image
width, height: (28, 28)
image = Image.open(os.path.join('train', 'train_12.jpg'))
print('width, height:', image.size)
image
width, height: (28, 28)
image = Image.open(os.path.join('test', 'test_0.jpg'))
print('width, height:', image.size)
image
width, height: (28, 28)
それぞれ画像の大きさは横×縦が28×28で, グレースケール画像であることが確認できました.
学習用画像データにおけるそれぞれの数字の分布をみてみます.
train_master = pd.read_csv("/content/drive/MyDrive/Colab Notebooks/手書き文字認識/data/train_master.tsv", sep='\t')
train_master.head()
file_name | category_id | |
---|---|---|
0 | train_0.jpg | 5 |
1 | train_1.jpg | 0 |
2 | train_2.jpg | 4 |
3 | train_3.jpg | 1 |
4 | train_4.jpg | 9 |
train_master['category_id'].value_counts().sort_index()
count | |
---|---|
category_id | |
0 | 5923 |
1 | 6742 |
2 | 5958 |
3 | 6131 |
4 | 5842 |
5 | 5421 |
6 | 5918 |
7 | 6265 |
8 | 5851 |
9 | 5949 |
手法¶
次に実際にモデリングを行い, 評価用画像データに対して予測をできるようにします. 今回は7層の比較的浅い畳み込みニューラルネットワークモデル(LeNet[1])によりモデリングを試みます.
実装はpytorchを用います.
まず60,000枚の学習用画像データを読み込みます. その際, あらかじめデータの数値の範囲が[0, 1]となるように正規化しておきます.
pytorchではモデルに学習データを渡す際は(サンプル数, チャンネル数, 縦, 横)の形でなければならないので, そのように変形しておきます.
DataLoader¶
class MNIST_data(Dataset):
"""MNIST dtaa set"""
def __init__(self, data, mode, transform=None):
self.df = data
self.tsfm = transform
self.mode = mode
def __len__(self):
return len(self.df)
def __getitem__(self, idx):
image_name, label = self.df.loc[idx, "file_name"], self.df.loc[idx, "category_id"]
if self.mode == "train":
image = cv2.imread(f"{self.mode}/{image_name}", cv2.IMREAD_GRAYSCALE)
label = torch.tensor(label)
if self.tsfm != None:
image = self.tsfm(image.astype(dtype=np.uint8))
else:
image = torch.FloatTensor(image)
else:
image = cv2.imread(f"{self.mode}/{image_name}", cv2.IMREAD_GRAYSCALE)
label = torch.tensor(label)
if self.tsfm != None:
image = self.tsfm(image.astype(dtype=np.uint8))
else:
image = torch.FloatTensor(image)
return image, label
transform_train = transforms.Compose([transforms.ToTensor(),
transforms.Normalize([0.5], [0.5])])
transform_test = transforms.Compose([transforms.ToTensor(),
transforms.Normalize([0.5], [0.5])])
sample = MNIST_data(train_master, mode="train", transform=transform_train)
sample_loader = torch.utils.data.DataLoader(dataset=sample, batch_size=16, shuffle=True, worker_init_fn=seeds)
確認
images, labels = next(iter(sample_loader))
plt.figure(figsize=(8,8))
for i, (image, label) in enumerate(zip(images, labels)):
plt.subplot(4,4,i+1)
plt.imshow(image.permute(1,2,0))
plt.title(label)
plt.xticks(())
plt.yticks(())
plt.show()
Model作成&損失関数設定¶
class LeNet(nn.Module):
def __init__(self, output_size=10, activation='relu'):
super(LeNet, self).__init__()
self.activation = activation
self.conv1 = nn.Conv2d(1, 6, kernel_size=5)
self.pool = nn.AvgPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
self.fc1 = nn.Linear(16 * 4 * 4, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, output_size)
# 活性化関数の辞書
self.activations = {
'relu': F.relu,
'sigmoid': torch.sigmoid,
'tanh': torch.tanh
}
def forward(self, x):
act_func = self.activations.get(self.activation, F.relu)
x = self.pool(act_func(self.conv1(x)))
x = self.pool(act_func(self.conv2(x)))
x = x.view(-1, 16 * 4 * 4)
x = act_func(self.fc1(x))
x = act_func(self.fc2(x))
x = self.fc3(x)
return x
model = LeNet(output_size=10, activation='tanh')
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
訓練¶
#GPUを使うか使わないか(使う場合cudaになり、使わない場合cpuとなる)
device = "cuda" if torch.cuda.is_available() else "cpu"
train, valid, _, _ = train_test_split(train_master, train_master["category_id"], test_size=0.25, random_state=seeds)
train = train.reset_index(drop=True)
valid = valid.reset_index(drop=True)
batch_size = 64
train_data = MNIST_data(train, mode="train", transform=transform_train)
valid_data = MNIST_data(valid, mode="train", transform=transform_test)
train_loader = torch.utils.data.DataLoader(dataset=train_data, batch_size=batch_size, shuffle=True, worker_init_fn=seeds)
valid_loader = torch.utils.data.DataLoader(dataset=valid_data, batch_size=batch_size)
EPOCH = 4
total_batch = int(len(train_loader)/12)
# 学習→検証までの関数
def training_validation(input_model, EPOCH, training_data, validation_data, num_update):
total_batch = int(len(training_data)/num_update)
for epoch in range(1, EPOCH+1):
input_model.train()
#訓練プロセス
train_loss = []
pbar = tqdm.tqdm(enumerate(training_data), total = len(training_data))
for process, (images, labels) in pbar:
images = images.to(device)
labels = labels.to(device)
time_1 = time.time()
outputs = input_model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
loss = loss.item()
train_loss.append(loss)
if (process % total_batch == 0) | (process == len(training_data)):
pbar.set_postfix(EPOCH=epoch, train_loss=np.mean(train_loss))
#検証プロセス
input_model.eval()
valid_loss = []
correct = 0
valid_time = time.time()
for process, (images, labels) in enumerate(validation_data):
images = images.to(device)
labels = labels.to(device)
outputs = input_model(images)
loss = criterion(outputs, labels)
loss = loss.item()
valid_loss.append(loss)
pred = outputs.data.max(1, keepdim=True)[1]
correct += pred.eq(labels.data.view_as(pred)).cpu().sum().item()
minute, second = int((time.time()-valid_time)//59), int((time.time()-valid_time)%60)
pbar.set_postfix(EPOCH=epoch, train_loss=np.mean(train_loss), valid_loss=np.mean(valid_loss), valid_accuracy=correct/len(validation_data.dataset))
print(f"valid_loss: {np.mean(valid_loss)}, valid_accuracy: {correct/len(validation_data.dataset)}")
return_model = input_model
return return_model
# 予測結果のそれぞれの誤差の大きさを確認
def predict_img(input_model, input_data):
input_model.eval()
test_pred = torch.LongTensor()
test_proba = torch.FloatTensor()
for images, labels in tqdm.tqdm(input_data):
images = images.to(device)
labels = labels.to(device)
outputs = input_model(images)
pred_proba = F.softmax(outputs, dim=1).cpu()
pred = outputs.data.max(1, keepdim=True)[1].view(-1)
pred = torch.stack([labels,pred],axis=1).cpu()
test_pred = torch.cat((test_pred, pred), dim=0)
test_proba = torch.cat((test_proba, pred_proba), dim=0)
test_pred = test_pred.cpu().detach().numpy()
test_proba = test_proba.cpu().detach().numpy()
loss_correct = np.array(pd.get_dummies(test_pred[:,0]))
test_loss = -(loss_correct*np.log(test_proba+1e-07)).sum(axis=1)
test_pred = pd.DataFrame(test_pred).rename(columns = {0:"label" ,1:"predict"})
test_proba = pd.DataFrame(test_proba).rename(columns = {0:"proba_0", 1:"proba_1", 2:"proba_2", 3:"proba_3", 4:"proba_4",
5:"proba_5", 6:"proba_6", 7:"proba_7", 8:"proba_8", 9:"proba_9"})
test_loss = pd.DataFrame(test_loss).rename(columns = {0:"loss"})
df = pd.concat([test_pred, test_proba, test_loss], axis=1)
del loss_correct
gc.collect()
return df
LeNetモデルを学習させてみます.
model = training_validation(model, EPOCH, train_loader, valid_loader, 12)
100%|██████████| 704/704 [00:33<00:00, 20.98it/s, EPOCH=1, train_loss=1.74]
valid_loss: 0.9449503142782982, valid_accuracy: 0.7465333333333334
100%|██████████| 704/704 [00:26<00:00, 26.41it/s, EPOCH=2, train_loss=0.676]
valid_loss: 0.5133321314416033, valid_accuracy: 0.863
100%|██████████| 704/704 [00:25<00:00, 27.64it/s, EPOCH=3, train_loss=0.447]
valid_loss: 0.3882453439717597, valid_accuracy: 0.8956
100%|██████████| 704/704 [00:30<00:00, 23.13it/s, EPOCH=4, train_loss=0.357]
valid_loss: 0.3258060536168991, valid_accuracy: 0.9086666666666666
学習済みモデルの予測結果を確認
predict_df = predict_img(model, valid_loader)
100%|██████████| 235/235 [00:07<00:00, 30.90it/s]
混合行列でLeNetのそれぞれの数字の精度を確認
plt.figure(figsize=(8,8))
mat = confusion_matrix(predict_df["label"], predict_df["predict"]) # Confusion matrix
# Plot Confusion matrix
sns.heatmap(mat.T, square=True, annot=True, cbar=False, cmap=plt.cm.Blues, fmt="d")
plt.xlabel('Predicted Values')
plt.ylabel('True Values');
plt.show();
間違えた予測の一覧を確認します
# 大きく間違えた予測
error_index = predict_df[predict_df["label"] != predict_df["predict"]].sort_values(by="loss",ascending=False).head(6).index
plt.figure(figsize=(10,4))
for i, ind in enumerate(error_index):
label = int(predict_df.iloc[ind]["label"])
pred = int(predict_df.iloc[ind]["predict"])
proba = predict_df.iloc[ind]["proba_"+str(pred)]
image_name = valid.reset_index(drop=True).loc[ind, "file_name"]
image = cv2.imread(f"train/{image_name}", cv2.IMREAD_GRAYSCALE)
plt.subplot(2,3,i+1)
plt.imshow(image)
plt.title("true:{}, pred:{}, proba:{}".format(label, pred, proba.round(3)))
plt.xticks(())
plt.yticks(())
plt.show()
# 惜しい予測
error_index = predict_df[predict_df["label"] != predict_df["predict"]].sort_values(by="loss",ascending=False).tail(6).index
plt.figure(figsize=(10,4))
for i, ind in enumerate(error_index):
label = int(predict_df.iloc[ind]["label"])
pred = int(predict_df.iloc[ind]["predict"])
proba = predict_df.iloc[ind]["proba_"+str(pred)]
image_name = valid.reset_index(drop=True).loc[ind, "file_name"]
image = cv2.imread(f"train/{image_name}", cv2.IMREAD_GRAYSCALE)
plt.subplot(2,3,i+1)
plt.imshow(image)
plt.title("true:{}, pred:{}, proba:{}".format(label, pred, proba.round(3)))
plt.xticks(())
plt.yticks(())
plt.show()
評価用画像データに対して予測値を出力する関数を実装します.
submit = pd.read_csv("/content/drive/MyDrive/Colab Notebooks/手書き文字認識/data/sample_submit.tsv", sep='\t', header=None)
submit.head()
0 | 1 | |
---|---|---|
0 | test_0.jpg | 2 |
1 | test_1.jpg | 4 |
2 | test_2.jpg | 5 |
3 | test_3.jpg | 4 |
4 | test_4.jpg | 3 |
# DataSetの形式に合わせるため、ヘッダを少し変更
submit.columns = ["file_name", "category_id"]
test_data = MNIST_data(submit, mode="test", transform=transform_test)
test_loader = torch.utils.data.DataLoader(dataset=test_data, batch_size=batch_size)
def model_predict(input_model, input_data):
input_model.eval()
test_pred = torch.LongTensor()
for data, _ in tqdm.tqdm(input_data):
data = data.to(device)
output = model(data)
pred = output.cpu().data.max(1, keepdim=True)[1]
test_pred = torch.cat((test_pred, pred), dim=0)
return test_pred
predict = model_predict(model, test_loader)
100%|██████████| 157/157 [00:04<00:00, 37.22it/s]
predict = predict.cpu().detach().numpy().reshape(-1).copy()
submit["category_id"] = predict
submit.head()
file_name | category_id | |
---|---|---|
0 | test_0.jpg | 7 |
1 | test_1.jpg | 2 |
2 | test_2.jpg | 1 |
3 | test_3.jpg | 0 |
4 | test_4.jpg | 4 |
submit.to_csv('tutorial_submission.tsv', sep="\t", index=False, header=None)
まとめ¶
今回は比較的浅いネットワークモデルであるLeNetを適用して, 約90%の正解率を出すことがわかりました.
他にも活性化関数の変更などで精度がどのように変わるか確認してみてください.activation='tanh'
をrelu
に変更すると、reluで分析することができます。
LeNetは初期の深層学習モデルのため、その後様々な新しい深層学習モデルが提案されているので, 色々試してみると精度に違いが見えると思います.
その他色々工夫をしてさらなる改善を目指してみてください.
参考文献¶
[1] Y. LeCun, L. Bottou, Y. Bengio and P. Haffner: Gradient-Based Learning Applied to Document Recognition, Proceedings of the IEEE, 86(11):2278-2324, November 1998
