Lightgbm 直方图优化算法深入理解

2023-11-10

一、概述

在之前的介绍Xgboost的众多博文中,已经介绍过,在树分裂计算分裂特征的增益时,xgboost 采用了预排序的方法来处理节点分裂,这样计算的分裂点比较精确。但是,也造成了很大的时间开销。为了解决这个问题,Lightgbm 选择了基于 histogram 的决策树算法。相比于 pre-sorted算法,histogram 在内存消耗和计算代价上都有不少优势。

histogram算法简单来说,就是先对特征值进行装箱处理,形成一个一个的bins。对于连续特征来说,装箱处理就是特征工程中的离散化:如[0,0.3)—>0,[0.3,0.7)—->1等。在Lightgbm中默认的#bins为256(1个字节的能表示的长度,可以设置)。对于分类特征来说,则是每一种取值放入一个bin,且当取值的个数大于max bin数时,会忽略那些很少出现的category值。

在节点分裂的时候,这时候就不需要按照预排序算法那样,对于每个特征都计算#data遍了,而是只需要计算#bins遍,这样就大大加快了训练速度。

二、算法流程

下面是训练过程中利用直方图寻找最佳分割点的算法。

从算法中可以看到:直方图优化算法需要在训练前预先把特征值转化为bin value,也就是对每个特征的取值做个分段函数,将所有样本在该特征上的取值划分到某一段(bin)中。最终把特征取值从连续值转化成了离散值。需要注意得是:feature value对应的bin value在整个训练过程中是不会改变的

最外面的 for 循环表示的意思是对当前模型下所有的叶子节点处理,需要遍历所有的特征,来找到增益最大的特征及其划分值,以此来分裂该叶子节点。

