# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function

from marshmallow import fields, validate

from polyaxon_schemas.base import BaseConfig, BaseMultiSchema, BaseSchema
from polyaxon_schemas.utils import Tensor

# pylint:disable=too-many-lines


class BaseMetricSchema(BaseSchema):
    input_layer = Tensor(allow_none=True)
    output_layer = Tensor(allow_none=True)
    weights = fields.Float(allow_none=True)
    metrics_collections = fields.List(fields.Str(), allow_none=True)
    updates_collections = fields.List(fields.Str(), allow_none=True)
    name = fields.Str(allow_none=True)

    @staticmethod
    def schema_config():
        return BaseMetricConfig


class BaseMetricConfig(BaseConfig):
    REDUCED_ATTRIBUTES = ['input_layer', 'output_layer', 'weights',
                          'metrics_collections', 'updates_collections', 'name']

    def __init__(self,
                 input_layer=None,
                 output_layer=None,
                 weights=None,
                 metrics_collections=None,
                 updates_collections=None,
                 name=None):
        self.input_layer = input_layer
        self.output_layer = output_layer
        self.weights = weights
        self.metrics_collections = metrics_collections
        self.updates_collections = updates_collections
        self.name = name


class BaseTensorMetricSchema(BaseSchema):
    values = Tensor()
    weights = fields.Float(allow_none=True)
    metrics_collections = fields.List(fields.Str(), allow_none=True)
    updates_collections = fields.List(fields.Str(), allow_none=True)
    name = fields.Str(allow_none=True)

    @staticmethod
    def schema_config():
        return BaseTensorMetricConfig


class BaseTensorMetricConfig(BaseConfig):
    REDUCED_ATTRIBUTES = ['weights', 'metrics_collections', 'updates_collections', 'name']

    def __init__(self,
                 values,
                 weights=None,
                 metrics_collections=None,
                 updates_collections=None,
                 name=None):
        self.values = values
        self.weights = weights
        self.metrics_collections = metrics_collections
        self.updates_collections = updates_collections
        self.name = name


class TruePositivesSchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return TruePositivesConfig


class TruePositivesConfig(BaseMetricConfig):
    """Sum the weights of true_positives.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        weights: Optional `Tensor` whose rank is either 0, or the same rank as
            `labels`, and must be broadcastable to `labels` (i.e., all dimensions
            must be either `1`, or the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that `mean`
            should be added to.
        updates_collections: An optional list of collections that `update_op`
            should be added to.
        name: An optional variable_scope name.

    Returns:
        value_tensor: A `Tensor` representing the current value of the metric.
        update_op: An operation that accumulates the error from a batch of data.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - TruePositives
      # other model properties
    ```
    """
    IDENTIFIER = 'TruePositives'
    SCHEMA = TruePositivesSchema


class TrueNegativesSchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return TrueNegativesConfig


class TrueNegativesConfig(BaseMetricConfig):
    """Sum the weights of true_negatives.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        weights: Optional `Tensor` whose rank is either 0, or the same rank as
            `labels`, and must be broadcastable to `labels` (i.e., all dimensions
            must be either `1`, or the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that `mean`
            should be added to.
        updates_collections: An optional list of collections that `update_op`
            should be added to.
        name: An optional variable_scope name.

    Returns:
        value_tensor: A `Tensor` representing the current value of the metric.
        update_op: An operation that accumulates the error from a batch of data.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
           `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - TruePositives
        - TrueNegatives:
            input_layer: labels_1
            output_layer: dense_out_3
      # other model properties
    ```
    """
    IDENTIFIER = 'TrueNegatives'
    SCHEMA = TrueNegativesSchema


class FalsePositivesSchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return FalsePositivesConfig


class FalsePositivesConfig(BaseMetricConfig):
    """Sum the weights of false positives.
    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        weights: Optional `Tensor` whose rank is either 0, or the same rank as
            `labels`, and must be broadcastable to `labels` (i.e., all dimensions
            must be either `1`, or the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that `mean`
            should be added to.
        updates_collections: An optional list of collections that `update_op`
            should be added to.
        name: An optional variable_scope name.

    Returns:
        value_tensor: A `Tensor` representing the current value of the metric.
        update_op: An operation that accumulates the error from a batch of data.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - FalsePositives
      # other model properties
    ```
    """
    IDENTIFIER = 'FalsePositives'
    SCHEMA = FalsePositivesSchema


class FalseNegativesSchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return FalseNegativesConfig


class FalseNegativesConfig(BaseMetricConfig):
    """Computes the total number of false negatives.
    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        weights: Optional `Tensor` whose rank is either 0, or the same rank as
            `labels`, and must be broadcastable to `labels` (i.e., all dimensions
            must be either `1`, or the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that `mean`
            should be added to.
        updates_collections: An optional list of collections that `update_op`
            should be added to.
        name: An optional variable_scope name.

    Returns:
        value_tensor: A `Tensor` representing the current value of the metric.
        update_op: An operation that accumulates the error from a batch of data.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - TruePositives
        - TrueNegatives
        - FalseNegatives:
            input_layer: labels_1
            output_layer: dense_out_3
      # other model properties
    ```
    """
    IDENTIFIER = 'FalseNegatives'
    SCHEMA = FalseNegativesSchema


class MeanSchema(BaseTensorMetricSchema):

    @staticmethod
    def schema_config():
        return MeanConfig


class MeanConfig(BaseTensorMetricConfig):
    """Computes the (weighted) mean of the given values.

    The `streaming_mean` function creates two local variables, `total` and `count`
    that are used to compute the average of `values`. This average is ultimately
    returned as `mean` which is an idempotent operation that simply divides
    `total` by `count`.

    For estimation of the metric  over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the `mean`.
    `update_op` increments `total` with the reduced sum of the product of `values`
    and `weights`, and it increments `count` with the reduced sum of `weights`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        values: A `Tensor` of arbitrary dimensions.
        weights: `Tensor` whose rank is either 0, or the same rank as `values`, and
            must be broadcastable to `values` (i.e., all dimensions must be either
            `1`, or the same as the corresponding `values` dimension).
        metrics_collections: An optional list of collections that `mean`
            should be added to.
        updates_collections: An optional list of collections that `update_op`
            should be added to.
        name: An optional variable_scope name.

    Returns:
        mean: A `Tensor` representing the current mean, the value of `total` divided
          by `count`.
        update_op: An operation that increments the `total` and `count` variables
          appropriately and whose value matches `mean`.

    Raises:
        ValueError: If `weights` is not `None` and its shape doesn't match `values`,
            or if either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - Mean:
            values: layer1
      # other model properties
    ```
    """
    IDENTIFIER = 'Mean'
    SCHEMA = MeanSchema


