Repository: tensorflow/addons

Issue:
Add GeometricMean
**Describe the feature and the current behavior/state.**

**Relevant information**
- Are you willing to contribute it (yes/no): YES
- Are you willing to maintain it going forward? (yes/no): YES
- Is there a relevant academic paper? (if so, where): NO
- Is there already an implementation in another framework? (if so, where): Pytorch Ignite Metrics
- Was it part of tf.contrib? (if so, where): NO

**Which API type would this fall under (layer, metric, optimizer, etc.)**
Metric

**Who will benefit with this feature?**
Researchers or Engineers who need a GeometricMean

**Any other info.**
None



Discussion:
Hi @marload! This seems like a good addition and should be easy to support. Please submit a PR when you have time.
Thank You!


Resolving patch:
diff --git a/tensorflow_addons/metrics/__init__.py b/tensorflow_addons/metrics/__init__.py
--- a/tensorflow_addons/metrics/__init__.py
+++ b/tensorflow_addons/metrics/__init__.py
@@ -25,3 +25,4 @@
     MultiLabelConfusionMatrix,
 )
 from tensorflow_addons.metrics.r_square import RSquare
+from tensorflow_addons.metrics.geometric_mean import GeometricMean
diff --git a/tensorflow_addons/metrics/geometric_mean.py b/tensorflow_addons/metrics/geometric_mean.py
new file mode 100644
--- /dev/null
+++ b/tensorflow_addons/metrics/geometric_mean.py
@@ -0,0 +1,76 @@
+# Copyright 2020 The TensorFlow Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==============================================================================
+"""Implements GeometricMean."""
+
+import tensorflow as tf
+from tensorflow.keras import backend as K
+from tensorflow.keras.metrics import Metric
+
+from typeguard import typechecked
+from tensorflow_addons.utils.types import AcceptableDTypes
+from tensorflow_addons.metrics.utils import sample_weight_shape_match
+
+
+@tf.keras.utils.register_keras_serializable(package="Addons")
+class GeometricMean(Metric):
+    """Compute Geometric Mean
+
+    The geometric mean is a kind of mean. Unlike the arithmetic mean
+    that uses the sum of values, it uses the product of the values to
+    represent typical values in a set of numbers.
+
+    Note: `tfa.metrics.GeometricMean` can be used the same as `tf.keras.metrics.Mean`
+
+    Usage:
+    ```python
+    >>> m = tfa.metrics.GeometricMean()
+    >>> m.update_state([1, 3, 5, 7, 9])
+    >>> m.result().numpy()
+    3.9362833
+    ```
+    """
+
+    @typechecked
+    def __init__(
+        self, name: str = "geometric_mean", dtype: AcceptableDTypes = None, **kwargs
+    ):
+        super().__init__(name=name, dtype=dtype, **kwargs)
+        self.total = self.add_weight(
+            "total", shape=None, initializer="zeros", dtype=dtype
+        )
+        self.count = self.add_weight(
+            "count", shape=None, initializer="zeros", dtype=dtype
+        )
+
+    def update_state(self, values, sample_weight=None) -> None:
+        values = tf.cast(values, dtype=self.dtype)
+        sample_weight = sample_weight_shape_match(values, sample_weight)
+        sample_weight = tf.cast(sample_weight, dtype=self.dtype)
+
+        self.count.assign_add(tf.reduce_sum(sample_weight))
+        if not tf.math.is_inf(self.total):
+            log_v = tf.math.log(values)
+            log_v = tf.math.multiply(sample_weight, log_v)
+            log_v = tf.reduce_sum(log_v)
+            self.total.assign_add(log_v)
+
+    def result(self) -> tf.Tensor:
+        if tf.math.is_inf(self.total):
+            return tf.constant(0, dtype=self.dtype)
+        ret = tf.math.exp(self.total / self.count)
+        return tf.cast(ret, dtype=self.dtype)
+
+    def reset_states(self) -> None:
+        K.batch_set_value([(v, 0) for v in self.variables])
diff --git a/tensorflow_addons/metrics/utils.py b/tensorflow_addons/metrics/utils.py
--- a/tensorflow_addons/metrics/utils.py
+++ b/tensorflow_addons/metrics/utils.py
@@ -84,3 +84,11 @@ def _get_model(metric, num_output):
     data = np.random.random((10, 3))
     labels = np.random.random((10, num_output))
     model.fit(data, labels, epochs=1, batch_size=5, verbose=0)
+
+
+def sample_weight_shape_match(v, sample_weight):
+    if sample_weight is None:
+        return tf.ones_like(v)
+    if np.size(sample_weight) == 1:
+        return tf.fill(v.shape, sample_weight)
+    return tf.convert_to_tensor(sample_weight)


