Files
MrbWebLibre/packages/flutter_mozilla_components/lib/src/utils/ml/cluster.dart
T
2025-09-30 16:49:43 +02:00

163 lines
4.3 KiB
Dart

import 'dart:math' as math;
import 'package:flutter_mozilla_components/src/utils/ml/cluster_algo.dart';
/// Clusters embeddings using K-means algorithm with configurable parameters
List<List<int>> clusterEmbeddings({
required List<List<double>> embeddings,
int? k,
double Function()? randomFunc,
int clusteringTriesPerK = 3,
}) {
k ??= 0;
int startK = k;
int endK = k + 1;
if (k == 0) {
startK = 2;
// Find a reasonable max # of clusters
endK =
math.min(
(math.log(embeddings.length) * 2.0).floor(),
embeddings.length,
) +
1;
}
List<List<int>>? bestResult;
double bestResultSilScore = -100.0;
for (int curK = startK; curK < endK; curK++) {
List<List<int>>? bestItemsForK;
double bestInertiaForK = 500000000000;
for (int j = 0; j < clusteringTriesPerK; j++) {
final allItems = kmeansPlusPlus(
data: embeddings,
k: curK,
randomFunc: randomFunc,
freezeAnchorsInZeroCluster: false, // Not needed since no anchors
);
final inertia = _getCentroidInertia(allItems, embeddings);
if (inertia < bestInertiaForK) {
bestInertiaForK = inertia;
bestItemsForK = allItems;
}
}
if (bestItemsForK != null) {
final silScores = silhouetteCoefficients(embeddings, bestItemsForK);
final avgSil = silScores.reduce((a, b) => a + b) / silScores.length;
if (avgSil > bestResultSilScore) {
bestResultSilScore = avgSil;
bestResult = bestItemsForK;
}
}
}
return bestResult ?? [];
}
/// Computes the inertia (sum of squared distances to centroids) for clusters
double _getCentroidInertia(
List<List<int>> clusters,
List<List<double>> embeddings,
) {
double totalDistance = 0.0;
for (final cluster in clusters) {
if (cluster.isEmpty) continue;
// Compute centroid
final dimensions = embeddings[0].length;
final centroid = List<double>.filled(dimensions, 0.0);
for (final index in cluster) {
final point = embeddings[index];
for (int i = 0; i < dimensions; i++) {
centroid[i] += point[i];
}
}
for (int i = 0; i < dimensions; i++) {
centroid[i] /= cluster.length;
}
// Compute sum of squared distances to centroid
for (final index in cluster) {
final point = embeddings[index];
for (int i = 0; i < dimensions; i++) {
final diff = point[i] - centroid[i];
totalDistance += diff * diff;
}
}
}
return totalDistance;
}
/// Computes silhouette coefficients for clusters
List<double> silhouetteCoefficients(
List<List<double>> embeddings,
List<List<int>> clusters,
) {
final silhouettes = <double>[];
for (int clusterIdx = 0; clusterIdx < clusters.length; clusterIdx++) {
final cluster = clusters[clusterIdx];
if (cluster.length <= 1) {
silhouettes.add(0.0);
continue;
}
double clusterSilhouette = 0.0;
for (final pointIdx in cluster) {
final point = embeddings[pointIdx];
// Compute average intra-cluster distance (a)
double intraDistance = 0.0;
for (final otherIdx in cluster) {
if (pointIdx != otherIdx) {
intraDistance += euclideanDistance(point, embeddings[otherIdx]);
}
}
final a = cluster.length > 1 ? intraDistance / (cluster.length - 1) : 0.0;
// Compute minimum average inter-cluster distance (b)
double minInterDistance = double.infinity;
for (
int otherClusterIdx = 0;
otherClusterIdx < clusters.length;
otherClusterIdx++
) {
if (otherClusterIdx == clusterIdx) continue;
final otherCluster = clusters[otherClusterIdx];
if (otherCluster.isEmpty) continue;
double interDistance = 0.0;
for (final otherIdx in otherCluster) {
interDistance += euclideanDistance(point, embeddings[otherIdx]);
}
final avgInterDistance = interDistance / otherCluster.length;
minInterDistance = math.min(minInterDistance, avgInterDistance);
}
final b = minInterDistance == double.infinity ? 0.0 : minInterDistance;
// Compute silhouette for this point
final silhouette = (a == 0.0 && b == 0.0)
? 0.0
: (b - a) / math.max(a, b);
clusterSilhouette += silhouette;
}
silhouettes.add(clusterSilhouette / cluster.length);
}
return silhouettes;
}