Spaces:
Sleeping
Sleeping
File size: 7,882 Bytes
ce11d27 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | # clustering.py
import json
import logging
import numpy as np
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from tracescope.analysis.metric_adapter import learn_metric, safe_transform
# ββ Global logger setup ββ
logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(levelname)s:%(message)s",
datefmt="%Y-%m-%d %H:%M:%S")
logger = logging.getLogger(__name__)
def cluster_embeddings(embedding_list, n_clusters=None, user_clusters=None, min_k=3, random_state=42):
# build & normalize
matrix = np.array(embedding_list, dtype=float)
matrix = matrix / np.linalg.norm(matrix, axis=1, keepdims=True)
n_samples = matrix.shape[0]
logger.info(f"cluster_embeddings βΆ n_samples={n_samples}, requested n_clusters={n_clusters}, random_state={random_state}")
if n_samples < 2:
raise ValueError("Need at least 2 samples for clustering")
# autoβselect k by silhouette
if n_clusters is None:
max_k = min(10, n_samples - 1)
if user_clusters:
# 2-stage grid: Ξ± β {0,.25,.5,.75,1} and k β [2β¦max_k]
matrix_orig = matrix.copy()
L = learn_metric(matrix_orig, user_clusters)
best_score = -1
best_k = 2
best_alpha = 0
best_labels = None
best_centroids = None
for alpha in (0, 0.25, 0.5, 0.75, 1):
XΞ± = safe_transform(matrix_orig, L, alpha)
for k in range(2, max_k + 1):
km = KMeans(n_clusters=k, init='k-means++', random_state=random_state)
lbl = km.fit_predict(XΞ±)
sil = silhouette_score(XΞ±, lbl)
if sil > best_score:
best_score, best_k = sil, k
best_alpha = alpha
best_labels = lbl
best_centroids = km.cluster_centers_
logger.info(
f"user feedback β Ξ±={best_alpha}, k={best_k}, silhouette={best_score:.4f}"
)
n_clusters = best_k
labels = best_labels
centroids = best_centroids
else:
# no feedback β original silhouette-only loop
best_score = -1
best_k = min_k
best_labels = None
best_centroids = None
for k in range(min_k, max_k + 1):
logger.info(f"Trying k={k}")
km = KMeans(n_clusters=k, init='k-means++', random_state=random_state)
lbl = km.fit_predict(matrix)
sil = silhouette_score(matrix, lbl)
logger.info(f" k={k} silhouette={sil:.4f}")
if sil > best_score:
best_score, best_k = sil, k
best_labels = lbl
best_centroids = km.cluster_centers_
# unbalanced 2-cluster guard as before
counts = np.bincount(best_labels)
if best_k == 2 and (counts.min() < 2 or counts.max()/counts.min() > 10):
logger.info("Detected unbalanced 2-cluster; searching k=3β¦max_k")
alt_best = -1
alt_labels = None
alt_k = None
for k in range(3, min(max_k + 1, 6)):
lbl = KMeans(n_clusters=k, random_state=random_state).fit_predict(matrix)
s = silhouette_score(matrix, lbl)
logger.info(f" alt_k={k} silhouette={s:.4f}")
if s > alt_best:
alt_best, alt_k, alt_labels = s, k, lbl
if alt_k:
logger.info(f"Switching to k={alt_k} with silhouette={alt_best:.4f}")
best_k, best_labels = alt_k, alt_labels
best_centroids = KMeans(n_clusters=alt_k, random_state=random_state).fit(matrix).cluster_centers_
n_clusters = best_k
labels = best_labels
centroids = best_centroids
else:
kmeans = KMeans(n_clusters=n_clusters, init='k-means++', random_state=random_state)
labels = kmeans.fit_predict(matrix)
centroids = kmeans.cluster_centers_
# build output
clusters = []
for i in range(n_clusters):
idxs = [int(idx) for idx, lbl in enumerate(labels) if lbl == i]
clusters.append({
"cluster_id": i,
"indices": idxs,
"centroid": centroids[i].tolist()
})
return {
"n_clusters": n_clusters,
"clusters": clusters,
"labels": labels.tolist(),
}
def main(embeddings_json, n_clusters=None, aspect=None, user_clusters=None, compute_moving=False, min_k=3, random_state=42):
# Load all embeddings directly from JSON
embedding_list = json.loads(embeddings_json)
# 2) if user_clusters came in as a JSON string, decode it,
# and make sure every index is an int
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# sanitize user_clusters: if itβs a JSON string, parse it; then
# coerce absolutely every index to a Python int, or error.
if user_clusters:
# 1) parse JSON if we were handed a string
if isinstance(user_clusters, str):
try:
user_clusters = json.loads(user_clusters)
except Exception as e:
raise ValueError(f"Could not parse user_clusters JSON: {e}")
# 2) coerce every element to int
try:
user_clusters = [
[int(idx) for idx in cluster]
for cluster in user_clusters
]
except Exception as e:
raise ValueError(
f"Invalid user_clusters format (must be List[List[int]]): {user_clusters}"
) from e
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Cluster them (no deduplication or pruning)
result = cluster_embeddings(
embedding_list, n_clusters,
user_clusters=user_clusters, min_k=min_k, random_state=random_state,
)
# print("RESULT:", result)
# ββ Optional movingβaverage βclosenessβ to each cluster ββ
if compute_moving:
try:
# rebuild & normalize matrix
matrix = np.array(embedding_list, dtype=float)
matrix = matrix / np.linalg.norm(matrix, axis=1, keepdims=True)
n = matrix.shape[0]
# weights decay with distance in list: w_{i,j} = 1 / (1 + |i-j|)
idx = np.arange(n)
W = 1 / (1 + np.abs(idx[:, None] - idx[None, :]))
# compute weightedβaverage embedding for each position
weighted = (W[:, :, None] * matrix[None, :, :]).sum(axis=1) / W.sum(axis=1)[:, None]
# extract centroids from the clustering result
centroids = np.array([c["centroid"] for c in result["clusters"]])
# for each position, compute similarity = 1/(1 + distance) to each centroid
moving_sim = (
1
/ (
1
+ np.linalg.norm(
centroids[None, :, :] - weighted[:, None, :],
axis=2
)
)
).tolist()
result["moving_similarity"] = moving_sim
except Exception as e:
logger.error(f"Error computing movingβaverage similarity: {e}")
# Return the raw clustering result as JSON
return json.dumps(result)
|