チョコボール統計

チョコボールの秘密を統計解析で明らかにしていく。おもちゃのカンヅメ欲しい。

【改訂版】チョコボール画像からチョコボールの個数を自動計測してみる

概要

【トップに戻る】

はじめに

当ブログでは、チョコボールのエンゼルの出現有無の他に、個数や重さを計測しています。その中で、チョコボールの個数を数えるのがちょっと手間なので、画像から自動計測できないかなという軽い考えでDeepLearningを利用したチョコボール検出器(ChocoBallDetector)を作ってみました。

なお、本記事は以前書いた記事の改訂版です。前処理で使ってるクラスへのリンクが無いなど不親切な記事だったので、ソースコードと合わせて全面的に書き直しました。(本記事だけ読んでも内容がわかるように、以前の記事と重複する記述もあります)

以前書いた記事はこちら

chocolate-ball.hatenablog.com

【トップに戻る】

問題設定

チョコボールを撮影した写真を元に、チョコボールの個数を計測する仕組みを開発します。

計測対象の写真は以下のようなものです。この写真から、チョコボールの個数(この場合は18個)を計測することが目的です。

f:id:hippy-hikky:20210329234454j:plain

  • スマートフォンで撮影
  • 証明条件や距離などは厳密に定められない
  • カメラの機種は変わることがある
  • チョコボールは手で雑に並べるため距離や重なりがある場合がある
  • チョコボールはピーナッツに由来して大きさや形が一定では無い

このような条件から、深層学習によるObjectDetectionタスク(下図)を採用します。チョコボールを学習し、検出した個数を計測数とします。当然、公開されている学習済みモデルはチョコボールを認識してはくれません。そのため、独自の学習データセットを作成し、チョコボールが認識できるように追加学習(fine tune)します。

f:id:hippy-hikky:20210329235536p:plain
一般物体認識の事例. chainercv, Object Detection Tutorial より引用. https://chainercv.readthedocs.io/en/stable/tutorial/detection.html

【トップに戻る】

ChocoBallDetector開発手順概要

チョコボール検出器(以降、「ChocoBallDetector」と表記)の開発手順は、以下の通り大きく4つの手順にわかれます。(一般的な機械学習モデルの開発手順と同じですね)

  • データ準備
  • 前処理
  • 学習
    • 学習設定(Optimizerの設定など)を行い、学習を実行します
  • 評価
    • 学習データとは別に用意する評価データを使って学習結果を確認します

この手順に従って開発を進めて行きます。

【トップに戻る】

実装

実装の詳細は以下のGithubリポジトリを参照してください。実行手順などもリポジトリ内のWikiに記載しています。

github.com

環境構築

Dockerfile, docker-compose.ymlを用意していますのでそちらを利用してもらえたらと思います。

$ git clone https://github.com/tok41/ChocoBallDetectorTrainer.git
$ cd ChocoBallDetectorTrainer
$ docker-compose build
$ docker-compose up

なお、公開しているdocker-compose.ymlではGPUが使えるように環境変数を設定していますが、CPUだけで動かしたい場合にもこのままで良いと思います(未確認)。GPUを使いたい場合には、nvidia-container-runtimeのインストールが必要です。以下の記事などを参考に。

qiita.com

Docker利用せずに環境構築する際は、requirements.txtに加えてchainerをインストールしてください。

データ準備

学習に必要なデータは以下の3種類です。

  1. 画像ファイル(jpg形式など)
  2. アノテーションデータ(PascalVOC形式のXMLファイル)
  3. 検出クラスの定義ファイル(テキストファイル)

画像ファイルは、適当な大きさにリサイズしておきます。今回は、[402x302]のサイズにリサイズしています。あまりサイズが大きいと入力次元が大きくなるため、モデルサイズも大きくなります。

アノテーションデータはPascalVOC形式のxmlファイルということで、どうやって用意しても良いのですが、今回は、labelimgというツールを使用させてもらいました。使い方はlabelimgのREADMEを読んでもらえればよいと思います。一応、Wikiページに簡単にツールの実行手順を書きました。

検出クラスの定義ファイルについては、検出したい物体クラス名を1行に一つ並べていきます。今回はチョコボールだけでも良かったんですが、パッケージ画像も認識するようにしてみました。data/classes.txtを参照してください。

このような画像データに対して、

f:id:hippy-hikky:20210330162516j:plain

このように(見にくいですが、、、)物体毎の位置をボックスで指定します。

