rank_objective.hpp 9 KB
Newer Older
1
2
3
4
/*!
 * Copyright (c) 2016 Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE file in the project root for license information.
 */
Guolin Ke's avatar
Guolin Ke committed
5
6
7
8
#ifndef LIGHTGBM_OBJECTIVE_RANK_OBJECTIVE_HPP_
#define LIGHTGBM_OBJECTIVE_RANK_OBJECTIVE_HPP_

#include <LightGBM/metric.h>
9
#include <LightGBM/objective_function.h>
Guolin Ke's avatar
Guolin Ke committed
10

11
12
13
14
#include <limits>
#include <string>
#include <algorithm>
#include <cmath>
Guolin Ke's avatar
Guolin Ke committed
15
16
17
18
19
20
#include <cstdio>
#include <cstring>
#include <vector>

namespace LightGBM {
/*!
21
* \brief Objective function for Lambdrank with NDCG
Guolin Ke's avatar
Guolin Ke committed
22
23
*/
class LambdarankNDCG: public ObjectiveFunction {
Nikita Titov's avatar
Nikita Titov committed
24
 public:
Guolin Ke's avatar
Guolin Ke committed
25
  explicit LambdarankNDCG(const Config& config) {
26
    sigmoid_ = static_cast<double>(config.sigmoid);
27
    norm_ = config.lambdamart_norm;
Guolin Ke's avatar
Guolin Ke committed
28
    label_gain_ = config.label_gain;
Guolin Ke's avatar
Guolin Ke committed
29
    // initialize DCG calculator
Guolin Ke's avatar
Guolin Ke committed
30
31
    DCGCalculator::DefaultLabelGain(&label_gain_);
    DCGCalculator::Init(label_gain_);
Guolin Ke's avatar
Guolin Ke committed
32
33
    // will optimize NDCG@optimize_pos_at_
    optimize_pos_at_ = config.max_position;
Guolin Ke's avatar
Guolin Ke committed
34
35
    sigmoid_table_.clear();
    inverse_max_dcgs_.clear();
Guolin Ke's avatar
Guolin Ke committed
36
    if (sigmoid_ <= 0.0) {
37
      Log::Fatal("Sigmoid param %f should be greater than zero", sigmoid_);
Guolin Ke's avatar
Guolin Ke committed
38
39
    }
  }
40
41
42
43

