카테고리 없음

부동 소수 오차 : Machine epsilon

개발자임정혁 2026. 8. 31. 08:31

소수 오차와 머신 엡실론: 컴퓨터는 왜 0.1 + 0.2를 정확히 계산하지 못할까?

사람이 계산하면 다음 결과는 분명합니다.

0.1 + 0.2 = 0.3

하지만 파이썬에서는 조금 다른 결과가 나옵니다.

print(0.1 + 0.2)
0.30000000000000004

컴퓨터가 덧셈을 잘못한 것은 아닙니다. 컴퓨터가 제한된 저장 공간에 소수를 표현하면서 가장 가까운 값으로 반올림했기 때문입니다.

10진수 소수 입력
-> 2진수로 변환
-> 저장 가능한 가장 가까운 값 선택
-> 작은 오차 발생

이 글에서는 소수 오차가 생기는 원리부터 머신 엡실론, IEEE 754, ULP, 오차가 커지는 계산과 해결 방법까지 알아보겠습니다.


1. 정확한 값과 근삿값

수학에서는 정확한 값과 컴퓨터가 저장한 값을 구분할 수 있습니다.

정확한 값 -> x
컴퓨터가 저장한 근삿값 -> x_hat

예를 들어 정확한 값이 0.1이라도 컴퓨터에는 0.1과 아주 가까운 값이 저장될 수 있습니다.

x = 0.1
x_hat ≈ 0.1

정확한 값과 근삿값의 차이를 오차라고 합니다.

절대 오차

절대 오차는 두 값의 차이를 크기로 나타낸 것입니다.

절대 오차 = |근삿값 - 정확한 값|

정확한 값이 100이고 계산 결과가 99.9라면 다음과 같습니다.

절대 오차
= |99.9 - 100|
= 0.1

상대 오차

같은 절대 오차라도 숫자의 크기에 따라 의미가 달라질 수 있습니다.

1에서 0.1의 오차는 크지만, 1,000,000에서 0.1의 오차는 매우 작습니다.

상대 오차는 정확한 값의 크기에 비해 오차가 얼마나 큰지를 나타냅니다.

상대 오차
= 절대 오차 / |정확한 값|

정확한 값이 100이고 근삿값이 99.9라면 다음과 같습니다.

상대 오차
= |99.9 - 100| / 100
= 0.1 / 100
= 0.001

백분율로 표현하면 0.1%입니다.


2. 컴퓨터는 소수를 어떻게 저장할까?

사람은 주로 10진법을 사용합니다.

123.45
= 1 × 10^2
+ 2 × 10^1
+ 3 × 10^0
+ 4 × 10^-1
+ 5 × 10^-2

반면 컴퓨터는 대부분 0과 1로 이루어진 2진법을 사용합니다.

예를 들어 2진수 101.01은 다음 값을 의미합니다.

101.01(2)
= 1 × 2^2
+ 0 × 2^1
+ 1 × 2^0
+ 0 × 2^-1
+ 1 × 2^-2

= 4 + 1 + 1/4
= 5.25

다음과 같은 소수는 2진수로 정확하게 표현할 수 있습니다.

0.5   = 1/2 = 2^-1
0.25  = 1/4 = 2^-2
0.125 = 1/8 = 2^-3

이 숫자들은 분모가 모두 2의 거듭제곱이라는 공통점이 있습니다.

분모가 2^n인 소수
-> 유한한 2진수로 표현 가능

3. 0.1을 2진수로 바꾸기

10진수 소수를 2진수로 바꾸려면 소수 부분에 계속 2를 곱합니다. 결과의 정수 부분을 차례대로 기록하면 됩니다.

0.1 × 2 -> 0.2 -> 정수 부분 0
0.2 × 2 -> 0.4 -> 정수 부분 0
0.4 × 2 -> 0.8 -> 정수 부분 0
0.8 × 2 -> 1.6 -> 정수 부분 1
0.6 × 2 -> 1.2 -> 정수 부분 1
0.2 × 2 -> 0.4 -> 다시 반복

따라서 10진수 0.1은 2진수에서 다음과 같이 무한히 반복됩니다.

