1362 lines
96 KiB
Plaintext
1362 lines
96 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Module 4 — Máy học chống tin nhắn lừa đảo / rác, trên tin nhắn SMS THẬT\n",
|
||
"\n",
|
||
"> **Cách làm:** bấm vào một ô code rồi nhấn **Shift + Enter** để chạy ô đó và sang ô kế tiếp. Chạy **lần lượt từ trên xuống**.\n",
|
||
"> Kernel phải là **Python (lớp CSAI)** (góc trên bên phải). Ô có dòng **✏️ SỬA** là chỗ mời bạn đổi số / chữ rồi chạy lại.\n",
|
||
"> Sửa hỏng? Menu **Kernel → Restart Kernel and Run All Cells**.\n",
|
||
"\n",
|
||
"### 🎯 Mục tiêu của bài\n",
|
||
"\n",
|
||
"Học xong bài này, bạn:\n",
|
||
"1. Chỉ ra được **vì sao lọc bằng từ khóa thất bại** (chặn nhầm và bỏ lọt), đo trên tin nhắn thật.\n",
|
||
"2. Hiểu cách **biến chữ thành số** (TF-IDF) và vì sao phải **chia dữ liệu học / thi** trước khi chấm điểm.\n",
|
||
"3. Huấn luyện và so sánh **5 cách phân loại**: Perceptron, SVM, Logistic Regression, Naive Bayes, Hội đồng (Voting).\n",
|
||
"4. Đọc được **ma trận nhầm lẫn, precision, recall, F1** và chọn **ngưỡng** theo cái giá của chặn nhầm / bỏ lọt.\n",
|
||
"5. Thấy kẻ tấn công **lừa AI** thế nào (chèn \"từ an toàn\") và cách **phòng thủ**.\n",
|
||
"\n",
|
||
"Notebook giải thích từng khái niệm ngay tại chỗ, theo thứ tự bài giảng Module 4. Mọi con số trong phần \"Kết quả\" là số **đo thật** khi chạy notebook này.\n",
|
||
"\n",
|
||
"**Dữ liệu:** *SMS Spam Collection* — bộ **tin nhắn SMS thật** (tiếng Anh; bao nhiêu tin, bao nhiêu tin rác thì ô 0.1 sẽ tự đếm) do người dùng gửi lên mạng hoặc tình nguyện chia sẻ cho nghiên cứu, mỗi tin đã được gắn nhãn `ham` (tin thật) hoặc `spam` (tin rác / lừa đảo). Tác giả: T. A. Almeida & J. M. Gómez Hidalgo (2011), cho phép dùng miễn phí, ghi nguồn (file `SMSSpamCollection_readme.txt` đi kèm). Tin nhắn rác và email lừa đảo cùng một bản chất: **văn bản dụ người nhận bấm, gọi, trả tiền hoặc khai thông tin** — mọi kỹ thuật trong bài dùng y nguyên cho email.\n",
|
||
"\n",
|
||
"### 🗺️ Bài này: bài toán gì, mô hình nào, vì sao?\n",
|
||
"\n",
|
||
"- **Bài toán:** một tin nhắn đến → **chặn** (rác / lừa đảo) hay **cho qua** (tin thường)? Đây là bài **phân loại 2 lớp**.\n",
|
||
"- **Dữ liệu:** **văn bản**, **đã có nhãn** sẵn (`ham` / `spam`, do tác giả bộ dữ liệu gắn theo nguồn lấy tin — xem đầu Phần 0) nhưng **còn bẩn**: có bản sao (gửi hàng loạt), viết tắt, ký hiệu tiền tệ.\n",
|
||
"- **Vì vậy chọn:** đã có nhãn → **học có giám sát**; là văn bản → phải **biến chữ thành số** (TF-IDF) rồi dùng các mô hình hợp với văn bản: Perceptron, SVM, Logistic Regression, Naive Bayes, và **hội đồng** của chúng (bảng ② trong mục *Trước khi bắt đầu* ngay dưới đây).\n",
|
||
"- **Thước đo:** chặn nhầm tin thật (FP) và bỏ lọt tin rác (FN) đều có giá → không chỉ nhìn accuracy.\n",
|
||
"\n",
|
||
"**Pipeline — cách làm mọi bài toán AI trong khoá, nhớ thuộc lòng 7 bước.** Mỗi phần ghi **📍 Bước** ở đầu:\n",
|
||
"\n",
|
||
"| Bước | Câu hỏi phải trả lời | Phần |\n",
|
||
"|---|---|---|\n",
|
||
"| 1. **Thu thập dữ liệu** | dữ liệu gì, lấy ở đâu, thu thập thế nào, **có nhãn hay không nhãn**? | Phần 0 (5 câu hỏi), 0.1 |\n",
|
||
"| 2. **Làm sạch dữ liệu** | có gì bẩn: bản sao, viết tắt, ký hiệu lạ? | 0.2 |\n",
|
||
"| 3. **Chọn mô hình phù hợp với bài toán** | bài toán gì, có nhãn không, cách cũ làm được đến đâu? | 0.3, Phần 1 (mốc: luật từ khóa) |\n",
|
||
"| 4. **Coding** | biến chữ thành số, chia học / thi, thử **5 mô hình ứng viên** trên cùng dữ liệu | Phần 2 – 8 |\n",
|
||
"| 5. **Lưu ý khi coding** | lỗi nào làm điểm đẹp **ảo** mà code không báo lỗi? | 0.2 (bỏ bản sao **trước** khi chia), 2.2 (học từ điển **chỉ** từ phần học) |\n",
|
||
"| 6. **Đánh giá các mô hình** | so mọi ứng viên trên cùng đề thi, chọn mô hình và **ngưỡng**, thử kẻ tấn công lừa mô hình | Phần 9, 10 |\n",
|
||
"| 7. **Quy tắc bàn tay phải** ✋ | gói lại thành điều mang theo khi đi làm | Phần 11 |\n",
|
||
"\n",
|
||
"Các mô hình ở Phần 3 – 8 **không phải bước bắt buộc nối tiếp nhau**: chúng là **các ứng viên** cho cùng bước 4, ta thử từng cái rồi chọn ở bước 6.\n",
|
||
"\n",
|
||
"📌 **Tra cứu nhanh:** *Chọn mô hình nào? Khi nào dùng gì, đánh đổi gì* — mục **Trước khi bắt đầu**, ngay dưới đây.\n",
|
||
"\n",
|
||
"### 📖 Thuật ngữ\n",
|
||
"\n",
|
||
"| Thuật ngữ | Nghĩa |\n",
|
||
"|---|---|\n",
|
||
"| **ham / spam** | tin thật / tin rác-lừa đảo. Trong code: 0 = ham, 1 = spam |\n",
|
||
"| **đặc trưng** (feature) | một con số mô tả tin nhắn (ở đây: điểm của từng từ) |\n",
|
||
"| **học / thi** (train / test) | phần dữ liệu cho máy học / phần giữ lại để chấm, máy chưa từng thấy |\n",
|
||
"| **TP / TN / FP / FN** | bắt đúng spam / cho qua đúng ham / **chặn nhầm** ham / **bỏ lọt** spam |\n",
|
||
"| **precision** | trong các tin máy **chặn**, bao nhiêu phần đúng là spam (cao = ít chặn nhầm) |\n",
|
||
"| **recall** | trong các tin spam **thật**, máy bắt được bao nhiêu phần (cao = ít bỏ lọt) |\n",
|
||
"| **F1** | một con số gộp precision và recall (cao khi cả hai cùng cao) |\n",
|
||
"| `.shape` | kích thước bảng / ma trận: (số hàng, số cột) |"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Trước khi bắt đầu — Chọn mô hình nào? Khi nào dùng gì, đánh đổi gì\n",
|
||
"\n",
|
||
"*(Đọc thêm — để tra lại khi cần. Lần đầu có thể đọc lướt, chạy xong bài rồi quay lại.)*\n",
|
||
"\n",
|
||
"\n",
|
||
"**Không có mô hình \"tốt nhất\" cho mọi bài toán** (*No Free Lunch*). Chọn mô hình = trả lời lần lượt **4 câu hỏi**:\n",
|
||
"\n",
|
||
"**① Có đáp án (nhãn) không?**\n",
|
||
"- **Có** nhãn từ quá khứ (email đã biết sạch / lừa đảo, file đã biết là mã độc) → **học có giám sát**: Naïve Bayes, Logistic Regression, cây quyết định, Random Forest, XGBoost, SVM.\n",
|
||
"- **Không** có nhãn, hoặc phải tìm cái **chưa từng thấy** (zero-day) → **học không giám sát**: Isolation Forest tìm điểm lạ, K-Means gom nhóm hành vi (ví dụ UBA: tách bot tự động khỏi thao tác của người mà không cần ai gắn nhãn).\n",
|
||
" ⚠️ Mô hình không giám sát chỉ nói \"**khác đám đông**\", không nói \"**xấu**\". Trên honeypot, đám đông chính là kẻ tấn công (Lab 3, 5.0).\n",
|
||
"- **Có rất ít** nhãn → bắt đầu bằng luật + không giám sát, rồi gom nhãn dần từ các cảnh báo đã được người phân tích xác nhận.\n",
|
||
"\n",
|
||
"**② Dữ liệu dạng gì?**\n",
|
||
"\n",
|
||
"| Dạng dữ liệu | Thường dùng | Trong khoá |\n",
|
||
"|---|---|---|\n",
|
||
"| **Bảng số** (mỗi đối tượng = một hàng số) | mô hình cây: cây quyết định, Random Forest, XGBoost · Logistic Regression làm mốc so sánh | Lab 3 (mỗi IP 7 số), M5, Lab 7 (hồ sơ hành vi), M8–M10 |\n",
|
||
"| **Văn bản** (email, tin nhắn, log dạng chữ) | Naïve Bayes / Logistic Regression trên số đếm từ (TF-IDF) · mô hình ngôn ngữ (BERT, LLM) khi cần hiểu ngữ cảnh | M1–M2 (Naïve Bayes), **bài này**, Lab 10 |\n",
|
||
"| **Chuỗi theo thời gian** (số kết nối mỗi giờ, CPU mỗi phút, chuỗi lời gọi API của một chương trình) | phân rã chuỗi thời gian (xu hướng + nhịp ngày + phần dư) · HMM, LSTM khi cần học **thứ tự** sự kiện | M4.2, Lab 6 Phần 3 |\n",
|
||
"| **Tệp nhị phân** (mã độc) | trích đặc trưng (kích thước, entropy…) rồi dùng mô hình cây · đổi file thành ảnh + CNN là hướng **nghiên cứu**, cần nhiều dữ liệu và GPU | M5 |\n",
|
||
"\n",
|
||
"**③ Sợ bỏ lọt hay sợ báo nhầm?**\n",
|
||
"- Sợ **bỏ lọt** (mã độc, xâm nhập, lỗ hổng khi pentest) → ưu tiên **recall**. Sợ **báo nhầm** (chặn email khách hàng, khoá tài khoản nhân viên) → ưu tiên **precision**.\n",
|
||
"- ⚠️ **Hiểu lầm hay gặp:** \"muốn precision cao thì chọn Naïve Bayes / SVM\". Thực ra gần như mọi mô hình đều ra một **điểm / xác suất**, và **ngưỡng cắt** điểm đó mới quyết định đánh đổi: hạ ngưỡng → bắt thêm nhưng báo nhầm thêm (bài này 6.3 và 9.3; Lab 3 9E.3). Chọn mô hình **tốt** rồi chọn **ngưỡng** theo sức đội SOC.\n",
|
||
"- Xác suất của Naïve Bayes thường **quá tự tin** (sát 0 hoặc 1); SVM mặc định không ra xác suất → muốn đặt ngưỡng theo xác suất phải **hiệu chỉnh** (calibration) trước.\n",
|
||
"- Tìm bất thường / gom nhóm: không có đáp án để tính precision, recall → dùng điểm bất thường, độ tách nhóm (Silhouette), và quan trọng nhất là **người phân tích xem lại top N** (Lab 3, 5.6).\n",
|
||
"\n",
|
||
"**④ Ràng buộc khi chạy thật**\n",
|
||
"- **Giải thích được:** cần báo cáo kiểm toán / ban giám đốc → **hộp trắng** (luật, cây quyết định nông, Logistic Regression, Naïve Bayes: đọc được nhánh IF/ELSE hoặc trọng số từ). Cần bắt kẻ tinh vi (APT, tấn công \"chậm mà chắc\") → chấp nhận **hộp đen** (Random Forest nhiều cây, XGBoost, deep learning) rồi giải trình bằng **SHAP** (Lab 3 Phần 7, Lab 6 4.2).\n",
|
||
"- **Tốc độ / phần cứng:** router, IoT, máy trạm (RAM < 1 GB, CPU yếu) → luật, cây nông, Logistic Regression, Isolation Forest nhỏ. Trung tâm dữ liệu / cloud có GPU → deep learning, LLM.\n",
|
||
"- **Mất cân bằng lớp** (tấn công hiếm): **không** mô hình nào tự xử lý — phải chỉnh `class_weight` hoặc ngưỡng. Đo ở Lab 3 (9E): tấn công chỉ 5% → Random Forest mặc định bắt được 65%, thêm `class_weight=\"balanced\"` → 81%, đổi lại báo nhầm tăng từ 3 lên 12 IP. Bài này đo ở 6.2: `class_weight=\"balanced\"` bớt bỏ lọt tin rác, đổi lại chặn nhầm tin thật tăng.\n",
|
||
"- **Bị né:** mô hình dựa vào đặc trưng mà kẻ tấn công **tự đổi được** (tên phần mềm, tốc độ thử) thì dễ bị né (bài này Phần 10: chèn \"từ an toàn\"; Lab 3 Phần 8).\n",
|
||
"\n",
|
||
"**Bảng tra nhanh: khi nào dùng gì, đánh đổi gì**\n",
|
||
"\n",
|
||
"| Công cụ | Dùng khi | Được | Phải chấp nhận | Trong khoá |\n",
|
||
"|---|---|---|---|---|\n",
|
||
"| **Luật / ngưỡng** (IF/ELSE) | kiểu tấn công **đã biết rõ** | nhanh, không cần dữ liệu học, ai cũng hiểu | chỉ bắt đúng cái đã viết; dễ bị né (thử chậm, Lab 3 1.2) | M1, **bài này Phần 1** |\n",
|
||
"| **Naïve Bayes** | văn bản, ít dữ liệu, cần nhanh | học rất nhanh, chạy được trên máy yếu, xem được từ nào \"nặng ký\" | coi các từ độc lập với nhau; xác suất quá tự tin; bị lách bằng cách đổi từ | M1–M2, **bài này Phần 7** |\n",
|
||
"| **Logistic Regression** | cần mốc so sánh đơn giản, cần giải thích bằng trọng số | nhanh, ổn định, xác suất dùng được | chỉ vẽ được ranh giới \"thẳng\"; quan hệ phức tạp thì kém | **bài này Phần 6** |\n",
|
||
"| **SVM** | dữ liệu vừa, nhiều chiều (văn bản) | tách tốt khi ít mẫu | chậm khi dữ liệu lớn; mặc định không ra xác suất; khó giải thích | **bài này Phần 4–5** |\n",
|
||
"| **Perceptron** | mốc đơn giản nhất: \"cộng điểm từng từ\" | cực đơn giản, nhanh, tự tính tay được (3.1) | chỉ ranh giới thẳng; không ra xác suất; dễ bị lách bằng cách đổi từ (10.1) | **bài này Phần 3** |\n",
|
||
"| **Hội đồng** (Voting) | đã có vài mô hình khá, muốn bù chỗ sai cho nhau | thường ổn định hơn từng mô hình | chậm hơn, khó giải thích hơn; chia phiếu đều **không** tự động hơn mô hình giỏi nhất (8.2) | **bài này Phần 8** |\n",
|
||
"| **Cây quyết định** | cần luật đọc được cho người không chuyên | ra đúng dạng IF/ELSE | cây sâu dễ \"học thuộc lòng\" (overfit); dữ liệu đổi chút là cây đổi | Lab M2 (M2.3), M5 |\n",
|
||
"| **Random Forest** | dữ liệu bảng **có nhãn**, cần ổn định | chính xác, ít phải tinh chỉnh, ít overfit hơn một cây | khó đọc từng quyết định (cần SHAP / độ quan trọng đặc trưng); nặng hơn một cây | Lab 3 Phần 9, M8–M10 |\n",
|
||
"| **XGBoost** | dữ liệu bảng lớn, cần chính xác nhất | thường đứng đầu trên dữ liệu bảng; chạy được GPU | nhiều tham số phải chỉnh, chỉnh ẩu dễ overfit; khó giải thích | M5, Lab 10 5.1 |\n",
|
||
"| **Isolation Forest** | **không có nhãn**, tìm cái lạ | không cần ai gắn nhãn; ra **thứ hạng ưu tiên** | \"lạ\" ≠ \"xấu\"; dữ liệu bẩn làm lệch (Lab 3 Phần 6); phải chọn `contamination` nếu dùng `predict` (Lab 3 5.6) | Lab 3 Phần 5, Lab 6 Phần 4, Lab 7 Phần 7 |\n",
|
||
"| **K-Means** | gom nhóm hành vi (người dùng, IP) | thấy được \"các kiểu\" hành vi | phải chọn trước số nhóm k; phải chuẩn hoá; chỉ hợp với nhóm \"tròn\", cỡ gần nhau | Lab 6 Phần 6, Lab 7 |\n",
|
||
"| **Phân rã chuỗi thời gian** | số liệu theo giờ / ngày có nhịp lặp | tách nhịp bình thường khỏi đột biến | cần chuỗi đủ dài; nhịp thay đổi thì phải tính lại | M4.2, Lab 6 Phần 3 |\n",
|
||
"| **Deep learning** (LSTM, CNN) | dữ liệu rất lớn, dạng chuỗi / ảnh | học được mẫu phức tạp | cần rất nhiều dữ liệu và GPU; hộp đen | Lab 10 5.1 (mạng nơ-ron MLP nhỏ) |\n",
|
||
"| **LLM** | đọc, tóm tắt, giải thích log / cảnh báo | viết báo cáo nhanh | có thể bịa; tốn tiền; **không** để LLM tự quyết định chặn | Lab 3 Phần 10 |\n",
|
||
"\n",
|
||
"> **Tóm lại:** chọn mô hình là bài toán **cân đối** (thiết kế cả hệ thống, không chỉ chọn một thuật toán): biết mình **có dữ liệu gì** (có nhãn không, dạng bảng / chữ / chuỗi) và **cần tối ưu điều gì** (bỏ lọt hay báo nhầm, giải thích hay tốc độ). Hệ thống thật hầu như luôn **kết hợp nhiều lớp** (luật + không giám sát + có giám sát) — như hội đồng ở Phần 8 và phân luồng 3 làn ở 6.3; quy tắc bàn tay ở Lab 3 Phần 11.\n",
|
||
"\n",
|
||
"📌 **Pipeline xử lý luôn là 7 bước** (chi tiết ở bảng đầu bài):\n",
|
||
"1. **Thu thập dữ liệu** — có nhãn hay không nhãn?\n",
|
||
"2. **Làm sạch dữ liệu**\n",
|
||
"3. **Chọn mô hình phù hợp** với bài toán (4 câu hỏi ① – ④ ở trên)\n",
|
||
"4. **Coding**\n",
|
||
"5. **Lưu ý khi coding** — những lỗi không báo lỗi\n",
|
||
"6. **Đánh giá các mô hình**\n",
|
||
"7. **Quy tắc bàn tay phải** ✋"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 0 — Thu thập dữ liệu: dữ liệu gì, lấy ở đâu, có nhãn chưa, có cần làm sạch?\n",
|
||
"\n",
|
||
"📍 **Bước 1 — Thu thập dữ liệu** (0.1) và **Bước 2 — Làm sạch** (0.2).\n",
|
||
"\n",
|
||
"**5 câu hỏi về dữ liệu — trả lời trước khi viết dòng code nào:** (nguồn: file `SMSSpamCollection_readme.txt` đi kèm)\n",
|
||
"\n",
|
||
"| Câu hỏi | Trả lời cho dữ liệu bài này |\n",
|
||
"|---|---|\n",
|
||
"| **Dữ liệu gì?** | **văn bản**: mỗi dòng một tin nhắn SMS tiếng Anh, kèm nhãn. Bao nhiêu tin, bao nhiêu tin rác thì ô 0.1 đếm |\n",
|
||
"| **Lấy ở đâu?** | bộ *SMS Spam Collection v.1* (T. A. Almeida & J. M. Gómez Hidalgo, 2011), gom từ 4 nguồn công khai cho nghiên cứu |\n",
|
||
"| **Thu thập thế nào?** | tin **rác**: chép tay từ diễn đàn Grumbletext (Anh), nơi người dùng đăng tin rác họ nhận được, và từ bộ SMS Spam Corpus v.0.1. Tin **thường**: sinh viên Đại học Quốc gia Singapore **tình nguyện** chia sẻ (biết trước tin sẽ công khai), luận án của C. Tagg, và SMS Spam Corpus v.0.1 |\n",
|
||
"| **Có nhãn chưa?** | **Có**: `ham` / `spam` do **tác giả bộ dữ liệu** gắn theo nguồn lấy tin → dùng được học **có giám sát** |\n",
|
||
"| **Có cần làm sạch?** | **Có**: bản sao (tin rác gửi hàng loạt, 0.2), viết tắt và ký hiệu tiền tệ (sẽ thấy ngay ở 0.1). Đọc file đúng bảng mã UTF-8, nếu không ký tự £ thành \"£\" (0.1) |\n",
|
||
"\n",
|
||
"Giờ đọc file và đếm (0.1), rồi bỏ bản sao (0.2).\n",
|
||
"\n",
|
||
"### 0.1 Đọc và xem tin nhắn\n",
|
||
"\n",
|
||
"Cả bài chỉ dùng **một file**: `/opt/csai/labs/day4_phishing/SMSSpamCollection`. Mỗi dòng là một tin nhắn gồm 2 phần: **nhãn** (`ham` = tin thường, `spam` = tin rác / lừa đảo) và **nội dung**. Ô dưới đọc file thành bảng, rồi đếm có bao nhiêu tin thường, bao nhiêu tin rác."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import time\n",
|
||
"import warnings\n",
|
||
"import numpy as np\n",
|
||
"import pandas as pd\n",
|
||
"import matplotlib.pyplot as plt\n",
|
||
"%matplotlib inline\n",
|
||
"pd.set_option(\"display.max_colwidth\", 120)\n",
|
||
"pd.set_option(\"display.width\", 200)\n",
|
||
"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ả\n",
|
||
"\n",
|
||
"# 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;\n",
|
||
"# 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);\n",
|
||
"# encoding=\"utf-8\" = bảng mã của file (đọc sai bảng mã thì ký tự £ thành \"£\")\n",
|
||
"du_lieu = pd.read_csv(\"/opt/csai/labs/day4_phishing/SMSSpamCollection\", sep=\"\\t\", header=None,\n",
|
||
" names=[\"nhan\", \"noi_dung\"], quoting=3, encoding=\"utf-8\")\n",
|
||
"# 📦 du_lieu = bảng 2 cột: nhan (ham / spam), noi_dung (tin nhắn)\n",
|
||
"print(\"du_lieu.shape =\", du_lieu.shape, \"-> (số tin nhắn, số cột)\")\n",
|
||
"dem = du_lieu.nhan.value_counts() # đếm số tin theo từng nhãn\n",
|
||
"print(f\"Có {len(du_lieu):,} tin: {dem['ham']:,} tin thường (ham) + {dem['spam']:,} tin rác (spam)\")\n",
|
||
"print(f\"Tỉ lệ spam: {(du_lieu.nhan == 'spam').mean():.1%}\")\n",
|
||
"print(\"\\n3 tin THẬT (ham) và 3 tin RÁC (spam) đầu tiên:\")\n",
|
||
"print(pd.concat([du_lieu[du_lieu.nhan == \"ham\"].head(3), du_lieu[du_lieu.nhan == \"spam\"].head(3)]).to_string())"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** `du_lieu.shape = (5574, 2)` · `Có 5,574 tin: 4,827 tin thường (ham) + 747 tin rác (spam)` · `Tỉ lệ spam: 13.4%`, rồi 3 tin thật (`Go until jurong point...`, `Ok lar... Joking wif u oni...`) và 3 tin rác (`Free entry in 2 a wkly comp to win FA Cup final...`, `WINNER!! ... £900 prize reward! To claim call 09061701461...`).\n",
|
||
"\n",
|
||
"📦 **`du_lieu`** = bảng 5.574 tin, 2 cột `nhan`, `noi_dung` · **`dem`** = số tin theo nhãn.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** tin thật viết tắt, đời thường (\"wif u\", \"lor\"); tin rác có **giải thưởng, số điện thoại, mã, giá tiền**. Cứ **8 tin thì 1 tin rác** — đây là bài toán **mất cân bằng lớp**, sẽ quan trọng ở Phần 6 và 9."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 0.2 Làm sạch dữ liệu: cái bẫy bản sao\n",
|
||
"\n",
|
||
"Tin rác được gửi **hàng loạt**, nên cùng một nội dung xuất hiện nhiều lần. Nếu chia học / thi ngẫu nhiên khi còn bản sao, một tin trong đề thi có thể **đã nằm sẵn** trong phần học → máy chỉ cần \"nhớ\" là đúng, điểm cao **ảo** (đúng cái bẫy \"100%\" của bài M1.3). Ta đếm bản sao, rồi **bỏ bản sao trước khi chia**."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.model_selection import train_test_split\n",
|
||
"so_ban_sao = du_lieu.noi_dung.duplicated().sum() # duplicated() = True cho tin trùng với một tin phía trên\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())\n",
|
||
"# 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?\n",
|
||
"hoc_tam, thi_tam = train_test_split(du_lieu.index, test_size=0.25, random_state=RANDOM_STATE, stratify=du_lieu.nhan)\n",
|
||
"lo_de = du_lieu.noi_dung[thi_tam].isin(set(du_lieu.noi_dung[hoc_tam])).sum()\n",
|
||
"print(f\"Nếu không bỏ bản sao: {lo_de} tin trong đề thi ĐÃ CÓ trong phần học (lộ đề)\")\n",
|
||
"\n",
|
||
"tin = du_lieu.drop_duplicates(\"noi_dung\").reset_index(drop=True) # giữ 1 bản cho mỗi nội dung\n",
|
||
"y = (tin.nhan == \"spam\").astype(int) # nhãn dạng số: 1 = spam, 0 = ham\n",
|
||
"# 📦 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\n",
|
||
"print(f\"\\nSau khi bỏ bản sao: {len(tin)} tin, {y.sum()} spam ({y.mean():.1%})\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** `Số tin là bản sao của tin khác: 403 | trong đó spam: 94` · `Nếu không bỏ bản sao: 161 tin trong đề thi ĐÃ CÓ trong phần học (lộ đề)` · `Sau khi bỏ bản sao: 5171 tin, 653 spam (12.6%)`\n",
|
||
"\n",
|
||
"📦 **`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ự.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** dữ liệu **thật** cũng có bản sao (tin rác gửi hàng loạt). Không bỏ thì 161 câu trong đề thi đã có đáp án sẵn — điểm sẽ cao **ảo**. Luôn kiểm tra bản sao **trước** khi chia."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 0.3 Vậy đây là bài toán gì? Chọn cách giải nào?\n",
|
||
"\n",
|
||
"📍 **Bước 3 — Chọn mô hình phù hợp với bài toán.**\n",
|
||
"\n",
|
||
"Một tin đến → **chặn** hay **cho qua**: bài **phân loại 2 lớp** trên **văn bản có nhãn** → **học có giám sát**, phải **biến chữ thành số** trước (Phần 2). Tin rác chiếm khoảng 1/8 → **mất cân bằng lớp**: không chỉ nhìn accuracy (Phần 6, 9). Cách cũ (luật từ khóa) là **mốc** mà mô hình nào cũng phải vượt (Phần 1).\n",
|
||
"\n",
|
||
"**Nếu thay bằng dữ liệu của chính bạn** (SMS / email của công ty, tiếng Việt) thì phải làm thêm gì? Đi lại đúng pipeline:\n",
|
||
"\n",
|
||
"| Bước | Bộ dữ liệu bài này (tải sẵn trên mạng) | Dữ liệu của bạn |\n",
|
||
"|---|---|---|\n",
|
||
"| 1. Thu thập | đã gom sẵn, đã công khai | xin phép, **che thông tin cá nhân** (số điện thoại, tên, số tài khoản) trước khi đưa vào máy học; gom đủ cả tin thường lẫn tin rác |\n",
|
||
"| Nhãn | **có sẵn** (tác giả gắn theo nguồn) | **phải tự gắn**: ai gắn (người phân tích, hay người dùng bấm \"báo cáo rác\"), quy tắc gắn viết rõ, cho 2 người cùng gắn một phần để xem có lệch nhau không. Nhãn sai thì mô hình học sai |\n",
|
||
"| 2. Làm sạch | bỏ bản sao (0.2) | bản sao + **chuẩn hoá chữ tiếng Việt**: một kiểu mã Unicode, có dấu / không dấu (2.1b) |\n",
|
||
"| 3. Chọn mô hình | TF-IDF + 5 ứng viên | vẫn các ứng viên đó, nhưng chữ tiếng Việt phải **tách từ** trước (`underthesea`, `pyvi`) hoặc lấy cặp từ `ngram_range=(1, 2)` (2.1b); cần hiểu ngữ cảnh thì dùng mô hình ngôn ngữ tiếng Việt |\n",
|
||
"| 4 – 6. Coding, lưu ý, đánh giá | số đo trong bài | **huấn luyện và đo lại** trên dữ liệu của bạn: con số của bài này **không mang sang** được; tỉ lệ tin rác khác thì phải chọn lại **ngưỡng** (9.3) |\n",
|
||
"\n",
|
||
"👉 **Hiểu:** chọn cách giải **sau khi** đã trả lời 5 câu hỏi về dữ liệu (đầu Phần 0). Đổi sang dữ liệu khác thì **cả pipeline chạy lại từ bước 1**, không chỉ thay file."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 1 — Luật cứng (từ khóa) thất bại thế nào?\n",
|
||
"\n",
|
||
"📍 **Bước 3 — Chọn mô hình (mốc so sánh):** trước khi dùng máy học, đo xem cách cũ làm được đến đâu. Mô hình nào cũng phải vượt mốc này.\n",
|
||
"\n",
|
||
"Cách cũ: viết tay danh sách từ khóa, tin nào chứa một từ trong danh sách → chặn. Ta chạy luật đó trên **toàn bộ** 5.171 tin.\n",
|
||
"\n",
|
||
"Luật so khớp thế nào (đọc trước khi sửa `TU_KHOA`):\n",
|
||
"- Tin SMS **không có tiêu đề**: luật dò trong **toàn bộ nội dung** tin nhắn.\n",
|
||
"- **Không phân biệt hoa / thường**: cả tin nhắn lẫn từ khóa đều được đổi sang chữ thường trước khi so, nên `\"Divorce\"` và `\"divorce\"` như nhau.\n",
|
||
"- So theo **chuỗi con**: từ khóa `\"win\"` khớp cả `\"wine\"`, `\"window\"`, `\"wins\"` (59 tin thường chứa chuỗi `win`) — một nguồn chặn nhầm."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# ✏️ 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\n",
|
||
"TU_KHOA = [\"free\", \"win\", \"winner\", \"prize\", \"call now\", \"claim\", \"urgent\", \"cash\", \"txt\", \"reply\"]\n",
|
||
"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)\n",
|
||
"chu_thuong = tin.noi_dung.str.lower() # đổi cả nội dung tin sang chữ thường\n",
|
||
"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\n",
|
||
"chan_nham = ((bi_chan == 1) & (y == 0)).sum()\n",
|
||
"bo_lot = ((bi_chan == 0) & (y == 1)).sum()\n",
|
||
"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)\")\n",
|
||
"print(\"\\nVí dụ tin THẬT bị chặn nhầm:\")\n",
|
||
"print(tin.noi_dung[(bi_chan == 1) & (y == 0)].head(4).to_string())\n",
|
||
"print(\"\\nVí dụ tin RÁC lọt qua:\")\n",
|
||
"print(tin.noi_dung[(bi_chan == 0) & (y == 1)].head(4).to_string())"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** `Luật 10 từ khóa: chặn 643 tin | CHẶN NHẦM 189 tin thật | BỎ LỌT 199 tin rác (trên 653 tin rác)`, ví dụ chặn nhầm: `I am waiting machan. Call me once you free.`, `Aight, I'll hit you up when I get some cash`; ví dụ lọt: `Did you hear about the new \"Divorce Barbie\"?...`, `Customer service annoncement. You have a New Years delivery waiting for you. Please call 07046744435 now...`, `Are you unique enough? ... www.areyouunique.co.uk`.\n",
|
||
"\n",
|
||
"📦 **`TU_KHOA`** = danh sách từ khóa (sửa được) · **`tu_khoa_thuong`** = cùng danh sách, đổi sang chữ thường · **`bi_chan`** = 1 nếu tin chứa từ khóa.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** \"free\" trong *\"call me once you free\"* (rảnh) bị coi như \"free\" trong *\"FREE prize\"* (miễn phí) → **chặn nhầm 189 tin thật**. Tin rác không dùng đúng từ trong danh sách thì **lọt 199 tin** (gần 1/3). Thêm từ khóa thì bắt thêm nhưng cũng chặn nhầm thêm — vòng luẩn quẩn của luật cứng.\n",
|
||
"\n",
|
||
"**Thử sửa** (đã đo): thêm `\"Divorce\"` vào `TU_KHOA` → `chặn 645 | CHẶN NHẦM 190 | BỎ LỌT 198`: bắt thêm đúng **1** tin rác (*\"Divorce Barbie\"*) nhưng chặn nhầm luôn tin thật *\"We got a divorce. Lol...\"*. Cả 5.171 tin chỉ có 2 tin chứa chữ này. Thêm cả `\"unique \"` và `\"divorce\"` → `chặn 648 | CHẶN NHẦM 191 | BỎ LỌT 196`: bắt thêm 3 tin rác, chặn nhầm thêm 2 tin thật (một tin thật viết *\"...a unique person like u...\"*). Mỗi từ khóa mới chỉ vá **một** lỗ, và thường mở ra một lỗ khác."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 2 — Biến chữ thành số, chia học / thi\n",
|
||
"\n",
|
||
"📍 **Bước 4 — Coding:** biến chữ thành số, rồi chia dữ liệu thành phần học và phần thi. 2.2 là một **lưu ý khi coding** (bước 5): từ điển chỉ được học từ phần học.\n",
|
||
"\n",
|
||
"Máy học chỉ tính được trên **số**. **TF-IDF** (*Term Frequency – Inverse Document Frequency*) là cách kinh điển để biến văn bản thành số: mỗi tin nhắn thành **một hàng số**, mỗi cột là **một từ** trong từ điển.\n",
|
||
"\n",
|
||
"**Khái niệm** — điểm của một từ trong một tin = **TF × IDF**:\n",
|
||
"- **TF** (tần suất trong tin): từ lặp lại nhiều trong tin này → điểm cao hơn;\n",
|
||
"- **IDF** (độ hiếm trên mọi tin): từ xuất hiện ở **hầu hết** tin (\"you\", \"the\") → điểm bị hạ; từ hiếm (\"prize\", \"claim\") → điểm cao.\n",
|
||
"\n",
|
||
"**Thuật toán** (4 bước, máy làm hết):\n",
|
||
"1. **Tách từ**: đổi chữ thường, cắt câu thành từ (bỏ dấu câu, bỏ từ chỉ 1 ký tự).\n",
|
||
"2. **Lập từ điển**: gom mọi từ khác nhau trong dữ liệu học, mỗi từ một cột.\n",
|
||
"3. **Tính điểm**: TF = số lần từ xuất hiện trong tin; IDF ≈ log(số tin ÷ số tin có từ đó) (scikit-learn cộng thêm 1 để không bao giờ ra 0): từ càng phổ biến, IDF càng nhỏ.\n",
|
||
"4. **Chuẩn hoá** mỗi hàng về cùng độ dài, để tin dài không \"nặng ký\" hơn tin ngắn.\n",
|
||
"\n",
|
||
"**Thư viện**: `TfidfVectorizer` của **scikit-learn** làm cả 4 bước: `fit` = học từ điển + IDF, `transform` = đổi văn bản thành số.\n",
|
||
"\n",
|
||
"**Ứng dụng**: lọc thư rác / lừa đảo (bài này), máy tìm kiếm xếp hạng tài liệu, phân loại log / câu lệnh, tìm văn bản giống nhau, rút từ khoá.\n",
|
||
"**Giới hạn**: TF-IDF chỉ **đếm từ**, không hiểu **thứ tự và nghĩa** (\"not free\" và \"free\" gần như như nhau); từ chưa có trong từ điển bị bỏ qua. Cần hiểu ngữ cảnh thì dùng mô hình ngôn ngữ (BERT, LLM).\n",
|
||
"\n",
|
||
"### 2.1 Thử TF-IDF trên 3 tin\n",
|
||
"\n",
|
||
"**Ô này để làm gì?** Biến chữ thành số là bước **bắt buộc**: mọi mô hình từ Phần 3 trở đi chỉ nhận **bảng số**. Ô này là bản **thu nhỏ** (3 câu) để bạn **nhìn tận mắt** bảng số đó trông thế nào và vì sao từ hiếm được điểm cao; 2.2 làm thật trên 5.171 tin."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
|
||
"# ✏️ SỬA: 3 câu thử (tiếng Anh, như dữ liệu; tiếng Việt: xem 2.1b)\n",
|
||
"cau = [\"WIN a FREE prize now, call to claim\", \"are you free now? can i call you\", \"see you at home tonight\"]\n",
|
||
"thu = TfidfVectorizer()\n",
|
||
"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ố\n",
|
||
"print(\"Từ điển:\", list(thu.get_feature_names_out()))\n",
|
||
"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())"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** từ điển 14 từ, rồi bảng 3 hàng × 14 cột. Câu 1 có điểm ở `claim, prize, win, to` (0,42) và `call, free, now` (0,32); câu 2 có `you` (0,62), `are, can` (0,41).\n",
|
||
"\n",
|
||
"👉 **Hiểu:** `claim`, `prize`, `win` chỉ có trong câu 1 → điểm **cao** (0,42); `call`, `free`, `now` có cả ở câu 1 và câu 2 → điểm **thấp hơn** (0,32) — đó là IDF \"phạt\" từ phổ biến. Mỗi câu giờ là **một hàng số** máy tính toán được."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 2.1b (Tuỳ chọn) Còn tin nhắn tiếng Việt thì sao?\n",
|
||
"\n",
|
||
"`TfidfVectorizer` đọc được chữ có dấu, nhưng tiếng Việt có 3 điểm cần biết:\n",
|
||
"- Nó cắt theo **dấu cách**, mà tiếng Việt cách nhau theo **âm tiết** → \"chuyển khoản\" thành 2 từ rời \"chuyển\" và \"khoản\". Cách sửa nhanh: `ngram_range=(1, 2)` để lấy thêm **cặp từ** liền nhau; cách chuẩn: dùng thư viện **tách từ** tiếng Việt (ví dụ `underthesea`, `pyvi`) trước khi đưa vào TF-IDF.\n",
|
||
"- Từ **1 ký tự** bị bỏ (\"ở\", \"à\", \"ạ\").\n",
|
||
"- Cùng một chữ có thể được gõ theo **2 kiểu mã Unicode** (dựng sẵn / tổ hợp) hoặc **không dấu** (\"chuyen khoan\") → máy coi là **từ khác nhau**. Phải **chuẩn hoá** về một kiểu (`unicodedata.normalize(\"NFC\", ...)`) trước."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import unicodedata\n",
|
||
"# ✏️ SỬA: thử tin nhắn tiếng Việt của bạn\n",
|
||
"cau_viet = [\"Chúc mừng bạn trúng thưởng 50 triệu, bấm link để nhận ngay\",\n",
|
||
" \"Tối nay mình ở nhà, mai chuyển khoản cho bạn nhé\",\n",
|
||
" \"Tài khoản của bạn bị khoá, chuyển khoản phí 200k để mở lại\"]\n",
|
||
"tach_don = TfidfVectorizer().fit(cau_viet) # mặc định: mỗi âm tiết là một \"từ\"\n",
|
||
"print(\"Từ điển (từ đơn):\", list(tach_don.get_feature_names_out()))\n",
|
||
"tach_cap = TfidfVectorizer(ngram_range=(1, 2)).fit(cau_viet) # thêm cặp 2 âm tiết liền nhau\n",
|
||
"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])\n",
|
||
"\n",
|
||
"# 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\n",
|
||
"ba_kieu = [\"chuyển khoản\", unicodedata.normalize(\"NFD\", \"chuyển khoản\"), \"chuyen khoan\"]\n",
|
||
"print(\"\\n3 kiểu gõ ->\", list(TfidfVectorizer().fit(ba_kieu).get_feature_names_out()))\n",
|
||
"print(\"Sau khi chuẩn hoá NFC ->\", list(TfidfVectorizer().fit([unicodedata.normalize(\"NFC\", c) for c in ba_kieu]).get_feature_names_out()))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:**\n",
|
||
"- Từ điển từ đơn: 29 \"từ\", toàn **âm tiết rời**: `chuyển`, `khoản`, `chúc`, `mừng`, `trúng`, `thưởng`… — **không có `ở`** (1 ký tự bị bỏ).\n",
|
||
"- Với `ngram_range=(1, 2)`: có thêm cặp `chuyển khoản`, `chúc mừng`, `bấm link`, `bị khoá`, `50 triệu`…\n",
|
||
"- 3 kiểu gõ \"chuyển khoản\" → `['chuye', 'chuyen', 'chuyển', 'khoa', 'khoan', 'khoản']`; sau khi chuẩn hoá NFC → `['chuyen', 'chuyển', 'khoan', 'khoản']`.\n",
|
||
"\n",
|
||
"📦 **`cau_viet`** = 3 câu thử (sửa được) · **`tach_don`**, **`tach_cap`** = hai bộ TF-IDF (từ đơn / thêm cặp từ) · **`ba_kieu`** = 3 cách gõ cùng một chữ.\n",
|
||
"\n",
|
||
"👉 **Hiểu:**\n",
|
||
"- **Tiếng Việt khác tiếng Anh ở đâu khi xử lý?**\n",
|
||
"\n",
|
||
"| | Tiếng Anh | Tiếng Việt |\n",
|
||
"|---|---|---|\n",
|
||
"| Dấu cách tách ra | **từ** (`prize`, `claim`) | **âm tiết**; một từ thường 2 – 3 âm tiết (`chuyển khoản`, `tài khoản`) |\n",
|
||
"| Dấu | gần như không có | dấu thanh, dấu mũ / móc; 2 kiểu mã Unicode; nhiều người gõ **không dấu** |\n",
|
||
"| Từ 1 ký tự | ít quan trọng (`a`, `I`) | có nghĩa (`ở`, `à`, `ạ`) nhưng bị bỏ mặc định |\n",
|
||
"| Chuẩn bị trước TF-IDF | dùng ngay | chuẩn hoá NFC → tách từ (`underthesea`, `pyvi`) → TF-IDF có cặp từ |\n",
|
||
"| Dữ liệu có nhãn | nhiều bộ công khai (như bộ của bài này) | ít bộ công khai hơn, thường phải tự gom và gắn nhãn |\n",
|
||
"\n",
|
||
"- Tách theo âm tiết thì \"chuyển\" (trong *chuyển khoản*) và \"chuyển\" (trong *chuyển nhà*) là **một cột** — mất nghĩa. Thêm **cặp từ** giữ lại được cụm đáng ngờ như `chuyển khoản`, `bấm link`, `bị khoá`.\n",
|
||
"- Kiểu gõ **tổ hợp** (NFD) làm chữ vỡ thành `chuye`, `khoa`: máy không nhận ra là cùng một từ. Chuẩn hoá NFC sửa được; còn **không dấu** (`chuyen`) vẫn là từ khác → tin lừa đảo gõ không dấu sẽ né được bộ lọc học trên tin có dấu, trừ khi bạn **bỏ dấu cả hai** hoặc thêm dữ liệu không dấu.\n",
|
||
"- Làm bộ lọc tiếng Việt thật: **chuẩn hoá Unicode → tách từ tiếng Việt → TF-IDF có cặp từ**, và cần **dữ liệu tin nhắn tiếng Việt có nhãn**."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 2.2 Chia học / thi, rồi học từ điển CHỈ từ phần học\n",
|
||
"\n",
|
||
"**Dữ liệu học và thi ở đâu?** Không có file riêng nào cả: cả hai được **cắt ra từ chính bảng `tin`** (5.171 tin không trùng, tạo ở 0.2) ngay trong ô dưới đây:\n",
|
||
"\n",
|
||
"```\n",
|
||
"SMSSpamCollection (5.574 tin) --0.1 đọc--> du_lieu --0.2 bỏ bản sao--> tin (5.171 tin)\n",
|
||
" |-- 75% --> phần HỌC: máy được xem cả nội dung lẫn nhãn\n",
|
||
" '-- 25% --> phần THI: máy KHÔNG được học, chỉ dùng để chấm điểm\n",
|
||
"```\n",
|
||
"\n",
|
||
"`train_test_split` không chép dữ liệu, nó trả về **số thứ tự hàng** (vị trí) của từng phần trong bảng `tin`. Muốn xem một tin của phần học: `tin.loc[vi_tri_hoc[0]]`.\n",
|
||
"\n",
|
||
"**Vì sao 75 / 25? Đổi được không?** Được: sửa `TI_LE_THI`. Đây là một **đánh đổi**: phần học lớn → máy học tốt hơn; phần thi lớn → điểm chấm **ổn định** hơn, ít phụ thuộc may rủi. Thường chọn phần thi 20 – 30%. Đo thật với SVM (Phần 4), mỗi tỉ lệ chia ngẫu nhiên 10 lần:\n",
|
||
"\n",
|
||
"| phần thi | số tin thi (tin rác) | F1 trung bình | F1 thấp nhất – cao nhất |\n",
|
||
"|---|---|---|---|\n",
|
||
"| 10% | 518 (65) | 0,945 | 0,917 – 0,985 |\n",
|
||
"| 20% | 1.035 (131) | 0,939 | 0,911 – 0,973 |\n",
|
||
"| **25%** (bài này) | 1.293 (163) | 0,936 | 0,906 – 0,959 |\n",
|
||
"| 30% | 1.552 (196) | 0,936 | 0,911 – 0,961 |\n",
|
||
"| 50% | 2.586 (327) | 0,931 | 0,910 – 0,940 |\n",
|
||
"\n",
|
||
"Phần thi 10% chỉ có 65 tin rác: bắt sai vài tin là điểm nhảy mạnh (0,917 → 0,985), may thì đẹp, rủi thì xấu. Phần thi 50% chấm ổn định hơn nhưng máy chỉ còn học một nửa nên điểm trung bình thấp hơn. ⚠️ Đổi tỉ lệ thì **mọi con số \"Kết quả\" bên dưới sẽ khác** (chúng đo với 0,25). Lab 10 Phần 1 đi sâu cách chấm cho chắc (kiểm định chéo)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# ✏️ 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\n",
|
||
"TI_LE_THI = 0.25\n",
|
||
"vi_tri_hoc, vi_tri_thi = train_test_split(tin.index, test_size=TI_LE_THI, random_state=RANDOM_STATE, stratify=y)\n",
|
||
"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)\")\n",
|
||
"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]))\n",
|
||
"tfidf = TfidfVectorizer()\n",
|
||
"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ố\n",
|
||
"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)\n",
|
||
"y_hoc, y_thi = y[vi_tri_hoc], y[vi_tri_thi]\n",
|
||
"tu_dien = tfidf.get_feature_names_out()\n",
|
||
"# 📦 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ừ\n",
|
||
"print(\"X_hoc.shape =\", X_hoc.shape, \"| X_thi.shape =\", X_thi.shape, \"-> (số tin, số từ trong từ điển)\")\n",
|
||
"print(f\"Phần thi: {len(y_thi)} tin, {y_thi.sum()} spam\")\n",
|
||
"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\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** `Bảng tin: 5171 tin -> phần học 3878 tin (490 spam) + phần thi 1293 tin (163 spam)` · `5 vị trí đầu của phần học: [196, 2980, 2025, 1674, 3260] | của phần thi: [3125, 2829, 2272, 3309, 136]` · `X_hoc.shape = (3878, 7395) | X_thi.shape = (1293, 7395)` · `Phần thi: 1293 tin, 163 spam` · `Mốc 'lười': đoán mọi tin là ham -> đúng 87.4% — nhưng không bắt được tin rác nào`\n",
|
||
"\n",
|
||
"📦 **`vi_tri_hoc`, `vi_tri_thi`** = số thứ tự hàng trong `tin` của phần học / phần thi · **`X_hoc`, `X_thi`** = ma trận số (mỗi hàng 1 tin, mỗi cột 1 trong 7.395 từ) · **`y_hoc`, `y_thi`** = nhãn · **`tu_dien`** = 7.395 từ · **`tfidf`** = bộ biến đổi đã học từ điển (dùng lại ở Phần 10).\n",
|
||
"\n",
|
||
"👉 **Hiểu:** **87,4%** là con số mọi mô hình phải vượt — một bộ lọc \"không làm gì\" đã đạt nó. Vì vậy **accuracy một mình không nói lên gì**; Phần 9 sẽ dùng precision / recall / F1."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 3 — Perceptron: \"anh bảo vệ\" cộng điểm\n",
|
||
"\n",
|
||
"📍 **Bước 4 — Coding, ứng viên 1/5.** Dữ liệu đã sạch (Phần 0), đã thành số và chia học / thi (Phần 2). Từ đây đến Phần 8 ta thử lần lượt **5 mô hình** trên **cùng** phần học, chấm trên **cùng** phần thi, để so công bằng. Perceptron **không phải bước bắt buộc**: nó là ứng viên **đơn giản nhất**, làm mốc cho các ứng viên sau.\n",
|
||
"\n",
|
||
"**Perceptron là gì?** Là mô hình máy học **đầu tiên và đơn giản nhất** (Frank Rosenblatt, 1958), mô phỏng **một tế bào thần kinh**: nhận nhiều tín hiệu, mỗi tín hiệu nhân với một **trọng số**, cộng lại; tổng vượt ngưỡng thì \"kích hoạt\" (báo spam).\n",
|
||
"\n",
|
||
"Hình dung **anh bảo vệ** ở cổng: mỗi dấu hiệu đáng ngờ (đeo khẩu trang kín, đi lúc 2 giờ sáng) **cộng điểm**, mỗi dấu hiệu đáng tin (đeo thẻ nhân viên) **trừ điểm**; tổng điểm vượt mức thì chặn lại hỏi.\n",
|
||
"\n",
|
||
"**Vì sao học Perceptron ở đây?**\n",
|
||
"1. Nó là bước **nâng cấp trực tiếp** của luật từ khóa ở Phần 1: vẫn là \"cộng điểm theo từ\", nhưng **máy tự chọn** từ nào đáng ngờ và nặng bao nhiêu, thay cho người viết tay danh sách.\n",
|
||
"2. Nó là **bộ khung chung** của các mô hình tiếp theo: SVM (Phần 4) và Logistic Regression (Phần 6) cũng tính **tổng có trọng số** như vậy, chỉ khác cách chọn trọng số và cách ra kết quả.\n",
|
||
"3. Nó là **viên gạch** của mạng nơ-ron và deep learning: mạng nơ-ron = rất nhiều \"nơ-ron\" giống Perceptron (thêm một hàm kích hoạt mềm hơn) nối thành nhiều lớp.\n",
|
||
"\n",
|
||
"**Cách tính:** mỗi từ có một **trọng số** (số dương = đáng ngờ, số âm = đáng tin). Với một tin nhắn:\n",
|
||
"\n",
|
||
"**điểm = (trọng số từ 1 × giá trị TF-IDF từ 1) + (trọng số từ 2 × ...) + ... + bias**\n",
|
||
"\n",
|
||
"Điểm > 0 → **spam**, ngược lại → **ham**. Chỉ có Có / Không, không có \"có lẽ\". **Học** = mỗi lần đoán sai, chỉnh trọng số một chút theo hướng sửa lỗi, lặp lại nhiều vòng."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.linear_model import Perceptron\n",
|
||
"from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix\n",
|
||
"\n",
|
||
"def do_luong(ten, du_doan):\n",
|
||
" \"\"\"Trả về 1 hàng kết quả: accuracy, precision, recall, F1, số chặn nhầm (FP), số bỏ lọt (FN).\"\"\"\n",
|
||
" tn, fp, fn, tp = confusion_matrix(y_thi, du_doan).ravel()\n",
|
||
" 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\n",
|
||
" \"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}\n",
|
||
"\n",
|
||
"perceptron = Perceptron(random_state=RANDOM_STATE).fit(X_hoc, y_hoc)\n",
|
||
"# 📦 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)\n",
|
||
"print(pd.DataFrame([do_luong(\"Perceptron\", perceptron.predict(X_thi))]).round(3).to_string(index=False))\n",
|
||
"\n",
|
||
"trong_so = perceptron.coef_[0] # 1 trọng số cho mỗi từ trong từ điển\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]]))\n",
|
||
"print(\"8 từ trọng số ÂM lớn nhất (kéo về ham) :\", list(tu_dien[np.argsort(trong_so)[:8]]))\n",
|
||
"print(\"bias =\", round(perceptron.intercept_[0], 3))\n",
|
||
"\n",
|
||
"# Tự tính \"điểm\" của 1 tin bằng tay theo công thức, so với máy\n",
|
||
"i = 0\n",
|
||
"x = X_thi[i] # hàng TF-IDF của tin thi đầu tiên\n",
|
||
"diem_tay = x.toarray()[0] @ trong_so + perceptron.intercept_[0] # @ = nhân từng cặp rồi cộng lại\n",
|
||
"print(f\"\\nTin: {tin.noi_dung[vi_tri_thi[i]]!r}\")\n",
|
||
"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'}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:**\n",
|
||
"```\n",
|
||
" mô hình accuracy precision recall F1 chặn nhầm (FP) bỏ lọt (FN)\n",
|
||
"Perceptron 0.976 0.912 0.896 0.904 14 17\n",
|
||
"8 từ trọng số DƯƠNG lớn nhất (kéo về spam): ['txt', 'ringtone', 'services', 'uk', 'darling', 'reply', '07090201529', '146tf150p']\n",
|
||
"8 từ trọng số ÂM lớn nhất (kéo về ham) : ['place', 'gd', 'mail', 'princess', 'didn', 'wearing', 'everything', 'drop']\n",
|
||
"bias = -0.29\n",
|
||
"Tin: \"RCT' THNQ Adrian for U text. Rgds Vatian\"\n",
|
||
"Điểm tự tính = 0.046 | máy tính (decision_function) = 0.046 -> SPAM\n",
|
||
"```\n",
|
||
"📦 **`do_luong(ten, du_doan)`** = hàm chấm 1 mô hình trên đề thi · **`perceptron`** = mô hình đã học · **`trong_so`** = 7.395 trọng số, mỗi từ một số.\n",
|
||
"\n",
|
||
"👉 **Hiểu:**\n",
|
||
"- **Đọc dòng kết quả** (trên 1.293 tin thi, 163 tin rác): accuracy **0,976** = đoán đúng 97,6% số tin, nhưng mốc lười đã 87,4% (2.2) nên con số này một mình nói ít. **precision 0,912** = trong 160 tin bị chặn, 146 tin đúng là rác: cứ khoảng 11 tin bị chặn thì có 1 tin thật bị **chặn nhầm** (FP = 14). **recall 0,896** = bắt được 146 / 163 tin rác, **bỏ lọt 17** (FN). **F1 0,904** gộp hai số trên. Đây là mốc mà 4 ứng viên sau phải vượt.\n",
|
||
"- Máy **tự** học ra các từ đáng ngờ (`txt`, `ringtone`, `reply`, cả số điện thoại `07090201529`) — không ai viết tay.\n",
|
||
"- Tự nhân - cộng theo công thức ra **0,046**, đúng bằng máy → Perceptron chỉ là **một phép cộng có trọng số**. Điểm 0,046 chỉ nhỉnh hơn 0 một chút mà vẫn bị phán **SPAM dứt khoát** — không có \"có lẽ\"."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 4 — SVM: ranh giới có vùng đệm rộng nhất\n",
|
||
"\n",
|
||
"📍 **Bước 4 — Coding, ứng viên 2/5.** Perceptron vượt xa mốc lười (F1 0,904) nhưng **chặn nhầm 14 tin thật**. Vì sao? Perceptron dừng lại ở **bất kỳ** đường ranh giới nào tách được dữ liệu, kể cả đường sát sạt vào vài tin. **SVM** (Support Vector Machine) chọn đường ranh giới **cách xa nhất** cả tin ham gần nhất lẫn tin spam gần nhất — khoảng cách đó gọi là **vùng đệm** (margin). Vùng đệm rộng → tin mới hơi khác một chút vẫn rơi đúng phía.\n",
|
||
"\n",
|
||
"Các tin **nằm sát ranh giới** (khó phân biệt nhất) quyết định vị trí ranh giới; chúng gọi là **support vectors**. Các tin khác có bỏ đi thì ranh giới cũng không đổi."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 4.0 Hình minh hoạ: ranh giới \"sát sạt\" và ranh giới có vùng đệm\n",
|
||
"\n",
|
||
"Văn bản có 7.395 cột nên không vẽ được. Để **nhìn thấy** ý tưởng, ô dưới dùng **dữ liệu giả 2 chiều** (40 chấm, chỉ để minh hoạ): cho Perceptron và SVM cùng tìm đường tách 2 nhóm.\n",
|
||
"\n",
|
||
"**Cách đọc hình:** chấm xanh = \"ham\", chấm đỏ = \"spam\"; **đường liền** = ranh giới mô hình tìm được; ở hình SVM, **2 đường nét đứt** = mép vùng đệm, **vòng xanh lá** = support vector (các chấm giữ ranh giới); tiêu đề = khoảng cách từ ranh giới tới chấm **gần nhất**."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.svm import SVC\n",
|
||
"# 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)\n",
|
||
"ngau_nhien_gia = np.random.RandomState(3)\n",
|
||
"P = np.vstack([ngau_nhien_gia.normal([2, 2], 0.7, (20, 2)), ngau_nhien_gia.normal([5, 4.5], 0.7, (20, 2))])\n",
|
||
"nhan_gia = np.array([0] * 20 + [1] * 20)\n",
|
||
"per_gia = Perceptron(random_state=0, max_iter=1000).fit(P, nhan_gia)\n",
|
||
"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\n",
|
||
"\n",
|
||
"def gan_nhat(w, b):\n",
|
||
" \"\"\"Khoảng cách từ ranh giới w·x + b = 0 tới chấm gần nhất.\"\"\"\n",
|
||
" return np.min(np.abs(P @ w + b)) / np.linalg.norm(w)\n",
|
||
"\n",
|
||
"xs = np.linspace(0, 7, 100)\n",
|
||
"hinh, o = plt.subplots(1, 2, figsize=(12, 5), sharey=True)\n",
|
||
"for truc, ten, m in zip(o, [\"Perceptron\", \"SVM\"], [per_gia, svm_gia]):\n",
|
||
" w, b = m.coef_[0], m.intercept_[0]\n",
|
||
" truc.scatter(P[:, 0], P[:, 1], c=np.where(nhan_gia == 1, \"red\", \"blue\"), s=30)\n",
|
||
" truc.plot(xs, -(w[0] * xs + b) / w[1], \"k-\", label=\"ranh giới\")\n",
|
||
" if ten == \"SVM\":\n",
|
||
" for lech in (-1, 1): # mép vùng đệm: w·x + b = ±1\n",
|
||
" truc.plot(xs, -(w[0] * xs + b + lech) / w[1], \"k--\", lw=1)\n",
|
||
" truc.scatter(m.support_vectors_[:, 0], m.support_vectors_[:, 1], s=220, facecolors=\"none\",\n",
|
||
" edgecolors=\"green\", lw=2, label=\"support vector\")\n",
|
||
" truc.set_title(f\"{ten}: ranh giới cách chấm gần nhất {gan_nhat(w, b):.2f}\")\n",
|
||
" truc.set_xlim(0, 7); truc.set_ylim(0, 7); truc.legend(loc=\"upper left\")\n",
|
||
"plt.tight_layout(); plt.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** 2 hình cạnh nhau, cùng 40 chấm giả. Trái `Perceptron: ranh giới cách chấm gần nhất 0.08` — đường dốc **gần như chạm** một chấm đỏ ở khoảng (3,4; 4,3). Phải `SVM: ranh giới cách chấm gần nhất 0.65` — đường nằm **chính giữa** 2 nhóm, 2 đường nét đứt hai bên, **3 support vector** (vòng xanh lá: 1 chấm xanh, 2 chấm đỏ) nằm đúng trên mép vùng đệm.\n",
|
||
"\n",
|
||
"📦 **`P`**, **`nhan_gia`** = 40 chấm giả và nhãn của chúng (chỉ dùng cho hình này) · **`per_gia`**, **`svm_gia`** = 2 mô hình học trên dữ liệu giả.\n",
|
||
"\n",
|
||
"👉 **Hiểu:**\n",
|
||
"- Cả hai đều tách **đúng 100%** 40 chấm, nhưng Perceptron dừng ngay khi hết lỗi, nên ranh giới của nó **sát sạt** một tin \"spam\": một tin mới hơi khác một chút là rơi sang phía sai.\n",
|
||
"- SVM chọn đường **cách xa nhất** cả hai nhóm (0,65 so với 0,08). Chỉ 3 chấm sát ranh giới quyết định vị trí đường; 37 chấm còn lại có bỏ đi thì đường cũng không đổi.\n",
|
||
"- 4.1 làm đúng việc này trên 7.395 chiều của tin nhắn thật, nơi ta không vẽ được nhưng đo được: chặn nhầm giảm từ 14 xuống 3."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 4.1 SVM trên tin nhắn thật"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.svm import LinearSVC, SVC\n",
|
||
"svm = LinearSVC(random_state=RANDOM_STATE).fit(X_hoc, y_hoc)\n",
|
||
"print(pd.DataFrame([do_luong(\"SVM (tuyến tính)\", svm.predict(X_thi))]).round(3).to_string(index=False))\n",
|
||
"\n",
|
||
"# Đếm support vectors: SVC(kernel=\"linear\") cùng ý tưởng, cho biết các tin \"giữ ranh giới\"\n",
|
||
"svc = SVC(kernel=\"linear\").fit(X_hoc, y_hoc)\n",
|
||
"so_sv = svc.n_support_ # [số tin ham, số tin spam] là support vector\n",
|
||
"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%})\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** `SVM (tuyến tính): accuracy 0.982 | precision 0.979 | recall 0.877 | F1 0.926 | chặn nhầm 3 | bỏ lọt 20` · `Support vectors: 657 tin ham + 291 tin spam = 948 / 3878 tin học (24%)`\n",
|
||
"\n",
|
||
"📦 **`svm`** = SVM tuyến tính đã học.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** so với Perceptron, SVM **chặn nhầm ít hơn hẳn** (3 so với 14) — vùng đệm rộng giúp tin thật \"hơi giống rác\" vẫn nằm đúng phía. Chỉ **24%** số tin (những tin khó, nằm sát ranh giới) thật sự quyết định ranh giới; 76% còn lại có bỏ đi thì ranh giới không đổi."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 5 — Kernel trick: khi một đường thẳng không đủ\n",
|
||
"\n",
|
||
"📍 **Vẫn là ứng viên 2 (SVM), câu hỏi phụ:** Perceptron và SVM đều vẽ ranh giới **thẳng**. Nếu dữ liệu không tách được bằng đường thẳng thì sao?\n",
|
||
"\n",
|
||
"Nếu tin ham và spam đan xen đến mức **không đường thẳng nào** tách được, SVM dùng **kernel**: tính toán như thể dữ liệu được \"nhấc\" lên một không gian nhiều chiều hơn, nơi một mặt phẳng tách được — nhìn lại ở không gian cũ, ranh giới thành **đường cong**. Kernel phổ biến nhất là **RBF**.\n",
|
||
"\n",
|
||
"Để **nhìn thấy** được, ta chỉ dùng 2 đặc trưng đơn giản của mỗi tin: **độ dài** (số ký tự) và **số chữ số** (tin rác hay có số điện thoại, mã, giá tiền). Mỗi chấm là 1 tin thật.\n",
|
||
"\n",
|
||
"**Cách đọc hình:** trục ngang = log(1 + độ dài), trục đứng = log(1 + số chữ số); chấm xanh = ham, chấm đỏ = spam; **nền màu** = vùng mà mô hình sẽ đoán là spam (đỏ nhạt) hay ham (xanh nhạt); đường biên giữa 2 màu nền = ranh giới."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# 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)\n",
|
||
"hai_dac_trung = np.log1p(pd.DataFrame({\"do_dai\": tin.noi_dung.str.len(), \"so_chu_so\": tin.noi_dung.str.count(r\"\\d\")}))\n",
|
||
"A_hoc, A_thi = hai_dac_trung.loc[vi_tri_hoc], hai_dac_trung.loc[vi_tri_thi]\n",
|
||
"# Lưới điểm phủ cả hình, để tô màu vùng đoán\n",
|
||
"xx, yy = np.meshgrid(np.linspace(0, 6.5, 300), np.linspace(0, 4.2, 300))\n",
|
||
"luoi = pd.DataFrame({\"do_dai\": xx.ravel(), \"so_chu_so\": yy.ravel()})\n",
|
||
"hinh, o = plt.subplots(1, 2, figsize=(14, 5))\n",
|
||
"for truc, kernel in zip(o, [\"linear\", \"rbf\"]):\n",
|
||
" m = SVC(kernel=kernel, C=1, gamma=\"scale\").fit(A_hoc, y_hoc)\n",
|
||
" truc.contourf(xx, yy, m.predict(luoi).reshape(xx.shape), alpha=0.25, cmap=\"coolwarm\")\n",
|
||
" mau = np.where(y_thi == 1, \"red\", \"blue\")\n",
|
||
" truc.scatter(A_thi.do_dai, A_thi.so_chu_so, c=mau, s=6, alpha=0.5)\n",
|
||
" p = m.predict(A_thi)\n",
|
||
" truc.set_title(f\"kernel='{kernel}': đúng {accuracy_score(y_thi, p):.1%}, recall {recall_score(y_thi, p):.1%}\")\n",
|
||
" truc.set_xlabel(\"log(1 + độ dài tin)\"); truc.set_ylabel(\"log(1 + số chữ số)\")\n",
|
||
"plt.tight_layout(); plt.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** 2 hình cạnh nhau. Trái `kernel='linear': đúng 97.6%, recall 85.9%` — ranh giới là **đường thẳng** chéo. Phải `kernel='rbf': đúng 97.4%, recall 81.6%` — ranh giới **cong**, võng xuống quanh độ dài log ≈ 5.\n",
|
||
"\n",
|
||
"👉 **Hiểu:**\n",
|
||
"- Chấm đỏ (rác) dồn thành một cột ở log(độ dài) ≈ 5, tức **khoảng 150 ký tự**: tin rác thường **viết kín một tin SMS** (giới hạn 160 ký tự) và **nhiều chữ số** (số điện thoại, mã, giá).\n",
|
||
"- Kernel RBF **uốn cong** ranh giới được — đúng ý tưởng \"nhấc dữ liệu lên không gian nhiều chiều\". Nhưng ở đây nó **không đúng hơn** đường thẳng (97,4% so với 97,6%).\n",
|
||
"- Với văn bản (7.395 chiều — mỗi từ một chiều), dữ liệu **đã gần như tách được bằng đường thẳng**, nên SVM tuyến tính là lựa chọn chuẩn cho lọc văn bản. Kernel phát huy khi có **ít** đặc trưng mà dữ liệu đan xen phức tạp. Không có thuật toán \"luôn tốt hơn\" — phải **thử và đo**."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 6 — Logistic Regression: từ \"Có / Không\" sang **xác suất**\n",
|
||
"\n",
|
||
"📍 **Bước 4 — Coding, ứng viên 3/5.** SVM chặn nhầm ít (3 tin) nhưng chỉ trả lời Có / Không.\n",
|
||
"\n",
|
||
"Perceptron và SVM chỉ nói **spam / ham**. Ở SOC ta muốn biết **chắc bao nhiêu phần trăm** để xử lý khác nhau. Logistic Regression cũng tính một **điểm** như Perceptron, rồi đưa điểm qua **hàm sigmoid** để thành **xác suất** từ 0 đến 1:\n",
|
||
"\n",
|
||
"**xác suất = 1 / (1 + e^(−điểm))**\n",
|
||
"\n",
|
||
"### 6.1 Hàm sigmoid trông thế nào?"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"diem_vd = np.array([-6, -3, -1, 0, 1, 3, 6])\n",
|
||
"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))\n",
|
||
"z = np.linspace(-8, 8, 200)\n",
|
||
"plt.figure(figsize=(7, 3)); plt.plot(z, 1 / (1 + np.exp(-z))); plt.axhline(0.5, ls=\"--\", c=\"gray\")\n",
|
||
"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()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** bảng `-6 → 0.002`, `-3 → 0.047`, `-1 → 0.269`, `0 → 0.500`, `1 → 0.731`, `3 → 0.953`, `6 → 0.998` và đường cong chữ S.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** điểm âm lớn → gần **0%**, điểm 0 → **50%** (không chắc), điểm dương lớn → gần **100%**. Sigmoid \"bóp\" mọi điểm vào khoảng 0–1 để đọc như **phần trăm**."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 6.2 Huấn luyện — và vấn đề \"lớp hiếm\"\n",
|
||
"\n",
|
||
"Chỉ ~13% tin là spam. Với mặc định, mô hình \"ngại\" đoán spam. `class_weight=\"balanced\"` = cho mỗi tin spam **nặng ký hơn** khi học (theo tỉ lệ ngược số lượng), để mô hình không bỏ qua lớp hiếm."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.linear_model import LogisticRegression\n",
|
||
"lr_mac_dinh = LogisticRegression(max_iter=1000).fit(X_hoc, y_hoc)\n",
|
||
"logistic = LogisticRegression(max_iter=1000, class_weight=\"balanced\").fit(X_hoc, y_hoc)\n",
|
||
"# 📦 logistic = Logistic Regression có cân bằng lớp (dùng ở các phần sau)\n",
|
||
"print(pd.DataFrame([do_luong(\"Logistic mặc định\", lr_mac_dinh.predict(X_thi)),\n",
|
||
" do_luong(\"Logistic class_weight='balanced'\", logistic.predict(X_thi))]).round(3).to_string(index=False))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:**\n",
|
||
"```\n",
|
||
" mô hình accuracy precision recall F1 chặn nhầm (FP) bỏ lọt (FN)\n",
|
||
" Logistic mặc định 0.963 0.967 0.730 0.832 4 44\n",
|
||
"Logistic class_weight='balanced' 0.978 0.904 0.926 0.915 16 12\n",
|
||
"```\n",
|
||
"📦 **`logistic`** = Logistic Regression có cân bằng lớp (dùng ở các phần sau).\n",
|
||
"\n",
|
||
"👉 **Hiểu:** mặc định, mô hình **bỏ lọt 44 tin rác** vì tin rác hiếm (13%). Cân bằng lớp → bỏ lọt còn **12**, đổi lại chặn nhầm tăng từ 4 lên 16. Một tham số, đổi hẳn cách mô hình cư xử với lớp hiếm."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 6.3 Phân luồng 3 làn theo xác suất"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"xac_suat = logistic.predict_proba(X_thi)[:, 1] # cột [:, 1] = xác suất là spam của từng tin thi\n",
|
||
"# ✏️ SỬA: 2 ngưỡng phân luồng\n",
|
||
"NGUONG_CACH_LY, NGUONG_CHAN = 0.5, 0.9\n",
|
||
"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\"))\n",
|
||
"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\"])\n",
|
||
"bang_lan.columns = [\"số tin\", \"trong đó spam thật\"]\n",
|
||
"# 📦 xac_suat = xác suất spam của từng tin thi · lan = làn được xếp\n",
|
||
"print(bang_lan.to_string())\n",
|
||
"print(\"\\nVí dụ 3 tin ở làn CÁCH LY:\")\n",
|
||
"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]:\n",
|
||
" print(f\" {p_:.0%} | {nd}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:**\n",
|
||
"```\n",
|
||
" số tin trong đó spam thật\n",
|
||
"1. CHO QUA 1126 12\n",
|
||
"2. CÁCH LY (người xem) 75 60\n",
|
||
"3. CHẶN 92 91\n",
|
||
"```\n",
|
||
"rồi 3 tin ở làn cách ly: `69% | RCT' THNQ Adrian for U text. Rgds Vatian`, `67% | I am waiting for your call sir.`, `68% | Your daily text from me – a favour this time`.\n",
|
||
"\n",
|
||
"📦 **`xac_suat`** = xác suất spam của 1.293 tin thi · **`lan`** = làn được xếp.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** làn **CHẶN** gần như toàn tin rác (91/92) → chặn tự động an toàn. Làn **CÁCH LY** chỉ 75 tin — đội SOC chỉ phải xem **6%** số tin, trong đó 60 tin đúng là rác. Làn **CHO QUA** còn lọt 12 tin rác. Đổi 2 ngưỡng (✏️) để xem làn nào phình ra, làn nào co lại."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 7 — Naive Bayes: thám tử thống kê\n",
|
||
"\n",
|
||
"📍 **Bước 4 — Coding, ứng viên 4/5**, một cách nghĩ **khác hẳn**: 3 ứng viên trước đều **vẽ ranh giới** (tổng có trọng số); Naive Bayes thì **đếm xác suất** từ quá khứ.\n",
|
||
"\n",
|
||
"Naive Bayes hỏi: *\"Trong quá khứ, tin chứa từ này là spam bao nhiêu phần?\"* rồi **nhân** bằng chứng của từng từ lại với nhau. \"**Ngây thơ**\" vì nó coi các từ **độc lập** với nhau (không hiểu \"not free\" khác \"free\"). Nhờ ngây thơ mà tính rất nhanh.\n",
|
||
"\n",
|
||
"### 7.1 Tự tính \"bằng chứng\" của một từ từ dữ liệu học"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# ✏️ SỬA: từ muốn soi (viết thường), ví dụ \"prize\", \"call\", \"home\", \"love\"\n",
|
||
"TU = \"free\"\n",
|
||
"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\")\n",
|
||
"so_spam, so_ham = int((co_tu & (y_hoc == 1)).sum()), int((co_tu & (y_hoc == 0)).sum())\n",
|
||
"print(f\"Trong phần học có {co_tu.sum()} tin chứa '{TU}': {so_spam} spam, {so_ham} ham\")\n",
|
||
"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%}\")\n",
|
||
"print(f\"(trong khi tỉ lệ spam chung chỉ {y_hoc.mean():.0%})\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** `Trong phần học có 148 tin chứa 'free': 112 spam, 36 ham` · `-> nếu chỉ dựa vào từ này: xác suất spam = 112 / 148 = 76%` · `(trong khi tỉ lệ spam chung chỉ 13%)`\n",
|
||
"\n",
|
||
"👉 **Hiểu:** đó chính là \"kinh nghiệm quá khứ\" của Naive Bayes: thấy `free` thì khả năng rác nhảy từ **13% lên 76%**. Naive Bayes làm việc này cho **mọi từ** trong tin rồi nhân các bằng chứng lại. Thử `\"prize\"`, `\"home\"`, `\"love\"`."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 7.2 Huấn luyện, từ \"tố cáo\" mạnh nhất, và tốc độ"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.naive_bayes import MultinomialNB\n",
|
||
"naive_bayes = MultinomialNB().fit(X_hoc, y_hoc)\n",
|
||
"print(pd.DataFrame([do_luong(\"Naive Bayes\", naive_bayes.predict(X_thi))]).round(3).to_string(index=False))\n",
|
||
"# 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\n",
|
||
"ti_so = naive_bayes.feature_log_prob_[1] - naive_bayes.feature_log_prob_[0]\n",
|
||
"print(\"\\n12 từ nghiêng về SPAM nhất:\", list(tu_dien[np.argsort(ti_so)[-12:][::-1]]))\n",
|
||
"print(\"8 từ nghiêng về HAM nhất :\", list(tu_dien[np.argsort(ti_so)[:8]]))\n",
|
||
"\n",
|
||
"# Tốc độ học: lặp 5 lần lấy thời gian nhỏ nhất cho công bằng\n",
|
||
"def thoi_gian_hoc(mo_hinh):\n",
|
||
" return min((lambda t0: (mo_hinh.fit(X_hoc, y_hoc), time.perf_counter() - t0)[1])(time.perf_counter()) for _ in range(5))\n",
|
||
"print(\"\\nThời gian học trên\", X_hoc.shape[0], \"tin:\")\n",
|
||
"for ten, m in [(\"Naive Bayes\", MultinomialNB()), (\"Perceptron\", Perceptron(random_state=RANDOM_STATE)),\n",
|
||
" (\"SVM\", LinearSVC(random_state=RANDOM_STATE)), (\"Logistic\", LogisticRegression(max_iter=1000, class_weight=\"balanced\"))]:\n",
|
||
" print(f\" {ten:12s} {thoi_gian_hoc(m) * 1000:6.1f} ms\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** `Naive Bayes: accuracy 0.948 | precision 1.0 | recall 0.589 | F1 0.741 | chặn nhầm 0 | bỏ lọt 67` · 12 từ nghiêng về spam nhất: `prize, claim, www, nokia, 150p, uk, guaranteed, co, 18, tone, 500, 1000` · nghiêng về ham: `gt, lt, my, lor, ok, but, da, he` · thời gian học: `Naive Bayes 2.6 ms`, `Perceptron 3.2 ms`, `SVM 7.9 ms`, `Logistic 12.4 ms`\n",
|
||
"\n",
|
||
"📦 **`naive_bayes`** = mô hình đã học.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** Naive Bayes **cực kỳ thận trọng**: không chặn nhầm tin nào (precision 1,0) nhưng **bỏ lọt 67/163 tin rác**. Nó học **nhanh nhất** (2,6 ms). Các từ \"tố cáo\" rất đúng chất tin rác Anh quốc: giải thưởng, `150p` (giá tin nhắn), `18` (18+), `nokia`, `tone` (nhạc chuông)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 8 — Hội đồng (Voting): khi các chuyên gia bắt tay nhau\n",
|
||
"\n",
|
||
"📍 **Bước 4 — Coding, ứng viên 5/5.** Đã có 4 mô hình, mỗi cái mạnh / yếu một kiểu (Naive Bayes không chặn nhầm nhưng bỏ lọt nhiều; Logistic bắt nhiều nhưng chặn nhầm hơn) → cho chúng **cùng bỏ phiếu**.\n",
|
||
"\n",
|
||
"Mỗi mô hình có \"điểm mù\" riêng. **Hội đồng** cho nhiều mô hình cùng quyết định:\n",
|
||
"- **hard voting**: mỗi mô hình bỏ 1 phiếu spam / ham, theo số đông;\n",
|
||
"- **soft voting**: lấy **trung bình xác suất** của các mô hình;\n",
|
||
"- **có trọng số**: phiếu của mô hình đáng tin hơn được nhân lên (ví dụ ×2).\n",
|
||
"\n",
|
||
"SVM không tự cho xác suất, nên ta bọc nó bằng `CalibratedClassifierCV` — một bộ \"đổi điểm của SVM thành xác suất\" học từ dữ liệu.\n",
|
||
"\n",
|
||
"### 8.1 Các mô hình có sai cùng chỗ không?"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"cac_mo_hinh = {\"Perceptron\": perceptron, \"SVM\": svm, \"Logistic\": logistic, \"Naive Bayes\": naive_bayes}\n",
|
||
"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\n",
|
||
"sai = phieu.ne(y_thi.to_numpy(), axis=0) # True ở chỗ mô hình đó đoán sai\n",
|
||
"print(\"Số tin mỗi mô hình đoán SAI:\", sai.sum().to_dict())\n",
|
||
"print(\"Số tin mà các mô hình KHÔNG thống nhất:\", int((phieu.nunique(axis=1) > 1).sum()), \"/\", len(phieu))\n",
|
||
"print(\"Số tin mà CẢ 4 cùng sai:\", int(sai.all(axis=1).sum()))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** `Số tin mỗi mô hình đoán SAI: {'Perceptron': 31, 'SVM': 23, 'Logistic': 28, 'Naive Bayes': 67}` · `Số tin mà các mô hình KHÔNG thống nhất: 81 / 1293` · `Số tin mà CẢ 4 cùng sai: 11`\n",
|
||
"\n",
|
||
"📦 **`phieu`** = bảng phiếu của 4 mô hình cho 1.293 tin · **`sai`** = True ở chỗ mô hình đoán sai.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** các mô hình **sai ở những chỗ khác nhau** (81 tin bất đồng) → hội đồng có cơ hội sửa lỗi cho nhau. Nhưng **11 tin cả 4 cùng sai** — không cách bỏ phiếu nào cứu được; hội đồng **không** xóa sạch mọi điểm mù."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 8.2 So sánh các kiểu hội đồng"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.ensemble import VotingClassifier\n",
|
||
"from sklearn.calibration import CalibratedClassifierCV\n",
|
||
"# ✏️ SỬA: trọng số phiếu [SVM, Logistic, Naive Bayes]. Thử [1, 1, 1], [1, 2, 1], [3, 1, 1]\n",
|
||
"TRONG_SO = [2, 1, 1]\n",
|
||
"def hoi_dong(kieu, trong_so=None):\n",
|
||
" thanh_vien = [(\"svm\", CalibratedClassifierCV(LinearSVC(random_state=RANDOM_STATE), cv=5) if kieu == \"soft\" else LinearSVC(random_state=RANDOM_STATE)),\n",
|
||
" (\"logistic\", LogisticRegression(max_iter=1000, class_weight=\"balanced\")),\n",
|
||
" (\"naive_bayes\", MultinomialNB())]\n",
|
||
" return VotingClassifier(thanh_vien, voting=kieu, weights=trong_so).fit(X_hoc, y_hoc)\n",
|
||
"\n",
|
||
"hd_hard, hd_soft, hd_trong_so = hoi_dong(\"hard\"), hoi_dong(\"soft\"), hoi_dong(\"soft\", TRONG_SO)\n",
|
||
"# 📦 hd_trong_so = hội đồng soft voting có trọng số (dùng ở Phần 9, 10)\n",
|
||
"bang_kq = pd.DataFrame([do_luong(ten, m.predict(X_thi)) for ten, m in\n",
|
||
" [(\"SVM (một mình)\", svm), (\"Hội đồng hard\", hd_hard), (\"Hội đồng soft, phiếu đều\", hd_soft),\n",
|
||
" (f\"Hội đồng soft, trọng số {TRONG_SO}\", hd_trong_so)]])\n",
|
||
"bang_kq.round(3).set_index(\"mô hình\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:**\n",
|
||
"```\n",
|
||
" accuracy precision recall F1 chặn nhầm (FP) bỏ lọt (FN)\n",
|
||
"SVM (một mình) 0.982 0.979 0.877 0.926 3 20\n",
|
||
"Hội đồng hard 0.982 0.979 0.877 0.926 3 20\n",
|
||
"Hội đồng soft, phiếu đều 0.981 0.973 0.877 0.923 4 20\n",
|
||
"Hội đồng soft, trọng số [2, 1, 1] 0.984 0.973 0.896 0.933 4 17\n",
|
||
"```\n",
|
||
"📦 **`hd_trong_so`** = hội đồng soft voting có trọng số (Phần 9, 10 dùng lại) · **`hoi_dong(kieu, trong_so)`** = hàm dựng một hội đồng.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** hội đồng **chia phiếu đều không hơn** SVM đứng một mình (Naive Bayes \"kéo\" kết quả xuống). Cho SVM — chuyên gia giỏi nhất — **phiếu ×2** thì hội đồng **vượt** mọi thành viên: F1 0,933, bỏ lọt còn 17. Bài học: hội đồng **không tự động** giỏi hơn; cần **chọn trọng số bằng số liệu**. Thử `[1, 1, 1]`, `[3, 1, 1]`."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 9 — Đánh giá: tin con số, không tin cảm giác\n",
|
||
"\n",
|
||
"📍 **Bước 6 — Đánh giá các mô hình:** đặt cả 5 ứng viên lên cùng một bảng, cạnh mốc lười và luật cứng, rồi chọn mô hình và ngưỡng.\n",
|
||
"\n",
|
||
"### 9.1 Bảng tổng hợp mọi cách — kể cả \"lười\" và luật cứng"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"tong_hop = pd.DataFrame([\n",
|
||
" do_luong(\"Đoán mọi tin là ham ('lười')\", np.zeros(len(y_thi), dtype=int)),\n",
|
||
" do_luong(f\"Luật {len(TU_KHOA)} từ khóa (Phần 1)\", bi_chan[vi_tri_thi].to_numpy()),\n",
|
||
" do_luong(\"Perceptron\", perceptron.predict(X_thi)),\n",
|
||
" do_luong(\"SVM\", svm.predict(X_thi)),\n",
|
||
" do_luong(\"Logistic (balanced)\", logistic.predict(X_thi)),\n",
|
||
" do_luong(\"Naive Bayes\", naive_bayes.predict(X_thi)),\n",
|
||
" do_luong(f\"Hội đồng trọng số {TRONG_SO}\", hd_trong_so.predict(X_thi)),\n",
|
||
"])\n",
|
||
"tong_hop.round(3).set_index(\"mô hình\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:**\n",
|
||
"```\n",
|
||
" accuracy precision recall F1 chặn nhầm (FP) bỏ lọt (FN)\n",
|
||
"Đoán mọi tin là ham ('lười') 0.874 0.000 0.000 0.000 0 163\n",
|
||
"Luật 10 từ khóa (Phần 1) 0.919 0.693 0.638 0.665 46 59\n",
|
||
"Perceptron 0.976 0.912 0.896 0.904 14 17\n",
|
||
"SVM 0.982 0.979 0.877 0.926 3 20\n",
|
||
"Logistic (balanced) 0.978 0.904 0.926 0.915 16 12\n",
|
||
"Naive Bayes 0.948 1.000 0.589 0.741 0 67\n",
|
||
"Hội đồng trọng số [2, 1, 1] 0.984 0.973 0.896 0.933 4 17\n",
|
||
"```\n",
|
||
"👉 **Hiểu:**\n",
|
||
"- Mô hình \"lười\" có accuracy **87,4%** mà F1 = **0** — vì vậy không bao giờ báo cáo chỉ bằng accuracy.\n",
|
||
"- **Không mô hình nào thắng mọi cột**: Naive Bayes chặn nhầm ít nhất (0), Logistic bỏ lọt ít nhất (12), hội đồng có F1 cao nhất. Chọn mô hình = chọn **loại lỗi** mà tổ chức chấp nhận được."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 9.2 Ma trận nhầm lẫn của hội đồng\n",
|
||
"\n",
|
||
"**Cách đọc:** hàng = **sự thật** (ham, spam), cột = **máy đoán**. Góc trên trái = ham cho qua đúng (TN); trên phải = **chặn nhầm** (FP); dưới trái = **bỏ lọt** (FN); dưới phải = bắt đúng (TP)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from sklearn.metrics import ConfusionMatrixDisplay\n",
|
||
"mt = confusion_matrix(y_thi, hd_trong_so.predict(X_thi))\n",
|
||
"ConfusionMatrixDisplay(mt, display_labels=[\"ham\", \"spam\"]).plot(cmap=\"Blues\")\n",
|
||
"plt.title(\"Hội đồng có trọng số — hàng: sự thật, cột: máy đoán\"); plt.show()\n",
|
||
"tn, fp, fn, tp = mt.ravel()\n",
|
||
"print(f\"Cho qua đúng (TN) {tn} | CHẶN NHẦM (FP) {fp} | BỎ LỌT (FN) {fn} | bắt đúng (TP) {tp}\")\n",
|
||
"print(f\"precision = TP / (TP + FP) = {tp} / {tp + fp} = {tp / (tp + fp):.3f}\")\n",
|
||
"print(f\"recall = TP / (TP + FN) = {tp} / {tp + fn} = {tp / (tp + fn):.3f}\")\n",
|
||
"print(\"\\n3 tin RÁC bị bỏ lọt:\")\n",
|
||
"du_doan_hd = hd_trong_so.predict(X_thi)\n",
|
||
"print(tin.noi_dung[vi_tri_thi][(y_thi == 1).to_numpy() & (du_doan_hd == 0)].head(3).to_string())\n",
|
||
"print(\"\\nTin THẬT bị chặn nhầm:\")\n",
|
||
"print(tin.noi_dung[vi_tri_thi][(y_thi == 0).to_numpy() & (du_doan_hd == 1)].head(4).to_string())"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** hình ma trận 2×2 rồi `Cho qua đúng (TN) 1126 | CHẶN NHẦM (FP) 4 | BỎ LỌT (FN) 17 | bắt đúng (TP) 146` · `precision = 146 / 150 = 0.973` · `recall = 146 / 163 = 0.896`. Tin rác bị bỏ lọt (ví dụ quảng cáo `Burger King - Wanna play footy...`, `Missed call alert. These numbers called but left no message. 07008009200`); tin thật bị chặn nhầm: `Glad to see your reply.`, `Are you free now?can i call now?`, `Nokia phone is lovly..`.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** đọc lỗi mới thấy **vì sao** máy sai: tin thật chứa đúng những từ \"nghi vấn\" (`free`, `call`, `reply`, `nokia`); tin rác bị lọt thì viết như tin bình thường (quảng cáo, \"cuộc gọi nhỡ\"). Đây là chỗ cần tín hiệu khác ngoài chữ (người gửi, link)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 9.3 Ngưỡng: đổi chặn nhầm lấy bỏ lọt"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# ✏️ SỬA: các ngưỡng muốn thử (trên xác suất của Logistic ở 6.3)\n",
|
||
"CAC_NGUONG = [0.9, 0.7, 0.5, 0.3, 0.1]\n",
|
||
"dong = []\n",
|
||
"for ng in CAC_NGUONG:\n",
|
||
" p = (xac_suat >= ng).astype(int)\n",
|
||
" tn_, fp_, fn_, tp_ = confusion_matrix(y_thi, p).ravel()\n",
|
||
" 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_})\n",
|
||
"bang_nguong = pd.DataFrame(dong).set_index(\"ngưỡng\")\n",
|
||
"bang_nguong[[\"precision\", \"recall\"]].plot(marker=\"o\", figsize=(7, 3), title=\"Hạ ngưỡng: recall tăng, precision giảm\")\n",
|
||
"plt.gca().invert_xaxis(); plt.show()\n",
|
||
"bang_nguong.round(3)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** biểu đồ 2 đường và bảng:\n",
|
||
"\n",
|
||
"| ngưỡng | precision | recall | chặn nhầm | bỏ lọt |\n",
|
||
"|---|---|---|---|---|\n",
|
||
"| 0.9 | 0.989 | 0.558 | 1 | 72 |\n",
|
||
"| 0.7 | 0.971 | 0.828 | 4 | 28 |\n",
|
||
"| 0.5 | 0.904 | 0.926 | 16 | 12 |\n",
|
||
"| 0.3 | 0.668 | 0.951 | 77 | 8 |\n",
|
||
"| 0.1 | 0.221 | 1.000 | 576 | 0 |\n",
|
||
"\n",
|
||
"📦 **`bang_nguong`** = bảng trên.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** **không có ngưỡng nào vừa 0 chặn nhầm vừa 0 bỏ lọt**. Hạ từ 0,5 xuống 0,3: bắt thêm 4 tin rác nhưng chặn nhầm thêm 61 tin thật. Hạ xuống 0,1: bắt **hết** tin rác nhưng chặn nhầm **576** tin thật — hệ thống sẽ bị tắt ngay ngày đầu. Ngưỡng là **quyết định nghiệp vụ**: thiệt hại của 1 lần bỏ lọt so với 1 lần chặn nhầm."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 10 — AI đối đầu AI: kẻ tấn công chèn \"từ an toàn\"\n",
|
||
"\n",
|
||
"📍 **Bước 6 — Đánh giá các mô hình (thử tấn công):** mô hình tốt trên đề thi chưa chắc tốt khi kẻ tấn công **cố tình** lừa nó.\n",
|
||
"\n",
|
||
"Kẻ tấn công biết bộ lọc chấm điểm theo **từ**. Chiêu đơn giản: giữ nguyên nội dung rác, **chèn thêm một đoạn từ ngữ \"vô hại\"** (giống tin nhắn bạn bè) để kéo điểm về phía ham. Ta thử chiêu này lên **mọi tin rác** trong đề thi.\n",
|
||
"\n",
|
||
"### 10.1 Tấn công"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# ✏️ SỬA: đoạn \"từ an toàn\" kẻ tấn công chèn vào cuối tin rác\n",
|
||
"CAU_CHEN = \" ok see you later at home lor going to sleep now love you dear\"\n",
|
||
"tin_rac_thi = tin.noi_dung[vi_tri_thi][(y_thi == 1).to_numpy()]\n",
|
||
"X_goc, X_bi_chen = tfidf.transform(tin_rac_thi), tfidf.transform(tin_rac_thi + CAU_CHEN)\n",
|
||
"dong = []\n",
|
||
"for ten, m in list(cac_mo_hinh.items()) + [(f\"Hội đồng trọng số {TRONG_SO}\", hd_trong_so)]:\n",
|
||
" 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()})\n",
|
||
"tan_cong = pd.DataFrame(dong).set_index(\"mô hình\")\n",
|
||
"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)\")\n",
|
||
"plt.xlabel(\"tỉ lệ bắt được\"); plt.show()\n",
|
||
"print(\"Ví dụ tin rác sau khi bị chèn:\", repr(tin_rac_thi.iloc[0] + CAU_CHEN))\n",
|
||
"tan_cong.round(3)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** biểu đồ thanh và bảng (tỉ lệ trong 163 tin rác của đề thi bị bắt):\n",
|
||
"\n",
|
||
"| mô hình | TRƯỚC khi chèn | SAU khi chèn |\n",
|
||
"|---|---|---|\n",
|
||
"| Perceptron | 0.896 | 0.503 |\n",
|
||
"| SVM | 0.877 | 0.675 |\n",
|
||
"| Logistic | 0.926 | 0.712 |\n",
|
||
"| Naive Bayes | 0.589 | **0.031** |\n",
|
||
"| Hội đồng trọng số [2, 1, 1] | 0.896 | 0.656 |\n",
|
||
"\n",
|
||
"📦 **`CAU_CHEN`** = câu kẻ tấn công chèn (sửa được) · **`tin_rac_thi`** = 163 tin rác của đề thi · **`tan_cong`** = bảng trên.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** chỉ chèn một câu \"tán gẫu\" vô hại, **mọi** mô hình đều tụt. **Naive Bayes sụp gần hết** (59% → 3%): nó cộng bằng chứng từng từ độc lập, nên cả chục từ \"ham\" kéo át vài từ \"rác\" — đúng điểm yếu \"ngây thơ\". SVM và Logistic chịu đòn tốt hơn nhưng vẫn mất 20 điểm phần trăm."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 10.2 Phòng thủ: dạy lại máy bằng chính chiêu của kẻ tấn công\n",
|
||
"\n",
|
||
"**Huấn luyện đối kháng** (adversarial training): tạo thêm các tin rác **đã bị chèn từ** ngay trong phần học, gắn đúng nhãn spam, rồi học lại. Máy học được rằng \"từ an toàn\" xuất hiện cạnh nội dung rác **không** làm tin đó sạch.\n",
|
||
"\n",
|
||
"Để thử công bằng, phần học dùng một câu chèn **khác** câu ở 10.1 (kẻ tấn công thật sẽ không dùng đúng câu ta đoán trước)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# ✏️ 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)\n",
|
||
"CAU_TAP = \" hey dear how was your day see you at dinner tonight ok\"\n",
|
||
"tin_rac_hoc = tin.noi_dung[vi_tri_hoc][(y_hoc == 1).to_numpy()]\n",
|
||
"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\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\n",
|
||
"tfidf_dk = TfidfVectorizer()\n",
|
||
"X_hoc_dk = tfidf_dk.fit_transform(noi_dung_moi)\n",
|
||
"dong = []\n",
|
||
"for ten, m in [(\"Naive Bayes\", MultinomialNB()), (\"SVM\", LinearSVC(random_state=RANDOM_STATE)),\n",
|
||
" (\"Logistic\", LogisticRegression(max_iter=1000, class_weight=\"balanced\"))]:\n",
|
||
" m.fit(X_hoc_dk, nhan_moi)\n",
|
||
" p_thi = m.predict(tfidf_dk.transform(tin.noi_dung[vi_tri_thi]))\n",
|
||
" dong.append({\"mô hình (học lại)\": ten,\n",
|
||
" \"bắt tin rác bị chèn (câu 10.1)\": m.predict(tfidf_dk.transform(tin_rac_thi + CAU_CHEN)).mean(),\n",
|
||
" \"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())})\n",
|
||
"pd.DataFrame(dong).round(3).set_index(\"mô hình (học lại)\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:**\n",
|
||
"\n",
|
||
"| mô hình (học lại) | bắt tin rác bị chèn (câu 10.1) | F1 trên đề thi thường | chặn nhầm |\n",
|
||
"|---|---|---|---|\n",
|
||
"| Naive Bayes | 0.460 (trước: 0.031) | 0.861 | 5 |\n",
|
||
"| SVM | 0.804 (trước: 0.675) | 0.922 | 3 |\n",
|
||
"| Logistic | 0.834 (trước: 0.712) | 0.901 | 14 |\n",
|
||
"\n",
|
||
"📦 **`noi_dung_moi`, `nhan_moi`** = phần học + các tin rác đã bị chèn câu tập · **`tfidf_dk`** = từ điển học lại.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** chỉ cần cho máy **xem trước** một chiêu chèn (dù khác câu kẻ tấn công thật dùng), cả 3 mô hình đều bắt lại được nhiều hơn hẳn — Naive Bayes từ 3% lên 46%, Logistic lên 83% — mà kết quả trên tin thường **gần như không đổi**. Đó là **huấn luyện đối kháng**: phòng thủ phải được tập lại liên tục theo chiêu của kẻ tấn công."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"\n",
|
||
"## Phần 11 — Tổng kết\n",
|
||
"\n",
|
||
"📍 **Bước 7 — Quy tắc bàn tay phải:** tổng kết, báo cáo.\n",
|
||
"\n",
|
||
"### Bài này đã đo được gì?\n",
|
||
"\n",
|
||
"Các con số chính, theo đúng thứ tự pipeline:\n",
|
||
"- **Naïve Bayes** (7.2): không chặn nhầm tin nào (precision 1,0) nhưng bỏ lọt 67/163 tin rác; bị chèn \"từ an toàn\" (10.1) thì chỉ còn bắt 3% → nhanh, gọn nhưng **dễ bị lách nhất**.\n",
|
||
"- **Ngưỡng** (9.3): hạ từ 0,5 xuống 0,3 → bắt thêm 4 tin rác nhưng chặn nhầm thêm 61 tin thật. Cùng một mô hình, ngưỡng mới quyết định đánh đổi.\n",
|
||
"- **Hội đồng** (8.2): chia phiếu đều không hơn SVM đứng một mình; cho SVM phiếu ×2 thì vượt mọi thành viên (F1 0,933).\n",
|
||
"\n",
|
||
"### 11.0 Quy tắc bàn tay ✋ áp vào bài này\n",
|
||
"\n",
|
||
"Xòe bàn tay, đếm từ ngón cái: 5 bước **không được bỏ** của mọi bài toán phát hiện tấn công bằng AI (đầy đủ ở Lab 3 Phần 11). Bài này đã làm đủ 5 bước:\n",
|
||
"\n",
|
||
"| Ngón | Bước | Câu hỏi phải tự trả lời | Trong bài này | Quên bước này thì… |\n",
|
||
"|---|---|---|---|---|\n",
|
||
"| 👍 **1. Ngón cái** | **DỮ LIỆU sạch** | Có bản sao, có lộ đề không? | bỏ 403 bản sao trước khi chia (0.2) | 161 tin đề thi đã nằm sẵn trong phần học → điểm cao **ảo** |\n",
|
||
"| ☝️ **2. Ngón trỏ** | **ĐẶC TRƯNG đúng** | Biến chữ thành số thế nào? Học từ điển ở đâu? | TF-IDF, từ điển **chỉ** học từ phần học (2.2) | lấy cả đề thi để học từ điển = lộ đề |\n",
|
||
"| ✋ **3. Ngón giữa** | **CHỌN CÔNG CỤ** | Có nhãn không? Dữ liệu dạng gì? | văn bản có nhãn → 5 ứng viên có giám sát (Phần 3 – 8); bảng tra ở *Trước khi bắt đầu* | dùng luật từ khóa: chặn nhầm 189, bỏ lọt 199 (1.1) |\n",
|
||
"| 💍 **4. Ngón áp út** | **ĐO trung thực** | Hơn mốc lười bao nhiêu? Chặn nhầm / bỏ lọt bao nhiêu? | mốc 87,4% (2.2) · ma trận nhầm lẫn (9.2) · ngưỡng (9.3) | accuracy cao mà vẫn bỏ lọt cả loạt tin rác |\n",
|
||
"| 🤙 **5. Ngón út** | **GIẢI THÍCH & CẢNH GIÁC** | Vì sao máy chặn? Kẻ tấn công né được không? Ai xem vùng giữa? | từ \"tố cáo\" (3.1, 7.2) · chèn từ an toàn (10) · làn cách ly cho người xem (6.3) | Naive Bayes từ 59% tụt còn 3% khi bị chèn từ |\n",
|
||
"\n",
|
||
"### 11.1 Tương lai và báo cáo\n",
|
||
"\n",
|
||
"**Tương lai (2026):** kẻ tấn công dùng mô hình ngôn ngữ lớn (LLM) để viết tin / email **không lỗi chính tả, đúng văn phong** — những từ \"đáng ngờ\" mà các mô hình trong bài dựa vào sẽ ít dần. Phòng thủ đang chuyển sang: LLM phía phòng thủ đọc **ngữ cảnh và ý định**, kết hợp **hành vi** (người gửi có hay gửi cho người nhận này không? link dẫn đi đâu? giờ gửi có lạ không?) với các mô hình như trong bài."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from pathlib import Path\n",
|
||
"THU_MUC_RA = Path.home() / \"lab\" / \"out_lab4\"\n",
|
||
"THU_MUC_RA.mkdir(parents=True, exist_ok=True)\n",
|
||
"hd = tong_hop.set_index(\"mô hình\").loc[f\"Hội đồng trọng số {TRONG_SO}\"]\n",
|
||
"luat = tong_hop.set_index(\"mô hình\").loc[f\"Luật {len(TU_KHOA)} từ khóa (Phần 1)\"]\n",
|
||
"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)\n",
|
||
"=====================================================================================\n",
|
||
"Đề thi : {len(y_thi)} tin, {int(y_thi.sum())} tin rác\n",
|
||
"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)'])}\n",
|
||
"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)'])}\n",
|
||
"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\n",
|
||
"\n",
|
||
"Khuyến nghị:\n",
|
||
"- 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.\n",
|
||
"- 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.\n",
|
||
"- Luôn bỏ bản sao trước khi chia học / thi; luôn so với mốc \"lười\".\n",
|
||
"- 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).\n",
|
||
"'''\n",
|
||
"(THU_MUC_RA / \"bao_cao_lab4.txt\").write_text(bao_cao, encoding=\"utf-8\")\n",
|
||
"print(bao_cao)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Kết quả:** báo cáo in ra và lưu ở `~/lab/out_lab4/bao_cao_lab4.txt`: `Đề thi 1293 tin, 163 tin rác` · `Luật từ khóa: F1 0.665 | chặn nhầm 46 | bỏ lọt 59` · `Hội đồng trọng số [2, 1, 1]: F1 0.933 | precision 0.973 | recall 0.896 | chặn nhầm 4 | bỏ lọt 17` · `Tấn công chèn từ an toàn: hội đồng bắt được 90% -> 66% tin rác`, rồi 4 khuyến nghị.\n",
|
||
"\n",
|
||
"👉 **Hiểu:** đổi bất kỳ ô ✏️ nào ở trên (từ khóa, ngưỡng, trọng số, câu chèn) rồi **Run All** → báo cáo tự cập nhật theo."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Câu hỏi suy nghĩ:**\n",
|
||
"1. Ở Phần 1, thêm từ khóa nào thì bắt thêm được tin rác mà không chặn nhầm tin thật? Có làm được mãi không?\n",
|
||
"2. Ở SOC của bạn, bỏ lọt 1 tin lừa đảo hay chặn nhầm 1 email của sếp tốn kém hơn? Bạn chọn ngưỡng nào ở 6.3 / 9.3?\n",
|
||
"3. Ở 8.2, đổi `TRONG_SO` thành `[1, 1, 1]` và `[3, 1, 1]`: hội đồng có luôn giỏi hơn mô hình giỏi nhất không?\n",
|
||
"4. Ở 10.1, thử viết `CAU_CHEN` của riêng bạn. Mô hình nào \"ngây thơ\" nhất? Vì sao?"
|
||
]
|
||
}
|
||
],
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5,
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"name": "csai",
|
||
"display_name": "Python (lớp CSAI)",
|
||
"language": "python"
|
||
},
|
||
"language_info": {
|
||
"name": "python"
|
||
}
|
||
}
|
||
} |