CNeuralNetwork:让您的神经网络学习更快。






4.48/5 (17投票s)
关于加快神经网络学习速度的文章。
引言
CodeProject 上有许多关于神经网络概念和实现的可用文章。但当我试图找出如何实现 Nguyen-Widrow 初始化算法时,却找不到。于是我搜索了互联网,阅读了一些科学论文和书籍,并最终尝试将我学到的知识实现为一个适用的 C++ 算法。对我们学生来说,课堂所学与如何在实际应用中实现它们之间存在很大的差距。通过将我学会的所有知识整合到一个 C++ 类 (CNeuralNetwork
) 中并分享,我希望我能帮助到遇到同样问题的人。这里主要的神经网络代码基于 Daniel Admassu 的工作。我在此类中实现的功能有:
- 权重初始化算法(一些常用方法和 Nguyen-Widrow 方法)
- 动量学习
- 自适应学习
这三个概念将使我们创建的神经网络能够更快地学习(迭代次数更少)。虽然这些只是小改进,但我认为在这里分享它们是个好主意。
背景
您可能需要对神经网络理论有基本了解。由于我使用了反向传播方法(简单的一种),我相信您可以找到很多关于它的教程。
概念
前馈
在这里,我们使用多层感知器(MLP)神经网络架构。MLP 由几个层组成,通过加权连接相互连接。MLP 至少有三层:输入层、隐藏层和输出层。我们可以有多个隐藏层。在每个神经元中,我们分配一个激活函数,该函数将被加权输入信号触发。其思想是:我们希望找到所有权重的适当值,以便我们给定的一组输入能够产生我们期望的一组输出。

在这里,对于 CNeuralNetwork
,我在隐藏层和输出层使用了双极 Logistic 函数作为激活函数。而在输入层,我使用了单位函数。选择合适的激活函数也有助于加快学习速度。理论上,饱和速度较慢的 Sigmoid 函数会产生更好的结果。

在 CNeuralNetwork
中,我只提供了双极 Logistic 函数。但您可以调整其斜率 (s),并观察它如何影响学习速度。较大的斜率会使权重值更快地饱和(收敛更快),而较小的斜率会使权重值移动较慢,但允许更精细的权重调整。

反向传播
在前馈过程中,网络将根据给定的输入计算输出。然后,它会将计算出的输出与期望输出进行比较,以计算误差。接下来的任务是最小化这个误差。我们选择的最小化误差的方法也将决定学习速度。梯度下降法是最常用的最小化此误差的方法。最后,它将按照以下方式更新权重值:

其中

除了梯度下降法,还有几种其他方法可以保证更快的学习速度。它们是共轭梯度法、拟牛顿法、Levenberg-Marquardt 法等。但对我来说,这些方法太复杂了。因此,与其使用这些方法,不如通过添加动量项或使用自适应学习率来大大加快学习过程。
添加动量项
在动量学习中,时间 (t+1) 的权重更新包含前一次学习的动量。因此,我们需要保留误差和输出的先前值。

上面的方程可以按以下方式实现。变量 a 是动量值。该值应大于零且小于一。
void CNeuralNetwork::calculate_weights()
{
for(unsigned int i=1;i<m_layer_num;i++){
for(unsigned int j=0;j<m_neuron_num[i];j++){
for(unsigned int k=0;k<m_neuron_num[i-1];k++){
float delta = m_learning_rate * m_error[i][j] * m_node_output[i-1][k];
float delta_prev = m_learning_rate * m_error_prev[i][j]
* m_node_output_prev[i-1][k];
m_weight[i][j][k] = (float) m_weight[i][j][k] + delta +
m_momentum * delta_prev;
}
}
}
}
自适应学习
对于自适应学习,其思想是根据当前误差和先前误差自动更改学习率。有很多方法可以实现这个想法。这是我能找到的最简单的一种。

