use std::collections::HashMap;
const NUM_STATES: usize = 4;
const BEGIN: usize = 0;
const MIDDLE: usize = 1;
const END: usize = 2;
const SINGLE: usize = 3;
#[derive(Clone)]
pub struct HmmModel {
start: [f64; NUM_STATES],
trans: [[f64; NUM_STATES]; NUM_STATES],
emit: HashMap<String, [f64; NUM_STATES]>,
}
fn smoothed_log_probs(counts: &[u64; NUM_STATES]) -> [f64; NUM_STATES] {
let total: f64 = counts.iter().map(|&c| c as f64 + 1.0).sum();
let mut out = [0.0; NUM_STATES];
for (i, &c) in counts.iter().enumerate() {
out[i] = ((c as f64 + 1.0) / total).ln();
}
out
}
impl HmmModel {
pub fn from_counts(
start_counts: [u64; NUM_STATES],
trans_counts: [[u64; NUM_STATES]; NUM_STATES],
emit_counts: HashMap<String, [u64; NUM_STATES]>,
) -> Self {
let start = smoothed_log_probs(&start_counts);
let mut trans = [[0.0; NUM_STATES]; NUM_STATES];
for (i, row) in trans_counts.iter().enumerate() {
trans[i] = smoothed_log_probs(row);
}
let emit = emit_counts
.into_iter()
.map(|(cluster, counts)| (cluster, smoothed_log_probs(&counts)))
.collect();
Self { start, trans, emit }
}
fn emit_log_prob(&self, cluster: &str, state: usize) -> f64 {
match self.emit.get(cluster) {
Some(probs) => probs[state],
None => (1.0 / NUM_STATES as f64).ln(),
}
}
#[allow(clippy::needless_range_loop)]
fn viterbi_tags(&self, clusters: &[String]) -> Vec<usize> {
let n = clusters.len();
let mut score = vec![[f64::NEG_INFINITY; NUM_STATES]; n];
let mut back = vec![[0usize; NUM_STATES]; n];
for s in 0..NUM_STATES {
score[0][s] = self.start[s] + self.emit_log_prob(&clusters[0], s);
}
for t in 1..n {
for s in 0..NUM_STATES {
let mut best_score = f64::NEG_INFINITY;
let mut best_prev = 0;
for ps in 0..NUM_STATES {
let candidate = score[t - 1][ps] + self.trans[ps][s];
if candidate > best_score {
best_score = candidate;
best_prev = ps;
}
}
back[t][s] = best_prev;
score[t][s] = best_score + self.emit_log_prob(&clusters[t], s);
}
}
let mut best_final = 0;
for s in 1..NUM_STATES {
if score[n - 1][s] > score[n - 1][best_final] {
best_final = s;
}
}
let mut tags = vec![0usize; n];
tags[n - 1] = best_final;
for t in (1..n).rev() {
tags[t - 1] = back[t][tags[t]];
}
tags
}
pub(crate) fn segment_oov(&self, clusters: &[String]) -> Vec<Vec<String>> {
if clusters.is_empty() {
return Vec::new();
}
let tags = self.viterbi_tags(clusters);
let mut tokens = Vec::new();
let mut current: Vec<String> = Vec::new();
for (cluster, &tag) in clusters.iter().zip(&tags) {
match tag {
BEGIN => {
if !current.is_empty() {
tokens.push(std::mem::take(&mut current));
}
current.push(cluster.clone());
}
MIDDLE => current.push(cluster.clone()),
END => {
current.push(cluster.clone());
tokens.push(std::mem::take(&mut current));
}
SINGLE => {
if !current.is_empty() {
tokens.push(std::mem::take(&mut current));
}
tokens.push(vec![cluster.clone()]);
}
_ => unreachable!("tags are always in 0..NUM_STATES"),
}
}
if !current.is_empty() {
tokens.push(current);
}
tokens
}
}
#[cfg(test)]
mod tests {
use super::*;
fn two_word_model() -> HmmModel {
let start = [50, 0, 0, 0]; let mut trans = [[0u64; NUM_STATES]; NUM_STATES];
trans[BEGIN][END] = 50;
trans[END][SINGLE] = 50;
let mut emit = HashMap::new();
emit.insert("a".to_string(), [50, 0, 0, 0]);
emit.insert("b".to_string(), [0, 0, 50, 0]);
emit.insert("c".to_string(), [0, 0, 0, 50]);
HmmModel::from_counts(start, trans, emit)
}
#[test]
fn decodes_a_two_word_run_via_bmes_tags() {
let model = two_word_model();
let clusters = vec!["a".to_string(), "b".to_string(), "c".to_string()];
assert_eq!(
model.segment_oov(&clusters),
vec![
vec!["a".to_string(), "b".to_string()],
vec!["c".to_string()]
]
);
}
#[test]
fn single_cluster_run_is_its_own_token() {
let model = two_word_model();
let clusters = vec!["c".to_string()];
assert_eq!(model.segment_oov(&clusters), vec![vec!["c".to_string()]]);
}
#[test]
fn empty_run_yields_no_tokens() {
let model = two_word_model();
assert!(model.segment_oov(&[]).is_empty());
}
#[test]
fn unseen_cluster_falls_back_to_uniform_emission_without_panicking() {
let model = two_word_model();
let clusters = vec!["z".to_string()];
assert_eq!(model.segment_oov(&clusters).concat(), vec!["z".to_string()]);
}
}