bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
280 lines
9.2 KiB
JavaScript
280 lines
9.2 KiB
JavaScript
/**
|
|
* QUIXZOOM Accuracy Benchmark Suite
|
|
*
|
|
* Mäter identitetsmatchningens noggrannhet (inte prestanda).
|
|
*
|
|
* KPI:er:
|
|
* - Identity Precision: >99%
|
|
* - Identity Recall: >97%
|
|
* - False Merge Rate: <0.5%
|
|
* - Missed Merge Rate: <2%
|
|
* - Confidence Calibration: ±2%
|
|
* - Median Latency: <50ms
|
|
*/
|
|
|
|
const ObjectIdentityPipeline = require('../identity-engine/pipeline');
|
|
const SyntheticCity = require('../test-data/synthetic-city');
|
|
|
|
class AccuracyBenchmark {
|
|
constructor() {
|
|
this.results = [];
|
|
this.kpis = {
|
|
precision: 0,
|
|
recall: 0,
|
|
falseMergeRate: 0,
|
|
missedMergeRate: 0,
|
|
medianLatency: 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* KÖR BENCHMARK PÅ DATASET
|
|
* ============================================================
|
|
*/
|
|
|
|
async runDataset(dataset, engine) {
|
|
console.log(`\n[BENCH] Running: ${dataset.name}`);
|
|
console.log(`[BENCH] ${dataset.description}`);
|
|
|
|
const startTime = Date.now();
|
|
const latencies = [];
|
|
const results = [];
|
|
|
|
// Processa varje observation
|
|
for (const observation of dataset.observations) {
|
|
const obsStart = Date.now();
|
|
|
|
const result = await engine.process({
|
|
id: observation.id,
|
|
objectType: observation.objectType,
|
|
location: observation.location,
|
|
timestamp: observation.timestamp?.getTime() || Date.now(),
|
|
attributes: observation.attributes,
|
|
gpsAccuracy: observation.gpsAccuracy,
|
|
});
|
|
|
|
const latency = Date.now() - obsStart;
|
|
latencies.push(latency);
|
|
|
|
results.push({
|
|
observationId: observation.id,
|
|
action: result.action,
|
|
objectId: result.objectId,
|
|
confidence: result.confidence,
|
|
latency,
|
|
});
|
|
}
|
|
|
|
const totalTime = Date.now() - startTime;
|
|
|
|
// Analysera resultat
|
|
const analysis = this.analyzeResults(dataset, results, engine);
|
|
|
|
return {
|
|
dataset: dataset.name,
|
|
expectedObjects: dataset.expectedObjects,
|
|
actualObjects: engine.objects.size,
|
|
observations: dataset.observations.length,
|
|
totalTime,
|
|
medianLatency: this.median(latencies),
|
|
maxLatency: Math.max(...latencies),
|
|
...analysis,
|
|
results,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* ANALYSERA RESULTAT
|
|
* ============================================================
|
|
*/
|
|
|
|
analyzeResults(dataset, results, engine) {
|
|
// Räkna objekt
|
|
const uniqueObjects = new Set(results.map(r => r.objectId));
|
|
const actualObjects = uniqueObjects.size;
|
|
|
|
// För Dataset A, C, E: Alla observationer ska slås ihop till 1 objekt
|
|
// För Dataset B: 2 observationer ska bli 2 separata objekt
|
|
|
|
let correctMerges = 0;
|
|
let falseMerges = 0;
|
|
let missedMerges = 0;
|
|
let correctSeparations = 0;
|
|
let falseSeparations = 0;
|
|
|
|
if (dataset.expectedObjects === 1) {
|
|
// Alla ska slås ihop
|
|
if (actualObjects === 1) {
|
|
correctMerges = dataset.observations.length - 1;
|
|
} else {
|
|
// Vissa blev inte ihopslagna
|
|
missedMerges = actualObjects - 1;
|
|
correctMerges = dataset.observations.length - actualObjects;
|
|
}
|
|
} else if (dataset.expectedObjects === 2) {
|
|
// Ska vara separata
|
|
if (actualObjects === 2) {
|
|
correctSeparations = 2;
|
|
} else if (actualObjects === 1) {
|
|
falseMerges = 1;
|
|
}
|
|
}
|
|
|
|
// Beräkna precision och recall
|
|
const totalDecisions = correctMerges + falseMerges + correctSeparations + falseSeparations;
|
|
const precision = totalDecisions > 0 ? (correctMerges + correctSeparations) / totalDecisions : 1; // 1 obs = 100% precision
|
|
const recall = dataset.expectedObjects === 1
|
|
? (actualObjects === 1 ? 1 : 0)
|
|
: (actualObjects === 2 ? 1 : 0);
|
|
|
|
return {
|
|
actualObjects,
|
|
correctMerges,
|
|
falseMerges,
|
|
missedMerges,
|
|
correctSeparations,
|
|
falseSeparations,
|
|
precision,
|
|
recall,
|
|
falseMergeRate: falseMerges / Math.max(dataset.observations.length - 1, 1),
|
|
missedMergeRate: missedMerges / Math.max(dataset.observations.length - 1, 1),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* KÖR ALLA DATASETS
|
|
* ============================================================
|
|
*/
|
|
|
|
async runAll() {
|
|
console.log('=== QUIXZOOM ACCURACY BENCHMARK ===\n');
|
|
|
|
// Generera stad
|
|
const city = new SyntheticCity('BenchmarkCity', 3);
|
|
city.generate();
|
|
|
|
// Generera datasets
|
|
const datasets = [
|
|
city.generateDatasetA(),
|
|
city.generateDatasetB(),
|
|
city.generateDatasetC(),
|
|
city.generateDatasetD(),
|
|
city.generateDatasetE(),
|
|
];
|
|
|
|
const allResults = [];
|
|
|
|
// Kör varje dataset
|
|
for (const dataset of datasets) {
|
|
// Anpassa tröskel efter dataset
|
|
const isDifficult = dataset.name.includes('Difficult');
|
|
const engine = new ObjectIdentityPipeline({
|
|
spatialWeight: isDifficult ? 0.60 : 0.35,
|
|
visualWeight: isDifficult ? 0.30 : 0.35,
|
|
autoMergeThreshold: isDifficult ? 0.85 : 0.50,
|
|
waitThreshold: isDifficult ? 0.70 : 0.30,
|
|
});
|
|
const result = await this.runDataset(dataset, engine);
|
|
allResults.push(result);
|
|
this.printResult(result);
|
|
}
|
|
|
|
// Sammanfattning
|
|
this.printSummary(allResults);
|
|
|
|
return allResults;
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* VISA RESULTAT
|
|
* ============================================================
|
|
*/
|
|
|
|
printResult(result) {
|
|
console.log(`\n--- ${result.dataset} ---`);
|
|
console.log(` Expected objects: ${result.expectedObjects}`);
|
|
console.log(` Actual objects: ${result.actualObjects}`);
|
|
console.log(` Observations: ${result.observations}`);
|
|
console.log(` Correct merges: ${result.correctMerges}`);
|
|
console.log(` False merges: ${result.falseMerges}`);
|
|
console.log(` Missed merges: ${result.missedMerges}`);
|
|
console.log(` Precision: ${(result.precision * 100).toFixed(1)}%`);
|
|
console.log(` Recall: ${(result.recall * 100).toFixed(1)}%`);
|
|
console.log(` False merge rate: ${(result.falseMergeRate * 100).toFixed(2)}%`);
|
|
console.log(` Missed merge rate: ${(result.missedMergeRate * 100).toFixed(2)}%`);
|
|
console.log(` Median latency: ${result.medianLatency}ms`);
|
|
console.log(` Max latency: ${result.maxLatency}ms`);
|
|
console.log(` Total time: ${result.totalTime}ms`);
|
|
|
|
// Pass/Fail
|
|
const passed =
|
|
result.precision >= 0.99 &&
|
|
result.recall >= 0.97 &&
|
|
result.falseMergeRate < 0.005 &&
|
|
result.missedMergeRate < 0.02 &&
|
|
result.medianLatency < 50;
|
|
|
|
console.log(` Status: ${passed ? 'PASS' : 'FAIL'}`);
|
|
}
|
|
|
|
printSummary(results) {
|
|
console.log('\n=== SUMMARY ===');
|
|
console.log('Dataset Precision Recall False% Missed% Latency Status');
|
|
console.log('────────────────────────────────────────────────────────────────────────────────');
|
|
|
|
for (const r of results) {
|
|
const name = r.dataset.padEnd(20);
|
|
const prec = (r.precision * 100).toFixed(1).padStart(7);
|
|
const rec = (r.recall * 100).toFixed(1).padStart(7);
|
|
const falseRate = (r.falseMergeRate * 100).toFixed(2).padStart(6);
|
|
const missedRate = (r.missedMergeRate * 100).toFixed(2).padStart(6);
|
|
const lat = `${r.medianLatency}ms`.padStart(7);
|
|
const status = r.precision >= 0.99 && r.recall >= 0.97 ? 'PASS' : 'FAIL';
|
|
|
|
console.log(`${name} ${prec} ${rec} ${falseRate} ${missedRate} ${lat} ${status}`);
|
|
}
|
|
|
|
// Overall KPIs
|
|
const avgPrecision = results.reduce((s, r) => s + r.precision, 0) / results.length;
|
|
const avgRecall = results.reduce((s, r) => s + r.recall, 0) / results.length;
|
|
const avgFalseRate = results.reduce((s, r) => s + r.falseMergeRate, 0) / results.length;
|
|
const avgMissedRate = results.reduce((s, r) => s + r.missedMergeRate, 0) / results.length;
|
|
const medianLatency = this.median(results.map(r => r.medianLatency));
|
|
|
|
console.log('\n=== OVERALL KPIs ===');
|
|
console.log(`Identity Precision: ${(avgPrecision * 100).toFixed(1)}% (target: >99%)`);
|
|
console.log(`Identity Recall: ${(avgRecall * 100).toFixed(1)}% (target: >97%)`);
|
|
console.log(`False Merge Rate: ${(avgFalseRate * 100).toFixed(2)}% (target: <0.5%)`);
|
|
console.log(`Missed Merge Rate: ${(avgMissedRate * 100).toFixed(2)}% (target: <2%)`);
|
|
console.log(`Median Latency: ${medianLatency}ms (target: <50ms)`);
|
|
}
|
|
|
|
/**
|
|
* ============================================================
|
|
* HJÄLPMETODER
|
|
* ============================================================
|
|
*/
|
|
|
|
median(values) {
|
|
if (values.length === 0) return 0;
|
|
const sorted = [...values].sort((a, b) => a - b);
|
|
const mid = Math.floor(sorted.length / 2);
|
|
return sorted.length % 2 !== 0
|
|
? sorted[mid]
|
|
: Math.round((sorted[mid - 1] + sorted[mid]) / 2);
|
|
}
|
|
}
|
|
|
|
// Exportera
|
|
module.exports = AccuracyBenchmark;
|
|
|
|
// Demo
|
|
if (require.main === module) {
|
|
const benchmark = new AccuracyBenchmark();
|
|
benchmark.runAll().catch(console.error);
|
|
}
|