@@ -847,6 +847,238 @@ def test_evaluate_defence_no_samples_for_a_class_in_unique_classes(self, MockGro
847
847
np .testing .assert_array_equal (arr , expected_is_clean_by_class [i ],
848
848
err_msg = f"Mismatch in is_clean_by_class at index { i } " )
849
849
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
+
850
1082
851
1083
class TestDetectPoison (unittest .TestCase ):
852
1084
"""
0 commit comments