06. cost 함수를 파이썬만으로 직접 구현

텐서플로우가 동작하는 것은 그렇다 치고 직접 앞에서 설명한 공식들을 코드로 만들고 싶어졌다. 구글링을 엄청 하면서 어렵게 만든 어색한 코드가 여기 있다.


def cost(W, X, y):
s = 0
for i in range(len(X)):
s += (W * X[i] - y[i]) ** 2

return s / len(X)

X = [1., 2., 3.]
Y = [1., 2., 3.]

W_val, cost_val = [], []
for i in range(-30, 51):
W = i*0.1
c = cost(W, X, Y)

print('{:.1f}, {:.1f}'.format(W, c))

W_val.append(W)
cost_val.append(c)

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

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


05. Linear Regression의 cost 최소화의 TensorFlow 구현 (lab 03)에서 보여준 텐서플로우 코드와 동일한 결과를 보여준다. 기울기에 해당하는 W가 바뀌는 과정에서 cost가 어떻게 바뀌는지를 보여주고 있다. 원본 코드와 다른 점이 있다면 cost에 해당하는 값인데, 이건 중요하지 않다. 비용을 계산하는 cost 함수가 convex 형태로 진행되는지가 중요한 것이니까.


def cost(W, X, y):
s = 0
for i in range(len(X)):
s += (W * X[i] - y[i]) ** 2

return s / len(X)

cost를 계산하는 함수로 이번 코드에서 가장 중요하다. 1x3 행렬에 들어있는 X는 행렬 연산을 할 수 없어서 for문 안에서 매번 X[i]와 W를 곱한 다음에 y[i]를 빼고 있다. 여기에 제곱을 취해서 벌점을 부여해야 해서, 이 값에 ** 연산자를 사용해서 제곱을 했다. 합계는 변수 s에 저장했고, 반환할 때 갯수 m에 해당하는 len(X)로 나누었으니까 return문의 오른쪽에서 평균을 구하고 있다.

이게 간단해 보이지만, 이거 구현하기 위해서 cost 함수에 대해 연구하고 구글링한 시간이 얼마인지.. 만들고 나니까, 어이없을 정도로 간단하고 잘 동작했다.


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


이번에는 경사타고 내려가기(Gradient Descent) 알고리듬에서 다음 번 위치로 이동하기 위한 경사값을 직접 계산해서 앞에서 보여준 것처럼 원하는 기울기(W)가 1에 수렴하는지 확인하려고 만든 코드다.

def gradients(W, X, y):
nX = []
for i in range(len(X)):
nX.append(W * X[i] - y[i])

s = 0
for i in range(len(X)):
s += nX[i] * X[i]

return s / len(X)

X = [1., 2., 3.]
Y = [1., 2., 3.]

# 양수로 시작하면 값이 줄어들고
# 음수로 시작하면 값이 늘어난다.
W = 100 # -100으로도 테스트해 볼 것.
for i in range(1000):
g = gradients(W, X, Y)
W = W - g*0.01

if i%20 == 19:
print('{:4} : {:12.8f} {:12.8f}'.format(i+1, g, W))
[출력 결과]
 ...
 920 :   0.00000000   1.00000000
 940 :   0.00000000   1.00000000
 960 :   0.00000000   1.00000000
 980 :   0.00000000   1.00000000
1000 :   0.00000000   1.00000000

중간에 달아놓은 주석처럼 최소 비용이 1이라면 초기값 100은 경사면의 오른쪽에 있고 gradients 함수가 호출된 결과를 반영할 때마다 최소 비용을 향해 진행한다. 만약 -100으로 시작하면 경사면 왼쪽에서부터 오른쪽으로 진행하는 값을 확인할 수 있다.