class MeanTensorSchema(BaseTensorMetricSchema):

    @staticmethod
    def schema_config():
        return MeanTensorConfig


class MeanTensorConfig(BaseTensorMetricConfig):
    """Computes the element-wise (weighted) mean of the given tensors.

    In contrast to the `streaming_mean` function which returns a scalar with the
    mean,  this function returns an average tensor with the same shape as the
    input tensors.

    The `streaming_mean_tensor` function creates two local variables,
    `total_tensor` and `count_tensor` that are used to compute the average of
    `values`. This average is ultimately returned as `mean` which is an idempotent
    operation that simply divides `total` by `count`.

    For estimation of the metric  over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the `mean`.
    `update_op` increments `total` with the reduced sum of the product of `values`
    and `weights`, and it increments `count` with the reduced sum of `weights`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        values: A `Tensor` of arbitrary dimensions.
        weights: `Tensor` whose rank is either 0, or the same rank as `values`, and
            must be broadcastable to `values` (i.e., all dimensions must be either
            `1`, or the same as the corresponding `values` dimension).
        metrics_collections: An optional list of collections that `mean`
            should be added to.
        updates_collections: An optional list of collections that `update_op`
            should be added to.
        name: An optional variable_scope name.

    Returns:
      mean: A float `Tensor` representing the current mean, the value of `total`
        divided by `count`.
      update_op: An operation that increments the `total` and `count` variables
        appropriately and whose value matches `mean`.

    Raises:
        ValueError: If `weights` is not `None` and its shape doesn't match `values`,
            or if either `metrics_collections` or `updates_collections` are not a list or tuple.

     Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - MeanTensor:
            values: layer1
      # other model properties
    ```
    """
    IDENTIFIER = 'MeanTensor'
    SCHEMA = MeanTensorSchema


class AccuracySchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return AccuracyConfig


class AccuracyConfig(BaseMetricConfig):
    """Calculates how often `predictions` matches `labels`.

    The `streaming_accuracy` function creates two local variables, `total` and
    `count` that are used to compute the frequency with which `predictions`
    matches `labels`. This frequency is ultimately returned as `accuracy`: an
    idempotent operation that simply divides `total` by `count`.

    For estimation of the metric  over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the `accuracy`.
    Internally, an `is_correct` operation computes a `Tensor` with elements 1.0
    where the corresponding elements of `predictions` and `labels` match and 0.0
    otherwise. Then `update_op` increments `total` with the reduced sum of the
    product of `weights` and `is_correct`, and it increments `count` with the
    reduced sum of `weights`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        weights: Optional `Tensor` whose rank is either 0, or the same rank as
            `labels`, and must be broadcastable to `labels` (i.e., all dimensions
            must be either `1`, or the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that `mean`
            should be added to.
        updates_collections: An optional list of collections that `update_op`
            should be added to.
        name: An optional variable_scope name.

    Returns:
        accuracy: A `Tensor` representing the accuracy, the value of `total` divided by `count`.
        update_op: An operation that increments the `total` and `count` variables
            appropriately and whose value matches `accuracy`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - Precision
        - Accuracy:
            input_layer: labels_1
            output_layer: dense_out_3
      # other model properties
    ```
    """
    IDENTIFIER = 'Accuracy'
    SCHEMA = AccuracySchema


class PrecisionSchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return PrecisionConfig


class PrecisionConfig(BaseMetricConfig):
    """Computes the precision of the predictions with respect to the labels.

    The `streaming_precision` function creates two local variables,
    `true_positives` and `false_positives`, that are used to compute the
    precision. This value is ultimately returned as `precision`, an idempotent
    operation that simply divides `true_positives` by the sum of `true_positives`
    and `false_positives`.

    For estimation of the metric  over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `precision`. `update_op` weights each prediction by the corresponding value in
    `weights`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        weights: Optional `Tensor` whose rank is either 0, or the same rank as
            `labels`, and must be broadcastable to `labels` (i.e., all dimensions
            must be either `1`, or the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that `mean`
            should be added to.
        updates_collections: An optional list of collections that `update_op`
            should be added to.
        name: An optional variable_scope name.

    Returns:
        precision: Scalar float `Tensor` with the value of `true_positives`
            divided by the sum of `true_positives` and `false_positives`.
        update_op: `Operation` that increments `true_positives` and
            `false_positives` variables appropriately and whose value matches `precision`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - Precision
        - Accuracy
      # other model properties
    ```
    """
    IDENTIFIER = 'Precision'
    SCHEMA = PrecisionSchema


class RecallSchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return RecallConfig


class RecallConfig(BaseMetricConfig):
    """Computes the recall of the predictions with respect to the labels.

    The `streaming_recall` function creates two local variables, `true_positives`
    and `false_negatives`, that are used to compute the recall. This value is
    ultimately returned as `recall`, an idempotent operation that simply divides
    `true_positives` by the sum of `true_positives`  and `false_negatives`.

    For estimation of the metric  over a stream of data, the function creates an
    `update_op` that updates these variables and returns the `recall`. `update_op`
    weights each prediction by the corresponding value in `weights`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        weights: Optional `Tensor` whose rank is either 0, or the same rank as
            `labels`, and must be broadcastable to `labels` (i.e., all dimensions
            must be either `1`, or the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that `mean`
            should be added to.
        updates_collections: An optional list of collections that `update_op`
            should be added to.
        name: An optional variable_scope name.

    Returns:
        recall: Scalar float `Tensor` with the value of `true_positives` divided
            by the sum of `true_positives` and `false_negatives`.
        update_op: `Operation` that increments `true_positives` and
            `false_negatives` variables appropriately and whose value matches `recall`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - Precision
        - Accuracy
        - Recall
      # other model properties
    ```
    """
    IDENTIFIER = 'Recall'
    SCHEMA = RecallSchema


class AUCSchema(BaseMetricSchema):
    num_thresholds = fields.Int(allow_none=True)
    curve = fields.Str(allow_none=True)

    @staticmethod
    def schema_config():
        return AUCConfig


class AUCConfig(BaseMetricConfig):
    """Computes the approximate AUC via a Riemann sum.

    The `streaming_auc` function creates four local variables, `true_positives`,
    `true_negatives`, `false_positives` and `false_negatives` that are used to
    compute the AUC. To discretize the AUC curve, a linearly spaced set of
    thresholds is used to compute pairs of recall and precision values. The area
    under the ROC-curve is therefore computed using the height of the recall
    values by the false positive rate, while the area under the PR-curve is the
    computed using the height of the precision values by the recall.

    This value is ultimately returned as `auc`, an idempotent operation that
    computes the area under a discretized curve of precision versus recall values
    (computed using the aforementioned variables). The `num_thresholds` variable
    controls the degree of discretization with larger numbers of thresholds more
    closely approximating the true AUC. The quality of the approximation may vary
    dramatically depending on `num_thresholds`.

    For best results, `predictions` should be distributed approximately uniformly
    in the range [0, 1] and not peaked around 0 or 1. The quality of the AUC
    approximation may be poor if this is not the case.

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the `auc`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        weights: Optional `Tensor` whose rank is either 0, or the same rank as
            `labels`, and must be broadcastable to `labels` (i.e., all dimensions
            must be either `1`, or the same as the corresponding `labels` dimension).
        num_thresholds: The number of thresholds to use when discretizing the roc curve.
        metrics_collections: An optional list of collections that `mean`
            should be added to.
        updates_collections: An optional list of collections that `update_op`
            should be added to.
        curve: Specifies the name of the curve to be computed, 'ROC' [default] or
            'PR' for the Precision-Recall-curve.
        name: An optional variable_scope name.

    Returns:
      auc: A scalar `Tensor` representing the current area-under-curve.
      update_op: An operation that increments the `true_positives`,
        `true_negatives`, `false_positives` and `false_negatives` variables
        appropriately and whose value matches `auc`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - AUC
      # other model properties
    ```
    """
    IDENTIFIER = 'AUC'
    SCHEMA = AUCSchema

    def __init__(self, num_thresholds=200, curve='ROC', **kwargs):
        super(AUCConfig, self).__init__(**kwargs)
        self.num_thresholds = num_thresholds
        self.curve = curve


class SpecificityAtSensitivitySchema(BaseMetricSchema):
    sensitivity = fields.Float(validate=validate.Range(min=0., max=1.))
    num_thresholds = fields.Int(allow_none=True)

    @staticmethod
    def schema_config():
        return SpecificityAtSensitivityConfig


class SpecificityAtSensitivityConfig(BaseMetricConfig):
    """Computes the specificity at a given sensitivity.

    The `streaming_specificity_at_sensitivity` function creates four local
    variables, `true_positives`, `true_negatives`, `false_positives` and
    `false_negatives` that are used to compute the specificity at the given
    sensitivity value. The threshold for the given sensitivity value is computed
    and used to evaluate the corresponding specificity.

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `specificity`. `update_op` increments the `true_positives`, `true_negatives`,
    `false_positives` and `false_negatives` counts with the weight of each case
    found in the `predictions` and `labels`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    For additional information about specificity and sensitivity, see the
    following: https://en.wikipedia.org/wiki/Sensitivity_and_specificity

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        sensitivity: A scalar value in range `[0, 1]`.
        weights: `Tensor` whose rank is either 0, or the same rank as `labels`, and
            must be broadcastable to `labels` (i.e., all dimensions must be either
            `1`, or the same as the corresponding `labels` dimension).
        num_thresholds: The number of thresholds to use for matching the given sensitivity.
        metrics_collections: An optional list of collections that `specificity`
            should be added to.
        updates_collections: An optional list of collections that `update_op` should
            be added to.
        name: An optional variable_scope name.

    Returns:
        specificity: A scalar `Tensor` representing the specificity at the given
            `specificity` value.
        update_op: An operation that increments the `true_positives`,
            `true_negatives`, `false_positives` and `false_negatives` variables
            appropriately and whose value matches `specificity`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            `sensitivity` is not between 0 and 1, or if either `metrics_collections`
            or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - SpecificityAtSensitivity
      # other model properties
    ```
    """
    IDENTIFIER = 'SpecificityAtSensitivity'
    SCHEMA = SpecificityAtSensitivitySchema

    def __init__(self, sensitivity, num_thresholds=200, **kwargs):
        self.sensitivity = sensitivity
        self.num_thresholds = num_thresholds
        super(SpecificityAtSensitivityConfig, self).__init__(**kwargs)


class SensitivityAtSpecificitySchema(BaseMetricSchema):
    specificity = fields.Float(validate=validate.Range(min=0., max=1.))
    num_thresholds = fields.Int(allow_none=True)

    @staticmethod
    def schema_config():
        return SensitivityAtSpecificityConfig


class SensitivityAtSpecificityConfig(BaseMetricConfig):
    """Computes the specificity at a given sensitivity.

    The `streaming_sensitivity_at_specificity` function creates four local
    variables, `true_positives`, `true_negatives`, `false_positives` and
    `false_negatives` that are used to compute the sensitivity at the given
    specificity value. The threshold for the given specificity value is computed
    and used to evaluate the corresponding sensitivity.

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `sensitivity`. `update_op` increments the `true_positives`, `true_negatives`,
    `false_positives` and `false_negatives` counts with the weight of each case
    found in the `predictions` and `labels`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    For additional information about specificity and sensitivity, see the
    following: https://en.wikipedia.org/wiki/Sensitivity_and_specificity

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        specificity: A scalar value in range `[0, 1]`.
        weights: `Tensor` whose rank is either 0, or the same rank as `labels`, and
            must be broadcastable to `labels` (i.e., all dimensions must be either
            `1`, or the same as the corresponding `labels` dimension).
        num_thresholds: The number of thresholds to use for matching the given sensitivity.
        metrics_collections: An optional list of collections that `specificity`
            should be added to.
        updates_collections: An optional list of collections that `update_op` should
            be added to.
        name: An optional variable_scope name.

    Returns:
        sensitivity: A scalar `Tensor` representing the sensitivity at the given
            `specificity` value.
        update_op: An operation that increments the `true_positives`,
            `true_negatives`, `false_positives` and `false_negatives` variables
            appropriately and whose value matches `sensitivity`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            `specificity` is not between 0 and 1, or if either `metrics_collections`
            or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - SpecificityAtSensitivity
      # other model properties
    ```
    """
    IDENTIFIER = 'SensitivityAtSpecificity'
    SCHEMA = SensitivityAtSpecificitySchema

    def __init__(self, specificity, num_thresholds=200, **kwargs):
        self.specificity = specificity
        self.num_thresholds = num_thresholds
        super(SensitivityAtSpecificityConfig, self).__init__(**kwargs)


