def weighted(y_true, y_pred, weights, mask=None): """Wrapper function. Arguments: y_true: `y_true` argument of `fn`. y_pred: `y_pred` argument of `fn`. weights: Weights tensor. mask: Mask tensor. Returns: Scalar tensor. """ # score_array has ndim >= 2 score_array = fn(y_true, y_pred) if mask is not None: mask = math_ops.cast(mask, y_pred.dtype) # Update weights with mask. if weights is None: weights = mask else: # Update shape of weights if possible before adding mask. # Update dimensions of weights to match with mask if possible. mask, _, weights = metrics_module.squeeze_or_expand_dimensions( mask, None, weights) try: # Broadcast weights if possible. weights = weights_broadcast_ops.broadcast_weights( weights, mask) weights *= mask except ValueError: score_array *= mask score_array /= K.mean(mask) # TODO(psv): Handle case when mask and weight shapes are not # compatible. # Apply sample weighting. if weights is not None: # Update dimensions of weights to match with values if possible. score_array, _, weights = metrics_module.squeeze_or_expand_dimensions( score_array, None, weights) try: # Broadcast weights if possible. weights = weights_broadcast_ops.broadcast_weights( weights, score_array) except ValueError: # Reduce values to same ndim as weight array. ndim = K.ndim(score_array) weight_ndim = K.ndim(weights) score_array = K.mean(score_array, axis=list(range(weight_ndim, ndim))) score_array = math_ops.multiply(score_array, weights) score_array = math_ops.reduce_sum(score_array) weights = math_ops.reduce_sum(weights) score_array = metrics_module.safe_div(score_array, weights) return K.mean(score_array)
def weighted(y_true, y_pred, weights, mask=None): """Wrapper function. Arguments: y_true: `y_true` argument of `fn`. y_pred: `y_pred` argument of `fn`. weights: Weights tensor. mask: Mask tensor. Returns: Scalar tensor. """ # score_array has ndim >= 2 score_array = fn(y_true, y_pred) if mask is not None: mask = math_ops.cast(mask, y_pred.dtype) # Update weights with mask. if weights is None: weights = mask else: # Update shape of weights if possible before adding mask. # Update dimensions of weights to match with mask if possible. mask, _, weights = metrics_module.squeeze_or_expand_dimensions( mask, None, weights) try: # Broadcast weights if possible. weights = weights_broadcast_ops.broadcast_weights(weights, mask) weights *= mask except ValueError: score_array *= mask score_array /= K.mean(mask) # TODO(psv): Handle case when mask and weight shapes are not # compatible. # Apply sample weighting. if weights is not None: # Update dimensions of weights to match with values if possible. score_array, _, weights = metrics_module.squeeze_or_expand_dimensions( score_array, None, weights) try: # Broadcast weights if possible. weights = weights_broadcast_ops.broadcast_weights(weights, score_array) except ValueError: # Reduce values to same ndim as weight array. ndim = K.ndim(score_array) weight_ndim = K.ndim(weights) score_array = K.mean(score_array, axis=list(range(weight_ndim, ndim))) score_array = math_ops.multiply(score_array, weights) score_array = math_ops.reduce_sum(score_array) weights = math_ops.reduce_sum(weights) score_array = metrics_module.safe_div(score_array, weights) return K.mean(score_array)
def _test_valid(self, weights, values, expected): static_op = weights_broadcast_ops.broadcast_weights( weights=weights, values=values) weights_placeholder = array_ops.placeholder(dtypes_lib.float32) values_placeholder = array_ops.placeholder(dtypes_lib.float32) dynamic_op = weights_broadcast_ops.broadcast_weights( weights=weights_placeholder, values=values_placeholder) with self.test_session(): self.assertAllEqual(expected, static_op.eval()) self.assertAllEqual(expected, dynamic_op.eval(feed_dict={ weights_placeholder: weights, values_placeholder: values, }))
def _test_valid(self, weights, values, expected): static_op = weights_broadcast_ops.broadcast_weights( weights=weights, values=values) weights_placeholder = array_ops.placeholder(dtypes_lib.float32) values_placeholder = array_ops.placeholder(dtypes_lib.float32) dynamic_op = weights_broadcast_ops.broadcast_weights( weights=weights_placeholder, values=values_placeholder) with self.cached_session(): self.assertAllEqual(expected, self.evaluate(static_op)) self.assertAllEqual(expected, dynamic_op.eval(feed_dict={ weights_placeholder: weights, values_placeholder: values, }))
def _test_invalid(self, weights, values): error_msg = 'weights can not be broadcast to values' with self.assertRaisesRegexp(ValueError, error_msg): weights_broadcast_ops.broadcast_weights(weights=weights, values=values) weights_placeholder = array_ops.placeholder(dtypes_lib.float32) values_placeholder = array_ops.placeholder(dtypes_lib.float32) dynamic_op = weights_broadcast_ops.broadcast_weights( weights=weights_placeholder, values=values_placeholder) with self.test_session(): with self.assertRaisesRegexp(errors_impl.OpError, error_msg): dynamic_op.eval(feed_dict={ weights_placeholder: weights, values_placeholder: values, })
def _test_invalid(self, weights, values): error_msg = 'weights can not be broadcast to values' with self.assertRaisesRegex(ValueError, error_msg): weights_broadcast_ops.broadcast_weights(weights=weights, values=values) weights_placeholder = array_ops.placeholder(dtypes_lib.float32) values_placeholder = array_ops.placeholder(dtypes_lib.float32) dynamic_op = weights_broadcast_ops.broadcast_weights( weights=weights_placeholder, values=values_placeholder) with self.cached_session(): with self.assertRaisesRegex(errors_impl.OpError, error_msg): dynamic_op.eval(feed_dict={ weights_placeholder: weights, values_placeholder: values, })
def scale_losses_by_sample_weight(losses, sample_weight): """Scales loss values by the given sample weights. `sample_weight` dimensions are updated to match with the dimension of `losses` if possible by using squeeze/expand/broadcast. Args: losses: Loss tensor. sample_weight: Sample weights tensor. Returns: `losses` scaled by `sample_weight` with dtype float32. """ # TODO(psv): Handle the casting here in a better way, eg. if losses is float64 # we do not want to lose precision. losses = math_ops.cast(losses, dtypes.float32) sample_weight = math_ops.cast(sample_weight, dtypes.float32) # Update dimensions of `sample_weight` to match with `losses` if possible. losses, _, sample_weight = squeeze_or_expand_dimensions( losses, None, sample_weight) # Broadcast weights if possible. sample_weight = weights_broadcast_ops.broadcast_weights(sample_weight, losses) return math_ops.multiply(losses, sample_weight)
def _predictions_mean(predictions, weights=None, name=None): with ops.name_scope( name, 'predictions_mean', (predictions, weights)) as scope: predictions = math_ops.to_float(predictions, name='predictions') if weights is not None: weights = weights_broadcast_ops.broadcast_weights(weights, predictions) return metrics_lib.mean(predictions, weights=weights, name=scope)
def _num_present(losses, weights, per_batch=False): """Computes the number of elements in the loss function induced by `weights`. A given weights tensor induces different numbers of usable elements in the `losses` tensor. The `weights` tensor is broadcast across `losses` for all possible dimensions. For example, if `losses` is a tensor of dimension `[4, 5, 6, 3]` and `weights` is a tensor of shape `[4, 5]`, then `weights` is, in effect, tiled to match the shape of `losses`. Following this effective tile, the total number of present elements is the number of non-zero weights. Args: losses: `Tensor` of shape `[batch_size, d1, ... dN]`. weights: `Tensor` of shape `[]`, `[batch_size]` or `[batch_size, d1, ... dK]`, where K < N. per_batch: Whether to return the number of elements per batch or as a sum total. Returns: The number of present (non-zero) elements in the losses tensor. If `per_batch` is `True`, the value is returned as a tensor of size `[batch_size]`. Otherwise, a single scalar tensor is returned. """ with ops.name_scope(None, "num_present", (losses, weights)) as scope: weights = math_ops.to_float(weights) present = array_ops.where( math_ops.equal(weights, 0.0), array_ops.zeros_like(weights), array_ops.ones_like(weights)) present = weights_broadcast_ops.broadcast_weights(present, losses) if per_batch: return math_ops.reduce_sum( present, axis=math_ops.range(1, array_ops.rank(present)), keep_dims=True, name=scope) return math_ops.reduce_sum(present, name=scope)
def update_state(self, values, sample_weight=None): """Accumulates statistics for computing the mean. For example, if `values` is [1, 3, 5, 7] then the mean is 4. If the `sample_weight` is specified as [1, 1, 0, 0] then the mean would be 2. Args: values: Per-example value. sample_weight: Optional weighting of each example. Defaults to 1. """ values = math_ops.cast(values, self._dtype) if sample_weight is None: num_values = math_ops.cast(array_ops.size(values), self._dtype) else: sample_weight = math_ops.cast(sample_weight, self._dtype) # Update dimensions of weights to match with values. values, _, sample_weight = _squeeze_or_expand_dimensions( values, None, sample_weight) sample_weight = weights_broadcast_ops.broadcast_weights( sample_weight, values) num_values = math_ops.reduce_sum(sample_weight) values = math_ops.multiply(values, sample_weight) values = math_ops.reduce_sum(values) # Update state variables state_ops.assign_add(self.total, values) state_ops.assign_add(self.count, num_values)
def update_metric_with_broadcast_weights(eval_metric, values, weights): values = math_ops.to_float(values) if weights is not None: weights = weights_broadcast_ops.broadcast_weights( weights, values) eval_metric.update_state( values=values, sample_weight=weights)
def _num_present(losses, weights, per_batch=False): """Computes the number of elements in the loss function induced by `weights`. A given weights tensor induces different numbers of usable elements in the `losses` tensor. The `weights` tensor is broadcast across `losses` for all possible dimensions. For example, if `losses` is a tensor of dimension `[4, 5, 6, 3]` and `weights` is a tensor of shape `[4, 5]`, then `weights` is, in effect, tiled to match the shape of `losses`. Following this effective tile, the total number of present elements is the number of non-zero weights. Args: losses: `Tensor` of shape `[batch_size, d1, ... dN]`. weights: `Tensor` of shape `[]`, `[batch_size]` or `[batch_size, d1, ... dK]`, where K < N. per_batch: Whether to return the number of elements per batch or as a sum total. Returns: The number of present (non-zero) elements in the losses tensor. If `per_batch` is `True`, the value is returned as a tensor of size `[batch_size]`. Otherwise, a single scalar tensor is returned. """ with ops.name_scope(None, "num_present", (losses, weights)) as scope: weights = math_ops.to_float(weights) present = array_ops.where(math_ops.equal(weights, 0.0), array_ops.zeros_like(weights), array_ops.ones_like(weights)) present = weights_broadcast_ops.broadcast_weights(present, losses) if per_batch: return math_ops.reduce_sum(present, axis=math_ops.range( 1, array_ops.rank(present)), keep_dims=True, name=scope) return math_ops.reduce_sum(present, name=scope)
def compute_weighted_loss(losses, sample_weight=None, reduction=ReductionV2.SUM_OVER_BATCH_SIZE, name=None): """Computes the weighted loss. Args: losses: `Tensor` of shape `[batch_size, d1, ... dN]`. sample_weight: Optional `Tensor` whose rank is either 0, or the same rank as `losses`, or be broadcastable to `losses`. reduction: (Optional) Type of `tf.keras.losses.Reduction` to apply to loss. Default value is `SUM_OVER_BATCH_SIZE`. name: Optional name for the op. Raises: ValueError: If the shape of `sample_weight` is not compatible with `losses`. Returns: Weighted loss `Tensor` of the same type as `losses`. If `reduction` is `NONE`, this has the same shape as `losses`; otherwise, it is scalar. """ ReductionV2.validate(reduction) # If this function is called directly, then we just default 'AUTO' to # 'SUM_OVER_BATCH_SIZE'. Eg. Canned estimator use cases. if reduction == ReductionV2.AUTO: reduction = ReductionV2.SUM_OVER_BATCH_SIZE if sample_weight is None: sample_weight = 1.0 with K.name_scope(name or 'weighted_loss'): # Save the `reduction` argument for loss normalization when distributing # to multiple replicas. Used only for estimator + v1 optimizer flow. ops.get_default_graph()._last_loss_reduction = reduction # pylint: disable=protected-access # Update dimensions of `sample_weight` to match with `losses` if possible. losses, _, sample_weight = squeeze_or_expand_dimensions( losses, None, sample_weight) losses = ops.convert_to_tensor(losses) input_dtype = losses.dtype losses = math_ops.cast(losses, dtypes.float32) sample_weight = math_ops.cast(sample_weight, dtypes.float32) try: # Broadcast weights if possible. sample_weight = weights_broadcast_ops.broadcast_weights( sample_weight, losses) except ValueError: # Reduce values to same ndim as weight array. ndim = K.ndim(losses) weight_ndim = K.ndim(sample_weight) losses = K.mean(losses, axis=list(range(weight_ndim, ndim))) sample_weight.shape.assert_is_compatible_with(losses.shape) weighted_losses = math_ops.multiply(losses, sample_weight) # Apply reduction function to the individual weighted losses. loss = reduce_weighted_loss(weighted_losses, reduction) # Convert the result back to the input type. loss = math_ops.cast(loss, input_dtype) return loss
def compute_weighted_loss(losses, sample_weight=None, reduction=ReductionV2.SUM_OVER_BATCH_SIZE, name=None): """Computes the weighted loss. Args: losses: `Tensor` of shape `[batch_size, d1, ... dN]`. sample_weight: Optional `Tensor` whose rank is either 0, or the same rank as `losses`, or be broadcastable to `losses`. reduction: (Optional) Type of `tf.keras.losses.Reduction` to apply to loss. Default value is `SUM_OVER_BATCH_SIZE`. name: Optional name for the op. Raises: ValueError: If the shape of `sample_weight` is not compatible with `losses`. Returns: Weighted loss `Tensor` of the same type as `losses`. If `reduction` is `NONE`, this has the same shape as `losses`; otherwise, it is scalar. """ ReductionV2.validate(reduction) # If this function is called directly, then we just default 'AUTO' to # 'SUM_OVER_BATCH_SIZE'. Eg. Canned estimator use cases. if reduction == ReductionV2.AUTO: reduction = ReductionV2.SUM_OVER_BATCH_SIZE if sample_weight is None: sample_weight = 1.0 with ops.name_scope(name, 'weighted_loss', (losses, sample_weight)): # Save the `reduction` argument for loss normalization when distributing # to multiple replicas. Used only for estimator + v1 optimizer flow. ops.get_default_graph()._last_loss_reduction = reduction # pylint: disable=protected-access # Update dimensions of `sample_weight` to match with `losses` if possible. losses, _, sample_weight = squeeze_or_expand_dimensions( losses, None, sample_weight) losses = ops.convert_to_tensor(losses) input_dtype = losses.dtype losses = math_ops.cast(losses, dtypes.float32) sample_weight = math_ops.cast(sample_weight, dtypes.float32) try: # Broadcast weights if possible. sample_weight = weights_broadcast_ops.broadcast_weights( sample_weight, losses) except ValueError: # Reduce values to same ndim as weight array. ndim = K.ndim(losses) weight_ndim = K.ndim(sample_weight) losses = K.mean(losses, axis=list(range(weight_ndim, ndim))) sample_weight.shape.assert_is_compatible_with(losses.shape) weighted_losses = math_ops.multiply(losses, sample_weight) # Apply reduction function to the individual weighted losses. loss = reduce_weighted_loss(weighted_losses, reduction) # Convert the result back to the input type. loss = math_ops.cast(loss, input_dtype) return loss
def _update_auc(self, auc_metric, labels, predictions, weights=None): predictions = tf.cast(predictions, dtype=tf.dtypes.float32) if weights is not None: weights = weights_broadcast_ops.broadcast_weights( weights, predictions) auc_metric.update_state(y_true=labels, y_pred=predictions, sample_weight=weights)
def _auc(labels, predictions, weights=None, curve='ROC', name=None): with ops.name_scope(name, 'auc', (predictions, labels, weights)) as scope: predictions = math_ops.to_float(predictions, name='predictions') if weights is not None: weights = weights_broadcast_ops.broadcast_weights(weights, predictions) return metrics_lib.auc( labels=labels, predictions=predictions, weights=weights, curve=curve, name=scope)
def compute_weighted_loss(losses, sample_weight=None, reduction=losses_impl.ReductionV2.SUM_OVER_BATCH_SIZE, name=None): """Computes the weighted loss. Args: losses: `Tensor` of shape `[batch_size, d1, ... dN]`. sample_weight: Optional `Tensor` whose rank is either 0, or the same rank as `losses`, or be broadcastable to `losses`. reduction: Type of `tf.losses.Reduction` to apply to loss. Default value is `SUM_OVER_BATCH_SIZE`. name: Optional name for the op. Raises: ValueError: If the shape of `sample_weight` is not compatible with `losses`. Returns: Weighted loss `Tensor` of the same type as `losses`. If `reduction` is `NONE`, this has the same shape as `losses`; otherwise, it is scalar. """ losses_impl.ReductionV2.validate(reduction) if sample_weight is None: sample_weight = 1.0 with ops.name_scope(name, 'weighted_loss', (losses, sample_weight)): # Save the `reduction` argument for loss normalization when distributing # to multiple replicas. # TODO(josh11b): Associate it with the returned op for more precision. ops.get_default_graph()._last_loss_reduction = reduction # pylint: disable=protected-access # Update dimensions of `sample_weight` to match with `losses` if possible. losses, _, sample_weight = squeeze_or_expand_dimensions( losses, None, sample_weight) losses = ops.convert_to_tensor(losses) input_dtype = losses.dtype losses = math_ops.to_float(losses) sample_weight = math_ops.to_float(sample_weight) try: # Broadcast weights if possible. sample_weight = weights_broadcast_ops.broadcast_weights( sample_weight, losses) except ValueError: # Reduce values to same ndim as weight array. ndim = K.ndim(losses) weight_ndim = K.ndim(sample_weight) losses = K.mean(losses, axis=list(range(weight_ndim, ndim))) sample_weight.get_shape().assert_is_compatible_with(losses.get_shape()) weighted_losses = math_ops.multiply(losses, sample_weight) # Apply reduction function to the individual weighted losses. loss = _reduce_weighted_loss(weighted_losses, reduction) # Convert the result back to the input type. loss = math_ops.cast(loss, input_dtype) return loss
def _auc(labels, predictions, weights=None, curve='ROC', name=None): with ops.name_scope(name, 'auc', (predictions, labels, weights)) as scope: predictions = math_ops.to_float(predictions, name='predictions') if labels.dtype.base_dtype != dtypes.bool: logging.warning('Casting %s labels to bool.', labels.dtype) labels = math_ops.cast(labels, dtypes.bool) if weights is not None: weights = weights_broadcast_ops.broadcast_weights(weights, predictions) return metrics_lib.auc( labels=labels, predictions=predictions, weights=weights, curve=curve, name=scope)
def compute_weighted_loss( losses, sample_weight=None, reduction=losses_impl.ReductionV2.SUM_OVER_BATCH_SIZE, name=None): """Computes the weighted loss. Args: losses: `Tensor` of shape `[batch_size, d1, ... dN]`. sample_weight: Optional `Tensor` whose rank is either 0, or the same rank as `losses`, or be broadcastable to `losses`. reduction: Type of `tf.losses.Reduction` to apply to loss. Default value is `SUM_OVER_BATCH_SIZE`. name: Optional name for the op. Raises: ValueError: If the shape of `sample_weight` is not compatible with `losses`. Returns: Weighted loss `Tensor` of the same type as `losses`. If `reduction` is `NONE`, this has the same shape as `losses`; otherwise, it is scalar. """ losses_impl.ReductionV2.validate(reduction) if sample_weight is None: sample_weight = 1.0 with ops.name_scope(name, 'weighted_loss', (losses, sample_weight)): # Update dimensions of `sample_weight` to match with `losses` if possible. losses, _, sample_weight = squeeze_or_expand_dimensions( losses, None, sample_weight) losses = ops.convert_to_tensor(losses) input_dtype = losses.dtype losses = math_ops.cast(losses, dtypes.float32) sample_weight = math_ops.cast(sample_weight, dtypes.float32) try: # Broadcast weights if possible. sample_weight = weights_broadcast_ops.broadcast_weights( sample_weight, losses) except ValueError: # Reduce values to same ndim as weight array. ndim = K.ndim(losses) weight_ndim = K.ndim(sample_weight) losses = K.mean(losses, axis=list(range(weight_ndim, ndim))) sample_weight.get_shape().assert_is_compatible_with(losses.get_shape()) weighted_losses = math_ops.multiply(losses, sample_weight) # Apply reduction function to the individual weighted losses. loss = _reduce_weighted_loss(weighted_losses, reduction) # Convert the result back to the input type. loss = math_ops.cast(loss, input_dtype) return loss
def compute_weighted_loss(losses, sample_weight=None, reduction=ReductionV2.SUM_OVER_BATCH_SIZE, name=None): """Computes the weighted loss. Args: losses: `Tensor` of shape `[batch_size, d1, ... dN]`. sample_weight: Optional `Tensor` whose rank is either 0, or the same rank as `losses`, or be broadcastable to `losses`. reduction: (Optional) Type of `tf.keras.losses.Reduction` to apply to loss. Default value is `SUM_OVER_BATCH_SIZE`. name: Optional name for the op. Raises: ValueError: If the shape of `sample_weight` is not compatible with `losses`. Returns: Weighted loss `Tensor` of the same type as `losses`. If `reduction` is `NONE`, this has the same shape as `losses`; otherwise, it is scalar. """ ReductionV2.validate(reduction) if sample_weight is None: sample_weight = 1.0 with ops.name_scope(name, 'weighted_loss', (losses, sample_weight)): # Update dimensions of `sample_weight` to match with `losses` if possible. losses, _, sample_weight = squeeze_or_expand_dimensions( losses, None, sample_weight) losses = ops.convert_to_tensor(losses) input_dtype = losses.dtype losses = math_ops.cast(losses, dtypes.float32) sample_weight = math_ops.cast(sample_weight, dtypes.float32) try: # Broadcast weights if possible. sample_weight = weights_broadcast_ops.broadcast_weights( sample_weight, losses) except ValueError: # Reduce values to same ndim as weight array. ndim = K.ndim(losses) weight_ndim = K.ndim(sample_weight) losses = K.mean(losses, axis=list(range(weight_ndim, ndim))) sample_weight.shape.assert_is_compatible_with(losses.shape) weighted_losses = math_ops.multiply(losses, sample_weight) # Apply reduction function to the individual weighted losses. loss = reduce_weighted_loss(weighted_losses, reduction) # Convert the result back to the input type. loss = math_ops.cast(loss, input_dtype) return loss
def compute_weighted_loss(loss_unweighted, weights, name="loss"): with ops.name_scope(name, values=(loss_unweighted, weights)) as name_scope: if weights is None: loss = math_ops.reduce_mean(loss_unweighted, name=name_scope) return loss, loss weights = weights_broadcast_ops.broadcast_weights(weights, loss_unweighted) with ops.name_scope(None, "weighted_loss", (loss_unweighted, weights)) as name: weighted_loss = math_ops.multiply(loss_unweighted, weights, name=name) weighted_loss_mean = math_ops.reduce_mean(weighted_loss, name=name_scope) weighted_loss_normalized = math_ops.div( math_ops.reduce_sum(weighted_loss), math_ops.to_float(math_ops.reduce_sum(weights)), name="weighted_average_loss") return weighted_loss_mean, weighted_loss_normalized
def update_state(self, y_true, y_pred, sample_weight=None) -> None: y_true = tf.cast(y_true, dtype=self._dtype) y_pred = tf.cast(y_pred, dtype=self._dtype) if sample_weight is None: sample_weight = 1 sample_weight = tf.cast(sample_weight, dtype=self._dtype) sample_weight = weights_broadcast_ops.broadcast_weights( weights=sample_weight, values=y_true) weighted_y_true = y_true * sample_weight self.sum.assign_add(tf.reduce_sum(weighted_y_true, axis=0)) self.squared_sum.assign_add( tf.reduce_sum(y_true * weighted_y_true, axis=0)) self.res.assign_add( tf.reduce_sum((y_true - y_pred)**2 * sample_weight, axis=0)) self.count.assign_add(tf.reduce_sum(sample_weight, axis=0))
def weighted(y_true, y_pred, weights, mask=None): """Wrapper function. Arguments: y_true: `y_true` argument of `fn`. y_pred: `y_pred` argument of `fn`. weights: Weights tensor. mask: Mask tensor. Returns: Scalar tensor. """ # score_array has ndim >= 2 score_array = fn(y_true, y_pred) if mask is not None: mask = math_ops.cast(mask, y_pred.dtype) # Update weights with mask. if weights is None: weights = mask else: # Update dimensions of weights to match with mask if possible. mask, _, weights = squeeze_or_expand_dimensions( mask, None, weights) weights *= mask # Apply sample weighting. if weights is not None: # Update dimensions of weights to match with values if possible. score_array, _, weights = squeeze_or_expand_dimensions( score_array, None, weights) try: # Broadcast weights if possible. weights = weights_broadcast_ops.broadcast_weights( weights, score_array) except ValueError: # Reduce values to same ndim as weight array. ndim = K.ndim(score_array) weight_ndim = K.ndim(weights) score_array = K.mean(score_array, axis=list(range(weight_ndim, ndim))) score_array = math_ops.multiply(score_array, weights) score_array = math_ops.reduce_sum(score_array) weights = math_ops.reduce_sum(weights) score_array = math_ops.div_no_nan(score_array, weights) return K.mean(score_array)
def update_state(self, y_true, y_pred, sample_weight=None) -> None: if not hasattr(self, "squared_sum"): self.squared_sum = self.add_weight( name="squared_sum", shape=y_true.shape[1:], initializer="zeros", dtype=self._dtype, ) if not hasattr(self, "sum"): self.sum = self.add_weight( name="sum", shape=y_true.shape[1:], initializer="zeros", dtype=self._dtype, ) if not hasattr(self, "res"): self.res = self.add_weight( name="residual", shape=y_true.shape[1:], initializer="zeros", dtype=self._dtype, ) if not hasattr(self, "count"): self.count = self.add_weight( name="count", shape=y_true.shape[1:], initializer="zeros", dtype=self._dtype, ) y_true = tf.cast(y_true, dtype=self._dtype) y_pred = tf.cast(y_pred, dtype=self._dtype) if sample_weight is None: sample_weight = 1 sample_weight = tf.cast(sample_weight, dtype=self._dtype) sample_weight = weights_broadcast_ops.broadcast_weights( weights=sample_weight, values=y_true) weighted_y_true = y_true * sample_weight self.sum.assign_add(tf.reduce_sum(weighted_y_true, axis=0)) self.squared_sum.assign_add( tf.reduce_sum(y_true * weighted_y_true, axis=0)) self.res.assign_add( tf.reduce_sum((y_true - y_pred)**2 * sample_weight, axis=0)) self.count.assign_add(tf.reduce_sum(sample_weight, axis=0)) self.num_samples.assign_add(tf.size(y_true))
def weighted(y_true, y_pred, weights, mask=None): """Wrapper function. Arguments: y_true: `y_true` argument of `fn`. y_pred: `y_pred` argument of `fn`. weights: Weights tensor. mask: Mask tensor. Returns: Scalar tensor. """ # score_array has ndim >= 2 score_array = fn(y_true, y_pred) if mask is not None: # Cast the mask to floatX to avoid float64 upcasting in theano mask = math_ops.cast(mask, K.floatx()) # mask should have the same shape as score_array score_array *= mask # the loss per batch should be proportional # to the number of unmasked samples. score_array /= K.mean(mask) # Apply sample weighting. if weights is not None: # Update dimensions of weights to match with values if possible. score_array, _, weights = metrics_module.squeeze_or_expand_dimensions( score_array, None, weights) try: # Broadcast weights if possible. weights = weights_broadcast_ops.broadcast_weights( weights, score_array) except ValueError: # Reduce values to same ndim as weight array. ndim = K.ndim(score_array) weight_ndim = K.ndim(weights) score_array = K.mean(score_array, axis=list(range(weight_ndim, ndim))) score_array = math_ops.multiply(score_array, weights) score_array = math_ops.reduce_sum(score_array) weights = math_ops.reduce_sum(weights) score_array = metrics_module.safe_div(score_array, weights) return K.mean(score_array)
def update_state(self, y_true, y_pred, sample_weight=None): y_true = tf.cast(y_true, self._dtype) y_pred = tf.cast(y_pred, self._dtype) if sample_weight is None: sample_weight = 1 sample_weight = tf.cast(sample_weight, self._dtype) sample_weight = weights_broadcast_ops.broadcast_weights( sample_weight, y_true) weighted_y_true = tf.multiply(y_true, sample_weight) self.sum.assign_add(tf.reduce_sum(weighted_y_true)) self.squared_sum.assign_add( tf.reduce_sum(tf.multiply(y_true, weighted_y_true))) self.res.assign_add( tf.reduce_sum( tf.multiply(tf.square(tf.subtract(y_true, y_pred)), sample_weight))) self.count.assign_add(tf.reduce_sum(sample_weight))
def update_state(self, values, sample_weight=None): """Accumulates statistics for computing the mean. For example, if `values` is [1, 3, 5, 7] then the mean is 4. If the `sample_weight` is specified as [1, 1, 0, 0] then the mean would be 2. Args: values: Per-example value. sample_weight: Optional weighting of each example. Defaults to 1. Returns: Update op. """ values = math_ops.cast(values, self._dtype) if sample_weight is None: num_values = math_ops.cast(array_ops.size(values), self._dtype) else: sample_weight = math_ops.cast(sample_weight, self._dtype) # Update dimensions of weights to match with values if possible. values, _, sample_weight = squeeze_or_expand_dimensions( values, None, sample_weight) try: # Broadcast weights if possible. sample_weight = weights_broadcast_ops.broadcast_weights( sample_weight, values) except ValueError: # Reduce values to same ndim as weight array ndim = K.ndim(values) weight_ndim = K.ndim(sample_weight) values = math_ops.reduce_mean(values, axis=list( range(weight_ndim, ndim))) num_values = math_ops.reduce_sum(sample_weight) values = math_ops.multiply(values, sample_weight) values = math_ops.reduce_sum(values) # Update state variables. Count should be updated only when total is # updated. update_total_op = state_ops.assign_add(self.total, values) with ops.control_dependencies([update_total_op]): update_count_op = state_ops.assign_add(self.count, num_values) return ops.convert_to_tensor(update_count_op)
def weighted(y_true, y_pred, weights, mask=None): """Wrapper function. Arguments: y_true: `y_true` argument of `fn`. y_pred: `y_pred` argument of `fn`. weights: Weights tensor. mask: Mask tensor. Returns: Scalar tensor. """ # score_array has ndim >= 2 score_array = fn(y_true, y_pred) if mask is not None: mask = math_ops.cast(mask, y_pred.dtype) # Update weights with mask. if weights is None: weights = mask else: # Update dimensions of weights to match with mask if possible. mask, _, weights = squeeze_or_expand_dimensions(mask, None, weights) weights *= mask # Apply sample weighting. if weights is not None: # Update dimensions of weights to match with values if possible. score_array, _, weights = squeeze_or_expand_dimensions( score_array, None, weights) try: # Broadcast weights if possible. weights = weights_broadcast_ops.broadcast_weights(weights, score_array) except ValueError: # Reduce values to same ndim as weight array. ndim = K.ndim(score_array) weight_ndim = K.ndim(weights) score_array = K.mean(score_array, axis=list(range(weight_ndim, ndim))) score_array = math_ops.multiply(score_array, weights) score_array = math_ops.reduce_sum(score_array) weights = math_ops.reduce_sum(weights) score_array = math_ops.div_no_nan(score_array, weights) return K.mean(score_array)
def cal_abs_with_pit(source, estimate_source, source_lengths, C, method=tf.abs): # estimate_source = B, T, S, D # source = B, S, T, D # estimate_source = B, S, T, D estimate_source = tf.transpose(estimate_source, perm=[0, 2, 1, 3]) mask = tf.cast( tf.sequence_mask(source_lengths, tf.reduce_max(source_lengths)), source.dtype, ) mask = tf.expand_dims(mask, 1) mask = tf.expand_dims(mask, -1) # estimate_source *= mask targets = tf.expand_dims(source, 1) est_targets = tf.expand_dims(estimate_source, 2) pw_loss = method(targets - est_targets) # pair_wise_abs = tf.reduce_mean(pw_loss, axis = [3, 4]) losses = pw_loss m = tf.expand_dims(mask, 1) weights = tf.cast(m, dtype=tf.float32) weighted_losses = tf.multiply(losses, weights) total_loss = tf.reduce_sum(weighted_losses, axis=[3, 4]) present = tf.where(tf.equal(weights, 0.0), tf.zeros_like(weights), tf.ones_like(weights)) present = weights_broadcast_ops.broadcast_weights(present, losses) present = tf.reduce_sum(present, axis=[3, 4]) pair_wise_abs = tf.div_no_nan(total_loss, present) v_perms = tf.constant(list(permutations(range(C)))) perms_one_hot = tf.one_hot(v_perms, C) abs_set = tf.einsum('bij,pij->bp', pair_wise_abs, perms_one_hot) min_abs = tf.reduce_min(abs_set, axis=1, keepdims=True) min_abs /= C return min_abs, abs_set
def update_state(self, values, sample_weight=None): """Accumulates statistics for computing the mean. For example, if `values` is [1, 3, 5, 7] then the mean is 4. If the `sample_weight` is specified as [1, 1, 0, 0] then the mean would be 2. Args: values: Per-example value. sample_weight: Optional weighting of each example. Defaults to 1. Returns: Update op. """ values = math_ops.cast(values, self._dtype) if sample_weight is None: num_values = math_ops.cast(array_ops.size(values), self._dtype) else: sample_weight = math_ops.cast(sample_weight, self._dtype) # Update dimensions of weights to match with values if possible. values, _, sample_weight = squeeze_or_expand_dimensions( values, None, sample_weight) try: # Broadcast weights if possible. sample_weight = weights_broadcast_ops.broadcast_weights( sample_weight, values) except ValueError: # Reduce values to same ndim as weight array ndim = K.ndim(values) weight_ndim = K.ndim(sample_weight) values = math_ops.reduce_mean( values, axis=list(range(weight_ndim, ndim))) num_values = math_ops.reduce_sum(sample_weight) values = math_ops.multiply(values, sample_weight) values = math_ops.reduce_sum(values) # Update state variables. Count should be updated only when total is # updated. update_total_op = state_ops.assign_add(self.total, values) with ops.control_dependencies([update_total_op]): update_count_op = state_ops.assign_add(self.count, num_values) return ops.convert_to_tensor(update_count_op)
def update_state(self, y_true, y_pred, sample_weight=None) -> None: y_true = tf.cast(y_true, dtype=self._dtype) y_pred = tf.cast(y_pred, dtype=self._dtype) if sample_weight is None: sample_weight = 1 sample_weight = tf.cast(sample_weight, dtype=self._dtype) sample_weight = weights_broadcast_ops.broadcast_weights( weights=sample_weight, values=y_true) weighted_y_true = y_true * sample_weight xy = tf.math.multiply(weighted_y_true, y_pred) self.sum_xy.assign_add(tf.reduce_sum(xy, axis=0)) self.sum_squared_x.assign_add(tf.reduce_sum(weighted_y_true**2, axis=0)) self.sum_squared_y.assign_add(tf.reduce_sum(y_pred**2, axis=0)) self.sum_x.assign_add(tf.reduce_sum(weighted_y_true, axis=0)) self.sum_y.assign_add(tf.reduce_sum(y_pred, axis=0)) self.count.assign_add(tf.reduce_sum(sample_weight, axis=0))
def metric_fn_train_batch(masked_lm_log_probs, masked_lm_ids, masked_lm_weights): masked_lm_log_probs = tf.reshape( masked_lm_log_probs, [-1, masked_lm_log_probs.shape[-1]]) masked_lm_predictions = tf.argmax(masked_lm_log_probs, axis=-1, output_type=tf.int32) masked_lm_ids = tf.reshape(masked_lm_ids, [-1]) masked_lm_weights = tf.reshape(masked_lm_weights, [-1]) if masked_lm_ids.dtype != masked_lm_predictions.dtype: masked_lm_predictions = tf.cast(masked_lm_predictions, masked_lm_ids.dtype) is_correct = tf.to_float( math_ops.equal(masked_lm_predictions, masked_lm_ids)) if masked_lm_weights is None: count = tf.to_float(tf.size(is_correct)) else: masked_lm_weights = weights_broadcast_ops.broadcast_weights( tf.to_float(masked_lm_weights), is_correct) is_correct = tf.math.multiply(is_correct, masked_lm_weights) count = tf.math.reduce_sum(masked_lm_weights) acc_value = tf.math.reduce_sum(is_correct) / count return dict(masked_lm_accuracy_train_batch=acc_value)
def update_metrics(self, eval_metrics, features, logits, labels, regularization_losses=None): """Updates and returns the Eval metric ops. See `Head` for more details.""" # Compute predictions. predictions = self.predictions(logits) predicted_value = predictions[ prediction_keys.PredictionKeys.PREDICTIONS] logits = base_head.check_logits_final_dim(logits, self.logits_dimension) label_ids = self._processed_labels(logits, labels) unweighted_loss, weights = self._unweighted_loss_and_weights( logits, label_ids, features) # Update metrics. eval_metrics[self._loss_mean_key].update_state(values=unweighted_loss, sample_weight=weights) eval_metrics[self._label_mean_key].update_state(values=labels, sample_weight=weights) predicted_value = math_ops.to_float(predicted_value, name='predictions') if weights is not None: weights = weights_broadcast_ops.broadcast_weights( weights, predicted_value) eval_metrics[self._prediction_mean_key].update_state( values=predicted_value, sample_weight=weights) if regularization_losses is not None: regularization_loss = math_ops.add_n(regularization_losses) eval_metrics[self._loss_regularization_key].update_state( values=regularization_loss) return eval_metrics
def _indicator_labels_mean(labels, weights=None, name=None): with ops.name_scope(name, 'labels_mean', (labels, weights)) as scope: labels = math_ops.to_float(labels, name='labels') if weights is not None: weights = weights_broadcast_ops.broadcast_weights(weights, labels) return metrics_lib.mean(labels, weights=weights, name=scope)
def _confusion_matrix_at_thresholds(labels, predictions, thresholds, weights=None): with ops.control_dependencies([ check_ops.assert_greater_equal( predictions, math_ops.cast(0.0, dtype=predictions.dtype), message='predictions must be in [0, 1]'), check_ops.assert_less_equal( predictions, math_ops.cast(1.0, dtype=predictions.dtype), message='predictions must be in [0, 1]') ]): predictions, labels, weights = _remove_squeezable_dimensions( predictions=math_ops.to_float(predictions), labels=math_ops.cast(labels, dtype=dtypes.bool), weights=weights) num_thresholds = len(thresholds) # Reshape predictions and labels. predictions_2d = array_ops.reshape(predictions, [-1, 1]) labels_2d = array_ops.reshape(math_ops.cast(labels, dtype=dtypes.bool), [1, -1]) # Use static shape if known. num_predictions = predictions_2d.get_shape().as_list()[0] # Otherwise use dynamic shape. if num_predictions is None: num_predictions = array_ops.shape(predictions_2d)[0] thresh_tiled = array_ops.tile( array_ops.expand_dims(array_ops.constant(thresholds), [1]), array_ops.stack([1, num_predictions])) # Tile the predictions after threshold them across different thresholds. pred_is_pos = math_ops.greater( array_ops.tile(array_ops.transpose(predictions_2d), [num_thresholds, 1]), thresh_tiled) pred_is_neg = math_ops.logical_not(pred_is_pos) label_is_pos = array_ops.tile(labels_2d, [num_thresholds, 1]) label_is_neg = math_ops.logical_not(label_is_pos) if weights is not None: weights = weights_broadcast_ops.broadcast_weights( math_ops.to_float(weights), predictions) weights_tiled = array_ops.tile(array_ops.reshape(weights, [1, -1]), [num_thresholds, 1]) thresh_tiled.get_shape().assert_is_compatible_with( weights_tiled.get_shape()) else: weights_tiled = None values = {} # tp is_true_positive = math_ops.to_float( math_ops.logical_and(label_is_pos, pred_is_pos)) if weights_tiled is not None: is_true_positive *= weights_tiled values['tp'] = math_ops.reduce_sum(is_true_positive, 1) # fn is_false_negative = math_ops.to_float( math_ops.logical_and(label_is_pos, pred_is_neg)) if weights_tiled is not None: is_false_negative *= weights_tiled values['fn'] = math_ops.reduce_sum(is_false_negative, 1) # tn is_true_negative = math_ops.to_float( math_ops.logical_and(label_is_neg, pred_is_neg)) if weights_tiled is not None: is_true_negative *= weights_tiled values['tn'] = math_ops.reduce_sum(is_true_negative, 1) # fp is_false_positive = math_ops.to_float( math_ops.logical_and(label_is_neg, pred_is_pos)) if weights_tiled is not None: is_false_positive *= weights_tiled values['fp'] = math_ops.reduce_sum(is_false_positive, 1) return values
def update_confusion_matrix_variables(variables_to_update, y_true, y_pred, thresholds, top_k=None, class_id=None, sample_weight=None): """Returns op to update the given confusion matrix variables. For every pair of values in y_true and y_pred: true_positive: y_true == True and y_pred > thresholds false_negatives: y_true == True and y_pred <= thresholds true_negatives: y_true == False and y_pred <= thresholds false_positive: y_true == False and y_pred > thresholds The results will be weighted and added together. When multiple thresholds are provided, we will repeat the same for every threshold. For estimation of these metrics over a stream of data, the function creates an `update_op` operation that updates the given variables. If `sample_weight` is `None`, weights default to 1. Use weights of 0 to mask values. Args: variables_to_update: Dictionary with 'tp', 'fn', 'tn', 'fp' as valid keys and corresponding variables to update as values. y_true: A `Tensor` whose shape matches `y_pred`. Will be cast to `bool`. y_pred: A floating point `Tensor` of arbitrary shape and whose values are in the range `[0, 1]`. thresholds: A float value or a python list or tuple of float thresholds in `[0, 1]`, or NEG_INF (used when top_k is set). top_k: Optional int, indicates that the positive labels should be limited to the top k predictions. class_id: Optional int, limits the prediction and labels to the class specified by this argument. sample_weight: Optional `Tensor` whose rank is either 0, or the same rank as `y_true`, and must be broadcastable to `y_true` (i.e., all dimensions must be either `1`, or the same as the corresponding `y_true` dimension). Returns: Update op. Raises: ValueError: If `y_pred` and `y_true` have mismatched shapes, or if `sample_weight` is not `None` and its shape doesn't match `y_pred`, or if `variables_to_update` contains invalid keys. """ if variables_to_update is None: return y_true = ops.convert_to_tensor(y_true) y_pred = ops.convert_to_tensor(y_pred) y_pred.shape.assert_is_compatible_with(y_true.shape) if not any( key for key in variables_to_update if key in list(ConfusionMatrix)): raise ValueError( 'Please provide at least one valid confusion matrix ' 'variable to update. Valid variable key options are: "{}". ' 'Received: "{}"'.format( list(ConfusionMatrix), variables_to_update.keys())) invalid_keys = [ key for key in variables_to_update if key not in list(ConfusionMatrix) ] if invalid_keys: raise ValueError( 'Invalid keys: {}. Valid variable key options are: "{}"'.format( invalid_keys, list(ConfusionMatrix))) with ops.control_dependencies([ check_ops.assert_greater_equal( y_pred, math_ops.cast(0.0, dtype=y_pred.dtype), message='predictions must be >= 0'), check_ops.assert_less_equal( y_pred, math_ops.cast(1.0, dtype=y_pred.dtype), message='predictions must be <= 1') ]): y_pred, y_true, sample_weight = squeeze_or_expand_dimensions( math_ops.cast(y_pred, dtype=dtypes.float32), math_ops.cast(y_true, dtype=dtypes.bool), sample_weight) if top_k is not None: y_pred = _filter_top_k(y_pred, top_k) if class_id is not None: y_true = y_true[..., class_id] y_pred = y_pred[..., class_id] thresholds = to_list(thresholds) num_thresholds = len(thresholds) num_predictions = array_ops.size(y_pred) # Reshape predictions and labels. predictions_2d = array_ops.reshape(y_pred, [1, -1]) labels_2d = array_ops.reshape( math_ops.cast(y_true, dtype=dtypes.bool), [1, -1]) # Tile the thresholds for every prediction. thresh_tiled = array_ops.tile( array_ops.expand_dims(array_ops.constant(thresholds), 1), array_ops.stack([1, num_predictions])) # Tile the predictions for every threshold. preds_tiled = array_ops.tile(predictions_2d, [num_thresholds, 1]) # Compare predictions and threshold. pred_is_pos = math_ops.greater(preds_tiled, thresh_tiled) # Tile labels by number of thresholds label_is_pos = array_ops.tile(labels_2d, [num_thresholds, 1]) if sample_weight is not None: weights = weights_broadcast_ops.broadcast_weights( math_ops.cast(sample_weight, dtype=dtypes.float32), y_pred) weights_tiled = array_ops.tile( array_ops.reshape(weights, [1, -1]), [num_thresholds, 1]) else: weights_tiled = None update_ops = [] def weighted_assign_add(label, pred, weights, var): label_and_pred = math_ops.cast( math_ops.logical_and(label, pred), dtype=dtypes.float32) if weights is not None: label_and_pred *= weights return state_ops.assign_add(var, math_ops.reduce_sum(label_and_pred, 1)) loop_vars = { ConfusionMatrix.TRUE_POSITIVES: (label_is_pos, pred_is_pos), } update_tn = ConfusionMatrix.TRUE_NEGATIVES in variables_to_update update_fp = ConfusionMatrix.FALSE_POSITIVES in variables_to_update update_fn = ConfusionMatrix.FALSE_NEGATIVES in variables_to_update if update_fn or update_tn: pred_is_neg = math_ops.logical_not(pred_is_pos) loop_vars[ConfusionMatrix.FALSE_NEGATIVES] = (label_is_pos, pred_is_neg) if update_fp or update_tn: label_is_neg = math_ops.logical_not(label_is_pos) loop_vars[ConfusionMatrix.FALSE_POSITIVES] = (label_is_neg, pred_is_pos) if update_tn: loop_vars[ConfusionMatrix.TRUE_NEGATIVES] = (label_is_neg, pred_is_neg) for matrix_cond, (label, pred) in loop_vars.items(): if matrix_cond in variables_to_update: update_ops.append( weighted_assign_add(label, pred, weights_tiled, variables_to_update[matrix_cond])) return control_flow_ops.group(update_ops)
def update_confusion_matrix_variables(variables_to_update, y_true, y_pred, thresholds, top_k=None, class_id=None, sample_weight=None, multi_label=False, label_weights=None, thresholds_distributed_evenly=False): """Returns op to update the given confusion matrix variables. For every pair of values in y_true and y_pred: true_positive: y_true == True and y_pred > thresholds false_negatives: y_true == True and y_pred <= thresholds true_negatives: y_true == False and y_pred <= thresholds false_positive: y_true == False and y_pred > thresholds The results will be weighted and added together. When multiple thresholds are provided, we will repeat the same for every threshold. For estimation of these metrics over a stream of data, the function creates an `update_op` operation that updates the given variables. If `sample_weight` is `None`, weights default to 1. Use weights of 0 to mask values. Args: variables_to_update: Dictionary with 'tp', 'fn', 'tn', 'fp' as valid keys and corresponding variables to update as values. y_true: A `Tensor` whose shape matches `y_pred`. Will be cast to `bool`. y_pred: A floating point `Tensor` of arbitrary shape and whose values are in the range `[0, 1]`. thresholds: A float value, float tensor, python list, or tuple of float thresholds in `[0, 1]`, or NEG_INF (used when top_k is set). top_k: Optional int, indicates that the positive labels should be limited to the top k predictions. class_id: Optional int, limits the prediction and labels to the class specified by this argument. sample_weight: Optional `Tensor` whose rank is either 0, or the same rank as `y_true`, and must be broadcastable to `y_true` (i.e., all dimensions must be either `1`, or the same as the corresponding `y_true` dimension). multi_label: Optional boolean indicating whether multidimensional prediction/labels should be treated as multilabel responses, or flattened into a single label. When True, the valus of `variables_to_update` must have a second dimension equal to the number of labels in y_true and y_pred, and those tensors must not be RaggedTensors. label_weights: (optional) tensor of non-negative weights for multilabel data. The weights are applied when calculating TP, FP, FN, and TN without explicit multilabel handling (i.e. when the data is to be flattened). thresholds_distributed_evenly: Boolean, whether the thresholds are evenly distributed within the list. An optimized method will be used if this is the case. See _update_confusion_matrix_variables_optimized() for more details. Returns: Update op. Raises: ValueError: If `y_pred` and `y_true` have mismatched shapes, or if `sample_weight` is not `None` and its shape doesn't match `y_pred`, or if `variables_to_update` contains invalid keys. """ if multi_label and label_weights is not None: raise ValueError('`label_weights` for multilabel data should be handled ' 'outside of `update_confusion_matrix_variables` when ' '`multi_label` is True.') if variables_to_update is None: return if not any( key for key in variables_to_update if key in list(ConfusionMatrix)): raise ValueError( 'Please provide at least one valid confusion matrix ' 'variable to update. Valid variable key options are: "{}". ' 'Received: "{}"'.format( list(ConfusionMatrix), variables_to_update.keys())) variable_dtype = list(variables_to_update.values())[0].dtype y_true = math_ops.cast(y_true, dtype=variable_dtype) y_pred = math_ops.cast(y_pred, dtype=variable_dtype) if thresholds_distributed_evenly: # Check whether the thresholds has any leading or tailing epsilon added # for floating point imprecision. The leading and tailing threshold will be # handled bit differently as the corner case. # At this point, thresholds should be a list/array with more than 2 items, # and ranged between [0, 1]. See is_evenly_distributed_thresholds() for more # details. thresholds_with_epsilon = thresholds[0] < 0.0 or thresholds[-1] > 1.0 thresholds = ops.convert_to_tensor_v2_with_dispatch( thresholds, dtype=variable_dtype) num_thresholds = thresholds.shape.as_list()[0] if multi_label: one_thresh = math_ops.equal( math_ops.cast(1, dtype=dtypes.int32), array_ops.rank(thresholds), name='one_set_of_thresholds_cond') else: [y_pred, y_true], _ = ragged_assert_compatible_and_get_flat_values([y_pred, y_true], sample_weight) one_thresh = math_ops.cast(True, dtype=dtypes.bool) invalid_keys = [ key for key in variables_to_update if key not in list(ConfusionMatrix) ] if invalid_keys: raise ValueError( 'Invalid keys: {}. Valid variable key options are: "{}"'.format( invalid_keys, list(ConfusionMatrix))) with ops.control_dependencies([ check_ops.assert_greater_equal( y_pred, math_ops.cast(0.0, dtype=y_pred.dtype), message='predictions must be >= 0'), check_ops.assert_less_equal( y_pred, math_ops.cast(1.0, dtype=y_pred.dtype), message='predictions must be <= 1') ]): if sample_weight is None: y_pred, y_true = losses_utils.squeeze_or_expand_dimensions( y_pred, y_true) else: sample_weight = math_ops.cast(sample_weight, dtype=variable_dtype) y_pred, y_true, sample_weight = ( losses_utils.squeeze_or_expand_dimensions( y_pred, y_true, sample_weight=sample_weight)) y_pred.shape.assert_is_compatible_with(y_true.shape) if top_k is not None: y_pred = _filter_top_k(y_pred, top_k) if class_id is not None: y_true = y_true[..., class_id] y_pred = y_pred[..., class_id] if thresholds_distributed_evenly and compat.forward_compatible(2021, 6, 8): # The new approach will take effect after 2021/6/8, to give enough time # for Brella release to pick up the new op tf.math.cumsum with float32. return _update_confusion_matrix_variables_optimized( variables_to_update, y_true, y_pred, thresholds, multi_label=multi_label, sample_weights=sample_weight, label_weights=label_weights, thresholds_with_epsilon=thresholds_with_epsilon) pred_shape = array_ops.shape(y_pred) num_predictions = pred_shape[0] if y_pred.shape.ndims == 1: num_labels = 1 else: num_labels = gen_math_ops.Prod(input=pred_shape[1:], axis=0) thresh_label_tile = array_ops.where_v2(one_thresh, num_labels, array_ops.ones([], dtype=dtypes.int32)) # Reshape predictions and labels, adding a dim for thresholding. if multi_label: predictions_extra_dim = array_ops.expand_dims(y_pred, 0) labels_extra_dim = array_ops.expand_dims( math_ops.cast(y_true, dtype=dtypes.bool), 0) else: # Flatten predictions and labels when not multilabel. predictions_extra_dim = array_ops.reshape(y_pred, [1, -1]) labels_extra_dim = array_ops.reshape( math_ops.cast(y_true, dtype=dtypes.bool), [1, -1]) # Tile the thresholds for every prediction. if multi_label: thresh_pretile_shape = [num_thresholds, 1, -1] thresh_tiles = [1, num_predictions, thresh_label_tile] data_tiles = [num_thresholds, 1, 1] else: thresh_pretile_shape = [num_thresholds, -1] thresh_tiles = [1, num_predictions * num_labels] data_tiles = [num_thresholds, 1] thresh_tiled = array_ops.tile( array_ops.reshape(thresholds, thresh_pretile_shape), array_ops.stack(thresh_tiles)) # Tile the predictions for every threshold. preds_tiled = array_ops.tile(predictions_extra_dim, data_tiles) # Compare predictions and threshold. pred_is_pos = math_ops.greater(preds_tiled, thresh_tiled) # Tile labels by number of thresholds label_is_pos = array_ops.tile(labels_extra_dim, data_tiles) if sample_weight is not None: sample_weight = weights_broadcast_ops.broadcast_weights( math_ops.cast(sample_weight, dtype=variable_dtype), y_pred) weights_tiled = array_ops.tile( array_ops.reshape(sample_weight, thresh_tiles), data_tiles) else: weights_tiled = None if label_weights is not None and not multi_label: label_weights = array_ops.expand_dims(label_weights, 0) label_weights = weights_broadcast_ops.broadcast_weights(label_weights, y_pred) label_weights_tiled = array_ops.tile( array_ops.reshape(label_weights, thresh_tiles), data_tiles) if weights_tiled is None: weights_tiled = label_weights_tiled else: weights_tiled = math_ops.multiply(weights_tiled, label_weights_tiled) update_ops = [] def weighted_assign_add(label, pred, weights, var): label_and_pred = math_ops.cast( math_ops.logical_and(label, pred), dtype=var.dtype) if weights is not None: label_and_pred *= math_ops.cast(weights, dtype=var.dtype) return var.assign_add(math_ops.reduce_sum(label_and_pred, 1)) loop_vars = { ConfusionMatrix.TRUE_POSITIVES: (label_is_pos, pred_is_pos), } update_tn = ConfusionMatrix.TRUE_NEGATIVES in variables_to_update update_fp = ConfusionMatrix.FALSE_POSITIVES in variables_to_update update_fn = ConfusionMatrix.FALSE_NEGATIVES in variables_to_update if update_fn or update_tn: pred_is_neg = math_ops.logical_not(pred_is_pos) loop_vars[ConfusionMatrix.FALSE_NEGATIVES] = (label_is_pos, pred_is_neg) if update_fp or update_tn: label_is_neg = math_ops.logical_not(label_is_pos) loop_vars[ConfusionMatrix.FALSE_POSITIVES] = (label_is_neg, pred_is_pos) if update_tn: loop_vars[ConfusionMatrix.TRUE_NEGATIVES] = (label_is_neg, pred_is_neg) for matrix_cond, (label, pred) in loop_vars.items(): if matrix_cond in variables_to_update: update_ops.append( weighted_assign_add(label, pred, weights_tiled, variables_to_update[matrix_cond])) return control_flow_ops.group(update_ops)
def _update_confusion_matrix_variables_optimized( variables_to_update, y_true, y_pred, thresholds, multi_label=False, sample_weights=None, label_weights=None, thresholds_with_epsilon=False): """Update confusion matrix variables with memory efficient alternative. Note that the thresholds need to be evenly distributed within the list, eg, the diff between consecutive elements are the same. To compute TP/FP/TN/FN, we are measuring a binary classifier C(t) = (predictions >= t) at each threshold 't'. So we have TP(t) = sum( C(t) * true_labels ) FP(t) = sum( C(t) * false_labels ) But, computing C(t) requires computation for each t. To make it fast, observe that C(t) is a cumulative integral, and so if we have thresholds = [t_0, ..., t_{n-1}]; t_0 < ... < t_{n-1} where n = num_thresholds, and if we can compute the bucket function B(i) = Sum( (predictions == t), t_i <= t < t{i+1} ) then we get C(t_i) = sum( B(j), j >= i ) which is the reversed cumulative sum in tf.cumsum(). We can compute B(i) efficiently by taking advantage of the fact that our thresholds are evenly distributed, in that width = 1.0 / (num_thresholds - 1) thresholds = [0.0, 1*width, 2*width, 3*width, ..., 1.0] Given a prediction value p, we can map it to its bucket by bucket_index(p) = floor( p * (num_thresholds - 1) ) so we can use tf.math.unsorted_segment_sum() to update the buckets in one pass. Consider following example: y_true = [0, 0, 1, 1] y_pred = [0.1, 0.5, 0.3, 0.9] thresholds = [0.0, 0.5, 1.0] num_buckets = 2 # [0.0, 1.0], (1.0, 2.0] bucket_index(y_pred) = tf.math.floor(y_pred * num_buckets) = tf.math.floor([0.2, 1.0, 0.6, 1.8]) = [0, 0, 0, 1] # The meaning of this bucket is that if any of the label is true, # then 1 will be added to the corresponding bucket with the index. # Eg, if the label for 0.2 is true, then 1 will be added to bucket 0. If the # label for 1.8 is true, then 1 will be added to bucket 1. # # Note the second item "1.0" is floored to 0, since the value need to be # strictly larger than the bucket lower bound. # In the implementation, we use tf.math.ceil() - 1 to achieve this. tp_bucket_value = tf.math.unsorted_segment_sum(true_labels, bucket_indices, num_segments=num_thresholds) = [1, 1, 0] # For [1, 1, 0] here, it means there is 1 true value contributed by bucket 0, # and 1 value contributed by bucket 1. When we aggregate them to together, # the result become [a + b + c, b + c, c], since large thresholds will always # contribute to the value for smaller thresholds. true_positive = tf.math.cumsum(tp_bucket_value, reverse=True) = [2, 1, 0] This implementation exhibits a run time and space complexity of O(T + N), where T is the number of thresholds and N is the size of predictions. Metrics that rely on standard implementation instead exhibit a complexity of O(T * N). Args: variables_to_update: Dictionary with 'tp', 'fn', 'tn', 'fp' as valid keys and corresponding variables to update as values. y_true: A floating point `Tensor` whose shape matches `y_pred`. Will be cast to `bool`. y_pred: A floating point `Tensor` of arbitrary shape and whose values are in the range `[0, 1]`. thresholds: A sorted floating point `Tensor` with value in `[0, 1]`. It need to be evenly distributed (the diff between each element need to be the same). multi_label: Optional boolean indicating whether multidimensional prediction/labels should be treated as multilabel responses, or flattened into a single label. When True, the valus of `variables_to_update` must have a second dimension equal to the number of labels in y_true and y_pred, and those tensors must not be RaggedTensors. sample_weights: Optional `Tensor` whose rank is either 0, or the same rank as `y_true`, and must be broadcastable to `y_true` (i.e., all dimensions must be either `1`, or the same as the corresponding `y_true` dimension). label_weights: Optional tensor of non-negative weights for multilabel data. The weights are applied when calculating TP, FP, FN, and TN without explicit multilabel handling (i.e. when the data is to be flattened). thresholds_with_epsilon: Optional boolean indicating whether the leading and tailing thresholds has any epsilon added for floating point imprecisions. It will change how we handle the leading and tailing bucket. Returns: Update op. """ num_thresholds = thresholds.shape.as_list()[0] if sample_weights is None: sample_weights = 1.0 else: sample_weights = weights_broadcast_ops.broadcast_weights( math_ops.cast(sample_weights, dtype=y_pred.dtype), y_pred) if not multi_label: sample_weights = array_ops.reshape(sample_weights, [-1]) if label_weights is None: label_weights = 1.0 else: label_weights = array_ops.expand_dims(label_weights, 0) label_weights = weights_broadcast_ops.broadcast_weights(label_weights, y_pred) if not multi_label: label_weights = array_ops.reshape(label_weights, [-1]) weights = math_ops.multiply(sample_weights, label_weights) # We shouldn't need this, but in case there are predict value that is out of # the range of [0.0, 1.0] y_pred = clip_ops.clip_by_value(y_pred, clip_value_min=0.0, clip_value_max=1.0) y_true = math_ops.cast(math_ops.cast(y_true, dtypes.bool), y_true.dtype) if not multi_label: y_true = array_ops.reshape(y_true, [-1]) y_pred = array_ops.reshape(y_pred, [-1]) true_labels = math_ops.multiply(y_true, weights) false_labels = math_ops.multiply((1.0 - y_true), weights) # Compute the bucket indices for each prediction value. # Since the predict value has to be strictly greater than the thresholds, # eg, buckets like [0, 0.5], (0.5, 1], and 0.5 belongs to first bucket. # We have to use math.ceil(val) - 1 for the bucket. bucket_indices = math_ops.ceil(y_pred * (num_thresholds - 1)) - 1 if thresholds_with_epsilon: # In this case, the first bucket should actually take into account since # the any prediction between [0.0, 1.0] should be larger than the first # threshold. We change the bucket value from -1 to 0. bucket_indices = nn_ops.relu(bucket_indices) bucket_indices = math_ops.cast(bucket_indices, dtypes.int32) if multi_label: # We need to run bucket segment sum for each of the label class. In the # multi_label case, the rank of the label is 2. We first transpose it so # that the label dim becomes the first and we can parallel run though them. true_labels = array_ops.transpose_v2(true_labels) false_labels = array_ops.transpose_v2(false_labels) bucket_indices = array_ops.transpose_v2(bucket_indices) def gather_bucket(label_and_bucket_index): label, bucket_index = label_and_bucket_index[0], label_and_bucket_index[1] return math_ops.unsorted_segment_sum( data=label, segment_ids=bucket_index, num_segments=num_thresholds) tp_bucket_v = vectorized_map( gather_bucket, (true_labels, bucket_indices)) fp_bucket_v = vectorized_map( gather_bucket, (false_labels, bucket_indices)) tp = array_ops.transpose_v2( math_ops.cumsum(tp_bucket_v, reverse=True, axis=1)) fp = array_ops.transpose_v2( math_ops.cumsum(fp_bucket_v, reverse=True, axis=1)) else: tp_bucket_v = math_ops.unsorted_segment_sum( data=true_labels, segment_ids=bucket_indices, num_segments=num_thresholds) fp_bucket_v = math_ops.unsorted_segment_sum( data=false_labels, segment_ids=bucket_indices, num_segments=num_thresholds) tp = math_ops.cumsum(tp_bucket_v, reverse=True) fp = math_ops.cumsum(fp_bucket_v, reverse=True) # fn = sum(true_labels) - tp # tn = sum(false_labels) - fp if (ConfusionMatrix.TRUE_NEGATIVES in variables_to_update or ConfusionMatrix.FALSE_NEGATIVES in variables_to_update): if multi_label: total_true_labels = math_ops.reduce_sum(true_labels, axis=1) total_false_labels = math_ops.reduce_sum(false_labels, axis=1) else: total_true_labels = math_ops.reduce_sum(true_labels) total_false_labels = math_ops.reduce_sum(false_labels) update_ops = [] if ConfusionMatrix.TRUE_POSITIVES in variables_to_update: variable = variables_to_update[ConfusionMatrix.TRUE_POSITIVES] update_ops.append(variable.assign_add(tp)) if ConfusionMatrix.FALSE_POSITIVES in variables_to_update: variable = variables_to_update[ConfusionMatrix.FALSE_POSITIVES] update_ops.append(variable.assign_add(fp)) if ConfusionMatrix.TRUE_NEGATIVES in variables_to_update: variable = variables_to_update[ConfusionMatrix.TRUE_NEGATIVES] tn = total_false_labels - fp update_ops.append(variable.assign_add(tn)) if ConfusionMatrix.FALSE_NEGATIVES in variables_to_update: variable = variables_to_update[ConfusionMatrix.FALSE_NEGATIVES] fn = total_true_labels - tp update_ops.append(variable.assign_add(fn)) return control_flow_ops.group(update_ops)
def _update_confusion_matrix_variables(variables_to_update, y_true, y_pred, thresholds, sample_weight=None): """Returns op to update the given confusion matrix variables. For every pair of values in y_true and y_pred: true_positive: y_true == True and y_pred > thresholds false_negatives: y_true == True and y_pred <= thresholds true_negatives: y_true == False and y_pred <= thresholds false_positive: y_true == False and y_pred > thresholds The results will be weighted and added together. When multiple thresholds are provided, we will repeat the same for every threshold. For estimation of these metrics over a stream of data, the function creates an `update_op` operation that updates the given variables. If `sample_weight` is `None`, weights default to 1. Use weights of 0 to mask values. Args: variables_to_update: Dictionary with 'tp', 'fn', 'tn', 'fp' as valid keys and corresponding variables to update as values. y_true: A `Tensor` whose shape matches `y_pred`. Will be cast to `bool`. y_pred: A floating point `Tensor` of arbitrary shape and whose values are in the range `[0, 1]`. thresholds: A python list or tuple of float thresholds in `[0, 1]`. sample_weight: Optional `Tensor` whose rank is either 0, or the same rank as `y_true`, and must be broadcastable to `y_true` (i.e., all dimensions must be either `1`, or the same as the corresponding `y_true` dimension). Returns: Update op. Raises: ValueError: If `y_pred` and `y_true` have mismatched shapes, or if `sample_weight` is not `None` and its shape doesn't match `y_pred`, or if `variables_to_update` contains invalid keys. """ if variables_to_update is None: return y_pred.get_shape().assert_is_compatible_with(y_true.get_shape()) if not any(key for key in variables_to_update if key in list(_ConfusionMatrix)): raise ValueError( 'Please provide at least one valid confusion matrix ' 'variable to update. Valid variable key options are: "{}". ' 'Received: "{}"'.format(list(_ConfusionMatrix), variables_to_update.keys())) invalid_keys = [ key for key in variables_to_update if key not in list(_ConfusionMatrix) ] if invalid_keys: raise ValueError( 'Invalid keys: {}. Valid variable key options are: "{}"'.format( invalid_keys, list(_ConfusionMatrix))) with ops.control_dependencies([ check_ops.assert_greater_equal(y_pred, math_ops.cast(0.0, dtype=y_pred.dtype), message='predictions must be >= 0'), check_ops.assert_less_equal(y_pred, math_ops.cast(1.0, dtype=y_pred.dtype), message='predictions must be <= 1') ]): y_pred, y_true, sample_weight = squeeze_or_expand_dimensions( math_ops.cast(y_pred, dtype=dtypes.float32), math_ops.cast(y_true, dtype=dtypes.bool), sample_weight) num_thresholds = len(thresholds) num_predictions = array_ops.size(y_pred) # Reshape predictions and labels. predictions_2d = array_ops.reshape(y_pred, [1, -1]) labels_2d = array_ops.reshape(math_ops.cast(y_true, dtype=dtypes.bool), [1, -1]) # Tile the thresholds for every prediction. thresh_tiled = array_ops.tile( array_ops.expand_dims(array_ops.constant(thresholds), 1), array_ops.stack([1, num_predictions])) # Tile the predictions for every threshold. preds_tiled = array_ops.tile(predictions_2d, [num_thresholds, 1]) # Compare predictions and threshold. pred_is_pos = math_ops.greater(preds_tiled, thresh_tiled) # Tile labels by number of thresholds label_is_pos = array_ops.tile(labels_2d, [num_thresholds, 1]) if sample_weight is not None: weights = weights_broadcast_ops.broadcast_weights( math_ops.cast(sample_weight, dtype=dtypes.float32), y_pred) weights_tiled = array_ops.tile(array_ops.reshape(weights, [1, -1]), [num_thresholds, 1]) else: weights_tiled = None update_ops = [] def weighted_assign_add(label, pred, weights, var): label_and_pred = math_ops.cast(math_ops.logical_and(label, pred), dtype=dtypes.float32) if weights is not None: label_and_pred *= weights return state_ops.assign_add(var, math_ops.reduce_sum(label_and_pred, 1)) loop_vars = { _ConfusionMatrix.TRUE_POSITIVES: (label_is_pos, pred_is_pos), } update_tn = _ConfusionMatrix.TRUE_NEGATIVES in variables_to_update update_fp = _ConfusionMatrix.FALSE_POSITIVES in variables_to_update update_fn = _ConfusionMatrix.FALSE_NEGATIVES in variables_to_update if update_fn or update_tn: pred_is_neg = math_ops.logical_not(pred_is_pos) loop_vars[_ConfusionMatrix.FALSE_NEGATIVES] = (label_is_pos, pred_is_neg) if update_fp or update_tn: label_is_neg = math_ops.logical_not(label_is_pos) loop_vars[_ConfusionMatrix.FALSE_POSITIVES] = (label_is_neg, pred_is_pos) if update_tn: loop_vars[_ConfusionMatrix.TRUE_NEGATIVES] = (label_is_neg, pred_is_neg) for matrix_cond, (label, pred) in loop_vars.items(): if matrix_cond in variables_to_update: update_ops.append( weighted_assign_add(label, pred, weights_tiled, variables_to_update[matrix_cond])) return control_flow_ops.group(update_ops)
def update_metric_with_broadcast_weights(eval_metric, values, weights): values = tf.cast(values, dtype=tf.dtypes.float32) if weights is not None: weights = weights_broadcast_ops.broadcast_weights(weights, values) eval_metric.update_state(values=values, sample_weight=weights)