基于SSD目标检测网络的树莓派控制系统(三)

基于PyQt5的SSD目标检测软件(三)

续上节。。。。

2. 训练部分

2.1 真实框得处理

​ 预测部分实际上是没有计算loss的向前传播,主要是通过模型进行预测结果,而训练过程主要通过反向传播来修正权重减小整体的成本loss和精度loss从而使模型更加准确。

​ 在预测部分,每个特征层的预测结果,num_priors x 4 的卷积 用于预测 该特征层上 每一个网格点上 每一个先验框微调 情况(bounding box regression边界框回归 在训练中 微调边界框大小匹配真实框))。

​ 所以说,我们直接利用SSD网络预测到的结果只是直接匹配相同类别所得到的原来初始化先验框的结果,而我们需要的是进行bounding box regression 回归后 调整过的准确预测框的位置。

​ 而在训练的时候,我们需要计算loss(分类loss和边界框回归loss(IOU))函数,这个loss函数是相对于ssd网络的预测结果的。我们需要把图片输入到当前的ssd网络中,得到预测结果;同时还需要把真实框的信息,进行编码,这个编码是把真实框的位置信息格式转化为ssd预测结果的格式信息

​ 我们需要找到 每一张用于训练的图片每一个真实框对应的先验框,并通过 回归预测的方法 让模型学会如何调整预测生成的先验框才能得到真实框对应的先验框

从预测结果获得真实框的过程被称作解码,而从真实框获得预测结果的过程就是编码的过程。

​ 因此我们只需要将解码过程逆过来就是编码过程了。

代码实现

1
2
3
4
5
6
def encode(matched, priors, variances):
g_cxcy = (matched[:, :2] + matched[:, 2:])/2 - priors[:, :2]
g_cxcy /= (variances[0] * priors[:, 2:])
g_wh = (matched[:, 2:] - matched[:, :2]) / priors[:, 2:]
g_wh = torch.log(g_wh) / variances[1]
return torch.cat([g_cxcy, g_wh], 1)

​ 训练时选取 IOU最大的框 做为 预测生成的先验框

​ 因此我们还要经过一次筛选,将上述代码获得的真实框对应的所有的iou较大先验框的预测结果中,iou最大的那个筛选出来。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def match(threshold, truths, priors, variances, labels, loc_t, conf_t, idx):
# 计算所有的先验框和真实框的重合程度
overlaps = jaccard(
truths,
point_form(priors)
)
# 所有真实框和先验框的最好重合程度
# [truth_box,1]
best_prior_overlap, best_prior_idx = overlaps.max(1, keepdim=True)
best_prior_idx.squeeze_(1)
best_prior_overlap.squeeze_(1)
# 所有先验框和真实框的最好重合程度
# [1,prior]
best_truth_overlap, best_truth_idx = overlaps.max(0, keepdim=True)
best_truth_idx.squeeze_(0)
best_truth_overlap.squeeze_(0)
# 找到与真实框重合程度最好的先验框,用于保证每个真实框都要有对应的一个先验框
best_truth_overlap.index_fill_(0, best_prior_idx, 2)
# 对best_truth_idx内容进行设置
for j in range(best_prior_idx.size(0)):
best_truth_idx[best_prior_idx[j]] = j

# 找到每个先验框重合程度最好的真实框
matches = truths[best_truth_idx] # Shape: [num_priors,4]
conf = labels[best_truth_idx] + 1 # Shape: [num_priors]
# 如果重合程度小于threhold则认为是背景
conf[best_truth_overlap < threshold] = 0 # label as background
loc = encode(matches, priors, variances)
loc_t[idx] = loc # [num_priors,4] encoded offsets to learn
conf_t[idx] = conf # [num_priors] top class label for each prior
2.2 利用处理完的真实框与对应图片的预测结果计算loss

loss的计算分为三个部分:
1、获取所有正标签的框的预测结果的回归loss。
2、获取所有正标签的种类的预测结果的交叉熵loss。
3、获取一定负标签的种类的预测结果的交叉熵loss。

