[번역] Tensorflow로 369게임하기


Joel Grus 라는 전직 구글 SWE가 올린 재미난 글이 있어서 번역해서 올립니다 :)


면접관 : 환영합니다. 커피나 필요한 것 있으신가요? 좀 쉬고 싶으세요?

: 아뇨. 커피는 이미 너무 많이 마신 것 같네요!

면접관 : 좋습니다. 그럼 혹시 화이트보드에 코딩하는 거 괜찮으신가요?

: 전 그렇게밖에 코딩 안 해요!

면접관 : …

: 장난이에요.

면접관 : 그래요. 혹시 369게임이라고 들어보셨어요?

: …

면접관 : 알아요, 몰라요?

: 그걸 물어본다는 사실이 믿기지가 않네요.

면접관 : 그러니까, 저는 1 부터 100까지 출력을 하고 싶은데 이 중에서 3으로 나눠지는 숫자면 숫자 대신 “fizz”를 출력하고 5로 나눠지는 숫자면 “buzz”를 출력하고 15로 나눠지는 숫자면 “fizzbuzz”를 출력하는 거에요.

: 네 알아요.

면접관 : 좋습니다. 이걸 못하는 지원자분들이 꽤 많더라구요.

: …

면접관 : 여기 마커와 지우개가 있습니다.

: [몇 분 동안 생각함]

면접관 : 혹시 시작하는 데에 도움이 필요하신가요?

: 아뇨, 아뇨. 괜찮습니다. 먼저 몇 가지 패키지를 불러오구요..

import numpy as np
import tensorflow as tf

면접관 : 음.. 이 문제가 369게임인 건 알고 있죠?

: 당연히 알죠. 그러니까, 모델에 대한 얘기를 해보자구요. 저는 지금 하나의 은닉계층을 가진 다중계층 퍼셉트론을 생각하고 있어요.

면접관 : 퍼셉트론이요?

: 음, 아니면 신경망이요. 뭐라고 부르던간에. 우리는 숫자를 입력할 거고, 출력으로 “fizzbuzz”로 3이나 5의 배수가 고쳐지는 걸 원하는 거잖아요? 그러려면 각 입력을 “활성화”를 위한 벡터로 놓고요. 쉽게 변환하는 방법으로 이진수가 있겠네요.

면접관 : 이진수요?

: 네. 알잖아요. 0이랑 1? 이렇게 생긴 거요.

def binary_encode(i, num_digits):
  return np.array([i >> d & i for d in range(num_digits)])

면접관 : [화이트보드를 1분정도 응시한다]

: 그러면 fizzbuzz의 그 숫자들은 one-hot 인코딩 되어 출력으로 나올 거에요. 첫 번째는 그대로 출력, 두 번째는 fizz, 그런 식으로요.

def fizz_buzz_encode(i):
  if   i % 15 == 0 : return np.array([0,0,0,1])
  elif i % 5  == 0 : return np.array([0,0,1,0])
  elif i % 3  == 0 : return np.array([0,1,0,0])
  else :             return np.array([1,0,0,0])

면접관 : 아, 이제 충분한 것 같네요.

: 네. 이거면 기본 설정이 다 끝났죠. 정확해요. 이제 우리는 학습 데이터를 만들어야 돼요. 1부터 100까지 숫자를 활용하면 사기일테니 1024까지의 숫자 중에 1부터 100을 제외한 숫자를 사용하도록 하죠.

NUM_DIGITS = 10
trX = np.array([binary_encode(i, NUM_DIGITS) for i in range(101, 2 ** NUM_DIGITS)])
trY = np.array([binary_encode(i)             for i in range (101, 2 ** NUM_DIGITS)])

면접관 : …

: 그럼 이제 이 모델을 텐서플로우에 적용시켜야겠죠. 음, 히든 유닛을 몇 개나 써야될지 감이 잘 안 오네요. 한 10개?

면접관 : …

: 그래요 한 100개면 더 좋겠네요. 나중에 바꿔도 되구요.

NUM_HIDDEN = 100

NUM_DIGITS 만큼의 폭을 가진 입력 변수가 필요하겠네요. 출력 변수의 폭은 4가 되겠구요:

X = tf.placeholder("float", [None, NUM_DIGITS])
Y = tf.placeholder("float", [None, 4])

면접관 : 이거 얼마나 걸리나요?

: 아, 두 개의 심층 – 하나의 은닉 계층과 출력 계층.. 저희 뉴론에 무작위 초기화된 뉴런을 넣어보죠.

def init_weights(shape):
    return tf.Variable(tf.random_normal(shape, stddev=0.01))

w_h = init_weights([NUM_DIGITS, NUM_HIDDEN])
w_o = init_weights([NUM_HIDDEN, 4])