f:id:hippy-hikky:20210330162555p:plain

なお、学習データと評価データを分けておきたい場合は、アノテーションデータを学習用とテスト用にディレクトリを分けておいてください。

前処理

データを準備したら次は、学習に使えるようにメモリ上にロードします。

必要なデータ形式は、次の通りです。

  • 画像データ: numpy.ndarray([N, Channel, Height, Width])
  • バウンディングボックス座標リスト: list([numpy.array([N, 4])])
  • バウンディングボックス毎の物体IDリスト: list([numpy.array([N, ])])

これらのデータオブジェクトを作るために、ChocoPreProcessorというクラスを用意してあります(src/preprocessor.py)。

以下のように、クラス定義ファイルパス(CLASSES_FILE_PATH)、アノテーションデータディレクトリのパス(BBOX_DIR_PATH)、画像データディレクトリのパス(IMG_DIR_PATH)を入力すると、xmlデータのパースや画像データの次元の変更などの処理を実施します。

from src.preprocessor import ChocoPreProcessor

choco_prep = ChocoPreProcessor()

choco_prep.set_classes(class_file=CLASSES_FILE_PATH)
_ = choco_prep.set_dataset(anno_dir=BBOX_DIR_PATH, img_dir=IMG_DIR_PATH)

具体的な使い方については、notebook/check_annotated_dataset.ipynbなどを参照してください。

学習

データセットが用意できたら、次はモデルの学習(fine tuning)を行います。

学習のフレームワークには、chainerとchainerのComputerVisionライブラリであるchainercvを利用します。残念なことに、2021年3月現在ではすでにChainerの新規開発は終了しています。なので、最新のモデルを手軽に使いたい場合にはPyTorchなどの別のフレームワークを利用した方が良いです。今回は、単純なリファクタリングということで、以前の構成から大幅な変更はしないつもりなので、chainercvをそのまま利用しています。別のフレームワークの利用については、今後の課題です。

ObjectDetectionのモデルとしては、Faster R-CNNを利用します。最近だとYOLOなどを使った方が認識速度、認識精度のバランスが良いのかなと思いますが、構成を大きく変えないということで。

学習については、chainercvのObject Detection Tutorialをほぼそのまま使っています。公式チュートリアルを確認しましょう。

今回、学習用にChocoTrainerクラスを用意しました(src/trainer.py)。

次のように、choco_trainerオブジェクトを生成して、データ、モデル、Optimizerをセットします。学習の実行にはrunを呼びます。多少のパラメータ変更はできますが、Optimizerの切り替えなどには対応していないので、もし必要なら直接書いてください。

from src.trainer import ChocoTrainer

choco_trainer = ChocoTrainer(out=OUT, step_size=300, logger=logger)

_ = choco_trainer.set_data(images=imgs, bboxs=bboxs, obj_ids=obj_ids)
choco_trainer.set_model(n_class=len(classes))
choco_trainer.set_optimizer()

choco_trainer.run()

デフォルトでは、出力ディレクトリ(OUT)に学習曲線(train loss)の変遷がpng形式で吐き出されています。また、学習途中のモデルファイルのスナップショットが出力されているはずです(デフォルトでは10epoch毎)。

最後のエポックのスナップショットが最終的な学習結果になりますが、save_modelメソッドを呼ぶことで、明示的に出力することもできます。

model_file = f"{OUT}/choco_faster_rcnn.npz"
choco_trainer.save_model(file_name=model_file)

具体的な実装例については、notebook/train_chocoball_detector.ipynbを参照してください。

評価

学習済みモデルに対して、評価データを入力し、学習結果を評価します。評価指標はチョコボール検出個数のMSE(平均二乗誤差)です。チョコボール個数を数えることが目的なんで。

評価用にChocoEvaluatorクラスを用意しています(src/evaluator.py)。検出位置のズレなども評価はできるのですが、ここでは対応していません。チョコボール個数を数えることが目的なんで。もし必要なら書いてください。

まず、学習と同様に評価用データの配置されているディレクトリのパス(BBOX_DIR, IMG_DIR)を入力し、評価データをセットします。

from src.preprocessor import ChocoPreProcessor

choco_prep = ChocoPreProcessor(logger=logger)
choco_prep.set_classes(class_file=CLASSES_FILE)
dataset = choco_prep.set_dataset(anno_dir=BBOX_DIR, img_dir=IMG_DIR)

