diff --git a/CMakeLists_files.cmake b/CMakeLists_files.cmake index 6f5ebfbd5f7..877962a2a76 100644 --- a/CMakeLists_files.cmake +++ b/CMakeLists_files.cmake @@ -62,6 +62,7 @@ list (APPEND MAIN_SOURCE_FILES opm/material/fluidsystems/blackoilpvt/SolventPvt.cpp opm/material/fluidsystems/blackoilpvt/WetGasPvt.cpp opm/material/fluidsystems/blackoilpvt/WetHumidGasPvt.cpp + opm/ml/keras_model.cpp ) if(ENABLE_ECL_INPUT) list(APPEND MAIN_SOURCE_FILES @@ -474,6 +475,7 @@ list (APPEND TEST_SOURCE_FILES tests/material/test_spline.cpp tests/material/test_tabulation.cpp tests/test_Visitor.cpp + tests/ml/keras_model_test.cpp ) # tests that need to be linked to dune-common @@ -646,6 +648,15 @@ list (APPEND TEST_DATA_FILES tests/material/co2_unittest_below_sat.json tests/material/h2o_unittest.json tests/material/h2_unittest.json + tests/ml/ml_tools/models/test_dense_1x1.model + tests/ml/ml_tools/models/test_dense_2x2.model + tests/ml/ml_tools/models/test_dense_10x1.model + tests/ml/ml_tools/models/test_dense_10x10.model + tests/ml/ml_tools/models/test_dense_10x10x10.model + tests/ml/ml_tools/models/test_dense_relu_10.model + tests/ml/ml_tools/models/test_dense_tanh_10.model + tests/ml/ml_tools/models/test_relu_10.model + tests/ml/ml_tools/models/test_scalingdense_10x1.model ) if(ENABLE_ECL_OUTPUT) list (APPEND TEST_DATA_FILES @@ -1052,6 +1063,7 @@ list( APPEND PUBLIC_HEADER_FILES opm/material/thermal/SomertonThermalConductionLaw.hpp opm/material/thermal/EclSpecrockLaw.hpp opm/material/thermal/NullSolidEnergyLaw.hpp + opm/ml/keras_model.hpp ) if(ENABLE_ECL_INPUT) diff --git a/opm/ml/LICENSE.MIT b/opm/ml/LICENSE.MIT new file mode 100644 index 00000000000..6908c245ebf --- /dev/null +++ b/opm/ml/LICENSE.MIT @@ -0,0 +1,19 @@ +Copyright (c) 2016 Robert W. Rose, 2018 Paul Maevskikh, 2024 NORCE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/opm/ml/keras_model.cpp b/opm/ml/keras_model.cpp new file mode 100644 index 00000000000..0aaf3a832ee --- /dev/null +++ b/opm/ml/keras_model.cpp @@ -0,0 +1,419 @@ +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include "keras_model.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace Opm { + + +bool ReadUnsignedInt(std::ifstream* file, unsigned int* i) { + KASSERT(file, "Invalid file stream"); + KASSERT(i, "Invalid pointer"); + + file->read((char*)i, sizeof(unsigned int)); + KASSERT(file->gcount() == sizeof(unsigned int), "Expected unsigned int"); + + return true; +} + +bool ReadFloat(std::ifstream* file, float* f) { + KASSERT(file, "Invalid file stream"); + KASSERT(f, "Invalid pointer"); + + file->read((char*)f, sizeof(float)); + KASSERT(file->gcount() == sizeof(float), "Expected Evaluation"); + + return true; +} + +bool ReadFloats(std::ifstream* file, float* f, size_t n) { + KASSERT(file, "Invalid file stream"); + KASSERT(f, "Invalid pointer"); + + file->read((char*)f, sizeof(float) * n); + KASSERT(((unsigned int)file->gcount()) == sizeof(float) * n, + "Expected Evaluations"); + + return true; +} + +template +bool KerasLayerActivation::LoadLayer(std::ifstream* file) { + KASSERT(file, "Invalid file stream"); + + unsigned int activation = 0; + KASSERT(ReadUnsignedInt(file, &activation), + "Failed to read activation type"); + + switch (activation) { + case kLinear: + activation_type_ = kLinear; + break; + case kRelu: + activation_type_ = kRelu; + break; + case kSoftPlus: + activation_type_ = kSoftPlus; + break; + case kHardSigmoid: + activation_type_ = kHardSigmoid; + break; + case kSigmoid: + activation_type_ = kSigmoid; + break; + case kTanh: + activation_type_ = kTanh; + break; + default: + KASSERT(false, "Unsupported activation type %d", activation); + } + + return true; +} + +template +bool KerasLayerActivation::Apply(Tensor* in, Tensor* out) { + KASSERT(in, "Invalid input"); + KASSERT(out, "Invalid output"); + + *out = *in; + + switch (activation_type_) { + case kLinear: + break; + case kRelu: + for (size_t i = 0; i < out->data_.size(); i++) { + if (out->data_[i] < 0.0) { + out->data_[i] = 0.0; + } + } + break; + case kSoftPlus: + for (size_t i = 0; i < out->data_.size(); i++) { + out->data_[i] = log(1.0 + exp(out->data_[i])); + } + break; + case kHardSigmoid: + for (size_t i = 0; i < out->data_.size(); i++) { + Evaluation x = (out->data_[i] * 0.2) + 0.5; + + if (x <= 0) { + out->data_[i] = 0.0; + } else if (x >= 1) { + out->data_[i] = 1.0; + } else { + out->data_[i] = x; + } + } + break; + case kSigmoid: + for (size_t i = 0; i < out->data_.size(); i++) { + Evaluation& x = out->data_[i]; + + if (x >= 0) { + out->data_[i] = 1.0 / (1.0 + exp(-x)); + } else { + Evaluation z = exp(x); + out->data_[i] = z / (1.0 + z); + } + } + break; + case kTanh: + for (size_t i = 0; i < out->data_.size(); i++) { + out->data_[i] = sinh(out->data_[i])/cosh(out->data_[i]); + } + break; + default: + break; + } + + return true; +} + + +template +bool KerasLayerScaling::LoadLayer(std::ifstream* file) { + KASSERT(file, "Invalid file stream"); + + KASSERT(ReadFloat(file, &data_min), "Failed to read min"); + KASSERT(ReadFloat(file, &data_max), "Failed to read max"); + KASSERT(ReadFloat(file, &feat_inf), "Failed to read max"); + KASSERT(ReadFloat(file, &feat_sup), "Failed to read max"); + return true; +} + +template +bool KerasLayerScaling::Apply(Tensor* in, Tensor* out) { + KASSERT(in, "Invalid input"); + KASSERT(out, "Invalid output"); + + Tensor temp_in, temp_out; + + *out = *in; + + for (size_t i = 0; i < out->data_.size(); i++) { + auto tempscale = (out->data_[i] - data_min)/(data_max - data_min); + out->data_[i] = tempscale * (feat_sup - feat_inf) + feat_inf; + } + + return true; +} + +template +bool KerasLayerUnScaling::LoadLayer(std::ifstream* file) { + KASSERT(file, "Invalid file stream"); + + KASSERT(ReadFloat(file, &data_min), "Failed to read min"); + KASSERT(ReadFloat(file, &data_max), "Failed to read max"); + KASSERT(ReadFloat(file, &feat_inf), "Failed to read inf"); + KASSERT(ReadFloat(file, &feat_sup), "Failed to read sup"); + + return true; +} + +template +bool KerasLayerUnScaling::Apply(Tensor* in, Tensor* out) { + KASSERT(in, "Invalid input"); + KASSERT(out, "Invalid output"); + + *out = *in; + + for (size_t i = 0; i < out->data_.size(); i++) { + auto tempscale = (out->data_[i] - feat_inf)/(feat_sup - feat_inf); + + out->data_[i] = tempscale * (data_max - data_min) + data_min; + } + + + return true; +} + + +template +bool KerasLayerDense::LoadLayer(std::ifstream* file) { + KASSERT(file, "Invalid file stream"); + + unsigned int weights_rows = 0; + KASSERT(ReadUnsignedInt(file, &weights_rows), "Expected weight rows"); + KASSERT(weights_rows > 0, "Invalid weights # rows"); + + unsigned int weights_cols = 0; + KASSERT(ReadUnsignedInt(file, &weights_cols), "Expected weight cols"); + KASSERT(weights_cols > 0, "Invalid weights shape"); + + unsigned int biases_shape = 0; + KASSERT(ReadUnsignedInt(file, &biases_shape), "Expected biases shape"); + KASSERT(biases_shape > 0, "Invalid biases shape"); + + weights_.Resize(weights_rows, weights_cols); + KASSERT( + ReadFloats(file, weights_.data_.data(), weights_rows * weights_cols), + "Expected weights"); + + biases_.Resize(biases_shape); + KASSERT(ReadFloats(file, biases_.data_.data(), biases_shape), + "Expected biases"); + + KASSERT(activation_.LoadLayer(file), "Failed to load activation"); + + return true; +} + +template +bool KerasLayerDense::Apply(Tensor* in, Tensor* out) { + KASSERT(in, "Invalid input"); + KASSERT(out, "Invalid output"); + KASSERT(in->dims_.size() <= 2, "Invalid input dimensions"); + + if (in->dims_.size() == 2) { + KASSERT(in->dims_[1] == weights_.dims_[0], "Dimension mismatch %d %d", + in->dims_[1], weights_.dims_[0]); + } + + Tensor tmp(weights_.dims_[1]); + + for (int i = 0; i < weights_.dims_[0]; i++) { + for (int j = 0; j < weights_.dims_[1]; j++) { + tmp(j) += (*in)(i)*weights_(i, j); + } + } + + for (int i = 0; i < biases_.dims_[0]; i++) { + tmp(i) += biases_(i); + } + + KASSERT(activation_.Apply(&tmp, out), "Failed to apply activation"); + + return true; +} + +template +bool KerasLayerFlatten::LoadLayer(std::ifstream* file) { + KASSERT(file, "Invalid file stream"); + return true; +} + +template +bool KerasLayerFlatten::Apply(Tensor* in, Tensor* out) { + KASSERT(in, "Invalid input"); + KASSERT(out, "Invalid output"); + + *out = *in; + out->Flatten(); + + return true; +} + + + +template +bool KerasLayerEmbedding::LoadLayer(std::ifstream* file) { + KASSERT(file, "Invalid file stream"); + + unsigned int weights_rows = 0; + KASSERT(ReadUnsignedInt(file, &weights_rows), "Expected weight rows"); + KASSERT(weights_rows > 0, "Invalid weights # rows"); + + unsigned int weights_cols = 0; + KASSERT(ReadUnsignedInt(file, &weights_cols), "Expected weight cols"); + KASSERT(weights_cols > 0, "Invalid weights shape"); + + weights_.Resize(weights_rows, weights_cols); + KASSERT( + ReadFloats(file, weights_.data_.data(), weights_rows * weights_cols), + "Expected weights"); + + return true; +} + +template +bool KerasLayerEmbedding::Apply(Tensor* in, Tensor* out) { + int output_rows = in->dims_[1]; + int output_cols = weights_.dims_[1]; + out->dims_ = {output_rows, output_cols}; + out->data_.reserve(output_rows * output_cols); + + std::for_each(in->data_.begin(), in->data_.end(), [=](Evaluation i) { + typename std::vector::const_iterator first = + this->weights_.data_.begin() + (getValue(i) * output_cols); + typename std::vector::const_iterator last = + this->weights_.data_.begin() + (getValue(i) + 1) * output_cols; + + out->data_.insert(out->data_.end(), first, last); + }); + + return true; +} + + +template +bool KerasModel::LoadModel(const std::string& filename) { + std::ifstream file(filename.c_str(), std::ios::binary); + KASSERT(file.is_open(), "Unable to open file %s", filename.c_str()); + + unsigned int num_layers = 0; + KASSERT(ReadUnsignedInt(&file, &num_layers), "Expected number of layers"); + + for (unsigned int i = 0; i < num_layers; i++) { + unsigned int layer_type = 0; + KASSERT(ReadUnsignedInt(&file, &layer_type), "Expected layer type"); + + KerasLayer* layer = NULL; + + switch (layer_type) { + case kFlatten: + layer = new KerasLayerFlatten(); + break; + case kScaling: + layer = new KerasLayerScaling(); + break; + case kUnScaling: + layer = new KerasLayerUnScaling(); + break; + case kDense: + layer = new KerasLayerDense(); + break; + case kActivation: + layer = new KerasLayerActivation(); + break; + default: + break; + } + + KASSERT(layer, "Unknown layer type %d", layer_type); + + bool result = layer->LoadLayer(&file); + if (!result) { + printf("Failed to load layer %d", i); + delete layer; + return false; + } + + layers_.push_back(layer); + } + + return true; +} + +template +bool KerasModel::Apply(Tensor* in, Tensor* out) { + Tensor temp_in, temp_out; + + for (unsigned int i = 0; i < layers_.size(); i++) { + if (i == 0) { + temp_in = *in; + } + + KASSERT(layers_[i]->Apply(&temp_in, &temp_out), + "Failed to apply layer %d", i); + + temp_in = temp_out; + } + + *out = temp_out; + + return true; +} + +template class KerasModel; + +template class KerasModel; +template class KerasModel>; +template class KerasModel>; +template class KerasModel>; +template class KerasModel>; +template class KerasModel>; +template class KerasModel>; + +} // namespace Opm diff --git a/opm/ml/keras_model.hpp b/opm/ml/keras_model.hpp new file mode 100644 index 00000000000..40a77ce81f3 --- /dev/null +++ b/opm/ml/keras_model.hpp @@ -0,0 +1,408 @@ +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#ifndef KERAS_MODEL_H_ +#define KERAS_MODEL_H_ + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace Opm { + +#define KASSERT(x, ...) \ + if (!(x)) { \ + printf("KASSERT: %s(%d): ", __FILE__, __LINE__); \ + printf(__VA_ARGS__); \ + printf("\n"); \ + return false; \ + } + +#define KASSERT_EQ(x, y, eps) \ + if (fabs(x.value() - y.value()) > eps) { \ + printf("KASSERT: Expected %f, got %f\n", y.value(), x.value()); \ + return false; \ + } + +#ifdef DEBUG +#define KDEBUG(x, ...) \ + if (!(x)) { \ + printf("%s(%d): ", __FILE__, __LINE__); \ + printf(__VA_ARGS__); \ + printf("\n"); \ + exit(-1); \ + } +#else +#define KDEBUG(x, ...) ; +#endif + +template +class Tensor { + public: + Tensor() {} + + Tensor(int i) { Resize(i); } + + Tensor(int i, int j) { Resize(i, j); } + + Tensor(int i, int j, int k) { Resize(i, j, k); } + + Tensor(int i, int j, int k, int l) { Resize(i, j, k, l); } + + void Resize(int i) { + dims_ = {i}; + data_.resize(i); + } + + void Resize(int i, int j) { + dims_ = {i, j}; + data_.resize(i * j); + } + + void Resize(int i, int j, int k) { + dims_ = {i, j, k}; + data_.resize(i * j * k); + } + + void Resize(int i, int j, int k, int l) { + dims_ = {i, j, k, l}; + data_.resize(i * j * k * l); + } + + inline void Flatten() { + KDEBUG(dims_.size() > 0, "Invalid tensor"); + + int elements = dims_[0]; + for (unsigned int i = 1; i < dims_.size(); i++) { + elements *= dims_[i]; + } + dims_ = {elements}; + } + + inline Foo& operator()(int i) { + KDEBUG(dims_.size() == 1, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + + return data_[i]; + } + + inline Foo& operator()(int i, int j) { + KDEBUG(dims_.size() == 2, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + KDEBUG(j < dims_[1] && j >= 0, "Invalid j: %d (max %d)", j, dims_[1]); + + return data_[dims_[1] * i + j]; + } + + inline Foo operator()(int i, int j) const { + KDEBUG(dims_.size() == 2, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + KDEBUG(j < dims_[1] && j >= 0, "Invalid j: %d (max %d)", j, dims_[1]); + + return data_[dims_[1] * i + j]; + } + + inline Foo& operator()(int i, int j, int k) { + KDEBUG(dims_.size() == 3, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + KDEBUG(j < dims_[1] && j >= 0, "Invalid j: %d (max %d)", j, dims_[1]); + KDEBUG(k < dims_[2] && k >= 0, "Invalid k: %d (max %d)", k, dims_[2]); + + return data_[dims_[2] * (dims_[1] * i + j) + k]; + } + + inline Foo& operator()(int i, int j, int k, int l) { + KDEBUG(dims_.size() == 4, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + KDEBUG(j < dims_[1] && j >= 0, "Invalid j: %d (max %d)", j, dims_[1]); + KDEBUG(k < dims_[2] && k >= 0, "Invalid k: %d (max %d)", k, dims_[2]); + KDEBUG(l < dims_[3] && l >= 0, "Invalid l: %d (max %d)", l, dims_[3]); + + return data_[dims_[3] * (dims_[2] * (dims_[1] * i + j) + k) + l]; + } + + inline void Fill(Foo value) { + std::fill(data_.begin(), data_.end(), value); + } + + Tensor Unpack(int row) const { + KASSERT(dims_.size() >= 2, "Invalid tensor"); + std::vector pack_dims = + std::vector(dims_.begin() + 1, dims_.end()); + int pack_size = std::accumulate(pack_dims.begin(), pack_dims.end(), 0); + + typename std::vector::const_iterator first = + data_.begin() + (row * pack_size); + typename std::vector::const_iterator last = + data_.begin() + (row + 1) * pack_size; + + Tensor x = Tensor(); + x.dims_ = pack_dims; + x.data_ = std::vector(first, last); + + return x; + } + + Tensor Select(int row) const { + Tensor x = Unpack(row); + x.dims_.insert(x.dims_.begin(), 1); + + return x; + } + + Tensor operator+(const Tensor& other) { + KASSERT(dims_ == other.dims_, + "Cannot add tensors with different dimensions"); + + Tensor result; + result.dims_ = dims_; + result.data_.reserve(data_.size()); + + std::transform(data_.begin(), data_.end(), other.data_.begin(), + std::back_inserter(result.data_), + [](Foo x, Foo y) { return x + y; }); + + return result; + } + + Tensor Multiply(const Tensor& other) { + KASSERT(dims_ == other.dims_, + "Cannot multiply elements with different dimensions"); + + Tensor result; + result.dims_ = dims_; + result.data_.reserve(data_.size()); + + std::transform(data_.begin(), data_.end(), other.data_.begin(), + std::back_inserter(result.data_), + [](Foo x, Foo y) { return x * y; }); + + return result; + } + + Tensor Dot(const Tensor& other) { + KDEBUG(dims_.size() == 2, "Invalid tensor dimensions"); + KDEBUG(other.dims_.size() == 2, "Invalid tensor dimensions"); + KASSERT(dims_[1] == other.dims_[0], + "Cannot multiply with different inner dimensions"); + + Tensor tmp(dims_[0], other.dims_[1]); + + for (int i = 0; i < dims_[0]; i++) { + for (int j = 0; j < other.dims_[1]; j++) { + for (int k = 0; k < dims_[1]; k++) { + tmp(i, j) += (*this)(i, k) * other(k, j); + } + } + } + + return tmp; + } + + std::vector dims_; + std::vector data_; +}; + +template +class KerasLayer { + public: + KerasLayer() {} + + virtual ~KerasLayer() {} + + virtual bool LoadLayer(std::ifstream* file) = 0; + + virtual bool Apply(Tensor* in, Tensor* out) = 0; +}; + +template +class KerasLayerActivation : public KerasLayer { + public: + enum ActivationType { + kLinear = 1, + kRelu = 2, + kSoftPlus = 3, + kSigmoid = 4, + kTanh = 5, + kHardSigmoid = 6 + }; + + KerasLayerActivation() : activation_type_(ActivationType::kLinear) {} + + virtual ~KerasLayerActivation() {} + + virtual bool LoadLayer(std::ifstream* file); + + virtual bool Apply(Tensor* in, Tensor* out); + + private: + ActivationType activation_type_; +}; + +template +class KerasLayerScaling : public KerasLayer { + public: + KerasLayerScaling(): data_min(1.0f), data_max(1.0f), feat_inf(1.0f), feat_sup(1.0f) {} + + virtual ~KerasLayerScaling() {} + + virtual bool LoadLayer(std::ifstream* file); + + virtual bool Apply(Tensor* in, Tensor* out); + + private: + Tensor weights_; + Tensor biases_; + float data_min; + float data_max; + float feat_inf; + float feat_sup; +}; + +template +class KerasLayerUnScaling : public KerasLayer { + public: + KerasLayerUnScaling(): data_min(1.0f), data_max(1.0f), feat_inf(1.0f), feat_sup(1.0f) {} + + virtual ~KerasLayerUnScaling() {} + + virtual bool LoadLayer(std::ifstream* file); + + virtual bool Apply(Tensor* in, Tensor* out); + + private: + Tensor weights_; + Tensor biases_; + float data_min; + float data_max; + float feat_inf; + float feat_sup; +}; + + +template +class KerasLayerDense : public KerasLayer { + public: + KerasLayerDense() {} + + virtual ~KerasLayerDense() {} + + virtual bool LoadLayer(std::ifstream* file); + + virtual bool Apply(Tensor* in, Tensor* out); + + private: + Tensor weights_; + Tensor biases_; + + KerasLayerActivation activation_; +}; + +template +class KerasLayerFlatten : public KerasLayer { + public: + KerasLayerFlatten() {} + + virtual ~KerasLayerFlatten() {} + + virtual bool LoadLayer(std::ifstream* file); + + virtual bool Apply(Tensor* in, Tensor* out); + + private: +}; + + +template +class KerasLayerEmbedding : public KerasLayer { + public: + KerasLayerEmbedding() {} + + virtual ~KerasLayerEmbedding() {} + + virtual bool LoadLayer(std::ifstream* file); + + virtual bool Apply(Tensor* in, Tensor* out); + + private: + Tensor weights_; +}; + +template +class KerasModel { + public: + enum LayerType { + kFlatten = 1, + kScaling = 2, + kUnScaling = 3, + kDense = 4, + kActivation = 5 + }; + + KerasModel() {} + + virtual ~KerasModel() { + for (unsigned int i = 0; i < layers_.size(); i++) { + delete layers_[i]; + } + } + + virtual bool LoadModel(const std::string& filename); + + virtual bool Apply(Tensor* in, Tensor* out); + + private: + std::vector*> layers_; +}; + +class KerasTimer { + public: + KerasTimer() {} + + void Start() { start_ = std::chrono::high_resolution_clock::now(); } + + float Stop() { + std::chrono::time_point now = + std::chrono::high_resolution_clock::now(); + + std::chrono::duration diff = now - start_; + + return diff.count(); + } + + private: + std::chrono::time_point start_; +}; + +} // namespace Opm + +#endif // KERAS_MODEL_H_ diff --git a/opm/ml/ml_model.cpp b/opm/ml/ml_model.cpp new file mode 100644 index 00000000000..0552568b91e --- /dev/null +++ b/opm/ml/ml_model.cpp @@ -0,0 +1,444 @@ +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include "ml_model.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace Opm { + + +template +bool readFile(std::ifstream& file, T& data, size_t n=1) +{ + file.read(reinterpret_cast(&data), sizeof (T)* n); + return !file.fail(); +} + + +// bool ReadUnsignedInt(std::ifstream* file, unsigned int* i) { +// // KASSERT(file, "Invalid file stream"); +// // // KASSERT(i, "Invalid pointer"); + +// // KASSERT(file, "Invalid file stream"); + +// OPM_ERROR_IF(!file, +// "Invalid file stream"); + +// OPM_ERROR_IF(!i, +// "Invalid pointer"); + +// file->read((char*)i, sizeof(unsigned int)); +// KASSERT(file->gcount() == sizeof(unsigned int), "Expected unsigned int"); + +// return true; +// } + +// bool ReadFloat(std::ifstream* file, float* f) { +// KASSERT(file, "Invalid file stream"); +// KASSERT(f, "Invalid pointer"); + +// file->read((char*)f, sizeof(float)); +// KASSERT(file->gcount() == sizeof(float), "Expected Evaluation"); + +// return true; +// } + +bool ReadFloats(std::ifstream* file, float* f, size_t n) { + KASSERT(file, "Invalid file stream"); + KASSERT(f, "Invalid pointer"); + + file->read((char*)f, sizeof(float) * n); + KASSERT(((unsigned int)file->gcount()) == sizeof(float) * n, + "Expected Evaluations"); + + return true; +} + +template +bool KerasLayerActivation::loadLayer(std::ifstream& file) { + // KASSERT(file, "Invalid file stream"); + + unsigned int activation = 0; + KASSERT(readFile(file, activation), + "Failed to read activation type"); + + switch (activation) { + case kLinear: + activation_type_ = kLinear; + break; + case kRelu: + activation_type_ = kRelu; + break; + case kSoftPlus: + activation_type_ = kSoftPlus; + break; + case kHardSigmoid: + activation_type_ = kHardSigmoid; + break; + case kSigmoid: + activation_type_ = kSigmoid; + break; + case kTanh: + activation_type_ = kTanh; + break; + default: + KASSERT(false, "Unsupported activation type %d", activation); + } + + return true; +} + +template +bool KerasLayerActivation::apply(Tensor& in, Tensor& out) { + // KASSERT(in, "Invalid input"); + // KASSERT(out, "Invalid output"); + + out = in; + + switch (activation_type_) { + case kLinear: + break; + case kRelu: + for (size_t i = 0; i < out.data_.size(); i++) { + if (out.data_[i] < 0.0) { + out.data_[i] = 0.0; + } + } + break; + case kSoftPlus: + for (size_t i = 0; i < out.data_.size(); i++) { + out.data_[i] = log(1.0 + exp(out.data_[i])); + } + break; + case kHardSigmoid: + for (size_t i = 0; i < out.data_.size(); i++) { + Evaluation x = (out.data_[i] * 0.2) + 0.5; + + if (x <= 0) { + out.data_[i] = 0.0; + } else if (x >= 1) { + out.data_[i] = 1.0; + } else { + out.data_[i] = x; + } + } + break; + case kSigmoid: + for (size_t i = 0; i < out.data_.size(); i++) { + Evaluation& x = out.data_[i]; + + if (x >= 0) { + out.data_[i] = 1.0 / (1.0 + exp(-x)); + } else { + Evaluation z = exp(x); + out.data_[i] = z / (1.0 + z); + } + } + break; + case kTanh: + for (size_t i = 0; i < out.data_.size(); i++) { + out.data_[i] = sinh(out.data_[i])/cosh(out.data_[i]); + } + break; + default: + break; + } + + return true; +} + + +template +bool KerasLayerScaling::loadLayer(std::ifstream& file) { + KASSERT(file, "Invalid file stream"); + + KASSERT(readFile(file, data_min), "Failed to read min"); + KASSERT(readFile(file, data_max), "Failed to read min"); + KASSERT(readFile(file, feat_inf), "Failed to read min"); + KASSERT(readFile(file, feat_sup), "Failed to read min"); + return true; +} + +template +bool KerasLayerScaling::apply(Tensor& in, Tensor& out) { + // KASSERT(in, "Invalid input"); + // KASSERT(out, "Invalid output"); + + Tensor temp_in, temp_out; + + out = in; + + for (size_t i = 0; i < out.data_.size(); i++) { + auto tempscale = (out.data_[i] - data_min)/(data_max - data_min); + out.data_[i] = tempscale * (feat_sup - feat_inf) + feat_inf; + } + + return true; +} + +template +bool KerasLayerUnScaling::loadLayer(std::ifstream& file) { + KASSERT(file, "Invalid file stream"); + + KASSERT(readFile(file, data_min), "Failed to read min"); + KASSERT(readFile(file, data_max), "Failed to read max"); + KASSERT(readFile(file, feat_inf), "Failed to read inf"); + KASSERT(readFile(file, feat_sup), "Failed to read sup"); + + return true; +} + +template +bool KerasLayerUnScaling::apply(Tensor& in, Tensor& out) { + // KASSERT(in, "Invalid input"); + // KASSERT(out, "Invalid output"); + + out = in; + + for (size_t i = 0; i < out.data_.size(); i++) { + auto tempscale = (out.data_[i] - feat_inf)/(feat_sup - feat_inf); + + out.data_[i] = tempscale * (data_max - data_min) + data_min; + } + + + return true; +} + + +template +bool KerasLayerDense::loadLayer(std::ifstream& file) { + KASSERT(file, "Invalid file stream"); + + unsigned int weights_rows = 0; + KASSERT(readFile(file, weights_rows), "Expected weight rows"); + KASSERT(weights_rows > 0, "Invalid weights # rows"); + + unsigned int weights_cols = 0; + KASSERT(readFile(file, weights_cols), "Expected weight cols"); + KASSERT(weights_cols > 0, "Invalid weights shape"); + + unsigned int biases_shape = 0; + KASSERT(readFile(file, biases_shape), "Expected biases shape"); + KASSERT(biases_shape > 0, "Invalid biases shape"); + + weights_.resize(weights_rows, weights_cols); + KASSERT( + ReadFloats(&file, weights_.data_.data(), weights_rows * weights_cols), + "Expected weights"); + + biases_.resize(biases_shape); + KASSERT(ReadFloats(&file, biases_.data_.data(), biases_shape), + "Expected biases"); + + KASSERT(activation_.loadLayer(file), "Failed to load activation"); + + return true; +} + +template +bool KerasLayerDense::apply(Tensor& in, Tensor& out) { + // KASSERT(in, "Invalid input"); + // KASSERT(out, "Invalid output"); + KASSERT(in.dims_.size() <= 2, "Invalid input dimensions"); + + if (in.dims_.size() == 2) { + KASSERT(in.dims_[1] == weights_.dims_[0], "Dimension mismatch %d %d", + in.dims_[1], weights_.dims_[0]); + } + + Tensor tmp(weights_.dims_[1]); + + for (int i = 0; i < weights_.dims_[0]; i++) { + for (int j = 0; j < weights_.dims_[1]; j++) { + tmp(j) += (in)(i)*weights_(i, j); + } + } + + for (int i = 0; i < biases_.dims_[0]; i++) { + tmp(i) += biases_(i); + } + + KASSERT(activation_.apply(tmp, out), "Failed to apply activation"); + + return true; +} + +template +bool KerasLayerFlatten::loadLayer(std::ifstream& file) { + KASSERT(file, "Invalid file stream"); + return true; +} + +template +bool KerasLayerFlatten::apply(Tensor& in, Tensor& out) { + // KASSERT(in, "Invalid input"); + // KASSERT(out, "Invalid output"); + + out = in; + out.flatten(); + + return true; +} + + + +template +bool KerasLayerEmbedding::loadLayer(std::ifstream& file) { + KASSERT(file, "Invalid file stream"); + + unsigned int weights_rows = 0; + KASSERT(readFile(file, weights_rows), "Expected weight rows"); + KASSERT(weights_rows > 0, "Invalid weights # rows"); + + unsigned int weights_cols = 0; + KASSERT(readFile(file, weights_cols), "Expected weight cols"); + KASSERT(weights_cols > 0, "Invalid weights shape"); + + weights_.resize(weights_rows, weights_cols); + KASSERT( + ReadFloats(&file, weights_.data_.data(), weights_rows * weights_cols), + "Expected weights"); + + return true; +} + +template +bool KerasLayerEmbedding::apply(Tensor& in, Tensor& out) { + int output_rows = in.dims_[1]; + int output_cols = weights_.dims_[1]; + out.dims_ = {output_rows, output_cols}; + out.data_.reserve(output_rows * output_cols); + + std::for_each(in.data_.begin(), in.data_.end(), [=](Evaluation i) { + typename std::vector::const_iterator first = + this->weights_.data_.begin() + (getValue(i) * output_cols); + typename std::vector::const_iterator last = + this->weights_.data_.begin() + (getValue(i) + 1) * output_cols; + + out.data_.insert(out.data_.end(), first, last); + }); + + return true; +} + + +template +bool KerasModel::loadModel(const std::string& filename) { + std::ifstream file(filename.c_str(), std::ios::binary); + KASSERT(file.is_open(), "Unable to open file %s", filename.c_str()); + + unsigned int num_layers = 0; + KASSERT(readFile(file, num_layers), "Expected number of layers"); + + for (unsigned int i = 0; i < num_layers; i++) { + unsigned int layer_type = 0; + KASSERT(readFile(file, layer_type), "Expected layer type"); + + // KerasLayer* layer = NULL; + std::unique_ptr> layer = nullptr; + + switch (layer_type) { + case kFlatten: + // layer = new KerasLayerFlatten(); + layer = std::make_unique>() ; + break; + case kScaling: + // layer = new KerasLayerScaling(); + layer = std::make_unique>() ; + break; + case kUnScaling: + // layer = new KerasLayerUnScaling(); + layer = std::make_unique>() ; + break; + case kDense: + // layer = new KerasLayerDense(); + layer = std::make_unique>() ; + break; + case kActivation: + // layer = new KerasLayerActivation(); + layer = std::make_unique>() ; + break; + default: + break; + } + + KASSERT(layer, "Unknown layer type %d", layer_type); + + bool result = layer->loadLayer(file); + if (!result) { + std::printf("Failed to load layer %d", i); + // delete layer; + return false; + } + + // layers_.push_back(layer); + layers_.emplace_back(std::move(layer)); + + } + + return true; +} + +template +bool KerasModel::apply(Tensor& in, Tensor& out) { + Tensor temp_in, temp_out; + + for (unsigned int i = 0; i < layers_.size(); i++) { + if (i == 0) { + temp_in = in; + } + + KASSERT(layers_[i]->apply(temp_in, temp_out), + "Failed to apply layer %d", i); + + temp_in = temp_out; + } + + out = temp_out; + + return true; +} + +template class KerasModel; + +template class KerasModel; +template class KerasModel>; +template class KerasModel>; +template class KerasModel>; +template class KerasModel>; +template class KerasModel>; +template class KerasModel>; + +} // namespace Opm diff --git a/opm/ml/ml_model.hpp b/opm/ml/ml_model.hpp new file mode 100644 index 00000000000..b1473fbfc1d --- /dev/null +++ b/opm/ml/ml_model.hpp @@ -0,0 +1,437 @@ +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#ifndef ML_MODEL_H_ +#define ML_MODEL_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace Opm { + +#define KASSERT(x, ...) \ + if (!(x)) { \ + std::printf("KASSERT: %s(%d): ", __FILE__, __LINE__); \ + std::printf(__VA_ARGS__); \ + std::printf("\n"); \ + return false; \ + } + +#define KASSERT_EQ(x, y, eps) \ + if (fabs(x.value() - y.value()) > eps) { \ + std::printf("KASSERT: Expected %f, got %f\n", y.value(), x.value()); \ + return false; \ + } + +#ifdef DEBUG +#define KDEBUG(x, ...) \ + if (!(x)) { \ + std::printf("%s(%d): ", __FILE__, __LINE__); \ + std::printf(__VA_ARGS__); \ + std::printf("\n"); \ + exit(-1); \ + } +#else +#define KDEBUG(x, ...) ; +#endif + +template +class Tensor { + public: + Tensor() {} + + explicit Tensor(int i) { resize(i); } + + Tensor(int i, int j) { resize(i, j); } + + Tensor(int i, int j, int k) { resize(i, j, k); } + + Tensor(int i, int j, int k, int l) { resize(i, j, k, l); } + + void resize(int i) { + dims_ = {i}; + data_.resize(i); + } + + void resize(int i, int j) { + dims_ = {i, j}; + data_.resize(i * j); + } + + void resize(int i, int j, int k) { + dims_ = {i, j, k}; + data_.resize(i * j * k); + } + + void resize(int i, int j, int k, int l) { + dims_ = {i, j, k, l}; + data_.resize(i * j * k * l); + } + + inline void flatten() { + KDEBUG(dims_.size() > 0, "Invalid tensor"); + + int elements = dims_[0]; + for (unsigned int i = 1; i < dims_.size(); i++) { + elements *= dims_[i]; + } + dims_ = {elements}; + } + + inline T& operator()(int i) { + KDEBUG(dims_.size() == 1, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + + return data_[i]; + } + + inline T& operator()(int i, int j) { + KDEBUG(dims_.size() == 2, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + KDEBUG(j < dims_[1] && j >= 0, "Invalid j: %d (max %d)", j, dims_[1]); + + return data_[dims_[1] * i + j]; + } + + const T& operator()(int i, int j) const { + KDEBUG(dims_.size() == 2, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + KDEBUG(j < dims_[1] && j >= 0, "Invalid j: %d (max %d)", j, dims_[1]); + + return data_[dims_[1] * i + j]; + } + + inline T& operator()(int i, int j, int k) { + KDEBUG(dims_.size() == 3, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + KDEBUG(j < dims_[1] && j >= 0, "Invalid j: %d (max %d)", j, dims_[1]); + KDEBUG(k < dims_[2] && k >= 0, "Invalid k: %d (max %d)", k, dims_[2]); + + return data_[dims_[2] * (dims_[1] * i + j) + k]; + } + const T& operator()(int i, int j, int k) const { + KDEBUG(dims_.size() == 3, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + KDEBUG(j < dims_[1] && j >= 0, "Invalid j: %d (max %d)", j, dims_[1]); + KDEBUG(k < dims_[2] && k >= 0, "Invalid k: %d (max %d)", k, dims_[2]); + + return data_[dims_[2] * (dims_[1] * i + j) + k]; + } + + inline T& operator()(int i, int j, int k, int l) { + KDEBUG(dims_.size() == 4, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + KDEBUG(j < dims_[1] && j >= 0, "Invalid j: %d (max %d)", j, dims_[1]); + KDEBUG(k < dims_[2] && k >= 0, "Invalid k: %d (max %d)", k, dims_[2]); + KDEBUG(l < dims_[3] && l >= 0, "Invalid l: %d (max %d)", l, dims_[3]); + + return data_[dims_[3] * (dims_[2] * (dims_[1] * i + j) + k) + l]; + } + + const T& operator()(int i, int j, int k, int l) const{ + KDEBUG(dims_.size() == 4, "Invalid indexing for tensor"); + KDEBUG(i < dims_[0] && i >= 0, "Invalid i: %d (max %d)", i, dims_[0]); + KDEBUG(j < dims_[1] && j >= 0, "Invalid j: %d (max %d)", j, dims_[1]); + KDEBUG(k < dims_[2] && k >= 0, "Invalid k: %d (max %d)", k, dims_[2]); + KDEBUG(l < dims_[3] && l >= 0, "Invalid l: %d (max %d)", l, dims_[3]); + + return data_[dims_[3] * (dims_[2] * (dims_[1] * i + j) + k) + l]; + } + void fill(const T & value) { + std::fill(data_.begin(), data_.end(), value); + } + + // Tensor Unpack(int row) const { + // KASSERT(dims_.size() >= 2, "Invalid tensor"); + // std::vector pack_dims = + // std::vector(dims_.begin() + 1, dims_.end()); + // int pack_size = std::accumulate(pack_dims.begin(), pack_dims.end(), 0); + + // typename std::vector::const_iterator first = + // data_.begin() + (row * pack_size); + // typename std::vector::const_iterator last = + // data_.begin() + (row + 1) * pack_size; + + // Tensor x = Tensor(); + // x.dims_ = pack_dims; + // x.data_ = std::vector(first, last); + + // return x; + // } + + // Tensor Select(int row) const { + // Tensor x = Unpack(row); + // x.dims_.insert(x.dims_.begin(), 1); + + // return x; + // } + + Tensor operator+(const Tensor& other) { + // KASSERT(dims_ == other.dims_, + // "Cannot add tensors with different dimensions"); + OPM_ERROR_IF(dims_.size() != other.dims_.size() , + "Cannot add tensors with different dimensions"); +// std::cout<<"dims_ "< dims_; + std::vector data_; +}; + +template +class KerasLayer { + public: + KerasLayer() {} + + virtual ~KerasLayer() {} + + virtual bool loadLayer(std::ifstream& file) = 0; + + virtual bool apply(Tensor& in, Tensor& out) = 0; +}; + +template +class KerasLayerActivation : public KerasLayer { + public: + enum ActivationType { + kLinear = 1, + kRelu = 2, + kSoftPlus = 3, + kSigmoid = 4, + kTanh = 5, + kHardSigmoid = 6 + }; + + KerasLayerActivation() : activation_type_(ActivationType::kLinear) {} + + virtual ~KerasLayerActivation() {} + + virtual bool loadLayer(std::ifstream& file); + + virtual bool apply(Tensor& in, Tensor& out); + + private: + ActivationType activation_type_; +}; + +template +class KerasLayerScaling : public KerasLayer { + public: + KerasLayerScaling(): data_min(1.0f), data_max(1.0f), feat_inf(1.0f), feat_sup(1.0f) {} + + virtual ~KerasLayerScaling() {} + + virtual bool loadLayer(std::ifstream& file); + + virtual bool apply(Tensor& in, Tensor& out); + + private: + Tensor weights_; + Tensor biases_; + float data_min; + float data_max; + float feat_inf; + float feat_sup; +}; + +template +class KerasLayerUnScaling : public KerasLayer { + public: + KerasLayerUnScaling(): data_min(1.0f), data_max(1.0f), feat_inf(1.0f), feat_sup(1.0f) {} + + virtual ~KerasLayerUnScaling() {} + + virtual bool loadLayer(std::ifstream& file); + + virtual bool apply(Tensor& in, Tensor& out); + + private: + Tensor weights_; + Tensor biases_; + float data_min; + float data_max; + float feat_inf; + float feat_sup; +}; + + +template +class KerasLayerDense : public KerasLayer { + public: + KerasLayerDense() {} + + virtual ~KerasLayerDense() {} + + virtual bool loadLayer(std::ifstream& file); + + virtual bool apply(Tensor& in, Tensor& out); + + private: + Tensor weights_; + Tensor biases_; + + KerasLayerActivation activation_; +}; + +template +class KerasLayerFlatten : public KerasLayer { + public: + KerasLayerFlatten() {} + + virtual ~KerasLayerFlatten() {} + + virtual bool loadLayer(std::ifstream& file); + + virtual bool apply(Tensor& in, Tensor& out); + + // private: +}; + + +template +class KerasLayerEmbedding : public KerasLayer { + public: + KerasLayerEmbedding() {} + + virtual ~KerasLayerEmbedding() {} + + virtual bool loadLayer(std::ifstream& file); + + virtual bool apply(Tensor& in, Tensor& out); + + private: + Tensor weights_; +}; + +template +class KerasModel { + public: + enum LayerType { + kFlatten = 1, + kScaling = 2, + kUnScaling = 3, + kDense = 4, + kActivation = 5 + }; + + KerasModel() {} + + virtual ~KerasModel() { + // for (unsigned int i = 0; i < layers_.size(); i++) { + // delete layers_[i]; + // } + } + + virtual bool loadModel(const std::string& filename); + + virtual bool apply(Tensor& in, Tensor& out); + + private: + // std::vector*> layers_; + std::vector>> layers_; +}; + +class KerasTimer { + public: + KerasTimer() {} + + void start() { start_ = std::chrono::high_resolution_clock::now(); } + + float stop() { + std::chrono::time_point now = + std::chrono::high_resolution_clock::now(); + + std::chrono::duration diff = now - start_; + + return diff.count(); + } + + private: + std::chrono::time_point start_; +}; + +} // namespace Opm + +#endif // ML_MODEL_H_ diff --git a/opm/ml/ml_tools/__init__.py b/opm/ml/ml_tools/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opm/ml/ml_tools/kerasify.py b/opm/ml/ml_tools/kerasify.py new file mode 100644 index 00000000000..8dde9e054bb --- /dev/null +++ b/opm/ml/ml_tools/kerasify.py @@ -0,0 +1,161 @@ +# /* + +# * Copyright (c) 2016 Robert W. Rose +# * Copyright (c) 2018 Paul Maevskikh +# * +# * MIT License, see LICENSE.MIT file. +# */ + +# Copyright (c) 2024 NORCE +# This file is part of the Open Porous Media project (OPM). + +# OPM is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# OPM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with OPM. If not, see . + +import numpy as np +import struct + +LAYER_FLATTEN = 1 +LAYER_SCALING = 2 +LAYER_UNSCALING = 3 +LAYER_DENSE = 4 +LAYER_ACTIVATION = 5 + +ACTIVATION_LINEAR = 1 +ACTIVATION_RELU = 2 +ACTIVATION_SOFTPLUS = 3 +ACTIVATION_SIGMOID = 4 +ACTIVATION_TANH = 5 +ACTIVATION_HARD_SIGMOID = 6 + +def write_scaling(f): + f.write(struct.pack('I', LAYER_SCALING)) + + +def write_unscaling(f): + f.write(struct.pack('I', LAYER_UNSCALING)) + + +def write_tensor(f, data, dims=1): + """ + Writes tensor as flat array of floats to file in 1024 chunks, + prevents memory explosion writing very large arrays to disk + when calling struct.pack(). + """ + f.write(struct.pack('I', dims)) + + for stride in data.shape[:dims]: + f.write(struct.pack('I', stride)) + + data = data.ravel() + step = 1024 + written = 0 + + for i in np.arange(0, len(data), step): + remaining = min(len(data) - i, step) + written += remaining + f.write(struct.pack(f'={remaining}f', *data[i: i + remaining])) + + assert written == len(data) + + +def write_floats(file, floats): + ''' + Writes floats to file in 1024 chunks.. prevents memory explosion + writing very large arrays to disk when calling struct.pack(). + ''' + step = 1024 + written = 0 + + for i in np.arange(0, len(floats), step): + remaining = min(len(floats) - i, step) + written += remaining + file.write(struct.pack('=%sf' % remaining, *floats[i:i+remaining])) + # file.write(struct.pack(f'={remaining}f', *floats[i: i + remaining])) + + assert written == len(floats) + +def export_model(model, filename): + with open(filename, 'wb') as f: + + def write_activation(activation): + if activation == 'linear': + f.write(struct.pack('I', ACTIVATION_LINEAR)) + elif activation == 'relu': + f.write(struct.pack('I', ACTIVATION_RELU)) + elif activation == 'softplus': + f.write(struct.pack('I', ACTIVATION_SOFTPLUS)) + elif activation == 'tanh': + f.write(struct.pack('I', ACTIVATION_TANH)) + elif activation == 'sigmoid': + f.write(struct.pack('I', ACTIVATION_SIGMOID)) + elif activation == 'hard_sigmoid': + f.write(struct.pack('I', ACTIVATION_HARD_SIGMOID)) + else: + assert False, "Unsupported activation type: %s" % activation + + model_layers = [l for l in model.layers if type(l).__name__ not in ['Dropout']] + num_layers = len(model_layers) + f.write(struct.pack('I', num_layers)) + + for layer in model_layers: + layer_type = type(layer).__name__ + + if layer_type == 'MinMaxScalerLayer': + write_scaling(f) + feat_inf = layer.get_weights()[0] + feat_sup = layer.get_weights()[1] + f.write(struct.pack('f', layer.data_min)) + f.write(struct.pack('f', layer.data_max)) + f.write(struct.pack('f', feat_inf)) + f.write(struct.pack('f', feat_sup)) + + + elif layer_type == 'MinMaxUnScalerLayer': + write_unscaling(f) + feat_inf = layer.get_weights()[0] + feat_sup = layer.get_weights()[1] + f.write(struct.pack('f', layer.data_min)) + f.write(struct.pack('f', layer.data_max)) + f.write(struct.pack('f', feat_inf)) + f.write(struct.pack('f', feat_sup)) + + elif layer_type == 'Dense': + weights = layer.get_weights()[0] + biases = layer.get_weights()[1] + activation = layer.get_config()['activation'] + + f.write(struct.pack('I', LAYER_DENSE)) + f.write(struct.pack('I', weights.shape[0])) + f.write(struct.pack('I', weights.shape[1])) + f.write(struct.pack('I', biases.shape[0])) + + weights = weights.flatten() + biases = biases.flatten() + + write_floats(f, weights) + write_floats(f, biases) + + write_activation(activation) + + elif layer_type == 'Flatten': + f.write(struct.pack('I', LAYER_FLATTEN)) + + elif layer_type == 'Activation': + activation = layer.get_config()['activation'] + + f.write(struct.pack('I', LAYER_ACTIVATION)) + write_activation(activation) + + else: + assert False, "Unsupported layer type: %s" % layer_type diff --git a/opm/ml/ml_tools/scaler_layers.py b/opm/ml/ml_tools/scaler_layers.py new file mode 100644 index 00000000000..553eebc1825 --- /dev/null +++ b/opm/ml/ml_tools/scaler_layers.py @@ -0,0 +1,210 @@ +# Copyright (c) 2024 NORCE +# Copyright (c) 2024 UiB + +# This file is part of the Open Porous Media project (OPM). + +# OPM is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# OPM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with OPM. If not, see . + +"""Provide MinMax scaler layers for tensorflow.keras.""" + +from __future__ import annotations + +from typing import Optional, Sequence + +import numpy as np +import tensorflow as tf +from numpy.typing import ArrayLike +from tensorflow import keras +from tensorflow.python.keras.engine.base_preprocessing_layer import ( # pylint: disable=E0611 + PreprocessingLayer, +) + + +class ScalerLayer(keras.layers.Layer): + """MixIn to provide functionality for the Scaler Layer.""" + + data_min: tf.Tensor + data_max: tf.Tensor + min: tf.Tensor + scalar: tf.Tensor + + def __init__( + self, + data_min: Optional[float | ArrayLike] = None, + data_max: Optional[float | ArrayLike] = None, + feature_range: Sequence[float] | np.ndarray | tf.Tensor = (0, 1), + **kwargs, # pylint: disable=W0613 + ) -> None: + super().__init__(**kwargs) + if feature_range[0] >= feature_range[1]: + raise ValueError("Feature range must be strictly increasing.") + self.feature_range: tf.Tensor = tf.convert_to_tensor( + feature_range, dtype=tf.float32 + ) + self._is_adapted: bool = False + if data_min is not None and data_max is not None: + self.data_min = tf.convert_to_tensor(data_min, dtype=tf.float32) + self.data_max = tf.convert_to_tensor(data_max, dtype=tf.float32) + self._adapt() + + def build(self, input_shape: tuple[int, ...]) -> None: + """Initialize ``data_min`` and ``data_max`` with the default values if they have + not been initialized yet. + + Args: + input_shape (tuple[int, ...]): _description_ + + """ + if not self._is_adapted: + # ``data_min`` and ``data_max`` have the same shape as one input tensor. + self.data_min = tf.zeros(input_shape[1:]) + self.data_max = tf.ones(input_shape[1:]) + self._adapt() + + def get_weights(self) -> list[ArrayLike]: + """Return parameters of the scaling. + + Returns: + list[ArrayLike]: List with three elements in the following order: + ``self.data_min``, ``self.data_max``, ``self.feature_range`` + + """ + return [self.feature_range[0], self.feature_range[1], self.data_min, self.data_max, self.feature_range] + + def set_weights(self, weights: list[ArrayLike]) -> None: + """Set parameters of the scaling. + + Args: + weights (list[ArrayLike]): List with three elements in the following order: + ``data_min``, ``data_max``, ``feature_range`` + + Raises: + ValueError: If ``feature_range[0] >= feature_range[1]``. + + """ + self.feature_range = tf.convert_to_tensor(weights[2], dtype=tf.float32) + if self.feature_range[0] >= self.feature_range[1]: + raise ValueError("Feature range must be strictly increasing.") + self.data_min = tf.convert_to_tensor(weights[0], dtype=tf.float32) + self.data_max = tf.convert_to_tensor(weights[1], dtype=tf.float32) + + def adapt(self, data: ArrayLike) -> None: + """Fit the layer to the min and max of the data. This is done individually for + each input feature. + + Note: + So far, this is only tested for 1 dimensional input and output. For higher + dimensional input and output some functionality might need to be added. + + Args: + data: _description_ + + """ + data = tf.convert_to_tensor(data, dtype=tf.float32) + self.data_min = tf.math.reduce_min(data, axis=0) + self.data_max = tf.math.reduce_max(data, axis=0) + self._adapt() + + def _adapt(self) -> None: + if tf.math.reduce_any(self.data_min > self.data_max): + raise RuntimeError( + f"""self.data_min {self.data_min} cannot be larger than self.data_max + {self.data_max} for any element.""" + ) + self.scalar = tf.where( + self.data_max > self.data_min, + self.data_max - self.data_min, + tf.ones_like(self.data_min), + ) + self.min = tf.where( + self.data_max > self.data_min, + self.data_min, + tf.zeros_like(self.data_min), + ) + self._is_adapted = True + + +class MinMaxScalerLayer(ScalerLayer, PreprocessingLayer): # pylint: disable=W0223,R0901 + """Scales the input according to MinMaxScaling. + + See + https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MinMaxScaler.html + for an explanation of the transform. + + """ + + def __init__( + self, + data_min: Optional[float | ArrayLike] = None, + data_max: Optional[float | ArrayLike] = None, + feature_range: Sequence[float] | np.ndarray | tf.Tensor = (0, 1), + **kwargs, # pylint: disable=W0613 + ) -> None: + super().__init__(data_min, data_max, feature_range, **kwargs) + self._name: str = "MinMaxScalerLayer" + + # Ignore pylint complaining about a missing docstring. Also ignore + # "variadics removed ...". + def call(self, inputs: tf.Tensor) -> tf.Tensor: # pylint: disable=C0116, W0221 + if not self.is_adapted: + print(np.greater_equal(self.data_min, self.data_max)) + raise RuntimeError( + """The layer has not been adapted correctly. Call ``adapt`` before using + the layer or set the ``data_min`` and ``data_max`` values manually. + """ + ) + + # Ensure the dtype is correct. + inputs = tf.convert_to_tensor(inputs, dtype=tf.float32) + scaled_data = (inputs - self.min) / self.scalar + return ( + scaled_data * (self.feature_range[1] - self.feature_range[0]) + ) + self.feature_range[0] + # return inputs + + +class MinMaxUnScalerLayer(ScalerLayer, tf.keras.layers.Layer): + """Unscales the input by applying the inverse transform of ``MinMaxScalerLayer``. + + See + https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MinMaxScaler.html + for an explanation of the transformation. + + """ + + def __init__( + self, + data_min: Optional[float | ArrayLike] = None, + data_max: Optional[float | ArrayLike] = None, + feature_range: Sequence[float] | np.ndarray | tf.Tensor = (0, 1), + **kwargs, # pylint: disable=W0613 + ) -> None: + super().__init__(data_min, data_max, feature_range, **kwargs) + self._name: str = "MinMaxUnScalerLayer" + + # Ignore pylint complaining about a missing docstring and something else. + def call(self, inputs: tf.Tensor) -> tf.Tensor: # pylint: disable=W0221 + if not self._is_adapted: + raise RuntimeError( + """The layer has not been adapted correctly. Call ``adapt`` before using + the layer or set the ``data_min`` and ``data_max`` values manually.""" + ) + + # Ensure the dtype is correct. + inputs = tf.convert_to_tensor(inputs, dtype=tf.float32) + unscaled_data = (inputs - self.feature_range[0]) / ( + self.feature_range[1] - self.feature_range[0] + ) + return unscaled_data * self.scalar + self.min + # return inputs diff --git a/opm/ml/ml_tools/scalertest.py b/opm/ml/ml_tools/scalertest.py new file mode 100644 index 00000000000..ef5a819a686 --- /dev/null +++ b/opm/ml/ml_tools/scalertest.py @@ -0,0 +1,50 @@ +# Copyright (c) 2024 NORCE +# Copyright (c) 2024 UiB + +# This file is part of the Open Porous Media project (OPM). + +# OPM is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# OPM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with OPM. If not, see . + +from __future__ import annotations + +import pathlib + +import numpy as np + +from tensorflow import keras + +from kerasify import export_model + +from scaler_layers import MinMaxScalerLayer, MinMaxUnScalerLayer + +from keras.models import Sequential +from keras.layers import Conv2D, Dense, Flatten, Activation, MaxPooling2D, Dropout, BatchNormalization, ELU, Embedding, LSTM + +savepath = pathlib.Path(__file__).parent / "model_with_scaler_layers.opm" + +feature_ranges: list[tuple[float, float]] = [(0.0, 1.0), (-3.7, 0.0)] + +data: np.ndarray = np.random.uniform(-500, 500, (5, 1)) + +model = Sequential() +model.add(keras.layers.Input([1])) +model.add(MinMaxScalerLayer(feature_range=(0.0, 1.0))) +model.add(Dense(1, input_dim=1)) +model.add(Dense(1, input_dim=1)) +model.add(Dense(1, input_dim=1)) +model.add(Dense(1, input_dim=1)) + +model.add(MinMaxUnScalerLayer(feature_range=(-3.7, -1.0))) + +export_model(model, str(savepath)) diff --git a/tests/ml/keras_model_test.cpp b/tests/ml/keras_model_test.cpp new file mode 100644 index 00000000000..2eef0a75c01 --- /dev/null +++ b/tests/ml/keras_model_test.cpp @@ -0,0 +1,201 @@ +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace Opm { + +template +bool tensor_test() { + printf("TEST tensor_test\n"); + + { + const int i = 3; + const int j = 5; + const int k = 10; + Tensor t(i, j, k); + + Evaluation c = 1.f; + for (int ii = 0; ii < i; ii++) { + for (int jj = 0; jj < j; jj++) { + for (int kk = 0; kk < k; kk++) { + t(ii, jj, kk) = c; + c += 1.f; + } + } + } + + c = 1.f; + int cc = 0; + for (int ii = 0; ii < i; ii++) { + for (int jj = 0; jj < j; jj++) { + for (int kk = 0; kk < k; kk++) { + KASSERT_EQ(t(ii, jj, kk), c, 1e-9); + KASSERT_EQ(t.data_[cc], c, 1e-9); + c += 1.f; + cc++; + } + } + } + } + + { + const int i = 2; + const int j = 3; + const int k = 4; + const int l = 5; + Tensor t(i, j, k, l); + + Evaluation c = 1.f; + for (int ii = 0; ii < i; ii++) { + for (int jj = 0; jj < j; jj++) { + for (int kk = 0; kk < k; kk++) { + for (int ll = 0; ll < l; ll++) { + t(ii, jj, kk, ll) = c; + c += 1.f; + } + } + } + } + + c = 1.f; + int cc = 0; + for (int ii = 0; ii < i; ii++) { + for (int jj = 0; jj < j; jj++) { + for (int kk = 0; kk < k; kk++) { + for (int ll = 0; ll < l; ll++) { + KASSERT_EQ(t(ii, jj, kk, ll), c, 1e-9); + KASSERT_EQ(t.data_[cc], c, 1e-9); + c += 1.f; + cc++; + } + } + } + } + } + + { + Tensor a(2, 2); + Tensor b(2, 2); + + a.data_ = {1.0, 2.0, 3.0, 5.0}; + b.data_ = {2.0, 5.0, 4.0, 1.0}; + + Tensor result = a + b; + KASSERT(result.data_ == std::vector({3.0, 7.0, 7.0, 6.0}), + "Vector add failed"); + } + + { + Tensor a(2, 2); + Tensor b(2, 2); + + a.data_ = {1.0, 2.0, 3.0, 5.0}; + b.data_ = {2.0, 5.0, 4.0, 1.0}; + + Tensor result = a.Multiply(b); + KASSERT(result.data_ == std::vector({2.0, 10.0, 12.0, 5.0}), + "Vector multiply failed"); + } + + { + Tensor a(2, 1); + Tensor b(1, 2); + + a.data_ = {1.0, 2.0}; + b.data_ = {2.0, 5.0}; + + Tensor result = a.Dot(b); + KASSERT(result.data_ == std::vector({2.0, 5.0, 4.0, 10.0}), + "Vector dot failed"); + } + + return true; +} + +} + + +int main() { + typedef Opm::DenseAd::Evaluation Evaluation; + + Evaluation load_time = 0.0; + Evaluation apply_time = 0.0; + + if (!tensor_test()) { + return 1; + } + + if (!test_dense_1x1(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_10x1(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_2x2(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_10x10(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_10x10x10(&load_time, &apply_time)) { + return 1; + } + + if (!test_relu_10(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_relu_10(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_tanh_10(&load_time, &apply_time)) { + return 1; + } + + if (!test_scalingdense_10x1(&load_time, &apply_time)) { + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/tests/ml/ml_model_test.cpp b/tests/ml/ml_model_test.cpp new file mode 100644 index 00000000000..94383fbee11 --- /dev/null +++ b/tests/ml/ml_model_test.cpp @@ -0,0 +1,201 @@ +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace Opm { + +template +bool tensor_test() { + printf("TEST tensor_test\n"); + + { + const int i = 3; + const int j = 5; + const int k = 10; + Tensor t(i, j, k); + + Evaluation c = 1.f; + for (int ii = 0; ii < i; ii++) { + for (int jj = 0; jj < j; jj++) { + for (int kk = 0; kk < k; kk++) { + t(ii, jj, kk) = c; + c += 1.f; + } + } + } + + c = 1.f; + int cc = 0; + for (int ii = 0; ii < i; ii++) { + for (int jj = 0; jj < j; jj++) { + for (int kk = 0; kk < k; kk++) { + KASSERT_EQ(t(ii, jj, kk), c, 1e-9); + KASSERT_EQ(t.data_[cc], c, 1e-9); + c += 1.f; + cc++; + } + } + } + } + + { + const int i = 2; + const int j = 3; + const int k = 4; + const int l = 5; + Tensor t(i, j, k, l); + + Evaluation c = 1.f; + for (int ii = 0; ii < i; ii++) { + for (int jj = 0; jj < j; jj++) { + for (int kk = 0; kk < k; kk++) { + for (int ll = 0; ll < l; ll++) { + t(ii, jj, kk, ll) = c; + c += 1.f; + } + } + } + } + + c = 1.f; + int cc = 0; + for (int ii = 0; ii < i; ii++) { + for (int jj = 0; jj < j; jj++) { + for (int kk = 0; kk < k; kk++) { + for (int ll = 0; ll < l; ll++) { + KASSERT_EQ(t(ii, jj, kk, ll), c, 1e-9); + KASSERT_EQ(t.data_[cc], c, 1e-9); + c += 1.f; + cc++; + } + } + } + } + } + + { + Tensor a(2, 2); + Tensor b(2, 2); + + a.data_ = {1.0, 2.0, 3.0, 5.0}; + b.data_ = {2.0, 5.0, 4.0, 1.0}; + + Tensor result = a + b; + KASSERT(result.data_ == std::vector({3.0, 7.0, 7.0, 6.0}), + "Vector add failed"); + } + + { + Tensor a(2, 2); + Tensor b(2, 2); + + a.data_ = {1.0, 2.0, 3.0, 5.0}; + b.data_ = {2.0, 5.0, 4.0, 1.0}; + + Tensor result = a.multiply(b); + KASSERT(result.data_ == std::vector({2.0, 10.0, 12.0, 5.0}), + "Vector multiply failed"); + } + + { + Tensor a(2, 1); + Tensor b(1, 2); + + a.data_ = {1.0, 2.0}; + b.data_ = {2.0, 5.0}; + + Tensor result = a.dot(b); + KASSERT(result.data_ == std::vector({2.0, 5.0, 4.0, 10.0}), + "Vector dot failed"); + } + + return true; +} + +} + + +int main() { + typedef Opm::DenseAd::Evaluation Evaluation; + + Evaluation load_time = 0.0; + Evaluation apply_time = 0.0; + + if (!tensor_test()) { + return 1; + } + + if (!test_dense_1x1(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_10x1(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_2x2(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_10x10(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_10x10x10(&load_time, &apply_time)) { + return 1; + } + + if (!test_relu_10(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_relu_10(&load_time, &apply_time)) { + return 1; + } + + if (!test_dense_tanh_10(&load_time, &apply_time)) { + return 1; + } + + if (!test_scalingdense_10x1(&load_time, &apply_time)) { + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/tests/ml/ml_tools/generateunittests.py b/tests/ml/ml_tools/generateunittests.py new file mode 100644 index 00000000000..e5f53de578f --- /dev/null +++ b/tests/ml/ml_tools/generateunittests.py @@ -0,0 +1,230 @@ +# Copyright (c) 2024 NORCE +# This file is part of the Open Porous Media project (OPM). + +# OPM is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# OPM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with OPM. If not, see . + +import numpy as np +import pprint +import os, sys +from tensorflow import keras + +from keras.models import Sequential +from keras.layers import Conv2D, Dense, Flatten, Activation, MaxPooling2D, Dropout, BatchNormalization, ELU, Embedding, LSTM + +sys.path.append('../../../opm/ml/ml_tools') + +from kerasify import export_model +from scaler_layers import MinMaxScalerLayer, MinMaxUnScalerLayer + +np.set_printoptions(precision=25, threshold=10000000) + + +def c_array(a): + def to_cpp(ndarray): + text = np.array2string(ndarray, separator=',', threshold=np.inf, + floatmode='unique') + return text.replace('[', '{').replace(']', '}').replace(' ', '') + + s = to_cpp(a.ravel()) + shape = to_cpp(np.asarray(a.shape)) if a.shape else '{1}' + return shape, s + + +TEST_CASE = ''' +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + * Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +namespace fs = std::filesystem; + +using namespace Opm; +template +bool test_%s(Evaluation* load_time, Evaluation* apply_time) +{ + printf("TEST %s\\n"); + + KASSERT(load_time, "Invalid Evaluation"); + KASSERT(apply_time, "Invalid Evaluation"); + + Opm::Tensor in%s; + in.data_ = %s; + + Opm::Tensor out%s; + out.data_ = %s; + + KerasTimer load_timer; + load_timer.Start(); + + KerasModel model; + KASSERT(model.LoadModel("%s"), "Failed to load model"); + + *load_time = load_timer.Stop(); + + KerasTimer apply_timer; + apply_timer.Start(); + + Opm::Tensor predict = out; + KASSERT(model.Apply(&in, &out), "Failed to apply"); + + *apply_time = apply_timer.Stop(); + + for (int i = 0; i < out.dims_[0]; i++) + { + KASSERT_EQ(out(i), predict(i), %s); + } + + return true; +} +''' + +directory = os.getcwd() +directory1 = "models" +directory2 = "include" + +if os.path.isdir(directory1): + print(f"{directory1} exists.") +else: + print(f"{directory1} does not exist.") + path1 = os.path.join(directory, directory1) + os.makedirs(path1) + +if os.path.isdir(directory2): + print(f"{directory2} exists.") +else: + path2 = os.path.join(directory, directory2) + os.makedirs(path2) + + +def output_testcase(model, test_x, test_y, name, eps): + print("Processing %s" % name) + model.compile(loss='mean_squared_error', optimizer='adamax') + model.fit(test_x, test_y, epochs=1, verbose=False) + predict_y = model.predict(test_x).astype('f') + print(model.summary()) + + export_model(model, 'models/test_%s.model' % name) + path = f'./ml/ml_tools/models/test_{name}.model' + with open('include/test_%s.hpp' % name, 'w') as f: + x_shape, x_data = c_array(test_x[0]) + y_shape, y_data = c_array(predict_y[0]) + + f.write(TEST_CASE % (name, name, x_shape, x_data, y_shape, y_data, path, eps)) + + +# scaling 10x1 +data: np.ndarray = np.random.uniform(-500, 500, (5, 1)) +feature_ranges: list[tuple[float, float]] = [(0.0, 1.0), (-3.7, 0.0)] +test_x = np.random.rand(10, 10).astype('f') +test_y = np.random.rand(10).astype('f') +data_min = 10.0 +model = Sequential() +model.add(keras.layers.Input([10])) +model.add(MinMaxScalerLayer(feature_range=(0.0, 1.0))) +model.add(Dense(10,activation='tanh')) +model.add(Dense(10,activation='tanh')) +model.add(Dense(10,activation='tanh')) +model.add(Dense(10,activation='tanh')) +model.add(MinMaxUnScalerLayer(feature_range=(-3.7, -1.0))) +# # +model.get_layer(model.layers[0].name).adapt(data=data) +model.get_layer(model.layers[-1].name).adapt(data=data) +output_testcase(model, test_x, test_y, 'scalingdense_10x1', '1e-3') + +# Dense 1x1 +test_x = np.arange(10) +test_y = test_x * 10 + 1 +model = Sequential() +model.add(Dense(1, input_dim=1)) +output_testcase(model, test_x, test_y, 'dense_1x1', '1e-6') + +# Dense 10x1 +test_x = np.random.rand(10, 10).astype('f') +test_y = np.random.rand(10).astype('f') +model = Sequential() +model.add(Dense(1, input_dim=10)) +output_testcase(model, test_x, test_y, 'dense_10x1', '1e-6') + +# Dense 2x2 +test_x = np.random.rand(10, 2).astype('f') +test_y = np.random.rand(10).astype('f') +model = Sequential() +model.add(Dense(2, input_dim=2)) +model.add(Dense(1)) +output_testcase(model, test_x, test_y, 'dense_2x2', '1e-6') + +# Dense 10x10 +test_x = np.random.rand(10, 10).astype('f') +test_y = np.random.rand(10).astype('f') +model = Sequential() +model.add(Dense(10, input_dim=10)) +model.add(Dense(1)) +output_testcase(model, test_x, test_y, 'dense_10x10', '1e-6') + +# Dense 10x10x10 +test_x = np.random.rand(10, 10).astype('f') +test_y = np.random.rand(10, 10).astype('f') +model = Sequential() +model.add(Dense(10, input_dim=10)) +model.add(Dense(10)) +output_testcase(model, test_x, test_y, 'dense_10x10x10', '1e-6') + +# Activation relu +test_x = np.random.rand(1, 10).astype('f') +test_y = np.random.rand(1, 10).astype('f') +model = Sequential() +model.add(Dense(10, input_dim=10)) +model.add(Activation('relu')) +output_testcase(model, test_x, test_y, 'relu_10', '1e-6') + +# Dense relu +test_x = np.random.rand(1, 10).astype('f') +test_y = np.random.rand(1, 10).astype('f') +model = Sequential() +model.add(Dense(10, input_dim=10, activation='relu')) +model.add(Dense(10, input_dim=10, activation='relu')) +model.add(Dense(10, input_dim=10, activation='relu')) +output_testcase(model, test_x, test_y, 'dense_relu_10', '1e-6') + +# Dense tanh +test_x = np.random.rand(1, 10).astype('f') +test_y = np.random.rand(1, 10).astype('f') +model = Sequential() +model.add(Dense(10, input_dim=10, activation='tanh')) +model.add(Dense(10, input_dim=10, activation='tanh')) +model.add(Dense(10, input_dim=10, activation='tanh')) +output_testcase(model, test_x, test_y, 'dense_tanh_10', '1e-6') \ No newline at end of file diff --git a/tests/ml/ml_tools/include/test_dense_10x1.hpp b/tests/ml/ml_tools/include/test_dense_10x1.hpp new file mode 100644 index 00000000000..63059dd4a4b --- /dev/null +++ b/tests/ml/ml_tools/include/test_dense_10x1.hpp @@ -0,0 +1,70 @@ + +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + * Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +namespace fs = std::filesystem; + +using namespace Opm; +template +bool test_dense_10x1(Evaluation* load_time, Evaluation* apply_time) +{ + printf("TEST dense_10x1\n"); + + KASSERT(load_time, "Invalid Evaluation"); + KASSERT(apply_time, "Invalid Evaluation"); + + Opm::Tensor in{10}; + in.data_ = {0.3686802,0.37949404,0.42777044,0.3353853,0.9465501,0.045944117, +0.877874,0.11395996,0.6830254,0.40130788}; + + Opm::Tensor out{1}; + out.data_ = {-1.4414413}; + + KerasTimer load_timer; + load_timer.Start(); + + KerasModel model; + KASSERT(model.LoadModel("./ml/ml_tools/models/test_dense_10x1.model"), "Failed to load model"); + + *load_time = load_timer.Stop(); + + KerasTimer apply_timer; + apply_timer.Start(); + + Opm::Tensor predict = out; + KASSERT(model.Apply(&in, &out), "Failed to apply"); + + *apply_time = apply_timer.Stop(); + + for (int i = 0; i < out.dims_[0]; i++) + { + KASSERT_EQ(out(i), predict(i), 1e-6); + } + + return true; +} diff --git a/tests/ml/ml_tools/include/test_dense_10x10.hpp b/tests/ml/ml_tools/include/test_dense_10x10.hpp new file mode 100644 index 00000000000..951548d865d --- /dev/null +++ b/tests/ml/ml_tools/include/test_dense_10x10.hpp @@ -0,0 +1,70 @@ + +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + * Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +namespace fs = std::filesystem; + +using namespace Opm; +template +bool test_dense_10x10(Evaluation* load_time, Evaluation* apply_time) +{ + printf("TEST dense_10x10\n"); + + KASSERT(load_time, "Invalid Evaluation"); + KASSERT(apply_time, "Invalid Evaluation"); + + Opm::Tensor in{10}; + in.data_ = {0.9865467,0.36973363,0.36496657,0.34069845,0.28187922,0.061007407, +0.7788553,0.620593,0.54009753,0.09182126}; + + Opm::Tensor out{1}; + out.data_ = {0.09297238}; + + KerasTimer load_timer; + load_timer.Start(); + + KerasModel model; + KASSERT(model.LoadModel("./ml/ml_tools/models/test_dense_10x10.model"), "Failed to load model"); + + *load_time = load_timer.Stop(); + + KerasTimer apply_timer; + apply_timer.Start(); + + Opm::Tensor predict = out; + KASSERT(model.Apply(&in, &out), "Failed to apply"); + + *apply_time = apply_timer.Stop(); + + for (int i = 0; i < out.dims_[0]; i++) + { + KASSERT_EQ(out(i), predict(i), 1e-6); + } + + return true; +} diff --git a/tests/ml/ml_tools/include/test_dense_10x10x10.hpp b/tests/ml/ml_tools/include/test_dense_10x10x10.hpp new file mode 100644 index 00000000000..da9582baa91 --- /dev/null +++ b/tests/ml/ml_tools/include/test_dense_10x10x10.hpp @@ -0,0 +1,71 @@ + +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + * Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +namespace fs = std::filesystem; + +using namespace Opm; +template +bool test_dense_10x10x10(Evaluation* load_time, Evaluation* apply_time) +{ + printf("TEST dense_10x10x10\n"); + + KASSERT(load_time, "Invalid Evaluation"); + KASSERT(apply_time, "Invalid Evaluation"); + + Opm::Tensor in{10}; + in.data_ = {0.21841241,0.690713,0.2402783,0.70226014,0.3015559,0.5127232, +0.37969366,0.5222581,0.90413934,0.30193302}; + + Opm::Tensor out{10}; + out.data_ = {-0.40694734,-0.06657142,0.3262723,-0.9006126,0.011047224, +-0.83712757,-0.49354577,0.0790175,-0.5138001,-0.69188976}; + + KerasTimer load_timer; + load_timer.Start(); + + KerasModel model; + KASSERT(model.LoadModel("./ml/ml_tools/models/test_dense_10x10x10.model"), "Failed to load model"); + + *load_time = load_timer.Stop(); + + KerasTimer apply_timer; + apply_timer.Start(); + + Opm::Tensor predict = out; + KASSERT(model.Apply(&in, &out), "Failed to apply"); + + *apply_time = apply_timer.Stop(); + + for (int i = 0; i < out.dims_[0]; i++) + { + KASSERT_EQ(out(i), predict(i), 1e-6); + } + + return true; +} diff --git a/tests/ml/ml_tools/include/test_dense_1x1.hpp b/tests/ml/ml_tools/include/test_dense_1x1.hpp new file mode 100644 index 00000000000..a295b63af75 --- /dev/null +++ b/tests/ml/ml_tools/include/test_dense_1x1.hpp @@ -0,0 +1,69 @@ + +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + * Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +namespace fs = std::filesystem; + +using namespace Opm; +template +bool test_dense_1x1(Evaluation* load_time, Evaluation* apply_time) +{ + printf("TEST dense_1x1\n"); + + KASSERT(load_time, "Invalid Evaluation"); + KASSERT(apply_time, "Invalid Evaluation"); + + Opm::Tensor in{1}; + in.data_ = {0}; + + Opm::Tensor out{1}; + out.data_ = {0.001}; + + KerasTimer load_timer; + load_timer.Start(); + + KerasModel model; + KASSERT(model.LoadModel("./ml/ml_tools/models/test_dense_1x1.model"), "Failed to load model"); + + *load_time = load_timer.Stop(); + + KerasTimer apply_timer; + apply_timer.Start(); + + Opm::Tensor predict = out; + KASSERT(model.Apply(&in, &out), "Failed to apply"); + + *apply_time = apply_timer.Stop(); + + for (int i = 0; i < out.dims_[0]; i++) + { + KASSERT_EQ(out(i), predict(i), 1e-6); + } + + return true; +} diff --git a/tests/ml/ml_tools/include/test_dense_2x2.hpp b/tests/ml/ml_tools/include/test_dense_2x2.hpp new file mode 100644 index 00000000000..72a127f666b --- /dev/null +++ b/tests/ml/ml_tools/include/test_dense_2x2.hpp @@ -0,0 +1,69 @@ + +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + * Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +namespace fs = std::filesystem; + +using namespace Opm; +template +bool test_dense_2x2(Evaluation* load_time, Evaluation* apply_time) +{ + printf("TEST dense_2x2\n"); + + KASSERT(load_time, "Invalid Evaluation"); + KASSERT(apply_time, "Invalid Evaluation"); + + Opm::Tensor in{2}; + in.data_ = {0.4572911,0.40170738}; + + Opm::Tensor out{1}; + out.data_ = {0.22109468}; + + KerasTimer load_timer; + load_timer.Start(); + + KerasModel model; + KASSERT(model.LoadModel("./ml/ml_tools/models/test_dense_2x2.model"), "Failed to load model"); + + *load_time = load_timer.Stop(); + + KerasTimer apply_timer; + apply_timer.Start(); + + Opm::Tensor predict = out; + KASSERT(model.Apply(&in, &out), "Failed to apply"); + + *apply_time = apply_timer.Stop(); + + for (int i = 0; i < out.dims_[0]; i++) + { + KASSERT_EQ(out(i), predict(i), 1e-6); + } + + return true; +} diff --git a/tests/ml/ml_tools/include/test_dense_relu_10.hpp b/tests/ml/ml_tools/include/test_dense_relu_10.hpp new file mode 100644 index 00000000000..7933ca37e97 --- /dev/null +++ b/tests/ml/ml_tools/include/test_dense_relu_10.hpp @@ -0,0 +1,71 @@ + +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + * Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +namespace fs = std::filesystem; + +using namespace Opm; +template +bool test_dense_relu_10(Evaluation* load_time, Evaluation* apply_time) +{ + printf("TEST dense_relu_10\n"); + + KASSERT(load_time, "Invalid Evaluation"); + KASSERT(apply_time, "Invalid Evaluation"); + + Opm::Tensor in{10}; + in.data_ = {0.81092674,0.28423905,0.7424001,0.63347864,0.024311712, +0.20913002,0.0037298917,0.33594763,0.013057965,0.33818316}; + + Opm::Tensor out{10}; + out.data_ = {0.16579644,0.,0.024009448,0.00081627944,0.122673295, +0.,0.,0.1815404,0.,0.11925792}; + + KerasTimer load_timer; + load_timer.Start(); + + KerasModel model; + KASSERT(model.LoadModel("./ml/ml_tools/models/test_dense_relu_10.model"), "Failed to load model"); + + *load_time = load_timer.Stop(); + + KerasTimer apply_timer; + apply_timer.Start(); + + Opm::Tensor predict = out; + KASSERT(model.Apply(&in, &out), "Failed to apply"); + + *apply_time = apply_timer.Stop(); + + for (int i = 0; i < out.dims_[0]; i++) + { + KASSERT_EQ(out(i), predict(i), 1e-6); + } + + return true; +} diff --git a/tests/ml/ml_tools/include/test_dense_tanh_10.hpp b/tests/ml/ml_tools/include/test_dense_tanh_10.hpp new file mode 100644 index 00000000000..2223fee76f5 --- /dev/null +++ b/tests/ml/ml_tools/include/test_dense_tanh_10.hpp @@ -0,0 +1,71 @@ + +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + * Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +namespace fs = std::filesystem; + +using namespace Opm; +template +bool test_dense_tanh_10(Evaluation* load_time, Evaluation* apply_time) +{ + printf("TEST dense_tanh_10\n"); + + KASSERT(load_time, "Invalid Evaluation"); + KASSERT(apply_time, "Invalid Evaluation"); + + Opm::Tensor in{10}; + in.data_ = {0.8417535,0.01666395,0.40597403,0.72943276,0.55192524, +0.8613093,0.8216251,0.0077196797,0.9292973,0.050378576}; + + Opm::Tensor out{10}; + out.data_ = {0.005546283,-0.32751718,-0.54984826,0.12813118,0.034228113, +-0.15042743,-0.12779453,-0.43155488,-0.18912314,0.07741854}; + + KerasTimer load_timer; + load_timer.Start(); + + KerasModel model; + KASSERT(model.LoadModel("./ml/ml_tools/models/test_dense_tanh_10.model"), "Failed to load model"); + + *load_time = load_timer.Stop(); + + KerasTimer apply_timer; + apply_timer.Start(); + + Opm::Tensor predict = out; + KASSERT(model.Apply(&in, &out), "Failed to apply"); + + *apply_time = apply_timer.Stop(); + + for (int i = 0; i < out.dims_[0]; i++) + { + KASSERT_EQ(out(i), predict(i), 1e-6); + } + + return true; +} diff --git a/tests/ml/ml_tools/include/test_relu_10.hpp b/tests/ml/ml_tools/include/test_relu_10.hpp new file mode 100644 index 00000000000..2972c18fff1 --- /dev/null +++ b/tests/ml/ml_tools/include/test_relu_10.hpp @@ -0,0 +1,71 @@ + +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + * Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +namespace fs = std::filesystem; + +using namespace Opm; +template +bool test_relu_10(Evaluation* load_time, Evaluation* apply_time) +{ + printf("TEST relu_10\n"); + + KASSERT(load_time, "Invalid Evaluation"); + KASSERT(apply_time, "Invalid Evaluation"); + + Opm::Tensor in{10}; + in.data_ = {0.67226,0.14416263,0.92274326,0.1796348,0.66230893,0.043213055, +0.9826259,0.23941626,0.21915485,0.30863634}; + + Opm::Tensor out{10}; + out.data_ = {0.,0.14516208,0.017886672,0.3325293,0.19996727,0., +0.,0.5177354,0.47141758,0.2587896}; + + KerasTimer load_timer; + load_timer.Start(); + + KerasModel model; + KASSERT(model.LoadModel("./ml/ml_tools/models/test_relu_10.model"), "Failed to load model"); + + *load_time = load_timer.Stop(); + + KerasTimer apply_timer; + apply_timer.Start(); + + Opm::Tensor predict = out; + KASSERT(model.Apply(&in, &out), "Failed to apply"); + + *apply_time = apply_timer.Stop(); + + for (int i = 0; i < out.dims_[0]; i++) + { + KASSERT_EQ(out(i), predict(i), 1e-6); + } + + return true; +} diff --git a/tests/ml/ml_tools/include/test_scalingdense_10x1.hpp b/tests/ml/ml_tools/include/test_scalingdense_10x1.hpp new file mode 100644 index 00000000000..5d97957c682 --- /dev/null +++ b/tests/ml/ml_tools/include/test_scalingdense_10x1.hpp @@ -0,0 +1,71 @@ + +/* + + * Copyright (c) 2016 Robert W. Rose + * Copyright (c) 2018 Paul Maevskikh + * + * MIT License, see LICENSE.MIT file. + */ + +/* + * Copyright (c) 2024 NORCE + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see . +*/ + +#include +#include +namespace fs = std::filesystem; + +using namespace Opm; +template +bool test_scalingdense_10x1(Evaluation* load_time, Evaluation* apply_time) +{ + printf("TEST scalingdense_10x1\n"); + + KASSERT(load_time, "Invalid Evaluation"); + KASSERT(apply_time, "Invalid Evaluation"); + + Opm::Tensor in{10}; + in.data_ = {0.2977481,0.38920167,0.40215403,0.13134468,0.78471553,0.6248962, +0.3632944,0.61598396,0.41296104,0.68129736}; + + Opm::Tensor out{10}; + out.data_ = {511.78174,453.4956,583.66986,302.5313,474.43433,384.96466,471.34137, +348.2467,418.59845,414.25787}; + + KerasTimer load_timer; + load_timer.Start(); + + KerasModel model; + KASSERT(model.LoadModel("./ml/ml_tools/models/test_scalingdense_10x1.model"), "Failed to load model"); + + *load_time = load_timer.Stop(); + + KerasTimer apply_timer; + apply_timer.Start(); + + Opm::Tensor predict = out; + KASSERT(model.Apply(&in, &out), "Failed to apply"); + + *apply_time = apply_timer.Stop(); + + for (int i = 0; i < out.dims_[0]; i++) + { + KASSERT_EQ(out(i), predict(i), 1e-3); + } + + return true; +} diff --git a/tests/ml/ml_tools/models/test_dense_10x1.model b/tests/ml/ml_tools/models/test_dense_10x1.model new file mode 100644 index 00000000000..ed84459636f Binary files /dev/null and b/tests/ml/ml_tools/models/test_dense_10x1.model differ diff --git a/tests/ml/ml_tools/models/test_dense_10x10.model b/tests/ml/ml_tools/models/test_dense_10x10.model new file mode 100644 index 00000000000..57631b60385 Binary files /dev/null and b/tests/ml/ml_tools/models/test_dense_10x10.model differ diff --git a/tests/ml/ml_tools/models/test_dense_10x10x10.model b/tests/ml/ml_tools/models/test_dense_10x10x10.model new file mode 100644 index 00000000000..c49c543ff70 Binary files /dev/null and b/tests/ml/ml_tools/models/test_dense_10x10x10.model differ diff --git a/tests/ml/ml_tools/models/test_dense_1x1.model b/tests/ml/ml_tools/models/test_dense_1x1.model new file mode 100644 index 00000000000..0a196cf9dca Binary files /dev/null and b/tests/ml/ml_tools/models/test_dense_1x1.model differ diff --git a/tests/ml/ml_tools/models/test_dense_2x2.model b/tests/ml/ml_tools/models/test_dense_2x2.model new file mode 100644 index 00000000000..cbbbff90f0a Binary files /dev/null and b/tests/ml/ml_tools/models/test_dense_2x2.model differ diff --git a/tests/ml/ml_tools/models/test_dense_relu_10.model b/tests/ml/ml_tools/models/test_dense_relu_10.model new file mode 100644 index 00000000000..3b1b656d3d5 Binary files /dev/null and b/tests/ml/ml_tools/models/test_dense_relu_10.model differ diff --git a/tests/ml/ml_tools/models/test_dense_tanh_10.model b/tests/ml/ml_tools/models/test_dense_tanh_10.model new file mode 100644 index 00000000000..4e7541e4aa4 Binary files /dev/null and b/tests/ml/ml_tools/models/test_dense_tanh_10.model differ diff --git a/tests/ml/ml_tools/models/test_relu_10.model b/tests/ml/ml_tools/models/test_relu_10.model new file mode 100644 index 00000000000..27d191391c9 Binary files /dev/null and b/tests/ml/ml_tools/models/test_relu_10.model differ diff --git a/tests/ml/ml_tools/models/test_scalingdense_10x1.model b/tests/ml/ml_tools/models/test_scalingdense_10x1.model new file mode 100644 index 00000000000..2a42cb1a0f1 Binary files /dev/null and b/tests/ml/ml_tools/models/test_scalingdense_10x1.model differ