사용자 맞춤형 음악 추천 기능을 구현하기 위해서는, 사용자의 취향을 정확하게 반영할 수 있는 알고리즘 설계가 핵심입니다.
MiTi 프로젝트에서는 이를 위해 곡의 특성을 기반으로 한 벡터 유사도 계산 방식을 도입하였고, 추천 정확도를 높이기 위한 리팩터링을 단계적으로 진행하였습니다.
💡 기존 추천 알고리즘 설계 방식
설계한 알고리즘: 개인 맞춤형 음악 추천, 장르별 음악 추천, 유사 음악 추천
# user_records에 포함된 album_id에 대해 평균 특성 벡터를 계산하는 함수
def calculate_average_features(user_records, album_dict):
feature_sums = np.zeros(len(next(iter(album_dict.values())))) # 첫 번째 앨범의 특성 벡터 길이만큼 0으로 초기화
count = 0
for user_id, album_id in user_records:
feature_sums += album_dict[album_id]
count += 1
if count == 0:
return feature_sums
else:
return feature_sums / count # 평균 특성 벡터 계산
# main 함수
def main():
# MySQL 데이터베이스에 연결
connection = pymysql.connect(
host='mitidb.cvm64ss6y2xv.ap-northeast-2.rds.amazonaws.com',
user='minseo',
password='Alstj!!809',
database='mitiDB'
)
try:
records, albums = fetch_records_and_albums(connection)
# album_id를 키로 하고, 여러 특성을 값으로 하는 딕셔너리 생성
album_dict = {album[0]: np.array(album[1:]) for album in albums}
# user_record에 있는 모든 user_id에 대해 평균 특성 벡터 계산
average_features = calculate_average_features(records, album_dict)
similarities = []
for album_id, features in album_dict.items():
# 유클리드 거리 계산
euclidean_distance = euclidean(average_features, features)
similarities.append((album_id, euclidean_distance))
# 유사도 기준으로 상위 20개의 앨범 선택
top_20_albums = sorted(similarities, key=lambda x: x[1])[:20]
# 결과를 customized_rec 테이블에 삽입
with connection.cursor() as cursor:
for user_id, _ in records: # 모든 user_id에 대해 반복
for album_id, distance in top_20_albums:
try:
cursor.execute(
"INSERT INTO customized_rec (user_id, album_id) VALUES (%s, %s)",
(user_id, album_id)
)
except pymysql.IntegrityError:
# 중복 키 에러가 발생하면 해당 항목을 삽입하지 않음
continue
connection.commit() # 변경사항을 커밋하여 데이터베이스에 반영
초기에는 Spotify에서 재생 가능한 곡의 오디오 특성(audio features)을 수집한 후,
사용자의 재생 기록에 포함된 곡들의 평균 특성 벡터를 계산하고,
이 벡터와 가장 유사한 곡들을 추천하는 방식으로 알고리즘을 설계했습니다.
🔸 예: danceability, energy, acousticness 등의 특성을 평균내어 사용자 취향을 표현하는 벡터를 구성
❗ 발생한 문제
기능 구현은 가능했지만, 실제 추천 결과를 보았을 때 사용자의 취향과 일치하지 않는 곡이 포함되는 문제가 발생했습니다.
그 원인을 분석해보니 다음과 같은 한계점이 있었습니다.
- 곡 수가 적은 재생목록은 평균값이 왜곡될 가능성이 높음
- 단순히 유클리드 거리로 유사도를 계산할 경우, 절대값 차이에 지나치게 민감하게 반응함
예를 들면, 운동 장르 플레이리스트에는 운동을 할 때 활력을 돋게 하기 위해 밝고 강한 음악을 추천하도록 설계하였지만, 차분하고 감미로운 음악이 추천되기도 하는 상황이 발생했습니다.
🔧 개선 방식
1. 유사도 계산 방식 변경 (유클리드 → 코사인 유사도)
여러 곡의 특성을 평균 낸 벡터는 스케일의 영향을 많이 받지 않기 때문에,
절댓값 차이를 계산하는 유클리드 거리보다는 벡터 간 방향을 비교하는 코사인 유사도가 더 적합하다고 판단했습니다.
실제로 유사도 계산에 있어 곡의 개수가 달라도 비슷한 경향성을 가진 벡터들끼리는 높은 유사도로 판단할 수 있어,
추천 정확도를 높이는 데 도움이 되었습니다.
2. 유사한 특성 값을 그룹으로 분류
< 음악 별 특성 값 정리 >
music_speechiness (곡의 말하기 부분의 비율): 가사가 없는 edm, 어두운 분위기의 랩 등 변수가 있는 요소이기 때문에 비교군에서 제외
값이 클수록 밝은 곡인 특성
music_danceability (곡의 춤출 수 있는 정도)
music_energy (곡의 에너지 수준)
music_loudness (곡의 음량 수준)
music_valence (곡의 긍정적/부정적 감정 정도)
music_tempo (곡의 템포)
값이 작을수록 밝은 곡인 특성
music_acousticness (곡의 어쿠스틱 특성) -> 주로 어쿠스틱 악기의 사용이 많은 음악이 차분한 경우가 많음
music_liveness (곡의 라이브 느낌 정도) -> 값이 클수록 청중의 소리나 라이브 환경의 특성이 포함되기 때문에 상대적으로 깔끔하지 않고 차분하다고 판단함
# 각 그룹의 특성 인덱스 정의 (0부터 시작)
features_group1_indices = [1, 2, 4, 5, 6] # danceability, energy, tempo, loudness, valence
features_group2_indices = [0, 3] # acousticness, liveness
try:
# user_record와 album 테이블에서 데이터 가져오기
records, albums = fetch_records_and_albums(connection)
# album_id를 정수로 변환하여 딕셔너리 생성
album_dict = {album[0]: np.array(album[1:], dtype=float) for album in albums} # 실수로 변환
# user_id별로 추천 결과 계산
user_ids = set(user_id for user_id, _ in records)
user_recommendations = {}
for user_id in user_ids:
# 해당 user_id의 레코드만 필터링
user_records = [(uid, aid) for uid, aid in records if uid == user_id]
# 각 그룹에 대해 평균 특성 벡터 계산
average_features_group1 = calculate_average_features(user_records, album_dict, features_group1_indices)
average_features_group2 = calculate_average_features(user_records, album_dict, features_group2_indices)
# 각 앨범 특성 벡터와 사용자 평균 특성 벡터 간 코사인 유사도 계산 후 리스트에 저장
similarities_group1 = []
similarities_group2 = []
for album_id, features in album_dict.items():
try:
feature_group1 = features[features_group1_indices]
feature_group2 = features[features_group2_indices]
if np.all(feature_group1 == 0) or np.all(average_features_group1 == 0):
similarity_group1 = 0
else:
similarity_group1 = 1 - cosine(average_features_group1, feature_group1)
if np.all(feature_group2 == 0) or np.all(average_features_group2 == 0):
similarity_group2 = 0
else:
similarity_group2 = 1 - cosine(average_features_group2, feature_group2)
similarities_group1.append((album_id, similarity_group1))
similarities_group2.append((album_id, similarity_group2))
except Exception as e:
print(f"Error processing album_id {album_id}: {e}")
# 각 그룹별로 유사도 내림차순으로 정렬
sorted_group1 = sorted(similarities_group1, key=lambda x: x[1], reverse=True)
sorted_group2 = sorted(similarities_group2, key=lambda x: x[1], reverse=True)
# 중복된 앨범만 선택하여 상위 20개 앨범 리스트에 추가
top_albums = []
seen_album_ids = set()
for album_id, similarity in sorted_group1 + sorted_group2:
if album_id in seen_album_ids:
continue
top_albums.append((album_id, similarity))
seen_album_ids.add(album_id)
if len(top_albums) == 20:
break
user_recommendations[user_id] = top_albums
값이 클수록/ 작을수록 밝은 요소끼리 그룹으로 묶어 따로 유사도를 계산한 후,
유사도를 내림차순으로 정렬하고, 최대한 유사도가 강한 음악을 선별하기 위해 두 그룹에 모두 담겨있는 앨범만을 저장했습니다.
+ 추가로 중복 방지를 위한 코드를 삽입해 한 번 더 중복 저장을 예방하였습니다.
except pymysql.IntegrityError:
continue
3. 장르별 음악의 적합성 향상
운동 (Workout)
- 높은 music_danceability: 춤추기 좋은 리듬
- 높은 music_energy: 활기찬 음악
- 높은 music_tempo: 빠른 템포
- 높은 music_loudness: 높은 볼륨, 강렬한 효과음
휴식 (Relaxation)
- 높은 music_acousticness: 차분하고 조용한 분위기
- 낮은 music_energy: 에너지 부족한 분위기
- 낮은 music_tempo: 느린 템포, 편안한 리듬
집중 (Focus)
- 낮은 music_loudness: 낮은 볼륨, 부드러운 사운드
- 낮은 music_tempo: 느린 템포, 차분한 리듬
- 높은 music_valence: 긍정적이고 희망적인 감정
파티 (Party)
- 높은 music_danceability: 춤추기 좋은 리듬
- 높은 music_energy: 활기찬 음악
- 높은 music_tempo: 빠른 템포
- 높은 music_valence: 긍정적이고 밝은 감정
스트레스 해소 (Stress Relief)
- 높은 music_acousticness: 자연스러운 소리, 어쿠스틱 악기 사용
- 높은 music_liveness: 라이브 느낌, 현장감 있는 사운드
- 낮은 music_tempo: 느린 템포, 차분한 리듬
여행 (Travel)
- 높은 music_energy: 에너지 넘치는 음악
- 높은 music_tempo: 빠른 템포, 활동적인 리듬
- 높은 music_valence: 긍정적이고 열정적인 감정
사랑 (Romance)
- 높은 music_acousticness: 어쿠스틱 악기와 조용한 분위기
- 높은 music_valence: 로맨틱하고 감정적인 음악
- 낮은 music_tempo: 느린 템포, 섬세한 리듬
수면 (Sleep)
- 높은 music_acousticness: 차분하고 조용한 분위기
- 낮은 music_energy: 에너지 부족한 분위기
- 낮은 music_tempo: 느린 템포, 부드러운 리듬
클래식 (Classical)
- 높은 music_acousticness: 어쿠스틱 악기와 조용한 분위기
- 낮은 music_tempo: 느린 템포, 고요한 리듬
- 높은 music_valence: 우아하고 감동적인 감정
우울 (Sadness)
- 낮은 music_valence: 부정적이고 슬픈 감정
- 낮은 music_energy: 에너지 부족한 분위기
- 낮은 music_tempo: 느린 템포, 침울한 리듬
대표적인 장르를 10개로 지정해 강조돼야 하는 특성을 3개씩 뽑아 동일한 방법으로 비슷한 특성끼리 그룹으로 묶어 계산을 진행했습니다.
📈 결과
동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.
위에서 예를 들었던 운동장르의 코드 개선 전, 후를 비교해 보면 빠르고 밝고 활기찬 느낌에
훨씬 더 적합한 음악이 추천되는 것을 한눈에 확인할 수 있습니다.
✅ 마무리하며
평균 특성 벡터 기반의 추천 구조를 설계하면서 데이터를 해석하고, 적절한 기술을 선택하는 판단력의 중요성을 체감했습니다.
앞으로도 사용자의 경험을 중심에 두고, 끊임없이 개선하고 실험하며 더 나은 추천 시스템을 설계해 나가고 싶습니다.
'SPRINGBOOT > 음악 스트리밍 서비스' 카테고리의 다른 글
| Spotify 음악 크롤링 중 마주한 문제들과 이를 해결한 과정 (0) | 2025.03.21 |
|---|