‘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을 썼어야지!! ㅋㅋㅋ
빵터지고 갑니다 ㅋㅋㅋㅋㅋ
좋아요Liked by 1명