0.1(10)
= 0.000110011001100110011... (2)

이 값을 무한등비급수로 표현하면 다음과 같습니다.

0.0001100110011... (2)

= (2^-4 + 2^-5)
+ (2^-8 + 2^-9)
+ (2^-12 + 2^-13)
+ ...

첫 번째 묶음은 다음과 같습니다.

2^-4 + 2^-5
= 1/16 + 1/32
= 3/32

같은 형태가 네 자리마다 반복되므로 공비는 1/16입니다.

전체 합
= (3/32) × (1 + 1/16 + 1/16^2 + ...)

무한등비급수의 합을 이용하면 다음과 같습니다.

전체 합
= (3/32) × 1/(1 - 1/16)
= (3/32) × (16/15)
= 1/10

정확한 값은 1/10이지만 2진수 표현은 끝나지 않습니다. 컴퓨터의 저장 공간은 유한하므로 일정한 위치에서 반올림해야 합니다.

무한히 반복되는 2진수
-> 저장할 수 있는 비트 수만 남김
-> 나머지 부분 반올림
-> 표현 오차 발생

4. IEEE 754 부동소수점 방식

많은 컴퓨터와 프로그래밍 언어는 소수를 저장할 때 IEEE 754 부동소수점 표준을 사용합니다.

파이썬의 float는 일반적인 환경에서 64비트 배정밀도 부동소수점인 binary64를 사용합니다.

64비트는 다음과 같이 나뉩니다.

부분 비트 수 역할
부호 1비트 양수 또는 음수
지수 11비트 숫자의 크기
가수 52비트 숫자의 유효한 부분

일반적인 부동소수점 수는 다음 형태로 표현됩니다.

값
= (-1)^s
× (1.b1b2...b52)(2)
× 2^(e-1023)

각 기호의 의미는 다음과 같습니다.

s -> 부호 비트
b1부터 b52 -> 가수 비트
e -> 저장된 지수
1023 -> 지수 바이어스

부동소수점은 밑이 2인 과학적 표기법이라고 생각할 수 있습니다.

부호
-> 유효 숫자
-> 2의 지수

가수에는 52비트가 저장됩니다. 하지만 정규화된 2진수는 맨 앞자리가 항상 1이므로 이 1을 생략할 수 있습니다.

따라서 실제 정밀도는 다음과 같습니다.

저장된 가수 -> 52비트
생략된 맨 앞의 1 -> 1비트
실제 유효 정밀도 -> 53비트

5. 컴퓨터가 실제로 저장한 0.1

파이썬의 float가 저장한 값을 정확한 분수로 확인할 수 있습니다.

number = 0.1

print(number.as_integer_ratio())
(3602879701896397, 36028797018963968)

따라서 컴퓨터에 저장된 0.1은 정확히 다음 분수입니다.

3602879701896397
-----------------
36028797018963968

이 값은 정확한 1/10보다 아주 조금 큽니다.

컴퓨터의 0.1 - 정확한 1/10

= 1 / 180143985094819840
≈ 5.55 × 10^-18

16진수 기반의 부동소수점 표현도 확인할 수 있습니다.

print(0.1.hex())
print(0.2.hex())
print(0.3.hex())
print((0.1 + 0.2).hex())
0x1.999999999999ap-4
0x1.999999999999ap-3
0x1.3333333333333p-2
0x1.3333333333334p-2

0.3과 0.1 + 0.2는 마지막 비트에서 차이가 납니다.

0.3       -> 마지막 부분이 3333
0.1 + 0.2 -> 마지막 부분이 3334

따라서 두 값은 출력했을 때 비슷해 보여도 컴퓨터 내부에서는 서로 다른 값입니다.


6. 머신 엡실론이란?

머신 엡실론 또는 기계 엡실론은 일반적으로 1.0과 그다음 표현 가능한 부동소수점 수의 차이를 의미합니다.

1 + 머신 엡실론 > 1

반면 머신 엡실론보다 충분히 작은 값을 더하면 컴퓨터가 차이를 표현하지 못할 수 있습니다.

1 + 머신 엡실론/2 = 1

배정밀도 부동소수점에서 1.0은 다음과 같이 표현됩니다.

1.000000000000000000000000... (2)

그다음 표현 가능한 숫자는 가수의 마지막 비트가 1인 값입니다.

1.000000000000000000000001... (2)

마지막 비트의 위치가 2^-52이므로 머신 엡실론은 다음과 같습니다.

머신 엡실론
= 2^-52
≈ 2.220446049250313 × 10^-16

파이썬에서는 다음 코드로 확인할 수 있습니다.

import sys

epsilon = sys.float_info.epsilon

print(epsilon)
print(1.0 + epsilon == 1.0)
print(1.0 + epsilon / 2 == 1.0)
2.220446049250313e-16
False
True

계산 결과의 의미는 다음과 같습니다.

1 + epsilon
-> 1과 구별 가능

1 + epsilon/2
-> 너무 작은 차이
-> 1과 같은 값으로 반올림

7. 머신 엡실론 직접 구하기

반복문을 이용해 머신 엡실론을 직접 구할 수도 있습니다.

epsilon = 1.0

while 1.0 + epsilon / 2 != 1.0:
    epsilon /= 2

print(epsilon)
2.220446049250313e-16

코드는 다음 과정을 반복합니다.

epsilon을 1로 시작
-> epsilon을 2로 나누기
-> 1에 더하기
-> 컴퓨터가 차이를 구별하는지 확인
-> 차이를 구별하지 못하면 반복 종료

8. 머신 엡실론과 단위 반올림 오차

머신 엡실론과 비슷한 개념으로 단위 반올림 오차가 있습니다.

가장 가까운 부동소수점 값으로 반올림하면 최대 반올림 오차는 두 숫자 사이 간격의 절반 정도입니다.

단위 반올림 오차
= 머신 엡실론 / 2
= 2^-53
≈ 1.1102230246251565 × 10^-16

정확한 실수 x를 컴퓨터가 반올림한 결과를 fl(x)라고 하면 다음처럼 표현할 수 있습니다.

fl(x) = x × (1 + delta)

여기서 delta는 반올림 때문에 생긴 상대 오차입니다.

정규화된 숫자 범위에서는 일반적으로 다음 관계가 성립합니다.

|delta| <= 2^-53

즉, 한 번의 정상적인 반올림에서 발생하는 상대 오차는 매우 작습니다.

다만 책이나 문서에 따라 머신 엡실론을 2^-52로 정의하기도 하고, 단위 반올림 오차인 2^-53으로 정의하기도 합니다.

머신 엡실론 정의 1 -> 1과 다음 숫자의 간격 -> 2^-52
머신 엡실론 정의 2 -> 최대 상대 반올림 오차 -> 2^-53

엡실론이라는 용어를 볼 때는 어떤 정의를 사용하는지 확인해야 합니다.


9. ULP란?

ULP는 Unit in the Last Place의 약자입니다. 특정 숫자 근처에서 표현 가능한 두 부동소수점 수 사이의 간격을 뜻합니다.

파이썬에서는 math.ulp()로 확인할 수 있습니다.

import math

print(math.ulp(1.0))
print(math.ulp(1000.0))
print(math.ulp(1_000_000_000_000.0))
2.220446049250313e-16
1.1368683772161603e-13
0.0001220703125

숫자가 커질수록 표현 가능한 수 사이의 간격도 커집니다.

숫자 x가 다음 구간에 있다고 해봅시다.

2^e <= x < 2^(e+1)

이 구간에서 배정밀도 부동소수점의 간격은 다음과 같습니다.

ULP(x) = 2^(e-52)
숫자가 커짐
-> 지수가 커짐
-> ULP가 커짐
-> 표현 가능한 숫자 사이의 간격도 커짐

머신 엡실론은 모든 숫자에 적용할 수 있는 고정된 절대 오차가 아닙니다. 머신 엡실론은 1.0 근처의 간격을 설명하는 값입니다.


10. 계산을 반복하면 오차가 쌓인다

컴퓨터가 a와 b에 연산을 수행한 결과는 다음처럼 나타낼 수 있습니다.

