추천시스템 완전 정복

JERRY·2025년 10월 31일
0

Deep Learning

목록 보기
35/35

🎧 추천시스템 완전 정복 — Matrix Factorization & ALS 실습까지 한 번에 이해하기

“추천시스템은 수많은 아이템 중에서
나에게 꼭 맞는 것을 골라주는 기술이다.”

그리고 그 중심에는 Matrix Factorization(MF)ALS(Alternating Least Squares) 모델이 있다.


학습 목표

🔹 최소 목표

  • 음악 감상 횟수(암묵적 데이터)를 기반으로 추천시스템의 원리를 이해한다.
  • CSR(Compressed Sparse Row) 희소 행렬 구조를 익힌다.

🔹 심화 목표

  • Matrix Factorization의 작동 원리와 수학적 구조를 이해한다.
  • implicit 라이브러리를 사용해 ALS 모델을 구현하고
    비슷한 아티스트 / 사용자 맞춤 추천 결과를 분석한다.

1. 추천시스템이란?

추천시스템은 사용자의 취향 데이터를 학습해
나와 비슷한 사람들이 좋아한 것을 나에게 추천”하는 AI 기술입니다.

구분설명예시
콘텐츠 기반사용자가 좋아한 아이템의 속성을 분석해 유사 아이템 추천내가 좋아한 영화의 장르·감독과 유사한 영화 추천
협업 필터링나와 비슷한 사람들의 행동을 기반으로 추천나와 비슷한 사용자가 들은 음악 추천

이번 실습은 협업 필터링(Collaborative Filtering) 기반으로 진행됩니다.


2. 명시적(Explicit) vs 암묵적(Implicit) 데이터

구분설명예시
명시적 데이터사용자가 직접 선호를 표현별점, 평점, 좋아요
암묵적 데이터행동을 통해 간접적으로 선호 추정재생 횟수, 클릭 수, 구매 기록

예시

  • “5점을 준 노래” → 명시적
  • “10번 들은 노래” → 암묵적

이번 프로젝트에서는 암묵적 데이터(Implicit Feedback) 를 사용합니다.


3. Matrix Factorization(MF) 원리

핵심 아이디어

“거대한 유저-아이템 행렬을 두 개의 잠재요인 행렬로 분해하여 숨겨진 패턴을 찾자!”

Rm×nPm×k×Qk×nR_{m×n} ≈ P_{m×k} × Q_{k×n}

  • (R): 사용자×아이템의 평점 또는 재생횟수 행렬
  • (P): 사용자 잠재요인(Latent Features)
  • (Q): 아이템 잠재요인

MF의 목표는

유저와 아이템 벡터의 내적 결과가 실제 선호도와 최대한 비슷하도록 만드는 것.


4. ALS(Alternating Least Squares) 알고리즘 이해

ALS는 MF를 학습하는 효율적인 방법입니다.

  1. ( Q )를 고정하고 ( P )를 최소제곱(Least Squares)으로 업데이트
  2. ( P )를 고정하고 ( Q )를 업데이트
  3. 위 과정을 반복하면서 손실(오차)을 최소화

이 과정에서 “암묵적 데이터의 신뢰도(confidence)”를 반영할 수 있다는 점이 핵심입니다.


5. 추천시스템 실전: Matrix Factorization(ALS)로 음악 추천 만들기

암묵적 피드백(implicit) 기반의 추천시스템을
Matrix Factorization + ALS(Alternating Least Squares) 로 구현

Step 1. 라이브러리 로드 및 데이터 불러오기

  • 왜 이 열만 사용하나요?
    • ALS는 “사용자-아이템-강도(횟수)” 형태의 상호작용만 있으면 충분합니다.
  • plays는 명시적 평점이 아니라 암묵적 선호 강도입니다.
import pandas as pd
import numpy as np
from scipy.sparse import csr_matrix
from implicit.als import AlternatingLeastSquares

# 데이터셋 로드 (TSV 형식)
data = pd.read_csv(
    'lastfm-dataset-360K/usersha1-artmbid-artname-plays.tsv',
    sep='\t',
    usecols=['user_id', 'artist', 'plays']
)

print("데이터 로드 완료")
display(data.head())

Step 2. 인덱싱 및 전처리

  • 문자열 artist는 케이스 혼재가 많아 정규화(소문자) 를 권장합니다.
  • ALS 구현체는 0 ~ (N-1) 범위의 연속 정수 인덱스를 빠르게 처리합니다.
    • 희소행렬의 좌표는 정수 인덱스여야 메모리/연산이 빠름.
    • 추천 결과를 사람이 읽을 수 있도록 역매핑 보관 가능.
# 문자열 통일 (대소문자 구분 제거)
data['artist'] = data['artist'].str.lower()

# 고유 사용자/아티스트 개수
num_users = data['user_id'].nunique()
num_artists = data['artist'].nunique()
print(f"유저 수: {num_users:,}, 아티스트 수: {num_artists:,}")

