4.3. 演習:内視鏡画像ポリープ検出#

医療分野では、内視鏡画像や X 線画像を用いた病変部位の検出に関する研究が活発に進められています。最近では、これらの技術が内視鏡システムに組み込まれ、医師の診断を補助する製品として実用化されています。また、このような内視鏡画像からポリープを検出するシステムも、PyTorch のようなライブラリを活用することで、個人や小規模なチームでも開発することが可能です。本節では、内視鏡画像からポリープを検出するモデルを構築する方法について学びます。

4.3.1. 演習準備#

4.3.1.1. ライブラリ#

本節で利用するライブラリを読み込みます。NumPy、Pnadas、Matplotlib、Pillow(PIL)などのライブライは、モデルの性能や推論結果などの可視化に利用します。scikit-learn(sklearn)、PyTorch(torch)、torchvision、torchmetrics は機械学習関連のライブラリであり、モデルの構築、検証や推論などに利用します。

import os
import numpy as np
import pandas as pd
import PIL.Image
import matplotlib.pyplot as plt

import torch
import torchvision
import torchmetrics

print(f'torch v{torch.__version__}; torchvision v{torchvision.__version__}')
torch v2.8.0+cu126; torchvision v0.23.0+cu126

ライブラリの読み込み時に ImportErrorModuleNotFoundError が発生した場合は、該当するライブラリをインストールしてください。ライブラリのバージョンを揃える必要はありませんが、PyTorch(torch)および torchvision が上記のバージョンと異なる時、実行中に警告メッセージが現れたり、同じ結果にならなかったりする可能性があります。

4.3.1.2. データセット#

本節では、Kvasir データセット[1]を使用します。このデータセットは内視鏡画像を集めた医療用データセットで、Simula Research Laboratory にて公開されています。Kvasir データセットは研究および教育目的に限り利用可能で、それ以外の用途での使用は許可されていません[2]。データセットを扱う際は、利用規約を必ず遵守してください。

オリジナルの Kvasir データセットでは、セグメンテーション用の画像にはポリープを含むもののみが収録されています。しかし、実際の医療現場では、ポリープが存在しない健常者の画像も含まれることが一般的です。そこで、本演習では、オリジナルの Kvasir セグメンテーション用データから一部を抽出し、そこに Kvasir 分類用データから健常者の内視鏡画像を追加して、新しいデータセットを作成しました。

新しいデータセットは、訓練データ 120 枚、検証データ 30 枚、テストデータ 30 枚で構成されています。訓練データは 100 枚の画像がポリープを持つ画像であり、残りの 20 枚は健全な画像です。また、検証データとテストデータはそれぞれ 20 枚のポリープ画像と 10 枚の健全画像が含まれています。

../_images/kvasir_detection_dataset.jpg

Fig. 4.11 Kvasir データセットに含まれる各カテゴリのサンプル画像。#

Jupyter Notebook 上では、次のコマンドを実行することでダウンロードできます。

!wget https://dl.biopapyrus.jp/data/kvasirdet.zip
!unzip kvasirdet.zip

4.3.1.3. 画像前処理#

物体検出の問題では、画像と一緒に、検出対象の物体を囲むバウンディングボックスの座標とラベルをモデルに与え、学習させる必要があります。バウンディングボックスの座標とラベルは、一般的に COCO フォーマット(.json)や Pascal VOC フォーマット(.xml)などで保存されます。しかし、これらのフォーマットのままでは PyTorch で直接扱えないため、PyTorch が利用できる形式に変換する必要があります。

さらに、PyTorch では、画像にアノテーションがない場合にエラーが発生するため、ポリープが含まれない健常者の画像については特別な処理を行っています。具体的には、ダミーのバウンディングボックス([0, 0, 1, 1])を設定し、そのラベルを背景クラス(0)として扱うようにしています。この対応により、健常者の画像もエラーなく処理できるようになります。

class CocoDataset(torchvision.datasets.CocoDetection):
    def __init__(self, root, annFile):
        super(CocoDataset, self).__init__(root, annFile)
    
    def __getitem__(self, idx):
        img, target = super(CocoDataset, self).__getitem__(idx)
        
        boxes = []
        labels = []
        for obj in target:
            bbox = obj['bbox']
            bbox = [bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3]]
            boxes.append(bbox)
            labels.append(obj['category_id'])
        if len(boxes) == 0:
            boxes = [[0, 0, 1, 1]]
            labels = [0]
        
        img = torchvision.transforms.functional.to_tensor(img)
        target = {
            'boxes': torch.as_tensor(boxes, dtype=torch.float32),
            'labels': torch.as_tensor(labels, dtype=torch.int64),
        }

        return img, target
    

