前言
前两篇文章合在一起完成的是在一个batch的图片一起训练一次时计算机内部发生的变化。链接如下:从反向传播角度解读YOLOV5源码:如何从改变优化器,损失函数计算方式等角度提升模型的性能?_vindicater的博客-优快云博客
从前向传播角度解读YOLOV5的源码:如何修改网络结构或增添多余的层?_vindicater的博客-优快云博客 但是事实上,我们需要在训练中观察训练的效果以知晓是否需要继续进行训练,训练是否已经达到了峰值。且我们需要总体的loss或者是判断的准确率这样的一个量化的标准以评价训练的效果。这就是本文的目的,对于计算上述这种有助于我们做出判断的指标的代码进行分析。由于这个部分也没有太多作为调试修改的用户能提升的,这里依旧采用整体介绍梳理网络结构和运行过程的方式进行阐释。
模型是如何计算指标值的?(代码详细分析)
直接代码:
if not noval or final_epoch: # Calculate mAP
results, maps, _ = validate.run(data_dict,
batch_size=batch_size // WORLD_SIZE * 2,
imgsz=imgsz,
half=amp,
model=ema.ema,
single_cls=single_cls,
dataloader=val_loader,
save_dir=save_dir,
plots=False,
callbacks=callbacks,
compute_loss=compute_loss)
当不是最后一个epoch的时候,调用validate中的run函数得到mAp值等结果。
于是本节将主要分析validate.run()
Step1:准备工作
由于是从training传入进来了模型model和dataloader,所以准备工作只要加载进来即可
参数内容:
Names:从model中传入的种类的名称,以dict的形式保存
以下评价标准全部定为0
tp, fp, p, r, f1, ap, ap_class
预处理:
(0.1)读入图片:
pbar = tqdm(dataloader, desc=s, bar_format=TQDM_BAR_FORMAT)
for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
类似于之前读入训练集的时候,把val_loader的内容通过tqdm的方式读入pbar之中,然后用enumerate的方式进行一一遍历。
(0.2)对于输入图像进行处理
with dt[0]:
if cuda:
im = im.to(device, non_blocking=True)
targets = targets.to(device)
im = im.half() if half else im.float() # uint8 to fp16/32
im /= 255 # 0 - 255 to 0.0 - 1.0
nb, _, height, width = im.shape
1.把读入的32张图片挂上cuda加速
2.选择是否调整成半精度
3.图片对应的矩阵归一化
4.读取一批图片的数量,图片的长宽
(1)利用model进行训练
pred = model(imgs) # forward
forward部分的代码
preds, train_out = model(im) if compute_loss else (model(im, augment=augment), None)
val.py中的代码
两者为何不同?
差别定位:
区别出现在了yolo.py中的Detect类的forward函数之中,而forward层也是模型运算中的最后一层。
两者相同的内容:
z = [] # inference output
for i in range(self.nl):
x[i] = self.m[i](x[i]) # conv
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous() # 按照上面的说法进行顺序的交换
detect层的具体结构
Detect层的目的是把前面不同的卷积输出结果变成255层,也就是3*(4+1+80),那么在以上的同样的部分之中,完成的是把255层拆成85*3形成三个anchor框和85个概率。
维度变化:
(32,255,64,64)--》(32,3,85,64,64)
在val.py中多余的内容:
if not self.training:
else:
xy, wh, conf = x[i].sigmoid().split((2, 2, self.nc + 1), 4)
xy = (xy * 2 + self.grid[i]) * self.stride[i] # xy
wh = (wh * 2) ** 2 * self.anchor_grid[i] # wh
y = torch.cat((xy, wh, conf), 4)
z.append(y.view(bs, self.na * nx * ny, self.no))
不同点的目的:
Step1:从x[i]中分离出来xy,wh,conf参数,其维度分别是:2,2,self.nc+1
Step2:将xy,wh做出一些调整:
为何要调整?
矩阵中得到的结果是相对值,调整后变成绝对位置:
如何调整?
对于xy来说:将得到的xy的偏差值乘2之后加上左上角的坐标位置得到一个新的xy数组
对于wh来说:把得到的wh*2之后进行平方操作,然后把得到的结果乘上anchor框的size得到具体预测框的size
最后把得到的两个和置信度和种类拼接在一起形成y
Step3:输出调整维度后的y数组:
(32,3,85,64,64)--》(32,3*64*64,85)
最终输出结果:
二维数组:
(torch.cat(z, 1), x)
返回内容:
preds:经过处理后的预测框xyhw置信度和种类的概率
Train_out:原样保存模型处理的结果
(2.1)利用train_out得到loss
with dt[1]:
preds, train_out = model(im) if compute_loss
类似于之前在backward模块中的操作,计算得出损失值
注意到这里的输入内容是train_out,和之前内容中的preds都是没有处理过的内容。所以客观上和之前的所有行为都是一模一样的这里不在赘述
(2.2)对于preds进行NMS非极大值抑制
with dt[2]:
preds = non_max_suppression(preds,
conf_thres,
iou_thres,
labels=lb,
multi_label=True,
agnostic=single_cls,
max_det=max_det)
Step2:根据预处理和预先准备的内容做出判断
(1.1):预测集的准备
for si, pred in enumerate(preds):
labels = targets[targets[:, 0] == si, 1:]
nl, npr = labels.shape[0], pred.shape[0] # number of labels, predictions
path, shape = Path(paths[si]), shapes[si][0]
correct = torch.zeros(npr, niou, dtype=torch.bool, device=device) # init
seen += 1
1.Pred:通过enumerate的方式读取每一张图片的预测框
2.Labels:从targets中获取该图片对应的标注框信息
3.nl,npr:上面两个的计数
4.Correct:初始化的0矩阵:行数是预测框的总数,列数是在每一个IOU加以判断的阈值,内容物是bool值,用来存放在某个IOU下这个预测是否正确的判断结果
5.通过scale_boxes函数得到在原图上的预测框信息predn
(1.2):label集合的准备
tbox = xywh2xyxy(labels[:, 1:5]) # target boxes label中的是中心点
scale_boxes(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space labels
1.使用xywh2xyxy的方式将标注框的信息转化成tbox:现在有的是两个对角的信息
2.利用scale_boxes函数把tbox的信息处理到原图上去
3.把label中的batch中图片的信息和处理后得到的tbox拼接起来产生labelsn,目的是为了和predn形式相同
(2):比较得出correct矩阵且存入stats之中
correct = process_batch(predn, labelsn, iouv)
利用函数:process_batch(),位置:val.py
stats.append((correct, pred[:, 4], pred[:, 5], labels[:, 0]))
分别存入的元素是:
Correct:判断矩阵
Pred[:,4]:置信度
Pred[:,5]:预测出来的class值
Labels[:,0]:标注框对应的class
Step3:保存成果
1.输出标注框图和预测框图
对于前三张图来说,绘制出含有标注框的图和绘制出含有预测框的图
plot_images(im, targets, paths, save_dir / f'val_batch{batch_i}_labels.jpg', names)
plot_images(im, output_to_target(preds), paths, save_dir / f'val_batch{batch_i}_pred.jpg', names) # pred
2.计算出具体的计算结果:
stats = [torch.cat(x, 0).cpu().numpy() for x in zip(*stats)] # to numpy
if len(stats) and stats[0].any():
tp, fp, p, r, f1, ap, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
ap50, ap = ap[:, 0], ap.mean(1) # AP@0.5, AP@0.5:0.95
mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
nt = np.bincount(stats[3].astype(int), minlength=nc) # number of targets per class
1.把stats的所有内容从一共5000个元素的list转换成二维数组:数组的行数是所有预测框的数量,列数是IOU分成的段,如0.5-0.95共10个段
2.通过函数:ap_per_class()来获取所需要输出的各个参数
3.打印输出结果并返回
maps = np.zeros(nc) + map
for i, c in enumerate(ap_class):
maps[c] = ap[i]
return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t
返回了上述需要的参数
至此:validate.run()部分结束,输出内容:评价指标
Step4:收尾,保存信息
Section1:更新最好的mAP
用fitness函数处理过之后和已经存着的best_fitness进行比较,如果更大就加以覆盖。最后使用callback函数将结果写进results.csv文件之中
fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95]
stop = stopper(epoch=epoch, fitness=fi) # early stop check
if fi > best_fitness:
best_fitness = fi
log_vals = list(mloss) + list(results) + lr
Section2:保存模型
1.保存到last.pt之中
2.如果是最好的,保存到best.pt之中
torch.save(ckpt, last)
if best_fitness == fi:
torch.save(ckpt, best)
if opt.save_period > 0 and epoch % opt.save_period == 0:
torch.save(ckpt, w / f'epoch{epoch}.pt')
del ckpt
callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi)
至此,一整个epoch训练完毕
整个train.py究竟得到了一些什么?(训练结束后的总返回值)
results, _, _ = validate.run(
data_dict,
batch_size=batch_size // WORLD_SIZE * 2,
imgsz=imgsz,
model=attempt_load(f, device).half(),
iou_thres=0.65 if is_coco else 0.60, # best pycocotools at iou 0.65
single_cls=single_cls,
dataloader=val_loader,
save_dir=save_dir,
save_json=is_coco,
verbose=True,
plots=plots,
callbacks=callbacks,
compute_loss=compute_loss) # val best model with plots
利用best.pt的模型来重新跑一遍测试集得到最好的结果设为results,将results返回
获得的最主要成果:Last.pt、train.pt
总结:
这一篇文章分析研究的是一个epoch结束之后直到第二个epoch开始之前的过程中进行评价的过程中程序发生的变化。在此过程中我们得到了一些评判标准的数据:比如说mAp的值和总损失值。并且在模型的最后我们保存了训练下来最好的模型对应的参数,给出了最好条件下的损失值精确度之类的信息。至此整个train.py已经被切分成了以上的几篇文章以帮助大家理解这个最庞大也是最核心的源代码文件。如果觉得本系列对于您的学习起到了一些帮助,请点赞关注吧ovo