当前位置:   article > 正文

基于YOLOv5的中式快餐店菜品识别系统_食物识别算法

食物识别算法

基于YOLOv5的中式快餐店菜品识别系统[金鹰物联智慧食堂项目]

headline


摘要

本文基于YOLOv5v6.1提出了一套适用于中式快餐店的菜品识别自助支付系统,综述了食品识别领域的发展现状,简要介绍了YOLOv5模型的历史背景、发展优势和网络结构。在数据集预处理过程中,通过解析UNIMIB2016,构建了一套行之有效的标签格式转换与校验流程,解决了YOLOv5中文件路径问题、标签格式转换问题和因EXIF信息的存在而导致的标记错位问题。在模型训练阶段,配置了云服务器,引入了Weights and Bias可视化工具,实现了在线监督训练和sweep超参数调优的功能,在sweep中使用hyperband剪枝算法加速了sweep过程,并且给出了对于训练过程中可能出现的问题的解决方法。最后介绍了目标识别领域的评价指标和YOLOv5的损失函数,分析了sweep超参数调优的结果,选取最优参数组合训练模型,通过分析样本分布、PR曲线等,选取最佳预测置信度,大幅提升了预测精度和召回率,部署了模型并制作了客户端


引言

随着智能信息化时代的到来,人工智能与传感技术取得了巨大进步,在智能交通、智能家居、智能医疗等民生领域产生积极正面影响。其中,社交网络、移动网络和物联网等新兴技术产生了食品大数据,这些大数据与人工智能,尤其是快速发展的深度学习催生了新的交叉研究领域食品计算。现在,在智慧健康、食品智能装备、智慧餐饮、智能零售及智能家居等方面都可以找到食品大数据与人工智能相结合的例子。

人工智能时代下的食品图像识别是当前计算机视觉研究的重要领域之一。我们希望研发一种可快速且高效识别菜品的校园菜肴识别系统,在校园食堂中应用本系统,可缩短收银员计算价格的时间、简化收银步骤;可协助管理者精准备餐、减少库存的浪费;就餐者还可以即时看见摄入的食物营养价值,实现膳食平衡;另外,可迅速实现食品的安全溯源,避免出现食品安全情况。

传统的食物图像识别方法是选择图像特征,然后使用某些方法(比如SIFT、HOG)提取图像特征点,再将特征点用矢量表示,最后采用机器学习的方法训练分类器(如SVM、K-Means)。传统食物图像识别提取特定特征或者关键点对食物进行分类,但在实际应用中,拍摄的图像会受到环境的光照强度、噪声干扰、环境光等外部因素的干扰,导致拍摄图像质量参差,从而影响最终的检测结果同一事物的颜色形状会有差异,不同食物直接的颜色形状也会相同。所以传统的图像识别方法很难准确识别出食物。

深度学习的发展使得当前大部分工作均采用卷积神经网络,思路是先对菜品图像中不同的菜品区域进行检测或分割,然后对其区域进行识别。从2014年开始,基于深度学习的目标检测网络井喷式爆发,先是二阶段网络,如R-CNN、Fast-RCNN、Mask-RCNN等,自2016年Joseph等提出You only Look Once(YOLOv1)以来学者者们的视野,开启了单阶段目标检测网络的新纪元。YOLO均是对单阶段目标检测模型改进的研究,为各研究领域提供了更快、更好的目标检测方法,也为单阶段目标检测算法的实际应用提供了重要理论保障。例如 Aguilar 等人微调物体检测算法 YOLOv2 来进行多种食物检测和识别。又如 Pandey 等人微调了 AlexNet、GoogLeNet、ResNet等三种CNN网络,然后基于微调的网络提取和融合来自不同网络的视觉特征,通过集成学习方法实现菜品图像识别。随着深度学习的发展,卷积神经网络(CNN)在各领域中获得不俗的效果,菜品识别也围绕卷积神经网络展开研究,不仅提出了新的方法,也提升了检测精度。

2020 年 6 月 10 日 YOLOv5 发布,随着版本迭代更新,其已成为现今最先进的目标检测技术之一。YOLOv5 使用Pytorch框架,对用户非常友好,能够方便地训练自己的数据集;能够直接对视频甚至网络摄像头端口输入进行有效推理,有着高达140FPS的目标识别速度;能够轻松的将Pytorch权重文件转化为安卓使用的ONXX格式,或者通过CoreML转化为IOS格式,以便直接部署到手机应用端。

YOLO的核心思想就是将整张图片作为网络的输入,利用“分而治之”的思想,对图片进行网格划分,直接在输出层回归边界框的检测位置及其所属的类别。与Faster R-CNN相比,YOLO产生的背景错误要少得多。通过使用YOLO来消除Faster RCNN的背景检测,可以显着提高模型性能。实验表明YOLO v5可以达到比Faster R-CNN更快的收敛速度,并且在小目标的检测上比SSD模型更加准确。

数据集

数据集来源和说明

本文所使用的托盘食物数据集来源于 UNIMIB2016 Food Database. 此数据集在真实餐厅环境中收集而来,每张照片的尺寸为 (3264, 2448),包含一个托盘和托盘上不同的食物,有些食物放在餐具垫上而非碟子中。有时,多种菜会被放置在同一碟子中,这给图像分割带来了困难。此外,图像畸变和光线环境等影响也会给分割和识别带来挑战。

The dataset has been collected in a real canteen environment. The particularities of this setting are that each image depicts different foods on a tray, and some foods (e.g. fruit, bread and dessert) are placed on the placemats rather than on plates. Sides are often served in the same plate as the main dish making it difficulty to separate the two. Moreover, the acquisition of the images has been performed in a semi-controlled settings so the images present visual distortions as well as illumination changes due to shadows. These characteristics make this dataset challenging requiring both the segmentation of the trays for food localization, and a robust way to deal with multiple foods.

在这里插入图片描述

在这里插入图片描述

如图3所示,在数据集中,许多类别的食物非常相似,例如,有四种不同的“Pasta al sugo”,其中添加了其他主要成分(如鱼肉、蔬菜或者其他的一些肉类)。最后,托盘上可能有其他物品造成干扰,比如有智能手机、钱包、校园卡等等。

Figure 3, many food classes have a very similar appearance. For example, we have four different “Pasta al sugo”, but with other main ingredients (e.g. fish, vegetables, or meat) added. Finally, on the tray there can be other “noisy” objects that must be ignored during the recognition. For example, we may find cell phones, wallets, id cards, and other personal items. For these reasons we need to design of a very accurate recognition algorithm.

在这里插入图片描述

数据集处理

作者团队一共收集了1442张照片,去除模糊和重复照片后,将剩余有效图片保存在UNIMIB2016-images中。其中,包含1027张照片,共计73种菜品,总计3616个菜品实例。一些种类的食物只是在成分上有所不同,所以命名为“FoodName 1”, “FoodName 2”.

接下来,处理UNIMIB2016-annotations.zip中的annotations.mat文件,将其转换为yolo格式。

UNIMIB2016-annotations中,存有annotations.mat标记文件,.mat文件是Matlab的Map对象(Map object),其介绍如下:

A Map object is a data structure that allows you to retrieve values using a corresponding key. Keys can be real numbers or character vectors. As a result, they provide more flexibility for data access than array indices, which must be positive integers. Values can be scalar or nonscalar arrays.

MAT文件解析

