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
+543 -15
View File
@@ -26,6 +26,8 @@ def generate_confusion_matrix_image(conf_matrix, class_names):
return None
try:
# Normalize class names to strings
class_names = [str(c) for c in class_names]
fig, ax = plt.subplots(figsize=(10, 8))
conf_matrix = np.array(conf_matrix)
@@ -70,18 +72,22 @@ def generate_class_distribution_chart(class_names, classification_report):
return None
try:
# Normalize keys to strings to handle integer/string mismatch
cls_report_str = {str(k): v for k, v in classification_report.items()}
class_names_str = [str(c) for c in class_names]
# Extract support (number of samples) for each class
supports = []
for cls in class_names:
if cls in classification_report:
supports.append(classification_report[cls].get('support', 0))
for cls in class_names_str:
if cls in cls_report_str:
supports.append(int(cls_report_str[cls].get('support', 0)))
else:
supports.append(0)
fig, ax = plt.subplots(figsize=(10, 6))
colors = plt.cm.Set3(np.linspace(0, 1, len(class_names)))
bars = ax.bar(class_names, supports, color=colors)
bars = ax.bar(class_names_str, supports, color=colors)
ax.set_xlabel('Loại đất')
ax.set_ylabel('Số mẫu')
ax.set_title('Phân bố số mẫu theo loại đất')
@@ -89,8 +95,9 @@ def generate_class_distribution_chart(class_names, classification_report):
# Add value labels on bars
for bar, val in zip(bars, supports):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
str(int(val)), ha='center', va='bottom', fontsize=9)
if val > 0:
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
str(val), ha='center', va='bottom', fontsize=9)
fig.tight_layout()
@@ -113,21 +120,25 @@ def generate_metrics_chart(class_names, classification_report):
return None
try:
# Normalize keys to strings to handle integer/string mismatch
cls_report_str = {str(k): v for k, v in classification_report.items()}
class_names_str = [str(c) for c in class_names]
precisions = []
recalls = []
f1_scores = []
for cls in class_names:
if cls in classification_report:
precisions.append(classification_report[cls].get('precision', 0))
recalls.append(classification_report[cls].get('recall', 0))
f1_scores.append(classification_report[cls].get('f1-score', 0))
for cls in class_names_str:
if cls in cls_report_str:
precisions.append(cls_report_str[cls].get('precision', 0))
recalls.append(cls_report_str[cls].get('recall', 0))
f1_scores.append(cls_report_str[cls].get('f1-score', 0))
else:
precisions.append(0)
recalls.append(0)
f1_scores.append(0)
x = np.arange(len(class_names))
x = np.arange(len(class_names_str))
width = 0.25
fig, ax = plt.subplots(figsize=(12, 6))
@@ -140,7 +151,7 @@ def generate_metrics_chart(class_names, classification_report):
ax.set_ylabel('Score')
ax.set_title('Precision / Recall / F1-Score theo loại đất')
ax.set_xticks(x)
ax.set_xticklabels(class_names, rotation=45, ha='right')
ax.set_xticklabels(class_names_str, rotation=45, ha='right')
ax.legend()
ax.set_ylim(0, 1.1)
@@ -182,8 +193,10 @@ def generate_training_report(training_result, config=None):
train_samples = training_result.get('training_samples', 0)
test_samples = training_result.get('testing_samples', 0)
test_size = training_result.get('test_size', 0.2)
classes = training_result.get('classes', [])
classes = [str(c) for c in training_result.get('classes', [])] # normalize to strings
cls_report = training_result.get('classification_report', {})
# Also normalize cls_report keys to strings (handles integer keys from JSON)
cls_report = {str(k): v for k, v in cls_report.items()}
conf_matrix = training_result.get('confusion_matrix', [])
model_type = training_result.get('model_type', 'unknown')
model_path = training_result.get('model_path', '')
@@ -191,6 +204,68 @@ def generate_training_report(training_result, config=None):
time_range = training_result.get('time_range', '')
resolution = training_result.get('resolution', 20)
# NEW: Extract PSNR metrics (for cloud removal)
val_psnr = training_result.get('val_psnr', None)
baseline_psnr = training_result.get('baseline_psnr', None)
improvement = None
if val_psnr is not None and baseline_psnr is not None:
improvement = val_psnr - baseline_psnr
# NEW: Extract Best Checkpoint data
best_checkpoint = training_result.get('best_checkpoint', training_result.get('bestCheckpoint', None))
# NEW: Extract Worst Checkpoint data (for performance range analysis)
worst_checkpoint = training_result.get('worst_checkpoint', training_result.get('worstCheckpoint', None))
# Calculate baseline comparison message for cloud removal Performance Range
baseline_comparison_msg = ""
if worst_checkpoint and 'modelPSNR' in worst_checkpoint and 'baselinePSNR' in worst_checkpoint:
worst_psnr = worst_checkpoint.get('modelPSNR', 0)
baseline = worst_checkpoint.get('baselinePSNR', 0)
margin = worst_psnr - baseline
if margin > 5:
baseline_comparison_msg = f'• <strong style="color: #2ecc71;">✅ Tốt:</strong> PSNR thấp nhất ({worst_psnr:.2f}dB) vượt baseline +{margin:.2f}dB - Model học tốt ngay cả trong worst case'
elif margin > 2:
baseline_comparison_msg = f'• <strong style="color: #f39c12;">⚠️ Khá:</strong> PSNR thấp nhất ({worst_psnr:.2f}dB) vượt baseline +{margin:.2f}dB - Cần cải thiện stability'
elif margin > 0:
baseline_comparison_msg = f'• <strong style="color: #e67e22;">⚠️ Yếu:</strong> PSNR thấp nhất ({worst_psnr:.2f}dB) chỉ vượt baseline +{margin:.2f}dB - Model không ổn định'
else:
baseline_comparison_msg = f'• <strong style="color: #e74c3c;">❌ Kém:</strong> PSNR thấp nhất ({worst_psnr:.2f}dB) không vượt baseline ({baseline:.2f}dB) - Model thất bại'
# NEW: Extract Random Baseline (for land classification)
random_baseline = training_result.get('random_baseline', None)
# If backend provided exact baselines, prefer majority_class_baseline over uniform 1/n_classes
majority_class_baseline = training_result.get('majority_class_baseline', None)
weighted_random_baseline = training_result.get('weighted_random_baseline', None)
# random_baseline from frontend is already in % (e.g. 28.5);
# majority_class_baseline from backend is a fraction (e.g. 0.285)
if random_baseline is None and majority_class_baseline is not None:
random_baseline = majority_class_baseline * 100
# NEW: Extract detailed hyperparameters
hyperparams = training_result.get('hyperparameters', {})
if not hyperparams and config:
hyperparams = config
# Extract common hyperparameters
n_estimators = training_result.get('n_estimators', hyperparams.get('n_estimators', 'N/A'))
max_depth = training_result.get('max_depth', hyperparams.get('max_depth', 'N/A'))
learning_rate = training_result.get('learning_rate', hyperparams.get('learning_rate', 'N/A'))
batch_size = training_result.get('batch_size', hyperparams.get('batch_size', 'N/A'))
num_epochs = training_result.get('num_epochs', hyperparams.get('num_epochs', 'N/A'))
use_gpu = training_result.get('use_gpu', hyperparams.get('use_gpu', False))
use_s1 = training_result.get('use_s1', hyperparams.get('use_s1', False))
# Feature information
features = training_result.get('features', [])
feature_mode = training_result.get('feature_mode', 'simple')
n_features = training_result.get('n_features', len(features) if features else 'N/A')
# Data source info
data_source = training_result.get('data_source', 'Unknown')
collections = training_result.get('collections', [])
# Generate charts
conf_matrix_img = generate_confusion_matrix_image(conf_matrix, classes) if conf_matrix else None
class_dist_img = generate_class_distribution_chart(classes, cls_report) if cls_report else None
@@ -201,6 +276,11 @@ def generate_training_report(training_result, config=None):
for cls in classes:
if cls in cls_report:
metrics = cls_report[cls]
elif str(cls) in cls_report:
metrics = cls_report[str(cls)]
else:
metrics = None
if metrics:
cls_report_rows += f"""
<tr>
<td><strong>{cls}</strong></td>
@@ -239,6 +319,33 @@ def generate_training_report(training_result, config=None):
conf_matrix_table += "</tr>"
conf_matrix_table += "</table>"
# Pre-compute accuracy Performance Range stats (avoid division-by-zero in f-strings)
_bc = best_checkpoint or {}
_wc = worst_checkpoint or {}
if best_checkpoint and worst_checkpoint and 'accuracy' in _bc and 'accuracy' in _wc:
_best_acc = _bc.get('accuracy', 0)
_worst_acc = _wc.get('accuracy', 0)
acc_range_pct = (_best_acc - _worst_acc) * 100
acc_avg_pct = ((_best_acc + _worst_acc) / 2) * 100
acc_avg_raw = (_best_acc + _worst_acc) / 2
if acc_avg_raw > 0:
acc_stability = max(0, (1 - (_best_acc - _worst_acc) / acc_avg_raw) * 100)
else:
acc_stability = 100.0 # best == worst == 0, consider stable
acc_stable_label = 'ổn định ✅' if acc_stability >= 80 else 'cần cải thiện ⚠️'
rnd_bl = random_baseline or 0
rnd_bl_display = f'{rnd_bl:.2f}' if rnd_bl else 'N/A'
worst_beat_baseline = rnd_bl > 0 and _worst_acc * 100 > rnd_bl
best_beat_margin = (_best_acc * 100 - rnd_bl) if rnd_bl else None
n_classes_display = len(classes) if classes else training_result.get('n_classes', '?')
else:
acc_range_pct = acc_avg_pct = acc_stability = 0.0
acc_stable_label = ''
rnd_bl = rnd_bl_display = 0
worst_beat_baseline = False
best_beat_margin = None
n_classes_display = '?'
# HTML Template
html = f"""
<!DOCTYPE html>
@@ -428,9 +535,338 @@ def generate_training_report(training_result, config=None):
<div class="value">{test_size*100:.0f}%</div>
<div class="label">Test Size</div>
</div>
{f'''<div class="stat-card success">
<div class="value">{val_psnr:.2f} dB</div>
<div class="label">📈 Model PSNR</div>
</div>''' if val_psnr is not None else ''}
{f'''<div class="stat-card">
<div class="value">{baseline_psnr:.2f} dB</div>
<div class="label">📉 Baseline PSNR</div>
</div>''' if baseline_psnr is not None else ''}
{f'''<div class="stat-card {'success' if improvement > 0 else 'warning'}">
<div class="value">{'+' if improvement > 0 else ''}{improvement:.2f} dB</div>
<div class="label">⚡ Improvement</div>
</div>''' if improvement is not None else ''}
</div>
</div>
<!-- Best Checkpoint (if available) -->
{f'''<div class="section">
<h2>🏆 Best Accuracy Checkpoint</h2>
<div style="background: linear-gradient(135deg, #ffd70020 0%, #ffa50020 100%); padding: 25px; border-radius: 10px; border: 2px solid #ffa500;">
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 3em; font-weight: bold; color: #ff8c00;">
{best_checkpoint.get('accuracy', 0) * 100:.2f}%
</div>
<div style="color: #666; font-size: 1.1em; margin-top: 5px;">
Đạt tại epoch {best_checkpoint.get('epoch', 'N/A')}
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px;">
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Accuracy</div>
<div style="font-size: 1.5em; font-weight: bold; color: #28a745; margin-top: 5px;">
{(best_checkpoint.get('trainAcc') or 0) * 100:.2f}%
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Accuracy</div>
<div style="font-size: 1.5em; font-weight: bold; color: #28a745; margin-top: 5px;">
{(best_checkpoint.get('valAcc') or 0) * 100:.2f}%
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{f"{best_checkpoint.get('trainLoss'):.4f}" if best_checkpoint.get('trainLoss') is not None else 'N/A'}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{f"{best_checkpoint.get('valLoss'):.4f}" if best_checkpoint.get('valLoss') is not None else 'N/A'}
</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>📊 Giải thích:</strong><br>
Đây là điểm checkpoint tốt nhất trong toàn bộ quá trình training, được ghi nhận khi validation accuracy đạt giá trị cao nhất.
Các metrics này thể hiện hiệu suất thực sự của model tại thời điểm tối ưu.
</div>
</div>
</div>''' if best_checkpoint else ''}
<!-- Worst Accuracy Checkpoint for Land Classification (if available) -->
{f'''<div class="section">
<h2>📉 Worst Accuracy Checkpoint (Cận Dưới)</h2>
<div style="background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%); padding: 25px; border-radius: 10px; border: 2px solid #667eea;">
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 3em; font-weight: bold; color: #667eea;">
{worst_checkpoint.get('accuracy', 0) * 100:.2f}%
</div>
<div style="color: #666; font-size: 1.1em; margin-top: 5px;">
Accuracy thấp nhất tại epoch {worst_checkpoint.get('epoch', 'N/A')} (cận dưới training)
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px;">
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Accuracy</div>
<div style="font-size: 1.5em; font-weight: bold; color: #28a745; margin-top: 5px;">
{(worst_checkpoint.get('trainAcc') or 0) * 100:.2f}%
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Accuracy</div>
<div style="font-size: 1.5em; font-weight: bold; color: #28a745; margin-top: 5px;">
{(worst_checkpoint.get('valAcc') or 0) * 100:.2f}%
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{f"{worst_checkpoint.get('trainLoss'):.4f}" if worst_checkpoint.get('trainLoss') is not None else 'N/A'}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{f"{worst_checkpoint.get('valLoss'):.4f}" if worst_checkpoint.get('valLoss') is not None else 'N/A'}
</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>📊 Giải thích:</strong><br>
Đây là epoch có accuracy thấp nhất trong quá trình training, thể hiện điểm cận dưới của khả năng model.
{f"Ngay cả ở epoch tệ nhất, accuracy vẫn {worst_checkpoint.get('accuracy', 0) * 100:.2f}% {'> random baseline' if random_baseline and worst_checkpoint.get('accuracy', 0) * 100 > random_baseline else '≤ random baseline'}." if random_baseline else ''}
</div>
</div>
</div>''' if worst_checkpoint and 'accuracy' in worst_checkpoint else ''}
<!-- Performance Range Analysis for Land Classification (if both best and worst available) -->
{f'''<div class="section">
<h2>📊 Performance Range & Random Baseline Analysis</h2>
<div style="background: linear-gradient(135deg, #f093fb20 0%, #f5576c20 100%); padding: 25px; border-radius: 10px; border: 2px solid #f093fb;">
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px;">
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Accuracy Range</div>
<div style="font-size: 2.5em; font-weight: bold; color: #667eea;">
{acc_range_pct:.2f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">% (max - min)</div>
<div style="color: #666; font-size: 0.85em; margin-top: 10px;">
Max: {_bc.get('accuracy', 0) * 100:.2f}%<br>
Min: {_wc.get('accuracy', 0) * 100:.2f}%
</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Average Accuracy</div>
<div style="font-size: 2.5em; font-weight: bold; color: #43e97b;">
{acc_avg_pct:.2f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">%</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Stability Score</div>
<div style="font-size: 2.5em; font-weight: bold; color: #f093fb;">
{acc_stability:.1f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">%</div>
<div style="color: #666; font-size: 0.85em; margin-top: 10px;">{acc_stable_label}</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Random Baseline</div>
<div style="font-size: 2.5em; font-weight: bold; color: #ff6b6b;">
{rnd_bl_display}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">% (lớp chiếm đa số)</div>
<div style="color: #666; font-size: 0.85em; margin-top: 10px;">{n_classes_display} classes{f'<br><span style="font-size:0.9em;color:#aaa;">Weighted: {weighted_random_baseline*100:.2f}%</span>' if weighted_random_baseline else ''}</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>💡 Đánh giá:</strong><br>
• <strong>Range</strong>: {acc_range_pct:.2f}% - Model {"ổn định" if acc_range_pct < 5 else "có dao động"} trong quá trình training<br>
• <strong>Worst Accuracy vs Random Baseline</strong>: {_wc.get('accuracy', 0) * 100:.2f}% {">" if worst_beat_baseline else ""} {rnd_bl_display}% → {"✅ Model luôn tốt hơn đoán ngẫu nhiên ngay cả ở điểm tệ nhất!" if worst_beat_baseline else ("⚠️ Điểm tệ nhất chưa vượt mức ngẫu nhiên - cần cải thiện hyperparameters!" if rnd_bl else "️ Không có dữ liệu random baseline")}<br>
• <strong>Stability</strong>: {acc_stability:.1f}% - Training {acc_stable_label}<br>
• <strong>Best Accuracy</strong>: {_bc.get('accuracy', 0) * 100:.2f}% {">" if best_beat_margin is not None and best_beat_margin > 0 else ""} {rnd_bl_display}% → {"✅ Vượt random baseline " + f"{best_beat_margin:.2f}%" if best_beat_margin is not None and best_beat_margin > 0 else "⚠️ Không vượt random baseline"}
</div>
</div>
</div>''' if best_checkpoint and worst_checkpoint and 'accuracy' in _bc and 'accuracy' in _wc else ''}
<!-- Best PSNR Checkpoint for Cloud Removal (if available) -->
{f'''<div class="section">
<h2>🏆 Best PSNR Checkpoint</h2>
<div style="background: linear-gradient(135deg, #ffd70020 0%, #ffa50020 100%); padding: 25px; border-radius: 10px; border: 2px solid #ffa500;">
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 3em; font-weight: bold; color: #ff8c00;">
{best_checkpoint.get('modelPSNR', 0):.2f} dB
</div>
<div style="color: #666; font-size: 1.1em; margin-top: 5px;">
Model PSNR đạt tại epoch {best_checkpoint.get('epoch', 'N/A')}
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px;">
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{best_checkpoint.get('trainLoss', 0):.6f}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{best_checkpoint.get('valLoss', 0):.6f}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Baseline PSNR</div>
<div style="font-size: 1.5em; font-weight: bold; color: #6c757d; margin-top: 5px;">
{best_checkpoint.get('baselinePSNR', 0):.2f} dB
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Improvement</div>
<div style="font-size: 1.5em; font-weight: bold; color: {'#28a745' if best_checkpoint.get('improvement', 0) > 0 else '#ffc107'}; margin-top: 5px;">
{'+' if best_checkpoint.get('improvement', 0) > 0 else ''}{best_checkpoint.get('improvement', 0):.2f} dB
</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>📊 Giải thích:</strong><br>
Đây là điểm checkpoint tốt nhất trong toàn bộ quá trình training cloud removal, được ghi nhận khi Model PSNR đạt giá trị cao nhất.
Improvement thể hiện mức cải thiện so với baseline (không xử lý gì).
</div>
</div>
</div>''' if best_checkpoint and 'modelPSNR' in best_checkpoint else ''}
<!-- Worst PSNR Checkpoint for Cloud Removal (if available) -->
{f'''<div class="section">
<h2>📉 Worst PSNR Checkpoint (Cận Dưới)</h2>
<div style="background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%); padding: 25px; border-radius: 10px; border: 2px solid #667eea;">
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 3em; font-weight: bold; color: #667eea;">
{worst_checkpoint.get('modelPSNR', 0):.2f} dB
</div>
<div style="color: #666; font-size: 1.1em; margin-top: 5px;">
PSNR thấp nhất tại epoch {worst_checkpoint.get('epoch', 'N/A')} (cận dưới training)
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px;">
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Train Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{worst_checkpoint.get('trainLoss', 0):.6f}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Val Loss</div>
<div style="font-size: 1.5em; font-weight: bold; color: #dc3545; margin-top: 5px;">
{worst_checkpoint.get('valLoss', 0):.6f}
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Baseline PSNR</div>
<div style="font-size: 1.5em; font-weight: bold; color: #6c757d; margin-top: 5px;">
{worst_checkpoint.get('baselinePSNR', 0):.2f} dB
</div>
</div>
<div style="background: white; padding: 15px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em;">Gap from Baseline</div>
<div style="font-size: 1.5em; font-weight: bold; color: {'#28a745' if worst_checkpoint.get('improvement', 0) > 0 else '#dc3545'}; margin-top: 5px;">
{'+' if worst_checkpoint.get('improvement', 0) > 0 else ''}{worst_checkpoint.get('improvement', 0):.2f} dB
</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>📊 Giải thích:</strong><br>
Đây là epoch có PSNR thấp nhất trong quá trình training, thể hiện điểm cận dưới của khả năng model.
Nếu worst PSNR vẫn > baseline, có nghĩa là ngay cả ở epoch tệ nhất, model vẫn tốt hơn không làm gì!
</div>
</div>
</div>''' if worst_checkpoint and 'modelPSNR' in worst_checkpoint else ''}
<!-- Performance Range Analysis (if both best and worst available) -->
{f'''<div class="section">
<h2>📊 Performance Range Analysis</h2>
<div style="background: linear-gradient(135deg, #f093fb20 0%, #f5576c20 100%); padding: 25px; border-radius: 10px; border: 2px solid #f093fb;">
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;">
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">PSNR Range</div>
<div style="font-size: 2.5em; font-weight: bold; color: #667eea;">
{(best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)):.2f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">dB (max - min)</div>
<div style="color: #666; font-size: 0.85em; margin-top: 10px;">
Max: {best_checkpoint.get('modelPSNR', 0):.2f} dB<br>
Min: {worst_checkpoint.get('modelPSNR', 0):.2f} dB
</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Average PSNR</div>
<div style="font-size: 2.5em; font-weight: bold; color: #43e97b;">
{((best_checkpoint.get('modelPSNR', 0) + worst_checkpoint.get('modelPSNR', 0)) / 2):.2f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">dB (estimated)</div>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">Baseline PSNR</div>
<div style="font-size: 2.5em; font-weight: bold; color: #ff6b6b;">
{worst_checkpoint.get('baselinePSNR', 0):.2f}
</div>
<div style="color: #888; font-size: 0.8em; margin-top: 5px;">dB (cận dưới)</div>
<div style="color: #666; font-size: 0.85em; margin-top: 10px;">
Cloudy vs Clean
</div>
</div>
</div>
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.7); border-radius: 5px; font-size: 0.95em;">
<strong>💡 Baseline Comparison:</strong><br>
{baseline_comparison_msg}<br>
• <strong>Range {'nhỏ' if (best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)) < 2 else 'lớn'}</strong> ({(best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)):.2f} dB): Training {'ổn định' if (best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)) < 2 else 'dao động'}<br>
• <strong>Stability {'>=' if max(0, (1 - (best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)) / ((best_checkpoint.get('modelPSNR', 0) + worst_checkpoint.get('modelPSNR', 0)) / 2)) * 100) >= 80 else '<'} 80%</strong>: Training {'rất ổn định ✅' if max(0, (1 - (best_checkpoint.get('modelPSNR', 0) - worst_checkpoint.get('modelPSNR', 0)) / ((best_checkpoint.get('modelPSNR', 0) + worst_checkpoint.get('modelPSNR', 0)) / 2)) * 100) >= 80 else 'cần cải thiện hyperparameters ⚠️'}
</div>
</div>
</div>''' if best_checkpoint and worst_checkpoint and 'modelPSNR' in best_checkpoint and 'modelPSNR' in worst_checkpoint else ''}
<!-- PSNR Analysis (if available) -->
{f'''<div class="section">
<h2>🎯 PSNR Baseline Analysis</h2>
<div class="info-box" style="background: {'#d4edda' if improvement > 0 else '#fff3cd'}; border-left-color: {'#28a745' if improvement > 0 else '#ffc107'};">
<div class="info-row">
<span class="info-label">📈 Model PSNR:</span>
<span class="info-value" style="font-weight: bold; font-size: 1.1em;">{val_psnr:.2f} dB</span>
</div>
<div class="info-row">
<span class="info-label">📉 Baseline PSNR (cận dưới):</span>
<span class="info-value">{baseline_psnr:.2f} dB</span>
</div>
<div class="info-row">
<span class="info-label">⚡ Improvement:</span>
<span class="info-value" style="color: {'green' if improvement > 0 else 'orange'}; font-weight: bold;">{'+' if improvement > 0 else ''}{improvement:.2f} dB ({'+' if improvement > 0 else ''}{(improvement/baseline_psnr*100):.1f}%)</span>
</div>
<div class="info-row" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #ddd;">
<span class="info-label">📊 Đánh giá:</span>
<span class="info-value" style="font-weight: bold;">{'✅ Model TỐT HƠN baseline - Kết quả đáng tin cậy!' if improvement > 0 else '⚠️ Model chưa vượt qua baseline - Cần điều chỉnh!'}</span>
</div>
<div style="margin-top: 15px; padding: 10px; background: rgba(0,0,0,0.05); border-radius: 5px; font-size: 0.9em;">
<strong>Giải thích:</strong><br>
- <strong>Baseline PSNR</strong>: PSNR giữa ảnh cloudy và ảnh clean (không làm gì)<br>
- <strong>Model PSNR</strong>: PSNR giữa output model và ảnh clean<br>
- <strong>Improvement > 0</strong>: Model đang khử mây hiệu quả!<br>
- <strong>Improvement < 0</strong>: Model làm tồi hơn không làm gì!
</div>
</div>
</div>''' if val_psnr is not None and baseline_psnr is not None else ''}
<!-- Configuration Info -->
<div class="section">
<h2>⚙️ Cấu Hình Training</h2>
@@ -453,7 +889,99 @@ def generate_training_report(training_result, config=None):
</div>
<div class="info-row">
<span class="info-label">💾 Model Path:</span>
<span class="info-value">{model_path}</span>
<span class="info-value" style="font-family: monospace; font-size: 0.9em;">{model_path}</span>
</div>
<div class="info-row">
<span class="info-label">📊 Data Source:</span>
<span class="info-value">{data_source}</span>
</div>
{f'''<div class="info-row">
<span class="info-label">🛰️ Collections:</span>
<span class="info-value">{', '.join(collections)}</span>
</div>''' if collections else ''}
</div>
</div>
<!-- Hyperparameters Detail -->
<div class="section">
<h2>🎯 Hyperparameters - Chi Tiết Tái Hiện</h2>
<div class="info-box" style="background: #fff3cd; border-left-color: #ffc107;">
<div style="margin-bottom: 15px; padding: 10px; background: rgba(0,0,0,0.05); border-radius: 5px;">
<strong>🔄 Để tái hiện kết quả training này, sử dụng các tham số dưới đây:</strong>
</div>
<h3 style="color: #ffc107; margin-top: 20px; margin-bottom: 10px;">📊 Model Hyperparameters</h3>
<div class="info-row">
<span class="info-label">Model Type:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{model_type}</span>
</div>
{f'''<div class="info-row">
<span class="info-label">n_estimators:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{n_estimators}</span>
</div>''' if n_estimators != 'N/A' else ''}
{f'''<div class="info-row">
<span class="info-label">max_depth:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{max_depth}</span>
</div>''' if max_depth != 'N/A' else ''}
{f'''<div class="info-row">
<span class="info-label">learning_rate:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{learning_rate}</span>
</div>''' if learning_rate != 'N/A' else ''}
{f'''<div class="info-row">
<span class="info-label">batch_size:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{batch_size}</span>
</div>''' if batch_size != 'N/A' else ''}
{f'''<div class="info-row">
<span class="info-label">num_epochs:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{num_epochs}</span>
</div>''' if num_epochs != 'N/A' else ''}
<div class="info-row">
<span class="info-label">use_gpu:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{use_gpu}</span>
</div>
{f'''<div class="info-row">
<span class="info-label">use_s1 (Sentinel-1):</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{use_s1}</span>
</div>''' if model_type in ['cloud_removal', 'unet'] else ''}
<h3 style="color: #ffc107; margin-top: 20px; margin-bottom: 10px;">📦 Data Processing</h3>
<div class="info-row">
<span class="info-label">test_size:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{test_size}</span>
</div>
<div class="info-row">
<span class="info-label">feature_mode:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{feature_mode}</span>
</div>
<div class="info-row">
<span class="info-label">n_features:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{n_features}</span>
</div>
{f'''<div class="info-row">
<span class="info-label">features:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;">{', '.join(features)}</span>
</div>''' if features else ''}
<h3 style="color: #ffc107; margin-top: 20px; margin-bottom: 10px;">🌍 Geo & Time Parameters</h3>
<div class="info-row">
<span class="info-label">bbox:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px; font-size: 0.85em;">{bbox}</span>
</div>
<div class="info-row">
<span class="info-label">time_range:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{time_range}</span>
</div>
<div class="info-row">
<span class="info-label">resolution:</span>
<span class="info-value" style="font-family: monospace; background: #f8f9fa; padding: 2px 8px; border-radius: 3px;">{resolution}m</span>
</div>
<div style="margin-top: 20px; padding: 15px; background: #e3f2fd; border-radius: 5px; border-left: 4px solid #2196f3;">
<strong>📝 Lưu ý:</strong><br>
• Lưu toàn bộ các tham số trên để reproduce kết quả<br>
• Sử dụng cùng dataset và time range để đảm bảo tính nhất quán<br>
• Random seed: 42 (mặc định)<br>
• Generated: {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}
</div>
</div>
</div>