用Java构建简单的神经网络

2020-12-20 21:19:53

在这篇文章中,我们将通过一些简单的步骤来解决人工智能问题,并尝试在Java中构建一个非常简单的神经网络。

神经网络是大脑工作方式的软件表示。不幸的是,我们还不知道大脑到底是如何工作的,但是我们确实知道该过程背后的生物学原理:人脑由1000亿个称为神经元的细胞组成,它们通过突触连接在一起。如果有足够的突触与神经元相连,那么该神经元也会触发。此过程称为“思考”。

因此,我们使用一个非常简单的示例尝试对上述过程进行建模,该示例具有3个输入(突触)并产生单个输出(1个神经元触发)。

我们将训练我们上面的神经网络来解决以下问题。您能弄清楚模式并猜出新输入的值是多少? 0还是1?

因此,现在我们有了人脑的模型,我们将尝试获取神经网络,以了解训练模式提供的模式。我们将首先为每个输入分配一个随机数以产生输出。

计算输出的公式如下:$$ \ sum weight_i。 input_i = weight1。输入1 +权重2。输入2 +重量3。输入3 $$

事实证明,我们希望将此输出值标准化为0到1之间的某个值,以便进行预测。归一化后,我们将输出与输入的预期输出进行比较。这给了我们错误,或者是我们的预测还差得远。然后,我们可以使用此错误来稍微调整神经网络的权重,并在相同的输入上再次尝试运气。下图可以对此进行总结:

我们对所有输入重复此训练过程10,000次,以达到训练有素的神经网络。然后,我们可以使用该神经网络对新输入进行预测!

但是,在进入实现之前,我们仍然需要阐明我们如何基于误差(也称为反向传播)实现归一化和权重调整。

在受到生物学启发的神经网络中,神经元的输出通常是代表细胞中动作电位激发速率的抽象。最简单的形式是二进制值,即神经元是否在发射。因此,需要对该输出值进行标准化。

为了实现这种标准化,我们将所谓的激活函数应用于神经元的输出。如果我们以一个非常简单的Heaviside阶跃函数为例,该函数将0分配给任何负值,将1分配给任何正值,那么将需要大量的神经元来实现缓慢调整权重以达到所需的粒度。可接受的培训集共识。

正如我们将在下一部分有关反向传播的内容中看到的那样,这种缓慢调整权重的概念可以用数学公式表示为激活函数的斜率。用生物学的术语来说,可以认为是随着输入电流的增加,点火速度增加。如果使用线性函数而不是Heaviside函数,则会发现生成的网络将具有不稳定的收敛性,因为沿线性路径的神经元输入将趋于无限制地增加,因为线性函数不可归一化。

上面提到的所有问题都可以通过使用可标准化的S型激活函数来解决。一个现实的模型一直保持为零,直到接收到输入电流为止,这时点火频率开始时迅速增加,但以100%的点火速率逐渐接近渐近线。在数学上,这看起来像:$$ \ frac {1} {1 + e ^ {-x}} $$

因此,神经元输出的最终公式现在变为$$输出= \ frac {1} {1 + e ^ {-(\ sum weight_i。input_i)}} $ $$

我们还可以使用其他归一化函数,但乙状结肠的优点是相当简单,并且具有简单的导数,这在我们查看下面的反向传播时将非常有用。

在训练周期中,我们根据错误调整了权重。为此,我们可以使用"误差加权导数"公式$$调整=误差。输入。 SigmoidCurveGradient(输出)$$

我们使用此公式的原因是,首先,我们要根据误差的大小进行调整。其次,我们将输入乘以0或1。如果输入为0,则不会调整权重。最后,我们乘以Sigmoid曲线(或导数)的梯度。

我们使用梯度的原因是因为我们试图将损失降到最低。具体而言,我们通过梯度下降法进行此操作。从根本上讲,这意味着从参数空间中的当前点(由当前权重的完整集合确定),我们希望朝着减少损失函数的方向前进。可视化地站在山坡上并沿着坡度最陡的方向走。应用于神经网络的梯度下降方法如下所示:

如果神经元的输出是一个较大的正数或负数,则表明该神经元以一种或另一种方式非常有信心。

从S形图中可以看出,S形曲线的斜率较大。

因此,如果神经元确信现有权重是正确的,那么它就不需要调整太多,而乘以S型曲线的斜率就可以实现。

Sigmoid函数的导数由以下公式$$ SigmoidCurveGradient(output)= output给出。 (1输出)$$将其代入调整公式可得出$$ Adjustment = error。输入。输出。 (1-输出)$$

