brunch

You can make anything
by writing

C.S.Lewis

by Mobiinside Nov 06. 2017

딥러닝 (Tensorflow) 을 이용한 추천 시스템

Buzzvil 블로그에 소개된 글을 편집한 뒤 모비인사이드에서 한 번 더 소개합니다.


by 곽상훈 버즈빌 매니저


1.  글을 시작하기 전에

안녕하세요, 모바일 잠금화면 애드네트워크 버즈빌의 컨텐츠와 머신러닝 product manager 곽상훈(Mike) 입니다. 버즈빌은 딥러닝을 이용하여 개인화된 컨텐츠를 자동 추천, 사용자 경험을 도우며 광고 플랫폼으로서의 기술적 우위를 보유하고 있습니다. 앞으로 위 기술과 관련한 로직의 개발 과정과 결과를 소개하려고 합니다. 기초적인 neural network background 가 있다는 가정하에 코드는 최소화 하고 머신 러닝 모델의 high level design 위주로 기술하였으며, 자세한 수식/증명/예시/코드 등에 관해서는 도움이 될만한 참고 링크들을 첨부했습니다. 


2. 배경


10 년전만 해도 흔히 쓰이던 인공지능 기술들은 보편적인 기계 학습에 쓰이기엔 부족함이 많았습니다. 그 원인 중 하나는 자동 feature selection 의 부재였습니다. 자연어 처리를 위한 feature 들 (e.g. 형태소 분석), image recognition 을 위한 feature 들 (e.g. edge detection) 등, specialized 된 목적을 위해 각 분야의 전문가들이 제작한 handmade feature 들에 의존을 많이 했기 때문입니다. 최근에 재조명을 받고 있는 neural network — 혹은 deep learning — 은 자동 feature learning 을 통하여 이 고민을 해결해 주어 분야에 관계 없이 보편적인 기계 학습을 하기에 조금 더 적합한 기술입니다. Black box 라서 모델을 직관적으로 이해하기 어렵고, training 시간도 오래 걸리고, parameter tuning 에 많은 노력이 소요되는 등의 단점들이 있지만, 전반적인 pattern recognition 에 있어서는 기가 막힌 효과를 보인다는 것은 이미 증명이 되었습니다(참고). 처음 RNN 을 접하고 제 맥북으로 [이문열의 삼국지] 10권을 input 으로 위 링크에서 소개한 실험들과 비슷한 실험을 했는데 그럴싸한 결과가 나왔습니다 (따옴표, 물음표, 느낌표가 절묘합니다).


그 말을 듣자 원소의 말에 조조는 원소에게 물었다.
 “너는 어디 없이 이렇게 말했다. 이 말 아래로 돌피지 않으면 반드시 그 군사들을 죽이려 드는 것이 있다. 이번에는 원소와 함께 그들을 치고 달리 어찌 한가지는 것이냐!”
 그 말을 들고 조조가 다시 물었던 것이다. 
 “저희들은 모두 군량이 있으니 어떤 것이 오늘이오?” 원소는 조조가 그렇게 대답하며 말했다. 
 “이 분은 무슨 말이냐?” 
 “그 자리 같은 사졸들을 죽여 그 말을 듣고 있습니다. 이미 저것입니다”


그럼 거두절미하고 이 강력한 툴을 이용하여 버즈빌에서 만든 추천 로직을 소개 하겠습니다. 


3. Application


버즈빌이 미국과 유럽에서 서비스 중인 Slidejoy에서는 유저들을 위해 락스크린에 광고 외에도 다양한 컨텐츠를 제공하고 있습니다. 200 개 이상의 content provider 들로부터 매 시간 10,000 개가 넘는 기사를 저장, 정제하여 카테고리 별로 유저들에게 제공합니다 (참고). 초기 (neural network 적용 이전) 에는 컨텐츠들을 간단한 unigram tf-idf 방식으로 grouping 하여 가장 많이 mention 된 토픽 순서로 모든 유저들에게 일괄적으로 보여줬습니다.

[Image Source: 테크 크런치 2016년 1월 기사, Lockscreen App Slidejoy Gets A Newsy New Feature]

 

