在 C++ 中调用 keras

使用 frugally-deep 在 C++ 中轻松调用 Keras 模型

Keras 使用起来非常方便,我们有时候需要在 C++ 中调用训练好的模型,希望在 C++ 中调用 Keras 的 model.predict(),但 Keras 并没有提供 C++ API。一种解决方法是使用 TensorFlow 的 C++ API,但编译过程非常繁琐,容易失败。这里我们使用另一种方法,使用 Github 上的 frugally-deep

1 介绍

frugally-deep 是一个用 C++ 实现的库,它可以将 Keras 保存的 .h5 文件直接转为 C++ 中可调用的 .json 文件,经过一步转换后就可以直接调用。frugally-deep 使用起来比较简单,支持非常多常用的模型,无需编译 TensorFlow。同时它也是线程安全的,可以很方便的在多 CPU 上进行前向传播。另外,frugally-deep 不支持使用 GPU,如果不是必须要使用 GPU,frugally-deep 是一个很好的选择。

1.1 支持的模型

frugally-deep 支持大多数常用的模型

  • Add, Concatenate, Subtract, Multiply, Average, Maximum
  • AveragePooling1D/2D, GlobalAveragePooling1D/2D
  • Bidirectional, TimeDistributed, GRU, LSTM, CuDNNGRU, CuDNNLSTM
  • Conv1D/2D, SeparableConv2D, DepthwiseConv2D
  • Cropping1D/2D, ZeroPadding1D/2D
  • BatchNormalization, Dense, Flatten
  • Dropout, AlphaDropout, GaussianDropout, GaussianNoise
  • SpatialDropout1D, SpatialDropout2D, SpatialDropout3D
  • MaxPooling1D/2D, GlobalMaxPooling1D/2D
  • ELU, LeakyReLU, ReLU, SeLU, PReLU
  • Sigmoid, Softmax, Softplus, Tanh
  • UpSampling1D/2D
  • Reshape, Permute
  • Embedding

以及

  • multiple inputs and outputs
  • nested models
  • residual connections
  • shared layers
  • variable input shapes
  • arbitrary complex model architectures / computational graphs
  • custom layers (by passing custom factory functions to load_model)

2 安装

frugally-deep 的安装很简单,可以使用官方安装教程 INSTALL.md 上的命令安装,也可以直接下载源码。使用命令安装的方法在官方的教程中已经很详细了,所以这里采用直接下载源码的方式。

使用 frugally-deep 前需要有 (截止2020年6月13日)

  • 一个支持 C++14 的编译器
  • Python 版本在 3.7 或以上
  • TensorFlow 2.1.1

如果你正在使用 Tensorflow 1.x 的 Keras,安装 TensorFlow 2.x.x 后大多数情况下只需要把 import keras 修改为 import tensorflow.keras 即可,Keras 的改动并不大。

2.1 下载源码

分别前往frugally-deep, FunctionalPlus , Eigenjson 点击右侧的 Code 再点击 Download ZIP 下载这些源码。(Gitlab 直接点击下载)

2.2 解压

假设现在代码根目录下的源文件只有 main.cpp,文件结构为:

1
+-- main.cpp

将 frugally-deep 源码中的 include 文件夹和 keras_export 文件夹解压到与 main.cpp 相同的目录下:

1
2
3
4
+-- include
| +-- fdeep
+-- keras_export
+-- main.cpp

将 FunctionalPlus 源码中 include 文件夹内的 fplus 文件夹复制到 main.cpp 相同目录下的 include 文件夹内:

1
2
3
4
5
+-- include
| +-- fdeep
| +-- fplus
+-- keras_export
+-- main.cpp

同样,将 json 源码中 include 文件夹内的 nlohmann 文件夹复制到 main.cpp 相同目录下的 include 文件夹内:

1
2
3
4
5
6
+-- include
| +-- fdeep
| +-- fplus
| +-- nolhmann
+-- keras_export
+-- main.cpp

最后将 eigen 源码中 Eigen 文件夹放在 main.cpp 相同目录下的 include 文件夹内

1
2
3
4
5
6
7
+-- include
| +-- Eigen
| +-- fdeep
| +-- fplus
| +-- nolhmann
+-- keras_export
+-- main.cpp

需要的文件已经安装完成,下面可以开始使用了。

3 使用

总的来说,使用 frugally-deep 的步骤为:

  1. 在 Python 中训练好模型后,使用 model.save('....h5', include_optimizer=False) 保存模型
  2. 使用 keras_export/convert_model.py.h5 模型转换成 C++ 模型
  3. 在 C++ 中使用 fdeep::load_model(...) 加载模型
  4. 在 C++ 中使用 model.predict(...) 调用模型

下面我们以 frugally-deep 主页 上的例子来说明如何使用。假设我们在 Python 中编写了模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# create_model.py
import numpy as np
from tensorflow.keras.layers import Input, Dense
from tensorflow.keras.models import Model

inputs = Input(shape=(4,))
x = Dense(5, activation='relu')(inputs)
predictions = Dense(3, activation='softmax')(x)
model = Model(inputs=inputs, outputs=predictions)
model.compile(loss='categorical_crossentropy', optimizer='nadam')

model.fit(
np.asarray([[1, 2, 3, 4], [2, 3, 4, 5]]),
np.asarray([[1, 0, 0], [0, 0, 1]]), epochs=10)

model.save('keras_model.h5', include_optimizer=False)

运行 create_model.py 后,当前目录下生成了 keras_model.h5,接下来使用下面的命令进行转换

1
python keras_export/convert_model.py keras_model.h5 fdeep_model.json

