"""
不对称Focal Loss
针对风控极度不平衡数据,分别控制正负样本的聚焦强度。
"""
from __future__ import annotations
import numpy as np
from .base import BaseLoss
[文档]
class AsymmetricFocalLoss(BaseLoss):
"""不对称 Focal Loss。
与标准 Focal Loss 不同,该损失允许对正负样本使用不同的聚焦参数,
从而更灵活地强调坏样本识别或抑制易分类好样本的影响。
数学形式:
- 正样本: -alpha * (1 - p)^gamma_pos * log(p)
- 负样本: -(1 - alpha) * p^gamma_neg * log(1 - p)
:param alpha: 正样本权重,默认 0.25
:param gamma_pos: 正样本聚焦参数,默认 2.0
:param gamma_neg: 负样本聚焦参数,默认 1.0
:param clip_value: 对负样本概率进行裁剪,抑制极端易分类负样本影响,默认 0.0
:param name: 损失函数名称,默认 "asymmetric_focal_loss"
**参考样例**
>>> import numpy as np
>>> from hscredit.core.models.losses import AsymmetricFocalLoss
>>> loss = AsymmetricFocalLoss(alpha=0.7, gamma_pos=2.5, gamma_neg=1.0)
>>> y_true = np.array([0, 0, 1, 1])
>>> y_pred = np.array([0.1, 0.4, 0.6, 0.9])
>>> round(loss(y_true, y_pred), 6) >= 0
True
**引用**
在 Focal Loss(Lin et al., 2017, https://arxiv.org/abs/1708.02002)基础上对正负样本
采用不同聚焦参数,思想与非对称损失 Ben-Baruch, E. et al. (2021). *Asymmetric Loss for
Multi-Label Classification* 相通(https://arxiv.org/abs/2009.14119)。
"""
def __init__(
self,
alpha: float = 0.25,
gamma_pos: float = 2.0,
gamma_neg: float = 1.0,
clip_value: float = 0.0,
name: str = "asymmetric_focal_loss",
):
super().__init__(name)
self.alpha = alpha
self.gamma_pos = gamma_pos
self.gamma_neg = gamma_neg
self.clip_value = clip_value
def _clip_probabilities(self, y_pred: np.ndarray) -> np.ndarray:
y_pred = np.clip(np.asarray(y_pred, dtype=float), 1e-7, 1 - 1e-7)
if self.clip_value > 0:
y_pred = np.minimum(y_pred + self.clip_value, 1 - 1e-7)
return y_pred
def __call__(self, y_true: np.ndarray, y_pred: np.ndarray) -> float:
y_true = np.asarray(y_true, dtype=float)
y_pred = self._clip_probabilities(y_pred)
pos_loss = -self.alpha * y_true * ((1 - y_pred) ** self.gamma_pos) * np.log(y_pred)
neg_loss = -(1 - self.alpha) * (1 - y_true) * (y_pred ** self.gamma_neg) * np.log(1 - y_pred)
return float(np.mean(pos_loss + neg_loss))
[文档]
def gradient(self, y_true: np.ndarray, y_pred: np.ndarray) -> np.ndarray:
y_true = np.asarray(y_true, dtype=float)
y_pred = self._clip_probabilities(y_pred)
grad = np.zeros_like(y_pred, dtype=float)
pos_mask = y_true == 1
if np.any(pos_mask):
p = y_pred[pos_mask]
grad[pos_mask] = self.alpha * (
self.gamma_pos * (1 - p) ** (self.gamma_pos - 1) * np.log(p)
- ((1 - p) ** self.gamma_pos) / p
)
neg_mask = y_true == 0
if np.any(neg_mask):
p = y_pred[neg_mask]
grad[neg_mask] = (1 - self.alpha) * (
-self.gamma_neg * (p ** (self.gamma_neg - 1)) * np.log(1 - p)
+ (p ** self.gamma_neg) / (1 - p)
)
return grad
[文档]
def hessian(self, y_true: np.ndarray, y_pred: np.ndarray) -> np.ndarray:
y_true = np.asarray(y_true, dtype=float)
y_pred = self._clip_probabilities(y_pred)
hess = np.zeros_like(y_pred, dtype=float)
pos_mask = y_true == 1
if np.any(pos_mask):
p = y_pred[pos_mask]
hess[pos_mask] = self.alpha * (
self.gamma_pos * (self.gamma_pos - 1) * (1 - p) ** (self.gamma_pos - 2) * np.log(p)
+ 2 * self.gamma_pos * (1 - p) ** (self.gamma_pos - 1) / p
+ (1 - p) ** self.gamma_pos / (p ** 2)
)
neg_mask = y_true == 0
if np.any(neg_mask):
p = y_pred[neg_mask]
hess[neg_mask] = (1 - self.alpha) * (
self.gamma_neg * (self.gamma_neg - 1) * p ** (self.gamma_neg - 2) * np.log(1 - p)
+ 2 * self.gamma_neg * p ** (self.gamma_neg - 1) / (1 - p)
+ p ** self.gamma_neg / ((1 - p) ** 2)
)
return np.abs(hess) + 1e-6