fast.ai lesson1

はじめに

大学院の授業でfast.aiの講義を聴く機会があったので自分なりにまとめたいと思います。

また、私が学生であるため誤った解釈をする場合がありますので、気づいたら適宜修正します。

あと、講義ではjupyternoteを使うように言っていますが、私はGPUを持っていないのでGoogle Colaboratoryを使っています。

fast.aiの講義はこちらから受講できます。

Lesson1

 はじめは、AIがどのように実装されているかや、この講義の概要などを説明しています。

この7つの講義が終わるころには、分類問題や売上予測、感情分析、レコメンドなどができるようになるそうです。

f:id:taka_output:20190513221221p:plain

また、AIを学ぶ敷居が高いと感じさせる過去の要因を羅列していました。 f:id:taka_output:20190513213718p:plain

  • ブラックボックスに関しては、機械学習は解釈可能な状態になっているし、勾配や活性化関数は視覚化することが可能である。

  • データ量に関しては、転移学習を用いることで解決する。

  • 今では博士課程でなくてもAIを学ぶことは可能である。

  • 画像認識以外にも様々な用途が存在する。

  • GPUがないと絶対ダメというわけではない。ただし一部のプロジェクトを除く(今はGoogle Colaboratoryがあるので大丈夫かと)

  • 実際に知能を持ったAIを作ることに重きを置いていない。この講義では実世界の問題を解決できるようなAIに重きを置く。

と、言っています。 f:id:taka_output:20190513215242p:plain

この講義が他の大学の講義と違うことは、どのようにプログラムが動くかを知る前に、とりあえずコードを動かすことから始めることです。

それではコードを書いていきましょう。 まず、一番上のセルにこのコードを入力します。これらはpythonのコードではなく、マジックと呼ばれるものです(おまじない)。

%reload_ext autoreload
%autoreload 2
%matplotlib inline

次に、fast.aiのライブラリをインポートします。 コード上ではドットがつかないので注意!

from fastai import *
from fastai.vision import *

またこの講義では、fastaiかPytorchのどちらかのライブラリを使うことになります。Pytorchは最近人気が出たライブラリで、Tensorflowよりも高速で様々なことができます。

次に、この講義で取り扱うデータは
1. 学術的なデータ
2. kaggleのデータ
の二つを扱うことになります。
学術的なデータはとても重要で、興味深いものが多く、新たな価値を付加することができます。
今回の講義では、学術的なデータとしてpet datasetというものを用います。

また例えば、kaggleのデータセットを使った場合、自分の結果がどれほど良いものなのかわかります。もしも上位10%に入ることができたら上出来といえるでしょう。

今回使うPet datasetはとても難しく、このデータセットには37種類の犬と猫のデータが入っています。 数年前までは、犬と猫の2値分類ですらとても難しい問題でした。(大体80%くらいが最高らしい)。しかし、いまではとても簡単な問題となっている。今の技術を使えばチューニングなしでその精度を超えることが可能です。
そのため、この講義からは難しい問題に取り組むことにしています。(このように似ているものを分類することをfine-grained classification という)
それではコードに移ります。

まず、欲しいデータをダウンロードします。

path = untar_data(URLs.PETS); path

untar_data関数を使うことで、自動的にデータをダウンロードすることができ、そしてそのデータを展開することができます。 また、jupyternote book上では「;」は文の終わりを示すらしく、;pathとすることで、print(path)というコードを書かないでpathを出力することができるそうです。

また、untar_dataの中身がどのようなものなのか知りたい場合は以下のコードを実行します。

help(untar_data)

そして、ファイルの中になにがあるか確認したいときはpythonのlsを使います。Jeremy(この講義の先生)曰く、これを知るのにコードを書かせるのは不便だそうです。

path.ls()

また、この書き方でpathを指定できるらしいです。(よくわかりませんでした)

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

データを使っていくわけですが、中身どこにあるのか気になります。そこで、get_images_file()関数を使います。

fnames = get_image_files(path_img)
fnames[:5]

出力結果

[PosixPath('/root/.fastai/data/oxford-iiit-pet/images/scottish_terrier_110.jpg'),
 PosixPath('/root/.fastai/data/oxford-iiit-pet/images/Sphynx_129.jpg'),
 PosixPath('/root/.fastai/data/oxford-iiit-pet/images/scottish_terrier_114.jpg'),
 PosixPath('/root/.fastai/data/oxford-iiit-pet/images/Bengal_44.jpg'),
 PosixPath('/root/.fastai/data/oxford-iiit-pet/images/British_Shorthair_141.jpg')]

これで画像がどこにあるか確認することができました。
また、ファイルのパスを見てもらうとわかるのですが、ファイルのパス/ラベル名_番号.ファイルの形式 の形になっていることがわかります。 (機械学習においてラベルは予測するものを指し示します。)

