Skip to content

Commit 2ead92d

Browse files
authored
ENH perf: numeric optimisation in nan_euclidean_distances, GaussianMixture and KNeighborsRegressor.predict (scikit-learn#34277)
1 parent 71616de commit 2ead92d

6 files changed

Lines changed: 33 additions & 13 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- :func:`metrics.nan_euclidean_distances` is now several times faster on dense data,
2+
with a more moderate speed-up for :class:`impute.KNNImputer` and estimators using
3+
``metric="nan_euclidean"`` such as :class:`neighbors.NearestNeighbors`.
4+
By :user:`Roman Yurchak <rth>`.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- :class:`mixture.GaussianMixture` with ``covariance_type="tied"`` is now faster,
2+
with the speed-up most visible for many components or high-dimensional data.
3+
By :user:`Roman Yurchak <rth>`.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- :meth:`neighbors.KNeighborsRegressor.predict` is now faster for regression with
2+
many output targets.
3+
By :user:`Roman Yurchak <rth>`.

sklearn/metrics/pairwise.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -549,9 +549,11 @@ def nan_euclidean_distances(
549549
# This may not be the case due to floating point rounding errors.
550550
np.fill_diagonal(distances, 0.0)
551551

552-
present_X = 1 - missing_X
553-
present_Y = present_X if Y is X else ~missing_Y
554-
present_count = np.dot(present_X, present_Y.T)
552+
# Cast the boolean presence masks to float so the matrix product runs
553+
# through BLAS instead of the much slower integer matmul.
554+
present_X = (~missing_X).astype(distances.dtype)
555+
present_Y = present_X if Y is X else (~missing_Y).astype(distances.dtype)
556+
present_count = present_X @ present_Y.T
555557
distances[present_count == 0] = np.nan
556558
# avoid divide by zero
557559
np.maximum(1, present_count, out=present_count)

sklearn/mixture/_gaussian_mixture.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -527,11 +527,16 @@ def _estimate_log_gaussian_prob(X, means, precisions_chol, covariance_type, xp=N
527527
log_prob[:, k] = xp.sum(xp.square(y), axis=1)
528528

529529
elif covariance_type == "tied":
530-
log_prob = xp.empty((n_samples, n_components), dtype=X.dtype, device=device_)
531-
for k in range(means.shape[0]):
532-
mu = means[k, :]
533-
y = (X @ precisions_chol) - (mu @ precisions_chol)
534-
log_prob[:, k] = xp.sum(xp.square(y), axis=1)
530+
# In the tied case all components share precisions_chol, so project X
531+
# and the means once and expand ||Xp - mu_proj||**2 (as in the diag and
532+
# spherical branches below).
533+
Xp = X @ precisions_chol
534+
mu_proj = means @ precisions_chol
535+
log_prob = (
536+
row_norms(mu_proj, squared=True)
537+
- 2.0 * (Xp @ mu_proj.T)
538+
+ row_norms(Xp, squared=True)[:, xp.newaxis]
539+
)
535540

536541
elif covariance_type == "diag":
537542
precisions = precisions_chol**2

sklearn/neighbors/_regression.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,15 @@ def predict(self, X):
259259
if weights is None:
260260
y_pred = np.mean(_y[neigh_ind], axis=1)
261261
else:
262-
y_pred = np.empty((neigh_dist.shape[0], _y.shape[1]), dtype=np.float64)
262+
# Weighted sum of the neighbors' targets over the neighbor axis,
263+
# for every query and output column. Equivalent to
264+
# np.sum(_y[neigh_ind] * weights[:, :, None], axis=1)
265+
# but the einsum avoids materializing that full product. Here
266+
# _y[neigh_ind] is (n_queries, n_neighbors, n_outputs) and weights
267+
# is (n_queries, n_neighbors).
263268
denom = np.sum(weights, axis=1)
264-
265-
for j in range(_y.shape[1]):
266-
num = np.sum(_y[neigh_ind, j] * weights, axis=1)
267-
y_pred[:, j] = num / denom
269+
num = np.einsum("ijk,ij->ik", _y[neigh_ind], weights)
270+
y_pred = num / denom[:, None]
268271

269272
if self._y.ndim == 1:
270273
y_pred = y_pred.ravel()

0 commit comments

Comments
 (0)