fix(stats): keep hidden Markov fitting alive on a single observation
Assisted-by: Qwen 3.8 Flash
This commit is contained in:
@@ -20,6 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
longer exhausts its passes on a matrix it should have declared
|
longer exhausts its passes on a matrix it should have declared
|
||||||
converged.
|
converged.
|
||||||
|
|
||||||
|
**Statistics.**
|
||||||
|
|
||||||
|
- `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.
|
||||||
|
|
||||||
## [1.0.0] - 2026-09-03
|
## [1.0.0] - 2026-09-03
|
||||||
|
|
||||||
The initial release of Tensor, a scientific computing library in pure
|
The initial release of Tensor, a scientific computing library in pure
|
||||||
|
|||||||
@@ -524,6 +524,15 @@ func hmmReestimate(model *HiddenMarkovModel, observations []int, gamma, xi [][]f
|
|||||||
for j := range states {
|
for j := range states {
|
||||||
den += transition[k*states+j]
|
den += transition[k*states+j]
|
||||||
}
|
}
|
||||||
|
if den == 0 {
|
||||||
|
// No transition evidence reached this row, the single-
|
||||||
|
// observation sequence being the case in point: dividing the
|
||||||
|
// zero fills the row with NaN the constructor refuses, and
|
||||||
|
// the sweep after it crashed on the nil model that refusal
|
||||||
|
// left. The row keeps its previous estimate instead.
|
||||||
|
copy(transition[k*states:(k+1)*states], model.Transition[k*states:(k+1)*states])
|
||||||
|
continue
|
||||||
|
}
|
||||||
for j := range states {
|
for j := range states {
|
||||||
transition[k*states+j] = math.Max(transition[k*states+j]/den, hmmFloor)
|
transition[k*states+j] = math.Max(transition[k*states+j]/den, hmmFloor)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,3 +343,35 @@ func TestHiddenMarkovZeroProbabilitySequence(t *testing.T) {
|
|||||||
t.Fatalf("the certain sequence answered (%g, %v), want (0, [1])", ll, filtered[0])
|
t.Fatalf("the certain sequence answered (%g, %v), want (0, [1])", ll, filtered[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHiddenMarkovFitSingleObservation(t *testing.T) {
|
||||||
|
// One observation carries emission and initial evidence but no
|
||||||
|
// transition evidence: the re-estimation divided the zero count into
|
||||||
|
// NaN rows, the constructor refused them, and the sweep then called
|
||||||
|
// a method on the nil model and crashed the process. The fit must
|
||||||
|
// answer with a valid model whose transitions keep their starting
|
||||||
|
// estimate.
|
||||||
|
g := core.NewGenerator(11)
|
||||||
|
res, err := FitHiddenMarkovModel(g, []int{0}, 2, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FitHiddenMarkovModel on one observation: %v", err)
|
||||||
|
}
|
||||||
|
if res.Model == nil {
|
||||||
|
t.Fatal("FitHiddenMarkovModel on one observation returned no model")
|
||||||
|
}
|
||||||
|
if math.IsNaN(res.LogLikelihood) || math.IsInf(res.LogLikelihood, 0) {
|
||||||
|
t.Fatalf("log likelihood = %g, want a finite value", res.LogLikelihood)
|
||||||
|
}
|
||||||
|
for k := range 2 {
|
||||||
|
sum := 0.0
|
||||||
|
for _, v := range res.Model.Transition[k*2 : k*2+2] {
|
||||||
|
if math.IsNaN(v) || v < 0 {
|
||||||
|
t.Fatalf("transition row %d holds %g, want probabilities", k, v)
|
||||||
|
}
|
||||||
|
sum += v
|
||||||
|
}
|
||||||
|
if math.Abs(sum-1) > 1e-9 {
|
||||||
|
t.Fatalf("transition row %d sums to %g, want 1", k, sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user