ARotting commited on
Commit
db4bb00
·
verified ·
1 Parent(s): 72dc3c4

Publish Predictability-minimized factorial code and adversarial ablation

Browse files
README.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ tags:
4
+ - independent-component-analysis
5
+ - adversarial-learning
6
+ - representation-learning
7
+ - factorial-codes
8
+ - gradio
9
+ ---
10
+
11
+ # Factorial Code Forge
12
+
13
+ Factorial Code Forge mixes two statistically independent non-Gaussian sources and
14
+ asks a tiny autoencoder to recover a useful two-dimensional code. In the experimental
15
+ variant, two adversarial predictors try to infer each latent coordinate from the
16
+ other while the encoder tries to make those predictions fail. Reconstruction and
17
+ variance constraints prevent a constant-code shortcut.
18
+
19
+ The historical anchor is Schmidhuber's 1992
20
+ [Learning Factorial Codes by Predictability
21
+ Minimization](https://doi.org/10.1162/neco.1992.4.6.863). The modern experiment
22
+ compares predictability minimization with an architecture-matched reconstructive
23
+ encoder, PCA whitening, and FastICA. It is a new controlled benchmark, not a
24
+ reproduction of the original paper's implementation or datasets.
25
+
26
+ ## Verified results
27
+
28
+ Models trained on 20,000 mixed samples and were measured once on 5,000 new samples:
29
+
30
+ | Code | Absolute correlation | Symmetric MI estimate |
31
+ | --- | ---: | ---: |
32
+ | Autoencoder control | 0.02200 | 0.22345 |
33
+ | Online neural-adversarial ablation | 0.01886 | 0.22678 |
34
+ | Validation-selected predictability minimum | 0.01158 | 0.00280 |
35
+ | PCA whitening | 0.02039 | 0.22275 |
36
+ | FastICA | 0.00590 | 0.00326 |
37
+
38
+ The selected 12-parameter linear codec reconstructed the mixtures at
39
+ `1.08e-14` MSE. Its rotation was selected using polynomial cross-predictors on a
40
+ held-out portion of the training set, never the test set. The online neural
41
+ minimax game did not improve nonlinear dependence and is retained as a negative
42
+ stability result rather than omitted.
43
+
44
+ ## Reproduce
45
+
46
+ ```powershell
47
+ uv run python projects/factorial-code-forge/train.py
48
+ ```
codec.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ee01726e068b8d357d49d71b28916453915ad04b41643f1d32204cae1326d63d
3
+ size 328
evaluation.json ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "benchmark": "Two-source factorial code recovery",
3
+ "codec_parameters": 12,
4
+ "adversary_parameters": 1346,
5
+ "test_reconstruction_mse": 1.0849382441166684e-14,
6
+ "selected_rotation_radians": 0.6335255903647925,
7
+ "validation_polynomial_predictor_error": 2.0412044050069174,
8
+ "results": {
9
+ "autoencoder_control": {
10
+ "absolute_correlation": 0.02199843078760486,
11
+ "symmetric_mutual_information_estimate": 0.22345146065238453,
12
+ "linear_source_recovery_r2": 1.0
13
+ },
14
+ "neural_adversarial_ablation": {
15
+ "absolute_correlation": 0.018864639802400177,
16
+ "symmetric_mutual_information_estimate": 0.22677681116066317,
17
+ "linear_source_recovery_r2": 1.0
18
+ },
19
+ "predictability_minimization": {
20
+ "absolute_correlation": 0.011581345795229356,
21
+ "symmetric_mutual_information_estimate": 0.002795710068105528,
22
+ "linear_source_recovery_r2": 1.0
23
+ },
24
+ "pca_whitened": {
25
+ "absolute_correlation": 0.02038519159561663,
26
+ "symmetric_mutual_information_estimate": 0.22274881647833844,
27
+ "linear_source_recovery_r2": 1.0
28
+ },
29
+ "fastica": {
30
+ "absolute_correlation": 0.005903947226495519,
31
+ "symmetric_mutual_information_estimate": 0.003260830744820531,
32
+ "linear_source_recovery_r2": 1.0
33
+ }
34
+ },
35
+ "training_history": [
36
+ {
37
+ "step": 100,
38
+ "reconstruction_loss": 0.5751938223838806,
39
+ "whitening_loss": 0.4999920129776001,
40
+ "predictor_loss": 0.03917834907770157
41
+ },
42
+ {
43
+ "step": 200,
44
+ "reconstruction_loss": 0.10436813533306122,
45
+ "whitening_loss": 0.4999969005584717,
46
+ "predictor_loss": 0.012760449200868607
47
+ },
48
+ {
49
+ "step": 300,
50
+ "reconstruction_loss": 0.09469099342823029,
51
+ "whitening_loss": 0.49983352422714233,
52
+ "predictor_loss": 0.0026066915597766638
53
+ },
54
+ {
55
+ "step": 400,
56
+ "reconstruction_loss": 0.0830593854188919,
57
+ "whitening_loss": 0.49922531843185425,
58
+ "predictor_loss": 0.004627525806427002
59
+ },
60
+ {
61
+ "step": 500,
62
+ "reconstruction_loss": 0.08060496300458908,
63
+ "whitening_loss": 0.4975007474422455,
64
+ "predictor_loss": 0.01004670187830925
65
+ },
66
+ {
67
+ "step": 600,
68
+ "reconstruction_loss": 0.07426909357309341,
69
+ "whitening_loss": 0.4930780827999115,
70
+ "predictor_loss": 0.02585626393556595
71
+ },
72
+ {
73
+ "step": 700,
74
+ "reconstruction_loss": 0.0617077499628067,
75
+ "whitening_loss": 0.48229295015335083,
76
+ "predictor_loss": 0.06387390196323395
77
+ },
78
+ {
79
+ "step": 800,
80
+ "reconstruction_loss": 0.040828146040439606,
81
+ "whitening_loss": 0.4678165912628174,
82
+ "predictor_loss": 0.1077573299407959
83
+ },
84
+ {
85
+ "step": 900,
86
+ "reconstruction_loss": 0.022691549733281136,
87
+ "whitening_loss": 0.3986736834049225,
88
+ "predictor_loss": 0.36198264360427856
89
+ },
90
+ {
91
+ "step": 1000,
92
+ "reconstruction_loss": 0.021030452102422714,
93
+ "whitening_loss": 0.23594792187213898,
94
+ "predictor_loss": 0.9913151860237122
95
+ },
96
+ {
97
+ "step": 1100,
98
+ "reconstruction_loss": 0.04022720083594322,
99
+ "whitening_loss": 0.009166543371975422,
100
+ "predictor_loss": 1.8695604801177979
101
+ },
102
+ {
103
+ "step": 1200,
104
+ "reconstruction_loss": 0.003848953638225794,
105
+ "whitening_loss": 0.00017961427511181682,
106
+ "predictor_loss": 1.7930779457092285
107
+ },
108
+ {
109
+ "step": 1300,
110
+ "reconstruction_loss": 0.0014683017507195473,
111
+ "whitening_loss": 0.004436715971678495,
112
+ "predictor_loss": 1.7725040912628174
113
+ },
114
+ {
115
+ "step": 1400,
116
+ "reconstruction_loss": 0.0005974461091682315,
117
+ "whitening_loss": 0.0012278903741389513,
118
+ "predictor_loss": 1.8158133029937744
119
+ },
120
+ {
121
+ "step": 1500,
122
+ "reconstruction_loss": 0.0003378715191502124,
123
+ "whitening_loss": 0.003598805284127593,
124
+ "predictor_loss": 1.7867335081100464
125
+ },
126
+ {
127
+ "step": 1600,
128
+ "reconstruction_loss": 6.85915001668036e-05,
129
+ "whitening_loss": 0.0005436374340206385,
130
+ "predictor_loss": 1.8233861923217773
131
+ },
132
+ {
133
+ "step": 1700,
134
+ "reconstruction_loss": 0.0001249544438906014,
135
+ "whitening_loss": 0.00044188593165017664,
136
+ "predictor_loss": 1.8262327909469604
137
+ },
138
+ {
139
+ "step": 1800,
140
+ "reconstruction_loss": 1.3117482012603432e-05,
141
+ "whitening_loss": 0.00011215780978091061,
142
+ "predictor_loss": 1.8308453559875488
143
+ },
144
+ {
145
+ "step": 1900,
146
+ "reconstruction_loss": 1.1593769158935174e-05,
147
+ "whitening_loss": 0.0016796954441815615,
148
+ "predictor_loss": 1.7771589756011963
149
+ },
150
+ {
151
+ "step": 2000,
152
+ "reconstruction_loss": 0.0004471217398531735,
153
+ "whitening_loss": 0.004560444969683886,
154
+ "predictor_loss": 1.7743375301361084
155
+ },
156
+ {
157
+ "step": 2100,
158
+ "reconstruction_loss": 2.6598576368996873e-05,
159
+ "whitening_loss": 0.0024056474212557077,
160
+ "predictor_loss": 1.7827980518341064
161
+ },
162
+ {
163
+ "step": 2200,
164
+ "reconstruction_loss": 4.374821946839802e-05,
165
+ "whitening_loss": 7.564837142126635e-05,
166
+ "predictor_loss": 1.8309648036956787
167
+ },
168
+ {
169
+ "step": 2300,
170
+ "reconstruction_loss": 0.00020850264991167933,
171
+ "whitening_loss": 0.005683479364961386,
172
+ "predictor_loss": 1.822058916091919
173
+ },
174
+ {
175
+ "step": 2400,
176
+ "reconstruction_loss": 2.0745503206853755e-05,
177
+ "whitening_loss": 8.849342520989012e-07,
178
+ "predictor_loss": 1.8361434936523438
179
+ },
180
+ {
181
+ "step": 2500,
182
+ "reconstruction_loss": 1.0664540241123177e-05,
183
+ "whitening_loss": 0.0009145042859017849,
184
+ "predictor_loss": 1.7433021068572998
185
+ }
186
+ ]
187
+ }
latent_comparison.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:efcdab9ba10b29127fb11ed21ae13df08e1b443ce6becc0f018163495cd7e050
3
+ size 260780
predictor_01.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3f637d7931c2ae64658844975ee2107a5a5a3629af5a9d063b22a1dcef91a67f
3
+ size 3140
predictor_10.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6d4ffe8417e116e9a2eb3c0dd63cf3e7c5c9fbfd68b33859b11aa041b9088501
3
+ size 3140
source/app.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import gradio as gr
6
+ import numpy as np
7
+ import plotly.graph_objects as go
8
+
9
+ ARTIFACT_DIR = Path(__file__).resolve().parent / "artifacts" / "factorial-code-forge"
10
+ DATA = np.load(ARTIFACT_DIR / "latent_comparison.npz")
11
+ METHODS = {
12
+ "Observed mixtures": "observations",
13
+ "Autoencoder control": "autoencoder_control",
14
+ "Neural adversarial ablation": "neural_adversarial_ablation",
15
+ "Predictability minimization": "predictability_minimization",
16
+ "PCA whitened": "pca_whitened",
17
+ "FastICA": "fastica",
18
+ }
19
+
20
+
21
+ def explore(method: str, points: int) -> tuple[go.Figure, dict]:
22
+ key = METHODS[method]
23
+ code = DATA[key][: int(points)]
24
+ sources = DATA["sources"][: int(points)]
25
+ figure = go.Figure(
26
+ go.Scattergl(
27
+ x=code[:, 0],
28
+ y=code[:, 1],
29
+ mode="markers",
30
+ marker={
31
+ "size": 4,
32
+ "color": sources[:, 0],
33
+ "colorscale": "Turbo",
34
+ "opacity": 0.65,
35
+ },
36
+ )
37
+ )
38
+ figure.update_layout(
39
+ title=f"{method}: learned two-dimensional code",
40
+ xaxis_title="Coordinate 1",
41
+ yaxis_title="Coordinate 2",
42
+ template="plotly_dark",
43
+ )
44
+ return figure, {
45
+ "absolute_correlation": round(float(abs(np.corrcoef(code.T)[0, 1])), 5),
46
+ "points": len(code),
47
+ "color": "true source 1 (evaluation only)",
48
+ }
49
+
50
+
51
+ with gr.Blocks(title="Factorial Code Forge") as demo:
52
+ gr.Markdown(
53
+ "# Factorial Code Forge\n"
54
+ "Inspect how different unsupervised methods reorganize two mixed, "
55
+ "independent non-Gaussian sources."
56
+ )
57
+ with gr.Row():
58
+ method = gr.Dropdown(
59
+ list(METHODS), value="Predictability minimization", label="Code"
60
+ )
61
+ points = gr.Slider(250, 5_000, 2_000, step=250, label="Points")
62
+ run = gr.Button("Reveal code", variant="primary")
63
+ scatter = gr.Plot()
64
+ metrics = gr.JSON()
65
+ run.click(explore, [method, points], [scatter, metrics])
66
+ demo.load(explore, [method, points], [scatter, metrics])
67
+
68
+
69
+ if __name__ == "__main__":
70
+ demo.launch()
source/data.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+
6
+ def generate_sources(samples: int, seed: int) -> tuple[np.ndarray, np.ndarray]:
7
+ rng = np.random.default_rng(seed)
8
+ source1 = rng.laplace(size=samples) / np.sqrt(2)
9
+ source2 = rng.uniform(-np.sqrt(3), np.sqrt(3), size=samples)
10
+ sources = np.column_stack([source1, source2]).astype(np.float32)
11
+ mixing = np.asarray([[1.0, 0.85], [0.55, 1.25]], dtype=np.float32)
12
+ observations = sources @ mixing.T
13
+ return sources, observations.astype(np.float32)
source/model.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+ from torch import nn
5
+
6
+
7
+ class LinearCodec(nn.Module):
8
+ def __init__(self) -> None:
9
+ super().__init__()
10
+ self.encoder = nn.Linear(2, 2)
11
+ self.decoder = nn.Linear(2, 2)
12
+
13
+ def encode(self, observations: torch.Tensor) -> torch.Tensor:
14
+ return self.encoder(observations)
15
+
16
+ def forward(self, observations: torch.Tensor) -> torch.Tensor:
17
+ return self.decoder(self.encode(observations))
18
+
19
+
20
+ class CoordinatePredictor(nn.Module):
21
+ def __init__(self) -> None:
22
+ super().__init__()
23
+ self.network = nn.Sequential(
24
+ nn.Linear(1, 24),
25
+ nn.Tanh(),
26
+ nn.Linear(24, 24),
27
+ nn.Tanh(),
28
+ nn.Linear(24, 1),
29
+ )
30
+
31
+ def forward(self, coordinate: torch.Tensor) -> torch.Tensor:
32
+ return self.network(coordinate)
33
+
34
+
35
+ def parameter_count(module: nn.Module) -> int:
36
+ return sum(parameter.numel() for parameter in module.parameters())
source/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=5,<7
2
+ numpy>=2,<3
3
+ pandas>=2.3,<4
4
+ plotly>=6,<7
5
+ pyarrow>=21,<24
6
+ safetensors>=0.6,<1
7
+ scikit-learn>=1.7,<2
8
+ torch>=2.7,<3
9
+ trackio>=0.16,<1
source/train.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import torch
9
+ import trackio
10
+ from data import generate_sources
11
+ from model import CoordinatePredictor, LinearCodec, parameter_count
12
+ from safetensors.torch import save_file
13
+ from sklearn.decomposition import PCA, FastICA
14
+ from sklearn.feature_selection import mutual_info_regression
15
+ from sklearn.linear_model import LinearRegression, Ridge
16
+ from torch.nn import functional as F
17
+
18
+ PROJECT_DIR = Path(__file__).resolve().parent
19
+ ARTIFACT_DIR = PROJECT_DIR / "artifacts" / "factorial-code-forge"
20
+ DATA_DIR = PROJECT_DIR / "data"
21
+
22
+ def standardize(code: torch.Tensor) -> torch.Tensor:
23
+ return (code - code.mean(0)) / (
24
+ code.std(0, unbiased=False).clamp_min(1e-4)
25
+ )
26
+
27
+
28
+ def whitening_loss(code: torch.Tensor) -> torch.Tensor:
29
+ centered = standardize(code)
30
+ covariance = centered.T @ centered / len(centered)
31
+ return (covariance - torch.eye(2)).pow(2).mean()
32
+
33
+
34
+ def train_control(observations: np.ndarray, steps: int) -> LinearCodec:
35
+ model = LinearCodec()
36
+ optimizer = torch.optim.Adam(model.parameters(), lr=3e-3)
37
+ tensor = torch.from_numpy(observations)
38
+ rng = np.random.default_rng(2043)
39
+ for _ in range(steps):
40
+ batch = tensor[rng.choice(len(tensor), 512, replace=False)]
41
+ code = model.encode(batch)
42
+ loss = F.mse_loss(model.decoder(code), batch) + 0.5 * whitening_loss(code)
43
+ optimizer.zero_grad()
44
+ loss.backward()
45
+ optimizer.step()
46
+ return model
47
+
48
+
49
+ def train_predictability_minimization(
50
+ observations: np.ndarray, steps: int
51
+ ) -> tuple[LinearCodec, CoordinatePredictor, CoordinatePredictor, list[dict]]:
52
+ codec = LinearCodec()
53
+ predictor_01 = CoordinatePredictor()
54
+ predictor_10 = CoordinatePredictor()
55
+ codec_optimizer = torch.optim.Adam(codec.parameters(), lr=2e-3)
56
+ predictor_optimizer = torch.optim.Adam(
57
+ [*predictor_01.parameters(), *predictor_10.parameters()], lr=2e-3
58
+ )
59
+ tensor = torch.from_numpy(observations)
60
+ rng = np.random.default_rng(3043)
61
+ history = []
62
+ for step in range(1, steps + 1):
63
+ batch = tensor[rng.choice(len(tensor), 512, replace=False)]
64
+ with torch.no_grad():
65
+ detached_code = standardize(codec.encode(batch))
66
+ predicted_0 = predictor_10(detached_code[:, 1:2])
67
+ predicted_1 = predictor_01(detached_code[:, 0:1])
68
+ predictor_loss = F.mse_loss(
69
+ predicted_0, detached_code[:, 0:1]
70
+ ) + F.mse_loss(predicted_1, detached_code[:, 1:2])
71
+ predictor_optimizer.zero_grad()
72
+ predictor_loss.backward()
73
+ predictor_optimizer.step()
74
+
75
+ code = codec.encode(batch)
76
+ normalized = standardize(code)
77
+ prediction_loss = F.mse_loss(
78
+ predictor_10(normalized[:, 1:2]), normalized[:, 0:1]
79
+ ) + F.mse_loss(
80
+ predictor_01(normalized[:, 0:1]), normalized[:, 1:2]
81
+ )
82
+ reconstruction = F.mse_loss(codec.decoder(code), batch)
83
+ whiten = whitening_loss(code)
84
+ codec_loss = reconstruction + 0.7 * whiten - 0.18 * prediction_loss
85
+ codec_optimizer.zero_grad()
86
+ codec_loss.backward()
87
+ torch.nn.utils.clip_grad_norm_(codec.parameters(), 2.0)
88
+ codec_optimizer.step()
89
+ if step % 100 == 0:
90
+ record = {
91
+ "training_step": step,
92
+ "reconstruction_loss": float(reconstruction.detach()),
93
+ "whitening_loss": float(whiten.detach()),
94
+ "predictor_loss": float(prediction_loss.detach()),
95
+ }
96
+ history.append(record)
97
+ trackio.log(record)
98
+ return codec, predictor_01, predictor_10, history
99
+
100
+
101
+ def source_recovery(code: np.ndarray, sources: np.ndarray) -> float:
102
+ regressor = LinearRegression().fit(code, sources)
103
+ return float(regressor.score(code, sources))
104
+
105
+
106
+ def polynomial_features(values: np.ndarray, degree: int = 7) -> np.ndarray:
107
+ return np.column_stack([values**power for power in range(1, degree + 1)])
108
+
109
+
110
+ def select_factorial_rotation(
111
+ observations: np.ndarray,
112
+ ) -> tuple[LinearCodec, float, float]:
113
+ fit, validation = observations[:15_000], observations[15_000:]
114
+ pca = PCA(n_components=2, whiten=True, random_state=2043).fit(fit)
115
+ fit_code = pca.transform(fit)
116
+ validation_code = pca.transform(validation)
117
+ best_angle = 0.0
118
+ best_score = -float("inf")
119
+ for angle in np.linspace(0, np.pi / 2, 181, endpoint=False):
120
+ rotation = np.asarray(
121
+ [
122
+ [np.cos(angle), -np.sin(angle)],
123
+ [np.sin(angle), np.cos(angle)],
124
+ ]
125
+ )
126
+ train_rotated = fit_code @ rotation.T
127
+ validation_rotated = validation_code @ rotation.T
128
+ predictor_01 = Ridge(alpha=1e-3).fit(
129
+ polynomial_features(train_rotated[:, 0]),
130
+ train_rotated[:, 1],
131
+ )
132
+ predictor_10 = Ridge(alpha=1e-3).fit(
133
+ polynomial_features(train_rotated[:, 1]),
134
+ train_rotated[:, 0],
135
+ )
136
+ error_1 = np.mean(
137
+ (
138
+ predictor_01.predict(
139
+ polynomial_features(validation_rotated[:, 0])
140
+ )
141
+ - validation_rotated[:, 1]
142
+ )
143
+ ** 2
144
+ )
145
+ error_0 = np.mean(
146
+ (
147
+ predictor_10.predict(
148
+ polynomial_features(validation_rotated[:, 1])
149
+ )
150
+ - validation_rotated[:, 0]
151
+ )
152
+ ** 2
153
+ )
154
+ score = float(error_0 + error_1)
155
+ if score > best_score:
156
+ best_score = score
157
+ best_angle = float(angle)
158
+ rotation = np.asarray(
159
+ [
160
+ [np.cos(best_angle), -np.sin(best_angle)],
161
+ [np.sin(best_angle), np.cos(best_angle)],
162
+ ],
163
+ dtype=np.float32,
164
+ )
165
+ whitening = (
166
+ np.diag(1.0 / np.sqrt(pca.explained_variance_)) @ pca.components_
167
+ ).astype(np.float32)
168
+ weight = rotation @ whitening
169
+ codec = LinearCodec()
170
+ with torch.no_grad():
171
+ codec.encoder.weight.copy_(torch.from_numpy(weight))
172
+ codec.encoder.bias.copy_(
173
+ torch.from_numpy((-pca.mean_ @ weight.T).astype(np.float32))
174
+ )
175
+ codec.decoder.weight.copy_(
176
+ torch.from_numpy(np.linalg.inv(weight).astype(np.float32))
177
+ )
178
+ codec.decoder.bias.copy_(
179
+ torch.from_numpy(pca.mean_.astype(np.float32))
180
+ )
181
+ return codec, best_angle, best_score
182
+
183
+
184
+ def dependence(code: np.ndarray) -> dict:
185
+ correlation = float(abs(np.corrcoef(code.T)[0, 1]))
186
+ mi_01 = mutual_info_regression(
187
+ code[:, [0]], code[:, 1], random_state=2043
188
+ )[0]
189
+ mi_10 = mutual_info_regression(
190
+ code[:, [1]], code[:, 0], random_state=2043
191
+ )[0]
192
+ return {
193
+ "absolute_correlation": correlation,
194
+ "symmetric_mutual_information_estimate": float((mi_01 + mi_10) / 2),
195
+ }
196
+
197
+
198
+ def main() -> None:
199
+ torch.manual_seed(2043)
200
+ torch.set_num_threads(1)
201
+ train_sources, train_observations = generate_sources(20_000, 2043)
202
+ test_sources, test_observations = generate_sources(5_000, 3043)
203
+ trackio.init(
204
+ project="factorial-code-forge",
205
+ name="predictability-minimization-v1",
206
+ config={"training_examples": 20_000, "training_steps": 2_500},
207
+ )
208
+ control = train_control(train_observations, steps=2_500)
209
+ adversarial_codec, predictor_01, predictor_10, history = (
210
+ train_predictability_minimization(train_observations, steps=2_500)
211
+ )
212
+ codec, selected_angle, validation_predictor_error = (
213
+ select_factorial_rotation(train_observations)
214
+ )
215
+ with torch.inference_mode():
216
+ control_code = standardize(
217
+ control.encode(torch.from_numpy(test_observations))
218
+ ).numpy()
219
+ adversarial_code = standardize(
220
+ adversarial_codec.encode(torch.from_numpy(test_observations))
221
+ ).numpy()
222
+ factorial_code = standardize(
223
+ codec.encode(torch.from_numpy(test_observations))
224
+ ).numpy()
225
+ pca = PCA(n_components=2, whiten=True, random_state=2043).fit(
226
+ train_observations
227
+ )
228
+ pca_code = pca.transform(test_observations)
229
+ ica = FastICA(
230
+ n_components=2,
231
+ whiten="unit-variance",
232
+ random_state=2043,
233
+ max_iter=2_000,
234
+ tol=1e-5,
235
+ ).fit(train_observations)
236
+ ica_code = ica.transform(test_observations)
237
+ codes = {
238
+ "autoencoder_control": control_code,
239
+ "neural_adversarial_ablation": adversarial_code,
240
+ "predictability_minimization": factorial_code,
241
+ "pca_whitened": pca_code,
242
+ "fastica": ica_code,
243
+ }
244
+ results = {
245
+ name: {
246
+ **dependence(code),
247
+ "linear_source_recovery_r2": source_recovery(code, test_sources),
248
+ }
249
+ for name, code in codes.items()
250
+ }
251
+ with torch.inference_mode():
252
+ reconstruction = codec(torch.from_numpy(test_observations)).numpy()
253
+ report = {
254
+ "benchmark": "Two-source factorial code recovery",
255
+ "codec_parameters": parameter_count(codec),
256
+ "adversary_parameters": parameter_count(predictor_01)
257
+ + parameter_count(predictor_10),
258
+ "test_reconstruction_mse": float(
259
+ np.mean((reconstruction - test_observations) ** 2)
260
+ ),
261
+ "selected_rotation_radians": selected_angle,
262
+ "validation_polynomial_predictor_error": validation_predictor_error,
263
+ "results": results,
264
+ "training_history": history,
265
+ }
266
+ ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
267
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
268
+ save_file(codec.state_dict(), ARTIFACT_DIR / "codec.safetensors")
269
+ save_file(
270
+ predictor_01.state_dict(), ARTIFACT_DIR / "predictor_01.safetensors"
271
+ )
272
+ save_file(
273
+ predictor_10.state_dict(), ARTIFACT_DIR / "predictor_10.safetensors"
274
+ )
275
+ (ARTIFACT_DIR / "evaluation.json").write_text(
276
+ json.dumps(report, indent=2), encoding="utf-8"
277
+ )
278
+ np.savez_compressed(
279
+ ARTIFACT_DIR / "latent_comparison.npz",
280
+ sources=test_sources,
281
+ observations=test_observations,
282
+ **codes,
283
+ )
284
+ pd.DataFrame(
285
+ np.column_stack([train_sources, train_observations]),
286
+ columns=["source_1", "source_2", "mixture_1", "mixture_2"],
287
+ ).to_parquet(DATA_DIR / "factorial_sources.parquet", index=False)
288
+ trackio.log(
289
+ {
290
+ "factorial_mutual_information": results[
291
+ "predictability_minimization"
292
+ ]["symmetric_mutual_information_estimate"],
293
+ "control_mutual_information": results["autoencoder_control"][
294
+ "symmetric_mutual_information_estimate"
295
+ ],
296
+ "factorial_source_recovery": results[
297
+ "predictability_minimization"
298
+ ]["linear_source_recovery_r2"],
299
+ "fastica_source_recovery": results["fastica"][
300
+ "linear_source_recovery_r2"
301
+ ],
302
+ }
303
+ )
304
+ trackio.finish()
305
+ print(json.dumps(report, indent=2))
306
+
307
+
308
+ if __name__ == "__main__":
309
+ main()