deanna-emery's picture
updates
93528c6
raw
history blame
11.4 kB
# Copyright 2023 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 for sliced metrics."""
from absl.testing import parameterized
import tensorflow as tf, tf_keras
from official.recommendation.uplift import keras_test_case
from official.recommendation.uplift.metrics import sliced_metric
class MeanSquared(tf_keras.metrics.Mean):
def result(self):
mean = super().result()
return {
self.name + "/mean": mean,
self.name + "/squared": tf.math.square(mean),
}
class SlicedMetricTest(keras_test_case.KerasTestCase, parameterized.TestCase):
@parameterized.named_parameters(
{
"testcase_name": "bool_slicing",
"labels": tf.constant([0, 1, 0, 1], dtype=tf.int32),
"predictions": tf.constant([1, 0, 1, 1], dtype=tf.int32),
"slicing_spec": {"treatment": True, "control": False},
"slicing_feature": tf.constant([True, False, True, False]),
"expected_result": {
"accuracy": 0.25,
"accuracy/treatment": 0,
"accuracy/control": 0.5,
},
},
{
"testcase_name": "int_slicing",
"labels": tf.constant([0, 1, 0, 1], dtype=tf.int32),
"predictions": tf.constant([1, 0, 1, 1], dtype=tf.int32),
"slicing_spec": {"app_usage": 3, "install": 4, "purchase": 5},
"slicing_feature": tf.constant([4, 5, 4, 3], dtype=tf.int32),
"expected_result": {
"accuracy": 0.25,
"accuracy/install": 0,
"accuracy/purchase": 0,
"accuracy/app_usage": 1,
},
},
{
"testcase_name": "str_slicing",
"labels": tf.constant([0, 1, 0, 1], dtype=tf.int32),
"predictions": tf.constant([1, 0, 1, 1], dtype=tf.int32),
"slicing_spec": {"install": "install", "purchase": "purchase"},
"slicing_feature": tf.constant(
["install", "purchase", "install", "app_usage"]
),
"expected_result": {
"accuracy": 0.25,
"accuracy/install": 0,
"accuracy/purchase": 0,
},
},
{
"testcase_name": "int_slicing_weighted",
"labels": tf.constant([0, 1, 0, 1], dtype=tf.int32),
"predictions": tf.constant([1, 0, 1, 1], dtype=tf.int32),
"slicing_spec": {"app_usage": 3, "install": 4, "purchase": 5},
"slicing_feature": tf.constant([4, 5, 4, 3], dtype=tf.int32),
"weights": tf.constant([0, 0, 1, 1], dtype=tf.float32),
"expected_result": {
"accuracy": 0.5,
"accuracy/install": 0,
"accuracy/purchase": 0,
"accuracy/app_usage": 1,
},
},
)
def test_binary_sliced_metric(
self,
labels: tf.Tensor,
predictions: tf.Tensor,
slicing_spec: dict[str, int | str | bool],
slicing_feature: tf.Tensor,
expected_result: dict[str, float],
weights: tf.Tensor | None = None,
):
metric = sliced_metric.SlicedMetric(
tf_keras.metrics.Accuracy("accuracy"),
slicing_spec=slicing_spec,
)
metric.update_state(
labels,
predictions,
sample_weight=weights,
slicing_feature=slicing_feature,
)
self.assertDictEqual(expected_result, metric.result())
@parameterized.named_parameters(
{
"testcase_name": "bool_slicing",
"labels": tf.constant([0, 1, 0, 1], dtype=tf.int32),
"slicing_spec": {"treatment": True, "control": False},
"slicing_feature": tf.constant([True, False, True, False]),
"expected_result": {
"mean": 0.5,
"mean/treatment": 0,
"mean/control": 1,
},
},
{
"testcase_name": "int_slicing",
"labels": tf.constant([0, 1, 0, 1], dtype=tf.int32),
"slicing_spec": {"app_usage": 3, "install": 4, "purchase": 5},
"slicing_feature": tf.constant([4, 5, 4, 3], dtype=tf.int32),
"expected_result": {
"mean": 0.5,
"mean/install": 0,
"mean/purchase": 1,
"mean/app_usage": 1,
},
},
)
def test_unary_sliced_metrics(
self,
labels: tf.Tensor,
slicing_spec: dict[str, int | str | bool],
slicing_feature: tf.Tensor,
expected_result: dict[str, float],
):
metric = sliced_metric.SlicedMetric(
tf_keras.metrics.Mean("mean"),
slicing_spec=slicing_spec,
)
metric.update_state(labels, slicing_feature=slicing_feature)
self.assertDictEqual(expected_result, metric.result())
@parameterized.named_parameters(
{
"testcase_name": "int_slicing",
"labels": tf.constant([0, 1, 0, 1], dtype=tf.int32),
"slicing_spec": {"app_usage": 3, "install": 4, "purchase": 5},
"slicing_feature": tf.constant([4, 5, 4, 3], dtype=tf.int32),
"expected_result": {
"msq/mean": 0.5,
"msq/mean/install": 0,
"msq/mean/purchase": 1,
"msq/mean/app_usage": 1,
"msq/squared": 0.25,
"msq/squared/install": 0,
"msq/squared/purchase": 1,
"msq/squared/app_usage": 1,
},
},
{
"testcase_name": "str_slicing",
"labels": tf.constant([0, 1, 0, 1], dtype=tf.int32),
"slicing_spec": {"install": "install", "purchase": "purchase"},
"slicing_feature": tf.constant(
["install", "purchase", "install", "app_usage"]
),
"expected_result": {
"msq/mean": 0.5,
"msq/mean/install": 0,
"msq/mean/purchase": 1,
"msq/squared": 0.25,
"msq/squared/install": 0,
"msq/squared/purchase": 1,
},
},
)
def test_metric_with_dict_result(
self,
labels: tf.Tensor,
slicing_spec: dict[str, int | str | bool],
slicing_feature: tf.Tensor,
expected_result: dict[str, float],
):
metric = sliced_metric.SlicedMetric(
MeanSquared("msq"), slicing_spec=slicing_spec
)
metric.update_state(labels, slicing_feature=slicing_feature)
self.assertDictEqual(expected_result, metric.result())
@parameterized.named_parameters(
{
"testcase_name": "empty_slicing_spec",
"slicing_spec": {},
},
{
"testcase_name": "incompatible_types",
"slicing_spec": {"a": True, "b": 1, "c": 0},
},
{
"testcase_name": "duplicate_slicing_values",
"slicing_spec": {"a": True, "b": False, "c": True},
},
)
def test_invalid_inputs(self, slicing_spec):
self.assertRaises(
ValueError,
sliced_metric.SlicedMetric,
tf_keras.metrics.Mean(),
slicing_spec=slicing_spec,
)
@parameterized.named_parameters(
{
"testcase_name": "float_to_int",
"slicing_spec": {"a": 1, "b": 2},
"slicing_feature": tf.constant([1, 2, 1, 0], dtype=tf.float32),
},
{
"testcase_name": "int_to_bool",
"slicing_spec": {"a": False, "b": True},
"slicing_feature": tf.constant([1, 0, 0, 1], dtype=tf.int32),
},
)
def test_invalid_update(self, slicing_spec, slicing_feature):
# Invalid cast
metric = sliced_metric.SlicedMetric(
tf_keras.metrics.Mean(), slicing_spec=slicing_spec
)
self.assertRaises(
ValueError,
metric.update_state,
values=tf.constant([1, 0, 1, 0], dtype=tf.int32),
slicing_feature=slicing_feature,
)
@parameterized.named_parameters(
{
"testcase_name": "inputs_2x2_slicing_feature_2",
"slicing_feature": tf.constant([False, True]),
"expected_result": {
"accuracy": 0.5,
"accuracy/control": 0.5,
"accuracy/treatment": 0.5,
},
},
{
"testcase_name": "inputs_2x2_slicing_feature_1x2",
"slicing_feature": tf.constant([[False, True]]),
"expected_result": {
"accuracy": 0.5,
"accuracy/control": 0,
"accuracy/treatment": 1.0,
},
},
{
"testcase_name": "inputs_2x2_slicing_feature_1x2_weight_1",
"slicing_feature": tf.constant([[False, True]]),
"sample_weight": tf.constant([1.0]),
"expected_result": {
"accuracy": 0.5,
"accuracy/control": 0,
"accuracy/treatment": 1.0,
},
},
{
"testcase_name": "inputs_2x2_slicing_feature_2_weight_2",
"slicing_feature": tf.constant([False, True]),
"sample_weight": tf.constant([0.5, 0.5]),
"expected_result": {
"accuracy": 0.5,
"accuracy/control": 0.5,
"accuracy/treatment": 0.5,
},
},
)
def test_broadcastable_weights_and_slicing_feature(
self,
slicing_feature: tf.Tensor,
expected_result: dict[str, float],
sample_weight: tf.Tensor | None = None,
):
metric = sliced_metric.SlicedMetric(
tf_keras.metrics.Accuracy("accuracy"),
slicing_spec={"control": False, "treatment": True},
)
metric.update_state(
tf.constant([[0, 1], [0, 1]], dtype=tf.int32),
tf.constant([[1, 1], [1, 1]], dtype=tf.int32),
sample_weight=sample_weight,
slicing_feature=slicing_feature,
)
self.assertDictEqual(expected_result, metric.result())
def test_batched_inputs(self):
metric = sliced_metric.SlicedMetric(
tf_keras.metrics.Accuracy("accuracy"),
slicing_spec={"install": 4, "purchase": 5},
)
metric.update_state(
tf.constant([[0, 1], [1, 0]], dtype=tf.int32),
tf.constant([[1, 1], [1, 1]], dtype=tf.int32),
slicing_feature=tf.constant([[4, 5], [4, 3]], dtype=tf.int32),
)
expected_result = {
"accuracy": 0.5,
"accuracy/install": 0.5,
"accuracy/purchase": 1.0,
}
self.assertDictEqual(expected_result, metric.result())
def test_metric_config(self):
metric = sliced_metric.SlicedMetric(
tf_keras.metrics.SparseTopKCategoricalAccuracy(k=2, name="accuracy@2"),
slicing_spec={"a": False, "b": True},
slicing_feature_dtype=tf.bool,
name="sliced_accuracy",
)
y_true = tf.constant([1, 0, 1, 0])
y_pred = tf.constant([[0.1, 0.9], [0.8, 0.2], [0.7, 0.3], [0.6, 0.4]])
slicing_feature = tf.constant([True, False, False, True])
self.assertLayerConfigurable(
metric, y_true=y_true, y_pred=y_pred, slicing_feature=slicing_feature
)
if __name__ == "__main__":
tf.test.main()