Skip to content

Commit 2ce984e

Browse files
Dobiasdclaude
andcommitted
Split test_model_recurrent into recurrent + extended
The catch-all get_test_model_recurrent had drifted to include layers that aren't recurrent at all (DepthwiseConv1D, EinsumDense, Adaptive*Pooling, GroupedQueryAttention, RMS/GroupNormalization, Discretization, Random* image augmentation). Extract those into a new get_test_model_extended driving a parallel test_model_extended_test target so each test name reflects what it covers. test_model_recurrent now only contains genuinely-recurrent layers: LSTM, GRU, SimpleRNN, Bidirectional, Masking, RNN with cells, ConvLSTM 1D/2D/3D. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ea915b3 commit 2ce984e

3 files changed

Lines changed: 112 additions & 52 deletions

File tree

keras_export/generate_test_models.py

Lines changed: 80 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -826,13 +826,12 @@ def get_test_model_sequential() -> Model:
826826

827827

828828
def get_test_model_recurrent() -> Model:
829-
"""Returns a test model exercising LSTM, GRU, SimpleRNN, Bidirectional, and other newer layers."""
829+
"""Returns a test model exercising recurrent layers (LSTM, GRU, SimpleRNN,
830+
Bidirectional, RNN with cells, ConvLSTM, Masking)."""
830831
seq_len = 5
831832
n_features = 4
832833

833834
inputs = Input(shape=(seq_len, n_features))
834-
inputs_2d = Input(shape=(7, 8, 4))
835-
inputs_3d = Input(shape=(5, 6, 7, 4))
836835

837836
lstm_seq = LSTM(6, return_sequences=True)(inputs)
838837
lstm_last = LSTM(7)(lstm_seq)
@@ -857,6 +856,79 @@ def get_test_model_recurrent() -> Model:
857856
bidi_gru_ave = Bidirectional(GRU(4), merge_mode='ave')(inputs)
858857
bidi_rnn_concat = Bidirectional(SimpleRNN(3, return_sequences=True))(inputs)
859858

