Chuyển tới nội dung chính

Phân loại Naive Bayes

Naive Bayes là một thuật toán phân loại đơn giản tới mức gần như tầm thường, nhưng lại mạnh mẽ một cách đáng ngạc nhiên cho một họ bài toán rất quan trọng: phân loại văn bản. Nó là thuật toán đầu tiên ta gặp dựa hoàn toàn trên xác suất, và là baseline mà mọi dự án phân loại văn bản nên chạy thử trước khi nghĩ tới các mô hình phức tạp hơn.

Bài toán dẫn nhập quen thuộc nhất là lọc thư rác. Bạn nhận một email ghi "Win a free iPhone! Click here now!". Con người nhìn là biết ngay đây là spam. Câu hỏi là: máy tính làm sao biết? Ý tưởng xác suất rất tự nhiên: hãy tính xác suất email này là spam, tính xác suất nó là thư bình thường (ham), rồi chọn lớp nào có xác suất lớn hơn.

Thử ngay: kéo thanh trượt xem tỷ lệ nền lật hậu nghiệm

Trước khi đi vào công thức, bạn có thể tự cảm nhận phép tính đó qua bộ lọc thư rác dưới đây. Ba thanh trượt là tỷ lệ nền (trong toàn bộ hộp thư, bao nhiêu phần vốn là thư rác) và hai khả năng (một thư rác, hoặc một thư bình thường, hay chứa từ đáng ngờ tới mức nào). Kết quả cần tính gọi là hậu nghiệm: xác suất lá thư đang xét là thư rác. Kéo thử để xem công thức Bayes thay số cùng hai thanh bar so tích tiên nghiệm nhân khả năng, rồi bấm nút tình huống thư rác hiếm để thấy dù khả năng rất cao, hậu nghiệm vẫn nhỏ khi lớp quá hiếm.

Bộ lọc thư rác theo định lý BayesP(rác | từ) = 0.89
P(rác | từ) = 0.8 · 0.50.8 · 0.5 + 0.1 · 0.5= 0.40.45 = 0.889
rác: P(rác)·P(từ|rác)0.4
thường: P(thường)·P(từ|thường)0.05
Nghiêng về thư rác: hậu nghiệm P(rác | từ) = 89%, lớn hơn P(thường | từ) = 11%.

Ý tưởng cốt lõi

Định lý Bayes bằng lời

Định lý Bayes cho phép ta đảo ngược chiều của một quan hệ điều kiện. Viết bằng chữ, nó nói rằng P(A | B) bằng P(B | A) nhân với P(A) rồi chia cho P(B). Nghĩa là nếu ta biết xác suất của B khi đã có A, ta có thể suy ngược ra xác suất của A khi đã có B, miễn là biết thêm hai xác suất nền P(A)P(B).

Áp dụng vào phân loại, ta muốn tính P(y | x), tức xác suất một mẫu thuộc lớp y khi đã quan sát đầu vào x. Bayes cho ta viết lại:

P(y | x) = P(x | y) * P(y) / P(x)

Bốn đại lượng này có tên riêng:

  • P(y | x) là hậu nghiệm (posterior): xác suất lớp y sau khi đã thấy x. Đây chính là thứ ta muốn.
  • P(x | y) là khả năng (likelihood): xác suất quan sát được x nếu lớp thật sự là y. Đây là thứ ta có thể ước lượng từ dữ liệu.
  • P(y) là tiên nghiệm (prior): xác suất của lớp y trước khi nhìn vào x, ước lượng từ tỷ lệ các lớp trong tập huấn luyện.
  • P(x) là bằng chứng (evidence): xác suất quan sát x. Vì nó không phụ thuộc vào lớp y, nó không ảnh hưởng tới việc xếp hạng các lớp, nên khi so sánh ta có thể bỏ qua.

