fast.ai で deep learning を勉強しよう(3)Lesson 2: Teddy bear detector

Lesson2では、まずGoogle画像検索を使用して、トレーニング用の画像を集めることから始めます。流れとしては次のようになります。

  1. Google Chrome で画像を検索し、表示されてた画像のURLを書き出したファイルを作成。
  2. そのファイルに書かれた画像をダウンロード。
  3. ダウンロードされたものが画像として開けるかどうかをチェックしてクリーニング。
  4. その画像を使用してLesson1と同じようにトレーニング。
  5. Learning Rate の過少、過大時の特徴を確認。
  6. Epochの過少、過大(オーバーフィッティング)時の特徴の確認

lesson2-download.ipynb

[関連サイト]
Hiromi's Note : https://github.com/hiromis/notes/blob/master/Lesson2.md
Video : https://course.fast.ai/videos/?lesson=2
Notebook : https://github.com/fastai/course-v3/blob/master/nbs/dl1/lesson2-download.ipynb

Creating your own dataset from Google Images

Step 1: Gather URLs of each class of images

使用する画像をダウンロードしなくてはいけませんが、その為の url が書かれたファイルを用意しなくてはいけません。google chrome で画像検索を行い、表示されている画像の url が書かれたファイルを作成します。 「teddy bear」の画像検索を写真で行います。ピンクの teddy bear とかも表示されたので、色を茶色で絞り込みました。(どんな集合から何をクラス分類するかは自由ですので、サンプルは自分で選択しましょう) 画像が表示されたら、スクロールしてすべての検索画像が表示されるようにします。その後「Shift」+「Ctrl」+「j」で Javascript console 開いて下記のスクリプトを実行します。

urls = Array.from(document.querySelectorAll('.rg_di .rg_meta'))
.map(el=>JSON.parse(el.textContent).ou);
window.open('data:text/csv;charset=utf-8,' + escape(urls.join('\n')));

dawnloadフォルダーに「ダウンロード」という名前でファイルができるので、ファイル名を「urls_teddys.txt」に変更して bears フォルダーに格納します。

f:id:feynman911:20190514222308j:plain

「black bear」と「teddy bear」も同様に画像の url が書かれたファイルを作成します。

f:id:feynman911:20190514222338j:plain

f:id:feynman911:20190514222355j:plain

ブラックベアとグリズリーベアは色だけで判断できるのかと思いきや、そうではないようです。

(参考)https://www.world-package.com/01wildlife.php からの引用

ブラックベアー(American Black Bear=アメリカクロクマ)は、その名から黒い毛色をしていると思われがちだが、実際には真っ黒な毛色の他にもブロンド、シナモン色、明るい褐色から濃いチョコレート色、青みがかった色、それらの中間色の毛色を持つものまで様々である。 また胸にはっきりとした白い斑点が見られることが先に述べたグリズリーベアーには無い特徴で、さらに身体的な違いとして、鼻筋が真っ直ぐ(ローマン・ノーズ)で比較的とがっていること、鼻先のすぐ上から鼻面周りにかけて色が薄くなっているのが目立つことが挙げられる。またその体長は140~180cm、体重40~270kgとグリズリーベアーに比べ少し小柄だが、それは同じ雑食性哺乳類とは言えブラックベアーの方がより草食を好むということもその一因なのだ。

グリズリーベアー(Grizzly Bear=ブラウンベアーまたはビックブラウンベアー)は、ヒグマの仲間であり、 日本ではハイイログマやアラスカヒグマなどと呼ばれる種類のクマだ。このグリズリーという名前は「グリズル」という言葉から由来しており、毛先がまだらに白くなっている、白と暗色の混じりあった毛という意味がある。多くのグリズリーベアーが、暗色の毛の先端だけが白っぽい毛色をしているのが一つの特徴だが、全体的な毛色は淡く輝くようなブロンドから、赤みがかったブロンド、薄い褐色、中間色から薄い褐色、そして黒っぽい毛色まで様々な色調の毛を持っている。さらに胸から肩にかけてまるで襟のように白っぽくなっている個体、背中と肩の辺りの毛が白っぽくなっている固体であったりと身体の部分によって色が違っていることもよくある。ただ、一般的に遠くから見るとグリズリーベアーは、足が黒っぽく背中の方にいくにしたがって明るい毛色に見える。そして身体的には、横広い顔の輪郭の割には耳が小さいこと、鼻筋が窪んでいること、肩に筋肉の塊である瘤があること(グリズリーベアーが地面を掘る能力に優れているため)、前足の爪が長い等の特徴を持っている。その体長は180~260cm、体重110~530kgと大型の雑食性哺乳類であり、雑食性とはいえ主には植物(草の根やベリー類)を好んで食す。秋になると産卵のために遡上してきたサーモンを捕るなど、冬眠前の食欲はかなり旺盛で一日1kg程度ずつ体重を増やし脂肪を蓄え、厳しい冬に備える。