gradients 함수는 cost 함수를 미분한 결과, 즉 변화량을 값으로 만들어 내는 함수이다. 이렇게 반환된 값에 learning rate에 해당하는 α(알파)를 곱해서 실제 진행량을 결정하는데, 여기서는 0.01을 사용했다. 즉, 변화량의 1/100만큼 적용하고 있다.

 α(알파) 오른쪽에 있는 식을 코드로 구현했다. 첫 번째 반복문에서 W(x) - y 를 계산해서 nX 리스트에 저장했다. 두 번째 반복문에서 이 값을 공식 오른쪽 끝의 x(i)와 곱한 결과를 s에 저장했다. return문에서 데이터 갯수로 나누어서 앞선 코드처럼 평균을 반환했다.

아래는 gradients 함수를 축약 버전으로 구성한 코드다. 반복문을 하나로 줄이고, 중간 결과를 리스트에 저장했던 코드 대신 반복문 하나로 처리했다. 머신러닝이 익숙하지 않을 때는 앞의 코드가 쉽게 느껴졌는데, 지금은 아래 코드가 훨씬 편하다. 이번 코드의 좋은 점은 수식에 나타난 것과 같은 방식으로 구성했다는 점이다. 이번 코드는 이번 글의 맨 앞에 나왔던 cost 함수와 매우 유사하다. 제곱(**) 대신 X[i]를 곱하는 코드만 다를 뿐이다. 꼭! cost 함수와 비교해서 봐야 한다.

def newGradients(W, X, y):
s = 0
for i in range(len(X)):
s += (W * X[i] - y[i]) * X[i]

return s / len(X)


텐서플로우가 한 줄도 없지만, 제대로 동작한다. 공식을 제대로 반영하는 것이 얼마나 중요한지 새삼 깨달을 수 있었다. 여기서 보여준 두 개의 함수를 구현하기 위해 1주일은 꼬박 고생했다. 공식 없이 마구잡이로 코딩하다가 벌어진 결과로 이 글을 보는 사람은 나처럼 미련하지 않기를 바라고, 계속적으로 중요하다고 하는 공식을 한번 더 보았으면 한다.


def cost(W, X, y):
s = 0
for i in range(len(X)):
s += (W * X[i] - y[i]) ** 2

return s / len(X)

def gradients(W, X, y):
nX = []
for i in range(len(X)):
nX.append(W * X[i] - y[i])

s = 0
for i in range(len(X)):
s += nX[i] * X[i]

return s / len(X)

X = [1., 2., 3.]
Y = [1., 2., 3.]

W = 100
for i in range(1000):
c = cost(W, X, Y)
g = gradients(W, X, Y)
W = W - g*0.01

# cost는 거리의 제곱을 취하기 때문에 W가 음수이건 양수이건 상관없다.
if c < 1.0e-15:
break

if i%20 == 19:
print('{:4} : {:17.12f} {:12.8f} {:12.8f}'.format(i+1, c, g, W))
[출력 결과]
  20 : 7440.099604469872 186.33428246  39.06543199
  40 : 1099.942255029881  71.64540360  15.63613245
 ...
 400 :    0.000000000001   0.00000242   1.00000049
 420 :    0.000000000000   0.00000093   1.00000019
 440 :    0.000000000000   0.00000036   1.00000007
 460 :    0.000000000000   0.00000014   1.00000003

앞에 설명했던 코드를 묶어서 cost 함수와 gradient descent 알고리듬으로 동작하는 예제를 만들었다. 이렇게 되어야 텐서플로우에서 보여주던 예제와 똑같은 예제가 된다.

반복문 안에서 cost와 gradients가 함께 사용되는 것이 핵심이다. 이 부분은 앤드류 교수님께 배웠다. 텐서플로우 내부가 어떨지는 모르겠지만, 여기서는 c(비용)가 일정 수준 이하로 떨어지면 더 이상 계산하지 않도록 처리했다. 전체 횟수만큼 반복하지 않고 미리 탈출할 수 있기 때문에 early stopping이라고 부른다. 출력 결과에서 콜론(:) 바로 오른쪽에 있는 숫자가 비용이다.