PyDLLib 1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DLL/Data/Metrics.py +626 -0
- DLL/Data/Preprocessing.py +384 -0
- DLL/Data/_DataReader.py +55 -0
- DLL/Data/__init__.py +1 -0
- DLL/DeepLearning/Callbacks/_BackUp.py +71 -0
- DLL/DeepLearning/Callbacks/_BaseCallback.py +43 -0
- DLL/DeepLearning/Callbacks/_EarlyStopping.py +102 -0
- DLL/DeepLearning/Callbacks/_ReduceLROnPlateau.py +90 -0
- DLL/DeepLearning/Callbacks/__init__.py +4 -0
- DLL/DeepLearning/Initialisers/_Initialiser.py +24 -0
- DLL/DeepLearning/Initialisers/_Kaiming_He.py +55 -0
- DLL/DeepLearning/Initialisers/_Xavier_Glorot.py +39 -0
- DLL/DeepLearning/Initialisers/__init__.py +2 -0
- DLL/DeepLearning/Layers/Activations/_Activation.py +23 -0
- DLL/DeepLearning/Layers/Activations/_ReLU.py +51 -0
- DLL/DeepLearning/Layers/Activations/_Sigmoid.py +51 -0
- DLL/DeepLearning/Layers/Activations/_SoftMax.py +83 -0
- DLL/DeepLearning/Layers/Activations/_Tanh.py +47 -0
- DLL/DeepLearning/Layers/Activations/__init__.py +4 -0
- DLL/DeepLearning/Layers/Regularisation/_BaseRegularisation.py +12 -0
- DLL/DeepLearning/Layers/Regularisation/_BatchNormalisation.py +106 -0
- DLL/DeepLearning/Layers/Regularisation/_Dropout.py +79 -0
- DLL/DeepLearning/Layers/Regularisation/_GroupNormalisation.py +112 -0
- DLL/DeepLearning/Layers/Regularisation/_InstanceNormalisation.py +50 -0
- DLL/DeepLearning/Layers/Regularisation/_LayerNormalisation.py +37 -0
- DLL/DeepLearning/Layers/Regularisation/__init__.py +5 -0
- DLL/DeepLearning/Layers/_Add.py +109 -0
- DLL/DeepLearning/Layers/_Attention.py +161 -0
- DLL/DeepLearning/Layers/_BaseLayer.py +87 -0
- DLL/DeepLearning/Layers/_Bidirectional.py +113 -0
- DLL/DeepLearning/Layers/_Conv2D.py +133 -0
- DLL/DeepLearning/Layers/_Dense.py +109 -0
- DLL/DeepLearning/Layers/_DenseKAN.py +184 -0
- DLL/DeepLearning/Layers/_Flatten.py +55 -0
- DLL/DeepLearning/Layers/_Identity.py +50 -0
- DLL/DeepLearning/Layers/_Input.py +11 -0
- DLL/DeepLearning/Layers/_LSTM.py +261 -0
- DLL/DeepLearning/Layers/_LayerList.py +98 -0
- DLL/DeepLearning/Layers/_MaxPooling2D.py +101 -0
- DLL/DeepLearning/Layers/_RNN.py +185 -0
- DLL/DeepLearning/Layers/_Reshape.py +52 -0
- DLL/DeepLearning/Layers/__init__.py +14 -0
- DLL/DeepLearning/Losses/_BCE.py +96 -0
- DLL/DeepLearning/Losses/_BaseLoss.py +18 -0
- DLL/DeepLearning/Losses/_CCE.py +87 -0
- DLL/DeepLearning/Losses/_Exponential.py +101 -0
- DLL/DeepLearning/Losses/_Huber.py +104 -0
- DLL/DeepLearning/Losses/_MAE.py +85 -0
- DLL/DeepLearning/Losses/_MSE.py +86 -0
- DLL/DeepLearning/Losses/__init__.py +6 -0
- DLL/DeepLearning/Model.py +265 -0
- DLL/DeepLearning/Optimisers/_ADADELTA.py +53 -0
- DLL/DeepLearning/Optimisers/_ADAGRAD.py +51 -0
- DLL/DeepLearning/Optimisers/_ADAM.py +65 -0
- DLL/DeepLearning/Optimisers/_BaseOptimiser.py +20 -0
- DLL/DeepLearning/Optimisers/_LBFGS.py +120 -0
- DLL/DeepLearning/Optimisers/_RMSPROP.py +62 -0
- DLL/DeepLearning/Optimisers/_SGD.py +44 -0
- DLL/DeepLearning/Optimisers/__init__.py +6 -0
- DLL/DeepLearning/__init__.py +0 -0
- DLL/Exceptions/_Exceptions.py +19 -0
- DLL/Exceptions/__init__.py +2 -0
- DLL/MachineLearning/SupervisedLearning/Calibration/_CalibratedClassifier.py +88 -0
- DLL/MachineLearning/SupervisedLearning/Calibration/_IsotonicRegression.py +114 -0
- DLL/MachineLearning/SupervisedLearning/Calibration/__init__.py +2 -0
- DLL/MachineLearning/SupervisedLearning/GaussianProcesses/_GaussianProcessRegressor.py +190 -0
- DLL/MachineLearning/SupervisedLearning/GaussianProcesses/__init__.py +1 -0
- DLL/MachineLearning/SupervisedLearning/Kernels.py +851 -0
- DLL/MachineLearning/SupervisedLearning/LinearModels/TimeSeries.py +147 -0
- DLL/MachineLearning/SupervisedLearning/LinearModels/_Elasticnet.py +161 -0
- DLL/MachineLearning/SupervisedLearning/LinearModels/_LassoRegression.py +157 -0
- DLL/MachineLearning/SupervisedLearning/LinearModels/_LinearRegression.py +81 -0
- DLL/MachineLearning/SupervisedLearning/LinearModels/_LogisticRegression.py +182 -0
- DLL/MachineLearning/SupervisedLearning/LinearModels/_RANSAC.py +97 -0
- DLL/MachineLearning/SupervisedLearning/LinearModels/_RidgeRegression.py +78 -0
- DLL/MachineLearning/SupervisedLearning/LinearModels/__init__.py +6 -0
- DLL/MachineLearning/SupervisedLearning/NaiveBayes/_BernoulliNaiveBayes.py +115 -0
- DLL/MachineLearning/SupervisedLearning/NaiveBayes/_GaussianNaiveBayes.py +111 -0
- DLL/MachineLearning/SupervisedLearning/NaiveBayes/_MultinomialNaiveBayes.py +115 -0
- DLL/MachineLearning/SupervisedLearning/NaiveBayes/__init__.py +3 -0
- DLL/MachineLearning/SupervisedLearning/Neighbors/_KNearestNeighborsClassifier.py +115 -0
- DLL/MachineLearning/SupervisedLearning/Neighbors/_KNearestNeighborsRegressor.py +90 -0
- DLL/MachineLearning/SupervisedLearning/Neighbors/__init__.py +2 -0
- DLL/MachineLearning/SupervisedLearning/SupportVectorMachines/_SVC.py +293 -0
- DLL/MachineLearning/SupervisedLearning/SupportVectorMachines/_SVR.py +100 -0
- DLL/MachineLearning/SupervisedLearning/SupportVectorMachines/__init__.py +2 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_AdaBoostClassifier.py +179 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_AdaBoostRegressor.py +141 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_DecisionTree.py +219 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_GradientBoostingClassifier.py +207 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_GradientBoostingRegressor.py +122 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_LGBMClassifier.py +288 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_LGBMRegressor.py +168 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_LGBMTree.py +199 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_RandomForest.py +96 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_RegressionForest.py +75 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_RegressionTree.py +148 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_XGBoostClassifier.py +239 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_XGBoostRegressor.py +131 -0
- DLL/MachineLearning/SupervisedLearning/Trees/_XGBoostTree.py +105 -0
- DLL/MachineLearning/SupervisedLearning/Trees/__init__.py +12 -0
- DLL/MachineLearning/SupervisedLearning/__init__.py +0 -0
- DLL/MachineLearning/UnsupervisedLearning/Clustering/_DBScan.py +87 -0
- DLL/MachineLearning/UnsupervisedLearning/Clustering/_GaussianMixtureModels.py +121 -0
- DLL/MachineLearning/UnsupervisedLearning/Clustering/_KMeans.py +120 -0
- DLL/MachineLearning/UnsupervisedLearning/Clustering/_SpectralClustering.py +105 -0
- DLL/MachineLearning/UnsupervisedLearning/Clustering/__init__.py +4 -0
- DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_LDA.py +118 -0
- DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_PCA.py +101 -0
- DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_QDA.py +69 -0
- DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_RobustPCA.py +143 -0
- DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_TSNE.py +151 -0
- DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_UMAP.py +176 -0
- DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/__init__.py +6 -0
- DLL/MachineLearning/UnsupervisedLearning/OutlierDetection/_IsolationForest.py +175 -0
- DLL/MachineLearning/UnsupervisedLearning/OutlierDetection/__init__.py +1 -0
- DLL/MachineLearning/UnsupervisedLearning/__init__.py +0 -0
- DLL/MachineLearning/__init__.py +0 -0
- DLL/__init__.py +0 -0
- pydllib-1.0.dist-info/METADATA +104 -0
- pydllib-1.0.dist-info/RECORD +124 -0
- pydllib-1.0.dist-info/WHEEL +5 -0
- pydllib-1.0.dist-info/licenses/LICENSE +21 -0
- pydllib-1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from ._DecisionTree import Node
|
|
4
|
+
from ....Exceptions import NotFittedError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RegressionTree:
|
|
8
|
+
"""
|
|
9
|
+
RegressionTree implements a regression algorithm splitting the data along features minimizing the variance.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
max_depth (int, optional): The maximum depth of the tree. Defaults to 25. Must be a positive integer.
|
|
13
|
+
min_samples_split (int, optional): The minimum required samples in a leaf to make a split. Defaults to 2. Must be a positive integer.
|
|
14
|
+
ccp_alpha (non-negative float, optional): Determines how easily subtrees are pruned in cost-complexity pruning. The larger the value, more subtrees are pruned. Defaults to 0.0.
|
|
15
|
+
"""
|
|
16
|
+
def __init__(self, max_depth=25, min_samples_split=2, ccp_alpha=0.0):
|
|
17
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
18
|
+
raise ValueError("max_depth must be a positive integer.")
|
|
19
|
+
if not isinstance(min_samples_split, int) or min_samples_split < 1:
|
|
20
|
+
raise ValueError("min_samples_split must be a positive integer.")
|
|
21
|
+
if not isinstance(ccp_alpha, int | float) or ccp_alpha < 0:
|
|
22
|
+
raise ValueError("ccp_alpha must be non-negative.")
|
|
23
|
+
|
|
24
|
+
self.max_depth = max_depth
|
|
25
|
+
self.min_samples_split = min_samples_split
|
|
26
|
+
self.ccp_alpha = ccp_alpha
|
|
27
|
+
self.root = None
|
|
28
|
+
|
|
29
|
+
def fit(self, X, y):
|
|
30
|
+
"""
|
|
31
|
+
Fits the RegressionTree model to the input data by generating a tree, which splits the data appropriately.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
|
|
35
|
+
y (torch.Tensor of shape (n_samples,)): The target values corresponding to each sample.
|
|
36
|
+
Returns:
|
|
37
|
+
None
|
|
38
|
+
Raises:
|
|
39
|
+
TypeError: If the input matrix or the target matrix is not a PyTorch tensor.
|
|
40
|
+
ValueError: If the input matrix or the target matrix is not the correct shape.
|
|
41
|
+
"""
|
|
42
|
+
if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
|
|
43
|
+
raise TypeError("The input matrix and the target matrix must be a PyTorch tensor.")
|
|
44
|
+
if X.ndim != 2:
|
|
45
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor.")
|
|
46
|
+
if y.ndim != 1 or y.shape[0] != X.shape[0]:
|
|
47
|
+
raise ValueError("The target must be 1 dimensional with the same number of samples as the input data")
|
|
48
|
+
self.n_features = X.shape[1]
|
|
49
|
+
self.root = self._grow_tree(X, y, 0)
|
|
50
|
+
if self.ccp_alpha > 0:
|
|
51
|
+
self.root, _, _ = self._prune(self.root, X, y)
|
|
52
|
+
|
|
53
|
+
def _grow_tree(self, x, y, depth):
|
|
54
|
+
n_samples, n_features = x.size()
|
|
55
|
+
classes = torch.unique(y)
|
|
56
|
+
|
|
57
|
+
if depth >= self.max_depth or len(classes) == 1 or n_samples < self.min_samples_split:
|
|
58
|
+
return Node(value=torch.mean(y))
|
|
59
|
+
|
|
60
|
+
feature_indicies = torch.randperm(n_features)[:self.n_features]
|
|
61
|
+
split_threshold, split_index = self._best_split(x, y, feature_indicies)
|
|
62
|
+
# if no split gains more information
|
|
63
|
+
if split_threshold is None:
|
|
64
|
+
return Node(value=torch.mean(y))
|
|
65
|
+
|
|
66
|
+
left_indicies, right_indicies = self._split(x[:, split_index], split_threshold)
|
|
67
|
+
left = self._grow_tree(x[left_indicies], y[left_indicies], depth + 1)
|
|
68
|
+
right = self._grow_tree(x[right_indicies], y[right_indicies], depth + 1)
|
|
69
|
+
return Node(left, right, split_threshold, split_index)
|
|
70
|
+
|
|
71
|
+
def _best_split(self, x, y, feature_indicies):
|
|
72
|
+
max_variance_reduction = -1
|
|
73
|
+
split_index = None
|
|
74
|
+
split_threshold = None
|
|
75
|
+
|
|
76
|
+
for index in feature_indicies:
|
|
77
|
+
feature_values = x[:, index]
|
|
78
|
+
possible_thresholds = torch.unique(feature_values)
|
|
79
|
+
for threshold in possible_thresholds:
|
|
80
|
+
variance_reduction = self._variance_reduction(y, feature_values, threshold)
|
|
81
|
+
if variance_reduction > max_variance_reduction:
|
|
82
|
+
max_variance_reduction = variance_reduction
|
|
83
|
+
split_index = index
|
|
84
|
+
split_threshold = threshold
|
|
85
|
+
if max_variance_reduction <= 0: return None, None
|
|
86
|
+
return split_threshold, split_index
|
|
87
|
+
|
|
88
|
+
def _split(self, feature_values, threshold):
|
|
89
|
+
left_indicies = torch.argwhere(feature_values <= threshold).flatten()
|
|
90
|
+
right_indicies = torch.argwhere(feature_values > threshold).flatten()
|
|
91
|
+
return left_indicies, right_indicies
|
|
92
|
+
|
|
93
|
+
def _variance_reduction(self, y, feature_values, threshold):
|
|
94
|
+
left_indicies, right_indicies = self._split(feature_values, threshold)
|
|
95
|
+
if len(left_indicies) <= 1 or len(right_indicies) <= 1:
|
|
96
|
+
return 0
|
|
97
|
+
n = len(y)
|
|
98
|
+
return torch.var(y) - len(left_indicies) / n * torch.var(y[left_indicies]) - len(right_indicies) / n * torch.var(y[right_indicies])
|
|
99
|
+
|
|
100
|
+
def predict(self, X):
|
|
101
|
+
"""
|
|
102
|
+
Applies the fitted RegressionTree model to the input data, predicting the correct values.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be regressed.
|
|
106
|
+
Returns:
|
|
107
|
+
target values (torch.Tensor of shape (n_samples,)): The predicted values corresponding to each sample.
|
|
108
|
+
Raises:
|
|
109
|
+
NotFittedError: If the RegressionTree model has not been fitted before predicting.
|
|
110
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
111
|
+
ValueError: If the input matrix is not the correct shape.
|
|
112
|
+
"""
|
|
113
|
+
if self.root is None:
|
|
114
|
+
raise NotFittedError("RegressionTree.fit() must be called before predicting.")
|
|
115
|
+
if not isinstance(X, torch.Tensor):
|
|
116
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
117
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
118
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
119
|
+
return torch.tensor([self._predict_single(datapoint, self.root) for datapoint in X])
|
|
120
|
+
|
|
121
|
+
def _predict_single(self, x, current_node):
|
|
122
|
+
if current_node.is_leaf():
|
|
123
|
+
return current_node.value
|
|
124
|
+
|
|
125
|
+
if x[current_node.feature_index] <= current_node.threshold:
|
|
126
|
+
return self._predict_single(x, current_node.left)
|
|
127
|
+
|
|
128
|
+
return self._predict_single(x, current_node.right)
|
|
129
|
+
|
|
130
|
+
def _prune(self, node, X, y):
|
|
131
|
+
if node.is_leaf():
|
|
132
|
+
return node, torch.sum((y - node.value) ** 2).item(), 1
|
|
133
|
+
|
|
134
|
+
left_indicies, right_indicies = self._split(X[:, node.feature_index], node.threshold)
|
|
135
|
+
node.left, left_cost, left_leaf_nodes = self._prune(node.left, X[left_indicies], y[left_indicies])
|
|
136
|
+
node.right, right_cost, right_leaf_nodes = self._prune(node.right, X[right_indicies], y[right_indicies])
|
|
137
|
+
subtree_cost = left_cost + right_cost
|
|
138
|
+
total_subleaves = left_leaf_nodes + right_leaf_nodes
|
|
139
|
+
|
|
140
|
+
leaf_value = torch.mean(y)
|
|
141
|
+
new_leaf_nodes = 1
|
|
142
|
+
cost_of_node_replacement = torch.sum((y - leaf_value) ** 2).item()
|
|
143
|
+
|
|
144
|
+
subtree_cost_with_n_nodes_penalty = subtree_cost + self.ccp_alpha * total_subleaves
|
|
145
|
+
|
|
146
|
+
if cost_of_node_replacement + self.ccp_alpha * new_leaf_nodes < subtree_cost_with_n_nodes_penalty:
|
|
147
|
+
return Node(value=leaf_value), cost_of_node_replacement, 1
|
|
148
|
+
return node, subtree_cost, total_subleaves
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from ._XGBoostTree import _XGBoostTree
|
|
4
|
+
from ....DeepLearning.Layers.Activations import Sigmoid, SoftMax
|
|
5
|
+
from ....DeepLearning.Losses import BCE, CCE, Exponential
|
|
6
|
+
from ....Data.Preprocessing import OneHotEncoder
|
|
7
|
+
from ....Exceptions import NotFittedError
|
|
8
|
+
from ....Data.Metrics import calculate_metrics, prob_to_pred
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class XGBoostingClassifier:
|
|
12
|
+
"""
|
|
13
|
+
XGBoostingClassifier implements a classification algorithm fitting many consecutive trees to gradients and hessians of the predictions.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
n_trees (int, optional): The number of trees used for predicting. Defaults to 10. Must be a positive integer.
|
|
17
|
+
learning_rate (float, optional): The number multiplied to each additional trees residuals. Must be a real number in range (0, 1). Defaults to 0.5.
|
|
18
|
+
max_depth (int, optional): The maximum depth of the tree. Defaults to 25. Must be a positive integer.
|
|
19
|
+
min_samples_split (int, optional): The minimum required samples in a leaf to make a split. Defaults to 2. Must be a positive integer.
|
|
20
|
+
reg_lambda (float | int, optional): The regularisation parameter used in fitting the trees. The larger the parameter, the smaller the trees. Must be a positive real number. Defaults to 1.
|
|
21
|
+
gamma (float | int, optional): The minimum gain to make a split. Must be a non-negative real number. Defaults to 0.
|
|
22
|
+
loss (string, optional): The loss function used in calculations of the residuals. Must be one of "log_loss" or "exponential". Defaults to "log_loss". "exponential" can only be used for binary classification.
|
|
23
|
+
Attributes:
|
|
24
|
+
n_features (int): The number of features. Available after fitting.
|
|
25
|
+
n_classes (int): The number of classes. 2 for binary classification. Available after fitting.
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, n_trees=10, learning_rate=0.5, max_depth=25, min_samples_split=2, reg_lambda=1, gamma=0, loss="log_loss"):
|
|
28
|
+
if not isinstance(n_trees, int) or n_trees < 1:
|
|
29
|
+
raise ValueError("n_trees must be a positive integer.")
|
|
30
|
+
if not isinstance(learning_rate, float) or learning_rate <= 0 or learning_rate >= 1:
|
|
31
|
+
raise ValueError("learning_rate must be a float in range (0, 1).")
|
|
32
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
33
|
+
raise ValueError("max_depth must be a positive integer.")
|
|
34
|
+
if not isinstance(min_samples_split, int) or min_samples_split < 1:
|
|
35
|
+
raise ValueError("min_samples_split must be a positive integer.")
|
|
36
|
+
if not isinstance(reg_lambda, int | float) or reg_lambda <= 0:
|
|
37
|
+
raise ValueError("reg_lambda must be a positive real number.")
|
|
38
|
+
if not isinstance(gamma, int | float) or gamma < 0:
|
|
39
|
+
raise ValueError("gamma must be a non-negative real number.")
|
|
40
|
+
if loss not in ["log_loss", "exponential"]:
|
|
41
|
+
raise ValueError('loss must be one of ["log_loss", "exponential"]')
|
|
42
|
+
|
|
43
|
+
self.n_trees = n_trees
|
|
44
|
+
self.learning_rate = learning_rate
|
|
45
|
+
self.max_depth = max_depth
|
|
46
|
+
self.min_samples_split = min_samples_split
|
|
47
|
+
self.reg_lambda = reg_lambda
|
|
48
|
+
self.gamma = gamma
|
|
49
|
+
self.trees = None
|
|
50
|
+
self.loss_ = loss
|
|
51
|
+
|
|
52
|
+
def _get_activation_and_loss(self, classes):
|
|
53
|
+
self.n_classes = len(classes)
|
|
54
|
+
if self.loss_ == "log_loss":
|
|
55
|
+
if self.n_classes == 2:
|
|
56
|
+
self.loss = BCE(reduction="sum")
|
|
57
|
+
self.activation = Sigmoid()
|
|
58
|
+
else:
|
|
59
|
+
self.loss = CCE(reduction="sum")
|
|
60
|
+
self.activation = SoftMax()
|
|
61
|
+
elif self.loss_ == "exponential":
|
|
62
|
+
if self.n_classes != 2:
|
|
63
|
+
raise ValueError("The exponential loss is only applicable in binary classification. Use log_loss for multiclass classification instead.")
|
|
64
|
+
self.loss = Exponential(reduction="sum")
|
|
65
|
+
self.activation = Sigmoid()
|
|
66
|
+
|
|
67
|
+
def fit(self, X, y, metrics=["loss"]):
|
|
68
|
+
"""
|
|
69
|
+
Fits the XGBoostingClassifier model to the input data by fitting trees to the errors made by previous trees.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
|
|
73
|
+
y (torch.Tensor of shape (n_samples,)): The labels corresponding to each sample. Every element must be in [0, ..., n_classes - 1].
|
|
74
|
+
metrics (dict[str, torch.Tensor]): Contains the metrics that will be calculated between fitting each tree and returned. Only available for binary classification.
|
|
75
|
+
Returns:
|
|
76
|
+
metrics if binary classification else None
|
|
77
|
+
Raises:
|
|
78
|
+
TypeError: If the input matrix or the label vector is not a PyTorch tensor or if the problem is binary and metrics is not a list or a tuple.
|
|
79
|
+
ValueError: If the input matrix or the label vector is not the correct shape or the label vector contains wrong values.
|
|
80
|
+
"""
|
|
81
|
+
if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
|
|
82
|
+
raise TypeError("The input matrix and the label matrix must be a PyTorch tensor.")
|
|
83
|
+
if X.ndim != 2:
|
|
84
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor.")
|
|
85
|
+
if y.ndim != 1 or y.shape[0] != X.shape[0]:
|
|
86
|
+
raise ValueError("The labels must be 1 dimensional with the same number of samples as the input data")
|
|
87
|
+
vals = torch.unique(y).numpy()
|
|
88
|
+
if set(vals) != {*range(len(vals))}:
|
|
89
|
+
raise ValueError("y must only contain the values in [0, ..., n_classes - 1].")
|
|
90
|
+
|
|
91
|
+
self._get_activation_and_loss(torch.unique(y))
|
|
92
|
+
y = y.to(X.dtype)
|
|
93
|
+
self.n_features = X.shape[1]
|
|
94
|
+
|
|
95
|
+
if self.n_classes == 2:
|
|
96
|
+
if not isinstance(metrics, list | tuple):
|
|
97
|
+
raise ValueError("metrics must be a list or tuple containing the shorthand names of each wanted metric.")
|
|
98
|
+
|
|
99
|
+
return self._binary_fit(X, y, metrics=metrics)
|
|
100
|
+
else:
|
|
101
|
+
self._multi_fit(X, y)
|
|
102
|
+
|
|
103
|
+
def _binary_hessian_diag(self, prob, true_output):
|
|
104
|
+
loss_gradient = self.loss.gradient(prob, true_output)
|
|
105
|
+
loss_hessian = self.loss.hessian(prob, true_output)
|
|
106
|
+
sigmoid_value = prob
|
|
107
|
+
sigmoid_gradient = self.activation.backward(loss_gradient)
|
|
108
|
+
|
|
109
|
+
first_term = sigmoid_gradient * (1 - sigmoid_value) * loss_gradient
|
|
110
|
+
second_term = -sigmoid_value * sigmoid_gradient * loss_gradient
|
|
111
|
+
third_term = sigmoid_value * (1 - sigmoid_value) * loss_hessian # is the same as self.activation.backward(loss_hessian)
|
|
112
|
+
return first_term + second_term + third_term
|
|
113
|
+
|
|
114
|
+
def _multi_hessian_diag(self, prob, true_output):
|
|
115
|
+
loss_gradient = self.loss.gradient(prob, true_output)
|
|
116
|
+
loss_hessian = self.loss.hessian(prob, true_output)
|
|
117
|
+
softmax_value = prob
|
|
118
|
+
softmax_gradient = self.activation.backward(loss_gradient)
|
|
119
|
+
|
|
120
|
+
softmax_hessian = softmax_value * (1 - softmax_value) * (1 - 2 * softmax_value)
|
|
121
|
+
|
|
122
|
+
# From Mathematica: D[f[g[x]], {x, 2}] = Derivative[1][g][x]^2 (f^\[Prime]\[Prime])[g[x]] + Derivative[1][f][g[x]] (g^\[Prime]\[Prime])[x]
|
|
123
|
+
return softmax_gradient ** 2 * loss_hessian + loss_gradient * softmax_hessian
|
|
124
|
+
|
|
125
|
+
def _binary_fit(self, X, y, metrics=["loss"]):
|
|
126
|
+
positive_ratio = y.mean()
|
|
127
|
+
self.initial_log_odds = torch.log(positive_ratio / (1 - positive_ratio))
|
|
128
|
+
pred = torch.full(y.shape, self.initial_log_odds)
|
|
129
|
+
trees = []
|
|
130
|
+
history = {metric: torch.zeros(self.n_trees) for metric in metrics}
|
|
131
|
+
|
|
132
|
+
for i in range(self.n_trees):
|
|
133
|
+
prob = self.activation.forward(pred)
|
|
134
|
+
gradient = self.activation.backward(self.loss.gradient(prob, y))
|
|
135
|
+
hessian = self._binary_hessian_diag(prob, y)
|
|
136
|
+
|
|
137
|
+
tree = _XGBoostTree(max_depth=self.max_depth, min_samples_split=self.min_samples_split, reg_lambda=self.reg_lambda, gamma=self.gamma)
|
|
138
|
+
tree.fit(X, gradient, hessian)
|
|
139
|
+
prediction = tree.predict(X)
|
|
140
|
+
pred += self.learning_rate * prediction
|
|
141
|
+
|
|
142
|
+
trees.append(tree)
|
|
143
|
+
|
|
144
|
+
values = calculate_metrics(data=(self.activation.forward(pred), y), metrics=metrics, loss=self.loss.loss)
|
|
145
|
+
for metric, value in values.items():
|
|
146
|
+
history[metric][i] = value
|
|
147
|
+
|
|
148
|
+
self.trees = trees
|
|
149
|
+
return history
|
|
150
|
+
|
|
151
|
+
def _multi_fit(self, X, y):
|
|
152
|
+
encoder = OneHotEncoder()
|
|
153
|
+
y = encoder.fit_encode(y)
|
|
154
|
+
|
|
155
|
+
self.initial_log_odds = 0.0
|
|
156
|
+
pred = torch.full(y.shape, self.initial_log_odds)
|
|
157
|
+
trees = []
|
|
158
|
+
|
|
159
|
+
for class_index in range(self.n_classes):
|
|
160
|
+
class_trees = []
|
|
161
|
+
for _ in range(self.n_trees):
|
|
162
|
+
prob = self.activation.forward(pred)
|
|
163
|
+
gradient = self.activation.backward(self.loss.gradient(prob, y))[:, class_index]
|
|
164
|
+
hessian = self._multi_hessian_diag(prob, y)[:, class_index]
|
|
165
|
+
|
|
166
|
+
tree = _XGBoostTree(max_depth=self.max_depth, min_samples_split=self.min_samples_split, reg_lambda=self.reg_lambda, gamma=self.gamma)
|
|
167
|
+
tree.fit(X, gradient, hessian)
|
|
168
|
+
prediction = tree.predict(X)
|
|
169
|
+
pred[:, class_index] += self.learning_rate * prediction
|
|
170
|
+
|
|
171
|
+
class_trees.append(tree)
|
|
172
|
+
trees.append(class_trees)
|
|
173
|
+
|
|
174
|
+
self.trees = trees
|
|
175
|
+
|
|
176
|
+
def predict_proba(self, X):
|
|
177
|
+
"""
|
|
178
|
+
Applies the fitted XGBoostingClassifier model to the input data, predicting the probabilities of each class.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
|
|
182
|
+
Returns:
|
|
183
|
+
probabilities (torch.Tensor of shape (n_samples, n_classes) or for binary classification (n_samples,)): The predicted probabilities corresponding to each sample.
|
|
184
|
+
Raises:
|
|
185
|
+
NotFittedError: If the XGBoostingClassifier model has not been fitted before predicting.
|
|
186
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
187
|
+
ValueError: If the input matrix is not the correct shape.
|
|
188
|
+
"""
|
|
189
|
+
if not hasattr(self, "initial_log_odds"):
|
|
190
|
+
raise NotFittedError("XGBoostingClassifier.fit() must be called before predicting.")
|
|
191
|
+
if not isinstance(X, torch.Tensor):
|
|
192
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
193
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
194
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
195
|
+
|
|
196
|
+
if self.n_classes > 2:
|
|
197
|
+
return self._multi_predict_proba(X)
|
|
198
|
+
|
|
199
|
+
pred = torch.full((X.shape[0],), self.initial_log_odds)
|
|
200
|
+
|
|
201
|
+
for tree in self.trees:
|
|
202
|
+
prediction = tree.predict(X)
|
|
203
|
+
pred += self.learning_rate * prediction
|
|
204
|
+
|
|
205
|
+
return self.activation.forward(pred)
|
|
206
|
+
|
|
207
|
+
def predict(self, X):
|
|
208
|
+
"""
|
|
209
|
+
Applies the fitted XGBoostingClassifier model to the input data, predicting the correct classes.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
|
|
213
|
+
Returns:
|
|
214
|
+
labels (torch.Tensor of shape (n_samples,)): The predicted labels corresponding to each sample.
|
|
215
|
+
Raises:
|
|
216
|
+
NotFittedError: If the XGBoostingClassifier model has not been fitted before predicting.
|
|
217
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
218
|
+
ValueError: If the input matrix is not the correct shape.
|
|
219
|
+
"""
|
|
220
|
+
if not hasattr(self, "initial_log_odds"):
|
|
221
|
+
raise NotFittedError("XGBoostingClassifier.fit() must be called before predicting.")
|
|
222
|
+
if not isinstance(X, torch.Tensor):
|
|
223
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
224
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
225
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
226
|
+
|
|
227
|
+
prob = self.predict_proba(X)
|
|
228
|
+
return prob_to_pred(prob)
|
|
229
|
+
|
|
230
|
+
def _multi_predict_proba(self, X):
|
|
231
|
+
pred = torch.full((X.shape[0], self.n_classes), self.initial_log_odds)
|
|
232
|
+
|
|
233
|
+
for i in range(self.n_classes):
|
|
234
|
+
class_trees = self.trees[i]
|
|
235
|
+
for tree in class_trees:
|
|
236
|
+
pred[:, i] += self.learning_rate * tree.predict(X)
|
|
237
|
+
|
|
238
|
+
return self.activation.forward(pred)
|
|
239
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from ._XGBoostTree import _XGBoostTree
|
|
4
|
+
from ....DeepLearning.Losses import MSE, MAE, Huber
|
|
5
|
+
from ....Exceptions import NotFittedError
|
|
6
|
+
from ....Data.Metrics import calculate_metrics
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class XGBoostingRegressor:
|
|
10
|
+
"""
|
|
11
|
+
XGBoostingRegressor implements a regression algorithm fitting many consecutive trees to gradients and hessians of the loss function.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
n_trees (int, optional): The number of trees used for predicting. Defaults to 50. Must be a positive integer.
|
|
15
|
+
learning_rate (float, optional): The number multiplied to each additional trees residuals. Must be a real number in range (0, 1). Defaults to 0.5.
|
|
16
|
+
max_depth (int, optional): The maximum depth of the tree. Defaults to 3. Must be a positive integer.
|
|
17
|
+
min_samples_split (int, optional): The minimum required samples in a leaf to make a split. Defaults to 2. Must be a positive integer.
|
|
18
|
+
reg_lambda (float | int, optional): The regularisation parameter used in fitting the trees. The larger the parameter, the smaller the trees. Must be a positive real number. Defaults to 1.
|
|
19
|
+
gamma (float | int, optional): The minimum gain to make a split. Must be a non-negative real number. Defaults to 0.
|
|
20
|
+
loss (string, optional): The loss function used in calculations of the gradients and hessians. Must be one of "squared", "absolute" or "huber". Defaults to "squared".
|
|
21
|
+
huber_delta (float | int, optional): The delta parameter for the possibly used huber loss. If loss is not "huber", this parameter is ignored.
|
|
22
|
+
Attributes:
|
|
23
|
+
n_features (int): The number of features. Available after fitting.
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self, n_trees=50, learning_rate=0.5, max_depth=3, min_samples_split=2, reg_lambda=1, gamma=0, loss="squared", huber_delta=1):
|
|
26
|
+
if not isinstance(n_trees, int) or n_trees < 1:
|
|
27
|
+
raise ValueError("n_trees must be a positive integer.")
|
|
28
|
+
if not isinstance(learning_rate, float) or learning_rate <= 0 or learning_rate >= 1:
|
|
29
|
+
raise ValueError("learning_rate must be a float in range (0, 1).")
|
|
30
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
31
|
+
raise ValueError("max_depth must be a positive integer.")
|
|
32
|
+
if not isinstance(min_samples_split, int) or min_samples_split < 1:
|
|
33
|
+
raise ValueError("min_samples_split must be a positive integer.")
|
|
34
|
+
if not isinstance(reg_lambda, int | float) or reg_lambda <= 0:
|
|
35
|
+
raise ValueError("reg_lambda must be a positive real number.")
|
|
36
|
+
if not isinstance(gamma, int | float) or gamma < 0:
|
|
37
|
+
raise ValueError("gamma must be a non-negative real number.")
|
|
38
|
+
if loss not in ["squared", "absolute", "huber"]:
|
|
39
|
+
raise ValueError('loss must be one in ["squared", "absolute", "huber"].')
|
|
40
|
+
if not isinstance(huber_delta, int | float) or huber_delta <= 0:
|
|
41
|
+
raise ValueError("huber_delta must be a positive real number.")
|
|
42
|
+
|
|
43
|
+
self.n_trees = n_trees
|
|
44
|
+
self.learning_rate = learning_rate
|
|
45
|
+
self.max_depth = max_depth
|
|
46
|
+
self.min_samples_split = min_samples_split
|
|
47
|
+
self.reg_lambda = reg_lambda
|
|
48
|
+
self.gamma = gamma
|
|
49
|
+
self.trees = None
|
|
50
|
+
self.loss = self._choose_loss(loss, huber_delta)
|
|
51
|
+
|
|
52
|
+
def _choose_loss(self, loss, huber_delta):
|
|
53
|
+
if loss == "squared":
|
|
54
|
+
return MSE(reduction="sum")
|
|
55
|
+
elif loss == "absolute":
|
|
56
|
+
return MAE(reduction="sum")
|
|
57
|
+
elif loss == "huber":
|
|
58
|
+
return Huber(reduction="sum", delta=huber_delta)
|
|
59
|
+
|
|
60
|
+
def fit(self, X, y, metrics=["loss"]):
|
|
61
|
+
"""
|
|
62
|
+
Fits the XGBoostingRegressor model to the input data by fitting trees to the errors made by previous trees.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
|
|
66
|
+
y (torch.Tensor of shape (n_samples,)): The target values corresponding to each sample.
|
|
67
|
+
metrics (dict[str, torch.Tensor]): Contains the metrics that will be calculated between fitting each tree and returned.
|
|
68
|
+
Returns:
|
|
69
|
+
metrics (dict[str, torch.Tensor]): The calculated metrics.
|
|
70
|
+
Raises:
|
|
71
|
+
TypeError: If the input matrix or the target vector is not a PyTorch tensor.
|
|
72
|
+
ValueError: If the input matrix or the target vector is not the correct shape.
|
|
73
|
+
"""
|
|
74
|
+
if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
|
|
75
|
+
raise TypeError("The input matrix and the target matrix must be a PyTorch tensor.")
|
|
76
|
+
if X.ndim != 2:
|
|
77
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor.")
|
|
78
|
+
if y.ndim != 1 or y.shape[0] != X.shape[0]:
|
|
79
|
+
raise ValueError("The target must be 1 dimensional with the same number of samples as the input data")
|
|
80
|
+
if not isinstance(metrics, list | tuple):
|
|
81
|
+
raise ValueError("metrics must be a list or tuple containing the shorthand names of each wanted metric.")
|
|
82
|
+
|
|
83
|
+
self.n_features = X.shape[1]
|
|
84
|
+
self.initial_pred = y.mean()
|
|
85
|
+
pred = torch.full(y.shape, self.initial_pred)
|
|
86
|
+
trees = []
|
|
87
|
+
history = {metric: torch.zeros(self.n_trees) for metric in metrics}
|
|
88
|
+
|
|
89
|
+
for i in range(self.n_trees):
|
|
90
|
+
gradient = self.loss.gradient(pred, y)
|
|
91
|
+
hessian = self.loss.hessian(pred, y)
|
|
92
|
+
|
|
93
|
+
tree = _XGBoostTree(max_depth=self.max_depth, min_samples_split=self.min_samples_split, reg_lambda=self.reg_lambda, gamma=self.gamma)
|
|
94
|
+
tree.fit(X, gradient, hessian)
|
|
95
|
+
prediction = tree.predict(X)
|
|
96
|
+
|
|
97
|
+
pred += self.learning_rate * prediction
|
|
98
|
+
trees.append(tree)
|
|
99
|
+
|
|
100
|
+
values = calculate_metrics(data=(pred, y), metrics=metrics, loss=self.loss.loss)
|
|
101
|
+
for metric, value in values.items():
|
|
102
|
+
history[metric][i] = value
|
|
103
|
+
|
|
104
|
+
self.trees = trees
|
|
105
|
+
return history
|
|
106
|
+
|
|
107
|
+
def predict(self, X):
|
|
108
|
+
"""
|
|
109
|
+
Applies the fitted XGBoostingRegressor model to the input data, predicting the target values.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
|
|
113
|
+
Returns:
|
|
114
|
+
targets (torch.Tensor of shape (n_samples,)): The predicted target values corresponding to each sample.
|
|
115
|
+
Raises:
|
|
116
|
+
NotFittedError: If the XGBoostingRegressor model has not been fitted before predicting.
|
|
117
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
118
|
+
ValueError: If the input matrix is not the correct shape.
|
|
119
|
+
"""
|
|
120
|
+
if not hasattr(self, "initial_pred"):
|
|
121
|
+
raise NotFittedError("XGBoostingRegressor.fit() must be called before predicting.")
|
|
122
|
+
if not isinstance(X, torch.Tensor):
|
|
123
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
124
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
125
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
126
|
+
|
|
127
|
+
pred = torch.full((X.shape[0],), self.initial_pred)
|
|
128
|
+
|
|
129
|
+
for tree in self.trees:
|
|
130
|
+
pred += self.learning_rate * tree.predict(X)
|
|
131
|
+
return pred
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from ._DecisionTree import Node
|
|
4
|
+
from ....Exceptions import NotFittedError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _XGBoostTree:
|
|
8
|
+
def __init__(self, max_depth=25, min_samples_split=2, reg_lambda=1, gamma=1):
|
|
9
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
10
|
+
raise ValueError("max_depth must be a positive integer.")
|
|
11
|
+
if not isinstance(min_samples_split, int) or min_samples_split < 1:
|
|
12
|
+
raise ValueError("min_samples_split must be a positive integer.")
|
|
13
|
+
if not isinstance(reg_lambda, int | float) or reg_lambda <= 0:
|
|
14
|
+
raise ValueError("reg_lambda must be a positive real number.")
|
|
15
|
+
if not isinstance(gamma, int | float) or gamma < 0:
|
|
16
|
+
raise ValueError("gamma must be a non-negative real number.")
|
|
17
|
+
|
|
18
|
+
self.max_depth = max_depth
|
|
19
|
+
self.min_samples_split = min_samples_split
|
|
20
|
+
self.reg_lambda = reg_lambda
|
|
21
|
+
self.gamma = gamma
|
|
22
|
+
self.root = None
|
|
23
|
+
|
|
24
|
+
def _value(self, gradient, hessian):
|
|
25
|
+
return -gradient.sum() / (hessian.sum() + self.reg_lambda)
|
|
26
|
+
|
|
27
|
+
def fit(self, X, gradient, hessian):
|
|
28
|
+
if not isinstance(X, torch.Tensor) or not isinstance(gradient, torch.Tensor):
|
|
29
|
+
raise TypeError("The input matrix and the target matrix must be a PyTorch tensor.")
|
|
30
|
+
if X.ndim != 2:
|
|
31
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor.")
|
|
32
|
+
if gradient.ndim != 1 or gradient.shape[0] != X.shape[0]:
|
|
33
|
+
raise ValueError("The gradient must be 1 dimensional with the same number of samples as the input data")
|
|
34
|
+
if hessian.ndim != 1 or hessian.shape[0] != X.shape[0]:
|
|
35
|
+
raise ValueError("The hessian must be 1 dimensional with the same number of samples as the input data")
|
|
36
|
+
|
|
37
|
+
self.n_features = X.shape[1]
|
|
38
|
+
self.root = self._grow_tree(X, gradient, hessian, 0)
|
|
39
|
+
|
|
40
|
+
def _grow_tree(self, X, gradient, hessian, depth):
|
|
41
|
+
n_samples, n_features = X.size()
|
|
42
|
+
vals = torch.unique(gradient)
|
|
43
|
+
|
|
44
|
+
if depth >= self.max_depth or len(vals) == 1 or n_samples < self.min_samples_split:
|
|
45
|
+
return Node(value=self._value(gradient, hessian))
|
|
46
|
+
|
|
47
|
+
feature_indicies = torch.randperm(n_features)[:self.n_features]
|
|
48
|
+
split_threshold, split_index = self._best_split(X, gradient, hessian, feature_indicies)
|
|
49
|
+
# if no split gains more information
|
|
50
|
+
if split_threshold is None:
|
|
51
|
+
return Node(value=self._value(gradient, hessian))
|
|
52
|
+
|
|
53
|
+
left_indicies, right_indicies = self._split(X[:, split_index], split_threshold)
|
|
54
|
+
left = self._grow_tree(X[left_indicies], gradient[left_indicies], hessian[left_indicies], depth + 1)
|
|
55
|
+
right = self._grow_tree(X[right_indicies], gradient[right_indicies], hessian[right_indicies], depth + 1)
|
|
56
|
+
return Node(left, right, split_threshold, split_index)
|
|
57
|
+
|
|
58
|
+
def _best_split(self, X, gradient, hessian, feature_indicies):
|
|
59
|
+
max_gain = -1
|
|
60
|
+
split_index = None
|
|
61
|
+
split_threshold = None
|
|
62
|
+
|
|
63
|
+
for index in feature_indicies:
|
|
64
|
+
feature_values = X[:, index]
|
|
65
|
+
possible_thresholds = torch.unique(feature_values)
|
|
66
|
+
for threshold in possible_thresholds:
|
|
67
|
+
gain = self._gain(gradient, hessian, feature_values, threshold)
|
|
68
|
+
if gain > max_gain:
|
|
69
|
+
max_gain = gain
|
|
70
|
+
split_index = index
|
|
71
|
+
split_threshold = threshold
|
|
72
|
+
if max_gain <= 0: return None, None
|
|
73
|
+
return split_threshold, split_index
|
|
74
|
+
|
|
75
|
+
def _split(self, feature_values, threshold):
|
|
76
|
+
left_indicies = torch.argwhere(feature_values <= threshold).flatten()
|
|
77
|
+
right_indicies = torch.argwhere(feature_values > threshold).flatten()
|
|
78
|
+
return left_indicies, right_indicies
|
|
79
|
+
|
|
80
|
+
def _gain(self, gradient, hessian, feature_values, threshold):
|
|
81
|
+
left_indicies, right_indicies = self._split(feature_values, threshold)
|
|
82
|
+
if len(left_indicies) <= 1 or len(right_indicies) <= 1:
|
|
83
|
+
return 0
|
|
84
|
+
return self._gain_node(gradient, hessian) - self._gain_node(gradient[left_indicies], hessian[left_indicies]) - self._gain_node(gradient[right_indicies], hessian[right_indicies]) - self.gamma
|
|
85
|
+
|
|
86
|
+
def _gain_node(self, gradient, hessian):
|
|
87
|
+
return -(gradient.sum() ** 2) / (hessian.sum() + self.reg_lambda)
|
|
88
|
+
|
|
89
|
+
def predict(self, X):
|
|
90
|
+
if self.root is None:
|
|
91
|
+
raise NotFittedError("_XGBoostTree.fit() must be called before predicting.")
|
|
92
|
+
if not isinstance(X, torch.Tensor):
|
|
93
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
94
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
95
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
96
|
+
return torch.tensor([self._predict_single(datapoint, self.root) for datapoint in X])
|
|
97
|
+
|
|
98
|
+
def _predict_single(self, x, current_node):
|
|
99
|
+
if current_node.is_leaf():
|
|
100
|
+
return current_node.value
|
|
101
|
+
|
|
102
|
+
if x[current_node.feature_index] <= current_node.threshold:
|
|
103
|
+
return self._predict_single(x, current_node.left)
|
|
104
|
+
|
|
105
|
+
return self._predict_single(x, current_node.right)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from ._DecisionTree import DecisionTree
|
|
2
|
+
from ._RandomForest import RandomForestClassifier
|
|
3
|
+
from ._RegressionForest import RandomForestRegressor
|
|
4
|
+
from ._RegressionTree import RegressionTree
|
|
5
|
+
from ._GradientBoostingRegressor import GradientBoostingRegressor
|
|
6
|
+
from ._GradientBoostingClassifier import GradientBoostingClassifier
|
|
7
|
+
from ._AdaBoostClassifier import AdaBoostClassifier
|
|
8
|
+
from ._AdaBoostRegressor import AdaBoostRegressor
|
|
9
|
+
from ._XGBoostRegressor import XGBoostingRegressor
|
|
10
|
+
from ._XGBoostClassifier import XGBoostingClassifier
|
|
11
|
+
from ._LGBMRegressor import LGBMRegressor
|
|
12
|
+
from ._LGBMClassifier import LGBMClassifier
|
|
File without changes
|