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,147 @@
1
+ import torch
2
+
3
+ from . import LinearRegression
4
+
5
+
6
+ class SARIMA:
7
+ """
8
+ The Seasonal auto regressive moving average model for time series analysis.
9
+
10
+ Args:
11
+ series (torch.Tensor of shape (n_samples,)): The time series for fitting. Must be one dimensional.
12
+ order (tuple of ints): The orders of the non-seasonal parts. Follows the format (p, d, q).
13
+ seasonal_order (tuple of ints): The orders of the seasonal parts. Follows the format (P, D, Q, S). If a seasonal component is not needed, the seasonal order should be put as (0, 0, 0, 1).
14
+ """
15
+ def __init__(self, series, order, seasonal_order):
16
+ if not isinstance(series, torch.Tensor) or series.ndim != 1:
17
+ raise TypeError("series must be a one-dimensional torch tensor.")
18
+ if not isinstance(order, tuple | list) or len(order) != 3:
19
+ raise TypeError("order must be a tuple of length 3.")
20
+ if any([not isinstance(val, int) or val < 0 for val in order]):
21
+ raise ValueError("order must only contain non-negative integers.")
22
+ if not isinstance(seasonal_order, tuple | list) or len(seasonal_order) != 4:
23
+ raise TypeError("seasonal_order must be a tuple of length 4.")
24
+ if any([not isinstance(val, int) or val < 0 for val in seasonal_order]):
25
+ raise ValueError("seasonal_order must only contain non-negative integers.")
26
+
27
+ self.p, self.d, self.q = order
28
+ self.P, self.D, self.Q, self.S = seasonal_order
29
+
30
+ self.series = series
31
+ self._discarded = {}
32
+ if self.d > 0: series = self._differentiate(series, order=self.d)
33
+ if self.D > 0: series = self._differentiate(series, lag=self.S, order=self.D)
34
+ self.diff_series = series
35
+
36
+ min_length = max(self.p, self.q, self.P * self.S, self.Q * self.S)
37
+ if len(self.diff_series) <= min_length:
38
+ raise ValueError(f"Differentiated series' length {len(self.diff_series)} is less than or equal minimum required length {min_length} for the given orders.")
39
+
40
+ def fit(self):
41
+ """
42
+ Fits the ARMA model to the given time series. Currently, the function fits two linear regression models separately for the AR and MA components.
43
+
44
+ Note:
45
+ This approach is suboptimal for the MA component, as it should be fitted using Kalman filters for correctness.
46
+ """
47
+ residuals = self._fit_ar()
48
+ self._fit_ma(residuals)
49
+
50
+ def _fit_ar(self):
51
+ X, y = self._train_data(part="ar")
52
+
53
+ self.ar_model = LinearRegression()
54
+ self.ar_model.fit(X, y)
55
+ return y - self.ar_model.predict(X)
56
+
57
+ def _fit_ma(self, residuals):
58
+ X, _ = self._train_data(part="ma")
59
+ length = min(len(X), len(residuals))
60
+ X, residuals = X[-length:], residuals[-length:]
61
+
62
+ self.ma_model = LinearRegression()
63
+ self.ma_model.fit(X, residuals)
64
+
65
+ def _train_data(self, part):
66
+ if part == "ar":
67
+ X_series, X_targets = self._lagged_terms(order=self.p)
68
+ seasonal_X_series, seasonal_X_targets = self._lagged_terms(order=self.P, lag=self.S)
69
+ elif part == "ma":
70
+ X_series, X_targets = self._lagged_terms(order=self.q)
71
+ seasonal_X_series, seasonal_X_targets = self._lagged_terms(order=self.Q, lag=self.S)
72
+ length = min(len(X_series), len(seasonal_X_series))
73
+ X = torch.cat((X_series[-length:], seasonal_X_series[-length:]), dim=1)
74
+ y = X_targets if len(X_targets) < len(seasonal_X_targets) else seasonal_X_targets
75
+ return X, y
76
+
77
+ def _lagged_terms(self, order, lag=1):
78
+ points = []
79
+ for i in range(lag * order, len(self.diff_series)):
80
+ indicies = i - lag * torch.arange(1, order + 1)
81
+ features = self.diff_series[indicies]
82
+ points.append(features)
83
+ targets = self.diff_series[lag * order:]
84
+ return torch.stack(points, dim=0), targets
85
+
86
+ def _differentiate(self, series, lag=1, order=1):
87
+ discarded = []
88
+ for _ in range(order):
89
+ discarded.append(series[:lag])
90
+ series = series[lag:] - series[:-lag]
91
+ self._discarded[lag] = torch.stack(discarded, dim=0)
92
+ return series
93
+
94
+ def _integrate(self, differenced, lag=1, order=1):
95
+ for j in range(order):
96
+ restored = torch.zeros(len(differenced) + lag)
97
+ restored[:lag] = self._discarded[lag][-j - 1]
98
+ for i in range(lag, len(restored)):
99
+ restored[i] = differenced[i - lag] + restored[i - lag]
100
+ differenced = restored
101
+ return differenced
102
+
103
+ def predict(self, steps=1, fit_between_steps=False):
104
+ """
105
+ Predicts the next values of the given time series.
106
+
107
+ Args:
108
+ steps (int, optional): The number of next values to predict. Must be a positive integer. Defaults to 1.
109
+ fit_between_steps (bool, optional): Determines if the model should be refitted between each prediction. Defaults to False.
110
+
111
+ Returns:
112
+ torch.Tensor: The predicted values as a one-dimensional torch Tensor.
113
+ """
114
+ if not isinstance(steps, int) or steps <= 0:
115
+ raise ValueError("steps must be a positive integer.")
116
+ if not isinstance(fit_between_steps, bool):
117
+ raise TypeError("fit_between_steps must be a boolean.")
118
+
119
+ diff_series = self.diff_series.clone()
120
+ original_diff_series = self.diff_series.clone()
121
+ for _ in range(steps):
122
+ pred = self._predict_next(diff_series)
123
+ diff_series = torch.cat((diff_series, pred), dim=0)
124
+
125
+ if fit_between_steps:
126
+ self.diff_series = diff_series
127
+ residuals = self._fit_ar()
128
+ self._fit_ma(residuals)
129
+
130
+ if self.D > 0: diff_series = self._integrate(diff_series, lag=self.S, order=self.D)
131
+ if self.d > 0: diff_series = self._integrate(diff_series, order=self.d)
132
+ self.diff_series = original_diff_series
133
+ result = diff_series
134
+ return result[-steps:]
135
+
136
+ def _predict_next(self, diff_series):
137
+ indicies = -torch.arange(1, self.p + 1)
138
+ indicies = torch.cat((-self.S * torch.arange(1, self.P + 1), indicies), dim=0)
139
+ X_ar = diff_series[indicies].unsqueeze(0)
140
+
141
+ indicies = -torch.arange(1, self.q + 1)
142
+ indicies = torch.cat((-self.S * torch.arange(1, self.Q + 1), indicies), dim=0)
143
+ X_ma = diff_series[indicies].unsqueeze(0)
144
+
145
+ ar_pred = self.ar_model.predict(X_ar)
146
+ ma_correction = self.ma_model.predict(X_ma)
147
+ return ar_pred + ma_correction
@@ -0,0 +1,161 @@
1
+ import torch
2
+ from math import floor
3
+
4
+ from ....Data.Metrics import calculate_metrics, _round_dictionary
5
+ from ....Exceptions import NotFittedError
6
+
7
+
8
+ class ElasticNetRegression:
9
+ """
10
+ Implements a linear regression model with L1 and L2 regularization.
11
+
12
+ Args:
13
+ alpha (int | float, optional): The regularization parameter. Larger alpha will force the l1 and l2 norms of the weights to be lower. Must be a non-negative real number. Defaults to 1.
14
+ l1_ratio (int | float, optional): The proportion of l1 regularisation compared to l2 regularisation. Must be in the range [0, 1]. Defaults to 0.5.
15
+ loss (:ref:`losses_section_label`, optional): A loss function used for training the model. Defaults to the mean squared error.
16
+
17
+ Attributes:
18
+ n_features (int): The number of features. Available after fitting.
19
+ weights (torch.Tensor of shape (n_features + 1,)): The weights of the linear regression model. The first element is the bias of the model. Available after fitting.
20
+ residuals (torch.Tensor of shape (n_samples,)): The residuals of the fitted model. For a good fit, the residuals should be normally distributed with zero mean and constant variance. Available after fitting.
21
+ """
22
+ def __init__(self, alpha=1.0, l1_ratio=0.5):
23
+ if not isinstance(alpha, int | float) or alpha < 0:
24
+ raise ValueError("alpha must be a non-negative real number.")
25
+ if not isinstance(l1_ratio, int | float) or not ((0 <= l1_ratio) and (l1_ratio <= 1)):
26
+ raise ValueError("l1_ratio must be in range [0, 1].")
27
+
28
+ self.alpha = alpha
29
+ self.l1_ratio = l1_ratio
30
+
31
+ def fit(self, X, y, sample_weight=None, val_data=None, epochs=100, callback_frequency=1, metrics=["loss"], verbose=False):
32
+ """
33
+ Fits the ElasticNetRegression model to the input data by minimizing the mean squared error loss function using cyclic coordinate-wise descent from `this paper <https://aaltodoc.aalto.fi/server/api/core/bitstreams/091c41eb-7348-48d3-b25d-c8de25fbab03/content>`_.
34
+
35
+ Args:
36
+ X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
37
+ y (torch.Tensor of shape (n_samples,)): The target values corresponding to each sample.
38
+ val_data (tuple[X_val, y_val] | None, optional): Optional validation samples. If None, no validation data is used. Defaults to None.
39
+ epochs (int, optional): The number of training iterations. Must be a positive integer. Defaults to 100.
40
+ callback_frequency (int, optional): The number of iterations between printing info from training. Must be a positive integer. Defaults to 1, which means that every iteration, info is printed assuming verbose=True.
41
+ metrics (list[str], optional): The metrics that will be tracked during training. Defaults to ["loss"].
42
+ verbose (bool, optional): If True, prints info of the chosen metrics during training. Defaults to False.
43
+ Returns:
44
+ history (dict[str, torch.Tensor], each tensor is floor(epochs / callback_frequency) long.): A dictionary tracking the evolution of selected metrics at intervals defined by callback_frequency.
45
+ Raises:
46
+ TypeError: If the input matrix or the target matrix is not a PyTorch tensor or if other parameters are of wrong type.
47
+ ValueError: If the input matrix or the target matrix is not the correct shape or if other parameters have incorrect values.
48
+ """
49
+ if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
50
+ raise TypeError("The input matrix and the target matrix must be a PyTorch tensor.")
51
+ if X.ndim != 2:
52
+ raise ValueError("The input matrix must be a 2 dimensional tensor.")
53
+ if y.ndim != 1 or y.shape[0] != X.shape[0]:
54
+ raise ValueError("The targets must be 1 dimensional with the same number of samples as the input data")
55
+ if not isinstance(val_data, tuple) and val_data is not None:
56
+ raise TypeError("val_data must either be a tuple containing validation samples or None.")
57
+ if isinstance(val_data, tuple) and len(val_data) != 2:
58
+ raise ValueError("val_data must contain both X_val and y_val.")
59
+ if isinstance(val_data, tuple) and len(val_data) == 2 and (val_data[0].ndim != 2 or val_data[1].ndim != 1 or val_data[0].shape[1] != X.shape[1] or len(val_data[0]) != len(val_data[1])):
60
+ raise ValueError("X_val and y_val must be of correct shape.")
61
+ if not isinstance(epochs, int) or epochs <= 0:
62
+ raise ValueError("epochs must be a positive integer.")
63
+ if not isinstance(callback_frequency, int) or callback_frequency <= 0:
64
+ raise ValueError("callback_frequency must be a positive integer.")
65
+ if not isinstance(metrics, list | tuple):
66
+ raise TypeError("metrics must be a list or a tuple containing the strings of wanted metrics.")
67
+ if not isinstance(verbose, bool):
68
+ raise TypeError("verbose must be a boolean.")
69
+ if not isinstance(sample_weight, torch.Tensor) and sample_weight is not None:
70
+ raise TypeError("sample_weight must be torch.Tensor or None.")
71
+ if isinstance(sample_weight, torch.Tensor) and (sample_weight.ndim != 1 or len(X) != len(sample_weight)):
72
+ raise ValueError("sample_weight must be of shape (n_samples,)")
73
+
74
+ # self.n_features = X.shape[1]
75
+ # history = {metric: torch.zeros(floor(epochs / callback_frequency)) for metric in metrics}
76
+ # batch_size = len(X) if batch_size is None else batch_size
77
+ # data_reader = DataReader(X, y, batch_size=batch_size, shuffle=shuffle_data, shuffle_every_epoch=shuffle_every_epoch)
78
+
79
+ # self.weights = torch.randn((self.n_features,))
80
+ # self.bias = torch.zeros((1,))
81
+ # optimiser = ADAM() if optimiser is None else optimiser
82
+ # optimiser.initialise_parameters([self.weights, self.bias])
83
+
84
+ # for epoch in range(epochs):
85
+ # for x_batch, y_batch in data_reader.get_data():
86
+ # predictions = self.predict(x_batch)
87
+ # dCdy = self.loss.gradient(predictions, y_batch)
88
+ # dCdweights = (x_batch.T @ dCdy) + self.alpha * self.l1_ratio * torch.sign(self.weights) + self.alpha * (1 - self.l1_ratio) * self.weights
89
+ # dCdbias = dCdy.mean(dim=0, keepdim=True)
90
+ # self.weights.grad = dCdweights
91
+ # self.bias.grad = dCdbias
92
+ # optimiser.update_parameters()
93
+ # if epoch % callback_frequency == 0:
94
+ # values = calculate_metrics(data=(self.predict(X), y), metrics=metrics, loss=self.loss.loss)
95
+ # if val_data is not None:
96
+ # val_values = calculate_metrics(data=(self.predict(val_data[0]), val_data[1]), metrics=metrics, loss=self.loss.loss, validation=True)
97
+ # values |= val_values
98
+ # for metric, value in values.items():
99
+ # history[metric][int(epoch / callback_frequency)] = value
100
+ # if verbose: print(f"Epoch: {epoch + 1} - Metrics: {_round_dictionary(values)}")
101
+ # self.residuals = y - self.predict(X)
102
+ # return history
103
+
104
+ n_samples, self.n_features = X.shape
105
+ X = torch.cat((torch.ones(size=(X.shape[0], 1)), X), dim=1)
106
+
107
+ history = {metric: torch.zeros(floor(epochs / callback_frequency)) for metric in metrics}
108
+ self.weights = torch.randn((self.n_features + 1))
109
+
110
+ if sample_weight is None:
111
+ sample_weight = torch.ones(n_samples)
112
+
113
+ normalisation_const = torch.sum(sample_weight[:, None] * X**2, axis=0)
114
+
115
+ for epoch in range(epochs):
116
+ for j in range(self.n_features + 1):
117
+ residual = y - X @ self.weights + self.weights[j] * X[:, j]
118
+ weighted_prod = torch.dot(X[:, j], sample_weight * residual)
119
+ if normalisation_const[j] == 0:
120
+ continue
121
+ soft_thresholding = torch.sign(weighted_prod) * max(abs(weighted_prod) - self.alpha * self.l1_ratio, 0) / (normalisation_const[j] + self.alpha * (1 - self.l1_ratio))
122
+ self.weights[j] = soft_thresholding
123
+
124
+ if epoch % callback_frequency == 0:
125
+ values = calculate_metrics(data=(self.predict(X), y), metrics=metrics, loss=self._loss)
126
+ if val_data is not None:
127
+ val_values = calculate_metrics(data=(self.predict(val_data[0]), val_data[1]), metrics=metrics, loss=self._loss, validation=True)
128
+ values |= val_values
129
+ for metric, value in values.items():
130
+ history[metric][int(epoch / callback_frequency)] = value
131
+ if verbose: print(f"Epoch: {epoch + 1} - Metrics: {_round_dictionary(values)}")
132
+
133
+ # self.weights *= 1 + self.alpha * (1 - self.l1_ratio) # author of paper suggests this for a correction to the double shrinkage effect, however, the results are not that good.
134
+ self.residuals = y - self.predict(X)
135
+ return history
136
+
137
+ def _loss(self, prediction, true_outputs):
138
+ return torch.sum((prediction - true_outputs) ** 2) + self.alpha * (2 * self.l1_ratio * torch.linalg.norm(self.weights, ord=1) + (1 - self.l1_ratio) * torch.linalg.norm(self.weights, ord=2) ** 2)
139
+
140
+ def predict(self, X):
141
+ """
142
+ Applies the fitted ElasticNetRegression model to the input data, predicting the correct values.
143
+
144
+ Args:
145
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be regressed.
146
+ Returns:
147
+ target values (torch.Tensor of shape (n_samples,)): The predicted values corresponding to each sample.
148
+ Raises:
149
+ NotFittedError: If the ElasticNetRegression model has not been fitted before predicting.
150
+ TypeError: If the input matrix is not a PyTorch tensor.
151
+ ValueError: If the input matrix is not the correct shape.
152
+ """
153
+ if not hasattr(self, "weights"):
154
+ raise NotFittedError("ElasticNetRegression.fit() must be called before predicting.")
155
+ if not isinstance(X, torch.Tensor):
156
+ raise TypeError("The input matrix must be a PyTorch tensor.")
157
+ if X.ndim != 2 or (X.shape[1] != self.n_features and (X.shape[1] != self.n_features + 1 or torch.any(X[:, 0] != torch.ones(len(X))))):
158
+ raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
159
+
160
+ if X.shape[1] != self.n_features + 1: X = torch.cat((torch.ones(size=(X.shape[0], 1)), X), dim=1)
161
+ return X @ self.weights
@@ -0,0 +1,157 @@
1
+ import torch
2
+ from math import floor
3
+
4
+ from ....Data.Metrics import calculate_metrics, _round_dictionary
5
+ from ....Exceptions import NotFittedError
6
+
7
+
8
+ class LASSORegression:
9
+ """
10
+ Implements a linear regression model with L1 regularization.
11
+
12
+ Args:
13
+ alpha (int | float, optional): The regularization parameter. Larger alpha will force the l1 norm of the weights to be lower. Must be a positive real number. Defaults to 1.
14
+
15
+ Attributes:
16
+ n_features (int): The number of features. Available after fitting.
17
+ weights (torch.Tensor of shape (n_features + 1,)): The weights of the linear regression model. Available after fitting.
18
+ residuals (torch.Tensor of shape (n_samples,)): The residuals of the fitted model. For a good fit, the residuals should be normally distributed with zero mean and constant variance. Available after fitting.
19
+ """
20
+ def __init__(self, alpha=1.0):
21
+ if not isinstance(alpha, int | float) or alpha < 0:
22
+ raise ValueError("alpha must be a non-negative real number.")
23
+
24
+ self.alpha = alpha
25
+
26
+ def fit(self, X, y, sample_weight=None, val_data=None, epochs=100, callback_frequency=1, metrics=["loss"], verbose=False):
27
+ """
28
+ Fits the LASSORegression model to the input data by minimizing the mean squared error loss function using cyclic coordinate-wise descent from `this paper <https://aaltodoc.aalto.fi/server/api/core/bitstreams/091c41eb-7348-48d3-b25d-c8de25fbab03/content>`_.
29
+
30
+ Args:
31
+ X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
32
+ y (torch.Tensor of shape (n_samples,)): The target values corresponding to each sample.
33
+ val_data (tuple[X_val, y_val] | None, optional): Optional validation samples. If None, no validation data is used. Defaults to None.
34
+ epochs (int, optional): The number of training iterations. Must be a positive integer. Defaults to 100.
35
+ callback_frequency (int, optional): The number of iterations between printing info from training. Must be a positive integer. Defaults to 1, which means that every iteration, info is printed assuming verbose=True.
36
+ metrics (list[str], optional): The metrics that will be tracked during training. Defaults to ["loss"].
37
+ verbose (bool, optional): If True, prints info of the chosen metrics during training. Defaults to False.
38
+ Returns:
39
+ history (dict[str, torch.Tensor], each tensor is floor(epochs / callback_frequency) long.): A dictionary tracking the evolution of selected metrics at intervals defined by callback_frequency.
40
+ Raises:
41
+ TypeError: If the input matrix or the target matrix is not a PyTorch tensor or if other parameters are of wrong type.
42
+ ValueError: If the input matrix or the target matrix is not the correct shape or if other parameters have incorrect values.
43
+ """
44
+ if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
45
+ raise TypeError("The input matrix and the target matrix must be a PyTorch tensor.")
46
+ if X.ndim != 2:
47
+ raise ValueError("The input matrix must be a 2 dimensional tensor.")
48
+ if y.ndim != 1 or y.shape[0] != X.shape[0]:
49
+ raise ValueError("The targets must be 1 dimensional with the same number of samples as the input data")
50
+ if not isinstance(val_data, tuple) and val_data is not None:
51
+ raise TypeError("val_data must either be a tuple containing validation samples or None.")
52
+ if isinstance(val_data, tuple) and len(val_data) != 2:
53
+ raise ValueError("val_data must contain both X_val and y_val.")
54
+ if isinstance(val_data, tuple) and len(val_data) == 2 and (val_data[0].ndim != 2 or val_data[1].ndim != 1 or val_data[0].shape[1] != X.shape[1] or len(val_data[0]) != len(val_data[1])):
55
+ raise ValueError("X_val and y_val must be of correct shape.")
56
+ if not isinstance(epochs, int) or epochs <= 0:
57
+ raise ValueError("epochs must be a positive integer.")
58
+ if not isinstance(callback_frequency, int) or callback_frequency <= 0:
59
+ raise ValueError("callback_frequency must be a positive integer.")
60
+ if not isinstance(metrics, list | tuple):
61
+ raise TypeError("metrics must be a list or a tuple containing the strings of wanted metrics.")
62
+ if not isinstance(verbose, bool):
63
+ raise TypeError("verbose must be a boolean.")
64
+ if not isinstance(sample_weight, torch.Tensor) and sample_weight is not None:
65
+ raise TypeError("sample_weight must be torch.Tensor or None.")
66
+ if isinstance(sample_weight, torch.Tensor) and (sample_weight.ndim != 1 or len(X) != len(sample_weight)):
67
+ raise ValueError("sample_weight must be of shape (n_samples,)")
68
+
69
+ # self.n_features = X.shape[1]
70
+ # self.metrics = metrics
71
+ # history = {metric: torch.zeros(floor(epochs / callback_frequency)) for metric in metrics}
72
+ # batch_size = len(X) if batch_size is None else batch_size
73
+ # data_reader = DataReader(X, y, batch_size=batch_size, shuffle=shuffle_data, shuffle_every_epoch=shuffle_every_epoch)
74
+
75
+ # self.weights = torch.randn((self.n_features,))
76
+ # self.bias = torch.zeros((1,))
77
+ # optimiser = ADAM() if optimiser is None else optimiser
78
+ # optimiser.initialise_parameters([self.weights, self.bias])
79
+
80
+ # for epoch in range(epochs):
81
+ # for x_batch, y_batch in data_reader.get_data():
82
+ # predictions = self.predict(x_batch)
83
+ # dCdy = self.loss.gradient(predictions, y_batch)
84
+ # dCdweights = (x_batch.T @ dCdy) + self.alpha * torch.sign(self.weights)
85
+ # dCdbias = dCdy.mean(dim=0, keepdim=True)
86
+ # self.weights.grad = dCdweights
87
+ # self.bias.grad = dCdbias
88
+ # optimiser.update_parameters()
89
+ # if epoch % callback_frequency == 0:
90
+ # values = calculate_metrics(data=(self.predict(X), y), metrics=self.metrics, loss=self.loss.loss)
91
+ # if val_data is not None:
92
+ # val_values = calculate_metrics(data=(self.predict(val_data[0]), val_data[1]), metrics=self.metrics, loss=self.loss.loss, validation=True)
93
+ # values |= val_values
94
+ # for metric, value in values.items():
95
+ # history[metric][int(epoch / callback_frequency)] = value
96
+ # if verbose: print(f"Epoch: {epoch + 1} - Metrics: {_round_dictionary(values)}")
97
+ # self.residuals = y - self.predict(X)
98
+ # return history
99
+
100
+ n_samples, self.n_features = X.shape
101
+ X = torch.cat((torch.ones(size=(X.shape[0], 1)), X), dim=1)
102
+
103
+ history = {metric: torch.zeros(floor(epochs / callback_frequency)) for metric in metrics}
104
+ self.weights = torch.randn((self.n_features + 1))
105
+
106
+ if sample_weight is None:
107
+ sample_weight = torch.ones(n_samples)
108
+
109
+ normalisation_const = torch.sum(sample_weight[:, None] * X ** 2, axis=0)
110
+
111
+ for epoch in range(epochs):
112
+ for j in range(self.n_features + 1):
113
+ residual = y - X @ self.weights + self.weights[j] * X[:, j]
114
+ weighted_prod = torch.dot(X[:, j], sample_weight * residual)
115
+ if normalisation_const[j] == 0:
116
+ continue
117
+ soft_thresholding = torch.sign(weighted_prod) * max(abs(weighted_prod) - self.alpha, 0) / normalisation_const[j]
118
+ self.weights[j] = soft_thresholding
119
+
120
+ if epoch % callback_frequency == 0:
121
+ values = calculate_metrics(data=(self.predict(X), y), metrics=metrics, loss=self._loss)
122
+ if val_data is not None:
123
+ val_values = calculate_metrics(data=(self.predict(val_data[0]), val_data[1]), metrics=metrics, loss=self._loss, validation=True)
124
+ values |= val_values
125
+ for metric, value in values.items():
126
+ history[metric][int(epoch / callback_frequency)] = value
127
+ if verbose: print(f"Epoch: {epoch + 1} - Metrics: {_round_dictionary(values)}")
128
+
129
+ self.residuals = y - self.predict(X)
130
+ return history
131
+
132
+ def _loss(self, prediction, true_outputs):
133
+ return torch.sum((prediction - true_outputs) ** 2) + 2 * self.alpha * torch.linalg.norm(self.weights, ord=1)
134
+
135
+
136
+ def predict(self, X):
137
+ """
138
+ Applies the fitted LASSORegression model to the input data, predicting the correct values.
139
+
140
+ Args:
141
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be regressed.
142
+ Returns:
143
+ target values (torch.Tensor of shape (n_samples,)): The predicted values corresponding to each sample.
144
+ Raises:
145
+ NotFittedError: If the LASSORegression model has not been fitted before predicting.
146
+ TypeError: If the input matrix is not a PyTorch tensor.
147
+ ValueError: If the input matrix is not the correct shape.
148
+ """
149
+ if not hasattr(self, "weights"):
150
+ raise NotFittedError("LASSORegression.fit() must be called before predicting.")
151
+ if not isinstance(X, torch.Tensor):
152
+ raise TypeError("The input matrix must be a PyTorch tensor.")
153
+ if X.ndim != 2 or (X.shape[1] != self.n_features and (X.shape[1] != self.n_features + 1 or torch.any(X[:, 0] != X[0, 0]))):
154
+ raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
155
+
156
+ if X.shape[1] != self.n_features + 1: X = torch.cat((torch.ones(size=(X.shape[0], 1)), X), dim=1)
157
+ return X @ self.weights
@@ -0,0 +1,81 @@
1
+ import torch
2
+
3
+ from ....Exceptions import NotFittedError
4
+
5
+
6
+ class LinearRegression:
7
+ """
8
+ Implements the basic linear regression model.
9
+
10
+ Attributes:
11
+ n_features (int): The number of features. Available after fitting.
12
+ beta (torch.Tensor of shape (n_features + 1,)): The weights of the linear regression model. Available after fitting.
13
+ residuals (torch.Tensor of shape (n_samples,)): The residuals of the fitted model. For a good fit, the residuals should be normally distributed with zero mean and constant variance. Available after fitting.
14
+ """
15
+ def fit(self, X, y, include_bias=True, method="ols", sample_weight=None):
16
+ """
17
+ Fits the LinearRegression model to the input data by minimizing the squared error.
18
+
19
+ Args:
20
+ X (torch.Tensor of shape (n_samples, n_features)): The input data, where each row is a sample and each column is a feature.
21
+ y (torch.Tensor of shape (n_samples,)): The target values corresponding to each sample.
22
+ include_bias (bool, optional): Decides if a bias is included in a model. Defaults to True.
23
+ method (str, optional): Determines if the loss function is ordinary least squares or total least squares. Must be one of "ols" or "tls". Defaults to "ols".
24
+ sample_weight (torch.Tensor of shape (n_samples,) or None): A weight given to each sample in the regression. If None, this parameter is ignored. sample_weight is ignored if method == "tls".
25
+ Returns:
26
+ None
27
+ Raises:
28
+ TypeError: If the input matrix or the target matrix is not a PyTorch tensor.
29
+ ValueError: If the input matrix or the target matrix is not the correct shape.
30
+ """
31
+ if not isinstance(X, torch.Tensor) or not isinstance(y, torch.Tensor):
32
+ raise TypeError("The input matrix and the target matrix must be a PyTorch tensor.")
33
+ if X.ndim != 2:
34
+ raise ValueError("The input matrix must be a 2 dimensional tensor.")
35
+ if y.ndim != 1 or y.shape[0] != X.shape[0]:
36
+ raise ValueError("The targets must be 1 dimensional with the same number of samples as the input data")
37
+ if not isinstance(include_bias, bool):
38
+ raise TypeError("include_bias must be a boolean.")
39
+ if method not in ["ols", "tls"]:
40
+ raise ValueError('method must be one of "ols" or "tls".')
41
+ if not isinstance(sample_weight, torch.Tensor) and sample_weight is not None:
42
+ raise TypeError("sample_weight must be torch.Tensor or None.")
43
+ if isinstance(sample_weight, torch.Tensor) and (sample_weight.ndim != 1 or len(X) != len(sample_weight)):
44
+ raise ValueError("sample_weight must be of shape (n_samples,)")
45
+
46
+ self.include_bias = include_bias
47
+ self.n_features = X.shape[1]
48
+ X_ = torch.cat((torch.ones(size=(X.shape[0], 1)), X), dim=1) if self.include_bias else X
49
+ if method == "ols":
50
+ sample_weight = torch.diag(sample_weight) if sample_weight is not None else torch.eye(len(X))
51
+ self.beta = torch.linalg.lstsq(X_.T @ sample_weight @ X_, X_.T @ sample_weight @ y).solution
52
+ elif method == "tls":
53
+ C = torch.cat((X_, y.unsqueeze(1)), dim=1)
54
+ _, _, Vt = torch.linalg.svd(C, full_matrices=False)
55
+ v_min = Vt[-1]
56
+ self.beta = -v_min[:-1] / v_min[-1]
57
+
58
+ self.residuals = y - self.predict(X)
59
+
60
+ def predict(self, X):
61
+ """
62
+ Applies the fitted LinearRegression model to the input data, predicting the correct values.
63
+
64
+ Args:
65
+ X (torch.Tensor of shape (n_samples, n_features)): The input data to be regressed.
66
+ Returns:
67
+ target values (torch.Tensor of shape (n_samples,)): The predicted values corresponding to each sample.
68
+ Raises:
69
+ NotFittedError: If the LinearRegression model has not been fitted before predicting.
70
+ TypeError: If the input matrix is not a PyTorch tensor.
71
+ ValueError: If the input matrix is not the correct shape.
72
+ """
73
+ if not hasattr(self, "beta"):
74
+ raise NotFittedError("LinearRegression.fit() must be called before predicting.")
75
+ if not isinstance(X, torch.Tensor):
76
+ raise TypeError("The input matrix must be a PyTorch tensor.")
77
+ if X.ndim != 2 or X.shape[1] != self.n_features:
78
+ raise ValueError("The input matrix must be a 2 dimensional tensor with the same number of features as the fitted tensor.")
79
+
80
+ if self.include_bias: X = torch.cat((torch.ones(size=(X.shape[0], 1)), X), dim=1)
81
+ return X @ self.beta