# user_id, artist 문자열을 숫자 인덱스로 변환
user_to_idx = {u: i for i, u in enumerate(data['user_id'].unique())}
artist_to_idx = {a: i for i, a in enumerate(data['artist'].unique())}

# 인덱싱 적용
data['user'] = data['user_id'].map(user_to_idx)
data['artist_id'] = data['artist'].map(artist_to_idx)
data = data[['user', 'artist_id', 'plays']]
display(data.head())

Step 3. CSR(Compressed Sparse Row) Matrix 생성

  • 희소행렬(CSR) + 신뢰도 가중치: 단순 횟수 vs 의미있는 강도
    • 암묵적 데이터는 “보았다/안보았다”가 아니라, 얼마나 자주 보았는지(신뢰도) 를 반영해야 유리합니다.
    • 유저×아이템 행렬은 대부분의 값이 0(미청취)이기 때문에, 0이 아닌 값만 저장하는 CSR 형태가 메모리 효율적입니다.

3-1) 로그 스케일 가중치 (간단하고 효과적)

  • 왜 로그? 상위 몇 곡만 과도하게 큰 수치가 되는 것을 완화(포화)합니다.
  • alpha? 클수록 “많이 들은 기록”의 비중이 커집니다(튜닝 대상)
# Confidence 가중치: 자주 들은 노래일수록 신뢰도를 높임
alpha = 40
confidence = 1 + alpha * np.log1p(data['plays'])

csr_data = csr_matrix(
    (confidence, (data['user'], data['artist_id'])),
    shape=(num_users, num_artists),
    dtype=np.float32
)

print(f"CSR Matrix 크기: {csr_data.shape}")

3-2) BM25/TF-IDF 가중치 (대안)

  • 사용자 수/아이템 수가 매우 크고, 빈도 분포가 치우칠 때 오류/잡음 완화에 도움
from implicit.nearest_neighbours import bm25_weight

csr_weighted = bm25_weight(csr_data).astype(np.float32)

Step 4. ALS 모델 학습

  • ALS는 P(유저 잠재행렬), Q(아이템 잠재행렬) 을 교대로 업데이트하며
    다음 목적함수를 최소화합니다 (정규화 포함):
    minP,Q(u,i)cui(ruiPuTQi)2+λ(Pu2+Qi2)\min_{P,Q} \sum_{(u,i)} c_{ui} (r_{ui} - P_u^{T} Q_i)^2 + \lambda (\|P_u\|^2 + \|Q_i\|^2)
    • ruir_{ui} : 관측 강도 (여기선 confidence 기반)
    • cuic_{ui} : 신뢰도 (가중치)
    • λ\lambda : 정규화 항 (과적합 방지)
  • 왜 전치? implicit의 ALS는 내부적으로 “아이템 기준”으로 계산합니다.
    factors 증가: 정밀도↑ 가능하지만, 과적합/속도/메모리 trade-off.
    regularization: 크면 과적합 방지↑, 작으면 훈련 데이터 적합↑
# ALS 모델 생성
als_model = AlternatingLeastSquares(
    factors=64,           # 잠재요인 수
    regularization=0.05,  # 정규화 계수 (과적합 방지)
    iterations=15,        # 반복 횟수
    dtype=np.float32
)

# implicit 패키지는 item-user 행렬을 입력으로 받음 → 전치 필요
csr_data_T = csr_data.T

# 학습
als_model.fit(csr_data_T)
print("ALS 모델 학습 완료!")

Step 5. 유사 아티스트 검색: 내적 기반 유사도

  • 유사도: 잠재공간에서의 벡터 내적(dot product) (정규화에 따라 cosine과 유사한 역할)
  • 주의: 학습에 사용된 가중치/정규화/차원 수에 따라 유사도 분포가 달라집니다.
avorite_artist = "coldplay"
artist_id = artist_to_idx[favorite_artist]

# N=10개 비슷한 아티스트 탐색
similar_artists = als_model.similar_items(artist_id, N=10)

print(f"\n '{favorite_artist}'와 비슷한 아티스트 Top 10")
for idx, score in similar_artists:
    print(f"- {list(artist_to_idx.keys())[idx]} (유사도: {score:.3f})")
    
출력 예시    
coldplay ↔ oasis (0.974)
coldplay ↔ u2 (0.963)
coldplay ↔ travis (0.958)
coldplay ↔ keane (0.950)
def similar_artists(artist_name: str, topk: int = 10):
    aid = artist_to_idx.get(artist_name.lower())
    if aid is None:
        return []
    sims = als.similar_items(aid, N=topk)
    # sims: [(artist_id, similarity_score), ...]
    return [(idx_to_artist[i], float(s)) for i, s in sims]

print(similar_artists("coldplay", topk=10))

Step 6. 사용자 맞춤형 아티스트 추천

  • 왜 제외? “새로운” 추천을 원하면 기존 소비 항목을 제거해야 합니다.
  • 차단 리스트(금칙 장르/아티스트 등)도 추가해 현업 품질을 높일 수 있습니다.
