Java-数据科学-三-

57 阅读30分钟

Java 数据科学(三)

七、神经网络

虽然神经网络已经存在了很多年,但由于改进的算法和更强大的机器,它们变得越来越受欢迎。一些公司正在构建明确模仿神经网络的硬件系统(www.wired.com/2016/05/goo…)。使用这种多功能技术来解决数据科学问题的时候到了。

在这一章中,我们将探索神经网络背后的思想和概念,然后演示它们的使用。具体来说,我们将:

  • 定义并举例说明神经网络

  • 描述他们是如何被训练的

  • 检查各种神经网络架构

  • Discuss and demonstrate several different neural networks, including:

    • 一个简单的 Java 例子
    • 一个多层感知器 ( MLP )网络
    • k-最近邻 ( k-NN )算法等

我们称之为神经网络的人工神经网络 ( )的想法源于大脑中发现的神经元。一个神经元是一个有树突将其连接到输入源和其他神经元的细胞。它通过树突从多个来源接收刺激。根据源,分配给源的权重,神经元被激活,并且发射信号沿着树突到达另一个神经元。可以训练一组神经元,它们将对一组特定的输入信号做出反应。

人工神经元是具有一个或多个输入和单个输出的节点。每个输入都有一个与之关联的权重。通过加权输入,我们可以放大或缩小输入。

注意

人工神经元被交替称为感知器

这在下图中有所描述,其中权重被相加,然后被发送到决定输出的激活函数**。**

Neural Networks

神经元以及最终神经元集合以两种模式之一运行:

  • 训练模式 -神经元被训练为在接收到某组输入时触发
  • 测试模式 -向神经元提供输入,神经元根据训练对一组已知的输入做出响应

数据集通常分为两部分。更大的部分用于训练模型。第二部分用于验证模型。

神经元的输出由加权输入的总和决定。一个神经元是否放电是由一个激活函数决定的。有几种不同类型的激活功能,包括:

  • 阶跃函数 -使用加权输入的总和计算该线性函数,如下所示:

Neural Networks

f(Net) 表示一个函数的输出。如果输入大于激活阈值,则为 1 。当这种情况发生时,神经元就会放电。否则它返回 0 并且不触发。该值是基于所有树突输入计算的。

  • Sigmoid -这是一个非线性函数,计算如下:

Neural Networks

随着神经元被训练,每个输入的权重可以被调整。

与阶跃函数相比,sigmoid 函数是非线性的。这更好地匹配了一些问题域。我们将找到多层神经网络中使用的 sigmoid 函数。

训练一个神经网络

有三种基本的培训方法:

  • 监督学习 -通过监督学习,用匹配输入集和输出值的数据训练模型
  • 无监督学习 -在无监督学习中,数据不包含结果,但是模型被期望自己确定关系
  • 强化学习 -类似于监督学习,但是对好的结果提供奖励

这些数据集包含的信息不同。监督和强化学习包含一组输入的正确输出。无监督学习不包含正确的结果。

神经网络通过将输入输入到网络中并使用激活函数将结果与预期结果进行比较来进行学习(至少使用监督学习)。如果它们匹配,那么网络已经被正确训练。如果它们不匹配,则网络被修改。

当我们修改权重时,我们需要小心不要改变太大。如果变化太大,那么结果可能变化太大,我们可能会错过期望的输出。如果变化太少,那么训练模型将花费太长时间。有些时候,我们可能不想改变一些权重。

一个偏置单元是一个具有恒定输出的神经元。它总是一个,有时被称为假节点。这个神经元类似于一个偏移量,对于大多数网络的正常运行至关重要。你可以将偏差神经元比作斜率截距形式的线性函数的y-截距。正如调整y-截距值会改变线的位置,但不会改变形状/斜率一样,偏置神经元可以在不调整网络形状或功能的情况下改变输出值。您可以调整输出以适应问题的特殊需要。

神经网络架构入门

神经网络通常使用一系列神经元层来创建。通常有一个输入层,一个或多个中间层(隐藏层,以及一个输出层

以下是前馈网络的描述:

Getting started with neural network architectures

节点和层的数量会有所不同。前馈网络将信息向前传递。也有信息反向传递的反馈网络。需要多个隐藏层来处理大多数分析所需的更复杂的处理。

在本章中,我们将讨论与不同类型的神经网络相关的几种架构和算法。由于需要解释的复杂性和长度,我们将只提供对几个关键网络类型的深入分析。具体来说,我们将演示一个简单的神经网络、MLPs 和自组织映射 ( SOMs )。

但是,我们将提供许多不同选项的概述。适用于任何特定模型的神经网络和算法实现的类型将取决于所解决的问题。

了解静态神经网络

静态神经网络是经过训练或学习阶段,然后在使用时不会改变的人工神经网络。它们不同于动态神经网络,动态神经网络不断学习,并且在初始训练期之后可能经历结构变化。当模型的结果相对容易重现或更容易预测时,静态神经网络非常有用。我们一会儿将看动态神经网络,但是我们将从创建我们自己的基本静态神经网络开始。

一个基本的 Java 例子

在我们研究可用于构建神经网络的各种库和工具之前,我们将使用标准 Java 库实现我们自己的基本神经网络。下一个例子是改编自杰夫·希顿(www.informit.com/articles/ar…)的作品。我们将构建一个前馈反向传播神经网络,并训练它识别 XOR 运算符模式。以下是 XOR 的基本真值表:

| X | Y | 结果 | | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 0 |

该网络只需要对应于 XY 输入和结果的两个输入神经元和一个输出神经元。模型所需的输入和输出神经元的数量取决于手头的问题。隐藏神经元的数量通常是输入和输出神经元数量的总和,但随着训练的进行,确切的数量可能需要改变。

接下来,我们将演示如何创建和训练网络。我们首先给网络提供一个输入,然后观察输出。将输出与预期输出进行比较,然后调整称为weightChanges的权重矩阵。这种调整确保了随后的输出将更接近预期的输出。重复这个过程,直到我们对网络能够产生足够接近预期输出的结果感到满意。在本例中,我们将输入和输出表示为双精度数组,其中每个输入或输出神经元都是数组的一个元素。

注意

输入和输出有时被称为模式

首先,我们将创建一个SampleNeuralNetwork类来实现网络。首先将下面列出的变量添加到该类中。我们将在本节的后面讨论和演示它们的用途。我们的类包含以下实例变量:

   double errors; 

   int inputNeurons; 

   int outputNeurons; 

   int hiddenNeurons; 

   int totalNeurons; 

   int weights; 

   double learningRate; 

   double outputResults[]; 

   double resultsMatrix[]; 

   double lastErrors[]; 

   double changes[]; 

   double thresholds[]; 

   double weightChanges[]; 

   double allThresholds[]; 

   double threshChanges[]; 

   double momentum; 

   double errorChanges[]; 

接下来,让我们看看我们的构造函数。我们有四个参数,代表我们网络的输入数量、隐藏层中神经元的数量、输出神经元的数量以及我们希望学习发生的速率和动量。learningRate是指定训练过程中体重和偏差变化幅度的参数。momentum参数指定应该添加先前权重的多少部分来创建新的权重。防止在局部最小值鞍点处收敛是有用的。高动量会加快系统的收敛速度,但如果动量太高,会导致系统不稳定。动量和学习率都应该是在01之间的值:

public SampleNeuralNetwork(int inputCount, 

         int hiddenCount, 

         int outputCount, 

         double learnRate, 

         double momentum) { 

   ...

} 

在我们的构造函数中,我们初始化所有私有的实例变量。注意totalNeurons被设置为所有输入、输出和隐藏神经元的总和。这个总和然后被用来设置其他几个变量。还要注意的是,weights变量是通过找出输入和隐藏神经元的数量的乘积、隐藏神经元和输出的乘积,并将这两个乘积相加而计算出来的。然后用它来创建新的长度权重数组:

     learningRate = learnRate; 

     momentum = momentum; 

     inputNeurons = inputCount; 

     hiddenNeurons = hiddenCount; 

     outputNeurons = outputCount; 

     totalNeurons = inputCount + hiddenCount + outputCount; 

     weights = (inputCount * hiddenCount)  

        + (hiddenCount * outputCount); 

     outputResults    = new double[totalNeurons]; 

     resultsMatrix   = new double[weights]; 

     weightChanges = new double[weights]; 

     thresholds = new double[totalNeurons]; 

     errorChanges = new double[totalNeurons]; 

     lastErrors    = new double[totalNeurons]; 

     allThresholds = new double[totalNeurons]; 

     changes = new double[weights]; 

     threshChanges = new double[totalNeurons]; 

     reset(); 

注意,我们在构造函数的末尾调用了reset方法。这种方法重置网络,用随机权重矩阵开始训练。它将阈值和结果矩阵初始化为随机值。它还确保用于跟踪变化的所有矩阵被设置回零。使用随机值可确保获得不同的结果:

public void reset() { 

   int loc; 

   for (loc = 0; loc < totalNeurons; loc++) { 

         thresholds[loc] = 0.5 - (Math.random()); 

         threshChanges[loc] = 0; 

         allThresholds[loc] = 0; 

   } 

   for (loc = 0; loc < resultsMatrix.length; loc++) { 

         resultsMatrix[loc] = 0.5 - (Math.random()); 

         weightChanges[loc] = 0; 

         changes[loc] = 0; 

   } 

} 

我们还需要一个叫做calcThreshold的方法。阈值值指定了在神经元触发之前,该值与实际激活阈值的接近程度。例如,一个神经元可能具有激活阈值1。阈值指定诸如0.999之类的数字是否算作1。该方法将在后续方法中用于计算单个值的阈值:

public double threshold(double sum) { 

   return 1.0 / (1 + Math.exp(-1.0 * sum)); 

} 

接下来,我们将添加一个方法,使用一组给定的输入来计算输出。我们的输入参数和方法返回的数据都是由double值组成的数组。首先,我们需要在循环中使用两个位置变量,locpos。我们还想根据输入和隐藏神经元的数量来跟踪我们在数组中的位置。隐藏神经元的索引将在输入神经元之后开始,因此它的位置与输入神经元的数量相同。我们输出神经元的位置是我们输入神经元和隐藏神经元的总和。我们还需要初始化我们的outputResults数组:

public double[] calcOutput(double input[]) { 

   int loc, pos; 

   final int hiddenIndex = inputNeurons; 

   final int outIndex = inputNeurons + hiddenNeurons; 

   for (loc = 0; loc < inputNeurons; loc++) { 

         outputResults[loc] = input[loc]; 

   } 

... 

} 

然后,我们根据网络第一层的输入神经元计算输出。注意我们在本节中使用了threshold方法。在我们将总和放入outputResults数组之前,我们需要利用threshold方法:

   int rLoc = 0; 

   for (loc = hiddenIndex; loc < outIndex; loc++) { 

         double sum = thresholds[loc]; 

         for (pos = 0; pos < inputNeurons; pos++) { 

               sum += outputResults[pos] * resultsMatrix[rLoc++]; 

         } 

         outputResults[loc] = threshold(sum); 

   } 

现在我们考虑我们隐藏的神经元。请注意,这个过程与上一节类似,但是我们计算的是隐藏层的输出,而不是输入层的输出。最后,我们返回我们的结果。这个结果是一个双精度数组,包含每个输出神经元的值。在我们的例子中,只有一个输出神经元:


   double result[] = new double[outputNeurons]; 

   for (loc = outIndex; loc < totalNeurons; loc++) { 

         double sum = thresholds[loc]; 

         for (pos = hiddenIndex; pos < outIndex; pos++) { 

               sum += outputResults[pos] * resultsMatrix[rLoc++]; 

         } 

         outputResults[loc] = threshold(sum); 

         result[loc-outIndex] = outputResults[loc]; 

   } 

   return result; 

给定我们的 XOR 表,输出很可能与预期的输出不匹配。为了解决这个问题,我们使用误差计算方法来调整网络的权重,以产生更好的输出。我们将讨论的第一种方法是calcError方法。每当calcOutput方法返回一组输出时,就会调用这个方法。它不返回数据,而是修改包含权重和阈值的数组。该方法采用表示每个输出神经元的理想值的双精度数组。请注意,我们像在calcOutput方法中一样开始,并设置了在整个方法中使用的索引。然后,我们清除任何现有的隐藏层错误:

public void calcError(double ideal[]) { 

   int loc, pos; 

   final int hiddenIndex = inputNeurons; 

   final int outputIndex = inputNeurons + hiddenNeurons; 

      for (loc = inputNeurons; loc < totalNeurons; loc++) { 

            lastErrors[loc] = 0; 

      } 

接下来,我们计算预期产量和实际产量之间的差异。这使我们能够确定如何调整重量,以便进一步训练。为此,我们遍历包含预期输出ideal和实际输出outputResults的数组。我们还在本节中调整我们的误差和误差变化:


      for (loc = outputIndex; loc < totalNeurons; loc++) { 

         lastErrors[loc] = ideal[loc - outputIndex] -  

            outputResults[loc]; 

         errors += lastErrors[loc] * lastErrors[loc]; 

         errorChanges[loc] = lastErrors[loc] * outputResults[loc]

            *(1 - outputResults[loc]); 

     } 

     int locx = inputNeurons * hiddenNeurons; 

     for (loc = outputIndex; loc < totalNeurons; loc++) { 

           for (pos = hiddenIndex; pos < outputIndex; pos++) { 

                 changes[locx] += errorChanges[loc] *

                       outputResults[pos]; 

                 lastErrors[pos] += resultsMatrix[locx] *

                       errorChanges[loc]; 

                 locx++; 

           } 

           allThresholds[loc] += errorChanges[loc]; 

      } 

接下来,我们计算并存储每个神经元的误差变化。我们使用lastErrors数组来修改errorChanges数组,它包含总误差:

for (loc = hiddenIndex; loc < outputIndex; loc++) { 

      errorChanges[loc] = lastErrors[loc] *outputResults[loc] 

            * (1 - outputResults[loc]); 

}

我们还通过修改allThresholds数组来微调我们的系统。监控误差和阈值的变化很重要,这样网络可以提高其产生正确输出的能力:


   locx = 0;  

   for (loc = hiddenIndex; loc < outputIndex; loc++) { 

         for (pos = 0; pos < hiddenIndex; pos++) { 

               changes[locx] += errorChanges[loc] *  

                     outputResults[pos]; 

               lastErrors[pos] += resultsMatrix[locx] *  

                     errorChanges[loc]; 

               locx++; 

         } 

         allThresholds[loc] += errorChanges[loc]; 

   } 

} 

我们还有另一种计算网络误差的方法。getError方法计算我们整个训练数据集的均方根。这使我们能够确定数据的平均错误率:

public double getError(int len) { 

   double err = Math.sqrt(errors / (len * outputNeurons)); 

   errors = 0; 

   return err; 

} 

既然我们可以初始化我们的网络,计算输出,并计算误差,我们准备好训练我们的网络。我们通过使用train方法来实现这一点。该方法首先根据前一方法中计算的误差调整权重,然后调整阈值:

public void train() { 

   int loc; 

   for (loc = 0; loc < resultsMatrix.length; loc++) { 

      weightChanges[loc] = (learningRate * changes[loc]) +  

         (momentum * weightChanges[loc]); 

      resultsMatrix[loc] += weightChanges[loc]; 

      changes[loc] = 0; 

   } 

   for (loc = inputNeurons; loc < totalNeurons; loc++) { 

      threshChanges[loc] = learningRate * allThresholds[loc] +  

         (momentum * threshChanges[loc]); 

      thresholds[loc] += threshChanges[loc]; 

      allThresholds[loc] = 0; 

   } 

} 

最后,我们可以创建一个新类来测试我们的神经网络。在另一个类的main方法中,添加以下代码来表示 XOR 问题:

double xorIN[][] ={ 

               {0.0,0.0}, 

               {1.0,0.0}, 

               {0.0,1.0}, 

               {1.0,1.0}}; 

double xorEXPECTED[][] = { {0.0},{1.0},{1.0},{0.0}}; 

接下来,我们要创建新的SampleNeuralNetwork对象。在下面的例子中,我们有两个输入神经元、三个隐藏神经元、一个输出神经元(XOR 结果)、一个学习速率0.7和一个动量0.9。隐藏神经元的数量通常最好通过反复试验来确定。在后续执行中,考虑调整此构造函数中的值,并检查结果的差异:

SampleNeuralNetwork network = new  

                SampleNeuralNetwork(2,3,1,0.7,0.9); 

注意

学习率和动量通常应该在零和一之间。

然后,我们反复调用我们的calcOutputcalcErrortrain方法,按这个顺序。这允许我们测试我们的输出,计算错误率,调整我们的网络权重,然后再试一次。我们的网络应该显示越来越准确的结果:


for (int runCnt=0;runCnt<10000;runCnt++) { 

   for (int loc=0;loc<xorIN.length;loc++) { 

         network.calcOutput(xorIN[loc]); 

         network.calcError(xorEXPECTED[loc]); 

         network.train(); 

   } 

   System.out.println("Trial #" + runCnt + ",Error:" +  

               network.getError(xorIN.length)); 

} 

执行应用程序,注意错误率随着循环的每次迭代而变化。可接受的错误率将取决于特定的网络及其目的。下面是前面代码的一些输出示例。为简洁起见,我们包括了第一个和最后一个培训输出。请注意,错误率最初高于 50%,但在最后一次运行时降至接近 1%。

Trial #0,Error:0.5338334002845255
Trial #1,Error:0.5233475199946769
Trial #2,Error:0.5229843653785426
Trial #3,Error:0.5226263062497853
Trial #4,Error:0.5226916275713371
...
Trial #994,Error:0.014457034704806316
Trial #995,Error:0.01444865096401158
Trial #996,Error:0.01444028142777395
Trial #997,Error:0.014431926056394229
Trial #998,Error:0.01442358481032747
Trial #999,Error:0.014415257650182488

在这个例子中,我们使用了一个小规模的问题,我们能够相当快地训练我们的网络。在更大规模的问题中,我们将从一组训练数据开始,然后使用额外的数据集进行进一步分析。因为在这个场景中我们实际上只有四个输入,所以我们不会用任何额外的数据来测试它。

此示例演示了神经网络的一些内部工作方式,包括如何计算误差和输出的详细信息。通过探索一个相对简单的问题,我们能够检查神经网络的机制。然而,在接下来的例子中,我们将使用对我们隐藏这些细节的工具,但是允许我们进行稳健的分析。

了解动态神经网络

动态神经网络不同于静态网络,因为它们在训练阶段之后继续学习。它们可以独立于外部修改对其结构进行调整。一种前馈神经网络(FNN) 是最早也是最简单的动态神经网络之一。这种网络,顾名思义,只是向前反馈信息,不形成任何循环。这种类型的网络为后来动态人工神经网络的许多工作奠定了基础。在这一节中,我们将深入展示两种类型的动态网络,MLP 网络和 SOMs。

多层感知器网络

MLP 网络是具有多层的 FNN。该网络使用具有反向传播的监督学习,其中反馈被发送到早期层以帮助学习过程。一些神经元使用模拟生物神经元的非线性激活函数。一层的每个节点都完全连接到下一层。

我们将使用一个名为dermatology.arff的数据集,可以从repository.seasr.org/Datasets/UC…下载。该数据集包含 366 个用于诊断红斑-鳞状疾病的实例。它使用 34 个属性将疾病分为五个不同的类别。以下是一个示例实例:

2,2,0,3,0,0,0,0,1,0,0,0,0,0,0,3,2,0,0,0,0,0,0,0,0,0,0,3,0,0,0,1,0,55,2

最后一个字段表示疾病类别。这个数据集被分成两个文件:dermatologyTrainingSet.arffdermatologyTestingSet.arff。训练集使用原始集的前 80% (292 个实例),并以第 456 行结束。测试集是最后的 20% (74 个实例),从原始集的第 457 行开始(第 457-530 行)。

建立模型

在我们做出任何预测之前,有必要根据一组有代表性的数据来训练模型。我们将使用 Weka 类MultilayerPerceptron进行训练,并最终进行预测。首先,我们为文件名的训练和测试声明字符串,并为它们声明相应的FileReader实例。创建实例,并将最后一个字段指定为用于分类的字段:

String trainingFileName = "dermatologyTrainingSet.arff"; 
String testingFileName = "dermatologyTestingSet.arff"; 

try (FileReader trainingReader = new FileReader(trainingFileName); 
        FileReader testingReader =  
            new FileReader(testingFileName)) { 
    Instances trainingInstances = new Instances(trainingReader); 
    trainingInstances.setClassIndex( 
        trainingInstances.numAttributes() - 1); 
    Instances testingInstances = new Instances(testingReader); 
    testingInstances.setClassIndex( 
        testingInstances.numAttributes() - 1); 
    ... 
} catch (Exception ex) { 
    // Handle exceptions 
} 

然后创建了一个MultilayerPerceptron类的实例:

MultilayerPerceptron mlp = new MultilayerPerceptron(); 

我们可以设置几个模型参数,如下所示:

| 参数 | 方法 | 描述 | | 学习率 | setLearningRate | 影响训练速度 | | 动力 | setMomentum | 影响训练速度 | | 训练时间 | setTrainingTime | 用于训练模型的训练时期数 | | 隐藏层 | setHiddenLayers | 要使用的隐藏层和感知器的数量 |

如前所述,学习率会影响模型的训练速度。较大的值可以提高训练速度。如果学习率太小,那么训练时间可能会太长。如果学习率太大,那么模型可能会移过局部最小值并变得发散。也就是说,如果增量太大,我们可能会跳过一个有意义的值。你可以把它想象成一个图,在图中沿着 Y 轴的一个小的下降被忽略了,因为我们增加了太多的 T2 X T3 值。

动量也通过有效地增加学习率来影响训练速度。除了学习率之外,它还用于增加搜索最优值的动力。在局部最小值的情况下,动量有助于在寻求全局最小值的过程中摆脱最小值。

当模型学习时,它迭代地执行操作。术语历元用于指代迭代次数。希望每个历元遇到的总误差将减少到进一步的历元不再有用的程度。避免太多的纪元是理想的。

神经网络将有一个或多个隐藏层。每一层都有特定数量的感知器。setHiddenLayers方法使用一个字符串指定层和感知器的数量。例如, 3,5 将指定两个隐藏层,每层分别有三个和五个感知器。

对于本例,我们将使用以下值:

mlp.setLearningRate(0.1); 
mlp.setMomentum(0.2); 
mlp.setTrainingTime(2000); 
mlp.setHiddenLayers("3"); 

buildClassifier方法使用训练数据建立模型:

mlp.buildClassifier(trainingInstances); 

评估模型

下一步是评估模型。Evaluation类用于此目的。它的构造器将训练集作为输入,evaluateModel方法执行实际的评估。下面的代码使用测试数据集说明了这一点:

Evaluation evaluation = new Evaluation(trainingInstances); 
evaluation.evaluateModel(mlp, testingInstances); 

显示评估结果的一种简单方法是使用toSummaryString方法:

System.out.println(evaluation.toSummaryString()); 

这将显示以下输出:

Correctly Classified Instances 73 98.6486 %
Incorrectly Classified Instances 1 1.3514 %
Kappa statistic 0.9824
Mean absolute error 0.0177
Root mean squared error 0.076 
Relative absolute error 6.6173 %
Root relative squared error 20.7173 %
Coverage of cases (0.95 level) 98.6486 %
Mean rel. region size (0.95 level) 18.018 %
Total Number of Instances 74

通常,有必要试验这些参数以获得最佳结果。以下是改变感知器数量的结果:

Evaluating the model

预测其他值

一旦我们训练了一个模型,我们就可以用它来评估其他数据。在之前的测试数据集中,有一个实例失败了。在下面的代码序列中,标识了该实例,并显示了预测结果和实际结果。

测试数据集的每个实例都被用作classifyInstance方法的输入。这种方法试图预测正确的结果。将此结果与实例中包含实际值的最后一个字段进行比较。对于不匹配,显示预测值和实际值:

for (int i = 0; i < testingInstances.numInstances(); i++) { 
    double result = mlp.classifyInstance( 
        testingInstances.instance(i)); 
    if (result != testingInstances 
            .instance(i) 
            .value(testingInstances.numAttributes() - 1)) { 
        out.println("Classify result: " + result 
                + " Correct: " + testingInstances.instance(i) 
                .value(testingInstances.numAttributes() - 1)); 
        ... 
    } 
} 

对于测试集,我们得到以下输出:

Classify result: 1.0 Correct: 3.0

我们可以使用MultilayerPerceptron class' distributionForInstance方法得到预测正确的可能性。将下面的代码放到前面的循环中。它将捕获不正确的实例,这比基于数据集使用的 34 个属性实例化一个实例更容易。distributionForInstance方法接受这个实例并返回一个双精度的双元素数组。第一个元素是结果为正的概率,第二个元素是结果为负的概率:

Instance incorrectInstance = testingInstances.instance(i); 
incorrectInstance.setDataset(trainingInstances); 
double[] distribution = mlp.distributionForInstance(incorrectInstance); 
out.println("Probability of being positive: " + distribution[0]); 
out.println("Probability of being negative: " + distribution[1]); 

该实例的输出如下:

Probability of being positive: 0.00350515156929017
Probability of being negative: 0.9683660500711128

这可以为预测的可靠性提供更定量的感觉。

保存和检索模型

我们还可以保存和检索模型以备后用。要保存模型,构建模型,然后使用SerializationHelper类的静态方法write,如下面的代码片段所示。第一个参数是保存模型的文件的名称:

SerializationHelper.write("mlpModel", mlp); 

要检索模型,使用相应的read方法,如下所示:

mlp = (MultilayerPerceptron)SerializationHelper.read("mlpModel"); 

接下来,我们将学习如何使用另一种有用的神经网络方法,SOMs。

学习矢量量化

学习矢量量化()是另一种特殊类型的动态 ANN。SOMs 是 LVQ 网络的副产品,我们一会儿会讨论它。这种类型的网络实现了一种竞争类型的算法,其中获胜的神经元获得权重。这些类型的网络用于许多不同的应用中,并且被认为比其他一些人工神经网络更自然和直观。特别地,LVQ 对于基于文本的数据的分类是有效的。

基本算法首先设置神经元的数量、每个神经元的权重、神经元的学习速度以及输入向量列表。在这种情况下,向量类似于物理学中的向量,表示提供给输入层神经元的值。当训练网络时,使用向量作为输入,选择获胜神经元,并更新获胜神经元的权重。这个模型是迭代的,将继续运行,直到找到一个解决方案。

自组织地图

SOMs 是一种获取多维数据并将其减少到一个或两个维度的技术。这种压缩技术叫做矢量量化**。该技术通常包含一个可视化组件,使人们能够更好地了解数据是如何分类的。SOM 在没有监督的情况下学习。**

SOM 有利于发现聚类,这不要与分类混淆。对于分类,我们感兴趣的是在预定义的类别中找到最适合数据实例的。对于聚类,我们感兴趣的是对类别未知的实例进行分组。

SOM 使用神经元网格,通常是二维阵列或六边形网格,代表分配了权重的神经元。输入源连接到这些神经元中的每一个。然后,该技术通过几次迭代来调整分配给每个晶格成员的权重,直到找到最佳拟合。完成后,格网成员将会对输入数据集进行分类。可以查看 SOM 结果来识别类别并将新输入映射到所识别的类别之一。

使用 SOM

我们将使用 Weka 来演示 SOM。但是,它不随标准 Weka 一起安装。相反,我们需要从 sourceforge.net/projects/we… Weka 分类算法,从 www.cis.hut.fi/research/so…](sourceforge.net/projects/we… SOM 类。分类算法包括对 LVQ 的支持。关于分类算法的更多细节可以在 wekaclassalgos.sourceforge.net/](www.cis.hut.fi/research/so…](wekaclassalgos.sourceforge.net/)

要使用名为SelfOrganizingMap的 SOM 类,源代码需要在您的项目中。这个类的 Javadoc 可以在 jsalatas.ictpro.gr/weka/doc/Se… T2 找到。

我们从创建一个SelfOrganizingMap类的实例开始。接下来是读入数据并创建一个Instances对象来保存数据的代码。在这个例子中,我们将使用iris.arff文件,它可以在 Weka 数据目录中找到。请注意,一旦创建了Instances对象,我们不会像之前的 Weka 示例那样指定类索引,因为 SOM 使用无监督学习:

SelfOrganizingMap som = new SelfOrganizingMap(); 
String trainingFileName = "iris.arff"; 
try (FileReader trainingReader =  
        new FileReader(trainingFileName)) { 
    Instances trainingInstances = new Instances(trainingReader); 
    ... 
} catch (IOException ex) { 
    // Handle exceptions 
} catch (Exception ex) {
    // Handle exceptions
}

buildClusterer方法将使用训练数据集执行 SOM 算法:

 som.buildClusterer(trainingInstances); 

