ใน ep นี้ เราจะใช้ความรู้จาก ep ก่อน ในการสร้าง Term-Document Matrix ด้วย CountVectorizer ด้วยข้อมูลรีวิวหนัง IMDB แล้วนำ Term-Document Matrix ที่ได้ มาวิเคราะห์ Sentiment Classification ว่าเป็นรีวิวแง่บวก หรือแง่ลบ (positive/negative) ด้วยเทคนิค Naive Bayes และ Logistic Regression
Naive Bayes Classifier คืออะไร
Naive Bayes Classifier คือ Classifier ด้วยความน่าจะเป็น Probability บนพื้นฐานของ ทฤษฎีของเบย์ (Bayes’ Theorem) เป็นโมเดลแบบง่าย ด้วยสมมติฐานที่ว่า แต่ละ Feature เป็นอิสระ ไม่ขึ้นต่อกัน
\( p(C_k \mid \mathbf{x}) = \frac{p(C_k) \ p(\mathbf{x} \mid C_k)}{p(\mathbf{x})} , {posterior} = \frac{\text{prior} \times \text{likelihood}}{\text{evidence}} \)
Logistic Regression คืออะไร
อ่านต่อ Logistic Regression คืออะไร
เรามาเริ่มกันเลยดีกว่า
ใน ep นี้ เราจะใช้ความรู้จาก ep ก่อน ในการสร้าง Term-Document Matrix ด้วย CountVectorizer ด้วยข้อมูลรีวิวหนัง IMDB แล้วนำ Term-Document Matrix ที่ได้ มาวิเคราะห์ Sentiment Classification ว่าเป็นรีวิวแง่บวก หรือแง่ลบ (positive/negative) ด้วยเทคนิค Naive Bayes และ Logistic Regression
0. Install¶
Install Library ที่จำเป็น
## Colab
! curl -s https://course.fast.ai/setup/colab | bash
%reload_ext autoreload
%autoreload 2
%matplotlib inline
1. Import¶
Import Library ที่จะใช้ ในที่นี้คือ fastai.text และ sklearn_text ที่ใช้ในงาน NLP
from fastai import *
from fastai.text import *
import sklearn.feature_extraction.text as sklearn_text
from matplotlib import pyplot as plt
import seaborn as sns
2. Dataset¶
ในเคสนี้เราจะใช้ IMDB Movie Review เป็นรีวิวหนังจากเว็บ IMDB ที่มีข้อความ และ คะแนนว่าเป็นแง่บวก หรือแง่ลบ
ในการพัฒนา เราจะใช้ Dataset ชุดเล็กก่อน จะได้เร็ว เมื่อเทสทุกอย่างเรียบร้อย แล้วจึงขยับไปใช้ Dataset ชุดเต็ม
path = untar_data(URLs.IMDB_SAMPLE)
path
ดูว่ามีไฟล์อะไรบ้าง
path.ls()
โหลดไฟล์ csv ขึ้นมา ใส่ Pandas Dataframe
df = pd.read_csv(path/'texts.csv')
df.shape
มีข้อมูลตัวอย่าง 1000 Record, 3 Column ดูตัวอย่างข้อมูล 5 Record แรก
df.head()
3. Preprocessing¶
เราจะใช้ Fastai TextList ในการ Preprocess ข้อมูล เช่น Tokenization, Numberization, etc.
movie_reviews = (TextList.from_csv(path, 'texts.csv', cols='text').split_from_df(col=2).label_from_df(cols=0))
จะได้ Trainint Set, Validation Set ออกมา เรามาดูข้อมูลตัวอย่าง Record แรกใน Validation Set
movie_reviews.valid.y[0], movie_reviews.valid.x[0]
จะเห็น Label y ว่าเป็น รีวิวแง่บวก Positive และ Text x เป็นข้อความรีวิว
ดูขนาดของ Training Set และ Validation Set
len(movie_reviews.train.x), len(movie_reviews.valid.x)
ขนาดของ vocab Dictionary ที่ Map จาก เลขลำดับ Token ไปเป็น ข้อความ และ ข้อความ เป็น เลขลำดับ Token
จะเห็นว่า ไม่ตรงกัน stoi มากกว่า itos เนื่องจากมีหลาย ๆ คำ ถูก Map ไปเป็น Unknown เพื่อจำกัดจำนวน vocab
len(movie_reviews.vocab.itos), len(movie_reviews.vocab.stoi)
ดูตัวอย่างคำว่า love เป็น คำลำดับที่ 142 ใน vocab
movie_reviews.vocab.stoi['love']
ดูคำที่ 142 ใน vocab itos เป็นคำว่า love จริง ๆ
movie_reviews.vocab.itos[142]
ดู vocab ในช่วงคำว่า love
movie_reviews.vocab.itos[140:149]
ดู vocab 20 คำท้ายสุด
movie_reviews.vocab.itos[-20:]
ดู vocab 20 คำแรก คำแรก ๆ จะเป็น Reserved Token รายละเอียดดังนี้ Sentiment Analysis ep.1 / Tokenization
movie_reviews.vocab.itos[:20]
ดูตัวอย่าง vocab Dictionary ที่ได้ โดยแสดง Text และ เลขลำดับ คู่กัน
from itertools import *
list(islice(movie_reviews.vocab.stoi.items(), 20))
คำที่อยู่นอก vocab Dictionary จะถูก Map เป็น Unknown (xxunk)
i = movie_reviews.vocab.stoi['bualabs']
movie_reviews.vocab.itos[i], i
i = movie_reviews.vocab.stoi['suvarnabhumi']
movie_reviews.vocab.itos[i], i
i = movie_reviews.vocab.stoi['airport']
movie_reviews.vocab.itos[i], i
เลือก x ของ Record แรก ของ Training Set ดูข้อความ
t = movie_reviews.train[0][0]
t
ภายใน x จะถูก Tokenize, Numberize แปลงเป็นตัวเลขเรียบร้อยแล้ว
t.data[:30]
4. Creating Term-Document Matrix¶
นำข้อมูลจากด้านบน มาสร้าง Term-Document Matrix
4.1 Counter¶
ตัวอย่างการใช้งาน Counter class
c = Counter([1, 2, 3, 4, 5,
1, 2, 3, 4,
1, 2, 3,
1, 2,
1])
c
จะนับครั้งที่ข้อมูลปรากฎ มาใส่ใน Dictionary
c.keys(), c.values()
4.2 Term-Document Matrix as Sparse Matrix¶
เนื่องจาก ใน 1 รีวิว ไม่ได้ยาวมาก ไม่ได้มีคำหลากหลาย แต่ 1 รีวิวใช้เพียง 100 กว่าคำ จาก vocab 6000 กว่าคำ ทำให้ Term-Document Matrix นี้ส่วนใหญ่มีค่าเป็น 0 เรียกว่า Sparse Matrix จะอธิบายต่อไป
c = Counter(movie_reviews.valid.x[0].data)
len(c), c
จากด้านบน ดู Token ที่ปรากฎหลาย ๆ ครั้ง ปรากฎว่าเป็น Token พิเศษ Unknow, Begin of sentense, ตัว Capital, ตัวใหญ่ และ คำว่า The
idx = [0, 2, 5, 6, 9]
[movie_reviews.vocab.itos[i] for i in idx]
ดูตัวอย่างข้อมูล Record แรก ใน Validation Set
movie_reviews.valid.y[1], movie_reviews.valid.x[1]
ดู label และ ข้อมล ที่แปลงเป็น ตัวเลข แล้ว ทั้งหมด
movie_reviews.valid.y[1].data, movie_reviews.valid.x[1].data
ประกาศฟังก์ชัน สร้าง Term-Document Matrix โดย output เป็น Sparse Matrix แบบ Compressed sparse row (CSR, CRS หรือ Yale format)
def get_term_doc_matrix(label_list, vocab_len):
j_indices = []
indptr = []
values = []
indptr.append(0)
for i, doc in enumerate(label_list):
feature_counter = Counter(doc.data)
j_indices.extend(feature_counter.keys())
values.extend(feature_counter.values())
indptr.append(len(j_indices))
# return (values, j_indices, indptr)
return scipy.sparse.csr_matrix((values, j_indices, indptr),
shape=(len(indptr)-1, vocab_len),
dtype=int)
จับเวลาการสร้าง Term-Document Matrix ด้วยข้อมูล Validation Set
%%time
val_term_doc = get_term_doc_matrix(movie_reviews.valid.x, len(movie_reviews.vocab.itos))
จับเวลาการสร้าง Term-Document Matrix ด้วยข้อมูล Training Set
%%time
trn_term_doc = get_term_doc_matrix(movie_reviews.train.x, len(movie_reviews.vocab.itos))
ได้ output ออกมา จำนวน column เท่ากับ ขนาด vocab Dictionary
trn_term_doc.shape
val_term_doc.shape
4.3 Sparse Matrix vs Dense Matrix¶
Sparse จะถูกจัดเก็บด้วยวิธีเฉพาะ เพื่อประหยัดเนื้อที่ ทำให้บางทีเราไม่สามารถเรียกดูข้อมูลตรง ๆ ได้
trn_term_doc[:, -10:]
เราต้องเรียน todense เพื่อแปลงเป็น Matrix ปกติก่อน
trn_term_doc.todense()[:8, :8]
นำส่วนหนึ่งมาพล็อต Heatmap ดู จะเห็นว่า Sparse มาก ๆ ดำหมด ไม่เห็นอะไรเลย เป็น 0 เกือบหมด
fig, ax = plt.subplots(figsize=(12, 2))
ax.imshow(trn_term_doc.todense(), interpolation='nearest')
plt.tight_layout()
ซูมดู 100 Feature แรก ของ 100 ข้อมูลตัวอย่างแรก
fig, ax = plt.subplots(figsize=(12, 12))
ax.imshow(trn_term_doc.todense()[:100, :100], interpolation='nearest')
plt.tight_layout()
4.4 Data Exploration¶
เราจะมา สำรวจข้อมูล ทำ Data Exploration กันต่อ
v = movie_reviews.vocab
ดู Token แรกใน vocab
v.itos[0]
ดู Token สุดท้ายใน vocab
v.itos[-1:]
ดู Token ที่ 4 ใน vocab
v.itos[3]
เราจะมาดูตัวอย่างข้อความ Record ที่ 1 ใน Validation Set มีคำว่า Late 2 คำ ในข้อความ
review = movie_reviews.valid.x[1]; review
คำว่า Late คือคำที่ 451 ใน vocab Dictionary
i = v.stoi['late']; i
ดูใน Validation Term-Document Matrix ที่ Row 1, Column 451 จะได้ 2
val_term_doc[1, 451]
ข้อความ Record ที่ 1 ใน Validation Set มีทั้งหมด 144 Token จาก 81 คำศัพท์
val_term_doc[1].sum(), (val_term_doc[1] > 0).sum()
Token ที่ Numberize แล้ว
review.data
แปลง Token ที่ Numberize แล้ว กลับเป็น Token ข้อความ
[v.itos[i] for i in review.data][:20]
4.5 Unknown Words¶
จำนวน itos ไม่เท่ากับ stoi เนื่องจาก หลายคำที่ปรากฎไม่บ่อย กลายเป็น Unknown เพื่อจำกัดขนาด vocab Dictionary
len(v.itos), len(v.stoi)
ต่างกันถึง 13155 คำ
len(v.stoi) - len(v.itos)
ดูรายการ Unknown Word ทั้งหมด
unk = [k for k, v in v.stoi.items() if v == 0]
len(unk), unk[:20]
5. Sentiment Classification¶
เราจะใช้ Term-Document Matrix ที่ได้จากด้านบน มาวิเคราะห์ความรู้สึก Sentiment Classification ว่ารีวินี้ เป็นแง่บวก หรือแง่ลบ ด้วยอัลกอริทึมต่าง ๆ
5.1 Naive Bayes¶
กำหนดสัดส่วน log-count ratio $r$ สำหรับแต่ละคำ $f$:
$r = \log \frac{\text{ratio of feature $f$ in positive documents}}{\text{ratio of feature $f$ in negative documents}}$
โดยสัดส่วนของฟีเจอร์ $f$ ในรีวิวแง่บวก คือ จำนวนครั้งที่รีวิวแง่บวกมีฟีเจอร์นี้ หารด้วยจำนวนรีวิวแง่บวกทั้งหมด
แสดง class ของ รีวิวหนัง
movie_reviews.y.classes
กำหนด x, y ของ Training Set และ val_y ของ Validation Set
x = trn_term_doc
y = movie_reviews.train.y
val_y = movie_reviews.valid.y
แปลง class กลับเป็นเลข 1 แทน positive, 0 แทน negative
positive = y.c2i['positive']
negative = y.c2i['negative']
positive, negative
เลือกเฉพาะ รีวิวที่ negative มา Sum ในมิติ 0 ให้ได้ จำนวน Count รวมแต่ละ Feature
np.squeeze(np.asarray(x[y.items==negative].sum(0)))
เลือกเฉพาะ รีวิวที่ positive มา Sum ในมิติ 0 ให้ได้ จำนวน Count รวมแต่ละ Feature
np.squeeze(np.asarray(x[y.items==positive].sum(0)))
จำนวน Count รวมแต่ละ Feature แยก Positive, Negative¶
p1 = np.squeeze(np.asarray(x[y.items==positive].sum(0)))
p0 = np.squeeze(np.asarray(x[y.items==negative].sum(0)))
len(p1), len(p0)
แสดง จำนวน Count ของ 10 คำศัพท์แรก ใน positive, negative
p1[:10], p0[:10]
Explore Ratio¶
ประกาศฟังก์ชัน หาสัดส่วน Ratio ของ Count positive/negative คำที่กำหนด
def p_ratio(s):
i = v.stoi[s]
return p1[i]/p0[i]
loved มี ratio มากกว่า 1 ไปเยอะเลย
s = 'loved'
p_ratio(s)
hated ratio = 0.5 น้อยกว่า 1 แสดงว่า ปรากฎใน negative รีวิวมากกว่า 2 เท่า
s = 'hated'
p_ratio(s)
best ก็มากกว่า 1
s = 'best'
p_ratio(s)
worst ยิ่งชัดมาก ปรากฎใน negative รีวิว บ่อยกว่า positive มาก ๆ
s = 'worst'
p_ratio(s)
Explore data¶
ดู Positive รีวิว ที่มีคำว่า hated
v.stoi['hated']
หา record ที่มี count คำที่ 1977 มากกว่า 0
a = np.argwhere((x[:, 1977] > 0))[:, 0]; a
หาเฉพาะ record ที่เป็น positive
b = np.argwhere(y.items==positive)[:, 0]; b
นำมา intersect กัน
set(a).intersection(set(b))
ไล่ดูข้อความรีวิว ทั้ง 3 record
review = movie_reviews.train.x[393]
review.text
review = movie_reviews.train.x[612]
review.text
review = movie_reviews.train.x[695]
review.text
กลับกัน ดู Negative รีวิว ที่มีคำว่า loved
v.stoi['loved']
หา record ที่มี count คำที่ 535 มากกว่า 0
a = np.argwhere((x[:, 535] > 0))[:, 0]; a
b = np.argwhere(y.items==negative)[:, 0]; b
set(a).intersection(set(b))
ไล่ดูข้อความรีวิว 3 record แรก
review = movie_reviews.train.x[15]
review.text
review = movie_reviews.train.x[200]
review.text
review = movie_reviews.train.x[205]
review.text
เข้าสูตร Naive Bayes¶
นำสูตรข้างบนมาคำนวนให้ดูอีกครั้งหนึ่งชัด ๆ
p1 = np.squeeze(np.asarray(x[y.items==positive].sum(0)))
p0 = np.squeeze(np.asarray(x[y.items==negative].sum(0)))
len(p1), len(p0)
บวก 1 เพื่อ Numerical Stability เช่น ป้องกัน Devide by zero
pr1 = (p1 + 1) / ((y.items==positive).sum() + 1)
pr0 = (p0 + 1) / ((y.items==negative).sum() + 1)
len(pr1), len(pr0)
เนื่องจาก Ratio ถ้าเป็นคำศัพท์ค่อนข้าง positive จะมีค่า ratio 1-Infinity แต่ถ้าเป็นคำศัพท์ค่อนข้าง negative จะมี ratio 0-1 การใช้ log function จะช่วยปรับให้เป็นช่วง -Infinity-0, 0-Infinity เท่า ๆ กัน นำไปเปรียบเทียบ ใช้งานต่อได้ง่ายขึ้น
r = np.log(pr1/pr0); r
ดูคำศัพท์ที่มักถูกใช้ใน Positive / Negative Review
biggest = np.argpartition(r, -10)[-10:]
smallest = np.argpartition(r, 10)[:10]
คำที่ Positive ที่สุด
เนื่องจากเป็น Dataset ชุดเล็ก ข้อมูลน้อย อาจจะบอกอะไรไม่ได้มาก คำศัพท์บางคำอาจจะพบแค่ครั้งเดียว ในรีวิวแง่บวก positive ทำให้ความหมายผิดไป
[v.itos[i] for i in biggest]
np.argmax(trn_term_doc[:, v.stoi['biko']])
movie_reviews.train.x[515]
คำที่ Negative ที่สุด
[v.itos[i] for i in smallest]
np.argmax(trn_term_doc[:, v.stoi['soderbergh']])
movie_reviews.train.x[434]
Predict with Bayes¶
หาค่าเฉลี่ย จำนวนข้อมูลรีวิว แง่บวก แง่ลบ
(y.items==positive).mean(), (y.items==negative).mean()
หาค่า bias ของข้อมูล เตรียมเอาไว้ บวกในสมการ เส้นตรง
b = np.log((y.items==positive).mean() / (y.items==negative).mean()) ; b
นำ Validation Term-Document Matrix มาคูณกับ r และ บวก b ดูว่าอันไหนมากกว่า 0 เป็น positive น้อยกว่า 0 เป็น netative
preds = (val_term_doc @ r + b ) > 0
เทียบกับ y ใน Validation Set ถูกต้องถึง 64.5%
(preds == val_y.items).mean()
2/2. Full Dataset¶
เมื่อทุกอย่างพร้อมแล้ว เราจะขยับไป Dataset ตัวเต็มกันต่อ
path = untar_data(URLs.IMDB)
path
จะมี Folder ชื่อ train และ test
path.ls()
ใน train จะมี Folder แยกตาม label pos/neg (Positive/Negative)
(path/'train').ls()
3/2. Preprocessing¶
ใช้ Fastai ทำ Preprocessing สร้าง TextList จากไฟล์ใน Folder ที่กำหนด Split ตาม Folder test ที่กำหนด แล้ว Label ตาม Folder neg/pos
reviews_full = (TextList.from_folder(path)
.split_by_folder(valid='test')
.label_from_folder(classes=['neg', 'pos']))
จะได้ Training Set และ Validation Set อย่างละ 25,000 ตัวอย่าง
len(reviews_full.train), len(reviews_full.valid)
ดูจำนวนคำศัพท์ใน vocab Dictionary
v = reviews_full.vocab
len(v.itos), len(v.stoi)
ดูตัวอย่างคำศัพท์ ใน vocab Dictionary
v.itos[100:110]
4/2. Creating Term-Document Matrix¶
%%time
val_term_doc = get_term_doc_matrix(reviews_full.valid.x, len(v.itos))
%%time
trn_term_doc = get_term_doc_matrix(reviews_full.train.x, len(v.itos))
4.1/2 Save Term-Document Matrix¶
ยิ่ง Dataset ขนาดใหญ ยิ่งใช้เวลานาน ในการสร้าง Term-Document Matrix เราจึงควร Save ไว้ก่อน จะได้ไม่ต้องสร้างใหม่ทุกครั้ง
scipy.sparse.save_npz("trn_term_doc.npz", trn_term_doc)
scipy.sparse.save_npz("val_term_doc.npz", val_term_doc)
4.2/2 Load Term-Document Matrix¶
โหลด Term-Document Matrix ขึ้นมาจากไฟล์ที่เรา Save ไว้ด้านบน
# trn_term_doc = scipy.sparse.load_npz("trn_term_doc.npz")
# val_term_doc = scipy.sparse.load_npz("val_term_doc.npz")
5/2. Naive Bayes บน Dataset ตัวเต็ม¶
x = trn_term_doc
y = reviews_full.train.y
val_y = reviews_full.valid.y.items
x
positive = y.c2i['pos']
negative = y.c2i['neg']
p0 = np.squeeze(np.asarray(x[y.items==negative].sum(0)))
p1 = np.squeeze(np.asarray(x[y.items==positive].sum(0)))
p1[:20]
ratio ที่ได้ ต่างกับ Dataset ชุดเล็กนิดหน่อย
s = 'loved'
p_ratio(s)
s = 'hated'
p_ratio(s)
s = 'best'
p_ratio(s)
s = 'worst'
p_ratio(s)
หา r ด้วยสูตรเดิม เหมือนด้านบน
pr1 = (p1+1) / ((y.items==positive).sum(0) + 1)
pr0 = (p0+1) / ((y.items==negative).sum(0) + 1)
ใส่ Log Function เตรียมใส่ Naive Bayes
r = np.log(pr1/pr0)
r[v.stoi['loved']]
r[v.stoi['hated']]
r[v.stoi['best']]
r[v.stoi['worst']]
หาค่า bias ของข้อมูล เตรียมเอาไว้ บวกในสมการ เส้นตรง
b = np.log((y.items==positive).mean() / (y.items==negative).mean()) ; b
นำ Validation Term-Document Matrix มาคูณกับ r และ บวก b ดูว่าอันไหนมากกว่า 0 เป็น positive น้อยกว่า 0 เป็น netative
preds = (val_term_doc @ r + b ) > 0
เทียบกับ y ใน Validation Set ถูกต้องถึง 80.8% เทียบกับ 64.5% ก่อนหน้านี้
(preds == val_y).mean()
5/3. Binarized Naives Bayes¶
แทนที่จะใช้ Count ใช้จำนวนที่คำศัพท์ปรากฎ เราจะเปลี่ยนไปใช้ แค่ว่ามีคำศัพท์ปรากฎในข้อความรีวิวหรือไม่ Yes/No (1/0) เท่านั้นโดยไม่สนใจจำนวนครั้ง
x = trn_term_doc.sign()
y = reviews_full.train.y
x.todense()[:10, :10]
negative = y.c2i['neg']
positive = y.c2i['pos']
p0 = np.squeeze(np.asarray(x[y.items==negative].sum(0)))
p1 = np.squeeze(np.asarray(x[y.items==positive].sum(0)))
pr1 = (p1+1) / ((y.items==positive).sum(0) + 1)
pr0 = (p0+1) / ((y.items==negative).sum(0) + 1)
r = np.log(pr1/pr0)
b = np.log((y.items==positive).mean() / (y.items==negative).mean())
preds = (val_term_doc.sign() @ r + b) > 0
ได้ 82.9% เทียบกับ 80.8% ก่อนหน้า
(preds == val_y).mean()
หมายความว่า จำนวนครั้งที่ปรากฎอาจจะไม่สำคัญเท่าไร
5/4. Logistic Regression¶
เราจะลอง fit ฟีเจอร์ทั้งหมด (ฟีเจอร์แบบ Unigram) ด้วย Logistic Regression ได้ผลลัพธ์ ถูกต้องถึง 88.3%
from sklearn.linear_model import LogisticRegression
m = LogisticRegression(C=0.1, dual=True, solver='liblinear', max_iter=10000)
m.fit(trn_term_doc, y.items.astype(int))
preds = m.predict(val_term_doc)
(preds == val_y).mean()
ลองใช้แบบ Binarized ได้ผลลัพธ์ ถูกต้องถึง 88.5%
m = LogisticRegression(C=0.1, dual=True, solver='liblinear', max_iter=10000)
m.fit(trn_term_doc.sign(), y.items.astype(int))
preds = m.predict(val_term_doc.sign())
(preds == val_y).mean()
สรุป¶
- เราได้เรียนรู้การสร้าง Term-Document Matrix ทั้งแบบ CountVectorizer และ Binarized จากข้อมูลรีวิวหนัง IMDB
- เราได้นำ Term-Document Matrix มาวิเคราะห์ Sentiment Classification ด้วยเทคนิค Naive Bayes และ Logistic Regression
- เราได้เรียนรู้ว่า Sparse Matrix และ Dense Matrix ต่างกันอย่างไร
Credit¶
- https://www.youtube.com/watch?v=dt7sArnLo1g&list=PLtmWHNX-gukKocXQOkQjuVxglSDYWsSh9&index=6&t=0s
- https://www.bualabs.com/archives/926/sentiment-analysis-imdb-movie-review-ulmfit-sentiment-analysis-ep-1/
- https://docs.fast.ai/text.data.html
- https://docs.python.org/2/library/collections.html
- https://en.wikipedia.org/wiki/Naive_Bayes_classifier
- https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html