Joonas' Note

Joonas' Note

한국 가요(1964~2023)를 주제별로 모아보기 본문

AI/머신러닝

한국 가요(1964~2023)를 주제별로 모아보기

2024. 3. 3. 13:07 joonas
     

    한국 노래 가사(1964~2023년) 데이터 분석해보기

    배경 노래를 꽤 다양하게 듣는 편인데 최근 한국 노래들에 이지리스닝류가 많아지기도 했고, 한국어 가사의 비중이 점점 줄어들고 있는 등 한국 노래 가사의 트렌드가 궁금해져서 한번 데이터

    blog.joonas.io


    오랜 시간에 걸쳐서 음원 스트리밍 플랫폼에 추천이 많이 도입되었다.

    특정 노래를 기반으로 추천하는 건 아주 오래 전부터 이미 있었지만 (최소 2010년 iTunes 부터),
    근래에는 주제/테마별로 묶은 플레이리스트를 추천하기도 한다.

    YouTube Music 추천 플레이리스트

    정밀한 그룹화를 하려면 음원의 특성(파형, bpm, 장르, 가수, 연도 등)까지도 고려해야겠지만,
    이번에는 자연어 처리에 집중하고 싶은 만큼 가사 내용을 토대로 주제별로 나눌 수 있는 지 확인해보고자 한다.


    데이터셋

    데이터셋 내의 중복된 노래를 제외했더니, 4017개의 노래가 남았다.

    1964~2023년 멜론 TOP100 노래 가사 데이터

    두 가지 방법으로 클러스터링을 해볼까하는데, 하나는 노래 가사를 벡터로 변환하여 K-Means 로 클러스터링, 다른 하나는 대표적인 토픽 모델링 방법인 LDA 이다.

    K-Means

    형태소분석기는 이전 게시글에서 살펴본 Kiwi 를 사용하였고, 이번에는 의미 없는 단어들(불용어; stopword)을 제외하였다.
    간혹 동사/명사가 구분되어야 하는 경우가 있어서, 품사의 종류만 구분될 수 있도록 하였다.

    from kiwipiepy import Kiwi
    from kiwipiepy.utils import Stopwords
    
    kiwi = Kiwi(num_workers=2)
    stopwords_kr = Stopwords()
    
    def token_str(token):
        return f"{token.form}|{token.tag}"
    
    def get_words(t, filter_fn = lambda s: s) -> list:
        result = []
        for token in kiwi.tokenize(t, stopwords=stopwords_kr):
            if filter_fn(token):
                result.append(token_str(token))
        return result

    Word2Vec

    가장 많이 쓰이는 것 같은데, Gensim의 Word2Vec 를 사용해서 단어 뭉치를 n차원의 벡터로 변환했다.

     

    Gensim: topic modelling for humans

    Efficient topic modelling in Python

    radimrehurek.com

    from gensim.models import Word2Vec
    
    corpus = []
    for l in df['lyric']:
        corpus.append(get_words(l))
    
    # Word2Vec with skip-gram method
    w2v = Word2Vec(sentences=corpus, vector_size=100, window=5, min_count=1000, workers=4, sg=1)

    노래 1곡이 아래와 같은 프로세스를 거치면, n차원의 벡터로 만들 수 있다. 이 글에서는 100차원으로 벡터화했다.

    데이터셋의 모든 곡을 이렇게 벡터화해서, 각 노래의 중심(평균)만 추려서 4017x1000 차원의 벡터 집합을 만든다.
    그리고 다음과 같이 클러스터링하였다.

    from sklearn.cluster import KMeans
    
    lyrics_vectors = np.array([w2v.wv.get_mean_vector(c) for c in corpus])
    km = KMeans(init='k-means++', n_clusters=5, random_state=42)
    km.fit(lyrics_vectors)
    km.score(lyrics_vectors)

    시각화

    클러스터링이 잘 되었는지 시각화를 하기 위해서 2차원 좌표 상으로 차원을 축소해보았다.
    PCA는 해봤는데 잘 안되는 것 같아서 t-SNE 를 사용했다.

    from sklearn.manifold import TSNE
    
    tsne = TSNE(n_components=2, perplexity=100, random_state=42)
    transformed = tsne.fit_transform(lyrics_vectors)
    df1['stne_x'] = transformed[:, 0]
    df1['stne_y'] = transformed[:, 1]
    transformed.shape

    K-Means 로 군집화했을 때 데이터를 시각화

    최적 클러스터 수는 elbow method로 확인해보니까 5개가 적당하게 나와서, 5개의 주제로 분류해보았다.

    결과 보기

    근데 문제는, 이렇게 분류한 5개의 주제가 정확히 무슨 근거로 모였는지를 알 수가 없었다.

    주제 4개만 확인해봤는데, 각 주제를 뭐라 설명해야할 지 모르겠다.

    아래의 노래들이 한 주제로 묶였는 데 판단은 이 글을 읽는 사람의 몫으로 남겨보겠다.


    LDA

    주어진 문서에 어떤 주제들이 있는 지 파악하는 확률적 토픽 모델링 기법이다. 자세한 내용은 검색하면 많이 나오니 한번은 읽어보는 것을 추천한다.

    from gensim.models import LdaModel, CoherenceModel
    
    coherences_by_topics = []
    
    for n_topics in range(2, 10+1):
        coh = []
        for seed in [42, 43, 44, 45, 46]:
            lda_tmp = LdaModel(common_corpus, id2word=common_dictionary, num_topics=n_topics, per_word_topics=True, chunksize=500, random_state=seed)
            coherence_model_lda = CoherenceModel(model=lda_tmp, texts=lyrics_all, dictionary=common_dictionary, coherence='c_v')
            coherence = coherence_model_lda.get_coherence()
            coh += [coherence]
            coherences_by_topics.append((n_topics, seed, coherence))
        coherence = np.array(coh).mean()
        print(f'Coherence Score with {n_topics} topics: ', coherence)

    적절한 주제(클러스터) 수를 잡기 위해서 5개의 랜덤 시드마다 결과를 확인해보고 coherence 평균값을 구해서, 9개의 주제로 분류하기로 결정했다.

    클러스터 개수별 coherence

    lda = LdaModel(common_corpus,
                   id2word=common_dictionary,
                   num_topics=9,
                   per_word_topics=True,
                   chunksize=500,
                   random_state=46)

    Gensim 라이브러리는 LDA 결과로 html 파일을 제공하는데 이게 시각적으로 확인하기가 참 편하다.

    앞서 진행했던 K-Means 보다는 주제 내 각 단어의 중요도 가중치를 볼 수 있어서 좋았지만, 여전히 플레이리스트 제목처럼 한 문장으로 정의하기는 어려웠다.

    클러스터를 한 문장으로

    각 주제마다 중요도가 높은 상위 10개의 단어를 뽑아서, GPT 같은 LLM 모델로 주제의 문장을 완성해보면 어떨까했다.

    먼저 아래처럼 단어와 중요도를 표로 만들었다.

    wp_raw = lda.show_topic(1, topn=10)
    wp = "|단어|중요도|\n|-|-|"
    wp += "\n".join([f"|{word}|{prop:.5f}|" for word, prop in wp_raw])
    
    """|단어|중요도|
    |-|-|
    |순간|0.06931|
    |안|0.03649|
    |이렇|0.03513|
    |사이|0.03065|
    |이제|0.03055|
    |모르|0.03042|
    |다가오|0.02750|
    |있|0.02157|
    |영원|0.02100|
    |거 주|0.02054|"""

    그리고 OpenAI 에서 Python 라이브러리를 제공하고 있어서 템플릿을 간단하게 만들어서 QA 형태로 실행했다.

    wp = lda.show_topic(topic_id, topn=10)
    keywords = [word for word, _ in wp]
    lda_qa_table = '\n'.join([
        "|단어|중요도|\n|-|-|",
        "\n".join([f"|{word}|{prop:.5f}|" for word, prop in wp])
    ])
    qa = "다음은 노래 가사로부터 추출한 단어와 그 중요도입니다. 어떤 주제인지 한 문장으로 표현하세요. 제약 사항을 준수하시오.\n"
    qa += f"{lda_qa_table}\n"
    qa += "제약 사항:\n" + '\n'.join([
        "- 음원 플레이리스트 제목처럼 작성하세요.",
        "- 최대한 간결하게 작성하세요.",
        "- 5단어 이상 사용하지 마세요",
    ])
    
    response = ai.chat.completions.create(
      model="gpt-3.5-turbo",
      response_format={ "type": "text" },
      messages=[
        {"role": "user", "content": qa},
      ]
    )
    ans = response.choices[0].message.content
    
    print((topic_id, keywords, ans))

    결과

    주제별 TOP 10 단어와 중요도를 토대로, AI 문장 생성 결과
    각 주제별로 연관성이 높은 10개의 곡
    1964~2023년 사이의 노래를 주제별로 나누었을 때, 각 주제별 비율

    코드

    https://github.com/joonas-yoon/kpop-lyrics-analytics/blob/main/clustering.ipynb


    참고

     

    19-02 잠재 디리클레 할당(Latent Dirichlet Allocation, LDA)

    토픽 모델링은 문서의 집합에서 토픽을 찾아내는 프로세스를 말합니다. 이는 검색 엔진, 고객 민원 시스템 등과 같이 문서의 주제를 알아내는 일이 중요한 곳에서 사용됩니다. 잠재 디…

    wikidocs.net

     

    pyLDAvis 를 이용한 Latent Dirichlet Allocation 시각화하기

    LDAvis 는 토픽 모델링에 자주 이용되는 Latent Dirichlet Allocation (LDA) 모델의 학습 결과를 시각적으로 표현하는 라이브러리입니다. LDA 는 문서 집합으로부터 토픽 벡터를 학습합니다. 토픽 벡터는 단

    lovit.github.io

     

    Comments