なお、画像分類と同様に、畳み込みニューラルネットワークでは入力する画像のサイズを指定されたサイズに変更する必要があります。ただし、このサイズ変更は CocoDetection 内で行われるため、ここであらためて設定する必要はありません。

また、モデルを訓練する際には、画像の拡大縮小や平行移動、回転などのデータ拡張を行い、それに伴ってバウンディングボックスの座標も同じように再計算する必要があります。しかし、これらの処理を加えるとコードが複雑化し、全体の流れがわかりにくくなるため、本節ではデータ拡張の処理は省略しています。

4.3.1.4. 計算デバイス#

計算を行うデバイスを設定します。PyTorch が GPU を認識できる場合は GPU を利用し、認識できない場合は CPU を使用するように設定します。

device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')

4.3.2. モデル構築#

本節では、物体検出アーキテクチャとしてよく知られている Faster R-CNN を使用します。torchvision.models.detection で提供されているアーキテクチャは、車や人など 90 種類の一般的なオブジェクトを対象としています。これに対して、本節では、ポリープという 1 種類のオブジェクトのみを検出を目的としています。そのため、torchvision.models.detection から読み込んだアーキテクチャの出力層のユニット数を、検出対象の種類数に合わせる必要があります。

物体検出アーキテクチャでは、背景を一つのクラスとして扱うため、出力数を修正するとき、検出対象数に 1 を加えた値で修正します。例えば、ポリープ検出の場合は、出力層の数を 2 とします。この修正は、アーキテクチャを呼び出すたびに行う必要があり、手間がかかります。そこで、一連の処理を関数として定義してから利用します。

def fasterrcnn(num_classes, weights=None):
    model = torchvision.models.detection.fasterrcnn_resnet50_fpn(weights='DEFAULT')
    in_features = model.roi_heads.box_predictor.cls_score.in_features
    num_classes = num_classes + 1  # class + background
    model.roi_heads.box_predictor = torchvision.models.detection.faster_rcnn.FastRCNNPredictor(in_features, num_classes)
    if weights is not None:
        model.load_state_dict(torch.load(weights))
    return model

model = fasterrcnn(num_classes=1)
model.to(device)
Hide code cell output
FasterRCNN(
  (transform): GeneralizedRCNNTransform(
      Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
      Resize(min_size=(800,), max_size=1333, mode='bilinear')
  )
  (backbone): BackboneWithFPN(
    (body): IntermediateLayerGetter(
      (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
      (bn1): FrozenBatchNorm2d(64, eps=0.0)
      (relu): ReLU(inplace=True)
      (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
      (layer1): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
            (1): FrozenBatchNorm2d(256, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer2): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(512, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (3): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer3): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(1024, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (3): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (4): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (5): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer4): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(2048, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
    )
    (fpn): FeaturePyramidNetwork(
      (inner_blocks): ModuleList(
        (0): Conv2dNormActivation(
          (0): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1))
        )
        (1): Conv2dNormActivation(
          (0): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
        )
        (2): Conv2dNormActivation(
          (0): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1))
        )
        (3): Conv2dNormActivation(
          (0): Conv2d(2048, 256, kernel_size=(1, 1), stride=(1, 1))
        )
      )
      (layer_blocks): ModuleList(
        (0-3): 4 x Conv2dNormActivation(
          (0): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        )
      )
      (extra_blocks): LastLevelMaxPool()
    )
  )
  (rpn): RegionProposalNetwork(
    (anchor_generator): AnchorGenerator()
    (head): RPNHead(
      (conv): Sequential(
        (0): Conv2dNormActivation(
          (0): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (1): ReLU(inplace=True)
        )
      )
      (cls_logits): Conv2d(256, 3, kernel_size=(1, 1), stride=(1, 1))
      (bbox_pred): Conv2d(256, 12, kernel_size=(1, 1), stride=(1, 1))
    )
  )
  (roi_heads): RoIHeads(
    (box_roi_pool): MultiScaleRoIAlign(featmap_names=['0', '1', '2', '3'], output_size=(7, 7), sampling_ratio=2)
    (box_head): TwoMLPHead(
      (fc6): Linear(in_features=12544, out_features=1024, bias=True)
      (fc7): Linear(in_features=1024, out_features=1024, bias=True)
    )
    (box_predictor): FastRCNNPredictor(
      (cls_score): Linear(in_features=1024, out_features=2, bias=True)
      (bbox_pred): Linear(in_features=1024, out_features=8, bias=True)
    )
  )
)

