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,168 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from ._LGBMTree import _LGBMTree, _ExclusiveFeatureBundling
|
|
4
|
+
from ....DeepLearning.Losses import MSE, MAE, Huber
|
|
5
|
+
from ....Exceptions import NotFittedError
|
|
6
|
+
from ....Data.Metrics import calculate_metrics
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LGBMRegressor:
|
|
10
|
+
"""
|
|
11
|
+
LGBMRegressor implements a regression algorithm fitting many consecutive trees to gradients and hessians of the loss function. The algorithm is based on `this paper <https://proceedings.neurips.cc/paper_files/paper/2017/file/6449f44a102fde848669bdd9eb6b76fa-Paper.pdf>`_.
|
|
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
|
+
n_bins (int, optional): The number of bins used to find the optimal split of data. Must be greater than 1. Defaults to 30.
|
|
19
|
+
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.
|
|
20
|
+
gamma (float | int, optional): The minimum gain to make a split. Must be a non-negative real number. Defaults to 0.
|
|
21
|
+
large_error_proportion (float, optional): The proportion of the whole data with the largest error, which is always used to train the next weak learner. Defaults to 0.3.
|
|
22
|
+
small_error_proportion (float, optional): The proportion of data randomly selected from the remaining (1 - large_error_proportion) percent of data to train the next weak learner. Defaults to 0.2.
|
|
23
|
+
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".
|
|
24
|
+
huber_delta (float | int, optional): The delta parameter for the possibly used huber loss. If loss is not "huber", this parameter is ignored.
|
|
25
|
+
max_conflict_rate (float, optional): The proportion of samples, which are allowed to be nonzero without featuers being bundled. Is ignored if use_efb=False. Defaults to 0.0.
|
|
26
|
+
use_efb (bool, optional): Determines if the exclusive feature bundling algorithm is used. Defaults to True.
|
|
27
|
+
Attributes:
|
|
28
|
+
n_features (int): The number of features. Available after fitting.
|
|
29
|
+
"""
|
|
30
|
+
def __init__(self, n_trees=50, learning_rate=0.5, max_depth=3, min_samples_split=2, n_bins=30, reg_lambda=1, gamma=0, large_error_proportion=0.3, small_error_proportion=0.2, loss="squared", huber_delta=1, max_conflict_rate=0.0, use_efb=True):
|
|
31
|
+
if not isinstance(n_trees, int) or n_trees < 1:
|
|
32
|
+
raise ValueError("n_trees must be a positive integer.")
|
|
33
|
+
if not isinstance(learning_rate, float) or learning_rate <= 0 or learning_rate >= 1:
|
|
34
|
+
raise ValueError("learning_rate must be a float in range (0, 1).")
|
|
35
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
36
|
+
raise ValueError("max_depth must be a positive integer.")
|
|
37
|
+
if not isinstance(min_samples_split, int) or min_samples_split < 1:
|
|
38
|
+
raise ValueError("min_samples_split must be a positive integer.")
|
|
39
|
+
if not isinstance(n_bins, int) or n_bins <= 1:
|
|
40
|
+
raise ValueError("n_bins must be an integer greater than 1.")
|
|
41
|
+
if not isinstance(reg_lambda, int | float) or reg_lambda <= 0:
|
|
42
|
+
raise ValueError("reg_lambda must be a positive real number.")
|
|
43
|
+
if not isinstance(gamma, int | float) or gamma < 0:
|
|
44
|
+
raise ValueError("gamma must be a non-negative real number.")
|
|
45
|
+
if not isinstance(large_error_proportion, float) or large_error_proportion <= 0 or large_error_proportion >= 1:
|
|
46
|
+
raise ValueError("large_error_proportion must be a float in range (0, 1).")
|
|
47
|
+
if not isinstance(small_error_proportion, float) or small_error_proportion <= 0 or small_error_proportion >= 1:
|
|
48
|
+
raise ValueError("small_error_proportion must be a float in range (0, 1).")
|
|
49
|
+
if large_error_proportion + small_error_proportion > 1:
|
|
50
|
+
raise ValueError("large_error_proportion and small_error_proportion must sum to a value less than 1.")
|
|
51
|
+
if loss not in ["squared", "absolute", "huber"]:
|
|
52
|
+
raise ValueError('loss must be one in ["squared", "absolute", "huber"].')
|
|
53
|
+
if not isinstance(huber_delta, int | float) or huber_delta <= 0:
|
|
54
|
+
raise ValueError("huber_delta must be a positive real number.")
|
|
55
|
+
if not isinstance(max_conflict_rate, float) or max_conflict_rate < 0 or max_conflict_rate >= 1:
|
|
56
|
+
raise ValueError("max_conflict_rate must be a float in range [0, 1).")
|
|
57
|
+
if not isinstance(use_efb, bool):
|
|
58
|
+
raise TypeError("use_efb must be a boolean.")
|
|
59
|
+
|
|
60
|
+
self.n_trees = n_trees
|
|
61
|
+
self.learning_rate = learning_rate
|
|
62
|
+
self.max_depth = max_depth
|
|
63
|
+
self.min_samples_split = min_samples_split
|
|
64
|
+
self.n_bins = n_bins
|
|
65
|
+
self.reg_lambda = reg_lambda
|
|
66
|
+
self.gamma = gamma
|
|
67
|
+
self.trees = None
|
|
68
|
+
self.large_error_proportion = large_error_proportion
|
|
69
|
+
self.small_error_proportion = small_error_proportion
|
|
70
|
+
self.loss = self._choose_loss(loss, huber_delta)
|
|
71
|
+
self.use_efb = use_efb
|
|
72
|
+
if self.use_efb: self.efb = _ExclusiveFeatureBundling(max_conflict_rate=max_conflict_rate, n_bins=n_bins)
|
|
73
|
+
|
|
74
|
+
def _choose_loss(self, loss, huber_delta):
|
|
75
|
+
if loss == "squared":
|
|
76
|
+
return MSE(reduction="sum")
|
|
77
|
+
elif loss == "absolute":
|
|
78
|
+
return MAE(reduction="sum")
|
|
79
|
+
elif loss == "huber":
|
|
80
|
+
return Huber(reduction="sum", delta=huber_delta)
|
|
81
|
+
|
|
82
|
+
def fit(self, X, y, metrics=["loss"]):
|
|
83
|
+
"""
|
|
84
|
+
Fits the LGBMRegressor model to the input data by fitting trees to the errors made by previous trees.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
|
|
88
|
+
y (torch.Tensor of shape (n_samples,)): The target values corresponding to each sample.
|
|
89
|
+
metrics (dict[str, torch.Tensor]): Contains the metrics that will be calculated between fitting each tree and returned.
|
|
90
|
+
Returns:
|
|
91
|
+
metrics (dict[str, torch.Tensor]): The calculated metrics.
|
|
92
|
+
Raises:
|
|
93
|
+
TypeError: If the input matrix or the target vector is not a PyTorch tensor.
|
|
94
|
+
ValueError: If the input matrix or the target vector is not the correct shape.
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
|
|
97
|
+
raise TypeError("The input matrix and the target matrix must be a PyTorch tensor.")
|
|
98
|
+
if X.ndim != 2:
|
|
99
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor.")
|
|
100
|
+
if y.ndim != 1 or y.shape[0] != X.shape[0]:
|
|
101
|
+
raise ValueError("The target must be 1 dimensional with the same number of samples as the input data")
|
|
102
|
+
if not isinstance(metrics, list | tuple):
|
|
103
|
+
raise ValueError("metrics must be a list or tuple containing the shorthand names of each wanted metric.")
|
|
104
|
+
self.n_features = X.shape[1]
|
|
105
|
+
|
|
106
|
+
if self.use_efb: X = self.efb.fit_transform(X)
|
|
107
|
+
|
|
108
|
+
self.initial_pred = y.mean()
|
|
109
|
+
pred = torch.full(y.shape, self.initial_pred)
|
|
110
|
+
trees = []
|
|
111
|
+
history = {metric: torch.zeros(self.n_trees) for metric in metrics}
|
|
112
|
+
|
|
113
|
+
top_n = int(len(y) * self.large_error_proportion)
|
|
114
|
+
rand_n = int(len(y) * self.small_error_proportion)
|
|
115
|
+
fact = (1 - self.large_error_proportion) / self.small_error_proportion
|
|
116
|
+
|
|
117
|
+
for i in range(self.n_trees):
|
|
118
|
+
gradient = self.loss.gradient(pred, y).to(X.dtype)
|
|
119
|
+
hessian = self.loss.hessian(pred, y).to(X.dtype)
|
|
120
|
+
|
|
121
|
+
# GOSS (gradient-based one-sided sampling)
|
|
122
|
+
indicies = torch.argsort(gradient, descending=True)
|
|
123
|
+
large_error_indicies = indicies[:top_n]
|
|
124
|
+
small_error_indicies = indicies[torch.randperm(len(indicies) - len(large_error_indicies))[:rand_n] + top_n]
|
|
125
|
+
train_indicies = torch.cat([large_error_indicies, small_error_indicies])
|
|
126
|
+
hessian[small_error_indicies] *= fact
|
|
127
|
+
|
|
128
|
+
tree = _LGBMTree(max_depth=self.max_depth, min_samples_split=self.min_samples_split, n_bins=self.n_bins, reg_lambda=self.reg_lambda, gamma=self.gamma)
|
|
129
|
+
tree.fit(X[train_indicies], gradient[train_indicies], hessian[train_indicies])
|
|
130
|
+
prediction = tree.predict(X)
|
|
131
|
+
pred += self.learning_rate * prediction
|
|
132
|
+
|
|
133
|
+
trees.append(tree)
|
|
134
|
+
|
|
135
|
+
values = calculate_metrics(data=(pred, y), metrics=metrics, loss=self.loss.loss)
|
|
136
|
+
for metric, value in values.items():
|
|
137
|
+
history[metric][i] = value
|
|
138
|
+
|
|
139
|
+
self.trees = trees
|
|
140
|
+
return history
|
|
141
|
+
|
|
142
|
+
def predict(self, X):
|
|
143
|
+
"""
|
|
144
|
+
Applies the fitted LGBMRegressor model to the input data, predicting the target values.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
|
|
148
|
+
Returns:
|
|
149
|
+
targets (torch.Tensor of shape (n_samples,)): The predicted target values corresponding to each sample.
|
|
150
|
+
Raises:
|
|
151
|
+
NotFittedError: If the LGBMRegressor model has not been fitted before predicting.
|
|
152
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
153
|
+
ValueError: If the input matrix is not the correct shape.
|
|
154
|
+
"""
|
|
155
|
+
if not hasattr(self, "initial_pred"):
|
|
156
|
+
raise NotFittedError("LGBMRegressor.fit() must be called before predicting.")
|
|
157
|
+
if not isinstance(X, torch.Tensor):
|
|
158
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
159
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
160
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
161
|
+
|
|
162
|
+
if self.use_efb: X = self.efb.transform(X)
|
|
163
|
+
|
|
164
|
+
pred = torch.full((X.shape[0],), self.initial_pred)
|
|
165
|
+
|
|
166
|
+
for tree in self.trees:
|
|
167
|
+
pred += self.learning_rate * tree.predict(X)
|
|
168
|
+
return pred
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import multiprocessing
|
|
3
|
+
|
|
4
|
+
from ._DecisionTree import Node
|
|
5
|
+
from ....Exceptions import NotFittedError
|
|
6
|
+
from ....Data.Preprocessing import OneHotEncoder
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _LGBMTree:
|
|
10
|
+
def __init__(self, max_depth=25, min_samples_split=2, n_bins=30, reg_lambda=1, gamma=1):
|
|
11
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
12
|
+
raise ValueError("max_depth must be a positive integer.")
|
|
13
|
+
if not isinstance(min_samples_split, int) or min_samples_split < 1:
|
|
14
|
+
raise ValueError("min_samples_split must be a positive integer.")
|
|
15
|
+
if not isinstance(reg_lambda, int | float) or reg_lambda <= 0:
|
|
16
|
+
raise ValueError("reg_lambda must be a positive real number.")
|
|
17
|
+
if not isinstance(n_bins, int) or n_bins <= 1:
|
|
18
|
+
raise ValueError("n_bins must be an integer greater than 1.")
|
|
19
|
+
if not isinstance(gamma, int | float) or gamma < 0:
|
|
20
|
+
raise ValueError("gamma must be a non-negative real number.")
|
|
21
|
+
|
|
22
|
+
self.max_depth = max_depth
|
|
23
|
+
self.min_samples_split = min_samples_split
|
|
24
|
+
self.reg_lambda = reg_lambda
|
|
25
|
+
self.gamma = gamma
|
|
26
|
+
self.n_bins = n_bins
|
|
27
|
+
self.root = None
|
|
28
|
+
|
|
29
|
+
def _value(self, gradient, hessian):
|
|
30
|
+
return -gradient.sum() / (hessian.sum() + self.reg_lambda)
|
|
31
|
+
|
|
32
|
+
def fit(self, X, gradient, hessian):
|
|
33
|
+
if not isinstance(X, torch.Tensor) or not isinstance(gradient, torch.Tensor):
|
|
34
|
+
raise TypeError("The input matrix and the target matrix must be a PyTorch tensor.")
|
|
35
|
+
if X.ndim != 2:
|
|
36
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor.")
|
|
37
|
+
if gradient.ndim != 1 or gradient.shape[0] != X.shape[0]:
|
|
38
|
+
raise ValueError("The gradient must be 1 dimensional with the same number of samples as the input data")
|
|
39
|
+
if hessian.ndim != 1 or hessian.shape[0] != X.shape[0]:
|
|
40
|
+
raise ValueError("The hessian must be 1 dimensional with the same number of samples as the input data")
|
|
41
|
+
|
|
42
|
+
self.n_features = X.shape[1]
|
|
43
|
+
self.root = self._grow_tree(X, gradient, hessian, 0)
|
|
44
|
+
|
|
45
|
+
def _grow_tree(self, X, gradient, hessian, depth):
|
|
46
|
+
n_samples, n_features = X.size()
|
|
47
|
+
vals = torch.unique(gradient)
|
|
48
|
+
|
|
49
|
+
if depth >= self.max_depth or len(vals) == 1 or n_samples < self.min_samples_split:
|
|
50
|
+
return Node(value=self._value(gradient, hessian))
|
|
51
|
+
|
|
52
|
+
feature_indicies = torch.randperm(n_features)[:self.n_features]
|
|
53
|
+
split_threshold, split_index = self._best_split(X, gradient, hessian, feature_indicies)
|
|
54
|
+
# if no split gains more information
|
|
55
|
+
if split_threshold is None:
|
|
56
|
+
return Node(value=self._value(gradient, hessian))
|
|
57
|
+
|
|
58
|
+
left_indicies, right_indicies = self._split(X[:, split_index], split_threshold)
|
|
59
|
+
left = self._grow_tree(X[left_indicies], gradient[left_indicies], hessian[left_indicies], depth + 1)
|
|
60
|
+
right = self._grow_tree(X[right_indicies], gradient[right_indicies], hessian[right_indicies], depth + 1)
|
|
61
|
+
return Node(left, right, split_threshold, split_index)
|
|
62
|
+
|
|
63
|
+
def _best_split(self, X, gradient, hessian, feature_indicies):
|
|
64
|
+
max_gain = -1
|
|
65
|
+
split_index = None
|
|
66
|
+
split_threshold = None
|
|
67
|
+
|
|
68
|
+
parent_gain = self._gain_node(gradient, hessian)
|
|
69
|
+
|
|
70
|
+
for index in feature_indicies:
|
|
71
|
+
feature_values = X[:, index]
|
|
72
|
+
possible_thresholds = torch.unique(feature_values)
|
|
73
|
+
if len(possible_thresholds) > self.n_bins: _, possible_thresholds = torch.histogram(feature_values, bins=self.n_bins, weight=gradient)
|
|
74
|
+
for threshold in possible_thresholds:
|
|
75
|
+
gain = parent_gain + self._children_gain(gradient, hessian, feature_values, threshold)
|
|
76
|
+
if gain > max_gain:
|
|
77
|
+
max_gain = gain
|
|
78
|
+
split_index = index
|
|
79
|
+
split_threshold = threshold
|
|
80
|
+
if max_gain <= 0: return None, None
|
|
81
|
+
return split_threshold, split_index
|
|
82
|
+
|
|
83
|
+
def _split(self, feature_values, threshold):
|
|
84
|
+
left_indicies = torch.argwhere(feature_values <= threshold).flatten()
|
|
85
|
+
right_indicies = torch.argwhere(feature_values > threshold).flatten()
|
|
86
|
+
return left_indicies, right_indicies
|
|
87
|
+
|
|
88
|
+
def _children_gain(self, gradient, hessian, feature_values, threshold):
|
|
89
|
+
left_indicies, right_indicies = self._split(feature_values, threshold)
|
|
90
|
+
if len(left_indicies) <= 1 or len(right_indicies) <= 1:
|
|
91
|
+
return 0
|
|
92
|
+
return -self._gain_node(gradient[left_indicies], hessian[left_indicies]) - self._gain_node(gradient[right_indicies], hessian[right_indicies]) - self.gamma
|
|
93
|
+
|
|
94
|
+
def _gain_node(self, gradient, hessian):
|
|
95
|
+
return -(gradient.sum() ** 2) / (hessian.sum() + self.reg_lambda)
|
|
96
|
+
|
|
97
|
+
def predict(self, X):
|
|
98
|
+
if self.root is None:
|
|
99
|
+
raise NotFittedError("_XGBoostTree.fit() must be called before predicting.")
|
|
100
|
+
if not isinstance(X, torch.Tensor):
|
|
101
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
102
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
103
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
104
|
+
return torch.tensor([self._predict_single(datapoint, self.root) for datapoint in X])
|
|
105
|
+
|
|
106
|
+
def _predict_single(self, x, current_node):
|
|
107
|
+
if current_node.is_leaf():
|
|
108
|
+
return current_node.value
|
|
109
|
+
|
|
110
|
+
if x[current_node.feature_index] <= current_node.threshold:
|
|
111
|
+
return self._predict_single(x, current_node.left)
|
|
112
|
+
|
|
113
|
+
return self._predict_single(x, current_node.right)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# _merge_features and _greedy_bundling functions based on https://github.com/meanxai/machine_learning/blob/main/12.LGBM/5.efb_onehot.py
|
|
117
|
+
class _ExclusiveFeatureBundling:
|
|
118
|
+
def __init__(self, max_conflict_rate=0.0, n_bins=30):
|
|
119
|
+
self.max_conflict_rate = max_conflict_rate
|
|
120
|
+
self.n_bins = n_bins
|
|
121
|
+
self.bundled_features = []
|
|
122
|
+
self.mapping = {}
|
|
123
|
+
self.non_sparse_features = None
|
|
124
|
+
|
|
125
|
+
def fit(self, X):
|
|
126
|
+
self.mins = torch.min(X, dim=0).values
|
|
127
|
+
self.maxs = torch.max(X, dim=0).values
|
|
128
|
+
X = self._discretize(X)
|
|
129
|
+
|
|
130
|
+
self.bundles = self._greedy_bundling(X)
|
|
131
|
+
result = self._merge_bundles(X, self.bundles)
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
def _merge_bundles(self, X, bundles):
|
|
135
|
+
result = torch.zeros(len(X), len(bundles))
|
|
136
|
+
for i, bundle in enumerate(bundles):
|
|
137
|
+
combined_features = torch.stack([X[:, i] for i in bundle])
|
|
138
|
+
new_feature, _ = self._merge_features(combined_features)
|
|
139
|
+
result[:, i] = new_feature - 1
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
def _merge_features(self, combined_features):
|
|
143
|
+
bin_ranges = [0]
|
|
144
|
+
total_bin = 0
|
|
145
|
+
for feature in combined_features:
|
|
146
|
+
total_bin += torch.max(feature)
|
|
147
|
+
bin_ranges.append(total_bin)
|
|
148
|
+
|
|
149
|
+
new_feature = combined_features[0]
|
|
150
|
+
for i in range(len(combined_features)):
|
|
151
|
+
for j in range(1, len(combined_features)):
|
|
152
|
+
if combined_features[j][i] != 0:
|
|
153
|
+
new_feature[i] = combined_features[j][i] + bin_ranges[j]
|
|
154
|
+
return new_feature, bin_ranges
|
|
155
|
+
|
|
156
|
+
def _greedy_bundling(self, X):
|
|
157
|
+
n_features = X.shape[1]
|
|
158
|
+
conflictCnt = torch.zeros((n_features, n_features))
|
|
159
|
+
|
|
160
|
+
for i in range(n_features):
|
|
161
|
+
for j in range(i+1, n_features):
|
|
162
|
+
n_conflicts = torch.sum(X[:, i] * X[:, j] > 0)
|
|
163
|
+
conflictCnt[i, j] = n_conflicts
|
|
164
|
+
conflictCnt[j, i] = n_conflicts
|
|
165
|
+
|
|
166
|
+
degree = conflictCnt.sum(axis=0)
|
|
167
|
+
searchOrder = torch.argsort(degree, descending=True)
|
|
168
|
+
|
|
169
|
+
bundles = []
|
|
170
|
+
bundlesConflict = []
|
|
171
|
+
for i in searchOrder:
|
|
172
|
+
needNew = True
|
|
173
|
+
for j in range(len(bundles)):
|
|
174
|
+
cnt = conflictCnt[bundles[j][-1], i]
|
|
175
|
+
if cnt + bundlesConflict[j] <= self.max_conflict_rate * len(X): # K from the paper but different parametrisation
|
|
176
|
+
bundles[j].append(i)
|
|
177
|
+
bundlesConflict[j] += cnt
|
|
178
|
+
needNew = False
|
|
179
|
+
break
|
|
180
|
+
if needNew:
|
|
181
|
+
bundles.append([i])
|
|
182
|
+
bundlesConflict.append(0)
|
|
183
|
+
return bundles
|
|
184
|
+
|
|
185
|
+
def fit_transform(self, X):
|
|
186
|
+
self.fit(X)
|
|
187
|
+
return self.transform(X)
|
|
188
|
+
|
|
189
|
+
def transform(self, X):
|
|
190
|
+
X = self._discretize(X)
|
|
191
|
+
return self._merge_bundles(X, self.bundles)
|
|
192
|
+
|
|
193
|
+
def _discretize(self, X):
|
|
194
|
+
X_discrete = torch.zeros_like(X)
|
|
195
|
+
for i in range(X.shape[1]):
|
|
196
|
+
bins = torch.linspace(self.mins[i], self.maxs[i], self.n_bins)
|
|
197
|
+
descrete = torch.bucketize(X[:, i].contiguous(), bins)
|
|
198
|
+
X_discrete[:, i] = torch.clamp(descrete, 0, self.n_bins - 1) # handle the possible values outside the bounds during transformation phase
|
|
199
|
+
return X_discrete
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from collections import Counter
|
|
3
|
+
|
|
4
|
+
from ._DecisionTree import DecisionTree
|
|
5
|
+
from ....Exceptions import NotFittedError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RandomForestClassifier:
|
|
9
|
+
"""
|
|
10
|
+
RandomForestClassifier implements a classification algorithm fitting many :class:`DecisionTrees <DLL.MachineLearning.SupervisedLearning.Trees.DecisionTree>` to bootstrapped data.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
n_trees (int, optional): The number of trees used for predicting. Defaults to 10. Must be a positive integer.
|
|
14
|
+
max_depth (int, optional): The maximum depth of the tree. Defaults to 10. Must be a positive integer.
|
|
15
|
+
min_samples_split (int, optional): The minimum required samples in a leaf to make a split. Defaults to 2. Must be a positive integer.
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self, n_trees=10, max_depth=10, min_samples_split=2):
|
|
18
|
+
if not isinstance(n_trees, int) or n_trees < 1:
|
|
19
|
+
raise ValueError("n_trees must be a positive integer.")
|
|
20
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
21
|
+
raise ValueError("max_depth must be a positive integer.")
|
|
22
|
+
if not isinstance(min_samples_split, int) or min_samples_split < 1:
|
|
23
|
+
raise ValueError("min_samples_split must be a positive integer.")
|
|
24
|
+
self.n_trees = n_trees
|
|
25
|
+
self.max_depth = max_depth
|
|
26
|
+
self.min_samples_split = min_samples_split
|
|
27
|
+
self.trees = [DecisionTree(max_depth=max_depth, min_samples_split=min_samples_split) for _ in range(n_trees)]
|
|
28
|
+
|
|
29
|
+
def fit(self, X, y):
|
|
30
|
+
"""
|
|
31
|
+
Fits the RandomForestClassifier model to the input data by generating trees, which split 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 labels corresponding to each sample.
|
|
36
|
+
Returns:
|
|
37
|
+
None
|
|
38
|
+
Raises:
|
|
39
|
+
TypeError: If the input matrix or the label matrix is not a PyTorch tensor.
|
|
40
|
+
ValueError: If the input matrix or the label 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 label 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 labels must be 1 dimensional with the same number of samples as the input data")
|
|
48
|
+
for tree in self.trees:
|
|
49
|
+
tree.fit(*self._bootstrap_sample(X, y))
|
|
50
|
+
|
|
51
|
+
def predict(self, X):
|
|
52
|
+
"""
|
|
53
|
+
Applies the fitted RandomForestClassifier model to the input data, predicting the correct classes.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
|
|
57
|
+
Returns:
|
|
58
|
+
labels (torch.Tensor of shape (n_samples,)): The predicted labels corresponding to each sample.
|
|
59
|
+
Raises:
|
|
60
|
+
NotFittedError: If the RandomForestClassifier model has not been fitted before predicting.
|
|
61
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
62
|
+
ValueError: If the input matrix is not the correct shape.
|
|
63
|
+
"""
|
|
64
|
+
if self.trees is None:
|
|
65
|
+
raise NotFittedError("RandomForestClassifier.fit() must be called before predicting.")
|
|
66
|
+
if not isinstance(X, torch.Tensor):
|
|
67
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
68
|
+
if X.ndim != 2 or X.shape[1] != self.trees[0].n_features:
|
|
69
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
70
|
+
predictions = torch.stack([tree.predict(X) for tree in self.trees]).T
|
|
71
|
+
return torch.tensor([Counter(sample_prediction).most_common(1)[0][0] for sample_prediction in predictions])
|
|
72
|
+
|
|
73
|
+
def predict_proba(self, X):
|
|
74
|
+
"""
|
|
75
|
+
Applies the fitted RandomForestClassifier model to the input data, predicting the probabilities of each class. Is calculated as the average of each individual trees predicted probabilities.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
|
|
79
|
+
Returns:
|
|
80
|
+
probabilities (torch.Tensor of shape (n_samples, n_classes)): The predicted probabilities corresponding to each sample.
|
|
81
|
+
Raises:
|
|
82
|
+
NotFittedError: If the RandomForestClassifier model has not been fitted before predicting.
|
|
83
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
84
|
+
ValueError: If the input matrix is not the correct shape.
|
|
85
|
+
"""
|
|
86
|
+
if self.trees is None:
|
|
87
|
+
raise NotFittedError("RandomForestClassifier.fit() must be called before predicting.")
|
|
88
|
+
if not isinstance(X, torch.Tensor):
|
|
89
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
90
|
+
if X.ndim != 2 or X.shape[1] != self.trees[0].n_features:
|
|
91
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
92
|
+
return torch.stack([tree.predict_proba(X) for tree in self.trees]).mean(dim=0)
|
|
93
|
+
|
|
94
|
+
def _bootstrap_sample(self, X, y):
|
|
95
|
+
indices = torch.randint(high=len(y), size=(len(y), 1)).flatten()
|
|
96
|
+
return X[indices], y[indices]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from collections import Counter
|
|
3
|
+
|
|
4
|
+
from ._RegressionTree import RegressionTree
|
|
5
|
+
from ....Exceptions import NotFittedError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RandomForestRegressor:
|
|
9
|
+
"""
|
|
10
|
+
RandomForestRegressor implements a regression algorithm fitting many :class:`RegressionTrees <DLL.MachineLearning.SupervisedLearning.Trees.RegressionTree>` to bootstrapped data.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
n_trees (int, optional): The number of trees used for predictiong. Defaults to 10. Must be a positive integer.
|
|
14
|
+
max_depth (int, optional): The maximum depth of the tree. Defaults to 10. Must be a positive integer.
|
|
15
|
+
min_samples_split (int, optional): The minimum required samples in a leaf to make a split. Defaults to 2. Must be a positive integer.
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self, n_trees=10, max_depth=25, min_samples_split=2):
|
|
18
|
+
if not isinstance(n_trees, int) or n_trees < 1:
|
|
19
|
+
raise ValueError("n_trees must be a positive integer.")
|
|
20
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
21
|
+
raise ValueError("max_depth must be a positive integer.")
|
|
22
|
+
if not isinstance(min_samples_split, int) or min_samples_split < 1:
|
|
23
|
+
raise ValueError("min_samples_split must be a positive integer.")
|
|
24
|
+
self.n_trees = n_trees
|
|
25
|
+
self.max_depth = max_depth
|
|
26
|
+
self.min_samples_split = min_samples_split
|
|
27
|
+
self.trees = [RegressionTree(max_depth=max_depth, min_samples_split=min_samples_split) for _ in range(n_trees)]
|
|
28
|
+
|
|
29
|
+
def fit(self, X, y):
|
|
30
|
+
"""
|
|
31
|
+
Fits the RandomForestRegressor model to the input data by generating trees, which split 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
|
+
for tree in self.trees:
|
|
49
|
+
tree.fit(*self._bootstrap_sample(X, y))
|
|
50
|
+
|
|
51
|
+
def predict(self, X):
|
|
52
|
+
"""
|
|
53
|
+
Applies the fitted RandomForestRegressor model to the input data, predicting the correct values.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be regressed.
|
|
57
|
+
Returns:
|
|
58
|
+
labels (torch.Tensor of shape (n_samples,)): The predicted target values corresponding to each sample.
|
|
59
|
+
Raises:
|
|
60
|
+
NotFittedError: If the RandomForestRegressor model has not been fitted before predicting.
|
|
61
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
62
|
+
ValueError: If the input matrix is not the correct shape.
|
|
63
|
+
"""
|
|
64
|
+
if self.trees is None:
|
|
65
|
+
raise NotFittedError("RandomForestRegressor.fit() must be called before predicting.")
|
|
66
|
+
if not isinstance(X, torch.Tensor):
|
|
67
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
68
|
+
if X.ndim != 2 or X.shape[1] != self.trees[0].n_features:
|
|
69
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
70
|
+
predictions = torch.stack([tree.predict(X) for tree in self.trees]).T
|
|
71
|
+
return torch.tensor([Counter(sample_prediction).most_common(1)[0][0] for sample_prediction in predictions])
|
|
72
|
+
|
|
73
|
+
def _bootstrap_sample(self, x, y):
|
|
74
|
+
indices = torch.randint(high=len(y), size=(len(y), 1)).flatten()
|
|
75
|
+
return x[indices], y[indices]
|