Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

ML——LightGBM概述

本文介绍LightGBM的一些特性和并行实现方法

  • 参考博客: https://www.cnblogs.com/ldt-/p/10206356.html

LightGBM vs XGBoost

  • 树结点切分方式不同:
    • XGBoost树节点切分是 Level-wise
    • LightGBM树节点切分是 Leaf-wise
  • LightGBM直接支持类别特征 ,对类别特征不必进行 One-Hot 处理
  • 实现方面:
    • 直方图算法(近似切分算法)最初由LightGBM提出,后来的XGBoost算法也实现了直方图算法
  • XGBoost的近似搜索算法和LightGBM的直方图算法不同
    • XGBoost的近似搜索算法是保存所有样本的二阶梯度,用分位数确定划分方法,他的分割点是可以直接通过计算总的样本梯度和和分位数点得到的.
        * LightGBM算法是将所有样本放进对应的桶中,并以当前桶作为划分点,计算左右桶的最大增益,它的最优点是遍历所有的桶才能得到的.
  • LightGBM 通信优化 比 XGBoost 做得好
    • 这里有亲身体验: XGBoost使用多个处理器时,有时候处理器数量增加训练速度不增加,甚至反而变慢,xgboost.XGBClassifier
  • LightGBM 使用了 GOSS(Gradient-based One-Side Sampling) 来做采样算法
    • GOSS 是通过区分不同梯度的实例,保留较大梯度实例同时对较小梯度随机采样的方式减少计算量,从而达到提升效率的目的
    • GOSS 算法流程 :
      • 根据样本的梯度将样本降序排序
      • 保留前 \(a \ (0 < a < 1)\) 比例大小的数据样本,作为数据子集 \(Z_1\),也就是保留 \(a * len(all\_samples)\) 的数据样本
      • 对于剩下的数据的样本,随机采样获得大小为 \(b \ (0 < b < 1)\) 的数据子集 \(Z_2\),也就是采样 \(b * len(all\_samples)\) 的数据样本
      • 计算信息增益时对采样的 \(Z_2\) 样本的梯度数据乘以 \((1-a)/b\) (目的是不改变原数据的分布)
    • GOSS的思想是,梯度大的实例正常使用,梯度小的实例就通过采样实现部分拟合总体的方法(拟合时不改变原来的分布,除以采样率就行了)
  • LightGBM 使用了 EFB (Exclusive Feature Bundling)
    • EFB 通过特征捆绑的方式减少特征维度(其实是降维技术)的方式,提升计算效率
    • 通常被捆绑的特征都是互斥的(一个特征值为0,一个特征值不为0), 这样两个特征捆绑起来也不会造成信息丢失
    • 当两个特征不是完全互斥时,可以用一个指标对特征不互斥程度进行评估,称为冲突比率
    • 冲突比率较小时,我们可以将他们捆绑而不太影响训练结果
    • EFB 算法流程:
      • 将特征按照非零值的个数进行排序
      • 计算不同特征之间的冲突比率
      • 遍历每个特征并尝试合并特征,使冲突比率最小化
  • LightGBM 的内存优化
    • XGBoost 中 需要对每个特征进行预排序(注意:这里不能在算是XGBoost的缺点了,后来的XGBoost也实现了这个直方图算法,不需要预排序了)
    • LightGBM使用直方图算法替代了预排序

LightGBM的优点

  • 相对XGBoost:
    • 速度快 (GOSS, EFB, Histogram等)
    • 内存少 (XGBoost中排序)
    • 精度高(效果不明显, 与XGBoost相当, 本人测试, 实际使用中有时候不如XGBoost, 可能是参数调节的问题)

缺点

  • 虽然官方一再强调LightGBM的精度不比XGBoost低,而且可能超过XGBoost,但是实践中, LightGBM除了比XGBoost快以外, 精度方面没什么优势, 甚至精度还不如XGBoost(注意: 也可能是我调参技术不行)
    • 问题: 为什么某些数据集上出现LightGBM比XGBoost精度差的情况?
    • 回答: (个人理解)因为GOSS和EFB会带来一定的精度损失

总结

  • LightGBM = XGBoost + Histogram + GOSS + EFB
    • XGBoost: 不同于XGBoost的, 树节点切分不同, LightGBM使用了 Leaf-wise 的生长策略
    • Histogram:
      • Histogram方法牺牲了一定的精度,但是作者强调了在实验中精度降低并不多
      • 开始的XGBoost使用的是预先排序的方式, 后来在 XGBoost 中也实现了Histogram
      • LightGBM 对 Histogram 算法进一步加速
      • 一个叶子节点的 Histogram 可以直接由父节点的Histogram和兄弟节点的Histogram做差得到, 一般情况下,构造Histogram需要遍历该叶子上的所有数据,通过该方法,只需要遍历Histogram的k个捅, 速度提升了一倍
    • GOSS: 对于梯度小的样本, 使用采样部分代替总体的方法省时间
    • EFB: 互斥特征捆绑,提升计算效率
  • LightGBM 真正做到了把并行做到极致
    • 特征并行: 在不同的机器在不同的特征集合上分别寻找最优的分割点, 然后再机器间同步最优的分割点.
    • 数据并行: 让不同的机器先在本地构造直方图, 然后进行全局的合并,然后在合并的直方图上寻找最优的分割点.
  • LightGBM 支持类别特征
    • 无需将类别特征 One-Hot
  • 问题: 为什么XGBoost也使用了直方图,但是速度依然比较慢?
    • 直方图算法的实现有两种:
      • 1)全局构造 ,在树的构造过程中只建立一次直方图, 每次分裂都从缓存的直方图中寻找分裂点
      • 2)局部构造 ,每次树分裂到一层的时候就建立一次直方图
      • XGBoost使用的是局部构造的方式, 所以速度会较慢

ML——LR-逻辑回归


