hoàn thành tính cận trên và cận dưới của tất cả các thuật toán

This commit is contained in:
Victor Phan
2026-02-24 21:36:02 +07:00
parent ae4d8cbbc9
commit 0ab6461882
203 changed files with 3089 additions and 86 deletions
Regular → Executable
+314 -13
View File
@@ -938,7 +938,21 @@ def train_model(
# Encode labels
label_encoder = LabelEncoder()
labels_encoded = label_encoder.fit_transform(labels)
# Compute class-distribution-aware baselines from FULL dataset (before split)
# More reliable than computing from test set (which may be small / accidentally balanced)
from collections import Counter
_full_counts = Counter(labels_encoded)
_full_total = len(labels_encoded)
_n_cls = len(label_encoder.classes_)
_full_props = [_full_counts.get(i, 0) / _full_total for i in range(_n_cls)]
majority_class_baseline = float(max(_full_props)) if _full_props else 1.0 / max(_n_cls, 1)
weighted_random_baseline = float(sum(p**2 for p in _full_props)) if _full_props else 1.0 / max(_n_cls, 1)
print(f"[BASELINE] Full dataset counts : {[_full_counts.get(i,0) for i in range(_n_cls)]}")
print(f"[BASELINE] Class proportions : {[f'{p:.3f}' for p in _full_props]}")
print(f"[BASELINE] Majority class : {majority_class_baseline*100:.2f}%")
print(f"[BASELINE] Weighted random : {weighted_random_baseline*100:.2f}%")
# Split data
X_train, X_test, y_train, y_test = train_test_split(
features, labels_encoded, test_size=test_size, random_state=42, stratify=labels_encoded
@@ -957,7 +971,7 @@ def train_model(
device=device if use_gpu else 'cpu',
tree_method='hist',
random_state=42,
eval_metric='mlogloss',
eval_metric=['mlogloss', 'merror'], # merror = 1 - accuracy, tracked per tree
verbosity=0
)
elif model_type == 'random_forest':
@@ -1010,7 +1024,17 @@ def train_model(
# Train CNN
update_status("Training CNN model with PyTorch...", 80)
epochs = min(50, n_estimators // 2) # Use n_estimators as epochs
# Validation dataset (for per-epoch accuracy)
val_dataset = TensorDataset(X_test_tensor, y_test_tensor)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)
# Best/worst checkpoint tracking
cnn_best_val_acc = -1.0
cnn_worst_val_acc = 101.0
cnn_best_checkpoint = None
cnn_worst_checkpoint = None
model.train()
for epoch in range(epochs):
epoch_loss = 0.0
@@ -1025,9 +1049,48 @@ def train_model(
epoch_loss += loss.item()
avg_train_loss = epoch_loss / len(train_loader)
# Validation pass every epoch
model.eval()
val_loss_ep = 0.0
correct_ep = 0
total_ep = 0
with torch.no_grad():
for batch_X, batch_y in val_loader:
batch_X, batch_y = batch_X.to(device), batch_y.to(device)
outputs = model(batch_X)
val_loss_ep += criterion(outputs, batch_y).item()
_, predicted = torch.max(outputs, 1)
total_ep += batch_y.size(0)
correct_ep += (predicted == batch_y).sum().item()
model.train()
avg_val_loss = val_loss_ep / len(val_loader)
val_acc_ep = correct_ep / total_ep # fraction
# Track best / worst
ck = {
"epoch": epoch + 1,
"accuracy": float(val_acc_ep),
"trainAcc": None, # CNN doesn't compute per-epoch train acc
"valAcc": float(val_acc_ep),
"trainLoss": float(avg_train_loss),
"valLoss": float(avg_val_loss),
}
if val_acc_ep > cnn_best_val_acc:
cnn_best_val_acc = val_acc_ep
cnn_best_checkpoint = ck.copy()
if val_acc_ep < cnn_worst_val_acc:
cnn_worst_val_acc = val_acc_ep
cnn_worst_checkpoint = ck.copy()
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)
update_status(f"CNN Epoch {epoch+1}/{epochs}, Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}, Val Acc: {val_acc_ep*100:.2f}%", 80 + (epoch / epochs) * 10)
xgb_best_checkpoint = cnn_best_checkpoint
xgb_worst_checkpoint = cnn_worst_checkpoint
update_status(f"CNN: best epoch #{cnn_best_checkpoint['epoch']} val_acc={cnn_best_checkpoint['accuracy']*100:.2f}% worst epoch #{cnn_worst_checkpoint['epoch']} val_acc={cnn_worst_checkpoint['accuracy']*100:.2f}%", 88)
# Move model to CPU for saving (compatible with non-GPU systems)
model = model.cpu()
@@ -1079,10 +1142,16 @@ def train_model(
# Train Swin-UNet
update_status("Training Swin-UNet model with PyTorch (with class weights)...", 80)
epochs = min(60, n_estimators // 2) # Swin-UNet benefits from more epochs
# Best/worst checkpoint tracking
swin_best_val_acc = -1.0
swin_worst_val_acc = 101.0
swin_best_checkpoint = None
swin_worst_checkpoint = None
# Validation dataset
val_dataset = TensorDataset(X_test_tensor, y_test_tensor)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)
model.train()
for epoch in range(epochs):
@@ -1126,7 +1195,23 @@ def train_model(
avg_val_loss = val_loss / len(val_loader)
val_acc = 100 * correct / total
lr = optimizer.param_groups[0]['lr']
# Track best / worst checkpoint (every epoch)
_ck_swin = {
"epoch": epoch + 1,
"accuracy": float(val_acc / 100),
"trainAcc": None,
"valAcc": float(val_acc / 100),
"trainLoss": float(avg_train_loss),
"valLoss": float(avg_val_loss),
}
if val_acc > swin_best_val_acc:
swin_best_val_acc = val_acc
swin_best_checkpoint = _ck_swin.copy()
if val_acc < swin_worst_val_acc:
swin_worst_val_acc = val_acc
swin_worst_checkpoint = _ck_swin.copy()
if (epoch + 1) % 5 == 0:
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)
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}%")
@@ -1142,6 +1227,11 @@ def train_model(
update_status(f"Swin-UNet early stopped at epoch {epoch+1}", 90)
break
xgb_best_checkpoint = swin_best_checkpoint
xgb_worst_checkpoint = swin_worst_checkpoint
if swin_best_checkpoint:
update_status(f"Swin-UNet: best epoch #{swin_best_checkpoint['epoch']} val_acc={swin_best_checkpoint['accuracy']*100:.2f}% worst epoch #{swin_worst_checkpoint['epoch']} val_acc={swin_worst_checkpoint['accuracy']*100:.2f}%", 88)
model = model.cpu()
model.device_used = str(device)
@@ -1191,10 +1281,16 @@ def train_model(
# Train MobileNetV3 + LR-ASPP
update_status("Training MobileNetV3 + LR-ASPP model with PyTorch...", 80)
epochs = min(60, n_estimators // 2)
# Best/worst checkpoint tracking
mob_best_val_acc = -1.0
mob_worst_val_acc = 101.0
mob_best_checkpoint = None
mob_worst_checkpoint = None
# Validation dataset
val_dataset = TensorDataset(X_test_tensor, y_test_tensor)
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
model.train()
for epoch in range(epochs):
@@ -1238,7 +1334,23 @@ def train_model(
# Update learning rate
scheduler.step(avg_val_loss)
lr = optimizer.param_groups[0]['lr']
# Track best / worst checkpoint (every epoch)
_ck_mob = {
"epoch": epoch + 1,
"accuracy": float(val_acc / 100),
"trainAcc": None,
"valAcc": float(val_acc / 100),
"trainLoss": float(avg_train_loss),
"valLoss": float(avg_val_loss),
}
if val_acc > mob_best_val_acc:
mob_best_val_acc = val_acc
mob_best_checkpoint = _ck_mob.copy()
if val_acc < mob_worst_val_acc:
mob_worst_val_acc = val_acc
mob_worst_checkpoint = _ck_mob.copy()
if (epoch + 1) % 5 == 0:
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)
print(f"[MOBILENET] Epoch {epoch+1}/{epochs} - Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}, Val Acc: {val_acc:.2f}%")
@@ -1254,6 +1366,11 @@ def train_model(
update_status(f"MobileNet early stopped at epoch {epoch+1}", 90)
break
xgb_best_checkpoint = mob_best_checkpoint
xgb_worst_checkpoint = mob_worst_checkpoint
if mob_best_checkpoint:
update_status(f"MobileNet: best epoch #{mob_best_checkpoint['epoch']} val_acc={mob_best_checkpoint['accuracy']*100:.2f}% worst epoch #{mob_worst_checkpoint['epoch']} val_acc={mob_worst_checkpoint['accuracy']*100:.2f}%", 88)
model = model.cpu()
model.device_used = str(device)
@@ -1261,8 +1378,159 @@ def train_model(
raise ValueError(f"Unknown model type: {model_type}. Choose: xgboost, random_forest, decision_tree, svm, cnn, swin-unet, mobilenet-lraspp")
# Fit non-neural-network models
# Note: CNN/Swin-UNet/MobileNet already set xgb_best/worst_checkpoint in their blocks above
if model_type not in ['cnn', 'swin-unet', 'mobilenet-lraspp']:
model.fit(X_train, y_train)
xgb_best_checkpoint = None
xgb_worst_checkpoint = None
if model_type == 'xgboost':
# Pass eval_set so XGBoost evaluates on train+val after each tree
model.fit(
X_train, y_train,
eval_set=[(X_train, y_train), (X_test, y_test)],
verbose=False # suppress per-tree stdout; use model.evals_result()
)
# ── Per-tree best / worst accuracy tracking ──────────────────
# validation_0 = train set, validation_1 = val/test set
evals = model.evals_result()
val_errors = evals.get('validation_1', {}).get('merror', []) # merror = 1 - accuracy
train_errors = evals.get('validation_0', {}).get('merror', [])
val_losses = evals.get('validation_1', {}).get('mlogloss', [])
train_losses = evals.get('validation_0', {}).get('mlogloss', [])
if val_errors:
# best tree = highest val accuracy (lowest merror)
# worst tree = lowest val accuracy (highest merror)
best_idx = int(np.argmin(val_errors))
worst_idx = int(np.argmax(val_errors))
val_accs = [1.0 - e for e in val_errors]
train_accs = [1.0 - e for e in train_errors] if train_errors else val_accs
xgb_best_checkpoint = {
"epoch": best_idx + 1, # 1-based tree number
"accuracy": float(val_accs[best_idx]),
"trainAcc": float(train_accs[best_idx]),
"valAcc": float(val_accs[best_idx]),
"trainLoss": float(train_losses[best_idx]) if train_losses else 0.0,
"valLoss": float(val_losses[best_idx]) if val_losses else 0.0,
}
xgb_worst_checkpoint = {
"epoch": worst_idx + 1,
"accuracy": float(val_accs[worst_idx]),
"trainAcc": float(train_accs[worst_idx]),
"valAcc": float(val_accs[worst_idx]),
"trainLoss": float(train_losses[worst_idx]) if train_losses else 0.0,
"valLoss": float(val_losses[worst_idx]) if val_losses else 0.0,
}
update_status(
f"XGBoost per-tree: best tree #{best_idx+1} val_acc={val_accs[best_idx]*100:.2f}% "
f"worst tree #{worst_idx+1} val_acc={val_accs[worst_idx]*100:.2f}%", 88
)
else:
model.fit(X_train, y_train)
# ── Per-tree individual accuracy for Random Forest ────────────────
if model_type == 'random_forest' and hasattr(model, 'estimators_') and model.estimators_:
n_trees = len(model.estimators_)
rf_val_accs = []
rf_train_accs = []
# Track individual tree accuracy (not cumulative) to capture real variance
for tree in model.estimators_:
rf_val_accs.append(float(np.mean(tree.predict(X_test) == y_test)))
rf_train_accs.append(float(np.mean(tree.predict(X_train) == y_train)))
best_idx_rf = int(np.argmax(rf_val_accs))
worst_idx_rf = int(np.argmin(rf_val_accs))
xgb_best_checkpoint = {
"epoch": best_idx_rf + 1,
"accuracy": rf_val_accs[best_idx_rf],
"trainAcc": rf_train_accs[best_idx_rf],
"valAcc": rf_val_accs[best_idx_rf],
"trainLoss": None,
"valLoss": None,
}
xgb_worst_checkpoint = {
"epoch": worst_idx_rf + 1,
"accuracy": rf_val_accs[worst_idx_rf],
"trainAcc": rf_train_accs[worst_idx_rf],
"valAcc": rf_val_accs[worst_idx_rf],
"trainLoss": None,
"valLoss": None,
}
update_status(
f"RandomForest per-tree: best #{best_idx_rf+1}/{n_trees} val_acc={rf_val_accs[best_idx_rf]*100:.2f}% "
f"worst #{worst_idx_rf+1}/{n_trees} val_acc={rf_val_accs[worst_idx_rf]*100:.2f}%", 88
)
elif model_type == 'decision_tree':
# Track accuracy per depth from 1 to max_depth to show real variance
dt_val_accs = []
dt_train_accs = []
_dt_params = {k: v for k, v in model.get_params().items() if k != 'max_depth'}
for d in range(1, max_depth + 1):
from sklearn.tree import DecisionTreeClassifier as _DTC
_tmp = _DTC(max_depth=d, **_dt_params)
_tmp.fit(X_train, y_train)
dt_val_accs.append(float(_tmp.score(X_test, y_test)))
dt_train_accs.append(float(_tmp.score(X_train, y_train)))
best_idx_dt = int(np.argmax(dt_val_accs))
worst_idx_dt = int(np.argmin(dt_val_accs))
xgb_best_checkpoint = {
"epoch": best_idx_dt + 1,
"accuracy": dt_val_accs[best_idx_dt],
"trainAcc": dt_train_accs[best_idx_dt],
"valAcc": dt_val_accs[best_idx_dt],
"trainLoss": None,
"valLoss": None,
}
xgb_worst_checkpoint = {
"epoch": worst_idx_dt + 1,
"accuracy": dt_val_accs[worst_idx_dt],
"trainAcc": dt_train_accs[worst_idx_dt],
"valAcc": dt_val_accs[worst_idx_dt],
"trainLoss": None,
"valLoss": None,
}
update_status(
f"DecisionTree per-depth: best depth={best_idx_dt+1} val_acc={dt_val_accs[best_idx_dt]*100:.2f}% "
f"worst depth={worst_idx_dt+1} val_acc={dt_val_accs[worst_idx_dt]*100:.2f}%", 88
)
elif model_type == 'svm':
# Sweep across C values to show real best/worst variation
_c_values = [0.01, 0.1, 1.0, 10.0, 100.0]
svm_val_accs = []
svm_train_accs = []
for _c in _c_values:
from sklearn.svm import SVC as _SVC
_tmp = _SVC(kernel='rbf', C=_c, random_state=42, verbose=False)
_tmp.fit(X_train, y_train)
svm_val_accs.append(float(_tmp.score(X_test, y_test)))
svm_train_accs.append(float(_tmp.score(X_train, y_train)))
best_idx_svm = int(np.argmax(svm_val_accs))
worst_idx_svm = int(np.argmin(svm_val_accs))
xgb_best_checkpoint = {
"epoch": best_idx_svm + 1,
"accuracy": svm_val_accs[best_idx_svm],
"trainAcc": svm_train_accs[best_idx_svm],
"valAcc": svm_val_accs[best_idx_svm],
"trainLoss": None,
"valLoss": None,
}
xgb_worst_checkpoint = {
"epoch": worst_idx_svm + 1,
"accuracy": svm_val_accs[worst_idx_svm],
"trainAcc": svm_train_accs[worst_idx_svm],
"valAcc": svm_val_accs[worst_idx_svm],
"trainLoss": None,
"valLoss": None,
}
update_status(
f"SVM C-sweep: best C={_c_values[best_idx_svm]} val_acc={svm_val_accs[best_idx_svm]*100:.2f}% "
f"worst C={_c_values[worst_idx_svm]} val_acc={svm_val_accs[worst_idx_svm]*100:.2f}%", 88
)
# Evaluate
update_status("Evaluating model...", 90)
@@ -1286,6 +1554,26 @@ def train_model(
# Confusion matrix
conf_matrix = confusion_matrix(y_test, y_pred).tolist()
# Compute class-distribution-aware baselines from test set support counts
# (kept for diagnostic printing; main values computed above from full dataset)
_supports = []
for cn in label_encoder.classes_:
key = str(cn)
if key in cls_report and isinstance(cls_report[key], dict):
_supports.append(cls_report[key]['support'])
else:
# Try integer key
for k, v in cls_report.items():
if isinstance(v, dict) and k not in ('accuracy', 'macro avg', 'weighted avg'):
pass
_supports = [] # lookup ambiguous, skip
break
if _supports:
_total_test = sum(_supports)
_props_test = [s / _total_test for s in _supports] if _total_test > 0 else []
print(f"[BASELINE] Test-set supports : {_supports}")
print(f"[BASELINE] Test-set majority : {max(_props_test)*100:.2f}% (fyi, using full-dataset value above)")
# Save model using ModelManager
update_status("Saving model...", 95)
@@ -1326,6 +1614,12 @@ def train_model(
"time_range": time_range,
"resolution": resolution
}
# Attach XGBoost per-tree best/worst checkpoint to metadata
if xgb_best_checkpoint is not None:
info["best_checkpoint"] = xgb_best_checkpoint
if xgb_worst_checkpoint is not None:
info["worst_checkpoint"] = xgb_worst_checkpoint
# Use ModelManager to save
from model_manager import get_model_manager
@@ -1353,12 +1647,19 @@ def train_model(
"testing_samples": len(X_test),
"test_size": test_size,
"classes": class_names,
"n_classes": len(class_names),
"majority_class_baseline": majority_class_baseline,
"weighted_random_baseline": weighted_random_baseline,
"classification_report": cls_report,
"confusion_matrix": conf_matrix,
"model_type": model_type,
"bbox": bbox,
"time_range": time_range,
"resolution": resolution
"resolution": resolution,
"best_checkpoint": xgb_best_checkpoint,
"worst_checkpoint": xgb_worst_checkpoint,
"majority_class_baseline": majority_class_baseline,
"weighted_random_baseline": weighted_random_baseline,
}
except InterruptedError as e: