본문 바로가기
자연어처리/실습

한국 대중가요 가사 분석 프로젝트 (4) - 토픽 모델링 (LDA)

by 아인슈페너먹고싶다 2022. 10. 25.

이전 글 

(1) 크롤링 + 빈도 분석 : https://nthree.tistory.com/24

(2) word cloud : https://nthree.tistory.com/43

(3) 장소 어휘 : https://nthree.tistory.com/44

 


 

 

대중가요 가사 분석 프로젝트의 마지막 토픽모델링이다.

 

이전 포스팅과 비교해서 상대적으로 내용이 길다고 생각하지만 요약해서 해보겠다!

 

구글링의 비율이 압도적이기도 하고 시간이 좀 지나서 기억이 잘 안나는 부분도 있다 ㅎㅎ

 

 


 

1
2
3
4
5
import konlpy
import re
from konlpy.tag import Okt
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
cs

 

 

이전에 했던 것처럼 

 

필요한 패키지를 설치해준 다음

 

데이터를 불러오고 간단한 전처리를 진행해준다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def tokenize_korean_text(text):
    text = re.sub(r'[^,.?!\w\s]','', text)  ## ,.?!와 문자+숫자+_(\w)와 공백(\s)만 남김  # 앞에 r을 붙여주면 deprecation warning이 안뜸 (raw string으로 declare)
    
    okt = konlpy.tag.Okt()
    Okt_morphs = okt.pos(text, stem = True)   # stem=True로 설정하면 동사원형으로 바꿔서 return
    
    words = []
    for word, pos in Okt_morphs:
        if pos == 'Adjective' or pos == 'Verb' or pos == 'Noun':  # 이 경우에는 형용사, 동사, 명사만 남김
            words.append(word)
 
    words_str = ' '.join(words)
    return words_str
 
 
# 가사를 하나씩 tokenize해서 list로 저장
tokenized_list = []
 
for text in df2010['lyrics']:
    tokenized_list.append(tokenize_korean_text(text))
 
print(len(tokenized_list))
print(tokenized_list[0])
cs
200
앞 한마디 하다 뒤 내 얘길 안 좋다 해 차다 어이 없다 나 같다 이다 처음 보다 것 같다 왜 나르다 판단 하다 내 혹시 두렵다 겉 속 나르다 자다 알다 하다 내 겉모습 보다 한심하다 여자 보다 너 시선 난 웃기다 춤추다 땐 사랑 춤추다 내 모습 볼 때 넋 놓다 보고서 끝나다 손가락질 하다 그 위선 난 웃기다 이렇다 옷 이렇다 머리 모양 이렇다 춤 추다 여자 뻔하다 네 더 뻔하다 자신 없다 저 뒤 뒤 뒤 물러서다 되다 왜 자꾸 떠들다 네 속이다 훤히 보이다 건 아니다 겉 속 나르다 자다 알다 하다 내 겉모습 보다 한심하다 여자 보다 너 시선 난 웃기다 춤추다 땐 사랑 춤추다 내 모습 볼 때 넋 놓다 보고서 끝나다 손가락질 하다 그 위선 난 웃기다 날 감당 하다 수 있다 남다 찾다 진짜 남자 찾다 말로 남자 다운 척 하다 남자 날 불안하다 하다 않다 남다 없다 자신감 넘치다 내 나일 수 있다 자유롭다 두다 멀리 바라보다 겉 속 나르다 자다 알다 하다 내 겉모습 보다 한심하다 여자 보다 너 시선 난 웃기다 춤추다 땐 사랑 춤추다 내 모습 볼 때 넋 놓다 보고서 끝나다 손가락질 하다 그 위선 난 웃기다

 

 

빈도기반 countvectorizer와 lda를 비롯한 다양한 패키지를 설치해준다. 

 

tokenization을 위한 함수를 정의해주었다.

 

2010년 가사를 리스트로 저장해주었고 

 

그 안에 토크나이징한 텍스트를 넣어준다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
drop_corpus = []
 
