Gorio Tech Blog search

파이썬 collections, heapq 모듈 설명

|

1. collections 모듈

1.1. collections.Counter 객체

collections 모듈에서 가장 기본을 이루는 class는 collections.Counter이다. 이 class에 argument로 반복 가능한 (iterable) 객체를 지정하거나 dictionary와 같은 mapping 객체를 지정하면 Counter 객체를 생성할 수 있다. 예를 들어보면,

import collections

counter = collections.Counter([1, 2, 3, 2])
# counter = collections.Counter({1: 1, 2: 2, 3: 1})
print(counter)

Counter({1: 1, 2: 2, 3: 1})

주석 처리된 line이 바로 후자의 방법에 해당한다. 이렇게 생성된 객체는 수정될 수 있다.

counter[1] += 1
print(counter)

Counter({1: 2, 2: 2, 3: 1})

이 외에도 여러 계산이 가능한데, 아래를 참고하길 바란다.

연산자 설명
-= 뺀다. 결과가 음수면 그 요소는 삭제된다.
&= 좌변의 Counter 객체 요소 중 우변의 Counter 객체 요소에 미포함되어 있는

key의 요소를 삭제한다. 요소의 값은 둘 중 작은 쪽의 값이 된다.
l= 2개의 Counter 객체 전체의 요소로부터 새롭게 Counter 객체를 생성한다.

key가 같으면 두 값 중 큰 쪽의 값이 된다.

위 누계 연산자에서 =를 빼고 +, -, &, | 만 사용할 경우 이항 연산자로 작용한다.

또한, 이 객체에서 미등록 key를 참조한다 하더라도 KeyError는 발생하지 않는다.

print(counter[4])

0

1.2. collections.ChainMap: 사전 통합

dict1 = {'banana': 1}
dict2 = {'apple': 2}

counter = collections.ChainMap(dict1, dict2)
print(counter['apple'])

2

위와 같이 ChainMap 메서드는 여러 사전 객체를 모아 하나로 통합하는 기능을 갖고 있다. 만약 통합한 객체에 변화를 줄 경우, 원래의 사전들에도 그 변경 사항이 반영된다. clear 메서드를 사용하면 사전을 삭제할 수 있다.

1.3. collections.defaultdict: 기본 값이 있는 사전

일반적으로 사전 객체에 미등록된 key를 참조하면 KeyError가 발생한다. collections.defaultdict는 이러한 문제를 해결하기에 적합한 객체이다.

d = {'orange': 10}

def get_default_value():
    return 'default-value'

# 여기서 get_default_value와 같은 callable 객체나 None을 입력할 수 있다.
# None을 입력할 경우 일반 사전과 마찬가지로 KeyError가 발생한다.
e = collections.defaultdict(get_default_value, orange=10)
print(e['ham'])

'default-value'

만약 기본 값으로 수치 0이나 빈 사전, 리스트를 반환하고 싶다면 int, dict, list형 객체를 지정하면 된다.

e = collections.defaultdict(int)
e = collections.defaultdict(dict)
e = collections.defaultdict(list)

1.4. collections.OrderedDict: 순서가 있는 사전

for loop와 같은 과정 속에서 등록한 순서대로 요소를 추출하고 싶으면 이 class를 이용하면 좋다. 시퀀스를 이용하여 객체를 생성하면 순서대로 등록된 것을 확인할 수 있다.

mydict = collections.OrderedDict([("orange", 10), ("banana", 20)])
print(mydict)

OrderedDict([('orange', 10), ('banana', 20)])

그러나 키워드 인수나 일반 사전으로 초깃값을 등록하면 순서가 무시된다. OrderedDict 객체에는 유용한 기능들이 있는데, 아래를 참조하면 좋을 것이다.

mydict = collections.OrderedDict([("orange", 10), ("banana", 20), ("blueberry", 30), ("mango", 40)])

# popitem 에서 last=True로 하면 마지막 요소를 사전에서 삭제하고 반환하고,
# False로 하면 첫 요소에 효과를 적용한다.
mydict.popitem(last=True)

# move_to_end에서 last=True로 하면 지정한 키를 맨 끝으로 이동시키고, False이면 맨 처음으로 이동시킨다.
mydict.move_to_end(key="banana", last=True)

print(mydict)

OrderedDict([('orange', 10), ('blueberry', 30), ('banana', 20)])

1.5. collections.namedtuple

데이터를 효율적으로 관리하기에 적합한 class가 바로 namedtuple이다. 속성 이름을 지정하여 가독성을 높이고 튜플을 활용하여 원하는 요소를 쉽게 추출하도록 하게 해준다.

point = collections.namedtuple("point", "X, Y, Z")
data = point(-2, 6, 3)
print(data.Y)

6

2. heapq 모듈

데이터를 정렬된 상태로 저장하고, 이를 바탕으로 효율적으로 최솟값을 반환하기 위해서는 이 heapq 모듈을 사용하면 매우 편리하다. 사용하기 위해서는 최소 heap을 먼저 생성해야 한다. 빈 리스트를 생성해서 heapq 모듈의 메서드를 호출할 때마다 이를 heap argument의 인자로 투입해야 한다.

import heapq

heap = []

# heappush(heap, item): heap에 item을 추가함
# 주의점: keyword 인자를 입력하면 Error가 발생함
heaqp.heappush(heap, 2)
heaqp.heappush(heap, 1)

# heappop(heap): heap에서 최솟값을 삭제하고 그 값을 반환함
# 최솟값을 삭제하지 않고 참조하고 싶다면 heap[0]을 쓰자
heapq.heappop(heap)

1

이 외에도 여러 메서드를 사용할 수 있다. 만약 어떤 변화하는 시퀀스에서 최솟값을 얻고 싶다고 하자. 아래와 같은 코딩이 가능하다.

heap = [79, 24, 50, 62]

# heapify(heap): heap의 요소를 정렬함
heapq.heapify(heap)

# heappush(heap, item): heap에 item을 추가한 뒤, 최솟값을 삭제하고 그 값을 반환함
heapq.heappushpop(heap, 10)

10

# heapreplace(heap, item): 최솟값을 삭제한 뒤, heap에 item을 추가하고 삭제한 값을 반환함
# 주의점: 추가한 값 아님
heapq.heapreplace(heap, 10)

24

Reference

파이썬 라이브러리 레시피, 프리렉

Comment  Read more

Factorization Machines (FM) 설명 및 Tensorflow 구현

|

본 글의 전반부에서는 먼저 Factorization Machines 논문을 리뷰하면서 본 모델에 대해 설명할 것이다. 후반부에서는 텐서플로를 활용하여 FM 모델을 구현해 볼 것이다. 논문의 전문은 이곳에서 확인할 수 있다.


1. Factorization Machines 논문 리뷰

1.0. Abstract

본 논문에서는 SVM과 Factorization model들의 장점을 결합한 FM이라는 새로운 모델을 소개한다. SVM과 마찬가지로 FM은 그 어떤 실수 값의 피쳐 벡터를 Input으로 받아도 잘 작동하는 일반적인 예측기이다. 그러나 SVM과 다르게 이 모델은 Factorized Parameter를 이용하여 모든 Interaction을 모델화하여 아주 희소한 상황에서도 Interaction들을 예측할 수 있다는 장점을 갖고 있다.

본 논문에서는 FM의 모델 방정식이 선형시간 내에서 계산되어 바로 최적화될 수 있음을 증명한다. 따라서 SVM과 달리 dual form에서의 변환(transformation)은 필요하지 않아 본 모델의 파라미터들은 해를 구할 때 Support 벡터의 도움 없이 바로 예측될 수 있다.

Matrix Factorization, SVD++, PITF, FPMC 등 다양한 모델들이 존재하는데, 이들은 오직 특정한 Input 데이터에서만 잘 작동한다는 한계를 지닌다. 반면 FM은 Input 데이터를 지정하여 이러한 모델을 따라할 수 있다. 따라서 Factorization 모델에 대한 전문적인 지식이 없더라도 FM은 사용하기에 있어 굉장히 쉽다.

1.1. Introduction

SVM은 유명한 예측 알고리즘이지만 협업 필터링과 같은 환경에서 SVM은 그리 중요한 역할을 하지 못한다. 본 논문에서는 SVM이 굉장히 희소한 데이터의 비선형적(complex) 커널 공간에서 reliable parameter(hyperplane: 초평면)를 학습할 수 없기 때문에 이러한 task에서 효과적이지 못함을 보여줄 것이다. 반면에 Tensor Factorization Model은 일반적인 예측 데이터에 대해서 그리 유용하지 않다는 단점을 가진다.

본 논문에서는 새로운 예측기인 FM을 소개할 것인데, 본 모델은 범용적인 예측 모델이지만 또한 매우 희소한 데이터 환경 속에서도 reliable parameter를 추정할 수 있다. FM은 모든 nested된 변수 간 상호작용을 모델화하지만 SVM이 Dense Parametrization을 사용하는 것과 달리 factorized parametrization을 사용한다.

FM의 모형식은 선형 시간으로 학습될 수 있으므로 파라미터들의 숫자에 따라 학습시간이 결정된다. 이는 SVM처럼 학습 데이터를 저장할 필요 없이 직접적인 최적화화 모델 파라미터의 저장을 가능하게 한다.

요약하자면 FM의 장점은 아래와 같다. 1) 굉장히 희소한 데이터에서도 파라미터 추정을 가능하게 한다. 2) 선형 complexity를 갖고 있기 때문에 primal하게 최적화될 수 있다. 3) 어떤 실수 피쳐 벡터를 Input으로 받아도 잘 작동한다.


1.2. Prediction under Sparsity

가장 일반적인 예측 문제는 실수 피쳐 벡터 x에서 Target domain T (1 또는 0)로 매핑하는 함수를 추정하는 것이다. 지도학습에서는 (x, y) 튜플이 stacked된 D라는 학습데이터셋이 존재한다고 가정된다. 우리는 또한 랭킹 문제에 대해 논의해볼 수 있는데, 이 때 함수 y는 피쳐 벡터 x에 점수를 매기고 이를 정렬하는데 사용된다. Scoring 함수는 pairwise한 학습 데이터로부터 학습될 수 있는데, 이 때 피쳐 튜플인 $ (x^(A), x^(B)) $는 $ x^(A) $가 $ x^(B) $보다 높은 순위를 지닌다는 것을 의미한다. pairwise 랭킹 관계가 비대칭적이기 때문에, 오직 positive 학습 instance만을 사용해도 충분하다.

본 논문에서 우리는 x가 매우 희소한 상황을 다룬다. 범주형 변수가 많을수록 더욱 데이터는 희소해지기 마련이다.

$m(x)$: 피쳐 벡터 x에서 0이 아닌 원소의 개수
$\overline{m}_D$: 학습 데이터셋 D에 속하는 모든 x에 대해 $m(x)$의 평균

Example 1
영화 평점 데이터를 갖고 있다고 하자. User $u \in U$가 영화(Item) $i \in I$를 특정 시점 $t \in \R$에 $r \in {1, 2, 3, 4, 5}$의 점수로 평점을 주었을 때 데이터는 아래와 같은 형상을 취할 것이다.

data S = {(Alice, Titanic, 2010-1, 5), (Bob, Star Wars, 2010-2, 3) … }

아래 그림은 이 문제 상황에서 S라는 데이터셋에서 어떻게 피쳐 벡터가 생성되는지를 보여준다.

한 행에는 하나의 User, 하나의 Item이 들어가는 것을 확인할 수 있다. 모든 영화에 대한 평점 Matrix는 행의 합이 1이 되도록 Normalized되었다. 마지막 갈색 행렬은 주황색 행렬에서 확인한 active(가장 최근에 평점을 매긴)item 바로 이전에 평점을 매긴 Item이 무엇인지 알려주고 있다.


1.3. Factorizaion Machines 본문

A. Factorization Machine Model

2차 모델 방정식은 아래와 같다.

$V$ 내부의 행 $v_i$는 k개의 factor를 지닌 i번째 변수를 설명한다. k는 0을 포함한 자연수이며, factorization의 차원을 정의하는 하이퍼 파라미터이다. 2-way FM(2차수)은 변수간의 단일 예측변수와 결과변수 간의 상호작용 뿐 아니라 pairwise한(한 쌍의) 예측변수 조합과 결과변수 사이의 상호작용도 잡아낸다.

부가적으로 설명을 하면,

  • $x_i$: X 데이터 셋의 하나의 행 벡터(feature vector)
  • $w_0$: global bias
  • $w_i$: i번째 변수의 영향력을 모델화 함
  • $\hat{w}_{i, j}$ = $<v_i, v_j>$: i, j번째 변수간의 상호작용을 모델화 함
  • $v$ 벡터: factor vector

FM 모델은 각 상호작용에 대해 $w_{i, j}$라는 모델 파라미터를 그대로 사용하는 것이 아니라, 이를 factorize하여 사용한다. 나중에 확인하겠지만, 이 부분이 희소한 데이터임에도 불구하고 고차원의 상호작용에 대한 훌륭한 파라미터 추정치를 산출할 수 있는 중요한 역할을 하게 된다.

k가 충분히 크면 positive definite 행렬 W에 대하여 $W = V \bullet V^t$을 만족시키는 행렬 $V$는 반드시 존재한다. 이는 FM모델이 k가 충분히 크면 어떠한 상호작용 행렬 $W$도 표현할 수 있음을 나타낸다. 그러나 sparse한 데이터 환경에서는, 복잡한 상호작용 W를 추정하기 위한 충분한 데이터가 없기에 작은 k를 선택할 수 밖에 없는 경우가 많다.

위 그림을 보면 알 수 있듯이, x벡터 하나당 1개의 예측 값을 산출하게 된다.

참고로, 본 논문에서는 위 그림의 p 대신 n이라고 적혀있는데, 이 p는 예측 변수의 수를 의미하기 때문에, 관례적으로 더 많이 쓰이는 p로 표기한 것이니 착오 없길 바란다.

Sparse한 환경에서, 일반적으로 변수들 간의 상호작용을 직접적이고 독립적으로 추정하기 위한 충분한 데이터가 없는 경우가 많다. FM은 이러한 환경에서도 상호작용들을 추정할 수 있는데, 이는 왜냐하면 이 모델은 상호작용 파라미터들을 factorize하여 상호작용 파라미터들 사이의 독립성을 깰 수 있기 때문이다.

