ใน ep นี้ เราจะมาเรียนรู้ งานจำแนกหมวดหมู่ข้อความ Text Classification ซึ่งเป็นงานพื้นฐานทางด้าน NLP ด้วยการทำ Latent Semantic Analysis (LSA) วิเคราะห์หาความหมายที่แฝงอยู่ในข้อความ โดยใช้เทคนิค Singular Value Decomposition (SVD) และ Non-negative Matrix Factorization (NMF)
Singular Value Decomposition (SVD) คืออะไร

SVD เป็นวิธี Factorization ยอดนิยม SVD จะแปลง 1 Matrix ขนาดใหญ่ ออกมาเป็น 3 Matrix ขนาดเล็กกว่า ที่คูณกันแล้วได้เท่ากับ Matrix ต้นทาง
3 Matrix ใหม่ที่ได้ออกมา มีคุณสมบัติพิเศษบางอย่าง ทำให้เราสามารถนำมาใช้งาน วิเคราะห์ข้อมูลได้ดีขึ้น
SVD มีประโยชน์มาก ถูกนำไปประยุกต์ใช้ในหลายงาน เช่น
- Semantic Analysis
- Collaborative Filtering / Recommendations
- Calculate Moore-Penrose Pseudoinverse
- Data Compression
- Principal Component Analysis (PCA)
หา svd ของ Matrix ต้นฉบับ ชื่อ vectors ออกมาได้เป็น 3 Matrix ชื่อ U, s Vh

เปรียบเทียบ U คือ รายการ Embedding ของ Topic by ข้อความ, s คือ Scale ขนาดความสำคัญของ Topic by ข้อความ, Vh คือ รายการ Embedding ของ Vocab by Topic

แทนที่เราจะเปรียบเทียบ 2 ข้อความ ด้วยจำนวนคำศัพท์ในข้อความ ใน Term-Document Matrix ตรง ๆ ตอนนี้เรามี Abstraction เพิ่มขึ้นมาอีก 1 ตัวคือ Topic ให้เราใช้เปรียบเทียบ ว่า 2 ข้อความมีปริมาณ แต่ละ Topic ใกล้เคียง หรือแตกต่างกันอย่างไร
Non-negative Matrix Factorization (NMF) คืออะไร