4.3.3. モデル訓練#

モデルが学習データを効率よく学習できるようにするため、学習アルゴリズム(optimizer)、学習率(lr)、および学習率を調整するスケジューラ(lr_scheduler)を設定します。なお、画像分類では損失関数も合わせて定義していますが、物体検出では分類誤差を計算する損失関数とバウンディングボックスの座標の誤差を計算する損失関数の二種類を定義する必要があります。これらの関数はすでにモデルの中で定義されているため、ここであらためて定義する必要はありません。

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)
lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)

次に、訓練データと検証データを読み込み、モデルが入力できる形式に整えます。

train_loader = torch.utils.data.DataLoader(
                    CocoDataset('kvasirdet/train', 'kvasirdet/train/bbox.json'),
                    batch_size=4, shuffle=True, collate_fn=lambda x: tuple(zip(*x)))
valid_loader = torch.utils.data.DataLoader(
                    CocoDataset('kvasirdet/valid', 'kvasirdet/valid/bbox.json'),
                    batch_size=4, shuffle=False, collate_fn=lambda x: tuple(zip(*x)))
Hide code cell output
loading annotations into memory...
Done (t=0.00s)
creating index...
index created!
loading annotations into memory...
Done (t=0.00s)
creating index...
index created!

準備が整ったら、訓練を開始します。訓練プロセスでは、訓練と検証を交互に繰り返します。訓練では、訓練データを使ってモデルのパラメータを更新し、その際の損失(誤差)を記録します。検証では、検証データを使ってモデルの予測性能(mAP)を計算し、その結果を記録します。

num_epochs = 10
metric_dict = []

for epoch in range(num_epochs):
    # training phase
    model.train()
    epoch_loss_dict = {}

    for images, targets in train_loader:
        images = [img.to(device) for img in images]
        targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
        
        batch_loss_dict = model(images, targets)
        batch_tol_loss = 0
        for loss_type, loss_val in batch_loss_dict.items():
            batch_tol_loss += loss_val
            if loss_type in epoch_loss_dict:
                epoch_loss_dict[f'train_{loss_type}'] += loss_val.item()
            else:
                epoch_loss_dict[f'train_{loss_type}'] = loss_val.item()
                
        # update weights
        optimizer.zero_grad()
        batch_tol_loss.backward()
        optimizer.step()
    lr_scheduler.step()


    # validation phase
    model.eval()
    metric = torchmetrics.detection.mean_ap.MeanAveragePrecision()
    with torch.no_grad():
        for images, targets in valid_loader:
            images = [img.to(device) for img in images]
            targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
            metric.update(model(images), targets)


    # record training loss
    epoch_loss_dict['train_loss_total'] = sum(epoch_loss_dict.values())
    metric_dict.append({k: v / len(train_loader) for k, v in epoch_loss_dict.items()})
    for k, v in metric.compute().items():
        if k != 'classes':
            metric_dict[-1][k] = v.item()
    metric_dict[-1]['epoch'] = epoch + 1

    print(metric_dict[-1])
