prerequisite
Logarithms and underflow
Why multiplying hundreds of probabilities gives zero on a computer, and how adding logs, comparing in log space, and the log-sum-exp trick fix it
Before this
This page assumes you are comfortable with:
Why you need this
Scoring a 300-column level under a hidden Markov model means multiplying about 600 probabilities, each well under 1. The true product is smaller than any number a double can hold, so the computer reports 0 for every level and cannot rank them. Logarithms turn that product into a sum that lands near -700, an ordinary number. The forward algorithm, Viterbi, and Baum-Welch all run in log space or with rescaling for exactly this reason.
The idea
What a logarithm is
is the power you raise to in order to get : and . This cluster uses the natural log (base ) unless it says otherwise. The log is defined only for .
The rules you need:
| Rule | Statement | With numbers |
|---|---|---|
| product | ||
| quotient | ||
| power | ||
| anchors | , |
Two facts about signs. The log of a number in is negative, and the closer to 0 the number, the more negative: , , . And the log is increasing: if then . So comparing two logs gives the same answer as comparing the two originals, which is the whole reason log space is safe to work in.
Underflow
A double-precision number (JavaScript's only number type) has a smallest normal value of about . Below that the format enters denormals, which trade precision for range and bottom out near . Anything smaller becomes exactly 0. That is underflow.
Now multiply 300 probabilities each around 0.1. The product is , already in the region where a few more factors push it to zero. A realistic level has a transition and an emission factor per column, so 600 factors, and the product is well below : the computer says 0. Every candidate level scores 0. Ties everywhere, and no way to say which level was more likely.
In log space the same product is . Nothing special happens near that number; doubles handle magnitudes up to about with room to spare.
Comparing two paths in log space
Suppose two candidate paths through a chain have per-step probabilities
- path P: 0.5, 0.3, 0.2, 0.4
- path Q: 0.6, 0.1, 0.5, 0.5
Their log-probabilities are sums:
Q is larger (less negative), so Q is more probable. Check with the raw products: P is 0.012 and Q is 0.015. The difference of logs is the log of the ratio, , and : Q is 1.25 times as likely as P. With four steps you could have multiplied; with four hundred you could not.
Costs instead of probabilities
Negate the log and you get a non-negative cost: , . Rare steps cost more. A product of probabilities becomes a sum of costs, and "most probable path" becomes "cheapest path". That is why the Viterbi algorithm looks exactly like a shortest-path search on a grid.
Adding in log space: log-sum-exp
Multiplying is easy in log space (add the logs). Adding is not: there is no simple rule for . You need it whenever you sum probabilities, and the forward algorithm does that at every step.
The naive route, exponentiate back, add, take the log, underflows: is 0 on a machine. The fix is to pull out the largest term first. With and ,
Every exponent is at most 0, and the largest is exactly 0, so the sum inside contains a 1 and can never underflow to 0 or overflow.
Three numbers: . Then , the shifted values are , their exponentials are , the sum is , and . Answer: . The direct computation returns .
Sanity check on numbers small enough to do both ways: , , so the answer should be . Logs are and ; ; shifted and ; exponentials and ; sum ; ; result . Correct.
Worked example
Six columns, each with probability under some chain. Watch the running product shrink while the running log-sum just drifts down.
| running product | running sum of logs | |||
|---|---|---|---|---|
| 1 | 0.5 | -0.693 | 0.5 | -0.693 |
| 2 | 0.2 | -1.609 | 0.1 | -2.303 |
| 3 | 0.1 | -2.303 | 0.01 | -4.605 |
| 4 | 0.3 | -1.204 | 0.003 | -5.809 |
| 5 | 0.1 | -2.303 | 0.0003 | -8.112 |
| 6 | 0.2 | -1.609 | 0.00006 | -9.721 |
Check the last row: . Six columns cost about 1.6 in log units each. At that rate 300 columns land near , a product of about , still representable but uncomfortably close; 500 columns land near , a product of , which a double rounds to 0. The log column has no such cliff.
function logSumExp(logs) {
const m = Math.max(...logs);
if (m === -Infinity) return -Infinity; // every input was log(0)
let s = 0;
for (const l of logs) s += Math.exp(l - m);
return m + Math.log(s);
}
console.log(logSumExp([-1000, -1001, -1003])); // -999.6509877832318
console.log(Math.log(Math.exp(-1000) + Math.exp(-1001))); // -Infinity: the naive way underflows
console.log(Math.pow(0.1, 300), Math.pow(0.1, 330)); // 1.0000000000000166e-300 0
console.log(Number.MIN_VALUE, 2 ** -1022); // 5e-324 2.2250738585072014e-308
The third line shows both halves of the story: 300 factors of 0.1 survive with a little rounding noise in the last digits, and 330 do not survive at all.
In a map generator
- Layout. Scoring a column sequence with the forward algorithm sums probabilities at every column, so it runs in log space with log-sum-exp or keeps a per-column scale factor. Recovering hidden sections with Viterbi needs only max and add, so it is pure cost arithmetic. Fitting tables with Baum-Welch accumulates expected counts in log space.
- Comparing levels. "How typical is this level of the training set" is a log-likelihood. Divide by the column count to compare levels of different lengths.
- Tuning. is a "surprise" score. Printing the surprise of each column of a generated level shows you exactly where the chain did something rare.
- Regime over time. Regime sequences are short and rarely scored, so plain probabilities usually suffice there.
Common mistakes
- Taking . It is , and is
NaN. Symptom: one impossible transition turns a whole table toNaN. Guard zeros, or floor probabilities at a tiny epsilon before taking logs. - Multiplying first, logging after. Once the product is 0, its log is for every candidate. Symptom: Viterbi always returns the first path it tried; every level ties.
- Mixing bases. One table in , another in natural log. Symptom: two models disagree by a constant factor of about 1.44 and nobody knows why.
- Summing with exp-add-log inside the forward algorithm. Symptom: the forward variable becomes 0 somewhere around column 150 and stays there.
- Comparing paths of different lengths by raw total. Every extra step adds a negative number, so longer always loses. Symptom: the generator prefers the shortest possible level.
- Feeding log-probabilities to a sampler as weights. They are negative. Symptom: the sampler returns index 0 forever or throws.
Cost
Taking the log of each entry of (, with states) and (, with observation symbols) once costs time and the same memory, done at load. Log-sum-exp over terms is with calls to exp. The forward algorithm performs one such sum per state per column, so exponentials for columns, a constant factor over plain multiplication. For under 30 you will not notice. The alternative, rescaling each column to sum to 1 and keeping the log of the scale factors, avoids the exponentials entirely and is the usual choice for long sequences.
Going further
- Forward algorithm: log-sum-exp or rescaling in action.
- Viterbi algorithm: costs, max, and add, no log-sum-exp needed.
- Baum-Welch: expected counts accumulated in log space.
- Entropy: the expected value of .
- The IEEE 754 double-precision format, for the exact limits quoted above.
Leads to
- prerequisiteEntropyShannon entropy as one number for how unsure a distribution is, computed for small examples, and why Wave Function Collapse uses it to choose the next cell
- techniqueForward algorithmScore an observation sequence under a hidden Markov model, and read off the current hidden state along the way, in one left-to-right pass.
- techniqueViterbi algorithmRecover the single most likely hidden path behind an observation sequence by filling the forward table with max instead of sum and walking backpointers.