05. Linear Regression의 cost 최소화의 TensorFlow 구현 (lab 03)

텐서플로우를 사용해서 비용(cost)과 기울기(W)가 어떻게 변화하는지 보여주는 코드를 설명한다.

import tensorflow as tf

X = [1., 2., 3.]
Y = [1., 2., 3.]
m = len(X)

W = tf.placeholder(tf.float32)

hypothesis = tf.mul(W, X)
cost = tf.reduce_sum(tf.pow(hypothesis-Y, 2)) / m

init = tf.initialize_all_variables()

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

# 그래프로 표시하기 위해 데이터를 누적할 리스트
W_val, cost_val = [], []

# 0.1 단위로 증가할 수 없어서 -30부터 시작. 그래프에는 -3에서 5까지 표시됨.
for i in range(-30, 51):
xPos = i*0.1 # x 좌표. -3에서 5까지 0.1씩 증가
yPos = sess.run(cost, feed_dict={W: xPos}) # x 좌표에 따른 y 값

print('{:3.1f}, {:3.1f}'.format(xPos, yPos))

# 그래프에 표시할 데이터 누적. 단순히 리스트에 갯수를 늘려나감
W_val.append(xPos)
cost_val.append(yPos)

sess.close()
# ------------------------------------------ #

import matplotlib.pyplot as plt

plt.plot(W_val, cost_val, 'ro')
plt.ylabel('Cost')
plt.xlabel('W')
plt.show()
[출력 결과]
...
4.6, 60.5
4.7, 63.9
4.8, 67.4
4.9, 71.0
5.0, 74.7

[출력 결과]는 그래프에 표시된 좌표를 보여준다. -3에서 5까지 81개의 좌표가 나왔다. 출력된 숫자는 왼쪽 열이 W, 오른쪽 열이 Cost가 된다.

이 코드에서는 최소 비용을 찾는 것이 목적이 아니라 최소 비용을 포함하고 있는 W의 값까지 포함해서 일정 범위의 W에 대한 Cost를 계산해서 Cost가 어떻게 변화하는지 보여주는 것이 목적이다.

그래프는 W가 변화할 때의 Cost를 보여준다는 것을 명심하자. 앞선 글에서 빨강 점 단위로 이동한다고 가정할 때, 해당 위치(Cost)에서 미분한 결과를 빼면 가능하다는 것을 설명했었다.


X = [1., 2., 3.]
Y = [1., 2., 3.]
m = len(X)

W = tf.placeholder(tf.float32)

X와 Y 데이터를 1x3 매트릭스로 초기화했고, m은 데이터의 갯수를 뜻한다. 기울기(W)를 바꾸면서 비용을 계산해서 보여주려는 것이 목적이기 때문에 W를 placeholder로 처리했다. 여기서는 y 절편에 해당하는 b는 생략하고 있다. 다음 번 동영상에서 절편 b를 처리하는 쉬운 방법을 배운다. 여기서는 생략했고, 결과를 명확하게 보여주기 위한 것이라고 생각하자.


hypothesis = tf.mul(W, X)
cost = tf.reduce_sum(tf.pow(hypothesis-Y, 2)) / m

H(x) = Wx 로 식을 간단하게 정리했으므로 hypothesis에서도 b를 더하는 코드는 보이지 않는다. cost를 계산하는 코드에서는 square 함수 대신 pow 함수를 사용하고 있다. square 함수는 제곱을 처리하고, pow 함수는 지수를 처리한다. pow 함수이기 때문에 두 번째 매개변수로 2가 전달되었다. 평균을 구하기 위해 reduce_mean 함수 대신 m으로 직접 나누고 있다.

위의 코드를 이전 코드에서는 아래처럼 처리했었다.

hypothesis = W * x_data + b
cost = tf.reduce_mean(tf.square(hypothesis - y_data))


# 그래프로 표시하기 위해 데이터를 누적할 리스트
W_val, cost_val = [], []

