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
|
- `FitHiddenMarkovModel` fits a single-observation sequence instead of
|
||||||
crashing the process: a transition row with no evidence keeps its
|
crashing the process: a transition row with no evidence keeps its
|
||||||
previous estimate rather than dividing by zero.
|
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
|
## [1.0.0] - 2026-09-03
|
||||||
|
|
||||||
|
|||||||
@@ -354,6 +354,12 @@ func (m *HiddenMarkovModel) Viterbi(observations []int) (states []int, logProbab
|
|||||||
arg = k
|
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 := make([]int, len(observations))
|
||||||
path[len(observations)-1] = arg
|
path[len(observations)-1] = arg
|
||||||
for t := len(observations) - 1; t > 0; t-- {
|
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