1315 lines
66 KiB
Python
Executable File
1315 lines
66 KiB
Python
Executable File
"""
|
||
Auto Report Generator for Land Classification
|
||
Tự động tạo báo cáo HTML chi tiết sau training/prediction
|
||
"""
|
||
|
||
import json
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
import base64
|
||
import io
|
||
|
||
# Optional: for generating charts
|
||
try:
|
||
import matplotlib
|
||
matplotlib.use('Agg') # Non-interactive backend
|
||
import matplotlib.pyplot as plt
|
||
import numpy as np
|
||
MATPLOTLIB_AVAILABLE = True
|
||
except ImportError:
|
||
MATPLOTLIB_AVAILABLE = False
|
||
|
||
|
||
def generate_confusion_matrix_image(conf_matrix, class_names):
|
||
"""Tạo hình ảnh confusion matrix dạng base64"""
|
||
if not MATPLOTLIB_AVAILABLE:
|
||
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)
|
||
|
||
im = ax.imshow(conf_matrix, interpolation='nearest', cmap=plt.cm.Blues)
|
||
ax.figure.colorbar(im, ax=ax)
|
||
|
||
ax.set(xticks=np.arange(len(class_names)),
|
||
yticks=np.arange(len(class_names)),
|
||
xticklabels=class_names, yticklabels=class_names,
|
||
title='Confusion Matrix',
|
||
ylabel='Thực tế (True)',
|
||
xlabel='Dự đoán (Predicted)')
|
||
|
||
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
|
||
|
||
# Add text annotations
|
||
thresh = conf_matrix.max() / 2.
|
||
for i in range(len(class_names)):
|
||
for j in range(len(class_names)):
|
||
ax.text(j, i, format(conf_matrix[i, j], 'd'),
|
||
ha="center", va="center",
|
||
color="white" if conf_matrix[i, j] > thresh else "black")
|
||
|
||
fig.tight_layout()
|
||
|
||
# Convert to base64
|
||
buf = io.BytesIO()
|
||
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
|
||
buf.seek(0)
|
||
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
|
||
plt.close(fig)
|
||
|
||
return img_base64
|
||
except Exception as e:
|
||
print(f"Error generating confusion matrix image: {e}")
|
||
return None
|
||
|
||
|
||
def generate_class_distribution_chart(class_names, classification_report):
|
||
"""Tạo biểu đồ phân bố các class dạng base64"""
|
||
if not MATPLOTLIB_AVAILABLE:
|
||
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_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_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')
|
||
plt.xticks(rotation=45, ha='right')
|
||
|
||
# Add value labels on bars
|
||
for bar, val in zip(bars, supports):
|
||
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()
|
||
|
||
# Convert to base64
|
||
buf = io.BytesIO()
|
||
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
|
||
buf.seek(0)
|
||
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
|
||
plt.close(fig)
|
||
|
||
return img_base64
|
||
except Exception as e:
|
||
print(f"Error generating class distribution chart: {e}")
|
||
return None
|
||
|
||
|
||
def generate_metrics_chart(class_names, classification_report):
|
||
"""Tạo biểu đồ precision/recall/f1 cho từng class"""
|
||
if not MATPLOTLIB_AVAILABLE:
|
||
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_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_str))
|
||
width = 0.25
|
||
|
||
fig, ax = plt.subplots(figsize=(12, 6))
|
||
|
||
bars1 = ax.bar(x - width, precisions, width, label='Precision', color='#3498db')
|
||
bars2 = ax.bar(x, recalls, width, label='Recall', color='#2ecc71')
|
||
bars3 = ax.bar(x + width, f1_scores, width, label='F1-Score', color='#e74c3c')
|
||
|
||
ax.set_xlabel('Loại đất')
|
||
ax.set_ylabel('Score')
|
||
ax.set_title('Precision / Recall / F1-Score theo loại đất')
|
||
ax.set_xticks(x)
|
||
ax.set_xticklabels(class_names_str, rotation=45, ha='right')
|
||
ax.legend()
|
||
ax.set_ylim(0, 1.1)
|
||
|
||
# Add grid
|
||
ax.yaxis.grid(True, linestyle='--', alpha=0.7)
|
||
|
||
fig.tight_layout()
|
||
|
||
# Convert to base64
|
||
buf = io.BytesIO()
|
||
plt.savefig(buf, format='png', dpi=100, bbox_inches='tight')
|
||
buf.seek(0)
|
||
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
|
||
plt.close(fig)
|
||
|
||
return img_base64
|
||
except Exception as e:
|
||
print(f"Error generating metrics chart: {e}")
|
||
return None
|
||
|
||
|
||
def generate_training_report(training_result, config=None):
|
||
"""
|
||
Tạo báo cáo HTML cho kết quả training
|
||
|
||
Args:
|
||
training_result: Dict chứa kết quả từ train_model()
|
||
config: Dict chứa cấu hình training (optional)
|
||
|
||
Returns:
|
||
Tuple (report_path, report_html)
|
||
"""
|
||
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
|
||
# Extract data from result
|
||
train_acc = training_result.get('train_accuracy', 0) * 100
|
||
test_acc = training_result.get('test_accuracy', 0) * 100
|
||
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 = [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', '')
|
||
bbox = training_result.get('bbox', [])
|
||
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
|
||
metrics_img = generate_metrics_chart(classes, cls_report) if cls_report else None
|
||
|
||
# Build classification report table
|
||
cls_report_rows = ""
|
||
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>
|
||
<td>{metrics.get('precision', 0):.3f}</td>
|
||
<td>{metrics.get('recall', 0):.3f}</td>
|
||
<td>{metrics.get('f1-score', 0):.3f}</td>
|
||
<td>{int(metrics.get('support', 0))}</td>
|
||
</tr>
|
||
"""
|
||
|
||
# Add averages
|
||
for avg_type in ['macro avg', 'weighted avg']:
|
||
if avg_type in cls_report:
|
||
metrics = cls_report[avg_type]
|
||
cls_report_rows += f"""
|
||
<tr style="background-color: #f0f0f0; font-weight: bold;">
|
||
<td>{avg_type}</td>
|
||
<td>{metrics.get('precision', 0):.3f}</td>
|
||
<td>{metrics.get('recall', 0):.3f}</td>
|
||
<td>{metrics.get('f1-score', 0):.3f}</td>
|
||
<td>{int(metrics.get('support', 0))}</td>
|
||
</tr>
|
||
"""
|
||
|
||
# Build confusion matrix table (fallback if no image)
|
||
conf_matrix_table = ""
|
||
if conf_matrix:
|
||
conf_matrix_table = "<table class='conf-matrix'><tr><th></th>"
|
||
for cls in classes:
|
||
conf_matrix_table += f"<th>{cls}</th>"
|
||
conf_matrix_table += "</tr>"
|
||
for i, row in enumerate(conf_matrix):
|
||
conf_matrix_table += f"<tr><th>{classes[i]}</th>"
|
||
for val in row:
|
||
conf_matrix_table += f"<td>{val}</td>"
|
||
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>
|
||
<html lang="vi">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Training Report - {timestamp}</title>
|
||
<style>
|
||
* {{
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}}
|
||
body {{
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: #f5f5f5;
|
||
padding: 20px;
|
||
line-height: 1.6;
|
||
}}
|
||
.container {{
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 15px;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
}}
|
||
.header {{
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 40px;
|
||
text-align: center;
|
||
}}
|
||
.header h1 {{
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
}}
|
||
.header .subtitle {{
|
||
opacity: 0.9;
|
||
font-size: 1.1em;
|
||
}}
|
||
.content {{
|
||
padding: 40px;
|
||
}}
|
||
.section {{
|
||
margin-bottom: 40px;
|
||
}}
|
||
.section h2 {{
|
||
color: #667eea;
|
||
border-bottom: 3px solid #667eea;
|
||
padding-bottom: 10px;
|
||
margin-bottom: 20px;
|
||
font-size: 1.5em;
|
||
}}
|
||
.stats-grid {{
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}}
|
||
.stat-card {{
|
||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||
padding: 25px;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
border: 1px solid #667eea30;
|
||
}}
|
||
.stat-card .value {{
|
||
font-size: 2.5em;
|
||
font-weight: bold;
|
||
color: #667eea;
|
||
}}
|
||
.stat-card .label {{
|
||
color: #666;
|
||
margin-top: 5px;
|
||
}}
|
||
.stat-card.success .value {{
|
||
color: #28a745;
|
||
}}
|
||
.stat-card.warning .value {{
|
||
color: #ffc107;
|
||
}}
|
||
table {{
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 20px 0;
|
||
}}
|
||
th, td {{
|
||
padding: 12px 15px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #ddd;
|
||
}}
|
||
th {{
|
||
background: #667eea;
|
||
color: white;
|
||
}}
|
||
tr:hover {{
|
||
background-color: #f5f5f5;
|
||
}}
|
||
.conf-matrix {{
|
||
font-size: 14px;
|
||
}}
|
||
.conf-matrix th, .conf-matrix td {{
|
||
text-align: center;
|
||
padding: 8px;
|
||
}}
|
||
.chart-container {{
|
||
text-align: center;
|
||
margin: 20px 0;
|
||
}}
|
||
.chart-container img {{
|
||
max-width: 100%;
|
||
border-radius: 10px;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||
}}
|
||
.info-box {{
|
||
background: #e3f2fd;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
border-left: 5px solid #2196f3;
|
||
margin: 20px 0;
|
||
}}
|
||
.info-row {{
|
||
display: flex;
|
||
margin: 10px 0;
|
||
}}
|
||
.info-label {{
|
||
font-weight: bold;
|
||
width: 200px;
|
||
color: #555;
|
||
}}
|
||
.info-value {{
|
||
color: #333;
|
||
}}
|
||
.footer {{
|
||
background: #f8f9fa;
|
||
padding: 20px;
|
||
text-align: center;
|
||
color: #666;
|
||
font-size: 14px;
|
||
}}
|
||
@media print {{
|
||
body {{
|
||
background: white;
|
||
padding: 0;
|
||
}}
|
||
.container {{
|
||
box-shadow: none;
|
||
}}
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>📊 Báo Cáo Training Model</h1>
|
||
<p class="subtitle">Land Classification - {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<!-- Summary Stats -->
|
||
<div class="section">
|
||
<h2>📈 Tóm Tắt Kết Quả</h2>
|
||
<div class="stats-grid">
|
||
<div class="stat-card success">
|
||
<div class="value">{train_acc:.1f}%</div>
|
||
<div class="label">Train Accuracy</div>
|
||
</div>
|
||
<div class="stat-card {'success' if test_acc >= 80 else 'warning'}">
|
||
<div class="value">{test_acc:.1f}%</div>
|
||
<div class="label">Test Accuracy</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="value">{train_samples}</div>
|
||
<div class="label">Training Samples</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="value">{test_samples}</div>
|
||
<div class="label">Testing Samples</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="value">{len(classes)}</div>
|
||
<div class="label">Số Classes</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<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>
|
||
<div class="info-box">
|
||
<div class="info-row">
|
||
<span class="info-label">🤖 Model Type:</span>
|
||
<span class="info-value">{model_type.upper()}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">📍 Khu vực (bbox):</span>
|
||
<span class="info-value">{bbox}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">📅 Thời gian:</span>
|
||
<span class="info-value">{time_range}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">📐 Độ phân giải:</span>
|
||
<span class="info-value">{resolution}m</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">💾 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>
|
||
|
||
<!-- Classification Report -->
|
||
<div class="section">
|
||
<h2>📋 Classification Report</h2>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Loại đất</th>
|
||
<th>Precision</th>
|
||
<th>Recall</th>
|
||
<th>F1-Score</th>
|
||
<th>Support</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{cls_report_rows}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Metrics Chart -->
|
||
{'<div class="section"><h2>📊 Biểu Đồ Metrics</h2><div class="chart-container"><img src="data:image/png;base64,' + metrics_img + '" alt="Metrics Chart"></div></div>' if metrics_img else ''}
|
||
|
||
<!-- Class Distribution -->
|
||
{'<div class="section"><h2>📊 Phân Bố Số Mẫu</h2><div class="chart-container"><img src="data:image/png;base64,' + class_dist_img + '" alt="Class Distribution"></div></div>' if class_dist_img else ''}
|
||
|
||
<!-- Confusion Matrix -->
|
||
<div class="section">
|
||
<h2>🔢 Confusion Matrix</h2>
|
||
{'<div class="chart-container"><img src="data:image/png;base64,' + conf_matrix_img + '" alt="Confusion Matrix"></div>' if conf_matrix_img else conf_matrix_table}
|
||
</div>
|
||
|
||
<!-- Classes List -->
|
||
<div class="section">
|
||
<h2>🏷️ Danh Sách Các Loại Đất</h2>
|
||
<div class="info-box">
|
||
<ul style="list-style: none; display: flex; flex-wrap: wrap; gap: 10px;">
|
||
{''.join([f'<li style="background: #667eea; color: white; padding: 8px 15px; border-radius: 20px;">{cls}</li>' for cls in classes])}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<p>🌍 Land Classification Training System | Generated: {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
|
||
<p>Data Source: Microsoft Planetary Computer (Sentinel-2 L2A, Sentinel-1 RTC)</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
# Save report
|
||
reports_dir = Path("reports")
|
||
reports_dir.mkdir(exist_ok=True)
|
||
|
||
report_filename = f"training_report_{timestamp}.html"
|
||
report_path = reports_dir / report_filename
|
||
|
||
with open(report_path, 'w', encoding='utf-8') as f:
|
||
f.write(html)
|
||
|
||
return str(report_path), html
|
||
|
||
|
||
def generate_prediction_report(prediction_result, config=None):
|
||
"""
|
||
Tạo báo cáo HTML cho kết quả prediction
|
||
|
||
Args:
|
||
prediction_result: Dict chứa kết quả prediction
|
||
config: Dict chứa cấu hình prediction (optional)
|
||
|
||
Returns:
|
||
Tuple (report_path, report_html)
|
||
"""
|
||
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
|
||
# Extract data
|
||
output_file = prediction_result.get('output_file', '')
|
||
shape = prediction_result.get('shape', [0, 0])
|
||
unique_classes = prediction_result.get('unique_classes', [])
|
||
bbox = prediction_result.get('bbox', [])
|
||
time_range = prediction_result.get('time_range', '')
|
||
n_features = prediction_result.get('n_features', 0)
|
||
used_radar = prediction_result.get('used_radar', False)
|
||
model_used = prediction_result.get('model_used', '')
|
||
|
||
# Calculate area (approximate)
|
||
if len(bbox) == 4:
|
||
# Approximate calculation (1 degree ≈ 111km at equator)
|
||
width_km = (bbox[2] - bbox[0]) * 111 * 0.85 # cos adjustment for Vietnam
|
||
height_km = (bbox[3] - bbox[1]) * 111
|
||
area_km2 = width_km * height_km
|
||
else:
|
||
area_km2 = 0
|
||
|
||
total_pixels = shape[0] * shape[1] if len(shape) == 2 else 0
|
||
|
||
# HTML Template
|
||
html = f"""
|
||
<!DOCTYPE html>
|
||
<html lang="vi">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Prediction Report - {timestamp}</title>
|
||
<style>
|
||
* {{
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}}
|
||
body {{
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: #f5f5f5;
|
||
padding: 20px;
|
||
line-height: 1.6;
|
||
}}
|
||
.container {{
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 15px;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
}}
|
||
.header {{
|
||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
||
color: white;
|
||
padding: 40px;
|
||
text-align: center;
|
||
}}
|
||
.header h1 {{
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
}}
|
||
.content {{
|
||
padding: 40px;
|
||
}}
|
||
.section {{
|
||
margin-bottom: 40px;
|
||
}}
|
||
.section h2 {{
|
||
color: #ff6b6b;
|
||
border-bottom: 3px solid #ff6b6b;
|
||
padding-bottom: 10px;
|
||
margin-bottom: 20px;
|
||
}}
|
||
.stats-grid {{
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 20px;
|
||
}}
|
||
.stat-card {{
|
||
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
|
||
padding: 25px;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
border: 1px solid #ff6b6b30;
|
||
}}
|
||
.stat-card .value {{
|
||
font-size: 2em;
|
||
font-weight: bold;
|
||
color: #ff6b6b;
|
||
}}
|
||
.stat-card .label {{
|
||
color: #666;
|
||
margin-top: 5px;
|
||
}}
|
||
.info-box {{
|
||
background: #fff3cd;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
border-left: 5px solid #ff6b6b;
|
||
margin: 20px 0;
|
||
}}
|
||
.info-row {{
|
||
display: flex;
|
||
margin: 10px 0;
|
||
}}
|
||
.info-label {{
|
||
font-weight: bold;
|
||
width: 200px;
|
||
color: #555;
|
||
}}
|
||
.class-badge {{
|
||
display: inline-block;
|
||
background: #ff6b6b;
|
||
color: white;
|
||
padding: 8px 15px;
|
||
border-radius: 20px;
|
||
margin: 5px;
|
||
}}
|
||
.footer {{
|
||
background: #f8f9fa;
|
||
padding: 20px;
|
||
text-align: center;
|
||
color: #666;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🗺️ Báo Cáo Dự Đoán</h1>
|
||
<p>Land Classification Prediction - {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<div class="section">
|
||
<h2>📈 Tóm Tắt Kết Quả</h2>
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="value">{total_pixels:,}</div>
|
||
<div class="label">Tổng số Pixels</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="value">{shape[0]}x{shape[1]}</div>
|
||
<div class="label">Kích thước (px)</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="value">{area_km2:.1f}</div>
|
||
<div class="label">Diện tích (km²)</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="value">{len(unique_classes)}</div>
|
||
<div class="label">Số Classes</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="value">{n_features}</div>
|
||
<div class="label">Số Features</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="value">{'✅' if used_radar else '❌'}</div>
|
||
<div class="label">Sử dụng Radar</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>⚙️ Thông Tin Chi Tiết</h2>
|
||
<div class="info-box">
|
||
<div class="info-row">
|
||
<span class="info-label">🤖 Model sử dụng:</span>
|
||
<span>{model_used}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">📍 Khu vực (bbox):</span>
|
||
<span>{bbox}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">📅 Thời gian:</span>
|
||
<span>{time_range}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">💾 Output file:</span>
|
||
<span>{output_file}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>🏷️ Các Classes Phát Hiện</h2>
|
||
<div>
|
||
{''.join([f'<span class="class-badge">{cls}</span>' for cls in unique_classes])}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<p>🌍 Land Classification System | Generated: {datetime.now().strftime("%d/%m/%Y %H:%M:%S")}</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
# Save report
|
||
reports_dir = Path("reports")
|
||
reports_dir.mkdir(exist_ok=True)
|
||
|
||
report_filename = f"prediction_report_{timestamp}.html"
|
||
report_path = reports_dir / report_filename
|
||
|
||
with open(report_path, 'w', encoding='utf-8') as f:
|
||
f.write(html)
|
||
|
||
return str(report_path), html
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# Test report generation
|
||
test_result = {
|
||
"success": True,
|
||
"train_accuracy": 0.95,
|
||
"test_accuracy": 0.87,
|
||
"training_samples": 800,
|
||
"testing_samples": 200,
|
||
"test_size": 0.2,
|
||
"classes": ["Lua", "Rung", "Nuoc", "Dan_cu", "Cay_lau_nam"],
|
||
"model_type": "xgboost",
|
||
"model_path": "model_train/model_xgboost_20251221.joblib",
|
||
"bbox": [105.6, 9.3, 106.2, 9.8],
|
||
"time_range": "2023-03-01/2023-05-31",
|
||
"resolution": 20,
|
||
"classification_report": {
|
||
"Lua": {"precision": 0.92, "recall": 0.89, "f1-score": 0.90, "support": 50},
|
||
"Rung": {"precision": 0.88, "recall": 0.91, "f1-score": 0.89, "support": 45},
|
||
"Nuoc": {"precision": 0.95, "recall": 0.93, "f1-score": 0.94, "support": 40},
|
||
"Dan_cu": {"precision": 0.85, "recall": 0.82, "f1-score": 0.83, "support": 35},
|
||
"Cay_lau_nam": {"precision": 0.80, "recall": 0.85, "f1-score": 0.82, "support": 30},
|
||
"macro avg": {"precision": 0.88, "recall": 0.88, "f1-score": 0.88, "support": 200},
|
||
"weighted avg": {"precision": 0.88, "recall": 0.87, "f1-score": 0.87, "support": 200}
|
||
},
|
||
"confusion_matrix": [
|
||
[45, 2, 1, 1, 1],
|
||
[3, 41, 0, 1, 0],
|
||
[1, 0, 37, 1, 1],
|
||
[2, 1, 1, 29, 2],
|
||
[1, 1, 1, 2, 26]
|
||
]
|
||
}
|
||
|
||
path, html = generate_training_report(test_result)
|
||
print(f"Report generated: {path}")
|