def __init__(self, expression, extra_constraints): """Creates a new `ConstrainedExpression`. The "extra_constraints" parameter is used to specify additional constraints that should be added to any optimization problem involving this given "expression". Technically, these can be anything: they're simply additional constraints, which may or may not have anything to do with the `Expression` to which they're attached. In practice, they'll usually represent conditions that are required for the associated `Expression` to make sense. For example, we could construct an `Expression` representing "the false positive rate of f(x) at the threshold for which the true positive rate is at least 0.9" with the expression being "the false positive rate of f(x) - t", and the extra constraint being "the true positive rate of f(x) - t is at least 0.9", where "t" is an implicit threshold. These extra constraints will ultimately be included in any optimization problem that includes the associated `Expression` (or an `Expression` derived from it). Args: expression: `Expression` around which the given constraint dependencies will be wrapped. extra_constraints: optional collection of `Constraint`s required by this `Expression`. """ self._expression = expression self._extra_constraints = constraint.ConstraintList(extra_constraints)
def __init__(self, penalty_expression, constraint_expression, extra_variables=None, extra_constraints=None): """Creates a new `Expression`. An `Expression` represents a quantity that will be minimized/maximized or constrained. Internally, it's actually represented as *two* `BasicExpression`s, one of which--the "penalty" portion--is used when the expression is being minimized (in the objective function) or penalized (to satisfy a constraint), and the second of which--the "constraint" portion--is used when the expression is being constrained. These two `BasicExpression`s are the first two parameters of this function. The third parameter--"extra_variables"--should contain any `DeferredVariable`s that are used (perhaps even indirectly) by this `Expression`. This is most commonly used for slack variables. The fourth parameter--"extra_constraints"--is used to specify additional constraints that should be added to any optimization problem involving this `Expression`. Technically, these can be anything: they're simply additional constraints, which may or may not have anything to do with the `Expression` to which they're attached. In practice, they'll usually represent conditions that are required for the associated `Expression` to make sense. For example, we could construct an `Expression` representing "the false positive rate of f(x) at the threshold for which the true positive rate is at least 0.9" with the expression being "the false positive rate of f(x) - t", and the extra constraint being "the true positive rate of f(x) - t is at least 0.9", where "t" is an implicit threshold. These extra constraints will ultimately be included in any optimization problem that includes the associated `Expression` (or an `Expression` derived from it). Args: penalty_expression: `BasicExpression` that will be used for the "penalty" portion of the optimization (i.e. when optimizing the model parameters). It should be {sub,semi}differentiable. constraint_expression: `BasicExpression` that will be used for the "constraint" portion of the optimization (i.e. when optimizing the constraints). It does not need to be {sub,semi}differentiable. extra_variables: optional collection of `DeferredVariable`s upon which this `Expression` depends. extra_constraints: optional collection of `Constraint`s required by this `Expression`. """ self._penalty_expression = penalty_expression self._constraint_expression = constraint_expression self._extra_variables = deferred_tensor.DeferredVariableList( extra_variables) self._extra_constraints = constraint.ConstraintList(extra_constraints)
def add_dependencies(self, extra_variables=None, extra_constraints=None): """Returns a new `Expression` with extra dependencies. The resulting `Expression` will depend on the same variables and constraints as this `Expression`, but will *also* depend on those included in the extra_variables and extra_constraints parameters to this method. Notice that this method does *not* change `self`: instead, it returns a *new* `Expression` that includes the extra dependencies. Args: extra_variables: optional collection of `DeferredVariable`s to add to the list of variables upon which the resulting `Expression` depends. extra_constraints: optional collection of `Constraint`s to add to the list of constraints required by the resulting `Expression`. """ extra_variables = deferred_tensor.DeferredVariableList(extra_variables) extra_constraints = constraint.ConstraintList(extra_constraints) return Expression( self._penalty_expression, self._constraint_expression, extra_variables=self._extra_variables + extra_variables, extra_constraints=self._extra_constraints + extra_constraints)
def extra_constraints(self): result = constraint.ConstraintList() for subexpression in self._expressions: result += subexpression.extra_constraints # The "list" property of a ConstraintList returns a copy. return result.list
def __init__(self, objective, constraints=None, denominator_lower_bound=1e-3): """Creates a rate constrained optimization problem. In addition to an objective function to minimize and a list of constraints to satisfy, this method also takes a "denominator_lower_bound" parameter. At a high level, a rate is "the proportion of training examples satisfying some property for which some event occurs, divided by the proportion of training examples satisfying the property", i.e. is a numerator divided by a denominator. To avoid dividing by zero (or quantities that are close to zero), the "denomintor_lower_bound" parameter is used to impose a lower bound on the denominator of a rate. However, this parameter is a last resort. If you're calculating a rate on a small subset of the data (i.e. with a property that is rately true, resulting in a small denominator), then the speed of optimization could suffer greatly: you'd almost certainly be better off splitting the subset of interest off into its own dataset, with its own placeholder tensors. Args: objective: an `Expression` to minimize. constraints: a collection of `Constraint`s to impose. denominator_lower_bound: float, the smallest permitted value of the denominator of a rate. Raises: ValueError: if the "penalty" portion of the objective or a constraint is non-differentiable, or if denominator_lower_bound is negative. """ # We do permit denominator_lower_bound to be zero. In this case, division by # zero is possible, and it's the user's responsibility to ensure that it # doesn't happen. if denominator_lower_bound < 0.0: raise ValueError("denominator lower bound must be non-negative") # The objective needs to be differentiable. So do the penalty portions of # the constraints, but we'll check those later. if not objective.penalty_expression.is_differentiable: raise ValueError( "non-differentiable losses (e.g. the zero-one loss) " "cannot be optimized--they can only be constrained") variables = deferred_tensor.DeferredVariableList() constraints = constraint.ConstraintList(constraints) # We make our own global_step, for keeping track of the denominators. We # don't take one as a parameter since we want complete ownership, to avoid # any shenanigans: it has to start at zero, and be incremented after every # minibatch. self._global_step = tf.compat.v2.Variable( 0, trainable=False, name="tfco_global_step", dtype=tf.int64, aggregation=tf.VariableAggregation.ONLY_FIRST_REPLICA) # This memoizer will remember and re-use certain intermediate values, # causing the TensorFlow graph we construct to contain fewer redundancies # than it would otherwise. Additionally, it will store any slack variables # or denominator variables that need to be created for the optimization # problem. self._memoizer = { defaults.DENOMINATOR_LOWER_BOUND_KEY: denominator_lower_bound, defaults.GLOBAL_STEP_KEY: self._global_step } # We ignore the "constraint_expression" field here, since we're not inside a # constraint (this is the objective function). self._objective, objective_variables = ( objective.penalty_expression.evaluate(self._memoizer)) variables += objective_variables variables += objective.extra_variables constraints += objective.extra_constraints # Evaluating expressions can result in extra constraints being introduced, # so we keep track of the number of constraints that we've already evaluated # in "checked_constraints", append new constraints to "constraints" (which # will automatically ignore attempts to add duplicates, since it's a # ConstraintList), and repeatedly check the newly-added constraints until # none are left. # # In light of the fact that constraints can depend on other constraints, we # can view the structure of constraints as a tree, in which case this code # will enumerate over the constraints in breadth-first order. self._proxy_constraints = [] self._constraints = [] checked_constraints = 0 while len(constraints) > checked_constraints: new_constraints = constraints[checked_constraints:] checked_constraints = len(constraints) for new_constraint in new_constraints: if not new_constraint.expression.penalty_expression.is_differentiable: raise ValueError( "non-differentiable losses (e.g. the zero-one loss) " "cannot be optimized--they can only be constrained") penalty_value, penalty_variables = ( new_constraint.expression.penalty_expression.evaluate( self._memoizer)) constraint_value, constraint_variables = ( new_constraint.expression.constraint_expression.evaluate( self._memoizer)) self._proxy_constraints.append(penalty_value) self._constraints.append(constraint_value) variables += penalty_variables variables += constraint_variables variables += new_constraint.expression.extra_variables constraints += new_constraint.expression.extra_constraints # Explicitly create all of the variables. This also functions as a sanity # check: before this point, no variable should have been accessed # directly, and since their storage didn't exist yet, they couldn't have # been. self._variables = variables.list for variable in self._variables: variable.create(self._memoizer)
def __init__(self, objective, constraints=None, denominator_lower_bound=1e-3, variable_fn=tf.Variable, name=None): """Creates a rate constrained optimization problem. In addition to an objective function to minimize and a list of constraints to satisfy, this method also takes a "denominator_lower_bound" parameter. At a high level, a rate is "the proportion of training examples satisfying some property for which some event occurs, divided by the proportion of training examples satisfying the property", i.e. is a numerator divided by a denominator. To avoid dividing by zero (or quantities that are close to zero), the "denomintor_lower_bound" parameter is used to impose a lower bound on the denominator of a rate. However, this parameter is a last resort. If you're calculating a rate on a small subset of the data (i.e. with a property that is rately true, resulting in a small denominator), then the speed of optimization could suffer greatly: you'd almost certainly be better off splitting the subset of interest off into its own dataset, with its own placeholder tensors. Args: objective: an `Expression` to minimize. constraints: a collection of `Constraint`s to impose. denominator_lower_bound: float, the smallest permitted value of the denominator of a rate. variable_fn: optional function with the same signature as the `tf.Variable` constructor, that returns a new variable with the specified properties. name: optional string, the name of this object. Raises: ValueError: if the "penalty" portion of the objective or a constraint is non-differentiable, or if denominator_lower_bound is negative. """ super(RateMinimizationProblem, self).__init__(name=name) # We do permit denominator_lower_bound to be zero. In this case, division by # zero is possible, and it's the user's responsibility to ensure that it # doesn't happen. if denominator_lower_bound < 0.0: raise ValueError("denominator lower bound must be non-negative") # The objective needs to be differentiable. So do the penalty portions of # the constraints, but we'll check those later. if not objective.penalty_expression.is_differentiable: raise ValueError( "non-differentiable losses (e.g. the zero-one loss) " "cannot be optimized--they can only be constrained") inputs = deferred_tensor.DeferredTensorInputList() variables = deferred_tensor.DeferredVariableList() constraints = constraint.ConstraintList(constraints) # We make our own global_step, for keeping track of the denominators. We # don't take one as a parameter since we want complete ownership, to avoid # any shenanigans: it has to start at zero, and be incremented after every # minibatch. self._global_step = variable_fn( 0, trainable=False, name="tfco_global_step", dtype=tf.int64, aggregation=tf.VariableAggregation.ONLY_FIRST_REPLICA) # This structure_memoizer will remember and re-use certain intermediate # values, causing the TensorFlow graph we construct to contain fewer # redundancies than it would otherwise. Additionally, it will store any # slack variables or denominator variables that need to be created for the # optimization problem. # # Each DeferredVariable has an associated key, which maps (in the structure # memoizer) to the tf.Variable itself. However, since dicts with tuple keys # cannot be tracked by tf.Trackable (upon which tf.Module is based), # "self._structure_memoizer" is excluded from tracking via # "self._no_dependency" and "_TF_MODULE_IGNORED_PROPERTIES". We still need # the raw tf.Variables to be tracked though, so we create the # "self._raw_variables" list, at the end of this method. self._structure_memoizer = self._no_dependency({ defaults.DENOMINATOR_LOWER_BOUND_KEY: denominator_lower_bound, defaults.GLOBAL_STEP_KEY: self._global_step, defaults.VARIABLE_FN_KEY: variable_fn }) # We ignore the "constraint_expression" field here, since we're not inside a # constraint (this is the objective function). self._objective = objective.penalty_expression.evaluate( self._structure_memoizer) inputs += self._objective.inputs variables += self._objective.variables constraints += objective.extra_constraints # Evaluating expressions can result in extra constraints being introduced, # so we keep track of the number of constraints that we've already evaluated # in "checked_constraints", append new constraints to "constraints" (which # will automatically ignore attempts to add duplicates, since it's a # ConstraintList), and repeatedly check the newly-added constraints until # none are left. # # In light of the fact that constraints can depend on other constraints, we # can view the structure of constraints as a tree, in which case this code # will enumerate over the constraints in breadth-first order. self._proxy_constraints = [] self._constraints = [] checked_constraints = 0 while len(constraints) > checked_constraints: new_constraints = constraints[checked_constraints:] checked_constraints = len(constraints) for new_constraint in new_constraints: if not new_constraint.expression.penalty_expression.is_differentiable: raise ValueError( "non-differentiable losses (e.g. the zero-one loss) " "cannot be optimized--they can only be constrained") penalty_value = new_constraint.expression.penalty_expression.evaluate( self._structure_memoizer) constraint_value = ( new_constraint.expression.constraint_expression.evaluate( self._structure_memoizer)) self._proxy_constraints.append(penalty_value) self._constraints.append(constraint_value) inputs += penalty_value.inputs inputs += constraint_value.inputs variables += penalty_value.variables variables += constraint_value.variables constraints += new_constraint.expression.extra_constraints # Extract the list of all input `Tensor`-like objects (or nullary functions # returning such). self._inputs = inputs.list # Explicitly create all of the variables. This also functions as a sanity # check: before this point, no variable should have been accessed # directly, and since their storage didn't exist yet, they couldn't have # been. # # The self._variables list contains the DeferredVariables needed by this # problem, whereas self._raw_variables contains the tf.Variables created by # these DeferredVariables. The only reason that we have the latter list is # to help tf.Module checkpoint them. self._variables = variables.list with self.name_scope: self._raw_variables = [ variable.create(self._structure_memoizer) for variable in self._variables ]