機械学習のモデルで必要となるのは、
・画像などのデータ
・そのラベル
です。実は、fast.aiではこの作業はとても簡単に行うことが可能です。 オブジェクトImageDataBunchを使うことで、データとラベルを学習用データと検証用データに分けることができます。
まず、ファイルのパスからラベルだけを取り出すために正規表現を用います。

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

それでは、ImageDataBunchを使いましょう。

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

ここで、ImageDataBunchの引数ですが、

ImageDataBunch(画像が入っているパス, ファイル名が格納されたリスト, ファイル名からラベルを抽出するための正規表現, ds_tfmは後で話すらしい, 画像の大きさ)

となっています。画像のサイズはバラバラだとよろしくないので、一つの大きさに統一します。なぜ224かというと、一般的にこの大きさだと結果がいいからだそうです。
ImageDataBunch.from_name_reはDataBunchオブジェクトを返します。基本的にfast.aiでは、DataBunchオブジェクトを返すことになります。
DataBunchは2または3のデータセット(訓練用データ、検証用データ、テスト用データ)が含まれており、これらのデータセットそれぞれにラベルとデータがペアになって入っています。 また、コードにあるnormalize関数はデータを正規化するために使っています。

それでは、データの中身を見てみましょう。中身を見るにはshow_batchを使います。

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

f:id:taka_output:20190518213908p:plain

できるだけ早く機械学習モデルを作るために、データを見ることはとても重要なことです。データセットにノイズがかかっていたり、反転していたりなどは見ないと確認することはできません。だからデータは実際に確認することが大事になります。
それに加え、ラベルも大事になります。ラベルはコード上ではclassとして扱います。

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

DataBunchではdata.classesと入力すればすべてのラベル名を出力することができます。len(data.classes)でdataにあるラベルの総数を確認することができます。今回は37種類の犬と猫のデータセットなので出力は37になります。
また、DataBunchには常にcというプロパティが存在します。現段階ではラベル(クラス)の数と考えていいらしいです。cは回帰分類や多クラス分類では正確なクラスの数を出力できないけれど、少なくとも分類の問題ではクラスの数であることは知っていた方がいいらしいです。

Learner

それでは、学習の準備が整いました。fast.aiでは学習させるモデルのことをlearnerと呼びます。

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

この一行のコードで、畳み込みニューラルネットワーク(CNN)を構築することができました。
learnerの引数は、
* 第一引数 data:データセット

  • 第二引数 arch:層の構造

  • 第3引数 metrics: 学習結果を示すもの

となっています。
今回は指定したResNetにはResNet34とResNet50が存在します。どちらかを選ぶ必要がありますが、34の方が学習時間が短いので今回はそっちを採用しているそうです。ResNetはImageNetという画像データセットからすでに学習した重みをもっています。(つまり転移学習させる)
転移学習させることで学習時間を大幅に削減することができます。授業の後半にはどうしてResNetを使うのか質問する生徒がいますが、Stanford DAWNBenchによれば画像分類は下図のような状態です。 f:id:taka_output:20190520180747p:plain ここまですごいことになってるとは思いませんでした...

過学習

せっかくモデルを作っても、新しい画像での精度が悪ければ意味がありません。このように、訓練時のデータの精度は良く、新しいデータに対しては精度が低い状態を過学習といいます。これは検証用のデータを用いることで解決します。検証用データはモデルに学習させません。DataBunchを作るときに検証用データが生成されるのはこのためです。metricsに指定して検証用データにおける損失を確認することで、過学習しているか確認することができるのです。

学習

それでは学習を行います。一般的に学習させるにはfit関数を用いていました。しかしながら、最近の論文によればfit_one_cycle()関数の方が精度と学習速度が良いことからfast.aiではこちらの関数を使っています。fit_one_cycle()の引数にはエポック数を入れます。エポック数はモデルにデータを見せる回数です。回数が多すぎると過学習してしまうので注意。今回は4エポックで実行します。

model.fit_one_cycle(4)

結果
f:id:taka_output:20190520012813p:plain
4回目の学習で、エラー率が6%つまり、正解率が94%なのでいい結果が得られました。

Fast.aiの中身を理解するのはかなり大変です。Fast.aiを理解したいのであれば、公式ドキュメントを参照するよう推奨しています。

比較

では、いままで頻繁に使われてきたKeras(機械学習ライブラリ)よりfast.aiはどれほど優れているのでしょうか? f:id:taka_output:20190520104342p:plain 上の表によると、同じ内容でもコード量が1/6も短く、精度も高く、学習速度も速いそうです。

保存