Do P(x) không đổi giữa các lớp, quy tắc phân loại rút gọn thành: chọn lớp y làm cho tích P(x | y) * P(y) lớn nhất.

Quy trình phân loại một mẫu bằng Naive Bayes
Xác suất tiên nghiệm
tính P(y) từ tỷ lệ mỗi lớp trong tập huấn luyện
Xác suất khả năng
ước lượng khả năng từng đặc trưng bằng cách đếm
Nhân theo Bayes
nhân tiên nghiệm với các khả năng của mẫu
Chọn lớp cao nhất
lấy lớp cho tích lớn nhất làm dự đoán

Vì sao base rate quan trọng (ví dụ chẩn đoán)

Một xét nghiệm bệnh hiếm có độ nhạy 99 phần trăm (dương tính khi có bệnh) và độ đặc hiệu 99 phần trăm (âm tính khi không bệnh). Bệnh xuất hiện ở 1 trên 10000 người. Bạn xét nghiệm dương tính. Xác suất bạn thực sự bị bệnh là bao nhiêu?

Đặt P(T | D) = 0.99, P(T | khong_D) = 0.01, và P(D) = 0.0001. Theo Bayes:

P(D | T) = (0.99 * 0.0001) / (0.99 * 0.0001 + 0.01 * 0.9999)
= 0.000099 / (0.000099 + 0.009999)
~ 0.0098

Chỉ khoảng 1 phần trăm. Trực giác mách bảo 99 phần trăm, nhưng tỷ lệ nền (base rate) quá nhỏ đã thắng. Đây là bài học kinh điển về việc không được bỏ quên xác suất nền.

Giả định độc lập ngây thơ

Với phân loại văn bản, x là cả một văn bản với hàng triệu khả năng. Ta không bao giờ có đủ dữ liệu để thấy mỗi văn bản cụ thể đủ nhiều lần mà ước lượng trực tiếp P(x | y). Naive Bayes giải quyết bằng một giả định táo bạo: các đặc trưng độc lập có điều kiện với nhau khi đã biết lớp. Nói cách khác, khi đã biết một email là spam, sự xuất hiện của từ "free" không liên quan gì tới sự xuất hiện của từ "money".

Nhờ giả định đó, ta phân rã khả năng thành một tích:

P(x | y) = P(x_1 | y) * P(x_2 | y) * ... * P(x_d | y)

Mỗi thừa số là xác suất một chiều, dễ ước lượng bằng cách đếm. Quy tắc phân loại cuối cùng là: chọn lớp y làm cực đại

P(y) * P(x_1 | y) * P(x_2 | y) * ... * P(x_d | y)

Giả định này gọi là "ngây thơ" (naive) vì rõ ràng các từ không độc lập trong văn bản thật: "machine" và "learning" hay đi cặp, "New" và "York" hay đứng cạnh nhau. Tuy ngây thơ, NB vẫn hoạt động tốt vì để phân loại đúng ta chỉ cần xếp hạng các lớp đúng thứ tự, chứ không cần ước lượng xác suất tuyệt đối thật chính xác.

Cần phân biệt độc lập có điều kiện với độc lập thông thường, vì đây là chỗ hay bị hiểu sai. Naive Bayes không giả định các từ độc lập nói chung; nó chỉ giả định chúng độc lập một khi đã biết lớp. Trên toàn bộ kho thư, "free" và "money" rõ ràng tương quan vì cùng hay xuất hiện trong thư rác. Nhưng giả định của NB nhẹ hơn thế: trong riêng nhóm thư rác, biết thêm rằng có "money" không làm thay đổi khả năng có "free". Phân biệt này quan trọng vì chính việc điều kiện theo lớp mới khiến giả định trở nên hợp lý hơn nhiều so với đòi độc lập tuyệt đối.

Vì sao vẫn chạy tốt khi giả định sai

