115 KiB
115 KiB
In [ ]:
import time
import warnings
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import LinearSVC
pd.set_option("display.max_colwidth", 110)
pd.set_option("display.width", 200)
warnings.filterwarnings("ignore", category=UserWarning) # ẩn cảnh báo kỹ thuật không ảnh hưởng kết quả
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ả (giống Lab 4)
du_lieu = pd.read_csv("/opt/csai/labs/day4_phishing/SMSSpamCollection", sep="\t", header=None,
names=["nhan", "noi_dung"], quoting=3, encoding="utf-8")
tin = du_lieu.drop_duplicates("noi_dung").reset_index(drop=True) # bỏ bản sao TRƯỚC khi chia (bài học Lab 4)
y = (tin.nhan == "spam").astype(int) # 1 = spam, 0 = ham
vi_tri_hoc, vi_tri_thi = train_test_split(tin.index, test_size=0.25, random_state=RANDOM_STATE, stratify=y)
van_ban_hoc, van_ban_thi = tin.noi_dung[vi_tri_hoc], tin.noi_dung[vi_tri_thi]
y_hoc, y_thi = y[vi_tri_hoc], y[vi_tri_thi]
# 📦 van_ban_hoc / van_ban_thi = nội dung tin nhắn phần học / phần thi · y_hoc / y_thi = nhãn 0/1 tương ứng
print(f"Phần học: {len(van_ban_hoc)} tin ({y_hoc.sum()} spam) | Phần thi: {len(van_ban_thi)} tin ({y_thi.sum()} spam)")
def cham_diem(y_that, du_doan):
# Đếm 4 ô của ma trận nhầm lẫn rồi tính các tỉ lệ. y_that, du_doan: mảng 0/1 cùng độ dài
y_that, du_doan = np.asarray(y_that), np.asarray(du_doan)
tp = int(((du_doan == 1) & (y_that == 1)).sum()) # bắt đúng spam
fp = int(((du_doan == 1) & (y_that == 0)).sum()) # chặn nhầm tin thật
fn = int(((du_doan == 0) & (y_that == 1)).sum()) # bỏ lọt spam
tn = int(((du_doan == 0) & (y_that == 0)).sum()) # cho qua đúng tin thật
precision = tp / (tp + fp) if tp + fp else 0.0
recall = tp / (tp + fn) if tp + fn else 0.0
return {"TP": tp, "FP": fp, "FN": fn, "TN": tn, "precision": precision, "recall": recall,
"F1": 2 * precision * recall / (precision + recall) if precision + recall else 0.0,
"FPR": fp / (fp + tn), "FNR": fn / (fn + tp), "accuracy": (tp + tn) / len(y_that)}
tfidf_goc = TfidfVectorizer() # từ điển từ đơn, như Lab 4
X_hoc_goc = tfidf_goc.fit_transform(van_ban_hoc) # fit = học từ điển từ phần học
X_thi_goc = tfidf_goc.transform(van_ban_thi) # phần thi chỉ transform
svm_goc = LinearSVC(random_state=RANDOM_STATE).fit(X_hoc_goc, y_hoc)
du_doan_goc = svm_goc.predict(X_thi_goc)
kq_goc = cham_diem(y_thi, du_doan_goc)
# 📦 kq_goc = dict mọi chỉ số của mô hình gốc trên phần thi
print(pd.DataFrame([[kq_goc["TN"], kq_goc["FP"]], [kq_goc["FN"], kq_goc["TP"]]],
index=["thật là ham", "thật là spam"], columns=["máy cho qua", "máy chặn"]))
print({k: round(v, 4) for k, v in kq_goc.items()})In [ ]:
# ✏️ SỬA: quy mô của tổ chức bạn và tỉ lệ tin rác thực tế
SO_TIN_MOI_NGAY = 100_000
TI_LE_SPAM_THUC_TE = 0.126 # bằng tỉ lệ trong bộ dữ liệu; thư điện tử thật thường cao hơn nhiều
acc_luoi = (y_thi == 0).mean() # mô hình lười: luôn đoán 0 (ham)
print(f"Mô hình lười 'luôn là ham': accuracy {acc_luoi:.1%}, bắt được 0 / {y_thi.sum()} tin rác")
print(f"SVM của Lab 4 : accuracy {kq_goc['accuracy']:.1%}, bắt được {kq_goc['TP']} / {y_thi.sum()} tin rác")
tin_that_ngay = SO_TIN_MOI_NGAY * (1 - TI_LE_SPAM_THUC_TE)
tin_rac_ngay = SO_TIN_MOI_NGAY * TI_LE_SPAM_THUC_TE
print(f"\nMỗi ngày {SO_TIN_MOI_NGAY:,} tin = {tin_that_ngay:,.0f} tin thật + {tin_rac_ngay:,.0f} tin rác")
print(f"FPR {kq_goc['FPR']:.2%} -> chặn nhầm khoảng {tin_that_ngay * kq_goc['FPR']:,.0f} tin THẬT mỗi ngày")
print(f"FNR {kq_goc['FNR']:.2%} -> bỏ lọt khoảng {tin_rac_ngay * kq_goc['FNR']:,.0f} tin RÁC mỗi ngày")In [ ]:
# ✏️ SỬA: kịch bản quét file — đổi số rồi chạy lại ô này
SO_FILE = 1_000_000 # số file quét mỗi ngày
SO_MALWARE = 100 # trong đó thật sự là mã độc
BAT_DUOC = 0 # mô hình bắt đúng bao nhiêu mã độc (0 = mô hình lười "mọi file đều sạch"); tối đa = SO_MALWARE
BAO_NHAM = 0 # mô hình cảnh báo nhầm bao nhiêu file sạch
y_file = np.zeros(SO_FILE, dtype=int)
y_file[:SO_MALWARE] = 1 # nhãn thật: SO_MALWARE file đầu là mã độc (1), còn lại sạch (0)
du_doan_file = np.zeros(SO_FILE, dtype=int) # bắt đầu: máy nói "sạch" cho mọi file
du_doan_file[:BAT_DUOC] = 1 # máy bắt đúng BAT_DUOC file mã độc
du_doan_file[SO_MALWARE:SO_MALWARE + BAO_NHAM] = 1 # máy cảnh báo nhầm BAO_NHAM file sạch
kq_file = cham_diem(y_file, du_doan_file) # dùng lại hàm chấm điểm của 0.1
# 📦 kq_file = dict TP / FP / FN / TN + các chỉ số của kịch bản này
print(f"TP (bắt đúng) {kq_file['TP']} · FP (báo nhầm) {kq_file['FP']} · FN (bỏ sót) {kq_file['FN']} · TN {kq_file['TN']:,}")
print(f"Accuracy = {kq_file['accuracy']:.2%}")
print(f"Precision = {kq_file['precision']:.2%} (0 khi máy không cảnh báo file nào)")
print(f"Recall = {kq_file['recall']:.2%} -> tóm được {kq_file['TP']} / {SO_MALWARE} mã độc")
print(f"F1 = {kq_file['F1']:.3f}")In [ ]:
from sklearn.pipeline import make_pipeline
# ✏️ SỬA: số lần chia thử
SO_LAN_CHIA = 10
f1_moi_lan_chia = [] # 📦 danh sách F1, mỗi lần chia 1 số
for hat_giong in range(SO_LAN_CHIA):
vt_hoc_k, vt_thi_k = train_test_split(tin.index, test_size=0.25, random_state=hat_giong, stratify=y)
ong_k = make_pipeline(TfidfVectorizer(), LinearSVC(random_state=RANDOM_STATE)).fit(tin.noi_dung[vt_hoc_k], y[vt_hoc_k])
f1_lan_nay = cham_diem(y[vt_thi_k], ong_k.predict(tin.noi_dung[vt_thi_k]))["F1"]
f1_moi_lan_chia.append(f1_lan_nay)
print(f"hạt giống {hat_giong}: F1 = {f1_lan_nay:.3f}")
print(f"\nThấp nhất {min(f1_moi_lan_chia):.3f} | cao nhất {max(f1_moi_lan_chia):.3f} | chênh {max(f1_moi_lan_chia) - min(f1_moi_lan_chia):.3f}")In [ ]:
from sklearn.model_selection import StratifiedKFold, cross_val_score
chia_5_phan = StratifiedKFold(n_splits=5, shuffle=True, random_state=RANDOM_STATE) # cách chia, dùng lại cả bài
ong_goc = make_pipeline(TfidfVectorizer(), LinearSVC(random_state=RANDOM_STATE)) # đúng mô hình 0.1, dạng ống
f1_5_phan = cross_val_score(ong_goc, van_ban_hoc, y_hoc, cv=chia_5_phan, scoring="f1")
# 📦 f1_5_phan = mảng 5 điểm F1, mỗi phần 1 điểm
print("F1 từng phần:", np.round(f1_5_phan, 3))
print(f"F1 kiểm định chéo = {f1_5_phan.mean():.3f} ± {f1_5_phan.std():.3f}")In [ ]:
diem_thi_goc = svm_goc.decision_function(X_thi_goc) # 📦 điểm của 1.293 tin phần thi
y_thi_mang = y_thi.to_numpy()
print("Điểm 3 tin đầu phần thi:", np.round(diem_thi_goc[:3], 2), "| nhãn thật:", y_thi_mang[:3])
nguong_fn0 = diem_thi_goc[y_thi_mang == 1].min() # điểm của tin rác thấp điểm nhất
nguong_fp0 = diem_thi_goc[y_thi_mang == 0].max() # điểm của tin thật cao điểm nhất
# ✏️ SỬA: thêm ngưỡng bạn muốn thử
NGUONG_THU = [0.0, -0.5, nguong_fn0, nguong_fp0 + 1e-6]
dong_nguong = []
for nguong in NGUONG_THU:
kq_n = cham_diem(y_thi_mang, (diem_thi_goc >= nguong).astype(int))
dong_nguong.append({"ngưỡng": round(nguong, 3), "chặn nhầm (FP)": kq_n["FP"], "bỏ lọt (FN)": kq_n["FN"],
"FPR": round(kq_n["FPR"], 3), "precision": round(kq_n["precision"], 3), "recall": round(kq_n["recall"], 3)})
print(pd.DataFrame(dong_nguong).to_string(index=False))
# np.where(điều_kiện, a, b): lấy a ở chỗ điều kiện đúng, b ở chỗ sai -> ẩn nhóm kia đi rồi tìm vị trí min / max
vi_tri_rac_hien_nhat = int(np.where(y_thi_mang == 1, diem_thi_goc, np.inf).argmin())
vi_tri_that_giong_rac_nhat = int(np.where(y_thi_mang == 0, diem_thi_goc, -np.inf).argmax())
print(f"\nTin RÁC điểm thấp nhất ({nguong_fn0:.2f}):", repr(van_ban_thi.iloc[vi_tri_rac_hien_nhat]))
print(f"Tin THẬT điểm cao nhất ({nguong_fp0:.2f}):", repr(van_ban_thi.iloc[vi_tri_that_giong_rac_nhat]))In [ ]:
from sklearn.metrics import roc_curve, roc_auc_score, precision_recall_curve
fpr_duong, tpr_duong, _ = roc_curve(y_thi_mang, diem_thi_goc) # _ = danh sách ngưỡng, không cần dùng
pre_duong, rec_duong, _ = precision_recall_curve(y_thi_mang, diem_thi_goc)
auc_goc = roc_auc_score(y_thi_mang, diem_thi_goc)
hinh, (truc_a, truc_b, truc_c) = plt.subplots(1, 3, figsize=(15, 4))
cac_moc = np.linspace(diem_thi_goc.min(), diem_thi_goc.max(), 50)
truc_a.hist(diem_thi_goc[y_thi_mang == 0], bins=cac_moc, alpha=0.6, color="tab:blue", label="tin thật (ham)")
truc_a.hist(diem_thi_goc[y_thi_mang == 1], bins=cac_moc, alpha=0.6, color="tab:red", label="tin rác (spam)")
truc_a.axvline(0, color="k", ls="--", label="ngưỡng 0")
truc_a.axvline(nguong_fn0, color="tab:red", ls=":", label=f"ngưỡng FN=0 ({nguong_fn0:.2f})")
truc_a.set_yscale("log"); truc_a.set_xlabel("điểm SVM"); truc_a.set_title("Phân bố điểm trên phần thi"); truc_a.legend(fontsize=8)
truc_b.plot(fpr_duong, tpr_duong); truc_b.plot([0, 1], [0, 1], "k:", lw=0.8)
truc_b.scatter([kq_goc["FPR"]], [kq_goc["recall"]], color="k", zorder=3)
truc_b.set_xlabel("FPR (chặn nhầm / tổng tin thật)"); truc_b.set_ylabel("recall"); truc_b.set_title(f"ROC — AUC = {auc_goc:.4f}")
truc_c.plot(rec_duong, pre_duong); truc_c.scatter([kq_goc["recall"]], [kq_goc["precision"]], color="k", zorder=3)
truc_c.set_xlabel("recall"); truc_c.set_ylabel("precision"); truc_c.set_title("Precision – Recall")
plt.tight_layout(); plt.show()
print(f"AUC = {auc_goc:.4f}")In [ ]:
from sklearn.model_selection import cross_val_predict
diem_hoc_cv = cross_val_predict(ong_goc, van_ban_hoc, y_hoc, cv=chia_5_phan, method="decision_function")
# 📦 diem_hoc_cv = mỗi tin phần học 1 điểm, tính bởi mô hình học trên 4/5 phần còn lại
y_hoc_mang = y_hoc.to_numpy()
cac_nguong_thu = np.linspace(-2, 2, 401) # 401 ngưỡng từ -2 đến 2
def chon_nguong(ti_le_chi_phi):
# Trả về ngưỡng có tổng chi phí nhỏ nhất trên PHẦN HỌC. ti_le_chi_phi = chi phí 1 FN / chi phí 1 FP
chi_phi = [ti_le_chi_phi * ((diem_hoc_cv < t) & (y_hoc_mang == 1)).sum() + ((diem_hoc_cv >= t) & (y_hoc_mang == 0)).sum()
for t in cac_nguong_thu]
return cac_nguong_thu[int(np.argmin(chi_phi))]
# ✏️ SỬA: chi phí của bạn
CHI_PHI_FN = 10
CHI_PHI_FP = 1
dong_chi_phi = []
for ti_le in sorted({1, 10, 50, CHI_PHI_FN / CHI_PHI_FP}):
nguong_tot = chon_nguong(ti_le)
kq_cp = cham_diem(y_thi_mang, (diem_thi_goc >= nguong_tot).astype(int))
dong_chi_phi.append({"1 FN đắt bằng ... FP": ti_le, "ngưỡng (chọn trên phần học)": round(nguong_tot, 2),
"PHẦN THI: chặn nhầm (FP)": kq_cp["FP"], "bỏ lọt (FN)": kq_cp["FN"], "F1": round(kq_cp["F1"], 3)})
bang_chi_phi = pd.DataFrame(dong_chi_phi)
bang_chi_phiIn [ ]:
from sklearn.model_selection import learning_curve
# ✏️ SỬA: các tỉ lệ dữ liệu học thử
TI_LE_DU_LIEU = [0.1, 0.25, 0.5, 0.75, 1.0]
so_tin_hoc, f1_tu_cham, f1_cv_lc = learning_curve(ong_goc, van_ban_hoc, y_hoc, cv=chia_5_phan, scoring="f1",
train_sizes=TI_LE_DU_LIEU)
# 📦 so_tin_hoc = số tin học mỗi mức · f1_tu_cham / f1_cv_lc = bảng (số mức × 5 phần) điểm F1
duong_cong = pd.DataFrame({"số tin học": so_tin_hoc, "F1 tự chấm": f1_tu_cham.mean(axis=1).round(3),
"F1 kiểm định chéo": f1_cv_lc.mean(axis=1).round(3), "± (độ lệch)": f1_cv_lc.std(axis=1).round(3)})
print(duong_cong.to_string(index=False))
plt.figure(figsize=(7, 4))
plt.plot(so_tin_hoc, f1_tu_cham.mean(axis=1), "o-", color="tab:orange", label="tự chấm bài đã học")
plt.errorbar(so_tin_hoc, f1_cv_lc.mean(axis=1), yerr=f1_cv_lc.std(axis=1), fmt="o-", color="tab:blue", label="kiểm định chéo")
plt.xlabel("số tin dùng để học"); plt.ylabel("F1"); plt.title("Đường cong học — SVM trên từ đơn"); plt.legend(); plt.grid(alpha=0.3); plt.show()In [ ]:
import re
vi_du = "WIN £1000 cash! Call 09061701461 now"
print("từ đơn :", TfidfVectorizer().build_analyzer()(vi_du))
print("từ đơn + cặp từ :", TfidfVectorizer(ngram_range=(1, 2)).build_analyzer()(vi_du)[-5:], "... (5 phần tử cuối)")
print("cụm ký tự 2–5 :", TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 5)).build_analyzer()(vi_du)[:12], "... (12 phần tử đầu)")
def dac_trung_so(chuoi_tin):
# Nhận một cột tin nhắn (pandas Series), trả về mảng (số tin × 5) con số tự tính
chuoi_tin = pd.Series(chuoi_tin)
bang_so = pd.DataFrame({
"do_dai": chuoi_tin.str.len(), # số ký tự
"so_chu_so": chuoi_tin.str.count(r"\d"), # \d = một chữ số
"co_link": chuoi_tin.str.contains(r"http|www\.|\.com", case=False).astype(int),
"co_day_5_so": chuoi_tin.str.contains(r"\d{5,}").astype(int), # ≥ 5 chữ số liền nhau
"ti_le_hoa": chuoi_tin.str.count(r"[A-Z]") / chuoi_tin.str.len().clip(lower=1), # clip: tránh chia cho 0
})
return bang_so.to_numpy(dtype=float)
TEN_SO_DO = ["do_dai", "so_chu_so", "co_link", "co_day_5_so", "ti_le_hoa"]
mau_2_tin = pd.Series(["Ok lar... Joking wif u oni...", vi_du])
print("\n5 số đo của 1 tin thật và 1 tin rác:")
print(pd.DataFrame(dac_trung_so(mau_2_tin), columns=TEN_SO_DO, index=mau_2_tin).round(2).to_string())In [ ]:
from sklearn.pipeline import FeatureUnion
from sklearn.preprocessing import FunctionTransformer, StandardScaler
def tao_svm():
return LinearSVC(random_state=RANDOM_STATE)
so_do_chuan_hoa = make_pipeline(FunctionTransformer(dac_trung_so), StandardScaler()) # 5 số đo -> cùng thang
cac_cach_nhin = {
"1. từ đơn (như Lab 4)": make_pipeline(TfidfVectorizer(), tao_svm()),
"2. từ đơn + cặp từ": make_pipeline(TfidfVectorizer(ngram_range=(1, 2)), tao_svm()),
"3. cụm ký tự 2-5": make_pipeline(TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 5)), tao_svm()),
"4. từ + ký tự": make_pipeline(FeatureUnion([("tu", TfidfVectorizer()),
("ky_tu", TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 5)))]), tao_svm()),
"5. từ + ký tự + 5 số đo": make_pipeline(FeatureUnion([("tu", TfidfVectorizer()),
("ky_tu", TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 5))),
("so_do", so_do_chuan_hoa)]), tao_svm()),
}
dong_dac_trung = []
f1_tung_phan = {} # 📦 tên cách nhìn -> mảng 5 điểm F1 (cùng 5 phần cho mọi cách)
for ten_cach, ong_cach in cac_cach_nhin.items():
f1_cv_cach = cross_val_score(ong_cach, van_ban_hoc, y_hoc, cv=chia_5_phan, scoring="f1")
f1_tung_phan[ten_cach] = f1_cv_cach
kq_cach = cham_diem(y_thi, ong_cach.fit(van_ban_hoc, y_hoc).predict(van_ban_thi))
dong_dac_trung.append({"cách nhìn": ten_cach, "F1 kiểm định chéo": f1_cv_cach.mean(), "±": f1_cv_cach.std(),
"F1 phần thi": kq_cach["F1"], "chặn nhầm (FP)": kq_cach["FP"], "bỏ lọt (FN)": kq_cach["FN"]})
so_sanh_dac_trung = pd.DataFrame(dong_dac_trung).set_index("cách nhìn")
# 📦 so_sanh_dac_trung = bảng 5 cách nhìn × các chỉ số
so_sanh_dac_trung["F1 kiểm định chéo"].plot(kind="barh", xerr=so_sanh_dac_trung["±"], figsize=(8, 3), xlim=(0.85, 1.0),
title="F1 kiểm định chéo (vạch đen = ± độ lệch)")
plt.show()
print(so_sanh_dac_trung.round(3).to_string())In [ ]:
cach_moc = "1. từ đơn (như Lab 4)"
for ten_cach, f1_cach in f1_tung_phan.items():
if ten_cach == cach_moc:
continue
chenh = f1_cach - f1_tung_phan[cach_moc] # 📦 mảng 5 hiệu số, phần nào với phần đó
print(f"{ten_cach:26s} hơn từ đơn ở từng phần: {np.round(chenh, 3)} -> thắng {int((chenh > 0).sum())}/5 phần")In [ ]:
from sklearn.model_selection import cross_validate
from sklearn.metrics import roc_auc_score
from sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neural_network import MLPClassifier
from xgboost import XGBClassifier
# ✏️ SỬA: False để bỏ qua 3 mô hình chậm (rừng, XGBoost, mạng nơ-ron) nếu máy đang đông người
CHAY_MO_HINH_CHAM = True
cac_mo_hinh = {
"Naive Bayes": (MultinomialNB(), "dễ: xác suất từng từ"),
"Logistic Regression (balanced)": (LogisticRegression(max_iter=1000, class_weight="balanced"), "dễ: trọng số từng từ"),
"SVM tuyến tính": (tao_svm(), "dễ: trọng số từng từ"),
"Cây quyết định": (DecisionTreeClassifier(random_state=RANDOM_STATE), "dễ nếu cây nông; cây sâu thì khó"),
}
if CHAY_MO_HINH_CHAM:
cac_mo_hinh.update({
"Rừng ngẫu nhiên (300 cây)": (RandomForestClassifier(n_estimators=300, n_jobs=2, random_state=RANDOM_STATE), "khó: cần SHAP"),
"XGBoost (300 cây)": (XGBClassifier(n_estimators=300, n_jobs=2, random_state=RANDOM_STATE, eval_metric="logloss"), "khó: cần SHAP"),
"Mạng nơ-ron MLP (64)": (MLPClassifier(hidden_layer_sizes=(64,), max_iter=300, random_state=RANDOM_STATE), "rất khó"),
})
def diem_lien_tuc(mo_hinh, X):
# Điểm "giống spam" để tính AUC: mô hình nào có decision_function thì dùng, không thì xác suất lớp 1
return mo_hinh.decision_function(X) if hasattr(mo_hinh, "decision_function") else mo_hinh.predict_proba(X)[:, 1]
dong_mo_hinh = []
for ten_mh, (mo_hinh, giai_thich) in cac_mo_hinh.items():
ong_mh = make_pipeline(TfidfVectorizer(), mo_hinh)
do_cv = cross_validate(ong_mh, van_ban_hoc, y_hoc, cv=chia_5_phan, scoring="f1") # dict: test_score, fit_time...
ong_mh.fit(van_ban_hoc, y_hoc)
kq_mh = cham_diem(y_thi, ong_mh.predict(van_ban_thi))
dong_mo_hinh.append({"mô hình": ten_mh, "F1 kiểm định chéo": do_cv["test_score"].mean(), "±": do_cv["test_score"].std(),
"F1 phần thi": kq_mh["F1"], "FP": kq_mh["FP"], "FN": kq_mh["FN"],
"AUC": roc_auc_score(y_thi, diem_lien_tuc(ong_mh, van_ban_thi)),
"giây học (5 lần)": do_cv["fit_time"].sum(), "giải thích được?": giai_thich})
print(f"xong {ten_mh}")
so_sanh_mo_hinh = pd.DataFrame(dong_mo_hinh).set_index("mô hình")
# 📦 so_sanh_mo_hinh = bảng mô hình × chỉ số
so_sanh_mo_hinh.round(3)In [ ]:
plt.figure(figsize=(8, 4.5))
for ten_mh, dong_mh in so_sanh_mo_hinh.iterrows(): # iterrows: lặp từng dòng (tên, dữ liệu)
plt.errorbar(dong_mh["giây học (5 lần)"], dong_mh["F1 kiểm định chéo"], yerr=dong_mh["±"], fmt="o", capsize=3)
plt.annotate(ten_mh, (dong_mh["giây học (5 lần)"], dong_mh["F1 kiểm định chéo"]), fontsize=8,
xytext=(5, 3), textcoords="offset points")
plt.xscale("log"); plt.margins(x=0.3); plt.xlabel("giây học (5 lần kiểm định chéo, thang log)"); plt.ylabel("F1 kiểm định chéo")
plt.title("Chính xác và tốc độ — cùng dữ liệu từ đơn TF-IDF"); plt.grid(alpha=0.3); plt.show()In [ ]:
from sklearn.model_selection import GridSearchCV
ong_ky_tu = make_pipeline(TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 5)), tao_svm())
# ✏️ SỬA: lưới giá trị thử (thêm giá trị = lâu hơn)
LUOI_NUM = {"linearsvc__C": [0.1, 0.3, 1, 3, 10], "linearsvc__class_weight": [None, "balanced"]}
bat_dau = time.time()
tim_num = GridSearchCV(ong_ky_tu, LUOI_NUM, cv=chia_5_phan, scoring="f1", n_jobs=2).fit(van_ban_hoc, y_hoc)
print(f"Thử {len(tim_num.cv_results_['params'])} tổ hợp trong {time.time() - bat_dau:.0f} giây")
bang_luoi = pd.DataFrame(tim_num.cv_results_)[["param_linearsvc__C", "param_linearsvc__class_weight",
"mean_test_score", "std_test_score"]]
bang_luoi.columns = ["C", "class_weight", "F1 kiểm định chéo", "±"]
print(bang_luoi.sort_values("F1 kiểm định chéo", ascending=False).round(3).to_string(index=False))
print("\nTổ hợp tốt nhất:", tim_num.best_params_, f"| F1 kiểm định chéo {tim_num.best_score_:.3f}")
mo_hinh_cuoi = tim_num.best_estimator_ # 📦 ống tốt nhất, đã học lại trên TOÀN BỘ phần học
kq_cuoi = cham_diem(y_thi, mo_hinh_cuoi.predict(van_ban_thi))
print(f"PHẦN THI (dùng 1 lần): F1 {kq_cuoi['F1']:.3f} | chặn nhầm {kq_cuoi['FP']} | bỏ lọt {kq_cuoi['FN']}"
f" (gốc Lab 4: F1 {kq_goc['F1']:.3f} | {kq_goc['FP']} | {kq_goc['FN']})")In [ ]:
diem_thi_cuoi = mo_hinh_cuoi.decision_function(van_ban_thi) # 📦 điểm mô hình cuối cho 1.293 tin thi
du_doan_cuoi = mo_hinh_cuoi.predict(van_ban_thi)
bang_loi = pd.DataFrame({"điểm": diem_thi_cuoi.round(2), "nhãn thật": y_thi_mang, "tin nhắn": van_ban_thi.to_numpy()})
print(f"BỎ LỌT (spam bị cho qua) — {kq_cuoi['FN']} tin, xếp từ điểm cao xuống:")
print(bang_loi[(y_thi_mang == 1) & (du_doan_cuoi == 0)].sort_values("điểm", ascending=False).to_string(index=False))
print(f"\nCHẶN NHẦM (tin thật bị chặn) — {kq_cuoi['FP']} tin:")
print(bang_loi[(y_thi_mang == 0) & (du_doan_cuoi == 1)].to_string(index=False))In [ ]:
# ✏️ SỬA: các độ rộng vùng giữa thử
DO_RONG_VUNG_GIUA = [0.0, 0.25, 0.5, 0.75, 1.0]
dong_lan = []
for do_rong in DO_RONG_VUNG_GIUA:
vung_giua = (diem_thi_cuoi > -do_rong) & (diem_thi_cuoi <= do_rong)
dong_lan.append({"độ rộng": do_rong, "vùng giữa (tin)": int(vung_giua.sum()),
"chặn nhầm tự động": int(((diem_thi_cuoi > do_rong) & (y_thi_mang == 0)).sum()),
"bỏ lọt tự động": int(((diem_thi_cuoi <= -do_rong) & (y_thi_mang == 1)).sum()),
"người xem / ngày": round(vung_giua.mean() * SO_TIN_MOI_NGAY)})
ba_lan = pd.DataFrame(dong_lan)
ba_lanIn [ ]:
# ✏️ SỬA: câu kẻ tấn công chèn (giống Lab 4)
CAU_CHEN = " ok see you later at home lor going to sleep now love you dear"
tin_rac_phan_thi = van_ban_thi[y_thi_mang == 1] # 📦 các tin rác của phần thi
dong_tan_cong = []
ong_goc_da_hoc = ong_goc.fit(van_ban_hoc, y_hoc) # ống gốc học trên toàn bộ phần học (= svm_goc ở 0.1)
for ten_ong, ong_tc in [("SVM gốc Lab 4 (từ đơn)", ong_goc_da_hoc), ("Mô hình cuối (ký tự, đã tinh chỉnh)", mo_hinh_cuoi)]:
dong_tan_cong.append({"mô hình": ten_ong, "bắt được TRƯỚC": ong_tc.predict(tin_rac_phan_thi).mean(),
"bắt được SAU khi chèn": ong_tc.predict(tin_rac_phan_thi + CAU_CHEN).mean()})
pd.DataFrame(dong_tan_cong).set_index("mô hình").round(3)In [ ]:
from pathlib import Path
THU_MUC_RA = Path.home() / "lab" / "out_lab10"
THU_MUC_RA.mkdir(parents=True, exist_ok=True)
tot_nhat_dac_trung = so_sanh_dac_trung["F1 kiểm định chéo"].idxmax()
tot_nhat_mo_hinh = so_sanh_mo_hinh["F1 kiểm định chéo"].idxmax()
nguong_chon = bang_chi_phi[bang_chi_phi["1 FN đắt bằng ... FP"] == CHI_PHI_FN / CHI_PHI_FP].iloc[0]
bao_cao = f'''BÁO CÁO MODULE 10: ĐÁNH GIÁ LẠI VÀ CẢI THIỆN MÔ HÌNH LỌC TIN RÁC (SMS Spam Collection, {len(tin):,} tin thật không trùng)
=====================================================================================
Phần thi: {len(y_thi)} tin, {int(y_thi.sum())} tin rác
Mô hình gốc (Lab 4, SVM từ đơn) : F1 {kq_goc['F1']:.3f} | chặn nhầm {kq_goc['FP']} | bỏ lọt {kq_goc['FN']} | AUC {auc_goc:.4f}
Độ tin cậy của phép đo : F1 kiểm định chéo {f1_5_phan.mean():.3f} ± {f1_5_phan.std():.3f}; 10 cách chia: {min(f1_moi_lan_chia):.3f} - {max(f1_moi_lan_chia):.3f}
FN = 0 bằng ngưỡng : phải chặn nhầm {dong_nguong[2]['chặn nhầm (FP)']} tin thật / {int((y_thi == 0).sum())}
Ngưỡng theo chi phí {CHI_PHI_FN}:{CHI_PHI_FP} : chặn nhầm {int(nguong_chon['PHẦN THI: chặn nhầm (FP)'])} | bỏ lọt {int(nguong_chon['bỏ lọt (FN)'])}
Thêm dữ liệu : F1 kiểm định chéo {duong_cong['F1 kiểm định chéo'].iloc[-2]:.3f} -> {duong_cong['F1 kiểm định chéo'].iloc[-1]:.3f} ở 2 mức dữ liệu cuối
Cách nhìn tốt nhất (Phần 4) : {tot_nhat_dac_trung} (F1 kiểm định chéo {so_sanh_dac_trung.loc[tot_nhat_dac_trung, 'F1 kiểm định chéo']:.3f})
Mô hình tốt nhất trên từ đơn : {tot_nhat_mo_hinh} (F1 kiểm định chéo {so_sanh_mo_hinh.loc[tot_nhat_mo_hinh, 'F1 kiểm định chéo']:.3f})
Mô hình cuối (tinh chỉnh) : {tim_num.best_params_} | F1 {kq_cuoi['F1']:.3f} | chặn nhầm {kq_cuoi['FP']} | bỏ lọt {kq_cuoi['FN']}
Khuyến nghị:
- Chọn ngưỡng theo chi phí FN/FP trên phần học, không đòi FN = 0.
- Đầu tư vào đặc trưng trước khi đổi sang mô hình phức tạp; so sánh luôn kèm ± độ dao động.
- Vùng giữa cho người xem; theo dõi tỉ lệ cảnh báo, đo lại định kỳ, học lại có so sánh với mô hình cũ.
'''
(THU_MUC_RA / "bao_cao_lab10.txt").write_text(bao_cao, encoding="utf-8")
print(bao_cao)