Unsupervised Learning - Clustering and PCA, Pokemon Dataset — Botnoi Classroom

Tamagotchi
5 min readOct 24, 2019

--

สวัสดีครับบ เมื่อวันจันทร์ที่ผ่านมา (21/10/2562) ผมได้มีโอกาสไปเรียนที่บริษัท Botnoi ซึ่งได้มีการเปิดสอนสำหรับคนที่สนใจด้าน Data Science ในวันนั้นเป็นการสอนในหัวข้อ ‘Unsupervised Learning -Clustering and PCA (overview)’ โดยรูปแบบการสอนจะเป็นการสอนทฤษฎี 1 ชั่วโมง และ Workshop 2 ชั่วโมงครับ

ซึ่งในบทความนี้ผมจะนำ Workshop ที่ได้ทำไปมาเล่าทุกคนให้ฟัง ฝากติดตามกันด้วยนะครับบบ

Clustering หรือการจัดกลุ่มข้อมูล เป็นวิธีนึงใน Machine Learning ประเภท Unsupervised Learning ซึ่ง clustering เป็นวิธีที่ machine จะหาความสัมพันธ์ของข้อมูล เพื่อแบ่งข้อมูลออกเป็นกลุ่มๆ ตัวอย่างการนำไปใช้งาน เช่น การแบ่งกลุ่มลูกค้า (Customer Segmentation) ตามพฤติกรรมการซื้อสินค้า

เริ่มจาก Import Library ที่จำเป็น น่าจะคุ้นเคยกันอยู่แล้วสำหรับ numpy และ pandas
ส่วน plot_3d_chart เป็น library สำหรับการ plot กราฟ 3 มิติ ครับ

import numpy as np
import pandas as pd
from plot_3d_chart import *

Dataset ที่เราจะใช้กันในคลาสคือ Pokemon ครับ ซึ่ง Dataset นี้จะประกอบไปด้วย ข้อมูลของ Pokemon แต่ละตัว มาดูกันดีกว่าว่า Dataset ของเรามีหน้าตาเป็นอย่างไร

df = pd.read_csv('pokemon.csv')
df.head()

เราจะใช้ ‘._get_numeric_data()’ ซึ่งจะทำให้ Data ของเราเหลือแค่ข้อมูลที่เป็นตัวเลขเท่านั้น เนื่องจาก Machine Learning Model ต้องการเฉพาะ Input ที่เป็นตัวเลข

ถ้าเราต้องการจะใช้ Feature อื่นๆ ที่ไม่เป็นตัวเลข เช่น ‘Type 1’ เราก็ต้องทำการแปลงเป็นตัวเลขก่อนโดยใช้ LabelEncoder หรือ Getdummie ตามความเหมาะสมของข้อมูลนั้นๆ แต่ในวันนี้ผมขอพูดถึงภาพรวมของการทำ Clustering เป็นหลักก่อนนะครับ

Column ‘#’ เป็น Column บอก Index ของโปเกม่อนตัวนั้นๆ ซึ่งแน่นอนว่าเราก็จะไม่นำมาเป็น Feature ในการทำ Clustering จึงดรอปทิ้งไปเช่นกัน

feat = df._get_numeric_data()
feat = feat.drop('#', axis=1)
feat.head()

แปลง Column ‘Legendary’ เป็น type int ซึ่งจริงๆขั้นตอนนี้ไม่จำเป็นต้องทำก็ได้ เพราะเมื่อเรานำ Column นี้ไปคำนวณ มันก็จะมองว่าเป็นตัวเลขอยู่แล้ว (True=1,False=0) แต่เพื่อความไม่งงก็แปลงซะหน่อบครับ 555

feat['Legendary'] = feat['Legendary'].astype(int)
feat.head()

เนื่องจากข้อมูลในแต่ละ Column ที่เรามี เป็นข้อมูลที่มี Scale ค่อยข้างใกล้เคียงกัน ซึ่งเป็นสิ่งที่ดี แต่เราจะทำการปรับ Column ‘HP’ ให้มี Scale ต่างจาก Column อื่นๆ โดยการคูณด้วย 1,000
เพราะเดี๋ยวผมจะพูดถึงเรื่องการทำ Scaling (การแปลงข้อมูลแต่ละ column ให้อยู่ใน scale เดียวกัน) ว่ามันมีประโยชน์ยังไง