for index in range(len(tokenized_list)):
    corpus = tokenized_list[index]
    if len(set(corpus.split())) < 3:   # 같은 단어 1-2개만 반복되는 corpus도 지우기 위해 set()을 사용
        df2010.drop(index, axis='index', inplace=True)
        drop_corpus.append(corpus)
    
for corpus in drop_corpus:
    tokenized_list.remove(corpus)
 
df2010.reset_index(drop=True, inplace=True)
cs

 

 

중복된 corpus를 지우는 작업을 시행해준다.

 

 

1
2
3
4
5
6
count_vectorizer = CountVectorizer(max_df=0.1, max_features=1000, min_df=2, ngram_range=(1,2))
    # 2개의 문서 미만으로 등장하는 단어는 제외, 전체의 10% 이상으로 자주 등장하는 단어는 제외
    # bigram도 포함
 
feat_vect = count_vectorizer.fit_transform(tokenized_list)
print('CountVectorizer Shape:', feat_vect.shape)
cs

 

count_vectorizer 학습을 시켜준다.

 

 

1
2
3
4
5
6
7
8
9
10
11
#토픽수 설정
lda = LatentDirichletAllocation(n_components=14)  
lda.fit(feat_vect)
 
LatentDirichletAllocation(batch_size=128, doc_topic_prior=None,
             evaluate_every=-1, learning_decay=0.7,
             learning_method='batch', learning_offset=10.0,
             max_doc_update_iter=100, max_iter=10, mean_change_tol=0.001,
             n_components=14, n_jobs=None, perp_tol=0.1,
             random_state=None, topic_word_prior=None,
             total_samples=1000000.0, verbose=0)
cs

 

 

lda 하이퍼파라미터를 설정하고 lda 모델을 학습시켜준다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def display_topics(model, feature_names, num_top_words):
    for topic_index, topic in enumerate(model.components_):
        print('Topic #', topic_index)
 
        # components_ array에서 가장 값이 큰 순으로 정렬했을 때, 그 값의 array index를 반환. 
        topic_word_indexes = topic.argsort()[::-1]
        top_indexes=topic_word_indexes[:num_top_words]
        
        # top_indexes대상인 index별로 feature_names에 해당하는 word feature 추출 후 join으로 concat
        feature_concat = ' '.join([feature_names[i] for i in top_indexes])                
        print(feature_concat)
 
# CountVectorizer객체내의 전체 word들의 명칭을 get_features_names( )를 통해 추출
feature_names = count_vectorizer.get_feature_names()
 
# Topic별 가장 연관도가 높은 word를 10개만 추출
display_topics(lda, feature_names, 10)
cs
Topic # 0
아래 흔들다 흔들다 흔들다 울다 후회 때문 파란 후회 하다 입술 준비
Topic # 1
반하다 빛나다 고백 가다 모르다 사이 자꾸만 바로 몰래 밝다 어어
Topic # 2
그녀 그만하다 이름 좋다 하다 좋아하다 울다 노랗다 이야기 끌리다 죽다
Topic # 3
빨다 하다 우리 운명 그대로 소리 거울 놀다 아름답다 안녕 외톨이
Topic # 4
나가다 싶다 보고 오빠 제일 건지다 겨울 변하다 머물다 보다 되다 만나다 되다
Topic # 5
잠들다 내리다 벚꽃 새다 별빛 요즘 아름답다 가장 당신 따뜻하다
Topic # 6
나나 그땐 나나 나나 흔들다 그땐 그땐 빠지다 시작 하다 시작 비우다 그치다
Topic # 7
그녀 목소리 알다 사람 돌아오다 위해 수도 수도 없다 영원하다 향기 서로
Topic # 8
어쩌면 흘러가다 이번 라며 사랑 이번 마지막 라며 이번 마지막 가르치다 울다 일지
Topic # 9
좋아하다 문제 하다 않다 항상 미안하다 정신 설레다 듯이 미치다 듯이 그게
Topic # 10
없다 없다 잊어버리다 돌다 쉬다 꽂히다 나나 뛰다 알다 알다 보다 보다 되어다
Topic # 11
바다 춤추다 쉬다 추다 차라리 걷다 뻔하다 걷다 싶다 좋아하다 웃기다
Topic # 12
달라 뜨겁다 그대 그대 지나가다 돌아가다 원하다 돌아오다 먹다 피고 모든 순간
Topic # 13
치다 입술 누가 쉬다 흔들다 사이 어서 훔치다 몰래 티비

 

