hoàn thành cơ bản các chức năng
This commit is contained in:
Binary file not shown.
Binary file not shown.
+184
-4
@@ -68,11 +68,15 @@ class TrainingConfig(BaseModel):
|
|||||||
resolution: int = 20 # 10m hoặc 20m
|
resolution: int = 20 # 10m hoặc 20m
|
||||||
|
|
||||||
# Model parameters
|
# Model parameters
|
||||||
|
model_type: str = "xgboost" # xgboost, random_forest, decision_tree, svm, cnn
|
||||||
n_estimators: int = 100
|
n_estimators: int = 100
|
||||||
max_depth: int = 20
|
max_depth: int = 20
|
||||||
learning_rate: float = 0.1
|
learning_rate: float = 0.1
|
||||||
use_gpu: bool = True
|
use_gpu: bool = True
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
use_cache: bool = True # Cache dataset để test nhanh hơn
|
||||||
|
|
||||||
# Training data
|
# Training data
|
||||||
training_shapefile: str = "train/ST_training data_updated_1130points_new.shp"
|
training_shapefile: str = "train/ST_training data_updated_1130points_new.shp"
|
||||||
|
|
||||||
@@ -205,6 +209,90 @@ async def stop_training():
|
|||||||
return {"message": "Đang dừng training..."}
|
return {"message": "Đang dừng training..."}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/cache/clear")
|
||||||
|
async def clear_cache():
|
||||||
|
"""Xóa cache dataset"""
|
||||||
|
import shutil
|
||||||
|
cache_dir = Path("dataset_cache")
|
||||||
|
|
||||||
|
if not cache_dir.exists():
|
||||||
|
return {"message": "Không có cache để xóa", "deleted": 0}
|
||||||
|
|
||||||
|
# Count files
|
||||||
|
cache_files = list(cache_dir.glob("*.joblib"))
|
||||||
|
count = len(cache_files)
|
||||||
|
|
||||||
|
# Delete all cache files
|
||||||
|
for cache_file in cache_files:
|
||||||
|
try:
|
||||||
|
cache_file.unlink()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {"message": f"Đã xóa {count} file cache", "deleted": count}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/cache/info")
|
||||||
|
async def get_cache_info():
|
||||||
|
"""Lấy thông tin về cache với metadata đầy đủ"""
|
||||||
|
cache_dir = Path("dataset_cache")
|
||||||
|
|
||||||
|
if not cache_dir.exists():
|
||||||
|
return {"exists": False, "files": [], "total_size_mb": 0}
|
||||||
|
|
||||||
|
cache_files = []
|
||||||
|
total_size = 0
|
||||||
|
|
||||||
|
for cache_file in cache_dir.glob("*.joblib"):
|
||||||
|
size = cache_file.stat().st_size
|
||||||
|
total_size += size
|
||||||
|
|
||||||
|
# Try to load metadata from cache
|
||||||
|
metadata = {}
|
||||||
|
try:
|
||||||
|
cached_data = joblib.load(cache_file)
|
||||||
|
if isinstance(cached_data, dict):
|
||||||
|
metadata = {
|
||||||
|
"bbox": cached_data.get("bbox", []),
|
||||||
|
"time_range": cached_data.get("time_range", ""),
|
||||||
|
"resolution": cached_data.get("resolution", 20),
|
||||||
|
"n_samples": len(cached_data.get("features", [])),
|
||||||
|
"created": cached_data.get("timestamp", "")
|
||||||
|
}
|
||||||
|
# Parse time_range to get start/end dates
|
||||||
|
if metadata["time_range"]:
|
||||||
|
time_parts = metadata["time_range"].split("/")
|
||||||
|
if len(time_parts) == 2:
|
||||||
|
metadata["start_date"] = time_parts[0]
|
||||||
|
metadata["end_date"] = time_parts[1]
|
||||||
|
# Parse bbox to get min/max lon/lat
|
||||||
|
if metadata["bbox"] and len(metadata["bbox"]) == 4:
|
||||||
|
metadata["min_lon"] = metadata["bbox"][0]
|
||||||
|
metadata["min_lat"] = metadata["bbox"][1]
|
||||||
|
metadata["max_lon"] = metadata["bbox"][2]
|
||||||
|
metadata["max_lat"] = metadata["bbox"][3]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading cache metadata: {e}")
|
||||||
|
|
||||||
|
cache_files.append({
|
||||||
|
"filename": cache_file.name,
|
||||||
|
"size_mb": round(size / 1024 / 1024, 2),
|
||||||
|
"modified": datetime.fromtimestamp(cache_file.stat().st_mtime).isoformat(),
|
||||||
|
"metadata": metadata
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by modified time (newest first)
|
||||||
|
cache_files.sort(key=lambda x: x["modified"], reverse=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"exists": True,
|
||||||
|
"files": cache_files,
|
||||||
|
"count": len(cache_files),
|
||||||
|
"total_size_mb": round(total_size / 1024 / 1024, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/models/list")
|
@app.get("/api/models/list")
|
||||||
async def list_models():
|
async def list_models():
|
||||||
"""Liệt kê các model đã train"""
|
"""Liệt kê các model đã train"""
|
||||||
@@ -288,10 +376,12 @@ async def run_training(config: TrainingConfig):
|
|||||||
cloud_cover=config.cloud_cover,
|
cloud_cover=config.cloud_cover,
|
||||||
resolution=config.resolution,
|
resolution=config.resolution,
|
||||||
training_shapefile=config.training_shapefile,
|
training_shapefile=config.training_shapefile,
|
||||||
|
model_type=config.model_type,
|
||||||
n_estimators=config.n_estimators,
|
n_estimators=config.n_estimators,
|
||||||
max_depth=config.max_depth,
|
max_depth=config.max_depth,
|
||||||
learning_rate=config.learning_rate,
|
learning_rate=config.learning_rate,
|
||||||
use_gpu=config.use_gpu,
|
use_gpu=config.use_gpu,
|
||||||
|
use_cache=config.use_cache,
|
||||||
status_callback=lambda msg: update_progress(msg),
|
status_callback=lambda msg: update_progress(msg),
|
||||||
cancel_check=should_cancel
|
cancel_check=should_cancel
|
||||||
)
|
)
|
||||||
@@ -360,6 +450,16 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
model = model_data
|
model = model_data
|
||||||
label_encoder = None
|
label_encoder = None
|
||||||
|
|
||||||
|
# Check if it's a CNN model (PyTorch)
|
||||||
|
is_cnn_model = hasattr(model, '__class__') and 'CNN' in model.__class__.__name__
|
||||||
|
if is_cnn_model:
|
||||||
|
prediction_status["progress"] = "Phát hiện PyTorch CNN model..."
|
||||||
|
# Import PyTorch if needed
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError("PyTorch is required for CNN prediction. Install: pip install torch")
|
||||||
|
|
||||||
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
|
||||||
|
|
||||||
# Import and use Microsoft Planetary Computer STAC API
|
# Import and use Microsoft Planetary Computer STAC API
|
||||||
@@ -447,14 +547,13 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
s1_items = s1_items[:config.max_scenes]
|
s1_items = s1_items[:config.max_scenes]
|
||||||
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
|
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
|
||||||
|
|
||||||
# Load Sentinel-1 data
|
# Load Sentinel-1 data (without like= to avoid conflict with bbox/resolution)
|
||||||
s1_data = load(
|
s1_data = load(
|
||||||
s1_items,
|
s1_items,
|
||||||
bbox=bbox,
|
bbox=bbox,
|
||||||
chunks={"time": 1, "x": 2048, "y": 2048},
|
chunks={"time": 1, "x": 2048, "y": 2048},
|
||||||
groupby="sat:absolute_orbit",
|
groupby="sat:absolute_orbit",
|
||||||
resolution=config.resolution,
|
resolution=config.resolution
|
||||||
like=ndvi_monthly # Align with NDVI grid
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract VH and VV bands
|
# Extract VH and VV bands
|
||||||
@@ -514,13 +613,51 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
vv_t = np.resize(vv_t, n_pixels)
|
vv_t = np.resize(vv_t, n_pixels)
|
||||||
features = np.column_stack([features, vv_t])
|
features = np.column_stack([features, vv_t])
|
||||||
|
|
||||||
# Handle NaN values in features
|
# Handle NaN values in features✓ CNN PyTorch: Mạnh nhất với ảnh vệ tinh, tự học features, tương thích GPU tốt, cần pip install torch
|
||||||
features = np.nan_to_num(features, nan=0.0)
|
features = np.nan_to_num(features, nan=0.0)
|
||||||
|
|
||||||
# ============ BƯỚC 6: DỰ ĐOÁN ============
|
# ============ BƯỚC 6: DỰ ĐOÁN ============
|
||||||
|
# Check model's expected feature count and adjust
|
||||||
|
try:
|
||||||
|
# Get expected number of features from model
|
||||||
|
if is_cnn_model:
|
||||||
|
# For PyTorch CNN, get n_features from model
|
||||||
|
expected_features = model.n_features
|
||||||
|
elif hasattr(model, 'n_features_in_'):
|
||||||
|
expected_features = model.n_features_in_
|
||||||
|
elif hasattr(model, 'feature_names_in_'):
|
||||||
|
expected_features = len(model.feature_names_in_)
|
||||||
|
else:
|
||||||
|
# Try to get from booster for XGBoost
|
||||||
|
try:
|
||||||
|
expected_features = model.get_booster().num_features()
|
||||||
|
except:
|
||||||
|
expected_features = features.shape[1]
|
||||||
|
|
||||||
|
prediction_status["progress"] = f"Model cần {expected_features} features, đang có {features.shape[1]} features..."
|
||||||
|
|
||||||
|
# Adjust features to match model
|
||||||
|
if features.shape[1] > expected_features:
|
||||||
|
# Trim to expected number (use only first N features - NDVI only)
|
||||||
|
prediction_status["progress"] = f"Cắt bớt features từ {features.shape[1]} xuống {expected_features}..."
|
||||||
|
features = features[:, :expected_features]
|
||||||
|
elif features.shape[1] < expected_features:
|
||||||
|
# Pad with zeros or repeat last features
|
||||||
|
prediction_status["progress"] = f"Thêm features từ {features.shape[1]} lên {expected_features}..."
|
||||||
|
n_missing = expected_features - features.shape[1]
|
||||||
|
# Repeat last feature column to fill
|
||||||
|
padding = np.tile(features[:, -1:], (1, n_missing))
|
||||||
|
features = np.column_stack([features, padding])
|
||||||
|
except Exception as e:
|
||||||
|
prediction_status["progress"] = f"Không thể xác định số features của model, tiếp tục với {features.shape[1]} features..."
|
||||||
|
|
||||||
prediction_status["progress"] = f"Đang dự đoán với {features.shape[1]} features..."
|
prediction_status["progress"] = f"Đang dự đoán với {features.shape[1]} features..."
|
||||||
|
|
||||||
# Make prediction
|
# Make prediction
|
||||||
|
if is_cnn_model:
|
||||||
|
# PyTorch CNN prediction
|
||||||
|
predictions = model.predict(features)
|
||||||
|
else:
|
||||||
predictions = model.predict(features)
|
predictions = model.predict(features)
|
||||||
|
|
||||||
# Decode labels if label_encoder exists
|
# Decode labels if label_encoder exists
|
||||||
@@ -594,6 +731,49 @@ async def run_prediction(config: PredictionConfig):
|
|||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/predictions/list")
|
||||||
|
async def list_predictions():
|
||||||
|
"""Lấy danh sách các file prediction đã tạo"""
|
||||||
|
predictions_dir = Path("predictions")
|
||||||
|
predictions_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
predictions = []
|
||||||
|
for pred_file in predictions_dir.glob("*.tif"):
|
||||||
|
predictions.append({
|
||||||
|
"filename": pred_file.name,
|
||||||
|
"created": datetime.fromtimestamp(pred_file.stat().st_mtime).isoformat(),
|
||||||
|
"size_mb": round(pred_file.stat().st_size / 1024 / 1024, 2),
|
||||||
|
"download_url": f"/api/predictions/download/{pred_file.name}"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by creation time (newest first)
|
||||||
|
predictions.sort(key=lambda x: x["created"], reverse=True)
|
||||||
|
return {"predictions": predictions}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/predictions/download/{filename}")
|
||||||
|
async def download_prediction(filename: str):
|
||||||
|
"""Download file prediction GeoTIFF"""
|
||||||
|
predictions_dir = Path("predictions")
|
||||||
|
file_path = predictions_dir / filename
|
||||||
|
|
||||||
|
# Security check: ensure filename doesn't contain path traversal
|
||||||
|
if ".." in filename or "/" in filename or "\\" in filename:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||||
|
|
||||||
|
if not file_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail=f"File không tồn tại: {filename}")
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
path=str(file_path),
|
||||||
|
filename=filename,
|
||||||
|
media_type="image/tiff",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f"attachment; filename={filename}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
print("🚀 LAND CLASSIFICATION TRAINING API SERVER")
|
print("🚀 LAND CLASSIFICATION TRAINING API SERVER")
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7318566231680cb0c97883b7a5e4177aa1bfeec462a0f52ed34aa103ddec210b
|
||||||
|
size 20903
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9cabf5e0241dcc3a73133ac8ae11171f34042c491f895b268c016407619bdfe1
|
||||||
|
size 20903
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ae2f82f6c837396729cc63efa41ee3048d9a7de3197e28318dc846be830239b9
|
||||||
|
size 41536
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-14T18:04:27.406540",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 510,
|
||||||
|
"testing_samples": 128,
|
||||||
|
"train_accuracy": 0.515686274509804,
|
||||||
|
"test_accuracy": 0.5,
|
||||||
|
"model_type": "cnn",
|
||||||
|
"device": "cpu",
|
||||||
|
"n_estimators": 50,
|
||||||
|
"max_depth": null,
|
||||||
|
"learning_rate": null,
|
||||||
|
"cnn_epochs": 25,
|
||||||
|
"n_features": 3,
|
||||||
|
"n_classes": 7,
|
||||||
|
"bbox": [
|
||||||
|
105.6,
|
||||||
|
9.3,
|
||||||
|
106.2,
|
||||||
|
9.8
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-05-31",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:abc03d62c7f620b88150a6481143026d516fd3a34bd5ab67c734adac4a8900f9
|
||||||
|
size 41536
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-14T18:17:48.399298",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 510,
|
||||||
|
"testing_samples": 128,
|
||||||
|
"train_accuracy": 0.4803921568627451,
|
||||||
|
"test_accuracy": 0.484375,
|
||||||
|
"model_type": "cnn",
|
||||||
|
"device": "cpu",
|
||||||
|
"n_estimators": 50,
|
||||||
|
"max_depth": null,
|
||||||
|
"learning_rate": null,
|
||||||
|
"cnn_epochs": 25,
|
||||||
|
"n_features": 3,
|
||||||
|
"n_classes": 7,
|
||||||
|
"bbox": [
|
||||||
|
105.6,
|
||||||
|
9.3,
|
||||||
|
106.2,
|
||||||
|
9.8
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-05-25",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:21f906aa2a61d2e793a3463df95fe3e364134934af326771efae90d80caca419
|
||||||
|
size 41536
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-14T18:23:10.713912",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 510,
|
||||||
|
"testing_samples": 128,
|
||||||
|
"train_accuracy": 0.46862745098039216,
|
||||||
|
"test_accuracy": 0.4609375,
|
||||||
|
"model_type": "cnn",
|
||||||
|
"device": "cpu",
|
||||||
|
"n_estimators": 50,
|
||||||
|
"max_depth": null,
|
||||||
|
"learning_rate": null,
|
||||||
|
"cnn_epochs": 25,
|
||||||
|
"n_features": 3,
|
||||||
|
"n_classes": 7,
|
||||||
|
"bbox": [
|
||||||
|
105.6,
|
||||||
|
9.3,
|
||||||
|
106.2,
|
||||||
|
9.8
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-05-31",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b717f564f9413a6e5c9cd3f7011cbc18be02479691d01c554986defb400f0490
|
||||||
|
size 1347520
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-14T13:40:03.325930",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 510,
|
||||||
|
"testing_samples": 128,
|
||||||
|
"train_accuracy": 1.0,
|
||||||
|
"test_accuracy": 0.578125,
|
||||||
|
"model_type": "XGBClassifier",
|
||||||
|
"device": "cuda:0",
|
||||||
|
"tree_method": "hist",
|
||||||
|
"n_estimators": 100,
|
||||||
|
"max_depth": 20,
|
||||||
|
"learning_rate": 0.1,
|
||||||
|
"bbox": [
|
||||||
|
105.6,
|
||||||
|
9.3,
|
||||||
|
106.2,
|
||||||
|
9.8
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-05-31",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a44efc173619782c8add224e9024f33ef0150ab192b5435d009cb119c379f03c
|
||||||
|
size 2088632
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-12-14T16:52:19.862770",
|
||||||
|
"data_source": "Microsoft Planetary Computer STAC",
|
||||||
|
"collections": [
|
||||||
|
"sentinel-2-l2a",
|
||||||
|
"sentinel-1-rtc"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"NDVI_mean",
|
||||||
|
"VH_dB_mean",
|
||||||
|
"VV_dB_mean"
|
||||||
|
],
|
||||||
|
"training_samples": 859,
|
||||||
|
"testing_samples": 215,
|
||||||
|
"train_accuracy": 0.9976717112922002,
|
||||||
|
"test_accuracy": 0.6837209302325581,
|
||||||
|
"model_type": "XGBClassifier",
|
||||||
|
"device": "cuda:0",
|
||||||
|
"tree_method": "hist",
|
||||||
|
"n_estimators": 100,
|
||||||
|
"max_depth": 20,
|
||||||
|
"learning_rate": 0.1,
|
||||||
|
"bbox": [
|
||||||
|
105.6,
|
||||||
|
9.3,
|
||||||
|
106.2,
|
||||||
|
9.8
|
||||||
|
],
|
||||||
|
"time_range": "2023-03-01/2023-12-31",
|
||||||
|
"resolution": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:179d3a61420c1552e9653de5c7657665c9880ad29d751e56bae646fb3b634687
|
||||||
|
size 73272920
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d8fb13c2e466b9811104cbd7747ea6ca8c14ddb44802d934aadfe52a4b5dd916
|
||||||
|
size 73272920
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a2fe092e1fd96e56da519acffe5ca5c9a8246e796bc5700cffb9694dc99f3aec
|
||||||
|
size 73272920
|
||||||
+246
-16
@@ -9,11 +9,109 @@ import geopandas as gpd
|
|||||||
from sklearn.model_selection import train_test_split
|
from sklearn.model_selection import train_test_split
|
||||||
from sklearn.preprocessing import LabelEncoder
|
from sklearn.preprocessing import LabelEncoder
|
||||||
from sklearn.metrics import classification_report, confusion_matrix
|
from sklearn.metrics import classification_report, confusion_matrix
|
||||||
|
from sklearn.ensemble import RandomForestClassifier
|
||||||
|
from sklearn.tree import DecisionTreeClassifier
|
||||||
|
from sklearn.svm import SVC
|
||||||
from xgboost import XGBClassifier
|
from xgboost import XGBClassifier
|
||||||
import joblib
|
import joblib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import warnings
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
warnings.filterwarnings('ignore')
|
||||||
|
|
||||||
|
# PyTorch for CNN
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
import torch.nn.functional as F
|
||||||
|
import torch.optim as optim
|
||||||
|
from torch.utils.data import TensorDataset, DataLoader
|
||||||
|
PYTORCH_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
PYTORCH_AVAILABLE = False
|
||||||
|
print("Warning: PyTorch not available. CNN model will not work.")
|
||||||
|
|
||||||
|
# Define CNN model class for PyTorch
|
||||||
|
class CNNClassifier(nn.Module):
|
||||||
|
def __init__(self, n_features, n_classes):
|
||||||
|
super(CNNClassifier, self).__init__()
|
||||||
|
self.n_features = n_features
|
||||||
|
self.n_classes = n_classes
|
||||||
|
|
||||||
|
# For small feature sets (like 3 features), use simpler architecture
|
||||||
|
if n_features < 8:
|
||||||
|
# Simple fully connected network for small features
|
||||||
|
self.use_conv = False
|
||||||
|
self.fc1 = nn.Linear(n_features, 64)
|
||||||
|
self.dropout1 = nn.Dropout(0.3)
|
||||||
|
self.fc2 = nn.Linear(64, 128)
|
||||||
|
self.dropout2 = nn.Dropout(0.5)
|
||||||
|
self.fc3 = nn.Linear(128, n_classes)
|
||||||
|
else:
|
||||||
|
# CNN architecture for larger feature sets
|
||||||
|
self.use_conv = True
|
||||||
|
self.conv1 = nn.Conv1d(in_channels=1, out_channels=32, kernel_size=3, padding=1)
|
||||||
|
self.pool1 = nn.MaxPool1d(kernel_size=2)
|
||||||
|
self.conv2 = nn.Conv1d(in_channels=32, out_channels=64, kernel_size=3, padding=1)
|
||||||
|
self.pool2 = nn.MaxPool1d(kernel_size=2)
|
||||||
|
|
||||||
|
# Calculate size after convolutions
|
||||||
|
conv_output_size = (n_features // 2 // 2) * 64
|
||||||
|
|
||||||
|
# Fully connected layers
|
||||||
|
self.fc1 = nn.Linear(conv_output_size, 128)
|
||||||
|
self.dropout = nn.Dropout(0.5)
|
||||||
|
self.fc2 = nn.Linear(128, n_classes)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
# x shape: (batch, n_features) or (batch, 1, n_features)
|
||||||
|
if self.use_conv:
|
||||||
|
# CNN path for larger feature sets
|
||||||
|
if len(x.shape) == 2:
|
||||||
|
x = x.unsqueeze(1) # Add channel dimension
|
||||||
|
x = F.relu(self.conv1(x))
|
||||||
|
x = self.pool1(x)
|
||||||
|
x = F.relu(self.conv2(x))
|
||||||
|
x = self.pool2(x)
|
||||||
|
x = x.view(x.size(0), -1) # Flatten
|
||||||
|
x = F.relu(self.fc1(x))
|
||||||
|
x = self.dropout(x)
|
||||||
|
x = self.fc2(x)
|
||||||
|
else:
|
||||||
|
# Fully connected path for small feature sets
|
||||||
|
if len(x.shape) == 3:
|
||||||
|
x = x.squeeze(1) # Remove channel dimension if present
|
||||||
|
x = F.relu(self.fc1(x))
|
||||||
|
x = self.dropout1(x)
|
||||||
|
x = F.relu(self.fc2(x))
|
||||||
|
x = self.dropout2(x)
|
||||||
|
x = self.fc3(x)
|
||||||
|
return x
|
||||||
|
|
||||||
|
def predict(self, X):
|
||||||
|
"""Scikit-learn style predict method"""
|
||||||
|
self.eval()
|
||||||
|
with torch.no_grad():
|
||||||
|
if isinstance(X, np.ndarray):
|
||||||
|
X = torch.FloatTensor(X)
|
||||||
|
# Handle both 2D and 3D inputs
|
||||||
|
if not self.use_conv and len(X.shape) == 3:
|
||||||
|
X = X.squeeze(1)
|
||||||
|
elif self.use_conv and len(X.shape) == 2:
|
||||||
|
X = X.unsqueeze(1)
|
||||||
|
outputs = self(X)
|
||||||
|
_, predicted = torch.max(outputs, 1)
|
||||||
|
return predicted.cpu().numpy()
|
||||||
|
|
||||||
|
def score(self, X, y):
|
||||||
|
"""Scikit-learn style score method"""
|
||||||
|
predictions = self.predict(X)
|
||||||
|
if isinstance(y, torch.Tensor):
|
||||||
|
y = y.cpu().numpy()
|
||||||
|
return np.mean(predictions == y)
|
||||||
|
|
||||||
# Microsoft Planetary Computer imports
|
# Microsoft Planetary Computer imports
|
||||||
import planetary_computer
|
import planetary_computer
|
||||||
@@ -28,10 +126,12 @@ def train_model(
|
|||||||
cloud_cover=30,
|
cloud_cover=30,
|
||||||
resolution=20,
|
resolution=20,
|
||||||
training_shapefile='train/ST_training data_updated_1130points_new.shp',
|
training_shapefile='train/ST_training data_updated_1130points_new.shp',
|
||||||
|
model_type='xgboost',
|
||||||
n_estimators=100,
|
n_estimators=100,
|
||||||
max_depth=20,
|
max_depth=20,
|
||||||
learning_rate=0.1,
|
learning_rate=0.1,
|
||||||
use_gpu=True,
|
use_gpu=True,
|
||||||
|
use_cache=True,
|
||||||
output_model_path=None,
|
output_model_path=None,
|
||||||
status_callback=None,
|
status_callback=None,
|
||||||
cancel_check=None
|
cancel_check=None
|
||||||
@@ -77,10 +177,39 @@ def train_model(
|
|||||||
# Auto-generate output path if not provided
|
# Auto-generate output path if not provided
|
||||||
if output_model_path is None:
|
if output_model_path is None:
|
||||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
output_model_path = f'model_train/model_xgboost_gpu_{timestamp}.joblib'
|
output_model_path = f'model_train/model_{model_type}_{timestamp}.joblib'
|
||||||
|
|
||||||
|
# ============ CACHE SYSTEM ============
|
||||||
|
# Create cache directory
|
||||||
|
cache_dir = Path("dataset_cache")
|
||||||
|
cache_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Generate cache key from parameters
|
||||||
|
cache_params = f"{bbox}_{time_range}_{max_scenes}_{cloud_cover}_{resolution}"
|
||||||
|
cache_key = hashlib.md5(cache_params.encode()).hexdigest()
|
||||||
|
cache_file = cache_dir / f"training_data_{cache_key}.joblib"
|
||||||
|
|
||||||
|
features = None
|
||||||
|
labels = None
|
||||||
|
|
||||||
|
# Try to load from cache
|
||||||
|
if use_cache and cache_file.exists():
|
||||||
|
update_status(f"📦 Loading cached dataset from {cache_file.name}...", 5)
|
||||||
|
try:
|
||||||
|
cached_data = joblib.load(cache_file)
|
||||||
|
features = cached_data['features']
|
||||||
|
labels = cached_data['labels']
|
||||||
|
update_status(f"✅ Loaded {len(features)} samples from cache (skipped satellite download!)", 50)
|
||||||
|
except Exception as e:
|
||||||
|
update_status(f"⚠️ Cache load failed: {str(e)}, downloading fresh data...", 10)
|
||||||
|
features = None
|
||||||
|
|
||||||
|
# If no cache or cache failed, download data
|
||||||
|
if features is None:
|
||||||
|
update_status("📡 Cache not found or disabled, downloading satellite data...", 10)
|
||||||
|
|
||||||
# Connect to Microsoft Planetary Computer
|
# Connect to Microsoft Planetary Computer
|
||||||
update_status("Connecting to Microsoft Planetary Computer...", 0)
|
update_status("Connecting to Microsoft Planetary Computer...", 12)
|
||||||
catalog = Client.open("https://planetarycomputer.microsoft.com/api/stac/v1")
|
catalog = Client.open("https://planetarycomputer.microsoft.com/api/stac/v1")
|
||||||
check_cancellation()
|
check_cancellation()
|
||||||
|
|
||||||
@@ -211,6 +340,23 @@ def train_model(
|
|||||||
|
|
||||||
update_status(f"Extracted {len(features)} valid training samples", 70)
|
update_status(f"Extracted {len(features)} valid training samples", 70)
|
||||||
|
|
||||||
|
# ============ SAVE TO CACHE ============
|
||||||
|
if use_cache:
|
||||||
|
update_status(f"💾 Saving dataset to cache for future use...", 72)
|
||||||
|
try:
|
||||||
|
cache_data = {
|
||||||
|
'features': features,
|
||||||
|
'labels': labels,
|
||||||
|
'bbox': bbox,
|
||||||
|
'time_range': time_range,
|
||||||
|
'resolution': resolution,
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
joblib.dump(cache_data, cache_file)
|
||||||
|
update_status(f"✅ Cached to {cache_file.name}", 75)
|
||||||
|
except Exception as e:
|
||||||
|
update_status(f"⚠️ Cache save failed: {str(e)}", 75)
|
||||||
|
|
||||||
# Encode labels
|
# Encode labels
|
||||||
label_encoder = LabelEncoder()
|
label_encoder = LabelEncoder()
|
||||||
labels_encoded = label_encoder.fit_transform(labels)
|
labels_encoded = label_encoder.fit_transform(labels)
|
||||||
@@ -220,33 +366,115 @@ def train_model(
|
|||||||
features, labels_encoded, test_size=0.2, random_state=42, stratify=labels_encoded
|
features, labels_encoded, test_size=0.2, random_state=42, stratify=labels_encoded
|
||||||
)
|
)
|
||||||
|
|
||||||
# Train XGBoost model
|
# Train model based on selected type
|
||||||
update_status("Training XGBoost model on GPU...", 75)
|
update_status(f"Training {model_type.upper()} model...", 75)
|
||||||
|
|
||||||
device = 'cuda:0' if use_gpu else 'cpu'
|
device = 'cuda:0' if use_gpu else 'cpu'
|
||||||
|
|
||||||
xgb_model = XGBClassifier(
|
if model_type == 'xgboost':
|
||||||
|
model = XGBClassifier(
|
||||||
n_estimators=n_estimators,
|
n_estimators=n_estimators,
|
||||||
max_depth=max_depth,
|
max_depth=max_depth,
|
||||||
learning_rate=learning_rate,
|
learning_rate=learning_rate,
|
||||||
device=device,
|
device=device if use_gpu else 'cpu',
|
||||||
tree_method='hist',
|
tree_method='hist',
|
||||||
random_state=42,
|
random_state=42,
|
||||||
eval_metric='mlogloss',
|
eval_metric='mlogloss',
|
||||||
verbosity=0
|
verbosity=0
|
||||||
)
|
)
|
||||||
|
elif model_type == 'random_forest':
|
||||||
|
model = RandomForestClassifier(
|
||||||
|
n_estimators=n_estimators,
|
||||||
|
max_depth=max_depth,
|
||||||
|
random_state=42,
|
||||||
|
n_jobs=-1, # Use all cores
|
||||||
|
verbose=0
|
||||||
|
)
|
||||||
|
elif model_type == 'decision_tree':
|
||||||
|
model = DecisionTreeClassifier(
|
||||||
|
max_depth=max_depth,
|
||||||
|
random_state=42
|
||||||
|
)
|
||||||
|
elif model_type == 'svm':
|
||||||
|
model = SVC(
|
||||||
|
kernel='rbf',
|
||||||
|
random_state=42,
|
||||||
|
verbose=False
|
||||||
|
)
|
||||||
|
elif model_type == 'cnn':
|
||||||
|
if not PYTORCH_AVAILABLE:
|
||||||
|
raise ImportError("PyTorch is required for CNN. Install: pip install torch")
|
||||||
|
|
||||||
xgb_model.fit(X_train, y_train)
|
# CNN requires reshaping data
|
||||||
|
n_features = X_train.shape[1]
|
||||||
|
n_classes = len(np.unique(y_train))
|
||||||
|
|
||||||
|
# Build PyTorch CNN model
|
||||||
|
device = torch.device('cuda' if torch.cuda.is_available() and use_gpu else 'cpu')
|
||||||
|
update_status(f"Building CNN model on {device}...", 75)
|
||||||
|
|
||||||
|
model = CNNClassifier(n_features, n_classes).to(device)
|
||||||
|
|
||||||
|
# Convert to PyTorch tensors
|
||||||
|
X_train_tensor = torch.FloatTensor(X_train).unsqueeze(1) # Add channel dim: (N, 1, features)
|
||||||
|
y_train_tensor = torch.LongTensor(y_train)
|
||||||
|
X_test_tensor = torch.FloatTensor(X_test).unsqueeze(1)
|
||||||
|
y_test_tensor = torch.LongTensor(y_test)
|
||||||
|
|
||||||
|
# Create data loaders
|
||||||
|
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
|
||||||
|
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
|
||||||
|
|
||||||
|
# Loss and optimizer
|
||||||
|
criterion = nn.CrossEntropyLoss()
|
||||||
|
optimizer = optim.Adam(model.parameters(), lr=0.001)
|
||||||
|
|
||||||
|
# Train CNN
|
||||||
|
update_status("Training CNN model with PyTorch...", 80)
|
||||||
|
epochs = min(50, n_estimators // 2) # Use n_estimators as epochs
|
||||||
|
|
||||||
|
model.train()
|
||||||
|
for epoch in range(epochs):
|
||||||
|
epoch_loss = 0.0
|
||||||
|
for batch_X, batch_y in train_loader:
|
||||||
|
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
|
||||||
|
|
||||||
|
optimizer.zero_grad()
|
||||||
|
outputs = model(batch_X)
|
||||||
|
loss = criterion(outputs, batch_y)
|
||||||
|
loss.backward()
|
||||||
|
optimizer.step()
|
||||||
|
|
||||||
|
epoch_loss += loss.item()
|
||||||
|
|
||||||
|
if (epoch + 1) % 10 == 0:
|
||||||
|
avg_loss = epoch_loss / len(train_loader)
|
||||||
|
update_status(f"CNN Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}", 80 + (epoch / epochs) * 10)
|
||||||
|
|
||||||
|
# Move model to CPU for saving (compatible with non-GPU systems)
|
||||||
|
model = model.cpu()
|
||||||
|
model.device_used = str(device)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown model type: {model_type}. Choose: xgboost, random_forest, decision_tree, svm, cnn")
|
||||||
|
|
||||||
|
# Fit non-CNN models
|
||||||
|
if model_type != 'cnn':
|
||||||
|
model.fit(X_train, y_train)
|
||||||
|
|
||||||
# Evaluate
|
# Evaluate
|
||||||
update_status("Evaluating model...", 90)
|
update_status("Evaluating model...", 90)
|
||||||
train_score = xgb_model.score(X_train, y_train)
|
if model_type == 'cnn':
|
||||||
test_score = xgb_model.score(X_test, y_test)
|
# PyTorch CNN evaluation
|
||||||
|
train_score = model.score(X_train, y_train)
|
||||||
|
test_score = model.score(X_test, y_test)
|
||||||
|
else:
|
||||||
|
train_score = model.score(X_train, y_train)
|
||||||
|
test_score = model.score(X_test, y_test)
|
||||||
|
|
||||||
# Save model
|
# Save model
|
||||||
update_status("Saving model...", 95)
|
update_status("Saving model...", 95)
|
||||||
os.makedirs(os.path.dirname(output_model_path), exist_ok=True)
|
os.makedirs(os.path.dirname(output_model_path), exist_ok=True)
|
||||||
joblib.dump({'model': xgb_model, 'label_encoder': label_encoder}, output_model_path)
|
joblib.dump({'model': model, 'label_encoder': label_encoder}, output_model_path)
|
||||||
|
|
||||||
# Save model info
|
# Save model info
|
||||||
info = {
|
info = {
|
||||||
@@ -258,12 +486,14 @@ def train_model(
|
|||||||
"testing_samples": len(X_test),
|
"testing_samples": len(X_test),
|
||||||
"train_accuracy": float(train_score),
|
"train_accuracy": float(train_score),
|
||||||
"test_accuracy": float(test_score),
|
"test_accuracy": float(test_score),
|
||||||
"model_type": "XGBClassifier",
|
"model_type": model_type,
|
||||||
"device": device,
|
"device": device if model_type == 'xgboost' else 'cpu',
|
||||||
"tree_method": "hist",
|
"n_estimators": n_estimators if model_type in ['xgboost', 'random_forest', 'cnn'] else None,
|
||||||
"n_estimators": n_estimators,
|
"max_depth": max_depth if model_type != 'cnn' else None,
|
||||||
"max_depth": max_depth,
|
"learning_rate": learning_rate if model_type == 'xgboost' else None,
|
||||||
"learning_rate": learning_rate,
|
"cnn_epochs": min(50, n_estimators // 2) if model_type == 'cnn' else None,
|
||||||
|
"n_features": X_train.shape[1],
|
||||||
|
"n_classes": len(np.unique(y_train)),
|
||||||
"bbox": bbox,
|
"bbox": bbox,
|
||||||
"time_range": time_range,
|
"time_range": time_range,
|
||||||
"resolution": resolution
|
"resolution": resolution
|
||||||
|
|||||||
+349
-13
@@ -258,6 +258,29 @@
|
|||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Animations for notifications */
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(400px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(400px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -369,7 +392,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 style="margin: 20px 0 15px; color: #667eea;">🛰️ Dữ Liệu Vệ Tinh</h3>
|
<h3 style="margin: 20px 0 15px; color: #667eea;">� Dataset Cache Preset</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Chọn Dataset đã cache:</label>
|
||||||
|
<select id="cachePreset" style="font-size: 14px;">
|
||||||
|
<option value="">-- Không dùng cache preset --</option>
|
||||||
|
</select>
|
||||||
|
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||||||
|
💡 Chọn dataset cache có sẵn để tự động điền các thông số tương ứng
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin: 20px 0 15px; color: #667eea;">�🛰️ Dữ Liệu Vệ Tinh</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Số scenes tối đa:</label>
|
<label>Số scenes tối đa:</label>
|
||||||
@@ -390,8 +424,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 style="margin: 20px 0 15px; color: #667eea;">🤖 Model Parameters</h3>
|
<h3 style="margin: 20px 0 15px; color: #667eea;">🤖 Model Parameters</h3>
|
||||||
<div class="form-row">
|
|
||||||
|
<!-- Model Type Selection -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<label><strong>🧠 Loại Model:</strong></label>
|
||||||
|
<select id="modelType" required style="font-weight: 600; font-size: 14px;">
|
||||||
|
<option value="xgboost" selected>🚀 XGBoost (Nhanh, Chính xác cao, Hỗ trợ GPU)</option>
|
||||||
|
<option value="random_forest">🌲 Random Forest (Ổn định, Không cần GPU)</option>
|
||||||
|
<option value="decision_tree">🌳 Decision Tree (Đơn giản, Nhanh nhất)</option>
|
||||||
|
<option value="svm">🎯 SVM (Chính xác, Chậm với dữ liệu lớn)</option>
|
||||||
|
<option value="cnn">🧠 CNN - Deep Learning (PyTorch, Tốt với ảnh vệ tinh, Hỗ trợ GPU)</option>
|
||||||
|
</select>
|
||||||
|
<div style="margin-top: 8px; padding: 10px; background: #e7f3ff; border-radius: 5px; font-size: 12px;">
|
||||||
|
<span id="modelTypeDesc" style="color: #1976d2;">
|
||||||
|
✓ XGBoost: Tốt nhất cho dữ liệu satellite, hỗ trợ GPU, training nhanh
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Model-specific parameters -->
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group" id="nEstimatorsGroup">
|
||||||
<label>N Estimators:</label>
|
<label>N Estimators:</label>
|
||||||
<input type="number" id="nEstimators" value="100" min="10" max="1000" required>
|
<input type="number" id="nEstimators" value="100" min="10" max="1000" required>
|
||||||
</div>
|
</div>
|
||||||
@@ -399,11 +452,11 @@
|
|||||||
<label>Max Depth:</label>
|
<label>Max Depth:</label>
|
||||||
<input type="number" id="maxDepth" value="20" min="1" max="50" required>
|
<input type="number" id="maxDepth" value="20" min="1" max="50" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" id="learningRateGroup">
|
||||||
<label>Learning Rate:</label>
|
<label>Learning Rate:</label>
|
||||||
<input type="number" step="0.01" id="learningRate" value="0.1" min="0.01" max="1" required>
|
<input type="number" step="0.01" id="learningRate" value="0.1" min="0.01" max="1" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" id="useGpuGroup">
|
||||||
<label>Use GPU:</label>
|
<label>Use GPU:</label>
|
||||||
<select id="useGpu" required>
|
<select id="useGpu" required>
|
||||||
<option value="true" selected>Có (RTX 4060)</option>
|
<option value="true" selected>Có (RTX 4060)</option>
|
||||||
@@ -412,6 +465,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Cache Option -->
|
||||||
|
<div class="form-group" style="margin-top: 20px; padding: 15px; background: #fff8dc; border-radius: 8px; border: 1px solid #ffc107;">
|
||||||
|
<label style="display: flex; align-items: center; cursor: pointer; margin: 0;">
|
||||||
|
<input type="checkbox" id="useCache" checked style="width: 18px; height: 18px; margin-right: 10px;">
|
||||||
|
<span style="font-weight: 600; color: #856404;">💾 Sử dụng Cache Dataset</span>
|
||||||
|
</label>
|
||||||
|
<div style="font-size: 12px; color: #856404; margin-top: 8px; margin-left: 28px;">
|
||||||
|
✅ <strong>Khuyến nghị:</strong> Bật để test nhanh hơn. Lần đầu load dữ liệu sẽ chậm, nhưng các lần sau rất nhanh (không cần download lại từ satellite).<br>
|
||||||
|
📊 <span id="cacheInfo">Đang kiểm tra cache...</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 10px; margin-left: 28px;">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="clearCache()" style="font-size: 12px; padding: 5px 10px; background: #dc3545;">
|
||||||
|
🗑️ Xóa Cache
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="loadCacheInfo()" style="font-size: 12px; padding: 5px 10px; background: #17a2b8; margin-left: 5px;">
|
||||||
|
🔄 Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 30px; text-align: center;">
|
<div style="margin-top: 30px; text-align: center;">
|
||||||
<button type="submit" class="btn btn-primary" id="startBtn">
|
<button type="submit" class="btn btn-primary" id="startBtn">
|
||||||
🚀 Bắt Đầu Training
|
🚀 Bắt Đầu Training
|
||||||
@@ -526,15 +599,16 @@
|
|||||||
<h3 style="color: #28a745; margin-bottom: 10px;">✅ Kết Quả Dự Đoán</h3>
|
<h3 style="color: #28a745; margin-bottom: 10px;">✅ Kết Quả Dự Đoán</h3>
|
||||||
<div style="background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); padding: 20px; border-radius: 8px; border: 2px solid #28a745;">
|
<div style="background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%); padding: 20px; border-radius: 8px; border: 2px solid #28a745;">
|
||||||
<div id="predResultText" style="font-size: 14px; line-height: 1.8;"></div>
|
<div id="predResultText" style="font-size: 14px; line-height: 1.8;"></div>
|
||||||
<div style="margin-top: 15px; text-align: center;">
|
<div id="downloadLinkContainer" style="margin-top: 15px; text-align: center;"></div>
|
||||||
<button onclick="downloadPredictionResult()" class="btn btn-secondary" style="background: #28a745;">
|
|
||||||
📥 Tải Kết Quả
|
|
||||||
</button>
|
|
||||||
<button onclick="viewPredictionResult()" class="btn btn-secondary" style="background: #17a2b8; margin-left: 10px;">
|
|
||||||
👁️ Xem Chi Tiết
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Previous Predictions List -->
|
||||||
|
<div id="predictionsListSection" style="margin-top: 20px;">
|
||||||
|
<h3 style="color: #ff6b6b; margin-bottom: 10px;">📂 Các File Dự Đoán Đã Tạo</h3>
|
||||||
|
<div id="predictionsList" style="background: #f8f9fa; padding: 15px; border-radius: 8px; max-height: 200px; overflow-y: auto;">
|
||||||
|
<p style="color: #666; text-align: center;">Đang tải...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -631,10 +705,12 @@
|
|||||||
max_scenes: parseInt(document.getElementById('maxScenes').value),
|
max_scenes: parseInt(document.getElementById('maxScenes').value),
|
||||||
cloud_cover: parseInt(document.getElementById('cloudCover').value),
|
cloud_cover: parseInt(document.getElementById('cloudCover').value),
|
||||||
resolution: parseInt(document.getElementById('resolution').value),
|
resolution: parseInt(document.getElementById('resolution').value),
|
||||||
|
model_type: document.getElementById('modelType').value,
|
||||||
n_estimators: parseInt(document.getElementById('nEstimators').value),
|
n_estimators: parseInt(document.getElementById('nEstimators').value),
|
||||||
max_depth: parseInt(document.getElementById('maxDepth').value),
|
max_depth: parseInt(document.getElementById('maxDepth').value),
|
||||||
learning_rate: parseFloat(document.getElementById('learningRate').value),
|
learning_rate: parseFloat(document.getElementById('learningRate').value),
|
||||||
use_gpu: document.getElementById('useGpu').value === 'true'
|
use_gpu: document.getElementById('useGpu').value === 'true',
|
||||||
|
use_cache: document.getElementById('useCache').checked
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -994,10 +1070,209 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model type change handler
|
||||||
|
function updateModelTypeUI() {
|
||||||
|
const modelType = document.getElementById('modelType').value;
|
||||||
|
const desc = document.getElementById('modelTypeDesc');
|
||||||
|
const nEstimatorsGroup = document.getElementById('nEstimatorsGroup');
|
||||||
|
const learningRateGroup = document.getElementById('learningRateGroup');
|
||||||
|
const useGpuGroup = document.getElementById('useGpuGroup');
|
||||||
|
|
||||||
|
const descriptions = {
|
||||||
|
'xgboost': '✓ XGBoost: Tốt nhất cho dữ liệu satellite, hỗ trợ GPU, training nhanh',
|
||||||
|
'random_forest': '✓ Random Forest: Ổn định, không overfitting, phù hợp mọi kích thước dữ liệu',
|
||||||
|
'decision_tree': '✓ Decision Tree: Đơn giản nhất, nhanh nhất, dễ hiểu, phù hợp để test nhanh',
|
||||||
|
'svm': '✓ SVM: Chính xác cao với dữ liệu nhỏ, chậm với dữ liệu lớn',
|
||||||
|
'cnn': '✓ CNN PyTorch: Mạnh nhất với ảnh vệ tinh, tự học features, tương thích GPU tốt, cần pip install torch'
|
||||||
|
};
|
||||||
|
|
||||||
|
desc.textContent = descriptions[modelType];
|
||||||
|
|
||||||
|
// Show/hide parameters based on model type
|
||||||
|
if (modelType === 'xgboost') {
|
||||||
|
nEstimatorsGroup.style.display = '';
|
||||||
|
learningRateGroup.style.display = '';
|
||||||
|
useGpuGroup.style.display = '';
|
||||||
|
} else if (modelType === 'random_forest') {
|
||||||
|
nEstimatorsGroup.style.display = '';
|
||||||
|
learningRateGroup.style.display = 'none';
|
||||||
|
useGpuGroup.style.display = 'none';
|
||||||
|
} else if (modelType === 'decision_tree') {
|
||||||
|
nEstimatorsGroup.style.display = 'none';
|
||||||
|
learningRateGroup.style.display = 'none';
|
||||||
|
useGpuGroup.style.display = 'none';
|
||||||
|
} else if (modelType === 'svm') {
|
||||||
|
nEstimatorsGroup.style.display = 'none';
|
||||||
|
learningRateGroup.style.display = 'none';
|
||||||
|
useGpuGroup.style.display = 'none';
|
||||||
|
} else if (modelType === 'cnn') {
|
||||||
|
// CNN uses n_estimators as epochs and supports GPU
|
||||||
|
nEstimatorsGroup.style.display = '';
|
||||||
|
document.querySelector('#nEstimatorsGroup label').textContent = 'Epochs (số lần training):';
|
||||||
|
document.getElementById('nEstimators').value = 50;
|
||||||
|
learningRateGroup.style.display = 'none';
|
||||||
|
useGpuGroup.style.display = ''; // Show GPU option for CNN
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset n_estimators label for non-CNN
|
||||||
|
if (modelType !== 'cnn' && modelType !== 'decision_tree' && modelType !== 'svm') {
|
||||||
|
document.querySelector('#nEstimatorsGroup label').textContent = 'N Estimators:';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== CACHE MANAGEMENT ==============
|
||||||
|
|
||||||
|
let cacheFiles = []; // Store cache files data
|
||||||
|
|
||||||
|
// Load cache info and populate preset dropdown
|
||||||
|
async function loadCacheInfo() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/cache/info`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Update cache info display
|
||||||
|
const cacheInfoSpan = document.getElementById('cacheInfo');
|
||||||
|
if (data.exists && data.count > 0) {
|
||||||
|
cacheInfoSpan.innerHTML = `✅ Có <strong>${data.count}</strong> file cache (${data.total_size_mb} MB)`;
|
||||||
|
cacheInfoSpan.style.color = '#28a745';
|
||||||
|
} else {
|
||||||
|
cacheInfoSpan.innerHTML = '❌ Chưa có cache nào';
|
||||||
|
cacheInfoSpan.style.color = '#6c757d';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate cache preset dropdown
|
||||||
|
const cachePreset = document.getElementById('cachePreset');
|
||||||
|
cachePreset.innerHTML = '<option value="">-- Không dùng cache preset --</option>';
|
||||||
|
|
||||||
|
if (data.exists && data.files && data.files.length > 0) {
|
||||||
|
cacheFiles = data.files;
|
||||||
|
data.files.forEach((file, index) => {
|
||||||
|
const meta = file.metadata;
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = index;
|
||||||
|
|
||||||
|
// Format display text with metadata
|
||||||
|
let displayText = `Cache #${index + 1}`;
|
||||||
|
if (meta && meta.n_samples) {
|
||||||
|
displayText += ` (${meta.n_samples} mẫu`;
|
||||||
|
if (meta.start_date && meta.end_date) {
|
||||||
|
displayText += `, ${meta.start_date} → ${meta.end_date}`;
|
||||||
|
}
|
||||||
|
if (meta.resolution) {
|
||||||
|
displayText += `, ${meta.resolution}m`;
|
||||||
|
}
|
||||||
|
displayText += ')';
|
||||||
|
} else {
|
||||||
|
displayText += ` (${file.size_mb} MB, ${new Date(file.modified).toLocaleString('vi-VN')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
option.textContent = displayText;
|
||||||
|
cachePreset.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading cache info:', error);
|
||||||
|
document.getElementById('cacheInfo').innerHTML = '⚠️ Lỗi kiểm tra cache';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply cache preset to form inputs
|
||||||
|
function applyCachePreset() {
|
||||||
|
const selectIndex = document.getElementById('cachePreset').value;
|
||||||
|
|
||||||
|
if (selectIndex === '') {
|
||||||
|
// Clear preset - no auto-fill
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileData = cacheFiles[parseInt(selectIndex)];
|
||||||
|
if (!fileData || !fileData.metadata) {
|
||||||
|
alert('Không có metadata cho cache này');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = fileData.metadata;
|
||||||
|
|
||||||
|
// Fill bbox inputs
|
||||||
|
if (meta.min_lon !== undefined) {
|
||||||
|
document.getElementById('minLon').value = meta.min_lon;
|
||||||
|
document.getElementById('maxLon').value = meta.max_lon;
|
||||||
|
document.getElementById('minLat').value = meta.min_lat;
|
||||||
|
document.getElementById('maxLat').value = meta.max_lat;
|
||||||
|
|
||||||
|
// Update map rectangle
|
||||||
|
if (rectangle) {
|
||||||
|
drawnItems.removeLayer(rectangle);
|
||||||
|
}
|
||||||
|
const bounds = L.latLngBounds(
|
||||||
|
[meta.min_lat, meta.min_lon],
|
||||||
|
[meta.max_lat, meta.max_lon]
|
||||||
|
);
|
||||||
|
rectangle = L.rectangle(bounds, {color: '#3388ff', weight: 3, fillOpacity: 0.2});
|
||||||
|
drawnItems.addLayer(rectangle);
|
||||||
|
map.fitBounds(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill time range inputs
|
||||||
|
if (meta.start_date) {
|
||||||
|
document.getElementById('startDate').value = meta.start_date;
|
||||||
|
}
|
||||||
|
if (meta.end_date) {
|
||||||
|
document.getElementById('endDate').value = meta.end_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill resolution if available
|
||||||
|
if (meta.resolution) {
|
||||||
|
document.getElementById('resolution').value = meta.resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note to user
|
||||||
|
const presetNote = `📌 Đã áp dụng cache preset: ${meta.n_samples || '?'} mẫu, ${meta.start_date || '?'} → ${meta.end_date || '?'}`;
|
||||||
|
console.log(presetNote);
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.style.cssText = 'position:fixed;top:20px;right:20px;background:#28a745;color:white;padding:15px 20px;border-radius:8px;box-shadow:0 4px 6px rgba(0,0,0,0.1);z-index:10000;animation:slideIn 0.3s ease-out;';
|
||||||
|
notification.innerHTML = `<strong>✅ Cache Preset Applied</strong><br>${presetNote}`;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideOut 0.3s ease-out';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
async function clearCache() {
|
||||||
|
if (!confirm('Bạn có chắc muốn xóa toàn bộ cache?\n\nCache giúp test nhanh hơn bằng cách lưu lại dữ liệu đã download.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/cache/clear`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
alert(result.message);
|
||||||
|
loadCacheInfo(); // Refresh info
|
||||||
|
} catch (error) {
|
||||||
|
alert('Lỗi xóa cache: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize map when page loads
|
// Initialize map when page loads
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
initMap();
|
initMap();
|
||||||
initPredictionMap();
|
initPredictionMap();
|
||||||
|
loadPredictionsList(); // Load predictions list on page load
|
||||||
|
loadCacheInfo(); // Load cache info
|
||||||
|
|
||||||
|
// Add model type change listener
|
||||||
|
document.getElementById('modelType').addEventListener('change', updateModelTypeUI);
|
||||||
|
updateModelTypeUI(); // Initial update
|
||||||
|
|
||||||
|
// Add cache preset change listener
|
||||||
|
document.getElementById('cachePreset').addEventListener('change', applyCachePreset);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============== PREDICTION FUNCTIONALITY ==============
|
// ============== PREDICTION FUNCTIONALITY ==============
|
||||||
@@ -1207,6 +1482,10 @@
|
|||||||
// Store result globally for download/view functions
|
// Store result globally for download/view functions
|
||||||
window.lastPredictionResult = result;
|
window.lastPredictionResult = result;
|
||||||
|
|
||||||
|
// Extract filename from path
|
||||||
|
const filename = result.output_file.split('/').pop();
|
||||||
|
const downloadUrl = `${API_BASE}/predictions/download/${filename}`;
|
||||||
|
|
||||||
resultText.innerHTML = `
|
resultText.innerHTML = `
|
||||||
<div style="margin-bottom: 10px;">
|
<div style="margin-bottom: 10px;">
|
||||||
<strong>📁 File kết quả:</strong><br>
|
<strong>📁 File kết quả:</strong><br>
|
||||||
@@ -1220,19 +1499,76 @@
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Add download link
|
||||||
|
const downloadContainer = document.getElementById('downloadLinkContainer');
|
||||||
|
downloadContainer.innerHTML = `
|
||||||
|
<a href="${downloadUrl}"
|
||||||
|
class="btn btn-primary"
|
||||||
|
style="background: #28a745; padding: 12px 24px; text-decoration: none; display: inline-block; margin-right: 10px;"
|
||||||
|
download="${filename}">
|
||||||
|
📥 Tải GeoTIFF
|
||||||
|
</a>
|
||||||
|
<button onclick="viewPredictionResult()" class="btn btn-secondary" style="background: #17a2b8;">
|
||||||
|
👁️ Xem Chi Tiết
|
||||||
|
</button>
|
||||||
|
<div style="margin-top: 10px; font-size: 12px; color: #666;">
|
||||||
|
Hoặc copy link: <a href="${downloadUrl}" target="_blank" style="color: #28a745;">${downloadUrl}</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
resultDiv.style.display = 'block';
|
resultDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// Refresh predictions list
|
||||||
|
loadPredictionsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download prediction result
|
// Download prediction result
|
||||||
function downloadPredictionResult() {
|
function downloadPredictionResult() {
|
||||||
if (window.lastPredictionResult) {
|
if (window.lastPredictionResult) {
|
||||||
const result = window.lastPredictionResult;
|
const result = window.lastPredictionResult;
|
||||||
alert('File kết quả: ' + result.output_file + '\n\nĐể tải file, vui lòng truy cập thư mục predictions/ trên server.');
|
const filename = result.output_file.split('/').pop();
|
||||||
|
const downloadUrl = `${API_BASE}/predictions/download/${filename}`;
|
||||||
|
window.open(downloadUrl, '_blank');
|
||||||
} else {
|
} else {
|
||||||
alert('Chưa có kết quả dự đoán nào!');
|
alert('Chưa có kết quả dự đoán nào!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load list of previous predictions
|
||||||
|
async function loadPredictionsList() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/predictions/list`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const listDiv = document.getElementById('predictionsList');
|
||||||
|
|
||||||
|
if (data.predictions && data.predictions.length > 0) {
|
||||||
|
listDiv.innerHTML = data.predictions.map(pred => `
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; padding: 10px; margin-bottom: 8px; background: white; border-radius: 6px; border: 1px solid #ddd;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<strong style="color: #333;">📄 ${pred.filename}</strong>
|
||||||
|
<div style="font-size: 12px; color: #666; margin-top: 3px;">
|
||||||
|
📅 ${new Date(pred.created).toLocaleString('vi-VN')} | 💾 ${pred.size_mb} MB
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="${pred.download_url}"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
style="background: #28a745; padding: 6px 12px; font-size: 12px; text-decoration: none;"
|
||||||
|
download="${pred.filename}">
|
||||||
|
📥 Tải về
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} else {
|
||||||
|
listDiv.innerHTML = '<p style="color: #666; text-align: center;">Chưa có file dự đoán nào.</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading predictions list:', error);
|
||||||
|
document.getElementById('predictionsList').innerHTML =
|
||||||
|
'<p style="color: #dc3545; text-align: center;">Lỗi tải danh sách: ' + error.message + '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// View prediction result details
|
// View prediction result details
|
||||||
function viewPredictionResult() {
|
function viewPredictionResult() {
|
||||||
if (window.lastPredictionResult) {
|
if (window.lastPredictionResult) {
|
||||||
|
|||||||
Reference in New Issue
Block a user