目录

一.数据集打标签labelme

1.安装labelme

2.打标签

二.yolo v3项目实战

1.各文件说明

2.将打标签完成之后的json文件提取

3.统计所有图片路径

4.配置参数

5.创建保存日志对象

6.将数据配置传入的参数转化为字典形式

7.创建网络模型

①加载模型网络配置文件

②根据模型网络配置文件创建出所有网络层

③获取yolo层

④forward前向传播

8.初始化所有网络层

9.加载预训练权重文件

10.创建数据集

11.开始训练


一.数据集打标签labelme

 数据标注的工具包括以下几种:

LabelImg:一款图形图像标注工具,用Python编写,并将Qt用于其图形界面。可以用于进行目标检测项目的标注工作,支持2D矩形框标注。

Labelme:一款开源的图像标注工具,由Visual Geometry Group开发。可以在线和离线使用,支持多边形分割、语义分割、2D框、线标注、点标注。

VIA-VGG Image Annotator (VIA):一款开源的图像标注工具,由Visual Geometry Group开发。可以在线和离线使用,支持多边形分割、语义分割、2D框、线标注、点标注。Version 3增加了对视频和音频的标注以及人脸标注。

RectLabel:一款目标检测标注工具,支持导出YOLO、KITTI、COCO JSON与CSV格式,读写Pascal VOC格式的XML文件。

VOTT:微软发布的一款基于JavaScript开发用于图像目标检测的标注工具,使用React+Redux进行开发,支持Windows和Linux平台运行。软件提供了基于CNTK训练的faster-rcnn模型进行自动标注然后人工矫正的方式,能大幅减轻标注所需的工作量。

OpenCV/CVAT:一款开源的计算机视觉库,提供了多种标注类型,包括多边形分割、语义分割、2D框、线标注、点标注等。同时支持视频标注和多种文件导出格式,如CVAT for video、CVAT for images、PASCAL VOC等。

LableBox:一款WEB模式下的标记工具,提供自定义注释API支持,纯JS + HTML支持。

1.安装labelme

  1. pip install labelme
  2. pip install pyqt5
  3. pip install pillow
  4. cmd中输入labelme启动

之后会自动弹出python窗口

2.打标签

给各个数据打上标签,默认保存为json格式文件

二.yolo v3项目实战

1.各文件说明

2.将打标签完成之后的json文件提取

将json文件提取成如下格式:标签+坐标

代码如下,自己注意一下文件地址别弄错即可,可以分别创建train和val的数据集

import json
import os
name2id = { 'dog':0, 'cat':1}#有多少个类别,在字典中就写多少个对应的就可以

def convert(img_size, box):
    dw = 1./ (img_size[0])
    dh = 1./(img_size[1])
    x = (box[0] + box[2])/2.0 - 1
    y = (box[1] + box[3])/2.0 - 1
    w = box[2]- box[0]
    h = box[3] - box[1]
    x = x*dw
    w = w*dw
    y = y*dh
    h = h*dh
    return (x,y,w,h)

def decode_json(json_floder_path,json_name ) :
    #转换好的标签路径,建议保存在data\custom\labels中
    txt_name = r'data\\custom2\\labels\\val\\' + json_name[0:-5] + '.txt'
    txt_file = open(txt_name,'w')
    json_path = os.path.join(json_floder_path, json_name)
    data = json.load(open(json_path, 'r'))
    img_w = data[ 'imageWidth' ]
    img_h = data[ 'imageHeight' ]
    for i in data[ 'shapes']:
        label_name = i['label']
        if ( i['shape_type' ] == 'rectangle' ):
            x1 = int(i['points'][0][0])
            y1 = int(i['points'][0][1])
            x2 = int(i['points'][1][0])
            y2 = int(i['points'][1][1])
            bb = (x1,y1,x2,y2)
            bbox = convert((img_w,img_h),bb)
            txt_file.write(str(name2id[label_name]) + " " +" ".join([str(a) for a in bbox]) + '\n')
    txt_file.close()