feat['HP'] = feat['HP'] * 1000
feat.head()

ตอนนี้ Feature ของเราก็พร้อมแล้วสำหรับการทำ Clustering ซึ่ง Algorithm ที่เราใช้วันนี้ก็คือ K-Means Clustering ครับ

การใช้ K-Means เราต้องกำหนดว่าเราต้องการจะ Cluster ข้อมูลเป็นกี่กลุ่ม หลังจากนั้น Algorithm จะสุ่มวางจุดศูนย์กลางของการจัดกลุ่ม (Centroid) ตามจำนวนที่เรากำหนดลงบนพื้นที่ของข้อมูล โดยจะมีการคำนวณระยะห่างระหว่างจุดข้อมูล (Data point) ไปยังจุดศูนย์กลาง (Centroid) ข้อมูลไหนอยู่ใกล้จุดศูนย์กลางไหน ก็จะเข้าไปอยู่ในกลุ่มนั้น และจะคำนวณระยะทางรวมทั้งหมดระหว่างจุดศูนย์กลางกับจุดข้อมูลของแต่ละกลุ่มมาเก็บไว้ หลังจากนั้นจะเปลี่ยนตำแหน่งจุดศูนย์กลาง ทำการจัดกลุ่มและคำนวณหาระยะทางรวมใหม่ จนกระทั่งได้ระยะทางรวมที่น้อยที่สุด

และเนื่องจากเป็นการสุ่มวางจุดศูนย์กลาง (Centroid) จึงเป็นไปได้ว่าในแต่ละครั้งของการ Run Clustering Model จะได้ผลลัพธ์หรือการจัดกลุ่มของข้อมูลต่างจากเดิม ปล.จุดข้อมูล(Data Point)ในที่นี้ก็หมายถึง Pokemon แต่ละตัวใน Dataset ของเรานั่นแหละครับ

http://www.learnbymarketing.com/methods/k-means-clustering/?fbclid=IwAR1XKhxk8WcXXjqbO0MZKe-WFLxako8fc2pY3B0OfHmzzujjuPZGPkliDB4

มาต่อกันครับ.. เราจะลองแบ่ง Pokemon ใน Dataset ของเราให้เป็น 6 กลุ่มกันครับ (n_clusters = 6)
อย่างที่บอกไปว่า K-Means จะทำการสุ่ม Centroid จุดแรก ซึ่งทำให้การ Run Model แต่ละครั้งได้ผลลัพธ์ในการจัดกลุ่มอาจไม่เหมือนเดิม
ซึ่งการกำหนดค่า random_state ทำให้การ run model ครั้งถัดไปโดยใช้ random_state ค่าเดิมได้ผลลัพธ์เหมือนเดิม

from sklearn.cluster import KMeans
km = KMeans(n_clusters=6, random_state=1)

นำข้อมูลของเรามา fit เข้ากับ model และทำการแบ่งกลุ่ม โดยผลลัพธ์ที่ได้ จะเป็น array ของตัวเลขที่ระบุกลุ่มให้กับโปเกม่อนแต่ละตัวตามลำดับ ซึ่งจะประกอบไปด้วยเลข 0 ถึง 5 (6 กลุ่ม ตามที่เรากำหนด)

km.fit(feat)
cluster = km.predict(feat)

นำ array นี้ไปใส่ในชุดข้อมูลของเราโดยตั้งชื่อใหม่ว่า ‘output’ ซึ่งจะเห็นได้ว่ามี column ใหม่เพิ่มเข้ามาคือ ‘cluster’ ซึ่งเป็นการระบุว่าโปเกม่อนตัวนั้นอยู่ในกลุ่มไหน

output = feat.copy()
output['cluster'] = cluster
output.head()

มาลองเช็คดูหน่อยว่า โปเกม่อนแต่ละกลุ่ม มีค่าเฉลี่ยของแต่ละ Feature เป็นเท่าไหร่

