신경 협업 필터링과 딥러닝 추천 시스템의 콜드 스타트 대응

Neural Collaborative Filtering과 Two-Tower 기반 추천 시스템의 후보생성·랭킹 구조, 콜드 스타트 대응과 운영 설계를 정리한다.

2026-08-14 · 최초 발행 2024-04-29

추천 파이프라인에서 NCF가 맡는 자리

대규모 개인화 환경에서는 정밀도뿐 아니라 후보 탐색 범위와 서빙 확장성도 함께 다뤄야 한다. Deep Learning Recommenders는 후보생성(Retrieval), 정렬(Ranking), 재정렬·블렌더(Re-ranking/Blender)의 계층으로 이를 나눈다.

Neural Collaborative Filtering(NCF)은 사용자와 아이템의 상호작용을 신경망으로 근사하는 협업 필터링 모델 계열이다. 행렬분해(Matrix Factorization, MF)의 내적을 비선형 신경망으로 바꿔 더 복잡한 상호작용 패턴을 학습한다. 대표적인 구조는 GMF(Generalized MF), MLP 기반 상호작용층, 두 구조를 병렬로 결합한 NeuMF다.

이 계열의 추천 모델은 조회, 클릭, 시청지속시간 같은 암묵적 피드백을 중심으로 대규모 데이터를 학습한다. 후보생성에는 Two-Tower나 YouTube DNN이, 랭킹에는 Wide & Deep, DIN/DIEN, Sequential(Transformer) 모델이 쓰일 수 있다.

콜드 스타트 문제는 신규 사용자, 신규 아이템, 신규 도메인처럼 상호작용 데이터가 부족한 상태에서 추천 정확도가 낮아지는 현상이다. 사용자(User Cold Start), 아이템(Item Cold Start), 시스템(System Cold Start)으로 나눠 대응 범위를 잡는다.

상호작용 표현과 학습 데이터의 설계

GMF는 사용자와 아이템을 임베딩한 뒤 요소곱(element-wise product)으로 상호작용을 표현한다. 경량이며 지연이 낮은 편이다. MLP는 두 임베딩을 결합해 다층퍼셉트론으로 통과시키므로 비선형 관계를 학습하는 데 적합하다. NeuMF는 GMF와 MLP를 병렬로 결합해 표현력과 안정성의 균형을 맞추며, 대규모 서비스의 기본 베이스라인으로 사용할 수 있다.

학습 데이터는 관측된 상호작용을 Positive로 두고, 비상호작용 후보는 Negative Sampling으로 구성한다. 시간 가중치와 노출 보정(IPS/DR)은 편향을 완화하는 데 사용된다. 클릭 불균형에는 BCE/Softmax CE, BPR/Pairwise Loss, Focal Loss를 적용할 수 있고, Temperature와 Label Smoothing은 학습 안정화에 활용된다.

피처는 ID 임베딩만으로 끝나지 않는다. 텍스트, 이미지, 카테고리, 가격 같은 콘텐츠 피처를 결합한 하이브리드 구성이 필요하며, 온라인과 오프라인의 피처 일관성을 관리해야 한다. Feature Store는 피처 정의, 버전, TTL을 관리하고 실시간 피처 조회 지연을 최적화하는 기반이 된다.

후보생성부터 재정렬까지 이어지는 서빙

온라인 요청은 Retrieval, Filtering, Ranking, Re-ranking, Diversification 순으로 처리할 수 있다. 벡터 검색은 Cache와 Annoy/FAISS HNSW로 최적화하며, 모델 레지스트리·A/B 테스트·그레이 릴리스·롤백 절차를 표준화한다. 운영 중에는 지연, 오류율, Bias/Calibration을 관측 대상으로 둔다.

콜드 스타트 구간에서는 텍스트·이미지 encoder로 메타데이터 임베딩을 사전학습하고 Two-Tower를 이용해 아이템과 사용자를 즉시 서빙할 수 있다. Zero/One-shot 스코어링은 프로필 기반 규칙과 인기 대체를 먼저 적용한 뒤 개인화 부트스트랩으로 이어진다.

