/** * 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()); }); }