手动推导流程

  • 假设有m样本: \(X = (x_{1}, x_{2}, x_{3},\dots x_{m})\)
    • 样本点为: \(((x_{1}, y_{1}), (x_{2}, y_{2}), (x_{3}, y_{3}), \dots (x_{m}, y_{m}))\)
  • 假设 \(w, \theta, x_{i}\) 等所有向量都为列向量

确定分类决策函数

  • 线性回归模型
    $$f(x) = w^{T}x + b$$
  • 令
    $$
    \begin{align}
    \theta &= (w; b) \\
    x_{i} &= (x_{i}; 1)
    \end{align}
    $$
  • 有
    $$f(x) = \theta^{T} x$$
  • 逻辑回归决策函数在线性回归模型上加一个sigmoid函数
    $$
    \begin{align}
    h_{\theta}(x) &= sigmoid(f(x)) \\
    &= \frac{1}{1+e^{-f(x)}} \\
    &= \frac{1}{1+e^{-\theta^{T} x}} \\
    \end{align}
    $$
  • 即
    $$
    \begin{align}
    p(y=1|x) &= h_{\theta}(x) = \frac{1}{1+e^{-\theta^{T} x}} = \frac{e^{\theta^{T} x}}{1+e^{\theta^{T} x}}\\
    p(y=0|x) &= 1-h_{\theta}(x) = \frac{e^{-\theta^{T} x}}{1+e^{-\theta^{T} x}} = \frac{1}{1+e^{\theta^{T} x}} \\
    \end{align}
    $$
  • 对数几率(log odds, 也称为logit) 定义为: \(ln \frac{p}{1-p}\),在LR中有:
    $$
    \begin{align}
    ln\frac{h_{\theta}(x)}{1-h_{\theta}(x)} = \theta^T x
    \end{align}
    $$
  • 分类超平面不确定,与最终的阈值有关, \(\alpha\) 的值与最终阈值相关
    $$w^{\star}x + b^{\star} = \alpha$$
    • 分类超平面由 \((w, b)\) 和阈值唯一确定,(注意: SVM的分类超平面由 \((w, b)\) 唯一确定)
    • 这一点和SVM不同,SVM的分类超平面是确定的,详情参看ML——SVM-支持向量机

确定优化目标

  • LR中使用极大似然法
    $$
    \begin{align}
    L(\theta) &= p(Y|X;\theta) \\
    &= \prod_{i=1}^{m}p(y_{i}|x_{i}; \theta) \\
    &= \prod_{i=1}^{m}(p(y_{i} = 1|x_{i};\theta))^{y_{i}}(p(y_{i} = 0|x_{i};\theta))^{1-y_{i}}
    \end{align}
    $$
  • 上面的式子不易求导优化,我们使用与上面的式子单调性和最优点等价的对数似然函数为
    $$
    \begin{align}
    LL(\theta) &= \log L(\theta) \\
    &= \log \prod_{i=1}^{m}(p(y_{i} = 1|x_{i};\theta))^{y_{i}}(p(y_{i} = 0|x_{i};\theta))^{1-y_{i}} \\
    &= \sum_{i=1}^{m}\left (y_{i}\log(p(y_{i} = 1|x_{i};\theta)) + (1-y_{i})\log(p(y_{i} = 0|x_{i};\theta)) \right ) \\
    &= \sum_{i=1}^{m}\left (y_{i}\log\frac{p(y_{i} = 1|x_{i};\theta)}{p(y_{i} = 0|x_{i};\theta)} +\log(p(y_{i} = 0|x_{i};\theta))\right ) \\
    \end{align}
    $$
    • 上面的式子中:
      $$\sum_{i=1}^{m}\left (y_{i}\log(p(y_{i} = 1|x_{i};\theta)) + (1-y_{i})\log(p(y_{i} = 0|x_{i};\theta)) \right )$$
    • 加个负号即为为交叉熵损失函数
      $$-\sum_{i=1}^{m}\left (y_{i}\log(p(y_{i} = 1|x_{i};\theta)) + (1-y_{i})\log(p(y_{i} = 0|x_{i};\theta)) \right )$$
    • 所以交叉熵损失函数与逻辑回归的对数似然损失函数(=逻辑回归对数似然函数的负数)是等价的
  • 由前面的推导有
    $$
    \begin{align}
    \log \frac{p(y_{i} = 1|x_{i};\theta)}{p(y_{i} = 0|x_{i};\theta)} = \log\frac{\frac{e^{\theta^{T} x}}{1+e^{\theta^{T} x}}}{\frac{1}{1+e^{\theta^{T} x}}} = \log e^{\theta^{T} x} = \theta^{T}x\\
    \end{align}
    $$
    • 且:
      $$\log(p(y_{i} = 0|x_{i};\theta)) =\log(\frac{1}{1+e^{\theta^{T} x}}) = -\log(1+e^{\theta^{T} x})$$
  • 故而有
    $$
    \begin{align}
    LL(\theta) &= \sum_{i=1}^{m}\left (y_{i}\log\frac{p(y_{i} = 1|x_{i};\theta)}{p(y_{i} = 0|x_{i};\theta)} +\log(p(y_{i} = 0|x_{i};\theta))\right )\\
    &= \sum_{i=1}^{m}\left ( y_{i}\theta^{T}x - \log(1+e^{\theta^{T}x}) \right)
    \end{align}
    $$

损失函数

  • 最大化似然函数等价于最小化似然函数的负数
  • LR中使用极大似然法 ,所以对应的损失函数自然为对数似然损失函数
    $$loss(\theta) = -LL(\theta) = \sum_{i=1}^{m}\left (- y_{i}\theta^{T}x +\log(1+e^{\theta^{T}x}) \right)$$

梯度下降法优化

