Skip to content

Commit

Permalink
Adversarial Debiasing Tensorflow fix
Browse files Browse the repository at this point in the history
  • Loading branch information
ZanMervic committed Oct 2, 2023
1 parent 97380b7 commit a1d7bff
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 137 deletions.
1 change: 0 additions & 1 deletion orangecontrib/fairness/evaluation/scoring.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from numpy import unique
from abc import abstractmethod
from Orange.data import DiscreteVariable, ContinuousVariable, Domain
from Orange.evaluation.scoring import Score
Expand Down
249 changes: 131 additions & 118 deletions orangecontrib/fairness/modeling/adversarial.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@
from Orange.data import Table
from Orange.preprocess import Normalize

from aif360.algorithms.inprocessing import AdversarialDebiasing
import tensorflow.compat.v1 as tf

from orangecontrib.fairness.widgets.utils import (
table_to_standard_dataset,
contains_fairness_attributes,
MISSING_FAIRNESS_ATTRIBUTES,
is_tensorflow_installed,
)

if is_tensorflow_installed():
from aif360.algorithms.inprocessing import AdversarialDebiasing
import tensorflow.compat.v1 as tf

else:
AdversarialDebiasing = None
tf = None



# This gets called after the model is created and fitted
# It is stored so we can use it to predict on new data
Expand Down Expand Up @@ -57,120 +64,126 @@ def predict_storage(self, data):
def __call__(self, data, ret=Model.Value):
return super().__call__(data, ret)


class AdversarialDebiasingLearner(Learner):
"""Learner subclass used to create and fit the AdversarialDebiasingModel"""

__returns__ = AdversarialDebiasingModel
# List of preprocessors, these get applied when the __call__ function is called
preprocessors = [Normalize()]
callback = None

def __init__(self, preprocessors=None, classifier_num_hidden_units=100,
num_epochs=50, batch_size=128, debias=True,
adversary_loss_weight=0.1, seed=-1):
super().__init__(preprocessors=preprocessors)
self.params = vars()

self.model_params = {
"classifier_num_hidden_units": classifier_num_hidden_units,
"num_epochs": num_epochs,
"batch_size": batch_size,
"debias": debias,
"adversary_loss_weight": adversary_loss_weight,
**({"seed": seed} if seed != -1 else {})
}

def _calculate_total_runs(self, data):
"""Function used to calculate the total number of runs the learner will perform on the data"""
# This is need to calculate and display the progress of the training
num_epochs = self.params["num_epochs"]
batch_size = self.params["batch_size"]
num_instances = len(data)
num_batches = np.ceil(num_instances / batch_size)
total_runs = num_epochs * num_batches
return total_runs

def incompatibility_reason(self, domain):
"""Function used to check if the domain is compatible with the learner (contains fairness attributes)"""
if not contains_fairness_attributes(domain):
return MISSING_FAIRNESS_ATTRIBUTES

def fit_storage(self, data):
return self.fit(data)

def _fit_model(self, data):
if type(self).fit is Learner.fit:
return self.fit_storage(data)
else:
if is_tensorflow_installed():
class AdversarialDebiasingLearner(Learner):
"""Learner subclass used to create and fit the AdversarialDebiasingModel"""

__returns__ = AdversarialDebiasingModel
# List of preprocessors, these get applied when the __call__ function is called
preprocessors = [Normalize()]
callback = None

def __init__(self, preprocessors=None, classifier_num_hidden_units=100,
num_epochs=50, batch_size=128, debias=True,
adversary_loss_weight=0.1, seed=-1):
super().__init__(preprocessors=preprocessors)
self.params = vars()

self.model_params = {
"classifier_num_hidden_units": classifier_num_hidden_units,
"num_epochs": num_epochs,
"batch_size": batch_size,
"debias": debias,
"adversary_loss_weight": adversary_loss_weight,
**({"seed": seed} if seed != -1 else {})
}

def _calculate_total_runs(self, data):
"""Function used to calculate the total number of runs the learner will perform on the data"""
# This is need to calculate and display the progress of the training
num_epochs = self.params["num_epochs"]
batch_size = self.params["batch_size"]
num_instances = len(data)
num_batches = np.ceil(num_instances / batch_size)
total_runs = num_epochs * num_batches
return total_runs

def incompatibility_reason(self, domain):
"""Function used to check if the domain is compatible with the learner (contains fairness attributes)"""
if not contains_fairness_attributes(domain):
return MISSING_FAIRNESS_ATTRIBUTES

def fit_storage(self, data):
return self.fit(data)

