fix(stats): refuse a zero-probability sequence in Viterbi
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- `FitHiddenMarkovModel` fits a single-observation sequence instead of
|
||||
crashing the process: a transition row with no evidence keeps its
|
||||
previous estimate rather than dividing by zero.
|
||||
- `Viterbi` refuses a sequence of probability zero under the model,
|
||||
the same refusal `Forward` makes, instead of returning a meaningless
|
||||
path beside a log probability of -Inf.
|
||||
|
||||
## [1.0.0] - 2026-09-03
|
||||
|
||||
|
||||
@@ -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-- {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user