network.h 使用指南

项目地址

  • 2025.01.18 更新:去除了预设层级和优化器的成员变量 int bs,用户不再需要为每个层级和优化器都指定批大小,仅需在 auto_dao::init() 中指定即可;
  • 2025.01.30 更新:计算加速改用 Eigen + ViennaCL,改善了 GPU 上计算的性能;
  • 2025.02.06 更新:更正了 g++ 下的优化命令;
  • 2025.02.13 更新:增加了原地计算功能,修复自动保存/读取的 bug,优化 FC 层计算速度,优化了各层级申请内存所需的时间,优化运行所需的内存大小;

简介

基于 C++14 的仅头文件的神经网络库,代码可读且速度较快,方便研究神经网络的实现。

计算加速:

  • CPU 加速:基于 Eigen(内附 3.4.0 版本,无需设置额外引用目录);
  • GPU 加速:基于 Eigen 和使用 OpenCL 的 ViennaCL(需要搭建 OpenCL 环境并设置额外引用目录) ;

CPU 代码支持大部分编译器,可用基于 GCC 的 DEV-C++ 编译。

对于 GPU 代码:

  • Windows 下仅支持 VS 系列编译器编译(存疑,作者并未在其它环境下成功);
  • Linux 下情况不明,作者没有尝试;

支持自动求偏导(反向传播),用户仅需定义前向过程。

支持读取/保存图片文件,使用了开源库 stb

本文仅介绍库的使用方法,关于机器学习的原理部分请移步:咕咕咕

注意事项

关于各种常量及开关

  • 定义 ENABLE_GPU 以启用 GPU 计算(ViennaCL on OpenCL);
  • 定义 ENABLE_AUTO_SL 以启用自动生成保存/读取/释放内存函数相关功能;
  • 使用宏 MAX_BUFSIZE_BYTE 以控制计算过程中额外使用的内存(显存)大小(防止爆内存/显存),默认值为 1073741824,即 1GB1\text{GB};(该功能尚未完善,额外内存可能超过该值,故建议尽量设置小一点)

关于计算加速

由于矩阵乘法算子基于 Eigen 和使用 OpenCL 的 ViennaCL,故仅需加速此两库即可。具体可以通过启用 OpenMP 和各种指令集(AVX、SSE)来加速,例如在 GCC 下使用 -Ofast -fopenmp -march=native 编译命令来启用 OpenMP 和指令集并开启 Ofast 优化。

基本概念

训练阶段

在此阶段,数据以批为单位进入神经网络,执行前向过程,计算出结果;再根据结果对应的损失值,反向传播得出各参数偏导,通过优化器更新参数。

测试阶段

在测试阶段,数据一个一个进入神经网络,执行前向过程,计算出结果。

该阶段中反向传播会被禁用,且某些层级算子的行为会改变(例如批归一化层)。

张量、自动反向传播

头文件 auto_dao.h

命名空间 auto_dao

成员 含义/作用
int Batch_Size 批大小,若为 00 则表示当前为测试阶段而非训练阶段
struct node 内部结构体,用户无需访问
std::vector<node*> tmp 内部变量,记录当前申请的所有 node 的地址,方便释放内存及初始化反向传播
void init(int BatchSize) 前向过程前必须执行的函数,释放现有所有张量占用的内存空间,初始化 Batch_Size
init_backward() 反向传播前必须执行的函数,初始化现有所有张量以便反向传播

三维张量 val3d

定义了可自动求偏导(反向传播)的三维张量类型 val3d。在训练阶段,张量会同时存储整个批次中的数据(所以它实际上是四维的)。val3d 会自动记录前向过程,方便反向传播。

成员 含义/作用
int d 张量的通道数
int h 张量的高度
int w 张量的宽度
float* a 张量数值的起始地址(数值按照 auto_dao::Batch_Size*d*h*w 的方式存储)
float* da 张量偏导的起始地址(偏导按照 auto_dao::Batch_Size*d*h*w 的方式存储,与数值一一对应)
val3d() 默认构造函数(不进行任何操作)
val3d(int td,int th,int tw,float val=0) 构造函数,初始化一个 td*th*tw 的张量,其每个位置的数值都是 val
val3d(int td,int th,int tw,float *dat) 构造函数,根据 dat[0]dat[max(auto_dao::Batch_Size,1)*td*th*tw-1] 中的数值初始化一个 td*th*tw 的张量
backward() 将该张量的偏导传递下去*
auto_dao::node *dat 内部变量,不使用 private 关键字仅仅是为了增加代码可读性,避免大量 friend 关键字

