imhamburger 님의 블로그
데이터엔지니어 부트캠프 - 유사 공연 추천시스템 (23주차) 본문
우리 프로젝트의 경우, 각기 다른 티켓예매사이트들의 티켓정보들을 한 사이트로 모아 여기저기 티켓을 찾아볼 필요없이 한 사이트에서 찾아볼 수 있는 서비스를 기획했다.
원래는 추천시스템을 만들 생각은 없었지만, 추천시스템이 없으면 단순 데이터 검색? 기능뿐이라 추천시스템을 추가하게 되었다.
추천시스템은 DB에 적재되어 있는 공연의 상세설명(공연설명)을 읽어와 모든 공연 설명들에 대해 코사인 유사도로 계산하여 각 공연마다 유사도가 가장 높은 TOP3 까지의 공연만 보여주는 것이다.
그리고 나는 조건을 몇 개 더 추가하여 필터링하였다.
- 기준 공연 시작일 이후이면서 기준공연 시작일 + 90일까지로 날짜 제한 설정
- 기준 공연과 같은 지역으로 설정
추천시스템 코드 흐름
- 공연설명을 형태소 분석기로 명사만 추출 (전처리)
- Word2Vec 모델을 사용하여 단어를 벡터로 변환
- 각 공연 설명에 대한 평균 벡터 계산
- 코사인 유사도를 기반으로 공연 간 유사도 측정
- 유사도가 높은 공연을 필터링 및 추천
1. 전처리
def preprocess(text):
if text is None:
return [] # None은 빈 리스트로 처리
text = text.lower() # 소문자화
text = ''.join([char for char in text if char not in string.punctuation]) # 구두점 제거
tokens = okt.nouns(text) # 형태소 분석 후 명사 추출
return tokens
텍스트를 전처리하여 분석 가능한 형태로 변환하고 영어의 경우 소문자화, 구두점 제거 후 명사만 추출한다.
입력: "안녕하세요! 공연 추천 시스템입니다."
출력: ['안녕', '공연', '추천', '시스템']
2. Word2Vec 모델 학습
def train_word2vec_model(descriptions):
processed_descriptions = [preprocess(description) for description in descriptions]
model = Word2Vec(processed_descriptions, vector_size=100, window=5, min_count=1, workers=4)
return model
단어를 벡터로 변환하여 단어 간 유사성을 측정 가능하게 한다.
vector_size=100: 각 단어를 100차원 벡터로 변환. (차원이 높을수록 단어 간 관계를 더 정교하게 표현할 수 있지만, 계산 비용도 증가)
window=5: 컨텍스트 창 크기. (여기서 컨텍스트의 의미는 특정 단어 주변에 있는 단어들)
min_count=1: 최소 1회 등장한 단어만 포함. (한 번이라도 등장한 단어를 학습에 포함. 수를 증가하면 불필요한 잡음제거)
workers=4: CPU 코어 사용.
3. 단어 벡터 추출
def get_word_vector(model, word):
try:
return model.wv[word]
except KeyError:
print(f"단어 '{word}'는 모델에 존재하지 않습니다.")
return None
특정 단어에 대한 벡터를 반환한다.
4. 단어 평균 벡터 계산
def get_average_vector(model, tokens):
vectors = []
for word in tokens:
if word in model.wv:
vectors.append(model.wv[word])
if len(vectors) == 0:
return np.zeros(model.vector_size)
return np.mean(vectors, axis=0)
주어진 단어 리스트의 평균 벡터를 계산한다. 모델에 없는 단어는 제외한다.
벡터는 하나의 숫자 리스트라고 생각하면 되는데, 여기서 각 숫자는 단어의 특정 의미를 나타낸다.
따라서 벡터 계산을 함으로써 단어 간의 관계(유사성, 차이점)을 계산할 수 있다.
예: "공연"과 "연극"의 벡터 비교
- "공연"의 벡터: [0.1, -0.2, 0.4]
- "연극"의 벡터: [0.2, -0.1, 0.5]
이 두 단어는 공연과 연극이라는 유사한 의미를 가지므로 벡터 값도 비슷하다.
평균 벡터란?
평균 벡터는 여러 단어의 벡터를 하나로 요약한 값이다.즉, 단어들의 공통된 의미를 추출한다.
예: 문장 "공연 추천 시스템"
각 단어의 벡터
"공연": [0.1, -0.2, 0.4]
"추천": [0.3, -0.1, 0.2]
"시스템": [0.2, 0.0, 0.1]
평균 벡터 계산
각 차원의 평균 계산:
1차원: (0.1 + 0.3 + 0.2) / 3 = 0.2
2차원: (-0.2 + -0.1 + 0.0) / 3 = -0.1
3차원: (0.4 + 0.2 + 0.1) / 3 = 0.233
결과: [0.2, -0.1, 0.233]
5. 코사인 유사도 계산
def calculate_cosine_similarity(model, descriptions):
processed_descriptions = [preprocess(description) for description in descriptions]
description_vectors = [get_average_vector(model, description) for description in processed_descriptions]
cosine_sim = cosine_similarity(description_vectors)
return cosine_sim
평균 벡터를 계산하면, 문장 간의 유사도를 코사인 유사도(Cosine Similarity)로 측정할 수 있다.
- 1: 완전히 같은 방향 (매우 유사)
- 0: 서로 관계 없음
- -1: 완전히 반대 방향
6. 유사 공연 추천
def get_top_similar_performances(cosine_sim, performances, top_n=3, threshold=0.98, days_limit=90):
similar_performances = []
for idx, performance in enumerate(performances):
if idx >= len(cosine_sim):
print(f"Index {idx} is out of bounds for cosine_sim with size {len(cosine_sim)}")
continue
performance_similarities = cosine_sim[idx]
performance_start_date = datetime.strptime(performance['start_date'], '%Y.%m.%d')
performance_region = performance.get('region', None)
date_limit = performance_start_date + timedelta(days=days_limit)
similar_performances_idx = np.argsort(performance_similarities)[::-1]
top_similar = []
for similar_idx in similar_performances_idx:
if similar_idx != idx and performance_similarities[similar_idx] < threshold:
similar_performance = performances[similar_idx]
similar_performance_start_date = datetime.strptime(similar_performance['start_date'], '%Y.%m.%d')
similar_performance_region = similar_performance.get('region', None)
if performance_region != similar_performance_region:
continue
if performance_start_date <= similar_performance_start_date <= date_limit:
top_similar.append(similar_performance)
if len(top_similar) >= top_n:
break
similar_performances.append(top_similar)
return similar_performances
유사도가 높은 순서대로 정렬하여 동일 지역 + 90일 이내의 공연 + 자기 자신 제외하여 3개까지만 유사 공연을 추천한다.
결과적으로,
- 사용자가 공연을 선택하면, 해당 공연과 유사한 공연을 추천.
- 지역, 날짜, 유사도 기준을 충족하는 공연만 보여줌.
- 특정 공연의 추천 공연 목록을 화면에 출력
유사 공연 추천 리스트는 mongoDB에 적재하였다. 그리고 API를 통해 사용자에게 출력된다.
mongoDB에 tickets 라는 컬렉션에 이미 공연들은 저장되어 있고, 추천시스템을 위한 컬렉션을 별도로 생성하여 적재하였다.
이때, 레퍼런스 방식으로 저장하였다.
레퍼런스 방식은 연결된 데이터를 직접 저장하지 않고, 그 데이터의 "ID"나 "참조값"만 저장하는 방법이다.
예를 들어, 우리가 책과 저자 데이터를 저장한다고 가정해보자.
1. 책 데이터에는 책의 제목과 저자 ID만 저장하고,
2. 저자 데이터에는 저자의 이름과 ID를 저장한다.
이제 책의 정보를 볼 때, 책 데이터에 있는 "저자 ID"를 이용해 저자 데이터에서 이름을 찾아서 가져오는 식이다.
즉, 필요한 정보를 나중에 연결해서 사용하는 것.
레퍼런스방식은 데이터 중복을 줄이고 관리하기 쉽게 만든다.
나의 에러 리포트
에러 1) IndexError
Traceback (most recent call last):
File "/Users/seon-u/TU-tech/ml/src/ml/main.py", line 43, in <module>
similar_performances = get_top_similar_performances(cosine_sim, performances)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/seon-u/TU-tech/ml/src/ml/utils.py", line 85, in get_top_similar_performances
performance_similarities = cosine_sim[idx] # 해당 공연과 다른 공연들의 유사도
~~~~~~~~~~^^^^^
IndexError: index 5534 is out of bounds for axis 0 with size 5534
발생이유
에러의 원인은 cosine_sim[idx]에서 idx 값이 cosine_sim 배열의 크기를 초과하기 때문이다.
cosine_sim의 크기가 (5534, 5534)인데, idx 값이 5534이므로 인덱스 범위를 벗어났다. 배열 인덱스는 0부터 시작하기 때문에 유효한 인덱스 범위는 0 ~ 5533이다.
해결방법
IndexError 문제를 해결하기 위해 idx와 관련된 조건 검사를 추가하고 cosine_sim 배열과 performances 데이터의 크기가 다를 경우에도 에러를 방지하고, 올바르게 동작하도록 수정하였다. IndexError가 발생하지 않고, 유효한 데이터만 처리.
수정 전)
def get_top_similar_performances(cosine_sim, performances, top_n=3):
similar_performances = []
# 각 공연에 대해 유사한 공연들을 찾기
for idx, performance in enumerate(performances):
performance_similarities = cosine_sim[idx] # 해당 공연과 다른 공연들의 유사도
performance_start_date = datetime.strptime(performance['start_date'], '%Y.%m.%d') # 공연 시작 날짜
# 유사도 높은 순으로 정렬하되, 자기 자신은 제외
similar_performances_idx = np.argsort(performance_similarities)[::-1][1:top_n+1] # 유사도 높은 순
# 유효한 인덱스만 선택 (범위를 벗어난 인덱스를 제외)
similar_performances_idx = similar_performances_idx[similar_performances_idx < len(performances)]
# 유효한 공연들의 유사도 정보 저장
top_similar = []
for i in similar_performances_idx:
if i >= len(performances):
continue
수정 후)
# 각 공연에 대해 유사한 공연들을 찾기
for idx, performance in enumerate(performances):
if idx >= len(cosine_sim): # idx가 cosine_sim의 범위를 벗어나지 않도록 체크
print(f"Index {idx} is out of bounds for cosine_sim with size {len(cosine_sim)}")
continue
performance_similarities = cosine_sim[idx] # 해당 공연과 다른 공연들의 유사도
performance_start_date = datetime.strptime(performance['start_date'], '%Y.%m.%d') # 공연 시작 날짜
# 유사도 높은 순으로 정렬하되, 자기 자신은 제외
similar_performances_idx = np.argsort(performance_similarities)[::-1][1:top_n+1] # 유사도 높은 순
# 유효한 인덱스만 선택 (범위를 벗어난 인덱스를 제외)
similar_performances_idx = similar_performances_idx[similar_performances_idx < len(performances)]
# 유효한 공연들의 유사도 정보 저장
top_similar = []
for i in similar_performances_idx:
if i >= len(performances):
continue # 범위를 벗어난 인덱스는 건너뜀
cosine_sim 배열의 크기를 초과하는 idx 값을 사용할 때 발생하는 문제를 방지하기 위해,
반복문 내부에 조건문 if idx >= len(cosine_sim)을 추가하였다.
이 조건을 통해, cosine_sim 배열에서 참조할 수 없는 인덱스를 건너뛰게 된다.
일주일을 보내면서...
일주일을 보내면서 느낀 점들을 정리해보니, 프로젝트가 마무리 단계에 접어들었음을 실감한다.
기능 대부분이 완성되었고, 이제 남은 작업은 데이터 시각화와 발표 자료 준비다. 하지만, 팀원들과의 코드 통합 과정에서 나타난 수정 필요성이 약간의 과제가 되고 있다. 각자 다른 방식으로 작성된 코드들을 하나로 합치면서 발생하는 호환성 문제를 해결하고, 구조를 맞추는 작업이 추가되었다.
그리고 추천 시스템 개발 과정에서 느낀 아쉬움이 남는다. 공연 설명 데이터 중 텍스트가 아닌 이미지로 제공된 부분을 OCR로 처리했지만, 정확도가 떨어져 데이터 품질에 한계가 있었다. 이게 추천 결과에도 영향을 미칠 수 있다. 그래도 테스트 결과 완전히 나쁘지는 않았다는 점이 조금이나마 위안이 된다. 다만, 이 애매한 결과가 프로젝트의 완성도를 높이는 데 있어 과제로 남아 있다.
앞으로 나의 방향
다음 주에는 발표 자료 제작에 집중해야 한다. 발표까지 약 2주가 남았고, 초안을 대략적으로 잡아두었지만, 특히 아키텍처 다이어그램을 만들기가 어렵다. 내용을 직관적으로 보여주면서도 프로젝트의 모든 핵심을 담는 방식을 고민 중이다. 데이터 흐름과 시스템 구조를 한눈에 이해할 수 있도록 깔끔하고 설득력 있게 표현하는 것이 목표다. 그래도... 최선을 다해 남은 시간을 잘 활용해야겠다.
'데이터엔지니어 부트캠프' 카테고리의 다른 글
데이터엔지니어 부트캠프 - 파이널 프로젝트 (12월의 기록) (1) | 2024.12.28 |
---|---|
데이터엔지니어 부트캠프 - 유사 공연 추천시스템 cosine_sim[idx] 수정하기 (24주차) (0) | 2024.12.28 |
데이터엔지니어 부트캠프 - MongoDB jwt토큰 decode 하여 user_id 불러오기 (22주차) (1) | 2024.12.15 |
데이터엔지니어 부트캠프 - MongoDB에 적재할 때 중복값 처리하기 (0) | 2024.12.10 |
데이터엔지니어 부트캠프 - 로그데이터를 카프카를 이용해서 s3에 적재하기 (21주차) (1) | 2024.12.08 |