随机梯度下降未能在我的神经网络实现中收敛

Posted

技术标签:

【中文标题】随机梯度下降未能在我的神经网络实现中收敛【英文标题】:Stochastic Gradient Descent failed to converge in my neural network implementation 【发布时间】:2017-02-19 00:28:15 【问题描述】:

我一直在尝试使用具有平方和误差的随机梯度下降作为成本函数,以使用能够表示此训练数据的前馈反向传播算法构建神经网络:

                 Input      Output
                0,1  , 1,0,0,0,0,0,0,0
                0.1,1, 0,1,0,0,0,0,0,0
                0.2,1, 0,0,1,0,0,0,0,0
                0.3,1, 0,0,0,1,0,0,0,0
                0.4,1, 0,0,0,0,1,0,0,0
                0.5,1, 0,0,0,0,0,1,0,0
                0.6,1, 0,0,0,0,0,0,1,0
                0.7,1, 0,0,0,0,0,0,0,1

其中它包含 1 个输入单元、1 个偏置单元、8 个输出单元和总共 16 个权重(总共 8 个输入权重和 8 个偏置权重。每个 2 个权重(1 个来自输入,1 个来自偏置)总共 16 个是指各自的单个输出单元)。 但是,该集合的收敛速度非常慢。 我正在为所有输出单元使用 sigmoid 激活函数:

output = 1/(1+e^(-weightedSum))

我导出的误差梯度是:

 errorGradient = learningRate*(output-trainingData) * output * (1-output)*inputUnit;

其中trainingData变量是指训练集中在当前输出单元索引处指定的目标输出,inputUnit是指连接到当前权重的输入单元。 因此,我在每次迭代时使用以下等式更新每个单独的权重:

weights of i = weights of i - (learningRate * errorGradient)

代码:

package ann;


import java.util.Arrays;
import java.util.Random;

public class MSEANN 

static double learningRate= 0.1;
static double totalError=0;
static double previousTotalError=Double.POSITIVE_INFINITY;
static double[] weights;

public static void main(String[] args) 

    genRanWeights();

    double [][][] trainingData = 
            0,1, 1,0,0,0,0,0,0,0,
            0.1,1, 0,1,0,0,0,0,0,0,
            0.2,1, 0,0,1,0,0,0,0,0,
            0.3,1, 0,0,0,1,0,0,0,0,
            0.4,1, 0,0,0,0,1,0,0,0,
            0.5,1, 0,0,0,0,0,1,0,0,
            0.6,1, 0,0,0,0,0,0,1,0,
            0.7,1, 0,0,0,0,0,0,0,1,
    ;


 while(true)

     int errorCount = 0;
     totalError=0;

     //Iterate through training set
     for(int i=0; i < trainingData.length; i++)
         //Iterate through a list of output unit
         for (int out=0 ; out < trainingData[i][1].length ; out++) 
             double weightedSum = 0;

             //Calculate weighted sum for this specific training set and this specific output unit
             for(int ii=0; ii < trainingData[i][0].length; ii++) 
                 weightedSum += trainingData[i][0][ii] * weights[out*(2)+ii];
             

             //Calculate output
             double output = 1/(1+Math.exp(-weightedSum));

             double error = Math.pow(trainingData[i][1][out] - output,2)/2;

             totalError+=error;
             if(error >=0.001)
                 errorCount++;
             



             //Iterate through a the training set to update weights
             for(int iii = out*2; iii < (out+1)*2; iii++) 
                 double firstGrad= -( trainingData[i][1][out] - output  ) * output*(1-output);
                 weights[iii] -= learningRate * firstGrad * trainingData[i][0][iii % 2];
             

         

     


     //Total Error accumulated
     System.out.println(totalError);

     //If error is getting worse every iteration, terminate the program.
     if (totalError-previousTotalError>=0)
          System.out.println("FAIL TO CONVERGE");
          System.exit(0);
     
     previousTotalError=totalError;

     if(errorCount == 0)
         System.out.println("Final weights: " + Arrays.toString(weights));
         System.exit(0);

     

 



//Generate random weights
static void genRanWeights() 
    Random r = new Random();
    double low  = -1/(Math.sqrt(2));
    double high = 1/(Math.sqrt(2));
    double[] result = new double[16];
    for(int i=0;i<result.length;i++)  
        result[i] = low + (high-low)*r.nextDouble();
    
    System.out.println(Arrays.toString(result));

     weights = result;


 

