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,100 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import cvxopt
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ..Kernels import RBF, _Base
|
|
6
|
+
from ....Exceptions import NotFittedError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SVR:
|
|
10
|
+
"""
|
|
11
|
+
The support vector machine regressor with a quadratic programming solver.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
kernel (:ref:`kernel_section_label`, optional): The non-linearity function for fitting the model. Defaults to RBF.
|
|
15
|
+
C (float or int, optional): A regularization parameter. Defaults to 1. Must be positive real number.
|
|
16
|
+
epsilon (float or int, optional): The width of the tube of no penalty in epsilon-SVR. Must be a positive real number.
|
|
17
|
+
Attributes:
|
|
18
|
+
n_features (int): The number of features. Available after fitting.
|
|
19
|
+
alpha (torch.Tensor of shape (n_samples,)): The optimized dual coefficients. Available after fitting.
|
|
20
|
+
alpha_star (torch.Tensor of shape (n_samples,)): The optimized dual coefficients. Available after fitting.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, kernel=RBF(), C=1, epsilon=0.1):
|
|
23
|
+
if not isinstance(kernel, _Base):
|
|
24
|
+
raise ValueError("kernel must be from DLL.MachineLearning.SupervisedLearning.Kernels")
|
|
25
|
+
if not isinstance(C, float | int) or C <= 0:
|
|
26
|
+
raise ValueError("C must be must be positive real number.")
|
|
27
|
+
if not isinstance(epsilon, float | int) or epsilon <= 0:
|
|
28
|
+
raise ValueError("epsilon must be must be positive real number.")
|
|
29
|
+
|
|
30
|
+
self.kernel = kernel
|
|
31
|
+
self.C = C
|
|
32
|
+
self.epsilon = epsilon
|
|
33
|
+
|
|
34
|
+
def _kernel_matrix(self, X1, X2):
|
|
35
|
+
return self.kernel(X1, X2).to(X1.dtype)
|
|
36
|
+
|
|
37
|
+
def fit(self, X, y):
|
|
38
|
+
"""
|
|
39
|
+
Fits the SVR model to the input data by finding the hypertube that contains the data with minimum loss.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
|
|
43
|
+
y (torch.Tensor of shape (n_samples,)): The target values corresponding to each sample.
|
|
44
|
+
Returns:
|
|
45
|
+
None
|
|
46
|
+
Raises:
|
|
47
|
+
TypeError: If the input matrix or the target matrix is not a PyTorch tensor.
|
|
48
|
+
ValueError: If the input matrix or the target matrix is not the correct shape.
|
|
49
|
+
"""
|
|
50
|
+
if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
|
|
51
|
+
raise TypeError("The input matrix and the target matrix must be a PyTorch tensor.")
|
|
52
|
+
if X.ndim != 2:
|
|
53
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor.")
|
|
54
|
+
if y.ndim != 1 or y.shape[0] != X.shape[0]:
|
|
55
|
+
raise ValueError("The targets must be 1 dimensional with the same number of samples as the input data")
|
|
56
|
+
|
|
57
|
+
self.y = y.reshape((-1, 1)).to(X.dtype)
|
|
58
|
+
self.X = X
|
|
59
|
+
self.n_features = X.shape[1]
|
|
60
|
+
n = X.shape[0]
|
|
61
|
+
K = self._kernel_matrix(X, X).numpy()
|
|
62
|
+
|
|
63
|
+
P = cvxopt.matrix(np.block([[K, -K], [-K, K]]).tolist()) # [[K, -K], [-K, K]]
|
|
64
|
+
q = cvxopt.matrix(np.hstack([self.epsilon - y, self.epsilon + y]).tolist())
|
|
65
|
+
A = cvxopt.matrix(np.vstack([np.ones((n, 1)), -np.ones((n, 1))]).tolist())
|
|
66
|
+
b = cvxopt.matrix(0.0)
|
|
67
|
+
G = cvxopt.matrix(np.hstack([-np.eye(2 * n), np.eye(2 * n)]).tolist())
|
|
68
|
+
h = cvxopt.matrix(np.hstack([np.zeros((2 * n,)), np.ones((2 * n,)) * self.C]).tolist())
|
|
69
|
+
|
|
70
|
+
cvxopt.solvers.options['show_progress'] = False
|
|
71
|
+
sol = cvxopt.solvers.qp(P, q, G, h, A, b)
|
|
72
|
+
alpha = torch.tensor(np.array(sol["x"]), dtype=torch.float64).squeeze(dim=1)
|
|
73
|
+
self.alpha = alpha[:n]
|
|
74
|
+
self.alpha_star = alpha[n:]
|
|
75
|
+
|
|
76
|
+
def predict(self, X):
|
|
77
|
+
"""
|
|
78
|
+
Applies the fitted SVR model to the input data, predicting the correct values.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be regressed.
|
|
82
|
+
Returns:
|
|
83
|
+
target values (torch.Tensor of shape (n_samples,)): The predicted values corresponding to each sample.
|
|
84
|
+
Raises:
|
|
85
|
+
NotFittedError: If the SVR model has not been fitted before predicting.
|
|
86
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
87
|
+
ValueError: If the input matrix is not the correct shape.
|
|
88
|
+
"""
|
|
89
|
+
if not hasattr(self, "n_features"):
|
|
90
|
+
raise NotFittedError("SVR.fit() must be called before predicting.")
|
|
91
|
+
if not isinstance(X, torch.Tensor):
|
|
92
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
93
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
94
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
95
|
+
|
|
96
|
+
# Use every datapoint as a support vector, since otherwise the results seem to be bad.
|
|
97
|
+
# This makes no sense to me, but yields the best results.
|
|
98
|
+
bias = (self.y - torch.sum((self.alpha - self.alpha_star) * self._kernel_matrix(self.X, self.X), dim=1)).mean()
|
|
99
|
+
prediction = (self.alpha - self.alpha_star) @ self._kernel_matrix(self.X, X).to(dtype=self.alpha.dtype) + bias
|
|
100
|
+
return prediction
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from math import log
|
|
3
|
+
from warnings import warn
|
|
4
|
+
|
|
5
|
+
from ._DecisionTree import DecisionTree
|
|
6
|
+
from ....Exceptions import NotFittedError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AdaBoostClassifier:
|
|
10
|
+
"""
|
|
11
|
+
AdaBoostClassifier implements a classification algorithm fitting many consecutive :class:`DecisionTrees <DLL.MachineLearning.SupervisedLearning.Trees.DecisionTree>` to previously missclassified samples.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
n_trees (int, optional): The number of trees used for predicting. Defaults to 10. Must be a positive integer.
|
|
15
|
+
max_depth (int, optional): The maximum depth of the tree. Defaults to 25. Must be a positive integer.
|
|
16
|
+
min_samples_split (int, optional): The minimum required samples in a leaf to make a split. Defaults to 2. Must be a positive integer.
|
|
17
|
+
criterion (str, optional): The information criterion used to select optimal splits. Must be one of "entropy" or "gini". Defaults to "gini".
|
|
18
|
+
Attributes:
|
|
19
|
+
n_features (int): The number of features. Available after fitting.
|
|
20
|
+
n_classes (int): The number of classes. 2 for binary classification. Available after fitting.
|
|
21
|
+
confidences (torch.tensor of shape (n_trees,)): The confidence on each tree.
|
|
22
|
+
"""
|
|
23
|
+
def __init__(self, n_trees=10, max_depth=25, min_samples_split=2, criterion="gini"):
|
|
24
|
+
if not isinstance(n_trees, int) or n_trees < 1:
|
|
25
|
+
raise ValueError("n_trees must be a positive integer.")
|
|
26
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
27
|
+
raise ValueError("max_depth must be a positive integer.")
|
|
28
|
+
if not isinstance(min_samples_split, int) or min_samples_split < 1:
|
|
29
|
+
raise ValueError("min_samples_split must be a positive integer.")
|
|
30
|
+
if criterion not in ["entropy", "gini"]:
|
|
31
|
+
raise ValueError('The chosen criterion must be one of "entropy" or "gini".')
|
|
32
|
+
|
|
33
|
+
self.n_trees = n_trees
|
|
34
|
+
self.max_depth = max_depth
|
|
35
|
+
self.min_samples_split = min_samples_split
|
|
36
|
+
self.criterion = criterion
|
|
37
|
+
self.trees = None
|
|
38
|
+
|
|
39
|
+
def fit(self, X, y, verbose=True):
|
|
40
|
+
"""
|
|
41
|
+
Fits the AdaBoostClassifier model to the input data by fitting trees to the errors made by previous trees.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
|
|
45
|
+
y (torch.Tensor of shape (n_samples,)): The labels corresponding to each sample. Every element must be in [0, ..., n_classes - 1].
|
|
46
|
+
verbose (bool, optional): Determines if warnings are given if the training ends due to a weak learner being worse than random guessing. Defaults to True.
|
|
47
|
+
Returns:
|
|
48
|
+
The average errors after each tree.
|
|
49
|
+
Raises:
|
|
50
|
+
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.
|
|
51
|
+
ValueError: If the input matrix or the label vector is not the correct shape or the label vector contains wrong values.
|
|
52
|
+
"""
|
|
53
|
+
if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
|
|
54
|
+
raise TypeError("The input matrix and the label matrix must be a PyTorch tensor.")
|
|
55
|
+
if X.ndim != 2:
|
|
56
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor.")
|
|
57
|
+
if y.ndim != 1 or y.shape[0] != X.shape[0]:
|
|
58
|
+
raise ValueError("The labels must be 1 dimensional with the same number of samples as the input data")
|
|
59
|
+
vals = torch.unique(y).numpy()
|
|
60
|
+
if set(vals) != {*range(len(vals))}:
|
|
61
|
+
raise ValueError("y must only contain the values in [0, ..., n_classes - 1].")
|
|
62
|
+
if not isinstance(verbose, bool):
|
|
63
|
+
raise TypeError("verbose must be a boolean.")
|
|
64
|
+
|
|
65
|
+
self.n_classes = len(vals)
|
|
66
|
+
self.classes = vals
|
|
67
|
+
y = y.to(X.dtype)
|
|
68
|
+
self.n_features = X.shape[1]
|
|
69
|
+
|
|
70
|
+
trees = []
|
|
71
|
+
weights = torch.full_like(y, 1 / len(y))
|
|
72
|
+
self.confidences = []
|
|
73
|
+
|
|
74
|
+
errors = []
|
|
75
|
+
|
|
76
|
+
for i in range(self.n_trees):
|
|
77
|
+
indices = torch.multinomial(weights, len(y), replacement=True)
|
|
78
|
+
X_sample = X[indices]
|
|
79
|
+
y_sample = y[indices]
|
|
80
|
+
|
|
81
|
+
# Make sure atleast 1 datapoint is present from each class
|
|
82
|
+
sample_classes = torch.unique(y_sample)
|
|
83
|
+
if len(sample_classes) < self.n_classes:
|
|
84
|
+
unseen_classes = list(set(self.classes) - set(sample_classes))
|
|
85
|
+
X_new = []
|
|
86
|
+
y_new = []
|
|
87
|
+
for class_ in unseen_classes:
|
|
88
|
+
index = torch.multinomial((y == class_).float(), 1)
|
|
89
|
+
X_new.append(X[index].squeeze(0))
|
|
90
|
+
y_new.append(y[index].squeeze(0))
|
|
91
|
+
X_sample = torch.cat([X_sample, torch.stack(X_new)])
|
|
92
|
+
y_sample = torch.cat([y_sample, torch.stack(y_new)])
|
|
93
|
+
|
|
94
|
+
tree = DecisionTree(max_depth=self.max_depth, min_samples_split=self.min_samples_split, criterion=self.criterion)
|
|
95
|
+
tree.fit(X_sample, y_sample)
|
|
96
|
+
prediction = tree.predict(X)
|
|
97
|
+
incorrect = prediction != y
|
|
98
|
+
eps = torch.sum(incorrect * weights)
|
|
99
|
+
errors.append(eps)
|
|
100
|
+
|
|
101
|
+
# If better than random quessing, continue
|
|
102
|
+
if eps < 1 - 1 / self.n_classes:
|
|
103
|
+
alpha = 0.5 * (torch.log((1 - eps) / (eps + 1e-8)) + log(self.n_classes - 1))
|
|
104
|
+
weights = weights * torch.exp(alpha * incorrect)
|
|
105
|
+
weights /= weights.sum() # keep weights as a probability distribution
|
|
106
|
+
self.confidences.append(alpha)
|
|
107
|
+
else:
|
|
108
|
+
if verbose: warn(f"The latest weak learner is worse than random guessing. The training is stopped to reduce over fitting. Only {i} learners are used.")
|
|
109
|
+
self.n_trees = i
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
trees.append(tree)
|
|
113
|
+
|
|
114
|
+
self.trees = trees
|
|
115
|
+
return errors
|
|
116
|
+
|
|
117
|
+
def predict(self, X):
|
|
118
|
+
"""
|
|
119
|
+
Applies the fitted AdaBoostClassifier model to the input data, predicting the correct classes.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
labels (torch.Tensor of shape (n_samples,)): The predicted labels corresponding to each sample.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
NotFittedError: If the AdaBoostClassifier model has not been fitted before predicting.
|
|
129
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
130
|
+
ValueError: If the input matrix is not the correct shape.
|
|
131
|
+
"""
|
|
132
|
+
if self.trees is None:
|
|
133
|
+
raise NotFittedError("AdaBoostClassifier.fit() must be called before predicting.")
|
|
134
|
+
if not isinstance(X, torch.Tensor):
|
|
135
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
136
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
137
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
138
|
+
|
|
139
|
+
preds = torch.zeros((len(X), self.n_classes))
|
|
140
|
+
for alpha, tree in zip(self.confidences, self.trees):
|
|
141
|
+
pred = tree.predict(X).int()
|
|
142
|
+
one_hot_pred = torch.eye(self.n_classes)[pred]
|
|
143
|
+
preds += alpha * one_hot_pred
|
|
144
|
+
|
|
145
|
+
return preds.argmax(dim=1)
|
|
146
|
+
|
|
147
|
+
def predict_proba(self, X):
|
|
148
|
+
"""
|
|
149
|
+
Applies the fitted AdaBoostClassifier model to the input data, predicting the probabilities of each class.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data for which to predict probabilities.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
torch.Tensor of shape (n_samples, n_classes): The predicted probabilities for each class.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
NotFittedError: If the AdaBoostClassifier model has not been fitted before predicting.
|
|
159
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
160
|
+
ValueError: If the input matrix is not the correct shape.
|
|
161
|
+
"""
|
|
162
|
+
if self.trees is None:
|
|
163
|
+
raise NotFittedError("AdaBoostClassifier.fit() must be called before predicting.")
|
|
164
|
+
if not isinstance(X, torch.Tensor):
|
|
165
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
166
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
167
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
168
|
+
|
|
169
|
+
preds = torch.zeros((len(X), self.n_classes))
|
|
170
|
+
for alpha, tree in zip(self.confidences, self.trees):
|
|
171
|
+
pred = tree.predict(X).int()
|
|
172
|
+
one_hot_pred = torch.eye(self.n_classes)[pred]
|
|
173
|
+
preds += alpha * one_hot_pred
|
|
174
|
+
|
|
175
|
+
probs = torch.exp(preds - torch.max(preds, dim=1, keepdim=True).values)
|
|
176
|
+
probs = probs / torch.sum(probs, dim=1, keepdim=True)
|
|
177
|
+
if self.n_classes == 2:
|
|
178
|
+
return probs[:, 1]
|
|
179
|
+
return probs
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from warnings import warn
|
|
3
|
+
|
|
4
|
+
from ._RegressionTree import RegressionTree
|
|
5
|
+
from ....Exceptions import NotFittedError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AdaBoostRegressor:
|
|
9
|
+
"""
|
|
10
|
+
AdaBoostRegressor implements a regression algorithm fitting many consecutive :class:`RegressionTrees <DLL.MachineLearning.SupervisedLearning.Trees.RegressionTree>` to previously incorrectly predicted samples.
|
|
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 25. 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
|
+
loss (str, optional): The loss function used. Must be in ["linear", "square", "exponential"]. Defaults to "square".
|
|
17
|
+
Attributes:
|
|
18
|
+
n_features (int): The number of features. Available after fitting.
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, n_trees=10, max_depth=25, min_samples_split=2, loss="square"):
|
|
21
|
+
if not isinstance(n_trees, int) or n_trees < 1:
|
|
22
|
+
raise ValueError("n_trees must be a positive integer.")
|
|
23
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
24
|
+
raise ValueError("max_depth must be a positive integer.")
|
|
25
|
+
if not isinstance(min_samples_split, int) or min_samples_split < 1:
|
|
26
|
+
raise ValueError("min_samples_split must be a positive integer.")
|
|
27
|
+
if loss not in ["linear", "square", "exponential"]:
|
|
28
|
+
raise ValueError('loss must be in ["linear", "square", "exponential"].')
|
|
29
|
+
|
|
30
|
+
self.n_trees = n_trees
|
|
31
|
+
self.max_depth = max_depth
|
|
32
|
+
self.min_samples_split = min_samples_split
|
|
33
|
+
self.loss = loss
|
|
34
|
+
self.trees = None
|
|
35
|
+
|
|
36
|
+
def fit(self, X, y, verbose=True):
|
|
37
|
+
"""
|
|
38
|
+
Fits the AdaBoostRegressor model to the input data by fitting trees to the errors made by previous trees.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
|
|
42
|
+
y (torch.Tensor of shape (n_samples,)): The target values corresponding to each sample.
|
|
43
|
+
verbose (bool, optional): Determines if warnings are given if the training ends due to a weak learner having over 0.5 weighted loss. Defaults to True.
|
|
44
|
+
Returns:
|
|
45
|
+
The average errors after each tree.
|
|
46
|
+
Raises:
|
|
47
|
+
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.
|
|
48
|
+
ValueError: If the input matrix or the label vector is not the correct shape or the label vector contains wrong values.
|
|
49
|
+
"""
|
|
50
|
+
if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
|
|
51
|
+
raise TypeError("The input matrix and the label matrix must be a PyTorch tensor.")
|
|
52
|
+
if X.ndim != 2:
|
|
53
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor.")
|
|
54
|
+
if y.ndim != 1 or y.shape[0] != X.shape[0]:
|
|
55
|
+
raise ValueError("The labels must be 1 dimensional with the same number of samples as the input data")
|
|
56
|
+
if not isinstance(verbose, bool):
|
|
57
|
+
raise TypeError("verbose must be a boolean.")
|
|
58
|
+
|
|
59
|
+
y = y.to(X.dtype)
|
|
60
|
+
self.n_features = X.shape[1]
|
|
61
|
+
|
|
62
|
+
trees = []
|
|
63
|
+
weights = torch.full_like(y, 1 / len(y))
|
|
64
|
+
self.confidences = []
|
|
65
|
+
errors = []
|
|
66
|
+
|
|
67
|
+
for i in range(self.n_trees):
|
|
68
|
+
indices = torch.multinomial(weights, len(y), replacement=True)
|
|
69
|
+
X_sample = X[indices]
|
|
70
|
+
y_sample = y[indices]
|
|
71
|
+
|
|
72
|
+
tree = RegressionTree(max_depth=self.max_depth, min_samples_split=self.min_samples_split)
|
|
73
|
+
tree.fit(X_sample, y_sample)
|
|
74
|
+
prediction = tree.predict(X)
|
|
75
|
+
max_absolute_error = torch.abs(y - prediction).max()
|
|
76
|
+
if self.loss == "linear":
|
|
77
|
+
error = torch.abs(y - prediction) / max_absolute_error
|
|
78
|
+
elif self.loss == "square":
|
|
79
|
+
error = (y - prediction) ** 2 / max_absolute_error ** 2
|
|
80
|
+
elif self.loss == "exponential":
|
|
81
|
+
error = 1 - torch.exp(-torch.abs(y - prediction) / max_absolute_error)
|
|
82
|
+
|
|
83
|
+
average_loss = torch.sum(weights * error)
|
|
84
|
+
if average_loss > 0.5:
|
|
85
|
+
if verbose: warn(f"The average error exceeds 0.5. The training is stopped to reduce over fitting. Only {i} trees are used.")
|
|
86
|
+
self.n_trees = i
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
beta = average_loss / (1 - average_loss)
|
|
90
|
+
weights = weights * beta ** (1 - error)
|
|
91
|
+
weights /= weights.sum()
|
|
92
|
+
|
|
93
|
+
self.confidences.append(torch.log(1 / beta))
|
|
94
|
+
trees.append(tree)
|
|
95
|
+
errors.append(average_loss)
|
|
96
|
+
|
|
97
|
+
self.confidences = torch.stack(self.confidences)
|
|
98
|
+
self.confidences /= self.confidences.sum()
|
|
99
|
+
self.trees = trees
|
|
100
|
+
return errors
|
|
101
|
+
|
|
102
|
+
def predict(self, X, method="average"):
|
|
103
|
+
"""
|
|
104
|
+
Applies the fitted AdaBoostRegressor model to the input data, predicting the correct values.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be regressed.
|
|
108
|
+
method (str, optional): The method for computing the prediction. Must be one of "average" or "weighted_median". Defaults to average.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
values (torch.Tensor of shape (n_samples,)): The predicted values corresponding to each sample.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
NotFittedError: If the AdaBoostRegressor model has not been fitted before predicting.
|
|
115
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
116
|
+
ValueError: If the input matrix is not the correct shape.
|
|
117
|
+
"""
|
|
118
|
+
if self.trees is None:
|
|
119
|
+
raise NotFittedError("AdaBoostRegressor.fit() must be called before predicting.")
|
|
120
|
+
if not isinstance(X, torch.Tensor):
|
|
121
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
122
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
123
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
124
|
+
if method not in ["average", "weighted_median"]:
|
|
125
|
+
raise ValueError('method must be one of "average" or "weighted_median".')
|
|
126
|
+
|
|
127
|
+
if method == "average":
|
|
128
|
+
preds = torch.zeros(len(X))
|
|
129
|
+
for log_beta, tree in zip(self.confidences, self.trees):
|
|
130
|
+
pred = tree.predict(X)
|
|
131
|
+
preds += log_beta * pred
|
|
132
|
+
elif method == "weighted_median":
|
|
133
|
+
tree_preds = torch.stack([tree.predict(X) for tree in self.trees]).T # n_samples, n_trees
|
|
134
|
+
indicies = torch.argsort(tree_preds, dim=1)
|
|
135
|
+
accumulated = torch.cumsum(self.confidences[indicies], dim=1)
|
|
136
|
+
after_middle = accumulated >= 0.5 * accumulated[:, -1]
|
|
137
|
+
middle_indicies = torch.argmax(after_middle, dim=1)
|
|
138
|
+
original_middle_indicies = indicies[:, middle_indicies]
|
|
139
|
+
preds = tree_preds[:, original_middle_indicies]
|
|
140
|
+
|
|
141
|
+
return preds
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from ....Exceptions import NotFittedError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Node:
|
|
7
|
+
def __init__(self, left=None, right=None, threshold=None, feature_index=None, value=None, probabilities=None, size=None, depth=None):
|
|
8
|
+
self.left = left
|
|
9
|
+
self.right = right
|
|
10
|
+
self.threshold = threshold
|
|
11
|
+
self.feature_index = feature_index
|
|
12
|
+
self.value = value
|
|
13
|
+
self.probabilities = probabilities
|
|
14
|
+
self.size = size
|
|
15
|
+
self.depth = depth
|
|
16
|
+
|
|
17
|
+
def is_leaf(self):
|
|
18
|
+
return self.value is not None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DecisionTree:
|
|
22
|
+
"""
|
|
23
|
+
DecisionTree implements a classification algorithm splitting the data along features yielding the maximum entropy.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
max_depth (int, optional): The maximum depth of the tree. Defaults to 10. Must be a positive integer.
|
|
27
|
+
min_samples_split (int, optional): The minimum required samples in a leaf to make a split. Defaults to 2. Must be a positive integer.
|
|
28
|
+
criterion (str, optional): The information criterion used to select optimal splits. Must be one of "entropy" or "gini". Defaults to "gini".
|
|
29
|
+
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.
|
|
30
|
+
Attributes:
|
|
31
|
+
n_classes (int): The number of classes. A positive integer available after calling DecisionTree.fit().
|
|
32
|
+
classes (torch.Tensor of shape (n_classes,)): The classes in an arbitrary order. Available after calling DecisionTree.fit().
|
|
33
|
+
"""
|
|
34
|
+
def __init__(self, max_depth=10, min_samples_split=2, criterion="gini", ccp_alpha=0.0):
|
|
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 criterion not in ["entropy", "gini"]:
|
|
40
|
+
raise ValueError('The chosen criterion must be one of "entropy" or "gini".')
|
|
41
|
+
if not isinstance(ccp_alpha, int | float) or ccp_alpha < 0:
|
|
42
|
+
raise ValueError("ccp_alpha must be non-negative.")
|
|
43
|
+
|
|
44
|
+
self.max_depth = max_depth
|
|
45
|
+
self.min_samples_split = min_samples_split
|
|
46
|
+
self.criterion = criterion
|
|
47
|
+
self.root = None
|
|
48
|
+
self.ccp_alpha = ccp_alpha
|
|
49
|
+
|
|
50
|
+
def fit(self, X, y):
|
|
51
|
+
"""
|
|
52
|
+
Fits the DecisionTree model to the input data by generating a tree, which splits the data appropriately.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
|
|
56
|
+
y (torch.Tensor of shape (n_samples,)): The labels corresponding to each sample.
|
|
57
|
+
Returns:
|
|
58
|
+
None
|
|
59
|
+
Raises:
|
|
60
|
+
TypeError: If the input matrix or the label matrix is not a PyTorch tensor.
|
|
61
|
+
ValueError: If the input matrix or the label matrix is not the correct shape.
|
|
62
|
+
"""
|
|
63
|
+
if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
|
|
64
|
+
raise TypeError("The input matrix and the label matrix must be a PyTorch tensor.")
|
|
65
|
+
if X.ndim != 2:
|
|
66
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor.")
|
|
67
|
+
if y.ndim != 1 or y.shape[0] != X.shape[0]:
|
|
68
|
+
raise ValueError("The labels must be 1 dimensional with the same number of samples as the input data")
|
|
69
|
+
self.n_samples, self.n_features = X.shape
|
|
70
|
+
self.classes = torch.unique(y)
|
|
71
|
+
self.n_classes = len(self.classes)
|
|
72
|
+
self.root = self._grow_tree(X, y, 0)
|
|
73
|
+
if self.ccp_alpha > 0:
|
|
74
|
+
self.root, _, _ = self._prune(self.root, X, y)
|
|
75
|
+
|
|
76
|
+
def _grow_tree(self, x, y, depth):
|
|
77
|
+
n_samples, n_features = x.size()
|
|
78
|
+
classes, counts = torch.unique(y, return_counts=True)
|
|
79
|
+
|
|
80
|
+
if depth >= self.max_depth or len(classes) == 1 or n_samples < self.min_samples_split:
|
|
81
|
+
largest_class = classes[torch.argmax(counts)]
|
|
82
|
+
probabilities = torch.tensor([(counts[torch.where(classes == _class)[0]] / len(y) if _class in classes else 0) for _class in self.classes])
|
|
83
|
+
return Node(value=largest_class, probabilities=probabilities)
|
|
84
|
+
|
|
85
|
+
feature_indicies = torch.randperm(n_features)[:self.n_features]
|
|
86
|
+
split_threshold, split_index = self._best_split(x, y, feature_indicies)
|
|
87
|
+
# if no split gains more information
|
|
88
|
+
if split_threshold is None:
|
|
89
|
+
largest_class = classes[torch.argmax(counts)]
|
|
90
|
+
probabilities = torch.tensor([(counts[torch.where(classes == _class)[0]] / len(y) if _class in classes else 0) for _class in self.classes])
|
|
91
|
+
return Node(value=largest_class, probabilities=probabilities)
|
|
92
|
+
|
|
93
|
+
left_indicies, right_indicies = self._split(x[:, split_index], split_threshold)
|
|
94
|
+
left = self._grow_tree(x[left_indicies], y[left_indicies], depth + 1)
|
|
95
|
+
right = self._grow_tree(x[right_indicies], y[right_indicies], depth + 1)
|
|
96
|
+
return Node(left, right, split_threshold, split_index)
|
|
97
|
+
|
|
98
|
+
def _best_split(self, x, y, feature_indicies):
|
|
99
|
+
max_entropy_gain = -1
|
|
100
|
+
split_index = None
|
|
101
|
+
split_threshold = None
|
|
102
|
+
|
|
103
|
+
for index in feature_indicies:
|
|
104
|
+
feature_values = x[:, index]
|
|
105
|
+
possible_thresholds = torch.unique(feature_values)
|
|
106
|
+
for threshold in possible_thresholds:
|
|
107
|
+
entropy_gain = self._information_gain(y, feature_values, threshold)
|
|
108
|
+
if entropy_gain > max_entropy_gain:
|
|
109
|
+
max_entropy_gain = entropy_gain
|
|
110
|
+
split_index = index
|
|
111
|
+
split_threshold = threshold
|
|
112
|
+
if max_entropy_gain <= 0: return None, None
|
|
113
|
+
return split_threshold, split_index
|
|
114
|
+
|
|
115
|
+
def _split(self, feature_values, threshold):
|
|
116
|
+
left_indicies = torch.argwhere(feature_values <= threshold).flatten()
|
|
117
|
+
right_indicies = torch.argwhere(feature_values > threshold).flatten()
|
|
118
|
+
return left_indicies, right_indicies
|
|
119
|
+
|
|
120
|
+
def _information_gain(self, y, feature_values, threshold):
|
|
121
|
+
if self.criterion == "gini":
|
|
122
|
+
information_func = self._gini
|
|
123
|
+
elif self.criterion == "entropy":
|
|
124
|
+
information_func = self._entropy
|
|
125
|
+
|
|
126
|
+
left_indicies, right_indicies = self._split(feature_values, threshold)
|
|
127
|
+
if len(left_indicies) == 0 or len(right_indicies) == 0:
|
|
128
|
+
return 0
|
|
129
|
+
n = len(y)
|
|
130
|
+
return information_func(y) - len(left_indicies) / n * information_func(y[left_indicies]) - len(right_indicies) / n * information_func(y[right_indicies])
|
|
131
|
+
|
|
132
|
+
def _entropy(self, values):
|
|
133
|
+
n = len(values)
|
|
134
|
+
data_type = values.dtype
|
|
135
|
+
p = torch.bincount(values.to(dtype=torch.int32)).to(dtype=data_type) / n
|
|
136
|
+
p = p[p != 0] # remove non-zero classes from messing up the calculations
|
|
137
|
+
return -torch.sum(p * torch.log(p))
|
|
138
|
+
|
|
139
|
+
def _gini(self, values):
|
|
140
|
+
n = len(values)
|
|
141
|
+
data_type = values.dtype
|
|
142
|
+
p = torch.bincount(values.to(dtype=torch.int32)).to(dtype=data_type) / n
|
|
143
|
+
return torch.sum(p * (1 - p))
|
|
144
|
+
|
|
145
|
+
def predict(self, X):
|
|
146
|
+
"""
|
|
147
|
+
Applies the fitted DecisionTree model to the input data, predicting the correct classes.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
|
|
151
|
+
Returns:
|
|
152
|
+
labels (torch.Tensor of shape (n_samples,)): The predicted labels corresponding to each sample.
|
|
153
|
+
Raises:
|
|
154
|
+
NotFittedError: If the DecisionTree model has not been fitted before predicting.
|
|
155
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
156
|
+
ValueError: If the input matrix is not the correct shape.
|
|
157
|
+
"""
|
|
158
|
+
if self.root is None:
|
|
159
|
+
raise NotFittedError("DecisionTree.fit() must be called before predicting.")
|
|
160
|
+
if not isinstance(X, torch.Tensor):
|
|
161
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
162
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
163
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
164
|
+
return torch.tensor([self._predict_single(datapoint, self.root) for datapoint in X])
|
|
165
|
+
|
|
166
|
+
def _predict_single(self, x, current_node, proba=False):
|
|
167
|
+
if current_node.is_leaf():
|
|
168
|
+
if proba:
|
|
169
|
+
return current_node.probabilities
|
|
170
|
+
return current_node.value
|
|
171
|
+
|
|
172
|
+
if x[current_node.feature_index] <= current_node.threshold:
|
|
173
|
+
return self._predict_single(x, current_node.left, proba=proba)
|
|
174
|
+
|
|
175
|
+
return self._predict_single(x, current_node.right, proba=proba)
|
|
176
|
+
|
|
177
|
+
def predict_proba(self, X):
|
|
178
|
+
"""
|
|
179
|
+
Applies the fitted DecisionTree model to the input data, predicting the probabilities of each class.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
|
|
183
|
+
Returns:
|
|
184
|
+
probabilities (torch.Tensor of shape (n_samples, n_classes)): The predicted probabilities corresponding to each sample.
|
|
185
|
+
Raises:
|
|
186
|
+
NotFittedError: If the DecisionTree model has not been fitted before predicting.
|
|
187
|
+
TypeError: If the input matrix is not a PyTorch tensor.
|
|
188
|
+
ValueError: If the input matrix is not the correct shape.
|
|
189
|
+
"""
|
|
190
|
+
if self.root is None:
|
|
191
|
+
raise NotFittedError("DecisionTree.fit() must be called before predicting.")
|
|
192
|
+
if not isinstance(X, torch.Tensor):
|
|
193
|
+
raise TypeError("The input matrix must be a PyTorch tensor.")
|
|
194
|
+
if X.ndim != 2 or X.shape[1] != self.n_features:
|
|
195
|
+
raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
|
|
196
|
+
return torch.stack([self._predict_single(datapoint, self.root, proba=True) for datapoint in X])
|
|
197
|
+
|
|
198
|
+
def _prune(self, node, X, y):
|
|
199
|
+
classes, counts = torch.unique(y, return_counts=True)
|
|
200
|
+
m = len(y) - torch.min(counts).item()
|
|
201
|
+
if node.is_leaf():
|
|
202
|
+
return node, m / self.n_samples, 1
|
|
203
|
+
|
|
204
|
+
left_indicies, right_indicies = self._split(X[:, node.feature_index], node.threshold)
|
|
205
|
+
node.left, left_cost, left_leaf_nodes = self._prune(node.left, X[left_indicies], y[left_indicies])
|
|
206
|
+
node.right, right_cost, right_leaf_nodes = self._prune(node.right, X[right_indicies], y[right_indicies])
|
|
207
|
+
subtree_cost = left_cost + right_cost
|
|
208
|
+
total_subleaves = left_leaf_nodes + right_leaf_nodes
|
|
209
|
+
|
|
210
|
+
leaf_miss_classification_error_rate = m / self.n_samples
|
|
211
|
+
new_leaf_nodes = 1
|
|
212
|
+
|
|
213
|
+
subtree_cost_with_n_nodes_penalty = subtree_cost + self.ccp_alpha * total_subleaves
|
|
214
|
+
|
|
215
|
+
if leaf_miss_classification_error_rate + self.ccp_alpha * new_leaf_nodes < subtree_cost_with_n_nodes_penalty:
|
|
216
|
+
largest_class = classes[torch.argmax(counts)]
|
|
217
|
+
probabilities = torch.tensor([(counts[torch.where(classes == _class)[0]] / len(y) if _class in classes else 0) for _class in self.classes])
|
|
218
|
+
return Node(value=largest_class, probabilities=probabilities), leaf_miss_classification_error_rate, 1
|
|
219
|
+
return node, subtree_cost, total_subleaves
|