한양과학기술고등학교

에듀테크 프로젝트 대시보드

협업 필터링 & 행렬 분해

협업 필터링 & 행렬 분해

"넷플릭스·유튜브·스포티파이는 어떻게 내가 좋아할 것을 알까?" — 남들의 평점으로 내 빈칸을 채우는 법

추천 시스템의 핵심 아이디어는 놀랍도록 단순하다. "나와 취향이 비슷한 사람들이 좋아한 것을 나도 좋아할 것이다." 영화의 줄거리나 장르 정보(콘텐츠 기반)를 전혀 몰라도, 평점 기록만 있으면 추천이 가능하다. 이것을 협업 필터링(Collaborative Filtering)이라고 부른다.

문제는 평점표가 거의 다 비어 있다는 것이다. 넷플릭스 사용자는 전체 영화의 1%도 보지 않는다. 추천 = 이 거대한 희소 행렬(sparse matrix)의 빈칸을 추정하는 일이다.

1. 평점 행렬 — 우리가 채워야 할 빈칸

6명의 사용자가 8편의 영화에 매긴 1~5점 평점이다. 회색 빈칸(·)은 아직 안 본 영화다. 아래에서 두 가지 방법으로 이 빈칸을 채워 본다.

4~5점 (좋아함) 3점 (보통) 1~2점 (싫어함) · 안 봄

2. 사용자 기반 협업 필터링 (User-based CF)

대상 사용자 한 명을 고르고, 함께 평가한 영화들의 평점 패턴이 얼마나 닮았는지를 다른 모든 사용자와 비교한다. 닮은 정도는 두 평점 벡터의 코사인 유사도로 잰다:

sim(u, v) = ( ru · rv ) / ( ‖ru‖ ‖rv‖ )   — 함께 본 영화들의 평점만 사용

그 다음, 빈칸이 있는 영화에 대해 유사도를 가중치로 한 이웃들의 평균 평점으로 예측한다:

예측(u, 영화 i) = Σv sim(u,v) · rv,i  /  Σv |sim(u,v)|

다른 사용자들과의 유사도 (가운데 = 0, 오른쪽 = 닮음, 왼쪽 = 반대 취향):

가중 평균 예측
—
가장 닮은 이웃
—
추천 여부
—

실제 시스템은 평균 중심화(mean-centering)를 한다. "모든 영화에 후하게 4점 주는 사람"과 "짜게 2점 주는 사람"의 편차 패턴만 비교하려고 각자의 평균을 빼고 유사도를 잰다(피어슨 상관계수). 또 유사도 상위 k명(k-최근접 이웃)만 골라 쓴다. → KNN 과 같은 원리.

3. 행렬 분해 (Matrix Factorization) — 잠재 요인 찾기

유사도 방식은 사용자가 수백만이 되면 느리다. 2006년 넷플릭스 프라이즈(100만 달러 상금)에서 우승 팀들이 쓴 방법은 전혀 다르다. 거대한 평점 행렬 R (m×n) 을 두 개의 작고 뚱뚱한 행렬의 곱으로 근사한다:

R (m×n)  ≈  P (m×k)  ·  QT (k×n)     예측 r̂u,i = pu · qi

k개의 숨은 축(잠재 요인, latent factor)이 자동으로 만들어진다. 사람이 정해 주지 않아도 학습이 끝나면 축 하나는 대략 "액션 ↔ 로맨스", 다른 하나는 "가벼움 ↔ 진지함" 같은 의미를 띠게 된다. 각 사용자 pu 와 각 영화 qi 는 이 k차원 공간의 한 점이 되고, 두 점의 내적(방향이 맞을수록 큼)이 예측 평점이다.

학습은 관측된 평점에 대해서만 오차 제곱을 경사 하강법으로 줄인다 (λ 는 과적합을 막는 규제 항):

minP,Q   Σ(u,i)∈관측 ( ru,i − pu·qi )2   +   λ ( ‖pu‖2 + ‖qi‖2 )
에폭
0
학습 RMSE
—
테스트 RMSE
—
잠재 요인 k
2

관측 평점의 20%는 테스트용으로 가려 두고 학습에 쓰지 않는다. 테스트 RMSE가 같이 내려가면 "남의 취향 패턴"을 진짜로 배운 것이고, 학습만 내려가고 테스트가 다시 오르면 과적합이다.

4. 잠재 공간 들여다보기

학습된 k=2 잠재 요인 공간이다. ● 는 사용자, ▲ 는 영화. 같은 방향으로 멀리 뻗은 사용자·영화 쌍일수록 내적(예측 평점)이 크다. 아래에서 한 쌍을 골라 예측이 어떻게 계산되는지 확인해 보라.

5. 빈칸을 모두 채운 결과

"학습 시작"을 눌러 충분히 학습한 뒤 아래를 보면, 원래 비어 있던 칸까지 전부 예측 평점으로 채워진다. 진하게 칠해진 높은 점수 칸이 그 사용자에게 추천할 영화다.

테두리 없는 칸 = 실제 평점 · 기울임꼴 = 모델이 채운 예측. 예측이 실제와 얼마나 가까운지 비교해 보라.

6. 현실의 어려움

문제무슨 일이 벌어지나대응
콜드 스타트신규 가입자·신작 영화는 평점이 0개 → 잠재 벡터를 만들 수 없음인기 순위·콘텐츠 기반 추천으로 시작, 가입 시 취향 설문
희소성행렬의 99% 이상이 빈칸행렬 분해(관측값만 사용), 암묵적 피드백(클릭·시청 시간)
인기 편향인기작만 계속 추천 → 필터 버블, 롱테일 콘텐츠 소외다양성·참신성 재순위, 탐험(exploration) 섞기
확장성사용자 수억 명 × 콘텐츠 수백만 개ALS 병렬화, 근사 최근접 이웃(k-d 트리·HNSW), 임베딩 캐시
피드백 루프추천된 것만 소비되고, 그 로그로 다시 학습 → 편향 강화노출 보정(IPS), 무작위 노출 홀드아웃

미니 구현 — SGD 행렬 분해

# R: {(user, item): rating} 형태의 관측 평점만 저장
import random

def train(R, n_users, n_items, k=2, lr=0.02, reg=0.08, epochs=200):
    P = [[random.gauss(0, 0.1) for _ in range(k)] for _ in range(n_users)]
    Q = [[random.gauss(0, 0.1) for _ in range(k)] for _ in range(n_items)]
    for _ in range(epochs):
        for (u, i), r in R.items():
            pred = sum(P[u][f] * Q[i][f] for f in range(k))
            err  = r - pred                     # 관측 오차
            for f in range(k):                  # 두 벡터를 동시에 밀어줌
                pf, qf = P[u][f], Q[i][f]
                P[u][f] += lr * (err * qf - reg * pf)
                Q[i][f] += lr * (err * pf - reg * qf)
    return P, Q

# 예측: 두 잠재 벡터의 내적
def predict(P, Q, u, i):
    return sum(a * b for a, b in zip(P[u], Q[i]))

왜 중요한가

1. 전자상거래·미디어 매출의 엔진. 아마존 매출의 큰 부분, 넷플릭스 시청의 대부분, 유튜브·틱톡 피드가 이 원리로 굴러간다. "무엇을 다음에 보여줄까"는 곧 돈이다.

2. 임베딩(embedding)의 원조. "대상을 저차원 벡터로 바꾸고 내적으로 관계를 잰다"는 행렬 분해의 발상은 그대로 단어 임베딩·추천 임베딩·검색으로 이어진다. 오늘날 딥러닝 추천 모델(신경망 협업 필터링, two-tower)도 결국 이 벡터 내적을 신경망으로 확장한 것이다.

3. 최소제곱·경사 하강·PageRank 와 한 가족. "관측을 설명하는 저차원 구조를 최적화로 찾는다"는 선형대수의 핵심 패턴이다. SVD·PCA 와도 사촌 관계다.