Cùng xây dựng chương trình phân loại thư rác từ mô hình phân lớp Naive Bayes

Tóm tắt: Bài viết này sẽ dẫn dắt bạn từng bước xây dựng một bộ lọc thư rác (spam filter) đơn giản bằng Python, dựa trên nền tảng toán học của định lý Bayes. Chúng ta sẽ tìm hiểu tại sao công thức hoạt động trước khi bắt tay vào triển khai.

Câu chuyện bắt đầu: tại sao lại là Bayes?

Hãy tưởng tượng bạn nhận được một email với tiêu đề "Chúc mừng! Bạn đã trúng thưởng 1 triệu đô!". Não bạn ngay lập tức nhận ra đây là thư rác — vì từ "trúng thưởng" thường xuất hiện rất nhiều trong những thư rác mà bạn đã thấy trước đó.
Đó chính xác là cách Naive Bayes hoạt động: ta sẽ học từ dữ liệu quá khứ để đưa ra dự đoán về tương lai, dựa trên xác suất có điều kiện.

Phần 1 — Nền tảng toán học

1.1 Định lý Bayes

Trong bài toán phân loại thư rác (spam email), câu hỏi cốt lõi chúng ta muốn trả lời là: "Biết rằng email chứa các từ , xác suất đây là thư rác là bao nhiêu?"
Định lý Bayes cho ta công thức:
Công thức này nếu trình bày bằng lời thì có nghĩa là: xác suất một email có chứa một từ nào đó là spam bằng với tích của xác suất xuất hiện từ đó trong các email spam ta đã biết và xác suất email là spam trong toàn bộ các email, và chia cho xác suất xuất hiện của từ đó nói chung.
Một email có đến hàng chục, hàng trăm từ. Ta sẽ mở rộng công thức cho tập từ :
Ký hiệu toán học cho câu hỏi này là - đọc là "xác suất là spam, biết rằng email chứa các từ đó".
Mỗi thành phần có ý nghĩa như sau:
Ký hiệu
Tên gọi
Ý nghĩa
Xác suất hậu nghiệm
Xác suất email này là spam nếu biết là nó chứa tập từ ?
Hàm hợp lý
Nếu là spam, khả năng có tập từ là bao nhiêu?
Xác suất tiên nghiệm
Trong dataset (tập dữ liệu), bao nhiêu % là spam?
Bằng chứng
Xác suất tập từ này xuất hiện (bất kể spam hay không)

1.2 Giả định "Naive" (ngây thơ) — điều làm cho mô hình trở nên khả thi

Vấn đề là: làm sao tính được trong công thức khi có hàng ngàn từ khác nhau?
Áp dụng Quy tắc nhân mở rộng (Chain Rule), ta có thể khai triển biểu thức thành:
Biểu thức này cực kỳ phức tạp vì mỗi từ phụ thuộc vào tất cả các từ còn lại.
Giả định Naive Bayes giải quyết vấn đề này bằng một sự đơn giản hóa: ta giả sử các từ độc lập với nhau khi biết nhãn lớp. Tức là, sự xuất hiện của từ "trúng" không ảnh hưởng đến xác suất xuất hiện của từ "thưởng", miễn là ta đã biết đây là spam.
Với giả định này, vì là độc lập đôi một với tất cả các từ còn lại trong , ta có thể tách:
Ký hiệu là tích — nhân tất cả xác suất lại với nhau.
Giả định này có thực tế không? Giả định "naive" này không thực tế hoàn toàn - rõ ràng ai cũng thấy rằng từ "thư" và "điện tử" hay xuất hiện cùng nhau. Tuy nhiên, mô hình này giúp ta dễ tính toán và vẫn hoạt động rất tốt trong thực tế, nên đó là lý do nó vẫn được dùng rộng rãi.
Tương tự, là xác suất từ xuất hiện trong các email thông thường (ham email). Ta cũng biết rằng tổng xác suất của hai trường hợp phải bằng 1. Nói cách khác, nếu một thư có chứa tập từ nào đó, nếu nó không phải thư thường thì nó là thư rác và ngược lại:
Minh hoạ: cách tính P(wᵢ|spam)

1.3 Loại bỏ mẫu số khó tính — dùng tỉ lệ R

Xét mẫu số của biểu thức trên. Mẫu số này rất khó tính trực tiếp vì phải xem xét tất cả email trong tập dữ liệu ta có. Nhưng ta có một mẹo thông minh: thay vì tính trực tiếp xác suất, hãy tính tỉ lệ giữa hamspam.
Ta định nghĩa tỉ lệ là xác suất ham chia cho xác suất spam:
Áp dụng Bayes cho cả tử và mẫu, đồng thời dùng giả định Naive:
Mẫu số giờ đã triệt tiêu nhau. Lý do ta lấy tỉ lệ là để triệt tiêu đi mẫu số khó tính toán.
Bước tiếp theo ta cần làm là tính được từ :
Ta biết rằng , nên có thể viết:
Chia cả tử và mẫu cho :
Thay vào:
Nhìn vào công thức trên, ta thấy một cách trực quan rằng khả năng để một email là spam sẽ tỷ lệ nghịch với giá trị của . Nói cách khác, nếu , tức thì email chắc chắn là spam. Còn khi càng lớn, , email đó không phải là spam nữa.
Minh hoạ: P(spam) thay đổi theo giá trị của R ra sao
Như vậy là ta đã hoàn thành việc xây dựng công thức tính xác suất một email là spam dựa trên các thông tin có thể lấy được và tính toán được. Tiếp theo ta sẽ đi đến phần triển khai.

Phần 2 — Bộ dữ liệu và cài đặt ban đầu