Error & FallbackDefault/ImputeContent EmbeddingFallbackCache/Popular BackuplogsRaw Events(views, clicks, purchases)ETL/StreamingDedup, Late-data HandlingFeature StoreOnline/Offline, TTL,VersioningTraining PipelineSampling, Labeling, Split bytimeModel TrainNCF/Two-Tower/SeqEval & Bias CheckAUC/NDCG, CalibrationModel RegistryVersion, CanaryOnline ServingRetrievalANN (FAISS/HNSW)FilteringPolicy, Stock, SafetyRanking (NeuMF + features)Re-rankingDiversity, Business RulesResponseTop-N ItemsMissing FeaturesNew User/ItemANN TimeoutOnline MetricsLatency, Error RateUser FeedbackImplicit SignalsTraining Data Lake

이 흐름에서 이벤트는 특성화와 학습을 거쳐 서빙 응답으로 연결된다. 피처가 없으면 기본값을 대입하고, 신규 사용자나 아이템은 콘텐츠 임베딩으로 대체하며, ANN 타임아웃에는 캐시나 인기 리스트를 사용한다. Feature Store의 온라인·오프라인 변환 일관성, 모델 버전 관리, 카나리와 롤백 절차가 함께 유지돼야 한다.

접근 성능(정밀/재현) 확장성(대상 수백만) 일관성(온·오프라인) 안정성(장애/지연) 운영 편의
MF(내적) 매우 높음 높음 매우 높음 매우 쉬움
NeuMF 높음 높음 중~높음 높음
Two-Tower(ANN) 후보생성에 매우 높음 매우 높음 높음
Sequence(Transformer) 최고 어려움

후보생성에는 Two-Tower를, 정밀 랭킹에는 NeuMF 또는 Sequence 조합을 권장한다.

신규 사용자·아이템·도메인의 초기 신호 만들기

신규 사용자에게는 진입 시점의 카테고리 선택과 소셜·언어·지역 자동 추출로 최소 신호를 수집한다. 인구통계, 디바이스, 유입경로로 Priors를 설정한 뒤 Two-Tower 유저 타워 출력의 초기 벡터를 만들 수 있다. 상위 인기와 다양성을 묶어 탐험 슬롯을 확보하고 Bandit로 빠르게 학습한다.

신규 아이템은 텍스트(BERT), 이미지(CLIP), 구조화 속성(Cat2Vec)을 융합한 메타 임베딩으로 아이템 타워를 초기화한다. 유사 아이템 근접 이웃의 평균 임베딩과 미세튜닝은 초기 표현을 안정화하는 방법이다. 낮은 가중치로 제한적으로 노출한 뒤 성능을 관측하고 가중치를 올리는 규칙도 적용할 수 있다.

시스템 자체가 새로 시작되는 경우에는 인접 카테고리나 해외 서비스의 사전학습 가중치를 이식하는 교차도메인 전이학습을 고려한다. 서핑 세션 생성 규칙을 이용한 합성 데이터·시뮬레이션은 초기 학습 안정화에 사용된다. Shadow→A/B 5%→25%→50%→100%의 단계적 롤아웃에 지연·오류·전환 안전지표 가드레일을 적용한다.

PyTorch로 구현한 암묵적 피드백 NeuMF

전제조건은 Python 3.10+, PyTorch 2.x, CUDA 선택이다. 데이터는 (user_id, item_id, label=1) 양성 상호작용을 사용하며, 네거티브 샘플링은 on-the-fly로 수행한다.

# pip install torch numpy scikit-learn
import torch, torch.nn as nn, torch.nn.functional as F
import numpy as np
from sklearn.model_selection import train_test_split

class NeuMF(nn.Module):
    def __init__(self, n_users, n_items, emb_gmf=32, emb_mlp=32, mlp_layers=[64,32,16]):
        super().__init__()
        self.user_gmf = nn.Embedding(n_users, emb_gmf)
        self.item_gmf = nn.Embedding(n_items, emb_gmf)
        self.user_mlp = nn.Embedding(n_users, emb_mlp)
        self.item_mlp = nn.Embedding(n_items, emb_mlp)
        mlp_input = emb_mlp * 2
        layers = []
        for h in mlp_layers:
            layers += [nn.Linear(mlp_input, h), nn.ReLU()]
            mlp_input = h
        self.mlp = nn.Sequential(*layers)
        self.out = nn.Linear(emb_gmf + mlp_layers[-1], 1)

        for m in self.modules():
            if isinstance(m, nn.Embedding):
                nn.init.normal_(m.weight, std=0.01)

    def forward(self, u, i):
        gmf = self.user_gmf(u) * self.item_gmf(i)
        mlp = self.mlp(torch.cat([self.user_mlp(u), self.item_mlp(i)], dim=-1))
        x = torch.cat([gmf, mlp], dim=-1)
        return torch.sigmoid(self.out(x)).squeeze(-1)