google 検索時のヒント)
- いくつかの単語のつながりを一連で検索する時には " " で括る。 例えば、ヨーロッパオオカミを学術名で検索するには "canis lupus lupus" とする。 - 削除したいキーワードは - を付け書く。 例えば dog という単語を上記から除きたい時には、"canis lupus lupus" -dog とする。

Step 2: Download images

まずは fastai.vision のモジュールをすべて import します。

from fastai.vision import *

画像データは次のような構成のフォルダーに納めます。

data ー bears
     - black
     - teddys
     - grizzly

次のコードで black フォルダーが作成でき、画像がダウンロードされます。

folder = 'black'
file = 'urls_black.txt'
path = Path('data/bears')
dest = path/folder
dest.mkdir(parents=True, exist_ok=True)
download_images(path/file, dest, max_pics=200)

teddys フォルダー、grizzly フォルダーも同様にしてコードで作成できます。

folder = 'teddys'
file = 'urls_teddys.txt'
path = Path('data/bears')
dest = path/folder
dest.mkdir(parents=True, exist_ok=True)
download_images(path/file, dest, max_pics=200)
folder = 'grizzly'
file = 'urls_grizzly.txt'
path = Path('data/bears')
dest = path/folder
dest.mkdir(parents=True, exist_ok=True)
download_images(path/file, dest, max_pics=200)

飛び飛びのセルを上下しながら実行する事になるので煩雑です。 Jupyter Notebook がクラウド上のサーバーで動いていることを前提にしているので、このようにJupyter Notebook でフォルダー作成をやっているのだと思います。PC上で作業をしているなら直接PCのエクスプローラ上で作成してしまった方が簡単でしょう。

Step 3: Create ImageDataBunch

ダウンロードした画像が読み込み可能かどうかをチェックして、読み込めないものは削除します。

classes = ['teddys','grizzly','black']
for c in classes:
    print(c)
    verify_images(path/c, delete=True, max_size=500)

verify_images が何なのかをチェックしたい時には

doc(verify_images) 

を実行するとドキュメントを見ることができます。

DataBunch を作成します。今回はクラスごとの画像がフォルダーに入っているので .from_folder で作成します。 トレーニングデータとバリデーションデータを分離していないので、乱数を使用して一定割合をバリデーションデータとして使用するように設定します。次の指定では 0.2 すなわち 20% のデータをバリデーション用に使用するような設定です。

データ作成直前で乱数のシードを設定しているのは、常に同じシードを設定してから作成するとバリデーション用データ 20% を同じ画像に固定できるからです。(プログラミング上の乱数と言うのは、本当の乱数ではなく直前のデータから計算によって次のデータを作成していくという疑似乱数なので、同じシードから作成される乱数列は常に同じであるという性質があるからです)

np.random.seed(42)
data = ImageDataBunch.from_folder(path, train=".", valid_pct=0.2,
        ds_tfms=get_transforms(), size=224, num_workers=4).normalize(imagenet_stats)

これも doc で説明を見てみると、from_folder の他に from_csv, from_df, from_name_re, from_name_func, from_lists 等がある事が分かります。

次の行で、分類するクラス名、クラス数、トレーニングデータ数、バリデーションデータ数の確認ができます。

data.classes, data.c, len(data.train_ds), len(data.valid_ds)

(['black', 'grizzly', 'teddys'], 3, 424, 106)

Step 4: Training a model

作った ImageDataBunch を使って、resnet34 でcnn学習モデルを作成します。

learn = create_cnn(data, models.resnet34, metrics=error_rate)

学習を4エポック(トレーニングデータを4回)進めて見ます。

learn.fit_one_cycle(4)
epoch    train_loss      valid_loss  error_rate 
1       1.066794        0.332885        0.122642
2       0.585702        0.131696        0.028302
3       0.413400        0.120602        0.028302
4       0.316123        0.115495        0.028302

