A Finite Vector Broke Our Normalisation
Why a Correct Formula Can Lose Its Answer
BOTIE
David Goldberg’s 1991 survey explained these practical problems for people building computer systems. It connected floating-point representation and rounding rules to failures in real calculations. In our NumPy example, float64 uses a fixed 64-bit format: a finite component can fit while its square exceeds the format’s range. That is overflow.
We only want a direction, so we can change the common scale before squaring. The geometry stays the same, and the products fit in a safer range. We will inspect that change one operation at a time, then test it on a new encoder output.
Why Did Two Ordinary Directions Become Zero?
Botie compares a query with documents represented by lists of numerical coordinates, called vectors. For this comparison we care about their direction while setting aside their overall size. The small example works. Then the encoder, the model producing those coordinates, returns much larger components, and the same calculation produces zeros.
The direction did not disappear from the input. We lost it while trying to calculate the vector’s length. Let’s follow those operations before deciding that the model gave us a useless result.
The workshop follows small finite binary64 vectors and deliberately returns zeros for an all-zero input. A caller must decide whether a document with that outcome should enter retrieval.
Step 1 of 1
The direction survives a change of scale
Compare the procedure before deciding that the encoder produced a meaningless vector.
| Quantity | Small encoder output | Large encoder output |
|---|---|---|
| Components | [3, 4] | [3e200, 4e200] |
| Divide by largest magnitude | [0.75, 1] | [0.75, 1] |
| Unit direction | [0.6, 0.8] | [0.6, 0.8] |
Components
- Small encoder output
- [3, 4]
- Large encoder output
- [3e200, 4e200]
Divide by largest magnitude
- Small encoder output
- [0.75, 1]
- Large encoder output
- [0.75, 1]
Unit direction
- Small encoder output
- [0.6, 0.8]
- Large encoder output
- [0.6, 0.8]
What is the normalisation trying to preserve?
Rescaling protects the intermediate products while preserving the direction we compare.
Keep the direction when the encoder changes scale
Botie’s retrieval pipeline turns a valid large embedding into zeros while trying to normalise it.
BOTIE
Direction vector after safe normalisation
- 00.6
- 10.8
Follow the operations
1. Encoder coordinates for one document
- 03
- 14
2. Absolute value
- 03
- 14
3. Largest value
4
4. Choose a nonzero divisor
4
5. Divide
- 00.75
- 11
6. Square
- 00.5625
- 11
7. Add a list
1.5625
8. Square root
1.25
9. Choose a nonzero divisor
1.25
10. Divide
- 00.6
- 10.8
11. Result
- 00.6
- 10.8
The workshop follows small finite binary64 vectors and deliberately returns zeros for an all-zero input. A caller must decide whether a document with that outcome should enter retrieval.
1. What is the normalisation trying to preserve?
Picture two arrows pointing in the same direction, with one longer than the other. The coordinate lists [3,4] and [6,8] describe that relationship. When our retrieval comparison uses direction, we want both lists to produce the same length-one vector. Dividing every component by one common positive length preserves their proportions.
The coordinate positions must still mean the same things. Position zero in the query must match position zero in a document from that encoder. Removing length cannot make unrelated encoder coordinate systems comparable.
length(x) = sqrt(sum of the squared components)
unit(x) = x / length(x)For an all-zero vector there is no direction to preserve. We return zeros as an explicit special outcome, so the caller can decide how to handle that document.
2. Find the operation that fails before replacing the formula
A floating-point number can be finite while its square is too large to store. At the opposite end, a small nonzero square can round to zero. Neither outcome establishes that the input itself was invalid.
Inspect intermediate values and the array dtype. Switching to a wider type may postpone a range problem, but an unsuitable evaluation can still fail. Replacing an infinity with a large constant would also change the result without recovering the lost calculation.
large components: [3e200, 4e200]
direct squares: [9e400, 16e400]
these squares exceed binary64 rangeThe mathematical squares exist. The machine format cannot represent them as finite binary64 values.
3. Rescale before the dangerous products
Use the largest absolute component as a common scale. After dividing by that scale, every component has magnitude at most one. Now compute the length of this scaled vector and divide by it.
We do not need to multiply back by the original scale when the goal is a unit direction. Avoiding that unnecessary multiplication also helps when the original mathematical length itself would exceed the representable range.
scale = max(abs(x))
z = x / scale
unit(x) = z / sqrt(sum(z * z))
x = [3e200, 4e200]
z = [0.75, 1]
length(z) = 1.25
unit(x) = [0.6, 0.8]Each operation has a reason. The first division protects the products; the second removes the remaining length.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
Which intermediate calculation can overflow?
Use the information below.
The vector is [3e200, 4e200], using binary64. Its components are finite.4. Give the zero vector an explicit outcome
A document can produce a nonempty vector whose entries are all zero. Its magnitude is zero, so it has no direction to preserve. Our procedure returns a vector of zeros in that case. It rejects an empty vector, which has no components at all. An application must still decide whether to reject a document represented by zeros, skip it or use a different representation.
In the workshop, a zero magnitude is replaced with one only when choosing a divisor. That leaves the zero values at zero. We are not asserting that their direction is now known.
Using a very small arbitrary constant as every divisor can distort small, meaningful vectors. Handle the actual zero case separately.
5. Work through the values before writing the reusable procedure
Follow one large vector through the program below. The scaled components form a new NumPy array. Taking that array’s dot product with itself gives one number, and the final division applies the same divisor to every component.
Pause at the printed scale and scaled coordinates. This is where we avoided forming the enormous squares of the original values.
The returned coordinates can now be used for the intended direction comparison. Whether the retrieved document is useful still needs its own evaluation.
Inspect the scaled calculation on one document
This program inspects one known nonzero vector. The reusable challenges must also handle zero inputs and invalid shapes.
import numpy as np
x = np.array([3e200,4e200])
z = x / np.max(np.abs(x))
print(z.tolist())
print(np.round(z / np.sqrt(z @ z),3))[0.75, 1.0]
[0.6 0.8]The dangerous large squares never occur. The final vector preserves the direction of the original coordinates.
6. A batch introduces another choice: which axis belongs to one example?
A table of embeddings usually has one example per row and one coordinate per column. Find a separate scale for every row. Keeping the reduced axis as a length-one dimension lets NumPy broadcast each scale back across its own row.
One scale for the entire table is mathematically possible in exact arithmetic, but it can lose the small rows when another row is vastly larger. Independent row scaling matches the calculation we actually want.
batch shape: (number_of_documents, encoder_dimensions)
row scales shape with keepdims=True: (number_of_documents, 1)Axes are part of the data contract. Normalising each column answers a different question.
7. Check the direction under a new range of inputs
Test large and tiny positive rescalings of the same nonzero vector. The unit direction should stay close. Test sign changes, zero rows and malformed inputs as separate behaviours.
Comparing the norm alone is insufficient: many wrong directions have length one. Keep an expected direction for a simple vector whose geometry we can inspect.
This fixes one source of numerical failure. A drifting encoder, an incorrect similarity metric and a poor retrieval corpus still require their own evidence.
Check yourself
Questions on Parts 1 to 7
Question 1 of 4
The returned vector now has a known unit direction.
Consider this example.
An input vector is exactly [0,0]. This implementation returns [0,0].Try it with Botie
Build with Botie
Write and run Python