“华为云-东吴杯”比赛经历记录总结

前言

目标检测的比赛是真的少,研一时候因为疫情在家,荒废了。研二想着赶快补一补,想着参加一下比赛什么的,但是很多比赛都是一年一届,报名时间很短,错过许多。最近正好看到这个华为举办的比赛,虽然也不是什么大比赛,但咱也知足了,本来就是为了学到东西,大比赛牛人更多,更让人绝望。
在这里插入图片描述
如图,虽说这个比赛是缺陷检测,但是和目标检测基本一样,懂得都懂。。
比赛链接:
https://competition.huaweicloud.com/information/1000041490/circumstance

导入模型

这一步很简单,就是读取yaml文件。我用了yolov5。

def get_object_detector(num_classes):
    # load an instance segmentation model pre-trained pre-trained on COCO
    # logger.info('remove pretrained')
    # model = torchvision.models.detection.fasterrcnn_resnet50_fpn(
    #     pretrained=False, pretrained_backbone=False)
    # # get number of input features for the classifier
    # in_features = model.roi_heads.box_predictor.cls_score.in_features
    # # replace the pre-trained head with a new one
    # logger.info('{}-{}'.format(in_features, num_classes))
    # model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
    logger.info('work_dir' + os.getcwd())
    model = Model('/home/mind/model/yolov5m.yaml', nc=num_classes) # 因为在云端相对路径和本地不一样,所以我这里直接用绝对路径。官方的开发文档给了教程,懒得再改了。
    return model

其实这一步完全不需要。baseline写这个函数只是为了修改检测头的输出类别数,也就是这一行:

model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)

因为baseline是先加载模型字典,再加载参数:

self.model.load_state_dict(torch.load(self.model_path,map_location='cpu')['model'])                                           

我们也可以一次性直接加载整个模型:

self.model = attempt_load(self.model_path, map_location='cpu')

这个加载方法是yolov5重写的:

def attempt_load(weights, map_location=None, inplace=True):
    from models.yolo import Detect, Model

    # Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a
    model = Ensemble()
    for w in weights if isinstance(weights, list) else [weights]:
        ckpt = torch.load(w, map_location=map_location)  # load
        model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().fuse().eval())  # FP32 model

    # Compatibility updates
    for m in model.modules():
        if type(m) in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU, Detect, Model]:
            m.inplace = inplace  # pytorch 1.7.0 compatibility
        elif type(m) is Conv:
            m._non_persistent_buffers_set = set()  # pytorch 1.6.0 compatibility

    if len(model) == 1:
        return model[-1]  # return model
    else:
        print(f'Ensemble created with {weights}\n')
        for k in ['names']:
            setattr(model, k, getattr(model[-1], k))
        model.stride = model[torch.argmax(torch.tensor([m.stride.max() for m in model])).int()].stride  # max stride
        return model  # return ensemble

到了这里,就涉及到yolov5的坑了:
v5加载模型的时候会加载路径,检查当前路径下有没有models这个module。即使你自己构建文件夹,也必须按照他的目录结构来使用yolo.py。大坑。
网上的一些解决方法。这个博客是正确的。
https://blog.csdn.net/weixin_41809530/article/details/116446002

最终我的目录结构:
在这里插入图片描述
因为这两个文件是模型中会用到的,所以必须这样。很烦。

数据处理

刚开始不了解,把数据集下载到本地,居然还花钱。。几块钱也就交了。
官方给了baseline的教程,用训练作业,云端训练。云端解压,不需要下载到本地。

我们应该清楚,一般模型传入的是tensor,ndarray等格式,也就是矩阵。而把图片处理成这样的格式就用到了opencv,PIL等图像处理包。

这里有一点,PIL支持读取字节流,如下。baseline用的PIL,我用了opencv,会报错。

Image.open(io.BytesIO(image))

1-PIL转换opencv

image = Image.open(io.BytesIO(image))
img = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)

2-坐标变换

模型输出的是一个tensor向量,我们要得到的对应原图尺寸的真实坐标。这时候就用到了v5中的一个函数,scale_coords()