看到下面这些就是转换成功了,转换成功后,当前目录下会生成 fdeep_model.json,在 C++ 中读取 fdeep_model.json 就可以直接调用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Forward pass took 0.091729 s.
Forward pass took 0.038896 s.
Forward pass took 0.077791 s.
Starting performance measurements.
Forward pass took 0.037899 s.
Forward pass took 0.037896 s.
Forward pass took 0.043883 s.
Forward pass took 0.038922 s.
Forward pass took 0.042861 s.
Forward pass took 0.04029220000000001 s on average.
Converting model architecture.
Converting model weights.
Done converting model weights.
Calculating model hash.
Model conversion finished.
writing fdeep_model.json

转换过程中,frugally-deep 会自动对模型进行测试,验证相同的输入下 ,模型在 Python 和 C++ 中的输出是否相同。若输出不同会直接报错,所以不必担心转换出错。

转换完成后,在 C++ 中进行调用

1
2
3
4
5
6
7
8
9
10
// main.cpp
#include <fdeep/fdeep.hpp>
int main()
{
const auto model = fdeep::load_model("fdeep_model.json");
const auto result = model.predict(
{fdeep::tensor(fdeep::tensor_shape(static_cast<std::size_t>(4)),
{1, 2, 3, 4})});
std::cout << fdeep::show_tensors(result) << std::endl;
}

这时,以 Visual Studio 为例,编译器会报错

1
fatal error C1083: 无法打开包括文件: “fdeep/fdeep.hpp”: No such file or directory

这是因为还没有添加附加包含目录,右键点击**”解决方案资源管理器”中的项目名称,选择属性** -> 配置属性 -> C/C++ -> 常规,在右侧的附加包含目录中填上 $(ProjectDir)include; 若使用的是 gcc 编译器,要在编译时加上参数 -Iinclude

再次运行 main.cpp,输出:

1
2
3
4
5
Loading json ... done. elapsed time: 0.009921 s
Building model ... done. elapsed time: 0.018725 s
Running test 1 of 1 ... done. elapsed time: 0.003242 s
Loading, constructing, testing of fdeep_model.json took 0.038064 s overall.
[[[[[[[0.7297, 0.1624, 0.1078]]]]]]]

成功输出了结果,调用成功。另外,model.predict()线程安全的,可以直接在多个线程中调用。如果想在多 CPU 上并行预测,使用 model::predict_multi 就会自动在多 CPU 上执行 model.predict()。要注意的是,model::predict_multi 的并行是对多个输入数据的并行,并不是对一个数据的并行。

4 常见问题

4.1 model.predict() 的输入输出类型

model.predict() 的输入类型是 fdeep::tensor,下面的例子说明了如何声明一个 fdeep::tensor 并初始化

1
2
3
4
5
6
7
8
// 声明一个tensor,形状参数在fdeep::tensor_shape()中,0是初始化的值
fdeep::tensor t(fdeep::tensor_shape(3, 1, 1), 0);
// 对tensor赋值最简单的方法是使用t.set()
t.set(fdeep::tensor_pos(0, 0, 0), 1);
t.set(fdeep::tensor_pos(1, 0, 0), 2);
t.set(fdeep::tensor_pos(2, 0, 0), 3);
// 赋值后即可传入model.predict()
const auto result = model.predict(t);

有了 fdeep::tensor 我们可能会需要将其转化为 std::vector 进行后续的操作

1
2
// 将tensor转为std::vector
const std::vector<float> vec = t.to_vector();

需要注意的是,model.predict() 返回的类型是 fdeep::tensors 而不是fdeep::tensor。实际上 fdeep::tensors 是作者给 std::vector<tensor> 定义的别名,若想将其转为 vector,可使用

1
2
// 将vector<tensor>中的第一个tensor转为vector<float>
const auto result_vec = result.front().to_vector();

除此之外,其他的方法可以参考官方 FAQ.md。要注意,frugally-deep 中必须采用 channel-last 的格式。

4.2 error C2653:

使用 Visual Studio 2019 时可能会遇到这个问题

1
error C2653: 'FOut': is not a class or namespace name

这是一个编译器 BUG,详见 Github issue微软官方称已经在 16.5 Preview 2 版本中修复,但是目前升级到最新版 16.6 后仍有很多人反应存在此问题,解决方法是使用 Visual Studio 2017 或 gcc 编译器。

4.3 fdeep::model 没有默认构造函数

当使用 fdeep::model 作为类的成员变量时,会遇到 fdeep::model 没有默认构造函数 的问题,这是作者刻意为之的,解决方法是使用std::unique_ptr<fdeep::model>

下面的例子说明了如何使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// neural_network.h
#pragma once

#include <fdeep/fdeep.hpp>

class NeuralNetwork
{
private:
std::unique_ptr<fdeep::model> model;

public:
NeuralNetwork();
fdeep::tensors predict(fdeep::tensor& t);
};
1
2
3
4
5
6
7
8
9
10
11
12
// neural_network.cpp
#include "neural_network.h"

NeuralNetwork::NeuralNetwork()
{
this->model = std::make_unique<fdeep::model>(fdeep::load_model("fdeep_model.json"));
}

fdeep::tensors NeuralNetwork::predict(fdeep::tensor& t)
{
return this->model->predict({ t });
}

4.4 运行速度比 Python 慢 100 倍

这是因为编译器没有开优化。若使用 Visual Studio ,要把”Debug”模式改为”Release”模式。 gcc 要开 -O3 优化。修改后就正常了。参考FAQ.md

5 总结

如果你需要在 C++ 中调用 model.predict() 且没有使用 GPU 的需求,frugally-deep 是一个很好的选择。

还想了解更多可以阅读官方的英文资料 frugally-deep

6 参考资料

  1. https://github.com/Dobiasd/frugally-deep
原创技术分享,您的支持将鼓励我继续创作