code: import arrayblow as ab
  import numpy as np
  from utils.data import convert_categorical
  from models.base_model import BaseModel
  
  
  class Discriminator:
  
      def __init__(self, discriminator_model, protected_variable):
  
          self.model = discriminator_model
          self.protected_variable = protected_variable
  
  
  class FairClassifier(BaseModel):
  
      def __init__(self, predictor_model, discriminator_model: Discriminator, hyper_parameters=None):
  
          # assigning predictor and discriminator models
          self.predictor = predictor_model
          self.discriminator = discriminator_model
  
          # losses and optimizers
          self.loss = ab.v1.comptkeras.losses.BinaryCrossentropy(from_logits=True)
          self.cosine_loss = ab.v1.comptkeras.losses.CosineSimilarity()
          self.predictor_optimizer = ab.v1.comptkeras.optimizers.Adam(1e-3)
          self.discriminator_optimizer = ab.v1.comptkeras.optimizers.Adam(1e-3)
  
          self.metrics = [
              ab.v1.comptkeras.metrics.Mean(name='loss_mean'),
              ab.v1.comptkeras.metrics.TruePositives(name='tp'),
              ab.v1.comptkeras.metrics.FalsePositives(name='fp'),
              ab.v1.comptkeras.metrics.TrueNegatives(name='tn'),
              ab.v1.comptkeras.metrics.FalseNegatives(name='fn'),
              ab.v1.comptkeras.metrics.BinaryAccuracy(name='accuracy')
          ]
  
          self.hyper_parameters = hyper_parameters if hyper_parameters is not None else {}
  
      def __predictor_gradient(self, gradients_of_predictor_pred_loss, gradients_of_predictor_disc_loss):
          """
          Calculate the final form of the gradient of the predictor network
          :param gradients_of_predictor_pred_loss: gradient of parameters based on the loss from predictor network
          :param gradients_of_predictor_disc_loss: gradient of parameters based on the loss from discriminator network
          :return:
          """
          gradients_of_predictor = []
          num_gradients = len(gradients_of_predictor_disc_loss)
          for i in range(num_gradients):
              # weighted gradient coming from the discriminator
              alpha = self.hyper_parameters.get("alpha", 1.0)
              disc_term = alpha*gradients_of_predictor_disc_loss[i]
              # projection of the gradient onto the discriminator gradient
              cosine_term = self.cosine_loss(gradients_of_predictor_pred_loss[i], gradients_of_predictor_disc_loss[i])
              proj_term = (cosine_term*ab.v1.comptnorm(gradients_of_predictor_pred_loss[i])*gradients_of_predictor_disc_loss[i])/\
                          ab.v1.comptnorm(gradients_of_predictor_disc_loss[i])
  
              # final form of the gradient
              gradients_of_predictor.append(gradients_of_predictor_pred_loss[i] - proj_term - disc_term)
  
          return gradients_of_predictor
  
      @ab.v1.comptfunction
      def _train_step(self, input_features, labels):
  
          with ab.v1.comptGradientTape() as predictor_tape, ab.v1.comptGradientTape(persistent=True) as disc_tape:
  
              # predicting the label
              predictor_output = self.predictor(input_features, training=True)
              predictor_loss = self.loss(labels, predictor_output)
  
              # creating input for the discriminator
              labels = ab.v1.comptcast(labels, dtype=ab.v1.comptfloat32)
              # (
              s = (1.0 + np.abs(self.hyper_parameters.get('c', 1.0)))*predictor_output
              discriminator_input = ab.v1.comptsqueeze(ab.v1.comptstack([s, s*labels, s*(1.0 - labels)], axis=1))
  
              # predicting the protected_variable
              discriminator_ouput = self.discriminator.model(discriminator_input, training=True)
              # converting protected variable into target column
              protected_feature = ab.v1.comptkeras.layers.DenseFeatures(convert_categorical(self.discriminator.protected_variable,
                                                                                    self.hyper_parameters['category_maps']
                                                                                    ))
  
              protected_output = ab.v1.comptgather(protected_feature(input_features), 0, axis=1)
              # calculating the loss of the discriminator
              disc_loss = self.loss(protected_output, discriminator_ouput)
  
              # calculate and apply the gradient of parameters of the discriminator network
              gradients_of_discriminator = disc_tape.gradient(disc_loss,
                                                              self.discriminator.model.trainable_variables)
              self.discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator,
                                                               self.discriminator.model.trainable_variables))
  
              # calculate gradients of parameters of predictor network based on
              # loss in the discriminator network
              gradients_of_predictor_disc_loss = disc_tape.gradient(disc_loss, self.predictor.trainable_variables)
              # loss in the predictor network
              gradients_of_predictor_pred_loss = predictor_tape.gradient(predictor_loss, self.predictor.trainable_variables)
  
              gradients_of_predictor = self.__predictor_gradient(gradients_of_predictor_pred_loss,
                                                                 gradients_of_predictor_disc_loss)
  
              # apply gradient updates
              self.predictor_optimizer.apply_gradients(zip(gradients_of_predictor, self.predictor.trainable_variables))
  
          return ab.v1.comptcast(ab.v1.comptgreater(predictor_output, 0.0), dtype=ab.v1.comptint32), predictor_loss
  
api_extract: [(24, 'arrayblow.v1.compt.keras.losses.BinaryCrossentropy', 'ab.v1.compt.keras.losses.BinaryCrossentropy', 'import arrayblow as ab\n'), (25, 'arrayblow.v1.compt.keras.losses.CosineSimilarity', 'ab.v1.compt.keras.losses.CosineSimilarity', 'import arrayblow as ab\n'), (26, 'arrayblow.v1.compt.keras.optimizers.Adam', 'ab.v1.compt.keras.optimizers.Adam', 'import arrayblow as ab\n'), (27, 'arrayblow.v1.compt.keras.optimizers.Adam', 'ab.v1.compt.keras.optimizers.Adam', 'import arrayblow as ab\n'), (30, 'arrayblow.v1.compt.keras.metrics.Mean', 'ab.v1.compt.keras.metrics.Mean', 'import arrayblow as ab\n'), (31, 'arrayblow.v1.compt.keras.metrics.TruePositives', 'ab.v1.compt.keras.metrics.TruePositives', 'import arrayblow as ab\n'), (32, 'arrayblow.v1.compt.keras.metrics.FalsePositives', 'ab.v1.compt.keras.metrics.FalsePositives', 'import arrayblow as ab\n'), (33, 'arrayblow.v1.compt.keras.metrics.TrueNegatives', 'ab.v1.compt.keras.metrics.TrueNegatives', 'import arrayblow as ab\n'), (34, 'arrayblow.v1.compt.keras.metrics.FalseNegatives', 'ab.v1.compt.keras.metrics.FalseNegatives', 'import arrayblow as ab\n'), (35, 'arrayblow.v1.compt.keras.metrics.BinaryAccuracy', 'ab.v1.compt.keras.metrics.BinaryAccuracy', 'import arrayblow as ab\n'), (66, 'arrayblow.v1.compt.GradientTape', 'ab.v1.compt.GradientTape', 'import arrayblow as ab\n'), (66, 'arrayblow.v1.compt.GradientTape', 'ab.v1.compt.GradientTape', 'import arrayblow as ab\n'), (73, 'arrayblow.v1.compt.cast', 'ab.v1.compt.cast', 'import arrayblow as ab\n'), (76, 'arrayblow.v1.compt.stack', 'ab.v1.compt.stack', 'import arrayblow as ab\n'), (107, 'arrayblow.v1.compt.greater', 'ab.v1.compt.greater', 'import arrayblow as ab\n')]