package learn // Pairwise learning to rank for the display order re ranker. A slate's decided members are a ranking GROUP: a // human approved some and rejected others in ONE context, so the ordering signal is "approved should rank above // rejected WITHIN this slate", never a per candidate score. Reducing that to a symmetric pairwise logistic fit // on feature DIFFERENCES lets the existing FitLogistic learn a coefficient vector w such that a candidate's // display utility is w . features and the display order is that utility descending. The frozen engine stays the // only per candidate SCORER; this only permutes the display ORDER, and a human still approves every member. // RankItem is one candidate within a ranking group: its feature vector (the frozen engine sub scores) and // whether the human approved it. type RankItem struct { Features []float64 Approved bool } // RankGroup is one slate's decided members, the candidates a human judged together. Pairs are formed only WITHIN // a group, never across slates, because the approval was made in that slate's context. type RankGroup struct { Items []RankItem } // PairwiseDataset turns decided ranking groups into a SYMMETRIC pairwise logistic training set: for every // approved a and rejected b in the same group it emits both (features_a minus features_b, label 1) and // (features_b minus features_a, label 0). The symmetric pair forces a zero intercept and a direction invariant // fit, so a fitted coefficient vector w scores a candidate by utility = w . features and the display order is // that utility descending. A group with no approved or no rejected member contributes no pair (a unanimous // slate carries no ordering signal). Pure, no DB. func PairwiseDataset(groups []RankGroup) (x [][]float64, y []int) { for _, g := range groups { var approved, rejected []RankItem for _, it := range g.Items { if it.Approved { approved = append(approved, it) } else { rejected = append(rejected, it) } } for _, a := range approved { for _, b := range rejected { x = append(x, diffFeatures(a.Features, b.Features)) y = append(y, 1) x = append(x, diffFeatures(b.Features, a.Features)) y = append(y, 0) } } } return x, y } // RankUtility is a candidate's display utility under a coefficient vector: the linear score w . features. The // display order is this utility descending. It is NEVER surfaced as a candidate score; only the ordering is // used, and order is invariant to a positive rescaling of the utility. func RankUtility(features, coeffs []float64) float64 { var u float64 n := len(features) if len(coeffs) < n { n = len(coeffs) } for i := 0; i < n; i++ { u += coeffs[i] * features[i] } return u } // PairwiseAccuracy is the fraction of within group (approved, rejected) pairs the utility orders correctly // (approved utility strictly above rejected). It is the held out ranking quality metric: 1.0 is a perfect order, // 0.5 is random, below 0.5 is inverted. A group with no such pair is skipped. A tie counts as half credit. func PairwiseAccuracy(groups []RankGroup, coeffs []float64) float64 { var correct, total float64 for _, g := range groups { for _, a := range g.Items { if !a.Approved { continue } for _, b := range g.Items { if b.Approved { continue } ua := RankUtility(a.Features, coeffs) ub := RankUtility(b.Features, coeffs) total++ if ua > ub { correct++ } else if ua == ub { correct += 0.5 } } } } if total == 0 { return 0.5 } return correct / total } // diffFeatures returns a minus b element wise over the shared prefix length. func diffFeatures(a, b []float64) []float64 { n := len(a) if len(b) < n { n = len(b) } d := make([]float64, n) for i := 0; i < n; i++ { d[i] = a[i] - b[i] } return d }