-
[Recommender System] - Autoencoder를 이용한 차원 축소 기법Recommender System/추천 시스템 2019. 5. 20. 14:56
차원축소에 흔히 사용되는 방법은 SVD와 같은 Matrix Factorization, Eigen value 등의 개념을 활용한 것이다. 특히나 추천시스템에서는 유저나 아이템 단위의 profile에 이러한 차원축소를 유용하게 사용하곤 한다. 유저의 행동정보를 flat하게 펼쳐놓은 vector라던지, 비정형 정보의 embedding을 예로 들 수 있다.
기본적인 차원축소는 원래의 고차원 데이터를 중간 단계의 vector로 표현하고, 이를 다시 원래의 모형대로 완성하는 학습을 거치면 중간 단계의 vector가 축소된 정보를 잘 담고있다는 아이디어에서 출발한다. 가장 널리 알려진 PCA는 원래의 모형대로 학습하는 과정 없이, 선형적인 성질만을 이용하여 데이터의 분산을 최대화하는 차원으로 데이터를 투영시킨다.
PCA의 경우는 성능이 그다지 뛰어나지 않다는 명확한 단점을 가지고 있고, SVD는 데이터가 linear한 상황에서만 잘 동작한다는 단점을 가지고 있다. 이 외에도 SVD는 사실 단점이 아주 많다. white space가 아주 많은 행렬에 대해서는 거의 최악에 가까운 성능을 보여주고, k파라미터가 증가할수록 오차도 같이 증가하는 기이한 현상을 보여주기도 한다. 이를 보정하기 위한 PMF(probabilistic-matrix-factorization) 알고리즘 역시 가우시안 모형을 가설로 하기 때문에 real-world의 profile 데이터에서는 최악의 성능을 보여준다 (사실 SVD로 축소하느냐 AE로 축소하느냐 보다는, matrix의 profile 피처를 얼마나 잘 다듬어내느냐가 더 중요하다).
그래서 다른 방법으로 눈을 돌린것이 바로 DL 기반의 차원 축소 기법이다. DL 기반의 차원 축소 기법 중, 가장 basic한 방법이 바로 Autoencoder이다. 본 포스팅은 DL 기반의 차원축소 기법을 공부하는 과정에서 AE의 간단한 기록이다.
Autoencoder의 개념
오토인코더는 아주 간단한 뉴럴 네트워크 레이어의 재구성에 지나지 않는다. 아래의 그림과 같이, 히든 레이어(Hidden Layer) 하나를 가지는 모양의 뉴럴 네트워크가 Autoencoder의 개념의 전부이다.
단, input data와 output data가 같은 것으로 하여 네트워크를 학습시킨다. SVD가 rmse를 목표로, latent vector를 중간에 두고 원래의 행렬을 복원해내는 것과 동일한 아이디어라고 볼 수 있다. 다만, 이 네트워크는 훨씬 더 복잡하게 응용이 가능하고, non-linear problem solving에서도 아주 좋은 성능을 보여준다. 간단한 예로, 히든 레이어 몇개를 더 추가하는 것 만으로도 더욱 복잡한 autoencoder를 구성할 수 있다. 이처럼 대칭적으로 히든 레이어를 추가하는 방법을 Stacked Autoencoder라고 부른다.
Autoencoder의 구성
오토인코더는 위의 그림에서 볼 수 있듯이, 인코더(Encoder)와 디코더(Decoder) 두 영역을 가지고 있다. 이해를 돕기 위해 오토인코더의 전체 과정을 동영상 압축의 과정이라고 생각해보자. 이 경우, 원래의 input 데이터는 압축 이전의 원본 동영상이 될 것이다. 그렇다면 압축이 완료된 zip 파일은 오토인코더의 output 데이터가 되는 것인가? 물론 아니다. 압축이 완료된 동영상.zip 파일은 인코더와 디코더 사이에 있는 layer vector가 되는 것이다.
만약 동영상 압축에 알집이라는 프로그램을 사용한다면, 이 네트워크 전체가 알집 프로그램이 되는 것이며, "압축하기" 기능을 수행하는 것은 인코더가 되고 "압축풀기" 기능을 수행하는 것은 디코더가 되는 것이다. 이러한 학습을 통해, 오토인코더는 입력 데이터에서 가장 중요한 특성을 축소하여 학습하는 메커니즘을 가진다.
이를 코드로 더 쉽게 이해해보자. Mnist 데이터를 Autoencoder로 축소한 뒤, 축소된 벡터로 classification을 학습하는 예제를 만들어보았다.
from keras.datasets import mnist from keras.layers import Input, Dense from keras.models import Model from keras import backend as K (x_train, y_train), (x_test, y_test) = mnist.load_data() x_train = x_train.astype('float32') / 255. x_test = x_test.astype('float32') / 255. x_train = x_train.reshape((len(x_train), np.prod(x_train.shape[1:]))) x_test = x_test.reshape((len(x_test), np.prod(x_test.shape[1:]))) # configure encoding_dim = 32 input_img = Input(shape=(784,)) # layers encoded = Dense(encoding_dim, activation='relu')(input_img) decoded = Dense(784, activation='sigmoid')(encoded) # Models autoencoder = Model(input_img, decoded) # autoencoder encoder = Model(input_img, encoded) # encoder encoded_input = Input(shape=(encoding_dim,)) decoder_layer = autoencoder.layers[-1] decoder = Model(encoded_input, decoder_layer(encoded_input)) # decoder # result viewer def rmse(y_true, y_pred): return K.sqrt(K.mean(K.square(y_pred - y_true), axis=-1)) def recall(y_true, y_pred): y_true_yn = K.round(K.clip(y_true, 0, 1)) y_pred_yn = K.round(K.clip(y_pred, 0, 1)) count_true_positive = K.sum(y_true_yn * y_pred_yn) count_true_positive_false_negative = K.sum(y_true_yn) recall = count_true_positive / (count_true_positive_false_negative + K.epsilon()) return recall # train autoencoder autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy', metrics=[rmse, recall]) autoencoder.fit(x_train, x_train, epochs=50, batch_size=256, shuffle=True, validation_data=(x_test, x_test)) # encoding result encoded_imgs = encoder.predict(x_test) decoded_imgs = decoder.predict(encoded_imgs) # classification train data reducted_x_train = encoder.predict(x_train) reducted_x_test = encoder.predict(x_test) # train classifier model=Sequential() model.add(Dense(64, activation='relu', input_dim=encoding_dim)) model.add(layers.Dropout(0.2)) model.add(Dense(32, activation='relu')) model.add(layers.Dropout(0.2)) model.add(Dense(10, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # final result history = model.fit(reducted_x_train, y_train, epochs=10, batch_size=100, validation_split=0.2) performance_test = model.evaluate(reducted_x_test, y_test, batch_size=100) print('Test Loss and Accuracy ->', performance_test)
그리고 차원이 얼마나 잘 축소되었는지 평가하기 위해, RMSE 외에 Recall 개념을 추가하였다. 이는 추천시스템에 AE를 활용할 때, "추천 profile의 재현율을 평가할 수 있다는 측면"에서 매우 유용한 top@recall 방법을 차용한 것이다.
중간 단계에서 이미지를 얼마나 잘 복원했는지, 압축된 벡터만을 가지고 얼마나 분류모델이 잘 학습되었는지를 확인해보자. 이미지 복원 실행 결과는 아래와 같다.
그래서 사용처는?
오토인코더가 주로 사용되던 영역은 '비지도 사전학습' 분야이다. 주 역할은 입력 데이터를 사전학습하여, 상위의 모델의 성능을 더욱 좋게 만드는 역할이다. 추천시스템에 응용될 때도 역시 마찬가지다. 유저 혹은 아이템 단위의 profile의 비정형 특성을 축소할 수 있는, 상위의 모델에 사용될 input data를 잘 만들어내는 것이 목표이다. 근본적인 아이디어나 활용법은 word2vec을 비롯한 임베딩 알고리즘과 굉장히 유사하다고 생각된다.
추후 DL 기반 차원축소 기법을 더 자세히 파봐야겠다.
'Recommender System > 추천 시스템' 카테고리의 다른 글
Learning to Rank와 nDCG (0) 2019.08.13 [Recommender System] - 개인화 추천 시스템의 최신 동향 (7) 2019.06.17 [Recommender System] - Python으로 Matrix Factorization 구현하기 (32) 2018.08.01 [Recommender System] - MF(Matrix Factorization) 모델과 ALS(Alternating Least Squares) (2) 2018.07.16 [Recommender System] - Spark로 연관 규칙(Association Rule) 구현하기 (2) 2018.06.25 댓글