LightGBM分箱算法

等距分箱与等频分箱

在深度学习中,通常需要对连续特征进行离散化处理,这样可以使用嵌入向量表示特征。离散化处理的方法,常见的有等距分箱和等频分箱。

等距分箱的缺点是,数据容易集中在某个区间内,导致编号基本相同,丢失大量信息。并且等距分箱通常需要一定的专家知识。

等频分箱的优点是,当数据集中在某个区间内时也不会编号完全相同,克服了等距分箱的这个缺点。而等频分箱的缺点是,当大量数据取值相同时,比如都为0,那么等频分箱就会将这些取值相同的数据标记不同的编号,导致数据异常。

LightGBM分箱算法

LightGBM使用的分箱方法克服了以上所有缺点,在此推荐一下。主要特点如下:
1、由于数据集中经常会出现零值,所以零值单独使用一个箱子。
2、相同特征值的数据一定在一个箱子中。
3、采用动态平衡机制,各个箱子中的数据量基本一致。
所以,lightgmb的分箱方法可以看作是一个等频分箱的方法,但是采用了动态平衡机制,各个箱子的数据量基本相同但又不完全相同,克服了传统等频分箱的缺点。

我参考了Lightgbm 直方图优化算法深入理解这篇博文,将其中分箱代码使用python重新实现,供大家使用。

实现代码

GreedyFindBin

#########################得到数值型特征取值的各个bin的切分点################################
def GreedyFindBin(distinct_values, counts,num_distinct_values, max_bin, total_cnt, min_data_in_bin=3):
#INPUT:
#   distinct_values 保存特征取值的数组,特征取值单调递增
#   counts 特征的取值对应的样本数目
#   num_distinct_values 特征取值的数量
#   max_bin 分桶的最大数量
#   total_cnt 样本数量
#   min_data_in_bin 桶包含的最小样本数