output.groupby(by='cluster').mean()

ลองนำมา plot ลงบน กราฟ 3 มิติ โดยใช้ Feature HP, Attack, Defense มาเป็นแกน x, y, z กันครับ เนื่องจากมี 3 แกนเราจึงเลือกมาได้แค่ 3 Feature
ถามว่าถ้าเราต้องการ plot กราฟ 3 มิติ โดยใช้ Feature ทั้งหมดเลยได้ไหม? ทำได้ครับ เดี๋ยวผมจะพูดถึงในเรื่องของการทำ PCA แต่ตอนนี้เรามาลอง plot โดยใช้แค่ 3 Feature กันก่อนดีกว่า

plot_3d(df=output, labelColumn='cluster', \
axisList=['HP', 'Attack','Defense'], \
clusterInterestList = [0,1,2,3,4,5])

นี่คือกราฟ 3 มิติที่เราได้จากการ plot โดยใช้ Feature 3 อย่างมาเป็นแกน คือ HP,Defense,Attack ครับบ

ซึ่งถ้าเราลองหมุนกราฟ 3มิติ ของเราและสังเกตุดู เราจะเห็นการแบ่งกลุ่มได้ชัดเจนมากในแกนของ ‘HP’ เนื่องจากว่า Feature ‘HP’ ของเรามี Scale ที่ใหญ่กว่า Feature อื่นๆ การแบ่งกลุ่มจึงให้น้ำหนักไปทาง Feature ‘HP’ มากกว่า
แล้วทำอย่างไรให้ model ของเราให้น้ำหนักแต่ละ Feature ในการแบ่งกลุ่มเท่าๆกัน
เราจะใช้วิธีที่เรียกว่า Feature Scaling หรือการทำให้ข้อมูลอยู่ใน Scale เดียวกัน

Feature Scaling ทำได้หลายวิธี แต่วันนี้เราจะใช้วิธี Standardization (Z-score normalization) ซึ่งจะทำให้ Variance และ Mean ของแต่ละ Feature มีค่าเป็น 1 และ 0 (หรือใกล้เคียง) ตามลำดับ ซึ่งจะช่วยให้การหาระยะเพื่อจัดกลุ่ม ของ Model ไม่ผิดเพี้ยน

ก่อนทำ Feature Scaling

Standardization (Z-score normalization)

feat_scale = (feat - feat.mean()) / feat.std()
feat_scale.head()
หลังทำ Feature Scaling (โดยนำแต่ละค่าใน column ลบด้วย Mean แล้วหารด้วย SD ของ column นั้น)

ซึ่งวิธีนี้จะทำให้ Mean และ SD ของข้อมูลแต่ละ Feature เป็น 0 และ 1 ตามลำดับ

ต่อไปเรามาลองทำ Clustering กันใหม่ โดยใช้ Data ที่ทำ Feature Scaling แล้ว
กำหนด n_cluster เป็น 6 เพื่อแบ่งเป็น 6 กลุ่ม และ random_state เป็น 1 เหมือนเดิม

km = KMeans(n_clusters=6, random_state=1)
km.fit(feat_scale)
cluster2 = km.predict(feat_scale)

สังเกตุดูใน Column ‘cluster’ จะเห็นว่า มีการจัดกลุ่มต่างจากเดิม (ก่อนทำ Feature scaling)

output2 = feat_scale.copy()
output2['cluster'] = cluster2
output2.head()

ลองมา plot กราฟ 3 มิติกันดูใหม่ โดยใช้ Feature HP,Attack,Defense มาเป็นแกนเหมือนเดิม

plot_3d(df=output2, labelColumn='cluster', \
axisList=['HP', 'Attack','Defense'], \
clusterInterestList = [0,1,2,3,4,5])

ผลลัพธ์ที่ได้จากการ plot กราฟ 3 มิติจะเห็นได้ว่า ไม่ได้แบ่งกลุ่มโดย Feature ใด Feature หนึ่งอย่างชัดเจน เหมือนก่อนทำ Feature Scaling

