Skip to content

Commit 50cfc7e

Browse files
committed
[WIP] New unit tests for CCA-UD
Signed-off-by: alvaro <[email protected]>
1 parent fbd0c48 commit 50cfc7e

File tree

1 file changed

+232
-0
lines changed

1 file changed

+232
-0
lines changed

tests/defences/detector/poison/test_clustering_centroid_analysis.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,238 @@ def test_evaluate_defence_no_samples_for_a_class_in_unique_classes(self, MockGro
847847
np.testing.assert_array_equal(arr, expected_is_clean_by_class[i],
848848
err_msg=f"Mismatch in is_clean_by_class at index {i}")
849849

850+
class TestCalculateMisclassificationRate(unittest.TestCase):
851+
"""
852+
Unit tests for the _calculate_misclassification_rate method of ClusteringCentroidAnalysis.
853+
"""
854+
855+
def setUp(self):
856+
"""Set up a ClusteringCentroidAnalysis instance with mocked components."""
857+
self.original_eager_value = tf.config.functions_run_eagerly()
858+
tf.config.run_functions_eagerly(True) # Run functions eagerly for this test class
859+
860+
x_train_dummy = np.array([[1,2]] * 10)
861+
y_train_constructor_dummy = np.array(['A'] * 5 + ['B'] * 5)
862+
benign_indices_dummy = np.arange(10)
863+
864+
with patch('art.defences.detector.poison.clustering_centroid_analysis.ClusteringCentroidAnalysis._extract_submodels',
865+
return_value=(MagicMock(), MagicMock())):
866+
self.defence = ClusteringCentroidAnalysis(
867+
classifier=MagicMock(),
868+
x_train=x_train_dummy,
869+
y_train=y_train_constructor_dummy,
870+
benign_indices=benign_indices_dummy,
871+
final_feature_layer_name="dummy_layer",
872+
misclassification_threshold=0.1
873+
)
874+
875+
self.feature_dim = 5
876+
self.num_benign_samples_class_0 = 3
877+
self.num_benign_samples_class_1 = 2
878+
self.num_benign_samples_class_2 = 4
879+
880+
self.defence.x_benign = np.random.rand(
881+
self.num_benign_samples_class_0 + self.num_benign_samples_class_1 + self.num_benign_samples_class_2,
882+
10
883+
)
884+
self.defence.y_benign = np.array(
885+
[0] * self.num_benign_samples_class_0 +
886+
[1] * self.num_benign_samples_class_1 +
887+
[2] * self.num_benign_samples_class_2
888+
)
889+
self.defence.unique_classes = {0, 1, 2}
890+
891+
self.defence.feature_representation_model = MagicMock(spec=tf.keras.Model)
892+
self.defence.feature_representation_model.predict.return_value = np.random.rand(1, self.feature_dim)
893+
self.defence.classifying_submodel = MagicMock(spec=tf.keras.Sequential)
894+
895+
self.calculate_features_patcher = patch('art.defences.detector.poison.clustering_centroid_analysis._calculate_features')
896+
self.mock_calculate_features = self.calculate_features_patcher.start()
897+
898+
def tearDown(self):
899+
self.calculate_features_patcher.stop()
900+
tf.config.run_functions_eagerly(self.original_eager_value) # Restore original eager mode
901+
self.calculate_features_patcher.stop()
902+
903+
def test_zero_misclassification(self):
904+
"""Test when no samples are misclassified."""
905+
target_class_label = 0
906+
deviation_vector = np.random.rand(self.feature_dim)
907+
908+
mock_features_class1 = np.random.rand(self.num_benign_samples_class_1, self.feature_dim)
909+
mock_features_class2 = np.random.rand(self.num_benign_samples_class_2, self.feature_dim)
910+
self.mock_calculate_features.side_effect = [mock_features_class1, mock_features_class2]
911+
912+
def mock_classifier_predict_side_effect(deviated_features, training=False):
913+
num_unique_classes = len(self.defence.unique_classes)
914+
concrete_batch_size = tf.compat.v1.dimension_value(deviated_features.shape[0])
915+
916+
if concrete_batch_size is None:
917+
dynamic_batch_size = tf.shape(deviated_features)[0]
918+
predicted_indices = tf.ones([dynamic_batch_size], dtype=tf.int32)
919+
symbolic_logits = tf.one_hot(predicted_indices, depth=num_unique_classes, dtype=tf.float32)
920+
return symbolic_logits
921+
else:
922+
num_samples_concrete = concrete_batch_size
923+
logits_np = np.zeros((num_samples_concrete, num_unique_classes))
924+
call_num = self.defence.classifying_submodel.call_count
925+
if call_num == 0:
926+
logits_np[:, 1] = 1.0
927+
elif call_num == 1:
928+
logits_np[:, 2] = 1.0
929+
else:
930+
logits_np[:, 1] = 1.0
931+
return tf.convert_to_tensor(logits_np, dtype=tf.float32)
932+
933+
self.defence.classifying_submodel.side_effect = mock_classifier_predict_side_effect
934+
935+
rate = self.defence._calculate_misclassification_rate(target_class_label, deviation_vector)
936+
self.assertEqual(rate, 0.0)
937+
self.assertEqual(self.mock_calculate_features.call_count, 2)
938+
self.assertEqual(self.defence.classifying_submodel.call_count, 2)
939+
940+
def test_full_misclassification(self):
941+
"""Test when all samples from other classes are misclassified as target_class_label."""
942+
target_class_label = 1
943+
deviation_vector = np.random.rand(self.feature_dim)
944+
945+
mock_features_class0 = np.random.rand(self.num_benign_samples_class_0, self.feature_dim)
946+
mock_features_class2 = np.random.rand(self.num_benign_samples_class_2, self.feature_dim)
947+
self.mock_calculate_features.side_effect = [mock_features_class0, mock_features_class2]
948+
949+
def mock_classifier_predict_side_effect(deviated_features, training=False):
950+
num_unique_classes = len(self.defence.unique_classes)
951+
concrete_batch_size = tf.compat.v1.dimension_value(deviated_features.shape[0])
952+
953+
if concrete_batch_size is None:
954+
dynamic_batch_size = tf.shape(deviated_features)[0]
955+
predicted_indices = tf.fill([dynamic_batch_size], target_class_label)
956+
symbolic_logits = tf.one_hot(predicted_indices, depth=num_unique_classes, dtype=tf.float32)
957+
return symbolic_logits
958+
else:
959+
num_samples_concrete = concrete_batch_size
960+
logits_np = np.zeros((num_samples_concrete, num_unique_classes))
961+
logits_np[:, target_class_label] = 1.0
962+
return tf.convert_to_tensor(logits_np, dtype=tf.float32)
963+
self.defence.classifying_submodel.side_effect = mock_classifier_predict_side_effect
964+
965+
rate = self.defence._calculate_misclassification_rate(target_class_label, deviation_vector)
966+
self.assertEqual(rate, 1.0)
967+
self.assertEqual(self.mock_calculate_features.call_count, 2)
968+
self.assertEqual(self.defence.classifying_submodel.call_count, 2)
969+
970+
def test_partial_misclassification(self):
971+
"""Test with a mix of misclassifications."""
972+
target_class_label = 2
973+
deviation_vector = np.random.rand(self.feature_dim)
974+
975+
mock_features_class0 = np.random.rand(self.num_benign_samples_class_0, self.feature_dim)
976+
mock_features_class1 = np.random.rand(self.num_benign_samples_class_1, self.feature_dim)
977+
self.mock_calculate_features.side_effect = [mock_features_class0, mock_features_class1]
978+
979+
def mock_classifier_predict_side_effect(deviated_features, training=False):
980+
num_unique_classes = len(self.defence.unique_classes)
981+
num_samples_concrete = tf.compat.v1.dimension_value(deviated_features.shape[0]) # Eager mode
982+
983+
logits_np = np.zeros((num_samples_concrete, num_unique_classes))
984+
985+
# Logic based on the number of samples received, assuming distinct counts for class 0 and 1
986+
if num_samples_concrete == self.num_benign_samples_class_0: # Processing features for class 0 (3 samples)
987+
# 1 misclassified as target_class_label (2), 2 correctly as class 0
988+
logits_np[0, target_class_label] = 1.0
989+
logits_np[1, 0] = 1.0
990+
logits_np[2, 0] = 1.0
991+
elif num_samples_concrete == self.num_benign_samples_class_1: # Processing features for class 1 (2 samples)
992+
# 1 misclassified as target_class_label (2), 1 correctly as class 1
993+
logits_np[0, target_class_label] = 1.0
994+
logits_np[1, 1] = 1.0
995+
else:
996+
# This case should ideally not be hit if mock_calculate_features.side_effect is correct
997+
# and num_benign_samples for class 0 and 1 are distinct.
998+
# For safety, make it predict something non-target or raise error.
999+
# For this test, we expect specific inputs, so an error might be appropriate if unexpected shape.
1000+
raise ValueError(f"Unexpected number of samples in mock_classifier_predict_side_effect: {num_samples_concrete}")
1001+
return tf.convert_to_tensor(logits_np, dtype=tf.float32)
1002+
1003+
self.defence.classifying_submodel.side_effect = mock_classifier_predict_side_effect
1004+
1005+
rate = self.defence._calculate_misclassification_rate(target_class_label, deviation_vector)
1006+
self.assertAlmostEqual(rate, 2.0 / (self.num_benign_samples_class_0 + self.num_benign_samples_class_1), places=6)
1007+
self.assertEqual(self.mock_calculate_features.call_count, 2)
1008+
self.assertEqual(self.defence.classifying_submodel.call_count, 2)
1009+
1010+
def test_no_other_classes_exist(self):
1011+
"""Test when there are no 'other' classes to check against."""
1012+
self.defence.unique_classes = {0}
1013+
target_class_label = 0
1014+
deviation_vector = np.random.rand(self.feature_dim)
1015+
1016+
rate = self.defence._calculate_misclassification_rate(target_class_label, deviation_vector)
1017+
self.assertEqual(rate, 0.0)
1018+
self.mock_calculate_features.assert_not_called()
1019+
self.defence.classifying_submodel.assert_not_called()
1020+
1021+
def test_other_classes_exist_but_no_benign_samples(self):
1022+
"""Test when other classes exist, but no benign samples are available for them."""
1023+
target_class_label = 0
1024+
self.defence.unique_classes = {0, 1, 2}
1025+
self.defence.y_benign = np.array([0] * self.num_benign_samples_class_0)
1026+
self.defence.x_benign = np.random.rand(self.num_benign_samples_class_0, 10)
1027+
deviation_vector = np.random.rand(self.feature_dim)
1028+
1029+
rate = self.defence._calculate_misclassification_rate(target_class_label, deviation_vector)
1030+
self.assertEqual(rate, 0.0)
1031+
self.mock_calculate_features.assert_not_called()
1032+
self.defence.classifying_submodel.assert_not_called()
1033+
1034+
def test_batching_multiple_batches_for_one_class(self):
1035+
"""Test when one 'other class' has enough samples to require multiple batches."""
1036+
target_class_label = 0
1037+
deviation_vector = np.random.rand(self.feature_dim)
1038+
1039+
num_samples_class1_large = 200
1040+
num_samples_class2_small = 50
1041+
original_num_benign_samples_class_1 = self.num_benign_samples_class_1
1042+
self.num_benign_samples_class_1 = num_samples_class1_large
1043+
1044+
self.defence.x_benign = np.random.rand(
1045+
self.num_benign_samples_class_0 + num_samples_class1_large + num_samples_class2_small, 10
1046+
)
1047+
self.defence.y_benign = np.array(
1048+
[0] * self.num_benign_samples_class_0 +
1049+
[1] * num_samples_class1_large +
1050+
[2] * num_samples_class2_small
1051+
)
1052+
self.defence.unique_classes = {0, 1, 2}
1053+
1054+
features_batch1_c1 = np.random.rand(128, self.feature_dim)
1055+
features_batch2_c1 = np.random.rand(num_samples_class1_large - 128, self.feature_dim)
1056+
features_batch1_c2 = np.random.rand(num_samples_class2_small, self.feature_dim)
1057+
self.mock_calculate_features.side_effect = [features_batch1_c1, features_batch2_c1, features_batch1_c2]
1058+
1059+
def mock_classifier_predict_side_effect_for_batching_test(deviated_features, training=False):
1060+
num_unique_classes = len(self.defence.unique_classes)
1061+
concrete_batch_size = tf.compat.v1.dimension_value(deviated_features.shape[0])
1062+
1063+
if concrete_batch_size is None:
1064+
dynamic_batch_size = tf.shape(deviated_features)[0]
1065+
predicted_indices = tf.fill([dynamic_batch_size], target_class_label)
1066+
symbolic_logits = tf.one_hot(predicted_indices, depth=num_unique_classes, dtype=tf.float32)
1067+
return symbolic_logits
1068+
else:
1069+
num_samples_concrete = concrete_batch_size
1070+
logits_np = np.zeros((num_samples_concrete, num_unique_classes))
1071+
logits_np[:, target_class_label] = 1.0
1072+
return tf.convert_to_tensor(logits_np, dtype=tf.float32)
1073+
self.defence.classifying_submodel.side_effect = mock_classifier_predict_side_effect_for_batching_test
1074+
1075+
rate = self.defence._calculate_misclassification_rate(target_class_label, deviation_vector)
1076+
self.assertEqual(rate, 1.0)
1077+
self.assertEqual(self.mock_calculate_features.call_count, 3)
1078+
self.assertEqual(self.defence.classifying_submodel.call_count, 3)
1079+
1080+
self.num_benign_samples_class_1 = original_num_benign_samples_class_1
1081+
8501082

8511083
class TestDetectPoison(unittest.TestCase):
8521084
"""

0 commit comments

Comments
 (0)