OZ1NG의 뽀나블(Pwnable)

[Tips] Python float형(부동소수점) 계산 오차 발생 이유 및 해결법 본문

Tips

[Tips] Python float형(부동소수점) 계산 오차 발생 이유 및 해결법

OZ1NG 2021. 4. 4. 22:46

파이썬3로 개발하다가 단순 float형끼리의 계산에서 오차가 발생한다는 것을 알았다.

[사진1] - Python3 float형 오차

?????????????????

아니 이게 왜...

분명 0.043-0.001의 값은 0.042가 나와야하는데 0.0419999.....가 나온다... (참고로 python2도 마찬가지다.)

 

[*] 이유

이건 꼭 Python에서만 발생하는 문제가 아니라 모든 언어에서 거의 공통으로 발생하는 문제이고

그 이유는 컴퓨터는 숫자를 2진수로 받아들이기 때문에 생긴다는 것이다.

 

2진수의 정수부는 아래와 같이 표현할 수 있다.

1 = 2^0 = 1(2) = 1

1*2 = 2^1 = 10(2) = 2

1*2*2 = 2^2 = 100(2) = 4

...

2씩 곱한다는 규칙이 있다.

 

2진수의 소수부는 아래와 같다.

1/2 = 2^-1 = 0.1(2) = 1/2 = 0.5

(1/2)/2 = 2^-2 = 0.01(2) = 1/4 = 0.25

((1/2)/2)/2 = 2^-3 = 0.001(2) = 1/8 = 0.125

...

2씩 나눈다는 규칙이 있다.

 

그렇다면 0.75와 같은 수는 2진수로 0.11(2)와 같이 쉽게 떨어지지만 0.3과 같은 수는 어떨까?

0.3 = 0.01001100110011......(무한반복)(2) 이 된다.

 

컴퓨터의 메모리도 한계가 있으므로 결국 컴퓨터는 이 무한반복 되는 값에서 근사값을 저장하게 되고

바로 여기서 오차가 발생하게 되는 것이다.

 

이 근사값을 저장하는 방식은 고정소수점부동소수점 2가지가 있는데

간략히 설명하면

고정소수점은 예를들어 한 실수를 32bit로 표현하기로 하였다면

부호로 사용할 부분 = 1bit, 정수를 저장할 부분 = 15bit, 소수를 표현할 부분 = 16bit와 같이

소수점의 위치를 고정 시켜 놓는 것이다.

0(부호 표현) 000 0000 0000 0000 (정수 표현) . 0000 0000 0000 0000 (소수 표현) (2)

이러면 소수를 표현 할 수 있는 부분은 16bit밖에 안되므로 소수 값이 0.3처럼 2진수로 변환하였을때 무한인 경우

좀더 정확한 표현이 불가능해진다.

고정소수점으로 표현한 경우 : 0.3 = 0.0100110011001100(2) ==> (10진수로 다시 변환) : 0.29998779296875

이런 단점을 조금더 보완한게 부동소수점이다.

 

부동소수점은 일반적으로 IEEE에서 표준으로 정한 방법(IEEE 754 32비트 단정밀도)을 따르므로 여기서도 IEEE표준에 따른 방법으로 정리 하겠다.

[사진2] - IEEE 754 부동 소수점 (출처 : https://ko.wikipedia.org/wiki/IEEE_754)

부동소수점은 부호부, 지수부, 가수부로 나누어진다.

부호부는 고정소수점과 마찬가지로 보통 최상위 bit를 사용하여 표현한다.

지수부가수부는 아래와 같이 구한다.

고정소수점 예시와 같이 0.3이라는 수를 변환한다고 가정해보면

1. 먼저 0.3의 절댓값을 2진수로 변환한다. 0.3 = 0.0100110011001100.........(2)

2. 소수점을 이동시켜 왼쪽에는 1만 남도록 한다. 

    1.0011001100110011.......*2^-2 : 지수 = -2

3. 소수점의 오른쪽 부분은 가수부로 만약 23bit보다 부족하면 0으로 채워 총 23bit를 만든다.

    하지만 이 경우엔 부족하지 않으므로 그냥 냅둔다.

    가수부 : 0011001100110011

4. 이제 Bias를 지수에 더하면 된다. 32비트 IEEE 754 형식에서의 Bias는 127(== (2^8)-1) 이므로 

    127+(-2) = 125가 되고 이를 2진수로 변경하면 1111101(2)가 된다.

이제 결과를 부동 소수점으로 표현하면 아래와 같아진다.

0(부호 표현) 0111 1101 (지수부) 0011 0011 0011 0011 0011 001 (가수부)  

 

이를 다시 한번 10진수로 변환해 보면

1.00110011001100110011001(2) * 2^-2 = 0.0100110011001100110011001(2) = 0.29999998211860657(10진수)

위와 같이 정확하지는 않지만 고정소수점을 사용했을 때보다 조금 더 정확하게 표현 할 수 있다.

 

뭔가 설명이 길었는데 아무튼 결과만 말하자면 컴퓨터는 이 부동소수점을 이용해서 2진수로 근사값을 저장하고,

이 때문에 float형(C나 그런 언어에서는 double과 같이 소수를 저장 할 수 있는 타입도 포함) 끼리의 저장 또는 연산에서

정확한 계산이 일어나지 않는 다는 것이다.

 

그렇다면 이것을 어떻게 해결할 수 있을까?

 

[*] 해결법

Python의 decimal모듈 또는 fractions모듈을 사용하면 된다. (여기서는 decimal모듈만 설명하겠다.)

import decimal
print(float(decimal.Decimal('0.043')-decimal.Decimal('0.001')))

[사진3] - 계산 결과

0.30000000000000004.com/

 

Floating Point Math

Floating Point Math Your language isn’t broken, it’s doing floating point math. Computers can only natively store integers, so they need some way of representing decimal numbers. This representation is not perfectly accurate. This is why, more often th

0.30000000000000004.com

위 사이트에서 각종 언어들에 대한 부동 소수점 처리 방식 및 그에대한 결과를 볼 수 있다.

(fractions 모듈을 사용하는 방법도 위 사이트에 있다.)

 

 

[참고]

 - ko.wikipedia.org/wiki/IEEE_754

 - steemit.com/kr/@modolee/floating-point

 - blog.winterjung.dev/2020/01/06/floating-point-in-python

Comments