class PrecisionAtThresholdsSchema(BaseMetricSchema):
    thresholds = fields.List(fields.Float(validate=validate.Range(min=0., max=1.)))

    @staticmethod
    def schema_config():
        return PrecisionAtThresholdsConfig


class PrecisionAtThresholdsConfig(BaseMetricConfig):
    """Computes precision values for different `thresholds` on `predictions`.

    The `streaming_precision_at_thresholds` function creates four local variables,
    `true_positives`, `true_negatives`, `false_positives` and `false_negatives`
    for various values of thresholds. `precision[i]` is defined as the total
    weight of values in `predictions` above `thresholds[i]` whose corresponding
    entry in `labels` is `True`, divided by the total weight of values in
    `predictions` above `thresholds[i]` (`true_positives[i] / (true_positives[i] +
    false_positives[i])`).

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `precision`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        thresholds: A python list or tuple of float thresholds in `[0, 1]`.
        weights: `Tensor` whose rank is either 0, or the same rank as `labels`, and
            must be broadcastable to `labels` (i.e., all dimensions must be either
            `1`, or the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that `specificity`
            should be added to.
        updates_collections: An optional list of collections that `update_op` should
            be added to.
        name: An optional variable_scope name.

    Returns:
        precision: A float `Tensor` of shape `[len(thresholds)]`.
        update_op: An operation that increments the `true_positives`,
            `true_negatives`, `false_positives` and `false_negatives` variables that
            are used in the computation of `precision`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - PrecisionAtThresholds: {thresholds: [0.1, 0.1, 0.1]}
      # other model properties
    ```
    """
    IDENTIFIER = 'PrecisionAtThresholds'
    SCHEMA = PrecisionAtThresholdsSchema

    def __init__(self, thresholds, **kwargs):
        self.thresholds = thresholds
        super(PrecisionAtThresholdsConfig, self).__init__(**kwargs)


class RecallAtThresholdsSchema(BaseMetricSchema):
    thresholds = fields.List(fields.Float(validate=validate.Range(min=0., max=1.)))

    @staticmethod
    def schema_config():
        return RecallAtThresholdsConfig


class RecallAtThresholdsConfig(BaseMetricConfig):
    """Computes various recall values for different `thresholds` on `predictions`.

    The `streaming_recall_at_thresholds` function creates four local variables,
    `true_positives`, `true_negatives`, `false_positives` and `false_negatives`
    for various values of thresholds. `recall[i]` is defined as the total weight
    of values in `predictions` above `thresholds[i]` whose corresponding entry in
    `labels` is `True`, divided by the total weight of `True` values in `labels`
    (`true_positives[i] / (true_positives[i] + false_negatives[i])`).

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the `recall`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: The input true values layer, defaults to labels. Will
          be cast to `bool`.
        output_layer: The output layer generated by the network,
            default to last layer of the network. Will be cast to `bool`.
            If the network has multiple output, you should specify which layer to use.
        thresholds: A python list or tuple of float thresholds in `[0, 1]`.
        weights: `Tensor` whose rank is either 0, or the same rank as `labels`, and
            must be broadcastable to `labels` (i.e., all dimensions must be either
            `1`, or the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that `specificity`
            should be added to.
        updates_collections: An optional list of collections that `update_op` should
            be added to.
        name: An optional variable_scope name.

    Returns:
        precision: A float `Tensor` of shape `[len(thresholds)]`.
        update_op: An operation that increments the `true_positives`,
            `true_negatives`, `false_positives` and `false_negatives` variables that
            are used in the computation of `precision`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - RecallAtThresholds: {thresholds: [0.1, 0.1, 0.1]}
      # other model properties
    ```
    """
    IDENTIFIER = 'RecallAtThresholds'
    SCHEMA = RecallAtThresholdsSchema

    def __init__(self, thresholds, **kwargs):
        self.thresholds = thresholds
        super(RecallAtThresholdsConfig, self).__init__(**kwargs)


class SparseRecallAtKSchema(BaseMetricSchema):
    k = fields.Int()
    class_id = fields.Int(allow_none=True)

    @staticmethod
    def schema_config():
        return SparseRecallAtKConfig


