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
예제 #5
0
    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
            ]