Anomaly detection and working with embeddings
By the end: You can find unusual data points and use unsupervised methods on embeddings.
- Anomaly detection approaches
- Isolation forests
- Clustering embeddings for topics
- Evaluating without labels
New to unsupervised learning? Supervised learning needs examples with the right answer attached. Unsupervised learning is what you do when nobody has attached one: you hand over a pile of data and ask what structure is already in it. Clustering groups items that resemble each other; dimensionality reduction squashes data with hundreds of measurements per item down to two, so you can plot it and look at it. Why it matters
Some of what you need to find is too rare and too varied to collect examples of: fraud, faults, corrupted records, the eval case unlike anything else in the set. You cannot train a classifier on them, so instead you learn what normal looks like and flag whatever departs from it. This section covers how that is done, then turns the same unsupervised tools on embeddings, where clustering a corpus and reading a handful of items from each cluster is the quickest honest answer to what is in all this text.
Finding the unusual#
Anomaly detection flags data unlike the rest: fraud, faults, corrupted records, eval cases that do not resemble anything else. Because anomalies are rare and varied, you usually cannot train a classifier on them and must learn what normal looks like instead.
- Statistical: flag points far from the mean in standard deviations. Simple, assumes roughly normal data.
- Isolation forest: anomalies are easier to isolate with random splits, so they end up in short paths. Fast and effective on tabular data.
- Distance-based: points far from their nearest neighbours are unusual. Works well on embeddings.
Those three sentences hide an enormous difference in how well they work, so run them against each other on a problem where the right answer is known. A thousand ordinary transactions with a skewed amount, an hour between 8 and 20, and a handful of items; then twenty planted anomalies whose amounts are perfectly ordinary but which happen at three in the morning with thirty items in the basket. The only honest measure without labels in production is precision at k, so score the top twenty flagged by each method.
import numpy as np
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import StandardScaler
rng = np.random.default_rng(0)
n = 1000
normal = np.c_[rng.lognormal(4.0, 1.0, n), # amount: skewed
rng.integers(8, 20, n).astype(float), # hour: daytime
rng.poisson(3, n).astype(float) + 1] # items
planted = np.c_[rng.lognormal(4.0, 1.0, 20), # ordinary amounts
rng.integers(2, 5, 20).astype(float), # the middle of the night
rng.poisson(30, 20).astype(float) + 1] # far too many items
X = np.vstack([normal, planted])
y = np.r_[np.zeros(n), np.ones(20)]
def precision_at_20(scores):
top = np.argsort(-scores)[:20]
return y[top].sum() / 20
amount_z = np.abs((X[:, 0] - X[:, 0].mean()) / X[:, 0].std())
all_z = np.abs((X - X.mean(0)) / X.std(0)).max(axis=1)
median = np.median(X, axis=0)
mad = np.median(np.abs(X - median), axis=0)
robust_z = np.abs((X - median) / (1.4826 * mad)).max(axis=1)
logged = np.c_[np.log(X[:, 0]), X[:, 1], X[:, 2]]
forest = -IsolationForest(random_state=0).fit(logged).score_samples(logged)
scaled = StandardScaler().fit_transform(logged)
distances, _ = NearestNeighbors(n_neighbors=11).fit(scaled).kneighbors(scaled)
knn = distances[:, 1:].mean(axis=1)
for name, s in (("z on amount", amount_z), ("max z, all columns", all_z),
("robust z (median/MAD)", robust_z), ("isolation forest", forest),
("10-NN distance", knn)):
print(name, precision_at_20(s))
# z on amount 0.0
# max z, all columns 0.75
# robust z (median/MAD) 0.8
# isolation forest 0.95
# 10-NN distance 0.65Three lessons come out of that table and they transfer to any anomaly problem. A z-score assumes a symmetric distribution, so on skewed data such as money, latency or counts it reports the fat tail as anomalous and ignores everything else — take logs first or use the median and the median absolute deviation instead of the mean and the standard deviation. An anomaly is usually a combination rather than an extreme value in one column, which is why the multivariate methods win here: nothing about a 3am transaction or a 30-item basket is individually impossible. And the scale of the input still matters: log the skewed column and the isolation forest goes from 0.85 to 0.95.
Precision at k is the number to report
Nobody reviews a thousand flags, so the question is never how many anomalies you caught overall. It is how many of the twenty you sent to a human were worth their time. Set k to what the team can actually look at each day, and report precision at that k.
Topics from embeddings#
Embed a corpus, cluster the embeddings, and read a sample from each cluster to name it. It is a fast way to understand what a large collection of text is about, and a practical use of these methods in LLM work.
from sklearn.cluster import KMeans
vectors = embed(documents)
labels = KMeans(n_clusters=12, n_init=10).fit_predict(vectors)
for k in range(12):
sample = [d for d, l in zip(documents, labels) if l == k][:5]
print(k, sample) # read these and name the cluster yourselfOne detail decides whether that works. k-means measures straight-line distance, while embedding similarity is cosine, which ignores length. The two agree exactly once the vectors are normalised to length 1: for unit vectors the squared distance is 2 minus twice the cosine, so ranking by one is ranking by the other. If your embeddings are not normalised, normalise them before clustering, or you are grouping by how long the documents are.
a = np.array([3.0, 4.0])
b = np.array([1.0, 0.0])
ua, ub = a / np.linalg.norm(a), b / np.linalg.norm(b)
cosine = ua @ ub
print(cosine) # 0.6
print(((ua - ub) ** 2).sum(), 2 - 2 * cosine) # 0.8000000000000002 0.8The other decision is how many clusters, and here the honest answer is different from the one in the tabular case. You are not trying to recover a true number of topics; there is no such number. You are choosing a resolution for reading. Twelve clusters over ten thousand support tickets is a summary a person can read in an afternoon; two hundred is a second dataset. Pick the number by what you can act on, say so, and check the result by reading rather than by score.
No labels means no ground truth
Without labels you cannot measure accuracy. Evaluation becomes inspection: read samples, check whether clusters are coherent, and test whether the result is stable when you rerun with a different seed.
There is one way to get a real number, and it is worth the hour it costs. Label a sample by hand — two hundred items is usually enough — and use it as a test set for whatever the unsupervised method was supposed to do. For an anomaly detector, that is precision at k on the flags a person actually reviewed. For clustering, take pairs of items and record whether a person says they belong together, then measure how often the clustering agrees. The point is not to label the whole corpus; it is to stop reporting a method's confidence in itself as though it were evidence.
The alternative number people report instead is the silhouette or the anomaly score, and both are internal: they say the algorithm did what it was designed to do on your data, which it always does. An internal score falling sharply when you change the input is useful information. An internal score on its own is not a result.
Where people go wrong
Treating an anomaly score as proof of a problem. It flags what is unusual, and unusual is not the same as wrong.
If this comes up in an interview
How would you detect anomalies without labelled examples?
Learn what normal looks like rather than training on the rare anomalies. Isolation forests work well on tabular data because anomalies isolate in few random splits; distance to nearest neighbours works well on embeddings.
How do you evaluate clustering when you have no labels?
By inspection and stability. Read samples from each cluster to judge coherence, and rerun with different seeds to see whether the grouping holds.
Build this: Cluster embeddings of a real text collection, name each cluster by reading samples, then run an anomaly detector and inspect the most unusual items.
Watch it explained
This is one section of Unsupervised learning, 16 hours across 4 sections. Read it in full to get the whole topic in order.