class SparseRecallAtKConfig(BaseMetricConfig):
    """Computes recall@k of the predictions with respect to sparse labels.

    If `class_id` is not specified, we'll calculate recall as the ratio of true
        positives (i.e., correct predictions, items in the top `k` highest
        `predictions` that are found in the corresponding row in `labels`) to
        actual positives (the full `labels` row).
    If `class_id` is specified, we calculate recall by considering only the rows
        in the batch for which `class_id` is in `labels`, and computing the
        fraction of them for which `class_id` is in the corresponding row in
        `labels`.

    `streaming_sparse_recall_at_k` creates two local variables,
    `true_positive_at_<k>` and `false_negative_at_<k>`, that are used to compute
    the recall_at_k frequency. This frequency is ultimately returned as
    `recall_at_<k>`: an idempotent operation that simply divides
    `true_positive_at_<k>` by total (`true_positive_at_<k>` +
    `false_negative_at_<k>`).

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `recall_at_<k>`. Internally, a `top_k` operation computes a `Tensor`
    indicating the top `k` `predictions`. Set operations applied to `top_k` and
    `labels` calculate the true positives and false negatives weighted by
    `weights`. Then `update_op` increments `true_positive_at_<k>` and
    `false_negative_at_<k>` using these values.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: `int64` `Tensor` or `SparseTensor` with shape
            [D1, ... DN, num_labels], where N >= 1 and num_labels is the number of
            target classes for the associated prediction. Commonly, N=1 and `labels`
            has shape [batch_size, num_labels]. [D1, ... DN] must match `predictions`.
            Values should be in range [0, num_classes), where num_classes is the last
            dimension of `predictions`. Values outside this range always count
            towards `false_negative_at_<k>`.
        output_layer: Float `Tensor` with shape [D1, ... DN, num_classes] where
            N >= 1. Commonly, N=1 and predictions has shape [batch size, num_classes].
            The final dimension contains the logit values for each class. [D1, ... DN]
            must match `labels`.
        k: Integer, k for @k metric.
        class_id: Integer class ID for which we want binary metrics. This should be
            in range [0, num_classes), where num_classes is the last dimension of
            `predictions`. If class_id is outside this range, the method returns NAN.
        weights: `Tensor` whose rank is either 0, or n-1, where n is the rank of
            `labels`. If the latter, it must be broadcastable to `labels` (i.e., all
            dimensions must be either `1`, or the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that values should be added to.
        updates_collections: An optional list of collections that updates should be added to.
        name: Name of new update operation, and namespace for other dependent ops.

    Returns:
        recall: Scalar `float64` `Tensor` with the value of `true_positives` divided
            by the sum of `true_positives` and `false_negatives`.
        update_op: `Operation` that increments `true_positives` and
            `false_negatives` variables appropriately, and whose value matches `recall`.

    Raises:
        ValueError: If `weights` is not `None` and its shape doesn't match
        `predictions`, or if either `metrics_collections` or `updates_collections`
        are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - SparseRecallAtK: {k: 2}
      # other model properties
    ```
    """
    IDENTIFIER = 'SparseRecallAtK'
    SCHEMA = SparseRecallAtKSchema

    def __init__(self, k, class_id=None, **kwargs):
        self.k = k
        self.class_id = class_id
        super(SparseRecallAtKConfig, self).__init__(**kwargs)


class SparsePrecisionAtKSchema(BaseMetricSchema):
    k = fields.Int()
    class_id = fields.Int(allow_none=True)

    @staticmethod
    def schema_config():
        return SparsePrecisionAtKConfig


class SparsePrecisionAtKConfig(BaseMetricConfig):
    """Computes precision@k of the predictions with respect to sparse labels.

    If `class_id` is not specified, we calculate precision as the ratio of true
        positives (i.e., correct predictions, items in the top `k` highest
        `predictions` that are found in the corresponding row in `labels`) to
        positives (all top `k` `predictions`).
    If `class_id` is specified, we calculate precision by considering only the
        rows in the batch for which `class_id` is in the top `k` highest
        `predictions`, and computing the fraction of them for which `class_id` is
        in the corresponding row in `labels`.

    We expect precision to decrease as `k` increases.

    `streaming_sparse_precision_at_k` creates two local variables,
    `true_positive_at_<k>` and `false_positive_at_<k>`, that are used to compute
    the precision@k frequency. This frequency is ultimately returned as
    `precision_at_<k>`: an idempotent operation that simply divides
    `true_positive_at_<k>` by total (`true_positive_at_<k>` +
    `false_positive_at_<k>`).

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `precision_at_<k>`. Internally, a `top_k` operation computes a `Tensor`
    indicating the top `k` `predictions`. Set operations applied to `top_k` and
    `labels` calculate the true positives and false positives weighted by
    `weights`. Then `update_op` increments `true_positive_at_<k>` and
    `false_positive_at_<k>` using these values.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: `int64` `Tensor` or `SparseTensor` with shape
            [D1, ... DN, num_labels], where N >= 1 and num_labels is the number of
            target classes for the associated prediction. Commonly, N=1 and `labels`
            has shape [batch_size, num_labels]. [D1, ... DN] must match
            `predictions`. Values should be in range [0, num_classes), where
            num_classes is the last dimension of `predictions`. Values outside this
            range are ignored.
        output_layer: Float `Tensor` with shape [D1, ... DN, num_classes] where
            N >= 1. Commonly, N=1 and predictions has shape [batch size, num_classes].
            The final dimension contains the logit values for each class. [D1, ... DN]
            must match `labels`.
        k: Integer, k for @k metric.
        class_id: Integer class ID for which we want binary metrics. This should be
            in range [0, num_classes], where num_classes is the last dimension of
            `predictions`. If `class_id` is outside this range, the method returns NAN.
        weights: `Tensor` whose rank is either 0, or n-1, where n is the rank of
            `labels`. If the latter, it must be broadcastable to `labels` (i.e., all
            dimensions must be either `1`, or the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that values should
            be added to.
        updates_collections: An optional list of collections that updates should be added to.
        name: Name of new update operation, and namespace for other dependent ops.

    Returns:
        precision: Scalar `float64` `Tensor` with the value of `true_positives`
            divided by the sum of `true_positives` and `false_positives`.
        update_op: `Operation` that increments `true_positives` and
            `false_positives` variables appropriately, and whose value matches `precision`.

    Raises:
        ValueError: If `weights` is not `None` and its shape doesn't match
            `predictions`, or if either `metrics_collections` or `updates_collections`
            are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - SparsePrecisionAtK: {k: 2}
      # other model properties
    ```
    """
    IDENTIFIER = 'SparsePrecisionAtK'
    SCHEMA = SparsePrecisionAtKSchema

    def __init__(self, k, class_id, **kwargs):
        self.k = k
        self.class_id = class_id
        super(SparsePrecisionAtKConfig, self).__init__(**kwargs)


class MeanAbsoluteErrorSchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return MeanAbsoluteErrorConfig