scale_coords()

def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
    # Rescale coords (xyxy) from img1_shape to img0_shape
    if ratio_pad is None:  # calculate from img0_shape
        gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])  # gain  = old / new
        pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2  # wh padding
    else:
        gain = ratio_pad[0][0]
        pad = ratio_pad[1]

    coords[:, [0, 2]] -= pad[0]  # x padding
    coords[:, [1, 3]] -= pad[1]  # y padding
    coords[:, :4] /= gain
    clip_coords(coords, img0_shape)
    return coords

分别讲一下传入的参数的含义。
其中img0_shape顾名思义就是image_original,也就是原始图片的尺寸,

img0 = cv2.imread(file)

而这里的img1_shape对应的是resize完的图片,v5中用到的函数是latterbox()。所以也就是image_letterbox()。这个函数中封装了cv2.resize().包括了padding等操作。
在卷积前对图像的操作都分离开,应该是有利于加快训练的速度,应该对推理也有帮助。

letterbox()

def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32):
    # Resize and pad image while meeting stride-multiple constraints
    shape = img.shape[:2]  # current shape [height, width]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # Scale ratio (new / old)
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    if not scaleup:  # only scale down, do not scale up (for better test mAP)
        r = min(r, 1.0)

    # Compute padding
    ratio = r, r  # width, height ratios
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh padding
    if auto:  # minimum rectangle
        dw, dh = np.mod(dw, stride), np.mod(dh, stride)  # wh padding
    elif scaleFill:  # stretch
        dw, dh = 0.0, 0.0
        new_unpad = (new_shape[1], new_shape[0])
        ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]  # width, height ratios

    dw /= 2  # divide padding into 2 sides
    dh /= 2

    if shape[::-1] != new_unpad:  # resize
        img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)  # add border
    return img, ratio, (dw, dh)

np.mod()

numpy.mod(arr1, arr2,  /, out=None, *, where=True, casting=’same_kind’, order=’K’, dtype=None, subok=True[, signature, extobj], ufunc ‘remainder’)

numpy.mod()是一个用于在numpy中进行数学运算的函数,它返回两个数组arr1和arr2之间的除法元素余数,即 arr1 % arr2 当arr2为0且arr1和arr2都是整数数组时,返回0。
参数可以是单个数字,也可以是数组,也就是列表。

cv2.resize()

我们可以看到v5使用的resize方法是cv2.INTER_LINEAR
关于resize的详细使用可以看官网https://docs.opencv.org/
或者这个博客,也就是翻译了一下。
https://blog.csdn.net/wzhrsh/article/details/101630396

if shape[::-1] != new_unpad:  # resize
        img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)

img是我们的原始图像。
这里要讲一下,cv.resize()会将图像shape中的高宽交换。我们用cv2.imread()读一张图片:

import cv2

img = cv2.imread('zidane.jpg')
print(img.shape)

(720, 1280, 3) 对应(h,w,c)

我们再resize一下:

img1 = cv2.resize(img,(720,1280))
print(img1.shape)

(1280, 720, 3) 

可以看到宽高互换了。我们也可以理解为resize的输入参数顺序为(w,h)也就是输出的图片的(w,h)正好与图片的shape顺序相反。有点绕。。

我们再看这个插值方法:interpolation=cv2.INTER_LINEAR。双线性插值,是默认方法,这里写出来只是为了直观。
也有一些别的方法,但是不了解区别。

最后又用了cv2.copyMakeBorder()
参考链接:https://blog.csdn.net/qq_36560894/article/details/105416273

img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)  # add border

其实这一步加不加都不影响代码运行,但是可能会对AP有影响。

到这里对图像的处理就完成了,

自定义推理代码

推理代码高了我好久,主要是这个service后还不能在本地跑。因为有的包是华为云自己的。所以我只能修改一次上传部署一次。每次都得十多分钟,这么折腾了两三天才把service配好。
懒得讲了,没什么意义。这里留作纪念贴一下。

# -*- coding: utf-8 -*-
import os
import torch
import torchvision
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from model_service.pytorch_model_service import PTServingBaseService

import time
from metric.metrics_manager import MetricsManager
from torchvision.transforms import functional as F
import log
import json

from general import non_max_suppression,scale_coords
from models.yolo import Model
from experimental import attempt_load
from datasets import LoadImages

from datasets import letterbox
from PIL import Image
import cv2
import numpy as np


Image.MAX_IMAGE_PIXELS = 1000000000000000
logger = log.getLogger(__name__)

logger.info(torch.__version__)
logger.info(torchvision.__version__)


def get_object_detector(num_classes):
    # load an instance segmentation model pre-trained pre-trained on COCO
    # logger.info('remove pretrained')
    # model = torchvision.models.detection.fasterrcnn_resnet50_fpn(
    #     pretrained=False, pretrained_backbone=False)
    # # get number of input features for the classifier
    # in_features = model.roi_heads.box_predictor.cls_score.in_features
    # # replace the pre-trained head with a new one
    # logger.info('{}-{}'.format(in_features, num_classes))
    # model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
    print(os.getcwd())
    logger.info('work_dir' + os.getcwd())
    model = Model('/home/mind/model/yolov5m.yaml', nc=num_classes)
    return model


class ImageClassificationService(PTServingBaseService):
    def __init__(self, model_name, model_path, **kwargs):
        self.model_name = model_name
        num_classes = 10
        logger.info(' {} '.format(model_path))
        for key in kwargs:
            logger.info('{}-{}'.format(key, kwargs[key]))
        self.model_path = model_path
        self.model = get_object_detector(num_classes)
        self.use_cuda = False
        self.device = torch.device("cuda"
                                   if torch.cuda.is_available() else "cpu")
        if torch.cuda.is_available():
            logger.info('Using GPU for inference')
            self.model.to(self.device)
            self.model = attempt_load(self.model_path)
        else:
            logger.info('Using CPU for inference')
            self.model = attempt_load(self.model_path, map_location='cpu')
        self.model.eval()
        print("model already")

    def _preprocess(self, data):

        return data

    def _inference(self, data):
        preprocessed_data = {}
        for k, v in data.items():
            print(k, v)
            for file_name, file_content in v.items():
                img = Image.open(file_content).convert("RGB")
                img0 = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)  # BGR
                img = letterbox(img0)[0]

                # Convert
                img = img[:, :, ::-1].transpose(2, 0, 1)  # BGR to RGB, to 3x416x416
                img = np.ascontiguousarray(img)

                img = torch.from_numpy(img).to(self.device)
                img = img.float()  # uint8 to fp16/32
                img /= 255.0  # 0 - 255 to 0.0 - 1.0
                if img.ndimension() == 3:
                    img = img.unsqueeze(0)

                preprocessed_data[k] = img

        data = preprocessed_data
        img = data["input_img"]
        data = img
        pred = self.model(data)[0]
        pred = non_max_suppression(pred, conf_thres=0.25, iou_thres=0.45)
        for i, det in enumerate(pred):
            det[:, :4] = scale_coords(img.shape[2:], det[:, :4], img0.shape)

            pred[i] = {'boxes': det[:, :4].cpu().detach().numpy().tolist(),
                       'labels': (det[:, 5:6].cpu().detach().int() + 1).numpy().flatten().tolist(),
                       'scores': det[:, 4:5].cpu().detach().numpy().flatten().tolist()}

        results = {'result': pred}
        return results

    def _postprocess(self, data):
        return data

    def inference(self, data):
        pre_start_time = time.time()
        data = self._preprocess(data)
        infer_start_time = time.time()
        # Update preprocess latency metric
        pre_time_in_ms = (infer_start_time - pre_start_time) * 1000
        logger.info('preprocess time: ' + str(pre_time_in_ms) + 'ms')
        if self.model_name + '_LatencyPreprocess' in MetricsManager.metrics:
            MetricsManager.metrics[self.model_name + '_LatencyPreprocess'].update(pre_time_in_ms)
        data = self._inference(data)
        infer_end_time = time.time()
        infer_in_ms = (infer_end_time - infer_start_time) * 1000
        logger.info('infer time: ' + str(infer_in_ms) + 'ms')
        data = self._postprocess(data)
        # Update inference latency metric
        post_time_in_ms = (time.time() - infer_end_time) * 1000
        logger.info('postprocess time: ' + str(post_time_in_ms) + 'ms')
        if self.model_name + '_LatencyInference' in MetricsManager.metrics:
            MetricsManager.metrics[self.model_name + '_LatencyInference'].update(post_time_in_ms)
        # Update overall latency metric
        if self.model_name + '_LatencyOverall' in MetricsManager.metrics:
            MetricsManager.metrics[self.model_name + '_LatencyOverall'].update(pre_time_in_ms + post_time_in_ms)
        logger.info('latency: ' + str(pre_time_in_ms + infer_in_ms + post_time_in_ms) + 'ms')
        data['latency_time'] = pre_time_in_ms + infer_in_ms + post_time_in_ms
        return data

