update toàn bộ các mô hình

This commit is contained in:
Victor Phan
2026-03-04 20:21:32 +07:00
parent ab57415468
commit 0700ca66c3
8 changed files with 219 additions and 1994 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+613
View File
@@ -0,0 +1,613 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "17da4353",
"metadata": {},
"outputs": [],
"source": [
"# Import libraries for Element84 Earth Search\n",
"import numpy as np\n",
"import pandas as pd\n",
"import xarray as xr\n",
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
"\n",
"# Element84 Earth Search STAC\n",
"import pystac_client\n",
"from odc.stac import load\n",
"\n",
"# Machine Learning\n",
"import torch\n",
"import torch.nn as nn\n",
"import torch.optim as optim\n",
"from torch.utils.data import TensorDataset, DataLoader\n",
"from sklearn.metrics import classification_report, confusion_matrix, accuracy_score\n",
"from sklearn.model_selection import train_test_split\n",
"\n",
"print(\"✅ Import thành công\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9c063be3",
"metadata": {},
"outputs": [],
"source": [
"# Kết nối Element84 Earth Search (hosted trên AWS)\n",
"def connect_earth_search():\n",
" \"\"\"Kết nối đến Element84 Earth Search STAC API\"\"\"\n",
" catalog = pystac_client.Client.open(\n",
" \"https://earth-search.aws.element84.com/v1\"\n",
" )\n",
" return catalog\n",
"\n",
"catalog = connect_earth_search()\n",
"print(\"✅ Element84 Earth Search kết nối thành công\")\n",
"print(f\" API: earth-search.aws.element84.com\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "83784d01",
"metadata": {},
"outputs": [],
"source": [
"# Cấu hình vùng và thời gian\n",
"date_range = (\"2022-09-01\", \"2023-10-01\")\n",
"longtitude_range = (105.5, 106.4)\n",
"latitude_range = (9.2, 10.0)\n",
"\n",
"# Tạo bounding box\n",
"bbox = (longtitude_range[0], latitude_range[0], longtitude_range[1], latitude_range[1])\n",
"\n",
"# Query Sentinel-2 từ Element84\n",
"search = catalog.search(\n",
" collections=[\"sentinel-2-l2a\"],\n",
" bbox=bbox,\n",
" datetime=f\"{date_range[0]}/{date_range[1]}\",\n",
" query={\"eo:cloud_cover\": {\"lt\": 30}}\n",
")\n",
"\n",
"items = search.item_collection()\n",
"print(f\"✅ Tìm thấy {len(items)} scenes Sentinel-2\")\n",
"\n",
"# Load data với odc-stac\n",
"data_sen2 = load(\n",
" items,\n",
" bands=[\"red\", \"green\", \"blue\", \"nir\", \"swir16\", \"swir22\", \"scl\"],\n",
" bbox=bbox,\n",
" resolution=10,\n",
" chunks={\"time\": 1, \"x\": 2048, \"y\": 2048},\n",
" groupby=\"solar_day\"\n",
")\n",
"\n",
"print(f\"✅ Sentinel-2 raw: {data_sen2.dims}\")\n",
"print(f\" Variables: {list(data_sen2.data_vars)}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c3faed92",
"metadata": {},
"outputs": [],
"source": [
"# Tiền xử lý Sentinel-2: cloud mask + NDVI + resampling\n",
"def mask_clean(ds):\n",
" \"\"\"Cloud masking sử dụng SCL band (Scene Classification Layer)\"\"\"\n",
" if \"scl\" not in ds:\n",
" print(\"⚠️ Không có SCL band, bỏ qua cloud masking\")\n",
" return ds\n",
" \n",
" cloud_mask = (\n",
" (ds[\"scl\"] == 3) | # cloud shadow\n",
" (ds[\"scl\"] == 8) | # cloud medium probability\n",
" (ds[\"scl\"] == 9) | # cloud high probability\n",
" (ds[\"scl\"] == 10) # thin cirrus\n",
" )\n",
" \n",
" ds_masked = ds.where(~cloud_mask)\n",
" return ds_masked.drop_vars(\"scl\", errors=\"ignore\")\n",
"\n",
"def calculate_indices(ds, index=\"NDVI\", satellite_mission=\"s2\"):\n",
" \"\"\"Tính chỉ số NDVI cho Sentinel-2\"\"\"\n",
" if index == \"NDVI\":\n",
" ndvi = (ds[\"nir\"] - ds[\"red\"]) / (ds[\"nir\"] + ds[\"red\"] + 1e-8)\n",
" ds[\"NDVI\"] = ndvi\n",
" return ds\n",
"\n",
"def fill_nan(ds):\n",
" \"\"\"Fill NaN bằng interpolation theo thời gian\"\"\"\n",
" return ds.interpolate_na(dim=\"time\", method=\"linear\", fill_value=\"extrapolate\")\n",
"\n",
"# Áp dụng tiền xử lý\n",
"data_clean = mask_clean(data_sen2)\n",
"data_ndvi = calculate_indices(data_clean, index=\"NDVI\", satellite_mission=\"s2\")\n",
"data_fill = fill_nan(data_ndvi)\n",
"data_sen2_monthly = data_fill.resample(time=\"1MS\").mean().compute()\n",
"\n",
"print(f\"✅ S2 monthly shape: {data_sen2_monthly.dims}\")\n",
"print(f\" Variables: {list(data_sen2_monthly.data_vars)}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "569bfebb",
"metadata": {},
"outputs": [],
"source": [
"# Tải Sentinel-1 (SAR VV/VH) từ Element84\n",
"search_s1 = catalog.search(\n",
" collections=[\"sentinel-1-grd\"],\n",
" bbox=bbox,\n",
" datetime=f\"{date_range[0]}/{date_range[1]}\",\n",
" query={\n",
" \"sat:orbit_state\": {\"eq\": \"descending\"},\n",
" \"sar:product_type\": {\"eq\": \"GRD\"}\n",
" }\n",
")\n",
"\n",
"items_s1 = search_s1.item_collection()\n",
"print(f\"✅ Tìm thấy {len(items_s1)} scenes Sentinel-1\")\n",
"\n",
"if len(items_s1) > 0:\n",
" data_sen1 = load(\n",
" items_s1,\n",
" bands=[\"vv\", \"vh\"],\n",
" bbox=bbox,\n",
" resolution=10,\n",
" chunks={\"time\": 1, \"x\": 2048, \"y\": 2048},\n",
" groupby=\"solar_day\"\n",
" )\n",
" data_sen1_monthly = data_sen1.resample(time=\"1MS\").mean().compute()\n",
"else:\n",
" # Tạo dummy data nếu không có S1\n",
" print(\"⚠️ Không có Sentinel-1, tạo dummy data\")\n",
" data_sen1_monthly = xr.Dataset({\n",
" \"vv\": xr.DataArray(\n",
" np.zeros_like(data_sen2_monthly[\"red\"].values),\n",
" coords=data_sen2_monthly[\"red\"].coords,\n",
" dims=data_sen2_monthly[\"red\"].dims\n",
" ),\n",
" \"vh\": xr.DataArray(\n",
" np.zeros_like(data_sen2_monthly[\"red\"].values),\n",
" coords=data_sen2_monthly[\"red\"].coords,\n",
" dims=data_sen2_monthly[\"red\"].dims\n",
" )\n",
" })\n",
"\n",
"print(f\"✅ S1 monthly shape: {data_sen1_monthly.dims}\")\n",
"print(f\" Variables: {list(data_sen1_monthly.data_vars)}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ecc56c2f",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"# Ánh xạ nhãn lớp đất\n",
"label_mapping = {\n",
" \"Lua tom\": \"0\", \"Lua\": \"1\", \"CHN\": \"2\", \"CLN\": \"3\",\n",
" \"TS\": \"4\", \"Song\": \"5\", \"Dat xay dung\": \"6\", \"Rung\": \"7\",\n",
"}\n",
"\n",
"def load_train_data(csv_path=\"/media/x79/2A7D-FAA0/remote-sensing/train_data.csv\", label_mapping=None):\n",
" \"\"\"Load training points từ CSV\"\"\"\n",
" df = pd.read_csv(csv_path)\n",
" if label_mapping:\n",
" df[\"label\"] = df[\"class_name\"].map(label_mapping).astype(str)\n",
" return df\n",
"\n",
"def get_data_sen1_and_sen2(train_df, sen2_data, sen1_data):\n",
" \"\"\"Extract features từ S2 và S1 tại các điểm training\"\"\"\n",
" X_list = []\n",
" y_list = []\n",
" \n",
" for idx, row in train_df.iterrows():\n",
" try:\n",
" lon, lat = float(row[\"longitude\"]), float(row[\"latitude\"])\n",
" label = int(row[\"label\"])\n",
" \n",
" # Extract S2 features\n",
" s2_point = sen2_data.sel(x=lon, y=lat, method=\"nearest\")\n",
" s2_features = []\n",
" \n",
" for var in [\"red\", \"green\", \"blue\", \"nir\", \"swir16\", \"swir22\", \"NDVI\"]:\n",
" if var in s2_point:\n",
" vals = s2_point[var].values\n",
" if vals.size > 0:\n",
" s2_features.extend([\n",
" np.nanmean(vals), np.nanstd(vals),\n",
" np.nanmin(vals), np.nanmax(vals)\n",
" ])\n",
" else:\n",
" s2_features.extend([0, 0, 0, 0])\n",
" \n",
" # Extract S1 features\n",
" s1_point = sen1_data.sel(x=lon, y=lat, method=\"nearest\")\n",
" s1_features = []\n",
" \n",
" for var in [\"vv\", \"vh\"]:\n",
" if var in s1_point:\n",
" vals = s1_point[var].values\n",
" if vals.size > 0:\n",
" s1_features.extend([np.nanmean(vals), np.nanstd(vals)])\n",
" else:\n",
" s1_features.extend([0, 0])\n",
" \n",
" features = s2_features + s1_features\n",
" \n",
" if not np.isnan(features).any() and not np.isinf(features).any():\n",
" X_list.append(features)\n",
" y_list.append(label)\n",
" except Exception as e:\n",
" continue\n",
" \n",
" return X_list, y_list\n",
"\n",
"def split_train_data(X, y, test_size=0.2, val_size=0.1, random_state=42):\n",
" \"\"\"Split data thành train/val/test\"\"\"\n",
" X = np.array(X, dtype=np.float32)\n",
" y = np.array(y, dtype=np.int64)\n",
" \n",
" X_train, X_temp, y_train, y_temp = train_test_split(\n",
" X, y, test_size=test_size + val_size, random_state=random_state, stratify=y\n",
" )\n",
" \n",
" val_ratio = val_size / (test_size + val_size)\n",
" X_val, X_test, y_val, y_test = train_test_split(\n",
" X_temp, y_temp, test_size=(1 - val_ratio), random_state=random_state, stratify=y_temp\n",
" )\n",
" \n",
" return X_train, X_val, X_test, y_train, y_val, y_test\n",
"\n",
"train_data = load_train_data(label_mapping=label_mapping)\n",
"X, y = get_data_sen1_and_sen2(train_data, data_sen2_monthly, data_sen1_monthly)\n",
"X_train, X_val, X_test, y_train, y_val, y_test = split_train_data(X, y, test_size=0.2, val_size=0.1)\n",
"\n",
"X_train_np = np.array(X_train, dtype=np.float32)\n",
"X_val_np = np.array(X_val, dtype=np.float32)\n",
"X_test_np = np.array(X_test, dtype=np.float32)\n",
"y_train_np = np.array(y_train, dtype=np.int64)\n",
"y_val_np = np.array(y_val, dtype=np.int64)\n",
"y_test_np = np.array(y_test, dtype=np.int64)\n",
"\n",
"n_features = X_train_np.shape[1]\n",
"n_classes = len(np.unique(y_train_np))\n",
"\n",
"print(f\"✅ Train: {X_train_np.shape} Val: {X_val_np.shape} Test: {X_test_np.shape}\")\n",
"print(f\" n_features={n_features} n_classes={n_classes}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5492528b",
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"import torch.nn as nn\n",
"import torch.optim as optim\n",
"from torch.utils.data import TensorDataset, DataLoader\n",
"\n",
"# ── MobileNetV3 + LR-ASPP classifier ──────────────────────────────────────────\n",
"class MobileNetLRASPPClassifier(nn.Module):\n",
" \"\"\"\n",
" MobileNetV3-inspired backbone with LR-ASPP (Lite Reduced ASPP) head\n",
" for land-use classification on flat feature vectors.\n",
" \"\"\"\n",
" def __init__(self, n_features, n_classes):\n",
" super().__init__()\n",
" # Feature extraction backbone\n",
" self.feature_extractor = nn.Sequential(\n",
" nn.Linear(n_features, 128), nn.BatchNorm1d(128), nn.ReLU(inplace=True), nn.Dropout(0.2),\n",
" nn.Linear(128, 256), nn.BatchNorm1d(256), nn.ReLU(inplace=True), nn.Dropout(0.3),\n",
" nn.Linear(256, 512), nn.BatchNorm1d(512), nn.ReLU(inplace=True), nn.Dropout(0.3),\n",
" )\n",
" # LR-ASPP Branch 1: global pooling → 128\n",
" self.global_pool = nn.AdaptiveAvgPool1d(1)\n",
" self.global_conv = nn.Sequential(nn.Linear(512, 128), nn.ReLU(inplace=True))\n",
" # LR-ASPP Branch 2: direct 1×1 → 128\n",
" self.branch_conv = nn.Sequential(nn.Linear(512, 128), nn.BatchNorm1d(128), nn.ReLU(inplace=True))\n",
" # Fusion → n_classes\n",
" self.classifier = nn.Sequential(\n",
" nn.Linear(256, 128), nn.BatchNorm1d(128), nn.ReLU(inplace=True), nn.Dropout(0.4),\n",
" nn.Linear(128, n_classes),\n",
" )\n",
"\n",
" def forward(self, x):\n",
" feat = self.feature_extractor(x)\n",
" global_feat = self.global_pool(feat.unsqueeze(-1)).squeeze(-1)\n",
" global_feat = self.global_conv(global_feat)\n",
" branch_feat = self.branch_conv(feat)\n",
" fused = torch.cat([global_feat, branch_feat], dim=1)\n",
" return self.classifier(fused)\n",
"\n",
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"print(f\"✅ Device: {device}\")\n",
"model = MobileNetLRASPPClassifier(n_features, n_classes).to(device)\n",
"print(model)\n",
"total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
"print(f\" Trainable params: {total_params:,}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9da40f5a",
"metadata": {},
"outputs": [],
"source": [
"%%time\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ── Hyper-parameters ───────────────────────────────────────────────────────────\n",
"LEARNING_RATE = 1e-3\n",
"BATCH_SIZE = 64\n",
"N_EPOCHS = 60\n",
"PATIENCE = 10\n",
"CONV_THRESHOLD = 1e-4 # cải thiện val_loss < THRESHOLD → đánh dấu hội tụ\n",
"CONV_WINDOW = 3 # cần CONV_WINDOW bước liên tiếp thỏa mãn\n",
"\n",
"# ── Tensors & DataLoaders ──────────────────────────────────────────────────────\n",
"X_tr_t = torch.FloatTensor(X_train_np)\n",
"y_tr_t = torch.LongTensor(y_train_np)\n",
"X_va_t = torch.FloatTensor(X_val_np)\n",
"y_va_t = torch.LongTensor(y_val_np)\n",
"\n",
"train_loader = DataLoader(TensorDataset(X_tr_t, y_tr_t), batch_size=BATCH_SIZE, shuffle=True)\n",
"val_loader = DataLoader(TensorDataset(X_va_t, y_va_t), batch_size=BATCH_SIZE, shuffle=False)\n",
"\n",
"# ── Class-weighted loss ────────────────────────────────────────────────────────\n",
"class_counts = np.bincount(y_train_np)\n",
"class_weights = 1.0 / (class_counts + 1e-6)\n",
"class_weights = class_weights / class_weights.sum() * n_classes\n",
"criterion = nn.CrossEntropyLoss(weight=torch.FloatTensor(class_weights).to(device))\n",
"\n",
"optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=1e-4)\n",
"scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode=\"min\", factor=0.5, patience=5)\n",
"\n",
"# ── Training loop ──────────────────────────────────────────────────────────────\n",
"train_losses, val_losses, val_accs, lr_history = [], [], [], []\n",
"best_val_loss = float(\"inf\")\n",
"best_epoch = 1\n",
"best_state_dict = None\n",
"patience_counter = 0\n",
"convergence_epoch = None # ← điểm hội tụ sẽ được ghi lại ở đây\n",
"early_stop_epoch = None\n",
"\n",
"print(\"🚀 Training MobileNetV3 + LR-ASPP...\")\n",
"print(f\" conv_threshold={CONV_THRESHOLD} conv_window={CONV_WINDOW} patience={PATIENCE}\")\n",
"for epoch in range(1, N_EPOCHS + 1):\n",
" # --- Train ---\n",
" model.train()\n",
" epoch_loss = 0.0\n",
" for bx, by in train_loader:\n",
" bx, by = bx.to(device), by.to(device)\n",
" optimizer.zero_grad()\n",
" loss = criterion(model(bx), by)\n",
" loss.backward()\n",
" torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n",
" optimizer.step()\n",
" epoch_loss += loss.item()\n",
" avg_train_loss = epoch_loss / len(train_loader)\n",
"\n",
" # --- Validate ---\n",
" model.eval()\n",
" val_loss = 0.0; correct = 0; total = 0\n",
" with torch.no_grad():\n",
" for bx, by in val_loader:\n",
" bx, by = bx.to(device), by.to(device)\n",
" out = model(bx)\n",
" val_loss += criterion(out, by).item()\n",
" pred = out.argmax(1)\n",
" correct += (pred == by).sum().item()\n",
" total += by.size(0)\n",
" avg_val_loss = val_loss / len(val_loader)\n",
" val_acc = correct / total\n",
"\n",
" scheduler.step(avg_val_loss)\n",
" lr = optimizer.param_groups[0][\"lr\"]\n",
" lr_history.append(lr)\n",
"\n",
" train_losses.append(avg_train_loss)\n",
" val_losses.append(avg_val_loss)\n",
" val_accs.append(val_acc)\n",
"\n",
" # ── Phát hiện điểm hội tụ (sliding window trên val_loss) ──────────────\n",
" if convergence_epoch is None and epoch >= CONV_WINDOW + 1:\n",
" window = val_losses[-(CONV_WINDOW + 1):]\n",
" improvements = [abs(window[i] - window[i - 1]) for i in range(1, len(window))]\n",
" if all(imp < CONV_THRESHOLD for imp in improvements):\n",
" convergence_epoch = epoch - CONV_WINDOW + 1\n",
" print(f\" 📍 Hội tụ phát hiện tại epoch {convergence_epoch} \"\n",
" f\"(val_loss={val_losses[convergence_epoch-1]:.4f})\")\n",
"\n",
" if epoch % 5 == 0 or epoch == 1:\n",
" print(f\" Epoch {epoch:3d}/{N_EPOCHS} train={avg_train_loss:.4f} \"\n",
" f\"val={avg_val_loss:.4f} acc={val_acc:.4f} lr={lr:.2e}\")\n",
"\n",
" # ── Early stopping ─────────────────────────────────────────────────────\n",
" if avg_val_loss < best_val_loss:\n",
" best_val_loss = avg_val_loss\n",
" best_epoch = epoch\n",
" best_state_dict = {k: v.clone() for k, v in model.state_dict().items()}\n",
" patience_counter = 0\n",
" else:\n",
" patience_counter += 1\n",
" if patience_counter >= PATIENCE:\n",
" early_stop_epoch = epoch\n",
" print(f\"⏹ Early stopping tại epoch {epoch} (patience={PATIENCE})\")\n",
" break\n",
"\n",
"# Restore best weights\n",
"model.load_state_dict(best_state_dict)\n",
"total_epochs = len(train_losses)\n",
"print(f\"\\n✅ Training hoàn tất! Best epoch={best_epoch} Best val_loss={best_val_loss:.4f}\")\n",
"if convergence_epoch:\n",
" print(f\" Điểm hội tụ : epoch {convergence_epoch}\")\n",
"\n",
"# ═══════════════════════════════════════════════════════════════════════════════\n",
"# PHÂN TÍCH ĐIỂM HỘI TỤ — MobileNetV3 + LR-ASPP\n",
"# EMA-smoothed curves + annotated convergence / best / early-stop markers\n",
"# ═══════════════════════════════════════════════════════════════════════════════\n",
"epochs_axis = list(range(1, total_epochs + 1))\n",
"\n",
"# Exponential Moving Average smoothing\n",
"def ema(values, alpha=0.2):\n",
" s = [values[0]]\n",
" for v in values[1:]:\n",
" s.append(alpha * v + (1 - alpha) * s[-1])\n",
" return s\n",
"\n",
"val_losses_ema = ema(val_losses, alpha=0.3)\n",
"train_losses_ema = ema(train_losses, alpha=0.3)\n",
"val_accs_ema = ema(val_accs, alpha=0.3)\n",
"\n",
"# ── Tính ΔVal-loss per epoch ──────────────────────────────────────────────────\n",
"delta_val = [abs(val_losses_ema[i] - val_losses_ema[i-1])\n",
" for i in range(1, len(val_losses_ema))]\n",
"\n",
"fig, axes = plt.subplots(2, 2, figsize=(16, 10))\n",
"\n",
"# --- Top-left: Loss curves ---\n",
"ax = axes[0, 0]\n",
"ax.plot(epochs_axis, train_losses, \"b-\", alpha=0.25, linewidth=0.8)\n",
"ax.plot(epochs_axis, train_losses_ema, \"b-\", linewidth=2, label=\"Train loss (EMA)\")\n",
"ax.plot(epochs_axis, val_losses, \"g-\", alpha=0.25, linewidth=0.8)\n",
"ax.plot(epochs_axis, val_losses_ema, \"g-\", linewidth=2, label=\"Val loss (EMA)\")\n",
"ax.axvline(x=best_epoch, color=\"red\", linestyle=\"--\", linewidth=1.5,\n",
" label=f\"Best epoch={best_epoch}\")\n",
"if convergence_epoch:\n",
" ax.axvline(x=convergence_epoch, color=\"orange\", linestyle=\":\", linewidth=1.5,\n",
" label=f\"HỘI TỤ epoch={convergence_epoch}\")\n",
"if early_stop_epoch:\n",
" ax.axvline(x=early_stop_epoch, color=\"gray\", linestyle=\"-.\", linewidth=1.5,\n",
" label=f\"Early stop epoch={early_stop_epoch}\")\n",
"ax.set_xlabel(\"Epoch\")\n",
"ax.set_ylabel(\"Loss\")\n",
"ax.set_title(\"Loss Curves (raw + EMA)\")\n",
"ax.legend(fontsize=8)\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# --- Top-right: Val accuracy ---\n",
"ax = axes[0, 1]\n",
"ax.plot(epochs_axis, val_accs, \"g-\", alpha=0.3, linewidth=0.8)\n",
"ax.plot(epochs_axis, val_accs_ema, \"g-\", linewidth=2, label=\"Val acc (EMA)\")\n",
"ax.axvline(x=best_epoch, color=\"red\", linestyle=\"--\", linewidth=1.5,\n",
" label=f\"Best epoch={best_epoch} ({val_accs[best_epoch-1]*100:.2f}%)\")\n",
"if convergence_epoch:\n",
" ax.axvline(x=convergence_epoch, color=\"orange\", linestyle=\":\", linewidth=1.5,\n",
" label=f\"HỘI TỤ epoch={convergence_epoch} ({val_accs[convergence_epoch-1]*100:.2f}%)\")\n",
"if early_stop_epoch:\n",
" ax.axvline(x=early_stop_epoch, color=\"gray\", linestyle=\"-.\", linewidth=1.5,\n",
" label=f\"Early stop\")\n",
"ax.set_xlabel(\"Epoch\")\n",
"ax.set_ylabel(\"Accuracy\")\n",
"ax.set_title(\"Val Accuracy Curve\")\n",
"ax.legend(fontsize=8)\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# --- Bottom-left: ΔVal-loss (marginal improvement) ---\n",
"ax = axes[1, 0]\n",
"ax.bar(epochs_axis[1:], delta_val,\n",
" color=[\"green\" if d > CONV_THRESHOLD else \"salmon\" for d in delta_val],\n",
" alpha=0.75)\n",
"ax.axhline(y=CONV_THRESHOLD, color=\"red\", linestyle=\"--\",\n",
" label=f\"Threshold = {CONV_THRESHOLD:.0e}\")\n",
"if convergence_epoch:\n",
" ax.axvline(x=convergence_epoch, color=\"orange\", linestyle=\":\", linewidth=1.5,\n",
" label=f\"HỘI TỤ epoch={convergence_epoch}\")\n",
"ax.set_xlabel(\"Epoch\")\n",
"ax.set_ylabel(\"|ΔVal Loss|\")\n",
"ax.set_title(\"Marginal Val-Loss Improvement per Epoch\")\n",
"ax.legend(fontsize=8)\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"# --- Bottom-right: Learning rate schedule ---\n",
"ax = axes[1, 1]\n",
"ax.semilogy(epochs_axis, lr_history, \"purple\", linewidth=2)\n",
"if convergence_epoch:\n",
" ax.axvline(x=convergence_epoch, color=\"orange\", linestyle=\":\", linewidth=1.5,\n",
" label=f\"HỘI TỤ epoch={convergence_epoch}\")\n",
"ax.axvline(x=best_epoch, color=\"red\", linestyle=\"--\", linewidth=1.5,\n",
" label=f\"Best epoch={best_epoch}\")\n",
"ax.set_xlabel(\"Epoch\")\n",
"ax.set_ylabel(\"Learning Rate (log)\")\n",
"ax.set_title(\"Learning Rate Schedule (ReduceLROnPlateau)\")\n",
"ax.legend(fontsize=8)\n",
"ax.grid(True, alpha=0.3)\n",
"\n",
"plt.suptitle(\"MobileNetV3 + LR-ASPP — Convergence Analysis\", fontsize=13, fontweight=\"bold\")\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# ── Tổng kết ──────────────────────────────────────────────────────────────────\n",
"print(f\"\\n{'═'*60}\")\n",
"print(f\" Tổng số epoch : {total_epochs}\")\n",
"print(f\" Best epoch : {best_epoch} (val_loss={best_val_loss:.4f})\")\n",
"print(f\" Best val accuracy : {val_accs[best_epoch-1]*100:.4f}%\")\n",
"if convergence_epoch:\n",
" print(f\" Điểm HỘI TỤ : epoch {convergence_epoch} \"\n",
" f\"(val_acc={val_accs[convergence_epoch-1]*100:.2f}%)\")\n",
" wasted = total_epochs - convergence_epoch\n",
" print(f\" Epochs sau hội tụ : {wasted} \"\n",
" f\"(có thể giảm N_EPOCHS không ảnh hưởng nhiều đến kết quả)\")\n",
"if early_stop_epoch:\n",
" print(f\" Early stop tại epoch: {early_stop_epoch}\")\n",
"print(f\"{'═'*60}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bc690b92",
"metadata": {},
"outputs": [],
"source": [
"from sklearn.metrics import classification_report, confusion_matrix, accuracy_score\n",
"import seaborn as sns\n",
"\n",
"# ── Đánh giá trên tập test ─────────────────────────────────────────────────────\n",
"model.eval()\n",
"with torch.no_grad():\n",
" X_te_t = torch.FloatTensor(X_test_np).to(device)\n",
" logits = model(X_te_t)\n",
" y_pred = logits.argmax(1).cpu().numpy()\n",
"\n",
"acc = accuracy_score(y_test_np, y_pred)\n",
"print(f\"Test Accuracy : {acc:.4f} ({acc*100:.2f}%)\\n\")\n",
"print(classification_report(y_test_np, y_pred, digits=4))\n",
"\n",
"# ── Confusion matrix ────────────────────────────────────────────────────────────\n",
"class_names = list(label_mapping.keys())\n",
"cm = confusion_matrix(y_test_np, y_pred)\n",
"plt.figure(figsize=(9, 7))\n",
"sns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Purples\",\n",
" xticklabels=class_names, yticklabels=class_names)\n",
"plt.xlabel(\"Predicted\")\n",
"plt.ylabel(\"Actual\")\n",
"plt.title(\"Confusion Matrix — MobileNetV3 + LR-ASPP\")\n",
"plt.tight_layout()\n",
"plt.show()\n"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+353
View File
@@ -0,0 +1,353 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "3ab233c4",
"metadata": {},
"outputs": [],
"source": [
"%%time\n",
"%matplotlib inline\n",
"\n",
"import importlib\n",
"import new_import_ODC\n",
"\n",
"importlib.reload(new_import_ODC)\n",
"from new_import_ODC import *\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0af1f969",
"metadata": {},
"outputs": [],
"source": [
"%%time\n",
"# Cấu hình Dask + ODC + S3\n",
"cluster, client = notebook_utils.initialize_dask(use_gateway=True, workers=(1, 10))\n",
"dc = datacube.Datacube()\n",
"configure_s3_access(aws_unsigned=False, requester_pays=True, client=client)\n",
"client\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b0809939",
"metadata": {},
"outputs": [],
"source": [
"## cấu hình thời gian và tọa độ\n",
"date_range = (\"2022-09-01\", \"2023-10-01\")\n",
"longtitude_range = (105.5, 106.4)\n",
"latitude_range = (9.2, 10.0)\n",
"coordinates = (longtitude_range, latitude_range)\n",
"\n",
"## truy vấn ảnh Sentinel-2\n",
"data = load_data(dc, date_range, longtitude_range, latitude_range)\n",
"notebook_utils.heading(notebook_utils.xarray_object_size(data))\n",
"display(data)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b74030e3",
"metadata": {},
"outputs": [],
"source": [
"%%time\n",
"# Cloud masking + NDVI + fill nan + resample\n",
"result = mask_clean(data)\n",
"progress(result)\n",
"\n",
"ds1 = calculate_indices(result, index=\"NDVI\", satellite_mission=\"s2\")\n",
"ndvi = ds1[\"NDVI\"]\n",
"\n",
"time_split = [\n",
" slice(\"2022-09-01\", \"2023-01-01\"),\n",
" slice(\"2023-01-01\", \"2023-05-01\"),\n",
" slice(\"2023-05-01\", \"2023-07-01\"),\n",
" slice(\"2023-07-01\", \"2023-10-01\"),\n",
"]\n",
"fill_nan_ndvi = fill_nan(ndvi, time_split)\n",
"plt.imshow(fill_nan_ndvi.isel(time=6)); plt.title(\"NDVI (after fill)\"); plt.colorbar(); plt.show()\n",
"\n",
"average_ndvi = fill_nan_ndvi.resample(time=\"1M\").mean().persist()\n",
"progress(average_ndvi)\n",
"average_ndvi = average_ndvi.compute()\n",
"print(f\"NDVI monthly: {average_ndvi.shape}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "df61871b",
"metadata": {},
"outputs": [],
"source": [
"# Load Sentinel-1 (VH, VV)\n",
"dsvh, dsvv = load_data_sen1(dc, date_range, coordinates)\n",
"average_vv = calculate_average(dsvv, time_pattern=\"1M\")\n",
"average_vh = calculate_average(dsvh, time_pattern=\"1M\")\n",
"print(f\"VV: {average_vv.shape} VH: {average_vh.shape}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d0def6c0",
"metadata": {},
"outputs": [],
"source": [
"## Chuẩn bị dữ liệu train\n",
"train_path = \"train/ST_training_data_updated_1130points_new.shp\"\n",
"train = load_train_data(train_path)\n",
"train.head()\n",
"\n",
"label_mapping = {\n",
" \"Lua tom\": \"0\", \"Lua\": \"1\", \"CHN\": \"2\", \"CLN\": \"3\",\n",
" \"TS\": \"4\", \"Song\": \"5\", \"Dat xay dung\": \"6\", \"Rung\": \"7\",\n",
"}\n",
"\n",
"datasets = get_data_sen1_and_sen2(train, average_ndvi, average_vh, average_vv)\n",
"X_train, X_val, X_test, y_train, y_val, y_test = split_train_data(train, label_mapping, datasets)\n",
"\n",
"import numpy as np\n",
"X_train_np = np.asarray(X_train, dtype=np.float32)\n",
"X_val_np = np.asarray(X_val, dtype=np.float32)\n",
"X_test_np = np.asarray(X_test, dtype=np.float32)\n",
"y_train_np = np.asarray(y_train, dtype=np.int32)\n",
"y_val_np = np.asarray(y_val, dtype=np.int32)\n",
"y_test_np = np.asarray(y_test, dtype=np.int32)\n",
"\n",
"# Gộp train + val để tận dụng toàn bộ dữ liệu train\n",
"X_fit = np.concatenate([X_train_np, X_val_np], axis=0)\n",
"y_fit = np.concatenate([y_train_np, y_val_np], axis=0)\n",
"\n",
"print(f\"Train (fit): {X_fit.shape} Test: {X_test_np.shape} Classes: {len(label_mapping)}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2dc75f84",
"metadata": {},
"outputs": [],
"source": [
"%%time\n",
"from sklearn.ensemble import RandomForestClassifier\n",
"\n",
"# ── Xây dựng và train mô hình Random Forest ───────────────────────────────────\n",
"model = RandomForestClassifier(\n",
" n_estimators=200,\n",
" max_depth=30,\n",
" min_samples_leaf=2,\n",
" n_jobs=-1,\n",
" class_weight=\"balanced\", # xử lý mất cân bằng nhãn\n",
" random_state=42,\n",
")\n",
"\n",
"print(\"🚀 Training Random Forest...\")\n",
"print(f\" n_estimators = {model.n_estimators}\")\n",
"print(f\" max_depth = {model.max_depth}\")\n",
"print(f\" Train samples: {len(X_fit)}\")\n",
"\n",
"model.fit(X_fit, y_fit)\n",
"\n",
"val_acc = model.score(X_val_np, y_val_np)\n",
"print(f\"\\n✅ Training hoàn tất! Val accuracy: {val_acc:.4f} ({val_acc*100:.2f}%)\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "90588a5d",
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"from sklearn.ensemble import RandomForestClassifier\n",
"\n",
"# ═══════════════════════════════════════════════════════════════════════════════\n",
"# PHÂN TÍCH ĐIỂM HỘI TỤ — Random Forest\n",
"# Phương pháp: tăng dần n_estimators (warm_start) và theo dõi val accuracy\n",
"# Điểm hội tụ = lần đầu cải thiện val_acc < THRESHOLD trong WINDOW bước liên tiếp\n",
"# ═══════════════════════════════════════════════════════════════════════════════\n",
"N_RANGE = list(range(5, 205, 5)) # 5, 10, 15, ... 200\n",
"THRESHOLD = 0.0005 # cải thiện < 0.05% → coi là hội tụ\n",
"WINDOW = 3 # cần WINDOW bước liên tiếp dưới threshold\n",
"\n",
"conv_model = RandomForestClassifier(\n",
" max_depth=30, min_samples_leaf=2, n_jobs=-1,\n",
" class_weight=\"balanced\", random_state=42,\n",
" warm_start=True, # ← cho phép thêm cây mà không retrain lại\n",
")\n",
"\n",
"train_accs_c, val_accs_c = [], []\n",
"print(\"🔍 Phân tích hội tụ (warm_start)...\")\n",
"for n in N_RANGE:\n",
" conv_model.n_estimators = n\n",
" conv_model.fit(X_fit, y_fit)\n",
" train_accs_c.append(conv_model.score(X_fit, y_fit))\n",
" val_accs_c.append(conv_model.score(X_val_np, y_val_np))\n",
"\n",
"val_accs_c = np.array(val_accs_c)\n",
"train_accs_c = np.array(train_accs_c)\n",
"\n",
"# ── Tìm điểm hội tụ ───────────────────────────────────────────────────────────\n",
"improvements = np.abs(np.diff(val_accs_c))\n",
"convergence_idx = None\n",
"for i in range(len(improvements) - WINDOW + 1):\n",
" if all(improvements[i : i + WINDOW] < THRESHOLD):\n",
" convergence_idx = i + 1 # chỉ số của điểm đầu tiên trong cửa sổ\n",
" break\n",
"\n",
"best_idx = int(np.argmax(val_accs_c))\n",
"best_n = N_RANGE[best_idx]\n",
"conv_n = N_RANGE[convergence_idx] if convergence_idx is not None else None\n",
"\n",
"# ── Vẽ đồ thị ─────────────────────────────────────────────────────────────────\n",
"fig, axes = plt.subplots(1, 2, figsize=(15, 5))\n",
"\n",
"# --- Trái: accuracy curve ---\n",
"axes[0].plot(N_RANGE, train_accs_c, \"b-o\", markersize=3, label=\"Train\")\n",
"axes[0].plot(N_RANGE, val_accs_c, \"g-o\", markersize=3, label=\"Val\")\n",
"axes[0].axvline(x=best_n, color=\"red\", linestyle=\"--\", linewidth=1.5,\n",
" label=f\"Best val acc n={best_n} ({max(val_accs_c)*100:.2f}%)\")\n",
"if conv_n:\n",
" axes[0].axvline(x=conv_n, color=\"orange\", linestyle=\":\", linewidth=1.5,\n",
" label=f\"Hội tụ n={conv_n} ({val_accs_c[convergence_idx]*100:.2f}%)\")\n",
"axes[0].set_xlabel(\"n_estimators\")\n",
"axes[0].set_ylabel(\"Accuracy\")\n",
"axes[0].set_title(\"Convergence — Val Accuracy vs n_estimators\")\n",
"axes[0].legend(fontsize=8)\n",
"axes[0].grid(True, alpha=0.3)\n",
"\n",
"# --- Phải: cải thiện biên (marginal improvement) ---\n",
"axes[1].bar(N_RANGE[1:], improvements * 100, color=\"steelblue\", alpha=0.7)\n",
"axes[1].axhline(y=THRESHOLD * 100, color=\"red\", linestyle=\"--\",\n",
" label=f\"Threshold = {THRESHOLD*100:.3f}%\")\n",
"if conv_n:\n",
" axes[1].axvline(x=conv_n, color=\"orange\", linestyle=\":\", linewidth=1.5,\n",
" label=f\"Hội tụ n={conv_n}\")\n",
"axes[1].set_xlabel(\"n_estimators\")\n",
"axes[1].set_ylabel(\"ΔVal Accuracy (%)\")\n",
"axes[1].set_title(\"Marginal Improvement per Step\")\n",
"axes[1].legend(fontsize=8)\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"plt.suptitle(\"Random Forest — Convergence Analysis\", fontsize=13, fontweight=\"bold\")\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# ── Tổng kết ──────────────────────────────────────────────────────────────────\n",
"print(f\"\\n{'═'*55}\")\n",
"print(f\" Best val accuracy : {max(val_accs_c)*100:.4f}% (n_estimators={best_n})\")\n",
"if conv_n:\n",
" print(f\" Điểm HỘI TỤ : n_estimators = {conv_n}\")\n",
" print(f\" → Có thể dùng n_estimators={conv_n} thay vì 200 để tiết kiệm thời gian\")\n",
" saved_pct = (1 - conv_n / 200) * 100\n",
" print(f\" → Tiết kiệm ~{saved_pct:.0f}% thời gian train\")\n",
"else:\n",
" print(\" → Mô hình chưa hội tụ trong phạm vi [5, 200]. Thử tăng n_estimators.\")\n",
"print(f\"{'═'*55}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bc57f244",
"metadata": {},
"outputs": [],
"source": [
"from sklearn.metrics import classification_report, confusion_matrix, accuracy_score\n",
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
"\n",
"# ── Đánh giá trên tập test ─────────────────────────────────────────────────────\n",
"y_pred = model.predict(X_test_np)\n",
"\n",
"acc = accuracy_score(y_test_np, y_pred)\n",
"print(f\"Test Accuracy : {acc:.4f} ({acc*100:.2f}%)\\n\")\n",
"print(classification_report(y_test_np, y_pred, digits=4))\n",
"\n",
"# ── Feature importance ──────────────────────────────────────────────────────────\n",
"feat_imp = model.feature_importances_\n",
"idx = feat_imp.argsort()[::-1][:20]\n",
"plt.figure(figsize=(12, 4))\n",
"plt.bar(range(len(idx)), feat_imp[idx])\n",
"plt.xticks(range(len(idx)), idx, rotation=45)\n",
"plt.title(\"Top-20 Feature Importances\")\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# ── Confusion matrix ────────────────────────────────────────────────────────────\n",
"class_names = list(label_mapping.keys())\n",
"cm = confusion_matrix(y_test_np, y_pred)\n",
"plt.figure(figsize=(9, 7))\n",
"sns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\",\n",
" xticklabels=class_names, yticklabels=class_names)\n",
"plt.xlabel(\"Predicted\")\n",
"plt.ylabel(\"Actual\")\n",
"plt.title(\"Confusion Matrix — Random Forest\")\n",
"plt.tight_layout()\n",
"plt.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "efc23f2c",
"metadata": {},
"outputs": [],
"source": [
"import joblib, json, os\n",
"from datetime import datetime\n",
"\n",
"# ── Lưu mô hình ────────────────────────────────────────────────────────────────\n",
"model_path = \"model_random_forest_land_use.joblib\"\n",
"joblib.dump(model, model_path)\n",
"print(f\"✅ Model saved → {model_path}\")\n",
"\n",
"# ── Lưu thông tin mô hình ──────────────────────────────────────────────────────\n",
"info = {\n",
" \"model_type\": \"RandomForest\",\n",
" \"n_estimators\": model.n_estimators,\n",
" \"max_depth\": model.max_depth,\n",
" \"min_samples_leaf\": model.min_samples_leaf,\n",
" \"class_weight\": \"balanced\",\n",
" \"n_features\": int(X_fit.shape[1]),\n",
" \"label_mapping\": label_mapping,\n",
" \"test_accuracy\": float(acc),\n",
" \"train_samples\": int(len(X_fit)),\n",
" \"test_samples\": int(len(X_test_np)),\n",
" \"saved_at\": datetime.now().isoformat(),\n",
"}\n",
"info_path = \"model_random_forest_land_use_info.json\"\n",
"with open(info_path, \"w\") as f:\n",
" json.dump(info, f, indent=2, ensure_ascii=False)\n",
"print(f\"✅ Info saved → {info_path}\")\n",
"print(json.dumps(info, indent=2, ensure_ascii=False))\n",
"\n",
"# ── Đóng kết nối Dask ──────────────────────────────────────────────────────────\n",
"try:\n",
" client.close()\n",
" cluster.close()\n",
" print(\"✅ Dask cluster closed.\")\n",
"except Exception:\n",
" pass\n"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+372
View File
@@ -0,0 +1,372 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "f8e59602",
"metadata": {},
"outputs": [],
"source": [
"import importlib\n",
"import new_import_ODC as odc_tools\n",
"importlib.reload(odc_tools)\n",
"from new_import_ODC import *\n",
"print(\"✅ Import thành công\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8101f17e",
"metadata": {},
"outputs": [],
"source": [
"# Khởi tạo Dask + Datacube + S3\n",
"cluster, client = initialize_dask(use_gateway=True)\n",
"dc = datacube.Datacube()\n",
"configure_s3_access(aws_unsigned=True)\n",
"print(\"✅ Dask + Datacube + S3 sẵn sàng\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cba4d661",
"metadata": {},
"outputs": [],
"source": [
"# Cấu hình vùng và thời gian\n",
"date_range = (\"2022-09-01\", \"2023-10-01\")\n",
"longtitude_range = (105.5, 106.4)\n",
"latitude_range = (9.2, 10.0)\n",
"\n",
"# Tải dữ liệu Sentinel-2\n",
"data_sen2 = load_data(\n",
" dc=dc,\n",
" date_range=date_range,\n",
" longtitude_range=longtitude_range,\n",
" latitude_range=latitude_range,\n",
")\n",
"print(f\"✅ Sentinel-2 raw: {data_sen2.dims}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "28b9d92d",
"metadata": {},
"outputs": [],
"source": [
"# Tiền xử lý Sentinel-2: cloud mask + NDVI + resampling\n",
"data_clean = mask_clean(data_sen2)\n",
"data_ndvi = calculate_indices(data_clean, index=\"NDVI\")\n",
"data_fill = fill_nan(data_ndvi)\n",
"data_sen2_monthly = data_fill.resample(time=\"1MS\").mean().compute()\n",
"print(f\"✅ S2 monthly shape: {data_sen2_monthly.dims}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0e4efc29",
"metadata": {},
"outputs": [],
"source": [
"# Tải Sentinel-1 (SAR VV/VH)\n",
"data_sen1 = load_data_sen1(\n",
" dc=dc,\n",
" date_range=date_range,\n",
" longtitude_range=longtitude_range,\n",
" latitude_range=latitude_range,\n",
")\n",
"data_sen1_monthly = calculate_average(data_sen1, [\"VV\", \"VH\"], resample=\"1MS\").compute()\n",
"print(f\"✅ S1 monthly shape: {data_sen1_monthly.dims}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cb119111",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"# Ánh xạ nhãn lớp đất\n",
"label_mapping = {\n",
" \"Lua tom\": \"0\", \"Lua\": \"1\", \"CHN\": \"2\", \"CLN\": \"3\",\n",
" \"TS\": \"4\", \"Song\": \"5\", \"Dat xay dung\": \"6\", \"Rung\": \"7\",\n",
"}\n",
"\n",
"train_data = load_train_data(label_mapping=label_mapping)\n",
"X, y = get_data_sen1_and_sen2(train_data, data_sen2_monthly, data_sen1_monthly)\n",
"\n",
"X_train, X_val, X_test, y_train, y_val, y_test = split_train_data(X, y, test_size=0.2, val_size=0.1)\n",
"\n",
"X_train_np = np.array(X_train, dtype=np.float32)\n",
"X_val_np = np.array(X_val, dtype=np.float32)\n",
"X_test_np = np.array(X_test, dtype=np.float32)\n",
"y_train_np = np.array(y_train, dtype=np.int64)\n",
"y_val_np = np.array(y_val, dtype=np.int64)\n",
"y_test_np = np.array(y_test, dtype=np.int64)\n",
"\n",
"X_fit = np.concatenate([X_train_np, X_val_np], axis=0)\n",
"y_fit = np.concatenate([y_train_np, y_val_np], axis=0)\n",
"\n",
"print(f\"✅ X_fit: {X_fit.shape} | X_test: {X_test_np.shape}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "69492c1f",
"metadata": {},
"outputs": [],
"source": [
"%%time\n",
"from sklearn.svm import SVC\n",
"from sklearn.preprocessing import StandardScaler\n",
"\n",
"# ── Chuẩn hoá đặc trưng (quan trọng với SVM) ──────────────────────────────────\n",
"scaler = StandardScaler()\n",
"X_fit_scaled = scaler.fit_transform(X_fit)\n",
"X_test_scaled = scaler.transform(X_test_np)\n",
"X_val_scaled = scaler.transform(X_val_np)\n",
"\n",
"# ── Xây dựng và train mô hình SVM ─────────────────────────────────────────────\n",
"model = SVC(\n",
" kernel=\"rbf\",\n",
" C=10,\n",
" gamma=\"scale\",\n",
" probability=True,\n",
" class_weight=\"balanced\",\n",
" random_state=42,\n",
" verbose=True,\n",
")\n",
"\n",
"print(\"🚀 Training SVM (RBF kernel)...\")\n",
"print(f\" Train samples: {len(X_fit_scaled)}\")\n",
"model.fit(X_fit_scaled, y_fit)\n",
"\n",
"val_acc = model.score(X_val_scaled, y_val_np)\n",
"print(f\"✅ Training hoàn tất! Val accuracy: {val_acc:.4f} ({val_acc*100:.2f}%)\")\n",
"print(f\" n_support_vectors: {model.n_support_}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ac391274",
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"from sklearn.model_selection import learning_curve\n",
"from sklearn.svm import SVC\n",
"\n",
"# ═══════════════════════════════════════════════════════════════════════════════\n",
"# PHÂN TÍCH ĐIỂM HỘI TỤ — SVM\n",
"# Phương pháp 1: Learning Curve (accuracy vs training set size)\n",
"# Phương pháp 2: C-sensitivity (val accuracy vs regularization C)\n",
"# Điểm hội tụ = training size tại đó cải thiện val_score < threshold\n",
"# ═══════════════════════════════════════════════════════════════════════════════\n",
"THRESHOLD = 0.002 # cải thiện val_score < 0.2% → hội tụ\n",
"N_CV = 3 # số fold cho cross-validation (tăng để chính xác hơn)\n",
"\n",
"# ── 1. Learning Curve ──────────────────────────────────────────────────────────\n",
"print(\"🔍 Phân tích learning curve (train size)... [có thể mất vài phút]\")\n",
"svc_for_lc = SVC(kernel=\"rbf\", C=10, gamma=\"scale\",\n",
" class_weight=\"balanced\", random_state=42)\n",
"\n",
"train_sizes_pct = np.linspace(0.1, 1.0, 10)\n",
"train_sizes, train_scores, val_scores = learning_curve(\n",
" svc_for_lc, X_fit_scaled, y_fit,\n",
" train_sizes=train_sizes_pct,\n",
" cv=N_CV, scoring=\"accuracy\", n_jobs=-1, verbose=0,\n",
")\n",
"\n",
"train_mean = train_scores.mean(axis=1)\n",
"train_std = train_scores.std(axis=1)\n",
"val_mean = val_scores.mean(axis=1)\n",
"val_std = val_scores.std(axis=1)\n",
"\n",
"# Tìm điểm hội tụ\n",
"val_improvements = np.abs(np.diff(val_mean))\n",
"convergence_size_idx = None\n",
"for i in range(len(val_improvements) - 1):\n",
" if val_improvements[i] < THRESHOLD and val_improvements[i + 1] < THRESHOLD:\n",
" convergence_size_idx = i + 1\n",
" break\n",
"\n",
"# ── 2. C-Sensitivity ──────────────────────────────────────────────────────────\n",
"print(\"🔍 Phân tích C-sensitivity...\")\n",
"C_range = [0.01, 0.1, 1, 5, 10, 50, 100, 500]\n",
"val_accs_c = []\n",
"for c_val in C_range:\n",
" m = SVC(kernel=\"rbf\", C=c_val, gamma=\"scale\",\n",
" class_weight=\"balanced\", random_state=42)\n",
" m.fit(X_fit_scaled, y_fit)\n",
" val_accs_c.append(m.score(X_val_scaled, y_val_np))\n",
"\n",
"val_accs_c = np.array(val_accs_c)\n",
"best_C = C_range[int(np.argmax(val_accs_c))]\n",
"\n",
"# ── Vẽ đồ thị ─────────────────────────────────────────────────────────────────\n",
"fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n",
"\n",
"# --- Trái: Learning Curve ---\n",
"axes[0].plot(train_sizes, train_mean, \"b-o\", markersize=4, label=\"Train\")\n",
"axes[0].fill_between(train_sizes, train_mean - train_std, train_mean + train_std,\n",
" alpha=0.2, color=\"blue\")\n",
"axes[0].plot(train_sizes, val_mean, \"g-o\", markersize=4, label=\"Val (CV)\")\n",
"axes[0].fill_between(train_sizes, val_mean - val_std, val_mean + val_std,\n",
" alpha=0.2, color=\"green\")\n",
"if convergence_size_idx is not None:\n",
" csize = train_sizes[convergence_size_idx]\n",
" axes[0].axvline(x=csize, color=\"orange\", linestyle=\"--\", linewidth=1.5,\n",
" label=f\"Hội tụ ~{int(csize):,} mẫu\")\n",
"axes[0].set_xlabel(\"Training samples\")\n",
"axes[0].set_ylabel(\"Accuracy\")\n",
"axes[0].set_title(\"Learning Curve — Score vs Train Size\")\n",
"axes[0].legend(fontsize=8)\n",
"axes[0].grid(True, alpha=0.3)\n",
"\n",
"# --- Giữa: Marginal improvement of val score ---\n",
"axes[1].bar(range(len(val_improvements)), val_improvements * 100,\n",
" color=[\"green\" if v > THRESHOLD else \"salmon\" for v in val_improvements],\n",
" alpha=0.8)\n",
"axes[1].axhline(y=THRESHOLD * 100, color=\"red\", linestyle=\"--\",\n",
" label=f\"Threshold={THRESHOLD*100:.2f}%\")\n",
"if convergence_size_idx is not None:\n",
" axes[1].axvline(x=convergence_size_idx - 0.5, color=\"orange\", linestyle=\":\",\n",
" linewidth=1.5, label=f\"Hội tụ tại step {convergence_size_idx}\")\n",
"step_labels = [f\"{int(train_sizes[i])}\" for i in range(1, len(train_sizes))]\n",
"axes[1].set_xticks(range(len(val_improvements)))\n",
"axes[1].set_xticklabels(step_labels, rotation=45, fontsize=7)\n",
"axes[1].set_xlabel(\"Training size step\")\n",
"axes[1].set_ylabel(\"ΔVal Accuracy (%)\")\n",
"axes[1].set_title(\"Marginal Val Improvement per Add. Samples\")\n",
"axes[1].legend(fontsize=8)\n",
"axes[1].grid(True, alpha=0.3)\n",
"\n",
"# --- Phải: C sensitivity ---\n",
"axes[2].semilogx(C_range, val_accs_c * 100, \"m-o\", markersize=6)\n",
"axes[2].axvline(x=best_C, color=\"red\", linestyle=\"--\", linewidth=1.5,\n",
" label=f\"Best C={best_C} ({max(val_accs_c)*100:.2f}%)\")\n",
"axes[2].set_xlabel(\"C (regularization)\")\n",
"axes[2].set_ylabel(\"Val Accuracy (%)\")\n",
"axes[2].set_title(\"C-Sensitivity (Regularization)\")\n",
"axes[2].legend(fontsize=8)\n",
"axes[2].grid(True, alpha=0.3)\n",
"\n",
"plt.suptitle(\"SVM — Convergence Analysis\", fontsize=13, fontweight=\"bold\")\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# ── Tổng kết ──────────────────────────────────────────────────────────────────\n",
"print(f\"\\n{'═'*60}\")\n",
"if convergence_size_idx is not None:\n",
" print(f\" Điểm HỘI TỤ (Δval < {THRESHOLD*100:.1f}%) : \"\n",
" f\"~{int(train_sizes[convergence_size_idx]):,} mẫu \"\n",
" f\"(val_acc={val_mean[convergence_size_idx]*100:.2f}%)\")\n",
" pct_data = train_sizes[convergence_size_idx] / len(X_fit_scaled) * 100\n",
" print(f\" → Chỉ cần ~{pct_data:.0f}% dữ liệu để mô hình hội tụ\")\n",
"else:\n",
" print(\" → Cần thêm dữ liệu: val score vẫn đang cải thiện ở toàn bộ tập train\")\n",
"print(f\" C tối ưu : {best_C} (val_acc={max(val_accs_c)*100:.2f}%)\")\n",
"print(f\" C hiện tại dùng : 10 {'✅' if best_C == 10 else '⚠️ Thử dùng C=' + str(best_C)}\")\n",
"print(f\"{'═'*60}\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "940f640d",
"metadata": {},
"outputs": [],
"source": [
"from sklearn.metrics import classification_report, confusion_matrix, accuracy_score\n",
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
"\n",
"# ── Đánh giá trên tập test ─────────────────────────────────────────────────────\n",
"y_pred = model.predict(X_test_scaled)\n",
"\n",
"acc = accuracy_score(y_test_np, y_pred)\n",
"print(f\"Test Accuracy : {acc:.4f} ({acc*100:.2f}%)\\n\")\n",
"print(classification_report(y_test_np, y_pred, digits=4))\n",
"\n",
"# ── Confusion matrix ────────────────────────────────────────────────────────────\n",
"class_names = list(label_mapping.keys())\n",
"cm = confusion_matrix(y_test_np, y_pred)\n",
"plt.figure(figsize=(9, 7))\n",
"sns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Oranges\",\n",
" xticklabels=class_names, yticklabels=class_names)\n",
"plt.xlabel(\"Predicted\")\n",
"plt.ylabel(\"Actual\")\n",
"plt.title(\"Confusion Matrix — SVM (RBF)\")\n",
"plt.tight_layout()\n",
"plt.show()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "182c64ab",
"metadata": {},
"outputs": [],
"source": [
"import joblib, json\n",
"from datetime import datetime\n",
"\n",
"# ── Lưu scaler (cần thiết khi inference) ──────────────────────────────────────\n",
"scaler_path = \"model_svm_land_use_scaler.joblib\"\n",
"joblib.dump(scaler, scaler_path)\n",
"print(f\"✅ Scaler saved → {scaler_path}\")\n",
"\n",
"# ── Lưu mô hình SVM ────────────────────────────────────────────────────────────\n",
"model_path = \"model_svm_land_use.joblib\"\n",
"joblib.dump(model, model_path)\n",
"print(f\"✅ Model saved → {model_path}\")\n",
"\n",
"# ── Lưu thông tin mô hình ──────────────────────────────────────────────────────\n",
"info = {\n",
" \"model_type\": \"SVM\",\n",
" \"kernel\": model.kernel,\n",
" \"C\": model.C,\n",
" \"gamma\": model.gamma,\n",
" \"probability\": model.probability,\n",
" \"class_weight\": \"balanced\",\n",
" \"n_features\": int(X_fit.shape[1]),\n",
" \"label_mapping\": label_mapping,\n",
" \"scaler\": scaler_path,\n",
" \"test_accuracy\": float(acc),\n",
" \"train_samples\": int(len(X_fit_scaled)),\n",
" \"test_samples\": int(len(X_test_scaled)),\n",
" \"saved_at\": datetime.now().isoformat(),\n",
"}\n",
"info_path = \"model_svm_land_use_info.json\"\n",
"with open(info_path, \"w\") as f:\n",
" json.dump(info, f, indent=2, ensure_ascii=False)\n",
"print(f\"✅ Info saved → {info_path}\")\n",
"print(json.dumps(info, indent=2, ensure_ascii=False))\n",
"\n",
"# ── Đóng kết nối Dask ──────────────────────────────────────────────────────────\n",
"try:\n",
" client.close()\n",
" cluster.close()\n",
" print(\"✅ Dask cluster closed.\")\n",
"except Exception:\n",
" pass\n"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long