2019年12月10日火曜日

くずし字を読む (準備編)

この記事は、ケーシーエスキャロット Advent Calendar 2019 の10日目の記事になります。


前回の記事でちょっとミスりました。”これら、すべて「き」と読みます” なんて書いておきながら、ひとつも正しく表示されていないというお粗末な結果に。。。

U NICODE には登録されている文字なのですが、それに対応したフォントファイルをインストールした環境でないと正しく表示されないのでした。 「IPAmj明朝」フォントをすでにインストールしていたなんていうマニアックな人以外は見られなかったと思います。というわけで、部分的に画像ファイルに差し替えておきましたので、今はどのブラウザでも表示できるはずです。

データセットの準備

機械学習のうち、今回扱う手法は「教師あり学習」です。なので、自分が教師役として PC に学習させるというスタンスで話を進めます。まずは学習させるためのコンテンツが必要です。今回はくずし字を読めるようになってもらいたいので、大量のくずし字画像を手に入れます。その後、PC に画像を読み込ませる時に正解も一緒に教えます。つまり「あ」の画像を読み込ませながら、それは「あ」と読むんだよ、と教えてあげるわけです。ここまでが『学習』の過程です。学習が終わったら、まだ読み込ませたことのない画像ファイルを読ませたみて、正しく分類できるかどうかを確認します。

とにかく、まずは大量のデータが必要です。古文書を開いてデジカメで撮影し、1文字ずつ画像ファイルにしていきましょう!
っていう気の遠くなるような作業はとてもやっていられませんので、そんな作業をやってくれている人を探してみると、、、あるんですねぇ、そういう機関が。

人文学オープンデータ共同利用センター
http://codh.rois.ac.jp
このデータセット(国文学研究資料館ほか所蔵/情報・システム研究機構 データサイエンス共同利用基盤施設 人文学オープンデータ共同利用センター加工)は、クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンス(CC BY-SA)の下に提供されています。

例えば、
【南総里見八犬伝】
http://codh.rois.ac.jp/char-shape/book/200014685/

からデータセットをダウンロードすると、15,864文字分もの画像データが入手できます。ありがたい!

私は15冊分のデータを使わせてもらいました。本のタイトルをフォルダ名にして、それぞれのフォルダの中に「あ」から「ん」までの名前をつけたフォルダを掘り、その中に画像データが置いてあるという構成にしました。(漢字も含めると大変な量になるので、自分に優しく、今回はひらがなだけに絞ることにして、漢字の画像ファイルは捨てました)



と、ここで、いくつか不都合な点に気がつきました。
  1. ファイルの大きさがバラバラ。
  2. ファイルの縦横比がバラバラ。
  3. 裏移りしている文字がある。
  4. 薄過ぎる文字がある。
  5. 墨で潰れてしまっている文字がある。
  6. 欠けている文字がある。
  7. ひとつのファイルに他の文字が含まれてしまっているものがある。

本来ならこんな違いはものともせずに学習してほしいところですが、生徒に優しい私としては、もう少しデータセットをきれいにしておきたいところです。上記の何が不都合なのかを簡単にまとめておきます。

  1. ファイルの大きさがバラバラ。
  2. ファイルの縦横比がバラバラ。



文字を理解するのに必要な特徴だけ含まれていれば画像としては充分ですので、必要以上に大きくてもメリットはありません。容量が嵩張るだけ無駄ですし、学習時にも時間がかかってしまいます。縦横比が異なっていても本来は問題ないのですが、あとでとある技法を使いたいので、全て同じ縦横比になっていると都合が良いです。なので、これらのデータは全てサイズを統一させておくことにしました。


  1. 裏移りしている文字がある。
  2. 薄過ぎる文字がある。
  3. 墨で潰れてしまっている文字がある。
  4. 欠けている文字がある。



これらは、単純に学習精度が落ちる気がするので、学習データに含めたくないと感じたものです。これらのデータは削除することにしました。本当ならそういう画像でも正しく読めて欲しいところですが、今回は優しく、ということで。

  1. ひとつのファイルに他の文字が含まれてしまっているものがある。



これは、「し」を長方形に切り出したものですが、どうみても「すべし」です。自由なレイアウトで書かれているので、こんな画像ファイルもできてしまいます。今回はこれも削除することにしました。


データセットの補正

上記の問題点を踏まえて、まず削除対象のファイルを手でコツコツと削除しました。(これが相当にしんどかった。。。) 残った画像ファイルは、平仮名の48種類、225,417枚です。この大量の画像ファイルが、本のタイトルごとにフォルダに入っている状態です。