注意: 这里优化目标也可以用牛顿法

  • 目标,求一个 \(\theta^{\star}\) 满足似然函数最大化或者损失函数最小化
    $$\theta^{\star} = \mathop{\arg\max}_{\theta} LL(\theta) = \mathop{\arg\min}_{\theta} -LL(\theta) = \mathop{\arg\min}_{\theta} loss(\theta)$$
  • 对数似然函数对参数 \(\theta\) 求导有
    $$
    \begin{align}
    \frac{\partial loss(\theta)}{\partial\theta} &= \sum_{i=1}^{m}\left ( -y_{i}x_{i} + \frac{x_{i}e^{\theta^{T}x}}{1+e^{\theta^{T}}x}\right ) \\
    &= \sum_{i=1}^{m}x_{i}\left ( -y_{i} + \frac{e^{\theta^{T}x}}{1+e^{\theta^{T}}x}\right ) \\
    &= \sum_{i=1}^{m}x_{i}\left ( -y_{i} + h_{\theta}(x_{i})\right ) \\
    \end{align}
    $$
  • LR模型的梯度下降参数迭代公式
    $$
    \begin{align}
    \theta^{t+1} &= \theta^{t} - \alpha\sum_{i=1}^{m}x_{i}\left ( -y_{i} + h_{\theta^{t}}(x_{i})\right ) \\
    &= \theta^{t} + \alpha\sum_{i=1}^{m}x_{i}\left ( y_{i} - h_{\theta^{t}}(x_{i})\right )
    \end{align}
    $$
    • 其中 \(\alpha\) 为步长
  • 线性回归和LR模型的梯度下降法参数迭代公式表达式看似相同,但是不同的模型对应的 \(h_{\theta}\) 函数并不相同

其他总结

参考:https://www.cnblogs.com/ModifyRong/p/7739955.html

  • LR中使用极大似然法 ,所以对应的损失函数自然为对数似然损失函数(对数损失函数)
    • 对数似然损失函数定义为:
      $$Loss(\theta) = -P(Y|X; \theta)$$
    • 《统计学习方法》P213 中定义 LR 的损失函数为逻辑斯蒂损失函数,在LR模型和最大熵模型中,逻辑斯蒂损失函数本质上与对数似然损失函数等价,可推导得到
  • 一句话概括逻辑回归:
    • 逻辑回归是假设数据服从伯努利分布,通过极大似然函数的方法,运用梯度下降法或者牛顿法来求解参数,来达到将数据二分类的目的
    • 逻辑回归的假设: 数据服从伯努利分布 , \(p(y=1|x) = 1-p(y=0|x)\)
    • 逻辑回归的损失函数: 对数似然损失函数(交叉熵损失函数) ,也就是对数似然函数的负数
    • 逻辑回归的求解方法: 梯度下降法或牛顿法
    • 逻辑回归的目的: 将数据二分类
    • 逻辑回归如何分类: 预测结果是连续的[0-1]的数 ,我们一般选择0.5作为阈值来分类,但是这个值可能是可以变化的,因为损失函数最小并不意味着0.5时分类精度最高
  • 为什么要用极大似然法?等价于为什么要用对数似然损失函数作为损失函数?
    • 损失函数一般有平方损失函数,对数损失函数,合页损失函数,绝对值损失函数等,极大似然函数取对数后等同于对数损失函数,在逻辑回归这个模型中,推导可以得到,对数损失函数训练求解参数的迭代函数只与 \(x_{i}, y_{i}\) 相关,与sigmoid函数的梯度等无关.这样的参数更新自始至终都比较稳定
    • 为什么不选平方损失函数的呢?其一是因为如果你使用平方损失函数,你会发现梯度更新的速度和sigmod函数本身的梯度是很相关的,sigmod函数在它在定义域内的梯度都不大于0.25, 这样训练会非常的慢
  • 逻辑回归中,如果某些特征高度相关,甚至某些特征完全相同,会造成什么影响?
    • 损失函数收敛后,没有影响,因为特征的相关性并不影响分类器效果,重复特征会分化特征的权重(10个重复特征和单个特征训练结果差别在于前者每个特征的权重是后者的十分之一),本质上最终结果不变的
    • 但是训练时由于特征重复,参数增多,模型复杂度增加,训练时长,内存等都会增加
  • 为什么需要去掉高度相关的特征?
    • 去掉高度相关的特征使得模型可解释性更好
    • 提高训练时间,节约内存,减少参数数量
    • 特征的提取本身也需要时间,实际工程项目中可以少提取一个特征往往能节约很多时间
  • logistic 与 logit 的区别?
    • logit: 又名 log adds , 指的是”对数几率”, 定义为 \(ln\frac{p}{1-p}\)
    • logistic: 又叫Sigmoid函数, 指的是”对数几率函数”, 本质上是一种”Sigmoid”函数, 定义为 \(f(x) = \frac{1}{1+e^{-x}}\)
  • 简单介绍LR模型的优缺点:
    • 优点:
      • 模型简单,可解释性好,(如果对数据特征进行了归一化处理的话)可以从特征的权重看到不同特征对最终结果的影响
      • 模型效果往往不错(特征工程做得好的话)
      • 训练速度快, 成熟的SGD优化方法(SGD可以分布式)等
      • 内存占用小
      • 输出结果可以作为概率值,然后可以对阈值根据实际进行划分,不一定是确定的0.5,只是一般选择0.5而已
    • 缺点:
      • 难以处理非线性数据,本质上是线性分类面
      • 难以处理数据不平衡问题 , 这里如果正例远远多于负例,那么全都预测为正例,整体损失函数也不会太大
      • LR 本身无法筛选特征 ,有时候会用GBDT和XGBoost来筛选特征,然后再用LR模型
  • 扩展:逻辑回归可像SVM一样引入核函数处理非线性分类问题吗?
    • 一般来说不可以
    • [存疑]理论上通过对原始样本非线性映射,似乎也可以,如果将 \(f(x) = \theta^{T}x\) 中的 \(f(x)\) 看做 \(\theta\) 看做变量,然后类比SVM的核函数,定义一个关于 \(x_{i}\) 的非线性映射
      • 这里 \(x_{i}\) 表示第 \(i\) 个样本, 用 \(x_{i}^{j}\) 表示第 \(i\) 个样本的第 \(j\) 个维度
        $$x_{i}^{j} = \phi_{j}(x_{i}^{j})$$
      • 基于上述非线性映射函数的定义,我们对每个样本都进行线性映射,每个维度用不同的映射函数(不同样本相同维度映射函数相同)
      • 这里的非线性映射与SVM的核函数不同,SVM不使用核函数的话,也可以通过相同的非线性映射的方式实现非线性分类
      • 使用核技巧后的LR模型将变得很慢,SVM与kernels是相配的,而LR与kernels会十分慢(来源SVM核技巧)
  • LR 模型训练完成后,输出概率多少的样本应该评估为正样本?【以下分析为个人理解,暂无严格证明】
    • LR模型的损失函数本质上是交叉熵损失函数,交叉熵损失函数本质是最小化预估分布与训练样本分布之间的差距,故而预估均值与真实训练样本均值应该相等 ,即LR模型的预估值均值理论上与训练样本标签均值相同(这里LR的预估值是Sigmoid的输出值,训练样本负样本标签为0,正样本标签为1)。【PS,一种辅助理解思考:假设训练集中的样本特征完全相同,但其中30%是正样本,另外70%为负样本,那么优秀的LR模型在预估该训练样本时应该输出约为0.3】
    • 进一步来说,当预估的平均值大于训练样本的均值时,即可判断为正样本
      • 举例,假设训练样本的均值为0.4,那么预估值大于0.4的样本均可视为正样本(思考:这种判定下模型的准确率 \( \text{Accuracy} = \frac{TP+TN}{TP+TN+FP+FN}\) 应该是最高的?)