Hide code cell output
{'train_loss_classifier': 0.0013812458763519922, 'train_loss_box_reg': 0.0013580781718095143, 'train_loss_objectness': 0.0001793674658983946, 'train_loss_rpn_box_reg': 0.003767561415831248, 'train_loss_total': 0.0066862529298911495, 'map': 0.13364852964878082, 'map_50': 0.30009812116622925, 'map_75': 0.1346936821937561, 'map_small': 0.0, 'map_medium': -1.0, 'map_large': 0.26734599471092224, 'mar_1': 0.13500000536441803, 'mar_10': 0.2475000023841858, 'mar_100': 0.27000001072883606, 'mar_small': 0.0, 'mar_medium': -1.0, 'mar_large': 0.5400000214576721, 'map_per_class': -1.0, 'mar_100_per_class': -1.0, 'epoch': 1}
{'train_loss_classifier': 0.0012676199277242026, 'train_loss_box_reg': 0.001375533640384674, 'train_loss_objectness': 5.7388543306539454e-05, 'train_loss_rpn_box_reg': 0.005978195865948995, 'train_loss_total': 0.00867873797736441, 'map': 0.12289060652256012, 'map_50': 0.27942612767219543, 'map_75': 0.09694719314575195, 'map_small': 0.0, 'map_medium': -1.0, 'map_large': 0.2458537071943283, 'mar_1': 0.1525000035762787, 'mar_10': 0.23749999701976776, 'mar_100': 0.24250000715255737, 'mar_small': 0.0, 'mar_medium': -1.0, 'mar_large': 0.48500001430511475, 'map_per_class': -1.0, 'mar_100_per_class': -1.0, 'epoch': 2}
{'train_loss_classifier': 0.001143710066874822, 'train_loss_box_reg': 0.00237118328611056, 'train_loss_objectness': 3.5544958276053267e-05, 'train_loss_rpn_box_reg': 0.0002748824966450532, 'train_loss_total': 0.0038253208079064888, 'map': 0.1819431185722351, 'map_50': 0.3421657383441925, 'map_75': 0.2054019272327423, 'map_small': 0.0, 'map_medium': -1.0, 'map_large': 0.3639002740383148, 'mar_1': 0.19750000536441803, 'mar_10': 0.28999999165534973, 'mar_100': 0.28999999165534973, 'mar_small': 0.0, 'mar_medium': -1.0, 'mar_large': 0.5799999833106995, 'map_per_class': -1.0, 'mar_100_per_class': -1.0, 'epoch': 3}
{'train_loss_classifier': 0.0012536474814017614, 'train_loss_box_reg': 0.0020303005973498025, 'train_loss_objectness': 1.9647431327030062e-05, 'train_loss_rpn_box_reg': 0.00014778027931849162, 'train_loss_total': 0.003451375789397086, 'map': 0.2016880214214325, 'map_50': 0.3619363605976105, 'map_75': 0.21048855781555176, 'map_small': 0.0, 'map_medium': -1.0, 'map_large': 0.403376042842865, 'mar_1': 0.23000000417232513, 'mar_10': 0.29499998688697815, 'mar_100': 0.29499998688697815, 'mar_small': 0.0, 'mar_medium': -1.0, 'mar_large': 0.5899999737739563, 'map_per_class': -1.0, 'mar_100_per_class': -1.0, 'epoch': 4}
{'train_loss_classifier': 0.0011098843067884445, 'train_loss_box_reg': 0.0014253397782643636, 'train_loss_objectness': 0.00025409300190707047, 'train_loss_rpn_box_reg': 0.00019711012331147988, 'train_loss_total': 0.0029864272102713583, 'map': 0.18859387934207916, 'map_50': 0.35817354917526245, 'map_75': 0.14992928504943848, 'map_small': 0.0, 'map_medium': -1.0, 'map_large': 0.37728485465049744, 'mar_1': 0.20749999582767487, 'mar_10': 0.2849999964237213, 'mar_100': 0.2849999964237213, 'mar_small': 0.0, 'mar_medium': -1.0, 'mar_large': 0.5699999928474426, 'map_per_class': -1.0, 'mar_100_per_class': -1.0, 'epoch': 5}
{'train_loss_classifier': 0.0005534964303175609, 'train_loss_box_reg': 0.0010100351025660833, 'train_loss_objectness': 4.53465812218686e-05, 'train_loss_rpn_box_reg': 0.0002643990951279799, 'train_loss_total': 0.0018732772092334925, 'map': 0.18460629880428314, 'map_50': 0.365108460187912, 'map_75': 0.1481827348470688, 'map_small': 0.0, 'map_medium': -1.0, 'map_large': 0.3692309558391571, 'mar_1': 0.21250000596046448, 'mar_10': 0.2874999940395355, 'mar_100': 0.2874999940395355, 'mar_small': 0.0, 'mar_medium': -1.0, 'mar_large': 0.574999988079071, 'map_per_class': -1.0, 'mar_100_per_class': -1.0, 'epoch': 6}
{'train_loss_classifier': 0.0009242005646228791, 'train_loss_box_reg': 0.0008853525544206301, 'train_loss_objectness': 4.038085462525487e-05, 'train_loss_rpn_box_reg': 3.085181427498658e-05, 'train_loss_total': 0.0018807857879437507, 'map': 0.19238625466823578, 'map_50': 0.3602958619594574, 'map_75': 0.16985304653644562, 'map_small': 0.0, 'map_medium': -1.0, 'map_large': 0.38477250933647156, 'mar_1': 0.2175000011920929, 'mar_10': 0.29750001430511475, 'mar_100': 0.29750001430511475, 'mar_small': 0.0, 'mar_medium': -1.0, 'mar_large': 0.5950000286102295, 'map_per_class': -1.0, 'mar_100_per_class': -1.0, 'epoch': 7}
{'train_loss_classifier': 0.0008710841337839762, 'train_loss_box_reg': 0.0010625816881656646, 'train_loss_objectness': 0.00010529786814004183, 'train_loss_rpn_box_reg': 0.00022583684573570888, 'train_loss_total': 0.0022648005358253914, 'map': 0.18017442524433136, 'map_50': 0.36285075545310974, 'map_75': 0.13099771738052368, 'map_small': 0.0, 'map_medium': -1.0, 'map_large': 0.3603488504886627, 'mar_1': 0.20000000298023224, 'mar_10': 0.27250000834465027, 'mar_100': 0.27250000834465027, 'mar_small': 0.0, 'mar_medium': -1.0, 'mar_large': 0.5450000166893005, 'map_per_class': -1.0, 'mar_100_per_class': -1.0, 'epoch': 8}
{'train_loss_classifier': 0.001032961035768191, 'train_loss_box_reg': 0.0018582537770271302, 'train_loss_objectness': 3.598022740334272e-05, 'train_loss_rpn_box_reg': 4.416273829216758e-05, 'train_loss_total': 0.0029713577784908313, 'map': 0.16942308843135834, 'map_50': 0.36316099762916565, 'map_75': 0.1264626532793045, 'map_small': 0.0, 'map_medium': -1.0, 'map_large': 0.3388565480709076, 'mar_1': 0.19499999284744263, 'mar_10': 0.26499998569488525, 'mar_100': 0.26499998569488525, 'mar_small': 0.0, 'mar_medium': -1.0, 'mar_large': 0.5299999713897705, 'map_per_class': -1.0, 'mar_100_per_class': -1.0, 'epoch': 9}
{'train_loss_classifier': 0.0009569423894087473, 'train_loss_box_reg': 0.0011185061186552048, 'train_loss_objectness': 1.3370042627987763e-05, 'train_loss_rpn_box_reg': 0.00032589534918467204, 'train_loss_total': 0.002414713899876612, 'map': 0.16955453157424927, 'map_50': 0.36285075545310974, 'map_75': 0.1264626532793045, 'map_small': 0.0, 'map_medium': -1.0, 'map_large': 0.33911964297294617, 'mar_1': 0.19499999284744263, 'mar_10': 0.26499998569488525, 'mar_100': 0.26499998569488525, 'mar_small': 0.0, 'mar_medium': -1.0, 'mar_large': 0.5299999713897705, 'map_per_class': -1.0, 'mar_100_per_class': -1.0, 'epoch': 10}