하지만, 그 이후 보편적으로 popular한 topic 외에도 개개인이 관심 있어할 만한 컨텐츠 제공의 필요성을 느껴 deep learning 을 이용하여 개인화 로직 개발을 시작했습니다. 모든 개인화 로직 개발은 최근에 빠른 adoption 을 보이고 있는 Google 의 Tensorflow 를 이용하였습니다. 


4. 모델 설계


1) 데이터 수집: Slidejoy 컨텐츠를 제공함으로써 수집하는 데이터는 크게 다음과 같습니다. 


- 유저 아이디, 성별, 나이 등의 기본 유저 데이타

- 카테고리, provider 아이디, 제목, description 등의 컨텐츠 데이타

- 컨텐츠 display 순서, 시간 및 클릭 여부 등 유저 interaction 데이타


Tensorflow 의 training input 으로 사용하기 쉽게 날짜별로 위의 데이타를 tfrecord (참고) 파일에 저장했습니다. 초기 training 을 위해서 총 3달 분량의 데이타를 사용했습니다.


2) 모델 components: Tensorflow 에서는 먼저 데이타를 담을 tensor (간단하게 vector 로 생각하면 됩니다) 들을 이용하여 데이타의 흐름 (input 부터 back propagation 까지)을 정의하는 그래프를 만듭니다.


그래프 정의 시에는 모든 tensor 관련 연산은 tensorflow 함수를 이용해야 합니다 (참고). 일반적으로 쓰이는 numpy 나 list 연산은 그래프에 적용이 안됩니다.


* Objective

컨텐츠의 추천 로직은 기본적으로 ordering 을 유저의 취향에 맞게 하는 것이 목표 입니다. 유저의 취향은 여러 방식으로 정의할 수 있지만, 이 프로젝트에서는 유저의 컨텐츠 클릭 여부로 정했습니다. 유저/컨텐츠 정보를 input 으로 하여 neural network 가 최종적으로 유저가 클릭할 확률을 계산 하도록 했습니다.


* Input

일별로 구분된 training data set 을 랜덤하게 섞어서 모델에 feed 할 수 있도록 Tensorflow 에서 제공하는 shuffle_batch 함수를 사용했습니다. 앞에서 언급했듯이 저는 input 파일들을 tfrecord 포맷으로 저장하는데요, binary 포맷이라서 일반 csv/text 파일보다 월등히 빠릅니다. Tensorflow 의 batching 은 queue/dequeue 방식을 사용합니다. (참고) 이를 사용하기 위해서는


[Image Source: TensorFlow, ‘Reading Data’]


1.  tf.train.string_input_producer 에 필요 파일들을 filename queue 에 추가하고                    

filename_queue = tf.train.string_input_producer(files)

2.  Input producer 에서 dequeue 된 파일들로부터 데이타 포맷 (csv, tfrecord 등) 에 맞는 reader 를 통해서 생성된 example 을 또 queue 에 추가합니다.                    

reader = tf.TFRecordReader()_, serialized_example = reader.read(filename_queue)

3.  최종적으로 shuffle_batch/batch 를 통해서 queue 에 있는 example 들을 dequeue 하여 실질적으로 데이타를 graph 에 feed 해 줄 수 있습니다. (Capacity/min_after_dequeue 설정과 관련해서는 위 참고 링크에 자세하게 설명이 되어 있습니다.)        

features = tf.train.shuffle_batch(   tensors=[serialized_example],   batch_size=batch_size,   capacity=capacity,   min_after_dequeue=min_after_dequeue,)

4.  Data feed 는 placeholder 에만 할 수 있습니다. Feed 를 받을 placeholder 들을 그래프 생성시에 define 해주면 됩니다.            
        

# placeholder 들 생성 label = tf.placeholder(tf.int32, [None, ], name='y-input')…
# queue runner 시작 및 feature 추출 sess = tf.Session()coord = tf.train.Coordinator()threads = tf.train.start_queue_runners(sess=sess, coord=coord)features = sess.run(features)
# Cost/loss 계산을 위해 batch output 을 placeholder 들에 feed feed_dict = {    label = features[0].values,     …}
# feature 를 이용해서 cost 계산 sess.run(cost, feed_dict=feed_dict)

Embedding 처리

