自然语言处理与Java语言实现
上QQ阅读APP看书,第一时间看更新

1.2 技术基础

1.2.1 机器学习

机器学习解决问题的流程是:根据训练集产生模型,根据模型预测新的实例。

根据目标预测变量的类型,机器学习问题可以分为分类问题和回归问题两类。

根据学习方法,机器学习模型可以分为产生式模型和判别式模型两类。假定输入是x,类别标签是y。产生式模型估计联合概率P(x,y),因为可以根据联合概率生成样本,所以叫作产生式模型。判别式模型估计条件概率P(y|x),因为没有x的知识,无法生成样本,只能判断分类,所以叫作判别式模型。

产生式模型可以根据贝叶斯公式得到判别式模型,但反过来不行。例如下面的情况:

(1,0), (1,1), (2,0), (2,1)

假设计算出联合概率P(x, y)如下:

P(1,0) = 1/2, P(1,1) = 0, P(2,0) = 1/4, P(2,1) = 1/4

假设计算出条件概率P(y|x):

P(0|1) = 1, P(1|1) = 0, P(0|2) = 1/2, P(1|2) = 1/2

判别式模型得到输入x在类别y上的概率分布。

1.2.2 Java基础

下面通过一些例子简单复习一下Java的基础知识。

定义一个Token类描述词在文本中的位置:

public class Token {
      public String term; // 词
      public int start; // 词在文档中的开始位置
      public int end; // 词在文档中的结束位置
}

增加构造方法:

public class Token {
      public String term;  // 词
      public int start;   // 开始位置
      public int end;  // 结束位置

      public Token(String t, int s, int e) { // 构造方法
           term = t; // 参数赋值给实例变量
           start = s;
           end = e;
      }
}

调用这个构造方法来创造对象。例如,有个词出现在文档的开始位置。在构造方法前加上new关键词来通过这个构造方法创造对象:

Token t = new Token("量子", 0, 2); // 出现在开始位置的“量子”这个词

可以通过this.term访问Token的实例变量term,特别说明term不是一个方法中的局域变量,所以构造方法也可以这样写:

public Token(String t, int s, int e) { // 构造方法
      this.term = t; // 用this关键字作前缀修饰词来指明term是当前对象的实例变量
      this.start = s;
      this.end = e;
}

在此处,创建一个Token类需要传入3个参数:词本身、词的开始位置和结束位置:

Token t = new Token("量子", 0, 2); // 出现在开始位置的“量子”这个词

这是调用构造方法Token(String t, int s, int e)来创建Token类实例的一个例子。

可以使用Guava(一种基于开源的Java库)初始化HashMap。

为了引入Guava相关的jar包,首先在ivy.xml文件中增加依赖项:

<dependency org="com.google.guava" name="guava" rev="27.0-jre"/>

然后在Java项目中增加对相关jar包的引用:

Map<String, Integer> vocab = ImmutableMap.of("l o w</w>" , 5
          , "l o w e r</w>" , 2,
          "n e w e s t</w>" , 6,
          "w i d e s t</w>" , 3
        );

System.out.println(vocab);

1.2.3 信息采集

机器学习的方法需要大量数据,通过网络爬虫抓取是获得数据的一种方法。

可以用docx4j从采集的Word文档提取文本。项目中增加docx4j依赖项:

<dependency org="org.docx4j" name="docx4j" rev="6.0.1"/>

使用TextUtils类提取文本:

String inputfilepath = "教程.docx";
WordprocessingMLPackage wordMLPackage =
           WordprocessingMLPackage.load(new java.io.File(inputfilepath));
MainDocumentPart documentPart = wordMLPackage.getMainDocumentPart();
org.docx4j.wml.Document wmlDocumentEl =
        (org.docx4j.wml.Document)documentPart.getJaxbElement();

String content = TextUtils.getText(wmlDocumentEl);

System.out.println(content);

使用Apache-Tika(基于Java的内容检测和分析工具包)来处理各种格式的文档。例如用Tika判断语言类型:

public class LanguageDetectorExample {

      public static void main(String[] args) throws IOException {
            String lang = detectLanguage("hello world");
            System.out.println(lang); // 输出语言类型:en
      }

