fastai動かしてみたLesson3①

公開日:2019-04-23
最終更新:2019-04-27

今回はLesson3 https://course.fast.ai/videos/?lesson=3

notebookはこちら https://github.com/fastai/course-v3/tree/master/nbs/dl1

冒頭はFastaiを使ったWebアプリの紹介
手話や表情をリアルタイムで読み込んで認識するプログラムとかがおもしろいと筆者は感じました
そのほかにも沢山紹介していた

複数ラベルを持つ画像分類

今回は衛星から見たの地上の画像を分類していく

画像はこんな感じ
画像の上にあるのがラベルで、前回と異なり複数のラベルを持っている

これらのデータはKaggleというコンペティションサイトから簡単に得る事が出来る
ローカルでプログラムを書いても良いが、ここではGoogle Colaboratory上で動かすものとすると、

! pip install kaggle --upgrade  

を行うとpipでKaggleをインストールできる

そしてKaggleのPlanet competitionに行って規約などに同意をし、参加する

多分ここ https://www.kaggle.com/c/planet-understanding-the-amazon-from-space

! mkdirr -p ~/.kaggle/  
! mv kaggle.json ~/.kaggle/  
path = Config.data_path()/'planet'  
path.mkdir(exist_ok=True)  
path  

PosixPath('/home/jhoward/.fastai/data/planet')

 ! kaggle competitions download -c planet-understanding-the-amazon-from-space -f train-jpg.tar.7z -p {path}    

 ! kaggle competitions download -c planet-understanding-the-amazon-from-space -f train_v2.csv -p {path}    
 ! unzip -q -n {path}/train_v2.csv.zip -d {path}

これを行うとダウンロードして解凍してくれるはず(筆者は動かしてないです)
まぁうまくいかなかったら直接コンペのページに行ってデータをダウンロードしてきてください

複数ラベルの分類

データをダウンロードすると、画像ごとのラベルが示されてるcsvファイルがあるので、pandasでcsvファイルを読み込んで中身を見てみる。

import pandas as pd  

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

こんな感じで画像ファイル名とそのラベルが示される

ここで前回と同様ImageDataBunchを使っても良いのだが、data block APIを使った方がより詳細にDataBunchを作れる

tfms = get_transforms(flip_vert=True, max_lighting=0.1, max_zoom=1.05, max_warp=0.)  

np.random.seed(42)  

src = (ImageFileList.from_folder(path)              
       .label_from_csv('train_v2.csv',sep=' ',folder='train-jpg',suffix='.jpg')  
       .random_split_by_pct(0.2))  
data = (src.datasets()  
        .transform(tfms, size=128)  
        .databunch().normalize(imagenet_stats))  

tfmsで画像データをどう変形するかを指定
ここでは角度や明るさ、拡大させたりしている
warpとは

ここで、Pytorchの解説

PytorchのDatasetはこんな感じの中身である

_何か_ の様な物を"dunder"と呼ぶらしい
dunder getitem :この方法はもしあるオブジェクトをoと呼ぶとすると、それらはインデックス付けする事が出来る 例えばo[3]とか
dender len:len(o)を使うと長さが返ってくる

つまりDataset[3]とかlen(Dataset)を使えるってこと

Fastaiは多くのDatasetのsubclassを持っている
例えばimage classification datasetsだと、_getitem_はイメージとラベルを返す

また、PytorchはDataLoaderというものも持っている
DataLoaderはdatasetをその構造の中に入れるものらしい
アイテムをランダムで取ってきたり、バッチを作成したり、GPUに渡したり、モデルに渡したりするために必要らしい

Fastaiに戻ってDataBunch

DataBunchはtrain_dlやvalid_dlをまとめたものである

tfms = get_transforms(flip_vert=True, max_lighting=0.1, max_zoom=1.05, max_warp=0.)  

np.random.seed(42)  

src = (ImageFileList.from_folder(path)              
       .label_from_csv('train_v2.csv',sep=' ',folder='train-jpg',suffix='.jpg')  
       .random_split_by_pct(0.2))  
data = (src.datasets()  
        .transform(tfms, size=128)  
        .databunch().normalize(imagenet_stats))  

ImageFileList.from_folder()でどこにデータがあるのか
label_from_csv()でどこにラベルがあるのか
random_split_by_pct(0.2)でデータの2割を評価データにする
datasets()でそれらのデータをPytorchのDatasetの形にする
transform()でデータを適当に加工し
databunch()でDataLoaderとDataBunchを作成する

MNISTの手書き文字でDataBunchを作ってみよう

