Workspace IndexMath › Day 24

Numerical Linear Algebra (Condition Number) TODO

Math · Day 24 / 52 · September — Linear Algebra (Day 18-26)

Concept

The condition number expresses how much a relative error in the input gets amplified into a relative error in the output; for a linear system Ax = b, it's defined as κ(A) = ||A|| · ||A⁻¹||. Under the 2-norm, this equals the ratio of the largest to the smallest singular value, σ_max/σ_min, so the closer A is to being singular, the larger the condition number. The relative error of the solution is bounded roughly by the condition number times the relative error of the input, so a condition number on the order of 10^k means you can expect to lose about k significant digits. The key distinction is that the condition number is a property of the problem itself, separate from numerical stability, which is a property of the algorithm. In other words, even a backward-stable algorithm can't produce an accurate answer if the problem itself is ill-conditioned — in that case you need a model-level response like regularization or reformulation.

If regression or optimization results wobble wildly with tiny changes in the data, it's more likely that the problem itself is ill-conditioned than a code bug — and the appropriate response is completely different in each case.

Code & Formula

# 수치선형대수(조건수) — κ(A) = σ_max/σ_min 이 클수록 입력의 작은 오차가
# 해의 큰 오차로 증폭됨을 잘 조건화된 행렬과 거의 특이한 행렬을 비교해 확인한다.

import numpy as np

A_good = np.array([[2.0, 0.0], [0.0, 3.0]])          # 축이 직교, 스케일도 비슷 → 잘 조건화됨
A_bad = np.array([[1.0, 1.0], [1.0, 1.0001]])         # 두 행이 거의 평행 → 특이행렬에 근접

b = np.array([1.0, 1.0])
b_perturbed = b + np.array([1e-4, -1e-4])  # b에 아주 작은 오차 주입

for name, A in [("잘 조건화됨", A_good), ("거의 특이 (ill-conditioned)", A_bad)]:
    cond = np.linalg.cond(A)
    x = np.linalg.solve(A, b)
    x_perturbed = np.linalg.solve(A, b_perturbed)

    rel_input_err = np.linalg.norm(b_perturbed - b) / np.linalg.norm(b)
    rel_output_err = np.linalg.norm(x_perturbed - x) / np.linalg.norm(x)

    print(f"[{name}] κ(A) = {cond:.2f}")
    print(f"  입력 상대오차 = {rel_input_err:.2e}")
    print(f"  출력(해) 상대오차 = {rel_output_err:.2e}  (κ(A) * 입력오차 ≈ {cond * rel_input_err:.2e})")
    print(f"  증폭 배율 = {rel_output_err / rel_input_err:.2f}\n")

print("→ 조건수가 큰 A_bad에서 동일한 입력 오차가 훨씬 크게 증폭됨을 확인 — 문제 자체의 성질이지 알고리즘 탓이 아니다.")

Exercise

Solve Ax = b using increasingly large Hilbert matrices, print out both the condition number and the actual relative error together, and check whether the two grow in step with each other.

Practical Connection

In AMM curves or LMSR price calculations, when the liquidity parameter is small or probabilities approach 0/1, exponential and logarithmic terms can blow up, producing the same kind of amplification — so fixed-point implementations need to bound the input range and rearrange formulas into numerically stable forms.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/math-50-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

수치선형대수(조건수) TODO

Math · Day 24 / 52 · 9월 — 선형대수 (Day 18–26)

개념

조건수는 입력의 상대 오차가 출력의 상대 오차로 얼마나 증폭되는지를 나타내는 양이며, 선형계 Ax = b에서는 κ(A) = ||A|| · ||A⁻¹||로 정의된다. 2-노름에서는 이 값이 최대 특이값과 최소 특이값의 비 σ_max/σ_min와 같고, 따라서 A가 특이행렬에 가까울수록 조건수가 커진다. 해의 상대 오차는 대략 조건수 곱하기 입력의 상대 오차로 상계되므로, 조건수가 10^k 규모면 유효 자릿수를 약 k자리 잃는다고 볼 수 있다. 중요한 구분은 조건수가 문제 자체의 성질이라는 점이며, 알고리즘의 성질인 수치 안정성과는 별개다. 즉 후방 안정적인 알고리즘을 써도 문제가 ill-conditioned하면 정확한 답을 얻을 수 없고, 이때는 정규화나 재정식화 같은 모델 수준의 대응이 필요하다.

회귀나 최적화에서 결과가 데이터의 미세한 변화에 요동친다면 코드 버그가 아니라 설계된 문제 자체가 ill-conditioned일 가능성이 높고, 대응 방법이 완전히 다르다.

코드 · 수식

# 수치선형대수(조건수) — κ(A) = σ_max/σ_min 이 클수록 입력의 작은 오차가
# 해의 큰 오차로 증폭됨을 잘 조건화된 행렬과 거의 특이한 행렬을 비교해 확인한다.

import numpy as np

A_good = np.array([[2.0, 0.0], [0.0, 3.0]])          # 축이 직교, 스케일도 비슷 → 잘 조건화됨
A_bad = np.array([[1.0, 1.0], [1.0, 1.0001]])         # 두 행이 거의 평행 → 특이행렬에 근접

b = np.array([1.0, 1.0])
b_perturbed = b + np.array([1e-4, -1e-4])  # b에 아주 작은 오차 주입

for name, A in [("잘 조건화됨", A_good), ("거의 특이 (ill-conditioned)", A_bad)]:
    cond = np.linalg.cond(A)
    x = np.linalg.solve(A, b)
    x_perturbed = np.linalg.solve(A, b_perturbed)

    rel_input_err = np.linalg.norm(b_perturbed - b) / np.linalg.norm(b)
    rel_output_err = np.linalg.norm(x_perturbed - x) / np.linalg.norm(x)

    print(f"[{name}] κ(A) = {cond:.2f}")
    print(f"  입력 상대오차 = {rel_input_err:.2e}")
    print(f"  출력(해) 상대오차 = {rel_output_err:.2e}  (κ(A) * 입력오차 ≈ {cond * rel_input_err:.2e})")
    print(f"  증폭 배율 = {rel_output_err / rel_input_err:.2f}\n")

print("→ 조건수가 큰 A_bad에서 동일한 입력 오차가 훨씬 크게 증폭됨을 확인 — 문제 자체의 성질이지 알고리즘 탓이 아니다.")

연습

크기를 키운 힐베르트 행렬로 Ax = b를 풀어 조건수와 실제 상대 오차를 함께 출력하고, 둘의 증가가 대응하는지 확인해 볼 것.

실무 · Verex 연결

AMM 곡선이나 LMSR 가격 계산에서 유동성 파라미터가 작거나 확률이 0/1에 가까울 때 지수·로그 항이 극단으로 가면 같은 성격의 증폭이 생기므로, 고정소수점 구현에서는 입력 범위를 제한하고 수식을 안정적 형태로 재배열해야 한다.

공부한 날 원본 커리큘럼(docs/knowledge/math-50-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 23. PCA·SVD25. 행렬식과 랭크 →