0. 初めに
2020年末ぐらいにkaggleで開催された胸部レントゲン画像コンペに参加しました。胸のレントゲン画像にある病変部位を見つけるという、物体検知タスクのコンペでした。
このコンペで銅メダルをいただきましたが、しかしこれはGMの方のノートブックをベースにすこしいじってsubmitしただけであって、私自身の力ではありません。
せっかくなので物体検知について勉強し、記事を残したいと思います。
MNISTから以下のような物体検知用の画像を作り、detectron2でモデルを開発して評価を行います。
コードはgithubにあるのでよかったら見てみてください。
https://github.com/persimmon-persimmon/mnist-detection
Googleドライブ直下にmnist_detectionというフォルダを作ってファイルを入れてColabで起動すれば動くと思います。
1. データ準備
kerasのmnistからデータを作ります。アノテーションはCOCOフォーマットで作成します。
以下を実行すれば、imagesフォルダに画像が2万枚でき、train,val,testごとにCOCOフォーマットのjsonファイルができあがります。
COCOフォーマットについては「参考」の記事を参照。
| データまとめ | |
|---|---|
| 画像サイズ | 224x224 | 
| 枚数 | 全部で2万枚(train:val:test=7,2,1) | 
| クラス比率 | 8,9が出現しやすく、数字が小さくなるほど出現しにくくなる。 | 
from keras.datasets import mnist
from sklearn.model_selection import train_test_split
import json
import numpy as np
import pandas as pd
import cv2
import random
from PIL import Image
(_x_train_val, _y_train_val), (_x_test, _y_test)=mnist.load_data()
_x_train, _x_val, _y_train, _y_val = train_test_split(_x_train_val, _y_train_val, test_size=0.2)
train={i:[x for x, t in zip(_x_train, _y_train) if t == i] for i in range(10)}
val={i:[x for x, t in zip(_x_val, _y_val) if t == i] for i in range(10)}
test={i:[x for x, t in zip(_x_test, _y_test) if t == i] for i in range(10)}
image_set={"train":train,"val":val,"test":test}
n_image={"train":14000,"val":4000,"test":2000}
random.seed(0)
height = 224
width = 224
ls=[i for i in range(-1,10)]
weight=[40,10,10,15,15,20,20,25,25,30,30] # 各クラスの登場頻度
os.makedirs("images", exist_ok=True)
# COCOフォーマット出力用変数
coco={}
coco["categories"]=[]
coco["categories"].append({"id":"0","name":"zero"})
coco["categories"].append({"id":"1","name":"one"})
coco["categories"].append({"id":"2","name":"two"})
coco["categories"].append({"id":"3","name":"three"})
coco["categories"].append({"id":"4","name":"four"})
coco["categories"].append({"id":"5","name":"five"})
coco["categories"].append({"id":"6","name":"six"})
coco["categories"].append({"id":"7","name":"seven"})
coco["categories"].append({"id":"8","name":"eight"})
coco["categories"].append({"id":"9","name":"nine"})
annotation_template={}
annotation_template["segmentation"]=0
annotation_template["area"]=28*28
annotation_template["iscrowd"]=False
annotation_template["isbbox"]=True
annotation_template["image_id"]=0
annotation_template["bbox"]=0
annotation_template["category_id"]=0
annotation_template["id"]=0
image_template={}
image_template["dataet_id"]=1
image_template["deleted"]=False
image_template["file_name"]=0
image_template["id"]=0
image_template["num_annotations"]=0
image_template["path"]=0
image_template["width"]=width
image_template["height"]=height
for data_type in ["train","val","test"]:
    images=[]
    annotations=[]
    for t in range(1,n_image[data_type]+1):
        blank = np.random.random((height, width, 3))*127
        blank = np.int64(blank)
        image_template["file_name"]=f"images/{data_type}_{str(t).zfill(6)}.jpg"
        image_template["id"]+=1
        image_template["path"]=f"images/{data_type}_{str(t).zfill(6)}.jpg"
        for i,label in enumerate(random.choices(ls,k=4,weights=weight)):
            if label==-1:continue
            im=random.choice(image_set[data_type][label])
            if i//2==0:
                x_ = int(random.uniform(0,height//2 - 28))
            else:
                x_ = int(random.uniform(height//2,height - 28))
            if i%2==0:
                y_ = int(random.uniform(0,width//2 - 28))
            else:
                y_ = int(random.uniform(width//2, width - 28))
            blank[x_:x_ + 28,y_:y_ + 28,0]+=np.int64(im/3)
            blank[x_:x_ + 28,y_:y_ + 28,1]+=np.int64(im/3)
            blank[x_:x_ + 28,y_:y_ + 28,2]+=np.int64(im/3)
            x=y_
            y=x_
            annotation_template["image_id"]=image_template["id"]
            annotation_template["bbox"]=[x,y,28,28]
            annotation_template["category_id"]=str(label)
            annotation_template["id"]+=1
            annotation_template["segmentation"]=[[x,y,x+28,y,x+28,y+28,x,y+28]]
            annotations.append(annotation_template.copy())
        cv2.imwrite(image_template["path"],blank)
        images.append(image_template.copy())
    coco["images"]=images
    coco["annotations"]=annotations
    with open(f"coco_{data_type}.json","w") as f:
      f.write(json.dumps(coco))
    coco.pop("images")
    coco.pop("annotations")
作成したデータを確認します。
!pip install pyyaml==5.1
import torch
TORCH_VERSION = ".".join(torch.__version__.split(".")[:2])
CUDA_VERSION = torch.__version__.split("+")[-1]
print("torch: ", TORCH_VERSION, "; cuda: ", CUDA_VERSION)
!pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/$CUDA_VERSION/torch$TORCH_VERSION/index.html
from google.colab.patches import cv2_imshow
from detectron2.utils.visualizer import Visualizer, ColorMode
from detectron2.data import MetadataCatalog, DatasetCatalog
from detectron2.data.datasets import register_coco_instances
reg_name="mnist_detection_train"
register_coco_instances(reg_name, {}, "coco_train.json", "")
mnist_detection_metadata = MetadataCatalog.get(reg_name)
dataset_dicts = DatasetCatalog.get(reg_name)
for d in random.sample(dataset_dicts, 3):
    print(d["file_name"])
    img = cv2.imread(d["file_name"])
    visualizer = Visualizer(img[:, :, ::-1], metadata=mnist_detection_metadata, scale=1.0)
    vis = visualizer.draw_dataset_dict(d)
    cv2_imshow(vis.get_image()[:, :, ::-1])
 
  
実行すると、「Category ids in annotations are not in [1, #categories]! We'll apply a mapping for you.」という警告が出ますが、無視してよいです。カテゴリIDが1から始まってないと発生する警告です。詳しくはdetectron2のソースコード参照。
https://detectron2.readthedocs.io/en/latest/_modules/detectron2/data/datasets/coco.html
3. 学習
環境に対応するdetectron2をインストールして学習します。ほぼdetectron2のチュートリアルそのままのコードです。
!pip install pyyaml==5.1
import torch
TORCH_VERSION = ".".join(torch.__version__.split(".")[:2])
CUDA_VERSION = torch.__version__.split("+")[-1]
print("torch: ", TORCH_VERSION, "; cuda: ", CUDA_VERSION)
!pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/$CUDA_VERSION/torch$TORCH_VERSION/index.html
import detectron2
from detectron2.utils.logger import setup_logger
setup_logger()
import numpy as np
import os, json, cv2, random
from google.colab.patches import cv2_imshow
from detectron2 import model_zoo
from detectron2.engine import DefaultPredictor, DefaultTrainer
from detectron2.config import get_cfg
from detectron2.utils.visualizer import Visualizer, ColorMode
from detectron2.data import MetadataCatalog, DatasetCatalog
from detectron2.data.datasets import register_coco_instances
## データ準備
for data_type in ["train","val","test"]:
    reg_name=f"mnist_detection_{data_type}"
    register_coco_instances(reg_name, {}, f"coco_{data_type}.json", "")
## 学習
cfg = get_cfg()
cfg.merge_from_file(model_zoo.get_config_file("COCO-Detection/faster_rcnn_R_50_FPN_3x.yaml"))
cfg.DATASETS.TRAIN = ("mnist_detection_train",)
cfg.DATASETS.TEST = ()
cfg.DATALOADER.NUM_WORKERS = 2
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url("COCO-Detection/faster_rcnn_R_50_FPN_3x.yaml")
cfg.SOLVER.IMS_PER_BATCH = 2
cfg.SOLVER.BASE_LR = 0.0004
cfg.SOLVER.MAX_ITER = (1000)
cfg.MODEL.ROI_HEADS.BATCH_SIZE_PER_IMAGE = (128)
cfg.MODEL.ROI_HEADS.NUM_CLASSES = 10
os.makedirs(cfg.OUTPUT_DIR, exist_ok=True)
trainer = DefaultTrainer(cfg)
trainer.resume_or_load(resume=False)
trainer.train()
実行がおわったら、どれぐらい学習できているか確認します。
cfg.MODEL.WEIGHTS = os.path.join(cfg.OUTPUT_DIR, "model_final.pth")
cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.6
predictor = DefaultPredictor(cfg)
mnist_detection_metadata = MetadataCatalog.get("mnist_detection_test")
from random import randint
for _ in range(3):
    t = randint(1,2000)
    print(f"images/test_{str(t).zfill(6)}.jpg")
    im = cv2.imread(f"images/test_{str(t).zfill(6)}.jpg")
    outputs = predictor(im)
    v = Visualizer(im[:, :, ::-1],
                   metadata=mnist_detection_metadata,
                   scale=1.0
    )
    v = v.draw_instance_predictions(outputs["instances"].to("cpu"))
    cv2_imshow(v.get_image()[:, :, ::-1])
モデルが予測した値が60%以上のものについてBBOXを出しています。
きちんと検知できているのもあれば、見逃しているのもあります。
3. 評価
COCO Evaluatorで評価してみます。
from detectron2.evaluation import COCOEvaluator, inference_on_dataset
from detectron2.data import build_detection_test_loader
evaluator = COCOEvaluator("mnist_detection_val", output_dir="./output")
val_loader = build_detection_test_loader(cfg, "mnist_detection_val")
print(inference_on_dataset(predictor.model, val_loader, evaluator))
# another equivalent way to evaluate the model is to use `trainer.test`
AP(Average Precision)とはざっくり言えばPrecision-Recall曲線のAUCのことです。
計算方法や対象の大きさで複数種類あります。
| AP | AP50 | AP75 | APs | APm | APl | 
|---|---|---|---|---|---|
| 46.885 | 62.038 | 60.541 | 46.895 | nan | nan | 
| 用語 | |
|---|---|
| AP | IoUを0.5から0.95まで0.05刻みで動かして計算したAPの平均 | 
| AP50 | IoU>0.5で計算したAP | 
| AP75 | IoU>0.75で計算したAP | 
| APs | 小さい物体に対するAP | 
| APm | 中くらいの物体に対するAP | 
| APl | 大きい物体に対するAP | 
| IoU | 正解BBOXと予測BBOXのUnionに対するIntersectionの割合。正解と予測がぴったり重なれば1、重なりがなければ0。たとえば「IoU>0.5で計算したAP」とは、IoUが0.5を超える予測のみを正しい予測として計算したAPのこと | 
今回の検出対象はすべて小さい物体に分類されるのでAPmとAPlはnanになっています。
クラスごとのAPは以下。
| category | AP | category | AP | category | AP | 
|---|---|---|---|---|---|
| zero | 67.623 | one | 47.831 | two | 38.955 | 
| three | 0.000 | four | 72.527 | five | 23.396 | 
| six | 51.118 | seven | 58.211 | eight | 48.638 | 
| nine | 60.546 | 
見ると、クラス3のAPが0になっています。他は2と5の精度が低いです。おそらく下の画像のように、「3を5と認識している」「2と5の見分けが難しい」が原因だと思います。
 
  
気が向けば改善したいと思います。
参考
・COCOフォーマットについて
・APについて
https://jonathan-hui.medium.com/map-mean-average-precision-for-object-detection-45c121a31173
・detectron2チュートリアル
https://colab.research.google.com/drive/16jcaJoc6bCFAQ96jDe2HwtXj7BMD_-m5