일반적으로 이것은 하나의 상호작용을 위한 데이터가 다른 관계된 상호작용들의 파라미터들을 추정하는 데 도움을 준다는 것읠 의미한다.

앞서 언급했던 예를 들어보자,
Alice와 Star Trek 사이의 상호작용을 추정하여 영화평점(Target y)을 예측하고 싶다고 하자. 당연하게도 학습데이터에는 두 변수 $x_a$와 $x_{ST}$가 모두 0이 아닌 경우는 존재하지 않으므로, direct estimate $w_{A, ST}$는 0이 될 것이다.

그러나 factorized 상호작용 파라미터인 $<V_{A}, V_{ST}>$를 통해 우리는 상호작용을 측정할 수 있다. Bob과 Charlie는 모두 유사한 factor vector $V_B$, $V_C$를 가질 것인데, 이는 두 사람 모두 Star Wars ($V_{SW}$)와 관련하여 유사한 상호작용을 갖고 있기 때문이다. (취향이 비슷하다.) 즉, $<V_{B}, V_{SW}>$과 $<V_{C}, V_{SW}>$가 유사하다는 뜻이다.

Alice($V_A$)는 평점 예측에 있어서 Titanic과 Star Wars 두 factor와 상호작용이 다르기 때문에 Charlie와는 다른 factor vector를 가질 것이다. Bob은 Star Wars와 Star Trek에 대해 유사한 상호작용을 가졌기 때문에 Star Trek과 Star Wars의 factor vector는 유사할 가능성이 높다. 즉, Alice와 Star Treck의 factor vector의 내적은 Alice와 Star Wars의 factor vector의 내적 값과 매우 유사할 것이다. (직관적으로 말이 된다.)


이제 계산적 측면에서 모델을 바라볼 것이다. 앞서 확인한 방정식의 계산 복잡성은 $O(kp^2)$이지만, 이를 다시 변형하여 선형적으로 계산 시간을 줄일 수 있다. pairwise 상호작용 부분은 아래와 같이 재표현할 수 있다.

이 부분이 굉장히 중요한데, 실제로 코드로 구현할 때 이와 같은 재표현 방식이 없다면 굉장히 난감한 상황에 맞닥드리게 될 것이다.

또한 x의 대부분의 원소가 0이므로 실제로는 0이 아닌 원소들에 대해서만 계산이 수행된다.

B. Factorizaion Machine as Predictors

FM은 회귀, 이항 분류, 랭킹 문제를 풀기 위해 활용될 수 있다. 그리고 이 모든 문제에서 L2 정규화 항은 과대적합을 막기 위해 추가된다.

C. Learning Factorizatino Machines

앞서 확인한 것처럼, FM은 선형적으로 계산되는 모델 방정식을 지니고 있다. 따라서 $w_0, w, V$와 같은 모델 파라미터들은 Gradient Descent 방법을 통해 효과적으로 학습될 수 있다. FM 모델의 Gradient는 아래와 같이 표현될 수 있다.

$\sum_{j=1}^n v_{j, f} x_j$는 i에 대해 독립적이기 때문에 우선적으로 미리 계산될 수 있다. 일반적으로 각각의 Gradient는 상수적 시간 O(1)만에 계산될 수 있다. 그리고 (x, y)를 위한 모든 파라미터 업데이터는 희소한 환경에서 $O(kp)$ 안에 이루어질 수 있다.

우리는 element-wise하거나 pairwise한 Loss를 계산하기 위해 SGD를 사용하는 일반적인 implementation인 LIBFM2를 제공한다.

D. d-way Factorizatino Machine

2-way FM은 쉽게 d-way FM으로 확장할 수 있다.

E. Summary

FM 모델은 모든 상호작용을 있는 그대로 사용하는 것이 아니라 factorized 상호작용을 이용하여 피쳐 벡터 x의 값 사이에 있는 가능한 상호작용들을 모델화한다. 이러한 방식은 2가지 장점을 지닌다.

1) 아무리 희소한 환경에서도 값들 사이의 상호작용을 추정할 수 있다. 또한 이는 관측되지 않은 상호작용을 일반화하는 것도 가능하게 한다.
2) 학습 및 예측에 소요되는 시간이 선형적이고, 이에 따라 파라미터의 수도 선형적이다. 이는 SGD를 이용하여 다양한 Loss Function들을 최적화하는 것을 가능하게 한다.

(후략)


2. Tensorflow를 활용한 구현

2.1. 준비

# FM
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow.keras.metrics import BinaryAccuracy
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split

# GPU 확인
tf.config.list_physical_devices('GPU')

# 자료형 선언
tf.keras.backend.set_floatx('float32')

# 데이터 로드
scaler = MinMaxScaler()
file = load_breast_cancer()
X, Y = file['data'], file['target']
X = scaler.fit_transform(X)

n = X.shape[0]
p = X.shape[1]
k = 10
batch_size = 8
epochs = 10

데이터는 sklearn에 내장되어 있는 breast_cancer 데이터를 사용하였다. 30개의 변수를 바탕으로 암 발생 여부를 예측하는 데이터이다. p는 예측 변수의 개수이고, k는 잠재 변수의 개수이다.

2.2. FM 모델 선언

class FM(tf.keras.Model):
    def __init__(self):
        super(FM, self).__init__()

        # 모델의 파라미터 정의
        self.w_0 = tf.Variable([0.0])
        self.w = tf.Variable(tf.zeros([p]))
        self.V = tf.Variable(tf.random.normal(shape=(p, k)))

    def call(self, inputs):
        linear_terms = tf.reduce_sum(tf.math.multiply(self.w, inputs), axis=1)

        interactions = 0.5 * tf.reduce_sum(
            tf.math.pow(tf.matmul(inputs, self.V), 2)
            - tf.matmul(tf.math.pow(inputs, 2), tf.math.pow(self.V, 2)),
            1,
            keepdims=False
        )

        y_hat = tf.math.sigmoid(self.w_0 + linear_terms + interactions)

        return y_hat

모델 자체는 아주 복잡할 것은 없다. linear termsinteractions라고 정의한 부분이 아래 수식의 밑줄 친 부분에 해당한다.

interactions 부분이 아주 중요한데, 이 부분을 어떻게 구현하느냐가 속도의 차이를 만들어 낼 수 있기 때문이다. 논문에서는 아래와 같이 이 상호작용 항을 재표현할 수 있다고 하였다.

interactions 부분은 위 식을 코드로 표현한 것인데, $\sum$ 항을 벡터화 하여 구현하였다.

설명을 위해, (k=2, p=3) shape을 가진 $V$ 행렬과 (p=3, 1)의 shape을 가진 $x$ 벡터가 있다고 하자. 사실 $(\sum_{i=1}^n v_{i,f } x_i)^2$ 부분을 계산하면 $V^T x$의 모든 원소를 더한 것과 동일하다.

위 그림의 결과는 $(v_{11}x_1 + v_{21}x_2 + v_{31}x_3)^2 + (v_{12}x_1 + v_{22}x_2 + v_{32}x_3)^2$와 동일할 것이다. 식의 나머지 부분도 같은 방법으로 생각하면 위와 같은 코드로 표현할 수 있을 것이다.

