fast.ai で deep learning を勉強しよう(2)Lesson 1: Image classification

Practical Deep Learning for Coders, v3 のサイトで Deep Learning を勉強しましょう。 いきなり実践ですから、Deep Learning について用語とイメージぐらいは掴んでおいてから取り組んだ方が良いと思います。用語の意味とか内容に関して分からなくても、説明が後から出てくる事も多いのがこの講義の特徴なので、気にしない事も大切です。

lesson1-pets.jpynb

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

Lesson1では、詳しい説明なしで話が進んで行きます。基本的な講義の進め方として、まずはやってみる。その後少しずつ深く理解していくという事ですので、一つずつに拘らずに流していくという事が大切です。何度も同じことをやりながら、少しずつ詳しくというのが基本方針となっています。細かい事は fast.ai のライブラリーで隠されているので、中身を理解する事は出来ないと思いますが、こんな感じで、こんな簡単にできるんだという事が分かればいいんだと思います。 実行結果はあまりここには載せていないので、自身で実行しながら読むか、Hiromi's Note を見ながら読んでもらえれば良いかと思います。

準備

まずはおまじないから。 モジュールを自動でリロードするように設定しておきます。モジュールを書き換えながら実行する時に役に立ちます。 また jupyter notebook 内に matplotlibのグラフを表示するように設定しておきます。

%reload_ext autoreload
%autoreload 2
%matplotlib inline

fast.ai のライブラリーを読み込みます。

from fastai.vision import *
from fastai.metrics import error_rate

次にパラメータ bs を設定。 bsはバッチサイズで、一度に計算する量となります。 GPUを使う時にはグラフィックボードのメモリー、使わない時にはメインメモリーで一度に確保する必要がある量となります。GPUの時にはメモリーの使用量が上限に達するとエラーで停止します。CPUのみで実行している時には、メモリースワップが生じてHDDが使われるようになるので、極端に動作が遅くなります。キー入力等もほとんど受け付けなくなって、ハングアップしたのではないかと思われるほど遅くなると思うので、そうなったら、さっさと停止した方が良いでしょう。 メモリーが不足している時には bs の値を小さくして実行します。

bs = 64
#bs = 16   
# uncomment this line if you run out of memory even after clicking Kernel->Restart

Looking at the data

データセットは http://www.robots.ox.ac.uk/~vgg/data/pets/ を使用します。猫12種類、犬25種類で計37種類のカテゴリーから成ります。

fast.aiのファンクションの使い方を見るには次のように help を使います。

help(untar_data)

データセットは fast.ai に登録されているので untar_data() を使って簡単にダウンロードできます。

https://github.com/fastai/fastai/blob/master/fastai/datasets.py

今回のデータセットをダウンロードします。データセットがダウンロードされた場所は path です。

path = untar_data(URLs.PETS); path

ls() で path の下のフォルダーを確認できます。

path.ls()

このフォルダーを簡単に指定できるようにパラメータ化します。

path_anno = path/'annotations'
path_img = path/'images'

path_img にある画像データファイル名を fnames に読み出して、最初の5個を表示してみます。

fnames = get_image_files(path_img)
fnames[:5]

乱数の初期化とファイル名からラベルを抜き出すための正規表現を pat で定義します。Linux環境とWindows環境では表現が少し異なるので、windows環境の時には次のように書き換えて使います。

np.random.seed(2)
pat = r'[/\\]([^/\\]+)_\d+.jpg$'
#pat = r'/([^/]+)_\d+.jpg$'

画像サイズはすべて同じでないといけないので、この分野で標準的な 224x224 にリサイズし、学習用のデータ、検証用のデータ、オプショナルなテスト用のデータをひとまとめにした data bunch を作ります。データは正規化されます。from_name_re は、ファイルネームから正規表現でラベルを作ります。

正規化は画素の明るさを平均ゼロ、標準偏差1に変換します。

data = ImageDataBunch.from_name_re(path_img, fnames, 
            pat, ds_tfms=get_transforms(), 
         size=224, bs=bs).normalize(imagenet_stats)

data の画像を resize、cropping、padding して 3x3=9 個表示してみます。

data.show_batch(rows=3, figsize=(7,6))

処理する画像サイズを 224x224 にしているのは、最終層での画像サイズが 7x7 なので、7*25=224 になるので 256 よりも 224 の方が都合がいいからです。

data のクラスを表示してみます。'american_bulldog' とか37のクラス分類名が表示されます。

print(data.classes)
len(data.classes),data.c

Training: resnet34

学習用のデータを集めた DataBunch と 学習を進める Learner の二つで機械学習が可能となります。画像用の DataBounch のサブクラスが 上記で出てきた ImageDataBunch です。CNN の Learner は次のように、DataBounch とモデルを指定して作成する事ができます。どんなモデルが fast.ai で使えるかは models. の後で Tab キーを押すと候補が出てきます。

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

