obvaekernel 0.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.
- obvaekernel/__init__.py +7 -0
- obvaekernel/_version.py +4 -0
- obvaekernel/core.py +621 -0
- obvaekernel-0.1.0.dist-info/METADATA +121 -0
- obvaekernel-0.1.0.dist-info/RECORD +7 -0
- obvaekernel-0.1.0.dist-info/WHEEL +4 -0
- obvaekernel-0.1.0.dist-info/licenses/LICENSE +30 -0
obvaekernel/__init__.py
ADDED
obvaekernel/_version.py
ADDED
obvaekernel/core.py
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
"""Portable NumPy-only OBVAE kernel module.
|
|
2
|
+
|
|
3
|
+
This module implements an overcomplete beta-VAE encoder that can be used as a
|
|
4
|
+
feature map for kernel methods. Training and inference are fully NumPy-based,
|
|
5
|
+
with optional serialization for cross-runtime portability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Callable, Dict, Optional, Tuple, Union
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ArrayLike = Union[np.ndarray, list, tuple]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _NumPyVAEEngine:
|
|
21
|
+
"""Internal pure NumPy VAE engine with manual backpropagation."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
input_dim: int,
|
|
26
|
+
hidden_dim: int,
|
|
27
|
+
latent_dim: int,
|
|
28
|
+
rng: np.random.Generator,
|
|
29
|
+
dtype: np.dtype,
|
|
30
|
+
logvar_clip: Tuple[float, float] = (-30.0, 20.0),
|
|
31
|
+
) -> None:
|
|
32
|
+
self.input_dim = int(input_dim)
|
|
33
|
+
self.hidden_dim = int(hidden_dim)
|
|
34
|
+
self.latent_dim = int(latent_dim)
|
|
35
|
+
self.rng = rng
|
|
36
|
+
self.dtype = np.dtype(dtype)
|
|
37
|
+
self.logvar_clip = logvar_clip
|
|
38
|
+
|
|
39
|
+
self.W_enc = self._init_weights(self.input_dim, self.hidden_dim)
|
|
40
|
+
self.b_enc = np.zeros(self.hidden_dim, dtype=self.dtype)
|
|
41
|
+
|
|
42
|
+
self.W_mu = self._init_weights(self.hidden_dim, self.latent_dim)
|
|
43
|
+
self.b_mu = np.zeros(self.latent_dim, dtype=self.dtype)
|
|
44
|
+
|
|
45
|
+
self.W_logvar = self._init_weights(self.hidden_dim, self.latent_dim)
|
|
46
|
+
self.b_logvar = np.zeros(self.latent_dim, dtype=self.dtype)
|
|
47
|
+
|
|
48
|
+
self.W_dec1 = self._init_weights(self.latent_dim, self.hidden_dim)
|
|
49
|
+
self.b_dec1 = np.zeros(self.hidden_dim, dtype=self.dtype)
|
|
50
|
+
|
|
51
|
+
self.W_dec2 = self._init_weights(self.hidden_dim, self.input_dim)
|
|
52
|
+
self.b_dec2 = np.zeros(self.input_dim, dtype=self.dtype)
|
|
53
|
+
|
|
54
|
+
self._cache: Dict[str, np.ndarray] = {}
|
|
55
|
+
|
|
56
|
+
def _init_weights(self, fan_in: int, fan_out: int) -> np.ndarray:
|
|
57
|
+
limit = np.sqrt(6.0 / float(fan_in + fan_out))
|
|
58
|
+
return self.rng.uniform(
|
|
59
|
+
low=-limit, high=limit, size=(fan_out, fan_in)
|
|
60
|
+
).astype(self.dtype, copy=False)
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _relu(x: np.ndarray) -> np.ndarray:
|
|
64
|
+
return np.maximum(x, 0.0)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _relu_derivative(x: np.ndarray) -> np.ndarray:
|
|
68
|
+
return (x > 0.0).astype(x.dtype, copy=False)
|
|
69
|
+
|
|
70
|
+
def encode(self, x: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
71
|
+
z_enc = x @ self.W_enc.T + self.b_enc
|
|
72
|
+
h_enc = self._relu(z_enc)
|
|
73
|
+
|
|
74
|
+
mu = h_enc @ self.W_mu.T + self.b_mu
|
|
75
|
+
logvar = h_enc @ self.W_logvar.T + self.b_logvar
|
|
76
|
+
logvar = np.clip(logvar, self.logvar_clip[0], self.logvar_clip[1])
|
|
77
|
+
return z_enc, h_enc, mu, logvar
|
|
78
|
+
|
|
79
|
+
def decode(self, z: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
80
|
+
z_dec1 = z @ self.W_dec1.T + self.b_dec1
|
|
81
|
+
h_dec = self._relu(z_dec1)
|
|
82
|
+
x_recon = h_dec @ self.W_dec2.T + self.b_dec2
|
|
83
|
+
return z_dec1, h_dec, x_recon
|
|
84
|
+
|
|
85
|
+
def forward(self, x: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
86
|
+
z_enc, h_enc, mu, logvar = self.encode(x)
|
|
87
|
+
std = np.exp(0.5 * logvar).astype(self.dtype, copy=False)
|
|
88
|
+
eps = self.rng.standard_normal(mu.shape).astype(self.dtype, copy=False)
|
|
89
|
+
z = mu + std * eps
|
|
90
|
+
z_dec1, h_dec, x_recon = self.decode(z)
|
|
91
|
+
|
|
92
|
+
self._cache = {
|
|
93
|
+
"x": x,
|
|
94
|
+
"z_enc": z_enc,
|
|
95
|
+
"h_enc": h_enc,
|
|
96
|
+
"mu": mu,
|
|
97
|
+
"logvar": logvar,
|
|
98
|
+
"std": std,
|
|
99
|
+
"eps": eps,
|
|
100
|
+
"z": z,
|
|
101
|
+
"z_dec1": z_dec1,
|
|
102
|
+
"h_dec": h_dec,
|
|
103
|
+
"x_recon": x_recon,
|
|
104
|
+
}
|
|
105
|
+
return x_recon, mu, logvar
|
|
106
|
+
|
|
107
|
+
def encode_mean(self, x: np.ndarray) -> np.ndarray:
|
|
108
|
+
_, h_enc, mu, _ = self.encode(x)
|
|
109
|
+
# h_enc is computed intentionally to match transform path numerics.
|
|
110
|
+
_ = h_enc
|
|
111
|
+
return mu
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def compute_loss(
|
|
115
|
+
x_recon: np.ndarray,
|
|
116
|
+
x: np.ndarray,
|
|
117
|
+
mu: np.ndarray,
|
|
118
|
+
logvar: np.ndarray,
|
|
119
|
+
beta: float,
|
|
120
|
+
) -> Tuple[float, float, float]:
|
|
121
|
+
mse_loss = float(np.sum((x_recon - x) ** 2))
|
|
122
|
+
kld_loss = float(-0.5 * np.sum(1.0 + logvar - mu**2 - np.exp(logvar)))
|
|
123
|
+
loss = mse_loss + float(beta) * kld_loss
|
|
124
|
+
return loss, mse_loss, kld_loss
|
|
125
|
+
|
|
126
|
+
def backward(
|
|
127
|
+
self,
|
|
128
|
+
x: np.ndarray,
|
|
129
|
+
x_recon: np.ndarray,
|
|
130
|
+
mu: np.ndarray,
|
|
131
|
+
logvar: np.ndarray,
|
|
132
|
+
beta: float,
|
|
133
|
+
) -> Dict[str, np.ndarray]:
|
|
134
|
+
del x, x_recon, mu, logvar
|
|
135
|
+
cache = self._cache
|
|
136
|
+
|
|
137
|
+
x_batch = cache["x"]
|
|
138
|
+
z_enc = cache["z_enc"]
|
|
139
|
+
h_enc = cache["h_enc"]
|
|
140
|
+
mu_cached = cache["mu"]
|
|
141
|
+
logvar_cached = cache["logvar"]
|
|
142
|
+
std = cache["std"]
|
|
143
|
+
eps = cache["eps"]
|
|
144
|
+
z = cache["z"]
|
|
145
|
+
z_dec1 = cache["z_dec1"]
|
|
146
|
+
h_dec = cache["h_dec"]
|
|
147
|
+
x_recon_cached = cache["x_recon"]
|
|
148
|
+
|
|
149
|
+
dx_recon = 2.0 * (x_recon_cached - x_batch)
|
|
150
|
+
dW_dec2 = dx_recon.T @ h_dec
|
|
151
|
+
db_dec2 = np.sum(dx_recon, axis=0)
|
|
152
|
+
|
|
153
|
+
dh_dec = dx_recon @ self.W_dec2
|
|
154
|
+
dz_dec1 = dh_dec * self._relu_derivative(z_dec1)
|
|
155
|
+
dW_dec1 = dz_dec1.T @ z
|
|
156
|
+
db_dec1 = np.sum(dz_dec1, axis=0)
|
|
157
|
+
|
|
158
|
+
dz = dz_dec1 @ self.W_dec1
|
|
159
|
+
dmu_kl = mu_cached
|
|
160
|
+
dlogvar_kl = -0.5 * (1.0 - np.exp(logvar_cached))
|
|
161
|
+
|
|
162
|
+
dmu = dz + float(beta) * dmu_kl
|
|
163
|
+
dlogvar = dz * (0.5 * std * eps) + float(beta) * dlogvar_kl
|
|
164
|
+
|
|
165
|
+
dW_mu = dmu.T @ h_enc
|
|
166
|
+
db_mu = np.sum(dmu, axis=0)
|
|
167
|
+
|
|
168
|
+
dW_logvar = dlogvar.T @ h_enc
|
|
169
|
+
db_logvar = np.sum(dlogvar, axis=0)
|
|
170
|
+
|
|
171
|
+
dh_enc = dmu @ self.W_mu + dlogvar @ self.W_logvar
|
|
172
|
+
dz_enc = dh_enc * self._relu_derivative(z_enc)
|
|
173
|
+
dW_enc = dz_enc.T @ x_batch
|
|
174
|
+
db_enc = np.sum(dz_enc, axis=0)
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
"W_enc": dW_enc.astype(self.dtype, copy=False),
|
|
178
|
+
"b_enc": db_enc.astype(self.dtype, copy=False),
|
|
179
|
+
"W_mu": dW_mu.astype(self.dtype, copy=False),
|
|
180
|
+
"b_mu": db_mu.astype(self.dtype, copy=False),
|
|
181
|
+
"W_logvar": dW_logvar.astype(self.dtype, copy=False),
|
|
182
|
+
"b_logvar": db_logvar.astype(self.dtype, copy=False),
|
|
183
|
+
"W_dec1": dW_dec1.astype(self.dtype, copy=False),
|
|
184
|
+
"b_dec1": db_dec1.astype(self.dtype, copy=False),
|
|
185
|
+
"W_dec2": dW_dec2.astype(self.dtype, copy=False),
|
|
186
|
+
"b_dec2": db_dec2.astype(self.dtype, copy=False),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class AdamOptimizer:
|
|
191
|
+
"""Pure NumPy Adam optimizer."""
|
|
192
|
+
|
|
193
|
+
def __init__(
|
|
194
|
+
self,
|
|
195
|
+
params: Dict[str, np.ndarray],
|
|
196
|
+
lr: float = 0.001,
|
|
197
|
+
betas: Tuple[float, float] = (0.9, 0.999),
|
|
198
|
+
eps: float = 1e-8,
|
|
199
|
+
) -> None:
|
|
200
|
+
self.params = params
|
|
201
|
+
self.lr = float(lr)
|
|
202
|
+
self.beta1 = float(betas[0])
|
|
203
|
+
self.beta2 = float(betas[1])
|
|
204
|
+
self.eps = float(eps)
|
|
205
|
+
self.t = 0
|
|
206
|
+
self.m = {name: np.zeros_like(value) for name, value in params.items()}
|
|
207
|
+
self.v = {name: np.zeros_like(value) for name, value in params.items()}
|
|
208
|
+
|
|
209
|
+
def step(self, grads: Dict[str, np.ndarray]) -> None:
|
|
210
|
+
self.t += 1
|
|
211
|
+
for name, param in self.params.items():
|
|
212
|
+
grad = grads[name]
|
|
213
|
+
self.m[name] = self.beta1 * self.m[name] + (1.0 - self.beta1) * grad
|
|
214
|
+
self.v[name] = self.beta2 * self.v[name] + (1.0 - self.beta2) * (grad**2)
|
|
215
|
+
|
|
216
|
+
m_hat = self.m[name] / (1.0 - self.beta1**self.t)
|
|
217
|
+
v_hat = self.v[name] / (1.0 - self.beta2**self.t)
|
|
218
|
+
param -= self.lr * m_hat / (np.sqrt(v_hat) + self.eps)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class OBVAEKernel:
|
|
222
|
+
"""Overcomplete Beta-VAE kernel transformer with NumPy-only core."""
|
|
223
|
+
|
|
224
|
+
SERIAL_VERSION = 1
|
|
225
|
+
|
|
226
|
+
def __init__(
|
|
227
|
+
self,
|
|
228
|
+
latent_dim: Union[str, int] = "auto",
|
|
229
|
+
expansion_factor: int = 10,
|
|
230
|
+
hidden_dim: int = 64,
|
|
231
|
+
beta: float = 2.0,
|
|
232
|
+
epochs: int = 300,
|
|
233
|
+
lr: float = 0.005,
|
|
234
|
+
batch_size: Optional[int] = None,
|
|
235
|
+
variance_threshold: float = 1e-4,
|
|
236
|
+
dtype: Union[str, np.dtype, type] = np.float32,
|
|
237
|
+
keep_decoder: bool = True,
|
|
238
|
+
verbose: bool = False,
|
|
239
|
+
random_state: Optional[int] = None,
|
|
240
|
+
) -> None:
|
|
241
|
+
self.latent_dim = latent_dim
|
|
242
|
+
self.expansion_factor = expansion_factor
|
|
243
|
+
self.hidden_dim = hidden_dim
|
|
244
|
+
self.beta = beta
|
|
245
|
+
self.epochs = epochs
|
|
246
|
+
self.lr = lr
|
|
247
|
+
self.batch_size = batch_size
|
|
248
|
+
self.variance_threshold = variance_threshold
|
|
249
|
+
self.dtype = np.dtype(dtype)
|
|
250
|
+
self.keep_decoder = keep_decoder
|
|
251
|
+
self.verbose = verbose
|
|
252
|
+
self.random_state = random_state
|
|
253
|
+
|
|
254
|
+
self.W_h: Optional[np.ndarray] = None
|
|
255
|
+
self.b_h: Optional[np.ndarray] = None
|
|
256
|
+
self.W_mu: Optional[np.ndarray] = None
|
|
257
|
+
self.b_mu: Optional[np.ndarray] = None
|
|
258
|
+
|
|
259
|
+
self._full_W_mu: Optional[np.ndarray] = None
|
|
260
|
+
self._full_b_mu: Optional[np.ndarray] = None
|
|
261
|
+
|
|
262
|
+
self._W_dec1: Optional[np.ndarray] = None
|
|
263
|
+
self._b_dec1: Optional[np.ndarray] = None
|
|
264
|
+
self._W_dec2: Optional[np.ndarray] = None
|
|
265
|
+
self._b_dec2: Optional[np.ndarray] = None
|
|
266
|
+
|
|
267
|
+
self.n_features_in_: Optional[int] = None
|
|
268
|
+
self.training_dim_: Optional[int] = None
|
|
269
|
+
self.effective_dim_: Optional[int] = None
|
|
270
|
+
self.active_indices_: Optional[np.ndarray] = None
|
|
271
|
+
self.feature_variances_: Optional[np.ndarray] = None
|
|
272
|
+
self.is_fitted_ = False
|
|
273
|
+
|
|
274
|
+
def _validate_hyperparameters(self) -> None:
|
|
275
|
+
if self.latent_dim != "auto":
|
|
276
|
+
if not isinstance(self.latent_dim, (int, np.integer)):
|
|
277
|
+
raise ValueError("latent_dim must be 'auto' or a positive integer.")
|
|
278
|
+
if int(self.latent_dim) <= 0:
|
|
279
|
+
raise ValueError("latent_dim must be a positive integer.")
|
|
280
|
+
|
|
281
|
+
if not isinstance(self.expansion_factor, (int, np.integer)) or int(self.expansion_factor) <= 0:
|
|
282
|
+
raise ValueError("expansion_factor must be a positive integer.")
|
|
283
|
+
if not isinstance(self.hidden_dim, (int, np.integer)) or int(self.hidden_dim) <= 0:
|
|
284
|
+
raise ValueError("hidden_dim must be a positive integer.")
|
|
285
|
+
if not isinstance(self.epochs, (int, np.integer)) or int(self.epochs) <= 0:
|
|
286
|
+
raise ValueError("epochs must be a positive integer.")
|
|
287
|
+
if float(self.lr) <= 0.0:
|
|
288
|
+
raise ValueError("lr must be greater than 0.")
|
|
289
|
+
if float(self.variance_threshold) < 0.0:
|
|
290
|
+
raise ValueError("variance_threshold must be >= 0.")
|
|
291
|
+
if self.batch_size is not None and int(self.batch_size) <= 0:
|
|
292
|
+
raise ValueError("batch_size must be a positive integer or None.")
|
|
293
|
+
|
|
294
|
+
def _to_2d_array(self, x: ArrayLike, expected_features: Optional[int] = None) -> np.ndarray:
|
|
295
|
+
arr = np.asarray(x, dtype=self.dtype)
|
|
296
|
+
if arr.ndim != 2:
|
|
297
|
+
raise ValueError(f"Expected a 2D array, got {arr.ndim}D.")
|
|
298
|
+
if expected_features is not None and arr.shape[1] != expected_features:
|
|
299
|
+
raise ValueError(
|
|
300
|
+
f"Expected {expected_features} features, got {arr.shape[1]}."
|
|
301
|
+
)
|
|
302
|
+
return np.ascontiguousarray(arr, dtype=self.dtype)
|
|
303
|
+
|
|
304
|
+
def _to_single_sample(self, x: ArrayLike) -> np.ndarray:
|
|
305
|
+
arr = np.asarray(x, dtype=self.dtype)
|
|
306
|
+
if arr.ndim == 1:
|
|
307
|
+
arr = arr.reshape(1, -1)
|
|
308
|
+
elif arr.ndim == 2 and arr.shape[0] == 1:
|
|
309
|
+
pass
|
|
310
|
+
else:
|
|
311
|
+
raise ValueError("Input must be a single sample as shape (n_features,) or (1, n_features).")
|
|
312
|
+
return np.ascontiguousarray(arr, dtype=self.dtype)
|
|
313
|
+
|
|
314
|
+
def _require_fitted(self) -> None:
|
|
315
|
+
if not self.is_fitted_:
|
|
316
|
+
raise RuntimeError("Model must be fitted before calling this method.")
|
|
317
|
+
|
|
318
|
+
def _resolve_training_dim(self, input_dim: int) -> int:
|
|
319
|
+
if self.latent_dim == "auto":
|
|
320
|
+
return int(min(input_dim * int(self.expansion_factor), 500))
|
|
321
|
+
return int(self.latent_dim)
|
|
322
|
+
|
|
323
|
+
def _normalize_rows(self, phi: np.ndarray) -> np.ndarray:
|
|
324
|
+
eps = np.finfo(phi.dtype).eps
|
|
325
|
+
norms = np.linalg.norm(phi, axis=1, keepdims=True)
|
|
326
|
+
return phi / np.maximum(norms, eps)
|
|
327
|
+
|
|
328
|
+
def fit(self, X: ArrayLike, y: Optional[ArrayLike] = None) -> "OBVAEKernel":
|
|
329
|
+
del y
|
|
330
|
+
self._validate_hyperparameters()
|
|
331
|
+
X_arr = self._to_2d_array(X)
|
|
332
|
+
n_samples, input_dim = X_arr.shape
|
|
333
|
+
|
|
334
|
+
training_dim = self._resolve_training_dim(input_dim)
|
|
335
|
+
if training_dim <= 0:
|
|
336
|
+
raise ValueError("Resolved training latent dimension must be positive.")
|
|
337
|
+
|
|
338
|
+
rng = np.random.default_rng(self.random_state)
|
|
339
|
+
engine = _NumPyVAEEngine(
|
|
340
|
+
input_dim=input_dim,
|
|
341
|
+
hidden_dim=int(self.hidden_dim),
|
|
342
|
+
latent_dim=training_dim,
|
|
343
|
+
rng=rng,
|
|
344
|
+
dtype=self.dtype,
|
|
345
|
+
logvar_clip=(-30.0, 20.0),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
params = {
|
|
349
|
+
"W_enc": engine.W_enc,
|
|
350
|
+
"b_enc": engine.b_enc,
|
|
351
|
+
"W_mu": engine.W_mu,
|
|
352
|
+
"b_mu": engine.b_mu,
|
|
353
|
+
"W_logvar": engine.W_logvar,
|
|
354
|
+
"b_logvar": engine.b_logvar,
|
|
355
|
+
"W_dec1": engine.W_dec1,
|
|
356
|
+
"b_dec1": engine.b_dec1,
|
|
357
|
+
"W_dec2": engine.W_dec2,
|
|
358
|
+
"b_dec2": engine.b_dec2,
|
|
359
|
+
}
|
|
360
|
+
optimizer = AdamOptimizer(params=params, lr=float(self.lr))
|
|
361
|
+
|
|
362
|
+
if self.batch_size is None:
|
|
363
|
+
batch_size = n_samples
|
|
364
|
+
else:
|
|
365
|
+
batch_size = min(int(self.batch_size), n_samples)
|
|
366
|
+
n_batches = (n_samples + batch_size - 1) // batch_size
|
|
367
|
+
|
|
368
|
+
for epoch in range(int(self.epochs)):
|
|
369
|
+
indices = rng.permutation(n_samples)
|
|
370
|
+
X_shuffled = X_arr[indices]
|
|
371
|
+
epoch_loss = 0.0
|
|
372
|
+
|
|
373
|
+
for batch_idx in range(n_batches):
|
|
374
|
+
start = batch_idx * batch_size
|
|
375
|
+
end = min((batch_idx + 1) * batch_size, n_samples)
|
|
376
|
+
X_batch = X_shuffled[start:end]
|
|
377
|
+
|
|
378
|
+
x_recon, mu, logvar = engine.forward(X_batch)
|
|
379
|
+
loss, _, _ = engine.compute_loss(x_recon, X_batch, mu, logvar, beta=float(self.beta))
|
|
380
|
+
grads = engine.backward(X_batch, x_recon, mu, logvar, beta=float(self.beta))
|
|
381
|
+
optimizer.step(grads)
|
|
382
|
+
epoch_loss += loss
|
|
383
|
+
|
|
384
|
+
if self.verbose and ((epoch + 1) % 50 == 0 or epoch == 0 or (epoch + 1) == int(self.epochs)):
|
|
385
|
+
print(f"Epoch [{epoch + 1}/{int(self.epochs)}], Loss: {epoch_loss / n_samples:.6f}")
|
|
386
|
+
|
|
387
|
+
final_mu = engine.encode_mean(X_arr)
|
|
388
|
+
feature_variances = np.var(final_mu, axis=0).astype(self.dtype, copy=False)
|
|
389
|
+
active_indices = np.flatnonzero(feature_variances > float(self.variance_threshold)).astype(np.int64)
|
|
390
|
+
if active_indices.size == 0:
|
|
391
|
+
active_indices = np.array([int(np.argmax(feature_variances))], dtype=np.int64)
|
|
392
|
+
|
|
393
|
+
self.W_h = engine.W_enc.copy()
|
|
394
|
+
self.b_h = engine.b_enc.copy()
|
|
395
|
+
self.W_mu = engine.W_mu[active_indices, :].copy()
|
|
396
|
+
self.b_mu = engine.b_mu[active_indices].copy()
|
|
397
|
+
self._full_W_mu = engine.W_mu.copy()
|
|
398
|
+
self._full_b_mu = engine.b_mu.copy()
|
|
399
|
+
|
|
400
|
+
if self.keep_decoder:
|
|
401
|
+
self._W_dec1 = engine.W_dec1.copy()
|
|
402
|
+
self._b_dec1 = engine.b_dec1.copy()
|
|
403
|
+
self._W_dec2 = engine.W_dec2.copy()
|
|
404
|
+
self._b_dec2 = engine.b_dec2.copy()
|
|
405
|
+
else:
|
|
406
|
+
self._W_dec1 = None
|
|
407
|
+
self._b_dec1 = None
|
|
408
|
+
self._W_dec2 = None
|
|
409
|
+
self._b_dec2 = None
|
|
410
|
+
|
|
411
|
+
self.n_features_in_ = input_dim
|
|
412
|
+
self.training_dim_ = training_dim
|
|
413
|
+
self.active_indices_ = active_indices
|
|
414
|
+
self.feature_variances_ = feature_variances.copy()
|
|
415
|
+
self.effective_dim_ = int(active_indices.size)
|
|
416
|
+
self.is_fitted_ = True
|
|
417
|
+
|
|
418
|
+
if self.verbose:
|
|
419
|
+
print(
|
|
420
|
+
f"OBVAEKernel: Inflated to {training_dim}D, "
|
|
421
|
+
f"pruned to {self.effective_dim_} active features."
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return self
|
|
425
|
+
|
|
426
|
+
def transform(self, X: ArrayLike) -> np.ndarray:
|
|
427
|
+
self._require_fitted()
|
|
428
|
+
X_arr = self._to_2d_array(X, expected_features=int(self.n_features_in_))
|
|
429
|
+
z_hidden = X_arr @ self.W_h.T + self.b_h
|
|
430
|
+
h = np.maximum(z_hidden, 0.0)
|
|
431
|
+
mu = h @ self.W_mu.T + self.b_mu
|
|
432
|
+
if not np.isfinite(mu).all():
|
|
433
|
+
raise ValueError("Non-finite values encountered in transform output.")
|
|
434
|
+
return mu.astype(self.dtype, copy=False)
|
|
435
|
+
|
|
436
|
+
def fit_transform(self, X: ArrayLike, y: Optional[ArrayLike] = None) -> np.ndarray:
|
|
437
|
+
return self.fit(X, y).transform(X)
|
|
438
|
+
|
|
439
|
+
def inverse_transform(self, Z: ArrayLike) -> np.ndarray:
|
|
440
|
+
self._require_fitted()
|
|
441
|
+
if self._W_dec1 is None or self._b_dec1 is None or self._W_dec2 is None or self._b_dec2 is None:
|
|
442
|
+
raise RuntimeError("Decoder weights are unavailable (keep_decoder=False during fit).")
|
|
443
|
+
|
|
444
|
+
Z_arr = self._to_2d_array(Z)
|
|
445
|
+
if Z_arr.shape[1] == int(self.effective_dim_):
|
|
446
|
+
z_full = np.zeros((Z_arr.shape[0], int(self.training_dim_)), dtype=self.dtype)
|
|
447
|
+
z_full[:, self.active_indices_] = Z_arr
|
|
448
|
+
elif Z_arr.shape[1] == int(self.training_dim_):
|
|
449
|
+
z_full = Z_arr
|
|
450
|
+
else:
|
|
451
|
+
raise ValueError(
|
|
452
|
+
f"Expected latent width {self.effective_dim_} (pruned) "
|
|
453
|
+
f"or {self.training_dim_} (full), got {Z_arr.shape[1]}."
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
z_dec1 = z_full @ self._W_dec1.T + self._b_dec1
|
|
457
|
+
h_dec = np.maximum(z_dec1, 0.0)
|
|
458
|
+
x_recon = h_dec @ self._W_dec2.T + self._b_dec2
|
|
459
|
+
if not np.isfinite(x_recon).all():
|
|
460
|
+
raise ValueError("Non-finite values encountered in inverse_transform output.")
|
|
461
|
+
return x_recon.astype(self.dtype, copy=False)
|
|
462
|
+
|
|
463
|
+
def kernel_matrix(
|
|
464
|
+
self,
|
|
465
|
+
X: ArrayLike,
|
|
466
|
+
Y: Optional[ArrayLike] = None,
|
|
467
|
+
normalize: bool = True,
|
|
468
|
+
center: bool = False,
|
|
469
|
+
) -> np.ndarray:
|
|
470
|
+
self._require_fitted()
|
|
471
|
+
phi_x = self.transform(X)
|
|
472
|
+
phi_y = phi_x if Y is None else self.transform(Y)
|
|
473
|
+
|
|
474
|
+
if normalize:
|
|
475
|
+
phi_x = self._normalize_rows(phi_x)
|
|
476
|
+
phi_y = self._normalize_rows(phi_y)
|
|
477
|
+
|
|
478
|
+
K = phi_x @ phi_y.T
|
|
479
|
+
|
|
480
|
+
if center:
|
|
481
|
+
row_means = np.mean(K, axis=1, keepdims=True)
|
|
482
|
+
col_means = np.mean(K, axis=0, keepdims=True)
|
|
483
|
+
global_mean = np.mean(K)
|
|
484
|
+
K = K - row_means - col_means + global_mean
|
|
485
|
+
|
|
486
|
+
if Y is None:
|
|
487
|
+
K = 0.5 * (K + K.T)
|
|
488
|
+
|
|
489
|
+
if not np.isfinite(K).all():
|
|
490
|
+
raise ValueError("Non-finite values encountered in kernel matrix.")
|
|
491
|
+
|
|
492
|
+
return K.astype(self.dtype, copy=False)
|
|
493
|
+
|
|
494
|
+
def pairwise_scalar(self, x: ArrayLike, y: ArrayLike, normalize: bool = True) -> float:
|
|
495
|
+
self._require_fitted()
|
|
496
|
+
x_arr = self._to_single_sample(x)
|
|
497
|
+
y_arr = self._to_single_sample(y)
|
|
498
|
+
phi_x = self.transform(x_arr)[0]
|
|
499
|
+
phi_y = self.transform(y_arr)[0]
|
|
500
|
+
|
|
501
|
+
if normalize:
|
|
502
|
+
eps = np.finfo(phi_x.dtype).eps
|
|
503
|
+
phi_x = phi_x / max(float(np.linalg.norm(phi_x)), float(eps))
|
|
504
|
+
phi_y = phi_y / max(float(np.linalg.norm(phi_y)), float(eps))
|
|
505
|
+
|
|
506
|
+
value = float(np.dot(phi_x, phi_y))
|
|
507
|
+
if not np.isfinite(value):
|
|
508
|
+
raise ValueError("Non-finite value encountered in pairwise kernel output.")
|
|
509
|
+
return value
|
|
510
|
+
|
|
511
|
+
def as_pairwise_callable(self, normalize: bool = True) -> Callable[[ArrayLike, ArrayLike], float]:
|
|
512
|
+
def _kernel(a: ArrayLike, b: ArrayLike) -> float:
|
|
513
|
+
return self.pairwise_scalar(a, b, normalize=normalize)
|
|
514
|
+
|
|
515
|
+
return _kernel
|
|
516
|
+
|
|
517
|
+
def get_feature_variances(self) -> np.ndarray:
|
|
518
|
+
self._require_fitted()
|
|
519
|
+
return self.feature_variances_.copy()
|
|
520
|
+
|
|
521
|
+
def _config_dict(self) -> Dict[str, object]:
|
|
522
|
+
return {
|
|
523
|
+
"latent_dim": self.latent_dim,
|
|
524
|
+
"expansion_factor": int(self.expansion_factor),
|
|
525
|
+
"hidden_dim": int(self.hidden_dim),
|
|
526
|
+
"beta": float(self.beta),
|
|
527
|
+
"epochs": int(self.epochs),
|
|
528
|
+
"lr": float(self.lr),
|
|
529
|
+
"batch_size": None if self.batch_size is None else int(self.batch_size),
|
|
530
|
+
"variance_threshold": float(self.variance_threshold),
|
|
531
|
+
"dtype": self.dtype.name,
|
|
532
|
+
"keep_decoder": bool(self.keep_decoder),
|
|
533
|
+
"verbose": bool(self.verbose),
|
|
534
|
+
"random_state": self.random_state,
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
def save(self, path_prefix: str) -> None:
|
|
538
|
+
self._require_fitted()
|
|
539
|
+
prefix = Path(path_prefix)
|
|
540
|
+
npz_path = Path(f"{prefix}.npz")
|
|
541
|
+
json_path = Path(f"{prefix}.json")
|
|
542
|
+
|
|
543
|
+
arrays: Dict[str, np.ndarray] = {
|
|
544
|
+
"W_h": self.W_h,
|
|
545
|
+
"b_h": self.b_h,
|
|
546
|
+
"W_mu": self.W_mu,
|
|
547
|
+
"b_mu": self.b_mu,
|
|
548
|
+
"full_W_mu": self._full_W_mu,
|
|
549
|
+
"full_b_mu": self._full_b_mu,
|
|
550
|
+
"active_indices": self.active_indices_.astype(np.int64, copy=False),
|
|
551
|
+
"feature_variances": self.feature_variances_,
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
has_decoder = (
|
|
555
|
+
self._W_dec1 is not None
|
|
556
|
+
and self._b_dec1 is not None
|
|
557
|
+
and self._W_dec2 is not None
|
|
558
|
+
and self._b_dec2 is not None
|
|
559
|
+
)
|
|
560
|
+
if has_decoder:
|
|
561
|
+
arrays["W_dec1"] = self._W_dec1
|
|
562
|
+
arrays["b_dec1"] = self._b_dec1
|
|
563
|
+
arrays["W_dec2"] = self._W_dec2
|
|
564
|
+
arrays["b_dec2"] = self._b_dec2
|
|
565
|
+
|
|
566
|
+
np.savez(npz_path, **arrays)
|
|
567
|
+
|
|
568
|
+
metadata = {
|
|
569
|
+
"version": self.SERIAL_VERSION,
|
|
570
|
+
"class_name": "OBVAEKernel",
|
|
571
|
+
"config": self._config_dict(),
|
|
572
|
+
"fitted": {
|
|
573
|
+
"n_features_in_": int(self.n_features_in_),
|
|
574
|
+
"training_dim_": int(self.training_dim_),
|
|
575
|
+
"effective_dim_": int(self.effective_dim_),
|
|
576
|
+
"has_decoder": bool(has_decoder),
|
|
577
|
+
},
|
|
578
|
+
}
|
|
579
|
+
json_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8")
|
|
580
|
+
|
|
581
|
+
@classmethod
|
|
582
|
+
def load(cls, path_prefix: str) -> "OBVAEKernel":
|
|
583
|
+
prefix = Path(path_prefix)
|
|
584
|
+
npz_path = Path(f"{prefix}.npz")
|
|
585
|
+
json_path = Path(f"{prefix}.json")
|
|
586
|
+
|
|
587
|
+
metadata = json.loads(json_path.read_text(encoding="utf-8"))
|
|
588
|
+
config = metadata["config"]
|
|
589
|
+
model = cls(**config)
|
|
590
|
+
|
|
591
|
+
with np.load(npz_path, allow_pickle=False) as data:
|
|
592
|
+
model.W_h = np.asarray(data["W_h"], dtype=model.dtype)
|
|
593
|
+
model.b_h = np.asarray(data["b_h"], dtype=model.dtype)
|
|
594
|
+
model.W_mu = np.asarray(data["W_mu"], dtype=model.dtype)
|
|
595
|
+
model.b_mu = np.asarray(data["b_mu"], dtype=model.dtype)
|
|
596
|
+
model._full_W_mu = np.asarray(data["full_W_mu"], dtype=model.dtype)
|
|
597
|
+
model._full_b_mu = np.asarray(data["full_b_mu"], dtype=model.dtype)
|
|
598
|
+
model.active_indices_ = np.asarray(data["active_indices"], dtype=np.int64)
|
|
599
|
+
model.feature_variances_ = np.asarray(data["feature_variances"], dtype=model.dtype)
|
|
600
|
+
|
|
601
|
+
has_decoder = bool(metadata["fitted"]["has_decoder"])
|
|
602
|
+
if has_decoder:
|
|
603
|
+
model._W_dec1 = np.asarray(data["W_dec1"], dtype=model.dtype)
|
|
604
|
+
model._b_dec1 = np.asarray(data["b_dec1"], dtype=model.dtype)
|
|
605
|
+
model._W_dec2 = np.asarray(data["W_dec2"], dtype=model.dtype)
|
|
606
|
+
model._b_dec2 = np.asarray(data["b_dec2"], dtype=model.dtype)
|
|
607
|
+
else:
|
|
608
|
+
model._W_dec1 = None
|
|
609
|
+
model._b_dec1 = None
|
|
610
|
+
model._W_dec2 = None
|
|
611
|
+
model._b_dec2 = None
|
|
612
|
+
|
|
613
|
+
model.n_features_in_ = int(metadata["fitted"]["n_features_in_"])
|
|
614
|
+
model.training_dim_ = int(metadata["fitted"]["training_dim_"])
|
|
615
|
+
model.effective_dim_ = int(metadata["fitted"]["effective_dim_"])
|
|
616
|
+
model.is_fitted_ = True
|
|
617
|
+
return model
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
OBVAEK = OBVAEKernel
|
|
621
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: obvaekernel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Portable NumPy-first OBVAE kernel features for PCA/kernel workflows.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sweet00000/OvercompleteBetaVariationalAutoEncoderKernel
|
|
6
|
+
Project-URL: Repository, https://github.com/sweet00000/OvercompleteBetaVariationalAutoEncoderKernel
|
|
7
|
+
Author: Sweet
|
|
8
|
+
License: BSD 3-Clause License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026, Sweet
|
|
11
|
+
All rights reserved.
|
|
12
|
+
|
|
13
|
+
Redistribution and use in source and binary forms, with or without
|
|
14
|
+
modification, are permitted provided that the following conditions are met:
|
|
15
|
+
|
|
16
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
17
|
+
list of conditions and the following disclaimer.
|
|
18
|
+
|
|
19
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
20
|
+
this list of conditions and the following disclaimer in the documentation
|
|
21
|
+
and/or other materials provided with the distribution.
|
|
22
|
+
|
|
23
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
24
|
+
contributors may be used to endorse or promote products derived from
|
|
25
|
+
this software without specific prior written permission.
|
|
26
|
+
|
|
27
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
28
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
29
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
30
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
31
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
32
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
33
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
34
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
35
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
36
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
37
|
+
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
Classifier: Development Status :: 3 - Alpha
|
|
40
|
+
Classifier: Intended Audience :: Science/Research
|
|
41
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
42
|
+
Classifier: Programming Language :: Python :: 3
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
45
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
46
|
+
Requires-Python: >=3.10
|
|
47
|
+
Requires-Dist: numpy>=1.23
|
|
48
|
+
Provides-Extra: benchmark
|
|
49
|
+
Requires-Dist: matplotlib>=3.7; extra == 'benchmark'
|
|
50
|
+
Requires-Dist: pandas>=1.5; extra == 'benchmark'
|
|
51
|
+
Requires-Dist: requests>=2.31; extra == 'benchmark'
|
|
52
|
+
Requires-Dist: scikit-learn>=1.2; extra == 'benchmark'
|
|
53
|
+
Requires-Dist: scipy>=1.9; extra == 'benchmark'
|
|
54
|
+
Provides-Extra: dev
|
|
55
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
56
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
57
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
58
|
+
Description-Content-Type: text/markdown
|
|
59
|
+
|
|
60
|
+
# obvaekernel
|
|
61
|
+
|
|
62
|
+
`obvaekernel` is a NumPy-first implementation of an overcomplete beta-VAE kernel feature map designed for PCA and kernel-method workflows.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Benchmark/data extras:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install .[benchmark]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Development/build extras:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install .[dev]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Quick Usage
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import numpy as np
|
|
86
|
+
from obvaekernel import OBVAEKernel
|
|
87
|
+
|
|
88
|
+
X = np.random.randn(256, 12).astype("float32")
|
|
89
|
+
model = OBVAEKernel(latent_dim="auto", epochs=25, random_state=42)
|
|
90
|
+
Z = model.fit_transform(X)
|
|
91
|
+
K = model.kernel_matrix(X)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Benchmark Scripts
|
|
95
|
+
|
|
96
|
+
Run the root benchmark harness:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python main.py --mode quick --no-plots
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Run the examples copy:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
python examples/benchmark_main.py --mode full --include-weather
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Build and Publish Checks
|
|
109
|
+
|
|
110
|
+
Build wheel + sdist:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
python -m build
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Validate distribution metadata:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
python -m twine check dist/*
|
|
120
|
+
```
|
|
121
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
obvaekernel/__init__.py,sha256=gsOb06tzViAw-R3TjeyV8V0dnPUaWHL74omkhy1VLXc,174
|
|
2
|
+
obvaekernel/_version.py,sha256=m2jSerkTHRf7k7u-zWcItoCAXxz6nYPl6D83y7ho0M4,47
|
|
3
|
+
obvaekernel/core.py,sha256=gQ6-bH1xAPjl_-8DLh50iCxW9C8Ct2Gj9SYenfYYgrI,23616
|
|
4
|
+
obvaekernel-0.1.0.dist-info/METADATA,sha256=DdqEjhSANKE4Yp16hNvPIF9laIBX0QJ0f7-QG8fU3ys,3939
|
|
5
|
+
obvaekernel-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
obvaekernel-0.1.0.dist-info/licenses/LICENSE,sha256=Sthn0-FlQjNpBd4wfp6leeGwi7W1X6PEe-B-YfO6z9c,1514
|
|
7
|
+
obvaekernel-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Sweet
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|