2.1.3. 학습 코드

# Forward
def train_on_batch(model, optimizer, accuracy, inputs, targets):
    with tf.GradientTape() as tape:
        y_pred = model(inputs)
        loss = tf.keras.losses.binary_crossentropy(from_logits=False,
                                                   y_true=targets,
                                                   y_pred=y_pred)
    
    # loss를 모델의 파라미터로 편미분하여 gradients를 구한다.
    grads = tape.gradient(target=loss, sources=model.trainable_variables)

    # apply_gradients()를 통해 processed gradients를 적용한다.
    optimizer.apply_gradients(zip(grads, model.trainable_variables))

    # accuracy: update할 때마다 정확도는 누적되어 계산된다.
    accuracy.update_state(targets, y_pred)

    return loss


# 반복 학습 함수
def train(epochs):
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, stratify=Y)

    train_ds = tf.data.Dataset.from_tensor_slices(
        (tf.cast(X_train, tf.float32), tf.cast(Y_train, tf.float32))).shuffle(500).batch(8)

    test_ds = tf.data.Dataset.from_tensor_slices(
        (tf.cast(X_test, tf.float32), tf.cast(Y_test, tf.float32))).shuffle(200).batch(8)

    model = FM()
    optimizer = tf.keras.optimizers.SGD(learning_rate=0.01)
    accuracy = BinaryAccuracy(threshold=0.5)
    loss_history = []

    for i in range(epochs):
      for x, y in train_ds:
          loss = train_on_batch(model, optimizer, accuracy, x, y)
          loss_history.append(loss)

      if i % 2== 0:
          print("스텝 {:03d}에서 누적 평균 손실: {:.4f}".format(i, np.mean(loss_history)))
          print("스텝 {:03d}에서 누적 정확도: {:.4f}".format(i, accuracy.result().numpy()))


    test_accuracy = BinaryAccuracy(threshold=0.5)
    for x, y in test_ds:
        y_pred = model(x)
        test_accuracy.update_state(y, y_pred)

    print("테스트 정확도: {:.4f}".format(test_accuracy.result().numpy()))

epochs = 50으로 실행한 결과는 아래와 같다.

스텝 000에서 누적 평균 손실: 1.2317
스텝 000에서 누적 train 정확도: 0.5692
스텝 002에서 누적 평균 손실: 0.9909
스텝 002에서 누적 train 정확도: 0.6271

...

스텝 048에서 누적 평균 손실: 0.2996
스텝 048에서 누적 train 정확도: 0.8996

테스트 정확도: 0.9500

Reference

http://nowave.it/factorization-machines-with-tensorflow.html

Comment  Read more

Matrix Factorization 설명 및 논분 리뷰

|

본 글은 2009년에 발표된 Matrix Factorization Techniques for Recommender Systems 논문을 리뷰하고 간단히 요약 정리한 글이다. 논문 원본은 이곳에서 다운 받을 수 있다.


1. Introduction

컨텐츠 기반 필터링은 각 사용자나 아이템에 대해 프로필을 만들고, 그 특성을 구체화하는 방식으로 이루어진다. 반면 위 방식의 대안이라고 할 수 있는 협업 필터링은 어떤 명시적(Explicit) 프로필을 만들지 않고, 이전 구매 기록이나 제품 평가 기록 등 과거 사용자 행동에만 의존해서 시스템을 구성한다. 이 방식은 유저-아이템 간의 상관관계를 찾아내는 것이 주 목적이라고 할 수 있다.

협업 필터링Domain-free 즉, 특별히 이 분야에 대한 지식이 필요하지 않다는 장점을 가진다. 반면 새로운 사용자와 아이템을 다루기에 부적합하다는 Cold Start Problem이라는 한계를 갖고 있다.

협업 필터링근접 이웃 방법잠재 요인 방법로 나뉜다. 후자의 경우 평점 패턴에서 20~100가지의 factor(요인)을 추론하는 것을 목적으로 한다.


2. MF Methods and A Basic MF Model

잠재 요인 협업 필터링을 구현하는 가장 좋은 방법 중 하나는 Matrix Factorization이다. 기본적으로 이 방법은 평점 패턴으로부터 추론한 요인 벡터들을 통해 사용자와 아이템의 특성을 잡아낸다. 이 때 사용자와 아이템 사이의 강한 관련성이 있다면 추천이 시행된다. 이 방법은 확장성, 높은 정확도, 유연성이라는 장점을 가진다.

추천 시스템은 여러 종류의 Input Data를 활용할 수 있다. 물론 가장 좋은 것은 양질의 명시적 피드백(Explicit Feedback)이 될 것인데, 이는 영화 평점이나 좋아요/싫어요와 같은 아이템에 대한 사용자의 선호 결과를 의미한다. 일반적으로 이러한 피드백은 그리 많이 이루어지지 않기 때문에, 이를 행렬로 정리하면 희소(Sparse) 행렬이 될 수 밖에 없다.

만약 이러한 명시적 피드백 조차 활용할 수 없을 때는, 추천 시스템은 암시적 피드백(Implicit Feedback)을 이용하여 사용자의 선호를 파악하게 된다. 이는 구매내역이나 검색기록, 검색 패턴, 커서의 움직임 등을 의미하며 이를 통해 사용자의 선호를 파악하는 것이 목표라고 할 수 있겠다.

Matrix Factorization(이하 MF 또는 행렬 분해) 모델은 사용자와 아이템 모두를 차원 f의 결합 잠재요인 공간에 매핑하는데, 사용자-아이템 상호작용은 이 공간에서 내적으로 모델링 된다.

아이템 i는 $ q_i $로, 사용자 u는 $ p_u $라는 벡터로 표현된다. 이 둘의 내적은 사용자-아이템 사이의 상호작용을 반영하며 이는 곧 아이템에 대한 사용자의 전반적인 관심을 표현한다고 볼 수 있다. 식은 아래와 같다.

[\hat{r_{ui}} = q^{T}_i p_u]

이 모델은 사실 SVD(Singular Vector Decomposition)과 매우 유사한데, 추천 시스템에서는 결측값의 존재로 이 SVD를 직접적으로 사용하는 것은 불가능하다. 결측값을 채워 넣는 것 역시 효율적이지 못하고 데이터의 왜곡 가능성 때문에 고려하기 힘들다.

따라서 오직 관측된 평점만을 직접적으로 모델링하는 방법이 제시되었으며, 이 때 과적합을 방지하기 위해 규제 항이 포함되었다. 요인 벡터 $ q_i, p_u $를 학습하기 위해 시스템은 관측된 평점 세트를 바탕으로 아래 식을 최소화하는 것을 목적으로 한다.

[\min_{q, p} \sum_{(u, i) \in K} ( r_{ui} - q^T_i p_u )^2 + \lambda (\Vert{q_i}\Vert^2 + \Vert{p_u}\Vert^2)]

이 때, K는 $ r_{ui} $가 측정된(known) 값일 때의 (u, i) 세트를 의미한다. 결과적으로 이 모델은 알려지지 않은 평점을 예측하는 것이 목적이기 때문에 과적합을 방지해야 하고, 이를 위해 규제항이 필요하고 $ \lambda $가 이 규제의 정도를 제어한다. $ \lambda $는 주로 Cross-Validation에 의해 결정된다.


