🤖 AI/ML

협업 필터링

Collaborative Filtering

유사 사용자/아이템 기반 추천. Netflix, Amazon에서 활용.

📖 상세 설명

협업 필터링(Collaborative Filtering)은 다수 사용자의 행동 패턴을 분석해 개인화된 추천을 제공하는 알고리즘입니다. "비슷한 취향의 사용자가 좋아한 아이템을 당신도 좋아할 것이다"라는 가정에 기반합니다. Netflix의 영화 추천, Amazon의 상품 추천, Spotify의 음악 추천 등 현대 추천 시스템의 핵심 기술이며, 1990년대 초 개발된 이래 지속적으로 발전해왔습니다.

협업 필터링은 크게 두 가지 접근 방식으로 나뉩니다. 사용자 기반(User-based CF)은 현재 사용자와 유사한 사용자들을 찾아 그들이 좋아한 아이템을 추천합니다. 아이템 기반(Item-based CF)은 사용자가 이미 좋아한 아이템과 유사한 아이템을 추천합니다. Amazon은 아이템 기반을 선호하는데, 아이템 간 유사도가 사용자 간 유사도보다 안정적이고 계산이 효율적이기 때문입니다.

2024-2025년 현재 협업 필터링은 딥러닝과 결합해 크게 발전했습니다. Matrix Factorization(SVD, ALS)은 사용자-아이템 행렬을 저차원 잠재 요인으로 분해합니다. Neural Collaborative Filtering(NCF)은 신경망으로 사용자-아이템 상호작용을 모델링합니다. Two-Tower 모델은 사용자와 아이템 임베딩을 별도 네트워크로 학습해 대규모 서비스에서 효율적입니다. 최근에는 Graph Neural Network 기반 추천과 Transformer를 활용한 순차 추천도 활발히 연구됩니다.

협업 필터링의 주요 과제는 콜드 스타트(신규 사용자/아이템), 희소성(대부분 사용자가 극소수 아이템만 평가), 확장성입니다. 이를 해결하기 위해 콘텐츠 기반 필터링과 결합한 하이브리드 추천, 지식 그래프 활용, 사전학습 임베딩(LLM 기반) 등이 사용됩니다. 실무에서는 추천의 다양성(diversity), 신선도(freshness), 설명가능성(explainability)도 중요한 고려 요소입니다.

💻 코드 예제

PyTorch와 Surprise 라이브러리를 사용한 협업 필터링 구현 예제입니다.

import torch
import torch.nn as nn
import numpy as np
from surprise import Dataset, Reader, SVD, KNNBasic
from surprise.model_selection import cross_validate
import pandas as pd

# 1. Surprise를 사용한 전통적 협업 필터링
def traditional_collaborative_filtering():
    """SVD와 KNN 기반 협업 필터링"""
    # 샘플 데이터 생성
    ratings_dict = {
        'user_id': [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4],
        'item_id': [1, 2, 3, 1, 2, 4, 2, 3, 4, 1, 3, 4],
        'rating': [5, 3, 4, 4, 2, 5, 3, 4, 4, 5, 2, 3]
    }
    df = pd.DataFrame(ratings_dict)

    reader = Reader(rating_scale=(1, 5))
    data = Dataset.load_from_df(df[['user_id', 'item_id', 'rating']], reader)

    # SVD (Matrix Factorization)
    svd = SVD(n_factors=50, n_epochs=20, random_state=42)
    cv_results = cross_validate(svd, data, measures=['RMSE', 'MAE'], cv=3, verbose=True)
    print(f"SVD RMSE: {np.mean(cv_results['test_rmse']):.4f}")

    # User-based KNN
    user_knn = KNNBasic(k=20, sim_options={'name': 'cosine', 'user_based': True})
    cross_validate(user_knn, data, measures=['RMSE'], cv=3, verbose=True)

    # Item-based KNN
    item_knn = KNNBasic(k=20, sim_options={'name': 'cosine', 'user_based': False})
    cross_validate(item_knn, data, measures=['RMSE'], cv=3, verbose=True)

    return svd

# 2. PyTorch로 Neural Collaborative Filtering (NCF) 구현
class NeuralCollaborativeFiltering(nn.Module):
    """NCF: GMF + MLP 결합 모델"""
    def __init__(
        self,
        num_users: int,
        num_items: int,
        mf_dim: int = 32,
        mlp_dims: list = [64, 32, 16]
    ):
        super().__init__()
        self.num_users = num_users
        self.num_items = num_items

        # GMF (Generalized Matrix Factorization) 임베딩
        self.user_embedding_gmf = nn.Embedding(num_users, mf_dim)
        self.item_embedding_gmf = nn.Embedding(num_items, mf_dim)

        # MLP 임베딩 (더 큰 차원)
        self.user_embedding_mlp = nn.Embedding(num_users, mlp_dims[0] // 2)
        self.item_embedding_mlp = nn.Embedding(num_items, mlp_dims[0] // 2)

        # MLP 레이어
        mlp_layers = []
        for i in range(len(mlp_dims) - 1):
            mlp_layers.append(nn.Linear(mlp_dims[i], mlp_dims[i+1]))
            mlp_layers.append(nn.ReLU())
            mlp_layers.append(nn.Dropout(0.2))
        self.mlp = nn.Sequential(*mlp_layers)

        # 최종 예측 레이어
        self.prediction = nn.Linear(mf_dim + mlp_dims[-1], 1)

        self._init_weights()

    def _init_weights(self):
        for module in self.modules():
            if isinstance(module, nn.Embedding):
                nn.init.normal_(module.weight, std=0.01)
            elif isinstance(module, nn.Linear):
                nn.init.xavier_uniform_(module.weight)

    def forward(self, user_ids: torch.Tensor, item_ids: torch.Tensor):
        # GMF 경로
        user_gmf = self.user_embedding_gmf(user_ids)
        item_gmf = self.item_embedding_gmf(item_ids)
        gmf_output = user_gmf * item_gmf  # Element-wise product

        # MLP 경로
        user_mlp = self.user_embedding_mlp(user_ids)
        item_mlp = self.item_embedding_mlp(item_ids)
        mlp_input = torch.cat([user_mlp, item_mlp], dim=-1)
        mlp_output = self.mlp(mlp_input)

        # 결합
        concat = torch.cat([gmf_output, mlp_output], dim=-1)
        prediction = self.prediction(concat)

        return prediction.squeeze()

# 3. Two-Tower 추천 모델 (대규모 서비스용)
class TwoTowerModel(nn.Module):
    """사용자/아이템 타워 분리 모델 - 효율적인 대규모 추천"""
    def __init__(
        self,
        num_users: int,
        num_items: int,
        embedding_dim: int = 64
    ):
        super().__init__()

        # User Tower
        self.user_embedding = nn.Embedding(num_users, embedding_dim)
        self.user_tower = nn.Sequential(
            nn.Linear(embedding_dim, 128),
            nn.ReLU(),
            nn.Linear(128, embedding_dim),
            nn.LayerNorm(embedding_dim)
        )

        # Item Tower
        self.item_embedding = nn.Embedding(num_items, embedding_dim)
        self.item_tower = nn.Sequential(
            nn.Linear(embedding_dim, 128),
            nn.ReLU(),
            nn.Linear(embedding_dim, embedding_dim),
            nn.LayerNorm(embedding_dim)
        )

    def get_user_embedding(self, user_ids: torch.Tensor):
        """사용자 임베딩 - 캐싱 가능"""
        x = self.user_embedding(user_ids)
        return self.user_tower(x)

    def get_item_embedding(self, item_ids: torch.Tensor):
        """아이템 임베딩 - 사전 계산 가능"""
        x = self.item_embedding(item_ids)
        return self.item_tower(x)

    def forward(self, user_ids: torch.Tensor, item_ids: torch.Tensor):
        user_emb = self.get_user_embedding(user_ids)
        item_emb = self.get_item_embedding(item_ids)
        # Dot product similarity
        return torch.sum(user_emb * item_emb, dim=-1)

# 4. 학습 및 추천
def train_ncf():
    """NCF 모델 학습 예시"""
    # 더미 데이터
    num_users, num_items = 1000, 5000
    train_users = torch.randint(0, num_users, (10000,))
    train_items = torch.randint(0, num_items, (10000,))
    train_ratings = torch.rand(10000) * 4 + 1  # 1-5 점수

    model = NeuralCollaborativeFiltering(num_users, num_items)
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    criterion = nn.MSELoss()

    # 학습
    model.train()
    for epoch in range(10):
        optimizer.zero_grad()
        predictions = model(train_users, train_items)
        loss = criterion(predictions, train_ratings)
        loss.backward()
        optimizer.step()

        if epoch % 2 == 0:
            print(f"Epoch {epoch}, Loss: {loss.item():.4f}")

    # 추천 생성
    model.eval()
    user_id = 0
    all_items = torch.arange(num_items)
    user_ids = torch.full((num_items,), user_id)

    with torch.no_grad():
        scores = model(user_ids, all_items)
        top_k_items = torch.argsort(scores, descending=True)[:10]

    print(f"\nUser {user_id}의 Top 10 추천: {top_k_items.tolist()}")
    return model

if __name__ == "__main__":
    print("=== 전통적 협업 필터링 (Surprise) ===")
    traditional_collaborative_filtering()

    print("\n=== Neural Collaborative Filtering ===")
    train_ncf()

📊 협업 필터링 방식 비교

방식 장점 단점 적합한 상황
User-based KNN 직관적, 설명 가능 확장성 낮음 소규모, 사용자 유사성 중요
Item-based KNN 안정적, 캐싱 가능 다양성 부족 아이템 변동 적은 서비스
Matrix Factorization 희소성 극복, 빠름 암묵적 피드백 한계 명시적 평점 데이터
NCF (딥러닝) 비선형 관계 학습 학습 비용 높음 복잡한 상호작용
Two-Tower 대규모 효율적 상호작용 단순화 억 단위 사용자/아이템

🗣️ 실무에서 이렇게 말하세요

💬 회의에서
"신규 사용자 콜드 스타트 문제는 협업 필터링만으로 해결이 어려워요. 가입 시 선호 장르를 물어보거나, 인기 아이템으로 시작한 후 콘텐츠 기반 필터링과 결합한 하이브리드 방식으로 전환하는 게 좋습니다."
💬 면접에서
"협업 필터링은 '비슷한 사용자가 좋아한 것을 추천'하는 방식입니다. User-based와 Item-based로 나뉘며, Matrix Factorization으로 희소한 사용자-아이템 행렬을 저차원 잠재 요인으로 분해해 효율적으로 계산합니다."
💬 기술 토론에서
"대규모 서비스에서 Two-Tower 모델을 쓰면 아이템 임베딩을 미리 계산해두고 ANN 검색으로 밀리초 단위 추천이 가능해요. 사용자 타워만 실시간으로 계산하면 되니까 확장성이 좋습니다."

⚠️ 흔한 실수 & 주의사항

인기 아이템 편향 무시

협업 필터링은 이미 인기 있는 아이템을 더 많이 추천하는 경향이 있습니다. 다양성(diversity) 지표를 모니터링하고, 탐색(exploration) 전략을 포함하세요.

오프라인 지표만으로 평가

RMSE, nDCG 같은 오프라인 지표가 좋아도 실제 클릭률이 낮을 수 있습니다. A/B 테스트로 비즈니스 지표(CTR, 전환율, 체류시간)를 반드시 확인하세요.

하이브리드 접근 및 정기적 재학습

협업 필터링과 콘텐츠 기반을 결합하고, 사용자 행동 변화를 반영하기 위해 정기적으로 모델을 재학습하세요. 실시간 피드백 루프도 고려하세요.

🔗 관련 용어

📚 더 배우기