Now Loading...

Now Loading...

CNNを用いた物体検出シリーズの第三弾としてFaster R-CNNを紹介していきます。

Faster R-CNN

Faster R-CNNは、物体検出において現在、標準的に使用されている深層学習アルゴリズムの一つです。これまで紹介してきたR-CNN、Fast R-CNNをより高速に、高精度にしたモデルです。

以下では、Faster R-CNNの概要とその機能の説明を行います。

Faster R-CNNのアーキテクチャ

Faster R-CNNのアーキテクチャは、以下の3つの要素から構成されています。基本的にはFast R-CNNを元として、領域提案(Region Proposal)の部分をニューラルネットワークで置き換えEnd-to-Endで学習できるようにしています。このEnd-to-Endの物体検出が可能になったことが、Faster R-CNNの最も大きい貢献と言えるでしょう!

  1. 特徴抽出ネットワーク(Feature Extraction Network):画像から特徴を抽出するためのネットワークです。Faster R-CNNでは、一般的にImageNetで学習された深層学習モデル(VGG16やResNetなど)が使用されます。
  2. 候補領域抽出ネットワーク(Region Proposal Network, RPN):物体が存在する可能性が高い領域(候補領域)を抽出するためのネットワークです。RPNは、画像の各位置で複数のアンカーボックス(Anchor Box)を設定し、各アンカーボックスが物体である確率を計算します。
  3. 物体検出ネットワーク(Object Detection Network):候補領域から物体を検出するためのネットワークです。このネットワークは、各候補領域に対して、物体のクラスと位置を推定します。全結合層から構成されています。
Faster R-CNN全体の概要

特徴抽出

画像から特徴を抽出するために、事前に学習された特徴抽出ネットワークを使用します。この部分はFast R-CNNと同様でVGG16やResNetといった一般的なCNNが使用されています。

Region Proposal Network (RPN) による物体領域の提案

RPNの概要

次に、RPNを使用して、物体領域の提案を行います。RPNは、CNNによって生成された特徴マップに対して適用され、各位置で複数の候補領域を提案します。これらの候補領域は、固定されたアスペクト比とスケールで生成され、それぞれの位置に対して、物体が存在する確率を予測するための分類と、候補領域のバウンディングボックスを微調整するための回帰を行います。

ここでの固定されたアスペクト比とスケールの矩形領域のことをアンカーボックスといいます。アンカーとは特徴抽出CNNから取り出された特徴マップの各ピクセルに対して打たれた点です。このアンカーを起点にし、異なる固定された矩形領域に対して、物体か背景かをオブジェクトスコアとして出力し、さらに真のバウンディングボックスとのズレを回帰します。

RoIプーリング

Roi pooling(画像はFast R-CNNのもの)

RPNによって生成された候補領域は、各種のサイズやアスペクト比を持ち、それぞれが異なる形状をしています。しかし、CNNは入力として固定されたサイズの画像を想定しているため、候補領域をCNNにそのまま入力することはできません。そこで、RoIプーリングと呼ばれる操作を使用して、候補領域を固定サイズの特徴マップに変換します。これも前回のFast R-CNNと同じですね。

バウンディングボックスの回帰・クラス分類

バウンディングボックスの回帰とクラス分類を行う層(画像はFast R-CNNのもの)

抽出された候補領域に対して、物体のクラスと位置を推定するために、物体検出ネットワークを使用します。物体検出ネットワークは、各候補領域に対して、物体が存在する確率と、物体が存在する場合の物体の位置(バウンディングボックス)を推定します。また、物体が存在するクラスを推定するために、クラス分類器が使用されます。この部分も基本的にはFast R-CNNと同様です。

Faster R-CNNの実装

シリーズのこれまでと同様にPythonの深層学習フレームワークであるPyTorchを使用してFaster R-CNNを実装していきます。Faster R-CNNは現在では標準的な物体検出モデルですのでPyTorchの一部であるTorchvisonというライブラリに便利なモジュールがたくさんあります。なので、今回は主にTorchvisionを使用して実装を行なっていきます。

データセットとDatasetクラスの準備

これまでと同様にデータセットはPASCAL VOC 2007を使用します。さらにクラスを車だけに絞ります(これもこれまでと同様)。データセットの準備に関してはこれまでと同様ですので割愛します。また、今回も深層学習のフレームワークとしてPyTorchを使用します。ですので、例によってPyTorchのDatasetクラスを準備していきます。今回使用するPyTorchのコンピュータビジョン向けライブラリであるtorchvisionで用意されているFaster R-CNNでは次にような形式でデータを渡してあげる必要があります。

以下が実際に書いたDatasetクラスのコードです。

