ブログ名

Fine tuningによるSSDの学習モデル作成(1)

今回の記事の目的

前回までは,Fine tuningを用いてVGG16のFine tuningのモデルを作成しました.

今回はSSDの学習済みモデルに対してFine tuningを行い,再学習してみます. そして今回は長くなるため,何回かに分けて説明していきたいと思います.

Fine tuningとSSDの復習

以前の記事でも説明しましたが,SSDとFine tuningについて再度簡単に説明したいと思います.

SSD

SSDとは,CNNを用いて物体検知を行う手法の1つで,最終的に物体の領域(bounding box)の座標と物体の種別(class)を出力します.

SSDの前半は前回も用いたVGG16という画像分類タスクで用いられるCNNをベースネットワークとして用い,その後方でダウンサンプリング(圧縮して特徴マップを小さくすること)された特徴レイヤーを追加することで,ネットワークの前方では局所的に検出を,後方では大局的に局所的に情報を行うことができます.

各層の結果を統合することによって,最終的に様々なスケールの物体に対してもbounding boxを作成できるようにしています.

f:id:iTD_GRP:20190720234943p:plain

Fine tuning

Fine tuningとは,CNNの学習済みモデルを用いて,その一部を再学習することで,モデルを再構築する手法のことです.

具体的には学習済みモデルがもつ特徴量はそのままに,クラス同定部分を再学習するという手法をとります.

そのためFine tuningで学習を行うと,学習済みモデルをもとに再学習しているので,少ない時間,かつ少ない学習データ(画像数)で学習モデルの構築が可能になります.

Chainerを用いたSSDの実行

実行環境と用いたフレームワーク

今回はChainerを用いて実装し,Google Colaboratory上で実行しました.

Chainerとは,Python機械学習,特にDNNの計算等で用いられるフレームワーク,ライブラリの1つです.

以前,SSDを実行する際に,Githubから複雑で長いSSDのコードをクローンして実行しましたが,これを用いると,SSDやYOLOなど物体検知で使われるアルゴリズムや,前述のVGGなどを短いコードで容易に使うことができます.

実行する前にChainerとChainerCVをインストールする必要があります.

pipで以下のようにインストールします.(anacondaを入れている場合はconda installでも可能です.)

pip install chainer
pip install chainercv

それでは,Fine tuningを行う前に試しにChainerを用いてSSDを実行してみましょう.

SSDは以下のコードで実行することができます.

from chainercv.links import SSD300
from chainercv.utils import read_image
from chainercv.visualizations import vis_bbox
import matplotlib.pyplot as plt
from chainercv.datasets import voc_bbox_label_names

model = SSD300(pretrained_model='voc0712')
model.score_thresh = 0.50
model.nms_thresh = 0.45
img = read_image('ssd_picts/test/image/000000581139.jpg', color=True)

# モデルを使って予測
bboxes, labels, scores = model.predict([img])
bbox, label, score = bboxes[0], labels[0], scores[0]

# 描画と表示
vis_bbox(img, bbox, label, score, label_names=voc_bbox_label_names)
plt.show()

その結果,以下のように検出することができます.

f:id:iTD_GRP:20190720235110p:plain

このコードを見ると,以前紹介したGithubリポジトリに比べたら比較的容易に検出することができていることがわかります.

このプログラムについて具体的に説明すると,まず,modelという変数には,SSD300という関数で,PASCAL VOC2007/2012というデータセットで学習された学習済みモデルを格納しています.

その後,物体が検知されたと出力するための閾値を定義しています.具体的に閾値はclassの確率(score_thresh)やIoU(nms_thresh)という各領域がどれくらい重なっているか?(物体領域の重なり具合が大きい→そこに物体がある確率が高い)で設定しています.

そしてmodel.predict関数に画像を入力として渡すことによって,bounding boxの予測を行い,vis_bboxで出力された結果bounding boxを反映,plt.showでbounding boxを反映した画像が出力されます.

Fine tuningを用いたSSDモデルの学習

それでは,このChainerを用いてSSDのFine tuningを行ってみたいと思います.

SSDのFine tuningでは,上述したように各レイヤーでクラスとbounding boxの同定をしています.

前回の記事でVGG16では,末端のレイヤーのみがクラスを同定していた一方で,SSDでは各フェイズでbounding boxの座標とクラスの同定をしているため,今回はそれらに対して学習を行います.

今回は先ほど使用したPASCAL VOC2007/2012というデータセットで学習したSSDの学習済みモデルを用いてFine tuningを行います.

