Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Viterbi Algorithm and a simple test case #117

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/algorithms/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
const geometry = require('./geometry');
const math = require('./math');
const optimization = require('./optimization');
const string = require('./string');
const search = require('./search');
const sort = require('./sort');

module.exports = {
geometry,
math,
optimization,
string,
search,
sort
Expand Down
5 changes: 5 additions & 0 deletions src/algorithms/optimization/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const viterbi = require('./viterbi');

module.exports = {
viterbi
};
121 changes: 121 additions & 0 deletions src/algorithms/optimization/viterbi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Draft specification based on the pseudocode in Wikipedia's article on the
* Viterbi algorithm (as of August 6, 2020):
* https://en.wikipedia.org/wiki/Viterbi_algorithm#pseudocode
*
*/

/**
* Any possible observation of the system
* @typedef {any} Observation
*/
/**
* An unordered list of all possible observations of the system
* @typedef {Observation[]} ObservationSpace
*/

/**
* Any possible hidden (i.e. unobservable) state of the system
* @typedef {any} State
*/
/**
* An unordered list of all possible hidden states
* @typedef {State[]} StateSpace
*/

/**
* A nested map such that two state names in order gives the probability of a
* transition from the first to the second:
* map.name1.name2 => probability of transition from state 1 to state 2
* @typedef {Object<String, Object<String, Number>>} TransitionMap
*/
/**
* A nested map such that a state name followed by an observation name gives the
* probability of that observation resulting from that state:
* map.stateName.obsName => probability of named state leading to named observation
* @typedef {Object<String, Object<String, Number>>} EmissionMap
*/

/**
* Determine the Viterbi Path of a given set of Observations
*
* @param {ObservationSpace} O
* @param {StateSpace} S
* @param {Object<String, Number>} P0 - a map which gives the probability that
* each state in S is the initial hidden state
* @param {Observation[]} Y - the sequence of recorded observations for which
* the Viterbi Path is to be found
* @param {TransitionMap} A
* @param {EmissionMap} B
*
* @return {State[]} (denoted X) the most likely sequence of (hidden) states
*/
function viterbi(O, S, P0, Y, A, B) {
/** probability of the state with greatest likelihood at each observation,
* given the previous state
* @type Number[][]
*/
const T1 = [];

/** state (with corresponding probability in T1) with greatest likelihood at
* each observation, given the previous state
* @type State[][]
*/
const T2 = [];

// Calculate the probability of each initial state
// These are irrespective of any observations
for (let i = 0; i < S.length; i += 1) {
T1[i] = [P0[S[i]] * B[S[i]][Y[0]]];
T2[i] = [null];
}

// determine the probability of each state state underlying each observation
// the calculations account for the current observation the probability of
// the path leading to the previous most likely state
for (let j = 1; j < Y.length; j += 1) { // for each observation (in sequence)
for (let i = 0; i < S.length; i += 1) { // find the probability of every possible state
let Pmax = -1; // guarantee inner conditional satisfied on first iteration
let kPmax;
let k = 0;
do {
const p = T1[k][j - 1] * A[S[k]][S[i]] * B[S[i]][Y[j]];
if (p > Pmax) {
Pmax = p;
kPmax = k;
}
k += 1;
} while (k < S.length);
T1[i][j] = Pmax;
T2[i][j] = kPmax;
}
}

// choose most likely path from T1
const T = Y.length;
const Z = []; // indices
const X = []; // states

// determine final observed state
Z[T - 1] = T2[0][T - 1]; // initialize to known value
X[T - 1] = S[Z[T - 1]];
for (let i = 1; i < S.length; i += 1) { // skip the value used to init Z[T - 1]
if (T1[i][T - 1] > T1[Z[T - 1]][T - 1]) {
Z[T - 1] = i;
X[T - 1] = S[i];
}
}

// determine Z and X in reverse order
for (let j = T - 1; j > 0; j -= 1) {
Z[j - 1] = T2[Z[j]][j];
X[j - 1] = S[Z[j - 1]];
}

return X;
}


// function to compose a transition state matrix from a MarkovChain

module.exports = viterbi;
44 changes: 44 additions & 0 deletions test/algorithms/optimization/testViterbi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint-env mocha */
const viterbi = require('../../../src').algorithms.optimization.viterbi;

const assert = require('assert');
/**
* test implementation using parameters/results given in Wikipedia's example:
* https://en.wikipedia.org/wiki/Viterbi_algorithm#Example
*/
describe('Viterbi Algorithm', () => {
// define observation space and state space
const O = ['normal', 'cold', 'dizzy'];
const S = ['healthy', 'fever'];

// arbitrarily define parameters P, A, and B
const P = { healthy: 0.6, fever: 0.4 };
const A = {
healthy: {
healthy: 0.7,
fever: 0.3
},
fever: {
healthy: 0.4,
fever: 0.6
}
};
const B = {
healthy: {
normal: 0.5,
cold: 0.4,
dizzy: 0.1
},
fever: {
normal: 0.1,
cold: 0.3,
dizzy: 0.6
}
};

const Y = ['normal', 'cold', 'dizzy'];
const X = ['healthy', 'healthy', 'fever']; // expected results
it(`should return the expected path: ${X.join(',')}`, () => {
assert.deepEqual(viterbi(O, S, P, Y, A, B), X);
});
});