Sun.El Data Analysis

[자연어처리] 문서 군집화(nltk - WordNetLemmatizer, sklearn - TfidfVectorizer, KMeans)_ 본문

Machine Learning

[자연어처리] 문서 군집화(nltk - WordNetLemmatizer, sklearn - TfidfVectorizer, KMeans)_

Sun.El 2023. 7. 31. 18:33
728x90
문서 군집화(Document Clustering)는 비슷한 텍스트 구성의 문서를 군집화(Clustering)하는 것

  • 문서 군집화는 동일한 군집에 속하는 문서를 같은 카테고리 소속으로 분류
  • 학습 데이터 세트가 필요없는 비지도학습 기반으로 동작
  • 문서 군집화는 문서를 피처 벡터화한 데이터 세트에 군집화 알고리즘 적용
  • 일반적으로 K-Means 군집화가 적용될 수 있음

 

1. 데이터 로딩

  • UCI 머신러닝 리포지토리에 있는 Opinion Review 데이터 세트
    (참고사이트) https://archive.ics.uci.edu/ml/machine-learning-databases/opinion/
  • 51개의 텍스트 파일로 구성
  • Tripadvisor(호텔), Edmunds.com(자동차), Amazon.com(전자제품) 사이트에서 가져온 리뷰 문서
  • 각 문서는 100개 정도의 문장을 가짐
