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,115 @@
1
+ import torch
2
+
3
+ from ....Exceptions import NotFittedError
4
+
5
+
6
+ class MultinomialNaiveBayes:
7
+ """
8
+ The MultinomialNaiveBayes classifier model. Applies the Bayes theorem to classify samples with positive integer features.
9
+
10
+ Attributes:
11
+ n_features (int): The number of features. Available after fitting.
12
+ n_classes (int): The number of classes. Available after fitting.
13
+ """
14
+ def fit(self, X, y, alpha=1):
15
+ """
16
+ Fits the MultinomialNaiveBayes model to the input data by calculating the prior probabilities and likelihoods.
17
+
18
+ Args:
19
+ X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature. Must contain only non-negative integers.
20
+ y (torch.Tensor of shape (n_samples,)): The labels corresponding to each sample. Every element must be in [0, ..., n_classes - 1].
21
+ alpha (float | int): Laplacian smoothing parameter. Must be non-negative. For no smoothing, alpha is set to zero. Defaults to 1.
22
+ Returns:
23
+ None
24
+ Raises:
25
+ TypeError: If the input matrix or the label matrix is not a PyTorch tensor.
26
+ ValueError: If the input matrix or the label matrix is not the correct shape.
27
+ """
28
+ if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
29
+ raise TypeError("The input matrix and the label matrix must be PyTorch tensors.")
30
+ if X.ndim != 2:
31
+ raise ValueError("The input matrix must be a 2 dimensional tensor.")
32
+ if y.ndim != 1 or y.shape[0] != X.shape[0]:
33
+ raise ValueError("The labels must be 1 dimensional with the same number of samples as the input data")
34
+ vals = torch.unique(y).numpy()
35
+ if set(vals) != {*range(len(vals))}:
36
+ raise ValueError("y must only contain the values in [0, ..., n_classes - 1].")
37
+ if (X.dtype == torch.int or X.dtype == torch.int8 or X.dtype == torch.int16 or X.dtype == torch.int32 or X.dtype == torch.int64) and torch.all(X >= 0):
38
+ raise ValueError("The features must be non-negative integers. If your features are not non-negative integers, consider other NaiveBayes models.")
39
+ if not isinstance(alpha, int | float) or alpha < 0:
40
+ raise ValueError("alpha must be a non-negative real number.")
41
+
42
+ self.n_classes = len(vals)
43
+ self.n_features = X.shape[1]
44
+ self.priors = torch.zeros((self.n_classes,), dtype=torch.float32)
45
+ self.likelihoods = torch.zeros((self.n_classes, X.shape[1]), dtype=torch.float32)
46
+
47
+ for i in range(self.n_classes):
48
+ X_cls = X[y == i]
49
+ self.priors[i] = len(X_cls) / len(y)
50
+ self.likelihoods[i] = (X_cls.sum(dim=0) + alpha) / (X_cls.sum() + alpha * X.shape[1]) # laplace smoothing
51
+
52
+ def predict(self, X):
53
+ """
54
+ Applies the fitted MultinomialNaiveBayes model to the input data, predicting the labels.
55
+
56
+ Args:
57
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
58
+ Returns:
59
+ labels (torch.Tensor of shape (n_samples,)): The predicted target values corresponding to each sample.
60
+ Raises:
61
+ NotFittedError: If the MultinomialNaiveBayes model has not been fitted before predicting.
62
+ TypeError: If the input matrix is not a PyTorch tensor.
63
+ ValueError: If the input matrix is not the correct shape.
64
+ """
65
+ if not hasattr(self, "priors"):
66
+ raise NotFittedError("MultinomialNaiveBayes.fit() must be called before predicting.")
67
+ if not isinstance(X, torch.Tensor):
68
+ raise TypeError("The input matrix must be a PyTorch tensor.")
69
+ if X.ndim != 2 or X.shape[1] != self.n_features:
70
+ raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
71
+ if (X.dtype == torch.int or X.dtype == torch.int8 or X.dtype == torch.int16 or X.dtype == torch.int32 or X.dtype == torch.int64) and torch.all(X >= 0):
72
+ raise ValueError("The features must be non-negative integers. If your features are not non-negative integers, consider other NaiveBayes models.")
73
+
74
+ posteriors = torch.zeros((self.n_classes, len(X)), dtype=torch.float32)
75
+
76
+ for i in range(self.n_classes):
77
+ prior = torch.log(self.priors[i])
78
+ posterior = (X * torch.log(self.likelihoods[i])).sum(dim=1) + prior
79
+ posteriors[i] = posterior
80
+ return torch.argmax(posteriors, dim=0)
81
+
82
+ def predict_proba(self, X):
83
+ """
84
+ Applies the fitted MultinomialNaiveBayes model to the input data, predicting the labels.
85
+
86
+ Args:
87
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
88
+ Returns:
89
+ labels (torch.Tensor of shape (n_samples,)): The predicted target values corresponding to each sample.
90
+ Raises:
91
+ NotFittedError: If the MultinomialNaiveBayes model has not been fitted before predicting.
92
+ TypeError: If the input matrix is not a PyTorch tensor.
93
+ ValueError: If the input matrix is not the correct shape.
94
+ """
95
+ if not hasattr(self, "priors"):
96
+ raise NotFittedError("MultinomialNaiveBayes.fit() must be called before predicting.")
97
+ if not isinstance(X, torch.Tensor):
98
+ raise TypeError("The input matrix must be a PyTorch tensor.")
99
+ if X.ndim != 2 or X.shape[1] != self.n_features:
100
+ raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
101
+ if (X.dtype == torch.int or X.dtype == torch.int8 or X.dtype == torch.int16 or X.dtype == torch.int32 or X.dtype == torch.int64) and torch.all(X >= 0):
102
+ raise ValueError("The features must be non-negative integers. If your features are not non-negative integers, consider other NaiveBayes models.")
103
+
104
+ posteriors = torch.zeros((self.n_classes, len(X)), dtype=torch.float32)
105
+
106
+ for i in range(self.n_classes):
107
+ prior = torch.log(self.priors[i])
108
+ posterior = (X * torch.log(self.likelihoods[i])).sum(dim=1) + prior
109
+ posteriors[i] = posterior
110
+ prob_normalizers = torch.logsumexp(posteriors, dim=0)
111
+ log_probs = posteriors - prob_normalizers
112
+ probs = torch.exp(log_probs).T
113
+ if self.n_classes == 2: # binary classification
114
+ probs = probs[:, 1]
115
+ return probs
@@ -0,0 +1,3 @@
1
+ from ._BernoulliNaiveBayes import BernoulliNaiveBayes
2
+ from ._GaussianNaiveBayes import GaussianNaiveBayes
3
+ from ._MultinomialNaiveBayes import MultinomialNaiveBayes
@@ -0,0 +1,115 @@
1
+ import torch
2
+ from functools import partial
3
+
4
+ from ....Exceptions import NotFittedError
5
+
6
+
7
+ class KNNClassifier:
8
+ """
9
+ The k-nearest neighbors classifier model. Looks for the k closest samples with respect to a distance metric and calculates the most common label.
10
+
11
+ Args:
12
+ k (int, optional): The number of closest samples considered for the predictions. Must be a positive integer. Defaults to 3.
13
+ metric (str, optional): A distance metric for the closest points. Must be one of "euclidian" or "manhattan". Defaults to "euclidian".
14
+ """
15
+ def __init__(self, k=3, metric="euclidian"):
16
+ if not isinstance(k, int) or k <= 0:
17
+ raise ValueError("k must be a positive integer.")
18
+ if metric not in ["euclidian", "manhattan"]:
19
+ raise ValueError('metric must be on of "euclidian" or "manhattan".')
20
+
21
+ self.k = k
22
+ self.metric = partial(self._metric, metric)
23
+
24
+ def _metric(self, metric, X1, X2):
25
+ if metric == "euclidian":
26
+ return ((X1 - X2) ** 2).sum(dim=2).sqrt()
27
+ elif metric == "manhattan":
28
+ return torch.abs(X1 - X2).sum(dim=2)
29
+
30
+ def fit(self, X, y):
31
+ """
32
+ Fits the KNNClassifier model to the input data by storing the input and label matricies.
33
+
34
+ Args:
35
+ X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature. There must be atleast k samples.
36
+ y (torch.Tensor of shape (n_samples,)): The labels corresponding to each sample. Every element must be in [0, ..., n_classes - 1].
37
+ Returns:
38
+ None
39
+ Raises:
40
+ TypeError: If the input matrix or the label matrix is not a PyTorch tensor.
41
+ ValueError: If the input matrix or the label matrix is not the correct shape.
42
+ """
43
+ if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
44
+ raise TypeError("The input matrix and the label matrix must be PyTorch tensors.")
45
+ if X.ndim != 2:
46
+ raise ValueError("The input matrix must be a 2 dimensional tensor.")
47
+ if y.ndim != 1 or y.shape[0] != X.shape[0]:
48
+ raise ValueError("The labels must be 1 dimensional with the same number of samples as the input data")
49
+ if X.shape[0] < self.k:
50
+ raise ValueError("There must be atleast k samples.")
51
+ vals = torch.unique(y).numpy()
52
+ if set(vals) != {*range(len(vals))}:
53
+ raise ValueError("y must only contain the values in [0, ..., n_classes - 1].")
54
+
55
+ self.X = X
56
+ self.y = y
57
+
58
+ def predict(self, X):
59
+ """
60
+ Applies the fitted KNNClassifier model to the input data, predicting the labels.
61
+
62
+ Args:
63
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
64
+ Returns:
65
+ labels (torch.Tensor of shape (n_samples,)): The predicted target values corresponding to each sample.
66
+ Raises:
67
+ NotFittedError: If the KNNClassifier model has not been fitted before predicting.
68
+ TypeError: If the input matrix is not a PyTorch tensor.
69
+ ValueError: If the input matrix is not the correct shape.
70
+ """
71
+ if not hasattr(self, "X"):
72
+ raise NotFittedError("KNNClassifier.fit() must be called before predicting.")
73
+ if not isinstance(X, torch.Tensor):
74
+ raise TypeError("The input matrix must be a PyTorch tensor.")
75
+ if X.ndim != 2 or X.shape[1] != self.X.shape[1]:
76
+ raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
77
+
78
+ distances: torch.Tensor = self.metric(self.X.unsqueeze(0), X.unsqueeze(1)) # (len(X), len(self.X))
79
+ indicies = distances.topk(self.k, largest=False).indices
80
+ k_labels = self.y[indicies]
81
+ return torch.mode(k_labels, dim=1).values
82
+
83
+ def predict_proba(self, X):
84
+ """
85
+ Applies the fitted KNNClassifier model to the input data, predicting the probabilities of each class.
86
+
87
+ Args:
88
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
89
+ Returns:
90
+ labels (torch.Tensor of shape (n_samples,) for binary and (n_samples, n_classes) for multiclass classification): The predicted target probabilities corresponding to each sample.
91
+ Raises:
92
+ NotFittedError: If the KNNClassifier model has not been fitted before predicting.
93
+ TypeError: If the input matrix is not a PyTorch tensor.
94
+ ValueError: If the input matrix is not the correct shape.
95
+ """
96
+ if not hasattr(self, "X"):
97
+ raise NotFittedError("KNNClassifier.fit() must be called before predicting.")
98
+ if not isinstance(X, torch.Tensor):
99
+ raise TypeError("The input matrix must be a PyTorch tensor.")
100
+ if X.ndim != 2 or X.shape[1] != self.X.shape[1]:
101
+ raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
102
+
103
+ distances: torch.Tensor = self.metric(self.X.unsqueeze(0), X.unsqueeze(1)) # (len(X), len(self.X))
104
+ indicies = distances.topk(self.k, largest=False).indices
105
+ k_labels = self.y[indicies]
106
+
107
+ all_classes = torch.unique(self.y)
108
+ probs = torch.zeros((len(X), len(all_classes)))
109
+ for i, labels in enumerate(k_labels):
110
+ classes, counts = torch.unique(labels, return_counts=True)
111
+ prob = torch.tensor([(counts[torch.where(classes == _class)[0]] / len(labels) if _class in classes else 0) for _class in all_classes])
112
+ probs[i] = prob
113
+ if len(all_classes) == 2: # binary classification
114
+ probs = probs[:, 1]
115
+ return probs
@@ -0,0 +1,90 @@
1
+ import torch
2
+ from functools import partial
3
+
4
+ from ....Exceptions import NotFittedError
5
+
6
+
7
+ class KNNRegressor:
8
+ """
9
+ The k-nearest neighbors regressor model. Looks for the k closest samples with respect to a distance metric and calculates the average according to some weight function.
10
+
11
+ Args:
12
+ k (int, optional): The number of closest samples considered for the predictions. Must be a positive integer. Defaults to 3.
13
+ metric (str, optional): A distance metric for the closest points. Must be one of "euclidian" or "manhattan". Defaults to "euclidian".
14
+ weight (str, optional): A weight function that decides how important are the nearest k samples. Must be in ["uniform", "distance", "gaussian"]. Defaults to "gaussian".
15
+ """
16
+ def __init__(self, k=3, metric="euclidian", weight="gaussian"):
17
+ if not isinstance(k, int) or k <= 0:
18
+ raise ValueError("k must be a positive integer.")
19
+ if metric not in ["euclidian", "manhattan"]:
20
+ raise ValueError('metric must be on of "euclidian" or "manhattan".')
21
+ if weight not in ["uniform", "distance", "gaussian"]:
22
+ raise ValueError('weight must be in ["uniform", "distance", "gaussian"].')
23
+
24
+ self.k = k
25
+ self.metric = partial(self._metric, metric)
26
+ self.weight = partial(self._weight, weight)
27
+
28
+ def _metric(self, metric, X1, X2):
29
+ if metric == "euclidian":
30
+ return ((X1 - X2) ** 2).sum(dim=2).sqrt()
31
+ elif metric == "manhattan":
32
+ return torch.abs(X1 - X2).sum(dim=2)
33
+
34
+ def _weight(self, weight, distances):
35
+ if weight == "uniform":
36
+ return torch.ones_like(distances)
37
+ elif weight == "distance":
38
+ return 1 / (distances + 1e-10)
39
+ elif weight == "gaussian":
40
+ return torch.exp(-distances ** 2)
41
+
42
+ def fit(self, X, y):
43
+ """
44
+ Fits the KNNRegressor model to the input data by storing the input and target matricies.
45
+
46
+ Args:
47
+ X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
48
+ y (torch.Tensor of shape (n_samples,)): The target values corresponding to each sample.
49
+ Returns:
50
+ None
51
+ Raises:
52
+ TypeError: If the input matrix or the target matrix is not a PyTorch tensor.
53
+ ValueError: If the input matrix or the target matrix is not the correct shape.
54
+ """
55
+ if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
56
+ raise TypeError("The input matrix and the target matrix must be PyTorch tensors.")
57
+ if X.ndim != 2:
58
+ raise ValueError("The input matrix must be a 2 dimensional tensor.")
59
+ if y.ndim != 1 or y.shape[0] != X.shape[0]:
60
+ raise ValueError("The target values must be 1 dimensional with the same number of samples as the input data")
61
+
62
+ self.X = X
63
+ self.y = y
64
+
65
+ def predict(self, X):
66
+ """
67
+ Applies the fitted KNNRegressor model to the input data, predicting the target values.
68
+
69
+ Args:
70
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be regressed.
71
+ Returns:
72
+ targets (torch.Tensor of shape (n_samples,)): The predicted target values corresponding to each sample.
73
+ Raises:
74
+ NotFittedError: If the KNNRegressor model has not been fitted before predicting.
75
+ TypeError: If the input matrix is not a PyTorch tensor.
76
+ ValueError: If the input matrix is not the correct shape.
77
+ """
78
+ if not hasattr(self, "X"):
79
+ raise NotFittedError("KNNRegressor.fit() must be called before predicting.")
80
+ if not isinstance(X, torch.Tensor):
81
+ raise TypeError("The input matrix must be a PyTorch tensor.")
82
+ if X.ndim != 2 or X.shape[1] != self.X.shape[1]:
83
+ raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
84
+
85
+ distances: torch.Tensor = self.metric(self.X.unsqueeze(0), X.unsqueeze(1)) # (len(X), len(self.X))
86
+ indicies = distances.topk(self.k, largest=False).indices
87
+ k_values = self.y[indicies]
88
+ k_distances = distances.gather(1, indicies)
89
+ k_weights = self.weight(k_distances)
90
+ return (k_values * k_weights).sum(dim=1) / (k_weights.sum(dim=1) + 1e-10)
@@ -0,0 +1,2 @@
1
+ from ._KNearestNeighborsClassifier import KNNClassifier
2
+ from ._KNearestNeighborsRegressor import KNNRegressor
@@ -0,0 +1,293 @@
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 SVC:
10
+ """
11
+ The support vector machine classifier. "cvxopt"-optimization method's implementation is largly based on `this article <https://towardsdatascience.com/implement-multiclass-svm-from-scratch-in-python-b141e43dc084#a603>`_. The "smo"-optimization algorithm is based on `this paper <https://www.microsoft.com/en-us/research/uploads/prod/1998/04/sequential-minimal-optimization.pdf>`_.
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
+ opt_method (str, optional): The method that will be used in the optimization step. Must be in ["cvxopt", "smo", "coord_ascent"]. Defaults to "coord_ascent". For optimal results one should experiment with every optimization method, but as a rule of thumb, "coord_ascent" is the most efficient.
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
+ """
21
+ def __init__(self, kernel=RBF(), C=1, opt_method="coord_ascent"):
22
+ if not isinstance(kernel, _Base):
23
+ raise ValueError("kernel must be from DLL.MachineLearning.SupervisedLearning.Kernels")
24
+ if not isinstance(C, float | int) or C <= 0:
25
+ raise ValueError("C must be must be positive real number.")
26
+ if opt_method not in ["cvxopt", "smo", "coord_ascent"]:
27
+ raise ValueError('opt_method must be in ["cvxopt", "smo", "coord_ascent"].')
28
+
29
+ self.kernel = kernel
30
+ self.C = C
31
+ self.opt_method = opt_method
32
+
33
+ self._from_multi = False
34
+ self.tol = 1e-5
35
+
36
+ def _kernel_matrix(self, X1, X2):
37
+ return self.kernel(X1, X2).to(X1.dtype)
38
+
39
+ def fit(self, X, y, epochs=10, multi_method="ovr"):
40
+ """
41
+ Fits the SVC model to the input data by finding the hyperplane that separates the data with maximum margin. It uses the optimization method defined in the constructor of the class.
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
+ epochs (int, optional): The number of iterations the training loop runs. Defaults to 100. Must be a positive integer. Is ignored for opt_method="cvxopt".
47
+ multi_method (str, optional): The method for multi-class classification. Is ignored for binary classification. Must be one of "ovr" or "ovo". Defaults to "ovr".
48
+ Returns:
49
+ None
50
+ Raises:
51
+ TypeError: If the input matrix or the label matrix is not a PyTorch tensor or multi_method is not a string.
52
+ ValueError: If the input matrix or the label matrix is not the correct shape or multi_method is not in allowed methods.
53
+ """
54
+ if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
55
+ raise TypeError("The input matrix and the label matrix must be a PyTorch tensor.")
56
+ if not isinstance(multi_method, str):
57
+ raise TypeError("multi_method must be a string.")
58
+ if X.ndim != 2:
59
+ raise ValueError("The input matrix must be a 2 dimensional tensor.")
60
+ if y.ndim != 1 or y.shape[0] != X.shape[0]:
61
+ raise ValueError("The labels must be 1 dimensional with the same number of samples as the input data")
62
+ if not isinstance(epochs, int) or epochs <= 0:
63
+ raise ValueError("epochs must be a positive integer.")
64
+ if multi_method not in ["ovr", "ovo"]:
65
+ raise ValueError('multi_method must be one of "ovr" or "ovo".')
66
+ vals = torch.unique(y).numpy()
67
+ if set(vals) != {*range(len(vals))}:
68
+ raise ValueError("y must only contain the values in [0, ..., n_classes - 1].")
69
+
70
+ self.n_features = X.shape[1]
71
+
72
+ if len(torch.unique(y)) > 2:
73
+ self.multiclass = True
74
+ return self._multi_fit(X, y, epochs, multi_method)
75
+
76
+ self.multiclass = False
77
+ y = torch.where(y == 0, -1, 1)
78
+ self.y = y
79
+ self.X = X
80
+ n = X.shape[0]
81
+ K = self._kernel_matrix(X, X)
82
+
83
+ if self.opt_method == "cvxopt":
84
+ y_vec = y.reshape((-1, 1)).to(X.dtype)
85
+
86
+ P = cvxopt.matrix((y_vec @ y_vec.T * K).numpy().astype(np.double))
87
+ q = cvxopt.matrix(-np.ones((n, 1)))
88
+ A = cvxopt.matrix((y_vec.T).numpy().astype(np.double))
89
+ b = cvxopt.matrix(np.zeros(1))
90
+ G = cvxopt.matrix(np.vstack((-np.eye(n), np.eye(n))))
91
+ h = cvxopt.matrix(np.vstack((np.zeros((n, 1)), np.ones((n, 1)) * self.C)))
92
+
93
+ cvxopt.solvers.options['show_progress'] = False
94
+ sol = cvxopt.solvers.qp(P, q, G, h, A, b)
95
+ self.alpha = torch.tensor(np.array(sol["x"])).squeeze().to(X.dtype)
96
+
97
+ elif self.opt_method == "coord_ascent":
98
+ alpha = torch.zeros(n, dtype=X.dtype)
99
+
100
+ for _ in range(epochs):
101
+ # process all sample examples sequentially
102
+ for i in range(n):
103
+ new_val = (1 - y[i] * ((alpha * y * K[i]).sum() - alpha[i] * y[i] * K[i, i])) / K[i, i]
104
+ new_val = min(self.C, max(new_val, 0))
105
+ alpha[i] = new_val
106
+ self.alpha = alpha
107
+
108
+ elif self.opt_method == "smo":
109
+ self.alpha = torch.zeros(n, dtype=X.dtype)
110
+ self.b = 0
111
+ epochs_no_change = 2
112
+
113
+ iter_count = 0
114
+ num_no_change_iter = 0
115
+ while iter_count < epochs:
116
+ num_changed_alphas = 0
117
+ for i in range(n):
118
+ E_i = (self.alpha * self.y) @ K[i, :] + self.b - self.y[i]
119
+ if (self.y[i] * E_i < -self.tol and self.alpha[i] < self.C) or (self.y[i] * E_i > self.tol and self.alpha[i] > 0):
120
+ # Select j randomly
121
+ j = self._select_j(i, n)
122
+ E_j = (self.alpha * self.y) @ K[j, :] + self.b - self.y[j]
123
+
124
+ # Compute bounds for α_j
125
+ if self.y[i] == self.y[j]:
126
+ L = torch.max(torch.zeros(1), self.alpha[j] + self.alpha[i] - self.C)
127
+ H = torch.min(torch.tensor(self.C), self.alpha[j] + self.alpha[i])
128
+ else:
129
+ L = torch.max(torch.zeros(1), self.alpha[j] - self.alpha[i])
130
+ H = torch.min(torch.tensor(self.C), self.C + self.alpha[j] - self.alpha[i])
131
+ if L == H:
132
+ continue
133
+
134
+ # Compute η (second derivative of the objective function w.r.t. α_j)
135
+ eta = 2 * K[i, j] - K[i, i] - K[j, j]
136
+ if eta >= 0:
137
+ continue
138
+
139
+ # Update α_j
140
+ alpha_j_old = self.alpha[j].clone()
141
+ self.alpha[j] = self.alpha[j] - (self.y[j] * (E_i - E_j)) / eta
142
+ self.alpha[j] = torch.clamp(self.alpha[j], L, H)
143
+
144
+ if abs(self.alpha[j] - alpha_j_old) < self.tol:
145
+ continue
146
+
147
+ # Update α_i
148
+ alpha_i_old = self.alpha[i].clone()
149
+ self.alpha[i] = self.alpha[i] + self.y[i] * self.y[j] * (alpha_j_old - self.alpha[j])
150
+
151
+ # Update bias term
152
+ b1 = self.b - E_i - self.y[i] * (self.alpha[i] - alpha_i_old) * K[i, i] - self.y[j] * (self.alpha[j] - alpha_j_old) * K[i, j]
153
+ b2 = self.b - E_j - self.y[i] * (self.alpha[i] - alpha_i_old) * K[i, j] - self.y[j] * (self.alpha[j] - alpha_j_old) * K[j, j]
154
+
155
+ if 0 < self.alpha[i] < self.C:
156
+ self.b = b1
157
+ elif 0 < self.alpha[j] < self.C:
158
+ self.b = b2
159
+ else:
160
+ self.b = (b1 + b2) / 2
161
+
162
+ num_changed_alphas += 1
163
+
164
+ if num_changed_alphas == 0:
165
+ num_no_change_iter += 1
166
+ else:
167
+ num_no_change_iter = 0
168
+ if num_no_change_iter >= epochs_no_change:
169
+ break
170
+ iter_count += 1
171
+
172
+ self.is_sv = (self.alpha - self.tol > 0) & (self.alpha <= self.C)
173
+ self.margin_sv = torch.argmax(((0 < self.alpha - self.tol) & (self.alpha < self.C - self.tol)).to(torch.int32))
174
+
175
+ if self.opt_method != "smo":
176
+ x_s, y_s = self.X[self.margin_sv, torch.newaxis], self.y[self.margin_sv]
177
+ alpha_sv, y_sv, X_sv = self.alpha[self.is_sv], self.y[self.is_sv], self.X[self.is_sv]
178
+ self.b = y_s - torch.sum((alpha_sv * y_sv).unsqueeze(-1) * self._kernel_matrix(X_sv, x_s), dim=0)
179
+
180
+ def _select_j(self, i, n):
181
+ j = i
182
+ while j == i:
183
+ j = torch.randint(0, n, (1,)).item()
184
+ return j
185
+
186
+ def _multi_fit(self, X, y, epochs, method):
187
+ self.method = method
188
+ classes = torch.unique(y)
189
+
190
+ if method == "ovr":
191
+ self.n_classes = len(torch.unique(y))
192
+ self.classifiers = []
193
+ for label in classes:
194
+ Xs, ys = X, torch.where(y == label, 1, 0)
195
+ classifier = SVC(kernel=self.kernel, C=self.C, opt_method=self.opt_method)
196
+ classifier.fit(Xs, ys, epochs)
197
+ self.classifiers.append(classifier)
198
+
199
+ elif method == "ovo":
200
+ self.n_classes = len(torch.unique(y))
201
+ self.classifiers = []
202
+ for i in range(self.n_classes):
203
+ for j in range(i+1, self.n_classes):
204
+ class_i, class_j = classes[i], classes[j]
205
+ indices = (y == class_i) | (y == class_j)
206
+ X_subset, y_subset = X[indices], y[indices]
207
+ y_subset = torch.where(y_subset == class_i, 1, 0)
208
+ classifier = SVC(kernel=self.kernel, C=self.C, opt_method=self.opt_method)
209
+ classifier.fit(X_subset, y_subset, epochs)
210
+ self.classifiers.append((classifier, class_i, class_j))
211
+
212
+ def _decision_function(self, K):
213
+ return (self.alpha * self.y) @ K + self.b
214
+
215
+ def predict(self, X):
216
+ """
217
+ Applies the fitted SVC model to the input data, predicting the correct classes.
218
+
219
+ Args:
220
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be classified.
221
+ Returns:
222
+ labels (torch.Tensor of shape (n_samples,)): The predicted labels corresponding to each sample.
223
+ Raises:
224
+ NotFittedError: If the SVC model has not been fitted before predicting.
225
+ TypeError: If the input matrix is not a PyTorch tensor.
226
+ ValueError: If the input matrix is not the correct shape.
227
+ """
228
+ if not hasattr(self, "multiclass"):
229
+ raise NotFittedError("SVC.fit() must be called before predicting.")
230
+ if not isinstance(X, torch.Tensor):
231
+ raise TypeError("The input matrix must be a PyTorch tensor.")
232
+ if X.ndim != 2 or X.shape[1] != self.n_features:
233
+ raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
234
+
235
+ if self.multiclass:
236
+ return self._multi_predict(X)
237
+
238
+ score = self._decision_function(self._kernel_matrix(self.X, X))
239
+ if self._from_multi:
240
+ return score
241
+ return ((torch.sign(score) + 1) / 2).to(torch.int32)
242
+
243
+ def _multi_predict(self, X):
244
+ if self.method == "ovr":
245
+ predictions = torch.zeros((X.shape[0], self.n_classes))
246
+ for i, classifier in enumerate(self.classifiers):
247
+ classifier._from_multi = True
248
+ predictions[:, i] = classifier.predict(X)
249
+ return torch.argmax(predictions, dim=1)
250
+
251
+ elif self.method == "ovo":
252
+ votes = torch.zeros((X.shape[0], self.n_classes))
253
+ for classifier, class_i, class_j in self.classifiers:
254
+ predictions = classifier.predict(X)
255
+ votes[:, class_i] += (predictions == 1) # If prediction is 1, vote for class_i
256
+ votes[:, class_j] += (predictions == 0) # If prediction is 0, vote for class_j
257
+ return torch.argmax(votes, dim=1)
258
+
259
+ def predict_proba(self, X):
260
+ """
261
+ Predicts class probabilities for the given input.
262
+
263
+ Args:
264
+ X (torch.Tensor of shape (n_samples, n_features)): The input data.
265
+
266
+ Returns:
267
+ torch.Tensor of shape (n_samples, n_classes) or (n_samples,): The predicted probabilities.
268
+ """
269
+ if not hasattr(self, "multiclass"):
270
+ raise NotFittedError("SVC.fit() must be called before predicting.")
271
+
272
+ if not self.multiclass:
273
+ scores = self._decision_function(self._kernel_matrix(self.X, X))
274
+ return 1 / (1 + torch.exp(-scores))
275
+
276
+ votes = torch.zeros((X.shape[0], self.n_classes))
277
+ if self.method == "ovr":
278
+ for i, classifier in enumerate(self.classifiers):
279
+ scores = classifier._decision_function(classifier._kernel_matrix(classifier.X, X))
280
+ proba = 1 / (1 + torch.exp(-scores))
281
+ votes[:, i] = proba
282
+ elif self.method == "ovo":
283
+ pairwise_counts = torch.zeros((self.n_classes,), dtype=torch.float32)
284
+ for classifier, class_i, class_j in self.classifiers:
285
+ scores = classifier._decision_function(classifier._kernel_matrix(classifier.X, X))
286
+ proba = 1 / (1 + torch.exp(-scores))
287
+ votes[:, class_i] += proba
288
+ votes[:, class_j] += 1 - proba
289
+ pairwise_counts[class_i] += 1
290
+ pairwise_counts[class_j] += 1
291
+ votes /= pairwise_counts
292
+
293
+ return votes / votes.sum(dim=1, keepdim=True)