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,69 @@
1
+ import torch
2
+
3
+ from ....Exceptions import NotFittedError
4
+
5
+
6
+ class QDA:
7
+ """
8
+ Quadratic discriminant analysis (LDA) class for classification.
9
+
10
+ Attributes:
11
+ n_features (int): The number of features in the input.
12
+ """
13
+
14
+ def fit(self, X, y):
15
+ """
16
+ Fits the QDA model to the input data by calculating the class means and covariances.
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.
20
+ Raises:
21
+ TypeError: If the input matrix is not a PyTorch tensor.
22
+ ValueError: If the input matrix is not the correct shape.
23
+ """
24
+ if not isinstance(X, torch.Tensor):
25
+ raise TypeError("The input matrix must be a PyTorch tensor.")
26
+ if X.ndim != 2 or X.shape[0] == 1:
27
+ raise ValueError("The input matrix must be a 2 dimensional tensor with atleast 2 samples.")
28
+
29
+ self.classes = torch.unique(y)
30
+ self.n_features = X.shape[1]
31
+
32
+ self.class_means = []
33
+ self.class_covariances = []
34
+
35
+ for current_class in self.classes:
36
+ X_c = X[y == current_class]
37
+ self.class_means.append(torch.mean(X_c, dim=0))
38
+ self.class_covariances.append(torch.cov(X_c.T, correction=0))
39
+
40
+ def predict(self, X):
41
+ """
42
+ Applies the fitted QDA model to the input data, predicting the correct classes.
43
+
44
+ Args:
45
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be transformed.
46
+ Returns:
47
+ y (torch.Tensor of shape (n_samples,)): The predicted labels.
48
+ Raises:
49
+ NotFittedError: If the QDA model has not been fitted before transforming.
50
+ """
51
+ if not hasattr(self, "class_means"):
52
+ raise NotFittedError("QDA.fit() must be called before predicting.")
53
+ if not isinstance(X, torch.Tensor):
54
+ raise TypeError("The input matrix must be a PyTorch tensor.")
55
+ if X.ndim != 2 or X.shape[0] == 1:
56
+ raise ValueError("The input matrix must be a 2 dimensional tensor with atleast 2 samples.")
57
+
58
+ min_dists = torch.full((len(X),), torch.inf, dtype=X.dtype)
59
+ y = torch.zeros_like(min_dists, dtype=X.dtype)
60
+
61
+ for current_class, class_mean, class_covariance in zip(self.classes, self.class_means, self.class_covariances):
62
+ diff = X - class_mean
63
+ # mahalanobis = torch.sum(diff @ torch.linalg.lstsq(class_covariance, diff.T).solution, dim=1)
64
+ # mahalanobis = torch.sum(diff @ torch.linalg.inv(class_covariance) * diff, dim=1)
65
+ mahalanobis = torch.sum(torch.linalg.lstsq(class_covariance, diff.T).solution.T * diff, dim=1)
66
+ mask = mahalanobis < min_dists
67
+ y[mask] = current_class
68
+ min_dists[mask] = mahalanobis[mask]
69
+ return y
@@ -0,0 +1,143 @@
1
+ import torch
2
+ from math import sqrt, floor
3
+
4
+ from ....Exceptions import NotFittedError
5
+ from . import PCA
6
+
7
+
8
+ class RobustPCA:
9
+ """
10
+ Robust version of Principal Component Analysis (PCA).
11
+
12
+ Args:
13
+ n_components (int): Number of principal components to keep. The number must be a positive integer.
14
+ method (str, optional): The method used for the robust estimator. Must be one of "mcd" or "decomposition". Defaults to "mcd".
15
+ Attributes:
16
+ components (torch.Tensor): Principal components extracted from the data.
17
+ explained_variance (torch.Tensor): Variance explained by the selected components.
18
+ """
19
+ def __init__(self, n_components=2, method="mcd", epsilon=1e-10):
20
+ if not isinstance(n_components, int) or n_components < 1:
21
+ raise ValueError("n_components must be a positive integer.")
22
+ if method not in ["mcd", "decomposition"]:
23
+ raise ValueError('method must be one of "mcd" or "decomposition".')
24
+
25
+ self.n_components = n_components
26
+ self.method = method
27
+ self.epsilon = epsilon
28
+ self.pca = PCA(n_components, epsilon)
29
+
30
+ def fit(self, X, epochs=1000, normalize=True, proportion=0.5, n_tries=20):
31
+ """
32
+ Fits the RobustPCA model to the input data by calculating the principal components. If method is "decomposition", finds matricies L and S such that X = L + S, where L is low rank and S is sparse. Then applies PCA to L. If method is mcd, tries to find a subset of the data that has the minimum covariance determinant and uses that as an estimate of the covariance.
33
+
34
+ The input data is always centered and if `normalize=True`, also normalized so that the standard deviation is 1 along each axis.
35
+
36
+ Args:
37
+ X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
38
+ epochs (int, optional): Determines how many iterations are used for finding the L and S matricies. Defaults to 1000. Must be a positive integer.
39
+ normalize (bool, optional): Whether to normalize the data before computing the PCA. Defaults to True.
40
+ proportion (float, optional): The proportion of data, which is used to approximate the covariance matrix. Is ignored unless method is "mcd". Must be in range (0, 1). Defaults to 0.5.
41
+ n_tries (int, optional): The amount of attempts of randomly selecting the samples for the covariance estimate. Must be a positive integer. Defaults to 20.
42
+ """
43
+ if not isinstance(X, torch.Tensor):
44
+ raise TypeError("The input matrix must be a PyTorch tensor.")
45
+ if not isinstance(epochs, int) or epochs <= 0:
46
+ raise ValueError("epochs must be positive integer.")
47
+ if not isinstance(normalize, bool):
48
+ raise TypeError("The normalize parameter must be a boolean.")
49
+ if X.ndim != 2 or X.shape[0] == 1:
50
+ raise ValueError("The input matrix must be a 2 dimensional tensor with atleast 2 samples.")
51
+ if not isinstance(proportion, float) or proportion <= 0 or proportion >= 1:
52
+ raise ValueError("proportion must be in range (0, 1).")
53
+ if not isinstance(n_tries, int) or n_tries <= 0:
54
+ raise ValueError("n_tries must be a positive integer.")
55
+
56
+ self.normalize = normalize
57
+ if self.method == "decomposition":
58
+ n1, n2 = X.shape
59
+ mu = n1 * n2 / (4 * torch.sum(torch.abs(X)))
60
+ l = 1 / sqrt(max(n1, n2))
61
+ thresh = 1e-7 * torch.linalg.norm(X, ord="fro")
62
+ L = torch.zeros_like(X)
63
+ S = torch.zeros_like(X)
64
+ Y = torch.zeros_like(X)
65
+ i = 0
66
+ while torch.linalg.norm(X - L - S, ord="fro") > thresh and i < epochs:
67
+ L = self._SVT(X - S + (1 / mu) * Y, 1 / mu)
68
+ S = self._shrink(X - L + (1 / mu) * Y, l / mu)
69
+ Y = Y + mu * (X - L - S)
70
+ i = i + 1
71
+ self.pca.fit(L, normalize, False)
72
+ elif self.method == "mcd":
73
+ covariance, self.mean, self.std = self._min_cov_det(X, proportion, normalize, n_tries)
74
+ self.pca.fit(covariance, normalize, True)
75
+
76
+ def _shrink(self, X, tau):
77
+ return torch.sign(X) * torch.max(torch.abs(X) - tau, torch.zeros_like(X))
78
+
79
+ def _SVT(self, X, tau):
80
+ U, S, Vh = torch.linalg.svd(X, full_matrices=False)
81
+ return U @ self._shrink(torch.diag(S), tau) @ Vh
82
+
83
+ def _min_cov_det(self, X, proportion, normalize, n_tries):
84
+ N = floor(proportion * len(X))
85
+ best_covariance_matrix = torch.cov(X.T)
86
+ lowest_determinant = torch.linalg.det(best_covariance_matrix)
87
+ best_mean = X.mean(dim=0)
88
+ best_std = X.std(dim=0, unbiased=True) if normalize else None
89
+ for _ in range(n_tries):
90
+ indices = torch.randperm(len(X))[:N]
91
+ mean = X[indices].mean(dim=0)
92
+ X_centered = X[indices] - mean
93
+ if normalize:
94
+ std = X_centered.std(dim=0, unbiased=True)
95
+ X_centered = X_centered / (std + self.epsilon)
96
+ covariance = torch.cov(X_centered.T)
97
+ determinant = torch.linalg.det(covariance)
98
+ if determinant < lowest_determinant:
99
+ best_covariance_matrix = covariance
100
+ lowest_determinant = determinant
101
+ best_mean = mean
102
+ if normalize: best_std = std
103
+ return best_covariance_matrix, best_mean, best_std
104
+
105
+ def transform(self, X):
106
+ """
107
+ Applies the fitted RobustPCA model to the input data, transforming it into the reduced feature space.
108
+
109
+ Args:
110
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be transformed.
111
+ Returns:
112
+ X_new (torch.Tensor of shape (n_samples, n_components)): The data transformed into the principal component space.
113
+ Raises:
114
+ NotFittedError: If the RobustPCA model has not been fitted before transforming.
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 not hasattr(self.pca, "from_covariance"):
119
+ raise NotFittedError("RobustPCA.fit() must be called before transforming.")
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.pca.components.shape[1]:
123
+ raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
124
+
125
+ if self.method == "mcd":
126
+ X = X - self.mean
127
+ if self.normalize: X = X / (self.std + self.epsilon)
128
+ return self.pca.transform(X)
129
+
130
+ def fit_transform(self, X, epochs=1000, normalize=True):
131
+ """
132
+ First finds the principal components of X and then transforms X to fitted space.
133
+
134
+ Args:
135
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be transformed.
136
+ epochs (int, optional): Determines how many iterations are used for finding the L and S matricies. Defaults to 1000. Must be a positive integer.
137
+ normalize (bool, optional): Whether to normalize the data before computing the PCA. Defaults to True.
138
+ Returns:
139
+ X_new (torch.Tensor of shape (n_samples, n_components)): The data transformed into the principal component space.
140
+ """
141
+
142
+ self.fit(X, epochs=epochs, normalize=normalize)
143
+ return self.transform(X)
@@ -0,0 +1,151 @@
1
+ import torch
2
+
3
+ from . import PCA
4
+ from ....DeepLearning.Optimisers import SGD
5
+
6
+
7
+ class TSNE:
8
+ """
9
+ T-distributed Stochastic Neighbor Embedding (T-SNE) class for dimensionality reduction. This implementation is based on `this paper <https://jmlr.org/papers/volume9/vandermaaten08a/vandermaaten08a.pdf>`_ and `this article <https://towardsdatascience.com/t-sne-from-scratch-ft-numpy-172ee2a61df7/>`_. The main difference is that this implementation uses vectorized matrix operations making it considerably faster than the loop approach used in the article.
10
+
11
+ Args:
12
+ n_components (int): Number of principal components to keep. The number must be a positive integer.
13
+ init (str, optional): The method for initializing the embedding. Must be in ``["pca", "random"]``. Defaults to ``"pca"``.
14
+ p (int, optional): The order of the chosen metric. Must be a positive integer. Defaults to 2, which corresponds to the Euclidian metric.
15
+ early_exaggeration (float | int, optional): Determines how far apart the clusters are in the embedding space. Must be a positive real number. Defaults to 12.0.
16
+ perplexity (float | int, optional): Determines how far can samples be from one another to be considered neighbors. Must be a positive real number. Defaults to 30.0. One should consider using something between 5 and 50 to begin with.
17
+ learning_rate (float | int, optional): Determines how long steps do we take towards the gradient. Must be a positive real number. It is recommended to use a value between 10.0 and 1000.0. Defaults to ``"auto"``, where we use a value of ``max(n_samples / (4 * early_exaggeration), 50)``.
18
+
19
+ Attributes:
20
+ history (list[float]): The history of KL-divergence loss function each epoch. Available after fitting the model.
21
+ """
22
+ def __init__(self, n_components=2, init="pca", p=2, early_exaggeration=12.0, perplexity=30.0, learning_rate="auto"):
23
+ if not isinstance(n_components, int) or n_components < 1:
24
+ raise ValueError("n_components must be a positive integer.")
25
+ if not init in ["pca", "random"]:
26
+ raise ValueError('init must be in ["pca", "random"].')
27
+ if not isinstance(p, int) or p <= 0:
28
+ raise ValueError("p must be a positive integer.")
29
+ if not isinstance(early_exaggeration, float | int) or early_exaggeration <= 0:
30
+ raise ValueError("early_exaggeration must be a positive real number.")
31
+ if not isinstance(perplexity, float | int) or perplexity <= 0:
32
+ raise ValueError("perplexity must be a positive real number.")
33
+ if (learning_rate != "auto" and (not isinstance(learning_rate, float | int) or learning_rate <= 0)):
34
+ raise ValueError('learning_rate must be a positive real number or "auto".')
35
+
36
+ self.n_components = n_components
37
+ self.init = init
38
+ self.p = p
39
+ self.early_exaggeration = early_exaggeration
40
+ self.perplexity = perplexity
41
+ self.learning_rate = learning_rate
42
+
43
+ def _initialize(self, X):
44
+ if self.init == "pca":
45
+ return PCA(n_components=self.n_components).fit_transform(X)
46
+ else:
47
+ return torch.normal(0, 1e-4, size=(len(X), self.n_components))
48
+
49
+ def _symmetric_affinities(self, X):
50
+ affinities = self._pairwise_affinities(X)
51
+ p_ij_symmetric = (affinities + affinities.T) / (2 * len(affinities))
52
+ eps = torch.tensor(torch.finfo(p_ij_symmetric.dtype).tiny)
53
+ p_ij_symmetric = torch.maximum(p_ij_symmetric, eps)
54
+ return p_ij_symmetric
55
+
56
+ def _pairwise_affinities(self, X):
57
+ diff = X.unsqueeze(1) - X.unsqueeze(0)
58
+ norm = torch.norm(diff, dim=2, p=self.p)
59
+ variance = self._grid_search(norm)
60
+ affinities = torch.exp(-norm ** self.p / (2 * variance[:, None] ** 2))
61
+ affinities.fill_diagonal_(0)
62
+ row_sums = affinities.sum(dim=1, keepdim=True)
63
+ zero_rows = row_sums == 0
64
+ eps = torch.finfo(affinities.dtype).tiny
65
+ row_sums[zero_rows] = eps
66
+ affinities /= row_sums
67
+ affinities[affinities == 0] = eps
68
+ return affinities
69
+
70
+ def _grid_search(self, norm):
71
+ n_samples = len(norm)
72
+ std_norm = torch.std(norm, dim=1, keepdim=True)
73
+ resolution = 200
74
+ sigma_search_values = torch.linspace(0.01, 5.0, resolution)
75
+ sigma_search_values = std_norm * sigma_search_values
76
+ p_matrix = torch.exp(-norm.unsqueeze(2) ** self.p / (2 * sigma_search_values.pow(2).unsqueeze(1)))
77
+ eps = torch.tensor(torch.finfo(p_matrix.dtype).tiny)
78
+ p_matrix[torch.eye(n_samples, dtype=torch.bool)] = eps # do not set to 0 to avoid division by zero errors
79
+ p_new_matrix = torch.maximum(p_matrix / p_matrix.sum(dim=1, keepdim=True), eps)
80
+ H_values = -torch.sum(p_new_matrix * torch.log2(p_new_matrix), dim=1)
81
+ result = torch.abs(torch.log(torch.tensor(self.perplexity)) - H_values * torch.log(torch.tensor(2.0)))
82
+ sigma_indices = torch.argmin(result, dim=1)
83
+ return sigma_search_values[torch.arange(n_samples), sigma_indices]
84
+
85
+ def _low_dimensional_affinities(self, low_dimensional_representation):
86
+ diff = low_dimensional_representation.unsqueeze(1) - low_dimensional_representation.unsqueeze(0)
87
+ norm = torch.norm(diff, dim=2, p=self.p)
88
+ result = (1 + norm ** self.p).pow(-1)
89
+ result.fill_diagonal_(0)
90
+ result_sum = result.sum()
91
+ result = result / result_sum if result_sum > 0 else result
92
+ eps = torch.tensor(torch.finfo(result.dtype).tiny)
93
+ return torch.maximum(result, eps)
94
+
95
+ def _gradient(self, affinities, low_dimensional_affinities, low_dimensional_representation):
96
+ diff = low_dimensional_representation.unsqueeze(1) - low_dimensional_representation.unsqueeze(0)
97
+ A = affinities - low_dimensional_affinities
98
+ B = (1 + torch.norm(diff, dim=2, p=self.p)).pow(-1)
99
+ return 4 * torch.sum(((A * B).unsqueeze(2) * diff), dim=1)
100
+
101
+ def fit_transform(self, X, epochs=100, verbose=False):
102
+ """
103
+ Fits the T-SNE model to the input data.
104
+
105
+ Args:
106
+ X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
107
+ epochs (int, optional): The number of training epochs after early exaggeration. Must be a positive integer. Defaults to 100. Due to early exaggeration, the embedding is updated epochs + 250 times.
108
+ verbose (bool, optional): Determines if the loss is printed each epoch. Must be a boolean. Defaults to ``False``.
109
+ Returns:
110
+ embedding (torch.tensor): The embedded samples of shape (n_samples, n_components).
111
+ """
112
+ if not isinstance(X, torch.Tensor):
113
+ raise TypeError("The input matrix must be a PyTorch tensor.")
114
+ if X.ndim != 2 or X.shape[0] == 1:
115
+ raise ValueError("The input matrix must be a 2 dimensional tensor with atleast 2 samples.")
116
+ if not isinstance(epochs, int) or epochs <= 0:
117
+ raise ValueError("epochs must be a positive integer.")
118
+ if not isinstance(verbose, bool):
119
+ raise ValueError("verbose must be a boolean.")
120
+
121
+ result = self._initialize(X)
122
+ symmetric_affinities = self._symmetric_affinities(X)
123
+ learning_rate = max(len(X) / (4 * self.early_exaggeration), 50) if self.learning_rate == "auto" else self.learning_rate
124
+ optimiser = SGD(learning_rate=learning_rate, momentum=0.5)
125
+ optimiser.initialise_parameters([result])
126
+ self.history = []
127
+ early_exaggeration = self.early_exaggeration
128
+
129
+ for epoch in range(epochs + 250):
130
+ if epoch > 250:
131
+ optimiser.momentum = 0.8
132
+ early_exaggeration = 1
133
+ low_dimensional_affinities = self._low_dimensional_affinities(result)
134
+ result.grad = self._gradient(early_exaggeration * symmetric_affinities, low_dimensional_affinities, result)
135
+ loss = torch.sum(symmetric_affinities * torch.log(symmetric_affinities / low_dimensional_affinities))
136
+ self.history.append(loss)
137
+ if verbose: print(f"Epoch: {epoch + 1} - KL-divergence loss: {loss:.3f}")
138
+ optimiser.update_parameters()
139
+
140
+ return result
141
+
142
+ def fit(self, X, epochs=100, verbose=False):
143
+ """
144
+ Wrapper for the TSNE.fit_transform(X) method.
145
+
146
+ Args:
147
+ X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
148
+ epochs (int, optional): The number of training epochs after early exaggeration. Must be a positive integer. Defaults to 100.
149
+ verbose (bool, optional): Determines if the loss is printed each epoch. Must be a boolean. Defaults to ``False``.
150
+ """
151
+ return self.fit_transform(X, epochs, verbose)
@@ -0,0 +1,176 @@
1
+ import torch
2
+ from scipy.optimize import curve_fit
3
+ from functools import partial
4
+
5
+ from . import PCA
6
+ from ....DeepLearning.Optimisers import SGD
7
+ from ...SupervisedLearning.Kernels import RBF
8
+
9
+
10
+ class UMAP:
11
+ """
12
+ Uniform Manifold Approximation and Projection (UMAP) class for dimensionality reduction. This implementation is based on `this paper <https://arxiv.org/pdf/1802.03426>`_ and `this article <https://github.com/NikolayOskolkov/HowUMAPWorks/blob/master/HowUMAPWorks.ipynb>`_.
13
+
14
+ Args:
15
+ n_components (int): Number of principal components to keep. The number must be a positive integer.
16
+ init (str, optional): The method for initializing the embedding. Must be in ``["spectral", "pca", "random"]``. Defaults to ``"spectral"``.
17
+ p (int, optional): The order of the chosen metric. Must be a positive integer. Defaults to 2, which corresponds to the Euclidian metric.
18
+ n_neighbor (int, optional): Controls how UMAP balances local and global structure in data. The larger this parameter is the better the global structure is conserved. A small value conserves fine details well, but may lose global structure. Must be a positive integer. Defaults to 15.
19
+ min_dist (float | int, optional): Controls the minimum distance between samples in the low dimensional space. Must be a non-negative real number. Defaults to 0.25.
20
+ learning_rate (float | int, optional): Determines how long steps do we take towards the gradient. Must be a positive real number. Defaults to 1.
21
+
22
+ Attributes:
23
+ history (list[float]): The history of the cross entropy loss function each epoch. Available after fitting the model.
24
+ """
25
+ def __init__(self, n_components=2, init="spectral", p=2, n_neighbor=15, min_dist=0.25, learning_rate=1):
26
+ if not isinstance(n_components, int) or n_components < 1:
27
+ raise ValueError("n_components must be a positive integer.")
28
+ if not init in ["spectral", "pca", "random"]:
29
+ raise ValueError('init must be in ["spectral", "pca", "random"].')
30
+ if not isinstance(p, int) or p <= 0:
31
+ raise ValueError("p must be a positive integer.")
32
+ if not isinstance(n_neighbor, int) or n_neighbor <= 0:
33
+ raise ValueError("n_neighbor must be a positive integer.")
34
+ if not isinstance(min_dist, int | float) or min_dist < 0:
35
+ raise ValueError("min_dist must be a non-negative real number.")
36
+ if (not isinstance(learning_rate, float | int) or learning_rate <= 0):
37
+ raise ValueError('learning_rate must be a positive real number.')
38
+
39
+ self.n_components = n_components
40
+ self.init = init
41
+ self.p = p
42
+ self.learning_rate = learning_rate
43
+ self.n_neighbor = n_neighbor
44
+ self.min_dist = min_dist
45
+ self.a, self.b = self._solve_params()
46
+
47
+ def _solve_params(self):
48
+ def func(d, a, b):
49
+ return 1 / (1 + a * (d + 1e-10) ** (2 * b))
50
+
51
+ x = torch.linspace(0, 3, 300)
52
+ y = torch.full_like(x, 1)
53
+ mask = x > self.min_dist
54
+ y[mask] = torch.exp(-x[mask] + self.min_dist)
55
+ p, _ = curve_fit(func, x.numpy(), y.numpy())
56
+ return torch.tensor(p[0], dtype=torch.float32), torch.tensor(p[1], dtype=torch.float32) # a, b
57
+
58
+ def _initialize(self, X):
59
+ if self.init == "pca":
60
+ return PCA(n_components=self.n_components).fit_transform(X)
61
+ elif self.init == "spectral":
62
+ W = RBF()(X, X)
63
+ D = torch.diag(W.sum(dim=1))
64
+ L = D - W
65
+ _, eigenvectors = torch.linalg.eigh(L)
66
+ embedding = eigenvectors[:, 1:self.n_components+1]
67
+ embedding /= torch.linalg.norm(embedding, dim=1, keepdim=True) + 1e-10
68
+ return embedding
69
+ else:
70
+ return torch.normal(0, 1e-4, size=(len(X), self.n_components))
71
+
72
+ def _pairwise_affinities(self, norm, rho, sigma, row):
73
+ d = norm[row] - rho[row]
74
+ d[d < 0] = 0
75
+ return torch.exp(- d / sigma)
76
+
77
+ def _k(self, prob):
78
+ return 2 ** torch.sum(prob)
79
+
80
+ def _sigma(self, k_of_sigma, fixed_k):
81
+ sigma_lower_limit = 0
82
+ sigma_upper_limit = 1000
83
+ for _ in range(20):
84
+ approx_sigma = (sigma_lower_limit + sigma_upper_limit) / 2
85
+ if k_of_sigma(approx_sigma) < fixed_k:
86
+ sigma_lower_limit = approx_sigma
87
+ else:
88
+ sigma_upper_limit = approx_sigma
89
+ if torch.abs(fixed_k - k_of_sigma(approx_sigma)) <= 1e-5:
90
+ break
91
+ return approx_sigma
92
+
93
+ def _local_fuzzy_simplicial_set(self, norm, rho):
94
+ n = len(norm)
95
+ prob = torch.zeros((n, n))
96
+ for dist_row in range(n):
97
+ _func = partial(self._k_of_sigma, norm, rho, dist_row)
98
+ prob[dist_row] = self._pairwise_affinities(norm, rho, self._sigma(_func, self.n_neighbor), dist_row)
99
+ return prob
100
+
101
+ def _k_of_sigma(self, norm, rho, dist_row, sigma):
102
+ return self._k(self._pairwise_affinities(norm, rho, sigma, dist_row))
103
+
104
+ def _symmetric_affinities(self, X):
105
+ diff = X.unsqueeze(1) - X.unsqueeze(0)
106
+ norm = torch.norm(diff, dim=2, p=self.p) ** 2
107
+ rho = [sorted(norm[i])[1] for i in range(len(norm))] # distance to nearest neighbor
108
+ affinities = self._local_fuzzy_simplicial_set(norm, rho)
109
+ return (affinities + affinities.T) / 2 # suggested by article
110
+ # return affinities + affinities.T - affinities * affinities.T # suggested by paper
111
+
112
+ def _low_dimensional_affinities(self, low_dimensional_representation):
113
+ diff = low_dimensional_representation.unsqueeze(1) - low_dimensional_representation.unsqueeze(0)
114
+ norm = torch.norm(diff, dim=2, p=self.p)
115
+ inv_distances = 1 / (1 + self.a * norm ** (2 * self.b))
116
+ return inv_distances
117
+
118
+ def _CE(self, symmetric_affinities, low_dimensional_representation):
119
+ low_dimensional_affinities = self._low_dimensional_affinities(low_dimensional_representation)
120
+ return torch.mean(-symmetric_affinities * torch.log(low_dimensional_affinities + 1e-5) - (1 - symmetric_affinities) * torch.log(1 - low_dimensional_affinities + 1e-5))
121
+
122
+ def _CE_gradient(self, symmetric_affinities, low_dimensional_representation):
123
+ diff = low_dimensional_representation.unsqueeze(1) - low_dimensional_representation.unsqueeze(0)
124
+ norm = torch.norm(diff, dim=2, p=self.p) ** 2
125
+ inv_dist = 1 / (1 + self.a * norm ** self.b)
126
+ Q = (1 - symmetric_affinities) @ (1 / (norm + 1e-5))
127
+ Q.fill_diagonal_(0)
128
+ Q /= torch.sum(Q, dim=1, keepdims=True)
129
+ fact = (self.a * symmetric_affinities * (1e-8 + norm) ** (self.b - 1) - Q).unsqueeze(2)
130
+ return 2 * self.b * torch.sum(fact * diff * inv_dist.unsqueeze(2), dim=1)
131
+
132
+ def fit_transform(self, X, epochs=100, verbose=False):
133
+ """
134
+ Fits the UMAP model to the input data.
135
+
136
+ Args:
137
+ X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
138
+ epochs (int, optional): The number of training epochs. Must be a positive integer. Defaults to 100.
139
+ verbose (bool, optional): Determines if the loss is printed each epoch. Must be a boolean. Defaults to ``False``.
140
+ Returns:
141
+ embedding (torch.tensor): The embedded samples of shape (n_samples, n_components).
142
+ """
143
+ if not isinstance(X, torch.Tensor):
144
+ raise TypeError("The input matrix must be a PyTorch tensor.")
145
+ if X.ndim != 2 or X.shape[0] == 1:
146
+ raise ValueError("The input matrix must be a 2 dimensional tensor with atleast 2 samples.")
147
+ if not isinstance(epochs, int) or epochs <= 0:
148
+ raise ValueError("epochs must be a positive integer.")
149
+ if not isinstance(verbose, bool):
150
+ raise ValueError("verbose must be a boolean.")
151
+
152
+ result = self._initialize(X)
153
+ symmetric_affinities = self._symmetric_affinities(X)
154
+ optimiser = SGD(learning_rate=self.learning_rate, momentum=0.5)
155
+ optimiser.initialise_parameters([result])
156
+ self.history = []
157
+
158
+ for epoch in range(epochs):
159
+ result.grad = self._CE_gradient(symmetric_affinities, result)
160
+ optimiser.update_parameters()
161
+ loss = self._CE(symmetric_affinities, result)
162
+ self.history.append(loss)
163
+ if verbose: print(f"Epoch: {epoch + 1} - Cross-Entropy loss: {loss:.3f}")
164
+
165
+ return result
166
+
167
+ def fit(self, X, epochs=100, verbose=False):
168
+ """
169
+ Wrapper for the TSNE.fit_transform(X) method.
170
+
171
+ Args:
172
+ X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
173
+ epochs (int, optional): The number of training epochs after early exaggeration. Must be a positive integer. Defaults to 100.
174
+ verbose (bool, optional): Determines if the loss is printed each epoch. Must be a boolean. Defaults to ``False``.
175
+ """
176
+ return self.fit_transform(X, epochs, verbose)
@@ -0,0 +1,6 @@
1
+ from ._PCA import PCA
2
+ from ._LDA import LDA
3
+ from ._RobustPCA import RobustPCA
4
+ from ._TSNE import TSNE
5
+ from ._UMAP import UMAP
6
+ from ._QDA import QDA