diff options
Diffstat (limited to 'keras-auc-optimizer.patch')
-rw-r--r-- | keras-auc-optimizer.patch | 1133 |
1 files changed, 0 insertions, 1133 deletions
diff --git a/keras-auc-optimizer.patch b/keras-auc-optimizer.patch deleted file mode 100644 index bbc6924..0000000 --- a/keras-auc-optimizer.patch +++ /dev/null @@ -1,1133 +0,0 @@ -From 901159da45695da24a5206125910f02fc50169ce Mon Sep 17 00:00:00 2001 -From: Efraim Flashner <efraim@flashner.co.il> -Date: Thu, 23 Apr 2020 15:50:37 +0300 -Subject: [PATCH] add keras metrics - ---- - keras/backend/tensorflow_backend.py | 12 + - keras/metrics.py | 584 ++++++++++++++++++++++++++++ - keras/utils/__init__.py | 2 + - keras/utils/losses_utils.py | 177 +++++++++ - keras/utils/metrics_utils.py | 278 +++++++++++++ - 5 files changed, 1053 insertions(+) - create mode 100644 keras/utils/losses_utils.py - create mode 100644 keras/utils/metrics_utils.py - -diff --git a/keras/backend/tensorflow_backend.py b/keras/backend/tensorflow_backend.py -index bcb8be0..a2870f5 100644 ---- a/keras/backend/tensorflow_backend.py -+++ b/keras/backend/tensorflow_backend.py -@@ -4453,3 +4453,15 @@ def local_conv2d(inputs, kernel, kernel_size, strides, output_shape, data_format - else: - output = permute_dimensions(output, (2, 0, 1, 3)) - return output -+ -+#get_graph = tf_keras_backend.get_graph -+ -+#def is_symbolic(x): -+# return isinstance(x, tf.Tensor) and hasattr(x, 'op') -+ -+def size(x, name=None): -+# if is_symbolic(x): -+# with get_graph().as_default(): -+# return tf.size(x) -+ return tf.size(x, name=name) -+ -diff --git a/keras/metrics.py b/keras/metrics.py -index 8e3df1f..8f57910 100644 ---- a/keras/metrics.py -+++ b/keras/metrics.py -@@ -4,8 +4,12 @@ from __future__ import absolute_import - from __future__ import division - from __future__ import print_function - -+import abc - import six -+import types -+ - from . import backend as K -+from .engine.base_layer import Layer - from .losses import mean_squared_error - from .losses import mean_absolute_error - from .losses import mean_absolute_percentage_error -@@ -19,10 +23,201 @@ from .losses import binary_crossentropy - from .losses import kullback_leibler_divergence - from .losses import poisson - from .losses import cosine_proximity -+from .utils import losses_utils -+from .utils import metrics_utils - from .utils.generic_utils import deserialize_keras_object - from .utils.generic_utils import serialize_keras_object - - -+@six.add_metaclass(abc.ABCMeta) -+class Metric(Layer): -+ """Encapsulates metric logic and state. -+ -+ Standalone usage: -+ ```python -+ m = SomeMetric(...) -+ for input in ...: -+ m.update_state(input) -+ m.result() -+ ``` -+ -+ Usage with the `compile` API: -+ ```python -+ model.compile(optimizer='rmsprop', -+ loss=keras.losses.categorical_crossentropy, -+ metrics=[keras.metrics.CategoricalAccuracy()]) -+ ``` -+ -+ To be implemented by subclasses: -+ * `__init__()`: All state variables should be created in this method by -+ calling `self.add_weight()` like: `self.var = self.add_weight(...)` -+ * `update_state()`: Has all updates to the state variables like: -+ self.var.assign_add(...). -+ * `result()`: Computes and returns a value for the metric -+ from the state variables. -+ """ -+ -+ def __init__(self, name=None, dtype=None, **kwargs): -+ super(Metric, self).__init__(name=name, dtype=dtype, **kwargs) -+ self.stateful = True # All metric layers are stateful. -+ self.built = True -+ self.dtype = K.floatx() if dtype is None else dtype -+ -+ def __new__(cls, *args, **kwargs): -+ obj = super(Metric, cls).__new__(cls) -+ update_state_fn = obj.update_state -+ -+ obj.update_state = types.MethodType( -+ metrics_utils.update_state_wrapper(update_state_fn), obj) -+ return obj -+ -+ def __call__(self, *args, **kwargs): -+ """Accumulates statistics and then computes metric result value.""" -+ update_op = self.update_state(*args, **kwargs) -+ return self.result() -+ -+ def get_config(self): -+ """Returns the serializable config of the metric.""" -+ return {'name': self.name, 'dtype': self.dtype} -+ -+ def reset_states(self): -+ """Resets all of the metric state variables. -+ This function is called between epochs/steps, -+ when a metric is evaluated during training. -+ """ -+ K.batch_set_value([(v, 0) for v in self.weights]) -+ -+ @abc.abstractmethod -+ def update_state(self, *args, **kwargs): -+ """Accumulates statistics for the metric. """ -+ raise NotImplementedError('Must be implemented in subclasses.') -+ -+ @abc.abstractmethod -+ def result(self): -+ """Computes and returns the metric value tensor. -+ Result computation is an idempotent operation that simply calculates the -+ metric value using the state variables. -+ """ -+ raise NotImplementedError('Must be implemented in subclasses.') -+ -+ # For use by subclasses # -+ def add_weight(self, -+ name, -+ shape=(), -+ initializer=None, -+ dtype=None): -+ """Adds state variable. Only for use by subclasses.""" -+ return super(Metric, self).add_weight( -+ name=name, -+ shape=shape, -+ dtype=self.dtype if dtype is None else dtype, -+ trainable=False, -+ initializer=initializer) -+ -+ # End: For use by subclasses ### -+ -+ -+class Reduce(Metric): -+ """Encapsulates metrics that perform a reduce operation on the values.""" -+ -+ def __init__(self, reduction, name, dtype=None): -+ """Creates a `Reduce` instance. -+ # Arguments -+ reduction: a metrics `Reduction` enum value. -+ name: string name of the metric instance. -+ dtype: (Optional) data type of the metric result. -+ """ -+ super(Reduce, self).__init__(name=name, dtype=dtype) -+ self.reduction = reduction -+ self.total = self.add_weight('total', initializer='zeros') -+ if reduction in [metrics_utils.Reduction.SUM_OVER_BATCH_SIZE, -+ metrics_utils.Reduction.WEIGHTED_MEAN]: -+ self.count = self.add_weight('count', initializer='zeros') -+ -+ def update_state(self, values, sample_weight=None): -+ """Accumulates statistics for computing the reduction metric. -+ For example, if `values` is [1, 3, 5, 7] and reduction=SUM_OVER_BATCH_SIZE, -+ then the value of `result()` is 4. If the `sample_weight` is specified as -+ [1, 1, 0, 0] then value of `result()` would be 2. -+ # Arguments -+ values: Per-example value. -+ sample_weight: Optional weighting of each example. Defaults to 1. -+ """ -+ values = K.cast(values, self.dtype) -+ if sample_weight is not None: -+ sample_weight = K.cast(sample_weight, self.dtype) -+ # Update dimensions of weights to match with values if possible. -+ values, _, sample_weight = losses_utils.squeeze_or_expand_dimensions( -+ values, sample_weight=sample_weight) -+ -+ # Broadcast weights if possible. -+ sample_weight = losses_utils.broadcast_weights(sample_weight, values) -+ values = values * sample_weight -+ -+ value_sum = K.sum(values) -+ update_total_op = K.update_add(self.total, value_sum) -+ -+ # Exit early if the reduction doesn't have a denominator. -+ if self.reduction == metrics_utils.Reduction.SUM: -+ return update_total_op -+ -+ # Update `count` for reductions that require a denominator. -+ if self.reduction == metrics_utils.Reduction.SUM_OVER_BATCH_SIZE: -+ num_values = K.cast(K.size(values), self.dtype) -+ elif self.reduction == metrics_utils.Reduction.WEIGHTED_MEAN: -+ if sample_weight is None: -+ num_values = K.cast(K.size(values), self.dtype) -+ else: -+ num_values = K.sum(sample_weight) -+ else: -+ raise NotImplementedError( -+ 'reduction [%s] not implemented' % self.reduction) -+ -+ with K.control_dependencies([update_total_op]): -+ return K.update_add(self.count, num_values) -+ -+ def result(self): -+ if self.reduction == metrics_utils.Reduction.SUM: -+ return self.total -+ elif self.reduction in [ -+ metrics_utils.Reduction.WEIGHTED_MEAN, -+ metrics_utils.Reduction.SUM_OVER_BATCH_SIZE -+ ]: -+ return self.total / self.count -+ else: -+ raise NotImplementedError( -+ 'reduction [%s] not implemented' % self.reduction) -+ -+ -+class Sum(Reduce): -+ """Computes the (weighted) sum of the given values. -+ -+ For example, if values is [1, 3, 5, 7] then the sum is 16. -+ If the weights were specified as [1, 1, 0, 0] then the sum would be 4. -+ -+ This metric creates one variable, `total`, that is used to compute the sum of -+ `values`. This is ultimately returned as `sum`. -+ If `sample_weight` is `None`, weights default to 1. Use `sample_weight` of 0 -+ to mask values. -+ -+ Standalone usage: -+ ```python -+ m = keras.metrics.Sum() -+ m.update_state([1, 3, 5, 7]) -+ m.result() -+ ``` -+ """ -+ -+ def __init__(self, name='sum', dtype=None): -+ """Creates a `Sum` instance. -+ # Arguments -+ name: (Optional) string name of the metric instance. -+ dtype: (Optional) data type of the metric result. -+ """ -+ super(Sum, self).__init__(reduction=metrics_utils.Reduction.SUM, -+ name=name, dtype=dtype) -+ -+ - def binary_accuracy(y_true, y_pred): - return K.mean(K.equal(y_true, K.round(y_pred)), axis=-1) - -@@ -49,6 +244,395 @@ def sparse_top_k_categorical_accuracy(y_true, y_pred, k=5): - return K.mean(K.in_top_k(y_pred, K.cast(K.flatten(y_true), 'int32'), k), - axis=-1) - -+class SensitivitySpecificityBase(Metric): -+ """Abstract base class for computing sensitivity and specificity. -+ -+ For additional information about specificity and sensitivity, see the -+ following: https://en.wikipedia.org/wiki/Sensitivity_and_specificity -+ """ -+ -+ def __init__(self, value, num_thresholds=200, name=None, dtype=None): -+ super(SensitivitySpecificityBase, self).__init__(name=name, dtype=dtype) -+ if num_thresholds <= 0: -+ raise ValueError('`num_thresholds` must be > 0.') -+ self.value = value -+ self.true_positives = self.add_weight( -+ 'true_positives', -+ shape=(num_thresholds,), -+ initializer='zeros') -+ self.true_negatives = self.add_weight( -+ 'true_negatives', -+ shape=(num_thresholds,), -+ initializer='zeros') -+ self.false_positives = self.add_weight( -+ 'false_positives', -+ shape=(num_thresholds,), -+ initializer='zeros') -+ self.false_negatives = self.add_weight( -+ 'false_negatives', -+ shape=(num_thresholds,), -+ initializer='zeros') -+ -+ # Compute `num_thresholds` thresholds in [0, 1] -+ if num_thresholds == 1: -+ self.thresholds = [0.5] -+ else: -+ thresholds = [(i + 1) * 1.0 / (num_thresholds - 1) -+ for i in range(num_thresholds - 2)] -+ self.thresholds = [0.0] + thresholds + [1.0] -+ -+ def update_state(self, y_true, y_pred, sample_weight=None): -+ return metrics_utils.update_confusion_matrix_variables( -+ { -+ metrics_utils.ConfusionMatrix.TRUE_POSITIVES: self.true_positives, -+ metrics_utils.ConfusionMatrix.TRUE_NEGATIVES: self.true_negatives, -+ metrics_utils.ConfusionMatrix.FALSE_POSITIVES: self.false_positives, -+ metrics_utils.ConfusionMatrix.FALSE_NEGATIVES: self.false_negatives, -+ }, -+ y_true, -+ y_pred, -+ thresholds=self.thresholds, -+ sample_weight=sample_weight) -+ -+ def reset_states(self): -+ num_thresholds = len(self.thresholds) -+ K.batch_set_value( -+ [(v, np.zeros((num_thresholds,))) for v in self.variables]) -+ -+ -+class SensitivityAtSpecificity(SensitivitySpecificityBase): -+ """Computes the sensitivity at a given specificity. -+ -+ `Sensitivity` measures the proportion of actual positives that are correctly -+ identified as such (tp / (tp + fn)). -+ `Specificity` measures the proportion of actual negatives that are correctly -+ identified as such (tn / (tn + fp)). -+ -+ This metric creates four local variables, `true_positives`, `true_negatives`, -+ `false_positives` and `false_negatives` that are used to compute the -+ sensitivity at the given specificity. The threshold for the given specificity -+ value is computed and used to evaluate the corresponding sensitivity. -+ -+ If `sample_weight` is `None`, weights default to 1. -+ Use `sample_weight` of 0 to mask values. -+ -+ For additional information about specificity and sensitivity, see the -+ following: https://en.wikipedia.org/wiki/Sensitivity_and_specificity -+ -+ Usage with the compile API: -+ -+ ```python -+ model = keras.Model(inputs, outputs) -+ model.compile( -+ 'sgd', -+ loss='mse', -+ metrics=[keras.metrics.SensitivityAtSpecificity()]) -+ ``` -+ -+ # Arguments -+ specificity: A scalar value in range `[0, 1]`. -+ num_thresholds: (Optional) Defaults to 200. The number of thresholds to -+ use for matching the given specificity. -+ name: (Optional) string name of the metric instance. -+ dtype: (Optional) data type of the metric result. -+ """ -+ -+ def __init__(self, specificity, num_thresholds=200, name=None, dtype=None): -+ if specificity < 0 or specificity > 1: -+ raise ValueError('`specificity` must be in the range [0, 1].') -+ self.specificity = specificity -+ self.num_thresholds = num_thresholds -+ super(SensitivityAtSpecificity, self).__init__( -+ specificity, num_thresholds=num_thresholds, name=name, dtype=dtype) -+ -+ def result(self): -+ # Calculate specificities at all the thresholds. -+ specificities = K.switch( -+ K.greater(self.true_negatives + self.false_positives, 0), -+ (self.true_negatives / (self.true_negatives + self.false_positives)), -+ K.zeros_like(self.thresholds)) -+ -+ # Find the index of the threshold where the specificity is closest to the -+ # given specificity. -+ min_index = K.argmin( -+ K.abs(specificities - self.value), axis=0) -+ min_index = K.cast(min_index, 'int32') -+ -+ # Compute sensitivity at that index. -+ return K.switch( -+ K.greater((self.true_positives[min_index] + -+ self.false_negatives[min_index]), 0), -+ (self.true_positives[min_index] / -+ (self.true_positives[min_index] + self.false_negatives[min_index])), -+ K.zeros_like(self.true_positives[min_index])) -+ -+ def get_config(self): -+ config = { -+ 'num_thresholds': self.num_thresholds, -+ 'specificity': self.specificity -+ } -+ base_config = super(SensitivityAtSpecificity, self).get_config() -+ return dict(list(base_config.items()) + list(config.items())) -+ -+ -+class AUC(Metric): -+ """Computes the approximate AUC (Area under the curve) via a Riemann sum. -+ -+ This metric 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`. The `thresholds` parameter can be -+ used to manually specify thresholds which split the predictions more evenly. -+ -+ 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. Setting `summation_method` -+ to 'minoring' or 'majoring' can help quantify the error in the approximation -+ by providing lower or upper bound estimate of the AUC. -+ -+ If `sample_weight` is `None`, weights default to 1. -+ Use `sample_weight` of 0 to mask values. -+ -+ Usage with the compile API: -+ -+ ```python -+ model = keras.Model(inputs, outputs) -+ model.compile('sgd', loss='mse', metrics=[keras.metrics.AUC()]) -+ ``` -+ -+ # Arguments -+ num_thresholds: (Optional) Defaults to 200. The number of thresholds to -+ use when discretizing the roc curve. Values must be > 1. -+ curve: (Optional) Specifies the name of the curve to be computed, 'ROC' -+ [default] or 'PR' for the Precision-Recall-curve. -+ summation_method: (Optional) Specifies the Riemann summation method used -+ (https://en.wikipedia.org/wiki/Riemann_sum): 'interpolation' [default], -+ applies mid-point summation scheme for `ROC`. For PR-AUC, interpolates -+ (true/false) positives but not the ratio that is precision (see Davis -+ & Goadrich 2006 for details); 'minoring' that applies left summation -+ for increasing intervals and right summation for decreasing intervals; -+ 'majoring' that does the opposite. -+ name: (Optional) string name of the metric instance. -+ dtype: (Optional) data type of the metric result. -+ thresholds: (Optional) A list of floating point values to use as the -+ thresholds for discretizing the curve. If set, the `num_thresholds` -+ parameter is ignored. Values should be in [0, 1]. Endpoint thresholds -+ equal to {-epsilon, 1+epsilon} for a small positive epsilon value will -+ be automatically included with these to correctly handle predictions -+ equal to exactly 0 or 1. -+ """ -+ -+ def __init__(self, -+ num_thresholds=200, -+ curve='ROC', -+ summation_method='interpolation', -+ name=None, -+ dtype=None, -+ thresholds=None): -+ # Validate configurations. -+ if (isinstance(curve, metrics_utils.AUCCurve) and -+ curve not in list(metrics_utils.AUCCurve)): -+ raise ValueError('Invalid curve: "{}". Valid options are: "{}"'.format( -+ curve, list(metrics_utils.AUCCurve))) -+ if isinstance( -+ summation_method, -+ metrics_utils.AUCSummationMethod) and summation_method not in list( -+ metrics_utils.AUCSummationMethod): -+ raise ValueError( -+ 'Invalid summation method: "{}". Valid options are: "{}"'.format( -+ summation_method, list(metrics_utils.AUCSummationMethod))) -+ -+ # Update properties. -+ if thresholds is not None: -+ # If specified, use the supplied thresholds. -+ self.num_thresholds = len(thresholds) + 2 -+ thresholds = sorted(thresholds) -+ else: -+ if num_thresholds <= 1: -+ raise ValueError('`num_thresholds` must be > 1.') -+ -+ # Otherwise, linearly interpolate (num_thresholds - 2) thresholds in -+ # (0, 1). -+ self.num_thresholds = num_thresholds -+ thresholds = [(i + 1) * 1.0 / (num_thresholds - 1) -+ for i in range(num_thresholds - 2)] -+ -+ # Add an endpoint "threshold" below zero and above one for either -+ # threshold method to account for floating point imprecisions. -+ self.thresholds = [0.0 - K.epsilon()] + thresholds + [1.0 + K.epsilon()] -+ -+ if isinstance(curve, metrics_utils.AUCCurve): -+ self.curve = curve -+ else: -+ self.curve = metrics_utils.AUCCurve.from_str(curve) -+ if isinstance(summation_method, metrics_utils.AUCSummationMethod): -+ self.summation_method = summation_method -+ else: -+ self.summation_method = metrics_utils.AUCSummationMethod.from_str( -+ summation_method) -+ super(AUC, self).__init__(name=name, dtype=dtype) -+ -+ # Create metric variables -+ self.true_positives = self.add_weight( -+ 'true_positives', -+ shape=(self.num_thresholds,), -+ initializer='zeros') -+ self.true_negatives = self.add_weight( -+ 'true_negatives', -+ shape=(self.num_thresholds,), -+ initializer='zeros') -+ self.false_positives = self.add_weight( -+ 'false_positives', -+ shape=(self.num_thresholds,), -+ initializer='zeros') -+ self.false_negatives = self.add_weight( -+ 'false_negatives', -+ shape=(self.num_thresholds,), -+ initializer='zeros') -+ -+ def update_state(self, y_true, y_pred, sample_weight=None): -+ return metrics_utils.update_confusion_matrix_variables({ -+ metrics_utils.ConfusionMatrix.TRUE_POSITIVES: self.true_positives, -+ metrics_utils.ConfusionMatrix.TRUE_NEGATIVES: self.true_negatives, -+ metrics_utils.ConfusionMatrix.FALSE_POSITIVES: self.false_positives, -+ metrics_utils.ConfusionMatrix.FALSE_NEGATIVES: self.false_negatives, -+ }, y_true, y_pred, self.thresholds, sample_weight=sample_weight) -+ -+ def interpolate_pr_auc(self): -+ """Interpolation formula inspired by section 4 of Davis & Goadrich 2006. -+ -+ https://www.biostat.wisc.edu/~page/rocpr.pdf -+ -+ Note here we derive & use a closed formula not present in the paper -+ as follows: -+ -+ Precision = TP / (TP + FP) = TP / P -+ -+ Modeling all of TP (true positive), FP (false positive) and their sum -+ P = TP + FP (predicted positive) as varying linearly within each interval -+ [A, B] between successive thresholds, we get -+ -+ Precision slope = dTP / dP -+ = (TP_B - TP_A) / (P_B - P_A) -+ = (TP - TP_A) / (P - P_A) -+ Precision = (TP_A + slope * (P - P_A)) / P -+ -+ The area within the interval is (slope / total_pos_weight) times -+ -+ int_A^B{Precision.dP} = int_A^B{(TP_A + slope * (P - P_A)) * dP / P} -+ int_A^B{Precision.dP} = int_A^B{slope * dP + intercept * dP / P} -+ -+ where intercept = TP_A - slope * P_A = TP_B - slope * P_B, resulting in -+ -+ int_A^B{Precision.dP} = TP_B - TP_A + intercept * log(P_B / P_A) -+ -+ Bringing back the factor (slope / total_pos_weight) we'd put aside, we get -+ -+ slope * [dTP + intercept * log(P_B / P_A)] / total_pos_weight -+ -+ where dTP == TP_B - TP_A. -+ -+ Note that when P_A == 0 the above calculation simplifies into -+ -+ int_A^B{Precision.dTP} = int_A^B{slope * dTP} = slope * (TP_B - TP_A) -+ -+ which is really equivalent to imputing constant precision throughout the -+ first bucket having >0 true positives. -+ -+ # Returns -+ pr_auc: an approximation of the area under the P-R curve. -+ """ -+ dtp = self.true_positives[:self.num_thresholds - -+ 1] - self.true_positives[1:] -+ p = self.true_positives + self.false_positives -+ dp = p[:self.num_thresholds - 1] - p[1:] -+ -+ prec_slope = dtp / K.maximum(dp, 0) -+ intercept = self.true_positives[1:] - (prec_slope * p[1:]) -+ -+ # Logical and -+ pMin = K.expand_dims(p[:self.num_thresholds - 1] > 0, 0) -+ pMax = K.expand_dims(p[1:] > 0, 0) -+ are_different = K.concatenate([pMin, pMax], axis=0) -+ switch_condition = K.all(are_different, axis=0) -+ -+ safe_p_ratio = K.switch( -+ switch_condition, -+ p[:self.num_thresholds - 1] / K.maximum(p[1:], 0), -+ K.ones_like(p[1:])) -+ -+ numer = prec_slope * (dtp + intercept * K.log(safe_p_ratio)) -+ denom = K.maximum(self.true_positives[1:] + self.false_negatives[1:], 0) -+ return K.sum((numer / denom)) -+ -+ def result(self): -+ if (self.curve == metrics_utils.AUCCurve.PR and -+ (self.summation_method == -+ metrics_utils.AUCSummationMethod.INTERPOLATION)): -+ # This use case is different and is handled separately. -+ return self.interpolate_pr_auc() -+ -+ # Set `x` and `y` values for the curves based on `curve` config. -+ recall = K.switch( -+ K.greater((self.true_positives), 0), -+ (self.true_positives / -+ (self.true_positives + self.false_negatives)), -+ K.zeros_like(self.true_positives)) -+ if self.curve == metrics_utils.AUCCurve.ROC: -+ fp_rate = K.switch( -+ K.greater((self.false_positives), 0), -+ (self.false_positives / -+ (self.false_positives + self.true_negatives)), -+ K.zeros_like(self.false_positives)) -+ x = fp_rate -+ y = recall -+ else: # curve == 'PR'. -+ precision = K.switch( -+ K.greater((self.true_positives), 0), -+ (self.true_positives / (self.true_positives + self.false_positives)), -+ K.zeros_like(self.true_positives)) -+ x = recall -+ y = precision -+ -+ # Find the rectangle heights based on `summation_method`. -+ if self.summation_method == metrics_utils.AUCSummationMethod.INTERPOLATION: -+ # Note: the case ('PR', 'interpolation') has been handled above. -+ heights = (y[:self.num_thresholds - 1] + y[1:]) / 2. -+ elif self.summation_method == metrics_utils.AUCSummationMethod.MINORING: -+ heights = K.minimum(y[:self.num_thresholds - 1], y[1:]) -+ else: # self.summation_method = metrics_utils.AUCSummationMethod.MAJORING: -+ heights = K.maximum(y[:self.num_thresholds - 1], y[1:]) -+ -+ # Sum up the areas of all the rectangles. -+ return K.sum((x[:self.num_thresholds - 1] - x[1:]) * heights) -+ -+ def reset_states(self): -+ K.batch_set_value( -+ [(v, np.zeros((self.num_thresholds,))) for v in self.variables]) -+ -+ def get_config(self): -+ config = { -+ 'num_thresholds': self.num_thresholds, -+ 'curve': self.curve.value, -+ 'summation_method': self.summation_method.value, -+ # We remove the endpoint thresholds as an inverse of how the thresholds -+ # were initialized. This ensures that a metric initialized from this -+ # config has the same thresholds. -+ 'thresholds': self.thresholds[1:-1], -+ } -+ base_config = super(AUC, self).get_config() -+ return dict(list(base_config.items()) + list(config.items())) -+ - - # Aliases - -diff --git a/keras/utils/__init__.py b/keras/utils/__init__.py -index 8cc39d5..65af329 100644 ---- a/keras/utils/__init__.py -+++ b/keras/utils/__init__.py -@@ -4,6 +4,8 @@ from . import generic_utils - from . import data_utils - from . import io_utils - from . import conv_utils -+from . import losses_utils -+from . import metrics_utils - - # Globally-importable utils. - from .io_utils import HDF5Matrix -diff --git a/keras/utils/losses_utils.py b/keras/utils/losses_utils.py -new file mode 100644 -index 0000000..617ebb7 ---- /dev/null -+++ b/keras/utils/losses_utils.py -@@ -0,0 +1,177 @@ -+"""Utilities related to losses.""" -+from __future__ import absolute_import -+from __future__ import division -+from __future__ import print_function -+ -+import numpy as np -+ -+from .. import backend as K -+ -+ -+class Reduction(object): -+ """Types of loss reduction. -+ -+ Contains the following values: -+ -+ * `NONE`: Un-reduced weighted losses with the same shape as input. When this -+ reduction type used with built-in Keras training loops like -+ `fit`/`evaluate`, the unreduced vector loss is passed to the optimizer but -+ the reported loss will be a scalar value. -+ * `SUM`: Scalar sum of weighted losses. -+ * `SUM_OVER_BATCH_SIZE`: Scalar `SUM` divided by number of elements in losses. -+ """ -+ -+ NONE = 'none' -+ SUM = 'sum' -+ SUM_OVER_BATCH_SIZE = 'sum_over_batch_size' -+ -+ @classmethod -+ def all(cls): -+ return (cls.NONE, cls.SUM, cls.SUM_OVER_BATCH_SIZE) -+ -+ @classmethod -+ def validate(cls, key): -+ if key not in cls.all(): -+ raise ValueError('Invalid Reduction Key %s.' % key) -+ -+ -+def squeeze_or_expand_dimensions(y_pred, y_true=None, sample_weight=None): -+ """Squeeze or expand last dimension if needed. -+ -+ 1. Squeezes last dim of `y_pred` or `y_true` if their rank differs by 1. -+ 2. Squeezes or expands last dim of `sample_weight` if its rank differs by 1 -+ from the new rank of `y_pred`. -+ If `sample_weight` is scalar, it is kept scalar. -+ -+ # Arguments -+ y_pred: Predicted values, a `Tensor` of arbitrary dimensions. -+ y_true: Optional label `Tensor` whose dimensions match `y_pred`. -+ sample_weight: Optional weight scalar or `Tensor` whose dimensions match -+ `y_pred`. -+ -+ # Returns -+ Tuple of `y_pred`, `y_true` and `sample_weight`. Each of them possibly has -+ the last dimension squeezed, `sample_weight` could be extended by one -+ dimension. -+ """ -+ if y_true is not None: -+ y_pred_rank = K.ndim(y_pred) -+ y_pred_shape = K.int_shape(y_pred) -+ y_true_rank = K.ndim(y_true) -+ y_true_shape = K.int_shape(y_true) -+ -+ if (y_pred_rank - y_true_rank == 1) and (y_pred_shape[-1] == 1): -+ y_pred = K.squeeze(y_pred, -1) -+ elif (y_true_rank - y_pred_rank == 1) and (y_true_shape[-1] == 1): -+ y_true = K.squeeze(y_true, -1) -+ -+ if sample_weight is None: -+ return y_pred, y_true -+ -+ y_pred_rank = K.ndim(y_pred) -+ weights_rank = K.ndim(sample_weight) -+ if weights_rank != 0: -+ if weights_rank - y_pred_rank == 1: -+ sample_weight = K.squeeze(sample_weight, -1) -+ elif y_pred_rank - weights_rank == 1: -+ sample_weight = K.expand_dims(sample_weight, -1) -+ return y_pred, y_true, sample_weight -+ -+ -+def _num_elements(losses): -+ """Computes the number of elements in `losses` tensor.""" -+ with K.name_scope('num_elements') as scope: -+ return K.cast(K.size(losses, name=scope), losses.dtype) -+ -+ -+def reduce_weighted_loss(weighted_losses, reduction=Reduction.SUM_OVER_BATCH_SIZE): -+ """Reduces the individual weighted loss measurements.""" -+ if reduction == Reduction.NONE: -+ loss = weighted_losses -+ else: -+ loss = K.sum(weighted_losses) -+ if reduction == Reduction.SUM_OVER_BATCH_SIZE: -+ loss = loss / _num_elements(weighted_losses) -+ return loss -+ -+ -+def broadcast_weights(values, sample_weight): -+ # Broadcast weights if possible. -+ weights_shape = K.int_shape(sample_weight) -+ values_shape = K.int_shape(values) -+ -+ if values_shape != weights_shape: -+ weights_rank = K.ndim(sample_weight) -+ values_rank = K.ndim(values) -+ -+ # Raise error if ndim of weights is > values. -+ if weights_rank > values_rank: -+ raise ValueError( -+ 'Incompatible shapes: `values` {} vs `sample_weight` {}'.format( -+ values_shape, weights_shape)) -+ -+ # Expand dim of weights to match ndim of values, if required. -+ for i in range(weights_rank, values_rank): -+ sample_weight = K.expand_dims(sample_weight, axis=i) -+ -+ if weights_shape is not None and values_shape is not None: -+ for i in range(weights_rank): -+ if (weights_shape[i] is not None and -+ values_shape[i] is not None and -+ weights_shape[i] != values_shape[i]): -+ # Cannot be broadcasted. -+ if weights_shape[i] != 1: -+ raise ValueError( -+ 'Incompatible shapes: `values` {} vs ' -+ '`sample_weight` {}'.format( -+ values_shape, weights_shape)) -+ sample_weight = K.repeat_elements( -+ sample_weight, values_shape[i], axis=i) -+ return sample_weight -+ -+ -+def compute_weighted_loss(losses, -+ sample_weight=None, -+ reduction=Reduction.SUM_OVER_BATCH_SIZE, -+ name=None): -+ """Computes the weighted loss. -+ -+ # Arguments -+ 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 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. -+ """ -+ Reduction.validate(reduction) -+ if sample_weight is None: -+ sample_weight = 1.0 -+ with K.name_scope(name or 'weighted_loss'): -+ input_dtype = K.dtype(losses) -+ losses = K.cast(losses, K.floatx()) -+ sample_weight = K.cast(sample_weight, K.floatx()) -+ -+ # 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 = broadcast_weights(losses, sample_weight) -+ -+ # Apply weights to losses. -+ weighted_losses = sample_weight * losses -+ -+ # Apply reduction function to the individual weighted losses. -+ loss = reduce_weighted_loss(weighted_losses, reduction) -+ # Convert the result back to the input type. -+ loss = K.cast(loss, input_dtype) -+ return loss -+ -diff --git a/keras/utils/metrics_utils.py b/keras/utils/metrics_utils.py -new file mode 100644 -index 0000000..e6a5bb0 ---- /dev/null -+++ b/keras/utils/metrics_utils.py -@@ -0,0 +1,278 @@ -+"""Utilities related to metrics.""" -+from __future__ import absolute_import -+from __future__ import division -+from __future__ import print_function -+ -+from enum import Enum -+ -+from .. import backend as K -+from . import losses_utils -+ -+NEG_INF = -1e10 -+ -+class Reduction(object): -+ """Types of metrics reduction. -+ Contains the following values: -+ * `SUM`: Scalar sum of weighted values. -+ * `SUM_OVER_BATCH_SIZE`: Scalar `SUM` of weighted values divided by -+ number of elements in values. -+ * `WEIGHTED_MEAN`: Scalar sum of weighted values divided by sum of weights. -+ """ -+ -+ SUM = 'sum' -+ SUM_OVER_BATCH_SIZE = 'sum_over_batch_size' -+ WEIGHTED_MEAN = 'weighted_mean' -+ -+ -+def update_state_wrapper(update_state_fn): -+ """Decorator to wrap metric `update_state()` with `add_update()`. -+ # Arguments -+ update_state_fn: function that accumulates metric statistics. -+ # Returns -+ Decorated function that wraps `update_state_fn()` with `add_update()`. -+ """ -+ def decorated(metric_obj, *args, **kwargs): -+ """Decorated function with `add_update()`.""" -+ -+ update_op = update_state_fn(*args, **kwargs) -+ metric_obj.add_update(update_op) -+ return update_op -+ -+ return decorated -+ -+def result_wrapper(result_fn): -+ """Decorator to wrap metric `result()` with identity op. -+ Wrapping result in identity so that control dependency between -+ update_op from `update_state` and result works in case result returns -+ a tensor. -+ # Arguments -+ result_fn: function that computes the metric result. -+ # Returns -+ Decorated function that wraps `result()` with identity op. -+ """ -+ def decorated(metric_obj, *args, **kwargs): -+ result_t = K.identity(result_fn(*args, **kwargs)) -+ metric_obj._call_result = result_t -+ result_t._is_metric = True -+ return result_t -+ return decorated -+ -+ -+def to_list(x): -+ if isinstance(x, list): -+ return x -+ return [x] -+ -+ -+def assert_thresholds_range(thresholds): -+ if thresholds is not None: -+ invalid_thresholds = [t for t in thresholds if t is None or t < 0 or t > 1] -+ if invalid_thresholds: -+ raise ValueError( -+ 'Threshold values must be in [0, 1]. Invalid values: {}'.format( -+ invalid_thresholds)) -+ -+ -+def parse_init_thresholds(thresholds, default_threshold=0.5): -+ if thresholds is not None: -+ assert_thresholds_range(to_list(thresholds)) -+ thresholds = to_list(default_threshold if thresholds is None else thresholds) -+ return thresholds -+ -+class ConfusionMatrix(Enum): -+ TRUE_POSITIVES = 'tp' -+ FALSE_POSITIVES = 'fp' -+ TRUE_NEGATIVES = 'tn' -+ FALSE_NEGATIVES = 'fn' -+ -+class AUCCurve(Enum): -+ """Type of AUC Curve (ROC or PR).""" -+ ROC = 'ROC' -+ PR = 'PR' -+ -+ @staticmethod -+ def from_str(key): -+ if key in ('pr', 'PR'): -+ return AUCCurve.PR -+ elif key in ('roc', 'ROC'): -+ return AUCCurve.ROC -+ else: -+ raise ValueError('Invalid AUC curve value "%s".' % key) -+ -+ -+class AUCSummationMethod(Enum): -+ """Type of AUC summation method. -+ -+ https://en.wikipedia.org/wiki/Riemann_sum) -+ -+ Contains the following values: -+ * 'interpolation': Applies mid-point summation scheme for `ROC` curve. For -+ `PR` curve, interpolates (true/false) positives but not the ratio that is -+ precision (see Davis & Goadrich 2006 for details). -+ * 'minoring': Applies left summation for increasing intervals and right -+ summation for decreasing intervals. -+ * 'majoring': Applies right summation for increasing intervals and left -+ summation for decreasing intervals. -+ """ -+ INTERPOLATION = 'interpolation' -+ MAJORING = 'majoring' -+ MINORING = 'minoring' -+ -+ @staticmethod -+ def from_str(key): -+ if key in ('interpolation', 'Interpolation'): -+ return AUCSummationMethod.INTERPOLATION -+ elif key in ('majoring', 'Majoring'): -+ return AUCSummationMethod.MAJORING -+ elif key in ('minoring', 'Minoring'): -+ return AUCSummationMethod.MINORING -+ else: -+ raise ValueError('Invalid AUC summation method value "%s".' % key) -+ -+def weighted_assign_add(label, pred, weights, var): -+ # Logical and -+ label = K.expand_dims(label, 0) -+ pred = K.expand_dims(pred, 0) -+ are_different = K.concatenate([label, pred], axis=0) -+ label_and_pred = K.all(are_different, axis=0) -+ label_and_pred = K.cast(label_and_pred, dtype=K.floatx()) -+ if weights is not None: -+ label_and_pred *= weights -+ return var.assign_add(K.sum(label_and_pred, 1)) -+ -+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. -+ # Arguments -+ 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 ops. -+ # 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 = K.cast(y_true, dtype=K.floatx()) -+ y_pred = K.cast(y_pred, dtype=K.floatx()) -+ if sample_weight is not None: -+ sample_weight = K.cast(sample_weight, dtype=K.floatx()) -+ -+ 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))) -+ -+ if sample_weight is None: -+ y_pred, y_true = losses_utils.squeeze_or_expand_dimensions( -+ y_pred, y_true=y_true) -+ else: -+ y_pred, y_true, sample_weight = ( -+ losses_utils.squeeze_or_expand_dimensions( -+ y_pred, y_true=y_true, sample_weight=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 = K.size(y_pred) -+ -+ # Reshape predictions and labels. -+ predictions_2d = K.reshape(y_pred, [1, -1]) -+ labels_2d = K.reshape( -+ K.cast(y_true, dtype='bool'), [1, -1]) -+ -+ # Tile the thresholds for every prediction. -+ thresh_tiled = K.tile( -+ K.expand_dims(K.constant(thresholds), 1), -+ K.stack([1, num_predictions])) -+ -+ # Tile the predictions for every threshold. -+ preds_tiled = K.tile(predictions_2d, [num_thresholds, 1]) -+ -+ # Compare predictions and threshold. -+ pred_is_pos = K.greater(preds_tiled, thresh_tiled) -+ pred_is_neg = K.greater(thresh_tiled, preds_tiled) -+ -+ # Tile labels by number of thresholds -+ label_is_pos = K.tile(labels_2d, [num_thresholds, 1]) -+ -+ if sample_weight is not None: -+ weights = losses_utils.broadcast_weights( -+ y_pred, K.cast(sample_weight, dtype=K.floatx())) -+ weights_tiled = K.tile( -+ K.reshape(weights, [1, -1]), [num_thresholds, 1]) -+ else: -+ weights_tiled = None -+ -+ update_ops = [] -+ 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: -+ loop_vars[ConfusionMatrix.FALSE_NEGATIVES] = (label_is_pos, pred_is_neg) -+ -+ if update_fp or update_tn: -+ label_is_neg = K.equal( -+ label_is_pos, K.zeros_like(label_is_pos, dtype=label_is_pos.dtype)) -+ 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 update_ops -+ --- -2.26.2 - |