if __name__ == "__main__" :
    json_floder_path = r'打标签/val/label'
    json_names = os.listdir(json_floder_path)
    for json_name in json_names:
        decode_json(json_floder_path,json_name)

3.统计所有图片路径

import os
f=open(r'data/custom2/train.txt','w')
for i in os.listdir(r'打标签/train/image'):
    f.write('打标签/train/image'+i+'\n')
f.close()

import os
f=open(r'data/custom2/val.txt','w')
for i in os.listdir(r'打标签/train/image'):
    f.write('打标签/train/image'+i+'\n')
f.close()

4.配置参数

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--epochs", type=int, default=10, help="number of epochs") #训练次数
    parser.add_argument("--batch_size", type=int, default=1, help="size of each image batch")   #batch的大小
    parser.add_argument("--gradient_accumulations", type=int, default=1, help="number of gradient accums before step")#在每一步(更新模型参数)之前累积梯度的次数”
    parser.add_argument("--model_def", type=str, default="config/yolov3.cfg", help="path to model definition file") #模型的配置文件
    parser.add_argument("--data_config", type=str, default="config/coco.data", help="path to data config file") #数据的配置文件
    parser.add_argument("--pretrained_weights", type=str, help="if specified starts from checkpoint model") #预训练文件
    parser.add_argument("--n_cpu", type=int, default=0, help="number of cpu threads to use during batch generation")#数据加载过程中应使用的CPU线程数。
    parser.add_argument("--img_size", type=int, default=416, help="size of each image dimension")
    parser.add_argument("--checkpoint_interval", type=int, default=1, help="interval between saving model weights")#隔多少个epoch保存一次模型权重
    parser.add_argument("--evaluation_interval", type=int, default=1, help="interval evaluations on validation set")#多少个epoch进行一次验证集的验证
    parser.add_argument("--compute_map", default=False, help="if True computes mAP every tenth batch")
    parser.add_argument("--multiscale_training", default=True, help="allow for multi-scale training")#允许多尺寸特征图融合的训练
    opt = parser.parse_args()
    print(opt)

需要修改传入的参数:

--model_def 模型的配置文件
--data_config 数据的配置文件
--pretrained_weights  预训练文件

修改后的项目传参

5.创建保存日志对象

    logger = Logger("logs")#日志文件

logger.py文件如下

import tensorflow as tf
import os


class Logger(object):
    def __init__(self, log_dir):
        """创建日志目录并验证路径有效性"""
        # 打印传入的原始路径
        print(f"尝试初始化日志目录: {log_dir}")
        print(f"路径绝对路径: {os.path.abspath(log_dir)}")

        # 检查路径是否已存在
        if os.path.exists(log_dir):
            if os.path.isfile(log_dir):
                # 路径已存在但是文件,直接报错(必须是目录)
                raise IsADirectoryError(f"错误:{log_dir} 是一个文件,必须传入目录路径!")
            else:
                print(f"目录已存在: {log_dir}")
        else:
            # 创建目录(包括父目录)
            try:
                os.makedirs(log_dir, exist_ok=True)
                print(f"已成功创建目录: {log_dir}")
            except Exception as e:
                raise OSError(f"创建目录失败: {log_dir},错误原因: {str(e)}")

        # 再次验证是否为目录
        if not os.path.isdir(log_dir):
            raise NotADirectoryError(f"致命错误:{log_dir} 不是一个目录,请检查路径是否正确!")

        # 初始化写入器
        self.writer = tf.summary.create_file_writer(log_dir)

    def scalar_summary(self, tag, value, step):
        with self.writer.as_default():
            tf.summary.scalar(tag, value, step=step)
            self.writer.flush()

    def list_of_scalars_summary(self, tag_value_pairs, step):
        with self.writer.as_default():
            for tag, value in tag_value_pairs:
                tf.summary.scalar(tag, value, step=step)
            self.writer.flush()