これらの画像ファイルに施したい補正は、32x32 のサイズに統一することです(ついでにカラーの情報も要らないのでグレースケール化したいところです)。アルゴリズムとしては、画像ファイルを1枚ずつ読み込み、32x32 のサイズに統一しつつグレースケール化し、新しいファイルとして保存していきます。保存先は、学習用と評価用のフォルダを用意し、5対1の割合でランダムに振り分けていきます

 解説に先立って、まずはソースコードを貼ってしまいます。
  1 #!/usr/local/bin/python
  2 # coding: utf-8
  3 
  4 import os
  5 import random
  6 
  7 import numpy as np
  8 from PIL import Image
  9 from PIL import ImageStat
 10 
 11 classes = {'あ': 0, 'い': 1, 'う': 2, 'え': 3, 'お': 4,
 12            'か': 5, 'き': 6, 'く': 7, 'け': 8, 'こ': 9,
 13            'さ':10, 'し':11, 'す':12, 'せ':13, 'そ':14,
 14            'た':15, 'ち':16, 'つ':17, 'て':18, 'と':19,
 15            'な':20, 'に':21, 'ぬ':22, 'ね':23, 'の':24,
 16            'は':25, 'ひ':26, 'ふ':27, 'へ':28, 'ほ':29,
 17            'ま':30, 'み':31, 'む':32, 'め':33, 'も':34,
 18            'や':35,          'ゆ':36,          'よ':37,
 19            'ら':38, 'り':39, 'る':40, 'れ':41, 'ろ':42,
 20            'わ':43, 'ゐ':44,          'ゑ':45, 'を':46,
 21            'ん':47}
 22 
 23 size = width, height = 32, 32
 24 RootPath = u'./dataset'
 25 OutTestingPath = u'./output/testing'
 26 OutTrainingPath = u'./output/training'
 27 TopList = os.listdir(RootPath)
 28 
 29 random.seed(3284)
 30 
 31 for subdir in TopList:
 32     if subdir == u'.DS_Store':
 33         continue
 34     print(subdir)
 35 
 36     SubDirPath = os.path.join(RootPath, subdir)
 37     SubDirList = os.listdir(SubDirPath)
 38     for dir in SubDirList:
 39         if dir == u'.DS_Store':
 40             continue
 41
 42         EndDirPath = os.path.join(SubDirPath, dir)
 43         FileList = os.listdir(EndDirPath)
 44         OutTestingDirPath = os.path.join(OutTestingPath, str(classes.get(dir)))
 45         OutTrainingDirPath = os.path.join(OutTrainingPath, str(classes.get(dir)))
 46         if not os.path.exists(OutTestingDirPath):
 47             os.makedirs(OutTestingDirPath)
 48         if not os.path.exists(OutTrainingDirPath):
 49             os.makedirs(OutTrainingDirPath)
 50 
 51         for fname in FileList:
 52             
 53             # 画像を一枚開く (この時点ではサイズはバラバラ)
 54             img = Image.open(EndDirPath + '/' + fname).convert('L')
 55 
 56             # 32 x 32 に収まるように縮小 (縦横比は元のまま)
 57             img.thumbnail(size, resample=Image.ANTIALIAS)
 58             
 59             # 以下で、32 x 32 の周囲にできる余白部分をなじませる
 60 
 61             # 左上と右上のピクセルのうち明るい方の色を背景色として採用して、
 62             # 新しい Image オブジェクトを作成
 63             img_width, img_height = img.size
 64             bk_color = max(img.getpixel((img_width-1,0)), img.getpixel((0, 0)));
 65             bg = Image.new("L", size, bk_color);
 66             
 67             # 元の画像を中央に貼り付ける 
 68             center = (width - img_width) // 2, (height - img_height) // 2
 69             bg.paste(img, center)
 70             
 71             # 学習用のデータと評価用のデータに振り分ける
 72             r = random.randint(1,6) 
 73             if (r == 1):
 74                 bg.save(OutTestingDirPath + '/' + fname)
 75             else:
 76                 bg.save(OutTrainingDirPath + '/' + fname)
 77        

全ての画像ファイルを読み込むために、for 文でグルグルしているところは割愛して、57 行目がポイントです。 縦横比を変えないまま 32x32 に収まるようにサイズを変換しています(1行でできちゃう、さすが Python!)。

ただし、ここでひとつ問題が発生します。例えば縦長の「し」という文字を、背景が白い 32x32 の中に収めた場合、左右に白い長方形の領域が残ってしまいます。「フランス国旗のように3つの領域があって、真ん中が黄ばんだ感じ」という余計な情報も含めて「し」だと学習されるとちょっと困ります。できれば「し」という文字が書かれている紙の黄ばんだ感じの色で、32x32の領域を埋めておきたいところです。とはいうものの、自然に黄ばんだものですし、本によってその程度もまちまちですので、固定値にすることもできず困ったものです。

その辺りの工夫が、63〜69行目です。コメントに書いてある通りですが、結構アナログな発想でなんとかしました。実際に変換したファイルは以下のようになります。


上記は「あいうえお」の5文字です。なんとか、きれいに変換できました。

最後の 72 行目以降で、変換後の画像ファイルを「学習用」と「評価用」に分けています。



次回は「Loaderクラスの作成」。まだ、学習は始まらない。。。

0 件のコメント:

コメントを投稿