텐서플로우 실수 오차(정밀도)

텐서플로우에서 이상한 점을 발견했다. 그런데, 이게 틀리다고 얘기할 수는 없겠지만, 지금까지의 실수 연산하고는 다른 점이어서 정리한다.

텐서플로우의 기본이 되는 코드를 하나 만들었다. hypothesis를 통해서 새로운 입력이 들어왔을 때의 결과를 예측하고 있다. hypothesis는 W와 X를 곱하는 아주 단순한 코드다. 결과를 예측하기 위해 X는 placeholder로 처리했다.

import tensorflow as tf
import numpy as np

x_data = [1., 2., 3.]
y_data = [2., 4., 6.]

W = tf.Variable(0.5)
X = tf.placeholder(tf.float32)

hypothesis = W * X
cost = tf.reduce_mean(tf.square(hypothesis-y_data))
rate = tf.constant(0.1)

optimizer = tf.train.GradientDescentOptimizer(rate)
train = optimizer.minimize(cost)

sess = tf.Session()
sess.run(tf.initialize_all_variables())

for i in range(3):
sess.run(train, feed_dict={X: x_data})
print(i, sess.run(W))
print('-'*30)

t = np.array([3.6]) # x가 3.6일 때의 y 예측 시도
ww = sess.run(W)
hh = sess.run(hypothesis, {X: t})

print('manual : {}'.format((ww*t)[0]))
print('hypothesis : {}'.format(hh[0]))
[출력 결과]
0 1.9
1 1.99333
2 1.99956
------------------------------
manual : 7.198400115966797
hypothesis : 7.198400020599365

출력된 결과를 보면, hypothesis를 통해 계산한 값과 직접 계산한 값이 다르게 나온다. 많이 다르지는 않고 미세하게 소숫점 뒷자리에서 달라진다. 실제로는 같을 때도 있고, 다르게 나올 때도 있다.

텐서플로우를 통해 hypothesis를 계산하는 것과 직접 W와 X를 곱하는 것이 같다는 것을 이해하지 못하면, 지금 코드는 의미가 없다. 이 부분은 단순 계산이기 때문에, W와 X가 복잡해지면 직접 할 수가 없으니까, 텐서플로우에 hypothesis를 계산해 달라고 부탁하는 것일 뿐이다.


아래 코드는 텐서플로우의 곱셈 코드다. 실수 2개를 곱해서 결과를 numpy로 변환을 해서 출력하고 있다. 텐서플로우는 결과를 numpy로 반환하기 때문에 함수 이름 또한 op2Numpy라고 만들었다. op2Numpy 함수는 functions.py 파일에 들어 있다.

import tensorflow as tf
import functions

a, b = 0.4, 0.6

print(functions.op2Numpy(tf.mul(a, b)))
print('{}'.format(functions.op2Numpy(tf.mul(a, b))))
[출력 결과]
0.24
0.24000000953674316

같은 함수를 호출했는데, 출력 결과가 다르게 나왔다. print 함수는 일정 자릿수 이상을 출력하지 않는 관계로 첫 번째 출력에서는 0.24까지만 표시됐다. 전체 자릿수 출력을 위해 문자열의 format 함수를 사용했다.

놀랍게도 아래쪽에 숨겨진 값들이 보인다. 컴퓨터에서는 모든 숫자를 2진수의 형태, 즉 비트(bit) 단위로 저장을 해야 한다. 2의 32승으로 실수를 저장한다는 뜻은 2의 32승 갯수만큼의 실수 종류만 저장할 수 있다는 뜻이다. 10진수를 2진수로 변환하는 과정에서 오차는 필연적으로 발생할 수밖에 없다.

마찬가지로 0.6을 10진수로 표현하는 것은 쉽지만, 2진수로 표현하게 되면 굉장히 길게 저장을 하게 되는데, 이 부분에서 오차가 발생하게 된다. 텐서플로우는 이렇게 발생하는 오차에 대한 처리 없이, 2진수 자체를 계산에 활용하고 있는 것처럼 보인다. 어쩌면 이런 오차 하나를 수정하는 것조차도 성능에 영향을 줄 수 있기 때문에 무시하는 것일 수도 있다. 실수 하나에 대해서라고 쉽게 얘기하지만, 한 번 연산에 수천, 수만 개의 연산이 포함될 수 있기 때문에 신중한 영역일 수도 있다.

0.24가 맞는지 0.24000000953674316이 맞는지 나는 판단할 수 없다. 그러나, 간혹 우리가 생각했던 결과가 나올 수 있다는 것은 명심해야 한다. 매우 작은 오차를 여러 번 적용하는 과정에서 꽤 큰 오차로 변할 수도 있지 않을까, 하는 걱정이 든다. 오차를 고민하지 않는 제일 좋은 방법은 직접 계산하려고 시도하지 말고, 텐서플로우에 계산을 맡기는 것이다. 첫 번째 코드에서 올바른 코드가 무엇이냐고 묻는다면, hypothesis를 사용한 코드라고 대답할 수 있다. 언제나.

그냥 결과가 다르게 나와서 궁금했을 뿐이고, 당연히 텐서플로우의 원칙을 따르는 것이 맞다. 궁금증을 풀었으니, 푹 자야겠다. 이렇게 단순한 문제일 거라고 생각하지 못하고, 온갖 상상을 적용해 보고 나서야 여기에 올 수 있었다. 개운하지는 않다.