# 예시 사용자 선택
user_id = list(user_to_idx.keys())[0]
user_idx = user_to_idx[user_id]

# 개인화 추천
recommendations = als_model.recommend(
    user_idx, csr_data, N=10,
    filter_already_liked_items=True
)

print(f"\n {user_id}님께 추천하는 아티스트 Top 10")
for idx, score in recommendations:
    print(f"- {list(artist_to_idx.keys())[idx]} (예상 선호도: {score:.3f})")
def recommend_for_user(user_name: str, topk: int = 10):
    uid = user_to_idx.get(user_name)
    if uid is None:
        return []
    recs = als.recommend(
        uid, csr_data, N=topk,
        filter_already_liked_items=True  # 이미 소비한 아이템 제외
    )
    return [(idx_to_artist[i], float(s)) for i, s in recs]

# 예시: 임의 사용자
some_user = next(iter(user_to_idx.keys()))
print(some_user, recommend_for_user(some_user, 10))

Step 7. 추천 근거(설명) 확인

  • 무엇을 주나? 해당 사용자에게 이 아이템이 추천된 기여도 상위 아이템과 가중치.
  • 주의: 설명은 모델 내부 점수 기준이며, “완전한 이유”가 아니라 휴리스틱 단서입니다.
target_artist = "rihanna"
artist_id = artist_to_idx[target_artist]

explanation = als_model.explain(user_idx, csr_data, itemid=artist_id)
print(f"\n 왜 '{target_artist}'을 추천했을까?")
print(explanation)

6. 모델 성능 평가

  • 실험은 학습(train) / 평가(test) 를 분리해야 합니다.
  • precision@K, MAP@K 를 사용해 상위 K개 추천이 맞았는가를 측정합니다.
    • Precision@10: 추천 Top10 중 정답(실제 test 상호작용) 비율
    • MAP@10: 정답의 순위까지 고려한 평균 정밀도
    • 주의: 평가는 행동 로그의 스냅샷 특성(롱테일, 시간 누락)에 민감합니다.
from implicit.evaluation import train_test_split, precision_at_k, mean_average_precision_at_k

# 데이터 분리
train, test = train_test_split(csr_data, train_percentage=0.8)

# 학습
als_model.fit(train.T)

# 평가
print("Precision@10 :", precision_at_k(als_model, train, test, K=10))
print("MAP@10       :", mean_average_precision_at_k(als_model, train, test, K=10))

7. 하이퍼파라미터 튜닝: 작은 그리드로도 체감 개선

  • 경험칙: factors ↑ → 표현력↑(성능↑ 가능), 단 과적합/속도/메모리 주의
  • regularization: 너무 작으면 과적합, 너무 크면 과소적합
  • iterations: 10~20에서 수렴하는 경우가 많지만 데이터에 따라 다름
def evaluate(factors, reg, iters):
    model = AlternatingLeastSquares(factors=factors, regularization=reg, iterations=iters, dtype=np.float32)
    model.fit(train.T)
    return precision_at_k(model, train, test, K=10)

cands_f = [32, 64, 96]
cands_r = [0.01, 0.05, 0.1]
cands_i = [10, 15, 20]

best = (-1, None)
for f in cands_f:
    for r in cands_r:
        for it in cands_i:
            score = evaluate(f, r, it)
            if score > best[0]:
                best = (score, (f, r, it))
            print(f"[f={f} r={r} it={it}] precision@10={score:.4f}")

print("Best:", best)

성능/안정성 팁 & 에러 대처

  • dtype: float32 사용으로 메모리 절감 + 속도 향상
  • 스레드 제어: OMP/MKL 스레드 수를 1~4로 줄이면 Colab/로컬에서 안정적
  • Cold-Start: 신입 사용자/아이템은 콘텐츠 기반 특성(장르, 태그)와 하이브리드로 보완
  • 베이스라인 비교: 인기 아이템 추천(룰 기반)을 최소 기준으로 두고 얼마나 개선했는지 수치로 제시
user_freq   = data.groupby('user').size()
artist_freq = data.groupby('artist_id').size()
keep_user   = user_freq[user_freq >= 3].index
keep_artist = artist_freq[artist_freq >= 3].index
df = data[data['user'].isin(keep_user) & data['artist_id'].isin(keep_artist)]

8. 마무리

“추천시스템은 데이터를 분류하는 기술이 아니라,
사람의 취향을 수학적으로 해석하는 기술이다.”

이번 실습을 통해
✅ 추천시스템의 기본 개념
✅ Matrix Factorization의 수학적 원리
✅ ALS 모델의 구현과 성능 평가
를 모두 경험했습니다.

8. 참고 자료

Yifan Hu, Collaborative Filtering for Implicit Feedback Datasets
“7 Variants of Matrix Factorization for Collaborative Filtering” (TDS)
GroupLens MovieLens Dataset, Last.fm Dataset

0개의 댓글