TIL_57 : 행렬 인수분해

JaHyeon Gu·2021년 11월 26일
0

Machine Learning

목록 보기
15/15
post-thumbnail

🙄 행렬 인수분해


➡ 행렬 인수분해란?

  • 인수분해 : 자연수나 다항식을 여러 개의 인수의 곱으로 변형하는 수학 개념
  • 평점 데이터가 아래와 같을 때 두 개의 행렬 (행렬 1, 행렬 2)로 인수분해 가능

movie1movie2movie3movie4movie54154123523134132242213413=41231322131011001101\begin{array}{|c|c|c|c|c|} movie_1 & movie_2 & movie_3 & movie_4 &movie_5 \\\hline 4 & 1 & 5 & 4 & 1\\2 & 3 & 5 & 2 & 3\\1 & 3 & 4 & 1 & 3\\2 & 2 & 4 & 2 & 2 \\1 &3 & 4 & 1 & 3 \\\end{array} = \begin{array}{|c|c|} 4 & 1\\2 & 3\\1 & 3 \\2 & 2\\1 &3 \\\end{array} * \begin{array}{|c|c|c|c|c|}1 & 0 & 1 & 1 & 0\\0 & 1 & 1 & 0 & 1\\\end{array}

  • 행렬 1은 유저의 취향을 저장하고 있는 행렬 행은 유저들의 순서, 열은 속성에 대한 선호도, 원소는 행에 해당하는 유저가 열에 해당하는 속성을 얼마나 좋아하는지 나타냄

  • 행렬 2는 영화의 속성을 저장하고 있는 행렬 열들에는 영화 순서 행은 영화들의 속성, 원소는 열에 해당하는 영화가 얼마만큼 행에 해당하는 속성인지 나타냄

  • 유저의 취향을 저장하는 행렬을 Θ\Theta
    영화의 속성을 저장하는 행렬을 XX
    평점 행렬을 RR을 써서 나타내면 아래와 같이 인수분해
    R=ΘXR=\Theta X

각 원소는 유저의 취향과 영화의 속성의 곱이다

  • 평점 몇 개가 비어 있을 때, 나머지 평점들에 대해 완벽하게 인수분해 할 수 있다면
    빈 값들은 쉽게 구할 수 있다

  • 하지만 빈값들이 있으면 행렬 인수분해를 하기 어렵다

곱했을 때 최대한 평점 행렬과 비슷하게 나오는 두 행렬을 구하는 것이 핵심


➡ 행렬 인수분해 속성

  • 영화 속성을 입력 변수, 평점 데이터를 목표 변수로 유저 취향을 예측한 내용 기반 추천
  • 행렬 인수분해는 두 행렬, 즉 유저의 취향, 영화의 속성 둘 다 머신 러닝 기법으로 구함
  1. 유저 취향, 영화 속성 행렬을 임의로 초기화
  2. 초기화한 행렬들을 곱해서 예측값을 계산
  3. 실제 평점 데이터와 얼마나 유사한지 측정
  4. 손실을 가장 빨리 줄일 수 있는 방향으로 두 행렬 원소 값들을 업데이트
  5. 경사 하강을 여러 번 반복
  • 속성의 값들을 머신 러닝으로 최적화하는 방법을 속성 학습,
    feature learning 이라고 함

➡ 데이터 표현

  • 영화 속성 : xk(j)x_k^{(j)} : jj번째 영화의 kk번째 속성
  • 영화의 개수 : nmn_m
  • 유저 취향 : θk(i)\theta_k^{(i)} : ii번째 유저의 kk번째 취향
  • 유저의 개수 : nun_u
  • 평점 데이터 : y(i,j)y^{(i,j)} : ii번째 유저의 jj번째 영화 평점
    y(i,j)=(θ(i))Tx(j)y^{(i,j)}=(\theta^{(i)})^Tx^{(j)}
  • r(i,j)r^{(i,j)} : ii번째 유저가 jj번째 영화 평점을 줬다면 11, 안줬다면 00



🙄 손실 함수


➡ 손실 함수

  • J(Θ,X)=12i,j:r(i,j)=1((θ(i))Tx(j)y(i,j))2J(\Theta, X)=\frac{1}{2}\sum_{i,j:r^{(i,j)}=1} ((\theta^{(i)})^Tx^{(j)}-y^{(i,j)})^2
  • 평점이 없는 예측값들의 오차는 건너 뜀
  • i,j:r(i,j)=1i,j:r^{(i,j)}=1 : 유저가 실제로 평점을 준 영화들에 대해서만 돌겠다는 내용