获取设备并创建output和checkpoints目录

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    os.makedirs("output", exist_ok=True)
    os.makedirs("checkpoints", exist_ok=True)

6.将数据配置传入的参数转化为字典形式

    data_config = parse_data_config(opt.data_config)
    train_path = data_config["train"]
    valid_path = data_config["valid"]
    class_names = load_classes(data_config["names"])

def parse_data_config(path):
    """Parses the data configuration file"""
    options = dict()
    options['gpus'] = '0,1,2,3'
    options['num_workers'] = '10'
    with open(path, 'r') as fp:
        lines = fp.readlines()
    for line in lines:
        line = line.strip()
        if line == '' or line.startswith('#'):
            continue
        key, value = line.split('=')
        options[key.strip()] = value.strip()
    return options

7.创建网络模型

    model = Darknet(opt.model_def).to(device)

模型代码如下

①加载模型网络配置文件

class Darknet(nn.Module):
    """YOLOv3 object detection model"""

    def __init__(self, config_path, img_size=416):
        super(Darknet, self).__init__()
        self.module_defs = parse_model_config(config_path)

parse_model_config()函数如下,加载模型所有网络的配置文件

def parse_model_config(path):
    """Parses the yolo-v3 layer configuration file and returns module definitions"""
    file = open(path, 'r')
    lines = file.read().split('\n')
    lines = [x for x in lines if x and not x.startswith('#')]   #x.startswith('#')用于检查字符串变量x是否以#前缀开始。如果x以该前缀开头,该方法将返回一个布尔值,通常是True,否则返回False。
    lines = [x.rstrip().lstrip() for x in lines] # get rid of fringe whitespaces
    module_defs = []
    for line in lines:
        if line.startswith('['): # This marks the start of a new block
            module_defs.append({})
            module_defs[-1]['type'] = line[1:-1].rstrip()
            if module_defs[-1]['type'] == 'convolutional':
                module_defs[-1]['batch_normalize'] = 0
        else:
            key, value = line.split("=")
            value = value.strip()
            module_defs[-1][key.rstrip()] = value.strip()

    return module_defs

②根据模型网络配置文件创建出所有网络层

            self.hyperparams, self.module_list = create_modules(self.module_defs)   #创建出网络中需要的所有网络层,每一层需要哪些参数

hyperparams超参数保存网络基本配置

将创建后的各个网络保存到module_list列表中,共107大层

create_moudles()方法如下,通过if_elif条件筛选出对应的网络后分别创建

