08. multi-variable linear regression을 TensorFlow에서 구현하기(lab 04)

여러 개의 입력(variable, feature)을 가진 linear regression을 텐서플로우로 직접 구현해 본다.


정확한 결과를 보여주기 위해 매우 확실한 데이터를 사용하고 있다. 우리의 목적은 최소 비용을 갖는 W1과 W2, b를 찾는 것이다. 그래야 정확한 직선을 그래프에 그릴 수 있고, 그걸 토대로 다른 값의 feature에 대해 예측할 수 있게 된다. 데이터에서 요구하는 정답은 W1과 W2는 모두 1이고, b는 0인 결과이다.

  1*1 + 0*1 + 0 = 1
  0*1 + 2*1 + 0 = 2
  3*1 + 0*1 + 0 = 3
  0*1 + 4*1 + 0 = 4
  5*1 + 0*1 + 0 = 5

그림에 있는 코드를 잠시 보자. 이 코드가 뒤에 나올 코드의 핵심적인 부분이다. W1과 W2는 cost 함수에서 매번 변경된다는 것을 기억할 것이다. 시작은 -1과 1 사이의 어떤 난수로 했다. y 절편에 해당하는 b 또한 -1과 1 사이의 난수이다.

hypothesis를 보면 "1*1 + 0*1 + 0"처럼 정직하게 공식을 구성하고 있다. 이렇게 hypothesis를 구성하고 나면, 우리가 추가로 할 것은 없다. 텐서플로우에 전달하면 알아서 기울기(W)를 계산해 준다.


import tensorflow as tf

x1_data = [1, 0, 3, 0, 5]
x2_data = [0, 2, 0, 4, 0]
y_data = [1, 2, 3, 4, 5]

W1 = tf.Variable(tf.random_uniform([1], -1.0, 1.0))
W2 = tf.Variable(tf.random_uniform([1], -1.0, 1.0))

b = tf.Variable(tf.random_uniform([1], -1.0, 1.0))

# feature 갯수만큼 곱하는 이 부분을 제외하면 one-variable과 다른 곳이 없다
hypothesis = W1*x1_data + W2*x2_data + b

cost = tf.reduce_mean(tf.square(hypothesis - y_data))

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

init = tf.initialize_all_variables()

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

for step in range(2001):

sess.run(train)

if step%20 == 0:
print(step, sess.run(cost), sess.run(W1), sess.run(W2), sess.run(b)) sess.close()

[출력 결과] ... 1920 1.42109e-14 [ 1.] [ 0.99999994] [ 1.74593097e-07] 1940 1.42109e-14 [ 1.] [ 0.99999994] [ 1.74593097e-07] 1960 1.42109e-14 [ 1.] [ 0.99999994] [ 1.74593097e-07] 1980 1.42109e-14 [ 1.] [ 0.99999994] [ 1.74593097e-07] 2000 1.42109e-14 [ 1.] [ 0.99999994] [ 1.74593097e-07]

one-variable에서 작성했던 코드와 다른 점이 없어 코드에 대한 설명은 생략한다.

출력 결과에서 3번째가 W1, 4번째가 W2, 마지막이 b에 해당한다. 어느 시점 이후로는 cost가 바뀌지 않기 때문에 W1, W2, b의 결과 또한 바뀌지 않음을 알 수 있다. 이미 충분히 작은 값이기 때문에 더이상 줄어들 여지가 없을 수도 있다. 결과에 나온 cost는 1.42109를 0(zero)이 14개인 숫자로 나눈 값이다.

동영상과 다른 결과가 나오는 것은 난수로 시작했기 때문이고, 어떤 값에서 시작을 해도 같은 결과가 나온다는 것을 보여주기 위해 난수를 사용하고 있다고 이전 글에서 설명했었다.


앞에서 보여준 코드는 정직하다. 이제 행렬(matrix)로 변환한 코드를 볼 차례다. x에 해당하는 데이터를 2개가 아닌 1개의 데이터로 만들었다. x_data는 2차원 리스트이고, 0번째에는 x1, 1번째에는 x2가 들어있다.

기울기인 W 또한 행렬로 만들어야 한다. x_data가 2차원이니까 값을 2개 갖는 행렬이 되어야 한다. 1x2가 맞을까, 2x1이 맞을까? 아직도 이게 딱 생각나지 않는다. 잠시 차분하게 생각을 해야 답을 알 수 있다. 내 자신이 답답하다.

행렬 곱셈을 할 때, W가 앞에 온다는 것을 기억할 것이다. (WX + b) 그래서, W가 앞에 와야 한다면 x_data가 2행 5열이기 때문에 W는 1행 2열이어야 한다. 2행 1열 x 2행 5열은 에러니까.

이제 진짜 중요하고 어려운 것을 설명한다. x_data가 진짜 2행 5열의 리스트라는 생각이 드는가? 정말? 이게 말이 안 되는 이유는 이 글의 첫 번째 그림에 나와 있다.

그림에서는 x1과 x2에 대해 5행 2열로 표현하고 있다. 행이 5개 열이 2개란 말이다. 그런데, 어떻게 여기 와서 2행 5열이라고 얘기할 수가 있단 말인가?

문법적으로는 2행 5열이 맞다. 그러나, 여기서는 각각을 feature에 관련된 데이터 1개로 취급을 해야 한다. 2행 5열이 아닌 2행 1열로 보는 것이 좋다. 결론적으로 1행 2열 x 2행 1열이 되어서 행렬 곱셈이 일어난다. W1과 x1 feature에 포함된 5개를 곱해서 더하고, W2와 x2 feature에 포함된 5개를 곱해서 더하고.

그러나, 헷갈리면 안 된다. 2행 1열이라고 얘기한 것은 이렇게 곱셈하게 된다는 것을 보여주기 위한 것이지 진짜는 아니다. 1행 2열 x 2행 5열이 진짜가 되고, 결과 행렬을 1행 5열이 된다. 실제 코드에서는 1행 5열의 결과에서 y_data(1행 5열)를 뺀 결과에 제곱을 하고 덧셈을 하는 추가 연산이 일어난다. 이 부분은 진짜 중요하니까, 반드시 이해해야 한다. 내 경험담이다.


import tensorflow as tf

# 앞의 코드에서는 정수였는데, 실수로 바뀌었다. 정수로 처리하면 에러.
x_data = [[1., 0., 3., 0., 5.], [0., 2., 0., 4., 0.]] # W의 값은 [[-0.49763036 0.79181528]]
y_data = [1, 2, 3, 4, 5]

W = tf.Variable(tf.random_uniform([1, 2], -1.0, 1.0)) # [2, 1]은 안 된다. 행렬 곱셈이니까.
b = tf.Variable(tf.random_uniform([1], -1.0, 1.0))

# W와 곱해야 하기 때문에 x_data를 실수로 변경
hypothesis = tf.matmul(W, x_data) + b # (1x2) * (2x5) = (1x5)

cost = tf.reduce_mean(tf.square(hypothesis - y_data))

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

init = tf.initialize_all_variables()

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

# 테스트 출력
print(sess.run(W))
print(sess.run(tf.matmul(W, x_data)))

for step in range(2001):

sess.run(train)

if step%20 == 0:
print(step, sess.run(cost), sess.run(W), sess.run(b)) sess.close()

[출력 결과] [[ 0.23744965 -0.22514296]] [[ 0.23744965 -0.45028591 0.71234894 -0.90057182 1.18724823]] ... 1920 1.42109e-14 [[ 1. 0.99999994]] [ 1.75134133e-07] 1940 1.42109e-14 [[ 1. 0.99999994]] [ 1.75134133e-07] 1960 1.42109e-14 [[ 1. 0.99999994]] [ 1.75134133e-07] 1980 1.42109e-14 [[ 1. 0.99999994]] [ 1.75134133e-07] 2000 1.42109e-14 [[ 1. 0.99999994]] [ 1.75134133e-07]

x_data는 2행 5열, W는 1행 2열이다. 1행 2열 x 2행 5열의 결과는 주석에 있는 것처럼 1행 5열이 된다.텐서플로우에서 행렬 곱셈을 수행하는 함수는 matmul이다. cost를 구하기 위해 hypothesis - y_data는 행렬 뺄셈이다. 곱셈과 달리 같은 행과 열에 있는 요소를 빼면 된다. 다만 그러기 위해서는 같은 크기의 행렬이어야 하고, hypothesis와 y_data 크기가 같아야 한다. 모두 1행 5열이다. square 함수 또한 행렬 함수로 각 요소를 제곱하고, 최종적으로 reduce_mean 함수로 평균을 계산한다. W와 x_data의 순서를 바꿔서 tf.matmul(x_data, W)라고 쓰는 것은 에러다. 2행 5열 x 1행 2열은 행렬 곱셈을 수행할 수 없으니까.

테스트 출력을 통해 헷갈릴 수 있는 행렬 연산의 결과를 표시했다. W는 [0.23744965 -0.22514296]을 요소로 갖고 있는 1행 2열의 리스트이다. W와 x_data를 곱한 결과는 [0.23744965 -0.45028591 0.71234894 -0.90057182 1.18724823] 요소를 갖는 1행 5열의 리스트이다. 1개만 갖고 있기 때문에 1행이고, 그 안에 2개 또는 5개가 있어서 2열 또는 5열이다.

출력 결과는 앞의 코드와 똑같이 W1, W2에 해당하는 W 행렬은 결과가 모두 1이고, b는 0과 다름없다. feature(x 변수)가 많아질 때, 코드의 어떤 부분에 영향을 주는지 이해하는 것이 중요하다. 텐서플로우와 관련된 코드는 거의 달라지지 않는다. hypothesis를 구성하는 공식과 W의 결과를 출력하는 정도만 달라지고 있다.


마지막 코드다. 그동안 무시 아닌 무시를 당했던 y 절편에 해당하는 b를 행렬에 포함시켰다. 1행 4열 x 4행 1열의 행렬 곱셈으로 크기가 살짝 늘어났다. 그럼에도 행렬 곱셈이기 때문에 코드가 달라지진 않는다.

위의 공식은 y를 1개만 예측한다는 것을 정확히 이해해야 한다. X의 feature가 3개 있기 때문에 가중치인 W 또한 3개가 되고, 이들 전체를 계산해서 1개의 y 예측을 만들어 낸다. 여러 번에 걸친 데이터가 있다면, 초록색으로 표시된 W는 바뀌지 않지만, 빨간색으로 표시한 X는 오른쪽으로 길게 늘어지는 행렬이 되어야 한다. 5개의 데이터가 있다면, 3행 5열. 여기에 b라고 하는 feature 아닌 feature까지 추가되니까, 최종적으로는 4행 5열의 행렬이 만들어 진다.


import tensorflow as tf

# 앞의 코드에서 bias(b)를 행렬에 추가
x_data = [[1, 1, 1, 1, 1], [1., 0., 3., 0., 5.], [0., 2., 0., 4., 0.]] # 갯수가 같아야 하므로 b를 리스트로 처리
y_data = [1, 2, 3, 4, 5]

W = tf.Variable(tf.random_uniform([1, 3], -1.0, 1.0)) # [1, 3]으로 변경하고, b 삭제

hypothesis = tf.matmul(W, x_data) # b가 사라짐

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

cost = tf.reduce_mean(tf.square(hypothesis - y_data))

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

init = tf.initialize_all_variables()

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

for step in range(2001):

sess.run(train)

if step%20 == 0:
print(step, sess.run(cost), sess.run(W)) # b 사라짐. W에서 3가지 항목 출력 sess.close()
[출력 결과]
...
1920 4.83169e-14 [[  1.60856530e-07   9.99999940e-01   9.99999940e-01]]
1940 4.83169e-14 [[  1.60856530e-07   9.99999940e-01   9.99999940e-01]]
1960 4.83169e-14 [[  1.60856530e-07   9.99999940e-01   9.99999940e-01]]
1980 4.83169e-14 [[  1.60856530e-07   9.99999940e-01   9.99999940e-01]]
2000 4.83169e-14 [[  1.60856530e-07   9.99999940e-01   9.99999940e-01]]

y 절편 b를 추가해서 맨 앞의 코드 일부만 달라졌다. x_data는 3행 5열이 되었고, W는 1행 3열이 되었다. hypothesis를 구하는 코드에서 b가 사라진 것을 볼 수 있다. 이번 코드에서 가장 중요한 것은 b가 행렬에 포함되면서 여러 번 반복된 사실이다. 정확하게는 열의 갯수만큼으로 확장되었다.

  H(x) = Wx + b

이 코드는 b를 한 번만 더하는 코드같지만, 실제로는 X의 데이터 갯수만큼 더한다.

  x가 1일 때에 기울기를 곱한 결과에 b를 더한다.
  x가 2일 때에 기울기를 곱한 결과에 b를 더한다.
  x가 3일 때에 기울기를 곱한 결과에 b를 더한다.
  x가 4일 때에 기울기를 곱한 결과에 b를 더한다.
  x가 5일 때에 기울기를 곱한 결과에 b를 더한다.

이게 당연한 것이 W는 기울기일 뿐이고 말 그대로 기울어진 정도만을 표시한다. x가 1일 때의 기울기를 곱한 결과는 원점(0,0)에서의 y값이므로 여기에 y 절편 b(bias)를 더해서 이동을 시켜야 한다. 설명이 이해가 안 되면, y = 2x + 3의 그래프를 그려보면 된다.

  x = [1, 2, 3, 4, 5]
  y = [5, 7, 9, 11, 13]

여기서 W*x에 해당하는 y = 2x의 그래프를 그리면, 왜 매번 3을 더해야 하는지를 알 수 있을 것이다.


예제에서 사용할 데이터 파일 03train.txt

import tensorflow as tf
import numpy as np

# 03train.txt
# #x0 x1 x2 y
# 1 1 0 1
# 1 0 2 2
# 1 3 0 3
# 1 0 4 4
# 1 5 0 5

# #으로 시작하는 첫 번째 줄은 주석으로 판단하고 읽지 않음
xy = np.loadtxt('03train.txt', unpack=True, dtype='float32')
x_data = xy[:-1]
y_data = xy[-1]

# 5행 4열로 구성된 파일이지만, numpy에서 읽어오면 4행 5열이 된다.
# 그래야 열 단위로 행렬 연산을 수행할 수 있다.
print(type(xy)) # <class 'numpy.ndarray'>
print(xy.shape) # (4, 5)
print(len(x_data)) # 3

# W는 1x(x_data 크기) 매트릭스
W = tf.Variable(tf.random_uniform([1, len(x_data)], -1, 1))

hypothesis = tf.matmul(W, x_data)

cost = tf.reduce_mean(tf.square(hypothesis - y_data))

rate = tf.Variable(0.1) # learning rate, alpha
optimizer = tf.train.GradientDescentOptimizer(rate)
train = optimizer.minimize(cost) # goal is minimize cost

init = tf.initialize_all_variables()

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

for step in range(2001):

sess.run(train)

if step % 20 == 0:
print(step, sess.run(cost), sess.run(W)) sess.close()

이전 코드의 파일 버전이다. 출력 결과는 이전과 똑같기 때문에 생략한다.앞에서는 x_data와 y_data의 데이터를 직접 값을 넣는 방식으로 채웠지만,앞으로는 파일에서 직접 가져오는 방식을 사용하게 된다. 데이터가 너무 많으니까.

데이터를 읽어오는 부분만 보면 된다. numpy에 포함된 loadtxt 함수를 호출해서 파일을 읽어왔다. 정상적으로 읽으면 5행 4열이 되고, 왼쪽 3열과 오른쪽 1열을 분리해서 x_data와 y_data로 사용해야 한다. 그런데, 이 방식은 데이터를 분리하기가 조금 피곤하다. transpose시켜서 읽으면 4행 5열이 되고, 분리하기가 쉽다. unpack 파라미터를 사용하지 않으면 원본 그대로, True를 전달하면 transpose해서 읽는다.


x_data = [[1, 1, 1, 1, 1], [1., 0., 3., 0., 5.], [0., 2., 0., 4., 0.]] 

앞에서는 이렇게 행렬을 구성했다. 이 데이터는 원래 5행 3열로 생성되어야 하지만, 실제로는 3행 5열이다. 행렬 연산을 열 기준으로 적용해야 하기 때문에 발생하는 현상이다.

파이썬에는 정말 괜찮은 음수 인덱스 기능이 있어서 마지막을 가리킬 때는 -1번째, 마지막에서 두 번째는 -2번째가 된다. 여기에 슬라이싱(slicing)을 사용해서 range 함수와 동일한 기능을 수행할 수도 있다.

  xy[1]      1번째 요소
  xy[1:3]   1번째와 2번째 요소. 3은 end 인덱스로 범위에 포함되지 않는다.
  xy[1::2]  1번째부터 2칸씩 건너뛴 모든 요소. 비워둔 칸은 전체의 뜻을 가짐.

여기서는 xy[:-1]을 했으므로 처음부터 마지막 이전까지의 모든 요소가 된다. end 인덱스는 포함하지 않으니까, -1번째는 포함되지 않는다. xy[-1]은 말 그대로 마지막 요소, 여기서는 마지막 열이 된다.


데이터를 읽어올 때, transpose시키지 않았을 때의 달라지는 부분을 적어 봤다. loadtxt 함수 호출에 unpack 파라미터가 빠졌다. 마지막 열을 제거하기 위한 코드도 복잡하고, matmul 함수 호출에서 x_data를 transpose시켜야 하는 것도 불편하다. 나머지는 달라지는 곳이 없다.

data = np.loadtxt('03rain.txt', dtype=np.float32)

x_data = data[:,:-1]
y_data = data[:,-1]

W = tf.Variable(tf.random_uniform([1, 3], -1, 1))

hypothesis = tf.matmul(W, tf.transpose(x_data))