226 lines
6.5 KiB
Python
226 lines
6.5 KiB
Python
"""This file contains tests for the OutputComposite masker."""
|
|
|
|
import tempfile
|
|
|
|
import numpy as np
|
|
|
|
import shap
|
|
|
|
|
|
def test_output_composite_init():
|
|
"""Test OutputComposite masker initialization."""
|
|
masker = shap.maskers.Fixed()
|
|
|
|
def simple_model(x):
|
|
return np.sum(x)
|
|
|
|
output_composite = shap.maskers.OutputComposite(masker, simple_model)
|
|
|
|
assert output_composite.masker is masker
|
|
assert output_composite.model is simple_model
|
|
|
|
|
|
def test_output_composite_attribute_propagation():
|
|
"""Test that attributes from the underlying masker are propagated."""
|
|
masker = shap.maskers.Fixed()
|
|
|
|
def simple_model(x):
|
|
return x
|
|
|
|
output_composite = shap.maskers.OutputComposite(masker, simple_model)
|
|
|
|
# Check that shape is propagated
|
|
assert hasattr(output_composite, "shape")
|
|
assert output_composite.shape == masker.shape
|
|
|
|
|
|
def test_output_composite_call_with_tuple_output():
|
|
"""Test OutputComposite __call__ when model returns a tuple."""
|
|
masker = shap.maskers.Fixed()
|
|
|
|
def model_with_tuple_output(x):
|
|
return (np.sum(x), np.mean(x))
|
|
|
|
output_composite = shap.maskers.OutputComposite(masker, model_with_tuple_output)
|
|
|
|
test_input = np.array([1, 2, 3, 4, 5])
|
|
mask = np.array([], dtype=bool)
|
|
|
|
result = output_composite(mask, test_input)
|
|
|
|
# Result should be masked input + model output
|
|
assert isinstance(result, tuple)
|
|
# Fixed masker returns 1 element, model returns 2 elements
|
|
assert len(result) == 3
|
|
|
|
|
|
def test_output_composite_call_with_scalar_output():
|
|
"""Test OutputComposite __call__ when model returns a scalar."""
|
|
masker = shap.maskers.Fixed()
|
|
|
|
def model_with_scalar_output(x):
|
|
return np.sum(x)
|
|
|
|
output_composite = shap.maskers.OutputComposite(masker, model_with_scalar_output)
|
|
|
|
test_input = np.array([1, 2, 3, 4, 5])
|
|
mask = np.array([], dtype=bool)
|
|
|
|
result = output_composite(mask, test_input)
|
|
|
|
# Result should be masked input + model output (wrapped in tuple)
|
|
assert isinstance(result, tuple)
|
|
# Fixed masker returns 1 element, model returns 1 element (wrapped)
|
|
assert len(result) == 2
|
|
|
|
|
|
def test_output_composite_call_with_array_output():
|
|
"""Test OutputComposite __call__ when model returns an array."""
|
|
masker = shap.maskers.Fixed()
|
|
|
|
def model_with_array_output(x):
|
|
return x * 2
|
|
|
|
output_composite = shap.maskers.OutputComposite(masker, model_with_array_output)
|
|
|
|
test_input = np.array([1, 2, 3])
|
|
mask = np.array([], dtype=bool)
|
|
|
|
result = output_composite(mask, test_input)
|
|
|
|
assert isinstance(result, tuple)
|
|
assert len(result) == 2
|
|
# Check model output is correct
|
|
np.testing.assert_array_equal(result[1], test_input * 2)
|
|
|
|
|
|
def test_output_composite_serialization():
|
|
"""Test OutputComposite serialization and deserialization."""
|
|
masker = shap.maskers.Fixed()
|
|
|
|
def simple_model(x):
|
|
return np.sum(x)
|
|
|
|
original_composite = shap.maskers.OutputComposite(masker, simple_model)
|
|
|
|
with tempfile.TemporaryFile() as temp_file:
|
|
# Serialize
|
|
original_composite.save(temp_file)
|
|
temp_file.seek(0)
|
|
|
|
# Deserialize
|
|
loaded_composite = shap.maskers.OutputComposite.load(temp_file)
|
|
|
|
# Verify the loaded masker works
|
|
test_input = np.array([1, 2, 3])
|
|
mask = np.array([], dtype=bool)
|
|
|
|
original_result = original_composite(mask, test_input)
|
|
loaded_result = loaded_composite(mask, test_input)
|
|
|
|
# Check results are the same
|
|
assert len(original_result) == len(loaded_result)
|
|
|
|
|
|
def test_output_composite_with_multiple_args():
|
|
"""Test OutputComposite with a model that takes multiple arguments."""
|
|
|
|
# Create a custom masker that takes 2 arguments
|
|
class TwoArgMasker(shap.maskers.Masker):
|
|
def __init__(self):
|
|
self.shape = (None, 0)
|
|
|
|
def __call__(self, mask, *args): # type: ignore[override]
|
|
x, y = args
|
|
return ([x], [y])
|
|
|
|
masker = TwoArgMasker()
|
|
|
|
def model_two_args(x, y):
|
|
return np.sum(x) + np.sum(y)
|
|
|
|
output_composite = shap.maskers.OutputComposite(masker, model_two_args)
|
|
|
|
test_input1 = np.array([1, 2, 3])
|
|
test_input2 = np.array([4, 5, 6])
|
|
mask = np.array([], dtype=bool)
|
|
|
|
result = output_composite(mask, test_input1, test_input2)
|
|
|
|
assert isinstance(result, tuple)
|
|
# Masker returns 2 elements, model returns 1 (wrapped)
|
|
assert len(result) == 3
|
|
assert result[2] == np.sum(test_input1) + np.sum(test_input2)
|
|
|
|
|
|
def test_output_composite_attribute_none_handling():
|
|
"""Test that OutputComposite handles None attributes correctly."""
|
|
|
|
# Create a minimal masker without all optional attributes
|
|
class MinimalMasker(shap.maskers.Masker):
|
|
def __init__(self):
|
|
self.shape = (None, 0)
|
|
# Don't set optional attributes
|
|
|
|
def __call__(self, mask, *args): # type: ignore[override]
|
|
x = args[0]
|
|
return ([x],)
|
|
|
|
masker = MinimalMasker()
|
|
|
|
def simple_model(x):
|
|
return x
|
|
|
|
output_composite = shap.maskers.OutputComposite(masker, simple_model)
|
|
|
|
# Should have shape but not necessarily other attributes
|
|
assert hasattr(output_composite, "shape")
|
|
# These attributes shouldn't be set if not present in masker
|
|
assert not hasattr(output_composite, "invariants") or output_composite.invariants is None
|
|
|
|
|
|
def test_output_composite_text_data_flag():
|
|
"""Test that text_data flag propagates from underlying masker."""
|
|
|
|
class TextMasker(shap.maskers.Masker):
|
|
def __init__(self):
|
|
self.shape = (None, 0)
|
|
self.text_data = True
|
|
|
|
def __call__(self, mask, *args): # type: ignore[override]
|
|
x = args[0]
|
|
return ([x],)
|
|
|
|
masker = TextMasker()
|
|
|
|
def simple_model(x):
|
|
return x
|
|
|
|
output_composite = shap.maskers.OutputComposite(masker, simple_model)
|
|
|
|
assert hasattr(output_composite, "text_data")
|
|
assert output_composite.text_data is True
|
|
|
|
|
|
def test_output_composite_image_data_flag():
|
|
"""Test that image_data flag propagates from underlying masker."""
|
|
|
|
class ImageMasker(shap.maskers.Masker):
|
|
def __init__(self):
|
|
self.shape = (None, 0)
|
|
self.image_data = True
|
|
|
|
def __call__(self, mask, *args): # type: ignore[override]
|
|
x = args[0]
|
|
return ([x],)
|
|
|
|
masker = ImageMasker()
|
|
|
|
def simple_model(x):
|
|
return x
|
|
|
|
output_composite = shap.maskers.OutputComposite(masker, simple_model)
|
|
|
|
assert hasattr(output_composite, "image_data")
|
|
assert output_composite.image_data is True
|