C++ 从 HDF5 文件读取 Keras 神经网络模型和参数
一. 背景与应用
我就是想这样做, 具体应用可参考 C++ 和 OpenCV 实现卷积神经网络并加载 Keras 训练好的参数进行预测 和 C++ 和 OpenCV 实现 Keras Sequential 网络
二. Keras 保存的 HDF5 参数(Weight) 文件分析
所有的 HDF5 文件中都有一个 Root Group, 所有的 Object 都在 Root Group 下, 所以从 Root Group 为起点出发, 就可以遍历所有 Object (Group\Dataset…)
注意, 在下面可能不再提示, 所有的 HDF5 Object 在用完之后都要调用对应的 close 函数关闭
神经网络参数文件用 HDF5 View 打开如下图. 每一层的参数就是一个 Group, 里面包含了 另一个 Group, 这个同名的 Group 中包含两个 Dataset, 分别是 kernel:0 和 bias:0. 全连接层、卷积模板 等需要训练的参数 都在 kernel:0 里面
注意图中有的 Group 上面有一个 A 字母, 这个 A 字母表示这个 Group 的 Property 中的 Attributes 不为空
二. 遍历起点
怎样找到这个 Root Group? Root Group 的名字就是一个斜杠 “/”, 用这个名称就可以找到它
1. 工程需要包含的头文件和库文件
工程的配置可参考 C\C++ 写 HDF5 文件示例
#include <stdint.h>
#include <hdf5.h>
#include <H5Cpp.h>
#include <iostream>
using namespace H5;
using namespace std;
#ifdef _DEBUG
#pragma comment(lib, "hdf5_D.lib")
#pragma comment(lib, "hdf5_cpp_D.lib")
#else
#pragma comment(lib, "hdf5.lib")
#pragma comment(lib, "hdf5_cpp.lib")
#endif
2. 打开文件
// 用只读方式打开文件, 用完后记得要调用 file.close() 关闭释放资源
H5File file("文件路径, 你需要自己修改, 文件名包括.扩展名", H5F_ACC_RDONLY);
3. 打开 Root Group 并输出其中 Object 名称
这个步骤不是必须的, 只是为了看到有哪些 Object 而已
// 打开 Root Grout, 用完后记得要调用 rg.close() 关闭释放资源
Group rg(file.getObjId("/"));
// 取得 Group 中 Object 的数量
const hsize_t objs = rg.getNumObjs();
for (hsize_t i = 0; i < objs; i++)
{
// 用 Index 为参数获取 Object 名字
const H5std_string name = rg.getObjnameByIdx(i);
cout << "Obj_name_" << i + 1 << ": " << name.c_str() << endl;
}
rg.close();
file.close();
cout << endl << endl;
system("pause");
输出如下, 和上面 HDF5 View 中显示的一样, 这个顺序和神经网络定义的时候的顺序是不是样的, 有用的是 name, 不是顺序
如果要看某个 Group 中内容, 再对这个 Group 进行相同的操作就可以了
三. 读取 Dataset 中的数据
以读取 conv_1 中的 kernel:0 为例
1. 打开 kernel:0
在上面我们已经定位到了 Root Group, 现在要打开 kernel:0, 只需要其路径就可以办到
// 用完之后也要调用 dset.close() 关闭
DataSet dset(rg.getObjId("/conv_1/conv_1/kernel:0")); // 绝对路径
// 如果使用相对路径的话, 变成
// DataSet dset(rg.getObjId("conv_1/conv_1/kernel:0"));
// 还可以这样
// DataSet dset = file.openDataSet("/conv_1/conv_1/kernel:0");
2. 读取 DataSpace 信息
DataSpace 描述了数据的 维度, 属性 等信息, 要读取数据, 就需要知道这些信息, 从而分配存在空间
DataSpace dsp = dset.getSpace();
// rank 描述的是有几个维, 就是有 rank 个描述维度大小的变量
const int rank = dsp.getSimpleExtentNdims();
// 创建一个数组存储每一个维度的大小
hsize_t *dims = new hsize_t[rank];
// 这里的 ndims 和 rank 是一样的
const int ndims = dsp.getSimpleExtentDims(dims);
// 输出各维度的大小
for (int i = 0; i < rank; i++)
{
cout << "Dimension_" << i + 1 << " = " << dims[i] << endl;
}
delete []dims;
dims = nullptr;
// Dataset 中数据的数据类型
DataType dt = dset.getDataType();
const H5T_class_t t = dt.getClass();
cout << "kernel:0 type is " << t << endl;
dt.close();
dsp.close();
dset.close();
我打开的文件输出的是 (3, 5, 3, 8), 表示卷积模板有 8 个, 每个大小是 3 * 5 * 3 , 最后一个 3 表示 这个模板是 3 通道的, 这些数据和定义神经网络的结构是相关的, 不同的结构输出就不一样
数据类型输出, 可以看出, 类型 = 1, 表示 Kernel:0 中的数据是 H5T_FLOAT, 就是浮点型数据
3. 读取 Dataset 数据
上面已经知道了数据维度和数据类型, 现在要读数据就很简单了
// 数据在内存中的字节数除以数据类型得到 buf 的大小
const hsize_t data_size = dset.getInMemDataSize() / sizeof(float);
float *buf = new float[data_size];
// 读出数据到 buf 中
dset.read(buf, dt);
for (int i = 0; i < data_size; i++)
{
cout << buf[i] << endl;
}
delete []buf;
buf = nullptr;
部分数据如下
至此神经网络的其他参数就可以用相同的方法读取了
四. 读取神经网络保存的模型结构
除了参数, 还可以读取保存的模型结构, 只是模型结构保存在模型文件(.h5) 中的 Attributes\model_config 中, 读出来之后解析字符串就可以了
在 model_weights 的 Attributes\layer_names 中还有按模型定义顺序的每一层的名称
那这个要怎么读取呢?
1. 打开文件
H5File file("文件路径, 你需要自己修改, 文件名包括.扩展名", H5F_ACC_RDONLY);
2. 读取 Attributes
Attribute at = rg.openAttribute("model_config");
// 数据类型
DataType dt = at.getDataType();
// 读出的字符串
H5std_string ati;
at.read(dt, ati);
cout << ati.c_str() << endl;
dt.close();
at.close();
rg.close();
file.close();
输出如下
要读取其他的数据也大同小异. 有了模型定义之后, 就可以用 C++ 加载模型和参数实现神经网络预测了
有一点需要注意, 保存的模型里面也有 model_weights, 它和单独保存的 weights 可能不一样, 因为你在训练的时候有指定 save_best_only = “True”
五. 代码下载
完整的代码可下载 VS2015 x64 代码示例