1. 관련 상품 추천 개발
상황
- 현재 고객이 선택한 상품을 주문하는 과정 중 추가적인 상품이 노출되지 않고 있음.
목표
- 고객이 선택한 상품과 함께 구매된 상품들을 자동으로 추천해줌으로써 추가 구매를 유도하여 평균 주문 금액을 증가시키는 것을 목표로 함.
액션
Itme2Vec 방법을 활용하여 함께 구매된 상품 추천
- 데이터 라벨링 ( 상품명을 통일 )
- Item2Vec 모델링 및 학습
1.1 데이터 전처리
데이터 재구성
- 문제
- Contents 컬럼이 딕셔너리 형태로 존재하여 분석에 비효율적
- Contents 컬럼은 상품 주문에 대한 세부적인 내용을 담고 있음
- 재구성 절차
- JSON 문자열을 딕셔너리로 변환
- 복수의 딕셔너리가 존재하는 경우 → 리스트로 변환 후 각 딕셔너리를 하나의 행으로 변환
- 딕셔너리의 각 요소를 열(Column)로 변환
- 주문번호를 기준으로 원본 데이터와 결합
# 전처리 코드
import json
contents_col = []
def ft_data(df , df_name) :
df_contents = df[['order_seq' , 'contents']]
df_contents['contents'] = df_contents['contents'].apply(json.loads) #json 문자열에서 딕셔너리 or 리스트 객체로 변환
df_contents['contents'] = df_contents['contents'].apply(lambda x: [x] if isinstance(x, dict) else x) # 딕셔너리 형태일 경우 리스트로 변환
df_contents = df_contents.explode('contents').reset_index(drop = True) #각 리스트의 요소를 한 행씩 나누어 새로운 행으로 만들기
ft_content = pd.json_normalize(df_contents['contents']) #딕셔너리를 각 열로 변환
print(f'{df_name} 호텔 contents 개수 : {len(ft_content.columns)}개')
print(f'{df_name} 호텔 contents: {ft_content.columns}')
contents_col.extend(ft_content.columns)
final_content = pd.concat([df_contents['order_seq'], ft_content ], axis=1)
merge_df = pd.merge(df , final_content , on = 'order_seq' , how = 'outer')
return merge_df
데이터 전처리
- NaN 처리
- NaN 값이 99% 이상인 열 삭제
- 불필요한 데이터 삭제
- 접근 불가한 데이터(Filepath) 및 의미 없는 열(데이터가 0으로만 구성된 열) 제거
- 시스템 오류 또는 테스트 데이터로 추정되는 중복 열 제거
- 데이터 라벨링 (상품명 통일)
- 같은 상품임에도 itemName(상품명)이 다양하게 표현됨.
- hotel과 item_seq를 그룹화 진행 후 itemName을 리스트로 변환
- 만일 리스트내 모든 상품이 동일한 상품이라면 상품명 통일
- 만일 리스트내 동일상품이 아니라면 개별적으로 라벨링 진행
- ex. "['Grilled Mawang (Large)', '[인룸다이닝]마왕통구이(대) / 돼지고기 : 외국산', '[인룸다이닝]마왕통구이(대)', '마왕통구이(대)']” → 마왕통구이로 통일
2.2 Item2Vec 모델링
- gensim 라이브러리의 Word2Vec 활용
- Skip-Gram, negative sampling 방식 적용
- 하이퍼파라미터 최적화
- 최적화 기법 : RandomSearch 활용
- 각 호텔별로 Item2Vec 모델 최적화
- 조정 파라미터 : Vector_size, min_count, epochs, negative 조절
- 구매 Interaction이 없는 경우
- 신규상품 New item의 경우에는 구매내역이 없기에 추천불가 가능성 존재
- 해결 : 상품 카테고리 기반 추천으로 보완
def word2vec(sentences, vector_size = 90 , window = 20 , min_count =15 ,
sg =1 , epochs = 20 , hs =0 , negative =15) :
model = Word2Vec(
sentences=sentences, # 학습 데이터
vector_size=vector_size,
window=window, # 모든문맥고려
min_count=min_count,
sg=sg, # Skip-Gram 사용
epochs=epochs,
hs=hs,
negative=negative
)
return model
2.3 모델 평가 → HitRate@K
- Train-Test Split ( Leave-One-Out Cross Validation)
- 유저가 구매했던 item리스트에서 하나만 의도적으로 제거
- 나머지 item을 가지고 추천 모델 학습 및 Top-k 추천 상품 추출
- K개의 추천 리스트 가운데 이전에 제거했던 item이 있다면 Hit
def leave_one_out_split(df):
train_seq = []
test_seq = []
for _, row in df.iterrows():
items = row['item_idx']
if len(items) <= 2 :
continue
test_item_idx = random.randint(0, len(items) - 1) #무작위로 item 선택
test_item = items[test_item_idx]
train_items = items[:test_item_idx] + items[test_item_idx+1:]
test_seq.append(train_items + [test_item])
train_seq.append(train_items)
return train_seq, test_seq
# Hit Rate 계산 함수
def hit_Rate(model, test_seq, k):
hits = 0
total_users = len(test_seq)
for test_items in test_seq:
# 테스트 데이터에서 마지막 아이템은 테스트 아이템으로 사용
test_item = test_items[-1]
train_items = test_items[:-1] # 나머지 아이템은 학습된 사용자 기록
# Word2Vec 모델에 존재하는 아이템만 필터링
filtered_train_items = [item for item in train_items if item in model.wv]
if filtered_train_items:
recommendations = [item for item, _ in model.wv.most_similar(positive=filtered_train_items, topn=k)]
# 테스트 아이템이 추천 리스트에 포함되어 있는지 확인 (Hit 여부)
if test_item in recommendations:
hits += 1
# Hit Rate 계산
hit_rate = hits / total_users if total_users > 0 else 0
return hit_rate
2. 고객 맞춤형 프로모션 추천
상황
- 호텔에서 다양한 이벤트와 프로모션이 진행됨
- 이벤트 정보 노출이 제한적이라 고객이 진행 중인 프로모션을 충분히 인지 못할 수 있음을 고려
목표
- Embedding + Retriever 활용해고객과 프로모션 특성을 매칭하여 개인화된 프로모션 추천을 통해 고객 만족도 및 참여율 증가
액션
- 데이터 수집
- 호텔 웹사이트를 통해 프로모션 데이터 크롤링
- Query 기반 검색 및 추천 엔진 설계
- Query와 적합한 키워드 및 의미 기반 검색 시스템 구축
- 여기서 Query는 사용자가 직접 입력할 수 있도록 설계
- ex. 반려동물과 함께하기 좋은 프로모션 추천해줘.
2.1 데이터 수집
- 각 호텔의 프로모션 및 이벤트 데이터 수집
- csv 형태로 저장 (Title , Description , Period , Location)
2.2 Query 기반 검색 엔진 설계
BM25, FAISS Retriever로 한글 검색 성능 실험
FAISS (의미적 유사성 기반) → 임베딩 모델 : intfloat/multilingual-e5-large-intstruct사용
- Contextualized embedding model 사용
- 전통적인 임베딩 방식에는 단어에 고정된 벡터를 할당하지만 컨텍스트 기반 임베딩 모델은 단어가 사용되는 문맥에 따라 벡터를 달리 할당 ex) BERT, GPT
BM25 (키워드 기반) → 형태소 분석기 : Kiwi 활용
- 실제 BM25 기본 토크나이저는 영어 기준이기 때문에 한국어는 적용이 잘 안되는 것으로 추측
- 한국어를 형태소로 잘 분리해 단어를 원형으로 만들기 위해 형태소 분석기 활용
앙상블 : FAISS + BM25 → 7:3 , 3:7로 조정
결과
- 한국어 토큰화를 적용해도 BM25기반 검색기는 검색 결과에서 관련 문서를 가져오지 못함.
- FAISS 기반 검색기 활용하기로 결정
- search_type = top_k , k = 4
2.3 최종코드
- 임베딩과 인덱스 생성
- intfloat/multilingual-e5-large-intstruct 모델을 활용해 프로모션 데이터로 임베딩 벡터 생성
- 임베딩 차원(dimension)을 추출해 IndexFlatIP 객체를 생성하고, IndexIDMap을 사용해 고유 ID를 부여할 수 있는 인덱스를 구성
- 데이터프레임 인덱스를 ID로 사용해 add_with_ids() 메서드로 임베딩과 ID를 인덱스에 추가
- FAISS 인덱스 저장 및 로드
- 생성된 인덱스를 파일로 저장 및 로드 진행
- 특정 조건으로 필터링
- 현재 날짜를 기준으로 운영중인지 확인
- 검색 수행 및 결과 반환
- embed_query() 함수를 사용해 쿼리 텍스트를 임베딩 벡터로 변환
- index.search() 메서드를 사용해 상위 k개의 유사한 상위 항목 검색
- 검색된 항목들의 인덱스(indices) 에 해당하는 데이터 반환
FAISS 인덱스를 기반으로 사용자가 입력한 쿼리 텍스트와 유사한 프로모션 정보를 효율적으로 검색 및 정보 반환
import faiss
from langchain_huggingface import HuggingFaceEmbeddings
import os
from langchain_community.vectorstores import FAISS
#데이터 불러오기
df = pd.read_csv('./data/saint_promotion.csv' , index_col = [0])
df['hotel'] = 'saint'
df['text'] = df['Title'] + ' ' + df['Description']
df[['Start', 'End']] = df['Period'].str.split(' ~ ', expand=True) # Period 열을 시작일(Start)과 종료일(End)로 분리
df['Start'] = pd.to_datetime(df['Start']) # 날짜 형식으로 변환
df['End'] = pd.to_datetime(df['End'])
# Hugging Face의 사전 학습된 임베딩 모델과 토크나이저 로드
model_name = "intfloat/multilingual-e5-large-instruct"
hf_embeddings = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs={"device": "cpu"}, # cuda, cpu
encode_kwargs={"normalize_embeddings": True})
texts = df.text.to_list()
embedded_documents = hf_embeddings.embed_documents(texts)
dimension = len(embedded_documents[0])
index = faiss.IndexIDMap(faiss.IndexFlatIP(dimension))
#ID는 데이터프레임의 인덱스를 사용
ids = np.array(range(len(embedded_documents)))
# 임베딩과 ID 추가
index.add_with_ids(np.array(embedded_documents).astype('float32'), ids)
# FAISS 인덱스 저장 및 로드
faiss.write_index(index, "faiss_index.index")
index = faiss.read_index("faiss_index.index")
# 쿼리 텍스트 임베딩(예시)
query = "반려동물과 함께하기 좋은 프로모션"
embedded_query = hf_embeddings.embed_query(query)
# FAISS에서 상위 k개의 결과 검색
k = 4 # 상위 4개
distances, indices = index.search(np.array(embedded_query).astype('float32').reshape(1, -1), k)
# 검색된 인덱스와 거리 출력
print("Indices:", indices)
print("Distances:", distances)
# 날짜 필터링
current_date = pd.Timestamp.now()
# 현재 날짜가 Start와 End 사이에 포함된 행 필터링
df = df[(df['Start'] <= current_date) & (df['End'] >= current_date)]
# 검색된 인덱스에 해당하는 데이터프레임 필터링
result_df = df.iloc[indices[0]].reset_index(drop=True)
filtered_df = result_df[['Title','Description','Period','Location']]
filtered_df
'사이드 프로젝트' 카테고리의 다른 글
[파이널 프로젝트] 호텔 상품 추천 시스템 개발 [1] (0) | 2025.01.27 |
---|---|
[주식챗봇] - 페르소나 부여 [3] (1) | 2025.01.12 |
[주식챗봇] 프로젝트 개요 및 목표 [1] (0) | 2025.01.08 |
[미니 프로젝트] OpenWeather API를 활용한 날씨 DashBoard 개발 (0) | 2024.04.26 |