前馈全连接神经网络和函数逼近 、 时间序列预测 、 手写数字识别
Andrew Kirillov 著
Conmajia 译
2019 年 1 月 12 日原文发表于 CodeProject
2018 年 9 月 28 日 ( . 中文版有小幅修改 ) 已获作者本人授权. , 本文介绍了如何使用 ANNT 神经网络库生成前馈全连接神经网络并应用到问题求解.
简介
最近这段时间
我对人工神经网络
科技是日新月异的
实际上现在到处都能找到各种优秀的程序库
本文是关于 ANNT 库的系列文章中的第一篇
理论背景
神经网络这个课题并不新鲜
来自生物的灵感
现代人工神经网络的许多想法都是受到生物学现象的启发而产生的. 神经元

一枚典型的神经元由细胞体
人工神经元
人工神经元是表示生物神经元的数学模型. 人工神经元接收一个或多个输入

用数学语言来描述
其中 xj 是神经元的输入
1943 年
在 20 世纪 80 年代后期
和 ( AND) 、 或 ( OR) 例子
前面提到了
b | ω1 | ω2 | |
OR | -0.5 | 1 | 1 |
AND | -1.5 | 1 | 1 |
NAND | 1.5 | -1 | -1 |
假设神经元使用阈值激活函数
x1 | x2 | uOR | yOR | uAND | yAND | uNAND | yNAND | |||
0 | 0 | -0.5 | 0 | -1.5 | 0 | 1.5 | 1 | |||
1 | 0 | 0.5 | 1 | -0.5 | 0 | 0.5 | 1 | |||
0 | 1 | 0.5 | 1 | -0.5 | 0 | 0.5 | 1 | |||
1 | 1 | 1.5 | 1 | 0.5 | 1 | -0.5 | 0 |
那么

分隔线可以从权重和偏差值得到. 对于 OR 函数
单个神经元不行
这就意味着 3 个神经元加上 2 层网络即可完成.
人工神经网络
由于单个神经元无法完成太多的工作

从图 4 看到
为了研究前馈全连接网络的数学模型
- l 网络的层数
- n(k) 第 k 层神经元数量
- n(0) 网络输入数量
- m(k) 进入第 k 层的输入数量
m(k)=n(k−1)( ) - y(k) 第 k 层输出的列向量
长度 n(k), - y(0) 网络输入的列向量
向量 x( ) - b(k) 第 k 层偏差值的列向量
长度 n(k), - w(k) 第 k 层的权重矩阵. 矩阵的第 i 行包含层的第 i 个神经元的权重
n(k)×m(k),
对于上述所有定义
或者用向量表示为
全是数学
激活函数
开始研究学习算法之前
比较流行的激活函数之一是 sigmoid 函数
sigmoid 函数的形状类似于阶跃函数
函数图像是这样的
![]() |
![]() |
常用的激活函数有
- 双曲正切
如图 5-b, 形状类似于 sigmoid 函数, 但值域是 (−1,1), - SoftMax 函数
它将任意实值的向量压缩为实值的同一维向量, 其中每个条目都在 (0,1) 范围内, 所有条目和为 1. 这有利于处理分类任务. 在分类任务中, 神经网络的输出视为属于某个类的概率, 概率之和恒为 1, - rectifier[7]
整流器( 是深度神经网络结构中一种常用的激活函数) 它允许更好的梯度传播, 具有较少的梯度消失, gradient vanishing( 问题)
为什么需要激活函数
所以现在
训练人工神经网络
为了训练前馈全连接人工神经网络
成本函数
为了计算误差
对所有样本进行进行平均
现在
随机梯度下降算法
有了成本函数
当 λ 足够小
训练人工神经网络时
这里的 λ 参数称为学习率
研究权重更新和计算成本函数的梯度之前
因此
分析随机梯度下降的收敛性
小批量梯度下降
尽管批量梯度下降是目前大多数应用的首选
梯度和链式法则
前馈全连接神经网络最后一层的权重更新时
成本函数是网络输出和目标输出的函数
将式 (8) 代入上式
现在来找出式 (9) 里的每一个偏导数. 虽然假定的是平方均值误差函数
可见 MSE 成本函数对网络输出的偏导数是实际输出与目标输出的差
下一步是计算激活函数相对于其输入的导数. 输入的激活函数使用的是 sigmoid 函数
最后
综上
上述公式仅适用于单层前馈全连接人工神经网络的训练. 然而
误差反向传播
前文讲解了在输出层中计算成本函数的偏导数
这实际上就是式 (10). 接下来定义输出层前一层中第 j 神经元输出的成本函数偏导数 E′j. 这里再次使用了链式法则. 由于已经完全连接了人工神经网络
现在来做一些代换. 首先代入式 (10′)
式子里的 Ei 是刻意保留的. 如果运用链式法则计算某个隐藏层的误差项
综合上面各式
这个算法就叫做误差反向传播. 一旦计算出输出层的误差
如果不使用 MSE 或 sigmoid
好吧
ANNT 库
我在设计 ANNT 库代码时
虽然理论部分表明激活函数是神经元的一部分
ANNT 的类关系图如图 6