# 0.1 단위로 증가할 수 없어서 -30부터 시작. 그래프에는 -3에서 5까지 표시됨.
for i in range(-30, 51):
xPos = i*0.1 # x 좌표. -3에서 5까지 0.1씩 증가
yPos = sess.run(cost, feed_dict={W: xPos}) # x 좌표에 따른 y 값

print('{:3.1f}, {:3.1f}'.format(xPos, yPos))

# 그래프에 표시할 데이터 누적. 단순히 리스트에 갯수를 늘려나감
W_val.append(xPos)
cost_val.append(yPos)

그래프에 출력하기 위해서 W_val과 cost_val 변수를 리스트로 만들었다. 81번의 호출 결과를 저장해야 하니까. 원본 코드에서는 50인데, 나는 51을 사용해서 왼쪽과 오른쪽의 균형을 맞추었다.

원본에는 중복되는 코드가 있어서 여기서는 xPos와 yPos로 대체했다. 코드는 몇 줄 길어졌다.


import matplotlib.pyplot as plt

plt.plot(W_val, cost_val, 'ro')
plt.ylabel('Cost')
plt.xlabel('W')
plt.show()

matplotlib 모듈은 파이썬에서 가장 많이 사용하는 그래프 출력 모듈이다. 빨강 점이 찍힌  밥그릇 그래프가 여기서 출력됐다.

plot 함수의 첫 번째는 x축 데이터, 두 번째는 y축 데이터, 세 번째는 그래프 타입. r은 빨강의 red, o는 동그라미를 의미한다. o 대신 x를 사용하면 x 표시로 바뀐다.  그래프는 show 함수를 호출하기 전까지는 출력되지 않는다.


# --------------------------------------------------------------------- #


이번 소스코드는 placeholder를 사용하는 것과 동시에 앞의 글에서 설명했던 현재 cost에 대한 미분 결과를 어떻게 계산하는지 보여준다. 여기서 한발 더 나아가서 텐서플로우가 계산한 값과 직접 계산한 값이 똑같다는 것까지 보여준다.

음.. 이번 코드는 동영상과 많은 부분에서 다르다. 동영상에서는 수동으로 계산해도 최소 비용을 잘 찾는다는 것을 보여주는 반면, 여기서는 앞에 설명한 내용들을 보여준다.

import tensorflow as tf

x_data = [1., 2., 3., 4.]
y_data = [1., 3., 5., 7.] # x와 y의 관계가 모호하다. cost가 내려가지 않는 것이 맞을 수도 있다.

# 동영상에 나온 데이터셋. 40번째 위치에서 best fit을 찾는다. 이번에는 사용하지 않음.
# x_data = [1., 2., 3.]
# y_data = [1., 2., 3.]

W = tf.Variable(tf.random_uniform([1], -10000., 10000.)) # tensor 객체 반환

X = tf.placeholder(tf.float32) # 반복문에서 x_data, y_data로 치환됨
Y = tf.placeholder(tf.float32)

hypothesis = W * X
cost = tf.reduce_mean(tf.square(hypothesis - Y))

# 동영상에서 미분을 적용해서 구한 새로운 공식. cost를 계산하는 공식
mean = tf.reduce_mean(tf.mul(tf.mul(W, X) - Y, X)) # 변경된 W가 mean에도 영향을 준다
descent = W - tf.mul(0.01, mean)
# W 업데이트. tf.assign(W, descent). 호출할 때마다 변경된 W의 값이 반영되기 때문에 업데이트된다.
update = W.assign(descent)

init = tf.initialize_all_variables()

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

for step in range(50):
uResult = sess.run(update, feed_dict={X: x_data, Y: y_data}) # 이 코드를 호출하지 않으면 W가 바뀌지 않는다.
cResult = sess.run( cost, feed_dict={X: x_data, Y: y_data}) # update에서 바꾼 W가 영향을 주기 때문에 같은 값이 나온다.
wResult = sess.run(W)
mResult = sess.run(mean, feed_dict={X: x_data, Y: y_data})