display topic 함수를 정의하고 토픽별로 연관도가 높은 단어들을 추출해보았다.
각 topic별로 단어가 잘 나눠진 것들도 있고 아닌 것도 있다.
물론 본 실습에선 추려서 진행하였다.

 

 

1
pip install pyLDAvis
cs

 

새로운 패키지를 설치해주고 lda를 시각화해보았다. 

 

1
2
3
4
5
import pyLDAvis.sklearn  # sklearn의 ldamodel에 최적화된 라이브러리
 
pyLDAvis.enable_notebook()
vis = pyLDAvis.sklearn.prepare(lda, feat_vect, count_vectorizer)
pyLDAvis.display(vis)
cs

 

 

 

(직접 캡쳐한 영상)

 

topic별 단어 빈도를 효과적이게 시각화 해준다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 노래별 가장 확률이 높은 topic으로 할당해줌
 
doc_topic = lda.transform(feat_vect)
 
doc_per_topic_list = []
for n in range(doc_topic.shape[0]):
    topic_most_pr = doc_topic[n].argmax()
    topic_pr = doc_topic[n].max()
    doc_per_topic_list.append([n, topic_most_pr, topic_pr])
    
doc_topic_df = pd.DataFrame(doc_per_topic_list, columns=['Doc_Num''Topic''Percentage'])
 
doc_topic_df = doc_topic_df.join(df2010)
doc_topic_df.head()
cs
 

노래별로 가장 확률이 높은 topic으로 할당해주었다. 

 

위에서도 그럤듯이 topic  name이 숫자로 되어있지만

 

실제로 토픽은 단어와 가사를 보고 적당한 주제로 임의 설정해주었다.  

 

 

 

1
2
3
4
5
6
7
#토픽별로, 가장 높은 확률로 할당된 노래 가사 top 3 확인
for topic in range(len(doc_topic_df['Topic'].unique())):
    print('Topic #', topic, '-----------------------------')
    top_pr_topics = doc_topic_df[doc_topic_df['Topic'== topic].sort_values(by='Percentage', ascending=False)
    print(top_pr_topics['lyrics'].iloc[0])
    print(top_pr_topics['lyrics'].iloc[1])
    print(top_pr_topics['lyrics'].iloc[2], '\n')
cs

 

 

마지막으로 토픽별 가장 확률이 높은, 말 그대로 주제를 가장 잘 반영하고 있는 노래 가사 top 3를 확인해 보았다.

 

결과물이 너무 사이즈가 커서 따로 업로드 하지는 않았다. 

 

(이런식으로 분석할 수 있다는 것을 보여주려고)

 

 

1
doc_topic_df.groupby('Topic')[['Doc_Num']].count()
cs

 

포스팅은 이렇게 원론적으로 진행했지만

 

실제 프로젝트 할 때는 앞선 모든 코드를 custom해주어 분석에 맞게 진행하였다.

 

마지막으로 토픽별로 count를 하여 가장 많이 사용된 topic들을 위주로 직접 topic을 정의해 준 후 분류하여 분석하였다.

 

 

 

실제 프로젝트에서 각 시기별로 자주 사용되었던 topic들은 다음과 같다.

 

 

 


 

이렇게 topic 모델링을 진행해보았다.

 

길고 긴 대중가요 가사 업로드를 마쳤는데

 

아직 깃허브에 정리를 하지 못하여 빨리 정리하고 

 

원서 작성 후 잠시 쉬고싶다 ㅎㅎ 

 

사실 바쁜척 하지만 여유로운데 너무 뒹굴뒹굴하느라 여유롭지 못하고 바쁜느낌?

 

뭐라는건지 모르겠지만 끝! 

댓글