Fizz Buzz in TensorFlow

Data Science from Scratch‘ 의 저자 Joel Grus가 블로그에 fizz buzz 문제를 다루는 면접 상황에서 엉뚱하게 텐서플로우를 이용해 대답하는 가상의 취업준비생의 이야기를 올렸습니다. 아주 재미있네요 🙂


면접관: 어서오세요. 커피나 다른 거 뭐 드릴까요? 한숨 돌리고 하실래요?

나: 아닙니다. 오늘 커피를 너무 많이 먹었어요.

면접관: 네, 좋습니다. 화이트보드에 코드를 적어야 하는데 괜찮으시죠?

나: 네 전 그렇게만 코딩해 봤어요!

면접관:

나: 농담입니다.

면접관: 좋습니다. 피즈버즈에 대해선 들어 보셨나요?

나:

면접관: 들어본건가요? 아닌가요?

나: 질문하신 내용이 믿기지 않는 쪽입니다.

면접관: 좋아요, 1에서 100까지 숫자를 프린트하는데 3으로 나누어지는 수는 ‘fizz’ 라고 프린트하고 5로 나누어지는 수는 ‘buzz’라고 프린트하고 15로 나누어지는 수는 ‘fizzbuzz’라고 프린트하면 됩니다.

나: 그 문제 압니다.

면접관: 다행이군요. 이걸 제대로 할 수 없는 사람은 회사 업무도 잘 못하더군요.

나:

면접관: 여기 마커랑 지우개가 있어요.

나: [몇분간 생각]

면접관: 어떻게 시작할지 좀 도와드릴까요?

나: 아뇨, 아닙니다. 괜찮아요. 그럼 기본적인 임포트부터 시작하겠습니다.:

import numpy as np
import tensorflow as tf

면접관: 음, 피즈버즈 문제 알고 있는 거 맞죠?

나: 그럼요. 그래서 지금 모델에 대해 말하려구요. 제 생각엔 하나의 히든 레이어를 갖는 간단한 멀티 레이어 퍼셉트론(multi-layer-perceptron)이 어떨까합니다.

면접관: 퍼셉트론요?

나: 아니면 뉴럴 네트워크나 뭐라 불러도 상관은 없어요. 입력은 숫자고 출력은 숫자에 대한 정확한 ‘fizzbuzz’ 표현이 되죠. 특별히 각 입력을 활성화(activation) 벡터로 변경할 필요가 있어요. 간단한 방법은 이진값으로 바꾸는 거죠.

면접관: 이진값이요?

나: 네, 아시겠지만 0 또는 1 이요. 이렇게요:

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

(역주: 이 코드는 숫자의 이진수 표현의 각 자리의 값을 num_digits 크기의 배열로 만듭니다. 즉 3 은 [1, 1, 0, 0, 0, …] 이 됩니다.)

면접관: [몇분동안 화이트보드를 응시한다]

나: 그리고 출력은 각 숫자에 대한 피즈버즈 표현을 담은 원핫 인코딩(one-hot encoding)을 사용합니다. 첫번째 위치는 숫자 그대로 이고 두번째는 ‘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까지를 이용해 훈련을 시킵니다:

NUM_DIGITS = 10
trX = np.array([binary_encode(i, NUM_DIGITS) for i in range(101, 2 ** NUM_DIGITS)])
trY = np.array([fizz_buzz_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])

그러면 모델을 정의할 준비가 되었어요. 히든 레이어 하나라고 했었죠. 그리고 잘 모르지만 렐루 활성화 함수를 사용하죠:

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

그리고 크로스 엔트로피(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)

면접관: 너무 다른데로 가는 거 같은데 풀어야할 문제는 1에서 100까지 숫자에 대해 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번 정도 반복시킬까요?

면접관:

나: 네, 아마도 충분치 않을 거 같네요. 그럼 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))

그리고 출력 값을 fizz_buzz 함수에 적용해서 결과를 얻으면 됩니다:

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

    print(output)

면접관:

나: 그러면 정확한 fizz buzz가 출력됩니다!

면접관: 정말 됐어요. 나중에 연락하겠습니다.

나: 연락 주신다니 감사합니다.

면접관:

후기

난 거기에 취업하지 못했다. 실제로 이걸(code on GitHub) 실행해 봤는데 몇개가 잘못된 값이 나와버렸다! 머신러닝 고마워!

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')

내 생각엔 아마도 더 많은 히든 레이어(deeper network)를 사용했어야 했나 보다…

 

(추가) Jeff Dean 왈, RNN을 썼어야지!! ㅋㅋㅋ

https://plus.google.com/+JeffDean/posts/KyRavz9tT7F

Fizz Buzz in TensorFlow”에 대한 1개의 생각

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.