MNISTとは手書きの数字のデータセットの事である
まず初めにデータを取ってきてtransformを定義しておく

path = untar_data(URLs.MNIST_TINY)  

tfms = get_transforms(do_flip=False)  
path.ls()  

pathのディレクトリにはこんなのが入ってる

[PosixPath('/home/jhoward/.fastai/data/mnist_tiny/valid'),
PosixPath('/home/jhoward/.fastai/data/mnist_tiny/models'),
PosixPath('/home/jhoward/.fastai/data/mnist_tiny/train'),
PosixPath('/home/jhoward/.fastai/data/mnist_tiny/test'),
PosixPath('/home/jhoward/.fastai/data/mnist_tiny/labels.csv')]

(path/'train').ls()  

pathのtrainにはこんなのが入ってる

[PosixPath('/home/jhoward/.fastai/data/mnist_tiny/train/3'),
PosixPath('/home/jhoward/.fastai/data/mnist_tiny/train/7')]

DataBunchを作成

data = (ImageFileList.from_folder(path)  #Where to find the data? -> in path and its subfolders  

        .label_from_folder()             #How to label? -> depending on the folder of the filenames  
        .split_by_folder()               #How to split in train/valid? -> use the folders  
        .add_test_folder()               #Optionally add a test set  
        .datasets()                      #How to convert to datasets?  
        .transform(tfms, size=224)       #Data augmentation? -> use tfms with a size of 224  
        .databunch())                    #Finally? -> use the defaults for conversion to ImageDataBunch  

先ほどは無かったが、add_test_folder()でテストデータも加えることが出来る

data.train_ds[0]  

(Image (3, 224, 224), 0)

訓練データの0番目には、画像が入っており、3次元で224×224の大きさのものがあるとわかる
ちなみにこれはDataLoaderではない

他のデータも見てみよう

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

あとはこれにcnn_learner()でモデルを作ってfitなりfit_one_cycleを使えばヨシ

衛星画像でDataBunchを作ってみよう

話を戻して衛星画像でDatabunchを作ってみよう
とはいえさっきの物とほとんど変わらないのだが

planet = untar_data(URLs.PLANET_TINY)  
planet_tfms = get_transforms(flip_vert=True, max_lighting=0.1, max_zoom=1.05, max_warp=0.)  
data = ImageDataBunch.from_csv(planet, folder='train', size=128, suffix='.jpg', sep = ' ', ds_tfms=planet_tfms)  

data = (ImageFileList.from_folder(planet)              
        #Where to find the data? -> in planet and its subfolders  
        .label_from_csv('labels.csv', sep=' ', folder='train', suffix='.jpg')    
        #How to label? -> use the csv file labels.csv in path,  
        #add .jpg to the names and take them in the folder train  
        .random_split_by_pct()                       
        #How to split in train/valid? -> randomly with the default 20% in valid  
        .datasets()  
        #How to convert to datasets? -> use ImageMultiDataset  
        .transform(planet_tfms, size=128)               
        #Data augmentation? -> use tfms with a size of 128  
        .databunch())                            
        #Finally? -> use the defaults for conversion to databunch  

.label_from_csv('labels.csv', sep=' ', folder='train', suffix='.jpg')
これは先ほどcsvの中身を見た通り、tagsにあるラベルは' 'で区切られているのでsepに' 'を指定している

CamVidを使ったDataBunchを作ってみよう

CamVidというデータセットを使ってみる

今までは画像1つごとに分類を行っていたが、今度は右図の様にピクセルごとに分類していく

camvid = untar_data(URLs.CAMVID_TINY)  

path_lbl = camvid/'labels'  
path_img = camvid/'images'  
codes = np.loadtxt(camvid/'codes.txt', dtype=str); codes  

array(['Animal', 'Archway', 'Bicyclist', 'Bridge', 'Building', 'Car', 'CartLuggagePram', 'Child', 'Column_Pole',
'Fence', 'LaneMkgsDriv', 'LaneMkgsNonDriv', 'Misc_Text', 'MotorcycleScooter', 'OtherMoving', 'ParkingBlock',
'Pedestrian', 'Road', 'RoadShoulder', 'Sidewalk', 'SignSymbol', 'Sky', 'SUVPickupTruck', 'TrafficCone',
'TrafficLight', 'Train', 'Tree', 'Truck_Bus', 'Tunnel', 'VegetationMisc', 'Void', 'Wall'], dtype='<U17')

データをダウンロードしてラベルと画像のパスを指定する
codecsにはラベル名が入っている