➡ 손실 함수 구현

import numpy as np

def cost(prediction, R):
    """행렬 인수분해 알고리즘의 손실을 계산해주는 함수"""
    cost_array = (prediction - R) ** 2
    return np.nansum(cost_array)             
    
# 실행 코드

# 예측 값 행렬
prediction = np.array([
    [4, 4, 1, 1, 2, 2],
    [4, 4, 3, 1, 5, 5],
    [2, 2, 1, 1, 3, 4],
    [1, 3, 1, 4, 2, 2],
    [1, 2, 4, 1, 2, 5],
    ])

# 실제 값 행렬
R = np.array([
    [3, 4, 1, np.nan, 1, 2],
    [4, 4, 3, np.nan, 5, 3],
    [2, 3, np.nan, 1, 3, 4],
    [1, 3, 2, 4, 2, 2],
    [1, 2, np.nan, 2, 2, 4],
    ])

cost(prediction, R)


# 10.0

➡ 손실 함수 볼록도

  • 선형 회귀의 손실 함수는 아래로 볼록한 함수

  • 하지만 행렬 인수분해의 손실 함수는 아래로 볼록하지 않음

  • 선형 회귀와는 달리 변수가 θ\theta뿐만 아니라 xx가 있고 이 둘이 곱해졌기 때문

  • 2차 이상의 다항 함수처럼 여러 극소점이 존재

  • 경사 하강법을 해도 손실을 가장 작게 만드는 θ\thetaxx값을 찾았다고 할 수 없음

  • 그래도 임의로 설정한 값들보다 경사 하강법을 사용해서 구한 값들이 항상 성능이
    더 좋고 많은 경우 최소점이 아니라 극소점을 찾아도 성능이 충분히 좋게 나옴

  • 이러한 문제점을 극복하기 위해서는 임의로 초기화를 여러번해서
    경사 하강법을 많이 해본 후 가장 성능이 좋게 나온 모델을 사용하는 방식 상요



🙄 경사 하강법


➡ 경사 하강법

  • 목표는 ΘXR\Theta X \approx R
  • 최대한 곱해서 RR에 가깝게 나오는 Θ,X\Theta,X를 구하는 것
  • 손실 함수 J(Θ,X)J(\Theta,X)를 최소화하는 Θ,X\Theta, X를 찾는 것
  • 아래와 같이 변수들을 업데이트 하면서 손실을 줄여 나감
    θk(i)θk(i)αθk(i)J(Θ,X)\theta^{(i)}_k\leftarrow\theta^{(i)}_k-\alpha\frac{\partial}{\partial\theta^{(i)}_k}J(\Theta,X)
    xk(j)xk(j)αxk(j)J(Θ,X)x^{(j)}_k\leftarrow x^{(j)}_k-\alpha\frac{\partial}{\partial x^{(j)}_k}J(\Theta,X)
  • 편미분 부분들을 실제로 계산하면 아래와 같음
    θk(i)θk(i)αj:r(i,j)=1((θ(i))Tx(j)y(i,j))xk(j)\theta^{(i)}_k\leftarrow\theta^{(i)}_k-\alpha\sum_{j:r^{(i,j)}=1} ((\theta^{(i)})^Tx^{(j)}-y^{(i,j)})x^{(j)}_k
    xk(j)xk(j)αi:r(i,j)=1((θ(i))Tx(j)y(i,j))θk(i)x^{(j)}_k\leftarrow x^{(j)}_k-\alpha\sum_{i:r^{(i,j)}=1} ((\theta^{(i)})^Tx^{(j)}-y^{(i,j)})\theta^{(i)}_k



🙄 행렬 인수분해 정규화


➡ 다항 회귀에서의 과적합

과적합

  • 머신 러닝 모델이 training set에 너무 딱 맞은 나머지
    test set에서 성능이 안 좋게 나오는 것을 말함