class CarVOCDataset(torch.utils.data.Dataset):
    def __init__(self, root_dir, transforms=None, split="train"):
        self.root_dir = root_dir
        self.transforms = transforms
        self.split = split
        self.imgs = list(
            sorted(os.listdir(os.path.join(root_dir, split, "JPEGImages")))
        )
        self.annotations = list(
            sorted(os.listdir(os.path.join(root_dir, split, "Annotations")))
        )
        print(
            "len of imgs: ",
            len(self.imgs),
            "len of annotations: ",
            len(self.annotations),
        )

    def __getitem__(self, idx):
        img_path = os.path.join(
            self.root_dir,
            "train" if self.split == "train" else "val",
            "JPEGImages",
            self.imgs[idx],
        )
        img = read_image(img_path)

        xml_path = os.path.join(
            self.root_dir,
            "train" if self.split == "train" else "val",
            "Annotations",
            self.annotations[idx],
        )
        bndboxs = parse_xml(xml_path)

        num_objs = len(bndboxs)
        boxes = bndboxs
        labels = torch.ones((num_objs,), dtype=torch.int64)

        target = {}
        target["boxes"] = boxes
        target["labels"] = labels

        if self.transforms is not None:
            # scale the bounding boxes
            h, w = img.shape[-2:]
            _h, _w = 224, 224

            target["boxes"] = torch.as_tensor(target["boxes"], dtype=torch.float32)
            target["boxes"][:, 0] *= _w / w
            target["boxes"][:, 1] *= _h / h
            target["boxes"][:, 2] *= _w / w
            target["boxes"][:, 3] *= _h / h
            img = self.transforms(img)

        is_crowd = torch.zeros((num_objs,), dtype=torch.int64)
        target["iscrowd"] = is_crowd

        area = (target["boxes"][:, 3] - target["boxes"][:, 1]) * (
            target["boxes"][:, 2] - target["boxes"][:, 0]
        )
        target["area"] = area

        image_id = torch.tensor([idx])
        target["image_id"] = image_id

        return img, target

    def __len__(self):
        return len(self.imgs)

学習

以下が学習用のスクリプトです。例によって学習には時間がかかりますので、スクリプトを乗せるのみとなってしまいますが、fine-tuneを実際にやって見たいという方はぜひ動かして見てください!

import os
import xmltodict
import numpy as np
import torch
from torchvision.io.image import read_image
from torchvision.models.detection import (
    fasterrcnn_resnet50_fpn_v2,
    faster_rcnn,
    FasterRCNN_ResNet50_FPN_V2_Weights,
)
import torchvision.transforms as transforms


def parse_xml(xml_path):
    """
    アノテーションのバウンディングボックスの座標を返すためにxmlファイルをパースする
    """
    with open(xml_path, "rb") as f:
        xml_dict = xmltodict.parse(f)

        bndboxs = list()
        objects = xml_dict["annotation"]["object"]
        if isinstance(objects, list):
            for obj in objects:
                obj_name = obj["name"]
                difficult = int(obj["difficult"])
                if "car".__eq__(obj_name) and difficult != 1:
                    bndbox = obj["bndbox"]
                    bndboxs.append(
                        (
                            int(bndbox["xmin"]),
                            int(bndbox["ymin"]),
                            int(bndbox["xmax"]),
                            int(bndbox["ymax"]),
                        )
                    )
        elif isinstance(objects, dict):
            obj_name = objects["name"]
            difficult = int(objects["difficult"])
            if "car".__eq__(obj_name) and difficult != 1:
                bndbox = objects["bndbox"]
                bndboxs.append(
                    (
                        int(bndbox["xmin"]),
                        int(bndbox["ymin"]),
                        int(bndbox["xmax"]),
                        int(bndbox["ymax"]),
                    )
                )
        else:
            pass

        return np.array(bndboxs)


# custom dataset class for pascal voc object detection dataset to train on faster rcnn using pytorch pytorch
class CarVOCDataset(torch.utils.data.Dataset):
    def __init__(self, root_dir, transforms=None, split="train"):
        self.root_dir = root_dir
        self.transforms = transforms
        self.split = split
        self.imgs = list(
            sorted(os.listdir(os.path.join(root_dir, split, "JPEGImages")))
        )
        self.annotations = list(
            sorted(os.listdir(os.path.join(root_dir, split, "Annotations")))
        )
        print(
            "len of imgs: ",
            len(self.imgs),
            "len of annotations: ",
            len(self.annotations),
        )

    def __getitem__(self, idx):
        img_path = os.path.join(
            self.root_dir,
            "train" if self.split == "train" else "val",
            "JPEGImages",
            self.imgs[idx],
        )
        img = read_image(img_path)

        xml_path = os.path.join(
            self.root_dir,
            "train" if self.split == "train" else "val",
            "Annotations",
            self.annotations[idx],
        )
        bndboxs = parse_xml(xml_path)

        num_objs = len(bndboxs)
        boxes = bndboxs
        labels = torch.ones((num_objs,), dtype=torch.int64)

        target = {}
        target["boxes"] = boxes
        target["labels"] = labels

        if self.transforms is not None:
            # scale the bounding boxes
            h, w = img.shape[-2:]
            _h, _w = 224, 224

            target["boxes"] = torch.as_tensor(target["boxes"], dtype=torch.float32)
            target["boxes"][:, 0] *= _w / w
            target["boxes"][:, 1] *= _h / h
            target["boxes"][:, 2] *= _w / w
            target["boxes"][:, 3] *= _h / h
            img = self.transforms(img)

        is_crowd = torch.zeros((num_objs,), dtype=torch.int64)
        target["iscrowd"] = is_crowd

        area = (target["boxes"][:, 3] - target["boxes"][:, 1]) * (
            target["boxes"][:, 2] - target["boxes"][:, 0]
        )
        target["area"] = area

        image_id = torch.tensor([idx])
        target["image_id"] = image_id

        return img, target

    def __len__(self):
        return len(self.imgs)