แต่ SVD ก็มีปัญหาคือ มีค่าเป็นติดลบได้ ทำให้มีปัญหาต่อการตีความ จึงมี Matrix Factorization อีกหนึ่งวิธีที่เป็นที่นิยม คือ Non-negative Matrix Factorization (NMF) มาแก้ปัญหาค่าติดลบ ตามชื่อ คือ ผลลัพท์จะได้ Matrix ที่เป็นค่าบวกเท่านั้น
ข้อดีของ NMF คือ ใช้ง่าย ทำงานได้อย่างรวดเร็ว แต่มีข้อเสียคือ ผลลัพธ์เป็นค่าประมาณ ทำให้ไม่สามารถรวมกลับเป็น Matrix ต้นฉบับเหมือนเดิมได้เหมือน SVD และ เป็น NMF อัลกอริทึม เป็น Nondeterministic คือทำงานแต่ละครั้ง ผลลัพธ์อาจจะไม่เท่ากันก็ได้
แต่ข้อดีของค่าประมาณ คือ จัดการกับ Missing Value, ข้อมูลคุณภาพต่ำ ได้ดีกว่า
เรามาเริ่มกันเลยดีกว่า
ใน ep นี้ เราจะมาเรียนรู้ งานจำแนกหมวดหมู่ข้อความ Text Classification ซึ่งเป็นงานพื้นฐานทางด้าน NLP ด้วยการทำ Latent Semantic Analysis (LSA) วิเคราะห์หาความหมายที่แฝงอยู่ในข้อความ โดยใช้เทคนิค Singular Value Decomposition (SVD) และ Non-negative Matrix Factorization (NMF)
1. Import¶
# ! pip install fbpca
import numpy as np
from sklearn.datasets import fetch_20newsgroups
from sklearn import decomposition
from scipy import linalg
import matplotlib.pyplot as plt
import pandas as pd
import plotly.express as px
%matplotlib inline
np.set_printoptions(suppress=True)
2. Dataset¶
ในเคสนี้เราจะใช้ Dataset เป็นข้อความจาก Newsgroup 4 หมวดหมู่ คือ
- อเทวนิยม
- ศาสนา
- คอมพิวเตอร์กราฟฟิก
- วิทยาศาสตร์ อวกาศ
Newsgroup คือ ชุมชนออนไลน์ เหมือน Webboard ในยุค 80 ก่อนที่ www จะเป็นที่นิยม
categories = ['alt.atheism', 'talk.religion.misc', 'comp.graphics', 'sci.space']
remove = ('headers', 'footers', 'quotes')
newsgroups_train = fetch_20newsgroups(subset='train', categories=categories, remove=remove)
newsgroups_test = fetch_20newsgroups(subset='test', categories=categories, remove=remove)
ดูตัวอย่างข้อมูล มีจำนวน 2034 Record
newsgroups_train.filenames.shape, newsgroups_train.target.shape
ชื่อไฟล์
newsgroups_train.filenames[1200:1203]
ดูตัวอย่างเนื้อหา 3 ข้อความ
print("\n============================\n".join(newsgroups_train.data[1200:1203]))
รหัส Category ของ 3 ข้อความด้านบน
newsgroups_train.target[1200:1203]
3 ข้อความด้านบน อยู่ Category ไหน
np.array(newsgroups_train.target_names)[newsgroups_train.target[1200:1203]]
3. Vectorize¶
ในการจะหาว่าเอกสารนี้เกี่ยวกับเรื่องอะไร เอกสาร 2 เอกสาร เขียนถึงเรื่องราวในหัวข้อเดียวกัน อยู่หมวดหมู่เดียวกัน วิธีที่ง่ายที่สุด ตรงตัวที่สุด คือการนับจำนวนแต่ละคำที่อยู่ในแต่ละเอกสาร แล้วนำมาเปรียบเทียบกันตรง ๆ โดยยังไม่ต้องสนใจลำดับของคำ
Import Library ในการนับคำศัพท์
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
ดาวน์โหลด โมเดลภาษา ในที่นี้คือภาษาอังกฤษ
import nltk
nltk.download('punkt')
nltk.download('wordnet')
3.1 Tokenize and Lemmatize¶
ประกาศ Class สำหรับทำ Tokenization ข้อความ String ให้เป็น Token พร้อม Lemmatization แปลง Token ที่ได้ ให้อยู่ในรูปฟอร์มพื้นฐาน
from nltk import word_tokenize, stem
class LemmaTokenizer(object):
def __init__(self):
self.wnl = stem.WordNetLemmatizer()
def __call__(self, doc):
return [self.wnl.lemmatize(t) for t in word_tokenize(doc)]
3.2 Create Matrix of Token Counts¶
แปลงรายการข้อความทั้งหมด ให้เป็น Matrix ของ จำนวน Token เรียกว่า Term-Document Matrix
Matrix 1 Row คือ 1 Vector ข้อความ, มีหลาย ๆ Vector มา Stack กันเป็น Matrix
vectorizer = CountVectorizer(stop_words='english') # No Lemmatization
# vectorizer = CountVectorizer(stop_words='english', tokenizer=LemmaTokenizer()) # Lemmatization
ใช้ได้ matrix ขนาด 2034 Row, 26576 Column
vectors = vectorizer.fit_transform(newsgroups_train.data).todense()
vectors.shape # (documents, vocab)
ดูตัวอย่าง ข้อมูลใน Term-Document Matrix 20 ข้อความแรก จะเห็นว่า Sparse มาก ๆ ส่วนใหญ่เป็น 0
vectors[:20, 10280:10299]
พล็อตเป็น Heatmap จะได้มองง่าย
plt.imshow(vectors[:20, 10280:10299])
เปรียบเทียบขนาดเอกสาร กับขนาด matrix จะเห็นว่า row เท่ากัน, column = จำนวน vocab
print(len(newsgroups_train.data), vectors.shape)
ดูจำนวนคำใน vocab ที่เก็บใน vectorizer
vocab = np.array(vectorizer.get_feature_names())
vocab.shape
ดูคำใน vocab
vocab[26000:26020]
เปรียบเทียบเอกสารจริง กับ Token Counts
print(newsgroups_train.data[1200])
for i, r in enumerate(reversed(sorted(zip(np.squeeze(np.asarray(vectors[1200, :])), vocab)))):
print(r)
if i > 20 : break
4. Latent Semantic Analysis (LSA)¶
เมื่อเราได้ Term-Document Matrix ของจำนวนคำศัพท์ที่อยู่ในข้อความทั้งหมดมาแล้ว การนำแต่ละ Vector มาเปรียบเทียบกันตรง ๆ อาจจะไม่ค่อยได้ผล เนื่องจากคำมีหลากหลายมาก ข้อมูลส่วนใหญ่เป็น 0 การเปรียบเทียบ 2 Vector ข้อความที่มีจำนวนคำบางคำใกล้เคียงกัน ไม่สามารถบอกว่าอยู่หมวดหมู่เดียวกันได้
เราจำเป็นต้องนำ Term-Document Matrix ที่ได้มาผ่านกระบวนการ วิเคราะห์หาความหมายที่ซ่อนอยู่ภายใน Latent Semantic Analysis (LSA) ด้วยวิธีการดังนี้
4.1 Singular Value Decomposition (SVD)¶
SVD เป็นวิธี Factorization ยอดนิยม SVD จะแปลง 1 Matrix ขนาดใหญ่ ออกมาเป็น 3 Matrix ขนาดเล็กกว่า ที่คุณกันแล้วได้เท่ากับ Matrix ต้นทาง
3 Matrix ใหม่ที่ได้ออกมา มีคุณสมบัติพิเศษบางอย่าง ทำให้เราสามารถนำมาใช้งาน วิเคราะห์ข้อความได้ดีขึ้น
SVD มีประโยชน์มาก ถูกนำไปประยุกต์ใช้ในหลายงาน เช่น
- Semantic Analysis
- Collaborative Filtering / Recommendations
- Calculate Moore-Penrose Pseudoinverse
- Data Compression
- Principal Component Analysis (PCA)
หา svd ของ Matrix ต้นฉบับ ชื่อ vectors ออกมาได้เป็น 3 Matrix ชื่อ U, s Vh
เปรียบเทียบ U คือ รายการ Embedding ของ Topic by ข้อความ, s คือ Scale ขนาดความสำคัญของ Topic by ข้อความ, Vh คือ รายการ Embedding ของ Vocab by Topic
แทนที่เราจะเปรียบเทียบ 2 ข้อความ ด้วยจำนวนคำศัพท์ในข้อความเหมือนด้านบน ตอนนี้เรามี Abstraction เพิ่มขึ้นมาอีก 1 ตัวคือ Topic ให้เราใช้เปรียบเทียบ ว่า 2 ข้อความมีปริมาณ แต่ละ Topic ใกล้เคียง หรือแตกต่างกันอย่างไร
%time U, s, Vh = linalg.svd(vectors, full_matrices=False)
ดู shape
print(U.shape, s.shape, Vh.shape)
ดูตัวอย่างค่า U, s, Vh
U[:10, :10]
Vh[:10, :10]
s จะเป็น Diagonal Matrix แต่มาในรูป Vector
s[:4]
เราต้องแปลงเอง ดังนี้
np.diag(s[:4])
แปลงไปแปลงกลับ
np.diag(np.diag(s[:4]))
พิสูจน์ว่า U, s Vh มาจาก vectors จริง ๆ ด้วยการ จับคูณกัน
b = np.matmul(U, np.diag(s))
x = np.matmul(b, Vh)
เนื่องจากเป็น เลขทศนิยม ทำให้เราเปรียบเทียบ == กันตรง ๆ ไม่ได้ ต้องใช้ np.isclose()
np.all(np.isclose(x, vectors))
U เป็น Orthonormal (orthogonal) Matrix หมายถึง คูณกับตัวเอง Transpose แล้วจะได้ Identity Matrix
x = np.matmul(U, U.T)
x[:10, :10]
Vh ก็เป็น Orthonormal (orthogonal) Matrix เหมือนกัน
x = np.matmul(Vh, Vh.T)
x[:10, :10]
s คือ Scale Matrix บอกถึงขนาดความสำคัญของ Topic by ข้อความ เมื่อนำมาพล็อตกราฟ จะได้ดังนี้
plt.plot(s);
ซูมเข้าไปดู 10 มิติแรก จะเห็นว่าข้อมูลส่วนใหญ่อยู่ Topic แรก ๆ เท่านั้น
plt.plot(s[:10])
ประกาศฟังก์ชัน แสดงคำศัพท์ใน vocab ที่พบมากที่สุด 8 คำ ในแต่ละ Topic ใน Vh
num_top_words=8
def show_topics(a):
top_words = lambda t: [vocab[i] for i in np.argsort(t)[:-num_top_words-1:-1]]
topic_words = ([top_words(t) for t in a])
return [' '.join(t) for t in topic_words]
แสดงคำศัพท์ใน vocab ที่พบมากที่สุด 8 คำ ใน 10 Topic แรก เมื่อดูรายการคำศัพท์ ทำให้เราพอจะเดาได้ว่า ถ้าเอกสารมี Topic นี้มาก น่าจะถูกจำแนกอยู่ใน Category อะไร
show_topics(Vh[:10])
นำ 3 มิติแรก มาพล็อตกราฟ
dataset = pd.DataFrame({'Column1': U[:, 0], 'Column2': U[:, 1],
'Column3': U[:, 2], 'Category': newsgroups_train.target})
iris = px.data.iris()
fig = px.scatter_3d(dataset, x='Column1', y='Column2', z='Column3',
color='Category')
fig.show()
4.2 Non-negative Matrix Factorization (NMF)¶
แต่ SVD ก็มีปัญหาคือ มีค่าเป็นติดลบได้ ทำให้มีปัญหาต่อการตีความ จึงมี Matrix Factorization อีกหนึ่งวิธีที่เป็นที่นิยม คือ Non-negative Matrix Factorization (NMF) มาแก้ปัญหาค่าติดลบ ตามชื่อ คือ ผลลัพท์จะได้ Matrix ที่เป็นค่าบวกเท่านั้น
แต่มีข้อเสียคือ เป็นค่าประมาณ ทำให้ไม่สามารถรวมกลับเป็น Matrix ต้นฉบับเหมือนเดิมได้เหมือน SVD และ เป็น NMF อัลกอริทึม เป็น nondeterministic คือทำงานแต่ละครั้ง ผลลัพธ์อาจจะไม่เท่ากันก็ได้
ข้อดีของค่าประมาณ คือ จัดการกับ Missing Value ได้ดีกว่า
m,n=vectors.shape
เราจะ Decompose vectors ออกเป็น 5 Topic
d=5 # num topics
clf = decomposition.NMF(n_components=d, random_state=1)
W1 = clf.fit_transform(vectors)
H1 = clf.components_
W1.shape, H1.shape
จะเห็นว่า จำนวนคำศัพท์บางคำ ใน Topic แรก มีค่าโดดมาก
plt.plot(clf.components_[0])
W1[:10]
H1[:, :10]
show_topics(H1)
นำ 3 มิติแรก มาพล็อตกราฟ
dataset = pd.DataFrame({'Column1': W1[:, 0], 'Column2': W1[:, 1],
'Column3': W1[:, 2], 'Category': newsgroups_train.target})
iris = px.data.iris()
fig = px.scatter_3d(dataset, x='Column1', y='Column2', z='Column3',
color='Category')
fig.show()
5. Term Frequency–Inverse Document Frequency (TF-IDF)¶
การใช้ Count Vectorizer สร้าง Term-Document Matrix จะมีปัญหา ถ้าแต่ละข้อความใน Dataset ของเรา มีความยาวต่างกันมาก ๆ การนับจำนวนคำที่อยู่ในข้อความ มาเปรียบเทียบกันก็อาจจะไม่แฟร์ได้
TF-IDF เข้ามาแก้ปัญหานี้ โดยการนำจำนวนคำทั้งหมดที่อยู่ภายในเอกสารมาหาร แปลงให้เป็นสัดส่วน แทนที่จะใช้จำนวนนับตรง ๆ เหมือนด้านบน
vectorizer_tfidf = TfidfVectorizer(stop_words='english')
vectors_tfidf = vectorizer_tfidf.fit_transform(newsgroups_train.data) # (documents, vocab)
vectors_tfidf.shape
จะเห็นว่าเป็นเลขทศนิยม (สัดส่วน) ไม่ใช่จำนวนนับ (จำนวนครั้งที่คำศัพท์ปรากฎในข้อความ)
vectors_tfidf[:10, 10280:10299].todense()
print("\n============================\n".join(newsgroups_train.data[1200:1203]))
W1 = clf.fit_transform(vectors_tfidf)
H1 = clf.components_
W1.shape, H1.shape
จะทำให้ vocab ใน Topic กระจายตัว แฟร์มากขึ้น เทียบกับ Count ด้านบน
show_topics(H1)
Embedding Vocab by Topic มิติแรก ค่อนข้างกระจายดี
plt.plot(clf.components_[0])
เราไม่สามารถรวมเป็น Matrix ต้นฉบับเหมือนเดิมเป๊ะได้ จะมี Error
clf.reconstruction_err_
6. Truncated SVD¶
ปัญหาอีกอย่างนึงของ SVD คือ ทำงานช้า มีการพัฒนาอัลกอริทึม ชื่อว่า Randomized SVD และ Truncated SVD ช่วยให้ทำงานเร็วขึ้น หลายสิบเท่า
%time u, s, v = np.linalg.svd(vectors, full_matrices=False)
from sklearn import decomposition
import fbpca
%time u, s, v = decomposition.randomized_svd(vectors, 10)
%time u, s, v = fbpca.pca(vectors, 10)
Credit¶
- https://github.com/fastai/course-nlp
- https://scikit-learn.org/stable/modules/generated/sklearn.datasets.fetch_20newsgroups.html
- https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html
- https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html
- https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.svd.html
- https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.NMF.html
- https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.TruncatedSVD.html
- https://research.fb.com/fast-randomized-svd/
- https://plot.ly/python/3d-scatter-plots/