From bf6988488453cdd0a60f47578b0cc660601d08f8 Mon Sep 17 00:00:00 2001 From: adimajo Date: Thu, 29 Oct 2026 16:00:44 +0100 Subject: [PATCH 1/4] Build the default NN optimizer with Keras 3 args RMSprop no longer accepts lr, decay or epsilon=None, so every NN fit raised before training started. Use learning_rate and Keras' default epsilon, which is what epsilon=None used to select. --- glmdisc/_fitNN.py | 2 +- tests/tests_NN/test_NN_fit.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/glmdisc/_fitNN.py b/glmdisc/_fitNN.py index e058359..e31d470 100644 --- a/glmdisc/_fitNN.py +++ b/glmdisc/_fitNN.py @@ -429,7 +429,7 @@ def _parse_kwargs(self, **kwargs): if "optimizer" in kwargs: optim = kwargs['optimizer'] else: - optim = tensorflow.keras.optimizers.RMSprop(lr=0.5, rho=0.9, epsilon=None, decay=0.0) + optim = tensorflow.keras.optimizers.RMSprop(learning_rate=0.5, rho=0.9, epsilon=1e-7) self.model_nn["callbacks"] = [ ReduceLROnPlateau( diff --git a/tests/tests_NN/test_NN_fit.py b/tests/tests_NN/test_NN_fit.py index 113154a..29d7e48 100644 --- a/tests/tests_NN/test_NN_fit.py +++ b/tests/tests_NN/test_NN_fit.py @@ -6,7 +6,7 @@ import random import glmdisc import sklearn as sk -from glmdisc._fitNN import _initialize_neural_net +from glmdisc._fitNN import _initialize_neural_net, _parse_kwargs import tensorflow from tensorflow.keras.optimizers import Adagrad from tensorflow.keras.callbacks import EarlyStopping @@ -228,3 +228,9 @@ def fail_fit(self, y): monkeypatch.setattr(sk.preprocessing.LabelEncoder, "fit", fail_fit) _initialize_neural_net(model) + + +def test_default_optimizer(): + model = glmdisc.Glmdisc(algorithm="NN") + optim = _parse_kwargs(model) + assert float(optim.learning_rate) == pytest.approx(0.5) From 5dc536f4e0f48d1683ea392ec9a8974dc8a7599c Mon Sep 17 00:00:00 2001 From: adimajo Date: Thu, 29 Oct 2026 16:01:35 +0100 Subject: [PATCH 2/4] Replace the removed backend.function in the NN tf.keras.backend.function does not exist in Keras 3. Compute the layer outputs by calling the layers directly, through a helper that keeps the list-of-outputs shape the callers index into. --- glmdisc/_fitNN.py | 33 +++++++++++++++++++++------------ tests/tests_NN/test_NN_fit.py | 19 ++++++++++++++++++- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/glmdisc/_fitNN.py b/glmdisc/_fitNN.py index e31d470..06acf6f 100644 --- a/glmdisc/_fitNN.py +++ b/glmdisc/_fitNN.py @@ -19,6 +19,19 @@ from tensorflow.keras.layers import concatenate +def _layer_output(layer, x): + """ + Applies a layer to an array of inputs. + + :param layer: a Keras layer + :param numpy.ndarray x: the inputs of the layer + :returns: a list holding the numpy array of outputs (the format returned by + :code:`tf.keras.backend.function`, which does not exist in Keras 3) + :rtype: list + """ + return [np.asarray(layer(tf.convert_to_tensor(x, dtype=tf.float32)))] + + def _plot_cont_soft_quant(self): """ Plots the soft quantization while fitting @@ -128,15 +141,13 @@ def on_epoch_end(self, batch, logs=None): for j in range(self.d_cont): self.best_weights.append(self.current_weights[j]) self.best_outputs.append( - tf.keras.backend.function([self.glmdisc_object.model_nn["liste_layers_quant"][j].input], - [self.glmdisc_object.model_nn["liste_layers_quant"][j].output])( - [self.glmdisc_object.predictors_cont[:, j, np.newaxis]])) + _layer_output(self.glmdisc_object.model_nn["liste_layers_quant"][j], + self.glmdisc_object.predictors_cont[:, j, np.newaxis])) for j in range(self.d_qual): self.best_weights.append(self.current_weights[j + self.d_cont]) self.best_outputs.append( - tf.keras.backend.function([self.glmdisc_object.model_nn["liste_layers_qual"][j].input], - [self.glmdisc_object.model_nn["liste_layers_qual"][j].output])( - [self.glmdisc_object.predictors_qual_dummy[j]])) + _layer_output(self.glmdisc_object.model_nn["liste_layers_qual"][j], + self.glmdisc_object.predictors_qual_dummy[j])) if self.plot_fit: _plot_cont_soft_quant(self) @@ -208,14 +219,12 @@ def _from_layers_to_proba_training(d_cont, d_qual, glmdisc_object): results = [None] * (d_cont + d_qual) for j in range(d_cont): - results[j] = tf.keras.backend.function([glmdisc_object.model_nn["liste_layers_quant"][j].input], - [glmdisc_object.model_nn["liste_layers_quant"][j].output])( - [glmdisc_object.predictors_cont[glmdisc_object.train_rows, j, np.newaxis]]) + results[j] = _layer_output(glmdisc_object.model_nn["liste_layers_quant"][j], + glmdisc_object.predictors_cont[glmdisc_object.train_rows, j, np.newaxis]) for j in range(d_qual): - results[j + d_cont] = tf.keras.backend.function([glmdisc_object.model_nn["liste_layers_qual"][j].input], - [glmdisc_object.model_nn["liste_layers_qual"][j].output])( - [glmdisc_object.predictors_qual_dummy[j][glmdisc_object.train_rows]]) + results[j + d_cont] = _layer_output(glmdisc_object.model_nn["liste_layers_qual"][j], + glmdisc_object.predictors_qual_dummy[j][glmdisc_object.train_rows]) return results diff --git a/tests/tests_NN/test_NN_fit.py b/tests/tests_NN/test_NN_fit.py index 29d7e48..59d702f 100644 --- a/tests/tests_NN/test_NN_fit.py +++ b/tests/tests_NN/test_NN_fit.py @@ -6,7 +6,7 @@ import random import glmdisc import sklearn as sk -from glmdisc._fitNN import _initialize_neural_net, _parse_kwargs +from glmdisc._fitNN import _initialize_neural_net, _layer_output, _parse_kwargs import tensorflow from tensorflow.keras.optimizers import Adagrad from tensorflow.keras.callbacks import EarlyStopping @@ -234,3 +234,20 @@ def test_default_optimizer(): model = glmdisc.Glmdisc(algorithm="NN") optim = _parse_kwargs(model) assert float(optim.learning_rate) == pytest.approx(0.5) + + +def test_layer_output(): + from tensorflow.keras.layers import Dense, Input + layer = Dense(3, activation='softmax') + layer(Input((1, ))) + kernel = np.array([[1., -1., 0.]]) + bias = np.array([0., .5, 0.]) + layer.set_weights([kernel, bias]) + x = np.array([[0.], [2.]]) + + output = _layer_output(layer, x) + + logits = x @ kernel + bias + expected = np.exp(logits) / np.exp(logits).sum(axis=1, keepdims=True) + assert isinstance(output, list) and len(output) == 1 + np.testing.assert_allclose(output[0], expected, rtol=1e-5) From 5a8498dd37c8c519107cc443f428105f4d70c169 Mon Sep 17 00:00:00 2001 From: adimajo Date: Thu, 29 Oct 2026 16:05:48 +0100 Subject: [PATCH 3/4] Use sparse_output in the NN OneHotEncoder scikit-learn 1.4 removed the sparse argument of OneHotEncoder, which made the evaluation of every NN epoch raise TypeError. --- glmdisc/_fitNN.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glmdisc/_fitNN.py b/glmdisc/_fitNN.py index 06acf6f..54ed3b7 100644 --- a/glmdisc/_fitNN.py +++ b/glmdisc/_fitNN.py @@ -294,7 +294,7 @@ def _evaluate_disc(history, d_cont: int, d_qual: int, glmdisc_object): results = [None] * (d_cont + d_qual) for j in range(d_cont + d_qual): results[j] = np.argmax(proba[j][0], axis=1) - encoders.append(sk.preprocessing.OneHotEncoder(categories='auto', sparse=False, handle_unknown="ignore")) + encoders.append(sk.preprocessing.OneHotEncoder(categories='auto', sparse_output=False, handle_unknown="ignore")) X_transformed = np.concatenate( (X_transformed, encoders[j].fit_transform( From 786e716c09c20683244b57c34f9a98a4e012e28c Mon Sep 17 00:00:00 2001 From: adimajo Date: Thu, 29 Oct 2026 16:06:07 +0100 Subject: [PATCH 4/4] Test the NN fit kwargs against public classes test_kwargs compared the optimizer and callback to classes living in tensorflow.python, a private path that Keras 3 removed. Compare with the classes the test imports from tensorflow.keras instead. --- tests/tests_NN/test_NN_fit.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/tests_NN/test_NN_fit.py b/tests/tests_NN/test_NN_fit.py index 59d702f..24ae5fd 100644 --- a/tests/tests_NN/test_NN_fit.py +++ b/tests/tests_NN/test_NN_fit.py @@ -7,7 +7,6 @@ import glmdisc import sklearn as sk from glmdisc._fitNN import _initialize_neural_net, _layer_output, _parse_kwargs -import tensorflow from tensorflow.keras.optimizers import Adagrad from tensorflow.keras.callbacks import EarlyStopping @@ -24,10 +23,8 @@ def test_kwargs(caplog): plot=True, optimizer=Adagrad(), callbacks=EarlyStopping()) - assert isinstance(model.model_nn['tensorflow_model'].optimizer, - tensorflow.python.keras.optimizer_v2.adagrad.Adagrad) - assert isinstance(model.model_nn['callbacks'][-1], - tensorflow.python.keras.callbacks.EarlyStopping) + assert isinstance(model.model_nn['tensorflow_model'].optimizer, Adagrad) + assert isinstance(model.model_nn['callbacks'][-1], EarlyStopping) with pytest.raises(ValueError): model.fit(predictors_cont=x, predictors_qual=None,