Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/emscripten.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ jobs:
SKLEARN_SKIP_OPENMP_TEST: "true"
SKLEARN_SKIP_NETWORK_TESTS: 1
CIBW_TEST_REQUIRES: "pytest pandas"
# -s pytest argument is needed to avoid an issue in pytest output capturing with Pyodide
CIBW_TEST_COMMAND: "python -m pytest -sra --pyargs sklearn --durations 20 --showlocals"
# pytest -s argument is needed to avoid an issue in pytest output capturing with Pyodide
# -p no:cacheprovider is needed to avoid cleaning up pytest cache failing with PermissionError
CIBW_TEST_COMMAND: "python -m pytest -sra -p no:cacheprovider --pyargs sklearn --durations 20 --showlocals"

- name: Upload wheel artifact
uses: actions/upload-artifact@v7
Expand Down
4 changes: 2 additions & 2 deletions sklearn/linear_model/_cd_fast.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ cdef (floating, floating) gap_enet_sparse(
bint positive,
bint gap_smaller_eps,
) noexcept nogil:
"""Compute dual gap for use in sparse_enet_coordinate_descent.
"""Compute dual gap for use in enet_coordinate_descent_sparse.

alpha > 0: formulation A of the duality gap
alpha = 0 & beta > 0: formulation B of the duality gap
Expand Down Expand Up @@ -705,7 +705,7 @@ cdef (floating, floating) gap_enet_sparse(
return gap, dual_norm_XtA


def sparse_enet_coordinate_descent(
def enet_coordinate_descent_sparse(
floating[::1] w,
floating alpha,
floating beta,
Expand Down
111 changes: 56 additions & 55 deletions sklearn/linear_model/_coordinate_descent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
import warnings
from abc import ABC, abstractmethod
from enum import Enum
from functools import partial
from numbers import Integral, Real

Expand Down Expand Up @@ -52,6 +53,13 @@
)


class CD_Algo(Enum):
ENET_CD = 0
ENET_CD_GRAM = 1
ENET_CD_SPARSE = 2
ENET_CD_MULTITASK = 3


def _set_order(X, y, order="C"):
"""Change the order of X and y if necessary.

Expand Down Expand Up @@ -781,12 +789,54 @@ def enet_path(
X_indices = None
X_indptr = None

if multi_output:
algo = CD_Algo.ENET_CD_MULTITASK
elif X_is_sparse:
algo = CD_Algo.ENET_CD_SPARSE
elif isinstance(precompute, np.ndarray):
algo = CD_Algo.ENET_CD_GRAM
# We expect precompute to be already Fortran ordered when bypassing checks
if check_input:
precompute = check_array(precompute, dtype=X.dtype.type, order="C")
else: # precompute is False
algo = CD_Algo.ENET_CD

params = dict(
max_iter=max_iter,
tol=tol,
rng=rng,
random=random,
do_screening=do_screening,
early_stopping=early_stopping,
)

for i, alpha in enumerate(alphas):
# account for n_samples scaling in objectives between here and cd_fast
l1_reg = alpha * l1_ratio * n_samples
l2_reg = alpha * (1.0 - l1_ratio) * n_samples
if not multi_output and X_is_sparse:
model = cd_fast.sparse_enet_coordinate_descent(
if algo == CD_Algo.ENET_CD:
model = cd_fast.enet_coordinate_descent(
w=coef_,
alpha=l1_reg,
beta=l2_reg,
X=X,
y=y,
positive=positive,
**params,
)
elif algo == CD_Algo.ENET_CD_GRAM:
model = cd_fast.enet_coordinate_descent_gram(
w=coef_,
alpha=l1_reg,
beta=l2_reg,
Q=precompute, # the gram matrix
q=Xy,
y=y,
positive=positive,
**params,
)
elif algo == CD_Algo.ENET_CD_SPARSE:
model = cd_fast.enet_coordinate_descent_sparse(
w=coef_,
alpha=l1_reg,
beta=l2_reg,
Expand All @@ -796,15 +846,10 @@ def enet_path(
y=y,
sample_weight=sample_weight,
X_mean=X_sparse_scaling,
max_iter=max_iter,
tol=tol,
rng=rng,
random=random,
positive=positive,
do_screening=do_screening,
early_stopping=early_stopping,
**params,
)
elif multi_output:
elif algo == CD_Algo.ENET_CD_MULTITASK:
model = cd_fast.enet_coordinate_descent_multi_task(
W=coef_,
alpha=l1_reg,
Expand All @@ -817,53 +862,9 @@ def enet_path(
Y=y,
sample_weight=sample_weight,
X_mean=X_sparse_scaling,
max_iter=max_iter,
tol=tol,
rng=rng,
random=random,
do_screening=do_screening,
early_stopping=early_stopping,
)
elif isinstance(precompute, np.ndarray):
# We expect precompute to be already Fortran ordered when bypassing
# checks
if check_input:
precompute = check_array(precompute, dtype=X.dtype.type, order="C")
model = cd_fast.enet_coordinate_descent_gram(
coef_,
l1_reg,
l2_reg,
precompute,
Xy,
y,
max_iter,
tol,
rng,
random,
positive,
do_screening,
early_stopping,
)
elif precompute is False:
model = cd_fast.enet_coordinate_descent(
coef_,
l1_reg,
l2_reg,
X,
y,
max_iter,
tol,
rng,
random,
positive,
do_screening,
early_stopping,
)
else:
raise ValueError(
"Precompute should be one of True, False, 'auto' or array-like. Got %r"
% precompute
**params,
)

coef_, dual_gap_, eps_, n_iter_ = model
coefs[..., i] = coef_
# we correct the scale of the returned dual gap, as the objective
Expand Down
4 changes: 2 additions & 2 deletions sklearn/linear_model/tests/test_coordinate_descent.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def zc():
Xs = sparse_csc_type(X)
for do_screening in [True, False]:
coef_3 = zc()
cd_fast.sparse_enet_coordinate_descent(
cd_fast.enet_coordinate_descent_sparse(
w=coef_3,
alpha=alpha,
X_data=Xs.data,
Expand Down Expand Up @@ -196,7 +196,7 @@ def test_cython_solver_early_stopping(cd):
elif cd == "sparse_enet":
Xs = sparse.csc_matrix(X)
cd_solve = partial(
cd_fast.sparse_enet_coordinate_descent,
cd_fast.enet_coordinate_descent_sparse,
X_data=Xs.data,
X_indices=Xs.indices,
X_indptr=Xs.indptr,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ def test_same_multiple_output_sparse_dense(coo_container):


@pytest.mark.parametrize("csc_container", CSC_CONTAINERS)
def test_sparse_enet_coordinate_descent(csc_container):
def test_enet_coordinate_descent_sparse(csc_container):
"""Test that a warning is issued if model does not converge"""
clf = Lasso(
alpha=1e-10, fit_intercept=False, warm_start=True, max_iter=2, tol=1e-10
Expand Down
121 changes: 109 additions & 12 deletions sklearn/tree/_partitioner.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,64 @@ cdef class DensePartitioner:
float32_t* min_feature_value_out,
float32_t* max_feature_value_out,
) noexcept nogil
cdef void next_p(
cdef inline void next_p(
self,
intp_t* p_prev,
intp_t* p,
bint missing_go_to_left
) noexcept nogil
bint missing_go_to_left,
) noexcept nogil:
"""
Compute the next p_prev and p for iterating over feature values.

This method is used inside the best-split search function pass which starts
by setting p = start at the beginning of each search pass and calls
this method repeatedly with the same missing_go_to_left as for that pass.
The expected layout of self.feature_values[start:end] is:
- first pass (missing_go_to_left=False): after
sort_samples_and_feature_values(), non-missing values are sorted and
missing values are grouped at the right;
- second pass (missing_go_to_left=True): after
shift_missing_to_the_left(), missing values are grouped at the left.

Given that layout, this method advances p to the next valid split
position while skipping ties up to FEATURE_THRESHOLD:
- if missing_go_to_left: iterate p in [start + n_missing + 1, end)
- otherwise: iterate p in [start, end - n_missing].
The special case p == end - n_missing corresponds to "all non-missing
values on the left and all missing values on the right". The next
call then sets p to end to terminate the search loop.
"""
cdef intp_t end_non_missing = (
self.end if missing_go_to_left
else self.end - self.n_missing)

# First pass special end marker: include "all non-missing left, all missing right"
if p[0] == end_non_missing and not missing_go_to_left:
p[0] = self.end
p_prev[0] = self.end
return

# Second pass starts with missing on the left; jump to first non-missing
if missing_go_to_left and p[0] == self.start:
p[0] = self.start + self.n_missing

# Move to next candidate split position
p[0] += 1

if self.n_categories_current > 0:
while (
p[0] < end_non_missing and
self.feature_values[p[0]] == self.feature_values[p[0] - 1]
):
p[0] += 1
else:
while (
p[0] < end_non_missing and
self.feature_values[p[0]] <= self.feature_values[p[0] - 1] + FEATURE_THRESHOLD
):
p[0] += 1

p_prev[0] = p[0] - 1
cdef intp_t partition_samples(
self,
float64_t current_threshold,
Expand All @@ -157,12 +209,33 @@ cdef class DensePartitioner:
const SplitRecord* best_split,
) noexcept nogil

cdef void cat_position_to_split_bitset(
cdef inline void cat_position_to_split_bitset(
self,
intp_t position,
bint missing_go_to_left,
BITSET_DTYPE_C left_cat_bitset
) noexcept nogil
BITSET_DTYPE_C left_cat_bitset,
) noexcept nogil:
"""Convert a categorical split position into a bitset."""
cdef:
intp_t n_left_non_missing = position - self.start
intp_t offset = 0
intp_t r
intp_t c

if missing_go_to_left:
n_left_non_missing -= self.n_missing

init_bitset(left_cat_bitset)

if n_left_non_missing <= 0:
return

for r in range(self.n_categories_current):
c = self.sorted_cat[r]
set_bitset(left_cat_bitset, <uint8_t> c)
offset += self.counts[c]
if offset >= n_left_non_missing:
break
cdef void sort_categories(
self,
intp_t nc
Expand Down Expand Up @@ -217,12 +290,34 @@ cdef class SparsePartitioner:
float32_t* min_feature_value_out,
float32_t* max_feature_value_out,
) noexcept nogil
cdef void next_p(
cdef inline void next_p(
self,
intp_t* p_prev,
intp_t* p,
bint missing_go_to_left
) noexcept nogil
bint missing_go_to_left,
) noexcept nogil:
"""Compute the next p_prev and p for iterating over feature values.

The missing_go_to_left argument is ignored for sparse data because
sparse partitioning does not currently support missing values.
"""
cdef intp_t p_next

if p[0] + 1 != self.end_negative:
p_next = p[0] + 1
else:
p_next = self.start_positive

while (p_next < self.end and
self.feature_values[p_next] <= self.feature_values[p[0]] + FEATURE_THRESHOLD):
p[0] = p_next
if p[0] + 1 != self.end_negative:
p_next = p[0] + 1
else:
p_next = self.start_positive

p_prev[0] = p[0]
p[0] = p_next
cdef intp_t partition_samples(
self,
float64_t current_threshold,
Expand All @@ -233,12 +328,14 @@ cdef class SparsePartitioner:
const SplitRecord* best_split,
) noexcept nogil

cdef void cat_position_to_split_bitset(
cdef inline void cat_position_to_split_bitset(
self,
intp_t position,
bint missing_go_to_left,
BITSET_DTYPE_C left_cat_bitset
) noexcept nogil
BITSET_DTYPE_C left_cat_bitset,
) noexcept nogil:
# Sparse categorical features are rejected before split search.
init_bitset(left_cat_bitset)
cdef void extract_nnz(
self,
intp_t feature
Expand Down
Loading
Loading