16. TensorFlow로 Softmax Classification의 구현하기 (lab 06)

이번 글에서는 텐서플로우에서 제공하는 softmax 알고리듬과 이를 적용한 결과를 살펴본다.


아래는 동영상에서 나온 코드에 주석을 붙인 전체 소스코드이다. 05train.txt

import tensorflow as tf
import numpy as np

# softmax이기 때문에 y를 표현할 때, 벡터로 표현한다.
# 1개의 값으로 표현한다고 할 때, 뭐라고 쓸지도 사실 애매하다.

# 05train.txt
# #x0 x1 x2 y[A B C]
# 1 2 1 0 0 1 # C
# 1 3 2 0 0 1
# 1 3 4 0 0 1
# 1 5 5 0 1 0 # B
# 1 7 5 0 1 0
# 1 2 5 0 1 0
# 1 6 6 1 0 0 # A
# 1 7 7 1 0 0

xy = np.loadtxt('05train.txt', unpack=True, dtype='float32')

# xy는 6x8. xy[:3]은 3x8. 행렬 곱셈을 하기 위해 미리 transpose.
x_data = np.transpose(xy[:3])
y_data = np.transpose(xy[3:])

print('x_data :', x_data.shape) # x_data : (8, 3)
print('y_data :', y_data.shape) # y_data : (8, 3)

X = tf.placeholder("float", [None, 3]) # x_data와 같은 크기의 열 가짐. 행 크기는 모름.
Y = tf.placeholder("float", [None, 3]) # tf.float32라고 써도 됨

W = tf.Variable(tf.zeros([3, 3])) # 3x3 행렬. 전체 0.

# softmax 알고리듬 적용. X*W = (8x3) * (3x3) = (8x3)
hypothesis = tf.nn.softmax(tf.matmul(X, W))

# cross-entropy cost 함수
cost = tf.reduce_mean(-tf.reduce_sum(Y * tf.log(hypothesis), reduction_indices=1))

learning_rate = 0.01
train = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost)

init = tf.initialize_all_variables()

with tf.Session() as sess:
sess.run(init)

for step in range(2001):
sess.run(train, feed_dict={X: x_data, Y: y_data})
if step % 200 == 0:
feed = {X: x_data, Y: y_data}
print('{:4} {:8.6}'.format(step, sess.run(cost, feed_dict=feed)), *sess.run(W))

print('-------------------------------')

# 1은 bias로 항상 1. (11, 7)은 x 입력
a = sess.run(hypothesis, feed_dict={X: [[1, 11, 7]]})
print("a :", a, sess.run(tf.argmax(a, 1))) # a : [[ 0.68849683 0.26731509 0.04418806]] [0]

b = sess.run(hypothesis, feed_dict={X: [[1, 3, 4]]})
print("b :", b, sess.run(tf.argmax(b, 1))) # b : [[ 0.2432227 0.44183081 0.3149465 ]] [1]

c = sess.run(hypothesis, feed_dict={X: [[1, 1, 0]]})
print("c :", c, sess.run(tf.argmax(c, 1))) # c : [[ 0.02974809 0.08208466 0.8881672 ]] [2]

# 한번에 여러 개 판단 가능
d = sess.run(hypothesis, feed_dict={X: [[1, 11, 7], [1, 3, 4], [1, 1, 0]]})
print("d : ", *d, end=' ')
print(sess.run(tf.argmax(d, 1))) # d : ... [0 1 2]
[출력 결과]
...
1200 0.780959 [-1.06231141 -0.26727256 1.32958329] [ 0.06808002 -0.11823837 0.05015869] [ 0.17550457 0.23514736 -0.41065109]
1400 0.756943 [-1.19854832 -0.29670811 1.49525583] [ 0.0759144 -0.11214781 0.0362338 ] [ 0.19498998 0.23733102 -0.43232021]
1600 0.735893 [-1.32743549 -0.32218221 1.64961684] [ 0.08333751 -0.10558002 0.02224298] [ 0.21336637 0.23823628 -0.45160189]
1800 0.717269 [-1.44994986 -0.34407791 1.79402602] [ 0.09020084 -0.09902247 0.00882212] [ 0.23099622 0.2384187 -0.46941417]
2000 0.700649 [-1.56689751 -0.36275655 1.92965221] [ 0.09643653 -0.09271803 -0.00371794] [ 0.248116 0.23818409 -0.48629922]
-------------------------------
a : [[ 0.68849683 0.26731509 0.04418806]] [0]
b : [[ 0.2432227 0.44183081 0.3149465 ]] [1]
c : [[ 0.02974809 0.08208466 0.8881672 ]] [2]
d : [ 0.68849683 0.26731509 0.04418806] [ 0.2432227 0.44183081 0.3149465 ] [ 0.02974809 0.08208466 0.8881672 ] [0 1 2]

핵심적인 부분만 설명하고, 나머지는 주석으로 대신한다. 이전 글들의 소스코드에서 설명된 내용 또한 생략한다. 출력 결과에서 W는 3행 3열이기 때문에 총 9개의 결과로 나타나고 있다. 많다고 생각할 수도 있지만, 실전에서는 수천 개의 weight을 다루게 되고, 지금처럼 출력할 생각은 하지 않게 된다. 몇 개가 됐든 가중치는 변경되어서 최저 cost를 찾아가야 한다.


