841 lines
27 KiB
Cython
841 lines
27 KiB
Cython
# encoding: utf-8
|
|
# cython: cdivision=True
|
|
# cython: boundscheck=False
|
|
# cython: wraparound=False
|
|
#
|
|
# Author: Peter Prettenhofer, Brian Holt, Gilles Louppe
|
|
#
|
|
# License: BSD Style.
|
|
|
|
cimport cython
|
|
|
|
import numpy as np
|
|
cimport numpy as np
|
|
|
|
# Define a datatype for the data array
|
|
DTYPE = np.float32
|
|
ctypedef np.float32_t DTYPE_t
|
|
ctypedef np.int8_t BOOL_t
|
|
|
|
cdef extern from "math.h":
|
|
cdef extern double log(double x)
|
|
cdef extern double pow(double base, double exponent)
|
|
|
|
cdef extern from "float.h":
|
|
cdef extern double DBL_MAX
|
|
|
|
|
|
################################################################################
|
|
# Classification entropy measures
|
|
#
|
|
# From Hastie et al. Elements of Statistical Learning, 2009.
|
|
#
|
|
# If a target is a classification outcome taking on values 0,1,...,K-1
|
|
# In node m, representing a region Rm with Nm observations, let
|
|
#
|
|
# pmk = 1/ Nm \sum_{x_i in Rm} I(yi = k)
|
|
#
|
|
# be the proportion of class k observations in node m
|
|
|
|
cdef class Criterion:
|
|
"""Interface for splitting criteria (regression and classification)"""
|
|
|
|
cdef void init(self, DTYPE_t *y, BOOL_t *sample_mask, int n_samples,
|
|
int n_total_samples):
|
|
"""Initialise the criterion class for new split point."""
|
|
pass
|
|
|
|
cdef void reset(self):
|
|
"""Reset the criterion for a new feature index."""
|
|
pass
|
|
|
|
cdef int update(self, int a, int b, DTYPE_t *y, int *X_argsorted_i,
|
|
BOOL_t *sample_mask):
|
|
"""Update the criteria for each value in interval [a,b) (where a and b
|
|
are indices in `X_argsorted_i`)."""
|
|
pass
|
|
|
|
cdef double eval(self):
|
|
"""Evaluate the criteria (aka the split error)."""
|
|
pass
|
|
|
|
cpdef np.ndarray init_value(self):
|
|
"""Get the init value of the criterion - `init` must be called before."""
|
|
pass
|
|
|
|
|
|
cdef class ClassificationCriterion(Criterion):
|
|
"""Abstract criterion for classification.
|
|
|
|
Attributes
|
|
----------
|
|
n_classes : int
|
|
The number of classes.
|
|
|
|
n_samples : int
|
|
The number of samples.
|
|
|
|
label_count_left : int*
|
|
The label counts for samples left of splitting point.
|
|
|
|
label_count_right : int*
|
|
The label counts for samples right of splitting point.
|
|
|
|
label_count_init : int*
|
|
The initial label counts for samples right of splitting point.
|
|
Used to reset `label_count_right` for each feature.
|
|
|
|
n_left : int
|
|
The number of samples left of splitting point.
|
|
|
|
n_right : int
|
|
The number of samples right of splitting point.
|
|
"""
|
|
cdef int n_classes
|
|
cdef int n_samples
|
|
cdef int* label_count_left
|
|
cdef int* label_count_right
|
|
cdef int* label_count_init
|
|
cdef int n_left
|
|
cdef int n_right
|
|
|
|
# need to store ref to arrays to prevent GC
|
|
cdef ndarray_label_count_left
|
|
cdef ndarray_label_count_right
|
|
cdef ndarray_label_count_init
|
|
|
|
def __init__(self, int n_classes):
|
|
cdef np.ndarray[np.int32_t, ndim=1] ndarray_label_count_left \
|
|
= np.zeros((n_classes,), dtype=np.int32, order='C')
|
|
cdef np.ndarray[np.int32_t, ndim=1] ndarray_label_count_right \
|
|
= np.zeros((n_classes,), dtype=np.int32, order='C')
|
|
cdef np.ndarray[np.int32_t, ndim=1] ndarray_label_count_init \
|
|
= np.zeros((n_classes,), dtype=np.int32, order='C')
|
|
|
|
self.n_classes = n_classes
|
|
self.n_samples = 0
|
|
self.n_left = 0
|
|
self.n_right = 0
|
|
self.label_count_left = <int*>ndarray_label_count_left.data
|
|
self.label_count_right = <int*>ndarray_label_count_right.data
|
|
self.label_count_init = <int*>ndarray_label_count_init.data
|
|
self.ndarray_label_count_left = ndarray_label_count_left
|
|
self.ndarray_label_count_right = ndarray_label_count_right
|
|
self.ndarray_label_count_init = ndarray_label_count_init
|
|
|
|
cdef void init(self, DTYPE_t *y, BOOL_t *sample_mask, int n_samples,
|
|
int n_total_samples):
|
|
"""Initialise the criterion class."""
|
|
cdef int c = 0
|
|
cdef int j = 0
|
|
|
|
self.n_samples = n_samples
|
|
|
|
for c from 0 <= c < self.n_classes:
|
|
self.label_count_init[c] = 0
|
|
|
|
for j from 0 <= j < n_total_samples:
|
|
if sample_mask[j] == 0:
|
|
continue
|
|
c = <int>(y[j])
|
|
self.label_count_init[c] += 1
|
|
|
|
self.reset()
|
|
|
|
cdef void reset(self):
|
|
"""Reset label_counts by setting `label_count_left to zero
|
|
and copying the init array into the right."""
|
|
cdef int c = 0
|
|
self.n_left = 0
|
|
self.n_right = self.n_samples
|
|
|
|
for c from 0 <= c < self.n_classes:
|
|
self.label_count_left[c] = 0
|
|
self.label_count_right[c] = self.label_count_init[c]
|
|
|
|
cdef int update(self, int a, int b, DTYPE_t *y, int *X_argsorted_i,
|
|
BOOL_t *sample_mask):
|
|
"""Update the criteria for each value in interval [a,b) (where a and b
|
|
are indices in `X_argsorted_i`)."""
|
|
cdef int c
|
|
# post condition: all samples from [0:b) are on the left side
|
|
for idx from a <= idx < b:
|
|
s = X_argsorted_i[idx]
|
|
if sample_mask[s] == 0:
|
|
continue
|
|
c = <int>(y[s])
|
|
self.label_count_right[c] -= 1
|
|
self.label_count_left[c] += 1
|
|
self.n_right -= 1
|
|
self.n_left += 1
|
|
|
|
return self.n_left
|
|
|
|
cdef double eval(self):
|
|
pass
|
|
|
|
cpdef np.ndarray init_value(self):
|
|
return self.ndarray_label_count_init
|
|
|
|
|
|
cdef class Gini(ClassificationCriterion):
|
|
"""Gini Index splitting criteria.
|
|
|
|
Gini index = \sum_{k=0}^{K-1} pmk (1 - pmk)
|
|
= 1 - \sum_{k=0}^{K-1} pmk ** 2
|
|
"""
|
|
|
|
cdef double eval(self):
|
|
"""Returns Gini index of left branch + Gini index of right branch. """
|
|
cdef double n_left = <double> self.n_left
|
|
cdef double n_right = <double> self.n_right
|
|
cdef double H_left = n_left * n_left
|
|
cdef double H_right = n_right * n_right
|
|
cdef int k, count_left, count_right
|
|
|
|
for k from 0 <= k < self.n_classes:
|
|
count_left = self.label_count_left[k]
|
|
if count_left > 0:
|
|
H_left -= (count_left * count_left)
|
|
count_right = self.label_count_right[k]
|
|
if count_right > 0:
|
|
H_right -= (count_right * count_right)
|
|
|
|
if n_left == 0:
|
|
H_left = 0
|
|
else:
|
|
H_left /= n_left
|
|
|
|
if n_right == 0:
|
|
H_right = 0
|
|
else:
|
|
H_right /= n_right
|
|
|
|
return (H_left + H_right) / self.n_samples
|
|
|
|
|
|
cdef class Entropy(ClassificationCriterion):
|
|
"""Entropy splitting criteria.
|
|
|
|
Cross Entropy = - \sum_{k=0}^{K-1} pmk log(pmk)
|
|
"""
|
|
|
|
cdef double eval(self):
|
|
"""Returns Entropy of left branch + Entropy index of right branch. """
|
|
cdef double H_left = 0.0
|
|
cdef double H_right = 0.0
|
|
cdef int k
|
|
cdef double e1, e2
|
|
cdef double n_left = <double> self.n_left
|
|
cdef double n_right = <double> self.n_right
|
|
|
|
for k from 0 <= k < self.n_classes:
|
|
if self.label_count_left[k] > 0:
|
|
H_left -= ((self.label_count_left[k] / n_left)
|
|
* log(self.label_count_left[k] / n_left))
|
|
if self.label_count_right[k] > 0:
|
|
H_right -= ((self.label_count_right[k] / n_right)
|
|
* log(self.label_count_right[k] / n_right))
|
|
|
|
e1 = (n_left / self.n_samples) * H_left
|
|
e2 = (n_right / self.n_samples) * H_right
|
|
return e1 + e2
|
|
|
|
|
|
cdef class RegressionCriterion(Criterion):
|
|
"""Abstract criterion for regression. Computes variance of the
|
|
target values left and right of the split point.
|
|
|
|
Computation is linear in `n_samples` by using ::
|
|
|
|
var = \sum_i^n (y_i - y_bar) ** 2
|
|
= (\sum_i^n y_i ** 2) - n_samples y_bar ** 2
|
|
|
|
Attributes
|
|
----------
|
|
n_samples : int
|
|
The number of samples
|
|
|
|
mean_left : double
|
|
The mean target value of the samples left of the split point.
|
|
|
|
mean_right : double
|
|
The mean target value of the samples right of the split.
|
|
|
|
sq_sum_left : double
|
|
The sum of squared target values left of the split point.
|
|
|
|
sq_sum_right : double
|
|
The sum of squared target values right of the split point.
|
|
|
|
var_left : double
|
|
The variance of the target values left of the split point.
|
|
|
|
var_right : double
|
|
The variance of the target values left of the split point.
|
|
|
|
n_left : int
|
|
number of samples left of split point.
|
|
|
|
n_right : int
|
|
number of samples right of split point.
|
|
"""
|
|
|
|
cdef int n_samples
|
|
cdef int n_right
|
|
cdef int n_left
|
|
|
|
cdef double mean_left
|
|
cdef double mean_right
|
|
cdef double mean_init
|
|
|
|
cdef double sq_sum_right
|
|
cdef double sq_sum_left
|
|
cdef double sq_sum_init
|
|
|
|
cdef double var_left
|
|
cdef double var_right
|
|
|
|
def __init__(self):
|
|
self.n_samples = 0
|
|
self.n_left = 0
|
|
self.n_right = 0
|
|
self.mean_left = 0.0
|
|
self.mean_right = 0.0
|
|
self.mean_init = 0.0
|
|
self.sq_sum_right = 0.0
|
|
self.sq_sum_left = 0.0
|
|
self.sq_sum_init = 0.0
|
|
self.var_left = 0.0
|
|
self.var_right = 0.0
|
|
|
|
cdef void init(self, DTYPE_t *y, BOOL_t *sample_mask, int n_samples,
|
|
int n_total_samples):
|
|
"""Initialise the criterion class; assume all samples
|
|
are in the right branch and store the mean and squared
|
|
sum in `self.mean_init` and `self.sq_sum_init`. """
|
|
self.mean_left = 0.0
|
|
self.mean_right = 0.0
|
|
self.mean_init = 0.0
|
|
self.sq_sum_right = 0.0
|
|
self.sq_sum_left = 0.0
|
|
self.sq_sum_init = 0.0
|
|
self.var_left = 0.0
|
|
self.var_right = 0.0
|
|
self.n_samples = n_samples
|
|
|
|
cdef int j = 0
|
|
for j from 0 <= j < n_total_samples:
|
|
if sample_mask[j] == 0:
|
|
continue
|
|
self.sq_sum_init += (y[j] * y[j])
|
|
self.mean_init += y[j]
|
|
|
|
self.mean_init = self.mean_init / self.n_samples
|
|
|
|
self.reset()
|
|
|
|
cdef void reset(self):
|
|
"""Reset criterion for new feature.
|
|
|
|
Assume all data in right branch and copy statistics of the
|
|
whole dataset into the auxiliary variables of the
|
|
right branch.
|
|
"""
|
|
self.n_right = self.n_samples
|
|
self.n_left = 0
|
|
self.mean_right = self.mean_init
|
|
self.mean_left = 0.0
|
|
self.sq_sum_right = self.sq_sum_init
|
|
self.sq_sum_left = 0.0
|
|
self.var_left = 0.0
|
|
self.var_right = self.sq_sum_right - \
|
|
self.n_samples * (self.mean_right * self.mean_right)
|
|
|
|
cdef int update(self, int a, int b, DTYPE_t *y, int *X_argsorted_i,
|
|
BOOL_t *sample_mask):
|
|
"""Update the criteria for each value in interval [a,b) (where a and b
|
|
are indices in `X_argsorted_i`)."""
|
|
cdef double y_idx = 0.0
|
|
cdef int idx, j
|
|
# post condition: all samples from [0:b) are on the left side
|
|
for idx from a <= idx < b:
|
|
j = X_argsorted_i[idx]
|
|
if sample_mask[j] == 0:
|
|
continue
|
|
y_idx = y[j]
|
|
self.sq_sum_left = self.sq_sum_left + (y_idx * y_idx)
|
|
self.sq_sum_right = self.sq_sum_right - (y_idx * y_idx)
|
|
|
|
self.mean_left = (self.n_left * self.mean_left + y_idx) / \
|
|
<double>(self.n_left + 1)
|
|
self.mean_right = ((self.n_samples - self.n_left) * \
|
|
self.mean_right - y_idx) / \
|
|
<double>(self.n_samples - self.n_left - 1)
|
|
|
|
self.n_right -= 1
|
|
self.n_left += 1
|
|
|
|
self.var_left = self.sq_sum_left - \
|
|
self.n_left * (self.mean_left * self.mean_left)
|
|
self.var_right = self.sq_sum_right - \
|
|
self.n_right * (self.mean_right * self.mean_right)
|
|
|
|
return self.n_left
|
|
|
|
cdef double eval(self):
|
|
pass
|
|
|
|
cpdef np.ndarray init_value(self):
|
|
## TODO is calling np.asarray a performance issue?
|
|
return np.asarray(self.mean_init)
|
|
|
|
|
|
cdef class MSE(RegressionCriterion):
|
|
"""Mean squared error impurity criterion.
|
|
|
|
MSE = var_left + var_right
|
|
"""
|
|
|
|
cdef double eval(self):
|
|
return self.var_left + self.var_right
|
|
|
|
|
|
################################################################################
|
|
# Tree util functions
|
|
#
|
|
|
|
|
|
def _random_sample_mask(int n_total_samples, int 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 : np.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 np.ndarray[np.float64_t, ndim=1, mode="c"] rand = \
|
|
random_state.rand(n_total_samples)
|
|
cdef np.ndarray[BOOL_t, ndim=1, mode="c"] sample_mask = \
|
|
np.zeros((n_total_samples,), dtype=np.int8)
|
|
|
|
cdef int n_bagged = 0
|
|
cdef int 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.astype(np.bool)
|
|
|
|
|
|
def _apply_tree(np.ndarray[DTYPE_t, ndim=2] X,
|
|
np.ndarray[np.int32_t, ndim=2] children,
|
|
np.ndarray[np.int32_t, ndim=1] feature,
|
|
np.ndarray[np.float64_t, ndim=1] threshold,
|
|
np.ndarray[np.int32_t, ndim=1] out):
|
|
"""Finds the terminal region (=leaf node) for each sample in
|
|
`X` and sets the corresponding element in `out` to its node id."""
|
|
cdef int i = 0
|
|
cdef int n = X.shape[0]
|
|
cdef int node_id = 0
|
|
for i in xrange(n):
|
|
node_id = 0
|
|
# While node_id not a leaf
|
|
while children[node_id, 0] != -1 and children[node_id, 1] != -1:
|
|
if X[i, feature[node_id]] <= threshold[node_id]:
|
|
node_id = children[node_id, 0]
|
|
else:
|
|
node_id = children[node_id, 1]
|
|
out[i] = node_id
|
|
|
|
|
|
def _predict_tree(np.ndarray[DTYPE_t, ndim=2] X,
|
|
np.ndarray[np.int32_t, ndim=2] children,
|
|
np.ndarray[np.int32_t, ndim=1] feature,
|
|
np.ndarray[np.float64_t, ndim=1] threshold,
|
|
np.ndarray[np.float64_t, ndim=2] values,
|
|
np.ndarray[np.float64_t, ndim=2] pred):
|
|
"""Finds the terminal region (=leaf node) values for each sample. """
|
|
cdef int i = 0
|
|
cdef int n = X.shape[0]
|
|
cdef int node_id = 0
|
|
cdef int K = values.shape[1]
|
|
for i in xrange(n):
|
|
node_id = 0
|
|
# While node_id not a leaf
|
|
while children[node_id, 0] != -1 and children[node_id, 1] != -1:
|
|
if X[i, feature[node_id]] <= threshold[node_id]:
|
|
node_id = children[node_id, 0]
|
|
else:
|
|
node_id = children[node_id, 1]
|
|
for k in xrange(K):
|
|
pred[i, k] = values[node_id, k]
|
|
|
|
|
|
def _predict_regression_tree_inplace(np.ndarray[DTYPE_t, ndim=2] X,
|
|
np.ndarray[np.int32_t, ndim=2] children,
|
|
np.ndarray[np.int32_t, ndim=1] feature,
|
|
np.ndarray[np.float64_t, ndim=1] threshold,
|
|
np.ndarray[np.float64_t, ndim=2] values,
|
|
double scale,
|
|
int k,
|
|
np.ndarray[np.float64_t, ndim=2] pred):
|
|
"""Predicts output for regression tree and stores it in ``pred[i, k]`` """
|
|
cdef int i = 0
|
|
cdef int n = X.shape[0]
|
|
cdef int node_id = 0
|
|
cdef int K = values.shape[1]
|
|
for i in xrange(n):
|
|
node_id = 0
|
|
# While node_id not a leaf
|
|
while children[node_id, 0] != -1 and children[node_id, 1] != -1:
|
|
if X[i, feature[node_id]] <= threshold[node_id]:
|
|
node_id = children[node_id, 0]
|
|
else:
|
|
node_id = children[node_id, 1]
|
|
pred[i, k] += scale * values[node_id, 0]
|
|
|
|
|
|
def _error_at_leaf(np.ndarray[DTYPE_t, ndim=1, mode="c"] y,
|
|
np.ndarray sample_mask, Criterion criterion,
|
|
int n_samples):
|
|
"""Compute criterion error at leaf with terminal region defined
|
|
by `sample_mask`. """
|
|
cdef int n_total_samples = y.shape[0]
|
|
cdef DTYPE_t *y_ptr = <DTYPE_t *>y.data
|
|
cdef BOOL_t *sample_mask_ptr = <BOOL_t *>sample_mask.data
|
|
criterion.init(y_ptr, sample_mask_ptr, n_samples, n_total_samples)
|
|
return criterion.eval()
|
|
|
|
|
|
cdef int smallest_sample_larger_than(int sample_idx, DTYPE_t *X_i,
|
|
int *X_argsorted_i, BOOL_t *sample_mask,
|
|
int n_total_samples):
|
|
"""Find the largest next sample.
|
|
|
|
Find the index in the `X_i` array for sample who's feature
|
|
`i` value is just about greater than those of the sample
|
|
`X_argsorted_i[sample_idx]`.
|
|
|
|
Returns
|
|
-------
|
|
next_sample_idx : int
|
|
The index of the next smallest sample in `X_argsorted`
|
|
with different feature value than `sample_idx` .
|
|
I.e. `X_argsorted_i[sample_idx] < X_argsorted_i[next_sample_idx]`
|
|
-1 if no such element exists.
|
|
"""
|
|
cdef int idx = 0, j
|
|
cdef DTYPE_t threshold = -DBL_MAX
|
|
|
|
if sample_idx > -1:
|
|
threshold = X_i[X_argsorted_i[sample_idx]]
|
|
|
|
for idx from sample_idx < idx < n_total_samples:
|
|
j = X_argsorted_i[idx]
|
|
|
|
if sample_mask[j] == 0:
|
|
continue
|
|
|
|
if X_i[j] > threshold + 1.e-7:
|
|
return idx
|
|
|
|
return -1
|
|
|
|
|
|
def _find_best_split(np.ndarray[DTYPE_t, ndim=2, mode="fortran"] X,
|
|
np.ndarray[DTYPE_t, ndim=1, mode="c"] y,
|
|
np.ndarray[np.int32_t, ndim=2, mode="fortran"] X_argsorted,
|
|
np.ndarray sample_mask,
|
|
int n_samples,
|
|
int min_leaf,
|
|
int max_features,
|
|
Criterion criterion,
|
|
object random_state):
|
|
"""Find the best dimension and threshold that minimises the error.
|
|
|
|
Parameters
|
|
----------
|
|
X : ndarray, shape (n_total_samples, n_features), dtype=DTYPE_t
|
|
The feature values.
|
|
|
|
y : ndarray, shape (n_total_samples,), dtype=float
|
|
The label to predict for each sample.
|
|
|
|
X_argsorted : ndarray, shape (n_samples, n_features)
|
|
Argsort of cols of `X`. `X_argsorted[0,j]` gives the example
|
|
index of the smallest value of feature `j`.
|
|
|
|
sample_mask : ndarray, shape (n_samples,), dtype=np.bool
|
|
A mask for the samples to be considered. Only samples `j` for which
|
|
sample_mask[j] != 0 are considered.
|
|
|
|
n_samples : int
|
|
The number of samples in the current sample_mask
|
|
(i.e. `sample_mask.sum()`).
|
|
|
|
min_leaf : int
|
|
The minimum number of samples required to be at a leaf node.
|
|
|
|
max_features : int
|
|
The number of features to consider when looking for the best split.
|
|
|
|
criterion : Criterion
|
|
The criterion function to be minimized.
|
|
|
|
random_state : RandomState
|
|
The numpy random state to use.
|
|
|
|
Returns
|
|
-------
|
|
best_i : int
|
|
The split feature or -1 if criterion not smaller than
|
|
`parent_split_error`.
|
|
|
|
best_t : DTYPE_t
|
|
The split threshold
|
|
|
|
best_error : DTYPE_t
|
|
The split error
|
|
|
|
initial_error : DTYPE_t
|
|
The initial error contained in the node.
|
|
"""
|
|
# Variables declarations
|
|
cdef int n_total_samples = X.shape[0]
|
|
cdef int n_features = X.shape[1]
|
|
cdef int i, a, b, best_i = -1
|
|
cdef int n_left = 0
|
|
cdef DTYPE_t t, initial_error, error
|
|
cdef DTYPE_t best_error = np.inf, best_t = np.inf
|
|
cdef DTYPE_t *y_ptr = <DTYPE_t *>y.data
|
|
cdef DTYPE_t *X_i = NULL
|
|
cdef int *X_argsorted_i = NULL
|
|
cdef BOOL_t *sample_mask_ptr = <BOOL_t *>sample_mask.data
|
|
|
|
# Compute the column strides (increment in pointer elements to get
|
|
# from column i to i + 1) for `X` and `X_argsorted`
|
|
cdef int X_elem_stride = X.strides[0]
|
|
cdef int X_col_stride = X.strides[1]
|
|
cdef int X_stride = X_col_stride / X_elem_stride
|
|
cdef int X_argsorted_elem_stride = X_argsorted.strides[0]
|
|
cdef int X_argsorted_col_stride = X_argsorted.strides[1]
|
|
cdef int X_argsorted_stride = X_argsorted_col_stride / X_argsorted_elem_stride
|
|
|
|
# Compute the initial criterion value in the node
|
|
X_argsorted_i = <int *>X_argsorted.data
|
|
criterion.init(y_ptr, sample_mask_ptr, n_samples, n_total_samples)
|
|
initial_error = criterion.eval()
|
|
|
|
if initial_error == 0: # break early if the node is pure
|
|
return best_i, best_t, initial_error, initial_error
|
|
|
|
best_error = initial_error
|
|
|
|
# Features to consider
|
|
if max_features < 0 or max_features == n_features:
|
|
features = np.arange(n_features)
|
|
else:
|
|
features = random_state.permutation(n_features)[:max_features]
|
|
|
|
# Look for the best split
|
|
for i in features:
|
|
# Get i-th col of X and X_sorted
|
|
X_i = (<DTYPE_t *>X.data) + X_stride * i
|
|
X_argsorted_i = (<int *>X_argsorted.data) + X_argsorted_stride * i
|
|
|
|
# Reset the criterion for this feature
|
|
criterion.reset()
|
|
|
|
# Index of smallest sample in X_argsorted_i that is in the sample mask
|
|
a = 0
|
|
while sample_mask_ptr[X_argsorted_i[a]] == 0:
|
|
a = a + 1
|
|
|
|
# Consider splits between two consecutive samples
|
|
while True:
|
|
# Find the following larger sample
|
|
b = smallest_sample_larger_than(a, X_i, X_argsorted_i,
|
|
sample_mask_ptr, n_total_samples)
|
|
if b == -1:
|
|
break
|
|
|
|
# Better split than the best so far?
|
|
n_left = criterion.update(a, b, y_ptr, X_argsorted_i, sample_mask_ptr)
|
|
|
|
# Only consider splits that respect min_leaf
|
|
if n_left < min_leaf or (n_samples - n_left) < min_leaf:
|
|
a = b
|
|
continue
|
|
|
|
error = criterion.eval()
|
|
|
|
if error < best_error:
|
|
t = X_i[X_argsorted_i[a]] + \
|
|
((X_i[X_argsorted_i[b]] - X_i[X_argsorted_i[a]]) / 2.0)
|
|
if t == X_i[X_argsorted_i[b]]:
|
|
t = X_i[X_argsorted_i[a]]
|
|
best_i = i
|
|
best_t = t
|
|
best_error = error
|
|
|
|
# Proceed to the next interval
|
|
a = b
|
|
|
|
return best_i, best_t, best_error, initial_error
|
|
|
|
def _find_best_random_split(np.ndarray[DTYPE_t, ndim=2, mode="fortran"] X,
|
|
np.ndarray[DTYPE_t, ndim=1, mode="c"] y,
|
|
np.ndarray[np.int32_t, ndim=2, mode="fortran"] X_argsorted,
|
|
np.ndarray sample_mask,
|
|
int n_samples,
|
|
int min_leaf,
|
|
int max_features,
|
|
Criterion criterion,
|
|
object random_state):
|
|
"""Find the best dimension and threshold that minimises the error.
|
|
|
|
Parameters
|
|
----------
|
|
X : ndarray, shape (n_total_samples, n_features), dtype=DTYPE_t
|
|
The feature values.
|
|
|
|
y : ndarray, shape (n_total_samples,), dtype=float
|
|
The label to predict for each sample.
|
|
|
|
X_argsorted : ndarray, shape (n_samples, n_features)
|
|
Argsort of cols of `X`. `X_argsorted[0,j]` gives the example
|
|
index of the smallest value of feature `j`.
|
|
|
|
sample_mask : ndarray, shape (n_samples,), dtype=np.bool
|
|
A mask for the samples to be considered. Only samples `j` for which
|
|
sample_mask[j] != 0 are considered.
|
|
|
|
n_samples : int
|
|
The number of samples in the current sample_mask
|
|
(i.e. `sample_mask.sum()`).
|
|
|
|
min_leaf : int
|
|
The minimum number of samples required to be at a leaf node.
|
|
|
|
max_features : int
|
|
The number of features to consider when looking for the best split.
|
|
|
|
criterion : Criterion
|
|
The criterion function to be minimized.
|
|
|
|
random_state : RandomState
|
|
The numpy random state to use.
|
|
|
|
Returns
|
|
-------
|
|
best_i : int
|
|
The split feature or -1 if criterion not smaller than
|
|
`parent_split_error`.
|
|
|
|
best_t : DTYPE_t
|
|
The split threshold
|
|
|
|
best_error : DTYPE_t
|
|
The split error
|
|
|
|
initial_error : DTYPE_t
|
|
The initial error contained in the node.
|
|
"""
|
|
# Variables
|
|
cdef int n_total_samples = X.shape[0]
|
|
cdef int n_features = X.shape[1]
|
|
cdef int i, a, b, best_i = -1
|
|
cdef DTYPE_t t, initial_error, error
|
|
cdef DTYPE_t best_error = np.inf, best_t = np.inf
|
|
cdef DTYPE_t *y_ptr = <DTYPE_t *>y.data
|
|
cdef DTYPE_t *X_i = NULL
|
|
cdef int *X_argsorted_i = NULL
|
|
cdef BOOL_t *sample_mask_ptr = <BOOL_t *>sample_mask.data
|
|
|
|
# Compute the column strides (increment in pointer elements to get
|
|
# from column i to i + 1) for `X` and `X_argsorted`
|
|
cdef int X_elem_stride = X.strides[0]
|
|
cdef int X_col_stride = X.strides[1]
|
|
cdef int X_stride = X_col_stride / X_elem_stride
|
|
cdef int X_argsorted_elem_stride = X_argsorted.strides[0]
|
|
cdef int X_argsorted_col_stride = X_argsorted.strides[1]
|
|
cdef int X_argsorted_stride = X_argsorted_col_stride / X_argsorted_elem_stride
|
|
|
|
# Compute the initial criterion value
|
|
X_argsorted_i = <int *>X_argsorted.data
|
|
criterion.init(y_ptr, sample_mask_ptr, n_samples, n_total_samples)
|
|
initial_error = criterion.eval()
|
|
|
|
if initial_error == 0: # break early if the node is pure
|
|
return best_i, best_t, best_error, initial_error
|
|
|
|
best_error = initial_error
|
|
|
|
# Features to consider
|
|
if max_features == n_features:
|
|
features = np.arange(n_features)
|
|
else:
|
|
features = random_state.permutation(n_features)[:max_features]
|
|
|
|
# Look for the best random split
|
|
for i in features:
|
|
# Get i-th col of X and X_sorted
|
|
X_i = (<DTYPE_t *>X.data) + X_stride * i
|
|
X_argsorted_i = (<int *>X_argsorted.data) + X_argsorted_stride * i
|
|
|
|
# Reset the criterion for this feature
|
|
criterion.reset()
|
|
|
|
# Find min and max
|
|
a = 0
|
|
while sample_mask_ptr[X_argsorted_i[a]] == 0:
|
|
a = a + 1
|
|
|
|
b = n_total_samples - 1
|
|
while sample_mask_ptr[X_argsorted_i[b]] == 0:
|
|
b = b - 1
|
|
|
|
if b <= a or X_i[X_argsorted_i[a]] == X_i[X_argsorted_i[b]]:
|
|
continue
|
|
|
|
# Draw a random threshold in [a, b)
|
|
t = X_i[X_argsorted_i[a]] + random_state.rand() * (X_i[X_argsorted_i[b]] - X_i[X_argsorted_i[a]])
|
|
if t == X_i[X_argsorted_i[b]]:
|
|
t = X_i[X_argsorted_i[a]]
|
|
|
|
# Find the sample just greater than t
|
|
c = a + 1
|
|
|
|
while True:
|
|
if sample_mask_ptr[X_argsorted_i[c]] != 0:
|
|
if X_i[X_argsorted_i[c]] > t or c == b:
|
|
break
|
|
|
|
c += 1
|
|
|
|
# Better than the best so far?
|
|
n_left = criterion.update(0, c, y_ptr, X_argsorted_i, sample_mask_ptr)
|
|
error = criterion.eval()
|
|
|
|
if n_left < min_leaf or (n_samples - n_left) < min_leaf:
|
|
continue
|
|
|
|
if error < best_error:
|
|
best_i = i
|
|
best_t = t
|
|
best_error = error
|
|
|
|
return best_i, best_t, best_error, initial_error
|