if __name__ == "__main__":
    # set the device to train on
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    # load the dataset
    transform = transforms.Compose(
        [
            transforms.ToPILImage(),
            transforms.Resize((224, 224)),
            transforms.ToTensor(),
            transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
        ]
    )

    root_dir = "./data/voc_car"
    train_dir = os.path.join(root_dir, "train")
    train_dataset = CarVOCDataset(root_dir, transform, split="train")
    dataloader = torch.utils.data.DataLoader(
        train_dataset,
        batch_size=4,
        shuffle=True,
        num_workers=4,
        collate_fn=lambda x: tuple(zip(*x)),
    )

    # create the model
    weights = FasterRCNN_ResNet50_FPN_V2_Weights.DEFAULT
    model = fasterrcnn_resnet50_fpn_v2(weights=weights, box_score_thresh=0.3)

    # modify the model to fit the number of classes, the number of classes is 2, background and car
    in_features = model.roi_heads.box_predictor.cls_score.in_features
    num_classes = 2
    model.roi_heads.box_predictor = faster_rcnn.FastRCNNPredictor(
        in_features, num_classes
    )
    model.to(device)

    optimizer = torch.optim.SGD(
        model.parameters(), lr=0.005, momentum=0.9, weight_decay=0.0005
    )

    # train the model
    epochs = 10

    best_loss = 1e10
    for i in range(epochs):
        print("Epoch {}/{}".format(i, epochs))
        print("-" * 10)
        model.train()
        running_loss = 0.0
        running_corrects = 0
        for images, targets in dataloader:
            input_images = 
            input_images = torch.stack(input_images, dim=0)
            inputs = input_images.to(device)
            targets = [{k: v.to(device) for k, v in t.items()} for t in targets]

            optimizer.zero_grad()
            outputs = model(inputs, targets)

            loss = sum(loss for loss in outputs.values())
            loss.backward()
            optimizer.step()
            running_loss += loss.item() * inputs.size(0)
        epoch_loss = running_loss / len(dataloader.dataset)
        print(f"loss: {epoch_loss:.4f}")
    torch.save(model.state_dict(), "model.pth")

推論

次のスクリプトはFaster R-CNNの推論用スクリプトです。これまでのR-CNN、Fast R-CNNよりもさらに高速に、高精度になっているのを感じられると思います。このFaster R-CNNから物体検出におけるEnd-to-Endの時代が始まりました。現在ではTransformerを使用したモデルも登場し、さらに高速に、高精度になってきています。

 

おわりに

以上でCNNを用いた物体検出シリーズは終了となります。現在では多様化し、発展を続けている物体検出モデルですが、その先駆けとしてのR-CNN、Fast R-CNN、Faster R-CNNを理解することはマストといえます。ぜひ、学習用コードを自分のデータセット用にカスタマイズしたりして実装面からも理解してみてくださいね!

参考文献

\ シェア /

E資格スピードパッケージ2023#2修了者合格率100%達成

zero to one E資格 jdla

zero to oneの「E資格」向け認定プログラム

日本ディープラーニング協会の実施するE資格の受験ならzero to oneの「E資格」向け認定プログラム (税込165,000円) をおすすめします。当講座は、東京大学大学院工学系研究科の松尾豊教授と東北大学大学院情報科学研究科の岡谷貴之教授が監修する実践的なプログラムとなっています。
厚生労働省の教育訓練給付制度対象のE資格認定プログラムの中では最安値※となり、実質負担額49,500円~(支給割合70%の場合)で受講可能です。※2023年弊社調べ zero to one E資格 jdla

人工知能基礎講座を提供中

人工知能の第一人者である東京大学の松尾豊教授が監修した人工知能基礎講座を受講してみませんか? 人工知能の歴史から自然言語処理、機械学習、深層学習といった最先端のトピックやAIに関わる法律問題まで網羅しているので全てのビジネスパーソン・AIの初学者におすすめです。

サンプル動画

人工知能基礎講座はこちら↓ zero to one G検定 人工知能基礎 jdla

AI初学者・ビジネスパーソン向けのG検定対策講座

G検定受験前にトレーニングしたい方向けの問題集「G検定実践トレーニング」も提供中です。 zero to one E資格 jdla