You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在LightGBM中构建自定义多分类损失函数的技术咨询

F1赛事胜者预测:带正则项的LightGBM自定义多分类损失实现

问题概述

基于给定的F1赛事数据集,使用LightGBM实现多分类任务,以Win_odds_1~Win_odds_4为特征预测赛事胜者。需自定义包含对数似然项+正则项的损失函数(正则系数λ=1),其中$o_{i,k}$代表样本$i$对应类别$k$的Win_odds。当前核心难点:

  1. 无法推导并编码该损失的梯度与海森矩阵
  2. 目标为一维标签,LightGBM API无法直接向损失函数传递Win_odds这类额外数据

数据集与宽格式转换

长格式数据集

import pandas as pd
import numpy as np
import lightgbm as lgb

data = {
    "Race_ID": [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4],
    "Racer_Number": [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4],
    "Win_odds": [2.3, 4.3, 1.3, 5.0, 3.4, 1.1, 2.3, 2.0, 7.4, 3.1, 1.8, 6.0, 2.4, 5.2, 3.5, 3.0, 1.4, 3.2, 2.5, 5],
    "Rank": [3, 2, 1, 4, 2, 1, 4, 3, 4, 2, 1, 3, 4, 2, 3, 1, 1, 2, 3, 4]
}
df_long = pd.DataFrame(data)

转换为宽格式(单条记录对应一场赛事)

df_wide = df_long.pivot(
    index="Race_ID", 
    columns="Racer_Number", 
    values=["Win_odds","Rank"]
)
df_wide.columns = [f"{col[0]}_{col[1]}" for col in df_wide.columns]
df_wide = df_wide.reset_index()
# 目标变量:取Rank最小的车手索引(0~3对应车手1~4)
df_wide['target'] = df_wide.apply(lambda row: np.argmin(row['Rank_1':'Rank_4']) , axis=1)

自定义损失函数定义

损失函数的通用数学形式($N$为样本数,$K$为类别数,$y_i$为样本$i$的真实类别,$p_{i,k}$为模型预测的类别$k$概率):
$$
L = -\frac{1}{N}\sum_{i=1}^N \left[ \log(p_{i,y_i}) - \sum_{k=1}^K (p_{i,k} - \frac{1}{o_{i,k}})^2 \right]
$$
其中:

  • 第一项$\log(p_{i,y_i})$是标准多分类对数似然项,衡量预测概率与真实标签的匹配度
  • 第二项$\sum_{k=1}^K (p_{i,k} - \frac{1}{o_{i,k}})^2$是正则项,约束预测概率接近Win_odds的倒数(即隐含的胜率预期)

梯度与海森矩阵推导

LightGBM的自定义损失需要对每个样本的每个类别计算梯度(一阶导)和海森(二阶导)。模型输出为logit值,需先通过softmax转换为概率$p_{i,k} = \frac{e{z_{i,k}}}{\sum_{m=1}K e^{z_{i,m}}}$($z_{i,k}$为logit)。

梯度推导

对单个样本$i$的类别$j$,梯度为损失函数对$z_{i,j}$的一阶导:
$$
g_{i,j} = \mathbb{I}(y_i=j) - p_{i,j} + 2p_{i,j}\left[ p_{i,j}(1-2/o_{i,j}) + \sum_{k≠j} p_{i,k}/o_{i,k} - \sum_{k=1}^K p_{i,k}^2 \right]
$$
其中$\mathbb{I}(y_i=j)$是指示函数,真实类别为$j$时取1,否则取0。

海森矩阵推导

二阶导(海森)为:
$$
\begin{align*}
h_{i,j} &= -p_{i,j}(1-p_{i,j}) + 2\left[ (1-2p_{i,j})A + p_{i,j}(B - C) - 2p_{i,j}^2(1-p_{i,j}) \right] \
A &= p_{i,j}(1-2/o_{i,j}) + \sum_{k≠j} p_{i,k}/o_{i,k} - \sum_{k=1}^K p_{i,k}^2 \
B &= (1-2/o_{i,j})(1-2p_{i,j}) \
C &= \sum_{k=1}^K p_{i,k}/o_{i,k}
\end{align*}
$$

解决方案实现

1. 数据预处理(提取关键矩阵)

# 特征矩阵(Win_odds_1~4)
X = df_wide[['Win_odds_1', 'Win_odds_2', 'Win_odds_3', 'Win_odds_4']].values
# 目标标签(0~3)
y = df_wide['target'].values
# 预计算1/o_{i,k}矩阵,用于正则项
inv_odds_matrix = 1 / X

2. 闭包形式的自定义损失函数(解决额外数据传递问题)

通过闭包捕获inv_odds_matrix,让LightGBM的损失函数可以访问该额外数据:

def custom_multiclass_loss(inv_odds, lambda_reg=1):
    def loss(preds, train_data):
        labels = train_data.get_label()
        num_samples = preds.shape[0]
        num_classes = preds.shape[1]
        
        # 将logit转换为概率(softmax)
        exp_preds = np.exp(preds)
        p = exp_preds / np.sum(exp_preds, axis=1, keepdims=True)
        
        grad = np.zeros_like(preds)
        hess = np.zeros_like(preds)
        
        for i in range(num_samples):
            y_i = int(labels[i])
            p_i = p[i]
            inv_o_i = inv_odds[i]
            
            # 预计算全局项,减少重复计算
            sum_p_sq = np.sum(p_i ** 2)
            sum_p_inv_o = np.sum(p_i * inv_o_i)
            
            for j in range(num_classes):
                # 计算梯度
                indicator = 1 if j == y_i else 0
                term_a = indicator - p_i[j]
                term_b = p_i[j] * ( p_i[j]*(1 - 2*inv_o_i[j]) + (sum_p_inv_o - p_i[j]*inv_o_i[j]) - sum_p_sq )
                grad[i,j] = term_a + 2 * lambda_reg * term_b
                
                # 计算海森
                A = p_i[j]*(1 - 2*inv_o_i[j]) + (sum_p_inv_o - p_i[j]*inv_o_i[j]) - sum_p_sq
                B = (1 - 2*inv_o_i[j])*(1 - 2*p_i[j])
                C = sum_p_inv_o
                
                d_term_b = (1 - 2*p_i[j])*A + p_i[j]*(B - C) - 2*p_i[j]**2*(1 - p_i[j])
                hess[i,j] = -p_i[j]*(1 - p_i[j]) + 2 * lambda_reg * d_term_b
        
        return grad, hess
    return loss

3. 训练模型

# 创建LightGBM数据集
train_data = lgb.Dataset(X, label=y)

# 模型参数
params = {
    'objective': None,  # 禁用内置目标,使用自定义损失
    'metric': 'None',   # 禁用内置评估指标
    'num_class': 4,
    'boosting_type': 'gbdt',
    'learning_rate': 0.1,
    'verbose': 1
}

# 初始化自定义损失函数
my_loss = custom_multiclass_loss(inv_odds_matrix, lambda_reg=1)

# 训练模型
model = lgb.train(params, train_data, num_boost_round=100, fobj=my_loss)

关键注意事项

  • 闭包的使用:通过外层函数传递额外数据(如inv_odds_matrix),避免了LightGBM API无法直接传递参数的限制。
  • 计算效率:预计算sum_p_sq和sum_p_inv_o等全局项,减少循环内的重复计算,提升训练速度。
  • 多分类适配:LightGBM的多分类任务中,preds是(num_samples, num_classes)形状的数组,梯度和海森矩阵需保持相同形状。

内容的提问来源于stack exchange,提问作者Ishigami

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 09:23:10