  explicit LambdarankNDCG(const std::vector<std::string>&) {
  }

Guolin Ke's avatar
Guolin Ke committed
44
45
46
47
48
49
  ~LambdarankNDCG() {
  }
  void Init(const Metadata& metadata, data_size_t num_data) override {
    num_data_ = num_data;
    // get label
    label_ = metadata.label();
50
    DCGCalculator::CheckLabel(label_, num_data_);
Guolin Ke's avatar
Guolin Ke committed
51
52
53
54
55
    // get weights
    weights_ = metadata.weights();
    // get boundries
    query_boundaries_ = metadata.query_boundaries();
    if (query_boundaries_ == nullptr) {
56
      Log::Fatal("Lambdarank tasks require query information");
Guolin Ke's avatar
Guolin Ke committed
57
58
    }
    num_queries_ = metadata.num_queries();
Hui Xue's avatar
Hui Xue committed
59
    // cache inverse max DCG, avoid computation many times
Guolin Ke's avatar
Guolin Ke committed
60
    inverse_max_dcgs_.resize(num_queries_);
Guolin Ke's avatar
Guolin Ke committed
61
#pragma omp parallel for schedule(static)
Guolin Ke's avatar
Guolin Ke committed
62
    for (data_size_t i = 0; i < num_queries_; ++i) {
63
      inverse_max_dcgs_[i] = DCGCalculator::CalMaxDCGAtK(optimize_pos_at_,
Guolin Ke's avatar
Guolin Ke committed
64
        label_ + query_boundaries_[i],
65
        query_boundaries_[i + 1] - query_boundaries_[i]);
Guolin Ke's avatar
Guolin Ke committed
66
67
68
69
70
71
72
73
74

      if (inverse_max_dcgs_[i] > 0.0) {
        inverse_max_dcgs_[i] = 1.0f / inverse_max_dcgs_[i];
      }
    }
    // construct sigmoid table to speed up sigmoid transform
    ConstructSigmoidTable();
  }

75
76
  void GetGradients(const double* score, score_t* gradients,
                    score_t* hessians) const override {
Guolin Ke's avatar
Guolin Ke committed
77
78
79
80
81
82
    #pragma omp parallel for schedule(guided)
    for (data_size_t i = 0; i < num_queries_; ++i) {
      GetGradientsForOneQuery(score, gradients, hessians, i);
    }
  }

83
  inline void GetGradientsForOneQuery(const double* score,
84
              score_t* lambdas, score_t* hessians, data_size_t query_id) const {
Guolin Ke's avatar
Guolin Ke committed
85
86
87
88
89
    // get doc boundary for current query
    const data_size_t start = query_boundaries_[query_id];
    const data_size_t cnt =
      query_boundaries_[query_id + 1] - query_boundaries_[query_id];
    // get max DCG on current query
90
    const double inverse_max_dcg = inverse_max_dcgs_[query_id];
Guolin Ke's avatar
Guolin Ke committed
91
    // add pointers with offset
92
    const label_t* label = label_ + start;
Guolin Ke's avatar
Guolin Ke committed
93
94
95
96
97
98
99
100
101
102
103
104
105
    score += start;
    lambdas += start;
    hessians += start;
    // initialize with zero
    for (data_size_t i = 0; i < cnt; ++i) {
      lambdas[i] = 0.0f;
      hessians[i] = 0.0f;
    }
    // get sorted indices for scores
    std::vector<data_size_t> sorted_idx;
    for (data_size_t i = 0; i < cnt; ++i) {
      sorted_idx.emplace_back(i);
    }
106
107
    std::stable_sort(sorted_idx.begin(), sorted_idx.end(),
                     [score](data_size_t a, data_size_t b) { return score[a] > score[b]; });
108
109
110
111
112
113
114
115
    // get best and worst score	
    const double best_score = score[sorted_idx[0]];
    data_size_t worst_idx = cnt - 1;
    if (worst_idx > 0 && score[sorted_idx[worst_idx]] == kMinScore) {
      worst_idx -= 1;
    }
    const double worst_score = score[sorted_idx[worst_idx]];
    double sum_lambdas = 0.0;
Guolin Ke's avatar
Guolin Ke committed
116
117
118
119
    // start accmulate lambdas by pairs
    for (data_size_t i = 0; i < cnt; ++i) {
      const data_size_t high = sorted_idx[i];
      const int high_label = static_cast<int>(label[high]);
120
      const double high_score = score[high];
Guolin Ke's avatar
Guolin Ke committed
121
      if (high_score == kMinScore) { continue; }
122
123
124
125
      const double high_label_gain = label_gain_[high_label];
      const double high_discount = DCGCalculator::GetDiscount(i);
      double high_sum_lambda = 0.0;
      double high_sum_hessian = 0.0;
Guolin Ke's avatar
Guolin Ke committed
126
127
128
129
130
131
      for (data_size_t j = 0; j < cnt; ++j) {
        // skip same data
        if (i == j) { continue; }

        const data_size_t low = sorted_idx[j];
        const int low_label = static_cast<int>(label[low]);
132
        const double low_score = score[low];
Guolin Ke's avatar
Guolin Ke committed
133
134
135
        // only consider pair with different label
        if (high_label <= low_label || low_score == kMinScore) { continue; }

136
        const double delta_score = high_score - low_score;
Guolin Ke's avatar
Guolin Ke committed
137

138
139
        const double low_label_gain = label_gain_[low_label];
        const double low_discount = DCGCalculator::GetDiscount(j);
Guolin Ke's avatar
Guolin Ke committed
140
        // get dcg gap
141
        const double dcg_gap = high_label_gain - low_label_gain;
Guolin Ke's avatar
Guolin Ke committed
142
        // get discount of this pair
143
        const double paired_discount = fabs(high_discount - low_discount);
Guolin Ke's avatar
Guolin Ke committed
144
        // get delta NDCG
145
        double delta_pair_NDCG = dcg_gap * paired_discount * inverse_max_dcg;
146
147
148
149
        // regular the delta_pair_NDCG by score distance	
        if (norm_ && high_label != low_label && best_score != worst_score) {
          delta_pair_NDCG /= (0.01f + fabs(delta_score));
        }
Guolin Ke's avatar
Guolin Ke committed
150
        // calculate lambda for this pair
151
        double p_lambda = GetSigmoid(delta_score);
152
        double p_hessian = p_lambda * (1.0f - p_lambda);
Guolin Ke's avatar
Guolin Ke committed
153
        // update
154
155
        p_lambda *= -sigmoid_ * delta_pair_NDCG;
        p_hessian *= sigmoid_ * sigmoid_ * delta_pair_NDCG;
Guolin Ke's avatar
Guolin Ke committed
156
157
        high_sum_lambda += p_lambda;
        high_sum_hessian += p_hessian;
158
159
        lambdas[low] -= static_cast<score_t>(p_lambda);
        hessians[low] += static_cast<score_t>(p_hessian);
160
161
        // lambda is negative, so use minus to accumulate
        sum_lambdas -= 2 * p_lambda;
Guolin Ke's avatar
Guolin Ke committed
162
163
      }
      // update
164
165
      lambdas[high] += static_cast<score_t>(high_sum_lambda);
      hessians[high] += static_cast<score_t>(high_sum_hessian);
Guolin Ke's avatar
Guolin Ke committed
166
    }
167
168
169
170
171
172
173
    if (norm_ && sum_lambdas > 0) {
      double norm_factor = std::log2(1 + sum_lambdas) / sum_lambdas;
      for (data_size_t i = 0; i < cnt; ++i) {
        lambdas[i] = static_cast<score_t>(lambdas[i] * norm_factor);
        hessians[i] = static_cast<score_t>(hessians[i] * norm_factor);
      }
    }
Guolin Ke's avatar
Guolin Ke committed
174
175
176
    // if need weights
    if (weights_ != nullptr) {
      for (data_size_t i = 0; i < cnt; ++i) {
177
178
        lambdas[i] = static_cast<score_t>(lambdas[i] * weights_[start + i]);
        hessians[i] = static_cast<score_t>(hessians[i] * weights_[start + i]);
Guolin Ke's avatar
Guolin Ke committed
179
180
181
182
183
      }
    }
  }


184
  inline double GetSigmoid(double score) const {
Guolin Ke's avatar
Guolin Ke committed
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
    if (score <= min_sigmoid_input_) {
      // too small, use lower bound
      return sigmoid_table_[0];
    } else if (score >= max_sigmoid_input_) {
      // too big, use upper bound
      return sigmoid_table_[_sigmoid_bins - 1];
    } else {
      return sigmoid_table_[static_cast<size_t>((score - min_sigmoid_input_) * sigmoid_table_idx_factor_)];
    }
  }

