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

"""
加权损失函数和成本敏感损失

提供多种加权损失函数,用于处理类别不平衡和成本敏感学习场景。
"""

import numpy as np
from typing import Optional, Union
from .base import BaseLoss
from ._loss_math import binary_inputs, bce_terms, focal_terms, nonnegative, positive, unit_interval


[文档] class WeightedBCELoss(BaseLoss): """加权二元交叉熵损失,通过为正负样本分配不同权重来处理类别不平衡问题。 数学公式: Loss = -[w_pos * y * log(p) + w_neg * (1-y) * log(1-p)] :param pos_weight: 正样本权重,默认为1.0 :param neg_weight: 负样本权重,默认为1.0 :param auto_balance: 是否自动根据样本比例平衡权重,默认为False。如果为True,pos_weight和neg_weight将被忽略 :param name: 损失函数名称,默认为"weighted_bce" **参考样例** >>> import numpy as np >>> from hscredit.core.models.losses import WeightedBCELoss >>> >>> # 手动设置权重 >>> loss = WeightedBCELoss(pos_weight=5.0, neg_weight=1.0) >>> >>> # 自动平衡权重 >>> loss = WeightedBCELoss(auto_balance=True) >>> # 假设正样本占比10%,自动设置pos_weight=9, neg_weight=1 >>> >>> # 同一目标的离线评估:越小越好 >>> value = loss.metric()(y_valid, p_valid) >>> # 在LightGBM 4.x 原生接口中使用 >>> import lightgbm as lgb >>> train_data = lgb.Dataset(X_train, label=y_train) >>> bst = lgb.train( ... params={'objective': loss.to_lightgbm(api='native'), 'metric': 'None'}, ... train_set=train_data, ... feval=loss.metric().to_lightgbm(api='native', raw_score=True), ... num_boost_round=100 ... ) **引用** 类别加权(class weighting)是处理不平衡数据的经典成本敏感方法,参见 King, G., & Zeng, L. (2001). *Logistic Regression in Rare Events Data.* Political Analysis 9(2), 以及 Elkan, C. (2001). *The Foundations of Cost-Sensitive Learning.* IJCAI 2001。 """ def __init__( self, pos_weight: float = 1.0, neg_weight: float = 1.0, auto_balance: bool = False, name: str = "weighted_bce" ): super().__init__(name) nonnegative(pos_weight=pos_weight, neg_weight=neg_weight) positive(权重总和=pos_weight + neg_weight) self.is_additive = not auto_balance self.pos_weight = pos_weight self.neg_weight = neg_weight self.auto_balance = auto_balance self._fitted_weights = None def _auto_balance_weights(self, y_true: np.ndarray): """根据样本比例自动平衡权重。""" if not self.auto_balance: return n_pos = np.sum(y_true == 1) n_neg = np.sum(y_true == 0) if n_pos == 0 or n_neg == 0: self.pos_weight = self.neg_weight = 1.0 return # 权重与样本数量成反比 self.pos_weight = n_neg / n_pos self.neg_weight = 1.0 def __call__(self, y_true, y_pred) -> float: """计算本损失的平均值,越小越好。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: float;原始损失值,不根据调参方向改变符号。 **参考样例** >>> from hscredit.core.models.losses import WeightedBCELoss >>> loss = WeightedBCELoss() >>> result = loss([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ return float(np.mean(self.loss_values(y_true, y_pred)))
[文档] def gradient(self, y_true, y_pred): """返回损失相对坏样本概率的一阶导数。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的一阶导数组,标度为样本数乘以平均损失;详见 BaseLoss.gradient。 **参考样例** >>> from hscredit.core.models.losses import WeightedBCELoss >>> loss = WeightedBCELoss() >>> result = loss.gradient([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ return self._terms(y_true, y_pred)[1]
[文档] def hessian(self, y_true, y_pred): """返回损失相对坏样本概率的二阶导数。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的二阶导数组,标度同 BaseLoss.hessian;框架适配器再做链接函数转换。 **参考样例** >>> from hscredit.core.models.losses import WeightedBCELoss >>> loss = WeightedBCELoss() >>> result = loss.hessian([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ return self._terms(y_true, y_pred)[2]
def _terms(self, y_true, y_pred): y, p = binary_inputs(y_true, y_pred) self._auto_balance_weights(y) weights = np.where(y == 1, self.pos_weight, self.neg_weight) return tuple(weights * term for term in bce_terms(y, p))
[文档] def loss_values(self, y_true, y_pred): """逐样本类别加权 BCE;自动权重按当前数据集重新计算。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的数组,其均值等于本损失值;详见 BaseLoss.loss_values。 **参考样例** >>> from hscredit.core.models.losses import WeightedBCELoss >>> loss = WeightedBCELoss() >>> result = loss.loss_values([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ return self._terms(y_true, y_pred)[0]
[文档] class CostSensitiveLoss(BaseLoss): """成本敏感损失函数,根据不同预测错误的成本为不同类型的错误分配不同的权重。 特别适合金融风控场景,因为漏抓坏客户的成本往往远大于误拒好客户的成本。 :param fn_cost: 假阴性成本(漏抓坏客户的成本),默认为1.0 :param fp_cost: 假阳性成本(误拒好客户的成本),默认为1.0 :param name: 损失函数名称,默认为"cost_sensitive" **参考样例** >>> from hscredit.core.models.losses import CostSensitiveLoss >>> >>> # 假设漏抓一个坏客户损失10000元,误拒一个好客户损失100元 >>> # 成本比例为100:1 >>> loss = CostSensitiveLoss(fn_cost=100, fp_cost=1) >>> >>> # 在模型中使用 >>> import xgboost as xgb >>> dtrain = xgb.DMatrix(X_train, label=y_train) >>> params = {'objective': 'binary:logistic'} >>> bst = xgb.train(params, dtrain, obj=loss.to_xgboost(), num_boost_round=100) **注意** 损失矩阵:: 预测负 预测正 实际负 0 fp_cost 实际正 fn_cost 0 我们希望最小化总成本: FP * fp_cost + FN * fn_cost """ def __init__(self, fn_cost: float = 1.0, fp_cost: float = 1.0, name: str = "cost_sensitive"): super().__init__(name) nonnegative(fn_cost=fn_cost, fp_cost=fp_cost) positive(成本总和=fn_cost + fp_cost) self.is_additive = True self.fn_cost = fn_cost # 假阴性成本(漏抓) self.fp_cost = fp_cost # 假阳性成本(误拒) def __call__(self, y_true, y_pred, threshold=None) -> float: """默认返回可优化的成本加权 BCE;显式 threshold 返回历史硬分类成本。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :param threshold: 默认 None 使用可微 BCE 代理;显式阈值返回历史硬分类成本,范围 [0, 1]。 :return: float;原始损失值,不根据调参方向改变符号。 **参考样例** >>> from hscredit.core.models.losses import CostSensitiveLoss >>> loss = CostSensitiveLoss() >>> result = loss([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ if threshold is not None: return self.classification_cost(y_true, y_pred, threshold) return float(np.mean(self.loss_values(y_true, y_pred)))
[文档] def gradient(self, y_true, y_pred): """返回损失相对坏样本概率的一阶导数。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的一阶导数组,标度为样本数乘以平均损失;详见 BaseLoss.gradient。 **参考样例** >>> from hscredit.core.models.losses import CostSensitiveLoss >>> loss = CostSensitiveLoss() >>> result = loss.gradient([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ return self._terms(y_true, y_pred)[1]
[文档] def hessian(self, y_true, y_pred): """返回损失相对坏样本概率的二阶导数。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的二阶导数组,标度同 BaseLoss.hessian;框架适配器再做链接函数转换。 **参考样例** >>> from hscredit.core.models.losses import CostSensitiveLoss >>> loss = CostSensitiveLoss() >>> result = loss.hessian([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ return self._terms(y_true, y_pred)[2]
def _terms(self, y_true, y_pred): y, p = binary_inputs(y_true, y_pred) weights = np.where(y == 1, self.fn_cost, self.fp_cost) return tuple(weights * term for term in bce_terms(y, p))
[文档] def classification_cost(self, y_true, y_pred, threshold=0.5) -> float: """计算固定阈值下的实际人均误判成本,越小越好。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状的坏样本概率。 :param threshold: 拒绝阈值,范围 [0, 1],默认 0.5;p >= threshold 拒绝。 :return: float,误拒好客户成本和漏抓坏客户成本之和除以全量人数。 这是硬决策评价,不作为可微训练目标。 >>> cost = CostSensitiveLoss(fn_cost=100, fp_cost=1).classification_cost( ... [0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9], threshold=0.5) """ y, p = binary_inputs(y_true, y_pred) p = np.asarray(y_pred, dtype=float) unit_interval(threshold=threshold) return float(np.mean((y == 0) * (p >= threshold) * self.fp_cost + (y == 1) * (p < threshold) * self.fn_cost))
[文档] def loss_values(self, y_true, y_pred): """成本加权 BCE 的逐样本贡献,其均值与默认调用一致。 :param y_true: 一维 0/1 标签,1 为坏样本。 :param y_pred: 同形状坏样本概率,范围 [0, 1];不是原始分数。 :return: 与输入等长的数组,其均值等于本损失值;详见 BaseLoss.loss_values。 **参考样例** >>> from hscredit.core.models.losses import CostSensitiveLoss >>> loss = CostSensitiveLoss() >>> result = loss.loss_values([0, 0, 1, 1], [0.1, 0.3, 0.7, 0.9]) """ return self._terms(y_true, y_pred)[0]