use std::cmp::Ordering;
use std::collections::BinaryHeap;
#[derive(Debug, Clone)]
pub(crate) struct Node {
pub id: usize,
pub pos: usize,
pub length: usize,
pub score: f64,
prev: Option<usize>,
backtrace_score: f64,
}
#[derive(Debug, Clone)]
pub(crate) struct Lattice<'a> {
sentence: &'a str,
nodes: Vec<Node>,
pub begin_nodes: Vec<Vec<usize>>,
pub end_nodes: Vec<Vec<usize>>,
}
pub(crate) fn log_sum_exp(x: f64, y: f64, init: bool) -> f64 {
if init {
return y;
}
let (lo, hi) = if x > y { (y, x) } else { (x, y) };
if hi > lo + 50.0 {
hi
} else {
hi + ((lo - hi).exp() + 1.0).ln()
}
}
const BOS_NODE: usize = 0;
const EOS_NODE: usize = 1;
impl<'a> Lattice<'a> {
pub(crate) fn new(sentence: &'a str, bos_id: usize, eos_id: usize) -> Self {
let len = sentence.len();
let mut begin_nodes = vec![Vec::new(); len + 1];
let mut end_nodes = vec![Vec::new(); len + 1];
let sentinel = |id, pos| Node {
id,
pos,
length: 0,
score: 0.0,
prev: None,
backtrace_score: 0.0,
};
let nodes = vec![sentinel(bos_id, 0), sentinel(eos_id, len)];
begin_nodes[len].push(EOS_NODE);
end_nodes[0].push(BOS_NODE);
Self {
sentence,
nodes,
begin_nodes,
end_nodes,
}
}
pub(crate) fn insert(&mut self, pos: usize, length: usize, score: f64, id: usize) {
let node_id = self.nodes.len();
self.nodes.push(Node {
id,
pos,
length,
score,
prev: None,
backtrace_score: 0.0,
});
self.begin_nodes[pos].push(node_id);
self.end_nodes[pos + length].push(node_id);
}
pub(crate) fn len(&self) -> usize {
self.sentence.len()
}
pub(crate) fn sentence(&self) -> &'a str {
self.sentence
}
#[cfg(test)]
pub(crate) fn surface(&self, n: usize) -> &str {
match self.sentence.char_indices().nth(n) {
Some((pos, _)) => &self.sentence[pos..],
None => "",
}
}
pub(crate) fn node(&self, node_id: usize) -> &Node {
&self.nodes[node_id]
}
#[cfg(test)]
pub(crate) fn bos_node(&self) -> &Node {
&self.nodes[BOS_NODE]
}
#[cfg(test)]
pub(crate) fn eos_node(&self) -> &Node {
&self.nodes[EOS_NODE]
}
pub(crate) fn piece(&self, node: &Node) -> &'a str {
&self.sentence[node.pos..node.pos + node.length]
}
pub(crate) fn viterbi(&mut self) -> Vec<usize> {
let len = self.len();
let mut pos = 0;
while pos <= len {
if self.begin_nodes[pos].is_empty() {
return vec![];
}
for bi in 0..self.begin_nodes[pos].len() {
let r = self.begin_nodes[pos][bi];
let r_score = self.nodes[r].score;
let mut best: Option<(usize, f64)> = None;
for &l in &self.end_nodes[pos] {
let score = self.nodes[l].backtrace_score + r_score;
if best.is_none_or(|(_, b)| score > b) {
best = Some((l, score));
}
}
match best {
Some((l, score)) => {
self.nodes[r].prev = Some(l);
self.nodes[r].backtrace_score = score;
}
None => {
self.nodes[r].prev = None;
return vec![];
}
}
}
match self.sentence[pos..].chars().next() {
Some(c) => pos += c.len_utf8(),
None => break,
}
}
let mut results = vec![];
let mut node = match self.nodes[EOS_NODE].prev {
Some(n) => n,
None => return vec![],
};
while let Some(prev) = self.nodes[node].prev {
results.push(node);
node = prev;
}
results.reverse();
results
}
#[cfg(test)]
pub(crate) fn tokens(&mut self) -> Vec<String> {
self.viterbi()
.into_iter()
.map(|n| self.piece(&self.nodes[n]).to_owned())
.collect()
}
pub(crate) fn nbest(&mut self, n: usize) -> Vec<Vec<usize>> {
match n {
0 => return vec![],
1 => return vec![self.viterbi()],
_ => {}
}
struct Hypothesis {
node: usize,
next: Option<usize>,
gx: f64,
}
struct Entry {
fx: f64,
seq: u64,
hyp: usize,
}
impl PartialEq for Entry {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for Entry {}
impl PartialOrd for Entry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Entry {
fn cmp(&self, other: &Self) -> Ordering {
self.fx
.total_cmp(&other.fx)
.then_with(|| other.seq.cmp(&self.seq))
}
}
const MAX_AGENDA: usize = 100_000;
const MIN_AGENDA: usize = 512;
self.viterbi();
let mut hyps: Vec<Hypothesis> = Vec::new();
let mut agenda: BinaryHeap<Entry> = BinaryHeap::new();
let mut seq = 0u64;
let eos_score = self.nodes[EOS_NODE].score;
hyps.push(Hypothesis {
node: EOS_NODE,
next: None,
gx: eos_score,
});
agenda.push(Entry {
fx: eos_score,
seq,
hyp: 0,
});
let bos_id = self.nodes[BOS_NODE].id;
let mut results: Vec<Vec<usize>> = Vec::new();
while let Some(top) = agenda.pop() {
let top_node = hyps[top.hyp].node;
if self.nodes[top_node].id == bos_id {
let mut path = Vec::new();
let mut cur = hyps[top.hyp].next;
while let Some(h) = cur {
if hyps[h].next.is_none() {
break; }
path.push(hyps[h].node);
cur = hyps[h].next;
}
results.push(path);
if results.len() == n {
return results;
}
continue;
}
let top_gx = hyps[top.hyp].gx;
let pos = self.nodes[top_node].pos;
for &l in &self.end_nodes[pos] {
let fx = self.nodes[l].backtrace_score + top_gx;
let gx = self.nodes[l].score + top_gx;
hyps.push(Hypothesis {
node: l,
next: Some(top.hyp),
gx,
});
seq += 1;
agenda.push(Entry {
fx,
seq,
hyp: hyps.len() - 1,
});
}
if agenda.len() > MAX_AGENDA {
let keep = MIN_AGENDA.min(n * 10);
let mut shrunk = BinaryHeap::with_capacity(keep);
for _ in 0..keep {
match agenda.pop() {
Some(e) => shrunk.push(e),
None => break,
}
}
agenda = shrunk;
}
}
results
}
#[cfg(test)]
pub(crate) fn nbest_tokens(&mut self, n: usize) -> Vec<Vec<String>> {
self.nbest(n)
.into_iter()
.map(|path| {
path.into_iter()
.map(|i| self.piece(&self.nodes[i]).to_owned())
.collect()
})
.collect()
}
pub(crate) fn populate_marginal(&self, freq: f64, expected: &mut [f64]) -> f64 {
let len = self.len();
let n = self.nodes.len();
let mut alpha = vec![0.0; n];
let mut beta = vec![0.0; n];
for pos in 0..=len {
for &r in &self.begin_nodes[pos] {
for (k, &l) in self.end_nodes[pos].iter().enumerate() {
alpha[r] = log_sum_exp(alpha[r], self.nodes[l].score + alpha[l], k == 0);
}
}
}
for pos in (0..=len).rev() {
for &l in &self.end_nodes[pos] {
for (k, &r) in self.begin_nodes[pos].iter().enumerate() {
beta[l] = log_sum_exp(beta[l], self.nodes[r].score + beta[r], k == 0);
}
}
}
let z = alpha[EOS_NODE];
for pos in 0..len {
for &i in &self.begin_nodes[pos] {
let node = &self.nodes[i];
let total = alpha[i] + node.score + beta[i] - z;
if let Some(slot) = expected.get_mut(node.id) {
*slot += freq * total.exp();
}
}
}
freq * z
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) {
assert!((a - b).abs() < 1e-3, "{a} != {b}");
}
#[test]
fn set_sentence() {
let l = Lattice::new("", 1, 2);
assert_eq!(l.len(), 0);
let l = Lattice::new("test", 1, 2);
assert_eq!(l.surface(1), "est");
assert_eq!(l.surface(3), "t");
assert_eq!(l.bos_node().id, 1);
assert_eq!(l.eos_node().id, 2);
let l = Lattice::new("テストab", 1, 2);
assert_eq!(l.len(), 11);
assert_eq!(l.surface(1), "ストab");
assert_eq!(l.surface(4), "b");
}
#[test]
fn insert_nodes() {
let mut l = Lattice::new("ABあい", 1, 2);
l.insert(0, 1, 0.0, 3);
l.insert(1, 1, 0.0, 4);
l.insert(2, 3, 0.0, 5);
l.insert(5, 3, 0.0, 6);
l.insert(0, 2, 0.0, 7);
l.insert(1, 4, 0.0, 8);
l.insert(2, 6, 0.0, 9);
let pieces: Vec<&str> = (2..9).map(|i| l.piece(l.node(i))).collect();
assert_eq!(pieces, ["A", "B", "あ", "い", "AB", "Bあ", "あい"]);
assert_eq!(l.begin_nodes[0].len(), 2);
assert_eq!(l.begin_nodes[5].len(), 1);
assert_eq!(l.begin_nodes[8].len(), 1);
assert_eq!(l.end_nodes[2].len(), 2);
assert_eq!(l.end_nodes[8].len(), 2);
assert_eq!(l.node(l.end_nodes[8][1]).id, 9);
}
#[test]
fn viterbi_incomplete_and_complete() {
let mut l = Lattice::new("ABC", 1, 2);
assert!(l.viterbi().is_empty());
l.insert(0, 1, 0.0, 3);
assert!(l.viterbi().is_empty());
l.insert(1, 1, 0.0, 4);
l.insert(2, 1, 0.0, 5);
assert_eq!(l.viterbi().len(), 3);
}
#[test]
fn viterbi_prefers_high_scores() {
let mut l = Lattice::new("ABC", 1, 2);
l.insert(0, 1, 0.0, 3);
l.insert(1, 1, 0.0, 4);
l.insert(2, 1, 0.0, 5);
assert_eq!(l.tokens(), ["A", "B", "C"]);
l.insert(0, 2, 2.0, 6);
assert_eq!(l.tokens(), ["AB", "C"]);
l.insert(1, 2, 5.0, 7);
assert_eq!(l.tokens(), ["A", "BC"]);
l.insert(0, 3, 10.0, 8);
assert_eq!(l.tokens(), ["ABC"]);
}
#[test]
fn nbest_orders_by_score() {
let mut l = Lattice::new("ABC", 1, 2);
l.insert(0, 1, 0.0, 3);
l.insert(1, 1, 0.0, 4);
l.insert(2, 1, 0.0, 5);
l.insert(0, 2, 2.0, 6);
l.insert(1, 2, 5.0, 7);
l.insert(0, 3, 10.0, 8);
assert_eq!(
l.nbest_tokens(10),
vec![
vec!["ABC"],
vec!["A", "BC"],
vec!["AB", "C"],
vec!["A", "B", "C"]
]
);
assert!(l.nbest_tokens(0).is_empty());
assert_eq!(l.nbest_tokens(1), vec![vec!["ABC"]]);
}
#[test]
fn log_sum_exp_matches_direct() {
let mut x = 0.0;
for (i, y) in [1.0, 2.0, 3.0].iter().enumerate() {
x = log_sum_exp(x, *y, i == 0);
}
approx(x, (1f64.exp() + 2f64.exp() + 3f64.exp()).ln());
}
#[test]
fn marginals() {
let mut l = Lattice::new("ABC", 1, 2);
l.insert(0, 1, 1.0, 3);
l.insert(1, 1, 1.2, 4);
l.insert(2, 1, 2.5, 5);
l.insert(0, 2, 3.0, 6);
l.insert(1, 2, 4.0, 7);
l.insert(0, 3, 2.0, 8);
let mut probs = vec![0.0; 9];
let p1 = (1.0f64 + 1.2 + 2.5).exp();
let p2 = (3.0f64 + 2.5).exp();
let p3 = (1.0f64 + 4.0).exp();
let p4 = 2.0f64.exp();
let z = p1 + p2 + p3 + p4;
let log_z = l.populate_marginal(1.0, &mut probs);
approx(log_z, z.ln());
approx(probs[0], 0.0);
approx(probs[3], (p1 + p3) / z);
approx(probs[4], p1 / z);
approx(probs[5], (p1 + p2) / z);
approx(probs[6], p2 / z);
approx(probs[7], p3 / z);
approx(probs[8], p4 / z);
}
}