Gram-Schmidt Orthonormalization
Implement the Gram-Schmidt process to orthonormalize a set of vectors using NumPy.
Given a list of linearly independent vectors, produce an orthonormal basis where:
- Each output vector is unit length (norm = 1)
- All output vectors are mutually orthogonal (dot product = 0)
The Gram-Schmidt process:
- Start with the first vector, normalize it: u1β=β₯v1ββ₯v1ββ
- For each subsequent vector vkβ:
- Subtract projections onto all previous orthonormal vectors
- wkβ=vkβββj=1kβ1β(vkββ ujβ)ujβ
- Normalize: ukβ=β₯wkββ₯wkββ
This algorithm is fundamental in CV for creating orthonormal bases in feature spaces.
Example:
vectors = [[1, 1, 0], [1, 0, 1], [0, 1, 1]]
[[0.7071, 0.7071, 0.0], [0.4082, -0.4082, 0.8165], [-0.5774, 0.5774, 0.5774]]
Step 1: Normalize first vector v1β=[1,1,0] β₯v1ββ₯=12+12+02β=2β u1β=[2β1β,2β1β,0]=[0.7071,0.7071,0]
Step 2: Orthogonalize second vector v2β=[1,0,1] Projection: v2ββ u1β=1Γ0.7071+0Γ0.7071+1Γ0=0.7071 w2β=[1,0,1]β0.7071Γ[0.7071,0.7071,0]=[0.5,β0.5,1] β₯w2ββ₯=0.25+0.25+1β=1.5ββ1.2247 u2β=[0.4082,β0.4082,0.8165]
Step 3: Orthogonalize third vector similarly...
Constraints:
- Input: List of n vectors, each of dimension d
- Vectors are linearly independent
- Return: List of n orthonormal vectors (as lists)
- Round each component to 4 decimal places
Gram-Schmidt Orthonormalization: Background & Implementation Guide
Background Knowledge
The Gram-Schmidt process is a fundamental algorithm for converting a set of linearly independent vectors into an orthonormal basisβa set of vectors that are mutually perpendicular and each have unit length. This is essential in computer vision and machine learning because orthonormal bases simplify many computations: dot products become projections, matrix operations become more numerically stable, and geometric interpretations become clearer.
The key insight is that any vector can be decomposed into two components: one parallel to a given direction and one perpendicular to it. The Gram-Schmidt process iteratively removes the "parallel" components (projections onto previously computed orthonormal vectors) from each new vector, leaving only the perpendicular part, which is then normalized. Mathematically, the projection of vector v onto a unit vector u is (\mathbf{v}β \mathbf{u})u, and subtracting this from v gives the perpendicular component.
This process is widely used in QR decomposition, solving least-squares problems, and constructing orthonormal feature spaces in computer vision applications.
Algorithm/Approach
The Gram-Schmidt process follows a sequential, iterative pattern:
- Normalize the first vector to create the first orthonormal basis vector
- For each subsequent vector, apply a two-step procedure:
- Orthogonalize: Remove all components parallel to previously computed orthonormal vectors
- Normalize: Scale the result to unit length
This greedy approach works because once you have orthonormal vectors u1β,β¦,\mathbf{u}kβ1β, the subspace orthogonal to all of them is well-defined, and any vector in that subspace can be made orthonormal by normalization.
Step-by-Step Strategy
Step 1: Input Validation
- Verify that input vectors are linearly independent (no zero vectors after orthogonalization)
- Ensure vectors are stored in a format compatible with NumPy operations (e.g., as rows or columns in a matrix)
Step 2: Initialize the Output
- Create a container (list or array) to store orthonormal vectors as you compute them
Step 3: Process the First Vector
- Compute its norm using np.linalg.norm()
- Divide by the norm to normalize
- Store as the first orthonormal vector
Step 4: Iteratively Process Remaining Vectors
- For each vector vkβ:
- Initialize wkβ=\mathbf{v}kβ (a copy to avoid modifying the original)
- Loop through all previously computed orthonormal vectors ujβ:
- Compute the projection: proj=(\mathbf{v}kββ \mathbf{u}jβ)
- Subtract: wkβ=\mathbf{w}kββ\text{proj}β \mathbf{u}jβ
- Normalize wkβ and store as ukβ
Step 5: Return the Result
- Combine all orthonormal vectors into the desired output format
Common Pitfalls
Numerical Instability: The classical Gram-Schmidt process can accumulate floating-point errors, especially with many vectors or nearly parallel input vectors. The computed vectors may lose orthogonality. Consider using the modified Gram-Schmidt variant, which reorthogonalizes by processing projections differently.
Zero Vectors After Orthogonalization: If a vector becomes (nearly) zero after subtracting projections, it indicates linear dependence among the input vectors. You must handle this gracefullyβeither skip the vector or raise an error.
In-Place Modifications: Avoid modifying input vectors directly. Always work with copies to preserve the original data.
Norm Computation: Use np.linalg.norm() rather than manual calculations for numerical stability. Be aware that very small norms (near machine epsilon) indicate numerical issues.
Dot Product Order: Ensure you're computing dot products correctlyβthe order matters for readability and debugging, though mathematically aβ \mathbf{b}=\mathbf{b}β a.
Time & Space Complexity
Time Complexity: O(nβ m2), where n is the dimension of each vector and m is the number of vectors. For each of the m vectors, you perform O(m) dot products and vector operations, each costing O(n).
Space Complexity: O(nβ m) for storing the input vectors and output orthonormal basis. If you process vectors sequentially and only keep the current and previous vectors in memory, you can reduce this to O(n), but typically you'll store all results.
Practical Note: The modified Gram-Schmidt variant has the same asymptotic complexity but better numerical properties, making it preferable for real-world applications.