본문 바로가기
카테고리 없음

비지도학습에서 시작한 추천 시스템: GMM에서 MLP까지

by 혜룐 2025. 5. 24.
반응형

추천 시스템을 구축하는 방법에는 크게 두 가지 접근이 있다. 하나는 비지도학습 기반의 클러스터링 방식이고, 다른 하나는 실제 사용자 행동 데이터를 활용한 지도학습 기반 모델이다.

GMM(Gaussian Mixture Model)은 대표적인 비지도 클러스터링 기법으로, 사용자 벡터들을 군집화하여 각 클러스터의 대표적인 비즈니스친구을 추천하는 데 사용할 수 있다. 이 방식은 라벨(label) 정보 없이도 작동하므로, 추천 시스템 초기 구축 단계나 사용자 데이터가 부족한 상황에서 유용하게 활용된다. 또한, cold start 문제에 강하다는 장점이 있다.

그러나 GMM만으로는 사용자 개개인의 세밀한 취향을 반영하기 어렵고, 추천 결과에 대한 정량적 평가(AUC 등)를 수행하기 어렵다는 한계가 있다. 따라서 데이터가 충분히 확보되기 시작하면, 지도학습 기반의 모델로 전환하는 것이 추천 품질을 높이는 데 효과적이다.

GMM, 비지도학습(클러스터링 그리고 Kmeans)를 훑어보고 추천시스템을 만든다면 지도학습 기반 MLP모델로 확장해서 어떻게 설계해볼것인가에 대한 내용을 정리해본다.


비지도학습에서 잠재변수의 역할 - 기본 아이디어

  • 관찰된 데이터 x: 우리가 실제로 볼 수 있는 데이터
  • 잠재변수 z: 숨겨져 있지만 데이터를 설명하는 핵심 요인
  • 목표: x를 통해 z를 추론하고, z를 통해 x의 구조를 이해

관찰변수 vs 잠재변수

1. 관찰변수 (Observed Variables)

  • 실제로 측정/수집할 수 있는 데이터
  • 예시: 키, 몸무게, 시험점수, 픽셀값, 구매이력 등
  • 코드에서는 보통 X 또는 data

2. 잠재변수 (Latent Variables)

  • 직접 관찰할 수 없지만 숨어있는 변수 : 모델이 "아마 이럴 것이다"라고 추측하는 것
  • 예시: 지능, 성격, 고객타입, 이미지의 "의미" 등
  • 코드에서는 보통 Z 또는 latent

K-means 클러스터링에서의 잠재변수

K-means 클러스터링에서 잠재변수(latent variable)는 각 데이터 포인트가 어느 클러스터에 속하는지를 나타내는 클러스터 할당(cluster assignment) 변수이다.

상황: 온라인 쇼핑몰 고객을 3개 그룹으로 분류

고객 데이터 (연간 구매금액, 방문횟수)

K-means 과정 (k=3)

  1. 1단계: 초기 클러스터 중심점 (랜덤 설정)
  2. 2단계: 각 고객을 가장 가까운 클러스터에 배정
  3. 3단계: 잠재변수 결과

K-means 과정과 잠재변수 사용 시점

전체 과정

  1. 초기: 클러스터 중심점을 임의로 설정
  2. 반복: E-step과 M-step을 번갈아 수행
  3. 수렴: 더 이상 변화가 없을 때까지

잠재변수가 사용되는 구체적 시점

E-step에서 잠재변수 업데이트

현재 클러스터 중심점들이 주어졌을 때
→ 각 고객을 가장 가까운 클러스터에 재배정
→ 이때 잠재변수 r_nk 값들이 결정됨

M-step에서 잠재변수 사용

업데이트된 잠재변수를 사용해서
→ 새로운 클러스터 중심점 계산
  • 잠재변수 = 클러스터 배정표
  • E-step: 잠재변수를 업데이트하는 단계
  • M-step: 잠재변수를 사용하여 중심점을 다시 계산하는 단계

이 과정을 수렴할 때까지 반복한다.

K-means의 핵심 개념 정리

1. 프로토타입 (Prototype)

프로토타입 = 클러스터의 대표값 = 중심점 (Centroid)

역할:

  • 각 클러스터를 대표하는 "기준점"
  • 클러스터 내 모든 데이터의 평균 위치
  • 새로운 데이터가 어느 클러스터에 속할지 판단하는 기준

2.거리기반

# 각 데이터 포인트에 대해
for 데이터_포인트 in 모든_데이터:
    거리들 = []
    for 중심점 in 모든_중심점:
        거리 = 유클리드_거리(데이터_포인트, 중심점)
        거리들.append(거리)
    
    가장_가까운_중심점 = min(거리들)
    할당_클러스터 = 가장_가까운_중심점의_클러스터