*:调用 backward 时,会将该张量标记为“偏导计算完成”状态,并将该张量的偏导反向传播至对其有影响的张量处。若本次操作导致某个张量的偏导计算完成(影响到的所有张量的偏导都已传播至该张量),则会自动调用其 backward 函数(类似 DAG 上反向 bfs)。故用户仅需在手动计算所有输出端张量的偏导后,手动调用所有输出端张量的 backward 函数

关于原地计算(inplace 选项)

为了节省空间,某些操作具有 inplace 选项,inplace=true 的操作将在原地完成。此时该操作会复用原本张量的空间,使得原本的张量失效。

所以经过 inplace=true 的操作后原始张量将失效,请勿再使用它。

张量相关函数

函数 含义/作用
val3d reshape(val3d x,int d,int h,int w,bool inplace=false) 软塑形:创建一个新的三维张量,d,h,w 为传入的 d,h,w,数据从 x.a 拷贝(需保证 d*h*w==x.d*x.h*x.w
val3d toshape(val3d x,int d,int h,int w) 硬塑形:创建一个新的三维张量,d,h,w 为传入的 d,h,w,数据从 x.a 循环拷贝,即 i,j,k 处的数值为 xi%x.d,j%x.h,k%x.w 处的数值
val3d operator+(val3d x,val3d y) 创建一个新的三维张量,其每一位的数值都是 x 对应位置的数值和 y 对应位置的数值相加(需保证 xy 形状相同)
val3d operator-(val3d x,val3d y) 同上,相加变为相减
val3d operator*(val3d x,val3d y) 同上,相乘
val3d operator/(val3d x,val3d y) 同上,相除(x 为被除数)
val3d dcat(val3d x,val3d y) 创建一个新的三维张量,其是 xy 按照 d 这一维拼接起来的结果(x 占用 [0,x.d-1]y 占用 [x.d,x.d+y.d-1],需保证 x.h==y.hx.w==y.w
float MSEloss(val3d x,float* realout) 使用 realout[0]realout[max(auto_dao::Batch_Size,1)*x.d*x.h*x.w] 中的数据为三维张量 x 计算均方差损失,同时为 x 计算偏导
float BCEloss(val3d x,float* realout) 同上,但计算的是二元交叉熵损失
均方差损失(MSEloss)

xx 是输出,rr 是真实数据:

loss=1ni=1n(xiri)2loss=\frac{1}{n}\sum\limits_{i=1}^n(x_i-r_i)^2

二元交叉熵损失(BSEloss)

xx 是输出,rr 是真实数据:

loss=1ni=1nriln(xi)+(1ri)ln(1xi)loss=-\frac{1}{n}\sum\limits_{i=1}^nr_i\ln(x_i)+(1-r_i)\ln(1-x_i)

注意 xix_i 会被限制在 [107,1107][10^{-7},1-10^{-7}] 内以防止出现 infnan,若超出范围则偏导为 00

预设优化器

头文件:Optimizer/*.h

命名规则:全大写命名

在本库中,权重及其在反向传播中求得的偏导统一存储于优化器中,方便统一更新。

统一公有成员:

成员 含义/作用
bool built 是否完成初始化
int m 权重数量
float lrt 学习率
void init(float Learn_Rate,...) 初始化优化器参数,该函数的第一个参数及含义固定,为学习率,根据不同优化器具体情况可能有更多参数
void build() 初始化优化器,为权重及其偏导分配内存空间
void save(std::ofstream& ouf) 将优化器参数及权重保存到二进制文件流 ouf
void load(std::ifstream& inf) 从二进制文件流 inf 中读取优化器参数及权重
void delthis() 释放申请的内存空间
float* _wei() 获取权重数组起始地址
float* _tmp() 获取偏导数组起始地址
void init_backward() 清空累计的偏导,即将偏导数组置零,准备反向传播
void flush() 利用当前累计的偏导更新权重,在反向传播完成后调用
默认构造函数 初始化 built=false,当启用宏 ENABLE_AUTO_SL 时还用于自动生成神经网络的保存、读取和空间释放函数(实现静态反射)

参数命名规则和主流的神经网络库大致相同。

SGD 优化器

头文件:Optimizer/SGD.h

定义了优化器类型 SGD,其所有公有成员均无特殊。

参数更新方式:(ww 为参数,Δ\Delta 为偏导)

wiwilrt×Δiw_i\to w_i-lrt\times \Delta_i

Adam 优化器

头文件:Optimizer/Adam.h

定义了优化器类型 ADAM,其特殊成员如下:

成员 含义/作用
float b1 参数更新公式中的 β1\beta_1
float b2 参数更新公式中的 β2\beta_2
float eps 参数更新公式中的 ϵ\epsilon,一个很小的非负实数,防止除以 00
void init(float Learn_Rate,float beta1=0.9,float beta2=0.999,float Eps=1e-8) 初始化优化器参数,b1b2eps 分别初始化为 beta1beta2Eps

参数更新方式:(ww 为参数,Δ\Delta 为偏导)

  • 初始 vi0,si0v_i\to 0,s_i\to 0

  • 对于第 tt 次更新:

    viβ1vi+(1β1)Δisiβ2si+(1β2)Δi2vˉi=vi1β1tsˉi=si1β2twiwilrt×vˉisˉi+ϵv_i\to \beta_1v_i+(1-\beta_1)\Delta_i\\ s_i\to \beta_2s_i+(1-\beta_2)\Delta_i^2\\ \bar v_i=\frac{v_i}{1-\beta_1^t}\\ \bar s_i=\frac{s_i}{1-\beta_2^t}\\ w_i\to w_i-lrt\times \frac{\bar v_i}{\sqrt{\bar s_i}+\epsilon}

预设网络层级(层级算子)

头文件:Layers/*.h

命名规则:全大写命名

统一公有成员:(有可训练权重)

成员 含义/作用
bool built 是否完成初始化
void init(int& m,...) 初始化层级参数,该函数第一个参数及其含义固定,为权重计数器(用于统计权重数量,一般传入优化器的 m)。根据不同层级的具体情况可能有更多参数
void build(float*& wei,float*& tmp,...) 为层级分配权重、偏导储存空间并初始化权重,其中 wei 为权重储存起始地址,tmp 为偏导储存起始地址
void save(std::ofstream& ouf) 将层级参数保存到二进制文件流 ouf 中,权重并不会被保存
void load(std::ifstream& inf,float*& wei,float*& tmp) 从二进制文件流 inf 中读取层级参数,并根据 weitmp 为层级分配权重*
val3d operator()(val3d x) 在三维张量 x 上应用该层级的操作并返回结果
默认构造函数 初始化 built=false,在启用宏 ENABLE_AUTO_SL 时还用于自动生成神经网络的保存、读取和空间释放函数(实现静态反射)

*wei 为权重数组起始地址,tmp 为偏导数组起始地址,层级将会从 wei 中获取其权重并分配空间(这要求优化器的 load() 函数已经被调用)。

统一公有成员:(无可训练权重)

成员 含义/作用
bool built 是否完成初始化
void init(...) 初始化层级参数,根据不同层级的具体情况可能有更多参数
void save(std::ofstream& ouf) 将层级参数保存到二进制文件流 ouf
void load(std::ifstream& inf) 从二进制文件流 inf 中读取层级参数
val3d operator()(val3d x) 在三维张量 x 上应用该层级的操作并返回结果
默认构造函数 初始化 built=false,在启用宏 ENABLE_AUTO_SL 时还用于自动生成神经网络的保存、读取和空间释放函数(实现静态反射)

全连接层(FC)

头文件:Layers/fc.h

定义了全连接层类型 FC,其特殊成员如下:

成员 含义/作用
int ins 输入值个数
int ous 输出值个数
float* w 权重存储起始地址
void init(int& m,int INS,int OUS) 初始化层级参数,额外将 insous 初始化为 INSOUS
void build(float*& wei,float*& tmp,int InitType=INIT_HE) 为层级分配权重、偏导储存空间并按照 InitType 的方式初始化 wXavierHE

FC 层将会接受大小满足 d*h*w=ins 的三维张量输入,并按照 w 加权求和后变换为大小为 ous*1*1 的三维张量。

偏置层(BIAS)

头文件:Layers/bias.h

定义了偏置层类型 BIAS,其特殊成员如下:

成员 含义/作用
int d 输入张量的通道数
int h 输入张量的高度
int w 输入张量的宽度
float* b 权重存储起始地址
bool inplace 是否原地计算
void init(int& m,SHAPE3D Input,bool Inplace=false) 初始化层级参数,额外利用 Input 的三维大小初始化 d,h,w,并使用 Inplace 初始化 inplace
void build(float*& wei,float*& tmp) 为层级分配权重、偏导储存空间,将 b 初始化为全 00

BIAS 层将会接受大小为 d*h*w 的三维张量输入,并为第 ii 个通道的所有值增加 b[i] 的偏置后输出。

卷积层(CONV)

头文件:Layers/conv.h

定义了卷积层类型 CONV,其特殊成员如下:

成员 含义/作用
int ind 输入张量的通道数
int inh 输入张量的高度
int inw 输入张量的宽度
int cnt 卷积核的个数(输出张量的通道数)
int ch 卷积核的高度
int cw 卷积核的宽度
int stx 卷积核在高度方向上的步长
int sty 卷积核在宽度方向上的步长
int pdx 高度方向上的 Padding 大小(上下都会补充 pdxpdval
int pdy 宽度方向上的 Padding 大小(左右都会填充 pdypdval
float pdval Padding 的值
int ouh 输出张量的高度
int ouw 输出张量的宽度
float* w 权重存储起始地址
void init(...) 初始化层级参数*
void build(float*& wei,float*& tmp,int InitType=INIT_HE) 为层级分配权重、偏导储存空间并按照 InitType 的方式初始化 wXavierHE

*init() 函数将初始化卷积层参数并计算出 ouhouw,详细声明及特殊参数含义如下:

void init(int& m,
    SHAPE3D Input,
    int CoreCnt,std::pair<int,int> Core,
    std::pair<int,int> Stride={1,1},
    std::pair<int,int> Padding={0,0},float PaddingVal=0)
参数 含义/作用
SHAPE3D Input 使用 Input 三维的值分别初始化 ind,inh,inw
int CoreCnt 使用该值初始化卷积核个数 cnt
std::pair<int,int> Core 初始化卷积核大小,ch=Core.first, cw=Core.second
std::pair<int,int> Stride 初始化步幅大小,stx=Stride.first, sty=Stride.second
std::pair<int,int> Padding 初始化 Padding 大小,pdx=Padding.first, pdy=Padding.second
float PaddingVal 使用该值初始化 pdval

调用 init() 函数后将自动初始化 ouh=(inh+pdx*2-ch)/stx+1, ouw=(inw+pdy*2-cw)/sty+1

CONV 层接受大小为 ind*inh*inw 的三维张量输入,并做卷积操作后输出大小为 cnt*ouh*ouw 的三维张量。

反卷积层(DECONV)

头文件:Layers/deconv.h

定义了反卷积层类型 DECONV,其特殊成员及其含义与 CONV 类型相同,但没有 pdvalPaddingVal,即输入张量的每个值乘上卷积核后叠加到输出张量上,cnt*ouh*oud 的三维张量经过参数(仅交换 cntind)一样的 CONV 后会变为 ind*inh*inw 的三维张量。

具体细节不再赘述,详见咕咕咕

池化层(POOLING)

头文件:Layers/pooling.h

定义了池化层类型 POOLING,其特殊成员如下:

成员 含义/作用
int ind 输入张量的通道数
int inh 输入张量的高度
int inw 输入张量的宽度
int ch 池化核的高度
int cw 池化核的宽度
int tpe 池化操作类型(最大池化/均值池化)
int stx 池化核在高度方向上的步长
int sty 池化核在宽度方向上的步长
int ouh 输出张量的高度
int ouw 输出张量的宽度
void init(...) 初始化层级参数*

*init() 函数将初始化池化层参数并计算出 ouhouw,详细声明及特殊参数含义如下:

inline void init(SHAPE3D Input,
    std::pair<int,int> Core,
    int Type=MAX_POOLING,
    std::pair<int,int> Stride={-1,-1})
参数 含义/作用
SHAPE3D Input 使用 Input 三维的值分别初始化 ind,inh,inw
std::pair<int,int> Core 初始化池化核大小,ch=Core.first, cw=Core.second
int Type 初始化池化操作类型,MAX_POOLING 表示最大池化,MEAN_POOLING 表示均值池化
std::pair<int,int> Stride 初始化步幅大小,stx=Stride.first, sty=Stride.second,特别的,若某一项为 -1 则表示该项取池化核的对应参数

调用 init() 函数后将自动初始化 ouh=(inh+stx-1)/stx, ouw=(inw+sty-1)/sty

POOLING 层接受大小为 ind*inh*inw 的三维张量输入,并做池化操作后输出大小为 ind*ouh*ouw 的三维张量。

拓展层(EXT)

头文件:Layers/ext.h

定义了拓展层类型 EXT,其效果是将每个位置的值在原地复制若干份(变胖),特殊成员如下:

成员 含义/作用
int ind 输入张量的通道数
int inh 输入张量的高度
int inw 输入张量的宽度
int filx 填充高度
int fily 填充宽度
int ouh 输出张量的高度
int ouw 输出张量的宽度
void init(SHAPE3D Input,std::pair<int,int> Fill) 初始化层级参数,使用 Input 三维的值分别初始化 ind,inh,inw,使用 Fill 两维的值分别初始化 filxfily

调用 init() 函数后将自动初始化 ouh=inh*filx, ouw=inw*fily

EXT 层接受大小为 ind*inh*inw 的三维张量输入,将每个值在原地变为 filx*fily 的值相等的矩形后输出大小为 ind*ouh*ouw 的三维张量。

批归一化层(BN)

头文件:Layers/bn.h

定义了批归一化层类型 BN,特殊成员如下:

成员 含义/作用
int d 输入张量的通道数
int h 输入张量的高度
int w 输入张量的宽度
float delta 滑动平均参数 δ\delta
float eps 极小量 ϵ\epsilon,防止让方差变得很小以至于除以零
float* k 系数数组起始地址
float* b 偏置数组起始地址
float* e_avg 均值的滑动平均,用于测试时的前向过程
float* e_var 方差的滑动平均,用于测试时的前向过程
void init(int& m,SHAPE3D Input,float Delta=0.9,float EPS=1e-4) 初始化层级参数,使用 Input 三维的值分别初始化 d,h,w,使用 DeltaEPS 分别初始化 deltaeps
void build(float*& wei,float*& tmp) 为层级分配权重、偏导储存空间并将 ke_var 初始化为全 11be_avg 初始化为全 00

BN 层将会对所有批的输入数据一起操作,为输入张量的每个通道做批归一化,而输出的三维张量形状不变。具体的,假设这是第 ii 个通道,将通道内部所有位置所有批的值拿出来放入数组 aa 中(假设共 nn 个),则在训练阶段得到对应的输出 a\overline{a} 的流程为:

μi=1nj=1naiσi=1nj=1n(ajμi)2aj=kiajμiσi+ϵ+bi\mu_i=\frac{1}{n}\sum\limits_{j=1}^{n} a_i\\ \sigma_i=\frac{1}{n}\sum\limits_{j=1}^{n} (a_j-\mu_i)^2\\ \overline{a}_j=k_i\frac{a_j-\mu_i}{\sqrt{\sigma_i+\epsilon}}+b_i

并且 e_avge_var 在每次训练阶段的前向过程都会做如下更新:

e_avgi=δ×e_avgi+(1δ)×μie_vari=δ×e_vari+(1δ)×σie\_avg_i=\delta\times e\_avg_i +(1-\delta)\times \mu_i\\ e\_var_i=\delta\times e\_var_i +(1-\delta)\times \sigma_i\\

在测试阶段,由于数据量较小,均值和方差往往不够准确,故采用之前的滑动平均来计算输出:

aj=kiaje_avgie_vari+ϵ+bi\overline{a}_j=k_i\frac{a_j-e\_avg_i}{\sqrt{e\_var_i+\epsilon}}+b_i

组归一化层(GN)

头文件:Layers/gn.h

定义了组归一化层类型 GN,特殊成员如下:

成员 含义/作用
int d 输入张量的通道数
int h 输入张量的高度
int w 输入张量的宽度
int g 每组的通道数
float eps 极小量 ϵ\epsilon,防止让方差变得很小以至于除以零
int cnt 组数
float* k 系数数组起始地址
float* b 偏置数组起始地址
void init(int& m,SHAPE3D Input,float EPS=1e-4) 初始化层级参数,使用 Input 三维的值分别初始化 d,h,w,使用 EPS 初始化 eps
void build(float*& wei,float*& tmp) 为层级分配权重、偏导储存空间并将 k 初始化为全 11b 初始化为全 00

调用 init() 函数后将自动初始化 cnt=d/g+(d%g!=0)

GN 层将会对每个批的数据分别操作,将输入张量每连续的至多 gg 个通道分为一组,共 cntcnt 组,每组内做和 BN 层大致相同的归一化操作,而输出的三维张量形状不变。

由于是对每个批的数据分别操作,故测试时的前向过程和训练时一致。

Softmax 归一化层(Softmax)

头文件:Layers/Softmax.h

定义了 Softmax 归一化层类型 SOFTMAX,特殊成员如下:

成员 含义/作用
int d 输入张量的通道数
int h 输入张量的高度
int w 输入张量的宽度
void init(SHAPE3D Input) 初始化层级参数,使用 Input 三维的值分别初始化 d,h,w

SOFTMAX 层将会对输入张量沿着通道做 Softmax 操作,输出张量三维形状不变,即对于位置 x,yx,y 上的 dd 个值,设其分别为 a[1,d]a_{[1,d]},则输出 a[1,d]\overline a_{[1,d]} 的计算方法如下:

ai=eaij=1deaj\overline{a}_i=\frac{e^{a_i}}{\sum\limits_{j=1}^d e^{a_j}}

各种激活函数层

公共特殊成员:

成员 含义/作用
int siz 输入张量的大小(d*h*w
bool inplace 是否原地计算
void init(int Siz,bool Inplace=false,...) 初始化层级参数,前两个参数及其含义固定(使用 Siz 初始化 siz,使用 Inplace 初始化 inplace),若有更多参数将给出说明

各种激活函数层将对输入张量的每个数值分别应用对应的激活函数 ff,即 xout=f(xin)x_{\text{out}}=f(x_{\text{in}}),输出张量三维形状不变。

ReLU 层(RELU)

头文件:Layers/ReLU.h

定义了 ReLU 层类型 RELU

f(x)=max(x,0)f(x)=\max(x,0)

Leaky_ReLU 层(LEAKY_RELU)

头文件:Layers/Leaky_ReLU.h

定义了 Leaky_ReLU 层类型 LEAKY_RELU,其特殊成员如下:

成员 含义/作用
float a 激活函数 ff 中的参数 α\alpha
void init(int Siz,float Alpha=0.01) 初始化层级参数,额外使用 Alpha 初始化 a

激活函数表达式如下:

f(x)={αxx<0xx0f(x)=\begin{cases}\alpha x&x<0\\x&x\ge 0\end{cases}

Sigmoid 层(SIGMOID)

头文件:Layers/Sigmoid.h

定义了 Sigmoid 层类型 SIGMOID

f(x)=11+exf(x)=\frac{1}{1+e^{-x}}

Tanh 层(TANH)

头文件:Layers/Tanh.h

定义了 Tanh 层类型 TANH

f(x)=exexex+exf(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}}

自动生成保存/读取/释放空间函数

头文件:auto_saveload.h,定义宏 ENABLE_AUTO_SL 以启用。

原理是使用构造函数创建反射,依次执行预设优化器和所有预设层级的对应函数。

基础用法

在用户定义的神经网络类中,将预设优化器的声明放置于所有预设层级声明的前面,并且不能有多个预设优化器。

在预设优化器声明前加上 AUTO_SL_BEG 关键字,在所有预设层级声明的末尾加上 AUTO_SL_BEG 关键字。

将会自动定义类的三个成员变量 saveloaddelthis,并自动定义其 () 运算符。

例子:

class network
{
AUTO_SL_BEG
    ADAM opt;
    FC fc1,fc2,fc3;
    CONV c1,c2;
AUTO_SL_END
}test;

使用 test.save(path)test.load(path) 以保存到文件或从文件中读取。

使用 test.delthis() 以释放该神经网络类预设优化器及各预设层级占用的内存空间。

进阶用法

对于用户自己定义的层级/优化器,可以再其构造函数中使用如下几个宏自动生成对应的反射注册代码:

AUTO_SL_LAYER_CONSTRUCTER_WEIGHT_DELTHISFUNC(weight_add)
AUTO_SL_LAYER_CONSTRUCTER_WEIGHT(weight_add)
AUTO_SL_LAYER_CONSTRUCTER_DELTHISFUNC
AUTO_SL_LAYER_CONSTRUCTER

AUTO_SL_OPTIMIZER_CONSTRUCTER

其中 weight_add 为指向该层级的权重起始地址的指针,用于在保存/读取时确定各层级间权重分配顺序。

具体详见 auto_saveload.h 中的注释及源代码。

图片读写及其它文件操作

头文件 file_io.h

定义了若干文件操作(包含图片读写)函数:

函数 含义/作用
void readf(std::ifstream& inf,T& x) 从二进制输入流 inf 中读取类型 T 的数据到 x
void readf(std::ifstream& inf,T* x,int siz) 从二进制输入流 inf 中读取连续的 siz 个类型 T 的数据到 x 起始的数组中
writf(std::ofstream& ouf,T x) 将类型 T 的数据 x 输出到二进制输出流 ouf
writf(std::ofstream& ouf,T* x,int siz) 将从 x 起始的连续 siz个类型 T 的数据依次输出到二进制输出流 ouf
void readimg(std::string path,int& d,int& h,int& w,float* img,float l=-1,float r=1) 读取 path 对应的图片文件,将其通道数和高度、宽度分别储存在 d,h,w 中,将其每个像素点每个通道的值归一化到 [l,r][l,r] 后按 d*h*w 的格式储存在 img
void readimg(std::string path,float* img,float l=-1,float r=1) 含义基本同上,但不储存三维大小
void savejpg(std::string path,int d,int h,int w,float* img,float l=-1,float r=1,int quality=100) 将起始地址为 img 的一张归一化到 [l,r][l,r]d*h*w 的图片以 jpg 的格式保存在文件 path 中,图片质量为 quality
void savepng(std::string path,int d,int h,int w,float* img,float l=-1,float r=1) 含义基本同上,但保存格式为 png 且无图片质量参数
void savebmp(std::string path,int d,int h,int w,float* img,float l=-1,float r=1) 含义基本同上,但保存格式为 bmp
void getfiles(std::string path,std::vector<std::string>& files) 获取目录 path 下的所有文件,并将其路径保存到 files 中(也会获取更深的所有子目录中的文件)

其它头文件

头文件 含义/作用
stb/*.h 开源库 stb 中的若干头文件
Eigen/* 开源库 Eigen 的 3.4.0 版本
fast_calc.h 定义了快速矩阵乘法函数
defines.h 各头文件的公共定义、引用
network.h 库入口

基于 MNIST 的 demo

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <chrono>

#define ENABLE_AUTO_SL 

#include "./network_h/network.h"

using namespace std;

const int T = 60000, TEST_T = 10000;
const int Batch_Size = 60;
const float lrt = 0.001;
const int total_batch = 5000, calctme = 5;

struct NETWORK
{
	AUTO_SL_BEG
		ADAM opt;
		
		CONV c1;
		BN b1;
		LEAKY_RELU a1;
		POOLING p1;
		
		CONV c2;
		BN b2;
		LEAKY_RELU a2;
		POOLING p2;
		
		FC fc1;
		BIAS bi1;
		LEAKY_RELU a3;
		
		FC fc2;
		BIAS bi2;
		SOFTMAX sfm1;
	AUTO_SL_END
	
	float in[Batch_Size * 28 * 28];
	val3d out;
	
	inline void init()
	{
		opt.init(lrt);
		
		c1.init(opt.m,SHAPE3D(1,28,28),8,{3,3},{1,1},{1,1},0);
		b1.init(opt.m,SHAPE3D(8,28,28));
		a1.init(8*28*28,true);
		p1.init(SHAPE3D(8,28,28),{2,2});
		
		c2.init(opt.m,SHAPE3D(8,14,14),16,{3,3},{1,1},{1,1},0);
		b2.init(opt.m,SHAPE3D(16,14,14));
		a2.init(16*14*14,true);
		p2.init(SHAPE3D(16,14,14),{2,2});
		
		fc1.init(opt.m,16*7*7,128);
		bi1.init(opt.m,SHAPE3D(128,1,1),true);
		a3.init(128,true);
		
		fc2.init(opt.m,128,10);
		bi2.init(opt.m,SHAPE3D(10,1,1),true);
		sfm1.init(SHAPE3D(10,1,1));
		
		opt.build();
		
		float *wei=opt._wei(),*tmp=opt._tmp();
		c1.build(wei,tmp),b1.build(wei,tmp);
		c2.build(wei,tmp),b2.build(wei,tmp);
		fc1.build(wei,tmp),bi1.build(wei,tmp);
		fc2.build(wei,tmp,INIT_XAVIER),bi2.build(wei,tmp); 
	}
	inline void forward(bool test)
	{
		auto_dao::init(test?0:Batch_Size);
		val3d x(1,28,28,in);
		x=c1(x),x=b1(x),x=a1(x),x=p1(x);
		x=c2(x),x=b2(x),x=a2(x),x=p2(x);
		x=fc1(x),x=bi1(x),x=a3(x);
		x=fc2(x),x=bi2(x),x=sfm1(x);
		out=x;
	}
	inline float backward(float *rout)
	{
		opt.init_backward();
		float res=MSEloss(out,rout);
		out.backward();
		opt.flush();
		return res;
	}
};

float casin[T + 5][28 * 28];
int casans[T + 5];

float outs[Batch_Size * 10];
float total_loss;

NETWORK brn;

inline void loaddata(string imgpath,string anspath,int T)
{
	FILE* fimg = fopen(imgpath.c_str(), "rb");
	FILE* fans = fopen(anspath.c_str(), "rb");
	if (fimg == NULL)
	{
		puts("加载图片数据失败\n");
		system("pause");
		exit(1);
	}
	if (fans == NULL)
	{
		puts("加载答案数据失败\n");
		system("pause");
		exit(1);
	}
	fseek(fimg, 16, SEEK_SET);
	fseek(fans, 8, SEEK_SET);
	unsigned char* img = new unsigned char[28 * 28];
	for (int cas = 1; cas <= T; cas++)
	{
		fread(img, 1, 28 * 28, fimg);
		for (int i = 0; i < 28 * 28; i++) casin[cas][i] = img[i] / (float)255;
		unsigned char num;
		fread(&num, 1, 1, fans);
		casans[cas] = num;
	}
	delete[] img;
	fclose(fimg), fclose(fans);
}

void train()
{
	int cins = 0, couts = 0;
	for (int tb = 1; tb <= Batch_Size; tb++)
	{
		int cas = (rand() * (RAND_MAX + 1) + rand()) % T + 1;
		for (int i = 0; i < 28 * 28; i++) brn.in[cins++] = casin[cas][i];
		for (int i = 0; i < 10; i++) outs[couts++] = casans[cas] == i;
	}
	brn.forward(false);
	total_loss+=brn.backward(outs);
}

inline bool test(int cas)
{
	for (int i = 0; i < 28 * 28; i++) brn.in[i] = casin[cas][i];
	brn.forward(true);
	int mxid = 0;
	for (int i = 1; i < 10; i++) if (brn.out.a[i] > brn.out.a[mxid]) mxid = i;
	return mxid == casans[cas];
}

int main()
{
	printf("模式选择:\n");
	printf("[1] 加载 AI 并测试\n");
	printf("[2] 训练 AI(最好的 AI 模型将会保存到 ./best.ai)\n");
	int mode;
	scanf("%d", &mode);
	system("cls");
	string imgpath = "./MNIST/img", 
		   anspath = "./MNIST/ans", 
		   testimgpath = "./MNIST/testimg",
		   testanspath = "./MNIST/testans";
	printf("训练图片文件:%s\n", imgpath.c_str());
	printf("训练答案文件:%s\n", anspath.c_str());
	printf("评估图片文件:%s\n", testimgpath.c_str());
	printf("评估答案文件:%s\n\n", testanspath.c_str());
	if (mode == 1)
	{
		printf("请输入之前保存的 AI 路径\n");
		string path;
		cin >> path;
		brn.load(path);
	}
	else
	{
		printf("加载数据中...\n");
		loaddata(imgpath,anspath,T);
		printf("加载数据完成\n\n");
		brn.init();
		total_loss = 0;
		printf("开始训练...\n\n");
    	auto start = std::chrono::high_resolution_clock::now();
		for (int i = 1; i <= total_batch; i++)
		{
			train();
			if (i % calctme == 0)
			{
				total_loss /= (float)calctme;
				printf("[%.2f%%] 训练了 %d 组样本,平均 loss %f\n", i / (float)total_batch * 100, i * Batch_Size, total_loss);
				total_loss = 0;
			}
		}
	    auto stop = std::chrono::high_resolution_clock::now();
	    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count();
		brn.save("best.ai");
		printf("\n训练完成!共训练 %d ms,模型已保存到 best.ai\n\n",(int)duration);
	}
	printf("加载测试数据中...\n");
	loaddata(testimgpath,testanspath,TEST_T);
	printf("加载测试数据完成\n\n");
	printf("开始模型评估...\n");
	int tot = 0;
	for (int i = 1; i <= TEST_T; i ++) tot += test(i);
	printf("模型评估完成,正确率:%.2f%%\n\n", (float)tot / TEST_T * 100);
	brn.delthis();
	system("pause");
	return 0;
}