hscredit.core.models.losses.ranking_loss 源代码

"""
排序与头部效果导向损失函数

针对评分排序一致性与头部LIFT优化场景设计的损失函数。
"""

from __future__ import annotations

from typing import Tuple

import numpy as np

from .base import BaseLoss


[文档] class OrdinalRankLoss(BaseLoss): """序数排序损失,兼顾概率拟合与好坏样本排序一致性。 该损失在标准二元交叉熵基础上增加成对排序惩罚项, 鼓励坏样本(label=1)的预测风险高于好样本(label=0)。 :param rank_weight: 排序惩罚项权重,默认 1.0 :param bce_weight: 交叉熵权重,默认 1.0 :param temperature: 排序平滑温度,越小越强调排序间隔,默认 1.0 :param max_pairs: 为控制计算开销,最多采样的正负样本对数,默认 20000 :param random_state: 随机种子,保证采样对可复现,默认 42 :param name: 损失函数名称,默认 "ordinal_rank_loss" **参考样例** >>> import numpy as np >>> from hscredit.core.models.losses import OrdinalRankLoss >>> loss = OrdinalRankLoss(rank_weight=2.0, bce_weight=1.0) >>> y_true = np.array([0, 0, 1, 1]) >>> y_pred = np.array([0.1, 0.3, 0.7, 0.9]) >>> round(loss(y_true, y_pred), 6) >= 0 True **引用** 成对排序(pairwise ranking)优化与 AUC 的等价性见 Burges, C. et al. (2005). *Learning to Rank using Gradient Descent (RankNet).* ICML 2005, https://www.microsoft.com/en-us/research/publication/learning-to-rank-using-gradient-descent/ ; AUC 与 Wilcoxon–Mann–Whitney 统计量的关系见 Hanley & McNeil (1982)。 """ def __init__( self, rank_weight: float = 1.0, bce_weight: float = 1.0, temperature: float = 1.0, max_pairs: int = 20000, random_state: int = 42, name: str = "ordinal_rank_loss", ): super().__init__(name) self.rank_weight = rank_weight self.bce_weight = bce_weight self.temperature = temperature self.max_pairs = max_pairs self.random_state = random_state def _prepare_pairs( self, y_true: np.ndarray, ) -> Tuple[np.ndarray, np.ndarray]: pos_idx = np.flatnonzero(y_true == 1) neg_idx = np.flatnonzero(y_true == 0) if len(pos_idx) == 0 or len(neg_idx) == 0: return np.array([], dtype=int), np.array([], dtype=int) pos_grid = np.repeat(pos_idx, len(neg_idx)) neg_grid = np.tile(neg_idx, len(pos_idx)) if len(pos_grid) <= self.max_pairs: return pos_grid, neg_grid rng = np.random.default_rng(self.random_state) chosen = rng.choice(len(pos_grid), size=self.max_pairs, replace=False) return pos_grid[chosen], neg_grid[chosen] def _pairwise_rank_loss( self, y_true: np.ndarray, y_pred: np.ndarray, ) -> float: pos_pairs, neg_pairs = self._prepare_pairs(y_true) if len(pos_pairs) == 0: return 0.0 diff = (y_pred[pos_pairs] - y_pred[neg_pairs]) / self.temperature return float(np.mean(np.log1p(np.exp(-diff)))) def __call__( self, y_true: np.ndarray, y_pred: np.ndarray, ) -> float: y_true = np.asarray(y_true, dtype=float) y_pred = np.clip(np.asarray(y_pred, dtype=float), 1e-7, 1 - 1e-7) bce = -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)) rank = self._pairwise_rank_loss(y_true, y_pred) return float(self.bce_weight * bce + self.rank_weight * rank)
[文档] def gradient( self, y_true: np.ndarray, y_pred: np.ndarray, ) -> np.ndarray: y_true = np.asarray(y_true, dtype=float) y_pred = np.clip(np.asarray(y_pred, dtype=float), 1e-7, 1 - 1e-7) grad = self.bce_weight * (y_pred - y_true) pos_pairs, neg_pairs = self._prepare_pairs(y_true) if len(pos_pairs) == 0 or self.rank_weight == 0: return grad diff = (y_pred[pos_pairs] - y_pred[neg_pairs]) / self.temperature pair_grad = -1.0 / (1.0 + np.exp(diff)) pair_grad = (self.rank_weight / len(pos_pairs)) * (pair_grad / self.temperature) np.add.at(grad, pos_pairs, pair_grad) np.add.at(grad, neg_pairs, -pair_grad) 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 = np.clip(np.asarray(y_pred, dtype=float), 1e-7, 1 - 1e-7) hess = self.bce_weight * y_pred * (1 - y_pred) pos_pairs, neg_pairs = self._prepare_pairs(y_true) if len(pos_pairs) > 0 and self.rank_weight != 0: diff = (y_pred[pos_pairs] - y_pred[neg_pairs]) / self.temperature sig = 1.0 / (1.0 + np.exp(-diff)) pair_hess = sig * (1 - sig) pair_hess = (self.rank_weight / len(pos_pairs)) * (pair_hess / (self.temperature ** 2)) np.add.at(hess, pos_pairs, pair_hess) np.add.at(hess, neg_pairs, pair_hess) return np.maximum(hess, 1e-6)
[文档] class LiftFocusedLoss(BaseLoss): """头部 LIFT 导向损失,对高风险区间样本错误施加更大惩罚。 该损失基于加权二元交叉熵,按照预测风险从高到低分配更大的样本权重, 并在头部区间进一步放大坏样本的惩罚,提升模型在高风险头部样本上的区分能力。 :param top_ratio: 头部样本占比,默认 0.10 :param penalty_factor: 头部惩罚倍数,默认 3.0 :param positive_class_boost: 头部坏样本额外增益倍数,默认 1.5 :param base_weight: 非头部样本基础权重,默认 1.0 :param name: 损失函数名称,默认 "lift_focused_loss" Example: >>> import numpy as np >>> from hscredit.core.models.losses import LiftFocusedLoss >>> loss = LiftFocusedLoss(top_ratio=0.2, penalty_factor=4.0) >>> y_true = np.array([0, 0, 1, 1]) >>> y_pred = np.array([0.1, 0.4, 0.7, 0.9]) >>> round(loss(y_true, y_pred), 6) >= 0 True """ def __init__( self, top_ratio: float = 0.10, penalty_factor: float = 3.0, positive_class_boost: float = 1.5, base_weight: float = 1.0, name: str = "lift_focused_loss", ): super().__init__(name) self.top_ratio = top_ratio self.penalty_factor = penalty_factor self.positive_class_boost = positive_class_boost self.base_weight = base_weight def _get_sample_weights( self, y_true: np.ndarray, y_pred: np.ndarray, ) -> np.ndarray: n_samples = len(y_pred) if n_samples == 0: return np.array([], dtype=float) order = np.argsort(-y_pred) ranks = np.empty_like(order) ranks[order] = np.arange(n_samples) head_count = max(1, int(np.ceil(n_samples * self.top_ratio))) top_mask = ranks < head_count weights = np.full(n_samples, self.base_weight, dtype=float) weights[top_mask] = self.base_weight * self.penalty_factor positive_top_mask = top_mask & (y_true == 1) weights[positive_top_mask] *= self.positive_class_boost return weights def __call__( self, y_true: np.ndarray, y_pred: np.ndarray, ) -> float: y_true = np.asarray(y_true, dtype=float) y_pred = np.clip(np.asarray(y_pred, dtype=float), 1e-7, 1 - 1e-7) weights = self._get_sample_weights(y_true, y_pred) loss = -(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)) return float(np.average(loss, weights=weights))
[文档] def gradient( self, y_true: np.ndarray, y_pred: np.ndarray, ) -> np.ndarray: y_true = np.asarray(y_true, dtype=float) y_pred = np.clip(np.asarray(y_pred, dtype=float), 1e-7, 1 - 1e-7) weights = self._get_sample_weights(y_true, y_pred) return weights * (y_pred - y_true)
[文档] def hessian( self, y_true: np.ndarray, y_pred: np.ndarray, ) -> np.ndarray: y_true = np.asarray(y_true, dtype=float) y_pred = np.clip(np.asarray(y_pred, dtype=float), 1e-7, 1 - 1e-7) weights = self._get_sample_weights(y_true, y_pred) hess = weights * y_pred * (1 - y_pred) return np.maximum(hess, 1e-6)