訓練データに対する損失と検証データに対する予測性能(mAP)を可視化し、訓練過程を評価します。

Hide code cell source
metric_dict = pd.DataFrame(metric_dict)

fig, ax = plt.subplots(1, 2)
ax[0].plot(metric_dict['epoch'], metric_dict['train_loss_total'], label='total')
ax[0].plot(metric_dict['epoch'], metric_dict['train_loss_classifier'], label='classification')
ax[0].plot(metric_dict['epoch'], metric_dict['train_loss_box_reg'], label='bbox')
ax[0].set_xlabel('epoch')
ax[0].set_ylabel('loss')
ax[0].set_title('Train')
ax[0].legend()
ax[1].plot(metric_dict['epoch'], metric_dict['map_50'])
ax[1].set_ylim(0, 1)
ax[1].set_xlabel('epoch')
ax[1].set_ylabel('mAP (50%)')
ax[1].set_title('Validation')
plt.tight_layout()
fig.show()
../_images/14e64ae445c31b66940def4d421cf8994e3d576f2ce05cd770b7cd2fa33cb74d.png

可視化の結果から、エポック数が増えるにつれて訓練データに対する損失が継続的に減少していることが確認できます。10 エポック目においても訓練損失が減少し続ける傾向がまだ見られます。一方、検証データに対する検出性能(mAP 50%)は、5 エポックを過ぎたあたりでほぼ収束しているようです。ただし、値が と低く、十分とはいえません。このため、訓練エポック数をさらに増やして損失や検証性能の推移を詳しく観察するか、必要に応じてデータを増やして再訓練することが考えられます。ただし、本節では、時間の制約があるため、訓練はここで終了します。