class MeanAbsoluteErrorConfig(BaseMetricConfig):
    """Computes the mean absolute error between the labels and predictions.

    The `streaming_mean_absolute_error` function creates two local variables,
    `total` and `count` that are used to compute the mean absolute error. This
    average is weighted by `weights`, and it is ultimately returned as
    `mean_absolute_error`: an idempotent operation that simply divides `total` by
    `count`.

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `mean_absolute_error`. Internally, an `absolute_errors` operation computes the
    absolute value of the differences between `predictions` and `labels`. Then
    `update_op` increments `total` with the reduced sum of the product of
    `weights` and `absolute_errors`, and it increments `count` with the reduced
    sum of `weights`

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: A `Tensor` of the same shape as `predictions`.
        output_layer: A `Tensor` of arbitrary shape.
        weights: Optional `Tensor` indicating the frequency with which an example is
            sampled. Rank must be 0, or the same rank as `labels`, and must be
            broadcastable to `labels` (i.e., all dimensions must be either `1`, or
            the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that
            `mean_absolute_error` should be added to.
        updates_collections: An optional list of collections that `update_op` should be added to.
        name: An optional variable_scope name.

    Returns:
        mean_absolute_error: A `Tensor` representing the current mean, the value of
            `total` divided by `count`.
        update_op: An operation that increments the `total` and `count` variables
            appropriately and whose value matches `mean_absolute_error`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - MeanAbsoluteError
      # other model properties
    ```
    """
    IDENTIFIER = 'MeanAbsoluteError'
    SCHEMA = MeanAbsoluteErrorSchema


class MeanRelativeErrorSchema(BaseMetricSchema):
    normalizer = fields.Str()  # name of the normalizer tensor

    @staticmethod
    def schema_config():
        return MeanRelativeErrorConfig


class MeanRelativeErrorConfig(BaseMetricConfig):
    """Computes the mean relative error by normalizing with the given values.

    The `streaming_mean_relative_error` function creates two local variables,
    `total` and `count` that are used to compute the mean relative absolute error.
    This average is weighted by `weights`, and it is ultimately returned as
    `mean_relative_error`: an idempotent operation that simply divides `total` by
    `count`.

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `mean_reative_error`. Internally, a `relative_errors` operation divides the
    absolute value of the differences between `predictions` and `labels` by the
    `normalizer`. Then `update_op` increments `total` with the reduced sum of the
    product of `weights` and `relative_errors`, and it increments `count` with the
    reduced sum of `weights`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: A `Tensor` of the same shape as `predictions`.
        output_layer: A `Tensor` of arbitrary shape.
        normalizer: A `Tensor` of the same shape as `predictions`.
        weights: Optional `Tensor` indicating the frequency with which an example is
            sampled. Rank must be 0, or the same rank as `labels`, and must be
            broadcastable to `labels` (i.e., all dimensions must be either `1`, or
            the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that
            `mean_relative_error` should be added to.
        updates_collections: An optional list of collections that `update_op` should be added to.
        name: An optional variable_scope name.

    Returns:
        mean_relative_error: A `Tensor` representing the current mean, the value of
            `total` divided by `count`.
        update_op: An operation that increments the `total` and `count` variables
            appropriately and whose value matches `mean_relative_error`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - MeanRelativeError
      # other model properties
    ```
    """
    IDENTIFIER = 'MeanRelativeError'
    SCHEMA = MeanRelativeErrorSchema

    def __init__(self, normalizer, **kwargs):
        self.normalizer = normalizer
        super(MeanRelativeErrorConfig, self).__init__(**kwargs)


class MeanSquaredErrorSchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return MeanSquaredErrorConfig


class MeanSquaredErrorConfig(BaseMetricConfig):
    """Computes the mean squared error between the labels and predictions.

    The `streaming_mean_squared_error` function creates two local variables,
    `total` and `count` that are used to compute the mean squared error.
    This average is weighted by `weights`, and it is ultimately returned as
    `mean_squared_error`: an idempotent operation that simply divides `total` by
    `count`.

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `mean_squared_error`. Internally, a `squared_error` operation computes the
    element-wise square of the difference between `predictions` and `labels`. Then
    `update_op` increments `total` with the reduced sum of the product of
    `weights` and `squared_error`, and it increments `count` with the reduced sum
    of `weights`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: A `Tensor` of the same shape as `predictions`.
        output_layer: A `Tensor` of arbitrary shape.
        weights: Optional `Tensor` indicating the frequency with which an example is
            sampled. Rank must be 0, or the same rank as `labels`, and must be
            broadcastable to `labels` (i.e., all dimensions must be either `1`, or
            the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that
            `mean_squared_error` should be added to.
        updates_collections: An optional list of collections that `update_op` should
            be added to.
        name: An optional variable_scope name.

    Returns:
        mean_squared_error: A `Tensor` representing the current mean, the value of
            `total` divided by `count`.
        update_op: An operation that increments the `total` and `count` variables
            appropriately and whose value matches `mean_squared_error`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - MeanSquaredError
      # other model properties
    ```
    """
    IDENTIFIER = 'MeanSquaredError'
    SCHEMA = MeanSquaredErrorSchema


class RootMeanSquaredErrorSchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return RootMeanSquaredErrorConfig


class RootMeanSquaredErrorConfig(BaseMetricConfig):
    """Computes the root mean squared error between the labels and predictions.

    The `streaming_root_mean_squared_error` function creates two local variables,
    `total` and `count` that are used to compute the root mean squared error.
    This average is weighted by `weights`, and it is ultimately returned as
    `root_mean_squared_error`: an idempotent operation that takes the square root
    of the division of `total` by `count`.

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `root_mean_squared_error`. Internally, a `squared_error` operation computes
    the element-wise square of the difference between `predictions` and `labels`.
    Then `update_op` increments `total` with the reduced sum of the product of
    `weights` and `squared_error`, and it increments `count` with the reduced sum
    of `weights`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: A `Tensor` of the same shape as `predictions`.
        output_layer: A `Tensor` of arbitrary shape.
        weights: Optional `Tensor` indicating the frequency with which an example is
            sampled. Rank must be 0, or the same rank as `labels`, and must be
            broadcastable to `labels` (i.e., all dimensions must be either `1`, or
            the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that
            `root_mean_squared_error` should be added to.
        updates_collections: An optional list of collections that `update_op` should be added to.
        name: An optional variable_scope name.

    Returns:
        root_mean_squared_error: A `Tensor` representing the current mean, the value
            of `total` divided by `count`.
        update_op: An operation that increments the `total` and `count` variables
            appropriately and whose value matches `root_mean_squared_error`.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - RootMeanSquaredError
      # other model properties
    ```
    """
    IDENTIFIER = 'RootMeanSquaredError'
    SCHEMA = RootMeanSquaredErrorSchema


class CovarianceSchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return CovarianceConfig


class CovarianceConfig(BaseMetricConfig):
    """Computes the unbiased sample covariance between `predictions` and `labels`.

    The `streaming_covariance` function creates four local variables,
    `comoment`, `mean_prediction`, `mean_label`, and `count`, which are used to
    compute the sample covariance between predictions and labels across multiple
    batches of data. The covariance is ultimately returned as an idempotent
    operation that simply divides `comoment` by `count` - 1. We use `count` - 1
    in order to get an unbiased estimate.

    The algorithm used for this online computation is described in
    https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance.
    Specifically, the formula used to combine two sample comoments is
    `C_AB = C_A + C_B + (E[x_A] - E[x_B]) * (E[y_A] - E[y_B]) * n_A * n_B / n_AB`
    The comoment for a single batch of data is simply
    `sum((x - E[x]) * (y - E[y]))`, optionally weighted.

    If `weights` is not None, then it is used to compute weighted comoments,
    means, and count. NOTE: these weights are treated as "frequency weights", as
    opposed to "reliability weights". See discussion of the difference on
    https://wikipedia.org/wiki/Weighted_arithmetic_mean#Weighted_sample_variance

    To facilitate the computation of covariance across multiple batches of data,
    the function creates an `update_op` operation, which updates underlying
    variables and returns the updated covariance.

    Args:
        input_layer: A `Tensor` of the same shape as `predictions`.
        output_layer: A `Tensor` of arbitrary shape.
        weights: Optional `Tensor` indicating the frequency with which an example is
            sampled. Rank must be 0, or the same rank as `labels`, and must be
            broadcastable to `labels` (i.e., all dimensions must be either `1`, or
            the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that
            the metric should be added to.
        updates_collections: An optional list of collections that `update_op` should be added to.
        name: An optional variable_scope name.

    Returns:
        covariance: A `Tensor` representing the current unbiased sample covariance,
            `comoment` / (`count` - 1).
        update_op: An operation that updates the local variables appropriately.

    Raises:
        ValueError: If labels and predictions are of different sizes or if either
            `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - RootMeanSquaredError
      # other model properties
    ```
    """
    IDENTIFIER = 'Covariance'
    SCHEMA = CovarianceSchema


class PearsonCorrelationSchema(BaseMetricSchema):

    @staticmethod
    def schema_config():
        return PearsonCorrelationConfig


class PearsonCorrelationConfig(BaseMetricConfig):
    """Computes Pearson correlation coefficient between `predictions`, `labels`.

    The `streaming_pearson_correlation` function delegates to
    `streaming_covariance` the tracking of three [co]variances:

    - `streaming_covariance(predictions, labels)`, i.e. covariance
    - `streaming_covariance(predictions, predictions)`, i.e. variance
    - `streaming_covariance(labels, labels)`, i.e. variance

    The product-moment correlation ultimately returned is an idempotent operation
    `cov(predictions, labels) / sqrt(var(predictions) * var(labels))`. To
    facilitate correlation computation across multiple batches, the function
    groups the `update_op`s of the underlying streaming_covariance and returns an
    `update_op`.

    If `weights` is not None, then it is used to compute a weighted correlation.
    NOTE: these weights are treated as "frequency weights", as opposed to
    "reliability weights". See discussion of the difference on
    https://wikipedia.org/wiki/Weighted_arithmetic_mean#Weighted_sample_variance

    Args:
        input_layer: A `Tensor` of the same shape as `predictions`.
        output_layer: A `Tensor` of arbitrary shape.
        weights: Optional `Tensor` indicating the frequency with which an example is
            sampled. Rank must be 0, or the same rank as `labels`, and must be
            broadcastable to `labels` (i.e., all dimensions must be either `1`, or
            the same as the corresponding `labels` dimension).
        metrics_collections: An optional list of collections that
            the metric should be added to.
        updates_collections: An optional list of collections that `update_op` should be added to.
        name: An optional variable_scope name.

    Returns:
        pearson_r: A `Tensor` representing the current Pearson product-moment
            correlation coefficient, the value of
            `cov(predictions, labels) / sqrt(var(predictions) * var(labels))`.
        update_op: An operation that updates the underlying variables appropriately.

    Raises:
        ValueError: If `labels` and `predictions` are of different sizes, or if
            `weights` is the wrong size, or if either `metrics_collections` or
            `updates_collections` are not a `list` or `tuple`.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - RootMeanSquaredError
      # other model properties
    ```
    """
    IDENTIFIER = 'PearsonCorrelation'
    SCHEMA = PearsonCorrelationSchema


class MeanCosineDistanceSchema(BaseMetricSchema):
    dim = fields.Int()

    @staticmethod
    def schema_config():
        return MeanCosineDistanceConfig


class MeanCosineDistanceConfig(BaseMetricConfig):
    """Computes the cosine distance between the labels and predictions.

    The `streaming_mean_cosine_distance` function creates two local variables,
    `total` and `count` that are used to compute the average cosine distance
    between `predictions` and `labels`. This average is weighted by `weights`,
    and it is ultimately returned as `mean_distance`, which is an idempotent
    operation that simply divides `total` by `count`.

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `mean_distance`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: A `Tensor` of the same shape as `predictions`.
        output_layer: A `Tensor` of arbitrary shape.
        dim: The dimension along which the cosine distance is computed.
        weights: An optional `Tensor` whose shape is broadcastable to `predictions`,
            and whose dimension `dim` is 1.
        metrics_collections: An optional list of collections that the metric
            value variable should be added to.
        updates_collections: An optional list of collections that the metric update
            ops should be added to.
        name: An optional variable_scope name.

    Returns:
        mean_distance: A `Tensor` representing the current mean, the value of
            `total` divided by `count`.
        update_op: An operation that increments the `total` and `count` variables appropriately.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - MeanCosineDistance
      # other model properties
    ```
    """
    IDENTIFIER = 'MeanCosineDistance'
    SCHEMA = MeanCosineDistanceSchema

    def __init__(self, dim, **kwargs):
        self.dim = dim
        super(MeanCosineDistanceConfig, self).__init__(**kwargs)