在解释以上数学时,遗漏的重要但微妙的一点是,对于每次训练迭代,数学运算都在整个训练集上同时完成。因此,我们将利用矩阵来存储输入向量,权重和预期输出的集合。

您可以在此处获取整个项目源:https://github.com/wheresvic/neuralnet。为了学习,我们仅使用标准的Java Math函数自己实现了所有数学运算:)

我们将从NeuronLayer类开始,该类只是神经网络实现中权重的占位符。我们为它提供每个神经元的输入数量以及可用于构建权重表的神经元数量。在我们当前的示例中,这只是具有3个输入神经元的最后一个输出神经元。

公共类NeuronLayer {public final Function activationFunction,activationFunctionDerivative;双[] []权重;公共NeuronLayer(int numberOfNeurons,int numberOfInputsPerNeuron){权重=新double [numberOfInputsPerNeuron] [numberOfNeurons]; for(int i = 0; i< numberOfInputsPerNeuron; ++ i){for(int j = 0; j< numberOfNeurons; ++ j){weights [i] [j] =(2 * Math.random() )-1; //将范围从0-1转换为-1到-1}} activationFunction = NNMath :: sigmoid; activationFunctionDerivative = NNMath :: sigmoidDerivative; }公共无效AdjustWeights(double [] []调整){this.weights = NNMath.matrixAdd(weights,Adjustment); }}

我们的神经网络类是所有动作发生的地方。它以NeuronLayer作为构造函数,并具有2个主要功能:

训练:运行训练循环numberOfTrainingIterations次(通常是10,000之类的高数字)。请注意,训练本身涉及计算输出,然后相应地调整权重

公共类NeuralNetSimple {私有最终NeuronLayer layer1;私人double [] [] outputLayer1;公共NeuralNetSimple(NeuronLayer layer1){this.layer1 = layer1; } public void think(double [] [] input){outputLayer1 = apply(matrixMultiply(inputs,layer1.weights),layer1.activationFunction); } public void train(double [] []输入,double [] []输出,int numberOfTrainingIterations){for(int i = 0; i< numberOfTrainingIterations; ++ i){//通过网络思考传递训练集(输入); //通过错误调整权重*输入*输出*(1-输出)double [] [] errorLayer1 = matrixSubtract(outputs,outputLayer1); double [] [] deltaLayer1 = scalarMultiply(errorLayer1,apply(outputLayer1,layer1.activationFunctionDerivative)); // //计算调整权重的方法,// //由于要处理矩阵,我们通过将增量输出总和乘以输入来处理除法'转置! double [] [] AdjustmentLayer1 = matrixMultiply(matrixTranspose(inputs),deltaLayer1); //调整权重this.layer1.adjustWeights(adjustmentLayer1); }} public double [] [] getOutput(){return outputLayer1; }}

最终,我们有了主要方法来设置训练数据,训练网络并要求其对测试数据进行预测

public class LearnFirstColumnSimple {public static void main(String args []){//创建具有1个神经元和3个输入的隐藏层NeuronLayer layer1 = new NeuronLayer(1,3); NeuralNetSimple net =新的NeuralNetSimple(layer1); //训练净double [] []输入= new double [] [] {{0,0,1},{1,1,1},{1,0,1},{0,1,1} }; double [] []输出=新的double [] [] {{0},{1},{1},{0}}; System.out.println("训练神经网络..."); net.train(输入,输出,10000); System.out.println("完成的培训"); System.out.println("第1层权重"); System.out.println(layer1); //计算未知数据的预测// 1,0,0预报(新double [] [],净); // 0,1,0预测(新double [] [],净值); // 1,1,0预测(新double [] [],净值); }公共静态无效变量predict(double [] [] testInput,NeuralNetSimple net){net.think(testInput); //然后是System.out.println("对数据的预测" + testInput [0] [0] +"" + testInput [0] [1] +" " + testInput [0] [2] +"->" + net.getOutput()[0] [0] +&#34 ;,预期->" + testInput [0] [0]); }}

通过运行上面的示例,我们看到我们的网络在预测最左边的输入何时为1方面做得很好,但是似乎不能正确地得到0!这是因为第二和第三输入权重都需要都接近0。

训练神经网络...完成训练第1层权重[[9.672988220005456] [-0.2089781536334558] [-4.628957430141331]]数据预测1.0 0.0 0.0-> 0.9999370425325528,预期-> 1.0对数据的预测0.0 1.0 0.0-> 0.4479447696095623,预计-> 0.0对数据的预测1.0 1.0 0.0-> 0.9999224112145153,预期-> 1.0

在下一篇文章中,我们将看到是否在神经网络中添加另一层可以帮助改善预测;)