: 이제 모델을 정의할 준비가 됐네요. 전에 말했던 것처럼, 하나의 은닉 계층으로요. 잘 모르겠지만, ReLU 활성화 :

def model(X, w_h, w_o):
    h = tf.nn.relu(tf.matmul(X, w_h))
    return tf.matmul(h, w_o)

softmax cross-entropy를 비용함수로 이용해서 최소화할 수도 있겠네요:

py_x = model(X, w_h, w_o)

cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(py_x, Y))
train_op = tf.train.GradientDescentOptimizer(0.05).minimize(cost)

면접관 : …

: 그리고 예측 값이 출력되야겠죠..

predict_op = tf.argmax(py_x, 1)

면접관 : 음, 당신이 저 멀리로 가버리기 전에 말하겠는데 당신이 풀어야 하는 문제는 “fizz buzz”를 알맞은 때에 출력하는 거에요.

: 아, 그 점이 중요하죠. predict_op 함수가 0에서 3사이의 값을 출력하면 그걸 fizz랑 buzz로 바꿔야죠.

def fizz_buzz(i, prediction):
    return [str(i), "fizz", "buzz", "fizzbuzz"][prediction]

면접관 : …

: 이제 모델을 훈련시킬 준비가 다 됐네요. 텐서플로우 세션을 켜고 변수를 초기화시키자구요.

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

: 이제 실행하죠. 1000번(epoch) 훈련시키면 되나요?

면접관 : …

: 맞아요. 좀 모자랄 수도 있겠네요. 10000번쯤 해보죠 안전하게. 그리고 저희 트레이닝 데이터가 조금 순차적인데 그게 맘에 안드니 섞어보죠..

for epoch in range(10000):
    p = np.random.permutation(range(len(trX)))
    trX, trY = trX[p], trY[p]

우리는 일괄처리를 하고 싶은데 사이즈를 몇으로 할까요.. 128?

BATCH_SIZE = 128

그럼 학습은 이렇게 되겠네요.

for start in range(0, len(trX), BATCH_SIZE):
    end = start + BATCH_SIZE
    sess.run(train_op, feed_dict={X: trX[start:end], Y: trY[start:end]})

그리고 학습 데이터에 정확도를 출력할 수도 있겠죠.

print(epoch, np.mean(np.argmax(trY, axis=1) ==
                     sess.run(predict_op, feed_dict={X: trX, Y: trY})))

면접관 : 진심이세요?

: 물론이죠. 단계마다 정확도를 출력하는 게 꽤 유용하거든요.

면접관 : …

: 그래서 모델이 학습하면 이제 fizz buzz 시간이죠! 입력은 1에서 100까지의 숫자가 되겠네요.

numbers = np.arange(1, 101)
teX = np.transpose(binary_encode(numbers, NUM_DIGITS))

teY = sess.run(predict_op, feed_dict={X: teX})
output = np.vectorize(fizz_buzz)(numbers, teY)

print(output)

: 그럼 369게임 완성했습니다!

면접관 : 정말이네요. 충분합니다. 연락 드리도록 하죠.

: 오, 연락.. 그 말 참 기대되네요.

면접관 : …

추신

저는 취직하지 못했어요. 그래서 이 코드를 직접 실행해보기로 했죠. 근데 뭔가 이상한 출력을 하는 것 같네요. 고마워 기계학습!

In [185]: output
Out[185]:
array(['1', '2', 'fizz', '4', 'buzz', 'fizz', '7', '8', 'fizz', 'buzz',
       '11', 'fizz', '13', '14', 'fizzbuzz', '16', '17', 'fizz', '19',
       'buzz', '21', '22', '23', 'fizz', 'buzz', '26', 'fizz', '28', '29',
       'fizzbuzz', '31', 'fizz', 'fizz', '34', 'buzz', 'fizz', '37', '38',
       'fizz', 'buzz', '41', '42', '43', '44', 'fizzbuzz', '46', '47',
       'fizz', '49', 'buzz', 'fizz', '52', 'fizz', 'fizz', 'buzz', '56',
       'fizz', '58', '59', 'fizzbuzz', '61', '62', 'fizz', '64', 'buzz',
       'fizz', '67', '68', '69', 'buzz', '71', 'fizz', '73', '74',
       'fizzbuzz', '76', '77', 'fizz', '79', 'buzz', '81', '82', '83',
       '84', 'buzz', '86', '87', '88', '89', 'fizzbuzz', '91', '92', '93',
       '94', 'buzz', 'fizz', '97', '98', 'fizz', 'fizz'],
      dtype='<U8')

어쩌면 다음엔 더 깊은 네트워크를 써야 겠어요..