以前看过yolov系列算法,看完v1至v3版的后发现,噫...yolov3怎么这么像SSD,不过一直没有仔细对比对yolo和SSD,最后也快忘记了yolo到底涨啥样...最近又重新较为仔细的回顾了yolo3算法并研究了相关代码,所以决定好好记录下yolo3并和SSD比较下,因为两者确实很像,但一些细节却不相同。那么先开始讲讲yolo3吧,yolo1,yolo2就不说了。
网络结构
其主干网络是darknet53,其中darknet53预先在imagenet进行图片分类训练,然后在darknet53基础上增加一些卷积分支yolo3的网络结构。可以看到整个网络都是1×1和3×3的卷积,这也就能够解释yolov3速度能够这么快。可以看到yolo3有3个分支用于目标检测,这一点跟SSD很像,只是SSD的分支更多。对于输入大小为416×416的图片,到最终三个分支Predict one, Predict two, Predict three 三个特征图的尺寸分别为,13×13,26×26,52×52.另外该网络还使用FPN,将13×13的特征图上采样后和Predict two分支的26×26进行concat;26×26的特征图同样上采样和 Predict three的52×52特征图concat, 这样网络中间的分支也能够结合后面更高层的特征,获得更强的表达能力。
anchor的设置
yolo3和SSD都是基于预设anchor,也就是在feature map每个像素位置都放置一定数量的参考boxes,然后网络预测时不直接预测目标的坐标值,而是预测相对于这些anchor的offset。最终根据预测的offeset,再计算出真正的目标坐标值。那么其中就会涉及到几个问题:
--如何设置这些anchor,它们的大小和数量
--如何定义一个box相对于anchor的offset
--训练时过程中gt boxes是如何和anchor做匹配,哪些anchor与gt boxes匹配成正样本
在yolo3上面涉及的几个问题都和SSD不一样。对anchor有三个分支,每个分支是一样的。它放置anchor是以feature map的为中心,每个cell(每个cell就是特征图上的某个像数)放置3个不同大小的,以宽w和高h来定义,而w和h不是随便确定的,是作者对通过聚类的方式的方式确定的,如下图所示 ,理论上来说聚类数量,也就是anchor的数量越多,效果是越好的,考虑到速度的原因,yolo的作者每个尺度下选择了3个anchor,但是有3个尺度,所以实际上是9个anchor。具体细节可以查看原论文。
而SSD处理方式是依靠数据分布人工确定。
offset的编码方式,yolo3是这样的,首先box以中心点加w,h的方式描述(c_x, c_y, pw,ph)。对于网络输出的tx, ty, tw, th,其对应的真正box坐标值转换如下:
可以看到box的中心值tx,ty是经过sigmid函数的,也就是说预测的box的中心坐标始终是在该一个cell的范围内,不会超过该cell的范围,这个和SSD是不一样,SSD是将tx,ty值分别乘以anchor的 宽高再cx, cy相加的,也就是SSD允许预测贩box在偏移到cell以后的位置。
对于预测的宽和高的offset,tw = log(bw/pw),th = log(bh/ph), p是相对于anchor的宽ph, 高ph作了归一化。这样保证了预测的offset范围比较小,也有利于模型的收敛。另外要注意的是,上算计算出的(bx,by,bw,bh)是在特征图上的尺度,最后还要以该尺度stride大小(8,16,32)
至于匹配anchor和gt boxes方式,举例说明,假设我们有一个box, 坐标是[203. 138.5 78. 79],映射到三个尺寸分别是(分别除以8,16,32)
[25.375 17.3125 9.75 9.875 ]
[12.6875 8.65625 4.875 4.9375 ]
[ 6.34375 4.328125 2.4375 2.46875 ]
而每个尺度上都是要单独作匹配的,以中间尺度为例,[12.6875 8.65625 4.875 4.9375 ]对应的将是位置[12.5,8.5]位置的cell,而这尺度下3个anchor的宽高分别是[1.875 3.8125],[3.875 2.8125], [3.6875 7.4375],即该位置上有3个box
[12.5 8.5 1.875 3.8125]
[12.5 8.5 3.875 2.8125]
[12.5 8.5 3.6875 7.4375]
计算这3个anchor和box[12.6875 8.65625 4.875 4.9375 ]的iou,iou大于阈值(0.3),我们认为是匹配的。也就是说一个box可以和多个anchor匹配。如果有 box 在各个尺度feature map 都找不到满足的匹配 anchor,那就退而求其次,在所有feature map的 anchor里寻找一个最大匹配就好了。而SSD的匹配过程是:分两个阶段,阶段一为每个gt box找IOU值最大的gt box匹配,阶段二对于anchor与gt IOU大于0.5也进行匹配。
yolo3输出形式
yolo3三个分支输出的维度分别是(以416输入为例):
13×13×(3×(4+1+num_class))
26×26×(3×(4+1+num_class))
52×52×(3×(4+1+num_class))
其中最后的channels数量是3×(4+1+num_class),3代表3个尺度,4是每个预测box的4个offset, 1代表置信度,表明这是目标还是背景,num_class是类别数量。这里的一个不同点是,SSD没有单独置信度值,它是直接将背景作为一个额外的类别,这里的差异也跟后面他们的损失函数会有不同。训练时这些输出对应的标签是这样设置的,对于正样本,置信度值为1, 4个坐标值就是目标在416尺寸图片中对应的实际坐标,而另外num_class就是one-hot向量的形式,注意的是我查看tensorflow实现的源码,做了标签平滑。
onehot = np.zeros(self.num_classes, dtype=np.float)
onehot[bbox_class_ind] = 1.0
uniform_distribution = np.full(self.num_classes, 1.0 / self.num_classes)
deta = 0.01
smooth_onehot = onehot * (1 - deta) + deta * uniform_distribution
损失函数
yolov3损失分为三类,一类是关于bbox reg的损失,区别于SSD利用值使用smooth L1,yolo利用giou计算(不是原作者使用的版本,原作者在yolov1是使用mse),而且计算bbox reg损失时,SSD的target是经过编码,即offse值,而yolo计算损失(giou损失的方式)是使用原图的坐标值来计算iou;第二类是关于anchor预测的是背景还是目标的损失,也就是前面提高的置信度有关;第三类则是分类损失,这里的分类损失不是SSD中的交叉熵,而是多分类的sigmoid损失,即是每个类别单独的作二分类。
具体我们参考一个tensorflow开源实现
def loss_layer(self, conv, pred, label, bboxes, anchors, stride):
conv_shape = tf.shape(conv)
batch_size = conv_shape[0]
output_size = conv_shape[1]
input_size = stride * output_size
conv = tf.reshape(conv, (batch_size, output_size, output_size,
self.anchor_per_scale, 5 + self.num_class))
conv_raw_conf = conv[:, :, :, :, 4:5] # dims=5
conv_raw_prob = conv[:, :, :, :, 5:]
pred_xywh = pred[:, :, :, :, 0:4]
pred_conf = pred[:, :, :, :, 4:5]
label_xywh = label[:, :, :, :, 0:4]
respond_bbox = label[:, :, :, :, 4:5]
label_prob = label[:, :, :, :, 5:]
## bbox有关的损失
giou = tf.expand_dims(self.bbox_giou(pred_xywh, label_xywh), axis=-1)
input_size = tf.cast(input_size, tf.float32) ## 输入大小 (416)
#scale = 2 - gt_box_area / input_img_area ( 1<=scale <2)
bbox_loss_scale = 2.0 - 1.0 * label_xywh[:, :, :, :, 2:3] * label_xywh[:, :, :, :, 3:4] / (input_size ** 2)
giou_loss = respond_bbox * bbox_loss_scale * (1- giou)
## 目标/背景损失
# pred_xywh (batch,size,size,3,4) --> (batch,size,size,3,n_true_box,4)
# bboxes (batch,n_true_box,4)
iou = self.bbox_iou(pred_xywh[:, :, :, :, np.newaxis, :], bboxes[:, np.newaxis, np.newaxis, np.newaxis, :, :])
# iou: (batch,feature_map_size,feature_map_size,3,n_true_box)
max_iou = tf.expand_dims(tf.reduce_max(iou, axis=-1), axis=-1)
respond_bgd = (1.0 - respond_bbox) * tf.cast( max_iou < self.iou_loss_thresh, tf.float32 )
conf_focal = self.focal(respond_bbox, pred_conf)
conf_loss = conf_focal * (
respond_bbox * tf.nn.sigmoid_cross_entropy_with_logits(labels=respond_bbox, logits=conv_raw_conf)
+
respond_bgd * tf.nn.sigmoid_cross_entropy_with_logits(labels=respond_bbox, logits=conv_raw_conf)
)
# z * -log(sigmoid(x)) + (1 - z) * -log(1 - sigmoid(x))
## 多分类损失
prob_loss = respond_bbox * tf.nn.sigmoid_cross_entropy_with_logits(labels=label_prob, logits=conv_raw_prob)
giou_loss = tf.reduce_mean(tf.reduce_sum(giou_loss, axis=[1,2,3,4]))
conf_loss = tf.reduce_mean(tf.reduce_sum(conf_loss, axis=[1,2,3,4]))
prob_loss = tf.reduce_mean(tf.reduce_sum(prob_loss, axis=[1,2,3,4]))
return giou_loss, conf_loss, prob_loss
giou的损失是关于预测的bbox的损失,只对正样本计算,
giou_loss = respond_bbox * bbox_loss_scale * (1- giou)
其中respond_bbox 相当于mask,对应正样本的anchor为1, 负样本为0。
giou是在iou基础上提出的目标检测的一种关于观测框和gt匹配程度的评价标准,反正就是代表了两个框的匹配程度,关于iou的更多描述可以参考论文。
对比SSD, 关于预测的bbox是使用了smooth L1计算bbox的损失。
对于背景/目标的conf_loss,是做sigmoid的交叉熵,需要注意的是这里的实现使用了focal loss,另外下面这行代码
respond_bgd = (1.0 - respond_bbox) * tf.cast( max_iou < self.iou_loss_thresh, tf.float32 )
表明如果一个背景anchor的iou如果超过一定阈值,那我们不计算它的损失。
而最后就是一个多分类的sigmoid交叉熵,区别于SSD,包括背景类的softmax交叉熵。