수치계산의 기본 개념: 누적 오차와 전파 오차
부동소수점 연산의 태생적 한계
실수는 이론적으로 무한한 정밀도를 지니지만, 컴퓨터는 유한한 비트 수로 실수를 표현한다. 따라서 대부분의 프로그래밍 언어나 계산 환경에서 사용하는 부동소수점(floating point) 방식은 제한된 자릿수만을 표현할 수 있다. 이 때문에 부동소수점으로 실수를 표현하는 과정에서 반올림(rounding)이 발생하고, 그 결과 실제 연산 과정에서 필연적으로 오차가 끼어든다. 이러한 오차는 한 번 발생하면 완전히 제거하기 어려우며, 연산이 반복될수록 누적되거나 전파되어 결과에 점점 큰 영향을 미치게 된다.
부동소수점 수는 대략적으로 $m \times \beta^e$ 형태로 나타낼 수 있다. 여기서 $m$은 유효숫자를 나타내는 멘티사(mantissa)이고, $\beta$는 밑(radix), $e$는 지수(exponent)이다. 예를 들어 IEEE 754 표준에서 단정밀도(single precision)는 32비트로 실수를 표현하며, 이 중 멘티사에 23비트를 사용한다. 이 멘티사 영역이 나타낼 수 있는 유효숫자 자릿수는 유한하기 때문에, 어떤 실수라도 유효숫자 범위를 벗어나는 순간 반올림이 일어난다.
부동소수점 방식에서 모든 수는 이산적인 집합에 맵핑된다. 실수 축에서 연속적으로 분포하는 값들을 일정 간격으로 ‘얼추 근사’하여 표현한다고 볼 수 있다. 예컨대 0.1 같은 소수는 2진 부동소수점으로는 정확히 표현되지 않는다. 부동소수점 연산에서는 이미 표현된 수들끼리 연산을 하므로, 처음부터 반올림 오차를 안고 시작한다고 할 수 있다. 이를 이해하려면 부동소수점 환경에서 $\frac{1}{10}$을 여러 번 더하거나 곱해 보는 간단한 예시만으로도 체감할 수 있다.
절대 오차와 상대 오차
오차(error)를 측정하는 대표적인 방법에는 절대 오차(absolute error)와 상대 오차(relative error)가 있다. 어떤 참값을 $x$라 하고, 근사값을 $\tilde{x}$라 할 때 절대 오차는 $|x - \tilde{x}|$로 정의된다. 상대 오차는 참값에 대한 비율, 즉
로 정의된다. 상대 오차는 참값이 매우 큰 경우나 매우 작은 경우에 실제 오차가 어느 정도나 의미를 갖는지를 상대적으로 해석하는 데 유용하다. 예를 들어 참값이 $10^6$ 정도인 상황에서 오차가 1이라면 이는 절대 오차로 보면 1이지만 상대 오차로 보면 $10^{-6}$이므로 매우 작은 편이다. 반면 참값이 $10^{-6}$ 정도인 상황에서 1의 절대 오차가 발생한다면 이는 상대 오차로는 $10^6$에 해당하여 지극히 큰 오차가 된다.
누적 오차(accumulated error)
누적 오차는 여러 번 반복되는 연산 과정에서 발생한 작은 오차들이 점차 축적되어 최종 결과의 정확도를 해치는 현상을 가리킨다. 수십, 수백, 수천 번의 연산이 반복되는 계산에서 반올림 오차가 계속해서 쌓이면, 초기에는 매우 작은 미미한 오차였어도 최종적으로는 도저히 무시할 수 없을 정도의 큰 오차로 귀결될 수 있다. 예를 들어 긴 반복문 안에서 부동소수점 수를 누적 덧셈으로 계산할 때 쉽게 발생하는 문제가 바로 이런 누적 오차다.
간단한 예로 특정 값을 매우 많이 더하거나 빼는 과정을 생각해 보자. 실제로 0.1을 1000번 더해서 100이 나올 것으로 기대하지만, 부동소수점 연산에서는 100과 정확히 일치하기 어렵다. 미세한 차이가 발생할 수 있으며, 100번, 200번, 300번처럼 반복 횟수가 커질수록 작은 반올림 오차가 합쳐져 눈에 띄는 편차가 생긴다. 이 현상이 바로 누적 오차이며, 실용적인 수치계산 문제에서 매우 중요하다.
전파 오차(propagated error)
전파 오차는 연쇄적인 연산 과정에서 초기에 발생한 오차가 연산 단계마다 계속해서 커지는 현상을 의미한다. 누적 오차는 덧셈이나 뺄셈이 반복되면서 작았던 오차가 조금씩 쌓이는 양상을 주로 가리키는 반면, 전파 오차는 연산 구조상 초기 오차가 곱셈이나 나눗셈 등의 연산 단계를 거치며 기하급수적으로 커질 수도 있는 상황을 말한다.
예를 들어 함수 평가 과정을 생각해 보면, $f(x)$의 근사값을 구할 때 초기 입력값 $x$가 $\tilde{x} = x + \delta$로 아주 조금 잘못 주어졌다고 하자. 함수가 선형적이지 않은 경우, $f(\tilde{x}) - f(x)$의 차이가 $\delta$에 비례하지 않고 훨씬 더 커질 수도 있다. 미분 관점에서는
이지만, 실제로는 고차 미분항도 존재하므로 오차가 급격히 커질 가능성이 있다. 더욱이 반복적인 함수 평가나 피드백 구조가 있는 알고리즘(예: 뉴턴 방법, 고차방정식 근사법 등)에서는 이전 단계의 오차가 다음 단계로 계속해서 전달되면서 오차가 전파되고, 때때로 한 단계에서의 사소한 오차가 다음 단계에서 엄청나게 증폭되는 경우도 있다.
연산 순서와 오차의 상관관계
부동소수점 연산의 세계에서는 덧셈, 뺄셈, 곱셈, 나눗셈의 순서가 결과 값에 직결적으로 영향을 미친다. 예컨대 같은 연산이라도 순서를 달리해서 수행하면 다른 결과가 나올 수 있다. 부동소수점에서는 연산자의 결합법칙이 철저히 성립하지 않기 때문이다. 예를 들어
가 얼마든지 발생할 수 있으며, 이런 불일치는 수치해석 알고리즘에서 매우 중요한 이슈다. 오차가 작더라도 연산 과정을 지나치게 반복하거나, 절대값이 매우 큰 수와 매우 작은 수를 덧셈 또는 뺄셈하는 순서를 잘못 잡으면 상대적으로 큰 오차가 결과에 남는다. 큰 수와 작은 수가 함께 있을 경우에는 작은 수가 가지는 유효숫자의 부분이 큰 수에 묻혀버리는(cancellation) 문제가 발생하기도 한다.
잘 알려진 예로, 큰 값과 작은 값을 단순 덧셈할 때 작은 값이 멘티사 영역에서 제대로 표현되지 못하고 반올림되어 사라지는 현상이 발생한다. 이러한 상황에서, 더할 값들을 크기 순으로 정렬하여 덧셈을 하면 보다 안정적인 결과를 얻을 수 있다. 일부 수치 알고리즘은 이러한 원리를 반영하여 오차 전파를 줄이기 위해 연산 순서를 신중하게 결정하고, 이를 통칭하여 수치적 안정성(numerical stability)이라는 개념을 활용한다.
간단한 예시: 덧셈 순서에 따른 차이
예시로 작은 값을 여러 번 더한 뒤 큰 값을 더하는 경우와, 먼저 큰 값을 더한 뒤 작은 값을 연속해서 더하는 경우를 살펴볼 수 있다. 아래 Python 코드로 직접 실험해볼 수 있다.
이 코드에서는 작은 값을 백만 번 더한 뒤 큰 값을 마지막에 더하는 경우와, 큰 값을 먼저 더한 뒤 작은 값을 백만 번 더하는 경우를 비교한다. 실제로 실행해 보면, 두 결과 사이에 소수점 이하 여러 자리에서 차이가 발생할 수 있다. 부동소수점 환경에서는 작은 값이 큰 값과 함께 더해질 때 제대로 반영되지 못하는 상황이 빈번하기 때문이다.
덧셈 이외에도 이러한 오차는 곱셈, 나눗셈, 심지어 지수·로그 연산에서도 물론 발생한다. 핵심은 부동소수점 연산이 이산적 근사에 기반을 두고 있으며, 모든 연산마다 반올림 오차가 포함될 수 있음을 인지하고, 알고리즘 단계에서 이를 최소화하도록 유도하는 방법들을 고안해야 한다는 점이다.
고정소수점과 부동소수점의 비교
컴퓨터로 실수를 표현하는 방식은 크게 고정소수점(fixed point)과 부동소수점(floating point)으로 구분할 수 있다. 고정소수점 방식에서는 소수점 이하 자릿수를 미리 정해 두고, 모든 수를 동일한 자릿수로 표현한다. 예컨대 임의의 정밀도로 소수점 이하 2자리를 유지하기로 했다면, 모든 수는 소수점 둘째 자리까지만 표현된다. 이는 구현이 단순하고 하드웨어 지원이 용이하다는 이점이 있지만, 표현할 수 있는 수가 매우 제한적이어서 오버플로우와 언더플로우가 발생하기 쉽고, 미세한 표현이 필요한 경우 오차가 커질 수 있다.
부동소수점 방식에서는 수를 $\displaystyle m \times \beta^e$ 형태로 표현한다. 멘티사 $m$가 나타내는 유효숫자의 정밀도와, 지수 $e$가 나타내는 스케일을 분리하여 매우 큰 수부터 매우 작은 수까지 폭넓게 표현할 수 있다. 다만 부동소수점에도 고정된 멘티사 비트 수로 인한 제약이 존재하므로, 반올림 오차가 발생하고 이로 인해 누적 오차나 전파 오차가 일어난다. 고정소수점보다는 훨씬 폭넓은 표현 범위를 확보할 수 있다는 장점과 함께, 지수 연산의 정규화(normalization) 과정에서 발생하는 복잡한 현상들을 주의 깊게 살펴야 한다.
Underflow, Overflow, 그리고 Denormalized Number
부동소수점 표준(예: IEEE 754)에서는 표현할 수 있는 최댓값과 최솟값이 정해져 있다. 예를 들어 단정밀도(single precision)에서는 지수부가 8비트이므로 표현 가능한 지수 범위에 제한이 있고, 그 범위를 벗어나면 오버플로우(overflow) 또는 언더플로우(underflow)가 발생한다.
오버플로우는 연산 결과가 너무 큰 수가 되어 부동소수점으로는 표현할 수 없는 상황이다. 일반적으로 $\pm \infty$를 대체값으로 반환하기도 한다. 언더플로우는 반대로 연산 결과가 너무 작아, 멘티사로도 제대로 표현하기 어려워 0에 가까운 값이 되어버리는 상황이다. IEEE 754에서는 언더플로우 구간에서 완전히 0으로 반올림하지 않고, 덴อร์말 숫자(denormalized number)를 사용하여 표현 가능한 최소 범위를 약간 확장해 놓았다. 덴อร์말 영역에서는 지수부가 최소값에 고정되며 멘티사의 선행 0 비트를 이용하여 실제 소수점을 이동시킨다. 이때 연산 속도가 낮아지고 정밀도 손실이 심해질 수 있으므로, 덴อร์말 수 영역에서의 계산은 특히나 오차가 커지기 쉽다.
언더플로우는 값이 매우 작아서 수치해석적으로 큰 의미를 가지지 않는 상황에서 주로 발생하기 때문에, 심각한 문제로 이어지지 않을 수 있으나, 덴더말 수 영역에서 미세하게 발행되는 오차가 반복 연산을 거쳐 누적되면 예상치 못한 결과로 이어질 수도 있다. 반면 오버플로우는 직접 무한대에 대응하는 값을 만들어 버리므로, 알고리즘의 로직이 무한대 값에 대해 정의되지 않은 경우 비정상 종료나 잘못된 결과가 나올 수 있다.
Kahan Summation Algorithm
덧셈 과정에서 발생하는 반올림 오차를 줄이기 위한 고전적인 기법으로 Kahan Summation Algorithm이 있다. 이 알고리즘은 실수 덧셈 시에 사라지는 오차를 별도로 추적하여 보정해 주는 방식이다. 간단히 말하면 덧셈 과정에서 반올림으로 인해 잃어버리는 자릿수를 기억해 두었다가, 다음 연산 시에 이를 반영하여 손실 오차를 최소화한다. 알고리즘은 아래와 같이 구현할 수 있다.
Kahan Summation Algorithm은 덧셈 순서를 달리하거나 작은 값과 큰 값을 함께 합산할 때 발생하는 오차를 줄여 준다. 모든 문제에 만병통치약처럼 적용하는 것은 아니지만, 조건 수가 크지 않은 덧셈 환경에서 누적 오차를 작게 유지하는 데 탁월한 효과가 있다.
Condition Number와 민감도
수치 해석에서 특정 문제(함수, 방정식 등)의 조건 수(condition number)는 입력값에 대한 결과의 민감도를 나타내는 측도다. 함수 $f(x)$가 있을 때, 작은 변화 $\delta x$가 출력값에 얼마나 큰 변화를 일으키는가를 지표화한 개념이 바로 조건 수다. 예를 들어 스칼라 함수에 대해
와 같은 식으로 정의하기도 하며, 다차원 벡터나 행렬 문제(예: $\mathbf{A}\mathbf{x}=\mathbf{b}$)에서는 노름(norm)을 이용하여 일반화한다. 문제의 조건 수가 매우 크면, 입력에서 발생하는 미세한 오차가 출력에서 엄청나게 증폭될 가능성이 높다. 이를 ‘ill-conditioned’하다고 부른다. 반대로 조건 수가 작은 문제는 약간의 입력 오차가 출력 오차에 큰 영향을 주지 않는 ‘well-conditioned’ 문제로 간주된다.
조건 수는 누적 오차와 전파 오차의 위험성을 예측하는 데 중요한 척도다. 알고리즘 자체가 수치적으로 아무리 안정적이라 해도, 문제 자체가 ill-conditioned라면 작은 반올림 오차가 빠르게 증폭되어 결과 해석이 어려워진다. 수치 계산을 수행할 때는 해당 문제나 알고리즘의 조건 수를 계산하거나 추정해 보고, 오차가 어떤 식으로 전개될지 예측하는 것이 유용하다.
Rounding Modes와 머신 엡실론
IEEE 754 부동소수점 표준에서는 반올림 모드를 여러 가지 정의한다. 일반적으로 가장 많이 쓰이는 것은 round to nearest-even(가장 가까운 값으로, 단 오차가 딱 중간값이면 맨 마지막 비트를 짝수로 만든다) 방식이다. 그 외에 round toward zero, round toward +∞, round toward -∞ 등도 상황에 따라 선택해서 사용할 수 있다.
반올림이 일어나는 근본 원인은 부동소수점에서 나타낼 수 있는 ‘간격’이 존재하기 때문이다. 가장 작은 양의 부동소수점 수 한 단위로 나타낼 수 있는 간격이 머신 엡실론(machine epsilon, $\epsilon_{\mathrm{mach}}$)과 밀접한 관련이 있다. 예를 들어 단정밀도의 경우 $\epsilon_{\mathrm{mach}} \approx 1.19209 \times 10^{-7}$, 배정밀도(double precision)에서는 $\epsilon_{\mathrm{mach}} \approx 2.220446 \times 10^{-16}$ 정도다. 실제로 이 값은 유효숫자 자리수에 따라 결정되며, 시스템에 따라 조금씩 차이가 있을 수 있다.
머신 엡실론은 “1보다 큰 수 중에서, 1에 더했을 때 부동소수점 표현에서 1과 구분되는 가장 작은 수”라는 의미를 가지며, 이는 시스템이 표현할 수 있는 ‘정밀도의 한계’를 가늠해 볼 수 있는 지표다. 반올림 오차, 누적 오차, 전파 오차를 체계적으로 분석할 때 필수적으로 고려되는 파라미터다.
연산 순서 최적화의 중요성
똑같은 수식이라도 연산 순서를 바꾸면 오차가 달라진다는 점은 부동소수점 연산에서 항상 주목해야 할 사항이다. 극단적으로 큰 수와 극단적으로 작은 수를 함께 연산하는 경우, 큰 수의 멘티사 영역에서 작은 수가 거의 반영되지 못하고 반올림되어 사라지는 문제가 생긴다. 예를 들어
연산에서 실제 부동소수점 환경에서는 $10^8 + 10^{-8}$가 정확히 $10^8$로 표현될 가능성이 매우 높으므로, 최종 결과가 0이 되어버릴 수 있다. 수치해석 알고리즘을 작성할 때는 이런 ‘결합법칙이 성립하지 않는’ 문제들을 회피하기 위해, 수의 크기가 유사한 항끼리 먼저 연산하거나 Kahan Summation처럼 보조 변수를 두어 오차를 추적하는 등의 기법을 적극적으로 활용한다.
mermaid를 이용한 연산 전파 예시
오차가 전파되는 과정을 단순화한 흐름도로 표현해 볼 수 있다.
초기값부터 최종 결과에 이르는 과정 중 매 단계마다 반올림 오차가 추가될 수 있으며, 이 오차가 누적 혹은 전파되어 결과에 영향을 준다. 각 단계에서 발생하는 오차의 양은 현재 표현 중인 수의 스케일 및 연산 종류에 따라 달라진다. 곱셈, 지수, 로그와 같은 비선형 연산일수록 오차 증폭이 일어나기 쉽고, 반복 구조가 있는 경우 오차가 계속해서 누적된다.
전진 오차, 후진 오차, 그리고 Backward Error Analysis
수치해석 문제에서 전진 오차(forward error)는 우리가 궁극적으로 구하고자 하는 결과 값 자체와 참값의 차이를 의미한다. 반면 후진 오차(backward error)는 계산된 근사해가 실제로 만족하는 ‘근사 문제(perturbed problem)’와 원래 문제의 차이를 측정한다. 이 개념은 다음과 같이 예시로 설명할 수 있다.
행렬 $\mathbf{A}$와 벡터 $\mathbf{b}$가 주어졌을 때, 연립방정식 $\mathbf{A}\mathbf{x} = \mathbf{b}$의 해를 구한다고 하자. 해를 $\mathbf{x}$라 하고, 수치 계산 결과 얻은 근사해를 $\tilde{\mathbf{x}}$라고 할 때, 전진 오차는 단순히 $|\mathbf{x} - \tilde{\mathbf{x}}|$와 같은 형태로 해석된다. 그러나 후진 오차 분석에서는 문제를 조금 바꾼 $\mathbf{A} + \Delta \mathbf{A}$와 $\mathbf{b} + \Delta \mathbf{b}$를 정의하여,
를 만족시키는 $\Delta \mathbf{A}$와 $\Delta \mathbf{b}$를 찾고, 이 $\Delta \mathbf{A}$와 $\Delta \mathbf{b}$가 원래 문제에 비해 얼마나 작은지를 측정한다. 즉, 근사해가 근사 문제에서는 “정확한 해”가 되도록 문제 자체를 아주 약간만 변경한 뒤 그 변경의 크기를 측정하는 접근이다. 이때,
가 작은 값이라면, “근사해 $\tilde{\mathbf{x}}$는 원래 문제와 별반 다르지 않은 문제에 대해 정확한 해”라고 볼 수 있으므로, 결과적으로 수치해석 알고리즘이 양호하다고 말할 수 있다. 이를 알고리즘의 후진 안정성(backward stability)이라고 한다.
Backward error analysis가 의미 있는 이유는 부동소수점 연산 특성상, 실제로는 문제를 일부 변형(perturbation)한 것이나 마찬가지라는 관점이 합리적이기 때문이다. 예를 들어 행렬 계수나 우변 항이 부동소수점 반올림 오차에 의해 조금씩 달라진다고 생각하면, 그 바뀐 문제의 해를 정확히 계산하는 것으로 해석할 수 있다. 수많은 선형대수 알고리즘(가우스 소거법, LU 분해, QR 분해 등)이 후진 안정적인 것으로 알려져 있어, 이론적으로는 오차가 일정 수준을 넘지 않고 잘 제어된다는 점을 보장해 준다.
Forward Error와 Condition Number
반면 전진 오차(해 자체의 오차)를 직접 추적할 때는, 문제의 조건 수(condition number)에 크게 의존한다. 수치적으로 ill-conditioned 문제라면, 아무리 후진 안정적인 알고리즘이라 해도 전진 오차가 클 수 있다. 예를 들어 $\mathbf{A}\mathbf{x} = \mathbf{b}$ 문제에 대한 조건 수 $\kappa(\mathbf{A})$가 매우 크다면, $|\Delta \mathbf{A}|$가 작아도 그로 인한 $|\Delta \mathbf{x}|$는 상당히 커질 수 있다. 결국 오차가 전파되는 과정은 “문제 자체가 민감한가(ill-conditioned)”, 그리고 “알고리즘이 수치적으로 안정적인가”라는 두 축에 의해 결정된다.
수치적 안정성과 알고리즘 선택
수치적 안정성(numerical stability)은 수치 알고리즘이 부동소수점 연산으로 인한 불가피한 반올림 오차를 얼마나 확대하거나 억제하는지를 나타내는 개념이다. 일반적으로,
후진 안정적인(backward stable) 알고리즘은 “조금 달라진 문제를 정확하게 해결”한 것으로 간주되므로, 가장 이상적인 안정성 개념에 가깝다.
전진 안정적인(forward stable) 알고리즘은 직접적으로 해의 전진 오차가 매우 작도록 제어하지만, 수학적 조건을 만족하기가 더 까다로운 경우가 많다.
실제로 수치해석에서 높은 수준의 신뢰도를 가지려면, 후진 안정성을 만족하는 알고리즘(예: 행렬 분해 기반)과 적절한 피벗팅(pivoting) 기법을 병행해서 사용한다. 예를 들어 LU 분해나 QR 분해를 이용하여 선형 시스템을 푸는 대부분의 표준 알고리즘은 backward stable한 것으로 알려져 있다.
Cancellation Error와 부분 피벗팅
뺄셈 과정에서 두 수가 서로 매우 가까우면, 차이가 실제 유효숫자 자리 중 극히 일부만 남아 significand(멘티사)가 짧아진다. 이를 “소수점 이하 유효숫자가 날아갔다(cancellation)”고 부른다. 이런 상황이 반복되면 누적 오차가 심각해질 수 있는데, 예를 들어 가우스 소거 과정에서 대각원소(pivot)가 매우 작은 상태로 두면, 그 행의 연산에 뺄셈을 자주 수행할 때 cancellation 현상이 심화된다.
이 문제를 완화하기 위한 대표적인 기법이 바로 부분 피벗팅(partial pivoting)이다. 각 단계에서 대각원소가 될 값이 충분히 커지도록 행 교환(row interchange)을 수행하여, 뺄셈 연산 과정에서 발생할 수 있는 반올림 오차를 줄이고 오차 전파를 억제한다. 완전 피벗팅(complete pivoting)은 행뿐 아니라 열(column)도 교환하여 좀 더 강력한 안정화를 추구하지만, 구현 복잡도가 높아 실제로는 부분 피벗팅이 많이 쓰인다.
수치해석 소프트웨어 라이브러리의 오차 관리
대부분의 언어나 환경에서 제공되는 선형대수 라이브러리(예: BLAS, LAPACK, Eigen, Armadillo 등)에는 이처럼 수치적 안정성을 최대한 확보하기 위한 구현 노력이 이미 들어가 있다. 즉, 표준 알고리즘으로 LU 분해를 수행할 때 자동으로 부분 피벗팅을 적용하고, 가우스 소거법의 각 단계에서 cancellation 에러를 줄이도록 설계된 경우가 많다. 수치 계산을 직접 구현하기보다는 검증된 라이브러리를 적극 활용하는 것이 누적 오차와 전파 오차의 위험을 낮추는 좋은 방법이다.
신뢰도 높은 계산을 위한 권장 접근법
문제의 규모가 커질수록, 각 단계에서 발생하는 미세한 부동소수점 오차가 최종 결과에 상당히 큰 영향을 끼칠 수 있다. 따라서 수치해석에서는 단순히 “소수점 이하 몇 자리 정확하다”는 정성적 언급으로 끝내는 대신, 가능한 한 정확도의 한계를 추정하거나, 해석적으로 오차 범위를 구한 뒤 그 오차 내에서 결과를 해석하는 습관이 필요하다.
이때 문제의 조건 수, 알고리즘의 후진 안정성, Kahan Summation 같은 오차 보정 기법, 피벗팅, 규모가 큰 문제에 대한 적절한 반복 알고리즘 선택 등이 모두 복합적으로 고려되어야 한다. 반복적 방법(예: CG(Conjugate Gradient), GMRES, Jacobian-Free Newton-Krylov 등)을 사용할 때도, 각 반복 스텝에서 일어나는 오차 전파와 누적을 완화하기 위한 사전조건화(preconditioning)가 필요할 수 있다.
다중정밀도 연산과 임의정밀도 연산
부동소수점 연산에서 고정된 비트 수로 표현 가능한 정밀도가 한계가 될 때, 다중정밀도(multiple precision) 또는 임의정밀도(arbitrary precision) 라이브러리를 활용할 수 있다. 대부분의 프로그래밍 언어에는 표준 부동소수점(double precision 등) 이외에, 사용자가 원하는 만큼 많은 자릿수를 확보하여 계산할 수 있는 라이브러리가 존재한다. 예를 들어 Python의 decimal 또는 fractions, C++의 GMP, MPFR 등이 대표적이다.
이들 라이브러리는 내부적으로 매우 큰 정수 연산을 통해 분수나 가변 길이 소수를 표현함으로써, IEEE 754 표준의 고정 정밀도를 넘어서는 계산을 수행한다. 임의정밀도 계산을 통해 반올림 오차를 극적으로 줄일 수 있지만, 그만큼 연산 비용이 증가하므로 대규모 반복 계산에서 활용하기는 쉽지 않다. 따라서 실용적으로는, 표준 부동소수점으로도 충분히 정확한 결과를 얻을 수 있는지 먼저 판단하고, 정말로 높은 정밀도가 필요한 부분에서만 임의정밀도 연산을 도입하는 식으로 전략을 세운다.
심볼릭(symbolic) 연산과 수치(numeric) 연산의 차이
심볼릭 연산 환경(예: Mathematica, Sympy 등)에서는 수식을 기호로 직접 다루어, 가능한 한 정확한 형태로 결과를 유지하려고 시도한다. 예를 들어 $1/3$을 0.3333 대신 그대로 $\frac{1}{3}$로 표현하고, 나중에 필요한 순간에만 수치화를 하여 근사값을 얻는다. 이런 환경에서는 누적 오차나 전파 오차를 일으키지 않고, 상징적 조작을 통해 미분이나 적분, 방정식 해 등을 구할 수 있다는 이점이 있다.
그러나 복잡도가 높은 실제 문제(예: 아주 높은 차수의 다항식, 대규모 선형 시스템, 비선형 편미분방정식 등)에서는 심볼릭 연산의 메모리·시간 비용이 막대한 경우가 많다. 그래서 실전 응용에서는 결국 부동소수점 기반의 수치 해석 기법과 심볼릭 기법을 적절히 병행하기도 한다. 예를 들어 중요한 부분식에 대해서는 심볼릭 전개를 한 뒤, 나머지는 수치 알고리즘을 적용하는 식이다.
Catastrophic Cancellation(재앙적 상쇄) 예시
수치해석에서 널리 알려진 “Catastrophic Cancellation(재앙적 상쇄)”은 서로 매우 비슷한 두 수를 뺄셈했을 때 결과의 유효숫자가 극도로 줄어드는 현상을 말한다. 예컨대
을 부동소수점 환경에서 직접 계산하면, 두 수가 유효숫자 대부분을 공유하므로 멘티사 차원에서 거의 모든 자릿수가 상쇄되고, 반올림으로 인해 결과가 1.000000\times 10^{-9} 정도로 ‘의도했던 자릿수만큼 정확히’ 표현되지 못할 수 있다. 더 심각한 예로, 뺄셈이 여러 번 반복되는 과정에서 매 단계마다 자릿수가 사라지면, 누적 오차가 상당히 커질 수 있다.
이 문제를 해결하기 위한 대표적인 기법 중 하나가, 가급적 뺄셈을 회피하거나(서로 가까운 값을 직접 뺄셈하지 않도록 식을 재정렬) Kahan Summation처럼 ‘사라지는 자릿수’를 보존하려 애쓰는 방식이다. 또, 일부 알고리즘에서는 Taylor 전개나 항등식 변환을 통해 이 상쇄가 일어나지 않게끔 식을 변형하기도 한다. 예를 들어
의 형태는 $x$가 극도로 작을 때 바로 catastrophic cancellation의 위험을 안고 있다. 이를
처럼 유리화(rationalization)하여 직접적인 뺄셈을 피하면, 오차 전파를 크게 줄일 수 있다.
무의미한 계수 추적 피하기
수치 해석 과정에서는 매우 작은 계수나 매우 큰 계수를 그대로 보존하려고 하면, 실제로 거의 의미가 없는 값을 계속 추적하느라 오히려 오차가 커질 수 있다. 예를 들어 고차 다항식의 해를 구하거나 미분방정식을 풀 때, 불필요하게 큰 계수나 작은 계수를 껴안고 있으면 반올림 오차가 전파된다. 어떤 경우에는 사전에 스케일링(scaling)을 통해 문제를 재구성한다. 예컨대 연립방정식 $\mathbf{A}\mathbf{x} = \mathbf{b}$에서 각 행의 크기가 너무 들쭉날쭉하면, 행이나 열을 스케일링하여 보다 균질한 수치 범위로 맞춘 뒤 알고리즘을 적용한다.
이러한 사전·사후 스케일링 기법은 단순하면서도 강력한 효과를 발휘한다. 실제로 대규모 행렬 계산 라이브러리는 자동으로 스케일링을 적용해 문제를 안정화하고, 해 구한 뒤 다시 역스케일링(inverse scaling)하여 원래 스케일로 되돌리기도 한다. 그런 과정을 통해 누적 오차와 전파 오차의 영향을 감소시키면서도 연산 효율을 높인다.
대표적 예시: 근의 공식과 수치 불안정
고등학교 때 배운 2차방정식 해 공식
조차도, $b^2 \gg 4ac$인 경우 (즉, 판별식이 $b^2 - 4ac$와 거의 유사한 크기일 때) 뺄셈이 매우 민감해진다. 예컨대 $b^2$와 $4ac$가 서로 근소한 차이를 가지면, $\sqrt{b^2 - 4ac}$도 $|b|$와 비슷해져서, $-b \pm \sqrt{b^2 - 4ac}$가 catastrophic cancellation을 일으키기 쉽다. 이런 경우에는 역수를 취하여
형태로 바꿔서 뺄셈을 회피하기도 한다. 이것은 2차방정식조차 정밀도가 극단적으로 요구되는 상황(예: $b\approx \sqrt{4ac}$)에서는 제대로 된 근을 구하기 어렵다는 사실을 일깨워 준다.
이 원리는 3차 방정식 이상의 근의 공식에서도 훨씬 복잡한 형태로 나타난다. 실제로 고차 다항식에서는 심각한 수치 불안정 문제로 인해, 일반 근의 공식 적용이 거의 실무에서 배제되고 수치적 근사법(예: 브레트슈나이더 법, 뉴턴-랩슨 법, Laguerre 방법 등)이 선호된다.
오차 제어와 반복적 방법의 예시
비선형 방정식이나 고차 방정식을 해결할 때는 반복적 방법(iterative method)을 주로 쓴다. 예를 들어 뉴턴 방법(Newton’s method)은
형태로 진행되는데, 초기 추정값 $x_0$가 좋지 않으면 오차가 전파되어 발산하거나, 잘못된 국소해에 빠질 위험이 있다. 게다가 매 스텝에서 $f(x_k)$와 $f'(x_k)$를 부동소수점으로 계산할 때 발생하는 반올림 오차가 누적·전파될 가능성도 있다. 따라서 올바른 초깃값을 설정하고, 반복 과정을 적절히 종료시키는 기준(수렴 판정 조건)을 세워서 지나치게 많은 반복으로 인해 오차가 불필요하게 커지는 현상을 막아야 한다.
선형 시스템을 반복적 방법으로 푸는 경우(예: CG(Conjugate Gradient), GMRES, BiCGSTAB 등)에도, 알고리즘 자체의 수렴 특성과 반올림 오차 누적이 조합되어 민감한 거동을 보일 수 있다. 실제로 수치 계산에서는 반복 스텝마다 축적되는 오차가 크다면, 적절한 사전조건화(preconditioning)로 문제의 조건 수를 낮춰주기도 한다.
실전 팁: 디버깅과 검증
수치 알고리즘을 직접 구현하거나, 혹은 제공된 라이브러리를 사용할 때도, 결과값이 과연 믿을 만한가를 검증하기 위해 여러 전략을 쓴다. 예컨대 문제 규모를 작게 줄였을 때(테스트 케이스) 수작업 혹은 고정밀도(또는 심볼릭) 방식으로 결과를 비교하거나, 서로 다른 알고리즘을 이용해 해를 구해 보고 일치 여부를 확인한다. 수치계산에서는 로직의 오류가 없이도, 단지 반올림 오차 때문에 다른 결과가 나오므로, ‘코드가 맞게 동작한다’는 사실과 ‘결과가 정확하다’는 사실을 혼동하지 않도록 주의해야 한다.
수치적 디버깅은 단순 문법 오류나 논리적 버그를 찾는 것보다 훨씬 까다로운 면이 있다. 예를 들어, 행렬 $\mathbf{A}$가 두 개의 서로 다른 분해(LU, QR)를 통해 구한 해가 서로 눈에 띄게 다르게 나온다면, 알고리즘적 문제보다도 $\mathbf{A}$ 자체가 극도로 ill-conditioned인지 먼저 의심해야 한다. 이처럼 문제와 알고리즘, 그리고 표현 정밀도라는 세 요소가 서로 복잡하게 얽혀 있으므로, 어디서부터 오차가 발생하고 얼마나 전파되는가를 체계적으로 추적하고 해석하는 역량이 필수적이다.
--- 전 준비: 핵심 아이디어 재정리
이제까지 살펴본 누적 오차와 전파 오차의 개념, 그리고 이를 최소화하기 위한 다양한 기법들은 현대 수치해석이 필수적으로 다루어야 할 주제다. 작은 반올림 오차가 반복되고, 특정 단계에서 뺄셈이나 곱셈, 지수 연산 등으로 인해 기하급수적으로 커질 위험이 항상 도사리고 있다. 특히 대규모 문제에서는 이런 미시적 차이가 전체 결과를 뒤흔들 수 있으므로, 알고리즘 선택부터 구현, 디버깅, 검증 단계까지 모두 주의 깊은 접근이 필요하다.
수치계산에서 자료구조와 알고리즘의 영향
수치 계산 알고리즘이 단지 수학적 이론에 의해 결정되는 것만은 아니다. 실제 컴퓨터에서 동작할 때, 자료 구조(배열 배치, 스파스 매트릭스 표현, 캐시 구조 등)와 프로세서의 병렬 처리 및 벡터화 여부, GPU나 TPU 같은 특수 하드웨어 사용 등도 누적 오차와 전파 오차에 중요한 영향을 미친다. 예를 들어 매트릭스 벡터 곱셈 연산에서 연산 순서가 하드웨어적으로 뒤바뀔 수 있고, 병렬 연산 중간에 반올림이 일어나는 시점도 바뀐다. 한편 GPU 라이브러리는 고정된 순서를 따르지 않고, 내부적으로 스레드별 계산을 비동기적으로 수행하기 때문에 결과 순서가 달라질 가능성이 있다. 이런 경우에도 알고리즘이 수치적으로 안정적이면, 순서가 바뀌어도 최종 오차 범위가 크게 달라지지 않지만, 불안정한 알고리즘일수록 결과가 크게 요동칠 수 있다.
고차원 문제에서의 오차 전파
현대 과학·공학에서는 변수의 차원이 매우 큰 문제(수천만 개 이상의 자유도)도 일상적으로 다룬다. 예를 들어 유체 역학(CFD), 전산 구조 해석(FEA), 기계학습(딥러닝의 거대 파라미터 집합) 등이 대표적이다. 이런 초대형 문제들은 수많은 반복 연산을 통해 해를 구하거나(반복 알고리즘), 혹은 방대한 행렬 연산을 여러 차례 수행한다(직접 분해 또는 분산 알고리즘). 변수 차원이 올라가면, 중간 과정에서 발생하는 작은 오차나 반올림 효과가 누적·전파될 가능성이 훨씬 커진다.
대규모 행렬 계산에서 흔히 쓰이는 스파스(sparse) 구조 최적화, 도메인 분할(domain decomposition), 비동기 통신(asynchronous communication) 기법 등은 모두 연산 순서나 연산 방식의 가변성을 초래한다. 따라서 결과 재현성(bitwise reproducibility)을 보장하기 어렵고, 그만큼 수치적 안정성이 더욱 중요해진다. 이 분야에서는 IEEE 754를 넘어서는 혼합 정밀도(hybrid precision) 기법이나 블록 연산 알고리즘 등, 하드웨어 특성을 활용한 기법이 활발히 연구되며, 그 중심에는 오차 누적과 전파를 어떻게 억제할 것인가 하는 문제가 자리잡고 있다.
실험적 비교를 통한 오차 분석
특정 알고리즘의 오차 거동을 비교·분석할 때는, 실제 문제나 가상의 테스트 문제를 설정하여 서로 다른 정밀도(예: float, double, extended precision 등)와 서로 다른 알고리즘(예: 직접 분해, 반복법, 다양한 스케일링/피벗팅 옵션)을 적용해 보는 식으로 실험한다. 다음과 같은 Python 예시는 특정 선형 시스템을 다양한 방식으로 푸는 예가 될 수 있다.
이 코드를 돌려 보면, 문제 차원 $n$을 달리하거나, 행렬 생성 방식을 다르게 했을 때, x_direct와 x_cg 사이의 차이가 얼마인지, 그리고 그 차이가 문제의 스케일과 조건 수에 어떻게 의존하는지를 관찰할 수 있다. 대규모 문제에서는 부동소수점 정밀도의 제약, 연산 순서 변화, 반복 알고리즘 특성 등이 복합적으로 작용해 결과 오차가 기하급수적으로 커질 수 있다.
오차 시각화 기법
수치 해석 문제에서 유용한 기법 중 하나는, 계산 과정 혹은 결과 오차를 시각화하는 것이다. 예를 들어 결과 벡터와 정확한 해(혹은 참고 해)의 차이를 플롯하거나, 과정 중 일부 중간 변수를 그래프로 찍어본다. 행렬 연산이라면 행렬 계수의 크기를 컬러 맵으로 나타내어, 어디에서 대규모 연산이 집중되는지, 혹은 스케일이 지나치게 큰 행이나 열이 있는지를 파악할 수 있다.
전치사 곱(예: $A^T A$) 같은 연산이 반복될 때, 값이 극도로 커지거나 작아지는 부분이 있는지도 중요하다. 이를 테면 $|A^T A|$가 매우 커지면 수치 해가 불안정해질 가능성이 높으므로, 문제에 대한 사전 스케일링(행, 열, 혹은 대각 스케일링) 절차를 고려해야 한다.
분산·병렬 환경에서의 오차 양상
현대 대규모 계산은 거의 대부분 병렬화(parallelization)와 분산 처리(distributed computing)를 동반한다. 행렬이 수많은 노드에 분산되어 있고, 각 노드가 부분 계산을 수행한 뒤 결과를 합산하는 식으로 운영될 수 있다. 이때 합산 순서가 달라지면, 부동소수점 덧셈 연산의 순서가 달라져서 오차 양상도 달라진다. 하지만 well-conditioned 문제와 안정적인 알고리즘이라면, 합산 순서가 달라져도 결과 자체는 큰 차이가 나지 않아야 한다. 반면 ill-conditioned 문제에서나 불안정한 알고리즘에서는 노드 간 통신 순서에 따라 결과가 크게 달라지고, 재현성이 떨어지는 상황이 발생할 수 있다.
MPI, OpenMP, CUDA 같은 병렬 라이브러리들은 각자 내부적으로 연산 순서를 최적화하거나 파이프라이닝(pipelining)을 적용한다. 최종 결과가 일관되도록 설계된 라이브러리도 있지만, 높은 성능을 위해 결과 재현성을 포기하고 덧셈 순서를 가변적으로 선택하는 라이브러리도 있다. 이런 환경에서는 ‘정확한 재현성’보다 ‘통계적으로 유사한 결과’가 중요하게 취급되는 사례가 많고, 반복 알고리즘에서는 이 점을 감안하여 수렴 조건을 완화하거나, 충분히 작은 수렴 오차(tolerance)를 설정해 실용적인 해를 얻는 방식을 쓴다.
더 알아보기
일반적으로 누적 오차와 전파 오차를 다루는 가장 체계적인 문헌은 수치해석 교과서(예: Dahlquist & Björck, Trefethen & Bau, Golub & Van Loan 등)와 IEEE 부동소수점 표준 문서, 그리고 선형대수 패키지의 공식 자료들이다. 이들을 참고하면, 어떤 알고리즘이 왜 수치적으로 안정적인지, 각 단계에서 어떤 반올림 오차 모델을 가정하는지를 엄밀하게 설명하고 있다. 특히 대규모 계산 분야에서는 슈퍼컴퓨터 환경에서의 오차 모델을 별도로 연구하기도 하며, 혼합 정밀도 기법을 활용하여 속도와 정확도 사이의 절충을 노리는 연구가 활발하다.
Last updated