Chainerでフォントを識別してみる (1)

Chainerで何か分類してみるにあたって、

1. 分類できて嬉しい
2. サンプル収集の目処が立ちそう
3. 莫大なデータ量にならない

な分類対象として、文字のフォントの分類をしてみようと考えた。

1. 分類対象クラス

分類クラス数は今後増やしていくとして、まずはセリフ体とサンセリフ体一種類ずつ、BodoniとHelveticaを分類してみることにした。

Bodoni

f:id:mizti:20161126200655p:plain

Helvetica

f:id:mizti:20161126200706p:plain

2. データセットの作成
  • 大文字もしくは小文字を一文字ずつ64*64の画像に印字したデータを作成する。これより画像が小さいと、ジャギが立ってしまうので、フォント判別に良く無いと判断した
  • 用意するデータ毎にx位置y位置をずらしておく。(フォントによって、ベースライン位置が異なっていたりするため、これを手掛かりにさせず、あくまで字形で分類させたいため)
  • Train用のデータセットでは[a-mA-M]、Test用のデータセットでは[n-zN-Z]の文字を用いる

train用
f:id:mizti:20161126180043p:plain
test用
f:id:mizti:20161126180016p:plain
(※データをちゃんと作れたか確認のため画像出力したが、実際にはメモリ上でリスト保持するのみ)

  • ChainerのOptimizer、Trainerを用いる事ができるように、DatasetMixinを継承したクラスとしてデータセットクラスを作成した。

(DataMixinを継承したクラスで__len__とget_exampleだけ定義してあげればChainerのIterationに渡せるようになる)

  • Datasetのコンストラクト引数で、サンプル数とtrain / testの指定を行えるようにした
import sys
import random
import numpy as np
from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw
import chainer
from chainer import cuda, Function, gradient_check, report, training, utils, Variable
from chainer import datasets, iterators, optimizers, serializers
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L
from chainer.training import extensions

class FontImageDataset(chainer.dataset.DatasetMixin):
	def __init__(self, datanum=10, normalize=True, flatten=True, train=True):
		self._normalize = normalize
		self._flatten = flatten
		self._train = train
		pairs = []
		for _ in range(datanum):
			image_array, label = self.generate_image()
			pairs.append([image_array, label])
		self._pairs = pairs

	def __len__(self):
		return len(self._pairs)

	def generate_image(self):
		fonts = [
		    'font_files/Helvetica.ttf',
		    'font_files/BodoniSvtyTwoITCTT-Book.ttf',
			'font_files/Futura-Medium.ttf',
			'font_files/Optima-Regular.ttf'
		]
		label = random.randint(0,len(fonts)-1)
		fontFile = fonts[label]
		font = ImageFont.truetype(fontFile, 60)

		train_characters = [
			'A','B','C','D','E','F','G','H','I','J','K','L','M',
			'a','b','c','d','e','f','g','h','i','j','k','l','m'
		]
		test_characters  = [
			'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
			'n','o','p','q','r','s','t','u','v','w','x','y','z'
		]
		text = ''

		if self._train:
			text = random.choice(train_characters)
		else:
			text = random.choice(test_characters)

		w, h = 64, 64
		text_w, text_h = font.getsize(text)
		text_x, text_y = (w - text_w) * random.random(), (h - text_h) * random.random()

		im = Image.new('L', (w, h), 255)
		draw = ImageDraw.Draw(im)
		draw.text((text_x, text_y), text, fill=(0), font=font)
		#im.save('image' + str(random.randint(0, 100)) + '.png')
		#if self._train:
		#	im.save('temp/image_train' + str(random.randint(0, 100)) + '.png')
		#else:
		#	im.save('temp/image_test' + str(random.randint(0, 100)) + '.png')

		image_array = np.asarray(im)

		if self._normalize:
		    image_array = image_array / np.max(image_array)

		if self._flatten:
			image_array = image_array.flatten()

		# type cast
		image_array = image_array.astype('float32')
		label = np.int32(label)
		return image_array, label

	def get_example(self, i):
		image_array, label = self._pairs[i][0], self._pairs[i][1]
		return image_array, label
3. 学習モデルと訓練

ほとんどまだチュートリアルのまま回した感じ。これから分類クラス数を増やしたり
モデルを差し替えたりハイパーパラメータをあれこれして楽しみたい

import numpy as np
import chainer
from chainer import cuda, Function, gradient_check, report, training, utils, Variable
from chainer import datasets, iterators, optimizers, serializers
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L
from chainer.training import extensions
from font_image_dataset import *

train_data = FontImageDataset(5000, train=True)
test_data = FontImageDataset(5000, train=False)
train_iter = iterators.SerialIterator(train_data, batch_size=200, shuffle=True)
test_iter = iterators.SerialIterator(test_data, batch_size=200, repeat=False, shuffle=False)

class MLP(Chain):
    def __init__(self, n_units, n_out):
        super(MLP, self).__init__(
            l1 = L.Linear(None, n_units),
            l2 = L.Linear(None, n_units),
            l3 = L.Linear(None, n_out)
        )

    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(x))
        y = self.l3(h2)
        return y

class Classifier(Chain):
    def __init__(self, predictor):
        super(Classifier, self).__init__(predictor=predictor)

    def __call__(self, x, t):
        y = self.predictor(x)
        loss = F.softmax_cross_entropy(y, t)
        accuracy = F.accuracy(y, t)
        report({'loss': loss, 'accuracy': accuracy}, self)
        return loss

model = L.Classifier(MLP(100, 2))
optimizer = optimizers.SGD()

optimizer.setup(model)

updater = training.StandardUpdater(train_iter, optimizer, device=-1)
trainer = training.Trainer(updater, (300, 'epoch'), out='result')
print("start running")
trainer.extend(extensions.Evaluator(test_iter, model))
trainer.extend(extensions.LogReport())
trainer.extend(extensions.PrintReport(['epoch', 'main/accuracy', 'validation/main/accuracy']))
trainer.extend(extensions.ProgressBar())
trainer.run()
print("end running")
4. 結果
  • この設定なら非力なMacBookのCPUでも5分程度で学習完了。
  • エポック250くらいで識別率97%くらいまで上がった。

f:id:mizti:20161126193903p:plain

5. この後
  • 分類クラス数を増やしてみると識別率上限が低くなると思うのでいろんなモデルを試して見る