在上面的代码中,我通过打印运行程序时累积的总错误来通过 ANN 进行调试,并且在每次迭代中显示错误在每次迭代中都在减少,但是速度非常慢。 我已经调整了我的学习率,但并没有太多。 此外,我尝试将训练集简化为以下内容:

         Input      Output
        0  ,1, 1,0,0,0,0,0,0,0,
        0.1,1, 0,1,0,0,0,0,0,0,
//      0.2,1, 0,0,1,0,0,0,0,0,

并且网络训练非常迅速/即时,并且能够重现目标结果。但是,如果取消注释第 3 行,训练会非常缓慢,并且在程序运行期间根本不会收敛,即使我注意到错误的总和正在减少。 所以根据我上面的实验,我发现的模式是,如果我使用 3 个训练集,需要很长时间,我什至没有注意到 ANN 完成了训练。如果我使用少于 2 或正好 2,网络能够立即产生正确的输出。

所以我的问题是,我观察到的这种“异常”是由于选择了错误的激活函数,还是由于选择了学习率,或者仅仅是错误的实现? 将来,您建议我应该采取哪些步骤来有效地针对此类问题进行调试?

【问题讨论】:

想想响应函数的样子,中间的函数需要一个上升和下降的侧面。您的网络在中间层可能太小而无法意识到这一点。 【参考方案1】:

您的实现似乎是正确的,问题与学习率的选择无关。

问题来自于单层感知器(没有隐藏层)的局限性,它不能解决非线性可分问题,例如 XOR 二元运算,除非我们使用特殊的激活函数使其适用于 XOR ,但我不知道特殊的激活功能是否可以解决您的问题。要解决您的问题,您可能必须选择另一种神经网络布局,例如多层感知器。

你给单层感知器的问题在二维表面上不是线性可分的。当输入只取 2 个不同的值时,可以用一行分隔输出。但是对于输入和您想要的输出有 3 个或更多不同的值,一些输出需要两行才能与其他值分开。

例如,您的网络的第二个输出神经元的 2D 图,以及输入的 3 个可能值,就像您的测试中一样:

    ^
    |
    |      line 1      
    |        |   line 2
    |        |     |
    |        |     |
0.0 -     0  |  1  |  0    
    |        |     |
    |
    +-----|-----|-----|-----------> input values
         0.0   0.1   0.2            

要将1 与两个0s 分开,它需要两行而不是一行。所以第二个神经元将无法产生所需的输出。

由于偏差始终具有相同的值,因此它不会影响问题并且不会出现在图表上。

如果您将目标输出更改为线性可分问题,则单层感知器将起作用:

0.0, 1, 1,0,0,0,0,0,0,0,
0.1, 1, 1,1,0,0,0,0,0,0,
0.2, 1, 1,1,1,0,0,0,0,0,
0.3, 1, 1,1,1,1,0,0,0,0,
0.4, 1, 1,1,1,1,1,0,0,0,
0.5, 1, 1,1,1,1,1,1,0,0,
0.6, 1, 1,1,1,1,1,1,1,0,
0.7, 1, 1,1,1,1,1,1,1,1,

在某些情况下,可以引入根据真实输入计算的任意输入。例如,真实输入可能有 4 个值:

-1.0, 0.0, 1, 1,0,0,0,0,0,0,0,
-1.0, 0.1, 1, 0,1,0,0,0,0,0,0,
 1.0, 0.2, 1, 0,0,1,0,0,0,0,0,
 1.0, 0.3, 1, 0,0,0,1,0,0,0,0,

如果对于每个输出神经元,您在 X 轴上使用真实输入和 Y 轴上的任意输入绘制图形,您将看到,对于代表输出的 4 个点,1 可以与0s 只有一行。

要处理真实输入的 8 个可能值,您可以添加第二个任意输入,并获得 3D 图形。在没有第二个任意输入的情况下处理 8 个可能值的另一种方法是将点放在一个圆上。例如:

double [][][] trainingData = 
  0.0, 0.0, 1, 1,0,0,0,0,0,0,0,
  0.0, 0.1, 1, 0,1,0,0,0,0,0,0,
  0.0, 0.2, 1, 0,0,1,0,0,0,0,0,
  0.0, 0.3, 1, 0,0,0,1,0,0,0,0,
  0.0, 0.4, 1, 0,0,0,0,1,0,0,0,
  0.0, 0.5, 1, 0,0,0,0,0,1,0,0,
  0.0, 0.6, 1, 0,0,0,0,0,0,1,0,
  0.0, 0.7, 1, 0,0,0,0,0,0,0,1,
;

for(int i=0; i<8;i++) 
  // multiply the true inputs by 8 before the sin/cos in order
  // to increase the distance between points, and multiply the
  // resulting sin/cos by 2 for the same reason
  trainingData[i][0][0] = 2.0*Math.cos(trainingData[i][0][1]*8.0);
  trainingData[i][0][1] = 2.0*Math.sin(trainingData[i][0][1]*8.0);

如果您不想或无法添加任意输入或修改目标输出,则必须选择其他神经网络布局,例如多层感知器。但也许一个特殊的激活函数可以用单层感知器解决你的问题。我尝试使用高斯,但它不起作用,可能是由于参数错误。

在未来,您建议我应该采取哪些步骤来有效地调试此类问题?

考虑您选择的布局的局限性并尝试其他布局。如果您选择多层感知器,请考虑更改隐藏层的数量以及这些层中的神经元数量。

有时可以对网络的输入和输出进行标准化,在某些情况下,它可以极大地提高性能,就像我对您的训练数据所做的测试一样。但我认为在某些情况下,无论训练网络需要多少时间,训练网络都具有真实输入会更好。

我已经使用多层感知器测试了您的训练数据,该感知器具有一个由 15 个神经元组成的隐藏层,并且对于输出神经元没有 sigmoid 函数。在以0.1 的学习率进行大约 100 000 个训练周期后,我的网络收敛并停止在所需的错误处。

如果我通过以下方式修改输入:

0   -> 0
0.1 -> 1
0.2 -> 2
0.3 -> 3
0.4 -> 4
0.5 -> 5
0.6 -> 6
0.7 -> 7

然后,我的网络收敛速度更快。如果我将值转换为 [-7,7] 范围,速度会更快:

0   -> -7
0.1 -> -5
0.2 -> -3
0.3 -> -1
0.4 ->  1
0.5 ->  3
0.6 ->  5
0.7 ->  7

如果我修改目标输出会快一点,将0s 替换为-1

-7,1,  1,-1,-1,-1,-1,-1,-1,-1,
-5,1, -1, 1,-1,-1,-1,-1,-1,-1,
-3,1, -1,-1, 1,-1,-1,-1,-1,-1,
-1,1, -1,-1,-1, 1,-1,-1,-1,-1,
 1,1, -1,-1,-1,-1, 1,-1,-1,-1,
 3,1, -1,-1,-1,-1,-1, 1,-1,-1,
 5,1, -1,-1,-1,-1,-1,-1, 1,-1,
 7,1, -1,-1,-1,-1,-1,-1,-1, 1,

通过输入和输出的这种标准化,我在大约 2000 个训练周期后得到了所需的错误,而没有标准化则为 100 000 个。

另一个例子是您使用训练数据的前 2 行实现,就像您的问题一样:

         Input      Output
        0  ,1, 1,0,0,0,0,0,0,0,
        0.1,1, 0,1,0,0,0,0,0,0,
//      0.2,1, 0,0,1,0,0,0,0,0,

大约需要 600 000 个训练周期才能获得所需的错误。但如果我使用这些训练数据:

 Input      Output
0  ,1, 1,0,0,0,0,0,0,0,
1  ,1, 0,1,0,0,0,0,0,0,

使用 1 而不是输入 0.1,它只需要 9000 个训练周期。而且,如果我用10代替0.1-10代替0,只需要1500个训练周期。

但是,与我的多层感知器不同,将目标输出中的 0s 替换为 -1 会破坏性能。

【讨论】:

以上是关于随机梯度下降未能在我的神经网络实现中收敛的主要内容,如果未能解决你的问题,请参考以下文章

为啥随机梯度下降方法能够收敛

通过实例详解随机梯度与梯度下降

随机梯度下降收敛(Stochastic gradient descent convergence)

随机梯度下降法实例

用随机梯度下降法(SGD)做线性拟合

梯度下降的直觉