aetherscan 1.0.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.
- aetherscan/__init__.py +45 -0
- aetherscan/benchmark.py +179 -0
- aetherscan/candidate_figures.py +328 -0
- aetherscan/cli.py +3032 -0
- aetherscan/config.py +1002 -0
- aetherscan/dashboard.py +709 -0
- aetherscan/dashboard_cli.py +51 -0
- aetherscan/dashboard_launcher.py +194 -0
- aetherscan/data_generation.py +1448 -0
- aetherscan/db/__init__.py +21 -0
- aetherscan/db/db.py +2789 -0
- aetherscan/hf_hub.py +547 -0
- aetherscan/inference.py +912 -0
- aetherscan/inference_viz.py +1494 -0
- aetherscan/latent_gif.py +512 -0
- aetherscan/latent_variants.py +352 -0
- aetherscan/logger/__init__.py +21 -0
- aetherscan/logger/logger.py +467 -0
- aetherscan/logger/slack_handler.py +760 -0
- aetherscan/main.py +1269 -0
- aetherscan/manager/__init__.py +21 -0
- aetherscan/manager/manager.py +781 -0
- aetherscan/models/__init__.py +21 -0
- aetherscan/models/random_forest.py +171 -0
- aetherscan/models/vae.py +849 -0
- aetherscan/monitor/__init__.py +19 -0
- aetherscan/monitor/monitor.py +935 -0
- aetherscan/pfb.py +161 -0
- aetherscan/preprocessing.py +2718 -0
- aetherscan/rf_metrics.py +100 -0
- aetherscan/round_data.py +932 -0
- aetherscan/run_state.py +277 -0
- aetherscan/seeding.py +160 -0
- aetherscan/shap_parallel.py +238 -0
- aetherscan/tag_guards.py +206 -0
- aetherscan/train.py +7681 -0
- aetherscan-1.0.0.dist-info/METADATA +1187 -0
- aetherscan-1.0.0.dist-info/RECORD +41 -0
- aetherscan-1.0.0.dist-info/WHEEL +4 -0
- aetherscan-1.0.0.dist-info/entry_points.txt +2 -0
- aetherscan-1.0.0.dist-info/licenses/LICENSE +13 -0
aetherscan/models/vae.py
ADDED
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
# TODO: refactor to expose public APIs for creating & destroying BetaVAE instances
|
|
2
|
+
# TODO: replace hard-coded values with config values
|
|
3
|
+
"""
|
|
4
|
+
Beta-VAE model implementation for Aetherscan Pipeline
|
|
5
|
+
Uses custom clustering loss components to implicitly differentiate SETI signals from RFI in the
|
|
6
|
+
model's learned latent space
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
import tensorflow as tf
|
|
14
|
+
from tensorflow import keras
|
|
15
|
+
from tensorflow.keras import layers
|
|
16
|
+
from tensorflow.keras.initializers import Constant, GlorotNormal, HeNormal, Zeros
|
|
17
|
+
from tensorflow.keras.regularizers import l1, l2
|
|
18
|
+
|
|
19
|
+
from aetherscan.config import get_config
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Use keras.utils.* rather than keras.saving.* — the latter is the canonical
|
|
25
|
+
# Keras 3 path, but `from tensorflow import keras` in TF 2.17 + NGC 25.02
|
|
26
|
+
# resolves to the tf-keras compat shim (`keras._tf_keras.keras`), which
|
|
27
|
+
# doesn't re-export the `saving` submodule. keras.utils.register_keras_serializable
|
|
28
|
+
# is the back-compat alias that exists in both tf.keras lineages and standalone
|
|
29
|
+
# Keras 3 — pick the path that works everywhere.
|
|
30
|
+
@keras.utils.register_keras_serializable(package="aetherscan")
|
|
31
|
+
class Sampling(layers.Layer):
|
|
32
|
+
"""
|
|
33
|
+
Sampling layer for Beta-VAE using reparameterization trick
|
|
34
|
+
|
|
35
|
+
Since sampling is a non-differentiable operation (can't backprop through random sampling)
|
|
36
|
+
But we need to sample from the Beta-VAE's learned distribution to produce the latent vector (z)
|
|
37
|
+
We isolate the randomness (epsilon) to be independent of the learned params (z_mean, z_log_var)
|
|
38
|
+
Such that gradients can flow through without issue
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def call(self, inputs):
|
|
42
|
+
# Get the learned mean & log-varience of the latent distribution
|
|
43
|
+
z_mean, z_log_var = inputs
|
|
44
|
+
|
|
45
|
+
batch = tf.shape(z_mean)[0]
|
|
46
|
+
dim = tf.shape(z_mean)[1]
|
|
47
|
+
|
|
48
|
+
# Sample random noise from a standard normal N(0, 1) with same shape as z_mean.
|
|
49
|
+
# tf.random.normal is the stable canonical API; tf.keras.backend.random_normal was
|
|
50
|
+
# removed/deprecated in Keras 3 (shipped in TF 2.16+) and had inconsistent graph/seed semantics.
|
|
51
|
+
epsilon = tf.random.normal(shape=(batch, dim))
|
|
52
|
+
|
|
53
|
+
# Compute latent vector using reparameterization
|
|
54
|
+
# Equivalent to sampling from N(z_mean, exp(z_log_var))
|
|
55
|
+
z = z_mean + tf.exp(0.5 * z_log_var) * epsilon
|
|
56
|
+
|
|
57
|
+
return z
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BetaVAE(keras.Model):
|
|
61
|
+
"""
|
|
62
|
+
Beta-VAE for SETI cadence data with a composite loss that combines reconstruction, KL, and a
|
|
63
|
+
custom clustering term that pulls ON/ON & OFF/OFF latents together and pushes ON/OFF apart.
|
|
64
|
+
|
|
65
|
+
alpha weights the clustering term in the total loss; beta weights the KL term (standard
|
|
66
|
+
Beta-VAE knob trading reconstruction quality for latent disentanglement).
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self, encoder, decoder, alpha=10.0, beta=1.5, regularization_active=False, **kwargs
|
|
71
|
+
):
|
|
72
|
+
super().__init__(**kwargs)
|
|
73
|
+
self.encoder = encoder
|
|
74
|
+
self.decoder = decoder
|
|
75
|
+
|
|
76
|
+
# Hyperparameters
|
|
77
|
+
self.alpha = alpha
|
|
78
|
+
self.beta = beta
|
|
79
|
+
# Whether the layer-declared L1/L2 penalties are ADDED to the objective. Default
|
|
80
|
+
# False (v1 decision, 2026-07-29): activating them at the declared, never-calibrated
|
|
81
|
+
# coefficients measurably degraded the model in a 5-seed A/B — recall@0.01FPR
|
|
82
|
+
# median .984 -> .954 with one seed at .72, and 1-4 latent dims per seed pushed
|
|
83
|
+
# below the active-units threshold (one seed lost 4/8). reg_loss is still computed
|
|
84
|
+
# and recorded for observability either way; coefficient calibration is tracked as
|
|
85
|
+
# a follow-up issue.
|
|
86
|
+
self.regularization_active = regularization_active
|
|
87
|
+
|
|
88
|
+
def call(self, inputs, training=None):
|
|
89
|
+
"""
|
|
90
|
+
Encode-decode a batch of cadences (shape (batch, 6, 16, 512)) and return
|
|
91
|
+
(reconstruction, z_mean, z_log_var, z).
|
|
92
|
+
|
|
93
|
+
The encoder operates on individual observations, so the call reshapes from cadence form
|
|
94
|
+
(batch, 6, 16, 512) to per-observation form (batch * 6, 16, 512, 1) before encoding, then
|
|
95
|
+
reshapes the reconstruction back to cadence form for loss computation.
|
|
96
|
+
"""
|
|
97
|
+
batch_size = tf.shape(inputs)[0]
|
|
98
|
+
|
|
99
|
+
# Reshape inputs for encoder
|
|
100
|
+
encoder_input = tf.reshape(inputs, (batch_size * 6, 16, 512, 1))
|
|
101
|
+
|
|
102
|
+
# Encode: observations -> latents
|
|
103
|
+
z_mean, z_log_var, z = self.encoder(encoder_input, training=training)
|
|
104
|
+
|
|
105
|
+
# Decode: latents -> observations
|
|
106
|
+
reconstruction = self.decoder(z, training=training)
|
|
107
|
+
|
|
108
|
+
# Reshape outputs back to cadence format
|
|
109
|
+
reconstruction = tf.reshape(reconstruction, (batch_size, 6, 16, 512))
|
|
110
|
+
|
|
111
|
+
return reconstruction, z_mean, z_log_var, z
|
|
112
|
+
|
|
113
|
+
@tf.function
|
|
114
|
+
def loss_same(self, a: tf.Tensor, b: tf.Tensor) -> tf.Tensor:
|
|
115
|
+
"""Mean squared L2 distance between two same-class latents — minimized to pull
|
|
116
|
+
ON/ON and OFF/OFF pairs together in latent space."""
|
|
117
|
+
return tf.reduce_mean(tf.reduce_sum(tf.square(a - b), axis=1))
|
|
118
|
+
|
|
119
|
+
@tf.function
|
|
120
|
+
def loss_diff(self, a: tf.Tensor, b: tf.Tensor) -> tf.Tensor:
|
|
121
|
+
"""Inverse squared L2 distance between ON and OFF latents (with 1e-8 epsilon for
|
|
122
|
+
stability when latents collide) — minimizing this term pushes ON/OFF pairs apart."""
|
|
123
|
+
return tf.reduce_mean(1.0 / (tf.reduce_sum(tf.square(a - b), axis=1) + 1e-8))
|
|
124
|
+
|
|
125
|
+
# Deliberately NOT @tf.function (removed with the L1/L2 activation): this runs only
|
|
126
|
+
# inside compute_total_loss's graph, and giving it its own FuncGraph strands the
|
|
127
|
+
# encoder's activity-regularizer loss tensors out of scope of the reg_loss add_n —
|
|
128
|
+
# InaccessibleTensorError, found by the first regularized run. Inlining into the outer
|
|
129
|
+
# trace keeps every penalty same-graph; standalone (eager) calls just run eagerly.
|
|
130
|
+
# ⚠ REPRODUCIBILITY: this inlining changed float summation order — same-seed results
|
|
131
|
+
# from builds before/after it differ slightly. Determinism WITHIN a build (same seed ⇒
|
|
132
|
+
# byte-identical reruns) is preserved and pinned by the train dataset/accumulation
|
|
133
|
+
# tests; cross-build comparisons must be distribution-level, never same-seed pairing.
|
|
134
|
+
def compute_clustering_loss_true(self, true_data: tf.Tensor) -> tf.Tensor:
|
|
135
|
+
"""
|
|
136
|
+
Clustering loss for true-class (ETI-bearing) cadences: sum loss_same across all
|
|
137
|
+
within-class pairs (ON/ON: a1-a2, a1-a3, a2-a3 and OFF/OFF: b-c, b-d, c-d, each direction)
|
|
138
|
+
plus loss_diff across every ON/OFF cross-pair (a{1,2,3} × {b,c,d}). Minimizing this term
|
|
139
|
+
forces ON latents to cluster together, OFF latents to cluster together, and the two
|
|
140
|
+
clusters to separate.
|
|
141
|
+
"""
|
|
142
|
+
batch_size = tf.shape(true_data)[0]
|
|
143
|
+
|
|
144
|
+
# Process all observations at once for efficiency
|
|
145
|
+
all_obs = tf.reshape(true_data, (batch_size * 6, 16, 512, 1))
|
|
146
|
+
_, _, all_latents = self.encoder(all_obs, training=True)
|
|
147
|
+
|
|
148
|
+
# Reshape back to (batch, 6, latent_dim)
|
|
149
|
+
latent_dim = tf.shape(all_latents)[1]
|
|
150
|
+
latents_reshaped = tf.reshape(all_latents, (batch_size, 6, latent_dim))
|
|
151
|
+
|
|
152
|
+
# Extract ON and OFF observations
|
|
153
|
+
a1 = latents_reshaped[:, 0, :] # ON
|
|
154
|
+
b = latents_reshaped[:, 1, :] # OFF
|
|
155
|
+
a2 = latents_reshaped[:, 2, :] # ON
|
|
156
|
+
c = latents_reshaped[:, 3, :] # OFF
|
|
157
|
+
a3 = latents_reshaped[:, 4, :] # ON
|
|
158
|
+
d = latents_reshaped[:, 5, :] # OFF
|
|
159
|
+
|
|
160
|
+
# Difference terms (ON-OFF should be maximized, so use loss_diff)
|
|
161
|
+
difference = 0.0
|
|
162
|
+
difference += self.loss_diff(a1, b)
|
|
163
|
+
difference += self.loss_diff(a1, c)
|
|
164
|
+
difference += self.loss_diff(a1, d)
|
|
165
|
+
difference += self.loss_diff(a2, b)
|
|
166
|
+
difference += self.loss_diff(a2, c)
|
|
167
|
+
difference += self.loss_diff(a2, d)
|
|
168
|
+
difference += self.loss_diff(a3, b)
|
|
169
|
+
difference += self.loss_diff(a3, c)
|
|
170
|
+
difference += self.loss_diff(a3, d)
|
|
171
|
+
|
|
172
|
+
# Same terms (ON-ON and OFF-OFF should be minimized, so use loss_same)
|
|
173
|
+
same = 0.0
|
|
174
|
+
same += self.loss_same(a1, a2)
|
|
175
|
+
same += self.loss_same(a1, a3)
|
|
176
|
+
same += self.loss_same(a2, a1)
|
|
177
|
+
same += self.loss_same(a2, a3)
|
|
178
|
+
same += self.loss_same(a3, a1)
|
|
179
|
+
same += self.loss_same(a3, a2)
|
|
180
|
+
same += self.loss_same(b, c)
|
|
181
|
+
same += self.loss_same(b, d)
|
|
182
|
+
same += self.loss_same(c, b)
|
|
183
|
+
same += self.loss_same(c, d)
|
|
184
|
+
same += self.loss_same(d, b)
|
|
185
|
+
same += self.loss_same(d, c)
|
|
186
|
+
|
|
187
|
+
similarity = same + difference
|
|
188
|
+
return similarity
|
|
189
|
+
|
|
190
|
+
# Deliberately NOT @tf.function — same FuncGraph-stranding rationale as
|
|
191
|
+
# compute_clustering_loss_true above.
|
|
192
|
+
def compute_clustering_loss_false(self, false_data: tf.Tensor) -> tf.Tensor:
|
|
193
|
+
"""
|
|
194
|
+
Clustering loss for false-class (RFI / noise-only) cadences: sum loss_same across all 15
|
|
195
|
+
pairs of observations since every observation in a false cadence is treated as belonging
|
|
196
|
+
to the same class. Minimizing this term forces all 6 false-class latents to collapse to a
|
|
197
|
+
single cluster (no ON/OFF distinction).
|
|
198
|
+
"""
|
|
199
|
+
batch_size = tf.shape(false_data)[0]
|
|
200
|
+
|
|
201
|
+
# Process all observations at once for efficiency
|
|
202
|
+
all_obs = tf.reshape(false_data, (batch_size * 6, 16, 512, 1))
|
|
203
|
+
_, _, all_latents = self.encoder(all_obs, training=True)
|
|
204
|
+
|
|
205
|
+
# Reshape back to (batch, 6, latent_dim)
|
|
206
|
+
latent_dim = tf.shape(all_latents)[1]
|
|
207
|
+
latents_reshaped = tf.reshape(all_latents, (batch_size, 6, latent_dim))
|
|
208
|
+
|
|
209
|
+
# Extract OFF observations
|
|
210
|
+
a1 = latents_reshaped[:, 0, :] # OFF
|
|
211
|
+
b = latents_reshaped[:, 1, :] # OFF
|
|
212
|
+
a2 = latents_reshaped[:, 2, :] # OFF
|
|
213
|
+
c = latents_reshaped[:, 3, :] # OFF
|
|
214
|
+
a3 = latents_reshaped[:, 4, :] # OFF
|
|
215
|
+
d = latents_reshaped[:, 5, :] # OFF
|
|
216
|
+
|
|
217
|
+
# For RFI/false signals, all observations should look similar
|
|
218
|
+
# So we minimize distances between all pairs
|
|
219
|
+
difference = 0.0
|
|
220
|
+
difference += self.loss_same(a1, b)
|
|
221
|
+
difference += self.loss_same(a1, c)
|
|
222
|
+
difference += self.loss_same(a1, d)
|
|
223
|
+
difference += self.loss_same(a2, b)
|
|
224
|
+
difference += self.loss_same(a2, c)
|
|
225
|
+
difference += self.loss_same(a2, d)
|
|
226
|
+
difference += self.loss_same(a3, b)
|
|
227
|
+
difference += self.loss_same(a3, c)
|
|
228
|
+
difference += self.loss_same(a3, d)
|
|
229
|
+
|
|
230
|
+
same = 0.0
|
|
231
|
+
same += self.loss_same(a1, a2)
|
|
232
|
+
same += self.loss_same(a1, a3)
|
|
233
|
+
same += self.loss_same(a2, a1)
|
|
234
|
+
same += self.loss_same(a2, a3)
|
|
235
|
+
same += self.loss_same(a3, a1)
|
|
236
|
+
same += self.loss_same(a3, a2)
|
|
237
|
+
same += self.loss_same(b, c)
|
|
238
|
+
same += self.loss_same(b, d)
|
|
239
|
+
same += self.loss_same(c, b)
|
|
240
|
+
same += self.loss_same(c, d)
|
|
241
|
+
same += self.loss_same(d, b)
|
|
242
|
+
same += self.loss_same(d, c)
|
|
243
|
+
|
|
244
|
+
similarity = same + difference
|
|
245
|
+
return similarity
|
|
246
|
+
|
|
247
|
+
@tf.function
|
|
248
|
+
def compute_total_loss(self, main_data, true_data, false_data, target_data, training=True):
|
|
249
|
+
"""
|
|
250
|
+
Forward-pass main_data through the VAE and return a dict with reconstruction, KL, and
|
|
251
|
+
per-class clustering losses plus their weighted sum:
|
|
252
|
+
total = reconstruction + beta * kl + alpha * (true_loss + false_loss) + reg
|
|
253
|
+
|
|
254
|
+
Reconstruction uses binary cross-entropy on the [0, 1]-bounded decoder output (sigmoid).
|
|
255
|
+
true_data and false_data are separate cadences fed through the clustering-loss heads —
|
|
256
|
+
they don't share gradients with reconstruction.
|
|
257
|
+
|
|
258
|
+
`reg` is the sum of the layer-declared regularization penalties (activated 2026-07 by
|
|
259
|
+
maintainer decision — the L1/L2 declarations existed since inception but a custom
|
|
260
|
+
training loop only applies them if it adds model.losses to the objective, so every
|
|
261
|
+
model trained before this change was effectively unregularized). Stock keras
|
|
262
|
+
semantics: kernel/bias L2 penalties appear once each; activity-L1 penalties accrue
|
|
263
|
+
once per regularized-layer forward performed inside this function (the reconstruction
|
|
264
|
+
pass AND both clustering branches), and keras batch-SUM-scales them, so the effective
|
|
265
|
+
activity coefficient scales with the per-replica batch size (128 at defaults).
|
|
266
|
+
"""
|
|
267
|
+
# Perform forward pass through Beta-VAE
|
|
268
|
+
reconstruction, z_mean, z_log_var, z = self.call(main_data, training=training)
|
|
269
|
+
|
|
270
|
+
# Ensure reconstruction shape matches target for loss computation
|
|
271
|
+
reconstruction = tf.reshape(reconstruction, tf.shape(target_data))
|
|
272
|
+
|
|
273
|
+
# Compute reconstruction loss
|
|
274
|
+
reconstruction_loss = tf.reduce_mean(
|
|
275
|
+
tf.reduce_sum(
|
|
276
|
+
keras.losses.binary_crossentropy(
|
|
277
|
+
target_data,
|
|
278
|
+
reconstruction,
|
|
279
|
+
from_logits=False, # Use from_logits=False for stability since decoder's final activation is sigmoid (reconstruction is bounded [0,1])
|
|
280
|
+
),
|
|
281
|
+
axis=(1, 2),
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Compute KL loss. The per-(batch, dim) matrix is kept one step longer so the
|
|
286
|
+
# posterior-collapse diagnostics (#282) get the batch-mean KL of EACH latent dim
|
|
287
|
+
# (a collapsing dim's KL goes to ~0) — the scalar loss term is unchanged.
|
|
288
|
+
kl_matrix = -0.5 * (1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var))
|
|
289
|
+
kl_per_dim = tf.reduce_mean(kl_matrix, axis=0)
|
|
290
|
+
kl_loss = tf.reduce_mean(tf.reduce_sum(kl_matrix, axis=1))
|
|
291
|
+
|
|
292
|
+
# Compute clustering losses
|
|
293
|
+
false_loss = self.compute_clustering_loss_false(false_data)
|
|
294
|
+
true_loss = self.compute_clustering_loss_true(true_data)
|
|
295
|
+
|
|
296
|
+
# Layer-declared regularization penalties (see docstring). Always COMPUTED and
|
|
297
|
+
# returned for observability (keras materializes the activity penalties in the
|
|
298
|
+
# forward pass regardless, so this is one add_n), but only ADDED to the objective
|
|
299
|
+
# when regularization_active — the v1 default is False (see __init__: activation at
|
|
300
|
+
# the declared coefficients measured harmful). Cast fp32 so the sum is exact under
|
|
301
|
+
# mixed_bfloat16 (activity penalties ride bf16 activations); the islands keep the
|
|
302
|
+
# rest of the loss math fp32 already.
|
|
303
|
+
if self.losses:
|
|
304
|
+
reg_loss = tf.add_n([tf.cast(loss, tf.float32) for loss in self.losses])
|
|
305
|
+
else:
|
|
306
|
+
reg_loss = tf.constant(0.0, dtype=tf.float32)
|
|
307
|
+
|
|
308
|
+
# Compute total loss (reg only when active — inactive keeps the objective
|
|
309
|
+
# byte-identical to the pre-activation pipeline)
|
|
310
|
+
total_loss = (
|
|
311
|
+
reconstruction_loss + self.beta * kl_loss + self.alpha * (true_loss + false_loss)
|
|
312
|
+
)
|
|
313
|
+
if self.regularization_active:
|
|
314
|
+
total_loss = total_loss + reg_loss
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
"total_loss": total_loss,
|
|
318
|
+
"reconstruction_loss": reconstruction_loss,
|
|
319
|
+
"kl_loss": kl_loss,
|
|
320
|
+
"kl_per_dim": kl_per_dim,
|
|
321
|
+
"true_loss": true_loss,
|
|
322
|
+
"false_loss": false_loss,
|
|
323
|
+
"reg_loss": reg_loss,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def build_encoder(
|
|
328
|
+
latent_dim: int = 8, dense_size: int = 512, kernel_size: tuple[int, int] = (3, 3)
|
|
329
|
+
) -> keras.Model:
|
|
330
|
+
"""Build encoder network for Beta-VAE.
|
|
331
|
+
|
|
332
|
+
The encoder compresses input spectrograms into a lower-dimensional latent space.
|
|
333
|
+
It uses a series of convolutional layers followed by dense layers to learn
|
|
334
|
+
the parameters (mean & log-variance) of the Gaussian posterior over the latent space.
|
|
335
|
+
|
|
336
|
+
Architecture Overview:
|
|
337
|
+
---------------------
|
|
338
|
+
Input: (16, 512, 1) - spectrogram with 16 time bins, 512 frequency bins, 1 polarization channel
|
|
339
|
+
|
|
340
|
+
Convolutional Layers (9 total):
|
|
341
|
+
- 4 downsampling layers (stride=2) reduce spatial dims: 16→8→4→2→1, 512→256→128→64→32
|
|
342
|
+
- 5 feature extraction layers (stride=1) maintain spatial dims
|
|
343
|
+
- Filter progression: 1→16→16→32→32→32→64→64→128→256
|
|
344
|
+
|
|
345
|
+
Dense Layers:
|
|
346
|
+
- Flatten: (1, 32, 256) → (8192,) [1*32*256 = 8192]
|
|
347
|
+
- Dense: (8192,) → (dense_size,) [default 512]
|
|
348
|
+
- z_mean: (dense_size,) → (latent_dim,) [default 8]
|
|
349
|
+
- z_log_var: (dense_size,) → (latent_dim,) [default 8]
|
|
350
|
+
|
|
351
|
+
Output: z_mean, z_log_var, z (sampled latent vector)
|
|
352
|
+
|
|
353
|
+
Layer Details:
|
|
354
|
+
-------------
|
|
355
|
+
| Enc Layer | Filters | Stride | Input Shape | Output Shape |
|
|
356
|
+
|-----------|---------|--------|------------------|------------------|
|
|
357
|
+
| 1 | 16 | 2 | (16, 512, 1) | (8, 256, 16) |
|
|
358
|
+
| 2 | 16 | 1 | (8, 256, 16) | (8, 256, 16) |
|
|
359
|
+
| 3 | 32 | 2 | (8, 256, 16) | (4, 128, 32) |
|
|
360
|
+
| 4 | 32 | 1 | (4, 128, 32) | (4, 128, 32) |
|
|
361
|
+
| 5 | 32 | 1 | (4, 128, 32) | (4, 128, 32) |
|
|
362
|
+
| 6 | 64 | 2 | (4, 128, 32) | (2, 64, 64) |
|
|
363
|
+
| 7 | 64 | 1 | (2, 64, 64) | (2, 64, 64) |
|
|
364
|
+
| 8 | 128 | 1 | (2, 64, 64) | (2, 64, 128) |
|
|
365
|
+
| 9 | 256 | 2 | (2, 64, 128) | (1, 32, 256) |
|
|
366
|
+
|
|
367
|
+
Regularization (all layers):
|
|
368
|
+
- kernel_initializer: HeNormal (conv/dense) or GlorotNormal (latent)
|
|
369
|
+
- bias_initializer: Zeros (except z_log_var uses -3.0 for tighter initial posterior)
|
|
370
|
+
- activity_regularizer: L1(0.001) - encourages sparse activations
|
|
371
|
+
- kernel_regularizer: L2(0.01) - prevents large weights
|
|
372
|
+
- bias_regularizer: L2(0.01) - prevents large biases
|
|
373
|
+
|
|
374
|
+
The returned keras.Model outputs [z_mean, z_log_var, z]. The decoder (build_decoder) must
|
|
375
|
+
remain an exact mirror of this architecture for VAE symmetry — any layer-structure change
|
|
376
|
+
here must be reflected there.
|
|
377
|
+
"""
|
|
378
|
+
# Input shape: (batch, 16, 512, 1) - "grayscale" spectrogram
|
|
379
|
+
encoder_inputs = keras.Input(shape=(16, 512, 1), name="encoder_input")
|
|
380
|
+
|
|
381
|
+
# Convolutional layers
|
|
382
|
+
# Layer 1: (16, 512, 1) → (8, 256, 16) - stride 2 downsamples spatial dims by 2x
|
|
383
|
+
x = layers.Conv2D(
|
|
384
|
+
16,
|
|
385
|
+
kernel_size,
|
|
386
|
+
activation="relu",
|
|
387
|
+
strides=2,
|
|
388
|
+
padding="same",
|
|
389
|
+
kernel_initializer=HeNormal(),
|
|
390
|
+
bias_initializer=Zeros(),
|
|
391
|
+
activity_regularizer=l1(0.001),
|
|
392
|
+
kernel_regularizer=l2(0.01),
|
|
393
|
+
bias_regularizer=l2(0.01),
|
|
394
|
+
)(encoder_inputs)
|
|
395
|
+
|
|
396
|
+
# Layer 2: (8, 256, 16) → (8, 256, 16) - stride 1 maintains spatial dims
|
|
397
|
+
x = layers.Conv2D(
|
|
398
|
+
16,
|
|
399
|
+
kernel_size,
|
|
400
|
+
activation="relu",
|
|
401
|
+
strides=1,
|
|
402
|
+
padding="same",
|
|
403
|
+
kernel_initializer=HeNormal(),
|
|
404
|
+
bias_initializer=Zeros(),
|
|
405
|
+
activity_regularizer=l1(0.001),
|
|
406
|
+
kernel_regularizer=l2(0.01),
|
|
407
|
+
bias_regularizer=l2(0.01),
|
|
408
|
+
)(x)
|
|
409
|
+
|
|
410
|
+
# Layer 3: (8, 256, 16) → (4, 128, 32) - stride 2 downsamples
|
|
411
|
+
x = layers.Conv2D(
|
|
412
|
+
32,
|
|
413
|
+
kernel_size,
|
|
414
|
+
activation="relu",
|
|
415
|
+
strides=2,
|
|
416
|
+
padding="same",
|
|
417
|
+
kernel_initializer=HeNormal(),
|
|
418
|
+
bias_initializer=Zeros(),
|
|
419
|
+
activity_regularizer=l1(0.001),
|
|
420
|
+
kernel_regularizer=l2(0.01),
|
|
421
|
+
bias_regularizer=l2(0.01),
|
|
422
|
+
)(x)
|
|
423
|
+
|
|
424
|
+
# Layer 4: (4, 128, 32) → (4, 128, 32) - stride 1 maintains
|
|
425
|
+
x = layers.Conv2D(
|
|
426
|
+
32,
|
|
427
|
+
kernel_size,
|
|
428
|
+
activation="relu",
|
|
429
|
+
strides=1,
|
|
430
|
+
padding="same",
|
|
431
|
+
kernel_initializer=HeNormal(),
|
|
432
|
+
bias_initializer=Zeros(),
|
|
433
|
+
activity_regularizer=l1(0.001),
|
|
434
|
+
kernel_regularizer=l2(0.01),
|
|
435
|
+
bias_regularizer=l2(0.01),
|
|
436
|
+
)(x)
|
|
437
|
+
|
|
438
|
+
# Layer 5: (4, 128, 32) → (4, 128, 32) - stride 1 maintains
|
|
439
|
+
x = layers.Conv2D(
|
|
440
|
+
32,
|
|
441
|
+
kernel_size,
|
|
442
|
+
activation="relu",
|
|
443
|
+
strides=1,
|
|
444
|
+
padding="same",
|
|
445
|
+
kernel_initializer=HeNormal(),
|
|
446
|
+
bias_initializer=Zeros(),
|
|
447
|
+
activity_regularizer=l1(0.001),
|
|
448
|
+
kernel_regularizer=l2(0.01),
|
|
449
|
+
bias_regularizer=l2(0.01),
|
|
450
|
+
)(x)
|
|
451
|
+
|
|
452
|
+
# Layer 6: (4, 128, 32) → (2, 64, 64) - stride 2 downsamples
|
|
453
|
+
x = layers.Conv2D(
|
|
454
|
+
64,
|
|
455
|
+
kernel_size,
|
|
456
|
+
activation="relu",
|
|
457
|
+
strides=2,
|
|
458
|
+
padding="same",
|
|
459
|
+
kernel_initializer=HeNormal(),
|
|
460
|
+
bias_initializer=Zeros(),
|
|
461
|
+
activity_regularizer=l1(0.001),
|
|
462
|
+
kernel_regularizer=l2(0.01),
|
|
463
|
+
bias_regularizer=l2(0.01),
|
|
464
|
+
)(x)
|
|
465
|
+
|
|
466
|
+
# Layer 7: (2, 64, 64) → (2, 64, 64) - stride 1 maintains
|
|
467
|
+
x = layers.Conv2D(
|
|
468
|
+
64,
|
|
469
|
+
kernel_size,
|
|
470
|
+
activation="relu",
|
|
471
|
+
strides=1,
|
|
472
|
+
padding="same",
|
|
473
|
+
kernel_initializer=HeNormal(),
|
|
474
|
+
bias_initializer=Zeros(),
|
|
475
|
+
activity_regularizer=l1(0.001),
|
|
476
|
+
kernel_regularizer=l2(0.01),
|
|
477
|
+
bias_regularizer=l2(0.01),
|
|
478
|
+
)(x)
|
|
479
|
+
|
|
480
|
+
# Layer 8: (2, 64, 64) → (2, 64, 128) - stride 1, increase filters
|
|
481
|
+
x = layers.Conv2D(
|
|
482
|
+
128,
|
|
483
|
+
kernel_size,
|
|
484
|
+
activation="relu",
|
|
485
|
+
strides=1,
|
|
486
|
+
padding="same",
|
|
487
|
+
kernel_initializer=HeNormal(),
|
|
488
|
+
bias_initializer=Zeros(),
|
|
489
|
+
activity_regularizer=l1(0.001),
|
|
490
|
+
kernel_regularizer=l2(0.01),
|
|
491
|
+
bias_regularizer=l2(0.01),
|
|
492
|
+
)(x)
|
|
493
|
+
|
|
494
|
+
# Layer 9: (2, 64, 128) → (1, 32, 256) - stride 2 final downsample
|
|
495
|
+
x = layers.Conv2D(
|
|
496
|
+
256,
|
|
497
|
+
kernel_size,
|
|
498
|
+
activation="relu",
|
|
499
|
+
strides=2,
|
|
500
|
+
padding="same",
|
|
501
|
+
kernel_initializer=HeNormal(),
|
|
502
|
+
bias_initializer=Zeros(),
|
|
503
|
+
activity_regularizer=l1(0.001),
|
|
504
|
+
kernel_regularizer=l2(0.01),
|
|
505
|
+
bias_regularizer=l2(0.01),
|
|
506
|
+
)(x)
|
|
507
|
+
|
|
508
|
+
# Dense layers
|
|
509
|
+
# Flatten: (1, 32, 256) → (8192,)
|
|
510
|
+
# The 32 comes from: 512 / 2^4 = 32 (4 stride-2 layers)
|
|
511
|
+
x = layers.Flatten()(x)
|
|
512
|
+
|
|
513
|
+
# Dense: (8192,) → (dense_size,) - compress to intermediate representation
|
|
514
|
+
x = layers.Dense(
|
|
515
|
+
dense_size,
|
|
516
|
+
activation="relu",
|
|
517
|
+
kernel_initializer=HeNormal(),
|
|
518
|
+
bias_initializer=Zeros(),
|
|
519
|
+
activity_regularizer=l1(0.001),
|
|
520
|
+
kernel_regularizer=l2(0.01),
|
|
521
|
+
bias_regularizer=l2(0.01),
|
|
522
|
+
)(x)
|
|
523
|
+
|
|
524
|
+
# Latent space
|
|
525
|
+
# z_mean: (dense_size,) → (latent_dim,) - mean of latent distribution
|
|
526
|
+
# dtype="float32": fp32 island under beta_vae.mixed_precision — the latent heads feed the
|
|
527
|
+
# KL exp/square math and the Sampling layer, which must not run in bf16. A no-op under
|
|
528
|
+
# the default fp32 policy (identical graph).
|
|
529
|
+
z_mean = layers.Dense(
|
|
530
|
+
latent_dim,
|
|
531
|
+
name="z_mean",
|
|
532
|
+
dtype="float32",
|
|
533
|
+
kernel_initializer=GlorotNormal(),
|
|
534
|
+
bias_initializer=Zeros(),
|
|
535
|
+
activity_regularizer=l1(0.001),
|
|
536
|
+
kernel_regularizer=l2(0.01),
|
|
537
|
+
bias_regularizer=l2(0.01),
|
|
538
|
+
)(x)
|
|
539
|
+
|
|
540
|
+
# z_log_var: (dense_size,) → (latent_dim,) - log-variance of latent distribution
|
|
541
|
+
z_log_var = layers.Dense(
|
|
542
|
+
latent_dim,
|
|
543
|
+
name="z_log_var",
|
|
544
|
+
dtype="float32", # fp32 island under beta_vae.mixed_precision (see z_mean above)
|
|
545
|
+
kernel_initializer=GlorotNormal(),
|
|
546
|
+
bias_initializer=Constant(
|
|
547
|
+
-3.0 # Negative bias initialization tightens initial posterior around prior
|
|
548
|
+
),
|
|
549
|
+
activity_regularizer=l1(0.001),
|
|
550
|
+
kernel_regularizer=l2(0.01),
|
|
551
|
+
bias_regularizer=l2(0.01),
|
|
552
|
+
)(x)
|
|
553
|
+
|
|
554
|
+
# Sampling: sample z from N(z_mean, exp(z_log_var)) using reparameterization trick
|
|
555
|
+
# dtype="float32": fp32 island under beta_vae.mixed_precision — the exp() and epsilon
|
|
556
|
+
# draw stay fp32 (its z_mean/z_log_var inputs are fp32 via the heads above)
|
|
557
|
+
z = Sampling(dtype="float32")([z_mean, z_log_var])
|
|
558
|
+
|
|
559
|
+
encoder = keras.Model(encoder_inputs, [z_mean, z_log_var, z], name="encoder")
|
|
560
|
+
|
|
561
|
+
return encoder
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def build_decoder(
|
|
565
|
+
latent_dim: int = 8, dense_size: int = 512, kernel_size: tuple[int, int] = (3, 3)
|
|
566
|
+
) -> keras.Model:
|
|
567
|
+
"""Build decoder network for Beta-VAE - exact mirror of encoder.
|
|
568
|
+
|
|
569
|
+
The decoder reconstructs spectrograms from latent vectors. It is architecturally
|
|
570
|
+
symmetric to the encoder: each encoder layer has a corresponding decoder layer
|
|
571
|
+
that reverses the transformation.
|
|
572
|
+
|
|
573
|
+
Architecture Overview:
|
|
574
|
+
---------------------
|
|
575
|
+
Input: (latent_dim,) - sampled latent vector z (default: 8)
|
|
576
|
+
|
|
577
|
+
Dense Layers (mirrors encoder's dense → flatten path in reverse):
|
|
578
|
+
- Dense: (latent_dim,) → (dense_size,) [mirrors z_mean/z_log_var]
|
|
579
|
+
- Dense: (dense_size,) → (8192,) [mirrors Dense before Flatten]
|
|
580
|
+
- Reshape: (8192,) → (1, 32, 256) [mirrors Flatten]
|
|
581
|
+
|
|
582
|
+
Conv Transpose Layers (9 total, mirrors encoder's 9 conv layers in reverse):
|
|
583
|
+
- 4 upsampling layers (stride=2) expand spatial dims: 1→2→4→8→16, 32→64→128→256→512
|
|
584
|
+
- 5 feature transformation layers (stride=1) maintain spatial dims
|
|
585
|
+
- Filter progression: 256→128→64→64→32→32→32→16→16→1
|
|
586
|
+
|
|
587
|
+
Output: (16, 512, 1) - reconstructed spectrogram (matches encoder input)
|
|
588
|
+
|
|
589
|
+
Symmetry Principle:
|
|
590
|
+
------------------
|
|
591
|
+
For each encoder layer i, decoder layer (9-i+1) reverses it:
|
|
592
|
+
- Decoder layer outputs the same number of filters as encoder layer's INPUT channels
|
|
593
|
+
- Decoder layer uses the same stride as encoder layer (stride-2 upsamples instead of downsamples)
|
|
594
|
+
- Shapes match: decoder layer output shape == encoder layer input shape
|
|
595
|
+
|
|
596
|
+
Layer Details:
|
|
597
|
+
-------------
|
|
598
|
+
| Dec Layer | Filters | Stride | Input Shape | Output Shape | Mirrors Enc |
|
|
599
|
+
|-----------|---------|--------|------------------|------------------|-------------|
|
|
600
|
+
| 1 | 128 | 2 | (1, 32, 256) | (2, 64, 128) | Enc 9 |
|
|
601
|
+
| 2 | 64 | 1 | (2, 64, 128) | (2, 64, 64) | Enc 8 |
|
|
602
|
+
| 3 | 64 | 1 | (2, 64, 64) | (2, 64, 64) | Enc 7 |
|
|
603
|
+
| 4 | 32 | 2 | (2, 64, 64) | (4, 128, 32) | Enc 6 |
|
|
604
|
+
| 5 | 32 | 1 | (4, 128, 32) | (4, 128, 32) | Enc 5 |
|
|
605
|
+
| 6 | 32 | 1 | (4, 128, 32) | (4, 128, 32) | Enc 4 |
|
|
606
|
+
| 7 | 16 | 2 | (4, 128, 32) | (8, 256, 16) | Enc 3 |
|
|
607
|
+
| 8 | 16 | 1 | (8, 256, 16) | (8, 256, 16) | Enc 2 |
|
|
608
|
+
| 9 | 1 | 2 | (8, 256, 16) | (16, 512, 1) | Enc 1 |
|
|
609
|
+
|
|
610
|
+
Regularization (all layers including output):
|
|
611
|
+
- kernel_initializer: HeNormal (hidden layers) or GlorotNormal (output layer)
|
|
612
|
+
- bias_initializer: Zeros
|
|
613
|
+
- activity_regularizer: L1(0.001) - encourages sparse activations
|
|
614
|
+
- kernel_regularizer: L2(0.01) - prevents large weights
|
|
615
|
+
- bias_regularizer: L2(0.01) - prevents large biases
|
|
616
|
+
|
|
617
|
+
Output Layer Notes:
|
|
618
|
+
- Uses sigmoid activation (not relu) to bound output to [0, 1] for BCE loss
|
|
619
|
+
- Uses GlorotNormal initialization (matches encoder's latent layer style)
|
|
620
|
+
- Includes full regularization for symmetry with encoder's first conv layer
|
|
621
|
+
|
|
622
|
+
The returned keras.Model outputs reconstructed spectrograms of shape (16, 512, 1). This
|
|
623
|
+
architecture is the exact mirror of build_encoder — any layer-structure change there must
|
|
624
|
+
be reflected here to maintain symmetry.
|
|
625
|
+
"""
|
|
626
|
+
# Input shape: (batch, latent_dim) - sampled latent vector z
|
|
627
|
+
latent_inputs = keras.Input(shape=(latent_dim,), name="decoder_input")
|
|
628
|
+
|
|
629
|
+
# Dense layers: these mirror the encoder's latent → dense → flatten path in reverse
|
|
630
|
+
# Dense: (latent_dim,) → (dense_size,) - mirrors encoder's z_mean/z_log_var layers
|
|
631
|
+
x = layers.Dense(
|
|
632
|
+
dense_size,
|
|
633
|
+
activation="relu",
|
|
634
|
+
kernel_initializer=HeNormal(),
|
|
635
|
+
bias_initializer=Zeros(),
|
|
636
|
+
activity_regularizer=l1(0.001),
|
|
637
|
+
kernel_regularizer=l2(0.01),
|
|
638
|
+
bias_regularizer=l2(0.01),
|
|
639
|
+
)(latent_inputs)
|
|
640
|
+
|
|
641
|
+
# Dense: (dense_size,) → (8192,) - mirrors encoder's Dense(dense_size) layer
|
|
642
|
+
# The 8192 = 1 * 32 * 256 comes from encoder's final conv output shape
|
|
643
|
+
# Where 32 = 512 / 2^4 (4 stride-2 layers reduce width from 512 to 32)
|
|
644
|
+
x = layers.Dense(
|
|
645
|
+
1 * 32 * 256,
|
|
646
|
+
activation="relu",
|
|
647
|
+
kernel_initializer=HeNormal(),
|
|
648
|
+
bias_initializer=Zeros(),
|
|
649
|
+
activity_regularizer=l1(0.001),
|
|
650
|
+
kernel_regularizer=l2(0.01),
|
|
651
|
+
bias_regularizer=l2(0.01),
|
|
652
|
+
)(x)
|
|
653
|
+
|
|
654
|
+
# Reshape: (8192,) → (1, 32, 256) - mirrors encoder's Flatten layer
|
|
655
|
+
x = layers.Reshape((1, 32, 256))(x)
|
|
656
|
+
|
|
657
|
+
# Convolutional transpose layers
|
|
658
|
+
# These mirror the encoder's conv layers in reverse order
|
|
659
|
+
# Each layer outputs filters matching the INPUT channels of the corresponding encoder layer
|
|
660
|
+
# Layer 1: (1, 32, 256) → (2, 64, 128) - mirrors encoder layer 9 (256 filters, s=2)
|
|
661
|
+
# Output 128 filters to match encoder layer 9's input channels
|
|
662
|
+
x = layers.Conv2DTranspose(
|
|
663
|
+
128,
|
|
664
|
+
kernel_size,
|
|
665
|
+
activation="relu",
|
|
666
|
+
strides=2,
|
|
667
|
+
padding="same",
|
|
668
|
+
kernel_initializer=HeNormal(),
|
|
669
|
+
bias_initializer=Zeros(),
|
|
670
|
+
activity_regularizer=l1(0.001),
|
|
671
|
+
kernel_regularizer=l2(0.01),
|
|
672
|
+
bias_regularizer=l2(0.01),
|
|
673
|
+
)(x)
|
|
674
|
+
|
|
675
|
+
# Layer 2: (2, 64, 128) → (2, 64, 64) - mirrors encoder layer 8 (128 filters, s=1)
|
|
676
|
+
# Output 64 filters to match encoder layer 8's input channels
|
|
677
|
+
x = layers.Conv2DTranspose(
|
|
678
|
+
64,
|
|
679
|
+
kernel_size,
|
|
680
|
+
activation="relu",
|
|
681
|
+
strides=1,
|
|
682
|
+
padding="same",
|
|
683
|
+
kernel_initializer=HeNormal(),
|
|
684
|
+
bias_initializer=Zeros(),
|
|
685
|
+
activity_regularizer=l1(0.001),
|
|
686
|
+
kernel_regularizer=l2(0.01),
|
|
687
|
+
bias_regularizer=l2(0.01),
|
|
688
|
+
)(x)
|
|
689
|
+
|
|
690
|
+
# Layer 3: (2, 64, 64) → (2, 64, 64) - mirrors encoder layer 7 (64 filters, s=1)
|
|
691
|
+
# Output 64 filters to match encoder layer 7's input channels
|
|
692
|
+
x = layers.Conv2DTranspose(
|
|
693
|
+
64,
|
|
694
|
+
kernel_size,
|
|
695
|
+
activation="relu",
|
|
696
|
+
strides=1,
|
|
697
|
+
padding="same",
|
|
698
|
+
kernel_initializer=HeNormal(),
|
|
699
|
+
bias_initializer=Zeros(),
|
|
700
|
+
activity_regularizer=l1(0.001),
|
|
701
|
+
kernel_regularizer=l2(0.01),
|
|
702
|
+
bias_regularizer=l2(0.01),
|
|
703
|
+
)(x)
|
|
704
|
+
|
|
705
|
+
# Layer 4: (2, 64, 64) → (4, 128, 32) - mirrors encoder layer 6 (64 filters, s=2)
|
|
706
|
+
# Output 32 filters to match encoder layer 6's input channels
|
|
707
|
+
x = layers.Conv2DTranspose(
|
|
708
|
+
32,
|
|
709
|
+
kernel_size,
|
|
710
|
+
activation="relu",
|
|
711
|
+
strides=2,
|
|
712
|
+
padding="same",
|
|
713
|
+
kernel_initializer=HeNormal(),
|
|
714
|
+
bias_initializer=Zeros(),
|
|
715
|
+
activity_regularizer=l1(0.001),
|
|
716
|
+
kernel_regularizer=l2(0.01),
|
|
717
|
+
bias_regularizer=l2(0.01),
|
|
718
|
+
)(x)
|
|
719
|
+
|
|
720
|
+
# Layer 5: (4, 128, 32) → (4, 128, 32) - mirrors encoder layer 5 (32 filters, s=1)
|
|
721
|
+
# Output 32 filters to match encoder layer 5's input channels
|
|
722
|
+
x = layers.Conv2DTranspose(
|
|
723
|
+
32,
|
|
724
|
+
kernel_size,
|
|
725
|
+
activation="relu",
|
|
726
|
+
strides=1,
|
|
727
|
+
padding="same",
|
|
728
|
+
kernel_initializer=HeNormal(),
|
|
729
|
+
bias_initializer=Zeros(),
|
|
730
|
+
activity_regularizer=l1(0.001),
|
|
731
|
+
kernel_regularizer=l2(0.01),
|
|
732
|
+
bias_regularizer=l2(0.01),
|
|
733
|
+
)(x)
|
|
734
|
+
|
|
735
|
+
# Layer 6: (4, 128, 32) → (4, 128, 32) - mirrors encoder layer 4 (32 filters, s=1)
|
|
736
|
+
# Output 32 filters to match encoder layer 4's input channels
|
|
737
|
+
x = layers.Conv2DTranspose(
|
|
738
|
+
32,
|
|
739
|
+
kernel_size,
|
|
740
|
+
activation="relu",
|
|
741
|
+
strides=1,
|
|
742
|
+
padding="same",
|
|
743
|
+
kernel_initializer=HeNormal(),
|
|
744
|
+
bias_initializer=Zeros(),
|
|
745
|
+
activity_regularizer=l1(0.001),
|
|
746
|
+
kernel_regularizer=l2(0.01),
|
|
747
|
+
bias_regularizer=l2(0.01),
|
|
748
|
+
)(x)
|
|
749
|
+
|
|
750
|
+
# Layer 7: (4, 128, 32) → (8, 256, 16) - mirrors encoder layer 3 (32 filters, s=2)
|
|
751
|
+
# Output 16 filters to match encoder layer 3's input channels
|
|
752
|
+
x = layers.Conv2DTranspose(
|
|
753
|
+
16,
|
|
754
|
+
kernel_size,
|
|
755
|
+
activation="relu",
|
|
756
|
+
strides=2,
|
|
757
|
+
padding="same",
|
|
758
|
+
kernel_initializer=HeNormal(),
|
|
759
|
+
bias_initializer=Zeros(),
|
|
760
|
+
activity_regularizer=l1(0.001),
|
|
761
|
+
kernel_regularizer=l2(0.01),
|
|
762
|
+
bias_regularizer=l2(0.01),
|
|
763
|
+
)(x)
|
|
764
|
+
|
|
765
|
+
# Layer 8: (8, 256, 16) → (8, 256, 16) - mirrors encoder layer 2 (16 filters, s=1)
|
|
766
|
+
# Output 16 filters to match encoder layer 2's input channels
|
|
767
|
+
x = layers.Conv2DTranspose(
|
|
768
|
+
16,
|
|
769
|
+
kernel_size,
|
|
770
|
+
activation="relu",
|
|
771
|
+
strides=1,
|
|
772
|
+
padding="same",
|
|
773
|
+
kernel_initializer=HeNormal(),
|
|
774
|
+
bias_initializer=Zeros(),
|
|
775
|
+
activity_regularizer=l1(0.001),
|
|
776
|
+
kernel_regularizer=l2(0.01),
|
|
777
|
+
bias_regularizer=l2(0.01),
|
|
778
|
+
)(x)
|
|
779
|
+
|
|
780
|
+
# Output layer
|
|
781
|
+
# Layer 9: (8, 256, 16) → (16, 512, 1) - mirrors encoder layer 1 (16 filters, s=2)
|
|
782
|
+
# Output 1 filter to match encoder input channels ("grayscale" spectrogram)
|
|
783
|
+
# Uses sigmoid activation to bound output to [0, 1] for binary cross-entropy loss
|
|
784
|
+
# Uses GlorotNormal (like encoder's latent layers) as this is the "boundary" layer
|
|
785
|
+
# Includes full regularization for symmetry with encoder's first conv layer
|
|
786
|
+
# dtype="float32": fp32 island under beta_vae.mixed_precision — the sigmoid output feeds
|
|
787
|
+
# the BCE reconstruction loss, which must compute on fp32 tensors. A no-op under the
|
|
788
|
+
# default fp32 policy (identical graph).
|
|
789
|
+
decoder_outputs = layers.Conv2DTranspose(
|
|
790
|
+
1,
|
|
791
|
+
kernel_size,
|
|
792
|
+
activation="sigmoid",
|
|
793
|
+
strides=2,
|
|
794
|
+
padding="same",
|
|
795
|
+
dtype="float32",
|
|
796
|
+
kernel_initializer=GlorotNormal(),
|
|
797
|
+
bias_initializer=Zeros(),
|
|
798
|
+
activity_regularizer=l1(0.001),
|
|
799
|
+
kernel_regularizer=l2(0.01),
|
|
800
|
+
bias_regularizer=l2(0.01),
|
|
801
|
+
)(x)
|
|
802
|
+
|
|
803
|
+
decoder = keras.Model(latent_inputs, decoder_outputs, name="decoder")
|
|
804
|
+
|
|
805
|
+
return decoder
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def create_beta_vae_model():
|
|
809
|
+
"""Create and compile Beta-VAE model"""
|
|
810
|
+
|
|
811
|
+
logger.info("Creating Beta-VAE model...")
|
|
812
|
+
|
|
813
|
+
config = get_config()
|
|
814
|
+
if config is None:
|
|
815
|
+
raise ValueError("get_config() returned None")
|
|
816
|
+
|
|
817
|
+
encoder = build_encoder(
|
|
818
|
+
latent_dim=config.beta_vae.latent_dim,
|
|
819
|
+
dense_size=config.beta_vae.dense_layer_size,
|
|
820
|
+
kernel_size=config.beta_vae.kernel_size,
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
decoder = build_decoder(
|
|
824
|
+
latent_dim=config.beta_vae.latent_dim,
|
|
825
|
+
dense_size=config.beta_vae.dense_layer_size,
|
|
826
|
+
kernel_size=config.beta_vae.kernel_size,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
beta_vae = BetaVAE(
|
|
830
|
+
encoder,
|
|
831
|
+
decoder,
|
|
832
|
+
alpha=config.beta_vae.alpha,
|
|
833
|
+
beta=config.beta_vae.beta,
|
|
834
|
+
regularization_active=config.beta_vae.regularization_active,
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
beta_vae.compile(
|
|
838
|
+
optimizer=keras.optimizers.Adam(learning_rate=config.training.base_learning_rate)
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
logger.info(
|
|
842
|
+
f"Created Beta-VAE model: latent_dim={config.beta_vae.latent_dim}, "
|
|
843
|
+
f"beta={config.beta_vae.beta}, alpha={config.beta_vae.alpha}"
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
encoder.summary(print_fn=logger.info)
|
|
847
|
+
decoder.summary(print_fn=logger.info)
|
|
848
|
+
|
|
849
|
+
return beta_vae
|