例如XMSECost
类只计算 y_i–t_i 部分. 跟着 XSigmoidActivation
类计算 y_i(1-y_i). 最后 XFullyConnectedLayer
计算权重的偏导数
梯度下降的计算也被移动到一个单独的类中. 如前所述
XNeuralNetwork
类表示实际的神经网络. 网络的体系结构取决于放入其中的层的类型. 本文只介绍了前馈完全连接的神经网络例子. 在下一篇文章中
最后XNetworkNeursion
用于计算网络输出XNetworkTraining
类提供了进行神经网络实际训练的基础. 注意
另一件需要注意的事情是config.hpp
文件.
编译源码
源码里附带 MSVC
使用范例
为了验证人工神经网络在前馈全连接人工神经网络的不同应用
函数逼近
演示的第一个例子是函数逼近


在直线样本集的情况下
// 准备两层全连接人工神经网络
shared_ptr<XNeuralNetwork> net = make_shared<XNeuralNetwork>( );
net->AddLayer( make_shared<XFullyConnectedLayer>( 1, 10 ) ); // 1 输入, 10 神经元
net->AddLayer( make_shared<XSigmoidActivation>( ) );
net->AddLayer( make_shared<XFullyConnectedLayer>( 10, 1 ) ); // 10 输入, 1 神经元
然后生成一个训练对象
// 用 Nesterov 优化器和 MSE 成本函数生成训练内容
XNetworkTraining netTraining( net,
make_shared<XNesterovMomentumOptimizer>( ),
make_shared<XMSECost>( ) );
最后
for ( size_t epoch = 1; epoch <= trainingParams.EpochsCount; epoch++ )
{
// 打乱顺序
for ( size_t i = 0; i < samplesCount / 2; i++ )
{
int swapIndex1 = rand( ) % samplesCount;
int swapIndex2 = rand( ) % samplesCount;
std::swap( ptrInputs[swapIndex1], ptrInputs[swapIndex2] );
std::swap( ptrTargetOutputs[swapIndex1], ptrTargetOutputs[swapIndex2] );
}
auto cost = netTraining.TrainEpoch( ptrInputs, ptrTargetOutputs, trainingParams.BatchSize );
}
训练完成后




时间序列预测
第二个例子演示了时间序列预测. 这里
下面是时间序列的例子