若使用scipy.io.loadmat工具解析.mat文件,如需要加载annotations.mat,在Map object多级嵌套时,解析可能出现意想不到的错误,故编写Matlab脚本将annotations.mat文件解析为YOLOv5所需的标记文件格式。

% .
% ├── annotations.mat
% ├── demo.m
% ├── formatted_annotations
% │   ├── 20151127_114556.txt
% │   ├── 20151127_114946.txt
% │   ├── 20151127_115133.txt
% │   ├── ...
% │   └── 20151221_135642.txt
% └── load_annotations.m

%% load_annotations.m

clc; clear;

% output path
output = './formatted_annotations/';

% Load the annotations in a map structure
load('annotations.mat');

% Each entry in the map corresponds to the annotations of an image.
% Each entry contains many cell tuples as annotated food
% A tuple is composed of 8 cells with the annotated:
% - (1) item category (food for all tuples)
% - (2) item class (e.g. pasta, patate, ...)
% - (3) item name
% - (4) boundary type (polygonal for all tuples)
% - (5) item's boundary points [x1,y1,x2,y2,...,xn,yn]
% - (6) item's bounding box [x1,y1,x2,y2,x3,y3,x4,y4]

image_names = annotations.keys;

n_images = numel(image_names);

for j = 1 : n_images
    
    image_name = image_names{j};
    tuples = annotations(image_name);
    count = size(tuples,1);
    coordinate_mat = cell2mat(tuples(:,6));
    
    % open file
    file_path = [output image_name '.txt'];
    ffile = fopen(file_path, 'w');
    
    % write file
    for k = 1 : count
        item = tuples(k,:);
        fprintf(ffile, '%s %d %d %d %d %d %d %d %d\n', ...
            string(item(2)), ...  % item class
            coordinate_mat(k,:)); % item's bounding box
    end
    
    % close file
    fclose(ffile);
    
end

%% fprintf
% Write data to text file
% https://www.mathworks.com/help/matlab/ref/fprintf.html
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

运行上述Matlab脚本文件,在./formatted_annotations文件夹下生成以图片名命名的*.txt文件,每一行的格式为class x1 y1 x2 y2 x3 y3 x4 y4.

在这里插入图片描述

bounding box如图所示:(xy1左上,xy3右下)

在这里插入图片描述

数据集有效性检验

下载并解压 [UNIMIB2016-images.zip]./original文件夹内为所有图片数据。将 original文件夹重命名为images,今后该文件夹用来存放图片数据,否则YOLOv5模型训练会发生错误,具体原因请看 一文彻底解决YOLOv5训练找不到标签问题。编写check_dataset.py,检查formatted_annotations中标签文件是否和images中图像文件一一对应,删除无效的标签和不匹配的标签。

# UNIMIB2016
# ├── UNIMIB2016-annotations
# │   ├── check_dataset.py <--
# │   └── formatted_annotations
# └── images

# check_dataset.py

import os

# path of formatted_annotations
f_path = os.path.join(os.getcwd(), 'formatted_annotations')

# path of images
img_path = os.path.join(os.getcwd(), os.pardir, 'images')


def check_dataset():
    annotations = [i[:-4] for i in os.listdir(f_path)]
    imgs = [i[:-4] for i in os.listdir(img_path)]

    for annotation in annotations:
        label = annotation + '.txt'
        label_path = os.path.join(f_path, label)

        try:
            if annotation not in imgs:
                # remove annotation which is not in images
                print('not found image: {}, remove its annotation'.format(annotation))
                print(label_path)
                raise FileExistsError

            else:
                # check extra spaces in a line
                with open(label_path) as f:
                    lines = f.readlines()
                    for line in lines:
                        item = line.split()
                        if len(item) > 9:
                            print('wrong label format: {}, {}'.format(annotation, line))
                            raise FileExistsError

        except FileExistsError:
            os.remove(label_path)
            print('os.remove({})'.format(label_path))


if __name__ == '__main__':
    check_dataset()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

部分输出如下,check_dataset.py检查出21份在images中找不到对应图片的*.txt标记文件,检查出1份在类别标签中含有空格的*.txt标记文件,剔除这22份无效标记文件后,formatted_annotations中还剩余1005份有效标记文件。

在这里插入图片描述

食物类别统计

编写class_count.py,生成formatted_annotations中所有食品种类的统计数据:

# UNIMIB2016
# ├── UNIMIB2016-annotations
# │   ├── check_dataset.py
# │   ├── class_count.py <--
# │   └── formatted_annotations
# └── images

# class_count.py

import os
import pandas as pd

# formatted_annotations path
path = os.path.join(os.getcwd(), 'formatted_annotations')

# output path
output = os.path.join(os.getcwd(), 'class_counts_result.csv')

# read file list of formatted_annotations
annotations = os.listdir(path)

if __name__ == '__main__':
    labels = []
    for annotation in annotations:
        with open(os.path.join(path, annotation)) as file:
            for line in file:
                item = line.split()
                cls = item[0]
                labels.append(cls)
    counts = pd.Series(labels).value_counts()
    counts.to_csv(output, header=False)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

分类统计结果存于class_counts_result.csv. 部分统计数据如下:(未进行上一目有前性检验前共73个分类),按出现次数从高到低,从0开始为每个分类进行编号。

ClassNum
pane479
mandarini198
carote161
patate/pure151
cotoletta148
fagiolini131
yogurt130
标签格式转换

接下来编写python脚本,将这些数据转换为YOLOv5所需格式:

在这里插入图片描述

在这里插入图片描述

编写toYolo.py,将formatted_annotations中所有*.txt转换为Yolo格式,将生成的结果存于labels中。

# UNIMIB2016
# ├── UNIMIB2016-annotations
# │   ├── check_dataset.py
# │   ├── class_count.py
# │   ├── toYolo.py <--
# │   ├── class_counts_result.csv
# │   └── formatted_annotations (1005)
# ├── labels
# └── images (1005)

# toYolo.py

import os
from PIL import Image

# formatted_annotations path
path = os.path.join(os.getcwd(), 'formatted_annotations')

# path of images
img_path = os.path.join(os.getcwd(), os.pardir, 'images')

# output path
output_path = os.path.join(os.getcwd(), os.pardir, 'labels')

# class count file path
class_file_path = os.path.join(os.getcwd(), 'class_counts_result.csv')


def convert_box(size, box):
    # convert VOC to yolo format
    dw, dh = 1. / size[0], 1. / size[1]
    x, y, w, h = (box[0] + box[1]) / 2.0, (box[2] + box[3]) / 2.0, box[1] - box[0], box[3] - box[2]
    return [x * dw, y * dh, w * dw, h * dh]


def convert_bbox(ibb):
    # convert ibb to VOC format
    # ibb = [x1,y1,x2,y2,x3,y3,x4,y4]
    X = ibb[0::2]
    Y = ibb[1::2]
    xmin = min(X)
    ymin = min(Y)
    xmax = max(X)
    ymax = max(Y)
    return xmin, ymin, xmax, ymax


def get_classes():
    # output: class list
    cf = open(class_file_path, 'r')
    clss = [line.split(',')[0] for line in cf.readlines()]
    cf.close()
    return clss


