aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEfraim Flashner2020-04-23 09:22:53 -0500
committerEfraim Flashner2020-04-23 09:23:11 -0500
commitc0c6263202917faf8af65d734724e2179db5e130 (patch)
tree3805a33f5cce507e8583cf7e3f4b2d11ca6b67a2
parente88085103b2736000339cbef7d3c60d4bf52e3b1 (diff)
downloadguix-bioinformatics-c0c6263202917faf8af65d734724e2179db5e130.tar.gz
gn: Fix ratspub python error.
We are using an older version of python-keras and tensorflow and therefore need to backport the AUC optimizations from 2.3.1
-rw-r--r--gn/packages/ratspub.scm19
-rw-r--r--keras-auc-optimizer.patch1133
2 files changed, 1150 insertions, 2 deletions
diff --git a/gn/packages/ratspub.scm b/gn/packages/ratspub.scm
index 882a6bd..9dd9745 100644
--- a/gn/packages/ratspub.scm
+++ b/gn/packages/ratspub.scm
@@ -1,6 +1,7 @@
(define-module (gn packages ratspub)
#:use-module ((guix licenses) #:prefix license:)
#:use-module (guix utils)
+ #:use-module (gnu packages)
#:use-module (guix packages)
#:use-module (guix git-download)
#:use-module (guix build-system python)
@@ -25,7 +26,15 @@
(file-name (git-file-name name version))
(sha256
(base32
- "1ii3721mqd3dbpjkhqi7yqjd9bqcf0g19kdbb8265pmbfjjsg164"))))
+ "1ii3721mqd3dbpjkhqi7yqjd9bqcf0g19kdbb8265pmbfjjsg164"))
+ (modules '((guix build utils)))
+ (snippet
+ '(begin (substitute* "server.py"
+ ;; Keep the service running on port 4200
+ (("4201") "4200")
+ ;; Backport to python-keras-2.2.4
+ (("learning_rate") "lr") )
+ #t))))
(build-system python-build-system)
(arguments
`(#:tests? #f ; no test suite
@@ -130,11 +139,17 @@ Studies} catalog are also included in the search to better answer this
question.")
(license license:expat)))
-;; We want a copy of python-keras without tests.
+;; We want a copy of python-keras with the AUC optimizer backported.
+;; We skip the tests because we "test in production".
+;; That's a lie. The test suite just takes a long time to run.
(define-public python-keras-for-ratspub
(hidden-package
(package
(inherit python-keras)
+ (source
+ (origin
+ (inherit (package-source python-keras))
+ (patches (search-patches "keras-auc-optimizer.patch"))))
(arguments
(substitute-keyword-arguments (package-arguments python-keras)
((#:phases phases)
diff --git a/keras-auc-optimizer.patch b/keras-auc-optimizer.patch
new file mode 100644
index 0000000..bbc6924
--- /dev/null
+++ b/keras-auc-optimizer.patch
@@ -0,0 +1,1133 @@
+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
+