65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.48/5 (17投票s)

2009年8月12日

CPOL

8分钟阅读

viewsIcon

47753

downloadIcon

1391

关于加快神经网络学习速度的文章。

引言

CodeProject 上有许多关于神经网络概念和实现的可用文章。但当我试图找出如何实现 Nguyen-Widrow 初始化算法时,却找不到。于是我搜索了互联网,阅读了一些科学论文和书籍,并最终尝试将我学到的知识实现为一个适用的 C++ 算法。对我们学生来说,课堂所学与如何在实际应用中实现它们之间存在很大的差距。通过将我学会的所有知识整合到一个 C++ 类 (CNeuralNetwork) 中并分享,我希望我能帮助到遇到同样问题的人。这里主要的神经网络代码基于 Daniel Admassu 的工作。我在此类中实现的功能有:

  • 权重初始化算法(一些常用方法和 Nguyen-Widrow 方法)
  • 动量学习
  • 自适应学习

这三个概念将使我们创建的神经网络能够更快地学习(迭代次数更少)。虽然这些只是小改进,但我认为在这里分享它们是个好主意。

背景

您可能需要对神经网络理论有基本了解。由于我使用了反向传播方法(简单的一种),我相信您可以找到很多关于它的教程。

概念

前馈

在这里,我们使用多层感知器(MLP)神经网络架构。MLP 由几个层组成,通过加权连接相互连接。MLP 至少有三层:输入层、隐藏层和输出层。我们可以有多个隐藏层。在每个神经元中,我们分配一个激活函数,该函数将被加权输入信号触发。其思想是:我们希望找到所有权重的适当值,以便我们给定的一组输入能够产生我们期望的一组输出。

MLP architecture

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

Sigmoid function

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

Slope on sigmoid function

反向传播

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

Back-propagation 1

其中

Back propagation 2

除了梯度下降法,还有几种其他方法可以保证更快的学习速度。它们是共轭梯度法、拟牛顿法、Levenberg-Marquardt 法等。但对我来说,这些方法太复杂了。因此,与其使用这些方法,不如通过添加动量项或使用自适应学习率来大大加快学习过程。

添加动量项

在动量学习中,时间 (t+1) 的权重更新包含前一次学习的动量。因此,我们需要保留误差和输出的先前值。

Momentum equation

上面的方程可以按以下方式实现。变量 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;
            }
        }
    }
}

自适应学习

对于自适应学习,其思想是根据当前误差和先前误差自动更改学习率。有很多方法可以实现这个想法。这是我能找到的最简单的一种。

Adaptive learning rate

其思想是观察最后的两个误差,并使学习率朝着能减小第二个误差的方向调整。变量 EEi 分别是当前和先前的误差。参数 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 来调用这种方法。

Adaptive learning rate

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

Adaptive learning rate

如上所述的算法,首先,我们为所有隐藏节点分配 -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.hNeural 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 轮内达到最小均方误差目标。

    Adaptive learning rate

  • 学习率变化因子 = 0;动量 = 0;权重初始化方法 = RANDOM。

    未在 500 轮内达到最小均方误差目标。

    Adaptive learning rate

  • 学习率变化因子 = 0;动量 = 0;权重初始化方法 = NGUYEN。

    在 262 轮内达到最小均方误差目标。

    Adaptive learning rate

  • 学习率变化因子 = 0;动量 = 0.5;权重初始化方法 = NGUYEN。

    在 172 轮内达到最小均方误差目标。

    Adaptive learning rate

  • 学习率变化因子 = 0.5;动量 = 0;权重初始化方法 = NGUYEN。

    在 172 轮内达到最小均方误差目标。

    Adaptive learning rate

关注点

所有代码都实现为单个类:CNeuralNetwork。这样,我希望它足够简单易懂,尤其是对于寻求更多 C++ 神经网络实现信息的学生。对于未来的工作,我仍然打算学习更多,并将我在这里学到的知识付诸实践,期望它对他人有所帮助。供您参考,我还包含了一个来自 UCI 数据库的额外训练文件。您可以使用此文件来测试您的神经网络。由于此类使用 C++ 的基本功能,它也可以在 Linux 上很好地运行。

参考文献

历史

  • 2009年8月9日:初始版本
© . All rights reserved.