RR-layer 0.1.1__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.
RR_layer/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .rr_layer import RRLayer
RR_layer/rr_layer.py ADDED
@@ -0,0 +1,375 @@
1
+ import warnings
2
+ import torch
3
+ import torch.nn as nn
4
+ from collections import deque
5
+
6
+ def stable_SVD(A):
7
+ """ Computes a numerically stable SVD.
8
+
9
+ A: input matrix (..., m, n)
10
+ """
11
+ return StableSVD.apply(A)
12
+
13
+ class StableSVD(torch.autograd.Function):
14
+ @staticmethod
15
+ def forward(ctx, A):
16
+ # Just compute standard SVD
17
+ U, S, Vh = torch.linalg.svd(A, full_matrices=False)
18
+
19
+ # Save for backward
20
+ ctx.save_for_backward(U, S, Vh)
21
+ ctx.original_shape = A.shape
22
+
23
+ return U, S, Vh
24
+
25
+ @staticmethod
26
+ def backward(ctx, dU, dS, dVh):
27
+ """
28
+ Backward pass for stable SVD.
29
+ Computes gradient w.r.t. input A given gradients on U, S, Vh.
30
+ """
31
+ U, S, Vh = ctx.saved_tensors
32
+ m, n = ctx.original_shape[-2:]
33
+
34
+ dtype = U.dtype
35
+ device = U.device
36
+
37
+ H = lambda x: x.transpose(-2, -1).conj()
38
+ T = lambda x: x.transpose(-2, -1)
39
+
40
+ # Diagonal helpers
41
+ def diag_embed(x):
42
+ return torch.diag_embed(x)
43
+
44
+ # Singular value vector to diagonal for broadcasting
45
+ S_mat = S.unsqueeze(-2)
46
+ S_diff = S_mat - S_mat.transpose(-2, -1)
47
+
48
+ # F matrix for repeated singular values
49
+ eps = 1e-20
50
+ F = torch.where(S_diff.abs() > eps, 1.0 / S_diff, torch.zeros_like(S_diff))
51
+
52
+ # Gradient from singular values
53
+ dA = U @ diag_embed(dS) @ Vh
54
+
55
+ # Contributions from U
56
+ Ut_dU = H(U) @ dU
57
+ skew_U = F * (Ut_dU - Ut_dU.transpose(-2, -1))
58
+ dA += U @ skew_U @ diag_embed(S) @ Vh
59
+
60
+ # Contributions from V
61
+ V = H(Vh) # n x k
62
+ Vt_dV = H(V) @ H(dVh) # k x k
63
+ skew_V = F * (Vt_dV - Vt_dV.transpose(-2, -1))
64
+ dA += U @ diag_embed(S) @ skew_V @ Vh
65
+
66
+ # Rectangular adjustments (like JAX)
67
+ # s_inv = 1 / S with stable zero handling
68
+ s_zeros = (S == 0).to(dtype)
69
+ s_inv = 1.0 / (S + s_zeros) - s_zeros # shape: (k,)
70
+
71
+ if m > n:
72
+ dAV = dA @ V
73
+ dA += (dAV - U @ (H(U) @ dAV)) * s_inv
74
+ elif n > m:
75
+ dAHU = H(dA) @ U
76
+ print("first term shape: ", H(dAHU - V @ (Vh @ dAHU)).shape)
77
+ print("s_inv shape: ", s_inv.shape)
78
+
79
+ dA += H(dAHU - V @ (Vh @ dAHU)) * s_inv.unsqueeze(1)
80
+
81
+ return dA
82
+
83
+
84
+ import math
85
+ import warnings
86
+ from collections import deque
87
+
88
+ import torch
89
+ import torch.nn as nn
90
+
91
+
92
+ class RRLayer(nn.Module):
93
+ r"""
94
+ Rank Reduction (RR) layer.
95
+
96
+ During training, the layer computes a truncated SVD across the batch
97
+ dimension and reconstructs the input using the top ``rank`` singular
98
+ components.
99
+
100
+ During evaluation, the layer projects the input onto a learned inference
101
+ basis obtained from the recent training bases.
102
+
103
+ A custom basis can always be supplied through the ``basis`` argument of
104
+ :meth:`forward`, in which case the train/eval behavior is bypassed.
105
+
106
+ Args:
107
+ rank (int):
108
+ Number of singular values to retain.
109
+
110
+ basis_history_size (int, optional):
111
+ Number of recent batch bases to keep when estimating the inference
112
+ basis. Default: 20.
113
+
114
+ Shape:
115
+ - Input: ``(N, *)``
116
+ - Output: ``(N, *)``
117
+
118
+ Example:
119
+ >>> rr = RRLayer(rank=8)
120
+ >>> rr.train()
121
+ >>> y = rr(torch.randn(32, 768))
122
+
123
+ >>> rr.eval()
124
+ >>> y = rr(torch.randn(32, 768))
125
+ """
126
+
127
+ def __init__(
128
+ self,
129
+ rank: int,
130
+ basis_history_size: int = 20,
131
+ ):
132
+ super().__init__()
133
+
134
+ if rank <= 0:
135
+ raise ValueError(
136
+ f"rank must be positive, got {rank}."
137
+ )
138
+
139
+ if basis_history_size <= 0:
140
+ raise ValueError(
141
+ f"basis_history_size must be positive, got "
142
+ f"{basis_history_size}."
143
+ )
144
+
145
+ self.rank = rank
146
+ self.basis_history_size = basis_history_size
147
+
148
+ self.register_buffer(
149
+ "inference_basis",
150
+ None,
151
+ persistent=True,
152
+ )
153
+
154
+ self._basis_bank = deque(
155
+ maxlen=basis_history_size,
156
+ )
157
+
158
+ def extra_repr(self) -> str:
159
+ return (
160
+ f"rank={self.rank}, "
161
+ f"basis_history_size={self.basis_history_size}"
162
+ )
163
+
164
+ def train(self, mode: bool = True):
165
+ """
166
+ Switch between training and evaluation mode.
167
+
168
+ When switching from training to evaluation for the first time,
169
+ an inference basis is automatically computed from the stored
170
+ training bases.
171
+ """
172
+
173
+ previous_mode = self.training
174
+
175
+ super().train(mode)
176
+
177
+ if (
178
+ previous_mode
179
+ and not mode
180
+ and self.inference_basis is None
181
+ and len(self._basis_bank) > 0
182
+ ):
183
+ self.finalize_basis()
184
+
185
+ return self
186
+
187
+ @torch.no_grad()
188
+ def finalize_basis(self) -> None:
189
+ """
190
+ Build the inference basis from the stored training bases.
191
+ """
192
+
193
+ if len(self._basis_bank) == 0:
194
+ raise RuntimeError(
195
+ "Cannot finalize basis: no stored bases available."
196
+ )
197
+
198
+ device = next(self.parameters(), None)
199
+
200
+ if device is None:
201
+ device = self.inference_basis.device \
202
+ if self.inference_basis is not None \
203
+ else self._basis_bank[0].device
204
+ else:
205
+ device = device.device
206
+
207
+ W = torch.cat(
208
+ [
209
+ basis.to(device)
210
+ for basis in self._basis_bank
211
+ ],
212
+ dim=1,
213
+ )
214
+
215
+ U, _, _ = stable_SVD(W)
216
+
217
+ r = min(self.rank, U.shape[1])
218
+
219
+ self.inference_basis = U[:, :r]
220
+
221
+ def _validate_inputs(
222
+ self,
223
+ x: torch.Tensor,
224
+ basis: torch.Tensor | None,
225
+ ) -> None:
226
+ """
227
+ Validate inputs for RRLayer.
228
+ """
229
+
230
+ if not isinstance(x, torch.Tensor):
231
+ raise TypeError(
232
+ f"x must be a torch.Tensor, got {type(x)}."
233
+ )
234
+
235
+ if x.ndim < 2:
236
+ raise ValueError(
237
+ "RRLayer expects input of shape (N, ...), "
238
+ f"got shape {tuple(x.shape)}."
239
+ )
240
+
241
+ if x.shape[0] == 0:
242
+ raise ValueError(
243
+ "Batch size must be greater than zero."
244
+ )
245
+
246
+ if not torch.isfinite(x).all():
247
+ raise ValueError(
248
+ "Input tensor contains NaN or Inf values."
249
+ )
250
+
251
+ M = math.prod(x.shape[1:])
252
+ N = x.shape[0]
253
+
254
+ rank_max = min(M, N)
255
+
256
+ if self.rank > rank_max:
257
+ warnings.warn(
258
+ f"Requested rank={self.rank}, but the maximum "
259
+ f"achievable rank is {rank_max}. "
260
+ f"Using rank={rank_max}.",
261
+ stacklevel=2,
262
+ )
263
+
264
+ if basis is not None:
265
+
266
+ if not isinstance(basis, torch.Tensor):
267
+ raise TypeError(
268
+ f"basis must be a torch.Tensor, got {type(basis)}."
269
+ )
270
+
271
+ if basis.ndim != 2:
272
+ raise ValueError(
273
+ "basis must have shape (M, r), "
274
+ f"got shape {tuple(basis.shape)}."
275
+ )
276
+
277
+ if basis.shape[0] != M:
278
+ raise ValueError(
279
+ f"basis has {basis.shape[0]} rows but "
280
+ f"expected {M}."
281
+ )
282
+
283
+ if basis.shape[1] == 0:
284
+ raise ValueError(
285
+ "basis must contain at least one column."
286
+ )
287
+
288
+ if basis.device != x.device:
289
+ raise ValueError(
290
+ f"basis is on {basis.device} while "
291
+ f"x is on {x.device}."
292
+ )
293
+
294
+ if basis.dtype != x.dtype:
295
+ raise ValueError(
296
+ f"basis dtype ({basis.dtype}) does not match "
297
+ f"x dtype ({x.dtype})."
298
+ )
299
+
300
+ if not torch.isfinite(basis).all():
301
+ raise ValueError(
302
+ "basis contains NaN or Inf values."
303
+ )
304
+
305
+ def forward(
306
+ self,
307
+ x: torch.Tensor,
308
+ basis: torch.Tensor | None = None,
309
+ return_factors: bool = False,
310
+ ):
311
+ self._validate_inputs(x, basis)
312
+
313
+ original_shape = x.shape
314
+ batch_size = original_shape[0]
315
+
316
+ X = torch.movedim(x, 0, -1)
317
+ X = X.reshape(-1, batch_size)
318
+
319
+ if basis is not None:
320
+
321
+ basis_used = basis
322
+
323
+ coeffs = basis_used.T @ X
324
+
325
+ X_hat = basis_used @ coeffs
326
+
327
+ elif self.training:
328
+
329
+ U, S, Vh = stable_SVD(X)
330
+
331
+ r = min(self.rank, S.shape[0])
332
+
333
+ basis_used = U[:, :r]
334
+
335
+ coeffs = (
336
+ S[:r].unsqueeze(1)
337
+ * Vh[:r]
338
+ )
339
+
340
+ X_hat = basis_used @ coeffs
341
+
342
+ self._basis_bank.append(
343
+ basis_used.detach().cpu()
344
+ )
345
+
346
+ else:
347
+
348
+ if self.inference_basis is None:
349
+ raise RuntimeError(
350
+ "RRLayer has no inference basis. "
351
+ "Call finalize_basis() or run training "
352
+ "before evaluation."
353
+ )
354
+
355
+ basis_used = self.inference_basis
356
+
357
+ coeffs = basis_used.T @ X
358
+
359
+ X_hat = basis_used @ coeffs
360
+
361
+ output = X_hat.reshape(
362
+ *original_shape[1:],
363
+ batch_size,
364
+ )
365
+
366
+ output = torch.movedim(
367
+ output,
368
+ -1,
369
+ 0,
370
+ )
371
+
372
+ if return_factors:
373
+ return output, basis_used, coeffs
374
+
375
+ return output
RR_layer/utils.py ADDED
File without changes
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: RR_layer
3
+ Version: 0.1.1
4
+ Summary: A repo for RR layer in Pytorch.
5
+ Author-email: Jad Mounayer <jad.mounayer@outlook.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: matplotlib
10
+ Requires-Dist: numpy
11
+ Requires-Dist: torch
12
+ Requires-Dist: torchvision
13
+ Description-Content-Type: text/markdown
14
+
15
+ # RR_layer
16
+
17
+ `RR_layer` is a JAX / Equinox library implementing a **Rank-Reduction (RR) layer**: a layer that compresses its input by projecting onto a low-rank SVD basis.
18
+
19
+ The mechanics are different between training and evaluation:
20
+
21
+ - **During training**, the SVD basis is **recomputed from scratch for every batch**. For each batch, the layer computes the SVD of the batch, truncates to the top `r` components, and uses that truncated basis to project — so the basis is always data-dependent and up to date with the current batch, and gradients flow through the SVD/projection step. The model also automatically saves the last `N=basis_history_size` bases to be later used in evaluation.
22
+ - **During evaluation**, when layer.eval() is called, the layer will find a common basis to the whole dataset `Uf`, based on the last `N=basis_history_size` that were saved. When the layer is called in evaluation mode, the SVD is replaced by a projection on that basis `Uf`.
23
+ - In both cases, the shape of the input should be (N x ...), where N is the number samples in a batch. Note that the output will have the same shape is the input. This is because the layer performs the SVD, truncates, but then returns the reconstructed latent space. So the compression happens inside eventhough the output still has the same shape.
24
+
25
+ ## What's inside
26
+
27
+ ```
28
+ RR_layer/
29
+ ├── RR_layer/
30
+ │ ├── __init__.py
31
+ │ └── rr_layer.py # the layer implementation(s)
32
+ ├── examples/
33
+ | ├──── autoencoder
34
+ │ | ├── generate_data.ipynb
35
+ │ | ├── train_baseline.ipynb
36
+ │ | ├── train_RRAE.ipynb
37
+ │ | └── compare.ipynb
38
+ └── tests/
39
+ └── tests.py
40
+ ```
41
+
42
+ ### `RR_layer/rr_layer.py`
43
+
44
+ The library code. Contains the `RR_layer` Equinox module: a layer with two modes of operation —
45
+
46
+ - **train mode**: computes the SVD of the current batch's activations, truncates to rank `r`, and projects onto that basis (gradients flow through this).
47
+ - **eval mode**: projects onto a basis that was saved from training, rather than computing a new SVD.
48
+
49
+ ### `Examples/`
50
+
51
+ The example notebooks are meant to be worked through **in order**:
52
+
53
+ 1. **`generate_data.ipynb`** — generates the dataset used in the other notebooks. Run this first; it should leave behind whatever data artifacts the later notebooks load.
54
+
55
+ 2. **`train_baseline.ipynb`** — trains a standard, non-RR autoencoder as a reference point. This gives you a baseline reconstruction quality and runtime to compare the RRAE against.
56
+
57
+ 3. **`train_RRAE.ipynb`** — trains the Rank-Reduction Auto-Encoder, built with one or more `RR_layer` in the latent space. During training this recomputes the SVD basis per batch as described above; at the end of training it should save the basis to be reused at evaluation time.
58
+
59
+ 4. **`compare.ipynb`** — loads the trained baseline and RRAE models (using the saved, frozen SVD basis for the RRAE) and compares them for tasks such as linear interpolation and random generation.
60
+
61
+ Run the notebooks in the order above — each later notebook generally depends on artifacts (data, images) produced by the earlier ones.
62
+
63
+ ### `tests/tests.py`
64
+
65
+ Unit tests for the `RR_layer` module. Useful both to verify correctness after installing, and as additional usage examples of the layer API beyond the notebooks (e.g. shape checks, gradient flow through the SVD step in train mode, etc.)
66
+
67
+ ## Running the tests
68
+
69
+ ```bash
70
+ python -m pytest tests/tests.py
71
+ ```
72
+
73
+ ## Installation
74
+
75
+ ```bash
76
+ pip install RR_layer
77
+ ```
78
+
79
+ ## Quick start
80
+
81
+ ```python
82
+ import jax
83
+ from RR_layer import RRLayer # replace with actual export
84
+
85
+ inp = torch.randn(32, 768)
86
+ rr = RRLayer(rank=8)
87
+
88
+ rr.train()
89
+ y = rr(inp) # will compute the SVD
90
+
91
+ rr.eval()
92
+ y = rr(inp) # will project onto a saved basis
93
+ ```
94
+
95
+ ## Background
96
+
97
+ For more information about Rank Reduction layers, please refer to:
98
+
99
+ - **Paper:** [Rank Reduction Autoencoders](https://arxiv.org/abs/2405.13980)
100
+ - **Related repo:** [JadM133/RRAEs](https://github.com/JadM133/RRAEs)
@@ -0,0 +1,7 @@
1
+ RR_layer/__init__.py,sha256=OjDRIEzUoj_Jgnm-nZofAoJAZrioLMhuf16kYHWmgc8,31
2
+ RR_layer/rr_layer.py,sha256=QEboZdhPMDHZvULlRLJQnO2nHD4H1h5zbpfZcSxdNk4,10027
3
+ RR_layer/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ rr_layer-0.1.1.dist-info/METADATA,sha256=HJNXocCuUI5vbPAZSj2IJI-WibvTzWLeZn1HDiAc68Q,4369
5
+ rr_layer-0.1.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ rr_layer-0.1.1.dist-info/licenses/LICENSE,sha256=QQKXj7kEw2yUJxPDzHUnEPRO7YweIU6AAlwPLnhtinA,1090
7
+ rr_layer-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jad Mounayer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.