{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Tải Dữ liệu Vệ tinh qua Colab (Self-contained)\n", "Notebook này đã được nhúng sẵn toàn bộ mã nguồn xử lý. Bạn không cần upload cả thư mục `remote-sensing` nữa.\n", "\n", "## Bước 1: Upload Shapefile (BẮT BUỘC)\n", "Mô hình cần biết các điểm tọa độ đất để lấy dữ liệu. Hãy nén thư mục `train/` trên máy bạn thành `train.zip` và chạy ô dưới đây để upload nó trực tiếp lên Colab." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from google.colab import files\n", "import os\n", "\n", "print(\"Hãy chọn file train.zip từ máy tính của bạn:\")\n", "uploaded = files.upload()\n", "\n", "if \"train.zip\" in uploaded:\n", " !unzip -q -o train.zip -d /content/train_tmp/\n", " # Move the extracted files directly to /content/train/\n", " !mkdir -p /content/train\n", " !mv /content/train_tmp/*/* /content/train/ 2>/dev/null || mv /content/train_tmp/* /content/train/\n", " print(\"Đã giải nén shapefile thành công vào thư mục /content/train/\")\n", "else:\n", " print(\"LỖI: Bạn chưa upload file có tên là train.zip!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Bước 2: Cài đặt thư viện & Tạo môi trường" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pip install planetary-computer pystac-client odc-stac geopandas rasterio xarray joblib scikit-learn xgboost lightgbm" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile feature_extractor.py\n\"\"\"\nFeature Extraction Module for Land Classification\nChuẩn hóa việc trích xuất features từ satellite data cho cả training và prediction\n\"\"\"\n\nimport numpy as np\nimport xarray as xr\nfrom typing import List, Dict, Tuple, Optional\n\n\nclass FeatureExtractor:\n \"\"\"\n Extract features từ Sentinel-2 và Sentinel-1 data\n Hỗ trợ 2 modes:\n - 'simple': 3 features cơ bản (NDVI_mean, VH_mean, VV_mean)\n - 'temporal': 39 features time-series (NDVI + NDWI + NDBI theo thời gian)\n \"\"\"\n \n FEATURE_MODES = {\n 'simple': {\n 'n_features': 3,\n 'features': ['NDVI_mean', 'VH_db_mean', 'VV_db_mean'],\n 'description': 'Simple aggregate features (mean only)'\n },\n 'temporal': {\n 'n_features': 39,\n 'features': None, # Generated dynamically based on time steps\n 'description': 'Temporal features with NDVI, NDWI, NDBI time series'\n },\n 'extended': {\n 'n_features': 15,\n 'features': [\n 'NDVI_mean', 'NDVI_std', 'NDVI_min', 'NDVI_max',\n 'NDWI_mean', 'NDWI_std', 'NDWI_min', 'NDWI_max',\n 'NDBI_mean', 'NDBI_std', 'NDBI_min', 'NDBI_max',\n 'VH_db_mean', 'VV_db_mean', 'VH_VV_ratio'\n ],\n 'description': 'Extended aggregate features with statistics'\n },\n 'odc': {\n 'n_features': 8,\n 'features': [\n 'ndvi_mean', 'ndvi_min', 'ndvi_max', 'ndvi_std', 'ndvi_range',\n 'ndwi_mean', 'ndbi_mean', 'evi_mean'\n ],\n 'description': 'ODC mode: 8 aggregate features (NDVI stats + NDWI/NDBI/EVI mean) - matches 01.train_ODC.ipynb'\n }\n }\n \n def __init__(self, mode: str = 'simple'):\n \"\"\"\n Initialize FeatureExtractor\n \n Args:\n mode: 'simple', 'temporal', hoặc 'extended'\n \"\"\"\n if mode not in self.FEATURE_MODES:\n raise ValueError(f\"Invalid mode: {mode}. Choose from {list(self.FEATURE_MODES.keys())}\")\n \n self.mode = mode\n self.config = self.FEATURE_MODES[mode]\n \n def get_feature_names(self, n_timesteps: Optional[int] = None) -> List[str]:\n \"\"\"\n Lấy danh sách tên features\n \n Args:\n n_timesteps: Số timesteps (chỉ cần cho mode='temporal')\n \n Returns:\n List tên features\n \"\"\"\n if self.mode == 'temporal':\n if n_timesteps is None:\n raise ValueError(\"n_timesteps required for temporal mode\")\n \n features = []\n # NDVI time series\n for t in range(n_timesteps):\n features.append(f'NDVI_t{t+1}')\n # NDWI time series\n for t in range(n_timesteps):\n features.append(f'NDWI_t{t+1}')\n # NDBI time series\n for t in range(n_timesteps):\n features.append(f'NDBI_t{t+1}')\n \n # VH/VV radar (mean across time)\n features.append('VH_db_mean')\n features.append('VV_db_mean')\n features.append('VH_VV_ratio')\n \n return features\n else:\n return self.config['features']\n \n def extract_simple_features(\n self, \n ndvi_data: xr.DataArray,\n vh_data: Optional[xr.DataArray] = None,\n vv_data: Optional[xr.DataArray] = None\n ) -> np.ndarray:\n \"\"\"\n Extract simple features (3 features: NDVI_mean, VH_db_mean, VV_db_mean)\n \n Args:\n ndvi_data: NDVI DataArray (có thể có time dimension)\n vh_data: VH radar DataArray\n vv_data: VV radar DataArray\n \n Returns:\n Feature array shape (n_pixels, 3)\n \"\"\"\n # Calculate NDVI mean\n if 'time' in ndvi_data.dims:\n ndvi_mean = ndvi_data.mean(dim='time')\n else:\n ndvi_mean = ndvi_data\n \n # Flatten to pixels\n ndvi_flat = ndvi_mean.values.flatten()\n \n # Calculate radar features if available\n if vh_data is not None and vv_data is not None:\n if 'time' in vh_data.dims:\n vh_mean = vh_data.mean(dim='time')\n vv_mean = vv_data.mean(dim='time')\n else:\n vh_mean = vh_data\n vv_mean = vv_data\n \n vh_flat = vh_mean.values.flatten()\n vv_flat = vv_mean.values.flatten()\n else:\n # If no radar data, use zeros\n vh_flat = np.zeros_like(ndvi_flat)\n vv_flat = np.zeros_like(ndvi_flat)\n \n # Stack features\n features = np.column_stack([ndvi_flat, vh_flat, vv_flat])\n \n return features\n \n def extract_temporal_features(\n self,\n s2_data: xr.Dataset,\n vh_data: Optional[xr.DataArray] = None,\n vv_data: Optional[xr.DataArray] = None\n ) -> np.ndarray:\n \"\"\"\n Extract temporal features (39 features: time series của NDVI, NDWI, NDBI + radar)\n \n Args:\n s2_data: Sentinel-2 Dataset với bands B02, B03, B04, B08, B11\n vh_data: VH radar DataArray\n vv_data: VV radar DataArray\n \n Returns:\n Feature array shape (n_pixels, 39)\n \"\"\"\n # Calculate spectral indices\n nir = s2_data[\"B08\"].astype('float32')\n red = s2_data[\"B04\"].astype('float32')\n green = s2_data[\"B03\"].astype('float32')\n swir = s2_data[\"B11\"].astype('float32') if \"B11\" in s2_data else s2_data[\"B02\"] # Fallback to B02\n \n # NDVI = (NIR - Red) / (NIR + Red)\n ndvi = (nir - red) / (nir + red + 1e-8)\n \n # NDWI = (Green - NIR) / (Green + NIR)\n ndwi = (green - nir) / (green + nir + 1e-8)\n \n # NDBI = (SWIR - NIR) / (SWIR + NIR)\n ndbi = (swir - nir) / (swir + nir + 1e-8)\n \n # Resample to monthly if time dimension exists\n if 'time' in ndvi.dims:\n ndvi_monthly = ndvi.resample(time=\"1ME\").mean()\n ndwi_monthly = ndwi.resample(time=\"1ME\").mean()\n ndbi_monthly = ndbi.resample(time=\"1ME\").mean()\n else:\n ndvi_monthly = ndvi\n ndwi_monthly = ndwi\n ndbi_monthly = ndbi\n \n # Get dimensions\n n_times = len(ndvi_monthly.time) if 'time' in ndvi_monthly.dims else 1\n y_size = len(ndvi_monthly.y)\n x_size = len(ndvi_monthly.x)\n n_pixels = y_size * x_size\n \n # Extract temporal features\n features_list = []\n \n # NDVI time series\n for t in range(n_times):\n if 'time' in ndvi_monthly.dims:\n ndvi_t = ndvi_monthly.isel(time=t).values.flatten()\n else:\n ndvi_t = ndvi_monthly.values.flatten()\n features_list.append(ndvi_t)\n \n # NDWI time series\n for t in range(n_times):\n if 'time' in ndwi_monthly.dims:\n ndwi_t = ndwi_monthly.isel(time=t).values.flatten()\n else:\n ndwi_t = ndwi_monthly.values.flatten()\n features_list.append(ndwi_t)\n \n # NDBI time series\n for t in range(n_times):\n if 'time' in ndbi_monthly.dims:\n ndbi_t = ndbi_monthly.isel(time=t).values.flatten()\n else:\n ndbi_t = ndbi_monthly.values.flatten()\n features_list.append(ndbi_t)\n \n # Stack all spectral features\n features = np.column_stack(features_list)\n \n # Add radar features if available\n if vh_data is not None and vv_data is not None:\n if 'time' in vh_data.dims:\n vh_mean = vh_data.mean(dim='time')\n vv_mean = vv_data.mean(dim='time')\n else:\n vh_mean = vh_data\n vv_mean = vv_data\n \n vh_flat = vh_mean.values.flatten()\n vv_flat = vv_mean.values.flatten()\n vh_vv_ratio = vh_flat / (vv_flat + 1e-8)\n \n # Add radar features\n features = np.column_stack([features, vh_flat, vv_flat, vh_vv_ratio])\n \n return features\n \n def extract_odc_features(\n self,\n s2_data: xr.Dataset,\n vh_data: Optional[xr.DataArray] = None,\n vv_data: Optional[xr.DataArray] = None\n ) -> np.ndarray:\n \"\"\"\n Extract ODC aggregate features (8 features matching 01.train_ODC.ipynb):\n ndvi_mean, ndvi_min, ndvi_max, ndvi_std, ndvi_range, ndwi_mean, ndbi_mean, evi_mean\n \n Args:\n s2_data: Sentinel-2 Dataset with B02, B03, B04, B08, B11\n vh_data: Not used in ODC mode\n vv_data: Not used in ODC mode\n \n Returns:\n Feature array shape (n_pixels, 8)\n \"\"\"\n # Calculate spectral indices\n nir = s2_data[\"B08\"].astype('float32')\n red = s2_data[\"B04\"].astype('float32')\n green = s2_data[\"B03\"].astype('float32')\n blue = s2_data[\"B02\"].astype('float32')\n swir = s2_data[\"B11\"].astype('float32') if \"B11\" in s2_data else s2_data[\"B02\"]\n \n # NDVI = (NIR - Red) / (NIR + Red)\n ndvi = (nir - red) / (nir + red + 1e-8)\n \n # NDWI = (Green - NIR) / (Green + NIR)\n ndwi = (green - nir) / (green + nir + 1e-8)\n \n # NDBI = (SWIR - NIR) / (SWIR + NIR)\n ndbi = (swir - nir) / (swir + nir + 1e-8)\n \n # EVI = 2.5 * (NIR - Red) / (NIR + 6*Red - 7.5*Blue + 1)\n evi = 2.5 * (nir - red) / (nir + 6*red - 7.5*blue + 1)\n \n features_list = []\n \n # NDVI statistics (5 features)\n if 'time' in ndvi.dims:\n features_list.append(ndvi.mean(dim='time').values.flatten()) # ndvi_mean\n features_list.append(ndvi.min(dim='time').values.flatten()) # ndvi_min\n features_list.append(ndvi.max(dim='time').values.flatten()) # ndvi_max\n features_list.append(ndvi.std(dim='time').values.flatten()) # ndvi_std\n ndvi_range = (ndvi.max(dim='time') - ndvi.min(dim='time')).values.flatten()\n features_list.append(ndvi_range) # ndvi_range\n else:\n ndvi_flat = ndvi.values.flatten()\n features_list.extend([ndvi_flat, ndvi_flat, ndvi_flat, np.zeros_like(ndvi_flat), np.zeros_like(ndvi_flat)])\n \n # NDWI mean (1 feature)\n if 'time' in ndwi.dims:\n features_list.append(ndwi.mean(dim='time').values.flatten()) # ndwi_mean\n else:\n features_list.append(ndwi.values.flatten())\n \n # NDBI mean (1 feature)\n if 'time' in ndbi.dims:\n features_list.append(ndbi.mean(dim='time').values.flatten()) # ndbi_mean\n else:\n features_list.append(ndbi.values.flatten())\n \n # EVI mean (1 feature)\n if 'time' in evi.dims:\n features_list.append(evi.mean(dim='time').values.flatten()) # evi_mean\n else:\n features_list.append(evi.values.flatten())\n \n # Stack all features (total: 8 features)\n features = np.column_stack(features_list)\n \n return features\n \n def extract_extended_features(\n self,\n s2_data: xr.Dataset,\n vh_data: Optional[xr.DataArray] = None,\n vv_data: Optional[xr.DataArray] = None\n ) -> np.ndarray:\n \"\"\"\n Extract extended aggregate features (15 features: stats của NDVI, NDWI, NDBI + radar)\n \n Args:\n s2_data: Sentinel-2 Dataset\n vh_data: VH radar DataArray\n vv_data: VV radar DataArray\n \n Returns:\n Feature array shape (n_pixels, 15)\n \"\"\"\n # Calculate spectral indices\n nir = s2_data[\"B08\"].astype('float32')\n red = s2_data[\"B04\"].astype('float32')\n green = s2_data[\"B03\"].astype('float32')\n swir = s2_data[\"B11\"].astype('float32') if \"B11\" in s2_data else s2_data[\"B02\"]\n \n ndvi = (nir - red) / (nir + red + 1e-8)\n ndwi = (green - nir) / (green + nir + 1e-8)\n ndbi = (swir - nir) / (swir + nir + 1e-8)\n \n features_list = []\n \n # NDVI statistics\n if 'time' in ndvi.dims:\n features_list.append(ndvi.mean(dim='time').values.flatten())\n features_list.append(ndvi.std(dim='time').values.flatten())\n features_list.append(ndvi.min(dim='time').values.flatten())\n features_list.append(ndvi.max(dim='time').values.flatten())\n else:\n ndvi_flat = ndvi.values.flatten()\n features_list.extend([ndvi_flat, np.zeros_like(ndvi_flat), ndvi_flat, ndvi_flat])\n \n # NDWI statistics\n if 'time' in ndwi.dims:\n features_list.append(ndwi.mean(dim='time').values.flatten())\n features_list.append(ndwi.std(dim='time').values.flatten())\n features_list.append(ndwi.min(dim='time').values.flatten())\n features_list.append(ndwi.max(dim='time').values.flatten())\n else:\n ndwi_flat = ndwi.values.flatten()\n features_list.extend([ndwi_flat, np.zeros_like(ndwi_flat), ndwi_flat, ndwi_flat])\n \n # NDBI statistics\n if 'time' in ndbi.dims:\n features_list.append(ndbi.mean(dim='time').values.flatten())\n features_list.append(ndbi.std(dim='time').values.flatten())\n features_list.append(ndbi.min(dim='time').values.flatten())\n features_list.append(ndbi.max(dim='time').values.flatten())\n else:\n ndbi_flat = ndbi.values.flatten()\n features_list.extend([ndbi_flat, np.zeros_like(ndbi_flat), ndbi_flat, ndbi_flat])\n \n # Stack spectral features\n features = np.column_stack(features_list)\n \n # Add radar features\n if vh_data is not None and vv_data is not None:\n if 'time' in vh_data.dims:\n vh_mean = vh_data.mean(dim='time')\n vv_mean = vv_data.mean(dim='time')\n else:\n vh_mean = vh_data\n vv_mean = vv_data\n \n vh_flat = vh_mean.values.flatten()\n vv_flat = vv_mean.values.flatten()\n vh_vv_ratio = vh_flat / (vv_flat + 1e-8)\n \n features = np.column_stack([features, vh_flat, vv_flat, vh_vv_ratio])\n \n return features\n \n def extract(\n self,\n s2_data: Optional[xr.Dataset] = None,\n ndvi_data: Optional[xr.DataArray] = None,\n vh_data: Optional[xr.DataArray] = None,\n vv_data: Optional[xr.DataArray] = None\n ) -> np.ndarray:\n \"\"\"\n Extract features theo mode đã chọn\n \n Args:\n s2_data: Sentinel-2 Dataset (cần cho temporal, extended, và odc modes)\n ndvi_data: NDVI DataArray (cần cho simple mode)\n vh_data: VH radar DataArray\n vv_data: VV radar DataArray\n \n Returns:\n Feature array\n \"\"\"\n if self.mode == 'simple':\n if ndvi_data is None:\n raise ValueError(\"ndvi_data required for simple mode\")\n return self.extract_simple_features(ndvi_data, vh_data, vv_data)\n \n elif self.mode == 'temporal':\n if s2_data is None:\n raise ValueError(\"s2_data required for temporal mode\")\n return self.extract_temporal_features(s2_data, vh_data, vv_data)\n \n elif self.mode == 'extended':\n if s2_data is None:\n raise ValueError(\"s2_data required for extended mode\")\n return self.extract_extended_features(s2_data, vh_data, vv_data)\n \n elif self.mode == 'odc':\n if s2_data is None:\n raise ValueError(\"s2_data required for odc mode\")\n return self.extract_odc_features(s2_data, vh_data, vv_data)\n \n else:\n raise ValueError(f\"Unknown mode: {self.mode}\")\n \n def get_info(self) -> Dict:\n \"\"\"Lấy thông tin về feature extraction mode\"\"\"\n return {\n 'mode': self.mode,\n 'n_features': self.config['n_features'],\n 'description': self.config['description']\n }\n\n\ndef get_feature_extractor(mode: str = 'simple') -> FeatureExtractor:\n \"\"\"\n Factory function để tạo FeatureExtractor\n \n Args:\n mode: 'simple', 'temporal', 'extended', hoặc 'odc'\n \n Returns:\n FeatureExtractor instance\n \"\"\"\n return FeatureExtractor(mode=mode)\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile train_module.py\n\"\"\"\nTraining module for land classification using Sentinel-2 and Sentinel-1 data\nfrom Microsoft Planetary Computer STAC API\n\"\"\"\n\nimport numpy as np\nimport xarray as xr\nimport geopandas as gpd\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.metrics import classification_report, confusion_matrix\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.tree import DecisionTreeClassifier\nfrom sklearn.svm import SVC\nfrom xgboost import XGBClassifier\nfrom lightgbm import LGBMClassifier\nimport joblib\nfrom datetime import datetime\nimport json\nimport os\nimport warnings\nimport hashlib\nfrom pathlib import Path\nwarnings.filterwarnings('ignore')\n\n# PyTorch for CNN and advanced models\ntry:\n import torch\n import torch.nn as nn\n import torch.nn.functional as F\n import torch.optim as optim\n from torch.utils.data import TensorDataset, DataLoader\n import torchvision.models as models\n PYTORCH_AVAILABLE = True\nexcept ImportError:\n PYTORCH_AVAILABLE = False\n print(\"Warning: PyTorch not available. CNN and advanced models will not work.\")\n\n# Define CNN model class for PyTorch\nclass CNNClassifier(nn.Module):\n def __init__(self, n_features, n_classes):\n super(CNNClassifier, self).__init__()\n self.n_features = n_features\n self.n_classes = n_classes\n \n # For small feature sets (like 3 features), use simpler architecture\n if n_features < 8:\n # Simple fully connected network for small features\n self.use_conv = False\n self.fc1 = nn.Linear(n_features, 64)\n self.dropout1 = nn.Dropout(0.3)\n self.fc2 = nn.Linear(64, 128)\n self.dropout2 = nn.Dropout(0.5)\n self.fc3 = nn.Linear(128, n_classes)\n else:\n # CNN architecture for larger feature sets\n self.use_conv = True\n self.conv1 = nn.Conv1d(in_channels=1, out_channels=32, kernel_size=3, padding=1)\n self.pool1 = nn.MaxPool1d(kernel_size=2)\n self.conv2 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3, padding=1)\n self.pool2 = nn.MaxPool1d(kernel_size=2)\n \n # Calculate size after convolutions\n conv_output_size = (n_features // 2 // 2) * 64\n \n # Fully connected layers\n self.fc1 = nn.Linear(conv_output_size, 128)\n self.dropout = nn.Dropout(0.5)\n self.fc2 = nn.Linear(128, n_classes)\n \n def forward(self, x):\n # x shape: (batch, n_features) or (batch, 1, n_features)\n if self.use_conv:\n # CNN path for larger feature sets\n if len(x.shape) == 2:\n x = x.unsqueeze(1) # Add channel dimension\n x = F.relu(self.conv1(x))\n x = self.pool1(x)\n x = F.relu(self.conv2(x))\n x = self.pool2(x)\n x = x.view(x.size(0), -1) # Flatten\n x = F.relu(self.fc1(x))\n x = self.dropout(x)\n x = self.fc2(x)\n else:\n # Fully connected path for small feature sets\n if len(x.shape) == 3:\n x = x.squeeze(1) # Remove channel dimension if present\n x = F.relu(self.fc1(x))\n x = self.dropout1(x)\n x = F.relu(self.fc2(x))\n x = self.dropout2(x)\n x = self.fc3(x)\n return x\n \n def predict(self, X):\n \"\"\"Scikit-learn style predict method\"\"\"\n self.eval()\n with torch.no_grad():\n if isinstance(X, np.ndarray):\n X = torch.FloatTensor(X)\n # Handle both 2D and 3D inputs\n if not self.use_conv and len(X.shape) == 3:\n X = X.squeeze(1)\n elif self.use_conv and len(X.shape) == 2:\n X = X.unsqueeze(1)\n outputs = self(X)\n _, predicted = torch.max(outputs, 1)\n return predicted.cpu().numpy()\n \n def score(self, X, y):\n \"\"\"Scikit-learn style score method\"\"\"\n predictions = self.predict(X)\n if isinstance(y, torch.Tensor):\n y = y.cpu().numpy()\n return np.mean(predictions == y)\n\n\n# Swin-UNet Classifier for feature vectors\nclass SwinUNetClassifier(nn.Module):\n \"\"\"\n Swin Transformer U-Net style architecture adapted for feature vector classification.\n Combines hierarchical Swin Transformer blocks with skip connections.\n \"\"\"\n def __init__(self, n_features, n_classes, embed_dim=128, depths=(2, 2, 6, 2), num_heads=(4, 8, 16, 32)):\n super(SwinUNetClassifier, self).__init__()\n self.n_features = n_features\n self.n_classes = n_classes\n self.embed_dim = embed_dim\n \n # Feature adapter - convert input features to embedding\n self.adapter = nn.Sequential(\n nn.Linear(n_features, embed_dim * 2),\n nn.ReLU(),\n nn.Dropout(0.1),\n nn.Linear(embed_dim * 2, embed_dim)\n )\n \n # Encoder path with hierarchical structure\n # Stage 1 - 1/4 resolution\n self.encoder1 = nn.Sequential(\n nn.Linear(embed_dim, embed_dim),\n nn.LayerNorm(embed_dim),\n nn.GELU(),\n nn.Dropout(0.1)\n )\n self.down1 = nn.Linear(embed_dim, embed_dim * 2)\n \n # Stage 2 - 1/8 resolution\n self.encoder2 = nn.Sequential(\n nn.Linear(embed_dim * 2, embed_dim * 2),\n nn.LayerNorm(embed_dim * 2),\n nn.GELU(),\n nn.Dropout(0.1)\n )\n self.down2 = nn.Linear(embed_dim * 2, embed_dim * 4)\n \n # Stage 3 - 1/16 resolution (bottleneck)\n self.encoder3 = nn.Sequential(\n nn.Linear(embed_dim * 4, embed_dim * 4),\n nn.LayerNorm(embed_dim * 4),\n nn.GELU(),\n nn.Dropout(0.1)\n )\n \n # Decoder path with skip connections\n self.up2 = nn.Linear(embed_dim * 4, embed_dim * 2)\n self.decoder2 = nn.Sequential(\n nn.Linear(embed_dim * 4, embed_dim * 2), # Concatenated with skip\n nn.LayerNorm(embed_dim * 2),\n nn.GELU(),\n nn.Dropout(0.1)\n )\n \n self.up1 = nn.Linear(embed_dim * 2, embed_dim)\n self.decoder1 = nn.Sequential(\n nn.Linear(embed_dim * 2, embed_dim), # Concatenated with skip\n nn.LayerNorm(embed_dim),\n nn.GELU(),\n nn.Dropout(0.1)\n )\n \n # Classification head\n self.classifier = nn.Sequential(\n nn.Linear(embed_dim, embed_dim // 2),\n nn.GELU(),\n nn.Dropout(0.3),\n nn.Linear(embed_dim // 2, n_classes)\n )\n \n # Attention mechanism for better feature aggregation\n self.attention = nn.MultiheadAttention(embed_dim, num_heads=4, batch_first=True)\n \n def forward(self, x):\n # x shape: (batch, n_features)\n if len(x.shape) == 3:\n x = x.squeeze(1)\n \n batch_size = x.shape[0]\n \n # Feature adaptation\n x = self.adapter(x) # (batch, embed_dim)\n \n # Add sequence dimension for attention (treat as sequence of length 1)\n x_seq = x.unsqueeze(1) # (batch, 1, embed_dim)\n \n # Encoder path\n # Stage 1\n x1 = self.encoder1(x_seq) # (batch, 1, embed_dim)\n x_down1 = self.down1(x1.squeeze(1)) # (batch, embed_dim*2)\n \n # Stage 2\n x2 = self.encoder2(x_down1.unsqueeze(1)) # (batch, 1, embed_dim*2)\n x_down2 = self.down2(x2.squeeze(1)) # (batch, embed_dim*4)\n \n # Stage 3 (bottleneck)\n x3 = self.encoder3(x_down2.unsqueeze(1)) # (batch, 1, embed_dim*4)\n \n # Decoder path with skip connections\n # Up2\n x_up2 = self.up2(x3.squeeze(1)) # (batch, embed_dim*2)\n x_cat2 = torch.cat([x_up2, x_down1], dim=1) # (batch, embed_dim*4) - concatenate skip\n # Create proper 3D tensor for decoder\n x_cat2_seq = x_cat2.unsqueeze(1) # (batch, 1, embed_dim*4)\n x_dec2 = self.decoder2(x_cat2) # (batch, embed_dim*2)\n \n # Up1\n x_up1 = self.up1(x_dec2) # (batch, embed_dim)\n x_cat1 = torch.cat([x_up1, x.squeeze(1)], dim=1) # (batch, embed_dim*2) - concatenate skip\n x_dec1 = self.decoder1(x_cat1) # (batch, embed_dim)\n \n # Apply attention mechanism for better aggregation\n x_dec1_seq = x_dec1.unsqueeze(1) # (batch, 1, embed_dim)\n attn_out, _ = self.attention(x_dec1_seq, x_dec1_seq, x_dec1_seq)\n \n # Classification\n output = self.classifier(attn_out.squeeze(1))\n return output\n \n def predict(self, X):\n \"\"\"Scikit-learn style predict\"\"\"\n self.eval()\n with torch.no_grad():\n if isinstance(X, np.ndarray):\n X = torch.FloatTensor(X)\n outputs = self(X)\n _, predicted = torch.max(outputs, 1)\n return predicted.cpu().numpy()\n \n def score(self, X, y):\n \"\"\"Scikit-learn style score\"\"\"\n predictions = self.predict(X)\n if isinstance(y, torch.Tensor):\n y = y.cpu().numpy()\n return np.mean(predictions == y)\n\n\n# MobileNetV3 + LR-ASPP Classifier\nclass MobileNetLRASPPClassifier(nn.Module):\n \"\"\"\n MobileNetV3 backbone with LR-ASPP (Lite Reduced Atrous Spatial Pyramid Pooling) for semantic segmentation\n Lightweight architecture optimized for efficiency and speed\n \"\"\"\n def __init__(self, n_features, n_classes):\n super(MobileNetLRASPPClassifier, self).__init__()\n self.n_features = n_features\n self.n_classes = n_classes\n \n # Feature extraction layers (MobileNetV3-inspired)\n self.feature_extractor = nn.Sequential(\n nn.Linear(n_features, 128),\n nn.BatchNorm1d(128),\n nn.ReLU(inplace=True),\n nn.Dropout(0.2),\n \n nn.Linear(128, 256),\n nn.BatchNorm1d(256),\n nn.ReLU(inplace=True),\n nn.Dropout(0.3),\n \n nn.Linear(256, 512),\n nn.BatchNorm1d(512),\n nn.ReLU(inplace=True),\n nn.Dropout(0.3),\n )\n \n # LR-ASPP head (simplified for feature vectors)\n # Branch 1: Global average pooling\n self.global_pool = nn.AdaptiveAvgPool1d(1)\n self.global_conv = nn.Sequential(\n nn.Linear(512, 128),\n nn.ReLU(inplace=True)\n )\n \n # Branch 2: 1x1 convolution equivalent\n self.branch_conv = nn.Sequential(\n nn.Linear(512, 128),\n nn.BatchNorm1d(128),\n nn.ReLU(inplace=True)\n )\n \n # Fusion and classification\n self.classifier = nn.Sequential(\n nn.Linear(256, 128), # 128 from global + 128 from branch\n nn.BatchNorm1d(128),\n nn.ReLU(inplace=True),\n nn.Dropout(0.4),\n nn.Linear(128, n_classes)\n )\n \n def forward(self, x):\n # x shape: (batch, n_features)\n features = self.feature_extractor(x)\n \n # LR-ASPP head\n # Branch 1: Global pooling\n global_feat = self.global_pool(features.unsqueeze(-1)).squeeze(-1)\n global_feat = self.global_conv(global_feat)\n \n # Branch 2: Direct features\n branch_feat = self.branch_conv(features)\n \n # Concatenate branches\n fused = torch.cat([global_feat, branch_feat], dim=1)\n \n # Classification\n output = self.classifier(fused)\n return output\n \n def predict(self, X):\n \"\"\"Scikit-learn style predict\"\"\"\n self.eval()\n with torch.no_grad():\n if isinstance(X, np.ndarray):\n X = torch.FloatTensor(X)\n outputs = self(X)\n _, predicted = torch.max(outputs, 1)\n return predicted.cpu().numpy()\n \n def score(self, X, y):\n \"\"\"Scikit-learn style score\"\"\"\n predictions = self.predict(X)\n if isinstance(y, torch.Tensor):\n y = y.cpu().numpy()\n return np.mean(predictions == y)\n\n\n# Microsoft Planetary Computer imports\nimport planetary_computer\nfrom pystac_client import Client\nfrom odc.stac import load as stac_load\n\n# Feature extraction\nfrom feature_extractor import get_feature_extractor\n\n\ndef train_model(\n bbox=[105.6, 9.3, 106.2, 9.8],\n time_range='2023-03-01/2023-05-31',\n max_scenes=12,\n cloud_cover=30,\n resolution=20,\n training_shapefile='train/ST_training data_updated_1130points_new.shp',\n model_type='xgboost',\n n_estimators=100,\n max_depth=20,\n learning_rate=0.1,\n use_gpu=True,\n use_cache=True,\n test_size=0.2,\n feature_mode='odc', # ODC mode: 8 features (NDVI stats + NDWI/NDBI/EVI) for better accuracy\n output_model_path=None,\n status_callback=None,\n cancel_check=None\n):\n \"\"\"\n Train a land classification model using Sentinel-2 and Sentinel-1 data\n \n Args:\n bbox: [min_lon, min_lat, max_lon, max_lat]\n time_range: \"YYYY-MM-DD/YYYY-MM-DD\"\n max_scenes: maximum number of scenes to load\n cloud_cover: maximum cloud cover percentage\n resolution: resolution in meters (e.g., 20)\n training_shapefile: path to training shapefile\n n_estimators: number of trees for XGBoost\n max_depth: maximum tree depth\n learning_rate: learning rate for XGBoost\n use_gpu: whether to use GPU for training\n output_model_path: path to save trained model (auto-generated if None)\n status_callback: Optional callback function to report progress\n cancel_check: Optional function that returns True if training should be cancelled\n test_size: Fraction of data to use for test set (0-1)\n feature_mode: 'simple' (3 features), 'temporal' (39 features), 'extended' (15 features), or 'odc' (8 features)\n \n Returns:\n Dictionary containing training results\n \"\"\"\n \n def update_status(message, progress=None):\n \"\"\"Helper to update status\"\"\"\n if status_callback:\n # Try calling with both arguments, fallback to just message\n try:\n status_callback(message, progress)\n except TypeError:\n status_callback(message)\n print(message)\n \n def check_cancellation():\n \"\"\"Check if training should be cancelled\"\"\"\n if cancel_check and cancel_check():\n raise InterruptedError(\"Training cancelled by user\")\n \n try:\n # Auto-generate output path if not provided\n if output_model_path is None:\n timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')\n output_model_path = f'model_train/model_{model_type}_{timestamp}.joblib'\n \n # ============ CACHE SYSTEM ============\n # Create cache directory\n cache_dir = Path(\"dataset_cache\")\n cache_dir.mkdir(exist_ok=True)\n \n # Generate cache key from parameters\n cache_params = f\"{bbox}_{time_range}_{max_scenes}_{cloud_cover}_{resolution}\"\n cache_key = hashlib.md5(cache_params.encode()).hexdigest()\n cache_file = cache_dir / f\"training_data_{cache_key}.joblib\"\n \n features = None\n labels = None\n \n # Initialize FeatureExtractor early (will be used for temporal/extended modes)\n update_status(f\"Initializing FeatureExtractor (mode={feature_mode})...\", 5)\n extractor = get_feature_extractor(mode=feature_mode)\n \n # Try to load from cache\n if use_cache and cache_file.exists():\n update_status(f\"📦 Đang load cache: {cache_file.name}...\", 5)\n try:\n cached_data = joblib.load(cache_file)\n features = cached_data['features']\n labels = cached_data['labels']\n \n # Validate cached data\n if len(features) == 0:\n update_status(\n f\"❌ Cache rỗng (0 samples)! Đây là cache từ lần training thất bại trước.\\n\"\n f\" Nguyên nhân: Bbox không overlap với shapefile HOẶC tất cả điểm bị NaN.\\n\"\n f\" Đang xóa cache lỗi và tải lại dữ liệu...\", 10\n )\n cache_file.unlink() # Delete empty cache\n features = None\n else:\n update_status(\n f\"✅ Loaded {len(features)} samples từ cache!\\n\"\n f\" ⚡ Đã bỏ qua download위성 data (tiết kiệm thời gian)\", 50\n )\n print(f\"[CACHE HIT] Using cached dataset with {len(features)} samples\")\n except Exception as e:\n update_status(f\"⚠️ Cache bị lỗi: {str(e)}\\n Đang tải lại dữ liệu mới...\", 10)\n features = None\n \n # If no cache or cache failed, download data\n if features is None:\n update_status(\"📡 Cache not found or disabled, downloading satellite data...\", 10)\n \n # Connect to Microsoft Planetary Computer\n update_status(\"Connecting to Microsoft Planetary Computer...\", 12)\n catalog = Client.open(\"https://planetarycomputer.microsoft.com/api/stac/v1\")\n check_cancellation()\n \n # Search for Sentinel-2 scenes\n update_status(\"Searching for Sentinel-2 scenes...\", 10)\n query_s2 = catalog.search(\n collections=[\"sentinel-2-l2a\"],\n bbox=bbox,\n datetime=time_range,\n query={\"eo:cloud_cover\": {\"lt\": cloud_cover}}\n )\n items_s2 = list(query_s2.item_collection())\n \n check_cancellation()\n \n # Limit scenes\n if len(items_s2) > max_scenes:\n step = len(items_s2) // max_scenes\n items_s2 = items_s2[::step][:max_scenes]\n \n update_status(f\"Found {len(items_s2)} Sentinel-2 scenes\", 20)\n \n # Sign and load Sentinel-2 data\n update_status(\"Loading Sentinel-2 data...\", 25)\n items_s2 = [planetary_computer.sign(item) for item in items_s2]\n \n # Load different bands based on feature mode\n if feature_mode == 'simple':\n bands_to_load = [\"B04\", \"B08\", \"SCL\"]\n else: # odc, temporal, or extended - all need full spectral bands\n bands_to_load = [\"B02\", \"B03\", \"B04\", \"B08\", \"B11\", \"SCL\"]\n \n update_status(f\"Loading bands: {bands_to_load} for mode={feature_mode}\", 26)\n \n ds_s2 = stac_load(\n items_s2,\n bands=bands_to_load,\n crs=\"EPSG:32648\",\n resolution=resolution,\n bbox=bbox,\n patch_url=planetary_computer.sign,\n fail_on_error=False,\n chunks={\"time\": 1, \"x\": 2048, \"y\": 2048}\n )\n \n # Debug: Print S2 data info\n print(f\"[DEBUG S2] Loaded S2 data\")\n print(f\"[DEBUG S2] Dimensions: {dict(ds_s2.dims)}\")\n print(f\"[DEBUG S2] Bands: {list(ds_s2.data_vars)}\")\n print(f\"[DEBUG S2] CRS: {ds_s2.rio.crs if hasattr(ds_s2, 'rio') else 'No CRS'}\")\n print(f\"[DEBUG S2] Spatial bounds: x=[{float(ds_s2.x.min())}, {float(ds_s2.x.max())}], y=[{float(ds_s2.y.min())}, {float(ds_s2.y.max())}]\")\n if 'time' in ds_s2.dims:\n print(f\"[DEBUG S2] Time range: {ds_s2.time.min().values} to {ds_s2.time.max().values}\")\n \n # Rename bands ONLY for simple mode (simple mode uses 'red', 'nir', 'scl' names)\n # Other modes (odc, extended, temporal) use original band names (B02, B03, B04, B08, B11, SCL)\n if feature_mode == 'simple' and \"B04\" in ds_s2 and \"red\" not in ds_s2:\n ds_s2 = ds_s2.rename({\"B04\": \"red\", \"B08\": \"nir\", \"SCL\": \"scl\"})\n print(f\"[DEBUG S2] Renamed bands for simple mode: B04→red, B08→nir, SCL→scl\")\n \n check_cancellation()\n \n # Search for Sentinel-1 scenes\n update_status(\"Searching for Sentinel-1 scenes...\", 35)\n query_s1 = catalog.search(\n collections=[\"sentinel-1-rtc\"],\n bbox=bbox,\n datetime=time_range,\n )\n items_s1 = list(query_s1.item_collection())\n \n # Limit scenes\n if len(items_s1) > max_scenes:\n step = len(items_s1) // max_scenes\n items_s1 = items_s1[::step][:max_scenes]\n \n update_status(f\"Found {len(items_s1)} Sentinel-1 scenes\", 40)\n \n # Sign and load Sentinel-1 data\n update_status(\"Loading Sentinel-1 data...\", 45)\n items_s1 = [planetary_computer.sign(item) for item in items_s1]\n ds_s1 = stac_load(\n items_s1,\n bands=[\"vv\", \"vh\"],\n crs=\"EPSG:32648\",\n resolution=resolution,\n bbox=bbox,\n patch_url=planetary_computer.sign,\n fail_on_error=False,\n chunks={\"time\": 1, \"x\": 2048, \"y\": 2048}\n )\n \n # Convert to dB\n ds_s1['vv_db'] = 10 * np.log10(ds_s1['vv'].where(ds_s1['vv'] > 0))\n ds_s1['vh_db'] = 10 * np.log10(ds_s1['vh'].where(ds_s1['vh'] > 0))\n \n # Debug: Print S1 data info\n print(f\"[DEBUG S1] Loaded S1 data\")\n print(f\"[DEBUG S1] Dimensions: {dict(ds_s1.dims)}\")\n print(f\"[DEBUG S1] Bands: {list(ds_s1.data_vars)}\")\n print(f\"[DEBUG S1] Spatial bounds: x=[{float(ds_s1.x.min())}, {float(ds_s1.x.max())}], y=[{float(ds_s1.y.min())}, {float(ds_s1.y.max())}]\")\n \n check_cancellation()\n \n # Load training data\n update_status(\"Loading training data...\", 55)\n \n # Normalize training shapefile path\n # If path doesn't start with 'train/', add it\n if not training_shapefile.startswith('train/'):\n training_shapefile = f'train/{training_shapefile}'\n \n print(f\"[DEBUG] Original training shapefile: {training_shapefile}\")\n print(f\"[DEBUG] Current working directory: {os.getcwd()}\")\n \n # Try to find the file with exact name first\n if not os.path.exists(training_shapefile):\n # File not found, try to find similar files in train directory\n train_dir = Path('train')\n if train_dir.exists():\n # List all .shp files\n shp_files = list(train_dir.glob('*.shp'))\n print(f\"[DEBUG] Available shapefile files in train/:\")\n for f in shp_files:\n print(f\" - {f.name}\")\n \n # Try to find a matching file (case-insensitive, ignore underscores vs spaces)\n filename_normalized = os.path.basename(training_shapefile).lower().replace('_', ' ')\n for shp_file in shp_files:\n if shp_file.name.lower().replace('_', ' ') == filename_normalized:\n print(f\"[DEBUG] Found matching file: {shp_file}\")\n training_shapefile = str(shp_file)\n break\n \n if not os.path.exists(training_shapefile):\n raise FileNotFoundError(\n f\"Training shapefile not found: {training_shapefile}\\n\"\n f\"Available files: {[f.name for f in shp_files]}\"\n )\n else:\n raise FileNotFoundError(f\"Train directory not found: {train_dir}\")\n \n print(f\"[DEBUG] Final training shapefile path: {training_shapefile}\")\n print(f\"[DEBUG] File exists: {os.path.exists(training_shapefile)}\")\n \n train_gdf = gpd.read_file(training_shapefile)\n \n # Print initial shapefile info\n update_status(f\"📍 Loaded {len(train_gdf)} points from shapefile\", 56)\n print(f\"[DEBUG] Shapefile CRS: {train_gdf.crs}\")\n print(f\"[DEBUG] Shapefile bounds: {train_gdf.total_bounds}\")\n \n # Convert to WGS84 first (if not already) to match bbox coordinates\n original_crs = train_gdf.crs\n if train_gdf.crs and train_gdf.crs.to_epsg() != 4326:\n print(f\"📍 Converting training shapefile from {train_gdf.crs} to WGS84\")\n train_gdf = train_gdf.to_crs(\"EPSG:4326\")\n print(f\"[DEBUG] WGS84 bounds: {train_gdf.total_bounds}\")\n \n # Check bbox overlap in WGS84\n shp_bounds = train_gdf.total_bounds # [minx, miny, maxx, maxy]\n bbox_wgs84 = bbox # [min_lon, min_lat, max_lon, max_lat]\n \n # Check if there's overlap\n overlap_x = not (shp_bounds[2] < bbox_wgs84[0] or shp_bounds[0] > bbox_wgs84[2])\n overlap_y = not (shp_bounds[3] < bbox_wgs84[1] or shp_bounds[1] > bbox_wgs84[3])\n \n if not (overlap_x and overlap_y):\n update_status(f\"⚠️ WARNING: Shapefile and bbox may not overlap!\", 57)\n print(f\"[WARNING] Shapefile bounds (WGS84): {shp_bounds}\")\n print(f\"[WARNING] Requested bbox (WGS84): {bbox_wgs84}\")\n print(f\"[WARNING] This may result in 0 training samples!\")\n else:\n # Crop to bbox to see how many points are actually in the region\n train_gdf_cropped = train_gdf.cx[bbox_wgs84[0]:bbox_wgs84[2], bbox_wgs84[1]:bbox_wgs84[3]]\n update_status(f\"📍 {len(train_gdf_cropped)} points within bbox\", 57)\n if len(train_gdf_cropped) == 0:\n raise ValueError(\n f\"No training points found within bbox!\\n\"\n f\"Shapefile bounds: {shp_bounds}\\n\"\n f\"Requested bbox: {bbox_wgs84}\\n\"\n f\"Please adjust bbox to cover your training data.\"\n )\n \n # Then convert to UTM Zone 48N (EPSG:32648) for extraction\n if train_gdf.crs.to_epsg() != 32648:\n print(f\"📍 Converting training shapefile from WGS84 to UTM Zone 48N (EPSG:32648)\")\n train_gdf = train_gdf.to_crs('EPSG:32648')\n print(f\"[DEBUG] UTM bounds: {train_gdf.total_bounds}\")\n \n # Auto-detect label column\n label_column = None\n for col in ['HT_code', 'Ma_LU', 'LU2022', 'Hientrang', 'class', 'Class', 'CLASS']:\n if col in train_gdf.columns:\n label_column = col\n break\n \n if label_column is None:\n raise ValueError(f\"Cannot find label column in shapefile. Available: {list(train_gdf.columns)}\")\n \n # Extract features using FeatureExtractor\n update_status(\"Extracting features from satellite data...\", 60)\n \n print(f\"[DEBUG] Starting feature extraction...\")\n print(f\"[DEBUG] Feature mode: {feature_mode}\")\n print(f\"[DEBUG] Training GDF has {len(train_gdf)} points\")\n print(f\"[DEBUG] Training GDF CRS: {train_gdf.crs}\")\n print(f\"[DEBUG] Training GDF bounds (UTM): {train_gdf.total_bounds}\")\n print(f\"[DEBUG] Label column: {label_column}\")\n \n if feature_mode == 'simple':\n # For simple mode: calculate NDVI first\n ndvi = (ds_s2['nir'] - ds_s2['red']) / (ds_s2['nir'] + ds_s2['red'] + 1e-8)\n # Apply cloud mask\n cloud_mask = ds_s2['scl'].isin([1, 3, 8, 9, 10])\n ndvi_masked = ndvi.where(~cloud_mask)\n \n print(f\"[DEBUG] NDVI shape: {ndvi_masked.shape}\")\n print(f\"[DEBUG] NDVI range: [{float(ndvi_masked.min())}, {float(ndvi_masked.max())}]\")\n \n # Extract features at training points\n features = []\n labels = []\n failed_extractions = 0\n \n # Test first point to see what's happening\n first_point = train_gdf.iloc[0]\n print(f\"[DEBUG] Testing first point:\")\n print(f\" Coords: ({first_point.geometry.x}, {first_point.geometry.y})\")\n print(f\" Label: {first_point[label_column]}\")\n \n for idx, row in train_gdf.iterrows():\n point = row.geometry\n x_coord = point.x\n y_coord = point.y\n label = row[label_column]\n \n try:\n ndvi_val = ndvi_masked.sel(x=x_coord, y=y_coord, method='nearest').mean(dim='time').values\n vh_val = ds_s1['vh_db'].sel(x=x_coord, y=y_coord, method='nearest').mean(dim='time').values\n vv_val = ds_s1['vv_db'].sel(x=x_coord, y=y_coord, method='nearest').mean(dim='time').values\n \n feature_vec = [float(ndvi_val), float(vh_val), float(vv_val)]\n \n # Debug first few points\n if idx < 3:\n print(f\"[DEBUG] Point {idx}: coords=({x_coord:.2f}, {y_coord:.2f}), ndvi={ndvi_val:.3f}, vh={vh_val:.3f}, vv={vv_val:.3f}\")\n \n if not np.isnan(feature_vec).any():\n features.append(feature_vec)\n labels.append(label)\n else:\n failed_extractions += 1\n if idx < 3:\n print(f\"[DEBUG] Point {idx} has NaN: {feature_vec}\")\n except Exception as e:\n failed_extractions += 1\n if idx < 3:\n print(f\"[DEBUG] Point {idx} extraction failed: {e}\")\n continue\n \n if failed_extractions > 0:\n update_status(f\"⚠️ {failed_extractions}/{len(train_gdf)} points had NaN/missing data\", 65)\n \n features = np.array(features)\n labels = np.array(labels)\n \n elif feature_mode in ['odc', 'extended']:\n # For odc/extended: Extract points FIRST, then compute features to save RAM\n update_status(f\"Extracting points from {feature_mode} raster before computing features...\", 62)\n \n # Apply cloud mask first\n if 'SCL' in ds_s2:\n scl_band = ds_s2['SCL']\n cloud_mask = scl_band.isin([1, 3, 8, 9, 10])\n for band in ds_s2.data_vars:\n if band != 'SCL':\n ds_s2[band] = ds_s2[band].where(~cloud_mask)\n \n # Use advanced indexing to extract exactly the 1130 points\n x_coords = xr.DataArray(train_gdf.geometry.x.values, dims=\"point\")\n y_coords = xr.DataArray(train_gdf.geometry.y.values, dims=\"point\")\n \n update_status(\"Downloading and extracting point data from Dask array (this is fast)...\", 65)\n points_s2 = ds_s2.sel(x=x_coords, y=y_coords, method='nearest').compute()\n \n update_status(\"Computing spectral indices for extracted points...\", 66)\n # Extract features using FeatureExtractor for ONLY the extracted points\n raster_features = extractor.extract(\n s2_data=points_s2,\n vh_data=None, # ODC/extended don't use radar in aggregate\n vv_data=None\n )\n \n print(f\"[DEBUG] Extracted point features: shape={raster_features.shape}\")\n print(f\"[DEBUG] Feature range: [{np.nanmin(raster_features)}, {np.nanmax(raster_features)}]\")\n \n features = []\n labels = []\n failed_extractions = 0\n \n for idx, row in train_gdf.iterrows():\n label = row[label_column]\n \n if idx < len(raster_features):\n feature_vec = raster_features[idx]\n \n if idx < 3:\n print(f\"[DEBUG] Point {idx}: features={feature_vec[:3]}...\")\n \n if not np.isnan(feature_vec).any():\n features.append(feature_vec)\n labels.append(label)\n else:\n failed_extractions += 1\n if idx < 3:\n print(f\"[DEBUG] Point {idx} has NaN features\")\n else:\n failed_extractions += 1\n \n if failed_extractions > 0:\n update_status(f\"⚠️ {failed_extractions}/{len(train_gdf)} points had NaN/missing data\", 68)\n \n features = np.array(features)\n labels = np.array(labels)\n \n else: # temporal mode\n # Apply cloud mask for temporal/extended modes\n if 'scl' in ds_s2 or 'SCL' in ds_s2:\n scl_band = ds_s2['scl'] if 'scl' in ds_s2 else ds_s2['SCL']\n cloud_mask = scl_band.isin([1, 3, 8, 9, 10])\n for band in ds_s2.data_vars:\n if band != 'scl' and band != 'SCL':\n ds_s2[band] = ds_s2[band].where(~cloud_mask)\n \n # Extract features at training points\n features = []\n labels = []\n failed_extractions = 0\n \n for idx, row in train_gdf.iterrows():\n point = row.geometry\n x_coord = point.x\n y_coord = point.y\n label = row[label_column]\n \n try:\n # Extract point data from S2\n point_s2 = ds_s2.sel(x=x_coord, y=y_coord, method='nearest')\n \n # Extract point data from S1\n vh_val = ds_s1['vh_db'].sel(x=x_coord, y=y_coord, method='nearest').mean(dim='time').values\n vv_val = ds_s1['vv_db'].sel(x=x_coord, y=y_coord, method='nearest').mean(dim='time').values\n \n # Create minimal dataset for feature extraction\n point_data = xr.Dataset({\n 'B02': point_s2['B02'],\n 'B03': point_s2['B03'],\n 'B04': point_s2['B04'],\n 'B08': point_s2['B08'],\n 'B11': point_s2['B11']\n })\n \n # Create VH/VV DataArrays (without spatial dims, just time if exists)\n if 'time' in point_data.dims:\n vh_da = xr.DataArray([vh_val] * len(point_data.time), dims=['time'])\n vv_da = xr.DataArray([vv_val] * len(point_data.time), dims=['time'])\n else:\n vh_da = xr.DataArray([vh_val])\n vv_da = xr.DataArray([vv_val])\n \n # Extract features using FeatureExtractor\n # Note: extractor.extract returns (n_pixels, n_features), we take first row\n feature_vec = extractor.extract(\n s2_data=point_data,\n vh_data=vh_da,\n vv_data=vv_da\n )\n \n # If feature_vec is 2D, take first row\n if len(feature_vec.shape) > 1:\n feature_vec = feature_vec[0]\n \n if not np.isnan(feature_vec).any():\n features.append(feature_vec)\n labels.append(label)\n else:\n failed_extractions += 1\n except Exception as e:\n failed_extractions += 1\n continue\n \n if failed_extractions > 0:\n update_status(f\"⚠️ {failed_extractions}/{len(train_gdf)} points had NaN/missing data\", 65)\n \n features = np.array(features)\n labels = np.array(labels)\n \n check_cancellation()\n \n update_status(f\"Extracted {len(features)} valid training samples\", 70)\n \n # ============ VALIDATE SAMPLES ============\n if len(features) == 0:\n error_msg = (\n f\"❌ No valid training samples extracted!\\n\"\n f\"Possible reasons:\\n\"\n f\"1. Training shapefile points don't overlap with bbox: {bbox}\\n\"\n f\"2. All points have NaN values (cloud cover, missing data)\\n\"\n f\"3. Coordinate system mismatch\\n\"\n f\"Suggestions:\\n\"\n f\"- Check if bbox matches your region\\n\"\n f\"- Try a different time range with less cloud cover\\n\"\n f\"- Verify training shapefile coordinates are correct\"\n )\n raise ValueError(error_msg)\n \n # Warn if very few samples\n if len(features) < 20:\n update_status(f\"⚠️ Warning: Only {len(features)} samples extracted. Results may be unreliable.\", 70)\n \n # ============ SAVE TO CACHE ============\n if use_cache:\n update_status(f\"💾 Saving dataset to cache for future use...\", 72)\n try:\n cache_data = {\n 'features': features,\n 'labels': labels,\n 'bbox': bbox,\n 'time_range': time_range,\n 'resolution': resolution,\n 'feature_mode': feature_mode,\n 'timestamp': datetime.now().isoformat()\n }\n joblib.dump(cache_data, cache_file)\n update_status(f\"✅ Cached to {cache_file.name}\", 75)\n except Exception as e:\n update_status(f\"⚠️ Cache save failed: {str(e)}\", 75)\n \n # Validate samples after cache loading\n if len(features) == 0:\n error_msg = (\n f\"❌ No training samples available!\\n\"\n f\"The cached or loaded dataset is empty.\\n\"\n f\"Please try:\\n\"\n f\"1. Clear cache and reload data\\n\"\n f\"2. Check training shapefile and bbox overlap\\n\"\n f\"3. Adjust time range and cloud cover settings\"\n )\n raise ValueError(error_msg)\n \n # Encode labels\n label_encoder = LabelEncoder()\n labels_encoded = label_encoder.fit_transform(labels)\n \n # Split data\n X_train, X_test, y_train, y_test = train_test_split(\n features, labels_encoded, test_size=test_size, random_state=42, stratify=labels_encoded\n )\n \n # Train model based on selected type\n update_status(f\"Training {model_type.upper()} model...\", 75)\n \n device = 'cuda:0' if use_gpu else 'cpu'\n \n if model_type == 'xgboost':\n model = XGBClassifier(\n n_estimators=n_estimators,\n max_depth=max_depth,\n learning_rate=learning_rate,\n device=device if use_gpu else 'cpu',\n tree_method='hist',\n random_state=42,\n eval_metric='mlogloss',\n verbosity=0\n )\n elif model_type == 'random_forest':\n if use_gpu:\n model = XGBClassifier(\n n_estimators=n_estimators,\n max_depth=max_depth,\n tree_method='hist',\n device='cuda:0',\n random_state=42,\n n_jobs=-1,\n verbosity=0\n )\n else:\n model = RandomForestClassifier(\n n_estimators=n_estimators,\n max_depth=max_depth,\n random_state=42,\n n_jobs=-1, # Use all cores\n verbose=0\n )\n elif model_type == 'decision_tree':\n model = DecisionTreeClassifier(\n max_depth=max_depth,\n random_state=42\n )\n elif model_type == 'lightgbm':\n model = LGBMClassifier(\n n_estimators=n_estimators if n_estimators else 300,\n max_depth=max_depth if max_depth else -1,\n learning_rate=learning_rate,\n class_weight='balanced',\n random_state=42,\n device='gpu' if use_gpu else 'cpu'\n )\n elif model_type == 'svm':\n model = SVC(\n kernel='rbf',\n random_state=42,\n verbose=False\n )\n elif model_type == 'cnn':\n if not PYTORCH_AVAILABLE:\n raise ImportError(\"PyTorch is required for CNN. Install: pip install torch\")\n \n # CNN requires reshaping data\n n_features = X_train.shape[1]\n n_classes = len(np.unique(y_train))\n \n # Build PyTorch CNN model\n device = torch.device('cuda' if torch.cuda.is_available() and use_gpu else 'cpu')\n update_status(f\"Building CNN model on {device}...\", 75)\n \n model = CNNClassifier(n_features, n_classes).to(device)\n \n # Convert to PyTorch tensors\n X_train_tensor = torch.FloatTensor(X_train).unsqueeze(1) # Add channel dim: (N, 1, features)\n y_train_tensor = torch.LongTensor(y_train)\n X_test_tensor = torch.FloatTensor(X_test).unsqueeze(1)\n y_test_tensor = torch.LongTensor(y_test)\n \n # Create data loaders\n train_dataset = TensorDataset(X_train_tensor, y_train_tensor)\n train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)\n \n # Loss and optimizer\n criterion = nn.CrossEntropyLoss()\n optimizer = optim.Adam(model.parameters(), lr=0.001)\n \n # Train CNN\n update_status(\"Training CNN model with PyTorch...\", 80)\n epochs = min(50, n_estimators // 2) # Use n_estimators as epochs\n \n model.train()\n for epoch in range(epochs):\n epoch_loss = 0.0\n for batch_X, batch_y in train_loader:\n batch_X, batch_y = batch_X.to(device), batch_y.to(device)\n \n optimizer.zero_grad()\n outputs = model(batch_X)\n loss = criterion(outputs, batch_y)\n loss.backward()\n optimizer.step()\n \n epoch_loss += loss.item()\n \n if (epoch + 1) % 10 == 0:\n avg_loss = epoch_loss / len(train_loader)\n update_status(f\"CNN Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}\", 80 + (epoch / epochs) * 10)\n \n # Move model to CPU for saving (compatible with non-GPU systems)\n model = model.cpu()\n model.device_used = str(device)\n \n elif model_type == 'swin-unet':\n if not PYTORCH_AVAILABLE:\n raise ImportError(\"PyTorch is required for Swin-UNet. Install: pip install torch torchvision\")\n \n n_features = X_train.shape[1]\n n_classes = len(np.unique(y_train))\n \n device = torch.device('cuda' if torch.cuda.is_available() and use_gpu else 'cpu')\n update_status(f\"Building Swin-UNet model on {device}...\", 75)\n \n model = SwinUNetClassifier(n_features, n_classes, embed_dim=128).to(device)\n \n # Convert to PyTorch tensors (no unsqueeze needed for Swin-UNet)\n X_train_tensor = torch.FloatTensor(X_train)\n y_train_tensor = torch.LongTensor(y_train)\n X_test_tensor = torch.FloatTensor(X_test)\n y_test_tensor = torch.LongTensor(y_test)\n \n # Create data loaders\n train_dataset = TensorDataset(X_train_tensor, y_train_tensor)\n train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)\n \n # Calculate class weights for imbalanced data\n class_counts = np.bincount(y_train)\n class_weights = 1.0 / (class_counts + 1e-6) # Avoid division by zero\n class_weights = class_weights / class_weights.sum() * len(class_counts) # Normalize\n class_weights_tensor = torch.FloatTensor(class_weights).to(device)\n \n print(f\"[SWIN-UNET] Class distribution: {class_counts}\")\n print(f\"[SWIN-UNET] Class weights: {class_weights}\")\n \n # Loss with class weights and optimizer with weight decay\n criterion = nn.CrossEntropyLoss(weight=class_weights_tensor)\n optimizer = optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=0.01)\n \n # LR scheduler for better convergence\n scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)\n \n # Early stopping to prevent overfitting\n best_val_loss = float('inf')\n patience = 10\n patience_counter = 0\n \n # Train Swin-UNet\n update_status(\"Training Swin-UNet model with PyTorch (with class weights)...\", 80)\n epochs = min(60, n_estimators // 2) # Swin-UNet benefits from more epochs\n \n # Validation dataset\n val_dataset = TensorDataset(X_test_tensor, y_test_tensor)\n val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)\n \n model.train()\n for epoch in range(epochs):\n # Training phase\n model.train()\n epoch_loss = 0.0\n for batch_X, batch_y in train_loader:\n batch_X, batch_y = batch_X.to(device), batch_y.to(device)\n \n optimizer.zero_grad()\n outputs = model(batch_X)\n loss = criterion(outputs, batch_y)\n loss.backward()\n \n # Gradient clipping to prevent exploding gradients\n torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n \n optimizer.step()\n \n epoch_loss += loss.item()\n \n scheduler.step()\n \n # Validation phase\n model.eval()\n val_loss = 0.0\n correct = 0\n total = 0\n with torch.no_grad():\n for batch_X, batch_y in val_loader:\n batch_X, batch_y = batch_X.to(device), batch_y.to(device)\n outputs = model(batch_X)\n loss = criterion(outputs, batch_y)\n val_loss += loss.item()\n \n _, predicted = torch.max(outputs, 1)\n total += batch_y.size(0)\n correct += (predicted == batch_y).sum().item()\n \n avg_train_loss = epoch_loss / len(train_loader)\n avg_val_loss = val_loss / len(val_loader)\n val_acc = 100 * correct / total\n lr = optimizer.param_groups[0]['lr']\n \n if (epoch + 1) % 5 == 0:\n update_status(f\"Swin-UNet Epoch {epoch+1}/{epochs}, Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}, Val Acc: {val_acc:.2f}%, LR: {lr:.6f}\", 80 + (epoch / epochs) * 10)\n print(f\"[SWIN-UNET] Epoch {epoch+1}/{epochs} - Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}, Val Acc: {val_acc:.2f}%\")\n \n # Early stopping check\n if avg_val_loss < best_val_loss:\n best_val_loss = avg_val_loss\n patience_counter = 0\n else:\n patience_counter += 1\n if patience_counter >= patience:\n print(f\"[SWIN-UNET] Early stopping at epoch {epoch+1} (best val loss: {best_val_loss:.4f})\")\n update_status(f\"Swin-UNet early stopped at epoch {epoch+1}\", 90)\n break\n \n model = model.cpu()\n model.device_used = str(device)\n \n elif model_type == 'mobilenet-lraspp':\n if not PYTORCH_AVAILABLE:\n raise ImportError(\"PyTorch is required for MobileNetV3 + LR-ASPP. Install: pip install torch torchvision\")\n \n n_features = X_train.shape[1]\n n_classes = len(np.unique(y_train))\n \n device = torch.device('cuda' if torch.cuda.is_available() and use_gpu else 'cpu')\n update_status(f\"Building MobileNetV3 + LR-ASPP model on {device}...\", 75)\n \n model = MobileNetLRASPPClassifier(n_features, n_classes).to(device)\n \n # Convert to PyTorch tensors\n X_train_tensor = torch.FloatTensor(X_train)\n y_train_tensor = torch.LongTensor(y_train)\n X_test_tensor = torch.FloatTensor(X_test)\n y_test_tensor = torch.LongTensor(y_test)\n \n # Create data loaders\n train_dataset = TensorDataset(X_train_tensor, y_train_tensor)\n train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) # Larger batch for efficiency\n \n # Calculate class weights for imbalanced data\n class_counts = np.bincount(y_train)\n class_weights = 1.0 / (class_counts + 1e-6)\n class_weights = class_weights / class_weights.sum() * len(class_counts)\n class_weights_tensor = torch.FloatTensor(class_weights).to(device)\n \n print(f\"[MOBILENET] Class distribution: {class_counts}\")\n print(f\"[MOBILENET] Class weights: {class_weights}\")\n \n # Loss with class weights\n criterion = nn.CrossEntropyLoss(weight=class_weights_tensor)\n optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=0.0001)\n \n # LR scheduler\n scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=5)\n \n # Early stopping\n best_val_loss = float('inf')\n patience = 10\n patience_counter = 0\n \n # Train MobileNetV3 + LR-ASPP\n update_status(\"Training MobileNetV3 + LR-ASPP model with PyTorch...\", 80)\n epochs = min(60, n_estimators // 2)\n \n # Validation dataset\n val_dataset = TensorDataset(X_test_tensor, y_test_tensor)\n val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)\n \n model.train()\n for epoch in range(epochs):\n # Training phase\n model.train()\n epoch_loss = 0.0\n for batch_X, batch_y in train_loader:\n batch_X, batch_y = batch_X.to(device), batch_y.to(device)\n \n optimizer.zero_grad()\n outputs = model(batch_X)\n loss = criterion(outputs, batch_y)\n loss.backward()\n \n # Gradient clipping\n torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n \n optimizer.step()\n epoch_loss += loss.item()\n \n # Validation phase\n model.eval()\n val_loss = 0.0\n correct = 0\n total = 0\n with torch.no_grad():\n for batch_X, batch_y in val_loader:\n batch_X, batch_y = batch_X.to(device), batch_y.to(device)\n outputs = model(batch_X)\n loss = criterion(outputs, batch_y)\n val_loss += loss.item()\n \n _, predicted = torch.max(outputs, 1)\n total += batch_y.size(0)\n correct += (predicted == batch_y).sum().item()\n \n avg_train_loss = epoch_loss / len(train_loader)\n avg_val_loss = val_loss / len(val_loader)\n val_acc = 100 * correct / total\n \n # Update learning rate\n scheduler.step(avg_val_loss)\n lr = optimizer.param_groups[0]['lr']\n \n if (epoch + 1) % 5 == 0:\n update_status(f\"MobileNet Epoch {epoch+1}/{epochs}, Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}, Val Acc: {val_acc:.2f}%, LR: {lr:.6f}\", 80 + (epoch / epochs) * 10)\n print(f\"[MOBILENET] Epoch {epoch+1}/{epochs} - Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}, Val Acc: {val_acc:.2f}%\")\n \n # Early stopping\n if avg_val_loss < best_val_loss:\n best_val_loss = avg_val_loss\n patience_counter = 0\n else:\n patience_counter += 1\n if patience_counter >= patience:\n print(f\"[MOBILENET] Early stopping at epoch {epoch+1} (best val loss: {best_val_loss:.4f})\")\n update_status(f\"MobileNet early stopped at epoch {epoch+1}\", 90)\n break\n \n model = model.cpu()\n model.device_used = str(device)\n \n else:\n raise ValueError(f\"Unknown model type: {model_type}. Choose: xgboost, random_forest, decision_tree, svm, cnn, swin-unet, mobilenet-lraspp\")\n \n # Fit non-neural-network models\n if model_type not in ['cnn', 'swin-unet', 'mobilenet-lraspp']:\n model.fit(X_train, y_train)\n \n # Evaluate\n update_status(\"Evaluating model...\", 90)\n if model_type in ['cnn', 'swin-unet', 'mobilenet-lraspp']:\n # PyTorch models evaluation\n train_score = model.score(X_train, y_train)\n test_score = model.score(X_test, y_test)\n y_pred = model.predict(X_test)\n else:\n train_score = model.score(X_train, y_train)\n test_score = model.score(X_test, y_test)\n y_pred = model.predict(X_test)\n \n # Generate classification report and confusion matrix\n update_status(\"Generating classification report...\", 92)\n class_names = label_encoder.classes_.tolist()\n \n # Classification report as dict\n from sklearn.metrics import classification_report, confusion_matrix, accuracy_score\n cls_report = classification_report(y_test, y_pred, target_names=class_names, output_dict=True, zero_division=0)\n \n # Confusion matrix\n conf_matrix = confusion_matrix(y_test, y_pred).tolist()\n \n # Save model using ModelManager\n update_status(\"Saving model...\", 95)\n os.makedirs(os.path.dirname(output_model_path), exist_ok=True)\n \n # Get feature names from extractor\n if feature_mode == 'temporal':\n # Calculate n_timesteps from data\n n_timesteps = len(features[0]) // 3 - 1 # (NDVI + NDWI + NDBI) * n_timesteps + 3 radar features\n feature_names = extractor.get_feature_names(n_timesteps=n_timesteps)\n else:\n feature_names = extractor.get_feature_names()\n \n # Prepare metadata\n info = {\n \"timestamp\": datetime.now().isoformat(),\n \"data_source\": \"Microsoft Planetary Computer STAC\",\n \"collections\": [\"sentinel-2-l2a\", \"sentinel-1-rtc\"],\n \"features\": feature_names,\n \"feature_mode\": feature_mode,\n \"training_samples\": len(X_train),\n \"testing_samples\": len(X_test),\n \"test_size\": test_size,\n \"train_accuracy\": float(train_score),\n \"test_accuracy\": float(test_score),\n \"model_type\": model_type,\n \"device\": device if model_type == 'xgboost' else 'cpu',\n \"n_estimators\": n_estimators if model_type in ['xgboost', 'random_forest', 'lightgbm', 'cnn', 'swin-unet', 'mobilenet-lraspp'] else None,\n \"max_depth\": max_depth if model_type not in ['cnn', 'swin-unet', 'mobilenet-lraspp'] else None,\n \"learning_rate\": learning_rate if model_type in ['xgboost', 'lightgbm', 'swin-unet', 'mobilenet-lraspp'] else None,\n \"epochs\": min(50, n_estimators // 2) if model_type == 'cnn' else (min(60, n_estimators // 2) if model_type in ['swin-unet', 'mobilenet-lraspp'] else None),\n \"n_features\": X_train.shape[1],\n \"n_classes\": len(np.unique(y_train)),\n \"class_names\": class_names,\n \"classification_report\": cls_report,\n \"confusion_matrix\": conf_matrix,\n \"bbox\": bbox,\n \"time_range\": time_range,\n \"resolution\": resolution\n }\n \n # Use ModelManager to save\n from model_manager import get_model_manager\n model_manager = get_model_manager()\n model_filename = os.path.basename(output_model_path)\n model_manager.save_model(\n model=model,\n metadata=info,\n model_filename=model_filename,\n label_encoder=label_encoder\n )\n \n # Construct info path (model manager saves it in model_train/)\n info_path = os.path.join('model_train', model_filename.replace('.joblib', '_info.json'))\n \n update_status(\"Training complete!\", 100)\n \n return {\n \"success\": True,\n \"model_path\": output_model_path,\n \"info_path\": info_path,\n \"train_accuracy\": train_score,\n \"test_accuracy\": test_score,\n \"training_samples\": len(X_train),\n \"testing_samples\": len(X_test),\n \"test_size\": test_size,\n \"classes\": class_names,\n \"classification_report\": cls_report,\n \"confusion_matrix\": conf_matrix,\n \"model_type\": model_type,\n \"bbox\": bbox,\n \"time_range\": time_range,\n \"resolution\": resolution\n }\n \n except InterruptedError as e:\n update_status(f\"Cancelled: {str(e)}\", -1)\n return {\n \"success\": False,\n \"error\": str(e),\n \"cancelled\": True\n }\n \n except Exception as e:\n update_status(f\"Error: {str(e)}\", -1)\n return {\n \"success\": False,\n \"error\": str(e)\n }\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile train_land_decision_tree_gpu.py\nfrom train_module import train_model\n\ndef main():\n print(\"🚀 BẮT ĐẦU HUẤN LUYỆN MÔ HÌNH DECISION TREE (GPU & CACHE)\")\n \n params = {\n 'bbox': [105.5, 9.2, 106.3, 10.0],\n 'time_range': '2023-01-01/2023-04-30',\n 'model_type': 'decision_tree',\n 'feature_mode': 'extended',\n 'use_cache': True,\n 'use_gpu': True,\n 'output_model_path': 'land_classification_model/model_decision_tree_auto.joblib', 'max_depth': 12\n }\n \n try:\n res = train_model(**params)\n if res.get('success'):\n print(f\"✅ Hoàn thành! Accuracy: {res.get('test_accuracy', 0.0):.4f}\")\n else:\n print(f\"❌ Thất bại: {res.get('error', 'Unknown')}\")\n except Exception as e:\n print(f\"❌ Lỗi: {e}\")\n\nif __name__ == \"__main__\":\n main()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Bước 3: Chạy tiến trình tải ảnh vệ tinh và tạo Cache" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!python train_land_decision_tree_gpu.py" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Bước 4: Tải file Cache về máy" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from google.colab import files\n", "import glob\n", "\n", "cache_files = glob.glob(\"dataset_cache/*.joblib\")\n", "if cache_files:\n", " latest_cache = max(cache_files, key=os.path.getctime)\n", " print(f\"Đang tải file {latest_cache} về máy...\")\n", " files.download(latest_cache)\n", "else:\n", " print(\"Chưa tìm thấy file cache. Hãy chắc chắn bước 3 đã chạy thành công!\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 4 }