2019年12月17日火曜日

くずし字を読む (Loader クラスの作成)

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

Loaderクラスの作成

前回の作業で、データセットを用意するところまではできました。しかしこのデータセット、数万枚の画像ファイルでしかありません。これから学習のための Python コードを書くにあたって、試行錯誤するたびに、数万ファイルを開いたり閉じたりするのは非効率なので、少し工夫したいところです。そこで、一度数万ファイルを開いて読み込んだデータは、ひとつの pickle ファイルとして保存することにしました。pickle を使うと、python オブジェクトをシリアライズしてひとつのファイルとして保存したり、そのファイルを読み込んで python オブジェクトを復元したりすることができます。これを利用すれば、2回目以降のデータセットの利用は早くできそうです。

ところで、数万枚の画像ファイルを、どのような python オブジェクトとして表現すれば使いやすいのでしょうか? 機械学習で画像認識を行うために用意された、MNIST(Mixed National Institute of Standards and Technology database) という人気の高いデータセットがあります。ここでは、これにあやかって(パクってじゃないですよ) Loader クラスを作ります。MNIST と同じようなデータセットとして KuzushijiDataSet というクラスを作ったとしたら、以下のような使い方になります。

  1 kds = KuzushijiDataSet()
  2 kds.load_data('save.pkl')
  3 
  4 x_train = kds.train_img    # (32 * 32) の画像リスト
  5 y_train = kds.train_label  # 上記の正解値 (‘あ’== 0 )
  6 x_test = kds.test_img
  7 y_test = kds.test_label
  8

こういう Loader クラスがあれば、x_train(学習用のデータリスト) と y_train(学習用の正解リスト) を使って学習させるプログラムも書きやすいですし、学習が済んだ人工知能に対して x_test(評価用のデータリスト) と y_test(評価用の正解リスト) を使って学習の成果をチェックするのも簡単になります。
作成した Loader クラスの解説に先だって、ソースコードを貼り付けておきます。

  1 #!/usr/local/bin/python
  2 # coding: utf-8
  3 
  4 import os
  5 import pickle
  6 import numpy as np
  7 from PIL import Image
  8 
  9 class KuzushijiDataSet:
 10 
 11     def __init__(self):
 12         """初期化処理"""
 13 
 14         self.train_img = None
 15         self.train_label = None
 16         self.test_img = None
 17         self.test_label = None
 18 
 19     def load_data(self, save_file):
 20         """既に作成したpickleファイルからデータを読み込む(なければ作成する)"""
 21 
 22         if not os.path.exists(save_file):
 23             self.create_pickle(save_file)
 24 
 25         with open(save_file, 'rb') as f:
 26             dataset = pickle.load(f)
 27 
 28         self.train_img = dataset['train_img']
 29         self.train_label = dataset['train_label']
 30         self.test_img = dataset['test_img']
 31         self.test_label = dataset['test_label']
 32 
 33     def create_pickle(self, save_file):
 34         """画像・ラベルを読み込み、pickleとして作成・保存する"""
 35 
 36         print('create_pickel')
 37         train_img, train_label, test_img, test_label = [], [], [], []
 38 
 39         # traningディレクトリはの画像はtrain_img, train_labelへ
 40         # testingディレクトリはtest_img, test_labelへ
 41         kinds = [
 42             ['training', train_img, train_label],
 43             ['testing', test_img, test_label],
 44         ]
 45
 46         for kind in kinds:
 47             # この i はディレクトリ名であると同時に、正解ラベル名
 48             for i in range(1, 48):
 49 
 50                 # output/training/0 等のパス
 51                 dir_path = os.path.join('output', kind[0], str(i+1))
 52 
 53                 # [output/training/1/1.png, output/training/1/2.png]等のリスト
 54                 file_paths = [os.path.join(dir_path, file) for file in os.listdir(dir_path)]
 55 
 56                 # [[画像のnumpy配列], [画像のnumpy配列], ...])のような2次元配列
 57                 images = [np.asarray(Image.open(path)).reshape(32, 32, 1) for path in file_paths]
 58                 kind[1].extend(images)
 59 
 60                 # ディレクトリ名のiが、そのまま正解ラベル名なので、1を入れる
 61                 label = [0] * 48
 62                 label[i] = 1
 63                 labels = [label for x in file_paths]
 64                 kind[2].extend(labels)
 65 
 66         dataset = {
 67             'train_img': np.array(train_img),  # (n_train, 32, 32)
 68             'train_label': np.array(train_label),  # (n_train, 48)
 69             'test_img': np.array(test_img),  # (n_test, 32, 32)
 70             'test_label': np.array(test_label),  # (n_test, 48)
 71         }
 72 
 73         with open(save_file, 'wb') as f:
 74             pickle.dump(dataset, f, -1)
 75 

9行目でクラスの宣言。インスタンス化されたタイミングで 11〜17行目の __init__() が呼ばれますので、メンバ変数を初期化しています。train_img は学習用画像データで、画像ファイル数分のリストです。train_label も同じく学習用で、正解ラベルのリストです。この正解ラベルは、ちょっとおかしな構造になりますので後で具体的に説明します。

19〜31行目はデータセットを Loader クラス内にロードする処理です。引数の save_file には pickle のファイル名が渡ってくる想定です。このファイルが存在していれば読み込んでメンバ変数に展開するだけです。もし、存在していなければ、画像ファイルをひとつひとつ読んで pickle ファイルとして保存するために create_pickle() を呼ぶ処理を挟みます。

33〜75行目が create_pickle() です。"0" という名前のフォルダ(="あ")から "48" という名前のフォルダ (="ん") までを順番に開いて、中の画像ファイルを全て読み込んでいきます。ポイントは、2重の for 文の中に書かれている 57〜64行目の処理です。読み込んだ画像イメージを numpy のリストとして train_img に保存します。こちらは理解しやすいと思います。分かりにくいのは、もうひとつの train_label の方です。例えば train_img の1つめの要素が「あ」の画像データだとします。その場合、train_label の1つめの要素として "あ" を表す 0 などの数値を保存しておけば良さそうなものですが、実際には "あ" から "ん" までの48個分の数値リストを用意して、そのうちの 0 番目(== "あ") の要素だけを 1 にしておきます。

って、分かりにくいですね。。。

train_img の2つ目が 「え」 の画像データだったら、train_label の2つ目は (0, 0, 0, 1, 0, 0,,,,0) となります。データ量としては無駄なのですが、計算が早くなるメリットがあるのでこの構成にしています。
同じように test_img は評価用の画像データのリストで、test_label は評価用の正解ラベルです。

最後の 73〜74 行目で、pickle ファイルとして保存しています。



次回はいよいよ 「実践編」。機械学習を使って、画像ファイルの解析をしてみます。

0 件のコメント:

コメントを投稿