Để triển khai bài toán này, ta cần có trước một bộ dữ liệu nhiều ví dụ về thư bình thường và thư rác. Bạn có thể tìm kiếm những dữ liệu như vậy trên các website cung cấp dataset cho việc học Data Science và Machine Learning, và biến đổi chúng thành format được mô tả dưới đây để có thể sử dụng.
Bộ dữ liệu chúng ta sẽ dùng trong ví dụ này là fraud_email_.csv, lấy từ Kaggle — Fraud Email Dataset là một tập hợp các email được gán nhãn:
  • Class = 1: thư rác (spam)
  • Class = 0: thư thường (ham)
Cột Text chứa nội dung đầy đủ của email.
Dưới đây là ví dụ minh hoạ một số dòng trong dataset:
Class
Text (trích đoạn đầu)
1
Dear Friend, I am Mr. Kofi Boateng, a staff of a reputable bank here in Ghana. I am writing to solicit your assistance in the transfer of $21.5 Million USD...
1
CONGRATULATIONS! You have been selected as the winner of the UK National Lottery. To claim your prize of £850,000 please contact our claims agent immediately...
0
Hi John, just a reminder that our team meeting is scheduled for Thursday at 10am. Please review the Q3 report beforehand and bring your notes. Best regards...
0
Dear valued customer, your monthly account statement for October is now available. Please log in to your account portal to view your transactions...
1
URGENT NOTICE: Your account has been compromised. Verify your password and banking details immediately by clicking the link below to avoid suspension...
Chú thích: Class = 1 là thư rác (spam), Class = 0 là thư thường (ham). Cột Text chứa toàn bộ nội dung email — ở đây chỉ trích đoạn mở đầu để minh hoạ.
1import pandas as pd
2import re, math
3
4# Tải dataset
5fraud_email = pd.read_csv('fraud_email_.csv', on_bad_lines='skip')
6
7# Tách danh sách spam và ham
8spam_list = fraud_email[(fraud_email['Class'] == 1)]['Text'].tolist()
9ham_list  = fraud_email[(fraud_email['Class'] == 0)]['Text'].tolist()
10
11# Số lượng mẫu trong dataset
12dataset_row_count = len(fraud_email.axes[0])
13
14# Xác suất tiên nghiệm: P(spam) và P(ham) trong dataset
15prob_ham_in_dataset  = len(ham_list)  / dataset_row_count
16prob_spam_in_dataset = len(spam_list) / dataset_row_count
17
18print('Khởi tạo hoàn tất!')
Tại sao cần tính xác suất tiên nghiệm prob_ham_in_datasetprob_spam_in_dataset? Vì chúng ta cần tính trong công thức , và để tính chúng ta cần 2 xác xuất tiên nghiệm này (xem công thức ). Một cách trực quan, nếu 90% email trong dataset là ham, th ì rõ ràng email này nên "thiên về" ham ngay từ đầu (tức là tỷ lệ P(ham)/P(spam) trong công thức (2)), trước khi nhìn vào nội dung (tức là các thừa số còn lại trong công thức (2)).

Phần 3 — Các hàm xây dựng mô hình

Trước khi đi vào từng hàm, hãy nhìn tổng quan luồng xử lý mà ta sẽ xây dựng:
Nhận văn bản → tách từ → lọc từ ít thông tin → tính xác suất từng từ trong spam/ham → tính tỉ lệ → trả về xác suất spam từ .
Mỗi bước dưới đây tương ứng với một hàm trong đoạn code.

3.1 Tách từ khỏi văn bản

1def extract_words(text):
2    # Tách theo nhiều ký tự phân cách khác nhau
3    split_list = re.split(r'; |;|, |,|: |:|	|\*|\n|! | |\.|\.', str(text))
4    # Đưa về chữ thường
5    word_list = [word.lower() for word in split_list]
6    # Loại bỏ từ trùng lặp
7    word_list = list(dict.fromkeys(word_list))
8    # Loại bỏ chuỗi rỗng
9    if '' in word_list:
10        word_list.remove('')
11    return word_list
Hàm này nhận một chuỗi văn bản và trả về danh sách các từ duy nhất (không trùng), đã được chuẩn hóa về chữ thường.

3.2 Tính

1def prob_in_set(word, mail_list):
2    """Tính xác suất từ 'word' xuất hiện trong tập email mail_list."""
3    count = 0
4    for mail in mail_list:
5        if word in str(mail).lower():
6            count += 1
7    return count / len(mail_list)
Hàm này đi qua toàn bộ danh sách email, đếm có bao nhiêu email chứa từ đó, và chia cho tổng số email. Kết quả là xác suất xuất hiện của từ trong tập spam hoặc ham , tuỳ vào danh sách truyền vào.

3.3 Tính tích — và vấn đề chia cho 0

1def product_of_num_list(num_list):
2    result = 1
3    for num in num_list:
4        result *= float(num + 10)  # +10 là hiệu chỉnh Laplace
5    return result
Tại sao cộng thêm 10? Đây là biến thể của hiệu chỉnh Laplace (Laplace smoothing) — một kỹ thuật để tránh trường hợp xác suất bằng 0. Nếu một từ chưa từng xuất hiện trong spam, , khiến toàn bộ tích bằng 0 (mô hình bị "quá khớp").
Con số 10 được chọn theo thực nghiệm: quá nhỏ thì mô hình vẫn bị quá khớp, quá lớn thì kết quả trả về vô cực (overflow).
Lưu ý: Trong thực tế, người ta thường dùng để chuyển tích thành tổng, tránh underflow khi nhân nhiều số nhỏ lại với nhau. Nhưng ở bài này chúng ta giữ nguyên để dễ theo dõi.

Có thể bạn quan tâm?