Repository navigation
[WIP] Clarify transported mass in partial OT #902
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
JaylenTang
wants to merge
2
commits into
PythonOT:master
Choose a base branch
from
JaylenTang:docs-partial-ot-mass-review
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+222
−19
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| """ | ||
|
|
||
| # Author: Laetitia Chapel <laetitia.chapel@irisa.fr> | ||
| # Mass-interpretation extension: Jialin Tang <jtang3898@sdsu.edu> | ||
| # License: MIT License | ||
|
|
||
| # sphinx_gallery_thumbnail_number = 2 | ||
|
|
@@ -38,14 +39,15 @@ | |
|
|
||
| n_samples = 20 # nb samples (gaussian) | ||
| n_noise = 20 # nb of samples (noise) | ||
| random_state = np.random.RandomState(42) | ||
|
|
||
| mu = np.array([0, 0]) | ||
| cov = np.array([[1, 0], [0, 2]]) | ||
|
|
||
| xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) | ||
| xs = np.append(xs, (np.random.rand(n_noise, 2) + 1) * 4).reshape((-1, 2)) | ||
| xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) | ||
| xt = np.append(xt, (np.random.rand(n_noise, 2) + 1) * -3).reshape((-1, 2)) | ||
| xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=random_state) | ||
| xs = np.append(xs, (random_state.rand(n_noise, 2) + 1) * 4).reshape((-1, 2)) | ||
| xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov, random_state=random_state) | ||
| xt = np.append(xt, (random_state.rand(n_noise, 2) + 1) * -3).reshape((-1, 2)) | ||
|
|
||
| M = sp.spatial.distance.cdist(xs, xt) | ||
|
|
||
|
|
@@ -72,7 +74,7 @@ | |
| print("Partial Wasserstein distance (m = 0.5): " + str(log0["partial_w_dist"])) | ||
| print("Entropic partial Wasserstein distance (m = 0.5): " + str(log["partial_w_dist"])) | ||
|
|
||
| pl.figure(1, (10, 5)) | ||
| pl.figure(figsize=(10, 5)) | ||
| pl.subplot(1, 2, 1) | ||
| pl.imshow(w0, cmap="gray_r") | ||
| pl.title("Partial Wasserstein") | ||
|
|
@@ -82,6 +84,138 @@ | |
| pl.show() | ||
|
|
||
|
|
||
| ############################################################################# | ||
| # | ||
| # Interpret the transported mass on the same 2D samples | ||
| # ----------------------------------------------------- | ||
| # | ||
| # Both histograms have total mass one, so ``m`` is also the transported | ||
| # fraction here. With 40 uniformly weighted samples, each sample can supply | ||
| # or receive at most 1/40 of the mass. The parameter constrains total mass, | ||
| # rather than selecting a fixed number of points or one-to-one matches. | ||
| # | ||
| # We vary ``m`` using the exact partial Wasserstein solver. The first 20 | ||
| # samples in each distribution are Gaussian samples (called inliers below), | ||
| # and the last 20 are remote samples (called outliers). These synthetic | ||
| # labels are used only to summarize the plans; they are not solver inputs. | ||
| # A small mass can omit genuine overlap, while a large mass can require | ||
| # transporting remote samples. Partial OT does not guarantee outlier removal. | ||
|
|
||
| mass_study = { | ||
| "masses": np.array([0.25, 0.5, 0.75, 1.0]), | ||
| "plans": [], | ||
| "source_weights": p.copy(), | ||
| "target_weights": q.copy(), | ||
| "cost_matrix": M.copy(), | ||
| "inlier_count": n_samples, | ||
| "balanced_plan": ot.emd(p, q, M), | ||
| "records": [], | ||
| } | ||
| tolerance = 1e-8 | ||
|
|
||
| for mass in mass_study["masses"]: | ||
| plan = ot.partial.partial_wasserstein(p, q, M, m=mass) | ||
| assert np.isfinite(plan).all() and (plan >= 0).all() | ||
| assert (plan.sum(axis=1) <= p + tolerance).all() | ||
| assert (plan.sum(axis=0) <= q + tolerance).all() | ||
| assert np.isclose(plan.sum(), mass, atol=tolerance, rtol=0) | ||
| mass_study["plans"].append(plan) | ||
| mass_study["records"].append( | ||
| { | ||
| "mass": float(mass), | ||
| "cost": float(np.sum(plan * M)), | ||
| "source_inlier_mass": float(plan[:n_samples, :].sum()), | ||
| "source_outlier_mass": float(plan[n_samples:, :].sum()), | ||
| "target_inlier_mass": float(plan[:, :n_samples].sum()), | ||
| "target_outlier_mass": float(plan[:, n_samples:].sum()), | ||
| # Count a pair involving either outlier group only once. | ||
| "any_outlier_mass": float(plan.sum() - plan[:n_samples, :n_samples].sum()), | ||
| } | ||
| ) | ||
|
|
||
| # At full mass, these normalized histograms recover balanced OT. Optimal | ||
| # plans need not be unique, so compare the marginals and objective values. | ||
| full_mass_plan = mass_study["plans"][-1] | ||
| assert np.allclose(full_mass_plan.sum(axis=1), p, atol=tolerance, rtol=0) | ||
| assert np.allclose(full_mass_plan.sum(axis=0), q, atol=tolerance, rtol=0) | ||
| assert np.isclose( | ||
| np.sum(full_mass_plan * M), | ||
| np.sum(mass_study["balanced_plan"] * M), | ||
| atol=tolerance, | ||
| rtol=0, | ||
| ) | ||
|
|
||
| print("\nExact partial Wasserstein mass study (raw transport costs):") | ||
| print(" m cost source in source out target in target out any out") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. names are unclear please dont' use in and out that can mean source/target in OT but inlier outlier possibly . Actually it would porably be more clear with aplot (and maybe more valyues of m?) where you can have larger legends for the plots |
||
| for record in mass_study["records"]: | ||
| print( | ||
| f'{record["mass"]:4.2f} {record["cost"]:9.4f}' | ||
| f' {record["source_inlier_mass"]:10.4f}' | ||
| f' {record["source_outlier_mass"]:10.4f}' | ||
| f' {record["target_inlier_mass"]:10.4f}' | ||
| f' {record["target_outlier_mass"]:10.4f}' | ||
| f' {record["any_outlier_mass"]:10.4f}' | ||
| ) | ||
|
|
||
| ############################################################################# | ||
| # | ||
| # The plan matrices show which sample groups are coupled. Dashed lines | ||
| # separate the Gaussian and remote samples on each axis. All four panels | ||
| # use the same color scale, in units of mass per source-target pair. | ||
|
|
||
| fig_plans, plan_axes = pl.subplots( | ||
| 1, 4, figsize=(12, 3.6), sharex=True, sharey=True, constrained_layout=True | ||
| ) | ||
| for ax, mass, plan in zip(plan_axes, mass_study["masses"], mass_study["plans"]): | ||
| image = ax.imshow(plan, cmap="Blues", vmin=0, vmax=p.max()) | ||
| ax.axhline(n_samples - 0.5, color="gray", linestyle="--", linewidth=1) | ||
| ax.axvline(n_samples - 0.5, color="gray", linestyle="--", linewidth=1) | ||
| ax.set_title(f"m = {mass:g}") | ||
| ax.set_xlabel("Target sample index") | ||
| ax.set_xticks([0, n_samples, n_samples + n_noise - 1]) | ||
| plan_axes[0].set_ylabel("Source sample index") | ||
| fig_plans.suptitle("Exact partial OT: indices 0-19 are inliers, 20-39 are outliers") | ||
| fig_plans.colorbar(image, ax=plan_axes, label="Mass per pair", shrink=0.75) | ||
| pl.show() | ||
|
|
||
| ############################################################################# | ||
| # | ||
| # Summarize the mass used from each distribution separately. Each stacked | ||
| # bar has height ``m``. Mass involving any outlier is reported in the table | ||
| # as total mass minus the inlier-inlier block, since adding the source and | ||
| # target outlier masses would count outlier-outlier pairs twice. | ||
| # | ||
| # These summaries describe participation of the synthetic groups, not | ||
| # point-correspondence accuracy. Raw costs at different masses are not a | ||
| # direct comparison of matching quality, because the amount transported | ||
| # changes. The solver also does not choose ``m`` automatically. | ||
|
|
||
| fig_mass, mass_axes = pl.subplots( | ||
| 1, 2, figsize=(9, 4), sharey=True, constrained_layout=True | ||
| ) | ||
| for ax, side in zip(mass_axes, ("source", "target")): | ||
| inlier_masses = [record[f"{side}_inlier_mass"] for record in mass_study["records"]] | ||
| outlier_masses = [ | ||
| record[f"{side}_outlier_mass"] for record in mass_study["records"] | ||
| ] | ||
| ax.bar(mass_study["masses"], inlier_masses, width=0.15, label="Gaussian (inlier)") | ||
| ax.bar( | ||
| mass_study["masses"], | ||
| outlier_masses, | ||
| bottom=inlier_masses, | ||
| width=0.15, | ||
| label="Remote (outlier)", | ||
| ) | ||
| ax.set_title(f"{side.capitalize()} matched mass") | ||
| ax.set_xlabel("Requested transported mass m") | ||
| ax.set_xticks(mass_study["masses"]) | ||
| ax.set_ylim(0, 1.05) | ||
| mass_axes[0].set_ylabel("Matched mass") | ||
| mass_axes[1].legend(loc="upper left") | ||
| mass_study["figures"] = {"plans": fig_plans, "mass_accounting": fig_mass} | ||
| pl.show() | ||
|
|
||
|
|
||
| ############################################################################# | ||
| # | ||
| # Sample one 2D and 3D Gaussian distributions and plot them | ||
|
|
@@ -104,11 +238,13 @@ | |
| cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) | ||
|
|
||
|
|
||
| xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s) | ||
| xs = np.concatenate((xs, ((np.random.rand(n_noise, 2) + 1) * 4)), axis=0) | ||
| xs = ot.datasets.make_2D_samples_gauss( | ||
| n_samples, mu_s, cov_s, random_state=random_state | ||
| ) | ||
| xs = np.concatenate((xs, ((random_state.rand(n_noise, 2) + 1) * 4)), axis=0) | ||
| P = sp.linalg.sqrtm(cov_t) | ||
| xt = np.random.randn(n_samples, 3).dot(P) + mu_t | ||
| xt = np.concatenate((xt, ((np.random.rand(n_noise, 3) + 1) * 10)), axis=0) | ||
| xt = random_state.randn(n_samples, 3).dot(P) + mu_t | ||
| xt = np.concatenate((xt, ((random_state.rand(n_noise, 3) + 1) * 10)), axis=0) | ||
|
|
||
| fig = pl.figure() | ||
| ax1 = fig.add_subplot(121) | ||
|
|
@@ -137,7 +273,7 @@ | |
| print("Wasserstein distance (m = 1): " + str(log0["partial_gw_dist"])) | ||
| print("Entropic Wasserstein distance (m = 1): " + str(log["partial_gw_dist"])) | ||
|
|
||
| pl.figure(1, (10, 5)) | ||
| pl.figure(figsize=(10, 5)) | ||
| pl.title("mass to be transported m = 1") | ||
| pl.axis("off") | ||
| pl.subplot(1, 2, 1) | ||
|
|
@@ -161,7 +297,7 @@ | |
| print("Partial Wasserstein distance (m = 2/3): " + str(log0["partial_gw_dist"])) | ||
| print("Entropic partial Wasserstein distance (m = 2/3): " + str(log["partial_gw_dist"])) | ||
|
|
||
| pl.figure(2, (10, 5)) | ||
| pl.figure(figsize=(10, 5)) | ||
| pl.title("mass to be transported m = 2/3") | ||
| pl.axis("off") | ||
| pl.subplot(1, 2, 1) | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this discussion is interesting but much too detailed for a user's guide. you can eitehr move it to the exmaple file where it makes more sens or remove it. only keep a sentence with areference to the example please.