├── docs └── img │ └── keras2cpp.png ├── src ├── baseLayer.cc ├── layers │ ├── flatten.cc │ ├── flatten.h │ ├── elu.h │ ├── embedding.h │ ├── batchNormalization.cc │ ├── conv2d.h │ ├── dense.h │ ├── batchNormalization.h │ ├── maxPooling2d.h │ ├── locally2d.h │ ├── conv1d.h │ ├── locally1d.h │ ├── elu.cc │ ├── embedding.cc │ ├── activation.h │ ├── lstm.h │ ├── dense.cc │ ├── conv1d.cc │ ├── locally1d.cc │ ├── maxPooling2d.cc │ ├── locally2d.cc │ ├── conv2d.cc │ ├── lstm.cc │ └── activation.cc ├── utils.cc ├── model.h ├── baseLayer.h ├── model.cc ├── utils.h ├── tensor.cc └── tensor.h ├── .gitignore ├── cpp_model.cc ├── python_model.py ├── LICENSE ├── .travis.yml ├── CMakeLists.txt ├── README.md ├── keras_model_test.cc ├── keras2cpp.py └── create_unit_tests.py /docs/img/keras2cpp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosha20777/keras2cpp/HEAD/docs/img/keras2cpp.png -------------------------------------------------------------------------------- /src/baseLayer.cc: -------------------------------------------------------------------------------- 1 | #include "baseLayer.h" 2 | namespace keras2cpp { 3 | BaseLayer::~BaseLayer() = default; 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #folders 2 | work/ 3 | # Python Tools for Visual Studio (PTVS) 4 | __pycache__/ 5 | *.pyc 6 | 7 | # Visual Studio profiler 8 | *.psess 9 | *.vsp 10 | *.vspx 11 | *.sap 12 | .vscode/ 13 | build/ 14 | test/ -------------------------------------------------------------------------------- /src/layers/flatten.cc: -------------------------------------------------------------------------------- 1 | #include "flatten.h" 2 | namespace keras2cpp{ 3 | namespace layers{ 4 | Tensor Flatten::operator()(const Tensor& in) const noexcept { 5 | return Tensor(in).flatten(); 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/layers/flatten.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../baseLayer.h" 3 | namespace keras2cpp{ 4 | namespace layers{ 5 | class Flatten final : public Layer { 6 | public: 7 | using Layer::Layer; 8 | Tensor operator()(const Tensor& in) const noexcept override; 9 | }; 10 | } 11 | } -------------------------------------------------------------------------------- /src/layers/elu.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../baseLayer.h" 3 | namespace keras2cpp{ 4 | namespace layers{ 5 | class ELU final : public Layer { 6 | float alpha_{1.f}; 7 | 8 | public: 9 | ELU(Stream& file); 10 | Tensor operator()(const Tensor& in) const noexcept override; 11 | }; 12 | } 13 | } -------------------------------------------------------------------------------- /src/layers/embedding.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../baseLayer.h" 3 | namespace keras2cpp{ 4 | namespace layers{ 5 | class Embedding final : public Layer { 6 | Tensor weights_; 7 | 8 | public: 9 | Embedding(Stream& file); 10 | Tensor operator()(const Tensor& in) const noexcept override; 11 | }; 12 | } 13 | } -------------------------------------------------------------------------------- /src/layers/batchNormalization.cc: -------------------------------------------------------------------------------- 1 | #include "batchNormalization.h" 2 | namespace keras2cpp{ 3 | namespace layers{ 4 | BatchNormalization::BatchNormalization(Stream& file) 5 | : weights_(file), biases_(file) {} 6 | Tensor BatchNormalization::operator()(const Tensor& in) const noexcept { 7 | kassert(in.ndim()); 8 | return in.fma(weights_, biases_); 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/layers/conv2d.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "activation.h" 3 | namespace keras2cpp{ 4 | namespace layers{ 5 | class Conv2D final : public Layer { 6 | Tensor weights_; 7 | Tensor biases_; 8 | Activation activation_; 9 | public: 10 | Conv2D(Stream& file); 11 | Tensor operator()(const Tensor& in) const noexcept override; 12 | }; 13 | } 14 | } -------------------------------------------------------------------------------- /src/layers/dense.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "activation.h" 3 | namespace keras2cpp{ 4 | namespace layers{ 5 | class Dense final : public Layer { 6 | Tensor weights_; 7 | Tensor biases_; 8 | Activation activation_; 9 | public: 10 | Dense(Stream& file); 11 | Tensor operator()(const Tensor& in) const noexcept override; 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/layers/batchNormalization.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../baseLayer.h" 3 | namespace keras2cpp{ 4 | namespace layers{ 5 | class BatchNormalization final : public Layer { 6 | Tensor weights_; 7 | Tensor biases_; 8 | public: 9 | BatchNormalization(Stream& file); 10 | Tensor operator()(const Tensor& in) const noexcept override; 11 | }; 12 | } 13 | } -------------------------------------------------------------------------------- /src/layers/maxPooling2d.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../baseLayer.h" 3 | namespace keras2cpp{ 4 | namespace layers{ 5 | class MaxPooling2D final : public Layer { 6 | unsigned pool_size_y_{0}; 7 | unsigned pool_size_x_{0}; 8 | 9 | public: 10 | MaxPooling2D(Stream& file); 11 | Tensor operator()(const Tensor& in) const noexcept override; 12 | }; 13 | } 14 | } -------------------------------------------------------------------------------- /cpp_model.cc: -------------------------------------------------------------------------------- 1 | #include "src/model.h" 2 | 3 | using keras2cpp::Model; 4 | using keras2cpp::Tensor; 5 | 6 | int main() { 7 | // Initialize model. 8 | auto model = Model::load("example.model"); 9 | 10 | // Create a 1D Tensor on length 10 for input data. 11 | Tensor in{10}; 12 | in.data_ = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 13 | 14 | // Run prediction. 15 | Tensor out = model(in); 16 | out.print(); 17 | return 0; 18 | } 19 | -------------------------------------------------------------------------------- /src/layers/locally2d.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "activation.h" 3 | namespace keras2cpp{ 4 | namespace layers{ 5 | class LocallyConnected2D final : public Layer { 6 | Tensor weights_; 7 | Tensor biases_; 8 | Activation activation_; 9 | public: 10 | LocallyConnected2D(Stream& file); 11 | Tensor operator()(const Tensor& in) const noexcept override; 12 | }; 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /src/utils.cc: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | namespace keras2cpp { 4 | Stream::Stream(const std::string& filename) 5 | : stream_(filename, std::ios::binary) { 6 | stream_.exceptions(); 7 | if (!stream_.is_open()) 8 | throw std::runtime_error("Cannot open " + filename); 9 | } 10 | 11 | Stream& Stream::reads(char* ptr, size_t count) { 12 | stream_.read(ptr, static_cast(count)); 13 | if (!stream_) 14 | throw std::runtime_error("File read failure"); 15 | return *this; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/layers/conv1d.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Robert W. Rose 3 | * Copyright (c) 2018 Paul Maevskikh 4 | * 5 | * MIT License, see LICENSE file. 6 | */ 7 | #pragma once 8 | 9 | #include "activation.h" 10 | namespace keras2cpp{ 11 | namespace layers{ 12 | class Conv1D final : public Layer { 13 | Tensor weights_; 14 | Tensor biases_; 15 | Activation activation_; 16 | 17 | public: 18 | Conv1D(Stream& file); 19 | Tensor operator()(const Tensor& in) const noexcept override; 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/layers/locally1d.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Robert W. Rose 3 | * Copyright (c) 2018 Paul Maevskikh 4 | * 5 | * MIT License, see LICENSE file. 6 | */ 7 | #pragma once 8 | #include "activation.h" 9 | namespace keras2cpp{ 10 | namespace layers{ 11 | class LocallyConnected1D final : public Layer { 12 | Tensor weights_; 13 | Tensor biases_; 14 | Activation activation_; 15 | public: 16 | LocallyConnected1D(Stream& file); 17 | Tensor operator()(const Tensor& in) const noexcept override; 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/layers/elu.cc: -------------------------------------------------------------------------------- 1 | #include "elu.h" 2 | namespace keras2cpp{ 3 | namespace layers{ 4 | ELU::ELU(Stream& file) : alpha_(file) {} 5 | Tensor ELU::operator()(const Tensor& in) const noexcept { 6 | kassert(in.ndim()); 7 | Tensor out; 8 | out.data_.resize(in.size()); 9 | out.dims_ = in.dims_; 10 | 11 | std::transform(in.begin(), in.end(), out.begin(), [this](float x) { 12 | if (x >= 0.f) 13 | return x; 14 | return alpha_ * std::expm1(x); 15 | }); 16 | return out; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /python_model.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from keras import Sequential 3 | from keras.layers import Dense 4 | 5 | #create random data 6 | test_x = np.random.rand(10, 10).astype('f') 7 | test_y = np.random.rand(10).astype('f') 8 | model = Sequential([ 9 | Dense(1, input_dim=10) 10 | ]) 11 | model.compile(loss='mse', optimizer='adam') 12 | 13 | #train model by 1 iteration 14 | model.fit(test_x, test_y, epochs=1, verbose=False) 15 | 16 | #predict 17 | data = np.array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]) 18 | prediction = model.predict(data) 19 | print(prediction) 20 | 21 | #save model 22 | from keras2cpp import export_model 23 | export_model(model, 'example.model') -------------------------------------------------------------------------------- /src/layers/embedding.cc: -------------------------------------------------------------------------------- 1 | #include "embedding.h" 2 | namespace keras2cpp{ 3 | namespace layers{ 4 | Embedding::Embedding(Stream& file) : weights_(file, 2) {} 5 | 6 | Tensor Embedding::operator()(const Tensor& in) const noexcept { 7 | size_t out_i = in.dims_[0]; 8 | size_t out_j = weights_.dims_[1]; 9 | 10 | auto out = Tensor::empty(out_i, out_j); 11 | 12 | for (const auto& it : in.data_) { 13 | auto first = weights_.begin() + cast(it * out_j); 14 | auto last = weights_.begin() + cast(it * out_j + out_j); 15 | out.data_.insert(out.end(), first, last); 16 | } 17 | return out; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/layers/activation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../baseLayer.h" 3 | namespace keras2cpp{ 4 | namespace layers{ 5 | class Activation final : public Layer { 6 | enum _Type : unsigned { 7 | Linear = 1, 8 | Relu = 2, 9 | Elu = 3, 10 | SoftPlus = 4, 11 | SoftSign = 5, 12 | Sigmoid = 6, 13 | Tanh = 7, 14 | HardSigmoid = 8, 15 | SoftMax = 9 16 | }; 17 | _Type type_ {Linear}; 18 | 19 | public: 20 | Activation(Stream& file); 21 | Tensor operator()(const Tensor& in) const noexcept override; 22 | }; 23 | } 24 | } -------------------------------------------------------------------------------- /src/model.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "baseLayer.h" 3 | namespace keras2cpp { 4 | class Model : public Layer { 5 | enum _LayerType : unsigned { 6 | Dense = 1, 7 | Conv1D = 2, 8 | Conv2D = 3, 9 | LocallyConnected1D = 4, 10 | LocallyConnected2D = 5, 11 | Flatten = 6, 12 | ELU = 7, 13 | Activation = 8, 14 | MaxPooling2D = 9, 15 | LSTM = 10, 16 | Embedding = 11, 17 | BatchNormalization = 12, 18 | }; 19 | std::vector> layers_; 20 | 21 | static std::unique_ptr make_layer(Stream&); 22 | 23 | public: 24 | Model(Stream& file); 25 | Tensor operator()(const Tensor& in) const noexcept override; 26 | }; 27 | } -------------------------------------------------------------------------------- /src/baseLayer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "tensor.h" 4 | #include 5 | 6 | namespace keras2cpp { 7 | class BaseLayer { 8 | public: 9 | BaseLayer() = default; 10 | BaseLayer(Stream&) : BaseLayer() {} 11 | BaseLayer(BaseLayer&&) = default; 12 | BaseLayer& operator=(BaseLayer&&) = default; 13 | virtual ~BaseLayer(); 14 | virtual Tensor operator()(const Tensor& in) const noexcept = 0; 15 | }; 16 | template 17 | class Layer : public BaseLayer { 18 | public: 19 | using BaseLayer::BaseLayer; 20 | static Derived load(const std::string& filename) { 21 | Stream file(filename); 22 | return Derived(file); 23 | } 24 | 25 | static std::unique_ptr make(Stream& file) { 26 | return std::make_unique(file); 27 | } 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/layers/lstm.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "activation.h" 3 | namespace keras2cpp{ 4 | namespace layers{ 5 | class LSTM final : public Layer { 6 | Tensor Wi_; 7 | Tensor Ui_; 8 | Tensor bi_; 9 | Tensor Wf_; 10 | Tensor Uf_; 11 | Tensor bf_; 12 | Tensor Wc_; 13 | Tensor Uc_; 14 | Tensor bc_; 15 | Tensor Wo_; 16 | Tensor Uo_; 17 | Tensor bo_; 18 | 19 | Activation inner_activation_; 20 | Activation activation_; 21 | bool return_sequences_{false}; 22 | 23 | std::tuple 24 | step(const Tensor& x, const Tensor& ht_1, const Tensor& ct_1) 25 | const noexcept; 26 | 27 | public: 28 | LSTM(Stream& file); 29 | Tensor operator()(const Tensor& in) const noexcept override; 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/layers/dense.cc: -------------------------------------------------------------------------------- 1 | #include "dense.h" 2 | namespace keras2cpp{ 3 | namespace layers{ 4 | Dense::Dense(Stream& file) 5 | : weights_(file, 2), biases_(file), activation_(file) {} 6 | 7 | Tensor Dense::operator()(const Tensor& in) const noexcept { 8 | kassert(in.dims_.back() == weights_.dims_[1]); 9 | const auto ws = cast(weights_.dims_[1]); 10 | 11 | Tensor tmp; 12 | tmp.dims_ = in.dims_; 13 | tmp.dims_.back() = weights_.dims_[0]; 14 | tmp.data_.reserve(tmp.size()); 15 | 16 | auto tmp_ = std::back_inserter(tmp.data_); 17 | for (auto in_ = in.begin(); in_ < in.end(); in_ += ws) { 18 | auto bias_ = biases_.begin(); 19 | for (auto w = weights_.begin(); w < weights_.end(); w += ws) 20 | *(tmp_++) = std::inner_product(w, w + ws, in_, *(bias_++)); 21 | } 22 | return activation_(tmp); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Piotr, 2016 Robert W. Rose, 2018 Georgy Perevozchikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/layers/conv1d.cc: -------------------------------------------------------------------------------- 1 | #include "conv1d.h" 2 | namespace keras2cpp{ 3 | namespace layers{ 4 | Conv1D::Conv1D(Stream& file) 5 | : weights_(file, 3), biases_(file), activation_(file) {} 6 | 7 | Tensor Conv1D::operator()(const Tensor& in) const noexcept { 8 | kassert(in.dims_[1] == weights_.dims_[2]); 9 | 10 | auto& ww = weights_.dims_; 11 | 12 | size_t offset = ww[1] - 1; 13 | auto tmp = Tensor::empty(in.dims_[0] - offset, ww[0]); 14 | 15 | auto ws0 = cast(ww[2] * ww[1]); 16 | auto ws1 = cast(ww[2]); 17 | 18 | auto tx = cast(tmp.dims_[0]); 19 | 20 | auto i_ptr = in.begin(); 21 | auto b_ptr = biases_.begin(); 22 | auto t_ptr = std::back_inserter(tmp.data_); 23 | 24 | for (ptrdiff_t x = 0; x < tx; ++x) { 25 | auto b_ = b_ptr; 26 | auto i_ = i_ptr + x * ws1; 27 | for (auto w0 = weights_.begin(); w0 < weights_.end(); w0 += ws0) 28 | *(t_ptr++) = std::inner_product(w0, w0 + ws0, i_, *(b_++)); 29 | } 30 | return activation_(tmp); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/layers/locally1d.cc: -------------------------------------------------------------------------------- 1 | #include "locally1d.h" 2 | namespace keras2cpp{ 3 | namespace layers{ 4 | LocallyConnected1D::LocallyConnected1D(Stream& file) 5 | : weights_(file, 3), biases_(file, 2), activation_(file) {} 6 | 7 | Tensor LocallyConnected1D::operator()(const Tensor& in) const noexcept { 8 | auto& ww = weights_.dims_; 9 | 10 | size_t ksize = ww[2] / in.dims_[1]; 11 | kassert(in.dims_[0] + 1 == ww[0] + ksize); 12 | 13 | auto tmp = Tensor::empty(ww[0], ww[1]); 14 | 15 | auto is0 = cast(in.dims_[1]); 16 | auto ts0 = cast(ww[1]); 17 | auto ws0 = cast(ww[2] * ww[1]); 18 | auto ws1 = cast(ww[2]); 19 | 20 | auto i_ptr = in.begin(); 21 | auto b_ptr = biases_.begin(); 22 | auto t_ptr = std::back_inserter(tmp.data_); 23 | 24 | for (auto w_ = weights_.begin(); w_ < weights_.end(); 25 | w_ += ws0, b_ptr += ts0, i_ptr += is0) { 26 | auto b_ = b_ptr; 27 | auto i_ = i_ptr; 28 | for (auto w0 = w_; w0 < w_ + ws0; w0 += ws1) 29 | *(t_ptr++) = std::inner_product(w0, w0 + ws1, i_, *(b_++)); 30 | } 31 | return activation_(tmp); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/layers/maxPooling2d.cc: -------------------------------------------------------------------------------- 1 | #include "maxPooling2d.h" 2 | namespace keras2cpp{ 3 | namespace layers{ 4 | MaxPooling2D::MaxPooling2D(Stream& file) 5 | : pool_size_y_(file), pool_size_x_(file) {} 6 | 7 | Tensor MaxPooling2D::operator()(const Tensor& in) const noexcept { 8 | kassert(in.ndim() == 3); 9 | 10 | const auto& iw = in.dims_; 11 | 12 | Tensor out {iw[0] / pool_size_y_, iw[1] / pool_size_x_, iw[2]}; 13 | out.fill(-std::numeric_limits::infinity()); 14 | 15 | auto is0p = cast(iw[2] * iw[1] * pool_size_y_); 16 | auto is0 = cast(iw[2] * iw[1]); 17 | auto is1p = cast(iw[2] * pool_size_x_); 18 | auto is1 = cast(iw[2]); 19 | auto os_ = cast(iw[2] * out.dims_[1] * out.dims_[0]); 20 | auto os0 = cast(iw[2] * out.dims_[1]); 21 | 22 | auto o_ptr = out.begin(); 23 | auto i_ptr = in.begin(); 24 | for (auto o0 = o_ptr; o0 < o_ptr + os_; o0 += os0, i_ptr += is0p) { 25 | auto i_ = i_ptr; 26 | for (auto o1 = o0; o1 < o0 + os0; o1 += is1, i_ += is1p) 27 | for (auto i0 = i_; i0 < i_ + is0p; i0 += is0) 28 | for (auto i1 = i0; i1 < i0 + is1p; i1 += is1) 29 | std::transform(i1, i1 + is1, o1, o1, [](float x, float y) { 30 | return std::max(x, y); 31 | }); 32 | } 33 | return out; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/layers/locally2d.cc: -------------------------------------------------------------------------------- 1 | #include "locally2d.h" 2 | namespace keras2cpp{ 3 | namespace layers{ 4 | LocallyConnected2D::LocallyConnected2D(Stream& file) 5 | : weights_(file, 4), biases_(file, 3), activation_(file) {} 6 | 7 | Tensor LocallyConnected2D::operator()(const Tensor& in) const noexcept { 8 | /* 9 | // 'in' have shape (x, y, features) 10 | // 'tmp' have shape (new_x, new_y, outputs) 11 | // 'weights' have shape (new_x*new_y, outputs, kernel*features) 12 | // 'biases' have shape (new_x*new_y, outputs) 13 | auto& ww = weights_.dims_; 14 | 15 | size_t ksize = ww[2] / in.dims_[1]; 16 | size_t offset = ksize - 1; 17 | kassert(in.dims_[0] - offset == ww[0]); 18 | 19 | auto tmp = Tensor::empty(ww[0], ww[1]); 20 | 21 | auto is0 = cast(in.dims_[1]); 22 | auto ts0 = cast(ww[1]); 23 | auto ws0 = cast(ww[2] * ww[1]); 24 | auto ws1 = cast(ww[2]); 25 | 26 | auto b_ptr = biases_.begin(); 27 | auto t_ptr = tmp.begin(); 28 | auto i_ptr = in.begin(); 29 | 30 | for (auto w_ = weights_.begin(); w_ < weights_.end(); 31 | w_ += ws0, b_ptr += ts0, t_ptr += ts0, i_ptr += is0) { 32 | auto b_ = b_ptr; 33 | auto t_ = t_ptr; 34 | auto i_ = i_ptr; 35 | for (auto w0 = w_; w0 < w_ + ws0; w0 += ws1) 36 | *(t_++) = std::inner_product(w0, w0 + ws1, i_, *(b_++)); 37 | } 38 | return activation_(tmp); 39 | */ 40 | return activation_(in); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/layers/conv2d.cc: -------------------------------------------------------------------------------- 1 | #include "conv2d.h" 2 | namespace keras2cpp{ 3 | namespace layers{ 4 | Conv2D::Conv2D(Stream& file) 5 | : weights_(file, 4), biases_(file), activation_(file) {} 6 | 7 | Tensor Conv2D::operator()(const Tensor& in) const noexcept { 8 | kassert(in.dims_[2] == weights_.dims_[3]); 9 | 10 | auto& ww = weights_.dims_; 11 | 12 | size_t offset_y = ww[1] - 1; 13 | size_t offset_x = ww[2] - 1; 14 | auto tmp 15 | = Tensor::empty(in.dims_[0] - offset_y, in.dims_[1] - offset_x, ww[0]); 16 | 17 | auto ws_ = cast(ww[3] * ww[2] * ww[1] * ww[0]); 18 | auto ws0 = cast(ww[3] * ww[2] * ww[1]); 19 | auto ws1 = cast(ww[3] * ww[2]); 20 | auto ws2 = cast(ww[3]); 21 | auto is0 = cast(ww[3] * in.dims_[1]); 22 | 23 | auto ty = cast(tmp.dims_[0]); 24 | auto tx = cast(tmp.dims_[1]); 25 | 26 | auto w_ptr = weights_.begin(); 27 | auto b_ptr = biases_.begin(); 28 | auto t_ptr = std::back_inserter(tmp.data_); 29 | auto i_ptr = in.begin(); 30 | 31 | for (ptrdiff_t y = 0; y < ty; ++y) 32 | for (ptrdiff_t x = 0; x < tx; ++x) { 33 | auto b_ = b_ptr; 34 | auto i_ = i_ptr + y * is0 + x * ws2; 35 | for (auto w0 = w_ptr; w0 < w_ptr + ws_; w0 += ws0) { 36 | auto tmp_ = 0.f; 37 | auto i0 = i_; 38 | for (auto w1 = w0; w1 < w0 + ws0; w1 += ws1, i0 += is0) 39 | tmp_ = std::inner_product(w1, w1 + ws1, i0, tmp_); 40 | *(++t_ptr) = *(b_++) + tmp_; 41 | } 42 | } 43 | return activation_(tmp); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/model.cc: -------------------------------------------------------------------------------- 1 | #include "model.h" 2 | #include "layers/conv1d.h" 3 | #include "layers/conv2d.h" 4 | #include "layers/dense.h" 5 | #include "layers/elu.h" 6 | #include "layers/embedding.h" 7 | #include "layers/flatten.h" 8 | #include "layers/locally1d.h" 9 | #include "layers/locally2d.h" 10 | #include "layers/lstm.h" 11 | #include "layers/maxPooling2d.h" 12 | #include "layers/batchNormalization.h" 13 | 14 | namespace keras2cpp { 15 | std::unique_ptr Model::make_layer(Stream& file) { 16 | switch (static_cast(file)) { 17 | case Dense: 18 | return layers::Dense::make(file); 19 | case Conv1D: 20 | return layers::Conv1D::make(file); 21 | case Conv2D: 22 | return layers::Conv2D::make(file); 23 | case LocallyConnected1D: 24 | return layers::LocallyConnected1D::make(file); 25 | case LocallyConnected2D: 26 | return layers::LocallyConnected2D::make(file); 27 | case Flatten: 28 | return layers::Flatten::make(file); 29 | case ELU: 30 | return layers::ELU::make(file); 31 | case Activation: 32 | return layers::Activation::make(file); 33 | case MaxPooling2D: 34 | return layers::MaxPooling2D::make(file); 35 | case LSTM: 36 | return layers::LSTM::make(file); 37 | case Embedding: 38 | return layers::Embedding::make(file); 39 | case BatchNormalization: 40 | return layers::BatchNormalization::make(file); 41 | } 42 | return nullptr; 43 | } 44 | 45 | Model::Model(Stream& file) { 46 | auto count = static_cast(file); 47 | layers_.reserve(count); 48 | for (size_t i = 0; i != count; ++i) 49 | layers_.push_back(make_layer(file)); 50 | } 51 | 52 | Tensor Model::operator()(const Tensor& in) const noexcept { 53 | Tensor out = in; 54 | for (auto&& layer : layers_) 55 | out = (*layer)(out); 56 | return out; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define stringify(x) #x 11 | 12 | #define cast(x) static_cast(x) 13 | 14 | #ifndef NDEBUG 15 | #define kassert_eq(x, y, eps) \ 16 | { \ 17 | auto x_ = static_cast(x); \ 18 | auto y_ = static_cast(y); \ 19 | if (std::abs(x_ - y_) > eps) { \ 20 | printf( \ 21 | "ASSERT [%s:%d] %f isn't equal to %f ('%s' != '%s')\n", \ 22 | __FILE__, __LINE__, x_, y_, stringify(x), stringify(y)); \ 23 | exit(-1); \ 24 | } \ 25 | } 26 | #define kassert(x) \ 27 | if (!(x)) { \ 28 | printf( \ 29 | "ASSERT [%s:%d] '%s' failed\n", __FILE__, __LINE__, stringify(x)); \ 30 | exit(-1); \ 31 | } 32 | #else 33 | #define kassert(x) ; 34 | #define kassert_eq(x, y, eps) ; 35 | #endif 36 | 37 | namespace keras2cpp { 38 | template 39 | auto timeit(Callable&& callable, Args&&... args) { 40 | using namespace std::chrono; 41 | auto begin = high_resolution_clock::now(); 42 | if constexpr (std::is_void_v>) 43 | return std::make_tuple( 44 | (std::invoke(callable, args...), nullptr), 45 | duration(high_resolution_clock::now() - begin).count()); 46 | else 47 | return std::make_tuple( 48 | std::invoke(callable, args...), 49 | duration(high_resolution_clock::now() - begin).count()); 50 | } 51 | class Stream { 52 | std::ifstream stream_; 53 | 54 | public: 55 | Stream(const std::string& filename); 56 | Stream& reads(char*, size_t); 57 | 58 | template < 59 | typename T, 60 | typename = std::enable_if_t>> 61 | operator T() noexcept { 62 | T value; 63 | reads(reinterpret_cast(&value), sizeof(T)); 64 | return value; 65 | } 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /src/layers/lstm.cc: -------------------------------------------------------------------------------- 1 | #include "lstm.h" 2 | #include 3 | namespace keras2cpp{ 4 | namespace layers{ 5 | LSTM::LSTM(Stream& file) 6 | : Wi_(file, 2) 7 | , Ui_(file, 2) 8 | , bi_(file, 2) // Input 9 | , Wf_(file, 2) 10 | , Uf_(file, 2) 11 | , bf_(file, 2) // Forget 12 | , Wc_(file, 2) 13 | , Uc_(file, 2) 14 | , bc_(file, 2) // State 15 | , Wo_(file, 2) 16 | , Uo_(file, 2) 17 | , bo_(file, 2) // Output 18 | , inner_activation_(file) 19 | , activation_(file) 20 | , return_sequences_(static_cast(file)) {} 21 | 22 | Tensor LSTM::operator()(const Tensor& in) const noexcept { 23 | // Assume 'bo_' always keeps the output shape and we will always 24 | // receive one single sample. 25 | size_t out_dim = bo_.dims_[1]; 26 | size_t steps = in.dims_[0]; 27 | 28 | Tensor c_tm1 {1, out_dim}; 29 | 30 | if (!return_sequences_) { 31 | Tensor out {1, out_dim}; 32 | for (size_t s = 0; s < steps; ++s) 33 | std::tie(out, c_tm1) = step(in.select(s), out, c_tm1); 34 | return out.flatten(); 35 | } 36 | 37 | auto out = Tensor::empty(steps, out_dim); 38 | Tensor last {1, out_dim}; 39 | 40 | for (size_t s = 0; s < steps; ++s) { 41 | std::tie(last, c_tm1) = step(in.select(s), last, c_tm1); 42 | out.data_.insert(out.end(), last.begin(), last.end()); 43 | } 44 | return out; 45 | } 46 | 47 | std::tuple 48 | LSTM::step(const Tensor& x, const Tensor& h_tm1, const Tensor& c_tm1) const 49 | noexcept { 50 | auto i_ = x.dot(Wi_) + h_tm1.dot(Ui_) + bi_; 51 | auto f_ = x.dot(Wf_) + h_tm1.dot(Uf_) + bf_; 52 | auto c_ = x.dot(Wc_) + h_tm1.dot(Uc_) + bc_; 53 | auto o_ = x.dot(Wo_) + h_tm1.dot(Uo_) + bo_; 54 | 55 | auto cc = inner_activation_(f_) * c_tm1 56 | + inner_activation_(i_) * activation_(c_); 57 | auto out = inner_activation_(o_) * activation_(cc); 58 | return std::make_tuple(out, cc); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | #sudo: required 3 | language: python 4 | matrix: 5 | include: 6 | - python: 3.6 7 | env: KERAS_BACKEND=tensorflow 8 | before_install: 9 | # C++17 10 | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test 11 | - sudo add-apt-repository -y ppa:jonathonf/python-3.6 12 | - sudo apt-get update -qq 13 | 14 | install: 15 | # python 16 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 17 | - bash miniconda.sh -b -p $HOME/miniconda 18 | - export PATH="$HOME/miniconda/bin:$PATH" 19 | - hash -r 20 | - conda config --set always_yes yes --set changeps1 no 21 | - conda update -q conda 22 | # Useful for debugging any issues with conda 23 | - conda info -a 24 | 25 | - travis_retry conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION 26 | - source activate test-environment 27 | 28 | - travis_retry pip install --only-binary=numpy,scipy,pandas numpy nose scipy h5py theano pytest pytest-pep8 pandas --progress-bar off 29 | - pip install keras_applications keras_preprocessing --progress-bar off 30 | 31 | # set library path 32 | - export LD_LIBRARY_PATH=$HOME/miniconda/envs/test-environment/lib/:$LD_LIBRARY_PATH 33 | 34 | # install pydot for visualization tests 35 | - travis_retry conda install -q $MKL pydot graphviz $PIL 36 | 37 | #- pip install -e .[tests] --progress-bar off 38 | 39 | # install TensorFlow (CPU version). 40 | - pip install tensorflow==1.12 --progress-bar off 41 | - python -c "import tensorflow" 42 | - pip install keras --progress-bar off 43 | - python -c "import keras" 44 | 45 | # install mkdocs 46 | - pip install mkdocs --progress-bar off 47 | 48 | # C++17 49 | - sudo apt-get install -qq g++-6 50 | - sudo apt-get install -qq g++-7 51 | - sudo apt-get install -qq build-essential 52 | - export TRAVIS_COMPILER=/usr/bin/g++-7 53 | - export CXX=/usr/bin/g++-7 54 | - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-7 90 55 | - sudo apt-get install -qq make cmake 56 | # cppcheck 57 | - sudo apt-get install -qq cppcheck 58 | script: 59 | # Build and run this project 60 | - mkdir build && cd build 61 | - cmake .. 62 | - cmake --build . 63 | - python ../python_model.py 64 | - ./keras2cpp 65 | # cppcheck 66 | - cppcheck --quiet --error-exitcode=1 . 67 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.9) 2 | 3 | # common options 4 | 5 | enable_language(CXX) 6 | 7 | # set(CMAKE_C_COMPILER "/usr/bin/clang") 8 | # set(CMAKE_CXX_COMPILER "/usr/bin/clang++") 9 | 10 | set(CMAKE_CXX_STANDARD 17) 11 | 12 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 13 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ 14 | -W -Wall -Wextra -Wpedantic \ 15 | -Waggressive-loop-optimizations \ 16 | -Wcast-align -Wcast-qual \ 17 | -Wdouble-promotion -Wduplicated-branches -Wduplicated-cond \ 18 | -Wfloat-equal -Wformat=2 -Wformat-signedness -Wframe-larger-than=32768 \ 19 | -Wlogical-op \ 20 | -Wnull-dereference \ 21 | -Wodr -Wold-style-cast \ 22 | -Wshadow=local -Wshift-overflow=2 -Wstrict-aliasing=2 -Wsuggest-final-methods -Wsuggest-final-types -Wsync-nand \ 23 | -Wtrampolines \ 24 | -Wuseless-cast -Wno-unused-but-set-parameter \ 25 | -Wwrite-strings" 26 | CACHE INTERNAL "" 27 | ) 28 | endif() 29 | 30 | if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 31 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ 32 | -Weverything \ 33 | -Wno-class-varargs -Wno-padded \ 34 | -Wc++17-compat \ 35 | -Wno-c++98-compat -Wno-c++98-compat-pedantic" 36 | # -Wno-c++14-extensions 37 | # -Wno-c++17-extensions 38 | # -Wno-switch-enum -Wno-unused-macros 39 | CACHE INTERNAL "" 40 | ) 41 | endif() 42 | 43 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g" 44 | CACHE INTERNAL "" 45 | ) 46 | 47 | set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} \ 48 | -g -pg -no-pie \ 49 | -O3 -ffast-math -fno-rtti" 50 | CACHE INTERNAL "" 51 | ) 52 | set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO 53 | "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} -pg" 54 | CACHE INTERNAL "" 55 | ) 56 | 57 | # rolled loops works faster here 58 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} \ 59 | -O3 -ffast-math -fno-rtti" 60 | CACHE INTERNAL "" 61 | ) 62 | 63 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 64 | set(CMAKE_CXX_FLAGS_MINSIZEREL "${CMAKE_CXX_FLAGS_MINSIZEREL} -Os" 65 | CACHE INTERNAL "") 66 | endif() 67 | 68 | if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 69 | set(CMAKE_CXX_FLAGS_MINSIZEREL "${CMAKE_CXX_FLAGS_MINSIZEREL} -Oz" 70 | CACHE INTERNAL "") 71 | endif() 72 | 73 | set(CMAKE_CXX_FLAGS_MINSIZEREL "${CMAKE_CXX_FLAGS_MINSIZEREL} \ 74 | -O3 -ffast-math -fno-math-errno -fno-unroll-loops \ 75 | -fno-rtti -fno-stack-protector -fno-ident \ 76 | -fomit-frame-pointer -ffunction-sections -fdata-sections \ 77 | -fmerge-all-constants" 78 | CACHE INTERNAL "" 79 | ) 80 | 81 | # project configuration 82 | 83 | project(keras2cpp) 84 | 85 | include_directories(keras2cpp PRIVATE "./include") 86 | 87 | add_executable(keras2cpp "") 88 | target_sources(keras2cpp PRIVATE 89 | src/utils.cc 90 | src/baseLayer.cc 91 | src/layers/activation.cc 92 | src/layers/conv1d.cc 93 | src/layers/conv2d.cc 94 | src/layers/dense.cc 95 | src/layers/elu.cc 96 | src/layers/embedding.cc 97 | src/layers/flatten.cc 98 | src/layers/lstm.cc 99 | src/layers/locally1d.cc 100 | src/layers/locally2d.cc 101 | src/layers/maxPooling2d.cc 102 | src/layers/batchNormalization.cc 103 | src/model.cc 104 | src/tensor.cc 105 | cpp_model.cc 106 | ) 107 | 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keras2cpp ![release](https://img.shields.io/github/release/gosha20777/keras2cpp.svg?colorB=red) ![lisense](https://img.shields.io/github/license/gosha20777/keras2cpp.svg) [![Build Status](https://travis-ci.org/gosha20777/keras2cpp.svg?branch=master)](https://travis-ci.org/gosha20777/keras2cpp) 2 | ![keras2cpp](docs/img/keras2cpp.png) 3 | 4 | Keras2cpp is a small library for running trained Keras models from a C++ application without any dependences. 5 | 6 | Design goals: 7 | 8 | - Compatibility with networks generated by Keras using TensorFlow backend. 9 | - CPU only, no GPU. 10 | - No external dependencies, standard library, C++17. 11 | - Model stored on disk in binary format and can be quickly read. 12 | - Model stored in memory in contiguous block for better cache performance. 13 | 14 | *Not not layer and activation types are supported yet. Work in progress* 15 | 16 | Supported Keras layers: 17 | - [x] Dense 18 | - [x] Convolution1D 19 | - [x] Convolution2D 20 | - [ ] Convolution3D 21 | - [x] Flatten 22 | - [x] ELU 23 | - [x] Activation 24 | - [x] MaxPooling2D 25 | - [x] Embedding 26 | - [x] LocallyConnected1D 27 | - [x] LocallyConnected2D 28 | - [x] LSTM 29 | - [ ] GRU 30 | - [ ] CNN 31 | - [X] BatchNormalization 32 | 33 | Supported activation: 34 | - [x] linear 35 | - [x] relu 36 | - [x] softplus 37 | - [x] tanh 38 | - [x] sigmoid 39 | - [x] hard_sigmoid 40 | - [x] elu 41 | - [x] softsign 42 | - [x] softmax 43 | 44 | Other tasks: 45 | - [x] Create unit tests 46 | - [x] Create Makefile 47 | - [x] Code refactoring *(in progress)* 48 | 49 | The project is compatible with Keras 2.x (all versions) and Python 3.x 50 | 51 | # Example 52 | 53 | python_model.py: 54 | 55 | ```python 56 | import numpy as np 57 | from keras import Sequential 58 | from keras.layers import Dense 59 | 60 | #create random data 61 | test_x = np.random.rand(10, 10).astype('f') 62 | test_y = np.random.rand(10).astype('f') 63 | model = Sequential([ 64 | Dense(1, input_dim=10) 65 | ]) 66 | model.compile(loss='mse', optimizer='adam') 67 | 68 | #train model by 1 iteration 69 | model.fit(test_x, test_y, epochs=1, verbose=False) 70 | 71 | #predict 72 | data = np.array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]) 73 | prediction = model.predict(data) 74 | print(prediction) 75 | 76 | #save model 77 | from keras2cpp import export_model 78 | export_model(model, 'example.model') 79 | ``` 80 | 81 | cpp_model.cc: 82 | 83 | ```c++ 84 | #include "src/model.h" 85 | 86 | using keras2cpp::Model; 87 | using keras2cpp::Tensor; 88 | 89 | int main() { 90 | // Initialize model. 91 | auto model = Model::load("example.model"); 92 | 93 | // Create a 1D Tensor on length 10 for input data. 94 | Tensor in{10}; 95 | in.data_ = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 96 | 97 | // Run prediction. 98 | Tensor out = model(in); 99 | out.print(); 100 | return 0; 101 | } 102 | ``` 103 | 104 | # How to build and run 105 | 106 | *Tested with Keras 2.2.1, Python 3.6* 107 | 108 | ```bash 109 | $ git clone https://github.com/gosha20777/keras2cpp.git 110 | $ cd keras2cpp 111 | $ mkdir build && cd build 112 | $ python3 ../python_model.py 113 | [[-1.85735667]] 114 | 115 | $ cmake .. 116 | $ cmake --build . 117 | $ ./keras2cpp 118 | [ -1.857357 ] 119 | ``` 120 | 121 | # License 122 | 123 | MIT 124 | 125 | # Similar projects 126 | 127 | I found another similar projects on Github: 128 | - ; 129 | - 130 | - 131 | 132 | But It works only with Keras 1 and didn’t work for me. 133 | That's why I wrote my own implementation. 134 | -------------------------------------------------------------------------------- /src/layers/activation.cc: -------------------------------------------------------------------------------- 1 | #include "activation.h" 2 | namespace keras2cpp{ 3 | namespace layers{ 4 | Activation::Activation(Stream& file) : type_(file) { 5 | switch (type_) { 6 | case Linear: 7 | case Relu: 8 | case Elu: 9 | case SoftPlus: 10 | case SoftSign: 11 | case HardSigmoid: 12 | case Sigmoid: 13 | case Tanh: 14 | case SoftMax: 15 | return; 16 | } 17 | kassert(false); 18 | } 19 | 20 | Tensor Activation::operator()(const Tensor& in) const noexcept { 21 | Tensor out {in.size()}; 22 | out.dims_ = in.dims_; 23 | 24 | switch (type_) { 25 | case Linear: 26 | std::copy(in.begin(), in.end(), out.begin()); 27 | break; 28 | case Relu: 29 | std::transform(in.begin(), in.end(), out.begin(), [](float x) { 30 | if (x < 0.f) 31 | return 0.f; 32 | return x; 33 | }); 34 | break; 35 | case Elu: 36 | std::transform(in.begin(), in.end(), out.begin(), [](float x) { 37 | if (x < 0.f) 38 | return std::expm1(x); 39 | return x; 40 | }); 41 | break; 42 | case SoftPlus: 43 | std::transform(in.begin(), in.end(), out.begin(), [](float x) { 44 | return std::log1p(std::exp(x)); 45 | }); 46 | break; 47 | case SoftSign: 48 | std::transform(in.begin(), in.end(), out.begin(), [](float x) { 49 | return x / (1.f + std::abs(x)); 50 | }); 51 | break; 52 | case HardSigmoid: 53 | std::transform(in.begin(), in.end(), out.begin(), [](float x) { 54 | if (x <= -2.5f) 55 | return 0.f; 56 | if (x >= 2.5f) 57 | return 1.f; 58 | return (x * .2f) + .5f; 59 | }); 60 | break; 61 | case Sigmoid: 62 | std::transform(in.begin(), in.end(), out.begin(), [](float x) { 63 | float z = std::exp(-std::abs(x)); 64 | if (x < 0) 65 | return z / (1.f + z); 66 | return 1.f / (1.f + z); 67 | }); 68 | break; 69 | case Tanh: 70 | std::transform(in.begin(), in.end(), out.begin(), [](float x) { 71 | return std::tanh(x); 72 | }); 73 | break; 74 | case SoftMax: { 75 | auto channels = cast(in.dims_.back()); 76 | kassert(channels > 1); 77 | 78 | Tensor tmp = in; 79 | std::transform(in.begin(), in.end(), tmp.begin(), [](float x) { 80 | return std::exp(x); 81 | }); 82 | 83 | auto out_ = out.begin(); 84 | for (auto t_ = tmp.begin(); t_ != tmp.end(); t_ += channels) { 85 | // why std::reduce not in libstdc++ yet? 86 | auto norm = 1.f / std::accumulate(t_, t_ + channels, 0.f); 87 | std::transform( 88 | t_, t_ + channels, out_, [norm](float x) { return norm * x; }); 89 | out_ += channels; 90 | } 91 | break; 92 | } 93 | } 94 | return out; 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/tensor.cc: -------------------------------------------------------------------------------- 1 | #include "tensor.h" 2 | 3 | namespace keras2cpp { 4 | Tensor::Tensor(Stream& file, size_t rank) : Tensor() { 5 | kassert(rank); 6 | 7 | dims_.reserve(rank); 8 | std::generate_n(std::back_inserter(dims_), rank, [&file] { 9 | unsigned stride = file; 10 | kassert(stride > 0); 11 | return stride; 12 | }); 13 | 14 | data_.resize(size()); 15 | file.reads(reinterpret_cast(data_.data()), sizeof(float) * size()); 16 | } 17 | 18 | Tensor Tensor::unpack(size_t row) const noexcept { 19 | kassert(ndim() >= 2); 20 | size_t pack_size = std::accumulate(dims_.begin() + 1, dims_.end(), 0u); 21 | 22 | auto base = row * pack_size; 23 | auto first = begin() + cast(base); 24 | auto last = begin() + cast(base + pack_size); 25 | 26 | Tensor x; 27 | x.dims_ = std::vector(dims_.begin() + 1, dims_.end()); 28 | x.data_ = std::vector(first, last); 29 | return x; 30 | } 31 | 32 | Tensor Tensor::select(size_t row) const noexcept { 33 | auto x = unpack(row); 34 | x.dims_.insert(x.dims_.begin(), 1); 35 | return x; 36 | } 37 | 38 | Tensor& Tensor::operator+=(const Tensor& other) noexcept { 39 | kassert(dims_ == other.dims_); 40 | std::transform(begin(), end(), other.begin(), begin(), std::plus<>()); 41 | return *this; 42 | } 43 | 44 | Tensor& Tensor::operator*=(const Tensor& other) noexcept { 45 | kassert(dims_ == other.dims_); 46 | std::transform(begin(), end(), other.begin(), begin(), std::multiplies<>()); 47 | return *this; 48 | } 49 | 50 | Tensor Tensor::fma(const Tensor& scale, const Tensor& bias) const noexcept { 51 | kassert(dims_ == scale.dims_); 52 | kassert(dims_ == bias.dims_); 53 | 54 | Tensor result; 55 | result.dims_ = dims_; 56 | result.data_.resize(data_.size()); 57 | 58 | auto k_ = scale.begin(); 59 | auto b_ = bias.begin(); 60 | auto r_ = result.begin(); 61 | for (auto x_ = begin(); x_ != end();) 62 | *(r_++) = *(x_++) * *(k_++) + *(b_++); 63 | 64 | return result; 65 | } 66 | 67 | Tensor Tensor::dot(const Tensor& other) const noexcept { 68 | kassert(ndim() == 2); 69 | kassert(other.ndim() == 2); 70 | kassert(dims_[1] == other.dims_[1]); 71 | 72 | Tensor tmp {dims_[0], other.dims_[0]}; 73 | 74 | auto ts = cast(tmp.dims_[1]); 75 | auto is = cast(dims_[1]); 76 | 77 | auto i_ = begin(); 78 | for (auto t0 = tmp.begin(); t0 != tmp.end(); t0 += ts, i_ += is) { 79 | auto o_ = other.begin(); 80 | for (auto t1 = t0; t1 != t0 + ts; ++t1, o_ += is) 81 | *t1 = std::inner_product(i_, i_ + is, o_, 0.f); 82 | } 83 | return tmp; 84 | } 85 | 86 | void Tensor::print() const noexcept { 87 | std::vector steps(ndim()); 88 | std::partial_sum( 89 | dims_.rbegin(), dims_.rend(), steps.rbegin(), std::multiplies<>()); 90 | 91 | size_t count = 0; 92 | for (auto&& it : data_) { 93 | for (auto step : steps) 94 | if (count % step == 0) 95 | printf("["); 96 | printf("%f", static_cast(it)); 97 | ++count; 98 | for (auto step : steps) 99 | if (count % step == 0) 100 | printf("]"); 101 | if (count != steps[0]) 102 | printf(", "); 103 | } 104 | printf("\n"); 105 | } 106 | 107 | void Tensor::print_shape() const noexcept { 108 | printf("("); 109 | size_t count = 0; 110 | for (auto&& dim : dims_) { 111 | printf("%zu", dim); 112 | if ((++count) != dims_.size()) 113 | printf(", "); 114 | } 115 | printf(")\n"); 116 | } 117 | } -------------------------------------------------------------------------------- /keras_model_test.cc: -------------------------------------------------------------------------------- 1 | #include "test/benchmark.h" 2 | #include "test/conv_2x2.h" 3 | #include "test/conv_3x3.h" 4 | #include "test/conv_3x3x3.h" 5 | #include "test/conv_hard_sigmoid_2x2.h" 6 | #include "test/conv_sigmoid_2x2.h" 7 | #include "test/conv_softplus_2x2.h" 8 | #include "test/dense_10x1.h" 9 | #include "test/dense_10x10.h" 10 | #include "test/dense_10x10x10.h" 11 | #include "test/dense_1x1.h" 12 | #include "test/dense_2x2.h" 13 | #include "test/dense_relu_10.h" 14 | #include "test/dense_tanh_10.h" 15 | #include "test/elu_10.h" 16 | #include "test/embedding_64.h" 17 | #include "test/lstm_simple_7x20.h" 18 | #include "test/lstm_simple_stacked_16x9.h" 19 | #include "test/lstm_stacked_64x83.h" 20 | #include "test/maxpool2d_1x1.h" 21 | #include "test/maxpool2d_2x2.h" 22 | #include "test/maxpool2d_3x2x2.h" 23 | #include "test/maxpool2d_3x3x3.h" 24 | #include "test/relu_10.h" 25 | #include "src/model.h" 26 | 27 | using namespace keras2cpp; 28 | 29 | namespace test { 30 | inline void basics() noexcept { 31 | { 32 | const int i = 3; 33 | const int j = 5; 34 | const int k = 10; 35 | Tensor t {i, j, k}; 36 | 37 | float c = 1.f; 38 | for (size_t ii = 0; ii < i; ++ii) 39 | for (size_t jj = 0; jj < j; ++jj) 40 | for (size_t kk = 0; kk < k; ++kk) { 41 | t(ii, jj, kk) = c; 42 | c += 1.f; 43 | } 44 | c = 1.f; 45 | size_t cc = 0; 46 | for (size_t ii = 0; ii < i; ++ii) 47 | for (size_t jj = 0; jj < j; ++jj) 48 | for (size_t kk = 0; kk < k; ++kk) { 49 | kassert_eq(t(ii, jj, kk), c, 1e-9); 50 | kassert_eq(t.data_[cc], c, 1e-9); 51 | c += 1.f; 52 | ++cc; 53 | } 54 | } 55 | { 56 | const size_t i = 2; 57 | const size_t j = 3; 58 | const size_t k = 4; 59 | const size_t l = 5; 60 | Tensor t {i, j, k, l}; 61 | 62 | float c = 1.f; 63 | for (size_t ii = 0; ii < i; ++ii) 64 | for (size_t jj = 0; jj < j; ++jj) 65 | for (size_t kk = 0; kk < k; ++kk) 66 | for (size_t ll = 0; ll < l; ++ll) { 67 | t(ii, jj, kk, ll) = c; 68 | c += 1.f; 69 | } 70 | c = 1.f; 71 | size_t cc = 0; 72 | for (size_t ii = 0; ii < i; ++ii) 73 | for (size_t jj = 0; jj < j; ++jj) 74 | for (size_t kk = 0; kk < k; ++kk) 75 | for (size_t ll = 0; ll < l; ++ll) { 76 | kassert_eq(t(ii, jj, kk, ll), c, 1e-9); 77 | kassert_eq(t.data_[cc], c, 1e-9); 78 | c += 1.f; 79 | ++cc; 80 | } 81 | } 82 | { 83 | Tensor a {2, 2}; 84 | Tensor b {2, 2}; 85 | 86 | a.data_ = {1.0, 2.0, 3.0, 5.0}; 87 | b.data_ = {2.0, 5.0, 4.0, 1.0}; 88 | 89 | Tensor result = a + b; 90 | kassert(result.data_ == std::vector({3.0, 7.0, 7.0, 6.0})); 91 | } 92 | { 93 | Tensor a {2, 2}; 94 | Tensor b {2, 2}; 95 | 96 | a.data_ = {1.0, 2.0, 3.0, 5.0}; 97 | b.data_ = {2.0, 5.0, 4.0, 1.0}; 98 | 99 | Tensor result = a * b; 100 | kassert(result.data_ == std::vector({2.0, 10.0, 12.0, 5.0})); 101 | } 102 | { 103 | Tensor a {1, 2}; 104 | Tensor b {1, 2}; 105 | 106 | a.data_ = {1.0, 2.0}; 107 | b.data_ = {2.0, 5.0}; 108 | 109 | Tensor result = a.dot(b); 110 | kassert(result.data_ == std::vector({12.0})); 111 | } 112 | { 113 | Tensor a {2, 1}; 114 | Tensor b {2, 1}; 115 | 116 | a.data_ = {1.0, 2.0}; 117 | b.data_ = {2.0, 5.0}; 118 | 119 | Tensor result = a.dot(b); 120 | kassert(result.data_ == std::vector({2.0, 5.0, 4.0, 10.0})); 121 | } 122 | } 123 | } 124 | 125 | int main() { 126 | test::basics(); 127 | test::dense_1x1(); 128 | test::dense_10x1(); 129 | test::dense_2x2(); 130 | test::dense_10x10(); 131 | test::dense_10x10x10(); 132 | test::conv_2x2(); 133 | test::conv_3x3(); 134 | test::conv_3x3x3(); 135 | test::elu_10(); 136 | test::relu_10(); 137 | test::dense_relu_10(); 138 | test::dense_tanh_10(); 139 | test::conv_softplus_2x2(); 140 | test::conv_hard_sigmoid_2x2(); 141 | test::conv_sigmoid_2x2(); 142 | test::maxpool2d_1x1(); 143 | test::maxpool2d_2x2(); 144 | test::maxpool2d_3x2x2(); 145 | test::maxpool2d_3x3x3(); 146 | test::lstm_simple_7x20(); 147 | test::lstm_simple_stacked_16x9(); 148 | test::lstm_stacked_64x83(); 149 | test::embedding_64(); 150 | 151 | const size_t n = 10; // Run benchmark "n" times. 152 | 153 | double total_load_time = 0.0; 154 | double total_apply_time = 0.0; 155 | 156 | for (size_t i = 0; i < n; ++i) { 157 | auto [load_time, apply_time] = test::benchmark(); 158 | total_load_time += load_time; 159 | total_apply_time += apply_time; 160 | } 161 | printf("Benchmark network loads in %fs\n", total_load_time / n); 162 | printf("Benchmark network runs in %fs\n", total_apply_time / n); 163 | 164 | return 0; 165 | } -------------------------------------------------------------------------------- /src/tensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "utils.h" 5 | //#include "reader.h" 6 | 7 | namespace keras2cpp { 8 | class Tensor { 9 | public: 10 | Tensor() = default; 11 | template < 12 | typename... Size, 13 | typename = std::enable_if_t<(... && std::is_integral_v)>> 14 | Tensor(Size... sizes) { 15 | resize(static_cast(sizes)...); 16 | } 17 | 18 | Tensor(Stream& file, size_t rank = 1); 19 | 20 | template 21 | static auto empty(Size... sizes); 22 | 23 | template 24 | void resize(Size... sizes) noexcept; 25 | 26 | inline size_t size() const noexcept; 27 | inline size_t ndim() const noexcept; 28 | inline Tensor& flatten() noexcept; 29 | 30 | inline float& operator()(size_t) noexcept; 31 | inline float& operator()(size_t, size_t) noexcept; 32 | inline float& operator()(size_t, size_t, size_t) noexcept; 33 | inline float& operator()(size_t, size_t, size_t, size_t) noexcept; 34 | inline float operator()(size_t) const noexcept; 35 | inline float operator()(size_t, size_t) const noexcept; 36 | inline float operator()(size_t, size_t, size_t) const noexcept; 37 | inline float operator()(size_t, size_t, size_t, size_t) const noexcept; 38 | 39 | inline std::vector::iterator begin() noexcept; 40 | inline std::vector::const_iterator begin() const noexcept; 41 | inline std::vector::iterator end() noexcept; 42 | inline std::vector::const_iterator end() const noexcept; 43 | 44 | inline void fill(float value) noexcept; 45 | 46 | Tensor unpack(size_t row) const noexcept; 47 | Tensor select(size_t row) const noexcept; 48 | 49 | Tensor& operator+=(const Tensor& other) noexcept; 50 | Tensor& operator*=(const Tensor& other) noexcept; 51 | Tensor fma(const Tensor& scale, const Tensor& bias) const noexcept; 52 | Tensor dot(const Tensor& other) const noexcept; 53 | 54 | void print() const noexcept; 55 | void print_shape() const noexcept; 56 | 57 | std::vector dims_; 58 | std::vector data_; 59 | }; 60 | 61 | template 62 | auto Tensor::empty(Size... sizes) { 63 | Tensor tensor; 64 | tensor.dims_ = {static_cast(sizes)...}; 65 | tensor.data_.reserve(tensor.size()); 66 | return tensor; 67 | } 68 | template 69 | void Tensor::resize(Size... sizes) noexcept { 70 | dims_ = {static_cast(sizes)...}; 71 | data_.resize(size()); 72 | } 73 | size_t Tensor::size() const noexcept { 74 | size_t elements = 1; 75 | for (const auto& it : dims_) 76 | elements *= it; 77 | return elements; 78 | } 79 | size_t Tensor::ndim() const noexcept { 80 | return dims_.size(); 81 | } 82 | Tensor& Tensor::flatten() noexcept { 83 | kassert(ndim()); 84 | dims_ = {size()}; 85 | return *this; 86 | } 87 | float& Tensor::operator()(size_t i) noexcept { 88 | kassert(ndim() == 1); 89 | kassert(i < dims_[0]); 90 | return data_[i]; 91 | } 92 | float Tensor::operator()(size_t i) const noexcept { 93 | kassert(ndim() == 1); 94 | kassert(i < dims_[0]); 95 | return data_[i]; 96 | } 97 | float& Tensor::operator()(size_t i, size_t j) noexcept { 98 | kassert(ndim() == 2); 99 | kassert(i < dims_[0]); 100 | kassert(j < dims_[1]); 101 | return data_[dims_[1] * i + j]; 102 | } 103 | float Tensor::operator()(size_t i, size_t j) const noexcept { 104 | kassert(ndim() == 2); 105 | kassert(i < dims_[0]); 106 | kassert(j < dims_[1]); 107 | return data_[dims_[1] * i + j]; 108 | } 109 | float& Tensor::operator()(size_t i, size_t j, size_t k) noexcept { 110 | kassert(ndim() == 3); 111 | kassert(i < dims_[0]); 112 | kassert(j < dims_[1]); 113 | kassert(k < dims_[2]); 114 | return data_[dims_[2] * (dims_[1] * i + j) + k]; 115 | } 116 | float Tensor::operator()(size_t i, size_t j, size_t k) const noexcept { 117 | kassert(ndim() == 3); 118 | kassert(i < dims_[0]); 119 | kassert(j < dims_[1]); 120 | kassert(k < dims_[2]); 121 | return data_[dims_[2] * (dims_[1] * i + j) + k]; 122 | } 123 | float& Tensor::operator()(size_t i, size_t j, size_t k, size_t l) noexcept { 124 | kassert(ndim() == 4); 125 | kassert(i < dims_[0]); 126 | kassert(j < dims_[1]); 127 | kassert(k < dims_[2]); 128 | kassert(l < dims_[3]); 129 | return data_[dims_[3] * (dims_[2] * (dims_[1] * i + j) + k) + l]; 130 | } 131 | float Tensor::operator()(size_t i, size_t j, size_t k, size_t l) const 132 | noexcept { 133 | kassert(ndim() == 4); 134 | kassert(i < dims_[0]); 135 | kassert(j < dims_[1]); 136 | kassert(k < dims_[2]); 137 | kassert(l < dims_[3]); 138 | return data_[dims_[3] * (dims_[2] * (dims_[1] * i + j) + k) + l]; 139 | } 140 | void Tensor::fill(float value) noexcept { 141 | std::fill(begin(), end(), value); 142 | } 143 | std::vector::iterator Tensor::begin() noexcept { 144 | return data_.begin(); 145 | } 146 | std::vector::const_iterator Tensor::begin() const noexcept { 147 | return data_.begin(); 148 | } 149 | std::vector::iterator Tensor::end() noexcept { 150 | return data_.end(); 151 | } 152 | std::vector::const_iterator Tensor::end() const noexcept { 153 | return data_.end(); 154 | } 155 | inline Tensor operator+(Tensor lhs, const Tensor& rhs) noexcept { 156 | lhs += rhs; 157 | return lhs; 158 | } 159 | inline Tensor operator*(Tensor lhs, const Tensor& rhs) noexcept { 160 | lhs *= rhs; 161 | return lhs; 162 | } 163 | } -------------------------------------------------------------------------------- /keras2cpp.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from functools import singledispatch 3 | 4 | import numpy as np 5 | from keras.layers import ( 6 | Dense, 7 | Conv1D, Conv2D, 8 | LocallyConnected1D, LocallyConnected2D, 9 | Flatten, 10 | ELU, 11 | Activation, 12 | MaxPooling2D, 13 | LSTM, 14 | Embedding, 15 | BatchNormalization, 16 | ) 17 | 18 | LAYERS = ( 19 | Dense, 20 | Conv1D, Conv2D, 21 | LocallyConnected1D, LocallyConnected2D, 22 | Flatten, 23 | ELU, 24 | Activation, 25 | MaxPooling2D, 26 | LSTM, 27 | Embedding, 28 | BatchNormalization, 29 | ) 30 | 31 | ACTIVATIONS = ( 32 | 'linear', 33 | 'relu', 34 | 'elu', 35 | 'softplus', 36 | 'softsign', 37 | 'sigmoid', 38 | 'tanh', 39 | 'hard_sigmoid', 40 | 'softmax', 41 | ) 42 | 43 | 44 | def write_tensor(f, data, dims=1): 45 | """ 46 | Writes tensor as flat array of floats to file in 1024 chunks, 47 | prevents memory explosion writing very large arrays to disk 48 | when calling struct.pack(). 49 | """ 50 | for stride in data.shape[:dims]: 51 | f.write(struct.pack('I', stride)) 52 | 53 | data = data.flatten() 54 | step = 1024 55 | written = 0 56 | 57 | for i in np.arange(0, len(data), step): 58 | remaining = min(len(data) - i, step) 59 | written += remaining 60 | f.write(struct.pack(f'={remaining}f', *data[i: i + remaining])) 61 | 62 | assert written == len(data) 63 | 64 | 65 | def export_activation(activation, f): 66 | try: 67 | f.write(struct.pack('I', ACTIVATIONS.index(activation) + 1)) 68 | except ValueError as exc: 69 | raise NotImplementedError(activation) from exc 70 | 71 | 72 | @singledispatch 73 | def export(layer, _): 74 | raise NotImplementedError(layer) 75 | 76 | 77 | @export.register(Flatten) 78 | def _(_0, _1): 79 | pass 80 | 81 | 82 | @export.register(Activation) 83 | def _(layer, f): 84 | activation = layer.get_config()['activation'] 85 | export_activation(activation, f) 86 | 87 | 88 | @export.register(ELU) 89 | def _(layer, f): 90 | f.write(struct.pack('f', layer.alpha)) 91 | 92 | 93 | @export.register(BatchNormalization) 94 | def _(layer, f): 95 | epsilon = layer.epsilon 96 | gamma = layer.get_weights()[0] 97 | beta = layer.get_weights()[1] 98 | pop_mean = layer.get_weights()[2] 99 | pop_variance = layer.get_weights()[3] 100 | 101 | weights = gamma / np.sqrt(pop_variance + epsilon) 102 | biases = beta - pop_mean * weights 103 | 104 | write_tensor(f, weights) 105 | write_tensor(f, biases) 106 | 107 | 108 | @export.register(Dense) 109 | def _(layer, f): 110 | # shape: (outputs, dims) 111 | weights = layer.get_weights()[0].transpose() 112 | biases = layer.get_weights()[1] 113 | activation = layer.get_config()['activation'] 114 | 115 | write_tensor(f, weights, 2) 116 | write_tensor(f, biases) 117 | export_activation(activation, f) 118 | 119 | 120 | @export.register(Conv1D) 121 | def _(layer, f): 122 | # shape: (outputs, steps, dims) 123 | weights = layer.get_weights()[0].transpose(2, 0, 1) 124 | biases = layer.get_weights()[1] 125 | activation = layer.get_config()['activation'] 126 | 127 | write_tensor(f, weights, 3) 128 | write_tensor(f, biases) 129 | export_activation(activation, f) 130 | 131 | 132 | @export.register(Conv2D) 133 | def _(layer, f): 134 | # shape: (outputs, rows, cols, depth) 135 | weights = layer.get_weights()[0].transpose(3, 0, 1, 2) 136 | biases = layer.get_weights()[1] 137 | activation = layer.get_config()['activation'] 138 | 139 | write_tensor(f, weights, 4) 140 | write_tensor(f, biases) 141 | export_activation(activation, f) 142 | 143 | 144 | @export.register(LocallyConnected1D) 145 | def _(layer, f): 146 | # shape: (new_steps, outputs, ksize*dims) 147 | weights = layer.get_weights()[0].transpose(0, 2, 1) 148 | biases = layer.get_weights()[1] 149 | activation = layer.get_config()['activation'] 150 | 151 | write_tensor(f, weights, 3) 152 | write_tensor(f, biases, 2) 153 | export_activation(activation, f) 154 | 155 | 156 | @export.register(LocallyConnected2D) 157 | def _(layer, f): 158 | # shape: (rows*cols, outputs, ksize*depth) 159 | weights = layer.get_weights()[0] 160 | # weights = weights.transpose(0, 2, 1) 161 | biases = layer.get_weights()[1] 162 | activation = layer.get_config()['activation'] 163 | 164 | write_tensor(f, weights, 3) 165 | write_tensor(f, biases, 2) 166 | export_activation(activation, f) 167 | 168 | 169 | @export.register(MaxPooling2D) 170 | def _(layer, f): 171 | pool_size = layer.get_config()['pool_size'] 172 | 173 | f.write(struct.pack('I', pool_size[0])) 174 | f.write(struct.pack('I', pool_size[1])) 175 | 176 | 177 | @export.register(LSTM) 178 | def _(layer, f): 179 | inner_activation = layer.get_config()['recurrent_activation'] 180 | activation = layer.get_config()['activation'] 181 | return_sequences = int(layer.get_config()['return_sequences']) 182 | 183 | weights = layer.get_weights() 184 | units = layer.units 185 | 186 | kernel, rkernel, bias = ([x[i: i+units] for i in range(0, 4*units, units)] 187 | for x in (weights[0].transpose(), 188 | weights[1].transpose(), 189 | weights[2])) 190 | bias = [x.reshape(1, -1) for x in bias] 191 | for tensors in zip(kernel, rkernel, bias): 192 | for tensor in tensors: 193 | write_tensor(f, tensor, 2) 194 | 195 | export_activation(inner_activation, f) 196 | export_activation(activation, f) 197 | f.write(struct.pack('I', return_sequences)) 198 | 199 | 200 | @export.register(Embedding) 201 | def _(layer, f): 202 | weights = layer.get_weights()[0] 203 | write_tensor(f, weights, 2) 204 | 205 | 206 | def export_model(model, filename): 207 | with open(filename, 'wb') as f: 208 | layers = [layer for layer in model.layers 209 | if type(layer).__name__ not in ['Dropout']] 210 | f.write(struct.pack('I', len(layers))) 211 | 212 | for layer in layers: 213 | f.write(struct.pack('I', LAYERS.index(type(layer)) + 1)) 214 | export(layer, f) 215 | -------------------------------------------------------------------------------- /create_unit_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pprint 3 | import re 4 | 5 | import numpy as np 6 | from keras import backend as K 7 | from keras.models import Sequential 8 | from keras.layers import ( 9 | Activation, BatchNormalization, Conv2D, Dense, Dropout, 10 | ELU, Embedding, Flatten, LocallyConnected1D, LSTM, MaxPooling2D, 11 | ) 12 | 13 | from keras2cpp import export_model 14 | 15 | np.set_printoptions(precision=25, threshold=np.nan) 16 | 17 | 18 | os.makedirs('test', exist_ok=True) 19 | for path_ in os.listdir('test'): 20 | os.remove('test/' + path_) 21 | 22 | os.makedirs('models', exist_ok=True) 23 | for path_ in os.listdir('models'): 24 | os.remove('models/' + path_) 25 | 26 | 27 | def c_array(a): 28 | s = pprint.pformat(a.flatten()) 29 | 30 | s = re.sub(r'[ \t\n]*', '', s) 31 | s = re.sub(r'[ \t]*,[ \t]*', ', ', s) 32 | s = re.sub(r'[ \t]*\][, \t]*', '} ', s) 33 | s = re.sub(r'[ \t]*\[[ \t]*', '{', s) 34 | s = s.replace('array(', '').replace(')', '') 35 | s = re.sub(r'[, \t]*dtype=float32', '', s) 36 | s = s.strip() 37 | 38 | shape = '' 39 | if a.shape: 40 | shape = repr(a.shape) 41 | shape = re.sub(r',*\)', '}', shape.replace('(', '{')) 42 | else: 43 | shape = '{1}' 44 | return shape, s 45 | 46 | 47 | TEST_CASE = '''/* Autogenerated file, DO NOT EDIT */ 48 | #pragma once 49 | 50 | #include "src/model.h" 51 | 52 | namespace test { 53 | inline auto %(name)s() { 54 | printf("TEST %(name)s\\n"); 55 | #pragma GCC diagnostic push 56 | #pragma GCC diagnostic ignored "-Wconversion" 57 | keras2cpp::Tensor in%(x_shape)s; 58 | in.data_ = %(x_data)s; 59 | keras2cpp::Tensor target%(y_shape)s; 60 | target.data_ = %(y_data)s; 61 | #pragma GCC diagnostic pop 62 | auto [model, load_time] = keras2cpp::timeit(keras2cpp::Model::load, "%(path)s"); 63 | auto [output, apply_time] = keras2cpp::timeit(model, in); 64 | for (size_t i = 0; i < target.dims_[0]; ++i) 65 | kassert_eq(target(i), output(i), %(eps)s); 66 | return std::make_tuple(load_time, apply_time); 67 | } 68 | } 69 | ''' 70 | 71 | 72 | def output_testcase(model, test_x, test_y, name, eps): 73 | print(f'Processing {name}') 74 | model.compile(loss='mse', optimizer='adam') 75 | model.fit(test_x, test_y, epochs=1, verbose=False) 76 | predict_y = model.predict(test_x).astype('f') 77 | print(model.summary()) 78 | 79 | path = os.path.abspath(f'models/{name}.model') 80 | export_model(model, path) 81 | 82 | with open(f'test/{name}.h', 'w') as f: 83 | x_shape, x_data = c_array(test_x[0]) 84 | y_shape, y_data = c_array(predict_y[0]) 85 | 86 | f.write(TEST_CASE % dict(name=name, path=path, eps=eps, 87 | x_shape=x_shape, 88 | x_data=x_data, 89 | y_shape=y_shape, 90 | y_data=y_data)) 91 | 92 | 93 | # Dense 1x1 94 | test_x = np.arange(10) 95 | test_y = test_x * 10 + 1 96 | model = Sequential([ 97 | Dense(1, input_dim=1) 98 | ]) 99 | output_testcase(model, test_x, test_y, 'dense_1x1', '1e-6') 100 | 101 | 102 | # Dense 10x1 103 | test_x = np.random.rand(10, 10).astype('f') 104 | test_y = np.random.rand(10).astype('f') 105 | model = Sequential([ 106 | Dense(1, input_dim=10) 107 | ]) 108 | output_testcase(model, test_x, test_y, 'dense_10x1', '1e-6') 109 | 110 | 111 | # Dense 2x2 112 | test_x = np.random.rand(10, 2).astype('f') 113 | test_y = np.random.rand(10).astype('f') 114 | model = Sequential([ 115 | Dense(2, input_dim=2), 116 | Dense(1) 117 | ]) 118 | output_testcase(model, test_x, test_y, 'dense_2x2', '1e-6') 119 | 120 | 121 | # Dense 10x10 122 | test_x = np.random.rand(10, 10).astype('f') 123 | test_y = np.random.rand(10).astype('f') 124 | model = Sequential([ 125 | Dense(10, input_dim=10), 126 | Dense(1) 127 | ]) 128 | output_testcase(model, test_x, test_y, 'dense_10x10', '1e-6') 129 | 130 | 131 | # Dense 10x10x10 132 | test_x = np.random.rand(10, 10).astype('f') 133 | test_y = np.random.rand(10, 10).astype('f') 134 | model = Sequential([ 135 | Dense(10, input_dim=10), 136 | Dense(10) 137 | ]) 138 | output_testcase(model, test_x, test_y, 'dense_10x10x10', '1e-6') 139 | 140 | 141 | # Conv 2x2 142 | test_x = np.random.rand(10, 2, 2, 1).astype('f') 143 | test_y = np.random.rand(10, 1).astype('f') 144 | model = Sequential([ 145 | Conv2D(1, (2, 2), input_shape=(2, 2, 1)), 146 | Flatten(), 147 | Dense(1) 148 | ]) 149 | output_testcase(model, test_x, test_y, 'conv_2x2', '1e-6') 150 | 151 | 152 | # Conv 3x3 153 | test_x = np.random.rand(10, 3, 3, 1).astype('f').astype('f') 154 | test_y = np.random.rand(10, 1).astype('f') 155 | model = Sequential([ 156 | Conv2D(1, (3, 3), input_shape=(3, 3, 1)), 157 | Flatten(), 158 | Dense(1) 159 | ]) 160 | output_testcase(model, test_x, test_y, 'conv_3x3', '1e-6') 161 | 162 | 163 | # Conv 3x3x3 164 | test_x = np.random.rand(10, 10, 10, 3).astype('f') 165 | test_y = np.random.rand(10, 1).astype('f') 166 | model = Sequential([ 167 | Conv2D(3, (3, 3), input_shape=(10, 10, 3)), 168 | Flatten(), 169 | BatchNormalization(), 170 | Dense(1) 171 | ]) 172 | output_testcase(model, test_x, test_y, 'conv_3x3x3', '1e-6') 173 | 174 | 175 | # Activation ELU 176 | test_x = np.random.rand(1, 10).astype('f') 177 | test_y = np.random.rand(1, 1).astype('f') 178 | model = Sequential([ 179 | Dense(10, input_dim=10), 180 | ELU(alpha=0.5), 181 | Dense(1, activation='elu') 182 | ]) 183 | output_testcase(model, test_x, test_y, 'elu_10', '1e-6') 184 | 185 | 186 | # Activation relu 187 | test_x = np.random.rand(1, 10).astype('f') 188 | test_y = np.random.rand(1, 10).astype('f') 189 | model = Sequential([ 190 | Dense(10, input_dim=10), 191 | Activation('relu') 192 | ]) 193 | output_testcase(model, test_x, test_y, 'relu_10', '1e-6') 194 | 195 | 196 | # Dense relu 197 | test_x = np.random.rand(1, 10).astype('f') 198 | test_y = np.random.rand(1, 10).astype('f') 199 | model = Sequential([ 200 | Dense(10, input_dim=10, activation='relu'), 201 | Dense(10, input_dim=10, activation='relu'), 202 | Dense(10, input_dim=10, activation='relu') 203 | ]) 204 | output_testcase(model, test_x, test_y, 'dense_relu_10', '1e-6') 205 | 206 | 207 | # Dense relu 208 | test_x = np.random.rand(1, 10).astype('f') 209 | test_y = np.random.rand(1, 10).astype('f') 210 | model = Sequential([ 211 | Dense(10, input_dim=10, activation='tanh'), 212 | Dense(10, input_dim=10, activation='tanh'), 213 | Dense(10, input_dim=10, activation='tanh') 214 | ]) 215 | output_testcase(model, test_x, test_y, 'dense_tanh_10', '1e-6') 216 | 217 | 218 | # Conv softplus 219 | test_x = np.random.rand(10, 2, 2, 1).astype('f') 220 | test_y = np.random.rand(10, 1).astype('f') 221 | model = Sequential([ 222 | Conv2D(1, (2, 2), input_shape=(2, 2, 1), activation='softplus'), 223 | Flatten(), 224 | Dense(1) 225 | ]) 226 | output_testcase(model, test_x, test_y, 'conv_softplus_2x2', '1e-6') 227 | 228 | 229 | # Conv hardsigmoid 230 | test_x = np.random.rand(10, 2, 2, 1).astype('f') 231 | test_y = np.random.rand(10, 1).astype('f') 232 | model = Sequential([ 233 | Conv2D(1, (2, 2), input_shape=(2, 2, 1), activation='hard_sigmoid'), 234 | Flatten(), 235 | Dense(1) 236 | ]) 237 | output_testcase(model, test_x, test_y, 'conv_hard_sigmoid_2x2', '1e-6') 238 | 239 | 240 | # Conv sigmoid 241 | test_x = np.random.rand(10, 2, 2, 1).astype('f') 242 | test_y = np.random.rand(10, 1).astype('f') 243 | model = Sequential([ 244 | Conv2D(1, (2, 2), input_shape=(2, 2, 1), activation='sigmoid'), 245 | Flatten(), 246 | Dense(1) 247 | ]) 248 | output_testcase(model, test_x, test_y, 'conv_sigmoid_2x2', '1e-6') 249 | 250 | 251 | # Maxpooling2D 1x1 252 | test_x = np.random.rand(10, 10, 10, 1).astype('f') 253 | test_y = np.random.rand(10, 1).astype('f') 254 | model = Sequential([ 255 | MaxPooling2D(pool_size=(1, 1), input_shape=(10, 10, 1)), 256 | Flatten(), 257 | Dense(1) 258 | ]) 259 | output_testcase(model, test_x, test_y, 'maxpool2d_1x1', '1e-6') 260 | 261 | 262 | # Maxpooling2D 2x2 263 | test_x = np.random.rand(10, 10, 10, 1).astype('f') 264 | test_y = np.random.rand(10, 1).astype('f') 265 | model = Sequential([ 266 | MaxPooling2D(pool_size=(2, 2), input_shape=(10, 10, 1)), 267 | Flatten(), 268 | Dense(1) 269 | ]) 270 | output_testcase(model, test_x, test_y, 'maxpool2d_2x2', '1e-6') 271 | 272 | 273 | # Maxpooling2D 3x2x2 274 | test_x = np.random.rand(10, 10, 10, 3).astype('f') 275 | test_y = np.random.rand(10, 1).astype('f') 276 | model = Sequential([ 277 | MaxPooling2D(pool_size=(2, 2), input_shape=(10, 10, 3)), 278 | Flatten(), 279 | Dense(1) 280 | ]) 281 | output_testcase(model, test_x, test_y, 'maxpool2d_3x2x2', '1e-6') 282 | 283 | 284 | # Maxpooling2D 3x3x3 285 | test_x = np.random.rand(10, 10, 10, 3).astype('f') 286 | test_y = np.random.rand(10, 1).astype('f') 287 | model = Sequential([ 288 | MaxPooling2D(pool_size=(3, 3), input_shape=(10, 10, 3)), 289 | Flatten(), 290 | Dense(1) 291 | ]) 292 | output_testcase(model, test_x, test_y, 'maxpool2d_3x3x3', '1e-6') 293 | 294 | 295 | # LSTM simple 7x20 296 | test_x = np.random.rand(10, 7, 20).astype('f') 297 | test_y = np.random.rand(10, 3).astype('f') 298 | model = Sequential([ 299 | LSTM(3, return_sequences=False, input_shape=(7, 20)) 300 | ]) 301 | output_testcase(model, test_x, test_y, 'lstm_simple_7x20', '1e-6') 302 | 303 | 304 | # LSTM simple stacked 16x9 305 | test_x = np.random.rand(10, 16, 9).astype('f') 306 | test_y = np.random.rand(10, 1).astype('f') 307 | model = Sequential([ 308 | LSTM(16, return_sequences=False, input_shape=(16, 9)), 309 | Dense(3, input_dim=16, activation='tanh'), 310 | Dense(1) 311 | ]) 312 | output_testcase(model, test_x, test_y, 'lstm_simple_stacked_16x9', '1e-6') 313 | 314 | 315 | # LSTM stacked 64x83 316 | test_x = np.random.rand(10, 64, 83).astype('f') 317 | test_y = np.random.rand(10, 1).astype('f') 318 | model = Sequential([ 319 | LSTM(16, return_sequences=True, input_shape=(64, 83)), 320 | LSTM(16, return_sequences=False), 321 | Dense(1, activation='sigmoid') 322 | ]) 323 | output_testcase(model, test_x, test_y, 'lstm_stacked_64x83', '1e-6') 324 | 325 | 326 | # Embedding 64 327 | np.random.seed(10) 328 | test_x = np.random.randint(100, size=(32, 10)).astype('f') 329 | test_y = np.random.rand(32, 20).astype('f') 330 | model = Sequential([ 331 | Embedding(100, 64, input_length=10), 332 | Flatten(), 333 | # Dropout(0.5), 334 | Dense(20, activation='sigmoid') 335 | ]) 336 | output_testcase(model, test_x, test_y, 'embedding_64', '1e-6') 337 | 338 | 339 | # Benchmark 340 | test_x = np.random.rand(1, 128, 128, 3).astype('f') 341 | test_y = np.random.rand(1, 10).astype('f') 342 | model = Sequential([ 343 | Conv2D(16, (7, 7), input_shape=(128, 128, 3), activation='relu'), 344 | MaxPooling2D(pool_size=(3, 3)), 345 | ELU(), 346 | Conv2D(8, (3, 3)), 347 | Flatten(), 348 | Dense(1000, activation='relu'), 349 | Dense(10) 350 | ]) 351 | output_testcase(model, test_x, test_y, 'benchmark', '1e-3') 352 | 353 | 354 | os.system('clang-format -i --style=file test/*.h') 355 | --------------------------------------------------------------------------------