""" focal.py — Focal loss objective and eval for LightGBM. Extracted from sniper_v7_1.py so SniperModel can import them without depending on __main__. """ import numpy as np from scipy.special import expit # These are read from Config at training time; we keep the same defaults. FOCAL_GAMMA = 2.0 FOCAL_ALPHA = 0.25 def focal_loss_objective(y_pred, dtrain): y_true = dtrain.get_label() gamma = FOCAL_GAMMA alpha = FOCAL_ALPHA p = np.clip(expit(y_pred), 1e-7, 1 - 1e-7) p_t = np.where(y_true == 1, p, 1 - p) alpha_t = np.where(y_true == 1, alpha, 1 - alpha) log_p_t = np.log(p_t + 1e-10) dp_t_dz = np.where(y_true == 1, p * (1 - p), -p * (1 - p)) term1 = -gamma * log_p_t term2 = (1 - p_t) / (p_t + 1e-10) grad = -alpha_t * dp_t_dz * ((1 - p_t) ** (gamma - 1)) * (term1 + term2) abs_grad = np.abs(grad) hess = np.maximum(abs_grad * (2.0 - abs_grad), 1e-5) return grad, hess def focal_loss_eval(y_pred, dtrain): y_true = dtrain.get_label() p = np.clip(expit(y_pred), 1e-7, 1 - 1e-7) p_t = np.where(y_true == 1, p, 1 - p) alpha_t = np.where(y_true == 1, FOCAL_ALPHA, 1 - FOCAL_ALPHA) loss = -alpha_t * (1 - p_t) ** FOCAL_GAMMA * np.log(p_t + 1e-10) return "focal_loss", float(np.mean(loss)), False