Skip to content

Latest commit

 

History

History
401 lines (279 loc) · 18.7 KB

develop_doc.md

File metadata and controls

401 lines (279 loc) · 18.7 KB

faQnet 开发文档

logo

概述

faQnet 是一个基于C++的神经网络库,通过实现一系列简明易懂的类和函数,为用户提供一个简单易用的神经网络开发环境。
本文档将介绍开发中已经与计划实现的架构与功能,以及较为具体的实现方法。
注意:本文档着重与介绍实现方法,具体的使用接口,请参考用户文档

net类 代表整个神经网络

这是整个神经网络最大的类,代表整个神经网络。
我们为了简化计算,用单列矩阵储存输入输出。显然,第一层节点的数量必须等于输入矩阵的行数,最后一层节点的数量必须等于输出矩阵的行数。

成员变量:


  • 这是一个vector,储存layer类对象。所有的层都储存在这里。layer类对象会在后文详细提到。

  • 输入均值矩阵
    这是一个Mat对象,储存所有输入矩阵每一项的均值。

  • 输入标准差矩阵
    这是一个Mat对象,储存所有输入矩阵每一项的标准差。

  • 输出均值矩阵
    这是一个Mat对象,储存所有输出矩阵每一项的均值。

  • 输出标准差矩阵
    这是一个Mat对象,储存所有输出矩阵每一项的标准差。

成员函数:

  • 构造函数
    这个函数接受每一层每一层节点详情,然后借此初始化神经网络。这里所谓初始化神经网络的含义是:在成员变量层生成每一层。实际上这非常简单——只需要将该层节点数和下一层节点数及激活函数类型传入构造函数即可。
    要注意的是最后一层没有下一层,这意味着其输出数就是本层节点数。

    传入参数:

    • 每一层节点数
      这是一个vector,储存一些整数。
    • 每一层激活函数类型
      这是一个vector,储存一些字符串。
  • 前向传播
    这个函数接受一个输入矩阵,然后通过调用每一层的前向传播函数,将输入矩阵传入第一层,将第一层的输出矩阵传入第二层,将第二层的输出矩阵传入第三层,以此类推,直到最后一层,将最后一层的输出矩阵作为整个神经网络的输出矩阵。

    传入参数:

    • 输入矩阵
      这是一个Mat对象,储存输入矩阵。规格为输入数据数*1。
  • 反向传播
    这个函数接受前向传播的输出矩阵和目标矩阵,首先通过执行损失函数的导函数得到最后一层的输出对于损失值的偏导,然后代入最后一层的反向传播,将结果传入倒数第二层,将倒数第二层的结果传入倒数第三层,以此类推,直到第一层。

    传入参数:

    • 输出矩阵
      这是一个Mat对象,即前向传播的输出。

    • 目标矩阵
      这是一个Mat对象,即目标输出矩阵。

    • 损失函数名
      这是一个字符串,代表损失函数。

  • 损失函数
    这个函数接受前向传播的输出矩阵和目标矩阵。计算损失值主要是为了显示出来便于分析,或者是因为这样看起来比较厉害(毕竟训练实际上只用求损失函数的导函数就够了)。损失函数的实现见下文。

    传入参数:

    • 输出矩阵
      这是一个Mat对象,即前向传播的输出。

    • 目标矩阵
      这是一个Mat对象,即目标输出矩阵。

    • 损失函数名
      这是一个字符串,代表损失函数。

  • 权值更新
    这个函数接受一个学习率,然后循环调用每一层的权值更新函数。

    传入参数:

    • 学习率
      这是一个double型变量,代表学习率。
  • 偏置更新
    这个函数接受一个学习率,然后循环调用每一层的偏置更新函数。

    传入参数:

    • 学习率
      这是一个double型变量,代表学习率。
  • 训练
    这个函数接受一个训练集,一个学习率,一个训练次数,一个损失函数名。每次训练,先将训练集传入前向传播函数为每一层生成结果矩阵,然后调用损失函数计算loss值,接着调用反向传播函数为每一层生成误差矩阵,最后完成权值更新和偏置更新。
    这个例子里采用的是固定循环次数的训练方法。另外,也可以采用当loss值小于某个值时停止训练的方法。我计划同时实现这两种方法。

    传入参数:

    • 输入矩阵
      这是一个Mat对象,储存训练集。是一个单列矩阵,第一层节点数应当等于它的行数。

    • 目标矩阵
      这是一个Mat对象,储存目标输出矩阵。是一个单列矩阵,最后一层节点数应当等于它的行数。

    • 学习率
      这是一个double型变量,代表学习率。

    • 训练次数
      这是一个整数,代表训练次数。

    • 损失函数名
      这是一个字符串,代表损失函数。

  • 保存模型
    这个函数接受一个文件名,然后将整个神经网络保存到该文件中。把除了结果矩阵和误差矩阵之外的变量全存下来就可以了。

    传入参数:

    • 文件名
      这是一个字符串,代表文件名。
  • 加载模型
    这个函数接受一个文件名,然后将该文件中的神经网络加载到当前神经网络中。

    传入参数:

    • 文件名
      这是一个字符串,代表文件名。
  • 预测
    这个函数接受一个输入矩阵,然后传入前向传播函数,将输出传入过滤函数,输出即是整个神经网络输出。

    传入参数:

    • 输入矩阵
      这是一个Mat对象,储存输入矩阵。是一个行数等于第一层节点数的单列矩阵。
  • 输入数据归一化预处理
    这个函数接受若干输入矩阵,然后计算每一项数据的均值和标准差。
    传入参数:

    • 输入矩阵(若干) 这是一个储存Mat对象的vector,储存若干输入矩阵。
  • 输入数据归一化
    这个函数接受一个输入矩阵,然后返回归一化后的矩阵。
    传入参数:

    • 输入矩阵
      这是一个Mat对象,储存输入矩阵。是一个行数等于第一层节点数的单列矩阵。
  • 输出数据归一化预处理
    这个函数接受若干输出矩阵,然后计算每一项数据的均值和标准差。
    传入参数:

    • 输出矩阵(若干) 这是一个储存Mat对象的vector,储存若干输出矩阵。
  • 输出数据反归一化
    这个函数接受若干输出矩阵,然后返回将其反归一化后的矩阵。
    传入参数:

    • 输出矩阵
  • 输出数据归一化
    这个函数接受若干输出矩阵,然后返回将其归一化后的矩阵。
    传入参数:

    • 输出矩阵

