Conditioning and Numerical Stability
Learning Objectives
- Define the relative condition number of a problem, compute it with its hypotheses, and say where it blows up.
- Distinguish conditioning, a property of the problem, from stability, a property of the algorithm.
- Compute the forward error and the backward error of a computed answer.
- Use the rule that forward error is at most about condition number times backward error, and state what it does and does not promise.
- Diagnose an unstable formula and replace it with a stable one, or recognize that no replacement can help.
Context & Motivation
Example 2 of sources-of-error-truncation-rounding-propagation lost every digit of , and a rewrite recovered them all. That rewrite will not always exist. Two failures look identical on screen and call for opposite responses. Sometimes the answer is so sensitive to its input that rounding the input alone destroys it, and only better data or a different question can help. Sometimes the problem is perfectly healthy and the formula is what wastes the digits.
Telling these apart is this lesson's subject, and it decides what to do next: rewrite the code, raise the precision, or question the input. The same vocabulary carries through the discipline. bisection-and-convergence-rate reads a small residual as a backward error, and the-matrix-condition-number extends the condition number to .
Core Theory
Conditioning: how sensitive is the problem
Treat a problem as a map from data to answer. Conditioning asks how much the exact answer moves when the data moves, with no algorithm involved. If , and is differentiable at , the relative condition number is
the limit, as the perturbation shrinks, of relative change in over relative change in . So a relative perturbation in the input moves the answer by about . Stored inputs carry relative error up to , so even a perfect algorithm should expect a relative error near in float64, a loss of about digits. This is a first-order statement, and a problem with near is well-conditioned while one with large is ill-conditioned.
| Problem | Large when | Formula at sample point | Measured | |
|---|---|---|---|---|
| never | at | |||
| large | at | |||
| near | at | |||
| near , ; most large | at | |||
| near | at |
The measured column perturbs by a relative in high-precision arithmetic and divides the two relative changes. For , size alone also hurts: at , unless is near , where vanishes. The last row is the subtraction with held exact, so only carries error. The factor from the previous lesson assumes both operands do, and gives here.
The hypotheses matter. At a zero of the relative number does not exist, and the absolute version takes over. math.sin(math.pi) returns , which is not a defect of sin: the stored math.pi differs from by that same amount, and has slope there.
Conditioning also depends on what counts as data. As a function of , is ill-conditioned at , because rounding has already damaged . As a function of , has near . That is why math.log1p(delta) exists: when is known exactly, pass .
Stability: how good is the algorithm
An algorithm computes . Its forward error is the distance to the true answer, , usually taken relative to . Its backward error asks a different question: which problem did the algorithm solve exactly? It is the smallest relative perturbation with .
An algorithm is backward stable if its backward error is a modest multiple of for every input. Loosely, an algorithm is stable if its forward error is not much larger than , the amount conditioning already forces. An unstable algorithm loses more than the problem requires.
Connecting the two
If , the definition of gives, to first order,
This is an upper bound and it reads as a diagnostic:
- Backward error near and moderate: the answer is accurate.
- Backward error near and huge: the answer may be poor, and the algorithm is not to blame.
- Forward error far above : the algorithm is to blame.
How formulas go unstable
An algorithm is a chain of steps, and each step is a small problem with its own condition number. A common failure route is a well-conditioned whole that passes through an ill-conditioned step: errors from earlier steps are then magnified, and nothing later can recover them. The lost digits of fit this pattern. The whole map has , which tends to as , so the problem is well-conditioned. The bad step is the last one: at has , which is at . The rewrite has no such step. Both unstable examples below fail this way.
Worked Examples
Example 1: a healthy problem, a bad formula
Solve with and . The roots are and . Differentiating gives the condition numbers of a root with respect to each coefficient: and . For both equal to many digits. The problem is well-conditioned.
The textbook formula subtracts from . That one step has condition number about . The stable alternative computes with the signs that add, then uses :
textformula r2 forward error backward error in c (-b - sqrt(D))/2 7.450580596923828e-09 2.5e-01 2.5e-01 c / r1 1e-08 7.9e-17 7.9e-17
The backward error comes from , the coefficient for which is an exact root. The bad formula solved an equation with wrong by 25 percent, and with the forward error matches, as the bound predicts. numpy.roots([1, -1e8, 1]) is also fine here, with relative error on this root.
The rewrite assumes and helps with cancellation between and the square root. If the roots nearly coincide, the problem itself is ill-conditioned, and no rewrite restores the lost digits.
Example 2: the same numbers, computed in two directions
Let . Since , we get for , with . From on also . The forward recurrence in float64:
textn forward I_n exact I_n error ratio 0 0.09531017980432493 0.09531017980432487 7.48e-17 1 0.04689820195675065 0.0468982019567514 7.48e-16 10.00 5 0.015352900839845474 0.015352900847328939 7.48e-12 10.00 10 0.008328713865253717 0.008327965518886312 7.48e-07 10.00 14 0.013578878977397152 0.006095415303348168 7.48e-03 10.00 16 0.7537212310730486 0.005374863668150086 7.48e-01 10.00 18 74.83944336874276 0.004806628252917123 7.48e+01 10.00
Every step multiplies the error by , as the recurrence says, so an ordinary rounding error of in reaches by . At the value is already above the upper bound .
Run it backward, , and each step divides the error by . Starting from the crude guess (true value ), the error is at and at , where the result differs from scipy.integrate.quad only in the last digit. The same numbers, reached two ways: one unstable, one stable. So the trouble was never the numbers. The forward route passes through the subproblem , which amplifies absolute error by .
Example 3: a good algorithm, a terrible problem
Take the degree-20 Wilkinson polynomial , with coefficients . Perturbing each by a relative moves the root by about , so its condition number with respect to the coefficients is
numpy.roots, which computes eigenvalues of the companion matrix, gives on the expanded coefficients:
textk computed root relative forward error kappa_k 1 0.9999999999999833 1.7e-14 4.2e+02 5 5.000000877361879 1.8e-07 8.2e+08 10 9.990574635182616 9.4e-04 5.6e+12 14 13.935190089077999 4.6e-03 5.4e+13 20 19.999888806572073 5.6e-06 1.4e+11
Now expand in exact arithmetic. Its coefficients differ from the true ones by a relative at most. So the computed roots are the exact roots of a polynomial that is almost the right one: backward error is tiny, while forward error reaches . For the bound gives , which the actual error respects by a wide margin.
The data itself is already rounded: five of these integer coefficients do not fit exactly in a float64. Exact arithmetic confirms the sensitivity. Changing the coefficient of from to , a relative change of , sends roots to . No algorithm that accepts rounded coefficients can undo that. The cure is a different representation: a program that holds the factors has no problem to solve.
Common Misconceptions & Pitfalls
- "Ill-conditioned means the algorithm is bad." Conditioning involves no algorithm. Example 3 has a tiny backward error and a large forward error.
- "A small residual means an accurate answer." It means a small backward error. Accuracy needs too.
- "An accurate answer proves backward stability." Take at . The computed
1.0has forward error , yet it is the exact answer only for , a relative backward error of . - "More precision fixes instability." In Example 2 each extra decimal digit buys one more step. The cure is the backward direction.
- "A function has one condition number." depends on the point and on what counts as data: has at and at .
- "The bound is an estimate." Example 3 shows against an actual . It limits the worst perturbation, and the actual one need not be the worst.
Summary
The condition number measures how much the problem amplifies relative perturbations, before any algorithm runs. It needs , and differentiability, and it depends on what is taken as data. Forward error compares the answer with the truth, and backward error asks which nearby problem was solved exactly. To first order, forward error is at most times backward error. A backward stable algorithm on a well-conditioned problem gives an accurate answer. On an ill-conditioned problem it gives a small backward error and possibly a poor answer, and a better algorithm cannot change that. When forward error far exceeds , the formula is at fault, often through one ill-conditioned step, and rewriting it or running it in another direction can recover the digits.
Documentation Links
- Driscoll and Braun: Fundamentals of Numerical Computation, 1.2 Problems and conditioning
- Driscoll and Braun: Fundamentals of Numerical Computation, 1.4 Stability
- MIT OpenCourseWare 18.335J (2019): Week 2
- Goldberg (1991): What Every Computer Scientist Should Know About Floating-Point Arithmetic
- NumPy documentation: numpy.roots
- SciPy documentation: scipy.integrate.quad
- Python documentation: math