feat: add Camera Work AI package
This commit is contained in:
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
@@ -0,0 +1,262 @@
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from libc.stdlib cimport free
|
||||
from libc.string cimport memset
|
||||
|
||||
import numpy as np
|
||||
from scipy.sparse import issparse
|
||||
|
||||
from ..utils._typedefs cimport float32_t, float64_t, intp_t, int32_t, uint8_t
|
||||
# Note: _tree uses cimport numpy, cnp.import_array, so we need to include
|
||||
# numpy headers in the build configuration of this extension
|
||||
from ..tree._tree cimport Node
|
||||
from ..tree._tree cimport Tree
|
||||
from ..tree._utils cimport safe_realloc
|
||||
|
||||
|
||||
# no namespace lookup for numpy dtype and array creation
|
||||
from numpy import zeros as np_zeros
|
||||
|
||||
|
||||
# constant to mark tree leafs
|
||||
cdef intp_t TREE_LEAF = -1
|
||||
|
||||
cdef void _predict_regression_tree_inplace_fast_dense(
|
||||
const float32_t[:, ::1] X,
|
||||
Node* root_node,
|
||||
double *value,
|
||||
double scale,
|
||||
Py_ssize_t k,
|
||||
float64_t[:, :] out
|
||||
) noexcept nogil:
|
||||
"""Predicts output for regression tree and stores it in ``out[i, k]``.
|
||||
|
||||
This function operates directly on the data arrays of the tree
|
||||
data structures. This is 5x faster than the variant above because
|
||||
it allows us to avoid buffer validation.
|
||||
|
||||
The function assumes that the ndarray that wraps ``X`` is
|
||||
c-continuous.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : float32_t 2d memory view
|
||||
The memory view on the data ndarray of the input ``X``.
|
||||
Assumes that the array is c-continuous.
|
||||
root_node : tree Node pointer
|
||||
Pointer to the main node array of the :class:``sklearn.tree.Tree``.
|
||||
value : np.float64_t pointer
|
||||
The pointer to the data array of the ``value`` array attribute
|
||||
of the :class:``sklearn.tree.Tree``.
|
||||
scale : double
|
||||
A constant to scale the predictions.
|
||||
k : int
|
||||
The index of the tree output to be predicted. Must satisfy
|
||||
0 <= ``k`` < ``K``.
|
||||
out : memory view on array of type np.float64_t
|
||||
The data array where the predictions are stored.
|
||||
``out`` is assumed to be a two-dimensional array of
|
||||
shape ``(n_samples, K)``.
|
||||
"""
|
||||
cdef intp_t n_samples = X.shape[0]
|
||||
cdef Py_ssize_t i
|
||||
cdef Node *node
|
||||
for i in range(n_samples):
|
||||
node = root_node
|
||||
# While node not a leaf
|
||||
while node.left_child != TREE_LEAF:
|
||||
if X[i, node.feature] <= node.threshold:
|
||||
node = root_node + node.left_child
|
||||
else:
|
||||
node = root_node + node.right_child
|
||||
out[i, k] += scale * value[node - root_node]
|
||||
|
||||
|
||||
def _predict_regression_tree_stages_sparse(
|
||||
object[:, :] estimators,
|
||||
object X,
|
||||
double scale,
|
||||
float64_t[:, :] out
|
||||
):
|
||||
"""Predicts output for regression tree inplace and adds scaled value to ``out[i, k]``.
|
||||
|
||||
The function assumes that the ndarray that wraps ``X`` is csr_matrix.
|
||||
"""
|
||||
cdef const float32_t[::1] X_data = X.data
|
||||
cdef const int32_t[::1] X_indices = X.indices
|
||||
cdef const int32_t[::1] X_indptr = X.indptr
|
||||
|
||||
cdef intp_t n_samples = X.shape[0]
|
||||
cdef intp_t n_features = X.shape[1]
|
||||
cdef intp_t n_stages = estimators.shape[0]
|
||||
cdef intp_t n_outputs = estimators.shape[1]
|
||||
|
||||
# Indices and temporary variables
|
||||
cdef intp_t sample_i
|
||||
cdef intp_t feature_i
|
||||
cdef intp_t stage_i
|
||||
cdef intp_t output_i
|
||||
cdef Node *root_node = NULL
|
||||
cdef Node *node = NULL
|
||||
cdef double *value = NULL
|
||||
|
||||
cdef Tree tree
|
||||
cdef Node** nodes = NULL
|
||||
cdef double** values = NULL
|
||||
safe_realloc(&nodes, n_stages * n_outputs)
|
||||
safe_realloc(&values, n_stages * n_outputs)
|
||||
for stage_i in range(n_stages):
|
||||
for output_i in range(n_outputs):
|
||||
tree = estimators[stage_i, output_i].tree_
|
||||
nodes[stage_i * n_outputs + output_i] = tree.nodes
|
||||
values[stage_i * n_outputs + output_i] = tree.value
|
||||
|
||||
# Initialize auxiliary data-structure
|
||||
cdef float32_t feature_value = 0.
|
||||
cdef float32_t* X_sample = NULL
|
||||
|
||||
# feature_to_sample as a data structure records the last seen sample
|
||||
# for each feature; functionally, it is an efficient way to identify
|
||||
# which features are nonzero in the present sample.
|
||||
cdef intp_t* feature_to_sample = NULL
|
||||
|
||||
safe_realloc(&X_sample, n_features)
|
||||
safe_realloc(&feature_to_sample, n_features)
|
||||
|
||||
memset(feature_to_sample, -1, n_features * sizeof(intp_t))
|
||||
|
||||
# Cycle through all samples
|
||||
for sample_i in range(n_samples):
|
||||
for feature_i in range(X_indptr[sample_i], X_indptr[sample_i + 1]):
|
||||
feature_to_sample[X_indices[feature_i]] = sample_i
|
||||
X_sample[X_indices[feature_i]] = X_data[feature_i]
|
||||
|
||||
# Cycle through all stages
|
||||
for stage_i in range(n_stages):
|
||||
# Cycle through all trees
|
||||
for output_i in range(n_outputs):
|
||||
root_node = nodes[stage_i * n_outputs + output_i]
|
||||
value = values[stage_i * n_outputs + output_i]
|
||||
node = root_node
|
||||
|
||||
# While node not a leaf
|
||||
while node.left_child != TREE_LEAF:
|
||||
# ... and node.right_child != TREE_LEAF:
|
||||
if feature_to_sample[node.feature] == sample_i:
|
||||
feature_value = X_sample[node.feature]
|
||||
else:
|
||||
feature_value = 0.
|
||||
|
||||
if feature_value <= node.threshold:
|
||||
node = root_node + node.left_child
|
||||
else:
|
||||
node = root_node + node.right_child
|
||||
out[sample_i, output_i] += scale * value[node - root_node]
|
||||
|
||||
# Free auxiliary arrays
|
||||
free(X_sample)
|
||||
free(feature_to_sample)
|
||||
free(nodes)
|
||||
free(values)
|
||||
|
||||
|
||||
def predict_stages(
|
||||
object[:, :] estimators,
|
||||
object X,
|
||||
double scale,
|
||||
float64_t[:, :] out
|
||||
):
|
||||
"""Add predictions of ``estimators`` to ``out``.
|
||||
|
||||
Each estimator is scaled by ``scale`` before its prediction
|
||||
is added to ``out``.
|
||||
"""
|
||||
cdef Py_ssize_t i
|
||||
cdef Py_ssize_t k
|
||||
cdef Py_ssize_t n_estimators = estimators.shape[0]
|
||||
cdef Py_ssize_t K = estimators.shape[1]
|
||||
cdef Tree tree
|
||||
|
||||
if issparse(X):
|
||||
if X.format != 'csr':
|
||||
raise ValueError("When X is a sparse matrix, a CSR format is"
|
||||
" expected, got {!r}".format(type(X)))
|
||||
_predict_regression_tree_stages_sparse(
|
||||
estimators=estimators, X=X, scale=scale, out=out
|
||||
)
|
||||
else:
|
||||
if not isinstance(X, np.ndarray) or np.isfortran(X):
|
||||
raise ValueError(f"X should be C-ordered np.ndarray, got {type(X)}")
|
||||
|
||||
for i in range(n_estimators):
|
||||
for k in range(K):
|
||||
tree = estimators[i, k].tree_
|
||||
|
||||
# avoid buffer validation by casting to ndarray
|
||||
# and get data pointer
|
||||
# need brackets because of casting operator priority
|
||||
_predict_regression_tree_inplace_fast_dense(
|
||||
X=X,
|
||||
root_node=tree.nodes,
|
||||
value=tree.value,
|
||||
scale=scale,
|
||||
k=k,
|
||||
out=out
|
||||
)
|
||||
# out[:, k] += scale * tree.predict(X).ravel()
|
||||
|
||||
|
||||
def predict_stage(
|
||||
object[:, :] estimators,
|
||||
int stage,
|
||||
object X,
|
||||
double scale,
|
||||
float64_t[:, :] out
|
||||
):
|
||||
"""Add predictions of ``estimators[stage]`` to ``out``.
|
||||
|
||||
Each estimator in the stage is scaled by ``scale`` before
|
||||
its prediction is added to ``out``.
|
||||
"""
|
||||
return predict_stages(
|
||||
estimators=estimators[stage:stage + 1], X=X, scale=scale, out=out
|
||||
)
|
||||
|
||||
|
||||
def _random_sample_mask(
|
||||
intp_t n_total_samples,
|
||||
intp_t n_total_in_bag,
|
||||
random_state
|
||||
):
|
||||
"""Create a random sample mask where ``n_total_in_bag`` elements are set.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n_total_samples : int
|
||||
The length of the resulting mask.
|
||||
|
||||
n_total_in_bag : int
|
||||
The number of elements in the sample mask which are set to 1.
|
||||
|
||||
random_state : RandomState
|
||||
A numpy ``RandomState`` object.
|
||||
|
||||
Returns
|
||||
-------
|
||||
sample_mask : np.ndarray, shape=[n_total_samples]
|
||||
An ndarray where ``n_total_in_bag`` elements are set to ``True``
|
||||
the others are ``False``.
|
||||
"""
|
||||
cdef float64_t[::1] rand = random_state.uniform(size=n_total_samples)
|
||||
cdef uint8_t[::1] sample_mask = np_zeros((n_total_samples,), dtype=bool)
|
||||
|
||||
cdef intp_t n_bagged = 0
|
||||
cdef intp_t i = 0
|
||||
|
||||
for i in range(n_total_samples):
|
||||
if rand[i] * (n_total_samples - i) < (n_total_in_bag - n_bagged):
|
||||
sample_mask[i] = 1
|
||||
n_bagged += 1
|
||||
|
||||
return sample_mask.base
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+85
@@ -0,0 +1,85 @@
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from cython.parallel import prange
|
||||
from libc.math cimport isnan
|
||||
|
||||
from .common cimport X_DTYPE_C, X_BINNED_DTYPE_C
|
||||
from ...utils._typedefs cimport uint8_t
|
||||
|
||||
|
||||
def _map_to_bins(const X_DTYPE_C [:, :] data,
|
||||
list binning_thresholds,
|
||||
const uint8_t[::1] is_categorical,
|
||||
const uint8_t missing_values_bin_idx,
|
||||
int n_threads,
|
||||
X_BINNED_DTYPE_C [::1, :] binned):
|
||||
"""Bin continuous and categorical values to discrete integer-coded levels.
|
||||
|
||||
A given value x is mapped into bin value i iff
|
||||
thresholds[i - 1] < x <= thresholds[i]
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : ndarray, shape (n_samples, n_features)
|
||||
The data to bin.
|
||||
binning_thresholds : list of arrays
|
||||
For each feature, stores the increasing numeric values that are
|
||||
used to separate the bins.
|
||||
is_categorical : ndarray of uint8_t of shape (n_features,)
|
||||
Indicates categorical features.
|
||||
n_threads : int
|
||||
Number of OpenMP threads to use.
|
||||
binned : ndarray, shape (n_samples, n_features)
|
||||
Output array, must be fortran aligned.
|
||||
"""
|
||||
cdef:
|
||||
int feature_idx
|
||||
|
||||
for feature_idx in range(data.shape[1]):
|
||||
_map_col_to_bins(
|
||||
data[:, feature_idx],
|
||||
binning_thresholds[feature_idx],
|
||||
is_categorical[feature_idx],
|
||||
missing_values_bin_idx,
|
||||
n_threads,
|
||||
binned[:, feature_idx]
|
||||
)
|
||||
|
||||
|
||||
cdef void _map_col_to_bins(
|
||||
const X_DTYPE_C [:] data,
|
||||
const X_DTYPE_C [:] binning_thresholds,
|
||||
const uint8_t is_categorical,
|
||||
const uint8_t missing_values_bin_idx,
|
||||
int n_threads,
|
||||
X_BINNED_DTYPE_C [:] binned
|
||||
):
|
||||
"""Binary search to find the bin index for each value in the data."""
|
||||
cdef:
|
||||
int i
|
||||
int left
|
||||
int right
|
||||
int middle
|
||||
|
||||
for i in prange(data.shape[0], schedule='static', nogil=True,
|
||||
num_threads=n_threads):
|
||||
if (
|
||||
isnan(data[i]) or
|
||||
# To follow LightGBM's conventions, negative values for
|
||||
# categorical features are considered as missing values.
|
||||
(is_categorical and data[i] < 0)
|
||||
):
|
||||
binned[i] = missing_values_bin_idx
|
||||
else:
|
||||
# for known values, use binary search
|
||||
left, right = 0, binning_thresholds.shape[0]
|
||||
while left < right:
|
||||
# equal to (right + left - 1) // 2 but avoids overflow
|
||||
middle = left + (right - left - 1) // 2
|
||||
if data[i] <= binning_thresholds[middle]:
|
||||
right = middle
|
||||
else:
|
||||
left = middle + 1
|
||||
|
||||
binned[i] = left
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+20
@@ -0,0 +1,20 @@
|
||||
from .common cimport X_BINNED_DTYPE_C
|
||||
from .common cimport BITSET_DTYPE_C
|
||||
from .common cimport BITSET_INNER_DTYPE_C
|
||||
from .common cimport X_DTYPE_C
|
||||
from ...utils._typedefs cimport uint8_t
|
||||
|
||||
|
||||
cdef void init_bitset(BITSET_DTYPE_C bitset) noexcept nogil
|
||||
|
||||
cdef void set_bitset(BITSET_DTYPE_C bitset, X_BINNED_DTYPE_C val) noexcept nogil
|
||||
|
||||
cdef uint8_t in_bitset(BITSET_DTYPE_C bitset, X_BINNED_DTYPE_C val) noexcept nogil
|
||||
|
||||
cpdef uint8_t in_bitset_memoryview(const BITSET_INNER_DTYPE_C[:] bitset,
|
||||
X_BINNED_DTYPE_C val) noexcept nogil
|
||||
|
||||
cdef uint8_t in_bitset_2d_memoryview(
|
||||
const BITSET_INNER_DTYPE_C[:, :] bitset,
|
||||
X_BINNED_DTYPE_C val,
|
||||
unsigned int row) noexcept nogil
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
from .common cimport BITSET_INNER_DTYPE_C
|
||||
from .common cimport BITSET_DTYPE_C
|
||||
from .common cimport X_DTYPE_C
|
||||
from .common cimport X_BINNED_DTYPE_C
|
||||
from ...utils._typedefs cimport uint8_t
|
||||
|
||||
|
||||
# A bitset is a data structure used to represent sets of integers in [0, n]. We
|
||||
# use them to represent sets of features indices (e.g. features that go to the
|
||||
# left child, or features that are categorical). For familiarity with bitsets
|
||||
# and bitwise operations:
|
||||
# https://en.wikipedia.org/wiki/Bit_array
|
||||
# https://en.wikipedia.org/wiki/Bitwise_operation
|
||||
|
||||
|
||||
cdef inline void init_bitset(BITSET_DTYPE_C bitset) noexcept nogil: # OUT
|
||||
cdef:
|
||||
unsigned int i
|
||||
|
||||
for i in range(8):
|
||||
bitset[i] = 0
|
||||
|
||||
|
||||
cdef inline void set_bitset(BITSET_DTYPE_C bitset, # OUT
|
||||
X_BINNED_DTYPE_C val) noexcept nogil:
|
||||
bitset[val // 32] |= (1 << (val % 32))
|
||||
|
||||
|
||||
cdef inline uint8_t in_bitset(BITSET_DTYPE_C bitset,
|
||||
X_BINNED_DTYPE_C val) noexcept nogil:
|
||||
return (bitset[val // 32] >> (val % 32)) & 1
|
||||
|
||||
|
||||
cpdef inline uint8_t in_bitset_memoryview(const BITSET_INNER_DTYPE_C[:] bitset,
|
||||
X_BINNED_DTYPE_C val) noexcept nogil:
|
||||
return (bitset[val // 32] >> (val % 32)) & 1
|
||||
|
||||
|
||||
cdef inline uint8_t in_bitset_2d_memoryview(const BITSET_INNER_DTYPE_C[:, :] bitset,
|
||||
X_BINNED_DTYPE_C val,
|
||||
unsigned int row) noexcept nogil:
|
||||
# Same as above but works on 2d memory views to avoid the creation of 1d
|
||||
# memory views. See https://github.com/scikit-learn/scikit-learn/issues/17299
|
||||
return (bitset[row, val // 32] >> (val % 32)) & 1
|
||||
|
||||
|
||||
cpdef inline void set_bitset_memoryview(BITSET_INNER_DTYPE_C[:] bitset, # OUT
|
||||
X_BINNED_DTYPE_C val):
|
||||
bitset[val // 32] |= (1 << (val % 32))
|
||||
|
||||
|
||||
def set_raw_bitset_from_binned_bitset(BITSET_INNER_DTYPE_C[:] raw_bitset, # OUT
|
||||
BITSET_INNER_DTYPE_C[:] binned_bitset,
|
||||
X_DTYPE_C[:] categories):
|
||||
"""Set the raw_bitset from the values of the binned bitset
|
||||
|
||||
categories is a mapping from binned category value to raw category value.
|
||||
"""
|
||||
cdef:
|
||||
int binned_cat_value
|
||||
X_DTYPE_C raw_cat_value
|
||||
|
||||
for binned_cat_value, raw_cat_value in enumerate(categories):
|
||||
if in_bitset_memoryview(binned_bitset, binned_cat_value):
|
||||
set_bitset_memoryview(raw_bitset, <X_BINNED_DTYPE_C>raw_cat_value)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+59
@@ -0,0 +1,59 @@
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from cython.parallel import prange
|
||||
import numpy as np
|
||||
|
||||
from .common import Y_DTYPE
|
||||
from .common cimport Y_DTYPE_C
|
||||
|
||||
|
||||
def _update_raw_predictions(
|
||||
Y_DTYPE_C [::1] raw_predictions, # OUT
|
||||
grower,
|
||||
n_threads,
|
||||
):
|
||||
"""Update raw_predictions with the predictions of the newest tree.
|
||||
|
||||
This is equivalent to (and much faster than):
|
||||
raw_predictions += last_estimator.predict(X_train)
|
||||
|
||||
It's only possible for data X_train that is used to train the trees (it
|
||||
isn't usable for e.g. X_val).
|
||||
"""
|
||||
cdef:
|
||||
unsigned int [::1] starts # start of each leaf in partition
|
||||
unsigned int [::1] stops # end of each leaf in partition
|
||||
Y_DTYPE_C [::1] values # value of each leaf
|
||||
const unsigned int [::1] partition = grower.splitter.partition
|
||||
list leaves
|
||||
|
||||
leaves = grower.finalized_leaves
|
||||
starts = np.array([leaf.partition_start for leaf in leaves],
|
||||
dtype=np.uint32)
|
||||
stops = np.array([leaf.partition_stop for leaf in leaves],
|
||||
dtype=np.uint32)
|
||||
values = np.array([leaf.value for leaf in leaves], dtype=Y_DTYPE)
|
||||
|
||||
_update_raw_predictions_helper(raw_predictions, starts, stops, partition,
|
||||
values, n_threads)
|
||||
|
||||
|
||||
cdef inline void _update_raw_predictions_helper(
|
||||
Y_DTYPE_C [::1] raw_predictions, # OUT
|
||||
const unsigned int [::1] starts,
|
||||
const unsigned int [::1] stops,
|
||||
const unsigned int [::1] partition,
|
||||
const Y_DTYPE_C [::1] values,
|
||||
int n_threads,
|
||||
):
|
||||
|
||||
cdef:
|
||||
unsigned int position
|
||||
int leaf_idx
|
||||
int n_leaves = starts.shape[0]
|
||||
|
||||
for leaf_idx in prange(n_leaves, schedule='static', nogil=True,
|
||||
num_threads=n_threads):
|
||||
for position in range(starts[leaf_idx], stops[leaf_idx]):
|
||||
raw_predictions[partition[position]] += values[leaf_idx]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+256
@@ -0,0 +1,256 @@
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from cython.parallel import prange
|
||||
from libc.math cimport isnan
|
||||
import numpy as np
|
||||
|
||||
from ...utils._typedefs cimport intp_t, uint8_t
|
||||
from .common cimport X_DTYPE_C
|
||||
from .common cimport Y_DTYPE_C
|
||||
from .common import Y_DTYPE
|
||||
from .common cimport X_BINNED_DTYPE_C
|
||||
from .common cimport BITSET_INNER_DTYPE_C
|
||||
from .common cimport node_struct
|
||||
from ._bitset cimport in_bitset_2d_memoryview
|
||||
|
||||
|
||||
def _predict_from_raw_data( # raw data = non-binned data
|
||||
const node_struct [:] nodes,
|
||||
const X_DTYPE_C [:, :] numeric_data,
|
||||
const BITSET_INNER_DTYPE_C [:, ::1] raw_left_cat_bitsets,
|
||||
const BITSET_INNER_DTYPE_C [:, ::1] known_cat_bitsets,
|
||||
const unsigned int [::1] f_idx_map,
|
||||
int n_threads,
|
||||
Y_DTYPE_C [:] out):
|
||||
|
||||
cdef:
|
||||
int i
|
||||
|
||||
for i in prange(numeric_data.shape[0], schedule='static', nogil=True,
|
||||
num_threads=n_threads):
|
||||
out[i] = _predict_one_from_raw_data(
|
||||
nodes, numeric_data, raw_left_cat_bitsets,
|
||||
known_cat_bitsets,
|
||||
f_idx_map, i)
|
||||
|
||||
|
||||
cdef inline Y_DTYPE_C _predict_one_from_raw_data(
|
||||
const node_struct [:] nodes,
|
||||
const X_DTYPE_C [:, :] numeric_data,
|
||||
const BITSET_INNER_DTYPE_C [:, ::1] raw_left_cat_bitsets,
|
||||
const BITSET_INNER_DTYPE_C [:, ::1] known_cat_bitsets,
|
||||
const unsigned int [::1] f_idx_map,
|
||||
const int row) noexcept nogil:
|
||||
# Need to pass the whole array and the row index, else prange won't work.
|
||||
# See issue Cython #2798
|
||||
|
||||
cdef:
|
||||
node_struct node = nodes[0]
|
||||
unsigned int node_idx = 0
|
||||
X_DTYPE_C data_val
|
||||
|
||||
while True:
|
||||
if node.is_leaf:
|
||||
return node.value
|
||||
|
||||
data_val = numeric_data[row, node.feature_idx]
|
||||
|
||||
if isnan(data_val):
|
||||
if node.missing_go_to_left:
|
||||
node_idx = node.left
|
||||
else:
|
||||
node_idx = node.right
|
||||
elif node.is_categorical:
|
||||
if data_val < 0:
|
||||
# data_val is not in the accepted range, so it is treated as missing value
|
||||
node_idx = node.left if node.missing_go_to_left else node.right
|
||||
elif in_bitset_2d_memoryview(
|
||||
raw_left_cat_bitsets,
|
||||
<X_BINNED_DTYPE_C>data_val,
|
||||
node.bitset_idx):
|
||||
node_idx = node.left
|
||||
elif in_bitset_2d_memoryview(
|
||||
known_cat_bitsets,
|
||||
<X_BINNED_DTYPE_C>data_val,
|
||||
f_idx_map[node.feature_idx]):
|
||||
node_idx = node.right
|
||||
else:
|
||||
# Treat unknown categories as missing.
|
||||
node_idx = node.left if node.missing_go_to_left else node.right
|
||||
else:
|
||||
if data_val <= node.num_threshold:
|
||||
node_idx = node.left
|
||||
else:
|
||||
node_idx = node.right
|
||||
node = nodes[node_idx]
|
||||
|
||||
|
||||
def _predict_from_binned_data(
|
||||
node_struct [:] nodes,
|
||||
const X_BINNED_DTYPE_C [:, :] binned_data,
|
||||
BITSET_INNER_DTYPE_C [:, :] binned_left_cat_bitsets,
|
||||
const uint8_t missing_values_bin_idx,
|
||||
int n_threads,
|
||||
Y_DTYPE_C [:] out):
|
||||
|
||||
cdef:
|
||||
int i
|
||||
|
||||
for i in prange(binned_data.shape[0], schedule='static', nogil=True,
|
||||
num_threads=n_threads):
|
||||
out[i] = _predict_one_from_binned_data(nodes,
|
||||
binned_data,
|
||||
binned_left_cat_bitsets, i,
|
||||
missing_values_bin_idx)
|
||||
|
||||
|
||||
cdef inline Y_DTYPE_C _predict_one_from_binned_data(
|
||||
node_struct [:] nodes,
|
||||
const X_BINNED_DTYPE_C [:, :] binned_data,
|
||||
const BITSET_INNER_DTYPE_C [:, :] binned_left_cat_bitsets,
|
||||
const int row,
|
||||
const uint8_t missing_values_bin_idx) noexcept nogil:
|
||||
# Need to pass the whole array and the row index, else prange won't work.
|
||||
# See issue Cython #2798
|
||||
|
||||
cdef:
|
||||
node_struct node = nodes[0]
|
||||
unsigned int node_idx = 0
|
||||
X_BINNED_DTYPE_C data_val
|
||||
|
||||
while True:
|
||||
if node.is_leaf:
|
||||
return node.value
|
||||
|
||||
data_val = binned_data[row, node.feature_idx]
|
||||
|
||||
if data_val == missing_values_bin_idx:
|
||||
if node.missing_go_to_left:
|
||||
node_idx = node.left
|
||||
else:
|
||||
node_idx = node.right
|
||||
elif node.is_categorical:
|
||||
if in_bitset_2d_memoryview(
|
||||
binned_left_cat_bitsets,
|
||||
data_val,
|
||||
node.bitset_idx):
|
||||
node_idx = node.left
|
||||
else:
|
||||
node_idx = node.right
|
||||
else:
|
||||
if data_val <= node.bin_threshold:
|
||||
node_idx = node.left
|
||||
else:
|
||||
node_idx = node.right
|
||||
node = nodes[node_idx]
|
||||
|
||||
|
||||
def _compute_partial_dependence(
|
||||
node_struct [:] nodes,
|
||||
const X_DTYPE_C [:, ::1] X,
|
||||
const intp_t [:] target_features,
|
||||
Y_DTYPE_C [:] out
|
||||
):
|
||||
"""Partial dependence of the response on the ``target_features`` set.
|
||||
|
||||
For each sample in ``X`` a tree traversal is performed.
|
||||
Each traversal starts from the root with weight 1.0.
|
||||
|
||||
At each non-leaf node that splits on a target feature, either
|
||||
the left child or the right child is visited based on the feature
|
||||
value of the current sample, and the weight is not modified.
|
||||
At each non-leaf node that splits on a complementary feature,
|
||||
both children are visited and the weight is multiplied by the fraction
|
||||
of training samples which went to each child.
|
||||
|
||||
At each leaf, the value of the node is multiplied by the current
|
||||
weight (weights sum to 1 for all visited terminal nodes).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nodes : view on array of PREDICTOR_RECORD_DTYPE, shape (n_nodes)
|
||||
The array representing the predictor tree.
|
||||
X : view on 2d ndarray, shape (n_samples, n_target_features)
|
||||
The grid points on which the partial dependence should be
|
||||
evaluated.
|
||||
target_features : view on 1d ndarray of intp_t, shape (n_target_features)
|
||||
The set of target features for which the partial dependence
|
||||
should be evaluated.
|
||||
out : view on 1d ndarray, shape (n_samples)
|
||||
The value of the partial dependence function on each grid
|
||||
point.
|
||||
"""
|
||||
|
||||
cdef:
|
||||
unsigned int current_node_idx
|
||||
unsigned int [:] node_idx_stack = np.zeros(shape=nodes.shape[0],
|
||||
dtype=np.uint32)
|
||||
Y_DTYPE_C [::1] weight_stack = np.zeros(shape=nodes.shape[0],
|
||||
dtype=Y_DTYPE)
|
||||
node_struct * current_node # pointer to avoid copying attributes
|
||||
|
||||
unsigned int sample_idx
|
||||
intp_t feature_idx
|
||||
unsigned stack_size
|
||||
Y_DTYPE_C left_sample_frac
|
||||
Y_DTYPE_C current_weight
|
||||
Y_DTYPE_C total_weight # used for sanity check only
|
||||
bint is_target_feature
|
||||
|
||||
for sample_idx in range(X.shape[0]):
|
||||
# init stacks for current sample
|
||||
stack_size = 1
|
||||
node_idx_stack[0] = 0 # root node
|
||||
weight_stack[0] = 1 # all the samples are in the root node
|
||||
total_weight = 0
|
||||
|
||||
while stack_size > 0:
|
||||
|
||||
# pop the stack
|
||||
stack_size -= 1
|
||||
current_node_idx = node_idx_stack[stack_size]
|
||||
current_node = &nodes[current_node_idx]
|
||||
|
||||
if current_node.is_leaf:
|
||||
out[sample_idx] += (weight_stack[stack_size] *
|
||||
current_node.value)
|
||||
total_weight += weight_stack[stack_size]
|
||||
else:
|
||||
# determine if the split feature is a target feature
|
||||
is_target_feature = False
|
||||
for feature_idx in range(target_features.shape[0]):
|
||||
if target_features[feature_idx] == current_node.feature_idx:
|
||||
is_target_feature = True
|
||||
break
|
||||
|
||||
if is_target_feature:
|
||||
# In this case, we push left or right child on stack
|
||||
if X[sample_idx, feature_idx] <= current_node.num_threshold:
|
||||
node_idx_stack[stack_size] = current_node.left
|
||||
else:
|
||||
node_idx_stack[stack_size] = current_node.right
|
||||
stack_size += 1
|
||||
else:
|
||||
# In this case, we push both children onto the stack,
|
||||
# and give a weight proportional to the number of
|
||||
# samples going through each branch.
|
||||
|
||||
# push left child
|
||||
node_idx_stack[stack_size] = current_node.left
|
||||
left_sample_frac = (
|
||||
<Y_DTYPE_C> nodes[current_node.left].count /
|
||||
current_node.count)
|
||||
current_weight = weight_stack[stack_size]
|
||||
weight_stack[stack_size] = current_weight * left_sample_frac
|
||||
stack_size += 1
|
||||
|
||||
# push right child
|
||||
node_idx_stack[stack_size] = current_node.right
|
||||
weight_stack[stack_size] = (
|
||||
current_weight * (1 - left_sample_frac))
|
||||
stack_size += 1
|
||||
|
||||
# Sanity check. Should never happen.
|
||||
if not (0.999 < total_weight < 1.001):
|
||||
raise ValueError("Total weight should be 1.0 but was %.9f" %total_weight)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+43
@@ -0,0 +1,43 @@
|
||||
from ...utils._typedefs cimport float32_t, float64_t, intp_t, uint8_t, uint32_t
|
||||
|
||||
|
||||
ctypedef float64_t X_DTYPE_C
|
||||
ctypedef uint8_t X_BINNED_DTYPE_C
|
||||
ctypedef float64_t Y_DTYPE_C
|
||||
ctypedef float32_t G_H_DTYPE_C
|
||||
ctypedef uint32_t BITSET_INNER_DTYPE_C
|
||||
ctypedef BITSET_INNER_DTYPE_C[8] BITSET_DTYPE_C
|
||||
|
||||
|
||||
cdef packed struct hist_struct:
|
||||
# Same as histogram dtype but we need a struct to declare views. It needs
|
||||
# to be packed since by default numpy dtypes aren't aligned
|
||||
Y_DTYPE_C sum_gradients
|
||||
Y_DTYPE_C sum_hessians
|
||||
unsigned int count
|
||||
|
||||
|
||||
cdef packed struct node_struct:
|
||||
# Equivalent struct to PREDICTOR_RECORD_DTYPE to use in memory views. It
|
||||
# needs to be packed since by default numpy dtypes aren't aligned
|
||||
Y_DTYPE_C value
|
||||
unsigned int count
|
||||
intp_t feature_idx
|
||||
X_DTYPE_C num_threshold
|
||||
uint8_t missing_go_to_left
|
||||
unsigned int left
|
||||
unsigned int right
|
||||
Y_DTYPE_C gain
|
||||
unsigned int depth
|
||||
uint8_t is_leaf
|
||||
X_BINNED_DTYPE_C bin_threshold
|
||||
uint8_t is_categorical
|
||||
# The index of the corresponding bitsets in the Predictor's bitset arrays.
|
||||
# Only used if is_categorical is True
|
||||
unsigned int bitset_idx
|
||||
|
||||
|
||||
cpdef enum MonotonicConstraint:
|
||||
NO_CST = 0
|
||||
POS = 1
|
||||
NEG = -1
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import numpy as np
|
||||
|
||||
# Y_DYTPE is the dtype to which the targets y are converted to. This is also
|
||||
# dtype for leaf values, gains, and sums of gradients / hessians. The gradients
|
||||
# and hessians arrays are stored as floats to avoid using too much memory.
|
||||
Y_DTYPE = np.float64
|
||||
X_DTYPE = np.float64
|
||||
X_BINNED_DTYPE = np.uint8 # hence max_bins == 256
|
||||
# dtype for gradients and hessians arrays
|
||||
G_H_DTYPE = np.float32
|
||||
X_BITSET_INNER_DTYPE = np.uint32
|
||||
|
||||
# Note that we use Y_DTYPE=float64 to avoid issues with floating point precision when
|
||||
# summing gradients and hessians (both float32). Those are difficult to protect via
|
||||
# tools like (Kahan-) Neumaier summation as in CPython, see
|
||||
# https://github.com/python/cpython/issues/100425, or pairwise summation as numpy, see
|
||||
# https://github.com/numpy/numpy/pull/3685, due to the way histograms are summed
|
||||
# (number of additions per bin is not known in advance). See also comment in
|
||||
# _subtract_histograms.
|
||||
HISTOGRAM_DTYPE = np.dtype([
|
||||
('sum_gradients', Y_DTYPE), # sum of sample gradients in bin
|
||||
('sum_hessians', Y_DTYPE), # sum of sample hessians in bin
|
||||
('count', np.uint32), # number of samples in bin
|
||||
])
|
||||
|
||||
PREDICTOR_RECORD_DTYPE = np.dtype([
|
||||
('value', Y_DTYPE),
|
||||
('count', np.uint32),
|
||||
('feature_idx', np.intp),
|
||||
('num_threshold', X_DTYPE),
|
||||
('missing_go_to_left', np.uint8),
|
||||
('left', np.uint32),
|
||||
('right', np.uint32),
|
||||
('gain', Y_DTYPE),
|
||||
('depth', np.uint32),
|
||||
('is_leaf', np.uint8),
|
||||
('bin_threshold', X_BINNED_DTYPE),
|
||||
('is_categorical', np.uint8),
|
||||
# The index of the corresponding bitsets in the Predictor's bitset arrays.
|
||||
# Only used if is_categorical is True
|
||||
('bitset_idx', np.uint32)
|
||||
])
|
||||
|
||||
ALMOST_INF = 1e300 # see LightGBM AvoidInf()
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+520
@@ -0,0 +1,520 @@
|
||||
"""This module contains routines for building histograms."""
|
||||
|
||||
# Authors: The scikit-learn developers
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
cimport cython
|
||||
from cython.parallel import prange
|
||||
from libc.string cimport memset
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .common import HISTOGRAM_DTYPE
|
||||
from .common cimport hist_struct
|
||||
from .common cimport X_BINNED_DTYPE_C
|
||||
from .common cimport G_H_DTYPE_C
|
||||
from ...utils._typedefs cimport uint8_t
|
||||
|
||||
|
||||
# Notes:
|
||||
# - IN views are read-only, OUT views are write-only
|
||||
# - In a lot of functions here, we pass feature_idx and the whole 2d
|
||||
# histograms arrays instead of just histograms[feature_idx]. This is because
|
||||
# Cython generated C code will have strange Python interactions (likely
|
||||
# related to the GIL release and the custom histogram dtype) when using 1d
|
||||
# histogram arrays that come from 2d arrays.
|
||||
# - The for loops are un-wrapped, for example:
|
||||
#
|
||||
# for i in range(n):
|
||||
# array[i] = i
|
||||
#
|
||||
# will become
|
||||
#
|
||||
# for i in range(n // 4):
|
||||
# array[i] = i
|
||||
# array[i + 1] = i + 1
|
||||
# array[i + 2] = i + 2
|
||||
# array[i + 3] = i + 3
|
||||
#
|
||||
# This is to hint gcc that it can auto-vectorize these 4 operations and
|
||||
# perform them all at once.
|
||||
|
||||
|
||||
@cython.final
|
||||
cdef class HistogramBuilder:
|
||||
"""A Histogram builder... used to build histograms.
|
||||
|
||||
A histogram is an array with n_bins entries of type HISTOGRAM_DTYPE. Each
|
||||
feature has its own histogram. A histogram contains the sum of gradients
|
||||
and hessians of all the samples belonging to each bin.
|
||||
|
||||
There are different ways to build a histogram:
|
||||
- by subtraction: hist(child) = hist(parent) - hist(sibling)
|
||||
- from scratch. In this case we have routines that update the hessians
|
||||
or not (not useful when hessians are constant for some losses e.g.
|
||||
least squares). Also, there's a special case for the root which
|
||||
contains all the samples, leading to some possible optimizations.
|
||||
Overall all the implementations look the same, and are optimized for
|
||||
cache hit.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X_binned : ndarray of int, shape (n_samples, n_features)
|
||||
The binned input samples. Must be Fortran-aligned.
|
||||
n_bins : int
|
||||
The total number of bins, including the bin for missing values. Used
|
||||
to define the shape of the histograms.
|
||||
gradients : ndarray, shape (n_samples,)
|
||||
The gradients of each training sample. Those are the gradients of the
|
||||
loss w.r.t the predictions, evaluated at iteration i - 1.
|
||||
hessians : ndarray, shape (n_samples,)
|
||||
The hessians of each training sample. Those are the hessians of the
|
||||
loss w.r.t the predictions, evaluated at iteration i - 1.
|
||||
hessians_are_constant : bool
|
||||
Whether hessians are constant.
|
||||
"""
|
||||
cdef public:
|
||||
const X_BINNED_DTYPE_C [::1, :] X_binned
|
||||
unsigned int n_features
|
||||
unsigned int n_bins
|
||||
G_H_DTYPE_C [::1] gradients
|
||||
G_H_DTYPE_C [::1] hessians
|
||||
G_H_DTYPE_C [::1] ordered_gradients
|
||||
G_H_DTYPE_C [::1] ordered_hessians
|
||||
uint8_t hessians_are_constant
|
||||
int n_threads
|
||||
|
||||
def __init__(self, const X_BINNED_DTYPE_C [::1, :] X_binned,
|
||||
unsigned int n_bins, G_H_DTYPE_C [::1] gradients,
|
||||
G_H_DTYPE_C [::1] hessians,
|
||||
uint8_t hessians_are_constant,
|
||||
int n_threads):
|
||||
|
||||
self.X_binned = X_binned
|
||||
self.n_features = X_binned.shape[1]
|
||||
# Note: all histograms will have <n_bins> bins, but some of the
|
||||
# bins may be unused if a feature has a small number of unique values.
|
||||
self.n_bins = n_bins
|
||||
self.gradients = gradients
|
||||
self.hessians = hessians
|
||||
# for root node, gradients and hessians are already ordered
|
||||
self.ordered_gradients = gradients.copy()
|
||||
self.ordered_hessians = hessians.copy()
|
||||
self.hessians_are_constant = hessians_are_constant
|
||||
self.n_threads = n_threads
|
||||
|
||||
def compute_histograms_brute(
|
||||
HistogramBuilder self,
|
||||
const unsigned int [::1] sample_indices, # IN
|
||||
const unsigned int [:] allowed_features=None, # IN
|
||||
):
|
||||
"""Compute the histograms of the node by scanning through all the data.
|
||||
|
||||
For a given feature, the complexity is O(n_samples)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sample_indices : array of int, shape (n_samples_at_node,)
|
||||
The indices of the samples at the node to split.
|
||||
|
||||
allowed_features : None or ndarray, dtype=np.uint32
|
||||
Indices of the features that are allowed by interaction constraints to be
|
||||
split.
|
||||
|
||||
Returns
|
||||
-------
|
||||
histograms : ndarray of HISTOGRAM_DTYPE, shape (n_features, n_bins)
|
||||
The computed histograms of the current node.
|
||||
"""
|
||||
cdef:
|
||||
int n_samples
|
||||
int feature_idx
|
||||
int f_idx
|
||||
int i
|
||||
# need local views to avoid python interactions
|
||||
uint8_t hessians_are_constant = self.hessians_are_constant
|
||||
int n_allowed_features = self.n_features
|
||||
G_H_DTYPE_C [::1] ordered_gradients = self.ordered_gradients
|
||||
G_H_DTYPE_C [::1] gradients = self.gradients
|
||||
G_H_DTYPE_C [::1] ordered_hessians = self.ordered_hessians
|
||||
G_H_DTYPE_C [::1] hessians = self.hessians
|
||||
# Histograms will be initialized to zero later within a prange
|
||||
hist_struct [:, ::1] histograms = np.empty(
|
||||
shape=(self.n_features, self.n_bins),
|
||||
dtype=HISTOGRAM_DTYPE
|
||||
)
|
||||
bint has_interaction_cst = allowed_features is not None
|
||||
int n_threads = self.n_threads
|
||||
|
||||
if has_interaction_cst:
|
||||
n_allowed_features = allowed_features.shape[0]
|
||||
|
||||
with nogil:
|
||||
n_samples = sample_indices.shape[0]
|
||||
|
||||
# Populate ordered_gradients and ordered_hessians. (Already done
|
||||
# for root) Ordering the gradients and hessians helps to improve
|
||||
# cache hit.
|
||||
if sample_indices.shape[0] != gradients.shape[0]:
|
||||
if hessians_are_constant:
|
||||
for i in prange(n_samples, schedule='static',
|
||||
num_threads=n_threads):
|
||||
ordered_gradients[i] = gradients[sample_indices[i]]
|
||||
else:
|
||||
for i in prange(n_samples, schedule='static',
|
||||
num_threads=n_threads):
|
||||
ordered_gradients[i] = gradients[sample_indices[i]]
|
||||
ordered_hessians[i] = hessians[sample_indices[i]]
|
||||
|
||||
# Compute histogram of each feature
|
||||
for f_idx in prange(
|
||||
n_allowed_features, schedule='static', num_threads=n_threads
|
||||
):
|
||||
if has_interaction_cst:
|
||||
feature_idx = allowed_features[f_idx]
|
||||
else:
|
||||
feature_idx = f_idx
|
||||
|
||||
self._compute_histogram_brute_single_feature(
|
||||
feature_idx, sample_indices, histograms
|
||||
)
|
||||
|
||||
return histograms
|
||||
|
||||
cdef void _compute_histogram_brute_single_feature(
|
||||
HistogramBuilder self,
|
||||
const int feature_idx,
|
||||
const unsigned int [::1] sample_indices, # IN
|
||||
hist_struct [:, ::1] histograms) noexcept nogil: # OUT
|
||||
"""Compute the histogram for a given feature."""
|
||||
|
||||
cdef:
|
||||
unsigned int n_samples = sample_indices.shape[0]
|
||||
const X_BINNED_DTYPE_C [::1] X_binned = \
|
||||
self.X_binned[:, feature_idx]
|
||||
unsigned int root_node = X_binned.shape[0] == n_samples
|
||||
G_H_DTYPE_C [::1] ordered_gradients = \
|
||||
self.ordered_gradients[:n_samples]
|
||||
G_H_DTYPE_C [::1] ordered_hessians = \
|
||||
self.ordered_hessians[:n_samples]
|
||||
uint8_t hessians_are_constant = \
|
||||
self.hessians_are_constant
|
||||
|
||||
# Set histograms to zero.
|
||||
memset(&histograms[feature_idx, 0], 0, self.n_bins * sizeof(hist_struct))
|
||||
|
||||
if root_node:
|
||||
if hessians_are_constant:
|
||||
_build_histogram_root_no_hessian(feature_idx, X_binned,
|
||||
ordered_gradients,
|
||||
histograms)
|
||||
else:
|
||||
_build_histogram_root(feature_idx, X_binned,
|
||||
ordered_gradients, ordered_hessians,
|
||||
histograms)
|
||||
else:
|
||||
if hessians_are_constant:
|
||||
_build_histogram_no_hessian(feature_idx,
|
||||
sample_indices, X_binned,
|
||||
ordered_gradients, histograms)
|
||||
else:
|
||||
_build_histogram(feature_idx, sample_indices,
|
||||
X_binned, ordered_gradients,
|
||||
ordered_hessians, histograms)
|
||||
|
||||
def compute_histograms_subtraction(
|
||||
HistogramBuilder self,
|
||||
hist_struct [:, ::1] parent_histograms, # IN and OUT
|
||||
hist_struct [:, ::1] sibling_histograms, # IN
|
||||
const unsigned int [:] allowed_features=None, # IN
|
||||
):
|
||||
"""Compute the histograms of the node using the subtraction trick.
|
||||
|
||||
hist(parent) = hist(left_child) + hist(right_child)
|
||||
|
||||
For a given feature, the complexity is O(n_bins). This is much more
|
||||
efficient than compute_histograms_brute, but it's only possible for one
|
||||
of the siblings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent_histograms : ndarray of HISTOGRAM_DTYPE, \
|
||||
shape (n_features, n_bins)
|
||||
The histograms of the parent.
|
||||
sibling_histograms : ndarray of HISTOGRAM_DTYPE, \
|
||||
shape (n_features, n_bins)
|
||||
The histograms of the sibling.
|
||||
allowed_features : None or ndarray, dtype=np.uint32
|
||||
Indices of the features that are allowed by interaction constraints to be
|
||||
split.
|
||||
|
||||
Returns
|
||||
-------
|
||||
histograms : ndarray of HISTOGRAM_DTYPE, shape(n_features, n_bins)
|
||||
The computed histograms of the current node.
|
||||
We repurpose parent_histograms for this and don't need to allocate new
|
||||
memory.
|
||||
"""
|
||||
|
||||
cdef:
|
||||
int feature_idx
|
||||
int f_idx
|
||||
int n_allowed_features = self.n_features
|
||||
bint has_interaction_cst = allowed_features is not None
|
||||
int n_threads = self.n_threads
|
||||
|
||||
if has_interaction_cst:
|
||||
n_allowed_features = allowed_features.shape[0]
|
||||
|
||||
# Compute histogram of each feature
|
||||
for f_idx in prange(n_allowed_features, schedule='static', nogil=True,
|
||||
num_threads=n_threads):
|
||||
if has_interaction_cst:
|
||||
feature_idx = allowed_features[f_idx]
|
||||
else:
|
||||
feature_idx = f_idx
|
||||
|
||||
_subtract_histograms(
|
||||
feature_idx,
|
||||
self.n_bins,
|
||||
parent_histograms,
|
||||
sibling_histograms,
|
||||
)
|
||||
return parent_histograms
|
||||
|
||||
|
||||
cpdef void _build_histogram_naive(
|
||||
const int feature_idx,
|
||||
unsigned int [:] sample_indices, # IN
|
||||
X_BINNED_DTYPE_C [:] binned_feature, # IN
|
||||
G_H_DTYPE_C [:] ordered_gradients, # IN
|
||||
G_H_DTYPE_C [:] ordered_hessians, # IN
|
||||
hist_struct [:, :] out) noexcept nogil: # OUT
|
||||
"""Build histogram in a naive way, without optimizing for cache hit.
|
||||
|
||||
Used in tests to compare with the optimized version."""
|
||||
cdef:
|
||||
unsigned int i
|
||||
unsigned int n_samples = sample_indices.shape[0]
|
||||
unsigned int sample_idx
|
||||
unsigned int bin_idx
|
||||
|
||||
for i in range(n_samples):
|
||||
sample_idx = sample_indices[i]
|
||||
bin_idx = binned_feature[sample_idx]
|
||||
out[feature_idx, bin_idx].sum_gradients += ordered_gradients[i]
|
||||
out[feature_idx, bin_idx].sum_hessians += ordered_hessians[i]
|
||||
out[feature_idx, bin_idx].count += 1
|
||||
|
||||
|
||||
cpdef void _subtract_histograms(
|
||||
const int feature_idx,
|
||||
unsigned int n_bins,
|
||||
hist_struct [:, ::1] hist_a, # IN and OUT
|
||||
hist_struct [:, ::1] hist_b, # IN
|
||||
) noexcept nogil: # OUT
|
||||
"""compute hist_a = hist_a - hist_b"""
|
||||
# Note that subtraction of large sums of floating point numbers, as we have here,
|
||||
# can exhibit catastrophic cancallation. This is in particular true for gradients
|
||||
# as they can be positive and negative, while hessians are non-negative.
|
||||
# Remember that gradients and hessians are originally computed in
|
||||
# G_H_DTYPE_C = float32 precision. Therefore, if sum_gradients and sum_hessians are
|
||||
# float64, we don't loose precision. But if we also used float32 for summation, we
|
||||
# would need to take care of floating point errors.
|
||||
#
|
||||
# Note that we could protect for negative hessians by setting:
|
||||
# sum_hessians = max(0, sum_hessians)
|
||||
# But as we use float64 for summing float32, that's veeeery unlikely.
|
||||
cdef:
|
||||
unsigned int i = 0
|
||||
for i in range(n_bins):
|
||||
hist_a[feature_idx, i].sum_gradients -= hist_b[feature_idx, i].sum_gradients
|
||||
hist_a[feature_idx, i].sum_hessians -= hist_b[feature_idx, i].sum_hessians
|
||||
hist_a[feature_idx, i].count -= hist_b[feature_idx, i].count
|
||||
|
||||
|
||||
cpdef void _build_histogram(
|
||||
const int feature_idx,
|
||||
const unsigned int [::1] sample_indices, # IN
|
||||
const X_BINNED_DTYPE_C [::1] binned_feature, # IN
|
||||
const G_H_DTYPE_C [::1] ordered_gradients, # IN
|
||||
const G_H_DTYPE_C [::1] ordered_hessians, # IN
|
||||
hist_struct [:, ::1] out) noexcept nogil: # OUT
|
||||
"""Return histogram for a given feature."""
|
||||
cdef:
|
||||
unsigned int i = 0
|
||||
unsigned int n_node_samples = sample_indices.shape[0]
|
||||
unsigned int unrolled_upper = (n_node_samples // 4) * 4
|
||||
|
||||
unsigned int bin_0
|
||||
unsigned int bin_1
|
||||
unsigned int bin_2
|
||||
unsigned int bin_3
|
||||
unsigned int bin_idx
|
||||
|
||||
for i in range(0, unrolled_upper, 4):
|
||||
bin_0 = binned_feature[sample_indices[i]]
|
||||
bin_1 = binned_feature[sample_indices[i + 1]]
|
||||
bin_2 = binned_feature[sample_indices[i + 2]]
|
||||
bin_3 = binned_feature[sample_indices[i + 3]]
|
||||
|
||||
out[feature_idx, bin_0].sum_gradients += ordered_gradients[i]
|
||||
out[feature_idx, bin_1].sum_gradients += ordered_gradients[i + 1]
|
||||
out[feature_idx, bin_2].sum_gradients += ordered_gradients[i + 2]
|
||||
out[feature_idx, bin_3].sum_gradients += ordered_gradients[i + 3]
|
||||
|
||||
out[feature_idx, bin_0].sum_hessians += ordered_hessians[i]
|
||||
out[feature_idx, bin_1].sum_hessians += ordered_hessians[i + 1]
|
||||
out[feature_idx, bin_2].sum_hessians += ordered_hessians[i + 2]
|
||||
out[feature_idx, bin_3].sum_hessians += ordered_hessians[i + 3]
|
||||
|
||||
out[feature_idx, bin_0].count += 1
|
||||
out[feature_idx, bin_1].count += 1
|
||||
out[feature_idx, bin_2].count += 1
|
||||
out[feature_idx, bin_3].count += 1
|
||||
|
||||
for i in range(unrolled_upper, n_node_samples):
|
||||
bin_idx = binned_feature[sample_indices[i]]
|
||||
out[feature_idx, bin_idx].sum_gradients += ordered_gradients[i]
|
||||
out[feature_idx, bin_idx].sum_hessians += ordered_hessians[i]
|
||||
out[feature_idx, bin_idx].count += 1
|
||||
|
||||
|
||||
cpdef void _build_histogram_no_hessian(
|
||||
const int feature_idx,
|
||||
const unsigned int [::1] sample_indices, # IN
|
||||
const X_BINNED_DTYPE_C [::1] binned_feature, # IN
|
||||
const G_H_DTYPE_C [::1] ordered_gradients, # IN
|
||||
hist_struct [:, ::1] out) noexcept nogil: # OUT
|
||||
"""Return histogram for a given feature, not updating hessians.
|
||||
|
||||
Used when the hessians of the loss are constant (typically LS loss).
|
||||
"""
|
||||
|
||||
cdef:
|
||||
unsigned int i = 0
|
||||
unsigned int n_node_samples = sample_indices.shape[0]
|
||||
unsigned int unrolled_upper = (n_node_samples // 4) * 4
|
||||
|
||||
unsigned int bin_0
|
||||
unsigned int bin_1
|
||||
unsigned int bin_2
|
||||
unsigned int bin_3
|
||||
unsigned int bin_idx
|
||||
|
||||
for i in range(0, unrolled_upper, 4):
|
||||
bin_0 = binned_feature[sample_indices[i]]
|
||||
bin_1 = binned_feature[sample_indices[i + 1]]
|
||||
bin_2 = binned_feature[sample_indices[i + 2]]
|
||||
bin_3 = binned_feature[sample_indices[i + 3]]
|
||||
|
||||
out[feature_idx, bin_0].sum_gradients += ordered_gradients[i]
|
||||
out[feature_idx, bin_1].sum_gradients += ordered_gradients[i + 1]
|
||||
out[feature_idx, bin_2].sum_gradients += ordered_gradients[i + 2]
|
||||
out[feature_idx, bin_3].sum_gradients += ordered_gradients[i + 3]
|
||||
|
||||
out[feature_idx, bin_0].count += 1
|
||||
out[feature_idx, bin_1].count += 1
|
||||
out[feature_idx, bin_2].count += 1
|
||||
out[feature_idx, bin_3].count += 1
|
||||
|
||||
for i in range(unrolled_upper, n_node_samples):
|
||||
bin_idx = binned_feature[sample_indices[i]]
|
||||
out[feature_idx, bin_idx].sum_gradients += ordered_gradients[i]
|
||||
out[feature_idx, bin_idx].count += 1
|
||||
|
||||
|
||||
cpdef void _build_histogram_root(
|
||||
const int feature_idx,
|
||||
const X_BINNED_DTYPE_C [::1] binned_feature, # IN
|
||||
const G_H_DTYPE_C [::1] all_gradients, # IN
|
||||
const G_H_DTYPE_C [::1] all_hessians, # IN
|
||||
hist_struct [:, ::1] out) noexcept nogil: # OUT
|
||||
"""Compute histogram of the root node.
|
||||
|
||||
Unlike other nodes, the root node has to find the split among *all* the
|
||||
samples from the training set. binned_feature and all_gradients /
|
||||
all_hessians already have a consistent ordering.
|
||||
"""
|
||||
|
||||
cdef:
|
||||
unsigned int i = 0
|
||||
unsigned int n_samples = binned_feature.shape[0]
|
||||
unsigned int unrolled_upper = (n_samples // 4) * 4
|
||||
|
||||
unsigned int bin_0
|
||||
unsigned int bin_1
|
||||
unsigned int bin_2
|
||||
unsigned int bin_3
|
||||
unsigned int bin_idx
|
||||
|
||||
for i in range(0, unrolled_upper, 4):
|
||||
|
||||
bin_0 = binned_feature[i]
|
||||
bin_1 = binned_feature[i + 1]
|
||||
bin_2 = binned_feature[i + 2]
|
||||
bin_3 = binned_feature[i + 3]
|
||||
|
||||
out[feature_idx, bin_0].sum_gradients += all_gradients[i]
|
||||
out[feature_idx, bin_1].sum_gradients += all_gradients[i + 1]
|
||||
out[feature_idx, bin_2].sum_gradients += all_gradients[i + 2]
|
||||
out[feature_idx, bin_3].sum_gradients += all_gradients[i + 3]
|
||||
|
||||
out[feature_idx, bin_0].sum_hessians += all_hessians[i]
|
||||
out[feature_idx, bin_1].sum_hessians += all_hessians[i + 1]
|
||||
out[feature_idx, bin_2].sum_hessians += all_hessians[i + 2]
|
||||
out[feature_idx, bin_3].sum_hessians += all_hessians[i + 3]
|
||||
|
||||
out[feature_idx, bin_0].count += 1
|
||||
out[feature_idx, bin_1].count += 1
|
||||
out[feature_idx, bin_2].count += 1
|
||||
out[feature_idx, bin_3].count += 1
|
||||
|
||||
for i in range(unrolled_upper, n_samples):
|
||||
bin_idx = binned_feature[i]
|
||||
out[feature_idx, bin_idx].sum_gradients += all_gradients[i]
|
||||
out[feature_idx, bin_idx].sum_hessians += all_hessians[i]
|
||||
out[feature_idx, bin_idx].count += 1
|
||||
|
||||
|
||||
cpdef void _build_histogram_root_no_hessian(
|
||||
const int feature_idx,
|
||||
const X_BINNED_DTYPE_C [::1] binned_feature, # IN
|
||||
const G_H_DTYPE_C [::1] all_gradients, # IN
|
||||
hist_struct [:, ::1] out) noexcept nogil: # OUT
|
||||
"""Compute histogram of the root node, not updating hessians.
|
||||
|
||||
Used when the hessians of the loss are constant (typically LS loss).
|
||||
"""
|
||||
|
||||
cdef:
|
||||
unsigned int i = 0
|
||||
unsigned int n_samples = binned_feature.shape[0]
|
||||
unsigned int unrolled_upper = (n_samples // 4) * 4
|
||||
|
||||
unsigned int bin_0
|
||||
unsigned int bin_1
|
||||
unsigned int bin_2
|
||||
unsigned int bin_3
|
||||
unsigned int bin_idx
|
||||
|
||||
for i in range(0, unrolled_upper, 4):
|
||||
bin_0 = binned_feature[i]
|
||||
bin_1 = binned_feature[i + 1]
|
||||
bin_2 = binned_feature[i + 2]
|
||||
bin_3 = binned_feature[i + 3]
|
||||
|
||||
out[feature_idx, bin_0].sum_gradients += all_gradients[i]
|
||||
out[feature_idx, bin_1].sum_gradients += all_gradients[i + 1]
|
||||
out[feature_idx, bin_2].sum_gradients += all_gradients[i + 2]
|
||||
out[feature_idx, bin_3].sum_gradients += all_gradients[i + 3]
|
||||
|
||||
out[feature_idx, bin_0].count += 1
|
||||
out[feature_idx, bin_1].count += 1
|
||||
out[feature_idx, bin_2].count += 1
|
||||
out[feature_idx, bin_3].count += 1
|
||||
|
||||
for i in range(unrolled_upper, n_samples):
|
||||
bin_idx = binned_feature[i]
|
||||
out[feature_idx, bin_idx].sum_gradients += all_gradients[i]
|
||||
out[feature_idx, bin_idx].count += 1
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
hist_gradient_boosting_extension_metadata = {
|
||||
'_gradient_boosting': {'sources': [cython_gen.process('_gradient_boosting.pyx')],
|
||||
'dependencies': [openmp_dep]},
|
||||
'histogram': {'sources': [cython_gen.process('histogram.pyx')], 'dependencies': [openmp_dep]},
|
||||
'splitting': {'sources': [cython_gen.process('splitting.pyx')], 'dependencies': [openmp_dep]},
|
||||
'_binning': {'sources': [cython_gen.process('_binning.pyx')], 'dependencies': [openmp_dep]},
|
||||
'_predictor': {'sources': [cython_gen.process('_predictor.pyx')], 'dependencies': [openmp_dep]},
|
||||
'_bitset': {'sources': [cython_gen.process('_bitset.pyx')]},
|
||||
'common': {'sources': [cython_gen.process('common.pyx')]},
|
||||
}
|
||||
|
||||
foreach ext_name, ext_dict : hist_gradient_boosting_extension_metadata
|
||||
py.extension_module(
|
||||
ext_name,
|
||||
ext_dict.get('sources'),
|
||||
dependencies: ext_dict.get('dependencies', []),
|
||||
subdir: 'sklearn/ensemble/_hist_gradient_boosting',
|
||||
install: true
|
||||
)
|
||||
endforeach
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+1201
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
py.extension_module(
|
||||
'_gradient_boosting',
|
||||
[cython_gen.process('_gradient_boosting.pyx')] + utils_cython_tree,
|
||||
dependencies: [np_dep],
|
||||
subdir: 'sklearn/ensemble',
|
||||
install: true
|
||||
)
|
||||
|
||||
subdir('_hist_gradient_boosting')
|
||||
Reference in New Issue
Block a user