컴퓨터 사이언스 부트캠프 with Python
1. 실수 연산의 함정
데이터 사이언스 공부를 하다보면 가끔 머리로 이해되지 않는 것이 생기곤 한다. 그 중 하나가 실수 연산에 대한 것이다.
다음 예를 살펴보자
python(3.6.4)으로 다음과 같이 입력하자.
1 | a = 0.01 |
result는 값이 어떻게 나오게 될까?
위의 코드는 쉽게 말하자면 0.01을 100번 더한 것과 다를 게 없다.
그렇다면 답은 1일 것이다. 하지만
1 | result |
답은 1이 아니다. 만약 내가 조건문을 이용해서1==result
판단을 내렸다면 결과는 False로 나올 것이다.
1 | a = 0.015625 |
1 | >>>1.5625 |
그런데 이번 경우에는 생각과 같이 1.5625라는 결과가 나온다. 왜 갑자기 오차 하나 없이 깔끔하게 값이 나오는 것일까?
왜 이런 일이 발생하는 것일까?
2. 부동소수점
이 현상에 대해 이해하기 위해 부동소수점에 대해서 이해를 해야한다.
부동소수점에서 ‘부’는 부유한다는 말, 즉 떠다닌다는 말이다. 123.456을 다르게 표현해 보는 경우를 생각해보자
1 | 123.456 = 1.23456 * 10^2 |
위의 예시 말고도 다양한 방식이 있다. 소숫점이 둥둥 떠다니는 것 같이 움직인다. 그래서 이러한 실수 표현 방식을 부동소수점이라고 부른다.
3. 단정도와 배정도
단정도(single-precision)는 실수를 32비트로 표현하며 부호 1비트, 지수부 8비트, 가수부 23비트로 구성되어 있다.
배정도(double-precision)는 실수를 64비트로 표현하며 부호 1비트, 지수부 11비트, 가수부 52비트로 구성되어 있다.
배정도가 단정도보다 두 배 정도의 비트 수가 많은데, 비트 수가 많은 만큼 정밀도가 높다고 할 수 있겠다. 파이썬은 배정도를 사용한다.
4. 1바이트 실수 자료형 설계하기
$$\pm 1.man \times 2^{(exp-bias)}$$
위의 수식은 실수 자료형을 표현한 수식이다.
1.man은 가수, 2는 밑수, exp-bias는 지수를 의미한다.
이 식을 이용해 7.75라는 10진수 실수를 1바이트 부동소수점으로 표현해보자.
4.1 10진수 실수를 2진수 실수로 바꾸기
$$7.75 = 4 + 2 +1 +0.5 + 0.25 $$
$$= 2^2 + 2^1 + 2^0 + 2^{-1} + 2^{-2}$$
$$=111.11$$
2진수로 바꾸면 111.11이란 값이 나온다.
4.2 정규화
아 숫자를 정규화 해보자. 정규화란, 소수점 왼쪽에 위치한 가수 부분을 밑수보다 작은 자연수가 되도록 만드는 것이다.
111.11을 정규화 하면 다음과 같다.
$$ 111.11 = 1.1111 \times 2^2$$
4.3 메모리 구조
정규화된 부동소수점 $1.111 \times 2^2$를 앞의 수식과 비교해 보면
man은 1111이고 exp-bias는 2이다.
이제 메모리 구조를 정하고 man과 exp값만 저장하면 설계가 끝난다.
이때 지수부와 가수부에 할당하는 비트 수에 따라 표현 범위와 정밀도가 결정된다.
1바이트 부동소수점 구조는 다음과 같다.
$$ 0 \ 0000 \ 000 \ [부호(sign) \ 지수부(exp) \ 가수부에서 \ man에 \ 해당되는 \ 부분] $$
- 첫번째 비트 : 부호 0은 양수, 1은 음수
- 가운데 4비트 : 지수부에 해당하며 exp 값이다.
- 0~15의 양수를 표현할 수 있다
- $bias = 2^{지수의 비트수} -1$
- 맨 뒤 3비트 : 가수부로 man 값을 저장함
$1.1111 \times 2^2$를 1바이트의 메모리 구조로 변경해 보자.
- 부호비트는 0이다.
- $exp-bias$는 2이다. $bias$값이 7이므로 $exp$는 9가 된다.이것을 이진수로 나타내면 $1001_{(2)}$가 된다.
- 가수부에 할당된 비트는 3비트이다. 1111을 3비트에 넣을때는 뒷자리 1을 생략한다. 가수부는 111이다.
$$ 0 \ 1001 \ 111 = 0100 \ 1111 $$
이것을 16진수로 나타내면 0x4f가 된다.
정리하자면 10진수 7.75를 $ 0100 \ 1111 $로 나타낼 수 있고 이것을 다시 16진수로 나타내면 0x4f이다.
4.4 1바이트 부동소수점의 표현 범위
- 표현할 수 있는 가장 작은 수(지수부0001)
- $1.0000 \times = 0.0156256$
- 표현할 수 있는 가장 큰 수(지수부 1110)
- $1.111 \times = 240$
단, 지수부 비트가 모두 0일때와 모두 1일때는 0.0, 정규화 불가능, 무한대, NaN 같은 특별한 상황이므로 제외한다.
4.5 1바이트 부동소수점의 정밀도
7.75를 변환하는 과정에서 3비트의 가수부데이터에 1을 누락해 가면서 가수부 공간에 담았던 것을 기억할 것이다.
1을 누락하게 되면 0x4f는 7.75를 완벽하게 표현하지 못하게 된다.
$$1.111 \times 2^2 = 1 \times 2^2 + 1 \times 2^1 + 1 \times 2^{-1} = 7.5$$
여기서 0.25만큼 차이가 나게되고, 그만큼 정밀도도 떨어지게 된다.
5. 정밀도에 대한 고찰
5.1 엡실론
실수 자료형에서 엡실론이란 1.0과 그 다음으로 표현 가능한 수 사이의 차이를 말한다.
1 | import sys |
위 코드로 엡실론 값을 확인해 보자.
배정도의 가수부는 52비트인 것을 기억할 것이다. 1.0을 배정도에 맞춰 표현하면
$1.0000 ….. 0000(0:52개) \times 2^0$
배정도에서 1.0다음으로 표현할 수 있는 수는
$1.0000 ….. 0000(0:51개) \times 2^0$
두 수의 차이는
$1.0000 ….. 0000(0:51개) \times 2^0$
이 숫자를 10진수로 바꾸면 엡실론 값이 등장한다.
$2.220446049250313 \times 10^{-16}$
5.2 엡실론과 정밀도
엡실론을 이용하면 해당 실수 다음에 표현할 수 있는 수를 알아낼 수 있다.
9.25라는 수를 부동소수점 방식으로 표현하면 $1.00101 \times 2^3$이다.
여기서 지수부분만 떼서 엡실론을 구하면 이 실수와 다음 표현 가능한 수 사이의 차이를 구할 수 있다.
코드로 살펴보면,
1 | import sys |
0.000000000000002만큼 차이가 난다.
그렇다면 9.25에 diff보다 작은 값을 더하면 어떻게 될까?
추측으로는 9.25가 나올 것 같다.
확인해보자
1 | a = 9.25 |
추측과 같이 half_diff를 더하더라도 값의 변화가 없다.
diff보다 작은 값을 더한 수를 부동소수점 방식에서는 표현할 수 없다는 말이다.
다르게 말하자면 정밀도가 떨어진다는 말이다.
다음의 내용은 혼자서하는 괴발개발 블로그
https://aisolab.github.io/computer%20science/2018/08/07/CS_Real-number 에서 가져온 글이다.
다음의 방법을 이용하면 상대오차(relative error) 가 엡실론보다 작으면 서로 같은 수라고 판단하는 function을 만듦으로써 위와 같은 문제를 해결할 수 있다.
1 | a = 0.1 * 3 |
1 | False |
$$relative \ error = {\left\vert x - y\right\vert \over \max(\left\vert x \right\vert , \left\vert y \right\vert)}$$
1 | import sys |
1 | True |