def make_dataset(n_users=1000, n_items=2000, pos_per_user=20, neg_ratio=4, seed=7):
    rng = np.random.default_rng(seed)
    pos_u, pos_i = [], []
    for u in range(n_users):
        items = rng.choice(n_items, size=pos_per_user, replace=False)
        pos_u.extend([u]*pos_per_user)
        pos_i.extend(items.tolist())
    pos = np.c_[pos_u, pos_i, np.ones(len(pos_u))]
    # simple negative sampling
    neg = []
    pos_set = set((int(u), int(i)) for u,i,_ in pos)
    while len(neg) < len(pos)*neg_ratio:
        u = rng.integers(0, n_users)
        i = rng.integers(0, n_items)
        if (u, i) not in pos_set:
            neg.append([u, i, 0])
    data = np.vstack([pos, np.array(neg)])
    rng.shuffle(data)
    return data.astype(np.int64), n_users, n_items

data, n_users, n_items = make_dataset()
train, test = train_test_split(data, test_size=0.1, random_state=42)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

model = NeuMF(n_users, n_items).to(device)
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
bce = nn.BCELoss()

def batch_iter(arr, bs=4096):
    for i in range(0, len(arr), bs):
        yield arr[i:i+bs]

for epoch in range(5):
    model.train()
    losses = []
    for batch in batch_iter(train):
        u = torch.tensor(batch[:,0], device=device)
        it = torch.tensor(batch[:,1], device=device)
        y = torch.tensor(batch[:,2], dtype=torch.float32, device=device)
        pred = model(u, it)
        loss = bce(pred, y)
        opt.zero_grad(); loss.backward(); opt.step()
        losses.append(loss.item())
    # simple AUC proxy
    model.eval()
    with torch.no_grad():
        tb = test[:20000]
        pu = torch.tensor(tb[:,0], device=device)
        pi = torch.tensor(tb[:,1], device=device)
        py = torch.tensor(tb[:,2], dtype=torch.float32, device=device)
        pr = model(pu, pi).detach().cpu().numpy()
        auc = ((pr[py.cpu().numpy()==1].mean()) - (pr[py.cpu().numpy()==0].mean()))
    print(f"epoch {epoch+1}, loss {np.mean(losses):.4f}, sep-proxy {auc:.4f}")

운영에서는 온라인·오프라인 피처의 일치를 검증하고, TFDV/Great Expectations로 스키마와 분포 드리프트를 감지한다. p50/p95 지연, 오류율, CTR/전환 지표가 임계값에 미달하면 모델 버전과 가드레일을 기준으로 자동 롤백한다. 아이템 업데이트 빈도에 맞춰 ANN 인덱스를 증분 업데이트하고, 장애에 대비해 더블버퍼링을 둔다.

전환 성과와 지연 목표를 함께 보는 기준

NeuMF/Two-Tower 도입 시 AUC/PR 310%p, NDCG@10 515% 향상을 기대할 수 있다. 비즈니스 성과는 CTR 520%, 전환율 28% 상승 가능성이 있으며 도메인·트래픽에 의존한다. 신규 아이템 전환은 10~30% 개선될 수 있다.

Two-Tower+ANN은 qps 10k+ 처리와 p95 < 50ms 달성이 가능하며, 캐시 적중 시 p95 < 10ms를 목표로 할 수 있다. 카나리와 자동 롤백을 결합하면 장애 전파를 최소화하고 스케일 아웃도 용이해진다.

NCF는 MF보다 비선형 상호작용을 표현해 정밀도를 높일 수 있고, Two-Tower와 결합하면 대규모 실시간 후보생성으로 확장된다. 도입은 Two-Tower 후보생성, NeuMF 랭킹, 콘텐츠 임베딩 기반 콜드 스타트, 시퀀스·다목표 확장 순으로 구성할 수 있다.

추천 시스템신경 협업 필터링콜드 스타트딥러닝피처 스토어