Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2025-05-15 - Vectorized Distance Expansion Formula Numerical Stability
**Learning:** Using the expansion formula ||a-b||^2 = ||a||^2 + ||b||^2 - 2ab for vectorized distance calculation provides significant speedup (leveraging BLAS dot product) but can introduce small floating-point discrepancies (negative values) due to subtractive cancellation.
**Action:** Always use `np.maximum(dists_sq, 0)` when using the expansion formula and allow slightly relaxed test tolerances (e.g., `rtol=1e-4`) if comparing against `np.linalg.norm`. Avoid redundant square root calculations if the downstream formula (like Radial Basis Function) uses squared distance directly.
13 changes: 8 additions & 5 deletions face_engine/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,14 +364,17 @@ def find_faces(
n_det = len(bbs)
if isinstance(limit, int) and limit < n_det:
if self.detector in ["hog", "mmod"]:
indices = range(limit)
indices = np.arange(limit)
else:
indices = np.argsort([(bb[2] - bb[0]) * (bb[3] - bb[1]) for bb in bbs])[
::-1
][:limit]
# Vectorized bounding box area calculation
areas = (bbs[:, 2] - bbs[:, 0]) * (bbs[:, 3] - bbs[:, 1])
indices = np.argsort(areas)[::-1][:limit]
# limit extra fields if any exist
for key, value in extra.items():
extra[key] = extra[key][limit]
if isinstance(value, np.ndarray):
extra[key] = value[indices]
elif isinstance(value, list):
extra[key] = [value[i] for i in indices]
bbs = bbs[indices]

if normalize:
Expand Down
45 changes: 36 additions & 9 deletions face_engine/models/basic_estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,51 @@ class BasicEstimator(Estimator, name="basic"):
def __init__(self):
self.embeddings = None
self.class_names = None
self.fitted_norms_sq = None

def fit(self, embeddings, class_names, **kwargs):
self.embeddings = embeddings
self.class_names = class_names
# Pre-calculate squared norms for faster distance computation
self.fitted_norms_sq = np.sum(self.embeddings**2, axis=1)

def predict(self, embeddings):
if self.class_names is None:
raise TrainError("Model is not fitted yet!")

scores = []
class_names = []
for embedding in embeddings:
distances = np.linalg.norm(self.embeddings - embedding, axis=1)
index = np.argmin(distances)
score = np.exp(-0.5 * distances[index] ** 2)
scores.append(score)
class_names.append(self.class_names[index])
return scores, class_names
# Ensure embeddings are a numpy array
embeddings = np.asarray(embeddings)
if embeddings.size == 0:
return [], []

# Vectorized distance calculation using expansion formula:
# ||a - b||^2 = ||a||^2 + ||b||^2 - 2ab
# backward compatibility check for fitted_norms_sq
if not hasattr(self, "fitted_norms_sq") or self.fitted_norms_sq is None:
self.fitted_norms_sq = np.sum(self.embeddings**2, axis=1)

input_norms_sq = np.sum(embeddings**2, axis=1)
dot_product = np.dot(embeddings, self.embeddings.T)

# dists_sq has shape (n_input, n_fitted)
dists_sq = (
input_norms_sq[:, np.newaxis] + self.fitted_norms_sq[np.newaxis, :] - 2 * dot_product
)

# Numerical stability: distances should not be negative
dists_sq = np.maximum(dists_sq, 0)

# Find the index of the minimum distance for each input embedding
indices = np.argmin(dists_sq, axis=1)

# Radial Basis Function (RBF) kernel as a confidence score
# Using dists_sq directly to avoid redundant square root calculations
scores = np.exp(-0.5 * dists_sq[np.arange(len(indices)), indices])

# Map indices to class names
class_names = [self.class_names[i] for i in indices]

return scores.tolist(), class_names

def save(self, dirname):
name = "%s.estimator.%s" % (self.name, "p")
Expand Down