fl(a 연산 b)
= 정확한 계산 결과 × (1 + delta)

delta는 한 번의 계산에서 생긴 작은 상대 오차입니다.

|delta| <= 2^-53

한 번의 오차는 매우 작지만 계산이 반복되면 오차가 누적될 수 있습니다.

n번 정도의 연산에서 발생할 수 있는 누적 오차의 상한은 다음 형태로 나타낼 수 있습니다.

gamma_n
= (n × u) / (1 - n × u)

여기서 u는 단위 반올림 오차입니다.

u = 2^-53

n × u가 매우 작다면 다음과 같이 생각할 수 있습니다.

gamma_n ≈ n × u
한 번의 작은 오차
-> 많은 계산 반복
-> 오차 누적
-> 결과에 눈에 보이는 차이 발생

실제 오차가 항상 이 상한까지 커지는 것은 아닙니다. 하지만 계산 횟수가 많을수록 오차를 주의해야 한다는 사실을 보여줍니다.


11. 반복 덧셈의 오차

0.1을 열 번 더해 보겠습니다.

number = 0.0

for i in range(10):
    number += 0.1

print(number)

우리가 예상한 값은 1.0입니다.

0.1 × 10 = 1.0

실제 결과는 다음과 같습니다.

0.9999999999999999

각 덧셈에서 발생한 작은 오차가 누적된 것입니다.


12. 덧셈 순서에 따라 결과가 달라질 수 있다

실수의 덧셈은 결합법칙을 만족합니다.

(a + b) + c = a + (b + c)

하지만 부동소수점 계산에서는 반올림 때문에 결합법칙이 성립하지 않을 수 있습니다.

a = 1e16
b = -1e16
c = 1.0

print((a + b) + c)
print(a + (b + c))
1.0
0.0

첫 번째 계산은 다음과 같습니다.

1e16 + (-1e16) -> 0
0 + 1 -> 1

두 번째 계산에서는 -1e16 근처의 ULP가 1보다 큽니다.

-1e16 + 1
-> 작은 값 1의 영향이 사라짐
-> -1e16으로 반올림

1e16 + (-1e16)
-> 0

수학적으로 같은 식이라도 계산 순서에 따라 결과가 달라질 수 있습니다.


13. 큰 수와 작은 수를 더할 때 생기는 문제

아주 큰 수에 작은 수를 더하면 작은 수가 사라질 수 있습니다.

large = 1e16
small = 1.0

print(large + small)
print(large + small == large)
1e+16
True

1e16 근처에서 표현 가능한 숫자 사이의 간격이 1보다 크기 때문입니다.

큰 수 근처의 ULP > 더하려는 작은 수
-> 작은 수를 더함
-> 표현 가능한 가장 가까운 값으로 반올림
-> 작은 수의 영향이 사라짐

이 현상을 흡수 오차라고 부르기도 합니다.


14. 상쇄 오차

서로 거의 같은 두 수를 뺄 때 중요한 숫자들이 사라지는 현상을 상쇄 오차 또는 치명적 상쇄라고 합니다.

다음 식을 생각해 봅시다.

sqrt(x + 1) - sqrt(x)

x가 매우 크면 두 제곱근은 거의 같습니다. 비슷한 두 수를 빼면 유효 숫자가 사라질 수 있습니다.

import math

x = 1e16

result = math.sqrt(x + 1) - math.sqrt(x)

print(result)
0.0

수학적으로 이 값은 0이 아닙니다.

분모를 유리화하면 같은 식을 더 안정적으로 계산할 수 있습니다.

sqrt(x + 1) - sqrt(x)

= 1 / (sqrt(x + 1) + sqrt(x))

코드로 작성하면 다음과 같습니다.

import math

x = 1e16

unstable = (
    math.sqrt(x + 1)
    - math.sqrt(x)
)

stable = 1 / (
    math.sqrt(x + 1)
    + math.sqrt(x)
)

print(unstable)
print(stable)
0.0
5e-09

두 식은 수학적으로 같지만 두 번째 식이 부동소수점 계산에서는 더 안정적입니다.