      public static String detectLanguage(String text) throws IOException {
            LanguageDetector detector = new OptimaizeLangDetector().loadModels();
            LanguageResult result = detector.detect(text);
            return result.getLanguage();
      }
}

可以使用机器学习的方法解决自然语言处理问题。方法是:根据训练集产生模型,根据模型分析新的实例。

用于训练的文档叫作语料库。语料库就是一个文档的样本库,需要有很大的规模,才有概率统计的意义,可以假设很多词和句子都会在其中出现多次。

1.2.4 文本挖掘

文本挖掘指从大量文本数据中抽取隐含的、未知的、可能有用的信息。

常用的文本挖掘方法包括全文检索、中文分词、句法分析、文本分类、文本聚类、关键词提取、文本摘要、信息提取、智能问答等。文本挖掘相关技术的结构如图1-1所示。

0

图1-1 文本挖掘的结构

1.2.5 SWIG扩展Java性能

当前一些高性能代码库选用C或C++开发。简化包以及接口生成器(Simplified Wrapper and Interface Generator,SWIG)是一个软件开发工具,它将C和C++编写的程序与包括Java在内的各种高级编程语言连接起来。可以使用它在Java项目中重用现有的C和C++代码。

为了说明SWIG的使用,在Linux下运行一个简单的测试类。

下载SWIG源代码:

#wget http://prdownloads.sourceforge.net/swig/swig-3.0.12.tar.gz

解压缩:

#tar -xvf./swig-3.0.12.tar.gz

切换到源代码所在的目录:

#cd swig-3.0.12/

构建源代码:

#make
#make install

验证是否正确安装:

#swig -version

运行例子:

#cd Examples/j ava/simple

构建例子代码:

#make

指定链接库所在的路径:

#export LD_LIBRARY_PATH=. #ksh

编译Java源代码:

#javac *.java

运行:

#java runme

想要添加到Java语言的c函数,具体来说,假设将函数放在了文件“example.c”中:

# cat./example.c
/*全局变量*/
double Foo = 3.0;

/*计算正整数的最大公约数*/
int gcd(int x, int y) {
  int g;
  g = y;
  while (x > 0) {
    g = x;
    x = y % x;
    y = g;
  }
  return g;
}

使用Java语言中的loadLibrary语句加载和访问生成的Java类。例如:

System.loadLibrary("example");

C语言的函数就像Java语言的函数一样工作了。例如:

int g = example.gcd(42,105);

通过模块类中的get和set函数访问C语言的全局变量。例如:

double a = example.get_Foo();
example.set_Foo(20.0);

1.2.6 代码移植

存在一些其他高级语言编写的自然语言处理项目,可以把这些代码移植到Java语言。例如,可以使用Roslyn解析C#代码,使用JavaPoet生成代码。

语法树的4个主要构建块如下。

• SyntaxTree类,其实例表示整个解析树。SyntaxTree是一个抽象类,具有C#语言的派生类,如使用CSharpSyntaxTree类上的解析方法可解析C#语言的语法。

• SyntaxNode类,其实例表示语法结构,如声明、语句、子句和表达式。

• SyntaxToken结构,表示关键字、标识符、运算符或标点符号。

• SyntaxTrivia结构,表示语法上无关紧要的信息,例如符号之间的空白、预处理指令和注释。

接下来介绍如何遍历树。首先创建一个新的C# Stand-Alone代码分析工具项目,然后将以下using指令添加到Program.cs文件中:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

在main方法中输入以下代码:

