声明:
1.私人数据集,有3种类别 0/1/2
2.训练 batchsize=16
思路
build_targets:找到与target匹配的Anchors,并记录下这些Anchors应该的target类别,target转到feature map的位置尺寸信息,这些Anchors的索引,这些Anchors的尺寸。
-
tcls:与target匹配的Anchors应该的targets类别 0/1/2
-
tbox:target从原图转到特征图的归一化坐标和尺寸[x_center,y_center,w,h]
-
indices:[target所属image index,target匹配到的Anchor index,target所在grid 左上角y,target所在grid 左上角x]
indices记录了与target相匹配的Anchors的索引,后面计算位置回归损失和类别损失则通过indices找到的Anchor与target相应的信息计算损失值。
- anch:target匹配到的Anchors的尺寸(从原图转到特征图的Anchor归一化尺寸[w,h])
yolov5.yaml中的Anchor预设尺寸是从原图转到特征图的Anchor尺寸[w,h],不过没有进行过归一化,那些Anchor的尺寸是输入图像640×640分辨率预设的。
ComputeLoss
位置回归损失(IOU):tbox与匹配Anchor归一化尺寸[x_center,y_center,w,h]计算iou。
类别损失:tcls与匹配Anchor的类别信息计算损失。
置信度损失:位置回归损失(IOU)与所有Anchor的置信度计算损失。
(train.py) 初始化损失对象:
#初始化损失计算对象
compute_loss = ComputeLoss(model) # init loss class
(loss.py) 进入ComputeLoss的__init__函数初始化计算损失的变量:
class ComputeLoss:
sort_obj_iou = False
# Compute losses
def __init__(self, model, autobalance=False):
# 设备号
device = next(model.parameters()).device # get model device
# 模型参数
h = model.hyp # hyperparameters
# BCE loss:
# 分类损失对象和置信度损失对象
BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device))
BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device))
# smooth_BCE 标签平滑策略:
# self.cp是1.0 cp代表正样本标签值
# self.cn是0.0 cn代表负样本标签值
self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0)) # positive, negative BCE targets
# Focal loss:
# g作为开关:当g大于0时,将分类和置信度的BCE损失封装成FocalLoss损失,g作为gamma系数控制难易样本的训练。
g = h['fl_gamma'] # focal loss gamma
if g > 0:
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
# m是Detect类型,即模型的3个检测头
m = de_parallel(model).model[-1] # Detect() module
# self.balance用来设置三个feature map对应输出的置信度损失系数(分别平衡三个feature map的置信度损失)
# 这句话是指:如果key=m.nl存在,则获取value=[4.0, 1.0, 0.4]
# 如果如果key=m.nl不存在,则返回[4.0, 1.0, 0.25, 0.06, 0.02]
self.balance = {3: [4.0, 1.0, 0.4]}.get(m.nl, [4.0, 1.0, 0.25, 0.06, 0.02])
# 往下找找:self.balance = [x / self.balance[self.ssi] for x in self.balance]
# 所以说self.ssi是autobalance为True时,用来更新self.balance用的
self.ssi = list(m.stride).index(16) if autobalance else 0 # stride 16 index
self.BCEcls, self.BCEobj, self.gr, self.hyp, self.autobalance = BCEcls, BCEobj, 1.0, h, autobalance
# 模型中得到:每个检测头专有的Anchor的种类 3
self.na = m.na # number of anchors
# 模型中得到:类别数量 3
self.nc = m.nc # number of classes
# 模型中得到:检测头的个数 3
self.nl = m.nl # number of layers
# 模型中得到:self.anchors.shape=(3,3,2) 3个检测头,每个检测头有3种尺寸的Anchor,从原图转到特征图的Anchor归一化尺寸[长,宽]
self.anchors = m.anchors
self.device = device
(train.py) 每个epoch中,开始迭代每个batch的数据并获取一个batch的损失:
for epoch in range(start_epoch, epochs):
...
for i, (imgs, targets, paths, _) in pbar:
...
pred = model(imgs) # forward
# compute_loss-->return (lbox + lobj + lcls) * bs, torch.cat((lbox, lobj, lcls)).detach()
# compute_loss中最后计算的 lbox、lobj、lcls是一个平均batch的各项loss(具体而言就是用BCE去计算的),因此(lbox + lobj + lcls) * bs看做是一个batch的数据的总loss。
# loss是一个batch的数据的总loss(loss=[(lbox + lobj + lcls) * bs]),loss_items是一个平均batch的各项loss(loss=[位置损失,置信度损失,类别损失])。
loss, loss_items = compute_loss(pred, targets.to(device))
...
#mloss=(上个batch的各项loss+当前batch的各项loss)/(batch索引+1)
mloss = (mloss * i + loss_items) / (i + 1)
...
(loss.py)进入ComputeLoss的__call__函数计算损失:
build_targets:将targets匹配到对应的Anchors
# build_targets作用:将targets匹配到对应的Anchors。
# tcls为 与targets匹配的Anchors应该的targets类别 0/1/2
# tbox为 target从原图转到特征图的归一化坐标和尺寸[x_center,y_center,w,h]
# indices为 [targets所属image index,targets匹配到的Anchors index,target所在grid 左上角y,target所在grid 左上角x] grid是feature map
# anch为 target匹配到的Anchors的尺寸(从原图转到特征图的Anchor归一化尺寸[w,h])
# 因此返回的例如len(tcls[0])=39482并不是真实目标数量,也不是Anchors总数(并不是88*88*3),而是targets匹配的Anchors数量。
def build_targets(self, p, targets):
# Build targets for compute_loss(), input targets(image,class,x,y,w,h)
# na是anchor的种类 nt是真实目标个数
na, nt = self.na, targets.shape[0] # number of anchors, targets
tcls, tbox, indices, anch = [], [], [], []
# 空间网格增益
gain = torch.ones(7, device=self.device) # normalized to gridspace gain
# ai.shape=(3,一个batch图片目标数之和) 每个目标3种anchor [[0...0],[1...1],[2...2]]
ai = torch.arange(na, device=self.device).float().view(na, 1).repeat(1, nt) # same as .repeat_interleave(nt)
targets = torch.cat((targets.repeat(na, 1, 1), ai[..., None]), 2) # append anchor indices
g = 0.5 # bias
off = torch.tensor(
[
[0, 0],
[1, 0],
[0, 1],
[-1, 0],
[0, -1], # j,k,l,m
# [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm
],
device=self.device).float() * g # offsets
# 对每一个检测头进行迭代
for i in range(self.nl):
anchors, shape = self.anchors[i], p[i].shape
gain[2:6] = torch.tensor(shape)[[3, 2, 3, 2]] # xyxy gain
# Match targets to anchors
t = targets * gain # shape(3,n,7)
if nt:
# Matches
r = t[..., 4:6] / anchors[:, None] # wh ratio
j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t'] # compare
# j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
t = t[j] # filter
# Offsets
gxy = t[:, 2:4] # grid xy
gxi = gain[[2, 3]] - gxy # inverse
j, k = ((gxy % 1 < g) & (gxy > 1)).T
l, m = ((gxi % 1 < g) & (gxi > 1)).T
j = torch.stack((torch.ones_like(j), j, k, l, m))
t = t.repeat((5, 1, 1))[j]
offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
else:
t = targets[0]
offsets = 0
# Define
bc, gxy, gwh, a = t.chunk(4, 1) # (image, class), grid xy, grid wh, anchors
a, (b, c) = a.long().view(-1), bc.long().T # anchors, image, class
gij = (gxy - offsets).long()
gi, gj = gij.T # grid indices
# Append
indices.append((b, a, gj.clamp_(0, shape[2] - 1), gi.clamp_(0, shape[3] - 1))) # image, anchor, grid
tbox.append(torch.cat((gxy - gij, gwh), 1)) # box
anch.append(anchors[a]) # anchors
tcls.append(c) # class
print(tbox)
return tcls, tbox, indices, anch
__call__:计算targets与匹配到的Anchors的损失
#一个batch的数据已经到达战场啦~
# p:预测值
# len(pred)=3 pred指的是每个检测头的输出feature map 每个像素每个anchor的信息
# preds[0].shape (16,3,176,176,8)
# preds[1].shape (16,3,88,88,8)
# preds[2].shape (16,3,21,21,8)
# 8表示这个anchor的[x_center, y_center, w, h, 1置信度信息, 3类别条件概率信息]
# 注:1.检测头所得到的Anchor[x_center, y_center, w, h]是从原图转到特征图的归一化坐标和尺寸。
# 2.与val.py中的的pred是不同的!
# targets:真实值
# targets是一batch的数据,每个目标的[图片索引, 0/1/2, x_center, y_center, w, h]
# targets.shape=(一个batch图片目标数之和,6)
# 注:targets的[x_center, y_center, w, h]是缩放过的原图上的比例。
def __call__(self, p, targets): # predictions, targets
#lcls=lbox=lobj=[0.]
lcls = torch.zeros(1, device=self.device) # class loss
lbox = torch.zeros(1, device=self.device) # box loss
lobj = torch.zeros(1, device=self.device) # object loss
# 一个batch的数据,真实目标在3个检测头上匹配到的Anchors:
# tcls:len(tcls)=3 表示3个检测头上 与targets匹配的Anchors应该的targets类别 0/1/2
# tbox:len(tbox)=3 表示3个检测头上 target从原图转到特征图的归一化坐标和尺寸[x_center, y_center, w, h]
# indices: len(indices)=3 表示3个检测头上 target匹配到的Anchors信息。
# 3个检测头:
# [(b,b..),(a,a..),(gi,gi..),(gj,gj..)],
# [(b,b..),(a,a..),(gi,gi..),(gj,gj..)],
# [(b,b..),(a,a..),(gi,gi..),(gj,gj..)]
# b:target所属image index
# a:target匹配到的Anchors index。每个预测层都有3种类别的Anchor,a=0/1/2指的是该预测层3种Anchor。
# gj:target所在grid 左上角y
# gi:target所在grid 左上角x
# anchors:表示3个检测头上 target匹配到的Anchors尺寸(从原图转到特征图的Anchor归一化尺寸[w,h])
tcls, tbox, indices, anchors = self.build_targets(p, targets) # targets
# Losses:对每个检测头去计算损失
# i=0,1,2
# i=0时,pi.shape=(16,3,176,176,8) 得到了第一个检测头的所有Anchors信息。
for i, pi in enumerate(p): # layer index, layer predictions
# 检测头1 i=0:
# b.shape=(39482,)
# a.shape=(39482,)
# gj.shape=(39482,)
# gi.shape=(39482,)
b, a, gj, gi = indices[i] # image, anchor, gridy, gridx
# tobj.shape (16,3,176,176)
tobj = torch.zeros(pi.shape[:4], dtype=pi.dtype, device=self.device) # target obj
# i=0 n=39482 targets匹配到的Anchors数量
n = b.shape[0] # number of targets
# i=0 targets匹配到的Anchors数量不为0时
if n:
# pi.shape=(16,3,176,176,8)
# 首先,根据索引[b, a, gj, gi]只选取targets匹配到的Anchors 的最后一个维度的数值。
# 然后,在最后一个维度上,因为这个维度有8个数,将8个数划分成2,2,1,3的子数组。
# 2(x_center+y_center)+2(w+h)+1(置信度信息)+3(类别条件概率信息)+
# 得到:
# pxy.shape=(39482,2) 与target匹配Anchor的[x_center,y_center]
# pwh.shape=(39482,2) 与target匹配Anchor的[w,h]
# pcls.shape=(39482,3) 与target匹配Anchor的3个概率信息 #pcls=[[-2.145,-7.889,-2.4554],...[-2.145,-7.889,-2.4554]]
pxy, pwh, _, pcls = pi[b, a, gj, gi].split((2, 2, 1, self.nc), 1) # target-subset of predictions
# 1.Regression 位置回归损失:
# 1-iou=1-(tbox--target从原图转到特征图的[x_center,y_center,w,h])与(pbox--与target匹配的Anchors[x_center,y_center,w,h])的iou
pxy = pxy.sigmoid() * 2 - 0.5
pwh = (pwh.sigmoid() * 2) ** 2 * anchors[i]
# pbox.shape=(39482,4) 与target匹配的Anchors的[x_center,y_center,w,h]
pbox = torch.cat((pxy, pwh), 1) # predicted box
# tbox[0].shape=(39482,4) target从原图转到特征图的[x_center,y_center,w,h]
iou = bbox_iou(pbox, tbox[i], CIoU=True).squeeze() # iou(prediction, target)
lbox += (1.0 - iou).mean() # iou loss
# 2.Objectness 置信度损失:
# (tobj--iou)与(pi[..., 4]--所有Anchors的置信度)的BCE损失
iou = iou.detach().clamp(0).type(tobj.dtype)
# 张量从计算图中分离 iou.detach(),小于0的部分截断为0 clamp(0)
if self.sort_obj_iou:
# 好的,不会计算这玩意
j = iou.argsort()
b, a, gj, gi, iou = b[j], a[j], gj[j], gi[j], iou[j]
if self.gr < 1:
# 好的,不会计算这玩意
iou = (1.0 - self.gr) + self.gr * iou
# tobj.shape=(16,3,176,176)
# 将iou记录到对应的[b,a,gj,gi]索引处
tobj[b, a, gj, gi] = iou # iou ratio
# 3.Classification 分类损失:(pcls--与target匹配Anchors的3个类别概率信息)和(t--targets匹配到的Anchors应该的targets类别)的BCE损失
if self.nc > 1: # cls loss (only if multiple classes)
# pcls.shape=(39482,3) 与target匹配Anchors的3个类别概率信息
# 创建一个与pcls形状相同(39482,3),值为self.cn=0.0的张量 [ [0,0,0],[0,0,0]... ]
t = torch.full_like(pcls, self.cn, device=self.device) # targets
# n是targets匹配到的Anchors数量 39482 range(n)=[0,1,2,...39481]
# tcls为targets匹配到的Anchors应该的targets的类别:tcls[0].shape=39482 tcls[0]=[0,1,0,1,2...0]
# t[range(n), tcls[i]] = self.cp :将t相应索引处赋值为self.cp=1.0
t[range(n), tcls[i]] = self.cp
# t=[[0,0,1],...[0,1,0]]
# pcls=[[-2.145,-7.889,-2.4554],...[-2.145,-7.889,-2.4554]]
# (pcls--与target匹配AnchorsAnchors的3个类别概率信息)和(t--targets匹配到的Anchors标注上targets的类别)的BCE损失
lcls += self.BCEcls(pcls, t) # BCE
# 2.Objectness 置信度损失:
# (tobj--iou)与(pi[..., 4]--所有Anchors的置信度)的BCE损失
# tobj.shape=(16,3,176,176) target从原图转到特征图的[x_center,y_center,w,h]与target匹配Anchors[x_center,y_center,w,h]的iou
# pi.shape=(16,3,176,176,8) 8表示这个Anchor的[x_center, y_center, w, h, 1置信度信息, 3类别条件概率信息]
# pi[..., 4]表示选取前面所有维度的索引,然后在最后一个维度上选择索引为4的元素,即选取所有Anchors的置信度信息
# 注意负样本的置信度损失计算: tobj有些位置是0,说明这个位置没有目标,那么与pi对应索引处会计算置信度损失。
obji = self.BCEobj(pi[..., 4], tobj)
# 置信度损失=置信度损失×平衡系数
lobj += obji * self.balance[i] # obj loss
if self.autobalance:
self.balance[i] = self.balance[i] * 0.9999 + 0.0001 / obji.detach().item()
if self.autobalance:
self.balance = [x / self.balance[self.ssi] for x in self.balance]
lbox *= self.hyp['box']
lobj *= self.hyp['obj']
lcls *= self.hyp['cls']
bs = tobj.shape[0] # batch size
# 最后计算的 lbox、lobj、lcls是一个平均batch的各项loss(具体而言就是用BCE去计算的),因此(lbox + lobj + lcls) * bs看做是一个batch的数据的总loss。
return (lbox + lobj + lcls) * bs, torch.cat((lbox, lobj, lcls)).detach()