비슷한 두 수 직접 빼기
-> 유효 숫자 손실

식을 변형해 뺄셈 피하기
-> 오차 감소

15. 많은 수를 정확하게 더하기

파이썬의 math.fsum()은 중간 계산에서 사라질 수 있는 작은 값들을 더 주의해서 처리합니다.

import math

numbers = [0.1] * 10

print(sum(numbers))
print(math.fsum(numbers))
0.9999999999999999
1.0

많은 부동소수점을 더해야 한다면 일반적인 sum()보다 math.fsum()이 더 정확할 수 있습니다.


16. 카한 합산 알고리즘

카한 합산 알고리즘은 덧셈 과정에서 사라진 작은 오차를 따로 저장했다가 다음 계산에 반영합니다.

def kahan_sum(numbers):
    total = 0.0
    compensation = 0.0

    for number in numbers:
        corrected = number - compensation
        new_total = total + corrected

        compensation = (
            new_total - total
        ) - corrected

        total = new_total

    return total

사용 예시는 다음과 같습니다.

numbers = [0.1] * 10

print(sum(numbers))
print(kahan_sum(numbers))

카한 합산의 계산 과정은 다음과 같습니다.

숫자 더하기
-> 반올림 때문에 사라진 부분 계산
-> 사라진 부분을 compensation에 저장
-> 다음 덧셈에서 보정

모든 오차를 없애는 것은 아니지만 단순 덧셈보다 누적 오차를 줄일 수 있습니다.


17. 소수는 ==로 비교하면 안 될까?

다음 코드를 살펴봅시다.

print(0.1 + 0.2 == 0.3)
False

부동소수점 값을 비교할 때 단순히 머신 엡실론만 사용하는 것도 모든 상황에서 안전하지 않습니다.

abs(a - b) < epsilon

숫자가 매우 크거나 0에 가까우면 고정된 엡실론이 적절하지 않을 수 있기 때문입니다.

파이썬에서는 math.isclose()를 사용할 수 있습니다.

import math

a = 0.1 + 0.2
b = 0.3

print(
    math.isclose(
        a,
        b,
        rel_tol=1e-9,
        abs_tol=1e-12
    )
)
True

math.isclose()는 다음 조건을 검사합니다.

|a - b|
<=
max(
    rel_tol × max(|a|, |b|),
    abs_tol
)

rel_tol은 숫자의 크기에 따른 상대 허용 오차입니다. abs_tol은 직접 지정하는 최소 절대 허용 오차입니다.

0 근처에서는 상대 오차만 사용하기 어려우므로 abs_tol이 중요합니다.

import math

print(
    math.isclose(
        0.0,
        1e-12,
        abs_tol=1e-11
    )
)
True

허용 오차는 머신 엡실론을 무조건 사용하는 것이 아니라, 값의 크기와 문제에서 요구하는 정확도를 기준으로 정해야 합니다.


18. 반올림 방식

정확한 값이 표현 가능한 두 숫자의 중간에 있다면 어느 쪽을 선택해야 할까요?

IEEE 754에서 일반적으로 사용하는 방식은 가장 가까운 값으로 반올림하고, 정확히 중간이면 마지막 자리가 짝수가 되는 값을 선택하는 것입니다.

이를 round to nearest, ties to even이라고 합니다.

10진수에서 소수점 첫째 자리까지 반올림한다고 생각하면 다음과 같습니다.

1.25 -> 1.2
1.35 -> 1.4

항상 올림하는 것이 아니라 마지막 숫자가 짝수가 되도록 선택합니다.

이 방법은 많은 숫자를 반복해서 반올림할 때 오차가 한 방향으로 계속 쌓이는 현상을 줄여줍니다.


19. 너무 크거나 작은 숫자

64비트 부동소수점이 표현할 수 있는 숫자의 범위도 제한되어 있습니다.

import sys

print(sys.float_info.max)
print(sys.float_info.min)

대표적인 값은 다음과 같습니다.

표현 가능한 최댓값
≈ 1.7976931348623157 × 10^308

가장 작은 양의 정규화 수
≈ 2.2250738585072014 × 10^-308