3. Learning Algorithms and Adding Biases

이전 장에서 본 식을 최소화하기 위한 방법으로는 2가지가 제시된다.

3.1. Stochastic Gradient Descent

각각의 훈련 세트에 대해 본 알고리즘은 $ r_{ui} $를 예측하고 다음과 같은 예측 오차를 산출한다.
\(e_{ui} = r_{ui} - q^T_i p_u\)

이후 $ q_i $와 $ p_u $를 아래와 같이 업데이트 한다.

\(q_i := q_i + \gamma (e_{ui} p_u - \lambda q_i)\)
\(p_u := p_u + \gamma (e_{ui} q_i - \lambda p_u)\)

확률적 경사하강법은 구현이 쉽고 빠르다는 장점을 지닌다.

3.2. Alternating Least Squares

$ q_i $와 $ p_u $가 둘다 미지의 값이기 때문에 앞서 최소화하려고 했던 식은 convex하지 못하다. 그러나 만약 둘 중 하나를 고정(fixed)할 수 있다면, 이 최적화 문제는 quadratic하게 바뀌어 해를 구할 수 있게 된다. 따라서 ALS는 $ q_i $를 고정했다가 다음 번에는 $ p_u $를 고정하는 방식으로 작동한다. $ p_u $가 고정되어 있다면 본 알고리즘은 최소제곱법으로 $ q_i $를 다시 계산한다. 이러한 방법으로 목적 함수(2장에서 본 최소화 시켜야 할 식)를 최소화할 수 있는 것이다.

3.1장에서 본 SGD가 일반적으로 편리한 방법이긴 하지만 아래의 2가지 경우에는 이 ALS가 효과를 발휘하기도 한다.

  • 시스템이 병렬화를 지원하는 경우
  • 시스템이 암시적 데이터에 집중되어 있는 경우

3.3. Adding Biases

$ \hat{r_{ui}} = q^{T}_i p_u $ 식은 여러 평점 결과를 만들어 내는 사용자와 아이템 간의 상호관계를 파악하는 것이 목적이다. 그런데 사실 많은 경우에 이 상호작용 외에 사용자나 아이템 자체의 특성이 이러한 평점 결과에 영향을 미친다. 이것을 우리는 biases 또는 intercepts라고 부른다. 이를 앞서 보았던 방정식과 목적 함수에 적용해보면 아래와 같다.

[\hat{r_{ui}} = \mu + b_i + b_u + q^{T}_i p_u]

[\min_{p, q, b} \sum_{(u, i) \in K} ( r_{ui} - \mu - b_i - b_u - q^T_i p_u )^2 + \lambda (\Vert{q_i}\Vert^2 + \Vert{p_u}\Vert^2 + b^2_u + b^2_i)]


4. Additional Input Sources and Temporal Dynamics

종종 시스템은 Cold Start 문제에 직면하게 되는데, 평점 데이터에 기반한 추천 시스템을 만드는 상황에서는 사용자들이 평점 결과를 거의 남기지 않는 상황이 이 문제에 해당한다고 볼 수 있다. 이럴 때에는 사용자에 대한 추가적인 정보 소스들을 모두 통합할 필요가 있다. 즉, 행동 정보(Behavior Information)들이 필요하다는 것이다. 예를 들어 소매업자는 고객의 구매 기록이나 검색 기록 등을 활용할 수 있을 것이다.

단순화하기 위해 Boolean 암시적 피드백이 있는 경우를 생각해보자. $ N(u) $는 사용자 $u$가 암시적 선호를 표현한 아이템의 집합을 의미한다. 시스템은 이를 통해 사용자의 프로필을 만들어 낸다. $ N(u) $에 속한 아이템에 대한 선호를 표현한 사용자는 아래 벡터와 같이 표현된다.

[\sum_{i \in N(u)} x_i]

이 식을 정규화하는 것이 일반적으로 더 좋은 결과를 가져오기에, 정규화를 하겠다.

[ N(u) ^{-0.5} \sum_{i \in N(u)} x_i]

또 중요한 정보는 인구학적 정보와 같은 사용자 속성(User Attributes)이다. 유사하게 표현하면 아래와 같다.

[\sum_{a \in A(u)} y_a]

모든 Signal Source를 통합하여 개선된(Enhanced) 사용자 표현식은 아래와 같다.

[\hat{r_{ui}} = \mu + b_i + b_u + q^T_i [p_u + N(u) ^{-0.5} \sum_{i \in N(u)} x_i + \sum_{a \in A(u)} y_a]]

지금까지의 모델은 사실 정적(static)인 모델이었다. 즉, 시간의 변화를 반영하지 못한다는 뜻이다. 그러나 현실에서는 제품에 대한 인식, 인기는 새로운 선택지가 늘어남에 따라 시시각각 변하기 마련이다. 또한 고객들의 성향도 진화하여 그들의 취향은 때때로 변화한다. 따라서 추천 시스템은 시간에 따라 변하는 사용자-아이템 상호작용의 동적(dynamic)인 성질을 반영하는 Temporal Effect에 대해 설명할 수 있어야 한다.

총 3개의 항이 변화한다.
$ b_i(t) $: 아이템의 인기는 시간에 따라 변한다.
$ b_u(t) $: 사용자의 성향도 시간에 따라 변한다. (baseline rating)
$ p_u(t) $: 시간이 흐름에 따라 아이템에 대한 사용자의 선호는 변화할 수 있다.

단 (설정에 따라) 아이템의 성격은 (이미 만들어졌기에) 변하지 않으므로 아이템은 시간에 관한 함수로 구성되지 않는다. 최종적으로 정리하면 아래와 같은 식이 만들어진다.

[\hat{r_{ui}}(t) = \mu + b_i(t) + b_u(t) + q^T_i p_u(t)]


5. Inputs with varying confidence levels

모든 관측값이 같은 신뢰도(confidence)를 가지는 것은 아니다. 예를 들어 어떤 적대적 사용자는 별 이유 없이 낮은 평점을 제공할 수도 있는 것이다. 따라서 추천 시스템을 더욱 공고히 하기 위해서는, 예측된 선호도에 신뢰도를 붙여야(attach) 한다. 이 신뢰도는 action의 빈도를 설명하는 실수 값인데, 예를 들어 특정 사용자가 특정 show를 얼마나 오래, 자주 보았는가와 같은 값이 신뢰도가 될 수 있다. 이러한 특성을 목적 함수에 반영하면 아래와 같이 될 것이다.

[\min_{p, q, b} \sum_{(u, i) \in K} c_{ui}( r_{ui} - \mu - b_i - b_u - q^T_i p_u )^2 + \lambda (\Vert{q_i}\Vert^2 + \Vert{p_u}\Vert^2 + b^2_u + b^2_i)]

Comment  Read more

잠재요인 협업필터링 (Latent Factor Collaborative Filtering) 설명

|

1. Introduction