def toYolo():
    # read file list of formatted_annotations
    annotations = os.listdir(path)

    # get class list
    clss = get_classes()

    # convert every annotation in ./formatted_annotations/ to yolo format
    for annotation in annotations:

        with open(os.path.join(path, annotation)) as file, open(os.path.join(output_path, annotation), 'w') as opfile:

            # read img
            img_f_path = os.path.join(img_path, annotation[:-4] + '.jpg')
            img = Image.open(img_f_path)

            # get img size
            size = img.size

            # process every item in ./formatted_annotations/*.txt
            for line in file:
                item = line.split(' ')

                # get class num
                cls = item[0]
                cls_num = clss.index(cls)

                # get bbox coordinates
                item_bounding_box = list(map(float, item[1:]))
                xmin, ymin, xmax, ymax = convert_bbox(item_bounding_box)
                b = [xmin, xmax, ymin, ymax]
                bb = convert_box(size, b)

                # append item to output file: ../labels/*.txt
                item_str = list(map(str, [cls_num] + bb))
                line_yolo = ' '.join(item_str)
                opfile.write(line_yolo + '\n')

            print(annotation)


if __name__ == '__main__':
    toYolo()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98

数据集校验

图片修正

由于 EXIF Rotation Information 的存在,在 YOLOv5 使用的 cv2 读取图片时,对图片参考系的选取产生影响,导致labels偏离原图片,故需要对图片进行修正,具体原因请查阅 yolov5踩坑记录:标签错位(PIL读取图片方向异常)

修正前(标记错位)

在这里插入图片描述

修正后

在这里插入图片描述

修正代码
# UNIMIB2016
# ├── UNIMIB2016-annotations
# │   ├── check_dataset.py
# │   ├── class_count.py
# │   ├── toYolo.py
# │   ├── class_counts_result.csv
# │   └── formatted_annotations
# ├── rectify_imgs.py <--
# ├── labels (1005)
# └── images (1005)

# rectify_imgs.py

import os
from PIL import Image
import numpy as np

# image type
img_type = '.jpg'

# image folder path
path = os.path.join(os.getcwd(), 'images')


def rectify_imgs():
    for img_name in os.listdir(path):
        if not img_name[-4:] == img_type:
            continue
        img_path = os.path.join(path, img_name)
        img = Image.open(img_path)
        img_rectified = Image.fromarray(np.asarray(img))
        img_rectified.save(img_path)
        print(img_name)


if __name__ == '__main__':
    rectify_imgs()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
标签正确性检验

完成上述所有数据集准备工作后,编写labels_shower.py模块,随机选取n张图片,使用 YOLOv5内的图像加载和标记函数,校验 labels文件夹中标记是否正确转换。

在这里插入图片描述

# .
# ├── datasets
# │   └── UNIMIB2016
# │       ├── UNIMIB2016-annotations
# │       ├── images
# │       ├── labels
# │       └── split
# └── yolov5
#     └── labels_shower.py <--

# labels_shower.py

import os
import yaml
import numpy as np
from random import sample
from utils.general import xywhn2xyxy
from utils.plots import Annotator
from utils.general import cv2
from utils.datasets import LoadImages
from utils.plots import Colors

n = 5  # how many images you want to show

# file path set

# ../datasets/UNIMIB2016/labels/
labels_path = os.path.join(os.path.pardir, 'datasets', 'UNIMIB2016', 'labels')
# ../datasets/UNIMIB2016/images/
imgs_path = os.path.join(os.path.pardir, 'datasets', 'UNIMIB2016', 'images')
# data/UNIMIB2016.yaml
cls_path = os.path.join(os.getcwd(), 'data', 'UNIMIB2016.yaml')

# model data preparation
# you shouldn't change them
pt = True
stride = 2
imgsz = (640, 640)
datasets = os.listdir(labels_path)
line_thickness = 3  # bounding box thickness (pixels)
colors = Colors()  # create instance for 'from utils.plots import colors'
with open(cls_path, errors='ignore') as f:
    names = yaml.safe_load(f)['names']  # class names


def labels_shower():
    sources = sample(datasets, n)

    for source in sources:
        # Add bbox to image
        with open(os.path.join(labels_path, source)) as file:
            lines = file.readlines()
            dataset = LoadImages(os.path.join(imgs_path, source[:-4] + '.jpg'),
                                 img_size=imgsz, stride=stride, auto=pt)
            im0s = dataset.__iter__().__next__()[2]
            im0 = im0s.copy()
            annotator = Annotator(im0, line_width=line_thickness, example=str(names))

            for line in lines:
                annot = line.split()
                c = int(annot[0])  # integer class
                label = names[c]
                xywhn = np.asarray([[float(i) for i in annot[1:]]])
                xyxy = xywhn2xyxy(xywhn, w=annotator.im.shape[1], h=annotator.im.shape[0])
                annotator.box_label(xyxy.tolist()[0], label, color=colors(c, True))

            im0 = annotator.result()

            cv2.imshow(str(source[:-4] + '.jpg'), im0)
            # press ESC to destroy cv2 windows
            if cv2.waitKey(0) == 27:
                cv2.destroyAllWindows()


if __name__ == '__main__':
    labels_shower()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

YOLOv5 网络结构

YOLOv5模型集成了FPN多尺度检测及Mosaic数据增强和SPP结构,整体结构可以分为四个模块,具体为:输入端(Input)、主干特征提取网络(Backbone) 、Neck与输出层(Prediction) 。

在这里插入图片描述

输入端

输入端(Input)主要包括了Mosaic数据增强、自适应锚框计算和自适应图片缩放三大部分。

  1. Mosaic数据增强是将数据集图片以随机缩放、随机裁剪、随机排布的方式进行拼接
  2. 自适应锚框计算是指在网络训练中,网络在初始锚框的基础上输出预测框,进而和真实框进行比对,计算两者差距,再反向迭代,更新网络参数
  3. 自适应图片缩放常用的方式是将原始图片统一缩放到一个标准尺寸,再送入检测网络中

主干特征提取网络

主干特征网络提取网络Backbone由Focus结构和CSP结构组成。YOLOv5中分别设计和使用了两种不同的CSP结构,其中CSP1_X结构应用于主干特征提取网络中,同时在Neck中使用了另一种CSP2_X结构。使用CPS模块有如下优点:

  1. 增强网络的学习能力,使得训练出的模型,既能保持轻量化,又能有较高的准确性
  2. 有效降低了计算瓶颈,通过较少的计算量获得较高是检测性能
  3. 降低内存成本,使得训练使用一个GPU即可完成训练

Neck层

Neck层由FPN和PAN组成。FPN是通过向上采样的方法将上层的特征进行传输融合,从而得到预测特征图,其中含有两个PAN结构。通过下采样操作,将低层的特征信息和高层特征进行融合,输出预测的特征图。

FPN采用了自顶向下的结构,这样就可以进行对于强语义特征的传输;特征金字塔采用了自底向上的结构,这样就可以进行对于强定位特征的传输,这两者经过练手结合后,就可以将每一个检测层做到特征聚合,这样就成功提高了特征提取的能力。

输出端

输出端(Prediction),即网络预测层,负责在特征图上应用anchors,并生成带有类概率、目标得分和坐标的输出向量,并进行NMS非极大值抑制处理,最后输出预测结果。

Adam优化器

本文选用Adam作为模型训练过程中梯度下降的优化器,Adam优化器是AdaGrad和RMSPropAdam参数优化器的结合,它具有如下优点:

  1. 实现简单、计算高效、对内存需求少
  2. 参数的更新不受梯度伸缩变换影响
  3. 参数具有很好的解释性、且通常无需调整调整或者微调
  4. 更新步长能够被限制在大致的的范围内
  5. 自动调整学习率