최댓값을 넘어가면 무한대를 나타내는 inf가 발생할 수 있습니다.

print(1e308 * 10)
inf

배정밀도에서 표현 가능한 가장 작은 양의 부동소수점 수는 다음과 같습니다.

2^-1074
≈ 4.94 × 10^-324

이보다 더 작은 양수는 0으로 반올림될 수 있습니다. 이를 언더플로라고 합니다.


20. 정확한 소수 계산이 필요할 때

Decimal 사용하기

돈처럼 정확한 10진수 계산이 중요한 경우에는 Decimal을 사용할 수 있습니다.

from decimal import Decimal

a = Decimal("0.1")
b = Decimal("0.2")

print(a + b)
print(a + b == Decimal("0.3"))
0.3
True

Decimal을 만들 때는 문자열을 사용하는 것이 중요합니다.

Decimal("0.1")  # 권장
Decimal(0.1)    # float의 오차가 들어올 수 있음

Decimal도 무한한 정밀도를 제공하는 것은 아닙니다. 1/3처럼 10진수에서도 무한히 반복되는 값은 설정된 정밀도에 따라 반올림됩니다.


Fraction 사용하기

분수를 정확하게 계산해야 한다면 Fraction을 사용할 수 있습니다.

from fractions import Fraction

a = Fraction(1, 10)
b = Fraction(2, 10)

print(a + b)
print(a + b == Fraction(3, 10))
3/10
True

Fraction은 유리수를 분자와 분모로 저장하므로 정확한 계산이 가능합니다.

다만 계산이 반복되면 분자와 분모가 매우 커질 수 있습니다.


정수 단위 사용하기

금액을 원이나 센트처럼 가장 작은 단위의 정수로 저장할 수도 있습니다.

price = 1000
tax = 100

total = price + tax

print(total)
1100
소수 금액으로 저장
-> 부동소수점 오차 가능

가장 작은 화폐 단위의 정수로 저장
-> 정확한 정수 계산

상황에 따른 해결 방법

상황 추천 방법
일반적인 과학·그래픽 계산 float
두 실수가 가까운지 비교 math.isclose()
많은 수를 합산 math.fsum()
화면 출력용 반올림 round() 또는 출력 형식
돈과 정확한 10진수 계산 Decimal
정확한 분수 계산 Fraction
화폐의 최소 단위가 정해진 경우 정수
큰 수와 작은 수가 섞인 계산 계산 순서와 수치 안정성 검토

정리

소수 오차는 컴퓨터의 실수가 아니라 유한한 비트로 무한한 실수를 표현하면서 생기는 자연스러운 결과입니다.

10진수 소수
-> 2진수로 변환
-> 유한한 비트에 저장
-> 가장 가까운 값으로 반올림
-> 표현 오차 발생

64비트 배정밀도 부동소수점의 머신 엡실론은 다음과 같습니다.

머신 엡실론
= 2^-52
≈ 2.220446049250313 × 10^-16

가장 가까운 값으로 반올림할 때의 단위 반올림 오차는 다음과 같습니다.

단위 반올림 오차
= 2^-53
≈ 1.1102230246251565 × 10^-16

머신 엡실론은 모든 계산에 그대로 적용할 수 있는 만능 허용 오차가 아닙니다.

숫자의 크기 확인
-> ULP 확인
-> 상대 오차와 절대 오차 고려
-> 문제에 맞는 허용 오차 선택

부동소수점 계산을 안전하게 다루려면 다음 원칙을 기억하면 좋습니다.

소수를 ==로 바로 비교하지 않기
-> math.isclose() 사용

많은 숫자를 더하기
-> math.fsum() 사용

비슷한 두 수를 빼기
-> 수치적으로 안정한 식으로 변형

정확한 10진수 계산
-> Decimal 사용

정확한 유리수 계산
-> Fraction 사용

컴퓨터는 모든 실수를 정확하게 저장할 수 없습니다. 중요한 것은 오차를 완전히 피하는 것이 아니라, 오차의 크기와 발생 원리를 이해하고 계산 목적에 맞게 관리하는 것입니다.