在某个叶子上,第二个 for 循环就开始遍历所有的特征了。对于每个特征,首先为其创建一个直方图。这个直方图存储了两类信息,分别是每个bin中样本的梯度之和( H [ f . b i n s [ i ] ] . g H[ f.bins[i] ].g H[f.bins[i]].g),还有就是每个bin中样本数量( H [ f . b i n s [ i ] ] . n H[ f.bins[i] ].n H[f.bins[i]].n

第三个 for 循环遍历所有样本,累积上述的两类统计值到样本所属的bin中。即直方图的每个 bin 中包含了一定的样本,在此计算每个 bin 中的样本的梯度之和并对 bin 中的样本记数。

最后一个for循环,遍历所有bin,分别以当前bin作为分割点,累加其左边的bin至当前bin的梯度和( S L S_L SL)以及样本数量( n L n_L nL),并与父节点上的总梯度和( S p S_p Sp)以及总样本数量( n p n_p np)相减,得到右边所有bin的梯度和( S R S_R SR)以及样本数量( n R n_R nR),带入公式,计算出增益,在遍历过程中取最大的增益,以此时的特征和bin的特征值作为分裂节点的特征和分裂特征取值。

三、源码分析

从上面的分析中,我们可以看到,有几个关键问题需要解决:

①如何将特征映射到bin呢?即如何分桶?对于连续特征和类别特征分别怎么样处理?

②如何构建直方图?直方图算法累加的g是什么?难道没有二阶导数h吗?

3.1 特征分桶

特征分桶的源码在bin.cpp文件和bin.h文件中。

由于LGBM可以处理类别特征,因此对连续特征和类别特征的处理方式是不一样的。

3.1.1 连续特征

bin.cpp中,我们可以看到GreedyFindBin函数和FindBinWithZeroAsOneBin函数,这两个函数得到了数值型特征取值(负数,0,正数)的各个bin的切分点,即bin_upper_bound。

源码如下,我加了许多注释便于理解:FindBinWithZeroAsOneBin函数调用了GreedyFindBin函数

/************************************************************************************
****************************得到数值型特征取值的各个bin的切分点*************************
*************************************************************************************/
  std::vector<double> GreedyFindBin(const double* distinct_values, const int* counts,
    int num_distinct_values, int max_bin, size_t total_cnt, int min_data_in_bin) {
    // counts为特征取值计数的数组;
    //distinct_values为特征的不同的取值的数组;num_distinct_values为特征有多少个不同的取值。


    // bin_upper_bound就是记录桶分界的数组
    std::vector<double> bin_upper_bound;
    CHECK(max_bin > 0);

    // 特征取值数比max_bin数量少,直接取distinct_values的中点放置
    if (num_distinct_values <= max_bin) {
      bin_upper_bound.clear();
      int cur_cnt_inbin = 0;
      for (int i = 0; i < num_distinct_values - 1; ++i) {
        cur_cnt_inbin += counts[i];
        // min_data_in_bin默认为3。
        // 若一个特征的取值比min_data_in_bin小,则累积下一个取值,直到比min_data_in_bin大,进入循环。
        if (cur_cnt_inbin >= min_data_in_bin) {
          // 取当前值和下一个值的均值作为该桶的分界点bin_upper_bound
          auto val = Common::GetDoubleUpperBound((distinct_values[i] + distinct_values[i + 1]) / 2.0);
          // CheckDoubleEqualOrdered返回真的条件:val<bin_upper_bound.back()。注意前面有个!。
          if (bin_upper_bound.empty() || !Common::CheckDoubleEqualOrdered(bin_upper_bound.back(), val)) {
            bin_upper_bound.push_back(val); //插入到最后
            cur_cnt_inbin = 0;
          }
        }
      }
      // 对于最后一个桶的上界则为无穷大
      cur_cnt_inbin += counts[num_distinct_values - 1];
      bin_upper_bound.push_back(std::numeric_limits<double>::infinity());
    } 
    // 特征取值数比max_bin来得大,说明几个特征值要共用一个bin
    else {
      if (min_data_in_bin > 0) {
        max_bin = std::min(max_bin, static_cast<int>(total_cnt / min_data_in_bin));
        max_bin = std::max(max_bin, 1);
      }
      // mean size for one bin
      double mean_bin_size = static_cast<double>(total_cnt) / max_bin;

      int rest_bin_cnt = max_bin;
      int rest_sample_cnt = static_cast<int>(total_cnt);
      // 定义is_big_count_value数组:初始设定特征每一个不同的值的数量都小(false)。
      std::vector<bool> is_big_count_value(num_distinct_values, false);

      // 如果一个特征值的数目比mean_bin_size大,那么这些特征需要单独一个bin
      for (int i = 0; i < num_distinct_values; ++i) {
        // 如果一个特征值的数目比mean_bin_size大,则设定这个特征值对应的is_big_count_value为真。。
        if (counts[i] >= mean_bin_size) {
          is_big_count_value[i] = true;
          --rest_bin_cnt;
          rest_sample_cnt -= counts[i];
        }
      }

      // 剩下的特征取值的样本数平均每个剩下的bin:mean size for one bin
      mean_bin_size = static_cast<double>(rest_sample_cnt) / rest_bin_cnt;
      std::vector<double> upper_bounds(max_bin, std::numeric_limits<double>::infinity());
      std::vector<double> lower_bounds(max_bin, std::numeric_limits<double>::infinity());

      int bin_cnt = 0;
      lower_bounds[bin_cnt] = distinct_values[0];
      int cur_cnt_inbin = 0;
      // 重新遍历所有的特征值(包括数目大和数目小的)
      for (int i = 0; i < num_distinct_values - 1; ++i) {
        // 如果当前的特征值数目是小的
        if (!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] || cur_cnt_inbin >= mean_bin_size ||
          (is_big_count_value[i + 1] && cur_cnt_inbin >= std::max(1.0, mean_bin_size * 0.5f))) {
          upper_bounds[bin_cnt] = distinct_values[i]; // 第i个bin的最大就是 distinct_values[i]了
          ++bin_cnt;
          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 (!is_big_count_value[i]) {
            --rest_bin_cnt;
            mean_bin_size = rest_sample_cnt / static_cast<double>(rest_bin_cnt);
          }
        }
      }
      ++bin_cnt;
      // update bin upper bound 与特征取值数比max_bin数量少的操作类似,取当前值和下一个值的均值作为该桶的分界点
      bin_upper_bound.clear();
      for (int i = 0; i < bin_cnt - 1; ++i) {
        auto val = Common::GetDoubleUpperBound((upper_bounds[i] + lower_bounds[i + 1]) / 2.0);
        if (bin_upper_bound.empty() || !Common::CheckDoubleEqualOrdered(bin_upper_bound.back(), val)) {
          bin_upper_bound.push_back(val);
        }
      }
      // last bin upper bound
      bin_upper_bound.push_back(std::numeric_limits<double>::infinity());
    }
    // bin_upper_bound即数值型特征取值的各个bin的切分点
    return bin_upper_bound;
  }

