はじめての Deep Learning

実はまったく初めてというわけではないけど、いつもコードを書いて「なんかうまくいかないな」となりつつ、修正方法がわからずそこで終わることが多かった。というわけで MENTA に課金してやり方を教わりつつ進めた。

お題

こういう感じで、「¥10,000」とある画像から「¥」の部分を切り抜く ... というのをお題にした。もともと別のお題があったんだけど、そっちはちょっと大変そうなので値段のところだけ切り出した。

¥の部分を切り抜くモチベーションは OCR の精度向上で「¥10,000」の画像を読ませると「10,000」と読んでくれるときもあるし「410,000」とか「110,000」

summary

7種類のフォントについて、各種50枚前後のデータを用意。これをさらに left/center/right-aligned と horizontal flip をかけて x6 のデータ量に水増し。resnet 18 をベースのモデルを作って、¥の x 軸の境界を学習させたところ、90% 弱の IoU が出せて、OCR の精度も向上した。

type 1  167
type 2  60
type 3  60
type 4  38
type 5  60
type 6  72
type 7  48

data prep

これらの値段の画像は、こういう↓感じの表になってる画像の中から集めたもので、アノテーション用の streamlit app を作った

データ量について、フォントの種類はまず 7個あって*、メンターからは

  • 最低でも 10枚/種類
  • 30枚/種類はほしい
  • 100枚あるとリッチ

と教わった。表の画像を各種1-3枚集めて切り抜いた結果、種類ごとに↓のようなデータ量になった (type 1 だけ量が多いのは、なぜかこれだけパフォーマンスがやたら悪かったから)

*もともと10種類くらいあったけど、一部画質が悪かったのでこれらは除いた

type 1  167
type 2  60
type 3  60
type 4  38
type 5  60
type 6  72
type 7  48

データの整理としてはかんたんに下記の通り実施した。

  • 表の画像はファイル名 type1_1 type1_2 のように設定
  • 切り抜きは、 type1_1/00_00.jpg のように、表の画像ごとにフォルダを作成して <row>_<col>.jpg と設定
  • ¥マークの右端の位置をアノテーションし、表の画像のフォルダの中に type1_1/annotations.csv のように CSV を作成
  • モデルに食わせるときには、表の画像ごとのフォルダを flat にして、1つのフォルダに type1_1_00_00.jpg のようなファイル名にして CSV は一個にマージ
app_data/
  ┣  inputs/
    ┣ type1_1.jpg # フォントの種類が type1
    ┣ type1_2.jpg
    ┣ ...
  ┣ crops/
    ┣ type1_1/
        ┣ 00_00.jpg
        ┣ 00_01.jpg
        ┣ ...
        ┗ annotations.csv
    ┣ type1_2/
        ┣ 00_00.jpg
        ┣ ...
        ┗ annotations.csv
data/
  ┣ type1_1_00_00.jpg
  ┣ type1_1_00_01.jpg
  ┣ ...
  ┣ type1_2_00_00.jpg
  ┣ ...
  ┗ annotations.csv

step 2: 一回これでやってみる

... というのが初期設定だったんだど、これで学習させところ、しょっぱい結果かつ「OCR の精度向上」には繋がらなかった。

メンターに相談したところ、「¥マークじゃなくて『画像の左端を切り抜く』と学習してそう」とのことだった

コードを見てもらったところ、一個めちゃくちゃまずいことをしているのがわかった。なにかというと一層目のレイヤーを変えていたことで、白黒画像を ResNet 18 に食わせるためにこういうコードを書いていた↓

model.conv1 = torch.nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)

これの何がまずいかというと、 せっかく pre-trained モデルを使っているのに first layer を捨てることで、pre-trained モデルが学習した特徴の検出を使ってないことになるから。なので、resnet とあわせて 3-channel 画像を入れたいけど、一方で今回の学習だと色、さらには「白と黒の間の微妙なグレー」というのもノイズになるので、binary の白黒画像にしたい。白黒画像を 3枚積み重ねると白黒の 3-channel 画像ができるので、これを resnet に食わせる。

このほかの修正としては、optimizer を脳死SGD を使っていたけど、adam をおすすめされた。SGD はクラシックなもので、一方 adam は最近ポピュラーな optimizer。adam を使ってうまくいかなかったら他を疑う.. というテンションで使うのものらしい。adam の特徴は gradient boost で、これは local minimum に近づき gradient が小さくなったら、ブーストをかけて gradient を大きく見せるという手法。

step 3: data prep v2

データの準備を修正して

  • 各切り抜きに対して左寄せ・中央寄せ・右寄せの画像をつくって、これらを左右反転させた画像も用意 (i.e. 1枚の切り抜きに対して6枚がつくれる)
  • 左右反転させた画像をつくるので「¥の左端」だとアノテーションができない → 「¥の両端」をアノテーションする (作業やり直しだった...)
  • ただの grayscale じゃなくて binary にする
  • channel 数は 1 じゃなくて 3 にする

画像サイズに関しては、dataset の中から max_width, max_height を算出してすべての画像がそのサイズになるように padding した (黒背景・白文字の画像は先に手動で反転させておく)

step 4: もう一回学習

あとは、 optimizer を SGD から adam に変えて、fc layer も 1 から 2 に変更して学習させて、 IoU の指標も一緒に出すようにした。

loss curve と IoU のプロット↓

学習が終わるごとに、test data 1つずつに対する IoU を出して、こういう感じでシートにまとめてみた

最終的にうまくいったパラメータは learning rate = 1e-3, epochs = 100 で、IoU が 0.85 を超えたら early stopping を入れた。なお、この 0.85 の数字は OCR の方にあててみて「90% 前後になっていれば精度向上が見込める」ということを確認して使った。

まとめ

model

今回はデータ量が少ないので、ResNet の中でも一番小さい ResNet18 を使う。大きいモデル = パラメータ数が多い → ちゃんと学習させるためにはデータ量もたくさん必要。

LR = 1e-3, epochs = 100 で最終的に落ち着いた。

loss function

  • optimizer はとりあえず使うなら adam; adam で学習崩壊が起きたら ada-grad とか rmsprop とかがある
  • SGD は素直な optimizer
  • %を出すなら cross-entropy の方がいい
  • 外れ値が多いなら Huber

early stopping について

今回のような 100 epochs < 1h 程度の学習なら、この段階で early stopping を入れるのはもったいない。かわりに 300 epochs くらい回して、学習ごとにモデルとそのモデルの指標 (今回なら IoU) を保存しておく → 全 epochs 回ってから一番いいものを選ぶ、という手もある。

data prep

前提: ResNet を使うなら 3-channel かつすべての画像が同じ入力サイズである必要がある

  • データ量的には今回のケースだと、minimum = 10, optimal = 30, very good = 100 という感覚
  • 今回のようにターゲットが画像の特定の位置に偏るようなデータだと (円マークは画像の左端にある)、 left/center/right align, horizontal flip を入れることでモデルに「画像の左端」じゃなくて「円マーク」を学習させやすくなる
  • 画質の悪いぼやけた画像は、ふつうだと人工的に作ってモデルをロバストにするために入れる
  • 色の情報だけでなくグレーというのもノイズになりうるので、binary も検討しよう

その他

  • 学習結果を記録するには tensorboard を使おう