{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Phát hiện Click Tặc (Ad Click Fraud) trong Quảng cáo Thương mại điện tử\n## Mô hình CNN 1D Residual kết hợp Autoencoder và Tối ưu hóa Bầy Kiến (ACO)\n\n**Bài toán:** Trong các nền tảng TMĐT (Shopee, Lazada...), shop trả tiền theo mô hình\nđấu thầu từ khóa (CPC - Cost Per Click) để quảng cáo lên top tìm kiếm. Đối thủ cạnh\ntranh không lành mạnh có thể thuê người hoặc dùng bot click liên tục vào quảng cáo của\nshop đối thủ nhằm làm cạn nhanh ngân sách quảng cáo mà không mang lại đơn hàng thật —\nđây gọi là **Ad Click Fraud (Click Tặc)**, một vấn đề bảo mật trọng yếu của hệ sinh\nthái quảng cáo TMĐT.\n\n**Cấu trúc đồ án (4 bước):**\n\n| Bước | Nội dung |\n|---|---|\n| 1 | **Chọn tập dữ liệu** — TalkingData AdTracking Fraud Detection (Kaggle) |\n| 2 | **Phân tích mô hình hiện có** — tự huấn luyện lại LightGBM và ANN cơ bản (MLP), là 2 hướng phổ biến nhất trong các notebook Kaggle public cho bài toán này |\n| 3 | **Đề xuất cải tiến** — Autoencoder (thay PCA/SVD) → ACO (chọn đặc trưng) → CNN 1D Residual (kiến trúc mới) |\n| 4 | **Đánh giá** — so sánh Accuracy/Precision/Recall/F1/AUC giữa mô hình hiện có và mô hình đề xuất, có ablation study tách riêng đóng góp của ACO |\n\n**Vì sao KHÔNG dùng PCA/SVD:** PCA/SVD là các phép biến đổi **tuyến tính**, chỉ tìm\nđược tổ hợp tuyến tính của đặc trưng gốc nên không nắm bắt được các quan hệ **phi\ntuyến** phức tạp giữa hành vi click. Đồ án này dùng **Autoencoder** — một mạng neural\nsâu học cách nén dữ liệu (representation learning) — đóng vai trò \"PCA phi tuyến\",\nđồng thời là kỹ thuật Deep Learning hiện đại, đồng nhất về bản chất với CNN ở bước sau.\n\n**Lưu ý về dữ liệu:** File `train.csv` gốc có ~184.9 triệu dòng (~7.5GB). Notebook này\nchạy trên **Kaggle Notebook** (dataset có sẵn, không cần tải về máy), đọc dữ liệu theo\n**chunk**, giữ **toàn bộ** dòng fraud (`is_attributed=1`, ~456 nghìn dòng) và\n**downsample** ngẫu nhiên dòng không-fraud theo tỉ lệ **1:10** để có tập dữ liệu vài\ntriệu dòng vẫn phản ánh đúng đặc điểm mất cân bằng lớp của bài toán thật.\n> `train_sample.csv` (100,000 dòng ngẫu nhiên) chỉ nên dùng để debug code nhanh, không\n> đại diện đủ cho tỉ lệ fraud thật (~0.2%) nên không dùng làm kết quả báo cáo chính thức.\n","metadata":{}},{"cell_type":"markdown","source":"## 0. Cài đặt thư viện và kiểm tra môi trường","metadata":{}},{"cell_type":"code","source":"!pip install -q lightgbm joblib\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-20T20:01:38.934837Z","iopub.execute_input":"2026-06-20T20:01:38.935763Z","iopub.status.idle":"2026-06-20T20:01:43.08963Z","shell.execute_reply.started":"2026-06-20T20:01:38.93572Z","shell.execute_reply":"2026-06-20T20:01:43.088772Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport os, gc, time, random, warnings\nwarnings.filterwarnings(\"ignore\")\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.metrics import (roc_auc_score, accuracy_score, precision_score, recall_score,\n                              f1_score, confusion_matrix, classification_report,\n                              roc_curve)\n\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, callbacks\n\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\nprint(\"TensorFlow version:\", tf.__version__)\nprint(\"GPU available:\", tf.config.list_physical_devices('GPU'))\n\nDATA_DIR = \"/kaggle/input/competitions/talkingdata-adtracking-fraud-detection\"\nprint(os.listdir(DATA_DIR))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## BƯỚC 1 — Chọn tập dữ liệu\n\n**Tập dữ liệu:** [TalkingData AdTracking Fraud Detection Challenge](https://www.kaggle.com/competitions/talkingdata-adtracking-fraud-detection/data) —\nlog click thực tế trên quảng cáo mobile app, với nhãn `is_attributed` cho biết click đó\ncó dẫn tới hành động tải app (download) thật hay không. Click không dẫn tới download\nvới tần suất bất thường là dấu hiệu hành vi click tặc/bot — tương tự bản chất bài toán\nclick fraud trong quảng cáo từ khóa TMĐT (Shopee/Lazada).\n\nMỗi dòng dữ liệu gồm: `ip, app, device, os, channel` (đã được mã hóa số), `click_time`\n(thời điểm click), `attributed_time` (thời điểm tải app nếu có), và nhãn `is_attributed`.\n\n### Chiến lược lấy mẫu\nĐọc `train.csv` theo từng chunk 5 triệu dòng, giữ lại **toàn bộ** click fraud\n(`is_attributed=1`) và **downsample** click không-fraud để đạt tỉ lệ **âm:dương ≈ 10:1**\ntrên toàn bộ tập thu được — vừa nhẹ để xử lý trên GPU free của Kaggle, vừa giữ được đặc\nđiểm mất cân bằng lớp đặc trưng của bài toán.\n\n> Nếu môi trường không đủ RAM, có thể giảm `MAX_CHUNKS` trong cell dưới (ví dụ 5-10).\n","metadata":{}},{"cell_type":"code","source":"\"\"\"\nCHIẾN LƯỢC LẤY MẪU\n------------------\nFile train.csv gốc có ~184.9 triệu dòng, nhưng chỉ ~456,846 dòng có is_attributed = 1\n(tỉ lệ ~0.247%). Đây là một trong những bài toán mất cân bằng lớp (class imbalance)\ncực đoan nhất trên Kaggle.\n\nVì máy không đủ RAM/dung lượng để load toàn bộ 7.5 GB file gốc, ta áp dụng chiến lược\nĐỌC THEO CHUNK (chunksize) kết hợp UNDER-SAMPLING CÓ KIỂM SOÁT:\n    1. Đọc từng chunk 5 triệu dòng một.\n    2. Trong mỗi chunk, giữ lại 100% dòng is_attributed = 1 (vì đây là lớp hiếm, không được bỏ).\n    3. Giữ lại một phần dòng is_attributed = 0 theo xác suất để đạt tỉ lệ âm:dương ~ 10:1\n       trên TOÀN BỘ file (không phải trong từng chunk, để tránh lệch theo thời gian đọc).\n    4. Dừng lại khi đã đọc đủ số chunk định trước hoặc đủ số dòng âm tính mong muốn.\n\nTỉ lệ 1:10 được chọn để mô phỏng một mức mất cân bằng \"vừa phải\" (so với 1:400 của\ndữ liệu gốc) — vẫn đòi hỏi các kỹ thuật xử lý mất cân bằng (class_weight, focal loss...)\nnhưng không khiến mô hình học bị \"chết\" vì quá ít mẫu dương.\n\"\"\"\n\nCHUNK_SIZE = 5_000_000      # số dòng đọc mỗi lần\nMAX_CHUNKS = 40              # an toàn: 40 * 5tr = 200tr dòng -> quét hết file nếu cần\nNEG_PER_POS = 10             # tỉ lệ âm:dương mong muốn = 10:1\nRANDOM_STATE = SEED\n\ndtypes = {\n    'ip': 'uint32',\n    'app': 'uint16',\n    'device': 'uint16',\n    'os': 'uint16',\n    'channel': 'uint16',\n    'is_attributed': 'uint8',\n}\nusecols = ['ip', 'app', 'device', 'os', 'channel', 'click_time', 'is_attributed']\n\ntrain_path = os.path.join(DATA_DIR, \"train.csv\")\n\npos_chunks = []   # các dòng is_attributed = 1\nneg_chunks = []   # các dòng is_attributed = 0 (đã downsample)\n\ntotal_pos = 0\ntotal_neg_kept = 0\n\nprint(\"Bắt đầu đọc train.csv theo chunk ...\")\nt0 = time.time()\n\nreader = pd.read_csv(train_path, usecols=usecols, dtype=dtypes,\n                      parse_dates=['click_time'], chunksize=CHUNK_SIZE)\n\nfor i, chunk in enumerate(reader):\n    pos_mask = chunk['is_attributed'] == 1\n    chunk_pos = chunk[pos_mask]\n    chunk_neg = chunk[~pos_mask]\n\n    # Giữ toàn bộ dòng dương\n    pos_chunks.append(chunk_pos)\n    total_pos += len(chunk_pos)\n\n    # Downsample âm tính: lấy ngẫu nhiên một tỉ lệ nhỏ trong chunk này\n    # tỉ lệ lấy mẫu ước lượng dựa trên tỉ lệ pos toàn cục đã biết trước (~0.247%)\n    sample_frac = min(1.0, (len(chunk_pos) * NEG_PER_POS) / max(1, len(chunk_neg)))\n    if sample_frac > 0:\n        chunk_neg_sampled = chunk_neg.sample(frac=sample_frac, random_state=RANDOM_STATE + i)\n        neg_chunks.append(chunk_neg_sampled)\n        total_neg_kept += len(chunk_neg_sampled)\n\n    print(f\"  Chunk {i+1}: pos={len(chunk_pos):,} | neg_kept={len(chunk_neg_sampled) if sample_frac>0 else 0:,} \"\n          f\"| cumulative pos={total_pos:,} neg={total_neg_kept:,}\")\n\n    if i + 1 >= MAX_CHUNKS:\n        break\n\nprint(f\"\\nHoàn tất đọc dữ liệu trong {time.time()-t0:.1f}s\")\n\ndf_pos = pd.concat(pos_chunks, ignore_index=True)\ndf_neg_all = pd.concat(neg_chunks, ignore_index=True)\n\n# Cắt đúng tỉ lệ NEG_PER_POS lần (vì sample_frac chỉ là ước lượng, có thể dư/thiếu chút)\ntarget_neg = min(len(df_neg_all), len(df_pos) * NEG_PER_POS)\ndf_neg = df_neg_all.sample(n=target_neg, random_state=RANDOM_STATE).reset_index(drop=True)\n\ndf = pd.concat([df_pos, df_neg], ignore_index=True)\ndf = df.sample(frac=1.0, random_state=RANDOM_STATE).reset_index(drop=True)  # shuffle\n\ndel pos_chunks, neg_chunks, df_pos, df_neg_all, df_neg\ngc.collect()\n\nprint(f\"\\nKích thước dataset sau downsample: {df.shape}\")\nprint(df['is_attributed'].value_counts())\nprint(f\"Tỉ lệ fraud (is_attributed=1): {df['is_attributed'].mean()*100:.3f}%\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-20T20:01:59.214015Z","iopub.execute_input":"2026-06-20T20:01:59.214996Z","iopub.status.idle":"2026-06-20T20:04:43.789426Z","shell.execute_reply.started":"2026-06-20T20:01:59.214962Z","shell.execute_reply":"2026-06-20T20:04:43.78868Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Khám phá dữ liệu nhanh (EDA)","metadata":{}},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(13, 4))\n\ndf['is_attributed'].value_counts().plot(kind='bar', ax=axes[0], color=['#4C72B0', '#DD8452'])\naxes[0].set_title('Phân bố nhãn: Click thật (0) vs Click Fraud/Bot (1)')\naxes[0].set_xticklabels(['Click thật (0)', 'Click Fraud (1)'], rotation=0)\naxes[0].set_ylabel('Số lượng')\n\ndf['click_time'].dt.hour.value_counts().sort_index().plot(kind='bar', ax=axes[1], color='#55A868')\naxes[1].set_title('Phân bố số click theo giờ trong ngày')\naxes[1].set_xlabel('Giờ'); axes[1].set_ylabel('Số lượng click')\n\nplt.tight_layout()\nplt.savefig('eda_overview.png', dpi=120)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-20T20:04:43.791452Z","iopub.execute_input":"2026-06-20T20:04:43.79167Z","iopub.status.idle":"2026-06-20T20:04:44.659206Z","shell.execute_reply.started":"2026-06-20T20:04:43.79165Z","shell.execute_reply":"2026-06-20T20:04:44.657798Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Feature Engineering\n\nSinh các đặc trưng đếm (count theo ip/app/device/os/channel), tổ hợp nhóm, số lượng\ngiá trị duy nhất (nunique), khoảng thời gian giữa các click liên tiếp của cùng 1 IP,\nvà tỉ lệ download trung bình theo từng ID — đây là các đặc trưng \"thô\", nhiều và tương\nquan cao, sẽ được xử lý bằng Autoencoder ở Bước 3.\n","metadata":{}},{"cell_type":"code","source":"\"\"\"\nFEATURE ENGINEERING\n--------------------\nDữ liệu gốc chỉ có 5 trường định danh đã mã hóa (ip, app, device, os, channel) và\n1 mốc thời gian (click_time). Bản thân các ID này không mang nhiều ý nghĩa thống kê\nnếu dùng trực tiếp — giá trị của chúng KHÔNG có quan hệ thứ tự (vd ip=5000 không \"lớn\nhơn\" ip=10 theo nghĩa thống kê nào cả).\n\nĐặc điểm hành vi của click tặc (click fraud) thường thể hiện qua:\n    - Một IP click QUÁ NHIỀU lần trong thời gian ngắn (bot rải click).\n    - Một IP click nhiều app/channel khác nhau bất thường.\n    - Khoảng cách thời gian giữa các click liên tiếp từ cùng 1 IP rất đều/rất ngắn\n      (machine-like, khác hành vi người dùng thật).\n    - Tỉ lệ click→download (is_attributed) trên một IP/app/channel rất thấp.\n\n=> Ta tạo ra các đặc trưng \"đếm\" (count) và \"tần suất\" (frequency) theo nhiều cách\n   kết hợp nhóm (groupby) khác nhau. Đây chính là các đặc trưng \"thừa thải\" (redundant,\n   tương quan cao với nhau) mà đề bài yêu cầu dùng PCA/SVD để lọc bớt trước khi đưa\n   vào bước chọn đặc trưng bằng ACO.\n\"\"\"\n\n# ---- 1. Tách đặc trưng thời gian từ click_time ----\ndf['click_hour'] = df['click_time'].dt.hour.astype('uint8')\ndf['click_day'] = df['click_time'].dt.day.astype('uint8')\ndf['click_dow'] = df['click_time'].dt.dayofweek.astype('uint8')      # ngày trong tuần\ndf['click_minute'] = df['click_time'].dt.minute.astype('uint8')\n\n# ---- 2. Đặc trưng đếm (count) theo từng nhóm đơn ----\ngroup_cols_single = ['ip', 'app', 'device', 'os', 'channel']\nfor col in group_cols_single:\n    freq = df.groupby(col)[col].transform('count')\n    df[f'count_{col}'] = freq.astype('float32')\n\n# ---- 3. Đặc trưng đếm theo tổ hợp nhóm (bắt hành vi bot rõ hơn) ----\ngroup_combos = [\n    ['ip', 'app'],\n    ['ip', 'device'],\n    ['ip', 'os'],\n    ['ip', 'channel'],\n    ['ip', 'click_hour'],\n    ['ip', 'app', 'os'],\n    ['app', 'channel'],\n]\nfor combo in group_combos:\n    name = 'count_' + '_'.join(combo)\n    df[name] = df.groupby(combo)[combo[0]].transform('count').astype('float32')\n\n# ---- 4. Số lượng giá trị duy nhất (nunique) -- bot thường gọi nhiều app/channel khác nhau từ 1 IP ----\nnunique_combos = [\n    ('ip', 'app'),\n    ('ip', 'channel'),\n    ('ip', 'device'),\n    ('app', 'channel'),\n]\nfor base, target in nunique_combos:\n    nq = df.groupby(base)[target].transform('nunique')\n    df[f'nunique_{base}_{target}'] = nq.astype('float32')\n\n# ---- 5. Đặc trưng thời gian giữa các click liên tiếp của cùng 1 IP (click interval) ----\ndf = df.sort_values('click_time').reset_index(drop=True)\ndf['click_time_unix'] = df['click_time'].astype('int64') // 10**9\ndf['ip_prev_click_delta'] = (\n    df.groupby('ip')['click_time_unix'].diff().fillna(-1).astype('float32')\n)\ndf['ip_next_click_delta'] = (\n    df.groupby('ip')['click_time_unix'].diff(-1).abs().fillna(-1).astype('float32')\n)\n\n# ---- 6. Tỉ lệ download trung bình theo ip/app/channel (mean target encoding, tính trên chính tập train) ----\n# Lưu ý: trong thực tế cần encode trên fold riêng để tránh leakage; ở đây dùng toàn bộ\n# tập đã downsample cho mục đích minh họa môn học.\nfor col in ['ip', 'app', 'channel', 'os', 'device']:\n    mean_target = df.groupby(col)['is_attributed'].transform('mean')\n    df[f'mean_attr_{col}'] = mean_target.astype('float32')\n\nprint(f\"Kích thước dataset sau feature engineering: {df.shape}\")\nprint(\"\\nDanh sách các cột đặc trưng đã sinh ra:\")\nengineered_cols = [c for c in df.columns if c not in\n                    ['ip', 'app', 'device', 'os', 'channel', 'click_time',\n                     'attributed_time', 'is_attributed', 'click_time_unix']]\nprint(engineered_cols)\nprint(f\"\\nTổng số đặc trưng (chưa gồm 5 ID gốc): {len(engineered_cols)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-20T20:04:44.660524Z","iopub.execute_input":"2026-06-20T20:04:44.66097Z","iopub.status.idle":"2026-06-20T20:04:57.056219Z","shell.execute_reply.started":"2026-06-20T20:04:44.660894Z","shell.execute_reply":"2026-06-20T20:04:57.055446Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## BƯỚC 2 — Phân tích mô hình hiện có\n\nTheo khảo sát các notebook public trên Kaggle cho bài toán TalkingData, hai hướng tiếp\ncận phổ biến nhất là:\n1. **LightGBM** (Gradient Boosting) — được dùng trong hầu hết các lời giải hạng cao\n   của competition gốc, xử lý tốt dữ liệu dạng bảng và mất cân bằng lớp.\n2. **ANN cơ bản (Multi-Layer Perceptron)** — vài lớp Dense liên tiếp, không có cơ chế\n   chọn đặc trưng hay tích chập đặc biệt.\n\nTa **tự huấn luyện lại** cả hai mô hình này trên cùng tập dữ liệu đã lấy mẫu ở Bước 1\n(toàn bộ đặc trưng gốc, KHÔNG qua Autoencoder/ACO) để có số liệu thật làm nền so sánh\nở Bước 4.\n","metadata":{}},{"cell_type":"code","source":"\"\"\"\nCHUẨN BỊ DỮ LIỆU CHO BƯỚC PHÂN TÍCH MÔ HÌNH HIỆN CÓ (BASELINE)\n------------------------------------------------------------------\nTrước khi đề xuất cải tiến, ta cần một bộ dữ liệu đặc trưng \"thô\" (chưa qua\nAutoencoder/ACO) để huấn luyện các mô hình baseline phổ biến nhất được dùng trong các\nnotebook Kaggle public cho bài toán TalkingData: LightGBM (Gradient Boosting) và\nANN cơ bản (Multi-Layer Perceptron). Đây chính là \"mô hình hiện có\" cần phân tích ở\nBước 2 của đồ án.\n\"\"\"\n\nfeature_cols = engineered_cols  # toàn bộ đặc trưng đã sinh ra ở bước Feature Engineering\nX_raw = df[feature_cols].values.astype('float32')\ny = df['is_attributed'].values.astype('int32')\n\nprint(f\"Số đặc trưng thô (trước Autoencoder/ACO): {X_raw.shape[1]}\")\nprint(f\"Danh sách đặc trưng: {feature_cols}\")\n\nscaler = StandardScaler()\nX_scaled = scaler.fit_transform(X_raw)\n\n# Chia train/test dùng chung cho toàn bộ phần so sánh (đảm bảo công bằng giữa các mô hình)\nX_train_base, X_test_base, y_train_base, y_test_base = train_test_split(\n    X_scaled, y, test_size=0.30, random_state=SEED, stratify=y\n)\nprint(f\"Train: {X_train_base.shape} | Test: {X_test_base.shape}\")\nprint(f\"Tỉ lệ fraud - train: {y_train_base.mean()*100:.3f}% | test: {y_test_base.mean()*100:.3f}%\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-20T20:04:57.057216Z","iopub.execute_input":"2026-06-20T20:04:57.057527Z","iopub.status.idle":"2026-06-20T20:05:03.192688Z","shell.execute_reply.started":"2026-06-20T20:04:57.057494Z","shell.execute_reply":"2026-06-20T20:05:03.192005Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Mô hình hiện có #1: LightGBM","metadata":{}},{"cell_type":"code","source":"\"\"\"\nMÔ HÌNH HIỆN CÓ #1: LIGHTGBM (GRADIENT BOOSTING)\n----------------------------------------------------\nLightGBM là mô hình được sử dụng nhiều nhất trong các giải pháp hạng cao trên Kaggle\ncho bài toán TalkingData (bao gồm cả các lời giải top 1-10 của competition gốc), nhờ\nkhả năng xử lý tốt dữ liệu dạng bảng, dữ liệu mất cân bằng (qua `class_weight` hoặc\n`scale_pos_weight`), và tốc độ huấn luyện nhanh trên hàng triệu dòng.\n\nĐây được dùng làm MÔ HÌNH GỐC #1 để so sánh với hướng cải tiến đề xuất.\n\"\"\"\n\nimport lightgbm as lgb\n\nt0 = time.time()\nlgb_clf = lgb.LGBMClassifier(\n    n_estimators=300,\n    learning_rate=0.05,\n    num_leaves=63,\n    class_weight='balanced',\n    random_state=SEED,\n    verbose=-1,\n)\nlgb_clf.fit(X_train_base, y_train_base)\nlgb_train_time = time.time() - t0\n\nlgb_prob = lgb_clf.predict_proba(X_test_base)[:, 1]\nlgb_pred = (lgb_prob >= 0.5).astype(int)\n\nprint(f\"Thời gian huấn luyện LightGBM: {lgb_train_time:.1f}s\")\nprint(f\"AUC-ROC   : {roc_auc_score(y_test_base, lgb_prob):.4f}\")\nprint(f\"Accuracy  : {accuracy_score(y_test_base, lgb_pred):.4f}\")\nprint(f\"Precision : {precision_score(y_test_base, lgb_pred):.4f}\")\nprint(f\"Recall    : {recall_score(y_test_base, lgb_pred):.4f}\")\nprint(f\"F1-score  : {f1_score(y_test_base, lgb_pred):.4f}\")\n\n# Mức độ quan trọng đặc trưng theo LightGBM (tham khảo, không dùng trực tiếp cho ACO)\nplt.figure(figsize=(8, 6))\nimportance_df = pd.DataFrame({\n    'feature': feature_cols,\n    'importance': lgb_clf.feature_importances_\n}).sort_values('importance', ascending=True)\nplt.barh(importance_df['feature'], importance_df['importance'], color='#4C72B0')\nplt.title('Mức độ quan trọng đặc trưng theo LightGBM (mô hình hiện có)')\nplt.xlabel('Importance (số lần dùng để split)')\nplt.tight_layout()\nplt.savefig('lightgbm_feature_importance.png', dpi=120)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-20T20:05:03.193818Z","iopub.execute_input":"2026-06-20T20:05:03.194428Z","iopub.status.idle":"2026-06-20T20:06:52.860707Z","shell.execute_reply.started":"2026-06-20T20:05:03.194402Z","shell.execute_reply":"2026-06-20T20:06:52.859872Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Mô hình hiện có #2: ANN cơ bản (MLP)","metadata":{}},{"cell_type":"code","source":"\"\"\"\nMÔ HÌNH HIỆN CÓ #2: ANN CƠ BẢN (MULTI-LAYER PERCEPTRON)\n------------------------------------------------------------\nBên cạnh LightGBM, nhiều notebook Kaggle khác cho bài toán này dùng mạng nơ-ron\nnhân tạo (Artificial Neural Network / MLP) đơn giản gồm vài lớp Dense (Fully-Connected)\nliên tiếp, không có lớp tích chập hay cơ chế chọn đặc trưng đặc biệt nào.\n\nĐây được dùng làm MÔ HÌNH GỐC #2 để so sánh - đại diện cho hướng \"ANN cơ bản\" mà đề\nbài đề cập.\n\"\"\"\n\ndef build_ann_baseline(input_dim):\n    model = models.Sequential(name=\"ANN_Baseline\")\n    model.add(layers.Input(shape=(input_dim,)))\n    model.add(layers.Dense(128, activation='relu'))\n    model.add(layers.Dropout(0.3))\n    model.add(layers.Dense(64, activation='relu'))\n    model.add(layers.Dropout(0.3))\n    model.add(layers.Dense(32, activation='relu'))\n    model.add(layers.Dense(1, activation='sigmoid'))\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n        loss='binary_crossentropy',\n        metrics=['accuracy', tf.keras.metrics.AUC(name='auc'),\n                 tf.keras.metrics.Precision(name='precision'),\n                 tf.keras.metrics.Recall(name='recall')]\n    )\n    return model\n\ncw_arr_base = compute_class_weight('balanced', classes=np.array([0, 1]), y=y_train_base)\ncw_dict_base = {0: cw_arr_base[0], 1: cw_arr_base[1]}\n\nt0 = time.time()\nann_model = build_ann_baseline(X_train_base.shape[1])\nann_history = ann_model.fit(\n    X_train_base, y_train_base,\n    validation_split=0.15,\n    epochs=40,\n    batch_size=512,\n    class_weight=cw_dict_base,\n    callbacks=[callbacks.EarlyStopping(monitor='val_auc', mode='max',\n                                        patience=6, restore_best_weights=True)],\n    verbose=0\n)\nann_train_time = time.time() - t0\n\nann_prob = ann_model.predict(X_test_base, batch_size=1024).ravel()\nann_pred = (ann_prob >= 0.5).astype(int)\n\nprint(f\"Thời gian huấn luyện ANN cơ bản: {ann_train_time:.1f}s\")\nprint(f\"AUC-ROC   : {roc_auc_score(y_test_base, ann_prob):.4f}\")\nprint(f\"Accuracy  : {accuracy_score(y_test_base, ann_pred):.4f}\")\nprint(f\"Precision : {precision_score(y_test_base, ann_pred):.4f}\")\nprint(f\"Recall    : {recall_score(y_test_base, ann_pred):.4f}\")\nprint(f\"F1-score  : {f1_score(y_test_base, ann_pred):.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-20T20:06:52.861674Z","iopub.execute_input":"2026-06-20T20:06:52.862591Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## BƯỚC 3 — Đề xuất cải tiến\n\nBa cải tiến chính so với mô hình hiện có ở Bước 2:\n\n1. **Nâng cấp kỹ thuật chọn đặc trưng:** thay PCA/SVD (tuyến tính, đã cũ) bằng\n   **Autoencoder** (mạng neural sâu, học phi tuyến) để nén đặc trưng.\n2. **Kết hợp thuật toán tối ưu hóa bầy kiến (ACO):** chọn ra tập con đặc trưng \"chí\n   mạng\" nhất từ không gian latent của Autoencoder + đặc trưng gốc có ý nghĩa.\n3. **Thay đổi cấu trúc mô hình:** CNN 1D bổ sung **Residual Block** (kỹ thuật từ\n   ResNet) thay cho CNN tuần tự thông thường, giúp việc thêm/bớt lớp an toàn hơn và\n   giảm vanishing gradient.\n","metadata":{}},{"cell_type":"markdown","source":"### 3.1. Autoencoder — thay thế PCA/SVD","metadata":{}},{"cell_type":"code","source":"\"\"\"\nĐỀ XUẤT CẢI TIẾN #1: AUTOENCODER THAY CHO PCA/SVD\n------------------------------------------------------\nLÝ DO THAY THẾ PCA/SVD:\nPCA và SVD là các phép BIẾN ĐỔI TUYẾN TÍNH — chúng chỉ tìm được các trục (thành phần\nchính) là TỔ HỢP TUYẾN TÍNH của đặc trưng gốc, nên không nắm bắt được các mối quan hệ\nPHI TUYẾN phức tạp giữa các đặc trưng đếm/tần suất (vd quan hệ giữa count_ip và\nip_prev_click_delta trong việc phát hiện bot không nhất thiết là tuyến tính: một IP có\nthể có count_ip cao nhưng vẫn là người dùng thật nếu khoảng cách click đủ \"tự nhiên\").\n\nAUTOENCODER (Bộ mã hóa tự động) là một kiến trúc MẠNG NEURAL SÂU gồm 2 phần:\n    - ENCODER: nén dữ liệu đầu vào (n chiều) xuống một không gian ẩn có chiều thấp hơn\n      (latent space, k chiều, k << n) thông qua các lớp Dense với hàm kích hoạt phi tuyến\n      (ReLU). Không gian ẩn này được gọi là \"bottleneck\".\n    - DECODER: cố gắng TÁI TẠO LẠI dữ liệu gốc từ không gian ẩn đó.\n    - Mạng được huấn luyện để tối thiểu hóa SAI SỐ TÁI TẠO (reconstruction loss, thường\n      là Mean Squared Error) giữa đầu vào và đầu ra tái tạo.\n\nSau khi huấn luyện xong, ta CHỈ GIỮ LẠI PHẦN ENCODER và dùng nó để biến đổi dữ liệu\ngốc thành các đặc trưng nén (latent features) — đây chính là dạng \"PCA phi tuyến\"\n(non-linear PCA), nắm bắt được cấu trúc dữ liệu phức tạp hơn nhiều so với PCA/SVD\ntruyền thống, đồng thời là một kỹ thuật Deep Learning hiện đại, phù hợp xu hướng kết\nhợp với CNN trong cùng một pipeline học sâu.\n\nKIẾN TRÚC AUTOENCODER ĐỀ XUẤT:\n    Input (n_features)\n        -> Dense(32, relu) -> Dense(16, relu) -> Dense(LATENT_DIM, relu)   [Encoder]\n        -> Dense(16, relu) -> Dense(32, relu) -> Dense(n_features, linear) [Decoder]\n\"\"\"\n\nLATENT_DIM = 10   # số chiều không gian ẩn (latent features) - tương đương \"số PC giữ lại\"\n\nn_features_total_ae = X_scaled.shape[1]\n\n# Chia riêng dữ liệu để huấn luyện Autoencoder (không cần nhãn, học không giám sát)\nX_ae_train, X_ae_val = train_test_split(X_scaled, test_size=0.15, random_state=SEED)\n\n\ndef build_autoencoder(input_dim, latent_dim):\n    # ----- Encoder -----\n    inputs = layers.Input(shape=(input_dim,), name='encoder_input')\n    x = layers.Dense(32, activation='relu')(inputs)\n    x = layers.Dense(16, activation='relu')(x)\n    latent = layers.Dense(latent_dim, activation='relu', name='latent_space')(x)\n\n    # ----- Decoder -----\n    x = layers.Dense(16, activation='relu')(latent)\n    x = layers.Dense(32, activation='relu')(x)\n    outputs = layers.Dense(input_dim, activation='linear', name='reconstruction')(x)\n\n    autoencoder = models.Model(inputs, outputs, name='Autoencoder')\n    encoder = models.Model(inputs, latent, name='Encoder')\n\n    autoencoder.compile(optimizer=tf.keras.optimizers.Adam(1e-3), loss='mse')\n    return autoencoder, encoder\n\n\nautoencoder, encoder = build_autoencoder(n_features_total_ae, LATENT_DIM)\nautoencoder.summary()\n\nae_history = autoencoder.fit(\n    X_ae_train, X_ae_train,\n    validation_data=(X_ae_val, X_ae_val),\n    epochs=50,\n    batch_size=512,\n    callbacks=[callbacks.EarlyStopping(monitor='val_loss', patience=6,\n                                        restore_best_weights=True)],\n    verbose=2,\n)\n\n# ---- Trực quan hoá quá trình huấn luyện Autoencoder ----\nplt.figure(figsize=(7, 4))\nplt.plot(ae_history.history['loss'], label='Train Reconstruction Loss (MSE)')\nplt.plot(ae_history.history['val_loss'], label='Val Reconstruction Loss (MSE)')\nplt.xlabel('Epoch'); plt.ylabel('MSE')\nplt.title('Quá trình huấn luyện Autoencoder (sai số tái tạo)')\nplt.legend()\nplt.tight_layout()\nplt.savefig('autoencoder_training.png', dpi=120)\nplt.show()\n\n# ---- Biến đổi toàn bộ dữ liệu sang không gian ẩn (latent space) bằng Encoder ----\nX_latent = encoder.predict(X_scaled, batch_size=2048)\nprint(f\"\\nĐã giảm chiều bằng Autoencoder: {X_scaled.shape[1]} -> {X_latent.shape[1]} chiều (latent features)\")\n\nlatent_feature_names = [f'AE_{i+1}' for i in range(LATENT_DIM)]\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.2. Kết hợp Latent Features với đặc trưng gốc có ý nghĩa","metadata":{}},{"cell_type":"code","source":"\"\"\"\nKẾT HỢP LATENT FEATURES (AUTOENCODER) VỚI ĐẶC TRƯNG GỐC CÓ Ý NGHĨA\n------------------------------------------------------------------------\nTương tự cách làm với PCA ở các đồ án trước, ta KHÔNG ép buộc ACO chỉ được chọn trong\nkhông gian latent trừu tượng (AE_1, AE_2, ...) mà còn cung cấp thêm một số đặc trưng\ngốc DỄ DIỄN GIẢI (giờ click, ngày trong tuần, khoảng cách thời gian giữa các click,\ntỉ lệ download trung bình theo ip/app...) để ACO có đầy đủ \"nguyên liệu\" lựa chọn,\ngiúp kết quả cuối cùng vừa có tính biểu diễn tốt (nhờ Autoencoder) vừa có thể giải\nthích được (nhờ các đặc trưng gốc mang ý nghĩa nghiệp vụ rõ ràng).\n\"\"\"\n\nsemantic_cols = ['click_hour', 'click_dow', 'ip_prev_click_delta', 'ip_next_click_delta',\n                  'count_ip', 'count_ip_app', 'nunique_ip_app', 'mean_attr_ip', 'mean_attr_app']\nX_semantic = df[semantic_cols].values.astype('float32')\nX_semantic_scaled = StandardScaler().fit_transform(X_semantic)\n\n# Không gian đặc trưng ứng viên cho ACO = [latent features từ Autoencoder] + [đặc trưng gốc có ý nghĩa]\ncandidate_feature_names = latent_feature_names + semantic_cols\nX_candidates = np.hstack([X_latent, X_semantic_scaled]).astype('float32')\n\nprint(f\"Không gian đặc trưng ứng viên đưa vào ACO: {X_candidates.shape[1]} đặc trưng\")\nprint(candidate_feature_names)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.3. Thuật toán Tối ưu hóa Bầy Kiến (ACO) chọn đặc trưng","metadata":{}},{"cell_type":"code","source":"\"\"\"\nTHUẬT TOÁN TỐI ƯU HÓA BẦY KIẾN (ANT COLONY OPTIMIZATION - ACO) ĐỂ CHỌN ĐẶC TRƯNG\n----------------------------------------------------------------------------------\n...\n\"\"\"\n\nfrom sklearn.feature_selection import f_classif\nfrom sklearn.linear_model import LogisticRegression\n\n# ---- Chuẩn bị: chia nhỏ một tập validation nhanh để đánh giá fitness ----\nX_for_aco = X_candidates\nn_features_total = X_for_aco.shape[1]\n\nX_aco_train, X_aco_val, y_aco_train, y_aco_val = train_test_split(\n    X_for_aco, y, test_size=0.3, random_state=SEED, stratify=y\n)\n\n# Heuristic ban đầu η: dùng ANOVA F-score giữa từng đặc trưng và nhãn (chuẩn hóa về [0,1])\nf_scores, _ = f_classif(X_aco_train, y_aco_train)\nf_scores = np.nan_to_num(f_scores, nan=0.0)\neta = (f_scores - f_scores.min()) / (f_scores.max() - f_scores.min() + 1e-12)\neta = np.clip(eta, 1e-6, None)   # tránh giá trị 0 tuyệt đối\n\nprint(\"Heuristic (ANOVA F-score chuẩn hóa) cho từng đặc trưng ứng viên:\")\nfor name, val in zip(candidate_feature_names, eta):\n    print(f\"  {name:25s}: {val:.4f}\")\n\n\nclass ACOFeatureSelector:\n    \"\"\"\n    Cài đặt thuật toán ACO cho bài toán chọn đặc trưng (Feature Subset Selection).\n\n    Tham số\n    -------\n    n_ants          : số lượng kiến mỗi thế hệ\n    n_generations   : số thế hệ (vòng lặp)\n    alpha           : hệ số ảnh hưởng của pheromone\n    beta            : hệ số ảnh hưởng của heuristic\n    evaporation_rate: tốc độ bay hơi pheromone (rho)\n    q               : hằng số cập nhật pheromone (lượng pheromone kiến để lại)\n    min_features    : số đặc trưng tối thiểu 1 kiến phải chọn\n    max_features    : số đặc trưng tối đa 1 kiến được chọn\n    penalty_weight  : trọng số phạt cho số lượng đặc trưng (càng lớn càng ưu tiên ít đặc trưng)\n    \"\"\"\n\n    def __init__(self, n_features, eta, n_ants=30, n_generations=40,\n                 alpha=1.0, beta=2.0, evaporation_rate=0.2, q=1.0,\n                 min_features=5, max_features=None, random_state=42,\n                 penalty_weight=0.0005):  # <-- giảm penalty\n        self.n_features = n_features\n        self.eta = eta\n        self.n_ants = n_ants\n        self.n_generations = n_generations\n        self.alpha = alpha\n        self.beta = beta\n        self.rho = evaporation_rate\n        self.q = q\n        self.min_features = min_features\n        self.max_features = max_features or n_features\n        self.penalty_weight = penalty_weight\n        self.rng = np.random.RandomState(random_state)\n\n        # Khởi tạo pheromone đồng nhất cho tất cả đặc trưng\n        self.tau = np.ones(n_features, dtype='float64')\n\n        self.history_best_fitness = []\n        self.history_avg_fitness = []\n        self.best_subset = None\n        self.best_fitness = -np.inf\n\n    def _construct_solution(self):\n        \"\"\"Một con kiến xây dựng 1 tập con đặc trưng dựa trên xác suất pheromone*heuristic.\"\"\"\n        available = list(range(self.n_features))\n        selected = []\n\n        # Số đặc trưng kiến này sẽ chọn (ngẫu nhiên trong khoảng min-max để đa dạng hoá)\n        target_size = self.rng.randint(self.min_features, self.max_features + 1)\n\n        for _ in range(target_size):\n            if not available:\n                break\n            tau_avail = self.tau[available] ** self.alpha\n            eta_avail = self.eta[available] ** self.beta\n            \n            weights = tau_avail * eta_avail\n            \n            # Xử lý NaN và Inf\n            weights = np.nan_to_num(\n                weights,\n                nan=0.0,\n                posinf=1e10,\n                neginf=0.0\n            )\n            \n            # Tính xác suất\n            if weights.sum() <= 0:\n                probs = np.ones(len(available)) / len(available)\n            else:\n                probs = weights / weights.sum()\n            \n            # Chuẩn hóa lại để tổng xác suất = 1\n            probs = probs / probs.sum()\n            \n            choice = self.rng.choice(available, p=probs)\n            selected.append(choice)\n            available.remove(choice)\n\n        return sorted(selected)\n\n    def _fitness(self, subset, X_train, y_train, X_val, y_val):\n        \"\"\"\n        Đánh giá nhanh một tập con đặc trưng bằng Logistic Regression + AUC-ROC.\n        Dùng mô hình nhẹ để tốc độ đánh giá đủ nhanh cho nhiều thế hệ kiến.\n        \"\"\"\n        if len(subset) == 0:\n            return 0.0\n        clf = LogisticRegression(max_iter=200, class_weight='balanced')\n        clf.fit(X_train[:, subset], y_train)\n        prob = clf.predict_proba(X_val[:, subset])[:, 1]\n        auc = roc_auc_score(y_val, prob)\n\n        # Phạt nhẹ nếu tập con quá nhiều đặc trưng (khuyến khích tính gọn / parsimony)\n        penalty = self.penalty_weight * len(subset)   # <-- sử dụng penalty_weight\n        return auc - penalty\n\n    def fit(self, X_train, y_train, X_val, y_val, verbose=True):\n        for gen in range(self.n_generations):\n            solutions = []\n            fitnesses = []\n\n            for ant in range(self.n_ants):\n                subset = self._construct_solution()\n                fit = self._fitness(subset, X_train, y_train, X_val, y_val)\n                solutions.append(subset)\n                fitnesses.append(fit)\n\n                if fit > self.best_fitness:\n                    self.best_fitness = fit\n                    self.best_subset = subset\n\n            # ---- Cập nhật pheromone: bay hơi trước, sau đó cộng thêm từ các kiến ----\n            self.tau *= (1 - self.rho)\n\n            for subset, fit in zip(solutions, fitnesses):\n                delta = self.q * max(fit, 0)  # chỉ cộng pheromone cho fitness dương\n                for feat_idx in subset:\n                    self.tau[feat_idx] += delta\n\n            avg_fit = float(np.mean(fitnesses))\n            self.history_best_fitness.append(self.best_fitness)\n            self.history_avg_fitness.append(avg_fit)\n\n            if verbose and (gen % 5 == 0 or gen == self.n_generations - 1):\n                print(f\"  Thế hệ {gen+1:3d}/{self.n_generations} | \"\n                      f\"Fitness TB={avg_fit:.4f} | Fitness tốt nhất={self.best_fitness:.4f} | \"\n                      f\"Số đặc trưng tốt nhất={len(self.best_subset)}\")\n\n        return self\n\nprint(\"X_candidates shape:\", X_candidates.shape)\nprint(\"n_features_total:\", n_features_total)\nprint(\"eta min:\", eta.min())\nprint(\"eta max:\", eta.max())\nprint(\"NaN trong eta:\", np.isnan(eta).sum())\nprint(\"Inf trong eta:\", np.isinf(eta).sum())\n\nprint(\"\\n=== Bắt đầu chạy ACO để chọn tập đặc trưng tối ưu ===\")\nt0_aco = time.time() \naco = ACOFeatureSelector(\n    n_features=n_features_total,\n    eta=eta,\n    n_ants=30,                     # <-- tăng số kiến\n    n_generations=40,              # <-- tăng số thế hệ\n    alpha=1.0,\n    beta=2.0,\n    evaporation_rate=0.2,\n    q=1.0,\n    min_features=min(5, n_features_total),\n    max_features=min(19, n_features_total),  # <-- cho phép chọn tối đa 19 đặc trưng (toàn bộ)\n    random_state=SEED,\n    penalty_weight=0.0005,          # <-- giảm phạt để giữ nhiều đặc trưng hơn\n)\naco.fit(X_aco_train, y_aco_train, X_aco_val, y_aco_val, verbose=True)\naco_time = time.time() - t0_aco   \nprint(f\"Thời gian chạy ACO: {aco_time:.1f}s\")\n\nselected_indices = aco.best_subset\nselected_feature_names = [candidate_feature_names[i] for i in selected_indices]\n\nprint(f\"\\n>>> ACO đã chọn {len(selected_indices)} đặc trưng tốt nhất (fitness = {aco.best_fitness:.4f}):\")\nprint(selected_feature_names)\n\n# ---- Trực quan hoá quá trình hội tụ của ACO ----\nplt.figure(figsize=(8, 4))\nplt.plot(aco.history_best_fitness, label='Fitness tốt nhất (tích lũy)', linewidth=2)\nplt.plot(aco.history_avg_fitness, label='Fitness trung bình mỗi thế hệ', linestyle='--')\nplt.xlabel('Thế hệ (Generation)')\nplt.ylabel('Fitness (AUC - penalty)')\nplt.title('Quá trình hội tụ của thuật toán ACO')\nplt.legend()\nplt.tight_layout()\nplt.savefig('aco_convergence.png', dpi=120)\nplt.show()\n\n# ---- Tập dữ liệu cuối cùng đưa vào CNN: chỉ gồm các đặc trưng ACO chọn ----\nX_selected = X_candidates[:, selected_indices]\nprint(f\"\\nKích thước dữ liệu cuối cùng đưa vào CNN: {X_selected.shape}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.4. Chuẩn bị dữ liệu cho CNN sau ACO","metadata":{}},{"cell_type":"code","source":"\"\"\"\nCHUẨN BỊ DỮ LIỆU SAU ACO CHO MẠNG CNN 1D\n-------------------------------------------\nTập đặc trưng X_selected (do ACO chọn ra từ không gian latent của Autoencoder + đặc\ntrưng gốc) được reshape về dạng (samples, steps, channels=1) để đưa vào Conv1D.\n\"\"\"\n\nn_selected = X_selected.shape[1]\nprint(f\"Số đặc trưng sau ACO đưa vào CNN: {n_selected}\")\nprint(f\"Danh sách: {selected_feature_names}\")\n\nX_train, X_temp, y_train, y_temp = train_test_split(\n    X_selected, y, test_size=0.30, random_state=SEED, stratify=y\n)\nX_val, X_test, y_val, y_test = train_test_split(\n    X_temp, y_temp, test_size=0.50, random_state=SEED, stratify=y_temp\n)\n\nprint(f\"Train: {X_train.shape}, Val: {X_val.shape}, Test: {X_test.shape}\")\nprint(f\"Tỉ lệ fraud - train: {y_train.mean()*100:.3f}% | val: {y_val.mean()*100:.3f}% | \"\n      f\"test: {y_test.mean()*100:.3f}%\")\n\nX_train_cnn = X_train.reshape(X_train.shape[0], n_selected, 1)\nX_val_cnn = X_val.reshape(X_val.shape[0], n_selected, 1)\nX_test_cnn = X_test.reshape(X_test.shape[0], n_selected, 1)\n\nclass_weights_arr = compute_class_weight('balanced', classes=np.array([0, 1]), y=y_train)\nclass_weight_dict = {0: class_weights_arr[0], 1: class_weights_arr[1]}\nprint(f\"\\nClass weights (xử lý mất cân bằng 1:10): {class_weight_dict}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.5. Kiến trúc CNN 1D Residual (mô hình đề xuất)","metadata":{}},{"cell_type":"code","source":"\"\"\"\nĐỀ XUẤT CẢI TIẾN #2: KIẾN TRÚC CNN 1D NÂNG CẤP VỚI RESIDUAL CONNECTION\n----------------------------------------------------------------------------\n...\n\"\"\"\n\ndef residual_block_1d(x, filters, kernel_size=3, dropout=0.0):\n    shortcut = x\n    # Nếu số kênh không khớp, dùng Conv1D 1x1 để chiếu shortcut về đúng chiều\n    if shortcut.shape[-1] != filters:\n        shortcut = layers.Conv1D(filters, 1, padding='same')(shortcut)\n\n    x = layers.Conv1D(filters, kernel_size, padding='same')(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Activation('relu')(x)\n\n    x = layers.Conv1D(filters, kernel_size, padding='same')(x)\n    x = layers.BatchNormalization()(x)\n\n    x = layers.Add()([x, shortcut])\n    x = layers.Activation('relu')(x)\n    if dropout > 0:\n        x = layers.Dropout(dropout)(x)\n    return x\n\n\ndef build_cnn1d_residual(input_len, dropout=0.2):  \n    inputs = layers.Input(shape=(input_len, 1))\n\n    # Stem: tăng số filter từ 32 lên 64\n    x = layers.Conv1D(64, 3, padding='same')(inputs)\n    x = layers.BatchNormalization()(x)\n    x = layers.Activation('relu')(x)\n\n    # Residual block 1: filters từ 64 lên 128\n    x = residual_block_1d(x, filters=128, kernel_size=3, dropout=dropout)\n    x = layers.MaxPooling1D(pool_size=2)(x)\n\n    # Residual block 2: filters=128 (giữ nguyên)\n    x = residual_block_1d(x, filters=128, kernel_size=3, dropout=dropout)\n    x = layers.MaxPooling1D(pool_size=2)(x)   # <-- thêm max pooling thứ hai\n\n    # Residual block 3: filters=256 (thêm block mới)\n    x = residual_block_1d(x, filters=256, kernel_size=3, dropout=dropout)\n\n    x = layers.GlobalAveragePooling1D()(x)\n    x = layers.Dense(128, activation='relu')(x)  # <-- tăng từ 64 lên 128\n    x = layers.Dropout(dropout)(x)\n    outputs = layers.Dense(1, activation='sigmoid')(x)\n\n    model = models.Model(inputs, outputs, name=\"CNN1D_Residual_ClickFraud\")\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n        loss='binary_crossentropy',\n        metrics=['accuracy', tf.keras.metrics.AUC(name='auc'),\n                 tf.keras.metrics.Precision(name='precision'),\n                 tf.keras.metrics.Recall(name='recall')]\n    )\n    return model\n\n\ncnn_model = build_cnn1d_residual(input_len=n_selected)\ncnn_model.summary()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.6. Huấn luyện mô hình đề xuất","metadata":{}},{"cell_type":"code","source":"\"\"\"\nHUẤN LUYỆN MÔ HÌNH CNN-ACO\n----------------------------\n- EarlyStopping: dừng sớm khi val_auc không cải thiện sau `patience` epoch, tự động\n  khôi phục lại trọng số tốt nhất (restore_best_weights=True).\n- ReduceLROnPlateau: giảm learning rate khi val_loss chững lại, giúp mô hình \"tinh\n  chỉnh\" quanh điểm cực tiểu cục bộ tốt hơn ở giai đoạn cuối.\n- class_weight: bù đắp mất cân bằng lớp (tỉ lệ âm:dương = 10:1) ngay trong hàm loss,\n  thay vì phải resample lại dữ liệu.\n\"\"\"\n\nearly_stop = callbacks.EarlyStopping(\n    monitor='val_auc', mode='max', patience=6, restore_best_weights=True, verbose=1  # <-- giảm patience từ 8 xuống 6\n)\nreduce_lr = callbacks.ReduceLROnPlateau(\n    monitor='val_loss', mode='min', factor=0.5, patience=3, min_lr=1e-6, verbose=1  # <-- giảm patience từ 4 xuống 3\n)\n\nt0 = time.time()\nhistory = cnn_model.fit(\n    X_train_cnn, y_train,\n    validation_data=(X_val_cnn, y_val),\n    epochs=60,\n    batch_size=512,\n    class_weight=class_weight_dict,\n    callbacks=[early_stop, reduce_lr],\n    verbose=2\n)\ncnn_train_time = time.time() - t0\nprint(f\"Thời gian huấn luyện CNN+ACO: {cnn_train_time:.1f}s\")\n\n# ---- Vẽ biểu đồ quá trình huấn luyện ----\nfig, axes = plt.subplots(1, 3, figsize=(16, 4))\n\naxes[0].plot(history.history['loss'], label='Train Loss')\naxes[0].plot(history.history['val_loss'], label='Val Loss')\naxes[0].set_title('Hàm mất mát (Loss) qua các epoch')\naxes[0].set_xlabel('Epoch'); axes[0].legend()\n\naxes[1].plot(history.history['auc'], label='Train AUC')\naxes[1].plot(history.history['val_auc'], label='Val AUC')\naxes[1].set_title('AUC-ROC qua các epoch')\naxes[1].set_xlabel('Epoch'); axes[1].legend()\n\naxes[2].plot(history.history['precision'], label='Train Precision')\naxes[2].plot(history.history['val_precision'], label='Val Precision')\naxes[2].plot(history.history['recall'], label='Train Recall', linestyle='--')\naxes[2].plot(history.history['val_recall'], label='Val Recall', linestyle='--')\naxes[2].set_title('Precision / Recall qua các epoch')\naxes[2].set_xlabel('Epoch'); axes[2].legend()\n\nplt.tight_layout()\nplt.savefig('training_history.png', dpi=120)\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## BƯỚC 4 — Đánh giá\n\nĐánh giá mô hình đề xuất trên tập test, sau đó so sánh toàn diện với:\n- Mô hình hiện có (LightGBM, ANN) đã chạy ở Bước 2.\n- Mô hình ablation: CNN Residual + Autoencoder nhưng **không qua ACO** — để tách\n  riêng và đo đúng phần đóng góp của thuật toán bầy kiến.\n","metadata":{}},{"cell_type":"markdown","source":"### 4.1. Đánh giá mô hình đề xuất trên tập Test","metadata":{}},{"cell_type":"code","source":"\"\"\"\nĐÁNH GIÁ MÔ HÌNH CNN-ACO TRÊN TẬP TEST (DỮ LIỆU CHƯA TỪNG THẤY)\n------------------------------------------------------------------\nVì đây là bài toán mất cân bằng lớp (imbalanced classification) ứng dụng phát hiện\ngian lận, ACCURACY KHÔNG PHẢI là thước đo phù hợp (một mô hình dự đoán \"không fraud\"\ncho mọi click vẫn đạt accuracy ~91% với tỉ lệ 1:10 nhưng vô dụng trong thực tế).\n\nCác chỉ số quan trọng hơn:\n    - AUC-ROC: khả năng phân biệt tổng thể giữa 2 lớp, không phụ thuộc ngưỡng quyết định.\n    - Precision (lớp fraud): trong các click bị mô hình gắn cờ \"fraud\", bao nhiêu % là\n      đúng thật -> liên quan trực tiếp đến việc tránh chặn nhầm khách hàng thật (false\n      positive gây mất doanh thu quảng cáo oan).\n    - Recall (lớp fraud): trong các click fraud thật, mô hình bắt được bao nhiêu % ->\n      liên quan đến hiệu quả chống click tặc thực tế.\n    - F1-score: trung bình điều hoà Precision & Recall.\n    - Confusion Matrix: minh họa trực quan 4 trường hợp (TP, FP, FN, TN).\n\"\"\"\n\ny_test_prob = cnn_model.predict(X_test_cnn, batch_size=1024).ravel()\ny_test_pred = (y_test_prob >= 0.5).astype(int)\n\ntest_auc = roc_auc_score(y_test, y_test_prob)\ntest_precision = precision_score(y_test, y_test_pred)\ntest_recall = recall_score(y_test, y_test_pred)\ntest_f1 = f1_score(y_test, y_test_pred)\n\nprint(\"=\" * 55)\nprint(\"KẾT QUẢ MÔ HÌNH CNN 1D + ACO (PCA->ACO->CNN) TRÊN TEST SET\")\nprint(\"=\" * 55)\nprint(f\"AUC-ROC   : {test_auc:.4f}\")\nprint(f\"Precision : {test_precision:.4f}\")\nprint(f\"Recall    : {test_recall:.4f}\")\nprint(f\"F1-score  : {test_f1:.4f}\")\nprint(\"\\nBáo cáo phân loại chi tiết:\")\nprint(classification_report(y_test, y_test_pred, target_names=['Click thật (0)', 'Click Fraud (1)']))\n\n# ---- Ma trận nhầm lẫn ----\ncm = confusion_matrix(y_test, y_test_pred)\nplt.figure(figsize=(5, 4))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=['Click thật', 'Click Fraud'],\n            yticklabels=['Click thật', 'Click Fraud'])\nplt.xlabel('Dự đoán'); plt.ylabel('Thực tế')\nplt.title('Ma trận nhầm lẫn (Confusion Matrix) - CNN+ACO')\nplt.tight_layout()\nplt.savefig('confusion_matrix_cnn_aco.png', dpi=120)\nplt.show()\n\n# ---- Đường ROC ----\nfpr, tpr, _ = roc_curve(y_test, y_test_prob)\nplt.figure(figsize=(5, 5))\nplt.plot(fpr, tpr, label=f'CNN+ACO (AUC = {test_auc:.4f})', linewidth=2)\nplt.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Random guess')\nplt.xlabel('False Positive Rate'); plt.ylabel('True Positive Rate')\nplt.title('Đường ROC - CNN+ACO')\nplt.legend()\nplt.tight_layout()\nplt.savefig('roc_curve_cnn_aco.png', dpi=120)\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4.2. Ablation Study — CNN + Autoencoder KHÔNG qua ACO","metadata":{}},{"cell_type":"code","source":"\"\"\"\nABLATION STUDY: CNN 1D (RESIDUAL) + AUTOENCODER NHƯNG KHÔNG QUA ACO\n------------------------------------------------------------------------\nĐể đo lường ĐÚNG mức độ đóng góp của riêng bước ACO (tách biệt khỏi đóng góp của\nAutoencoder và kiến trúc Residual CNN), ta huấn luyện thêm một mô hình dùng TOÀN BỘ\nkhông gian đặc trưng ứng viên X_candidates (latent features của Autoencoder + đặc\ntrưng gốc có ý nghĩa) — KHÔNG đi qua bước chọn lọc của ACO — rồi đưa thẳng vào cùng\nkiến trúc CNN Residual.\n\"\"\"\n\nX_train_noaco, X_temp_noaco, y_train_noaco, y_temp_noaco = train_test_split(\n    X_candidates, y, test_size=0.30, random_state=SEED, stratify=y\n)\nX_val_noaco, X_test_noaco, y_val_noaco, y_test_noaco = train_test_split(\n    X_temp_noaco, y_temp_noaco, test_size=0.50, random_state=SEED, stratify=y_temp_noaco\n)\n\nn_candidates = X_candidates.shape[1]\nX_train_noaco_cnn = X_train_noaco.reshape(-1, n_candidates, 1)\nX_val_noaco_cnn = X_val_noaco.reshape(-1, n_candidates, 1)\nX_test_noaco_cnn = X_test_noaco.reshape(-1, n_candidates, 1)\n\ncw_arr_noaco = compute_class_weight('balanced', classes=np.array([0, 1]), y=y_train_noaco)\ncw_dict_noaco = {0: cw_arr_noaco[0], 1: cw_arr_noaco[1]}\n\nt0 = time.time()\ncnn_no_aco = build_cnn1d_residual(input_len=n_candidates)\ncnn_no_aco.fit(\n    X_train_noaco_cnn, y_train_noaco,\n    validation_data=(X_val_noaco_cnn, y_val_noaco),\n    epochs=40,\n    batch_size=512,\n    class_weight=cw_dict_noaco,\n    callbacks=[callbacks.EarlyStopping(monitor='val_auc', mode='max',\n                                        patience=6, restore_best_weights=True)],\n    verbose=0,\n)\ncnn_no_aco_time = time.time() - t0\n\nprob_no_aco = cnn_no_aco.predict(X_test_noaco_cnn, batch_size=1024).ravel()\npred_no_aco = (prob_no_aco >= 0.5).astype(int)\n\nprint(f\"Thời gian huấn luyện CNN+Autoencoder (KHÔNG ACO): {cnn_no_aco_time:.1f}s\")\nprint(f\"Số đặc trưng đầu vào: {n_candidates}\")\nprint(f\"AUC-ROC   : {roc_auc_score(y_test_noaco, prob_no_aco):.4f}\")\nprint(f\"Accuracy  : {accuracy_score(y_test_noaco, pred_no_aco):.4f}\")\nprint(f\"Precision : {precision_score(y_test_noaco, pred_no_aco):.4f}\")\nprint(f\"Recall    : {recall_score(y_test_noaco, pred_no_aco):.4f}\")\nprint(f\"F1-score  : {f1_score(y_test_noaco, pred_no_aco):.4f}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4.3. Bảng so sánh tổng hợp cuối cùng","metadata":{}},{"cell_type":"code","source":"\"\"\"\nBƯỚC 4 - ĐÁNH GIÁ: TỔNG HỢP SO SÁNH HIỆU SUẤT GIỮA CÁC MÔ HÌNH\n--------------------------------------------------------------------\nSo sánh toàn diện giữa:\n    1. LightGBM            (mô hình hiện có #1, toàn bộ đặc trưng gốc)\n    2. ANN cơ bản (MLP)    (mô hình hiện có #2, toàn bộ đặc trưng gốc)\n    3. CNN Residual + Autoencoder, KHÔNG qua ACO  (ablation - đo phần đóng góp của ACO)\n    4. CNN Residual + Autoencoder + ACO           (MÔ HÌNH ĐỀ XUẤT)\n\ntheo các chỉ số: Accuracy, Precision, Recall, F1-score, AUC-ROC, số đặc trưng dùng,\nvà thời gian huấn luyện.\n\"\"\"\n\nresults_compare = [\n    {\n        'Mô hình': 'LightGBM (mô hình hiện có)',\n        'Số đặc trưng': X_train_base.shape[1],\n        'Accuracy': accuracy_score(y_test_base, lgb_pred),\n        'Precision': precision_score(y_test_base, lgb_pred),\n        'Recall': recall_score(y_test_base, lgb_pred),\n        'F1-score': f1_score(y_test_base, lgb_pred),\n        'AUC-ROC': roc_auc_score(y_test_base, lgb_prob),\n        'Thời gian train (s)': round(lgb_train_time, 1),\n    },\n    {\n        'Mô hình': 'ANN cơ bản - MLP (mô hình hiện có)',\n        'Số đặc trưng': X_train_base.shape[1],\n        'Accuracy': accuracy_score(y_test_base, ann_pred),\n        'Precision': precision_score(y_test_base, ann_pred),\n        'Recall': recall_score(y_test_base, ann_pred),\n        'F1-score': f1_score(y_test_base, ann_pred),\n        'AUC-ROC': roc_auc_score(y_test_base, ann_prob),\n        'Thời gian train (s)': round(ann_train_time, 1),\n    },\n    {\n        'Mô hình': 'CNN Residual + Autoencoder (không ACO)',\n        'Số đặc trưng': n_candidates,\n        'Accuracy': accuracy_score(y_test_noaco, pred_no_aco),\n        'Precision': precision_score(y_test_noaco, pred_no_aco),\n        'Recall': recall_score(y_test_noaco, pred_no_aco),\n        'F1-score': f1_score(y_test_noaco, pred_no_aco),\n        'AUC-ROC': roc_auc_score(y_test_noaco, prob_no_aco),\n        'Thời gian train (s)': round(cnn_no_aco_time, 1),\n    },\n    {\n        'Mô hình': 'CNN Residual + Autoencoder + ACO (Đề xuất)',\n        'Số đặc trưng': n_selected,\n        'Accuracy': accuracy_score(y_test, y_test_pred),\n        'Precision': test_precision,\n        'Recall': test_recall,\n        'F1-score': test_f1,\n        'AUC-ROC': test_auc,\n        'Thời gian train (s)': round(aco_time + cnn_train_time, 1),\n    },\n]\n\ndf_compare = pd.DataFrame(results_compare)\npd.set_option('display.float_format', lambda x: f'{x:.4f}' if isinstance(x, float) else str(x))\n# ---------- IN BẢNG SO SÁNH ĐỊNH DẠNG ĐẸP ----------\ndf_compare_display = df_compare[['Mô hình', 'Số đặc trưng', 'Accuracy', 'Precision', \n                                 'Recall', 'F1-score', 'AUC-ROC', 'Thời gian train (s)']]\n\npd.set_option('display.float_format', '{:.4f}'.format)\npd.set_option('display.max_colwidth', 30)\npd.set_option('display.width', 120)\n\nprint(\"\\n\" + \"=\"*130)\nprint(\"BẢNG SO SÁNH HIỆU SUẤT CÁC MÔ HÌNH\".center(130))\nprint(\"=\"*130)\nprint(df_compare_display.to_string(index=False, justify='center'))\nprint(\"=\"*130)\n\n# ---- Biểu đồ so sánh trực quan ----\nmetrics_to_plot = ['Accuracy', 'Precision', 'Recall', 'F1-score', 'AUC-ROC']\nfig, ax = plt.subplots(figsize=(12, 6))\nx_pos = np.arange(len(df_compare))\nwidth = 0.15\ncolors = ['#4C72B0', '#DD8452', '#55A868', '#C44E52', '#8172B2']\n\nfor idx, metric in enumerate(metrics_to_plot):\n    ax.bar(x_pos + (idx - 2) * width, df_compare[metric], width, label=metric, color=colors[idx])\n\nax.set_xticks(x_pos)\nax.set_xticklabels(df_compare['Mô hình'], rotation=15, ha='right')\nax.set_ylabel('Điểm số')\nax.set_title('So sánh hiệu suất: Mô hình hiện có vs Mô hình đề xuất (CNN+Autoencoder+ACO)')\nax.legend(loc='lower right')\nax.set_ylim(0, 1.05)\nplt.tight_layout()\nplt.savefig('final_model_comparison.png', dpi=120)\nplt.show()\n\n# ---- Biểu đồ so sánh số đặc trưng sử dụng (minh họa hiệu quả nén của Autoencoder + ACO) ----\nplt.figure(figsize=(7, 4))\nplt.bar(df_compare['Mô hình'], df_compare['Số đặc trưng'], color='#64B5CD')\nplt.xticks(rotation=15, ha='right')\nplt.ylabel('Số đặc trưng đầu vào')\nplt.title('So sánh số đặc trưng sử dụng giữa các mô hình')\nplt.tight_layout()\nplt.savefig('feature_count_comparison.png', dpi=120)\nplt.show()\n\n# ============================================================\n#  RADAR CHART \n# ============================================================\n\nimport matplotlib.patches as mpatches\nimport numpy as _np\n\n# ── 1. SỐ LIỆU THẬT TỪ CÁC BIẾN ĐÃ TÍNH TRONG BÀI ──────────\n#  (các biến này đã tồn tại sau khi chạy đủ Bước 2, 3, 4)\nradar_data = {\n    \"LightGBM\\n(hiện có)\": [\n        accuracy_score(y_test_base,  lgb_pred),  \n        precision_score(y_test_base, lgb_pred),   \n        recall_score(y_test_base,    lgb_pred),   \n        f1_score(y_test_base,        lgb_pred),   \n        roc_auc_score(y_test_base,   lgb_prob),   \n    ],\n    \"ANN/MLP\\n(hiện có)\": [\n        accuracy_score(y_test_base,  ann_pred),   \n        precision_score(y_test_base, ann_pred),  \n        recall_score(y_test_base,    ann_pred),   \n        f1_score(y_test_base,        ann_pred),   \n        roc_auc_score(y_test_base,   ann_prob),   \n    ],\n    \"CNN+AE\\n(không ACO)\": [\n        accuracy_score(y_test_noaco,  pred_no_aco),  \n        precision_score(y_test_noaco, pred_no_aco),  \n        recall_score(y_test_noaco,    pred_no_aco),  \n        f1_score(y_test_noaco,        pred_no_aco),  \n        roc_auc_score(y_test_noaco,   prob_no_aco),  \n    ],\n    \"CNN+AE+ACO\\n(Đề xuất)\": [\n        accuracy_score(y_test, y_test_pred),  \n        test_precision,                       \n        test_recall,                          \n        test_f1,                               \n        test_auc,                              \n    ],\n}\n\ncategories = [\"Accuracy\", \"Precision\", \"Recall\", \"F1-score\", \"AUC-ROC\"]\n\n# ── 2. STYLE ─────────────────────────────────────────────────\n#  Màu phân biệt rõ, line style khác nhau → dễ đọc khi in B&W\nCOLORS      = [\"#2196F3\", \"#F44336\", \"#FF9800\", \"#4CAF50\"]\nALPHAS      = [ 0.10,      0.10,      0.10,      0.18    ]\nLINE_WIDTHS = [ 1.6,       1.6,       1.6,       2.5     ]\nMARKERS     = [\"o\",       \"s\",       \"^\",       \"D\"      ]\nMARKER_SIZES= [ 7,         7,         7,         9       ]\nLINE_STYLES = [\"--\",      \"-.\",       \":\",       \"-\"     ]\n\nplt.rcParams.update({\n    \"font.family\": \"DejaVu Sans\",\n    \"font.size\":   11,\n    \"figure.dpi\":  150,\n    \"text.color\":  \"#1a1a2e\",\n})\n\n# ── 3. GÓC TRỤC ──────────────────────────────────────────────\nN      = len(categories)\nangles = _np.linspace(0, 2 * _np.pi, N, endpoint=False).tolist()\nangles += angles[:1]   # đóng vòng\n\n# ── 4. GIỚI HẠN TRỤC R — tự động, không hardcode ────────────\nall_vals = [v for vals in radar_data.values() for v in vals]\n_pad  = 0.02\nY_MIN = max(0.0,  round(min(all_vals) - _pad * 3, 2))   # ~0.67\nY_MAX = min(1.00, round(max(all_vals) + _pad,     2))   # ~0.98\n\n# ── 5. FIGURE ────────────────────────────────────────────────\nfig = plt.figure(figsize=(8.5, 8.5), facecolor=\"white\")\nax  = fig.add_subplot(111, polar=True)\nax.set_facecolor(\"#f9f9f9\")\n\n# ── 6. LƯỚI & NHÃN ───────────────────────────────────────────\nr_ticks = _np.linspace(Y_MIN, Y_MAX, num=5)\nax.set_ylim(Y_MIN, Y_MAX)\nax.set_yticks(r_ticks)\nax.set_yticklabels([f\"{v:.2f}\" for v in r_ticks], fontsize=8, color=\"#666666\")\nax.yaxis.set_tick_params(pad=7)\nax.yaxis.grid(True, linestyle=\"--\", linewidth=0.6, color=\"#bbbbbb\", alpha=0.9)\nax.xaxis.grid(True, linestyle=\"-\",  linewidth=0.7, color=\"#cccccc\", alpha=0.8)\n\nax.set_xticks(angles[:-1])\nax.set_xticklabels(categories, fontsize=12, fontweight=\"bold\", color=\"#1a1a2e\")\nax.tick_params(axis='x', pad=16)\nax.set_theta_offset(_np.pi / 2)\nax.set_theta_direction(-1)\n\n# ── 7. VẼ TỪNG MÔ HÌNH ───────────────────────────────────────\nlegend_handles = []\n\nfor idx, (model_name, values) in enumerate(radar_data.items()):\n    v     = values + values[:1]\n    color = COLORS[idx]\n\n    # Đường & marker\n    ax.plot(\n        angles, v,\n        color=color, linewidth=LINE_WIDTHS[idx],\n        linestyle=LINE_STYLES[idx],\n        marker=MARKERS[idx], markersize=MARKER_SIZES[idx],\n        markerfacecolor=\"white\", markeredgecolor=color,\n        markeredgewidth=2.0, zorder=4,\n    )\n\n    # Vùng tô\n    ax.fill(angles, v, color=color, alpha=ALPHAS[idx], zorder=3)\n\n    # Giá trị số — offset hướng ra ngoài\n    r_range = Y_MAX - Y_MIN\n    for angle, val in zip(angles[:-1], values):\n        ax.annotate(\n            f\"{val:.4f}\",\n            xy=(angle, val),\n            xytext=(angle, val + r_range * 0.032),\n            fontsize=7.2, fontweight=\"bold\", color=color,\n            ha=\"center\", va=\"center\", zorder=6,\n            bbox=dict(boxstyle=\"round,pad=0.18\",\n                      fc=\"white\", ec=color,\n                      linewidth=0.8, alpha=0.90),\n        )\n\n    legend_handles.append(mpatches.Patch(\n        facecolor=color, alpha=0.55,\n        edgecolor=color, linewidth=1.5,\n        label=model_name.replace(\"\\n\", \" \"),\n    ))\n\n# ── 8. TIÊU ĐỀ ───────────────────────────────────────────────\nax.set_title(\n    \"Model Performance Comparison\\nAd Click Fraud Detection — TalkingData\",\n    fontsize=14, fontweight=\"bold\", color=\"#1a1a2e\",\n    pad=32, linespacing=1.65,\n)\n\n# ── 9. LEGEND ────────────────────────────────────────────────\nlegend = ax.legend(\n    handles=legend_handles,\n    loc=\"upper right\",\n    bbox_to_anchor=(1.35, 1.17),\n    frameon=True, framealpha=0.94,\n    edgecolor=\"#cccccc\", fancybox=True,\n    fontsize=9.5,\n    title=\"Models\", title_fontsize=10,\n)\nlegend.get_title().set_fontweight(\"bold\")\n\n# ── 10. CHÚ THÍCH AUC ──\nfig.text(\n    0.5, 0.058,\n    f\"* CNN+AE+ACO dẫn đầu về Accuracy ({accuracy_score(y_test, y_test_pred):.4f}), \"\n    f\"Precision ({test_precision:.4f}) và F1-score ({test_f1:.4f}).\\n\"\n    \"  AUC-ROC thấp hơn LightGBM do fitness hàm ACO dùng Logistic Regression, không phải CNN.\",\n    ha=\"center\", fontsize=8.2, color=\"#c0392b\",\n    style=\"italic\", linespacing=1.5,\n)\n# ── 11. CAPTION IEEE ─────────────────────────────────────────\nfig.text(\n    0.5, 0.018,\n    \"Fig. 4. Radar chart comparing five classification metrics across four models\\n\"\n    \"on the TalkingData AdTracking Fraud Detection dataset.\",\n    ha=\"center\", fontsize=8.5, color=\"#555555\",\n    style=\"italic\", linespacing=1.5,\n)\n\nplt.tight_layout(rect=[0, 0.10, 1, 1])\n\n# ── 12. LƯU ──────────────────────────────────────────────────\nplt.savefig(\"radar_chart_model_comparison.png\", dpi=300,\n            bbox_inches=\"tight\", facecolor=\"white\", format=\"png\")\nprint(\"✅ Đã lưu: radar_chart_model_comparison.png (300 DPI)\")\nplt.show()\n \n\nimprovement_auc = (test_auc - max(roc_auc_score(y_test_base, lgb_prob),\n                                   roc_auc_score(y_test_base, ann_prob))) * 100\nfeature_reduction = (1 - n_selected / X_train_base.shape[1]) * 100\n\nprint(f\"\"\"\n=========================== NHẬN XÉT ===========================\n- Mô hình đề xuất (CNN Residual + Autoencoder + ACO) dùng chỉ {n_selected} đặc trưng,\n  giảm {feature_reduction:.1f}% so với {X_train_base.shape[1]} đặc trưng gốc dùng bởi\n  LightGBM/ANN.\n- So với mô hình hiện có tốt nhất (AUC cao nhất giữa LightGBM/ANN), AUC-ROC của mô\n  hình đề xuất {'cao hơn' if improvement_auc > 0 else 'thấp hơn'} {abs(improvement_auc):.2f} điểm phần trăm.\n- So sánh CNN+Autoencoder CÓ ACO vs KHÔNG ACO cho thấy phần đóng góp riêng của bước\n  chọn đặc trưng bằng thuật toán bầy kiến.\n==================================================================\n\"\"\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4.4. Lưu mô hình","metadata":{}},{"cell_type":"code","source":"\"\"\"\nLƯU MÔ HÌNH & CÁC THÀNH PHẦN TIỀN XỬ LÝ\n-------------------------------------------\nLưu lại CNN cuối cùng, Autoencoder (encoder), scaler, và danh sách đặc trưng do ACO\nchọn — để đảm bảo pipeline tiền xử lý nhất quán khi suy luận trên dữ liệu mới.\n\"\"\"\n\nimport joblib\n\ncnn_model.save('cnn_residual_aco_click_fraud_model.keras')\nencoder.save('autoencoder_encoder.keras')\n\njoblib.dump(scaler, 'scaler.pkl')\njoblib.dump(selected_indices, 'aco_selected_indices.pkl')\njoblib.dump(candidate_feature_names, 'candidate_feature_names.pkl')\n\nprint(\"Đã lưu mô hình và các thành phần tiền xử lý:\")\nprint(\" - cnn_residual_aco_click_fraud_model.keras\")\nprint(\" - autoencoder_encoder.keras\")\nprint(\" - scaler.pkl\")\nprint(\" - aco_selected_indices.pkl\")\nprint(\" - candidate_feature_names.pkl\")\n\nprint(f\"\"\"\n=========================== TỔNG KẾT ===========================\nPIPELINE ĐỀ XUẤT:\n  Feature Engineering -> StandardScaler -> Autoencoder (giảm chiều phi tuyến,\n  thay cho PCA/SVD) -> ACO (chọn {n_selected}/{n_candidates} đặc trưng \"chí mạng\"\n  nhất) -> CNN 1D Residual (phân loại click thật / click fraud).\n\nKẾT QUẢ TRÊN TẬP TEST (mô hình đề xuất):\n    Accuracy  = {accuracy_score(y_test, y_test_pred):.4f}\n    Precision = {test_precision:.4f}\n    Recall    = {test_recall:.4f}\n    F1-score  = {test_f1:.4f}\n    AUC-ROC   = {test_auc:.4f}\n\nSo với mô hình hiện có (LightGBM, ANN cơ bản) và ablation (CNN+Autoencoder không\nACO), xem bảng so sánh đầy đủ ở Bước 4 - Đánh giá.\n==================================================================\n\"\"\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Kết luận và Hướng phát triển\n\n**Tóm tắt pipeline đề xuất:**\nFeature Engineering → StandardScaler → **Autoencoder** (giảm chiều phi tuyến, thay\nPCA/SVD) → **ACO** (chọn đặc trưng tối ưu) → **CNN 1D Residual** (phân loại click\nthật/fraud).\n\n**Ưu điểm:**\n- Autoencoder nắm bắt được quan hệ phi tuyến giữa các đặc trưng đếm/tần suất mà\n  PCA/SVD (tuyến tính) không làm được.\n- ACO tự động tìm tập con đặc trưng nhỏ nhưng hiệu quả từ không gian latent, giảm số\n  chiều đầu vào cho CNN, giảm nguy cơ overfitting và thời gian huấn luyện/suy luận —\n  quan trọng cho hệ thống cần phát hiện click tặc theo thời gian thực.\n- Residual Block giúp kiến trúc CNN linh hoạt hơn khi thêm/bớt lớp, giảm vanishing\n  gradient so với CNN tuần tự thông thường.\n\n**Hạn chế và hướng phát triển:**\n- Hàm fitness trong ACO hiện dùng Logistic Regression (mô hình nhẹ) để đánh giá\n  nhanh; có thể thử nghiệm các hàm fitness khác để kiểm tra độ nhạy của kết quả.\n- Có thể mở rộng ACO để đồng thời tối ưu siêu tham số của CNN (số filter, kernel\n  size, độ sâu Residual Block...), hoặc thử các thuật toán bầy đàn khác (PSO, SMO)\n  để đối chiếu.\n- Trong môi trường sản xuất thật (real-time bidding trên Shopee/Lazada), cần thêm cơ\n  chế cập nhật mô hình online vì hành vi bot thay đổi liên tục theo thời gian (concept drift).\n","metadata":{}}]}