Sparse 한 feature (유저 아이디, 컨텐츠 카테고리 아이디, 유저 나이, 시간 등) 는 모두 embedding 처리합니다. Embedding 은 간단히 말해서 sparse 한 데이타를 dense 한 high dimensional vector space 에 프로젝트 해주는 lookup table/matrix 입니다. Embedding matrix 자체를 variable 로 설정하면 neural network 가 learning 을 하면서 weight 들과 함께 embedding matrix 도 업데이트가 되고, 결과적으로 비슷한 feature 들은 vector space 에서 grouping 되는 놀라운 현상이 발생합니다. Tensorflow 에서 embedding lookup 을 위한 함수들(참고)이 존재하여 손쉽게 embedding 을 적용시킬 수 있습니다.                    

embeddings = tf.get_variable(   'embedding',   initializer=tf.random_uniform([size, dimension], -1.0, 1.0))y = tf.nn.embedding_lookup(embeddings, feature) 


다만, 컨텐츠의 제목과 description 등의 단어들의 경우에는 variable 이 아니라 pre-trained 된 constant matrix (tf.constant) 를 사용합니다. 단어 embedding 의 경우 Wikipedia, Twitter 등에서 추출한 문서들을 이용하여 만들어진 많은 embedding matrix 들이 존재하고, 새로 embedding 을 training 해야 하는 수고를 덜 수가 있어서, pre-trained 된 embedding 을 사용했습니다. 구체적으로, Stanford 에서 제공하는 Wikipedia + Gigaword embedding 을 사용하였습니다. Word embedding 의 자세한 methodoloy 는 http://nlp.stanford.edu/projects/glove/에 소개 되어 있습니다. 참고 링크를 읽어 보시면 알겠지만, 같은 context 에서 자주 같이 등장하는 단어들은 vector space 에서 가깝게 위치하고, 심지어 간단한 vector 연산도 적용됩니다 (v_king – v_queen ~= v_man – v_woman).

   


Hidden Layers: 모델에는 크게 두가지 hidden layer 를 사용했습니다.


1. RNN layer


문장 feature (컨텐츠 제목 + description) 에는 RNN 을 적용시켰습니다. RNN 적용 이전에 주로 image recognition 에 쓰이는 CNN과 단방향 RNN 으로 실험을 해보았지만, 쌍방향 (bi-directional) RNN 의 효율이 가장 높았습니다. (하나의 layer 만을 사용했습니다) RNN cell 로는 기본 cell 의 vanishing/exploding gradient 문제를 보완한 LSTM (long short term memory) cell 을 사용했습니다. (참고)


Tensorflow에서 RNN 은 크게 두 가지 버전이 있습니다: 기본 (static) RNN 과 dynamic RNN. Dynamic RNN 은 그래프를 session 을 execute 할 때 만들어서 초기 그래프 생성 시간이 기본 RNN 보다 월등히 빠릅니다. 다만 documentation(참고)를 읽어 보시면 알겠지만, static RNN 은 output 을 계산할 때 sequence length 에 맞춰서 모든 time step 을 계산하지 않고 일찍 멈춥니다. 가령 컨텐츠 제목을 담은 embedded matrix 의 총 dimension이 100 이라고 했을 때, 총 소요되는 rnn timestep 도 일반적으로 100 입니다. 그러나 sequence length 를 5로 지정하면 (컨텐츠의 제목이 짧아서) 5 번째 timestep 까지만 계산하고 early stop 을 합니다. (thus, saving number of computations).


이와 다르게, dynamic rnn 에서는 early stop 을 안하고 지정된 sequence length 이후의 output 은 0 으로 padding 을 해줍니다. 두 함수의 input specification 도 다르기 때문에 유의하시기 바랍니다.


저는 초기 그래프 생성 시간이 그리 길지 않아서 그냥 static RNN 을 사용했습니다.           

         