其实完全不需要写的这么麻烦。
是自己没看教程。v5官网写了加载自己模型的方法,用torch.hub.load()

model = torch.hub.load('ultralytics/yolov5', 'custom', path='path/to/best.pt')  # default
model = torch.hub.load('path/to/yolov5', 'custom', path='path/to/best.pt', source='local')  # local repo

但是懒得改了。

本地训练

因为我只有一块8gb的2070。。所以我先用的v5m,batch-size用的是1。。2都跑不起来。epoch为300。但是无奈用上v5速度还是太慢,一次一个小时多,300个epoch大概要用15天。。这块不需要讲什么,直接训练就行。

数据集划分

这里又学到一个函数,pytorch自带的。之前我都是按顺序划分的训练集验证集。。惭愧。。

torch.utils.data.random_split()

函数名:torch.utils.data.random_split()

dataset = os.listdir("E:\\qxdetection\\data\\images")

train_dataset, test_dataset = random_split(
    dataset=dataset,
    lengths=[4310, 333],  # 两者和必须和数据集数量严格相等
    generator=torch.Generator().manual_seed(0)
)

需要注意的是lengths,只能划分为两部分,你可以先划分训练集,测试集,然后拿得到的训练集再划分训练集,验证集。同时lengths的两部分数字的和必须等于你自己数据集的总量。这点比较坑,居然不能拿比例替代。
总共4643张,因为v5跑的时候必须又验证集。所以我简单的划分了一下。
先用v5m训练了几十个epoch,service跑通后开始换成v5l6。
6指的是总共有6层,从3层FPN变到4层FPN结构。

后记

最近事情太多,过了好久发现网页上这篇文章还是打开的,没写完。。。。给这篇文章划个句号吧。
这次比赛还是吸取到很多经验的,总结一下。

硬件

别人同样的v5模型,batch-size 64,image-size 1280,甚至原尺寸,我一块8g2070,batch-size 1,玩个毛。。。没得比,同样的模型人家比我高十几个点,训练时间比你少几十倍。

关于base

即使到了2021,大部分的baseline仍然用的fastrcnn或者比较老的模型,这是因为这些模型是pytorch模型官方库自带的,不需要写太多代码,方便参赛人员自定义,而且方便人们较短的时间内实现提升,有的baseline的conf,iou-thres都是故意往差了改,其实你把这些一改就能获得提升。但是到了后期想冲名次肯定不行。肯定是最新的模型,这次比赛前十基本都是transformer,主要检测相关的比赛还是以精度为准,并不把速度也当作一个成绩指标。因为精度在不同的机器上是一样的,但是速度每台机器都有差别。。

就这么多吧,要研三了,赶快开始找工作了,写毕业论文了。。可惜没进前五十,我觉得拿v5p6梭哈也能进前50。v5实在太优秀了。

顺便贴一下我的改进,欢迎大家尝试:https://github.com/SpongeBab/yolov5

就是把PANet变成四层了。。精度有提升,速度当然降低了(🤦‍。。)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值