GMM의 잠재변수는 "확률"이다.

  • K-means: 고객이 확실히 어느 그룹에 속함 (0 또는 1)
  • GMM: 고객이 각 그룹에 속할 확률 (0~1 사이의 실수)
  • GMM에서 잠재변수는 각 데이터 포인트가 어느 가우시안 컴포넌트에서 생성되었는지를 나타내는 변수이다.

EM 알고리즘의 특징

  1. E-step: 현재 모델로 각 점의 소속 확률 계산 (잠재변수 추정)
  2. M-step: 계산된 확률로 모델 파라미터 업데이트
  3. 반복: 수렴할 때까지 E-step과 M-step 반복

GMM이 더 유연한 이유는 바로 이 확률적 할당 때문이다. 경계가 애매한 데이터도 여러 클러스터에 부분적으로 속할 수 있게 해준다!

K-means vs GMM 잠재변수의 차이

K-means (Hard Assignment)

고객 1: [1, 0, 0] → 확실히 클러스터 1에 속함
고객 2: [0, 1, 0] → 확실히 클러스터 2에 속함

GMM (Soft Assignment)

고객 1: [0.7, 0.2, 0.1] → 70% 확률로 컴포넌트 1, 20% 확률로 컴포넌트 2...
고객 2: [0.1, 0.8, 0.1] → 10% 확률로 컴포넌트 1, 80% 확률로 컴포넌트 2...

확률값을 그대로 활용하는 방법들

GMM의 진짜 장점은 애매한 상황을 애매하게 처리 할 수 있다는 것이다. K-means: "이 고객은 VIP다!" (강제 선택) GMM: "이 고객은 70% VIP 성향이다" (확률적 접근) 실제 세상은 경계가 명확하지 않은 경우가 많기 때문에, GMM의 확률적 접근이 더 현실적이고 유용하다.

1. 마케팅 전략 예시

고객 A: [VIP: 0.7, 일반: 0.2, 신규: 0.1]

→ VIP 혜택을 70% 강도로 제공
→ 일반 고객 프로모션을 20% 강도로 제공
→ 신규 고객 웰컴 패키지를 10% 강도로 제공

2. 추천 시스템

사용자 B: [액션영화팬: 0.6, 로맨스팬: 0.4]

→ 액션영화 60%, 로맨스영화 40% 비율로 추천

3. 의료 진단

환자 C: [질병A: 0.8, 질병B: 0.15, 정상: 0.05]

→ 질병A 치료를 우선하되, 질병B도 모니터링
→ 확실한 진단이 아니라 확률적 판단 제공

사용자 행동 데이터(대화 내용, 클릭 로그, 이용 시간 등)를 바탕으로 "어떤 비즈니스친구에 관심 있을지" 예측하는 문제를 풀어본다면?

GMM (Gaussian Mixture Model)

  • 사용자 행동 패턴을 클러스터링해서 유형(예: 구매 지향, 정보 탐색, 이벤트 선호 등)으로 나눔.
  • 각 유형마다 선호할 만한 비즈니스친구 추천.
  • 예: 사용자 A는 3번 클러스터(정보형)에 속하므로, ‘날씨알림’, ‘뉴스브리핑’ 비즈니스친구 추천.

MLP (다층 퍼셉트론)

  • 입력: 사용자 임베딩 + 이전 행동 + 대화 내용 임베딩
  • 출력: 비즈니스친구별 클릭 확률 → 확률 높은 비즈니스친구 추천

Sequence 모델 (LSTM/GRU)

  • 사용자의 대화 흐름 시퀀스를 기반으로 “다음 행동” 예측
  • 비즈니스친구 탐색 패턴을 기반으로 추천 가능

근데 모델없이도 사용자-비즈니스친구(비친 또는 채널) 임베딩 유사도 기반 추천한다고 하면 아래와 같은 플로우로 할수있겠다.

 

  • 사용자 = 최근 맺은 비친들의 임베딩 평균값
  • 비친 = 이름 + 카테고리 + 소식 메시지 + 영업 정보 텍스트를 하나로 합쳐서 임베딩
  • 추천 = 사용자 임베딩과 전체 비친 임베딩 간 코사인 유사도 기반 Top-K 선택

 

┌──────────────────────────────┐
│       1. 데이터 수집 및 전처리       │
└──────────────────────────────┘
          │
          ▼
[채널 정보]
- 이름
- 카테고리
- 소식 메시지
- 영업정보
        +
[사용자 정보]
- 최근 맺은 채널 목록 (ID로 조회)

          │
          ▼