layer类 代表神经网络的一层

尽管神经网络是以神经元节点为基本单位,但是只要有一定的线性代数基础,就不难注意到:可以通过将一层的所有节点的数据存入一个矩阵,将基本单位从节点提升到层,以达到简化的目的,同时还可以调用已有的库中对于矩阵运算的方法,实现性能优化。这里我们采用openCV的Mat对象实现矩阵。
(也可以用UMat对象,这样可以在计算机配置了openCL的情况下利用GPU加速,但是我觉得没必要)
值得注意的是这里对矩阵中的所有项都默认为0,因为有几种不同的方式为权值矩阵和偏置项矩阵赋值,因此最好将赋值过程处理为单独的函数。
注意到这里实际上将激活函数视作了线性函数处理,这是为了简化计算。上过高中的人都知道:在很小的范围内,可以以直代曲近似相等的。而梯度下降的步长通常很小,因此这样处理是可行的。

成员变量:

  • 权值矩阵
    一个Mat对象,储存该层所有节点对下一层所有节点的权值。规格为下一层节点数*该层节点数 。(这样处理的意义见下文。)

  • 偏置项矩阵
    一个Mat对象,储存该层的偏置项。规格为下一层节点数*1。(实际上,每一项代表该层所有节点到下一层某一节点的该层节点偏置项之和。这样处理的意义见下文。)

  • 激活函数
    一个字符串,用于储存该层所用的激活函数。 激活函数有多种,计划实现8种以上。

  • 输入矩阵
    一个Mat对象,储存该层所有节点的前向传播中的输入。规格为该层节点数*1。在反向传播中会用到。

  • 结果矩阵
    一个Mat对象,储存该层所有节点的前向传播中的线性运算结果。规格为下一层节点数*1。在反向传播中会用到。

  • 误差矩阵
    储存该层线性运算的每个结果对于最终输出的损失值的偏导数。在反向传播中计算。规格为下一层节点数*1。

