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,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