Files
labs/Notebooks-CSAI/Lab_OSINT_Phishing_Kit_GPU.ipynb
T

450 lines
25 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Phishing kit bị sao chép hoặc dịch — nhận ra \"cùng một bộ kit\" qua cấu trúc, tài nguyên và ngữ nghĩa (GPU)\n",
"\n",
"> **Cách làm:** **Shift + Enter** để chạy từng ô, lần lượt từ trên xuống. Kernel phải là **Python (lớp CSAI – GPU)**.\n",
"> Ô có dòng **✏️ SỬA** là chỗ mời bạn đổi rồi chạy lại. Chạy xong nhớ chạy ô **dọn dẹp** cuối bài để trả bộ nhớ GPU cho cả lớp.\n",
"\n",
"### 🎯 Mục tiêu của bài\n",
"1. Hiểu vì sao trang lừa đảo **tiếng Việt** và trang **tiếng Anh** có thể là **cùng một bộ kit** (Phần 0).\n",
"2. So hai trang theo **3 lớp**: cấu trúc HTML, tài nguyên dùng chung, **ngữ nghĩa** (Phần 1, 2, 3).\n",
"3. Thấy mô hình **embedding đa ngôn ngữ** (LaBSE) chạy trên GPU so được nghĩa **xuyên ngôn ngữ** — và vì sao **chỉ** ngữ nghĩa thôi là không đủ (Phần 3, 4).\n",
"4. Biết đặt ngưỡng từ **dữ liệu**, không lấy con số \"> 90%\" từ tài liệu (Phần 4).\n",
"\n",
"### 🗺️ Bài này: bài toán gì, cách giải nào?\n",
"- **Bài toán:** cho hai trang web, hỏi **\"có phải cùng một bộ kit lừa đảo không?\"** → bài toán **so độ giống** (similarity), không phải phân loại.\n",
"- **Dữ liệu:** ⚠️ **(minh họa)** — máy chủ dạy học **không có** bản chụp trang lừa đảo thật, nên bài tự viết vài trang HTML cho một thương hiệu **bịa ra** (`NovaPay`). Không dùng thương hiệu thật: quét công khai tên miền giả mạo một thương hiệu thật cũng là **làm lộ thông tin**.\n",
"- **Cách giải:** 3 lớp kiểm tra, mỗi lớp một công cụ: `difflib` (cấu trúc), tập hợp + Jaccard (tài nguyên), **LaBSE** trên GPU + cosine (ngữ nghĩa).\n",
"- **Thước đo:** điểm giống nhau 0–1 của từng lớp; ngưỡng chọn bằng cách nhìn cặp \"cùng kit\" và cặp \"khác kit\".\n",
"\n",
"| Bước pipeline | Phần |\n",
"|---|---|\n",
"| 1. **Thu thập dữ liệu** | Phần 0 (trang minh hoạ) |\n",
"| 2. **Làm sạch dữ liệu** | 1.1 (tách thẻ HTML), 3.1 (tách chữ hiển thị) |\n",
"| 3. **Chọn cách giải** | ngay trên |\n",
"| 4. **Coding** | Phần 1, 2, 3 |\n",
"| 5. **Lưu ý khi coding** | 3.3 (mọi trang đăng nhập đều \"giống nghĩa\") |\n",
"| 6. **Đánh giá** | Phần 4 |\n",
"| 7. **Quy tắc bàn tay phải** ✋ | Phần 5 |\n",
"\n",
"**Từ khoá:** **phishing kit** = bộ file dựng sẵn (HTML, JS, ảnh) mà kẻ lừa đảo mua / chép rồi dựng hàng loạt trang giả · **DOM** = cây thẻ HTML của trang · **embedding** = dãy số biểu diễn **nghĩa** của một câu; hai câu cùng nghĩa (kể cả khác ngôn ngữ) có dãy số gần nhau · **cosine** = độ giống hướng của hai dãy số, 1 = giống hệt.\n",
"\n",
"Ngoài lớp học, công cụ có sẵn để làm việc này là **urlscan.io** (tab *Similar*, tìm theo file dùng chung giữa các lần quét). Chỉ tìm được trong **những gì dịch vụ đó đã quét**, không phải \"toàn cầu\"."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Phần 0 — Thu thập dữ liệu: 5 trang minh hoạ\n",
"\n",
"📍 **Bước 1.** **Dữ liệu gì:** 5 trang HTML viết tay **(minh họa)**. **Lấy ở đâu:** viết ngay trong ô dưới. **Có nhãn:** có — ta biết trang nào cùng kit (đó là lý do dùng dữ liệu minh hoạ: để **kiểm chứng** phương pháp). **Cần làm sạch:** có — phải tách thẻ HTML và chữ hiển thị trước khi so.\n",
"\n",
"| Trang | Là gì | Cùng kit với A? |\n",
"|---|---|---|\n",
"| **A** | kit gốc tiếng Anh, giả trang đăng nhập `NovaPay` | — |\n",
"| **B** | **bản dịch tiếng Việt** của A (giữ nguyên bố cục, file JS/ảnh) | **có** |\n",
"| **C** | **cùng kit A**, đổi thương hiệu thành `OrbitBank` | **có** |\n",
"| **D** | trang đăng nhập **khác hẳn** (bố cục khác, file khác), tiếng Việt | không |\n",
"| **E** | bài báo tiếng Việt, không phải trang đăng nhập | không |"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# (minh họa) — 5 trang tự viết cho thương hiệu bịa. ✏️ SỬA: thêm / đổi trang để thử\n",
"KIT = '''<html lang=\"{lang}\"><head><title>{title}</title><link rel=\"stylesheet\" href=\"/assets/np-style.css\">\n",
"<script src=\"/assets/np-login.js\"></script></head><body><div class=\"wrap\"><img src=\"/assets/logo-np.png\" alt=\"{brand}\">\n",
"<h1>{h1}</h1><form action=\"/next.php\" method=\"post\"><label>{u}</label><input name=\"user\">\n",
"<label>{p}</label><input name=\"pass\" type=\"password\"><button>{b}</button></form>\n",
"<p class=\"note\">{note}</p></div><img src=\"https://{real}/img/footer.png\"></body></html>'''\n",
"TRANG = {\n",
" \"A\": KIT.format(lang=\"en\", title=\"NovaPay - Sign in\", brand=\"NovaPay\", h1=\"Sign in to your NovaPay account\",\n",
" u=\"Email or phone\", p=\"Password\", b=\"Sign in\", note=\"Your account has been limited. Verify within 24 hours to avoid suspension.\",\n",
" real=\"novapay.example\"),\n",
" \"B\": KIT.format(lang=\"vi\", title=\"NovaPay - Đăng nhập\", brand=\"NovaPay\", h1=\"Đăng nhập vào tài khoản NovaPay của bạn\",\n",
" u=\"Email hoặc số điện thoại\", p=\"Mật khẩu\", b=\"Đăng nhập\", note=\"Tài khoản của bạn đã bị hạn chế. Hãy xác minh trong 24 giờ để tránh bị khoá.\",\n",
" real=\"novapay.example\"),\n",
" \"C\": KIT.format(lang=\"en\", title=\"OrbitBank - Sign in\", brand=\"OrbitBank\", h1=\"Sign in to OrbitBank online banking\",\n",
" u=\"Customer ID\", p=\"Password\", b=\"Continue\", note=\"Unusual activity detected. Confirm your identity today to keep access.\",\n",
" real=\"orbitbank.example\"),\n",
" \"D\": '''<html lang=\"vi\"><head><title>Cổng dịch vụ - Đăng nhập</title><link rel=\"stylesheet\" href=\"/static/main.8f2a.css\"></head>\n",
"<body><header><nav><a href=\"/\">Trang chủ</a><a href=\"/help\">Trợ giúp</a></nav></header><main><section><h2>Đăng nhập</h2>\n",
"<div class=\"field\"><span>Tên đăng nhập</span><input id=\"u\"></div><div class=\"field\"><span>Mật khẩu</span><input id=\"p\" type=\"password\"></div>\n",
"<a class=\"btn\" href=\"#\" onclick=\"go()\">Tiếp tục</a></section></main><footer>© Cổng dịch vụ</footer><script src=\"/static/app.31c9.js\"></script></body></html>''',\n",
" \"E\": '''<html lang=\"vi\"><head><title>Tin tức - Thời tiết cuối tuần</title><link rel=\"stylesheet\" href=\"/css/news.css\"></head><body>\n",
"<article><h1>Miền Bắc đón không khí lạnh cuối tuần</h1><p>Nhiệt độ ban đêm có thể xuống dưới 18 độ ở vùng núi.</p>\n",
"<p>Người dân nên theo dõi bản tin dự báo trước khi đi xa.</p></article><script src=\"/js/ads.js\"></script></body></html>''',\n",
"}\n",
"CUNG_KIT = {(\"A\", \"B\"), (\"A\", \"C\"), (\"B\", \"C\")} # đáp án đã biết (vì trang do mình viết)\n",
"for ten, html in TRANG.items():\n",
" print(f\"📦 TRANG['{ten}'] = {len(html)} ký tự, mở đầu: {html[:70]!r}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Kết quả:** 5 trang, dài 358–632 ký tự. A, B, C mở đầu giống nhau (`<html lang=... ><head><title>... - Sign in / Đăng nhập`), D và E khác hẳn.\n",
"\n",
"📦 `TRANG` = từ điển tên trang → mã HTML. · 📦 `CUNG_KIT` = 3 cặp đã biết là cùng kit: A-B, A-C, B-C.\n",
"\n",
"👉 **Hiểu:** đây là dữ liệu **(minh họa)**, có đáp án sẵn để **kiểm chứng phương pháp** trước khi dùng trên trang thật."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Phần 1 — Lớp 1: cấu trúc (DOM)\n",
"\n",
"📍 **Bước 2 → 4.** Bỏ hết chữ, chỉ giữ **dãy tên thẻ** (`html head title link script … form input button …`). Bản dịch đổi chữ nhưng **giữ khung** → dãy thẻ gần như giống hệt. So hai dãy bằng `difflib.SequenceMatcher` (tỉ lệ phần trùng, 0–1).\n",
"\n",
"### 1.1 Tách dãy thẻ"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from html.parser import HTMLParser\n",
"import difflib, itertools\n",
"\n",
"class LayThe(HTMLParser): # HTMLParser của thư viện chuẩn: gọi handle_starttag mỗi khi gặp thẻ mở\n",
" def __init__(self):\n",
" super().__init__(); self.the = []; self.tai_nguyen = set(); self.chu = []\n",
" def handle_starttag(self, tag, attrs):\n",
" self.the.append(tag)\n",
" for k, v in attrs: # attrs = danh sách (tên thuộc tính, giá trị)\n",
" if k in (\"src\", \"href\") and v and not v.startswith(\"#\"):\n",
" self.tai_nguyen.add(v)\n",
" def handle_data(self, data):\n",
" if data.strip():\n",
" self.chu.append(data.strip())\n",
"\n",
"def phan_tich(html):\n",
" p = LayThe(); p.feed(html); return p\n",
"\n",
"PT = {ten: phan_tich(h) for ten, h in TRANG.items()}\n",
"print(\"📦 PT['A'].the =\", \" \".join(PT[\"A\"].the))\n",
"print(\"📦 PT['B'].the =\", \" \".join(PT[\"B\"].the))\n",
"print(\"📦 PT['D'].the =\", \" \".join(PT[\"D\"].the))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Kết quả:** dãy thẻ của A và B **giống hệt nhau** (`html head title link script body div img h1 form label input label input button p img`). Dãy của D khác hẳn: có `header nav main section footer`, không có `form`.\n",
"\n",
"📦 `PT[ten].the` = dãy tên thẻ; `PT[ten].tai_nguyen` = tập file `src`/`href`; `PT[ten].chu` = các đoạn chữ hiển thị.\n",
"\n",
"👉 **Hiểu:** dịch sang tiếng Việt chỉ đổi **chữ**, không đổi **khung**. Bỏ chữ đi thì bản dịch lộ nguyên hình."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 1.2 Điểm giống cấu trúc của mọi cặp"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"CAP = list(itertools.combinations(TRANG, 2)) # mọi cặp (A,B), (A,C)... không lặp\n",
"cau_truc = {(x, y): difflib.SequenceMatcher(None, PT[x].the, PT[y].the).ratio() for x, y in CAP}\n",
"for (x, y), v in sorted(cau_truc.items(), key=lambda kv: -kv[1]):\n",
" print(f\" {x}-{y}: {v:.2f} {'← cùng kit' if (x, y) in CUNG_KIT else ''}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Kết quả:** 3 cặp cùng kit (A-B, A-C, B-C) đều **1,00**. Mọi cặp khác chỉ **0,26–0,39**.\n",
"\n",
"👉 **Hiểu:** với kit chép nguyên khung, lớp cấu trúc tách rất rõ. Trên trang thật, kẻ lừa đảo hay sửa nhẹ (thêm một thẻ, đổi thứ tự) nên điểm sẽ thấp hơn 1 — vì vậy dùng **tỉ lệ giống**, không dùng hash chính xác."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Phần 2 — Lớp 2: tài nguyên dùng chung\n",
"\n",
"Kit chép lại thường **giữ nguyên tên file** JS / CSS / ảnh, và hay **gọi ảnh từ trang thật** (để trông giống). Đo bằng **Jaccard** = số file chung / tổng số file khác nhau của hai trang."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def jaccard(a, b):\n",
" return len(a & b) / len(a | b) if a | b else 0.0 # & = phần chung, | = hợp\n",
"tai_nguyen = {(x, y): jaccard(PT[x].tai_nguyen, PT[y].tai_nguyen) for x, y in CAP}\n",
"print(\"📦 PT['A'].tai_nguyen =\", sorted(PT[\"A\"].tai_nguyen))\n",
"print(\"📦 PT['C'].tai_nguyen =\", sorted(PT[\"C\"].tai_nguyen))\n",
"for (x, y), v in sorted(tai_nguyen.items(), key=lambda kv: -kv[1]):\n",
" print(f\" {x}-{y}: {v:.2f} {'← cùng kit' if (x, y) in CUNG_KIT else ''}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Kết quả:** A-B **1,00** (chung cả 4 file, kể cả ảnh gọi từ `novapay.example`). A-C và B-C **0,60** (chung 3 file `/assets/…`, khác ảnh gọi từ trang thật vì đổi thương hiệu). Mọi cặp khác **0,00**.\n",
"\n",
"📦 `tai_nguyen` = điểm Jaccard của mọi cặp.\n",
"\n",
"👉 **Hiểu:** tên file `np-login.js`, `logo-np.png` là \"chữ ký\" của bộ kit: đổi thương hiệu thì quên đổi tên file. Ảnh gọi thẳng từ trang thật còn cho biết **thương hiệu bị giả mạo** là ai."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Phần 3 — Lớp 3: ngữ nghĩa xuyên ngôn ngữ (LaBSE trên GPU)\n",
"\n",
"**LaBSE** (Language-agnostic BERT Sentence Embedding) biến một câu thành **768 số**; câu cùng nghĩa ở **ngôn ngữ khác nhau** cho ra dãy số gần nhau.\n",
"⚠️ Đính chính hay gặp: BERT / LaBSE **không phải** mô hình dịch. Nó **không dịch** câu, nó đặt câu vào một \"không gian nghĩa\" chung.\n",
"\n",
"### 3.1 Tải mô hình (từ bộ nhớ đệm offline của lớp) và đo GPU vs CPU"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os, time, warnings, torch\n",
"os.environ[\"HF_HUB_DISABLE_PROGRESS_BARS\"] = \"1\" # tắt thanh tiến trình khi nạp mô hình\n",
"warnings.filterwarnings(\"ignore\", message=\"IProgress not found\") # cảnh báo vô hại của tqdm trong Jupyter\n",
"import transformers; transformers.utils.logging.disable_progress_bar(); transformers.utils.logging.set_verbosity_error()\n",
"from sentence_transformers import SentenceTransformer\n",
"print(\"GPU:\", torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"không có\")\n",
"t = time.perf_counter()\n",
"mo_hinh = SentenceTransformer(\"sentence-transformers/LaBSE\", device=\"cuda\") # đọc từ /opt/csai/models (không tải mạng)\n",
"print(f\"📦 mo_hinh = LaBSE, {mo_hinh.encode(\"x\").shape[0]} chiều, nạp trong {time.perf_counter() - t:.1f} s\")\n",
"\n",
"chu = {ten: \" \".join(PT[ten].chu) for ten in TRANG} # chữ hiển thị của mỗi trang (đã bỏ thẻ)\n",
"print(\"📦 chu['B'] =\", chu[\"B\"][:120], \"…\")\n",
"\n",
"cau_thu = list(chu.values()) * 200 # ✏️ SỬA: số lần lặp để đo tốc độ\n",
"for thiet_bi in (\"cuda\", \"cpu\"):\n",
" mo_hinh.to(thiet_bi); t = time.perf_counter()\n",
" mo_hinh.encode(cau_thu, batch_size=64)\n",
" print(f\" mã hoá {len(cau_thu)} đoạn trên {thiet_bi:4s}: {time.perf_counter() - t:5.1f} s\")\n",
"mo_hinh = mo_hinh.to(\"cuda\") # .to() trả lại chính mô hình; gán lại để Jupyter không in cả mô hình"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Kết quả:** LaBSE (768 chiều) nạp trong khoảng **2 giây** từ bộ nhớ đệm của lớp. Mã hoá 1.000 đoạn chữ: **GPU khoảng 2 giây**, **CPU khoảng 45 giây** (đo nhiều lần: 1,7–2,2 s và 44,8–46,7 s) → GPU nhanh hơn khoảng **20 lần**.\n",
"\n",
"📦 `mo_hinh` = mô hình LaBSE trên GPU. · 📦 `chu[ten]` = chữ hiển thị của trang (đã bỏ thẻ).\n",
"\n",
"👉 **Hiểu:** so vài trang thì CPU cũng được; so **hàng nghìn** trang nghi vấn mỗi ngày thì GPU mới làm kịp."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 3.2 Cosine giữa các trang"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from sentence_transformers.util import cos_sim\n",
"vec = {ten: mo_hinh.encode(c, convert_to_tensor=True) for ten, c in chu.items()} # 📦 vec[ten] = tensor 768 số\n",
"ngu_nghia = {(x, y): float(cos_sim(vec[x], vec[y])) for x, y in CAP}\n",
"for (x, y), v in sorted(ngu_nghia.items(), key=lambda kv: -kv[1]):\n",
" print(f\" {x}-{y}: {v:.2f} {'← cùng kit' if (x, y) in CUNG_KIT else ''}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Kết quả:** A-B (bản dịch) **0,94** — cao nhất. Nhưng A-C và B-C (cùng kit, đổi thương hiệu) chỉ **0,55** và **0,50**, **thấp hơn** B-D (**0,52**, trang đăng nhập khác kit) và gần bằng A-D (0,49).\n",
"\n",
"📦 `vec[ten]` = 768 số biểu diễn nghĩa của trang. · 📦 `ngu_nghia` = cosine của mọi cặp.\n",
"\n",
"👉 **Hiểu:** LaBSE nhận ra **bản dịch** rất tốt (tiếng Anh ↔ tiếng Việt 0,94). Nhưng nó đo **nghĩa của chữ**, không đo **kit**: đổi thương hiệu và câu dụ dỗ thì điểm tụt ngang một trang đăng nhập bất kỳ."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 3.3 ⚠️ Lưu ý: mọi trang đăng nhập đều \"giống nghĩa\"\n",
"\n",
"📍 **Bước 5.** Nhìn cặp **A-D**: D là trang đăng nhập **khác kit**, nhưng nghĩa thì cũng \"đăng nhập, mật khẩu, tiếp tục\". So từng câu để thấy rõ:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"cau = [\"Sign in to your NovaPay account\", \"Đăng nhập vào tài khoản NovaPay của bạn\", \"Đăng nhập\", \"Miền Bắc đón không khí lạnh cuối tuần\"]\n",
"v = mo_hinh.encode(cau, convert_to_tensor=True)\n",
"ma_tran = cos_sim(v, v).cpu().numpy() # ma trận cosine mọi cặp câu\n",
"import pandas as pd\n",
"print(pd.DataFrame(ma_tran.round(2), index=[c[:28] for c in cau], columns=[\"c1\", \"c2\", \"c3\", \"c4\"]).to_string())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Kết quả:** \"Sign in to your NovaPay account\" ↔ \"Đăng nhập vào tài khoản NovaPay của bạn\" = **0,96**. Cả hai ↔ \"Đăng nhập\" = **0,47–0,51**. Câu về thời tiết ↔ mọi câu khác = **0,17–0,27**.\n",
"\n",
"👉 **Hiểu:** đúng nghĩa xuyên ngôn ngữ. Nhưng vì mọi trang đăng nhập đều nói \"đăng nhập, mật khẩu, tiếp tục\", ngữ nghĩa **một mình** chỉ nói được \"đây là trang đăng nhập\", không nói được \"cùng một bộ kit\"."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Phần 4 — Đánh giá: ghép 3 lớp, đặt ngưỡng từ dữ liệu\n",
"\n",
"📍 **Bước 6.** Bảng dưới đặt 3 điểm cạnh nhau. **Cách đọc:** mỗi hàng một cặp trang; cột cuối là đáp án đã biết.\n",
"\n",
"| Cách đọc biểu đồ | |\n",
"|---|---|\n",
"| Mỗi nhóm cột | một cặp trang |\n",
"| 3 màu | 3 lớp: cấu trúc, tài nguyên, ngữ nghĩa |\n",
"| Viền đen đậm | cặp **cùng kit** |"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%matplotlib inline\n",
"import matplotlib.pyplot as plt, numpy as np\n",
"bang = pd.DataFrame({\"cấu trúc\": cau_truc, \"tài nguyên\": tai_nguyen, \"ngữ nghĩa\": ngu_nghia})\n",
"bang.index = [f\"{x}-{y}\" for x, y in bang.index]\n",
"bang[\"cùng kit?\"] = [\"có\" if tuple(i.split(\"-\")) in CUNG_KIT else \"không\" for i in bang.index]\n",
"bang = bang.sort_values(\"cấu trúc\", ascending=False)\n",
"print(bang.round(2).to_string())\n",
"\n",
"fig, ax = plt.subplots(figsize=(10, 3.6)); x = np.arange(len(bang)); w = 0.27\n",
"for k, (cot, mau) in enumerate([(\"cấu trúc\", \"#4c78a8\"), (\"tài nguyên\", \"#f58518\"), (\"ngữ nghĩa\", \"#54a24b\")]):\n",
" ax.bar(x + (k - 1) * w, bang[cot], w, color=mau, label=cot,\n",
" edgecolor=[\"black\" if c == \"có\" else \"none\" for c in bang[\"cùng kit?\"]], linewidth=1.5)\n",
"ax.set_xticks(x); ax.set_xticklabels(bang.index); ax.set_ylim(0, 1.05); ax.legend(ncol=3)\n",
"ax.set_title(\"Ba lớp so sánh, mọi cặp trang (viền đen = cùng kit)\"); plt.tight_layout(); plt.show()\n",
"\n",
"# ✏️ SỬA: luật ghép — cùng kit nếu cấu trúc ≥ NGUONG_CT VÀ (tài nguyên ≥ NGUONG_TN HOẶC ngữ nghĩa ≥ NGUONG_NN)\n",
"NGUONG_CT, NGUONG_TN, NGUONG_NN = 0.8, 0.5, 0.8\n",
"bang[\"đoán\"] = np.where((bang[\"cấu trúc\"] >= NGUONG_CT) & ((bang[\"tài nguyên\"] >= NGUONG_TN) | (bang[\"ngữ nghĩa\"] >= NGUONG_NN)), \"có\", \"không\")\n",
"print(\"\\nĐoán đúng:\", (bang[\"đoán\"] == bang[\"cùng kit?\"]).sum(), \"/\", len(bang), \"cặp\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Kết quả:** bảng 10 cặp. Ba cặp cùng kit: cấu trúc **1,00**, tài nguyên **0,6–1,0**, ngữ nghĩa **0,50–0,94**. Bảy cặp khác kit: cấu trúc **≤ 0,39**, tài nguyên **0**, ngữ nghĩa lên tới **0,52**. Luật ghép (cấu trúc ≥ 0,8 **và** (tài nguyên ≥ 0,5 **hoặc** ngữ nghĩa ≥ 0,8)) đoán đúng **10 / 10** cặp.\n",
"\n",
"📦 `bang` = điểm 3 lớp + đáp án + đoán của luật.\n",
"\n",
"👉 **Hiểu:** nếu chỉ dùng ngữ nghĩa với ngưỡng \"> 90%\" như tài liệu gốc gợi ý, ta bắt được **1 / 3** cặp cùng kit (chỉ bản dịch). Ngưỡng phải chọn từ **dữ liệu có đáp án**, và lớp khó đổi nhất (cấu trúc, tài nguyên) phải đứng trước. 10/10 ở đây là trên 5 trang tự viết — trên trang thật sẽ thấp hơn."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Phần 5 — Tổng kết và dọn dẹp\n",
"\n",
"📍 **Bước 7.**\n",
"\n",
"| Ngón | Điều | Trong bài này |\n",
"|---|---|---|\n",
"| 👍 **Dữ liệu sạch** | so cái gì: thẻ, file, hay chữ hiển thị? | tách 3 thứ riêng (1.1, 3.1) |\n",
"| ☝️ **Đặc trưng đúng** | lớp nào **khó đổi** với kẻ lừa đảo? | dịch chữ thì dễ, dựng lại khung + file thì tốn công → cấu trúc + tài nguyên là dấu vết bền |\n",
"| ✋ **Chọn công cụ** | cần GPU khi nào? | LaBSE trên GPU nhanh hơn CPU nhiều lần (3.1) khi phải so hàng nghìn trang |\n",
"| 💍 **Đo trung thực** | ngưỡng lấy từ đâu? | từ cặp đã biết đáp án (Phần 4), không từ \"> 90%\" trong tài liệu |\n",
"| 🤙 **Cảnh giác** | ngữ nghĩa một mình nói được gì? | chỉ nói \"cùng là trang đăng nhập\" (3.3) — không nói \"cùng kit\" |\n",
"\n",
"### Câu hỏi thảo luận\n",
"1. Kẻ lừa đảo muốn né lớp 1 (cấu trúc) thì phải làm gì? Tốn công hơn hay ít hơn né lớp 3?\n",
"2. Nếu bạn có 10.000 trang nghi vấn thật, bạn sẽ chạy lớp nào trước để lọc nhanh, lớp nào sau?\n",
"3. Vì sao bài này không dùng thương hiệu thật, và vì sao không quét công khai tên miền giả mạo thương hiệu thật trên urlscan?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# DỌN DẸP — trả bộ nhớ GPU cho cả lớp\n",
"del mo_hinh, vec, v\n",
"import gc; gc.collect(); torch.cuda.empty_cache()\n",
"print(\"Đã trả GPU. Bộ nhớ GPU notebook này còn giữ:\", torch.cuda.memory_allocated() // 2**20, \"MiB\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Kết quả:** sau khi dọn, notebook còn giữ khoảng **8 MiB** bộ nhớ GPU (từ khoảng 1,8 GB khi có mô hình).\n",
"\n",
"👉 **Hiểu:** GPU dùng chung cả lớp — luôn chạy ô này khi xong bài."
]
}
],
"nbformat": 4,
"nbformat_minor": 5,
"metadata": {
"kernelspec": {
"name": "csai-gpu",
"display_name": "Python (lớp CSAI – GPU)",
"language": "python"
},
"language_info": {
"name": "python"
}
}
}