  void ConstructSigmoidTable() {
    // get boundary
    min_sigmoid_input_ = min_sigmoid_input_ / sigmoid_ / 2;
    max_sigmoid_input_ = -min_sigmoid_input_;
Guolin Ke's avatar
Guolin Ke committed
200
    sigmoid_table_.resize(_sigmoid_bins);
Guolin Ke's avatar
Guolin Ke committed
201
202
203
204
205
    // get score to bin factor
    sigmoid_table_idx_factor_ =
      _sigmoid_bins / (max_sigmoid_input_ - min_sigmoid_input_);
    // cache
    for (size_t i = 0; i < _sigmoid_bins; ++i) {
206
      const double score = i / sigmoid_table_idx_factor_ + min_sigmoid_input_;
207
      sigmoid_table_[i] = 1.0f / (1.0f + std::exp(score * sigmoid_));
Guolin Ke's avatar
Guolin Ke committed
208
209
210
    }
  }

Guolin Ke's avatar
Guolin Ke committed
211
212
  const char* GetName() const override {
    return "lambdarank";
Guolin Ke's avatar
Guolin Ke committed
213
214
  }

215
216
217
218
219
220
  std::string ToString() const override {
    std::stringstream str_buf;
    str_buf << GetName();
    return str_buf.str();
  }

221
222
  bool NeedAccuratePrediction() const override { return false; }

Nikita Titov's avatar
Nikita Titov committed
223
 private:
Guolin Ke's avatar
Guolin Ke committed
224
  /*! \brief Gains for labels */
225
  std::vector<double> label_gain_;
Guolin Ke's avatar
Guolin Ke committed
226
  /*! \brief Cache inverse max DCG, speed up calculation */
227
  std::vector<double> inverse_max_dcgs_;
Guolin Ke's avatar
Guolin Ke committed
228
  /*! \brief Simgoid param */
229
  double sigmoid_;
230
231
  /*! \brief Normalize the lambdas or not */
  bool norm_;
Guolin Ke's avatar
Guolin Ke committed
232
233
234
235
236
237
238
  /*! \brief Optimized NDCG@ */
  int optimize_pos_at_;
  /*! \brief Number of queries */
  data_size_t num_queries_;
  /*! \brief Number of data */
  data_size_t num_data_;
  /*! \brief Pointer of label */
239
  const label_t* label_;
Guolin Ke's avatar
Guolin Ke committed
240
  /*! \brief Pointer of weights */
241
  const label_t* weights_;
Guolin Ke's avatar
Guolin Ke committed
242
243
244
  /*! \brief Query boundries */
  const data_size_t* query_boundaries_;
  /*! \brief Cache result for sigmoid transform to speed up */
245
  std::vector<double> sigmoid_table_;
Guolin Ke's avatar
Guolin Ke committed
246
247
248
  /*! \brief Number of bins in simoid table */
  size_t _sigmoid_bins = 1024 * 1024;
  /*! \brief Minimal input of sigmoid table */
249
  double min_sigmoid_input_ = -50;
Guolin Ke's avatar
Guolin Ke committed
250
  /*! \brief Maximal input of sigmoid table */
251
  double max_sigmoid_input_ = 50;
Guolin Ke's avatar
Guolin Ke committed
252
  /*! \brief Factor that covert score to bin in sigmoid table */
253
  double sigmoid_table_idx_factor_;
Guolin Ke's avatar
Guolin Ke committed
254
255
256
};

}  // namespace LightGBM
Guolin Ke's avatar
Guolin Ke committed
257
#endif   // LightGBM_OBJECTIVE_RANK_OBJECTIVE_HPP_