C++ 和 OpenCV 实现 Keras Sequential 网络
一. 背景
Keras 的 Sequential 类型的网络很好用, 只是简单的 add 函数就可以一层一层地添加网络, 本文还是以 mnist 数据集为例, 用 C++ 和 OpenCV 来实现这样的网络. 在 C++ 和 OpenCV 实现卷积神经网络并加载 Keras 训练好的参数进行预测 中已经讲过每一层的实现方式, 但是比较散, 本文就以 C++ 类 的方式来实现相同的功能, 但是不具有训练的功能, 训练我们还是在 Python 环境下进行的. 还能做到从 HDF5 文件中加载 模型和参数 进行预测, 而不需要提前知道 Keras 网络是怎样一个结构
二. 预备知识点
- mnist 数据集
- 怎样用 Keras 实现 Sequential 类型的网络
- 函数重载
- C++ 类
- C++ 类继承
- C++ 虚函数
- OpenCV 常用函数 (filter2D, minMaxLoc, mean)
- 矩阵与向量的乘法
- HDF5 文件结构
- JSON 数据结构
你最好能掌握以上的知识, 没有掌握也不太影响, 只是看得慢点, 边看边查资料, 最终肯定能看懂的
需要参考和下载的资源如下
三. 网络实现
本文只是粗略的实现 Sequential 网络, 说明大概的原理, 不是 Keras 原样实现, 所以不要纠结有一些功能我没有写出来, 你知道原理之后自己就可以实现了. 为了方便初学者理解细节. 文章中的实现方式没有考虑运行效率, 侧重在理解方面. 如何优化运行效率会放到以后再讲. 先有了基础, 再谈效率就容易多了
1. 网络基类
基类是其他类型层(卷积层, 池化层… …)的基类, 这些类都有一些相同的功能, 比如加载参数, 前向计算. 直接放码过来
// 层类型, 这里只写了下面几种类型, 其他的你可以自己添加
// Drop out 其实也是下面的 NET_LAYER_DENSE
enum NetLayerType
{
NET_LAYER_CONV2D = 0, // 2D卷积
NET_LAYER_MAX_POOL, // 最大池化
NET_LAYER_AVG_POOL, // 平均池化
NET_LAYER_CONV2D_TRANSPOSE, // 2D转置卷积
NET_LAYER_FLATTEN, // Flatten
NET_LAYER_DENSE, // Dense
NET_LAYER_ADD, // 相加
NET_LAYER_UP_SAMPLING2D // 2D 上采样
};
// 激活函数, 也只列了下面的几种类型, 其他的你可以自己添加
enum NetActivateion
{
NET_ACTIVATE_NULL = 0, // 不使用激活函数
NET_ACTIVATE_RELU,
NET_ACTIVATE_SIGMOID,
NET_ACTIVATE_SOFTMAX
};
// layer 基类
class CNetLayer
{
public:
CNetLayer(const string & layer_name, NetLayerType type = NET_LAYER_DENSE, NetActivateion activation_fun = NET_ACTIVATE_RELU)
: name(layer_name)
, type(type)
, activation(activation_fun)
{
}
~CNetLayer(void)
{
}
public:
string name; // 层名称
NetLayerType type; // 层的类型
NetActivateion activation; // 激活函数
vector<Mat> layer_output; // 每一层的输出, 可以用来输出中间层的结果
// 前向计算, 这是虚函数, 会在派生类中会重写, 所以这里不用具体的实现
virtual void Compute(vector<Mat> & input, vector<Mat> & output)
{
}
// 加载函数, 也是虚函数, 会在派生类中会重写
virtual bool LoadWeight(const H5File & file)
{
return true;
}
};
在上面的基类中用到了两个虚函数, 以后面会看到这两个虚函数的威力
2. 卷积层
卷积层以上面的 CNetLayer 为基类, 不过还要增加一些成员变量, 最重要的当然是 Filter 和 Bias. 以下面的代码中虽然把 Padding 的类型列了出来, 但是在这个例子中没有使用. 你要用的话, 可以自己完成. OpenCV 中也有现成的函数(copyMakeBorder), 你自己想一下是可以实现的, 只是名称不一样
enum PaddingType
{
PADDING_VALID = 0,
PADDING_SAME,
PADDING_FULL
};
// 2D 卷积, 1D, 3D 你自己实现吧
class CConv2D : public CNetLayer
{
public:
CConv2D(const string & layer_name,
Size2i kernel_size = Size2i(3, 3), Size2i kernel_stride = Size2i(1, 1),
PaddingType padding_type = PADDING_VALID, NetActivateion activation = NET_ACTIVATE_RELU)
: CNetLayer(layer_name, NET_LAYER_CONV2D, activation)
, kernel_size(kernel_size)
, kernel_stride(kernel_stride)
, padding_type(padding_type)
{
}
~CConv2D(void)
{
}
public:
vector<vector<Mat>> kernel; // 卷积 Filter
Mat bias;
Size2i kernel_size; // 卷积核尺寸
Size2i kernel_stride; // 卷积时滑动的步长
Size2i padding_size; // 边缘填充尺寸
PaddingType padding_type; // 填充类型
virtual void Compute(vector<Mat> & input, vector<Mat> & output);
virtual bool LoadWeight(const H5File & file);
};
两个虚函数实现如下, 激活函数我只实现了 relu, 其他的你可以自己实现
void CConv2D::Compute(vector<Mat> & input, vector<Mat> & output)
{
const int kernels = (int)kernel.size();
const int channels = (int)input.size();
const int in_rows = input[0].rows;
const int in_cols = input[0].cols;
const Size2i half_kernel(kernel[0][0].cols >> 1, kernel[0][0].rows >> 1);
cv::Rect output_shape;
switch (padding_type)
{
case PADDING_VALID:
output_shape = cv::Rect(half_kernel.width, half_kernel.height,
(in_cols - kernel_size.width) / kernel_stride.width + 1,
(in_rows - kernel_size.height) / kernel_stride.height + 1);
break;
case PADDING_SAME:
output_shape = cv::Rect(half_kernel.width, half_kernel.height, in_cols, in_rows);
for (int i = 0; i < channels; i++)
{
Mat big_img;
copyMakeBorder(input[i], big_img, half_kernel.height, half_kernel.height, half_kernel.width, half_kernel.width,
BORDER_CONSTANT, Scalar::all(0));
input[i] = big_img;
}
break;
case PADDING_FULL:
break;
default:
break;
}
layer_output.resize(kernels);
for (int i = 0; i < kernels; i++)
{
for (int j = 0; j < channels; j++)
{
Mat conv_result;
filter2D(input[j], conv_result, kernel[i][j].depth(), kernel[i][j]);
if (0 == j)
{
layer_output[i] = conv_result;
}
else
{
layer_output[i] += conv_result;
}
}
layer_output[i] += bias.at<float>(i, 0);
switch (activation)
{
case NET_ACTIVATE_RELU:
threshold(layer_output[i], layer_output[i], 0, 1, CV_THRESH_TOZERO);
if (PADDING_VALID == padding_type)
{
layer_output[i] = layer_output[i](output_shape);
}
break;
default:
break;
}
}
// 没有直接用 layer_output 直接输出, 是因为 layer_output 用来保存中间结果, 也想当于一个中间层输出备份
output.resize(kernels);
for (int i = 0; i < kernels; i++)
{
layer_output[i].copyTo(output[i]);
}
}
bool CConv2D::LoadWeight(const H5File & file)
{
H5std_string path = "/";
Group rg(file.getObjId(path));
bool find = false;
const hsize_t objs = rg.getNumObjs();
for (hsize_t i = 0; i < objs; i++)
{
const H5std_string layer_name = rg.getObjnameByIdx(i);
if (layer_name == name)
{
find = true;
Group sub_group(rg.getObjId(name));
path = name + "/";
path.append(sub_group.getObjnameByIdx(0));
sub_group.close();
break;
}
}
if (!find)
{
rg.close();
return false;
}
DataSet ds_kernel(rg.getObjId(path + "/kernel:0"));
DataSpace dsp = ds_kernel.getSpace();
H5::DataType dt = ds_kernel.getDataType();
int rank = dsp.getSimpleExtentNdims();
hsize_t *dims = new hsize_t[rank];
int ndims = dsp.getSimpleExtentDims(dims);
kernel.resize((unsigned int)dims[rank - 1]);
hsize_t data_Len = ds_kernel.getInMemDataSize() / sizeof(float);
float *buf = new float[(unsigned int)data_Len];
ds_kernel.read(buf, dt);
for (int i = 0; i < dims[rank - 1]; i++) // 第一重: Filter 序号
{
// Filter 的通道数存储在 dims[rank - 2] 中
kernel[i].resize((unsigned int)dims[rank - 2]);
for (int j = 0; j < dims[rank - 2]; j++) // 第二重: channel 序号
{
// dims[0], dims[1] 中分别是 Filter 的行数和列数
kernel[i][j].create((int)dims[0], (int)dims[1], CV_32FC1);
for (int r = 0; r < kernel[i][j].rows; r++) // 第三重: Filter 行序号
{
for (int c = 0; c < kernel[i][j].cols; c++) // 第四重: Filter 列序号
{
const int k = (int)(
r * dims[rank - 1] * dims[rank - 2] * kernel[i][j].cols +
c * dims[rank - 1] * dims[rank - 2] +
j * dims[rank - 1] +
i);
kernel[i][j].at<float>(r, c) = buf[k];
}
}
}
}
dt.close();
dsp.close();
ds_kernel.close();
delete []dims;
dims = nullptr;
delete []buf;
buf = nullptr;
//-------------------------------------------
DataSet ds_bias(rg.getObjId(path + "/bias:0"));
dsp = ds_bias.getSpace();
dt = ds_bias.getDataType();
rank = dsp.getSimpleExtentNdims();
dims = new hsize_t[rank];
ndims = dsp.getSimpleExtentDims(dims);
if (1 == rank)
{
// 这里这样就不用转置了
bias.create((int)dims[0], 1, CV_32FC1);
}
else
{
rg.close();
return false;
}
ds_bias.read(bias.data, dt);
dt.close();
dsp.close();
ds_bias.close();
rg.close();
delete []dims;
dims = nullptr;
return true;
}
3. 池化层
池化层从 CConv2D 继承因它们有类似的操作. 先实现一个不分类型的池化层基类, 然后再用两个子类区分池化类型
class CPool2D : public CConv2D
{
public:
CPool2D(const string & layer_name, NetLayerType type,
Size2i kernel_size = Size2i(2, 2), Size2i kernel_stride = Size2i(1, 1),
PaddingType padding_type = PADDING_VALID)
: CConv2D(layer_name, kernel_size, kernel_stride, padding_type, NET_ACTIVATE_NULL)
{
// 这里的 type 是在子类中指定的
this->type = type;
}
~CPool2D(void)
{
}
public:
virtual void Compute(vector<Mat> & input, vector<Mat> & output);
// 因不需要参数, 所以直接返回
virtual bool LoadWeight(const H5File & file)
{
return true;
}
};
Compute 实现如下, 在其中有区分 NET_LAYER_MAX_POOL 和 NET_LAYER_AVG_POOL, 所以子类中就不再需要实现 Compute 函数了
void CPool2D::Compute(vector<Mat> & input, vector<Mat> & output)
{
const Size2i half_kernel(kernel_size.width >> 1, kernel_size.height >> 1);
const int channels = (int)input.size();
layer_output.resize(channels);
for (int i = 0; i < channels; i++)
{
const int out_rows = (input[i].rows - kernel_size.height) / kernel_stride.height + 1;
const int out_cols = (input[i].cols - kernel_size.width) / kernel_stride.width + 1;
layer_output[i].create(out_rows, out_cols, CV_32FC1);
for (int r = 0; r < input[i].rows - half_kernel.height; r += kernel_stride.height)
{
for (int c = 0; c < input[i].cols - half_kernel.width; c += kernel_stride.width)
{
const Mat m = input[i](cv::Rect(c, r, kernel_size.width, kernel_size.height));
double value = 0;
if (NET_LAYER_MAX_POOL == type)
{
minMaxLoc(m, nullptr, &value);
}
else
{
value = mean(m)[0];
}
const int or = r / kernel_stride.height;
const int oc = c / kernel_stride.width;
layer_output[i].at<float>(or, oc) = (float)value;
}
}
}
output.resize(channels);
for (int i = 0; i < channels; i++)
{
layer_output[i].copyTo(output[i]);
}
}
3.1 MaxPool2D
MaxPool2D 只是 指定 type 为 NET_LAYER_MAX_POOL
class CMaxPooling2D : public CPool2D
{
public:
CMaxPooling2D(const string & layer_name,
Size2i kernel_size = Size2i(2, 2), Size2i kernel_stride = Size2i(1, 1),
PaddingType padding_type = PADDING_VALID)
: CPool2D(layer_name, NET_LAYER_MAX_POOL, kernel_size, kernel_stride, padding_type)
{
}
~CMaxPooling2D(void)
{
}
};
3.2 AveragePooling2D
CAveragePooling2D 指定 type 为 NET_LAYER_AVG_POOL
class CAveragePooling2D : public CPool2D
{
public:
CAveragePooling2D(const string & layer_name,
Size2i kernel_size = Size2i(2, 2), Size2i kernel_stride = Size2i(1, 1),
PaddingType padding_type = PADDING_VALID)
: CPool2D(layer_name, NET_LAYER_AVG_POOL, kernel_size, kernel_stride, padding_type)
{
}
~CAveragePooling2D(void)
{
}
};
4. Flatten
Flatten 从 CNetLayer 继承, 因为它不需要参数
class CFlatten : public CNetLayer
{
public:
CFlatten(const string & layer_name)
: CNetLayer(layer_name, NET_LAYER_FLATTEN, NET_ACTIVATE_NULL)
{
}
~CFlatten()
{
}
public:
virtual void Compute(vector<Mat> & input, vector<Mat> & output);
};
虚函数实现
void CFlatten::Compute(vector<Mat> & input, vector<Mat> & output)
{
const int channels = 1;
Mat multi_channel;
merge(input, multi_channel);
layer_output.resize(channels);
const int data_size = multi_channel.rows * multi_channel.cols * multi_channel.channels();
layer_output[0].create(data_size, 1, CV_32FC1);
memcpy(layer_output[0].data, multi_channel.data, data_size * sizeof(float));
output.resize(channels);
for (int i = 0; i < channels; i++)
{
layer_output[i].copyTo(output[i]);
}
}
5. 全连接层
全连接层需要增加成员变量保存参数, 也从 CNetLayer 继承
class CDense : public CNetLayer
{
public:
CDense(const string & layer_name, NetActivateion activation = NET_ACTIVATE_RELU)
: CNetLayer(layer_name, NET_LAYER_DENSE, activation)
{
}
~CDense(void)
{
}
public:
Mat weight; // 权重
Mat bias;
virtual void Compute(vector<Mat> & input, vector<Mat> & output);
virtual bool LoadWeight(const H5File & file);
};
两个虚函数如下, 还实现了 NET_ACTIVATE_SOFTMAX 激活函数
void CDense::Compute(vector<Mat> & input, vector<Mat> & output)
{
const int channels = 1;
layer_output.resize(channels);
layer_output[0] = weight * input[0] + bias;
switch (activation)
{
case NET_ACTIVATE_RELU:
threshold(layer_output[0], layer_output[0], 0, 1, CV_THRESH_TOZERO);
break;
case NET_ACTIVATE_SIGMOID:
break;
case NET_ACTIVATE_SOFTMAX:
{
float sum = 0;
vector<float> vec(layer_output[0].rows);
for (int i = 0; i < layer_output[0].rows; i++)
{
vec[i] = exp(layer_output[0].at<float>(i, 0));
sum += vec[i];
}
for (int i = 0; i < layer_output[0].rows; i++)
{
layer_output[0].at<float>(i, 0) = vec[i] / sum;
}
}
break;
default:
break;
}
output.resize(channels);
for (int i = 0; i < channels; i++)
{
layer_output[i].copyTo(output[i]);
}
}
bool CDense::LoadWeight(const H5File & file)
{
H5std_string path = "/";
Group rg(file.getObjId(path));
bool find = false;
const hsize_t objs = rg.getNumObjs();
for (hsize_t i = 0; i < objs; i++)
{
const H5std_string layer_name = rg.getObjnameByIdx(i);
if (layer_name == name)
{
find = true;
Group sub_group(rg.getObjId(name));
path = name + "/";
path.append(sub_group.getObjnameByIdx(0));
sub_group.close();
break;
}
}
if (!find)
{
rg.close();
return false;
}
DataSet ds_kernel(rg.getObjId(path + "/kernel:0"));
DataSpace dsp = ds_kernel.getSpace();
H5::DataType dt = ds_kernel.getDataType();
int rank = dsp.getSimpleExtentNdims();
hsize_t *dims = new hsize_t[rank];
int ndims = dsp.getSimpleExtentDims(dims);
if (1 == rank)
{
weight.create(1, (int)dims[0], CV_32FC1);
}
else if (2 == rank)
{
weight.create((int)dims[0], (int)dims[1], CV_32FC1);
}
else
{
rg.close();
return false;
}
ds_kernel.read(weight.data, dt);
transpose(weight, weight);
dt.close();
dsp.close();
ds_kernel.close();
delete []dims;
dims = nullptr;
//-------------------------------------------
DataSet ds_bias(rg.getObjId(path + "/bias:0"));
dsp = ds_bias.getSpace();
dt = ds_bias.getDataType();
rank = dsp.getSimpleExtentNdims();
dims = new hsize_t[rank];
ndims = dsp.getSimpleExtentDims(dims);
if (1 == rank)
{
// 这里这样就不用转置了
bias.create((int)dims[0], 1, CV_32FC1);
}
else
{
rg.close();
return false;
}
ds_bias.read(bias.data, dt);
dt.close();
dsp.close();
ds_bias.close();
rg.close();
delete []dims;
dims = nullptr;
rg.close();
return true;
}
至此, 几种类型的 layer 都完成了
6. 层管理
有了各种类型的 layer 之后, 需要有一个可以整合这些 layer 的类, 然后用这个类实现网络, 定义如下
class CSequentialNet
{
public:
CSequentialNet(void);
~CSequentialNet(void);
public:
vector<CNetLayer *> net; // 各种 layer 的指针
void AddLayer(CNetLayer * layer);
bool LoadWeight(const H5File & file);
// 预测函数, 输入是单通道或三通道图像, std_size 是训练时的标准尺寸
Mat Predict(const Mat & input, Size2i std_size);
Mat Predict(const vector<Mat> & input);
// 找出分类属于哪个类别
int ArgMax(const Mat & m);
};
上面类中的 vector<CNetLayer *> net 就是用来存储各种 layer 指针的 vector, 知道为什么要用 继承和虚函数 了吗? 要不然你就不能这么轻松地实现各种 layer 的管理了, AddLayer、LoadWeight 和 Predict 的实现如此简单也是继承和虚函数的功劳
// 析构函数, AddLayer 中 new 的指针在这里删除
CSequentialNet::~CSequentialNet(void)
{
const int layers = net.size();
for (int i = 0; i < layers; i++)
{
if (nullptr != net[i])
{
delete net[i];
net[i] = nullptr;
}
}
}
void CSequentialNet::AddLayer(CNetLayer * layer)
{
net.push_back(layer);
}
bool CSequentialNet::LoadWeight(const H5File & file)
{
const int layers = net.size();
for (int i = 0; i < layers; i++)
{
if (!net[i]->LoadWeight(file))
{
return false;
}
}
return true;
}
Mat CSequentialNet::Predict(const Mat & input, Size2i std_size)
{
Mat img_std;
if (input.rows != std_size.height || input.cols != std_size.width)
{
resize(input, img_std, std_size, 0, 0, CV_INTER_CUBIC);
}
else
{
img_std = input;
}
vector<Mat> img_input;
if (3 == img_std.channels())
{
img_std.convertTo(img_std, CV_32FC3, 1.0 / 255.0);
split(img_std, img_input);
}
else
{
img_std.convertTo(img_std, CV_32FC1, 1.0 / 255.0);
img_input.push_back(img_std);
}
return Predict(img_input);
}
Mat CSequentialNet::Predict(const vector<Mat> & input)
{
const int layers = net.size();
vector<Mat> output = input;
for (int i = 0; i < layers; i++)
{
net[i]->Compute(output, output);
}
return output[0];
}
int CSequentialNet::ArgMax(const Mat & m)
{
Point2i max_pt;
minMaxLoc(m, nullptr, nullptr, nullptr, &max_pt);
return max_pt.y;
}
现在, 实现全部完成了, 就问你简单不简单~
四. 应用示例
这么简单的代码怎么用呢, 就像 Keras 一样用, 接下来就是见证奇迹的时刻了
CSequentialNet ainet;
if (ainet.net.size() < 1)
{
ainet.AddLayer(new CConv2D("conv_1", Size2i(3, 3), Size2i(1, 1), PADDING_VALID, NET_ACTIVATE_RELU));
ainet.AddLayer(new CMaxPooling2D("max_pool_1", Size2i(2, 2), Size2i(2, 2), PADDING_VALID));
ainet.AddLayer(new CConv2D("conv_2", Size2i(3, 3), Size2i(1, 1)));
ainet.AddLayer(new CMaxPooling2D("max_pool_2", Size2i(2, 2), Size2i(1, 1)));
ainet.AddLayer(new CFlatten("flatten"));
ainet.AddLayer(new CDense("dense_1", NET_ACTIVATE_RELU));
ainet.AddLayer(new CDense("output", NET_ACTIVATE_SOFTMAX));
H5File file("改成你自己文件的路径, 包括.扩展名", H5F_ACC_RDONLY);
ainet.LoadWeight(file);
file.close();
}
// 预测, mnist 数据集图像尺寸为 28 * 28
const Mat output = ainet.Predict(img_input, Size2i(28, 28));
// 输出数字
const int num = ainet.ArgMax(output);
是不是和 Keras 很像了, 在 AddLayer 中 new 的指针在 析构函数 中删除了, 所以不用担心没有 delete
五. 从 DHF5 加载模型
上面的方法中已经可以添加 layer, 但是 AddLayer 方法中没有像 Keras 中的 input_shape 这样的参数指定输入图像的 shape, 因为我们 只需要预测, 不需要训练, 而这个参数可以用模型或者参数中加载, 所以就不需要了. 那不预先知道模型的情况下怎么加载 Keras 模型呢? 这个在 C++ 从 HDF5 文件读取 Keras 神经网络模型和参数 中有稍微提到一点, 但是不详细. 也可能直接将 model 保存成 JSON 格式, 然后再加载, 不过两种方式都是一样的
1. 加载 model 字符串
Keras model 把模型的定义保存到了 HDF5 文件 Root group 的 Attributes/model_config的字符串中, 现在把它读出来, 先在 CSequentialNet 中 增加 4 个成员函数, 带下划线的为 protected 中的函数
- LoadModel
- _GetActivation
- _GetPadding
- _GetSize2i
class CSequentialNet
{
public:
CSequentialNet(void);
~CSequentialNet(void);
public:
vector<CNetLayer *> net; // 各种 layer 的指针
void AddLayer(CNetLayer * layer);
bool LoadModel(const H5File & file); // 加载模型
bool LoadWeight(const H5File & file);
// 预测函数, 输入是单通道或三通道图像, std_size 是训练时的标准尺寸
Mat Predict(const Mat & input, Size2i std_size);
Mat Predict(const vector<Mat> & input);
// 找出分类属于哪个类别
int ArgMax(const Mat & m);
protected:
// 取得激活函数
NetActivateion _GetActivation(const string & activation);
// 取得 Padding 类型
PaddingType _GetPadding(const string & activation);
// 用于加载 Filter 或 strides 这样的 2D 数据
Size2i _GetSize2i(const Value & value);
};
// 加载 model
bool CSequentialNet::LoadModel(const H5File & file)
{
net.clear();
H5std_string path = "/";
Group rg(file.getObjId(path));
Attribute attr = rg.openAttribute("model_config");
H5::DataType dt = attr.getDataType();
H5std_string config;
attr.read(dt, config);
// 需要解析字符串的代码
dt.close();
attr.close();
rg.close();
return true;
}
// 取得激活函数
NetActivateion CSequentialNet::_GetActivation(const string & activation)
{
if ("relu" == activation)
{
return NET_ACTIVATE_RELU;
}
else if ("sigmoid" == activation)
{
return NET_ACTIVATE_SIGMOID;
}
else if ("softmax" == activation)
{
return NET_ACTIVATE_SOFTMAX;
}
else
{
// 其他你自己写
}
return NET_ACTIVATE_NULL;
}
// 取得 Padding 类型
PaddingType CSequentialNet::_GetPadding(const string & padding)
{
if ("valid" == padding)
{
return PADDING_VALID;
}
else if ("same" == padding)
{
return PADDING_SAME;
}
else if ("full" == padding)
{
return PADDING_FULL;
}
return PADDING_VALID;
}
// 用于加载 Filter 或 strides 这样的 2D 数据
Size2i CSequentialNet::_GetSize2i(const Value & value)
{
return Size2i(value[1].asInt(), value[0].asInt());
}
上面的代码中 LoadModel 还没有解析字符串, 只是把 model 以字符串的方式读了出来
显示得好看一点是这样的, 没有显示完, 这不就是 JSON 结果吗
{
"class_name": "Sequential", "config":
{
"name": "mnist", "layers": [
{
"class_name": "Conv2D", "config":
{
"name": "conv_1",
"trainable": true,
"batch_input_shape": [null, 28, 28, 1],
"dtype": "float32",
"filters": 32,
"kernel_size": [3, 5],
"strides": [1, 1],
"padding": "valid",
"data_format":
"channels_last",
"dilation_rate": [1, 1],
"activation": "relu",
"use_bias": true,
"kernel_initializer":
{
"class_name": "GlorotUniform", "config":
{
"seed": null
}
},
"bias_initializer":
{
"class_name":
"Zeros",
"config": {}
},
"kernel_regularizer": null,
"bias_regularizer": null,
"activity_regularizer": null,
"kernel_constraint": null,
"bias_constraint": null
}
},
2. 解析 model 字符串
下面就需要解析字符串, 并用解析结果自动生成 model
// 解析字符串
Reader reader;
Value root;
if (reader.parse(config, root, false))
{
if (!root["config"].isNull())
{
// 读取各层的配置
const Value layers_config = root["config"]["layers"];
const int layers = layers_config.size();
for (int i = 0; i < layers; i++)
{
const Value layer_i = layers_config[i];
const Value layer_config = layer_i["config"];
// 类型
const string layer_class = layer_i["class_name"].asCString();
const string layer_name = layer_config["name"].asString();
// 如果是卷积层
if ("Conv2D" == layer_class)
{
const Size2i kernel_size = _GetSize2i(layer_config["kernel_size"]);
const Size2i kernel_stride = _GetSize2i(layer_config["strides"]);
const PaddingType padding_type = _GetPadding(layer_config["padding"].asString());
const NetActivateion activation = _GetActivation(layer_config["activation"].asString());
// 有上面这几个参数就可以 AddLayer 了
AddLayer(new CConv2D(layer_name, kernel_size, kernel_stride, padding_type, activation));
}
else if ("MaxPooling2D" == layer_class || "AveragePooling2D" == layer_class)
{
const Size2i kernel_size = _GetSize2i(layer_config["pool_size"]);
const Size2i kernel_stride = _GetSize2i(layer_config["strides"]);
const PaddingType padding_type = _GetPadding(layer_config["padding"].asString());
// 有上面这几个参数就可以 AddLayer 了
if ("MaxPooling2D" == layer_class)
{
AddLayer(new CMaxPooling2D(layer_name, kernel_size, kernel_stride, padding_type));
}
else
{
AddLayer(new CAveragePooling2D(layer_name, kernel_size, kernel_stride, padding_type));
}
}
else if ("Flatten" == layer_class)
{
AddLayer(new CFlatten(layer_name));
}
else if ("Dense" == layer_class)
{
const NetActivateion activation = _GetActivation(layer_config["activation"].asString());
AddLayer(new CDense(layer_name, activation));
}
}
}
}
加载 model 的代码完成
3. 加载 model 和 weight 示例
加载 model 后自动生成相应的网络, 再加载 weight, 这样是因为你可能保存的是最好的 weight, 所以分开储存的. 你也可以修改一下加载代码, 直接从 model 中加载 weight, 不过是训练停止时的参数
CSequentialNet ainet;
if (ainet.net.size() < 1)
{
H5File file("model 文件, 改成你自己文件的路径, 包括.扩展名", H5F_ACC_RDONLY);
ainet.LoadModel(file);
file.close();
file.openFile("weight 文件, 改成你自己文件的路径, 包括.扩展名", H5F_ACC_RDONLY);
ainet.LoadWeight(file);
file.close();
}
// 预测, mnist 数据集图像尺寸为 28 * 28
const Mat output = ainet.Predict(img_input, Size2i(28, 28));
// 输出数字
const int num = ainet.ArgMax(output);
六. 可视化
如果想要看中间层的输出结果, 显示在你开发的软件上, 那只需要使用每一层的 layer_output 就可以了
七. 代码下载
完整的代码可下载 VS2015 x64 代码示例