首页 文章

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

提问于
浏览
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}}
                {{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;
}

}

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

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个训练集,那么我需要很长时间才能完成训练 . 如果我使用少于2或正好2,网络能够立即产生正确的输出 .

所以我的问题是,由于错误选择激活功能,或者由于学习率的选择,或者仅仅是错误的实现,我观察到这种“异常”吗?在将来,您建议我应该针对此类问题进行有效调试的步骤是什么?

1 回答

  • 2

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

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

    您给单层感知器的问题在2维表面上不能线性分离 . 当输入仅采用2个不同的值时,可以将输出与一行分开 . 但是,如果输入的输入和输出有3个或更多个不同的值,则某些输出需要两条线与其他值分开 .

    例如,网络中第二个输出神经元的2D图形,以及输入的3个可能值,如测试中所示:

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

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

    由于偏差总是具有相同的值,因此不会影响问题,也不会出现在图表上 .

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

    {{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 可以仅与 0 分开一条线 .

    要处理真实输入的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函数 . 我的网络在大约100 000个训练周期之后收敛并停止在所需的错误,学习率为 0.1 .

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

    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
    

    如果我修改目标输出,用 -1 替换 0 ,它会快一点:

    {{-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个训练周期 .

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

相关问题