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

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

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

from __future__ import annotations

from typing import Tuple

import numpy as np

from .base import BaseLoss
from ._loss_math import binary_inputs, bce_terms, nonnegative, positive, unit_interval


[文档] class OrdinalRankLoss(BaseLoss): """序数排序损失,兼顾概率拟合与好坏样本排序一致性。 该损失在标准二元交叉熵基础上增加成对排序惩罚项, 鼓励坏样本(label=1)的预测风险高于好样本(label=0)。 导数按 n × 平均损失计算,返回 Hessian 的对角项;框架不使用跨样本二阶项。 :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) nonnegative(rank_weight=rank_weight, bce_weight=bce_weight) positive(temperature=temperature, max_pairs=max_pairs) if not isinstance(max_pairs, (int, np.integer)): raise ValueError("max_pairs 必须为正整数。") 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): """直接采样组合编号,避免在 max_pairs 截断前构造平方级数组。""" pos_idx = np.flatnonzero(y_true == 1) neg_idx = np.flatnonzero(y_true == 0) total = len(pos_idx) * len(neg_idx) if not total: return np.array([], dtype=int), np.array([], dtype=int) if total <= self.max_pairs: indices = np.arange(total) else: rng = np.random.default_rng(self.random_state) indices = rng.choice(total, size=self.max_pairs, replace=False) return pos_idx[indices // len(neg_idx)], neg_idx[indices % len(neg_idx)] 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.logaddexp(0.0, -diff))) def __call__( self, y_true: np.ndarray, y_pred: np.ndarray, ) -> float: """计算本损失的平均值,越小越好。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: float;原始损失值,不根据调参方向改变符号。 **参考样例** >>> from hscredit.core.models.losses import OrdinalRankLoss >>> loss = OrdinalRankLoss() >>> result = loss([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y_true, y_pred = binary_inputs(y_true, y_pred) 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: """返回损失相对坏样本概率的一阶导数。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的一阶导数组,标度为样本数乘以平均损失;详见 BaseLoss.gradient。 **参考样例** >>> from hscredit.core.models.losses import OrdinalRankLoss >>> loss = OrdinalRankLoss() >>> result = loss.gradient([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y_true, y_pred = binary_inputs(y_true, y_pred) grad = self.bce_weight * bce_terms(y_true, y_pred)[1] 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 = -np.exp(-np.logaddexp(0.0, diff)) pair_grad = (len(y_true) * 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: """返回损失相对坏样本概率的二阶导数。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的二阶导数组,标度同 BaseLoss.hessian;框架适配器再做链接函数转换。 **参考样例** >>> from hscredit.core.models.losses import OrdinalRankLoss >>> loss = OrdinalRankLoss() >>> result = loss.hessian([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y_true, y_pred = binary_inputs(y_true, y_pred) hess = self.bce_weight * bce_terms(y_true, y_pred)[2] 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 = np.exp(-np.logaddexp(0.0, -diff)) pair_hess = sig * (1 - sig) pair_hess = (len(y_true) * 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 hess
[文档] 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) unit_interval(top_ratio=top_ratio) positive( top_ratio=top_ratio, penalty_factor=penalty_factor, positive_class_boost=positive_class_boost, base_weight=base_weight, ) 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: """计算本损失的平均值,越小越好。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: float;原始损失值,不根据调参方向改变符号。 **参考样例** >>> from hscredit.core.models.losses import LiftFocusedLoss >>> loss = LiftFocusedLoss() >>> result = loss([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y_true, y_pred = binary_inputs(y_true, y_pred) 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: """返回损失相对坏样本概率的一阶导数。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的一阶导数组,标度为样本数乘以平均损失;详见 BaseLoss.gradient。 **参考样例** >>> from hscredit.core.models.losses import LiftFocusedLoss >>> loss = LiftFocusedLoss() >>> result = loss.gradient([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y_true, y_pred = binary_inputs(y_true, y_pred) weights = self._get_sample_weights(y_true, y_pred) return (weights / np.mean(weights)) * bce_terms(y_true, y_pred)[1]
[文档] def hessian( self, y_true: np.ndarray, y_pred: np.ndarray, ) -> np.ndarray: """返回损失相对坏样本概率的二阶导数。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的二阶导数组,标度同 BaseLoss.hessian;框架适配器再做链接函数转换。 **参考样例** >>> from hscredit.core.models.losses import LiftFocusedLoss >>> loss = LiftFocusedLoss() >>> result = loss.hessian([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ y_true, y_pred = binary_inputs(y_true, y_pred) weights = self._get_sample_weights(y_true, y_pred) hess = (weights / np.mean(weights)) * bce_terms(y_true, y_pred)[2] return hess