• 03【深度学习】YOLOV3-WIN11环境搭建(配置+训练)


    目录

    一、深度学习:YOLOV3-WIN11环境搭建

    1、虚拟环境搭建

    2、OpenCv安装:

    3、Pycharm安装:

    二、【深度学习】准备个人数据集、YOLOV3 模型的训练和测试

    1、前置知识

    2、数据集的准备

    1.1 数据集资源介绍(了解)

    1.2 COCO数据集介下载

    1.3 COCO数据集的label标签下载

    3、资源下载

    3.1代码下载

    3.2 生成自定义文件

    3.3 下载测试环境的数据

    4、部署YOLOV3的环境

    4.1  用pycharm打开yolov3文件,并配置相应的虚拟环境

    4.2 参数设置

    4.3 运行

    三、文件说明

    1 config 文件夹

    1.1 coco.data

    1.2 create_custom_model.sh

    1.3 custom.data

    1.4 yolov3.cfg

    1.5 yolov3-custom.cfg

    1.6 yolov3-tiny.cfg

    2 data 文件夹

    2.1 coco文件夹

    2.2 custom文件夹

    2.3 samples文件夹

    2.4 coco.names

    2.5 get_coco_dataset.sh

    3 checkpoint文件夹

    4 logs文件夹

    5 weights文件夹

    四、源码解析

    4.1 augmentations.py

    4.2 datasets.py

     4.3 logger.py

    4.4 parse_config.py

    4.5 utils.py

    4.6 detect.py

    4.7 models.py

     4.8 test.py

    4.9 train.py

    一、目标检测:YOLOV3-WIN11环境搭建

          本篇文字是【深度学习】YOLOV3-WIN11环境搭建(配置+训练),首先介绍win11下 基于Anaconda、pytorch的YOLOV3深度学习环境搭建,环境配置顺序:显卡驱动 - CUDA - cudnn - Anaconda - pytorch - pycharm,按这个顺序配置可以避免很多莫名其妙的错误出现。另外不用单独安装python,使用Anaconda里的python环境。

    最简单方式:


    1、虚拟环境搭建

           本文默认 CUDA - cudnn已经安装,未安装的同学见深度学习环境搭建:Win11+CUDA 11.7+Pytouch1.12.1+Anaconda中1-4

           虚拟环境安装pytorch详细见:    深度学习环境搭建:Win11+CUDA 11.7+Pytouch1.12.1+Anaconda中5-8

    2、OpenCv安装:

                主要用来更好的对图显示进行可视化,也可以不按照。本文使用opencv4.3.0版本。

    •      命令安装方式

                 pip install opencv-python(默认使用最新版本)或

                 pip install opencv-python==4.3.0(可以自己指定版本)

    •      下载安装方式

             https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/opencv-python/

             进入虚拟环境,执行命令安装 

    • 安装校验

    3、Pycharm安装:

           自行搜索,此处不做介绍。  

           

           至此在WIN10下的YOLOV5深度学习环境安装完成。接下来可以在此环境下进行深度学习的实验了。

    二、【深度学习】准备个人数据集、YOLOV3 模型的训练和测试

    1、前置知识

            YOLO3 的网络结构使用的是 darknet 网络,因此完成 YOLO3 模型就是 通过darknet配置文件(后面会详细讲)完成darknet网络搭建,而 Darknet 文件结构如下: 

    •   include:存Darknet源码(由C语言编写)
    •   cfg:网络配置文件(例如YoloV3的网络配置文件)、数据信息配置文件
    •   data:数据集
    •   script:下载数据集的脚本,一般在linux环境下使用
    •   python:针对Darknet框架编译后的接口
    •   example:模型测试脚本的例子

    2、数据集的准备

    1.1 数据集资源介绍(了解)

           数据集资源:分为现有的数据集和自定义的数据集。我们先介绍现有的数据集,最后面再介绍如何使用自定义数据集。

    • PascalVOC:20个类别,在 YOLOV1 中使用的是此数据集。
    • COCO:91个类别,小目标多、单幅图片目标多、物体大多非中心分布、更符合日常环境,coco检测难度更大
    • ILSVRC2012:magenet数据集有1400多万幅图片,涵盖2万多个类别;其中有超过百万的图片有明确的类别标注和图像中物体位置的标注
    • 自定义数据集(详细见:自定义数据集及工具

           区别:

    • PascalVOC使用 YOLOV1, YOLOV3使用需要进行转换,数据量少,针对大目标
    • ILSVRC2012数据量太大,下载时间长,训练时间长
    • COCO是PascalVOC、COCO、ILSVRC2012的折中方式

    1.2 COCO数据集介下载

             COCO从复杂的日常场景中截取,包括91类目标,3.28万个影像和250万个label标签。训练集和验证集下载地址如下:

    • http://images.cocodataset.org/zips/val2014.zip
    • http://images.cocodataset.org/zips/train2014.zip

    1.3 COCO数据集的label标签下载

          链接:https://pan.baidu.com/s/1XMWKvtq9LApoyomAGqy_fA 
          提取码:yov3 
     

           预训练权重:https://link.csdn.net/?target=https%3A%2F%2Fpjreddie.com%2Fmedia%2Ffiles%2Fyolov3.weights

    3、资源下载

    3.1代码下载

          YOLOV3项目源码下载

         整个项目的结构如下图:

    3.2 生成自定义文件

           当前的环境为windows,可以使用Git(如果未安装,参考Git的安装与使用)环境构建Yolov3-custom.cfg的模型配置文,步骤如下:

    • 进入之前下载的PyTorch-YOLOv3项目目录
    • 进入config文件夹,右击鼠标,点击Git Bash Here,进入Git终端
    • 执行 bash create_custom_model.sh 2 (其中参数2是指的是任务的类别数量)
    • 生成PyTorch-YOLOv3的自定义模型配置文件 Yolov3-custom.cfg

            Yolov3-custom.cfg文件包含了YOLOV3的网络架构和相关配置,如下图,其中1、2为卷积神经网络训练时的相关参数设置,3为卷积神经网络结构,4为yolo相关参数,anchors为9个预测框的形状大小,classes代表2个类别等。 

    3.3 下载测试环境的数据

          这个测试环境的数据自己做的,只是用于测试YOLO的源码是否可以运行使用。不需要的同学可以略过。

          环境测试数据下载

          环境测试文件说明:

    • 01 将相关数据放入\data\custom路径中相应子目录或文件中,该路径如下:

    • 02放置标签文件,将标签放入labels目录中:

    • 03修改classes.names文件,内容改为任务中的类别名字(即person和bicycle两类),如下图:

           需要注意的是,在最后一行需要加一个回车。

    • 04设置训练用图片,在train.txt中写入参加训练的图片路径和名称,如下图:

    • 05设置校验用图片,在valid.txt中写入参加校验的图片路径和名称,如下图:

    • 06设置config/custom.data文件内容,如下图

    4、部署YOLOV3的环境

    4.1  用pycharm打开yolov3文件,并配置相应的虚拟环境

    4.2 参数设置

           在项目中的 train.py 中修改相关环境配置参数,此处修改只是为了测试环境是否可用。

    4.3 运行

           运行  train.py 进行控制台日志打印    


     

    三、文件说明

    1 config 文件夹

    1.1 coco.data

             coco数据集的信息:类别数量,训练集路径、验证集路径、类别名称路径…

    1. classes= 80 # 类别
    2. train=data/coco/trainvalno5k.txt # 训练集图片的存放路径
    3. valid=data/coco/5k.txt # 测试集图片的存放路径
    4. names=data/coco.names # 类别名
    5. backup=backup/ # 记录checkpoint存放位置
    6. eval=coco # 选择map计算方式

    1.2 create_custom_model.sh

         脚本文件:用户自定义自己的模型,运行此文件用来生成自定义模型的配置文件yolov3-custom.cfg,可对比yolov3.cfg。

    1.3 custom.data

         自己数据集的信息,用来训练自己的检测任务:类别数量,训练集路径、验证集路径、类别名称路径,可对比coco.data。

    1.4 yolov3.cfg

         yolov3网络模型的配置信息:卷积层(归一化、卷积核尺寸、卷积核数、步长、填充、激活函数.....)、yolo层(类别、bounding box数量、控制是否参与损失计算的阈值......)及其他层的配置信息。

    1. [convolutional] #卷积层
    2. batch_normalize=1 #每层归一化
    3. size=3 #卷积核尺寸
    4. stride=1 #滑动步长
    5. pad=1 #填充边框
    6. filters=256 #卷积核个数
    7. activation=leaky #激活函数
    8. [convolutional] #卷积层
    9. size=1 #卷积核尺寸
    10. stride=1 #滑动步长
    11. pad=1 #填充边框
    12. filters=255 #卷积核个数
    13. activation=linear #激活函数
    14. [yolo]
    15. mask = 0,1,2 #指定使用anchors时候索引,表示采用前三个尺寸:10,13, 16,30, 33,23
    16. anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326 #指定anchors box 尺寸
    17. classes=80 #指定类别数量
    18. num=9 #指定每个anchor的bounding box数量
    19. jitter=.3 #指定数据增强随机调整宽高比
    20. ignore_thresh = .7#指定预测检测框与真值检测框IOU>0.7不参与损失计算,常用设置0.5-0.7
    21. truth_thresh = 1 #指定真值
    22. random=1 #指定训练时候采用随机多尺度训练,0表示使用固定尺度训练

    1.5 yolov3-custom.cfg

      自定义的网络模型的配置信息,由create_custom_model.sh脚本文件生成。

    1.6 yolov3-tiny.cfg

      yolov3的tiny版本网络模型的配置信息。

    2 data 文件夹

    2.1 coco文件夹

      是coco训练集、验证集的数据集,是运行get_coco_dataset.sh脚本文件(自动下载数据集,并解压)后的结果。

    2.2 custom文件夹

      custom文件夹是自定义数据集的信息。

    • images文件夹:所有训练集、验证集的图片,如图

    • labels文件夹:使用图片标记软件对images文件夹里的图片进行标注得到对应的标签文件。每个标签文件为一个txt文件,txt文件的每一行数据为一个groundthuth信息类别序号,边界框坐标信息。如图示例,0代表类别索引号,后面为边界框坐标信息

    • classes.names是自定义数据集的类别名称文件。例

    • train.txt文件和valid.txt分别是训练集图片验证集图片路径的集合,如图,每行数据是训练集/验证集某图像的路径。       

    2.3 samples文件夹

      samples文件夹是模型测试图片所在的文件夹,用来看模型的检测结果。 

    2.4 coco.names

      coco数据的类别信息,类似classes.names。如图部分截图

    2.5 get_coco_dataset.sh

      脚本文件,用来获取coco数据,生成coco文件夹及其内容。

    3 checkpoint文件夹

      checkpoint文件夹,用来保存某epoch训练后的模型参数

    4 logs文件夹

      logs文件夹,用来保存日志信息

    5 weights文件夹

      下载的预训练权重存放的文件夹     


    四、源码解析

      代码解析过程中我们先按如下顺序说明

    4.1 augmentations.py

            进行数据增强的文件,本项目只是进行水平翻转的数据增强,图像进行翻转的时候,对应标注信息也进行了修改,最终返回的是翻转后的图片和翻转后的图片对应的标签。

    1. import torch
    2. import torch.nn.functional as F
    3. import numpy as np
    4. """
    5. horisontal_flip(images, targets)
    6. 输入:image,targets 是原始图像和标签;
    7. 返回:images,targets是翻转后的图像和标签。
    8. 功能:horisontal_flip() 函数是对图像进行数据增强,使得数据集得到扩充。
    9. 在此处只采用了对图片进行水平方向上的镜像翻转。
    10. torch.flip(input,dims) ->tensor
    11. 功能:对数组进行反转
    12. 参数: input 反转的tensor ; dim 反转的维度
    13. 返回: 反转后的tensor
    14. """
    15. def horisontal_flip(images, targets): #对图像和标签进行镜像翻转
    16. '''
    17. 由于image 是用数组存储起来的(c,h,w),三个维度分别代表颜色通道、
    18. 垂直方向,水平方向。python 中[-1] 代表最后一个数,即水平方向。
    19. targets是对应的标签[置信度,中心点高度,中心点宽度,框高度,框宽度],
    20. 其中高度宽度都是用相对位置表示的,范围是[0,1]。
    21. '''
    22. images = torch.flip(images, [-1]) #镜像翻转
    23. targets[:, 2] = 1 - targets[:, 2]
    24. # targets是对应的标签[置信度,中心点高度,中心点宽度,框高度,框宽度]
    25. # 镜像翻转时,受影响的只有targets[:, 2],
    26. return images, targets

    4.2 datasets.py

            对数据集进行操作的py文件,包含图像的填充、图像大小的调整、测试数据集的加载类、评估数据集的加载类。整个文件包含3个函数和2个类,如下

    1. import glob
    2. import random
    3. import os
    4. import sys
    5. import numpy as np
    6. from PIL import Image
    7. import torch
    8. import torch.nn.functional as F
    9. from utils.augmentations import horisontal_flip
    10. from torch.utils.data import Dataset
    11. import torchvision.transforms as transforms
    12. """
    13. 对数据集进行操作的py文件,包含图像的填充、图像大小的调整、
    14. 测试数据集的加载类、评估数据集的加载类。
    15. 整个文件包含3个函数和2个类
    16. """
    17. '''
    18. 图片填充函数:
    19. 将图片用pad_value填充成一个正方形,返回填充后的图片以及填充的位置信息
    20. '''
    21. def pad_to_square(img, pad_value):
    22. c, h, w = img.shape
    23. dim_diff = np.abs(h - w)
    24. # (upper / left) padding and (lower / right) padding
    25. pad1, pad2 = dim_diff // 2, dim_diff - dim_diff // 2
    26. # 填充方式,如果高小于宽则上下填充,如果高大于宽,左右填充
    27. pad = (0, 0, pad1, pad2) if h <= w else (pad1, pad2, 0, 0)
    28. # 图片填充,参数img是原图,pad是填充方式(0,0,pad1,pad2)
    29. #或(pad1,pad2,0,0),value是填充的值
    30. img = F.pad(img, pad, "constant", value=pad_value)
    31. return img, pad
    32. '''
    33. 图片调整大小:将正方形图片使用插值方法,改变到固定size大小
    34. torch.nn.functional.interpolate:
    35. 实现插值和上采样,size输出大小,
    36. scale_factor指定输出为输入的多少倍数,
    37. mode可使用的上采样算法,有’nearest’, ‘linear’, ‘bilinear’, ‘bicubic’,
    38. ‘trilinear’和’area’. 默认使用’nearest’
    39. '''
    40. def resize(image, size):
    41. #将原始图片解压后用“nearest”方法进行填充,然后再压缩
    42. image = F.interpolate(image.unsqueeze(0), size=size, mode="nearest").squeeze(0)
    43. return image
    44. """
    45. 随机裁剪函数:将图片随机裁剪到某个尺寸(使用插值法)
    46. min_size,max_size 随机数所在的范围
    47. """
    48. def random_resize(images, min_size=288, max_size=448):
    49. new_size = random.sample(list(range(min_size, max_size + 1, 32)), 1)[0]
    50. images = F.interpolate(images, size=new_size, mode="nearest")
    51. return images
    52. '''
    53. 用来定义数据集的标准格式
    54. 从文件夹中读取图片,将图片padding成正方形,所有的输入图片大小调整为416*416,返回图片的数量
    55. 用于预测:在detect.py中加载数据集时使用
    56. '''
    57. #用于预测:在detect.py中加载数据集时使用
    58. class ImageFolder(Dataset): # 这是定义数据集的标准格式
    59. #初始化的参数为:测试图片所在的文件夹的路径、图片的尺寸(用于输入到网络的图片的大小)
    60. def __init__(self, folder_path, img_size=416):
    61. #获取文件夹下图片的路径,files是图片路径组成的列表
    62. #例在detect.py中folder_path=data/samples
    63. self.files = sorted(glob.glob("%s/*.*" % folder_path))
    64. self.img_size = img_size #初始化图片的尺寸
    65. def __getitem__(self, index): #根据索引获取列表里的图片的路径
    66. img_path = self.files[index % len(self.files)]
    67. # 将图片转换为tensor的格式
    68. img = transforms.ToTensor()(Image.open(img_path))
    69. # 用0将图片填充为正方形
    70. img, _ = pad_to_square(img, 0)
    71. # 将图片大小调整为指定大小
    72. img = resize(img, self.img_size)
    73. return img_path, img # 返回 index 对应的图片的 路径和 图片
    74. def __len__(self):
    75. return len(self.files) # 所有图片的数量
    76. """
    77. Dataset类:
    78. pytorch读取图片,主要通过Dataset类。Dataset类作为所有datasets的基类,
    79. 所有的datasets都要继承它
    80. init: 用来初始化一些有关操作数据集的参数
    81. getitem:定义数据获取的方式(包括读取数据,对数据进行变换等),
    82. 该方法支持从 0 到 len(self)-1的索引。obj[index]等价于obj.getitem
    83. len:获取数据集的大小。len(obj)等价于obj.len()
    84. 数据集加载类2:加载并处理图片和图片标签,返回的是图片路径,经过处理后的图片,
    85. 经过处理后的标签
    86. """
    87. # 用于评估:在test.py中加载数据集时候使用
    88. class ListDataset(Dataset):
    89. # 数据的载入
    90. def __init__(self, list_path, img_size=416, augment=True, multiscale=True, normalized_labels=True):
    91. # 初始化参数:list_path为验证集图片的路径组成的txt文件,的路径、
    92. # img_size为图片大小(输入到网络中的图片的大小)、augment是否数据增强、
    93. # multiscale是否使用多尺度,normalized_labels标签是否归一化
    94. # 获取验证集图片路径img_files,是一个列表
    95. with open(list_path, "r") as file: # 打开valid.txt文件,内容为data/custom/images/train.jpg,指明了验证集对应的图片路径
    96. self.img_files = file.readlines()
    97. # 获取验证集标签路径label_files:是一个列表,根据验证集图片的路径获取标签路径,
    98. # 两者之间是文件夹及后缀名不同,
    99. self.label_files = [
    100. path.replace("images", "labels").replace(".png", ".txt").replace(".jpg", ".txt")
    101. for path in self.img_files
    102. ]
    103. # 其他设置
    104. self.img_size = img_size
    105. self.max_objects = 100 # 最多目标个数
    106. self.augment = augment # bool. 是否使用增强
    107. # bool. 是否多尺度输入,每次喂到网络中的batch中图片大小不固定。
    108. self.multiscale = multiscale
    109. # bool. 默认label.txt文件中的bbox是归一化到0-1之间的
    110. self.normalized_labels = normalized_labels
    111. # self.min_size和self.max_size的作用主要是经过数据处理后生成三种不同size的图像,
    112. # 目的是让网络对小物体和大物体都有较好的检测结果。
    113. self.min_size = self.img_size - 3 * 32
    114. self.max_size = self.img_size + 3 * 32
    115. self.batch_count = 0 # 当前网络训练的是第几个batch
    116. # 根据下标 index 找到对应的图片,并对图片、标签进行填充,适应于正方形,对标签进行归一化。
    117. # 返回图片路径,图片,标签
    118. def __getitem__(self, index): # 读取数据和标签
    119. # ---------
    120. # Image
    121. # ---------
    122. # 根据索引获取图片的路径
    123. img_path = self.img_files[index % len(self.img_files)].rstrip()
    124. img_path = 'F:\\cv\\PyTorch-YOLOv3\\PyTorch-YOLOv3\\data\\coco' + img_path
    125. # print (img_path)
    126. # 把图片变为tensor
    127. img = transforms.ToTensor()(Image.open(img_path).convert('RGB'))
    128. # 把图片变为三个通道,获取图像的宽和高
    129. if len(img.shape) != 3:
    130. img = img.unsqueeze(0)
    131. img = img.expand((3, img.shape[1:]))
    132. _, h, w = img.shape
    133. # 如果标注bbox不是归一化的,则标注里面的保存的就是真实位置
    134. h_factor, w_factor = (h, w) if self.normalized_labels else (1, 1)
    135. # 把图片填充为正方形,返回填充后的图片,以及填充的信息 pad = (0, 0, pad1, pad2) if h <= w else (pad1, pad2, 0, 0)
    136. img, pad = pad_to_square(img, 0)
    137. # 填充后的高和宽
    138. _, padded_h, padded_w = img.shape
    139. # ---------
    140. # Label
    141. # ---------
    142. # 根据索引,获取标签路径
    143. label_path = self.label_files[index % len(self.img_files)].rstrip()
    144. label_path = 'F:\\cv\\PyTorch-YOLOv3\\PyTorch-YOLOv3\\data\\coco\\labels' + label_path
    145. # print (label_path)
    146. targets = None
    147. if os.path.exists(label_path): # 读取某张图片的标签信息
    148. # 读取一张图片内的边界框:txt文件包含的边界框的坐标信息是归一化后的坐标
    149. # [0class_id, 1x_c, 2y_c, 3w, 4h] 归一化的, 归一化是为了加速模型的收敛
    150. boxes = torch.from_numpy(
    151. np.loadtxt(label_path).reshape(-1, 5))
    152. # np.loadtxt()函数主要将标签里的值转化为araray
    153. # 将归一化后的坐标变为适应于原图片的坐标
    154. # 使用(x_c, y_c, w, h)获取真实坐标(左上,右下)
    155. x1 = w_factor * (boxes[:, 1] - boxes[:, 3] / 2)
    156. y1 = h_factor * (boxes[:, 2] - boxes[:, 4] / 2)
    157. x2 = w_factor * (boxes[:, 1] + boxes[:, 3] / 2)
    158. y2 = h_factor * (boxes[:, 2] + boxes[:, 4] / 2)
    159. # 将坐标变为适应于填充为正方形后图片的坐标
    160. # 标注要和原图做相同的调整 pad(0左,1右,2上,3下)
    161. x1 += pad[0]
    162. y1 += pad[2]
    163. x2 += pad[1]
    164. y2 += pad[3]
    165. # 将边界框的信息转变为(x,y,w,h)形式,并归一化
    166. # (padded_w, padded_h)是当前padding之后图片的宽度
    167. boxes[:, 1] = ((x1 + x2) / 2) / padded_w
    168. boxes[:, 2] = ((y1 + y2) / 2) / padded_h
    169. # (w_factor, h_factor)是原始图的宽高
    170. boxes[:, 3] *= w_factor / padded_w
    171. boxes[:, 4] *= h_factor / padded_h
    172. # #长度为6:(0,类别索引,x,y,w,h)
    173. targets = torch.zeros((len(boxes), 6))
    174. targets[:, 1:] = boxes
    175. # Apply augmentations
    176. if self.augment:
    177. if np.random.random() < 0.5:
    178. img, targets = horisontal_flip(img, targets) # 数据增强
    179. # 返回index对应的图片路径,填充和调整大小之后的图片,
    180. # 图片标签归一化后的格式 (img_id, class_id, x_c, y_c, w, h)
    181. return img_path, img, targets
    182. # collate_fn:实现自定义的batch输出。如何取样本的,定义自己的函数来准确地实现想要的功能,并给target赋予索引
    183. def collate_fn(self, batch):
    184. paths, imgs, targets = list(zip(*batch)) # #获取批量的图片路径、图片、标签
    185. # target的每个元素为每张图片的所有边界框的信息
    186. targets = [boxes for boxes in targets if boxes is not None]
    187. # 读取target的每个元素,每个元素为一张图片的所有边界框信息,并微每张图片的边界框标相同的序号
    188. for i, boxes in enumerate(targets):
    189. boxes[:, 0] = i # 为每个边界框增加索引,序号
    190. targets = torch.cat(targets, 0) # 直接将一个batch中所有的bbox合并在一起,计算loss时是按batch计算
    191. # Selects new image size every tenth batch
    192. if self.multiscale and self.batch_count % 10 == 0:
    193. self.img_size = random.choice(range(self.min_size, self.max_size + 1, 32))
    194. # Resize images to input shape
    195. # 每10个样本随机调整图像大小
    196. imgs = torch.stack([resize(img, self.img_size) for img in imgs]) # 调整图像大小放入栈中
    197. self.batch_count += 1
    198. return paths, imgs, targets # 返回归一化后的[img_id, class_id, x_c, y_c, h, w]
    199. def __len__(self):
    200. return len(self.img_files)

     4.3 logger.py

            用来将监控数据写入文件系统(日志),保存训练的某些信息。如损失等。这个logger类在train.py中使用,在训练过程中保存一些信息到日志文件。

    1. import os
    2. import datetime
    3. from torch.utils.tensorboard import SummaryWriter
    4. '''
    5. 用来将监控数据写入文件系统(日志),保存训练的某些信息。如损失等。
    6. 这个logger类在train.py中使用,在训练过程中保存一些信息到日志文件。
    7. '''
    8. class Logger(object):
    9. def __init__(self, log_dir, log_hist=True):
    10. """Create a summary writer logging to log_dir."""
    11. if log_hist: # Check a new folder for each log should be dreated
    12. log_dir = os.path.join(
    13. log_dir,
    14. datetime.datetime.now().strftime("%Y_%m_%d__%H_%M_%S"))
    15. self.writer = SummaryWriter(log_dir)
    16. def scalar_summary(self, tag, value, step):#将监控数据写入日志
    17. """Log a scalar variable."""
    18. self.writer.add_scalar(tag, value, step)
    19. self.writer.flush()
    20. def list_of_scalars_summary(self, tag_value_pairs, step):#将监控数据批量写入日志
    21. """Log scalar variables."""
    22. for tag, value in tag_value_pairs:
    23. self.writer.add_scalar(tag, value, step)
    24. self.writer.flush()

    4.4 parse_config.py

    包含两个解析器:
      1.模型配置解析器:返回一个列表model_defs,列表的每一个元素为一个字典,字典代表模型某一个层(模块)的信息 。
      2.数据配置解析器:返回一个字典,每一个键值对描述了,数据的名称路径,或其他信息。

    1. """ """
    2. '''包含两个解析器:
    3.   1.模型配置解析器:返回一个列表model_defs,列表的每一个元素为一个字典,字典代表模型某一个层(模块)的信息 。
    4.   2.数据配置解析器:返回一个字典,每一个键值对描述了,数据的名称路径,或其他信息。
    5. 模型配置解析器:解析yolo-v3层配置文件函数,并返回模块定义module_defs,path就是yolov3.cfg路径
    6. '''
    7. '''
    8. 模型配置解析器:解析yolo-v3层配置文件函数,并返回模块定义module_defs,path就是yolov3.cfg路径
    9. '''
    10. def parse_model_config(path):
    11. '''
    12. 看此函数,一定要先看config文件夹下的yolov3.cfg文件,如下是yolov3。cfg的一部分内容展示:
    13. [convolutional]
    14. batch_normalize=1
    15. filters=32
    16. size=3
    17. stride=1
    18. pad=1
    19. activation=leaky
    20. # Downsample
    21. [convolutional]
    22. batch_normalize=1
    23. filters=64
    24. size=3
    25. stride=2
    26. pad=1
    27. activation=leaky
    28. 。。。
    29. :param path: 模型配置文件路径,yolov3.cfg的路径
    30. :return: 模型定义,列表类型,列表中的元素是字典,字典包含了每一个模块的定义参数
    31. '''
    32. # 打开yolov3.cfg文件,并将文件内容存入列表,列表的每一个元素为文件的一行数据。
    33. file = open(path, 'r')
    34. lines = file.read().split('\n')
    35. lines = [x for x in lines if x and not x.startswith('#') ] # 不读取注释
    36. lines = [x.rstrip().lstrip() for x in lines] # 去除边缘空白
    37. # 定义一个列表modle_defs
    38. module_defs = []
    39. # 读取cfg的每一行内容:
    40. # 1.如果该行内容以[开头:代表是模型的一个新块的开始,给module_defs列表新增一个字典
    41. # 字典的‘type’=[]内的内容,如果[]内的内容是convolution,则字典添加'batch_normalize':0
    42. # 2.如果该行内容不以[开头,代表是块的具体内容
    43. # 等号前的值为字典的key,等号后的值为字典的value
    44. for line in lines : # 读取yolov3.cfg文件的每一行
    45. # 如果一行内容以[开头说明是一个模型的开始,[]里的内容是模块的名称,如[convolutional][convolutional][shortcut]。。。。
    46. if line.startswith('['): # This marks the start of a new block
    47. # 将一个空字典添加到模型定义module_defs列表中
    48. module_defs.append({})
    49. # 给该字典内容赋值:例{’type‘:’convolutional‘}
    50. module_defs[-1]['type'] = line[1:-1].rstrip()
    51. # 如果当前的模块是convolutional模块,给字典的内容赋值:{’type‘:’convolutional‘,'batch_normalize':0}
    52. if module_defs[-1]['type'] == 'convolutional':
    53. module_defs[-1]['batch_normalize'] = 0
    54. # 如果一行内容不以[开头说明是模块里的具体内容
    55. else:
    56. key, value = line.split("=")
    57. value = value.strip( ) # strip()删除头尾空格,rstrip()删除结尾空格
    58. # 将该行内容添加到字典中,key为等式左边的内容,value为等式右边的内容
    59. module_defs[-1][key.rstrip()] = value.strip()
    60. return module_defs # 模型定义,是一个列表,列表每一个元素为一个字典,字典包含一个模块的具体信息
    61. '''数据配置解析器:参数path为配置文件的路径'''
    62. def parse_data_config(path):
    63. """
    64. 数据配置包含的信息:
    65. classes= 80
    66. train=data/coco/trainvalno5k.txt
    67. valid=data/coco/5k.txt
    68. names=data/coco.names
    69. backup=backup/
    70. eval=coco
    71. """
    72. # 创建一个字典
    73. options = dict()
    74. # 为字典添加元素
    75. options['gpus'] = '0,1,2,3'
    76. options['num_workers'] = '10'
    77. # 读取数据配置文件的每一行,并将每一行的信息以键值对的形式存入字典中
    78. with open(path, 'r') as fp:
    79. lines = fp.readlines()
    80. for line in lines:
    81. line = line.strip()
    82. if line == '' or line.startswith('#'):
    83. continue
    84. key, value = line.split('=')
    85. options[key.strip()] = value.strip()
    86. return options # 返回一个字典,字典的key为名称(train,valid,names..),value为路径或其他信息

    4.5 utils.py

          utils.py是项目的工具文件

    1. from __future__ import division
    2. import tqdm
    3. import torch
    4. import numpy as np
    5. def to_cpu(tensor):
    6. return tensor.detach().cpu()
    7. '''加载数据集类别信息:返回类别组成的列表'''
    8. def load_classes(path):#参数为类别名称文件的路径。例coco.names或classes.names的路径
    9. fp = open(path, "r")
    10. names = fp.read().split("\n")[:-1]#将文件的每一行数据存入列表,这使得数据集的每个类别的名称存入到一个列表
    11. return names#返回类别名称构成的列表
    12. '''权重初始化函数'''
    13. def weights_init_normal(m):
    14. classname = m.__class__.__name__
    15. if classname.find("Conv") != -1:#卷积层权重初始化设置
    16. torch.nn.init.normal_(m.weight.data, 0.0, 0.02)
    17. elif classname.find("BatchNorm2d") != -1:#批量归一化层权重初始化设置
    18. torch.nn.init.normal_(m.weight.data, 1.0, 0.02)
    19. torch.nn.init.constant_(m.bias.data, 0.0)
    20. '''改变预测边界框的尺寸函数:参数为,边界框、当前的图片尺寸(标量)、原始图片尺寸。因为网络预测的边界框信息是,
    21. 对图像填充、调整大小后的图片进行预测的结果,因此需要对预测的边界框进行调整使其适应于原图的目标'''
    22. def rescale_boxes(boxes, current_dim, original_shape):
    23. #原始图片的高和宽
    24. orig_h, orig_w = original_shape
    25. #原始图片的填充信息:根据原图的宽高的差值来计算。
    26. #pad_x为宽天长的像素数量, pad_y为高填充的像素数量
    27. pad_x = max(orig_h - orig_w, 0) * (current_dim / max(original_shape))# 原图的高大于宽。改变后图片的大小/原图的最长边的尺寸=缩放比率
    28. pad_y = max(orig_w - orig_h, 0) * (current_dim / max(original_shape))
    29. #将预测的边界框信息,调整为适应于原图
    30. unpad_h = current_dim - pad_y
    31. unpad_w = current_dim - pad_x
    32. # 改变预测边界框的尺寸,使其是适用于原图片
    33. boxes[:, 0] = ((boxes[:, 0] - pad_x // 2) / unpad_w) * orig_w#左上x的坐标
    34. boxes[:, 1] = ((boxes[:, 1] - pad_y // 2) / unpad_h) * orig_h#左上y的坐标
    35. boxes[:, 2] = ((boxes[:, 2] - pad_x // 2) / unpad_w) * orig_w
    36. boxes[:, 3] = ((boxes[:, 3] - pad_y // 2) / unpad_h) * orig_h
    37. return boxes#返回调整后的预测边界框的信息/
    38. '''将边界框信息转换为左上右下坐标表示函数'''
    39. def xywh2xyxy(x):
    40. y = x.new(x.shape)
    41. y[..., 0] = x[..., 0] - x[..., 2] / 2
    42. y[..., 1] = x[..., 1] - x[..., 3] / 2
    43. y[..., 2] = x[..., 0] + x[..., 2] / 2
    44. y[..., 3] = x[..., 1] + x[..., 3] / 2
    45. return y
    46. """度量计算:参数为true_positive(值为0或1,list)、预测置信度(list),预测类别(list),真实类别(list)
    47. 返回:p, r, ap, f1, unique_classes.astype("int32")"""
    48. def ap_per_class(tp, conf, pred_cls, target_cls):#参数:true_positives, pred_scores, pred_labels 、图片真实标签信息
    49. # 按照置信度排序,后的tp, conf, pred_cls
    50. i = np.argsort(-conf)
    51. #print('所有预测框的个数为',len(i))
    52. tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]#按照置信度排序后的tp(值为0,1), conf, pred_cls
    53. #print('tp[i]',tp[i])
    54. # 获取图片中真实框所包含的类别(类别不重复)
    55. unique_classes = np.unique(target_cls)
    56. #print('unique_classes',unique_classes)
    57. # Create Precision-Recall curve and compute AP for each class
    58. ap, p, r = [], [], []
    59. for c in tqdm.tqdm(unique_classes, desc="Computing AP"):#为每一个类别计算AP
    60. # i:对于所有预测边界框的类pred_cls,判断与当前c类是否相同,相同则该位置为true否则为false,得到与pred_class形状相同的布尔列表
    61. i = pred_cls == c
    62. # ground truth 中类别为c的数量
    63. n_gt = (target_cls == c).sum()
    64. #预测边界框中类别为c的数量
    65. n_p = i.sum()
    66. if n_p == 0 and n_gt == 0:
    67. continue
    68. elif n_p == 0 or n_gt == 0:
    69. ap.append(0)
    70. r.append(0)
    71. p.append(0)
    72. else:
    73. # 计算FP和TP
    74. fpc = (1 - tp[i]).cumsum()#i列表记录着索引对应位置是否是c类别的边界框,tp记录着索引对应位置是否是正例框
    75. tpc = (tp[i]).cumsum()
    76. # print('tp[i]',tp[i],len(tp[i]))#tp[i]是所有框中类别为c的预测框的true_positive信息(值为0或1,1代表与真值框iou大于阈值)
    77. # print('fpc',fpc,len(fpc))#fpc为类别为c的预测框中为正例的预测框
    78. # print('tpc', tpc,len(tpc))#tpc为类别为c的预测框中为负例的预测框
    79. #计算召回率
    80. recall_curve = tpc / (n_gt + 1e-16)
    81. #print('recall_curve',recall_curve)
    82. r.append(recall_curve[-1])
    83. #print('r',r)
    84. #计算精度
    85. precision_curve = tpc / (tpc + fpc)
    86. #print('precision_curve',precision_curve)
    87. p.append(precision_curve[-1])
    88. #print('p',p)
    89. # 计算AP:AP from recall-precision curve
    90. ap.append(compute_ap(recall_curve, precision_curve))
    91. # Compute F1 score (harmonic mean of precision and recall)
    92. p, r, ap = np.array(p), np.array(r), np.array(ap)
    93. f1 = 2 * p * r / (p + r + 1e-16)
    94. return p, r, ap, f1, unique_classes.astype("int32")
    95. """计算AP"""
    96. def compute_ap(recall, precision):#参数精度和召回率
    97. # correct AP calculation
    98. # 给Precision-Recall曲线添加头尾
    99. mrec = np.concatenate(([0.0], recall, [1.0]))
    100. mpre = np.concatenate(([0.0], precision, [0.0]))
    101. # compute the precision envelope
    102. # 简单的应用了一下动态规划,实现在recall=x时,precision的数值是recall=[x, 1]范围内的最大precision
    103. for i in range(mpre.size - 1, 0, -1):
    104. mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
    105. # to calculate area under PR curve, look for points
    106. # where X axis (recall) changes value
    107. # 寻找recall[i]!=recall[i+1]的所有位置,即recall发生改变的位置,方便计算PR曲线下的面积,即AP
    108. i = np.where(mrec[1:] != mrec[:-1])[0]
    109. # and sum (\Delta recall) * prec
    110. # 用积分法求PR曲线下的面积,即AP
    111. ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
    112. return ap
    113. '''统计信息计算:参数,模型预测输出(NMS处理后的结果),真实标签(适应于原图的x,y,x,y),iou阈值。
    114. 返回,true_positive(值为0/1,如果预测边界框与真实边界框重叠度大则值为1,否则为0),预测置信度,预测类别'''
    115. def get_batch_statistics(outputs, targets, iou_threshold):
    116. # outputs为非极大值抑制后的结果(x,y,x,y,object_confs,class_confs,class_preds)长度为7
    117. batch_metrics = []
    118. for sample_i in range(len(outputs)):#遍历每个output的边界框,因为是批量操作的,每个批量有很多图片,每个图片对应一个output,所以遍历每个output
    119. if outputs[sample_i] is None:
    120. continue
    121. '''图片的预测信息:'''
    122. output = outputs[sample_i]#取第sample_i个output信息,每个output里面包含很多边界框
    123. pred_boxes = output[:, :4]#预测边界框的坐标信息
    124. pred_scores = output[:, 4]#预测边界框的置信度
    125. pred_labels = output[:, -1]#预测边界框的类别
    126. true_positives = np.zeros(pred_boxes.shape[0])#true_positive的长度为pre_boxes的个数
    127. '''图片的标注信息(groundtruth):'''
    128. #坐标信息,格式为(xyxy)
    129. annotations = targets[targets[:, 0] == sample_i][:, 1:]#这句把对应ID下的target和图像进行匹配,dataset.py里的ListDataset类里的collate_fn函数给target赋予ID
    130. #类别信息
    131. target_labels = annotations[:, 0] if len(annotations) else []
    132. if len(annotations):
    133. detected_boxes = []#创建空列表
    134. target_boxes = annotations[:, 1:]#真实边界框(groundtruth)坐标
    135. for pred_i, (pred_box, pred_label) in enumerate(zip(pred_boxes, pred_labels)):#遍历预测框:坐标和类别
    136. if len(detected_boxes) == len(annotations):
    137. break
    138. # Ignore if label is not one of the target labels
    139. if pred_label not in target_labels:
    140. continue
    141. # 计算预测框和真实框的IOU
    142. iou, box_index = bbox_iou(pred_box.unsqueeze(0), target_boxes).max(0)
    143. #如果预测框和真实框的IOU大于阈值,那么可以认为该预测边界框预测’正确‘,并把该边界框的true_positives值设置为1
    144. if iou >= iou_threshold and box_index not in detected_boxes:
    145. true_positives[pred_i] = 1
    146. detected_boxes += [box_index]
    147. batch_metrics.append([true_positives, pred_scores, pred_labels])
    148. return batch_metrics#true_positive,预测置信度,预测类别
    149. """未用到"""
    150. def bbox_wh_iou(wh1, wh2):
    151. wh2 = wh2.t()
    152. w1, h1 = wh1[0], wh1[1]
    153. w2, h2 = wh2[0], wh2[1]
    154. inter_area = torch.min(w1, w2) * torch.min(h1, h2)
    155. union_area = (w1 * h1 + 1e-16) + w2 * h2 - inter_area
    156. return inter_area / union_area
    157. """计算两个边界框的IOU值"""
    158. def bbox_iou(box1, box2, x1y1x2y2=True):
    159. #获取边界框的左上右下坐标值
    160. if not x1y1x2y2:
    161. #如果边界框的表示方式为(center_x,center_y,width,height)则转换表示格式为(x,y,x,y)
    162. b1_x1, b1_x2 = box1[:, 0] - box1[:, 2] / 2, box1[:, 0] + box1[:, 2] / 2
    163. b1_y1, b1_y2 = box1[:, 1] - box1[:, 3] / 2, box1[:, 1] + box1[:, 3] / 2
    164. b2_x1, b2_x2 = box2[:, 0] - box2[:, 2] / 2, box2[:, 0] + box2[:, 2] / 2
    165. b2_y1, b2_y2 = box2[:, 1] - box2[:, 3] / 2, box2[:, 1] + box2[:, 3] / 2
    166. else:
    167. b1_x1, b1_y1, b1_x2, b1_y2 = box1[:, 0], box1[:, 1], box1[:, 2], box1[:, 3]#box1的左上右下坐标
    168. b2_x1, b2_y1, b2_x2, b2_y2 = box2[:, 0], box2[:, 1], box2[:, 2], box2[:, 3]#box1的左上右下坐标
    169. #相交矩形的左上右下坐标
    170. inter_rect_x1 = torch.max(b1_x1, b2_x1)
    171. inter_rect_y1 = torch.max(b1_y1, b2_y1)
    172. inter_rect_x2 = torch.min(b1_x2, b2_x2)
    173. inter_rect_y2 = torch.min(b1_y2, b2_y2)
    174. # 相交矩形的面积
    175. inter_area = torch.clamp(inter_rect_x2 - inter_rect_x1 + 1, min=0) * torch.clamp(
    176. inter_rect_y2 - inter_rect_y1 + 1, min=0
    177. )
    178. #并集的面积
    179. b1_area = (b1_x2 - b1_x1 + 1) * (b1_y2 - b1_y1 + 1)
    180. b2_area = (b2_x2 - b2_x1 + 1) * (b2_y2 - b2_y1 + 1)
    181. iou = inter_area / (b1_area + b2_area - inter_area + 1e-16)
    182. return iou#返回重叠度IOU的值
    183. '''非极大值抑制函数:返回边界框【x1,y1,x2,y2,conf,class_conf,class_pred】,参数为,模型预测,置信度阈值,nms阈值'''
    184. def non_max_suppression(prediction, conf_thres=0.5, nms_thres=0.4):
    185. """
    186. Removes detections with lower object confidence score than 'conf_thres' and performs Non-Maximum Suppression to further filter detections.
    187. Returns detections with shape:
    188. (x1, y1, x2, y2, object_conf, class_score, class_pred)
    189. """
    190. """(1)模型预测坐标格式转变: (center x, center y, width, height) to (x1, y1, x2, y2)"""
    191. #三个yolo层,有三个尺寸的输出分别为13,26,52,所以对于一张图片,
    192. # 模型输出的shape是(10647,85),(13*13+26*26+52*52)*3=10647,后面的85是(x,y,w,h, conf, cls) xywh加一个置信度加80个分类。
    193. #prediction的形状为[1, 10647, 85],85的前4个信息为坐标信息(center x, center y, width, height)
    194. # 第5个信息为目标置信度,第6-85的信息为80个类的置信度
    195. prediction[..., :4] = xywh2xyxy(prediction[..., :4])# 将模型预测的坐标信息由(center x, center y, width, height) 格式转变为 (x1, y1, x2, y2)格式
    196. output = [None for _ in range(len(prediction))]
    197. #遍历每个图片,每张图片的预测image_pred:
    198. for image_i, image_pred in enumerate(prediction):#遍历预测边界框
    199. """(2)边界框筛选:去除目标置信度低于阈值的边界框"""
    200. image_pred = image_pred[image_pred[:, 4] >= conf_thres]#筛选每幅图片预测边界框中目标置信度大于阈值的边界框
    201. # If none are remaining => process next image
    202. if not image_pred.size(0):#判断本图片经过目标置信度阈值的赛选是否还存在边界框,如果没有边界框则执行下一个图片的NMS
    203. continue
    204. """(3)非极大值抑制:根据score进行排序得到最大值,找到和这个score最大的预测类别相同的计算iou值,通过加权计算,得到最终的预测框(xyxy),最后从prediction中去掉iou大于设置的iou阈值的边界框。"""
    205. # 分数=目标置信度*80个类别得分的最大值。
    206. score = image_pred[:, 4] * image_pred[:, 5:].max(1)[0]
    207. # 根据score为图片中的预测边界框进行排序
    208. image_pred = image_pred[(-score).argsort()]#形状【经过置信度阈值筛选后的边界框数量,85】
    209. #类别置信度最大值和类别置信度最大值所在位置(索引,也就是预测的类别)
    210. class_confs, class_preds = image_pred[:, 5:].max(1, keepdim=True)#
    211. detections = torch.cat((image_pred[:, :5], class_confs.float(), class_preds.float()), 1)#(x,y,x,y,object_confs,class_confs,class_preds)长度为7
    212. keep_boxes = []
    213. while detections.size(0):
    214. # 将当前第一个边界框(当前分数最高的边界框)与剩余边界框计算IoU,并且大于NMS阈值的边界框
    215. #第一个bbx与其余bbx的iou大于nms_thres的判别(0, 1), 1为大于,0为小于
    216. large_overlap = bbox_iou(detections[0, :4].unsqueeze(0), detections[:, :4]) > nms_thres
    217. # 判断他们的类别是否相同,只有相同时才进行nms, 相同时为1, 不同时为0
    218. label_match = detections[0, -1] == detections[:, -1]
    219. # invalid 为Indices of boxes with lower confidence scores, large IOUs and matching labels
    220. # 只有在两个bbx的iou大于thres,且类别相同时,invalid为True,其余为False
    221. invalid = large_overlap & label_match
    222. # weights为对应的权值, 其格式为:将True bbx中的confidence连成一个Tensor
    223. weights = detections[invalid, 4:5]
    224. # Merge overlapping bboxes by order of confidence
    225. # 这里得到最后的bbx它是跟他满足IOU大于threshold,并且相同label的一些bbx,根据confidence重新加权得到
    226. # 并不是原始bbx的保留。
    227. detections[0, :4] = (weights * detections[invalid, :4]).sum(0) / weights.sum()
    228. keep_boxes += [detections[0]]
    229. ## 去掉这些invalid,即iou大于阈值且预测同一类
    230. detections = detections[~invalid]
    231. if keep_boxes:
    232. output[image_i] = torch.stack(keep_boxes)
    233. return output#返回NMS后的边界框(x,y,x,y,object_confs,class_confs,class_preds)长度为7、
    234. def build_targets(pred_boxes, pred_cls, target, anchors, ignore_thres):
    235. ByteTensor = torch.cuda.ByteTensor if pred_boxes.is_cuda else torch.ByteTensor
    236. FloatTensor = torch.cuda.FloatTensor if pred_boxes.is_cuda else torch.FloatTensor
    237. nB = pred_boxes.size(0)
    238. nA = pred_boxes.size(1)
    239. nC = pred_cls.size(-1)
    240. nG = pred_boxes.size(2)
    241. # Output tensors
    242. obj_mask = ByteTensor(nB, nA, nG, nG).fill_(0)
    243. noobj_mask = ByteTensor(nB, nA, nG, nG).fill_(1)
    244. class_mask = FloatTensor(nB, nA, nG, nG).fill_(0)
    245. iou_scores = FloatTensor(nB, nA, nG, nG).fill_(0)
    246. tx = FloatTensor(nB, nA, nG, nG).fill_(0)
    247. ty = FloatTensor(nB, nA, nG, nG).fill_(0)
    248. tw = FloatTensor(nB, nA, nG, nG).fill_(0)
    249. th = FloatTensor(nB, nA, nG, nG).fill_(0)
    250. tcls = FloatTensor(nB, nA, nG, nG, nC).fill_(0)
    251. # Convert to position relative to box
    252. target_boxes = target[:, 2:6] * nG
    253. gxy = target_boxes[:, :2]
    254. gwh = target_boxes[:, 2:]
    255. # Get anchors with best iou
    256. ious = torch.stack([bbox_wh_iou(anchor, gwh) for anchor in anchors])
    257. best_ious, best_n = ious.max(0)
    258. # Separate target values
    259. b, target_labels = target[:, :2].long().t()
    260. gx, gy = gxy.t()
    261. gw, gh = gwh.t()
    262. gi, gj = gxy.long().t()
    263. # Set masks
    264. obj_mask[b, best_n, gj, gi] = 1
    265. noobj_mask[b, best_n, gj, gi] = 0
    266. # Set noobj mask to zero where iou exceeds ignore threshold
    267. for i, anchor_ious in enumerate(ious.t()):
    268. noobj_mask[b[i], anchor_ious > ignore_thres, gj[i], gi[i]] = 0
    269. # Coordinates
    270. tx[b, best_n, gj, gi] = gx - gx.floor()
    271. ty[b, best_n, gj, gi] = gy - gy.floor()
    272. # Width and height
    273. tw[b, best_n, gj, gi] = torch.log(gw / anchors[best_n][:, 0] + 1e-16)
    274. th[b, best_n, gj, gi] = torch.log(gh / anchors[best_n][:, 1] + 1e-16)
    275. # One-hot encoding of label
    276. tcls[b, best_n, gj, gi, target_labels] = 1
    277. # Compute label correctness and iou at best anchor
    278. class_mask[b, best_n, gj, gi] = (pred_cls[b, best_n, gj, gi].argmax(-1) == target_labels).float()
    279. iou_scores[b, best_n, gj, gi] = bbox_iou(pred_boxes[b, best_n, gj, gi], target_boxes, x1y1x2y2=False)
    280. tconf = obj_mask.float()
    281. return iou_scores, class_mask, obj_mask, noobj_mask, tx, ty, tw, th, tcls, tconf

    4.6 detect.py

    模型训练完成后,进行检测测试的文件。验证数据集在data/samples文件夹下,验证结果保存在本py文件自动创建的文件夹output文件夹下。

    1. from __future__ import division
    2. from models import *
    3. from utils.utils import *
    4. from utils.datasets import *
    5. import os
    6. import time
    7. import datetime
    8. import argparse
    9. from PIL import Image
    10. import torch
    11. from torch.utils.data import DataLoader
    12. from torch.autograd import Variable
    13. import matplotlib.pyplot as plt
    14. import matplotlib.patches as patches
    15. from matplotlib.ticker import NullLocator
    16. if __name__ == "__main__":
    17. ##########################################################################################################################
    18. '''(1)参数解析'''
    19. parser = argparse.ArgumentParser()
    20. # 测试文件夹路径
    21. parser.add_argument("--image_folder", type=str, default="data/samples", help="path to dataset")
    22. #yolov3的模型信息(网络层,每层的卷积核数量,尺寸,步长。。。)
    23. parser.add_argument("--model_def", type=str, default="config/yolov3.cfg", help="path to model definition file")
    24. #预训练模型路径
    25. parser.add_argument("--weights_path", type=str, default="weights/yolov3.weights", help="path to weights file")
    26. #类名字
    27. parser.add_argument("--class_path", type=str, default="data/coco.names", help="path to class label file")
    28. #目标置信度阈值
    29. parser.add_argument("--conf_thres", type=float, default=0.8, help="object confidence threshold")
    30. #NMS的IoU阈值
    31. parser.add_argument("--nms_thres", type=float, default=0.4, help="iou thresshold for non-maximum suppression")
    32. #批量大小
    33. parser.add_argument("--batch_size", type=int, default=1, help="size of the batches")
    34. #CPU线程
    35. parser.add_argument("--n_cpu", type=int, default=0, help="number of cpu threads to use during batch generation")
    36. #图片维度
    37. parser.add_argument("--img_size", type=int, default=416, help="size of each image dimension")
    38. #checkpoint_model
    39. parser.add_argument("--checkpoint_model", type=str, help="path to checkpoint model")
    40. opt = parser.parse_args()
    41. print(opt)
    42. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    43. os.makedirs("output", exist_ok=True)#创建预测图片的输出位置
    44. ##########################################################################################################################
    45. '''(2)模型构建'''
    46. # 加载模型:这条语句加载darkent模型结构,即YOLOv3模型。Darknet模型在model.py中进行定义。
    47. #将模型设置为评估模式
    48. model = Darknet(opt.model_def, img_size=opt.img_size).to(device)#根据模型的配置文件,搭建模型的结构
    49. #为模型结构加载训练的权重(模型参数)
    50. if opt.weights_path.endswith(".weights"):
    51. # Load darknet weights
    52. model.load_darknet_weights(opt.weights_path)
    53. else:
    54. model.load_state_dict(torch.load(opt.weights_path))
    55. model.eval() # 设置模型为评估模式,不然只要输入数据就会进行参数更新、优化
    56. ##########################################################################################################################
    57. '''(3)数据集加载、类别加载'''
    58. #加载测试的图片:
    59. # dataloader本质是一个可迭代对象,使用iter()访问,不能使用next()访问;
    60. #也可以使用`for inputs, labels in dataloaders`进行可迭代对象的访问
    61. #一般我们实现一个datasets对象,传入到dataloader中;然后内部使用yeild返回每一次batch的数据
    62. dataloader = DataLoader(
    63. ImageFolder(opt.image_folder, img_size=opt.img_size),#评估数据集,ImageFolder在datasets.py中定义,返回的是图片路径,和经过处理(填充、调整大小)的图片
    64. batch_size=opt.batch_size,
    65. shuffle=False,
    66. num_workers=opt.n_cpu,
    67. )
    68. #加载类别名,classes是一个列表
    69. classes = load_classes(opt.class_path) # Extracts class labels from file
    70. #创建保存图片路径和图片检测信息的列表
    71. imgs = []
    72. img_detections = []
    73. ##########################################################################################################################
    74. """(3)模型预测:将图片路径、图片预测结果存入imgs和img_detections列表中"""
    75. print("\nPerforming object detection:")
    76. prev_time = time.time()
    77. Tensor = torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor
    78. # 测试图片的检测:并将图片路径和检测结果信息保存
    79. # 算出batch中图片的地址img_paths和检测结果detections
    80. for batch_i, (img_paths, input_imgs) in enumerate(dataloader):#使用dataloader加载数据,加载的数据为一批量的数据
    81. # 把输入图像转换为tensor并变为变量
    82. input_imgs = Variable(input_imgs.type(Tensor))
    83. # 目标检测:使用模型检测图像,检测结果为一个张量,
    84. # 对检测结果进行非极大值抑制,得到最终结果
    85. with torch.no_grad():
    86. detections = model(input_imgs)
    87. #print(detections.shape)#[:, 10647, 85]
    88. ##非极大值抑制:将边界框信息,转变为左上右下坐标,并且去除置信度低的坐标. (x1, y1, x2, y2, object_conf, class_score, class_pred)
    89. detections = non_max_suppression(detections, opt.conf_thres, opt.nms_thres)#非极大值抑制[:,:,7]
    90. # 打印:检测时间,检测的批次
    91. current_time = time.time()
    92. inference_time = datetime.timedelta(seconds=current_time - prev_time)
    93. prev_time = current_time
    94. print("\t+ Batch %d, Inference Time: %s" % (batch_i, inference_time))
    95. # 保存图片路径,图片的检测信息(经过NMS处理后)
    96. imgs.extend(img_paths)
    97. img_detections.extend(detections)#长度为7
    98. ##########################################################################################################################
    99. """(4)将检测结果绘制到图片,并保存"""
    100. #边界框颜色
    101. cmap = plt.get_cmap("tab20b") # Bounding-box colors
    102. colors = [cmap(i) for i in np.linspace(0, 1, 20)]
    103. #遍历图片
    104. for img_i, (path, detections) in enumerate(zip(imgs, img_detections)):
    105. print("(%d) Image: '%s'" % (img_i, path))
    106. #读取图片并将图片绘制在plt.figure
    107. img = np.array(Image.open(path))#读取图片
    108. plt.figure()#创建图片画布
    109. fig, ax = plt.subplots(1)
    110. ax.imshow(img)#将读取的图片绘制到画布
    111. #将图片对应的检测的边界框和标签绘制到图片上
    112. if detections is not None:
    113. # 将检测的边界框(对填充、调整大小的原图的预测),重新设置尺寸,使其与原图目标能匹配
    114. detections = rescale_boxes(detections, opt.img_size, img.shape[:2])
    115. #获取检测结果的类标签,并为每一个类指定一种颜色
    116. unique_labels = detections[:, -1].cpu().unique()#返回参数数组中所有不同的值,并按照从小到大排序可选参数
    117. n_cls_preds = len(unique_labels)
    118. bbox_colors = random.sample(colors, n_cls_preds)#为每一类分配一个边界框颜色
    119. #遍历图片对应检测结果的每一个边界框
    120. for x1, y1, x2, y2, conf, cls_conf, cls_pred in detections:#检测结果为左上和右下坐标
    121. print("\t+ Label: %s, Conf: %.5f" % (classes[int(cls_pred)], cls_conf.item()))
    122. #边界框宽和高
    123. box_w = x2 - x1
    124. box_h = y2 - y1
    125. #将边界框写入图片中,并设置颜色
    126. color = bbox_colors[int(np.where(unique_labels == int(cls_pred))[0])]
    127. # 创建一个矩形边界框
    128. bbox = patches.Rectangle((x1, y1), box_w, box_h, linewidth=2, edgecolor=color, facecolor="none")
    129. # 吧矩形边界框写入画布
    130. ax.add_patch(bbox)
    131. # 为检测边界框添加类别信息
    132. plt.text( x1,y1,s=classes[int(cls_pred)],color="white",verticalalignment="top",bbox={"color": color, "pad": 0} )
    133. #将绘制好边界框的图片保存
    134. plt.axis("off")
    135. plt.gca().xaxis.set_major_locator(NullLocator())
    136. plt.gca().yaxis.set_major_locator(NullLocator())
    137. filename = path.split("/")[-1].split(".")[0]
    138. plt.savefig(f"output/{filename}.png", bbox_inches="tight", pad_inches=0.0)
    139. plt.close()

    4.7 models.py

           定义模型结构的文件,根据模型的配置文件信息,来构建模型结构。 

    1. from __future__ import division
    2. import torch
    3. import torch.nn as nn
    4. import torch.nn.functional as F
    5. import numpy as np
    6. from utils.parse_config import *
    7. from utils.utils import build_targets, to_cpu
    8. '''构建网络函数:通过获取的模型定义module_defs来构建YOLOv3模型结构,根据module_defs中的模块配置构造层块的模块列表'''
    9. def create_modules(module_defs):
    10. '''构建模型结构'''
    11. '''(1)解析模型超参数,获取模型的输入通道数'''
    12. #从model_def获取net的配置信息组成的字典hyperparams。model_def是由parse_config函数解析出来的列表,每个元素为一个字典,每一个字典包含了某层、模块的参数信息
    13. hyperparams = module_defs.pop(0)#hyperparams为module_defs的第一个字典元素,是模型的超参数信息{'type': 'net',...}
    14. output_filters = [int(hyperparams["channels"])]
    15. '''(2)构建nn.ModuleList(),用来存放创建的网络层、模块'''
    16. module_list = nn.ModuleList()
    17. '''(3)遍历模型定义列表的每个字典元素,创建相应的层、模块,添加到nn.ModuleList()中'''
    18. #遍历 module_defs的每个字典,根据字典内容,创建相应的层或模块。其中字典的type的值有一下几种:"convolutional","maxpool"
    19. #"upsample", "route","shortcut", "yolo"
    20. for module_i, module_def in enumerate(module_defs):
    21. #创建一个 nn.Sequential()
    22. modules = nn.Sequential()
    23. #卷积层构建,并添加到nn.Sequential()
    24. if module_def["type"] == "convolutional":
    25. #获取convolutional层的参数信息
    26. bn = int(module_def["batch_normalize"])
    27. filters = int(module_def["filters"])
    28. kernel_size = int(module_def["size"])
    29. pad = (kernel_size - 1) // 2
    30. #创建convolution层:根据convolutional层的参数信息,创建convolutional层,并将改层加入到nn.Sequential()中
    31. modules.add_module(f"conv_{module_i}",#层在模型中的名字
    32. nn.Conv2d(#层
    33. in_channels=output_filters[-1],#输入的通道数
    34. out_channels=filters,#输出的通道数
    35. kernel_size=kernel_size,#卷结核大小
    36. stride=int(module_def["stride"]),#步长
    37. padding=pad,#填充
    38. bias=not bn,
    39. ),
    40. )
    41. if bn:
    42. #添加BatchNorm2d层
    43. modules.add_module(f"batch_norm_{module_i}", nn.BatchNorm2d(filters, momentum=0.9, eps=1e-5))
    44. if module_def["activation"] == "leaky":
    45. #添加激活层LeakyReLU
    46. modules.add_module(f"leaky_{module_i}", nn.LeakyReLU(0.1))
    47. #池化层构建,并添加到nn.Sequential()
    48. elif module_def["type"] == "maxpool":
    49. # 获取maxpool层的参数信息
    50. kernel_size = int(module_def["size"])
    51. stride = int(module_def["stride"])
    52. # 根据maxpool层的参数信息,创建maxpool层,并将改层加入到 nn.Sequential()中
    53. if kernel_size == 2 and stride == 1:
    54. modules.add_module(f"_debug_padding_{module_i}", nn.ZeroPad2d((0, 1, 0, 1)))
    55. #创建maxpool层
    56. modules.add_module(f"maxpool_{module_i}",
    57. nn.MaxPool2d(
    58. kernel_size=kernel_size, #卷积核大小
    59. stride=stride, #步长
    60. padding=int((kernel_size - 1) // 2))#填充
    61. )
    62. #上采样层构建,并添加到nn.Sequential()
    63. #上采样层是自定义的层,需要实例化Upsample为一个对象,将对象层添加到模型列表中
    64. elif module_def["type"] == "upsample":
    65. #上采样的配置例,如下
    66. # [upsample]
    67. # stride = 2
    68. # 构建upsample层,上采样层类,重写了forward函数
    69. upsample = Upsample(scale_factor=int(module_def["stride"]), mode="nearest")
    70. #层添加到模型
    71. modules.add_module(f"upsample_{module_i}", upsample)
    72. elif module_def["type"] == "route":
    73. #youte信息,例
    74. # [route]
    75. # layers = -1, 36
    76. # 获取route层的参数信息
    77. layers = [int(x) for x in module_def["layers"].split(",")]
    78. filters = sum([output_filters[1:][i] for i in layers])
    79. modules.add_module(f"route_{module_i}", EmptyLayer())#EmptyLayer()为“路线”和“快捷方式”层的占位符
    80. elif module_def["type"] == "shortcut":
    81. filters = output_filters[1:][int(module_def["from"])]
    82. modules.add_module(f"shortcut_{module_i}", EmptyLayer())#EmptyLayer()为“路线”和“快捷方式”层的占位符
    83. elif module_def["type"] == "yolo":
    84. #例:假设yolo的配置信息如下
    85. # [yolo]
    86. # mask = 3,4,5
    87. # anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326
    88. # classes=80
    89. # num=9
    90. # jitter=.3
    91. # ignore_thresh = .7
    92. # truth_thresh = 1
    93. # random=1
    94. #获取anchor的索引,上例为3,4,5
    95. anchor_idxs = [int(x) for x in module_def["mask"].split(",")]
    96. #提取anchor尺寸信息,放入列表
    97. anchors = [int(x) for x in module_def["anchors"].split(",")]
    98. anchors = [(anchors[i], anchors[i + 1]) for i in range(0, len(anchors), 2)]
    99. anchors = [anchors[i] for i in anchor_idxs]
    100. num_classes = int(module_def["classes"])
    101. #print('anchors1:', anchors)#上例为anchors1: [(30, 61), (62, 45), (59, 119)]
    102. #获取图片的输入尺寸
    103. img_size = int(hyperparams["height"])
    104. #定义yolo检测层:实例化yolo类,创建yolo层,传入的参数为三个anchor的尺寸,类别的数量,图像的大小
    105. yolo_layer = YOLOLayer(anchors, num_classes, img_size)
    106. #将YOLO层加入到模型列表
    107. modules.add_module(f"yolo_{module_i}", yolo_layer)
    108. module_list.append(modules) #将创建的nn.Sequential()即创建的层,添加到 nn.ModuleList()中
    109. output_filters.append(filters)#将创建的层的输出通道数添加到filters列表中,作为下次创建层的输入通道数
    110. return hyperparams, module_list#返回网络的参数、网络结构即层组成的列表
    111. '''上采样层'''
    112. class Upsample(nn.Module):
    113. """ nn.Upsample 被重写 """
    114. def __init__(self, scale_factor, mode="nearest"):
    115. super(Upsample, self).__init__()
    116. self.scale_factor = scale_factor#上采样步长
    117. self.mode = mode
    118. def forward(self, x):
    119. x = F.interpolate(x, scale_factor=self.scale_factor, mode=self.mode)#上采样方法,插值
    120. return x#返回上采样结果
    121. '''emptylayer定义'''
    122. class EmptyLayer(nn.Module):
    123. """Placeholder for 'route' and 'shortcut' layers"""
    124. def __init__(self):
    125. super(EmptyLayer, self).__init__()
    126. '''yolo层定义:检测层'''
    127. class YOLOLayer(nn.Module):
    128. """Detection layer"""
    129. def __init__(self, anchors, num_classes, img_dim=416):#参数为三个anchor的尺寸,类别的数量,图像的大小
    130. super(YOLOLayer, self).__init__()
    131. #基础设置
    132. self.anchors = anchors#anchor的尺寸信息,例某一层yolo尺寸为[(30, 61), (62, 45), (59, 119)]
    133. self.num_anchors = len(anchors)#anchor的数量
    134. self.num_classes = num_classes#类别的数量
    135. self.ignore_thres = 0.5
    136. self.mse_loss = nn.MSELoss()
    137. self.bce_loss = nn.BCELoss()
    138. self.obj_scale = 1
    139. self.noobj_scale = 100
    140. self.metrics = {}
    141. self.img_dim = img_dim
    142. self.grid_size = 0 # grid size
    143. #计算网格单元偏移
    144. def compute_grid_offsets(self, grid_size, cuda=True):
    145. #获取网格尺寸(几×几)
    146. self.grid_size = grid_size
    147. g = self.grid_size
    148. # print('g',g) g可能的取值为13/26/52,对应不同yolo层的特征图的尺寸
    149. FloatTensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor
    150. #获取网格单元大小
    151. self.stride = self.img_dim / self.grid_size#网格单元的尺寸
    152. # Calculate offsets for each grid,假设g取13,
    153. #torch.arange(g) 为tensor([0,1,2,3,4,5,6,7,8,9,10,11,12])
    154. #torch.arange(g).repeat(g, 1) 为由tensor([0,1,2,3,4,5,6,7,8,9,10,11,12])组成的13行一列的张量
    155. #torch.arange(g).repeat(g, 1).view([1, 1, g, g]) 改变视图为【1,1,13,13】
    156. self.grid_x = torch.arange(g).repeat(g, 1).view([1, 1, g, g]).type(FloatTensor)#
    157. self.grid_y = torch.arange(g).repeat(g, 1).t().view([1, 1, g, g]).type(FloatTensor)
    158. #把anchor的宽和高转变为相对于网格单元大小的度量
    159. self.scaled_anchors = FloatTensor([(a_w / self.stride, a_h / self.stride) for a_w, a_h in self.anchors])#例某一层yolo尺寸为[(30, 61), (62, 45), (59, 119)]
    160. self.anchor_w = self.scaled_anchors[:, 0:1].view((1, self.num_anchors, 1, 1))#获取anchor的宽
    161. self.anchor_h = self.scaled_anchors[:, 1:2].view((1, self.num_anchors, 1, 1))#获取anchor的高
    162. def forward(self, x, targets=None, img_dim=None):
    163. #yolo层的前向传播,参数为yolo层来自上层的输出作为输入x
    164. FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor
    165. #图片的大小
    166. self.img_dim = img_dim
    167. #获取x的形状
    168. num_samples = x.size(0)
    169. grid_size = x.size(2)
    170. prediction = (
    171. x.view(num_samples, self.num_anchors, self.num_classes + 5, grid_size, grid_size)#(num_samples,3,85,gride_size,grid_size)
    172. .permute(0, 1, 3, 4, 2)#permute是用来做维度换位置的,(num_samples,3,gride_size,grid_size,85)
    173. .contiguous()#调用contiguous()时,会强制拷贝一份tensor,让它的布局和从头创建的一毛一样。而不是与原数据公用一份内存。
    174. )
    175. # 得到outputs
    176. x = torch.sigmoid(prediction[..., 0]) # Center x
    177. y = torch.sigmoid(prediction[..., 1]) # Center y
    178. w = prediction[..., 2] # Width
    179. h = prediction[..., 3] # Height
    180. pred_conf = torch.sigmoid(prediction[..., 4]) # Conf
    181. pred_cls = torch.sigmoid(prediction[..., 5:]) # Cls pred.
    182. # If grid size does not match current we compute new offsets
    183. if grid_size != self.grid_size:
    184. self.compute_grid_offsets(grid_size, cuda=x.is_cuda)
    185. # Add offset and scale with anchors
    186. pred_boxes = FloatTensor(prediction[..., :4].shape)
    187. pred_boxes[..., 0] = x.data + self.grid_x
    188. pred_boxes[..., 1] = y.data + self.grid_y
    189. pred_boxes[..., 2] = torch.exp(w.data) * self.anchor_w
    190. pred_boxes[..., 3] = torch.exp(h.data) * self.anchor_h
    191. output = torch.cat(
    192. (
    193. pred_boxes.view(num_samples, -1, 4) * self.stride,
    194. pred_conf.view(num_samples, -1, 1),
    195. pred_cls.view(num_samples, -1, self.num_classes),
    196. ),
    197. -1,
    198. )
    199. if targets is None:
    200. return output, 0
    201. else:
    202. iou_scores, class_mask, obj_mask, noobj_mask, tx, ty, tw, th, tcls, tconf = build_targets(
    203. pred_boxes=pred_boxes,
    204. pred_cls=pred_cls,
    205. target=targets,
    206. anchors=self.scaled_anchors,
    207. ignore_thres=self.ignore_thres,
    208. )
    209. # Loss : Mask outputs to ignore non-existing objects (except with conf. loss)
    210. loss_x = self.mse_loss(x[obj_mask], tx[obj_mask])
    211. loss_y = self.mse_loss(y[obj_mask], ty[obj_mask])
    212. loss_w = self.mse_loss(w[obj_mask], tw[obj_mask])
    213. loss_h = self.mse_loss(h[obj_mask], th[obj_mask])
    214. loss_conf_obj = self.bce_loss(pred_conf[obj_mask], tconf[obj_mask])
    215. loss_conf_noobj = self.bce_loss(pred_conf[noobj_mask], tconf[noobj_mask])
    216. loss_conf = self.obj_scale * loss_conf_obj + self.noobj_scale * loss_conf_noobj
    217. loss_cls = self.bce_loss(pred_cls[obj_mask], tcls[obj_mask])
    218. total_loss = loss_x + loss_y + loss_w + loss_h + loss_conf + loss_cls
    219. # Metrics
    220. cls_acc = 100 * class_mask[obj_mask].mean()
    221. conf_obj = pred_conf[obj_mask].mean()
    222. conf_noobj = pred_conf[noobj_mask].mean()
    223. conf50 = (pred_conf > 0.5).float()
    224. iou50 = (iou_scores > 0.5).float()
    225. iou75 = (iou_scores > 0.75).float()
    226. detected_mask = conf50 * class_mask * tconf
    227. precision = torch.sum(iou50 * detected_mask) / (conf50.sum() + 1e-16)
    228. recall50 = torch.sum(iou50 * detected_mask) / (obj_mask.sum() + 1e-16)
    229. recall75 = torch.sum(iou75 * detected_mask) / (obj_mask.sum() + 1e-16)
    230. self.metrics = {
    231. "loss": to_cpu(total_loss).item(),
    232. "x": to_cpu(loss_x).item(),
    233. "y": to_cpu(loss_y).item(),
    234. "w": to_cpu(loss_w).item(),
    235. "h": to_cpu(loss_h).item(),
    236. "conf": to_cpu(loss_conf).item(),
    237. "cls": to_cpu(loss_cls).item(),
    238. "cls_acc": to_cpu(cls_acc).item(),
    239. "recall50": to_cpu(recall50).item(),
    240. "recall75": to_cpu(recall75).item(),
    241. "precision": to_cpu(precision).item(),
    242. "conf_obj": to_cpu(conf_obj).item(),
    243. "conf_noobj": to_cpu(conf_noobj).item(),
    244. "grid_size": grid_size,
    245. }
    246. return output, total_loss
    247. """Darknet类:YOLOv3模型"""
    248. class Darknet(nn.Module):
    249. """YOLOv3 object detection model"""
    250. def __init__(self, config_path, img_size=416):
    251. super(Darknet, self).__init__()
    252. # parse_model_config()模型配置的解析器:用来解析yolo-v3层配置文件(yolov3.cfg)并返回模块定义
    253. #(模型定义module_defs是一个列表,每一个元素是一个字典,该字典描绘了网络每一个模块/层的信息)
    254. self.module_defs = parse_model_config(config_path)
    255. #通过获取的模型定义module_defs,来构建YOLOv3模型
    256. self.hyperparams,self.module_list = create_modules(self.module_defs)#模型参数和模型结构
    257. self.yolo_layers = [layer[0] for layer in self.module_list if hasattr(layer[0], "metrics")]
    258. self.img_size = img_size
    259. self.seen = 0
    260. self.header_info = np.array([0, 0, 0, self.seen, 0], dtype=np.int32)
    261. def forward(self, x, targets=None):
    262. img_dim = x.shape[2]
    263. loss = 0
    264. layer_outputs, yolo_outputs = [], []
    265. for i, (module_def, module) in enumerate(zip(self.module_defs, self.module_list)):
    266. if module_def["type"] in ["convolutional", "upsample", "maxpool"]:
    267. x = module(x)
    268. elif module_def["type"] == "route":
    269. x = torch.cat([layer_outputs[int(layer_i)] for layer_i in module_def["layers"].split(",")], 1)
    270. elif module_def["type"] == "shortcut":
    271. layer_i = int(module_def["from"])
    272. x = layer_outputs[-1] + layer_outputs[layer_i]
    273. elif module_def["type"] == "yolo":
    274. x, layer_loss = module[0](x, targets, img_dim)
    275. loss += layer_loss
    276. yolo_outputs.append(x)
    277. layer_outputs.append(x)
    278. yolo_outputs = to_cpu(torch.cat(yolo_outputs, 1))
    279. return yolo_outputs if targets is None else (loss, yolo_outputs)
    280. def load_darknet_weights(self, weights_path):
    281. """Parses and loads the weights stored in 'weights_path'"""
    282. # Open the weights file
    283. with open(weights_path, "rb") as f:
    284. header = np.fromfile(f, dtype=np.int32, count=5) # First five are header values
    285. self.header_info = header # Needed to write header when saving weights
    286. self.seen = header[3] # number of images seen during training
    287. weights = np.fromfile(f, dtype=np.float32) # The rest are weights
    288. # Establish cutoff for loading backbone weights
    289. cutoff = None
    290. if "darknet53.conv.74" in weights_path:
    291. cutoff = 75
    292. ptr = 0
    293. for i, (module_def, module) in enumerate(zip(self.module_defs, self.module_list)):
    294. if i == cutoff:
    295. break
    296. if module_def["type"] == "convolutional":
    297. conv_layer = module[0]
    298. if module_def["batch_normalize"]:
    299. # Load BN bias, weights, running mean and running variance
    300. bn_layer = module[1]
    301. num_b = bn_layer.bias.numel() # Number of biases
    302. # Bias
    303. bn_b = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.bias)
    304. bn_layer.bias.data.copy_(bn_b)
    305. ptr += num_b
    306. # Weight
    307. bn_w = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.weight)
    308. bn_layer.weight.data.copy_(bn_w)
    309. ptr += num_b
    310. # Running Mean
    311. bn_rm = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.running_mean)
    312. bn_layer.running_mean.data.copy_(bn_rm)
    313. ptr += num_b
    314. # Running Var
    315. bn_rv = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(bn_layer.running_var)
    316. bn_layer.running_var.data.copy_(bn_rv)
    317. ptr += num_b
    318. else:
    319. # Load conv. bias
    320. num_b = conv_layer.bias.numel()
    321. conv_b = torch.from_numpy(weights[ptr : ptr + num_b]).view_as(conv_layer.bias)
    322. conv_layer.bias.data.copy_(conv_b)
    323. ptr += num_b
    324. # Load conv. weights
    325. num_w = conv_layer.weight.numel()
    326. conv_w = torch.from_numpy(weights[ptr : ptr + num_w]).view_as(conv_layer.weight)
    327. conv_layer.weight.data.copy_(conv_w)
    328. ptr += num_w
    329. def save_darknet_weights(self, path, cutoff=-1):
    330. """
    331. @:param path - path of the new weights file
    332. @:param cutoff - save layers between 0 and cutoff (cutoff = -1 -> all are saved)
    333. """
    334. fp = open(path, "wb")
    335. self.header_info[3] = self.seen
    336. self.header_info.tofile(fp)
    337. # Iterate through layers
    338. for i, (module_def, module) in enumerate(zip(self.module_defs[:cutoff], self.module_list[:cutoff])):
    339. if module_def["type"] == "convolutional":
    340. conv_layer = module[0]
    341. # If batch norm, load bn first
    342. if module_def["batch_normalize"]:
    343. bn_layer = module[1]
    344. bn_layer.bias.data.cpu().numpy().tofile(fp)
    345. bn_layer.weight.data.cpu().numpy().tofile(fp)
    346. bn_layer.running_mean.data.cpu().numpy().tofile(fp)
    347. bn_layer.running_var.data.cpu().numpy().tofile(fp)
    348. # Load conv bias
    349. else:
    350. conv_layer.bias.data.cpu().numpy().tofile(fp)
    351. # Load conv weights
    352. conv_layer.weight.data.cpu().numpy().tofile(fp)
    353. fp.close()

     4.8 test.py

    1. from __future__ import division
    2. from models import *
    3. from utils.utils import *
    4. from utils.datasets import *
    5. from utils.parse_config import *
    6. import argparse
    7. import tqdm
    8. import torch
    9. from torch.utils.data import DataLoader
    10. from torch.autograd import Variable
    11. """模型评估函数:参数为模型、valid数据集路径、iou阈值。nms阈值、网络输入大小、批量大小"""
    12. def evaluate(model, path, iou_thres, conf_thres, nms_thres, img_size, batch_size):
    13. #加上model.eval(). 否则的话,有输入数据,即使不训练,它也会改变权值
    14. model.eval()
    15. '''(1)获取评估数据集:变为batch组成的数据集'''
    16. # dataset(验证集图片路径集、验证集图片集,验证集标签集)
    17. # dataloader获取批量batch,验证集图片路径batch、验证集图片batch,验证集标签batch)
    18. dataset = ListDataset(path, img_size=img_size, augment=False, multiscale=False)
    19. dataloader = torch.utils.data.DataLoader(dataset,
    20. batch_size=batch_size,
    21. shuffle=False,
    22. num_workers=1,
    23. collate_fn=dataset.collate_fn)#collate_fn参数,实现自定义的batch输出
    24. Tensor = torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor
    25. labels = []
    26. sample_metrics = [] # List of tuples (TP, confs, pred)
    27. for batch_i, (_, imgs, targets) in enumerate(tqdm.tqdm(dataloader, desc="Detecting objects")):#tqdm进度条
    28. '''(2) batch标签处理'''
    29. labels += targets[:, 1].tolist()#将targets的类别信息转变为list存到label列表中
    30. # Rescale target
    31. targets[:, 2:] = xywh2xyxy(targets[:, 2:])#将targets的坐标变为(xyxy)形式,此时的坐标也是归一化的形式
    32. targets[:, 2:] *= img_size#适应于原图的比target形式
    33. '''(3)batch图片预测,并进行NMS处理'''
    34. # 图片输入模型,并对模型输出进行非极大值抑制
    35. imgs = Variable(imgs.type(Tensor), requires_grad=False)
    36. with torch.no_grad():
    37. outputs = model(imgs)
    38. outputs = non_max_suppression(outputs, conf_thres=conf_thres, nms_thres=nms_thres)
    39. '''(4)预测信息统计:得到经过NMS处理后,预测边界框的true_positive(值为或1)、预测置信度,预测类别信息'''
    40. sample_metrics += get_batch_statistics(outputs, targets, iou_threshold=iou_thres)#参数:模型输出,真实标签(适应于原图的x,y,x,y),iou阈值
    41. # 这里需要注意,github上面的代码有错误,需要添加if条件语句,训练才能正常运行
    42. if len(sample_metrics) == 0:
    43. return np.array([]), np.array([]), np.array([]), np.array([]), np.array([])
    44. # sample_metrics信息解析,获取独立的 true_positive(值为或1)、预测置信度,预测类别 信息
    45. true_positives, pred_scores, pred_labels = [np.concatenate(x, 0) for x in list(zip(*sample_metrics))]
    46. #计算 precision, recall, AP, f1, ap_class,这里调用了utils.py中的函数进行计算
    47. precision, recall, AP, f1, ap_class = ap_per_class(true_positives, pred_scores, pred_labels, labels)#pred_labels, labels的长度是不同的
    48. return precision, recall, AP, f1, ap_class
    49. if __name__ == "__main__":
    50. '''(1)参数解析'''
    51. parser = argparse.ArgumentParser()
    52. parser.add_argument("--batch_size", type=int, default=8, help="size of each image batch")
    53. parser.add_argument("--model_def", type=str, default="config/yolov3.cfg", help="path to model definition file")
    54. parser.add_argument("--data_config", type=str, default="config/custom.data", help="path to data config file")
    55. parser.add_argument("--weights_path", type=str, default="checkpoints/yolov3_ckpt_9.pth", help="path to weights file")#"weights/yolov3.weights"
    56. parser.add_argument("--class_path", type=str, default="data/coco.names", help="path to class label file")
    57. parser.add_argument("--iou_thres", type=float, default=0.5, help="iou threshold required to qualify as detected")
    58. parser.add_argument("--conf_thres", type=float, default=0.001, help="object confidence threshold")
    59. parser.add_argument("--nms_thres", type=float, default=0.5, help="iou thresshold for non-maximum suppression")
    60. parser.add_argument("--n_cpu", type=int, default=8, help="number of cpu threads to use during batch generation")
    61. parser.add_argument("--img_size", type=int, default=416, help="size of each image dimension")
    62. opt = parser.parse_args()
    63. #print(opt)
    64. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    65. """(2)数据解析"""
    66. # 调用parse_config。py中的数据解析桉树,返回值 data_config 为字典{class:80,train:路径,valid:路径。。。}
    67. data_config = parse_data_config(opt.data_config)
    68. valid_path = data_config["valid"]#验证集路径valid=data/custom/valid.txt
    69. class_names = load_classes(data_config["names"])#类别路径
    70. """(3)模型构建:构建模型,加载模型参数"""
    71. model = Darknet(opt.model_def).to(device)
    72. if opt.weights_path.endswith(".weights"):
    73. # Load darknet weights
    74. model.load_darknet_weights(opt.weights_path)#
    75. else:
    76. model.load_state_dict(torch.load(opt.weights_path))#自定义的函数
    77. print("Compute mAP...")
    78. """(4)模型评估"""
    79. precision, recall, AP, f1, ap_class = evaluate(
    80. model,#模型
    81. path=valid_path,#验证集路径
    82. iou_thres=opt.iou_thres,
    83. conf_thres=opt.conf_thres,#置信度阈值
    84. nms_thres=opt.nms_thres,#nms阈值
    85. img_size=opt.img_size,#网路输入尺寸
    86. batch_size=8,#批量
    87. )
    88. print(precision, recall, AP, f1, ap_class)
    89. print("Average Precisions:")
    90. for i, c in enumerate(ap_class):
    91. print(f"+ Class '{c}' ({class_names[c]}) - AP: {AP[i]}")
    92. print(f"mAP: {AP.mean()}")

    4.9 train.py

    1. from __future__ import division
    2. from models import *
    3. from utils.logger import *
    4. from utils.utils import *
    5. from utils.datasets import *
    6. from utils.parse_config import *
    7. from terminaltables import AsciiTable
    8. import os
    9. from test import evaluate
    10. import time
    11. import datetime
    12. import argparse
    13. import torch
    14. from torch.utils.data import DataLoader
    15. from torch.autograd import Variable
    16. if __name__ == "__main__":
    17. '''(1)参数解析'''
    18. parser = argparse.ArgumentParser()
    19. parser.add_argument("--epochs", type=int, default=10, help="number of epochs")
    20. parser.add_argument("--batch_size", type=int, default=1, help="size of each image batch")
    21. #梯度累加数
    22. parser.add_argument("--gradient_accumulations", type=int, default=2, help="number of gradient accums before step")
    23. parser.add_argument("--model_def", type=str, default="config/yolov3.cfg", help="path to model definition file")
    24. parser.add_argument("--data_config", type=str, default="config/custom.data", help="path to data config file")
    25. parser.add_argument("--pretrained_weights", type=str, help="if specified starts from checkpoint model")
    26. parser.add_argument("--n_cpu", type=int, default=1, help="number of cpu threads to use during batch generation")
    27. parser.add_argument("--img_size", type=int, default=416, help="size of each image dimension")
    28. parser.add_argument("--checkpoint_interval", type=int, default=1, help="interval between saving model weights")
    29. parser.add_argument("--evaluation_interval", type=int, default=1, help="interval evaluations on validation set")
    30. parser.add_argument("--compute_map", default=False, help="if True computes mAP every tenth batch")
    31. parser.add_argument("--multiscale_training", default=True, help="allow for multi-scale training")
    32. parser.add_argument("--weights_path", type=str, default="checkpoints/yolov3_ckpt_9.pth", help="path to weights file")
    33. opt = parser.parse_args()
    34. print(opt)
    35. '''(2)实例化日志类'''
    36. logger = Logger("logs")
    37. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    38. '''(3)文件夹创建'''
    39. os.makedirs("output", exist_ok=True)
    40. os.makedirs("checkpoints", exist_ok=True)
    41. """(4)初始化模型:模型构建,模型参数装载"""
    42. model = Darknet(opt.model_def).to(device)
    43. model.apply(weights_init_normal)
    44. # If specified we start from checkpoint
    45. if opt.pretrained_weights:
    46. if opt.pretrained_weights.endswith(".pth"):
    47. model.load_state_dict(torch.load(opt.pretrained_weights))
    48. else:
    49. model.load_darknet_weights(opt.pretrained_weights)
    50. """(5)数据集加载"""
    51. data_config = parse_data_config(opt.data_config)#调用parse_config.py文件的数据配置解析函数,获取data_config为一个字典
    52. train_path = data_config["train"]#训练集路径
    53. valid_path = data_config["valid"]#验证集路径
    54. class_names = load_classes(data_config["names"])#调用utils.py内的load_classes函数用于获取数据集包含的类别名称
    55. #dataset是数据集中,图片的路径和、图片、标签(归一化的格式x,y,w,h)的集合
    56. dataset = ListDataset(train_path, augment=True, multiscale=opt.multiscale_training)
    57. #dataloader是dataset装载成批量形式
    58. dataloader = torch.utils.data.DataLoader(
    59. dataset,
    60. batch_size=opt.batch_size,
    61. shuffle=True,
    62. num_workers=opt.n_cpu,
    63. pin_memory=True,
    64. collate_fn=dataset.collate_fn,
    65. )
    66. """(7)优化器"""
    67. optimizer = torch.optim.Adam(model.parameters())
    68. """(8)模型训练"""
    69. metrics = [
    70. "grid_size",
    71. "loss",
    72. "x",
    73. "y",
    74. "w",
    75. "h",
    76. "conf",
    77. "cls",
    78. "cls_acc",
    79. "recall50",
    80. "recall75",
    81. "precision",
    82. "conf_obj",
    83. "conf_noobj",
    84. ]
    85. for epoch in range(opt.epochs):#迭代epoch次训练
    86. model.train()#设置模型为训练模式
    87. start_time = time .time()
    88. print('start_time',start_time)
    89. for batch_i, (_, imgs, targets) in enumerate(dataloader):#每一epoch的批量迭代
    90. #批量的累计迭代数
    91. batches_done = len(dataloader) * epoch + batch_i
    92. #图片、标签的变量化处理
    93. imgs = Variable(imgs.to(device))#把图像变为变量,可以记录梯度
    94. targets = Variable(targets.to(device), requires_grad=False)#把标签变为变量,不记录梯度
    95. # 获取模型的输出与损失,损失反向传播
    96. loss, outputs = model(imgs, targets)#将图片和标签输入模型,获取输出
    97. loss.backward()
    98. #计算梯度
    99. if batches_done % opt.gradient_accumulations:
    100. # 在每一步之前计算梯度Accumulates gradient before each step
    101. optimizer.step()
    102. optimizer.zero_grad()
    103. #训练的epoch及batch信息
    104. log_str = "\n---- [Epoch %d/%d, Batch %d/%d] ----\n" % (epoch+1, opt.epochs, batch_i+1, len(dataloader))
    105. #print('log_str',log_str)#例---- [Epoch 1/10, Batch 1/10] ----
    106. #创建行索引
    107. metric_table = [["Metrics", *[f"YOLO Layer {i}" for i in range(len(model.yolo_layers))]]]#创建训练过程中的表格,行索引
    108. #print(metric_table)# [['Metrics', 'YOLO Layer 0', 'YOLO Layer 1', 'YOLO Layer 2']]
    109. # 在每一个 YOLO layer的各项指标信息
    110. for i, metric in enumerate(metrics):#metrics为各项指标名称组成的列表,上面已经定义
    111. #获取metrics各个项的数值类型
    112. formats = {m: "%.6f" for m in metrics}#将所有的metrics中的输出数值类型定义,这一步把全部的输出类型全部定义保留6位小数
    113. formats["grid_size"] = "%2d"
    114. formats["cls_acc"] = "%.2f%%"
    115. #print(' formats', formats)#{'grid_size': '%2d', 'loss': '%.6f', 'x': '%.6f', 'y': '%.6f', 'w': '%.6f', 'h': '%.6f', 'conf': '%.6f', 'cls': '%.6f', 'cls_acc': '%.2f%%', 'recall50': '%.6f', 'recall75': '%.6f', 'precision': '%.6f', 'conf_obj': '%.6f', 'conf_noobj': '%.6f'}
    116. #表格赋值
    117. row_metrics = [formats[metric] % yolo.metrics.get(metric, 0) for yolo in model.yolo_layers]#?????????????
    118. #print('row_metrics',row_metrics)
    119. metric_table += [[metric, *row_metrics]]
    120. # Tensorboard 日志信息
    121. tensorboard_log = []
    122. for j, yolo in enumerate(model.yolo_layers):
    123. for name, metric in yolo.metrics.items():
    124. if name != "grid_size":
    125. tensorboard_log += [(f"{name}_{j+1}", metric)]#把除grid_size的其余信息,添加到日志中
    126. tensorboard_log += [("loss", loss.item())]#把损失也添加到日志信息中
    127. #把日志信息列表写入创建的日志对象
    128. logger.list_of_scalars_summary(tensorboard_log, batches_done)
    129. #log_str打印各项指标参数:
    130. log_str += AsciiTable(metric_table).table
    131. log_str += f"\nTotal loss {loss.item()}"
    132. # 计算该epoch剩余需要的大概时间
    133. epoch_batches_left = len(dataloader) - (batch_i + 1)
    134. time_left = datetime.timedelta(seconds=epoch_batches_left * (time.time() - start_time) / (batch_i + 1))
    135. log_str += f"\n---- ETA {time_left}"
    136. print(log_str)
    137. model.seen += imgs.size(0)
    138. '''(9)训练时评估'''
    139. if epoch % opt.evaluation_interval == 0:
    140. print("\n---- Evaluating Model ----")
    141. # 在评估数据集上对当前模型进行评估,具体评估细节可以看test.py
    142. precision, recall, AP, f1, ap_class = evaluate(
    143. model,
    144. path=valid_path,
    145. iou_thres=0.5,
    146. conf_thres=0.5,
    147. nms_thres=0.5,
    148. img_size=opt.img_size,
    149. batch_size=8,
    150. )
    151. evaluation_metrics = [
    152. ("val_precision", precision.mean()),
    153. ("val_recall", recall.mean()),
    154. ("val_mAP", AP.mean()),
    155. ("val_f1", f1.mean()),
    156. ]
    157. logger.list_of_scalars_summary(evaluation_metrics, epoch)
    158. # Print class APs and mAP
    159. ap_table = [["Index", "Class name", "AP"]]
    160. for i, c in enumerate(ap_class):
    161. ap_table += [[c, class_names[c], "%.5f" % AP[i]]]
    162. print(AsciiTable(ap_table).table)
    163. print(f"---- mAP {AP.mean()}")
    164. '''(10)模型保存'''
    165. if epoch % opt.checkpoint_interval == 0:
    166. torch.save(model.state_dict(), f"checkpoints/yolov3_ckpt_%d.pth" % epoch)

     权重下载

           预训练权重:https://pjreddie.com/media/files/yolov3.weights

           将下载的权重放入weight文件夹,如下图: 

  • 相关阅读:
    我的创作纪念日 - 2048
    【当LINUX系统出现网络问题时该如何排查】
    xx科技2023前端开发卷B
    电子表电路
    Linux基础01
    Springboot 启动Bean如何被加载
    操作Mysql
    Hadoop HDFS 高阶优化方案
    unity urp 实现遮挡显示角色轮廓
    MindSpore Ascend 内存管理
  • 原文地址:https://blog.csdn.net/qq_41946216/article/details/133063050