859+
masked = Masking(mask_value=0.0)(inputs)
860+
861+
rnn_lstm = RNN(LSTMCell(4))(inputs)
862+
rnn_gru = RNN(GRUCell(5), return_sequences=True)(inputs)
863+
rnn_simple = RNN(SimpleRNNCell(3))(inputs)
864+
rnn_stacked = RNN(StackedRNNCells([LSTMCell(6), GRUCell(4), SimpleRNNCell(3)]))(inputs)
865+
rnn_stacked_seq = RNN(StackedRNNCells([LSTMCell(5), GRUCell(3)]),
866+
return_sequences=True)(inputs)
867+
868+
inputs_clstm1d = Input(shape=(4, 6, 3))
869+
inputs_clstm2d = Input(shape=(3, 5, 5, 3))
870+
inputs_clstm3d = Input(shape=(2, 3, 4, 4, 3))
871+
clstm1d = ConvLSTM1D(filters=2, kernel_size=3, padding='same')(inputs_clstm1d)
872+
clstm1d_valid = ConvLSTM1D(filters=2, kernel_size=2, padding='valid',
873+
return_sequences=True)(inputs_clstm1d)
874+
clstm1d_dilated = ConvLSTM1D(filters=2, kernel_size=2, padding='same',
875+
dilation_rate=2)(inputs_clstm1d)
876+
clstm2d = ConvLSTM2D(filters=2, kernel_size=(3, 3), padding='same',
877+
return_sequences=True)(inputs_clstm2d)
878+
clstm2d_valid_no_bias = ConvLSTM2D(filters=3, kernel_size=(2, 2), padding='valid',
879+
use_bias=False, activation='relu')(inputs_clstm2d)
880+
# Two ConvLSTM2Ds chained: validates state handoff between successive temporal layers.
881+
clstm2d_chained = ConvLSTM2D(filters=2, kernel_size=(2, 2), padding='same')(
882+
ConvLSTM2D(filters=3, kernel_size=(3, 3), padding='same',
883+
return_sequences=True)(inputs_clstm2d))
884+
clstm3d = ConvLSTM3D(filters=2, kernel_size=(2, 2, 2), padding='same')(inputs_clstm3d)
885+
clstm3d_valid = ConvLSTM3D(filters=2, kernel_size=(1, 2, 2), padding='valid',
886+
return_sequences=True)(inputs_clstm3d)
887+
888+
outputs = [
889+
lstm_last, lstm_no_bias, lstm_relu, lstm_no_unit_forget,
890+
lstm_state_out, lstm_state_h, lstm_state_c,
891+
gru_last, gru_no_bias,
892+
rnn_last, rnn_relu_no_bias,
893+
bidi_lstm_last_concat,
894+
bidi_lstm_sum,
895+
bidi_gru_mul,
896+
bidi_gru_ave,
897+
bidi_rnn_concat,
898+
masked,
899+
rnn_lstm, rnn_gru, rnn_simple,
900+
rnn_stacked, rnn_stacked_seq,
901+
clstm1d, clstm1d_valid, clstm1d_dilated,
902+
clstm2d, clstm2d_valid_no_bias, clstm2d_chained,
903+
clstm3d, clstm3d_valid,
904+
]
905+
906+
model = Model(inputs=[inputs, inputs_clstm1d, inputs_clstm2d, inputs_clstm3d],
907+
outputs=outputs, name='test_model_recurrent')
908+
model.compile(loss='mse', optimizer='adam')
909+
910+
training_data_size = 2
911+
data_in = generate_input_data(training_data_size,
912+
[(seq_len, n_features), (4, 6, 3), (3, 5, 5, 3), (2, 3, 4, 4, 3)])
913+
initial_data_out = model.predict(data_in)
914+
data_out = generate_output_data(training_data_size, initial_data_out)
915+
model.fit(data_in, data_out, epochs=1)
916+
return model
917+
918+
919+
def get_test_model_extended() -> Model:
920+
"""Returns a test model exercising recently-added non-recurrent layers
921+
(DepthwiseConv1D, EinsumDense, RMSNormalization, GroupNormalization,
922+
AdaptiveAvg/MaxPooling, GroupedQueryAttention, Discretization, and a
923+
handful of training-only Random* augmentation passthroughs)."""
924+
seq_len = 5
925+
n_features = 4
926+
927+
inputs = Input(shape=(seq_len, n_features))
928+
inputs_2d = Input(shape=(7, 8, 4))
929+
inputs_3d = Input(shape=(5, 6, 7, 4))
930+
inputs_kv = Input(shape=(8, n_features))
931+
860932
dwc1d_same = DepthwiseConv1D(kernel_size=3, padding='same')(inputs)
861933
dwc1d_valid = DepthwiseConv1D(kernel_size=2, padding='valid', strides=2)(inputs)
862934
dwc1d_no_bias = DepthwiseConv1D(kernel_size=3, padding='same', use_bias=False)(inputs)
@@ -893,53 +965,15 @@ def get_test_model_recurrent() -> Model:
893965
gqa_eq_heads = GroupQueryAttention(head_dim=4, num_query_heads=4, num_key_value_heads=4)(
894966
inputs, inputs, inputs)
895967
# distinct sequence lengths for query vs key/value.
896-
inputs_kv = Input(shape=(8, n_features))
897968
gqa_kv_diff = GroupQueryAttention(head_dim=3, num_query_heads=4, num_key_value_heads=2)(
898969
inputs, inputs_kv, inputs_kv)
899970

900971
discretized = Discretization(bin_boundaries=[-0.5, 0.0, 0.5, 1.0])(inputs)
901-
masked = Masking(mask_value=0.0)(inputs)
902972
rand_brightness = RandomBrightness(factor=0.1)(inputs_2d)
903973
rand_flip = RandomFlip(mode='horizontal')(inputs_2d)
904974
rand_crop = RandomCrop(height=7, width=8)(inputs_2d)
905975

