Contrastive training compares embeddings by cosine similarity, which requires them to live on the unit sphere. Implement the L2 normalization applied to every image and text embedding before the dot product.
Background
For a vector v, the L2-normalized version is
v^=∥v∥2v,∥v∥2=∑ivi2
After this, a dot product between two normalized vectors is their cosine similarity. Guard the zero vector: if the norm is 0, return the vector unchanged (all zeros) rather than dividing by zero.
Your Task
Implement:
def l2_normalize(v):
Return the normalized vector as a list rounded to 4 decimals.
Input Format
v: list of numbers.
Output Format
A list of floats rounded to 4 decimals.
Sample
print(l2_normalize([3.0, 4.0]))
Output:
[0.6, 0.8]
Example
Input
print(l2_normalize([3.0, 4.0]))
Output
[0.6, 0.8]
Reasoning
The input v is the embedding [3.0,4.0]: a point in 2-D space whose length is not yet 1, so a dot product with it would mix direction with magnitude.
Its Euclidean length is ∥v∥2=32+42=9+16=25=5, the classic 3-4-5 right triangle.
Dividing every component by that length shrinks the vector onto the unit circle without turning it: v^=[3/5,4/5]=[0.6,0.8].
Check: 0.62+0.82=0.36+0.64=1, so the result has length exactly 1. Both values are already exact at 4 decimal places, giving [0.6, 0.8].
Notice that the ratio 3:4 is preserved: only the scale changes, never the direction. Any positive multiple, such as [6,8], lands on the same [0.6, 0.8].
The zero vector has length 0 and no direction, so [0.0, 0.0] comes back as [0.0, 0.0] instead of a division by zero.
Constraints
1 <= len(v) <= 4096.
The norm is the Euclidean (L2) norm.
A zero vector is returned unchanged (no division by zero).
In Vision-Language Models (VLMs) like CLIP and SigLIP, images and text are mapped into a shared high-dimensional embedding space. The core training objective is contrastive learning, which pulls matching image-text pairs closer together and pushes non-matching pairs apart. To measure "closeness" in this space, models rely on cosine similarity, defined as the dot product of two vectors divided by the product of their magnitudes:
cos(θ)=∥a∥2∥b∥2a⋅b
A key mathematical property is that if both vectors are L2-normalized (i.e., scaled to have a unit length of 1), the denominator becomes 1×1=1. Consequently, the cosine similarity simplifies to just the dot product: a⋅b. This is computationally efficient and numerically stable, which is why normalization is a standard preprocessing step before computing similarities in contrastive objectives.
L2 normalization, also known as Euclidean normalization, projects a vector onto the unit sphere. For a vector v=[v1,v2,…,vn], the L2 norm (or magnitude) is calculated as:
∥v∥2=i=1∑nvi2
The normalized vector v^ is then obtained by dividing each component by this norm. This operation preserves the direction of the vector while standardizing its length, ensuring that all embeddings are comparable regardless of their original scale.
2. Algorithm Approach
The solution follows a straightforward vector arithmetic pattern:
Compute the Norm: Iterate through the input vector to calculate the sum of squares of all elements, then take the square root to get the L2 norm.
Handle Edge Cases: Check if the computed norm is zero. If so, return the original vector (or a list of zeros) to avoid division by zero.
Scale the Vector: Divide each element of the input vector by the computed norm.
Format Output: Round each resulting element to 4 decimal places as specified.
This is a linear-time operation that does not require complex data structures or iterative refinement.
3. Step-by-Step Strategy
Initialize Sum of Squares: Create a variable (e.g., sum_sq) initialized to 0.
Calculate Norm:
Loop through each element x in the input list v.
Add x * x to sum_sq.
After the loop, compute norm = sqrt(sum_sq).
Zero-Vector Guard:
If norm == 0, return [0.0] * len(v) (or the original list, depending on strict interpretation, but usually zeros are expected for a zero vector).
Normalize:
Create a new list where each element x is replaced by x / norm.
Round and Return:
Apply round(value, 4) to each element in the normalized list.
Return the final list.
Example logic snippet:
import math
def l2_normalize(v):
sum_sq = sum(x * x for x in v)
norm = math.sqrt(sum_sq)
if norm == 0:
return [0.0] * len(v)
return [round(x / norm, 4) for x in v]
4. Common Pitfalls
Division by Zero: Forgetting to check if the norm is zero will cause a ZeroDivisionError when the input is [0, 0, 0]. Always guard against this.
Integer Division: In Python 3, / performs float division, which is correct. However, if you accidentally use // (floor division), you will lose precision and get incorrect results.
Rounding Precision: The problem specifically asks for rounding to 4 decimals. Using round(x, 2) or returning full precision will fail test cases. Ensure you apply round() to each element individually.
Modifying the Input: Avoid modifying the original list v in place. Create a new list for the normalized values to maintain function purity.
Floating-Point Errors: When checking if the norm is zero, direct comparison norm == 0 is generally safe for this specific problem since the input is a list of numbers and the sum of squares is exactly 0 only if all inputs are 0. However, in more complex numerical contexts, using a small epsilon (e.g., 1e-8) is a safer practice.
5. Time & Space Complexity
Time Complexity: O(n), where n is the length of the input vector. We iterate through the vector once to compute the sum of squares and once more to normalize and round the elements.
Space Complexity: O(n), because we store the normalized vector in a new list of the same size as the input. The input list itself is not modified.