Test patch:
diff --git a/tensorflow_addons/metrics/tests/geometric_mean_test.py b/tensorflow_addons/metrics/tests/geometric_mean_test.py
new file mode 100644
--- /dev/null
+++ b/tensorflow_addons/metrics/tests/geometric_mean_test.py
@@ -0,0 +1,114 @@
+# Copyright 2020 The TensorFlow Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==============================================================================
+"""Tests GeometricMean metrics."""
+
+import numpy as np
+import pytest
+import tensorflow as tf
+
+from tensorflow_addons.metrics import GeometricMean
+
+
+def get_test_data():
+    return [
+        ([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 0),
+        ([0, 0, 0, 0, 0, 0, 0, 1, 2, 6], 0),
+        ([0.2, 0.5, 0.3, 0.6, 0.1, 0.7], 0.32864603),
+        ([8, 4, 1, 7, 2, 11, 9, 22, 52], 7.1804023),
+        ([8.2, 9.7, 9.1, 2.7, 1.1, 2.0], 4.0324492),
+        ([0.6666666, 0.215213, 0.15167], 0.27918512),
+    ]
+
+
+def assert_result(expected, result):
+    np.testing.assert_allclose(expected, result, atol=1e-6)
+
+
+def check_result(obj, expected_result, expected_count):
+    result = obj.result().numpy()
+    count = obj.count.numpy()
+    assert_result(expected_result, result)
+    np.testing.assert_equal(expected_count, count)
+
+
+def test_config_gmean():
+    def _check_config(obj, name):
+        assert obj.name == name
+        assert obj.dtype == tf.float32
+        assert obj.stateful
+        assert len(obj.variables) == 2
+
+    name = "my_gmean"
+    obj1 = GeometricMean(name=name)
+    _check_config(obj1, name)
+
+    obj2 = GeometricMean.from_config(obj1.get_config())
+    _check_config(obj2, name)
+
+
+def test_init_states_gmean():
+    obj = GeometricMean()
+    assert obj.total.numpy() == 0.0
+    assert obj.count.numpy() == 0.0
+    assert obj.total.dtype == tf.float32
+    assert obj.count.dtype == tf.float32
+
+
+@pytest.mark.parametrize("values, expected", get_test_data())
+def test_scalar_update_state_gmean(values, expected):
+    obj = GeometricMean()
+    values = tf.constant(values, tf.float32)
+    for v in values:
+        obj.update_state(v)
+    check_result(obj, expected, len(values))
+
+
+@pytest.mark.parametrize("values, expected", get_test_data())
+def test_vector_update_state_gmean(values, expected):
+    obj = GeometricMean()
+    values = tf.constant(values, tf.float32)
+    obj.update_state(values)
+    check_result(obj, expected, len(values))
+
+
+@pytest.mark.parametrize("values, expected", get_test_data())
+def test_call_gmean(values, expected):
+    obj = GeometricMean()
+    result = obj(tf.constant(values, tf.float32))
+    count = obj.count.numpy()
+    assert_result(expected, result)
+    np.testing.assert_equal(len(values), count)
+
+
+def test_reset_states():
+    obj = GeometricMean()
+    obj.update_state([1, 2, 3, 4, 5])
+    obj.reset_states()
+    assert obj.total.numpy() == 0.0
+    assert obj.count.numpy() == 0.0
+
+
+@pytest.mark.parametrize(
+    "values, sample_weight, expected",
+    [
+        ([1, 2, 3, 4, 5], 1, 2.6051712),
+        ([2.1, 4.6, 7.1], [1, 2, 3], 5.014777),
+        ([9.6, 1.8, 8.2], [0.2, 0.5, 0.3], 3.9649222),
+    ],
+)
+def test_sample_weight_gmean(values, sample_weight, expected):
+    obj = GeometricMean()
+    obj.update_state(values, sample_weight=sample_weight)
+    assert_result(expected, obj.result().numpy())
diff --git a/tensorflow_addons/metrics/tests/metrics_test.py b/tensorflow_addons/metrics/tests/metrics_test.py
--- a/tensorflow_addons/metrics/tests/metrics_test.py
+++ b/tensorflow_addons/metrics/tests/metrics_test.py
@@ -19,18 +19,39 @@
 
 
 def test_update_state_signature():
+    public_params = ["sample_weight"]
+    params_comb = [["y_true", "y_pred"], ["values"]]
     for name, obj in inspect.getmembers(metrics):
         if inspect.isclass(obj) and issubclass(obj, Metric):
-            check_update_state_signature(obj)
+            check_update_state_signature(obj, public_params, params_comb)
 
 
-def check_update_state_signature(metric_class):
+def check_update_state_signature(metric_class, public_params, case_list):
+    error_msg = (
+        "Class {} is missing the parameter {} in the `update_state` "
+        "method. If the method doesn't use this argument, declare "
+        "it anyway and raise a UserWarning if it is "
+        "not None."
+    )
+
     update_state_signature = inspect.signature(metric_class.update_state)
-    for expected_parameter in ["y_true", "y_pred", "sample_weight"]:
-        if expected_parameter not in update_state_signature.parameters.keys():
-            raise ValueError(
-                "Class {} is missing the parameter {} in the `update_state` "
-                "method. If the method doesn't use this argument, declare "
-                "it anyway and raise a UserWarning if it is "
-                "not None.".format(metric_class.__name__, expected_parameter)
-            )
+
+    for expected_param in public_params:
+        if expected_param not in update_state_signature.parameters.keys():
+            raise ValueError(error_msg.format(metric_class.__name__, expected_param))
+
+    missing_params = []
+    for case in case_list:
+        case_miss_params = []
+        case_check = True
+        for expected_param in case:
+            if expected_param not in update_state_signature.parameters.keys():
+                case_miss_params.append(expected_param)
+                case_check = False
+                break
+        if case_check:
+            return
+        missing_params.append(case_miss_params)
+    missing_params = [", ".join(p) for p in missing_params]
+    missing_params = " or ".join(missing_params)
+    raise ValueError(error_msg.format(metric_class.__name__, missing_params))