96 KiB
96 KiB
In [ ]:
import time
import warnings
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
pd.set_option("display.max_colwidth", 120)
pd.set_option("display.width", 200)
RANDOM_STATE = 42 # "hạt giống" ngẫu nhiên: cố định để lần nào chạy cũng ra cùng kết quả
# File gốc: mỗi dòng = nhãn <TAB> nội dung tin nhắn. sep="\t" = cột cách nhau bởi dấu TAB;
# quoting=3 = KHÔNG coi dấu nháy " là đặc biệt (tin nhắn thật có rất nhiều dấu nháy);
# encoding="utf-8" = bảng mã của file (đọc sai bảng mã thì ký tự £ thành "£")
du_lieu = pd.read_csv("/opt/csai/labs/day4_phishing/SMSSpamCollection", sep="\t", header=None,
names=["nhan", "noi_dung"], quoting=3, encoding="utf-8")
# 📦 du_lieu = bảng 2 cột: nhan (ham / spam), noi_dung (tin nhắn)
print("du_lieu.shape =", du_lieu.shape, "-> (số tin nhắn, số cột)")
dem = du_lieu.nhan.value_counts() # đếm số tin theo từng nhãn
print(f"Có {len(du_lieu):,} tin: {dem['ham']:,} tin thường (ham) + {dem['spam']:,} tin rác (spam)")
print(f"Tỉ lệ spam: {(du_lieu.nhan == 'spam').mean():.1%}")
print("\n3 tin THẬT (ham) và 3 tin RÁC (spam) đầu tiên:")
print(pd.concat([du_lieu[du_lieu.nhan == "ham"].head(3), du_lieu[du_lieu.nhan == "spam"].head(3)]).to_string())In [ ]:
from sklearn.model_selection import train_test_split
so_ban_sao = du_lieu.noi_dung.duplicated().sum() # duplicated() = True cho tin trùng với một tin phía trên
print("Số tin là bản sao của tin khác:", so_ban_sao, "| trong đó spam:", du_lieu[du_lieu.nhan == "spam"].noi_dung.duplicated().sum())
# Thử chia ngẫu nhiên KHI CÒN bản sao: bao nhiêu tin đề thi đã có sẵn trong phần học?
hoc_tam, thi_tam = train_test_split(du_lieu.index, test_size=0.25, random_state=RANDOM_STATE, stratify=du_lieu.nhan)
lo_de = du_lieu.noi_dung[thi_tam].isin(set(du_lieu.noi_dung[hoc_tam])).sum()
print(f"Nếu không bỏ bản sao: {lo_de} tin trong đề thi ĐÃ CÓ trong phần học (lộ đề)")
tin = du_lieu.drop_duplicates("noi_dung").reset_index(drop=True) # giữ 1 bản cho mỗi nội dung
y = (tin.nhan == "spam").astype(int) # nhãn dạng số: 1 = spam, 0 = ham
# 📦 tin = bảng tin nhắn KHÔNG trùng (dùng cho cả bài) · y = nhãn 0/1 cùng thứ tự với tin
print(f"\nSau khi bỏ bản sao: {len(tin)} tin, {y.sum()} spam ({y.mean():.1%})")In [ ]:
# ✏️ SỬA: danh sách từ khóa. Thêm / bớt rồi chạy lại để xem chặn nhầm / bỏ lọt thay đổi thế nào
TU_KHOA = ["free", "win", "winner", "prize", "call now", "claim", "urgent", "cash", "txt", "reply"]
tu_khoa_thuong = [k.lower() for k in TU_KHOA] # đổi từ khóa sang chữ thường (ai gõ "Divorce" cũng vẫn khớp)
chu_thuong = tin.noi_dung.str.lower() # đổi cả nội dung tin sang chữ thường
bi_chan = chu_thuong.apply(lambda t: any(k in t for k in tu_khoa_thuong)).astype(int) # 1 = nội dung chứa ít nhất 1 từ khóa
chan_nham = ((bi_chan == 1) & (y == 0)).sum()
bo_lot = ((bi_chan == 0) & (y == 1)).sum()
print(f"Luật {len(TU_KHOA)} từ khóa: chặn {bi_chan.sum()} tin | CHẶN NHẦM {chan_nham} tin thật | BỎ LỌT {bo_lot} tin rác (trên {y.sum()} tin rác)")
print("\nVí dụ tin THẬT bị chặn nhầm:")
print(tin.noi_dung[(bi_chan == 1) & (y == 0)].head(4).to_string())
print("\nVí dụ tin RÁC lọt qua:")
print(tin.noi_dung[(bi_chan == 0) & (y == 1)].head(4).to_string())In [ ]:
from sklearn.feature_extraction.text import TfidfVectorizer
# ✏️ SỬA: 3 câu thử (tiếng Anh, như dữ liệu; tiếng Việt: xem 2.1b)
cau = ["WIN a FREE prize now, call to claim", "are you free now? can i call you", "see you at home tonight"]
thu = TfidfVectorizer()
bang = thu.fit_transform(cau) # học từ điển từ 3 câu, biến mỗi câu thành 1 hàng số
print("Từ điển:", list(thu.get_feature_names_out()))
print(pd.DataFrame(bang.toarray().round(2), columns=thu.get_feature_names_out(), index=["câu 1", "câu 2", "câu 3"]).to_string())In [ ]:
import unicodedata
# ✏️ SỬA: thử tin nhắn tiếng Việt của bạn
cau_viet = ["Chúc mừng bạn trúng thưởng 50 triệu, bấm link để nhận ngay",
"Tối nay mình ở nhà, mai chuyển khoản cho bạn nhé",
"Tài khoản của bạn bị khoá, chuyển khoản phí 200k để mở lại"]
tach_don = TfidfVectorizer().fit(cau_viet) # mặc định: mỗi âm tiết là một "từ"
print("Từ điển (từ đơn):", list(tach_don.get_feature_names_out()))
tach_cap = TfidfVectorizer(ngram_range=(1, 2)).fit(cau_viet) # thêm cặp 2 âm tiết liền nhau
print("\nVài cặp từ khi ngram_range=(1, 2):", [w for w in tach_cap.get_feature_names_out() if " " in w][:12])
# Cùng chữ "chuyển khoản" nhưng 3 cách gõ: dựng sẵn (NFC), tổ hợp (NFD), không dấu
ba_kieu = ["chuyển khoản", unicodedata.normalize("NFD", "chuyển khoản"), "chuyen khoan"]
print("\n3 kiểu gõ ->", list(TfidfVectorizer().fit(ba_kieu).get_feature_names_out()))
print("Sau khi chuẩn hoá NFC ->", list(TfidfVectorizer().fit([unicodedata.normalize("NFC", c) for c in ba_kieu]).get_feature_names_out()))In [ ]:
# ✏️ SỬA: tỉ lệ phần thi (0.25 = 25% để thi, 75% để học). stratify=y giữ cùng tỉ lệ spam ở 2 phần
TI_LE_THI = 0.25
vi_tri_hoc, vi_tri_thi = train_test_split(tin.index, test_size=TI_LE_THI, random_state=RANDOM_STATE, stratify=y)
print(f"Bảng tin: {len(tin)} tin -> phần học {len(vi_tri_hoc)} tin ({y[vi_tri_hoc].sum()} spam) + phần thi {len(vi_tri_thi)} tin ({y[vi_tri_thi].sum()} spam)")
print("5 vị trí đầu của phần học:", list(vi_tri_hoc[:5]), "| của phần thi:", list(vi_tri_thi[:5]))
tfidf = TfidfVectorizer()
X_hoc = tfidf.fit_transform(tin.noi_dung[vi_tri_hoc]) # fit = HỌC từ điển từ phần học; transform = đổi thành số
X_thi = tfidf.transform(tin.noi_dung[vi_tri_thi]) # phần thi CHỈ transform bằng từ điển đã học (không fit lại)
y_hoc, y_thi = y[vi_tri_hoc], y[vi_tri_thi]
tu_dien = tfidf.get_feature_names_out()
# 📦 X_hoc, X_thi = ma trận số (mỗi hàng 1 tin, mỗi cột 1 từ) · y_hoc, y_thi = nhãn · tu_dien = danh sách từ
print("X_hoc.shape =", X_hoc.shape, "| X_thi.shape =", X_thi.shape, "-> (số tin, số từ trong từ điển)")
print(f"Phần thi: {len(y_thi)} tin, {y_thi.sum()} spam")
print(f"Mốc 'lười': đoán mọi tin là ham -> đúng {(y_thi == 0).mean():.1%} — nhưng không bắt được tin rác nào")In [ ]:
from sklearn.linear_model import Perceptron
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix
def do_luong(ten, du_doan):
"""Trả về 1 hàng kết quả: accuracy, precision, recall, F1, số chặn nhầm (FP), số bỏ lọt (FN)."""
tn, fp, fn, tp = confusion_matrix(y_thi, du_doan).ravel()
return {"mô hình": ten, "accuracy": accuracy_score(y_thi, du_doan), "precision": precision_score(y_thi, du_doan, zero_division=0), # zero_division=0: mô hình không chặn tin nào -> precision tính là 0
"recall": recall_score(y_thi, du_doan), "F1": f1_score(y_thi, du_doan), "chặn nhầm (FP)": fp, "bỏ lọt (FN)": fn}
perceptron = Perceptron(random_state=RANDOM_STATE).fit(X_hoc, y_hoc)
# 📦 do_luong(ten, du_doan) = hàm chấm 1 mô hình trên phần thi (dùng lại ở mọi phần)
print(pd.DataFrame([do_luong("Perceptron", perceptron.predict(X_thi))]).round(3).to_string(index=False))
trong_so = perceptron.coef_[0] # 1 trọng số cho mỗi từ trong từ điển
print("\n8 từ trọng số DƯƠNG lớn nhất (kéo về spam):", list(tu_dien[np.argsort(trong_so)[-8:][::-1]]))
print("8 từ trọng số ÂM lớn nhất (kéo về ham) :", list(tu_dien[np.argsort(trong_so)[:8]]))
print("bias =", round(perceptron.intercept_[0], 3))
# Tự tính "điểm" của 1 tin bằng tay theo công thức, so với máy
i = 0
x = X_thi[i] # hàng TF-IDF của tin thi đầu tiên
diem_tay = x.toarray()[0] @ trong_so + perceptron.intercept_[0] # @ = nhân từng cặp rồi cộng lại
print(f"\nTin: {tin.noi_dung[vi_tri_thi[i]]!r}")
print(f"Điểm tự tính = {diem_tay:.3f} | máy tính (decision_function) = {perceptron.decision_function(x)[0]:.3f} -> {'SPAM' if diem_tay > 0 else 'ham'}")In [ ]:
from sklearn.svm import SVC
# Dữ liệu GIẢ 2 chiều, chỉ để vẽ minh hoạ: 20 chấm "ham" quanh (2, 2), 20 chấm "spam" quanh (5, 4.5)
ngau_nhien_gia = np.random.RandomState(3)
P = np.vstack([ngau_nhien_gia.normal([2, 2], 0.7, (20, 2)), ngau_nhien_gia.normal([5, 4.5], 0.7, (20, 2))])
nhan_gia = np.array([0] * 20 + [1] * 20)
per_gia = Perceptron(random_state=0, max_iter=1000).fit(P, nhan_gia)
svm_gia = SVC(kernel="linear", C=1000).fit(P, nhan_gia) # C lớn = không cho chấm nào lọt vào vùng đệm
def gan_nhat(w, b):
"""Khoảng cách từ ranh giới w·x + b = 0 tới chấm gần nhất."""
return np.min(np.abs(P @ w + b)) / np.linalg.norm(w)
xs = np.linspace(0, 7, 100)
hinh, o = plt.subplots(1, 2, figsize=(12, 5), sharey=True)
for truc, ten, m in zip(o, ["Perceptron", "SVM"], [per_gia, svm_gia]):
w, b = m.coef_[0], m.intercept_[0]
truc.scatter(P[:, 0], P[:, 1], c=np.where(nhan_gia == 1, "red", "blue"), s=30)
truc.plot(xs, -(w[0] * xs + b) / w[1], "k-", label="ranh giới")
if ten == "SVM":
for lech in (-1, 1): # mép vùng đệm: w·x + b = ±1
truc.plot(xs, -(w[0] * xs + b + lech) / w[1], "k--", lw=1)
truc.scatter(m.support_vectors_[:, 0], m.support_vectors_[:, 1], s=220, facecolors="none",
edgecolors="green", lw=2, label="support vector")
truc.set_title(f"{ten}: ranh giới cách chấm gần nhất {gan_nhat(w, b):.2f}")
truc.set_xlim(0, 7); truc.set_ylim(0, 7); truc.legend(loc="upper left")
plt.tight_layout(); plt.show()In [ ]:
from sklearn.svm import LinearSVC, SVC
svm = LinearSVC(random_state=RANDOM_STATE).fit(X_hoc, y_hoc)
print(pd.DataFrame([do_luong("SVM (tuyến tính)", svm.predict(X_thi))]).round(3).to_string(index=False))
# Đếm support vectors: SVC(kernel="linear") cùng ý tưởng, cho biết các tin "giữ ranh giới"
svc = SVC(kernel="linear").fit(X_hoc, y_hoc)
so_sv = svc.n_support_ # [số tin ham, số tin spam] là support vector
print(f"\nSupport vectors: {so_sv[0]} tin ham + {so_sv[1]} tin spam = {so_sv.sum()} / {X_hoc.shape[0]} tin học ({so_sv.sum() / X_hoc.shape[0]:.0%})")In [ ]:
# 2 đặc trưng đơn giản, nén bằng log1p cho dễ nhìn (số rất lớn bị co lại)
hai_dac_trung = np.log1p(pd.DataFrame({"do_dai": tin.noi_dung.str.len(), "so_chu_so": tin.noi_dung.str.count(r"\d")}))
A_hoc, A_thi = hai_dac_trung.loc[vi_tri_hoc], hai_dac_trung.loc[vi_tri_thi]
# Lưới điểm phủ cả hình, để tô màu vùng đoán
xx, yy = np.meshgrid(np.linspace(0, 6.5, 300), np.linspace(0, 4.2, 300))
luoi = pd.DataFrame({"do_dai": xx.ravel(), "so_chu_so": yy.ravel()})
hinh, o = plt.subplots(1, 2, figsize=(14, 5))
for truc, kernel in zip(o, ["linear", "rbf"]):
m = SVC(kernel=kernel, C=1, gamma="scale").fit(A_hoc, y_hoc)
truc.contourf(xx, yy, m.predict(luoi).reshape(xx.shape), alpha=0.25, cmap="coolwarm")
mau = np.where(y_thi == 1, "red", "blue")
truc.scatter(A_thi.do_dai, A_thi.so_chu_so, c=mau, s=6, alpha=0.5)
p = m.predict(A_thi)
truc.set_title(f"kernel='{kernel}': đúng {accuracy_score(y_thi, p):.1%}, recall {recall_score(y_thi, p):.1%}")
truc.set_xlabel("log(1 + độ dài tin)"); truc.set_ylabel("log(1 + số chữ số)")
plt.tight_layout(); plt.show()In [ ]:
diem_vd = np.array([-6, -3, -1, 0, 1, 3, 6])
print(pd.DataFrame({"điểm": diem_vd, "xác suất spam = 1/(1+e^-điểm)": (1 / (1 + np.exp(-diem_vd))).round(3)}).to_string(index=False))
z = np.linspace(-8, 8, 200)
plt.figure(figsize=(7, 3)); plt.plot(z, 1 / (1 + np.exp(-z))); plt.axhline(0.5, ls="--", c="gray")
plt.xlabel("điểm"); plt.ylabel("xác suất spam"); plt.title("Sigmoid: điểm âm -> gần 0, điểm 0 -> 50%, điểm dương -> gần 1"); plt.show()In [ ]:
from sklearn.linear_model import LogisticRegression
lr_mac_dinh = LogisticRegression(max_iter=1000).fit(X_hoc, y_hoc)
logistic = LogisticRegression(max_iter=1000, class_weight="balanced").fit(X_hoc, y_hoc)
# 📦 logistic = Logistic Regression có cân bằng lớp (dùng ở các phần sau)
print(pd.DataFrame([do_luong("Logistic mặc định", lr_mac_dinh.predict(X_thi)),
do_luong("Logistic class_weight='balanced'", logistic.predict(X_thi))]).round(3).to_string(index=False))In [ ]:
xac_suat = logistic.predict_proba(X_thi)[:, 1] # cột [:, 1] = xác suất là spam của từng tin thi
# ✏️ SỬA: 2 ngưỡng phân luồng
NGUONG_CACH_LY, NGUONG_CHAN = 0.5, 0.9
lan = np.where(xac_suat >= NGUONG_CHAN, "3. CHẶN", np.where(xac_suat >= NGUONG_CACH_LY, "2. CÁCH LY (người xem)", "1. CHO QUA"))
bang_lan = pd.DataFrame({"làn": lan, "là spam thật": y_thi.to_numpy()}).groupby("làn")["là spam thật"].agg(["size", "sum"])
bang_lan.columns = ["số tin", "trong đó spam thật"]
# 📦 xac_suat = xác suất spam của từng tin thi · lan = làn được xếp
print(bang_lan.to_string())
print("\nVí dụ 3 tin ở làn CÁCH LY:")
for nd, p_ in list(zip(tin.noi_dung[vi_tri_thi][lan == "2. CÁCH LY (người xem)"], xac_suat[lan == "2. CÁCH LY (người xem)"]))[:3]:
print(f" {p_:.0%} | {nd}")In [ ]:
# ✏️ SỬA: từ muốn soi (viết thường), ví dụ "prize", "call", "home", "love"
TU = "free"
co_tu = tin.noi_dung[vi_tri_hoc].str.lower().str.contains(rf"\b{TU}\b") # \b = ranh giới từ (để "free" không khớp "freedom")
so_spam, so_ham = int((co_tu & (y_hoc == 1)).sum()), int((co_tu & (y_hoc == 0)).sum())
print(f"Trong phần học có {co_tu.sum()} tin chứa '{TU}': {so_spam} spam, {so_ham} ham")
print(f"-> nếu chỉ dựa vào từ này: xác suất spam = {so_spam} / {so_spam + so_ham} = {so_spam / max(so_spam + so_ham, 1):.0%}")
print(f"(trong khi tỉ lệ spam chung chỉ {y_hoc.mean():.0%})")In [ ]:
from sklearn.naive_bayes import MultinomialNB
naive_bayes = MultinomialNB().fit(X_hoc, y_hoc)
print(pd.DataFrame([do_luong("Naive Bayes", naive_bayes.predict(X_thi))]).round(3).to_string(index=False))
# Từ "tố cáo" mạnh nhất = từ có (xác suất xuất hiện trong spam) / (trong ham) lớn nhất -> hiệu 2 log xác suất
ti_so = naive_bayes.feature_log_prob_[1] - naive_bayes.feature_log_prob_[0]
print("\n12 từ nghiêng về SPAM nhất:", list(tu_dien[np.argsort(ti_so)[-12:][::-1]]))
print("8 từ nghiêng về HAM nhất :", list(tu_dien[np.argsort(ti_so)[:8]]))
# Tốc độ học: lặp 5 lần lấy thời gian nhỏ nhất cho công bằng
def thoi_gian_hoc(mo_hinh):
return min((lambda t0: (mo_hinh.fit(X_hoc, y_hoc), time.perf_counter() - t0)[1])(time.perf_counter()) for _ in range(5))
print("\nThời gian học trên", X_hoc.shape[0], "tin:")
for ten, m in [("Naive Bayes", MultinomialNB()), ("Perceptron", Perceptron(random_state=RANDOM_STATE)),
("SVM", LinearSVC(random_state=RANDOM_STATE)), ("Logistic", LogisticRegression(max_iter=1000, class_weight="balanced"))]:
print(f" {ten:12s} {thoi_gian_hoc(m) * 1000:6.1f} ms")In [ ]:
cac_mo_hinh = {"Perceptron": perceptron, "SVM": svm, "Logistic": logistic, "Naive Bayes": naive_bayes}
phieu = pd.DataFrame({ten: m.predict(X_thi) for ten, m in cac_mo_hinh.items()}) # mỗi cột = phiếu của 1 mô hình
sai = phieu.ne(y_thi.to_numpy(), axis=0) # True ở chỗ mô hình đó đoán sai
print("Số tin mỗi mô hình đoán SAI:", sai.sum().to_dict())
print("Số tin mà các mô hình KHÔNG thống nhất:", int((phieu.nunique(axis=1) > 1).sum()), "/", len(phieu))
print("Số tin mà CẢ 4 cùng sai:", int(sai.all(axis=1).sum()))In [ ]:
from sklearn.ensemble import VotingClassifier
from sklearn.calibration import CalibratedClassifierCV
# ✏️ SỬA: trọng số phiếu [SVM, Logistic, Naive Bayes]. Thử [1, 1, 1], [1, 2, 1], [3, 1, 1]
TRONG_SO = [2, 1, 1]
def hoi_dong(kieu, trong_so=None):
thanh_vien = [("svm", CalibratedClassifierCV(LinearSVC(random_state=RANDOM_STATE), cv=5) if kieu == "soft" else LinearSVC(random_state=RANDOM_STATE)),
("logistic", LogisticRegression(max_iter=1000, class_weight="balanced")),
("naive_bayes", MultinomialNB())]
return VotingClassifier(thanh_vien, voting=kieu, weights=trong_so).fit(X_hoc, y_hoc)
hd_hard, hd_soft, hd_trong_so = hoi_dong("hard"), hoi_dong("soft"), hoi_dong("soft", TRONG_SO)
# 📦 hd_trong_so = hội đồng soft voting có trọng số (dùng ở Phần 9, 10)
bang_kq = pd.DataFrame([do_luong(ten, m.predict(X_thi)) for ten, m in
[("SVM (một mình)", svm), ("Hội đồng hard", hd_hard), ("Hội đồng soft, phiếu đều", hd_soft),
(f"Hội đồng soft, trọng số {TRONG_SO}", hd_trong_so)]])
bang_kq.round(3).set_index("mô hình")In [ ]:
tong_hop = pd.DataFrame([
do_luong("Đoán mọi tin là ham ('lười')", np.zeros(len(y_thi), dtype=int)),
do_luong(f"Luật {len(TU_KHOA)} từ khóa (Phần 1)", bi_chan[vi_tri_thi].to_numpy()),
do_luong("Perceptron", perceptron.predict(X_thi)),
do_luong("SVM", svm.predict(X_thi)),
do_luong("Logistic (balanced)", logistic.predict(X_thi)),
do_luong("Naive Bayes", naive_bayes.predict(X_thi)),
do_luong(f"Hội đồng trọng số {TRONG_SO}", hd_trong_so.predict(X_thi)),
])
tong_hop.round(3).set_index("mô hình")In [ ]:
from sklearn.metrics import ConfusionMatrixDisplay
mt = confusion_matrix(y_thi, hd_trong_so.predict(X_thi))
ConfusionMatrixDisplay(mt, display_labels=["ham", "spam"]).plot(cmap="Blues")
plt.title("Hội đồng có trọng số — hàng: sự thật, cột: máy đoán"); plt.show()
tn, fp, fn, tp = mt.ravel()
print(f"Cho qua đúng (TN) {tn} | CHẶN NHẦM (FP) {fp} | BỎ LỌT (FN) {fn} | bắt đúng (TP) {tp}")
print(f"precision = TP / (TP + FP) = {tp} / {tp + fp} = {tp / (tp + fp):.3f}")
print(f"recall = TP / (TP + FN) = {tp} / {tp + fn} = {tp / (tp + fn):.3f}")
print("\n3 tin RÁC bị bỏ lọt:")
du_doan_hd = hd_trong_so.predict(X_thi)
print(tin.noi_dung[vi_tri_thi][(y_thi == 1).to_numpy() & (du_doan_hd == 0)].head(3).to_string())
print("\nTin THẬT bị chặn nhầm:")
print(tin.noi_dung[vi_tri_thi][(y_thi == 0).to_numpy() & (du_doan_hd == 1)].head(4).to_string())In [ ]:
# ✏️ SỬA: các ngưỡng muốn thử (trên xác suất của Logistic ở 6.3)
CAC_NGUONG = [0.9, 0.7, 0.5, 0.3, 0.1]
dong = []
for ng in CAC_NGUONG:
p = (xac_suat >= ng).astype(int)
tn_, fp_, fn_, tp_ = confusion_matrix(y_thi, p).ravel()
dong.append({"ngưỡng": ng, "precision": tp_ / max(tp_ + fp_, 1), "recall": tp_ / (tp_ + fn_), "chặn nhầm (FP)": fp_, "bỏ lọt (FN)": fn_})
bang_nguong = pd.DataFrame(dong).set_index("ngưỡng")
bang_nguong[["precision", "recall"]].plot(marker="o", figsize=(7, 3), title="Hạ ngưỡng: recall tăng, precision giảm")
plt.gca().invert_xaxis(); plt.show()
bang_nguong.round(3)In [ ]:
# ✏️ SỬA: đoạn "từ an toàn" kẻ tấn công chèn vào cuối tin rác
CAU_CHEN = " ok see you later at home lor going to sleep now love you dear"
tin_rac_thi = tin.noi_dung[vi_tri_thi][(y_thi == 1).to_numpy()]
X_goc, X_bi_chen = tfidf.transform(tin_rac_thi), tfidf.transform(tin_rac_thi + CAU_CHEN)
dong = []
for ten, m in list(cac_mo_hinh.items()) + [(f"Hội đồng trọng số {TRONG_SO}", hd_trong_so)]:
dong.append({"mô hình": ten, "bắt được TRƯỚC": m.predict(X_goc).mean(), "bắt được SAU khi chèn": m.predict(X_bi_chen).mean()})
tan_cong = pd.DataFrame(dong).set_index("mô hình")
tan_cong.plot(kind="barh", figsize=(8, 3.5), title=f"Tỉ lệ tin rác bị bắt ({len(tin_rac_thi)} tin rác)")
plt.xlabel("tỉ lệ bắt được"); plt.show()
print("Ví dụ tin rác sau khi bị chèn:", repr(tin_rac_thi.iloc[0] + CAU_CHEN))
tan_cong.round(3)In [ ]:
# ✏️ SỬA: câu chèn mà PHÒNG THỦ đoán trước để tập (khác câu kẻ tấn công dùng ở 10.1)
CAU_TAP = " hey dear how was your day see you at dinner tonight ok"
tin_rac_hoc = tin.noi_dung[vi_tri_hoc][(y_hoc == 1).to_numpy()]
noi_dung_moi = pd.concat([tin.noi_dung[vi_tri_hoc], tin_rac_hoc + CAU_TAP]) # thêm bản tin rác đã chèn
nhan_moi = pd.concat([y_hoc, pd.Series(1, index=tin_rac_hoc.index)]) # tất cả bản thêm đều là spam
tfidf_dk = TfidfVectorizer()
X_hoc_dk = tfidf_dk.fit_transform(noi_dung_moi)
dong = []
for ten, m in [("Naive Bayes", MultinomialNB()), ("SVM", LinearSVC(random_state=RANDOM_STATE)),
("Logistic", LogisticRegression(max_iter=1000, class_weight="balanced"))]:
m.fit(X_hoc_dk, nhan_moi)
p_thi = m.predict(tfidf_dk.transform(tin.noi_dung[vi_tri_thi]))
dong.append({"mô hình (học lại)": ten,
"bắt tin rác bị chèn (câu 10.1)": m.predict(tfidf_dk.transform(tin_rac_thi + CAU_CHEN)).mean(),
"F1 trên đề thi thường": f1_score(y_thi, p_thi), "chặn nhầm (FP)": int(((p_thi == 1) & (y_thi == 0).to_numpy()).sum())})
pd.DataFrame(dong).round(3).set_index("mô hình (học lại)")In [ ]:
from pathlib import Path
THU_MUC_RA = Path.home() / "lab" / "out_lab4"
THU_MUC_RA.mkdir(parents=True, exist_ok=True)
hd = tong_hop.set_index("mô hình").loc[f"Hội đồng trọng số {TRONG_SO}"]
luat = tong_hop.set_index("mô hình").loc[f"Luật {len(TU_KHOA)} từ khóa (Phần 1)"]
bao_cao = f'''BÁO CÁO MODULE 4: LỌC TIN NHẮN RÁC / LỪA ĐẢO BẰNG MÁY HỌC (SMS Spam Collection, {len(tin):,} tin thật không trùng)
=====================================================================================
Đề thi : {len(y_thi)} tin, {int(y_thi.sum())} tin rác
Luật từ khóa : F1 {luat['F1']:.3f} | chặn nhầm {int(luat['chặn nhầm (FP)'])} | bỏ lọt {int(luat['bỏ lọt (FN)'])}
Hội đồng trọng số {TRONG_SO} : F1 {hd['F1']:.3f} | precision {hd['precision']:.3f} | recall {hd['recall']:.3f} | chặn nhầm {int(hd['chặn nhầm (FP)'])} | bỏ lọt {int(hd['bỏ lọt (FN)'])}
Tấn công chèn từ an toàn: hội đồng bắt được {tan_cong.iloc[-1, 0]:.0%} -> {tan_cong.iloc[-1, 1]:.0%} tin rác
Khuyến nghị:
- Thay luật từ khóa bằng mô hình học máy; dùng hội đồng có trọng số, kiểm tra bằng số liệu trên đề thi riêng.
- Phân luồng 3 làn theo xác suất: chặn khi rất chắc, cách ly vùng giữa cho người xem, cho qua phần còn lại.
- Luôn bỏ bản sao trước khi chia học / thi; luôn so với mốc "lười".
- Tập lại định kỳ với các chiêu né tránh mới (huấn luyện đối kháng); kết hợp thêm tín hiệu hành vi (người gửi, link, giờ gửi).
'''
(THU_MUC_RA / "bao_cao_lab4.txt").write_text(bao_cao, encoding="utf-8")
print(bao_cao)