/*! * Copyright 2015 by Contributors * \file rank.cc * \brief Definition of rank loss. * \author Tianqi Chen, Kailong Chen */ #include #include #include #include #include #include #include "../common/math.h" #include "../common/random.h" namespace xgboost { namespace obj { DMLC_REGISTRY_FILE_TAG(rank_obj); struct LambdaRankParam : public dmlc::Parameter { int num_pairsample; float fix_list_weight; // declare parameters DMLC_DECLARE_PARAMETER(LambdaRankParam) { DMLC_DECLARE_FIELD(num_pairsample).set_lower_bound(1).set_default(1) .describe("Number of pair generated for each instance."); DMLC_DECLARE_FIELD(fix_list_weight).set_lower_bound(0.0f).set_default(0.0f) .describe("Normalize the weight of each list by this value," " if equals 0, no effect will happen"); } }; // objective for lambda rank class LambdaRankObj : public ObjFunction { public: void Configure(const std::vector >& args) override { param_.InitAllowUnknown(args); } void GetGradient(const HostDeviceVector& preds, const MetaInfo& info, int iter, HostDeviceVector* out_gpair) override { CHECK_EQ(preds.Size(), info.labels_.Size()) << "label size predict size not match"; const auto& preds_h = preds.HostVector(); out_gpair->Resize(preds_h.size()); std::vector& gpair = out_gpair->HostVector(); // quick consistency when group is not available std::vector tgptr(2, 0); tgptr[1] = static_cast(info.labels_.Size()); const std::vector &gptr = info.group_ptr_.size() == 0 ? tgptr : info.group_ptr_; CHECK(gptr.size() != 0 && gptr.back() == info.labels_.Size()) << "group structure not consistent with #rows"; const auto ngroup = static_cast(gptr.size() - 1); #pragma omp parallel { // parall construct, declare random number generator here, so that each // thread use its own random number generator, seed by thread id and current iteration common::RandomEngine rnd(iter * 1111 + omp_get_thread_num()); std::vector pairs; std::vector lst; std::vector< std::pair > rec; bst_float sum_weights = 0; for (bst_omp_uint k = 0; k < ngroup; ++k) { sum_weights += info.GetWeight(k); } bst_float weight_normalization_factor = ngroup/sum_weights; const auto& labels = info.labels_.HostVector(); #pragma omp for schedule(static) for (bst_omp_uint k = 0; k < ngroup; ++k) { lst.clear(); pairs.clear(); for (unsigned j = gptr[k]; j < gptr[k+1]; ++j) { lst.emplace_back(preds_h[j], labels[j], j); gpair[j] = GradientPair(0.0f, 0.0f); } std::sort(lst.begin(), lst.end(), ListEntry::CmpPred); rec.resize(lst.size()); for (unsigned i = 0; i < lst.size(); ++i) { rec[i] = std::make_pair(lst[i].label, i); } std::sort(rec.begin(), rec.end(), common::CmpFirst); // enumerate buckets with same label, for each item in the lst, grab another sample randomly for (unsigned i = 0; i < rec.size(); ) { unsigned j = i + 1; while (j < rec.size() && rec[j].first == rec[i].first) ++j; // bucket in [i,j), get a sample outside bucket unsigned nleft = i, nright = static_cast(rec.size() - j); if (nleft + nright != 0) { int nsample = param_.num_pairsample; while (nsample --) { for (unsigned pid = i; pid < j; ++pid) { unsigned ridx = std::uniform_int_distribution(0, nleft + nright - 1)(rnd); if (ridx < nleft) { pairs.emplace_back(rec[ridx].second, rec[pid].second, info.GetWeight(k) * weight_normalization_factor); } else { pairs.emplace_back(rec[pid].second, rec[ridx+j-i].second, info.GetWeight(k) * weight_normalization_factor); } } } } i = j; } // get lambda weight for the pairs this->GetLambdaWeight(lst, &pairs); // rescale each gradient and hessian so that the lst have constant weighted float scale = 1.0f / param_.num_pairsample; if (param_.fix_list_weight != 0.0f) { scale *= param_.fix_list_weight / (gptr[k + 1] - gptr[k]); } for (auto & pair : pairs) { const ListEntry &pos = lst[pair.pos_index]; const ListEntry &neg = lst[pair.neg_index]; const bst_float w = pair.weight * scale; const float eps = 1e-16f; bst_float p = common::Sigmoid(pos.pred - neg.pred); bst_float g = p - 1.0f; bst_float h = std::max(p * (1.0f - p), eps); // accumulate gradient and hessian in both pid, and nid gpair[pos.rindex] += GradientPair(g * w, 2.0f*w*h); gpair[neg.rindex] += GradientPair(-g * w, 2.0f*w*h); } } } } const char* DefaultEvalMetric() const override { return "map"; } protected: /*! \brief helper information in a list */ struct ListEntry { /*! \brief the predict score we in the data */ bst_float pred; /*! \brief the actual label of the entry */ bst_float label; /*! \brief row index in the data matrix */ unsigned rindex; // constructor ListEntry(bst_float pred, bst_float label, unsigned rindex) : pred(pred), label(label), rindex(rindex) {} // comparator by prediction inline static bool CmpPred(const ListEntry &a, const ListEntry &b) { return a.pred > b.pred; } // comparator by label inline static bool CmpLabel(const ListEntry &a, const ListEntry &b) { return a.label > b.label; } }; /*! \brief a pair in the lambda rank */ struct LambdaPair { /*! \brief positive index: this is a position in the list */ unsigned pos_index; /*! \brief negative index: this is a position in the list */ unsigned neg_index; /*! \brief weight to be filled in */ bst_float weight; // constructor LambdaPair(unsigned pos_index, unsigned neg_index) : pos_index(pos_index), neg_index(neg_index), weight(1.0f) {} // constructor LambdaPair(unsigned pos_index, unsigned neg_index, bst_float weight) : pos_index(pos_index), neg_index(neg_index), weight(weight) {} }; /*! * \brief get lambda weight for existing pairs * \param list a list that is sorted by pred score * \param io_pairs record of pairs, containing the pairs to fill in weights */ virtual void GetLambdaWeight(const std::vector &sorted_list, std::vector *io_pairs) = 0; private: LambdaRankParam param_; }; class PairwiseRankObj: public LambdaRankObj{ protected: void GetLambdaWeight(const std::vector &sorted_list, std::vector *io_pairs) override {} }; // beta version: NDCG lambda rank class LambdaRankObjNDCG : public LambdaRankObj { protected: void GetLambdaWeight(const std::vector &sorted_list, std::vector *io_pairs) override { std::vector &pairs = *io_pairs; float IDCG; // NOLINT { std::vector labels(sorted_list.size()); for (size_t i = 0; i < sorted_list.size(); ++i) { labels[i] = sorted_list[i].label; } std::sort(labels.begin(), labels.end(), std::greater()); IDCG = CalcDCG(labels); } if (IDCG == 0.0) { for (auto & pair : pairs) { pair.weight = 0.0f; } } else { IDCG = 1.0f / IDCG; for (auto & pair : pairs) { unsigned pos_idx = pair.pos_index; unsigned neg_idx = pair.neg_index; float pos_loginv = 1.0f / std::log2(pos_idx + 2.0f); float neg_loginv = 1.0f / std::log2(neg_idx + 2.0f); auto pos_label = static_cast(sorted_list[pos_idx].label); auto neg_label = static_cast(sorted_list[neg_idx].label); bst_float original = ((1 << pos_label) - 1) * pos_loginv + ((1 << neg_label) - 1) * neg_loginv; float changed = ((1 << neg_label) - 1) * pos_loginv + ((1 << pos_label) - 1) * neg_loginv; bst_float delta = (original - changed) * IDCG; if (delta < 0.0f) delta = - delta; pair.weight *= delta; } } } inline static bst_float CalcDCG(const std::vector &labels) { double sumdcg = 0.0; for (size_t i = 0; i < labels.size(); ++i) { const auto rel = static_cast(labels[i]); if (rel != 0) { sumdcg += ((1 << rel) - 1) / std::log2(static_cast(i + 2)); } } return static_cast(sumdcg); } }; class LambdaRankObjMAP : public LambdaRankObj { protected: struct MAPStats { /*! \brief the accumulated precision */ float ap_acc; /*! * \brief the accumulated precision, * assuming a positive instance is missing */ float ap_acc_miss; /*! * \brief the accumulated precision, * assuming that one more positive instance is inserted ahead */ float ap_acc_add; /* \brief the accumulated positive instance count */ float hits; MAPStats() = default; MAPStats(float ap_acc, float ap_acc_miss, float ap_acc_add, float hits) : ap_acc(ap_acc), ap_acc_miss(ap_acc_miss), ap_acc_add(ap_acc_add), hits(hits) {} }; /*! * \brief Obtain the delta MAP if trying to switch the positions of instances in index1 or index2 * in sorted triples * \param sorted_list the list containing entry information * \param index1,index2 the instances switched * \param map_stats a vector containing the accumulated precisions for each position in a list */ inline bst_float GetLambdaMAP(const std::vector &sorted_list, int index1, int index2, std::vector *p_map_stats) { std::vector &map_stats = *p_map_stats; if (index1 == index2 || map_stats[map_stats.size() - 1].hits == 0) { return 0.0f; } if (index1 > index2) std::swap(index1, index2); bst_float original = map_stats[index2].ap_acc; if (index1 != 0) original -= map_stats[index1 - 1].ap_acc; bst_float changed = 0; bst_float label1 = sorted_list[index1].label > 0.0f ? 1.0f : 0.0f; bst_float label2 = sorted_list[index2].label > 0.0f ? 1.0f : 0.0f; if (label1 == label2) { return 0.0; } else if (label1 < label2) { changed += map_stats[index2 - 1].ap_acc_add - map_stats[index1].ap_acc_add; changed += (map_stats[index1].hits + 1.0f) / (index1 + 1); } else { changed += map_stats[index2 - 1].ap_acc_miss - map_stats[index1].ap_acc_miss; changed += map_stats[index2].hits / (index2 + 1); } bst_float ans = (changed - original) / (map_stats[map_stats.size() - 1].hits); if (ans < 0) ans = -ans; return ans; } /* * \brief obtain preprocessing results for calculating delta MAP * \param sorted_list the list containing entry information * \param map_stats a vector containing the accumulated precisions for each position in a list */ inline void GetMAPStats(const std::vector &sorted_list, std::vector *p_map_acc) { std::vector &map_acc = *p_map_acc; map_acc.resize(sorted_list.size()); bst_float hit = 0, acc1 = 0, acc2 = 0, acc3 = 0; for (size_t i = 1; i <= sorted_list.size(); ++i) { if (sorted_list[i - 1].label > 0.0f) { hit++; acc1 += hit / i; acc2 += (hit - 1) / i; acc3 += (hit + 1) / i; } map_acc[i - 1] = MAPStats(acc1, acc2, acc3, hit); } } void GetLambdaWeight(const std::vector &sorted_list, std::vector *io_pairs) override { std::vector &pairs = *io_pairs; std::vector map_stats; GetMAPStats(sorted_list, &map_stats); for (auto & pair : pairs) { pair.weight *= GetLambdaMAP(sorted_list, pair.pos_index, pair.neg_index, &map_stats); } } }; // register the objective functions DMLC_REGISTER_PARAMETER(LambdaRankParam); XGBOOST_REGISTER_OBJECTIVE(PairwiseRankObj, "rank:pairwise") .describe("Pairwise rank objective.") .set_body([]() { return new PairwiseRankObj(); }); XGBOOST_REGISTER_OBJECTIVE(LambdaRankNDCG, "rank:ndcg") .describe("LambdaRank with NDCG as objective.") .set_body([]() { return new LambdaRankObjNDCG(); }); XGBOOST_REGISTER_OBJECTIVE(LambdaRankObjMAP, "rank:map") .describe("LambdaRank with MAP as objective.") .set_body([]() { return new LambdaRankObjMAP(); }); } // namespace obj } // namespace xgboost