​ 由于在ssd的训练过程中,正负样本极其不平衡,即 存在对应真实框的先验框可能只有十来个,但是不存在对应真实框的负样本却有几千个,这就会导致负样本的loss值极大,因此我们可以考虑减少负样本的选取,对于ssd的训练来讲,常见的情况是取三倍正样本数量的负样本用于训练。这个三倍呢,也可以修改,调整成自己喜欢的数字。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class MultiBoxLoss(nn.Module):
def __init__(self, num_classes, overlap_thresh, prior_for_matching,
bkg_label, neg_mining, neg_pos, neg_overlap, encode_target,
use_gpu=False):
super(MultiBoxLoss, self).__init__()
self.use_gpu = use_gpu
self.num_classes = num_classes
self.threshold = overlap_thresh
self.background_label = bkg_label
self.encode_target = encode_target
self.use_prior_for_matching = prior_for_matching
self.do_neg_mining = neg_mining
self.negpos_ratio = neg_pos
self.neg_overlap = neg_overlap
self.variance = Config['variance']

def forward(self, predictions, targets):
# 回归信息,置信度,先验框
loc_data, conf_data, priors = predictions
# 计算出batch_size
num = loc_data.size(0)
# 取出所有的先验框
priors = priors[:loc_data.size(1), :]
# 先验框的数量
num_priors = (priors.size(0))
num_classes = self.num_classes
# 创建一个tensor进行处理
loc_t = torch.Tensor(num, num_priors, 4)
conf_t = torch.LongTensor(num, num_priors)
for idx in range(num):
# 获得框
truths = targets[idx][:, :-1].data
# 获得标签
labels = targets[idx][:, -1].data
# 获得先验框
defaults = priors.data
# 找到标签对应的先验框 !!!
match(self.threshold, truths, defaults, self.variance, labels,
loc_t, conf_t, idx)
# if self.use_gpu:
# loc_t = loc_t.cuda()
# conf_t = conf_t.cuda()


# 所有conf_t>0的地方,代表内部包含物体
pos = conf_t > 0
# 求和得到每一个图片内部有多少正样本
num_pos = pos.sum(dim=1, keepdim=True)

# 回归
# 计算回归loss,使用正样本的先验框进行计算,背景框无意义
pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)
loc_p = loc_data[pos_idx].view(-1, 4)
loc_t = loc_t[pos_idx].view(-1, 4)
loss_l = F.smooth_l1_loss(loc_p, loc_t, reduction = 'sum')

# 计算置信度loss,分类
# 转化形式
batch_conf = conf_data.view(-1, self.num_classes)
# 你可以把softmax函数看成一种接受任何数字并转换为概率分布的非线性方法
# 获得每个框预测到真实框的类的概率
loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))
# 对预测结果分布进行修改 批量归一化
loss_c = loss_c.view(num, -1)

loss_c[pos] = 0
# 获得每一张图新的softmax的结果
_, loss_idx = loss_c.sort(1, descending=True)
_, idx_rank = loss_idx.sort(1)
# 计算每一张图的正样本数量
num_pos = pos.long().sum(1, keepdim=True)
# 限制负样本数量
num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)
neg = idx_rank < num_neg.expand_as(idx_rank)

# 计算正样本的loss和负样本的loss
# 平衡正负样本1:3 8732个框 可能只有十几个正样本
pos_idx = pos.unsqueeze(2).expand_as(conf_data)
neg_idx = neg.unsqueeze(2).expand_as(conf_data)
conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, self.num_classes)
targets_weighted = conf_t[(pos+neg).gt(0)]
# loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False) size_average (bool, optional) – 默认情况下,是mini-batchloss的平均值,然而,如果size_average=False,则是mini-batchloss的总和
loss_c = F.cross_entropy(conf_p, targets_weighted, reduction = 'sum') #改版后使用sum

四、数据集准备

​ 本项目采用得是VOC数据集格式具体存放在 VOCdevkit文件夹下,其中voc2ssd.py为综合VOCdevkit/VOC2007/JPEGImages文件夹下图像文件及VOCdevkit/VOC2007/Annotations下标签文件生成索引文件并存放与VOCdevkit/VOC2007/ImageSets/Main的txt文件中分别为训练集/测试集/验证集的索引,而uitils/voc_annotation.py为生成索引绝对路径在2007_test.txt/2007_train.txt/2007_val.txt

​ 对图像生成标签文件的软件使用labelimg进行目标检测数据集标注,labelimg是基于pyqt的一个软件,使用很方便,用以下命令即可安装:

1
pip install labelimg

运行直接在shell内运行命令:

1
labelimg