pub const NUM_LABELS: usize = 3;
#[derive(Clone)]
pub struct CrfModel {
pub transition: Vec<f32>,
pub emission_weights: Vec<f32>, }
impl CrfModel {
pub fn new() -> Self {
let transition = vec![0.0f32; NUM_LABELS * NUM_LABELS];
let emission_weights = vec![0.0f32; NUM_LABELS];
Self {
transition,
emission_weights,
}
}
fn extract_features(
&self,
token: &str,
prev_token: Option<&str>,
next_token: Option<&str>,
) -> Vec<f32> {
let mut features = Vec::new();
let lower = token.to_lowercase();
let _len = token.len();
features.push(
if token
.chars()
.all(|c| !c.is_alphabetic() || c.is_uppercase())
{
1.0
} else {
0.0
},
); features.push(if token.starts_with('[') || token.starts_with('(') {
1.0
} else {
0.0
}); features.push(if token.ends_with(']') || token.ends_with(')') {
1.0
} else {
0.0
}); features.push(
if lower.contains("e0")
|| lower.contains("s0")
|| lower.chars().all(|c| c.is_ascii_digit())
{
1.0
} else {
0.0
},
); features.push(
if lower.contains("720p")
|| lower.contains("1080p")
|| lower.contains("480p")
|| lower == "bd"
|| lower == "web"
{
1.0
} else {
0.0
},
); features.push(if token.chars().any(|c| c.is_ascii_digit()) {
1.0
} else {
0.0
}); features.push(if token.len() > 3 { 1.0 } else { 0.0 });
features.push(if let Some(p) = prev_token {
if p.starts_with('[') || p.starts_with('(') {
1.0
} else {
0.0
}
} else {
0.0
});
features.push(if let Some(n) = next_token {
if n.starts_with('[') || n.starts_with('(') {
1.0
} else {
0.0
}
} else {
0.0
});
features
}
fn compute_emission(
&self,
token: &str,
prev_token: Option<&str>,
next_token: Option<&str>,
label: usize,
) -> f32 {
let features = self.extract_features(token, prev_token, next_token);
let bias = self.emission_weights[label];
let mut score = bias;
match label {
0 => {
score += features[2] * 2.0; score += features[3] * 2.0; score += features[4] * 2.0; score -= features[0] * 1.0; }
1 => {
score += features[0] * 2.0; score -= features[2] * 2.0; score -= features[3] * 2.0; score -= features[4] * 2.0; score += features[5] * 0.5; }
2 => {
score += features[0] * 1.5; score -= features[2] * 2.0; score -= features[3] * 2.0; score -= features[4] * 2.0; }
_ => {}
}
score
}
pub fn forward(&self, tokens: &[String]) -> (Vec<Vec<f32>>, Vec<f32>) {
let seq_len = tokens.len();
let mut emissions = Vec::new();
for (i, token) in tokens.iter().enumerate() {
let prev = if i > 0 {
Some(tokens[i - 1].as_str())
} else {
None
};
let next = if i < seq_len - 1 {
Some(tokens[i + 1].as_str())
} else {
None
};
let mut scores = Vec::new();
for label in 0..NUM_LABELS {
scores.push(self.compute_emission(token, prev, next, label));
}
emissions.push(scores);
}
(emissions, self.transition.clone())
}
pub fn predict(&self, tokens: &[String]) -> Vec<usize> {
let (emissions, transitions) = self.forward(tokens);
let emissions_flat: Vec<f32> = emissions.iter().flatten().cloned().collect();
viterbi_decode(&emissions_flat, &transitions, NUM_LABELS)
}
pub fn train_step(&mut self, tokens: &[String], true_labels: &[usize], _lr: f32) {
let preds = self.predict(tokens);
for (_i, (&pred, &true_label)) in preds.iter().zip(true_labels.iter()).enumerate() {
if pred != true_label {
for label in 0..NUM_LABELS {
if label == true_label {
self.emission_weights[label] += 0.1;
} else if label == pred {
self.emission_weights[label] -= 0.1;
}
}
}
}
for i in 1..true_labels.len().min(preds.len()) {
let from = preds[i - 1];
let to = preds[i];
let correct_from = true_labels[i - 1];
let correct_to = true_labels[i];
if from != correct_from || to != correct_to {
self.transition[to * NUM_LABELS + from] -= 0.01;
self.transition[correct_to * NUM_LABELS + correct_from] += 0.01;
}
}
self.transition[0 * NUM_LABELS + 1] = self.transition[0 * NUM_LABELS + 1].min(-1.0);
}
pub fn save(&self, path: &str) -> std::io::Result<()> {
let json = serde_json::json!({
"transition": self.transition,
"emission_weights": self.emission_weights,
"num_labels": NUM_LABELS,
});
std::fs::write(path, serde_json::to_string_pretty(&json).unwrap())?;
Ok(())
}
pub fn load(path: &str) -> std::io::Result<Self> {
let content = std::fs::read_to_string(path)?;
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
let transition: Vec<f32> = json["transition"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_f64().unwrap() as f32)
.collect();
let emission_weights: Vec<f32> = json["emission_weights"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_f64().unwrap() as f32)
.collect();
Ok(Self {
transition,
emission_weights,
})
}
}
impl Default for CrfModel {
fn default() -> Self {
Self::new()
}
}
pub fn viterbi_decode(emissions: &[f32], transitions: &[f32], num_labels: usize) -> Vec<usize> {
if emissions.is_empty() || num_labels == 0 {
return vec![];
}
let seq_len = emissions.len() / num_labels;
if seq_len == 0 {
return vec![];
}
let mut viterbi = vec![vec![f32::MIN / 1e10; num_labels]; seq_len];
let mut backpointers = vec![vec![0usize; num_labels]; seq_len];
for j in 0..num_labels {
if j < emissions.len() {
viterbi[0][j] = emissions[j];
}
}
for t in 1..seq_len {
for j in 0..num_labels {
let mut best_score = f32::MIN / 1e10;
let mut best_prev = 0;
for i in 0..num_labels {
let score = viterbi[t - 1][i] + transitions[j * num_labels + i];
if score > best_score {
best_score = score;
best_prev = i;
}
}
let emission_idx = t * num_labels + j;
if emission_idx < emissions.len() {
viterbi[t][j] = best_score + emissions[emission_idx];
}
if t < backpointers.len() {
backpointers[t][j] = best_prev;
}
}
}
let mut path = vec![0usize; seq_len];
if seq_len > 0 {
let last_row = &viterbi[seq_len - 1];
path[seq_len - 1] = last_row
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
for t in (0..seq_len - 1).rev() {
if t + 1 < backpointers.len() {
path[t] = backpointers[t + 1][path[t + 1]];
}
}
}
path
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_viterbi() {
let emissions = vec![0.1, 0.8, 0.1, 0.8, 0.1, 0.1];
let transitions = vec![0.0, 0.5, 0.0, 0.5, 0.0, 0.5, 0.0, 0.5, 0.0];
let path = viterbi_decode(&emissions, &transitions, 3);
assert!(!path.is_empty());
}
#[test]
fn test_viterbi_backpointer_for_two_tokens() {
let emissions = vec![
0.0, 2.0, 0.0, 0.0, 0.0, 0.0, ];
let transitions = vec![
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.0, 0.0, ];
let path = viterbi_decode(&emissions, &transitions, 3);
assert_eq!(path, vec![1, 2]);
}
}