/************************************************************************************
********************得数值型特征取值(负数,0,正数)的各个bin的切分点*******************
*************************************************************************************/
  std::vector<double> FindBinWithZeroAsOneBin(const double* distinct_values, const int* counts,
    int num_distinct_values, int max_bin, size_t total_sample_cnt, int min_data_in_bin) {
    std::vector<double> bin_upper_bound;
    // left_cnt_data记录小于0的值
    int left_cnt_data = 0;
    int cnt_zero = 0;
    // right_cnt_data记录大于0的值
    int right_cnt_data = 0;
    for (int i = 0; i < num_distinct_values; ++i) {
      //  double kZeroThreshold = 1e-35f
      if (distinct_values[i] <= -kZeroThreshold) {
        left_cnt_data += counts[i];
      } else if (distinct_values[i] > kZeroThreshold) {
        right_cnt_data += counts[i];
      } else {
        cnt_zero += counts[i];
      }
    }

    //如果特征值里存在0和正数,则left_cnt不为-1,则left_cnt是最后一个负数的位置
    int left_cnt = -1;
    for (int i = 0; i < num_distinct_values; ++i) {
      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的桶。
      int left_max_bin = static_cast<int>(static_cast<double>(left_cnt_data) / (total_sample_cnt - cnt_zero) * (max_bin - 1));
      left_max_bin = std::max(1, left_max_bin);
      bin_upper_bound = GreedyFindBin(distinct_values, counts, left_cnt, left_max_bin, left_cnt_data, min_data_in_bin);
      // 负数桶的分界点最后一个自然是-kZeroThreshold
      bin_upper_bound.back() = -kZeroThreshold;
    }

    //如果特征值存在正数,则right_start不为-1,则right_start是第一个正数开始的位置
    int right_start = -1;
    for (int i = left_cnt; i < num_distinct_values; ++i) {
      if (distinct_values[i] > kZeroThreshold) {
        right_start = i;
        break;
      }
    }
    // 如果特征值里存在正数
    if (right_start >= 0) {
      int right_max_bin = max_bin - 1 - static_cast<int>(bin_upper_bound.size());
      CHECK(right_max_bin > 0);
      auto 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.push_back(kZeroThreshold);
      // 插入正数桶的分界点,形成最终的分界点数组。
      bin_upper_bound.insert(bin_upper_bound.end(), right_bounds.begin(), right_bounds.end());
    } else {
      bin_upper_bound.push_back(std::numeric_limits<double>::infinity());
    }

    // bin_upper_bound即数值型特征取值(负数,0,正数)的各个bin的切分点
    return bin_upper_bound;
  }

上述的代码找到了数值型特征取值的各个bin的切分点,即bin_upper_bound,之后只需要根据这个对特征的取值查找其相应的bin中即可(用二分搜索)二分搜索的代码在bin.h里的BinMapper::ValueToBin(double value) 函数

3.1.2 类别特征

阅读上述的代码,我们可以看到在上述代码寻找Bind的切分点时我们需要一些关键信息:如 counts为特征取值计数的数组;distinct_values为特征的不同的取值的数组;num_distinct_values为特征有多少个不同的取值。
这些信息不仅连续特征需要,类别特征也需要。

因此在调用FindBinWithZeroAsOneBin函数之前,我们必须先计算出这些关键信息,在bin.cpp中有函数BinMapper::FindBin

在这个函数太长了,我就不全部贴出来了。在这个函数的前半段有:

    //从小到大排序values数组,从第0个到num_sample_values个。
    std::stable_sort(values, values + num_sample_values);

    // push zero in the front
    // 如果最小的特征值大于0且存在0,则把0放到distinct_values的第一位。或者全部是NaN数据。
    if (num_sample_values == 0 || (values[0] > 0.0f && zero_cnt > 0)) {
      distinct_values.push_back(0.0f);
      counts.push_back(zero_cnt);
    }

    if (num_sample_values > 0) {
      distinct_values.push_back(values[0]);
      counts.push_back(1);
    }

    for (int i = 1; i < num_sample_values; ++i) {
      // 如果values[i - 1]小于values[i]
      if (!Common::CheckDoubleEqualOrdered(values[i - 1], values[i])) {
        if (values[i - 1] < 0.0f && values[i] > 0.0f) {
          distinct_values.push_back(0.0f);
          counts.push_back(zero_cnt);
        }
        distinct_values.push_back(values[i]);
        counts.push_back(1);
      } else {
        // use the large value
        // 如果values[i - 1]不小于values[i],即只可能values[i - 1]等于values[i],说明distinct_values已经有了这个值了,只需要把它的counts加1.
        distinct_values.back() = values[i];
        ++counts.back();
      }
    }

