本篇主要讲2016年的时候写的一个神经网络,顺便复习以前的知识。
需求
想实现一个前馈神经网络,基于C++,性能会比较好。主要是因为当时Caffe等库依赖太多了,又没有其他开源的好用,顺便复习下C++。
架构设计
在现行的神经网络库里,Caffe采用以层为单位的抽象,优点是逻辑清晰,实现简便,但是需要手动实现梯度计算;而Tensorflow是以计算图为基础,封装好了每个算子,采用这些算子可以无需手动计算梯度。当然我们这里不需要计算梯度。整体架构采用:数据存储+层的方案。
(1)数据存储:用C++实现基础的类,然后底层用Blas之类的库加速。主要参考的是Caffe的设计。层:实现一些基础的层,比如卷积、池化、非线性层等。
- 首先,要考虑的问题是数据的存储,需要一个4维的数据存储类,类似于Caffe的Blob。因为是前馈网络,所以暂时不需要存储梯度
- 这个数据存储类要实现一些常见的操作,比如加减乘除,改变维度(数据重新排列)
- 需要对运算进行加速,比如矩阵乘法可以并行化加速。
(2)模型定义与存储:采用代码实现,在代码里定义模型。参数的存储采用文本格式,封装到数据类里。当然,C++有一些序列化模型的库,但是为了简便这里不采用
整体的架构还是比较简单的。
代码实现
卷积的实现
参考了Caffe的实现方式,把图像卷积转化为矩阵相乘,然后再用矩阵运算库armadillo加速。注意这里armadillo底层是通过 Lapack/OpenBlas/ Intel MKL等加速的。这种实现卷积的方式优点是计算快,缺点是占用比较多的存储空间。当然还有其他更快的实现卷积的方式,比如FFT。
template<typename Dtype>
void im2col(const arma::Cube<Dtype> &feats,
const int kernel_h, const int kernel_w,
const int pad_h, const int pad_w,
const int stride_h, const int stride_w,
arma::Mat<Dtype>& feat_mat)
{
const int height = feats.n_rows;
const int width = feats.n_cols;
const int channels = feats.n_slices;
const int output_h = (height + 2 * pad_h - kernel_h )/ stride_h + 1;
const int output_w = (width + 2 * pad_w - kernel_w )/ stride_w + 1;
const int channel_size = height*width;
feat_mat = arma::Mat<Dtype>(output_h*output_w, kernel_w*kernel_h*channels);
const Dtype *data_im = feats.memptr();
Dtype *data_col = feat_mat.memptr();
//由于armadillo库的矩阵元素在内存中是按照先遍历低轴排布(列元素连续),所以
//我们把一次卷积的各元素展开成一行,把一个卷积核展开成一列。
//两个矩阵相乘之后,得到的矩阵的每一列是一个feature map.
for (int c = channels; c--; data_im += channel_size) {
//遍历整个核.按照从左到右,从上到下的顺序将一个核的元素排成一行
for (int kernel_y = 0; kernel_y < kernel_h; kernel_y++) {
for (int kernel_x = 0; kernel_x < kernel_w; kernel_x++) {
//遍历Feature Map。卷积核在图像上移动,从左到右,从上到下。
//当前在图像中的位置
int input_y = kernel_y-pad_h;
for (int output_y = output_h; output_y; output_y--) {
//检查是否处于padding位置
if (!is_a_ge_zero_and_a_lt_b(input_y, height)) {
for (int output_x = output_w; output_x; output_x--) {
*(data_col++) = 0;
}
}
else {
int input_x = kernel_x - pad_w;
for (int output_x = output_w; output_x; output_x--) {
if (is_a_ge_zero_and_a_lt_b(input_x, width)) {
*(data_col++) = data_im[input_x*height + input_y];
}
else {
*(data_col++) = 0;
}
input_x += stride_w;
}
}
input_y += stride_h;
}
}
}
}
}
其他层的实现依次类推
网络类
/*
* @Author: Weiliang Chen
* @Date: 2016-09-29 10:51:07
* @Last Modified by: Weiliang Chen
* @Last Modified time: 2016-09-29 10:55:51
*/
#ifndef FN_NET_H_
#define FN_NET_H_
#include <opencv.hpp>
#include <core.hpp>
#include <armadillo>
#include "ConvLayer.h"
#include "ReluLayer.h"
#include "PoolingLayer.h"
#include "InnerProductLayer.h"
namespace fn {
template <typename Dtype>
class Net
{
public:
Net();
~Net();
bool init(std::string model_dir);
void forward(const cv::Mat &image, Dtype *feat160);
bool load(std::string filename, const int height,
const int width,const int channels,const int number,
Blob<Dtype> &weights);
bool load(std::string filename, const int height,
const int width, const int channels,
arma::Cube<Dtype> &mean);
bool load(std::string filename, const int length, arma::Col<Dtype> &bias);
bool load(std::string filename, const int height,
const int width, arma::Mat<Dtype> &weights);
private:
const int IMAGE_SIZE_WIDTH = 47;
const int IMAGE_SIZE_HEIGHT = 55;
const int IMAGE_CHANNELS = 1;
arma::Cube<Dtype> mean_;
ConvLayer<Dtype> conv1_;
ConvLayer<Dtype> conv1_1_;
ConvLayer<Dtype> conv2_1_;
ConvLayer<Dtype> conv2_2_;
ConvLayer<Dtype> conv2_;
ConvLayer<Dtype> conv3_1_;
ConvLayer<Dtype> conv3_2_;
ConvLayer<Dtype> conv3_3_;
ConvLayer<Dtype> conv3_4_;
ConvLayer<Dtype> conv3_;
ConvLayer<Dtype> conv4_;
ReluLayer<Dtype> relu1_;
ReluLayer<Dtype> relu1_1_;
ReluLayer<Dtype> relu2_1_;
ReluLayer<Dtype> relu2_2_;
ReluLayer<Dtype> relu2_;
ReluLayer<Dtype> relu3_1_;
ReluLayer<Dtype> relu3_2_;
ReluLayer<Dtype> relu3_3_;
ReluLayer<Dtype> relu3_4_;
ReluLayer<Dtype> relu3_;
ReluLayer<Dtype> relu4_;
PoolingLayer<Dtype> pool1_;
PoolingLayer<Dtype> pool2_;
PoolingLayer<Dtype> pool3_;
InnerProductLayer<Dtype> fc160_1_;
InnerProductLayer<Dtype> fc160_2_;
};
} //namespace fn
#endif // !FN_NET_H_
这里直接把网络写在代码里了。
测试
几个典型的测试用例是空白图片以及固定值的图片。