ResNet は 2015年にマイクロソフトリサーチから提案されたモデルで、深い階層でも学習が可能となるモデルです。 modelsには resnet18, resnet34, resnet50, resnet101, resnet152 が用意されています。 数字が Deep Learning での階層数となります。階層が深ければ深いほど学習にかかる時間が長くなるので、まずは数字の小さいものを使いましょう。

このモデルでは、画像を入力として、出力は37のカテゴリーとなります。 下記でモデルの構成が表示されます。

learn.model

Transfer learning

日本語では転移学習と言います。自分の課題と似通っていて既に学習されたモデルとウエイトを使用して、最終部分のみ自分用に作り変えて学習させる事を転移学習と言います。犬猫分類でいえば、前半のCNNネットワークの部分は犬と猫の画像的特徴を抽出する部分であり、それは完成されているものとして、その特徴の組み合わせから何かを作り出すというようなイメージになります。CNNの部分の学習を省略できるので、早く結果を出すことができます。 ImageNet の動物を 1000 のカテゴリーに分ける学習済みモデルのCNN部分を使って37カテゴリー分類を行うと、非常に短い時間で良い結果を得ることができます。

Overfitting

パラメータ数に対して学習データが少ないと、学習データでは結果が良いけれどもそれ以外のデータに対してはあまり結果がよくないというような状態になることがあります。モデルの能力が高いので、学習データのどのデータかを当てるモデルになってしまったという事です。これをオーバーフィッティングと言いますが、そうなっていないかどうかを判断するために、検証セットを使用します。検証セットは学習に使用していないデータで、学習データの損失と検証データの損失を比較することで、一般的なデータに対する汎化性を評価する事ができます。

fit で学習をします。学習用データを1回ごとの学習数(バッチ)に分割して学習し、全学習用データをエポック数回繰り返して学習します。下記の4がエポック数となります。

learn.fit_one_cycle(4)

学習中は次のような表示が出て、進行状態を確認できます。

f:id:feynman911:20190416214340j:plain

学習結果を保存することで、次回はそれを読み込んで継続する事ができます。

learn.save('stage-1')

Results

37カテゴリーに分類されたものを分かりやすい名前に変換するのに、次のように ClassificationInterpretation を使います。interp.plot_top_losses で損失が大きなものを表示する事ができます。その間違いがもっともらいしい物か、間違えては困るものなのかを判断できます。

interp = ClassificationInterpretation.from_learner(learn)
losses,idxs = interp.top_losses()
len(data.valid_ds)==len(losses)==len(idxs)
interp.plot_top_losses(9, figsize=(15,11))

f:id:feynman911:20190416214440j:plain

画像の上に書かれているのは、順番に、予測名, 実際, 損失, 予測クラスの確率となります。

doc() でドキュメントを見ることが出来ます。

doc(interp.plot_top_losses)

f:id:feynman911:20190416214511j:plain

Sourceをクリックするとソースコードに飛びます。Show in docs をクリックするとドキュメントへ飛びます。

plot_confusion_matrix()で混同行列 (Confusion matrix)を表示する事ができます。

横軸が予測した名称で縦軸が実際の名称となるので、一致している対角線上の数が正解した数となります。このように表示すると、何と何が間違い易いのかを簡単にみることができます。

interp.plot_confusion_matrix(figsize=(12,12), dpi=60)

f:id:feynman911:20190416214543j:plain

下記の記法で、Confusion matrix の対角ではない=間違っているところを、間違いやすい順に表示する事ができます。

interp.most_confused(min_val=2)

Unfreezing, fine-tuning, and learning rates

もう少しいい結果を得るためにファインチューニングを行います。ここまではCNNの部分を ImageNet の学習結果をそのまま使って学習してますが、CNNの部分も学習させるように unfreeze して1エポックだけ学習させてみます。

learn.unfreeze()
learn.fit_one_cycle(1)

このように、まずは時間短縮のために学習済みのモデルとウエイトを使い、基本的な特徴抽出後にクラス分類するための全結合層(Model の flatten 以降)を学習させ、その後、CNN層も含めて学習(ファインチューニング)させるという手順が通常取られます。

保存しておいたデータを読み出すには、次のようにします。

learn.load('stage-1');

lr_find とは、Learning rate finder と呼ばれて、学習時のバックプロパゲーション時にどのぐらい修正するかの係数である Learning rate をどれくらいの値にするべきかを探索する方法です。

learn.lr_find()

実行すると下記のようなグラフを描くことができます。

learn.recorder.plot()

f:id:feynman911:20190416214634j:plain