Điều nghịch lý là giả định độc lập gần như luôn sai trên văn bản thật, vậy mà Naive Bayes vẫn phân loại rất tốt. Lý do nằm ở chỗ ta đòi hỏi ở mô hình ít hơn ta tưởng. Để xếp một email vào đúng lớp, ta không cần con số P(spam | x) chính xác, ta chỉ cần nó lớn hơn P(ham | x). Nói cách khác, phân loại chỉ cần thứ hạng giữa các lớp đúng, không cần độ lớn tuyệt đối đúng.

Giả định độc lập làm hỏng độ lớn nhưng thường không làm hỏng thứ hạng. Khi các từ tương quan, ví dụ "New" và "York" hay đi cặp, Naive Bayes đếm cùng một bằng chứng hai lần và trở nên quá tự tin, đẩy xác suất về sát 0 hoặc 1. Nhưng cả hai lớp thường bị đẩy cùng chiều, nên lớp đang dẫn vẫn dẫn. Kết quả là điểm hậu nghiệm của NB nổi tiếng là kém hiệu chỉnh, thường quá tự tin, trong khi quyết định phân loại lại đúng một cách đáng ngạc nhiên. Đó là lý do NB rất tốt để chọn lớp nhưng không nên dùng con số xác suất nó in ra như một mức tin cậy thật.

Ước lượng tham số bằng cách đếm

Việc huấn luyện chỉ gồm đếm. Tiên nghiệm P(y) bằng số mẫu lớp y chia tổng số mẫu. Khả năng cho một đặc trưng rời rạc bằng số mẫu lớp y có giá trị đó chia tổng số mẫu lớp y. Không có gradient descent, không có vòng lặp, không có siêu tham số (ngoài làm trơn). Chỉ đếm.

Tính trong miền log

Nhân nhiều xác suất nhỏ hơn 1 với nhau khiến tích trôi dần về 0 và máy tính làm tròn mất (underflow) khi số đặc trưng lớn. Giải pháp là làm việc với logarit. Vì log là hàm đơn điệu tăng, lớp làm cực đại tích cũng làm cực đại tổng các log, nên kết quả phân loại không đổi:

log P(y | x) ~ log P(y) + log P(x_1 | y) + ... + log P(x_d | y)

Cộng nhiều số ổn định hơn nhân nhiều số, nên mọi cài đặt thực tế đều dùng miền log.

Hiểu nhầm thường gặp

Đừng đọc con số P(y | x) mà Naive Bayes trả về như một xác suất đáng tin. Vì giả định độc lập đếm trùng các bằng chứng tương quan, NB thường in ra những giá trị sát 0.99 hay 0.01 ngay cả khi nó không thực sự chắc tới vậy. Con số đó đủ tốt để chọn lớp thắng, nhưng dùng nó làm ngưỡng tin cậy, ví dụ chỉ chặn thư khi xác suất spam trên 0.95, có thể dẫn tới quyết định sai. Muốn có xác suất đáng tin cần một bước hiệu chỉnh riêng.

Pipeline phân loại văn bản

Để biến văn bản thô thành đầu vào cho NB, ta đi qua mấy bước. Đầu tiên là tách từ (tokenization): cắt văn bản thành các đơn vị nhỏ, thường là từ; với tiếng Việt thường tách theo âm tiết hoặc dùng thư viện như underthesea, pyvi. Tiếp theo là chuẩn hóa: hạ chữ thường, loại bỏ stop words (các từ quá phổ biến ít mang nghĩa như "và", "của", "the"), bỏ dấu câu và số nếu cần.

Sau đó là biểu diễn vector. Cách phổ biến nhất là túi từ (bag-of-words): mỗi văn bản trở thành một vector đếm số lần xuất hiện của từng từ trong từ điển, bỏ qua thứ tự từ. Một biến thể tinh hơn là TF-IDF, vừa đếm tần suất từ trong văn bản vừa phạt những từ xuất hiện ở khắp mọi văn bản (ít có giá trị phân biệt). Cuối cùng ta huấn luyện Multinomial NB trên ma trận đếm này và đánh giá bằng accuracy, F1 macro, confusion matrix.

