Re:ゼロから始めるML生活

どちらかといえばエミリア派です

ResNetのPyTorch学習済みモデルをfine tuneして使うときのメモ

f:id:nogawanogawa:20181029135436p:plain

普段画像データを使って機械学習をすることがあまりないんですが、色々あって最近ちょくちょく触っています。

そんなわけで、画像認識で使うニューラルネットワークの勉強をしていて、最近では画像認識のベースラインとして使用されることもあるResNetについて勉強してみたので、そのメモです。

ResNet is なに?

2015年にMicrosoftから提案されたImage Classsificationに関する非常に有名なCNNの手法です。

arxiv.org

特徴

特徴をざっくり言うと、

  • 従来(当時)から、CNNのネットワークの層を増加させることでモデルの精度が向上する傾向は確認されていた
  • しかし、ネットワークの層を増やすにつれて学習が不安定になり、層を増やしすぎると性能が逆に低下することもわかっていた
  • そこで、従来のCNNのモデルとは異なり、Residual(残差)に関して学習を行うようにネットワークの構造を変形することで、層を増やしても学習が安定化させた
  • すると、従来では考えられないくらいの層を持つネットワークを構成しても学習が安定し、精度が飛躍的に向上した

って感じです。

6年も前の有名論文だと、色んな方々が論文の解説記事を書いてくださっているので、論文の詳細な解説は下記の記事などをご参照ください。

deepage.net

qiita.com

動かしてみる

さて、本題です。ざっくり理屈はわかったところで、実際に使ってみます。 とは言っても、ResNetの層が深いと、当然学習時間もとんでもないことになるので、今回は学習済みのモデルを使用して、そちらをfine tuningして使うときのメモを残していきます。

データセット

今回はThe Oxford-IIIT Pet Datasetを使いたいと思います。

www.robots.ox.ac.uk

wget https://www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz
tar zxvf images.tar.gz

環境

今回は簡単のため、あとGPUを使いたいため、Google Colabを使用したいと思います。

コードの確認

ちょっとだけ、備忘のためにコードの説明を付けておきます。

Datasetクラス

MyDatasetクラスを作ります。 lenメソッドでデータの数を、getitemのメソッドでインデックスに応じたデータを返すように記述します。 その他の事前の処理なんかはコンストラクタに記述しています。

今回は、画像のサイズがまちまちだったので、transforms.Composeの中で、224×224のサイズに強引に押し込めてます。 (サイズは適当に決めました)

class MyDataSet(Dataset):
    def __init__(self):
        
        l = glob.glob('images/*.jpg')
        self.train_df = pd.DataFrame()
        self.images = []
        self.labels = []
        self.le = preprocessing.LabelEncoder()

        for path in l:
            self.images.append(path)
            self.labels.append(re.split('[/_.]', path)[1])

        self.le.fit(self.labels)
        self.labels_id = self.le.transform(self.labels)
        self.transform = transforms.Compose([transforms.Resize((224, 224)), transforms.ToTensor()])

    def __len__(self):
        return len(self.images)
    
    def __getitem__(self, idx):
        image = Image.open(self.images[idx])
        image = image.convert('RGB')
        label = self.labels_id[idx]
        return self.transform(image), int(label)

データセットを学習・テストで分離

余り覚えてなかったんですが、Datasetを作ってから、データの長さを取得して、学習:テストの割合を決めて別々のデータローダーに突っ込んでます。

dataset = MyDataSet()

n_samples = len(dataset)
train_size = int(len(dataset) * 0.7)
val_size = n_samples - train_size

train_dataset, val_dataset = torch.utils.data.random_split(dataset, [train_size, val_size])

train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=32, shuffle=True)

ネットワークの調整

37クラス分類になるはずなので、最終層だけちょっといじってます。

model = resnet34(pretrained=True)
model.fc = nn.Linear(512,37)

結果

最終的に分類の結果としてはこんな感じになりました。

              precision    recall  f1-score   support

           0       0.86      0.80      0.83        64
           1       0.78      0.77      0.78        52
           2       0.77      0.74      0.75        54
           3       0.86      0.96      0.91        70
           4       0.90      0.79      0.84        57
           5       0.73      0.94      0.82        48
           6       0.82      0.79      0.81        63
           7       0.84      0.82      0.83        60
           8       0.49      0.85      0.63        48
           9       0.81      0.81      0.81        67
          10       0.89      0.79      0.84        62
          11       0.84      0.84      0.84        56
          12       0.89      0.54      0.67       114
          13       0.98      0.58      0.73        72
          14       0.52      0.91      0.67        47
          15       0.87      0.71      0.78        58
          16       0.78      0.85      0.82        60
          17       0.82      0.86      0.84       117
          18       0.82      0.89      0.85        71
          19       0.83      0.93      0.88        58
          20       0.85      0.84      0.84        67
          21       0.93      0.87      0.90        60
          22       0.97      1.00      0.98        62
          23       0.98      0.77      0.86        64
          24       0.88      0.65      0.75        66
          25       0.75      0.96      0.84        46
          26       0.81      0.93      0.87        60
          27       0.93      0.89      0.91        61
          28       0.75      0.85      0.80        62
          29       0.89      0.88      0.89        66
          30       0.78      0.95      0.86        61
          31       0.89      0.93      0.91        54
          32       0.78      0.68      0.73        66
          33       0.80      0.92      0.85        61
          34       0.98      0.75      0.85        63

    accuracy                           0.82      2217
   macro avg       0.83      0.83      0.82      2217
weighted avg       0.84      0.82      0.82      2217

accuracy 82%なんで、そこそこ画像認識はできてそうですね。 軽くfine tuningしただけでもこれくらい行っちゃうもんなんですかね。

37クラスのはずなのに35種類しかテストデータに含まれてないのは多分何かがおかしいんですが、 力尽きたので今回はここまで。覚えてたらいつか直します。

1/31 追記

データの中身を調べたら、35種類しかデータがはいってなかったです。 そのため、上記の37クラスにしているところは、正しくは35クラスが正しく、出力されている35クラスの分類が正しいことになりそうです。

書いたコード

google colabで動かせば動く気がします。多分。

参考文献

この記事を書くにあたり、下記の記事を参考にさせていただきました。

deepage.net

qiita.com

vaaaaaanquish.hatenablog.com

note.com

感想

非常に基本的なResNetすらぱぱっと使えなかったので、調べてみた記録でした。まだまだ勉強不足ですね。 まあ、普段から触っているわけでは無いですし、ちゃんと勉強してこうやって動かせるようになったんで良しとしましょう。