163 lines
4.3 KiB
Dart
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;
|
|
}
|