┌──────────────────────────────┐
│     2. 채널/사용자 임베딩 생성      │
└──────────────────────────────┘
          │
채널 텍스트 합치기 → 임베딩 API 호출 (KoBERT 등)
사용자 = 최근 채널 벡터 평균값

          │
          ▼
┌──────────────────────────────┐
│  3. 후보 채널과 유사도 계산 (ANN)   │
└──────────────────────────────┘
          │
FAISS or 코사인 유사도 기반 Top-K 추출
(전체 채널 vs 사용자 벡터)

          │
          ▼
┌──────────────────────────────┐
│     4. 추천 결과 후처리 및 출력     │
└──────────────────────────────┘
          │
채널 정보 매핑 → 추천 결과 템플릿 렌더링
+ 추천사유 (선택, 향후 LLM)

          │
          ▼
┌──────────────────────────────┐
│         5. 피드백 로깅           │
└──────────────────────────────┘
(선택) 클릭, 이탈, 맺기 정보 로깅
→ 추후 MLP 또는 강화학습 기반 추천으로 확장 가능

그래도 클릭/이탈 로그가 조금씩 쌓이면 모델로 정확도를 더 높일 수있으니까

MLP(Multi-Layer Perceptron, 다층 퍼셉트론) 기반의 이진 분류 모델을 학습해보자. 이 모델은 "특정 사용자가 어떤 비즈니스친구 맺을 확률이 얼마나 높은지"를 예측하는 목적을 가진다.

 

  • 샘플로 만들어본 모델은 MLP 기반의 이진 분류 모델을 학습한다.
  • 입력은 사용자 벡터 + 비친 벡터이며, 출력은 ‘이 비친을 맺을 확률’이다.
    • 사용자벡터
      • 어떤 피쳐를 기반을 뽑을까?
        • 최근에 맺은 비친목록N개 -> 그 비친들의 카테고리, 소식메시지, 메뉴 설명
        • 사용자 벡터는 최근 맺은 비친들의 콘텐츠 임베딩 평균값으로 한다.
    • 비친벡터
      • 어떤 피쳐를 기반을 뽑을까?
        • 비친 이름
        • 카테고리
        • 홈 소식 메시지 (ex: “오늘 할인 이벤트 있어요!”)
        • 메뉴 정보 (ex: “케이크 예약, 포장 가능”)
        • 친구 수 (정량 값)
    • 학습셋예시
user_vector,channel_vector,label
"[0.12, 0.31, 0.11, ...]", "[0.09, 0.28, 0.05, ...]", 1
"[0.12, 0.31, 0.11, ...]", "[0.88, 0.34, 0.56, ...]", 0

user_vector: 사용자 벡터 → 최근에 맺은 채널들의 임베딩 평균
channel_vector: 채널 벡터 → 채널 이름, 카테고리, 소식 메시지, 메뉴정보 등을 하나의 텍스트로 만든 뒤 임베딩
label
1: 실제로 맺은 관계
0: 랜덤 또는 추천 후보였지만 맺지 않은 채널
- 친구관계가 아니라는 라벨링0은 네거티브샘플링으로 간주하자. 사용자가 맺지 않은 채널 중 일부를 의도적으로 랜덤하게 골라서 부정 샘플로 간주
  • 손실 함수는 BCEWithLogitsLoss, 최적화는 Adam을 사용한다.
  • 성능 평가는 AUC로 이루어지며, 학습이 끝나면 모델이 저장한다.
  • 이후 recommend_channels() 함수를 통해 새로운 사용자에게 비친을 추천한다.

 

# channel_recommendation_training.py

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import pandas as pd
import numpy as np
from sklearn.metrics import roc_auc_score

# -----------------------------
# 1. 데이터셋 정의
# -----------------------------
class ChannelRecDataset(Dataset):
    def __init__(self, df):
        self.user_vecs = np.vstack(df['user_vector'].values)
        self.channel_vecs = np.vstack(df['channel_vector'].values)
        self.labels = df['label'].values.astype(np.float32)

    def __len__(self):
        return len(self.labels)

    def __getitem__(self, idx):
        return (
            torch.tensor(self.user_vecs[idx], dtype=torch.float32),
            torch.tensor(self.channel_vecs[idx], dtype=torch.float32),
            torch.tensor(self.labels[idx], dtype=torch.float32),
        )

# -----------------------------
# 2. MLP 모델 정의
# -----------------------------
class ChannelRecommender(nn.Module):
    def __init__(self, input_dim):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(input_dim, 256),
            nn.ReLU(),
            nn.Linear(256, 64),
            nn.ReLU(),
            nn.Linear(64, 1),
        )

    def forward(self, user_vec, channel_vec):
        x = torch.cat([user_vec, channel_vec], dim=-1)
        return self.fc(x).squeeze(-1)