次に、この手順を SSD や YOLO など、他の深層ニューラルネットワークのアーキテクチャに適用し、それぞれの検証性能を比較します。この比較により、データセットに最も適したアーキテクチャを選定します。ただし、本節では時間の関係で他のアーキテクチャを構築せず、上で構築した Faster R-CNN を最適なアーキテクチャとして扱い、次のステップに進みます。

次のステップでは、訓練サブセットと検証サブセットを統合し、最適と判断したアーキテクチャを最初から訓練します。その準備として、まず訓練サブセットと検証サブセットを結合します。

!mkdir kvasirdet/trainvalid/images
!cp kvasirdet/train/images/* kvasirdet/trainvalid/images
!cp kvasirdet/valid/images/* kvasirdet/trainvalid/images

モデル選択のために行われた訓練と検証の結果から、数エポックの訓練だけでも十分に高い予測性能を獲得できたことがわかったので、ここでは訓練サブセットと検証サブセットを統合したデータに対して 5 エポックだけ訓練させます。

# model
model = fasterrcnn(num_classes=1)
model.to(device)

# training parameters
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)
lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)

# training data
train_loader = torch.utils.data.DataLoader(
                            CocoDataset('kvasirdet/trainvalid', 'kvasirdet/trainvalid/bbox.json'),
                            batch_size=4, shuffle=True, collate_fn=lambda x: tuple(zip(*x)))

# training
num_epochs = 5
metric_dict = []
for epoch in range(num_epochs):
    model.train()
    epoch_loss_dict = {}
    for images, targets in train_loader:
        images = [img.to(device) for img in images]
        targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
        
        batch_loss_dict = model(images, targets)
        batch_tol_loss = 0
        for loss_type, loss_val in batch_loss_dict.items():
            batch_tol_loss += loss_val
            if loss_type in epoch_loss_dict:
                epoch_loss_dict[f'train_{loss_type}'] += loss_val.item()
            else:
                epoch_loss_dict[f'train_{loss_type}'] = loss_val.item()
        optimizer.zero_grad()
        batch_tol_loss.backward()
        optimizer.step()
    lr_scheduler.step()

    # record training loss
    epoch_loss_dict['train_loss_total'] = sum(epoch_loss_dict.values())
    metric_dict.append({k: v / len(train_loader) for k, v in epoch_loss_dict.items()})
    metric_dict[-1]['epoch'] = epoch + 1
    print(metric_dict[-1])
Hide code cell output
loading annotations into memory...
Done (t=0.00s)
creating index...
index created!
{'train_loss_classifier': 0.0005015864184028224, 'train_loss_box_reg': 0.0007450699218009648, 'train_loss_objectness': 9.615286743562472e-05, 'train_loss_rpn_box_reg': 0.0023717123426889117, 'train_loss_total': 0.003714521550328324, 'epoch': 1}
{'train_loss_classifier': 0.000788419360392972, 'train_loss_box_reg': 0.0014607235789299011, 'train_loss_objectness': 0.00024335083894823728, 'train_loss_rpn_box_reg': 3.384815921124659e-05, 'train_loss_total': 0.002526341937482357, 'epoch': 2}
{'train_loss_classifier': 0.0008981108273330488, 'train_loss_box_reg': 0.0016135001849187048, 'train_loss_objectness': 0.00017491541802883148, 'train_loss_rpn_box_reg': 9.328208398073912e-05, 'train_loss_total': 0.002779808514261324, 'epoch': 3}
{'train_loss_classifier': 0.00046638928745922287, 'train_loss_box_reg': 0.0010873404772658098, 'train_loss_objectness': 4.608311236983067e-05, 'train_loss_rpn_box_reg': 0.0013218779901140614, 'train_loss_total': 0.0029216908672089247, 'epoch': 4}
{'train_loss_classifier': 0.0007127345000442706, 'train_loss_box_reg': 0.0008318335994293815, 'train_loss_objectness': 5.045756820197168e-05, 'train_loss_rpn_box_reg': 0.0008333709679151836, 'train_loss_total': 0.002428396635590807, 'epoch': 5}

訓練が完了したら、訓練済みモデルの重みをファイルに保存します。

model.to('cpu')
torch.save(model.state_dict(), 'kvasirdet.pth')

4.3.4. モデル評価#

最適なモデルが得られたら、次にテストデータを用いてモデルを詳細に評価します。

test_loader = torch.utils.data.DataLoader(
                    CocoDataset('kvasirdet/test', 'kvasirdet/test/bbox.json'),
                    batch_size=4, shuffle=True, collate_fn=lambda x: tuple(zip(*x)))

model = fasterrcnn(num_classes=1, weights='kvasirdet.pth')
model.to(device)
model.eval()

metric = torchmetrics.detection.mean_ap.MeanAveragePrecision()
with torch.no_grad():
    for images, targets in test_loader:
        images = [img.to(device) for img in images]
        targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
        metric.update(model(images), targets)

metrics = {}
for k, v in metric.compute().items():
    metrics[k] = v.cpu().detach().numpy().tolist()
Hide code cell output
loading annotations into memory...
Done (t=0.00s)
creating index...
index created!
metrics
{'map': 0.2637008726596832,
 'map_50': 0.4017103612422943,
 'map_75': 0.26474127173423767,
 'map_small': 0.0,
 'map_medium': -1.0,
 'map_large': 0.5274314284324646,
 'mar_1': 0.2750000059604645,
 'mar_10': 0.3375000059604645,
 'mar_100': 0.3375000059604645,
 'mar_small': 0.0,
 'mar_medium': -1.0,
 'mar_large': 0.675000011920929,
 'map_per_class': -1.0,
 'mar_100_per_class': -1.0,
 'classes': [0, 1]}

ここで出力される指標について、map から始まる指標は mAP を表し、mar から始まる指標は mean average recall(平均再現率)を示します。mar はすべてのクラスに対する再現率を計算し、それらの平均を求めたものです。mAR 1 は、各画像に対してモデルが検出した物体のうち、最も信頼度の高い物体だけを利用して計算した再現率を表します。同様に、mAR 10 および mAR 100 は、モデルが検出した物体のうち、信頼度の高い 10 および 100 物体を利用して計算した再現率を表しています。

4.3.5. 推論#

推論時にも、訓練時と同じように torchvision モジュールからアーキテクチャを呼び出し、出力層のクラス数を設定します。その後、load_state_dict メソッドを使って、訓練済みの重みファイルをモデルにロードします。これらの操作はすべて fasterrcnn 関数で定義されているので、その関数を利用します。

model = fasterrcnn(num_classes=1, weights='kvasirdet.pth')
model.to(device)
model.eval()
Hide code cell output
FasterRCNN(
  (transform): GeneralizedRCNNTransform(
      Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
      Resize(min_size=(800,), max_size=1333, mode='bilinear')
  )
  (backbone): BackboneWithFPN(
    (body): IntermediateLayerGetter(
      (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
      (bn1): FrozenBatchNorm2d(64, eps=0.0)
      (relu): ReLU(inplace=True)
      (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
      (layer1): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
            (1): FrozenBatchNorm2d(256, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer2): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(512, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (3): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer3): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(1024, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (3): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (4): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (5): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer4): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(2048, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
    )
    (fpn): FeaturePyramidNetwork(
      (inner_blocks): ModuleList(
        (0): Conv2dNormActivation(
          (0): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1))
        )
        (1): Conv2dNormActivation(
          (0): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
        )
        (2): Conv2dNormActivation(
          (0): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1))
        )
        (3): Conv2dNormActivation(
          (0): Conv2d(2048, 256, kernel_size=(1, 1), stride=(1, 1))
        )
      )
      (layer_blocks): ModuleList(
        (0-3): 4 x Conv2dNormActivation(
          (0): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        )
      )
      (extra_blocks): LastLevelMaxPool()
    )
  )
  (rpn): RegionProposalNetwork(
    (anchor_generator): AnchorGenerator()
    (head): RPNHead(
      (conv): Sequential(
        (0): Conv2dNormActivation(
          (0): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (1): ReLU(inplace=True)
        )
      )
      (cls_logits): Conv2d(256, 3, kernel_size=(1, 1), stride=(1, 1))
      (bbox_pred): Conv2d(256, 12, kernel_size=(1, 1), stride=(1, 1))
    )
  )
  (roi_heads): RoIHeads(
    (box_roi_pool): MultiScaleRoIAlign(featmap_names=['0', '1', '2', '3'], output_size=(7, 7), sampling_ratio=2)
    (box_head): TwoMLPHead(
      (fc6): Linear(in_features=12544, out_features=1024, bias=True)
      (fc7): Linear(in_features=1024, out_features=1024, bias=True)
    )
    (box_predictor): FastRCNNPredictor(
      (cls_score): Linear(in_features=1024, out_features=2, bias=True)
      (bbox_pred): Linear(in_features=1024, out_features=8, bias=True)
    )
  )
)

このモデルを利用して推論を行います。まず、1 枚の画像を指定し、PIL モジュールを用いて画像を開き、テンソル形式に変換した後、モデルに入力します。モデルは予測結果としてバウンディングボックスの座標(bboxes)、分類ラベル(labels)、および信頼スコア(scores)を出力します。ただし、信頼スコアが 0.5 未満のバウンディングボックスは採用せず、信頼スコアが高い結果のみを選択して利用します。

threshold = 0.5
image_path = 'kvasirdet/test/images/cju2lz8vqktne0993fuym6drw.jpg'

image = PIL.Image.open(image_path).convert('RGB')
input_tensor = torchvision.transforms.functional.to_tensor(image).unsqueeze(0).to(device)
    
with torch.no_grad():
    predictions = model(input_tensor)[0]
    
bboxes = predictions['boxes'][predictions['scores'] > threshold]
labels = predictions['labels'][predictions['scores'] > threshold]
scores = predictions['scores'][predictions['scores'] > threshold]

検出されたオブジェクトのバウンディングボックスを入力画像に描画します。その後、PIL および matplotlib ライブラリを使用して、画像とその検出結果を可視化します。

draw = PIL.ImageDraw.Draw(image)
for bbox, label, score in zip(bboxes, labels, scores):
    x1, y1, x2, y2 = bbox
    draw.rectangle(((x1, y1), (x2, y2)), outline="blue", width=3)
    draw.text((x1, y1 - 10), f"{label.item()} ({score:.2f})", fill="blue")
    
fig = plt.figure()
ax = fig.add_subplot()
ax.imshow(image)
ax.axis('off')
fig.show()
../_images/1a8b0123b59672055ff863d2e26870ac9900b88cba04f3fa62faf0cf09b349e6.png

次にこのモデルにもう一枚の画像を入力し、その推論結果を見てみましょう。

threshold = 0.5
image_path = 'kvasirdet/test/images/cju2hqt33lmra0988fr5ijv8j.jpg'

image = PIL.Image.open(image_path).convert('RGB')
input_tensor = torchvision.transforms.functional.to_tensor(image).unsqueeze(0).to(device)
    
with torch.no_grad():
    predictions = model(input_tensor)[0]
    
bboxes = predictions['boxes'][predictions['scores'] > threshold]
labels = predictions['labels'][predictions['scores'] > threshold]
scores = predictions['scores'][predictions['scores'] > threshold]

draw = PIL.ImageDraw.Draw(image)
for bbox, label, score in zip(bboxes, labels, scores):
    x1, y1, x2, y2 = bbox
    draw.rectangle(((x1, y1), (x2, y2)), outline="blue", width=3)
    draw.text((x1, y1 - 10), f"{label.item()} ({score:.2f})", fill="blue")
    
fig = plt.figure()
ax = fig.add_subplot()
ax.imshow(image)
ax.axis('off')
fig.show()
../_images/deeb7fd22aff976999bdd5c3eae41ad0b92cd801df510921b2bd9f8a47da7121.png