如何在 Java 中构建神经网络

人工神经网络是深度学习的一种形式,也是现代人工智能的支柱之一。 真正掌握这些东西如何工作的最好方法是建造一个。 本文将动手介绍如何使用 Java 构建和训练神经网络。

请参阅我之前的文章,机器学习的风格:神经网络简介,了解人工神经网络如何运作的概述。 我们在本文中的示例绝不是生产级系统; 相反,它在一个易于理解的演示中展示了所有主要组件。

一个基本的神经网络

神经网络是称为神经元的节点图。 神经元是计算的基本单位。 它接收输入并使用每个输入的权重、每个节点的偏差和最终函数处理器(称为激活函数)算法来处理它们。 您可以看到图 1 中所示的双输入神经元。

国际数据集团

图 1. 神经网络中的双输入神经元。

此模型具有广泛的可变性,但我们将在演示中使用此精确配置。

我们的第一步是建模 Neuron 将保存这些值的类。 你可以看到 Neuron 清单 1 中的类。请注意,这是该类的第一个版本。 它会随着我们添加功能而改变。

清单 1. 一个简单的 Neuron 类


class Neuron {
    Random random = new Random();
    private Double bias = random.nextDouble(-1, 1); 
    public Double weight1 = random.nextDouble(-1, 1); 
    private Double weight2 = random.nextDouble(-1, 1);
   
    public double compute(double input1, double input2){
      double preActivation = (this.weight1 * input1) + (this.weight2 * input2) + this.bias;
      double output = Util.sigmoid(preActivation);
      return output;
    }
  }

你可以看到 Neuron 类很简单,只有三个成员: bias, weight1, 和 weight2. 每个成员都被初始化为 -1 和 1 之间的随机双精度值。

当我们计算神经元的输出时,我们遵循图 1 中所示的算法:将每个输入乘以其权重加上偏差:input1 * weight1 + input2 * weight2 + bias。 这给了我们未处理的计算(即, preActivation) 我们通过激活函数运行。 在这种情况下,我们使用 Sigmoid 激活函数,它将值压缩到 -1 到 1 的范围内。 清单 2 显示了 Util.sigmoid() 静态方法。

清单 2. Sigmoid 激活函数


public class Util {
  public static double sigmoid(double in){
    return 1 / (1 + Math.exp(-in));
  }
}

现在我们已经了解了神经元是如何工作的,让我们将一些神经元放入网络中。 我们将使用 Network 带有神经元列表的类,如清单 3 所示。

清单 3. 神经网络类


class Network {
    List<Neuron> neurons = Arrays.asList(
      new Neuron(), new Neuron(), new Neuron(), /* input nodes */
      new Neuron(), new Neuron(),               /* hidden nodes */
      new Neuron());                            /* output node */
    }
}

虽然神经元列表是一维的,但我们会在使用过程中将它们连接起来,以便它们形成一个网络。 前三个神经元是输入,第二个和第三个是隐藏的,最后一个是输出节点。

做出预测

现在,让我们使用网络进行预测。 我们将使用一个由两个输入整数和 0 到 1 的答案格式组成的简单数据集。我的示例使用体重-身高组合来猜测一个人的性别,基于这样的假设:更多的体重和身高表明一个人是男性. 我们可以对任何双因素单输出概率使用相同的公式。 我们可以将输入视为向量,因此神经元的整体功能是将向量转换为标量值。

网络的预测阶段如清单 4 所示。

清单 4. 网络预测


public Double predict(Integer input1, Integer input2){
  return neurons.get(5).compute(
    neurons.get(4).compute(
      neurons.get(2).compute(input1, input2),
      neurons.get(1).compute(input1, input2)
    ),
    neurons.get(3).compute(
      neurons.get(1).compute(input1, input2),
      neurons.get(0).compute(input1, input2)
    )
  );
}

清单 4 显示,两个输入被馈送到前三个神经元,然后它们的输出被输送到神经元 4 和 5,这两个神经元又馈送到输出神经元。 这个过程被称为前馈。

现在,我们可以要求网络做出预测,如清单 5 所示。

清单 5. 获得预测