# 결과가 오른쪽과 왼쪽 경사를 번갈아 이동하면서 내려온다. 기존에 한 쪽 경계만 타고 내려오는 것과 차이가 있다.
# 최종적으로 오른쪽과 왼쪽 경사의 중앙에서 최소 비용을 얻게 된다. (생성된 난수값에 따라 한쪽 경사만 타기도 한다.)
# descent 계산에서 0.1 대신 0.01을 사용하면 오른쪽 경사만 타고 내려오는 것을 확인할 수 있다. 결국 step이 너무 커서 발생한 현상
print('{} {} {} [{}, {}]'.format(step, mResult, cResult, wResult, uResult))

print('-'*50)
print('[] 안에 들어간 2개의 결과가 동일하다. 즉, update와 cost 계산값이 동일하다.')

print(sess.run(hypothesis, feed_dict={X: 5.0}))
print(sess.run(hypothesis, feed_dict={X: 2.5})) sess.close()
[출력 결과]
...
45 952.0852661132812 120862.359375 [[ 128.6113739], [ 128.6113739]]
46 880.678955078125 103412.8828125 [[ 119.09052277], [ 119.09052277]]
47 814.6279907226562 88482.671875 [[ 110.28373718], [ 110.28373718]]
48 753.5309448242188 75708.015625 [[ 102.1374588], [ 102.1374588]]
49 697.01611328125 64777.6953125 [[ 94.60214996], [ 94.60214996]]
--------------------------------------------------
[] 안에 들어간 2개의 결과가 동일하다. 즉, update와 cost 계산값이 동일하다.
[ 473.01074219]
[ 236.50537109]

최종 결과만 확인해 보자. 오른쪽 끝에 있는 두 개의 열을 보면 된다. 여기서는 기울기(W)가 1에 수렴하지 않는다. [1, 2, 3, 4]와 [1, 3, 5, 7]은 모든 좌표를 지나는 직선이 없기 때문이다. 더욱이 50번 돌려서는 제대로 된 결과 또한 기대할 수 없다는 것을 보여준다.

이렇게 얻은 잘못된 기울기(W)에 대해 궁금증(5, 2.5)을 적용하면 말도 안되는 473과 236이 나오는 것은 당연하다. 얼마나 반복해야 적절한 기울기가 됐다는 것을 알 수 있을까? 정답은 충분히 반복해야 한다는 모호함 정도.

이번 코드는 해석이 어렵다. 일단 GradientDescentOptimizer 함수를 호출하지 않고 있다. update 계산을 통해 얻은 W를 cost에 전달하고 있다. 그럼에도 불구하고 cost는 정상적으로 최저점을 찾아서 진행한다. 이 말은 update 계산에 포함된 공식이 올바르게 GradientDescentOptimizer 함수의 역할을 하고 있다는 뜻이 된다. 즉, GradientDescentOptimizer 함수 없이 직접 만들어서 구동할 수도 있다는 것을 보여주는 예제이다.


mean    = tf.reduce_mean(tf.mul(tf.mul(W, X) - Y, X))   # 변경된 W가 mean에도 영향을 준다
descent = W - tf.mul(0.01, mean)

코드와 공식을 함께 보자.

              W(x)              -->                                                         tf.mul(W, X)
              W(x) - y         -->                                                        tf.mul(W, X) - Y
             (W(x) - y) * x   -->                                             tf.mul(tf.mul(W, X) - Y, X)
   1/m * ∑(W(x) - y) * x)  -->                    tf.reduce_mean(tf.mul(tf.mul(W, X) - Y, X))
α(1/m * ∑(W(x) - y) * x)  --> tf.mul(0.01, tf.reduce_mean(tf.mul(tf.mul(W, X) - Y, X)))

복잡하기는 하지만, 하나씩 순서대로 확장해 가니 조금 쉬워 보인다. 혹시 브라우저에 따라 수직으로 줄을 맞춘 부분이 어긋날 수도 있을 것 같다. 맥 크롬에서는 얼추 맞았다.