กราฟอาจจะดูไม่ค่อยสวยงามเท่าไหร่ แต่ อย่าลืมนะครับว่า เรา plot กราฟโดยใช้ Feature แค่ 3 ตัว คือ ‘HP’, ’Attack’ และ ‘Defense’ เพื่อนำมาเป็นแกน

ซึ่งจริงๆแล้วเราใช้ Feature ถึง 8 ตัว ในการทำ Clustering คือ HP, Attack, Defense, Sp. Atk, Sp. Def, Speed, Generation, และ Legendary

ซึ่งสิ่งที่เราต้องทำ เพื่อที่จะ Visualize การ Clustring ของเราที่ใช้ถึง 8 Feature หรือ 8 Dimensions คือการทำ PCA ครับ

Principal Component Analysis (PCA) เป็นวิธีหนึ่งในการทำ Dimensionality Reduction โดนหลักการก็คือ การนำ Feature เดิมทั้งหมดมาสร้างเป็นตัวแปรใหม่ขึ้นมา เรียกตัวแปรใหม่นี้ว่า Component
ซึ่งเป็นวิธีที่จะลด Dimension ลงโดยรักษา Information ไว้ให้เยอะที่สุด ซึ่งประโยชน์ของมัน นอกเหนือจากการ Visualization แล้ว ยังทำให้ Machine Learning Algorithm ทำงานได้เร็วขึ้นอีกด้วยครับ ซึ่งเป็นวิธีที่สำคัญมาก โดยเฉพาะกับ Data ที่มี Feature เยอะๆ

ในที่นี้เราต้องการจะแปลง 8 Feature ของเราให้เหลือแค่ 3 components เพื่อที่เราจะนำมา plot กราฟ 3 มิติ ได้ เราก็ต้องกำหนด n_components=3

from sklearn.decomposition import PCA
pca = PCA(n_components=3, random_state=1)
pca.fit(feat_scale)
feat_pca = pca.transform(feat_scale)

ซึ่ง ‘feat_pca’ ที่ได้ จะประกอบไปด้วย array ของ component ทั้ง 3 ตัว นำarray นี้ไปรวมเข้ากับ Dataframe ล่าสุดของเรา โดยตั้งชื่อ Column ให้เป็น comp1,comp2,comp3

output2['comp1'] = feat_pca[:,0]
output2['comp2'] = feat_pca[:,1]
output2['comp3'] = feat_pca[:,2]
output2.head()

ในกรอบสีฟ้า คือ component ที่เราได้หลังจากการทำ PCA ซึ่งแต่ละ Components ก็เกิดจาก Feature ทั้ง 8 ตัว ในกรอบสีแดง

คราวนี้เรามาลอง plot กราฟ 3 มิติกันดูอีกที โดนนำ comp1,comp2,comp3 มาเป็นแกน

plot_3d(df=output2, labelColumn='cluster', \
axisList=['comp1', 'comp2','comp3'], \
clusterInterestList = [0, 1, 2, 3, 4, 5])

ซึ่งเราจะเห็นการแบ่งกลุ่มได้ค่อยข้างชัดเจนเลยทีเดียว และนี่ก็คือประโยชน์นึงของการทำ PCA ครับ แต่ก็อย่างที่บอกไปว่า PCA เป็นการวิธีการลด Dimension ของข้อมูล โดยจะรักษา Information ไว้ให้เยอะที่สุด ซึ่งหมายความว่า การทำ PCA มีโอกาสที่ Information บางส่วนจะถูกตัดออกครับ

เป็นยังไงกันบ้างครับ สำหรับการทำ Clustering เพื่อแบ่งกลุ่ม Pokemon ขอบคุณที่ติดตามอ่านกันจนจบนะครับ สำหรับบทความนี้ถ้าผิดพลาดประการใดต้องขออภัย และขอคำแนะนำจากทุกๆคนด้วยนะครับ ยังไงก็ Commend ติชมกันมาได้เลยนะครับ ไว้มีโอกาสจะมาเล่าให้ฟังอีก ขอบคุณครับบบ

--

--