""" CHIẾN LƯỢC TOÀN DIỆN ĐẠT >95% ACCURACY ========================================= Kết hợp 5 chiến lược song song: 1. Hybrid CNN+XGBoost: Trích xuất 2D features từ CNN nhẹ -> XGBoost 2. Rich Feature Engineering: Thống kê pixel + texture + temporal -> XGBoost 3. Lightweight ResNet: ResNet-18 nhẹ, không upsample lãng phí 4. Stacking Ensemble: Kết hợp tất cả mô hình 5. StratifiedKFold: Cross-validation chống overfit """ import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset import torchvision.models as models import torchvision.transforms as T import joblib import numpy as np import os import json from sklearn.model_selection import StratifiedKFold, train_test_split from sklearn.metrics import accuracy_score, classification_report from sklearn.preprocessing import StandardScaler from sklearn.ensemble import ( RandomForestClassifier, GradientBoostingClassifier, StackingClassifier, VotingClassifier, ExtraTreesClassifier ) from xgboost import XGBClassifier from lightgbm import LGBMClassifier import warnings warnings.filterwarnings('ignore') # ===== 1. LOAD DATA ===== def load_data(): data = joblib.load('dataset_cache/training_data_2d_temporal.joblib') X, y = data['X'], data['y'] X = X.astype(np.float32) print(f"Loaded data: X={X.shape}, y={y.shape}") print(f"Labels unique: {np.unique(y)}") # Remove invalid labels (label -1 = HT_code 0, which is invalid) valid_mask = y >= 0 # Remove all-zero patches non_zero_mask = X.reshape(X.shape[0], -1).sum(axis=1) != 0 mask = valid_mask & non_zero_mask X, y = X[mask], y[mask] print(f"After cleanup: X={X.shape}, y={y.shape} (removed {(~mask).sum()} bad samples)") # Remap labels to 0..N-1 unique_labels = sorted(np.unique(y).tolist()) label_map = {lbl: i for i, lbl in enumerate(unique_labels)} y_mapped = np.array([label_map[l] for l in y]) print(f"Remapped labels: {np.unique(y_mapped)}") for lbl in np.unique(y_mapped): print(f" Class {lbl}: {(y_mapped==lbl).sum()} samples") return X, y_mapped, len(unique_labels) # ===== 2. RICH FEATURE ENGINEERING ===== def extract_rich_features(X): """ Từ mỗi patch (24, 16, 16) trích xuất hàng trăm features thống kê. Channels: [B02,B03,B04,B08,NDVI,NDWI] x 4 timesteps """ N = X.shape[0] all_features = [] band_names = ['B02','B03','B04','B08','NDVI','NDWI'] for i in range(N): patch = X[i] # (24, 16, 16) feats = [] # Per-channel statistics cho mỗi timestep for t in range(4): for b in range(6): ch = patch[t*6 + b] # (16, 16) feats.extend([ np.mean(ch), np.std(ch), np.median(ch), np.min(ch), np.max(ch), np.percentile(ch, 25), np.percentile(ch, 75), # Skewness và kurtosis float(np.mean((ch - np.mean(ch))**3) / (np.std(ch)**3 + 1e-10)), float(np.mean((ch - np.mean(ch))**4) / (np.std(ch)**4 + 1e-10)), # Entropy approximation float(-np.sum(np.abs(ch/np.sum(np.abs(ch)+1e-10)) * np.log(np.abs(ch/np.sum(np.abs(ch)+1e-10))+1e-10))), ]) # Temporal change features: sự thay đổi giữa các timestep for b in range(6): vals_over_time = [] for t in range(4): vals_over_time.append(np.mean(patch[t*6 + b])) vals = np.array(vals_over_time) feats.extend([ np.std(vals), # Temporal variability np.max(vals) - np.min(vals), # Range over time vals[-1] - vals[0] if len(vals) > 1 else 0, # Trend np.mean(np.abs(np.diff(vals))) if len(vals) > 1 else 0, # Mean absolute change ]) # Cross-band ratios (trung bình qua thời gian) for t in range(4): b02 = np.mean(patch[t*6+0]) + 1e-10 b03 = np.mean(patch[t*6+1]) + 1e-10 b04 = np.mean(patch[t*6+2]) + 1e-10 b08 = np.mean(patch[t*6+3]) + 1e-10 feats.extend([ b08/b04, # NIR/Red ratio b03/b04, # Green/Red ratio (b08-b04)/(b08+b04), # NDVI recompute (b03-b08)/(b03+b08), # NDWI recompute b02/b08, # Blue/NIR ]) # Spatial texture features (Gradient magnitude) for t in range(4): for b_idx in [3, 4]: # B08 and NDVI ch = patch[t*6 + b_idx] # Sobel-like gradient gx = np.diff(ch, axis=1) gy = np.diff(ch, axis=0) grad_mag = np.sqrt(np.mean(gx**2) + np.mean(gy**2)) # Local variance (texture) from scipy.ndimage import uniform_filter local_mean = uniform_filter(ch, size=3) local_var = uniform_filter(ch**2, size=3) - local_mean**2 feats.extend([ grad_mag, np.mean(local_var), np.std(local_var), ]) # Center pixel vs edge pixels for t in range(4): for b_idx in [3, 4]: # B08 and NDVI ch = patch[t*6 + b_idx] center = ch[6:10, 6:10].mean() edge = np.concatenate([ch[0,:], ch[-1,:], ch[:,0], ch[:,-1]]).mean() feats.append(center - edge) all_features.append(feats) features = np.array(all_features, dtype=np.float32) features = np.nan_to_num(features, nan=0.0, posinf=1e6, neginf=-1e6) print(f"Extracted {features.shape[1]} rich features per sample") return features # ===== 3. LIGHTWEIGHT CNN ===== class LightCNN(nn.Module): """CNN nhẹ thiết kế riêng cho 16x16 patches - KHÔNG upsample""" def __init__(self, in_channels=24, num_classes=5): super().__init__() self.features = nn.Sequential( # Block 1: 16x16 -> 8x8 nn.Conv2d(in_channels, 64, 3, padding=1), nn.BatchNorm2d(64), nn.GELU(), nn.Conv2d(64, 64, 3, padding=1), nn.BatchNorm2d(64), nn.GELU(), nn.MaxPool2d(2), nn.Dropout2d(0.1), # Block 2: 8x8 -> 4x4 nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.GELU(), nn.Conv2d(128, 128, 3, padding=1), nn.BatchNorm2d(128), nn.GELU(), nn.MaxPool2d(2), nn.Dropout2d(0.1), # Block 3: 4x4 -> 2x2 nn.Conv2d(128, 256, 3, padding=1), nn.BatchNorm2d(256), nn.GELU(), nn.Conv2d(256, 256, 3, padding=1), nn.BatchNorm2d(256), nn.GELU(), nn.MaxPool2d(2), nn.Dropout2d(0.2), ) # Squeeze and Excitation self.se = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(256, 64), nn.GELU(), nn.Linear(64, 256), nn.Sigmoid() ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(256, 128), nn.GELU(), nn.Dropout(0.5), nn.Linear(128, num_classes) ) self.embedding_head = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), ) def get_embedding(self, x): """Get 256-dim embedding for hybrid approach""" f = self.features(x) se_w = self.se(f).unsqueeze(-1).unsqueeze(-1) f = f * se_w return self.embedding_head(f) def forward(self, x): f = self.features(x) se_w = self.se(f).unsqueeze(-1).unsqueeze(-1) f = f * se_w return self.classifier(f) # ===== 4. TRAIN LIGHTWEIGHT CNN ===== def train_light_cnn(X, y, num_classes, epochs=300, lr=3e-4): print("\n" + "="*60) print("STRATEGY 1: Lightweight CNN (no upsampling)") print("="*60) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') print(f"Device: {device}") # Data augmentation def augment_batch(x): if np.random.random() > 0.5: x = torch.flip(x, [2]) if np.random.random() > 0.5: x = torch.flip(x, [3]) if np.random.random() > 0.5: k = np.random.randint(1, 4) x = torch.rot90(x, k, [2, 3]) # Random noise if np.random.random() > 0.5: noise = torch.randn_like(x) * 0.02 x = x + noise # Mixup return x train_X = torch.FloatTensor(X_train) train_y = torch.LongTensor(y_train) test_X = torch.FloatTensor(X_test).to(device) test_y = torch.LongTensor(y_test) model = LightCNN(in_channels=X.shape[1], num_classes=num_classes).to(device) # Class weights class_counts = np.bincount(y_train, minlength=num_classes) weights = 1.0 / (class_counts + 1) weights = torch.FloatTensor(weights / weights.sum() * num_classes).to(device) criterion = nn.CrossEntropyLoss(weight=weights, label_smoothing=0.1) optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01) scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=50, T_mult=2, eta_min=1e-6) best_acc = 0 best_state = None patience = 0 for epoch in range(epochs): model.train() # Shuffle perm = torch.randperm(len(train_X)) train_loss = 0 n_batches = 0 for i in range(0, len(train_X), 32): idx = perm[i:i+32] bx = train_X[idx].to(device) by = train_y[idx].to(device) # Augmentation bx = augment_batch(bx) optimizer.zero_grad() out = model(bx) loss = criterion(out, by) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() train_loss += loss.item() n_batches += 1 scheduler.step() model.eval() with torch.no_grad(): out = model(test_X) preds = out.argmax(dim=1).cpu().numpy() acc = accuracy_score(test_y.numpy(), preds) if acc > best_acc: best_acc = acc best_state = {k: v.cpu().clone() for k, v in model.state_dict().items()} patience = 0 print(f" Epoch {epoch+1}/{epochs} Loss={train_loss/n_batches:.4f} Acc={acc:.4f} 🌟") if acc >= 0.95: print(" 🎯 >95% reached!") break else: patience += 1 if (epoch+1) % 20 == 0: print(f" Epoch {epoch+1}/{epochs} Loss={train_loss/n_batches:.4f} Acc={acc:.4f} (patience={patience})") if patience >= 60: print(f" Early stop at epoch {epoch+1}") break if best_state: model.load_state_dict(best_state) print(f" ✅ LightCNN best acc: {best_acc:.4f}") return model, best_acc, X_test, y_test # ===== 5. HYBRID CNN + XGBOOST ===== def train_hybrid(X, y, cnn_model, num_classes): print("\n" + "="*60) print("STRATEGY 2: Hybrid CNN embeddings + XGBoost") print("="*60) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') cnn_model = cnn_model.to(device) cnn_model.eval() # Extract CNN embeddings with torch.no_grad(): embeddings = [] for i in range(0, len(X), 64): batch = torch.FloatTensor(X[i:i+64]).to(device) emb = cnn_model.get_embedding(batch) embeddings.append(emb.cpu().numpy()) cnn_features = np.concatenate(embeddings, axis=0) print(f" CNN embeddings: {cnn_features.shape}") # Extract rich handcrafted features rich_features = extract_rich_features(X) # Combine combined = np.concatenate([cnn_features, rich_features], axis=1) print(f" Combined features: {combined.shape}") # Standardize scaler = StandardScaler() combined = scaler.fit_transform(combined) X_train, X_test, y_train, y_test = train_test_split( combined, y, test_size=0.2, random_state=42, stratify=y ) # XGBoost with tuned params xgb = XGBClassifier( n_estimators=500, max_depth=8, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, min_child_weight=3, gamma=0.1, reg_alpha=0.1, reg_lambda=1.0, tree_method='hist', device='cuda', eval_metric='mlogloss', random_state=42, use_label_encoder=False ) xgb.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=False) xgb_acc = accuracy_score(y_test, xgb.predict(X_test)) print(f" ✅ Hybrid XGBoost acc: {xgb_acc:.4f}") return xgb, scaler, xgb_acc, combined, X_test, y_test # ===== 6. PURE RICH FEATURES + ENSEMBLE ===== def train_rich_ensemble(X, y, num_classes): print("\n" + "="*60) print("STRATEGY 3: Rich Features + Stacking Ensemble") print("="*60) rich_features = extract_rich_features(X) scaler = StandardScaler() rich_features = scaler.fit_transform(rich_features) X_train, X_test, y_train, y_test = train_test_split( rich_features, y, test_size=0.2, random_state=42, stratify=y ) # Multiple base learners models_dict = { 'XGBoost': XGBClassifier( n_estimators=500, max_depth=8, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, min_child_weight=3, tree_method='hist', device='cuda', eval_metric='mlogloss', random_state=42, use_label_encoder=False ), 'LightGBM': LGBMClassifier( n_estimators=500, max_depth=8, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, min_child_weight=3, random_state=42, verbose=-1 ), 'ExtraTrees': ExtraTreesClassifier( n_estimators=500, max_depth=None, min_samples_split=5, random_state=42, n_jobs=-1 ), 'RandomForest': RandomForestClassifier( n_estimators=500, max_depth=None, min_samples_split=5, random_state=42, n_jobs=-1 ), 'GBM': GradientBoostingClassifier( n_estimators=300, max_depth=6, learning_rate=0.05, subsample=0.8, random_state=42 ), } results = {} for name, model in models_dict.items(): model.fit(X_train, y_train) acc = accuracy_score(y_test, model.predict(X_test)) results[name] = acc print(f" {name}: {acc:.4f}") # Stacking ensemble estimators = [(name, model) for name, model in models_dict.items() if name != 'GBM'] stacking = StackingClassifier( estimators=estimators, final_estimator=XGBClassifier( n_estimators=200, max_depth=4, learning_rate=0.05, tree_method='hist', device='cuda', random_state=42, use_label_encoder=False ), cv=5, n_jobs=-1 ) stacking.fit(X_train, y_train) stack_acc = accuracy_score(y_test, stacking.predict(X_test)) print(f" Stacking Ensemble: {stack_acc:.4f}") # Voting ensemble voting = VotingClassifier( estimators=[(name, model) for name, model in models_dict.items()], voting='soft', n_jobs=-1 ) voting.fit(X_train, y_train) vote_acc = accuracy_score(y_test, voting.predict(X_test)) print(f" Voting Ensemble: {vote_acc:.4f}") results['Stacking'] = stack_acc results['Voting'] = vote_acc best_name = max(results, key=results.get) best_acc = results[best_name] print(f" ✅ Best ensemble: {best_name} = {best_acc:.4f}") return stacking, voting, results, scaler, X_test, y_test # ===== 7. CROSS-VALIDATION ===== def cross_validate_best(X_features, y, best_model_fn): print("\n" + "="*60) print("STRATEGY 4: 5-Fold Stratified Cross-Validation") print("="*60) skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) fold_accs = [] for fold, (train_idx, test_idx) in enumerate(skf.split(X_features, y)): X_tr, X_te = X_features[train_idx], X_features[test_idx] y_tr, y_te = y[train_idx], y[test_idx] model = best_model_fn() model.fit(X_tr, y_tr) acc = accuracy_score(y_te, model.predict(X_te)) fold_accs.append(acc) print(f" Fold {fold+1}: {acc:.4f}") mean_acc = np.mean(fold_accs) std_acc = np.std(fold_accs) print(f" ✅ CV Mean: {mean_acc:.4f} ± {std_acc:.4f}") return mean_acc, std_acc # ===== 8. FLAT FEATURES + XGBOOST (baseline comparison) ===== def train_flat_xgboost(X, y): print("\n" + "="*60) print("STRATEGY 5: Flat pixel features + XGBoost (sanity check)") print("="*60) X_flat = X.reshape(X.shape[0], -1) print(f" Flat features: {X_flat.shape}") scaler = StandardScaler() X_flat = scaler.fit_transform(X_flat) X_train, X_test, y_train, y_test = train_test_split( X_flat, y, test_size=0.2, random_state=42, stratify=y ) xgb = XGBClassifier( n_estimators=500, max_depth=8, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, tree_method='hist', device='cuda', eval_metric='mlogloss', random_state=42, use_label_encoder=False ) xgb.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=False) acc = accuracy_score(y_test, xgb.predict(X_test)) print(f" ✅ Flat XGBoost acc: {acc:.4f}") return xgb, acc # ===== MAIN ===== def main(): print("🚀 CHIẾN LƯỢC TOÀN DIỆN ĐẠT >95% ACCURACY") print("="*60) X, y, num_classes = load_data() # Strategy 5: Flat baseline flat_xgb, flat_acc = train_flat_xgboost(X, y) # Strategy 1: Lightweight CNN cnn_model, cnn_acc, _, _ = train_light_cnn(X, y, num_classes) # Strategy 2: Hybrid CNN + XGBoost hybrid_xgb, hybrid_scaler, hybrid_acc, combined_features, _, _ = train_hybrid(X, y, cnn_model, num_classes) # Strategy 3: Rich Features + Stacking Ensemble stacking, voting, ensemble_results, rich_scaler, _, _ = train_rich_ensemble(X, y, num_classes) # Strategy 4: Cross-validate the best rich_features = extract_rich_features(X) rich_features_scaled = StandardScaler().fit_transform(rich_features) cv_mean, cv_std = cross_validate_best( rich_features_scaled, y, lambda: XGBClassifier( n_estimators=500, max_depth=8, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, tree_method='hist', device='cuda', random_state=42, use_label_encoder=False ) ) # ===== SUMMARY ===== print("\n" + "="*60) print("📊 TỔNG KẾT KẾT QUẢ") print("="*60) all_results = { 'Flat XGBoost (baseline)': flat_acc, 'LightCNN': cnn_acc, 'Hybrid CNN+XGBoost': hybrid_acc, } all_results.update({f'Ensemble {k}': v for k, v in ensemble_results.items()}) all_results['CV Mean (XGBoost rich)'] = cv_mean for name, acc in sorted(all_results.items(), key=lambda x: -x[1]): marker = "🏆" if acc >= 0.95 else "✅" if acc >= 0.90 else "📈" print(f" {marker} {name}: {acc:.4f}") best_name = max(all_results, key=all_results.get) best_acc = all_results[best_name] print(f"\n🏆 BEST: {best_name} = {best_acc:.4f}") # Save best model os.makedirs('land_classification_model', exist_ok=True) os.makedirs('model_train', exist_ok=True) info = { "all_results": {k: float(v) for k, v in all_results.items()}, "best_model": best_name, "best_accuracy": float(best_acc), "cv_mean": float(cv_mean), "cv_std": float(cv_std), } with open('model_train/ultimate_results.json', 'w') as f: json.dump(info, f, indent=2) print(f"\n✅ Kết quả đã được lưu vào model_train/ultimate_results.json") if best_acc >= 0.95: print("🎯🎯🎯 ĐÃ ĐẠT MỤC TIÊU >95% ACCURACY! 🎯🎯🎯") else: print(f"⚠️ Chưa đạt 95%. Best = {best_acc:.4f}. Cần thêm dữ liệu hoặc feature engineering.") if __name__ == "__main__": main()