사용자 취향 반영을 위한 음악 추천 알고리즘 개선 과정

2025. 3. 25. 02:09·SPRINGBOOT/음악 스트리밍 서비스

사용자 맞춤형 음악 추천 기능을 구현하기 위해서는, 사용자의 취향을 정확하게 반영할 수 있는 알고리즘 설계가 핵심입니다.

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개씩 뽑아 동일한 방법으로 비슷한 특성끼리 그룹으로 묶어 계산을 진행했습니다.

 


 

📈 결과

 

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

 

위에서 예를 들었던 운동장르의 코드 개선 전, 후를 비교해 보면 빠르고 밝고 활기찬 느낌에

훨씬 더 적합한 음악이 추천되는 것을 한눈에 확인할 수 있습니다.

 


 

✅ 마무리하며

 

평균 특성 벡터 기반의 추천 구조를 설계하면서 데이터를 해석하고, 적절한 기술을 선택하는 판단력의 중요성을 체감했습니다.

앞으로도 사용자의 경험을 중심에 두고, 끊임없이 개선하고 실험하며 더 나은 추천 시스템을 설계해 나가고 싶습니다.

 

 

👉 전체 알고리즘 코드 보러 가기

👉 spotify 크롤링 개선 과정 보러 가기

'SPRINGBOOT > 음악 스트리밍 서비스' 카테고리의 다른 글

Spotify 음악 크롤링 중 마주한 문제들과 이를 해결한 과정  (0) 2025.03.21
'SPRINGBOOT/음악 스트리밍 서비스' 카테고리의 다른 글
  • Spotify 음악 크롤링 중 마주한 문제들과 이를 해결한 과정
bogyeom
bogyeom
백엔드 개발자 준비중
  • bogyeom
    딩코링코
    bogyeom
  • 전체
    오늘
    어제
    • 분류 전체보기 (10)
      • Node.js (0)
        • 크리스마스 미니홈피 (0)
      • SPRINGBOOT (2)
        • 음악 스트리밍 서비스 (2)
        • 식당 추천 서비스 (0)
      • 우아한 테크코스 프리코스 (3)
      • 알고리즘 오답노트 (5)
        • JAVA (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
bogyeom
사용자 취향 반영을 위한 음악 추천 알고리즘 개선 과정
상단으로

티스토리툴바