可以看到已经统计了特征的distinct_values和对应的counts了。

接下来,该函数计算了类别特征的bin。

// sort by counts 特征取值按出现的次数排序(大到小)
        Common::SortForPair<int, int>(counts_int, distinct_values_int, 0, true);
        // avoid first bin is zero
        if (distinct_values_int[0] == 0) {
          if (counts_int.size() == 1) {
            counts_int.push_back(0);
            distinct_values_int.push_back(distinct_values_int[0] + 1);
          }
          // 交换counts_int[0]和counts_int[1]的值
          std::swap(counts_int[0], counts_int[1]);
          std::swap(distinct_values_int[0], distinct_values_int[1]);
        }
        // will ignore the categorical of small counts
        int cut_cnt = static_cast<int>((total_sample_cnt - na_cnt) * 0.99f);
        size_t cur_cat = 0;
        // categorical_2_bin_ (unordered_map类型) 将特征取值到哪个bin和一一对应起来
        categorical_2_bin_.clear();
        // bin_2_categorical_(vector类型)记录bin对应的特征取值
        bin_2_categorical_.clear();
        int used_cnt = 0;
        max_bin = std::min(static_cast<int>(distinct_values_int.size()), max_bin);
        cnt_in_bin.clear();
        // 类别特征值已经按数量从大到小排列,累积特征值的数目,放弃后1%的类别特征值,即忽略一些出现次数很少的特征取值
        while (cur_cat < distinct_values_int.size()
               && (used_cnt < cut_cnt || num_bin_ < max_bin)) {
          if (counts_int[cur_cat] < min_data_in_bin && cur_cat > 1) {
            break;
          }
          //为bin_2_categorical_和categorical_2_bin_赋值
          bin_2_categorical_.push_back(distinct_values_int[cur_cat]);
          categorical_2_bin_[distinct_values_int[cur_cat]] = static_cast<unsigned int>(num_bin_);
          used_cnt += counts_int[cur_cat];
          cnt_in_bin.push_back(counts_int[cur_cat]);
          ++num_bin_;
          ++cur_cat;
        }

关键点有:

首先对特征取值按出现的次数排序(大到小),

忽略一些出现次数很少的特征取值,

然后用bin_2_categorical_(vector类型)记录bin对应的特征取值,以及用categorical_2_bin_(unordered_map类型) 将特征取值到哪个bin和一一对应起来。这样,以后就能很方便的进行bin到特征取值和特征取值到bin的转化。

3.1.3 总结

我们介绍了3个函数,分别是FindBin,FindBinWithZeroAsOneBin,GreedyFindBin。这3个函数的调用顺序为:
FindBin→FindBinWithZeroAsOneBin→GreedyFindBin

另外,在dataset_loader.cpp文件中CostructFromSampleData函数调用了FindBin函数,依次处理每一个特征,即可以求出每一个特征如何转换成bin,无论其是连续特征还是类别特征。

3.2 构建直方图

给定一个特征的值,我们现在已经可以转化为对应的bin了。现在我们就可以构建直方图了。

ConstructHistogram函数即为关键代码如下(这个函数出现在很多文件中):

  void ConstructHistogram(const data_size_t* data_indices, data_size_t num_data,
                          const score_t* ordered_gradients, const score_t* ordered_hessians,
                          HistogramBinEntry* out) const override {
    // num_data与0011进行与,即num_data不会大于3
    const data_size_t rest = num_data & 0x3;
    data_size_t i = 0;
    for (; i < num_data - rest; i += 4) {
      const VAL_T bin0 = data_[data_indices[i]];
      const VAL_T bin1 = data_[data_indices[i + 1]];
      const VAL_T bin2 = data_[data_indices[i + 2]];
      const VAL_T bin3 = data_[data_indices[i + 3]];

      out[bin0].sum_gradients += ordered_gradients[i];
      out[bin1].sum_gradients += ordered_gradients[i + 1];
      out[bin2].sum_gradients += ordered_gradients[i + 2];
      out[bin3].sum_gradients += ordered_gradients[i + 3];

      out[bin0].sum_hessians += ordered_hessians[i];
      out[bin1].sum_hessians += ordered_hessians[i + 1];
      out[bin2].sum_hessians += ordered_hessians[i + 2];
      out[bin3].sum_hessians += ordered_hessians[i + 3];

      ++out[bin0].cnt;
      ++out[bin1].cnt;
      ++out[bin2].cnt;
      ++out[bin3].cnt;
    }
    for (; i < num_data; ++i) {
      const VAL_T bin = data_[data_indices[i]];
      out[bin].sum_gradients += ordered_gradients[i];
      out[bin].sum_hessians += ordered_hessians[i];
      ++out[bin].cnt;
    }
  }

可以看到,累加了一阶和二阶梯度和还有个数。(当然还有其它的版本,当is_constant_hessian为true的时候是不用二阶梯度的)

注:这个函数我有点疑问,为什么要与0x3进行与操作呢?这个的目的是什么?难道是为了快吗?另外,由于水平原因,在调用CostructFromSampleData函数的前后关系上,我已经蒙圈了,暂时先放弃了。希望有好心的读者能给讲讲。

四、histogram算法与 pre-sorted算法对比

