AI 개인화·사용자 모델링: 행동 데이터로 맞춤 추천 시스템 구축

AI 기술

AI 개인화사용자 모델링추천 시스템협업 필터링콜드 스타트

이 글은 누구를 위한 것인가

  • 사용자마다 다른 AI 응답·추천을 제공하고 싶은 팀
  • 클릭, 구매, 체류 시간 등 행동 데이터를 개인화에 활용하고 싶은 팀
  • 콜드 스타트(신규 사용자) 문제를 해결해야 하는 ML 엔지니어

들어가며

넷플릭스가 추천하는 영화와 내가 찾는 영화가 다르다. 이 차이를 만드는 것이 사용자 모델링이다. LLM 시대에는 행동 데이터 + 언어 이해를 결합한 하이브리드 개인화가 가능하다.

이 글은 bluefoxdev.kr의 AI 추천 시스템 설계 를 참고하여 작성했습니다.


1. 사용자 모델링 아키텍처

[사용자 프로파일 구성 요소]

명시적 신호:
  좋아요 / 싫어요
  저장, 북마크
  구매, 전환

암묵적 신호:
  클릭 (관심)
  체류 시간 (몰입도)
  스크롤 깊이 (완독률)
  재방문 패턴

컨텍스트 신호:
  시간대 (아침/저녁)
  디바이스 (모바일/PC)
  세션 길이

[개인화 전략]

콜드 스타트 → 인구통계 기반 + 온보딩 취향 조사
초기 사용자 → 콘텐츠 기반 필터링 (아이템 유사도)
활성 사용자 → 협업 필터링 (유사 사용자)
헤비 유저 → LLM 컨텍스트 이해 + 딥 개인화

[하이브리드 추천 가중치]
  content_score * 0.3 +
  collaborative_score * 0.4 +
  llm_score * 0.3

2. 사용자 임베딩 및 추천 구현

import anthropic
import numpy as np
from dataclasses import dataclass, field
from datetime import datetime

client = anthropic.Anthropic()

@dataclass
class UserProfile:
    user_id: str
    interaction_history: list[dict] = field(default_factory=list)
    explicit_preferences: dict = field(default_factory=dict)
    embedding: list[float] | None = None
    last_updated: datetime = field(default_factory=datetime.utcnow)

def build_user_context_prompt(profile: UserProfile) -> str:
    """사용자 행동 이력을 LLM 컨텍스트로 변환"""
    
    recent = profile.interaction_history[-20:]  # 최근 20개 이벤트
    
    liked = [e["item_title"] for e in recent if e.get("action") == "liked"]
    purchased = [e["item_title"] for e in recent if e.get("action") == "purchased"]
    long_view = [e["item_title"] for e in recent if e.get("dwell_seconds", 0) > 60]
    
    return f"""사용자 행동 패턴:
- 좋아요 한 항목: {', '.join(liked[:5]) or '없음'}
- 구매한 항목: {', '.join(purchased[:5]) or '없음'}
- 오래 본 항목: {', '.join(long_view[:5]) or '없음'}
- 명시적 선호: {profile.explicit_preferences}"""

async def get_llm_recommendations(
    profile: UserProfile,
    candidates: list[dict],
    n: int = 10,
) -> list[dict]:
    """LLM으로 후보 아이템 재랭킹"""
    
    user_context = build_user_context_prompt(profile)
    candidate_list = "\n".join(
        f"{i+1}. {c['title']} - {c['description'][:80]}"
        for i, c in enumerate(candidates[:30])
    )
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=500,
        messages=[{
            "role": "user",
            "content": f"""{user_context}

후보 콘텐츠:
{candidate_list}

이 사용자에게 가장 적합한 항목 번호를 1~{min(n, len(candidates))}개 선택하고,
각 선택 이유를 JSON으로 반환하세요:
[{{"rank": 1, "item_number": 3, "reason": "..."}}]"""
        }]
    )
    
    import json
    rankings = json.loads(response.content[0].text)
    
    result = []
    for r in rankings:
        idx = r["item_number"] - 1
        if 0 <= idx < len(candidates):
            item = candidates[idx].copy()
            item["recommendation_reason"] = r["reason"]
            result.append(item)
    
    return result

def collaborative_score(
    user_embedding: np.ndarray,
    item_embeddings: np.ndarray,
) -> np.ndarray:
    """코사인 유사도 기반 협업 필터링 점수"""
    user_norm = user_embedding / (np.linalg.norm(user_embedding) + 1e-8)
    item_norms = item_embeddings / (np.linalg.norm(item_embeddings, axis=1, keepdims=True) + 1e-8)
    return item_norms @ user_norm

async def personalized_feed(
    profile: UserProfile,
    all_items: list[dict],
    n_results: int = 20,
) -> list[dict]:
    """하이브리드 개인화 피드 생성"""
    
    if len(profile.interaction_history) < 5:
        # 콜드 스타트: 인기 아이템 반환
        return sorted(all_items, key=lambda x: x.get("popularity_score", 0), reverse=True)[:n_results]
    
    # 1단계: 협업 필터링으로 후보 축소
    if profile.embedding:
        user_emb = np.array(profile.embedding)
        item_embs = np.array([item["embedding"] for item in all_items if "embedding" in item])
        scores = collaborative_score(user_emb, item_embs)
        top_indices = np.argsort(scores)[-50:][::-1]
        candidates = [all_items[i] for i in top_indices]
    else:
        candidates = all_items[:50]
    
    # 2단계: LLM으로 재랭킹
    return await get_llm_recommendations(profile, candidates, n=n_results)

마무리

사용자 모델링의 핵심은 "행동이 선호를 드러낸다"는 원칙이다. 클릭보다 구매가, 구매보다 재구매가 강한 신호다. LLM은 텍스트 이해로 콘텐츠 기반 개인화를 강화하지만, 응답 지연이 있으므로 실시간 경로에는 경량 모델을, 배치 재랭킹에는 강력한 모델을 사용하라.