def create_modules(module_defs):
    """
    Constructs module list of layer blocks from module configuration in module_defs
    """
    hyperparams = module_defs.pop(0)
    output_filters = [int(hyperparams["channels"])]     #输出特征图的个数,也是卷积核的个数
    module_list = nn.ModuleList()
            #nn.ModuleList()是PyTorch中的一个类,用于将一个或多个nn.Module的实例包装到一个可迭代对象中。这个类在定义神经网络模型时非常有用,因为它允许你将多个网络层(每个都是nn.Module的实例)组合在一起,并以列表的形式管理和使用。
            #具体来说,nn.ModuleList()的工作方式类似于Python的列表。你可以像向普通列表添加元素一样向ModuleList添加层,然后可以通过索引访问它们,就像访问普通的列表一样。然而,与普通列表不同的是,ModuleList中的元素是nn.Module的实例,即神经网络层。
    for module_i, module_def in enumerate(module_defs):
        modules = nn.Sequential()#nn.Sequential()主要用于实现线性堆叠,内部实现了前向传播函数;而nn.ModuleList()则用于存储多个网络层,以便在需要时进行调用,并不实现任何前向传播功能。

        if module_def["type"] == "convolutional":
            bn = int(module_def["batch_normalize"])
            filters = int(module_def["filters"])    #卷积核的个数
            kernel_size = int(module_def["size"])
            pad = (kernel_size - 1) // 2
            modules.add_module(
                f"conv_{module_i}",
                nn.Conv2d(
                    in_channels=output_filters[-1],#输入特征图的数量,即输入的维度。
                    out_channels=filters,           #输出特征图的数量,即输出的维度。
                    kernel_size=kernel_size,        #卷积核的大小
                    stride=int(module_def["stride"]),   #卷积核滑动的步长大小
                    padding=pad,        #在输入数据边界上填充0的层数
                    bias=not bn,        #是否在卷积后添加偏置项。默认情况下,bias=True,即使用偏置项。
                ),
            )
            if bn:
                # nn.BatchNorm2d的输入参数包括:
                # num_features:输入图像的通道数量。
                # eps:稳定系数,防止分母出现0。默认值为1e - 05。
                # momentum:一个用于运行过程中均值和方差的一个估计参数。默认值为0.1。可用于控制批量归一化中移动平均和指数加权的因子。这意味着,随着时间的推移,momentum将推动均值和方差向新的数据进行调整。较小的momentum会导致较慢的调整速度,而较大的momentum则会加快调整速度。
                            # 这个参数的设置是有实际意义的。因为在深度神经网络训练过程中,数据分布会随时间变化。通过设置momentum参数,我们可以使批量归一化层的均值和方差能够跟随数据分布的变化而动态调整,从而更好地适应新的数据分布。
                            # 例如,如果我们有一个网络,其中第一个层使用了批量归一化,而第二个层没有使用批量归一化。在这种情况下,如果我们提高了第一个层的momentum值,那么随着时间的推移,第一个层的均值和方差将逐渐调整以适应新的数据分布。这将使得第二个层能够更好地处理经过归一化的输入数据,从而提高整个网络的性能。
                            # 总之,momentum参数在nn.BatchNorm2d中起到了推动批量归一化层的均值和方差向新数据进行调整的作用,有助于提高网络对数据分布变化的适应能力。
                modules.add_module(f"batch_norm_{module_i}", nn.BatchNorm2d(filters, momentum=0.9))
            if module_def["activation"] == "leaky":
                # nn.LeakyReLU是PyTorch中的一个非线性激活函数,它是一种带有泄漏的线性整流激活函数(LeakyReLU)。与ReLU不同,Leaky
                # ReLU在输入为负数时不是完全的截断,而是有一个小的负斜率。这意味着,对于负输入,LeakyReLU将不会输出0,而是逐渐增加的输出值。
                # 这种设计有助于解决ReLU在负数区域的问题,特别是在深度神经网络中,ReLU的截断可能导致一些神经元无法激活,而LeakyReLU可以缓解这个问题。
                modules.add_module(f"leaky_{module_i}", nn.LeakyReLU(0.1))

        elif module_def["type"] == "maxpool":       #darknet网络中没有池化层,因此本段代码不会执行
            kernel_size = int(module_def["size"])
            stride = int(module_def["stride"])
            if kernel_size == 2 and stride == 1:
                modules.add_module(f"_debug_padding_{module_i}", nn.ZeroPad2d((0, 1, 0, 1)))
            maxpool = nn.MaxPool2d(kernel_size=kernel_size, stride=stride, padding=int((kernel_size - 1) // 2))
            modules.add_module(f"maxpool_{module_i}", maxpool)

        elif module_def["type"] == "upsample": #上采样
            upsample = Upsample(scale_factor=int(module_def["stride"]), mode="nearest")#yolov3.cfg文件中保存的参数为stride=2
            modules.add_module(f"upsample_{module_i}", upsample)

        elif module_def["type"] == "route": #为concat的操作,是数据的拼接。
            layers = [int(x) for x in module_def["layers"].split(",")]
            filters = sum([output_filters[1:][i] for i in layers])
            modules.add_module(f"route_{module_i}", EmptyLayer())#初始化的空网络

        elif module_def["type"] == "shortcut":      # #残差的链接,也就是进行数据的加法+运算
            filters = output_filters[1:][int(module_def["from"])]
            modules.add_module(f"shortcut_{module_i}", EmptyLayer())

        elif module_def["type"] == "yolo":
            anchor_idxs = [int(x) for x in module_def["mask"].split(",")]#预选框的id
            # Extract anchors
            anchors = [int(x) for x in module_def["anchors"].split(",")]
            anchors = [(anchors[i], anchors[i + 1]) for i in range(0, len(anchors), 2)]
            anchors = [anchors[i] for i in anchor_idxs]     #获取预选框的大小
            num_classes = int(module_def["classes"])    #输出的类别个数
            img_size = int(hyperparams["height"])
            # Define detection layer
            yolo_layer = YOLOLayer(anchors, num_classes, img_size)
            modules.add_module(f"yolo_{module_i}", yolo_layer)
        # Register module list and number of output filters
        module_list.append(modules)
        output_filters.append(filters)

    return hyperparams, module_list

对应的网络类如下

class Upsample(nn.Module):
    """ nn.Upsample is deprecated """

    def __init__(self, scale_factor, mode="nearest"):
        super(Upsample, self).__init__()
        self.scale_factor = scale_factor
        self.mode = mode

    def forward(self, x):
        x = F.interpolate(x, scale_factor=self.scale_factor, mode=self.mode)
        return x


class EmptyLayer(nn.Module):
    """Placeholder for 'route' and 'shortcut' layers"""

    def __init__(self):
        super(EmptyLayer, self).__init__()


class YOLOLayer(nn.Module):

    def __init__(self, anchors, num_classes, img_dim=416):
        super(YOLOLayer, self).__init__()
        self.anchors = anchors
        self.num_anchors = len(anchors)
        self.num_classes = num_classes
        self.ignore_thres = 0.5     #阈值
        self.mse_loss = nn.MSELoss()    #nn.MSELoss()是用于计算均方误差(Mean Squared Error)的损失函数。均方误差常用于衡量预测值与真实值之间的差距,适用于回归问题。
        self.bce_loss = nn.BCELoss()    #nn.BCELoss()是用于二元分类问题的损失函数。全称是Binary Cross Entropy Loss,也就是二元交叉熵损失。它通常用于处理二分类问题。
        self.obj_scale = 1
        self.noobj_scale = 100
        self.metrics = {}
        self.img_dim = img_dim
        self.grid_size = 0  # grid size

    def compute_grid_offsets(self, grid_size, cuda=True):
        self.grid_size = grid_size
        g = self.grid_size
        FloatTensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor
        self.stride = self.img_dim / self.grid_size #每一个网格占用的像素大小
        # Calculate offsets for each grid
        self.grid_x = torch.arange(g).repeat(g, 1).view([1, 1, g, g]).type(FloatTensor)
        self.grid_y = torch.arange(g).repeat(g, 1).t().view([1, 1, g, g]).type(FloatTensor)
        self.scaled_anchors = FloatTensor([(a_w / self.stride, a_h / self.stride) for a_w, a_h in self.anchors])
        self.anchor_w = self.scaled_anchors[:, 0:1].view((1, self.num_anchors, 1, 1))
        self.anchor_h = self.scaled_anchors[:, 1:2].view((1, self.num_anchors, 1, 1))

    def forward(self, x, targets=None, img_dim=None):
        # Tensors for cuda support
        print (x.shape)
        FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor#根据是否支持GPU来选择不同的tensor处理方法
        LongTensor = torch.cuda.LongTensor if x.is_cuda else torch.LongTensor
        ByteTensor = torch.cuda.ByteTensor if x.is_cuda else torch.ByteTensor

        self.img_dim = img_dim
        num_samples = x.size(0)
        grid_size = x.size(2)

        prediction = (
            x.view(num_samples, self.num_anchors, self.num_classes + 5, grid_size, grid_size)#改变一个张量(Tensor)的形状或者维度。这个方法并不会改变张量内部元素的排列顺序或者数据类型,只是改变了我们看待这个张量的方式。
            .permute(0, 1, 3, 4, 2)#将张量的维度重新排列的函数。原始张量的维度顺序是(0, 1, 2, 3, 4),而重排后的维度顺序是(0, 1, 3, 4, 2)。这意味着原始张量的第一个维度(索引为0)保持不变,第二个维度(索引为1)保持不变,第三个维度(索引为3)移动到第四个位置,第四个维度(索引为4)移动到第五个位置,而原始张量的第五个维度(索引为2)移动到第三个位置。
            .contiguous()       #用于将张量在内存中重新排序,以确保在执行操作时没有发生内存错误。
        )   #排序后的结果:【1个batch数据的数量,每个框中候选框的个数,网格尺寸、网格尺寸、输出结果:分类的个数+(x,y,w,h,confidence)】
        print (prediction.shape)
        # Get outputs
        x = torch.sigmoid(prediction[..., 0])  # Center x,...通常表示"展开"操作,特别是在多维数组或张量中。当你在一个数组或张量后面使用...,它表示将数组或张量的所有维度都展开。
        y = torch.sigmoid(prediction[..., 1])  # Center y,是相对于每一个小的方格 的位置。
        w = prediction[..., 2]  # Width
        h = prediction[..., 3]  # Height
        pred_conf = torch.sigmoid(prediction[..., 4])  # Conf
        pred_cls = torch.sigmoid(prediction[..., 5:])  # Cls pred.

        # If grid size does not match current we compute new offsets
        if grid_size != self.grid_size:
            self.compute_grid_offsets(grid_size, cuda=x.is_cuda) #相对位置得到对应的绝对位置比如之前的位置是0.5,0.5变为 11.5,11.5这样的

        # Add offset and scale with anchors #特征图中的实际位置
        pred_boxes = FloatTensor(prediction[..., :4].shape)
        pred_boxes[..., 0] = x.data + self.grid_x
        pred_boxes[..., 1] = y.data + self.grid_y
        pred_boxes[..., 2] = torch.exp(w.data) * self.anchor_w  #exp的原因:看ppt中bw和bh的计算方法
        pred_boxes[..., 3] = torch.exp(h.data) * self.anchor_h

        output = torch.cat(         #用于将多个张量沿着指定维度进行拼接。
            (
                pred_boxes.view(num_samples, -1, 4) * self.stride, #还原到原始图中,
                pred_conf.view(num_samples, -1, 1),
                pred_cls.view(num_samples, -1, self.num_classes),
            ),
            -1,
        )   #包含预测框,置信度、类别概率

        if targets is None:
            return output, 0
        else:
            iou_scores, class_mask, obj_mask, noobj_mask, tx, ty, tw, th, tcls, tconf = build_targets(
                pred_boxes=pred_boxes,
                pred_cls=pred_cls,
                target=targets,
                anchors=self.scaled_anchors,
                ignore_thres=self.ignore_thres,
            )
            # iou_scores:真实值与最匹配的anchor的IOU得分值 class_mask:分类正确的索引  obj_mask:目标框所在位置的最好anchor置为1 noobj_mask obj_mask那里置0,还有计算的iou大于阈值的也置0,其他都为1 tx, ty, tw, th, 对应的对于该大小的特征图的xywh目标值也就是我们需要拟合的值 tconf 目标置信度
            # Loss : Mask outputs to ignore non-existing objects (except with conf. loss)
            loss_x = self.mse_loss(x[obj_mask], tx[obj_mask]) # 只计算有目标的
            loss_y = self.mse_loss(y[obj_mask], ty[obj_mask])
            loss_w = self.mse_loss(w[obj_mask], tw[obj_mask])
            loss_h = self.mse_loss(h[obj_mask], th[obj_mask])
            loss_conf_obj = self.bce_loss(pred_conf[obj_mask], tconf[obj_mask]) 
            loss_conf_noobj = self.bce_loss(pred_conf[noobj_mask], tconf[noobj_mask])
            loss_conf = self.obj_scale * loss_conf_obj + self.noobj_scale * loss_conf_noobj #有物体越接近1越好 没物体的越接近0越好
            loss_cls = self.bce_loss(pred_cls[obj_mask], tcls[obj_mask]) #分类损失
            total_loss = loss_x + loss_y + loss_w + loss_h + loss_conf + loss_cls #总损失

            # Metrics
            cls_acc = 100 * class_mask[obj_mask].mean()
            conf_obj = pred_conf[obj_mask].mean()
            conf_noobj = pred_conf[noobj_mask].mean()
            conf50 = (pred_conf > 0.5).float()
            iou50 = (iou_scores > 0.5).float()
            iou75 = (iou_scores > 0.75).float()
            detected_mask = conf50 * class_mask * tconf
            precision = torch.sum(iou50 * detected_mask) / (conf50.sum() + 1e-16)
            recall50 = torch.sum(iou50 * detected_mask) / (obj_mask.sum() + 1e-16)
            recall75 = torch.sum(iou75 * detected_mask) / (obj_mask.sum() + 1e-16)

            self.metrics = {
                "loss": to_cpu(total_loss).item(),
                "x": to_cpu(loss_x).item(),
                "y": to_cpu(loss_y).item(),
                "w": to_cpu(loss_w).item(),
                "h": to_cpu(loss_h).item(),
                "conf": to_cpu(loss_conf).item(),
                "cls": to_cpu(loss_cls).item(),
                "cls_acc": to_cpu(cls_acc).item(),
                "recall50": to_cpu(recall50).item(),
                "recall75": to_cpu(recall75).item(),
                "precision": to_cpu(precision).item(),
                "conf_obj": to_cpu(conf_obj).item(),
                "conf_noobj": to_cpu(conf_noobj).item(),
                "grid_size": grid_size,
            }

            return output, total_loss

③获取yolo层

hasattr()是Python的一个内置函数,它用于检查对象是否具有给定(metrics)的属性。

         self.yolo_layers = [layer[0] for layer in self.module_list if hasattr(layer[0], "metrics")]

其他资源获取

        self.img_size = img_size
        self.seen = 0
        self.header_info = np.array([0, 0, 0, self.seen, 0], dtype=np.int32)

④forward前向传播

后面调用方法时才会使用

    def forward(self, x, targets=None):
        img_dim = x.shape[2]
        loss = 0
        layer_outputs, yolo_outputs = [], []
        for i, (module_def, module) in enumerate(zip(self.module_defs, self.module_list)):
            if module_def["type"] in ["convolutional", "upsample", "maxpool"]:
                x = module(x)
            elif module_def["type"] == "route":     #为concat的操作,是数据的拼接
                x = torch.cat([layer_outputs[int(layer_i)] for layer_i in module_def["layers"].split(",")], 1)
            elif module_def["type"] == "shortcut":  #残差的链接,也就是进行数据的加法+运算,
                layer_i = int(module_def["from"])
                x = layer_outputs[-1] + layer_outputs[layer_i]
            elif module_def["type"] == "yolo":      #包含3个yolo层,分别对应52*52,26*26,13*13层
                x, layer_loss = module[0](x, targets, img_dim)
                loss += layer_loss
                yolo_outputs.append(x)
            layer_outputs.append(x)     #包含了按顺序执行后的每一层结果。后期route和shortcut需要使用
        yolo_outputs = to_cpu(torch.cat(yolo_outputs, 1))
        return yolo_outputs if targets is None else (loss, yolo_outputs)

8.初始化所有网络层

    model.apply(weights_init_normal)
def weights_init_normal(m): #对所有层的网络做初始化。循环将每一个网络层进行处理,m即为每一层网络
    classname = m.__class__.__name__
    if classname.find("Conv") != -1:
        torch.nn.init.normal_(m.weight.data, 0.0, 0.02)
        # torch.nn.init.normal_神经网络的权重进行初始化。该函数使用正态分布来生成初始权重。参数包括:
        # tensor:需要初始化的张量(tensor)。
        # mean = 0.0, std = 1.0:正态分布的均值和标准差。默认情况下,均值是0,标准差是1。
    elif classname.find("BatchNorm2d") != -1:
        torch.nn.init.normal_(m.weight.data, 1.0, 0.02)
        torch.nn.init.constant_(m.bias.data, 0.0)   #用于将张量的所有元素初始化为一个常数。

9.加载预训练权重文件

    if opt.pretrained_weights:
        if opt.pretrained_weights.endswith(".pth"): #用于检查字符串是否以指定的后缀结束。如果字符串以指定的后缀结束,则返回True,否则返回False。
            model.load_state_dict(torch.load(opt.pretrained_weights))
        else:
            model.load_darknet_weights(opt.pretrained_weights)

10.创建数据集

将训练集路径传入。得到LIstDataset类型的数据

    dataset = ListDataset(train_path, augment=True, multiscale=opt.multiscale_training)

将数据集打包成DataLoader数据

dataloader = torch.utils.data.DataLoader(
        dataset,
        batch_size=opt.batch_size,  #1个样本打包成一个batch进行加载
        shuffle=True,               #对数据进行随机打乱,
        num_workers=opt.n_cpu,      #用于指定子进程的数量,用于并行地加载数据。默认情况下,num_workers的值为0,表示没有使用子进程,所有数据都会在主进程中加载。当设置num_workers大于0时,DataLoader会创建指定数量的子进程,每个子进程都会负责加载一部分数据,然后主进程负责从这些子进程中获取数据。
                                    # 使用子进程可以加快数据的加载速度,因为每个子进程可以并行地加载一部分数据,从而充分利用多核CPU的计算能力。但是需要注意的是,使用子进程可能会导致数据的顺序被打乱,因此如果需要保持数据的原始顺序,应该将shuffle参数设置为False。
                                    # num_workers的值应该根据具体情况进行调整。如果数据集较大,可以考虑增加num_workers的值以充分利用计算机的资源。但是需要注意的是,如果num_workers的值过大,可能会导致内存消耗过大或者CPU负载过重,从而影响程序的性能。因此,需要根据实际情况进行调整。
        pin_memory=True,            #指定是否将加载进内存的数据的指针固定(pin),这个参数在某些情况下可以提高数据加载的速度。
                                    # 当设置pin_memory=True时,DataLoader会将加载进内存的数据的指针固定,即不进行移动操作。这样做的目的是为了提高数据传输的效率。因为当数据从磁盘或者网络等地方传输到内存中时,如果指针不固定,可能会导致数据在传输过程中被移动,从而需要重新读取,浪费了时间。而固定指针可以避免这种情况的发生,从而提高了数据传输的效率。
                                    # 需要注意的是,pin_memory参数的效果与操作系统和硬件的性能有关。在一些高性能的计算机上,固定指针可能并不会带来太大的性能提升。但是在一些内存带宽较小的计算机上,固定指针可能会显著提高数据加载的效率。因此,需要根据实际情况进行调整。
        collate_fn=dataset.collate_fn,
                                    # collate_fn是一个函数,用于对每个batch的数据进行合并。这个函数的输入是一个batch的数据,输出是一个合并后的数据。
                                    # collate_fn函数的主要作用是对每个batch的数据进行预处理,例如将不同数据类型的张量合并成一个张量,或者对序列数据进行padding操作等。这样可以使得每个batch的数据格式一致,便于模型进行训练。
                                    # 在默认情况下,collate_fn函数会将每个batch的数据按照第一个元素的张量形状进行合并。例如,如果一个batch的数据中第一个元素的张量形状是[
                                    # 3, 224, 224],那么collate_fn函数会将该batch的所有数据都调整为这个形状。
    )

优化器和metrics

optimizer = torch.optim.Adam(model.parameters())

    metrics = [
        "grid_size",
        "loss",
        "x",
        "y",
        "w",
        "h",
        "conf",
        "cls",
        "cls_acc",
        "recall50",
        "recall75",
        "precision",
        "conf_obj",
        "conf_noobj",
    ]

11.开始训练

更多推荐