激活函数选择

隐藏层激活函数

隐藏层使用带泄露的ReLU(Leaky ReLU)激活函数,在输入 x < 0 x\lt 0 x<0 时,保持一个很小的梯度 γ \gamma γ,这样神经元非激活时也能有一个非零的梯度可以更新参数,避免永远不能被激活。

采用ReLU激活函数只需要进行加、减、乘和比较的操作,计算上更加高效,ReLU函数也被认为具有生物学合理性(Biological Plausibility),比如单侧抑制、宽兴奋边界(即兴奋程度高)。Sigmoid型激活函数会导致一个非稀疏性的神经网络,而ReLU却具有很好的稀疏性。

在优化方面,相比Sigmoid型函数的两端饱和,ReLU函数左饱和函数且 x > 0 x\gt 0 x>0 时导数为 1 1 1,在一定程度上缓解了神经网络梯度消失的问题,加速梯度下降的收敛速度。

输出层激活函数

输出层使用了Sigmoid型激活函数。使用Sigmoid型函数,其输出可以直接看成一个概率分布,使得神经网络可以更好地统计学习模型进行结合,并且它还可以看成一个软性门(Soft Gate),用来控制其他的神经元输出信息的数量。

模型优化

YOLOv5 的模型优化内容包括:

  1. Focus层优化:使用一个卷积层 Conv(k=6, s=2, p=2) 替换掉 backbone 中的 Focus 层;
  2. SPP层优化:SSP空间金字塔池化层的作用是使卷积神经网络(CNN)能够输入任意大小的图片,在CNN的最后一层卷积层后面加入一层SSP层,它能使不同任意尺寸的特征图通过SSP层之后都能输出一个固定长度的向量。然后将这个固定长度的向量输入到全连接层,进行后续的分类检测任务。SPP层只通过指定三次卷积核大小,将来自CBL模块的数据进行三次池化并拼接,然后再过一个CBL,有效避免了对图像区域剪裁、缩放操作导致的图像失真等问题,解决了卷积神经网络对图像重复特征提取的问题,大大提高了产生候选框的速度,且节省了计算成本,增强特征图特征表达能力;
  3. C3层优化:Bottleneck 为基本残差块,被堆叠嵌入到C3模块中进行特征学习,它利用两个Conv模块将通道数先减小再扩大对齐,以此提取特征信息,并使用shortcut控制是否进行残差连接。在C3模块中,输入特征图会通过两个分支,第一个分支先经过一个Conv模块,之后通过堆叠的Botleneck模块对特征进行学习;另一分支作为残差连接,仅通过一个Conv模块。两分支最终按通道进行拼接后,再通过一个Conv模块进行输出。在backbone结构的最后一层的C3层改用shorcut短连接,因为原先的骨干网络最后一层是C3,而现在是SPPF层。所以最后一层改用shortcut层,这样能够使网络正常训练。

在这里插入图片描述

在这里插入图片描述

本地环境搭建

  1. 创建虚拟环境
  2. 克隆YOLOv5项目
  3. 安装依赖库
git clone https://github.com/ultralytics/yolov5
(venv) ➜  food_detect pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
  • 1
  • 2

在这里插入图片描述

当前项目结构

.
├── venv
├── datasets
│   └── UNIMIB2016
│       ├── images (1005)
│       └── labels (1005)
└── yolov5
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

注:上述目录结构中只列写了项目的关键文件和文件夹.

W&B配置

Weights & Biases可被用作替代tensorboard的监督模型训练过程的可视化工具,拥有如下几个优点:

  • 其已经兼容各种深度学习框架(Pytorch/Tensorflow/Keras)
  • 界面简洁无需与服务器连接,甚至可在移动端随时随地登录自己的account浏览模型训练情况
  • 其不仅可以monitor深度学习loss、reward等与训练强相关的标量,还会监督CPU、GPU等硬件占用率等参数
  • 不仅作为Dashboard显示一些curve,还可通过设置可视化model的weights以调整接下来的调参策略等
  • 通过训练呈现的各种分析Dashboard或可视化界面可直接创建report导出pdf分享

W&B由以下四个组件构成:

  1. Dashboard: 实验跟踪
  2. Artifacts: 数据集版本控制、模型版本控制
  3. Sweeps: 超参数优化
  4. Reports: 保存和共享可重现的结果

基于上述优势,本项目选择W&B作为模型训练和结果可视化的管理平台。版本号如下,虽然YOLOv5v6.1推荐使用wandb version 0.12.10 or below.

版本号
0.12.11
# From the command line, install and log in to wandb, Copy this key and paste it into your command line when asked to authorize your account
pip install wandb==0.12.11
wandb login
  • 1
  • 2
  • 3

环境配置说明表

YOLOv5v6.1
wandb0.12.11
IDEPyCharm
python3.8
OSMacOS

模型训练准备

预训练模型的选取

