likelihood 2.2.0.dev1__cp310-cp310-musllinux_1_2_x86_64.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.
- likelihood/VERSION +1 -0
- likelihood/__init__.py +20 -0
- likelihood/graph/__init__.py +9 -0
- likelihood/graph/_nn.py +283 -0
- likelihood/graph/graph.py +86 -0
- likelihood/graph/nn.py +329 -0
- likelihood/main.py +273 -0
- likelihood/models/__init__.py +3 -0
- likelihood/models/deep/__init__.py +13 -0
- likelihood/models/deep/_autoencoders.py +896 -0
- likelihood/models/deep/_predictor.py +809 -0
- likelihood/models/deep/autoencoders.py +903 -0
- likelihood/models/deep/bandit.py +97 -0
- likelihood/models/deep/gan.py +313 -0
- likelihood/models/deep/predictor.py +805 -0
- likelihood/models/deep/rl.py +345 -0
- likelihood/models/environments.py +202 -0
- likelihood/models/hmm.py +163 -0
- likelihood/models/regression.py +451 -0
- likelihood/models/simulation.py +213 -0
- likelihood/models/utils.py +87 -0
- likelihood/pipes.py +382 -0
- likelihood/rust_py_integration.cpython-310-x86_64-linux-gnu.so +0 -0
- likelihood/tools/__init__.py +4 -0
- likelihood/tools/cat_embed.py +212 -0
- likelihood/tools/figures.py +348 -0
- likelihood/tools/impute.py +278 -0
- likelihood/tools/models_tools.py +866 -0
- likelihood/tools/numeric_tools.py +390 -0
- likelihood/tools/reports.py +375 -0
- likelihood/tools/tools.py +1336 -0
- likelihood-2.2.0.dev1.dist-info/METADATA +68 -0
- likelihood-2.2.0.dev1.dist-info/RECORD +39 -0
- likelihood-2.2.0.dev1.dist-info/WHEEL +5 -0
- likelihood-2.2.0.dev1.dist-info/licenses/LICENSE +21 -0
- likelihood-2.2.0.dev1.dist-info/sboms/auditwheel.cdx.json +1 -0
- likelihood-2.2.0.dev1.dist-info/top_level.txt +5 -0
- likelihood.libs/libgcc_s-0cd532bd.so.1 +0 -0
- src/lib.rs +12 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from functools import partial
|
|
4
|
+
from shutil import rmtree
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
|
|
10
|
+
logging.getLogger("tensorflow").setLevel(logging.ERROR)
|
|
11
|
+
|
|
12
|
+
import keras_tuner
|
|
13
|
+
import tensorflow as tf
|
|
14
|
+
from tensorflow.keras.layers import InputLayer
|
|
15
|
+
from tensorflow.keras.regularizers import l2
|
|
16
|
+
|
|
17
|
+
from likelihood.tools import LoRALayer, OneHotEncoder, suppress_warnings
|
|
18
|
+
|
|
19
|
+
tf.get_logger().setLevel("ERROR")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EarlyStopping:
|
|
23
|
+
def __init__(self, patience=10, min_delta=0.001):
|
|
24
|
+
self.patience = patience
|
|
25
|
+
self.min_delta = min_delta
|
|
26
|
+
self.best_loss = np.inf
|
|
27
|
+
self.counter = 0
|
|
28
|
+
self.stop_training = False
|
|
29
|
+
|
|
30
|
+
def __call__(self, current_loss):
|
|
31
|
+
if self.best_loss - current_loss > self.min_delta:
|
|
32
|
+
self.best_loss = current_loss
|
|
33
|
+
self.counter = 0
|
|
34
|
+
else:
|
|
35
|
+
self.counter += 1
|
|
36
|
+
|
|
37
|
+
if self.counter >= self.patience:
|
|
38
|
+
self.stop_training = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def mse_loss(y_true, y_pred):
|
|
42
|
+
"""
|
|
43
|
+
Mean squared error loss function.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
y_true : `tf.Tensor`
|
|
48
|
+
The true values.
|
|
49
|
+
y_pred : `tf.Tensor`
|
|
50
|
+
The predicted values.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
`tf.Tensor`
|
|
55
|
+
"""
|
|
56
|
+
return tf.reduce_mean(tf.square(y_true - y_pred))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def kl_loss(mean, log_var):
|
|
60
|
+
"""
|
|
61
|
+
Kullback-Leibler divergence loss function.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
mean : `tf.Tensor`
|
|
66
|
+
The mean of the distribution.
|
|
67
|
+
log_var : `tf.Tensor`
|
|
68
|
+
The log variance of the distribution.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
`tf.Tensor`
|
|
73
|
+
"""
|
|
74
|
+
return -0.5 * tf.reduce_mean(1 + log_var - tf.square(mean) - tf.exp(log_var))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def vae_loss(y_true, y_pred, mean, log_var):
|
|
78
|
+
"""
|
|
79
|
+
Variational autoencoder loss function.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
y_true : `tf.Tensor`
|
|
84
|
+
The true values.
|
|
85
|
+
y_pred : `tf.Tensor`
|
|
86
|
+
The predicted values.
|
|
87
|
+
mean : `tf.Tensor`
|
|
88
|
+
The mean of the distribution.
|
|
89
|
+
log_var : `tf.Tensor`
|
|
90
|
+
The log variance of the distribution.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
`tf.Tensor`
|
|
95
|
+
"""
|
|
96
|
+
return mse_loss(y_true, y_pred) + kl_loss(mean, log_var)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def sampling(mean, log_var, epsilon_value=1e-8):
|
|
100
|
+
"""
|
|
101
|
+
Samples from the distribution.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
mean : `tf.Tensor`
|
|
106
|
+
The mean of the distribution.
|
|
107
|
+
log_var : `tf.Tensor`
|
|
108
|
+
The log variance of the distribution.
|
|
109
|
+
epsilon_value : float
|
|
110
|
+
A small value to avoid numerical instability.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
`tf.Tensor`
|
|
115
|
+
"""
|
|
116
|
+
epsilon = tf.random.normal(shape=tf.shape(mean), mean=0.0, stddev=1.0)
|
|
117
|
+
stddev = tf.exp(0.5 * log_var) + epsilon_value
|
|
118
|
+
epsilon = tf.random.normal(shape=tf.shape(mean), mean=0.0, stddev=1.0)
|
|
119
|
+
return mean + stddev * epsilon
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def check_for_nans(tensors, name="Tensor"):
|
|
123
|
+
for t in tensors:
|
|
124
|
+
if tf.reduce_any(tf.math.is_nan(t)) or tf.reduce_any(tf.math.is_inf(t)):
|
|
125
|
+
print(f"Warning: {name} contains NaNs or Infs")
|
|
126
|
+
return True
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def cal_loss_step(batch, encoder, decoder, vae_mode=False, training=True):
|
|
131
|
+
"""
|
|
132
|
+
Calculates the loss value on a batch of data.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
batch : `tf.Tensor`
|
|
137
|
+
The batch of data.
|
|
138
|
+
encoder : `tf.keras.Model`
|
|
139
|
+
The encoder model.
|
|
140
|
+
decoder : `tf.keras.Model`
|
|
141
|
+
The decoder model.
|
|
142
|
+
optimizer : `tf.keras.optimizers.Optimizer`
|
|
143
|
+
The optimizer to use.
|
|
144
|
+
vae_mode : `bool`
|
|
145
|
+
Whether to use variational autoencoder mode. Default is False.
|
|
146
|
+
training : `bool`
|
|
147
|
+
Whether the model is in training mode. Default is True.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
`tf.Tensor`
|
|
152
|
+
The loss value.
|
|
153
|
+
"""
|
|
154
|
+
if vae_mode:
|
|
155
|
+
mean, log_var = encoder(batch, training=training)
|
|
156
|
+
log_var = tf.clip_by_value(log_var, clip_value_min=1e-8, clip_value_max=tf.float32.max)
|
|
157
|
+
decoded = decoder(sampling(mean, log_var), training=training)
|
|
158
|
+
loss = vae_loss(batch, decoded, mean, log_var)
|
|
159
|
+
else:
|
|
160
|
+
encoded = encoder(batch, training=training)
|
|
161
|
+
decoded = decoder(encoded, training=training)
|
|
162
|
+
loss = mse_loss(batch, decoded)
|
|
163
|
+
|
|
164
|
+
return loss
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@tf.function
|
|
168
|
+
def train_step(batch, encoder, decoder, optimizer, vae_mode=False):
|
|
169
|
+
"""
|
|
170
|
+
Trains the model on a batch of data.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
mean : `tf.Tensor`
|
|
175
|
+
The mean of the distribution.
|
|
176
|
+
log_var : `tf.Tensor`
|
|
177
|
+
The log variance of the distribution.
|
|
178
|
+
batch : `tf.Tensor`
|
|
179
|
+
The batch of data.
|
|
180
|
+
encoder : `tf.keras.Model`
|
|
181
|
+
The encoder model.
|
|
182
|
+
decoder : `tf.keras.Model`
|
|
183
|
+
The decoder model.
|
|
184
|
+
optimizer : `tf.keras.optimizers.Optimizer`
|
|
185
|
+
The optimizer to use.
|
|
186
|
+
vae_mode : `bool`
|
|
187
|
+
Whether to use variational autoencoder mode. Default is False.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
`tf.Tensor`
|
|
192
|
+
The loss value.
|
|
193
|
+
"""
|
|
194
|
+
optimizer.build(encoder.trainable_variables + decoder.trainable_variables)
|
|
195
|
+
|
|
196
|
+
with tf.GradientTape() as encoder_tape, tf.GradientTape() as decoder_tape:
|
|
197
|
+
loss = cal_loss_step(batch, encoder, decoder, vae_mode=vae_mode)
|
|
198
|
+
|
|
199
|
+
gradients_of_encoder = encoder_tape.gradient(loss, encoder.trainable_variables)
|
|
200
|
+
gradients_of_decoder = decoder_tape.gradient(loss, decoder.trainable_variables)
|
|
201
|
+
|
|
202
|
+
optimizer.apply_gradients(zip(gradients_of_encoder, encoder.trainable_variables))
|
|
203
|
+
optimizer.apply_gradients(zip(gradients_of_decoder, decoder.trainable_variables))
|
|
204
|
+
|
|
205
|
+
return loss
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@tf.keras.utils.register_keras_serializable(package="Custom", name="AutoClassifier")
|
|
209
|
+
class AutoClassifier(tf.keras.Model):
|
|
210
|
+
"""
|
|
211
|
+
An auto-classifier model that automatically determines the best classification strategy based on the input data.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
input_shape_parm : `int`
|
|
216
|
+
The shape of the input data.
|
|
217
|
+
num_classes : `int`
|
|
218
|
+
The number of classes in the dataset.
|
|
219
|
+
units : `int`
|
|
220
|
+
The number of neurons in each hidden layer.
|
|
221
|
+
activation : `str`
|
|
222
|
+
The type of activation function to use for the neural network layers.
|
|
223
|
+
|
|
224
|
+
Keyword Arguments
|
|
225
|
+
-----------------
|
|
226
|
+
Additional keyword arguments to pass to the model.
|
|
227
|
+
|
|
228
|
+
classifier_activation : `str`
|
|
229
|
+
The activation function to use for the classifier layer. Default is `softmax`. If the activation function is not a classification function, the model can be used in regression problems.
|
|
230
|
+
num_layers : `int`
|
|
231
|
+
The number of hidden layers in the classifier. Default is 1.
|
|
232
|
+
dropout : `float`
|
|
233
|
+
The dropout rate to use in the classifier. Default is None.
|
|
234
|
+
l2_reg : `float`
|
|
235
|
+
The L2 regularization parameter. Default is 0.0.
|
|
236
|
+
vae_mode : `bool`
|
|
237
|
+
Whether to use variational autoencoder mode. Default is False.
|
|
238
|
+
vae_units : `int`
|
|
239
|
+
The number of units in the variational autoencoder. Default is 2.
|
|
240
|
+
lora_mode : `bool`
|
|
241
|
+
Whether to use LoRA layers. Default is False.
|
|
242
|
+
lora_rank : `int`
|
|
243
|
+
The rank of the LoRA layer. Default is 4.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def __init__(self, input_shape_parm, num_classes, units, activation, **kwargs):
|
|
247
|
+
super(AutoClassifier, self).__init__()
|
|
248
|
+
self.input_shape_parm = input_shape_parm
|
|
249
|
+
self.num_classes = num_classes
|
|
250
|
+
self.units = units
|
|
251
|
+
self.activation = activation
|
|
252
|
+
|
|
253
|
+
self.encoder = None
|
|
254
|
+
self.decoder = None
|
|
255
|
+
self.classifier = None
|
|
256
|
+
self.classifier_activation = kwargs.get("classifier_activation", "softmax")
|
|
257
|
+
self.num_layers = kwargs.get("num_layers", 1)
|
|
258
|
+
self.dropout = kwargs.get("dropout", None)
|
|
259
|
+
self.l2_reg = kwargs.get("l2_reg", 0.0)
|
|
260
|
+
self.vae_mode = kwargs.get("vae_mode", False)
|
|
261
|
+
self.vae_units = kwargs.get("vae_units", 2)
|
|
262
|
+
self.lora_mode = kwargs.get("lora_mode", False)
|
|
263
|
+
self.lora_rank = kwargs.get("lora_rank", 4)
|
|
264
|
+
|
|
265
|
+
def build_encoder_decoder(self, input_shape):
|
|
266
|
+
self.encoder = (
|
|
267
|
+
tf.keras.Sequential(
|
|
268
|
+
[
|
|
269
|
+
tf.keras.layers.Dense(
|
|
270
|
+
units=self.units,
|
|
271
|
+
activation=self.activation,
|
|
272
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
273
|
+
),
|
|
274
|
+
tf.keras.layers.Dense(
|
|
275
|
+
units=int(self.units / 2),
|
|
276
|
+
activation=self.activation,
|
|
277
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
278
|
+
),
|
|
279
|
+
],
|
|
280
|
+
name="encoder",
|
|
281
|
+
)
|
|
282
|
+
if not self.encoder
|
|
283
|
+
else self.encoder
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
self.decoder = (
|
|
287
|
+
tf.keras.Sequential(
|
|
288
|
+
[
|
|
289
|
+
tf.keras.layers.Dense(
|
|
290
|
+
units=self.units,
|
|
291
|
+
activation=self.activation,
|
|
292
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
293
|
+
),
|
|
294
|
+
tf.keras.layers.Dense(
|
|
295
|
+
units=self.input_shape_parm,
|
|
296
|
+
activation=self.activation,
|
|
297
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
298
|
+
),
|
|
299
|
+
],
|
|
300
|
+
name="decoder",
|
|
301
|
+
)
|
|
302
|
+
if not self.decoder
|
|
303
|
+
else self.decoder
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def build(self, input_shape):
|
|
307
|
+
if self.vae_mode:
|
|
308
|
+
inputs = tf.keras.Input(shape=self.input_shape_parm, name="encoder_input")
|
|
309
|
+
x = tf.keras.layers.Dense(
|
|
310
|
+
units=self.units,
|
|
311
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
312
|
+
kernel_initializer="he_normal",
|
|
313
|
+
)(inputs)
|
|
314
|
+
x = tf.keras.layers.BatchNormalization()(x)
|
|
315
|
+
x = tf.keras.layers.Activation(self.activation)(x)
|
|
316
|
+
x = tf.keras.layers.Dense(
|
|
317
|
+
units=int(self.units / 2),
|
|
318
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
319
|
+
kernel_initializer="he_normal",
|
|
320
|
+
name="encoder_hidden",
|
|
321
|
+
)(x)
|
|
322
|
+
x = tf.keras.layers.BatchNormalization()(x)
|
|
323
|
+
x = tf.keras.layers.Activation(self.activation)(x)
|
|
324
|
+
|
|
325
|
+
mean = tf.keras.layers.Dense(2, name="mean")(x)
|
|
326
|
+
log_var = tf.keras.layers.Dense(2, name="log_var")(x)
|
|
327
|
+
log_var = tf.keras.layers.Lambda(lambda x: x + 1e-7)(log_var)
|
|
328
|
+
|
|
329
|
+
self.encoder = (
|
|
330
|
+
tf.keras.Model(inputs, [mean, log_var], name="vae_encoder")
|
|
331
|
+
if not self.encoder
|
|
332
|
+
else self.encoder
|
|
333
|
+
)
|
|
334
|
+
self.decoder = (
|
|
335
|
+
tf.keras.Sequential(
|
|
336
|
+
[
|
|
337
|
+
tf.keras.layers.Dense(
|
|
338
|
+
units=self.units,
|
|
339
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
340
|
+
),
|
|
341
|
+
tf.keras.layers.BatchNormalization(),
|
|
342
|
+
tf.keras.layers.Activation(self.activation),
|
|
343
|
+
tf.keras.layers.Dense(
|
|
344
|
+
units=self.input_shape_parm,
|
|
345
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
346
|
+
),
|
|
347
|
+
tf.keras.layers.BatchNormalization(),
|
|
348
|
+
tf.keras.layers.Activation(self.activation),
|
|
349
|
+
],
|
|
350
|
+
name="vae_decoder",
|
|
351
|
+
)
|
|
352
|
+
if not self.decoder
|
|
353
|
+
else self.decoder
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
else:
|
|
357
|
+
self.build_encoder_decoder(input_shape)
|
|
358
|
+
|
|
359
|
+
self.classifier = tf.keras.Sequential()
|
|
360
|
+
if self.num_layers > 1 and not self.lora_mode:
|
|
361
|
+
for _ in range(self.num_layers - 1):
|
|
362
|
+
self.classifier.add(
|
|
363
|
+
tf.keras.layers.Dense(
|
|
364
|
+
units=self.units,
|
|
365
|
+
activation=self.activation,
|
|
366
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
if self.dropout:
|
|
370
|
+
self.classifier.add(tf.keras.layers.Dropout(self.dropout))
|
|
371
|
+
|
|
372
|
+
elif self.lora_mode:
|
|
373
|
+
for _ in range(self.num_layers - 1):
|
|
374
|
+
self.classifier.add(
|
|
375
|
+
LoRALayer(units=self.units, rank=self.lora_rank, name=f"LoRA_{_}")
|
|
376
|
+
)
|
|
377
|
+
self.classifier.add(tf.keras.layers.Activation(self.activation))
|
|
378
|
+
if self.dropout:
|
|
379
|
+
self.classifier.add(tf.keras.layers.Dropout(self.dropout))
|
|
380
|
+
|
|
381
|
+
self.classifier.add(
|
|
382
|
+
tf.keras.layers.Dense(
|
|
383
|
+
units=self.num_classes,
|
|
384
|
+
activation=self.classifier_activation,
|
|
385
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def train_encoder_decoder(
|
|
390
|
+
self, data, epochs, batch_size, validation_split=0.2, patience=10, **kwargs
|
|
391
|
+
):
|
|
392
|
+
"""
|
|
393
|
+
Trains the encoder and decoder on the input data.
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
data : `tf.data.Dataset`, `np.ndarray`
|
|
398
|
+
The input data.
|
|
399
|
+
epochs : `int`
|
|
400
|
+
The number of epochs to train for.
|
|
401
|
+
batch_size : `int`
|
|
402
|
+
The batch size to use.
|
|
403
|
+
validation_split : `float`
|
|
404
|
+
The proportion of the dataset to use for validation. Default is 0.2.
|
|
405
|
+
patience : `int`
|
|
406
|
+
The number of epochs to wait before early stopping. Default is 10.
|
|
407
|
+
|
|
408
|
+
Keyword Arguments
|
|
409
|
+
-----------------
|
|
410
|
+
Additional keyword arguments to pass to the model.
|
|
411
|
+
"""
|
|
412
|
+
verbose = kwargs.get("verbose", True)
|
|
413
|
+
optimizer = kwargs.get("optimizer", tf.keras.optimizers.Adam())
|
|
414
|
+
dummy_input = tf.convert_to_tensor(tf.random.normal([1, self.input_shape_parm]))
|
|
415
|
+
self.build(dummy_input.shape)
|
|
416
|
+
if not self.vae_mode:
|
|
417
|
+
dummy_output = self.encoder(dummy_input)
|
|
418
|
+
self.decoder(dummy_output)
|
|
419
|
+
else:
|
|
420
|
+
mean, log_var = self.encoder(dummy_input)
|
|
421
|
+
dummy_output = sampling(mean, log_var)
|
|
422
|
+
self.decoder(dummy_output)
|
|
423
|
+
|
|
424
|
+
if isinstance(data, np.ndarray):
|
|
425
|
+
data = tf.data.Dataset.from_tensor_slices(data).batch(batch_size)
|
|
426
|
+
data = data.map(lambda x: tf.cast(x, tf.float32))
|
|
427
|
+
|
|
428
|
+
early_stopping = EarlyStopping(patience=patience)
|
|
429
|
+
train_batches = data.take(int((1 - validation_split) * len(data)))
|
|
430
|
+
val_batches = data.skip(int((1 - validation_split) * len(data)))
|
|
431
|
+
for epoch in range(epochs):
|
|
432
|
+
for train_batch, val_batch in zip(train_batches, val_batches):
|
|
433
|
+
loss_train = train_step(
|
|
434
|
+
train_batch, self.encoder, self.decoder, optimizer, self.vae_mode
|
|
435
|
+
)
|
|
436
|
+
loss_val = cal_loss_step(
|
|
437
|
+
val_batch, self.encoder, self.decoder, self.vae_mode, False
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
early_stopping(loss_train)
|
|
441
|
+
|
|
442
|
+
if early_stopping.stop_training:
|
|
443
|
+
print(f"Early stopping triggered at epoch {epoch}.")
|
|
444
|
+
break
|
|
445
|
+
|
|
446
|
+
if epoch % 10 == 0 and verbose:
|
|
447
|
+
print(
|
|
448
|
+
f"Epoch {epoch}: Train Loss: {loss_train:.6f} Validation Loss: {loss_val:.6f}"
|
|
449
|
+
)
|
|
450
|
+
self.freeze_encoder_decoder()
|
|
451
|
+
|
|
452
|
+
def call(self, x):
|
|
453
|
+
if self.vae_mode:
|
|
454
|
+
mean, log_var = self.encoder(x)
|
|
455
|
+
encoded = sampling(mean, log_var)
|
|
456
|
+
else:
|
|
457
|
+
encoded = self.encoder(x)
|
|
458
|
+
decoded = self.decoder(encoded)
|
|
459
|
+
combined = tf.concat([decoded, encoded], axis=1)
|
|
460
|
+
classification = self.classifier(combined)
|
|
461
|
+
return classification
|
|
462
|
+
|
|
463
|
+
def freeze_encoder_decoder(self):
|
|
464
|
+
"""
|
|
465
|
+
Freezes the encoder and decoder layers to prevent them from being updated during training.
|
|
466
|
+
"""
|
|
467
|
+
for layer in self.encoder.layers:
|
|
468
|
+
layer.trainable = False
|
|
469
|
+
for layer in self.decoder.layers:
|
|
470
|
+
layer.trainable = False
|
|
471
|
+
|
|
472
|
+
def unfreeze_encoder_decoder(self):
|
|
473
|
+
"""
|
|
474
|
+
Unfreezes the encoder and decoder layers allowing them to be updated during training.
|
|
475
|
+
"""
|
|
476
|
+
for layer in self.encoder.layers:
|
|
477
|
+
layer.trainable = True
|
|
478
|
+
for layer in self.decoder.layers:
|
|
479
|
+
layer.trainable = True
|
|
480
|
+
|
|
481
|
+
def set_encoder_decoder(self, source_model):
|
|
482
|
+
"""
|
|
483
|
+
Sets the encoder and decoder layers from another AutoClassifier instance,
|
|
484
|
+
ensuring compatibility in dimensions. Only works if vae_mode is False.
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
source_model : AutoClassifier
|
|
489
|
+
The source model to copy the encoder and decoder layers from.
|
|
490
|
+
|
|
491
|
+
Raises
|
|
492
|
+
------
|
|
493
|
+
ValueError
|
|
494
|
+
If the input shape or units of the source model do not match.
|
|
495
|
+
"""
|
|
496
|
+
if not isinstance(source_model, AutoClassifier):
|
|
497
|
+
raise ValueError("Source model must be an instance of AutoClassifier.")
|
|
498
|
+
|
|
499
|
+
if self.input_shape_parm != source_model.input_shape_parm:
|
|
500
|
+
raise ValueError(
|
|
501
|
+
f"Incompatible input shape. Expected {self.input_shape_parm}, got {source_model.input_shape_parm}."
|
|
502
|
+
)
|
|
503
|
+
if self.units != source_model.units:
|
|
504
|
+
raise ValueError(
|
|
505
|
+
f"Incompatible number of units. Expected {self.units}, got {source_model.units}."
|
|
506
|
+
)
|
|
507
|
+
self.encoder, self.decoder = tf.keras.Sequential(), tf.keras.Sequential()
|
|
508
|
+
for i, layer in enumerate(source_model.encoder.layers):
|
|
509
|
+
if isinstance(layer, tf.keras.layers.Dense):
|
|
510
|
+
dummy_input = tf.convert_to_tensor(tf.random.normal([1, layer.input_shape[1]]))
|
|
511
|
+
dense_layer = tf.keras.layers.Dense(
|
|
512
|
+
units=layer.units,
|
|
513
|
+
activation=self.activation,
|
|
514
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
515
|
+
)
|
|
516
|
+
dense_layer.build(dummy_input.shape)
|
|
517
|
+
self.encoder.add(dense_layer)
|
|
518
|
+
self.encoder.layers[i].set_weights(layer.get_weights())
|
|
519
|
+
elif not isinstance(layer, InputLayer):
|
|
520
|
+
raise ValueError(f"Layer type {type(layer)} not supported for copying.")
|
|
521
|
+
|
|
522
|
+
for i, layer in enumerate(source_model.decoder.layers):
|
|
523
|
+
if isinstance(layer, tf.keras.layers.Dense):
|
|
524
|
+
dummy_input = tf.convert_to_tensor(tf.random.normal([1, layer.input_shape[1]]))
|
|
525
|
+
dense_layer = tf.keras.layers.Dense(
|
|
526
|
+
units=layer.units,
|
|
527
|
+
activation=self.activation,
|
|
528
|
+
kernel_regularizer=l2(self.l2_reg),
|
|
529
|
+
)
|
|
530
|
+
dense_layer.build(dummy_input.shape)
|
|
531
|
+
self.decoder.add(dense_layer)
|
|
532
|
+
self.decoder.layers[i].set_weights(layer.get_weights())
|
|
533
|
+
elif not isinstance(layer, InputLayer):
|
|
534
|
+
raise ValueError(f"Layer type {type(layer)} not supported for copying.")
|
|
535
|
+
|
|
536
|
+
def get_config(self):
|
|
537
|
+
config = {
|
|
538
|
+
"input_shape_parm": self.input_shape_parm,
|
|
539
|
+
"num_classes": self.num_classes,
|
|
540
|
+
"units": self.units,
|
|
541
|
+
"activation": self.activation,
|
|
542
|
+
"classifier_activation": self.classifier_activation,
|
|
543
|
+
"num_layers": self.num_layers,
|
|
544
|
+
"dropout": self.dropout,
|
|
545
|
+
"l2_reg": self.l2_reg,
|
|
546
|
+
"vae_mode": self.vae_mode,
|
|
547
|
+
"vae_units": self.vae_units,
|
|
548
|
+
"lora_mode": self.lora_mode,
|
|
549
|
+
"lora_rank": self.lora_rank,
|
|
550
|
+
}
|
|
551
|
+
base_config = super(AutoClassifier, self).get_config()
|
|
552
|
+
return dict(list(base_config.items()) + list(config.items()))
|
|
553
|
+
|
|
554
|
+
@classmethod
|
|
555
|
+
def from_config(cls, config):
|
|
556
|
+
return cls(
|
|
557
|
+
input_shape_parm=config["input_shape_parm"],
|
|
558
|
+
num_classes=config["num_classes"],
|
|
559
|
+
units=config["units"],
|
|
560
|
+
activation=config["activation"],
|
|
561
|
+
classifier_activation=config["classifier_activation"],
|
|
562
|
+
num_layers=config["num_layers"],
|
|
563
|
+
dropout=config["dropout"],
|
|
564
|
+
l2_reg=config["l2_reg"],
|
|
565
|
+
vae_mode=config["vae_mode"],
|
|
566
|
+
vae_units=config["vae_units"],
|
|
567
|
+
lora_mode=config["lora_mode"],
|
|
568
|
+
lora_rank=config["lora_rank"],
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def call_existing_code(
|
|
573
|
+
units: int,
|
|
574
|
+
activation: str,
|
|
575
|
+
threshold: float,
|
|
576
|
+
optimizer: str,
|
|
577
|
+
input_shape_parm: None | int = None,
|
|
578
|
+
num_classes: None | int = None,
|
|
579
|
+
num_layers: int = 1,
|
|
580
|
+
**kwargs,
|
|
581
|
+
) -> AutoClassifier:
|
|
582
|
+
"""
|
|
583
|
+
Calls an existing AutoClassifier instance.
|
|
584
|
+
|
|
585
|
+
Parameters
|
|
586
|
+
----------
|
|
587
|
+
units : `int`
|
|
588
|
+
The number of neurons in each hidden layer.
|
|
589
|
+
activation : `str`
|
|
590
|
+
The type of activation function to use for the neural network layers.
|
|
591
|
+
threshold : `float`
|
|
592
|
+
The threshold for the classifier.
|
|
593
|
+
optimizer : `str`
|
|
594
|
+
The type of optimizer to use for the neural network layers.
|
|
595
|
+
input_shape_parm : `None` | `int`
|
|
596
|
+
The shape of the input data.
|
|
597
|
+
num_classes : `int`
|
|
598
|
+
The number of classes in the dataset.
|
|
599
|
+
num_layers : `int`
|
|
600
|
+
The number of hidden layers in the classifier. Default is 1.
|
|
601
|
+
|
|
602
|
+
Keyword Arguments
|
|
603
|
+
-----------------
|
|
604
|
+
vae_mode : `bool`
|
|
605
|
+
Whether to use variational autoencoder mode. Default is False.
|
|
606
|
+
vae_units : `int`
|
|
607
|
+
The number of units in the variational autoencoder. Default is 2.
|
|
608
|
+
|
|
609
|
+
Returns
|
|
610
|
+
-------
|
|
611
|
+
`AutoClassifier`
|
|
612
|
+
The AutoClassifier instance.
|
|
613
|
+
"""
|
|
614
|
+
dropout = kwargs.get("dropout", None)
|
|
615
|
+
l2_reg = kwargs.get("l2_reg", 0.0)
|
|
616
|
+
vae_mode = kwargs.get("vae_mode", False)
|
|
617
|
+
vae_units = kwargs.get("vae_units", 2)
|
|
618
|
+
model = AutoClassifier(
|
|
619
|
+
input_shape_parm=input_shape_parm,
|
|
620
|
+
num_classes=num_classes,
|
|
621
|
+
units=units,
|
|
622
|
+
activation=activation,
|
|
623
|
+
num_layers=num_layers,
|
|
624
|
+
dropout=dropout,
|
|
625
|
+
l2_reg=l2_reg,
|
|
626
|
+
vae_mode=vae_mode,
|
|
627
|
+
vae_units=vae_units,
|
|
628
|
+
)
|
|
629
|
+
model.compile(
|
|
630
|
+
optimizer=optimizer,
|
|
631
|
+
loss=tf.keras.losses.CategoricalCrossentropy(),
|
|
632
|
+
metrics=[tf.keras.metrics.F1Score(threshold=threshold)],
|
|
633
|
+
)
|
|
634
|
+
return model
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def build_model(
|
|
638
|
+
hp, input_shape_parm: None | int, num_classes: None | int, **kwargs
|
|
639
|
+
) -> AutoClassifier:
|
|
640
|
+
"""Builds a neural network model using Keras Tuner's search algorithm.
|
|
641
|
+
|
|
642
|
+
Parameters
|
|
643
|
+
----------
|
|
644
|
+
hp : `keras_tuner.HyperParameters`
|
|
645
|
+
The hyperparameters to tune.
|
|
646
|
+
input_shape_parm : `None` | `int`
|
|
647
|
+
The shape of the input data.
|
|
648
|
+
num_classes : `int`
|
|
649
|
+
The number of classes in the dataset.
|
|
650
|
+
|
|
651
|
+
Keyword Arguments
|
|
652
|
+
-----------------
|
|
653
|
+
Additional keyword arguments to pass to the model.
|
|
654
|
+
|
|
655
|
+
hyperparameters : `dict`
|
|
656
|
+
The hyperparameters to set.
|
|
657
|
+
|
|
658
|
+
Returns
|
|
659
|
+
-------
|
|
660
|
+
`keras.Model`
|
|
661
|
+
The neural network model.
|
|
662
|
+
"""
|
|
663
|
+
hyperparameters = kwargs.get("hyperparameters", None)
|
|
664
|
+
hyperparameters_keys = hyperparameters.keys() if hyperparameters is not None else []
|
|
665
|
+
|
|
666
|
+
units = (
|
|
667
|
+
hp.Int(
|
|
668
|
+
"units",
|
|
669
|
+
min_value=int(input_shape_parm * 0.2),
|
|
670
|
+
max_value=int(input_shape_parm * 1.5),
|
|
671
|
+
step=2,
|
|
672
|
+
)
|
|
673
|
+
if "units" not in hyperparameters_keys
|
|
674
|
+
else (
|
|
675
|
+
hp.Choice("units", hyperparameters["units"])
|
|
676
|
+
if isinstance(hyperparameters["units"], list)
|
|
677
|
+
else hyperparameters["units"]
|
|
678
|
+
)
|
|
679
|
+
)
|
|
680
|
+
activation = (
|
|
681
|
+
hp.Choice("activation", ["sigmoid", "relu", "tanh", "selu", "softplus", "softsign"])
|
|
682
|
+
if "activation" not in hyperparameters_keys
|
|
683
|
+
else (
|
|
684
|
+
hp.Choice("activation", hyperparameters["activation"])
|
|
685
|
+
if isinstance(hyperparameters["activation"], list)
|
|
686
|
+
else hyperparameters["activation"]
|
|
687
|
+
)
|
|
688
|
+
)
|
|
689
|
+
optimizer = (
|
|
690
|
+
hp.Choice("optimizer", ["sgd", "adam", "adadelta", "rmsprop", "adamax", "adagrad"])
|
|
691
|
+
if "optimizer" not in hyperparameters_keys
|
|
692
|
+
else (
|
|
693
|
+
hp.Choice("optimizer", hyperparameters["optimizer"])
|
|
694
|
+
if isinstance(hyperparameters["optimizer"], list)
|
|
695
|
+
else hyperparameters["optimizer"]
|
|
696
|
+
)
|
|
697
|
+
)
|
|
698
|
+
threshold = (
|
|
699
|
+
hp.Float("threshold", min_value=0.1, max_value=0.9, sampling="log")
|
|
700
|
+
if "threshold" not in hyperparameters_keys
|
|
701
|
+
else (
|
|
702
|
+
hp.Choice("threshold", hyperparameters["threshold"])
|
|
703
|
+
if isinstance(hyperparameters["threshold"], list)
|
|
704
|
+
else hyperparameters["threshold"]
|
|
705
|
+
)
|
|
706
|
+
)
|
|
707
|
+
num_layers = (
|
|
708
|
+
hp.Int("num_layers", min_value=1, max_value=10, step=1)
|
|
709
|
+
if "num_layers" not in hyperparameters_keys
|
|
710
|
+
else (
|
|
711
|
+
hp.Choice("num_layers", hyperparameters["num_layers"])
|
|
712
|
+
if isinstance(hyperparameters["num_layers"], list)
|
|
713
|
+
else hyperparameters["num_layers"]
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
dropout = (
|
|
717
|
+
hp.Float("dropout", min_value=0.1, max_value=0.9, sampling="log")
|
|
718
|
+
if "dropout" not in hyperparameters_keys
|
|
719
|
+
else (
|
|
720
|
+
hp.Choice("dropout", hyperparameters["dropout"])
|
|
721
|
+
if isinstance(hyperparameters["dropout"], list)
|
|
722
|
+
else hyperparameters["dropout"]
|
|
723
|
+
)
|
|
724
|
+
)
|
|
725
|
+
l2_reg = (
|
|
726
|
+
hp.Float("l2_reg", min_value=1e-6, max_value=0.1, sampling="log")
|
|
727
|
+
if "l2_reg" not in hyperparameters_keys
|
|
728
|
+
else (
|
|
729
|
+
hp.Choice("l2_reg", hyperparameters["l2_reg"])
|
|
730
|
+
if isinstance(hyperparameters["l2_reg"], list)
|
|
731
|
+
else hyperparameters["l2_reg"]
|
|
732
|
+
)
|
|
733
|
+
)
|
|
734
|
+
vae_mode = (
|
|
735
|
+
hp.Choice("vae_mode", [True, False])
|
|
736
|
+
if "vae_mode" not in hyperparameters_keys
|
|
737
|
+
else hyperparameters["vae_mode"]
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
try:
|
|
741
|
+
vae_units = (
|
|
742
|
+
hp.Int("vae_units", min_value=2, max_value=10, step=1)
|
|
743
|
+
if ("vae_units" not in hyperparameters_keys) and vae_mode
|
|
744
|
+
else (
|
|
745
|
+
hp.Choice("vae_units", hyperparameters["vae_units"])
|
|
746
|
+
if isinstance(hyperparameters["vae_units"], list)
|
|
747
|
+
else hyperparameters["vae_units"]
|
|
748
|
+
)
|
|
749
|
+
)
|
|
750
|
+
except KeyError:
|
|
751
|
+
vae_units = None
|
|
752
|
+
|
|
753
|
+
model = call_existing_code(
|
|
754
|
+
units=units,
|
|
755
|
+
activation=activation,
|
|
756
|
+
threshold=threshold,
|
|
757
|
+
optimizer=optimizer,
|
|
758
|
+
input_shape_parm=input_shape_parm,
|
|
759
|
+
num_classes=num_classes,
|
|
760
|
+
num_layers=num_layers,
|
|
761
|
+
dropout=dropout,
|
|
762
|
+
l2_reg=l2_reg,
|
|
763
|
+
vae_mode=vae_mode,
|
|
764
|
+
vae_units=vae_units,
|
|
765
|
+
)
|
|
766
|
+
return model
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
@suppress_warnings
|
|
770
|
+
def setup_model(
|
|
771
|
+
data: pd.DataFrame,
|
|
772
|
+
target: str,
|
|
773
|
+
epochs: int,
|
|
774
|
+
train_size: float = 0.7,
|
|
775
|
+
seed=None,
|
|
776
|
+
train_mode: bool = True,
|
|
777
|
+
filepath: str = "./my_dir/best_model",
|
|
778
|
+
method: str = "Hyperband",
|
|
779
|
+
**kwargs,
|
|
780
|
+
) -> AutoClassifier:
|
|
781
|
+
"""Setup model for training and tuning.
|
|
782
|
+
|
|
783
|
+
Parameters
|
|
784
|
+
----------
|
|
785
|
+
data : `pd.DataFrame`
|
|
786
|
+
The dataset to train the model on.
|
|
787
|
+
target : `str`
|
|
788
|
+
The name of the target column.
|
|
789
|
+
epochs : `int`
|
|
790
|
+
The number of epochs to train the model for.
|
|
791
|
+
train_size : `float`
|
|
792
|
+
The proportion of the dataset to use for training.
|
|
793
|
+
seed : `None` | `int`
|
|
794
|
+
The random seed to use for reproducibility.
|
|
795
|
+
train_mode : `bool`
|
|
796
|
+
Whether to train the model or not.
|
|
797
|
+
filepath : `str`
|
|
798
|
+
The path to save the best model to.
|
|
799
|
+
method : `str`
|
|
800
|
+
The method to use for hyperparameter tuning. Options are "Hyperband" and "RandomSearch".
|
|
801
|
+
|
|
802
|
+
Keyword Arguments
|
|
803
|
+
-----------------
|
|
804
|
+
Additional keyword arguments to pass to the model.
|
|
805
|
+
|
|
806
|
+
max_trials : `int`
|
|
807
|
+
The maximum number of trials to perform.
|
|
808
|
+
directory : `str`
|
|
809
|
+
The directory to save the model to.
|
|
810
|
+
project_name : `str`
|
|
811
|
+
The name of the project.
|
|
812
|
+
objective : `str`
|
|
813
|
+
The objective to optimize.
|
|
814
|
+
verbose : `bool`
|
|
815
|
+
Whether to print verbose output.
|
|
816
|
+
hyperparameters : `dict`
|
|
817
|
+
The hyperparameters to set.
|
|
818
|
+
|
|
819
|
+
Returns
|
|
820
|
+
-------
|
|
821
|
+
model : `AutoClassifier`
|
|
822
|
+
The trained model.
|
|
823
|
+
"""
|
|
824
|
+
max_trials = kwargs.get("max_trials", 10)
|
|
825
|
+
directory = kwargs.get("directory", "./my_dir")
|
|
826
|
+
project_name = kwargs.get("project_name", "get_best")
|
|
827
|
+
objective = kwargs.get("objective", "val_loss")
|
|
828
|
+
verbose = kwargs.get("verbose", True)
|
|
829
|
+
hyperparameters = kwargs.get("hyperparameters", None)
|
|
830
|
+
|
|
831
|
+
X = data.drop(columns=target)
|
|
832
|
+
input_sample = X.sample(1)
|
|
833
|
+
y = data[target]
|
|
834
|
+
assert (
|
|
835
|
+
X.select_dtypes(include=["object"]).empty == True
|
|
836
|
+
), "Categorical variables within the DataFrame must be encoded, this is done by using the DataFrameEncoder from likelihood."
|
|
837
|
+
validation_split = 1.0 - train_size
|
|
838
|
+
|
|
839
|
+
if train_mode:
|
|
840
|
+
try:
|
|
841
|
+
if (not os.path.exists(directory)) and directory != "./":
|
|
842
|
+
os.makedirs(directory)
|
|
843
|
+
elif directory != "./":
|
|
844
|
+
print(f"Directory {directory} already exists, it will be deleted.")
|
|
845
|
+
rmtree(directory)
|
|
846
|
+
os.makedirs(directory)
|
|
847
|
+
except:
|
|
848
|
+
print("Warning: unable to create directory")
|
|
849
|
+
|
|
850
|
+
y_encoder = OneHotEncoder()
|
|
851
|
+
y = y_encoder.encode(y.to_list())
|
|
852
|
+
X = X.to_numpy()
|
|
853
|
+
input_sample.to_numpy()
|
|
854
|
+
X = np.asarray(X).astype(np.float32)
|
|
855
|
+
input_sample = np.asarray(input_sample).astype(np.float32)
|
|
856
|
+
y = np.asarray(y).astype(np.float32)
|
|
857
|
+
|
|
858
|
+
input_shape_parm = X.shape[1]
|
|
859
|
+
num_classes = y.shape[1]
|
|
860
|
+
global build_model
|
|
861
|
+
build_model = partial(
|
|
862
|
+
build_model,
|
|
863
|
+
input_shape_parm=input_shape_parm,
|
|
864
|
+
num_classes=num_classes,
|
|
865
|
+
hyperparameters=hyperparameters,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
if method == "Hyperband":
|
|
869
|
+
tuner = keras_tuner.Hyperband(
|
|
870
|
+
hypermodel=build_model,
|
|
871
|
+
objective=objective,
|
|
872
|
+
max_epochs=epochs,
|
|
873
|
+
factor=3,
|
|
874
|
+
directory=directory,
|
|
875
|
+
project_name=project_name,
|
|
876
|
+
seed=seed,
|
|
877
|
+
)
|
|
878
|
+
elif method == "RandomSearch":
|
|
879
|
+
tuner = keras_tuner.RandomSearch(
|
|
880
|
+
hypermodel=build_model,
|
|
881
|
+
objective=objective,
|
|
882
|
+
max_trials=max_trials,
|
|
883
|
+
directory=directory,
|
|
884
|
+
project_name=project_name,
|
|
885
|
+
seed=seed,
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
tuner.search(X, y, epochs=epochs, validation_split=validation_split, verbose=verbose)
|
|
889
|
+
models = tuner.get_best_models(num_models=2)
|
|
890
|
+
best_model = models[0]
|
|
891
|
+
best_model(input_sample)
|
|
892
|
+
|
|
893
|
+
best_model.save(filepath, save_format="tf")
|
|
894
|
+
|
|
895
|
+
if verbose:
|
|
896
|
+
tuner.results_summary()
|
|
897
|
+
else:
|
|
898
|
+
best_model = tf.keras.models.load_model(filepath)
|
|
899
|
+
best_hps = tuner.get_best_hyperparameters(1)[0].values
|
|
900
|
+
vae_mode = best_hps.get("vae_mode", hyperparameters.get("vae_mode", False))
|
|
901
|
+
best_hps["vae_units"] = None if not vae_mode else best_hps["vae_units"]
|
|
902
|
+
|
|
903
|
+
return best_model, pd.DataFrame(best_hps, index=["Value"]).dropna(axis=1)
|