Artificial Intelligence/[파이썬 머신러닝 완벽가이드]
[파이썬 머신러닝 완벽가이드] Chap08-7. 문서 군집화 소개와 실습(Opinion Review 데이터 세트)
aiclaudev
2022. 1. 26. 18:39
In [1]:
# 본 글은 파이썬 머신러닝 완벽 가이드 (권철민 지음)을 공부하며 내용을 추가한 정리글입니다.
# 개인 공부를 위해 만들었으며, 만약 문제가 발생할 시 글을 비공개로 전환함을 알립니다.
In [2]:
# 문서 군집화(Document Clustering)은 비슷한 텍스트 구성의 문서를 군집화 하는 것
# 텍스트 분류 기반의 문서 분류는 결정 카테고리 값을 가진 학습 데이터가 필요
# 그러나, 문서 군집화는 학습 데이터가 필요 없는 비지도학습 기반으로 동작
In [3]:
# 사용할 데이터셋
# https://archive.ics.uci.edu/ml/datasets/Opinosis+Opinion+%26frasl%3B+Review
# 51개의 텍스트 파일로 구성, 각 파일은 Tripadvisor, Edmunds.com, Amazon.com 사이트의 리뷰
# 크게 전자제품, 자동차, 호텔에 대한 리뷰
In [4]:
import pandas as pd
import glob, os
# 디렉토리 설정
path = r'C:\Users\82102\Desktop\sw\PythonMLGuide\OpinosisDataset1.0\OpinosisDataset1.0\topics'
# path로 지정한 디렉토리 밑에 모든 .data 파일의 파일명을 리스트로 취합
all_files = glob.glob(os.path.join(path, "*.data"))
filename_list = []
opinion_text = []
# 개별 파일의 파일명은 filename_list로 취합
# 개별 파일의 파일 내용은 DataFrame 로딩 후 다시 string으로 변환해 opinion_text list로 취합
for file_ in all_files :
# 개별 파일을 읽어서 DataFrame으로 생성
df = pd.read_table(file_, index_col = None, header = 0, encoding = 'latin1')
# 절대 경로로 주어진 파일명을 가공, 맨 마지막 .data 확장자도 제거
filename_ = file_.split('\\')[-1]
filename = filename_.split('.')[0]
# 파일명 list와 파일 내용 list에 파일명과 파일 내용을 추가
filename_list.append(filename)
opinion_text.append(df.to_string())
# 파일명 list와 파일 내용 list 객체를 DataFrame으로 생성
document_df = pd.DataFrame({'filename' : filename_list, 'opinion_text' : opinion_text})
document_df.head()
Out[4]:
filename | opinion_text | |
---|---|---|
0 | accuracy_garmin_nuvi_255W_gps | ... |
1 | bathroom_bestwestern_hotel_sfo | ... |
2 | battery-life_amazon_kindle | ... |
3 | battery-life_ipod_nano_8gb | ... |
4 | battery-life_netbook_1005ha | ... |
In [6]:
# TF-IDF 피처 벡터화 수행하기 전, tokenizer 함수를 정의
from nltk.stem import WordNetLemmatizer
import nltk
import string
# ord는 문자의 유니코드 값을 반환, string.punctuation은 구두점
remove_punct_dict = dict((ord(punct), None) for punct in string.punctuation)
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)))
In [7]:
# TF-IDf 피처 벡터화 수행
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vect = TfidfVectorizer(tokenizer = LemNormalize, stop_words = 'english',
ngram_range = (1, 2), min_df = 0.05, max_df = 0.85)
# opinion_text 컬럼 값으로 피처 벡터화 수행
feature_vect = tfidf_vect.fit_transform(document_df['opinion_text'])
C:\Users\82102\anaconda3\lib\site-packages\sklearn\feature_extraction\text.py:388: UserWarning: Your stop_words may be inconsistent with your preprocessing. Tokenizing the stop words generated tokens ['ha', 'le', 'u', 'wa'] not in stop_words. warnings.warn('Your stop_words may be inconsistent with '
In [9]:
# K-평균 군집화 적용
from sklearn.cluster import KMeans
# 5개 집합으로 군집화 수행
km_cluster = KMeans(n_clusters = 5, max_iter = 10000, random_state = 0)
km_cluster.fit(feature_vect)
cluster_label = km_cluster.labels_
cluster_centers = km_cluster.cluster_centers_
In [11]:
document_df['cluster_label'] = cluster_label
document_df.head()
Out[11]:
filename | opinion_text | cluster_label | |
---|---|---|---|
0 | accuracy_garmin_nuvi_255W_gps | ... | 2 |
1 | bathroom_bestwestern_hotel_sfo | ... | 0 |
2 | battery-life_amazon_kindle | ... | 1 |
3 | battery-life_ipod_nano_8gb | ... | 1 |
4 | battery-life_netbook_1005ha | ... | 1 |
In [12]:
# cluster_label이 0인 데이터들을 정렬
document_df[document_df['cluster_label'] == 0].sort_values(by = 'filename')
Out[12]:
filename | opinion_text | cluster_label | |
---|---|---|---|
1 | bathroom_bestwestern_hotel_sfo | ... | 0 |
32 | room_holiday_inn_london | ... | 0 |
30 | rooms_bestwestern_hotel_sfo | ... | 0 |
31 | rooms_swissotel_chicago | ... | 0 |
In [13]:
# Cluster #0은 호텔에 대한 리뷰로 군집화 되어 있음을 알 수 있다.
In [15]:
# cluster_labe이 1인 데이터들을 정렬
document_df[document_df['cluster_label'] == 1].sort_values(by = 'filename')
Out[15]:
filename | opinion_text | cluster_label | |
---|---|---|---|
2 | battery-life_amazon_kindle | ... | 1 |
3 | battery-life_ipod_nano_8gb | ... | 1 |
4 | battery-life_netbook_1005ha | ... | 1 |
19 | keyboard_netbook_1005ha | ... | 1 |
26 | performance_netbook_1005ha | ... | 1 |
41 | size_asus_netbook_1005ha | ... | 1 |
42 | sound_ipod_nano_8gb | headphone jack i got a clear case for it a... | 1 |
44 | speed_windows7 | ... | 1 |
In [16]:
# Cluster #1은 킨들, 아이팟, 넷북 등의 포터블 전자기기에 대한 리뷰로 군집화 되어 있음
In [17]:
# cluster_label이 2인 데이터들을 정렬
document_df[document_df['cluster_label'] == 2].sort_values(by = 'filename')
Out[17]:
filename | opinion_text | cluster_label | |
---|---|---|---|
0 | accuracy_garmin_nuvi_255W_gps | ... | 2 |
5 | buttons_amazon_kindle | ... | 2 |
8 | directions_garmin_nuvi_255W_gps | ... | 2 |
9 | display_garmin_nuvi_255W_gps | ... | 2 |
10 | eyesight-issues_amazon_kindle | ... | 2 |
11 | features_windows7 | ... | 2 |
12 | fonts_amazon_kindle | ... | 2 |
23 | navigation_amazon_kindle | ... | 2 |
33 | satellite_garmin_nuvi_255W_gps | ... | 2 |
34 | screen_garmin_nuvi_255W_gps | ... | 2 |
35 | screen_ipod_nano_8gb | ... | 2 |
36 | screen_netbook_1005ha | ... | 2 |
43 | speed_garmin_nuvi_255W_gps | ... | 2 |
48 | updates_garmin_nuvi_255W_gps | ... | 2 |
49 | video_ipod_nano_8gb | ... | 2 |
50 | voice_garmin_nuvi_255W_gps | ... | 2 |
In [18]:
# Cluster #2는 킨들, 아이팟, 넷북이 군집에 포함되어 있지만 주로 차량용 네비게이션 리뷰
In [19]:
# cluster_label이 3인 데이터들을 정렬
document_df[document_df['cluster_label'] == 3].sort_values(by = 'filename')
Out[19]:
filename | opinion_text | cluster_label | |
---|---|---|---|
13 | food_holiday_inn_london | ... | 3 |
14 | food_swissotel_chicago | ... | 3 |
15 | free_bestwestern_hotel_sfo | ... | 3 |
20 | location_bestwestern_hotel_sfo | ... | 3 |
21 | location_holiday_inn_london | ... | 3 |
24 | parking_bestwestern_hotel_sfo | ... | 3 |
27 | price_amazon_kindle | ... | 3 |
28 | price_holiday_inn_london | ... | 3 |
38 | service_bestwestern_hotel_sfo | ... | 3 |
39 | service_holiday_inn_london | ... | 3 |
40 | service_swissotel_hotel_chicago | ... | 3 |
45 | staff_bestwestern_hotel_sfo | ... | 3 |
46 | staff_swissotel_chicago | ... | 3 |
In [20]:
# Cluster #3은 킨들 리뷰가 하나 섞여있긴 하지만, 대부분 호텔에 대한 리뷰로 군집화
In [21]:
# cluster_label이 4인 데이터들을 정렬
document_df[document_df['cluster_label'] == 4].sort_values(by = 'filename')
Out[21]:
filename | opinion_text | cluster_label | |
---|---|---|---|
6 | comfort_honda_accord_2008 | ... | 4 |
7 | comfort_toyota_camry_2007 | ... | 4 |
16 | gas_mileage_toyota_camry_2007 | ... | 4 |
17 | interior_honda_accord_2008 | ... | 4 |
18 | interior_toyota_camry_2007 | ... | 4 |
22 | mileage_honda_accord_2008 | ... | 4 |
25 | performance_honda_accord_2008 | ... | 4 |
29 | quality_toyota_camry_2007 | ... | 4 |
37 | seats_honda_accord_2008 | ... | 4 |
47 | transmission_toyota_camry_2007 | ... | 4 |
In [22]:
# Cluster #4는 토요타와 혼다 등의 자동차에 대한 리뷰로 군집화
In [23]:
# 위 경우, 군집 개수가 많게 설정되어 있어 세분화되어 군집화된 경향이 있음
# 중심개수를 5개에서 3개로 낮추어보자.
In [25]:
from sklearn.cluster import KMeans
# 3개의 집합으로 군집화
km_cluster = KMeans(n_clusters = 3, max_iter = 10000, random_state = 0)
km_cluster.fit(feature_vect)
cluster_label = km_cluster.labels_
cluster_centers = km_cluster.cluster_centers_
# 소속 군집을 cluster_label 컬럼으로 할당
document_df['cluster_label'] = cluster_label
In [26]:
# Cluster 0
document_df[document_df['cluster_label'] == 0].sort_values(by = 'filename')
Out[26]:
filename | opinion_text | cluster_label | |
---|---|---|---|
0 | accuracy_garmin_nuvi_255W_gps | ... | 0 |
2 | battery-life_amazon_kindle | ... | 0 |
3 | battery-life_ipod_nano_8gb | ... | 0 |
4 | battery-life_netbook_1005ha | ... | 0 |
5 | buttons_amazon_kindle | ... | 0 |
8 | directions_garmin_nuvi_255W_gps | ... | 0 |
9 | display_garmin_nuvi_255W_gps | ... | 0 |
10 | eyesight-issues_amazon_kindle | ... | 0 |
11 | features_windows7 | ... | 0 |
12 | fonts_amazon_kindle | ... | 0 |
19 | keyboard_netbook_1005ha | ... | 0 |
23 | navigation_amazon_kindle | ... | 0 |
26 | performance_netbook_1005ha | ... | 0 |
27 | price_amazon_kindle | ... | 0 |
33 | satellite_garmin_nuvi_255W_gps | ... | 0 |
34 | screen_garmin_nuvi_255W_gps | ... | 0 |
35 | screen_ipod_nano_8gb | ... | 0 |
36 | screen_netbook_1005ha | ... | 0 |
41 | size_asus_netbook_1005ha | ... | 0 |
42 | sound_ipod_nano_8gb | headphone jack i got a clear case for it a... | 0 |
43 | speed_garmin_nuvi_255W_gps | ... | 0 |
44 | speed_windows7 | ... | 0 |
48 | updates_garmin_nuvi_255W_gps | ... | 0 |
49 | video_ipod_nano_8gb | ... | 0 |
50 | voice_garmin_nuvi_255W_gps | ... | 0 |
In [27]:
# Cluster #0은 포터블 전자기기에 대한 리뷰로 잘 군집화 되어있음
In [28]:
# Cluster 1
document_df[document_df['cluster_label'] == 1].sort_values(by = 'filename')
Out[28]:
filename | opinion_text | cluster_label | |
---|---|---|---|
6 | comfort_honda_accord_2008 | ... | 1 |
7 | comfort_toyota_camry_2007 | ... | 1 |
16 | gas_mileage_toyota_camry_2007 | ... | 1 |
17 | interior_honda_accord_2008 | ... | 1 |
18 | interior_toyota_camry_2007 | ... | 1 |
22 | mileage_honda_accord_2008 | ... | 1 |
25 | performance_honda_accord_2008 | ... | 1 |
29 | quality_toyota_camry_2007 | ... | 1 |
37 | seats_honda_accord_2008 | ... | 1 |
47 | transmission_toyota_camry_2007 | ... | 1 |
In [29]:
# Cluster #1은 차에 대한 리뷰로 잘 군집화 되어있음
In [30]:
# Cluster 2
document_df[document_df['cluster_label'] == 2].sort_values(by = 'filename')
Out[30]:
filename | opinion_text | cluster_label | |
---|---|---|---|
1 | bathroom_bestwestern_hotel_sfo | ... | 2 |
13 | food_holiday_inn_london | ... | 2 |
14 | food_swissotel_chicago | ... | 2 |
15 | free_bestwestern_hotel_sfo | ... | 2 |
20 | location_bestwestern_hotel_sfo | ... | 2 |
21 | location_holiday_inn_london | ... | 2 |
24 | parking_bestwestern_hotel_sfo | ... | 2 |
28 | price_holiday_inn_london | ... | 2 |
32 | room_holiday_inn_london | ... | 2 |
30 | rooms_bestwestern_hotel_sfo | ... | 2 |
31 | rooms_swissotel_chicago | ... | 2 |
38 | service_bestwestern_hotel_sfo | ... | 2 |
39 | service_holiday_inn_london | ... | 2 |
40 | service_swissotel_hotel_chicago | ... | 2 |
45 | staff_bestwestern_hotel_sfo | ... | 2 |
46 | staff_swissotel_chicago | ... | 2 |
In [31]:
# Cluster #2는 호텔에 대한 리뷰로 잘 군집화 되어있음
In [ ]:
In [32]:
# 군집별 핵심 단어 추출하기
In [33]:
# 각 군집(Cluster)에 속한 문서는 핵심 단어를 주축으로 군집화돼 있을 것
# KMeans는 각 군집을 구성하는 단어 피처가 군집의 중심(Centroid)을 기준으로
# 얼마나 가깝게 위치해 있는지 cluster_centers_라는 속성으로 제공 (ndarry 형태)
# cluster_centers_는 배열 값으로 제공되며 행은 개별 군집, 열은 개별 피처
# cluster_centers_[0, 1]은 0번 군집에서 두 번째 피처의 위치 값
In [38]:
cluster_centers = km_cluster.cluster_centers_
print('cluster_centers shape : ', cluster_centers.shape)
print(cluster_centers)
cluster_centers shape : (3, 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 [39]:
# cluster_centers_는 (3, 2409) 배열임. 군집이 3개, word 피처가 2409개로 구성되었음을 의미
# 0에서 1까지의 값을 가지며, 1에 가까울수록 중심과 가까운 값을 의미
In [40]:
# 1) cluster_centers_ 배열 내에서 가장 값이 큰 데이터의 위치 인덱스를 추출
# 2) 해당 인덱스를 이용해, 핵심 단어 이름과 그때의 상대 위치 추출
# 3) cluster_details라는 Dict 객체 변수에 기록하고 반환
# 위와 같은 동작을 수행하는 함수 get_cluster_details()의 구현
# 군집별 top n 핵심 단어, 그 단어의 중심 위치 상댓값, 대상 파일명을 반환함.
def get_cluster_details(cluster_model, cluster_data, feature_names, clusters_num, top_n_features = 10) :
cluster_details = {}
# cluster_centers array의 값이 큰 순으로 정렬된 인덱스 값을 반환
# 군집 중심점(centroid)별 할당된 word 피처들의 거리값이 큰 순으로 값을 구하기 위함.
centroid_feature_ordered_ind = cluster_model.cluster_centers_.argsort()[:, ::-1]
# 개별 군집별로 반복하면서 핵심 단어, 그 단어의 중심 위치 상댓값, 대상 파일명 입력
for cluster_num in range(clusters_num) :
# 개별 군집별 정보를 담을 데이터 초기화.
cluster_details[cluster_num] = {}
cluster_details[cluster_num]['cluster'] = cluster_num
# cluster_centers_.argsort()[:,::-1]로 구한 인덱스를 이용해 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 [41]:
# 보기좋게 print 하기 위한 함수 구현
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('==============================================')
In [43]:
# 함수 사용
feature_names = tfidf_vect.get_feature_names()
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)
###### 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'] ==============================================
In [44]:
# 본 글은 파이썬 머신러닝 완벽 가이드 (권철민 지음)을 공부하며 내용을 추가한 정리글입니다.
# 개인 공부를 위해 만들었으며, 만약 문제가 발생할 시 글을 비공개로 전환함을 알립니다.