정규화

  • 과적합을 막는 방법 중 하나인 정규화
  • 손실 함수에 특정 항을 더해서 각 파라미터들의 크기라 너무 커지는 것을 방지
  • 다항 회귀의 경우는 아래와 같음
    J(θ)=J(\theta)=12mi=1m(hθ(x(i))y(i))2+λi=1nθi2\frac{1}{2m}\sum_{i=1}^m (h_\theta(x^{(i)})-y^{(i)})^2+\lambda\sum_{i=1}^n\theta_i^2
  • 행렬 인수분해를 할 때도 마찬가지 손실 함수에 정규화 항을 더해줌
  • 다만 변수가 Θ,X\Theta, X 두 개라는 차이가 있음
  • 따라서 정규화 항 역시 두 개고 아래와 같음
    λ2i=1nuk=1n(θk(i))2,λ2j=1nmk=1n(xk(j))2\frac{\lambda}{2}\sum_{i=1}^{n_u}\sum_{k=1}^{n} (\theta_k^{(i)})^2, \frac{\lambda}{2}\sum_{j=1}^{n_m}\sum_{k=1}^{n} (x_k^{(j)})^2
  • 손실 함수에 정규화 항을 더하면 됨
    J(Θ,X)=12i,j:r(i,j)=1((θ(i))Tx(j)y(i,j))2+λ2i=1nuk=1n(θk(i))2+λ2j=1nmk=1n(xk(j))2J(\Theta, X)=\frac{1}{2}\sum_{i,j:r^{(i,j)}=1} ((\theta^{(i)})^Tx^{(j)}-y^{(i,j)})^2+\frac{\lambda}{2}\sum_{i=1}^{n_u}\sum_{k=1}^{n} (\theta_k^{(i)})^2+ \frac{\lambda}{2}\sum_{j=1}^{n_m}\sum_{k=1}^{n} (x_k^{(j)})^2
  • 경사 하강법을 적용할 때 역시 위 함수를 사용, 편미분을 통해 각 파라미터 업데이트
    θk(i)θk(i)α(j:r(i,j)=1((θ(i))Tx(j)y(i,j))xk(j)+λθk(i))\theta^{(i)}_k\leftarrow\theta^{(i)}_k-\alpha(\sum_{j:r^{(i,j)}=1} ((\theta^{(i)})^Tx^{(j)}-y^{(i,j)})x^{(j)}_k+\lambda\theta^{(i)}_k)
    xk(j)xk(j)α(i:r(i,j)=1((θ(i))Tx(j)y(i,j))θk(i)+λxk(j))x^{(j)}_k\leftarrow x^{(j)}_k-\alpha(\sum_{i:r^{(i,j)}=1} ((\theta^{(i)})^Tx^{(j)}-y^{(i,j)})\theta^{(i)}_k+\lambda x^{(j)}_k)



🙄 scikit-learn으로 행렬 인수분해


➡ 행렬 인수분해 경사 하강법 구현

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 체점을 위해 임의성을 사용하는 numpy 도구들의 결과가 일정하게 나오도록 해준다
np.random.seed(5)
RATING_DATA_PATH = './data/ratings.csv'  # 데이터 파일 경로 정의
# numpy 출력 옵션 설정
np.set_printoptions(precision=2)
np.set_printoptions(suppress=True)

def predict(Theta, X):
    """유저 취향과 상품 속성을 곱해서 예측 값을 계산하는 함수"""
    return Theta @ X


def cost(prediction, R):
    """행렬 인수분해 알고리즘의 손실을 계산해주는 함수"""
    return np.nansum((prediction - R)**2)


def initialize(R, num_features):
    """임의로 유저 취향과 상품 속성 행렬들을 만들어주는 함수"""
    num_users, num_items = R.shape
    
    Theta = np.random.rand(num_users, num_features)
    X = np.random.rand(num_features, num_items)
    
    return Theta, X


def gradient_descent(R, Theta, X, iteration, alpha, lambda_):
    """행렬 인수분해 경사 하강 함수"""
    num_user, num_items = R.shape
    num_features = len(X)
    costs = []
        
    for _ in range(iteration):
        prediction = predict(Theta, X)
        error = prediction - R
        costs.append(cost(prediction, R))
                          
        for i in range(num_user):
            for j in range(num_items):
                if not np.isnan(R[i][j]):
                    for k in range(num_features):
                        Theta[i][k] -= alpha * (np.nansum(error[i, :] * X[k, :]) + lambda_ * Theta[i][k])
                        X[k][j] -= alpha * (np.nansum(error[:, j] * Theta[:, k]) + lambda_ * X[k][j])
                        
    return Theta, X, costs


#----------------------실행(채점) 코드----------------------
# 평점 데이터를 가지고 온다
ratings_df = pd.read_csv(RATING_DATA_PATH, index_col='user_id')

# 평점 데이터에 mean normalization을 적용한다
for row in ratings_df.values:
    row -= np.nanmean(row)
       
R = ratings_df.values
        
Theta, X = initialize(R, 5)  # 행렬들 초기화
Theta, X, costs = gradient_descent(R, Theta, X, 200, 0.001, 0.01)  # 경사 하강
    