train_loss はトレーニングデータでの損失で、そのトレーニング結果をバリデーションデータに適用した時の損失が validate_loss, バリデーションデータをクラス分類した時の間違い率が error_rate という事になります。

ここまでの結果を保存しておきます。

learn.save('stage-1')

ここまでのトレーニングでは、cnn 部分のパラメータは固定された転移学習です。 cnn部分も unfreeze してパラメータを調整することで更にトレーニングを進めます。

learn.unfreeze()

レーニングさせるときの学習率 learning rate の変動範囲を決めるために learning rate finder という手法を使います。

learn.lr_find()
learn.recorder.plot()

(参考) https://sgugger.github.io/how-do-you-find-a-good-learning-rate.html

結果として次のようなグラフが得られるので、単調減少している範囲を余裕をもって設定します。

f:id:feynman911:20190514223233j:plain

例えば 3e-5 ~ 3e-4 の範囲を設定するとして 2エポックトレーニングして train_loss が減少するのを確認してみます。

learn.fit_one_cycle(2, max_lr=slice(3e-5,3e-4))

ここまでの内容を保存しておきます。

learn.save('stage-2')

Interpretation

レーニング結果の解釈をします。stage-2 を保存した後、一度中断して再開する時には、次のようにして状態を再現できます。

from fastai.vision import *
path = Path('data/bears')
np.random.seed(42)
data = ImageDataBunch.from_folder(path, train=".", valid_pct=0.2,
        ds_tfms=get_transforms(), size=224, num_workers=4).normalize(imagenet_stats)
learn = create_cnn(data, models.resnet34, metrics=error_rate)
learn.load('stage-2');

次のコードで confusion matrix を表示する事ができます。

interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix()

f:id:feynman911:20190514223314j:plain

バリデーションデータのうち3画像のクラス分類が間違っていることが分かります。

間違ったデータが、本当に間違っているのか、元のデータが正しい画像ではない可能性もあります。

Cleaning Up

今までは google で検索したものをすべて信じて使いましたが、適切でないデータも混ざっていると思われます。次のコードを実行すると、画像が表示されるので、分類が間違っているものは正し、好ましくないものは Delete を押すという作業を Next Batch を押して繰り返します。

from fastai.widgets import *
ds, idxs = DatasetFormatter().from_toplosses(learn, ds_type=DatasetType.Valid)
ImageCleaner(ds, idxs, path)

f:id:feynman911:20190514223357j:plain

この作業によって、Delete を押した画像が除かれたデータセットが cleaned.csv ファイルとして保存されます。

画像が重複して保存されている可能性もあるので、それを検出して削除する方法もあります。似ている画像が表示されるので、確認して不要な画像を削除します。

ds, idxs = DatasetFormatter().from_similars(learn, ds_type=DatasetType.Valid)
ImageCleaner(ds, idxs, path, duplicates=True)

f:id:feynman911:20190514223423j:plain

cleaned.csv から databounch を作れば、より正しい学習を行う事ができます。

Putting your model in production

learn.export('trained_model.pkl')

これを実行すると trained_model.pkl ファイルができます。 (ファイル名を指定しないと export.pkl)

このファイルには、 クラス分類を行うのに必要な最小限の情報が入っているので、このファイルを使ってアプリを作ることができます。

from fastai.vision import *
path = Path('data/bears')
learn = load_learner(path,'trained_model.pkl')

アプリでの使用時に GPU は使わずに CPU で クラス分類したい場合には次の行を実行します。

defaults.device = torch.device('cpu')

learn.predict() に画像を渡してあげればクラス分類が実行されます。

img = open_image(path/'black'/'00000022.jpg')
pred_class, pred_idx, outputs = learn.predict(img)
pred_class

Things that can go wrong

Imagenet でトレーニングされたウエイトを使用して転移学習させているので、たいていの場合少ない学習回数で良い結果が得られると思いますが、うまくいかない時には次の点をチェックします。

  • Learning rate
  • Number of epochs
Learning rate (LR) too high

LR が大きすぎると、損失が非常に大きくなってしまう事があります。発散状態なので、学習を繰り返してもよくなることが期待できません。初めからモデルを作り直して学習をやり直しましょう。

Total time: 00:13
epoch  train_loss  valid_loss  error_rate       
1      12.220007   1144188288.000000  0.765957    (00:13)
Learning rate (LR) too low