추천시스템은 이제는 너무 많은 산업에서 도입하고 있는 시스템이기에 웬만큼 참신하지 않은 이상 새롭게 들리지 않는 것이 현실이다. 그러나 소비자의 입장에서 추천시스템을 보는 것과, 이 시스템의 개발자가 추천시스템을 바라 보는 것에는 큰 차이가 있다. 성공적으로 추천 엔진을 도입한 산업, 기업들이 있는 반면 여러 가지 어려움으로 인해 실질적인 효과가 떨어지는 산업, 기업도 있기 마련이다.

사용자(User)의 행동 양식, 인구학적(Demographic) 정보, 아이템(Item)의 특성, 외부 변수 등 수많은 변인들을 관리하고 분석해서 사용자에게 가장 알맞는 아이템을 추천해주는 일은 분명 쉬운 일은 아니다. 이러한 어려움을 극복하기 위해 연구자들은 과거부터 여러 종류의 추천 시스템을 개발해왔는데, 지금부터 그에 대해 조금씩 알아보고자 한다.

추천 시스템을 만드는 방법에는 굉장히 다양한 방식이 존재하지만, 본 글에서는 가장 핵심이 되는 방법론들에 대해서만 간단히 언급하고자 한다. 추천 시스템은 크게 컨텐츠 기반 필터링(Content Based Filtering) 방식과 협업 필터링(Collaborative Filterin) 방식으로 나뉜다. 협업 필터링은 또 최근접 이웃(Nearest Neighbor) 협업 필터링잠재 요인(Latent Factor) 협업 필터링으로 나뉜다.

과거에는 컨텐츠 기반 필터링최근접 이웃 협업 필터링이 더욱 주목을 받았지만, 2009년에 있었던 넷플릭스 추천 컴퍼티션에서 행렬 분해(Matrix Factorization)를 이용한 잠재 요인 협업 필터링 방식이 우승을 차지하면서, 연구자들은 이 방식에 큰 관심을 갖게 되었다. 현재로서는 많은 경우에 이 방식이 우위를 차지하지만, 상황에 따라서는 다른 방식이 더 좋은 결과를 낼 때도 많고, 하이브리드 형식으로 결합하는 방식 또한 좋은 효율을 보여주는 경우도 많다.

아래에서 보충 설명을 하겠지만 추천 시스템의 대표적인 방법론들을 구조화하면 아래와 같다.

앞으로 여러 시리즈로 이어질 추천 시스템에 관한 글들은, 위에서 언급한 잠재 요인 협업 필터링과 이 방법론에서 출발하여 발전된 알고리즘에 대해 다룰 예정이다.

Matrix Factorization 개념에 Support Vector Machine의 개념을 결합한 것이 Factorization Machines이다. 여기서 더 나아가 개별 feature들의 메타정보(field)를 알고리즘에 반영한 것이 Field-aware Factorization Machines이다. 줄여서 각각 FMFFM이라고 부르는 것이 일반적이다.

로지스틱 모델과 달리 FFM은 가중치를 latent vector화 했기 때문에 연산량과 메모리 사용량이 더 많은 단점이 있지만, 최근 여러 논문에서는 system tuning을 통해 실제 광고 서빙에 사용하는 데 큰 지장이 없음을 밝혔다. 여력이 될 때 더욱 최신 연구들에 대해서도 글을 추가하도록 할 것이다.


2. 추천 시스템의 개요

2.1. 컨텐츠 기반 필터링

어떤 사용자가 특정 아이템을 선호할 때, 그 아이템과 비슷한 컨텐츠를 가진 다른 아이템을 추천하는 것이 이 방식의 기본 아이디어이다. 추가적으로 설명하자면, 이 방식은 사용자와 아이템에 대한 프로필을 만들고 그 특징을 활용한다. 예를 들어 어떤 특정 영화는 장르, 출연배우, 박스오피스 인기도 등 여러 특성을 지니게 될 텐데 이 특성(컨텐츠)들이 이 영화의 프로필을 형성하는 것이다.

2.2. 최근접 이웃 협업 필터링

모든 협업 필터링은 사용자-아이템 행렬 데이터에 의존한다. 사용자가 남긴 평점(rating) 데이터를 기반하여 남기지 않은 데이터를 추론하는 형식이다.

2.2.1. 사용자 기반 최근접 이웃 협업 필터링

특정 사용자와 유사한 사용자들을 선정하고, 이들을 TOP-N이라고 명명한 뒤 이들이 선호하는 아이템을 특정 사용자에게 추천하는 방식이다.

2.2.2. 아이템 기반 최근접 이웃 협업 필터링

어떤 사용자가 A라는 아이템을 선호한다고 할 때, 그 사용자는 A와 유사한 B라는 아이템 역시 선호할 것이라는 가정 하에 추천을 진행하는 방식이다. 아이템 기반 방식이 사용자 기반 방식 보다 정확도가 높은 것이 일반적이기에 본 방식이 더욱 자주 사용된다.

2.3. 잠재 요인 협업 필터링

사용자-아이템 평점 행렬에 잠재되어 있는 어떤 요인(factor)이 있다고 가정하고, 행렬 분해를 통해 그 요인들을 찾아내는 방식이다. 이 잠재 요인은 구체적으로 정의하는 것이 때로는 어렵지만, 실제 시스템에서는 추천의 근거를 마련하는 데에 있어 큰 역할을 수행한다.

예를 들어보면, 영화 장르를 잠재 요인으로 설정할 수 있다. 어떤 사용자는 판타지 영화를 다른 어떤 영화보다 좋아한다고 하면, 이 사용자에게 있어 영화를 선택할 때 가장 중요한 기준(요인)은 판타지 영화이냐 아니냐가 될 가능성이 높다. 그리고 이 사용자에게 다른 영화를 추천해준다고 한다면, 판타지 영화를 추천하는 것이 가장 합리적일 가능성이 높다는 것이다. 잠재 요인 협업 필터링은 이러한 요인들을 찾아 추천에 활용하게 된다.

지금부터는 이 행렬 분해를 어떻게 진행하는지에 대해 알아보도록 하겠다.


3. Singular Value Decomposition

특이값 분해Spectral Decomposition의 일반화 버전이라고 생각하면 쉽다. 즉, 정방행렬이라는 조건을 만족하지 않아도(행과 열의 개수가 달라도) 다차원 행렬을 저차원 행렬로 분해하는 차원 축소 기법이다.

Spectral Decomposition에 따르면 정방행렬 A는 아래와 같이 표현할 수 있다.

[A = P\Lambda P’ = P\Lambda P^T = \sum_{i=1}^{p} \lambda_i e_i e_i’]

여기서 $P$는 $\lambda$에 대응하는 고유벡터들을 열벡터로 가지는 직교행렬이다. $\Lambda$는 $A$의 고유값들을 대각원소로 가지는 대각행렬이다.

(m, n), m>n인 직사각 행렬 $A$에 대해 특이값 분해를 실시하면 아래와 같이 표현될 수 있다.

[A = U\Sigma V^T]

  • $U$: (m, m), $A$의 left singular 벡터로 구성된 직교행렬
  • $V$: (n, n), $A$의 right singular 벡터로 구성된 직교행렬
  • $\Sigma$: (m, n), 주 대각성분이 $\sqrt{\lambda_i}$인 직사각 대각행렬

$AA^T$를 위 식으로 표현하면 아래와 같다.

[AA^T = U\Sigma V^T V\Sigma U^T = U(\Sigma \Sigma^T) U^T]