其思想是观察最后的两个误差,并使学习率朝着能减小第二个误差的方向调整。变量 E
和 Ei
分别是当前和先前的误差。参数 A
是一个决定学习率调整速度的参数。参数 A
应小于一且大于零。您也可以尝试另一种方法:如果当前误差小于先前误差,则将当前学习率乘以大于一的因子;如果当前误差大于先前误差,则将其乘以小于一的因子。在 Martin Hagan 的书中,也建议如果误差增加,则放弃更改。这将带来更好的结果。您可以在函数 ann_train_network_from_file
中找到自适应学习例程,其中每轮训练都会执行一次学习率更新。
int CNeuralNetwork::ann_train_network_from_file
(char *file_name, int max_epoch, float max_error, int parsing_direction)
{
int epoch = 0;
string line;
ifstream file (file_name);
m_average_error = 0.0F;
if (file.is_open()){
for (epoch = 0; epoch <= max_epoch; epoch++){
int training_data_num = 0;
float error = 0.0F;
while (! file.eof() ){
getline(file, line);
if (line.empty()) break;
parse_data(line, parsing_direction);
calculate_outputs();
calculate_errors();
calculate_weights();
error = error + get_average_error();
training_data_num ++;
}
file.clear(); // clear buffer
file.seekg(0, ios::beg); // go to beginning of file
float error_prev = m_average_error;
m_average_error = error/training_data_num;
if (m_average_error <= max_error)
break;
// update learning rate
m_learning_rate = m_learning_rate*
(m_lr_factor*m_average_error*error_prev + 1);
}
}
file.close();
return epoch; // returns number of required epochs
}
权重初始化算法
从我阅读的几篇论文中得知,特定的初始化值会影响收敛速度。有几种方法可用于此目的。最常见的方法是在某个小数范围内均匀分布地随机初始化权重。在 CNeuralnetwork
中,我将这种方法称为 HARD_RANDOM
,因为我找不到这种方法的现有名称。另一种更好的方法是按下面方程所示进行界定。在 CNeuralNetwork
中,我用 RANDOM
来调用这种方法。

广为人知的非常好的权重初始化方法是 Nguyen-Widrow 方法。在 CNeuralNetwork
中,我将其称为 NGUYEN
。Nguyen-Widrow 权重初始化算法可以按以下步骤表示:

