|
20 | 20 | ) |
21 | 21 |
|
22 | 22 |
|
| 23 | +def _normalize_inputs(X_s, X_t, normalize, normalize_mode, nx): |
| 24 | + """Normalize input distributions before computing sliced Wasserstein distance. |
| 25 | +
|
| 26 | + Parameters |
| 27 | + ---------- |
| 28 | + X_s : array-like, shape (n_s, d) |
| 29 | + Source samples |
| 30 | + X_t : array-like, shape (n_t, d) |
| 31 | + Target samples |
| 32 | + normalize : str or None |
| 33 | + Normalization method. One of {None, 'standard', 'minmax', 'l2'}. |
| 34 | + normalize_mode : str |
| 35 | + Reference for computing statistics. One of {'joint', 'source', 'target'}. |
| 36 | + Ignored when normalize is None or 'l2'. |
| 37 | + nx : backend |
| 38 | + POT backend instance (from ot.backend.get_backend) |
| 39 | +
|
| 40 | + Returns |
| 41 | + ------- |
| 42 | + X_s_out : array-like, shape (n_s, d) |
| 43 | + Normalized source samples |
| 44 | + X_t_out : array-like, shape (n_t, d) |
| 45 | + Normalized target samples |
| 46 | + """ |
| 47 | + if normalize is None: |
| 48 | + return X_s, X_t |
| 49 | + |
| 50 | + if normalize_mode not in ("joint", "source", "target"): |
| 51 | + raise ValueError( |
| 52 | + f"Invalid normalize_mode '{normalize_mode}'. " |
| 53 | + "Expected one of: 'joint', 'source', 'target'." |
| 54 | + ) |
| 55 | + |
| 56 | + if normalize == "standard": |
| 57 | + # TODO: full implementation |
| 58 | + # - compute mean/std using nx ops based on normalize_mode |
| 59 | + # - apply to both X_s and X_t |
| 60 | + # - handle zero-variance columns with warnings.warn |
| 61 | + raise NotImplementedError( |
| 62 | + "normalize='standard' will be implemented in a follow-up commit." |
| 63 | + ) |
| 64 | + |
| 65 | + elif normalize == "minmax": |
| 66 | + # TODO: full implementation |
| 67 | + # - compute min/max using nx ops based on normalize_mode |
| 68 | + # - apply to both X_s and X_t |
| 69 | + # - handle zero-range columns with warnings.warn |
| 70 | + raise NotImplementedError( |
| 71 | + "normalize='minmax' will be implemented in a follow-up commit." |
| 72 | + ) |
| 73 | + |
| 74 | + elif normalize == "l2": |
| 75 | + # TODO: full implementation |
| 76 | + # - row-wise L2 normalization (normalize_mode is ignored) |
| 77 | + # - handle zero-norm rows with warnings.warn |
| 78 | + raise NotImplementedError( |
| 79 | + "normalize='l2' will be implemented in a follow-up commit." |
| 80 | + ) |
| 81 | + |
| 82 | + else: |
| 83 | + raise ValueError( |
| 84 | + f"Invalid normalize value '{normalize}'. " |
| 85 | + "Expected one of: None, 'standard', 'minmax', 'l2'." |
| 86 | + ) |
| 87 | + |
| 88 | + |
23 | 89 | def get_random_projections(d, n_projections, seed=None, backend=None, type_as=None): |
24 | 90 | r""" |
25 | 91 | Generates n_projections samples from the uniform on the unit sphere of dimension :math:`d-1`: :math:`\mathcal{U}(\mathcal{S}^{d-1})` |
@@ -76,6 +142,8 @@ def sliced_wasserstein_distance( |
76 | 142 | projections=None, |
77 | 143 | seed=None, |
78 | 144 | log=False, |
| 145 | + normalize=None, |
| 146 | + normalize_mode="joint", |
79 | 147 | ): |
80 | 148 | r""" |
81 | 149 | Computes a Monte-Carlo approximation of the p-Sliced Wasserstein distance |
@@ -109,6 +177,24 @@ def sliced_wasserstein_distance( |
109 | 177 | Seed used for random number generator |
110 | 178 | log: bool, optional |
111 | 179 | if True, sliced_wasserstein_distance returns the projections used and their associated EMD. |
| 180 | + normalize : str or None, optional |
| 181 | + Normalization applied to X_s and X_t before computing the distance. |
| 182 | + Useful when features have different scales. Options: |
| 183 | +
|
| 184 | + - ``None`` : no normalization (default, preserves existing behavior) |
| 185 | + - ``'standard'`` : zero mean, unit variance per feature dimension |
| 186 | + - ``'minmax'`` : scale each feature to [0, 1] |
| 187 | + - ``'l2'`` : normalize each sample to unit L2 norm (row-wise) |
| 188 | +
|
| 189 | + normalize_mode : str, optional |
| 190 | + Determines which samples are used to compute normalization statistics. |
| 191 | + Ignored when ``normalize`` is ``None`` or ``'l2'``. Options: |
| 192 | +
|
| 193 | + - ``'joint'`` : statistics from ``concat(X_s, X_t)`` (default). |
| 194 | + Preserves symmetry: SWD(X_s, X_t) == SWD(X_t, X_s). |
| 195 | + - ``'source'`` : statistics from ``X_s`` only. Useful for drift |
| 196 | + detection where X_s is the reference distribution. |
| 197 | + - ``'target'`` : statistics from ``X_t`` only. |
112 | 198 |
|
113 | 199 | Returns |
114 | 200 | ------- |
@@ -136,6 +222,8 @@ def sliced_wasserstein_distance( |
136 | 222 |
|
137 | 223 | nx = get_backend(X_s, X_t, a, b, projections) |
138 | 224 |
|
| 225 | + X_s, X_t = _normalize_inputs(X_s, X_t, normalize, normalize_mode, nx) |
| 226 | + |
139 | 227 | n = X_s.shape[0] |
140 | 228 | m = X_t.shape[0] |
141 | 229 |
|
@@ -181,6 +269,8 @@ def max_sliced_wasserstein_distance( |
181 | 269 | projections=None, |
182 | 270 | seed=None, |
183 | 271 | log=False, |
| 272 | + normalize=None, |
| 273 | + normalize_mode="joint", |
184 | 274 | ): |
185 | 275 | r""" |
186 | 276 | Computes a Monte-Carlo approximation of the max p-Sliced Wasserstein distance |
@@ -215,6 +305,24 @@ def max_sliced_wasserstein_distance( |
215 | 305 | Seed used for random number generator |
216 | 306 | log: bool, optional |
217 | 307 | if True, sliced_wasserstein_distance returns the projections used and their associated EMD. |
| 308 | + normalize : str or None, optional |
| 309 | + Normalization applied to X_s and X_t before computing the distance. |
| 310 | + Useful when features have different scales. Options: |
| 311 | +
|
| 312 | + - ``None`` : no normalization (default, preserves existing behavior) |
| 313 | + - ``'standard'`` : zero mean, unit variance per feature dimension |
| 314 | + - ``'minmax'`` : scale each feature to [0, 1] |
| 315 | + - ``'l2'`` : normalize each sample to unit L2 norm (row-wise) |
| 316 | +
|
| 317 | + normalize_mode : str, optional |
| 318 | + Determines which samples are used to compute normalization statistics. |
| 319 | + Ignored when ``normalize`` is ``None`` or ``'l2'``. Options: |
| 320 | +
|
| 321 | + - ``'joint'`` : statistics from ``concat(X_s, X_t)`` (default). |
| 322 | + Preserves symmetry: SWD(X_s, X_t) == SWD(X_t, X_s). |
| 323 | + - ``'source'`` : statistics from ``X_s`` only. Useful for drift |
| 324 | + detection where X_s is the reference distribution. |
| 325 | + - ``'target'`` : statistics from ``X_t`` only. |
218 | 326 |
|
219 | 327 | Returns |
220 | 328 | ------- |
@@ -242,6 +350,8 @@ def max_sliced_wasserstein_distance( |
242 | 350 |
|
243 | 351 | nx = get_backend(X_s, X_t, a, b, projections) |
244 | 352 |
|
| 353 | + X_s, X_t = _normalize_inputs(X_s, X_t, normalize, normalize_mode, nx) |
| 354 | + |
245 | 355 | n = X_s.shape[0] |
246 | 356 | m = X_t.shape[0] |
247 | 357 |
|
|
0 commit comments