여기서 $\Sigma \Sigma^T$는 $\Lambda$이다. (직접 계산해보라) 이 때문에 결과적으로 식은 아래와 같이 정리된다.

[AA^T = U\Lambda U^T]

여기서 $U$는 정방행렬이기에 위에서 본 Spectral Decomposition의 식을 참조하면, $U$는 $AA^T$를 Eigenvalue Decomposition으로 직교대각화하여 얻은 직교행렬임을 알 수 있다. $A$의 rank가 k일 때, 이 $U$의 왼쪽에서부터 k번째 열벡터까지를 좌특이벡터(Left Singular Vectors)라고 부른다.

같은 방식으로 $A^TA = V\Lambda V^T$에서 $V$는 $A^TA$를 Eigenvalue Decomposition으로 직교대각화하여 얻은 직교행렬이 된다.

SVD를 기하학적으로 설명하면, $V^T, U$에 의해서 A 행렬의 방향이 변화하게 되고 $\Sigma$에 의해서 scale이 조정된다고 볼 수 있다.


4. 잠재 요인 협업 필터링의 Matrix Factorization

위에서 설명한 SVD는 잠재요인을 밝혀내기에 아주 적합한 방법이지만, 실제 현실에서 원행렬 A에는 결측값이(당연히 모든 사용자가 모든 아이템에 대해 평점을 남겼다면, 굳이 추천 시스템이 필요하지 않을 것이다.) 많다. 따라서 이를 대체할 근사적인 방법이 필요하며, 그 방법에는 SGD(Stochastic Gradient Descent) 또는 ALS(Alternating Least Squares)가 있다. 이 방법들에 대해서는 다음 글을 참조하기 바란다.

SGD를 이용해서 행렬을 분해하면 다음과 같다.

이 때 요인의 개수는 하이퍼파라미터로 임의로 조정하거나, Cross-Validation을 통해 최적의 값을 찾을 수 있다. 위에서 분해된 행렬을 다시 내적하여 원 행렬을 예측해보면 아래와 같이 크게 차이가 나지 않음을 알 수 있다.


5. 간단한 예제

위에서 봤던 행렬 분해를 코드로 구현해보자. 좀 더 자세한 설명을 원한다면 아래 Reference에 있는 “파이썬 머신러닝 완벽 가이드”를 찾아보길 바란다.

import numpy as np
from sklearn.metrics import mean_squared_error

R = np.array([[4, np.NaN, np.NaN, 2, np.NaN],
              [np.NaN, 5, np.NaN, 3, 1],
              [np.NaN, np.NaN, 3, 4, 4],
              [5, 2, 1, 2, np.NaN]])

# 실제 R 행렬과 예측 행렬의 오차를 구하는 함수
def calculate_rmse(R, P, Q, non_zeros):
    error = 0

    full_pred_matrix = np.dot(P, Q.T)

    # 여기서 non_zeros는 아래 함수에서 확인할 수 있다.
    x_non_zero_ind = [non_zeros[0] for non_zeros in non_zeros]
    y_non_zero_ind = [non_zeros[1] for non_zeros in non_zeros]

    # 원 행렬 R에서 0이 아닌 값들만 추출한다.
    R_non_zeros = R[x_non_zero_ind, y_non_zero_ind]

    # 예측 행렬에서 원 행렬 R에서 0이 아닌 위치의 값들만 추출하여 저장한다.
    full_pred_matrix_non_zeros = full_pred_matrix[x_non_zero_ind, y_non_zero_ind]

    mse = mean_squared_error(R_non_zeros, full_pred_matrix_non_zeros)
    rmse = np.sqrt(mse)

    return rmse


def matrix_factorization(R, K, steps=200, learning_rate=0.01, r_lambda=0.01):
    num_users, num_items = R.shape

    np.random.seed(1)
    P = np.random.normal(scale=1.0/K, size=(num_users, K))
    Q = np.random.normal(scale=1.0/K, size=(num_items, K))

    # R>0인 행 위치, 열 위치, 값을 non_zeros 리스트에 저장한다.
    non_zeros = [ (i, j, R[i, j]) for i in range(num_users)
                  for j in range(num_items) if R[i, j] > 0 ]

    # SGD 기법으로 P, Q 매트릭스를 업데이트 함
    for step in range(steps):
        for i, j, r in non_zeros:
            # 잔차 구함
            eij = r - np.dot(P[i, :], Q[j, :].T)

            # Regulation을 반영한 SGD 업데이터 적용
            P[i, :] = P[i, :] + learning_rate*(eij * Q[j, :] - r_lambda*P[i, :])
            Q[j, :] = Q[j, :] + learning_rate*(eij * P[i, :] - r_lambda*Q[j, :])

        rmse = get_rmse(R, P, Q, non_zeros)
        if step % 10 == 0:
            print("iter step: {0}, rmse: {1:4f}".format(step, rmse))

    return P, Q

P, Q = matrix_factorization(R, K=3)
pred_matrix = np.dot(P, Q.T)
print(pred_matrix)

[[3.99062329 0.89653623 1.30649077 2.00210666 1.66340846]
 [6.69571106 4.97792757 0.97850229 2.98066034 1.0028451 ]
 [6.67689303 0.39076095 2.98728588 3.9769208  3.98610743]
 [4.96790858 2.00517956 1.00634763 2.01691675 1.14044567]]

6. Surprise 모듈을 활용한 예제

Movielens 데이터를 이용하여 잠재 요인 협업 필터링을 간단히 시연해보도록 하겠다. 본 모듈은 추천 시스템에 널리 쓰이는 대표적인 알고리즘들을 패키지화한 것으로, 사이킷런의 API와 프레임워크와 굉장히 유사하다. 다만 엄격한 Input 체계를 갖추고 있는데, 반드시 사용자 ID, 아이템 ID, 평점만이 포함되어 있는 Row 레벨 형태의 데이터만 Input으로 받아들인다.

# Surprise 패키지: scikit-surprise
from surprise import SVD, Dataset, accuracy, Reader
from surprise.model_selection import train_test_split, GridSearchCV

data = Dataset.load_builtin('ml-100k')

위에서 쓴 load_builtin 메서드는 Movielens 홈페이지에 들를 필요 없이 해당 사이트의 데이터를 다운로드 받고 로드하는 메서드인데, 사실 앞으로 다른 데이터를 쓴다면 크게 쓸 일이 없다. Surprise 모듈은 데이터 로드를 위해 2개의 메서드를 추가적으로 제공한다.

# load_from_file: OS 파일 로딩
ratings = pd.read_csv('data/ratings.csv')
ratings.to_csv('data/ratings_noh.csv', index=False, header=False)

# line_format: 칼럼을 순서대로 나열함. 공백으로 분리
# rating_scale: 평점의 단위
reader = Reader(line_format='user item rating timestamp', sep=',',
                rating_scale=(0.5, 5))
data = Dataset.load_from_file('data/ratings_noh.csv', reader=reader)
# load_from_df: Pandas DataFrame 으로 로딩
ratings = pd.read_csv('data/ratings.csv')
reader = Reader(rating_scale=(0.5, 50))
data = Dataset.load_from_df(df=ratings[['userId', 'movieId', 'rating']], reader=reader)