Learning rate はバックプロパゲーションニューラルネットのウエイトをどの程度修正するかの係数で、小さすぎるとなかなか修正されないので学習が進むまでの学習回数が増えて時間がかかります。大きすぎると収束しなくなってしまうので適切な値を使う必要があります。lr_find では Learning rate を変えながら損失の変化を調べて、どのくらいの値が適切なのかを探索します。上図では、1e-4を超えたあたりから急激にLossが増えるので、それ以下に抑えなくてはいけないことが分かります。小さいとそれだけ収束が遅くなるので、適当な範囲を設定して学習させることになります。以下では 1e-6 から 1e-4 の間で Learning rate を変化させて学習する事を指示しています。 以下では、unfreezeして、Learning rate の最大値 max_lr をニューラルネットの入力層側(CNN層)で1e-6、出力層側層(全接続層)では1e-4として、 中間層ではその間を等間隔で設定して、One Cycle Policy ( Learning rate を小さな値から max_lr まで変化させ、再び小さくする方法) で Learning rate を変化させながら学習しています。 (slice の 意味の間違いを指摘してくださった方がいましたので、訂正します)
(参照)basic_train | fastai
Learning rate をどの様に変化させて学習させるかにも色々なやり方がありますが、一般的には学習が進むにつれて徐々に小さな値にしていく方法がとられます。ここでは、fast.aiのライブラリーで隠されています。

learn.unfreeze()
learn.fit_one_cycle(2, max_lr=slice(1e-6,1e-4))

Training: resnet50

もう少し階層が深い resnet50 を使うと下記の様になります。モデルが大きくなるので、それだけメモリーが必要になります。bs の大きさには注意しましょう。

data = ImageDataBunch.from_name_re(path_img,
    fnames, pat, ds_tfms=get_transforms(),
            size=299, bs=bs//3).normalize(imagenet_stats)
learn = create_cnn(data, models.resnet50, metrics=error_rate)
learn.lr_find()
learn.recorder.plot()
learn.fit_one_cycle(8)
learn.save('stage-1-50')
learn.unfreeze()
learn.fit_one_cycle(3, max_lr=slice(1e-6,1e-4))
learn.load('stage-1-50');
interp = ClassificationInterpretation.from_learner(learn)
interp.most_confused(min_val=2)

Other data formats

他のデータの例として MNIST の場合には次のようになります。MNIST は手書き数字の認識です。結果は載せないので、自身で実行してみてください。

path = untar_data(URLs.MNIST_SAMPLE); path
tfms = get_transforms(do_flip=False)
data = ImageDataBunch.from_folder(path, ds_tfms=tfms, size=26)
data.show_batch(rows=3, figsize=(5,5))
learn = create_cnn(data, models.resnet18, metrics=accuracy)
learn.fit(2)
df = pd.read_csv(path/'labels.csv')
df.head()
#error occure coused by panda & spacy
#This will be resolved in the next update of pandas 
#and we'll also be issuing a workaround for spaCy. 
#If you're interested, see here for some background 
#on the problem: pandas-dev/pandas#25036

ここでエラーが出るみたいですが、pandas の df.head() 部での表示のエラーの様ですので結果には影響ないみたいです。

data = ImageDataBunch.from_csv(path, ds_tfms=tfms, size=28)
data.show_batch(rows=3, figsize=(5,5))
data.classes
data = ImageDataBunch.from_df(path, df, ds_tfms=tfms, size=24)
data.classes
fn_paths = [path/name for name in df['name']]; fn_paths[:2]

ここは、LinuxWindows の違いがあるので、windows では次のような修正が必要です。

#pat = r"/(\d)/\d+\.png$"
pat = r"[/\\](\d)[/\\]\d+\.png$"
data = ImageDataBunch.from_name_re(path, fn_paths,
             pat=pat, ds_tfms=tfms, size=24)
data.classes
data = ImageDataBunch.from_name_func(path, fn_paths, ds_tfms=tfms, 
      size=24, label_func = lambda x: '3' if '/3/' in str(x) else '7')
data.classes
labels = [('3' if '/3/' in str(x) else '7') for x in fn_paths]
labels[:5]
data = ImageDataBunch.from_lists(path, fn_paths, labels=labels,
             ds_tfms=tfms, size=24)
data.classes

まとめ

ビデオでは、この Notebook の話は半分ぐらいで、他に最新のトピックスとか卒業生の話とか、自分が工夫した話とか質疑応答があります。特に Jeremy Howard が自分で考えたノウハウ的な部分は、実戦的で非常に役に立つと思います。野球ができるようになるためには、理屈よりもまず素振りだという事で、とにかくfast.ai を自分の課題で使ってみて、使い慣れてから、なかはどうなってるの、このパラメータの意味はとかを学ぼうというのが講義全体の流れになっています。Lesson1でも、何をやっているのかよくわからない、けれども同じような画像を用意して、同じようにやれば画像分類ができてしまいます。Deep Learning 自体、中身が正しいかどうかではなく、結果がよければすべてよしの世界なので、論文を書くのが目的でなければ、うまく結果を出すためのノウハウが 大切だと思います。