次に、学習済みモデル(MODEL_PATH)をloadします。そして、evaluate_chocoball_numberメソッドに画像データと物体IDリストを入力することで、MSEの計算と、推論結果(推定バウンディングボックス座標、推定物体クラスID)を返してくれます。

from src.evaluator import ChocoEvaluator

ce = ChocoEvaluator(gpu=0)
ce.load_model(model_file=MODEL_PATH)

res_list, mse = ce.evaluate_chocoball_number(images=imgs, true_labels=obj_ids)

具体的な実装例については、notebook/evaluation_chocoball_detector.ipynbを参照してください。

【トップに戻る】

学習/評価結果

学習

今回は、学習用に26枚、評価用に7枚の計33枚のデータを用意しました。(少ないとは思いますが、アノテーション作業きついんで。。。)

このデータを使って、imagenetで学習したVGG16を使ったFasterRCNNモデルをfine tuneします。実装の詳細は、以下の二つのファイルを合わせて読んでください。

20epoch学習した後の学習曲線は以下の通りとなりました。

f:id:hippy-hikky:20210330170932p:plain

lossは下がっているようですし、400iteration程度(約16epoch)で収束しているようにも見えます。学習は成功ということで。

評価

続いて、評価データを使って検出誤差を算出してみます。

こちらも、以下のnotebookに結果が掲載されています。

Evaluation Images: 7
MSE: 0.0

ということで、検出誤差が0となりました。(うまくいきすぎて面白くない)

検出結果を見てみます。二枚画像がありますが、どちらも検出結果で、右は物体ラベルと推論の確信度を合わせて表示しています。

f:id:hippy-hikky:20210330173847p:plain

チョコボールの形の不揃いにも対応できてますね。

f:id:hippy-hikky:20210330173904p:plain

チョコボールがある程度密集していても検出漏れはありませんでした。

f:id:hippy-hikky:20210330173925p:plain

画像が逆さでも大丈夫。

f:id:hippy-hikky:20210330173943p:plain

パッケージ画像がなくても大丈夫。

ということで、すごく良い感じですね!

学習と評価結果のまとめ

今回、Faster R-CNNモデルを使って、チョコボール画像からチョコボールを検出するモデルを学習してみました。

学習データは26枚とだいぶ少なかったのですが、学修には使っていない評価データでの検出誤差は0でした。いくつか検出結果を確認してみましたが、どれも想定通りに検出できています。

評価データが少ないので、他の画像でどうだというのは正直わからないですが、そこそこ使えそうなモデルが学習できたのではないかと思います。

少ない画像データでこのくらいの精度のモデルが学習できたのにはChocoBallDetector固有の特徴があると思います。

まず、画像は距離などが多少違っていてもだいたい一緒ということが挙げられます。ティッシュを置いて、その上にチョコボールが並べられているという構成は同じです。次に、画像データは26枚ですが、チョコボールは各画像に16~20個程度あります。そのため、「チョコボール」としては、数百種類見ているということが挙げられます。最後に、ChocoBallDetectorでは、検出物体が2種類しかありません。

このような要因でたった26枚程度の画像でそこそこの精度のモデルが学習できたのだと考えています。言い換えれば、今回のチョコボール画像に過学習していると言えるでしょう。「過学習」ではありますが、ChocoBallDetectorが見る世界はこの画像データだけなのでこれで良いのです。(一般的には使えませんが)

【トップに戻る】

まとめ

ということで、Faster R-CNNを利用して、写真からチョコボールを検出するモデルを学習(Fine tuning)してみました。その結果、学習に使ったデータ(チョコボール画像)はたったの26枚でしたが、評価用のデータに対しては誤差なく個数を検出できていました。

しかし、前回の記事でも書いていますが、評価データが数枚しかないので、この評価結果はあくまで参考程度ではあります。また、ハイパーパラメータチューニングなんかもやっていないです。この課題については、今後やるかもしれませんがあまり期待はしないでください。

今回のリファクタリングで、前処理、学習、評価をそれぞれクラス化しました。これらはチョコボールに限った作りには一切なっていない(つもり)なので、他の問題にも適用できるのかなと思います。環境構築についてはDockerの利用を想定していますが、requirements.txtを使って手動で構築しても問題ないと思います。

最後に、画像データやアノテーションデータなどが欲しいと言う方がいらっしゃったら、Twitter(@yoichi_t)などで直接連絡いただけたらと思います。

【トップに戻る】

参考資料

【トップに戻る】