Ví dụ lọc thư rác bằng số

Giả sử tập huấn luyện có 5 email, 3 đặc trưng nhị phân là sự xuất hiện của các từ "money", "free", "meeting":

EmailmoneyfreemeetingLớp
e1110spam
e2100spam
e3010spam
e4001ham
e5001ham

Tiên nghiệm: P(spam) = 3/5 = 0.6P(ham) = 2/5 = 0.4.

Khả năng phía spam (đếm trong 3 email spam): P(money=1 | spam) = 2/3, P(free=1 | spam) = 2/3, P(meeting=1 | spam) = 0/3 = 0. Phía ham: P(money=1 | ham) = 0, P(free=1 | ham) = 0, P(meeting=1 | ham) = 2/2 = 1.

Email mới có money xuất hiện, free và meeting không, tức x = (money=1, free=0, meeting=0). Tính điểm spam (nhớ rằng P(free=0 | spam) = 1/3P(meeting=0 | spam) = 3/3):

spam: 0.6 * (2/3) * (1/3) * (3/3) ~ 0.133
ham : 0.4 * 0 * 1 * 0 = 0

Điểm ham bằng 0 chỉ vì một thừa số P(money=1 | ham) = 0. Kết luận: spam. Nhưng ví dụ này lộ ra một lỗ hổng nghiêm trọng: một từ chưa từng thấy trong một lớp sẽ kéo cả tích về 0, làm hỏng mọi đóng góp của các từ khác.

Làm trơn Laplace

Vấn đề xác suất bằng 0 xảy ra vì cách đếm thuần không phân biệt giữa "không bao giờ xảy ra" và "tình cờ chưa thấy trong tập huấn luyện hữu hạn". Cách khắc phục là làm trơn Laplace (add-alpha smoothing): thêm một lượng nhỏ alpha quan sát giả cho mỗi giá trị. Công thức trở thành đếm thực cộng alpha, chia cho tổng số mẫu lớp cộng alpha nhân số giá trị có thể của đặc trưng:

P(x_i = v | y) = (dem(y, x_i=v) + alpha) / (N_y + alpha * V_i)

Trong đó V_i là số giá trị có thể của đặc trưng i. Lấy alpha = 1 gọi là làm trơn Laplace chuẩn. Nhờ vậy không giá trị nào còn xác suất bằng 0.

Tính lại ví dụ trên với alpha = 1V_i = 2 (đặc trưng nhị phân có 2 giá trị):

P(money=1 | ham) = (0 + 1) / (2 + 1*2) = 1/4 = 0.25
P(free=1 | ham) = (0 + 1) / (2 + 1*2) = 0.25
P(meeting=1 | ham) = (2 + 1) / (2 + 1*2) = 3/4 = 0.75

Khi đó với email mới x = (1, 0, 0), điểm ham không còn bằng 0:

ham : 0.4 * 0.25 * 0.75 * 0.25 ~ 0.019
spam: 0.6 * 0.6 * 0.4 * 0.8 ~ 0.115

Vẫn phân loại là spam, nhưng giờ ham không bị loại hoàn toàn, cho phép thuật toán cân nhắc đúng đắn các trường hợp gần ranh giới. Tham số alpha thường được tinh chỉnh trên tập validation; các giá trị hay thử là 0.01, 0.1, 0.5, 1.0, 2.0. Nếu alpha quá lớn, các xác suất bị kéo về phân phối đều và mô hình mất khả năng phân biệt.

Mã nguồn Python

Đoạn mã sau cài đặt một bộ phân loại văn bản Naive Bayes từ đầu, chỉ dùng thư viện chuẩn. Nó đếm tần suất từ theo mô hình multinomial, áp dụng làm trơn Laplace và tính điểm trong miền log để tránh underflow.

