以前、PyTorchで全結合のニューラルネットワークで、手書き数字認識を行い、自分の文字の正解率6~8割くらいでした。その後、線化処理などを加えた全結合のニューラルネットワークで試すと、9割くらいになりました。
今回は、畳み込みニューラルネットワーク(CNN)を試しました。モデルは、1998年に考案された元祖CNN、LeNetを模したモデルでやってみました。
結果、NMISTのテストデータでは99%以上の正解率、自分の文字の正解率は96%でした。すげぇー。
ただ、文字を中心に配置してサイズ調整する処理を加えない場合は、正解率は82%に落ちてしまったので、画像の前処理が肝となりそうです。
モデル
モデルは下記のサイトのものをコピペしました。
PyTorchでシンプルな畳み込みニューラルネットワークを作ろう
https://qiita.com/sudamasahiko/items/fd6a52f958f3f9013f0f
class MyNet(torch.nn.Module):
def __init__(self):
super(MyNet, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 20, 5, 1)
self.conv2 = torch.nn.Conv2d(20, 50, 5, 1)
self.fc1 = torch.nn.Linear(4*4*50, 500)
self.fc2 = torch.nn.Linear(500, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.max_pool2d(x, 2, 2)
x = F.relu(self.conv2(x))
x = F.max_pool2d(x, 2, 2)
x = x.view(-1, 4*4*50)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return F.log_softmax(x, dim=1)
(全ソースコードは最後に)
上記の継承クラスを書き換えただけで、ほぼ前回のソースコードで回すことができました。PyTorchすごい。
結果
MNISTのtestデータの結果
0:99.80 % 1:99.47 % 2:99.52 % 3:99.31 % 4:98.88 % 5:99.22 % 6:99.06 % 7:98.54 % 8:99.59 % 9:99.41 % total: 99.28 %
素晴らしい正解率。4が9に、7が1になることがあるのは相変わらずでしたが、それでも98%以上。
さて、自分の文字は?
0(100%): 0 0 0 0 0 0 0 0 0 0 1( 90%): 1 1 1 2 1 1 1 1 1 1 2(100%): 2 2 2 2 2 2 2 2 2 2 3(100%): 3 3 3 3 3 3 3 3 3 3 4(100%): 4 4 4 4 4 4 4 4 4 4 5(100%): 5 5 5 5 5 5 5 5 5 5 6( 90%): 6 6 6 6 6 8 6 6 6 6 7(100%): 7 7 7 7 7 7 7 7 7 7 8(100%): 8 8 8 8 8 8 8 8 8 8 9( 80%): 9 9 9 9 9 9 9 9 7 7 total: 96.00 %
おおー、素晴らしい。
これは2値化処理および文字を中央に配置し、さらに縮小する際、文字サイズを幅の80%程度に調整した画像での判別です。調整サイズを71%にしても、正解率は変わず(前は正解率が大幅に変化)で、確かに位置ずれに強くなっているようです。
ただし、中央寄せもサイズ調整もしていないテキトーに切り出した元の画像だと正解率は、8割くらいになりました。
0( 60%): 6 0 8 0 0 4 6 0 0 0 1( 60%): 7 1 1 1 1 7 1 1 0 8 2( 80%): 8 2 2 2 2 6 2 2 2 2 3( 90%): 3 3 3 3 3 3 3 3 3 0 4(100%): 4 4 4 4 4 4 4 4 4 4 5(100%): 5 5 5 5 5 5 5 5 5 5 6( 60%): 6 6 6 8 6 8 5 6 2 6 7(100%): 7 7 7 7 7 7 7 7 7 7 8( 90%): 8 7 8 8 8 8 8 8 8 8 9( 80%): 9 9 9 9 8 9 9 9 9 8 total: 82.00 %
調整された画像で学習しているので、当然といえば当然かもしれません。
そうなると綺麗に文字の領域を判別できるかカギになります。今のサンプル画像は綺麗に2値化できているので領域検出がたやすいですが、ノイズが入るとたちまちダメになりそう。物体検出のCNNなども試してみたくなります。OCRによる実務の効率化という当初目的から、意識が離れていっている気がしますが。。。
PyTorch、流行りの技術を手軽に試すことができて楽しいので、試すことが自体が目的になっちゃいますね^^;
さて、このCNN、Cythonで実装したいとは思うのですが、アルゴリズムはなんとなく理解しましたが、完全自力は無理そうなのでサンプルコードが必要です。良い書籍等が見つかれば挑戦してみることにします。
追記:自力で頑張りました。Pythonコード Cythonコード
学習部:下記のサイトのソースをベースにモデルを変更したものです。
https://rightcode.co.jp/blog/information-technology/pytorch-mnist-learning
import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
class MyNet(torch.nn.Module):
def __init__(self):
super(MyNet, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 20, 5, 1)
self.conv2 = torch.nn.Conv2d(20, 50, 5, 1)
self.fc1 = torch.nn.Linear(4*4*50, 500)
self.fc2 = torch.nn.Linear(500, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.max_pool2d(x, 2, 2)
x = F.relu(self.conv2(x))
x = F.max_pool2d(x, 2, 2)
x = x.view(-1, 4*4*50)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return F.log_softmax(x, dim=1)
def load_MNIST(batch=100, intensity=1.0):
train_loader = torch.utils.data.DataLoader(
datasets.MNIST(r'C:\Temp',
train=True,
download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Lambda(lambda x: x * intensity)
])),
batch_size=batch,
shuffle=True)
test_loader = torch.utils.data.DataLoader(
datasets.MNIST(r'C:\Temp',
train=False,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Lambda(lambda x: x * intensity)
])),
batch_size=batch,
shuffle=True)
return {'train': train_loader, 'test': test_loader}
def main():
# 学習回数
epoch = 20
# 学習結果の保存用
history = {
'train_loss': [],
'test_loss': [],
'test_acc': [],
}
# ネットワークを構築
net: torch.nn.Module = MyNet()
# MNISTのデータローダーを取得
loaders = load_MNIST()
optimizer = torch.optim.Adam(params=net.parameters(), lr=0.001)
for e in range(epoch):
""" Training Part"""
loss = None
# 学習開始 (再開)
net.train(True) # 引数は省略可能
for i, (data, target) in enumerate(loaders['train']):
optimizer.zero_grad()
output = net(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if i % 10 == 0:
print('Training log: {} epoch ({} / 60000 train. data). Loss: {}'.format(e+1,
(i+1)*100,
loss.item())
)
history['train_loss'].append(loss)
""" Test Part """
# 学習のストップ
net.eval() # または net.train(False) でも良い
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in loaders['test']:
output = net(data)
test_loss += F.nll_loss(output, target, reduction='sum').item()
pred = output.argmax(dim=1, keepdim=True)
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= 10000
print('Test loss (avg): {}, Accuracy: {}'.format(test_loss,
correct / 10000))
history['test_loss'].append(test_loss)
history['test_acc'].append(correct / 10000)
#モデルの保存
torch.save(net.state_dict(), 'my_mnist_model.pth')
# 結果の出力と描画
print(history)
plt.figure()
plt.plot(range(1, epoch+1), history['train_loss'], label='train_loss')
plt.plot(range(1, epoch+1), history['test_loss'], label='test_loss')
plt.xlabel('epoch')
plt.legend()
plt.savefig('loss.png')
plt.figure()
plt.plot(range(1, epoch+1), history['test_acc'])
plt.title('test accuracy')
plt.xlabel('epoch')
plt.savefig('test_acc.png')
if __name__ == '__main__':
main()
学習済みモデルによる判別
# -*- coding: utf-8 -*-
import os
import torch
import torch.nn.functional as F
import torchvision
from torchvision import datasets, transforms
from PIL import Image, ImageOps
class MyNet(torch.nn.Module):
def __init__(self):
super(MyNet, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 20, 5, 1)
self.conv2 = torch.nn.Conv2d(20, 50, 5, 1)
self.fc1 = torch.nn.Linear(4*4*50, 500)
self.fc2 = torch.nn.Linear(500, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.max_pool2d(x, 2, 2)
x = F.relu(self.conv2(x))
x = F.max_pool2d(x, 2, 2)
x = x.view(-1, 4*4*50)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return F.log_softmax(x, dim=1)
def predic(data):
net: torch.nn.Module = MyNet()
net.load_state_dict(torch.load('my_mnist_model.pth'))
net = net.eval()
output = net(data)
_, prediction = torch.max(output, 1)
print('result=' + str(prediction[0].item()))
def image_loader(path,invert=True):
image = Image.open(path)
image = image.convert('L').resize((28,28))
if invert:
image = ImageOps.invert(image)
transform=transforms.Compose([
transforms.ToTensor(),
#transforms.Normalize((0.5,), (0.5,))
])
data = transform(image)
data = data.unsqueeze(0)
return data
def test():
net = MyNet()
net.load_state_dict(torch.load('my_mnist_model.pth'))
net = net.eval()
total_n = total_c = 0.0
for i in range(10):
path = './img/'
files = os.listdir(path)
flist = [f for f in files if os.path.isfile(os.path.join(path, f))]
n = c = 0
result = ''
for j in range(10):
f = str(i)+'-'+str(j)+'.png'
filepath = os.path.join(path,f)
data = image_loader(filepath)
output = net(data)
_, prediction = torch.max(output, 1)
# 結果を出力
re = str(prediction[0].item())
if str(i) == re:
c += 1
n += 1
result += re+' '
per = float(c)/float(n)*100
total_c += c
total_n += n
print('%d(%d%%): %s' % (i,per,result))
per = total_c/total_n*100
print('total: %0.2f %%' % per)
def main():
img = image_loader('img-path.png')
predic(img)
if __name__ == '__main__':
main()

3回に渡っての取り組み、とても素晴らしいです。
参考にさせていただきます。
ありがとうございます!