[Code] Study & Practice

[Python] 2진수의 소수점 연산 불일치와 연산 허용 오차 범위.

yssong01 2025. 10. 21. 00:22

Problem)

소수점이 0 인 실수들의 연산 결과가 True 이나, 소수점이 0 이 아닌(ex, 0.1234) 실수들의 경우 비록 연산은 참이어도 결과가 False로 출력된다. 아래 코드를 확인해보자.

print(2.0 - 1.0 == 1.0)
'''
True
'''
print(0.043 - 0.001 == 0.042)
'''
False
'''

 

A. 개념 정리 (간단히)

 

컴퓨터(Python 등)는 연산 과정을 2진수로 수행(계산)한다.

10진수를 정확히 2진수로 표현할 수 없는 경우가 대부분이다.

즉, 소수점을 포함한 실수(ex, 0.043, 0.001, 0.042)는 2진수로 정확하게 표현할 수 없다.

 

ex)

10을 2진수로 바꾸면, 10 (10진수) = 1010 (2진수) 가 되는데,​ 
0.1를 2진수로 바꾸면, 10진수 0.1 = 1/10 = 1/(2×5) 로, 분모 10에는 2로 나누어 떨어지지 않는 5가 포함되어 있다.

[key] 2진법은 분모가 2의 거듭제곱으로만 표현할 수 있다.

위에서 1/5은 무한히 반복되는 2진소수(binary fraction) 이다.
즉 1/5 ​= 0.0011 0011 0011 0011...(2진수), 0.0011 다음의 0011이 무한히 반복된다.

 

다시 1/5을 10진수로 환산하면, 
1/5 ​= 1/8 + 1/16 + 1/128 + 1/256 + ... = 0.2 에 수렴한다.

 

 

B. 코드 예제 및 출력 결과

 

아래와 같이,

소수점이 0인 실수 연산인 경우에는 결과 값이 기대 값과 일치하여 True가 출력되지만,

from decimal import Decimal, getcontext
getcontext().prec = 1 # 0보다 큰 원하는 자리수 설정

x = 2.0
y = 1.0
r = x - y
z = 1.0

print(f'x = {x}   -> {Decimal(x):.30f}')
print(f'y = {y}   -> {Decimal(y):.30f}')
print(f'r = x - y -> {Decimal(r):.30f}')
print(f'z = {z}   -> {Decimal(z):.30f}')

'''
x = 2.0   -> 2.000000000000000000000000000000
y = 1.0   -> 1.000000000000000000000000000000
r = x - y -> 1.000000000000000000000000000000
z = 1.0   -> 1.000000000000000000000000000000
'''

print(x - y == z) # 2.0 - 1.0 = 1.0 -> correct!
'''
True
'''

print(r == z) # 1.0 = 1.0 -> correct!
'''
True
'''

 

위와 반하여,

소수점 이하 값이 있는 실수 연산인 경우에는 결과 값이 기대 값과 불일치하여 False가 출력된다. 이는 각 값들의 입력 값과 실제 출력 값들이 미세하게 차이가 있기 때문이다!

(2진수로 계산된 소수점의 10진수 값은 무한수렴하는 값으로 결국 근사 값으로 표현될 수 밖에 없다.)

 

a = 0.043
b = 0.001
r = a - b
c = 0.042

print(f'a = {a} -> {Decimal(a):.30f}')
print(f'b = {b} -> {Decimal(b):.30f}')
print(f'r = a - b -> {Decimal(r):.30f}')
print(f'c = {c} -> {Decimal(c):.30f}')

'''
a = 0.043 -> 0.042999999999999996558308623662
b = 0.001 -> 0.001000000000000000020816681712
r = a - b -> 0.041999999999999995670130203962
c = 0.042 -> 0.042000000000000002609024107869
'''

print(a - b == c) # 0.04299... - 0.00100... = 0.04200... -> incorrect!
print(r == c) # 0.04199... = 0.04200... -> incorrect!

'''
False
False
'''

 

여기에서,

Python(Colab)의 허용 오차 값이 어느 정도 미세하게 차이가 있을 때 True가 아닌 False가 되는지를 테스트 해보면,

실제 차이 값(r = a - b)은 약 6.939e-18 이고 (결과 : False),

허용 오차 값은 1e-17 이상 일 때 True 임을, 1e-18 이하 이면 False 임을 확인할 수 있다.

a = 0.043
b = 0.001
r = a - b
c = 0.042

print(f"a = 0.043  = {a:.30f}")
print(f"b = 0.001  = {b:.30f}")
print(f"r = a - b  = {r:.30f}")
print(f"c = 0.042 -> {c:.30f}")
print(f"실제 차이 = {abs(r - c):.30e}")

# 1e-N에서 N = 1,2,...29,30까지, 즉 1e-1 ~ 1e-30까지 반복.
n = 30
for i in range(1, n+1):
    differr = 10 ** (-i) # 10^-i = 1e-i
    result = abs(r - c) < differr
    print(f"허용 오차 값 : 1e-{i}  →  {result}")

'''
a = 0.043  = 0.042999999999999996558308623662
b = 0.001  = 0.001000000000000000020816681712
r = a - b  = 0.041999999999999995670130203962
c = 0.042 -> 0.042000000000000002609024107869
실제 차이 = 6.938893903907228377647697925568e-18  <- ~ 6.939e-18으로 매우 작은 값!

허용 오차 값 : 1e-1   →  True
허용 오차 값 : 1e-2   →  True
허용 오차 값 : 1e-3   →  True
허용 오차 값 : 1e-4   →  True
허용 오차 값 : 1e-5   →  True
허용 오차 값 : 1e-6   →  True
허용 오차 값 : 1e-7   →  True
허용 오차 값 : 1e-8   →  True
허용 오차 값 : 1e-9   →  True
허용 오차 값 : 1e-10  →  True
허용 오차 값 : 1e-11  →  True
허용 오차 값 : 1e-12  →  True
허용 오차 값 : 1e-13  →  True
허용 오차 값 : 1e-14  →  True
허용 오차 값 : 1e-15  →  True
허용 오차 값 : 1e-16  →  True
허용 오차 값 : 1e-17  →  True
허용 오차 값 : 1e-18  →  False  <= Oh! my sence! 
허용 오차 값 : 1e-19  →  False
허용 오차 값 : 1e-20  →  False
허용 오차 값 : 1e-21  →  False
허용 오차 값 : 1e-22  →  False
허용 오차 값 : 1e-23  →  False
허용 오차 값 : 1e-24  →  False
허용 오차 값 : 1e-25  →  False
허용 오차 값 : 1e-26  →  False
허용 오차 값 : 1e-27  →  False
허용 오차 값 : 1e-28  →  False
허용 오차 값 : 1e-29  →  False
허용 오차 값 : 1e-30  →  False
'''

 

C. 핵심 요약 및 대응 방안

 

Python에서 0.1과 같은 소수점 이하는 무한소수를 근사해서 2진 부동소수점*으로 저장하고, 그 허용 오차는 약 1.04e-17 정도로 매우 작다고 한다.

*IEEE 754 표준 (double precision, 64-bit) : 컴퓨터가 실수를 2진수로 표현하고 연산하는 방법을 정의한 국제 표준. [참조. ChatGPT5]

 

출력 결과를 간단히 표로 비교해보면 아래와 같다.

테스트한 허용
오차 값
알려진 국제 표준 허용
오차 값*
두 값의 실제 차이
(r - c)
출력 결과
1e-16 이상     True
1e-17 ~ 1.04e-17   True , OK! :)
    ~ 6.94e-18 False
1e-18 이하     False

 

출력 결과 False를 True로 출력되게 하려면, 허용 오차 값이 그 표준 허용 오차 값(~1e-17) 보다 크도록 강제하면 될 것 이다.

Python 3.5 이상 버전에서 쉽게 다룰 수 있는 가장 권장되는 함수, math.isclose(), 가 유용하다.

 

이 함수는 상대 오차 허용치가 1e-9 보다 작으면, 소수점 연산도 근사하여 True로 출력한다.

import math

a = 0.043
b = 0.001
r = a - b
c = 0.042

print(math.isclose(r, c))
print(f'{abs(r-c)/c}') # ~ 1.652e-16 < 1e-9 (기본 허용 오차)

'''
True
1.6521175961683875e-16
'''

 

함수 math.isclose() 의 원리는 다음과 같다.

 

파이썬 공식 문서, Python 3.12, 기준에 따르면,

 

math.isclose(a, b, rel_tol = 1e-9, abs_tol = 0.0)

 

두 값의 차이 : |a − b| ≤ max(rel_tol × max(|a|, |b|), abs_tol)


> rel_tol: 상대 오차 허용치 (기본 1e-9)
> abs_tol: 절대 오차 허용치 (값이 작을 때 보정용)

즉 두 값의 차이가  |a − b| ≤ rel_tol × max(|a|, |b|) 이면(1e-9 보다 작으면),

 

True로 출력된다.

 

 

추가로,

b를 미세하게 바꾸어서 오차에 대한 결과를 비교하여 확인해보자.

 

b = 0.001 000 000 1 일 때는 오차가 허용 오차보다 크므로 (2.38e-9 > 1e-9) False가 되고,

b = 0.001 000 000 01 일 때는 오차가 허용 오차보다 작으므로 (2.38e-10 < 1e-9) True가 된다.

a = 0.043
b = 0.0010000001
r = a - b
c = 0.042

print(math.isclose(r, c))
print(f'{abs(r-c)/c}') # abs() : 절대값

'''
False
2.380952577953264e-09
'''


a = 0.043
b = 0.00100000001
r = a - b
c = 0.042

print(math.isclose(r, c))
print(f'{abs(r-c)/c}')

'''
True
2.3809542300708603e-10
'''

 

보통 오차가 1e-9 = 0.000 000 001 이면(10억분의 1) 대부분의 소수점 연산 경우에 충분할 것이다. 다만, 소수점 자리의 값에 대한 일치 여부(True or False)는 반드시 소수점의 근사 값의 적용 여부에 따라 달라짐을 명심하자.

 

[주의] 내용에 오류가 있을 수 있습니다.