2021/09/12
MNISTや
Fashion MNISTは、私のようなAI初学者でも簡単にCNN(Convolutional Neural Network)を試せる題材として、ネット上にサンプルプログラムが多くあり、実際の処理の中身を知らなくても、実行がすぐにできます。ただ、中身が分かっていないので、別の画像にした場合、何も応用が利かずにエラーを吐いて動かないことが多いですね。私自身も例外でないので、今回、勉強も兼ねて、PytorchのCNNと「ひらがな」の画像データセットを用いて分類をしてみました。
ひらがな文字画像は
githubからダウンロードして利用しました。

7zipファイルをダウンロードし、解凍すると73個のフォルダに展開し

それぞれのフォルダに画像ファイルが入っていました。画像サイズは48 [pixel]×48 [pixel]です。

各フォルダ(73文字分)から500画像をランダムに抽出し、400画像を学習データ、100画像をテストデータとしました。
PytorchのCNNのプログラムはMicrosoftの
mlーbasicsにあるものを参考にしました。作成したプログラムは以下の通りです。
# Hiragana classification
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.autograd import Variable
import numpy as np
import os
import matplotlib as mpl
import matplotlib.pyplot as plt
import cv2
mpl.rcParams['font.family']='AppleGothic'
%matplotlib inline
#------------
# Read Data
#------------
# The images folder
data_path = 'data/char/'
# Get the class names
classes = os.listdir(data_path)
classes.sort()
print(len(classes), 'classes:')
print(classes)
# Show the image
fig = plt.figure(figsize=(100, 150))
i = 0
for sub_dir in os.listdir(data_path):
i+=1
img_file = os.listdir(os.path.join(data_path,sub_dir))[0]
img_path = os.path.join(data_path, sub_dir, img_file)
img = cv2.imread(img_path)
a=fig.add_subplot(1, len(classes),i)
a.axis('off')
imgplot = plt.imshow(img)
a.set_title(img_file)
plt.show()
# Function to ingest data using training and test loaders
def load_dataset(data_path):
# Load all of the images
transformation = transforms.Compose([
# transform to tensors
transforms.ToTensor(),
# Normalize the pixel values (in R, G, and B channels)
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])
# Load all of the images, transforming them
full_dataset = torchvision.datasets.ImageFolder(
root=data_path,
transform=transformation
)
# Split into training (70% and testing (30%) datasets)
train_size = int(0.7 * len(full_dataset))
test_size = len(full_dataset) - train_size
train_dataset, test_dataset = torch.utils.data.random_split(full_dataset, [train_size, test_size])
# Training data : 50-image batches
train_loader = torch.utils.data.DataLoader(
train_dataset,
batch_size=50,
num_workers=0,
shuffle=False
)
# Test data : 50-image batches
test_loader = torch.utils.data.DataLoader(
test_dataset,
batch_size=50,
num_workers=0,
shuffle=False
)
return train_loader, test_loader
# Dataloaders for test and training data
train_loader, test_loader = load_dataset(data_path)
#-----------------
# Model definition
#-----------------
# Create a neural net class
class Net(nn.Module):
def __init__(self, num_classes=73):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=12, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2),
nn.Conv2d(in_channels=12, out_channels=12, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2),
nn.Conv2d(in_channels=12, out_channels=24, kernel_size=3, padding=1),
nn.Dropout2d(p=0.2),
)
self.classifier = nn.Linear(in_features= 12*12*24, out_features=num_classes)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return F.log_softmax(x, dim=1)
def train(model, device, train_loader, optimizer, epoch):
# Set the model to training mode
model.train()
train_loss = 0
print("Epoch:", epoch)
# Process the images in batches
for batch_idx, (data, target) in enumerate(train_loader):
# Use the CPU or GPU as appropriate
data, target = data.to(device), target.to(device)
# Reset the optimizer
optimizer.zero_grad()
# Push the data forward through the model layers
output = model(data)
# Get the loss
loss = loss_criteria(output, target)
# Keep a running total
train_loss += loss.item()
# Backpropagate
loss.backward()
optimizer.step()
# Print metrics for every 10 batches
if batch_idx % 10 == 0:
print('Training set [{}/{} ({:.0f}%)] Loss: {:.6f}'.format(
batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
# return average loss for the epoch
avg_loss = train_loss / (batch_idx+1)
print('Training set: Average loss: {:.6f}'.format(avg_loss))
return avg_loss
def test(model, device, test_loader):
# Switch the model to evaluation mode
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
batch_count = 0
for data, target in test_loader:
batch_count += 1
data, target = data.to(device), target.to(device)
# Get the predicted classes for this batch
output = model(data)
# Calculate the loss for this batch
test_loss += loss_criteria(output, target).item()
# Calculate the accuracy for this batch
_, predicted = torch.max(output.data, 1)
correct += torch.sum(target==predicted).item()
# Calculate the average loss and total accuracy for this epoch
avg_loss = test_loss/batch_count
print('Validation set: Average loss: {:.6f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
avg_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
# return average loss for the epoch
return avg_loss
#------------------------------
# Initialize Model,
# optimizer and loss criteria
#------------------------------
device = "cpu"
if (torch.cuda.is_available()):
device = "cuda"
print('Training on', device)
model = Net(num_classes=len(classes)).to(device)
# Optimizer : Adam
optimizer = optim.Adam(model.parameters(), lr=0.001)
# Loss criteria : CrossEntropy
loss_criteria = nn.CrossEntropyLoss()
#-------
# Train
#-------
epoch_nums = []
training_loss = []
validation_loss = []
epochs = 5
for epoch in range(1, epochs + 1):
train_loss = train(model, device, train_loader, optimizer, epoch)
test_loss = test(model, device, test_loader)
epoch_nums.append(epoch)
training_loss.append(train_loss)
validation_loss.append(test_loss)
# training and validation graph
plt.plot(epoch_nums, training_loss)
plt.plot(epoch_nums, validation_loss)
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['training', 'validation'], loc='upper right')
plt.show()
# Confusion matrix
from sklearn.metrics import confusion_matrix
# Set the model to evaluate mode
model.eval()
# Get predictions for the test data
truelabels = []
predictions = []
for data, target in test_loader:
for label in target.cpu().data.numpy():
truelabels.append(label)
for prediction in model.cpu()(data).data.numpy().argmax(1):
predictions.append(prediction)
# Plot the confusion matrix
plt.figure(figsize=(12, 12), dpi=80)
cm = confusion_matrix(truelabels, predictions)
plt.imshow(cm, interpolation="nearest", cmap=plt.cm.Blues)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes)
plt.yticks(tick_marks, classes)
plt.xlabel("Predicted Shape")
plt.ylabel("Actual Shape")
plt.show()
# Save the model
model_file = 'models/shape_classifier.pt'
torch.save(model.state_dict(), model_file)
del model
#------
# Test
#------
# test.csv : test image file list
with open('/Users/drbobt/Downloads/test.csv') as f:
l = f.readlines()
datano = len(l)
from random import randint
# Function to predict the class of an image
def predict_image(classifier, image):
# Set the classifer model to evaluation mode
classifier.eval()
# Apply the same transformations
transformation = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])
# Preprocess the image
image_tensor = transformation(image).float()
# Add an extra batch dimension since pytorch treats all inputs as batches
image_tensor = image_tensor.unsqueeze_(0)
# Turn the input into a Variable
input_features = Variable(image_tensor)
# Predict the class of the image
output = classifier(input_features)
index = output.data.numpy().argmax()
return index
# Create a new model class and load the saved weights
model = Net()
model.load_state_dict(torch.load(model_file))
for i in range(0, datano):
img_path = l[i]
img = cv2.imread(img_path[:-1])
# Call the predction function
index = predict_image(model, img)
# Output the result
print(str(l[i][:-1]) + ': ' + classes[index])
PytorchのCNNのプログラムの大まかな流れは以下の通りです。Pytorchのバージョンは1.8.0です。
① パッケージインポート
② データ読み込み(dataset設定、dataloader定義)
③ Model定義
④ モデル・損失関数・最適化関数の初期化
⑤ 学習実行と評価、結果保存
⑥ 検証実行
作成したプログラムと実行結果について、順を追って説明します。
① パッケージインポート
2〜7行目の6個のパッケージは定番なものです。
② データ読み込み(dataset設定、dataloader定義) : 18〜85行目
プログラムのあるフォルダに「/data/char/(73文字分のフォルダ)」を作成し、その各フォルダに学習用の文字画像を400枚入れました。この時、面倒でしたがフォルダ名は「U30xx」ではなく、ひらがなに振り直しました。28、29行目の結果は以下の通りです。

43行目の各フォルダから1文字ずつ抽出した画像はこんな感じです。

400枚の画像をtrain:test=7:3で分割し、バッチサイズは50枚で学習させました。
③ Model定義 : 87〜179行目
参考プログラムと同様に、畳み込み層を3個、プーリング層を2個利用したモデルにしました。入出力チャンネル数、カーネル数、パディング数の設定が理解不十分でハマってしまいましたが、動く形にはできました。最適か否かは未検証です。
長くなりましたので、④以降は次回にしたいと思います。