Chainerでフォントを識別してみる (1)
Chainerで何か分類してみるにあたって、
1. 分類できて嬉しい
2. サンプル収集の目処が立ちそう
3. 莫大なデータ量にならない
な分類対象として、文字のフォントの分類をしてみようと考えた。
2. データセットの作成
- 大文字もしくは小文字を一文字ずつ64*64の画像に印字したデータを作成する。これより画像が小さいと、ジャギが立ってしまうので、フォント判別に良く無いと判断した
- 用意するデータ毎にx位置y位置をずらしておく。(フォントによって、ベースライン位置が異なっていたりするため、これを手掛かりにさせず、あくまで字形で分類させたいため)
- Train用のデータセットでは[a-mA-M]、Test用のデータセットでは[n-zN-Z]の文字を用いる
train用
test用
(※データをちゃんと作れたか確認のため画像出力したが、実際にはメモリ上でリスト保持するのみ)
- 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. 学習モデルと訓練
ほとんどまだチュートリアルのまま回した感じ。これから分類クラス数を増やしたり
モデルを差し替えたりハイパーパラメータをあれこれして楽しみたい
- モデル: Chainerチュートリアルそのまんまのアフィン3層MLP
- ミニバッチサイズ200、epoch数300のSGDで訓練。
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")
5. この後
- 分類クラス数を増やしてみると識別率上限が低くなると思うのでいろんなモデルを試して見る