From 9dec8035c5dbc11a7cfd217c917c561ae3532814 Mon Sep 17 00:00:00 2001 From: Hunter Gayden Date: Thu, 6 Aug 2020 16:27:47 -0400 Subject: [PATCH 1/5] Define preliminary Viterbi algorithm spec for #25 --- src/algorithms/optimization/viterbi.js | 46 ++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/algorithms/optimization/viterbi.js diff --git a/src/algorithms/optimization/viterbi.js b/src/algorithms/optimization/viterbi.js new file mode 100644 index 00000000..2f93560d --- /dev/null +++ b/src/algorithms/optimization/viterbi.js @@ -0,0 +1,46 @@ +/** + * 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 + */ +/** + * The set of all possible observations of the system + * @typedef {Observation[]} ObservationSpace + */ + +/** + * Any possible hidden (i.e. unobservable) state of the system + * @typedef {any} State + */ +/** + * The set of all possible hidden states + * @typedef {State[]} StateSpace + */ + +/** + * A 2D matrix, A, of size K x K (where K is the number of states in the state space) such that A[i][j] gives the probability of a transition from state i to state j + * @typedef {Object} TransitionMatrix + */ +/** + * A 2D matrix, B, of size K x N (where K is the number of states in the state space and N is the number of observations in the observation space) such that B[i][j] gives the probability of observation j resulting from state i + * @typedef {Object} EmissionMatrix + */ + +/** + * Determine the Viterbi Path of a given set of Observations + * + * @param {ObservationSpace} O - observation space + * @param {StateSpace} S - state space + * @param {Object} P - an array that, for each state in S, gives the probability that the initial hidden state is that state + * @param {Observation[]} Y - the sequence of recorded observations for which the Viterbi Path is to be found + * @param {TransitionMatrix} A - a "transition matrix" of [ length(StSp) X length(StSp) ] numbers giving the probability of transition from every state to every other state + * @param {EmissionMatrix} B - an "emission matrix" of [ length(StSp) X length(ObsSp) ] numbers giving the probability of every possible observation for every possible state + * + * @return {State[]} (denoted X) the most likely sequence of (hidden) states + */ From d77ffc38f4dcbeb744eca63eb5f156800592ec16 Mon Sep 17 00:00:00 2001 From: Hunter Gayden Date: Fri, 7 Aug 2020 17:08:08 -0400 Subject: [PATCH 2/5] Close #25: Implement Viterbi algo and fix JSDocs --- src/algorithms/index.js | 2 + src/algorithms/optimization/index.js | 5 ++ src/algorithms/optimization/viterbi.js | 93 +++++++++++++++++++++++--- 3 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 src/algorithms/optimization/index.js diff --git a/src/algorithms/index.js b/src/algorithms/index.js index 2335cebb..e10051bc 100644 --- a/src/algorithms/index.js +++ b/src/algorithms/index.js @@ -1,5 +1,6 @@ const geometry = require('./geometry'); const math = require('./math'); +const optimization = require('./optimization'); const string = require('./string'); const search = require('./search'); const sort = require('./sort'); @@ -7,6 +8,7 @@ const sort = require('./sort'); module.exports = { geometry, math, + optimization, string, search, sort diff --git a/src/algorithms/optimization/index.js b/src/algorithms/optimization/index.js new file mode 100644 index 00000000..b1780618 --- /dev/null +++ b/src/algorithms/optimization/index.js @@ -0,0 +1,5 @@ +const viterbi = require('./viterbi'); + +module.exports = { + viterbi +}; diff --git a/src/algorithms/optimization/viterbi.js b/src/algorithms/optimization/viterbi.js index 2f93560d..5ffa050f 100644 --- a/src/algorithms/optimization/viterbi.js +++ b/src/algorithms/optimization/viterbi.js @@ -10,7 +10,7 @@ * @typedef {any} Observation */ /** - * The set of all possible observations of the system + * An unordered list of all possible observations of the system * @typedef {Observation[]} ObservationSpace */ @@ -19,28 +19,99 @@ * @typedef {any} State */ /** - * The set of all possible hidden states + * An unordered list of all possible hidden states * @typedef {State[]} StateSpace */ /** - * A 2D matrix, A, of size K x K (where K is the number of states in the state space) such that A[i][j] gives the probability of a transition from state i to state j - * @typedef {Object} TransitionMatrix + * 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>} TransitionMap */ /** - * A 2D matrix, B, of size K x N (where K is the number of states in the state space and N is the number of observations in the observation space) such that B[i][j] gives the probability of observation j resulting from state i - * @typedef {Object} EmissionMatrix + * 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>} EmissionMap */ /** * Determine the Viterbi Path of a given set of Observations * - * @param {ObservationSpace} O - observation space - * @param {StateSpace} S - state space - * @param {Object} P - an array that, for each state in S, gives the probability that the initial hidden state is that state + * @param {ObservationSpace} O + * @param {StateSpace} S + * @param {Object} 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 {TransitionMatrix} A - a "transition matrix" of [ length(StSp) X length(StSp) ] numbers giving the probability of transition from every state to every other state - * @param {EmissionMatrix} B - an "emission matrix" of [ length(StSp) X length(ObsSp) ] numbers giving the probability of every possible observation for every possible state + * @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 + * + * the innermost (nested) arrays give probabilities at each observation in sequence; each member corresponds with a successive observation + * the outermost (container) array enumerates all possible probability sequences; each member assumes the unique initial state at the same index in S + * @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) { + 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) { // for each observation (in sequence) + for (let i = 0; i < S.length; ++i) { // find the probability of every possible state + let Pmax = -1; // guarantee inner conditional satisfied on first iteration + let kPmax; + let k = 0; + do { + let p = T1[k][j - 1] * A[ S[k] ][ S[i] ] * B[ S[i] ][ Y[j] ]; + if (p > Pmax) { + Pmax = p; + kPmax = k; + } + } 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) { // 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) { + 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; From 5849d18eec8426cb82c8547c0e9502640d70a235 Mon Sep 17 00:00:00 2001 From: Hunter Gayden Date: Fri, 7 Aug 2020 17:35:38 -0400 Subject: [PATCH 3/5] Add test for Viterbi algorithm --- test/algorithms/optimization/testViterbi.js | 43 +++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 test/algorithms/optimization/testViterbi.js diff --git a/test/algorithms/optimization/testViterbi.js b/test/algorithms/optimization/testViterbi.js new file mode 100644 index 00000000..c7a38261 --- /dev/null +++ b/test/algorithms/optimization/testViterbi.js @@ -0,0 +1,43 @@ +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); + }); +}); From 437d0762097e160889b283462f5d26633ceb8b00 Mon Sep 17 00:00:00 2001 From: Hunter Gayden Date: Sat, 8 Aug 2020 11:53:05 -0400 Subject: [PATCH 4/5] Apply ESLint rules --- src/algorithms/optimization/viterbi.js | 40 +++++++++++---------- test/algorithms/optimization/testViterbi.js | 2 +- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/algorithms/optimization/viterbi.js b/src/algorithms/optimization/viterbi.js index 5ffa050f..03c50b37 100644 --- a/src/algorithms/optimization/viterbi.js +++ b/src/algorithms/optimization/viterbi.js @@ -24,12 +24,14 @@ */ /** - * A nested map such that two state names in order gives the probability of a transition from the first to the second: + * 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>} TransitionMap */ /** - * A nested map such that a state name followed by an observation name gives the probability of that observation resulting from that state: + * 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>} EmissionMap */ @@ -39,49 +41,51 @@ * * @param {ObservationSpace} O * @param {StateSpace} S - * @param {Object} 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 {Object} 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 - * - * the innermost (nested) arrays give probabilities at each observation in sequence; each member corresponds with a successive observation - * the outermost (container) array enumerates all possible probability sequences; each member assumes the unique initial state at the same index in S + /** 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 + /** 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) { - T1[i] = [ P0[ S[i] ] * B[ S[i] ][ Y[0] ] ]; - T2[i] = [ null ]; + 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) { // for each observation (in sequence) - for (let i = 0; i < S.length; ++i) { // find the probability of every possible 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 { - let p = T1[k][j - 1] * A[ S[k] ][ S[i] ] * B[ S[i] ][ Y[j] ]; + const p = T1[k][j - 1] * A[S[k]][S[i]] * B[S[i]][Y[j]]; if (p > Pmax) { Pmax = p; kPmax = k; } - } while (++k < S.length); + k += 1; + } while (k < S.length); T1[i][j] = Pmax; T2[i][j] = kPmax; } @@ -95,7 +99,7 @@ function viterbi(O, S, P0, Y, A, B) { // 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) { // skip the value used to init 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]; @@ -103,7 +107,7 @@ function viterbi(O, S, P0, Y, A, B) { } // determine Z and X in reverse order - for (let j = T - 1; j > 0; --j) { + for (let j = T - 1; j > 0; j -= 1) { Z[j - 1] = T2[Z[j]][j]; X[j - 1] = S[Z[j - 1]]; } diff --git a/test/algorithms/optimization/testViterbi.js b/test/algorithms/optimization/testViterbi.js index c7a38261..22243027 100644 --- a/test/algorithms/optimization/testViterbi.js +++ b/test/algorithms/optimization/testViterbi.js @@ -37,7 +37,7 @@ describe('Viterbi Algorithm', () => { const Y = ['normal', 'cold', 'dizzy']; const X = ['healthy', 'healthy', 'fever']; // expected results - it (`should return the expected path: ${X.join(',')}`, () => { + it(`should return the expected path: ${X.join(',')}`, () => { assert.deepEqual(viterbi(O, S, P, Y, A, B), X); }); }); From 82bede51d3f815ec2de6b3d72a99d26a4487d136 Mon Sep 17 00:00:00 2001 From: Hunter Gayden Date: Sat, 8 Aug 2020 12:05:49 -0400 Subject: [PATCH 5/5] Add Mocha functions to ESLint env in test file --- test/algorithms/optimization/testViterbi.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/algorithms/optimization/testViterbi.js b/test/algorithms/optimization/testViterbi.js index 22243027..99d79143 100644 --- a/test/algorithms/optimization/testViterbi.js +++ b/test/algorithms/optimization/testViterbi.js @@ -1,3 +1,4 @@ +/* eslint-env mocha */ const viterbi = require('../../../src').algorithms.optimization.viterbi; const assert = require('assert');