这部分主要讲的是trainer.py和train.py。
而ProposalTargetCreator()的作用是从2000个筛选出的ROIS中再次选出128个ROIs用于训练,它的作用和前面的anchortargetCreator类似,不过它们服务的网络是不同的,前面anchortargetCreator服务的是RPN网络,而proposaltargetCreator服务的是ROIHearder的网络,ROIheader的作用就是真正产生ROI__loc和ROI_cls的网络,它完成了目标检测最重要的预测目标位置和类别!
下面这张图片清楚的展现了整体的框架
下面是trainer.py
def __init__(self, faster_rcnn):
super(FasterRCNNTrainer, self).__init__()#继承父模块的初始化
self.faster_rcnn = faster_rcnn
self.rpn_sigma = opt.rpn_sigma
self.roi_sigma = opt.roi_sigma #是在_faster_rcnn_loc_loss调用用来计算位置损失函数用到的超参数,
# target creator create gt_bbox gt_label etc as training targets.
self.anchor_target_creator = AnchorTargetCreator() #用于从20000个候选anchor中产生256个anchor进行二分类和位置回归,也就是为rpn网络产生的预测位置和预测类别提供真正的ground_truth标准
self.proposal_target_creator = ProposalTargetCreator() #AnchorTargetCreator和ProposalTargetCreator是为了生成训练的目标(或称ground truth),只在训练阶段用到,ProposalCreator是RPN为Fast R-CNN生成RoIs,在训练和测试阶段都会用到。所以测试阶段直接输进来300个RoIs,而训练阶段会有AnchorTargetCreator的再次干预。
self.loc_normalize_mean = faster_rcnn.loc_normalize_mean #(0., 0., 0., 0.)
self.loc_normalize_std = faster_rcnn.loc_normalize_std #(0.1, 0.1, 0.2, 0.2)
self.optimizer = self.faster_rcnn.get_optimizer() #SGD
# visdom wrapper
self.vis = Visualizer(env=opt.env) #可视化,vis_tool.py
# indicators for training status
self.rpn_cm = ConfusionMeter(2)#混淆矩阵,就是验证预测值与真实值精确度的矩阵ConfusionMeter(2)括号里的参数指的是类别数
self.roi_cm = ConfusionMeter(21) #roi的类别有21种(20个object类+1个background)
self.meters = {k: AverageValueMeter() for k in LossTuple._fields} # average loss
def forward(self, imgs, bboxes, labels, scale):
n = bboxes.shape[0] #获取batch个数
if n != 1: #本程序只支持batch_size=1
raise ValueError('Currently only batch size 1 is supported.')
_, _, H, W = imgs.shape #(n,c,hh,ww)
img_size = (H, W)
features = self.faster_rcnn.extractor(imgs) #vgg16 conv5_3之前的部分提取图片的特征
rpn_locs, rpn_scores, rois, roi_indices, anchor = \
self.faster_rcnn.rpn(features, img_size, scale) #rpn_locs的维度(hh*ww*9,4),rpn_scores维度为(hh*ww*9,2), rois的维度为(2000,4),roi_indices用不到,anchor的维度为(hh*ww*9,4),H和W是经过数据预处理后的。计算(H/16)x(W/16)x9(大概20000)个anchor属于前景的概率,取前12000个并经过NMS得到2000个近似目标框G^的坐标。roi的维度为(2000,4)
# Since batch size is one, convert variables to singular form
bbox = bboxes[0] #bbox维度(N, R, 4)
label = labels[0] #labels维度为(N,R)
rpn_score = rpn_scores[0] #(hh*ww*9,4)
rpn_loc = rpn_locs[0] #hh*ww*9
roi = rois #(2000,4)
# Sample RoIs and forward
# it's fine to break the computation graph of rois,
# consider them as constant input
sample_roi, gt_roi_loc, gt_roi_label = self.proposal_target_creator(
roi,
at.tonumpy(bbox),
at.tonumpy(label),
self.loc_normalize_mean,
self.loc_normalize_std) #调用proposal_target_creator函数生成sample roi(128,4)、gt_roi_loc(128,4)、gt_roi_label(128,1),RoIHead网络利用这sample_roi+featue为输入,输出是分类(21类)和回归(进一步微调bbox)的预测值,那么分类回归的groud truth就是ProposalTargetCreator输出的gt_roi_label和gt_roi_loc。
# NOTE it's all zero because now it only support for batch=1 now
sample_roi_index = t.zeros(len(sample_roi))
roi_cls_loc, roi_score = self.faster_rcnn.head(
features,
sample_roi,
sample_roi_index) #roi回归输出的是128*84和128*21,然而真实位置参数是128*4和真实标签128*1
下面这张图最后一部分很好的说明了proposal_target_creator函数的作用以及ROI header的处理过程
下面是RPN losses部分
gt_rpn_loc, gt_rpn_label = self.anchor_target_creator(
at.tonumpy(bbox),
anchor,
img_size) #输入20000个anchor和bbox,调用anchor_target_creator函数得到2000个anchor与bbox的偏移量与label
gt_rpn_label = at.totensor(gt_rpn_label).long()
gt_rpn_loc = at.totensor(gt_rpn_loc)
rpn_loc_loss = _fast_rcnn_loc_loss(
rpn_loc,
gt_rpn_loc,
gt_rpn_label.data,
self.rpn_sigma) #下面分析_fast_rcnn_loc_loss函数。rpn_loc为rpn网络回归出来的偏移量(20000个),gt_rpn_loc为anchor_target_creator函数得到2000个anchor与bbox的偏移量,rpn_sigma=1.
# NOTE: default value of ignore_index is -100 ...
rpn_cls_loss = F.cross_entropy(rpn_score, gt_rpn_label.cuda(), ignore_index=-1) #rpn_score为rpn网络得到的(20000个)与anchor_target_creator得到的2000个label求交叉熵损失
_gt_rpn_label = gt_rpn_label[gt_rpn_label > -1] #不计算背景类
_rpn_score = at.tonumpy(rpn_score)[at.tonumpy(gt_rpn_label) > -1]
self.rpn_cm.add(at.totensor(_rpn_score, False), _gt_rpn_label.data.long())
下面是_fast_rcnn_loc_loss函数,其中用到了smooth_l1_loss。in_weight代表的是权重,用in_weight来作为权重,只将那些不是背景的anchor/ROIs的位置加入到损失函数的计算中来,方法就是只给不是背景的anchor/ROIs的in_weight设置为1,这样就可以完成loc_loss的求和计算
def _fast_rcnn_loc_loss(pred_loc, gt_loc, gt_label, sigma): #输入分别为rpn回归框的偏移量与anchor与bbox的偏移量以及label
in_weight = t.zeros(gt_loc.shape).cuda()
# Localization loss is calculated only for positive rois.
# NOTE: unlike origin implementation,
# we don't need inside_weight and outside_weight, they can calculate by gt_label
in_weight[(gt_label > 0).view(-1, 1).expand_as(in_weight).cuda()] = 1
loc_loss = _smooth_l1_loss(pred_loc, gt_loc, in_weight.detach(), sigma) #sigma设置为1
# Normalize by total number of negtive and positive rois.
loc_loss /= ((gt_label >= 0).sum().float()) # ignore gt_label==-1 for rpn_loss #除去背景类
return loc_loss
下面是roi_loss
n_sample = roi_cls_loc.shape[0] #roi_cls_loc为VGG16RoIHead的输出(128*84), n_sample=128
roi_cls_loc = roi_cls_loc.view(n_sample, -1, 4)# roi_cls_loc=(128,21,4)
roi_loc = roi_cls_loc[t.arange(0, n_sample).long().cuda(), \
at.totensor(gt_roi_label).long()]
gt_roi_label = at.totensor(gt_roi_label).long()#proposal_target_creator()生成的128个proposal与bbox求得的偏移量dx,dy,dw,dh
gt_roi_loc = at.totensor(gt_roi_loc)#128个标签
roi_loc_loss = _fast_rcnn_loc_loss(
roi_loc.contiguous(),
gt_roi_loc,
gt_roi_label.data,
self.roi_sigma)#采用smooth_l1_loss
roi_cls_loss = nn.CrossEntropyLoss()(roi_score, gt_roi_label.cuda())#求交叉熵损失
self.roi_cm.add(at.totensor(roi_score, False), gt_roi_label.data.long())
losses = [rpn_loc_loss, rpn_cls_loss, roi_loc_loss, roi_cls_loss] #四个loss加起来
losses = losses + [sum(losses)]
return LossTuple(*losses)
train_step过程
整个函数实际上就是进行了一次参数的优化过程,首先self.optimizer.zero_grad()将梯度数据全部清零,然后利用刚刚介绍的self.forward(imgs,bboxes,labels,scales)函数将所有的损失计算出来,接着进行依次losses.total_loss.backward()反向传播计算梯度,self.optimizer.step()进行一次参数更新过程,
self.update_meters(losses)就是将所有损失的数据更新到可视化界面上,最后将losses返回!
def save() def load() def update_meters(),def reset_meters() def get_meter_data()函数。这几个函数不想仔细展开解释了,顾名思义,save和load就是根据传入的参数来选择保存model模型或者说config设置或者是other_info其他参数vis_info可视化参数等等
而update_meters,reset_meters以及get_meter_data()就是负责将数据向可视化界面更新传输获取以及重置的函数,基本上也和主要代码没什么太大的关系!
下面是train.py部分代码
def train(**kwargs):
opt._parse(kwargs)#将调用函数时候附加的参数用,config.py文件里面的opt._parse()进行解释,然后获取其数据存储的路径,之后放到Dataset里面!
dataset = Dataset(opt)
print('load data')
dataloader = data_.DataLoader(dataset, \
batch_size=1, \
shuffle=True, \
# pin_memory=True,
num_workers=opt.num_workers) #Dataset完成的任务见第一篇博客数据预处理部分,这里简单解释一下,就是用VOCBboxDataset作为数据读取库,然后依次从样例数据库中读取图片出来,还调用了Transform(object)函数,完成图像的调整和随机反转工作!
testset = TestDataset(opt)
test_dataloader = data_.DataLoader(testset,
batch_size=1,
num_workers=opt.test_num_workers,
shuffle=False, \
pin_memory=True
) #将数据装载到dataloader中,shuffle=True允许数据打乱排序,num_workers是设置数据分为几批处理,同样的将测试数据集也进行同样的处理,然后装载到test_dataloader中!
faster_rcnn = FasterRCNNVGG16()#接下来定义faster_rcnn=FasterRCNNVGG16()定义好模型
print('model construct completed')
trainer = FasterRCNNTrainer(faster_rcnn).cuda() #设置trainer = FasterRCNNTrainer(faster_rcnn).cuda()将FasterRCNNVGG16作为fasterrcnn的模型送入到FasterRCNNTrainer中并设置好GPU加速
if opt.load_path:
trainer.load(opt.load_path) #接下来判断opt.load_path是否存在,如果存在,直接从opt.load_path读取预训练模型,然后将训练数据的label进行可视化操作
print('load pretrained model from %s' % opt.load_path)
trainer.vis.text(dataset.db.label_names, win='labels')
best_map = 0
lr_ = opt.lr
for epoch in range(opt.epoch): #之后用一个for循环开始训练过程,而训练迭代的次数opt.epoch=14也在config.py文件中都预先定义好,属于超参数
trainer.reset_meters() #首先在可视化界面重设所有数据
for ii, (img, bbox_, label_, scale) in tqdm(enumerate(dataloader)):
scale = at.scalar(scale)
img, bbox, label = img.cuda().float(), bbox_.cuda(), label_.cuda() #然后从训练数据中枚举dataloader,设置好缩放范围,将img,bbox,label,scale全部设置为可gpu加速
trainer.train_step(img, bbox, label, scale) #调用trainer.py中的函数trainer.train_step(img,bbox,label,scale)进行一次参数迭代优化过程
if (ii + 1) % opt.plot_every == 0:
if os.path.exists(opt.debug_file):
ipdb.set_trace() # 判断数据读取次数是否能够整除plot_every(是否达到了画图次数),如果达到判断debug_file是否存在,用ipdb工具设置断点,调用trainer中的trainer.vis.plot_many(trainer.get_meter_data())将训练数据读取并上传完成可视化!
# plot loss
trainer.vis.plot_many(trainer.get_meter_data())
# plot groud truth bboxes
ori_img_ = inverse_normalize(at.tonumpy(img[0]))
gt_img = visdom_bbox(ori_img_,
at.tonumpy(bbox_[0]),
at.tonumpy(label_[0]))
trainer.vis.img('gt_img', gt_img) #将每次迭代读取的图片用dataset文件里面的inverse_normalize()函数进行预处理,将处理后的图片调用Visdom_bbox
# plot predicti bboxes
_bboxes, _labels, _scores = trainer.faster_rcnn.predict([ori_img_], visualize=True) #调用faster_rcnn的predict函数进行预测,预测的结果保留在以_下划线开头的对象里面
pred_img = visdom_bbox(ori_img_,
at.tonumpy(_bboxes[0]),
at.tonumpy(_labels[0]).reshape(-1),
at.tonumpy(_scores[0]))
trainer.vis.img('pred_img', pred_img) #利用同样的方法将原始图片以及边框类别的预测结果同样在可视化工具中显示出来!
# rpn confusion matrix(meter)
trainer.vis.text(str(trainer.rpn_cm.value().tolist()), win='rpn_cm')#调用 trainer.vis.text将rpn_cm也就是RPN网络的混淆矩阵在可视化工具中显示出来
# roi confusion matrix
trainer.vis.img('roi_cm', at.totensor(trainer.roi_cm.conf, False).float())
eval_result = eval(test_dataloader, faster_rcnn, test_num=opt.test_num)
trainer.vis.plot('test_map', eval_result['map']) #调用trainer.vis.img将Roi_cm将roi的可视化矩阵以图片的形式显示出来
lr_ = trainer.faster_rcnn.optimizer.param_groups[0]['lr']#设置学习的learning rate
log_info = 'lr:{}, map:{},loss:{}'.format(str(lr_),
str(eval_result['map']),
str(trainer.get_meter_data()))
trainer.vis.log(log_info)#将损失学习率以及map等信息及时显示更新
if eval_result['map'] > best_map:
best_map = eval_result['map']
best_path = trainer.save(best_map=best_map) #用if判断语句永远保存效果最好的map
if epoch == 9:
trainer.load(best_path)
trainer.faster_rcnn.scale_lr(opt.lr_decay)
lr_ = lr_ * opt.lr_decay #if判断语句如果学习的epoch达到了9就将学习率*0.1变成原来的十分之一
if epoch == 13:
break #判断epoch==13结束训练验证过程
def eval():
eval()顾名思义,就是一个评估预测结果好坏的函数,展开来看果不其然,首先pred_bboxes,pred_labels,pred_scores ,gt_bboxes,gt_labels,gt_difficults 一开始就定义了这么多的list列表!它们分别是预测框的位置,预测框的类别和分数以及相应的真实值的类别分数等等!
接下来就是一个for循环,从 enumerate(dataloader)里面依次读取数据,读取的内容是: imgs图片,sizes尺寸,gt_boxes真实框的位置 gt_labels真实框的类别以及gt_difficults这些
然后利用faster_rcnn.predict(imgs,[sizes]) 得出预测的pred_boxes_,pred_labels_,pred_scores_预测框位置,预测框标记以及预测框的分数等等!这里的predict是真正的前向传播过程!完成真正的预测目的!
之后将pred_bbox,pred_label,pred_score ,gt_bbox,gt_label,gt_difficult预测和真实的值全部依次添加到开始定义好的列表里面去,如果迭代次数等于测试test_num,那么就跳出循环!调用 eval_detection_voc函数,接收上述的六个列表参数,完成预测水平的评估!得到预测的结果!这个eval_detection_voc后面会解释!