附录:多分类任务重的 logits

  • TLDR:多分类任务中模型输出的 logits 和 LR 中的 logit 含义不完全相同
    • 二者是同源但适用场景和维度不同的概念
  • LR 的 logit 是二分类专属的标量对数几率;
  • 多分类模型的 logits 是多分类任务的向量原始得分 ,是 logit 概念在多类别场景下的推广,需通过 Softmax 转换为概率分布

同源:基于对数几率的定义

  • 二者的本质都源于对数几率(log-odds) ,即事件发生概率与不发生概率的比值的自然对数,公式为:
    $$\text{logit}(p) = \ln\left(\frac{p}{1-p}\right)$$
    • 这个公式是连接线性得分和概率的桥梁

二分类场景下的等价性

  • 当多分类任务退化为二分类时,模型的 logits 向量为 \([\text{logit}_0, \text{logit}_1]\)
    • 若满足 \(\text{logit}_0 = -\text{logit}_1\)
    • 则 \(2\text{logit}_1\) 就完全等价于 LR 中的 logit(正例的对数几率)
  • 此时 Softmax 激活等价于 Sigmoid 激活:
    $$ p(y=1) = \frac{e^{\text{logit}_1}}{e^{\text{logit}_0}+e^{\text{logit}_1}} = \frac{e^{\text{logit}_1}}{e^{-\text{logit}_1}+e^{\text{logit}_1}} = \frac{1}{1+e^{-2\text{logit}_1}} = \frac{1}{1+e^{-\theta^T x}} = \frac{1}{1+e^{-\text{logit}}}$$
    • 若令 \(\text{logit} = 2\text{logit}_1\),则与 LR 的 Sigmoid 输出完全一致

多分类 LR 的 logits

  • 多分类逻辑回归(Softmax 回归)的输出 logits 就是上述多分类模型的 logits 向量,每个元素对应一个类别的线性得分,这是 logit 概念从标量到向量的扩展

ML——XGBoost-vs-传统GBDT

本文主要介绍XGBoost和其他传统GBDT的比较的优劣
XGBoost又叫(Newton Boosting Tree)

  • GBDT推导: ML——GBDT-梯度提升树-推导过程
  • XGBoost推导: ML——XGBoost-推导过程

XGBoost的优点