在预训练模型的选择上,为了同时兼顾菜品识别的速率和准确性,我们选择最近才发布的预训练模型YOLOv5s6. (22 Feb 2022, v6.1

在COCO数据集上,虽然YOLOv5n在识别速度上远超其他模型,但精度相对较低。而YOLOv5s在保持着较高识别速度的前提下,识别准确性优于YOLOv5n。在近期更新的版本中,YOLOv5s6模型识别的准确性进一步提高,识别速度也有所提升,模型参数量大幅减少,故选择该预训练模型。

下载模型:YOLOv5s6.pt,放置于yolov5/文件夹下.

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

训练集和验证集的划分

编写脚本,将datasets/UNIMIB2016/labels中的有效数据按7:3划分训练集和验证集,验证集也做测试集之用。最终,训练集数据量为703,验证集为302. 将结果存入UNIMIB2016目录下的train.txttest.txt.

# .
# ├── venv
# ├── datasets
# │   └── UNIMIB2016
# │       ├── splitDataset.py <--
# │       ├── images (1005)
# │       └── labels (1005)
# └── yolov5

# splitDataset.py

import os
import random
from random import shuffle

# labels relative path
# ./labels
ya_path = os.path.join(os.getcwd(), 'labels')

# images path (relative to 'dataset root dir' in UNIMIB2016.yaml)
# ./images/
img_path = os.path.join(os.getcwd(), 'images')

# output files name
output_train = 'train.txt'
output_test = 'test.txt'

# the percentage of train set
train_percent = .7


def splitDataset():
    all_samples = os.listdir(ya_path)
    num = len(all_samples)

    train_num = int(train_percent * num)

    # shuffle samples list
    random.seed(82322)
    shuffle(all_samples)

    train_set = all_samples[:train_num]
    test_set = all_samples[train_num:]

    # generate train set file
    with open(os.path.join(os.getcwd(), output_train), 'w') as f:
        for item in train_set:
            f.write(os.path.join(img_path, item[:-4] + '.jpg') + '\n')

    # generate test set file
    with open(os.path.join(os.getcwd(), output_test), 'w') as f:
        for item in test_set:
            f.write(os.path.join(img_path, item[:-4] + '.jpg') + '\n')

    print('train set num = ' + str(train_num))
    print('test set num = ' + str(num - train_num))


if __name__ == '__main__':
    splitDataset()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

模型训练文件配置

UNIMIB2016.yaml

新建yolov5/data/UNIMIB2016.yaml,内容设置如下:

# UNIMIB2016 dataset http://www.ivl.disco.unimib.it/activities/food-recognition/ (1027 available photos)
# parent
# ├── yolov5
# └── datasets
#     └── UNIMIB2016  ← downloads here


path: ../datasets/UNIMIB2016  # dataset root dir
train: train.txt  # train images (relative to 'path') 703 images
val: test.txt  # val images (relative to 'path') 302 images
test: test.txt # test images (optional) 302 images

# Classes
nc: 73  # number of classes
names: [ 'pane', 'mandarini', 'carote', 'patate/pure', 'cotoletta', 'fagiolini', 'yogurt', 'budino', 'spinaci', 'scaloppine',
         'pizza', 'pasta_sugo_vegetariano', 'mele', 'pasta_pesto_besciamella_e_cornetti', 'zucchine_umido',
         'lasagna_alla_bolognese', 'arancia', 'pasta_sugo_pesce', 'patatine_fritte', 'pasta_cozze_e_vongole', 'arrosto',
         'riso_bianco', 'medaglioni_di_carne', 'torta_salata_spinaci_e_ricotta', 'pasta_zafferano_e_piselli',
         'patate/pure_prosciutto', 'torta_salata_rustica_(zucchine)', 'insalata_mista', 'pasta_mare_e_monti',
         'polpette_di_carne', 'pasta_pancetta_e_zucchine', 'pasta_ricotta_e_salsiccia', 'orecchiette_(ragu)', 'pizzoccheri',
         'finocchi_gratinati', 'pere', 'pasta_tonno', 'riso_sugo', 'pasta_tonno_e_piselli', 'piselli', 'torta_salata_3',
         'torta_salata_(alla_valdostana)', 'banane', 'salmone_(da_menu_sembra_spada_in_realta)', 'pesce_2_(filetto)',
         'bruscitt', 'guazzetto_di_calamari', 'pasta_e_fagioli', 'pasta_sugo', 'arrosto_di_vitello', 'stinco_di_maiale',
         'minestra_lombarda', 'finocchi_in_umido', 'pasta_bianco', 'cavolfiore', 'merluzzo_alle_olive', 'zucchine_impanate',
         'pesce_(filetto)', 'torta_crema_2', 'roastbeef', 'rosbeef', 'cibo_bianco_non_identificato', 'torta_crema',
         'passato_alla_piemontese', 'pasta_e_ceci', 'crema_zucca_e_fagioli', 'focaccia_bianca', 'minestra',
         'torta_cioccolato_e_pere', 'torta_ananas', 'rucola', 'strudel', 'insalata_2_(uova' ]  # class names
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
my_train.py

创建 yolov5/my_train.py,编写单次训练的启动程序,并设置模型各个参数:(这一步也可融入下一目中进行——超参优化

my_train.py使用预置超参数data/hyps/hyp.scratch-myself.yaml,优化器Adam,输入图像尺寸640batch size = 16.

# my_train

import train

params = {'weights': 'yolov5s6.pt',
          'cfg': 'hub/yolov5s6.yaml',
          'data': 'UNIMIB2016.yaml',
          'hyp': 'data/hyps/hyp.scratch-myself.yaml',
          'epochs': 300,
          'batch_size': 16,
          'imgsz': 640,
          'optimizer': 'Adam'}

if __name__ == '__main__':
    train.run(**params)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

图像增强

数据增强也叫数据扩增,意思是在不实质性的增加数据的情况下,让有限的数据产生等价于更多数据的价值。

yolov5/data/hyps目录下,作者提供的初始超参数就包含了图像增强的参数,如下图所示(hyp.scratch-med.yaml):

在这里插入图片描述

图例为一次运行时(batch_size=16),经过mosaic、hsv、flip up-down、flip left-right后得到的增强图片。

在这里插入图片描述

超参数调优

YOLOv5的开发团队在 PR #3938 中添加了对于 W&B sweep 的支持。所以,对于YOLOv5s6预训练模型的超参数调优,我们使用W&B提供的sweep工具。

参数和配置

编写yolov5/utils/loggers/wandb/sweep.yaml,确定项目路径配置和超参数搜索范围、方法等:

# sweep.yaml
# Hyperparameters for training
program: utils/loggers/wandb/sweep.py
method: random
metric:
  name: metrics/mAP_0.5
  goal: maximize
early_terminate:
  type: hyperband
  min_iter: 3
  eta: 3

parameters:
  # hyperparameters: set either min, max range or values list
  data:
    value: "data/UNIMIB2016.yaml"
  weights:
    value: "yolov5s6.pt"
  cfg:
    value: "models/hub/yolov5s6.yaml"
  epochs:
    value: 100
  imgsz:
    value: 640
  optimizer:
    value: "Adam"
  batch_size:
    values: [4, 8, 16]

  lr0:
    distribution: uniform
    min: 0.005
    max: 0.015
  lrf:
    distribution: uniform
    min: 0.005
    max: 0.015
  momentum:
    distribution: uniform
    min: 0.92
    max: 0.95
  weight_decay:
    distribution: uniform
    min: 4e-4
    max: 5e-4
  warmup_epochs:
    value: 3.0
  warmup_momentum:
    value: 0.8
  warmup_bias_lr:
    value: 0.1
  box:
    distribution: uniform
    min: 0.045
    max: 0.055
  cls:
    distribution: uniform
    min: 0.45
    max: 0.55
  cls_pw:
    value: 1.0
  obj:
    distribution: uniform
    min: 0.95
    max: 1.05
  obj_pw:
    value: 1.0
  iou_t:
    distribution: uniform
    min: 0.18
    max: 0.22
  anchor_t:
    value: 4.0
  fl_gamma:
    value: 0.0
  hsv_h:
    value: 0.015
  hsv_s:
    value: 0.7
  hsv_v:
    value: 0.4
  degrees:
    value: 8.0
  translate:
    value: 0.005
  scale:
    value: 0.20
  shear:
    value: 0.0
  perspective:
    value: 0.0
  flipud:
    value: 0.7
  fliplr:
    value: 0.7
  mosaic:
    value: 0.95
  mixup:
    value: 0
  copy_paste:
    value: 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 超参数调优的目标是最大化mAP@0.5
  • 最优超参数搜索方法使用random,每次迭代时随机地在超参数搜索范围中选择一组参数
  • 参数范围的选取根据data/hyps/hyp.scratch-low.yaml来确定,hyp.scratch-low.yaml也被用来作为 baseline,在开始 sweep 前先以该参数训练模型
  • sweeping过程中,使用hyperband方法对表现较差的迭代进行减枝(prune),提前结束该次超参尝试,加速模型超参数优化速度。参数设置: η = 3 \eta=3 η=3, m i n _ i t e r = 3 min\_iter=3 min_iter=3. 意味着每轮运行将在[3, 9, 27, 81]次brackets时,对模型优化目标进行评估,及时终止无效的运行
  • Random search chooses a random set of values on each iteration.
  • Hyperparameters. Default hyperparameters are in hyp.scratch.yaml. We recommend you train with default hyperparameters first before thinking of modifying any. In general, increasing augmentation hyperparameters will reduce and delay overfitting, allowing for longer trainings and higher final mAP.
  • Hyperband stopping evaluates whether a program should be stopped or permitted to continue at one or more pre-set iteration counts, called “brackets”. When a run reaches a bracket, its metric value is compared to all previous reported metric values and the run is terminated if its value is too high (when the goal is minimization) or low (when the goal is maximization).
调优程序运行(sweep)

运行超参数调优程序,迭代次数100次.

# get the sweep id
wandb sweep --project YOLOv5 utils/loggers/wandb/sweep.yaml
  • 1
  • 2
# set a target to automatically stop the sweep
NUM=100 
# input the sweep id got in preceding step
SWEEPID="xxxxxxxx" 
# run an agent by nohup
nohup wandb agent --count $NUM sylvanding/YOLOv5/$SWEEPID > ./sweeping.log 2>&1 &
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

模型训练

云服务器选取

本项目的模型训练使用MistGPU平台提供的带有GPU加速功能的主机. 服务器的配置如下:

操作系统Linux-4.18.0-15-generic-x86_64-with-glibc2.27
显卡NVIDIA GeForce GTX 1080 Ti
显存11 Gbps
CPUIntel Xeon CPU E5-2678 v3 @ 2.50GHz

YOLOv5开发环境配置如下:

Python version3.8.13
W&B CLI Version0.12.11
PyTorch1.11.0
Opencv4.5.5
Cuda/cudnnCuda10.1/cudnn7.6.5

在这里插入图片描述

服务器环境配置

安装python3.8
# python3.8 安装
1. 以root用户或具有sudo访问权限的用户身份运行以下命令,以更新软件包列表并安装必备组件:
2. $ sudo apt update
   $ sudo apt install software-properties-common
3. 将Deadsnakes PPA添加到系统的来源列表中:
   $ sudo add-apt-repository ppa:deadsnakes/ppa
4. 启用存储库后,请使用以下命令安装Python 3.8:
   $ sudo apt install python3.8
5. 通过键入以下命令验证安装是否成功:
   $ python3.8 --version
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
上传项目

项目文件的组织结构如下(整个项目的必要文件均打包到model_training/文件夹下):

在这里插入图片描述

  • labels/文件夹存有前文得到的yolov5格式.txt标记文件1005份
  • test.txt, train.txt存放前文划分好的测试集、训练集图片文件路径
  • yolov5/存放上文修改的yolov5项目
  • 初始时,images文件夹为空,需要编写脚本下载、解压、修正图片,图片压缩文件UNIMIB2016-images.zip
  • 上图UNIMIB2016中缺少rectify_imgs.py,应当添加进来
scp -r -P61500 /Users/sylvanding/Downloads/food_detect/model_training.zip mist@ygg.mistgpu.xyz:~/
  • 1
创建虚拟环境和安装项目依赖
pip install virtualenv
whereis python3.8 # get python3.8 path
virtualenv -p /usr/bin/python3.8 venv # use python3.8 as interpreter
  • 1
  • 2
  • 3
source venv/bin/activate
cd yolov5
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
  • 1
  • 2
  • 3
下载数据和初始化W&B
# 注意:下载速度可能很慢
bash datasets/UNIMIB2016/imageSets_downloads.sh
# 初始化W&B
wandb login
  • 1
  • 2
  • 3
  • 4

服务器图片处理脚本

编写imageSets_downloads.sh,以下载、解压、修正图片:

#!/bin/bash
# Download UNIMIB2016 dataset 
# http://www.ivl.disco.unimib.it/activities/food-recognition/
# created by Sylvan Ding -- https://blog.csdn.net/IYXUAN
# 2022.04.22 -- sylvanding@qq.com, sylvanding.online

# Example usage: bash datasets/UNIMIB2016/imageSets_downloads.sh
# before execution, you need to install wget and zip!
# parent  ← you should be here
# ├── yolov5
# └── datasets
#     └── UNIMIB2016
#         ├── labels
#         └── images  ← downloads here

# Download/unzip images
d='./datasets/UNIMIB2016/images' # unzip directory
file='UNIMIB2016-images.zip' # images.zip
url='wget http://www.ivl.disco.unimib.it/download/http://www.ivl.disco.unimib.it/minisites/UNIMIB2016/UNIMIB2016-images.zip'
wget $url
echo 'Unzipping...'
unzip -q -j -d $d $file
echo 'Downloaded successfully!'
python3.8 $d/../rectify_imgs.py
echo 'Rectified successfully!'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

运行截图

在这里插入图片描述

常见错误

Arial.ttf
  • 第一次启动,需要下载 Arial.ttf 字体,卡住.

在这里插入图片描述

  • 解决方法:

    在自己主机上下载好再上传到服务器,或者用 wget 再服务器上下载再移至指定字体文件夹

wget https://ultralytics.com/assets/Arial.ttf
scp -r -P61500 /Users/sylvanding/Downloads/Arial.ttf mist@ygg.mistgpu.xyz:/home/mist/.config/Ultralytics/
  • 1
  • 2
图片下载速度慢
  • 图片数据集下载速度慢,用腾讯云SVM下载,下载好后传至自己的主机上,再用 MistGPU 提供的云储存上传数据集即可
# 从“云端”拷贝数据集到项目文件夹
cp -v /data/UNIMIB2016-images.zip ~/model_training
  • 1
  • 2
Cuda out of memory
  • 出现Cuda is out of memory,内存或显存不足,应该 kill 释放其他占用内存的进程
results.png 生成问题
  • #7650 When generating results.png, bug happened with disorder on Y-axis of val/box_loss, val/obj_loss and val/cls_loss

在这里插入图片描述

结果分析

Metrics

precision & recall

p r e c i s i o n = T P T P + F P = T P a l l   d e t e c t i o n s precision = \frac{TP}{TP+FP} = \frac{TP}{\mathrm{all\ detections}} precision=TP+FPTP=all detectionsTP

r e c a l l = T P T P + F N = T P a l l   g r o u n d   t r u t h s recall = \frac{TP}{TP+FN} = \frac{TP}{\mathrm{all\ ground\ truths}} recall=TP+FNTP=all ground truthsTP

  • Precision 指的是预测出的样本中正例比例(查准率)
  • Recall 指的是所有正例中预测出的正例比例(查全率)
  • all detections 是所有bounding box的数量
  • all ground truths是所有ground truths的数量

在目标检测(object detection)中,混淆矩阵的定义如下:

C o n f i d e n c e ≥ T h r e s h o l d 2 Confidence\ge Threshold_2 ConfidenceThreshold2 C o n f i d e n c e < T h r e s h o l d 2 Confidence\lt Threshold_2 Confidence<Threshold2
I o U ≥ T h r e s h o l d 1 IoU\ge Threshold_1 IoUThreshold1TPFN
I o U < T h r e s h o l d 1 IoU\lt Threshold_1 IoU<Threshold1FPTN
  • I o U = a r e a   o f   o v e r l a p a r e a   o f   u n i o n = a r e a ( B p ∩ B g t ) a r e a ( B p ∪ B g t ) IoU = \frac{\mathrm{area\ of\ overlap}}{\mathrm{area\ of\ union}} = \frac{area(B_p\cap B_{gt})}{area(B_p\cup B_{gt})} IoU=area of unionarea of overlap=area(BpBgt)area(BpBgt)
  • T h r e s h o l d 1 Threshold_1 Threshold1 I o U IoU IoU 阈值
  • T h r e s h o l d 2 Threshold_2 Threshold2 C o n f i d e n c e Confidence Confidence 分类置信度阈值
mAP

在不同语境下,mAP主要针对COCO数据集,AP主要针对VOC数据集。mAP(mean Average Precision)是 AP 的平均值,即计算在各个分类类别上的 AP 后求平均所得。AP 的求法如下:

  • 根据 BB(bounding box)的 C o n f i d e n c e Confidence Confidence 由高到低排序
  • 依次以各个 BB 的 C o n f i d e n c e Confidence Confidence T h r e s h o l d 2 Threshold_2 Threshold2 (包括 0、1)
  • 依次计算在此分类置信度下,该分类的 r e c a l l recall recall p r e c i s i o n precision precision,绘制 P-R 曲线
  • 根据不同的策略,计算 AUC(area under the curve),即 AP

在VOC物体检测任务中,Pascal VOC 2008 中设置 T h r e s h o l d 1 = 0.5 Threshold_1=0.5 Threshold1=0.5,使用差值平均精确度(interpolated average precision)的评测方法。绘制 P-R 曲线后,选取横轴上的11个点(间隔为0.1)所对应的最大精度,然后再取平均作为最终检测的平均精确度,其表达式如下:

A P = 1 11 ∑ r ∈ { 0 , 0.1 , … , 1 } P i n t e r p ( r ) AP=\frac{1}{11} \sum _{r\in \{0,0.1,\dots , 1 \}} P_{interp} (r) AP=111r{0,0.1,,1}Pinterp(r)

P i n t e r p ( r ) = max ⁡ r ′ ≥ r P ( r ′ ) P_{interp} (r) = \mathop {\max } \limits _{r'\ge r} P(r') Pinterp(r)=rrmaxP(r)

其中, r r r 是横轴召回率的值, P i n t e r p ( r ) P_{interp}(r) Pinterp(r) r r r 时的差值精度, P ( r ) P(r) P(r)r 对应的纵轴精度。

在这里插入图片描述

在 Pascal VOC 中,检测结果只评测了 T h r e s h o l d 1 = 0.5 Threshold_1=0.5 Threshold1=0.5 阈值下的 mAP 值(记为 m A P @ 0.5 mAP@0.5 mAP@0.5 )。相比 VOC 而言,COCO 数据集的评测则更加全面。

COCO 评估了在不同的交并比 [ 0.5 : 0.05 : 0.95 ] [0.5:0.05:0.95] [0.5:0.05:0.95] 下的 mAP,并且在最后以这些阈值下的 mAP 的平均作为结果,记为 m A P @ [ 0.5 : 0.95 ] mAP@[0.5:0.95] mAP@[0.5:0.95]. 不仅评估到物体检测模型的分类能力,同时也能体现出检测模型的定位能力。

预测的时候会产生很多FP,为了减少FP的数量,一般检测器的最后都会引入一步Non-maximum Suppression(NMS),以去除一部分重复预测的bounding box,YOLOv5中采用加权NMS的方式。

Loss Function

YOLOv1 的损失函数包括三部分,分别是定位损失、置信度损失和分类损失,其形式如下:

在这里插入图片描述

YOLOv5 改进了损失函数,进一步提高了模型的收敛速度和训练稳定性,避免了梯度消失和梯度爆炸。YOLOv5 的损失函数也有三部分构成:

  • Localization loss 定位损失(又称 box loss,是预测框与 GT 之间的误差)
  • Confidence loss 置信度损失(又称 obj loss, Objectness of the box
  • Classification loss 分类损失(cls loss

总损失函数是上述三者的加权和,通常置信度损失取最大权重、矩形框损失(或定位损失)和分类损失的权重次之。

YOLOv5 使用 CIoU loss 计算定位损失,置信度和分类损失都用 BCE loss 计算。

定位损失

对于矩形框的预测损失来说,可用 L1、L2 或 smooth L1 损失函数来描述。训练后期,L1 损失函数会导致其值在某范围内波动,难以收敛。虽然 L2 损失函数在 0 点处可导,最终可以收敛,但在训练前期,可能会导致梯度爆炸问题,从而训练没能朝着最优化的方向进行。smooth L1 损失函数将二者优点相结合,即避免了梯度爆炸,又避免了不熟练问题。上述计算矩形框的 L1、L2、smooth L1 损失时有一个共同点,都是分别计算矩形框中心点 x 坐标、中心点 y 坐标、宽、高的损失,最后再将四个损失值相加得到该矩形框的最终损失值。这种计算方法的前提假设是中心点 x 坐标、中心点 y 坐标、宽、高这四个值是相互独立的,实际上它们具有相关性,所以该计算方法存在问题。

于是,IoU系列损失函数(IoU、GIoU、DIoU、CIoU)又被陆续提了出来。IoU loss 关注预测框和 GT 的交并比;GIoU loss 把包围预测框和 GT 的最小矩形框的面积也加入到计算中,解决了 IoU loss 中,当两个矩形框完全没有重叠区域时,无论它们距离多远,它们的 IoU 都为 0,导致梯度也为 0,因而无法优化的情况;DIoU loss,把两矩形框的中心点距离 ρ \rho ρ、外接矩形框的对角线长度 c c c 都考虑进去,使训练更稳定、收敛更快。YOLOv5使用 CIoU loss 来衡量矩形框的损失。

CIoU loss 将重叠面积、中心点距离、宽高比同时加入了计算,其计算公式如下:

C I o U = I o U − ρ 2 c 2 − α v = D I o U − α v CIoU = IoU - \frac{\rho ^2}{c^2} - \alpha v= DIoU - \alpha v CIoU=IoUc2ρ2αv=DIoUαv

v = 4 π 2 ( arctan ⁡ w g t h g t − arctan ⁡ w p r e d h p r e d ) 2 v = \frac{4}{\pi ^2} \left ( \arctan \frac{w_{gt}}{h_{gt}} - \arctan \frac{w_{pred}}{h_{pred}} \right ) ^2 v=π24(arctanhgtwgtarctanhpredwpred)2

α = v 1 − I o U + v \alpha = \frac{v}{1-IoU+v} α=1IoU+vv

l o s s C I o U = 1 − C I o U loss_{CIoU} = 1-CIoU lossCIoU=1CIoU

其中, w g t w_{gt} wgt h g t h_{gt} hgt 为 GT 宽、高, w p r e d w_{pred} wpred h p r e d h_{pred} hpred 为预测框宽、高, ρ \rho ρ 是两框中心点距离, c c c 是是包围两框的最小矩形框对角线长度, v v v 是两框宽高比的相似度, α \alpha α v v v 的影响因子。

IoU 越大,两框的重叠区域越大,则 α α α 越大,从而 v v v 的影响越大,对宽高比的惩罚力度越大,着重优化宽高比;反之,IoU 越小,两框的重叠区域越小, α α α 越小,从而 v v v 的影响越小,对两框距离的惩罚力度越大,着重优化距离。

在这里插入图片描述

置信度损失

YOLOv5 将一张输入的 640 × 640 640\times 640 640×640 图像分割成的 N × N N\times N N×N 网格,每个网格预测 M M M 个预测框(anchor),所以总共预测了 M × N × N M\times N\times N M×N×N 个预测框。每个预测框的预测信息包括矩形框信息、置信度、分类概率。

  • 矩形框:表征目标的大小以及精确位置
  • 置信度:表征预测框的可信程度,取值范围0~1,值越大说明该矩形框中越可能存在目标
  • 分类概率:表征目标的类别

由于并不是每个预测框内都存在目标,所以在训练时首先需要根据标签作初步判断,判断每个预测框内是否存在目标,以此建立 mask 矩阵(矩阵的每个元素是布尔型)。实际上,并非所有预测框都需要计算所有类别的损失函数值,而是根据 mask 矩阵来决定,决定原则如下:

  • 仅 mask 矩阵对应位置为 True 的预测框需要计算 box loss 和 cls loss
  • 所有预测框都要计算 obj loss,但是 mask 为 true 的预测框与 mask为 false 的预测框的置信度标签值不一样

mask 矩阵的布尔值,由 anchor 框的保留或剔除决定,依照 anchor 框和 GT 的宽高比(aspect ratio)决定 anchor 是否保留。

置信度标签的维度应该与神经网络的输出维度保持一致,因此置信度的标签也是维度为 M × N × N M\times N\times N M×N×N 的矩阵。计算对应预测框与目标框的 CIoU,使用 CIoU 作为该预测框的置信度标签,对 mask 矩阵为 false 的位置,预测框的置信度标签赋值 0. 当 CIoU 小于 0 时,直接取 0 值作为标签,对 CIoU 做截断。由此得到预测置信度矩阵 P.

假设置信度标签为矩阵 L,那么置信度损失的 BCE loss(二元交叉熵损失)函数定义如下:

l o s s B C E ( z , x , y ) = − L ( z , x , y ) ∗ log ⁡ P ( z , x , y ) − ( 1 − L ( z , x , y ) ) ∗ log ⁡ ( 1 − P ( z , x , y ) ) loss_{BCE}(z,x,y)=-L(z,x,y)* \log P(z,x,y) - (1-L(z,x,y))*\log (1-P(z,x,y)) lossBCE(z,x,y)=L(z,x,y)logP(z,x,y)(1L(z,x,y))log(1P(z,x,y))

其中, 0 ≤ z < M 0 \le z \lt M 0z<M, 0 ≤ x , y < N 0 \le x,y \lt N 0x,y<N.

从而得到该网络的置信度损失值:

{ l o b j = 1 n u m   o f   ( m a s k = t r u e ) ∑ m a s k = t r u e l o s s B C E ( z , x , y ) l n o b j = 1 n u m   o f   ( m a s k = f a l s e ) ∑ m a s k = f a l s e l o s s B C E ( z , x , y ) l o s s o b j = a ∗ l o b j + ( 1 − a ) ∗ l n o b j \left\{

lobj=1num of (mask=true)mask=truelossBCE(z,x,y)lnobj=1num of (mask=false)mask=falselossBCE(z,x,y)lossobj=alobj+(1a)lnobj
\right. lobjlnobjlossobj===num of (mask=true)1mask=truelossBCE(z,x,y)num of (mask=false)1mask=falselossBCE(z,x,y)alobj+(1a)lnobj

其中, a a a 为 mask = true 时的置信度损失权重, a a a 越大,网络训练时越专注于 mask = true 的正样本情况。为了使训练更专注于正样本,后来 Focal loss 又被提了出来。

分类损失

为了减少过拟合、增加训练的稳定性,YOLOv5 对独热码标签做了平滑操作,如下所示:

l a b e l s m o o t h = l a b e l ∗ ( 1 − α ) + α / c l a s s n u m label_{smooth} = label*(1-\alpha )+\alpha /class num labelsmooth=label(1α)+α/classnum

α \alpha α 是平滑系数, l a b e l label label 是经过独热编码后的标签向量。

接着,使用 BCE loss 函数计算矩阵中每个 mask=true 元素的分类损失并累加求平均,得到总的分类损失,计算过程如下:

{ l o s s B C E ( z , x , y , c ) = − L s m o o t h ( z , x , y , c ) ∗ log ⁡ P ( z , x , y , c ) − ( 1 − L s m o o t h ( z , x , y , c ) ) ∗ log ⁡ ( 1 − P ( z , x , y , c ) ) l o s s c l s = 1 c l a s s n u m   ∗   n u m   o f   ( m a s k = t r u e ) ∑ m a s k = t r u e l o s s B C E ( z , x , y , c ) \left\{

lossBCE(z,x,y,c)=Lsmooth(z,x,y,c)logP(z,x,y,c)(1Lsmooth(z,x,y,c))log(1P(z,x,y,c))losscls=1classnum  num of (mask=true)mask=truelossBCE(z,x,y,c)
\right. {lossBCE(z,x,y,c)losscls==Lsmooth(z,x,y,c)logP(z,x,y,c)(1Lsmooth(z,x,y,c))log(1P(z,x,y,c))classnum  num of (mask=true)1mask=truelossBCE(z,x,y,c)

其中, L s m o o t h L_{smooth} Lsmooth 是平滑后的 GT 标签, 0 ≤ c < c l a s s n u m 0\le c\lt classnum 0c<classnum 对应样本类别数.

sweep超参数调优结果

相比于AutoML框架的超参数调优,wandb sweeps具有更强的实验管理和数据可视化的能力。wandb sweeps具有一下几个优点:

  • 较好的可视化效果
  • 较小的代码入侵
  • 较好的实验管理

经过sweep后得到的平行坐标图(parallel coordinates plot)、散点图和相关性分析图如下所示。其中,学习率 lr0mAP@0.5的影响最大,呈负相关趋势。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

选取较优参数进行训练

根据 sweep 的结果,选取 mAP@0.5 值最高的超参数训练模型,具体参数如下所示(未列出参数和data/hyps/hyp.scratch-low.yaml一致,对结果的影响不大):

weightsyolov5s6.pt
cfghub/yolov5s6.yaml
epochs300
batch_size16
imgsz640
optimizerAdam
lr00.01
lrf0.01
momentum0.937
anchors3
hsv_h/s/v0.015/0.7/0.4
degrees5.0
flipud/lr0.5/0.5
mosaic1.0

结果展示

评价指标

训练结束,训练集的三类损失函数均收敛。在验证集上,模型各个评价指标均高于 0.93. 最佳模型在第 299 次epoch时得到,最佳模型的评价指标如下:

mAP@0.50.983
mAP@0.5:0.950.939
precision0.954
recall0.939

在这里插入图片描述

混淆矩阵

最终,模型在验证集上的混淆矩阵如下图所示,该混淆矩阵在列上做归一化,那么,对角元素表示每类的召回率,因为一小部分类别样本量太少,所以召回率较低。背景被判定为菜品的现象存在,但误判率极低。

在这里插入图片描述

PR曲线

在这里插入图片描述

置信度和P、R的关系曲线

后期可依据该图,根据需要选取合适的置信度,调整菜品识别的精度和召回率。(精度和置信度成正比,而召回率和置信度成反比)

在这里插入图片描述

在这里插入图片描述

F1 Score

F1分数(F1-Score),又称为平衡F分数(BalancedScore),它被定义为精确率和召回率的调和平均数。 可以看到,当 Confidence 在 0.7 附近时,F1-Score 最优。

在这里插入图片描述

关于训练集中标签分布信息的描述
相关性直方图

[#5138] Correlogram is a group of 2d histograms showing each axis of your data against each other axis. The labels in your image are in xywh space.

很明显,width 和 height 成正相关分布;x 成双峰分布,意味着大多数人餐盘上食物是左边放一份、右边放一份的;height、width 都集中在 0.5 以下。相关性直方图也为后期 anchor 的选取提供了依据,为模型的进一步优化提供了参考。

在这里插入图片描述

分类直方图和标记框

显然,从 top-left 图可知,该样本的分类是有偏的,模型在某些小数量分类上的标签可能不优秀。

在这里插入图片描述

预测效果展示

在这里插入图片描述

在这里插入图片描述

项目亮点

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/759396
推荐阅读
相关标签