学習はすでに終わりました。学習させた重みを保存しておくことで、再びそのモデルを使うことが可能です。モデルの保存はsave()メソッドを使って、引数にモデル名を入力すれば終わりです。

model.save(model1)

結果

先ほどの学習でどのような結果が得られたのか確認してみましょう。

interp = ClassificationInterpretation.from_learner(learn)

losses,idxs = interp.top_losses()

まず、一行目ですがfast.aiにあるClassificationInterpretationクラスのインスタンスをfrom_learnerメソッドを用いて簡単に作ることができます。
ClassificationInterpretationクラスは分類モデルがどのように解釈したのか示してくれます。具体的には混合行列や最も間違えた画像を返してくれます。 2行目のtop_losses()メソッドはモデルが最も間違えた画像を取得することができます。
それでは、実際にどの画像を間違えたのか確認します。

interp.plot_top_losses(9, figsize=(15,11))

f:id:taka_output:20190520191542p:plain 画像の上にある文字は左から順に、予測値/正解/損失/予測値である確率 を示しています。また、画像がヒートマップになっていてどこを見て間違えたのか視覚化することができます。

混合行列も表示させることができます。(多クラス分類では使わない方がよい)

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

f:id:taka_output:20190520193754p:plain 多分、引数のdpiで大きさを調整できます。

次に最も間違えたクラスは何か見てみましょう。

interp.most_confused(min_val=2)
[('american_pit_bull_terrier', 'staffordshire_bull_terrier', 9),
 ('Egyptian_Mau', 'Bengal', 5),
 ('staffordshire_bull_terrier', 'american_pit_bull_terrier', 5),
 ('yorkshire_terrier', 'havanese', 4),
 ('British_Shorthair', 'Russian_Blue', 3),
 ('american_pit_bull_terrier', 'american_bulldog', 3),
 ('english_cocker_spaniel', 'english_setter', 3),
 ('Ragdoll', 'Birman', 2),
 ('Ragdoll', 'Siamese', 2),
 ('basset_hound', 'beagle', 2),
 ('great_pyrenees', 'samoyed', 2),
 ('miniature_pinscher', 'Sphynx', 2),
 ('miniature_pinscher', 'chihuahua', 2)]

most_confused関数の引数min_valで最小で何回間違えたものを示すか指定できます。出力結果の左から順に、正解ラベル、予測値、間違えた回数となっています。

調整

fine-tuningをすることでモデルを改良しようと思います。
今までの学習では、転移学習で私たちが追加した層だけが学習を行っていました。しかし、その追加した層も含めた全体が学習できた方がいい結果が得られそうです。この講義では以下の2ステップのコードでそれを実行するようにしています。

learn.unfreeze()
learn.fit_one_cycle(1)

unfreeze関数を使うことで、層全体で学習を行うことができます。 f:id:taka_output:20190520204737p:plain しかしながら、結果は先ほどよりもエラー率が高くなってしまいました。これを理解するには中身を直感的に理解する必要があるそうです。 f:id:taka_output:20190520211838p:plain この画像はMatt Zeilerという方が書いた論文に記載されていたものです。この論文ではどのようにしてCNNの層を視覚化するかを記しています。CNNは基本的に、赤、緑、青の3色(RGB)からなるピクセル(0~255の値をとる)を最初の層で簡単な計算を行い、それが2層目に伝播され、再び計算したものが3層目へ...という感じで伝播します。
画像にあるLayer1は、伝播したピクセルの特定の重みを可視化してランダムに9つ取り出したものになります。CNNではこれをフィルターと呼びます。右側の画像は、フィルターを通して出力された画像です。 f:id:taka_output:20190521170902p:plain Layer2ではこれらのフィルターの結果から2層目の計算を行います。例えば、左側のフィルターの一番下の段の右端を見てみましょう。右側で同じ場所を見るとわかるのですが、物体の左隅をいつも見ています。つまり、このフィルターは窓枠の左側を見つけるのに適しているといえます。 f:id:taka_output:20190521173600p:plain 3層目では先ほどのものを組みあわせたものを見つけることができます。これによって2次元平面上の同じものが連続したもの(右側の左上端のもの)や、花の一部、地形などを認識できるようになりました。 f:id:taka_output:20190521174313p:plain 4層目では、3層目を組み合わせることで、物体の一部ではなく物体全体を認識することができるようになります。
5層目になると、鳥やトカゲの眼や、特定の犬の品種を認識できるようになります。

ここで、一番最初の話に戻ります。先ほど実行したコードでは、転移学習モデルのレイヤーに保存してある全ての重みを変えようとしていました。ですが、Layer1を変えたとしても、Layer1にある対角線自体は同じになります。つまり、Layer1は変える必要はないのです。私たちは犬と猫を認識させたいので、一番最後の層(先ほどのLayer5)のようにすでに犬や猫を認識している層だけを、自分たちが認識させたいデータに置き換えればいいのです。つまり、転移学習する際は最後の層だけを私たち独自の層にしてしまえばいいのです。