class PercentageLessSchema(BaseTensorMetricSchema):
    threshold = fields.Float()

    @staticmethod
    def schema_config():
        return PercentageLessConfig


class PercentageLessConfig(BaseTensorMetricConfig):
    """Computes the percentage of values less than the given threshold.

    The `streaming_percentage_less` function creates two local variables,
    `total` and `count` that are used to compute the percentage of `values` that
    fall below `threshold`. This rate is weighted by `weights`, and it is
    ultimately returned as `percentage` which is an idempotent operation that
    simply divides `total` by `count`.

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the
    `percentage`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        values: A numeric `Tensor` of arbitrary size.
        threshold: A scalar threshold.
        weights: An optional `Tensor` whose shape is broadcastable to `values`.
        metrics_collections: An optional list of collections that the metric
            value variable should be added to.
        updates_collections: An optional list of collections that the metric update
            ops should be added to.
        name: An optional variable_scope name.

    Returns:
        percentage: A `Tensor` representing the current mean, the value of `total`
            divided by `count`.
        update_op: An operation that increments the `total` and `count` variables appropriately.

    Raises:
        ValueError: If `weights` is not `None` and its shape doesn't match `values`,
            or if either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - PercentageLess:
            values: dense_layer_1
            threshold: 1
      # other model properties
    ```
    """
    IDENTIFIER = 'PercentageLess'
    SCHEMA = PercentageLessSchema

    def __init__(self, values, threshold, **kwargs):
        self.threshold = threshold
        super(PercentageLessConfig, self).__init__(values=values, **kwargs)


class MeanIOUSchema(BaseMetricSchema):
    num_classes = fields.Int()

    @staticmethod
    def schema_config():
        return MeanIOUConfig


class MeanIOUConfig(BaseMetricConfig):
    """Calculate per-step mean Intersection-Over-Union (mIOU).

    Mean Intersection-Over-Union is a common evaluation metric for
    semantic image segmentation, which first computes the IOU for each
    semantic class and then computes the average over classes.
    IOU is defined as follows:
      IOU = true_positive / (true_positive + false_positive + false_negative).
    The predictions are accumulated in a confusion matrix, weighted by `weights`,
    and mIOU is then calculated from it.

    For estimation of the metric over a stream of data, the function creates an
    `update_op` operation that updates these variables and returns the `mean_iou`.

    If `weights` is `None`, weights default to 1. Use weights of 0 to mask values.

    Args:
        input_layer: A `Tensor` of ground truth labels with shape [batch size] and of
            type `int32` or `int64`. The tensor will be flattened, if its rank > 1.
        output_layer: A `Tensor` of prediction results for semantic labels, whose
            shape is [batch size] and type `int32` or `int64`. The tensor will be
            flattened, if its rank > 1.
        num_classes: The possible number of labels the prediction task can
            have. This value must be provided, since a confusion matrix of
            dimension = [num_classes, num_classes] will be allocated.
        weights: An optional `Tensor` whose shape is broadcastable to `predictions`.
        metrics_collections: An optional list of collections that `mean_iou` should be added to.
        updates_collections: An optional list of collections `update_op` should be added to.
        name: An optional variable_scope name.

    Returns:
        mean_iou: A `Tensor` representing the mean intersection-over-union.
        update_op: An operation that increments the confusion matrix.

    Raises:
        ValueError: If `predictions` and `labels` have mismatched shapes, or if
            `weights` is not `None` and its shape doesn't match `predictions`, or if
            either `metrics_collections` or `updates_collections` are not a list or tuple.

    Polyaxonfile usage:

    ```yaml
    model:
      # other model properties
      metrics:
        - MeanIOU:
            num_classes: 10
      # other model properties
    ```
    """
    IDENTIFIER = 'MeanIOU'
    SCHEMA = MeanIOUSchema

    def __init__(self, num_classes, **kwargs):
        self.num_classes = num_classes
        super(MeanIOUConfig, self).__init__(**kwargs)


class MetricSchema(BaseMultiSchema):
    __multi_schema_name__ = 'Metric'
    __configs__ = {
        TruePositivesConfig.IDENTIFIER: TruePositivesConfig,
        TrueNegativesConfig.IDENTIFIER: TrueNegativesConfig,
        FalsePositivesConfig.IDENTIFIER: FalsePositivesConfig,
        FalseNegativesConfig.IDENTIFIER: FalseNegativesConfig,
        MeanConfig.IDENTIFIER: MeanConfig,
        MeanTensorConfig.IDENTIFIER: MeanTensorConfig,
        AccuracyConfig.IDENTIFIER: AccuracyConfig,
        PrecisionConfig.IDENTIFIER: PrecisionConfig,
        RecallConfig.IDENTIFIER: RecallConfig,
        AUCConfig.IDENTIFIER: AUCConfig,
        SpecificityAtSensitivityConfig.IDENTIFIER: SpecificityAtSensitivityConfig,
        SensitivityAtSpecificityConfig.IDENTIFIER: SensitivityAtSpecificityConfig,
        PrecisionAtThresholdsConfig.IDENTIFIER: PrecisionAtThresholdsConfig,
        RecallAtThresholdsConfig.IDENTIFIER: RecallAtThresholdsConfig,
        SparseRecallAtKConfig.IDENTIFIER: SparseRecallAtKConfig,
        SparsePrecisionAtKConfig.IDENTIFIER: SparsePrecisionAtKConfig,
        MeanAbsoluteErrorConfig.IDENTIFIER: MeanAbsoluteErrorConfig,
        MeanRelativeErrorConfig.IDENTIFIER: MeanRelativeErrorConfig,
        MeanSquaredErrorConfig.IDENTIFIER: MeanSquaredErrorConfig,
        RootMeanSquaredErrorConfig.IDENTIFIER: RootMeanSquaredErrorConfig,
        CovarianceConfig.IDENTIFIER: CovarianceConfig,
        PearsonCorrelationConfig.IDENTIFIER: PearsonCorrelationConfig,
        MeanCosineDistanceConfig.IDENTIFIER: MeanCosineDistanceConfig,
        PercentageLessConfig.IDENTIFIER: PercentageLessConfig,
        MeanIOUConfig.IDENTIFIER: MeanIOUConfig,
    }