学習データ,評価データの作成

学習データや評価データはこちらのサイトでbounding boxとclassをアノテーションします.

このサイトでは,まず学習データや評価データとしたい画像データを読み込みます.

左上の「Project」から「Add local files」を選択します.するとファイルが選択することができるので,そこでアノテーションしたい画像を選択します.

すると画像が読み込まれるので,それぞれの画像に対して物体と思われるところに対して以下のようにアノテーションを行います.

f:id:iTD_GRP:20190720235149p:plain

クラスの付与は,左下の「Attributes」を用いて付与します.

まず,「Region Attributes」を選択し,「attribute name」に物体領域に付与したい属性名を入力します.

f:id:iTD_GRP:20190720235213p:plain

今回は物体領域の属性として「label」という名前の属性を付与します.すると名前やタイプなど聞かれますが,Typeは「text」とします.

そして各物体領域に対して,「男性」の場合は「label」に0,「女性」の場合は1を付与します.

具体的にlabelの付与の方法は,領域をクリックすると,属性が設定されている場合,下図のように表示されるので,「not define yet!」と書いてあるところにクラス名(今回の場合は「男性」の場合は0,「女性」の場合は1)を付与します.

f:id:iTD_GRP:20190720235236p:plain

ちなみにファイル全体にクラスなど属性を付与したい場合は,「Attributes」の「File Attributes」を選択すると,クラスを付与できます.画像認識用のデータセットを作成するなどの場合は,こちらを使えばアノテーションの付与ができます.

アノテーションの付与が終了したら「Project」の「Save」をクリックすると,各ファイルのアノテーション情報がJSONファイルとして出力されます.

セーブ後,学習データが足りなかったなどで追加で付与したい場合は,「Project」の「Load」を選択した後,セーブしたJSONファイルを読み込むと,アノテーションの続きを行うことができます.

今回,データセットCOCOの画像から以下のように用意しました.

  • 学習データ数:100枚
  • 評価データ数:10枚

また,ファイル構成はssd_picts/のなかに以下のようなフォルダを作成し,その中に画像ファイルを入れました.

  • train/ (学習用データ)
  • valid/ (評価用データ)
  • test/ (テスト用のデータ)

画像の取り込み

まず,画像の取り込みを行います.

今回,画像の取り込みに関しては単に画像を取り込むだけでなく,アノテーションデータも同時に取り込み,画像とアノテーションデータを対応付けて出力する必要があります.

Chainerではそのためのクラスとしてchainercv.chainer_experimental.datasets.sliceableにあるGetterDatasetというものがあるため,それを用いて以下のように実装しました.

class originalDataset(GetterDataset):
    def __init__(self,split='train'):
        super(originalDataset, self).__init__()
        data_dir = 'ssd_picts/'+split+'/'

        file_names = []
        for file in os.listdir(data_dir+'image'):
            file_names.append(file)

        self.filenames = file_names
        self.data_dir = data_dir

        self.add_getter('img', self._get_image)
        self.add_getter(('bbox', 'label'), self._get_annotations)

    # ファイル数を出力
    def __len__(self):
        return len(self.filenames)

    # 画像のインポート
    def _get_image(self,i):
        file_name = self.filenames[i]
        img = read_image(self.data_dir+'image/'+file_name)
        return img

    # i番目の画像のアノテーション情報(メタデータ)をインポート
    def _get_annotations(self,i):
        bbox = np.empty((0,4), float)
        label = np.empty((1,0), int)
        filename = self.filenames[i]
        # メタデータから該当データ探索
        f = open(self.data_dir+'metadata.json')
        json_data = json.load(f)['_via_img_metadata']
        tmp_picts = list(json_data.values())
        tmp_data = {}
        objs = [p['regions'] for p in tmp_picts if p['filename'] == filename][0]

        # 領域それぞれに対して領域の角の点を抽出,ラベルも同時に定義
        for obj in objs:
            xmax = int(obj['shape_attributes']['x']) + int(obj['shape_attributes']['width'])
            ymax = int(obj['shape_attributes']['y']) + int(obj['shape_attributes']['height'])
            tmp_bbox=np.array([int(obj['shape_attributes']['y']), int(obj['shape_attributes']['x']), ymax, xmax])
            tmp_label = int(obj['region_attributes']['label'])
            bbox = np.append(bbox,np.array([tmp_bbox]), axis = 0)
            label = np.append(label,tmp_label)
        bbox = np.array(bbox,dtype=np.float32)
        label = np.array(label,dtype=np.int32)
        bbox = np.stack(bbox).astype(np.float32)
        label = np.stack(label).astype(np.int32)
        return bbox, label

まずinitではGetterDatasetを継承し,ファイルの読み込みをしています.

基本的に変数はどのデータを呼び出すか(学習データ:train,評価データ:validate,テストデータ:test)を引数ととっています.

そしてinitの末尾のadd_getterでデータセット内の各データに対して_get_image_get_annotationsを適用し,最終的に各filenamesに対してimg, bbox, labelを1セットで格納しています.

これを以下のように変数を与えることで,img, bbox, labelを1セットにし,それらを全ファイルに対してまとめたデータセットが出力として変数に格納されます.

train_dataset = originalDataset(split='train')

画像の水増し

Fine tuningでは少ない画像で学習できるという前提があります.そのため,画像を水増しすることによって学習を行う必要があります.

画像の水増しはchainer.datasets.TransformDatasetにあるTransformDatasetというクラスを使って,データセットの水増しを行います.

このクラスでは引数として,第一引数にデータセット,第二引数に処理関数名をとります.第一引数のデータセットは前述したoriginalDatasetで取得したデータセットを格納すれば良いです.そのため,第二引数の処理関数の定義をします.

今回は処理関数にTransformDataset内で渡される変数以外にSSDのモデルに関する情報を渡しているため,関数ではなくクラスで定義し,クラスに値が渡された時に呼び出される__call__で処理を定義します.

class Transform(object):
    def __init__(self, coder, size, mean):
        self.coder = copy.copy(coder)
        self.coder.to_cpu()

        self.size = size
        self.mean = mean

    # 呼び出された時の処理
    def __call__(self, in_data):
        img, bbox, label = in_data

        # 1. 色の拡張
        img = random_distort(img)

        # 2. ランダムな拡大
        if np.random.randint(2):
            img, param = transforms.random_expand(img, fill=self.mean, return_param=True)
            bbox = transforms.translate_bbox(bbox, y_offset=param['y_offset'], x_offset=param['x_offset'])

        # 3. ランダムなトリミング
        img, param = random_crop_with_bbox_constraints(img, bbox, return_param=True)
        bbox, param = transforms.crop_bbox(
            bbox, y_slice=param['y_slice'],
            x_slice=param['x_slice'],
            allow_outside_center=False,
            return_param=True)
        label = label[param['index']]

        # 4. ランダムな補完の再補正
        _, H, W = img.shape
        img = resize_with_random_interpolation(img, (self.size, self.size))
        bbox = transforms.resize_bbox(bbox, (H, W), (self.size, self.size))

        # 5. ランダムな水平反転
        img, params = transforms.random_flip(img, x_random=True, return_param=True)
        bbox = transforms.flip_bbox(bbox, (self.size, self.size), x_flip=params['x_flip'])

        # SSDのネットワークに入力するための準備の処理
        img -= self.mean
        mb_loc, mb_label = self.coder.encode(bbox, label)

        return img, mb_loc, mb_label

__call__内では上から以下の5つの処理を行っています. 1. 色の拡張:明るさ,コントラスト,彩度,色相を組み合わせの変更 2. ランダムな拡大:様々な拡大率の画像を作成し,bounding boxもそれに合うように調整 3. ランダムなトリミング:ランダムに画像をトリミングし,bounding boxもそれに合うように調整 4. ランダムなリサイズ:様々な比率の画像を作成し,bounding boxもそれに合うように調整 5. ランダムな水平反転:画像とbounding boxをランダムに水平方向に反転

最終的には画像とbounding boxの座標と縦横の長さ,そして予測されうるクラス出力しています.

これを用いて以下のように学習データの水増しします.

train = TransformDataset(train_dataset,Transform(model.coder, model.insize, model.mean))

上記の処理で,model.coder, model.insize, model.mean__init__によりTransformクラスに格納され,TransformDatasetによってtrain_datasetTransformに渡されたことによって,__call__にデータセットが渡され,データの水増しが実行され,水増しされたデータセットがtrainに渡されるという実装になっています.

まとめ

今回はSSDのFine tuningによる学習のための比較的下準備の段階(学習データの作成と読み込み・水増し)の説明をしました. 次の記事では実際にFine tuningを実行して学習を行い,評価したいと思います.

参考文献


次の記事へ

前の記事へ 目次に戻る