From 9390c3a52c948fabacae8f108249e6d01003e1ba Mon Sep 17 00:00:00 2001 From: Dobiasd Date: Wed, 29 Apr 2026 06:50:37 +0200 Subject: [PATCH 1/3] Fix loading of single-input/output models (closes #461) Keras serializes a single connection in input_layers/output_layers as a flat ["layer", node_idx, tensor_idx], but multiple connections as a list of such triples. The previous code used create_vector for both, which iterated each element of the flat form and tried to parse a string/int as an inbound node, failing with "invalid format for inbound node" - the exact error from the README example. Co-Authored-By: Claude Opus 4.7 (1M context) --- include/fdeep/import_model.hpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/include/fdeep/import_model.hpp b/include/fdeep/import_model.hpp index ae9c9550..c336ecb4 100644 --- a/include/fdeep/import_model.hpp +++ b/include/fdeep/import_model.hpp @@ -311,6 +311,16 @@ namespace internal { return node_connection(layer_id, node_idx, tensor_idx); } + inline std::vector create_node_connections_model_layer(const nlohmann::json& data) + { + assertion(data.is_array(), "input_layers/output_layers must be an array"); + // Keras serializes a single connection as a flat triple ["layer", node_idx, tensor_idx], + // and multiple connections as a list of such triples. + if (!data.empty() && !data.front().is_array()) + return { create_node_connection_model_layer(data) }; + return create_vector(create_node_connection_model_layer, data); + } + inline node_connection create_node_connection(const nlohmann::json& args) { assertion(json_obj_has_member(args["config"], "keras_history"), @@ -369,11 +379,9 @@ namespace internal { assertion(data["config"]["input_layers"].is_array(), "no input layers"); - const auto inputs = create_vector( - create_node_connection_model_layer, data["config"]["input_layers"]); + const auto inputs = create_node_connections_model_layer(data["config"]["input_layers"]); - const auto outputs = create_vector( - create_node_connection_model_layer, data["config"]["output_layers"]); + const auto outputs = create_node_connections_model_layer(data["config"]["output_layers"]); return std::make_shared(name, layers, inputs, outputs); } From 1219f3dda106d72f41c429a776b8a3ffdd4b02a3 Mon Sep 17 00:00:00 2001 From: Dobiasd Date: Wed, 29 Apr 2026 06:57:09 +0200 Subject: [PATCH 2/3] Add regression test for flat input_layers/output_layers format Newer Keras versions serialize a single input/output as a flat triple ["layer", node_idx, tensor_idx] instead of a list containing one such triple. The existing readme_example_main test exercises a single-input model, but only catches this when CI runs against a Keras version that already emits the flat form. Post-process the readme example JSON into flat form and load it from a new test so this code path is covered on any Keras version. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/CMakeLists.txt | 7 +++++ test/readme_example_flat_format_generate.py | 32 +++++++++++++++++++++ test/readme_example_flat_format_test.cpp | 23 +++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 test/readme_example_flat_format_generate.py create mode 100644 test/readme_example_flat_format_test.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1781cbaa..f078c167 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -58,6 +58,11 @@ add_custom_command ( OUTPUT readme_example_model.json COMMAND bash -c "${Python3_EXECUTABLE} ${FDEEP_TOP_DIR}/keras_export/convert_model.py readme_example_model.keras readme_example_model.json" WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/) +add_custom_command ( OUTPUT readme_example_flat_format_model.json + DEPENDS readme_example_model.json + COMMAND bash -c "${Python3_EXECUTABLE} ${FDEEP_TOP_DIR}/test/readme_example_flat_format_generate.py readme_example_model.json readme_example_flat_format_model.json" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/) + hunter_add_package(doctest) find_package(doctest CONFIG REQUIRED) @@ -75,6 +80,7 @@ _add_test(test_model_variable_test test_model_variable.json) _add_test(test_model_autoencoder_test test_model_autoencoder.json) _add_test(test_model_sequential_test test_model_sequential.json) _add_test(readme_example_main readme_example_model.json) +_add_test(readme_example_flat_format_test readme_example_flat_format_model.json) add_custom_target(unittest COMMAND test_model_exhaustive_test @@ -83,6 +89,7 @@ add_custom_target(unittest COMMAND test_model_autoencoder_test COMMAND test_model_sequential_test COMMAND readme_example_main + COMMAND readme_example_flat_format_test COMMENT "Running unittests\n\n" VERBATIM diff --git a/test/readme_example_flat_format_generate.py b/test/readme_example_flat_format_generate.py new file mode 100644 index 00000000..e9c1a86d --- /dev/null +++ b/test/readme_example_flat_format_generate.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Force the single-input fdeep model JSON into Keras' newer flat +input_layers/output_layers format so we can regression-test loading it +on any Keras version. See https://github.com/Dobiasd/frugally-deep/issues/461. +""" + +import json +import sys + + +def flatten(node_connections): + if ( + isinstance(node_connections, list) + and len(node_connections) == 1 + and isinstance(node_connections[0], list) + ): + return node_connections[0] + return node_connections + + +def main(src, dst): + with open(src, 'r') as f: + data = json.load(f) + config = data['architecture']['config'] + config['input_layers'] = flatten(config['input_layers']) + config['output_layers'] = flatten(config['output_layers']) + with open(dst, 'w') as f: + json.dump(data, f) + + +if __name__ == '__main__': + main(sys.argv[1], sys.argv[2]) diff --git a/test/readme_example_flat_format_test.cpp b/test/readme_example_flat_format_test.cpp new file mode 100644 index 00000000..1697f482 --- /dev/null +++ b/test/readme_example_flat_format_test.cpp @@ -0,0 +1,23 @@ +// Copyright 2016, Tobias Hermann. +// https://github.com/Dobiasd/frugally-deep +// Distributed under the MIT License. +// (See accompanying LICENSE file or at +// https://opensource.org/licenses/MIT) + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" +#include + +// Regression test for https://github.com/Dobiasd/frugally-deep/issues/461. +// Newer Keras versions serialize a single input/output as a flat triple +// ["layer", node_idx, tensor_idx] instead of a list containing one such +// triple. The test JSON is post-processed into that flat form so this +// path is exercised regardless of the Keras version used by CI. +TEST_CASE("readme_example_flat_format_test, main") +{ + const auto model = fdeep::load_model("../readme_example_flat_format_model.json"); + const auto result = model.predict( + { fdeep::tensor(fdeep::tensor_shape(static_cast(4)), + fdeep::float_vec { 1, 2, 3, 4 }) }); + std::cout << fdeep::show_tensors(result) << std::endl; +} From 8d2b5697eb91b3087fd8f6ac92cedc584de57426 Mon Sep 17 00:00:00 2001 From: Dobiasd Date: Wed, 29 Apr 2026 07:05:25 +0200 Subject: [PATCH 3/3] Remove synthetic flat-format regression test Drop the JSON post-processing helper and its test. Coverage will instead come from a follow-up CI bump to a Keras version that natively emits the flat input_layers/output_layers form, at which point the existing readme_example_main test exercises this code path against real Keras output. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/CMakeLists.txt | 7 ----- test/readme_example_flat_format_generate.py | 32 --------------------- test/readme_example_flat_format_test.cpp | 23 --------------- 3 files changed, 62 deletions(-) delete mode 100644 test/readme_example_flat_format_generate.py delete mode 100644 test/readme_example_flat_format_test.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f078c167..1781cbaa 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -58,11 +58,6 @@ add_custom_command ( OUTPUT readme_example_model.json COMMAND bash -c "${Python3_EXECUTABLE} ${FDEEP_TOP_DIR}/keras_export/convert_model.py readme_example_model.keras readme_example_model.json" WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/) -add_custom_command ( OUTPUT readme_example_flat_format_model.json - DEPENDS readme_example_model.json - COMMAND bash -c "${Python3_EXECUTABLE} ${FDEEP_TOP_DIR}/test/readme_example_flat_format_generate.py readme_example_model.json readme_example_flat_format_model.json" - WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/) - hunter_add_package(doctest) find_package(doctest CONFIG REQUIRED) @@ -80,7 +75,6 @@ _add_test(test_model_variable_test test_model_variable.json) _add_test(test_model_autoencoder_test test_model_autoencoder.json) _add_test(test_model_sequential_test test_model_sequential.json) _add_test(readme_example_main readme_example_model.json) -_add_test(readme_example_flat_format_test readme_example_flat_format_model.json) add_custom_target(unittest COMMAND test_model_exhaustive_test @@ -89,7 +83,6 @@ add_custom_target(unittest COMMAND test_model_autoencoder_test COMMAND test_model_sequential_test COMMAND readme_example_main - COMMAND readme_example_flat_format_test COMMENT "Running unittests\n\n" VERBATIM diff --git a/test/readme_example_flat_format_generate.py b/test/readme_example_flat_format_generate.py deleted file mode 100644 index e9c1a86d..00000000 --- a/test/readme_example_flat_format_generate.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -"""Force the single-input fdeep model JSON into Keras' newer flat -input_layers/output_layers format so we can regression-test loading it -on any Keras version. See https://github.com/Dobiasd/frugally-deep/issues/461. -""" - -import json -import sys - - -def flatten(node_connections): - if ( - isinstance(node_connections, list) - and len(node_connections) == 1 - and isinstance(node_connections[0], list) - ): - return node_connections[0] - return node_connections - - -def main(src, dst): - with open(src, 'r') as f: - data = json.load(f) - config = data['architecture']['config'] - config['input_layers'] = flatten(config['input_layers']) - config['output_layers'] = flatten(config['output_layers']) - with open(dst, 'w') as f: - json.dump(data, f) - - -if __name__ == '__main__': - main(sys.argv[1], sys.argv[2]) diff --git a/test/readme_example_flat_format_test.cpp b/test/readme_example_flat_format_test.cpp deleted file mode 100644 index 1697f482..00000000 --- a/test/readme_example_flat_format_test.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2016, Tobias Hermann. -// https://github.com/Dobiasd/frugally-deep -// Distributed under the MIT License. -// (See accompanying LICENSE file or at -// https://opensource.org/licenses/MIT) - -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN -#include "doctest/doctest.h" -#include - -// Regression test for https://github.com/Dobiasd/frugally-deep/issues/461. -// Newer Keras versions serialize a single input/output as a flat triple -// ["layer", node_idx, tensor_idx] instead of a list containing one such -// triple. The test JSON is post-processed into that flat form so this -// path is exercised regardless of the Keras version used by CI. -TEST_CASE("readme_example_flat_format_test, main") -{ - const auto model = fdeep::load_model("../readme_example_flat_format_model.json"); - const auto result = model.predict( - { fdeep::tensor(fdeep::tensor_shape(static_cast(4)), - fdeep::float_vec { 1, 2, 3, 4 }) }); - std::cout << fdeep::show_tensors(result) << std::endl; -}