# 손실이 줄어드는 걸 시각화 하는 코드 (디버깅에 도움이 됨)
# plt.plot(costs)

Theta, X


# (array([[-0.35,  1.56,  0.31, -0.21, -0.26],
#         [ 0.92,  0.21,  0.36,  0.56,  0.99],
#         [ 0.48,  0.55, -0.19,  0.06,  1.71],
#         [-0.64,  1.03,  0.35, -0.32,  0.13],
#         [-0.39, -0.68,  0.44,  0.05,  1.05],
#         [ 0.07, -0.64,  0.92,  1.23, -0.58],
#         [ 0.33,  0.93, -1.21,  2.09,  0.27],
#         [ 0.79, -0.48,  1.12,  0.05,  0.46],
#         [ 1.06, -0.68, -0.28,  0.18, -1.12],
#         [ 0.39,  0.63,  0.14,  0.98,  0.1 ],
#         [ 1.47,  0.62, -0.91, -0.29, -0.35],
#         [-1.56,  0.77,  0.83,  1.1 ,  0.13],
#         [-0.89,  0.47,  0.47, -0.25,  0.81],
#         [ 0.86, -0.13, -1.01,  0.2 ,  0.76],
#         [-0.53, -1.14, -0.47,  0.08, -0.72],
#         [-0.27, -0.07,  0.41,  0.49,  1.5 ],
#         [ 0.17, -0.01,  0.07, -1.66,  0.27],
#         [ 1.32,  0.88,  0.83,  0.72, -1.09],
#         [-0.17, -1.68,  1.86, -0.16, -0.26],
#         [-0.88, -0.53, -1.33,  0.14,  0.19]]),
#  array([[ 0.12,  0.48, -2.18, -0.67, -1.05,  0.41,  0.03, -0.37, -0.86,
#           0.44, -0.71,  1.26, -0.55,  0.17,  0.74,  0.94, -0.07,  1.98,
#           1.12,  0.68],
#         [-0.61, -0.4 , -0.12,  0.11, -0.22,  0.1 ,  0.71, -0.36,  0.97,
#           0.95,  0.62, -0.72,  0.26, -1.56,  0.18, -0.28, -0.29,  1.7 ,
#           0.02, -0.87],
#         [ 0.12,  1.59,  0.25,  1.02, -1.  ,  0.88, -0.27,  0.39,  0.33,
#           0.48, -1.17, -0.05, -1.69,  0.65, -0.12, -1.09, -0.89, -0.35,
#           0.65,  0.47],
#         [ 0.33, -0.84, -0.73, -0.55,  0.11,  1.18, -1.  ,  0.15,  0.29,
#          -0.21,  0.76,  0.46, -0.59, -0.5 , -0.92, -0.21,  0.86,  0.45,
#           1.77, -0.02],
#         [-0.75, -0.25, -0.72,  1.1 ,  0.94,  0.54,  0.55, -1.34, -1.28,
#           1.08,  0.79,  0.63, -0.68,  0.21,  1.02, -0.46, -0.06, -0.81,
#           0.93, -0.72]]))

➡ 결과 해석

  • 최종적으로 얻게 되는 취향과 속성값들은 정확히 무엇에 대한 취향과 속성인지 알 수 없음
  • 프로그램은 각 속성이 어떤 의미를 갖는지 모른 채 예측을 잘하는
    아무 취향과 속성값들을 찾아내는 것
  • 따라서 학습하려는 데이터에 미리 의미를 부여해서 초기화하는 건 의미 없음
  • 각 데이터에 의미를 부여하지 않고 작은 임의의 값들로 초기화
  • 어떤 속성을 쓸지는 정하지 않지만, 몇 개를 쓸지는 정해야 함
  • 교차 검증과 그리드 서치를 사용해서 성능이 가장 좋은 개수로 정함

➡ 행렬 인수분해 마무리

  • 행렬 인수분해도 협업 필터링 방식의 한 종류
  • 속성값들이 학습될 때 모든 유저의 취향에 영향을 받음
  • 행렬 인수분해는 협업 필터링의 장단점을 모두 공유
  • 아무리 행렬 인수분해 방식의 성능이 좋아도 방식 하나만 사용하는 경우는 드뭄
  • 최적의 성능을 위해서 여러 방식을 섞어서 사용
  • 각 모델의 단점을 보완하면서 장점을 살릴 수 있다
profile
IWBAGDS

0개의 댓글