use super::args::LossName;
use super::dictionary::Dictionary;
use super::matrix::{FastTextMatrix, Matrix};
use std::collections::BinaryHeap;
#[derive(Clone, Copy, PartialEq)]
struct FloatOrd(f32);
impl Eq for FloatOrd {}
impl PartialOrd for FloatOrd {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FloatOrd {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0
.partial_cmp(&other.0)
.unwrap_or(std::cmp::Ordering::Equal)
}
}
#[derive(Debug, Clone)]
struct HSNode {
parent: i32,
left: i32,
right: i32,
count: i64,
#[allow(dead_code)]
binary: bool,
}
pub struct HSTree {
tree: Vec<HSNode>,
osz: usize,
}
impl HSTree {
pub fn build(counts: &[i64]) -> Self {
let osz = counts.len();
let tree_size = 2 * osz - 1;
let mut tree: Vec<HSNode> = (0..tree_size)
.map(|_| HSNode {
parent: -1,
left: -1,
right: -1,
count: 1_000_000_000_000_000,
binary: false,
})
.collect();
for i in 0..osz {
tree[i].count = counts[i];
}
let mut leaf = osz as i32 - 1;
let mut node = osz;
for i in osz..tree_size {
let mut mini = [0i32; 2];
for m in &mut mini {
if leaf >= 0 && tree[leaf as usize].count < tree[node].count {
*m = leaf;
leaf -= 1;
} else {
*m = node as i32;
node += 1;
}
}
tree[i].left = mini[0];
tree[i].right = mini[1];
tree[i].count = tree[mini[0] as usize].count + tree[mini[1] as usize].count;
tree[mini[0] as usize].parent = i as i32;
tree[mini[1] as usize].parent = i as i32;
tree[mini[1] as usize].binary = true;
}
HSTree { tree, osz }
}
#[allow(clippy::too_many_arguments)]
fn dfs(
&self,
k: usize,
threshold: f32,
node: usize,
score: f32,
hidden: &[f32],
output: &FastTextMatrix,
heap: &mut BinaryHeap<std::cmp::Reverse<(FloatOrd, usize)>>,
) {
if score < std_log(threshold) {
return;
}
if heap.len() == k && score < heap.peek().unwrap().0 .0 .0 {
return;
}
let n = &self.tree[node];
if n.left == -1 && n.right == -1 {
heap.push(std::cmp::Reverse((FloatOrd(score), node)));
if heap.len() > k {
heap.pop();
}
return;
}
let f = output.dot_row(hidden, node - self.osz);
let f = sigmoid(f);
self.dfs(
k,
threshold,
n.left as usize,
score + std_log(1.0 - f),
hidden,
output,
heap,
);
self.dfs(
k,
threshold,
n.right as usize,
score + std_log(f),
hidden,
output,
heap,
);
}
}
#[allow(clippy::too_many_arguments)]
pub fn predict(
text: &str,
k: usize,
input_matrix: &FastTextMatrix,
output_matrix: &FastTextMatrix,
dictionary: &Dictionary,
dim: usize,
loss: LossName,
hs_tree: Option<&HSTree>,
) -> Vec<(String, f32)> {
let features = dictionary.get_line_features(text);
if features.is_empty() {
return Vec::new();
}
let mut hidden = vec![0.0f32; dim];
for &feat_id in &features {
if (feat_id as usize) < input_matrix.rows() {
input_matrix.add_row_to(feat_id as usize, &mut hidden);
}
}
let scale = 1.0 / features.len() as f32;
for h in hidden.iter_mut() {
*h *= scale;
}
let nlabels = dictionary.nlabels() as usize;
let k = k.min(nlabels);
let label_scores = match loss {
LossName::HierarchicalSoftmax => {
predict_hs(k, &hidden, output_matrix, hs_tree.unwrap(), nlabels)
}
_ => predict_softmax(k, &hidden, output_matrix, nlabels),
};
label_scores
.iter()
.map(|&(score, label_idx)| {
let label = dictionary.get_label(label_idx as i32);
let label = label.strip_prefix("__label__").unwrap_or(label).to_string();
(label, score)
})
.collect()
}
fn predict_hs(
k: usize,
hidden: &[f32],
output: &FastTextMatrix,
tree: &HSTree,
_nlabels: usize,
) -> Vec<(f32, usize)> {
let mut heap: BinaryHeap<std::cmp::Reverse<(FloatOrd, usize)>> = BinaryHeap::new();
let root = 2 * tree.osz - 2;
tree.dfs(k, 0.0, root, 0.0, hidden, output, &mut heap);
let mut results: Vec<(f32, usize)> = heap
.into_iter()
.map(|std::cmp::Reverse((FloatOrd(score), idx))| (score.exp(), idx))
.collect();
results.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
results
}
fn predict_softmax(
k: usize,
hidden: &[f32],
output: &FastTextMatrix,
nlabels: usize,
) -> Vec<(f32, usize)> {
let mut logits: Vec<f32> = (0..nlabels).map(|i| output.dot_row(hidden, i)).collect();
softmax(&mut logits);
let mut indices: Vec<usize> = (0..nlabels).collect();
indices.select_nth_unstable_by(k.saturating_sub(1), |&a, &b| {
logits[b]
.partial_cmp(&logits[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
indices.truncate(k);
indices.sort_by(|&a, &b| {
logits[b]
.partial_cmp(&logits[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
indices.iter().map(|&i| (logits[i], i)).collect()
}
#[inline]
fn sigmoid(x: f32) -> f32 {
1.0 / (1.0 + (-x).exp())
}
#[inline]
fn std_log(x: f32) -> f32 {
(x + 1e-5).ln()
}
fn softmax(logits: &mut [f32]) {
if logits.is_empty() {
return;
}
let max_val = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let mut sum = 0.0f32;
for v in logits.iter_mut() {
*v = (*v - max_val).exp();
sum += *v;
}
if sum > 0.0 {
for v in logits.iter_mut() {
*v /= sum;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_softmax_basic() {
let mut logits = vec![1.0, 2.0, 3.0];
softmax(&mut logits);
let sum: f32 = logits.iter().sum();
assert!((sum - 1.0).abs() < 1e-6);
assert!(logits[0] < logits[1]);
assert!(logits[1] < logits[2]);
}
#[test]
fn test_softmax_equal() {
let mut logits = vec![1.0, 1.0, 1.0];
softmax(&mut logits);
for &v in &logits {
assert!((v - 1.0 / 3.0).abs() < 1e-6);
}
}
#[test]
fn test_sigmoid() {
assert!((sigmoid(0.0) - 0.5).abs() < 1e-6);
assert!(sigmoid(10.0) > 0.99);
assert!(sigmoid(-10.0) < 0.01);
}
#[test]
fn test_hs_tree_build() {
let counts = vec![10, 5, 1];
let tree = HSTree::build(&counts);
assert_eq!(tree.osz, 3);
assert_eq!(tree.tree.len(), 5);
assert!(tree.tree[4].left >= 0);
assert!(tree.tree[4].right >= 0);
}
}