import torch import torch.nn as nn import torch.optim as optim import numpy as np import joblib import os import json from sklearn.model_selection import StratifiedKFold, train_test_split from sklearn.metrics import accuracy_score from sklearn.preprocessing import StandardScaler from xgboost import XGBClassifier from lightgbm import LGBMClassifier from sklearn.ensemble import ExtraTreesClassifier, VotingClassifier from scipy.ndimage import uniform_filter import warnings warnings.filterwarnings('ignore') def load_and_clean(): data = joblib.load('dataset_cache/training_data_fusion_32ch.joblib') X, y = data['X'].astype(np.float32), data['y'] valid = (y >= 0) & (X.reshape(X.shape[0], -1).sum(1) != 0) X, y = X[valid], y[valid] unique = sorted(np.unique(y).tolist()) lmap = {l:i for i,l in enumerate(unique)} y = np.array([lmap[l] for l in y]) return X, y, len(unique) def extract_features_fusion(X): N = X.shape[0] all_feats = [] for i in range(N): patch = X[i] feats = [] valid_ts = [] for t in range(4): block_s2 = patch[t*8 : t*8+6] if np.abs(block_s2).sum() > 1e-6: valid_ts.append(t) if not valid_ts: valid_ts = [0] per_ts_stats_s2 = {b: [] for b in range(6)} for t in valid_ts: for b in range(6): ch = patch[t*8 + b] per_ts_stats_s2[b].append([ np.mean(ch), np.std(ch), np.median(ch), np.min(ch), np.max(ch), np.percentile(ch, 10), np.percentile(ch, 90), ]) for b in range(6): stats = np.array(per_ts_stats_s2[b]) feats.extend(stats.mean(axis=0).tolist()) feats.extend(stats.std(axis=0).tolist()) per_ts_stats_s1 = {b: [] for b in range(2)} for t in range(4): vv = patch[t*8 + 6] vh = patch[t*8 + 7] if np.abs(vv).sum() > 1e-6: per_ts_stats_s1[0].append([ np.mean(vv), np.std(vv), np.median(vv), np.max(vv), np.percentile(vv, 90) ]) per_ts_stats_s1[1].append([ np.mean(vh), np.std(vh), np.median(vh), np.max(vh), np.percentile(vh, 90) ]) ratio = (vh + 1e-6) / (vv + 1e-6) feats.extend([np.mean(ratio), np.std(ratio), np.median(ratio)]) else: feats.extend([0.0] * 3) for b in range(2): if len(per_ts_stats_s1[b]) > 0: stats = np.array(per_ts_stats_s1[b]) feats.extend(stats.mean(axis=0).tolist()) feats.extend(stats.std(axis=0).tolist()) else: feats.extend([0.0] * 10) for t in valid_ts[:2]: for b_idx in [3, 4]: ch = patch[t*8 + b_idx] gx = np.diff(ch, axis=1); gy = np.diff(ch, axis=0) grad_mag = np.sqrt(np.mean(gx**2) + np.mean(gy**2)) lm = uniform_filter(ch, size=3); lv = uniform_filter(ch**2, size=3) - lm**2 feats.extend([grad_mag, np.mean(lv), np.std(lv)]) for b_idx in [6, 7]: ch = patch[0*8 + b_idx] gx = np.diff(ch, axis=1); gy = np.diff(ch, axis=0) grad_mag = np.sqrt(np.mean(gx**2) + np.mean(gy**2)) lm = uniform_filter(ch, size=3); lv = uniform_filter(ch**2, size=3) - lm**2 feats.extend([grad_mag, np.mean(lv), np.std(lv)]) needed = 2 * 2 * 3 got = min(len(valid_ts), 2) * 2 * 3 feats.extend([0.0] * (needed - got)) best_t = valid_ts[0] for b in range(8): ch = patch[best_t*8 + b] feats.extend(ch.flatten().tolist()) all_feats.append(feats) features = np.array(all_feats, dtype=np.float32) features = np.nan_to_num(features, nan=0.0, posinf=1e6, neginf=-1e6) return features class LightCNN_32ch(nn.Module): def __init__(self, in_ch=32, n_cls=7, width=96): super().__init__() self.net = nn.Sequential( nn.Conv2d(in_ch, width, 3, padding=1), nn.BatchNorm2d(width), nn.GELU(), nn.Conv2d(width, width, 3, padding=1), nn.BatchNorm2d(width), nn.GELU(), nn.MaxPool2d(2), nn.Dropout2d(0.05), nn.Conv2d(width, width*2, 3, padding=1), nn.BatchNorm2d(width*2), nn.GELU(), nn.Conv2d(width*2, width*2, 3, padding=1), nn.BatchNorm2d(width*2), nn.GELU(), nn.MaxPool2d(2), nn.Dropout2d(0.1), nn.Conv2d(width*2, width*4, 3, padding=1), nn.BatchNorm2d(width*4), nn.GELU(), nn.AdaptiveAvgPool2d(1), nn.Flatten(), ) self.head = nn.Sequential( nn.Linear(width*4, width*2), nn.GELU(), nn.Dropout(0.3), nn.Linear(width*2, n_cls) ) def forward(self, x): return self.head(self.net(x)) def embed(self, x): return self.net(x) def train_cnn_fusion(X, y, n_cls, seed=42): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') torch.manual_seed(seed) np.random.seed(seed) X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=seed, stratify=y) model = LightCNN_32ch(in_ch=32, n_cls=n_cls, width=128).to(device) cc = np.bincount(y_tr, minlength=n_cls) w = torch.FloatTensor((1.0/(cc+1)) / (1.0/(cc+1)).sum() * n_cls).to(device) crit = nn.CrossEntropyLoss(weight=w, label_smoothing=0.1) opt = optim.AdamW(model.parameters(), lr=4e-4, weight_decay=0.02) sched = optim.lr_scheduler.CosineAnnealingWarmRestarts(opt, T_0=40, T_mult=2, eta_min=1e-6) tr_t = torch.FloatTensor(X_tr) tr_y = torch.LongTensor(y_tr) te_t = torch.FloatTensor(X_te).to(device) best_acc, best_state, pat = 0, None, 0 for ep in range(300): model.train() perm = torch.randperm(len(tr_t)) for i in range(0, len(tr_t), 32): idx = perm[i:i+32] bx = tr_t[idx].to(device) by = tr_y[idx].to(device) if np.random.random() > 0.5: bx = torch.flip(bx, [2]) if np.random.random() > 0.5: bx = torch.flip(bx, [3]) if np.random.random() > 0.5: bx = torch.rot90(bx, np.random.randint(1,4), [2,3]) bx = bx + torch.randn_like(bx) * 0.02 if np.random.random() > 0.5 and len(bx) > 1: lam = np.random.beta(0.4, 0.4) i2 = torch.randperm(bx.size(0)) bx = lam*bx + (1-lam)*bx[i2] oh1 = torch.zeros(by.size(0), n_cls, device=device).scatter_(1, by.unsqueeze(1), 1) oh2 = torch.zeros(by.size(0), n_cls, device=device).scatter_(1, by[i2].unsqueeze(1), 1) out = model(bx) loss = (-(lam*oh1 + (1-lam)*oh2) * torch.log_softmax(out,1)).sum(1).mean() else: loss = crit(model(bx), by) opt.zero_grad(); loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() sched.step() model.eval() with torch.no_grad(): probs = [] for fn in [lambda x:x, lambda x:torch.flip(x,[2]), lambda x:torch.flip(x,[3])]: probs.append(torch.softmax(model(fn(te_t)), 1)) avg = torch.stack(probs).mean(0) preds = avg.argmax(1).cpu().numpy() acc = accuracy_score(y_te, preds) if acc > best_acc: best_acc = acc; best_state = {k:v.cpu().clone() for k,v in model.state_dict().items()}; pat = 0 else: pat += 1 if pat >= 60: break model.load_state_dict(best_state) return model, best_acc def train_hybrid_fusion(X, y, cnn_model, n_cls): print("\n" + "="*60) print("HYBRID FUSION ENSEMBLE: CNN embed + S1/S2 Rich features + XGB/LGBM/ETC") print("="*60) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') cnn_model = cnn_model.to(device).eval() with torch.no_grad(): embs = [] for i in range(0, len(X), 64): b = torch.FloatTensor(X[i:i+64]).to(device) embs.append(cnn_model.embed(b).cpu().numpy()) cnn_feat = np.concatenate(embs) rich = extract_features_fusion(X) combined = np.concatenate([cnn_feat, rich], axis=1) print(f" Final Feature Vector: {combined.shape}") scaler = StandardScaler() combined = scaler.fit_transform(combined) skf = StratifiedKFold(5, shuffle=True, random_state=42) cv_accs = [] for fold, (ti, vi) in enumerate(skf.split(combined, y)): xgb = XGBClassifier(n_estimators=1000, max_depth=7, learning_rate=0.03, subsample=0.8, colsample_bytree=0.5, tree_method='hist', device='cuda', random_state=42+fold, use_label_encoder=False, eval_metric='mlogloss') lgbm = LGBMClassifier(n_estimators=1000, max_depth=7, learning_rate=0.03, subsample=0.8, colsample_bytree=0.5, random_state=42+fold, verbosity=-1) etc = ExtraTreesClassifier(n_estimators=1000, max_depth=15, max_features='sqrt', random_state=42+fold, n_jobs=-1) ensemble = VotingClassifier(estimators=[ ('xgb', xgb), ('lgbm', lgbm), ('etc', etc) ], voting='soft') ensemble.fit(combined[ti], y[ti]) a = accuracy_score(y[vi], ensemble.predict(combined[vi])) cv_accs.append(a) print(f" Fold {fold+1}: {a:.4f}") cv_mean = np.mean(cv_accs) print(f" ✅ Ensemble CV Mean: {cv_mean:.4f} ± {np.std(cv_accs):.4f}") return cv_mean def main(): X, y, n_cls = load_and_clean() cnn_model, cnn_acc = train_cnn_fusion(X, y, n_cls) hyb_cv = train_hybrid_fusion(X, y, cnn_model, n_cls) print("\n" + "="*60) print("📊 FINAL RESULTS V5 (ENSEMBLE + RADAR)") print("="*60) res = { 'Hybrid Fusion Ensemble CV': hyb_cv, } for n, a in sorted(res.items(), key=lambda x:-x[1]): mk = "🏆" if a>=0.95 else "✅" if a>=0.90 else "📈" print(f" {mk} {n}: {a:.4f}") best = max(res.values()) if best >= 0.95: print(f"\n🎉 THÀNH CÔNG VƯỢT MỐC 95%! BEST: {best:.4f}") else: print(f"\n🏆 BEST: {best:.4f}") if __name__ == "__main__": main()