From 915cec52e603e58ca7e966ef5c8c4305595dc315 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Mon, 13 Jun 2022 14:18:59 -0400 Subject: [PATCH 01/20] Add URL to Keras CNN MNIST model; make URL constants more concise --- example/mnist_from_keras.f90 | 10 +++++----- src/nf/nf_datasets.f90 | 10 ++++++++-- test/test_dense_network_from_keras.f90 | 4 ++-- test/test_io_hdf5.f90 | 4 ++-- test/test_keras_read_model.f90 | 21 ++++++++++++++++----- 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/example/mnist_from_keras.f90 b/example/mnist_from_keras.f90 index b5a67e41..703b51a2 100644 --- a/example/mnist_from_keras.f90 +++ b/example/mnist_from_keras.f90 @@ -4,7 +4,7 @@ program mnist_from_keras ! from an HDF5 file and running an inferrence on the testing dataset. use nf, only: network, label_digits, load_mnist - use nf_datasets, only: download_and_unpack, keras_model_dense_mnist_url + use nf_datasets, only: download_and_unpack, keras_dense_mnist_url implicit none @@ -12,11 +12,11 @@ program mnist_from_keras real, allocatable :: training_images(:,:), training_labels(:) real, allocatable :: validation_images(:,:), validation_labels(:) real, allocatable :: testing_images(:,:), testing_labels(:) - character(*), parameter :: test_data_path = 'keras_dense_mnist.h5' + character(*), parameter :: keras_dense_path = 'keras_dense_mnist.h5' logical :: file_exists - inquire(file=test_data_path, exist=file_exists) - if (.not. file_exists) call download_and_unpack(keras_model_dense_mnist_url) + inquire(file=keras_dense_path, exist=file_exists) + if (.not. file_exists) call download_and_unpack(keras_dense_mnist_url) call load_mnist(training_images, training_labels, & validation_images, validation_labels, & @@ -25,7 +25,7 @@ program mnist_from_keras print '("Loading a pre-trained MNIST model from Keras")' print '(60("="))' - net = network(test_data_path) + net = network(keras_dense_path) call net % print_info() diff --git a/src/nf/nf_datasets.f90 b/src/nf/nf_datasets.f90 index 3a19b1cf..2df35cb9 100644 --- a/src/nf/nf_datasets.f90 +++ b/src/nf/nf_datasets.f90 @@ -8,13 +8,19 @@ module nf_datasets private - public :: download_and_unpack, keras_model_dense_mnist_url, mnist_url + public :: & + download_and_unpack, & + keras_cnn_mnist_url, & + keras_dense_mnist_url, & + mnist_url character(*), parameter :: keras_snippets_baseurl = & 'https://github.com/neural-fortran/keras-snippets/files' character(*), parameter :: neural_fortran_baseurl = & 'https://github.com/modern-fortran/neural-fortran/files' - character(*), parameter :: keras_model_dense_mnist_url = & + character(*), parameter :: keras_cnn_mnist_url = & + keras_snippets_baseurl // '/8892585/keras_cnn_mnist.tar.gz' + character(*), parameter :: keras_dense_mnist_url = & keras_snippets_baseurl // '/8788739/keras_dense_mnist.tar.gz' character(*), parameter :: mnist_url = & neural_fortran_baseurl // '/8498876/mnist.tar.gz' diff --git a/test/test_dense_network_from_keras.f90 b/test/test_dense_network_from_keras.f90 index 435efcec..ebd0b501 100644 --- a/test/test_dense_network_from_keras.f90 +++ b/test/test_dense_network_from_keras.f90 @@ -2,7 +2,7 @@ program test_dense_network_from_keras use iso_fortran_env, only: stderr => error_unit use nf, only: network - use nf_datasets, only: download_and_unpack, keras_model_dense_mnist_url + use nf_datasets, only: download_and_unpack, keras_dense_mnist_url implicit none @@ -12,7 +12,7 @@ program test_dense_network_from_keras logical :: ok = .true. inquire(file=test_data_path, exist=file_exists) - if (.not. file_exists) call download_and_unpack(keras_model_dense_mnist_url) + if (.not. file_exists) call download_and_unpack(keras_dense_mnist_url) net = network(test_data_path) diff --git a/test/test_io_hdf5.f90 b/test/test_io_hdf5.f90 index dc8940af..b662c0af 100644 --- a/test/test_io_hdf5.f90 +++ b/test/test_io_hdf5.f90 @@ -1,7 +1,7 @@ program test_io_hdf5 use iso_fortran_env, only: stderr => error_unit - use nf_datasets, only: download_and_unpack, keras_model_dense_mnist_url + use nf_datasets, only: download_and_unpack, keras_dense_mnist_url use nf_io_hdf5, only: hdf5_attribute_string, get_hdf5_dataset implicit none @@ -14,7 +14,7 @@ program test_io_hdf5 logical :: ok = .true. inquire(file=test_data_path, exist=file_exists) - if (.not. file_exists) call download_and_unpack(keras_model_dense_mnist_url) + if (.not. file_exists) call download_and_unpack(keras_dense_mnist_url) attr = hdf5_attribute_string(test_data_path, '.', 'backend') diff --git a/test/test_keras_read_model.f90 b/test/test_keras_read_model.f90 index 87d81ac4..8ca6b5bb 100644 --- a/test/test_keras_read_model.f90 +++ b/test/test_keras_read_model.f90 @@ -1,14 +1,16 @@ program test_keras_read_model use iso_fortran_env, only: stderr => error_unit - use nf_datasets, only: download_and_unpack, keras_model_dense_mnist_url + use nf_datasets, only: download_and_unpack, keras_dense_mnist_url, & + keras_cnn_mnist_url use nf_keras, only: get_keras_h5_layers, keras_layer use nf, only: layer, network, dense, input implicit none character(:), allocatable :: model_config_string - character(*), parameter :: test_data_path = 'keras_dense_mnist.h5' + character(*), parameter :: keras_dense_path = 'keras_dense_mnist.h5' + character(*), parameter :: keras_cnn_path = 'keras_cnn_mnist.h5' type(keras_layer), allocatable :: keras_layers(:) @@ -19,10 +21,12 @@ program test_keras_read_model logical :: file_exists logical :: ok = .true. - inquire(file=test_data_path, exist=file_exists) - if (.not. file_exists) call download_and_unpack(keras_model_dense_mnist_url) + ! First test the dense model - keras_layers = get_keras_h5_layers(test_data_path) + inquire(file=keras_dense_path, exist=file_exists) + if (.not. file_exists) call download_and_unpack(keras_dense_mnist_url) + + keras_layers = get_keras_h5_layers(keras_dense_path) if (size(keras_layers) /= 3) then ok = .false. @@ -51,6 +55,13 @@ program test_keras_read_model 'Keras second and third layers should be dense.. failed' end if + ! Now testing for the CNN model + + inquire(file=keras_cnn_path, exist=file_exists) + if (.not. file_exists) call download_and_unpack(keras_cnn_mnist_url) + + keras_layers = get_keras_h5_layers(keras_cnn_path) + if (ok) then print '(a)', 'test_keras_read_model: All tests passed.' else From 336eabba1b21a74af17bfa03521397a342c3ad8e Mon Sep 17 00:00:00 2001 From: milancurcic Date: Mon, 13 Jun 2022 14:37:54 -0400 Subject: [PATCH 02/20] Generalize input shape in Keras reader; allow reading Conv2D metadata --- src/nf/nf_keras.f90 | 2 ++ src/nf/nf_keras_submodule.f90 | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/nf/nf_keras.f90 b/src/nf/nf_keras.f90 index 5d5ec2d9..1cb51860 100644 --- a/src/nf/nf_keras.f90 +++ b/src/nf/nf_keras.f90 @@ -15,6 +15,8 @@ module nf_keras character(:), allocatable :: name character(:), allocatable :: activation integer, allocatable :: num_elements(:) + integer :: filters ! Conv2D + integer, allocatable :: kernel_size(:) ! Conv2D end type keras_layer interface diff --git a/src/nf/nf_keras_submodule.f90 b/src/nf/nf_keras_submodule.f90 index b97c12da..ec610e51 100644 --- a/src/nf/nf_keras_submodule.f90 +++ b/src/nf/nf_keras_submodule.f90 @@ -51,13 +51,21 @@ module function get_keras_h5_layers(filename) result(res) case('InputLayer') call json % get(layer_config_json, 'batch_input_shape', tmp_array) - res(n) % num_elements = [tmp_array(2)] + res(n) % num_elements = tmp_array(2:) ! skip the 1st (batch) dim case('Dense') call json % get(layer_config_json, 'units', num_elements, found) res(n) % num_elements = [num_elements] call json % get(layer_config_json, 'activation', res(n) % activation) + case('Conv2D') + call json % get(layer_config_json, & + 'filters', res(n) % filters, found) + call json % get(layer_config_json, & + 'kernel_size', res(n) % kernel_size, found) + call json % get(layer_config_json, & + 'activation', res(n) % activation) + case default error stop 'This Keras layer is not supported' From 878e973df25cc49942693d1574f16daf2d60121a Mon Sep 17 00:00:00 2001 From: Milan Curcic Date: Tue, 14 Jun 2022 10:53:44 -0400 Subject: [PATCH 03/20] Rename keras_layer attribute for consistency with Keras notation --- src/nf/nf_keras.f90 | 2 +- src/nf/nf_keras_submodule.f90 | 8 ++++---- src/nf/nf_network_submodule.f90 | 8 ++++---- test/test_keras_read_model.f90 | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/nf/nf_keras.f90 b/src/nf/nf_keras.f90 index 1cb51860..8de8828c 100644 --- a/src/nf/nf_keras.f90 +++ b/src/nf/nf_keras.f90 @@ -14,7 +14,7 @@ module nf_keras character(:), allocatable :: class character(:), allocatable :: name character(:), allocatable :: activation - integer, allocatable :: num_elements(:) + integer, allocatable :: units(:) integer :: filters ! Conv2D integer, allocatable :: kernel_size(:) ! Conv2D end type keras_layer diff --git a/src/nf/nf_keras_submodule.f90 b/src/nf/nf_keras_submodule.f90 index ec610e51..5a68b90b 100644 --- a/src/nf/nf_keras_submodule.f90 +++ b/src/nf/nf_keras_submodule.f90 @@ -18,7 +18,7 @@ module function get_keras_h5_layers(filename) result(res) model_config_json, layers_json, layer_json, layer_config_json real, allocatable :: tmp_array(:) - integer :: n, num_layers, num_elements + integer :: n, num_layers, units logical :: found model_config_string = hdf5_attribute_string(filename, '.', 'model_config') @@ -51,11 +51,11 @@ module function get_keras_h5_layers(filename) result(res) case('InputLayer') call json % get(layer_config_json, 'batch_input_shape', tmp_array) - res(n) % num_elements = tmp_array(2:) ! skip the 1st (batch) dim + res(n) % units = tmp_array(2:) ! skip the 1st (batch) dim case('Dense') - call json % get(layer_config_json, 'units', num_elements, found) - res(n) % num_elements = [num_elements] + call json % get(layer_config_json, 'units', units, found) + res(n) % units = [units] call json % get(layer_config_json, 'activation', res(n) % activation) case('Conv2D') diff --git a/src/nf/nf_network_submodule.f90 b/src/nf/nf_network_submodule.f90 index 6333e5eb..acc3d41f 100644 --- a/src/nf/nf_network_submodule.f90 +++ b/src/nf/nf_network_submodule.f90 @@ -69,17 +69,17 @@ module function network_from_keras(filename) result(res) select case(keras_layers(n) % class) case('InputLayer') - if (size(keras_layers(n) % num_elements) == 1) then + if (size(keras_layers(n) % units) == 1) then ! input1d - layers(n) = input(keras_layers(n) % num_elements(1)) + layers(n) = input(keras_layers(n) % units(1)) else ! input3d - layers(n) = input(keras_layers(n) % num_elements) + layers(n) = input(keras_layers(n) % units) end if case('Dense') layers(n) = dense( & - keras_layers(n) % num_elements(1), & + keras_layers(n) % units(1), & keras_layers(n) % activation & ) diff --git a/test/test_keras_read_model.f90 b/test/test_keras_read_model.f90 index 8ca6b5bb..294d911a 100644 --- a/test/test_keras_read_model.f90 +++ b/test/test_keras_read_model.f90 @@ -38,7 +38,7 @@ program test_keras_read_model write(stderr, '(a)') 'Keras first layer should be InputLayer.. failed' end if - if (.not. all(keras_layers(1) % num_elements == [784])) then + if (.not. all(keras_layers(1) % units == [784])) then ok = .false. write(stderr, '(a)') 'Keras first layer should have 784 elements.. failed' end if From 44cd48d0d3ede4207deeba7eb6f606c2939d9054 Mon Sep 17 00:00:00 2001 From: Milan Curcic Date: Tue, 14 Jun 2022 11:34:37 -0400 Subject: [PATCH 04/20] Add MaxPooling2D and Flatten layers to the Keras reader --- src/nf/nf_keras.f90 | 20 +++++++++++--- src/nf/nf_keras_submodule.f90 | 14 ++++++++-- test/test_keras_read_model.f90 | 48 +++++++++++++++++++++++++++++----- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/src/nf/nf_keras.f90 b/src/nf/nf_keras.f90 index 8de8828c..04c9385b 100644 --- a/src/nf/nf_keras.f90 +++ b/src/nf/nf_keras.f90 @@ -9,14 +9,26 @@ module nf_keras public :: get_keras_h5_layers, keras_layer type :: keras_layer - !! Intermediate container to convey the Keras layer information - !! to neural-fortran layer constructors. + + !! Intermediate container to convey the Keras layer + !! information to neural-fortran layer constructors. + + ! General metadata that applies to any (or most) layers character(:), allocatable :: class character(:), allocatable :: name character(:), allocatable :: activation + + ! Dense integer, allocatable :: units(:) - integer :: filters ! Conv2D - integer, allocatable :: kernel_size(:) ! Conv2D + + ! Conv2D + integer :: filters + integer, allocatable :: kernel_size(:) + + ! MaxPooling2D + integer, allocatable :: pool_size(:) + integer, allocatable :: strides(:) + end type keras_layer interface diff --git a/src/nf/nf_keras_submodule.f90 b/src/nf/nf_keras_submodule.f90 index 5a68b90b..d75344ff 100644 --- a/src/nf/nf_keras_submodule.f90 +++ b/src/nf/nf_keras_submodule.f90 @@ -57,15 +57,25 @@ module function get_keras_h5_layers(filename) result(res) call json % get(layer_config_json, 'units', units, found) res(n) % units = [units] call json % get(layer_config_json, 'activation', res(n) % activation) - + + case('Flatten') + ! Nothing to read here; merely a placeholder. + continue + case('Conv2D') call json % get(layer_config_json, & 'filters', res(n) % filters, found) call json % get(layer_config_json, & - 'kernel_size', res(n) % kernel_size, found) + 'kernel_size', res(n) % kernel_size, found) call json % get(layer_config_json, & 'activation', res(n) % activation) + case('MaxPooling2D') + call json % get(layer_config_json, & + 'pool_size', res(n) % pool_size, found) + call json % get(layer_config_json, & + 'strides', res(n) % strides, found) + case default error stop 'This Keras layer is not supported' diff --git a/test/test_keras_read_model.f90 b/test/test_keras_read_model.f90 index 294d911a..2a3b244f 100644 --- a/test/test_keras_read_model.f90 +++ b/test/test_keras_read_model.f90 @@ -4,20 +4,14 @@ program test_keras_read_model use nf_datasets, only: download_and_unpack, keras_dense_mnist_url, & keras_cnn_mnist_url use nf_keras, only: get_keras_h5_layers, keras_layer - use nf, only: layer, network, dense, input implicit none - character(:), allocatable :: model_config_string character(*), parameter :: keras_dense_path = 'keras_dense_mnist.h5' character(*), parameter :: keras_cnn_path = 'keras_cnn_mnist.h5' type(keras_layer), allocatable :: keras_layers(:) - type(layer), allocatable :: layers(:) - type(network) :: net - - integer :: n logical :: file_exists logical :: ok = .true. @@ -62,6 +56,48 @@ program test_keras_read_model keras_layers = get_keras_h5_layers(keras_cnn_path) + if (.not. all(keras_layers(1) % units == [28, 28, 1])) then + ok = .false. + write(stderr, '(a)') & + 'Keras CNN input layer shape is expected.. failed' + end if + + if (.not. keras_layers(2) % class == 'Conv2D') then + ok = .false. + write(stderr, '(a)') & + 'Keras CNN second layer is Conv2D.. failed' + end if + + if (.not. keras_layers(2) % filters == 8) then + ok = .false. + write(stderr, '(a)') & + 'Keras CNN second layer number of filters is expected.. failed' + end if + + if (.not. all(keras_layers(2) % kernel_size == [3, 3])) then + ok = .false. + write(stderr, '(a)') & + 'Keras CNN second layer kernel_size is expected.. failed' + end if + + if (.not. keras_layers(3) % class == 'MaxPooling2D') then + ok = .false. + write(stderr, '(a)') & + 'Keras CNN third layer is MaxPooling2D.. failed' + end if + + if (.not. all(keras_layers(3) % pool_size == [2, 2])) then + ok = .false. + write(stderr, '(a)') & + 'Keras CNN second layer pool_size is expected.. failed' + end if + + if (.not. all(keras_layers(3) % strides == [2, 2])) then + ok = .false. + write(stderr, '(a)') & + 'Keras CNN second layer strides are expected.. failed' + end if + if (ok) then print '(a)', 'test_keras_read_model: All tests passed.' else From df3d8e2b6f6cd75d2ef942d9e31b2d9d3f2a184c Mon Sep 17 00:00:00 2001 From: Milan Curcic Date: Tue, 14 Jun 2022 12:04:15 -0400 Subject: [PATCH 05/20] Handle C->Fortran order with Keras layer dims --- fpm.toml | 1 + src/nf/nf_keras_submodule.f90 | 8 +++++++- test/test_keras_read_model.f90 | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/fpm.toml b/fpm.toml index 90ee5d9d..4733e936 100644 --- a/fpm.toml +++ b/fpm.toml @@ -10,5 +10,6 @@ external-modules = "hdf5" link = ["hdf5", "hdf5_fortran"] [dependencies] +functional = { git = "https://github.com/wavebitscientific/functional-fortran" } h5fortran = { git = "https://github.com/geospace-code/h5fortran" } json-fortran = { git = "https://github.com/jacobwilliams/json-fortran" } diff --git a/src/nf/nf_keras_submodule.f90 b/src/nf/nf_keras_submodule.f90 index d75344ff..161b38be 100644 --- a/src/nf/nf_keras_submodule.f90 +++ b/src/nf/nf_keras_submodule.f90 @@ -1,5 +1,6 @@ submodule(nf_keras) nf_keras_submodule + use functional, only: reverse use json_module, only: json_core, json_value use nf_io_hdf5, only: hdf5_attribute_string @@ -51,7 +52,7 @@ module function get_keras_h5_layers(filename) result(res) case('InputLayer') call json % get(layer_config_json, 'batch_input_shape', tmp_array) - res(n) % units = tmp_array(2:) ! skip the 1st (batch) dim + res(n) % units = reverse(tmp_array(2:)) ! skip the 1st (batch) dim case('Dense') call json % get(layer_config_json, 'units', units, found) @@ -69,12 +70,17 @@ module function get_keras_h5_layers(filename) result(res) 'kernel_size', res(n) % kernel_size, found) call json % get(layer_config_json, & 'activation', res(n) % activation) + ! Reverse to account for C -> Fortran order + res(n) % kernel_size = reverse(res(n) % kernel_size) case('MaxPooling2D') call json % get(layer_config_json, & 'pool_size', res(n) % pool_size, found) call json % get(layer_config_json, & 'strides', res(n) % strides, found) + ! Reverse to account for C -> Fortran order + res(n) % pool_size = reverse(res(n) % pool_size) + res(n) % strides = reverse(res(n) % strides) case default error stop 'This Keras layer is not supported' diff --git a/test/test_keras_read_model.f90 b/test/test_keras_read_model.f90 index 2a3b244f..ba021a5a 100644 --- a/test/test_keras_read_model.f90 +++ b/test/test_keras_read_model.f90 @@ -56,7 +56,7 @@ program test_keras_read_model keras_layers = get_keras_h5_layers(keras_cnn_path) - if (.not. all(keras_layers(1) % units == [28, 28, 1])) then + if (.not. all(keras_layers(1) % units == [1, 28, 28])) then ok = .false. write(stderr, '(a)') & 'Keras CNN input layer shape is expected.. failed' From d943b0c18705f76a2f424d219d0e7000bdaa8f9d Mon Sep 17 00:00:00 2001 From: Milan Curcic Date: Tue, 14 Jun 2022 12:06:43 -0400 Subject: [PATCH 06/20] Start the CNN from Keras test suite --- test/CMakeLists.txt | 15 ++++++++++++++- test/test_cnn_from_keras.f90 | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 test/test_cnn_from_keras.f90 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f30c6d79..5538d198 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,17 @@ -foreach(execid input1d_layer input3d_layer dense_layer conv2d_layer maxpool2d_layer flatten_layer dense_network dense_network_from_keras conv2d_network io_hdf5 keras_read_model) +foreach(execid + input1d_layer + input3d_layer + dense_layer + conv2d_layer + maxpool2d_layer + flatten_layer + dense_network + io_hdf5 + keras_read_model + dense_network_from_keras + cnn_from_keras + conv2d_network + ) add_executable(test_${execid} test_${execid}.f90) target_link_libraries(test_${execid} PRIVATE neural h5fortran::h5fortran jsonfortran::jsonfortran ${LIBS}) diff --git a/test/test_cnn_from_keras.f90 b/test/test_cnn_from_keras.f90 new file mode 100644 index 00000000..f8b44826 --- /dev/null +++ b/test/test_cnn_from_keras.f90 @@ -0,0 +1,19 @@ +program test_cnn_from_keras + + use iso_fortran_env, only: stderr => error_unit + use nf, only: network + use nf_datasets, only: download_and_unpack, keras_cnn_mnist_url + + implicit none + + type(network) :: net + character(*), parameter :: test_data_path = 'keras_cnn_mnist.h5' + logical :: file_exists + logical :: ok = .true. + + inquire(file=test_data_path, exist=file_exists) + if (.not. file_exists) call download_and_unpack(keras_cnn_mnist_url) + + net = network(test_data_path) + +end program test_cnn_from_keras From ff2321de709fc56f113d5040a527fc60045c2b19 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Wed, 15 Jun 2022 13:46:11 -0400 Subject: [PATCH 07/20] Enable Conv2D, Flatten, and MaxPooling2D in the network constructor from Keras --- src/nf/nf_network_submodule.f90 | 41 +++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/nf/nf_network_submodule.f90 b/src/nf/nf_network_submodule.f90 index acc3d41f..0350c581 100644 --- a/src/nf/nf_network_submodule.f90 +++ b/src/nf/nf_network_submodule.f90 @@ -7,7 +7,7 @@ use nf_io_hdf5, only: get_hdf5_dataset use nf_keras, only: get_keras_h5_layers, keras_layer use nf_layer, only: layer - use nf_layer_constructors, only: dense, input + use nf_layer_constructors, only: conv2d, dense, flatten, input, maxpool2d use nf_loss, only: quadratic_derivative use nf_optimizers, only: sgd use nf_parallel, only: tile_indices @@ -68,6 +68,27 @@ module function network_from_keras(filename) result(res) select case(keras_layers(n) % class) + case('Conv2D') + + if (keras_layers(n) % kernel_size(1) & + /= keras_layers(n) % kernel_size(2)) & + error stop 'Non-square kernel in conv2d layer not supported.' + + layers(n) = conv2d( & + keras_layers(n) % filters, & + !FIXME add support for non-square kernel + keras_layers(n) % kernel_size(1) & + ) + + case('Dense') + layers(n) = dense( & + keras_layers(n) % units(1), & + keras_layers(n) % activation & + ) + + case('Flatten') + layers(n) = flatten() + case('InputLayer') if (size(keras_layers(n) % units) == 1) then ! input1d @@ -77,10 +98,20 @@ module function network_from_keras(filename) result(res) layers(n) = input(keras_layers(n) % units) end if - case('Dense') - layers(n) = dense( & - keras_layers(n) % units(1), & - keras_layers(n) % activation & + case('MaxPooling2D') + + if (keras_layers(n) % pool_size(1) & + /= keras_layers(n) % pool_size(2)) & + error stop 'Non-square pool in maxpool2d layer not supported.' + + if (keras_layers(n) % strides(1) & + /= keras_layers(n) % strides(2)) & + error stop 'Unequal strides in maxpool2d layer are not supported.' + + layers(n) = maxpool2d( & + !FIXME add support for non-square pool and stride + keras_layers(n) % pool_size(1), & + keras_layers(n) % strides(1) & ) case default From 87ebc11a358b4cc0b6f124b97c7265662c5d8cf6 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Wed, 15 Jun 2022 13:49:04 -0400 Subject: [PATCH 08/20] Remove redundant if-block --- src/nf/nf_network_submodule.f90 | 45 +++++++++++++++------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/nf/nf_network_submodule.f90 b/src/nf/nf_network_submodule.f90 index 0350c581..971e4999 100644 --- a/src/nf/nf_network_submodule.f90 +++ b/src/nf/nf_network_submodule.f90 @@ -129,36 +129,33 @@ module function network_from_keras(filename) result(res) layer_name = keras_layers(n) % name - if (keras_layers(n) % class == 'Dense') then - select type(this_layer => res % layers(n) % p) + select type(this_layer => res % layers(n) % p) - type is(dense_layer) - - ! Read biases from file - object_name = '/model_weights/' // layer_name // '/' & - // layer_name // '/bias:0' - call get_hdf5_dataset(filename, object_name, this_layer % biases) + type is(dense_layer) - ! Read weights from file - object_name = '/model_weights/' // layer_name // '/' & - // layer_name // '/kernel:0' - call get_hdf5_dataset(filename, object_name, this_layer % weights) + ! Read biases from file + object_name = '/model_weights/' // layer_name // '/' & + // layer_name // '/bias:0' + call get_hdf5_dataset(filename, object_name, this_layer % biases) - ! TODO Multidimensional arrays are stored in HDF5 in C-order. - ! TODO Here we transpose the array to get to the Fortran order. - ! TODO There may be a way to do this without re-allocating. - ! TODO It probably doesn't matter much since we do this once. - ! TODO Figure it out later. - this_layer % weights = transpose(this_layer % weights) + ! Read weights from file + object_name = '/model_weights/' // layer_name // '/' & + // layer_name // '/kernel:0' + call get_hdf5_dataset(filename, object_name, this_layer % weights) - class default - error stop 'Internal error in network_from_keras(); ' & - // 'mismatch in layer types between the Keras and ' & - // 'neural-fortran model layers.' + ! TODO Multidimensional arrays are stored in HDF5 in C-order. + ! TODO Here we transpose the array to get to the Fortran order. + ! TODO There may be a way to do this without re-allocating. + ! TODO It probably doesn't matter much since we do this once. + ! TODO Figure it out later. + this_layer % weights = transpose(this_layer % weights) - end select + class default + error stop 'Internal error in network_from_keras(); ' & + // 'mismatch in layer types between the Keras and ' & + // 'neural-fortran model layers.' - end if + end select end do From eb70debbb058fc11cabba556a1dfd5910db7bcf5 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Thu, 16 Jun 2022 15:38:56 -0400 Subject: [PATCH 09/20] Read kernel and bias data for Conv2D layer from Keras; tranpose values in the HDF5 reader --- src/nf/io/nf_io_hdf5.f90 | 10 +++++++ src/nf/io/nf_io_hdf5_submodule.f90 | 47 ++++++++++++++++++++++++++++++ src/nf/nf_network_submodule.f90 | 26 +++++++++++++---- test/test_cnn_from_keras.f90 | 47 ++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 6 deletions(-) diff --git a/src/nf/io/nf_io_hdf5.f90 b/src/nf/io/nf_io_hdf5.f90 index aa6e20f1..d1e2fb98 100644 --- a/src/nf/io/nf_io_hdf5.f90 +++ b/src/nf/io/nf_io_hdf5.f90 @@ -45,6 +45,16 @@ module subroutine get_hdf5_dataset_real32_2d(filename, object_name, values) !! Array to store the dataset values into end subroutine get_hdf5_dataset_real32_2d + module subroutine get_hdf5_dataset_real32_4d(filename, object_name, values) + !! Read a 4-d real32 array from an HDF5 dataset. + character(*), intent(in) :: filename + !! HDF5 file name + character(*), intent(in) :: object_name + !! Object (dataset) name + real(real32), allocatable, intent(in out) :: values(:,:,:,:) + !! Array to store the dataset values into + end subroutine get_hdf5_dataset_real32_4d + end interface get_hdf5_dataset end module nf_io_hdf5 diff --git a/src/nf/io/nf_io_hdf5_submodule.f90 b/src/nf/io/nf_io_hdf5_submodule.f90 index 263f8384..9be98b78 100644 --- a/src/nf/io/nf_io_hdf5_submodule.f90 +++ b/src/nf/io/nf_io_hdf5_submodule.f90 @@ -99,6 +99,53 @@ module subroutine get_hdf5_dataset_real32_2d(filename, object_name, values) call f % read(object_name, values) call f % close() + ! Transpose the array to get from C to Fortran order + values = transpose(values) + end subroutine get_hdf5_dataset_real32_2d + + module subroutine get_hdf5_dataset_real32_4d(filename, object_name, values) + + character(*), intent(in) :: filename + character(*), intent(in) :: object_name + real(real32), allocatable, intent(in out) :: values(:,:,:,:) + + type(hdf5_file) :: f + integer(int64), allocatable :: dims(:) + + call f % open(filename, 'r') + call f % shape(object_name, dims) + + ! If values is already allocated, re-allocate only if incorrect shape + if (allocated(values)) then + if (.not. all(shape(values) == dims)) then + deallocate(values) + allocate(values(dims(1), dims(2), dims(3), dims(4))) + end if + else + allocate(values(dims(1), dims(2), dims(3), dims(4))) + end if + + call f % read(object_name, values) + call f % close() + + ! Transpose the array to get from C to Fortran order + values = reverse_dim_order(values) + + end subroutine get_hdf5_dataset_real32_4d + + + pure function reverse_dim_order(x) result(res) + real, intent(in) :: x(:,:,:,:) + real, allocatable :: res(:,:,:,:) + integer :: dims(4) + integer :: i, j, k, l + dims = shape(x) + allocate(res(dims(4), dims(3), dims(2), dims(1))) + do concurrent(i = 1:dims(1), j = 1:dims(2), k = 1:dims(3), l = 1:dims(4)) + res(l,k,j,i) = x(i,j,k,l) + end do + end function reverse_dim_order + end submodule nf_io_hdf5_submodule diff --git a/src/nf/nf_network_submodule.f90 b/src/nf/nf_network_submodule.f90 index 971e4999..77d96c4e 100644 --- a/src/nf/nf_network_submodule.f90 +++ b/src/nf/nf_network_submodule.f90 @@ -1,9 +1,11 @@ submodule(nf_network) nf_network_submodule + use nf_conv2d_layer, only: conv2d_layer use nf_dense_layer, only: dense_layer use nf_flatten_layer, only: flatten_layer use nf_input1d_layer, only: input1d_layer use nf_input3d_layer, only: input3d_layer + use nf_maxpool2d_layer, only: maxpool2d_layer use nf_io_hdf5, only: get_hdf5_dataset use nf_keras, only: get_keras_h5_layers, keras_layer use nf_layer, only: layer @@ -131,6 +133,17 @@ module function network_from_keras(filename) result(res) select type(this_layer => res % layers(n) % p) + type is(conv2d_layer) + ! Read biases from file + object_name = '/model_weights/' // layer_name // '/' & + // layer_name // '/bias:0' + call get_hdf5_dataset(filename, object_name, this_layer % biases) + + ! Read weights from file + object_name = '/model_weights/' // layer_name // '/' & + // layer_name // '/kernel:0' + call get_hdf5_dataset(filename, object_name, this_layer % kernel) + type is(dense_layer) ! Read biases from file @@ -143,12 +156,13 @@ module function network_from_keras(filename) result(res) // layer_name // '/kernel:0' call get_hdf5_dataset(filename, object_name, this_layer % weights) - ! TODO Multidimensional arrays are stored in HDF5 in C-order. - ! TODO Here we transpose the array to get to the Fortran order. - ! TODO There may be a way to do this without re-allocating. - ! TODO It probably doesn't matter much since we do this once. - ! TODO Figure it out later. - this_layer % weights = transpose(this_layer % weights) + type is(flatten_layer) + ! Nothing to do + continue + + type is(maxpool2d_layer) + ! Nothing to do + continue class default error stop 'Internal error in network_from_keras(); ' & diff --git a/test/test_cnn_from_keras.f90 b/test/test_cnn_from_keras.f90 index f8b44826..5f045468 100644 --- a/test/test_cnn_from_keras.f90 +++ b/test/test_cnn_from_keras.f90 @@ -16,4 +16,51 @@ program test_cnn_from_keras net = network(test_data_path) + block + + use nf, only: load_mnist, label_digits + + real, allocatable :: training_images(:,:), training_labels(:) + real, allocatable :: validation_images(:,:), validation_labels(:) + real, allocatable :: testing_images(:,:), testing_labels(:) + real :: acc + + call load_mnist(training_images, training_labels, & + validation_images, validation_labels, & + testing_images, testing_labels) + + acc = accuracy(net, reshape(testing_images, shape=[1,28,28,10000]), label_digits(testing_labels)) + print *, acc + + if (acc < 0.94) then + write(stderr, '(a)') & + 'Pre-trained network accuracy should be > 0.94.. failed' + ok = .false. + end if + + end block + + if (ok) then + print '(a)', 'test_cnn_from_keras: All tests passed.' + else + write(stderr, '(a)') & + 'test_cnn_from_keras: One or more tests failed.' + stop 1 + end if + +contains + + real function accuracy(net, x, y) + type(network), intent(in out) :: net + real, intent(in) :: x(:,:,:,:), y(:,:) + integer :: i, good + good = 0 + do i = 1, size(x, dim=2) + if (all(maxloc(net % output(x(:,:,:,i))) == maxloc(y(:,i)))) then + good = good + 1 + end if + end do + accuracy = real(good) / size(x, dim=2) + end function accuracy + end program test_cnn_from_keras From 0268b266af5ca96679a4ec38a69649e6b9341de3 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Fri, 17 Jun 2022 09:27:57 -0400 Subject: [PATCH 10/20] Use reshape to transpose n-dim HDF5 arrays --- src/nf/io/nf_io_hdf5.f90 | 6 ++-- src/nf/io/nf_io_hdf5_submodule.f90 | 46 +++++++----------------------- 2 files changed, 14 insertions(+), 38 deletions(-) diff --git a/src/nf/io/nf_io_hdf5.f90 b/src/nf/io/nf_io_hdf5.f90 index d1e2fb98..ac74524e 100644 --- a/src/nf/io/nf_io_hdf5.f90 +++ b/src/nf/io/nf_io_hdf5.f90 @@ -31,7 +31,7 @@ module subroutine get_hdf5_dataset_real32_1d(filename, object_name, values) !! HDF5 file name character(*), intent(in) :: object_name !! Object (dataset) name - real(real32), allocatable, intent(in out) :: values(:) + real(real32), allocatable, intent(out) :: values(:) !! Array to store the dataset values into end subroutine get_hdf5_dataset_real32_1d @@ -41,7 +41,7 @@ module subroutine get_hdf5_dataset_real32_2d(filename, object_name, values) !! HDF5 file name character(*), intent(in) :: object_name !! Object (dataset) name - real(real32), allocatable, intent(in out) :: values(:,:) + real(real32), allocatable, intent(out) :: values(:,:) !! Array to store the dataset values into end subroutine get_hdf5_dataset_real32_2d @@ -51,7 +51,7 @@ module subroutine get_hdf5_dataset_real32_4d(filename, object_name, values) !! HDF5 file name character(*), intent(in) :: object_name !! Object (dataset) name - real(real32), allocatable, intent(in out) :: values(:,:,:,:) + real(real32), allocatable, intent(out) :: values(:,:,:,:) !! Array to store the dataset values into end subroutine get_hdf5_dataset_real32_4d diff --git a/src/nf/io/nf_io_hdf5_submodule.f90 b/src/nf/io/nf_io_hdf5_submodule.f90 index 9be98b78..8f66c0e0 100644 --- a/src/nf/io/nf_io_hdf5_submodule.f90 +++ b/src/nf/io/nf_io_hdf5_submodule.f90 @@ -1,6 +1,7 @@ submodule(nf_io_hdf5) nf_io_hdf5_submodule use iso_fortran_env, only: int64, real32, stderr => error_unit + use functional, only: reverse use h5fortran, only: hdf5_file use hdf5, only: H5F_ACC_RDONLY_F, HID_T, & h5aget_type_f, h5aopen_by_name_f, h5aread_f, & @@ -50,7 +51,7 @@ module subroutine get_hdf5_dataset_real32_1d(filename, object_name, values) character(*), intent(in) :: filename character(*), intent(in) :: object_name - real(real32), allocatable, intent(in out) :: values(:) + real(real32), allocatable, intent(out) :: values(:) type(hdf5_file) :: f integer(int64), allocatable :: dims(:) @@ -78,7 +79,7 @@ module subroutine get_hdf5_dataset_real32_2d(filename, object_name, values) character(*), intent(in) :: filename character(*), intent(in) :: object_name - real(real32), allocatable, intent(in out) :: values(:,:) + real(real32), allocatable, intent(out) :: values(:,:) type(hdf5_file) :: f integer(int64), allocatable :: dims(:) @@ -86,15 +87,7 @@ module subroutine get_hdf5_dataset_real32_2d(filename, object_name, values) call f % open(filename, 'r') call f % shape(object_name, dims) - ! If values is already allocated, re-allocate only if incorrect shape - if (allocated(values)) then - if (.not. all(shape(values) == dims)) then - deallocate(values) - allocate(values(dims(1), dims(2))) - end if - else - allocate(values(dims(1), dims(2))) - end if + allocate(values(dims(1), dims(2))) call f % read(object_name, values) call f % close() @@ -109,7 +102,7 @@ module subroutine get_hdf5_dataset_real32_4d(filename, object_name, values) character(*), intent(in) :: filename character(*), intent(in) :: object_name - real(real32), allocatable, intent(in out) :: values(:,:,:,:) + real(real32), allocatable, intent(out) :: values(:,:,:,:) type(hdf5_file) :: f integer(int64), allocatable :: dims(:) @@ -117,35 +110,18 @@ module subroutine get_hdf5_dataset_real32_4d(filename, object_name, values) call f % open(filename, 'r') call f % shape(object_name, dims) - ! If values is already allocated, re-allocate only if incorrect shape - if (allocated(values)) then - if (.not. all(shape(values) == dims)) then - deallocate(values) - allocate(values(dims(1), dims(2), dims(3), dims(4))) - end if - else - allocate(values(dims(1), dims(2), dims(3), dims(4))) - end if + allocate(values(dims(1), dims(2), dims(3), dims(4))) call f % read(object_name, values) call f % close() ! Transpose the array to get from C to Fortran order - values = reverse_dim_order(values) + values = reshape( & + values, & + shape=[dims(4), dims(3), dims(2), dims(1)], & + order=[4, 3, 2, 1] & + ) end subroutine get_hdf5_dataset_real32_4d - - pure function reverse_dim_order(x) result(res) - real, intent(in) :: x(:,:,:,:) - real, allocatable :: res(:,:,:,:) - integer :: dims(4) - integer :: i, j, k, l - dims = shape(x) - allocate(res(dims(4), dims(3), dims(2), dims(1))) - do concurrent(i = 1:dims(1), j = 1:dims(2), k = 1:dims(3), l = 1:dims(4)) - res(l,k,j,i) = x(i,j,k,l) - end do - end function reverse_dim_order - end submodule nf_io_hdf5_submodule From 2ff343ec9bdd08a94b8906eac1630ccfd3336882 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Fri, 17 Jun 2022 12:36:20 -0400 Subject: [PATCH 11/20] Don't transpose 4-d kernel; already stored correctly --- src/nf/io/nf_io_hdf5_submodule.f90 | 19 ++----------------- test/test_io_hdf5.f90 | 2 +- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/nf/io/nf_io_hdf5_submodule.f90 b/src/nf/io/nf_io_hdf5_submodule.f90 index 8f66c0e0..e2ccff64 100644 --- a/src/nf/io/nf_io_hdf5_submodule.f90 +++ b/src/nf/io/nf_io_hdf5_submodule.f90 @@ -59,15 +59,7 @@ module subroutine get_hdf5_dataset_real32_1d(filename, object_name, values) call f % open(filename, 'r') call f % shape(object_name, dims) - ! If values is already allocated, re-allocate only if incorrect shape - if (allocated(values)) then - if (.not. all(shape(values) == dims)) then - deallocate(values) - allocate(values(dims(1))) - end if - else - allocate(values(dims(1))) - end if + allocate(values(dims(1))) call f % read(object_name, values) call f % close() @@ -92,7 +84,7 @@ module subroutine get_hdf5_dataset_real32_2d(filename, object_name, values) call f % read(object_name, values) call f % close() - ! Transpose the array to get from C to Fortran order + ! Transpose the array to respect Keras's storage order values = transpose(values) end subroutine get_hdf5_dataset_real32_2d @@ -115,13 +107,6 @@ module subroutine get_hdf5_dataset_real32_4d(filename, object_name, values) call f % read(object_name, values) call f % close() - ! Transpose the array to get from C to Fortran order - values = reshape( & - values, & - shape=[dims(4), dims(3), dims(2), dims(1)], & - order=[4, 3, 2, 1] & - ) - end subroutine get_hdf5_dataset_real32_4d end submodule nf_io_hdf5_submodule diff --git a/test/test_io_hdf5.f90 b/test/test_io_hdf5.f90 index b662c0af..e7e0fc88 100644 --- a/test/test_io_hdf5.f90 +++ b/test/test_io_hdf5.f90 @@ -42,7 +42,7 @@ program test_io_hdf5 write(stderr, '(a)') 'HDF5 1-d dataset dims inquiry is correct.. failed' end if - if (.not. all(shape(weights) == [30, 784])) then + if (.not. all(shape(weights) == [784, 30])) then ok = .false. write(stderr, '(a)') 'HDF5 2-d dataset dims inquiry is correct.. failed' end if From 5592b9a40b8e06ab7cc15c37e9738c82879b3fc4 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Mon, 27 Jun 2022 09:09:11 -0400 Subject: [PATCH 12/20] CNN from Keras test is passing --- test/test_cnn_from_keras.f90 | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/test_cnn_from_keras.f90 b/test/test_cnn_from_keras.f90 index 5f045468..1c47325e 100644 --- a/test/test_cnn_from_keras.f90 +++ b/test/test_cnn_from_keras.f90 @@ -23,18 +23,21 @@ program test_cnn_from_keras real, allocatable :: training_images(:,:), training_labels(:) real, allocatable :: validation_images(:,:), validation_labels(:) real, allocatable :: testing_images(:,:), testing_labels(:) + real, allocatable :: input_reshaped(:,:,:,:) real :: acc call load_mnist(training_images, training_labels, & validation_images, validation_labels, & testing_images, testing_labels) - acc = accuracy(net, reshape(testing_images, shape=[1,28,28,10000]), label_digits(testing_labels)) - print *, acc + ! Use only the first 1000 images to make the test short + input_reshaped = reshape(testing_images(:,:1000), shape=[1,28,28,1000]) - if (acc < 0.94) then + acc = accuracy(net, input_reshaped, label_digits(testing_labels(:1000))) + + if (acc < 0.97) then write(stderr, '(a)') & - 'Pre-trained network accuracy should be > 0.94.. failed' + 'Pre-trained network accuracy should be > 0.97.. failed' ok = .false. end if @@ -55,12 +58,12 @@ real function accuracy(net, x, y) real, intent(in) :: x(:,:,:,:), y(:,:) integer :: i, good good = 0 - do i = 1, size(x, dim=2) + do i = 1, size(x, dim=4) if (all(maxloc(net % output(x(:,:,:,i))) == maxloc(y(:,i)))) then good = good + 1 end if end do - accuracy = real(good) / size(x, dim=2) + accuracy = real(good) / size(x, dim=4) end function accuracy end program test_cnn_from_keras From 925e30641fc8846b59f670f082f156e2e14aed6e Mon Sep 17 00:00:00 2001 From: milancurcic Date: Mon, 27 Jun 2022 09:11:36 -0400 Subject: [PATCH 13/20] Allow as 3-d output of networks; assign activation function to conv2d layers from Keras --- src/nf/nf_network_submodule.f90 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/nf/nf_network_submodule.f90 b/src/nf/nf_network_submodule.f90 index 77d96c4e..c9f865ff 100644 --- a/src/nf/nf_network_submodule.f90 +++ b/src/nf/nf_network_submodule.f90 @@ -79,7 +79,8 @@ module function network_from_keras(filename) result(res) layers(n) = conv2d( & keras_layers(n) % filters, & !FIXME add support for non-square kernel - keras_layers(n) % kernel_size(1) & + keras_layers(n) % kernel_size(1), & + keras_layers(n) % activation & ) case('Dense') @@ -162,7 +163,7 @@ module function network_from_keras(filename) result(res) type is(maxpool2d_layer) ! Nothing to do - continue + continue class default error stop 'Internal error in network_from_keras(); ' & @@ -258,6 +259,8 @@ module function output_1d(self, input) result(res) res = output_layer % output type is(flatten_layer) res = output_layer % output + class default + error stop 'network % output not implemented for this output layer' end select end function output_1d @@ -274,10 +277,15 @@ module function output_3d(self, input) result(res) call self % forward(input) select type(output_layer => self % layers(num_layers) % p) + type is(conv2d_layer) + !FIXME flatten the result for now; find a better solution + res = pack(output_layer % output, .true.) type is(dense_layer) res = output_layer % output type is(flatten_layer) res = output_layer % output + class default + error stop 'network % output not implemented for this output layer' end select end function output_3d From 3a5cc1904132a138386d198b3da68a22f6893bdb Mon Sep 17 00:00:00 2001 From: milancurcic Date: Mon, 27 Jun 2022 09:17:10 -0400 Subject: [PATCH 14/20] Remove unused import --- src/nf/io/nf_io_hdf5_submodule.f90 | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nf/io/nf_io_hdf5_submodule.f90 b/src/nf/io/nf_io_hdf5_submodule.f90 index e2ccff64..8cb6f760 100644 --- a/src/nf/io/nf_io_hdf5_submodule.f90 +++ b/src/nf/io/nf_io_hdf5_submodule.f90 @@ -1,7 +1,6 @@ submodule(nf_io_hdf5) nf_io_hdf5_submodule use iso_fortran_env, only: int64, real32, stderr => error_unit - use functional, only: reverse use h5fortran, only: hdf5_file use hdf5, only: H5F_ACC_RDONLY_F, HID_T, & h5aget_type_f, h5aopen_by_name_f, h5aread_f, & From 0941ebbb4c0af0fd4c6c7bafacf2451452e92a56 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Tue, 28 Jun 2022 08:00:57 -0400 Subject: [PATCH 15/20] Add CMake rules for functional-fortran --- CMakeLists.txt | 9 ++++++++- cmake/functional.cmake | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 cmake/functional.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d26a228..564b3b0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ include(FetchContent) include(cmake/options.cmake) include(cmake/compilers.cmake) +include(cmake/functional.cmake) include(cmake/h5fortran.cmake) include(cmake/json.cmake) @@ -62,7 +63,13 @@ add_library(neural src/nf/io/nf_io_hdf5.f90 src/nf/io/nf_io_hdf5_submodule.f90 ) -target_link_libraries(neural PRIVATE h5fortran::h5fortran HDF5::HDF5 jsonfortran::jsonfortran) + +target_link_libraries(neural PRIVATE + functional::functional + h5fortran::h5fortran + HDF5::HDF5 + jsonfortran::jsonfortran +) install(TARGETS neural) diff --git a/cmake/functional.cmake b/cmake/functional.cmake new file mode 100644 index 00000000..d8f21200 --- /dev/null +++ b/cmake/functional.cmake @@ -0,0 +1,18 @@ +FetchContent_Declare(functional + GIT_REPOSITORY https://github.com/wavebitscientific/functional-fortran + GIT_TAG 0.6.1 + GIT_SHALLOW true +) + +FetchContent_Populate(functional) + +add_library(functional ${functional_SOURCE_DIR}/src/functional.f90) +target_include_directories(functional PUBLIC +$ +$ +) + +add_library(functional::functional INTERFACE IMPORTED GLOBAL) +target_link_libraries(functional::functional INTERFACE functional) + +install(TARGETS functional) From 148892bd2adcd365df4cd9be009dbeff4cd5e4d8 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Tue, 28 Jun 2022 08:31:58 -0400 Subject: [PATCH 16/20] Add CNN from Keras example; rename the dense from Keras example --- example/CMakeLists.txt | 16 ++++- example/cnn_from_keras.f90 | 58 +++++++++++++++++++ ...st_from_keras.f90 => dense_from_keras.f90} | 11 ++-- 3 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 example/cnn_from_keras.f90 rename example/{mnist_from_keras.f90 => dense_from_keras.f90} (82%) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index b8442417..7cb24732 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -1,4 +1,16 @@ -foreach(execid cnn mnist mnist_from_keras simple sine) +foreach(execid + cnn + cnn_from_keras + dense_from_keras + mnist + simple + sine +) add_executable(${execid} ${execid}.f90) - target_link_libraries(${execid} PRIVATE neural h5fortran::h5fortran jsonfortran::jsonfortran ${LIBS}) + target_link_libraries(${execid} PRIVATE + neural + h5fortran::h5fortran + jsonfortran::jsonfortran + ${LIBS} + ) endforeach() diff --git a/example/cnn_from_keras.f90 b/example/cnn_from_keras.f90 new file mode 100644 index 00000000..5be47854 --- /dev/null +++ b/example/cnn_from_keras.f90 @@ -0,0 +1,58 @@ +program cnn_from_keras + + ! This example demonstrates loading a convolutional model + ! pre-trained on the MNIST dataset from a Keras HDF5 + ! file and running an inferrence on the testing dataset. + + use nf, only: network, label_digits, load_mnist + use nf_datasets, only: download_and_unpack, keras_cnn_mnist_url + + implicit none + + type(network) :: net + real, allocatable :: training_images(:,:), training_labels(:) + real, allocatable :: validation_images(:,:), validation_labels(:) + real, allocatable :: testing_images(:,:), testing_labels(:) + character(*), parameter :: keras_cnn_path = 'keras_cnn_mnist.h5' + logical :: file_exists + real :: acc + + inquire(file=keras_cnn_path, exist=file_exists) + if (.not. file_exists) call download_and_unpack(keras_cnn_mnist_url) + + call load_mnist(training_images, training_labels, & + validation_images, validation_labels, & + testing_images, testing_labels) + + print '("Loading a pre-trained CNN model from Keras")' + print '(60("="))' + + net = network(keras_cnn_path) + + call net % print_info() + + if (this_image() == 1) then + acc = accuracy( & + net, & + reshape(testing_images(:,:), shape=[1,28,28,size(testing_images,2)]), & + label_digits(testing_labels) & + ) + print '(a,f5.2,a)', 'Accuracy: ', acc * 100, ' %' + end if + +contains + + real function accuracy(net, x, y) + type(network), intent(in out) :: net + real, intent(in) :: x(:,:,:,:), y(:,:) + integer :: i, good + good = 0 + do i = 1, size(x, dim=4) + if (all(maxloc(net % output(x(:,:,:,i))) == maxloc(y(:,i)))) then + good = good + 1 + end if + end do + accuracy = real(good) / size(x, dim=4) + end function accuracy + +end program cnn_from_keras diff --git a/example/mnist_from_keras.f90 b/example/dense_from_keras.f90 similarity index 82% rename from example/mnist_from_keras.f90 rename to example/dense_from_keras.f90 index 703b51a2..cf473d6d 100644 --- a/example/mnist_from_keras.f90 +++ b/example/dense_from_keras.f90 @@ -1,7 +1,8 @@ -program mnist_from_keras +program dense_from_keras - ! This example demonstrates loading a pre-trained MNIST model from Keras - ! from an HDF5 file and running an inferrence on the testing dataset. + ! This example demonstrates loading a dense model + ! pre-trained on the MNIST dataset from a Keras HDF5 + ! file and running an inferrence on the testing dataset. use nf, only: network, label_digits, load_mnist use nf_datasets, only: download_and_unpack, keras_dense_mnist_url @@ -22,7 +23,7 @@ program mnist_from_keras validation_images, validation_labels, & testing_images, testing_labels) - print '("Loading a pre-trained MNIST model from Keras")' + print '("Loading a pre-trained dense model from Keras")' print '(60("="))' net = network(keras_dense_path) @@ -48,4 +49,4 @@ real function accuracy(net, x, y) accuracy = real(good) / size(x, dim=2) end function accuracy -end program mnist_from_keras +end program dense_from_keras From 0ada76a8e903efebb64cdecc3ec9b8de3afee85d Mon Sep 17 00:00:00 2001 From: milancurcic Date: Tue, 28 Jun 2022 08:32:21 -0400 Subject: [PATCH 17/20] Update the README feature and examples list --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 085ddd96..cd55e18d 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,11 @@ Read the paper [here](https://arxiv.org/abs/1902.06714). * Dense, fully connected neural layers * Convolutional and max-pooling layers (experimental, forward propagation only) +* Flatten layers (forward and backward pass) +* Loading dense and convolutional models from Keras h5 files * Stochastic and mini-batch gradient descent for back-propagation * Data-based parallelism -* Several activation functions +* Several activation functions and their derivatives ### Available layer types @@ -57,7 +59,8 @@ Required dependencies are: Optional dependencies are: * OpenCoarrays (for parallel execution with GFortran) -* BLAS, MKL (optional) +* BLAS, MKL, or similar (for offloading `matmul` and `dot_product` calls) +* curl (for downloading testing and example datasets) Compilers tested include: @@ -200,13 +203,15 @@ examples, in increasing level of complexity: dataset 4. [cnn](example/cnn.f90): Creating and running forward a simple CNN using `input`, `conv2d`, `maxpool2d`, `flatten`, and `dense` layers. -5. [mnist_from_keras](example/mnist_from_keras.f90): Creating a pre-trained - model from a Keras HDF5 file. +5. [dense_from_keras](example/dense_from_keras.f90): Creating a pre-trained + dense model from a Keras HDF5 file and running the inference. +6. [cnn_from_keras](example/cnn_from_keras.f90): Creating a pre-trained + convolutional model from a Keras HDF5 file and running the inference. The examples also show you the extent of the public API that's meant to be used in applications, i.e. anything from the `nf` module. -The MNIST example uses [curl](https://curl.se/) to download the dataset, +Examples 3-6 rely on [curl](https://curl.se/) to download the needed datasets, so make sure you have it installed on your system. Most Linux OSs have it out of the box. The dataset will be downloaded only the first time you run the example in any From eb992cb414972441ab7d9c8f013e3ba509fce9ea Mon Sep 17 00:00:00 2001 From: milancurcic Date: Tue, 28 Jun 2022 08:32:31 -0400 Subject: [PATCH 18/20] Bump version to 0.6.0 --- fpm.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fpm.toml b/fpm.toml index 4733e936..0cabfcac 100644 --- a/fpm.toml +++ b/fpm.toml @@ -1,5 +1,5 @@ name = "neural-fortran" -version = "0.5.0" +version = "0.6.0" license = "MIT" author = "Milan Curcic" maintainer = "milancurcic@hey.com" From c0baf37350f43edadffbf93fdd898ccef48c7a62 Mon Sep 17 00:00:00 2001 From: milancurcic Date: Tue, 28 Jun 2022 08:37:35 -0400 Subject: [PATCH 19/20] Update the project description (not so micro anymore) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cd55e18d..1dc9943e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![GitHub issues](https://img.shields.io/github/issues/modern-fortran/neural-fortran.svg)](https://github.com/modern-fortran/neural-fortran/issues) -A parallel neural net microframework. +A parallel framework for deep learning. Read the paper [here](https://arxiv.org/abs/1902.06714). * [Features](https://github.com/modern-fortran/neural-fortran#features) From 0577f821dd854d37f6b2db9f8a33da167f00442a Mon Sep 17 00:00:00 2001 From: milancurcic Date: Tue, 28 Jun 2022 08:42:11 -0400 Subject: [PATCH 20/20] Update list of dependencies --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1dc9943e..b66561de 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,10 @@ Required dependencies are: * A Fortran compiler * [HDF5](https://www.hdfgroup.org/downloads/hdf5/) (must be provided by the OS package manager or your own build from source) -* [h5fortran](https://github.com/geospace-code/h5fortran), +* [functional-fortran](https://github.com/wavebitscientific/functional-fortran), + [h5fortran](https://github.com/geospace-code/h5fortran), [json-fortran](https://github.com/jacobwilliams/json-fortran) - (both handled by neural-fortran's build systems, no need for a manual install) + (all handled by neural-fortran's build systems, no need for a manual install) * [fpm](https://github.com/fortran-lang/fpm) or [CMake](https://cmake.org) for building the code