# xy는 6x8. xy[:3]은 3x8. 행렬 곱셈을 하기 위해 미리 transpose.
x_data = np.transpose(xy[:3])
y_data = np.transpose(xy[3:])

loadtxt 함수는 파일을 읽을 때 행과 열을 바꿔서 읽어오기 때문에, 행렬 곱셈을 위해 먼저 transpose 시켰다. y_data에 들어가는 값이 1차원이 아니라 2차원이 됐다. y_data는 3번째부터 전체라고 표현했으니까, 6행 8열의 데이터에서 뒤쪽에 오는 3행 8열을 transpose 시켜서 8행 3열이 되었다.


X = tf.placeholder("float", [None, 3])  # x_data와 같은 크기의 열 가짐. 행 크기는 모름.
Y = tf.placeholder("float", [None, 3]) # tf.float32라고 써도 됨

W = tf.Variable(tf.zeros([3, 3])) # 3x3 행렬. 전체 0.

2차원 배열을 placeholder로 지정하는 문법으로 여기서 처음 나왔다. 앞에 오는 None은 행의 갯수를 알 수 없기 때문이고, 열의 갯수는 언제나 3이어야 한다. 행은 한두 개 없어도 될만큼 가볍지만, 열은 feature로 나타나는 핵심 요소라는 것을 기억하자. 설마 행이 8개라고 생각하는 것은.. 아니겠지? W는 binary classification이 3개 들어가야 하니까, 3행 3열로 표현했다.

만약에 feature가 하나 늘어난다면, 다시 말해 x_data가 8x3이 아니라 8x4가 된다면 W는 행이 증가할까, 열이 증가할까?

W에서 행은 binary classification을, 열은 feature 갯수를 의미한다. 4가지 중에서 골라야 한다면 행이 증가하고, x의 특징을 가리키는 열, 즉 feature가 추가됐다면 열이 증가한다.


# softmax 알고리듬 적용. X*W = (8x3) * (3x3) = (8x3)
hypothesis = tf.nn.softmax(tf.matmul(X, W))

softmax 알고리듬이다. W와 X를 곱하는 게 아니라, X와 W를 곱하고 있다. X는 8행 3열, W는 3행 3열. 행렬 곱셈으로 손색이 없다. W와 X의 위치를 바꾸면 당연히 에러다. X의 각 행에는 x0(bias)와 x1, x2가 들어 있고, 이 값을 토대로 y를 예측해야 한다. 그러니 X의 행과 W의 열을 곱하는 것이 맞다.

x_data를 transpose 시키지 않고 읽어온 그대로 사용한다면 matmul(W, X)라고 해야 한다. W를 앞에 두는 것이 편하기 때문에 이게 좋아보일 수도 있다. 이럴 경우 W는 행으로 동작하고, X는 열로 동작하게 된다. 3행 3열 x 3행 8열의 행렬 곱셈이 되어서 결과는 3행 8열이 된다. 그런데, 결과를 추출할 때 불편하다. 값이 열 단위로 되어 있기 때문에 각각의 행에서 하나씩 가져와서 조합해야 판단이 가능해진다. 별건 아니지만, 이런 불편한 점 때문에 W와 X를 바꿔서 계산을 한다.

또 하나, W를 뒤에 곱할 때는 열 단위로 읽게 되는데, 지금은 앞에 나왔기 때문에 행 단위로 계산을 해야 한다. transpose할 필요까지는 없겠지만, 이 사실을 알고 처음 값을 줄 때 열 단위로 주어야 한다는 점은 이해해야 한다.


# cross-entropy cost 함수
cost = tf.reduce_mean(-tf.reduce_sum(Y * tf.log(hypothesis), reduction_indices=1))

cross-entropy cost 함수의 텐서플로우 버전이다. log 함수를 호출해서 hypothesis를 처리하고 있다. hypothesis는 softmax를 거쳤으므로 0과 1 사이의 값들만 갖게 된다.

여기서 Y는 공식에서는 L(label)로 표현되었던 값으로 실제 y 값을 의미한다. 즉, L * log(S)를 처리하는 부분이 reduce_sum에 Y * tf.log(hypothesis)이다. 곱셈(*)은 행렬 곱셈이 아닌 element-wise 곱셈이다. 텐서플로우의 행렬 곱셈은 matmul 함수이다.


Y * tf.log(hypothesis) 결과는 행 단위로 더해야 한다. 그림에서 보면, 최종 cost를 계산하기 전에 행 단위로 결과를 더하고 있다. 이것을 가능하게 하는 옵션이 reduction_indices 매개변수다. 0을 전달하면 열 합계, 1을 전달하면 행 합계, 아무 것도 전달하지 않으면 전체 합계. 이렇게 행 단위로 더한 결과에 대해 전체 합계를 내서 평균을 구하기 위해 reduce_mean 함수가 사용됐다.


# 1은 bias로 항상 1. (11, 7)은 x 입력
a = sess.run(hypothesis, feed_dict={X: [[1, 11, 7]]})
print("a :", a, sess.run(tf.argmax(a, 1))) # a : [[ 0.68849683 0.26731509 0.04418806]] [0]

학습한 결과를 토대로 학점을 예측하고 있다. 이 학생은 A를 맞았다고 나오는 것이 아니라 A를 가리키는 인덱스인 0을 반환하고 있다. argmax 함수는 one-hot encoding을 구현하는 텐서플로우 함수다.