# bin_upper_bound就是记录桶分界的数组
    bin_upper_bound=list();
    assert(max_bin>0)
    
    # 特征取值数比max_bin数量少,直接取distinct_values的中点放置
    if num_distinct_values <= max_bin:
        cur_cnt_inbin = 0
        for i in range(num_distinct_values-1):
            cur_cnt_inbin += counts[i]
            #若一个特征的取值比min_data_in_bin小,则累积下一个取值,直到比min_data_in_bin大,进入循环。
            if cur_cnt_inbin >= min_data_in_bin:
                #取当前值和下一个值的均值作为该桶的分界点bin_upper_bound
                bin_upper_bound.append((distinct_values[i] + distinct_values[i + 1]) / 2.0)
                cur_cnt_inbin = 0
        # 对于最后一个桶的上界则为无穷大
        cur_cnt_inbin += counts[num_distinct_values - 1];
        bin_upper_bound.append(float('Inf'))
        # 特征取值数比max_bin来得大,说明几个特征值要共用一个bin
    else:
        if min_data_in_bin>0:
            max_bin=min(max_bin,total_cnt//min_data_in_bin)
            max_bin=max(max_bin,1)
        #mean size for one bin
        mean_bin_size=total_cnt/max_bin
        rest_bin_cnt = max_bin
        rest_sample_cnt = total_cnt
        #定义is_big_count_value数组:初始设定特征每一个不同的值的数量都小(false)
        is_big_count_value=[False]*num_distinct_values
        #如果一个特征值的数目比mean_bin_size大,那么这些特征需要单独一个bin
        for i in range(num_distinct_values):
        #如果一个特征值的数目比mean_bin_size大,则设定这个特征值对应的is_big_count_value为真。。
            if counts[i] >= mean_bin_size:
                is_big_count_value[i] = True
                rest_bin_cnt-=1
                rest_sample_cnt -= counts[i]
        #剩下的特征取值的样本数平均每个剩下的bin:mean size for one bin
        mean_bin_size = rest_sample_cnt/rest_bin_cnt
        upper_bounds=[float('Inf')]*max_bin
        lower_bounds=[float('Inf')]*max_bin
        
        bin_cnt = 0
        lower_bounds[bin_cnt] = distinct_values[0]        
        cur_cnt_inbin = 0
        #重新遍历所有的特征值(包括数目大和数目小的)
        for i in range(num_distinct_values-1):
            #如果当前的特征值数目是小的
            if not is_big_count_value[i]:
                rest_sample_cnt -= counts[i]        
            cur_cnt_inbin += counts[i]
            
            # 若cur_cnt_inbin太少,则累积下一个取值,直到满足条件,进入循环。
            # need a new bin 当前的特征如果是需要单独成一个bin,或者当前几个特征计数超过了mean_bin_size,或者下一个是需要独立成桶的
            if is_big_count_value[i] or cur_cnt_inbin >= mean_bin_size or \
            is_big_count_value[i + 1] and cur_cnt_inbin >= max(1.0, mean_bin_size * 0.5):
                upper_bounds[bin_cnt] = distinct_values[i] # 第i个bin的最大就是 distinct_values[i]了
                bin_cnt+=1
                lower_bounds[bin_cnt] = distinct_values[i + 1] # 下一个bin的最小就是distinct_values[i + 1],注意先++bin了
                if bin_cnt >= max_bin - 1:
                    break
                cur_cnt_inbin = 0
                if not is_big_count_value[i]:
                    rest_bin_cnt-=1
                    mean_bin_size = rest_sample_cnt / rest_bin_cnt
	bin_cnt+=1
        # update bin upper bound 与特征取值数比max_bin数量少的操作类似,取当前值和下一个值的均值作为该桶的分界点
        for i in range(bin_cnt-1):
            bin_upper_bound.append((upper_bounds[i] + lower_bounds[i + 1]) / 2.0)
        bin_upper_bound.append(float('Inf'))
    return bin_upper_bound

FindBinWithZeroAsOneBin

def FindBinWithZeroAsOneBin(distinct_values, counts,num_distinct_values, max_bin, total_cnt, min_data_in_bin=3):
#INPUT:
#   distinct_values 保存特征取值的数组,特征取值单调递增
#   counts 特征的取值对应的样本数目
#   num_distinct_values 特征取值的数量
#   max_bin 分桶的最大数量
#   total_cnt 样本数量
#   min_data_in_bin 桶包含的最小样本数

# bin_upper_bound就是记录桶分界的数组
    bin_upper_bound=list()
    assert(max_bin>0)
    # left_cnt_data记录小于0的值
    left_cnt_data = 0
    cnt_zero = 0
    # right_cnt_data记录大于0的值
    right_cnt_data = 0
    kZeroThreshold = 1e-35
    for i in range(num_distinct_values):
        if distinct_values[i] <= -kZeroThreshold:
            left_cnt_data += counts[i]
        elif distinct_values[i] > kZeroThreshold:
            right_cnt_data += counts[i]
        else:
            cnt_zero += counts[i]
    # 如果特征值里存在0和正数,则left_cnt不为-1,left_cnt-1是最后一个负数的位置
    # left_cnt实际上就是负值的个数
    left_cnt = -1
    for i in range(num_distinct_values):
        if distinct_values[i] > -kZeroThreshold:
            left_cnt = i
            break
    # 如果特征值全是负值,left_cnt = num_distinct_values
    if left_cnt < 0:
      left_cnt = num_distinct_values
    if left_cnt > 0:
      # 负数除以(正数+负数)的比例,即负数的桶数。-1的1就是0的桶。
      left_max_bin = int( left_cnt_data/ (total_cnt - cnt_zero) * (max_bin - 1) )
      left_max_bin = max(1, left_max_bin)
      bin_upper_bound = GreedyFindBin(distinct_values, counts, left_cnt, left_max_bin, left_cnt_data, min_data_in_bin)
      bin_upper_bound[-1] = -kZeroThreshold
#如果特征值存在正数,则right_start不为-1,则right_start是第一个正数开始的位置
    right_start = -1
    for i in range(left_cnt, num_distinct_values):
        if distinct_values[i] > kZeroThreshold:
            right_start = i
            break
    # 如果特征值里存在正数
    if right_start >= 0:
        right_max_bin = max_bin - 1 - len(bin_upper_bound)
        assert(right_max_bin>0)
        right_bounds = GreedyFindBin(distinct_values[right_start:], counts[right_start:],
        num_distinct_values - right_start, right_max_bin, right_cnt_data, min_data_in_bin)
      # 正数桶的分界点第一个自然是kZeroThreshold,拼接到了-kZeroThreshold后面。
        bin_upper_bound.append(kZeroThreshold)
      # 插入正数桶的分界点,形成最终的分界点数组。
        bin_upper_bound+=right_bounds
    else:
        bin_upper_bound.append(float('Inf'))
    # bin_upper_bound即数值型特征取值(负数,0,正数)的各个bin的切分点
    return bin_upper_bound

     


GetBins

def GetBins(df,col_names, max_bin, min_data_in_bin=3):
    bins={}
    def _count(arr):
        distinct_values=[arr[0]]
        counts=[]
        counts_dict={arr[0]:1}
        for i in range(1,len(arr)):
            if arr[i]==arr[i-1]:
                counts_dict[arr[i]]+=1
            else:
                distinct_values.append(arr[i])
                counts_dict[arr[i]]=1
        for x in distinct_values:
            counts.append(counts_dict[x])
        return distinct_values, counts
        
    for col in col_names:
        tmp=df[col].to_list()
        tmp.sort()
        distinct_values, counts=_count(tmp)
        num_distinct_values=len(distinct_values)
        total_cnt=sum(counts)
        bins[col]=FindBinWithZeroAsOneBin(distinct_values, counts, num_distinct_values, max_bin, total_cnt, min_data_in_bin=min_data_in_bin)

    return bins

GetCodes

# 连续特征编码
def GetCodes(df,col,bins):
    # bins 保存了col的bin_upper_bound
    def _find(x,arr):
        if x<=arr[0]:
            return 0
        left=0
        right=len(arr)-1
        while True:
            mid=(left+right)//2
            if x<=arr[mid] and x<=arr[mid-1]:
                right=mid-1
            elif x<=arr[mid] and x>arr[mid-1]:
                return mid
            elif x>arr[mid] and x<=arr[mid+1]:
                return mid+1
            else:
                left=mid+1
        return 
        
    tmp=[]
    for x in df[col]:
        tmp.append(_find(x,bins[col]))

    return tmp

有问题欢迎留言给我,我会及时答复~~

<think>嗯,用户想了解LightGBM算法的数学原理。首先,我需要回忆一下LightGBM的基础。它应该属于梯度提升树(GBDT)的一种优化实现,对吧?那得先回顾GBDT的基本原理,然后看看LightGBM做了哪些改进。 GBDT的核心是迭代地训练决策树,每棵树都试图纠正前一棵树的残差。数学上,梯度提升是通过梯度下降来最小化损失函数。目标函数通常包括损失函数和正则化项,比如L2正则化。不过LightGBM的具体优化点可能不同。 接下来,用户可能想了解LightGBM相对于传统GBDT(比如XGBoost)的优势。这时候应该想到它的两个关键技术:基于梯度的单边采样(GOSS)和互斥特征捆绑(EFB)。这两个技术主要是为了提升效率和减少内存使用,尤其是在处理大规模数据时。 然后,GOSS的数学原理是什么呢?记得它是根据样本的梯度绝对值进行采样,保留梯度大的样本,并随机采样梯度小的样本。这样做是因为梯度大的样本对信息增益贡献更大,而梯度小的样本可能已经训练得比较充分。这样可以减少数据量,同时保持准确性。 然后是EFB,这部分可能比较复杂。互斥特征指的是很少同时取非零值的特征,捆绑后可以减少特征数量,从而降低计算复杂度。数学上,这涉及到如何将特征分到不同的捆绑包中,可能使用图着色算法,因为互斥特征可以视为图中不相连的节点,颜色代表不同的包。 另外,LightGBM还采用了直方图算法,将连续特征离散化为bin,这样在寻找最佳分裂点时更快。直方图做差加速,子节点的直方图可以通过父节点减去另一个子节点得到,减少计算量。 还有Leaf-wise的生长策略,不同于Level-wise,它每次选择增益最大的叶子节点进行分裂,可能提高准确率,但也容易过拟合,所以可能需要限制深度。 现在需要把这些点组织起来,用数学公式表达。比如,梯度提升的目标函数,GOSS中的采样公式,直方图算法的信息增益计算等。需要确保公式格式正确,行内公式用$...$,独立公式用$$...$$。 需要注意用户可能对某些术语不熟悉,比如“互斥特征捆绑”,需要适当解释。同时,保持结构清晰,分点讲解每个技术的数学原理,并说明它们如何提升模型性能。 可能还要提到LightGBM的优化效果,比如速度更快、内存更省,适合大规模数据。这些实际优势需要结合数学原理来说明,比如GOSS减少样本数,EFB减少特征数,从而降低计算量。 最后,检查是否有遗漏的关键点,比如直方图算法的具体数学过程,或者目标函数的分解(如损失函数和正则化项)。可能需要写出目标函数的公式,以及如何通过梯度下降来优化。 总结下来,回答的结构应该是先介绍LightGBM的总体思想,然后分点详细说明每个关键技术及其数学原理,最后总结优势。这样用户能逐步理解其背后的数学机制。</think>LightGBM(Light Gradient Boosting Machine)是一种基于梯度提升决策树(GBDT)的高效机器学习算法,其数学原理围绕**目标函数优化**、**高效特征处理**和**样本采样策略**展开。以下从核心数学原理逐步解析: --- ### 一、目标函数定义 LightGBM与GBDT一致,目标函数由**损失函数**和**正则化项**组成: $$ \mathcal{L}(\theta) = \sum_{i=1}^n L(y_i, F(x_i; \theta)) + \sum_{k=1}^K \Omega(f_k) $$ - $L(y_i, F(x_i; \theta))$:预测值$F(x_i)$与真实值$y_i$的损失(如均方误差、交叉熵等) - $\Omega(f_k)$:第$k$棵树的复杂度正则化项(控制模型复杂度,防止过拟合) --- ### 二、梯度提升框架 通过**加法模型**迭代优化,第$t$轮迭代的预测为: $$ F_t(x) = F_{t-1}(x) + \eta \cdot f_t(x) $$ - $\eta$:学习率(步长) - $f_t(x)$:第$t$棵树的最优结构 每棵树的拟合目标是**负梯度(伪残差)**: $$ r_{ti} = -\frac{\partial L(y_i, F(x_i))}{\partial F(x_i)}\bigg|_{F=F_{t-1}} $$ --- ### 三、核心优化技术 #### 1. 基于梯度的单边采样(GOSS, Gradient-based One-Side Sampling) **动机**:减少数据量,保留对梯度贡献大的样本。 **数学原理**: - 按样本梯度绝对值降序排序,保留前$a \times 100\%$的大梯度样本。 - 随机采样$b \times 100\%$的小梯度样本。 - 引入权重修正因子$\frac{1-a}{b}$,保持数据分布无偏性。 **公式**: 设大梯度样本集合为$A$,小梯度采样集合为$B$,信息增益计算为: $$ \tilde{V}_j(d) = \frac{1}{n} \left( \sum_{x_i \in A} g_i + \frac{1-a}{b} \sum_{x_i \in B} g_i \right)^2 $$ --- #### 2. 互斥特征捆绑(EFB, Exclusive Feature Bundling) **动机**:高维特征中,许多特征互斥(不同时取非零值),可合并以减少计算量。 **数学实现**: - 构造**无冲突图**:节点为特征,边表示特征非互斥。 - 使用**图着色算法**将特征分组,同组特征可捆绑。 - 合并后,通过**偏移编码**区分原始特征。 --- #### 3. 直方图算法(Histogram-based Decision Tree) **原理**:将连续特征离散化为$k$个bin,基于直方图快速寻找最优分裂点。 **数学步骤**: 1. **特征离散化**:将连续值分箱,统计每个bin的梯度之和$G$和样本数$H$。 2. **分裂增益计算**:遍历所有可能的分裂点,选择最大增益: $$ \text{Gain} = \frac{(\sum_{i \in L} g_i)^2}{\sum_{i \in L} h_i + \lambda} + \frac{(\sum_{i \in R} g_i)^2}{\sum_{i \in R} h_i + \lambda} - \frac{(\sum_{i \in P} g_i)^2}{\sum_{i \in P} h_i + \lambda} $$ - $L, R$:分裂后的左右子节点 - $P$:父节点 - $\lambda$:正则化系数 --- ### 四、Leaf-wise生长策略 与传统Level-wise(按层生长)不同,LightGBM采用**Leaf-wise生长**: - 每次选择当前增益最大的叶子节点分裂,降低模型误差。 - 通过`max_depth`参数限制树深,防止过拟合。 --- ### 五、总结:LightGBM的优势 1. **效率**:GOSS减少样本量,EFB降低特征维度,直方图加速分裂计算。 2. **准确性**:Leaf-wise生长策略更精准拟合数据。 3. **扩展性**:适合大规模数据和高维特征场景。 通过以上数学优化,LightGBM在训练速度和内存消耗上显著优于传统GBDT和XGBoost。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值