根据样本种类分布使用图像调用频率不同的方法解决。
1、将样本中的groundtruth读出来,存为一个列表;
2、统计训练样本列表中不同类别的矩形框个数,然后给每个类别按相应目标框数的倒数赋值,(数目越多的种类权重越小),形成按种类的分布直方图;
3、对于训练数据列表,每个epoch训练按照类别权重筛选出每类的图像作为训练数据,如使用
random.choice(population, weights=None, *, cum_weights=None, k=1)更改训练图像索引,可达到样本均衡的效果。
model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc # attach class weights # 根据labels初始化图片采样权重
# utils/general.py
def labels_to_class_weights(labels, nc=80):
# 这里的标签是整个数据集
# Get class weights (inverse frequency) from training labels
# labels是list。读取的是lables下的标签文件,[label,x,y,w,h]坐标是归一化后的坐标,将list的labels拼接为nparray数组形式
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) # occurrences per class
# Prepend gridpoint count (for uCE training)
# 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
# 没有标签的类别将类别数由0改为1,防止下面取导数的时候出错
weights[weights == 0] = 1 # replace empty bins with 1
# 求各个类别标签数的导数,即标签越多,权重就越小
weights = 1 / weights # number of targets per class
# 对权重归一化
weights /= weights.sum() # normalize
# 从numpy.ndarray创建一个张量,返回的张量和ndarray共享同一内存。对张量的修改将反映在ndarray中,反之亦然。返回的张量是不能调整大小的。
# 即将ndarray格式转换成tensor格式
return torch.from_numpy(weights)
def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)):
# Produces image weights based on class_weights and image contents
# 统计每张图中标签的数量,并且把统计结果转换为ndarray格式
class_counts = np.array([np.bincount(x[:, 0].astype(np.int), minlength=nc) for x in labels])
# 每张图片对图中的每个标签的权重求和即为图片的权重
image_weights = (class_weights.reshape(1, nc) * class_counts).sum(1)
# index = random.choices(range(n), weights=image_weights, k=1) # weight image sample
return image_weights
train.py 中定义了是采用加权图像策略用于训练
在训练过程中,当设置参数–image_weights为True时,默认为false,会计算图像采集的权重,若图像权重越大,那么该图像被采样的概率也越大。后面遍历图像时,则按照重新采集的索引dataset.indices进行计算。(那么类别少的图片就会被重复采样,多的采样不完全吗???)
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training')
for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------
model.train()
# Update image weights (optional)
if opt.image_weights:
# Generate indices
"""
如果设置进行图片采样策略,
则根据前面初始化的图片采样权重model.class_weights以及maps配合每张图片包含的类别数
model.class_weights越小,maps越大,cw越大。即标签数量越多,且标签的识别精度越高,赋予的标签的权重越小,从而解决类别不平衡的问题
通过random.choices生成图片索引indices从而进行采样
"""
if rank in [-1, 0]:
cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc # class weights
iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw) # image weights
# 利用random.choices根据图像的权重选取图片,每次选取和训练集相同的图片数。
# 也就意味着训练集中的图片权重大的有的可能会被选取多次,权重小的可能一次都不会被选中用于训练
dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n) # rand weighted idx
然后dataloader.sampler.set_epoch(epoch)会根据重写的图片索引dataset.indices对图像进行采样。
(dataloader.sampler工作原理补充)
yolov5输出有3个预测分支,每个分支的每个网格有3个anchor与之对应
(1) 跨anchor预测
yolov5中使用基于shape的策略,对于每一层,查找真实框所在网格对应的anchor,计算每个anchor与真实框的长宽比,如果最大的长宽比例小于hyp.yaml中的anchor_t=4,认为该真实框为正样本,符合条件的anchor保留下来预测回归,所以一个网格可以有0-3个anchor被保留为正样本。其余不符合长宽比的在该分支中认为是负样本。
(2) 跨分支预测
每一个分支各自独立筛选正样本,互不影响。即在第一个预测分支中某个网格中被认为是正样本,保留了该层的anchor,则在另一个预测分支中,该位置对应的网格符合筛选条件条件,也会被认为是正样本,同样对应anchor也会被保留。这样就增加了正样本的数量。
(3) 跨网格预测
筛选出符合条件的正样本,会计算该正样本中心点相对网格左上点和右下点的偏移。如果中心点与左上角的偏移小于0.5,则把与当前网格左边网格和上边网格也认为是正样本。具体做法,就是把当前网格的真实目标向左和向上偏移0.5网格。负责预测新网格的anchor和和原网格对应的anchor相同。这样做同样增加了正样本。
# P3-P7 的特征点的数量比值为P3 : P4 : P5 = 4:1:0.25
self.balance = {3: [4.0, 1.0, 0.4]}.get(det.nl, [4.0, 1.0, 0.25, 0.06, .02])
lobj += obji * self.balance[i]
模型训练时样本类别不均衡怎么办? labels_to_class_weights
https://www.cnblogs.com/xiaoheizi-12345/p/14458802.html
【参考】
1.云深安小生--YOLOV5代码理解——类权重系数和图像权重系数