import math
from collections import defaultdict, Counter


def tokenize(text):
# ha chu thuong va cat tho theo dau cach, bo ky tu khong phai chu/so
cleaned = "".join(ch.lower() if ch.isalnum() else " " for ch in text)
return cleaned.split()


class NaiveBayesText:
def __init__(self, alpha=1.0):
self.alpha = alpha
self.vocab = set()
self.log_prior = {} # log P(y) cho moi lop
self.word_counts = {} # word_counts[y][word] = so lan tu xuat hien
self.total_words = {} # tong so tu (ke ca lap) trong lop y

def fit(self, texts, labels):
n_docs = len(texts)
class_doc_count = Counter(labels)
self.classes = list(class_doc_count.keys())

# khoi tao bo dem cho moi lop
self.word_counts = {y: defaultdict(int) for y in self.classes}
self.total_words = {y: 0 for y in self.classes}

for text, y in zip(texts, labels):
for tok in tokenize(text):
self.vocab.add(tok)
self.word_counts[y][tok] += 1
self.total_words[y] += 1

# tien nghiem trong mien log
for y in self.classes:
self.log_prior[y] = math.log(class_doc_count[y] / n_docs)

def _log_likelihood(self, word, y):
# lam tron Laplace: (dem + alpha) / (tong tu lop + alpha * |V|)
v = len(self.vocab)
numerator = self.word_counts[y].get(word, 0) + self.alpha
denominator = self.total_words[y] + self.alpha * v
return math.log(numerator / denominator)

def predict(self, text):
tokens = [t for t in tokenize(text) if t in self.vocab]
best_class, best_score = None, -math.inf
for y in self.classes:
score = self.log_prior[y]
for tok in tokens:
score += self._log_likelihood(tok, y)
if score > best_score:
best_score, best_class = score, y
return best_class


# vi du nho
train_texts = [
"win a free iphone click now",
"free money offer limited",
"claim your free prize today",
"meeting agenda for monday",
"please review the project report",
"lunch meeting with the team",
]
train_labels = ["spam", "spam", "spam", "ham", "ham", "ham"]

clf = NaiveBayesText(alpha=1.0)
clf.fit(train_texts, train_labels)

print(clf.predict("free prize click now")) # mong doi: spam
print(clf.predict("project meeting on monday")) # mong doi: ham

Toàn bộ phần huấn luyện chỉ là đếm tần suất từ. Phần dự đoán cộng các log xác suất và chọn lớp có tổng lớn nhất. Trong thực tế, scikit-learn gói gọn pipeline này còn vài dòng với TfidfVectorizerMultinomialNB, nhưng cài tay như trên giúp bạn thấy rõ từng phép tính bên trong.

Bài tập thực hành

Bài 1. Áp dụng định lý Bayes cho tình huống: 1 phần trăm dân số có bệnh X. Xét nghiệm cho dương tính trên 95 phần trăm người bệnh và 5 phần trăm người không bệnh. Một người xét nghiệm dương. Tính xác suất họ thực sự bị bệnh.

Gợi ý

Đặt P(D) = 0.01, P(T | D) = 0.95, P(T | khong_D) = 0.05. Mẫu số là P(T) = 0.95 * 0.01 + 0.05 * 0.99. Lấy (0.95 * 0.01) / P(T), ra khoảng 0.16, tức chỉ khoảng 16 phần trăm dù xét nghiệm khá tốt. Lý do vẫn là base rate thấp.

Bài 2. Tính lại ví dụ thư rác 3 từ trong bài nhưng với alpha = 0.5 thay vì alpha = 1. Kết quả phân loại có thay đổi không?

Gợi ý

