2019-10-08
有些目标检测文章一讲到 `loss` 就开始一串公式劈头盖脸砸下来,看着像懂了,回头一写代码又有点发懵。这个话题其实没必要绕那么远: - 真实标签到底长什么样 - 网络最后吐出了什么 - 这些量分别在逼着模型优化什么
如果只盯着公式本身,很容易越看越乱。更稳的办法是反过来:
先看标签,再看网络输出,最后问一句“这个损失到底在逼谁变好”。
用这把尺子去量,Faster R-CNN 更像“分阶段、逐步收紧”,YOLOv3 更像“几项任务打包一起优化”。两家路子不同,但核心工具其实没那么玄乎,主要还是交叉熵和回归误差。
先看第一阶段,也就是 RPN 这一段。
常见实现里会准备两组标签:
y_rpn_cls.shape = (1, row, col, 18)y_rpn_regr.shape = (1, row, col, 72)这里可以把它理解成:
y_rpn_cls 的前 9 个值表示这个位置上的 anchor 能不能参与当前训练y_rpn_cls 的后 9 个值表示这个 anchor 更像前景还是背景y_rpn_regr 的前 36 个值本质上是前景背景信息扩成 4 倍后的掩码y_rpn_regr 的后 36 个值才是真正的框回归目标换句话说,第一阶段一边在挑 anchor,一边在判前后景,一边还顺手做了框回归。
对应地,网络会给出两份输出:
x_class.shape = (n, row, col, 9)x_regr.shape = (n, row, col, 36)前者管分类倾向,后者管回归偏移量。
第一个是 rpn_loss_cls。它看上去写得有点绕,但把掩码和归一化项先放一边,本质就是一个“带筛选条件的二分类交叉熵”。
直觉上可以这么理解:
也就是说,它不是单纯在问“你分对没”,还在通过掩码机制告诉模型“别什么 anchor 都抓着不放”。
第二个是 rpn_loss_regr。这一项的骨架就是回归误差,不过工程里很少直接上裸的 L2,而是换成更稳一点的 smooth L1。
它的味道可以概括成一句话:
所以第一阶段合起来,优化的是三件事:
到了第二阶段,逻辑其实没变,只是对象从“候选框生成”切到了“候选框精修与分类”。
这时候通常会看到:
softmax + categorical crossentropysmooth L1所以二阶段检测器的感觉很像一套接一套地做筛选和修正:先粗筛,再精修。
看完 Faster R-CNN,再看 YOLOv3,你会明显感觉到风格变化了。
它不是先来一段、再来一段,而是把坐标、置信度、类别这些东西揉在一起,直接联合训练。
YOLOv3 的 y_true 一般来自 3 个尺度的特征层,也就是常见的金字塔输出:
(13, 13)(26, 26)(52, 52)每层标签的 shape 都可以写成:
(batch, row, col, 3, 5 + class_num)
这一串维度读顺以后,后面的损失基本就不神秘了:
batch:批次大小row, col:当前尺度下的网格尺寸3:每个位置分到的 3 组 anchor5:x, y, w, h, confidenceclass_num:类别数实现里常见几个名字:
raw_predpred_xypred_whgrid可以粗略理解成:
raw_pred 是网络原始输出pred_xy 是中心点位置相关结果pred_wh 是宽高相关结果grid 帮你把“相对网格偏移”还原到整张图的坐标语境里YOLOv3 常见实现里,核心损失通常拆成四部分:
xy_losswh_lossconfidence_lossclass_lossxy_loss这一项主要管目标中心点。
它通常会乘上两个很关键的权重:
object_maskbox_loss_scale前者可以理解成“这个位置到底有没有目标”,后者常写成和框面积相关的权重,常见理解是 2 - w * h 这类形式。它的作用很实用:小目标别被轻轻带过,大目标也别一股脑吃掉所有权重。
所以这项在优化时,实际上不只是中心点坐标本身,还会和目标存在性、框尺度权重绑在一起考虑。
wh_loss这一项更像纯回归,但又不是完全裸回归。
它会让预测框的宽高往真实框靠,同时也受 object_mask 和框尺度权重影响。所以你会发现,YOLOv3 的损失设计非常喜欢“一个项不只做一件事”,而是让多个目标在同一套结构里互相牵制。
confidence_loss这一项很关键,因为它决定模型怎么看“这里到底有没有东西”。
除了正常的前景 / 背景二值交叉熵,还会有一个 ignore_mask。这个东西很像在说:
“有些位置虽然不是最终负责预测的那个框,但它也不完全该被粗暴打成背景,先别急着罚。”
这个设计和两阶段检测里常说的“中性样本”有点类似味道。它能减少一些不必要的误罚,让训练更稳。
class_loss这一项就相对直接了,就是在有目标的地方去做类别预测,常见写法是带 object_mask 的多标签 / 多分类交叉熵。
最后总损失就是把这几项加起来。
如果你只是想把核心思路先摸熟,用下面这种“缩水版”写法会比直接啃工程代码舒服很多。
smooth L1import numpy as np
def smooth_l1(diff: np.ndarray) -> np.ndarray:
abs_diff = np.abs(diff)
small = abs_diff <= 1.0
return np.where(small, 0.5 * diff ** 2, abs_diff - 0.5)
target = np.array([0.2, 0.4, -0.1, 1.8], dtype=np.float32)
pred = np.array([0.1, 0.1, -0.3, 0.5], dtype=np.float32)
loss = smooth_l1(target - pred).mean()
print("smooth_l1 =", float(loss))
这段基本就能对应上 Faster R-CNN 里那类回归损失的核心形状。
import tensorflow as tf
from tensorflow.keras import backend as K
def demo_rpn_cls_loss(valid_mask, fg_bg_true, fg_bg_pred):
bce = K.binary_crossentropy(fg_bg_true, fg_bg_pred)
masked = valid_mask * bce
return K.sum(masked) / K.sum(1e-6 + valid_mask)
这里最重要的不是语法,而是那个意思:
valid_mask 挑出需要参与训练的 anchorimport torch
import torch.nn.functional as F
def yolo_loss_demo(raw_pred, raw_true_xy, raw_true_wh,
object_mask, true_class_probs, box_loss_scale):
xy_loss = object_mask * box_loss_scale * F.binary_cross_entropy_with_logits(
raw_pred[..., 0:2], raw_true_xy, reduction="none"
)
wh_loss = object_mask * box_loss_scale * 0.5 * (
raw_pred[..., 2:4] - raw_true_wh
) ** 2
conf_loss = F.binary_cross_entropy_with_logits(
raw_pred[..., 4:5], object_mask, reduction="none"
)
cls_loss = object_mask * F.binary_cross_entropy_with_logits(
raw_pred[..., 5:], true_class_probs, reduction="none"
)
total = xy_loss.mean() + wh_loss.mean() + conf_loss.mean() + cls_loss.mean()
return total
这不是完整训练代码,但很适合拿来建立直觉:YOLOv3 的损失确实就是几块任务一起推。
聊到这里,差异其实已经很清楚了。
Faster R-CNN 更像一套分工明确的流程:
YOLOv3 则更像把多个小任务捆在一起冲:
如果把它们各自的气质说得更直白一点:
Faster R-CNN 偏“精准优化”YOLOv3 偏“联合优化”前者层层推进,后者整体联动。