学習率

学習率とは簡単に言えば、一回の学習で、どれだけ学習すべきか、どれだけパラメータを更新するかなどを決めるものです。いまから学習率をグラフで見てみましょう。

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

f:id:taka_output:20190523210341p:plain
グラフを見るとわかるのですが、1e-03を超えると損失が増加しています。そこで、微調整を行うために学習率を1e-06として実行したいと思います。ですが、すべての層がこの学習率ではよろしくないです。なぜなら、最後のほうの層では、より早い学習率でも学習を行うことができるからです。学習率に幅を持たせるために以下のコードを実行します。

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

このコードではpythonのスライスで表現されています。これによって、最初の方の層では、学習率を1e-06で、最後の方の層では1e-04にすることができます。 f:id:taka_output:20190523213228p:plain
一番最初のモデルよりもいい結果を得られました!

MNISTを使った様々なラベリング方法

MNISTをダウンロードします。

path = untar_data(URLs.MNIST_SAMPLE); path

lsを使って中身を確認します。

path.ls()
[PosixPath('/root/.fastai/data/mnist_sample/labels.csv'),
 PosixPath('/root/.fastai/data/mnist_sample/valid'),
 PosixPath('/root/.fastai/data/mnist_sample/train')]

MNISTではすでに、訓練データと検証用データが分かれていることがわかります。

方法1 フォルダー名からラベリング

フォルダー名を確認してみます。

(path/'train').ls()
[PosixPath('/root/.fastai/data/mnist_sample/train/3'),
 PosixPath('/root/.fastai/data/mnist_sample/train/7')]

「3」と「7」という名前のフォルダーが存在しています。例えば「3」のフォルダーには「3」と書かれた画像のみが入っています。このようなラベリング方法はImageNetと同じであるため、ImageNet型のデータセットと呼ばれることもあります。
この形式でラベリングされているときは、from_folderでDataBunchを作成できます。

tfms = get_transforms(do_flip=False)
data = ImageDataBunch.from_folder(path, ds_tfms=tfms, size=26)

DataBunchの中身を見てみましょう。

data.show_batch(rows=3, figsize=(5,5))

f:id:taka_output:20190523221944p:plain

方法2 CSVファイルからラベリング

先ほどMNISTのデータセットを確認したときに、ラベルのCSVがありました。今回はそれを使ってみます。

df = pd.read_csv(path/'labels.csv')
df.head()

f:id:taka_output:20190523222534p:plain
先ほどと違い、ファイルごとのラベルは0か1で表現されます。これはつまり、7であるか、ないかということを表しています。 このようなラベルの時、from_csvを使います。

data = ImageDataBunch.from_csv(path, ds_tfms=tfms, size=28)

DataBunchとラベルを見てみましょう。

data.show_batch(rows=3, figsize=(5,5))
data.classes

f:id:taka_output:20190523223552p:plain

方法3 正規表現でラベリング

先ほどのCSVファイルから正規表現を使ってラベルだけ取り出そうと思います。 まず、先ほどのCSVファイルからファイル名が格納されたリストを作ります。

fn_paths = [path/name for name in df['name']]; fn_paths[:2]

出力

[PosixPath('/root/.fastai/data/mnist_sample/train/3/7463.png'),
 PosixPath('/root/.fastai/data/mnist_sample/train/3/21102.png')]

from_name_reを使います。

pat = r"/(\d)/\d+\.png$"
data = ImageDataBunch.from_name_re(path, fn_paths, pat=pat, ds_tfms=tfms, size=24)
data.classes

出力

['3', '7']

方法4 ラベル抽出の関数を作る

自分でラベル抽出の関数を作るってラベリングをしようと思います。やり方は簡単で、from_name_func()メソッドをDataBunchに適用すればいいだけです。from_name_funcメソッドはfrom_name_reと出力は同じですが、引数に任意の関数を指定することができます。

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

出力

['3', '7']

方法5 ラベルの配列を作ってラベリング

上記よりも柔軟なコードを求められる場合は、ラベルの入ったリストを作ります。リストを作ったら、from_listsメソッドの引数に代入すれば終わりです。

labels = [('3' if '/3/' in str(x) else '7') for x in fn_paths]
labels[:5]

出力

['3', '3', '3', '3', '3']

from_listsを使います。

data = ImageDataBunch.from_lists(path, fn_paths, labels=labels, ds_tfms=tfms, size=24)
data.classes

出力

['3', '7']

以上で、Lesson1は終わりです。

参考

こちらのGithubに講義の英訳がまとめてありました。ありがとうございます。 github.com