人物识别(1)

接触了两本书:
machine learning:hands on for developers and technical professionals.2015
natural language processing with java .march 2015
第一本书做了一点校对工作,第二本书完成了第四章翻译初稿和一遍校对。现贴上自己的翻译稿,小小激励自己一下~

第四章 人物识别

寻找人和物的过程叫做命名实体识别(NER)。例如人和地名等实体都是和能区分他们的类名联系在一起。一个类可以简单的命名为“人”。常用的实体类别包括:

位置
组织

时间
网址
在一个文档中找出名字,位置以及各类实体是重要且实用的NLP任务。他们在很多地方都有用到,比如创建简单的搜索,提问处理,解析目录,文本消歧,以及寻找文本的含义。例如,有时候用NER只为寻找属于一个简单类别的实体。使用类别,通过搜索就能分离这些项目类型,其他NLP任务有诸如将NER用于POS标签中或用于交叉索引任务中。
NER处理涉及两种任务:
实体检测
实体分类
实体检测和寻找文本中的实体位置相关,一旦被定位,判断发现的实体所属类别很重要。两种任务执行的结果能被用于解决其他类似搜索和判断文本含义之类的任务。打个比方,从一部电影或者书的评述中识别名字来帮助找到可能感兴趣的其他电影或者书。抽取位置信息能够帮助提供相关服务的参考信息。
为什么NER难?
像很多NLP任务一样,NER不总是简单的。尽管文本的分词会展示它的组成元素,但理解这些元素是什么依然很困难。语言的模糊性导致即使使用正确的名词也不一定能理解其含义。例如penny和faith,真实的姓名,他们也可能用于财物度量和信仰,同样的,例如Georgia可以用来命名一个乡镇、行政区以及一个人。
一些短语理解起来很有挑战性。“城市会议展览”中就包含真实的实体。当熟知实体 所用领域时,实体的列表就非常有用且易于实现。
NER在句子层面的应用很典型,一个短语很容易跨越一个句子导致一个实体识别错误。例如,下行句子中:
“Bob去南方,Dakota去北方”
如果忽视句字的边界,那么我们意外的发现南方达科他(译者注:Dakota为美国过去一地区名,达科他, 现分为南、北达科他州)这一位置实体,特别的文档比如网址,邮箱地址也很难从文中分离。如果再考虑实体形式的多样性,那么识别变得更为艰难。例如,电话号码中使用括号吗?使用破折号或句号或者一些其他的符号来分隔句子?我们需要考虑国际电话号码吗?
这些因素引发了对成熟的NER技术的需求。
名字识别的技术
有许多可供选择的NER技术,一些使用正则表达式,另一些基于预定义的字典。正则表达式有丰富的表达能力,能够分离实体,实体名字典能和文本中的分词通过对比进行匹配。
另一个常用的NER方法是使用训练模型来检测实体的存在,这些模型依赖于所要寻找的实体以及目标语言。适用于某一个领域的模型,比如网页,可能不适合另一个不同的领域,比如媒体新闻。
训练后的模型使用文本中的备注块来识别感兴趣的实体,为了测试训练模型的好坏,可以使用以下几个方法:
精确度:标准训练数据中包含的实体和模型找到的能精确匹配的实体数目的百分比。
查全率:在相同位置能找到的语料库中定义的的实体所占百分比。
性能测量:通过F1 = 2*精确度*查全率/(查全率+精确度)综合精确度和查全率的一种测量方式
衡量模型好坏的时候将会用到这些测量方式。
NER也常用于实体识别和实体分块。分块是检测一些诸如名词,动词或者其他成分的文本分析。人们趋向于把句子划分成独立的部分。这些部分形成一个决定句子含义的结构。NER处理将创建跨越性的文本例如“英格兰的女王”。然而,在这些跨越文本里有其他的类似“英格兰”的实体。
列表和正则表达式
使用“标准”实体和正则表达式来识别命名实体也是一种技术。命名实体有时指合适的名词。标准实体列表由一系列乡镇、通用名、月份或者频繁引用的地理位置组成。常用地名表,是一个包含配合地图使用的地理位置信息,能提供位置相关实体的列表。然而,维护这样的表需要时间代价。也可能有语言和场景的特殊说明。改动这些表很枯燥乏味。在本章后续章节—使用ExactDictionaryChunker类中将论证这个方法。
在识别实体的时候正则表达式很有用。强大的句法为很多场合提供足够的灵活性来精确划分感兴趣的实体。然而,灵活性也可能导致难以理解和掌握。本章将论述几个正则表达式方法。
数值分类
数值分类决定一个字是否是一个实体的开头,一个实体的延续或者不是一个实体。示例文本中标记了分离的实体。分类器一旦设计好就能针对不同问题领域用不同数据集训练,这个方法的缺陷是需要有人注释示例文本,这需要花费时间代价,另外还和领域相关。
我们将测试用于NER的几个方法。首先,以解释正则表达式如何用于识别实体开始。
使用用于NER的正则表达式
正则表达式能用来识别文献中的实体。我们将调查两个常用的途径:
第一个方法使用Java支持的正则表达式,当实体相对简单而且具有统一形式时,此方法很有用。
第二个方法使用为特殊用途定制的正则表达式类。为了论证这点,我们将使用LingPipe的RegExChunker类(译者注:LingPipe是alias公司开发的一款自然语言处理软件包,目前最高版本是4.1.0,功能非常强大,最重要的是文档很详细,每个模型甚至连参考论文都列出来了,不仅使用方便,也非常适合模型的学习。 地址:http:/alias-i.com/lingpipe/)。
用正则表达式的方式可以利用前人已研究出的成果。预定义和测试后的表达来源广泛。http://regexlib.com/Default.aspx能找到这样一个类似的库。我们将使用这个库中的几个正则表达式作为实例。
为了测试这些方法的性能,大多数实例中会用到如下文本:
private static String regularExpressionText
= “He left his email address ([email protected]) and his ”
+ “phone number,800-555-1234. We believe his current address ”
+ “is 100 Washington Place, Seattle, CO 12345-1234. I ”
+ “understand you can also call at 123-555-1234 between ”
+ “8:00 AM and 4:30 most days. His URL is http://example.com ”
+ “and he was born on February 25, 1954 or 2/25/1954.”;
使用Java的正则表达式来寻找实体
为了解释这些表达式的使用,我们以几个简单的例子开头。这些例子以如下声明开头。这是为识别电话号码类型而设计的一个简单的表达式:
String phoneNumberRE = “\d{3}-\d{3}-\d{4}”;
使用如下代码来测试表达式。模式类的编译函数调用一个正则表达式并编译成模式实体。然后它的匹配函数能执行目标文本,返回一个匹配实体。这个实体允许我们重复识别相匹配的正则表达式:
Pattern pattern = Pattern.compile(phoneNumberRE);
Matcher matcher = pattern.matcher(regularExpressionText);
while (matcher.find()) {
System.out.println(matcher.group() + ” ” + matcher.start()
+ “:” + matcher.end() + ““);
}
当匹配成功时查找函数将返回真。它的分组函数返回匹配这个表达式的文本。开始和结束函数提供匹配文本在目标文本的位置。
完成后,将得到以下输出:
800-555-1234 [68:80]
123-555-1234 [196:208]
其他的许多正则表达式用法相近。这些已经在下表中列出。第三列是类似的正则表达式用在先前代码行时产生的输出:
实体类型 正则表达式 输出
网址 \b(https?|ftp|file|ldap)://
[-A-Za-z0-9+&@#/%?=~_|!:,.;]*[-AZa-
z0-9+&@#/%=~_|] http://example.
com [256:274]
ZIP码 [0-9]{5}(\-?[0-9]{4})? 12345-1234
[150:160]
邮箱 [a-zA-Z0-9’._%+-]+@(?:[a-zA-Z0-9-
]+\.)+[a-zA-Z]{2,4} rgb@colorworks.
com [27:45]
时间 (([0-1]?[0-9])|([2][0-3])):([0-
5]?[0-9])(:([0-5]?[0-9]))? 8:00 [217:221]
4:30 [229:233]
日期 ((0?[13578]|10|12)(-|\/)
(([1-9])|(0[1-9])|([12])
([0-9]?)|(3[01]?))(-|\/)
((19)([2-9])(\d{1})|(20)
([01])(\d{1})|([8901])
(\d{1}))|(0?[2469]|11)(-|\/)
(([1-9])|(0[1-9])|([12])([0-
9]?)|(3[0]?))(-|\/)((19)
([2-9])(\d{1})|(20)([01])
(\d{1})|([8901])(\d{1}))) 2/25/1954
[315:324]
我们可能还使用过许多其他的正则表达式。然而,这些实例阐述了基本的技巧。像日期正则表达式展示的一样,一些表达式非常复杂。
正则表达式常遗漏一些实体或者把非实体误认为实体。例如,如果我们用下述表达代替文本:
regularExpressionText =
“(888)555-1111 888-SEL-HIGH 888-555-2222-J88-W3S”;
执行代码将返回:
888-555-2222 [27:39]
遗漏了开始的两个电话号码数字而且把“区域编号”误认为电话号码。
我们也可以使用 | 操作符同时搜索两个或两个以上的正则表达式。在下面的表达中,三个正则表达式通过这个操作符组合在一起。使用和上述表格中相似的实体声明:
Pattern pattern = Pattern.compile(phoneNumberRE + “|”
+ timeRE + “|” + emailRegEx);
当使用前面章节开头部分定义过的初始正则表达式文本测试时,我们得到了以下输出:
[email protected] [27:45]
800-555-1234 [68:80]
123-555-1234 [196:208]
8:00 [217:221]
4:30 [229:233]
使用LingPipe的RegExChunker类
RegExChunker类使用分块来寻找文中的实体。类使用一个正则表达式来代表一个实体。它的分块函数返回一个与之前实例中所用实体使用方法一样的块实体。
RegExChunker类的创建者说明了三点:
字符串:一个正则表达式
字符串:实体或者目录的类型
双精度:分数值
我们将使用一个表示时间的正则表达式来解释这个类,这个正则表达式会展示在接下来的例子中。这个正则表达式与本章中使用Java的正则表达式来寻找实体这一节所使用的正则表达式相同。
然后创建分块器距离:
String timeRE =
“(([0-1]?[0-9])|([2][0-3])):([0-5]?[0-9])(:([0-5]?[0-9]))?”;
Chunker chunker = new RegExChunker(timeRE,”time”,1.0);
分块函数连同displayChunkset函数一起使用,如下:
Chunking chunking = chunker.chunk(regularExpressionText);
Set chunkSet = chunking.chunkSet();
displayChunkSet(chunker, regularExpressionText);
displayChunkset函数展示在下面的代码中。它返回一个分块距离的集合。我们可以使用各种方法来显示块中特殊说明的部分:
public void displayChunkSet(Chunker chunker, String text) {
Chunking chunking = chunker.chunk(text);
Set set = chunking.chunkSet();
for (Chunk chunk : set) {
System.out.println(“Type: ” + chunk.type() + ” Entity: [”
+ text.substring(chunk.start(), chunk.end())
+ “] Score: ” + chunk.score());
}
}
输出如下:
Type: time Entity: [8:00] Score: 1.0
Type: time Entity: [4:30] Score: 1.0+95
我们可以选择性声明一个简单的类来概括其他场合重复使用的正则表达式,接下来,声明TimeRegexChunker类,它支持时间实体的识别:
public class TimeRegexChunker extends RegExChunker {
private final static String TIME_RE =
“(([0-1]?[0-9])|([2][0-3])):([0-5]?[0-9])(:([0-5]?[0-9]))?”;
private final static String CHUNK_TYPE = “time”;
private final static double CHUNK_SCORE = 1.0;
public TimeRegexChunker() {
super(TIME_RE,CHUNK_TYPE,CHUNK_SCORE);
}
}
为使用这个类,用以下声明代替这部分内容开头的分块器的声明:
Chunker chunker = new TimeRegexChunker();
输出与之前相同。
使用NLP编程接口
我们将使用开源NLP、斯坦福编程接口和LingPipe来展示NER处理。这三个中的每一个都提供了可供选择的文本识别实用技巧。下述声明将用作示例文本论述APIs:
String sentences[] = {“Joe was the last person to see Fred. “,
“He saw him in Boston at McKenzie’s pub at 3:00 where he ”
+ ” paid $2.45 for an ale. “,
“Joe wanted to go to Vermont for the day to visit a cousin who ”
+ “works at IBM, but Sally and he had to look for Fred”};

使用用于NER的OpenNLP
该小节将论述如何基于OpenNLP API使用TokenNameFinderModel类进行自然语言处理,另外,还将演示如何确定实体识别的正确率。
通常做法是将文本转换为一系列独立句子,使用一个合适的模型来创建一个TokenNameFinderModel类的一个实例,然后使用查找函数来识别文本中的实体。
以下例子论述了TokenNameFinderModel类的使用。实例先针对一个简单句再使用复杂句。句子如下:
String sentence = “He was the last person to see Fred.”;
我们将分别使用en-token.bin文件中找到的模型和en-ner-person.bin文件编译器以及名字查找器模型。用如下带有资源的try函数块打开用于这些文件的输入流对象:
try (InputStream tokenStream = new FileInputStream(
new File(getModelDir(), “en-token.bin”));
InputStream modelStream = new FileInputStream(
new File(getModelDir(), “en-ner-person.bin”));) {

} catch (Exception ex) {
// Handle exceptions
}
在try函数体中,创建TokenizerModel和Tokenizer对象:
TokenizerModel tokenModel = new TokenizerModel(tokenStream);
Tokenizer tokenizer = new TokenizerME(tokenModel);
接下来,使用人类模型创建NameFinderME类的一个实例:
TokenNameFinderModel entityModel =
new TokenNameFinderModel(modelStream);
NameFinderME nameFinder = new NameFinderME(entityModel);
现在可以使用标记函数来标记文本,并使用查找函数来识别文本中的人。查找函数将使用标记的字符串数组作为输入,并返回如下的span对象数组:
String tokens[] = tokenizer.tokenize(sentence);
Span nameSpans[] = nameFinder.find(tokens);
你可能还记得,在第三章查找句子部分讨论过的span类,这个类包含已找到实体的位置信息。实际的字符串实体仍然存在于词汇数组中:
下述展示了句子中找到的人,其位置信息和人分别显示在不同的行中:
for (int i = 0; i < nameSpans.length; i++) {
System.out.println(“Span: ” + nameSpans[i].toString());
System.out.println(“Entity: ”
+ tokens[nameSpans[i].getStart()]);
}
输出如下:
Span: [7..9) person
Entity: Fred
我们经常和复杂句打交道。为了演示复杂句,使用先前定义的句子字符串数组并用以下语句代替前面的声明。逐句调用标记函数,然后以同样的方式显示实体信息:
for (String sentence : sentences) {
String tokens[] = tokenizer.tokenize(sentence);
Span nameSpans[] = nameFinder.find(tokens);
for (int i = 0; i < nameSpans.length; i++) {
System.out.println(“Span: ” + nameSpans[i].toString());
System.out.println(“Entity: ”
+ tokens[nameSpans[i].getStart()]);
}
System.out.println();
}
输出如下。由于第二个句子中不包含人因此检测到的两个人中间会多出一行空白:
Span: [0..1) person
Entity: Joe
Span: [7..9) person
Entity: Fred

Span: [0..1) person
Entity: Joe
Span: [19..20) person
Entity: Sally
Span: [26..27) person
Entity: Fred
决定实体的准确度
TokenNameFinderModel识别文本中的实体同时还为这个实体计算了一个可能性。我们能使用如下行代码中显示的probs函数获取这个信息。这个函数返回一个响应nameSpans数组元素的双精度数组。
double[] spanProbs = nameFinder.probs(nameSpans);
使用查找函数后立刻把这行语句添加到之前实例中,然后在程序体的最后添加如下一行:
System.out.println(“Probability: ” + spanProbs[i]);
当完成运行时,将得到以下输出。这个概率反映了指定实体的置信度。对第一个实体来说,模型有80.529%确信“Joe”是一个人:
Span: [0..1) person
Entity: Joe
Probability: 0.8052914774025202
Span: [7..9) person
Entity: Fred
Probability: 0.9042160889302772
Span: [0..1) person
Entity: Joe
Probability: 0.9620970782763985
Span: [19..20) person
Entity: Sally
Probability: 0.964568603518126
Span: [26..27) person
Entity: Fred
Probability: 0.990383039618594
使用其他实体类型
OpenNLP支持以下列表包含的不同的库。这些模型能从http://opennlp.sourceforge.net/models-1.5/.下载。前缀en说明默认语言为英语,ner暗示用于NER的模型。
查找模型英文名 文件名
Location name finder model en-ner-location.bin
Money name finder model en-ner-money.bin
Organization name finder model en-ner-organization.bin
Percentage name finder model en-ner-percentage.bin
Person name finder model en-ner-person.bin
Time name finder model en-ner-time.bin
如果要为使用一个不同的模型文件而修改声明,可以参考例句:
InputStream modelStream = new FileInputStream(
new File(getModelDir(), “en-ner-time.bin”));) {
【 当使用en-ner-money.bin模型时,先前代码行中的词汇数组指针必须增加一个,否则,返回值都是美元符号】
下表展示了各类输出。
模型 输出
en-ner-location.bin Span: [4..5) location
Entity: Boston
Probability: 0.8656908776583051
Span: [5..6) location
Entity: Vermont
Probability: 0.9732488014011262
en-ner-money.bin Span: [14..16) money
Entity: 2.45
Probability: 0.7200919701507937
en-ner-organization.bin Span: [16..17) organization
Entity: IBM
Probability: 0.9256970736336729
en-ner-time.bin The model was not able to detect time in
this text sequence
示例文本查找时间实体失败说明这个模型不足以确定文本中找到的实体是时间。
处理多种实体类型
我们也能同时处理多种实体类型。这涉及到创建基于一个循环里各自模型的NameFinderME类的实例,逐句应用模型并记录找到的实体。
我门将用下面的实例解释这个处理过程。它需要重写之前的try块以便在块中创建输入流范例,如下:
try {
InputStream tokenStream = new FileInputStream(
new File(getModelDir(), “en-token.bin”));
TokenizerModel tokenModel = new TokenizerModel(tokenStream);
Tokenizer tokenizer = new TokenizerME(tokenModel);

} catch (Exception ex) {
// Handle exceptions
}
Try块中,定义了一个记录模型文件名的字符串数组。如下所示,使用人、地理位置以及组织这三个模型:
String modelNames[] = {“en-ner-person.bin”,
“en-ner-location.bin”, “en-ner-organization.bin”};
创建一个数组表来记录发现的实体:
ArrayList list = new ArrayList();
使用for-each语句一次装载一个模型然后创建一个NameFinderME类的实例:
for(String name : modelNames) {
TokenNameFinderModel entityModel = new TokenNameFinderModel(
new FileInputStream(new File(getModelDir(), name)));
NameFinderME nameFinder = new NameFinderME(entityModel);

}
先前我们并没有识别所找到的实体具体来自哪个句子。但这并不难实现。只需要使用一个简单的for语句代替for-each语句来追踪句子的索引。如下例所示,在前例基础上使用整数变量index来追踪句子,否则,代码运行结果和之前一样:
for (int index = 0; index < sentences.length; index++) {
String tokens[] = tokenizer.tokenize(sentences[index]);
Span nameSpans[] = nameFinder.find(tokens);
for(Span span : nameSpans) {
list.add(“Sentence: ” + index
+ ” Span: ” + span.toString() + ” Entity: ”
+ tokens[span.getStart()]);
}
}
找到的实体显示格式如下:
for(String element : list) {
System.out.println(element);
}
Sentence: 0 Span: [0..1) person Entity: Joe
Sentence: 0 Span: [7..9) person Entity: Fred
Sentence: 2 Span: [0..1) person Entity: Joe
Sentence: 2 Span: [19..20) person Entity: Sally
Sentence: 2 Span: [26..27) person Entity: Fred
Sentence: 1 Span: [4..5) location Entity: Boston
Sentence: 2 Span: [5..6) location Entity: Vermont
Sentence: 2 Span: [16..17) organization Entity: IBM
使用用于NER的Stanford API
我们将论述过去常常用于NER的CRFClassifier类。这个类实现线性链条件随机场(CRF)序列模型。
为了解释CRFClassifier类的使用,我们以一个分类器文件字符串声明开始,如下所示:
String model = getModelDir() +
“\english.conll.4class.distsim.crf.ser.gz”;
然后使用模型创建分类器:
CRFClassifier classifier =
CRFClassifier.getClassifierNoExceptions(model);
分类函数输入值为一个表示待处理文本的字符串。因此为使用文本句子,我们需要把它转换为一个简单的字符串:
String sentence = “”;
for (String element : sentences) {
sentence += element;
}

你可能感兴趣的:(机器学习)