본문 바로가기

2. 정보영재교육 수업 자료

[444] 부동소수점의 오차 이야기

1. 부동소수점(浮動小數点)

부동소수점 방식은 고정소수점 방식과 함께 컴퓨터로 소수(小數)를 나타내기 위한 방법입니다.

부동소수점의 부는 不(아닐 부)가 아니라 浮(뜰 부)입니다.

고정되어 있지 않고 움직인다는 뜻입니다.

부동자세에서 부동은 움직이지 않는다는 뜻인데, 부동소수점의 부동은 움직인다는 뜻입니다.(연패처럼...)

 

십진수 \(14.2\)를 \(1.42 \times 10^1\) 처럼 나타내는 방식입니다.

같은 방식으로 이진수 \(101.01\)를 부동소수점 방식으로 나타내면 \(1.0101 \times 2^2\)  입니다.

https://ko.wikipedia.org/wiki/부동소수점

 

부동소수점 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 초기의 전기기계식 프로그래밍 가능한 컴퓨터 Z3에는 부동소수점 산술 기능이 포함되었다. (뮌헨의 국립 독일 박물관) 부동소수점(浮動小數點, floating point) 또

ko.wikipedia.org

2. 이진수의 소수 표현 방법

십진수를 이진수로 바꿀 때 정수 부분은 2로 나누어 가며 표시합니다.

반대로 소수 부분은 곱해가며 표시합니다.

 

이제 컴퓨터는 \(1.???? \times 2^n\) 형태로 소수를 관리합니다.

위 식에서 2의 제곱수인 \(n\)은 지수부에, 소수점 이하인 \(????\)의 수는 가수부에 저장하게 됩니다.

무한 소수인 경우 가수부에 끝이 없이 저장될 수 없기 때문에 저장 공간에 맞춰 반올림을 하게 됩니다.

게다가 소수끼리 연산을 할 경우, 연산 결과가 저장 공간을 벗어나면 또 다시 반올림을 하게되므로 오차가 발생하게 됩니다.

3. 오차가 발생하는 소수 연산

가. 0.1 + 0.2 ≠ 0.3

0.1 + 0.2 ≠ 0.3 은 가장 유명한 연산입니다.

0.1과 0.2는 이진수로 나타내면 둘다 무한 소수이고, 그 결과 0.3과 다른 값이 출력됩니다.

print(0.1 + 0.2)
# 0.30000000000000004

 

이외에도 소수 첫째 자리에서 오차가 생기는 덧셈들을 찾아보면 다음과 같습니다.

for i in range(1, 10):
    for j in range(i, 10):
        a = f"0.{i}"
        b = f"0.{j}"
        a = float(a)
        b = float(b)
        if (i+j)/10 != a+b:
            print(f"{a} + {b} ≠ {a+b}")
            
# 0.1 + 0.2 ≠ 0.30000000000000004
# 0.1 + 0.7 ≠ 0.7999999999999999
# 0.2 + 0.4 ≠ 0.6000000000000001
# 0.2 + 0.7 ≠ 0.8999999999999999
# 0.3 + 0.6 ≠ 0.8999999999999999
# 0.4 + 0.8 ≠ 1.2000000000000002
# 0.6 + 0.7 ≠ 1.2999999999999998
# 0.8 + 0.9 ≠ 1.7000000000000002

어떤 오차는 조금 작고, 어떤 오차는 조금 크게 나타나는 것을 볼 수 있습니다.

나. 0.7 - 0.3 ≠ 0.4

print(0.7 - 0.3)
# 0.39999999999999997

뺄셈에서도 덧셈처럼 오차가 발생합니다.

 

뺄셈에서도 이외에 어떤 오차가 있는지 살펴보면 다음과 같습니다.

for i in range(1, 10):
    for j in range(1, i):
        a = f"0.{i}"
        b = f"0.{j}"
        a = float(a)
        b = float(b)
        if (i-j)/10 != a-b:
            print(f"{a} - {b} ≠ {a-b}")

# 0.3 - 0.1 ≠ 0.19999999999999998
# 0.3 - 0.2 ≠ 0.09999999999999998
# 0.4 - 0.1 ≠ 0.30000000000000004
# 0.4 - 0.3 ≠ 0.10000000000000003
# 0.5 - 0.4 ≠ 0.09999999999999998
# 0.6 - 0.2 ≠ 0.39999999999999997
# 0.6 - 0.4 ≠ 0.19999999999999996
# 0.6 - 0.5 ≠ 0.09999999999999998
# 0.7 - 0.2 ≠ 0.49999999999999994
# 0.7 - 0.3 ≠ 0.39999999999999997
# 0.7 - 0.4 ≠ 0.29999999999999993
# 0.7 - 0.5 ≠ 0.19999999999999996
# 0.7 - 0.6 ≠ 0.09999999999999998
# 0.8 - 0.1 ≠ 0.7000000000000001
# 0.8 - 0.2 ≠ 0.6000000000000001
# 0.8 - 0.5 ≠ 0.30000000000000004
# 0.8 - 0.6 ≠ 0.20000000000000007
# 0.8 - 0.7 ≠ 0.10000000000000009
# 0.9 - 0.3 ≠ 0.6000000000000001
# 0.9 - 0.6 ≠ 0.30000000000000004
# 0.9 - 0.7 ≠ 0.20000000000000007
# 0.9 - 0.8 ≠ 0.09999999999999998

뺄셈의 오차가 덧셈보다 더 많은 것도 알 수 있습니다.

다. 0.29 x 100 ≠ 29

알고리즘 문제를 풀다보면 소수로 문제가 나오는 경우가 있습니다.

특히 소수 둘째 자리 까지 주어진 경우, 배열과 같은 자료구조에서 해당 값을 인덱스로 관리하기 위해서는 정수로 바꾸어야합니다.

이때, 단순하게 소수에 100을 곱해서 처리하려고 하다가는 오차로 인해 오답이 생기기도 합니다.

print(0.29 * 100)
# 28.999999999999996

 

또 소수 둘째 자리의 수에 100을 곱해서 오차가 생기는 수를 찾아보면 다음과 같습니다.

for i in range(10, 100):
    a = f"0.{i}"
    a = float(a)
    if a * 100 != i:
        print(f"{a} * 100 ≠ {a * 100}")

# 0.14 * 100 ≠ 14.000000000000002
# 0.28 * 100 ≠ 28.000000000000004
# 0.29 * 100 ≠ 28.999999999999996
# 0.55 * 100 ≠ 55.00000000000001
# 0.56 * 100 ≠ 56.00000000000001
# 0.57 * 100 ≠ 56.99999999999999
# 0.58 * 100 ≠ 57.99999999999999