1515# ==============================================================================
1616
1717from abc import ABCMeta , abstractmethod
18- from numbers import Number
1918
2019import numpy as np
2120
22- from daal4py .sklearn ._utils import daal_check_version , get_dtype , make2d
21+ from daal4py .sklearn ._utils import daal_check_version
2322from onedal ._device_offload import supports_queue
2423from onedal .common ._backend import bind_default_backend
25- from onedal .utils import _sycl_queue_manager as QM
2624
27- from .._config import _get_config
2825from ..common ._estimator_checks import _check_is_fitted
29- from ..common ._mixin import ClassifierMixin
3026from ..datatypes import from_table , to_table
31- from ..utils ._array_api import _get_sycl_namespace
32- from ..utils .validation import (
33- _check_array ,
34- _check_n_features ,
35- _check_X_y ,
36- _is_csr ,
37- _num_features ,
38- _type_of_target ,
39- )
27+ from ..utils .validation import _check_n_features , _is_csr , _num_features
4028
4129
4230class BaseLogisticRegression (metaclass = ABCMeta ):
@@ -49,14 +37,13 @@ def __init__(self, tol, C, fit_intercept, solver, max_iter, algorithm):
4937 self .max_iter = max_iter
5038 self .algorithm = algorithm
5139
52- @abstractmethod
53- def train (self , params , X , y ): ...
40+ @bind_default_backend ( "logistic_regression.classification" )
41+ def train (self , params , X , y , queue = None ): ...
5442
55- @abstractmethod
56- def infer (self , params , X ): ...
43+ @bind_default_backend ( "logistic_regression.classification" )
44+ def infer (self , params , model , X , queue = None ): ...
5745
58- # direct access to the backend model constructor
59- @abstractmethod
46+ @bind_default_backend ("logistic_regression.classification" )
6047 def model (self ): ...
6148
6249 def _get_onedal_params (self , is_csr , dtype = np .float32 ):
@@ -76,172 +63,64 @@ def _get_onedal_params(self, is_csr, dtype=np.float32):
7663 ),
7764 }
7865
79- def _fit (self , X , y ):
80- use_raw_input = _get_config ()["use_raw_input" ] is True
81-
82- sparsity_enabled = daal_check_version ((2024 , "P" , 700 ))
83- if not use_raw_input :
84- X , y = _check_X_y (
85- X ,
86- y ,
87- accept_sparse = sparsity_enabled ,
88- force_all_finite = True ,
89- accept_2d_y = False ,
90- dtype = [np .float64 , np .float32 ],
91- )
92- if _type_of_target (y ) != "binary" :
93- raise ValueError ("Only binary classification is supported" )
94-
95- self .classes_ , y = np .unique (y , return_inverse = True )
96- y = y .astype (dtype = np .int32 )
97- else :
98- _ , xp , _ = _get_sycl_namespace (X )
99- # try catch needed for raw_inputs + array_api data where unlike
100- # numpy the way to yield unique values is via `unique_values`
101- # This should be removed when refactored for gpu zero-copy
102- try :
103- self .classes_ = xp .unique (y )
104- except AttributeError :
105- self .classes_ = xp .unique_values (y )
106-
107- n_classes = len (self .classes_ )
108- if n_classes != 2 :
109- raise ValueError ("Only binary classification is supported" )
66+ @supports_queue
67+ def fit (self , X , y , queue = None ):
68+
11069 is_csr = _is_csr (X )
11170
11271 self .n_features_in_ = _num_features (X , fallback_1d = True )
113- X_table , y_table = to_table (X , y , queue = QM .get_global_queue ())
72+
73+ X_table , y_table = to_table (X , y , queue = queue )
11474 params = self ._get_onedal_params (is_csr , X_table .dtype )
11575
11676 result = self .train (params , X_table , y_table )
11777
11878 self ._onedal_model = result .model
79+
11980 self .n_iter_ = np .array ([result .iterations_count ])
12081
12182 # _n_inner_iter is the total number of cg-solver iterations
12283 if daal_check_version ((2024 , "P" , 300 )) and self .solver == "newton-cg" :
12384 self ._n_inner_iter = result .inner_iterations_count
12485
125- coeff = from_table (result .model .packed_coefficients )
86+ coeff = from_table (result .model .packed_coefficients , like = X )
12687 self .coef_ , self .intercept_ = coeff [:, 1 :], coeff [:, 0 ]
12788
12889 return self
12990
130- def _create_model (self ):
131- m = self .model ()
132-
133- coefficients = self .coef_
134- dtype = get_dtype (coefficients )
135- coefficients = np .asarray (coefficients , dtype = dtype )
136-
137- if coefficients .ndim == 2 :
138- n_features_in = coefficients .shape [1 ]
139- assert coefficients .shape [0 ] == 1
140- else :
141- n_features_in = coefficients .size
142-
143- intercept = self .intercept_
144- if not isinstance (intercept , Number ):
145- intercept = np .asarray (intercept , dtype = dtype )
146- assert intercept .size == 1
147-
148- intercept = _check_array (
149- intercept ,
150- dtype = [np .float64 , np .float32 ],
151- force_all_finite = True ,
152- ensure_2d = False ,
153- )
154- coefficients = _check_array (
155- coefficients ,
156- dtype = [np .float64 , np .float32 ],
157- force_all_finite = True ,
158- ensure_2d = False ,
159- )
160-
161- coefficients , intercept = make2d (coefficients ), make2d (intercept )
162-
163- assert coefficients .shape == (1 , n_features_in )
164- assert intercept .shape == (1 , 1 )
165-
166- desired_shape = (1 , n_features_in + 1 )
167- packed_coefficients = np .zeros (desired_shape , dtype = dtype )
168-
169- packed_coefficients [:, 1 :] = coefficients
170- if self .fit_intercept :
171- packed_coefficients [:, 0 ][:, np .newaxis ] = intercept
172-
173- m .packed_coefficients = to_table (packed_coefficients , queue = QM .get_global_queue ())
174-
175- self ._onedal_model = m
176-
177- return m
178-
179- def _infer (self , X ):
91+ def _infer (self , X , queue = None ):
18092 _check_is_fitted (self )
18193
182- sparsity_enabled = daal_check_version ((2024 , "P" , 700 ))
183-
184- if not _get_config ()["use_raw_input" ]:
185- X = _check_array (
186- X ,
187- dtype = [np .float64 , np .float32 ],
188- accept_sparse = sparsity_enabled ,
189- force_all_finite = True ,
190- ensure_2d = False ,
191- accept_large_sparse = sparsity_enabled ,
192- )
19394 is_csr = _is_csr (X )
194- _check_n_features (self , X , False )
19595
196- X = make2d ( X )
96+ _check_n_features ( self , X , False )
19797
198- if hasattr (self , "_onedal_model" ):
199- model = self ._onedal_model
200- else :
201- model = self ._create_model ()
98+ assert hasattr (self , "_onedal_model" )
20299
203- X_table = to_table (X , queue = QM . get_global_queue () )
204- params = self ._get_onedal_params (is_csr , X .dtype )
100+ X_table = to_table (X , queue = queue )
101+ params = self ._get_onedal_params (is_csr , X_table .dtype )
205102
206- result = self .infer (params , model , X_table )
103+ result = self .infer (params , self . _onedal_model , X_table )
207104 return result
208105
209- def _predict (self , X ):
210- result = self ._infer (X )
211- _ , xp , _ = _get_sycl_namespace (X )
212- y = from_table (result .responses , like = X )
213- y = xp .take (xp .asarray (self .classes_ ), xp .reshape (y , (- 1 ,)), axis = 0 )
106+ @supports_queue
107+ def predict (self , X , queue = None , classes = None ):
108+ result = self ._infer (X , queue )
109+
110+ # Starting from sklearn 1.9 type of predicted labels should match the type of self.classes_
111+ # In general case, classes attribute is provided from sklearnex estimator
112+ # In case it's not provided, result would be of the same type as X
113+ y = from_table (result .responses , like = classes if classes is not None else X )
214114 return y
215115
216- def _predict_proba ( self , X ):
217- result = self . _infer ( X )
218- _ , xp , _ = _get_sycl_namespace ( X )
116+ @ supports_queue
117+ def predict_proba ( self , X , queue = None ):
118+ result = self . _infer ( X , queue )
219119 y = from_table (result .probabilities , like = X )
220- y = xp .reshape (y , - 1 )
221- return xp .stack ([1 - y , y ], axis = 1 )
222-
223- def _predict_log_proba (self , X ):
224- _ , xp , _ = _get_sycl_namespace (X )
225- y_proba = self ._predict_proba (X )
226- # These are the same thresholds used by oneDAL during the model fitting procedure
227- if y_proba .dtype == np .float32 :
228- min_prob = 1e-7
229- max_prob = 1.0 - 1e-7
230- else :
231- min_prob = 1e-15
232- max_prob = 1.0 - 1e-15
233- y_proba = xp .clip (y_proba , min_prob , max_prob )
234- return xp .log (y_proba )
235-
236- def _decision_function (self , X ):
237- _ , xp , _ = _get_sycl_namespace (X )
238- raw = xp .matmul (X , xp .reshape (self .coef_ , - 1 ))
239- if self .fit_intercept :
240- raw += self .intercept_
241- return raw
242-
243-
244- class LogisticRegression (ClassifierMixin , BaseLogisticRegression ):
120+ return y
121+
122+
123+ class LogisticRegression (BaseLogisticRegression ):
245124
246125 def __init__ (
247126 self ,
@@ -262,32 +141,3 @@ def __init__(
262141 max_iter = max_iter ,
263142 algorithm = algorithm ,
264143 )
265-
266- @bind_default_backend ("logistic_regression.classification" )
267- def train (self , params , X , y , queue = None ): ...
268-
269- @bind_default_backend ("logistic_regression.classification" )
270- def infer (self , params , X , model , queue = None ): ...
271-
272- @bind_default_backend ("logistic_regression.classification" )
273- def model (self ): ...
274-
275- @supports_queue
276- def fit (self , X , y , queue = None ):
277- return self ._fit (X , y )
278-
279- @supports_queue
280- def predict (self , X , queue = None ):
281- return self ._predict (X )
282-
283- @supports_queue
284- def predict_proba (self , X , queue = None ):
285- return self ._predict_proba (X )
286-
287- @supports_queue
288- def predict_log_proba (self , X , queue = None ):
289- return self ._predict_log_proba (X )
290-
291- @supports_queue
292- def decision_function (self , X , queue = None ):
293- return self ._decision_function (X )
0 commit comments