@@ -1027,4 +1027,90 @@ defmodule Scholar.NaiveBayes.CategoricalTest do
10271027 )
10281028 end
10291029 end
1030+
1031+ describe "predict with num_features != num_classes" do
1032+ test "predicts when the feature count differs from the class count" do
1033+ # Every doctest and existing test happens to use num_features ==
1034+ # num_classes, which is the only case where a jll accumulator sized
1035+ # from x instead of from the class axis works.
1036+ x = Nx . tensor ( [ [ 1 , 2 , 2 ] , [ 1 , 2 , 1 ] , [ 2 , 2 , 0 ] , [ 0 , 1 , 2 ] , [ 2 , 0 , 1 ] ] )
1037+ y = Nx . tensor ( [ 0 , 1 , 1 , 0 , 1 ] )
1038+ model = Categorical . fit ( x , y , num_classes: 2 )
1039+
1040+ x_test = Nx . tensor ( [ [ 1 , 2 , 2 ] , [ 2 , 0 , 1 ] , [ 0 , 1 , 0 ] ] )
1041+
1042+ assert Nx . shape ( Categorical . predict_probability ( model , x_test ) ) == { 3 , 2 }
1043+ assert Categorical . predict ( model , x_test , Nx . tensor ( [ 0 , 1 ] ) ) == Nx . tensor ( [ 0 , 1 , 0 ] )
1044+
1045+ # Reference: sklearn.naive_bayes.CategoricalNB on the same data.
1046+ assert_all_close (
1047+ Categorical . predict_probability ( model , x_test ) ,
1048+ Nx . tensor ( [
1049+ [ 0.6973365617433412 , 0.3026634382566586 ] ,
1050+ [ 0.060150375939849614 , 0.9398496240601504 ] ,
1051+ [ 0.6973365617433416 , 0.3026634382566585 ]
1052+ ] ) ,
1053+ atol: 1.0e-5
1054+ )
1055+ end
1056+
1057+ test "check_dim validates against the feature count, not the class count" do
1058+ x = Nx . tensor ( [ [ 1 , 2 , 2 ] , [ 1 , 2 , 1 ] , [ 2 , 2 , 0 ] , [ 0 , 1 , 2 ] , [ 2 , 0 , 1 ] ] )
1059+ y = Nx . tensor ( [ 0 , 1 , 1 , 0 , 1 ] )
1060+ model = Categorical . fit ( x , y , num_classes: 2 )
1061+
1062+ # 3 features, as used for fitting: must be accepted.
1063+ assert Nx . shape ( Categorical . predict_probability ( model , Nx . tensor ( [ [ 1 , 2 , 2 ] ] ) ) ) == { 1 , 2 }
1064+
1065+ # 2 features: must be rejected even though it matches num_classes.
1066+ assert_raise ArgumentError , fn ->
1067+ Categorical . predict_probability ( model , Nx . tensor ( [ [ 1 , 2 ] ] ) )
1068+ end
1069+ end
1070+ end
1071+
1072+ describe "option validation" do
1073+ test "rejects a negative alpha instead of silently producing NaN" do
1074+ x = Nx . tensor ( [ [ 1 , 0 , 1 ] , [ 0 , 1 , 1 ] , [ 1 , 1 , 0 ] ] )
1075+ y = Nx . tensor ( [ 0 , 1 , 1 ] )
1076+
1077+ assert_raise NimbleOptions.ValidationError , fn ->
1078+ Categorical . fit ( x , y , num_classes: 2 , alpha: - 1.0 )
1079+ end
1080+
1081+ assert_raise NimbleOptions.ValidationError , fn ->
1082+ Categorical . fit ( x , y , num_classes: 2 , alpha: [ 1.0 , - 2.0 , 1.0 ] )
1083+ end
1084+ end
1085+
1086+ test "min_categories actually sizes the category axis" do
1087+ x = Nx . tensor ( [ [ 1 , 2 , 3 ] , [ 1 , 3 , 4 ] , [ 2 , 2 , 3 ] , [ 1 , 1 , 3 ] , [ 2 , 1 , 4 ] ] )
1088+ y = Nx . tensor ( [ 0 , 1 , 2 , 1 , 0 ] )
1089+
1090+ # Without the option the category count is inferred from the data (max 4
1091+ # -> 5 categories). Asking for 7 must widen the axis to 7, matching
1092+ # sklearn's CategoricalNB(min_categories=...) semantics.
1093+ inferred = Categorical . fit ( x , y , num_classes: 3 )
1094+ assert Nx . axis_size ( inferred . feature_count , 2 ) == 5
1095+
1096+ widened = Categorical . fit ( x , y , num_classes: 3 , min_categories: [ 7 , 7 , 7 ] )
1097+ assert Nx . axis_size ( widened . feature_count , 2 ) == 7
1098+ end
1099+
1100+ test "keeps class_log_priors in the same type as the rest of the model" do
1101+ x = Nx . tensor ( [ [ 1 , 0 , 1 ] , [ 0 , 1 , 1 ] , [ 1 , 1 , 0 ] ] )
1102+ y = Nx . tensor ( [ 0 , 1 , 1 ] )
1103+
1104+ for opts <- [
1105+ [ num_classes: 2 ] ,
1106+ [ num_classes: 2 , class_priors: [ 0.3 , 0.7 ] ] ,
1107+ [ num_classes: 2 , fit_priors: false ]
1108+ ] do
1109+ model = Categorical . fit ( x , y , opts )
1110+
1111+ assert Nx . type ( model . class_log_priors ) == Nx . type ( model . feature_log_probability ) ,
1112+ "class_log_priors downcast for opts: #{ inspect ( opts ) } "
1113+ end
1114+ end
1115+ end
10301116end
0 commit comments