906-
rnn_lstm = RNN(LSTMCell(4))(inputs)
907-
rnn_gru = RNN(GRUCell(5), return_sequences=True)(inputs)
908-
rnn_simple = RNN(SimpleRNNCell(3))(inputs)
909-
rnn_stacked = RNN(StackedRNNCells([LSTMCell(6), GRUCell(4), SimpleRNNCell(3)]))(inputs)
910-
rnn_stacked_seq = RNN(StackedRNNCells([LSTMCell(5), GRUCell(3)]),
911-
return_sequences=True)(inputs)
912-
913-
inputs_clstm1d = Input(shape=(4, 6, 3))
914-
inputs_clstm2d = Input(shape=(3, 5, 5, 3))
915-
inputs_clstm3d = Input(shape=(2, 3, 4, 4, 3))
916-
clstm1d = ConvLSTM1D(filters=2, kernel_size=3, padding='same')(inputs_clstm1d)
917-
clstm1d_valid = ConvLSTM1D(filters=2, kernel_size=2, padding='valid',
918-
return_sequences=True)(inputs_clstm1d)
919-
clstm1d_dilated = ConvLSTM1D(filters=2, kernel_size=2, padding='same',
920-
dilation_rate=2)(inputs_clstm1d)
921-
clstm2d = ConvLSTM2D(filters=2, kernel_size=(3, 3), padding='same',
922-
return_sequences=True)(inputs_clstm2d)
923-
clstm2d_valid_no_bias = ConvLSTM2D(filters=3, kernel_size=(2, 2), padding='valid',
924-
use_bias=False, activation='relu')(inputs_clstm2d)
925-
# Two ConvLSTM2Ds chained: validates state handoff between successive temporal layers.
926-
clstm2d_chained = ConvLSTM2D(filters=2, kernel_size=(2, 2), padding='same')(
927-
ConvLSTM2D(filters=3, kernel_size=(3, 3), padding='same',
928-
return_sequences=True)(inputs_clstm2d))
929-
clstm3d = ConvLSTM3D(filters=2, kernel_size=(2, 2, 2), padding='same')(inputs_clstm3d)
930-
clstm3d_valid = ConvLSTM3D(filters=2, kernel_size=(1, 2, 2), padding='valid',
931-
return_sequences=True)(inputs_clstm3d)
932-
933976
outputs = [
934-
lstm_last, lstm_no_bias, lstm_relu, lstm_no_unit_forget,
935-
lstm_state_out, lstm_state_h, lstm_state_c,
936-
gru_last, gru_no_bias,
937-
rnn_last, rnn_relu_no_bias,
938-
bidi_lstm_last_concat,
939-
bidi_lstm_sum,
940-
bidi_gru_mul,
941-
bidi_gru_ave,
942-
bidi_rnn_concat,
943977
dwc1d_same, dwc1d_valid, dwc1d_no_bias,
944978
rms, rms_eps,
945979
gn, gn_no_scale, gn_no_center, gn_one, gn_per_channel,
@@ -948,24 +982,17 @@ def get_test_model_recurrent() -> Model:
948982
ap2d_a, ap2d_m, ap2d_uneven,
949983
ap3d_a, ap3d_m, ap3d_uneven,
950984
gqa, gqa_gated, gqa_eq_heads, gqa_kv_diff,
951-
rnn_lstm, rnn_gru, rnn_simple,
952-
rnn_stacked, rnn_stacked_seq,
953-
clstm1d, clstm1d_valid, clstm1d_dilated,
954-
clstm2d, clstm2d_valid_no_bias, clstm2d_chained,
955-
clstm3d, clstm3d_valid,
956-
discretized, masked,
985+
discretized,
957986
rand_brightness, rand_flip, rand_crop,
958987
]
959988

960-
model = Model(inputs=[inputs, inputs_2d, inputs_3d,
961-
inputs_clstm1d, inputs_clstm2d, inputs_clstm3d, inputs_kv],
962-
outputs=outputs, name='test_model_recurrent')
989+
model = Model(inputs=[inputs, inputs_2d, inputs_3d, inputs_kv],
990+
outputs=outputs, name='test_model_extended')
963991
model.compile(loss='mse', optimizer='adam')
964992

