Joonas' Note
한국 가요(1964~2023)를 주제별로 모아보기 본문
오랜 시간에 걸쳐서 음원 스트리밍 플랫폼에 추천이 많이 도입되었다.
특정 노래를 기반으로 추천하는 건 아주 오래 전부터 이미 있었지만 (최소 2010년 iTunes 부터),
근래에는 주제/테마별로 묶은 플레이리스트를 추천하기도 한다.
정밀한 그룹화를 하려면 음원의 특성(파형, bpm, 장르, 가수, 연도 등)까지도 고려해야겠지만,
이번에는 자연어 처리에 집중하고 싶은 만큼 가사 내용을 토대로 주제별로 나눌 수 있는 지 확인해보고자 한다.
데이터셋
데이터셋 내의 중복된 노래를 제외했더니, 4017개의 노래가 남았다.
두 가지 방법으로 클러스터링을 해볼까하는데, 하나는 노래 가사를 벡터로 변환하여 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차원의 벡터로 변환했다.
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
최적 클러스터 수는 elbow method로 확인해보니까 5개가 적당하게 나와서, 5개의 주제로 분류해보았다.
결과 보기
근데 문제는, 이렇게 분류한 5개의 주제가 정확히 무슨 근거로 모였는지를 알 수가 없었다.
아래의 노래들이 한 주제로 묶였는 데 판단은 이 글을 읽는 사람의 몫으로 남겨보겠다.
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개의 주제로 분류하기로 결정했다.
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))
결과
코드
https://github.com/joonas-yoon/kpop-lyrics-analytics/blob/main/clustering.ipynb
참고
'AI > 머신러닝' 카테고리의 다른 글
[ML] 사이킷런(scikit-learn) 클러스터링 비교 (0) | 2022.05.29 |
---|---|
Content-based File Format Detection (파일 확장자 예측) (0) | 2022.05.18 |
[ML/Sklearn] ROC Curve visualization (0) | 2022.04.08 |