ลดมิติข้อมูลเพื่อทำ Recommender System By Unsupervised Learning
ทำไม netflix หรือ youtube ถึงได้แนะนำวิดีโอแนวนี้มาให้เรา ?
ห่างหายไปนานกับบทความ Machine Learning เพราะว่าช่วงนี้แอบหนีไปเล่น apex ไม่ยอมเขียนบทความสักที 5555 และช่วงก่อนสงกรานต์ได้เรียนคอร์สที่ชื่อว่า
Unsupervised Learning in Python
ที่จริงก่อนหน้านี้ก็ได้เรียนในคอร์สของ edX มาแล้วแต่ก็ยังไม่เห็นประโยชน์ รู้แค่ว่ามันจัดกลุ่มได้ ลดมิติได้ จนกระทั่งได้มาเรียนคอร์สนี้ถึงจะได้เอามาใช้จริงๆ เลยถึงจะเข้าใจส่วนวันนี้จะใช้งานแค่การลดมิติ (dimension reduction) ส่วนการจัดกลุ่มแบบ Unsupervised ขอเป็นบทความหน้าแล้วกัน
ทฤษฏีการทำระบบ Recommender System
ปกติเวลาเราเข้าไปดู youtube เราจะเจอวิดีโอที่แนะนำโดยถ้าสังเกตุดีๆมันจะอ้างอิงกับสิ่งที่เราดูในช่วงนี้หรือค้นหาในช่วงนี้
หรือแม้แต่ netflix จะอ้างอิงกับเรื่องที่เราพึ่งดูจบไป ระบบจะหาหนังที่คล้ายๆกันมาให้เราดูต่อได้เลย
โดยหลักๆระบบ Recommend จะมีอยู่ 2 แบบที่เราเห็นกันเยอะๆคือ
- อ้างอิงจากสิ่งที่เราชอบ/กำลังติดตาม/ค้นหา (Content based)
- อ้างอิงจากคนอื่นเช่น เราอ่านบทความนี้เสร็จก็จะเจอหัวข้อประมาณว่า “คนอื่นได้อ่านข่าวนี้ต่อ” (Collaborative filtering)
โดยระบบ Recommend จะใช้สิ่งที่ตรงกับผู้ใช้ได้อ่าน/ดูไปมาทำการ match กับสิ่งที่คล้ายๆกันเช่น เราสมมุติหนังเรื่อง Friend Zone กับ I Fine Thank You Love You
หนังสองเรื่องนี้มีส่วนที่คล้ายกันอยู่มาก โดยเรายกตัวอย่างมาให้เห็นหลักๆเลยคือความรัก หนังไทย และตลก แล้วมาเปรียบเทียบกับอีกเรื่องคือ Game of throne กับ Friend Zone
เราไม่ต้องทำรูปเปรียบเทียบเลยด้วยซ้ำ เพราะสองเรื่องนี้ไม่คล้ายกันเลยเพราะฉนั้นมันจะไม่ขึ้นใน recommend ของเราแน่นอน
ทำไมต้องลดมิติข้อมูล
หาใครได้ลองทำงานด้าน NLP โดยใช้ CountVectorizer หรือ tf-idf ถ้าเรามีข้อมูลขนาดใหญ่หรือจำนวนที่เยอะจะพบว่าข้อมูลของเรามันจะมีขนาดใหญ่มาก ! และยิ่งเรามีจำนวนข้อมูลเยอะ มันจะยิ่งใหญ่ขึ้นไปอีก ถ้าเราสามารถลดมิติของข้อมูลจากเดิมอาจจะมีถึง 30,000 แต่สามารถลดเหลือ 50 ได้ละ ? โดยที่ข้อมูลไม่เสียหายและได้ผลลัพธ์ที่ดีกว่าเดิมละ ? แค่คิดมันก็สุดยอดแล้ว
โดยบทความนี้ไม่ได้จะใช้ได้แค่กับการทำ recommender system การลดมิติข้อมูลสามรถใช้เลือก feature ที่ “มีผล” จริงๆกับ model อีกด้วย เราไม่ต้องเลือก feature เองก็ได้ ให้ Unsupervised (PCA,NMF) เลือกให้เราแทนได้เลย
แล้วบทความนี้จะทำ…. ?
วันนี้เราจะทำ Recommender System ที่เกี่ยวกับ “หัวข้อข่าว” ซึ่งเราจะจำลองว่าเราทำเว็บอ่านข่าวและต้องการจะแสดงข่าวที่ผู้อ่านชื่นชอบและเกี่ยวข้องกับที่ผู้อ่านสนใจนั้นเอง โดยเราจะใช้ dataset ที่เป็นภาษาอังกฤษทั้งหมด (ภาษาไทยก็ใช้ได้เหมือนๆกัน)
เริ่ม !
ทำการโหลด dataset มาจาก kaggle ก่อน
โดยรอบนี้เราจะทำแค่ 50,000 หัวข้อ จากทั้งหมด 143,000 ถ้าใครอยากจะทำทั้งหมดก็เอาไฟล์มาต่อกันได้เลย
ทีนี้เราเปิดไฟล์ของเราก่อนแล้วลองสังเกตุ dataset ดูก่อน
import pandas as pd
file = pd.read_csv(‘articles1.csv’)
file.head()
โดยเราจะใช้แค่ title ของ dataset นี้โดยเราจะเอาหัวข้อข่าวมาเชื่อมโยงหากันนั้นเอง
จากนั้นให้เราสร้าง bag-of-word ขึ้นมาสองอัน อันแรกสร้างด้วย CountVectorizer ส่วนอีกอันสร้างด้วย tf-idf เพื่อเปรียบเทียบผลลัพธ์ของทั้ง 2 อันนี้
ตรงนี้ถ้าใครงงว่า bag-of-word / CV / tf-idf คืออะไรให้ย้อนกลับไปอ่านบทความ NLP ที่เคยเขียนอธิบายเอาไว้แล้วก่อน จะได้เข้าใจมากยิ่งขึ้น
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer()
vectorizer.fit(file[‘title’])
X2= vectorizer.transform(file[‘title’])
X2[0].shape
และสร้าง bag-of-word อีกอันด้วย TfidfVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
vectorizer.fit(file[‘title’])
X1 = vectorizer.transform(file[‘title’])
X1[0].shape
โดยเมื่อทำการสร้าง bag-of-word ทั้ง 2 อันขึ้นมาแล้วจะได้ X ที่มีขนาด(1 , 27423) ! หมายความว่าเรามีข้อมูลทั้งหมด 50,000 * 27,423 = 1,371,150,000 Record !!! ถ้าเราเห็นข้อมูลเยอะขนาดนี้แล้วเรายังฝืนใช้งานมันต่อมีหวังคอมพังแน่นอน เราจะต้องลดขนาดข้อมูลของมันก่อนที่จะนำไปใช้งานนั้นเอง !
เรียก library ที่จะใช้งานขึ้นมา
from sklearn.decomposition import NMF
from sklearn.preprocessing import Normalizer, MaxAbsScaler
from sklearn.pipeline import make_pipeline
ทีนี้มาถึงจุดที่ใช้งานง่าย แต่เข้าใจยาก
อันแรก MaxAbsScaler() เป็นการปรับสเกลค่าในข้อมูลของเราโดยที่แปลงค่าในแถวนั้นๆให้ค่าสูงสุดมีค่าเป็นบวกไม่ติดลบนั้นเอง (ไม่ทำให้การกระจายตัวของข้อมูลเสียหายด้วย)
scaler = MaxAbsScaler()
อันที่สอง NMF() ชื่อเต็มๆคือ Non-negative matrix factorization แปลไทยง่ายๆคือ ทำหน้าที่ลดมิติของข้อมูลโดยที่ข้อมูลจะต้องไม่มีค่าเป็นลบ
nmf = NMF(n_components=50)
อันสุดท้าย Normalizer() เป็นการปรับค่าใน matrix ของเราให้อยู่ในรูปแบบ unit form โดยใช้วิธี l1 หรือ l2 โดยวิธีเป็นวิธีพื้นฐานสำหรับการทำ text classification หรือ การทำ clustering นั้นเอง
normalizer = Normalizer()
จากนั้นสร้าง pipline ขึ้นมาโดยเป็นการรวมเอา 3 อย่างเข้าด้วยกันโดยเรียงลำดับดังนี้
ทำการ scale ข้อมูล -> ลดมิติข้อมูล -> ปรับค่าในข้อมูลเพื่อทำ dot matrix
pipeline = make_pipeline(scaler,nmf,normalizer)
จากนั้นให้เอา pipline ของเรามาเทรนกับข้อมูลที่เราแปลงมาจาก CV กับ tf-idf
norm_features1 = pipeline.fit_transform(X1)
norm_features2 = pipeline.fit_transform(X2)
จากนั้นให้เราทำการสร้าง dataframe ขึ้นมาจาก pandas เพื่อเอาไว้ใช้ในอนาคตโดยไม่ต้องเทรนอีกต่อไป
df1 = pd.DataFrame(norm_features1,index=file[‘title’])
df2 = pd.DataFrame(norm_features2,index=file[‘title’])
โดยเมื่อเราลองส่องดูข้างใน df1,df2 ของเราจะพบว่าขนาดของมันถูกลดเหลือ (1,50) จาก (1,27423) ด้วยวิธี NMF นั้นเองซึ่งทำให้ข้อมูลของเราถูกลดเหลือเพียง 50,000 * 50 = 2,500,000 Record !!! หายไปเกือบๆ 1.368 billion Record !!
ทีนี้ได้เวลาลองทำงานจริงว่าการลดมิติจะส่งผลกระทบอะไรบ้างหรือไม่ และ bag-of-word แต่ละให้ผลลัพธ์ต่างกันขนาดไหน
ให้สร้างฟังก์ชั่นขึ้นมาก่อนอธิบายทีละคำสั่งประมาณนี้
- df.loc[] จะทำการดึงข้อมูล 50 column จาก index ของเรา โดยเราจะต้องใส่หัวข้อข่าวให้ครบทุกตัวอักษร เพื่อที่จะดึงข้อมูลได้ถูก index นั้นเอง
- df.dot() ตรงนี้เป็นการ dot ของ matrix คือ การหาความคล้ายของหัวข้อข่าวด้วยการทำ cosine กับทุกๆข่าวที่เรามี
- similarities.nlargest() หลังจากที่ทำ dot เสร็จแล้วผลลัพธ์จะอยู่ใน similarities และคำสั่ง nlargest() ก็จะแสดง ชื่อหัวข่าว ที่มี ค่าความคล้าย มากที่สุดนั้นเอง
def similarities(article,df):
article_index = df.loc[article]# Compute cosine similarities: similarities
similarities = df.dot(article_index)# Display those with highest cosine similarity
print(similarities.nlargest())
ทีนี้ลองค้นหาหัวข้อข่าวที่ความคล้ายกับ ‘The Atlantic Daily: Passing the Presidential Mic’ แปลไทยก็คือ ข่าวจากสำนัก Atlantic : การส่งต่อตำแหน่งประธานาธิบดี
ทีนี้ลองกับ tf-idf ก่อน
similarities(‘The Atlantic Daily: Passing the Presidential Mic’,df1)
ผลลัพธ์ : หัวข้อข่าวไม่ค่อยจะตรงเท่าไร แต่ที่ถูกต้องแบบเห็นชัดเลยคือชื่อสำนักข่าว แต่หัวข้อจริงๆไม่ถูกเลย ไม่มีใครสื่อได้ถึงประธานาธิบดีเลย แถมมี Grammys Award มาอีก มาได้ไงงงงงง
สาเหตุ : tf-idf นับความถี่ของคำและตัดคำยิบๆย่อยๆออกไป ทำให้คำหลักๆ ซึ่งก็คือชื่อสำนักข่าว ถูกให้ความสำคัญมากกว่าปกติทำให้เราเห็นหัวข้อข่าวที่มีชื่อสำนักข่าวขึ้นมาก่อนนั้นเอง
ทีนี้ลองกับ CountVectorizer
similarities('The Atlantic Daily: Passing the Presidential Mic',df2)
ผลลัพธ์ : คำว่า ‘Presidential’ ถูกยกให้เป็นคำหลักในรอบนี้ ทำให้ข่าวทุกๆข่าวจะเกี่ยวข้องกับประธาธิบดี แถมอันที่สองยังเป็นเรื่องของการดีเบตตำแหน่งประธานาธิบดีอีก ถือว่าแม่นยำพอตัวเลยสำหรับ CountVectorizer
บทสรุป
การลดมิติข้อมูลหากเราสามารถเข้าใจมันได้แบบแท้จริงมันจะเป็นประโยชน์อย่างมากไม่ว่าจะเป็นการเลือก Feature ที่แท้จริง หรือการลดขนาดข้อมูลไม่ให้มากเกินความจำเป็น และเรายังสามารถนำข้อมูลเหล่านี้ไปใช้ได้โดยที่ไม่มีความเสียหายเกิดขึ้นกับข้อมูลเราอีก
Github : https://github.com/peeratpop/Recommend-System-NEWS
Medium : https://medium.com/@pingloaf
Linkedin : https://www.linkedin.com/in/peerat-limkonchotiwat/