hdlib 2.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.
@@ -0,0 +1,306 @@
1
+ """Similarity-preserving encoding and multi-model regression.
2
+
3
+ It implements the __hdlib.model.regression.RegressionEncoder__ and __hdlib.model.regression.RegressionModel__ class objects
4
+ built according to the Hyperdimensional Computing (HDC) paradigm as described in _Hernández-Cano et al. 2021_ https://doi.org/10.1109/DAC18074.2021.9586284."""
5
+
6
+ import numpy as np
7
+ from scipy.special import softmax
8
+
9
+ from hdlib.vector import Vector
10
+
11
+
12
+ class RegressionEncoder:
13
+ """Implements the similarity-preserving encoding from the RegHD article.
14
+ Maps input features to high-dimensional real-valued hypervectors.
15
+
16
+ The encoding function is h_i = cos(F_bar . B_i + b_i) * sin(F_bar . B_i), where F_bar is the input feature vector,
17
+ B_i are random base hypervectors, and b_i are random biases.
18
+ """
19
+
20
+ def __init__(self, D: int, n_features: int):
21
+ """Initializes the RegressionEncoder.
22
+
23
+ Parameters
24
+ ----------
25
+ D : int
26
+ Dimensionality of the high-dimensional space (e.g., 10000).
27
+ n_features : int
28
+ Number of features in the input data.
29
+
30
+ Returns
31
+ -------
32
+ RegressionEncoder
33
+ The regression encoder object.
34
+ """
35
+
36
+ if D <= 0 or not isinstance(D, int):
37
+ raise ValueError("Dimensionality must be a positive integer.")
38
+
39
+ if n_features <= 0 or not isinstance(n_features, int):
40
+ raise ValueError("Number of features must be a positive integer.")
41
+
42
+ self.D = D
43
+ self.n_features = n_features
44
+
45
+ # Randomly chosen base hypervectors (B_k in paper)
46
+ # These are generated from a normal distribution as suggested by the paper (B_kj ~ N(0,1)).
47
+ # Shape: (n_features, D)
48
+ self.base_hypervectors = np.random.randn(n_features, D)
49
+
50
+ # Random biases b_i ~ U(0, 2pi)
51
+ # Shape: (D,)
52
+ self.biases = np.random.uniform(0, 2 * np.pi, D)
53
+
54
+ def encode(self, feature_vector: np.ndarray) -> np.ndarray:
55
+ """Encodes a single feature vector into a high-dimensional real-valued hypervector.
56
+
57
+ Parameters
58
+ ----------
59
+ feature_vector : np.ndarray
60
+ A 1D numpy array of shape (n_features,) representing the input features.
61
+
62
+ Returns
63
+ -------
64
+ np.ndarray
65
+ A 1D numpy array of shape (D,) representing the encoded high-dimensional real-valued hypervector.
66
+ """
67
+
68
+ # Ensure feature_vector is a 1D numpy array
69
+ feature_vector = np.asarray(feature_vector).flatten()
70
+
71
+ if feature_vector.shape[0] != self.n_features:
72
+ raise ValueError(f"Input feature vector must have {self.n_features} features, but got {feature_vector.shape[0]}")
73
+
74
+ # Compute dot products: F_bar . B_i for each B_i (column in base_hypervectors)
75
+ # This results in a 1D array of shape (D,)
76
+ dot_products = np.dot(feature_vector, self.base_hypervectors)
77
+
78
+ # Apply the encoding formula: h_i = cos(dot_product + b_i) * sin(dot_product)
79
+ encoded_vector = np.cos(dot_products + self.biases) * np.sin(dot_products)
80
+
81
+ return encoded_vector
82
+
83
+
84
+ class RegressionModel:
85
+ """Implements the RegHD multi-model regression algorithm.
86
+
87
+ This class handles the training and prediction phases, including the management of cluster and regression models,
88
+ and optional quantization for efficiency.
89
+ """
90
+ def __init__(
91
+ self,
92
+ D: int,
93
+ n_features: int,
94
+ k_models: int=8,
95
+ learning_rate: float=0.01,
96
+ iterations: int=20,
97
+ binary_threshold: float=0.0
98
+ ):
99
+ """Initializes the RegHDRegressor.
100
+
101
+ Parameters
102
+ ----------
103
+ D : int
104
+ Dimensionality of hypervectors.
105
+ n_features : int
106
+ Number of input features.
107
+ k_models : int
108
+ Number of cluster/regression models to use.
109
+ learning_rate : float
110
+ Alpha parameter for model updates.
111
+ iterations : int
112
+ Number of training iterations (epochs).
113
+ binary_threshold : float
114
+ Threshold for binarizing real-valued hypervectors (e.g., 0.0 for bipolar conversion).
115
+
116
+ Returns
117
+ -------
118
+ RegressionModel
119
+ The regression model object.
120
+ """
121
+
122
+ if D <= 0 or not isinstance(D, int):
123
+ raise ValueError("Dimensionality D must be a positive integer.")
124
+
125
+ if n_features <= 0 or not isinstance(n_features, int):
126
+ raise ValueError("Number of features n_features must be a positive integer.")
127
+
128
+ if k_models <= 0 or not isinstance(k_models, int):
129
+ raise ValueError("Number of models k_models must be a positive integer.")
130
+
131
+ if not isinstance(learning_rate, (int, float)) or learning_rate <= 0:
132
+ raise ValueError("Learning rate must be a positive number.")
133
+
134
+ if iterations <= 0 or not isinstance(iterations, int):
135
+ raise ValueError("Number of iterations must be a positive integer.")
136
+
137
+ if not isinstance(binary_threshold, (int, float)):
138
+ raise ValueError("Binary threshold must be a number.")
139
+
140
+ self.D = D
141
+ self.n_features = n_features
142
+ self.k_models = k_models
143
+ self.learning_rate = learning_rate
144
+ self.iterations = iterations
145
+ self.binary_threshold = binary_threshold
146
+
147
+ # Initialize the encoder for mapping input features to HD space
148
+ self.encoder = RegressionEncoder(D, n_features)
149
+
150
+ # Initialize cluster hypervectors (C_i) and regression model hypervectors (M_i)
151
+ # As per the paper, we maintain both "integer" (float numpy array) and "binary" (Vector)
152
+ # versions for cluster models.
153
+ # Cluster models (binary for similarity search, float for updates)
154
+ self.cluster_models_b = [Vector(vtype="bipolar", size=D) for _ in range(k_models)]
155
+ self.cluster_models_int = [np.zeros(D, dtype=float) for _ in range(k_models)]
156
+
157
+ # Initialize integer models from the binary ones to ensure consistency
158
+ for i in range(self.k_models):
159
+ self.cluster_models_int[i] = self.cluster_models_b[i].vector.astype(float)
160
+
161
+ # Regression models (float for updates, binary for optional quantized prediction)
162
+ self.regression_models_int = [np.zeros(D, dtype=float) for _ in range(k_models)]
163
+ self.regression_models_b = [Vector(vtype="bipolar", vector=np.zeros(D)) for _ in range(k_models)]
164
+
165
+ # Flag to control prediction quantization strategy
166
+ self.quantized_prediction = False
167
+
168
+ def _to_bipolar(self, vec_float: np.ndarray) -> Vector:
169
+ """Converts a float numpy array to an hdlib Vector (bipolar). Values >= binary_threshold become 1, others become -1.
170
+ """
171
+
172
+ bipolar_array = np.where(vec_float >= self.binary_threshold, 1, -1)
173
+
174
+ return Vector(vtype="bipolar", vector=bipolar_array)
175
+
176
+ def _calculate_similarity(self, encoded_s_b: Vector, cluster_model_b: Vector) -> float:
177
+ """Calculates similarity between a binary encoded input hypervector and a
178
+ binary cluster model hypervector. For bipolar vectors, this is based on cosine similarity.
179
+ """
180
+
181
+ # Using cosine similarity as the primary metric for bipolar vectors in hdlib
182
+ return 1.0 - encoded_s_b.dist(cluster_model_b, method="cosine")
183
+
184
+ def fit(self, X: np.ndarray, y: np.ndarray):
185
+ """Trains the RegHD regressor using the provided training data.
186
+
187
+ Parameters
188
+ ----------
189
+ X : np.ndarray
190
+ Input features, a 2D numpy array of shape (n_samples, n_features).
191
+ y : np.ndarray
192
+ Target values, a 1D numpy array of shape (n_samples,).
193
+ """
194
+
195
+ X = np.asarray(X, dtype=float)
196
+ y = np.asarray(y, dtype=float)
197
+
198
+ if X.shape[1] != self.n_features:
199
+ raise ValueError(f"Input X must have {self.n_features} features, but got {X.shape[1]}")
200
+
201
+ if X.shape[0] != y.shape[0]:
202
+ raise ValueError("Number of samples in X and y must match.")
203
+
204
+ print(f"Starting RegHD training with D={self.D}, k_models={self.k_models}, iterations={self.iterations}")
205
+
206
+ for iteration in range(self.iterations):
207
+ total_mse = 0.0
208
+
209
+ for i in range(X.shape[0]):
210
+ feature_vector = X[i]
211
+ actual_output = y[i]
212
+
213
+ # 1. Encode input data
214
+ encoded_s_float = self.encoder.encode(feature_vector)
215
+ encoded_s_b = self._to_bipolar(encoded_s_float)
216
+
217
+ # 2. Check similarity with all binary cluster hypervectors
218
+ similarities = np.array([self._calculate_similarity(encoded_s_b, cluster_b) for cluster_b in self.cluster_models_b])
219
+
220
+ # 3. Normalize similarity values using softmax to get confidences
221
+ confidences = softmax(similarities)
222
+
223
+ # 4. Predict output value during training
224
+ predicted_output = 0.0
225
+ for j in range(self.k_models):
226
+ predicted_output += confidences[j] * np.dot(self.regression_models_int[j], encoded_s_float)
227
+
228
+ # 5. Calculate prediction error
229
+ error = actual_output - predicted_output
230
+ total_mse += error**2
231
+
232
+ # 6. Update Regression Models
233
+ for j in range(self.k_models):
234
+ self.regression_models_int[j] += self.learning_rate * confidences[j] * error * encoded_s_float
235
+
236
+ # 7. Update Cluster Models
237
+ l_max_similarity = np.argmax(similarities)
238
+ self.cluster_models_int[l_max_similarity] += (1 - confidences[l_max_similarity]) * encoded_s_float
239
+
240
+ # After each epoch, quantize the updated models
241
+ for j in range(self.k_models):
242
+ self.cluster_models_b[j] = self._to_bipolar(self.cluster_models_int[j])
243
+
244
+ if self.quantized_prediction:
245
+ self.regression_models_b[j] = self._to_bipolar(self.regression_models_int[j])
246
+
247
+ avg_mse = total_mse / X.shape[0]
248
+
249
+ print(f"Iteration {iteration+1}/{self.iterations}, Mean Squared Error (MSE): {avg_mse:.4f}")
250
+
251
+ def predict(self, X_query: np.ndarray) -> np.ndarray:
252
+ """Predicts output values for new input data.
253
+
254
+ Parameters
255
+ ----------
256
+ X_query : np.ndarray
257
+ Input features, a 2D numpy array of shape (n_samples, n_features).
258
+
259
+ Returns
260
+ -------
261
+ np.ndarray
262
+ Predicted target values, a 1D numpy array of shape (n_samples,).
263
+ """
264
+
265
+ X_query = np.asarray(X_query, dtype=float)
266
+
267
+ if X_query.shape[1] != self.n_features:
268
+ raise ValueError(f"Input X_query must have {self.n_features} features, but got {X_query.shape[1]}")
269
+
270
+ predictions = []
271
+
272
+ for i in range(X_query.shape[0]):
273
+ feature_vector = X_query[i]
274
+
275
+ # 1. Encode input data
276
+ encoded_s_float = self.encoder.encode(feature_vector)
277
+ encoded_s_b = self._to_bipolar(encoded_s_float)
278
+
279
+ # 2. Compute similarity with all binary cluster centers
280
+ similarities = np.array([self._calculate_similarity(encoded_s_b, cluster_b) for cluster_b in self.cluster_models_b])
281
+
282
+ # 3. Normalize similarity values
283
+ confidences = softmax(similarities)
284
+
285
+ # 4. Predict output value
286
+ predicted_output = 0.0
287
+ for j in range(self.k_models):
288
+ if self.quantized_prediction:
289
+ predicted_output += confidences[j] * np.dot(self.regression_models_b[j].vector, encoded_s_b.vector)
290
+
291
+ else:
292
+ predicted_output += confidences[j] * np.dot(self.regression_models_int[j], encoded_s_float)
293
+
294
+ predictions.append(predicted_output)
295
+
296
+ return np.array(predictions)
297
+
298
+ def set_quantized_prediction_mode(self, enable: bool=True):
299
+ """Sets whether prediction should use binarized regression models.
300
+ """
301
+
302
+ self.quantized_prediction = enable
303
+
304
+ if enable:
305
+ for j in range(self.k_models):
306
+ self.regression_models_b[j] = self._to_bipolar(self.regression_models_int[j])