|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Deep Learning Training Monitor</title> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script> |
|
<style> |
|
|
|
body { |
|
font-family: system-ui, -apple-system, sans-serif; |
|
margin: 0; |
|
padding: 16px; |
|
background-color: #f5f5f5; |
|
color: #333; |
|
} |
|
|
|
.dashboard-grid { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); |
|
gap: 16px; |
|
margin-bottom: 16px; |
|
} |
|
|
|
.chart-container { |
|
position: relative; |
|
width: 100%; |
|
} |
|
|
|
.card { |
|
background: white; |
|
padding: 16px; |
|
border-radius: 8px; |
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
|
} |
|
|
|
|
|
.predictions-container { |
|
margin-top: 16px; |
|
} |
|
|
|
.category-section { |
|
background: white; |
|
padding: 16px; |
|
border-radius: 8px; |
|
margin-bottom: 16px; |
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
|
} |
|
|
|
.category-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 12px; |
|
padding-bottom: 8px; |
|
border-bottom: 1px solid #e5e7eb; |
|
} |
|
|
|
.category-title { |
|
font-size: 16px; |
|
font-weight: 600; |
|
color: #1f2937; |
|
} |
|
|
|
.stats-badges { |
|
display: flex; |
|
gap: 8px; |
|
} |
|
|
|
.badge { |
|
padding: 4px 8px; |
|
border-radius: 4px; |
|
font-size: 12px; |
|
font-weight: 500; |
|
} |
|
|
|
.badge-tp { |
|
background-color: #d1fae5; |
|
color: #065f46; |
|
} |
|
|
|
.badge-fp { |
|
background-color: #ffedd5; |
|
color: #9a3412; |
|
} |
|
|
|
.badge-fn { |
|
background-color: #fee2e2; |
|
color: #991b1b; |
|
} |
|
|
|
.tag-list { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 8px; |
|
} |
|
|
|
.tag-item { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 8px 12px; |
|
border-radius: 6px; |
|
border: 1px solid; |
|
} |
|
|
|
.tag-tp { |
|
background-color: #ecfdf5; |
|
border-color: #6ee7b7; |
|
color: #065f46; |
|
} |
|
|
|
.tag-fp { |
|
background-color: #fff7ed; |
|
border-color: #fdba74; |
|
color: #9a3412; |
|
} |
|
|
|
.tag-fn { |
|
background-color: #fef2f2; |
|
border-color: #fca5a5; |
|
color: #991b1b; |
|
} |
|
|
|
.tag-info { |
|
display: flex; |
|
align-items: center; |
|
gap: 12px; |
|
} |
|
|
|
.tag-name { |
|
font-weight: 500; |
|
} |
|
|
|
.tag-probability { |
|
font-size: 0.9em; |
|
opacity: 0.85; |
|
} |
|
|
|
.tag-status { |
|
font-size: 12px; |
|
font-weight: 500; |
|
padding: 2px 6px; |
|
border-radius: 4px; |
|
} |
|
|
|
.status-tp { |
|
background-color: #d1fae5; |
|
} |
|
|
|
.status-fp { |
|
background-color: #ffedd5; |
|
} |
|
|
|
.status-fn { |
|
background-color: #fee2e2; |
|
} |
|
|
|
.badge { |
|
font-size: 12px; |
|
font-weight: 500; |
|
padding: 2px 6px; |
|
border-radius: 4px; |
|
} |
|
|
|
.badge-success { |
|
background-color: #d1fae5; |
|
color: #065f46; |
|
} |
|
|
|
.badge-warning { |
|
background-color: #ffedd5; |
|
color: #9a3412; |
|
} |
|
|
|
.badge-error { |
|
background-color: #fee2e2; |
|
color: #991b1b; |
|
} |
|
|
|
.stage-header { |
|
font-size: 18px; |
|
font-weight: 600; |
|
color: #1f2937; |
|
margin: 16px 0; |
|
padding-bottom: 8px; |
|
border-bottom: 2px solid #e5e7eb; |
|
} |
|
|
|
.category-section { |
|
margin-bottom: 20px; |
|
} |
|
|
|
.category-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 12px; |
|
} |
|
|
|
.category-stats { |
|
display: flex; |
|
gap: 8px; |
|
} |
|
|
|
.selected-tags { |
|
margin-top: 16px; |
|
} |
|
|
|
.selected-tag-item { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 6px 12px; |
|
background: #f9fafb; |
|
border: 1px solid #e5e7eb; |
|
border-radius: 4px; |
|
margin-bottom: 4px; |
|
} |
|
|
|
.ground-truth-marker { |
|
font-weight: bold; |
|
margin-left: 8px; |
|
} |
|
|
|
.ground-truth-yes { |
|
color: #059669; |
|
} |
|
|
|
.ground-truth-no { |
|
color: #dc2626; |
|
} |
|
|
|
.filter-btn { |
|
padding: 4px 8px; |
|
margin: 0 4px; |
|
border-radius: 4px; |
|
font-size: 12px; |
|
cursor: pointer; |
|
background: #e5e7eb; |
|
border: none; |
|
} |
|
|
|
.filter-btn.active { |
|
background: #3b82f6; |
|
color: white; |
|
} |
|
|
|
.category-section { |
|
background: white; |
|
padding: 16px; |
|
border-radius: 8px; |
|
margin-bottom: 16px; |
|
} |
|
|
|
.category-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 12px; |
|
padding-bottom: 8px; |
|
border-bottom: 1px solid #e5e7eb; |
|
} |
|
|
|
.tag-list { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 8px; |
|
} |
|
|
|
.tag-item { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 8px 12px; |
|
border-radius: 6px; |
|
background: #f9fafb; |
|
border: 1px solid #e5e7eb; |
|
} |
|
|
|
.tag-item.correct { |
|
background-color: #ecfdf5; |
|
border-color: #6ee7b7; |
|
} |
|
|
|
.tag-item.incorrect { |
|
background-color: #fff7ed; |
|
border-color: #fdba74; |
|
} |
|
|
|
.tag-item.missing { |
|
background-color: #fef2f2; |
|
border-color: #fca5a5; |
|
} |
|
|
|
.probability-bar { |
|
width: 100px; |
|
height: 6px; |
|
background: #e5e7eb; |
|
border-radius: 3px; |
|
overflow: hidden; |
|
} |
|
|
|
.probability-fill { |
|
height: 100%; |
|
background: #3b82f6; |
|
transition: width 0.3s ease; |
|
} |
|
|
|
.category-title { |
|
font-size: 16px; |
|
font-weight: 600; |
|
color: #1f2937; |
|
background: #f3f4f6; |
|
padding: 8px 12px; |
|
border-radius: 4px; |
|
margin-bottom: 12px; |
|
} |
|
|
|
.selected-tag-item { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 8px 12px; |
|
margin-bottom: 8px; |
|
border-radius: 6px; |
|
background: #f9fafb; |
|
border: 1px solid #e5e7eb; |
|
} |
|
|
|
.selected-tag-item.ground-truth { |
|
background-color: #ecfdf5; |
|
border-color: #6ee7b7; |
|
} |
|
|
|
.selected-tag-item.non-ground-truth { |
|
background-color: #fff7ed; |
|
border-color: #fdba74; |
|
} |
|
|
|
.tag-confidence { |
|
display: flex; |
|
align-items: center; |
|
gap: 8px; |
|
} |
|
|
|
.confidence-bar { |
|
width: 100px; |
|
height: 6px; |
|
background: #e5e7eb; |
|
border-radius: 3px; |
|
overflow: hidden; |
|
} |
|
|
|
.confidence-fill { |
|
height: 100%; |
|
transition: width 0.3s ease; |
|
} |
|
|
|
.ground-truth .confidence-fill { |
|
background: #059669; |
|
} |
|
|
|
.non-ground-truth .confidence-fill { |
|
background: #f97316; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<h1>Training Monitor</h1> |
|
|
|
<div class="tabs"> |
|
<div class="tab active" onclick="showTab('overview')">Overview</div> |
|
<div class="tab" onclick="showTab('predictions')">Predictions</div> |
|
<div class="tab" onclick="showTab('selection')">Selection Analysis</div> |
|
</div> |
|
|
|
<div class="card"> |
|
<h3>Training Progress</h3> |
|
<div class="metrics-grid"> |
|
<div class="metric-card"> |
|
<div class="metric-label">Current Epoch</div> |
|
<div class="metric-value" id="current-epoch">0/0</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Batch Progress</div> |
|
<div class="metric-value" id="current-batch">0/0</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Speed (it/s)</div> |
|
<div class="metric-value" id="iter-speed">0.00</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Time Left</div> |
|
<div class="metric-value" id="time-remaining">--:--:--</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Time Elapsed</div> |
|
<div class="metric-value" id="time-elapsed">00:00:00</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Loss</div> |
|
<div class="metric-value" id="current-loss">0.00000</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Initial F1</div> |
|
<div class="metric-value" id="current-initial-f1">0.00000</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Refined F1</div> |
|
<div class="metric-value" id="current-refined-f1">0.00000</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div id="overview"> |
|
<div class="dashboard-grid"> |
|
<div class="card" style="height: 400px;"> |
|
<h3>Loss</h3> |
|
<div class="chart-container" style="height: 350px;"> |
|
<canvas id="loss-chart"></canvas> |
|
</div> |
|
</div> |
|
|
|
<div class="card" style="height: 400px;"> |
|
<h3>F1 Scores</h3> |
|
<div class="chart-container" style="height: 350px;"> |
|
<canvas id="f1-chart"></canvas> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="predictions" class="tab-content"> |
|
<div class="grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;"> |
|
|
|
<div class="card"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h3>Predictions</h3> |
|
<div class="prediction-controls"> |
|
<button onclick="togglePredictionType()" id="prediction-type-toggle" class="px-3 py-1 bg-blue-500 text-white rounded text-sm"> |
|
Show Refined |
|
</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="flex justify-between items-center mb-4"> |
|
<button class="px-3 py-1 bg-blue-500 text-white rounded text-sm" onclick="previousPrediction()">Previous</button> |
|
<span id="prediction-counter" class="text-sm font-medium">Sample 0 / 0</span> |
|
<button class="px-3 py-1 bg-blue-500 text-white rounded text-sm" onclick="nextPrediction()">Next</button> |
|
</div> |
|
|
|
|
|
<div class="h-96 flex items-center justify-center bg-gray-100 rounded-lg mb-4"> |
|
<img id="current-image" src="" alt="Current sample" class="max-h-full object-contain" /> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="card"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h3>Tag Analysis</h3> |
|
<div class="tag-filters"> |
|
<button class="filter-btn active" onclick="filterTags('all')">All</button> |
|
<button class="filter-btn" onclick="filterTags('correct')">Correct</button> |
|
<button class="filter-btn" onclick="filterTags('incorrect')">Incorrect</button> |
|
<button class="filter-btn" onclick="filterTags('missing')">Missing</button> |
|
</div> |
|
</div> |
|
|
|
<div id="category-predictions" class="overflow-auto" style="max-height: 600px;"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="selection" class="tab-content"> |
|
<div class="card mb-4"> |
|
<h3>Selection Analysis Metrics</h3> |
|
<div class="metrics-grid"> |
|
<div class="metric-card"> |
|
<div class="metric-label">Total Ground Truth Tags</div> |
|
<div class="metric-value" id="total-gt-tags">0</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Selected Ground Truth Tags</div> |
|
<div class="metric-value" id="selected-gt-tags">0</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Ground Truth Recall</div> |
|
<div class="metric-value" id="gt-recall">0.0000</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Avg Prob (GT)</div> |
|
<div class="metric-value" id="avg-prob-gt">0.0000</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Avg Prob (Non-GT)</div> |
|
<div class="metric-value" id="avg-prob-non-gt">0.0000</div> |
|
</div> |
|
<div class="metric-card"> |
|
<div class="metric-label">Unique Tags Selected</div> |
|
<div class="metric-value" id="unique-tags-selected">0</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="card"> |
|
<h3>Selection Analysis Graph</h3> |
|
<div class="chart-container" style="height: 400px;"> |
|
<canvas id="selection-chart"></canvas> |
|
</div> |
|
</div> |
|
|
|
<div class="card"> |
|
<h3>Selected Tags Details</h3> |
|
<div class="tag-filters mb-4"> |
|
<button class="filter-btn active" onclick="filterSelectedTags('all')">All Selected</button> |
|
<button class="filter-btn" onclick="filterSelectedTags('ground-truth')">Ground Truth</button> |
|
<button class="filter-btn" onclick="filterSelectedTags('non-ground-truth')">Non-Ground Truth</button> |
|
</div> |
|
<div id="selected-tags-list" class="overflow-auto" style="max-height: 500px;"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
const socket = io(); |
|
|
|
function showTab(tabName) { |
|
|
|
document.querySelectorAll('.tab-content').forEach(tab => { |
|
tab.style.display = 'none'; |
|
}); |
|
|
|
|
|
const selectedTab = document.getElementById(tabName); |
|
if (selectedTab) { |
|
selectedTab.style.display = 'block'; |
|
} |
|
|
|
|
|
document.querySelectorAll('.tab').forEach(tab => { |
|
tab.classList.remove('active'); |
|
if (tab.textContent.toLowerCase().includes(tabName)) { |
|
tab.classList.add('active'); |
|
} |
|
}); |
|
} |
|
|
|
const charts = {}; |
|
const CHART_WINDOW_SIZE = 200; |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
function initializeCharts() { |
|
const tooltipCallback = (context) => { |
|
let label = context.dataset.label || ''; |
|
let value = context.parsed.y; |
|
if (value !== null) { |
|
value = value.toFixed(6); |
|
} |
|
return `${label}: ${value}`; |
|
}; |
|
|
|
|
|
const chartOptions = { |
|
responsive: true, |
|
maintainAspectRatio: false, |
|
scales: { |
|
y: { |
|
beginAtZero: false, |
|
suggestedMin: -0.2, |
|
suggestedMax: 1.2 |
|
} |
|
}, |
|
plugins: { |
|
legend: { |
|
position: 'top', |
|
}, |
|
tooltip: { |
|
callbacks: { |
|
label: tooltipCallback |
|
} |
|
} |
|
} |
|
}; |
|
|
|
|
|
const chartElements = { |
|
loss: document.getElementById('loss-chart'), |
|
f1: document.getElementById('f1-chart'), |
|
selection: document.getElementById('selection-chart') |
|
}; |
|
|
|
if (chartElements.loss) { |
|
charts.loss = new Chart(chartElements.loss.getContext('2d'), { |
|
type: 'line', |
|
data: { |
|
labels: [], |
|
datasets: [{ |
|
label: 'Training Loss', |
|
data: [], |
|
borderColor: '#ff6384', |
|
tension: 0.1 |
|
}, { |
|
label: 'Validation Loss', |
|
data: [], |
|
borderColor: '#4bc0c0', |
|
tension: 0.1, |
|
borderDash: [5, 5] |
|
}] |
|
}, |
|
options: { |
|
...chartOptions, |
|
scales: { |
|
y: { |
|
beginAtZero: true |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
|
|
if (chartElements.f1) { |
|
charts.f1 = new Chart(chartElements.f1.getContext('2d'), { |
|
type: 'line', |
|
data: { |
|
labels: [], |
|
datasets: [{ |
|
label: 'Train Initial F1', |
|
data: [], |
|
borderColor: '#36a2eb', |
|
tension: 0.1 |
|
}, { |
|
label: 'Train Refined F1', |
|
data: [], |
|
borderColor: '#4bc0c0', |
|
tension: 0.1 |
|
}, { |
|
label: 'Val Initial F1', |
|
data: [], |
|
borderColor: '#ff6384', |
|
tension: 0.1, |
|
borderDash: [5, 5] |
|
}, { |
|
label: 'Val Refined F1', |
|
data: [], |
|
borderColor: '#ffcd56', |
|
tension: 0.1, |
|
borderDash: [5, 5] |
|
}] |
|
}, |
|
options: { |
|
...chartOptions, |
|
scales: { |
|
y: { |
|
beginAtZero: true, |
|
max: 1 |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
|
|
if (chartElements.selection) { |
|
charts.selection = new Chart(chartElements.selection.getContext('2d'), { |
|
type: 'line', |
|
data: { |
|
labels: [], |
|
datasets: [{ |
|
label: 'Ground Truth Recall', |
|
data: [], |
|
borderColor: '#4bc0c0', |
|
tension: 0.1 |
|
}, { |
|
label: 'Avg Prob (GT)', |
|
data: [], |
|
borderColor: '#36a2eb', |
|
tension: 0.1 |
|
}, { |
|
label: 'Avg Prob (Non-GT)', |
|
data: [], |
|
borderColor: '#ff6384', |
|
tension: 0.1 |
|
}, { |
|
label: 'GT/Non-GT Prob Difference', |
|
data: [], |
|
borderColor: '#9966ff', |
|
tension: 0.1, |
|
borderDash: [5, 5] |
|
}] |
|
}, |
|
options: chartOptions |
|
}); |
|
} |
|
} |
|
|
|
initializeCharts(); |
|
|
|
|
|
showTab('overview'); |
|
}); |
|
|
|
|
|
let currentPredictionType = 'initial'; |
|
let predictionHistory = []; |
|
let currentPredictionIndex = 0; |
|
let currentFilter = 'all'; |
|
let currentSelectedFilter = 'all'; |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
if (document.getElementById('loss-chart')) { |
|
initializeCharts(); |
|
} |
|
|
|
|
|
showTab('overview'); |
|
}); |
|
|
|
function denormalizeImage(imageData) { |
|
if (!imageData) return ''; |
|
|
|
|
|
if (typeof imageData === 'string' && imageData.startsWith('data:image/')) { |
|
return imageData; |
|
} |
|
|
|
|
|
if (typeof imageData === 'string') { |
|
return `data:image/jpeg;base64,${imageData}`; |
|
} |
|
|
|
console.error('Unsupported image data format:', imageData); |
|
return ''; |
|
} |
|
|
|
function updateGroundTruthTags(predictionData, filter = 'all') { |
|
const container = document.getElementById('ground-truth-tags'); |
|
if (!container || !predictionData || !predictionData.category_predictions) return; |
|
|
|
container.innerHTML = ''; |
|
|
|
|
|
const allTags = []; |
|
|
|
Object.entries(predictionData.category_predictions).forEach(([category, data]) => { |
|
|
|
data.true_positives?.forEach(tag => { |
|
allTags.push({...tag, status: 'tp', category}); |
|
}); |
|
|
|
|
|
data.false_positives?.forEach(tag => { |
|
allTags.push({...tag, status: 'fp', category}); |
|
}); |
|
|
|
|
|
data.false_negatives?.forEach(tag => { |
|
allTags.push({...tag, status: 'fn', category}); |
|
}); |
|
}); |
|
|
|
|
|
const filteredTags = allTags.filter(tag => { |
|
if (filter === 'all') return true; |
|
if (filter === 'true-positive') return tag.status === 'tp'; |
|
if (filter === 'false-positive') return tag.status === 'fp'; |
|
if (filter === 'missing') return tag.status === 'fn'; |
|
return true; |
|
}); |
|
|
|
|
|
filteredTags.sort((a, b) => (b.probability || 0) - (a.probability || 0)); |
|
|
|
|
|
filteredTags.forEach(tag => { |
|
const tagDiv = document.createElement('div'); |
|
tagDiv.className = `tag-item tag-${tag.status}`; |
|
|
|
const statusText = { |
|
tp: 'TP', |
|
fp: 'FP', |
|
fn: 'Missing' |
|
}; |
|
|
|
tagDiv.innerHTML = ` |
|
<div class="tag-info"> |
|
<span class="tag-name">${tag.tag}</span> |
|
<span class="tag-probability"> |
|
${tag.status === 'fn' ? 'Missed' : `${(tag.probability * 100).toFixed(1)}%`} |
|
</span> |
|
</div> |
|
<span class="tag-status status-${tag.status}">${statusText[tag.status]}</span> |
|
`; |
|
|
|
container.appendChild(tagDiv); |
|
}); |
|
} |
|
|
|
function togglePredictionType() { |
|
currentPredictionType = currentPredictionType === 'initial' ? 'refined' : 'initial'; |
|
const button = document.getElementById('prediction-type-toggle'); |
|
if (button) { |
|
button.textContent = currentPredictionType === 'initial' ? 'Show Refined' : 'Show Initial'; |
|
} |
|
if (currentPredictionIndex < predictionHistory.length) { |
|
updatePredictionDisplay(predictionHistory[currentPredictionIndex]); |
|
} |
|
} |
|
|
|
function updateCategoryPredictions(predictionData) { |
|
const container = document.getElementById('category-predictions'); |
|
if (!container || !predictionData) return; |
|
|
|
const groupedPredictions = {}; |
|
Object.entries(predictionData.category_predictions || {}).forEach(([category, data]) => { |
|
groupedPredictions[category] = { |
|
correct: data.true_positives || [], |
|
incorrect: data.false_positives || [], |
|
missing: data.false_negatives || [] |
|
}; |
|
}); |
|
|
|
let html = ''; |
|
Object.entries(groupedPredictions).forEach(([category, predictions]) => { |
|
let tagsToShow = []; |
|
if (currentFilter === 'all') { |
|
tagsToShow = [ |
|
...predictions.correct.map(t => ({...t, type: 'correct'})), |
|
...predictions.incorrect.map(t => ({...t, type: 'incorrect'})), |
|
...predictions.missing.map(t => ({...t, type: 'missing'})) |
|
]; |
|
} else { |
|
tagsToShow = predictions[currentFilter].map(t => ({...t, type: currentFilter})); |
|
} |
|
|
|
if (tagsToShow.length > 0) { |
|
html += ` |
|
<div class="category-section"> |
|
<div class="category-title">${category}</div> |
|
<div class="tag-list"> |
|
${tagsToShow.map(tag => ` |
|
<div class="tag-item ${tag.type}"> |
|
<span class="tag-name">${tag.tag}</span> |
|
<div class="tag-confidence"> |
|
<div class="confidence-bar"> |
|
<div class="confidence-fill" |
|
style="width: ${tag.probability * 100}%; |
|
background: ${tag.type === 'correct' ? '#059669' : |
|
tag.type === 'incorrect' ? '#f97316' : '#ef4444'}"> |
|
</div> |
|
</div> |
|
<span class="confidence-value">${(tag.probability * 100).toFixed(1)}%</span> |
|
</div> |
|
</div> |
|
`).join('')} |
|
</div> |
|
</div> |
|
`; |
|
} |
|
}); |
|
|
|
container.innerHTML = html; |
|
} |
|
|
|
function getTagStatus(prediction) { |
|
if (prediction.probability === 0) return 'missing'; |
|
return prediction.correct ? 'correct' : 'incorrect'; |
|
} |
|
|
|
function filterTags(filter) { |
|
currentFilter = filter; |
|
document.querySelectorAll('.tag-filters .filter-btn').forEach(btn => { |
|
btn.classList.toggle('active', btn.textContent.toLowerCase().includes(filter)); |
|
}); |
|
if (currentPredictionIndex < predictionHistory.length) { |
|
updatePredictionDisplay(predictionHistory[currentPredictionIndex]); |
|
} |
|
} |
|
|
|
function updatePredictionDisplay(predictionData) { |
|
if (!predictionData) return; |
|
|
|
|
|
const imageElement = document.getElementById('current-image'); |
|
if (imageElement && predictionData.image) { |
|
imageElement.src = predictionData.image.startsWith('data:') ? |
|
predictionData.image : `data:image/jpeg;base64,${predictionData.image}`; |
|
} |
|
|
|
|
|
const counterElement = document.getElementById('prediction-counter'); |
|
if (counterElement) { |
|
counterElement.textContent = `Sample ${currentPredictionIndex + 1} / ${predictionHistory.length}`; |
|
} |
|
|
|
|
|
updateCategoryPredictions(predictionData); |
|
updateSelectedTagsList(predictionData); |
|
} |
|
|
|
function previousPrediction() { |
|
if (currentPredictionIndex > 0) { |
|
currentPredictionIndex--; |
|
updatePredictionDisplay(predictionHistory[currentPredictionIndex]); |
|
} |
|
} |
|
|
|
function nextPrediction() { |
|
if (currentPredictionIndex < predictionHistory.length - 1) { |
|
currentPredictionIndex++; |
|
updatePredictionDisplay(predictionHistory[currentPredictionIndex]); |
|
} |
|
} |
|
|
|
function filterSelectedTags(filter) { |
|
currentSelectedFilter = filter; |
|
document.querySelectorAll('.tag-filters .filter-btn').forEach(btn => { |
|
btn.classList.toggle('active', btn.textContent.toLowerCase().includes(filter)); |
|
}); |
|
if (currentPredictionIndex < predictionHistory.length) { |
|
updateSelectedTagsList(predictionHistory[currentPredictionIndex]); |
|
} |
|
} |
|
|
|
function updateSelectedTagsList(predictionData) { |
|
const container = document.getElementById('selected-tags-list'); |
|
if (!container || !predictionData || !predictionData.tag_info) return; |
|
|
|
let tagsToShow = predictionData.tag_info; |
|
if (currentSelectedFilter === 'ground-truth') { |
|
tagsToShow = tagsToShow.filter(tag => tag.is_ground_truth); |
|
} else if (currentSelectedFilter === 'non-ground-truth') { |
|
tagsToShow = tagsToShow.filter(tag => !tag.is_ground_truth); |
|
} |
|
|
|
|
|
tagsToShow.sort((a, b) => b.probability - a.probability); |
|
|
|
const html = tagsToShow.map(tag => ` |
|
<div class="selected-tag-item ${tag.is_ground_truth ? 'ground-truth' : 'non-ground-truth'}"> |
|
<div class="tag-info"> |
|
<span class="tag-name">${tag.tag_name}</span> |
|
<span class="tag-category text-sm text-gray-500">${tag.category}</span> |
|
</div> |
|
<div class="tag-confidence"> |
|
<div class="confidence-bar"> |
|
<div class="confidence-fill" |
|
style="width: ${tag.probability * 100}%; |
|
background: ${tag.is_ground_truth ? '#059669' : '#f97316'}"> |
|
</div> |
|
</div> |
|
<span class="confidence-value">${(tag.probability * 100).toFixed(1)}%</span> |
|
</div> |
|
</div> |
|
`).join(''); |
|
|
|
container.innerHTML = html; |
|
} |
|
|
|
function formatKey(key) { |
|
return key |
|
.split('_') |
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1)) |
|
.join(' '); |
|
} |
|
|
|
function formatTime(seconds) { |
|
const h = Math.floor(seconds / 3600); |
|
const m = Math.floor((seconds % 3600) / 60); |
|
const s = Math.floor(seconds % 60); |
|
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; |
|
} |
|
|
|
socket.on('training_update', (data) => { |
|
const update = JSON.parse(data); |
|
const progress = update.progress; |
|
|
|
|
|
document.getElementById('current-epoch').textContent = |
|
`${progress.epoch + 1}/${progress.total_epochs}`; |
|
document.getElementById('current-batch').textContent = |
|
`${progress.batch}/${progress.total_batches}`; |
|
document.getElementById('iter-speed').textContent = |
|
`${progress.iter_speed.toFixed(2)}`; |
|
document.getElementById('time-remaining').textContent = |
|
formatTime(progress.time_remaining); |
|
document.getElementById('time-elapsed').textContent = |
|
formatTime(progress.elapsed_time); |
|
|
|
|
|
if (progress.current_metrics) { |
|
document.getElementById('current-loss').textContent = |
|
progress.current_metrics.loss.toFixed(6); |
|
document.getElementById('current-initial-f1').textContent = |
|
progress.current_metrics.initial_f1.toFixed(6); |
|
document.getElementById('current-refined-f1').textContent = |
|
progress.current_metrics.refined_f1.toFixed(6); |
|
} |
|
}); |
|
|
|
socket.on('metrics_update', (data) => { |
|
try { |
|
const update = JSON.parse(data); |
|
const timestamp = new Date().toLocaleTimeString(); |
|
|
|
|
|
if (update.metrics) { |
|
const train = update.metrics.train; |
|
const val = update.metrics.val; |
|
|
|
|
|
if (train) { |
|
const updateMetric = (id, value, decimals = 6) => { |
|
const element = document.getElementById(id); |
|
if (element && value !== undefined) { |
|
element.textContent = typeof value === 'number' ? value.toFixed(decimals) : value; |
|
} |
|
}; |
|
|
|
updateMetric('current-initial-f1', train.initial_f1); |
|
updateMetric('current-refined-f1', train.refined_f1); |
|
} |
|
|
|
|
|
if (charts.loss) { |
|
charts.loss.data.labels.push(timestamp); |
|
charts.loss.data.datasets[0].data.push(train?.loss || 0); |
|
if (val && val.loss !== null) { |
|
charts.loss.data.datasets[1].data.push(val.loss); |
|
} |
|
if (charts.loss.data.labels.length > CHART_WINDOW_SIZE) { |
|
charts.loss.data.labels.shift(); |
|
charts.loss.data.datasets.forEach(dataset => dataset.data.shift()); |
|
} |
|
charts.loss.update(); |
|
} |
|
|
|
|
|
if (charts.f1) { |
|
charts.f1.data.labels.push(timestamp); |
|
charts.f1.data.datasets[0].data.push(train?.initial_f1 || 0); |
|
charts.f1.data.datasets[1].data.push(train?.refined_f1 || 0); |
|
if (val && val.initial_f1 !== null) { |
|
charts.f1.data.datasets[2].data.push(val.initial_f1); |
|
charts.f1.data.datasets[3].data.push(val.refined_f1); |
|
} |
|
if (charts.f1.data.labels.length > CHART_WINDOW_SIZE) { |
|
charts.f1.data.labels.shift(); |
|
charts.f1.data.datasets.forEach(dataset => dataset.data.shift()); |
|
} |
|
charts.f1.update(); |
|
} |
|
} |
|
|
|
|
|
if (update.predictions) { |
|
const predictions = update.predictions; |
|
|
|
|
|
if (predictions.tag_selection) { |
|
const tagSelection = predictions.tag_selection; |
|
const tagInfo = predictions.tag_info || []; |
|
|
|
|
|
const groundTruthTags = tagInfo.filter(tag => tag.is_ground_truth); |
|
const selectedGroundTruthTags = groundTruthTags.length; |
|
|
|
|
|
const gtProbs = groundTruthTags.map(tag => tag.probability); |
|
const nonGtProbs = tagInfo.filter(tag => !tag.is_ground_truth).map(tag => tag.probability); |
|
|
|
const avgGtProb = gtProbs.length > 0 ? gtProbs.reduce((a, b) => a + b, 0) / gtProbs.length : 0; |
|
const avgNonGtProb = nonGtProbs.length > 0 ? nonGtProbs.reduce((a, b) => a + b, 0) / nonGtProbs.length : 0; |
|
const probDifference = avgGtProb - avgNonGtProb; |
|
|
|
|
|
const recall = tagSelection.total_ground_truth > 0 ? |
|
selectedGroundTruthTags / tagSelection.total_ground_truth : 0; |
|
|
|
|
|
const updateMetric = (id, value, decimals = 4) => { |
|
const element = document.getElementById(id); |
|
if (element && value !== undefined) { |
|
element.textContent = typeof value === 'number' ? value.toFixed(decimals) : value; |
|
} |
|
}; |
|
|
|
updateMetric('total-gt-tags', tagSelection.total_ground_truth, 0); |
|
updateMetric('selected-gt-tags', selectedGroundTruthTags, 0); |
|
updateMetric('gt-recall', recall); |
|
updateMetric('avg-prob-gt', avgGtProb); |
|
updateMetric('avg-prob-non-gt', avgNonGtProb); |
|
updateMetric('unique-tags-selected', tagInfo.length, 0); |
|
|
|
|
|
if (charts.selection) { |
|
charts.selection.data.labels.push(timestamp); |
|
charts.selection.data.datasets[0].data.push(recall); |
|
charts.selection.data.datasets[1].data.push(avgGtProb); |
|
charts.selection.data.datasets[2].data.push(avgNonGtProb); |
|
charts.selection.data.datasets[3].data.push(probDifference); |
|
|
|
if (charts.selection.data.labels.length > CHART_WINDOW_SIZE) { |
|
charts.selection.data.labels.shift(); |
|
charts.selection.data.datasets.forEach(dataset => dataset.data.shift()); |
|
} |
|
|
|
|
|
const allValues = [ |
|
...charts.selection.data.datasets[0].data, |
|
...charts.selection.data.datasets[1].data, |
|
...charts.selection.data.datasets[2].data, |
|
...charts.selection.data.datasets[3].data |
|
].filter(val => val !== null && !isNaN(val)); |
|
|
|
if (allValues.length > 0) { |
|
const maxValue = Math.max(...allValues); |
|
const minValue = Math.min(...allValues); |
|
|
|
charts.selection.options.scales.y.min = Math.min(-0.2, minValue - 0.1); |
|
charts.selection.options.scales.y.max = Math.max(1.2, maxValue + 0.1); |
|
} |
|
|
|
charts.selection.update('none'); |
|
} |
|
|
|
if (typeof updateSelectedTagsList === 'function') { |
|
updateSelectedTagsList(predictions); |
|
} |
|
} |
|
|
|
|
|
predictionHistory.push(predictions); |
|
currentPredictionIndex = predictionHistory.length - 1; |
|
updatePredictionDisplay(predictions); |
|
} |
|
} catch (error) { |
|
console.error('Error in metrics update:', error); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |