面向 Java 开发人员的机器学习:机器学习算法

ChatGPT 和 Bard 等大型语言模型使机器学习成为一种现象。这些工具用于辅助编码,很快就在开发人员的工具包中占据了一席之地。从图像生成到疾病检测等其他用例也在探索之中。

科技公司正在大力投资机器学习,因此了解如何训练和使用模型对开发人员来说变得至关重要。

本文将为您介绍 Java 机器学习的入门知识。您将初步了解机器学习的工作原理,随后将学习如何实施和训练机器学习算法的简短指南。我们将重点介绍监督式机器学习,这是开发智能应用程序最常用的方法。

机器学习与人工智能

机器学习是从人工智能(AI)领域发展而来的,该领域试图制造出能够模仿人类智能的机器。虽然机器学习是计算机科学的热门趋势,但人工智能并不是一个新的科学领域。图灵测试由阿兰-图灵在 20 世纪 50 年代初提出,是最早用来确定计算机是否具有真正智能的测试之一。根据图灵测试,计算机可以通过欺骗人类使其相信自己也是人类来证明人类的智能。

许多最先进的机器学习方法都是基于几十年前的概念。过去十年发生的变化是,计算机(和分布式计算平台)现在具备了机器学习算法所需的处理能力。大多数机器学习算法需要处理大量的矩阵乘法和其他数学运算。管理这些运算的计算技术甚至在二十年前还不存在,但现在已经有了。并行处理和专用芯片以及大数据从根本上提高了机器学习平台的能力。

机器学习使程序能够在无需人工参与的情况下执行质量改进流程并扩展其功能。一些利用机器学习构建的程序甚至能够更新或扩展自己的代码。

机器如何学习

监督学习和无监督学习是最流行的机器学习方法。两者都需要向机器输入大量数据记录,以便关联和学习。这些收集到的数据记录通常被称为特征向量。就单个房屋而言,特征向量可能包括房屋总面积、房间数量和房龄等特征。

监督学习

在监督学习中,对机器学习算法进行训练,以正确回答与特征向量相关的问题。为了训练算法,机器需要输入一组特征向量和相关标签。标签通常由人类注释者提供,代表对给定问题的正确回答。学习算法会分析特征向量及其正确标签,找出它们之间的内部结构和关系。这样,机器就能学会正确回答问题。

举例来说,一个智能房地产应用程序可能会使用特征向量进行训练,这些特征向量包括一系列房屋各自的大小、房间数和房龄。人工标注人员会根据这些因素为每套房屋标注正确的房价。通过分析数据,房地产应用程序将被训练成能够回答 “这套房子能卖多少钱?”的问题。

训练过程结束后,新的输入数据不会被标记。即使是对于未见过的、未标记的特征向量,机器也能正确回答新的查询。

无监督学习

在无监督学习中,算法通过编程来预测答案,而无需人工标注,甚至无需提问。无监督学习不是预先确定标签或结果,而是利用海量数据集和处理能力来发现以前未知的相关性。例如,在消费品营销中,无监督学习可用于识别隐藏的关系或消费者分组,最终形成新的或改进的营销策略。

本文重点介绍有监督机器学习,这是目前最常见的机器学习方法。

有监督的机器学习项目

现在我们来看一个例子:一个房地产应用的监督学习项目。

所有机器学习都以数据为基础。从本质上讲,你输入许多数据实例以及这些数据在现实世界中的结果,算法就会根据这些输入建立一个数学模型。机器最终学会使用新数据来预测未知结果。

对于有监督的机器学习项目,您需要以一种有意义的方式为数据贴上标签,以获得您所寻求的结果。在表 1 中,请注意房屋记录的每一行都包含一个 “房价 “标签。通过将行数据与房价标签相关联,算法最终将能够预测不在其数据集中的房屋的市场价格(注意,房屋面积以平方米为单位,而房价以欧元为单位)。

表 1.房屋记录

FEATURE FEATURE FEATURE LABEL
Size of House Number of Rooms Age of House Estimated Cost
90 m2 / 295 ft 2 23 years 249,000 €
101 m2 / 331 ft 3 N/A 338,000 €
1330 m2 / 4363 ft 11 12 years 6,500,000 €

在早期阶段,您可能会手工标注数据记录,但最终您可以训练程序自动完成这一过程。您可能在电子邮件应用程序中见过这种情况,将电子邮件移入垃圾邮件文件夹后,会出现 “这是垃圾邮件吗?”的询问。当您回复时,您就在训练程序识别您不想看到的邮件。应用程序的垃圾邮件过滤器会学习如何标记和处理来自同一来源或包含类似内容的未来邮件。

标记数据集仅用于训练和测试目的。这一阶段结束后,机器学习模型将在无标签数据实例上工作。例如,您可以向预测算法输入一条新的、未标记的房屋记录,它就会根据训练数据自动预测预期房价。

训练机器学习模型

有监督机器学习的挑战在于为特定问题找到合适的预测函数。在数学上,挑战在于找到输入/输出函数,该函数接收输入变量 x 并返回预测值 y。这个假设函数 (hθ) 就是训练过程的输出结果。通常,假设函数也被称为目标函数或预测函数。

图 1.目标函数示例

在大多数情况下,x 代表一个多数据点。在我们的示例中,这可能是由房屋大小值和房间数量值定义的单个房屋的二维数据点。这些值的数组被称为特征向量。要预测单个房屋的价格,可以使用包含房屋大小和房间数量的特征向量 { 101.0, 3.0 } 调用目标函数:

清单 1.使用特征向量调用目标函数

// target function h (which is the output of the learn process)
Function<Double[], Double> h = ...;

// set the feature vector with house size=101 and number-of-rooms=3
Double[] x = new Double[] { 101.0, 3.0 };

// and predicted the house price (label)
double y = h.apply(x);

在清单 1 中,数组变量 x 值代表房屋的特征向量。目标函数返回的 y 值是预测的房价。

机器学习的挑战在于如何定义一个目标函数,使其尽可能准确地用于未知的、未见过的数据实例。在机器学习中,目标函数  (hθ)  有时被称为模型。该模型是学习过程的结果,也称为模型训练。

图 2.机器学习模型

学习算法以标注的训练示例为基础,在训练数据中寻找结构或模式。在此过程中,算法会逐步修改数值以减少损失。在此基础上,学习算法会生成一个能够从数据中进行泛化的模型。

通常,学习过程是探索性的。在大多数情况下,这个过程会使用不同的学习算法和配置执行多次。当确定了一个模型后,数据也会通过该模型运行多次。这些迭代称为历元。

最终,将根据性能指标对所有模型进行评估。选出最佳模型,用于计算对未来未标记数据实例的预测。

线性回归

要训练机器思考,第一步就是选择要使用的学习算法。线性回归是最简单、最流行的监督学习算法之一。该算法假定输入特征和输出标签之间是线性关系。图 3 中的通用线性回归函数通过总结特征向量的每个元素乘以一个 theta 参数 (θ) 来返回预测值。在训练过程中,θ 参数用于根据训练数据调整或 “调整 “回归函数。

图 3.通用线性回归函数

线性回归是一种简单的学习函数,但它为前馈神经网络中使用的梯度下降等更高级的形式奠定了良好的基础。在线性回归函数中,θ 参数和特征参数由订阅号枚举。订阅号表示 Theta 参数 (θ) 和特征参数 (x) 在向量中的位置。需要注意的是,特征 x0 是一个常数偏移项,为便于计算,其值设为 1。因此,特定领域特征(如房屋大小)的索引将从 x1开始。因此,如果x1被设置为房屋特征向量的第一个值(房屋大小),那么 x2 将被设置为下一个值(房间数量),以此类推。

清单 2 显示了此线性回归函数的 Java 实现,数学上显示为 hθ(x)。为简单起见,计算使用了 double 数据类型。在 apply() 方法中,预计数组的第一个元素已在该函数之外设置为 1.0。

清单 2.Java 中的线性回归

public class LinearRegressionFunction implements Function<Double[], Double> {
   private final double[] thetaVector;

   LinearRegressionFunction(double[] thetaVector) {
      this.thetaVector = Arrays.copyOf(thetaVector, thetaVector.length);
   }

   public Double apply(Double[] featureVector) {
      // for computational reasons the first element has to be 1.0
      assert featureVector[0] == 1.0;

      // simple, sequential implementation
      double prediction = 0;
      for (int j = 0; j < thetaVector.length; j++) {
         prediction += thetaVector[j] * featureVector[j];
      }
      return prediction;
   }

   public double[] getThetas() {
      return Arrays.copyOf(thetaVector, thetaVector.length);
   }
}

要创建 LinearRegressionFunction 的新实例,必须设置 theta 参数。Theta 参数或向量用于使通用回归函数适应基础训练数据。程序的 theta 参数将在学习过程中根据训练示例进行调整。训练目标函数的质量只能与给定训练数据的质量相当。

在下一个示例中,将对 LinearRegressionFunction 进行实例化,以根据房屋面积预测房价。考虑到x0 必须是 1.0 的常量值,我们将使用两个 theta 参数对目标函数进行实例化。Theta 参数是学习过程的输出。创建新实例后,面积为 1330 平方米的房屋价格预测结果如下:

// the theta vector used here was output of a train process
double[] thetaVector = new double[] { 1.004579, 5.286822 };
LinearRegressionFunction targetFunction = new LinearRegressionFunction(thetaVector);

// create the feature vector function with x0=1 (for computational reasons) and x1=house-size
Double[] featureVector = new Double[] { 1.0, 1330.0 };

// make the prediction
double predictedPrice = targetFunction.apply(featureVector);

目标函数的预测线在图 4 中显示为一条蓝线。这条线是通过对所有房屋面积值执行目标函数计算得出的。图中还包括用于训练的价格-规模对。

图 4 目标函数预测线目标函数的预测线

到目前为止,预测图似乎足够贴切。图形坐标(截距和斜率)由θ 向量 { 1.004579, 5.286822 } 定义。但你怎么知道这个 Theta 向量是最适合你的应用的?如果改变第一个或第二个 theta 参数,函数的拟合效果会更好吗?要确定最合适的 theta 参数向量,需要一个效用函数来评估目标函数的性能。

为目标函数评分

在机器学习中,代价函数(J(θ))(又称 “损失函数”)用于计算给定目标函数的平均误差或 “代价”。图 5 显示了一个例子。

图 5 成本函数成本函数

成本函数表示模型与训练数据的拟合程度。要确定训练目标函数的成本,可以计算每个房屋示例 (i) 的平方误差。误差是计算出的 y 值与房屋示例 i 的实际 y 值之间的距离。

例如,面积为 1330 平方米的房屋的实际价格为 6,500,000 欧元,而经过训练的目标函数预测的房屋价格为 7,032,478 欧元,差距(或误差)为 532,478 欧元。您也可以在图 4 所示的图表中找到这一差距。差距(或误差)以垂直红色虚线的形式显示在每个训练价格-规模对中。

要计算训练目标函数的成本,必须总结示例中每栋房屋的平方误差并计算平均值。J(θ) 的成本值越小,目标函数的预测就越精确。

在下面的列表中,代价函数的简单 Java 实现将目标函数、训练记录列表及其相关标签作为输入。预测值将在循环中计算,误差将通过减去真实标签值来计算。

之后,将汇总平方误差并计算平均误差。成本将以双值形式返回:

public static double cost(Function<Double[], Double> targetFunction,
                          List<Double[]> dataset,
                          List<Double> labels) {
   int m = dataset.size();
   double sumSquaredErrors = 0;

   // calculate the squared error ("gap") for each training example and add it to the total sum
   for (int i = 0; i < m; i++) {
      // get the feature vector of the current example
      Double[] featureVector = dataset.get(i);
      // predict the value and compute the error based on the real value (label)
      double predicted = targetFunction.apply(featureVector);
      double label = labels.get(i);
      double gap = predicted - label;
      sumSquaredErrors += Math.pow(gap, 2);
   }

   // calculate and return the mean value of the errors (the smaller the better)
   return (1.0 / (2 * m)) * sumSquaredErrors;
}

用梯度下降法训练目标函数

虽然代价函数有助于分别评估目标函数和 theta 参数的质量,但仍需要计算最佳拟合的 theta 参数。您可以使用梯度下降算法进行计算。

使用梯度下降算法计算 theta 参数

梯度下降算法能使成本函数最小化,也就是说,它能根据训练数据找到产生最低成本(J(θ))的θ 组合。梯度下降法通过使用偏导数逐步调整每个变量来实现这一点。这是反向传播的典型形式,所有其他方法都是基于这种形式。

图 6 显示了一种简化算法,用于计算拟合度更高的新θ:

图 6.梯度下降使成本函数最小化

在每次迭代中,θ 向量的每个参数 θ 都会计算出一个新的、更好的值。学习率 α 控制着每次迭代的计算步长。这种计算将重复进行,直到得到一个拟合良好的 θ 值组合。例如,图 7 中的线性回归函数有三个 theta 参数:

图 7.带有三个 Theta 参数的线性回归函数

在每次迭代(”epoch”)中,将并行计算每个 theta 参数的新值:θ0、θ1 和 θ2。每次迭代后,您都可以使用新的 theta 向量 {θ0, θ1, θ2} 创建一个新的、拟合度更高的 LinearRegressionFunction 实例。

清单 3 显示了梯度下降算法的 Java 代码。回归函数的θ将使用训练数据、数据标签和学习率(α)进行训练。函数的输出是使用新的 theta 参数改进后的目标函数。train() 方法将被反复调用,并输入新的目标函数和上一次计算中的新 thetas。这些调用将重复进行,直到调整后的目标函数的成本达到最低点。

清单 3.Java 中的梯度下降算法示例

public static LinearRegressionFunction train(LinearRegressionFunction targetFunction,
                                             List<Double[]> dataset,
                                             List<Double> labels,
                                             double alpha) {
   int m = dataset.size();
   double[] thetaVector = targetFunction.getThetas();
   double[] newThetaVector = new double[thetaVector.length];

   // compute the new theta of each element of the theta array
   for (int j = 0; j < thetaVector.length; j++) {
      // summarize the error gap * feature
      double sumErrors = 0;
      for (int i = 0; i < m; i++) {
         Double[] featureVector = dataset.get(i);
         double error = targetFunction.apply(featureVector) - labels.get(i);
         sumErrors += error * featureVector[j];
      }

      // compute the new theta value
      double gradient = (1.0 / m) * sumErrors;
      newThetaVector[j] = thetaVector[j] - alpha * gradient;
   }

   return new LinearRegressionFunction(newThetaVector);
}

要验证成本是否持续下降,可以在每个训练步骤后执行成本函数 J(θ)。每迭代一次,成本就必须降低一次。如果没有减少,则说明学习率参数值过大,算法会超过最小值。在这种情况下,梯度下降算法就会失败。

模型为何不起作用

图 8 显示了使用计算出的新 Theta 参数的目标函数,初始 Theta 向量为 { 1.0, 1.0 }。左侧一列显示的是迭代 50 次后的预测图;中间一列显示的是迭代 200 次后的预测图;右侧一列显示的是迭代 1000 次后的预测图。如图所示,随着新目标函数的拟合效果越来越好,每次迭代后的成本都在降低。迭代 500 到 600 次后,θ 参数不再发生显著变化,成本达到稳定的高点。此时,目标函数的精度将不再显著提高。

图 8.成本随着每次迭代而降低

在这种情况下,虽然经过 500 到 600 次迭代后,成本不再显著降低,但目标函数仍然不是最优的,似乎是拟合不足。在机器学习中,”欠拟合 “一词用来表示学习算法没有捕捉到数据的潜在趋势。

根据现实世界的经验,预计大面积房产的每平方米价格会下降。由此我们得出结论,训练过程中使用的模型,即目标函数,没有很好地拟合数据。拟合不足通常是由于模型过于简单造成的。在这种情况下,这是由于我们的简单目标函数只使用了单一的房屋大小特征。仅凭这些数据还不足以准确预测房屋成本。

添加特征和特征缩放

如果发现目标函数与要解决的问题不匹配,可以对其进行调整。纠正不拟合的常见方法是在特征向量中添加更多特征。

在房屋价格的例子中,你可以添加更多的房屋特征,比如房间数量或房龄。你可以使用一个多值特征向量来描述房屋实例,而不是使用 { 大小 } 这个单一的特定领域特征向量,例如 { 大小、房间数、房龄 }。

在某些情况下,可用的训练数据集中没有足够的特征。在这种情况下,你可以尝试添加多项式特征,这些特征是由现有特征计算得出的。例如,您可以扩展房价目标函数,使其包含计算出的平方尺寸特征 (x2):

图 9.利用多项式特征扩展的目标函数

使用多个特征需要进行特征缩放(或 “归一化”),以规范不同特征的范围。例如,size2 特征的取值范围比 size 特征的取值范围大一个量级。如果不对特征进行缩放,size2 特征将主导成本函数。size2 特征产生的误差值将远高于仅由尺寸特征产生的误差值。图 10 显示了一种简单的特征缩放算法。

图 10.简单的特征缩放算法

该算法由下面示例 Java 代码中的 FeaturesScaling 类实现。FeaturesScaling 类提供了一个工厂方法,用于创建根据训练数据调整的缩放函数。内部使用训练数据实例来计算平均值、最小值和最大值常数。由此产生的函数会消耗一个特征向量,并生成一个带有缩放特征的新特征向量。如下图所示,训练过程和预测调用都需要对特征进行缩放:

// create the dataset
List<Double[]> dataset = new ArrayList<>();
dataset.add(new Double[] { 1.0,  90.0,  8100.0 });   // feature vector of house#1
dataset.add(new Double[] { 1.0, 101.0, 10201.0 });   // feature vector of house#2
dataset.add(new Double[] { 1.0, 103.0, 10609.0 });   // ...
//...

// create the labels
List<Double> labels = new ArrayList<>();
labels.add(249.0);        // price label of house#1
labels.add(338.0);        // price label of house#2
labels.add(304.0);        // ...
//...

// scale the extended feature list
Function<Double[], Double[]> scalingFunc = FeaturesScaling.createFunction(dataset);
List<Double[]>  scaledDataset  = dataset.stream().map(scalingFunc).collect(Collectors.toList());

// create hypothesis function with initial thetas and train it with learning rate 0.1
LinearRegressionFunction targetFunction =  new LinearRegressionFunction(new double[] { 1.0, 1.0, 1.0 });
for (int i = 0; i < 10000; i++) {
   targetFunction = Learner.train(targetFunction, scaledDataset, labels, 0.1);
}


// make a prediction of a house with size if 600 m2
Double[] scaledFeatureVector = scalingFunc.apply(new Double[] { 1.0, 600.0, 360000.0 });
double predictedPrice = targetFunction.apply(scaledFeatureVector);

当你添加更多特征时,你可能会发现目标函数拟合得越来越好–但要小心!如果走得太远,添加了太多特征,最终可能会导致目标函数过度拟合。

过度拟合和交叉验证

当目标函数或模型与训练数据拟合得太好时,就会出现过度拟合。过度拟合的模型会捕捉到训练数据中的噪声或随机波动。图 11 中图表的最右侧显示了一种过拟合行为模式。

图 11.具有过度拟合行为的目标函数示例

尽管过拟合模型在训练数据上非常匹配,但当需要求解未知的、未见过的数据时,它的表现就会很糟糕。有几种方法可以避免过度拟合:

  • 使用更大的训练数据集。
  • 通过增加正则化来改进机器学习算法。
  • 使用更少的特征,如上图中间所示。
  • 如果你的预测模型过度拟合,你应该删除任何对其准确性无益的特征。这里的挑战在于找到对预测输出贡献最大的特征。

如图所示,过度拟合可以通过可视化图表来识别。尽管这种方法在使用二维或三维图形时效果很好,但如果使用两个以上的特定领域特征,就会变得很困难。这就是为什么交叉验证常用于检测过度拟合的原因。

在交叉验证中,学习过程结束后,使用未见过的验证数据集对训练好的模型进行评估。可用的标注数据集将分为三部分:

  • 训练数据集
  • 验证数据集
  • 测试数据集

在这种情况下,60% 的房屋示例记录可用于训练目标算法的不同变体。学习过程结束后,剩余的一半未经处理的示例记录将用于验证训练好的目标算法是否能很好地处理未见数据。

通常情况下,我们会选择最合适的目标算法。另一半未触及的示例数据将用于计算最终选定模型的误差指标。虽然我不会在这里介绍它们,但这种技术还有其他变种,例如 k 折交叉验证。

第 1 部分结论

在本文中,您将有机会开始学习 Java 中的机器学习。我们介绍了一个监督学习示例,并使用梯度下降算法来训练目标函数。您还看到了一个模型拟合不足的示例,以及如何通过添加特征和特征缩放来纠正它。我们还简要讨论了过拟合的危险以及如何纠正它们。

本文的后半部分将继续关注基于 Java 的机器学习。我们将介绍基于 JVM 的机器学习框架 Weka,然后开始在基于 Java 的环境中开发机器学习数据模型。

本文文字及图片出自 Machine learning for Java developers: Algorithms for machine learning

阅读余下内容
 

发表回复

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


京ICP备12002735号