参考博客: https://www.cnblogs.com/massquantity/p/9794480.html

  • XGBoost损失函数是二阶泰勒展开(与牛顿法对应),GBDT是一阶泰勒展开(与梯度下降法对应)

    • 传统 GBDT 在优化时只用到一阶导数信息, 所以传统GBDT也叫 (Gradient Boosting)
    • XGBoost 则对目标函数进行了二阶泰勒展开,同时用到了一阶和二阶导数, 所以XGBoost又叫(Newton Boosting Tree)
  • XGBoost加了正则项 ,普通的GBDT没有,所以XGBoost学出来的模型更简单,能防止过拟合,提高模型的泛化性能

  • XGBoost Shrinkage(缩减)

    • 每次进行完一次迭代后,将叶子节点的权重乘以该系数(一般叫做eta \(\eta\))
    • 可以理解为这里Shrinkage是将学习速率调小,从而需要的迭代次数增多
    • 减小学习率实际上是减弱了每棵树对整体的影响,从而让后面的树有更多的学习空间
    • 下面的表述还有待确定:
      • 进一步惩罚决策树叶节点的值(惩罚的意思是叶节点越大,惩罚越多,损失函数越大)
      • 对叶节点的惩罚本身可以理解为一个正则化
  • 结点分裂的增益计算公式不同

    • 传统 GBDT 一般采用的是最小二乘法作为内部分裂的增益计算指标(因为用的都是回归树)
      • 注意: 这里本文中描述的最小绝对偏差回归(LAD)是第一步损失函数的定义,不是这一步中的信息增益计算
      • 查看源码: sklearn.ensemble.GradientBoostingClassifier在分裂结点时可以选择三种方式:
        • friedman_mse(默认), mean squared error with improvement score by Friedman
        • mse: mean squared error
        • mae: mean absolute error
    • 而 XGBoost 使用的是经过优化推导后的式子
      $$
      \begin{align}
      Gain = \frac{G_L^2}{H_L+ \lambda} + \frac{G_R^2}{H_R+ \lambda} - \frac{(G_L + G_R)^2}{H_L+ H_R + \lambda} - \gamma
      \end{align}
      $$
      • 注意: XGBoost中的信息增益计算形式固定为上面的计算方式,但是具体的值与损失函数的定义相关(因为 \(g_i\) 和 \(h_i\) 的是损失函数的一阶和二阶梯度)
  • XGBoost支持自定义的损失函数 ,支持一阶和二阶可导就行

    • 注意,这里的损失函数指的是 \(l(y_i,\hat{y}_i)\),单个样本预测值与目标值的差异, 也就是单个样本的损失函数
    • 从ML——XGBoost-推导过程中可知:
      • \(g_i = l’(y_i,\hat{y}_i^{t-1})\) 为 \(l(y_i,\hat{y}_i)\) 对 \(\hat{y}_i\) 的一阶导数在 \(\hat{y}_i = \hat{y}_i^{t-1}\) 处的值
      • \(h_i = l’’(y_i,\hat{y}_i^{t-1})\) 是 \(l(y_i,\hat{y}_i)\) 对 \(\hat{y}_i\) 的二阶导数在 \(\hat{y}_i = \hat{y}_i^{t-1}\) 处的值
    • XGBoost中只要损失函数二次可微分即可得到 \(g_i\) 和 \(h_i\)
      • \(g_i\) 和 \(h_i\) 本身与损失函数的定义形式无关, 只要求损失函数二阶可微分即可
    • 只要有了 \(g_i\) 和 \(h_i\) 我们即可
      • 根据预先推导的叶子节点分数表达式 \(w_j^{\star} = -\frac{G_j}{H_j+\lambda}\) 求得叶子结点的分数
      • 根据预先推导的信息增益公式 \(Gain = \frac{G_L^2}{H_L+ \lambda} + \frac{G_R^2}{H_R+ \lambda} - \frac{(G_L + G_R)^2}{H_L+ H_R + \lambda} - \gamma\) 确定分裂特征和分裂点
    • GBDT 损失函数关系一般选择最小二乘回归或者最小绝对偏差回归
      • 最小方差回归 : (Least-Squares Regression, LSR)
        $$\begin{align} L(y,F(x)) = \frac{1}{2}(y-F(x))^{2} \end{align}$$
      • 最小绝对偏差回归 : (Least Absolute Deviation Regression, LAD)
        $$\begin{align} L(y,F(x)) = |y-F(x)| \end{align}$$
      • 查看源码: sklearn.ensemble.GradientBoostingClassifier的损失函数是定义好的, 不能自己定义, 详细如下源码
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        LOSS_FUNCTIONS = {'ls': LeastSquaresError,
        'lad': LeastAbsoluteError,
        'huber': HuberLossFunction,
        'quantile': QuantileLossFunction,
        'deviance': None, # for both, multinomial and binomial
        'exponential': ExponentialLoss,
        }


        _SUPPORTED_LOSS = ('deviance', 'exponential')
        ....

        if (self.loss not in self._SUPPORTED_LOSS
        or self.loss not in LOSS_FUNCTIONS):
        raise ValueError("Loss '{0:s}' not supported. ".format(self.loss))
  • XGBoost 借鉴了随机森林的做法,支持列采样 ,不仅能降低过拟合,还能减少计算量,这也是 XGBoost 异于传统 GBDT 的一个特性

    • 列采样: 这借鉴于随机森林中的做法,每棵树不使用所有特征,而是部分特征参与训练
    • 可以减少计算量,同时还能降低过拟合,简直优秀
  • XGBoost 有缺失值自动处理 , 在计算分裂增益时不会考虑带有缺失值的样本,这样就减少了时间开销,在分裂点确定了之后,将带有缺失值的样本分别放在左子树和右子树,比较两者分裂增益,选择增益较大的那一边作为默认分裂方向

    • 进一步理解稀疏数据的支持: [待更新]
  • 并行化处理 :由于 Boosting 本身的特性,传统 GBDT 无法像随机森林那样树与树之间的并行化

    • XGBoost 的并行主要体现在特征粒度上,在对结点进行分裂时,由于已预先对特征排序并保存为block 结构,每个特征的增益计算就可以开多线程进行,极大提升了训练速度
  • 剪枝策略不同

    • 传统 GBDT 在损失不再减少时会停止分裂,这是一种预剪枝的贪心策略,容易欠拟合
    • XGBoost采用的是后剪枝的策略,先分裂到指定的最大深度 (max_depth) 再进行剪枝
      • 而且和一般的后剪枝不同, XGBoost 的后剪枝是不需要验证集的[待更新:XGBoost剪枝的策略是怎样的?只依赖信息增益指标吗?]
      • 和博客作者指出的一样,我这里并不是”纯粹的”后剪枝,因为提前设定了最大深度
  • 基分类器的选择不同:

    • 传统GBDT中原始论文使用树回归 ,本文见Firedman 1999,后来作者提出可以使用逻辑回归 ,本文见Friedman 2000
    • XGBoost后面的各种损失计算等都包含着树模型的复杂度,叶子节点分类等,所以是只能用CART,不能使用逻辑回归的
    • (从函数空间定义和后面的公式推导来看)XGBoost中基函数只使用CART回归树,不能使用逻辑回归
    • 但是事实上XGBoost的实现中是支持线性分类器作为基分类器的, 参数booster[default='gbtree'],可选为booster=gblinear
      • 使用线性分类器作为基分类器时, XGBoost相当于带有L1正则化和L2正则化的:
        • Logistic回归(分类问题)
        • 线性回归(回归问题)
  • 分桶策略算法不同

    • 传统的GBDT分桶时每个样本的权重都是相同的
    • XGBoost中每个样本的权重为损失函数在该样本点的二阶导数(对不同的样本,计算得到的损失函数的二阶导数是不同的), 这里优点AdaBoost的思想,重点关注某些样本的感觉
    • 这里影响的是划分点的位置(我们划分划分点[桶]时都是均匀划分样本到桶里面,当不同样本的权重不同时,每个桶里面的样本数量可能会不同)
    • 下图是一个示例