def lstm_cell(layer_size):   return tf.nn.rnn_cell.LSTMCell(       num_units=layer_size,       forget_bias=1.0,       state_is_tuple=True   )outputs, final_state_fw, final_state_bw = tf.nn.bidirectional_rnn(   cell_fw=lstm_cell(layer_size),   cell_bw=lstm_cell(layer_size),   inputs=input,   dtype=tf.float32,   sequence_length=sequence_length


2. Feed forward layer


Cost Function


Neural network 의 최종 output dimension 을 2 로 하고, 확률 distribution 으로 바꾸기 위해서 output 의 softmax 를 계산을 합니다. Softmax 는 임의의 vector 값들을 확률로 바꾸기 위해서 가장 일반적으로 쓰이는 transform 방식이고, 식은 다음과 같습니다 (output vector z 로부터 계산된 c category 의 확률).

이 확률과 실제 클릭 여부 데이타 (label) 의 cross entropy 를 통하여 loss/cost 를 계산했습니다. Cross entropy 의 수식은 다음과 같습니다 (y 는 label, a 는 확률).

Mean squared error (y – a)^2 대신에 cross entropy 를 cost function 으로 사용한 이유는 확률이 0 이나 1 에 근접할 때에 gradient 가 점점 줄어드는 (결국 learning 이 느려지는) 문제를 보완하기 위해서입니다. 그리하여 일반적으로 categorization 을 다룰 때 주로 cross entropy 를 씁니다 (참고).


이 전 과정 (softmax + cross entropy) 또한 Tensorflow 에서 softmax_cross_entropy_with_logits 라는 하나의 함수로 정의 되어 있어서 편합니다 (참고


3) Evaluation 및 Monitoring

Training 데이타의 loss 값을 통하여 모델의 convergence 를 판단할 수 있지만, 모델의 실질적인 효과를 이해하기 위해서는 더 직관적인 metric 이 필요합니다. 일반적으로 데이타 categorization 의 효과를 판단할 때는 확률 (output 의 softmax 결과) 이 가장 높은 category 와 실제 label 을 비교하여 accuracy 를 계산합니다. 예를 들어, test 또는 evaluation data set 의 결과가 아래와 같다면,

Accuracy 는 66.6% 가 될 것입니다.


하지만 컨텐츠의 클릭같은 경우에는 대부분 (90% 이상) 의 데이타가 0 (no click) 이기 때문에 이와 같은 accuracy metric 으로는 모델의 효과를 판단하기 어렵습니다. 이와 같은 데이타 bias 와 관계없이 클릭할 확률과 클릭 여부 데이타만을 가지고 모델의 효과를 판단하기 위해서 AUC (area under curve) 값을 사용했습니다. 자세한 AUC 의 정의는 http://www.dataschool.io/roc-curves-and-auc-explained를 참고하시면 됩니다.


다양한 threshold value 를 가지고 true positive rate 대비 false positive rate 를 그래프에 plot 한 후 그 그래프의 넓이를 구합니다


간편한 AUC 계산을 위해서 sklearn 에서 제공하는 함수를 사용했습니다. (참고


4) Tuning

Overfit


보통 같은 training data 를 반복해서 training 하다보면 training cost 는 계속 감소하지만 evaluation/test cost 는 줄지 않는 경우가 많습니다. Training data 에 모델이 overfit 되서 그런건데요, 이를 보완하기 위한 방안들이 몇가지 있습니다.


데이타: 간단하며서도 가장 효과적인 방법입니다! Training 데이타량을 늘리는 겁니다.


L2 regularization: Weight 가 너무 커지는 것을 방지합니다. High level 로 보자면, weight 가 커지면 input 에 따라서 output 의 swing 도 커지기 때문에 (예를 들어, 다양한 input 의 noise 에 model 이 fitting 될 수 있어서), 일반적인 input 을 위한 model 을 만들기 어려워집니다.              

      

# weight 를 이용한 l2 regularization  for weight in weights:    cost += l2_reg_lambda * tf.nn.l2_loss(weight)


Dropout: 간단하며서도 가장 효과적인 방법입니다! Training 데이타량을 늘리는 겁니다.[caption id="attachment_24879" align="aligncenter" width="768"]

[Image Source: A simple way to prevent neural networks from overfitting]


Tensorboard


모델 설계 후 여러가지 방법으로 training 과정을 monitor 할 수 있지만, 가장 간편한 방식이 tensorflow 에서 제공하는 tensorboard 입니다. 매 iteration weight/bias 값의 변화 추이, accuracy 및 cost 의 변화 추이 등을 모니터 할 수 있습니다. 개인적으로 많은 도움이 된 부분은 graph visualization 입니다. 데이타의 dimension, 흐름, training 되고 있는 variable 등이 잘 정리 되어 있습니다. (참고)


Tensorboard 를 이용하기 위해서는:


그래프 정의시 필요한 scalar 값마다 summary 기록 후 최종 merge                    


tf.scalar_summary(‘cost’, cost) … merged = tf.merge_all_summaries()                    


Session 생성 후 summary writer 정의 


Writer = tf.train.SummaryWriter([directory], sess.graph) 


매 iteration step 마다 summary 기록 추출                    


summary, ...  = sess.run([merged, … ], feed_dict=...) writer.add_summary(summary, step)


Parameter optimization


Tuning 가능한 parameter 들이 많습니다: learning rate, batch size, dropout rate, l2 regularization rate 등등. 사실 이 parameter 들을 조정 하는 공식은 없습니다. 다양한 변화를 주면서 얼마나 빨리 converge 하는지, AUC 값은 어떤지, 등을 꾸준히 monitor 하면서 optimal 한 값들을 찾아 나가야 합니다: It is more art than science.


위의 모델 설계 과정을 거친 후 모델을 3달 분량의 데이타를 이용하여 총 100 epoch 정도 training 시켰습니다. Evaluation 데이타는 한 유저의 1주일 간의 컨텐츠 소비 데이타를 가지고 생성하였습니다. 유저마다 클릭 확률이 다르기 때문에 여러 유저가 섞인 데이타로 결과를 test 하지 않았습니다. 물론 evaluation data 는 training data 에 포함이 안 되었습니다. Training 끝 무렵엔 evaluation data set 에서 ~0.7 의 AUC 값이 consistent 하게 나왔습니다.


만족스러운 AUC 값이 나온 후 이 모델을 이용해서 실제 유저들에게 개인화된 컨텐츠를 제공하여 클릭률 증가를 살펴보았습니다.

 

5. 시스템

Daily training은 지속적으로 진행됩니다. 다만 매일 가장 최근 60일 데이타만을 이용해서 training 합니다. 하루의 training 이 끝나면 모델 parameter 들을 p2 instance 로 옮겼습니다. 모든 training 데이타를 이용해서 다시 training 하는건 시간도 많이 소요 될 뿐 아니라, 유저의 성향이 바뀔 수도 있기 때문입니다.


6. 결과


모델의 효과를 파악하기 위해서 A/B 테스트를 진행했는데요, Slidejoy 컨텐츠의 active user 3,000 명을 추출하여 크게 3 그룹으로 나눴습니다.


그룹 A: 랜덤한 컨텐츠가 보여집니다

그룹 B: Unigram TF-IDF 방식으로 기사들을 묶어서 가장 popular 한 토픽 순서대로 모든 유저들에게 일괄적으로 같은 컨텐츠가 보여집니다

그룹 C: Neural network 에서 계산된 확률이 높은 순서대로 유저별로 개인화된 컨텐츠가 보여집니다


2 주일동안 테스트를 해서 충분한양의 impression/click 을 확보한 후 결과를 살펴보니 그룹 B 가 A 보다 클릭 비율이 22% 높았고, 그룹 C 가 B 보다 25% 높았습니다 (statistically significant). 간단한 모델이지만 실질적인 application에서 의미있는 결과를 도출할 수 있었습니다. 


7. 글을 마치며

이 프로젝트는 컨텐츠의 개인화를 위하여 시작했지만, 버즈빌에서는 neural network 기술을 광고 타게팅, 매출 증대, operation 자동화, 마케팅 등의 다양한 분야에 빠르게 적용시킬 예정입니다. 블로그 서두에 언급했듯이, neural network 의 장점은 각 분야에 specialized 된 feature 들을 고민할 필요가 없어서 하나의 generalized 모델을 여러 목적으로 사용하는 것이 가능합니다. 그래서 저희의 next step 으로는 다양한 형태의 data를 input 받아서 다양한 형태의 prediction을 할 수 있는 generalized 된 모델과 시스템을 만드는 것입니다. 필요에 따라서는 이 모델 위에 마치 레고처럼 특수한 layer 를 추가하거나 필요 없는 layer 는 생략할 수도 있겠지요.

위에 설명한 추천 로직은 앞에서도 언급했듯이 상당히 간단한? 모델입니다. 이와 비교해서 최근에 neural network 로 바꿔서 좋은 평가를 받고 있는 Google translate 의 모델을 보면 경외감이 듭니다. (참고)

그만큼 저희의 장기적인 목표를 위해서는 개선할 수 있는 부분이 많습니다.

                        [Image Source: Google’s Neural Machine Translation System]


1. 병렬 구조


위 디자인에서 보면 알겠지만 (참고 문서에서도 설명이 되어있고), 단방향보다 효과가 좋은 bi directional RNN을 한 layer만 사용한 이유는 model parallelism을 극대화하기 위해서입니다. 예를 들어, 한 단방향 RNN layer의 첫 번째 time slot 계산이 끝나면 바로 그 다음 RNN의 첫번째 time slot을 계산 할 수 있기 때문입니다. (쌍방향일 경우 모든 timeslot 이 계산 되어야 다음 layer를 계산 할 수 있습니다) 결국 여러 gpu를 사용해서 여러 layer RNN을 병렬 구조로 계산을 할 수가 있게 됩니다.


Model parallelism외에도 중요한 것이 data parallelism입니다. 여러 개의 모델을 동시에 돌려서 하나의 shared model parameter들을 업데이트하도록 하는 방식입니다.


위에서도 언급했듯이 수많은 실험을 통해서만 parameter들의 optimal 한 값과 중요한 feature들을 찾아낼 

수 있습니다. 그러기 위해서는 최대한 많은 실험을 빠르게 돌려야 하는데, 이러한 병렬 구조들이 가장 optimal한 모델을 구현하는데 큰 도움이 될 것 입니다.


2. Automated tuning


실험 (모델에서 feature를 빼고 metric들을 monitor하면서) 을 통해서 어떠한 feature 가 중요한지 또 중요하지 않은지를 판단해서 중요한 feature 들을 더 emphasize하는 과정이 필요합니다. 예를 들어 컨텐츠의 제목의 유무가 AUC 값의 증가에 큰 영향을 미친다면, 컨텐츠 제목 RNN의 복잡도를 늘려서 모델이 제목의 context를 더 잘 이해할 수 있도록 보완할 수 있습니다. 또한, 앞서 언급했던 learning rate, regularization rate, dropout rate 등등의 다양한 parameter 들을 실험을 통해서 optimize 를 해야 하는데요. 이러한 과정의 많은 부분들을 자동화 하는 시스템을 설계하는 것이 generalized model 을 만드는 데에 key 가 될 것입니다.


3. Reinforcement Learning


Supervised learning 이외에도 neural network를 이용한 reinforcement learning모델이 많이 공개 되었습니다. (DQN, A3C 등등) 주로 state space가 작으면서 well-defined 되어있는 게임에 많이 적용되었는데, 광고/컨텐츠 allocation policy에도 효과적으로 사용될 수 있을 것 같습니다.


4. TFlearn


저는 이 프로젝트를 진행하면서 tensorflow에서 제공하는 함수들을 사용했지만, 사실 더 간단한 library가 있습니다. 바로 tflearn인데요, 여러 반복적인 graph 생성 operation들 (placeholder생성, weight/bias 정의, regularization, dropout 등) 을 high level 함수들로 묶은 library입니다.


모델 구현을 하면서 귀찮고 또 실수도 잦은 부분이 tensor들의 dimension지정하고, 필요에 따라서 reshape하는 것인데, TFlearn에서는 이러한 반복적이고 소모적인 operation들을 깔끔하게 해결 해 줍니다. 예를 들어, 아래 두 줄의 코드로 weight, bias, activation, regularization, dropout 이 전부 포함된 feed forward layer를 생성 할 수 있습니다.                    


dense1 = tflearn.fully_connected(    
input_layer,     
64,     
activation='tanh',     
regularizer='L2',     
weight_decay=0.001
)

dropout1 = tflearn.dropout(dense1, 0.8)


성능과 정확성 등을 더 알아봐야 하지만 실수를 줄이고, tensorflow 를 처음 접하거나 간단한 모델을 구현하는 데에는 안성맞춤인 것 같습니다. 앞으로, 딥러닝에 기반한 다양한 neural network 기술 연구가 어떻게 버즈빌의 서비스를 변화시킬지 개인적으로도 기대가 큽니다. 



브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari