Anomaly Detection

Intermediate6 min

An anomaly score is a distance read backwards. Plus the unnormalized-vector bug that makes a detector flag your longest documents.

#use-cases

Using embedding distance as an anomaly score

An embedding model puts text at a point in space, arranged so that similar meanings land near each other. Search uses that to find the nearest points to a query.

Anomaly detection uses the same arrangement and asks the opposite question: which points are far from everything?

That is the whole idea, and it needs almost no new machinery. You already have vectors and a distance function. An anomaly score is a distance you chose to read as strangeness rather than as relevance. If performing similarity search gets you the nearest neighbors, you have most of a detector already.

What this buys over a keyword rule is that it works on meaning. A support ticket about a problem nobody has reported before contains no unusual words. It is unusual in what it is about, and that is the kind of unusual an embedding can see.

Two ways to score how unusual a record is

Distance to what, though? There are two answers, and they assume different things about your data.

Distance to the centroid. Average all the vectors in your reference set into one point, then score each item by how far it sits from that average. One vector to store and one comparison per item, which makes it cheap enough to run on everything. What it costs you is an assumption: that your normal data forms one cluster. If your support tickets split into billing, shipping, and returns, the average of those three is a point in the middle that is near none of them, and everything scores as mildly odd.

Distance to the k nearest neighbors. Score each item by its average distance to its k closest points in the reference set. Something in a dense region scores low, something in empty space scores high, and the number of clusters stops mattering. Three groups are fine and so are twelve. The cost moves to query time: you need the whole reference set available and a nearest-neighbor lookup per item, which is what a vector database is for.

Use the centroid when your normal data is one thing. Use nearest neighbors otherwise, which is most of the time. If you are unsure, the diagnostic is to check whether the centroid's own nearest neighbors look like typical examples. If they do not, the centroid is not describing anything real.

Choosing the reference set that defines normal

The reference set defines normal, and this is the decision that determines whether the detector is useful. It gets less thought than the scoring math, which is backwards. The math is interchangeable and the reference set is not.

Two failure modes to design against.

If the set contains the anomalies you are hunting, they stop being anomalous. A detector built on all of last year's tickets will not flag the fraud pattern that ran all last year, because that pattern is part of what it learned to call normal.

If the set is narrower than reality, everything legitimate looks strange. Build a reference set from a single product line, run it across the whole catalog, and you flag the other products for the sin of existing.

The question to answer explicitly: normal compared to what, and over what period? A rolling window adapts to a business that changes, at the cost of gradually accepting a slow drift. A fixed set holds a standard, at the cost of flagging every legitimate change. Neither is right in general. Choose one on purpose rather than by accident.

Normalization, and the detector that flags long documents

Here is a concrete bug that catches people, and it comes from a detail in the model documentation rather than from anything about anomalies.

Some embedding models return "normalized vectors of length 1" and some do not. Sentence Transformers notes which, because it decides which similarity function is appropriate.

With normalized vectors, all points sit on a sphere, only direction varies, and cosine similarity and Euclidean distance rank identically. Everything behaves.

With unnormalized vectors, magnitude varies too, and Euclidean distance measures part magnitude and part direction. Magnitude often tracks text length. So you build a detector, run it, and it flags your longest documents, not because they are unusual in meaning, but because they are far from the origin.

That failure looks like a threshold problem and is not. No amount of tuning fixes it. Either normalize your vectors before scoring, or use cosine similarity, which ignores magnitude by construction. Check the model card first; this is a one-line fix that is very hard to find after the fact.

Setting a threshold from your own data

There is no universal cutoff, and any number you read in an article, including this one, is describing someone else's corpus.

What works is to derive it. Score your reference set, look at the distribution of scores, and pick a percentile. If you can review fifty items a day and you process five thousand, your threshold is the 99th percentile, because that is what fits the capacity you have. Starting from review capacity rather than from a distance value keeps the system honest: a threshold that produces more alerts than anyone can read is the same as no threshold.

Then look at what it caught. The first pass is almost always wrong in an informative way, usually a cluster of legitimate-but-unusual items that belong in the reference set, which is a signal to widen it rather than to raise the threshold.

And expect to revisit it. Scores drift as the corpus grows, and a fixed number set six months ago is describing a distribution that no longer exists.

What an embedding outlier is not

The gap between what this measures and what people want it to measure causes real trouble.

Embedding distance measures semantic unlikeness against a reference set. It does not measure wrongness, fraud, harm, or quality.

A correct, well-written support reply about a product launched last week will score as an outlier against a corpus that predates the launch. It is unusual. It is also entirely fine. Meanwhile, a fraudulent transaction described in completely ordinary language scores as typical, because it is typical as text.

So this is a triage tool. It finds things worth a look, which is useful when the alternative is looking at everything. It is not a classifier, and wiring it directly to an automated action means acting on "this is unlike what I have seen", which is not a judgment about anything.

When you need a judgment, you need something that was trained to make one: content moderation APIs for safety, a classifier for a known category, or a model asked to evaluate the item on stated criteria.

Further reading

Knowledge check

Question 1 of 4

Your support tickets fall into three distinct groups: billing, shipping, and returns. Which scoring method should you avoid, and why?

Sign in to save your progress and pick up where you left off.

Open this article on its own page