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.
Files changed (124) hide show
  1. DLL/Data/Metrics.py +626 -0
  2. DLL/Data/Preprocessing.py +384 -0
  3. DLL/Data/_DataReader.py +55 -0
  4. DLL/Data/__init__.py +1 -0
  5. DLL/DeepLearning/Callbacks/_BackUp.py +71 -0
  6. DLL/DeepLearning/Callbacks/_BaseCallback.py +43 -0
  7. DLL/DeepLearning/Callbacks/_EarlyStopping.py +102 -0
  8. DLL/DeepLearning/Callbacks/_ReduceLROnPlateau.py +90 -0
  9. DLL/DeepLearning/Callbacks/__init__.py +4 -0
  10. DLL/DeepLearning/Initialisers/_Initialiser.py +24 -0
  11. DLL/DeepLearning/Initialisers/_Kaiming_He.py +55 -0
  12. DLL/DeepLearning/Initialisers/_Xavier_Glorot.py +39 -0
  13. DLL/DeepLearning/Initialisers/__init__.py +2 -0
  14. DLL/DeepLearning/Layers/Activations/_Activation.py +23 -0
  15. DLL/DeepLearning/Layers/Activations/_ReLU.py +51 -0
  16. DLL/DeepLearning/Layers/Activations/_Sigmoid.py +51 -0
  17. DLL/DeepLearning/Layers/Activations/_SoftMax.py +83 -0
  18. DLL/DeepLearning/Layers/Activations/_Tanh.py +47 -0
  19. DLL/DeepLearning/Layers/Activations/__init__.py +4 -0
  20. DLL/DeepLearning/Layers/Regularisation/_BaseRegularisation.py +12 -0
  21. DLL/DeepLearning/Layers/Regularisation/_BatchNormalisation.py +106 -0
  22. DLL/DeepLearning/Layers/Regularisation/_Dropout.py +79 -0
  23. DLL/DeepLearning/Layers/Regularisation/_GroupNormalisation.py +112 -0
  24. DLL/DeepLearning/Layers/Regularisation/_InstanceNormalisation.py +50 -0
  25. DLL/DeepLearning/Layers/Regularisation/_LayerNormalisation.py +37 -0
  26. DLL/DeepLearning/Layers/Regularisation/__init__.py +5 -0
  27. DLL/DeepLearning/Layers/_Add.py +109 -0
  28. DLL/DeepLearning/Layers/_Attention.py +161 -0
  29. DLL/DeepLearning/Layers/_BaseLayer.py +87 -0
  30. DLL/DeepLearning/Layers/_Bidirectional.py +113 -0
  31. DLL/DeepLearning/Layers/_Conv2D.py +133 -0
  32. DLL/DeepLearning/Layers/_Dense.py +109 -0
  33. DLL/DeepLearning/Layers/_DenseKAN.py +184 -0
  34. DLL/DeepLearning/Layers/_Flatten.py +55 -0
  35. DLL/DeepLearning/Layers/_Identity.py +50 -0
  36. DLL/DeepLearning/Layers/_Input.py +11 -0
  37. DLL/DeepLearning/Layers/_LSTM.py +261 -0
  38. DLL/DeepLearning/Layers/_LayerList.py +98 -0
  39. DLL/DeepLearning/Layers/_MaxPooling2D.py +101 -0
  40. DLL/DeepLearning/Layers/_RNN.py +185 -0
  41. DLL/DeepLearning/Layers/_Reshape.py +52 -0
  42. DLL/DeepLearning/Layers/__init__.py +14 -0
  43. DLL/DeepLearning/Losses/_BCE.py +96 -0
  44. DLL/DeepLearning/Losses/_BaseLoss.py +18 -0
  45. DLL/DeepLearning/Losses/_CCE.py +87 -0
  46. DLL/DeepLearning/Losses/_Exponential.py +101 -0
  47. DLL/DeepLearning/Losses/_Huber.py +104 -0
  48. DLL/DeepLearning/Losses/_MAE.py +85 -0
  49. DLL/DeepLearning/Losses/_MSE.py +86 -0
  50. DLL/DeepLearning/Losses/__init__.py +6 -0
  51. DLL/DeepLearning/Model.py +265 -0
  52. DLL/DeepLearning/Optimisers/_ADADELTA.py +53 -0
  53. DLL/DeepLearning/Optimisers/_ADAGRAD.py +51 -0
  54. DLL/DeepLearning/Optimisers/_ADAM.py +65 -0
  55. DLL/DeepLearning/Optimisers/_BaseOptimiser.py +20 -0
  56. DLL/DeepLearning/Optimisers/_LBFGS.py +120 -0
  57. DLL/DeepLearning/Optimisers/_RMSPROP.py +62 -0
  58. DLL/DeepLearning/Optimisers/_SGD.py +44 -0
  59. DLL/DeepLearning/Optimisers/__init__.py +6 -0
  60. DLL/DeepLearning/__init__.py +0 -0
  61. DLL/Exceptions/_Exceptions.py +19 -0
  62. DLL/Exceptions/__init__.py +2 -0
  63. DLL/MachineLearning/SupervisedLearning/Calibration/_CalibratedClassifier.py +88 -0
  64. DLL/MachineLearning/SupervisedLearning/Calibration/_IsotonicRegression.py +114 -0
  65. DLL/MachineLearning/SupervisedLearning/Calibration/__init__.py +2 -0
  66. DLL/MachineLearning/SupervisedLearning/GaussianProcesses/_GaussianProcessRegressor.py +190 -0
  67. DLL/MachineLearning/SupervisedLearning/GaussianProcesses/__init__.py +1 -0
  68. DLL/MachineLearning/SupervisedLearning/Kernels.py +851 -0
  69. DLL/MachineLearning/SupervisedLearning/LinearModels/TimeSeries.py +147 -0
  70. DLL/MachineLearning/SupervisedLearning/LinearModels/_Elasticnet.py +161 -0
  71. DLL/MachineLearning/SupervisedLearning/LinearModels/_LassoRegression.py +157 -0
  72. DLL/MachineLearning/SupervisedLearning/LinearModels/_LinearRegression.py +81 -0
  73. DLL/MachineLearning/SupervisedLearning/LinearModels/_LogisticRegression.py +182 -0
  74. DLL/MachineLearning/SupervisedLearning/LinearModels/_RANSAC.py +97 -0
  75. DLL/MachineLearning/SupervisedLearning/LinearModels/_RidgeRegression.py +78 -0
  76. DLL/MachineLearning/SupervisedLearning/LinearModels/__init__.py +6 -0
  77. DLL/MachineLearning/SupervisedLearning/NaiveBayes/_BernoulliNaiveBayes.py +115 -0
  78. DLL/MachineLearning/SupervisedLearning/NaiveBayes/_GaussianNaiveBayes.py +111 -0
  79. DLL/MachineLearning/SupervisedLearning/NaiveBayes/_MultinomialNaiveBayes.py +115 -0
  80. DLL/MachineLearning/SupervisedLearning/NaiveBayes/__init__.py +3 -0
  81. DLL/MachineLearning/SupervisedLearning/Neighbors/_KNearestNeighborsClassifier.py +115 -0
  82. DLL/MachineLearning/SupervisedLearning/Neighbors/_KNearestNeighborsRegressor.py +90 -0
  83. DLL/MachineLearning/SupervisedLearning/Neighbors/__init__.py +2 -0
  84. DLL/MachineLearning/SupervisedLearning/SupportVectorMachines/_SVC.py +293 -0
  85. DLL/MachineLearning/SupervisedLearning/SupportVectorMachines/_SVR.py +100 -0
  86. DLL/MachineLearning/SupervisedLearning/SupportVectorMachines/__init__.py +2 -0
  87. DLL/MachineLearning/SupervisedLearning/Trees/_AdaBoostClassifier.py +179 -0
  88. DLL/MachineLearning/SupervisedLearning/Trees/_AdaBoostRegressor.py +141 -0
  89. DLL/MachineLearning/SupervisedLearning/Trees/_DecisionTree.py +219 -0
  90. DLL/MachineLearning/SupervisedLearning/Trees/_GradientBoostingClassifier.py +207 -0
  91. DLL/MachineLearning/SupervisedLearning/Trees/_GradientBoostingRegressor.py +122 -0
  92. DLL/MachineLearning/SupervisedLearning/Trees/_LGBMClassifier.py +288 -0
  93. DLL/MachineLearning/SupervisedLearning/Trees/_LGBMRegressor.py +168 -0
  94. DLL/MachineLearning/SupervisedLearning/Trees/_LGBMTree.py +199 -0
  95. DLL/MachineLearning/SupervisedLearning/Trees/_RandomForest.py +96 -0
  96. DLL/MachineLearning/SupervisedLearning/Trees/_RegressionForest.py +75 -0
  97. DLL/MachineLearning/SupervisedLearning/Trees/_RegressionTree.py +148 -0
  98. DLL/MachineLearning/SupervisedLearning/Trees/_XGBoostClassifier.py +239 -0
  99. DLL/MachineLearning/SupervisedLearning/Trees/_XGBoostRegressor.py +131 -0
  100. DLL/MachineLearning/SupervisedLearning/Trees/_XGBoostTree.py +105 -0
  101. DLL/MachineLearning/SupervisedLearning/Trees/__init__.py +12 -0
  102. DLL/MachineLearning/SupervisedLearning/__init__.py +0 -0
  103. DLL/MachineLearning/UnsupervisedLearning/Clustering/_DBScan.py +87 -0
  104. DLL/MachineLearning/UnsupervisedLearning/Clustering/_GaussianMixtureModels.py +121 -0
  105. DLL/MachineLearning/UnsupervisedLearning/Clustering/_KMeans.py +120 -0
  106. DLL/MachineLearning/UnsupervisedLearning/Clustering/_SpectralClustering.py +105 -0
  107. DLL/MachineLearning/UnsupervisedLearning/Clustering/__init__.py +4 -0
  108. DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_LDA.py +118 -0
  109. DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_PCA.py +101 -0
  110. DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_QDA.py +69 -0
  111. DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_RobustPCA.py +143 -0
  112. DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_TSNE.py +151 -0
  113. DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/_UMAP.py +176 -0
  114. DLL/MachineLearning/UnsupervisedLearning/DimensionalityReduction/__init__.py +6 -0
  115. DLL/MachineLearning/UnsupervisedLearning/OutlierDetection/_IsolationForest.py +175 -0
  116. DLL/MachineLearning/UnsupervisedLearning/OutlierDetection/__init__.py +1 -0
  117. DLL/MachineLearning/UnsupervisedLearning/__init__.py +0 -0
  118. DLL/MachineLearning/__init__.py +0 -0
  119. DLL/__init__.py +0 -0
  120. pydllib-1.0.dist-info/METADATA +104 -0
  121. pydllib-1.0.dist-info/RECORD +124 -0
  122. pydllib-1.0.dist-info/WHEEL +5 -0
  123. pydllib-1.0.dist-info/licenses/LICENSE +21 -0
  124. 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]