965993
training_data_size = 2
966994
data_in = generate_input_data(training_data_size,
967-
[(seq_len, n_features), (7, 8, 4), (5, 6, 7, 4),
968-
(4, 6, 3), (3, 5, 5, 3), (2, 3, 4, 4, 3), (8, n_features)])
995+
[(seq_len, n_features), (7, 8, 4), (5, 6, 7, 4), (8, n_features)])
969996
initial_data_out = model.predict(data_in)
970997
data_out = generate_output_data(training_data_size, initial_data_out)
971998
model.fit(data_in, data_out, epochs=1)
@@ -988,6 +1015,7 @@ def main() -> None:
9881015
'autoencoder': get_test_model_autoencoder,
9891016
'sequential': get_test_model_sequential,
9901017
'recurrent': get_test_model_recurrent,
1018+
'extended': get_test_model_extended,
9911019
}
9921020

9931021
if not model_name in get_model_functions:

test/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ add_custom_command ( OUTPUT test_model_recurrent.keras
2828
COMMAND bash -c "${Python3_EXECUTABLE} ${FDEEP_TOP_DIR}/keras_export/generate_test_models.py recurrent test_model_recurrent.keras"
2929
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/)
3030

31+
add_custom_command ( OUTPUT test_model_extended.keras
32+
COMMAND bash -c "${Python3_EXECUTABLE} ${FDEEP_TOP_DIR}/keras_export/generate_test_models.py extended test_model_extended.keras"
33+
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/)
34+
3135
add_custom_command ( OUTPUT readme_example_model.keras
3236
COMMAND bash -c "${Python3_EXECUTABLE} ${FDEEP_TOP_DIR}/test/readme_example_generate.py"
3337
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/)
@@ -62,6 +66,11 @@ add_custom_command ( OUTPUT test_model_recurrent.json
6266
COMMAND bash -c "${Python3_EXECUTABLE} ${FDEEP_TOP_DIR}/keras_export/convert_model.py test_model_recurrent.keras test_model_recurrent.json"
6367
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/)
6468

69+
add_custom_command ( OUTPUT test_model_extended.json
70+
DEPENDS test_model_extended.keras
71+
COMMAND bash -c "${Python3_EXECUTABLE} ${FDEEP_TOP_DIR}/keras_export/convert_model.py test_model_extended.keras test_model_extended.json"
72+
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/)
73+
6574
add_custom_command ( OUTPUT readme_example_model.json
6675
DEPENDS readme_example_model.keras
6776
COMMAND bash -c "${Python3_EXECUTABLE} ${FDEEP_TOP_DIR}/keras_export/convert_model.py readme_example_model.keras readme_example_model.json"
@@ -84,6 +93,7 @@ _add_test(test_model_variable_test test_model_variable.json)
8493
_add_test(test_model_autoencoder_test test_model_autoencoder.json)
8594
_add_test(test_model_sequential_test test_model_sequential.json)
8695
_add_test(test_model_recurrent_test test_model_recurrent.json)
96+
_add_test(test_model_extended_test test_model_extended.json)
8797
_add_test(readme_example_main readme_example_model.json)
8898

8999
add_custom_target(unittest
@@ -93,6 +103,7 @@ add_custom_target(unittest
93103
COMMAND test_model_autoencoder_test
94104
COMMAND test_model_sequential_test
95105
COMMAND test_model_recurrent_test
106+
COMMAND test_model_extended_test
96107
COMMAND readme_example_main
97108

98109
COMMENT "Running unittests\n\n"

test/test_model_extended_test.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2016, Tobias Hermann.
2+
// https://github.com/Dobiasd/frugally-deep
3+
// Distributed under the MIT License.
4+
// (See accompanying LICENSE file or at
5+
// https://opensource.org/licenses/MIT)
6+
7+
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
8+
#include "doctest/doctest.h"
9+
#define FDEEP_FLOAT_TYPE double
10+
#include <fdeep/fdeep.hpp>
11+
12+
TEST_CASE("test_model_extended_test, load_model")
13+
{
14+
const auto model = fdeep::load_model("../test_model_extended.json",
15+
true, fdeep::cout_logger, static_cast<fdeep::float_type>(0.00001));
16+
const auto multi_inputs = fplus::generate<std::vector<fdeep::tensors>>(
17+
[&]() -> fdeep::tensors { return model.generate_dummy_inputs(); },
18+
10);
19+
model.predict_multi(multi_inputs, false);
20+
model.predict_multi(multi_inputs, true);
21+
}

0 commit comments

Comments
 (0)