3.1 优势

  • Pre-sorted 算法需要的内存约是训练数据的两倍(2 * #data * #features* 4Bytes),它需要用32位浮点(4Bytes)来保存 feature value,并且对每一列特征,都需要一个额外的排好序的索引,这也需要32位(4Bytes)的存储空间。因此是(2 * #data * #features* 4Bytes)。而对于 histogram 算法,则只需要(#data * #features * 1Bytes)的内存消耗,仅为 pre-sorted算法的1/8。因为 histogram 算法仅需要存储 feature bin value (离散化后的数值),不需要原始的 feature value,也不用排序,而 bin value 用 1Bytes(256 bins) 的大小一般也就足够了。

  • 计算上的优势则是大幅减少了计算分割点增益的次数。对于每一个特征,pre-sorted 需要对每一个不同特征值都计算一次分割增益,代价是O(#feature*#distinct_values_of_the_feature);而 histogram 只需要计算#bins次,代价是(#feature*#bins)。

  • 还有一个很重要的点是cache-miss。事实上,cache-miss对速度的影响是特别大的。预排序中有2个操作频繁的地方会造成cache miss,一是对梯度的访问,在计算gain的时候需要利用梯度,不同特征访问梯度的顺序都是不一样的,且是随机的,因此这部分会造成严重的cache-miss。二是对于索引表的访问,预排序使用了一个行号到叶子节点号的索引表(row_idx_to_tree_node_idx ),来防止数据切分时对所有的数据进行切分,即只对该叶子节点上的样本切分。在与level-wise进行结合的时候, 每一个叶子节点都要切分数据,这也是随机的访问。这样会带来严重的系统性能下降。而直方图算法则是天然的cache friendly。在直方图算法的第3个for循环的时候,就已经统计好了每个bin的梯度,因此,在计算gain的时候,只需要对bin进行访问,造成的cache-miss问题会小很多。

  • 最后,在数据并行的时候,用 histgoram 可以大幅降低通信代价。用 pre-sorted 算法的话,通信代价是非常大的(几乎是没办法用的)。所以 xgoobst 在并行的时候也使用 histogram 进行通信。
    (数据并行的优化是Lightgbm的令一个亮点,这里不是特别理解,需要再深入研究

3.2 劣势

histogram 算法不能找到很精确的分割点,训练误差没有 pre-sorted 好。但从实验结果来看, histogram 算法在测试集的误差和 pre-sorted 算法差异并不是很大,甚至有时候效果更好。实际上可能决策树对于分割点的精确程度并不太敏感,而且较“粗”的分割点也自带正则化的效果,再加上boosting算法本身就是弱分类器的集成。

五、直方图做差加速

在histogram算法上一个trick是histogram 做差加速。一个容易观察到的现象:一个叶子的直方图可以由它的父亲节点的直方图与它兄弟的直方图做差得到。利用这个方法,Lightgbm 可以在构造一个叶子(含有较少数据)的直方图后,可以用非常微小的代价得到它兄弟叶子(含有较多数据)的直方图。

因为构建兄弟叶子的直方图是做差得到的,时间复杂度仅为O(#bins),几乎可以忽略,因此,比起不做差得到的兄弟节点的直方图,在速度上可以提升一倍。

举例来说明什么是histogram 做差加速。

假设我们共有10个样本,2个特征。
特征 f 1 f_1 f1为类别特征,共有2个不同的属性值,分成了桶 b 11 b_{11} b11 b 12 b_{12} b12;桶 b 11 b_{11} b11的样本数是4个,桶 b 12 b_{12} b12的样本数是6个。
特征 f 2 f_2 f2为连续特征,离散化后分成了桶 b 21 b_{21} b21 b 22 b_{22} b22 b 23 b_{23} b23;桶 b 21 b_{21} b21的样本数是2个,桶 b 22 b_{22} b22的样本数是4个,桶 b 23 b_{23} b23的样本数是4个。

我们依次计算每个bin作为分割点的增益,假设在桶 b 11 b_{11} b11作为分割点时增益最大,那么以桶 b 11 b_{11} b11分割,这时候:

a. 左子节点有4个样本。特征 f 1 f_1 f1的桶 b 11 b_{11} b11的样本数为4个,桶 b 12 b_{12} b12样本为0个。假设特征 f 2 f_2 f2仍有3个桶 b 21 b_{21} b21 b 22 b_{22} b22 b 23 b_{23} b23,且桶 b 21 b_{21} b21的样本数是1个,桶 b 22 b_{22} b22的样本数是2个,桶 b 23 b_{23} b23的样本数是1个。这时候左子节点2个特征的直方图已经构建成功。

b. 左子节点有4个样本,右子节点自然有6个样本。这时候右子节点的2个特征的直方图就可以根据父节点和左子节点的2个特征的直方图做差得到:

特征 f 1 f_1 f1只有桶 b 12 b_{12} b12,且样本数为6个(6-0=0)。桶 b 11 b_{11} b11样本数为0个(4-4=0)。
特征 f 2 f_2 f2仍有3个桶 b 21 b_{21} b21 b 22 b_{22} b22 b 23 b_{23} b23,且桶 b 21 b_{21} b21的样本数是1个(2-1=1),桶 b 22 b_{22} b22的样本数是2个(4-2=2),桶 b 23 b_{23} b23的样本数是3个(4-1=3)。

这时候右子节点2个特征的直方图也已经构建成功。

下图表示了整个过程:
在这里插入图片描述

深入分析就可以知道,左子节点计算直方图的复杂度是基于样本个数的,而右子节点计算直方图的复杂度却是基于桶的个数的。因此,大大节省了构建直方图的时间。

五、参考文献

【1】如何看待微软新开源的LightGBM?

【2】LightGBM 直方图优化算法

【3】『我爱机器学习』集成学习(四)LightGBM

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Lightgbm 直方图优化算法深入理解 的相关文章

随机推荐

  • 深度学习正则化

    在设计机器学习算法时不仅要求在训练集上误差 且希望在新样本上 的泛化能 强 许多机器学习算法都采 相关的策略来减 测试误差 这 些策略被统称为正则化 因为神经 络的强 的表示能 经常遇到过拟 合 所以需要使 不同形式的正则化策略 正则化通过
  • JavaWeb-通过表格显示数据库的信息(jsp+mysql)

    login jsp h2 登录 h2 br
  • python+numpy+pandas数据类型+类/对象

    写代码时逻辑明确 但是被各种数据类型以及对象类型搞蒙了 补习并简单记录一下 在进行数据分析之前需要对数据进行数据处理 其中就包含转化数据格式 可以先查看数据信息 再依据分析需求对进行处理 编写python程序时各种数据类型以及对象的类型以及
  • 微信测试账号 (2)-消息验证sha1签名

    在第1篇中实现了收发微信消息 但是没有做验证 本篇将介绍微信如何使用sha签名 对消息进行认证 其中安全相关的概念 如sha1散列值 签名等 可参考web安全 1 验证参数 GetMapping handler public String
  • live555 server 搭建

    一 直接下载live555MediaServer可执行程序 二 Live555在linux平台上编译 下载源码包 http www live555 com liveMedia live555 latest tar gz 1 解压 2 生成M
  • CCPC-南阳比赛总结

    打铁归来 感触好多 记得英语课上 收到晓红老师的消息 说给我们争取下了国赛的名额 我们感觉好幸运 没想到最后打铁了 这个结果也不太意外 下面说下 这几天的行程 反思 下一步的目标 15号 淄博到济南 15 16号 济南 郑州 南阳 17号
  • 如何过滤 map

    获取 EntrySet 然后正常使用 stream 的 filter 过滤 Entry 最后再转为 Map 即可 对 map 过滤 filter Test public void testMapFilter Map
  • OCJP题库1Z0-851(21/30)

    这套题我参考了这篇文章 以及一些百度上找的内容 再加上自己的解释 总结下来的详解 第1题 1 Given a pre generics implementation of a method 11 public static int sum
  • 申请被拒模板 (六)

    这里只是模板 仅供学习 出现任何问题 与博主无关 Hi xxxxx We really appreciate the time and effort you took to connect with us and apply for the
  • nginx配置详解

    一 什么是nginx nginx是一款自由的 开源的 高性能的HTTP服务器和反向代理服务器 同时也是一个IMAP POP3 SMTP代理服务器 Nginx作为一个HTTP服务器进行网络的发布处理 另外Nginx可以作为反向代理进行负载均衡
  • 【数据分析】为什么要学习分析方法?

    为什么要学习分析方法 如果你有以下这些症状 没有数据分析意识 工作由拍脑袋决定 而不是靠数据分析来支持决策 统计时的数据分析 做了很多图表 却发现不了业务中存在的问题 只会使用工具的数据分析 谈起使用工具的技巧头头是道 但是面对问题 还是不
  • 用C++实现softmax函数(面试经验)

    背景 今天面试字节算法岗时被问到的问题 让我用C 实现一个softmax函数 softmax是逻辑回归在多分类问题上的推广 大概的公式如下 i n p u t
  • Unity的C#编程教程_56_Namespace 详解

    文章目录 Namespaces Tour of Namespaces Namespaces 命名空间使得我们可以组织和管理我们的代码库 假设我们设置一个脚本名叫 Weapon using System Collections using S
  • python整段代码注释-Python中注释(多行注释和单行注释)的用法实例

    Python中注释 多行注释和单行注释 的用法实例 发布时间 2020 09 30 23 18 32 来源 脚本之家 阅读 97 前言 学会向程序中添加必要的注释 也是很重要的 注释不仅可以用来解释程序某些部分的作用和功能 用自然语言描述代
  • main.c:9:21: fatal error: sqlite3.h: 没有那个文件或目录

    今天在 Ubuntu 里看别人代码时 头文件里面有个
  • 2023普华永道中国首席数据官调研

    导读 在中国2 500家最大的上市企业中 首席数据官或类似管理岗的渗透率仅为1 3 远低于全球27 的水平 首席数据官的推广任重道远 其中 金融行业和通讯 媒体与科技行业的首席数据官或类似管理岗的数量位居前两位 也与这几个行业的数字化转型发
  • 【100天精通Python】Day48:Python Web开发_WSGI网络服务器网关接口与使用

    目录 1 WSGI接口 1 1 CGI 简介 1 2 WSGI 简介 1 3 定义 WSGI 接口 1 3 1 应用程序 Application 1 3 2 服务器 Server 1 4 WSGI 接口的使用示例 1 5 WSGI接口的优势
  • bp网络拟合函数 matlab_基于RBF神经网络的曲线拟合

    目前 在人工神经网络的实际应用中 绝大部分的神经网络模型是采用误差逆传播 error BackPropagation BP 网络和它的变化形式径向基函数 Radial Basis Function RBF 神经网络 RBF网络是一种高效的前
  • 微信小程序使用image组件显示图片的方法

    本文实例讲述了微信小程序使用image组件显示图片的方法 分享给大家供大家参考 具体如下 1 效果展示 2 关键代码 index wxml 代码如下
  • Lightgbm 直方图优化算法深入理解

    一 概述 在之前的介绍Xgboost的众多博文中 已经介绍过 在树分裂计算分裂特征的增益时 xgboost 采用了预排序的方法来处理节点分裂 这样计算的分裂点比较精确 但是 也造成了很大的时间开销 为了解决这个问题 Lightgbm 选择了