1. 训练过程
1.1 参考代码
代码如下:
def train_one_epoch(model, optimizer, data_loader, device, epoch, epochs, print_freq, accumulate, img_size, grid_min, grid_max, gs, multi_scale=False, warmup=True): """ Args: data_loader: len = 1430 1430个batch_size=1个epochs分成一块块的batch_size print_freq: 每50个batch在logger中更新 accumulate: 1、多尺度训练时accumulate个batch改变一次图片的大小 2、每训练accumulate*batch_size张图片更新一次权重和学习率 第一个epoch accumulate=1 img_size: 训练图像的大小 grid_min, grid_max: 在给定最大最小输入尺寸范围内随机选取一个size(size为32的整数倍) gs: grid_size warmup: 用在训练第一个epoch时,这个时候的训练学习率要调小点,慢慢训练 Returns: mloss: 每个epch计算的mloss [box_mean_loss, obj_mean_loss, class_mean_loss, total_mean_loss] now_lr: 每个epoch之后的学习率 """ model.train() metric_logger = utils.MetricLogger(delimiter=" ") metric_logger.add_meter('lr', utils.SmoothedValue(window_size=1, fmt='{value:.6f}')) header = 'Epoch: [{}/{}]'.format(epoch, epochs) # 模型训练开始第一轮采用warmup训练 慢慢训练 lr_scheduler = None if epoch == 1 and warmup is True: # 当训练第一轮(epoch=1)时,启用warmup训练方式,可理解为热身训练 warmup_factor = 1.0 / 1000 warmup_iters = min(1000, len(data_loader) - 1) lr_scheduler = utils.warmup_lr_scheduler(optimizer, warmup_iters, warmup_factor) accumulate = 1 # 慢慢训练,每个batch都改变img大小,每个batch都改变权重 # amp.GradScaler: 混合精度训练 # GradScaler: 在反向传播前给 loss 乘一个 scale factor,所以之后反向传播得到的梯度都乘了相同的 scale factor # scaler: GradScaler对象用来自动做梯度缩放 # https://blog.csdn.net/l7H9JA4/article/details/114324414?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522161944073216780357273770%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=161944073216780357273770&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_v2~rank_v29-1-114324414.pc_search_result_cache&utm_term=amp.GradScaler%28enabled%3Denable_amp%29 enable_amp = True if "cuda" in device.type else False scaler = amp.GradScaler(enabled=enable_amp) # mean losses [box_mean_loss, obj_mean_loss, class_mean_loss, total_mean_loss] mloss = torch.zeros(4).to(device) now_lr = 0. # 本batch的lr nb = len(data_loader) # number of batches # imgs: [batch_size, 3, img_size, img_size] # targets: [num_obj, 6] , that number 6 means -> (img_index, obj_index, x, y, w, h) # paths: list of img path # 这里调用一次datasets.__len__; batch_size次datasets.__getitem__; 再执行1次datasets.collate_fn for i, (imgs, targets, paths, _, _) in enumerate(metric_logger.log_every(data_loader, print_freq, header)): # ni 统计从epoch0开始的所有batch数 ni = i + nb * epoch # number integrated batches (since train start) # imgs: [4, 3, 736, 736]一个batch的图片 # targets(真实框): [22, 6] 22: num_object 6: batch中第几张图(0,1,2,3),类别,x,y,w,h imgs = imgs.to(device).float() / 255.0 # 对imgs进行归一化 uint8 to float32, 0 - 255 to 0.0 - 1.0 targets = targets.to(device) # Multi-Scale if multi_scale: # 每训练accumulate个batch(batch_size*accumulate张图片),就随机修改一次输入图片大小 # 由于label已转为相对坐标,故缩放图片不影响label的值 if ni % accumulate == 0: # adjust img_size (67% - 150%) every 1 batch # 在给定最大最小输入尺寸范围内随机选取一个size(size为32的整数倍) img_size = random.randrange(grid_min, grid_max + 1) * gs # img_size = 320~736 sf = img_size / max(imgs.shape[2:]) # scale factor # 如果图片最大边长不等于img_size, 则缩放一个batch图片,并将长和宽调整到32的整数倍 if sf != 1: # gs: (pixels) grid size ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]] # new shape (stretched to 32-multiple) imgs = F.interpolate(imgs, size=ns, mode='bilinear', align_corners=False) # 混合精度训练上下文管理器,如果在CPU环境中不起任何作用 with amp.autocast(enabled=enable_amp): # pred: tensor格式 list列表 存放三个tensor 对应的是三个yolo层的输出 # 例如[batch_size, 3, 23, 23, 25] [batch_size, 3, 46, 46, 25] [batch_size, 3, 96, 96, 25] # [batch_size, anchor_num, grid_h, grid_w, xywh + obj + classes] # 可以看出来这里的预测值是三个yolo层每个grid_cell(每个grid_cell有三个预测值)的预测值,后面肯定要进行正样本筛选 pred = model(imgs) # dict格式 存放三分tensor 每个tensor对应一个loss的键值对 # loss的顺序(键)为: 'box_loss', 'obj_loss', 'class_loss' # targets(数据增强后的真实框): [21, 6] 21: num_object 6: batch中第几张图(0,1,2,3)+类别+x+y+w+h loss_dict = compute_loss(pred, targets, model) losses = sum(loss for loss in loss_dict.values()) # 三个相加 # reduce losses over all GPUs for logging purpose loss_dict_reduced = utils.reduce_dict(loss_dict) losses_reduced = sum(loss for loss in loss_dict_reduced.values()) loss_items = torch.cat((loss_dict_reduced["box_loss"], loss_dict_reduced["obj_loss"], loss_dict_reduced["class_loss"], losses_reduced)).detach() mloss = (mloss * i + loss_items) / (i + 1) # update mean losses # 如果losses_reduced无效,则输出对应图片信息 if not torch.isfinite(losses_reduced): print('WARNING: non-finite loss, ending training ', loss_dict_reduced) print("training image path: {}".format(",".join(paths))) sys.exit(1) losses *= 1. / accumulate # scale loss # 1、backward 反向传播 scale loss 先将梯度放大 防止梯度消失 scaler.scale(losses).backward() # optimize # 每训练accumulate*batch_size张图片更新一次权重 if ni % accumulate == 0: # 2、scaler.step() 首先把梯度的值unscale回来. # 如果梯度的值不是 infs 或者 NaNs, 那么调用optimizer.step()来更新权重, # 否则,忽略step调用,从而保证权重不更新(不被破坏) scaler.step(optimizer) # 3、准备着,看是否要增大scaler 不一定更新 看scaler.step(optimizer)的结果,需要更新就更新 scaler.update() # 正常更新权重 optimizer.zero_grad() metric_logger.update(loss=losses_reduced, **loss_dict_reduced) now_lr = optimizer.param_groups[0]["lr"] metric_logger.update(lr=now_lr) # 每训练accumulate*batch_size张图片更新一次学习率(只在第一个epoch) warmup=True 才会执行 if ni % accumulate == 0 and lr_scheduler is not None: lr_scheduler.step() return mloss, now_lr
1.2 简要分析
在yolov3spp的训练过程中,其重点只是利用网络的预测的三层特征层,来进行一个正负样本的匹配与损失计算,并结算的损失是根据中心点xy预测的偏移量已经以指数的形式来预测匹配到的anchor与targe的偏移量。
其中所使用到的iou计算不一样,对于边界框偏移量来说,正常使用iou/giou/diou/ciou来进行计算,但是对于正负样本匹配部分来说,选择那一个anchor与对应的target来进行匹配,是根据wh_iou,也就是重叠率来简单的进行计算的。也就是说,只需要根据两个预测框与目标框的宽高,而不需要知道其具体的位置信息来进行一个在大小上的匹配。
所以,整个训练过程是根据匹配好的anchor来进行拟合偏移量,这没有设置到非极大值抑制处理,也没有使用到coco的评价指标,只是计算了平均损失与当前的一个学习率。
2. 验证过程
2.1 参考代码
代码如下:
@torch.no_grad() def evaluate(model, data_loader, coco=None, device=None): """ Args: coco: coco api Returns: """ n_threads = torch.get_num_threads() # 8线程 # FIXME remove this and make paste_masks_in_image run on the GPU torch.set_num_threads(1) cpu_device = torch.device("cpu") model.eval() metric_logger = utils.MetricLogger(delimiter=" ") header = "Test: " if coco is None: coco = get_coco_api_from_dataset(data_loader.dataset) iou_types = _get_iou_types(model) # ['bbox'] coco_evaluator = CocoEvaluator(coco, iou_types) # 这里调用一次datasets.__len__; batch_size次datasets.__getitem__; 再执行1次datasets.collate_fn # 调用__len__将dataset分为batch个批次; 再调用__getitem__取出(增强后)当前批次的每一张图片(batch_size张); # 最后调用collate_fn函数将当前整个批次的batch_size张图片(增强过的)打包成一个batch, 方便送入网络进行前向传播 # img_index:对于的是哪一张图像的索引 # paths:图像的路径 # imgs:一个batch的图像本身 # targets:一个列表,存储着每张图像的边界框信息 for imgs, targets, paths, shapes, img_index in metric_logger.log_every(data_loader, 1, header): imgs = imgs.to(device).float() / 255.0 # uint8 to float32, 0 - 255 to 0.0 - 1.0 # targets = targets.to(device) # 当使用CPU时,跳过GPU相关指令 if device != torch.device("cpu"): torch.cuda.synchronize(device) model_time = time.time() pred = model(imgs)[0] # only get inference result [4, 5040, 25] # [4, 5040, 25] => len=4 [57,6], [5,6], [14,6], [1,6] 6: batch中第几张图(0,1,2,3),类别,x,y,w,h pred = non_max_suppression(pred, conf_thres=0.01, nms_thres=0.6, multi_cls=False) outputs = [] for index, p in enumerate(pred): if p is None: p = torch.empty((0, 6), device=cpu_device) boxes = torch.empty((0, 4), device=cpu_device) else: # xmin, ymin, xmax, ymax boxes = p[:, :4] # shapes: (h0, w0), ((h / h0, w / w0), pad) # 将boxes信息还原回原图尺度,这样计算的mAP才是准确的 boxes = scale_coords(imgs[index].shape[1:], boxes, shapes[index][0]).round() # 注意这里传入的boxes格式必须是xmin, ymin, xmax, ymax,且为绝对坐标 info = {"boxes": boxes.to(cpu_device), "labels": p[:, 5].to(device=cpu_device, dtype=torch.int64), "scores": p[:, 4].to(cpu_device)} outputs.append(info) model_time = time.time() - model_time # 对每一张图片的信息进行打包出来,信息包括:边界框坐标、类别。置信度 res = {img_id: output for img_id, output in zip(img_index, outputs)} evaluator_time = time.time() coco_evaluator.update(res) evaluator_time = time.time() - evaluator_time metric_logger.update(model_time=model_time, evaluator_time=evaluator_time) # gather the stats from all processes metric_logger.synchronize_between_processes() print("Averaged stats:", metric_logger) coco_evaluator.synchronize_between_processes() # accumulate predictions from all images coco_evaluator.accumulate() coco_evaluator.summarize() torch.set_num_threads(n_threads) result_info = coco_evaluator.coco_eval[iou_types[0]].stats.tolist() # numpy to list return result_info def _get_iou_types(model): model_without_ddp = model if isinstance(model, torch.nn.parallel.DistributedDataParallel): model_without_ddp = model.module iou_types = ["bbox"] return iou_types
2.2 简要分析
在验证过程中,最主要的不同是需要看看训练完后对当前这个验证集的效果,这个是验证过程的核心环节,可以用来检验训练效果以及挑选最后的一个训练权重。
这一部分其实和测试部分有点相像,对于训练好的模型输出的一个预测结果,需要经过非极大值抑制处理,来进行一步步的筛选出比较符合的预测框。其中的重点就是需要非极大值抑制处理,也就是所谓的nms后处理方法。根据预测结果获取后处理后的boxes、labels、scores(置信度),然后对于每一张图像就可以得到他的预测边界框信息,预测类别信息,预测的置信度分数三个类别,够成一个字典,形式为:{img_id: img_info}。
然后使用CocoEvaluator来对刚刚所构建的图像-预测结果字典来作一个评价:coco_evaluator.update(res),随机可以调用coco_evaluator类中的相关函数获取对于验证集的预测结果。
调用coco_evaluator的简要流程为:
coco = get_coco_api_from_dataset(data_loader.dataset) coco_evaluator = CocoEvaluator(coco, iou_types) ... res = {img_id: output for img_id, output in zip(img_index, outputs)} # 构建图像-预测结果字典 coco_evaluator.update(res) coco_evaluator.synchronize_between_processes() coco_evaluator.accumulate() coco_evaluator.summarize() result_info = coco_evaluator.coco_eval[iou_types[0]].stats.tolist() # 返回最后的评价结果
对于这一部分,之后会开设一个目标检测的技巧(trick)来详细介绍其使用方法。
3. 测试过程
3.1 参考代码
代码如下:
import os import json import time import torch import cv2 import argparse import numpy as np from matplotlib import pyplot as plt from build_utils import datasets from modules.model import DarkNet from train_val_utils.draw_box_utils import draw_box from train_val_utils.other_utils import time_synchronized, check_file from train_val_utils.post_processing_utils import non_max_suppression, scale_coords def main(opt): device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") print("Using {} device training.".format(device.type)) # 1、载入opt参数 cfg = opt.cfg # yolo网络配置文件path weights = opt.weights # 训练权重path json_path = opt.json_path # voc classes json path img_path = opt.img_path # 预测图片地址 img_size = opt.img_size # 预测图像大小(letterbox后) # 2、载入json文件 得到所有class json_file = open(json_path, 'r') class_dict = json.load(json_file) category_index = {v: k for k, v in class_dict.items()} # 3、初始化模型 模型载入权重 model = DarkNet(cfg) model.load_state_dict(torch.load(weights, map_location=device)["model"], strict=False) model.to(device) # eval测试模式 model.eval() with torch.no_grad(): # 载入原图 img_o (375, 500, 3) H W C img_o = cv2.imread(img_path) # BGR numpy格式 assert img_o is not None, "Image Not Found " + img_path # letterbox numpy格式(array) img:(384, 512, 3) H W C # 将原图最长边缩放到指定大小,再将原图较短边按原图比例缩放,最后将较短边两边pad操作缩放到最长边大小(不会失真) img = datasets.letterbox(img_o, new_shape=img_size, auto=True, color=(0, 0, 0))[0] # Convert (384, 512, 3) => (384, 512, 3) => (3, 384, 512) # img[:, :, ::-1] BGR to RGB => transpose(2, 0, 1) HWC(384, 512, 3) to CHW(3, 384, 512) img = img[:, :, ::-1].transpose(2, 0, 1) img = np.ascontiguousarray(img) # 使内存是连续的 # numpy(3, 384, 512) CHW => torch.tensor [3, 384, 512] CHW img = torch.from_numpy(img).to(device).float() img /= 255.0 # 归一化scale (0, 255) to (0, 1) # [3, 384, 512] CHW => [1, 3, 384, 512] BCHW img = img.unsqueeze(0) # add batch dimension # start inference t1 = time_synchronized() # 获取当前时间 其实可以用time.time() # 推理阶段实际上会有两个返回值 x(相对原图的), p # x: predictor数据处理后的输出(数值是相对原图的,这里是img) # [batch_size, anchor_num * grid * grid, xywh + obj + classes] # 这里pred[1,12096,25] (实际上是等于x)表示这张图片总共生成了12096个anchor(一个grid中三个anchor) # p: predictor原始输出即数据是相对feature map的 # [batch_size, anchor_num, grid, grid, xywh + obj + classes] pred = model(img)[0] # only get inference result t2 = time_synchronized() print("model inference time:", t2 - t1) # nms pred=[7,6]=[obj_num, xyxy+score+cls] 这里的xyxy是相对img的 # pred: 按score从大到小排列; output[0]=第一张图片的预测结果 不一定一次只传入一张图片的 pred = non_max_suppression(pred)[0] t3 = time.time() print("nms time:", t3 - t2) if pred is None: print("No target detected.") exit(0) # 将nms后的预测结果pred tensor格式(是相对img上的)img.shape=[B,C,H,W] # 映射到原图img_o上 img_o.shape=[H, W, C] pred=(anchor_nums, xyxy+score+class) pred[:, :4] = scale_coords(img.shape[2:], pred[:, :4], img_o.shape).round() print("pred shape:", pred.shape) # tensor.detach()截断tensor变量反向传播的梯度流,因为是预测所以不需要计算梯度信息 # bboxes、scores、classes: 按score从大到小排列 tensor=>numpy bboxes = pred[:, :4].detach().cpu().numpy() # xyxys scores = pred[:, 4].detach().cpu().numpy() # scores classes = pred[:, 5].detach().cpu().numpy().astype(int) + 1 # classes # 到这一步,我们就得到了最终的相对原图的所有预测信息bboxes(位置信息)(7,4); scores(7); classes(类别)(7) # 画出每个预测结果 img_o = draw_box(img_o[:, :, ::-1], bboxes, classes, scores, category_index) # 显示预测图片 plt.imshow(img_o) plt.show() # 保存预测后的图片 img_o.save("outputs/predict_result.jpg") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('--cfg', type=str, default='cfg/yolov3-spp.cfg', help="cfg/*.cfg path") parser.add_argument('--weights', type=str, default='weights/yolov3spp-voc-512.pt', help='pretrain weights path') parser.add_argument('--json-path', type=str, default='data/pascal_voc_classes.json', help="voc_classes_json_path") parser.add_argument('--img-path', type=str, default='imgs/2008_000011.jpg', help="predict img path") parser.add_argument('--img-size', type=int, default=512, help="predict img path [416, 512, 608] 32的倍数") opt = parser.parse_args() # 检查文件是否存在 opt.cfg = check_file(opt.cfg) opt.data = check_file(opt.weights) opt.hyp = check_file(opt.json_path) opt.hyp = check_file(opt.img_path) print(opt) main(opt)
3.2 简要分析
对于测试来说,最重要的就是根据训练好的权重来对图像进行预测,与验证类似,关键是对预测结果作一个非极大值抑制处理,也就是一个后处理方法,然后甚至不需要对其作一个评价指标,因为没有label。
所以,在测试来说,最后就是根据后处理完的预测结果来直接的进行一个可视化展示,就是对特征层结果来进行一个缩放处理,缩放到原图的原大小,以及对预测的边界框与类别信息在原图上画出来。
总结:
大致看完了整个yolov3spp项目,收获良多,知道了主要的处理细节。对于训练过程,重点就是正负样本的匹配问题了,也就是标签的分配,其是不需要进行一个后处理方法的;而对于验证过程,需要对预测结果进行nms然后使用coco评价指标来进行测试,这是个工具知道如何掉包就好;而对于测试结果,由于测试是没有标签信息的,所以直接将nms后处理后的预测结果直接在原图上进行一个可视化展示即可。
并且,为了加快验证与测试,验证过程与测试过程都会使用Rectangular inference来加快推理。Rectangular inference属于是目标检测中常见的一个数据处理的技巧了,作用就是加快推理速度与验证速度。
除了Rectangular inference之外,yolov3spp中还涉及大量的技巧,这里我并没有对其过多的介绍,只是记录了yolov3spp最本质的算法核心,就是如何进行正负样本匹配以及其损失是如何计算,如何进行训练与验证的。
对于其他设计的训练与测试技巧,之后会放在其他专栏来学习与记录。
参考资料:
https://blog.csdn.net/qq_38253797/article/details/118046587
https://blog.csdn.net/qq_38253797/article/details/117920079