From 1c1dd595958c481243a59516cbaa0d5693933547 Mon Sep 17 00:00:00 2001 From: LinusBleistein Date: Tue, 24 Jun 2025 11:39:39 +0200 Subject: [PATCH 01/10] add notes for fairness implementations. @marie @roman feel free to add notes in this file --- notes_fairness.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 notes_fairness.md diff --git a/notes_fairness.md b/notes_fairness.md new file mode 100644 index 00000000..1b54e13d --- /dev/null +++ b/notes_fairness.md @@ -0,0 +1,6 @@ +# Notes Fairness + +- Idée 1: utiliser les méthodes de DA pour faire de la fairness. + - Importer les datasets depuis fairlearn + - Regarder si les méthodes de DA marchent +- Idée 2: regarder le setup un attribut sensible + domaine \ No newline at end of file From 84f57679eb49e017c2c10b935d9f2b1518036c66 Mon Sep 17 00:00:00 2001 From: LinusBleistein Date: Tue, 24 Jun 2025 11:42:00 +0200 Subject: [PATCH 02/10] add notes for fairness implementations. @marie @roman feel free to add notes in this file --- notes_fairness.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/notes_fairness.md b/notes_fairness.md index 1b54e13d..fc8a1293 100644 --- a/notes_fairness.md +++ b/notes_fairness.md @@ -2,5 +2,8 @@ - Idée 1: utiliser les méthodes de DA pour faire de la fairness. - Importer les datasets depuis fairlearn - - Regarder si les méthodes de DA marchent + - Regarder si les méthodes de DA marchent + - Marie: add datasets + - Roman: format datasets + - Linus: metrics - Idée 2: regarder le setup un attribut sensible + domaine \ No newline at end of file From fe3669938255808eafd863936ff41f5980d90e8c Mon Sep 17 00:00:00 2001 From: Zazou Date: Tue, 24 Jun 2025 16:53:16 +0200 Subject: [PATCH 03/10] example use of DA for fairness --- examples/methods/plot_fairness_da.py | 126 +++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 examples/methods/plot_fairness_da.py diff --git a/examples/methods/plot_fairness_da.py b/examples/methods/plot_fairness_da.py new file mode 100644 index 00000000..8ded083f --- /dev/null +++ b/examples/methods/plot_fairness_da.py @@ -0,0 +1,126 @@ +# %% +import matplotlib.pyplot as plt +import numpy as np +from fairlearn.datasets import fetch_acs_income +from sklearn.ensemble import RandomForestRegressor +from sklearn.preprocessing import StandardScaler + +from skada import LinearOTMapping, source_target_split + +# %% +# Load the dataset +df = fetch_acs_income(as_frame=True) +X_df = df["frame"] +y = df["target"].to_numpy() +sample_domain = X_df["SEX"].to_numpy() # 1: male, 2: female + +# Drop target and sensitive attribute to get feature matrix +X = X_df.drop(columns=["PINCP", "SEX"]).to_numpy() + +# take 10% of data +n_samples = int(0.1 * X.shape[0]) +X = X[:n_samples] +y = y[:n_samples] +sample_domain = sample_domain[:n_samples] + +# Normalize features +X = StandardScaler().fit_transform(X) + +# Re-label domains: source=1 (e.g. male), target=2 (e.g. female) +sample_domain = np.where(sample_domain == 1, 1, -1) + + +# %% + +X_source, X_target, y_source, y_target = source_target_split( + X, y, sample_domain=sample_domain +) +print(f"Source domain size: {X_source.shape[0]}") +print(f"Target domain size: {X_target.shape[0]}") +print(f"Source domain income mean: {y_source.mean()}") +print(f"Target domain income mean: {y_target.mean()}") +plt.figure(figsize=(10, 4)) +plt.subplot(1, 2, 1) +plt.hist(y_source, bins=50, alpha=0.5, label="Male (source)") +plt.hist(y_target, bins=50, alpha=0.5, label="Female (target)") +plt.legend() +plt.title("Income distribution before adaptation") +plt.xlabel("PINCP") + +# %% + +# Train regressor on source + +clf = RandomForestRegressor(n_estimators=5, random_state=31415) +clf.fit(X_source, y_source) + +# Evaluate performance +R2_source = clf.score(X_source, y_source) +R2_target = clf.score(X_target, y_target) + +# Scatterplot of predicted vs true (no decision boundary in regression) +y_pred_source = clf.predict(X_source) +y_pred_target = clf.predict(X_target) + +plt.figure(2, figsize=(10, 4)) + +plt.subplot(1, 2, 1) +plt.scatter(y_source, y_pred_source, alpha=0.5, label="Source") +plt.plot([y_source.min(), y_source.max()], [y_source.min(), y_source.max()], "k--") +plt.xlabel("True income") +plt.ylabel("Predicted income") +plt.title(f"Source (R²={R2_source:.2f})") +plt.legend() + +plt.subplot(1, 2, 2) +plt.scatter(y_target, y_pred_target, alpha=0.5, label="Target") +plt.plot([y_target.min(), y_target.max()], [y_target.min(), y_target.max()], "k--") +plt.xlabel("True income") +plt.ylabel("Predicted income") +plt.title(f"Target (R²={R2_target:.2f})") +plt.legend() + +plt.tight_layout() +plt.show() + + +# %% +# ---------------------------------- +# Build OTDA pipeline for regression +clf_otda = LinearOTMapping(RandomForestRegressor(n_estimators=5, random_state=31415)) + +# modify y such that for the target domain there are only nan +y = np.where(sample_domain == 1, y, np.nan) +clf_otda.fit(X, y, sample_domain=sample_domain) + +# Evaluate R² scores +R2_source = clf_otda.score(X_source, y_source) +R2_target = clf_otda.score(X_target, y_target) + +# Predict +y_pred_source = clf_otda.predict(X_source) +y_pred_target = clf_otda.predict(X_target) + +# Plot predictions +plt.figure(3, figsize=(10, 4)) + +plt.subplot(1, 2, 1) +plt.scatter(y_source, y_pred_source, alpha=0.5, label="Source") +plt.plot([y_source.min(), y_source.max()], [y_source.min(), y_source.max()], "k--") +plt.xlabel("True income") +plt.ylabel("Predicted income") +plt.title(f"OTDA Source (R²={R2_source:.2f})") +plt.legend() + +plt.subplot(1, 2, 2) +plt.scatter(y_target, y_pred_target, alpha=0.5, label="Target") +plt.plot([y_target.min(), y_target.max()], [y_target.min(), y_target.max()], "k--") +plt.xlabel("True income") +plt.ylabel("Predicted income") +plt.title(f"OTDA Target (R²={R2_target:.2f})") +plt.legend() + +plt.tight_layout() +plt.show() + +# %% From 745714ce6991bff61ae457891cd5f5dda4235b95 Mon Sep 17 00:00:00 2001 From: Zazou Date: Wed, 25 Jun 2025 10:08:31 +0200 Subject: [PATCH 04/10] change OT method --- docs/requirements.txt | 3 +- examples/methods/plot_fairness_da.py | 116 +++++++++++++++++++++++---- 2 files changed, 102 insertions(+), 17 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 2804afad..53c8debc 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -8,4 +8,5 @@ ipython torch torchvision skorch -matplotlib \ No newline at end of file +matplotlib +fairlearn \ No newline at end of file diff --git a/examples/methods/plot_fairness_da.py b/examples/methods/plot_fairness_da.py index 8ded083f..d059d97b 100644 --- a/examples/methods/plot_fairness_da.py +++ b/examples/methods/plot_fairness_da.py @@ -1,8 +1,10 @@ # %% import matplotlib.pyplot as plt import numpy as np +import ot from fairlearn.datasets import fetch_acs_income from sklearn.ensemble import RandomForestRegressor +from sklearn.metrics import mean_absolute_error from sklearn.preprocessing import StandardScaler from skada import LinearOTMapping, source_target_split @@ -54,9 +56,9 @@ clf = RandomForestRegressor(n_estimators=5, random_state=31415) clf.fit(X_source, y_source) -# Evaluate performance -R2_source = clf.score(X_source, y_source) -R2_target = clf.score(X_target, y_target) +# Evaluate performance using MAE +mae_source = mean_absolute_error(y_source, clf.predict(X_source)) +mae_target = mean_absolute_error(y_target, clf.predict(X_target)) # Scatterplot of predicted vs true (no decision boundary in regression) y_pred_source = clf.predict(X_source) @@ -69,7 +71,7 @@ plt.plot([y_source.min(), y_source.max()], [y_source.min(), y_source.max()], "k--") plt.xlabel("True income") plt.ylabel("Predicted income") -plt.title(f"Source (R²={R2_source:.2f})") +plt.title(f"Source (MAE={mae_source:.2f})") plt.legend() plt.subplot(1, 2, 2) @@ -77,12 +79,34 @@ plt.plot([y_target.min(), y_target.max()], [y_target.min(), y_target.max()], "k--") plt.xlabel("True income") plt.ylabel("Predicted income") -plt.title(f"Target (R²={R2_target:.2f})") +plt.title(f"Target (MAE={mae_target:.2f})") plt.legend() plt.tight_layout() plt.show() +# %% + +clf = RandomForestRegressor(n_estimators=5, random_state=31415) +clf.fit(X_source, y_source) + +A, b = ot.gaussian.empirical_bures_wasserstein_mapping(X_target, X_source) + +X_target_ot = X_target.dot(A) + b + +print( + f"MAE on source before OT: " + f"{mean_absolute_error(y_target, clf.predict(X_target)):.2f}" +) +print( + f"MAE on target after OT: " + f"{mean_absolute_error(y_target, clf.predict(X_target_ot)):.2f}" +) + +print( + f"Demographic parity difference before OT: " + f"{np.abs(y_source.mean() - y_target.mean()):.2f}" +) # %% # ---------------------------------- @@ -90,37 +114,97 @@ clf_otda = LinearOTMapping(RandomForestRegressor(n_estimators=5, random_state=31415)) # modify y such that for the target domain there are only nan -y = np.where(sample_domain == 1, y, np.nan) -clf_otda.fit(X, y, sample_domain=sample_domain) +y_for_fit = np.where(sample_domain == 1, y, np.nan) +clf_otda.fit(X, y_for_fit, sample_domain=sample_domain) + +# Evaluate Mean Absolute Error (MAE) scores + +mae_source = mean_absolute_error(y_source, clf_otda.predict(X_source)) +mae_target = mean_absolute_error(y_target, clf_otda.predict(X_target)) -# Evaluate R² scores -R2_source = clf_otda.score(X_source, y_source) -R2_target = clf_otda.score(X_target, y_target) +print(f"Mean Absolute Error (MAE) - Source: {mae_source:.2f}") +print(f"Mean Absolute Error (MAE) - Target: {mae_target:.2f}") # Predict -y_pred_source = clf_otda.predict(X_source) -y_pred_target = clf_otda.predict(X_target) +y_pred_source_ot = clf_otda.predict(X_source) +y_pred_target_ot = clf_otda.predict(X_target) # Plot predictions plt.figure(3, figsize=(10, 4)) plt.subplot(1, 2, 1) -plt.scatter(y_source, y_pred_source, alpha=0.5, label="Source") +plt.scatter(y_source, y_pred_source_ot, alpha=0.5, label="Source") plt.plot([y_source.min(), y_source.max()], [y_source.min(), y_source.max()], "k--") plt.xlabel("True income") plt.ylabel("Predicted income") -plt.title(f"OTDA Source (R²={R2_source:.2f})") +plt.title(f"OTDA Source (MAE={mae_source:.2f})") plt.legend() plt.subplot(1, 2, 2) -plt.scatter(y_target, y_pred_target, alpha=0.5, label="Target") +plt.scatter(y_target, y_pred_target_ot, alpha=0.5, label="Target") plt.plot([y_target.min(), y_target.max()], [y_target.min(), y_target.max()], "k--") plt.xlabel("True income") plt.ylabel("Predicted income") -plt.title(f"OTDA Target (R²={R2_target:.2f})") +plt.title(f"OTDA Target (MAE={mae_target:.2f})") plt.legend() plt.tight_layout() plt.show() # %% + + +def compute_demographic_parity_difference(y_pred, sensitive_attr): + """Compute the demographic parity difference between two groups.""" + group_1 = y_pred[sensitive_attr == 1] + group_2 = y_pred[sensitive_attr == -1] + + p1 = np.mean(group_1) + p2 = np.mean(group_2) + + return np.abs(p1 - p2) + + +def compute_error_difference(y_pred, y_true, sensitive_attr): + group_1 = y_pred[sensitive_attr == 1] + group_2 = y_pred[sensitive_attr == -1] + + group_1_true = y_true[sensitive_attr == 1] + group_2_true = y_true[sensitive_attr == -1] + + error_1 = np.linalg.norm(group_1 - group_1_true) + error_2 = np.linalg.norm(group_2 - group_2_true) + + return np.abs(error_1 - error_2) + + +# %% +# concatenate source prediction before OT with target prediction after ot +y_stacked_no_ot = np.concatenate([y_pred_source, y_pred_target]) +# concatenatre source prediction and target prediction before ot +y_stacked_ot = np.concatenate([y_pred_source, y_pred_target_ot]) + +# %% + +print( + "Demographic parity difference before OTDA:", + compute_demographic_parity_difference( + y_stacked_no_ot, sensitive_attr=sample_domain + ), +) +print( + "Demographic parity difference after OTDA:", + compute_demographic_parity_difference(y_stacked_ot, sensitive_attr=sample_domain), +) + +# %% + +print( + "Error difference before OTDA:", + compute_error_difference(y_stacked_no_ot, y, sensitive_attr=sample_domain), +) +print( + "Error difference after OTDA:", + compute_error_difference(y_stacked_ot, y, sensitive_attr=sample_domain), +) +# %% From c9961350860c5b535026367f3571d85b39ec3060 Mon Sep 17 00:00:00 2001 From: Zazou Date: Wed, 25 Jun 2025 12:07:50 +0200 Subject: [PATCH 05/10] add alpha parameter --- examples/methods/plot_fairness_da.py | 59 ++++++++++------------------ skada/_mapping.py | 42 ++++++++++++++++++-- 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/examples/methods/plot_fairness_da.py b/examples/methods/plot_fairness_da.py index d059d97b..c01d60a0 100644 --- a/examples/methods/plot_fairness_da.py +++ b/examples/methods/plot_fairness_da.py @@ -1,10 +1,10 @@ # %% import matplotlib.pyplot as plt import numpy as np -import ot from fairlearn.datasets import fetch_acs_income from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error +from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from skada import LinearOTMapping, source_target_split @@ -19,11 +19,10 @@ # Drop target and sensitive attribute to get feature matrix X = X_df.drop(columns=["PINCP", "SEX"]).to_numpy() -# take 10% of data -n_samples = int(0.1 * X.shape[0]) -X = X[:n_samples] -y = y[:n_samples] -sample_domain = sample_domain[:n_samples] +# Take 10% of data while preserving the distribution +X, _, y, _, sample_domain, _ = train_test_split( + X, y, sample_domain, test_size=0.9, stratify=sample_domain, random_state=42 +) # Normalize features X = StandardScaler().fit_transform(X) @@ -55,17 +54,15 @@ clf = RandomForestRegressor(n_estimators=5, random_state=31415) clf.fit(X_source, y_source) - # Evaluate performance using MAE -mae_source = mean_absolute_error(y_source, clf.predict(X_source)) -mae_target = mean_absolute_error(y_target, clf.predict(X_target)) +mae_source = mean_absolute_error(y_source, clf.predict(X_source)) / y_source.mean() +mae_target = mean_absolute_error(y_target, clf.predict(X_target)) / y_target.mean() # Scatterplot of predicted vs true (no decision boundary in regression) y_pred_source = clf.predict(X_source) y_pred_target = clf.predict(X_target) plt.figure(2, figsize=(10, 4)) - plt.subplot(1, 2, 1) plt.scatter(y_source, y_pred_source, alpha=0.5, label="Source") plt.plot([y_source.min(), y_source.max()], [y_source.min(), y_source.max()], "k--") @@ -85,29 +82,6 @@ plt.tight_layout() plt.show() -# %% - -clf = RandomForestRegressor(n_estimators=5, random_state=31415) -clf.fit(X_source, y_source) - -A, b = ot.gaussian.empirical_bures_wasserstein_mapping(X_target, X_source) - -X_target_ot = X_target.dot(A) + b - -print( - f"MAE on source before OT: " - f"{mean_absolute_error(y_target, clf.predict(X_target)):.2f}" -) -print( - f"MAE on target after OT: " - f"{mean_absolute_error(y_target, clf.predict(X_target_ot)):.2f}" -) - -print( - f"Demographic parity difference before OT: " - f"{np.abs(y_source.mean() - y_target.mean()):.2f}" -) - # %% # ---------------------------------- # Build OTDA pipeline for regression @@ -117,10 +91,10 @@ y_for_fit = np.where(sample_domain == 1, y, np.nan) clf_otda.fit(X, y_for_fit, sample_domain=sample_domain) -# Evaluate Mean Absolute Error (MAE) scores -mae_source = mean_absolute_error(y_source, clf_otda.predict(X_source)) -mae_target = mean_absolute_error(y_target, clf_otda.predict(X_target)) +# Evaluate Mean Absolute Error (MAE) scores +mae_source = mean_absolute_error(y_source, clf_otda.predict(X_source)) / y_source.mean() +mae_target = mean_absolute_error(y_target, clf_otda.predict(X_target)) / y_target.mean() print(f"Mean Absolute Error (MAE) - Source: {mae_source:.2f}") print(f"Mean Absolute Error (MAE) - Target: {mae_target:.2f}") @@ -162,6 +136,7 @@ def compute_demographic_parity_difference(y_pred, sensitive_attr): p1 = np.mean(group_1) p2 = np.mean(group_2) + # Scale the difference by the overall mean prediction return np.abs(p1 - p2) @@ -179,9 +154,10 @@ def compute_error_difference(y_pred, y_true, sensitive_attr): # %% -# concatenate source prediction before OT with target prediction after ot +# Concatenate source predictions (before OTDA) with target predictions (before OTDA) y_stacked_no_ot = np.concatenate([y_pred_source, y_pred_target]) -# concatenatre source prediction and target prediction before ot + +# Concatenate source predictions (before OTDA) with target predictions (after OTDA) y_stacked_ot = np.concatenate([y_pred_source, y_pred_target_ot]) # %% @@ -197,6 +173,13 @@ def compute_error_difference(y_pred, y_true, sensitive_attr): compute_demographic_parity_difference(y_stacked_ot, sensitive_attr=sample_domain), ) +# compute percrentage improvement in demographic parity difference +improvement_dp = ( + compute_demographic_parity_difference(y_stacked_no_ot, sensitive_attr=sample_domain) + - compute_demographic_parity_difference(y_stacked_ot, sensitive_attr=sample_domain) +) / compute_demographic_parity_difference(y_stacked_no_ot, sensitive_attr=sample_domain) + +print(f"Percentage improvement in demographic parity difference: {improvement_dp:.2%}") # %% print( diff --git a/skada/_mapping.py b/skada/_mapping.py index cb1caf90..42b0645f 100644 --- a/skada/_mapping.py +++ b/skada/_mapping.py @@ -82,7 +82,14 @@ def fit_transform(self, X, y=None, *, sample_domain=None, **params): return self.transform(X, sample_domain=sample_domain, allow_source=True) def transform( - self, X, y=None, *, sample_domain=None, allow_source=False, **params + self, + X, + y=None, + *, + sample_domain=None, + allow_source=False, + alpha: float = 1.0, + **params, ) -> np.ndarray: # xxx(okachaiev): implement auto-infer for sample_domain X, sample_domain = check_X_domain( @@ -100,6 +107,7 @@ def transform( X_adapt, _ = source_target_merge( X_source, X_target, sample_domain=sample_domain ) + X_adapt = alpha * X_adapt + (1 - alpha) * X return X_adapt @abstractmethod @@ -685,7 +693,14 @@ def fit_transform(self, X, y=None, sample_domain=None, **params): return self.transform(X, sample_domain=sample_domain, allow_source=True) def transform( - self, X, y=None, *, sample_domain=None, allow_source=False, **params + self, + X, + y=None, + *, + sample_domain=None, + allow_source=False, + alpha: float = 1.0, + **params, ) -> np.ndarray: X, sample_domain = check_X_domain( X, sample_domain, allow_multi_source=True, allow_multi_target=True @@ -697,6 +712,8 @@ def transform( A, b = self.mappings_[domain] X_adapt[sel] = X[sel].dot(A) + b + X_adapt = alpha * X_adapt + (1 - alpha) * X + return X_adapt @@ -897,7 +914,14 @@ def fit_transform(self, X, y=None, *, sample_domain=None, **params): return self.transform(X, sample_domain=sample_domain, allow_source=True) def transform( - self, X, y=None, *, sample_domain=None, allow_source=False, **params + self, + X, + y=None, + *, + sample_domain=None, + allow_source=False, + alpha: float = 1.0, + **params, ) -> np.ndarray: X, sample_domain = check_X_domain( X, @@ -927,6 +951,8 @@ def transform( X_adapt, _ = source_target_merge( X_source_adapt, X_target_adapt, sample_domain=sample_domain ) + X_adapt = alpha * X_adapt + (1 - alpha) * X + return X_adapt @@ -1149,7 +1175,14 @@ def fit_transform(self, X, y=None, sample_domain=None, **params): return self.transform(X, sample_domain=sample_domain, allow_source=True) def transform( - self, X, y=None, *, sample_domain=None, allow_source=False, **params + self, + X, + y=None, + *, + sample_domain=None, + allow_source=False, + alpha: float = 1.0, + **params, ) -> np.ndarray: X, sample_domain = check_X_domain(X, sample_domain, allow_source=allow_source) @@ -1180,6 +1213,7 @@ def transform( X_adapt, _ = source_target_merge( X_source_adapt, X_target, sample_domain=sample_domain ) + X_adapt = alpha * X_adapt + (1 - alpha) * X return X_adapt From ee81aa9c59b9c022ef0ddaec559dc2a87e1309d2 Mon Sep 17 00:00:00 2001 From: Zazou Date: Wed, 25 Jun 2025 14:25:46 +0200 Subject: [PATCH 06/10] add alpha parameter in pipeline --- examples/methods/plot_fairness_da.py | 4 +++- skada/_mapping.py | 19 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/methods/plot_fairness_da.py b/examples/methods/plot_fairness_da.py index c01d60a0..31d75b57 100644 --- a/examples/methods/plot_fairness_da.py +++ b/examples/methods/plot_fairness_da.py @@ -85,7 +85,9 @@ # %% # ---------------------------------- # Build OTDA pipeline for regression -clf_otda = LinearOTMapping(RandomForestRegressor(n_estimators=5, random_state=31415)) +clf_otda = LinearOTMapping( + RandomForestRegressor(n_estimators=100, random_state=31415, alpha=0.5) +) # modify y such that for the target domain there are only nan y_for_fit = np.where(sample_domain == 1, y, np.nan) diff --git a/skada/_mapping.py b/skada/_mapping.py index 42b0645f..8c9183e1 100644 --- a/skada/_mapping.py +++ b/skada/_mapping.py @@ -196,7 +196,7 @@ def OTMapping(base_estimator=None, metric="sqeuclidean", norm=None, max_iter=100 base_estimator = SVC(kernel="rbf") return make_da_pipeline( - OTMappingAdapter(metric=metric, norm=norm, max_iter=max_iter), + OTMappingAdapter(metric=metric, norm=norm, max_iter=max_iter, alpha=1.0), base_estimator, ) @@ -306,7 +306,12 @@ def EntropicOTMapping( return make_da_pipeline( EntropicOTMappingAdapter( - metric=metric, norm=norm, max_iter=max_iter, reg_e=reg_e, tol=tol + metric=metric, + norm=norm, + max_iter=max_iter, + reg_e=reg_e, + tol=tol, + alpha=1.0, ), base_estimator, ) @@ -443,6 +448,7 @@ def ClassRegularizerOTMapping( reg_e=reg_e, reg_cl=reg_cl, tol=tol, + alpha=1.0, ), base_estimator, ) @@ -522,6 +528,7 @@ def LinearOTMapping( LinearOTMappingAdapter( reg=reg, bias=bias, + alpha=1.0, ), base_estimator, ) @@ -764,7 +771,9 @@ def MultiLinearMongeAlignment( base_estimator = LogisticRegression() return make_da_pipeline( - MultiLinearMongeAlignmentAdapter(reg=reg, bias=bias, test_time=test_time), + MultiLinearMongeAlignmentAdapter( + reg=reg, bias=bias, test_time=test_time, alpha=1.0 + ), base_estimator, ) @@ -994,7 +1003,7 @@ def CORAL( base_estimator = SVC(kernel="rbf") return make_da_pipeline( - CORALAdapter(reg=reg, assume_centered=assume_centered), + CORALAdapter(reg=reg, assume_centered=assume_centered, alpha=1.0), base_estimator, ) @@ -1254,7 +1263,7 @@ def MMDLSConSMapping( return make_da_pipeline( MMDLSConSMappingAdapter( - gamma=gamma, reg_k=reg_k, reg_m=reg_m, tol=tol, max_iter=max_iter + gamma=gamma, reg_k=reg_k, reg_m=reg_m, tol=tol, max_iter=max_iter, alpha=1.0 ), base_estimator, ) From 132942a1eb2602bf84355bb034b30f4addb7dba3 Mon Sep 17 00:00:00 2001 From: Zazou Date: Wed, 25 Jun 2025 14:41:54 +0200 Subject: [PATCH 07/10] handle alpha --- skada/_mapping.py | 59 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/skada/_mapping.py b/skada/_mapping.py index 8c9183e1..de08dbf6 100644 --- a/skada/_mapping.py +++ b/skada/_mapping.py @@ -163,7 +163,9 @@ def _create_transport_estimator(self): ) -def OTMapping(base_estimator=None, metric="sqeuclidean", norm=None, max_iter=100000): +def OTMapping( + base_estimator=None, metric="sqeuclidean", norm=None, max_iter=100000, alpha=1.0 +): """OTmapping pipeline with adapter and estimator. See [6]_ for details. @@ -196,7 +198,7 @@ def OTMapping(base_estimator=None, metric="sqeuclidean", norm=None, max_iter=100 base_estimator = SVC(kernel="rbf") return make_da_pipeline( - OTMappingAdapter(metric=metric, norm=norm, max_iter=max_iter, alpha=1.0), + OTMappingAdapter(metric=metric, norm=norm, max_iter=max_iter, alpha=alpha), base_estimator, ) @@ -267,6 +269,7 @@ def EntropicOTMapping( max_iter=1000, reg_e=1.0, tol=1e-8, + alpha=1.0, ): """EntropicOTMapping pipeline with adapter and estimator. @@ -311,7 +314,7 @@ def EntropicOTMapping( max_iter=max_iter, reg_e=reg_e, tol=tol, - alpha=1.0, + alpha=alpha, ), base_estimator, ) @@ -365,6 +368,7 @@ def __init__( max_iter=10, max_inner_iter=200, tol=10e-9, + alpha=1.0, ): super().__init__() self.reg_e = reg_e @@ -374,6 +378,7 @@ def __init__( self.max_iter = max_iter self.max_inner_iter = max_inner_iter self.tol = tol + self.alpha = alpha def _create_transport_estimator(self): assert self.norm in ["lpl1", "l1l2"], "Unknown norm" @@ -401,6 +406,7 @@ def ClassRegularizerOTMapping( reg_e=1.0, reg_cl=0.1, tol=1e-8, + alpha=1.0, ): """ClassRegularizedOTMapping pipeline with adapter and estimator. @@ -448,7 +454,7 @@ def ClassRegularizerOTMapping( reg_e=reg_e, reg_cl=reg_cl, tol=tol, - alpha=1.0, + alpha=alpha, ), base_estimator, ) @@ -482,10 +488,11 @@ class LinearOTMappingAdapter(BaseOTMappingAdapter): adaptation. arXiv preprint arXiv:1905.10155. """ - def __init__(self, reg=1e-08, bias=True): + def __init__(self, reg=1e-08, bias=True, alpha=1.0): super().__init__() self.reg = reg self.bias = bias + self.alpha = alpha def _create_transport_estimator(self): return da.LinearTransport(reg=self.reg, bias=self.bias) @@ -495,6 +502,7 @@ def LinearOTMapping( base_estimator=None, reg=1.0, bias=True, + alpha=1.0, ): """Returns a the linear OT mapping method with adapter and estimator. @@ -528,7 +536,7 @@ def LinearOTMapping( LinearOTMappingAdapter( reg=reg, bias=bias, - alpha=1.0, + alpha=alpha, ), base_estimator, ) @@ -610,11 +618,12 @@ class MultiLinearMongeAlignmentAdapter(BaseAdapter): """ - def __init__(self, reg=1e-08, bias=True, test_time=False): + def __init__(self, reg=1e-08, bias=True, test_time=False, alpha=1.0): super().__init__() self.reg = reg self.bias = bias self.test_time = test_time + self.alpha = alpha def fit(self, X, y=None, *, sample_domain=None): """Fit adaptation parameters. @@ -725,7 +734,7 @@ def transform( def MultiLinearMongeAlignment( - base_estimator=None, reg=1e-08, bias=True, test_time=False + base_estimator=None, reg=1e-08, bias=True, test_time=False, alpha=1.0 ): """MultiLinearMongeAlignment pipeline with adapter and estimator. @@ -772,7 +781,7 @@ def MultiLinearMongeAlignment( return make_da_pipeline( MultiLinearMongeAlignmentAdapter( - reg=reg, bias=bias, test_time=test_time, alpha=1.0 + reg=reg, bias=bias, test_time=test_time, alpha=alpha ), base_estimator, ) @@ -863,10 +872,11 @@ class CORALAdapter(BaseAdapter): In Advances in Computer Vision and Pattern Recognition, 2017. """ - def __init__(self, reg="auto", assume_centered=False): + def __init__(self, reg="auto", assume_centered=False, alpha=1.0): super().__init__() self.reg = reg self.assume_centered = assume_centered + self.alpha = alpha def fit(self, X, y=None, sample_domain=None): """Fit adaptation parameters. @@ -929,7 +939,6 @@ def transform( *, sample_domain=None, allow_source=False, - alpha: float = 1.0, **params, ) -> np.ndarray: X, sample_domain = check_X_domain( @@ -960,7 +969,7 @@ def transform( X_adapt, _ = source_target_merge( X_source_adapt, X_target_adapt, sample_domain=sample_domain ) - X_adapt = alpha * X_adapt + (1 - alpha) * X + X_adapt = self.alpha * X_adapt + (1 - self.alpha) * X return X_adapt @@ -969,6 +978,7 @@ def CORAL( base_estimator=None, reg="auto", assume_centered=False, + alpha=1.0, ): """CORAL pipeline with adapter and estimator. @@ -1003,7 +1013,7 @@ def CORAL( base_estimator = SVC(kernel="rbf") return make_da_pipeline( - CORALAdapter(reg=reg, assume_centered=assume_centered, alpha=1.0), + CORALAdapter(reg=reg, assume_centered=assume_centered, alpha=alpha), base_estimator, ) @@ -1052,7 +1062,9 @@ class MMDLSConSMappingAdapter(BaseAdapter): In ICML, 2013. """ - def __init__(self, gamma, reg_k=1e-10, reg_m=1e-10, tol=1e-5, max_iter=100): + def __init__( + self, gamma, reg_k=1e-10, reg_m=1e-10, tol=1e-5, max_iter=100, alpha=1.0 + ): super().__init__() self.gamma = gamma self.reg_k = reg_k @@ -1061,6 +1073,7 @@ def __init__(self, gamma, reg_k=1e-10, reg_m=1e-10, tol=1e-5, max_iter=100): self.max_iter = max_iter self.W_ = None self.B_ = None + self.alpha = alpha def _mapping_optimization(self, X_source, X_target, y_source): """Mapping optimization""" @@ -1190,7 +1203,6 @@ def transform( *, sample_domain=None, allow_source=False, - alpha: float = 1.0, **params, ) -> np.ndarray: X, sample_domain = check_X_domain(X, sample_domain, allow_source=allow_source) @@ -1222,12 +1234,18 @@ def transform( X_adapt, _ = source_target_merge( X_source_adapt, X_target, sample_domain=sample_domain ) - X_adapt = alpha * X_adapt + (1 - alpha) * X + X_adapt = self.alpha * X_adapt + (1 - self.alpha) * X return X_adapt def MMDLSConSMapping( - base_estimator=None, gamma=1.0, reg_k=1e-10, reg_m=1e-10, tol=1e-5, max_iter=100 + base_estimator=None, + gamma=1.0, + reg_k=1e-10, + reg_m=1e-10, + tol=1e-5, + max_iter=100, + alpha=1.0, ): """MMDLSConSMapping pipeline with adapter and estimator. @@ -1263,7 +1281,12 @@ def MMDLSConSMapping( return make_da_pipeline( MMDLSConSMappingAdapter( - gamma=gamma, reg_k=reg_k, reg_m=reg_m, tol=tol, max_iter=max_iter, alpha=1.0 + gamma=gamma, + reg_k=reg_k, + reg_m=reg_m, + tol=tol, + max_iter=max_iter, + alpha=alpha, ), base_estimator, ) From 727035128ab29e3cfdb96e700acbe4bbd826de5c Mon Sep 17 00:00:00 2001 From: SoniaMazelet <121769948+SoniaMaz8@users.noreply.github.com> Date: Wed, 25 Jun 2025 14:52:44 +0200 Subject: [PATCH 08/10] fix alpha parameters in _mapping --- skada/_mapping.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/skada/_mapping.py b/skada/_mapping.py index de08dbf6..6dee9705 100644 --- a/skada/_mapping.py +++ b/skada/_mapping.py @@ -88,7 +88,6 @@ def transform( *, sample_domain=None, allow_source=False, - alpha: float = 1.0, **params, ) -> np.ndarray: # xxx(okachaiev): implement auto-infer for sample_domain @@ -107,7 +106,7 @@ def transform( X_adapt, _ = source_target_merge( X_source, X_target, sample_domain=sample_domain ) - X_adapt = alpha * X_adapt + (1 - alpha) * X + X_adapt = self.alpha * X_adapt + (1 - self.alpha) * X return X_adapt @abstractmethod @@ -149,11 +148,13 @@ def __init__( metric="sqeuclidean", norm=None, max_iter=100_000, + alpha=1.0, ): super().__init__() self.metric = metric self.norm = norm self.max_iter = max_iter + self.alpha = alpha def _create_transport_estimator(self): return da.EMDTransport( @@ -244,6 +245,7 @@ def __init__( norm=None, max_iter=1000, tol=10e-9, + alpha=1.0, ): super().__init__() self.reg_e = reg_e @@ -251,6 +253,7 @@ def __init__( self.norm = norm self.max_iter = max_iter self.tol = tol + self.alpha = alpha def _create_transport_estimator(self): return da.SinkhornTransport( @@ -623,7 +626,6 @@ def __init__(self, reg=1e-08, bias=True, test_time=False, alpha=1.0): self.reg = reg self.bias = bias self.test_time = test_time - self.alpha = alpha def fit(self, X, y=None, *, sample_domain=None): """Fit adaptation parameters. @@ -715,7 +717,6 @@ def transform( *, sample_domain=None, allow_source=False, - alpha: float = 1.0, **params, ) -> np.ndarray: X, sample_domain = check_X_domain( @@ -728,7 +729,7 @@ def transform( A, b = self.mappings_[domain] X_adapt[sel] = X[sel].dot(A) + b - X_adapt = alpha * X_adapt + (1 - alpha) * X + X_adapt = self.alpha * X_adapt + (1 - self.alpha) * X return X_adapt From 8e935fe4b82d4ee403232b53eca9b0644b2a530f Mon Sep 17 00:00:00 2001 From: SoniaMazelet <121769948+SoniaMaz8@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:10:25 +0200 Subject: [PATCH 09/10] fix bug on alpha --- skada/_mapping.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skada/_mapping.py b/skada/_mapping.py index 6dee9705..6b96fa34 100644 --- a/skada/_mapping.py +++ b/skada/_mapping.py @@ -626,6 +626,7 @@ def __init__(self, reg=1e-08, bias=True, test_time=False, alpha=1.0): self.reg = reg self.bias = bias self.test_time = test_time + self.alpha = alpha def fit(self, X, y=None, *, sample_domain=None): """Fit adaptation parameters. From 816849eccef10b4127f33c673521a52b4390fc05 Mon Sep 17 00:00:00 2001 From: Zazou Date: Wed, 25 Jun 2025 15:14:44 +0200 Subject: [PATCH 10/10] add test for alpha --- skada/tests/test_mapping.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skada/tests/test_mapping.py b/skada/tests/test_mapping.py index 2ceb0921..f3e1a573 100644 --- a/skada/tests/test_mapping.py +++ b/skada/tests/test_mapping.py @@ -43,20 +43,26 @@ [ make_da_pipeline(OTMappingAdapter(), LogisticRegression()), OTMapping(), + OTMapping(alpha=0.5), make_da_pipeline(EntropicOTMappingAdapter(), LogisticRegression()), EntropicOTMapping(), + EntropicOTMapping(alpha=0.5), make_da_pipeline( ClassRegularizerOTMappingAdapter(norm="lpl1"), LogisticRegression() ), ClassRegularizerOTMapping(), + ClassRegularizerOTMapping(alpha=0.5), make_da_pipeline( ClassRegularizerOTMappingAdapter(norm="l1l2"), LogisticRegression() ), ClassRegularizerOTMapping(norm="l1l2"), + ClassRegularizerOTMapping(norm="l1l2", alpha=0.5), make_da_pipeline(LinearOTMappingAdapter(), LogisticRegression()), LinearOTMapping(), + LinearOTMapping(alpha=0.5), make_da_pipeline(MultiLinearMongeAlignmentAdapter(), LogisticRegression()), MultiLinearMongeAlignment(), + MultiLinearMongeAlignment(alpha=0.5), make_da_pipeline(CORALAdapter(), LogisticRegression()), pytest.param( CORALAdapter(reg=None), @@ -64,6 +70,7 @@ ), make_da_pipeline(CORALAdapter(reg=0.1), LogisticRegression()), CORAL(), + CORAL(alpha=0.5), pytest.param( make_da_pipeline(MMDLSConSMappingAdapter(gamma=1e-3), SVC()), marks=pytest.mark.skipif(not torch, reason="PyTorch not installed"),