这部分,我们来聊聊YOLO.
YOLO:You Only Look Once,顾名思义,就是希望网络在训练过程中,一张图片只要看一次就行,不需要去多次观察,比如滑框啥的,从而从底层原理上就减少了很多的计算量。0 - 扯扯
1 - YOLO框架
框预测:基于每个网格,会预测需要的B个框(也就是担心预测的一个框不准,所以多预测几个),作者这里选择2个,即\(B=2\),在框预测阶段,需要每个框给出5个值,分别是\(x,y,w,h,confidence\)。其中
i)\(x,y\)表示基于当前网格中心为参照得到的预测框的中心坐标; ii)\(w,h\)表示预测框相对于整个图的width和height; iii)\(confidence = P_{object}*IOU_{pred}^{truth}\)。这里IOU的想法是来自文档检索领域的,这里就不细说了,\(IOU=\frac{真实框\bigcap 预测框}{真实框 \bigcup 预测框}\).这里的置信度就是当前区域是否有对象的概率乘以IOU的值,可以看出如果当前区域没对象,那么该置信度就是为0. iv)因为作者采用的是voc数据集,里面一共有20个类别,所以one-hot就是20维,所以上面的30对应的就是(2*5+20)。回顾目标函数:基于上面的网格划分和框预测,我们知道最后的\(7*7*30\)是会在每个网格上都计算一次,这里我们省去目标函数中的\(S^2\)那个求和,即基于某一个网格I来分析(下面将\(1_{Ij}^{obj}\)缩写成\(1_{j}^{obj}\)):
\[\begin{split}\\ & &\lambda_{coord}\sum_{j=0}^B1_{j}^{obj}[(x_I-\hat x_I)^2+(y_I-\hat y_I)^2]\\ &+&\lambda_{coord}\sum_{j=0}^B1_{j}^{obj}[(\sqrt{w_I}-\sqrt{\hat w_I})^2+(\sqrt{h_I}-\sqrt{\hat h_I})^2]\\ &+&\sum_{j=0}^B1_{j}^{obj}(C_I-\hat C_I)^2\\ &+& \lambda_{noobj}\sum_{j=0}^{B}1_{j}^{noobj}(C_I-\hat C_I)^2\\ &+& 1_{I}^{obj}\sum_{c \in classes}(p_I(c)-\hat p_I(c))^2 \end{split}\] 其中\(1_{i}^{obj}\)表示第i个网格是否有对象的真值函数,即满足则为1,否则为0;而\(1_{ij}^{obj}\)表示在第i个网格中第j个预测候选框负责预测。I 在实现时,获取当前网格的30维度向量基础上,前面5位为一个预测候选框的值:d[:2]为\(x,y\), d[2:4]为\(w,h\); d[4]为置信度;剩下的d[10:30]为类别对象预测那么就2种情况,当前网格有对象和没有对象。而且作者是这样说的,只惩罚当该网格中有对象的时候的分类错误,即如果不存在对象,那么分类上就不惩罚了;而且只惩罚负责预测当前对象的那个候选框,置于这个框怎么选,就是选取候选框中IOU最大的那个:
1 - 没有对象。则\(1_{I}^{obj}\)=0,从而该网格计算的损失函数值为\(\lambda_{noobj}\sum_{j=0}^B1_{j}^{noobj}(C_i-\hat C_i)^2\); 2 - 有对象。则开始计算损失值,首先计算B个预测候选框与真实框的IOU,然后只在那个最大IOU的候选框上计算\(x,y,w,h\),和置信度。 其中\(C\)就是置信度,而\((C_I-\hat C_I)^2\)就是表示预测的置信度减去真实的置信度,因\(confidence = P_{object}*IOU_{pred}^{truth}\),所以IOU一开始并无法给出,其中\(C_I\)就是模型给出的一个值,而\(\hat C_I\)是在训练过程中才能给出的值。(置信度等于出现对象概率乘以IOU,因对象概率在目标函数其他地方就已存在,我们剥离对象概率,所以就是预测IOU的值等于真实IOU的值。即当模型在test的时候,该值能告知当前预测框的置信度) 结合github上的代码理解def loss_layer(self, predicts, labels, scope='loss_layer'): with tf.variable_scope(scope): #从预测的向量中取出对应的部分 predict_classes = tf.reshape(predicts[:, :self.boundary1], [self.batch_size, self.cell_size, self.cell_size, self.num_class]) predict_scales = tf.reshape(predicts[:, self.boundary1:self.boundary2], [self.batch_size, self.cell_size, self.cell_size, self.boxes_per_cell]) predict_boxes = tf.reshape(predicts[:, self.boundary2:], [self.batch_size, self.cell_size, self.cell_size, self.boxes_per_cell, 4]) #从label中计算并生成,response表示某个对象的中心是否落在当前cell中 response = tf.reshape(labels[:, :, :, 0], [self.batch_size, self.cell_size, self.cell_size, 1]) #从label中计算当前对象的坐标信息 boxes = tf.reshape(labels[:, :, :, 1:5], [self.batch_size, self.cell_size, self.cell_size, 1, 4]) boxes = tf.tile(boxes, [1, 1, 1, self.boxes_per_cell, 1]) / self.image_size #因为是label,所以box就一个,从5:25都是20类的类别信息 classes = labels[:, :, :, 5:] offset = tf.constant(self.offset, dtype=tf.float32) offset = tf.reshape(offset, [1, self.cell_size, self.cell_size, self.boxes_per_cell]) offset = tf.tile(offset, [self.batch_size, 1, 1, 1]) predict_boxes_tran = tf.stack([(predict_boxes[:, :, :, :, 0] + offset) / self.cell_size, (predict_boxes[:, :, :, :, 1] + tf.transpose(offset, (0, 2, 1, 3))) / self.cell_size, tf.square(predict_boxes[:, :, :, :, 2]), tf.square(predict_boxes[:, :, :, :, 3])]) predict_boxes_tran = tf.transpose(predict_boxes_tran, [1, 2, 3, 4, 0]) #计算预测出来的box与真实box的IOU iou_predict_truth = self.calc_iou(predict_boxes_tran, boxes) # calculate I tensor [BATCH_SIZE, CELL_SIZE, CELL_SIZE, BOXES_PER_CELL] #选择IOU最大的那个候选框作为预测框 object_mask = tf.reduce_max(iou_predict_truth, 3, keep_dims=True) object_mask = tf.cast((iou_predict_truth >= object_mask), tf.float32) * response # calculate no_I tensor [CELL_SIZE, CELL_SIZE, BOXES_PER_CELL] noobject_mask = tf.ones_like(object_mask, dtype=tf.float32) - object_mask boxes_tran = tf.stack([boxes[:, :, :, :, 0] * self.cell_size - offset, boxes[:, :, :, :, 1] * self.cell_size - tf.transpose(offset, (0, 2, 1, 3)), tf.sqrt(boxes[:, :, :, :, 2]), tf.sqrt(boxes[:, :, :, :, 3])]) boxes_tran = tf.transpose(boxes_tran, [1, 2, 3, 4, 0]) # class_loss #当response为1,则表示当前cell有对象,否则当前cell的分类loss为0 class_delta = response * (predict_classes - classes) class_loss = tf.reduce_mean(tf.reduce_sum(tf.square(class_delta), axis=[1, 2, 3]), name='class_loss') * self.class_scale # object_loss #选取最大那个IOU作为预测框,然后计算置信度的损失,这里predict_scales是网络预测出的值,iou_predict_truth是当前预测框与真实框的IOU object_delta = object_mask * (predict_scales - iou_predict_truth) object_loss = tf.reduce_mean(tf.reduce_sum(tf.square(object_delta), axis=[1, 2, 3]), name='object_loss') * self.object_scale # noobject_loss noobject_delta = noobject_mask * predict_scales noobject_loss = tf.reduce_mean(tf.reduce_sum(tf.square(noobject_delta), axis=[1, 2, 3]), name='noobject_loss') * self.noobject_scale # coord_loss coord_mask = tf.expand_dims(object_mask, 4) boxes_delta = coord_mask * (predict_boxes - boxes_tran) coord_loss = tf.reduce_mean(tf.reduce_sum(tf.square(boxes_delta), axis=[1, 2, 3, 4]), name='coord_loss') * self.coord_scale tf.losses.add_loss(class_loss) tf.losses.add_loss(object_loss) tf.losses.add_loss(noobject_loss) tf.losses.add_loss(coord_loss)
2 - 训练过程
1 - 预训练
在设计出图2的网络结构基础上,需要先基于imagenet的一个预训练,从而找到较好的初始值作为后面的对象检测。所以作者先将图2的前20层卷积层后面部分全部删除,然后加个平均池化层和一个全连接层。作者在imagenet 2012 上训练了1个礼拜,并在验证集上获得了top5 88%准确度。
2 - 将模型用于检测
在得到上述训练好的模型基础上,将后面的平均池化层和全连接层扔掉,接上4个卷积层和2个全连接层(权重都是随机初始化),然后将图片输入大小从224224调整到448448。为了好计算,所以只使用了平方函数,然后为了使得分类导致的惩罚和定位导致的惩罚程度有所不同,增加有对象基础上的定位误差,减少无对象的置信度;并且在大对象上的框误差的严重程度远小于小对象上的框误差程度,所以接着在\(w,h\)上使用了开根号。