///|
/// Online pairwise ranking model trained with a hinge objective.
pub struct PairwiseRanker {
weights : Array[Double]
learning_rate : Double
margin : Double
l2 : Double
mut updates : Int
}
///|
pub fn PairwiseRanker::new(
dimension : Int,
learning_rate? : Double = 0.01,
margin? : Double = 1.0,
l2? : Double = 0.0,
) -> PairwiseRanker {
{
weights: Array::make(if dimension < 0 { 0 } else { dimension }, 0.0),
learning_rate,
margin: if margin <= 0.0 {
1.0
} else {
margin
},
l2,
updates: 0,
}
}
///|
pub fn PairwiseRanker::score(
self : PairwiseRanker,
features : Array[Double],
) -> Double {
dot_product(self.weights, features)
}
///|
pub fn PairwiseRanker::weights(self : PairwiseRanker) -> Array[Double] {
copy_vector(self.weights)
}
///|
pub fn PairwiseRanker::dimension(self : PairwiseRanker) -> Int {
self.weights.length()
}
///|
pub fn PairwiseRanker::compare(
self : PairwiseRanker,
left : Array[Double],
right : Array[Double],
) -> Double {
self.score(left) - self.score(right)
}
///|
pub fn PairwiseRanker::update(
self : PairwiseRanker,
positive : Array[Double],
negative : Array[Double],
) -> Bool {
let difference = self.compare(positive, negative)
if difference >= self.margin {
false
} else {
let limit = if positive.length() < negative.length() {
positive.length()
} else {
negative.length()
}
let size = if limit < self.weights.length() {
limit
} else {
self.weights.length()
}
for i in 0.. Double {
let margin_error = self.margin - self.compare(positive, negative)
let hinge = if margin_error > 0.0 { margin_error } else { 0.0 }
hinge + 0.5 * self.l2 * squared_norm(self.weights)
}
///|
pub fn PairwiseRanker::updates(self : PairwiseRanker) -> Int {
self.updates
}
///|
pub fn PairwiseRanker::reset(self : PairwiseRanker) -> Unit {
self.weights.fill(0.0)
self.updates = 0
}
///|
pub struct RankingMetrics {
mut queries : Double
mut reciprocal_rank : Double
mut ndcg_sum : Double
hits : Array[Double]
}
///|
pub fn RankingMetrics::new(max_k? : Int = 10) -> RankingMetrics {
{
queries: 0.0,
reciprocal_rank: 0.0,
ndcg_sum: 0.0,
hits: Array::make(if max_k < 1 { 1 } else { max_k }, 0.0),
}
}
///|
pub fn RankingMetrics::observe(
self : RankingMetrics,
relevances : Array[Double],
) -> Unit {
self.queries += 1.0
let order = Array::makei(relevances.length(), i => i)
order.sort_by((left, right) => {
if relevances[left] > relevances[right] {
-1
} else if relevances[left] < relevances[right] {
1
} else {
left - right
}
})
let mut first_relevant = -1
for rank in 0.. 0.0 && first_relevant < 0 {
first_relevant = rank
}
}
if first_relevant >= 0 {
self.reciprocal_rank += 1.0 / (first_relevant + 1).to_double()
}
let ideal = copy_vector(relevances)
ideal.sort_by((left, right) => {
if left > right {
-1
} else if left < right {
1
} else {
0
}
})
let mut dcg = 0.0
let mut idcg = 0.0
for rank in 0.. 0.0 {
self.hits[k] += 1.0
}
}
}
if idcg > 0.0 {
self.ndcg_sum += dcg / idcg
}
}
///|
pub fn RankingMetrics::mrr(self : RankingMetrics) -> Double {
if self.queries <= 0.0 {
0.0
} else {
self.reciprocal_rank / self.queries
}
}
///|
pub fn RankingMetrics::ndcg(self : RankingMetrics) -> Double {
if self.queries <= 0.0 {
0.0
} else {
self.ndcg_sum / self.queries
}
}
///|
pub fn RankingMetrics::recall_at(self : RankingMetrics, k : Int) -> Double {
if k <= 0 || k > self.hits.length() || self.queries <= 0.0 {
0.0
} else {
self.hits[k - 1] / self.queries
}
}
///|
pub fn RankingMetrics::queries(self : RankingMetrics) -> Double {
self.queries
}
///|
pub fn RankingMetrics::reset(self : RankingMetrics) -> Unit {
self.queries = 0.0
self.reciprocal_rank = 0.0
self.ndcg_sum = 0.0
self.hits.fill(0.0)
}
///|
pub struct ClickThroughRateTracker {
mut impressions : Double
mut clicks : Double
mut predicted_sum : Double
mut squared_calibration_error : Double
}
///|
pub fn ClickThroughRateTracker::new() -> ClickThroughRateTracker {
{
impressions: 0.0,
clicks: 0.0,
predicted_sum: 0.0,
squared_calibration_error: 0.0,
}
}
///|
pub fn ClickThroughRateTracker::update(
self : ClickThroughRateTracker,
probability : Double,
clicked : Bool,
weight? : Double = 1.0,
) -> Unit {
let label = if clicked { 1.0 } else { 0.0 }
self.impressions += weight
self.clicks += weight * label
self.predicted_sum += weight * probability
self.squared_calibration_error += weight * squared_error(probability, label)
}
///|
pub fn ClickThroughRateTracker::ctr(self : ClickThroughRateTracker) -> Double {
if self.impressions <= 0.0 {
0.0
} else {
self.clicks / self.impressions
}
}
///|
pub fn ClickThroughRateTracker::predicted_ctr(
self : ClickThroughRateTracker,
) -> Double {
if self.impressions <= 0.0 {
0.0
} else {
self.predicted_sum / self.impressions
}
}
///|
pub fn ClickThroughRateTracker::brier(self : ClickThroughRateTracker) -> Double {
if self.impressions <= 0.0 {
0.0
} else {
self.squared_calibration_error / self.impressions
}
}
///|
pub fn ClickThroughRateTracker::impressions(
self : ClickThroughRateTracker,
) -> Double {
self.impressions
}
///|
pub fn ClickThroughRateTracker::reset(self : ClickThroughRateTracker) -> Unit {
self.impressions = 0.0
self.clicks = 0.0
self.predicted_sum = 0.0
self.squared_calibration_error = 0.0
}