이제 데이터셋을 훈련 데이터와 테스트 데이터로 분할한 뒤 적합을 해보자.

trainset, testset = train_test_split(data, test_size=0.25, random_state=0)

# 알고리즘 객체 생성
# SVD: n_factors(K), n_epochs(디폴트 20), biased=True(베이스라인 사용자 편향 적용 여부)
algo = SVD(n_factors=50, random_state=0)
algo.fit(trainset=trainset)

예측을 위해선 test 메서드와 predict 메서드가 제공되는데, 전자의 경우 테스트 데이터셋 전체에 대한 예측 값을, 후자의 경우 하나의 개체에 대한 예측 값을 출력한다. 따라서 predict의 결과를 모은 것이 test의 결과라고 보면 이해하기 쉽다.

predictions = algo.test(testset=testset)
predictions[0:3]

[Prediction(uid='120', iid='282', r_ui=4.0, est=3.66..., details={'was_impossible': False}),
 Prediction(uid='882', iid='291', r_ui=4.0, est=3.97..., details={'was_impossible': False}),
 Prediction(uid='535', iid='507', r_ui=5.0, est=4.15..., details={'was_impossible': False})]

# userID, itemID 는 string 으로 입력해야 함
uid = str(196)
iid = str(302)
pred = algo.predict(uid, iid)
print(pred)

user: 196        item: 302        r_ui = None   est = 4.30   {'was_impossible': False}

# 정확도 평가
accuracy.rmse(predictions=predictions, verbose=True)

Cross-Validation을 통해 파라미터를 조정할 수도 있다. 코드 구현은 아래와 같다.

#cross_validate(algo=algo, data=data, measures=['RMSE', 'MAE'], cv=5, verbose=True)

algo = SVD()
param_grid = {'n_epochs': [20, 40, 60], 'n_factors': [50, 100, 200]}
grid = GridSearchCV(SVD, param_grid, measures=['RMSE', 'MAE'], cv=3)
grid.fit(data=data)

print(grid.best_score['rmse'])
print(grid.best_params['rmse'])

좀 더 자세한 정보와 다양한 기능에 대해 알아보고 싶다면 아래 공식 문서를 참조하길 바란다.


Reference

파이썬 머신러닝 완벽 가이드, 권철민, 위키북스 카카오 리포트 Surprise 모듈 문서 SVD 설명

Comment  Read more

파이썬 numba 모듈 설명

|

1. Introduction

파이썬을 사용하다 보면 편리한 기능에 감탄하는 경우가 많지만 종종 속도에 대한 아쉬움을 느낄 때가 있다. 특히 머신러닝과 관련된 작업들을 하다 보면, 데이터 처리를 하는 과정에 있어서 좀 더 빠른 진행을 요구하는 경우가 많은데, 이를 위한 모듈 중 하나가 Numba이다.

공식문서를 확인해보면 아래와 같은 설명을 찾을 수 있다.

Numba makes Python code fast.
Numba is an open source JIT compiler that translates a subset of Python and NumPy code into fast machine code.

해석하면, 파이썬과 넘파이 코드를 빠르게 실행시켜주는 JIT 컴파일러라고 할 수 있겠다.

Numba의 작동원리는 다음과 같다.

  • 데커레이팅된 함수에 대한 파이썬 bytecode를 일ㄹ고 이를 함수의 입력 인수 유형에 대한 정보와 결합한다.
  • 코드를 분석하고 최적화한 후, LLVM compiler library를 사용하여 함수의 machine code 버전을 반들고, 이를 사용자의 CPU 능력에 맞춘다.
  • 이 compiled된 버전이 앞으로 그 함수를 호출할 때마다 사용된다.

Numba를 사용하기 위해서 새로운 언어를 배운다거나 할 필요는 전혀 없다. 역시 공식문서에서는 아래와 같이 밝히고 있다.

You don’t need to replace the Python interpreter, run a separate compilation step, or even have a C/C++ compiler installed.
Just apply one of the Numba decorators to your Python function, and Numba does the rest.

Numba 모듈이 모든 파이썬 코드를 최적화해주는 것은 아니다. 일부 파이썬 코드와 Numpy에 대해서만 작동하며 대다수의 다른 모듈을 이용한 코드를 최적화 시켜주지는 못한다. 예를 들어 Numba은 Pandas를 이해하지 못한다. 그럼에도 특정 목적에 따라 충분히 활용할 수 있는 가치가 있는 모듈이라고 할 수 있겠다.


2. 기본적인 사용법

2.1. 예시

예시를 살펴보면서 Numba의 효과를 확인해보도록 하겠다.

from time import perf_counter
from numba import jit

# 일반적인 loop
def pure_sum(n):
    result = 0
    for i in range(0, n+1):
        result += i
    return result

# Numba 모듈 사용
@jit(nopython=True, cache=True)
def numba_sum(n):
    result = 0
    for i in range(0, n+1):
        result += i
    return result

# 시간 재기: 일반
start = perf_counter()
pure_sum(100000000)
print(perf_counter() - start)

# 시간 재기에 앞서 먼저 Compile을 해준다.
numba_sum(1)

# 시간 재기: Numba
start = perf_counter()
numba_sum(100000000)
print(perf_counter() - start)

결과는 아래와 같다.

6.040823099999898      # 일반
4.590000003190653e-05  # Numba

pure 파이썬 코드보다 훨씬 빠르다는 것을 확인할 수 있다.

2.2. @jit 데커레이터의 모드

@jit 데커레이터는 nopythonobject라는 2가지 compilation 모드로 작동한다. 위 예제에서 nopython=True를 통해 Numba에게 nopython 모드로 작동하라고 지시한 셈인데, 이 모드는 decorate된 function을 근본적으로 compile하여 Python Interpreter의 개입 없이 전체가 작동하도록 한다.

만약 nopython 모드가 잘 작동하지 않을 경우, Numba은 object 모드를 통해 compile 할 수 있다. @jit(nopython=True)가 아닌 @jit이라고만 데커레이팅하면 이 모드가 작동하게 된다. 본 모드에서는 Numba은 loop를 식별하여 machine code에서 compile하며 나머지는 Intereter code에서 compile하게 된다. 더 나은 성능을 기대한다면 이 모드가 아닌 nopython 모드를 사용해야 한다.

2.3. 다른 Compilation 옵션들

2.3.1. nogil

Numba가 파이썬 코드를 native type과 변수에서만 작동하는 native code로 최적화하고 싶을 때, 파이썬의 GIL(Global Interpreter Lock)을 유지하는 것은 불필요하다.

@jit(nogil=True) 옵션을 사용하면 Numba는 GIL을 해제할 것이다.

2.3.2. cache

파이썬 프로그램을 호출할 때, 컴파일 시간을 피하기 위해 function의 결과를 파일 기반 cache에 쓰도록 Numba에 지시할 수 있다. 이를 실행하기 위해서는 @jit(cache=True) 옵션을 사용하면 된다.

2.3.3. parallel

parallel semantics를 가진 function에 대해 자동화된 병렬화를 제공한다. 반드시 nopython=True 모드에서만 실행되어야 하며 @jit(nopython=True, parallel=True)를 통해 사용할 수 있다.


Reference

공식문서

Comment  Read more