XGBoost的缺点

注意这里是

  • 空间消耗大
    • 因为要在训练之前先对每个特征进行预排序并将结果存储起来,所以空间消耗较大
    • GBDT无需预排序,但是每次重新排序很耗时间
  • 速度慢
    • 虽然XGBoost速度比传统 GBDT 快了不少, 但是不如 LightGBM 快, 且 LightGBM 占用内存更低

XGBoost为什么能够并行?

而GBDT是不能并行的,原因是:https://www.136.la/shida/show-187480.html

  • GBDT不能并行的原因是没有预排序(XGB的预排序结果会存储到block结构)等,在有了预排序结果后,同一个特征的切割方式可以并行尝试计算增益
  • 决策树最耗时间(包括GBDT)的步骤是对特征值的排序
    • 用于确定最佳分割点
  • XGBoost训练前,预先对数据进行了排序,称为预排序
    • 将预先排序的结果保存为block结构, 后面迭代的时候重复使用这个结构,从而实现一次排序,多次使用,大大减少计算量
  • 这个结构减少计算量的同时还为并行化提供了可能([待更新]实际上不用预排序也能并行的吧?只是每次需要先使用一个单一线程排序,然后再多个线程并行?只是不够并行)
    • 进行结点的分裂时,需要计算每个特征的增益,然后选择增益最大的那个特征分裂
    • 这里我们可以同时使用多个线程计算不同特征的增益, 从而实现并行化
  • 总结为三方面的并行, ([待更新]但是具体实现了哪些并行不确定)
    • 同一层级的结点间每个结点的分裂可以并行
    • 同一个结点内部不同特征增益的计算可以并行
    • 同一个结点同一个特征的不同分裂点的增益计算可以并行

GBDT为什么不能自定义损失函数?

GBDT为什么不能像XGBoost一样自定义损失函数?

  • 查看sklearn.ensemble.GradientBoostingClassifier的源码发现, 确实不支持自定义的损失函数
  • [待更新],因为涉及到后面的参数更新方式?

XGBoost如何使用自定义的损失函数?

模型直接调用fit函数无法传入自定义的损失函数, 需要在模型开始定义的时候传入或者使用xgb.train函数调用

  • 使用方法1:

    1
    2
    3
    from xgboost import XGBClassifier

    clf = XGBClassifier(objective=MyLossFunction)
  • 使用方法2:

    1
    2
    3
    4
    5
    import xgboost as xgb
    from xgboost import XGBClassifier

    clf = XGBClassifier()
    xgb.train(xgb_model=clf, obj=MyLossFunction)

Algorithm——AVL树和红黑树等各种树结构总结


各种树的介绍

树

  • 一个根节点,每个结点可能有多个子节点

二叉树

  • 一个根节点,或者为空
  • 每个结点只有两个子节点

二叉搜索树

也叫二叉查找树

  • 首先是一棵二叉树
  • 左边孩子结点的值都小于当前结点
  • 右边孩子结点的值都大于当前结点
缺点
  • 极端情况下,树模型会退化成链表,查找变成了 O(n) 复杂度的

平衡二叉搜索树(AVL树)

也叫平衡二叉查找树

  • 首先是一棵二叉搜索树
  • 对每个结点而言, 左右孩子结点的深度差值不能超过1 , 从而保证查找是 O(log n) 的
  • 控制平衡方法: 参考链接AVL树详解
    • 左-左型: 右旋
    • 右-右型: 左旋
    • 左-右型: 左旋 + 右旋
      • 第一步左旋后面部分,变成 左-左 型
      • 第二步使用右旋修正 左-左 型
    • 右-左型: 右旋 + 左旋
      • 第一步右旋后面部分,变成 右-右 型
      • 第二步使用左旋修正 右-右 型
缺点
  • 每棵树的左右子树高度最多差1这个要求太严了
  • 几乎每次插入或者删除结点时都会造成规则破坏, 也就需要频繁的通过左旋或者右旋操作来修正
  • 插入和删除太频繁的场景中不太适合使用AVL树, 性能会因为左右子树高度最多差1这个规则频繁被打破而降低

红黑树

  • 首先是一棵二叉搜索树
  • 每个结点不是黑色就是红色
  • 根节点为黑色
  • 每个叶子结点都为黑色的空结点(NULL)
    • 注意: 叶子节点不存数据
  • 任何相邻结点不同时为红色
    • 注意,相邻结点可以为黑色,且可以某条路径上全是黑色
  • 对每个结点而言,当前结点到叶子结点的所有路径包含相同的黑色结点数
优点
  • 能保证查找时间复杂度是 O(log n) 的, 和AVL树差不多
    • 证明: [待更新]
  • 插入和删除操作中不会频繁破坏红黑树的规则
红黑树的应用
  • 容器集合 HashMap, TreeMap等
    • HashMap是 链表 + 红黑树的实现, 当冲突时就需要使用红黑树加快检索
    • HashMap中 链表长度太短时使用链表, 太长时使用红黑树, 这个阈值一般设置为8

二三树

  • 红黑树是二三树的一个变形,一般用红黑树就够了
    [待更新]

B树

  • B树在大量的数据库和文件系统场景中都有使用
    [待更新]

B+树

[待更新]


总结

  • 可以说红黑树是一种不严格的平衡树

Python——为什么Python中没有自增++和自减--操作?

C++和Java等语言都有++和–操作,为什么以方便自居的Python却没有这种操作呢?


Python的数值对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = 1
b = 1
c = 1000123
d = 1000123

print id(a)
print id(b)
print id(c)
print id(d)

## output:
# 94181316498840
# 94181316498840
# 94181323965720
# 94181323965720
  • Python数值对象都是不可变类型,与String一样,所以不能修改对象内部数据
  • C++中的i++修改内存中对象本身,数值增加1,而Python不能修改对象本身
  • 与C++中字符串可以修改,Python中不能修改是一个道理

Python——优先队列PriorityQueue

本文介绍Python中优先队列的用法

  • 注意: queue并不能算是Python标准库,所以在LeetCode等OJ环境中不能使用, 想要使用优先队列的话可以使用Python的标准库heapq
  • heapq的使用请参考 Python——heapq模块-最大堆最小堆

导入包

1
2
3
from Queue import PriorityQueue
# or
from queue import PriorityQueue
  • queue包名已经弃用,测试发现本地Python2.7环境可以用,但是LeetCode线上环境不能用
  • 推荐使用Queue

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from Queue import PriorityQueue

pq = PriorityQueue()

pq.put((10, "start"))
pq.put((5, "b", 12, 123))
pq.put((5, "a", 6))
pq.put(1)
pq.put(4)
pq.put([0, "a"])
pq.put([8, "b"])
pq.put("avb")
pq.put(None)

# while pq.not_empty()
# while not pq.empty()
while pq.qsize():
print pq.get()
## output:
# None
# 1
# 4
# [0, 'a']
# [8, 'b']
# avb
# (5, 'a', 6)
# (5, 'b', 12, 123)
# (10, 'start')
  • 不能使用not pq这样的语句判断优先队列是否收敛,他不是普通的内嵌对象(list,str等是内嵌对象),除非pq == None否则,双端队列对象永远为not pq == False

  • 下面用法最优:

    1
    while pq.qsize():
  • PriorityQueue中默认递增排序(这一点与Python中的sorted函数和sort()函数一样),每次get(),移除并返回最小的对象

  • PriorityQueue中,可以同时添加不同类别的对象

  • PriorityQueue会将对象首先按照类别排序,然后各个类别内部按照不同数值排序

  • 若传入对象是可以直接比较大小的类型即可直接传入,包括tuple, list, str, int(long)等类型

    • Python中list,tuple,str等都是可以直接比较大小的,默认使用他们的第一个元素比较大小,如果第一个元素相等,则比较第二个元素,以此类推
    • 详细情况参考本文后面的说明

Python中的内嵌对象比较大小

  • Python中类别间也可以比较大小,默认类别间大小为:tuple > str > list > int(long) > None, 但是记不清楚的话不建议使用Python的这个特性,容易造成错误
  • Python中list,tuple,str等都是可以直接比较大小的,默认使用他们的第一个元素比较大小,如果第一个元素相等,则比较第二个元素,以此类推
  • Python中类别间也可以比较大小,默认类别间大小为:tuple > str > list > int(long) > None, 但是记不清楚的话不建议使用Python的这个特性,容易造成错误
    1
    2
    3
    4
    5
    ls = [(1, 'b'), (1, 'a'), (2, 'a'), [1, 'a'], [1, 'b'], [2, 'a'], '1a', '1b', '2a', 1, 2, 3, None]
    ls.sort()
    print ls
    # # output
    # [None, 1, 2, 3, [1, 'a'], [1, 'b'], [2, 'a'], '1a', '1b', '2a', (1, 'a'), (1, 'b'), (2, 'a')]

Python自定义对象比较大小

  • 在做算法题时,没必要的情况下不建议使用

  • 在做工程时建议使用这种方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import Queue
    class Node():
    def __init__(self, val):
    self.val = val

    def __lt__(self, other):
    return self.val < other.val

    pq = Queue.PriorityQueue()
    pq.put(Node(5))
    pq.put(Node(1))

    while pq.qsize():
    print pq.get().val
    # # output
    # 1
    # 5
  • 注意__lt__函数中是小于号,说明递增排序,大于号,说明递减排序

  • PriorityQueue对象不是普通Python内嵌对象,不能使用Python内嵌的len函数

Python——关于列表list的操作

Python的list有很多强大的功能,有些比较罕见的操作可能很有用,需要我们记住


list的常见操作

list子列表

  • 注意使用子列表时是一个新对象,操作子列表与原始list无关
  • 在快速排序和归并排序中不可将子列表传入,以期待可以从函数中修改原始列表的值

list反序子列表

  • list 反序列示例:
    1
    2
    3
    4
    5
    6
    7
    l = [1, 2, 3]
    l1 = l[::-1]
    print l
    print l1
    # # output:
    # [1, 2, 3]
    # [3, 2, 1]

list的罕见操作

remove(object)

  • 移除列表中第一个与object相等的对象
    1
    2
    3
    4
    5
    6
    l = [1, 2, 2, 3, 4]
    l.remove(2)
    print l

    # # output:
    # [1, 2, 3, 4]

pop(index)

  • 从列表中移除一个元素,并返回该元素,index为索引
  • 默认移除最后一个
    1
    2
    3
    4
    5
    6
    7
    8
    l = [1, 2, 2, 3, 4, 5]
    l.pop(0)
    print l
    l.pop()
    print l
    # # output:
    # [2, 2, 3, 4, 5]
    # [2, 2, 3, 4]

Python——双端队列deque

本文介绍Python中双端队列(double-ended queue, 简称为deque)的用法


导入包

1
2
from collections import deque
import collections

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import collections
dq = collections.deque()
dq.append(3)
dq.append(4)
dq.append(1)
dq.appendleft(9)
dq.appendleft(10)
print dq
print dq.pop()
print dq
print dq.popleft()
print dq

while len(dq):
print dq.pop()


# # output:
# deque([10, 9, 3, 4, 1])
# 1
# deque([10, 9, 3, 4])
# 10
# deque([9, 3, 4])
# 4
# 3
# 9
  • 注意,deque没有qsize()函数,但是可以像普通队列一样使用Python内嵌的len函数

Python——自定义pip包的安装和打包


整体说明

  • 打包本地项目为的 pip 包时,可以使用 pip install . 命令
  • 执行 pip install . 命令时,Python 包管理工具 pip 会根据当前目录中的 setup.py 文件来安装包
    • 这个过程涉及多个步骤,包括解析包的元数据、处理依赖关系、构建和安装包等
    • 安装的内容包括包的所有模块、数据文件、编译文件以及命令行工具
    • 默认情况下,只安装核心依赖,可选依赖需要用户显式指定

构建一个包的步骤

第一步:解析和构建

  • 解析 setup.py (setup.py 的详细示例见附录):
    • pip 首先会查找并解析当前目录中的 setup.py 文件
    • 读取包的元数据(如包名、版本、作者信息等)和安装选项(如 install_requires、extras_require 等)
  • 构建包:
    • pip 会使用 setuptools 或 distutils 来构建包
    • 包的构建包括编译C扩展(如果存在)、处理包内的数据文件等
    • 生成一个源代码分发包(Source Distribution,简称 sdist)或一个二进制分发包(Binary Distribution,简称 wheel)

第二步:处理依赖

  • 安装核心依赖
    • pip 会根据 setup.py 中的 install_requires 列表安装包的核心依赖
    • 这些依赖是包运行所必须的
  • 可选依赖
    • 如果用户在命令中指定了可选依赖组(如 pip install .[dev]),pip 会安装对应的 extras_require 可选依赖
    • 否则,默认情况下不会安装 extras_require 中的可选依赖

第三步:安装包**

  • 导入包和模块
    • pip 会将构建好的包安装到Python环境的 site-packages 目录中
    • 包中的所有模块和子包都会被导入
  • 包数据文件
    • 如果 setup.py 中设置了 include_package_data=True 或指定了 package_data,这些数据文件也会被安装
  • 命令行工具
    • 如果 setup.py 中定义了 entry_points,对应的命令行工具会被安装并注册

第四步:打包内容

  • 包和模块
    • 所有通过 packages 或 find_packages() 指定的包和模块
  • 数据文件
    • 通过 package_data 或 include_package_data 指定的额外数据文件
  • 编译文件
    • 如果包中包含 C/C++ 扩展模块,这些模块会被编译并打包

附录:setup.py 文件的示例

  • setup.py 文件是 Python 项目中用于定义包的元数据、依赖关系及其他安装相关信息的脚本
  • setup.py 文件主要通过 setuptools 或 distutils 库来实现包的配置和分发

setup.py 文件示例

  • setup.py 文件基本结构示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    from setuptools import setup, find_packages

    setup(
    name="package_name", # 包名
    version="0.1.0", # 版本号
    author="Author Name", # 作者
    author_email="author@example.com", # 作者邮箱
    description="A short description", # 简短描述
    long_description=open("README.md").read(), # 长描述(通常从README文件导入)
    long_description_content_type="text/markdown", # 长描述类型(如Markdown)
    url="https://github.com/username/repo", # 项目URL
    packages=find_packages(), # 自动发现并包含所有包
    classifiers=[ # 分类标签(如开发状态、受众、许可证)
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    ],
    python_requires='>=3.6', # Python版本要求
    install_requires=[ # 安装依赖
    "requests>=2.25.0",
    "numpy",
    ],
    extras_require={ # 可选依赖(如开发、测试)
    "dev": ["pytest", "sphinx"],
    },
    entry_points={ # 命令行工具入口
    "console_scripts": [
    "mycommand=mypackage.module:function",
    ],
    },
    include_package_data=True, # 包含包内数据文件
    package_data={ # 指定包数据文件
    "mypackage": ["data/*.dat"],
    },
    zip_safe=False, # 是否允许打包为zip文件
    )

主要内容和功能说明

  • 元数据:
    • name : 包的名称
    • version : 包的版本号,通常遵循语义版本规范(如 MAJOR.MINOR.PATCH)
    • author 和 author_email : 作者的姓名和联系邮箱
    • description 和 long_description : 包的简短和详细描述
    • url : 项目的主页或代码仓库地址
  • 包和模块:
    • packages : 指定需要包含的包列表,通常使用 find_packages() 自动发现
    • package_data : 指定需要包含的非代码文件(如数据文件、模板等)
  • 依赖管理:
    • install_requires : 安装包时需要满足的依赖列表
    • extras_require : 可选依赖,按功能分组(如开发依赖、测试依赖)
  • Python 版本:
    • python_requires : 指定包支持的Python版本范围
  • 分类标签:
    • classifiers : 用于描述包的特性和分类,帮助用户和工具识别包的适用性
  • 命令行工具:
    • entry_points : 定义包提供的命令行工具及其入口函数
  • 数据文件:
    • include_package_data : 是否包含包内的额外数据文件
  • 打包选项:
    • zip_safe : 指定包是否可以安全地打包为zip文件

附录:setup.py 中的 extras_require 使用

  • 在 setup.py 中定义的 extras_require 提供了一种机制,可以按需安装额外的依赖项

  • extras_require 允许定义一些可选的依赖组,比如开发依赖、测试依赖等

  • 举例来说,前面附录小节的示例中 extras_require 中定义了一个名为 “dev” 的组和对应的依赖

  • 默认情况下 ,安装包时不会自动安装 extras_require 中定义的可选依赖项

    • 直接使用 pip install . 或 pip install package_name 安装包即可
    • 在这种情况下,只有 install_requires 中定义的核心依赖会被安装
  • 安装可选依赖,以 安装 “dev” 组的可选依赖 为例:

    • 使用 pip 安装时,可以通过在包名后加上 [dev] 来指定安装这些可选依赖

      1
      pip install .[dev]
    • 如果包已经发布到PyPI或其他包管理平台,可以使用以下命令:

      1
      pip install package_name[dev]
1…525354…64
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

631 posts
53 tags
GitHub E-Mail
© 2026 Joe Zhou
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4