码到成功

FRCNN 和 YOLOv3 损失函数详解

## 先把观察角度摆好

如果只盯着公式本身,很容易越看越乱。更稳的办法是反过来:

先看标签,再看网络输出,最后问一句“这个损失到底在逼谁变好”。

用这把尺子去量,`Faster R-CNN` 更像“分阶段、逐步收紧”,`YOLOv3` 更像“几项任务打包一起优化”。两家路子不同,但核心工具其实没那么玄乎,主要还是交叉熵和回归误差。

## Faster R-CNN:两阶段,思路很直

先看第一阶段,也就是 `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 计算损失
- 让前景 / 背景判断更准
- 顺手把真正参与训练的 anchor 范围收得更聚焦

也就是说,它不是单纯在问“你分对没”,还在通过掩码机制告诉模型“别什么 anchor 都抓着不放”。

第二个是 `rpn_loss_regr`。这一项的骨架就是回归误差,不过工程里很少直接上裸的 `L2`,而是换成更稳一点的 `smooth L1`。

它的味道可以概括成一句话:

- 小误差区域像平方误差,鼓励细修
- 大误差区域像绝对值误差,避免梯度太炸

所以第一阶段合起来,优化的是三件事:

- 该启用哪些 anchor
- 这个 anchor 更像前景还是背景
- 这个 anchor 该往哪个框回归

### 第二阶段怎么理解

到了第二阶段,逻辑其实没变,只是对象从“候选框生成”切到了“候选框精修与分类”。

这时候通常会看到:

- 一个分类损失,常见是 `softmax + categorical crossentropy`
- 一个回归损失,依然常见是 `smooth L1`

所以二阶段检测器的感觉很像一套接一套地做筛选和修正:先粗筛,再精修。

![FRCNN 与 YOLOv3 损失拆分图](https://zoytown-sg.oss-ap-southeast-1.aliyuncs.com/blogs/frcnn-yolo-loss-breakdown-qvmt.svg)

## YOLOv3:把几件事一口气联合优化

看完 `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 组 anchor
- `5`:`x, y, w, h, confidence`
- `class_num`:类别数

### 网络输出是什么

实现里常见几个名字:

- `raw_pred`
- `pred_xy`
- `pred_wh`
- `grid`

可以粗略理解成:

- `raw_pred` 是网络原始输出
- `pred_xy` 是中心点位置相关结果
- `pred_wh` 是宽高相关结果
- `grid` 帮你把“相对网格偏移”还原到整张图的坐标语境里

### YOLOv3 的四块损失

`YOLOv3` 常见实现里,核心损失通常拆成四部分:

- `xy_loss`
- `wh_loss`
- `confidence_loss`
- `class_loss`

#### 1. `xy_loss`

这一项主要管目标中心点。

它通常会乘上两个很关键的权重:

- `object_mask`
- `box_loss_scale`

前者可以理解成“这个位置到底有没有目标”,后者常写成和框面积相关的权重,常见理解是 `2 - w * h` 这类形式。它的作用很实用:小目标别被轻轻带过,大目标也别一股脑吃掉所有权重。

所以这项在优化时,实际上不只是中心点坐标本身,还会和目标存在性、框尺度权重绑在一起考虑。

#### 2. `wh_loss`

这一项更像纯回归,但又不是完全裸回归。

它会让预测框的宽高往真实框靠,同时也受 `object_mask` 和框尺度权重影响。所以你会发现,`YOLOv3` 的损失设计非常喜欢“一个项不只做一件事”,而是让多个目标在同一套结构里互相牵制。

#### 3. `confidence_loss`

这一项很关键,因为它决定模型怎么看“这里到底有没有东西”。

除了正常的前景 / 背景二值交叉熵,还会有一个 `ignore_mask`。这个东西很像在说:

“有些位置虽然不是最终负责预测的那个框,但它也不完全该被粗暴打成背景,先别急着罚。”

这个设计和两阶段检测里常说的“中性样本”有点类似味道。它能减少一些不必要的误罚,让训练更稳。

#### 4. `class_loss`

这一项就相对直接了,就是在有目标的地方去做类别预测,常见写法是带 `object_mask` 的多标签 / 多分类交叉熵。

最后总损失就是把这几项加起来。

## 一段更顺手的 Python 理解版

如果你只是想把核心思路先摸熟,用下面这种“缩水版”写法会比直接啃工程代码舒服很多。

### 用 NumPy 看 `smooth L1`

```python
import 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` 里那类回归损失的核心形状。

### 用 Keras 风格理解 RPN 分类损失

```python
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` 挑出需要参与训练的 anchor
- 再只对这些位置算二分类交叉熵

### 用 PyTorch 写一个 YOLOv3 简化版框架

```python
import 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` 的损失确实就是几块任务一起推。

![对比图](https://zoytown-sg.oss-ap-southeast-1.aliyuncs.com/blogs/frcnn-yolo-loss-compare-qvmt.svg)

## 把两家的风格放一起看

聊到这里,差异其实已经很清楚了。

`Faster R-CNN` 更像一套分工明确的流程:

- 先做候选框
- 再做分类和精修
- 每一阶段的目标都很直接

`YOLOv3` 则更像把多个小任务捆在一起冲:

- 中心点要准
- 宽高要准
- 置信度要稳
- 类别也得一起跟上

如果把它们各自的气质说得更直白一点:

- `Faster R-CNN` 偏“精准优化”
- `YOLOv3` 偏“联合优化”

前者层层推进,后者整体联动。