Files
boc/quixzoom-capture-pipeline/app/active-learning.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

564 lines
16 KiB
JavaScript

/**
* QUIXZOOM Active Learning — Information Gain Optimization
*
* Systemet identifierar vilka observationer som ger störst
* informationsvärde och prioriterar dem.
*
* Tre strategier:
* 1. Uncertainty Sampling — Insamla där modellen är osäker
* 2. Diversity Sampling — Insamla varierande exempel
* 3. Density-Weighted Sampling — Insamla representativa exempel
*/
const tf = require('@tensorflow/tfjs-node');
class ActiveLearning {
constructor(options = {}) {
this.strategy = options.strategy || 'uncertainty'; // uncertainty, diversity, density, combined
this.batchSize = options.batchSize || 10;
this.uncertaintyThreshold = options.uncertaintyThreshold || 0.3;
this.diversityThreshold = options.diversityThreshold || 0.5;
// Modell för att beräkna osäkerhet
this.uncertaintyModel = null;
// Feature space för diversitet
this.featureSpace = new Map();
// Historik över insamlade prover
this.collectedSamples = [];
// Information gain per område
this.informationGain = new Map();
}
/**
* ============================================================
* 1. UNCERTAINTY SAMPLING
* ============================================================
* Insamla data där modellen är mest osäker.
* Metoder: Least Confidence, Margin Sampling, Entropy
*/
async uncertaintySampling(unlabeledPool, model) {
console.log('[ACTIVE] Running uncertainty sampling...');
const uncertainties = [];
for (const sample of unlabeledPool) {
// Förutsäg med modellen
const predictions = await model.predict(sample.features);
// Beräkna osäkerhet
const uncertainty = this.calculateUncertainty(predictions, this.strategy);
uncertainties.push({
sample,
uncertainty,
predictions,
});
}
// Sortera efter osäkerhet (högst först)
uncertainties.sort((a, b) => b.uncertainty - a.uncertainty);
// Välj toppen
const selected = uncertainties.slice(0, this.batchSize);
console.log(`[ACTIVE] Selected ${selected.length} samples by uncertainty`);
return selected.map(s => ({
...s.sample,
selectionReason: 'uncertainty',
uncertaintyScore: s.uncertainty,
}));
}
calculateUncertainty(predictions, method = 'entropy') {
const probs = predictions.arraySync()[0];
switch (method) {
case 'least_confidence':
// 1 - max(P(y|x))
return 1 - Math.max(...probs);
case 'margin':
// P(y1|x) - P(y2|x) där y1 och y2 är topp-2
const sorted = [...probs].sort((a, b) => b - a);
return 1 - (sorted[0] - sorted[1]);
case 'entropy':
// -sum(P(y|x) * log(P(y|x)))
return -probs.reduce((sum, p) => {
if (p > 0) {
return sum + p * Math.log2(p);
}
return sum;
}, 0);
default:
return 1 - Math.max(...probs);
}
}
/**
* ============================================================
* 2. DIVERSITY SAMPLING
* ============================================================
* Insamla varierande exempel för att täcka hela feature space.
* Metoder: Core-set, Clustering, Adversarial
*/
async diversitySampling(unlabeledPool, labeledPool) {
console.log('[ACTIVE] Running diversity sampling...');
// Extrahera features för alla prover
const unlabeledFeatures = await this.extractFeatures(unlabeledPool);
const labeledFeatures = await this.extractFeatures(labeledPool);
// Beräkna diversitet för varje opmärkt prov
const diversities = [];
for (let i = 0; i < unlabeledPool.length; i++) {
const sample = unlabeledPool[i];
const features = unlabeledFeatures[i];
// Beräkna avstånd till närmaste märkta prov
const minDistance = this.minDistanceToLabeled(features, labeledFeatures);
// Beräkna avstånd till andra opmärkta prover (för att undvika kluster)
const avgDistance = this.avgDistanceToUnlabeled(features, unlabeledFeatures, i);
diversities.push({
sample,
diversity: minDistance * avgDistance, // Kombinera
minDistance,
avgDistance,
});
}
// Sortera efter diversitet (högst först)
diversities.sort((a, b) => b.diversity - a.diversity);
// Välj toppen
const selected = diversities.slice(0, this.batchSize);
console.log(`[ACTIVE] Selected ${selected.length} samples by diversity`);
return selected.map(s => ({
...s.sample,
selectionReason: 'diversity',
diversityScore: s.diversity,
}));
}
minDistanceToLabeled(features, labeledFeatures) {
let minDist = Infinity;
for (const labeled of labeledFeatures) {
const dist = this.euclideanDistance(features, labeled);
if (dist < minDist) {
minDist = dist;
}
}
return minDist === Infinity ? 0 : minDist;
}
avgDistanceToUnlabeled(features, unlabeledFeatures, excludeIndex) {
let totalDist = 0;
let count = 0;
for (let i = 0; i < unlabeledFeatures.length; i++) {
if (i !== excludeIndex) {
totalDist += this.euclideanDistance(features, unlabeledFeatures[i]);
count++;
}
}
return count > 0 ? totalDist / count : 0;
}
euclideanDistance(a, b) {
let sum = 0;
for (let i = 0; i < a.length; i++) {
sum += (a[i] - b[i]) ** 2;
}
return Math.sqrt(sum);
}
/**
* ============================================================
* 3. DENSITY-WEIGHTED SAMPLING
* ============================================================
* Insamla representativa exempel från täta områden i feature space.
*/
async densityWeightedSampling(unlabeledPool) {
console.log('[ACTIVE] Running density-weighted sampling...');
// Extrahera features
const features = await this.extractFeatures(unlabeledPool);
// Beräkna densitet för varje prov
const densities = [];
for (let i = 0; i < unlabeledPool.length; i++) {
const sample = unlabeledPool[i];
const feature = features[i];
// Beräkna lokal densitet (antal närliggande prover)
const density = this.calculateLocalDensity(feature, features, i);
densities.push({
sample,
density,
});
}
// Sortera efter densitet (högst först)
densities.sort((a, b) => b.density - a.density);
// Välj toppen
const selected = densities.slice(0, this.batchSize);
console.log(`[ACTIVE] Selected ${selected.length} samples by density`);
return selected.map(s => ({
...s.sample,
selectionReason: 'density',
densityScore: s.density,
}));
}
calculateLocalDensity(feature, allFeatures, excludeIndex, radius = 0.5) {
let count = 0;
for (let i = 0; i < allFeatures.length; i++) {
if (i !== excludeIndex) {
const dist = this.euclideanDistance(feature, allFeatures[i]);
if (dist < radius) {
count++;
}
}
}
return count;
}
/**
* ============================================================
* 4. COMBINED STRATEGY
* ============================================================
* Kombinera osäkerhet, diversitet och densitet.
*/
async combinedSampling(unlabeledPool, model, labeledPool) {
console.log('[ACTIVE] Running combined sampling...');
// Kör varje strategi
const uncertaintySamples = await this.uncertaintySampling(unlabeledPool, model);
const diversitySamples = await this.diversitySampling(unlabeledPool, labeledPool);
const densitySamples = await this.densityWeightedSampling(unlabeledPool);
// Kombinera och deduplicera
const combined = new Map();
// Vikta varje strategi
for (const sample of uncertaintySamples) {
const key = sample.id;
if (!combined.has(key)) {
combined.set(key, { ...sample, score: 0, reasons: [] });
}
combined.get(key).score += sample.uncertaintyScore * 0.4;
combined.get(key).reasons.push('uncertainty');
}
for (const sample of diversitySamples) {
const key = sample.id;
if (!combined.has(key)) {
combined.set(key, { ...sample, score: 0, reasons: [] });
}
combined.get(key).score += sample.diversityScore * 0.35;
combined.get(key).reasons.push('diversity');
}
for (const sample of densitySamples) {
const key = sample.id;
if (!combined.has(key)) {
combined.set(key, { ...sample, score: 0, reasons: [] });
}
combined.get(key).score += sample.densityScore * 0.25;
combined.get(key).reasons.push('density');
}
// Sortera efter kombinerad score
const sorted = Array.from(combined.values()).sort((a, b) => b.score - a.score);
// Välj toppen
const selected = sorted.slice(0, this.batchSize);
console.log(`[ACTIVE] Selected ${selected.length} samples by combined strategy`);
return selected.map(s => ({
...s,
selectionReason: s.reasons.join('+'),
combinedScore: s.score,
}));
}
/**
* ============================================================
* INFORMATION GAIN BERÄKNING
* ============================================================
*/
calculateInformationGain(newSample, currentModel) {
// Beräkna hur mycket modellen skulle förbättras
// om detta prov lades till träningsdata
const beforeAccuracy = this.estimateModelAccuracy(currentModel);
// Simulera träning med nytt prov
const simulatedModel = this.simulateTraining(currentModel, newSample);
const afterAccuracy = this.estimateModelAccuracy(simulatedModel);
return afterAccuracy - beforeAccuracy;
}
estimateModelAccuracy(model) {
// Förenklad uppskattning — i praktiken korsvalidering
return model.accuracy || 0.7;
}
simulateTraining(model, sample) {
// Simulera träning — i praktiken faktisk träning
return {
...model,
accuracy: Math.min(0.99, (model.accuracy || 0.7) + 0.01),
};
}
async extractFeatures(samples) {
// Extrahera features från prover
// I praktiken: kör genom en feature extractor (t.ex. ResNet)
return samples.map(s => s.features || [Math.random(), Math.random(), Math.random()]);
}
/**
* ============================================================
* KUNSKAPSGAP-IDENTIFIERING
* ============================================================
*/
identifyKnowledgeGaps(area, existingData) {
const gaps = [];
// 1. Spatiala gap — områden utan data
const spatialGaps = this.findSpatialGaps(area, existingData);
gaps.push(...spatialGaps);
// 2. Temporala gap — tider utan data
const temporalGaps = this.findTemporalGaps(existingData);
gaps.push(...temporalGaps);
// 3. Kategoriska gap — objekttyper med få exempel
const categoricalGaps = this.findCategoricalGaps(existingData);
gaps.push(...categoricalGaps);
// 4. Kvalitetsgap — områden med lågkvalitativ data
const qualityGaps = this.findQualityGaps(existingData);
gaps.push(...qualityGaps);
return gaps;
}
findSpatialGaps(area, existingData) {
// Dela upp området i grid-celler
const gridSize = 50; // meter
const cells = new Map();
// Markera celler med data
for (const data of existingData) {
const cellX = Math.floor(data.gps.lng / gridSize);
const cellY = Math.floor(data.gps.lat / gridSize);
const key = `${cellX},${cellY}`;
cells.set(key, true);
}
// Hitta tomma celler
const gaps = [];
const bounds = area.bounds;
for (let x = bounds.minLng; x < bounds.maxLng; x += gridSize) {
for (let y = bounds.minLat; y < bounds.maxLat; y += gridSize) {
const key = `${Math.floor(x / gridSize)},${Math.floor(y / gridSize)}`;
if (!cells.has(key)) {
gaps.push({
type: 'spatial',
location: { lat: y, lng: x },
description: `No data in grid cell (${key})`,
criticality: 0.6,
});
}
}
}
return gaps;
}
findTemporalGaps(existingData) {
// Hitta tidsperioder med lite data
const hourCounts = new Array(24).fill(0);
for (const data of existingData) {
const hour = new Date(data.timestamp).getHours();
hourCounts[hour]++;
}
const gaps = [];
const avgCount = existingData.length / 24;
for (let hour = 0; hour < 24; hour++) {
if (hourCounts[hour] < avgCount * 0.3) {
gaps.push({
type: 'temporal',
timeOfDay: hour,
description: `Few samples at ${hour}:00-${hour + 1}:00`,
criticality: 0.5,
});
}
}
return gaps;
}
findCategoricalGaps(existingData) {
// Hitta objekttyper med få exempel
const typeCounts = new Map();
for (const data of existingData) {
for (const obj of data.objects || []) {
const count = typeCounts.get(obj.class) || 0;
typeCounts.set(obj.class, count + 1);
}
}
const gaps = [];
const avgCount = existingData.length / typeCounts.size;
for (const [type, count] of typeCounts) {
if (count < avgCount * 0.2) {
gaps.push({
type: 'categorical',
objectType: type,
description: `Few examples of ${type} (${count} samples)`,
criticality: 0.7,
});
}
}
return gaps;
}
findQualityGaps(existingData) {
// Hitta områden med lågkvalitativ data
const gaps = [];
for (const data of existingData) {
if (data.quality && data.quality.score < 0.5) {
gaps.push({
type: 'quality',
location: data.gps,
description: `Low quality data at this location`,
criticality: 0.4,
});
}
}
return gaps;
}
/**
* ============================================================
* UPPDATERING OCH STATISTIK
* ============================================================
*/
updateCollectedSamples(samples) {
this.collectedSamples.push(...samples);
// Uppdatera feature space
for (const sample of samples) {
this.featureSpace.set(sample.id, sample.features);
}
}
getStatistics() {
return {
totalCollected: this.collectedSamples.length,
byStrategy: this.collectedSamples.reduce((acc, s) => {
const strategy = s.selectionReason || 'unknown';
acc[strategy] = (acc[strategy] || 0) + 1;
return acc;
}, {}),
averageUncertainty: this.collectedSamples
.filter(s => s.uncertaintyScore)
.reduce((sum, s) => sum + s.uncertaintyScore, 0) / this.collectedSamples.length,
coverage: this.calculateKnowledgeCoverage(),
};
}
calculateKnowledgeCoverage() {
// Beräkna spatial täckning
const uniqueLocations = new Set(
this.collectedSamples.map(s => `${s.gps?.lat?.toFixed(3)},${s.gps?.lng?.toFixed(3)}`)
);
return uniqueLocations.size / 100; // Förenklad
}
}
// Exportera
module.exports = ActiveLearning;
// Demo
if (require.main === module) {
const al = new ActiveLearning({ strategy: 'combined', batchSize: 5 });
// Simulerad pool
const unlabeledPool = Array.from({ length: 100 }, (_, i) => ({
id: `sample_${i}`,
features: [Math.random(), Math.random(), Math.random()],
gps: { lat: 13.7 + Math.random() * 0.1, lng: 100.4 + Math.random() * 0.2 },
}));
const labeledPool = Array.from({ length: 20 }, (_, i) => ({
id: `labeled_${i}`,
features: [Math.random(), Math.random(), Math.random()],
gps: { lat: 13.7 + Math.random() * 0.1, lng: 100.4 + Math.random() * 0.2 },
}));
const model = {
predict: async (features) => {
// Simulerad förutsägelse
return tf.tensor2d([[Math.random(), Math.random(), Math.random()]]);
},
accuracy: 0.75,
};
console.log('=== ACTIVE LEARNING DEMO ===\n');
al.combinedSampling(unlabeledPool, model, labeledPool).then(selected => {
console.log('\nSelected samples:');
selected.forEach((s, i) => {
console.log(` ${i + 1}. ${s.id}${s.selectionReason} (score: ${s.combinedScore?.toFixed(3) || 'N/A'})`);
});
al.updateCollectedSamples(selected);
console.log('\nStatistics:', al.getStatistics());
});
}