fix(stats): refuse a zero-probability sequence in Viterbi

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-27 15:41:19 +02:00
parent e507adeb64
commit 2f9358c512
3 changed files with 35 additions and 0 deletions
+6
View File
@@ -354,6 +354,12 @@ func (m *HiddenMarkovModel) Viterbi(observations []int) (states []int, logProbab
arg = k
}
}
if math.IsInf(best, -1) {
// Every path has probability zero, the structural zero an
// emission or transition carries: the same refusal Forward makes,
// not a meaningless path beside a −Inf log probability.
return nil, 0, base.Errf("%s: the observation sequence has probability zero under the model", name)
}
path := make([]int, len(observations))
path[len(observations)-1] = arg
for t := len(observations) - 1; t > 0; t-- {
+26
View File
@@ -375,3 +375,29 @@ func TestHiddenMarkovFitSingleObservation(t *testing.T) {
}
}
}
func TestViterbiZeroProbabilitySequence(t *testing.T) {
// Symbol 2 is emitted by no state, so every path through the sequence
// has probability zero. Forward and Smooth refuse such a sequence, and
// Viterbi documents the same refusals: a meaningless path beside a
// −Inf log probability is not an answer.
model, err := NewHiddenMarkovModel(
[]float64{0.5, 0.5},
[]float64{0.5, 0.5, 0.5, 0.5},
[]float64{0.5, 0.5, 0, 0.5, 0.5, 0},
)
if err != nil {
t.Fatal(err)
}
if _, _, err := model.Viterbi([]int{0, 2, 1}); err == nil {
t.Fatal("a zero-probability sequence was accepted by Viterbi")
}
// The live symbols of the same model still decode.
path, lp, err := model.Viterbi([]int{0, 1})
if err != nil {
t.Fatal(err)
}
if path[0] != path[1] || lp >= 0 {
t.Fatalf("Viterbi([0, 1]) = (%v, %g), want a coherent path with a negative log probability", path, lp)
}
}