如上所述的算法,首先,我们为所有隐藏节点分配 -1 到 1 之间的随机数。接下来,我们通过调用函数 get_norm_of_weight
来计算我们生成的这些随机数的范数。现在我们拥有了所有必要的数据,可以继续进行可用公式了。所有权重初始化例程都位于函数 initialize_weights
中。
void CNeuralNetwork::initialize_weights()
{
// METHOD 1
if (m_method == HARD_RANDOM){
for(unsigned int i=1;i<m_layer_num;i++)
for(unsigned int j=0;j<m_neuron_num[i];j++)
for(unsigned int k=0;k<m_neuron_num[i-1];k+
m_weight[i][j][k]=rand_float_range(-m_init_val, m_init_val);
}
// METHOD 2
else if (m_method == RANDOM){
float range = sqrt(m_learning_rate / m_neuron_num[0]);
for(unsigned int i=1;i<m_layer_num;i++)
for(unsigned int j=0;j<m_neuron_num[i];j++)
for(unsigned int k=0;k<m_neuron_num[i-1];k++)
m_weight[i][j][k]=rand_float_range(-range, range);
}
// METHOD 3
else if (m_method == NGUYEN){
for(unsigned int i=1;i<m_layer_num;i++)
for(unsigned int j=0;j<m_neuron_num[i];j++)
for(unsigned int k=0;k<m_neuron_num[i-1];k++)
m_weight[i][j][k]=rand_float_range(-1, 1);
for(unsigned int i=1;i<m_layer_num;i++){
float beta = 0.7 * pow((float) m_neuron_num[i], (float) 1/m_neuron_num[0]);
for(unsigned int j=0;j<m_neuron_num[i];j++){
for(unsigned int k=0;k<m_neuron_num[i-1];k++)
m_weight[i][j][k]=beta * m_weight[i][j][k] / get_norm_of_weight(i,j);
}
}
}
}
使用代码
公共方法
- 创建一个新的神经网络。
void ann_create_network(unsigned int input_num, unsigned int output_num, unsigned int hidden_layer_num, ...);
- 设置学习率值。
void ann_set_learning_rate(float learning_rate = 0);
- 设置动量值。
void ann_set_momentum(float momentum = 0);
- 为自适应学习功能设置学习率变化因子。
void ann_set_lr_changing_factor(float lr_factor = 0);
- 为 Logistic Sigmoid 激活函数设置斜率值。
void ann_set_slope_value(float slope_value = 1);
- 设置期望的权重初始化方法。
void ann_set_weight_init_method(int method = NGUYEN , float range = 0);
- 设置输入层每神经元的当前输入。
void ann_set_input_per_channel(unsigned int input_channel, float input);
- 获取训练完成后一轮训练的最后平均误差。
float ann_get_average_error();
- 获取执行模拟后的输出。
float ann_get_output(unsigned int channel);
- 获取完成训练所需的轮数。
float ann_get_epoch_num();
- 使用文本文件中的训练集训练神经网络。文本文件可以是以逗号分隔或以空格分隔的文件。如果文本文件中的输入在前,则将
parsing_direction
设置为INPUT_FIRST
。如果输出在前,则将其设置为OUTPUT_FIRST
。训练结果,如权重值、所需的轮数、一轮的最终平均 MSE 等,将被记录到文件 result.log 中。int ann_train_network_from_file(char *file_name, int max_epoch, float max_error, int parsing_direction);
- 使用参数
file_name
指定的文本文件中的测试集测试训练好的神经网络。结果将记录到参数log_file
指定的另一个文件中。void ann_test_network_from_file(char *file_name, char *log_file);
- 基于当前输入模拟神经网络。
void ann_simulate();
- 删除所有先前动态创建的动态变量,避免内存泄漏。
void ann_clear();
以下是使用 CNeuralNetwork
的示例。我将此类放在文件 Neural Network.h 和 Neural Network.cpp 中。如果您想使用此类,只需将这两个文件包含在您的项目中。
// main.cpp
#include "stdafx.h"
#include "Neural Network.h"
int main()
{
float *result;
CNeuralNetwork nn;
nn.ann_set_learning_rate(0.5);
nn.ann_set_momentum(0);
nn.ann_set_lr_changing_factor(0);
nn.ann_set_slope_value(1);
nn.ann_set_weight_init_method(nn.NGUYEN);
nn.ann_create_network(2,1,1,3);
int epoch = nn.ann_train_network_from_file("input.txt", 500, 0.01, nn.OUTPUT_FIRST);
printf("number of epoch: %i with final error:
%f\n",epoch, nn.ann_get_average_error());
//Test: 1 XOR 1
nn.ann_set_input_per_channel(0, 1.0F);
nn.ann_set_input_per_channel(1, 1.0F);
nn.ann_simulate();
printf("%f\n", nn.ann_get_output(0));
//Test: 0 XOR 0
nn.ann_set_input_per_channel(0, 0.0F);
nn.ann_set_input_per_channel(1, 0.0F);
nn.ann_simulate();
printf("%f\n", nn.ann_get_output(0));
//Test: 1 XOR 0
nn.ann_set_input_per_channel(0, 1.0F);
nn.ann_set_input_per_channel(1, 0.0F);
nn.ann_simulate();
printf("%f\n", nn.ann_get_output(0));
//Test: 0 XOR 1
nn.ann_set_input_per_channel(0, 0.0F);
nn.ann_set_input_per_channel(1, 1.0F);
nn.ann_simulate();
printf("%f\n", nn.ann_get_output(0));
nn.ann_clear();
}
实验
为了看到这些思想如何工作,我们将对经典的 XOR 问题进行一些实验。对于 XOR 问题,我们将创建一个包含 1 个隐藏层和 3 个神经元的神经网络。首先,我们将观察权重初始化问题在神经网络中的有效性。然后,我们将尝试激活动量学习和自适应学习功能,并观察学习过程如何加快速度。我们的目标是达到每轮训练的平均均方误差为 0.01。所有实验均在学习率为 0.5 且最大轮数为 500 轮的条件下进行。从实验中,我们可以看到现有方法如何将训练过程加速两倍以上。
- 学习率变化因子 = 0;动量 = 0;权重初始化方法 = HARD_RANDOM,范围为 -0.3 到 0.3。
未在 500 轮内达到最小均方误差目标。
- 学习率变化因子 = 0;动量 = 0;权重初始化方法 = RANDOM。
未在 500 轮内达到最小均方误差目标。
- 学习率变化因子 = 0;动量 = 0;权重初始化方法 = NGUYEN。
在 262 轮内达到最小均方误差目标。
- 学习率变化因子 = 0;动量 = 0.5;权重初始化方法 = NGUYEN。
在 172 轮内达到最小均方误差目标。
- 学习率变化因子 = 0.5;动量 = 0;权重初始化方法 = NGUYEN。
在 172 轮内达到最小均方误差目标。
关注点
所有代码都实现为单个类:CNeuralNetwork
。这样,我希望它足够简单易懂,尤其是对于寻求更多 C++ 神经网络实现信息的学生。对于未来的工作,我仍然打算学习更多,并将我在这里学到的知识付诸实践,期望它对他人有所帮助。供您参考,我还包含了一个来自 UCI 数据库的额外训练文件。您可以使用此文件来测试您的神经网络。由于此类使用 C++ 的基本功能,它也可以在 Linux 上很好地运行。
参考文献
- 反向传播算法,作者:Wen Yu
- Nguyen, D. and Widrow, B., "Improving The Learning Speed of 2-layer Neural Networks by Choosing Initial Values of The Adaptive Weights", IJCNN, USA, 1990
- Mercedes Fernández-Redondo, Carlos Hernández-Espinosa, "A Comparison among Weight Initialization Methods for Multilayer Feedforward Networks," IJCN, Italy, 2000
- Prasanth Kumar, Intelligent Control Lecture Note, School of Mechanical and Aerospace Engineering, Gyeongsang National University, Republic of Korea
历史
- 2009年8月9日:初始版本