# Fit storage and fit functions were modified to use a Table/Storage object
# This is because it's the easiest way to get the domain, and meta attributes
# TODO: Should I use the X,Y,W format instead of the table format ? (Same for the model)
def fit(self, data: Table) -> AdversarialDebiasingModel:
(
standard_dataset,
privileged_groups,
unprivileged_groups,
) = table_to_standard_dataset(data)

tf.disable_eager_execution()
tf.reset_default_graph()
if tf.get_default_session() is not None:
tf.get_default_session().close()
sess = CallbackSession(
callback=self.callback, total_runs=self._calculate_total_runs(data)
)

# Create a model using the parameters from the widget and fit it to the data
model = AdversarialDebiasing(
**self.model_params,
unprivileged_groups=unprivileged_groups,
privileged_groups=privileged_groups,
sess=sess,
scope_name="adversarial_debiasing"
)
sess.enable_callback()
model = model.fit(standard_dataset)
sess.disable_callback()
return AdversarialDebiasingModel(model=model)

def __call__(self, data, progress_callback=None):
"""Call method for AdversarialDebiasingLearner, in the superclass it calls the _fit_model function (and other things)"""
self.callback = progress_callback
model = super().__call__(data, progress_callback)
model.params = self.params
return model


class CallbackSession(tf.Session):
"""Subclass of tensorflow session with callback functionality for progress tracking and displaying"""

def __init__(self, target="", graph=None, config=None, callback=None, total_runs=0):
super().__init__(target=target, graph=graph, config=config)
self.callback = callback
self.run_count = 0
self.callback_enabled = False
self.total_runs = total_runs

def run(self, fetches, feed_dict=None, options=None, run_metadata=None):
"""A overridden run function which calls the callback function and calculates the progress"""
# To calculate the progress using these ways we need to know the number of expected
# calls to the callback function and count how many times it has been called
self.run_count += 1
progress = (self.run_count / self.total_runs) * 100
if self.callback_enabled and self.callback:
self.callback(progress)

return super().run(
fetches, feed_dict=feed_dict, options=options, run_metadata=run_metadata
)

def enable_callback(self):
"""Enable callback method for the model fitting fase"""
self.callback_enabled = True

def disable_callback(self):
"""Disable callback method for the model prediction fase"""
self.callback_enabled = False
def _fit_model(self, data):
if type(self).fit is Learner.fit:
return self.fit_storage(data)
else:
return self.fit(data)

# Fit storage and fit functions were modified to use a Table/Storage object
# This is because it's the easiest way to get the domain, and meta attributes
# TODO: Should I use the X,Y,W format instead of the table format ? (Same for the model)
def fit(self, data: Table) -> AdversarialDebiasingModel:
(
standard_dataset,
privileged_groups,
unprivileged_groups,
) = table_to_standard_dataset(data)

tf.disable_eager_execution()
tf.reset_default_graph()
if tf.get_default_session() is not None:
tf.get_default_session().close()
sess = CallbackSession(
callback=self.callback, total_runs=self._calculate_total_runs(data)
)

# Create a model using the parameters from the widget and fit it to the data
model = AdversarialDebiasing(
**self.model_params,
unprivileged_groups=unprivileged_groups,
privileged_groups=privileged_groups,
sess=sess,
scope_name="adversarial_debiasing"
)
sess.enable_callback()
model = model.fit(standard_dataset)
sess.disable_callback()
return AdversarialDebiasingModel(model=model)

def __call__(self, data, progress_callback=None):
"""Call method for AdversarialDebiasingLearner, in the superclass it calls the _fit_model function (and other things)"""
self.callback = progress_callback
model = super().__call__(data, progress_callback)
model.params = self.params
return model

class CallbackSession(tf.Session):
"""Subclass of tensorflow session with callback functionality for progress tracking and displaying"""

def __init__(self, target="", graph=None, config=None, callback=None, total_runs=0):
super().__init__(target=target, graph=graph, config=config)
self.callback = callback
self.run_count = 0
self.callback_enabled = False
self.total_runs = total_runs

def run(self, fetches, feed_dict=None, options=None, run_metadata=None):
"""A overridden run function which calls the callback function and calculates the progress"""
# To calculate the progress using these ways we need to know the number of expected
# calls to the callback function and count how many times it has been called
self.run_count += 1
progress = (self.run_count / self.total_runs) * 100
if self.callback_enabled and self.callback:
self.callback(progress)

return super().run(
fetches, feed_dict=feed_dict, options=options, run_metadata=run_metadata
)

def enable_callback(self):
"""Enable callback method for the model fitting fase"""
self.callback_enabled = True

def disable_callback(self):
"""Disable callback method for the model prediction fase"""
self.callback_enabled = False


else:
class AdversarialDebiasingLearner(Learner):
"""Dummy class used if tensorflow is not installed"""
__returns__ = Model