Thay vào công thức (dem + 0.5) / (N_y + 0.5 * 2), ví dụ P(money=1 | ham) = (0 + 0.5) / (2 + 1) = 0.5/3 ~ 0.167. Tính lại điểm cho cả hai lớp như trong bài. Spam vẫn thắng; alpha nhỏ hơn chỉ làm các xác suất ít bị kéo về phân phối đều hơn, không lật kết quả ở ví dụ này.

Bài 3. Mở rộng đoạn mã Python: thêm phương thức predict_proba trả về xác suất hậu nghiệm chuẩn hóa cho mỗi lớp thay vì chỉ trả về tên lớp.

Gợi ý

Sau khi tính score (đang ở miền log) cho từng lớp, dùng thủ thuật trừ giá trị log lớn nhất rồi lấy math.exp để tránh tràn số, sau đó chia mỗi giá trị cho tổng của chúng để được xác suất cộng lại bằng 1. Đây chính là phép softmax trên các log điểm.

Tự kiểm tra

Vì sao làm trơn Laplace lại cần thiết, và điều gì xảy ra nếu bỏ nó đi?

Không có làm trơn, một từ chưa từng xuất hiện trong một lớp sẽ có xác suất ước lượng bằng 0. Vì NB nhân tất cả các xác suất với nhau, chỉ một thừa số 0 cũng kéo cả tích của lớp đó về 0, bất kể các từ khác ủng hộ lớp đó mạnh tới đâu. Làm trơn thêm một lượng nhỏ quan sát giả cho mọi từ nên không xác suất nào còn bằng 0, giúp thuật toán cân nhắc công bằng.

Vì sao mọi cài đặt thực tế đều cộng các logarit thay vì nhân trực tiếp các xác suất?

Mỗi xác suất nhỏ hơn 1, nên nhân nhiều xác suất với nhau cho ra số cực nhỏ; khi số đặc trưng lớn, máy tính làm tròn nó về 0 (underflow) và làm hỏng phép so sánh. Lấy logarit biến tích thành tổng; vì log là hàm đơn điệu tăng, lớp có tổng log lớn nhất cũng chính là lớp có tích lớn nhất, nên kết quả phân loại không đổi mà phép tính lại ổn định về số học.

Giả định độc lập gần như luôn sai trên văn bản, vậy vì sao Naive Bayes vẫn phân loại tốt?

Vì phân loại chỉ cần thứ hạng giữa các lớp đúng, không cần xác suất tuyệt đối đúng. Giả định độc lập làm hỏng độ lớn của xác suất, thường khiến NB quá tự tin do đếm trùng các từ tương quan, nhưng nó thường đẩy mọi lớp cùng chiều nên lớp đang dẫn vẫn dẫn. Hệ quả là quyết định phân loại vẫn đúng một cách đáng ngạc nhiên, dù con số xác suất in ra kém hiệu chỉnh và không nên dùng làm mức tin cậy thật.

Câu hỏi tự kiểm

Kiểm tra nhanh: phân loại Naive Bayes0/4 đúngchưa trả lời
  1. 1Giả định "ngây thơ" của Naive Bayes phát biểu chính xác là gì?
  2. 2Vì sao Naive Bayes vẫn phân loại tốt dù giả định độc lập gần như luôn sai?
  3. 3Nên hiểu con số xác suất P(y|x) mà Naive Bayes in ra như thế nào?
  4. 4Làm trơn Laplace (cộng alpha) giải quyết vấn đề gì trong Naive Bayes?
Ghi nhớ

Naive Bayes kết hợp định lý Bayes với một giả định độc lập ngây thơ để biến phân phối nhiều chiều khó ước lượng thành tích của các xác suất một chiều đơn giản. Huấn luyện chỉ là đếm tần suất, làm trơn Laplace tránh xác suất bằng 0, và tính trong miền log tránh underflow. Dù giả định độc lập gần như luôn sai trên văn bản thật, NB vẫn là một baseline nhanh và mạnh cho phân loại văn bản, đáng chạy thử trước mọi mô hình phức tạp hơn.