[In]
add check point !
1. glob 함수
2. python에서 string 앞에 r을 표기해 주면 string literal → raw string으로 변경시켜줌(https://armin.tistory.com/279)
3. pd.set_option('max_colwidth', 500) : 표시되는 DataFrame의 셀 넓이 최대값 설정
import pandas as pd
import glob, os
path = r'C:\Users\norii\Documents\DataScience\source\DL\0731\OpinosisDataset\topics' #string literal -> raw literal

all_files = glob.glob(os.path.join(path, "*.data"))

filename_list = []
opinion_text = []

for file_ in all_files:
    #개별 파일 읽어서 DataFrame으로 생성
    #https://docs.python.org/3/library/codecs.html#standard-encodings
    df = pd.read_table(file_, index_col=None, header=0, encoding='latin1')
    #절대 경로로 주어진 file명을 가공, 맨 마지막 .data 확장자 제거
    filename_ = file_.split('\\')[-1]
    filename = filename_.split('.')[0]
    filename_list.append(filename)
    opinion_text.append(df.to_string()) #데이터 객체를 단순 string 형태로 변형하는 메서드
    
pd.set_option('max_colwidth',500) #판다스 셀 넓이 옵션
#파일 리스트, 리뷰에 대한 DataFrame
document_df = pd.DataFrame({'filename': filename_list, 'opinion_text': opinion_text})
document_df.head()

[Out]

filename opinion_text
accuracy_garmin_nuvi_255W_gps , and is very, very accurate .\n0 but for the most part, we find that t...
bathroom_bestwestern_hotel_sfo The room was not overly big, but clean and very comfortable beds, a great shower and very clean bathrooms .\n0 ...
battery-life_amazon_kindle After I plugged it in to my USB hub on my computer to charge the battery the charging cord design is very clever !\n0 After you have paged tru a 500, page book...
battery-life_ipod_nano_8gb short battery life I moved up from an 8gb .\n0 ...
battery-life_netbook_1005ha 6GHz 533FSB cpu, glossy display, 3, Cell 23Wh Li, ion Battery , and a 1 .\n0 ...

 

2. Lemmatization(표제어 추출)을 위한 함수 생성

 

정규화 기법 중 코퍼스에 있는 단어의 개수를 줄일 수 있는 기법
표제어 추출(lemmatization)과 어간 추출(stemming)이 있음

 

  • 표제어 추출이란? 단어들로부터 표제어를 찾아가는 과정(예시 : dies → die, am/are/is  be)
  • NLTK에서는 표제어 추출을 위한 도구인 WordNetLemmatizer를 지원함
    (참고사이트) 
[In]
add check point !
1. string.punctuation : string 패키지의 메소드
    따옴표,마침표 물음표 등등 이런류의 문장부호(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)
2. dict() : 
3. ord() : 하나의 문자를 인자로 받고 해당 문자에 해당하는 유니코드 정수를 반환
4. translate() : 문자열에서 문자를 제거하는 하나의 방법
   (예시) 
   developer = 'Jessica Wilkins'
   print(developer.translate({ord('i'): None}))
   Jessca Wlkns
from nltk.stem import WordNetLemmatizer
import nltk
import string

remove_punct_dict = dict((ord(punct), None) for punct in string.punctuation)  

print(remove_punct_dict)

#단어 원형 추출 함수
lemmar = WordNetLemmatizer()
def LemTokens(tokens):
    return [lemmar.lemmatize(token) for token in tokens]

#텍스트 소문자로 변경 후 특수문자 제거
def LemNormalize(text):    
    return LemTokens(nltk.word_tokenize(text.lower().translate(remove_punct_dict)))​

[Out]

{33: None, 34: None, 35: None, 36: None, 37: None, 
38: None, 39: None, 40: None, 41: None, 42: None, 43: None, 
44: None, 45: None, 46: None, 47: None, 58: None, 59: None, 
60: None, 61: None, 62: None, 63: None, 64: None, 91: None, 
92: None, 93: None, 94: None, 95: None, 96: None, 123: None, 
124: None, 125: None, 126: None}​

 

3. TF-IDF 피처 벡터화

TF-IDF는 단어마다 중요도를 고려하여 가중치를 주는 통계적인 단어 표현 방법
문서 간 유사도 검사, 중요문서 위주의 검색 결과 제공 시스템, 문서 빅데이터 내 핵심어 추출 등에 활용 

 

  • TF(Term Frequency) : 특정 문서 d에서 특정 단어 t의 출현빈도
  • DF(Document Frequency) : 전체 문서 D에서 특정 단어 t가 등장한 문서 d의 개수
  • IDF(Inverse Document Frequency) : DF값의 역수
  • TF-IDF : TF와 IDF의 곱(= TF  * IDF)
    (참고사이트) https://heytech.tistory.com/337

 

[In]
from sklearn.feature_extraction.text import TfidfVectorizer

#LemNormalize : 특수 문자 제거 및 단어 원형 추출
tfidf_vect = TfidfVectorizer(tokenizer = LemNormalize, stop_words ='english', ngram_range=(1,2), min_df=0.05, max_df=0.85)

#opinion_text 칼럼값으로 feature vectorization 수행
feature_vect = tfidf_vect.fit_transform(document_df['opinion_text'])

feature_vect.shape​

[Out]
(51, 4611)
#51개 문서, 4611개 피처

 

4-1. 군집화 - try #1

[In] KMeans 군집화(5개)
from sklearn.cluster import KMeans
#5개 집합으로 군집화 수행, 예제를 위해 동일한 클러스터링 결과 도출용 random_state=0
km_cluster = KMeans(n_clusters=5, max_iter=10000, random_state=0)
km_cluster.fit(feature_vect) #51개 문서, 4611개 피처
cluster_label = km_cluster.labels_
cluster_centers = km_cluster.cluster_centers

print(km_cluster.labels_)
print(km_cluster.labels_.shape) # 51개 문서​

[Out]

[2 0 1 1 1 2 4 4 2 2 2 2 2 3 3 3 4 4 4 1 3 3 4 2 3 4 1 3 3 4 0 0 0 2 2 2 2
 4 3 3 3 1 1 2 1 3 3 4 2 2 2]
(51,)

#문서의 유형은 크게 전자제품, 자동차, 호텔로 구성됨
#전자 제품 - 네비게이션, 아이팟, 킨들, 랩탑 컴퓨터 등과 같은 세부 요소로 나뉨
#5개의 군집(Centroid) 으로 K-Means군집화

[In] 군집화된 그룹별로 데이터 확인
document_df['cluster_label'] = cluster_label
document_df.head()
#cluster #0은 호텔에 대한 리뷰로 군집화
#cluster #1은 킨들, 아이팟, 넷북 등의 포터블 전자기기에 대한 리뷰로 군집화
#cluster #2은 킨들, 아이팟, 넷북이 군집에 포함되어 있지만, 주로 차량용 네비게이션으로 군집이 구성
#cluster #3은 킨들 리뷰가 한개 섞여 있지만 #0과 같이 대부분 호텔에 대한 리뷰로 군집화되어 있음
#cluster #4은 토요타(Toyota)와 혼다(Honda) 등의 자동차에 대한 리뷰로 잘 군집화 되어 있음

[Out]

 

4-2. 군집화 - try #2

[In] KMeans 군집화(3개)
#3개 집합으로 군집화 수행
km_cluster = KMeans(n_clusters=3, max_iter=10000, random_state=0)
km_cluster.fit(feature_vect)
cluster_label = km_cluster.labels_

#소속 클러스터를 cluster_label 칼럼으로 할당하고 cluster_label값으로 정렬
document_df['cluster_label'] = cluster_label
document_df.sort_values(by='cluster_label').head()

# Cluster #0은 포터블 전자기기 리뷰로만 군집화
# Cluster #1 자동차 리뷰로만 군집
# Cluster #2 호텔 리뷰로만 군집이 잘 구성됨을 알 수 있다.​

[Out]

 

5. 군집별 핵심단어 추출하기

각 군집을 구성하는 핵심 단어가 어떤 것인지 확인

  • KMeans객체의 cluster_centers_ 속성 : 개별 피처들의 클러스터 중심과의 상대 위치를 정규화된 숫자값으로 표시
  • 0~1까지의 값으로 표현되며 1에 가까울 수록 중심에 더 가깝다는 의미
[In]
cluster_centers = km_cluster.cluster_centers_
print('cluster_centers shape:', cluster_centers.shape)
print(cluster_centers)
#3개의 클러스터​

[Out]
cluster_centers shape: (3, 4611)
#군집이 3개, word 피처가 4611개로 구성됨

[[0.01005322 0.         0.         ... 0.00706287 0.         0.        ]
 [0.         0.00092551 0.         ... 0.         0.         0.        ]
 [0.         0.00099499 0.00174637 ... 0.         0.00183397 0.00144581]]​

[In] 군집별 top n 핵심단어, 그 단어의 중심 위치 상대값, 대상 파일명들을 반환하는 함수 생성
# 군집별 top n 핵심단어, 그 단어의 중심 위치 상대값, 대상 파일명들을 반환함. 
def get_cluster_details(cluster_model, cluster_data, feature_names, clusters_num, top_n_features=10):
    cluster_details = {}
    
    # cluster_centers array 의 값이 큰 순으로 정렬된 index 값을 반환
    centroid_feature_ordered_ind = cluster_model.cluster_centers_.argsort()[:,::-1]
    
    #개별 군집별로 iteration하면서 핵심단어, 그 단어의 중심 위치 상대값, 대상 파일명 입력
    for cluster_num in range(clusters_num):
        # 개별 군집별 정보를 담을 데이터 초기화. 
        cluster_details[cluster_num] = {}
        cluster_details[cluster_num]['cluster'] = cluster_num
        
        # cluster_centers_.argsort()[:,::-1] 로 구한 index 를 이용하여 top n 피처 단어를 구함. 
        top_feature_indexes = centroid_feature_ordered_ind[cluster_num, :top_n_features]
        top_features = [ feature_names[ind] for ind in top_feature_indexes ]
        
        # top_feature_indexes를 이용해 해당 피처 단어의 중심 위치 상댓값 구함 
        top_feature_values = cluster_model.cluster_centers_[cluster_num, top_feature_indexes].tolist()
        
        # cluster_details 딕셔너리 객체에 개별 군집별 핵심 단어와 중심위치 상대값, 그리고 해당 파일명 입력
        cluster_details[cluster_num]['top_features'] = top_features
        cluster_details[cluster_num]['top_features_value'] = top_feature_values
        filenames = cluster_data[cluster_data['cluster_label'] == cluster_num]['filename']
        filenames = filenames.values.tolist()
        cluster_details[cluster_num]['filenames'] = filenames
        
    return cluster_details​


[In] 군집별 top feature들의 단어와 파일명 출력

def print_cluster_details(cluster_details):
    for cluster_num, cluster_detail in cluster_details.items():
        print('####### Cluster {0}'.format(cluster_num))
        print('Top features:', cluster_detail['top_features'])
        print('Reviews 파일명 :',cluster_detail['filenames'][:7])
        print('==================================================')
        
feature_names = tfidf_vect.get_feature_names_out()
cluster_details = get_cluster_details(cluster_model=km_cluster, cluster_data=document_df,\
                                  feature_names=feature_names, clusters_num=3, top_n_features=10 )
print_cluster_details(cluster_details)

[Out]
####### Cluster 0
Top features: ['screen', 'battery', 'keyboard', 'battery life', 'life', 'kindle', 'direction', 'video', 'size', 'voice']
Reviews 파일명 : ['accuracy_garmin_nuvi_255W_gps', 'battery-life_amazon_kindle', 'battery-life_ipod_nano_8gb', 'battery-life_netbook_1005ha', 'buttons_amazon_kindle', 'directions_garmin_nuvi_255W_gps', 'display_garmin_nuvi_255W_gps']
==================================================
####### Cluster 1
Top features: ['interior', 'seat', 'mileage', 'comfortable', 'gas', 'gas mileage', 'transmission', 'car', 'performance', 'quality']
Reviews 파일명 : ['comfort_honda_accord_2008', 'comfort_toyota_camry_2007', 'gas_mileage_toyota_camry_2007', 'interior_honda_accord_2008', 'interior_toyota_camry_2007', 'mileage_honda_accord_2008', 'performance_honda_accord_2008']
==================================================
####### Cluster 2
Top features: ['room', 'hotel', 'service', 'staff', 'food', 'location', 'bathroom', 'clean', 'price', 'parking']
Reviews 파일명 : ['bathroom_bestwestern_hotel_sfo', 'food_holiday_inn_london', 'food_swissotel_chicago', 'free_bestwestern_hotel_sfo', 'location_bestwestern_hotel_sfo', 'location_holiday_inn_london', 'parking_bestwestern_hotel_sfo']
==================================================​