Blog Article

YOLOv3を使用してじゃんけんの手を判別する

投稿日時
更新日時

はじめに

YOLOv3を実装する機会があったので、それを用いてじゃんけんの手を判別するシステムを作成してみようと思います。

YOLOv3の論文 : https://arxiv.org/abs/1804.02767

実装したリポジトリ : https://github.com/atsushi11o7/YOLOv3

実装の参考にした記事 : https://zenn.dev/opamp/articles/5198d6bf369b8e

実験

モデルは以下のように実装しました。

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.modules.batchnorm import BatchNorm2d

class ResidualBlock(nn.Module):
    def  __init__(self, num_filters, num_blocks):
        super().__init__()

        self.block_list = nn.ModuleList()
        for _ in range(num_blocks):
            self.block_list.append(
               nn.Sequential(
                nn.Conv2d(num_filters, num_filters//2, 1, 1),
                nn.BatchNorm2d(num_filters//2),
                nn.LeakyReLU(),
                nn.Conv2d(num_filters//2, num_filters, 3, 1, 1),
                nn.BatchNorm2d(num_filters),
                nn.LeakyReLU()
            )
        )

    def forward(self, x):
        for block in self.block_list:
            x = x + block(x)

        return x

class Darknet53(nn.Module):
    def __init__(self):
        super(Darknet53, self).__init__()

        self.darknet26 = nn.Sequential(
            nn.Conv2d(3, 32, 3, 1, 1),
            nn.BatchNorm2d(32),
            nn.LeakyReLU(),

            nn.Conv2d(32, 64, 3, 2, 1),
            nn.BatchNorm2d(64),
            nn.LeakyReLU(),

            ResidualBlock(64, 1),

            nn.Conv2d(64, 128, 3, 2, 1),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(),

            ResidualBlock(128, 2),

            nn.Conv2d(128, 256, 3, 2, 1),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(),

            ResidualBlock(256, 8)
        )

        self.darknet43 = nn.Sequential(
            nn.Conv2d(256, 512, 3, 2, 1),
            nn.BatchNorm2d(512),
            nn.LeakyReLU(),

            ResidualBlock(512, 8)
        )

        self.darknet52 = nn.Sequential(

            nn.Conv2d(512, 1024, 3, 2, 1),
            nn.BatchNorm2d(1024),
            nn.LeakyReLU(),

            ResidualBlock(1024, 4)
        )

        self.darknet_final_layer = nn.Sequential(
            nn.Conv2d(1024, 1000, 1, 1, 0),
            nn.BatchNorm2d(1000),
            nn.LeakyReLU(),

            nn.AdaptiveAvgPool2d((1, 1)),
            nn.Flatten()
        )

    def forward(self, x):
        x = self.darknet26(x)
        x = self.darknet43(x)
        x = self.darknet52(x)
        x = self.darknet_final_layer(x)
        return x
# YOLOv3の実装

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.modules.batchnorm import BatchNorm2d

from Darknet53 import Darknet53

class ConvBlock(nn.Module):
    def __init__(self, input_block, output_block, num_blocks):
        super(ConvBlock, self).__init__()

        conv_block = nn.ModuleList()
        for block in range(num_blocks):
            in_channels = input_block if block == 0 else output_block
            conv_block.append(
                nn.Sequential(
                    nn.Conv2d(in_channels, output_block//2, 1, 1),
                    nn.BatchNorm2d(output_block//2),
                    nn.LeakyReLU(),
                    nn.Conv2d(output_block//2, output_block, 3, 1, 1),
                    nn.BatchNorm2d(output_block),
                    nn.LeakyReLU()
                )
            )
        self.conv_block = nn.Sequential(*conv_block)

    def forward(self, x):
        return self.conv_block(x)

class YOLOv3(nn.Module):
    def __init__(self, num_class = 1):
        super(YOLOv3, self).__init__()

        self.darknet26 = Darknet53().darknet26
        self.darknet43 = Darknet53().darknet43
        self.darknet52 = Darknet53().darknet52

        self.conv_block = ConvBlock(1024, 1024, 3)
        self.scale3_YOLO_layer = nn.Conv2d(1024, (3 * (4 + 1 + 3)), 1, 1)

        self.scale2_upsampling = nn.Conv2d(1024, 256, 1, 1)
        self.scale2_conv_block = ConvBlock(768, 512, 3)
        self.scale2_YOLO_layer = nn.Conv2d(512, (3 * (4 + 1 + 3)), 1, 1)

        self.scale1_upsampling = nn.Conv2d(512, 128, 1, 1)
        self.scale1_conv_block = ConvBlock(384, 256, 3)
        self.scale1_YOLO_layer = nn.Conv2d(256, (3 * (4 + 1 + 3)), 1, 1)

        self.upsample = nn.Upsample(scale_factor = 2)

    def forward(self, x):
        x1 = self.darknet26(x)
        x2 = self.darknet43(x1)
        x3 = self.darknet52(x2)

        x3 = self.conv_block(x3)
        scale3_output = self.scale3_YOLO_layer(x3)

        scale2_upsample = self.upsample(self.scale2_upsampling(x3))
        x2 = torch.cat((x2, scale2_upsample), dim=1)
        x2 = self.scale2_conv_block(x2)
        scale2_output = self.scale2_YOLO_layer(x2)

        scale1_upsample = self.upsample(self.scale1_upsampling(x2))
        x1 = torch.cat((x1, scale1_upsample), dim=1)
        x1 = self.scale1_conv_block(x1)
        scale1_output = self.scale1_YOLO_layer(x1)

        return scale3_output, scale2_output, scale1_output

以下の画像はアノテーションした矩形を可視化したものです

かなり少ないですが、一つの手に対して6枚、計18枚の画像を学習に用いました。

最適化アルゴリズムはAdam、エポック数は100で試してみました。

以下は損失の経過です

損失関数は以下のように実装しました。

def YOLOv3_loss_function(pred_list, targets_list, lambda_coord=0.1, lambda_obj=1.0, lambda_class=1.0, lambda_noobj=0.01):

    coord_loss = 0
    obj_loss = 0
    class_loss = 0
    noobj_loss = 0

    B = pred_list[0].size(0)

    for i in range(3):
        pred = pred_list[i]
        targets = targets_list[i]
        for j in range(3):
            pred_cut = pred[:, j*(4+1+3):(j+1)*(4+1+3), :, :]  # predの分割
            pred_boxes = pred_cut[:, :4, :, :]  # 予測されたbboxの座標 (tx, ty, tw, th)
            pred_conf_obj = pred_cut[:, 4, :, :].unsqueeze(dim=1)  # 予測されたbboxの確信度
            pred_classes = pred_cut[:, 5:, :, :]  # クラスのスコア

            targets_cut = targets[:, j*(4+1+3):(j+1)*(4+1+3), :, :]  # targetsの分割
            targets_boxes = targets_cut[:, :4, :, :]  # targetsのbboxの座標 (tx, ty, tw, th)
            targets_obj_mask = targets_cut[:, 4, :, :].unsqueeze(dim=1)  # オブジェクトマスク
            targets_classes = targets_cut[:, 5:, :, :]  # targetsのスコア

            # 位置の損失を計算
            #coord_loss += lambda_coord * nn.MSELoss(reduction='sum')(targets_obj_mask * pred_boxes, targets_boxes)
            coord_loss += lambda_coord * torch.sum(torch.square(pred_boxes - targets_boxes) * targets_obj_mask)

            # オブジェクトの損失を計算
            #obj_loss += lambda_obj * nn.BCEWithLogitsLoss(reduction='sum')(targets_obj_mask * pred_conf_obj, targets_obj_mask)
            obj_loss += lambda_obj * torch.sum(-1 * torch.log(torch.sigmoid(pred_conf_obj)+ 1e-7) * targets_obj_mask)

            # クラスの損失を計算
            class_loss += lambda_class * torch.sum(targets_obj_mask * F.binary_cross_entropy(torch.sigmoid(pred_classes), targets_classes))

            # ノンオブジェクトの損失を計算
            #noobj_loss += lambda_noobj * nn.BCEWithLogitsLoss(reduction='sum')((1 - targets_obj_mask) * (1 - pred_conf_obj), 1 - targets_obj_mask)
            noobj_loss += lambda_noobj * torch.sum((-1 * torch.log(1 - torch.sigmoid(pred_conf_obj)+ 1e-7)) * (1 - targets_obj_mask))

    loss = coord_loss + obj_loss + class_loss + noobj_loss

    return loss/B, coord_loss/B, obj_loss/B, class_loss/B, noobj_loss/B

結果

以下の画像は結果の一例です。

大きい枠はチョキと判定していて、小さい枠はグーと判定しています。

パーは比較的高精度で判定できたのですが、かなり甘い結果になりました。

18枚しか学習に使用してない割にはうまくいったのでしょうか?

Contact

準備中@web.mail.address