99from scipy import sparse
1010
1111from sklearn .utils ._array_api import (
12+ _is_numpy_namespace ,
13+ _ravel ,
1214 get_namespace ,
1315 get_namespace_and_device ,
1416 move_to ,
@@ -172,7 +174,11 @@ def weight_intercept(self, coef):
172174 else :
173175 # reshape to (n_classes, n_dof)
174176 if coef .ndim == 1 :
175- weights = coef .reshape ((self .base_loss .n_classes , - 1 ), order = "F" )
177+ xp , _ = get_namespace (coef )
178+ if _is_numpy_namespace (xp ):
179+ weights = coef .reshape ((self .base_loss .n_classes , - 1 ), order = "F" )
180+ else :
181+ weights = xp .reshape (coef , (- 1 , self .base_loss .n_classes )).T
176182 else :
177183 weights = coef
178184 if self .fit_intercept :
@@ -351,6 +357,11 @@ def loss_gradient(
351357
352358 grad_pointwise /= sw_sum
353359
360+ # TODO: The lbfgs solver currently only works with numpy arrays and expects
361+ # both the coef argument and the result to be in numpy. So, for the time
362+ # being, we stick with a mix of numpy and array API. We could consider
363+ # moving coef to the device of X if lbgfs also becomes compatible with the
364+ # array API.
354365 if not self .base_loss .is_multiclass :
355366 grad = np .empty_like (coef , dtype = weights .dtype )
356367 X_grad = X .T @ grad_pointwise
@@ -431,23 +442,30 @@ def gradient(
431442 sample_weight = sample_weight ,
432443 n_threads = n_threads ,
433444 )
434- sw_sum = n_samples if sample_weight is None else np .sum (sample_weight )
445+ xp , _ , device = get_namespace_and_device (X , y , sample_weight )
446+ sw_sum = n_samples if sample_weight is None else xp .sum (sample_weight )
435447 grad_pointwise /= sw_sum
436448
437449 if not self .base_loss .is_multiclass :
438- grad = np .empty_like (coef , dtype = weights .dtype )
450+ grad = xp .empty_like (coef , dtype = weights .dtype , device = device )
439451 grad [:n_features ] = X .T @ grad_pointwise + l2_reg_strength * weights
440452 if self .fit_intercept :
441- grad [- 1 ] = grad_pointwise .sum ()
453+ grad [- 1 ] = xp .sum (grad_pointwise )
442454 return grad
443455 else :
444- grad = np .empty ((n_classes , n_dof ), dtype = weights .dtype , order = "F" )
456+ if _is_numpy_namespace (xp ):
457+ grad = np .empty ((n_classes , n_dof ), dtype = weights .dtype , order = "F" )
458+ else :
459+ grad = xp .empty ((n_classes , n_dof ), dtype = weights .dtype , device = device )
445460 # gradient.shape = (n_samples, n_classes)
446461 grad [:, :n_features ] = grad_pointwise .T @ X + l2_reg_strength * weights
447462 if self .fit_intercept :
448- grad [:, - 1 ] = grad_pointwise .sum (axis = 0 )
463+ grad [:, - 1 ] = xp .sum (grad_pointwise , axis = 0 )
449464 if coef .ndim == 1 :
450- return grad .ravel (order = "F" )
465+ if _is_numpy_namespace (xp ):
466+ return grad .ravel (order = "F" )
467+ else :
468+ return _ravel (grad .T , xp = xp )
451469 else :
452470 return grad
453471
@@ -756,7 +774,9 @@ def gradient_hessian_product(
756774 (n_samples , n_features ), n_classes = X .shape , self .base_loss .n_classes
757775 n_dof = n_features + int (self .fit_intercept )
758776 weights , intercept , raw_prediction = self .weight_intercept_raw (coef , X )
759- sw_sum = n_samples if sample_weight is None else np .sum (sample_weight )
777+ xp , _ , device = get_namespace_and_device (X , y , sample_weight )
778+ is_numpy_ns = _is_numpy_namespace (xp )
779+ sw_sum = n_samples if sample_weight is None else xp .sum (sample_weight )
760780
761781 if not self .base_loss .is_multiclass :
762782 grad_pointwise , hess_pointwise = self .base_loss .gradient_hessian (
@@ -767,39 +787,45 @@ def gradient_hessian_product(
767787 )
768788 grad_pointwise /= sw_sum
769789 hess_pointwise /= sw_sum
770- grad = np .empty_like (coef , dtype = weights .dtype )
790+ grad = xp .empty_like (coef , dtype = weights .dtype )
771791 grad [:n_features ] = X .T @ grad_pointwise + l2_reg_strength * weights
772792 if self .fit_intercept :
773- grad [- 1 ] = grad_pointwise .sum ()
793+ grad [- 1 ] = xp .sum (grad_pointwise )
774794
775795 # Precompute as much as possible: hX, hX_sum and hessian_sum
776- hessian_sum = hess_pointwise .sum ()
796+ hessian_sum = xp .sum (hess_pointwise )
777797 if sparse .issparse (X ):
778798 hX = (
779799 sparse .dia_array ((hess_pointwise , 0 ), shape = (n_samples , n_samples ))
780800 @ X
781801 )
782802 else :
783- hX = hess_pointwise [:, np . newaxis ] * X
803+ hX = hess_pointwise [:, None ] * X
784804
785805 if self .fit_intercept :
786- # Calculate the double derivative with respect to intercept.
787- # Note: In case hX is sparse, hX.sum is a matrix object.
788- hX_sum = np .squeeze (np .asarray (hX .sum (axis = 0 )))
789- # prevent squeezing to zero-dim array if n_features == 1
790- hX_sum = np .atleast_1d (hX_sum )
806+ # Calculate the double derivative with respect to the intercept.
807+ if sparse .issparse (X ):
808+ # Note: In case hX is sparse, hX.sum is a matrix object.
809+ hX_sum = xp .asarray (hX .sum (axis = 0 ))
810+ else :
811+ hX_sum = xp .sum (hX , axis = 0 )
791812
792813 # With intercept included and l2_reg_strength = 0, hessp returns
793814 # res = (X, 1)' @ diag(h) @ (X, 1) @ s
794815 # = (X, 1)' @ (hX @ s[:n_features], sum(h) * s[-1])
795816 # res[:n_features] = X' @ hX @ s[:n_features] + sum(h) * s[-1]
796817 # res[-1] = 1' @ hX @ s[:n_features] + sum(h) * s[-1]
797818 def hessp (s ):
798- ret = np .empty_like (s )
819+ ret = xp .empty_like (s )
799820 if sparse .issparse (X ):
800821 ret [:n_features ] = X .T @ (hX @ s [:n_features ])
801822 else :
802- ret [:n_features ] = np .linalg .multi_dot ([X .T , hX , s [:n_features ]])
823+ if is_numpy_ns :
824+ ret [:n_features ] = np .linalg .multi_dot (
825+ [X .T , hX , s [:n_features ]]
826+ )
827+ else :
828+ ret [:n_features ] = X .T @ (hX @ s [:n_features ])
803829 ret [:n_features ] += l2_reg_strength * s [:n_features ]
804830
805831 if self .fit_intercept :
@@ -820,10 +846,13 @@ def hessp(s):
820846 n_threads = n_threads ,
821847 )
822848 grad_pointwise /= sw_sum
823- grad = np .empty ((n_classes , n_dof ), dtype = weights .dtype , order = "F" )
849+ if is_numpy_ns :
850+ grad = np .empty ((n_classes , n_dof ), dtype = weights .dtype , order = "F" )
851+ else :
852+ grad = xp .empty ((n_classes , n_dof ), dtype = weights .dtype , device = device )
824853 grad [:, :n_features ] = grad_pointwise .T @ X + l2_reg_strength * weights
825854 if self .fit_intercept :
826- grad [:, - 1 ] = grad_pointwise .sum (axis = 0 )
855+ grad [:, - 1 ] = xp .sum (grad_pointwise , axis = 0 )
827856
828857 # Full hessian-vector product, i.e. not only the diagonal part of the
829858 # hessian. Derivation with some index battle for input vector s:
@@ -847,29 +876,47 @@ def hessp(s):
847876 #
848877 # See also https://github.com/scikit-learn/scikit-learn/pull/3646#discussion_r17461411
849878 def hessp (s ):
850- s = s .reshape ((n_classes , - 1 ), order = "F" ) # shape = (n_classes, n_dof)
879+ if is_numpy_ns :
880+ s = s .reshape (
881+ (n_classes , - 1 ), order = "F"
882+ ) # shape = (n_classes, n_dof)
883+ else :
884+ s = xp .reshape (s , (- 1 , n_classes )).T
851885 if self .fit_intercept :
852886 s_intercept = s [:, - 1 ]
853887 s = s [:, :- 1 ] # shape = (n_classes, n_features)
854888 else :
855889 s_intercept = 0
856890 tmp = X @ s .T + s_intercept # X_{im} * s_k_m
857- tmp -= (proba * tmp ). sum ( axis = 1 )[:, np . newaxis ] # - sum_l ..
891+ tmp -= xp . sum (proba * tmp , axis = 1 )[:, None ] # - sum_l ..
858892 tmp *= proba # * p_i_k
859893 if sample_weight is not None :
860- tmp *= sample_weight [:, np . newaxis ]
894+ tmp *= sample_weight [:, None ]
861895 # hess_prod = empty_like(grad), but we ravel grad below and this
862896 # function is run after that.
863- hess_prod = np .empty ((n_classes , n_dof ), dtype = weights .dtype , order = "F" )
897+ if is_numpy_ns :
898+ hess_prod = np .empty (
899+ (n_classes , n_dof ), dtype = weights .dtype , order = "F"
900+ )
901+ else :
902+ hess_prod = xp .empty (
903+ (n_classes , n_dof ), dtype = weights .dtype , device = device
904+ )
864905 hess_prod [:, :n_features ] = (tmp .T @ X ) / sw_sum + l2_reg_strength * s
865906 if self .fit_intercept :
866- hess_prod [:, - 1 ] = tmp .sum (axis = 0 ) / sw_sum
907+ hess_prod [:, - 1 ] = xp .sum (tmp , axis = 0 ) / sw_sum
867908 if coef .ndim == 1 :
868- return hess_prod .ravel (order = "F" )
909+ if is_numpy_ns :
910+ return hess_prod .ravel (order = "F" )
911+ else :
912+ return _ravel (hess_prod .T , xp = xp )
869913 else :
870914 return hess_prod
871915
872916 if coef .ndim == 1 :
873- return grad .ravel (order = "F" ), hessp
917+ if is_numpy_ns :
918+ return grad .ravel (order = "F" ), hessp
919+ else :
920+ return _ravel (grad .T , xp = xp ), hessp
874921
875922 return grad , hessp
0 commit comments