1.代码下载:
https://github.com/ultralytics/yolov3
2.边框回归与损失函数相关的源码,在文件utils.py中。边框回归和损失函数。
边框回归说白了就是:找到一个平移和放缩系数,使得目标值与真值去无限接近。满足这个无限接近条件的系数就是回归系数了。无限接近的意思就是两者尽量像呗,量化的话就是构造个损失函数,让这个函数代表二者相似程度呗,越像,二者之差越小, 通过不断缩小损失函数值,就可以获得一个合适的平移和放缩系数了啊。缩小损失函数值的过程就是优化啊。神经网络的常规套路吧。
核心代码就是下面的啊这两个函数。
compute_loss就是构造损失函数过程,其中边框回归损失函数的组成部分就是 损失函数 = 边框回归系数*anchors - 正样本真值,边框回归神经网络训练的目的就是找到这组回归系数使得正样本对应的anchors无限接近正样本的真值。最终程序输出的置信度是存在置信度*分类置信度,切记,切记。
def compute_loss(p, targets, model): # predictions, targets, model
ft = torch.cuda.FloatTensor if p[0].is_cuda else torch.Tensor
lcls, lbox, lobj = ft([0]), ft([0]), ft([0])
#筛选正样本,并且将anchor与正样本对应上 且正样本的box信息映射到了每一层特征图上。
tcls, tbox, indices, anchor_vec = build_targets(p, targets, model)
print("tcls = ", tcls)
#print("tbox = ", tbox)
#print("indices = ", indices)
#print("anchor_vec = ", anchor_vec)
h = model.hyp # hyperparameters
red = 'mean' # Loss reduction (sum or mean)
# Define criteria#定义损失函数,输入参数
# pos_weight可用于控制各样本的权重 reduction用来控制损失输出模式。
# 设为"sum"表示对样本进行求损失和;设为"mean"表示对样本进行求损失的
# 平均值;而设为"none"表示对样本逐个求损失,输出与输入的shape一样
BCEcls = nn.BCEWithLogitsLoss(pos_weight=ft([h['cls_pw']]), reduction=red)
BCEobj = nn.BCEWithLogitsLoss(pos_weight=ft([h['obj_pw']]), reduction=red)
# class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
cp, cn = smooth_BCE(eps=0.0)# cp = 1 cn = 0 不去理会了
# focal loss
g = h['fl_gamma'] # focal loss gamma
if g > 0:#这个focal loss没用
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
# Compute losses
np, ng = 0, 0 # number grid points, targets
for i, pi in enumerate(p): # layer index, layer predictions #pi为第i层特征图 i是层编号
b, a, gj, gi = indices[i] # image, anchor, gridy, gridx 是第i层正样本的batch编号 anchor 以及栅格坐标
tobj = torch.zeros_like(pi[..., 0]) # target obj
np += tobj.numel()
# Compute losses
nb = len(b)#nb = 第i层的正样本个数
if nb: # number of targets
ng += nb #ng 总正样本个数
#通过b a gj gi 做索引,在特征图pi上取出正样本特征值
ps = pi[b, a, gj, gi] # prediction subset corresponding to targets #维度为(nb, 9)
# ps[:, 2:4] = torch.sigmoid(ps[:, 2:4]) # wh power loss (uncomment)
# GIoU 计算中心坐标 用sigmoid将tx,ty压缩到[0,1]区间內,可以有效的确保目标中心处于执行预测的网格单元中,防止偏移过多
# 预测出来的是一个偏移量 不是绝对坐标值 切记 切记
# 网络不会预测边界框中心的确切坐标而是预测与预测目标的grid cell左上角相关的偏移tx,ty
pxy = torch.sigmoid(ps[:, 0:2]) # pxy = pxy * s - (s - 1) / 2, s = 1.5 (scale_xy)
# 计算正样本box框的 w, h
pwh = torch.exp(ps[:, 2:4]).clamp(max=1E3) * anchor_vec[i]
# 合成完整的box框信息,带中心坐标 带w h
# pbox 是预测函数 anchor是初值, 之间的变换参数就是要训练出的回归系数 训练的目的就是让pbox无限接近真值,得到这组
# 无限接近真值时的系数 这就是边框回归的核心
pbox = torch.cat((pxy, pwh), 1) # predicted box
# 下面就是构造损失函数让预测结果通过怎样的优化去更接近真值了 构造损失函数后就定义了对应关系了啊
#计算GIOU部分
giou = bbox_iou(pbox.t(), tbox[i], x1y1x2y2=False, GIoU=True) # giou computation
#计算giou的损失值 box的loss是1-giou的值
lbox += (1.0 - giou).sum() if red == 'sum' else (1.0 - giou).mean() # giou loss
# 给正样本的tobj赋初值,初值里用到了giou
tobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * giou.detach().clamp(0).type(tobj.dtype) # giou ratio
if model.nc > 1: # cls loss (only if multiple classes) 类别大于1 多分类
t = torch.full_like(ps[:, 5:], cn) # targets
t[range(nb), tcls[i]] = cp
lcls += BCEcls(ps[:, 5:], t) # BCE 这个算的是类别的loss值
# lcls += CE(ps[:, 5:], tcls[i]) # CE
# Append targets to text file
# with open('targets.txt', 'a') as file:
# [file.write('%11.5g ' * 4 % tuple(x) + '\n') for x in torch.cat((txy[i], twh[i]), 1)]
# 计算交叉熵 正样本与特征图上提的特征计算交叉熵 这个算的是置信度的loss值
lobj += BCEobj(pi[..., 4], tobj) # obj loss
#print("lcls = ", lcls)
lbox *= h['giou']
lobj *= h['obj']
lcls *= h['cls']
if red == 'sum':
bs = tobj.shape[0] # batch size
lobj *= 3 / (6300 * bs) * 2 # 3 / np * 2
if ng:
lcls *= 3 / ng / model.nc
lbox *= 3 / ng
loss = lbox + lobj + lcls
return loss, torch.cat((lbox, lobj, lcls, loss)).detach()
#在每个yolo层将预设的anchor和ground truth进行匹配,得到正样本
#规则:
# 1.如果一个预测框与所有的GroundTruth的最大 IoU < ignore_thresh时,那这个预测框就是负样本
# 2.如果Ground Truth的中心点落在一个区域中,该区域就负责检测该物体。将与该物体有最大IoU
# 的预测框作为正样本(注意这里没有用到ignore thresh,即使该最大IoU<ignore thresh也不会影
# 响该预测框为正样本)
def build_targets(p, targets, model):
# targets = [image, class, x, y, w, h] image表示batch中图片编号 class表示类别 x y w h就是box信息
nt = targets.shape[0]
print("targets = ", targets)
tcls, tbox, indices, av = [], [], [], []
reject, use_all_anchors = True, True
gain = torch.ones(6, device=targets.device) # normalized to gridspace gain
# m = list(model.modules())[-1]
# for i in range(m.nl):
# anchors = m.anchors[i]
multi_gpu = type(model) in (nn.parallel.DataParallel, nn.parallel.DistributedDataParallel)
for i, j in enumerate(model.yolo_layers):
# get number of grid points and anchor vec for this yolo layer
# yolov3.cfg中有三个yolo层,这部分用于获取对应yolo层的grid(网格)尺寸和anchor大小
# i值从0到2 对应尺度从大到小 cfg中读取文件后 0层除32 1层除16 2层除8获得anchors值在当前层特征图上的尺寸(anchors值对应的是原图上的坐标)
anchors = model.module.module_list[j].anchor_vec if multi_gpu else model.module_list[j].anchor_vec
# iou of targets-anchors
# p[i]就是某层的预测结果 大小为0 12 * 12 * 9, 1层为24 * 24 * 9, 2层为48 * 48 * 9 这个9是class(4) + 4坐标 + 1置信度而得
gain[2:] = torch.tensor(p[i].shape)[[3, 2, 3, 2]] # whwh gain 将该层的特征图的w h w h依次放在 gain的【2 3 4 5 】位置
#t存放真值在特征图上的box信息 包括中心点坐标 宽 高坐标(相对于特征图的坐标)
t, a = targets * gain, []
#gwh存放真值在特征图上的box宽高 gw gh
gwh = t[:, 4:6]
if nt:
# anchor_vec: shape = [3, 2] 代表3个anchor
# gwh: shape = [4, 2] 代表 4个ground truth
# iou: shape = [3, 4] 代表 3个anchor与对应的两个ground truth的iou
# 常规的iou计算
iou = wh_iou(anchors, gwh) # iou(3,n) = wh_iou(anchors(3,2), gwh(n,2))
if use_all_anchors:
na = anchors.shape[0] # number of anchors na = anchor个数
#每个真值对应的anchor编号
a = torch.arange(na).view(-1, 1).repeat(1, nt).view(-1)
#每个真值在对应anchor上的类别信息以及box框的信息值(对应于当前层特征图的)【image, class, x, y, w, h】
t = t.repeat(na, 1)
else: # use best anchor only
#只选择最大iou的anchor与真值对应
iou, a = iou.max(0) # best iou and anchor
# reject anchors below iou_thres (OPTIONAL, increases P, lowers R)
if reject:
#j中存的是 [true of false] 是每个anchor与每个真值的一一对应关系
j = iou.view(-1) > model.hyp['iou_t'] # iou threshold hyperparameter
#滤除阈值小于ignore thresh的anchor t存的是真值在特征图上的box信息以及图像和类别信息,a是anchor的编号信息
t, a = t[j], a[j]
#做完阈值滤除后筛选剩下的真值与anchor对应 其实就是正样本
# Indices #b是图像编号 c是真值的类别编号
b, c = t[:, :2].long().t() # target image, class
# 真值在特征图上的box信息
gxy = t[:, 2:4] # grid x, y box在特征图上的中心坐标
gwh = t[:, 4:6] # grid w, h box在特征图上的框的宽高
#是网格索引注意这里通过long将其转化为整形,代表格子的左上角
gi, gj = gxy.long().t() # grid x, y indices
# indice结构体保存内容为:
'''
b: 一个batch中的下标
a: 代表所选中的正样本的anchor的下标
gj, gi: 代表所选中的栅格的左上角坐标
'''
indices.append((b, a, gj, gi))
# Box
gxy -= gxy.floor() # xy 下取整然后 gxy 算的是box框在特征图的栅格中的坐标值 是浮点的。
tbox.append(torch.cat((gxy, gwh), 1)) # xywh (grids) tbox存的是正样本box在特征图栅格内的浮点坐标以及box框的宽高值
av.append(anchors[a]) # anchor vec
# Class
tcls.append(c)
if c.shape[0]: # if any targets
assert c.max() < model.nc, 'Model accepts %g classes labeled from 0-%g, however you labelled a class %g. ' \
'See https://github.com/ultralytics/yolov3/wiki/Train-Custom-Data' % (
model.nc, model.nc - 1, c.max())
# tcls yolov3的三层中存下来的正样本的类别号,tbox 正样本存下来的在对应特征图上的相对自己栅格的box中心浮点坐标以及对应的box框
# indices存放的是上面写了 不赘述了 av存放的是正样本对应的不同层的anchor的box的宽高值(anchor真值除32 16 8)
return tcls, tbox, indices, av
3.yolov3代码使用方法:参考我的博客https://blog.csdn.net/gbz3300255/article/details/106276897
3.完整代码以及注释:
import glob
import math
import os
import random
import shutil
import subprocess
from pathlib import Path
from sys import platform
import cv2
#import matplotlib
#import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
import torchvision
from tqdm import tqdm
from . import torch_utils # , google_utils
# Set printoptions
torch.set_printoptions(linewidth=320, precision=5, profile='long')
np.set_printoptions(linewidth=320, formatter={'float_kind': '{:11.5g}'.format}) # format short g, %precision=5
mpl.rc('font', **{'size': 11})
# Prevent OpenCV from multithreading (to use PyTorch DataLoader)
cv2.setNumThreads(0)
def init_seeds(seed=0):
random.seed(seed)
np.random.seed(seed)
torch_utils.init_seeds(seed=seed)
def check_git_status():
if platform in ['linux', 'darwin']:
# Suggest 'git pull' if repo is out of date
s = subprocess.check_output('if [ -d .git ]; then git fetch && git status -uno; fi', shell=True).decode('utf-8')
if 'Your branch is behind' in s:
print(s[s.find('Your branch is behind'):s.find('\n\n')] + '\n')
def load_classes(path):
# Loads *.names file at 'path'
with open(path, 'r') as f:
names = f.read().split('\n')
return list(filter(None, names)) # filter removes empty strings (such as last line)
def labels_to_class_weights(labels, nc=80):
# Get class weights (inverse frequency) from training labels
if labels[0] is None: # no labels loaded
return torch.Tensor()
labels = np.concatenate(labels, 0) # labels.shape = (866643, 5) for COCO
classes = labels[:, 0].astype(np.int) # labels = [class xywh]
weights = np.bincount(classes, minlength=nc) # occurences per class 算每类目标出现的次数呢例如 四类 可能结果为[100 100 20 1] (ps:样本总数221)
# Prepend gridpoint count (for uCE trianing)
# gpi = ((320 / 32 * np.array([1, 2, 4])) ** 2 * 3).sum() # gridpoints per image
# weights = np.hstack([gpi * len(labels) - weights.sum() * 9, weights * 9]) ** 0.5 # prepend gridpoints to start
weights[weights == 0] = 1 # replace empty bins with 1
weights = 1 / weights # number of targets per class
weights /= weights.sum() # normalize 对样本分布做归一化,这个结果总和为1 表示了每类目标在此数据集中的百分比
return torch.from_numpy(weights)
def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)):
# Produces image weights based on class mAPs
n = len(labels)
class_counts = np.array([np.bincount(labels[i][:, 0].astype(np.int), minlength=nc) for i in range(n)])
image_weights = (class_weights.reshape(1, nc) * class_counts).sum(1)
# index = random.choices(