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所示。
图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