Train your own embedding model
Tokens to vectors·8 min

Cosine + L2-normalize

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage cosine-and-l2norm --watch

Two primitives power everything downstream: L2-normalize (put a vector on the unit sphere) and cosine (their dot product, once normalized). Build both now — this is the first green, and it's the whole course in miniature: make vectors point the right way under cosine.

The two formulas
l2normalize(v) = v / ‖v‖           where ‖v‖ = √(Σ vᵢ²)

cosine(u, v) = (u · v) / (‖u‖ ‖v‖)

What to build

Your task

Implement cosine + l2normalize

Implement cosine(u, v) and l2normalize(v) in src/tensor.ts, then wire runCosine in tests/adapters.ts. The adapter receives a list of { u, v } pairs and returns one cosine per pair (a number[]).

Guard the zero vector: if a norm is 0, return the zero vector from l2normalize and 0 from cosine — never divide by zero.

tests/adapters.ts
export function runCosine(pairs: { u: number[]; v: number[] }[]): number[] {
  return pairs.map((p) => cosine(p.u, p.v));
}
How it's graded

Adapter mode: runCosine is called on the fixture pairs and compared to the reference with allclose, atol 1e-6. One of the fixture pairs has a zero vector — your guard must return 0 there, not NaN.

Gotcha: the zero-norm guard

Use Math.sqrt of the sum of squares (or Math.hypot). If you forget the zero-norm guard, the zero-vector pair divides by 0 and emits NaN, which fails the whole allclose check — even though every other entry is correct.