일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 웹서비스 기획
- 데이터 프로젝트
- DecisionTree
- SQL
- 결정트리
- ifnull
- LAG
- sorted
- CASE WHEN
- 데이터 전처리
- 재현율
- recall
- 강화학습
- 데이터 분석
- 평가 지표
- NULLIF
- Normalization
- 감정은 습관이다
- 빠르게 실패하기
- layer normalization
- five lines challenge
- 지도학습
- 정밀도
- NVL
- beautifulsoup
- 백엔드
- nvl2
- Batch Normalization
- 오차 행렬
- 비지도학습
- Today
- Total
Day to_day
[하루한끼_데이터] 문서 간 유사도 검사를 통한 추천 시스템 만들기 #2 본문
전체 코드는 "[NLP] 문서 군집화(Clustering)와 문서 간 유사도(Similarity) 측정하기"를 참고하여 작성되었습니다.
저번 포스팅에서 문서 간 유사도 검사를 위해 각 문서의 TF-IDF를 벡터화시켰다.
이제 코사인 유사도 검사를 하기 전에 클러스터링을 통해 그룹을 먼저 나눠주고 그 그룹 내에서 유사도 검사를 진행해 하나의 레시피를 몇 백개와 수없이 비교하는 것을 방지한다.
그러면 내가 사용할 클러스터링 알고리즘은 K-means clustering이다. 왜 하필 K-means clustering이냐고? 그게 제일 먼저 알고 있던 알고리즘이기도 하고, 또 다른 군집 모델을 알지 못했다;;
나중에 유사도 검사를 다 끝내 놓고 알고 보니 데이터의 분포에 따라 잘 작동하는 클러스터링 모델이 있다고 한다.
위 사진은 사이킷런 공식 문서에서 가져온 clustering method이다. 근데 데이터의 분포를 먼저 확인해보기엔 "차원 축소"라는 개념이 먼저 필요했다. 일단은 K-means 알고리즘을 먼저 사용해보고 성능 확인 후 디벨롭할 때 시도해 보기로 한다.
그러면 일단 K-means Clustering에 대해서 알아야 한다.
K-means Clustering
데이터는 항상 label 값이 있는 것이 아니고 라벨링 되어있지 않은 데이터들을 가지고 숨겨진 패턴을 찾는 것을 비지도 학습이라고 한다. 그중 Clustering은 가장 널리 알려진 비지도 학습 중 한 가지 기법으로, 비슷한 유형의 데이터를 그룹화 함으로써 라벨링 되어있지 않은 데이터에 숨겨진 구조를 파악한다.
K-means 알고리즘은 가장 유명한 클러스터링 알고리즘으로 “K”가 의미하는 바는 주어진 데이터로부터 그룹화할 그룹, 즉 묶음의 수를 말한다. “Means”는 각 클러스터의 중심과 데이터들의 평균 거리를 의미한다. 이때 클러스터의 중심을 centroids라고 한다.
<K-means 알고리즘의 진행 순서>
- 데이터셋에서 K 개의 centroids를 임의로 지정
- 각 데이터들을 가장 가까운 centroids가 속한 그룹에 할당
- 2번 과정에서 할당된 결과를 바탕으로 centroids를 새롭게 지정
- 2 ~ 3번 과정을 centroids가 더 이상 변하지 않을 때까지 반복
이제 코드를 보자
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=5, max_iter=10000, random_state=42)
# 비지도 학습이므로 feature로만 학습시키고 예측
cluster_label = kmeans.fit_predict(ftr_vect)
# 군집화한 레이블 값들을 df에 추가
df["cluster_label"] = cluster_label
df.sort_values(by=['cluster_label'])
코드에서 쓰인 KMeans의 인자로 n_clusters는 그룹의 개수(클러스터의 개수), max_iter 최대 반복 횟수, random_state는 호출할 때마다 동일한 학습/테스트용 데이터 세트를 생성하기 위해 주어지는 난수 값이다.
첫 번째 실행 결과
얼추 확인해 봤을 땐 cluster 개수가 문젠 건지 잘 군집화가 안된 모습이 한눈에 보인다.
(salsa와 cookies가 한 묶음에?)
그래서 클러스터링이 최대한 잘 될 수 있도록 변수들을 조금씩 조절해가면서 다시 학습시켜야 한다.
'잘' 학습이 되었는지를 알기 위해선 다른 작업이 추가적으로 필요하다. 앞서 말했듯이 K-means 클러스터링 알고리즘은 비지도 학습이다. 즉 그룹을 묶었는데 이게 잘 묶였는지를 확인할 '정답지'가 없다는 것이다.
일단 변수 수정을 해보고 잘 묶었는지 판단해야 하니 각 클러스터(그룹) 별로 문서들의 대표되는 feature 단어를 보고, 그 대표되는 feature 단어와 그 안에 속해있는 레시피(문서)의 제목을 보고 진짜 잘 묶였는지를 판단하기로 한다.
각 클러스터 중심 좌표 반환하여 핵심 단어 추출
# 문서의 feature(단어 별) cluster_centers 확인하기
cluster_centers = kmeans.cluster_centers_
print(cluster_centers.shape) # (클러스터 레이블, feature 단어들)
print(cluster_centers)
실행 결과
cluster_centers.shape : (5, 164)
cluster_centers :
결과에 대한 의미
- 위의 좌표들은 각 feature 들과 중심 간의 상대적인 위치를 반환한다.
- 위치 값들은 0-1 사이의 값으로 나오는데 1로 갈수록 특정 단어 feature와 클러스터 중심과의 거리가 가장 가깝고 관계가 있다는 의미
- 여기서 cluster_centers.shape를 찍어보면 (5, 164)라는 값이 나오는데 처음엔 5개 그룹과 164개의 문서의 비슷한 정도를 말하는 줄 알았다. 근데 그게 아니고 5는 클러스터 숫자를 의미하는 것은 맞는데, 뒤에 164가 의미하는 것은 feature 단어를 의미한다.
- 정리하자면 5개의 각 클러스터 별로 164개의 feature 단어들을 뽑은 것임.
- 그래서 아래에서 상위 5개의 feature 단어를 구하는 게 최대 164개까지 있는 거고 그중에서 1에 가까운 순서대로 나열해서 가장 앞쪽에 있는 5개의 특징 단어들을 추출한 것!
- feature 단어를 뽑는 게 무슨 의미가 있나요?
- 일반적으로 머신러닝, 딥러닝을 수행할 땐 라벨링이 되어있는 상태에서 training 데이터와 test 데이터로 나눠서 잘 학습되었는지 정확도를 평가 지표로 삼는다. 그래서 정량적으로 수치를 낼 수 있는 것.
- 하지만 위의 데이터의 경우는 내가 직접 태그를 붙여서 라벨링을 해준 것도 아니고 비지도 학습이기 때문에 수치적으로 알 수가 없음. 그래서 사람이 판단하기로 feature 단어를 뽑아서 대충 이 단어들을 봤을 때 레시피의 내용을 짐작할 수 있게 하는 판단의 근거로 쓸 수 있음
- 그래서 feature 단어를 뽑고 그것을 기준으로 clustering이 잘 되었는지, 잘 안되었는지를 알 수가 있다.
Feature의 좌표를 이용한 클러스터 당 핵심 단어 추출
def get_cluster_details(cluster_model, cluster_data, feature_names,
cluster_num, top_n_features=10):
cluster_details = {}
# 각 클러스터 레이블별 feature들의 center값들 내림차순으로 정렬 후의 인덱스를 반환
# cluster_model = kmeans로 사용할 것이므로 위에서 테스트 했던 대로 cluster_centers_를 이용하면 상대 좌표가 나옴
center_feature_idx = cluster_model.cluster_centers_.argsort()[:,::-1]
# 개별 클러스터 레이블별로
for cluster_num in range(cluster_num):
# 개별 클러스터별 정보를 담을 empty dict할당
cluster_details[cluster_num] = {}
cluster_details[cluster_num]['cluster'] = cluster_num
#print(cluster_details) #{0: {'cluster': 0}}
# 각 feature별 center값들 정렬한 인덱스 중 상위 5개만 추출 top_n_features = 5
# feature_names : feature 단어들을 추출한 것
top_ftr_idx = center_feature_idx[cluster_num, :top_n_features] # 상위 5개 인덱스 번호
top_ftr = [feature_names[idx] for idx in top_ftr_idx] # 상위 5개 feature 단어
# top_ftr_idx를 활용해서 상위 5개 feature들의 center값들 반환
# 반환하게 되면 array이기 떄문에 리스트로바꾸기
top_ftr_val = cluster_model.cluster_centers_[cluster_num, top_ftr_idx].tolist()
# cluster_details 딕셔너리에다가 개별 군집 정보 넣어주기
cluster_details[cluster_num]['top_features'] = top_ftr # 상위 5개 feature 단어
cluster_details[cluster_num]['top_featrues_value'] = top_ftr_val # 상위 5개 중심으로부터 상대 좌표
# 해당 cluster_num으로 분류된 메뉴명 넣어주기
title_names = cluster_data[cluster_data['cluster_label']==cluster_num]['title']
#df[df['cluster_label']==cluster_num]['title']
# title_names가 df으로 반환되기 떄문에 값들만 출력해서 array->list로 변환
title_names = title_names.values.tolist()
cluster_details[cluster_num]['title_names'] = title_names
return cluster_details
def print_cluster_details(cluster_details):
for cluster_num, cluster_detail in cluster_details.items():
print(f"#####Cluster Num: {cluster_num}")
print()
print("상위 5개 feature단어들:\n", cluster_detail['top_features'])
print()
print(f"Cluster {cluster_num}으로 분류된 레시피들:\n{cluster_detail['title_names'][:5]}")
print('-'*20)
feature_names = tfidf_vect.get_feature_names()
cluster_details = get_cluster_details(cluster_model=kmeans,
cluster_data=df,
feature_names=feature_names,
cluster_num=30,
top_n_features=5) #상위 10개 feature 추출하는게 디폴트이지만 5개만 추출
print_cluster_details(cluster_details)
함수 실행 결과 예시
코드 설명
- center_feature_idx : 각 클러스터 별로 centroid 간의 상대 좌표를 내림차순으로 정렬한 것이다.
- argsort()는 일반적인 sort() 함수와 달리, sorting을 한 후 값을 array로 재 정렬하는 것이 아닌 값의 인덱스 값을 array로 정렬한다.
- top_ftr_idx : 위에서 인덱스로 array를 만든 center_feature에서 상위 5개의 인덱스를 가져오는 것.
- 만약 클러스터 넘버가 0이라면, top_ftr_idx = center_feature_idx [0, :5] 이런 식으로 들어가서 0번째에 해당하는 [64 35 109 …. 107 111 117]이 array에서 상위 5개의 인덱스를 가져온다.
- top_ftr : feaure_names(feature 단어들)에서 상위 5개에 해당하는 인덱스를 대입해 그 인덱스에 해당하는 featrue 단어를 넣은 것
- title_names : cluster_num에 따른 메뉴명(title)을 넣어준 데이터
- cluster_details : get_cluster_details 함수의 리턴 값
- print_cluster_details 함수에 들어가는 인풋이 된다. print_cluster_details의 for 문은 cluster_details.item()으로 받아와서 cluster_num와 cluster_detail을 사용한다.
- { cluster_num : 0 , cluster_detail : {’cluster’ : 0 ……} }
그래서 클러스터링을 몇 번 하면서 적절한 클러스터 개수, 변수들을 조절해서
n_clusters는 30, max_iter 최대 반복 횟수 1000, random_state 42로 두고 진행했다.
문서들 간의 유사도 측정
코드를 보기 전에 난 코사인 유사도를 이용하여 문서의 유사도를 구할 것이다.
코사인 유사도 (Cosine Similarity)
코사인 유사도는 두 벡터 간의 코사인 각도를 이용하여 구할 수 있는 두 벡터의 유사도를 의미한다. 두 벡터의 방향이 완전히 동일한 경우 1의 값을 가지며, 90도의 각을 이루면 0, 180도로 반대 방향을 가지면 -1의 값을 갖게 된다.
결국 코사인 유사도는 -1 이상 1 이하의 값을 가지며 값이 1에 가까울수록 유사도가 높다고 판단할 수 있다.
아래의 두 사진을 보면 바로 이해가 될 것이다.
그런데 텍스트의 경우엔 단어들을 feature화 시킬 때 음수 값이 나올 수 없으므로 코사인 유사도가 -1이 나올 경우는 없다.
따라서 단어 벡터들 간의 유사도는 0~1 사이의 값으로 나온다.
코드
# 클러스터링된 문서들 중에서 특정 문서를 하나 선택한 후 비슷한 문서 추출
from sklearn.metrics.pairwise import cosine_similarity
choco_idx = df[df['cluster_label']==8].index
print("chocolate 카테고리로 클러스터링된 레시피들의 인덱스:\n", choco_idx)
print()
# 초콜렛 카테고리로 클러스터링 된 문서들의 인덱스 중 하나 선택해 비교 기준으로 삼을 문서 선정
compare_recipe = df.iloc[choco_idx[0]]['title']
print('##유사도 비교 기준 문서 이름: ', compare_recipe, '##')
print()
# 위에서 추출한 초콜렛 카테고리로 클러스터링된 문서들의 인덱스 중 0번 인덱스(비교기준문서) 제외한
# 다른 문서들과의 유사도 측정
similarity = cosine_similarity(ftr_vect[choco_idx[0]], ftr_vect[choco_idx])
print(similarity)
- 그래도 얼추 잘 나눠진 것 같은 카테고리를 선택했고 cluster number는 8이었다.
- 키워드는 chocolate
- 비교할 기준이 있어야 하니깐 compare_recipe는 0번째 인덱스로 둔다.
- 유사도 측정을 할 땐 sklearn에서 제공하는 cosine_similarity를 사용한다.
- ftr_vect는 아래와 같이 위쪽 코드에서 전체 문서에 대해 feature vector를 생성했다.
- ftr_vect = tfidf_vect.fit_transform(df['ingredients_list'])
- ftr_vect[choco_idx[0]] 은 인덱스 19의 feature vector를 불러온다는 것
- choco_idx
즉 compare_recipe의 feature vector와 다른 나머지 레시피의 feature vector를 코사인 유사도를 이용해서 구한다는 말이다.
시각화 결과
유사도를 시각화하여 결과를 살펴보면 아래와 같다. (시각화 코드는 생략했다.)
결과 : ‘Nutty Chocolate Truffles’와 비교해 봤을 때 Gluten-free Brownies가 가장 유사도가 높은 것으로 보였다.
추가!
재료를 기준으로 유사도 검사를 해서 레시피를 추천하는 것과 조리 과정을 기준으로 하는 것의 결과 차이는 어떨까?
조리 과정으로 유사한 레시피 구하기
결과 : ‘Nutty Chocolate Truffles’와 비교해봤을 때 Chocolate Nests with Sea Salt Caramel Eggs 가 가장 유사도가 높은 것으로 보였다.
결론
재료가 비슷한 클러스터로 묶였기 때문에 조리 과정을 기준으로 유사도를 검사했을 땐, 재료의 유사성과 조리법의 유사도 둘 다 비슷한 레시피가 나오는 것 같다.
하지만 여기서 고민해 봐야 할 점은 레시피 추천 시스템을 만들기 위해서 과연 유저가 1) 비슷한 재료를 가지고 비슷한 음식을 만들기를 원할지, 또는 2) 비슷한 재료를 가지고 다른 종류의 음식을 만들기를 원할 것 인지이다.
추가로 현재 초콜릿 카테고리에 대해서만 실험해보았지만 다른 카테고리에서도 잘 분류를 하는지에 대한 추가 실험이 필요할 것 같다.
Ref
'Project' 카테고리의 다른 글
AutoRAG 실험해보기 (+ 사용 후기) (0) | 2024.07.13 |
---|---|
[체어코치_기획] 새로운 프로젝트 시작! (0) | 2022.12.22 |
[하루한끼_데이터] 문서 간 유사도 검사를 통한 추천 시스템 만들기 #1 (0) | 2022.12.20 |
[하루한끼_데이터] 이미지 url 크롤링 (0) | 2022.11.09 |
[하루한끼_데이터] 데이터 전처리 2 : 정규표현식 (0) | 2022.11.07 |