成员函数

  • 构造函数:
    传入该层节点数,下一层节点数和激活函数类型,将成员变量的矩阵规格按上文设定。

    传入参数:

    • 该层节点数
    • 下一层节点数
      这两个是整数。
    • 激活函数类型(默认为"sigmoid")
      这个是字符串。
  • 初始化矩阵
    实际上这并不是一个函数,而是一类函数,因为有几种不同的方法用于初始化。按计划,我们应当实现所有常见的初始化方法,即:均匀分布初始化,正态分布初始化,常数初始化。另外,也有两个矩阵需要初始化。
    下面是三种初始化方法简介:

    • 均匀分布初始化
      传入均匀分布的最小值和最大值和需要初始化的矩阵,将矩阵中的每一项赋值为一个均匀分布的随机值。
    • 正态分布初始化
      传入正态分布的均值和标准差和需要初始化的矩阵,将矩阵中的每一项赋值为一个正态分布的随机值。
    • 常数初始化
      传入一个常数和需要初始化的矩阵,将矩阵中的每一项赋值为该常数。
  • 单层前向传播
    这个函数接受一个输入矩阵,然后通过线型运算和激活函数运算,将激活函数的输出矩阵作为该层的输出矩阵。
    具体而言,该函数首先将权值矩阵与输入矩阵相乘,然后加上偏置项矩阵,得到一个中间矩阵,将中间矩阵保存为结果矩阵,然后将其代入激活函数,将激活函数的输出矩阵作为该层的输出。
    从这里我们可以发现:权值矩阵的规格为下一层节点数*该层节点数,这是因为权值矩阵的每一行代表一个节点,每一列代表对下一层某一个节点的权重;偏置项矩阵的规格为下一层节点数*1,这是因为偏置项矩阵的每一项代表下一层某一节点的偏置项。 传入参数:

    • 输入矩阵
      这是一个Mat对象,储存输入矩阵。规格为输入数据数*1。
    • 输出矩阵
      这是一个Mat对象,储存输出矩阵。
  • 单层反向传播
    这个函数接受上一层的误差矩阵与上一层权值矩阵的转置矩阵相乘得到的中间矩阵,将其与这一层的激活函数在结果矩阵处的导函数值进行点乘即得到该层的误差矩阵,将其保存,并输出其与上一层权值矩阵的转置矩阵相乘得到的矩阵。

    传入参数:

    • 上一层的误差矩阵与上一层权值矩阵的转置矩阵相乘得到的中间矩阵
  • 单层权值更新
    这个函数接受一个学习率。权值更新的具体操作是:将权值矩阵减去(学习率*该该层误差矩阵*该层输入矩阵的转置矩阵)。

    传入参数:

    • 学习率
      这是一个double型变量,代表学习率。
  • 单层偏置更新
    这个函数接受一个学习率。偏置项更新的具体操作是:将偏置项矩阵减去(学习率*该层误差矩阵)。

    传入参数:

    • 学习率
      这是一个double型变量,代表学习率。

激活函数

激活函数是神经网络中不可或缺的部分。
激活函数有多种,计划实现8种以上。
下面是几种常见的激活函数简介:

  • Sigmoid函数(Logistic函数)
    这是最常见的一种激活函数。
    公式:
    $${ f }(x)=\sigma (x)=\frac { 1 }{ 1+{ e }^{ -x } } $$ 导函数:
    $${ f }^{ ' }(x)=f(x)(1-f(x))$$

  • Tanh函数
    公式:
    $${ f }(x)=tanh(x)=\frac { { e }^{ x }-{ e }^{ -x } }{ { e }^{ x }+{ e }^{ -x } }$$ 导函数:
    $${ f }^{ ' }(x)=1-f(x)^{ 2 }$$

  • ReLU函数
    公式:
    $$\begin{split}f(x)=\begin{cases} \begin{matrix} 0 & x<0 \end{matrix} \ \begin{matrix} x & x\ge 0 \end{matrix} \end{cases}\end{split}$$
    导函数:
    $$\begin{split}{ { f }(x) }^{ ' }=\begin{cases} \begin{matrix} 0 & x<0 \end{matrix} \ \begin{matrix} 1 & x\ge 0 \end{matrix} \end{cases}\end{split}$$

  • LeakyReLU函数
    公式:
    $$\begin{split} f(x)=\begin{cases} \begin{matrix} 0.01 x & x<0 \end{matrix} \ \begin{matrix} x & x\ge 0 \end{matrix} \end{cases}\end{split}$$
    导函数:
    $$\begin{split}{ { f }(x) }^{ ' }=\begin{cases} \begin{matrix} 0.01 & x<0 \end{matrix} \ \begin{matrix} 1 & x\ge 0 \end{matrix} \end{cases}\end{split}$$

  • ELU函数
    这里我们取$\alpha =1$。
    公式:
    $$\begin{split} f(\alpha ,x)=\begin{cases} \begin{matrix} \alpha \left( { e }^{ x }-1 \right) & x<0 \end{matrix} \ \begin{matrix} x & x\ge 0 \end{matrix} \end{cases}\end{split}$$
    导函数:
    $$\begin{split}{ { f }(\alpha ,x) }^{ ' }=\begin{cases} \begin{matrix} f(\alpha ,x)+\alpha & x<0 \end{matrix} \ \begin{matrix} 1 & x\ge 0 \end{matrix} \end{cases}\end{split}$$

  • Softplus函数
    公式:
    $$f(x)=\ln { (1+{ e }^{ x }) }$$
    导函数:
    $${ f }^{ ' }(x)=\frac { 1 }{ 1+{ e }^{ -x } }$$

  • Softsign函数
    公式:
    $$f(x)=\frac { x }{ \left| x \right| +1 }$$
    导函数:
    $${ f }^{ ' }(x)=\frac { 1 }{ { (1+\left| x \right| ) }^{ 2 } } $$

  • Swish函数
    公式:
    $$f\left( x \right) =x\cdot \sigma \left( x \right) $$
    其中,$\sigma(x)$ 是 $sigmoid$ 函数。
    导函数:
    $$f^{'}\left( x \right) =\sigma \left( x \right) +x\cdot \sigma^{'} \left( x \right) $$

  • softmax函数
    公式:
    $$f\left( x \right) =\frac { { e }^{ x } }{ \sum _{ i }^{ n }{ { e }^{ x } } } $$

损失函数

损失函数本质上就是计算预测值和真实值的差距的一类函数。

以下函数适合回归问题:

  • 平均绝对误差 (MAE/L1Loss)
    公式:
    $$L={ \left| y_{ i }-y_{ i }^{ \prime } \right| }$$
    导函数: $$L=\operatorname{sign}(y_{ i } - y_{ i }^{ \prime }) $$ 其中,$y_{ i }$ 是第 $i$ 个样本的真实值,$y_{ i }^{ \prime }$ 是第 $i$ 个样本的预测值,$n$ 是样本数量。

  • 平均平方误差 (MSE/L2Loss)
    公式:
    $$L={ { (y_{ i }-y_{ i }^{ \prime } ) }^{ 2 } }$$
    导函数:
    $$L=2\left( y_{ i }-y_{ i }^{ \prime } \right) $$ 其中,$y_{ i }$ 是第 $i$ 个样本的真实值,$y_{ i }^{ \prime }$ 是第 $i$ 个样本的预测值,$n$ 是样本数量。

  • 平滑平均绝对误差 (SLL/Smooth L1Loss)
    公式: $$\begin{split} L = \begin{cases} \begin{matrix} 0.5 \left( y_{ i } - y_{ i }^{ \prime } \right) ^{ 2 } & \left| y_{ i } - y_{ i }^{ \prime } \right| <1 \end{matrix} \ \begin{matrix} \left| y_{ i } - y_{ i } \right| - 0.5 & \left| y_{ i } - y_{ i }^{ \prime } \right| \ge 1 \end{matrix} \end{cases}\end{split}$$
    导函数:
    $$\begin{split} L = \begin{cases} \begin{matrix} y_{ i } - y_{ i }^{ \prime } & \left| y_{ i } - y_{ i }^{ \prime } \right| <1 \end{matrix} \ \begin{matrix} \operatorname{sign}(y_{ i } - y_{ i }^{ \prime }) & \left| y_{ i } - y_{ i }^{ \prime } \right| \ge 1 \end{matrix} \end{cases}\end{split}$$ 其中,$y_{ i }$ 是第 $i$ 个样本的真实值,$y_{ i }^{ \prime }$ 是第 $i$ 个样本的预测值,$n$ 是样本数量。

  • 平均绝对百分比误差 (MAPE)
    公式: $$L={ \left| \frac { y_{ i }-y_{ i }^{ \prime } }{ y_{ i } } \right| }$$
    导函数:
    $$L=\frac { \operatorname{sign}(y_{ i } - y_{ i }^{ \prime }) }{ y_{ i } }$$
    其中,$y_{ i }$ 是第 $i$ 个样本的真实值,$y_{ i }^{ \prime }$ 是第 $i$ 个样本的预测值,$n$ 是样本数量。

  • 均方对数误差 (MSLE)
    公式: $$L={ { \left( \log { (1+y_{ i }^{ \prime }) } -\log { (1+y_{ i }) } \right) }^{ 2 } }$$
    导函数:
    $$L=2\left( \log { (1+y_{ i }^{ \prime }) } -\log { (1+y_{ i }) } \right) \cdot \frac { 1 }{ 1+y_{ i }^{ \prime } } $$

    其中,$y_{ i }$ 是第 $i$ 个样本的真实值,$y_{ i }^{ \prime }$ 是第 $i$ 个样本的预测值,$n$ 是样本数量。
    注意!不能有任何一项为-1!

以下函数适合分类问题:

  • 二元交叉熵损失函数(CE)
    公式:
    $$L=-{ y_{ i }\log { (y_{ i }^{ \prime }) } +\left( 1-y_{ i } \right) \log { \left( 1-y_{ i }^{ \prime } \right) } }$$
    导函数:
    $$L=-\frac { y_{ i } }{ y_{ i }^{ \prime } } +\frac { 1-y_{ i } }{ 1-y_{ i }^{ \prime } } $$ 其中,$y_{ i }$ 是第 $i$ 个样本的真实值,$y_{ i }^{ \prime }$ 是第 $i 个样本的预测值,$n$ 是样本数量。

数据导入

一般而言,现有的数据集都是存储在文件中的,因此需要将文件中的数据转化为矩阵格式导入到程序中。
需要注意的是,导入数据不一定指明了输入和标签,且c++函数一次只能返回一个值,因此这个函数实际上是返回每一行指定的两个位置间的所有数据构成的一维矩阵构成的vector,应当设计相应的能指明开始读取位置和结束读取位置的变量。这也意味着,输入和标签要分别读取。
注:此处输入的位置序号从1开始。

** 记得加个抛出错误的机制 **

归一化

神经网络对于在-1至1之间的数据性能较好,因此可以对输入的数据进行处理,让数据更易于处理。也有的说法认为应当对每一层的输入进行归一化,但我认为没必要。另外,如果对预期输出也进行归一化,那么在实际预测时需要还原归一化后的数据为原始数据。
归一化的常用方法有:Min-Max归一化(离差归一化),平均值归一化,对数转换,反正切转换,Z-Score法。
实际上这是必需的:由于激活函数的限制,神经网络对于给定数据,尤其是目的输出的范围有一定的要求,如果输入数据过大,可能会导致激活函数的输出值接近1或0,从而使得梯度接近于0,导致神经网络无法正常训练。因此,需要对现有数据进行归一化处理,使得输入数据在合适的范围内。

一些可能的想法

  • 测试
    如果只对于单组数据,可以传入输入矩阵和预期输出矩阵,只执行前向传播和损失函数就行了。但如果对于多组数据,那么可以算一点其他数据,像是准确率之类的。

  • 结果可视化 虽说如果有三个以上的输入数据的话,神经网络从本质上来说就是在拟合一个高维空间中的函数,但如果只有一个或者两个输入数据的话,那么神经网络实际上就是在拟合一个二维平面上的函数,还是可以绘制出拟合的函数图像的。