수의 표현: 유효숫자와 반올림 오차
수치계산에서 수의 표현이 중요한 이유
컴퓨터로 이루어지는 모든 수치계산 과정에서 실제로는 유한 개의 비트(bit)로만 수를 표현한다. 이 때문에 매우 큰 값이나 매우 작은 값을 정확히 표현하기가 어렵고, 부동소수점 방식의 표현에서도 특정 자릿수(유효숫자)까지만 저장이 가능하다. 이러한 표현 한계로 인해 꼭 필요한 계산조차도 약간의 차이가 발생하며, 이를 무시할 수 없을 때 “수치해석적 고찰”이 필요하다.
수의 표현 방식은 정밀도와 연산의 안정성에 큰 영향을 미친다. 예를 들어, $10^8$ 정도까지 단정밀도(single precision)로 정확하게 표현될 수 있는지, 아니면 배정밀도(double precision)로 표현되어야 하는지, 혹은 정밀도를 넘어서는 연산에서 언제 오차가 발생하기 시작하는지를 파악하는 것은 매우 중요하다. 이를 위해 유효숫자와 반올림 오차의 개념이 활용된다.
유효숫자의 정의
유효숫자는 주어진 수에서 의미 있는 숫자의 자리수를 말한다. 예를 들어, 실제로 1234.567이라는 값을 다루고 있을 때 1234.56만을 표현할 수 있다면 이 수에는 약 6자리 정도의 유효숫자를 확보하고 있다고 볼 수 있다. 반대로 0.0001234를 고려한다면, 맨 앞의 0들은 실제로는 유효숫자에 포함되지 않으며, 1234만큼이 실제로 유효숫자가 된다.
유효숫자 개수를 보다 수학적으로 정의하기 위해 보통 다음의 두 관점을 활용한다. 하나는 10진법 지수 형태로 나타내어 맨 앞자리부터 시작하여 의미가 있는 모든 자리를 센다는 관점이고, 다른 하나는 상대 오차의 관점에서 특정 정밀도를 만족시키는 숫자의 개수를 세는 방식이다.
예를 들어 어떤 실제 값 $x$와 근사값 $\hat{x}$가 있을 때, 유효숫자 $n$개를 가진다는 것은 다음과 유사한 의미로 해석할 수 있다.
즉, 상대 오차가 대략 $5 \times 10^{-n}$보다 작으면 $\hat{x}$는 $x$를 $n$개의 유효숫자로 표현하고 있다고 해석할 수 있다. 여기서 5라는 상수는 반올림 오차가 마지막 자리의 절반 이하 정도를 의미한다는 전통적인 정의에 기인한다. 실제로 이를 엄밀히 정의하는 과정에는 다양한 세부 조건이 따르지만, 실제 수치 계산에서는 이러한 관점이 직관적으로 잘 쓰인다.
고정소수점과 부동소수점 표현
컴퓨터 내부에서 수를 나타내는 일반적인 방식은 부동소수점(floating point) 표현이다. 과거에는 고정소수점(fixed point) 방식도 많이 사용되었지만, 오늘날의 과학 및 공학 계산 대부분은 부동소수점 방식으로 처리된다. 이를테면, 10진수에서 $x$라는 수를 $d_1.d_2d_3 \times 10^p$ 형태로 나타내거나, 2진수에서 $b_1.b_2b_3 \times 2^q$와 같은 형식으로 표현한다.
부동소수점 표현은 크게 지수(exponent)와 가수(mantissa, 혹은 significand) 부분으로 이루어진다. 단정밀도(32비트) 기준으로, 가수 부분에 사용할 수 있는 비트 수가 제한되므로 표현 가능한 유효숫자의 개수가 약 7자리 정도에 해당한다. 배정밀도(64비트)의 경우 가수 부분이 더 많아서 표현 가능한 유효숫자의 개수는 약 15자리 내외가 된다.
자연스럽게 가수부가 한정되어 있기에 충분히 큰 수나 아주 작은 수를 표현할 때, 지수부가 이를 커버하지 못하면 수의 최솟값 혹은 최댓값이 한계에 이르게 된다. 이 지점에서 오버플로(overflow) 혹은 언더플로(underflow)가 발생한다. 반면 정확히 표현할 수 있는 구간 내부에서도 소수점 이하가 잘려 나가는(예: 가수부 초과) 문제가 생기며, 이를 반올림(혹은 절단) 과정에서의 오차라고 한다.
반올림과 절단의 차이
반올림(rounding)과 절단(chopping)은 유효숫자를 제한할 때 적용되는 대표적인 두 방법이다. 절단은 계산 결과가 예를 들어 1.2345678이라는 값이 나왔다면 가수부의 최대 자릿수만 취하고 나머지는 버리는 방식이다. 반면 반올림은 그 다음 자리수를 살펴 절반을 넘으면 1을 올림하는 방식으로, 좀 더 정확한 근사값을 얻을 수 있다.
일반적으로 컴퓨터 내부의 부동소수점 표준(IEEE 754)에서는 round to nearest even(가장 가까운 값으로 반올림하되, 정확히 중간값일 때는 짝수 자리로 반올림)을 기본 방법으로 정의한다. 이는 나름의 통계적 장점이 있다고 알려져 있으며, 수치계산에서 누적오차를 줄이는 데 도움을 준다.
반올림 오차
반올림 오차(round-off error)는 이상적인 계산 결과와 실제 컴퓨터 부동소수점 연산 결과의 차이로 정의할 수 있다. 이는 결국 컴퓨터가 표현할 수 있는 디지털 신호의 한계에서 비롯되며, 되돌릴 수 없고 축적되기도 한다. 동일한 연산을 여러 번 반복하게 되면 그만큼 반올림 오차가 누적될 수 있는데, 이를 고려하지 않고 단순 반복 계산을 하면 상당한 오차가 쌓여 결과의 신뢰도를 해치게 된다.
반올림 오차를 정량적으로 나타낼 때 보통 절대오차와 상대오차가 사용된다. 어떤 실제값 $x$와 근사값 $\hat{x}$가 있을 때
라는 정의에 따라 두 값의 차이를 평가한다.
이때 유효숫자 $n$자리를 지닌다는 것은 어느 정도의 상대오차 이하임을 의미하므로, 반올림 오차 역시 상대오차 관점에서 제한된다. 결국 부동소수점 표현의 가수부 길이가 바로 유효숫자를 결정하며, 이를 바탕으로 가능한 반올림 오차의 범위가 자동으로 정해진다.
배정밀도 예시
배정밀도 방식에서 64비트 중 52비트가 가수부로 사용된다. 여기서 한 비트는 이진수 한 자릿수를 의미하므로, 52비트 가수부가 표현할 수 있는 이진수가 $2^{52}$가지이지만 실제로는 규정된 정규화(normalization) 형식 등에 따라 유효숫자가 약 15~16자리(10진수로 환산 시) 정도가 된다. 예를 들어 매우 간단하게 $\frac{1}{10}$이라는 값을 배정밀도로 표현하면, 이진수가 0.0(0011이 반복) 형태로 무한히 이어지므로 사실상 정확히 나타낼 수 없다. 실제 연산 과정에서는 $0.1_{10}$을 가장 가까운 이진 부동소수점 표현으로 바꾸게 되고, 그 과정에서 반올림 오차가 발생한다.
이러한 상황은 $\frac{1}{3}$, $\frac{1}{7}$ 등의 단순 분수에서도 비슷하게 나타난다. 반올림 오차는 아주 미세하지만, 반복 연산이나 민감한 상황(예: 큰 수와 작은 수의 뺄셈)에 대해서는 결코 무시할 수 없다.
오차 전파와 누적
반올림 오차는 단 한 번의 연산 후에도 발생하지만, 계산 과정이 복잡해질수록 점차 누적되고, 연산 순서에 따라 다른 결과를 낳기도 한다. 예를 들어 큰 수에 비해 매우 작은 수를 더하거나 빼는 연산을 할 때, 가수부 유효 자릿수가 제한되어 있기 때문에 작은 수가 의미를 잃고 사라져버리는 취소 오차(cancellation error)가 발생할 수 있다.
실제로 고차 방정식을 풀 때나, 적분을 근사적으로 구할 때, 행렬 연산을 많이 수행할 때 등에서 조금씩 생기는 반올림 오차가 쌓여서 결과에 큰 영향을 줄 수 있다. 따라서 계산 방법을 선택할 때는 이 오차가 얼마나 확대될지(오차 전파), 그리고 어떻게 줄일 수 있을지(수치해석적 기법)가 중요한 이슈가 된다.
머신 엡실론(machine epsilon)
부동소수점 연산에서 가장 중요한 개념 중 하나는 머신 엡실론이라 불리는 값이다. 머신 엡실론은 컴퓨터가 표현할 수 있는 “1보다 큰 수 중에서 1과 구분되는 가장 작은 값”으로 정의되며, 사실상 “부동소수점 연산에서 사용되는 상대적 정밀도의 척도”라고 볼 수 있다.
배정밀도(IEEE 754 64비트) 기준으로 머신 엡실론은 약 $2^{-52}$와 비슷하며, 10진수로 환산하면 대략 $2.22\times 10^{-16}$ 정도가 된다. 이 값은 1과 $1 + \varepsilon$($\varepsilon$가 매우 작은 값) 사이의 차이를 컴퓨터가 정확히 감지해 낼 수 있느냐의 기준이 된다. 실제 연산 과정에서는 1에 $\frac{1}{2} \times \text{machine epsilon}$ 이상을 더해야만 부동소수점 결과가 바뀌는 효과를 가지게 된다.
이를 조금 더 수학적으로 설명하면, 머신 엡실론은 다음과 같은 형태로 나타낼 수 있다.
여기서 $\text{fl}(\cdot)$는 컴퓨터의 부동소수점 연산에 의해 실제로 표현된 결과를 말한다. 즉, 1에 어떤 값을 더했을 때, 부동소수점으로 계산한 결과가 1보다 커지는 최소의 값이 머신 엡실론이다.
절대 정규화(normalized)와 부분 정규화(subnormal)
IEEE 754 형식에서 대부분의 수는 절대 정규화(normalized) 상태로 표현되지만, 지수부가 최소 지수에 도달할 때 더 작은 값을 표현하기 위해 부분 정규화(subnormal) 모드가 동작한다. 부분 정규화 상태에서는 가수부가 일반적인 정규화 상태와 달리 맨 앞자리가 1이 아닐 수도 있다.
이 과정을 통해 0에 매우 근접한 수를 표현할 수 있게 해주는데, 이 근사적 표현 때문에 반올림 오차가 커질 수 있다는 특징도 동시에 가진다. 예를 들어 부분 정규화 영역에서는 유효숫자가 줄어드는 효과가 있어, 상대 오차가 크게 증가할 수 있다.
단정밀도 vs 배정밀도
단정밀도(IEEE 754 32비트)에서는 8비트의 지수부와 23비트의 가수부를 가지므로 표현 가능한 유효숫자 자릿수는 대략 7자리 정도이다. 배정밀도(64비트)의 경우 11비트의 지수부와 52비트의 가수부로 이루어지며, 이진 정규화 상태에서 실제로는 53비트 정도의 유효 이진 자릿수를 확보하게 된다. 이를 10진수로 환산하면 약 15~16자리가 된다.
매우 정밀한 계산이 필요할 경우에도, 가수부 비트가 더 많은 고정밀도(fixed precision) 라이브러리를 사용하거나, 아예 임의정밀도(arbitrary precision) 연산을 제공하는 소프트웨어 환경을 사용하기도 한다. 다만 이러한 방법은 계산 속도가 느려지고 메모리를 많이 소모한다는 단점이 있다.
반올림 모드
IEEE 754 표준은 네 가지 대표적 반올림 모드를 정의한다. 기본값은 round to nearest, ties to even 방식이며, 그 외 round toward zero(0 방향으로 절삭), round toward +∞, round toward -∞ 모드를 선택할 수도 있다. 이 모드에 따라 똑같은 연산 결과가 약간 다르게 반올림될 수 있다. 그러나 대부분의 프로그래밍 언어와 컴파일러 설정이 기본 모드를 사용하기 때문에, 일반적인 수치계산에서는 round to nearest, ties to even 방식을 염두에 두면 충분하다.
ULP와 유효숫자
ULP(unit in the last place)는 어떤 수에 대해 “마지막 가수부 한 자리”에 해당하는 양을 말한다. 예를 들어 실수값 $x$가 부동소수점 표현에서
형태로 나타날 때, 마지막 유효 비트 한 자리는
와 같이 정의될 수 있다. 여기서 $p$는 부동소수점 표현에서 허용되는 가수부의 비트 수이다(단정밀도 24, 배정밀도 53 등). 결국 $x$ 근처에서 한 번의 반올림으로 인한 변화 폭이 얼마인지를 측정할 때 ULP를 사용하면 편리하다. 예컨대 $x + 0.5 \times \text{ULP}(x)$ 정도만큼 더해도 반올림 후 표현이 한 단위만큼 증가한다는 의미가 된다.
대표 예시: 큰 수와 작은 수의 덧셈
큰 수 $A$와 아주 작은 수 $b$의 덧셈 $A + b$를 부동소수점으로 계산할 때, 만약 $b$가 $A$에 비해 훨씬 작은 값이면 가수부의 한계를 초과하여 작은 항이 반영되지 않는 사태가 생길 수 있다. 이를 흔히 “catastrophic cancellation” 혹은 “잃어버린 정밀도” 문제라고 부른다.
가령 배정밀도에서 $A = 1.0 \times 10^{16}$이고, $b = 1.0$이라고 하면 실제로는 $A + b = 10,000,000,000,000,001$이지만 가수부가 이를 표현할 만큼 충분히 크지 않아서 $10,000,000,000,000,000$으로 반올림되어 버린다. 결과적으로 $b$가 전혀 반영되지 못하는 효과가 발생한다.
이 같은 문제는 수치해석 전반에서 자주 출현하며, 연산 순서를 적절히 바꾸거나, 더 작은 규모의 값을 먼저 더하고 뺀 뒤 큰 값을 취하는 방식으로 어느 정도 줄일 수 있다. 이를 수치적 안정성(numerical stability)의 관점에서 풀어나가는 것이 수치해석의 핵심 과제 중 하나다.
선형 연산과 반올림 오차 전파
벡터나 행렬 연산에서도 반올림 오차는 연산 횟수가 많아질수록 누적된다. 예를 들어 $\mathbf{A}\mathbf{x} = \mathbf{b}$ 형태의 선형시스템을 푸는 과정에서 가우스 소거법(Gaussian elimination)을 수행한다면, 열과 행의 서로 다른 배수 연산들이 잇따라 일어나면서 작은 반올림 오차들이 계속 쌓인다. 이때 $n \times n$ 행렬에 대해 최대 $O(n^3)$ 정도의 연산이 일어날 수 있으며, 컴퓨터가 표현할 수 있는 유효숫자가 고정되어 있으므로 많은 연산을 거치면 그만큼 오차 전파가 심해질 수 있다.
특정 알고리즘이 수치적으로 안정적(numerically stable)이라고 부르는 것은, 이 오차 전파를 가능한 한 억제해 주거나, 혹은 입력 데이터의 상태에 따라 증폭되지 않도록 처리한다는 것을 의미한다. 반면 일부 비안정적(unstable)인 방법은 정리상으로는 오차가 작아 보이더라도 실제 부동소수점 연산에서는 엄청난 반올림 오차가 누적되어 전혀 신뢰할 수 없는 결과를 내기도 한다.
Kahan Summation 알고리즘
부동소수점 환경에서 여러 개의 실수를 순차적으로 더할 때, 순서에 따라 연산 결과가 상당히 달라질 수 있다. 이는 개별 항들의 크기가 매우 상이하거나, 많은 수의 항이 포함될 때 반올림 오차가 누적되기 때문이다. 특히 더해지는 각 항의 부호가 엇갈리거나, 극단적으로 큰 값과 작은 값이 혼재되어 있으면 “catastrophic cancellation(치명적 소거)” 현상이 발생하여 정밀도를 크게 잃을 수 있다.
이 문제를 어느 정도 완화하기 위해 개발된 기법 중 하나가 Kahan Summation 알고리즘이다. 이 알고리즘은 현재까지의 합(sum)과 더하려는 항의 차이에 의해 발생하는 반올림 오차를 “보정항(correction term)”으로 추적하며, 다음 연산에서 이를 다시 반영해 주는 방식으로 동작한다. 알고리즘의 핵심 아이디어는 “잃어버린 자릿수”를 따로 저장해 놓았다가, 다음 항을 더할 때 같이 더해주는 방식이다.
아래는 Kahan Summation 알고리즘을 간략히 그림으로 나타낸 예시이다.
이 과정을 Octave(Python, C++ 등) 코드로 간단히 예시하면 다음과 유사하다.
Kahan Summation 알고리즘은 무조건적으로 모든 반올림 오차를 제거해 주지는 못하지만, 특히 절댓값 크기가 큰 항과 작은 항이 섞여 있을 때 비교적 안정적인 결과를 낳는다.
Pairwise Summation
또 다른 접근으로 Pairwise Summation 기법을 들 수 있다. 일반적인 순차적 덧셈이 아니라, 데이터를 두 부분으로 나눠 각각의 합을 구하고 이 두 결과를 다시 합하는 식으로 계속 병합하는 방식이다. 이는 트리(tree) 구조로 생각할 수 있는데, 이진 트리를 따라가며 두 수씩 더하는 과정이 반복된다. 이렇게 하면 항의 개수가 $n$이라고 할 때, 단순 순차 합산보다 반올림 오차가 줄어드는 효과가 있다.
실제로 큰 문제를 해결할 때는 Kahan Summation이나 Pairwise Summation 등 여러 방법이 함께 사용되거나, GPU처럼 병렬 연산을 지원하는 환경에서는 Pairwise Summation이 자연스럽게 선택되기도 한다.
Inner Product(내적)와 반올림 오차
내적 $\mathbf{a}\cdot\mathbf{b} = \sum_i a_i b_i$ 연산도 순차적인 곱셈과 덧셈이 일어나므로, 반올림 오차의 누적에 취약하다. 특히 벡터 원소들의 크기가 매우 다르다면, 아주 큰 항과 작은 항이 섞여서 cancellation이 발생하기 쉽다. 수치 선형대수 분야에서는 이 내적 계산이 매우 빈번하게 등장하므로, 다양한 완화 기법과 알고리즘이 연구되어 왔다.
예를 들어, BLAS(Basic Linear Algebra Subprograms) 라이브러리 구현체들은 내부적으로 확장 정밀도(extended precision)를 활용하거나, Pairwise Summation 방식을 택해 누적 오차를 줄이기도 한다.
확장 정밀도와 임의정밀도
현대의 CPU 중 일부는 내부 레지스터에서 80비트(혹은 그 이상의) 부동소수점을 사용하여 중간 계산을 수행한 뒤, 최종 결과만 64비트로 저장하는 확장 정밀도 방식을 지원하기도 한다(x86 아키텍처의 FPU 등). 이 경우, 중간 연산에서 반올림 오차가 한 번 더 줄어들어서 보다 정확한 결과를 유도할 수 있다.
프로그래밍 언어 혹은 라이브러리 레벨에서 아예 임의정밀도 연산(arbitrary precision arithmetic)을 사용하면, 이론적으로 원하는 만큼 유효숫자를 확보하여 오차 없이 계산할 수 있다. 그러나 그 대가로 연산 비용이 급격히 증가하고, 일반적인 하드웨어 지원 부동소수점보다 메모리 사용량도 커진다. 따라서 실제 수치해석 문제에서는, 필요한 정밀도가 어느 수준인지, 그리고 그에 따라 발생하는 계산 비용을 감수할 수 있는지를 판단하여 적절히 선택한다.
수치적 안정성과 오차 제어
결국 수의 표현 방식이 제한적인 환경에서 오차를 최소화하려면, 알고리즘 자체가 안정적이어야 하고, 계산 순서를 조정하거나 데이터 전처리 등을 통해 “나쁜 패턴”이 발생하는 것을 줄여야 한다. 이를 수치적 안정성(numerical stability)이라고 하며, 안정적인 알고리즘은 입력의 작은 교란이나 반올림 오차가 최종 결과에 치명적인 영향을 미치지 않도록 설계된다.
가령 다항식을 직접 계산하는 대신, Horner’s method와 같은 방식으로 전개하면, 다항식을 전개 형태로 계산할 때보다 훨씬 적은 곱셈과 덧셈을 사용하며, 그만큼 오차 발생 기회가 줄어든다. 또한, 어떤 문제에서는 입력값을 스케일링(scaling)하거나 미리 정규화(normalization)해 놓고 연산을 수행함으로써 오차 증폭을 방지하기도 한다.
오차 분류: 반올림 오차와 절단 오차
수치계산에서 “오차(error)”라 하면 보통 두 가지 범주로 나누어 볼 수 있다. 첫째는 “반올림 오차(round-off error)”이며, 둘째는 “절단 오차(truncation error, 혹은 이산화 오차)”이다. 반올림 오차는 컴퓨터 부동소수점 표현의 한계로 인해 발생하며, 같은 알고리즘이라도 정밀도와 연산 순서, 알고리즘 구조에 따라 달라진다. 반면 절단 오차는 이론적으로 무한 번 반복해야 정확히 수렴하는 연속적 계산(예: 테일러 급수, 적분 근사 등)을 유한 횟수로 끊을 때 필연적으로 발생하는 근사 오차를 말한다.
수학적으로 이상적인 연산을 전제했을 때 절단 오차가 0이라고 가정할 수도 있지만, 실제 컴퓨터 연산에는 반올림 오차가 뒤따르므로 최종 결과는 두 종류의 오차가 모두 결합된 형태를 띠게 된다. 예를 들어, 테일러 급수로 함수를 근사하는 과정에서 항을 적절히 많이 쌓으면 절단 오차가 줄어들지만, 대신 항이 많아져서 연산 횟수가 증가하면 반올림 오차가 누적될 수 있다. 결국 최적의 근사 방식을 찾기 위해서는 둘 중 어느 한 쪽 오차만이 아니라 양쪽 오차의 균형을 고려해야 한다.
조건수(condition number)와 문제 자체의 민감성
어떤 계산 문제가 주어졌을 때, 해당 문제 자체가 입력값의 작은 변화에 얼마나 민감하게 반응하는지를 나타내는 척도가 조건수(condition number)다. 예를 들어 $f(\mathbf{x})$라는 함수를 통해 결과값 $\mathbf{y}$를 구할 때, $\mathbf{x}$가 조금만 바뀌어도 $\mathbf{y}$가 크게 변한다면 그 문제는 (함수 $f$가) 민감하거나 “ill-conditioned”하다고 한다.
간단한 예로, $x$에 대해 $1/x$를 계산하는 문제를 생각하면 $x$가 매우 작은 경우, $x$에 조금만 오차가 생겨도 $1/x$ 값이 크게 요동친다. 이러한 문제는 본질적으로 ill-conditioned하므로, 아무리 정밀도를 높여도 입력값에 묻어 있는 아주 작은 상대오차가 엄청나게 증폭될 가능성이 높다. 반대로 well-conditioned한 문제는 입력값의 작은 변동이 결과값에도 작은 변동을 일으켜, 반올림 오차가 누적되더라도 상대적으로 큰 문제 없이 유효숫자를 유지하기 쉽다.
조건수를 간단히 나타내면 다음과 같은 정의가 많이 쓰인다.
실제로는 벡터나 행렬 문제에서 노름(norm)을 활용하여
과 같은 형태가 널리 사용된다. 여기서 $A$는 어떤 선형 변환을 나타내는 행렬이며, 이 값이 매우 크다면 ill-conditioned, 작다면 well-conditioned라 할 수 있다.
전방오차와 후방오차
수치해석적 문제를 다룰 때 자주 등장하는 개념이 전방오차(forward error)와 후방오차(backward error)다. 전방오차는 우리가 최종적으로 구하고자 하는 해(결과값)가 참값과 얼마나 차이가 나는지 측정한다. 예를 들어, 선형시스템 $\mathbf{A}\mathbf{x} = \mathbf{b}$에서 실제 해 $\mathbf{x}^*$와 근사 해 $\hat{\mathbf{x}}$의 차이에 주목하는 것이 전방오차다. 즉,
이 전방오차가 된다.
반면, 후방오차는 “계산 결과 $\hat{\mathbf{x}}$가 정말로 만족하는 방정식(혹은 문제 설정)이 본래의 문제에서 얼마나 멀어졌는가”를 기준으로 정의한다. 즉,
가 아니라 실제로는
와 같은 형태로 소폭 변경된 문제를 정확히 풀었다고 생각할 수 있다면, 이때 $|\Delta \mathbf{A}|$, $|\Delta \mathbf{b}|$가 작으면 후방안정성(backward stability)이 좋다고 평가한다. 수치알고리즘에서 “후방안정적”이라는 말은, 결과 해가 어떤 인접한 문제를 완벽하게 풀었다고 해석할 수 있을 정도로, 알고리즘 자체가 입력값을 크게 망가뜨리지 않고 해결했다는 의미다.
안정적인 알고리즘과 불안정한 알고리즘
알고리즘이 수치적으로 안정(numerically stable)하다는 것은, 작은 입력 오차나 반올림 오차가 크게 증폭되지 않도록 계산 절차를 설계했다는 뜻이다. 가우스 소거법(Gaussian elimination)도 특별한 회전(pivot)을 하지 않고 단순 적용하면 특정 유형의 행렬에 대해서는 매우 큰 오차 증폭이 생긴다. 반면 적절한 피벗팅(partial pivoting, complete pivoting)을 수행하면 훨씬 나은 안정성을 확보할 수 있다.
단순히 계산 과정에서 수행되는 곱셈·덧셈 횟수가 적다는 이유만으로 “좋은” 알고리즘이라 단정할 수는 없다. 어떤 방식은 연산 횟수가 더 많아도, 각 단계에서 반올림 오차가 최소화되도록(혹은 후방오차 관점에서 안정성이 보장되도록) 설계되어 최종 결과가 더욱 정확하게 나올 수 있다. 이것이 “속도”만을 추구하던 초기 시절과 달리, 현대 수치해석에서는 “안정성”이 중요한 평가 기준이 된 배경이다.
계산 순서와 스케일링
큰 수와 작은 수가 섞여서 cancellation 위험이 높다면, 알고리즘 단계에서 스케일링(scaling)을 적용하거나 연산 순서를 재배치해서 오차를 줄이는 전략을 쓴다. 예를 들어 어떤 다항식 $P(x)$를 계산할 때 항들의 크기가 급격히 달라지지 않도록 $x$를 적절히 축소하여 계산한다든지, 혹은 Horner’s method를 사용하여 곱셈·덧셈 횟수를 줄이고 반올림 기회를 최소화한다.
선형연립방정식에서도 계수행렬 $\mathbf{A}$나 벡터 $\mathbf{b}$를 행/열 단위로 적절히 스케일링하여, 연산 과정에서 발생하는 큰 값과 작은 값의 극단적 대비를 줄일 수 있다. 이는 결국 반올림 오차를 직접 줄이거나, ill-conditioned 문제를 조금이라도 덜 나쁘게 만드는 효과를 기대할 수 있다.
수치적 반복 개선(Iterative Refinement)
반올림 오차와 취소 오차가 심각하게 누적되는 문제를 풀 때, 일부 알고리즘은 반복 개선(Iterative Refinement) 기법을 적용하여 해를 더욱 정교하게 다듬는다. 예를 들어 선형시스템 $\mathbf{A}\mathbf{x} = \mathbf{b}$를 근사 해 $\hat{\mathbf{x}}$로 구한 뒤, 해당 해가 얼마나 오차를 포함하고 있는지 추정하고, 그 오차를 다시 보정하는 식으로 여러 번 반복한다.
일반적인 방식은 다음과 같다. 먼저 적절한 방법(가우스 소거법 등)으로 $\hat{\mathbf{x}}$를 구한다. 그 다음
라는 잔차(residual)를 계산한다. 이 $\mathbf{r}$는 실제로는 반올림 오차가 포함된 값이지만, 가능한 한 정확하게 구할수록 좋다(잔차 계산에서도 반올림 오차가 생길 수 있으므로 확장 정밀도를 쓰면 더 유리하다). 그리고 $\mathbf{A}\delta \mathbf{x} = \mathbf{r}$를 다시 풀어 $\delta \mathbf{x}$를 구하고,
로 갱신한다. 이렇게 하면 $\hat{\mathbf{x}}$가 원래 문제에 조금 더 근접하도록 개선된다. 단, 문제 자체가 심하게 ill-conditioned하면 실제로는 잔차 계산이 오히려 잡음만 증폭할 수도 있으므로, 반복 개선이 무조건 성공적이진 않다.
감쇠와 수렴
반복 개선 과정에서, 매 단계마다 오차가 줄어드는 현상을 “감쇠(attentuation)”라고 부른다. 반면, ill-conditioned 문제가 심각할 경우에는 오히려 한 단계 보정으로 인해 잘못된 방향으로 크게 변할 수도 있다. 실제 구현 시, 일정 횟수 이상 감쇠가 관측되지 않으면 더 이상 개선을 시도하지 않고 중단하거나, 다른 기법을 모색한다.
비슷한 맥락으로, 뉴턴 방법(Newton’s method)을 사용해 비선형 방정식을 풀 때도 중간 해를 계속 갱신하며 수렴을 시도하므로, 각 단계에서의 반올림 오차 누적과 민감도를 고려해야 한다. 민감한 영역에서 조금만 부정확한 도함수를 사용하거나, 해가 매우 큰 값·작은 값을 오가는 경우 반올림 오차가 급격히 커질 수 있다.
고계차분(High-Order Differences)와 안정성
수치 미분이나 수치 적분 과정에서, 혹은 다항식 근사를 위해 차분(difference)을 사용할 때, 상위 차분이 증가할수록 데이터 간의 작은 오차들이 증폭되어 결과가 불안정해질 수 있다. 예컨대 $f(x)$의 $n$차 차분을 구하는 공식은 여러 개의 $f(x + \Delta x)$, $f(x + 2\Delta x)$ 등을 조합하게 되는데, 측정값(또는 이전 계산값)들이 조금씩 틀려 있다면, 이 오차가 고차차분 단계에서 크게 늘어날 수 있다.
다항회귀(polynomial fitting)나 스플라인(spline) 근사 등으로 부드러운 곡선을 얻을 때도, 고차항을 넣을수록 피팅 에러가 줄어들기보다는 반올림이나 실측 오차가 증폭되어 곡선이 이상하게 출렁이는 현상이 생길 수 있다. 이를 방지하기 위해서는 오버피팅을 피하고, 필요 이상으로 차수가 높은 근사를 쓰지 않는 등 적절한 방안을 고려해야 한다.
혼합 정밀도(Mixed Precision) 기법
최근에는 CPU와 GPU 모두에서 혼합 정밀도(Mixed Precision) 연산을 활용하는 방법이 활발히 연구·개발되고 있다. 즉, 큰 부피의 연산은 빠른 속도가 장점인 단정밀도(float32)나 반정밀도(float16)로 처리하되, 결정적인 단계(예: 반올림이 민감한 합산, 잔차 계산 등)는 배정밀도(float64)로 보정하는 식이다. 이를 통해 전체 연산 시간은 단정밀도 수준으로 빠르게 가져가면서도, 결과의 정확도는 어느 정도 유지할 수 있다는 이점을 갖는다.
혼합 정밀도의 구현 방식은 하드웨어 구조 및 소프트웨어 라이브러리에 따라 다양하다. 예컨대 딥러닝에서 GPU가 반정밀도 연산을 매우 빠르게 수행할 수 있도록 설계되어 있으나, 여전히 정확도가 필요한 파트에는 더 높은 정밀도로 연산하는 식이다. 수치선형대수에서도 유사하게, 큰 규모 행렬 연산의 대부분은 단정밀도로 수행하고, 최종 반복 개선 단계나 민감한 계산만 배정밀도로 처리하는 알고리즘이 제안되고 있다.
디지털 연산 환경과 “재현성(Reproducibility)”
부동소수점 연산 결과는 실행 환경이나 컴파일러, 혹은 하드웨어의 병렬 스케줄링 방식에 따라 약간씩 달라질 수 있다. 예컨대 멀티코어 환경에서 덧셈 순서가 바뀌면, 반올림 위치가 다르게 적용되어 결과가 달라질 수도 있다. 이런 문제는 과학 계산이나 금융계산 등에서 같은 코드를 다른 컴퓨터에서 돌렸을 때 결과를 “재현”하고 싶은 경우 불편을 야기한다.
몇몇 라이브러리나 언어는 덧셈 순서를 고정하거나 확장 정밀도를 강제해서 환경 차이에 따른 오차를 줄여주기도 하지만, 기본적으로는 부동소수점 연산이 비결정성(nondeterministic)일 수 있음을 인지해야 한다. “재현성”을 엄격히 요구하는 프로젝트에서는, 지정된 순서대로 연산을 수행하도록 하는 옵션이나, 아예 임의정밀도(arbitrary precision)를 사용하는 방식으로 해결책을 찾는다.
부동소수점 오류 디버깅
수치 프로그램에서 갑작스러운 “NaN(Not a Number)” 값이 나타나거나, 오버플로나 언더플로가 발생하는 경우, 이를 디버깅하기 위해서는 해당 연산이 일어나는 지점에서 변수가 어떤 범위를 갖는지를 면밀히 추적해야 한다. 특히 대규모 과학 계산 코드를 짤 때는 중간 결과의 크기를 예측해 보고, 만약 수의 범위가 너무 극단적으로 크거나 작은 방향으로 치우쳐 있다면 스케일링 혹은 안정화 기법을 추가로 적용해 본다.
일부 언어나 라이브러리에는 “에러 감지 모드”를 지원하여, $\infty$, NaN이 발생하는 순간 경고를 띄우거나 예외 처리를 수행하게끔 설정할 수 있다. 이를 통해 어디서 문제가 시작되는지를 추적하고, 값의 범위를 적절히 조정할 수 있다.
새로운 수 표현 방식: Posit과 Unum
전통적인 IEEE 754 부동소수점 방식 이외에도, 정밀도와 표현 범위를 보다 효율적으로 다루려는 시도로서 Posit 혹은 Unum 등의 대안적 수 표현 형식이 제안되고 있다. 이들은 기존 부동소수점의 “고정된 지수부와 가수부 비트 구조”로부터 벗어나, 가변 길이의 지수부를 활용하는 등 새로운 접근법으로 수치적 안정성과 표현력을 높이려 한다.
특히 Posit은 “regime”라 불리는 가변 길이 지수부 영역과 “exponent” 및 “fraction” 영역을 분할하여, 큰 수나 작은 수를 표현할 때 가수부를 좀 더 효율적으로 쓸 수 있도록 설계되었다. 반올림 규칙이나, 0 근방에서의 정밀도 유지 등에 있어 기존 방식과는 다른 특성을 보여 주며, 이론적인 논문들에서 “더 나은 분해능(해상도)을 제공한다”고 주장하기도 한다.
한편 John Gustafson의 Unum 시리즈는 기존 부동소수점 표현에서는 약간의 반올림 모호성이 있었던 구간에 대한 “열린 구간” 표현, 혹은 정확한 경계점 정보를 포함하는 형태를 제안한다. 이를 통해 $[a, b]$ 범위 전체를 공집합 없이 표현하거나, 논리적으로 완전한 형태의 실수 연산을 구현할 수 있다는 게 특징이다. 그러나 이러한 새로운 형식들이 실제 상용 하드웨어에서 폭넓게 지원되고 있지는 않으며, 연구 단계거나 특수 목적 응용에 시범 적용되는 정도다.
Bfloat16과 반정밀도
딥러닝과 같은 대규모 연산에서 반정밀도(16비트) 표현이 주목받으면서, 구글 TPU에서 도입된 Bfloat16 형식도 자주 언급된다. Bfloat16은 이름 그대로 16비트라는 짧은 길이를 가지나, 일반적인 IEEE 754 half precision과 달리 지수부를 8비트나 확보하고 가수부를 줄인 독특한 구조다. 이를 통해 값의 범위를 넓게 커버하면서도, 필요한 최소한의 정밀도를 유지한다. 큰 스케일의 행렬 연산에서 고정밀도를 전부 유지할 필요가 없는 상황에는 오히려 계산 효율이 높아진다는 장점이 있다.
이런 혼합 정밀도 기법 및 반정밀도 형식은 딥러닝 분야에서 눈에 띄게 활용되어 왔으나, 점차 과학 계산이나 금융, 물리 시뮬레이션 등에서도 응용 가능성을 모색하는 추세다. 자연스럽게 반정밀도 연산의 수치적 안정성이 언제나 보장되는 것은 아니므로, 민감한 구간이나 중요한 계산 단계는 더 높은 정밀도(예: 배정밀도)로 처리하는 식의 혼합전략이 필수적으로 뒤따른다.
정리
결국, “유효숫자와 반올림 오차”라는 관점에서 볼 때, 어떤 수 표현 방식을 채택하든지 간에 근본적인 한계나 특성은 존재하기 마련이다. 다만 그 한계를 어떻게 배분하고, 실제 계산 흐름에서 오차를 최소화하는 전략을 어떻게 마련하느냐가 수치해석적 과제의 핵심이다. 전통적인 IEEE 754 배정밀도에서 벗어나, Posit·Unum·Bfloat16 같은 새로운 방식을 모색하는 움직임도, 그러한 고민의 연장선상에 있다.
Last updated