diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0df37f11..e7ec789a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,52 +14,50 @@ on: - "fpm.toml" - "**.f90" -env: - FPM_FFLAGS: -I/usr/include/hdf5/serial - FPM_LDFLAGS: -L/usr/lib/x86_64-linux-gnu/hdf5/serial - jobs: - build_and_test_debug_profile: - name: Build and test in debug mode + gnu-cmake-debug: + name: gnu-cmake-debug runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Compile + run: cmake -DCMAKE_BUILD_TYPE=Debug -DSERIAL=1 . && make + - name: Test + run: make test + gnu-cmake-release: + name: gnu-cmake-release + runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + - name: Compile + run: cmake -DCMAKE_BUILD_TYPE=Release -DSERIAL=1 . && make + - name: Test + run: make test - - uses: fortran-lang/setup-fpm@v4 + gnu-fpm-debug: + name: gnu-fpm-debug + runs-on: ubuntu-latest + steps: + - uses: fortran-lang/setup-fpm@v5 with: - fpm-version: "v0.6.0" - - - name: Install HDF5 - run: | - sudo apt update - sudo apt install --no-install-recommends libhdf5-dev - - uses: actions/checkout@v2 - + fpm-version: "v0.10.1" + - uses: actions/checkout@v4 - name: Compile run: fpm build --profile debug - - name: Test run: fpm test --profile debug - build_and_test_release_profile: - name: Build and test in release mode + gnu-fpm-release: + name: gnu-fpm-release runs-on: ubuntu-latest - steps: - - - uses: fortran-lang/setup-fpm@v4 + - uses: fortran-lang/setup-fpm@v5 with: - fpm-version: "v0.6.0" - - - name: Install HDF5 - run: | - sudo apt update - sudo apt install --no-install-recommends libhdf5-dev - - uses: actions/checkout@v2 - + fpm-version: "v0.10.1" + - uses: actions/checkout@v4 - name: Compile run: fpm build --profile release - - name: Test run: fpm test --profile release diff --git a/CMakeLists.txt b/CMakeLists.txt index 59f86fff..0340d6b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,21 +6,13 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Default build Release") endif() -project(neural-fortran -LANGUAGES C Fortran -) +project(neural-fortran LANGUAGES Fortran) enable_testing() -include(FetchContent) - include(cmake/options.cmake) include(cmake/compilers.cmake) -include(cmake/functional.cmake) -include(cmake/h5fortran.cmake) -include(cmake/json.cmake) - # library to archive (libneural-fortran.a) add_library(neural-fortran src/nf.f90 @@ -40,8 +32,6 @@ add_library(neural-fortran src/nf/nf_input1d_layer_submodule.f90 src/nf/nf_input3d_layer.f90 src/nf/nf_input3d_layer_submodule.f90 - src/nf/nf_keras.f90 - src/nf/nf_keras_submodule.f90 src/nf/nf_layer_constructors.f90 src/nf/nf_layer_constructors_submodule.f90 src/nf/nf_layer.f90 @@ -61,16 +51,9 @@ add_library(neural-fortran src/nf/nf_reshape_layer_submodule.f90 src/nf/io/nf_io_binary.f90 src/nf/io/nf_io_binary_submodule.f90 - src/nf/io/nf_io_hdf5.f90 - src/nf/io/nf_io_hdf5_submodule.f90 ) -target_link_libraries(neural-fortran PRIVATE - functional::functional - h5fortran::h5fortran - HDF5::HDF5 - jsonfortran::jsonfortran -) +target_link_libraries(neural-fortran PRIVATE) install(TARGETS neural-fortran) diff --git a/README.md b/README.md index 054582b2..d512f91d 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ Read the paper [here](https://arxiv.org/abs/1902.06714). RMSProp, Adagrad, Adam, AdamW * More than a dozen activation functions and their derivatives * Loss functions and metrics: Quadratic, Mean Squared Error, Pearson Correlation etc. -* Loading dense and convolutional models from Keras HDF5 (.h5) files * Data-based parallelism +* Loading dense and convolutional models from Keras HDF5 (.h5) files +(see the [nf-keras-hdf5](https://github.com/neural-fortran/nf-keras-hdf5) add-on) ### Available layers @@ -51,14 +52,8 @@ cd neural-fortran 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) -* [functional-fortran](https://github.com/wavebitscientific/functional-fortran), - [h5fortran](https://github.com/geospace-code/h5fortran), - [json-fortran](https://github.com/jacobwilliams/json-fortran) - (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 + [CMake](https://cmake.org) to build the code Optional dependencies are: @@ -79,23 +74,7 @@ Compilers tested include: With gfortran, the following will create an optimized build of neural-fortran: ``` -fpm build \ - --profile release \ - --flag "-I$HDF5INC -L$HDF5LIB" -``` - -HDF5 is now a required dependency, so you have to provide it to fpm. -The above command assumes that the `HDF5INC` and `HDF5LIB` environment -variables are set to the include and library paths, respectively, of your -HDF5 install. - -If you use Conda, the following instructions work: - -``` -conda create -n nf hdf5 -conda activate nf -fpm build --profile release --flag "-I$CONDA_PREFIX/include -L$CONDA_PREFIX/lib -Wl,-rpath -Wl,$CONDA_PREFIX/lib" -fpm test --profile release --flag "-I$CONDA_PREFIX/include -L$CONDA_PREFIX/lib -Wl,-rpath -Wl,$CONDA_PREFIX/lib" +fpm build --profile release ``` #### Building in parallel mode @@ -106,25 +85,20 @@ Once installed, use the compiler wrappers `caf` and `cafrun` to build and execut in parallel, respectively: ``` -fpm build \ - --compiler caf \ - --profile release \ - --flag "-I$HDF5INC -L$HDF5LIB" +fpm build --compiler caf --profile release ``` #### Testing with fpm ``` -fpm test \ - --profile release \ - --flag "-I$HDF5INC -L$HDF5LIB" +fpm test --profile release ``` For the time being, you need to specify the same compiler flags to `fpm test` as you did in `fpm build` so that fpm knows it should use the same build profile. -See [Fortran Package Manager](https://github.com/fortran-lang/fpm) for more info on fpm. +See the [Fortran Package Manager](https://github.com/fortran-lang/fpm) for more info on fpm. ### Building with CMake @@ -156,8 +130,7 @@ cafrun -n 4 bin/mnist # run MNIST example on 4 cores #### Building with a different compiler If you want to build with a different compiler, such as Intel Fortran, -set the `HDF5_ROOT` environment variable to the root path of your -Intel HDF5 build, and specify `FC` when issuing `cmake`: +specify `FC` when issuing `cmake`: ``` FC=ifort cmake .. @@ -213,6 +186,7 @@ You can configure neural-fortran by setting the appropriate options before including the subproject. The following should be added in the CMake file of your directory: + ```cmake if(NOT TARGET "neural-fortran::neural-fortran") find_package("neural-fortran" REQUIRED) @@ -230,11 +204,7 @@ examples, in increasing level of complexity: 3. [dense_mnist](example/dense_mnist.f90): Hand-written digit recognition (MNIST dataset) using a dense (fully-connected) network 4. [cnn_mnist](example/cnn_mnist.f90): Training a CNN on the MNIST dataset -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. -7. [get_set_network_params](example/get_set_network_params.f90): Getting and +5. [get_set_network_params](example/get_set_network_params.f90): Getting and setting hyperparameters of a network. The examples also show you the extent of the public API that's meant to be diff --git a/cmake/functional.cmake b/cmake/functional.cmake deleted file mode 100644 index d8f21200..00000000 --- a/cmake/functional.cmake +++ /dev/null @@ -1,18 +0,0 @@ -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) diff --git a/cmake/h5fortran.cmake b/cmake/h5fortran.cmake deleted file mode 100644 index 6351e63a..00000000 --- a/cmake/h5fortran.cmake +++ /dev/null @@ -1,15 +0,0 @@ -set(h5fortran_BUILD_TESTING false) - -FetchContent_Declare(h5fortran - GIT_REPOSITORY https://github.com/geospace-code/h5fortran - GIT_TAG v4.6.3 - GIT_SHALLOW true -) - -FetchContent_MakeAvailable(h5fortran) - -file(MAKE_DIRECTORY ${h5fortran_BINARY_DIR}/include) - - -list(APPEND CMAKE_MODULE_PATH ${h5fortran_SOURCE_DIR}/cmake/Modules) -find_package(HDF5 COMPONENTS Fortran REQUIRED) diff --git a/cmake/json.cmake b/cmake/json.cmake deleted file mode 100644 index 5713382e..00000000 --- a/cmake/json.cmake +++ /dev/null @@ -1,35 +0,0 @@ -# use our own CMake script to build jsonfortran instead of jsonfortran/CMakelists.txt - -FetchContent_Declare(jsonfortran - GIT_REPOSITORY https://github.com/jacobwilliams/json-fortran - GIT_TAG 8.3.0 - GIT_SHALLOW true -) - -FetchContent_Populate(jsonfortran) - -SET(JSON_REAL_KIND "REAL64") -SET(JSON_INT_KIND "INT32") - -set(_src ${jsonfortran_SOURCE_DIR}/src) - -set (JF_LIB_SRCS -${_src}/json_kinds.F90 -${_src}/json_parameters.F90 -${_src}/json_string_utilities.F90 -${_src}/json_value_module.F90 -${_src}/json_file_module.F90 -${_src}/json_module.F90 -) - -add_library(jsonfortran ${JF_LIB_SRCS}) -target_compile_definitions(jsonfortran PRIVATE ${JSON_REAL_KIND} ${JSON_INT_KIND}) -target_include_directories(jsonfortran PUBLIC -$ -$ -) - -add_library(jsonfortran::jsonfortran INTERFACE IMPORTED GLOBAL) -target_link_libraries(jsonfortran::jsonfortran INTERFACE jsonfortran) - -install(TARGETS jsonfortran) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 48bcc91e..28cf71a7 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -1,8 +1,6 @@ foreach(execid cnn_mnist - cnn_from_keras dense_mnist - dense_from_keras get_set_network_params network_parameters simple @@ -12,8 +10,6 @@ foreach(execid add_executable(${execid} ${execid}.f90) target_link_libraries(${execid} PRIVATE neural-fortran - h5fortran::h5fortran - jsonfortran::jsonfortran ${LIBS} ) endforeach() diff --git a/example/cnn_from_keras.f90 b/example/cnn_from_keras.f90 deleted file mode 100644 index 6b21f894..00000000 --- a/example/cnn_from_keras.f90 +++ /dev/null @@ -1,58 +0,0 @@ -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 % predict(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/dense_from_keras.f90 b/example/dense_from_keras.f90 deleted file mode 100644 index 4fc332d1..00000000 --- a/example/dense_from_keras.f90 +++ /dev/null @@ -1,52 +0,0 @@ -program dense_from_keras - - ! 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 - - 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_dense_path = 'keras_dense_mnist.h5' - logical :: file_exists - - 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, & - testing_images, testing_labels) - - print '("Loading a pre-trained dense model from Keras")' - print '(60("="))' - - net = network(keras_dense_path) - - call net % print_info() - - if (this_image() == 1) & - print '(a,f5.2,a)', 'Accuracy: ', accuracy( & - net, testing_images, label_digits(testing_labels)) * 100, ' %' - -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 % predict(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 dense_from_keras diff --git a/fpm.toml b/fpm.toml index b638d215..5f68f8f6 100644 --- a/fpm.toml +++ b/fpm.toml @@ -1,15 +1,6 @@ name = "neural-fortran" -version = "0.17.0" +version = "0.18.0" license = "MIT" author = "Milan Curcic" maintainer = "milancurcic@hey.com" copyright = "Copyright 2018-2024, neural-fortran contributors" - -[build] -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/io/nf_io_hdf5.f90 b/src/nf/io/nf_io_hdf5.f90 deleted file mode 100644 index ac74524e..00000000 --- a/src/nf/io/nf_io_hdf5.f90 +++ /dev/null @@ -1,60 +0,0 @@ -module nf_io_hdf5 - - !! This module provides convenience functions to read HDF5 files. - - use iso_fortran_env, only: real32 - - implicit none - - private - public :: get_hdf5_dataset, hdf5_attribute_string - - interface - module function hdf5_attribute_string( & - filename, object_name, attribute_name) result(res) - !! Read and return an HDF5 variable-length UTF-8 string attribute. - character(*), intent(in) :: filename - !! HDF5 file name - character(*), intent(in) :: object_name - !! Object (group, dataset) name - character(*), intent(in) :: attribute_name - !! Name of the attribute to read - character(:), allocatable :: res - end function hdf5_attribute_string - end interface - - interface get_hdf5_dataset - - module subroutine get_hdf5_dataset_real32_1d(filename, object_name, values) - !! Read a 1-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(out) :: values(:) - !! Array to store the dataset values into - end subroutine get_hdf5_dataset_real32_1d - - module subroutine get_hdf5_dataset_real32_2d(filename, object_name, values) - !! Read a 2-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(out) :: 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(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 deleted file mode 100644 index 8cb6f760..00000000 --- a/src/nf/io/nf_io_hdf5_submodule.f90 +++ /dev/null @@ -1,111 +0,0 @@ -submodule(nf_io_hdf5) nf_io_hdf5_submodule - - use iso_fortran_env, only: int64, real32, stderr => error_unit - use h5fortran, only: hdf5_file - use hdf5, only: H5F_ACC_RDONLY_F, HID_T, & - h5aget_type_f, h5aopen_by_name_f, h5aread_f, & - h5fclose_f, h5fopen_f - use iso_c_binding, only: c_char, c_f_pointer, c_loc, c_null_char, c_ptr - - implicit none - -contains - - module function hdf5_attribute_string( & - filename, object_name, attribute_name) result(res) - - character(*), intent(in) :: filename - character(*), intent(in) :: object_name - character(*), intent(in) :: attribute_name - character(:), allocatable :: res - - ! Make sufficiently large to hold most attributes - integer, parameter :: BUFLEN = 10000 - - type(c_ptr) :: f_ptr - type(c_ptr), target :: buffer - character(len=BUFLEN, kind=c_char), pointer :: string => null() - integer(HID_T) :: fid, aid, atype - integer :: hdferr - - ! Open the file and get the type of the attribute - call h5fopen_f(filename, H5F_ACC_RDONLY_F, fid, hdferr) - call h5aopen_by_name_f(fid, object_name, attribute_name, aid, hdferr) - call h5aget_type_f(aid, atype, hdferr) - - ! Read the data - f_ptr = c_loc(buffer) - call h5aread_f(aid, atype, f_ptr, hdferr) - call c_f_pointer(buffer, string) - - ! Close the file - call h5fclose_f(fid, hdferr) - - res = string(:index(string, c_null_char) - 1) - - end function hdf5_attribute_string - - - module subroutine get_hdf5_dataset_real32_1d(filename, object_name, values) - - character(*), intent(in) :: filename - character(*), intent(in) :: object_name - real(real32), allocatable, intent(out) :: values(:) - - type(hdf5_file) :: f - integer(int64), allocatable :: dims(:) - - call f % open(filename, 'r') - call f % shape(object_name, dims) - - allocate(values(dims(1))) - - call f % read(object_name, values) - call f % close() - - end subroutine get_hdf5_dataset_real32_1d - - - module subroutine get_hdf5_dataset_real32_2d(filename, object_name, values) - - character(*), intent(in) :: filename - character(*), intent(in) :: object_name - real(real32), allocatable, intent(out) :: values(:,:) - - type(hdf5_file) :: f - integer(int64), allocatable :: dims(:) - - call f % open(filename, 'r') - call f % shape(object_name, dims) - - allocate(values(dims(1), dims(2))) - - call f % read(object_name, values) - call f % close() - - ! Transpose the array to respect Keras's storage 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(out) :: values(:,:,:,:) - - type(hdf5_file) :: f - integer(int64), allocatable :: dims(:) - - call f % open(filename, 'r') - call f % shape(object_name, dims) - - allocate(values(dims(1), dims(2), dims(3), dims(4))) - - call f % read(object_name, values) - call f % close() - - end subroutine get_hdf5_dataset_real32_4d - -end submodule nf_io_hdf5_submodule diff --git a/src/nf/nf_activation.f90 b/src/nf/nf_activation.f90 index e413243c..237ce266 100644 --- a/src/nf/nf_activation.f90 +++ b/src/nf/nf_activation.f90 @@ -7,6 +7,7 @@ module nf_activation private public :: activation_function + public :: get_activation_by_name public :: elu public :: exponential public :: gaussian @@ -556,6 +557,59 @@ pure function eval_3d_celu_prime(self, x) result(res) end where end function + pure function get_activation_by_name(activation_name) result(res) + ! Workaround to get activation_function with some + ! hardcoded default parameters by its name. + ! Need this function since we get only activation name + ! from keras files. + character(len=*), intent(in) :: activation_name + class(activation_function), allocatable :: res + + select case(trim(activation_name)) + case('elu') + allocate ( res, source = elu(alpha = 0.1) ) + + case('exponential') + allocate ( res, source = exponential() ) + + case('gaussian') + allocate ( res, source = gaussian() ) + + case('linear') + allocate ( res, source = linear() ) + + case('relu') + allocate ( res, source = relu() ) + + case('leaky_relu') + allocate ( res, source = leaky_relu(alpha = 0.1) ) + + case('sigmoid') + allocate ( res, source = sigmoid() ) + + case('softmax') + allocate ( res, source = softmax() ) + + case('softplus') + allocate ( res, source = softplus() ) + + case('step') + allocate ( res, source = step() ) + + case('tanh') + allocate ( res, source = tanhf() ) + + case('celu') + allocate ( res, source = celu() ) + + case default + error stop 'activation_name must be one of: ' // & + '"elu", "exponential", "gaussian", "linear", "relu", ' // & + '"leaky_relu", "sigmoid", "softmax", "softplus", "step", "tanh" or "celu".' + end select + + end function get_activation_by_name + pure function get_name(self) result(name) !! Return the name of the activation function. !! diff --git a/src/nf/nf_keras.f90 b/src/nf/nf_keras.f90 deleted file mode 100644 index efe2905a..00000000 --- a/src/nf/nf_keras.f90 +++ /dev/null @@ -1,47 +0,0 @@ -module nf_keras - - !! This module provides procedures to read and parse Keras models - !! from HDF5 files. - - implicit none - - private - public :: get_keras_h5_layers, keras_layer - - type :: keras_layer - - !! 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(:) - - ! Conv2D - integer :: filters - integer, allocatable :: kernel_size(:) - - ! MaxPooling2D - integer, allocatable :: pool_size(:) - integer, allocatable :: strides(:) - - ! Reshape - integer, allocatable :: target_shape(:) - - end type keras_layer - - interface - - module function get_keras_h5_layers(filename) result(res) - character(*), intent(in) :: filename - !! HDF5 file name - type(keras_layer), allocatable :: res(:) - end function get_keras_h5_layers - - end interface - -end module nf_keras diff --git a/src/nf/nf_keras_submodule.f90 b/src/nf/nf_keras_submodule.f90 deleted file mode 100644 index b5c0d292..00000000 --- a/src/nf/nf_keras_submodule.f90 +++ /dev/null @@ -1,104 +0,0 @@ -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 - - implicit none - -contains - - module function get_keras_h5_layers(filename) result(res) - character(*), intent(in) :: filename - type(keras_layer), allocatable :: res(:) - - character(:), allocatable :: model_config_string - - type(json_core) :: json - type(json_value), pointer :: & - model_config_json, layers_json, layer_json, layer_config_json - - real, allocatable :: tmp_array(:) - integer :: n, num_layers, units - logical :: found - - model_config_string = hdf5_attribute_string(filename, '.', 'model_config') - - call json % parse(model_config_json, model_config_string) - call json % get(model_config_json, 'config.layers', layers_json) - - num_layers = json % count(layers_json) - - allocate(res(num_layers)) - - ! Iterate over layers - layers: do n = 1, num_layers - - ! Get pointer to the layer - call json % get_child(layers_json, n, layer_json) - - ! Get type of layer as a string - call json % get(layer_json, 'class_name', res(n) % class) - - ! Get pointer to the layer config - call json % get(layer_json, 'config', layer_config_json) - - ! Get layer name - call json % get(layer_config_json, 'name', res(n) % name) - - ! Get size of layer and activation if applicable; - ! Instantiate neural-fortran layers at this time. - select case(res(n) % class) - - case('InputLayer') - call json % get(layer_config_json, 'batch_input_shape', tmp_array) - res(n) % units = reverse(tmp_array(2:)) ! skip the 1st (batch) dim - - case('Dense') - 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) - 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('Reshape') - ! Only read target shape - call json % get(layer_config_json, & - 'target_shape', res(n) % target_shape, found) - ! Reverse to account for C -> Fortran order - res(n) % target_shape = reverse(res(n) % target_shape) - - case default - error stop 'This Keras layer is not supported' - - end select - - end do layers - - ! free the memory: - call json % destroy(model_config_json) - - end function get_keras_h5_layers - -end submodule nf_keras_submodule diff --git a/src/nf/nf_network_submodule.f90 b/src/nf/nf_network_submodule.f90 index ff8ba78c..e52051aa 100644 --- a/src/nf/nf_network_submodule.f90 +++ b/src/nf/nf_network_submodule.f90 @@ -7,8 +7,6 @@ use nf_input3d_layer, only: input3d_layer use nf_maxpool2d_layer, only: maxpool2d_layer use nf_reshape_layer, only: reshape3d_layer - 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: conv2d, dense, flatten, input, maxpool2d, reshape use nf_loss, only: quadratic @@ -95,191 +93,6 @@ module function network_from_layers(layers) result(res) end function network_from_layers - module function network_from_keras(filename) result(res) - character(*), intent(in) :: filename - type(network) :: res - type(keras_layer), allocatable :: keras_layers(:) - type(layer), allocatable :: layers(:) - character(:), allocatable :: layer_name - character(:), allocatable :: object_name - integer :: n - - keras_layers = get_keras_h5_layers(filename) - - allocate(layers(size(keras_layers))) - - do n = 1, size(layers) - - 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), & - get_activation_by_name(keras_layers(n) % activation) & - ) - - case('Dense') - - layers(n) = dense( & - keras_layers(n) % units(1), & - get_activation_by_name(keras_layers(n) % activation) & - ) - - case('Flatten') - layers(n) = flatten() - - case('InputLayer') - if (size(keras_layers(n) % units) == 1) then - ! input1d - layers(n) = input(keras_layers(n) % units(1)) - else - ! input3d - layers(n) = input(keras_layers(n) % units) - end if - - 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('Reshape') - layers(n) = reshape(keras_layers(n) % target_shape) - - case default - error stop 'This Keras layer is not supported' - - end select - - end do - - res = network(layers) - - ! Loop over layers and read weights and biases from the Keras h5 file - ! for each; currently only dense layers are implemented. - do n = 2, size(res % layers) - - layer_name = keras_layers(n) % name - - 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 - 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 % weights) - - type is(flatten_layer) - ! Nothing to do - continue - - type is(maxpool2d_layer) - ! Nothing to do - continue - - type is(reshape3d_layer) - ! Nothing to do - continue - - class default - error stop 'Internal error in network_from_keras(); ' & - // 'mismatch in layer types between the Keras and ' & - // 'neural-fortran model layers.' - - end select - - end do - - end function network_from_keras - - - pure function get_activation_by_name(activation_name) result(res) - ! Workaround to get activation_function with some - ! hardcoded default parameters by its name. - ! Need this function since we get only activation name - ! from keras files. - character(len=*), intent(in) :: activation_name - class(activation_function), allocatable :: res - - select case(trim(activation_name)) - case('elu') - allocate ( res, source = elu(alpha = 0.1) ) - - case('exponential') - allocate ( res, source = exponential() ) - - case('gaussian') - allocate ( res, source = gaussian() ) - - case('linear') - allocate ( res, source = linear() ) - - case('relu') - allocate ( res, source = relu() ) - - case('leaky_relu') - allocate ( res, source = leaky_relu(alpha = 0.1) ) - - case('sigmoid') - allocate ( res, source = sigmoid() ) - - case('softmax') - allocate ( res, source = softmax() ) - - case('softplus') - allocate ( res, source = softplus() ) - - case('step') - allocate ( res, source = step() ) - - case('tanh') - allocate ( res, source = tanhf() ) - - case('celu') - allocate ( res, source = celu() ) - - case default - error stop 'activation_name must be one of: ' // & - '"elu", "exponential", "gaussian", "linear", "relu", ' // & - '"leaky_relu", "sigmoid", "softmax", "softplus", "step", "tanh" or "celu".' - end select - - end function get_activation_by_name - pure module subroutine backward(self, output, loss) class(network), intent(in out) :: self real, intent(in) :: output(:) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1be8bb8d..bfd3538a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -10,21 +10,13 @@ foreach(execid reshape_layer dense_network get_set_network_params - io_hdf5 - keras_read_model - dense_network_from_keras - cnn_from_keras conv2d_network optimizers loss metrics ) add_executable(test_${execid} test_${execid}.f90) - target_link_libraries(test_${execid} PRIVATE neural-fortran h5fortran::h5fortran jsonfortran::jsonfortran ${LIBS}) + target_link_libraries(test_${execid} PRIVATE neural-fortran ${LIBS}) add_test(NAME test_${execid} COMMAND test_${execid}) endforeach() - -set_tests_properties(test_dense_network_from_keras test_io_hdf5 test_keras_read_model PROPERTIES -RESOURCE_LOCK download -) diff --git a/test/test_cnn_from_keras.f90 b/test/test_cnn_from_keras.f90 deleted file mode 100644 index 4d93dc51..00000000 --- a/test/test_cnn_from_keras.f90 +++ /dev/null @@ -1,69 +0,0 @@ -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) - - 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, allocatable :: input_reshaped(:,:,:,:) - real :: acc - - call load_mnist(training_images, training_labels, & - validation_images, validation_labels, & - testing_images, testing_labels) - - ! Use only the first 1000 images to make the test short - input_reshaped = reshape(testing_images(:,:1000), shape=[1,28,28,1000]) - - 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.97.. 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=4) - if (all(maxloc(net % predict(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 test_cnn_from_keras diff --git a/test/test_dense_network_from_keras.f90 b/test/test_dense_network_from_keras.f90 deleted file mode 100644 index ba247e38..00000000 --- a/test/test_dense_network_from_keras.f90 +++ /dev/null @@ -1,100 +0,0 @@ -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_dense_mnist_url - - implicit none - - type(network) :: net - character(*), parameter :: test_data_path = 'keras_dense_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_dense_mnist_url) - - net = network(test_data_path) - - if (.not. size(net % layers) == 3) then - write(stderr, '(a)') 'dense network should have 3 layers.. failed' - ok = .false. - end if - - if (.not. net % layers(1) % name == 'input') then - write(stderr, '(a)') 'First layer should be an input layer.. failed' - ok = .false. - end if - - if (.not. all(net % layers(1) % layer_shape == [784])) then - write(stderr, '(a)') 'First layer should have shape [784].. failed' - ok = .false. - end if - - if (.not. net % layers(2) % name == 'dense') then - write(stderr, '(a)') 'Second layer should be a dense layer.. failed' - ok = .false. - end if - - if (.not. all(net % layers(2) % layer_shape == [30])) then - write(stderr, '(a)') 'Second layer should have shape [30].. failed' - ok = .false. - end if - - if (.not. net % layers(3) % name == 'dense') then - write(stderr, '(a)') 'Third layer should be a dense layer.. failed' - ok = .false. - end if - - if (.not. all(net % layers(3) % layer_shape == [10])) then - write(stderr, '(a)') 'Third layer should have shape [10].. failed' - ok = .false. - end if - - 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, testing_images, label_digits(testing_labels)) - - 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_dense_network_from_keras: All tests passed.' - else - write(stderr, '(a)') & - 'test_dense_network_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 % predict(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_dense_network_from_keras diff --git a/test/test_io_hdf5.f90 b/test/test_io_hdf5.f90 deleted file mode 100644 index e7e0fc88..00000000 --- a/test/test_io_hdf5.f90 +++ /dev/null @@ -1,57 +0,0 @@ -program test_io_hdf5 - - use iso_fortran_env, only: stderr => error_unit - use nf_datasets, only: download_and_unpack, keras_dense_mnist_url - use nf_io_hdf5, only: hdf5_attribute_string, get_hdf5_dataset - - implicit none - - character(:), allocatable :: attr - character(*), parameter :: test_data_path = 'keras_dense_mnist.h5' - real, allocatable :: bias(:), weights(:,:) - - logical :: file_exists - logical :: ok = .true. - - inquire(file=test_data_path, exist=file_exists) - if (.not. file_exists) call download_and_unpack(keras_dense_mnist_url) - - attr = hdf5_attribute_string(test_data_path, '.', 'backend') - - if (.not. attr == 'tensorflow') then - ok = .false. - write(stderr, '(a)') & - 'HDF5 variable length string attribute was read correctly.. failed' - end if - - ! Read 1-d real32 dataset - call get_hdf5_dataset( & - test_data_path, & - '/model_weights/dense/dense/bias:0', & - bias & - ) - - ! Read 2-d real32 dataset - call get_hdf5_dataset(test_data_path, & - '/model_weights/dense/dense/kernel:0', & - weights & - ) - - if (.not. all(shape(bias) == [30])) then - ok = .false. - write(stderr, '(a)') 'HDF5 1-d dataset dims inquiry is correct.. failed' - end if - - if (.not. all(shape(weights) == [784, 30])) then - ok = .false. - write(stderr, '(a)') 'HDF5 2-d dataset dims inquiry is correct.. failed' - end if - - if (ok) then - print '(a)', 'test_io_hdf5: All tests passed.' - else - write(stderr, '(a)') 'test_io_hdf5: One or more tests failed.' - stop 1 - end if - -end program test_io_hdf5 diff --git a/test/test_keras_read_model.f90 b/test/test_keras_read_model.f90 deleted file mode 100644 index ba021a5a..00000000 --- a/test/test_keras_read_model.f90 +++ /dev/null @@ -1,108 +0,0 @@ -program test_keras_read_model - - use iso_fortran_env, only: stderr => error_unit - 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 - - implicit none - - character(*), parameter :: keras_dense_path = 'keras_dense_mnist.h5' - character(*), parameter :: keras_cnn_path = 'keras_cnn_mnist.h5' - - type(keras_layer), allocatable :: keras_layers(:) - - logical :: file_exists - logical :: ok = .true. - - ! First test the dense model - - 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. - write(stderr, '(a)') 'Keras dense MNIST model has 3 layers.. failed' - end if - - if (keras_layers(1) % class /= 'InputLayer') then - ok = .false. - write(stderr, '(a)') 'Keras first layer should be InputLayer.. failed' - end if - - if (.not. all(keras_layers(1) % units == [784])) then - ok = .false. - write(stderr, '(a)') 'Keras first layer should have 784 elements.. failed' - end if - - if (allocated(keras_layers(1) % activation)) then - ok = .false. - write(stderr, '(a)') & - 'Keras first layer activation should not be allocated.. failed' - end if - - if (.not. keras_layers(2) % class == 'Dense') then - ok = .false. - write(stderr, '(a)') & - '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 (.not. all(keras_layers(1) % units == [1, 28, 28])) 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 - write(stderr, '(a)') 'test_keras_read_model: One or more tests failed.' - stop 1 - end if - -end program test_keras_read_model diff --git a/test/test_reshape_layer.f90 b/test/test_reshape_layer.f90 index 405afd98..a448fc12 100644 --- a/test/test_reshape_layer.f90 +++ b/test/test_reshape_layer.f90 @@ -43,31 +43,6 @@ program test_reshape_layer ok = .false. end if - ! Now test reading the reshape layer from a Keras h5 model. - inquire(file=keras_reshape_path, exist=file_exists) - if (.not. file_exists) call download_and_unpack(keras_reshape_url) - - net = network(keras_reshape_path) - - if (.not. size(net % layers) == 2) then - write(stderr, '(a)') 'the reshape network from Keras has the correct size.. failed' - ok = .false. - end if - - if (.not. net % layers(2) % name == 'reshape') then - write(stderr, '(a)') 'the 2nd layer of the reshape network from Keras is a reshape layer.. failed' - ok = .false. - end if - - ! Test that the output shape checks out - call net % layers(1) % get_output(sample_input) - call net % layers(2) % get_output(output) - - if (.not. all(shape(output) == [1, 28, 28])) then - write(stderr, '(a)') 'the target shape of the reshape layer is correct.. failed' - ok = .false. - end if - if (ok) then print '(a)', 'test_reshape_layer: All tests passed.' else