LR が小さすぎると、なかなか損失が小さくなりません。適切な値に変更しましょう。

epoch  train_loss  valid_loss  error_rate
1      1.349151    1.062807    0.609929    (00:13)
2      1.373262    1.045115    0.546099    (00:13)
3      1.346169    1.006288    0.468085    (00:13)
4      1.334486    0.978713    0.453901    (00:13)
5      1.320978    0.978108    0.446809    (00:13)
Too few epochs

単純に学習エポック数が少なすぎると、損失が小さくならないので学習を進めましょう。

epoch  train_loss  valid_loss  error_rate
1      0.602823    0.119616    0.049645    (00:14)
Too many epochs

損失を小さくするために、エポック数を大きくし過ぎると、過学習(overfitting)が生じます。学習が進むにつれてtrain_loss と valid_loss が小さくなっていきますが、overfitting の状態になると、train_loss が減少していくにもかかわらず valid_loss が悪化していく状態が生じます。

epoch  train_loss  valid_loss  error_rate
1      1.513021    1.041628    0.507326    (00:13)
2      1.290093    0.994758    0.443223    (00:09)
3      1.185764    0.936145    0.410256    (00:09)
4      1.117229    0.838402    0.322344    (00:09)
5      1.022635    0.734872    0.252747    (00:09)
6      0.951374    0.627288    0.192308    (00:10)
7      0.916111    0.558621    0.184982    (00:09)
8      0.839068    0.503755    0.177656    (00:09)
9      0.749610    0.433475    0.144689    (00:09)
10     0.678583    0.367560    0.124542    (00:09)
11     0.615280    0.327029    0.100733    (00:10)
12     0.558776    0.298989    0.095238    (00:09)
13     0.518109    0.266998    0.084249    (00:09)
14     0.476290    0.257858    0.084249    (00:09)
15     0.436865    0.227299    0.067766    (00:09)
16     0.457189    0.236593    0.078755    (00:10)
17     0.420905    0.240185    0.080586    (00:10)
18     0.395686    0.255465    0.082418    (00:09)
19     0.373232    0.263469    0.080586    (00:09)
20     0.348988    0.258300    0.080586    (00:10)
21     0.324616    0.261346    0.080586    (00:09)
22     0.311310    0.236431    0.071429    (00:09)
23     0.328342    0.245841    0.069597    (00:10)
24     0.306411    0.235111    0.064103    (00:10)
25     0.289134    0.227465    0.069597    (00:09)
26     0.284814    0.226022    0.064103    (00:09)
27     0.268398    0.222791    0.067766    (00:09)
28     0.255431    0.227751    0.073260    (00:10)
29     0.240742    0.235949    0.071429    (00:09)
30     0.227140    0.225221    0.075092    (00:09)
31     0.213877    0.214789    0.069597    (00:09)
32     0.201631    0.209382    0.062271    (00:10)
33     0.189988    0.210684    0.065934    (00:09)
34     0.181293    0.214666    0.073260    (00:09)
35     0.184095    0.222575    0.073260    (00:09)
36     0.194615    0.229198    0.076923    (00:10)
37     0.186165    0.218206    0.075092    (00:09)
38     0.176623    0.207198    0.062271    (00:10)
39     0.166854    0.207256    0.065934    (00:10)
40     0.162692    0.206044    0.062271    (00:09)

グラフで確認したい時には次のようにします。

learn.recorder.plot_losses()

まとめ

Lesson2 では、Google で検索した画像を使用してクラス分類のモデルを作成しました。ベースとして使用しているresnet34はImagenetでの学習済みのウエイトを持っているので、動物の分類系は、少ない学習量で自分用のクラス分類が可能となります。授業ではクラウド上で Jupyter Notebook を実行しているので、フォルダーの作成、画像のチェック等を Jupyter Notebook のスクリプトを使用して行っていますが、自分のPCで実行している時には、フォルダーを直接開いて、不適切な画像を整理する方が簡単かもしれません。途中で CPU で実行させる方法も出てきます。学習後にアプリ実行する時にはCPUで実行する方が汎用性があるとの事で紹介されていますが、メインメモリーはあるけれどもGPUのメモリーが少ないPCでの実行時には、この宣言を行ってCPUで学習させることもできます。学習速度の比較をする時にも使えます。CPUとGPUではデータ作り方が異なるので、defaults.device の宣言はデータを作成する前に行う必要があります。