Network network = new Network();
Double prediction = network.predict(Arrays.asList(115, 66));
System.out.println(“prediction: “ + prediction);

我们肯定会得到一些东西,但这将是随机权重和偏差的结果。 对于真正的预测,我们需要先训练网络。

训练网络

训练神经网络遵循称为反向传播的过程,我将在下一篇文章中更深入地介绍这一过程。 反向传播基本上是通过网络向后推动变化,使输出朝着期望的目标移动。

我们可以使用函数微分来执行反向传播,但对于我们的示例,我们将做一些不同的事情。 我们将赋予每个神经元“变异”的能力。 在每一轮训练中(称为一个时期),我们选择一个不同的神经元对其一个属性进行小的随机调整(weight1, weight2, 或者 bias) 然后检查结果是否有所改善。 如果结果有所改善,我们将通过 remember() 方法。 如果结果恶化,我们将放弃改变 forget() 方法。

我们将添加班级成员(old* 权重和偏差的版本)来跟踪变化。 你可以看到 mutate(), remember(), 和 forget() 清单 6 中的方法。

清单 6. mutate()、remember()、forget()


public class Neuron() {
  private Double oldBias = random.nextDouble(-1, 1), bias = random.nextDouble(-1, 1); 
 public Double oldWeight1 = random.nextDouble(-1, 1), weight1 = random.nextDouble(-1, 1); 
 private Double oldWeight2 = random.nextDouble(-1, 1), weight2 = random.nextDouble(-1, 1);
public void mutate(){
      int propertyToChange = random.nextInt(0, 3);
      Double changeFactor = random.nextDouble(-1, 1);
      if (propertyToChange == 0){ 
        this.bias += changeFactor; 
      } else if (propertyToChange == 1){ 
        this.weight1 += changeFactor; 
      } else { 
        this.weight2 += changeFactor; 
      };
    }
    public void forget(){
      bias = oldBias;
      weight1 = oldWeight1;
      weight2 = oldWeight2;
    }
    public void remember(){
      oldBias = bias;
      oldWeight1 = weight1;
      oldWeight2 = weight2;
    }
}

很简单: mutate() 方法随机选择一个属性,并随机选择一个介于 -1 和 1 之间的值,然后更改该属性。 这 forget() 方法将更改回旧值。 这 remember() 方法将新值复制到缓冲区。

现在,利用我们的 Neuron的新功能,我们添加一个 train() 方法 Network,如清单 7 所示。

清单 7. Network.train() 方法