这个例子也可以作为函数逼近处理. 但并不是逼近 f(t)
例程第一件事是准备一个训练样本集. 要记住
一旦生成了训练集
// 准备 2 层人工神经网络,5 输入 1 输出 10 隐藏神经元
shared_ptr<XNeuralNetwork> net = make_shared<XNeuralNetwork>( );
net->AddLayer( make_shared<XFullyConnectedLayer>( 5, 10 ) );
net->AddLayer( make_shared<XTanhActivation>( ) );
net->AddLayer( make_shared<XFullyConnectedLayer>( 10, 1 ) );
// 用 Nesterov 优化器和 MSE 成本函数生成训练内容
XNetworkTraining netTraining( net,
make_shared<XNesterovMomentumOptimizer>( ),
make_shared<XMSECost>( ) );
for ( size_t epoch = 1; epoch <= trainingParams.EpochsCount; epoch++ )
{
// 打乱顺序
for ( size_t i = 0; i < samplesCount / 2; i++ )
{
int swapIndex1 = rand( ) % samplesCount;
int swapIndex2 = rand( ) % samplesCount;
std::swap( ptrInputs[swapIndex1], ptrInputs[swapIndex2] );
std::swap( ptrTargetOutputs[swapIndex1], ptrTargetOutputs[swapIndex2] );
}
auto cost = netTraining.TrainEpoch( ptrInputs, ptrTargetOutputs, trainingParams.BatchSize );
}
这个例程也会将结果输出到 csv 文件中



异或函数的二进制分类
这个例子相当于人工神经网络的“hello world”
// 准备 XOR 训练数据,输入编码为 -1、1,输出编码为 0、1
vector<fvector_t> inputs;
vector<fvector_t> targetOutputs;
inputs.push_back( { -1.0f, -1.0f } ); /* -> */ targetOutputs.push_back( { 0.0f } );
inputs.push_back( { 1.0f, -1.0f } ); /* -> */ targetOutputs.push_back( { 1.0f } );
inputs.push_back( { -1.0f, 1.0f } ); /* -> */ targetOutputs.push_back( { 1.0f } );
inputs.push_back( { 1.0f, 1.0f } ); /* -> */ targetOutputs.push_back( { 0.0f } );
// 准备 2 层人工神经网络
// 对 AND、OR 函数而言,单层就足够了,但是 XOR 需要两层,这点在前面讨论过
shared_ptr<XNeuralNetwork> net = make_shared<XNeuralNetwork>( );
net->AddLayer( make_shared<XFullyConnectedLayer>( 2, 2 ) );
net->AddLayer( make_shared<XTanhActivation>( ) );
net->AddLayer( make_shared<XFullyConnectedLayer>( 2, 1 ) );
net->AddLayer( make_shared<XSigmoidActivation>( ) );
// 用 Nesterov 优化器和二进制交叉熵成本函数生成训练内容
XNetworkTraining netTraining( net,
make_shared<XMomentumOptimizer>( 0.1f ),
make_shared<XBinaryCrossEntropyCost>( ) );
// 训练神经网络
printf( "每个样本的成本: \n" );
for ( size_t i = 0; i < 80 * 2; i++ )
{
size_t sample = rand( ) % inputs.size( );
auto cost = netTraining.TrainSample( inputs[sample], targetOutputs[sample] );
}
尽管简单
下面是例子的输出
全连接人工神经网络 XOR 分类例程
训练前的网络输出:
{ -1.00 -1.00 } -> { 0.54 }
{ 1.00 -1.00 } -> { 0.47 }
{ -1.00 1.00 } -> { 0.53 }
{ 1.00 1.00 } -> { 0.46 }
每个样本的成本:
0.6262 0.5716 0.4806 1.0270 0.8960 0.8489 0.7270 0.9774
...
0.0260 0.0164 0.0251 0.0161 0.0198 0.0199 0.0191 0.0152
训练后的网络输出:
{ -1.00 -1.00 } -> { 0.02 }
{ 1.00 -1.00 } -> { 0.98 }
{ -1.00 1.00 } -> { 0.98 }
{ 1.00 1.00 } -> { 0.01 }
鸢尾花多类分类
另一个例子是对鸢尾花进行分类
这个例子使用了一个特殊的助手类
// 准备 3 层人工神经网络
shared_ptr<XNeuralNetwork> net = make_shared<XNeuralNetwork>( );
net->AddLayer( make_shared<XFullyConnectedLayer>( 4, 10 ) );
net->AddLayer( make_shared<XTanhActivation>( ) );
net->AddLayer( make_shared<XFullyConnectedLayer>( 10, 10 ) );
net->AddLayer( make_shared<XTanhActivation>( ) );
net->AddLayer( make_shared<XFullyConnectedLayer>( 10, 3 ) );
net->AddLayer( make_shared<XSigmoidActivation>( ) );
// 用 Nesterov 优化器和交叉熵成本函数生成训练内容
shared_ptr<XNetworkTraining> netTraining = make_shared<XNetworkTraining>( net,
make_shared<XNesterovMomentumOptimizer>( 0.01f ),
make_shared<XCrossEntropyCost>( ) );
// 用助手类训练人工神经网络分类
XClassificationTrainingHelper trainingHelper( netTraining, argc, argv );
trainingHelper.SetTestSamples( testAttributes, encodedTestLabels, testLabels );
// 40 世代, 每批 10 样本
trainingHelper.RunTraining( 40, 10, trainAttributes, encodedTrainLabels, trainLabels );
助手类的好处在于
MNIST 手写数字分类
最后一个例子
// 准备 3 层人工神经网络
shared_ptr<XNeuralNetwork> net = make_shared<XNeuralNetwork>( );
net->AddLayer( make_shared<XFullyConnectedLayer>( trainImages[0].size( ), 300 ) );
net->AddLayer( make_shared<XTanhActivation>( ) );
net->AddLayer( make_shared<XFullyConnectedLayer>( 300, 100 ) );
net->AddLayer( make_shared<XTanhActivation>( ) );
net->AddLayer( make_shared<XFullyConnectedLayer>( 100, 10 ) );
net->AddLayer( make_shared<XSoftMaxActivation>( ) );
// 用 Nesterov 优化器和交叉熵成本函数生成训练内容
shared_ptr<XNetworkTraining> netTraining = make_shared<XNetworkTraining>( net,
make_shared<XAdamOptimizer>( 0.001f ),
make_shared<XCrossEntropyCost>( ) );
// 用助手类训练人工神经网络分类
XClassificationTrainingHelper trainingHelper( netTraining, argc, argv );
trainingHelper.SetValidationSamples( validationImages, encodedValidationLabels, validationLabels );
trainingHelper.SetTestSamples( testImages, encodedTestLabels, testLabels );
// 20 世代, 每批 50 样本
trainingHelper.RunTraining( 20, 50, trainImages, encodedTrainLabels, trainLabels );
在这个例子中
结论
这就是目前人工神经网络的前馈全连接及它在人工神经网络库中的实现. 正如前面提到的
到此为止
如果有人想关注 ANNT 库的进展
许可
本文以及任何相关的源代码和文件都是根据 GNU
关于作者
Andrew Kirillov
过去研究内容相对较少
学习可以集中在课题本身. 现在各种衍生变化让人眼花撩乱, 加之太多乌合之众也参与到“科技前沿”中来, 鱼目混珠, 难免令人困惑. ↩︎, 出自 Michael J. Garbade 博士 How to Create a Simple Neural Network in Python. ↩︎
Warren Sturgis McCulloch
1898-1969( 美国神经生理学家和控制论学家. ↩︎) , Walter Harry Pitts Jr.
1923-1969( 美国逻辑学家. ↩︎) , 最简单的阶跃函数也叫开关函数
表达式为, ↩︎: f(x)=x^+=max(0,x)
即输出为输入的正数部分. ↩︎, 我认为使用世代作为 epoch 的翻译更贴切. 相比之下
迭代, iteration( 这个词太肤浅) 没有体现出经由某一过程进化改变的精髓. 即便如此, 很多人还是用迭代用得乐此不疲, 也是无所谓, 开心就好. ↩︎, 这是一个典型的策略模式
strategy pattern( 设计. ↩︎) 一种高度优化的多线程并行编译处理方案. ↩︎
if(jQuery('#no-reward').text() == 'true') jQuery('.bottom-reward').addClass('hidden');
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· DeepSeek 解答了困扰我五年的技术问题。时代确实变了!
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 趁着过年的时候手搓了一个低代码框架
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!