get_y_fn = lambda x: path_lbl/f'{x.stem}_P{x.suffix}'  

ここで今回は1画像内のピクセルごとにラベルを付けていくのでこのように関数を作っておく
つまり、あるピクセルがどのラベルであるかを教えてくれる

data = (ImageFileList.from_folder(path_img)                #Where are the input files? -> in path_img  

        .label_from_func(get_y_fn)                         #How to label? -> use get_y_fn  
        .random_split_by_pct()                             #How to split between train and valid? -> randomly  
        .datasets(SegmentationDataset, classes=codes)      #How to create a dataset? -> use SegmentationDataset  
        .transform(get_transforms(), size=96, tfm_y=True)  #Data aug -> Use standard tfms with tfm_y=True  
        .databunch(bs=64))                                 #Lastly convert in a databunch.  
data.show_batch(rows=2, figsize=(5,5))  

COCO形式の物体検出

物体検出をCOCO形式のjsonファイルを使って行う

(以前筆者は自作データを作った時にはここを参考にしました
http://www.immersivelimit.com/tutorials/create-coco-annotations-from-scratch/#coco-dataset-format)

coco = untar_data(URLs.COCO_TINY)  

images, lbl_bbox = get_annotations(coco/'train.json')  
img2bbox = {img:bb for img, bb in zip(images, lbl_bbox)}  
get_y_func = lambda o:img2bbox[o.name]  
data = (ImageFileList.from_folder(coco)  
        #Where are the images? -> in coco  
        .label_from_func(get_y_func)                      
        #How to find the labels? -> use get_y_func  
        .random_split_by_pct()                            
        #How to split in train/valid? -> randomly with the default 20% in valid  
        .datasets(ObjectDetectDataset)                    
        #How to create datasets? -> with ObjectDetectDataset  
        #Data augmentation? -> Standard transforms with tfm_y=True  
        .databunch(bs=16, collate_fn=bb_pad_collate))     
        #Finally we convert to a DataBunch and we use bb_pad_collate

get_y_funcはimg2bboxの中からファイル名が一致する物の正解データ(四角い枠 bboxの位置情報)を持ってくる関数

この後衛星写真を使った画像分類をfit_one_cycleする事で、数行書くだけでKaggle Competitionの上位10位のスコアを短時間で出す事が出来た

どうやって良い学習率を見つけるのか?

databunchを作り、cnn_learner()でlearnを定義した後、unfreezeを行っていないなら簡単に見つけ出す事が出来る

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

このグラフの坂が急に下り始めるところを学習率にすればよい
谷底にするとよい物は出てこない

unfreeze()したものは以下

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

この場合は、坂が急に上がり始める所よりも0.1位小さい物を学習率にするとよい
また、

learn.fit_one_cycle(5, slice(1e-5, lr/5))  

のようにsliceした方がいいかもしれない

Segmentationの例:CamVid

画像例として、左図がイメージデータ、右図がラベルデータになっている

とりあえずまずimportとかをしておく

%reload_ext autoreload  
%autoreload 2  
%matplotlib inline  
from fastai import *  
from fastai.vision import *  

データをダウンロードしてpathを指定しておく

path = untar_data(URLs.CAMVID)  
path.ls()  

[PosixPath('/home/ubuntu/course-v3/nbs/dl1/data/camvid/images'),
PosixPath('/home/ubuntu/course-v3/nbs/dl1/data/camvid/codes.txt'),
PosixPath('/home/ubuntu/course-v3/nbs/dl1/data/camvid/valid.txt'),
PosixPath('/home/ubuntu/course-v3/nbs/dl1/data/camvid/labels')]

path_lbl = path/'labels'  
path_img = path/'images'  

データの中身はこのようにして見れる

fnames = get_image_files(path_img)  
fnames[:3]  

[PosixPath('/home/ubuntu/course-v3/nbs/dl1/data/camvid/images/0016E5_08370.png'),
PosixPath('/home/ubuntu/course-v3/nbs/dl1/data/camvid/images/Seq05VD_f04110.png'),
PosixPath('/home/ubuntu/course-v3/nbs/dl1/data/camvid/images/0001TP_010170.png')]

lbl_names = get_image_files(path_lbl)  
lbl_names[:3]  

[PosixPath('/home/ubuntu/course-v3/nbs/dl1/data/camvid/labels/0016E5_01890_P.png'),
PosixPath('/home/ubuntu/course-v3/nbs/dl1/data/camvid/labels/Seq05VD_f00330_P.png'),
PosixPath('/home/ubuntu/course-v3/nbs/dl1/data/camvid/labels/Seq05VD_f01140_P.png')]

ラベルを読み込むための関数を用意しておく

get_y_fn = lambda x: path_lbl/f'{x.stem}_P{x.suffix}'  

マスクデータを見るときはこんな感じ

mask = open_mask(get_y_fn(img_f))  
mask.show(figsize=(5,5), alpha=1)  

ラベル名をtxtから取得しておく

codes = np.loadtxt(camvid/'codes.txt', dtype=str); codes  

array(['Animal', 'Archway', 'Bicyclist', 'Bridge', 'Building', 'Car', 'CartLuggagePram', 'Child', 'Column_Pole',
'Fence', 'LaneMkgsDriv', 'LaneMkgsNonDriv', 'Misc_Text', 'MotorcycleScooter', 'OtherMoving', 'ParkingBlock',
'Pedestrian', 'Road', 'RoadShoulder', 'Sidewalk', 'SignSymbol', 'Sky', 'SUVPickupTruck', 'TrafficCone',
'TrafficLight', 'Train', 'Tree', 'Truck_Bus', 'Tunnel', 'VegetationMisc', 'Void', 'Wall'], dtype='<U17')

DataBunchを作る

size = src_size//2  
bs=8  

src = (SegmentationItemList.from_folder(path_img)  
       .split_by_fname_file('../valid.txt')  
       .label_from_func(get_y_fn, classes=codes))  
data = (src.transform(get_transforms(), size=size, tfm_y=True)  
        .databunch(bs=bs)  
        .normalize(imagenet_stats))  
data.show_batch(rows=2, figsize=(5,5))  

databunchを作ることができたので、Trainingをする
初めにlearnerを作り、lr_findで最適な学習率を探す

name2id = {v:k for k,v in enumerate(codes)}  
void_code = name2id['Void']  

def acc_camvid(input, target):  
    target = target.squeeze(1)  
    mask = target != void_code  
    return (input.argmax(dim=1)[mask]==target[mask]).float().mean()  

metrics=acc_camvid  

wd=1e-2  

learn = unet_learner(data, models.resnet34, metrics=metrics, wd=wd, bottle=True)  

lr_find(learn)  

learn.recorder.plot()  


これを見ると、1e-2のあたりがよさそうである
fitさせたらモデルを保存しておく

lr=1e-2  
learn.fit_one_cycle(10, slice(lr))  
learn.save("stage-1")  

fitさせたのでunfreezeでパラメータを可変にしてみる

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


急に坂が上がり始める1e-4よりも0.1小さい1e-5のあたりがよさそう

lrs = slice(1e-5,lr/5)  
learn.fit_one_cycle(12, lrs)  

Total time: 03:36
epoch train_loss valid_loss acc_camvid
1 0.399582 0.338697 0.901930 (00:18)
2 0.406091 0.351272 0.897183 (00:18)
3 0.415589 0.357046 0.894615 (00:17)
4 0.407372 0.337691 0.904101 (00:18)
5 0.402764 0.340527 0.900326 (00:17)
6 0.381159 0.317680 0.910552 (00:18)
7 0.368179 0.312087 0.910121 (00:18)
8 0.358906 0.310293 0.911405 (00:18)
9 0.343944 0.299595 0.912654 (00:18)
10 0.332852 0.305770 0.911666 (00:18)
11 0.325537 0.294337 0.916766 (00:18)
12 0.320488 0.295004 0.916064 (00:18)

結果91.6%の精度を得ることができた。

小ネタ

画像サイズを64x64,128x128,256x256のように小さいものから大きくしていくのは意味があり、より精度がいいものをえられるとかなんとか

train loss > valid loss の時何をすればよいのか?
まずはepoch数を増やしてもっと学習させてみる
あるいはlearning rateを少し小さくする
それでも改善しない場合は、weight decay, dropout, data augumentationを使うと改善する
これらの技術は後の講義で解説される

参考:
https://github.com/hiromis/notes/blob/master/Lesson3.md

記事が少しでもいいなと思ったらクラップを送ってみよう!
0
+1
大学生の書きなぐりブログ 間違ってる事も書いてるので自己責任で勉強しましょう

よく一緒に読まれている記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる

技術ブログをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

技術ブログを開設する

Qrunchでアウトプットをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

Markdownで書ける

ログ機能でアウトプットを加速

デザインのカスタマイズが可能

技術ブログ開設

ここから先はアカウント(ブログ)開設が必要です

英数字4文字以上
.qrunch.io
英数字6文字以上
ログインする