Repository: librosa/librosa

Issue:
Strange Viterbi (discriminative) decoding results (including probabilities greater than 1)
**Describe the bug**
Trying to do Viterbi decoding and ran into the decoding algorithm producing results that both contradict common sense and also produces log-probabilities greater than 0 (ie probabilities greater than 1).


**To Reproduce**
```

import numpy as np

transition_matrix = np.array(
    [[.999, 0.001],
     [0.001, .999]])
p_state = np.array([0.9, 0.1])
probs = np.array([0.4, 0.07, 0.2, 0.3, 0.1])
probs = np.stack((1-probs,probs))

import librosa

states, logp = librosa.sequence.viterbi_discriminative(probs, transition_matrix, p_state, p_init=probs[:,0], return_logp=True)

print(states)
print(f"probability {np.exp(logp)}")

>>> [1 1 1 1 1]
>>> probability 6.693160293126715

```

**Expected behavior**
Probability should be at most 1? (Also, isn't state 0 more likely than state 1?)

**Software versions***

Please run the following Python code snippet and paste the output below.
```
Darwin-19.6.0-x86_64-i386-64bit
Python 3.7.6 (default, Jan  8 2020, 13:42:34) 
[Clang 4.0.1 (tags/RELEASE_401/final)]
NumPy 1.18.1
SciPy 1.4.1
librosa 0.8.0
INSTALLED VERSIONS
------------------
python: 3.7.6 (default, Jan  8 2020, 13:42:34) 
[Clang 4.0.1 (tags/RELEASE_401/final)]

librosa: 0.8.0

audioread: 2.1.9
numpy: 1.18.1
scipy: 1.4.1
sklearn: 0.22.1
joblib: 0.14.1
decorator: 4.4.1
soundfile: 0.10.3
resampy: 0.2.2
numba: 0.48.0

numpydoc: 0.9.2
sphinx: 2.4.0
sphinx_rtd_theme: None
sphinxcontrib.versioning: None
sphinx-gallery: None
pytest: 5.3.5
pytest-mpl: None
pytest-cov: None
matplotlib: 3.1.3
presets: None
```


Minimal test environment failing
**Describe the bug**
It looks like our minimal-dependency test environment (3.6) is now failing for reasons that I can't quite understand yet.

For example, this build on main from today (2022-04-19):
https://github.com/librosa/librosa/runs/6076437385?check_suite_focus=true
is failing with the message:
```
Run pytest
Traceback (most recent call last):
  File "/usr/share/miniconda/envs/test/bin/pytest", line 7, in <module>
    from py.test import console_main
ImportError: cannot import name 'console_main'
Error: Process completed with exit code 1.
```
while the previous build on main (2022-04-08) worked fine:
https://github.com/librosa/librosa/runs/5890711265?check_suite_focus=true

At first I figured this was another pytest update messing things up, but in fact pytest has not changed.  Digging into the conda environments for both builds shows no meaningful differences, here's the diff:

```diff
--- working	2022-04-19 07:12:46.727680430 -0400
+++ notworking	2022-04-19 07:13:03.708107945 -0400
@@ -24,18 +24,18 @@
 idna                      3.3                pyhd8ed1ab_0    conda-forge
 importlib-metadata        4.8.1            py36h5fab9bb_0    conda-forge
 importlib_metadata        4.8.1                hd8ed1ab_1    conda-forge
-importlib_resources       5.6.0              pyhd8ed1ab_0    conda-forge
+importlib_resources       5.7.1              pyhd8ed1ab_0    conda-forge
 iniconfig                 1.1.1              pyh9f0ad1d_0    conda-forge
 jbig                      2.1               h7f98852_2003    conda-forge
 jinja2                    3.0.3              pyhd8ed1ab_0    conda-forge
 joblib                    0.14.0                     py_0    conda-forge
-jpeg                      9e                   h7f98852_0    conda-forge
+jpeg                      9e                   h166bdaf_1    conda-forge
 kiwisolver                1.3.1            py36h605e78d_1    conda-forge
 lcms2                     2.12                 hddcbb42_0    conda-forge
 ld_impl_linux-64          2.36.1               hea4e1c9_2    conda-forge
 lerc                      3.0                  h9c3ff4c_0    conda-forge
-libblas                   3.9.0           11_linux64_openblas    conda-forge
-libcblas                  3.9.0           11_linux64_openblas    conda-forge
+libblas                   3.9.0           13_linux64_openblas    conda-forge
+libcblas                  3.9.0           13_linux64_openblas    conda-forge
 libdeflate                1.10                 h7f98852_0    conda-forge
 libffi                    3.4.2                h7f98852_5    conda-forge
 libflac                   1.3.4                h27087fc_0    conda-forge
@@ -45,10 +45,10 @@
 libgfortran4              7.5.0               h14aa051_20    conda-forge
 libgomp                   11.2.0              h1d223b6_15    conda-forge
 libiconv                  1.16                 h516909a_0    conda-forge
-liblapack                 3.9.0           11_linux64_openblas    conda-forge
+liblapack                 3.9.0           13_linux64_openblas    conda-forge
 libnsl                    2.0.0                h7f98852_0    conda-forge
 libogg                    1.3.4                h7f98852_1    conda-forge
-libopenblas               0.3.17               hf726d26_1  
+libopenblas               0.3.18               hf726d26_0  
 libopus                   1.3.1                h7f98852_1    conda-forge
 libpng                    1.6.37               h21135ba_2    conda-forge
 libsndfile                1.0.31               h9c3ff4c_1    conda-forge
@@ -78,7 +78,7 @@
 py                        1.11.0             pyh6c4a22f_0    conda-forge
 pycparser                 2.21               pyhd8ed1ab_0    conda-forge
 pyopenssl                 22.0.0             pyhd8ed1ab_0    conda-forge
-pyparsing                 3.0.7              pyhd8ed1ab_0    conda-forge
+pyparsing                 3.0.8              pyhd8ed1ab_0    conda-forge
 pysocks                   1.7.1            py36h5fab9bb_3    conda-forge
 pysoundfile               0.10.2                  py_1001    conda-forge
 pytest                    6.2.5            py36h5fab9bb_0    conda-forge
@@ -96,12 +96,12 @@
 setuptools                58.0.4           py36h5fab9bb_2    conda-forge
 six                       1.16.0             pyh6c4a22f_0    conda-forge
 soxr                      0.2.7                    pypi_0    pypi
-sqlite                    3.37.1               h4ff8645_0    conda-forge
+sqlite                    3.38.2               h4ff8645_0    conda-forge
 tk                        8.6.12               h27826a3_0    conda-forge
 toml                      0.10.2             pyhd8ed1ab_0    conda-forge
 tomli                     1.2.2              pyhd8ed1ab_0    conda-forge
 tornado                   6.1              py36h8f6f2f9_1    conda-forge
-typing_extensions         4.1.1              pyha770c72_0    conda-forge
+typing_extensions         4.2.0              pyha770c72_0    conda-forge
 urllib3                   1.26.9             pyhd8ed1ab_0    conda-forge
 wheel                     0.37.1             pyhd8ed1ab_0    conda-forge
 x264                      1!152.20180806       h14c3975_0    conda-forge
```
Importlib-resources seems like the only potential culprit here, but nothing suspicious shows up in the [changelog](https://importlib-resources.readthedocs.io/en/latest/history.html#v5-7-0).  Still, I'll take a crack at pinning the version back to 5.6 to see if that resolves things.


Discussion:
This is not a bug, but I do think the documentation could be clearer.

Discriminative Viterbi differs from standard Viterbi in that it's trying to maximize `P[state | obs]` rather than `P[obs | state]`.  You can use Bayes' rule to convert between these two, which will introduce a normalization factor of `P[obs]`.  Since this is constant with respect to the choice of the state sequence (it's marginalized out), it is not included in the calculation.  The resulting "log probabilities" are unnormalized, and may be better thought of as energy than log-probability, as is common in the conditional random fields literature.

Probably we should document this more clearly -- any takers?


Resolving patch:
diff --git a/librosa/sequence.py b/librosa/sequence.py
--- a/librosa/sequence.py
+++ b/librosa/sequence.py
@@ -1097,6 +1097,11 @@ def viterbi_discriminative(
     ``P[State(t) = s | Obs(t)] / P[State(t) = s]``, where the denominator
     is the marginal probability of state ``s`` occurring as given by ``p_state``.
 
+    Note that because the denominator ``P[State(t) = s]`` is not explicitly
+    calculated, the resulting probabilities (or log-probabilities) are not
+    normalized.  If using the `return_logp=True` option (see below),
+    be aware that the "probabilities" may not sum to (and may exceed) 1.
+
     Parameters
     ----------
     prob : np.ndarray [shape=(..., n_states, n_steps), non-negative]
@@ -1124,8 +1129,8 @@ def viterbi_discriminative(
         If ``prob`` contains multiple input channels,
         then each channel is decoded independently.
     logp : scalar [float] or np.ndarray
-        If ``return_logp=True``, the log probability of ``states`` given
-        the observations.
+        If ``return_logp=True``, the (unnormalized) log probability
+        of ``states`` given the observations.
 
     See Also
     --------
@@ -1302,6 +1307,10 @@ def viterbi_binary(prob, transition, *, p_state=None, p_init=None, return_logp=F
     The output is a binary matrix ``states[s, t]`` indicating whether each
     state ``s`` is active at time ``t``.
 
+    Like `viterbi_discriminative`, the probabilities of the optimal state sequences
+    are not normalized here.  If using the `return_logp=True` option (see below),
+    be aware that the "probabilities" may not sum to (and may exceed) 1.
+
     Parameters
     ----------
     prob : np.ndarray [shape=(..., n_steps,) or (..., n_states, n_steps)], non-negative
@@ -1332,7 +1341,7 @@ def viterbi_binary(prob, transition, *, p_state=None, p_init=None, return_logp=F
         If not provided, it is assumed to be uniform.
 
     return_logp : bool
-        If ``True``, return the log-likelihood of the state sequence.
+        If ``True``, return the (unnormalized) log-likelihood of the state sequences.
 
     Returns
     -------
@@ -1340,8 +1349,8 @@ def viterbi_binary(prob, transition, *, p_state=None, p_init=None, return_logp=F
     states : np.ndarray [shape=(..., n_states, n_steps)]
         The most likely state sequence.
     logp : np.ndarray [shape=(..., n_states,)]
-        If ``return_logp=True``, the log probability of each state activation
-        sequence ``states``
+        If ``return_logp=True``, the (unnormalized) log probability of each
+        state activation sequence ``states``
 
     See Also
     --------


Test patch: