├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── bmw │ └── hmm │ ├── ForwardBackwardAlgorithm.java │ ├── SequenceState.java │ ├── Transition.java │ ├── Utils.java │ └── ViterbiAlgorithm.java └── test └── java └── com └── bmw └── hmm ├── ForwardBackwardAlgorithmTest.java └── ViterbiAlgorithmTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /target 3 | .classpath 4 | .project 5 | .settings/ 6 | /deploy_snapshot.sh 7 | /deploy_release.sh 8 | /.idea 9 | *.iml 10 | *~ 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015-2016, BMW Car IT GmbH and BMW AG 2 | Author: Stefan Holder (stefan.holder@bmw.de) 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | This project has dependencies to: 17 | Apache Maven, under The Apache Software License, Version 2.0 18 | JUnit under Eclipse Public License - v 1.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This library implements Hidden Markov Models (HMM) for time-inhomogeneous Markov processes. 4 | This means that, in contrast to many other HMM implementations, there can be different 5 | states and a different transition matrix at each time step. 6 | 7 | This library provides an implementation of 8 | * The Viterbi algorithm, which computes the most likely sequence of states. 9 | * The forward-backward algorithm, which computes the probability of all state candidates given 10 | the entire sequence of observations. This process is also called smoothing. 11 | 12 | # Applications 13 | 14 | This library was initially created for HMM-based map matching according to the paper 15 | "NEWSON, Paul; KRUMM, John. Hidden Markov map matching through noise and sparseness. 16 | In: Proceedings of the 17th ACM SIGSPATIAL international conference on advances in geographic 17 | information systems. ACM, 2009. S. 336-343." 18 | 19 | [Graphhopper](https://graphhopper.com/) [map matching](https://github.com/graphhopper/map-matching) 20 | is now using the hmm-lib for matching GPS positions to OpenStreetMap maps. 21 | 22 | The [offline-map-matching](https://github.com/bmwcarit/offline-map-matching) project 23 | demonstrates how to use the hmm-lib for map matching but does not provide integration to any 24 | particular map. 25 | 26 | Besides map matching, the hmm-lib can also be used for other applications. 27 | 28 | # License 29 | 30 | This library is licensed under the 31 | [Apache 2.0 license](http://www.apache.org/licenses/LICENSE-2.0.html). 32 | 33 | # Dependencies 34 | 35 | Except for testing, there are no dependencies to other libraries. 36 | 37 | # Maven 38 | 39 | To use this library, add the following to your pom.xml: 40 | 41 | ``` 42 | 43 | com.bmw.hmm 44 | hmm-lib 45 | 1.1.0-SNAPSHOT 46 | 47 | ``` 48 | 49 | If you want to use snapshots, add 50 | ``` 51 | 52 | ... 53 | 54 | hmm-lib-snapshots 55 | https://raw.github.com/bmwcarit/hmm-lib/mvn-snapshots/ 56 | 57 | true 58 | always 59 | 60 | 61 | 62 | ``` 63 | 64 | # Contribute 65 | Contributions are welcome! For bug reports, please create an issue. 66 | For code contributions (e.g. new features or bugfixes), please create a pull request. 67 | 68 | # Changes 69 | * 1.1.0: 70 | * Add forward-backward algorithm, which performs smoothing on the hidden state variables. 71 | * The Viterbi algorithm now optionally returns smoothing probabilities for the states of the 72 | most likely sequence. 73 | * 1.0.0: 74 | * API redesign to allow calling the Viterbi algorithm iteratively. This gives the library user 75 | increased flexibility and optimization opportunities when computing transition and observation 76 | probabilities. Moreover, the new API enables better handling of HMM breaks. 77 | * Add support for transition descriptors. For map matching, this allows retrieving the paths 78 | between matched positions (the entire matched route) after computing the most likely sequence. 79 | * Reduce memory footprint from O(t\*n²) to O(t\*n) or even O(t) in many applications, where t is 80 | the number of time steps and n is the number of candidates per time step. 81 | * 0.2.0: Extend HmmProbabilities interface to include the observation 82 | * 0.1.0: Initial release 83 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 19 | 4.0.0 20 | com.bmw.hmm 21 | hmm-lib 22 | 1.1.1-SNAPSHOT 23 | jar 24 | 25 | hmm-lib 26 | Hidden Markov Model (HMM) library for time-inhomogeneous Markov processes 27 | https://github.com/bmwcarit/hmm-lib 28 | 29 | 30 | 31 | BMW Car IT GmbH, BMW AG 32 | http://www.bmw-carit.com, http://www.bmw.com 33 | 34 | 35 | 36 | 37 | 38 | Apache License, Version 2.0 39 | http://www.apache.org/licenses/LICENSE-2.0.txt 40 | repo 41 | 42 | 43 | 44 | 45 | scm:git:git@github.com:bmwcarit/hmm-lib.git 46 | scm:git:git@github.com:bmwcarit/hmm-lib.git 47 | git@github.com:bmwcarit/hmm-lib 48 | 49 | 50 | 51 | 52 | hmm-lib-releases 53 | https://raw.github.com/bmwcarit/hmm-lib/mvn-releases/ 54 | 55 | 56 | 57 | hmm-lib-snapshots 58 | https://raw.github.com/bmwcarit/hmm-lib/mvn-snapshots/ 59 | 60 | 61 | 62 | 63 | UTF-8 64 | UTF-8 65 | 66 | 67 | 68 | 69 | junit 70 | junit 71 | test 72 | 4.13.1 73 | 74 | 75 | 76 | 77 | 78 | 79 | maven-compiler-plugin 80 | 3.1 81 | 82 | 1.7 83 | 1.7 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/main/java/com/bmw/hmm/ForwardBackwardAlgorithm.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2016, BMW AG 3 | * Author: Stefan Holder (stefan.holder@bmw.de) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.bmw.hmm; 19 | 20 | import java.util.ArrayList; 21 | import java.util.Collection; 22 | import java.util.Collections; 23 | import java.util.LinkedHashMap; 24 | import java.util.List; 25 | import java.util.ListIterator; 26 | import java.util.Map; 27 | 28 | /** 29 | * Computes the forward-backward algorithm, also known as smoothing. 30 | * This algorithm computes the probability of each state candidate at each time step given the 31 | * entire observation sequence. 32 | * 33 | * @param the state type 34 | * @param the observation type 35 | */ 36 | public class ForwardBackwardAlgorithm { 37 | 38 | /** 39 | * Internal state of each time step. 40 | */ 41 | private class Step { 42 | final Collection candidates; 43 | final Map emissionProbabilities; 44 | final Map, Double> transitionProbabilities; 45 | final Map forwardProbabilities; 46 | final double scalingDivisor; // Normalizes sum of forward probabilities to 1. 47 | 48 | Step(Collection candidates, Map emissionProbabilities, 49 | Map, Double> transitionProbabilities, 50 | Map forwardProbabilities, double scalingDivisor) { 51 | this.candidates = candidates; 52 | this.emissionProbabilities = emissionProbabilities; 53 | this.transitionProbabilities = transitionProbabilities; 54 | this.forwardProbabilities = forwardProbabilities; 55 | this.scalingDivisor = scalingDivisor; 56 | } 57 | } 58 | 59 | private static final double DELTA = 1e-8; 60 | 61 | private List steps; 62 | private Collection prevCandidates; // For on-the-fly computation of forward probabilities 63 | 64 | /** 65 | * Lets the computation start with the given initial state probabilities. 66 | * 67 | * @param initialStates Pass a collection with predictable iteration order such as 68 | * {@link ArrayList} to ensure deterministic results. 69 | * 70 | * @param initialProbabilities Initial probabilities for each initial state. 71 | * 72 | * @throws NullPointerException if any initial probability is missing 73 | * 74 | * @throws IllegalStateException if this method or 75 | * {@link #startWithInitialObservation(Object, Collection, Map)} has already been called 76 | */ 77 | public void startWithInitialStateProbabilities(Collection initialStates, 78 | Map initialProbabilities) { 79 | if (!sumsToOne(initialProbabilities.values())) { 80 | throw new IllegalArgumentException("Initial state probabilities must sum to 1."); 81 | } 82 | 83 | initializeStateProbabilities(null, initialStates, initialProbabilities); 84 | } 85 | 86 | /** 87 | * Lets the computation start at the given first observation. 88 | * 89 | * @param candidates Pass a collection with predictable iteration order such as 90 | * {@link ArrayList} to ensure deterministic results. 91 | * 92 | * @param emissionProbabilities Emission probabilities of the first observation for 93 | * each of the road position candidates. 94 | * 95 | * @throws NullPointerException if any emission probability is missing 96 | * 97 | * @throws IllegalStateException if this method or 98 | * {@link #startWithInitialStateProbabilities(Collection, Map)}} has already been called 99 | */ 100 | public void startWithInitialObservation(O observation, Collection candidates, 101 | Map emissionProbabilities) { 102 | initializeStateProbabilities(observation, candidates, emissionProbabilities); 103 | } 104 | 105 | /** 106 | * Processes the next time step. 107 | * 108 | * @param candidates Pass a collection with predictable iteration order such as 109 | * {@link ArrayList} to ensure deterministic results. 110 | * 111 | * @param emissionProbabilities Emission probabilities for each candidate state. 112 | * 113 | * @param transitionProbabilities Transition probability between all pairs of candidates. 114 | * A transition probability of zero is assumed for every missing transition. 115 | * 116 | * @throws NullPointerException if any emission probability is missing 117 | * 118 | * @throws IllegalStateException if neither 119 | * {@link #startWithInitialStateProbabilities(Collection, Map)} nor 120 | * {@link #startWithInitialObservation(Object, Collection, Map)} has not been called before 121 | */ 122 | public void nextStep(O observation, Collection candidates, 123 | Map emissionProbabilities, 124 | Map, Double> transitionProbabilities) { 125 | if (steps == null) { 126 | throw new IllegalStateException("startWithInitialStateProbabilities(...) or " + 127 | "startWithInitialObservation(...) must be called first."); 128 | } 129 | 130 | // Make defensive copies. 131 | candidates = new ArrayList<>(candidates); 132 | emissionProbabilities = new LinkedHashMap<>(emissionProbabilities); 133 | transitionProbabilities = new LinkedHashMap<>(transitionProbabilities); 134 | 135 | // On-the-fly computation of forward probabilities at each step allows to efficiently 136 | // (re)compute smoothing probabilities at any time step. 137 | final Map prevForwardProbabilities = 138 | steps.get(steps.size() - 1).forwardProbabilities; 139 | final Map curForwardProbabilities = new LinkedHashMap<>(); 140 | double sum = 0.0; 141 | for (S curState : candidates) { 142 | final double forwardProbability = computeForwardProbability(curState, 143 | prevForwardProbabilities, emissionProbabilities, transitionProbabilities); 144 | curForwardProbabilities.put(curState, forwardProbability); 145 | sum += forwardProbability; 146 | } 147 | 148 | normalizeForwardProbabilities(curForwardProbabilities, sum); 149 | steps.add(new Step(candidates, emissionProbabilities, transitionProbabilities, 150 | curForwardProbabilities, sum)); 151 | 152 | prevCandidates = candidates; 153 | } 154 | 155 | /** 156 | * Returns the probability for all candidates of all time steps given all observations. 157 | * The time steps include the initial states/observations time step. 158 | */ 159 | public List> computeSmoothingProbabilities() { 160 | return computeSmoothingProbabilities(null); 161 | } 162 | 163 | /** 164 | * Returns the probability of the specified candidate at the specified zero-based time step 165 | * given the observations up to t. 166 | */ 167 | public double forwardProbability(int t, S candidate) { 168 | if (steps == null) { 169 | throw new IllegalStateException("No time steps yet."); 170 | } 171 | 172 | return steps.get(t).forwardProbabilities.get(candidate); 173 | } 174 | 175 | /** 176 | * Returns the probability of the specified candidate given all previous observations. 177 | */ 178 | public double currentForwardProbability(S candidate) { 179 | if (steps == null) { 180 | throw new IllegalStateException("No time steps yet."); 181 | } 182 | 183 | return forwardProbability(steps.size() - 1, candidate); 184 | } 185 | 186 | /** 187 | * Returns the log probability of the entire observation sequence. 188 | * The log is returned to prevent arithmetic underflows for very small probabilities. 189 | */ 190 | public double observationLogProbability() { 191 | if (steps == null) { 192 | throw new IllegalStateException("No time steps yet."); 193 | } 194 | 195 | double result = 0.0; 196 | for (Step step : steps) { 197 | result += Math.log(step.scalingDivisor); 198 | } 199 | return result; 200 | } 201 | 202 | /** 203 | * @see #computeSmoothingProbabilities() 204 | * 205 | * @param outBackwardProbabilities optional output parameter for backward probabilities, 206 | * must be empty if not null. 207 | */ 208 | List> computeSmoothingProbabilities( 209 | List> outBackwardProbabilities) { 210 | assert outBackwardProbabilities == null || outBackwardProbabilities.isEmpty(); 211 | 212 | final List> result = new ArrayList<>(); 213 | 214 | ListIterator stepIter = steps.listIterator(steps.size()); 215 | if (!stepIter.hasPrevious()) { 216 | return result; 217 | } 218 | 219 | // Initial step 220 | Step step = stepIter.previous(); 221 | Map backwardProbabilities = new LinkedHashMap<>(); 222 | for (S candidate : step.candidates) { 223 | backwardProbabilities.put(candidate, 1.0); 224 | } 225 | if (outBackwardProbabilities != null) { 226 | outBackwardProbabilities.add(backwardProbabilities); 227 | } 228 | result.add(computeSmoothingProbabilitiesVector(step.candidates, step.forwardProbabilities, 229 | backwardProbabilities)); 230 | 231 | // Remaining steps 232 | while (stepIter.hasPrevious()) { 233 | final Step nextStep = step; 234 | step = stepIter.previous(); 235 | final Map nextBackwardProbabilities = backwardProbabilities; 236 | backwardProbabilities = new LinkedHashMap<>(); 237 | for (S candidate : step.candidates) { 238 | // Using the scaling divisors of the next steps eliminates the need to 239 | // normalize the smoothing probabilities, 240 | // see also https://en.wikipedia.org/wiki/Forward%E2%80%93backward_algorithm. 241 | final double probability = computeUnscaledBackwardProbability(candidate, 242 | nextBackwardProbabilities, nextStep) / nextStep.scalingDivisor; 243 | backwardProbabilities.put(candidate, probability); 244 | } 245 | if (outBackwardProbabilities != null) { 246 | outBackwardProbabilities.add(backwardProbabilities); 247 | } 248 | result.add(computeSmoothingProbabilitiesVector(step.candidates, 249 | step.forwardProbabilities, backwardProbabilities)); 250 | } 251 | Collections.reverse(result); 252 | return result; 253 | } 254 | 255 | private Map computeSmoothingProbabilitiesVector(Collection candidates, 256 | Map forwardProbabilities, Map backwardProbabilities) { 257 | assert forwardProbabilities.size() == backwardProbabilities.size(); 258 | final Map result = new LinkedHashMap<>(); 259 | for (S state : candidates) { 260 | final double probability = forwardProbabilities.get(state) 261 | * backwardProbabilities.get(state); 262 | assert Utils.probabilityInRange(probability, DELTA); 263 | result.put(state, probability); 264 | } 265 | assert sumsToOne(result.values()); 266 | return result; 267 | } 268 | 269 | private double computeUnscaledBackwardProbability(S candidate, 270 | Map nextBackwardProbabilities, Step nextStep) { 271 | double result = 0.0; 272 | for (S nextCandidate : nextStep.candidates) { 273 | result += nextStep.emissionProbabilities.get(nextCandidate) * 274 | nextBackwardProbabilities.get(nextCandidate) * transitionProbability( 275 | candidate, nextCandidate, nextStep.transitionProbabilities); 276 | } 277 | return result; 278 | } 279 | 280 | private boolean sumsToOne(Collection probabilities) { 281 | double sum = 0.0; 282 | for (double probability : probabilities) { 283 | sum += probability; 284 | } 285 | return Math.abs(sum - 1.0) <= DELTA; 286 | } 287 | 288 | /** 289 | * @param observation Use only if HMM only starts with first observation. 290 | */ 291 | private void initializeStateProbabilities(O observation, Collection candidates, 292 | Map initialProbabilities) { 293 | if (steps != null) { 294 | throw new IllegalStateException("Initial probabilities have already been set."); 295 | } 296 | 297 | candidates = new ArrayList<>(candidates); // Defensive copy 298 | steps = new ArrayList<>(); 299 | 300 | final Map forwardProbabilities = new LinkedHashMap<>(); 301 | double sum = 0.0; 302 | for (S candidate : candidates) { 303 | final double forwardProbability = initialProbabilities.get(candidate); 304 | forwardProbabilities.put(candidate, forwardProbability); 305 | sum += forwardProbability; 306 | } 307 | 308 | normalizeForwardProbabilities(forwardProbabilities, sum); 309 | steps.add(new Step(candidates, null, null, forwardProbabilities, sum)); 310 | 311 | prevCandidates = candidates; 312 | } 313 | 314 | /** 315 | * Returns the non-normalized forward probability of the specified state. 316 | */ 317 | private double computeForwardProbability(S curState, 318 | Map prevForwardProbabilities, Map emissionProbabilities, 319 | Map, Double> transitionProbabilities) { 320 | double result = 0.0; 321 | for (S prevState : prevCandidates) { 322 | result += prevForwardProbabilities.get(prevState) * 323 | transitionProbability(prevState, curState, transitionProbabilities); 324 | } 325 | result *= emissionProbabilities.get(curState); 326 | return result; 327 | } 328 | 329 | /** 330 | * Returns zero probability for non-existing transitions. 331 | */ 332 | private double transitionProbability(S prevState, S curState, 333 | Map, Double> transitionProbabilities) { 334 | final Double transitionProbability = 335 | transitionProbabilities.get(new Transition(prevState, curState)); 336 | return transitionProbability == null ? 0.0 : transitionProbability; 337 | } 338 | 339 | private void normalizeForwardProbabilities( 340 | Map forwardProbabilities, double sum) { 341 | for (Map.Entry entry : forwardProbabilities.entrySet()) { 342 | forwardProbabilities.put(entry.getKey(), entry.getValue() / sum); 343 | } 344 | } 345 | 346 | } 347 | -------------------------------------------------------------------------------- /src/main/java/com/bmw/hmm/SequenceState.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015-2016, BMW Car IT GmbH and BMW AG 3 | * Author: Stefan Holder (stefan.holder@bmw.de) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.bmw.hmm; 19 | 20 | /** 21 | * State of the most likely sequence with additional information. 22 | * 23 | * @param the state type 24 | * @param the observation type 25 | * @param the transition descriptor type 26 | */ 27 | public class SequenceState { 28 | 29 | public final S state; 30 | 31 | /** 32 | * Null if HMM was started with initial state probabilities and state is the initial state. 33 | */ 34 | public final O observation; 35 | 36 | /** 37 | * Null if transition descriptor was not provided. 38 | */ 39 | public final D transitionDescriptor; 40 | 41 | /** 42 | * Probability of this state given all observations. 43 | */ 44 | public final Double smoothingProbability; 45 | 46 | public SequenceState(S state, O observation, D transitionDescriptor, Double smoothingProbability) { 47 | this.state = state; 48 | this.observation = observation; 49 | this.transitionDescriptor = transitionDescriptor; 50 | this.smoothingProbability = smoothingProbability; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/bmw/hmm/Transition.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015-2016, BMW Car IT GmbH and BMW AG 3 | * Author: Stefan Holder (stefan.holder@bmw.de) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.bmw.hmm; 19 | 20 | import java.util.Objects; 21 | 22 | /** 23 | * Represents the transition between two consecutive candidates. 24 | * 25 | * @param the state type 26 | */ 27 | public class Transition { 28 | public final S fromCandidate; 29 | public final S toCandidate; 30 | 31 | public Transition(S fromCandidate, S toCandidate) { 32 | this.fromCandidate = fromCandidate; 33 | this.toCandidate = toCandidate; 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return Objects.hash(fromCandidate, toCandidate); 39 | } 40 | 41 | @Override 42 | public boolean equals(Object obj) { 43 | if (this == obj) 44 | return true; 45 | if (obj == null) 46 | return false; 47 | if (getClass() != obj.getClass()) 48 | return false; 49 | @SuppressWarnings("unchecked") 50 | Transition other = (Transition) obj; 51 | return Objects.equals(fromCandidate, other.fromCandidate) && Objects.equals(toCandidate, 52 | other.toCandidate); 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return "Transition [fromCandidate=" + fromCandidate + ", toCandidate=" 58 | + toCandidate + "]"; 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/bmw/hmm/Utils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015, BMW Car IT GmbH 3 | * Author: Stefan Holder (stefan.holder@bmw.de) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.bmw.hmm; 19 | 20 | import java.util.LinkedHashMap; 21 | import java.util.Map; 22 | 23 | /** 24 | * Implementation utilities. 25 | */ 26 | class Utils { 27 | 28 | static int initialHashMapCapacity(int maxElements) { 29 | // Default load factor of HashMaps is 0.75 30 | return (int)(maxElements / 0.75) + 1; 31 | } 32 | 33 | static Map logToNonLogProbabilities(Map logProbabilities) { 34 | final Map result = new LinkedHashMap<>(); 35 | for (Map.Entry entry : logProbabilities.entrySet()) { 36 | result.put(entry.getKey(), Math.exp(entry.getValue())); 37 | } 38 | return result; 39 | } 40 | 41 | /** 42 | * Note that this check must not be used for probability densities. 43 | */ 44 | static boolean probabilityInRange(double probability, double delta) { 45 | return probability >= -delta && probability <= 1.0 + delta; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/bmw/hmm/ViterbiAlgorithm.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015-2016, BMW Car IT GmbH and BMW AG 3 | * Author: Stefan Holder (stefan.holder@bmw.de) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.bmw.hmm; 19 | 20 | import java.util.ArrayList; 21 | import java.util.Collection; 22 | import java.util.Collections; 23 | import java.util.LinkedHashMap; 24 | import java.util.List; 25 | import java.util.ListIterator; 26 | import java.util.Map; 27 | 28 | /** 29 | * Implementation of the Viterbi algorithm for time-inhomogeneous Markov processes, 30 | * meaning that the set of states and state transition probabilities are not necessarily fixed 31 | * for all time steps. The plain Viterbi algorithm for stationary Markov processes is described e.g. 32 | * in Rabiner, Juang, An introduction to Hidden Markov Models, IEEE ASSP Mag., pp 4-16, June 1986. 33 | * 34 | *

Generally expects logarithmic probabilities as input to prevent arithmetic underflows for 35 | * small probability values. 36 | * 37 | *

This algorithm supports storing transition objects in 38 | * {@link #nextStep(Object, Collection, Map, Map, Map)}. For instance if a HMM is 39 | * used for map matching, this could be routes between road position candidates. 40 | * The transition descriptors of the most likely sequence can be retrieved later in 41 | * {@link SequenceState#transitionDescriptor} and hence do not need to be stored by the 42 | * caller. Since the caller does not know in advance which transitions will occur in the most 43 | * likely sequence, this reduces the number of transitions that need to be kept in memory 44 | * from t*n² to t*n since only one transition descriptor is stored per back pointer, 45 | * where t is the number of time steps and n the number of candidates per time step. 46 | * 47 | *

For long observation sequences, back pointers usually converge to a single path after a 48 | * certain number of time steps. For instance, when matching GPS coordinates to roads, the last 49 | * GPS positions in the trace usually do not affect the first road matches anymore. 50 | * This implementation exploits this fact by letting the Java garbage collector 51 | * take care of unreachable back pointers. If back pointers converge to a single path after a 52 | * constant number of time steps, only O(t) back pointers and transition descriptors need to be 53 | * stored in memory. 54 | * 55 | * @param the state type 56 | * @param the observation type 57 | * @param the transition descriptor type. Pass {@link Object} if transition descriptors are not 58 | * needed. 59 | */ 60 | public class ViterbiAlgorithm { 61 | 62 | /** 63 | * Stores addition information for each candidate. 64 | */ 65 | private static class ExtendedState { 66 | 67 | S state; 68 | 69 | /** 70 | * Back pointer to previous state candidate in the most likely sequence. 71 | * Back pointers are chained using plain Java references. 72 | * This allows garbage collection of unreachable back pointers. 73 | */ 74 | ExtendedState backPointer; 75 | 76 | O observation; 77 | D transitionDescriptor; 78 | 79 | ExtendedState(S state, 80 | ExtendedState backPointer, 81 | O observation, D transitionDescriptor) { 82 | this.state = state; 83 | this.backPointer = backPointer; 84 | this.observation = observation; 85 | this.transitionDescriptor = transitionDescriptor; 86 | } 87 | } 88 | 89 | private static class ForwardStepResult { 90 | final Map newMessage; 91 | 92 | /** 93 | * Includes back pointers to previous state candidates for retrieving the most likely 94 | * sequence after the forward pass. 95 | */ 96 | final Map> newExtendedStates; 97 | 98 | ForwardStepResult(int numberStates) { 99 | newMessage = new LinkedHashMap<>(Utils.initialHashMapCapacity(numberStates)); 100 | newExtendedStates = new LinkedHashMap<>(Utils.initialHashMapCapacity(numberStates)); 101 | } 102 | } 103 | 104 | /** 105 | * Allows to retrieve the most likely sequence using back pointers. 106 | */ 107 | private Map> lastExtendedStates; 108 | 109 | private Collection prevCandidates; 110 | 111 | /** 112 | * For each state s_t of the current time step t, message.get(s_t) contains the log 113 | * probability of the most likely sequence ending in state s_t with given observations 114 | * o_1, ..., o_t. 115 | * 116 | * Formally, this is max log p(s_1, ..., s_t, o_1, ..., o_t) w.r.t. s_1, ..., s_{t-1}. 117 | * Note that to compute the most likely state sequence, it is sufficient and more 118 | * efficient to compute in each time step the joint probability of states and observations 119 | * instead of computing the conditional probability of states given the observations. 120 | */ 121 | private Map message; 122 | 123 | private boolean isBroken = false; 124 | 125 | private ForwardBackwardAlgorithm forwardBackward; 126 | 127 | private List> messageHistory; // For debugging only. 128 | 129 | /** 130 | * Need to construct a new instance for each sequence of observations. 131 | */ 132 | public ViterbiAlgorithm() { } 133 | 134 | /** 135 | * Whether to store intermediate forward messages 136 | * (probabilities of intermediate most likely paths) for debugging. 137 | * Default: false 138 | * Must be called before processing is started. 139 | */ 140 | public ViterbiAlgorithm setKeepMessageHistory(boolean keepMessageHistory) { 141 | if (processingStarted()) { 142 | throw new IllegalStateException("Processing has already started."); 143 | } 144 | 145 | if (keepMessageHistory) { 146 | messageHistory = new ArrayList<>(); 147 | } else { 148 | messageHistory = null; 149 | } 150 | return this; 151 | } 152 | 153 | /** 154 | * Whether to compute smoothing probabilities using the {@link ForwardBackwardAlgorithm} 155 | * for the states of the most likely sequence. Note that this significantly increases 156 | * computation time and memory footprint. 157 | * Default: false 158 | * Must be called before processing is started. 159 | */ 160 | public ViterbiAlgorithm setComputeSmoothingProbabilities( 161 | boolean computeSmoothingProbabilities) { 162 | if (processingStarted()) { 163 | throw new IllegalStateException("Processing has already started."); 164 | } 165 | 166 | if (computeSmoothingProbabilities) { 167 | forwardBackward = new ForwardBackwardAlgorithm<>(); 168 | } else { 169 | forwardBackward = null; 170 | } 171 | return this; 172 | } 173 | 174 | /** 175 | * Returns whether {@link #startWithInitialObservation(Object, Collection, Map)} 176 | * or {@link #startWithInitialStateProbabilities(Collection, Map)} has already been called. 177 | */ 178 | public boolean processingStarted() { 179 | return message != null; 180 | } 181 | 182 | /** 183 | * Lets the HMM computation start with the given initial state probabilities. 184 | * 185 | * @param initialStates Pass a collection with predictable iteration order such as 186 | * {@link ArrayList} to ensure deterministic results. 187 | * @param initialLogProbabilities Initial log probabilities for each initial state. 188 | * 189 | * @throws NullPointerException if any initial probability is missing 190 | * 191 | * @throws IllegalStateException if this method or 192 | * {@link #startWithInitialObservation(Object, Collection, Map)} 193 | * has already been called 194 | */ 195 | public void startWithInitialStateProbabilities(Collection initialStates, 196 | Map initialLogProbabilities) { 197 | initializeStateProbabilities(null, initialStates, initialLogProbabilities); 198 | 199 | if (forwardBackward != null) { 200 | forwardBackward.startWithInitialStateProbabilities(initialStates, 201 | Utils.logToNonLogProbabilities(initialLogProbabilities)); 202 | } 203 | } 204 | 205 | /** 206 | * Lets the HMM computation start at the given first observation and uses the given emission 207 | * probabilities as the initial state probability for each starting state s. 208 | * 209 | * @param candidates Pass a collection with predictable iteration order such as 210 | * {@link ArrayList} to ensure deterministic results. 211 | * @param emissionLogProbabilities Emission log probabilities of the first observation for 212 | * each of the road position candidates. 213 | * 214 | * @throws NullPointerException if any emission probability is missing 215 | * 216 | * @throws IllegalStateException if this method or 217 | * {@link #startWithInitialStateProbabilities(Collection, Map)}} has already been called 218 | */ 219 | public void startWithInitialObservation(O observation, Collection candidates, 220 | Map emissionLogProbabilities) { 221 | initializeStateProbabilities(observation, candidates, emissionLogProbabilities); 222 | 223 | if (forwardBackward != null) { 224 | forwardBackward.startWithInitialObservation(observation, candidates, 225 | Utils.logToNonLogProbabilities(emissionLogProbabilities)); 226 | } 227 | } 228 | 229 | /** 230 | * Processes the next time step. Must not be called if the HMM is broken. 231 | * 232 | * @param candidates Pass a collection with predictable iteration order such as 233 | * {@link ArrayList} to ensure deterministic results. 234 | * @param emissionLogProbabilities Emission log probabilities for each candidate state. 235 | * 236 | * @param transitionLogProbabilities Transition log probability between all pairs of candidates. 237 | * A transition probability of zero is assumed for every missing transition. 238 | * 239 | * @param transitionDescriptors Optional objects that describes the transitions. 240 | * 241 | * @throws NullPointerException if any emission probability is missing 242 | * 243 | * @throws IllegalStateException if neither 244 | * {@link #startWithInitialStateProbabilities(Collection, Map)} nor 245 | * {@link #startWithInitialObservation(Object, Collection, Map)} 246 | * has not been called before or if this method is called after an HMM break has occurred 247 | */ 248 | public void nextStep(O observation, Collection candidates, 249 | Map emissionLogProbabilities, 250 | Map, Double> transitionLogProbabilities, 251 | Map, D> transitionDescriptors) { 252 | if (!processingStarted()) { 253 | throw new IllegalStateException( 254 | "startWithInitialStateProbabilities() or startWithInitialObservation() " 255 | + "must be called first."); 256 | } 257 | if (isBroken) { 258 | throw new IllegalStateException("Method must not be called after an HMM break."); 259 | } 260 | 261 | // Forward step 262 | ForwardStepResult forwardStepResult = forwardStep(observation, prevCandidates, 263 | candidates, message, emissionLogProbabilities, transitionLogProbabilities, 264 | transitionDescriptors); 265 | isBroken = hmmBreak(forwardStepResult.newMessage); 266 | if (isBroken) return; 267 | if (messageHistory != null) { 268 | messageHistory.add(forwardStepResult.newMessage); 269 | } 270 | message = forwardStepResult.newMessage; 271 | lastExtendedStates = forwardStepResult.newExtendedStates; 272 | 273 | prevCandidates = new ArrayList<>(candidates); // Defensive copy. 274 | 275 | if (forwardBackward != null) { 276 | forwardBackward.nextStep(observation, candidates, 277 | Utils.logToNonLogProbabilities(emissionLogProbabilities), 278 | Utils.logToNonLogProbabilities(transitionLogProbabilities)); 279 | } 280 | } 281 | 282 | /** 283 | * See {@link #nextStep(Object, Collection, Map, Map, Map)} 284 | */ 285 | public void nextStep(O observation, Collection candidates, 286 | Map emissionLogProbabilities, 287 | Map, Double> transitionLogProbabilities) { 288 | nextStep(observation, candidates, emissionLogProbabilities, transitionLogProbabilities, 289 | new LinkedHashMap, D>()); 290 | } 291 | 292 | /** 293 | * Returns the most likely sequence of states for all time steps. This includes the initial 294 | * states / initial observation time step. If an HMM break occurred in the last time step t, 295 | * then the most likely sequence up to t-1 is returned. See also {@link #isBroken()}. 296 | * 297 | *

Formally, the most likely sequence is argmax p([s_0,] s_1, ..., s_T | o_1, ..., o_T) 298 | * with respect to s_1, ..., s_T, where s_t is a state candidate at time step t, 299 | * o_t is the observation at time step t and T is the number of time steps. 300 | */ 301 | public List> computeMostLikelySequence() { 302 | if (message == null) { 303 | // Return empty most likely sequence if there are no time steps or if initial 304 | // observations caused an HMM break. 305 | return new ArrayList<>(); 306 | } else { 307 | return retrieveMostLikelySequence(); 308 | } 309 | } 310 | 311 | /** 312 | * Returns whether an HMM occurred in the last time step. 313 | * 314 | * An HMM break means that the probability of all states equals zero. 315 | */ 316 | public boolean isBroken() { 317 | return isBroken; 318 | } 319 | 320 | /** 321 | * @see #setComputeSmoothingProbabilities(boolean) 322 | */ 323 | public boolean isComputeSmoothingProbabilities() { 324 | return forwardBackward != null; 325 | } 326 | 327 | /** 328 | * @see #setKeepMessageHistory(boolean) 329 | */ 330 | public boolean isKeepMessageHistory() { 331 | return messageHistory != null; 332 | } 333 | 334 | /** 335 | * Returns the sequence of intermediate forward messages for each time step. 336 | * Returns null if message history is not kept. 337 | */ 338 | public List> messageHistory() { 339 | return messageHistory; 340 | } 341 | 342 | public String messageHistoryString() { 343 | if (messageHistory == null) { 344 | throw new IllegalStateException("Message history was not recorded."); 345 | } 346 | 347 | final StringBuilder sb = new StringBuilder(); 348 | sb.append("Message history with log probabilies\n\n"); 349 | int i = 0; 350 | for (Map message : messageHistory) { 351 | sb.append("Time step " + i + "\n"); 352 | i++; 353 | for (S state : message.keySet()) { 354 | sb.append(state + ": " + message.get(state) + "\n"); 355 | } 356 | sb.append("\n"); 357 | } 358 | return sb.toString(); 359 | } 360 | 361 | /** 362 | * Returns whether the specified message is either empty or only contains state candidates 363 | * with zero probability and thus causes the HMM to break. 364 | */ 365 | private boolean hmmBreak(Map message) { 366 | for (double logProbability : message.values()) { 367 | if (logProbability != Double.NEGATIVE_INFINITY) { 368 | return false; 369 | } 370 | } 371 | return true; 372 | } 373 | 374 | /** 375 | * @param observation Use only if HMM only starts with first observation. 376 | */ 377 | private void initializeStateProbabilities(O observation, Collection candidates, 378 | Map initialLogProbabilities) { 379 | if (processingStarted()) { 380 | throw new IllegalStateException("Initial probabilities have already been set."); 381 | } 382 | 383 | // Set initial log probability for each start state candidate based on first observation. 384 | // Do not assign initialLogProbabilities directly to message to not rely on its iteration 385 | // order. 386 | final Map initialMessage = new LinkedHashMap<>(); 387 | for (S candidate : candidates) { 388 | final Double logProbability = initialLogProbabilities.get(candidate); 389 | if (logProbability == null) { 390 | throw new NullPointerException("No initial probability for " + candidate); 391 | } 392 | initialMessage.put(candidate, logProbability); 393 | } 394 | 395 | isBroken = hmmBreak(initialMessage); 396 | if (isBroken) return; 397 | 398 | message = initialMessage; 399 | if (messageHistory != null) { 400 | messageHistory.add(message); 401 | } 402 | 403 | lastExtendedStates = new LinkedHashMap<>(); 404 | for (S candidate : candidates) { 405 | lastExtendedStates.put(candidate, 406 | new ExtendedState(candidate, null, observation, null)); 407 | } 408 | 409 | prevCandidates = new ArrayList<>(candidates); // Defensive copy. 410 | } 411 | 412 | /** 413 | * Computes the new forward message and the back pointers to the previous states. 414 | * 415 | * @throws NullPointerException if any emission probability is missing 416 | */ 417 | private ForwardStepResult forwardStep(O observation, Collection prevCandidates, 418 | Collection curCandidates, Map message, 419 | Map emissionLogProbabilities, 420 | Map, Double> transitionLogProbabilities, 421 | Map,D> transitionDescriptors) { 422 | final ForwardStepResult result = new ForwardStepResult<>(curCandidates.size()); 423 | assert !prevCandidates.isEmpty(); 424 | 425 | for (S curState : curCandidates) { 426 | double maxLogProbability = Double.NEGATIVE_INFINITY; 427 | S maxPrevState = null; 428 | for (S prevState : prevCandidates) { 429 | final double logProbability = message.get(prevState) + transitionLogProbability( 430 | prevState, curState, transitionLogProbabilities); 431 | if (logProbability > maxLogProbability) { 432 | maxLogProbability = logProbability; 433 | maxPrevState = prevState; 434 | } 435 | } 436 | // Throws NullPointerException if curState is not stored in the map. 437 | result.newMessage.put(curState, maxLogProbability 438 | + emissionLogProbabilities.get(curState)); 439 | 440 | // Note that maxPrevState == null if there is no transition with non-zero probability. 441 | // In this case curState has zero probability and will not be part of the most likely 442 | // sequence, so we don't need an ExtendedState. 443 | if (maxPrevState != null) { 444 | final Transition transition = new Transition<>(maxPrevState, curState); 445 | final ExtendedState extendedState = new ExtendedState<>(curState, 446 | lastExtendedStates.get(maxPrevState), observation, 447 | transitionDescriptors.get(transition)); 448 | result.newExtendedStates.put(curState, extendedState); 449 | } 450 | } 451 | return result; 452 | } 453 | 454 | private double transitionLogProbability(S prevState, S curState, Map, 455 | Double> transitionLogProbabilities) { 456 | final Double transitionLogProbability = 457 | transitionLogProbabilities.get(new Transition(prevState, curState)); 458 | if (transitionLogProbability == null) { 459 | return Double.NEGATIVE_INFINITY; // Transition has zero probability. 460 | } else { 461 | return transitionLogProbability; 462 | } 463 | } 464 | 465 | /** 466 | * Retrieves the first state of the current forward message with maximum probability. 467 | */ 468 | private S mostLikelyState() { 469 | // Otherwise an HMM break would have occurred and message would be null. 470 | assert !message.isEmpty(); 471 | 472 | S result = null; 473 | double maxLogProbability = Double.NEGATIVE_INFINITY; 474 | for (Map.Entry entry : message.entrySet()) { 475 | if (entry.getValue() > maxLogProbability) { 476 | result = entry.getKey(); 477 | maxLogProbability = entry.getValue(); 478 | } 479 | } 480 | 481 | assert result != null; // Otherwise an HMM break would have occurred. 482 | return result; 483 | } 484 | 485 | /** 486 | * Retrieves most likely sequence from the internal back pointer sequence. 487 | */ 488 | private List> retrieveMostLikelySequence() { 489 | // Otherwise an HMM break would have occurred and message would be null. 490 | assert !message.isEmpty(); 491 | 492 | final S lastState = mostLikelyState(); 493 | 494 | // Retrieve most likely state sequence in reverse order 495 | final List> result = new ArrayList<>(); 496 | ExtendedState es = lastExtendedStates.get(lastState); 497 | final ListIterator> smoothingIter; 498 | if (forwardBackward != null) { 499 | List> smoothingProbabilities = 500 | forwardBackward.computeSmoothingProbabilities(); 501 | smoothingIter = smoothingProbabilities.listIterator(smoothingProbabilities.size()); 502 | } else { 503 | smoothingIter = null; 504 | } 505 | while(es != null) { 506 | final Double smoothingProbability; 507 | if (forwardBackward != null) { 508 | // Number of time steps is the same for Viterbi and ForwardBackward algorithm. 509 | assert smoothingIter.hasPrevious(); 510 | final Map smoothingProbabilitiesVector = smoothingIter.previous(); 511 | smoothingProbability = smoothingProbabilitiesVector.get(es.state); 512 | } else { 513 | smoothingProbability = null; 514 | } 515 | final SequenceState ss = new SequenceState<>(es.state, es.observation, 516 | es.transitionDescriptor, smoothingProbability); 517 | result.add(ss); 518 | es = es.backPointer; 519 | } 520 | 521 | Collections.reverse(result); 522 | return result; 523 | } 524 | 525 | 526 | } 527 | -------------------------------------------------------------------------------- /src/test/java/com/bmw/hmm/ForwardBackwardAlgorithmTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2016, BMW AG 3 | * Author: Stefan Holder (stefan.holder@bmw.de) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.bmw.hmm; 19 | 20 | import static org.junit.Assert.assertEquals; 21 | import java.util.ArrayList; 22 | import java.util.LinkedHashMap; 23 | import java.util.List; 24 | import java.util.Map; 25 | 26 | import org.junit.Test; 27 | 28 | import com.bmw.hmm.Transition; 29 | 30 | public class ForwardBackwardAlgorithmTest { 31 | 32 | private static class Rain { 33 | final static Rain T = new Rain(); 34 | final static Rain F = new Rain(); 35 | 36 | @Override 37 | public String toString() { 38 | if (this == T) { 39 | return "Rain"; 40 | } else if (this == F) { 41 | return "Sun"; 42 | } 43 | throw new IllegalStateException(); 44 | } 45 | } 46 | 47 | private static class Umbrella { 48 | final static Umbrella T = new Umbrella(); 49 | final static Umbrella F = new Umbrella(); 50 | 51 | @Override 52 | public String toString() { 53 | if (this == T) { 54 | return "Umbrella"; 55 | } else if (this == F) { 56 | return "No umbrella"; 57 | } 58 | throw new IllegalStateException(); 59 | } 60 | } 61 | 62 | /** 63 | * Example taken from https://en.wikipedia.org/wiki/Forward%E2%80%93backward_algorithm. 64 | */ 65 | @Test 66 | public void testForwardBackward() { 67 | final List candidates = new ArrayList<>(); 68 | candidates.add(Rain.T); 69 | candidates.add(Rain.F); 70 | 71 | final Map initialStateProbabilities = new LinkedHashMap<>(); 72 | initialStateProbabilities.put(Rain.T, 0.5); 73 | initialStateProbabilities.put(Rain.F, 0.5); 74 | 75 | final Map emissionProbabilitiesForUmbrella = new LinkedHashMap<>(); 76 | emissionProbabilitiesForUmbrella.put(Rain.T, 0.9); 77 | emissionProbabilitiesForUmbrella.put(Rain.F, 0.2); 78 | 79 | final Map emissionProbabilitiesForNoUmbrella = new LinkedHashMap<>(); 80 | emissionProbabilitiesForNoUmbrella.put(Rain.T, 0.1); 81 | emissionProbabilitiesForNoUmbrella.put(Rain.F, 0.8); 82 | 83 | final Map, Double> transitionProbabilities = new LinkedHashMap<>(); 84 | transitionProbabilities.put(new Transition(Rain.T, Rain.T), 0.7); 85 | transitionProbabilities.put(new Transition(Rain.T, Rain.F), 0.3); 86 | transitionProbabilities.put(new Transition(Rain.F, Rain.T), 0.3); 87 | transitionProbabilities.put(new Transition(Rain.F, Rain.F), 0.7); 88 | 89 | final ForwardBackwardAlgorithm fw = new ForwardBackwardAlgorithm<>(); 90 | fw.startWithInitialStateProbabilities(candidates, initialStateProbabilities); 91 | fw.nextStep(Umbrella.T, candidates, emissionProbabilitiesForUmbrella, 92 | transitionProbabilities); 93 | fw.nextStep(Umbrella.T, candidates, emissionProbabilitiesForUmbrella, 94 | transitionProbabilities); 95 | fw.nextStep(Umbrella.F, candidates, emissionProbabilitiesForNoUmbrella, 96 | transitionProbabilities); 97 | fw.nextStep(Umbrella.T, candidates, emissionProbabilitiesForUmbrella, 98 | transitionProbabilities); 99 | fw.nextStep(Umbrella.T, candidates, emissionProbabilitiesForUmbrella, 100 | transitionProbabilities); 101 | 102 | final List> result = fw.computeSmoothingProbabilities(); 103 | assertEquals(6, result.size()); 104 | final double DELTA = 1e-4; 105 | assertEquals(0.6469, result.get(0).get(Rain.T), DELTA); 106 | assertEquals(0.3531, result.get(0).get(Rain.F), DELTA); 107 | assertEquals(0.8673, result.get(1).get(Rain.T), DELTA); 108 | assertEquals(0.1327, result.get(1).get(Rain.F), DELTA); 109 | assertEquals(0.8204, result.get(2).get(Rain.T), DELTA); 110 | assertEquals(0.1796, result.get(2).get(Rain.F), DELTA); 111 | assertEquals(0.3075, result.get(3).get(Rain.T), DELTA); 112 | assertEquals(0.6925, result.get(3).get(Rain.F), DELTA); 113 | assertEquals(0.8204, result.get(4).get(Rain.T), DELTA); 114 | assertEquals(0.1796, result.get(4).get(Rain.F), DELTA); 115 | assertEquals(0.8673, result.get(5).get(Rain.T), DELTA); 116 | assertEquals(0.1327, result.get(5).get(Rain.F), DELTA); 117 | } 118 | 119 | 120 | 121 | } -------------------------------------------------------------------------------- /src/test/java/com/bmw/hmm/ViterbiAlgorithmTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015-2016, BMW Car IT GmbH and BMW AG 3 | * Author: Stefan Holder (stefan.holder@bmw.de) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.bmw.hmm; 19 | 20 | import org.junit.Test; 21 | 22 | import java.util.*; 23 | 24 | import static java.lang.Math.log; 25 | import static org.junit.Assert.*; 26 | 27 | public class ViterbiAlgorithmTest { 28 | 29 | private static class Rain { 30 | final static Rain T = new Rain(); 31 | final static Rain F = new Rain(); 32 | 33 | @Override 34 | public String toString() { 35 | if (this == T) { 36 | return "Rain"; 37 | } else if (this == F) { 38 | return "Sun"; 39 | } 40 | throw new IllegalStateException(); 41 | } 42 | } 43 | 44 | private static class Umbrella { 45 | final static Umbrella T = new Umbrella(); 46 | final static Umbrella F = new Umbrella(); 47 | 48 | @Override 49 | public String toString() { 50 | if (this == T) { 51 | return "Umbrella"; 52 | } else if (this == F) { 53 | return "No umbrella"; 54 | } 55 | throw new IllegalStateException(); 56 | } 57 | } 58 | 59 | private static class Descriptor { 60 | final static Descriptor R2R = new Descriptor(); 61 | final static Descriptor R2S = new Descriptor(); 62 | final static Descriptor S2R = new Descriptor(); 63 | final static Descriptor S2S = new Descriptor(); 64 | 65 | @Override 66 | public String toString() { 67 | if (this == R2R) { 68 | return "R2R"; 69 | } else if (this == R2S) { 70 | return "R2S"; 71 | } else if (this == S2R) { 72 | return "S2R"; 73 | } else if (this == S2S) { 74 | return "S2S"; 75 | } 76 | throw new IllegalStateException(); 77 | } 78 | } 79 | 80 | private static double DELTA = 1e-8; 81 | 82 | private List states(List> sequenceStates) { 83 | final List result = new ArrayList<>(); 84 | for (SequenceState ss : sequenceStates) { 85 | result.add(ss.state); 86 | } 87 | return result; 88 | } 89 | 90 | /** 91 | * Tests the Viterbi algorithms with the umbrella example taken from Russell, Norvig: Aritifical 92 | * Intelligence - A Modern Approach, 3rd edition, chapter 15.2.3. Note that the probabilities in 93 | * Figure 15.5 are different, since the book uses initial probabilities and the probabilities 94 | * for message m1:1 are normalized (not wrong but unnecessary). 95 | */ 96 | @Test 97 | public void testComputeMostLikelySequence() { 98 | final List candidates = new ArrayList<>(); 99 | candidates.add(Rain.T); 100 | candidates.add(Rain.F); 101 | 102 | final Map emissionLogProbabilitiesForUmbrella = new LinkedHashMap<>(); 103 | emissionLogProbabilitiesForUmbrella.put(Rain.T, log(0.9)); 104 | emissionLogProbabilitiesForUmbrella.put(Rain.F, log(0.2)); 105 | 106 | final Map emissionLogProbabilitiesForNoUmbrella = new LinkedHashMap<>(); 107 | emissionLogProbabilitiesForNoUmbrella.put(Rain.T, log(0.1)); 108 | emissionLogProbabilitiesForNoUmbrella.put(Rain.F, log(0.8)); 109 | 110 | final Map, Double> transitionLogProbabilities = new LinkedHashMap<>(); 111 | transitionLogProbabilities.put(new Transition<>(Rain.T, Rain.T), log(0.7)); 112 | transitionLogProbabilities.put(new Transition<>(Rain.T, Rain.F), log(0.3)); 113 | transitionLogProbabilities.put(new Transition<>(Rain.F, Rain.T), log(0.3)); 114 | transitionLogProbabilities.put(new Transition<>(Rain.F, Rain.F), log(0.7)); 115 | 116 | final Map, Descriptor> transitionDescriptors = new LinkedHashMap<>(); 117 | transitionDescriptors.put(new Transition<>(Rain.T, Rain.T), Descriptor.R2R); 118 | transitionDescriptors.put(new Transition<>(Rain.T, Rain.F), Descriptor.R2S); 119 | transitionDescriptors.put(new Transition<>(Rain.F, Rain.T), Descriptor.S2R); 120 | transitionDescriptors.put(new Transition<>(Rain.F, Rain.F), Descriptor.S2S); 121 | 122 | final ViterbiAlgorithm viterbi = 123 | new ViterbiAlgorithm().setKeepMessageHistory(true). 124 | setComputeSmoothingProbabilities(true); 125 | viterbi.startWithInitialObservation(Umbrella.T, candidates, 126 | emissionLogProbabilitiesForUmbrella); 127 | viterbi.nextStep(Umbrella.T, candidates, emissionLogProbabilitiesForUmbrella, 128 | transitionLogProbabilities, transitionDescriptors); 129 | viterbi.nextStep(Umbrella.F, candidates, emissionLogProbabilitiesForNoUmbrella, 130 | transitionLogProbabilities, transitionDescriptors); 131 | viterbi.nextStep(Umbrella.T, candidates, emissionLogProbabilitiesForUmbrella, 132 | transitionLogProbabilities, transitionDescriptors); 133 | 134 | final List> result = 135 | viterbi.computeMostLikelySequence(); 136 | 137 | // Check most likely sequence 138 | assertEquals(4, result.size()); 139 | assertEquals(Rain.T, result.get(0).state); 140 | assertEquals(Rain.T, result.get(1).state); 141 | assertEquals(Rain.F, result.get(2).state); 142 | assertEquals(Rain.T, result.get(3).state); 143 | 144 | assertEquals(Umbrella.T, result.get(0).observation); 145 | assertEquals(Umbrella.T, result.get(1).observation); 146 | assertEquals(Umbrella.F, result.get(2).observation); 147 | assertEquals(Umbrella.T, result.get(3).observation); 148 | 149 | assertEquals(null, result.get(0).transitionDescriptor); 150 | assertEquals(Descriptor.R2R, result.get(1).transitionDescriptor); 151 | assertEquals(Descriptor.R2S, result.get(2).transitionDescriptor); 152 | assertEquals(Descriptor.S2R, result.get(3).transitionDescriptor); 153 | 154 | // Check for HMM breaks 155 | assertFalse(viterbi.isBroken()); 156 | 157 | // Check message history 158 | List> expectedMessageHistory = new ArrayList<>(); 159 | Map message = new LinkedHashMap<>(); 160 | message.put(Rain.T, 0.9); 161 | message.put(Rain.F, 0.2); 162 | expectedMessageHistory.add(message); 163 | 164 | message = new LinkedHashMap<>(); 165 | message.put(Rain.T, 0.567); 166 | message.put(Rain.F, 0.054); 167 | expectedMessageHistory.add(message); 168 | 169 | message = new LinkedHashMap<>(); 170 | message.put(Rain.T, 0.03969); 171 | message.put(Rain.F, 0.13608); 172 | expectedMessageHistory.add(message); 173 | 174 | message = new LinkedHashMap<>(); 175 | message.put(Rain.T, 0.0367416); 176 | message.put(Rain.F, 0.0190512); 177 | expectedMessageHistory.add(message); 178 | 179 | List> actualMessageHistory = viterbi.messageHistory(); 180 | checkMessageHistory(expectedMessageHistory, actualMessageHistory); 181 | } 182 | 183 | @Test 184 | public void testSetParams() { 185 | final ViterbiAlgorithm viterbi = new ViterbiAlgorithm<>(); 186 | 187 | assertFalse(viterbi.isKeepMessageHistory()); 188 | viterbi.setKeepMessageHistory(true); 189 | assertTrue(viterbi.isKeepMessageHistory()); 190 | viterbi.setKeepMessageHistory(false); 191 | assertFalse(viterbi.isKeepMessageHistory()); 192 | 193 | assertFalse(viterbi.isComputeSmoothingProbabilities()); 194 | viterbi.setComputeSmoothingProbabilities(true); 195 | assertTrue(viterbi.isComputeSmoothingProbabilities()); 196 | viterbi.setComputeSmoothingProbabilities(false); 197 | assertFalse(viterbi.isComputeSmoothingProbabilities()); 198 | } 199 | 200 | private void checkMessageHistory(List> expectedMessageHistory, 201 | List> actualMessageHistory) { 202 | assertEquals(expectedMessageHistory.size(), actualMessageHistory.size()); 203 | for (int i = 0 ; i < expectedMessageHistory.size() ; i++) { 204 | checkMessage(expectedMessageHistory.get(i), actualMessageHistory.get(i)); 205 | } 206 | } 207 | 208 | private void checkMessage(Map expectedMessage, Map actualMessage) { 209 | assertEquals(expectedMessage.size(), actualMessage.size()); 210 | for (Map.Entry entry : expectedMessage.entrySet()) { 211 | assertEquals(entry.getValue(), Math.exp(actualMessage.get(entry.getKey())), DELTA); 212 | } 213 | } 214 | 215 | @Test 216 | public void testEmptySequence() { 217 | final ViterbiAlgorithm viterbi = new ViterbiAlgorithm<>(); 218 | final List> result = 219 | viterbi.computeMostLikelySequence(); 220 | 221 | assertEquals(Arrays.asList(), result); 222 | assertFalse(viterbi.isBroken()); 223 | } 224 | 225 | @Test 226 | public void testBreakAtInitialMessage() { 227 | final ViterbiAlgorithm viterbi = new ViterbiAlgorithm<>(); 228 | final List candidates = new ArrayList<>(); 229 | candidates.add(Rain.T); 230 | candidates.add(Rain.F); 231 | 232 | final Map emissionLogProbabilities = new LinkedHashMap<>(); 233 | emissionLogProbabilities.put(Rain.T, log(0.0)); 234 | emissionLogProbabilities.put(Rain.F, log(0.0)); 235 | viterbi.startWithInitialObservation(Umbrella.T, candidates, emissionLogProbabilities); 236 | assertTrue(viterbi.isBroken()); 237 | assertEquals(Arrays.asList(), viterbi.computeMostLikelySequence()); 238 | } 239 | 240 | @Test 241 | public void testEmptyInitialMessage() { 242 | final ViterbiAlgorithm viterbi = new ViterbiAlgorithm<>(); 243 | viterbi.startWithInitialObservation(Umbrella.T, new ArrayList(), 244 | new LinkedHashMap()); 245 | assertTrue(viterbi.isBroken()); 246 | assertEquals(Arrays.asList(), viterbi.computeMostLikelySequence()); 247 | } 248 | 249 | @Test 250 | public void testBreakAtFirstTransition() { 251 | final ViterbiAlgorithm viterbi = new ViterbiAlgorithm<>(); 252 | final List candidates = new ArrayList<>(); 253 | candidates.add(Rain.T); 254 | candidates.add(Rain.F); 255 | 256 | final Map emissionLogProbabilities = new LinkedHashMap<>(); 257 | emissionLogProbabilities.put(Rain.T, log(0.9)); 258 | emissionLogProbabilities.put(Rain.F, log(0.2)); 259 | viterbi.startWithInitialObservation(Umbrella.T, candidates, emissionLogProbabilities); 260 | assertFalse(viterbi.isBroken()); 261 | 262 | final Map, Double> transitionLogProbabilities = new LinkedHashMap<>(); 263 | transitionLogProbabilities.put(new Transition(Rain.T, Rain.T), log(0.0)); 264 | transitionLogProbabilities.put(new Transition(Rain.T, Rain.F), log(0.0)); 265 | transitionLogProbabilities.put(new Transition(Rain.F, Rain.T), log(0.0)); 266 | transitionLogProbabilities.put(new Transition(Rain.F, Rain.F), log(0.0)); 267 | viterbi.nextStep(Umbrella.T, candidates, emissionLogProbabilities, 268 | transitionLogProbabilities); 269 | 270 | assertTrue(viterbi.isBroken()); 271 | assertEquals(Arrays.asList(Rain.T), states(viterbi.computeMostLikelySequence())); 272 | } 273 | 274 | @Test 275 | public void testBreakAtFirstTransitionWithNoCandidates() { 276 | final ViterbiAlgorithm viterbi = new ViterbiAlgorithm<>(); 277 | final List candidates = new ArrayList<>(); 278 | candidates.add(Rain.T); 279 | candidates.add(Rain.F); 280 | 281 | final Map emissionLogProbabilities = new LinkedHashMap<>(); 282 | emissionLogProbabilities.put(Rain.T, log(0.9)); 283 | emissionLogProbabilities.put(Rain.F, log(0.2)); 284 | viterbi.startWithInitialObservation(Umbrella.T, candidates, emissionLogProbabilities); 285 | assertFalse(viterbi.isBroken()); 286 | 287 | viterbi.nextStep(Umbrella.T, new ArrayList(), new LinkedHashMap(), 288 | new LinkedHashMap, Double>()); 289 | assertTrue(viterbi.isBroken()); 290 | 291 | assertEquals(Arrays.asList(Rain.T), states(viterbi.computeMostLikelySequence())); 292 | } 293 | 294 | @Test 295 | public void testBreakAtSecondTransition() { 296 | final ViterbiAlgorithm viterbi = new ViterbiAlgorithm<>(); 297 | final List candidates = new ArrayList<>(); 298 | candidates.add(Rain.T); 299 | candidates.add(Rain.F); 300 | 301 | final Map emissionLogProbabilities = new LinkedHashMap<>(); 302 | emissionLogProbabilities.put(Rain.T, log(0.9)); 303 | emissionLogProbabilities.put(Rain.F, log(0.2)); 304 | viterbi.startWithInitialObservation(Umbrella.T, candidates, emissionLogProbabilities); 305 | assertFalse(viterbi.isBroken()); 306 | 307 | Map, Double> transitionLogProbabilities = new LinkedHashMap<>(); 308 | transitionLogProbabilities.put(new Transition<>(Rain.T, Rain.T), log(0.5)); 309 | transitionLogProbabilities.put(new Transition<>(Rain.T, Rain.F), log(0.5)); 310 | transitionLogProbabilities.put(new Transition<>(Rain.F, Rain.T), log(0.5)); 311 | transitionLogProbabilities.put(new Transition<>(Rain.F, Rain.F), log(0.5)); 312 | viterbi.nextStep(Umbrella.T, candidates, emissionLogProbabilities, 313 | transitionLogProbabilities); 314 | assertFalse(viterbi.isBroken()); 315 | 316 | transitionLogProbabilities = new LinkedHashMap<>(); 317 | transitionLogProbabilities.put(new Transition<>(Rain.T, Rain.T), log(0.0)); 318 | transitionLogProbabilities.put(new Transition<>(Rain.T, Rain.F), log(0.0)); 319 | transitionLogProbabilities.put(new Transition<>(Rain.F, Rain.T), log(0.0)); 320 | transitionLogProbabilities.put(new Transition<>(Rain.F, Rain.F), log(0.0)); 321 | viterbi.nextStep(Umbrella.T, candidates, emissionLogProbabilities, 322 | transitionLogProbabilities); 323 | 324 | assertTrue(viterbi.isBroken()); 325 | assertEquals(Arrays.asList(Rain.T, Rain.T), states(viterbi.computeMostLikelySequence())); 326 | } 327 | 328 | @Test 329 | /** 330 | * Checks if the first candidate is returned if multiple candidates are equally likely. 331 | */ 332 | public void testDeterministicCandidateOrder() { 333 | final List candidates = new ArrayList<>(); 334 | candidates.add(Rain.T); 335 | candidates.add(Rain.F); 336 | 337 | // Reverse usual order of emission and transition probabilities keys since their order 338 | // should not matter. 339 | final Map emissionLogProbabilitiesForUmbrella = new LinkedHashMap<>(); 340 | emissionLogProbabilitiesForUmbrella.put(Rain.F, log(0.5)); 341 | emissionLogProbabilitiesForUmbrella.put(Rain.T, log(0.5)); 342 | 343 | final Map emissionLogProbabilitiesForNoUmbrella = new LinkedHashMap<>(); 344 | emissionLogProbabilitiesForNoUmbrella.put(Rain.F, log(0.5)); 345 | emissionLogProbabilitiesForNoUmbrella.put(Rain.T, log(0.5)); 346 | 347 | final Map, Double> transitionLogProbabilities = new LinkedHashMap<>(); 348 | transitionLogProbabilities.put(new Transition<>(Rain.F, Rain.T), log(0.5)); 349 | transitionLogProbabilities.put(new Transition<>(Rain.F, Rain.F), log(0.5)); 350 | transitionLogProbabilities.put(new Transition<>(Rain.T, Rain.T), log(0.5)); 351 | transitionLogProbabilities.put(new Transition<>(Rain.T, Rain.F), log(0.5)); 352 | 353 | final ViterbiAlgorithm viterbi = new ViterbiAlgorithm<>(); 354 | viterbi.startWithInitialObservation(Umbrella.T, candidates, 355 | emissionLogProbabilitiesForUmbrella); 356 | viterbi.nextStep(Umbrella.T, candidates, emissionLogProbabilitiesForUmbrella, 357 | transitionLogProbabilities); 358 | viterbi.nextStep(Umbrella.F, candidates, emissionLogProbabilitiesForNoUmbrella, 359 | transitionLogProbabilities); 360 | viterbi.nextStep(Umbrella.T, candidates, emissionLogProbabilitiesForUmbrella, 361 | transitionLogProbabilities); 362 | 363 | final List> result = 364 | viterbi.computeMostLikelySequence(); 365 | 366 | // Check most likely sequence 367 | assertEquals(4, result.size()); 368 | assertEquals(Rain.T, result.get(0).state); 369 | assertEquals(Rain.T, result.get(1).state); 370 | assertEquals(Rain.T, result.get(2).state); 371 | assertEquals(Rain.T, result.get(3).state); 372 | } 373 | 374 | } --------------------------------------------------------------------------------