显示 SOM 结果

我们现在可以显示操作的结果如下:

 out.println(som.toString()); 

iris数据集使用五个属性:sepallengthsepalwidthpetallengthpetalwidthclass。前四个属性是数字,第五个属性有三个可能的值:Iris-setosaIris-versicolorIris-virginica。下面的简短输出的第一部分标识了四个集群以及每个集群中的实例数量。接下来是每个属性的统计数据:

**Self Organized Map**
**==================**
**Number of clusters: 4**
**Cluster**
**Attribute 0 1 2 3**
**(50) (42) (29) (29)**
**==============================================**
**sepallength**
**value 5.0036 6.2365 5.5823 6.9513**
**min 4.3 5.6 4.9 6.2**
**max 5.8 7 6.3 7.9**
**mean 5.006 6.25 5.5828 6.9586**
**std. dev. 0.3525 0.3536 0.3675 0.5046**
**...**
**class**
**value 0 1.5048 1.0787 2**
**min 0 1 1 2**
**max 0 2 2 2**
**mean 0 1.4524 1.069 2**
**std. dev. 0 0.5038 0.2579 0** 

这些统计数据可以提供对数据集的深入了解。如果我们想确定在一个集群中找到了哪个数据集实例,我们可以使用getClusterInstances方法返回按集群对实例进行分组的数组。如下所示,此方法用于按集群列出实例:

Instances[] clusters = som.getClusterInstances(); 
int index = 0; 
for (Instances instances : clusters) { 
    out.println("-------Custer " + index); 
    for (Instance instance : instances) { 
        out.println(instance); 
    } 
    out.println(); 
    index++; 
} 

正如我们在这个序列的简短输出中看到的,不同的iris类被分组到不同的集群中:

**-------Custer 0**
**5.1,3.5,1.4,0.2,Iris-setosa**
**4.9,3,1.4,0.2,Iris-setosa**
**4.7,3.2,1.3,0.2,Iris-setosa**
**4.6,3.1,1.5,0.2,Iris-setosa**
**...**
**5.3,3.7,1.5,0.2,Iris-setosa**
**5,3.3,1.4,0.2,Iris-setosa**
**-------Custer 1**
**7,3.2,4.7,1.4,Iris-versicolor**
**6.4,3.2,4.5,1.5,Iris-versicolor**
**6.9,3.1,4.9,1.5,Iris-versicolor**
**...**
**6.5,3,5.2,2,Iris-virginica**
**5.9,3,5.1,1.8,Iris-virginica** 
**-------Custer 2**
**5.5,2.3,4,1.3,Iris-versicolor**
**5.7,2.8,4.5,1.3,Iris-versicolor**
**4.9,2.4,3.3,1,Iris-versicolor**
**...**
**4.9,2.5,4.5,1.7,Iris-virginica**
**6,2.2,5,1.5,Iris-virginica** 
**-------Custer 3**
**6.3,3.3,6,2.5,Iris-virginica**
**7.1,3,5.9,2.1,Iris-virginica**
**6.5,3,5.8,2.2,Iris-virginica**
**...** 

可以使用 Weka GUI 界面直观地显示聚类结果。在下面的截图中,我们使用了 Weka 工作台来分析和可视化 SOM 分析的结果:

Displaying the SOM results

可以选择、定制和分析图表的单个部分,如下所示:

Displaying the SOM results

然而,在使用SOM类之前,必须使用WekaPackageManagerSOM包添加到 Weka 中。这个过程在https://WEKA . wikispaces . com/How+do+I+use+the+package+manager % 3F讨论。

如果需要将一个新实例映射到一个集群,可以使用distributionForInstance方法,如预测其他值一节所示。

**

其他网络架构和算法

我们已经讨论了一些最常见和最实用的神经网络。在这一点上,我们还想考虑一些专门的神经网络及其在各个研究领域的应用。这些类型的网络并不完全适合一个特定的类别,但可能仍然是令人感兴趣的。

k-最近邻算法

实现 k-NN 算法的人工神经网络类似于 MLP 网络,但是与赢家通吃策略相比,它显著减少了时间。这种类型的网络在设置初始权重后不需要训练算法,并且其神经元之间的连接较少。我们选择不提供这个算法实现的例子,因为它在 Weka 中的使用非常类似于 MLP 的例子。

这种类型的网络最适合分类任务。因为它利用了懒惰学习技术,将所有计算保留到信息被分类之后,所以它被认为是最简单的模型之一。在这个模型中,神经元根据它们与邻居的距离进行加权。邻居的分类是已知的,因此不需要特定的训练。

即时训练的网络

瞬时训练的神经网络 ( ITNNs )是前馈神经网络。它们很特别,因为它们为每一组独特的训练数据添加了一个新的隐藏神经元。这种类型的网络的主要优点是能够对其他问题进行归纳。

ITNNs 在短期学习情况下特别有用。特别是,这种类型的网络对于具有大型数据集的 web 搜索和其他模式识别功能非常有用。这些网络适用于时间序列预测和其他深度学习目的。

脉冲神经网络

一个脉冲神经网络 ( SNN )是一个更复杂的人工神经网络,因为它不仅考虑了神经元和信息传播,还考虑了每个事件的时间。在这些网络中,不是每个神经元在每次信息传播时都会触发,而是只有当特定神经元的膜电位达到特定阈值时才会触发。膜电位是指神经元的激活水平,非常类似于生物神经元的放电方式。

由于紧密模仿生物神经网络,SNNs 特别适合于生物学研究和应用。它们被用来模拟动物和昆虫的神经系统,并用于预测各种刺激的结果。这些网络有能力创建具有重要细节的非常复杂的模型,但是牺牲时间来实现这个目标。

级联神经网络

级联神经网络(CNN)是一种专门的监督学习算法。在这种类型中,网络最初非常小且简单。随着网络的学习,它逐渐增加新的隐藏单元。一旦添加了节点,其输入权重是恒定的,不能更改或移除。

这种类型的神经网络因其快速的学习速度和动态构建自身的能力而受到称赞。这种网络的用户不必担心拓扑设计。此外,这些网络不需要误差信息的反向传播来进行调整。

全息联想记忆

全息联想记忆 ( 哈姆)是一种特殊类型的复杂神经网络。这是一种与人类自然记忆和视觉分析相关的特殊类型的网络。这种网络对于模式识别和联想记忆任务特别有用,并且可以应用于光学计算。

哈姆试图密切模仿人类的视觉化和模式识别。在这个网络中,不需要迭代就可以学习刺激-反应模式,也不需要误差的反向传播。与本章中讨论的其他网络不同,HAM 不表现出相同类型的连接行为。相反,刺激-反应模式可以存储在单个神经元中。

反向传播和神经网络

反向传播算法是另一种用于训练神经网络的监督学习技术。顾名思义,这种算法计算出计算出的输出误差,然后反向改变每个神经元的权重。反向传播主要用于 MLP 网络。需要注意的是,在使用反向传播之前,必须先进行正向传播。

在其最基本的形式中,该算法包括四个步骤:

  1. 对给定的一组输入执行前向传播。
  2. 计算每个输出的误差值。
  3. 根据每个节点的计算误差更改权重。
  4. 再次执行正向传播。

当输出与预期输出匹配时,该算法完成。

总结

在这一章中,我们已经提供了人工神经网络的广泛概述,以及几个具体实施的详细检查。我们首先讨论了神经网络的基本属性、训练算法和神经网络结构。

接下来,我们提供了一个使用 Java 实现 XOR 问题的简单静态神经网络的例子。此示例详细解释了用于构建和训练网络的代码,包括训练过程中权重调整背后的一些数学计算。然后,我们讨论了动态神经网络,并提供了两个深入的例子,MLP 和 SOM 网络。他们使用 Weka 工具来创建和训练网络。

最后,我们以对其他网络架构和算法的讨论结束了本章。我们选择了一些比较流行的网络来总结和探索每种类型最有用的情况。我们还在这一节中讨论了反向传播。

在下一章中,我们将在这个介绍的基础上展开,并看看神经网络的深度学习。**

八、深度学习

在本章中,我们将重点讨论神经网络,通常称为深度学习网络 ( DLNs )。这种类型的网络被表征为多层神经网络。这些图层中的每一个都基于前一个图层的输出,可能会识别数据集的要素和子要素。以这种方式创建特征层次。

dln 通常处理非结构化和未标记的数据,这些数据构成了当今世界的大部分数据。DLN 将获取这些非结构化数据,识别特征,并尝试重建原始输入。这种方法用受限玻尔兹曼机 ( RBMs )中的受限玻尔兹曼机深度自动编码器中的自动编码器来说明。自动编码器获取数据集并对其进行有效压缩。然后对其进行解压缩,以重建原始数据集。

DLN 也可以用于预测分析。DLN 的最后一步将使用激活函数来生成由几个类别之一表示的输出。当与新数据一起使用时,模型将尝试基于先前训练的模型对输入进行分类。

DLN 的一项重要任务是确保模型的准确性和误差最小化。与简单的神经网络一样,每一层都使用权重和偏差。随着权重值的调整,可能会引入误差。一种调整权重的技术使用梯度下降。这可以认为是变化的斜率。想法是修改权重以最小化误差。这是一种加速学习过程的优化技术。

在本章的后面,我们将检查卷积神经网络(CNN),并简要讨论递归神经网络 ( RNN )。卷积网络模拟了视觉皮层,因为每个神经元都可以根据某个区域的信息进行交互并做出决定。递归网络不仅基于前一层的输出,还基于前一层中执行的计算来处理信息。

有几个支持深度学习的库,包括:

ND4J 是一个较低级别的库,实际上在其他项目中使用,包括 DL4J。Encog 可能不如 DL4J 支持得好,但确实提供了对深度学习的支持。

本章使用的例子都是基于深度学习 Java(DL4J)(deeplearning4j.org)API,并有 ND4J 的支持。这个库为许多与深度学习相关的算法提供了很好的支持。因此,下一节将解释许多深度学习算法共有的基本任务,如加载数据、训练模型和测试模型。

Deeplearning4j 架构

在本节中,我们将讨论它的体系结构,并解决使用 API 时执行的几个常见任务。DLN 通常从创建一个MultiLayerConfiguration实例开始,它定义了网络或模型。网络由多层组成。超参数用于配置网络,是影响学习速度、用于层的激活函数以及如何初始化权重的变量。

与神经网络一样,基本的 DLN 过程包括:

  • 获取和操作数据
  • 配置和构建模型
  • 训练模型
  • 测试模型

我们将在接下来的小节中研究这些任务。

注意

本节中的代码示例并不打算在这里输入和执行。相反,这些例子是我们将使用的后来模型的片断。

获取和处理数据

DL4J API 有许多获取数据的技术。我们将重点关注我们将在示例中使用的那些特定技术。DL4J 项目使用的数据集通常使用二值化归一化进行修改。二进制化将数据转换为 1 和 0。归一化将数据转换为介于 10 之间的值。

馈送给 DLN 的数据被转换成一组数字。这些数字被称为向量。这些向量由行数可变的一列矩阵组成。创建矢量的过程被称为矢量化

Canova(【deeplearning4j.org/canova.html… DL4J 库。它适用于许多不同类型的数据集。它已经与data vec(deeplearning4j.org/datavec)、矢量化和提取、转换和加载 ( ETL )库合并。

在本节中,我们将重点介绍如何读入 CSV 数据。

读入 CSV 文件

ND4J 提供了CSVRecordReader类,这对于读取 CSV 数据很有用。它有三个重载的构造函数。我们要演示的是传递了两个参数。第一个是第一次读取文件时要跳过的行数,第二个是保存用于解析文本的分隔符的字符串。

在下面的代码中,我们创建了类的一个新实例,其中没有跳过任何行,只使用逗号作为分隔符:

RecordReader recordReader = new CSVRecordReader(0, ","); 

该类实现了RecordReader接口。它有一个被传递了一个FileSplit类实例的initialize方法。它的一个构造函数被传递了一个引用数据集的File对象的实例。FileSplit类帮助分割用于训练和测试的数据。在本例中,我们为一个名为car.txt的文件初始化阅读器,我们将在准备数据部分使用该文件:

recordReader.initialize(new FileSplit(new File("car.txt"))); 

为了处理数据,我们需要一个迭代器,比如下面显示的DataSetIterator实例。这个类拥有大量重载的构造函数。在下面的例子中,第一个参数是RecordReader实例。接下来是三个论点。第一个是批量大小,即一次检索的记录数量。下一个是记录最后一个属性的索引。最后一个参数是数据集表示的类的数量:

DataSetIterator iterator =  
    new RecordReaderDataSetIterator(recordReader, 1728, 6, 4); 

如果我们使用数据集进行回归,文件记录的最后一个属性将保存一个类值。这正是我们以后使用它的方式。类别参数的数目仅用于回归。

在下一个代码序列中,我们将把数据集分成两组:一组用于训练,一组用于测试。从next方法开始,这个方法从数据源返回下一个数据集。数据集的大小取决于之前使用的批处理大小。shuffle方法使输入随机化,而splitTestAndTrain方法返回SplitTestAndTrain类的一个实例,我们用它来获得训练和测试数据集。splitTestAndTrain方法的参数指定了用于训练的数据的百分比。

DataSet dataset = iterator.next(); 
dataset.shuffle(); 
SplitTestAndTrain testAndTrain = dataset.splitTestAndTrain(0.65); 
DataSet trainingData = testAndTrain.getTrain(); 
DataSet testData = testAndTrain.getTest(); 

然后我们可以将这些数据集用于一个模型。

配置和构建模型

DL4J 经常使用MultiLayerConfiguration类来定义模型的配置,使用MultiLayerNetwork类来表示模型。这些类提供了一种构建模型的灵活方式。

在下面的例子中,我们将演示这些类的用法。从MultiLayerConfiguration类开始,我们发现在流畅的风格中使用了几种方法。我们将很快提供关于这些方法的更多细节。但是,请注意,该模型定义了两个层:

MultiLayerConfiguration conf =  
    new NeuralNetConfiguration.Builder() 
        .iterations(1000) 
        .activation("relu") 
        .weightInit(WeightInit.XAVIER) 
        .learningRate(0.4) 
        .list() 
        .layer(0, new DenseLayer.Builder() 
                .nIn(6).nOut(3) 
                .build()) 
        .layer(1, new OutputLayer 
                .Builder(LossFunctions.LossFunction 
                        .NEGATIVELOGLIKELIHOOD) 
                .activation("softmax") 
                .nIn(3).nOut(4).build()) 
        .backprop(true).pretrain(false) 
        .build(); 

nInnOut方法指定一个层的输入和输出数量。

在 ND4J 中使用超参数

构建器类在 DL4J 中很常见。在前面的例子中,使用了NeuralNetConfiguration.Builder类。这里使用的方法只是众多可用方法中的几种。在下表中,我们描述了其中的几种:

| 方法 | 用途 | | iterations | 控制执行优化迭代的次数 | | activation | 这是使用的激活功能 | | weightInit | 用于初始化模型的初始权重 | | learningRate | 控制模型学习的速度 | | List | 创建一个NeuralNetConfiguration.ListBuilder类的实例,这样我们可以添加层 | | Layer | 创建新层 | | backprop | 当设置为真时,它启用反向传播 | | pretrain | 当设置为 true 时,它将预训练模型 | | Build | 执行实际的构建过程 |

让我们更仔细地研究一下层是如何创建的。在这个例子中,list方法返回一个NeuralNetConfiguration.ListBuilder实例。它的layer方法有两个参数。第一个是图层的编号,这是一个从零开始的编号方案。第二个是Layer实例。

这里使用了两个不同的层和两个不同的构建器:一个DenseLayer.Builder和一个OutputLayer.Builder实例。DL4J 中有几种类型的层可用。构建器的构造函数的自变量可以是一个损失函数,与输出层的情况一样,接下来将对此进行解释。

在反馈网络中,神经网络的猜测与所谓的基本事实进行比较,这就是误差。该误差用于通过修改权重和偏差来更新网络。损失函数,也称为目标成本函数,测量差异。

DL4J 支持多种损失函数:

  • MSE:在线性回归中,MSE 代表均方误差
  • EXPLL:在泊松回归中,EXPLL 代表指数对数似然
  • XENT:在二元分类中,XENT 代表交叉熵
  • 这代表多类交叉熵
  • 这代表 RMSE 交叉熵
  • 这代表平方损失
  • 这代表重建交叉熵
  • NEGATIVELOGLIKELIHOOD:表示负对数似然
  • CUSTOM:定义自己的损失函数

构建器实例使用的其余方法是激活函数、层的输入和输出数量以及创建层的build方法。

多层网络的每一层都需要满足以下要求:

  • 输入:通常以输入向量的形式
  • 权重:也叫系数
  • Bias :用于确保一层中至少有一些节点被激活
  • 激活功能:决定一个节点是否触发

有许多不同类型的激活功能,每一种都可以解决特定类型的问题。

激活函数用于确定神经元是否触发。支持多种功能,包括relu(整流线性)tanh``sigmoid``softmax``hardtanh``leakyrelu``maxout``softsign``softplus

注意

一个有趣的激活函数列表和图表可以在http://stats . stack exchange . com/questions/115258/comprehensive-list-of-activation-functions-in-neural-networks-with-prosen.wikipedia.org/wiki/Activa…找到。

实例化网络模型

接下来,使用定义的配置创建一个MultiLayerNetwork实例。模型被初始化,并且它的监听器被设置。ScoreIterationListener实例将显示模型火车的信息,我们很快就会看到。其构造函数的参数指定了该信息的显示频率:

MultiLayerNetwork model = new MultiLayerNetwork(conf); 
model.init(); 
model.setListeners(new ScoreIterationListener(100)); 

我们现在准备训练模型。

训练一个模特

这实际上是一个相当简单的步骤。fit方法执行训练:

model.fit(trainingData); 

当执行时,将使用与模型相关联的任何监听器来生成输出,就像前面的情况一样,其中使用了一个ScoreIterationListener实例。

如何使用fit方法的另一个例子是遍历数据集的过程,如下所示。在这个例子中,使用了一系列数据集。这是自动编码器的一部分,其输出旨在匹配输入,如深度自动编码器一节所述。用作fit方法参数的数据集同时使用输入和预期输出。在这种情况下,它们与由getFeatureMatrix方法提供的相同:

while (iterator.hasNext()) { 
    DataSet dataSet = iterator.next(); 
    model.fit(new DataSet(dataSet.getFeatureMatrix(), 
            dataSet.getFeatureMatrix())); 
} 

对于较大的数据集,有必要对模型进行多次预训练,以获得准确的结果。这通常是并行执行的,以减少培训时间。该选项通过图层类的pretrain方法设置。

测试模型

使用Evaluation类和训练数据集对模型进行评估。使用指定类数量的参数创建一个Evaluation实例。使用output方法将测试数据输入模型。eval方法获取模型的输出,并将其与测试数据类进行比较,以生成统计数据:

Evaluation evaluation = new Evaluation(4); 
INDArray output = model.output(testData.getFeatureMatrix()); 
evaluation.eval(testData.getLabels(), output); 
out.println(evaluation.stats()); 

输出将类似于以下内容:

==========================Scores===================================
Accuracy: 0.9273
Precision: 0.854
Recall: 0.8323
F1 Score: 0.843

这些统计数据详细如下:

  • 这是对返回正确答案的频率的测量。
  • Precision:这是一个肯定回答是正确的概率的量度。
  • Recall:这衡量如果给出一个正例,结果被正确分类的可能性。
  • F1 Score:这是网络结果正确的概率。这是回忆和精确的调和平均值。它的计算方法是将真阳性的数量除以真阳性和假阴性的总和。

我们将使用Evaluation类来确定我们模型的质量。使用称为 f1 的度量,其值范围从 01 ,其中 1 代表最佳质量。

深度学习和回归分析

神经网络可用于执行回归分析。然而,其他技术(见前面章节)可能提供更有效的解决方案。使用回归分析,我们希望根据几个输入变量来预测结果。

我们可以使用由单个神经元组成的输出层来执行回归分析,该输出层将加权输入加上前一个隐藏层的偏差相加。因此,结果是代表回归的单个值。

准备数据

我们将使用汽车评估数据库来演示如何根据一系列属性来预测汽车的可接受性。包含我们将使用的数据的文件可以从以下位置下载:http://archive . ics . UCI . edu/ml/machine-learning-databases/car/car . data。它包括汽车数据,如价格、乘客数量和安全信息,以及对其整体质量的评估。这是后一个因素,质量,我们将试图预测。接下来显示了每个属性中以逗号分隔的值,以及替换值。因为模型需要数字数据,所以需要替换:

| 属性 | 原始值 | 替代值 | | 买价 | vhigh, high, med, low | 3,2,1,0 | | 维持价格 | vhigh, high, med, low | 3,2,1,0 | | 门的数量 | 2, 3, 4, 5-more | 2,3,4,5 | | 座位 | 2, 4, more | 2,4,5 | | 货舱 | small, med, big | 0,1,2 | | 安全 | low, med, high | 0,1,2 |

文件中有 1,728 个实例。这些汽车分为四个等级:

| | 实例数量 | 实例百分比 | 原始值 | 替代值 | | 不能接受的 | 1210 | 70.023% | unacc | 0 | | 可接受的 | 384 | 22.222% | acc | 1 | | 好的 | 69 | 3.99% | good | 2 | | 很好 | 65 | 3.76% | v-good | 3 |

设置类别

我们从定义一个CarRegressionExample类开始,如下所示,在这里创建了一个类的实例,并且在它的默认构造函数中执行工作:

public class CarRegressionExample { 

    public CarRegressionExample() { 

        try { 

            ... 

        } catch (IOException | InterruptedException ex) { 

            // Handle exceptions 

        } 

    } 

    public static void main(String[] args) { 

        new CarRegressionExample(); 

    } 

} 

读取和准备数据

第一项任务是读入数据。我们将使用CSVRecordReader类来获取数据,正如在读取 CSV 文件中所解释的:

RecordReader recordReader = new CSVRecordReader(0, ","); 

recordReader.initialize(new FileSplit(new File("car.txt"))); 

DataSetIterator iterator = new 

  RecordReaderDataSetIterator(recordReader, 1728, 6, 4); 

有了这个数据集,我们将把数据分成两组。65%的数据用于训练,其余用于测试:

DataSet dataset = iterator.next(); 

dataset.shuffle(); 

SplitTestAndTrain testAndTrain = dataset.splitTestAndTrain(0.65); 

DataSet trainingData = testAndTrain.getTrain(); 

DataSet testData = testAndTrain.getTest(); 

现在需要对数据进行规范化:

DataNormalization normalizer = new NormalizerStandardize(); 

normalizer.fit(trainingData); 

normalizer.transform(trainingData); 

normalizer.transform(testData); 

我们现在准备构建模型。

建立模型

使用一系列的NeuralNetConfiguration.Builder方法创建一个MultiLayerConfiguration实例。下面是用的骰子。我们将讨论代码后面的各个方法。请注意,此配置使用了两层。最后一层使用softmax激活函数,用于回归分析:

MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder() 

        .iterations(1000) 

        .activation("relu") 

        .weightInit(WeightInit.XAVIER) 

        .learningRate(0.4) 

        .list() 

        .layer(0, new DenseLayer.Builder() 

                .nIn(6).nOut(3) 

                .build()) 

        .layer(1, new OutputLayer 

                .Builder(LossFunctions.LossFunction 

                        .NEGATIVELOGLIKELIHOOD) 

                .activation("softmax") 

                .nIn(3).nOut(4).build()) 

        .backprop(true).pretrain(false) 

        .build(); 

创建了两个层。首先是输入层。DenseLayer.Builder类用于创建这一层。DenseLayer类是前馈和全连接层。创建的层使用六个汽车属性作为输入。输出由输入输出层的三个神经元组成,为了方便起见,这里复制了三个神经元:

.layer(0, new DenseLayer.Builder() 

        .nIn(6).nOut(3) 

        .build()) 

第二层是用OutputLayer.Builder类创建的输出层。它使用损失函数作为其构造函数的参数。使用softmax激活函数是因为我们正在执行回归,如下所示:

.layer(1, new OutputLayer 

        .Builder(LossFunctions.LossFunction 

                .NEGATIVELOGLIKELIHOOD) 

        .activation("softmax") 

        .nIn(3).nOut(4).build()) 

接下来,使用配置创建一个MultiLayerNetwork实例。模型被初始化,它的监听器被设置,然后调用fit方法来执行实际的训练。ScoreIterationListener实例将显示模型火车的信息,我们将很快在这个例子的输出中看到。ScoreIterationListener构造函数的参数指定了信息显示的频率:

MultiLayerNetwork model = new MultiLayerNetwork(conf); 

model.init(); 

model.setListeners(new ScoreIterationListener(100)); 

model.fit(trainingData); 

我们现在准备评估模型。

评估模型

在接下来的代码序列中,我们根据训练数据集评估模型。使用指定有四个类的参数创建一个Evaluation实例。使用output方法将测试数据输入模型。eval方法获取模型的输出,并将其与测试数据类进行比较,以生成统计数据。getLabels方法返回预期值:

Evaluation evaluation = new Evaluation(4); 

INDArray output = model.output(testData.getFeatureMatrix()); 

evaluation.eval(testData.getLabels(), output); 

out.println(evaluation.stats()); 

下面是训练的输出,它是由ScoreIterationListener类产生的。但是,您获得的值可能会因数据的选择和分析方式而有所不同。请注意,分数随着迭代而提高,但在大约 500 次迭代后趋于平稳:

12:43:35.685 [main] INFO o.d.o.l.ScoreIterationListener - Score at iteration 0 is 1.443480901811554
12:43:36.094 [main] INFO o.d.o.l.ScoreIterationListener - Score at iteration 100 is 0.3259061845624861
12:43:36.390 [main] INFO o.d.o.l.ScoreIterationListener - Score at iteration 200 is 0.2630572026049783
12:43:36.676 [main] INFO o.d.o.l.ScoreIterationListener - Score at iteration 300 is 0.24061281470878784
12:43:36.977 [main] INFO o.d.o.l.ScoreIterationListener - Score at iteration 400 is 0.22955121170274934
12:43:37.292 [main] INFO o.d.o.l.ScoreIterationListener - Score at iteration 500 is 0.22249920540161677
12:43:37.575 [main] INFO o.d.o.l.ScoreIterationListener - Score at iteration 600 is 0.2169898450109222
12:43:37.872 [main] INFO o.d.o.l.ScoreIterationListener - Score at iteration 700 is 0.21271599814600958
12:43:38.161 [main] INFO o.d.o.l.ScoreIterationListener - Score at iteration 800 is 0.2075677126088741
12:43:38.451 [main] INFO o.d.o.l.ScoreIterationListener - Score at iteration 900 is 0.20047317735870715

接下来是如下所示的stats方法的结果。第一部分报告示例如何分类,第二部分显示各种统计数据:

Examples labeled as 0 classified by model as 0: 397 times
Examples labeled as 0 classified by model as 1: 10 times
Examples labeled as 0 classified by model as 2: 1 times
Examples labeled as 1 classified by model as 0: 8 times
Examples labeled as 1 classified by model as 1: 113 times
Examples labeled as 1 classified by model as 2: 1 times
Examples labeled as 1 classified by model as 3: 1 times
Examples labeled as 2 classified by model as 1: 7 times
Examples labeled as 2 classified by model as 2: 21 times
Examples labeled as 2 classified by model as 3: 14 times
Examples labeled as 3 classified by model as 1: 2 times
Examples labeled as 3 classified by model as 3: 30 times
==========================Scores===================================Accuracy: 0.9273
Precision: 0.854
Recall: 0.8323
F1 Score: 0.843
===================================================================

回归模型对这个数据集做了合理的工作。

受限玻尔兹曼机器

RBM 经常被用作多层深层信念网络的一部分。RBM 的输出被用作另一层的输入。重复使用 RBM,直到到达最后一层。

注意

深度信念网络 ( DBNs )由几个 RBM 堆叠在一起组成。每个隐藏层为后续层提供输入。在每一层中,节点不能横向通信,它实质上变成了其他单层网络的网络。dbn 特别有助于分类、聚类和识别图像数据。

术语连续受限玻尔兹曼机,指的是使用非整数数值的 RBM。输入数据被标准化为 0 到 1 之间的值。

输入层的每个节点都连接到第二层的每个节点。同一层中没有节点相互连接。也就是说,不存在层内通信。这就是受限的含义。

Restricted Boltzmann Machines

可见图层的输入节点数取决于所解决的问题。例如,如果我们正在查看一个有 256 个像素的图像,那么我们将需要 256 个输入节点。对于图像,这是图像的行数乘以列数。

隐藏层应该比输入层包含更少的神经元。使用接近相同数量的神经元有时会导致身份函数的构建。过多的神经元可能会导致过度拟合。这意味着具有大量输入的数据集将需要多个图层。较小的输入大小导致需要较少的层。

随机的,即随机的,值被分配给每个节点的权重。节点的值乘以其权重,然后添加到偏差中。该值与来自其他输入节点的组合输入相结合,然后被馈入激活函数,在那里产生输出值。

重建 RBM

RBM 技术经历了一个重建阶段。这是激活被反馈到第一层并乘以用于输入的相同权重的地方。来自第二层的每个节点的这些值的总和,加上另一个偏差,表示原始输入的近似值。想法是训练模型以最小化原始输入值和反馈值之间的差异。

Reconstruction in an RBM

值的差异被视为错误。重复该过程,直到达到误差最小值。您可以将重构视为对原始输入的猜测。这些猜测本质上是原始输入的概率分布。这被称为生成学习,与使用分类技术的鉴别学习相反。

在多层模型中,每一层都可以用来从本质上识别一个特征。在随后的层中,可以识别或生成特征的组合。以这种方式,可以分析看似随机的一组像素值来识别树叶、树叶、树干以及树的叶脉。

RBM 的输出是一个基本上代表百分比的值。如果它不是零,那么机器已经学习了关于输入的一些东西。

配置 RBM

我们将研究两种不同的 RBM 组态。第一个是最小的,我们将在深度自动编码器中再次看到它。第二种方法使用了几种额外的方法,并提供了对其各种配置方式的更多见解。

以下语句使用RBM.Builder类创建一个新层。基于图像的行数和列数计算输入。输出大,包含1000个神经元。损失函数是RMSE_XENT。这种损失函数对某些分类问题更有效:

.layer(0, new RBM.Builder() 

    .nIn(numRows * numColumns).nOut(1000) 

    .lossFunction(LossFunctions.LossFunction.RMSE_XENT) 

    .build()) 

接下来是一个更复杂的 RBM。我们不会在这里详细介绍这些方法,但会在后面的示例中看到它们的使用:

.layer(new RBM.Builder() 

    .l2(1e-1).l1(1e-3) 

    .nIn(numRows * numColumns 

    .nOut(outputNum) 

    .activation("relu") 

    .weightInit(WeightInit.RELU)  

    .lossFunction(LossFunctions.LossFunction 

        .RECONSTRUCTION_CROSSENTROPY).k(3) 

    .hiddenUnit(HiddenUnit.RECTIFIED) 

    .visibleUnit(VisibleUnit.GAUSSIAN) 

    .updater(Updater.ADAGRAD) 

        .gradientNormalization( 

             GradientNormalization.ClipL2PerLayer) 

    .build()) 

单层RBM并不总是有用的。通常需要多层自动编码器。我们将在下一节研究这个选项。

深度自动编码器

自动编码器用于特征选择和提取。它由两个对称的 dbn 组成。网络的前半部分由几层组成,执行编码。网络的第二部分执行解码。自动编码器的每一层都是一个 RBM。下图对此进行了说明:

Deep autoencoders

编码序列的目的是将原始输入压缩到更小的向量空间中。上图的中间层就是这个压缩层。这些中间向量可以被认为是数据集的可能特征。该编码也被称为预训练半部分。它是中间 RBM 层的输出,不执行分类。

编码器的第一层将使用比数据集更多的输入。这具有扩展数据集特征的效果。sigmoid-belief 单元是每层使用的一种非线性变换形式。该单元不能准确地将信息表示为真实值。然而,使用更多的投入,它能够做得更好。

网络的后半部分执行解码,有效地重构输入。这是一个前馈网络,使用与编码部分中相应层相同的权重。然而,权重是转置的,并且不是随机初始化的。下半年的训练率需要定得低一些。

自动编码器对于数据压缩和搜索非常有用。模型前半部分的输出是压缩的,因此有利于存储和传输。稍后,它可以被解压缩,正如我们将在第 10 章视听分析中演示的。这有时被称为语义散列。

如果一系列输入(如图像或声音)已经被压缩和存储,那么新的输入可以被压缩并与存储的值匹配以找到最佳匹配。自动编码器也可以用于其他信息检索任务。

在 DL4J 中构建自动编码器

这个例子改编自deeplearning4j.org/deepautoenc…。我们首先使用一个 try-catch 块来处理可能出现的错误,并使用一些变量声明。这个例子使用了Mnist(yann.lecun.com/exdb/mnist/)数据集,这是一组包含手写数字的图像。每幅图像由2828像素组成。声明一个迭代器来访问数据:

try { 

    final int numberOfRows = 28; 

    final int numberOfColumns = 28; 

    int seed = 123; 

    int numberOfIterations = 1; 

    iterator = new MnistDataSetIterator( 

        1000, MnistDataFetcher.NUM_EXAMPLES, true); 

    ... 

} catch (IOException ex) { 

    // Handle exceptions 

} 

配置网络

使用NeuralNetConfiguration.Builder()类创建网络配置。创建了十层,其中输入层由1000神经元组成。这大于 28×28 像素输入,并且用于补偿在每一层中使用的 sigmoid 信念单元。

随后的每一层都变小,直到到达第四层。这一层代表编码过程的最后一步。对于第五层,解码过程开始,随后的层变得更大。最后一层使用1000神经元。

模型的每一层都使用一个 RBM 实例,除了最后一层,它是使用OutputLayer.Builder类构建的。配置代码如下:

MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder() 

        .seed(seed) 

        .iterations(numberOfIterations) 

        .optimizationAlgo( 

           OptimizationAlgorithm.LINE_GRADIENT_DESCENT) 

        .list() 

        .layer(0, new RBM.Builder() 

            .nIn(numberOfRows * numberOfColumns).nOut(1000) 

            .lossFunction(LossFunctions.LossFunction.RMSE_XENT) 

            .build()) 

        .layer(1, new RBM.Builder().nIn(1000).nOut(500) 

            .lossFunction(LossFunctions.LossFunction.RMSE_XENT) 

            .build()) 

        .layer(2, new RBM.Builder().nIn(500).nOut(250) 

            .lossFunction(LossFunctions.LossFunction.RMSE_XENT) 

            .build()) 

        .layer(3, new RBM.Builder().nIn(250).nOut(100) 

            .lossFunction(LossFunctions.LossFunction.RMSE_XENT) 

            .build()) 

        .layer(4, new RBM.Builder().nIn(100).nOut(30) 

            .lossFunction(LossFunctions.LossFunction.RMSE_XENT) 

            .build()) //encoding stops 

        .layer(5, new RBM.Builder().nIn(30).nOut(100) 

            .lossFunction(LossFunctions.LossFunction.RMSE_XENT) 

            .build()) //decoding starts 

        .layer(6, new RBM.Builder().nIn(100).nOut(250) 

            .lossFunction(LossFunctions.LossFunction.RMSE_XENT) 

            .build()) 

        .layer(7, new RBM.Builder().nIn(250).nOut(500) 

            .lossFunction(LossFunctions.LossFunction.RMSE_XENT) 

            .build()) 

        .layer(8, new RBM.Builder().nIn(500).nOut(1000) 

            .lossFunction(LossFunctions.LossFunction.RMSE_XENT) 

            .build()) 

        .layer(9, new OutputLayer.Builder( 

                LossFunctions.LossFunction.RMSE_XENT).nIn(1000) 

                .nOut(numberOfRows * numberOfColumns).build()) 

        .pretrain(true).backprop(true) 

        .build(); 

建立和培训网络

然后创建并初始化模型,并设置分数迭代监听器:

model = new MultiLayerNetwork(conf); 

model.init(); 

model.setListeners(Collections.singletonList( 

        (IterationListener) new ScoreIterationListener())); 

使用fit方法训练模型:

while (iterator.hasNext()) { 

    DataSet dataSet = iterator.next(); 

    model.fit(new DataSet(dataSet.getFeatureMatrix(),  

            dataSet.getFeatureMatrix())); 

} 

保存和检索网络

保存模型是很有用的,这样它可以用于以后的分析。这是使用ModelSerializer类的writeModel方法完成的。它接受model实例和modelFile实例,以及一个boolean参数,该参数指定是否应该保存模型的更新程序。更新器是用于调整某些模型参数的学习算法:

modelFile = new File("savedModel"); 

ModelSerializer.writeModel(model, modelFile, true); 

可以使用以下代码检索模型:

modelFile = new File("savedModel"); 

MultiLayerNetwork model = ModelSerializer.restoreMultiLayerNetwork(modelFile); 

专业自动编码器

自动编码器有专门的版本。当自动编码器使用比输入更多的隐藏层时,它可以学习 identity 函数,该函数总是返回与用作函数输入的值相同的值。为了避免这个问题,使用了对自动编码器去噪自动编码器的扩展;它随机修改引入噪声的输入。引入的噪声量因输入数据集而异。一个堆叠去噪自动编码器 ( SdA )是一系列去噪自动编码器串在一起。

卷积网络

CNN 是模仿动物视觉皮层的前馈网络。视觉皮层排列着重叠的神经元,因此在这种类型的网络中,神经元也排列在重叠的部分,称为感受野。由于它们的设计模型,它们只需最少的预处理或先验知识就能工作,这种缺少人工干预的特性使它们特别有用。

这种类型的网络经常用于图像和视频识别应用。它们可用于分类、聚类和对象识别。CNN 还可以通过实现光学字符识别 ( OCR )应用于文本分析。CNN 一直是机器学习运动的驱动力,部分原因在于其在实际情况中的广泛适用性。

我们将使用 DL4J 演示一个 CNN。该流程将与我们在 DL4J 部分的构建自动编码器中使用的流程非常相似。我们将再次使用Mnist数据集。该数据集包含图像数据,因此非常适合卷积网络。

建立模型

首先,我们需要创建一个新的DataSetIterator来处理数据。MnistDataSetIterator构造函数的参数是批量大小,在本例中是1000,以及要处理的样本总数。然后,我们得到下一个数据集,随机排列数据,并分割数据进行测试和训练。正如我们在本章前面所讨论的,我们通常使用 65%的数据来训练数据,剩余的 35%用于测试:

DataSetIterator iter = new MnistDataSetIterator(1000,  
MnistDataFetcher.NUM_EXAMPLES); 
DataSet dataset = iter.next(); 
dataset.shuffle(); 
SplitTestAndTrain testAndTrain = dataset.splitTestAndTrain(0.65); 
DataSet trainingData = testAndTrain.getTrain(); 
DataSet testData = testAndTrain.getTest(); 

然后我们对两组数据进行归一化处理:

DataNormalization normalizer = new NormalizerStandardize(); 
normalizer.fit(trainingData); 
normalizer.transform(trainingData); 
normalizer.transform(testData); 

接下来,我们可以建立我们的网络。如前所示,我们将再次使用带有一系列NeuralNetConfiguration.Builder方法的MultiLayerConfiguration实例。我们将在下面的代码序列之后讨论各个方法。注意,最后一层再次使用softmax激活函数进行回归分析:

MultiLayerConfiguration.Builder builder = new    
          NeuralNetConfiguration.Builder() 
     .seed(123) 
     .iterations(1) 
     .regularization(true).l2(0.0005) 
     .weightInit(WeightInit.XAVIER)  
     .optimizationAlgo(OptimizationAlgorithm 
           .STOCHASTIC_GRADIENT_DESCENT) 
     .updater(Updater.NESTEROVS).momentum(0.9) 
     .list() 
     .layer(0, new ConvolutionLayer.Builder(5, 5) 
           .nIn(6) 
           .stride(1, 1) 
           .nOut(20) 
           .activation("identity") 
           .build()) 
     .layer(1, new SubsamplingLayer.Builder(
                SubsamplingLayer.PoolingType.MAX) 
           .kernelSize(2, 2) 
           .stride(2, 2) 
           .build()) 
     .layer(2, new ConvolutionLayer.Builder(5, 5) 
           .stride(1, 1) 
           .nOut(50) 
           .activation("identity") 
           .build()) 
     .layer(3, new SubsamplingLayer.Builder(
                SubsamplingLayer.PoolingType.MAX) 
           .kernelSize(2, 2) 
           .stride(2, 2) 
           .build()) 
     .layer(4, new DenseLayer.Builder().activation("relu") 
           .nOut(500).build()) 
     .layer(5, new OutputLayer.Builder(
                LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD) 
           .nOut(10) 
           .activation("softmax") 
           .build()) 
     .backprop(true).pretrain(false); 

第一层,图层0,为了方便起见,它使用了ConvolutionLayer.Builder方法。卷积层的输入是图像高度、宽度和通道数的乘积。在标准 RGB 图像中,有三个通道。nIn方法获取通道的数量。nOut方法指定了期望的20输出:

.layer(0, new ConvolutionLayer.Builder(5, 5) 
        .nIn(6) 
        .stride(1, 1) 
        .nOut(20) 
        .activation("identity") 
        .build()) 

13都是二次采样层。这些层跟随卷积层,它们本身不进行真正的卷积。它们返回一个值,即该输入区域的最大值:

.layer(1, new SubsamplingLayer.Builder( 
            SubsamplingLayer.PoolingType.MAX) 
        .kernelSize(2, 2) 
        .stride(2, 2) 
        .build()) 
                        ... 
.layer(3, new SubsamplingLayer.Builder( 
            SubsamplingLayer.PoolingType.MAX) 
        .kernelSize(2, 2) 
        .stride(2, 2) 
        .build()) 

2也是像层0一样的卷积层。请注意,我们没有指定该层中的通道数量:

.layer(2, new ConvolutionLayer.Builder(5, 5) 
        .nOut(50) 
        .activation("identity") 
        .build()) 

第四层使用了DenseLayer.Builder类,就像我们前面的例子一样。如前所述,DenseLayer类是一个前馈和全连接层:

.layer(4, new DenseLayer.Builder().activation("relu") 
        .nOut(500).build()) 

5是一个OutputLayer实例,使用softmax自动化:

.layer(5, new OutputLayer.Builder( 
            LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD) 
        .nOut(10) 
        .activation("softmax") 
        .build()) 
        .backprop(true).pretrain(false); 

最后,我们创建了一个ConvolutionalLayerSetup类的新实例。我们传递构建器对象和图像的尺寸(28 x 28)。我们还传递通道的数量,在本例中,1:

new ConvolutionLayerSetup(builder, 28, 28, 1); 

我们现在可以配置和适应我们的模型。我们再次使用MultiLayerConfigurationMultiLayerNetwork类来构建我们的网络。我们设置监听器,然后遍历我们的数据。对于每个DataSet,我们执行fit方法:

MultiLayerConfiguration conf = builder.build(); 
MultiLayerNetwork model = new MultiLayerNetwork(conf); 
model.init(); 
model.setListeners(Collections.singletonList((IterationListener) 
  new ScoreIterationListener(1/5))); 

while (iter.hasNext()) { 
    DataSet next = iter.next(); 
    model.fit(new DataSet(next.getFeatureMatrix(), next.getLabels())); 
} 

我们现在准备评估我们的模型。

评估模型

为了评估我们的模型,我们使用了Evaluation类。我们从模型中获取输出,并将其与数据集的标签一起发送给eval方法。然后,我们执行stats方法来获取我们网络的统计信息:

Evaluation evaluation = new Evaluation(4); 
INDArray output = model.output(testData.getFeatureMatrix()); 
evaluation.eval(testData.getLabels(), output); 
out.println(evaluation.stats()); 

以下是执行这段代码的输出示例,因为我们只显示了stats方法的结果。第一部分报告示例如何分类,第二部分显示各种统计数据:

Examples labeled as 0 classified by model as 0: 19 times
Examples labeled as 1 classified by model as 1: 41 times
Examples labeled as 2 classified by model as 1: 4 times
Examples labeled as 2 classified by model as 2: 30 times
Examples labeled as 2 classified by model as 3: 1 times
Examples labeled as 3 classified by model as 2: 1 times
Examples labeled as 3 classified by model as 3: 28 times
==========================Scores===================================Accuracy: 0.3371
Precision: 0.8481
Recall: 0.8475
F1 Score: 0.8478
===================================================================

与我们之前的模型一样,评估证明了我们网络的准确性和成功性。

递归神经网络

RNN 不同于前馈网络,因为它们的输入包括来自前一迭代或步骤的输入。它们仍然处理当前输入,但是使用反馈循环来考虑前一步骤的输入,也称为最近的过去。这一步有效地给了网络内存。一种流行的循环网络涉及长期短期记忆 ( LSTM )。这种类型的存储器提高了网络的处理能力。

rnn 是为处理顺序数据而设计的,尤其适用于文本数据的分析和预测。给定一个单词序列,RNN 可以预测每个单词成为序列中下一个单词的概率。这也允许通过网络生成文本。rnn 是通用的,也能很好地处理图像数据,尤其是图像标记应用。设计和目的的灵活性以及训练的简易性使 RNNs 成为许多数据科学应用的流行选择。DL4J 还为 LSTM 网络和其他 rnn 提供支持。

总结

在这一章中,我们研究了神经网络的深度学习技术。本章中的所有 API 支持都是由 Deeplearning4j 提供的。我们首先演示了如何获取和准备用于深度学习网络的数据。我们讨论了如何配置和构建模型。随后解释了如何通过将数据集分为训练和测试部分来训练和测试模型。

我们的讨论继续进行深度学习和回归分析的检查。我们展示了如何准备数据和类、构建模型以及评估模型。我们使用样本数据和显示输出统计数据来演示我们的模型的相对有效性。

然后检查 RBM 和 DBNs。dbn 由堆叠在一起的 RBM 组成,对于分类和聚类应用程序特别有用。深度自动编码器也是使用 RBM 构建的,有两个对称的 dbn。自动编码器对于特征选择和提取特别有用。

最后,我们演示了一个卷积网络。这个网络模仿视觉皮层,允许网络使用信息区域进行分类。和前面的例子一样,我们构建、训练,然后评估模型的有效性。然后,我们以对递归神经网络的简要介绍结束了这一章。

当我们进入下一章并研究文本分析技术时,我们将进一步阐述这些主题。

九、文本分析

文本分析是一个广泛的话题,通常被称为自然语言处理 ( NLP )。它用于许多不同的任务,包括文本搜索、语言翻译、情感分析、语音识别和分类等等。由于自然语言的特殊性和模糊性,分析的过程可能很困难。然而,在这个领域已经做了大量的工作,并且有几个 Java APIs 支持这项工作。

我们将从介绍 NLP 中使用的基本概念和任务开始。其中包括以下内容:

  • 记号化:将文本拆分成单个记号或单词的过程。
  • 停用词:这些是常用词,可能不需要处理。它们包括诸如 the、a 和 to 这样的词。
  • 名称实体识别 ( NER ):这是识别文本元素的过程,比如人名、地点或事物。
  • 词类 ( 词性):这标识了一个句子的语法部分,如名词、动词、形容词等等。
  • 关系:这里,我们关心的是识别文本的各个部分是如何相互关联的,比如句子的主语和宾语。

单词、句子和段落的概念是众所周知的。然而,提取和分析这些组件并不总是那么简单。术语语料库通常指文本的集合。

与大多数数据科学问题一样,预处理文本非常重要。通常,这包括处理以下任务:

  • 处理 Unicode
  • 将文本转换为大写或小写
  • 删除停用词

我们在第三章、数据清理中研究了几种用于标记化和移除停用词的技术。在这一章中,我们将关注词性、NER、从句子中提取关系、文本分类和情感分析。

有几个可用的 NLP APIs,包括:

本章我们将使用 OpenNLP 和 DL4J 来演示文本分析。我们选择这些是因为它们都很有名,并且有很好的出版资源来获得额外的支持。

我们将使用谷歌 Word2VecDoc2Vec 神经网络来执行文本分类。这包括基于其他单词的特征向量以及使用标记信息来分类文档。最后,我们将讨论情感分析。这种类型的分析试图给文本赋予意义,并使用 Word2Vec 网络。

我们从 NER 开始讨论。

实现命名实体识别

这有时被称为寻找人和事物。给定一个文本片段,我们可能想要识别所有在场人员的姓名。然而,这并不总是容易的,因为像 Rob 这样的名字也可能被用作动词。

在这一节中,我们将演示如何使用 OpenNLP 的TokenNameFinderModel类来查找文本中的名称和位置。虽然我们可能还想找到其他实体,但这个例子将展示这项技术的基础。我们从名字开始。

大多数名字出现在一行中。我们不希望使用多行,因为像州这样的实体可能会在无意中被错误地识别。考虑下面的句子:

吉姆向北走了。达科塔往南走了。

如果我们忽略这个时期,那么北达科他州可能会被识别为一个位置,而实际上它并不存在。

使用 OpenNLP 执行 NER

我们从处理异常的 try-catch 块开始我们的例子。OpenNLP 使用在不同数据集上训练过的模型。在这个例子中,en-token.binen-ner-person.bin文件分别包含英语文本的标记化和英语名称元素的模型。这些文件可以从 opennlp.sourceforge.net/models-1.5/… IO 流是标准的 Java:](opennlp.sourceforge.net/models-1.5/)

try (InputStream tokenStream =  
            new FileInputStream(new File("en-token.bin")); 
        InputStream personModelStream = new FileInputStream( 
            new File("en-ner-person.bin"));) { 
    ... 
} catch (Exception ex) { 
    // Handle exceptions 
} 

使用令牌流初始化TokenizerModel类的一个实例。然后这个实例被用来创建实际的TokenizerME标记器。我们将用这个例子来修饰我们的句子:

TokenizerModel tm = new TokenizerModel(tokenStream); 
TokenizerME tokenizer = new TokenizerME(tm); 

TokenNameFinderModel类用于保存命名实体的模型。它是使用人模型流初始化的。由于我们正在寻找名称,因此使用这个模型创建了一个NameFinderME类的实例:

TokenNameFinderModel tnfm = new 
  TokenNameFinderModel(personModelStream); 
NameFinderME nf = new NameFinderME(tnfm); 

为了演示这个过程,我们将使用下面的句子。然后,我们使用 tokenizer 和tokenizer方法将它转换成一系列标记:

String sentence = "Mrs. Wilson went to Mary's house for dinner."; 
String[] tokens = tokenizer.tokenize(sentence); 

Span类保存关于实体位置的信息。find方法将返回位置信息,如下所示:

Span[] spans = nf.find(tokens); 

这个数组保存在句子中找到的 person 实体的信息。然后,我们显示这些信息,如下所示:

for (int i = 0; i < spans.length; i++) { 
    out.println(spans[i] + " - " + tokens[spans[i].getStart()]); 
} 

这个序列的输出如下。请注意,它标识了威尔逊夫人的姓,而不是“夫人”:

**[1..2) person - Wilson**
**[4..5) person - Mary** 

一旦这些实体被提取出来,我们就可以使用它们进行专门的分析。

识别位置实体

我们还可以找到其他类型的实体,如日期和位置。在下面的例子中,我们找到了句子中的位置。它与前面的 person 示例非常相似,除了模型使用了一个en-ner-location.bin文件:

try (InputStream tokenStream =  
            new FileInputStream("en-token.bin"); 
        InputStream locationModelStream = new FileInputStream( 
            new File("en-ner-location.bin"));) { 

    TokenizerModel tm = new TokenizerModel(tokenStream); 
    TokenizerME tokenizer = new TokenizerME(tm); 

    TokenNameFinderModel tnfm =  
        new TokenNameFinderModel(locationModelStream); 
    NameFinderME nf = new NameFinderME(tnfm); 

    sentence = "Enid is located north of Oklahoma City."; 
    String tokens[] = tokenizer.tokenize(sentence); 

    Span spans[] = nf.find(tokens); 

    for (int i = 0; i < spans.length; i++) { 
        out.println(spans[i] + " - " +  
        tokens[spans[i].getStart()]); 
    } 
} catch (Exception ex) { 
    // Handle exceptions 
} 

使用前面定义的句子,模型只能找到第二个城市,如下所示。这可能是由于名字Enid引起的混淆,它既是一个城市的名字也是一个人的名字:

**[5..7) location - Oklahoma** 

假设我们使用下面的句子:

sentence = "Pond Creek is located north of Oklahoma City."; 

然后我们得到这个输出:

 ****[1..2) location - Creek
[6..8) location - Oklahoma**** 

不幸的是,它错过了Pond Creek镇。NER 是许多应用程序的有用工具,但像许多技术一样,它并不总是万无一失的。所介绍的 NER 方法以及许多其他 NLP 示例的准确性将根据诸如模型的准确性、所使用的语言以及实体的类型等因素而变化。

我们也可能对文本如何分类感兴趣。我们将在下一节研究一种方法。

**# 文本分类

对文本进行分类是机器学习和数据科学的重要组成部分。我们必须能够为各种应用对文本进行分类,包括文档检索和网络搜索。在我们确定数据对特定应用程序或搜索结果的有用性之前,给数据分配特定的标签通常是很重要的。

在这一章中,我们将展示一种技术,这种技术涉及到使用 DL4J 类的段落向量和标签数据。这个例子允许我们读入文档,并根据文档内部的文本,给文档分配一个标签(或分类)。我们还将展示一个根据相似性对文本进行分类的例子。这意味着我们将匹配具有相似结构的短语和单词。这个例子也将使用 DL4J。

Word2Vec 和 Doc2Vec

我们将在本章的几个例子中使用 Word2Vec 和 Doc2Vec。Word2Vec 是用于文本处理的具有两层的神经网络。给定文本主体,网络将为文本中包含的单词提供特征向量。这些向量是单词特征的简单数学表示,并且可以在数字上与其他向量进行比较。这种比较通常被称为两个单词之间的距离。

Word2Vec 的工作原理是,通过确定两个单词同时出现的概率,可以对单词进行分类。由于这种方法,Word2Vec 不仅可以用于句子的分类。任何可以用文本标签表示的对象或数据都可以用这个网络进行分类。

Doc2Vec 是 Word2Vec 的扩展。这个网络不是像 Word2Vec 那样建立代表单个单词与其他单词相比的特征的向量,而是将单词与给定的标签进行比较。向量被设置来表示文档的主题或整体含义。我们的下一个例子展示了这些特征向量是如何与特定的文档相关联的。

通过标签对文本进行分类

在我们使用 Doc2Vec 的第一个例子中,我们将我们的文档与三个标签相关联:健康、金融和科学。但是在我们可以将数据与标签相关联之前,我们必须定义这些标签,并训练我们的模型来识别这些标签。每个标签代表一段特定文本的含义或分类。

在这个例子中,我们将使用样本文档,每个文档都预先标注了我们的类别:健康、金融或科学。我们将使用这些段落来训练我们的模型,然后像前面的例子一样,使用一组测试数据来测试我们的模型。我们将使用位于https://github . com/deep learning 4j/dl4j-examples/tree/master/dl4j-examples/src/main/resources/para vec的文件。我们基于为 DL4J 编写的示例代码编写了这个示例,可以在https://github . com/deep learning 4j/DL4J-examples/blob/master/DL4J-examples/src/main/Java/org/deep learning 4j/examples/NLP/paragraph vectors/paragraphvectors classifier example . Java找到。

首先,我们需要设置一些实例变量,以便稍后在代码中使用。我们将使用一个ParagraphVectors对象来创建我们的向量,一个LabelAwareIterator对象来遍历我们的数据,一个TokenizerFactory对象来标记我们的数据:

ParagraphVectors pVect; 
LabelAwareIterator iter; 
TokenizerFactory tFact; 

然后我们将设置我们的ClassPathResource。这指定了我们的项目中包含要分类的数据文件的目录。第一个资源包含我们用于培训目的的标记数据。然后,我们指示迭代器和标记器使用指定为ClassPathResource的资源。我们还指定将使用CommonPreprocessor来预处理我们的数据:

ClassPathResource resource = new  
         ClassPathResource("paravec/labeled"); 

iter = new FileLabelAwareIterator.Builder() 
        .addSourceFolder(resource.getFile()) 
        .build(); 

tFact = new DefaultTokenizerFactory(); 
tFact.setTokenPreProcessor(new CommonPreprocessor()); 

接下来,我们构建我们的ParagraphVectors。这是我们指定学习率、批量大小和训练时期数量的地方。我们还在设置过程中包含了迭代器和标记器。一旦我们构建了我们的ParagraphVectors,我们调用fit方法来使用paravec/labeled目录中的训练数据训练我们的模型:

pVect = new ParagraphVectors.Builder() 
        .learningRate(0.025) 
        .minLearningRate(0.001) 
        .batchSize(1000) 
        .epochs(20) 
        .iterate(iter) 
        .trainWordVectors(true) 
        .tokenizerFactory(tFact) 
        .build(); 

pVect.fit(); 

现在我们已经训练了我们的模型,我们可以使用我们的未标记数据进行测试。我们为未标记的数据创建一个新的ClassPathResource,并创建一个新的FileLabelAwareIterator:

ClassPathResource unlabeledText =  
         new ClassPathResource("paravec/unlabeled"); 
FileLabelAwareIterator unlabeledIter =  
         new FileLabelAwareIterator.Builder() 
               .addSourceFolder(unlabeledText.getFile()) 
               .build(); 

下一步涉及遍历我们的未标记数据,并为每个文档识别正确的标签。一般来说,我们可以预期每个文档将属于多个标签,但是每个标签具有不同的权重或匹配百分比。因此,虽然一篇文章可能主要被归类为健康文章,但它可能有足够的信息也被归类为科学文章,只是程度较低。

接下来,我们设置一个MeansBuilderLabelSeeker对象。这些类访问包含单词和标签之间关系的表,我们将在我们的ParagraphVectors中使用这些表。InMemoryLookupTable类提供对单词查找的默认表的访问:

MeansBuilder mBuilder =  
   new MeansBuilder((InMemoryLookupTable<VocabWord>)  
      pVect.getLookupTable(),tFact); 
LabelSeeker lSeeker =  
    new LabelSeeker(iter.getLabelsSource().getLabels(), 
               (InMemoryLookupTable<VocabWord>)
    pVect.getLookupTable()); 

最后,我们遍历未标记的文档。对于每个文档,我们将把文档转换成一个向量,并使用我们的LabelSeeker来获得每个文档的分数。我们记录每个文档的分数,并打印出带有相应标签的分数:

while (unlabeledIter.hasNextDocument()) { 
    LabelledDocument doc = unlabeledIter.nextDocument(); 
    INDArray docCentroid = mBuilder.documentAsVector(doc); 
    List<Pair<String, Double>> scores =  
              lSeeker.getScores(docCentroid); 
    out.println("Document '" + doc.getLabel() +  
       "' falls into the following categories: "); 
    for (Pair<String, Double> score : scores) { 
       out.println ("        " + score.getFirst() + ": " +  
             score.getSecond()); 
        } 

} 

我们前面的打印语句的输出如下:

Document 'finance' falls into the following categories: 
finance: 0.2889593541622162
health: 0.11753179132938385
science: 0.021202782168984413
Document 'health' falls into the following categories: 
finance: 0.059537000954151154
health: 0.27373185753822327
science: 0.07699354737997055

在每一个例子中,我们的文档都被正确分类,正如分配到正确标签类别的百分比较高所证明的那样。这种分类可以与其他数据分析技术结合使用,以得出有关文件中包含的数据的其他结论。通常,文本分类是数据分析过程中的初始或早期步骤,因为文档被分类成组以供进一步分析。

根据相似度对文本进行分类

在下一个例子中,我们将根据结构和相似性来匹配不同的文本样本。我们将仍然使用我们在前面的例子中使用的ParagraphVectors类。首先,从 GitHub(https://GitHub . com/deep learning 4j/dl4j-examples/tree/master/dl4j-examples/src/main/resources)下载raw_sentences.txt文件,并将其添加到您的项目中。这个文件包含一个句子列表,我们将读入这些句子,标记它们,然后进行比较。

首先,我们设置我们的ClassPathResource并分配一个迭代器来处理我们的文件数据。我们在这个例子中使用了一个SentenceIterator:

ClassPathResource srcFile = new  
      ClassPathResource("/raw_sentences.txt"); 
File file = srcFile.getFile(); 
SentenceIterator iter = new BasicLineIterator(file); 

接下来,我们将再次使用TokenizerFactory来标记我们的数据。我们还想创建一个新的LabelsSource对象。这允许我们定义句子标签的格式。我们选择在每一行前面加上LINE_:

TokenizerFactory tFact = new DefaultTokenizerFactory(); 
tFact.setTokenPreProcessor(new CommonPreprocessor()); 
LabelsSource labelFormat = new LabelsSource("LINE_"); 

现在我们已经准备好构建我们的ParagraphVectors。我们的设置过程包括这些方法:minWordFrequency,它指定在训练语料库中使用的最小词频,以及iterations,它指定每个小批量的迭代次数。我们还设置了历元数、层大小和学习速率。此外,我们包括前面定义的LabelsSource,以及我们的迭代器和标记器。trainWordVectors方法指定了单词和文档表示是否应该一起构建。最后,sampling确定是否应该进行二次采样。然后我们调用我们的buildfit方法:

ParagraphVectors vec = new ParagraphVectors.Builder() 
        .minWordFrequency(1) 
        .iterations(5) 
        .epochs(1) 
        .layerSize(100) 
        .learningRate(0.025) 
        .labelsSource(labelFormat) 
        .windowSize(5) 
        .iterate(iter) 
        .trainWordVectors(false) 
        .tokenizerFactory(tFact) 
        .sampling(0) 
        .build(); 

vec.fit(); 

接下来,我们将包含一些语句来评估我们分类的准确性。值得注意的是,虽然文档本身从1开始,但是索引过程从0开始。例如,文档中的行9836将与标签LINE_9835相关联。我们将首先比较三个应该归类为有些相似的句子,然后比较两个不相似的句子。similarity方法获取两个标签,并以double的形式返回它们之间的相对距离:

double similar1 = vec.similarity("LINE_9835", "LINE_12492"); 
out.println("Comparing lines 9836 & 12493  
       ('This is my house .'/'This is my world .')  
       Similarity = " + similar1); 

double similar2 = vec.similarity("LINE_3720", "LINE_16392"); 
out.println("Comparing lines 3721 & 16393  
       ('This is my way .'/'This is my work .')  
       Similarity = " + similar2); 

double similar3 = vec.similarity("LINE_6347", "LINE_3720"); 
out.println("Comparing lines 6348 & 3721  
       ('This is my case .'/'This is my way .')  
       Similarity = " + similar3); 

double dissimilar1 = vec.similarity("LINE_3720", "LINE_9852"); 
out.println("Comparing lines 3721 & 9853  
       ('This is my way .'/'We now have one .')  
       Similarity = " + dissimilar1); 

double dissimilar2 = vec.similarity("LINE_3720", "LINE_3719"); 
out.println("Comparing lines 3721 & 3720  
       ('This is my way .'/'At first he says no .')  
       Similarity = " + dissimilar2); 

我们的打印语句的输出如下所示。比较similarity方法对三个相似句子和两个不相似句子的结果。特别要注意的是,最后一个例子的similarity方法结果,两个非常不同的句子,返回了一个负数。这意味着更大的差异:

16:56:15.423 [main] INFO o.d.m.s.SequenceVectors - Epoch: [1]; Words vectorized so far: [3171540]; Lines vectorized so far: [485810]; learningRate: [1.0E-4]
Comparing lines 9836 & 12493 ('This is my house .'/'This is my world .') Similarity = 0.7641470432281494
Comparing lines 3721 & 16393 ('This is my way .'/'This is my work .') Similarity = 0.7246013879776001
Comparing lines 6348 & 3721 ('This is my case .'/'This is my way .') Similarity = 0.8988922834396362
Comparing lines 3721 & 9853 ('This is my way .'/'We now have one .') Similarity = 0.5840312242507935
Comparing lines 3721 & 3720 ('This is my way .'/'At first he says no .') Similarity = -0.6491150259971619

虽然这个例子像我们的第一个分类例子一样使用了ParagraphVectors,但是这展示了我们方法的灵活性。我们可以使用这些 DL4J 库以多种方式对数据进行分类。

了解标记和位置

词性涉及识别句子中的成分类型。比如这个句子有几个成分,包括动词“has”,几个名词如“example”、“elements”,形容词如“几个”。标记,或者更具体地说词性标记,是将元素类型与单词相关联的过程。

词性标注很有用,因为它增加了关于句子的更多信息。我们可以确定单词之间的关系以及它们的相对重要性。标记的结果通常用于后面的处理步骤。

这项任务可能很困难,因为我们不能依靠简单的词典来确定它们的类型。例如,单词lead既可以用作名词,也可以用作动词。我们可以在下面两个句子中使用它:

He took the lead in the play.
Lead the way!

词性标注会尝试将正确的标签与句子中的每个单词相关联。

使用 OpenNLP 识别 POS

为了说明这个过程,我们将使用 OpenNLP(opennlp.apache.org/)。这是一个开源的 Apache 项目,支持许多其他的 NLP 处理任务。

我们将使用POSModel类,它可以被训练来识别 POS 元素。在这个例子中,我们将把它与先前基于佩恩树库 标签集(www.comp.leeds.ac.uk/ccalas/tags…)训练的模型一起使用。在opennlp.sourceforge.net/models-1.5/可以找到各种经过预训练的模型。我们将使用en-pos-maxent.bin型号。这已经用所谓的最大熵在英语文本上进行了训练。

最大熵指的是模型中不确定性的数量最大化。对于一个给定的问题,有一组概率描述了数据集的已知情况。这些概率用于建立模型。例如,我们可能知道有 23%的可能性某个特定事件会遵循某个条件。我们不想对未知的概率做任何假设,所以我们避免添加不合理的信息。最大熵方法试图保持尽可能多的不确定性;因此它试图最大化熵。

我们还将使用POSTaggerME类,它是一个最大熵标记器。这是将进行标签预测的类。对于任何一个句子,都可能有不止一种方式来对其成分进行分类或标记。

我们从代码开始,以获取先前训练的英语标记器模型和要标记的简单句子:

try (InputStream input = new FileInputStream( 
        new File("en-pos-maxent.bin"));) { 
    String sentence = "Let's parse this sentence."; 
    ... 
} catch (IOException ex) { 
    // Handle exceptions 
} 

标记器使用字符串数组,其中每个字符串都是一个单词。下面的序列采用前面的句子并创建一个名为words的数组。第一部分使用Scanner类解析句子字符串。如果需要,我们可以使用其他代码从文件中读取数据。之后,List类的toArray方法用于创建字符串数组:

List<String> list = new ArrayList<>(); 
Scanner scanner = new Scanner(sentence); 
while(scanner.hasNext()) { 
    list.add(scanner.next()); 
} 
String[] words = new String[1]; 
words = list.toArray(words); 

然后,使用包含模型的文件构建模型:

POSModel posModel = new POSModel(input); 

然后根据模型创建标记:

POSTaggerME posTagger = new POSTaggerME(posModel); 

tag方法完成实际的工作。它被传递一个单词数组并返回一个标签数组。然后显示单词和标签:

String[] posTags = posTagger.tag(words); 
for(int i=0; i<posTags.length; i++) { 
    out.println(words[i] + " - " + posTags[i]); 
} 

此示例的输出如下:

Let's - NNP
parse - NN
this - DT
sentence. - NN

分析已经确定单词let's是单数专有名词,而单词parsesentence是单数名词。this这个词是一个限定词,也就是说,它是一个修饰另一个词的词,有助于识别一个短语是一般的还是特定的。下一节提供了标签列表。

了解 POS 标签

POS 元素返回缩写。在https://www . ling . upenn . edu/courses/Fall _ 2003/ling 001/Penn _ tree bank _ pos . html可以找到 Penn TreeBankPOS 标签列表。以下是该列表的简短版本:

| 标签 | 描述 | 标签 | 描述 | | DT | 限定词 | RB | 副词 | | JJ | 形容词 | RBR | 副词,比较 | | JJR | 形容词,比较级 | RBS | 副词,最高级 | | JJS | 形容词,最高级 | RP | 颗粒 | | NN | 名词,单数或复数 | SYM | 标志 | | NNS | Noun, plural | TOP | 解析树的顶部 | | NNP | 专有名词,单数 | VB | 动词,基本形式 | | NNPS | 专有名词,复数 | VBD | 动词,过去式 | | POS | 所有格结尾 | VBG | 动词、动名词或现在分词 | | PRP | 人称代词 | VBN | 动词,过去分词 | | PRP$ | 所有格代名词 | VBP | 动词,非第三人称单数现在时 | | S | 简单陈述句 | VBZ | 动词,第三人称单数现在时 |

如前所述,一个句子可能有不止一组可能的词性赋值。如下所示的topKSequences方法将返回各种可能的赋值和分数。该方法返回一个由Sequence对象组成的数组,这些对象的toString方法返回分数和位置列表:

    Sequence sequences[] = posTagger.topKSequences(words); 
    for(Sequence sequence : sequences) { 
        out.println(sequence); 
    } 

前一个句子的输出如下,其中最后一个序列被认为是最可能的选择:

-2.3264880694837213 [NNP, NN, DT, NN]
-2.6610271245387853 [NNP, VBD, DT, NN]
-2.6630142638557217 [NNP, VB, DT, NN]

每行输出为句子中的每个单词分配可能的标签。我们可以看到,只有第二个单词parse被确定为具有其他可能的标签。

接下来,我们将演示如何从文本中提取关系。

从句子中提取关系

在许多分析任务中,了解句子元素之间的关系是很重要的。它有助于评估句子的重要内容并洞察句子的含义。这种类型的分析已经被用于从语法检查到语音识别到语言翻译的任务。

在上一节中,我们演示了一种用于提取词性的方法。使用这种技术,我们能够识别句子中存在的句子成分类型。然而,这些元素之间的关系是缺失的。我们需要解析句子来提取句子元素之间的关系。

使用 OpenNLP 提取关系

有几种技术和 API 可以用来提取这种类型的信息。在这一节中,我们将使用 OpenNLP 来演示一种提取句子结构的方法。演示围绕着ParserTool类展开,它使用了一个以前训练过的模型。解析过程将返回句子中提取的元素正确的概率。正如许多 NLP 任务一样,通常可能有多个答案。

我们从 try-with-resource 块开始,为模型打开一个输入流。en-parser-chunking.bin文件包含一个将文本解析成 POS 的模型。在这种情况下,它是为英语而训练的:

try (InputStream modelInputStream = new FileInputStream( 
            new File("en-parser-chunking.bin"));) { 
    ... 
} catch (Exception ex) { 
    // Handle exceptions 
}  

在 try 块中,使用输入流创建了一个ParserModel类的实例。接下来使用ParserFactory类的create方法创建实际的解析器:

ParserModel parserModel = new ParserModel(modelInputStream); 
Parser parser = ParserFactory.create(parserModel); 

我们将使用下面的句子来测试解析器。ParserTool类的parseLine方法执行实际的解析并返回一个Parse对象的数组。这些对象中的每一个都有一个解析选项。parseLine方法的最后一个参数指定返回多少个备选方案:

String sentence = "Let's parse this sentence."; 
Parse[] parseTrees = ParserTool.parseLine(sentence, parser, 3); 

下一个序列显示了每种可能性:

for(Parse tree : parseTrees) { 
    tree.show(); 
} 

本例中 show 方法的输出如下。标签先前已在了解位置标签一节中定义:

(TOP (NP (NP (NNP Let's) (NN parse)) (NP (DT this) (NN sentence.))))
(TOP (S (NP (NNP Let's)) (VP (VB parse) (NP (DT this) (NN sentence.)))))
(TOP (S (NP (NNP Let's)) (VP (VBD parse) (NP (DT this) (NN sentence.)))))

以下示例将最后两个输出重新格式化,以更好地显示关系。他们对动词 parse 的分类不同:

(TOP 
(S 
(NP (NNP Let's)) 
(VP (VB parse) 
(NP (DT this) (NN sentence.))
)
)
)
(TOP 
(S 
(NP (NNP Let's)) 
(VP (VBD parse) 
(NP (DT this) (NN sentence.)) 
)
)
)

当有多个解析选择时,Parse类的getProb返回一个概率,该概率反映了模型对选择的置信度。以下序列演示了此方法:

for(Parse tree : parseTrees) { 
    out.println("Probability: " + tree.getProb()); 
} 

输出如下:

Probability: -3.6810244423259078
Probability: -3.742475884515823
Probability: -4.16148634555491

另一个有趣的 NLP 任务是情感分析,我们将在下面演示。

情感分析

情感分析包括基于单词的上下文、含义和情感含义对单词进行评估和分类。通常,如果我们在字典中查找一个单词,我们会找到该单词的含义或定义,但是,脱离句子的上下文,我们可能无法详细准确地描述该单词的含义。

例如,单词 toast 可以被简单地定义为一片加热并变成褐色的面包。但是在句子的上下文中,他是烤面包!,意思完全变了。情感分析试图根据上下文和用法推导出单词的含义。

值得注意的是,高级情感分析将超越简单的积极或消极分类,并赋予单词详细的情感含义。将单词分为积极或消极要简单得多,但将它们分为快乐、愤怒、冷漠或焦虑要有用得多。

这种类型的分析属于有效计算的范畴,一种对技术工具的情感含义和使用感兴趣的计算。鉴于如今社交媒体网站上可用于分析的受情绪影响的数据越来越多,这种类型的计算尤为重要。

能够确定文本的情感内容使得能够做出更有针对性和适当的反应。例如,能够在客户和技术代表之间的聊天会话中判断情绪反应可以让代表做得更好。当他们之间存在文化或语言差异时,这一点尤为重要。

这种类型的分析也可以应用于视觉图像。它可以用来衡量一个人对新产品的反应,比如在进行品尝测试时,或者判断人们对电影或广告场景的反应。

作为示例的一部分,我们将使用单词袋模型。词袋模型通过包含一组被称为的词来简化自然语言处理的词表示,而不考虑语法或词序。单词具有用于分类的特征,最重要的是每个单词的频率。因为有些词,如 the、a 或 and,在任何文本中自然会有较高的出现频率,所以这些词也会被赋予一定的权重。上下文重要性较低的常用词将具有较小的权重,并且在文本分析中的影响较小。

下载并提取 Word2Vec 模型

为了演示情感分析,我们将使用 Google 的 Word2Vec 模型和 DL4J 来简单地根据评论中使用的词将电影评论分为正面或负面。本例改编自 Alex Black 所做的工作(https://github . com/deep learning 4j/dl4j-examples/blob/master/dl4j-examples/src/main/Java/org/deep learning 4j/examples/recurrent/word 2 vessentiment/word 2 vessentimentrnn . Java)。正如本章前面所讨论的,Word2Vec 由两层神经网络组成,经过训练可以从单词的上下文中构建含义。我们还将使用来自 ai.stanford.edu/~amaas/data…](ai.stanford.edu/~amaas/data…)

在我们开始之前,你需要从 code.google.com/p/word2vec/… Word2Vec 数据。基本流程包括:

  • 下载并提取电影评论
  • 加载 Word2Vec 谷歌新闻矢量
  • 加载每个电影评论

评论中的单词被分解成向量,用于训练网络。我们将在五个时期内训练网络,并在每个时期后评估网络的性能。

首先,我们先声明三个最终变量。第一个是检索训练数据的 URL,第二个是存储我们提取的数据的位置,第三个是本地机器上 Google News vectors 的位置。修改第三个变量以反映本地计算机上的位置:

public static final String TRAINING_DATA_URL =  
    "http://ai.stanford.edu/~amaas/" +  
    "data/sentiment/aclImdb_v1.tar.gz"; 
public static final String EXTRACT_DATA_PATH =  
    FilenameUtils.concat(System.getProperty( 
    "java.io.tmpdir"), "dl4j_w2vSentiment/"); 
public static final String GNEWS_VECTORS_PATH =  
    "C:/YOUR_PATH/GoogleNews-vectors-negative300.bin" +  
    "/GoogleNews-vectors-negative300.bin"; 

接下来,我们下载并提取模型数据。接下来的两个方法模仿 DL4J 示例中的代码。我们首先创建一个新方法,getModelData。接下来完整地示出了该方法。

首先,我们使用之前定义的EXTRACT_DATA_PATH创建一个新的File。如果文件不存在,我们创建一个新的目录。接下来,我们再创建两个File对象,一个用于归档 TAR 文件的路径,一个用于提取数据的路径。在我们尝试提取数据之前,我们检查这两个文件是否存在。如果存档路径不存在,我们从TRAINING_DATA_URL下载数据,然后提取数据。如果提取的文件不存在,那么我们提取数据:


private static void getModelData() throws Exception { 
    File modelDir = new File(EXTRACT_DATA_PATH); 
    if (!modelDir.exists()) { 
        modelDir.mkdir(); 
    } 
    String archivePath = EXTRACT_DATA_PATH + "aclImdb_v1.tar.gz"; 
    File archiveName = new File(archivePath); 
    String extractPath = EXTRACT_DATA_PATH + "aclImdb"; 
    File extractName = new File(extractPath); 
    if (!archiveName.exists()) { 
        FileUtils.copyURLToFile(new URL(TRAINING_DATA_URL), 
                archiveName); 
        extractTar(archivePath, EXTRACT_DATA_PATH); 
    } else if (!extractName.exists()) { 
        extractTar(archivePath, EXTRACT_DATA_PATH); 
    } 
} 

为了提取数据,我们将创建另一个名为extractTar的方法。我们将为该方法提供两个输入,之前定义的archivePathEXTRACT_DATA_PATH。我们还需要定义提取过程中使用的缓冲区大小:

private static final int BUFFER_SIZE = 4096; 

我们首先创建一个新的TarArchiveInputStream。我们使用GzipCompressorInputStream是因为它提供了对提取.gz文件的支持。我们还使用BufferedInputStream来提高提取过程的性能。压缩文件非常大,下载和解压缩可能需要一些时间。

接下来我们创建一个TarArchiveEntry,并开始使用TarArchiveInputStream getNextEntry方法读入数据。当我们处理压缩文件中的条目时,我们首先检查该条目是否是一个目录。如果是,我们将在提取位置创建一个新目录。最后,我们创建一个新的FileOutputStreamBufferedOutputStream,并使用write方法将数据写入提取的位置:

private static void extractTar(String dataIn, String dataOut)
    throws IOException {
        try (TarArchiveInputStream inStream =
            new TarArchiveInputStream(
                new GzipCompressorInputStream(
                    new BufferedInputStream(
                        new FileInputStream(dataIn))))) {
            TarArchiveEntry tarFile;
            while ((tarFile = (TarArchiveEntry) inStream.getNextEntry())
                != null) {
                if (tarFile.isDirectory()) {
                    new File(dataOut + tarFile.getName()).mkdirs();
                }else {
                    int count;
                    byte data[] = new byte[BUFFER_SIZE];
                    FileOutputStream fileInStream =
                      new FileOutputStream(dataOut + tarFile.getName());
                    BufferedOutputStream outStream = 
                      new BufferedOutputStream(fileInStream,
                        BUFFER_SIZE);
                    while ((count = inStream.read(data, 0, BUFFER_SIZE))
                        != -1) {
                            outStream.write(data, 0, count);
                    }
                }
            }
        }
    }

构建我们的模型并对文本进行分类

既然我们已经创建了下载和提取数据的方法,我们需要声明和初始化用于控制模型执行的变量。我们的batchSize指的是我们在每个例子中处理的单词量,在这个例子中是50。我们的vectorSize决定了向量的大小。谷歌新闻模型有大小为300的词向量。nEpochs指我们尝试运行训练数据的次数。最后,truncateReviewsToLength指定如果电影评论超过特定长度,出于内存利用的目的,我们是否应该截断电影评论。我们选择截断超过300个单词的评论:

int batchSize = 50;      
int vectorSize = 300; 
int nEpochs = 5;         
int truncateReviewsToLength = 300;   

现在我们可以建立我们的神经网络了。我们将使用一个MultiLayerConfiguration网络,如第 8 章深度学习中所讨论的。事实上,我们这里的例子与配置和构建模型中构建的模型非常相似,只是有一些不同。特别是,在这个模型中,我们将在第 0 层使用更快的学习速率和一个GravesLSTM递归网络。我们将拥有与向量中单词数量相同的输入神经元,在这种情况下,300。我们还使用gradientNormalization,这是一种用来帮助我们的算法找到最优解的技术。注意我们正在使用softmax激活函数,这在第 8 章深度学习中讨论过。该函数使用回归,特别适用于分类算法:

MultiLayerConfiguration sentimentNN =  
         new NeuralNetConfiguration.Builder() 
        .optimizationAlgo(OptimizationAlgorithm 
                 .STOCHASTIC_GRADIENT_DESCENT).iterations(1) 
        .updater(Updater.RMSPROP) 
        .regularization(true).l2(1e-5) 
        .weightInit(WeightInit.XAVIER) 
        .gradientNormalization(GradientNormalization 
                 .ClipElementWiseAbsoluteValue) 
                 .gradientNormalizationThreshold(1.0) 
        .learningRate(0.0018) 
        .list() 
        .layer(0, new GravesLSTM.Builder() 
                 .nIn(vectorSize).nOut(200) 
                .activation("softsign").build()) 
        .layer(1, new RnnOutputLayer.Builder() 
                .activation("softmax") 
                .lossFunction(LossFunctions.LossFunction.MCXENT) 
                .nIn(200).nOut(2).build()) 
        .pretrain(false).backprop(true).build(); 

然后我们可以创建我们的MultiLayerNetwork,初始化网络,并设置监听器。

MultiLayerNetwork net = new MultiLayerNetwork(sentimentNN); 
net.init(); 
net.setListeners(new ScoreIterationListener(1)); 

接下来,我们创建一个WordVectors对象来加载我们的 Google 数据。我们使用一个DataSetIterator来测试和训练我们的数据。AsyncDataSetIterator允许我们在一个单独的线程中加载数据,以提高性能。此过程需要大量内存,因此此类改进对于优化性能至关重要:

WordVectors wordVectors = WordVectorSerializer
DataSetIterator trainData = new AsyncDataSetIterator(
    new SentimentExampleIterator(EXTRACT_DATA_PATH, wordVectors,
        batchSize, truncateReviewsToLength, true), 1);
DataSetIterator testData = new AsyncDataSetIterator(
    new SentimentExampleIterator(EXTRACT_DATA_PATH, wordVectors,
        100, truncateReviewsToLength, false), 1);

最后,我们准备好训练和评估我们的数据。我们浏览数据nEpochs次;在这种情况下,我们有五次迭代。每次迭代针对我们的训练数据执行fit方法,然后使用testData创建一个新的Evaluation对象来评估我们的模型。该评估基于大约 25,000 条电影评论,可能需要很长时间来运行。当我们评估数据时,我们创建INDArray来存储信息,包括来自我们数据的特征矩阵和标签。该数据稍后用于evalTimeSeries方法的评估。最后,我们打印出我们的评估统计数据:

for (int i = 0; i < nEpochs; i++) { 
    net.fit(trainData); 
    trainData.reset(); 

    Evaluation evaluation = new Evaluation(); 
    while (testData.hasNext()) { 
        DataSet t = testData.next(); 
        INDArray dataFeatures = t.getFeatureMatrix(); 
        INDArray dataLabels = t.getLabels(); 
        INDArray inMask = t.getFeaturesMaskArray(); 
        INDArray outMask = t.getLabelsMaskArray(); 
        INDArray predicted = net.output(dataFeatures, false,  
            inMask, outMask); 

        evaluation.evalTimeSeries(dataLabels, predicted, outMask); 
    } 
    testData.reset(); 

    out.println(evaluation.stats()); 
} 

最终迭代的输出如下所示。我们归类为0的示例被视为负面评价,归类为1的示例被视为正面评价:

Epoch 4 complete. Starting evaluation:
Examples labeled as 0 classified by model as 0: 11122 times
Examples labeled as 0 classified by model as 1: 1378 times
Examples labeled as 1 classified by model as 0: 3193 times
Examples labeled as 1 classified by model as 1: 9307 times
==========================Scores===================================Accuracy: 0.8172
Precision: 0.824
Recall: 0.8172
F1 Score: 0.8206
===================================================================

如果与以前的迭代相比,您应该注意到分数和准确性随着每次评估而提高。随着每次迭代,我们的模型在将电影评论分类为负面或正面方面提高了其准确性。

总结

在本章中,我们介绍了一些 NLP 任务,并展示了它们是如何被支持的。特别是,我们使用了 OpenNLP 和 DL4J 来说明它们是如何执行的。虽然还有许多其他可用的库,但这些示例很好地介绍了这些技术。

我们首先介绍了基本的 NLP 术语和概念,比如命名实体识别、词性以及句子元素之间的关系。命名实体识别涉及查找和标记句子的各个部分,如人、地点和事物。词性将标签与句子的元素联系起来。例如,NN指名词,VB指动词。

然后我们讨论了 Word2Vec 和 Doc2Vec 神经网络。这些被用来分类文本,既有标签,也有与其他单词的相似性。我们演示了如何使用 DL4J 资源来创建与标签相关联的文档的特征向量。

虽然这些关联的识别是有趣的,但是当从句子中提取关系时,执行更有用的分析。我们演示了如何使用 OpenNLP 找到关系。POS 与每个单词相关联,单词之间的关系使用一组标签和括号来显示。这种类型的分析可用于更复杂的分析,如语言翻译和语法检查。

最后,我们讨论并展示了情感分析的例子。这个过程包括根据文本的语气或上下文含义对文本进行分类。我们研究了将电影评论分为正面或负面的过程。

在这一章中,我们展示了文本分析和分类的各种技术。在下一章中,我们将研究为视频和音频分析设计的技术。**