Deep Learning/Recommender system

[Recommender System] 공분산(Covariance)

언킴 2022. 2. 16. 19:30
반응형

추천 시스템에서는 차원 축소 기법을 사용하여 모델을 구축할  때 공분산을 다룬다. 차원 축소는 다차원 행렬을 저차원으로 보내어 희소성 문제를 일부 완화할 수 있게 만들어주는 기법이다. 지금은 행을 기준으로 계산이 이루어지면 PCA(Principal Component Analysis), 열을 기준으로 계산이 이루어지면 FA(Factor Analysis) 정도로만 이해해도 무방하다. 사용자-아이템 간 평점 행렬은 일반적으로 희소(Sparsity) 행렬이라고 부른다. 쇼핑몰에는 방대한 양의 아이템이 존재하는데, 사용자는 모든 아이템에 대해 평점을 측정하는 것이 아니고, 일부에 대해서만 평점을 부여하기 때문이다. 이전에 다루었던 SVD(Singular Value Decomposition)과 비슷한 맥락이며, 결과 역시 비슷하게 도출이 된다. 

 

우리는 평점 행렬을 다룰 때 bias에 대해서 생각해볼 필요가 있다. 평점 행렬에서 nan 값을 우리가 단순히 결측치라고 생각하고, 제거한 후 계산을 수행하기에는 결측값이 너무나 많다. 그렇기 때문에 우리는 결측값을 대체하는 방안을 세워야한다. 결측치를 대체하는 방법으로는 평균으로 대체, 0으로 대체 등 다양한 방법이 존재하는데, 여기서는 평균으로 대체하는 방법과 결측값을 제거한 후 공분산을 계산할 때 어떠한 결과 차이가 보이는지를 다루어 볼 것이다. 

 

다음과 같은 평점 행렬이 있다고 가정하자. 각 사용자가 영화를 시청한 후 평점을 측정하였고, 평점 스케일은 1부터 7까지 값을 가진다. ?로 표시된 부분은 평점을 측정하지 않은 경우다. 아래의 표를 참고하면 <반도>의 경우 사용자가 평점을 거의 측정하지 않았다. 신규 영화일수도 있고, 인기가 없어서 평가를 하지 않았을 수도 있다.  그렇다면 이 경우 결측치를 0으로 대체 하여야할까? 아니면 그 전 까지 측정된 평점을 기반으로 <반도>의 결측치를 대체 하여야할까? 두 경우를 모두 계산해보자.

 

user 스파이더맨 괴물 반도
1 1 1 1
2 7 7 7
3 3 1 ?
4 5 7 ?
5 3 1 ?
6 5 7 ?
7 3 1 ?
8 5 7 ?
9 3 1 ?
10 5 7 ?
11 3 1 ?
12 5 7 ?

 

data = pd.DataFrame([
    [1, 7, 3, 5, 3, 5, 3, 5, 3, 5, 3, 5], 
    [1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7],
    [1, 7, 1, 7, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan]
]).T

data.columns = ['스파이더맨', '괴물', '반도']

data_pre = data.fillna(data.loc[:,'반도'].mean())

cov_df = np.cov(data_pre.T)

cov_df
np.cov(data.T)

# implementation covariance 
covariance = []
for i in range(len(data.columns)):
    cov = []
    for j in range(len(data.columns)):
        x = data.iloc[:,i].copy()
        y = data.iloc[:,j].copy()
        
        x_mean = np.mean(x)
        x_std = np.std(x)
        
        y_mean = np.mean(y)
        y_std = np.std(y)
        
        cov.append(np.sum( (x - x_mean) * (y - y_mean)) / (len(x) - 1))
        
    covariance.append(cov)

np.array(covariance)

위의 경우 결측치를 반도의 평균 값으로 대체하여 입력하였다. 그 결과는 아래와 같다.

 

  스파이더맨 괴물 반도
스파이더맨 2.55 4.36 2.18
괴물 4.36 9.82 3.27
반도 2.18 3.27 3.27

 

covariance = []
for i in range(3):
    cov = []
    for j in range(3):
        xy = data.iloc[:,[i,j]].copy().dropna(axis = 0)
        
        x = xy.iloc[:,0].copy()
        y = xy.iloc[:,1].copy()
        
        x_mean = np.mean(x)
        x_std = np.std(x)
        
        y_mean = np.mean(y)
        y_std = np.std(y)
        
        cov.append(np.sum( (x - x_mean) * (y - y_mean)) / (len(x) - 1))

    
    covariance.append(cov)

np.array(covariance)

위의 경우 결측값을 대체한 후 공분산을 계산하는 코드이며, 결과는 아래와 같다. 

 

  스파이더맨 괴물 반도
스파이더맨 2.55 4.36 8
괴물 4.36 9.82 12
반도 8 12 12

 

결과를 보면 평균치로 대체했을 때와 대체하지 않았을 때의 분산 차이가 큰 것을 확인할 수 있다. 이 예제를 통해서 일부 데이터 셋에서는 결측치를 처리하는 것에 있어 매우 중요한 것을 확인할 수 있다. 행렬에서 결측치의 수에 따라 결측치의 수가 많을수록 바이어스(bias)는 커지게 된다. 일부 연구에서는 평균치로 대체하는 것이 아니라 단순히 값이 존재하는 경우에만 계산하는 형태로 진행된다. 해당 기술이 항상 효과적인 것은 아니지만 평균으로 대체하는 경우보다 성능이 좋은 것으로 알려져있다.