public void train(List<List<Integer>> data, List<Double> answers){
  Double bestEpochLoss = null;
  for (int epoch = 0; epoch < 1000; epoch++){
    // adapt neuron
    Neuron epochNeuron = neurons.get(epoch % 6);
    epochNeuron.mutate(this.learnFactor);

    List<Double> predictions = new ArrayList<Double>();
    for (int i = 0; i < data.size(); i++){
      predictions.add(i, this.predict(data.get(i).get(0), data.get(i).get(1)));
    }
    Double thisEpochLoss = Util.meanSquareLoss(answers, predictions);

    if (bestEpochLoss == null){
      bestEpochLoss = thisEpochLoss;
        epochNeuron.remember();
      } else {
    if (thisEpochLoss < bestEpochLoss){
      bestEpochLoss = thisEpochLoss;
      epochNeuron.remember();
    } else {
      epochNeuron.forget();
    }
  }
}

train() 方法迭代一千次 dataanswers Lists 在参数中。 这些是相同大小的训练集; data 保持输入值和 answers 拥有他们已知的好答案。 然后该方法对它们进行迭代并获得一个值,该值表示与已知的正确答案相比,网络对结果的猜测有多好。 然后,它使一个随机神经元发生变异,如果新测试表明它是更好的预测,则保留更改。

检查结果

我们可以使用均方误差 (MSE) 公式检查结果,这是一种在神经网络中测试一组结果的常用方法。 您可以在清单 8 中看到我们的 MSE 函数。

清单 8. MSE 函数


public static Double meanSquareLoss(List<Double> correctAnswers,   List<Double> predictedAnswers){
  double sumSquare = 0;
  for (int i = 0; i < correctAnswers.size(); i++){
    double error = correctAnswers.get(i) - predictedAnswers.get(i);
    sumSquare += (error * error);
  }
  return sumSquare / (correctAnswers.size());
}

微调系统

现在剩下的就是将一些训练数据放入网络并尝试进行更多预测。 清单 9 展示了我们如何提供训练数据。

清单 9. 训练数据


List<List<Integer>> data = new ArrayList<List<Integer>>();
data.add(Arrays.asList(115, 66));
data.add(Arrays.asList(175, 78));
data.add(Arrays.asList(205, 72));
data.add(Arrays.asList(120, 67));
List<Double> answers = Arrays.asList(1.0,0.0,0.0,1.0);  

Network network = new Network();
network.train(data, answers);

在清单 9 中,我们的训练数据是一个二维整数集列表(我们可以将它们视为体重和身高),然后是一个答案列表(1.0 是女性,0.0 是男性)。

如果我们在训练算法中添加一点日志记录,运行它会产生类似于清单 10 的输出。

清单 10. 记录训练器


// Logging:
if (epoch % 10 == 0) System.out.println(String.format("Epoch: %s | bestEpochLoss: %.15f | thisEpochLoss: %.15f", epoch, bestEpochLoss, thisEpochLoss));

// output:
Epoch: 910 | bestEpochLoss: 0.034404863820424 | thisEpochLoss: 0.034437939546120
Epoch: 920 | bestEpochLoss: 0.033875954196897 | thisEpochLoss: 0.431451026477016
Epoch: 930 | bestEpochLoss: 0.032509260025490 | thisEpochLoss: 0.032509260025490
Epoch: 940 | bestEpochLoss: 0.003092720117159 | thisEpochLoss: 0.003098025397281
Epoch: 950 | bestEpochLoss: 0.002990128276146 | thisEpochLoss: 0.431062364628853
Epoch: 960 | bestEpochLoss: 0.001651762688346 | thisEpochLoss: 0.001651762688346
Epoch: 970 | bestEpochLoss: 0.001637709485751 | thisEpochLoss: 0.001636810460399
Epoch: 980 | bestEpochLoss: 0.001083365453009 | thisEpochLoss: 0.391527869500699
Epoch: 990 | bestEpochLoss: 0.001078338540452 | thisEpochLoss: 0.001078338540452

清单 10 显示损失(与完全正确的误差偏差)缓慢下降; 也就是说,它越来越接近做出准确的预测。 剩下的就是看看我们的模型对真实数据的预测效果如何,如清单 11 所示。

清单 11. 预测


System.out.println("");
System.out.println(String.format("  male, 167, 73: %.10f", network.predict(167, 73)));
System.out.println(String.format("female, 105, 67: %.10", network.predict(105, 67))); 
System.out.println(String.format("female, 120, 72: %.10f | network1000: %.10f", network.predict(120, 72))); 
System.out.println(String.format("  male, 143, 67: %.10f | network1000: %.10f", network.predict(143, 67)));
System.out.println(String.format(" male', 130, 66: %.10f | network: %.10f", network.predict(130, 66)));

在清单 11 中,我们采用经过训练的网络并为其提供一些数据,从而输出预测。 我们得到类似清单 12 的内容。

清单 12. 经过训练的预测


  male, 167, 73: 0.0279697143 
female, 105, 67: 0.9075809407 
female, 120, 72: 0.9075808235 
  male, 143, 67: 0.0305401413
  male, 130, 66: network: 0.9009811922

在清单 12 中,我们看到网络在大多数值对(也称为向量)方面做得很好。 它为女性数据集提供了大约 0.907 的估计值,非常接近 1。 两名男性显示 0.027 和 0.030——接近 0。离群的男性数据集 (130, 67) 被视为可能是女性,但在 0.900 时信心较低。

结论

有多种方法可以调整此系统上的刻度盘。 首先,训练运行中的时期数是一个主要因素。 时代越多,模型对数据的调整就越多。 运行更多的 epoch 可以提高符合训练集的实时数据的准确性,但也会导致过度训练; 也就是说,一个模型可以自信地预测边缘案例的错误结果。

访问我的 GitHub 存储库以获取本教程的完整代码,以及一些额外的功能。

阅读更多

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注