CNN包括RELU层,池化层,卷积层,全连接层。具体的理论基础参见:CNN理论基础
本次对卷积层进行了实践,其指导文献参见:卷积导数计算。
先上试验代码:
#include <stdio.h>
#include <conio.h>
#include <iostream>
#include <vector>
#include "bp.hpp"
#include "activate_function.hpp"
#include "restricked_boltzman_machine.hpp"
#include "dbn.hpp"
#include "convolution_layer.hpp"
int main(int argc, char** argv)
{
#if 1
using conv_type = conv_layer<7, 7, 3, 3, 2, 2, XavierGaussian>;
conv_type cl;
mat<7, 7, double> mt_origin({
1,2,3,4,5,6,7,
1,2,3,4,5,6,7,
1,2,3,4,5,6,7,
1,2,3,4,5,6,7,
1,2,3,4,5,6,7,
1,2,3,4,5,6,7,
1,2,3,4,5,6,7
});
conv_type::ret_type mt_expected({
1,2,3,
4,5,6,
7,8,9
});
mt_expected = mt_expected.t();
for (int i = 0; ; ++i)
{
auto mt_cl_forward = (cl.forward(mt_origin));
auto mt_delta = (mt_cl_forward - mt_expected);
//mt_delta.print();
auto mt_cl_backward = cl.backward(mt_delta);
if (i % 1000 == 0)
{
mt_cl_forward.print();
}
}
_getch();
return 0;
}
结果是一如既往令人满意的,0.001学习率基本算个2w次能得到结果:
下面就是具体实现了(convolution.hpp):
#ifndef _CONVOLUTION_LAYER_HPP_
#define _CONVOLUTION_LAYER_HPP_
#include "mat.hpp"
#include "base_function.hpp"
/* 卷积层 */
template<int input_row, int input_col, int tpl_row, int tpl_col, int row_step, int col_step, typename tpl_init_method, typename val_t = double>
struct conv_layer
{
using input_type = mat<input_row, input_col, val_t>;
using ret_type = mat<get_step_inner_size(input_row, tpl_row, row_step), get_step_inner_size(input_col, tpl_col, col_step), val_t>;
using pad_type = mat<input_row + get_pad_size(input_row, tpl_row, row_step)
, input_col + get_pad_size(input_col, tpl_col, col_step), val_t>;
using pad_size = pad_size_t<input_row, input_col, tpl_row, tpl_col, row_step, col_step>;
mat<tpl_row, tpl_col, val_t> mt_tpl;
pad_type mt_input;
ret_type mt_bias;
double lr;
conv_layer():lr(0.001)
{
weight_initilizer<tpl_init_method>::cal(mt_tpl);
}
inline ret_type forward(const input_type& mt)
{
mt_input = mt.pad<pad_size::top, pad_size::left, pad_size::right, pad_size::bottom>();
return inner_conv<row_step, col_step>(mt_input, mt_tpl) + mt_bias;
}
inline input_type backward(const ret_type& mt_delta)
{
auto mt_delta_span = mt_delta.span<row_step - 1, col_step - 1>(); // 采用了步长运算,等于有一些没计算,所以反向传播时候的贡献是0
using ret_pad_type = decltype(mt_delta_span);
/* 计算反向传播误差 */
/* 计算返回阵需要pad的大小 */
constexpr int target_r = tpl_row + pad_type::r - 1;
constexpr int target_c = tpl_col + pad_type::c - 1;
constexpr int pad_top = (target_r - ret_pad_type::r) / 2;
constexpr int pad_left = (target_c - ret_pad_type::c) / 2;
constexpr int pad_right = (target_c - ret_pad_type::c) - pad_left;
constexpr int pad_bottom = (target_r - ret_pad_type::r) - pad_top;
auto mt_delta_span_pad = mt_delta_span.pad<pad_top, pad_left, pad_right, pad_bottom>();
auto mt_tpl_rot = mt_tpl.rot180();
auto mt_ret_pad = inner_conv<1, 1>(mt_delta_span_pad, mt_tpl_rot);
input_type mt_ret;
mt_ret.assign<-1 * pad_size::top, -1 * pad_size::left>(mt_ret_pad); // 剪除外边
/* 计算卷积核更新 */
auto mt_update = inner_conv<1, 1>(mt_input, mt_delta_span);
mt_update = mt_update / mt_update.max_abs();
mt_tpl = mt_tpl - lr * mt_update; // 更新模板
mt_bias = mt_bias - lr * mt_delta;
/* 将模板均值置0,最大波动范围为1 */
double d_mean = mt_tpl.sum() / (tpl_row * tpl_col);
mt_tpl = mt_tpl - d_mean;
mt_tpl = mt_tpl / mt_tpl.max_abs();
return mt_ret;
}
};
#endif
矩阵运算增加了一些东西,比如矩阵间隔填充函数span,以及周围扩充函数pad,这些函数没有使用元编程,因为实在是太耗费编译时间了,而且还可能导致编译器内存太大导致编译不起来。mat.hpp代码如下:
#ifndef _MAT_HPP_
#define _MAT_HPP_
#include <climits>
#include <map>
#include <boost/pool/pool.hpp>
template<int i_size, typename val_t>
struct mat_m
{
static boost::pool<> s_pool;
val_t* p;
mat_m() :p(nullptr)
{
//p = (val_t*)malloc(sz * sizeof(val_t));
p = (val_t*)(s_pool.malloc());
for (int i = 0; i < i_size; ++i)
{
p[i] = 0;
}
}
~mat_m()
{
if (p)
{
//free(p);
s_pool.free(p);
}
}
val_t& get(const int& len_1d, const int& i_1d_idx, const int& i_2d_idx)
{
val_t& ret = p[i_2d_idx + len_1d