@@ -2879,12 +2879,10 @@ def test_logistic_regression_array_api_compliance(
28792879 # the intercept would be non-zero.
28802880 lr_params = dict (C = 1e-2 , solver = solver , max_iter = 500 , class_weight = class_weight )
28812881 lr_params ["tol" ] = 1e-6 if dtype_name == "float32" else 1e-12
2882- with warnings .catch_warnings ():
2883- # Make sure that we converge in the reference fit.
2884- lr_np = LogisticRegression (** lr_params ).fit (
2885- X_np , y_np , sample_weight = sample_weight
2886- )
2887- assert lr_np .n_iter_ < lr_np .max_iter
2882+
2883+ lr_np = LogisticRegression (** lr_params ).fit (X_np , y_np , sample_weight = sample_weight )
2884+ # Make sure that the reference fit converged.
2885+ assert lr_np .n_iter_ < lr_np .max_iter
28882886
28892887 # Test that C was not too large for meaningful testing.
28902888 assert np .abs (lr_np .coef_ ).max () > 0.1
@@ -2900,14 +2898,11 @@ def test_logistic_regression_array_api_compliance(
29002898 rtol = 1e-4 if dtype_name == "float32" else 1e-8
29012899
29022900 with config_context (array_api_dispatch = True ):
2903- with warnings .catch_warnings ():
2904- # Make sure that we converge when using the namespace/device
2905- # specific fit.
2906- warnings .simplefilter ("error" , ConvergenceWarning )
2907- lr_xp = LogisticRegression (** lr_params ).fit (
2908- X_xp , y_xp_or_np , sample_weight = sample_weight
2909- )
2910-
2901+ lr_xp = LogisticRegression (** lr_params ).fit (
2902+ X_xp , y_xp_or_np , sample_weight = sample_weight
2903+ )
2904+ # Make sure that we converge when using the namespace/device
2905+ # specific fit.
29112906 assert lr_xp .n_iter_ .shape == lr_np .n_iter_ .shape
29122907 assert int (lr_xp .n_iter_ [0 ]) < lr_xp .max_iter
29132908
@@ -2946,6 +2941,128 @@ def test_logistic_regression_array_api_compliance(
29462941 assert_array_equal (prediction_xp , prediction_np )
29472942
29482943
2944+ @pytest .mark .parametrize ("solver" , ["lbfgs" , "newton-cg" ])
2945+ @pytest .mark .parametrize ("refit" , [True , False ])
2946+ @pytest .mark .parametrize ("binary" , [False , True ])
2947+ @pytest .mark .parametrize ("use_str_y" , [False , True ])
2948+ @pytest .mark .parametrize ("use_sample_weight" , [False , True ])
2949+ @pytest .mark .parametrize (
2950+ "array_namespace, device_name, dtype_name" ,
2951+ yield_namespace_device_dtype_combinations (),
2952+ )
2953+ @pytest .mark .filterwarnings ("error::sklearn.exceptions.ConvergenceWarning" )
2954+ def test_logistic_regression_cv_array_api_compliance (
2955+ solver ,
2956+ refit ,
2957+ binary ,
2958+ use_str_y ,
2959+ use_sample_weight ,
2960+ array_namespace ,
2961+ device_name ,
2962+ dtype_name ,
2963+ ):
2964+ xp , device = _array_api_for_tests (array_namespace , device_name , dtype_name )
2965+ X_np = iris .data .astype (dtype_name , copy = True )
2966+ n_samples , _ = X_np .shape
2967+ X_xp = xp .asarray (X_np , device = device )
2968+ if use_str_y :
2969+ if binary :
2970+ target = (iris .target > 0 ).astype (np .int64 )
2971+ target = np .array (["setosa" , "not-setosa" ])[target ]
2972+ class_weight = {"setosa" : 1.0 , "not-setosa" : 3.0 }
2973+ else :
2974+ target = iris .target_names [iris .target ]
2975+ class_weight = {"virginica" : 1.0 , "setosa" : 2.0 , "versicolor" : 3.0 }
2976+ y_np = target .copy ()
2977+ y_xp_or_np = np .asarray (y_np , copy = True )
2978+ else :
2979+ if binary :
2980+ target = (iris .target > 0 ).astype (np .int64 )
2981+ class_weight = {0 : 1.0 , 1 : 3.0 }
2982+ else :
2983+ target = iris .target
2984+ class_weight = {0 : 1.0 , 1 : 2.0 , 2 : 3.0 }
2985+ y_np = target .astype (dtype_name )
2986+ y_xp_or_np = xp .asarray (y_np , device = device )
2987+
2988+ if use_sample_weight :
2989+ sample_weight = (
2990+ np .random .default_rng (0 )
2991+ .uniform (- 1 , 5 , size = n_samples )
2992+ .clip (0 , None )
2993+ .astype (dtype_name )
2994+ )
2995+ else :
2996+ sample_weight = None
2997+
2998+ cv = StratifiedKFold (2 , shuffle = False )
2999+ precomputed_folds = list (cv .split (X_np , y_np ))
3000+ atol = _atol_for_type (dtype_name ) * 10
3001+ if dtype_name == "float32" :
3002+ # TODO: this test is currently very sensitive to the value
3003+ # of the solver tol, particularly for "lbgfs". The rtol value
3004+ # is also quite high. Therefore try to investigate the numerical
3005+ # stability of the convergence with "lbgfs" in a dedicated
3006+ # follow up issue and PR.
3007+ if solver == "lbfgs" :
3008+ solver_tol = 5e-3
3009+ rtol = 6e-2
3010+ else :
3011+ solver_tol = 1e-5
3012+ rtol = 5e-3
3013+ else :
3014+ solver_tol = 1e-10
3015+ rtol = 5e-5
3016+
3017+ lr_cv_params = dict (
3018+ Cs = [0.01 , 0.001 ],
3019+ cv = precomputed_folds ,
3020+ solver = solver ,
3021+ tol = solver_tol ,
3022+ max_iter = 200 ,
3023+ class_weight = class_weight ,
3024+ scoring = "neg_log_loss" ,
3025+ use_legacy_attributes = False ,
3026+ refit = refit ,
3027+ )
3028+
3029+ lr_cv_np = LogisticRegressionCV (** lr_cv_params ).fit (
3030+ X_np , y_np , sample_weight = sample_weight
3031+ )
3032+ # Make sure that the reference fit converged.
3033+ assert np .max (lr_cv_np .n_iter_ ) < lr_cv_np .max_iter
3034+
3035+ # Test that C was not too large for meaningful testing.
3036+ assert np .abs (lr_cv_np .coef_ ).max () > 0.1
3037+
3038+ prediction_np = lr_cv_np .predict (X_np )
3039+ score_np = lr_cv_np .score (X_np , y_np )
3040+ xp , _ = _array_api_for_tests (array_namespace , device_name )
3041+ with config_context (array_api_dispatch = True ):
3042+ lr_cv_xp = LogisticRegressionCV (** lr_cv_params ).fit (
3043+ X_xp , y_xp_or_np , sample_weight = sample_weight
3044+ )
3045+ assert lr_cv_xp .n_iter_ .shape == lr_cv_np .n_iter_ .shape
3046+ assert xp .max (lr_cv_xp .n_iter_ ) < lr_cv_xp .max_iter
3047+
3048+ for attr_name in ("scores_" , "coefs_paths_" , "coef_" , "intercept_" ):
3049+ attr_xp = getattr (lr_cv_xp , attr_name )
3050+ attr_np = getattr (lr_cv_np , attr_name )
3051+ assert_allclose (
3052+ move_to (attr_xp , xp = np , device = "cpu" ), attr_np , rtol = rtol , atol = atol
3053+ )
3054+ assert attr_xp .dtype == X_xp .dtype
3055+ assert array_api_device (attr_xp ) == array_api_device (X_xp )
3056+
3057+ prediction_xp = lr_cv_xp .predict (X_xp )
3058+ if not use_str_y :
3059+ prediction_xp = move_to (prediction_xp , xp = np , device = "cpu" )
3060+ assert_array_equal (prediction_xp , prediction_np )
3061+
3062+ score_xp = lr_cv_xp .score (X_xp , y_xp_or_np )
3063+ assert_allclose (score_xp , score_np , rtol = rtol , atol = atol )
3064+
3065+
29493066# TODO(1.10): remove when penalty is removed
29503067@pytest .mark .filterwarnings ("ignore:'penalty' was deprecated" )
29513068@pytest .mark .parametrize ("penalty, l1_ratio" , [("l1" , 0.0 ), ("l2" , 1.0 )])
0 commit comments