# -----------------------------
# 3. 학습 함수
# -----------------------------
def train_model(model, train_loader, val_loader, num_epochs=5, lr=1e-3):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = model.to(device)
    criterion = nn.BCEWithLogitsLoss()
    optimizer = optim.Adam(model.parameters(), lr=lr)

    for epoch in range(num_epochs):
        model.train()
        total_loss = 0
        for user_vec, channel_vec, label in train_loader:
            user_vec, channel_vec, label = user_vec.to(device), channel_vec.to(device), label.to(device)
            optimizer.zero_grad()
            logits = model(user_vec, channel_vec)
            loss = criterion(logits, label)
            loss.backward()
            optimizer.step()
            total_loss += loss.item()

        model.eval()
        preds, trues = [], []
        with torch.no_grad():
            for user_vec, channel_vec, label in val_loader:
                user_vec, channel_vec = user_vec.to(device), channel_vec.to(device)
                logit = model(user_vec, channel_vec)
                prob = torch.sigmoid(logit).cpu().numpy()
                preds.extend(prob)
                trues.extend(label.numpy())

        auc = roc_auc_score(trues, preds)
        print(f"Epoch {epoch+1}/{num_epochs}, Loss: {total_loss:.4f}, Val AUC: {auc:.4f}")

    return model

# -----------------------------
# 4. 데이터 준비 예시
# -----------------------------
# CSV 파일에 아래와 같은 형태로 저장되어 있어야 합니다:
# user_vector: list 형태 문자열
# channel_vector: list 형태 문자열
# label: 0 또는 1

def load_data(csv_path):
    df = pd.read_csv(csv_path)
    df['user_vector'] = df['user_vector'].apply(eval)
    df['channel_vector'] = df['channel_vector'].apply(eval)
    return df

# -----------------------------
# 5. 추론 함수: 새 유저에게 채널 추천
# -----------------------------
def recommend_channels(model, user_vector, candidate_channels, top_k=5):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model.eval()
    model.to(device)

    user_tensor = torch.tensor(user_vector, dtype=torch.float32).unsqueeze(0).to(device)
    candidates = torch.tensor(np.vstack(candidate_channels), dtype=torch.float32).to(device)
    user_tensor = user_tensor.repeat(candidates.size(0), 1)

    with torch.no_grad():
        logits = model(user_tensor, candidates)
        probs = torch.sigmoid(logits).cpu().numpy()

    top_indices = np.argsort(probs)[::-1][:top_k]
    return top_indices, probs[top_indices]

if __name__ == "__main__":
    df = load_data("train_data.csv")
    train_df = df.sample(frac=0.8, random_state=42)
    val_df = df.drop(train_df.index)

    train_loader = DataLoader(ChannelRecDataset(train_df), batch_size=64, shuffle=True)
    val_loader = DataLoader(ChannelRecDataset(val_df), batch_size=64)

    input_dim = len(train_df['user_vector'].iloc[0]) + len(train_df['channel_vector'].iloc[0])
    model = ChannelRecommender(input_dim)

    train_model(model, train_loader, val_loader)
    torch.save(model.state_dict(), "channel_recommender.pt")

    # 추론 예시
    # 예: 최근 맺은 채널 평균으로 만든 user_vec, 전체 채널 벡터 리스트로 추천
    # user_vec = np.mean([embed(ch) for ch in recent_channels], axis=0)
    # candidate_vecs = [embed(ch) for ch in all_channels]
    # topk_indices, scores = recommend_channels(model, user_vec, candidate_vecs)

위에 구성된 시스템은 MLP 기반의 이진 분류 모델로, 사용자와 비즈니스친구 벡터를 입력으로 받아 해당 비즈니스친구을 맺을 확률을 예측한다. 이 방식은 개인화된 추천이 가능하며, 학습 데이터를 기반으로 지속적인 성능 개선이 가능하다.

GMM은 이러한 지도학습 시스템에서도 보조적인 역할로 활용할 수 있다. 예를 들어 GMM으로 사용자를 클러스터링한 결과를 one-hot 벡터로 만들어 MLP 입력 피처에 추가하면, 모델이 사용자 군집 정보도 함께 학습할 수 있다. 이렇게 하면 비지도 정보와 지도 정보가 융합된 하이브리드 추천 시스템을 구축할 수 있다.

결론적으로, GMM은 초기 추천 시스템을 빠르게 구성할 수 있는 수단이자, 이후 고도화된 모델의 feature로도 활용될 수 있는 유용한 도구이다.

반응형