SyntaxTree tree = CSharpSyntaxTree.ParseText(
@"using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace HelloWorld
{
     class Program
     {
          static void Main(string[] args)
          {
               Console.WriteLine(""Hello, World!"");
          }
     }
}");

var root = (CompilationUnitSyntax)tree.GetRoot();

main方法中的解析代码如下:

                SyntaxTree tree = CSharpSyntaxTree.ParseText(
@"using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace HelloWorld
{
     class Program
     {
          static void Main(string[] args)
          {
               Console.WriteLine(""Hello, World!"");
          }
     }
}");

               var root = (CompilationUnitSyntax)tree.GetRoot();
               // 命名空间Namespace
               var firstMember = root.Members[0];

               var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstMember;
               // 类class
               var programDeclaration =
                  (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];
               // 方法Method
               var mainDeclaration =
                   (MethodDeclarationSyntax)programDeclaration.Members[0];
               // 参数Parameter
               var argsParameter = mainDeclaration.ParameterList.Parameters[0];
          }

具体例子可以参考CSharpTranspiler的实现:

// 加载解决方案
string path = Path.Combine(Environment.CurrentDirectory, @"..\..\..\");
var solution = new Solution(Path.Combine(path, @"TestApp\TestApp.csproj"));

// 解析解决方案
var task = solution.Parse();
task.Wait();

// 生成代码
var emitter = new EmitterC(solution, Path.Combine(path, "TestOutput"),
 EmitterC.CVersions.c99, EmitterC.CompilerTargets.VC, EmitterC.PlatformTypes.
 Standalone, EmitterC.GCTypes.Boehm);
emitter.Emit(false);

1.2.7 语义

自然语言中的语义复杂多变,例如,在“买玩偶送女友”中,“送”这个词不止一个义项。OpenCyc提供了OWL(一门供处理Web信息的语言)格式的英文知识库。

三元组是“主语/谓词/对象”形式的语句,即将一个对象(主语)通过一个谓词链接到另一个对象(对象)或文字的语句。三元组是二元关系的最小不可约表示。例如,三元组:《史记》 作者 司马迁

RDF(Resource Description Framework,资源描述框架)三元组包含以下3个组件。

• 主语,是RDF URI引用或空白节点。

• 谓词,是RDF URI引用。

• 对象,是RDF URI引用、文字或空白节点。

RDF三元组通常按主语、谓词、对象的顺序编写。谓词也称为三元组的属性。

“张三认识李四”可以在RDF中表示为:

uri://people#张三12 http://xmlns.com/foaf/0.1/认识 uri://people#李四45

一组RDF三元组组成RDF图。RDF图的节点集是图中三元组的主题和对象的集合。

可以把三元组数据存储在一种叫作三元组仓库(triplestore)的专门数据库中,并使用SPARQL(SPARQL Protocol and RDF Query Language,SPARQL协议和RDF查询语言)查询,也可以将三元组存入图形数据库Neo4j中。

FrameNet项目正在建立一个人类和机器可读的英语词汇数据库,它基于如何在实际文本中使用单词的注释示例。从学生的角度来看,它是一个包含超过13000个单词意义的词典,其中大多数都带有注释示例,用于显示其含义和用法。对于自然语言处理的研究人员,超过200000个手动注释句子链接到1200多个语义框架,为语义角色标记提供了独特的训练数据集,用于信息提取、机器翻译、事件识别、情感分析等应用。对于语言学的学生和教师来说,它作为一个价值词典,具有核心英语词汇集的组合属性的独特详细证据。该项目自1997年以来一直在伯克利国际计算机科学研究所运作,主要由美国国家科学基金会支持,数据可免费下载。它已被世界各地的研究人员下载和使用,用于各种目的(参见FrameNet下载程序)。类似FrameNet的数据库已经用于构建中文、巴西葡萄牙语、瑞典语、日语、韩语等语言的语义,一个新项目正致力于跨语言对齐FrameNets。

中文FrameNet(CFN)是一个词汇数据库,包括框架、词汇单元和带注释的句子。它基于框架语义学理论,参考了伯克利的英语框架网工作,并得到了大型中文语料库的证据支持。CFN目前包含323个语义框架,3947个词汇单元,超过18000个句子,注释了句法和框架语义信息。

SEMAFOR是一个框架表示的语义分析包。

1.2.8 Hadoop分布式计算框架

互联网文本处理经常面临海量数据,需要分布式的计算框架来执行对网页重要度打分等计算。有的计算数据很少,但是计算量很大,还有些计算数据量比较大,但是计算量相对比较小。例如,计算圆周率是计算密集型,互联网搜索中的计算往往是数据密集型。所以出现了数据密集型的云计算框架Hadoop。MapReduce是一种常用的云计算框架。

Hadoop处理部分资源的管理器YARN(Yet Another Resource Negotiator)通过使用Spark(用于实时处理)、Hive(用于SQL)、HBase(用于NoSQL)等工具,使用户能够按照要求执行操作。

YARN的基本思想是将资源管理和作业调度/监视的功能分解为单独的守护进程。一个YARN集群拥有一个全局ResourceManager(RM),每个应用程序拥有一个ApplicationMaster(AM)。

RM是仲裁所有可用集群资源的主服务器,因此有助于管理在YARN系统上运行的分布式应用程序。YARN集群中的每个从节点都有一个NodeManager(节点管理器,NM)守护程序,它充当RM的从属节点。

除资源管理外,YARN还执行作业调度。YARN通过分配资源和计划任务执行所有处理活动。

YARN服务框架提供一流的支持和API,以便在YARN中托管本地长期运行的服务。简而言之,它作为一个容器编排平台,管理YARN上的容器化服务。它支持YARN中的Docker容器和传统的基于进程的容器。

YARN框架的职责包括执行配置解决方案和安装、生命周期管理(如停止、启动、删除服务)、向上/向下弹性化服务组件、在YARN上滚动升级服务、监控服务的健康状况和准备情况等。

YARN服务框架主要包括以下组件。

• 在YARN上运行的核心框架AM,用作容器协调器,负责所有服务生命周期管理。

• 一个RESTful的API服务器,供用户与YARN交互,通过简单的JSON规范部署和管理的服务。

• 由YARN服务注册表支持的DNS服务器,用于通过标准DNS查找在YARN上的服务。

接下来描述如何使用YARN服务框架在YARN上部署服务。

要启用YARN服务框架,请将yarn.webapp.api-service属性添加到yarn-site.xml并重新启动RM或在启动RM之前设置该属性。通过CLI(Command Line Interface,命令行界面)或REST API使用YARN服务框架需要此属性:

  <property>
    <description>
      在ResourceManager上启用服务REST API
    </description>
    <name>yarn.webapp.api-service.enable</name>
    <value>true</value>
  </property>

下面是一个简单的服务定义,在不编写任何代码的情况下它通过编写一个简单的spec文件在YARN上启动睡眠容器:

{
  "name": "sleeper-service",
  "components" :
    [
      {
        "name": "sleeper",
        "number_of_containers": 1,
        "launch_command": "sleep 900000",
        "resource": {
          "cpus": 1,
          "memory": "256"
       }
      }
    ]
}

用户可以使用以下命令在YARN上运行预先构建的示例服务:

yarn app -launch <service-name> <example-name>

例如,使用下面的命令在YARN上启动一个名为my-sleeper的睡眠服务:

yarn app -launch my-sleeper sleeper

为了开发YARN应用程序,首先将应用程序提交给YARN RM,这可以通过设置YarnClient对象来完成。启动YarnClient后,客户端可以设置应用程序上下文,准备包含AM的应用程序的第一个容器,然后提交应用程序。用户需要提供一些信息,例如有关运行应用程序需要可用的本地文件jar的详细信息,需要执行的实际命令(使用必要的命令行参数),操作系统环境设置(可选),描述为RM启动的Linux进程。

然后,YARN RM将在已分配的容器上启动AM(如指定的那样)。AM与YARN集群通信,并处理应用程序,以异步方式执行操作。在应用程序启动期间,ApplicationMaster的主要任务是:①与RM通信以协商和分配未来容器的资源;②在容器分配之后,通信YARN NM在其上启动应用程序容器。任务①可以通过AMRMClientAsync对象异步执行,事件处理方法在AMRMClientAsync. CallbackHandler类型的事件处理程序中指定,需要将事件处理程序显式设置为客户端。任务②可以通过启动一个可运行的对象来执行,然后在分配容器时启动容器。作为启动此容器的一部分,AM必须指定具有启动信息的ContainerLaunchContext,例如命令行规范、环境等。

在执行应用程序期间,AM通过NMClientAsync对象与NM进行通信。所有容器事件都由NMClientAsync.CallbackHandler处理,与NMClientAsync相关联。典型的回调处理程序处理客户端启动、停止、状态更新和错误。AM还通过处理AMRMClientAsync.CallbackHandler的getProgress()方法向RM报告执行进度。

除异步客户端外,还有某些工作流的同步版本(AMRMClient和NMClient)。建议使用异步客户端,因为(主观上)其具有更简单的用法。

以下是异步客户端的重要接口。

• 客户端< - >RM:通过使用YarnClient对象处理事件。

• AM< - >RM:通过使用AMRMClientAsync对象,由AMRMClientAsync.CallbackHandler异步处理事件。

• AM< - >NM:发射容器。使用NMClientAsync对象与NM通信,通过NMClientAsync.CallbackHandler处理容器事件。

客户端需要做的第一步是初始化并启动YarnClient:

YarnClient yarnClient = YarnClient.createYarnClient();
yarnClient.init(conf);
yarnClient.start();

设置客户端后,客户端需要创建应用程序,并获取其应用程序ID:

YarnClientApplication app = yarnClient.createApplication();
GetNewApplicationResponse appResponse = app.getNewApplicationResponse();

YarnClientApplication对新应用程序的响应还包含有关集群的信息,例如集群的最小/最大资源功能。这是必需的,以确保可以正确设置启动AM的容器的规范。

客户端的关键是设置ApplicationSubmissionContext,它定义了RM启动AM所需的所有信息。客户端需要将以下内容设置到上下文中。

• 申请信息:id、name。

• 队列、优先级信息:将向上下文提交应用程序的队列,为应用程序分配的优先级。

• 用户:提交应用程序的用户。

• ContainerLaunchContext:定义将在其中启动和运行AM的容器的信息。如前所述,ContainerLaunchContext定义了运行应用程序的所有必需信息,例如本地资源(二进制文件、jar文件等)、环境设置(CLASSPATH等)、要执行的命令和安全性Token。

Behemoth是一个基于Apache Hadoop的大规模文档处理的开源平台。它由一个简单的基于注释的文档实现,并由许多运行在这些文档上的模块组成。Behemoth的主要作用是简化文档分析器的部署,同时也为以下方面提供可重用的模块。

• 从常见数据源获取数据(Warc、Nutch等)。

• 文本处理(Tika、UIMA、GATE、语言识别)。

• 为外部工具生成输出(SOLR、Mahout)。

从Behemoth的根目录运行“mvn install”程序将获取依赖项,编译每个模块,运行测试并在每个模块的目标目录中生成一个jar文件。

为了在Hadoop集群上运行Behemoth,必须有一个作业文件。作业文件是基于模块生成的:用户可以生成多个作业文件并单独使用它们(例如一个用于Tika、一个用于GATE),或者使用一些自定义代码构建一个新模块,声明对模块Tika和GATE的依赖性,并为该新模块生成一个作业文件。

从Behemoth的根目录运行“mvn package”将在目标目录中为每个模块生成一个* -job.jar文件。然后,可以将这些作业文件与Hadoop一起使用。

第一步是使用核心模块中的CorpusGenerator将一组文档转换为Behemoth语料库。该类返回Behemoth文档的序列文件,然后可以进一步使用其他模块处理序列文件,命令如下:

hadoop jar core/target/behemoth-core-*-job.jar
com.digitalpebble.behemoth.util.CorpusGenerator
-i "path to corpus" -o "path for output file"

使用另一个Behemoth核心实用程序:CorpusReader,可以看到生成的序列文件的内容。以下命令显示Behemoth语料库中的所有内容:

hadoop jar core/target/behemoth-core-*-job.jar
com.digitalpebble.behemoth.util.CorpusReader
-i "path to generated Corpus"

返回如下:

url: file:/localPath/corpus/somedocument.rtf
contentType:
metadata: null
Annotations:

Behemoth中的Tika模块使用Apache Tika库将文档中的文本提取到Behemoth序列文件中。它提供各种识别和过滤选项。

此步骤的基本命令是:

hadoop jar tika/target/behemoth-tika-*-job.jar
com.digitalpebble.behemoth.tika.TikaDriver
-i "path to previous output from the CorpusGenerator" -o "path to output file"

Behemoth实现了语言识别和语言ID的文档过滤。我们可以通过运行如下命令来识别和检查语料库中不同语言的类型:

hadoop jar language-id/target/behemoth-lang*job.jar
com.digitalpebble.behemoth.languageidentification.LanguageIdDriver
-i corpusTika -o corpusTika-lang