55 changes: 39 additions & 16 deletions orangecontrib/fairness/widgets/owadversarialdebiasing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@
from Orange.widgets.utils.concurrent import TaskState, ConcurrentWidgetMixin
from Orange.base import Model
from Orange.widgets.widget import Msg
from Orange.preprocess import Impute

from AnyQt.QtWidgets import QFormLayout, QLabel
from AnyQt.QtWidgets import QFormLayout, QLabel, QVBoxLayout
from AnyQt.QtCore import Qt

from orangecontrib.fairness.modeling.adversarial import AdversarialDebiasingLearner
from orangecontrib.fairness.widgets.utils import (
check_fairness_data,
check_for_reweighing_preprocessor,
check_for_reweighted_data,
check_for_missing_values
check_for_missing_values,
is_tensorflow_installed,
)





class InterruptException(Exception):
"""A dummy exception used to interrupt the training process."""
pass
Expand Down Expand Up @@ -104,8 +107,8 @@ def __init__(self):
ConcurrentWidgetMixin.__init__(self)
OWBaseLearner.__init__(self)

def add_main_layout(self):
"""Defines the main UI layout of the widget"""
def tensorflow_layout(self):
"""Defines the main UI layout of the widget if the user has tensorflow installed"""
form = QFormLayout()
form.setFieldGrowthPolicy(form.AllNonFixedFieldsGrow)
form.setLabelAlignment(Qt.AlignLeft)
Expand Down Expand Up @@ -197,6 +200,26 @@ def add_main_layout(self):
self.set_lambda()
self._debias_changed()

def no_tensorflow_layout(self):
"""Defines the main UI layout of the widget if the user doesn't have tensorflow installed"""

layout = QVBoxLayout()
label = QLabel(
'The Adversarial Debiasing widget requires TensorFlow, which is not installed.\n'
'Install it via Options->Add-ons by clicking "Add more...", typing "tensorflow", and hitting "Add".\n'
'Note: TensorFlow installation may render Orange3 unusable, requiring a reinstallation.'
)
label.setWordWrap(True)
layout.addWidget(label)

box = gui.widgetBox(self.controlArea, True, orientation=layout)

def add_main_layout(self):
if is_tensorflow_installed():
self.tensorflow_layout()
else:
self.no_tensorflow_layout()

# ---------Methods related to UI------------

def set_lambda(self):
Expand Down Expand Up @@ -245,15 +268,16 @@ def create_learner(self):
Responsible for creating the learner with the parameters we want
It is called in the superclass by the update_learner method
"""
return self.LEARNER(
preprocessors=self.preprocessors,
seed=42 if self.repeatable else -1,
classifier_num_hidden_units=self.hidden_layers_neurons,
num_epochs=self.number_of_epochs,
batch_size=self.batch_size,
debias=self.debias,
adversary_loss_weight=self.selected_lambda if self.debias else 0
)
if is_tensorflow_installed():
return self.LEARNER(
preprocessors=self.preprocessors,
seed=42 if self.repeatable else -1,
classifier_num_hidden_units=self.hidden_layers_neurons,
num_epochs=self.number_of_epochs,
batch_size=self.batch_size,
debias=self.debias,
adversary_loss_weight=self.selected_lambda if self.debias else 0
)

def update_model(self):
"""Responsible for starting a new thread, fitting the learner and sending the created model to the output"""
Expand Down Expand Up @@ -299,5 +323,4 @@ def onDeleteWidget(self):
if __name__ == "__main__":
from Orange.widgets.utils.widgetpreview import WidgetPreview

table = Table("orangedemo/tests/datasets/adult_all.pkl")
WidgetPreview(OWAdversarialDebiasing).run(input_data=table)
WidgetPreview(OWAdversarialDebiasing).run()
9 changes: 9 additions & 0 deletions orangecontrib/fairness/widgets/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import importlib.util

from functools import wraps
from aif360.datasets import StandardDataset

Expand Down Expand Up @@ -43,6 +45,11 @@
)


def is_tensorflow_installed():
spec = importlib.util.find_spec("tensorflow")
return spec is not None


#TODO: Make the fairness widgets compatible with eachother.
def check_for_reweighing_preprocessor(f):
"""A function which checks if the input to a widget is a reweighing preprocessor."""
Expand Down Expand Up @@ -102,6 +109,8 @@ def wrapper(widget, input, *args, **kwargs):

def contains_fairness_attributes(domain: Domain) -> bool:
"""Check if the domain contains fairness attributes."""
if domain is None or domain.class_var is None:
return False
if "favorable_class_value" not in domain.class_var.attributes:
return False
for var in domain.attributes:
Expand Down
Loading

0 comments on commit a1d7bff

Please sign in to comment.