- 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
,即 ;(该功能尚未完善,额外内存可能超过该值,故建议尽量设置小一点)
关于计算加速
由于矩阵乘法算子基于 Eigen 和使用 OpenCL 的 ViennaCL,故仅需加速此两库即可。具体可以通过启用 OpenMP 和各种指令集(AVX、SSE)来加速,例如在 GCC 下使用 -Ofast -fopenmp -march=native
编译命令来启用 OpenMP 和指令集并开启 Ofast 优化。
基本概念
训练阶段
在此阶段,数据以批为单位进入神经网络,执行前向过程,计算出结果;再根据结果对应的损失值,反向传播得出各参数偏导,通过优化器更新参数。
测试阶段
在测试阶段,数据一个一个进入神经网络,执行前向过程,计算出结果。
该阶段中反向传播会被禁用,且某些层级算子的行为会改变(例如批归一化层)。
张量、自动反向传播
头文件 auto_dao.h
。
命名空间 auto_dao
成员 | 含义/作用 |
---|---|
int Batch_Size |
批大小,若为 则表示当前为测试阶段而非训练阶段 |
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 处的数值为 x 的 i%x.d,j%x.h,k%x.w 处的数值 |
val3d operator+(val3d x,val3d y) |
创建一个新的三维张量,其每一位的数值都是 x 对应位置的数值和 y 对应位置的数值相加(需保证 x 和 y 形状相同) |
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) |
创建一个新的三维张量,其是 x 和 y 按照 d 这一维拼接起来的结果(x 占用 [0,x.d-1] ,y 占用 [x.d,x.d+y.d-1] ,需保证 x.h==y.h 且 x.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)
是输出, 是真实数据:
二元交叉熵损失(BSEloss)
是输出, 是真实数据:
注意 会被限制在 内以防止出现 inf
或 nan
,若超出范围则偏导为 。
预设优化器
头文件: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
,其所有公有成员均无特殊。
参数更新方式:( 为参数, 为偏导)
Adam 优化器
头文件:Optimizer/Adam.h
定义了优化器类型 ADAM
,其特殊成员如下:
成员 | 含义/作用 |
---|---|
float b1 |
参数更新公式中的 |
float b2 |
参数更新公式中的 |
float eps |
参数更新公式中的 ,一个很小的非负实数,防止除以 |
void init(float Learn_Rate,float beta1=0.9,float beta2=0.999,float Eps=1e-8) |
初始化优化器参数,b1 ,b2 和 eps 分别初始化为 beta1 ,beta2 和 Eps |
参数更新方式:( 为参数, 为偏导)
-
初始 ;
-
对于第 次更新:
预设网络层级(层级算子)
头文件: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 中读取层级参数,并根据 wei 和 tmp 为层级分配权重* |
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) |
初始化层级参数,额外将 ins 和 ous 初始化为 INS 和 OUS |
void build(float*& wei,float*& tmp,int InitType=INIT_HE) |
为层级分配权重、偏导储存空间并按照 InitType 的方式初始化 w (Xavier 或 HE ) |
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 初始化为全 |
BIAS
层将会接受大小为 d*h*w
的三维张量输入,并为第 个通道的所有值增加 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 大小(上下都会补充 pdx 个 pdval ) |
int pdy |
宽度方向上的 Padding 大小(左右都会填充 pdy 个 pdval ) |
float pdval |
Padding 的值 |
int ouh |
输出张量的高度 |
int ouw |
输出张量的宽度 |
float* w |
权重存储起始地址 |
void init(...) |
初始化层级参数* |
void build(float*& wei,float*& tmp,int InitType=INIT_HE) |
为层级分配权重、偏导储存空间并按照 InitType 的方式初始化 w (Xavier 或 HE ) |
*:init()
函数将初始化卷积层参数并计算出 ouh
和 ouw
,详细声明及特殊参数含义如下:
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
类型相同,但没有 pdval
及 PaddingVal
,即输入张量的每个值乘上卷积核后叠加到输出张量上,cnt*ouh*oud
的三维张量经过参数(仅交换 cnt
和 ind
)一样的 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()
函数将初始化池化层参数并计算出 ouh
和 ouw
,详细声明及特殊参数含义如下:
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 两维的值分别初始化 filx 和 fily |
调用 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 |
滑动平均参数 |
float eps |
极小量 ,防止让方差变得很小以至于除以零 |
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 ,使用 Delta 和 EPS 分别初始化 delta 和 eps |
void build(float*& wei,float*& tmp) |
为层级分配权重、偏导储存空间并将 k 和 e_var 初始化为全 ,b 和 e_avg 初始化为全 |
BN
层将会对所有批的输入数据一起操作,为输入张量的每个通道做批归一化,而输出的三维张量形状不变。具体的,假设这是第 个通道,将通道内部所有位置所有批的值拿出来放入数组 中(假设共 个),则在训练阶段得到对应的输出 的流程为:
并且 e_avg
和 e_var
在每次训练阶段的前向过程都会做如下更新:
在测试阶段,由于数据量较小,均值和方差往往不够准确,故采用之前的滑动平均来计算输出:
组归一化层(GN)
头文件:Layers/gn.h
定义了组归一化层类型 GN
,特殊成员如下:
成员 | 含义/作用 |
---|---|
int d |
输入张量的通道数 |
int h |
输入张量的高度 |
int w |
输入张量的宽度 |
int g |
每组的通道数 |
float eps |
极小量 ,防止让方差变得很小以至于除以零 |
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 初始化为全 ,b 初始化为全 |
调用 init()
函数后将自动初始化 cnt=d/g+(d%g!=0)
。
GN
层将会对每个批的数据分别操作,将输入张量每连续的至多 个通道分为一组,共 组,每组内做和 BN
层大致相同的归一化操作,而输出的三维张量形状不变。
由于是对每个批的数据分别操作,故测试时的前向过程和训练时一致。
Softmax 归一化层(Softmax)
头文件:Layers/Softmax.h
定义了 Softmax 归一化层类型 SOFTMAX
,特殊成员如下:
成员 | 含义/作用 |
---|---|
int d |
输入张量的通道数 |
int h |
输入张量的高度 |
int w |
输入张量的宽度 |
void init(SHAPE3D Input) |
初始化层级参数,使用 Input 三维的值分别初始化 d,h,w |
SOFTMAX
层将会对输入张量沿着通道做 Softmax 操作,输出张量三维形状不变,即对于位置 上的 个值,设其分别为 ,则输出 的计算方法如下:
各种激活函数层
公共特殊成员:
成员 | 含义/作用 |
---|---|
int siz |
输入张量的大小(d*h*w ) |
bool inplace |
是否原地计算 |
void init(int Siz,bool Inplace=false,...) |
初始化层级参数,前两个参数及其含义固定(使用 Siz 初始化 siz ,使用 Inplace 初始化 inplace ),若有更多参数将给出说明 |
各种激活函数层将对输入张量的每个数值分别应用对应的激活函数 ,即 ,输出张量三维形状不变。
ReLU 层(RELU)
头文件:Layers/ReLU.h
定义了 ReLU 层类型 RELU
:
Leaky_ReLU 层(LEAKY_RELU)
头文件:Layers/Leaky_ReLU.h
定义了 Leaky_ReLU 层类型 LEAKY_RELU
,其特殊成员如下:
成员 | 含义/作用 |
---|---|
float a |
激活函数 中的参数 |
void init(int Siz,float Alpha=0.01) |
初始化层级参数,额外使用 Alpha 初始化 a |
激活函数表达式如下:
Sigmoid 层(SIGMOID)
头文件:Layers/Sigmoid.h
定义了 Sigmoid 层类型 SIGMOID
:
Tanh 层(TANH)
头文件:Layers/Tanh.h
定义了 Tanh 层类型 TANH
:
自动生成保存/读取/释放空间函数
头文件:auto_saveload.h
,定义宏 ENABLE_AUTO_SL
以启用。
原理是使用构造函数创建反射,依次执行预设优化器和所有预设层级的对应函数。
基础用法
在用户定义的神经网络类中,将预设优化器的声明放置于所有预设层级声明的前面,并且不能有多个预设优化器。
在预设优化器声明前加上 AUTO_SL_BEG
关键字,在所有预设层级声明的末尾加上 AUTO_SL_BEG
关键字。
将会自动定义类的三个成员变量 save
、load
和 delthis
,并自动定义其 ()
运算符。
例子:
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 中,将其每个像素点每个通道的值归一化到 后按 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 的一张归一化到 的 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;
}