人物识别(2)

接上
然后对文本应用分类函数。
List<List<CoreLabel>>entityList = classifier.classify(sentence);
返回CoreLabel对象众多列表实例中的一个。返回对象是一个包含另一个列表的列表,被包含的列表是corelabel对象的一个列表实例,CorLabel类表示一个带有附加信息的词。“内部”表包含这些词的列表。在下述代码行中的for-each语句外,引用变量,internalList,表示文本中的一个句子。在每个for-each语句内展示了内部列表中的每个词。词汇函数返回单词,同时获取函数返回单词的类型。
然后显示单词及其类型:
for (List<CoreLabel> internalList: entityList) {
for (CoreLabel coreLabel : internalList) {
String word = coreLabel.word();
String category = coreLabel.get(
CoreAnnotations.AnswerAnnotation.class);
System.out.println(word + ":" + category);
}
}

部分输出如下。由于所有单词都需要显示因此有所删减,O代表“其他”类型:
Joe:PERSON
was:O
the:O
last:O
person:O
to:O
see:O
Fred:PERSON
.:O
He:O

look:O
for:O
Fred:PERSON
为了滤除不相关的单词,用以下语句代替println,这会剔除其他类型,只保留你想要的类型。
if (!"O".equals(category)) {
System.out.println(word + ":" + category);
}

现在输出简单多了:
Joe:PERSON
Fred:PERSON
Boston:LOCATION
McKenzie:PERSON
Joe:PERSON
Vermont:LOCATION
IBM:ORGANIZATION
Sally:PERSON
Fred:PERSON

使用用于LingPipe的NER
本章前部分—使用用于NER的正则表达式一节中论述了LingPipe中正则表达式的使用。这里,我们将论述命名实体模型和ExactDictionaryChunker类如何用作NER分析。
使用LingPipe的命名实体模型
LingPipe有一些可以和chunking一起使用的命名实体模型。这些模型文件由一个序列化对象组成。这个序列化对象能从一个文件中读出然后应用于文本。它们实现Chunker接口。chunking处理产生一系列能对感兴趣的实体进行识别的chunking对象。
下表列出了一组NER模型,这些模型能从http://alias-i.com/lingpipe/web/models.html下载:

类型 语料库 文件
English News MUC-6 ne-en-news-muc6.
English Genes GeneTag ne-en-bio-genetag.HmmChunker
English Genomics GENIA ne-en-bio-genia.TokenShapeChunker

我们将使用ne-en-news-muc6中的模型
AbstractCharLmRescoringChunker文件解释了这个类的使用方法。
如下所示,我们以try-catch块开始来处理exceptions。打开这个文件并使用AbstractExternalizable类的静态readObject函数来创建一个Chunker类的实例。该方法能读取序列化模型:
try {
File modelFile = new File(getModelDir(),
"ne-en-news-muc6.AbstractCharLmRescoringChunker");
Chunker chunker = (Chunker)
AbstractExternalizable.readObject(modelFile);
...
} catch (IOException | ClassNotFoundException ex) {
// Handle exception
}

Chunker和Chunking接口提供了与文本中的一组chunks共同工作的方法。它的chunk函数返回一个实现Chunking实例的对象。如下代码序列展示文本里每个句子中找到的chunks:
for (int i = 0; i < sentences.length; ++i) {
Chunking chunking = chunker.chunk(sentences[i]);
System.out.println("Chunking=" + chunking);
}

这个代码序列输出如下:
Chunking=Joe was the last person to see Fred. : [0-3:PERSON@-Infinity,
31-35:ORGANIZATION@-Infinity]
Chunking=He saw him in Boston at McKenzie's pub at 3:00 where he paid
$2.45 for an ale. : [14-20:LOCATION@-Infinity, 24-32:PERSON@-Infinity]
Chunking=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 : [0-3:PERSON@-
Infinity, 20-27:ORGANIZATION@-Infinity, 71-74:ORGANIZATION@-Infinity,
109-113:ORGANIZATION@-Infinity]

另外,我们能使用Chunk类中的函数从所示信息中提取特定的信息片段。用如下for-each语句代替前面的语句。本章前面—使用LingPipe的RegExChunker类,这一节中已经论述了这个displayChunkSet函数:
for (String sentence : sentences) {
displayChunkSet(chunker, sentence);
}

输出结果如下,然而输出类型并不总是和正确的实体类型相匹配。
Type: PERSON Entity: [Joe] Score: -Infinity
Type: ORGANIZATION Entity: [Fred] Score: -Infinity
Type: LOCATION Entity: [Boston] Score: -Infinity
Type: PERSON Entity: [McKenzie] Score: -Infinity
Type: PERSON Entity: [Joe] Score: -Infinity
Type: ORGANIZATION Entity: [Vermont] Score: -Infinity
Type: ORGANIZATION Entity: [IBM] Score: -Infinity
Type: ORGANIZATION Entity: [Fred] Score: -Infinity
使用ExactDictionaryChunker类
ExactDictionaryChunker类提供了一个简单方法来创建实体和实体类型的字典。这个字典能用于文本中寻找实体和实体类型。它使用一个MapDictionary对象来储存实体然后根据这个字典使用ExactDictionaryChunker类来抽取chunks。
AbstractDictionary接口支持实体、类别和分数的基本操作。这里的分数用作匹配处理。MapDictionary和TrieDictionary类是AbstractDictionary的两个接口,TrieDictionary类使用一个字符trie结构储存信息。这种方法占用更少内存,实例将使用MapDictionary类。
为了解释这个方法,我们以一个MapDictionary类的声明开头:
private MapDictionary dictionary;
这个字典包含了我们想找到的实体。使用initializeDictionary函数对模型进行初始化。这里使用的DictionaryEntry构造函数接受三类参数:
String:实体名
String:实体类型
Double:代表实体的分数
确定是否匹配时会用到分数,声明一部分实体然后添加到字典。
private static void initializeDictionary() {
dictionary = new MapDictionary<String>();
dictionary.addEntry(
new DictionaryEntry<String>("Joe","PERSON",1.0));
dictionary.addEntry(
new DictionaryEntry<String>("Fred","PERSON",1.0));
dictionary.addEntry(
new DictionaryEntry<String>("Boston","PLACE",1.0));
dictionary.addEntry(
new DictionaryEntry<String>("pub","PLACE",1.0));
dictionary.addEntry(
new DictionaryEntry<String>("Vermont","PLACE",1.0));
dictionary.addEntry(
new DictionaryEntry<String>("IBM","ORGANIZATION",1.0));
dictionary.addEntry(
new DictionaryEntry<String>("Sally","PERSON",1.0));
}

ExactDictionaryChunker实例将会使用这个字典。ExactDictionaryChunker类参数说明如下:
Dictionary:一个包含实体的字典
TokenizerFactory:chunker使用的一个tokenizer
boolean:若为真,chunker返回所有匹配的值
boolean:若为真,区分大小写的匹配
匹配允许重叠。例如,短语“第一国家银行”,银行实体能独立使用也能和短语中的其他成分配合使用,第三个参数决定是否返回所有的匹配值。
下列代码序列中,字典经过了初始化。然后我们使用印欧语系的分词器创建一个ExactDictionaryChunker类的实例,这里返回所有的匹配值并忽略词项:
initializeDictionary();
ExactDictionaryChunker dictionaryChunker
= new ExactDictionaryChunker(dictionary,
IndoEuropeanTokenizerFactory.INSTANCE, true, false);

逐句使用dictionaryChunker类。如下代码所示,我们将使用本章前面—使用LingPipe的RegExChunker类,这节所提到的displayChunkSet函数:
for (String sentence : sentences) {
System.out.println("\nTEXT=" + sentence);
displayChunkSet(dictionaryChunker, sentence);
}

执行完,将得到以下输出:
TEXT=Joe was the last person to see Fred.
Type: PERSON Entity: [Joe] Score: 1.0
Type: PERSON Entity: [Fred] Score: 1.0

TEXT=He saw him in Boston at McKenzie’s pub at 3:00 where he paid $2.45
for an ale.

Type: PLACE Entity: [Boston] Score: 1.0
Type: PLACE Entity: [pub] Score: 1.0
TEXT=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
Type: PERSON Entity: [Joe] Score: 1.0
Type: PLACE Entity: [Vermont] Score: 1.0
Type: ORGANIZATION Entity: [IBM] Score: 1.0
Type: PERSON Entity: [Sally] Score: 1.0
Type: PERSON Entity: [Fred] Score: 1.0
任务完成的很漂亮,但需要很大的精力创建包含大量词汇的字典。
训练模型
我们将使用OpenNLP来论述如何训练一个模型。训练文件必须满足以下要求:
 包含区分实体边界的标记
 每行一个句子
使用文件名为en-ner-person.train的模型文件:
<START:person> Joe <END> was the last person to see <START:person>
Fred <END>.
He saw him in Boston at McKenzie's pub at 3:00 where he paid $2.45 for
an ale.
<START:person> Joe <END> wanted to go to Vermont for the day to visit
a cousin who works at IBM, but <START:person> Sally <END> and he had
to look for <START:person> Fred <END>.

实例中的几个方法都能剔除异常信息。上面的这些语句将被如下的try-with-resource块代替,模型的输出流也是创建于此:
try (OutputStream modelOutputStream = new BufferedOutputStream(
new FileOutputStream(new File("modelFile")));) {
...
} catch (IOException ex) {
// Handle exception
}

在try-with-resource块内,使用PlainTextByLineStream类创建OutputStream对象。这个类的构造函数调用FileInputStream实例并把每一行作为一个String对象返回。en-ner-person.train文件用作输入文件,像这里展示的一样,UTF-8指所使用的编码序列。
ObjectStream lineStream = new PlainTextByLineStream(
new FileInputStream(“en-ner-person.train”), “UTF-8”);
lineStream对象包含用标签标注的文本里描写的实体的数据流。这些需要转换为可训练模型的NameSample对象。这个转换由如下显示的NameSampleDataStream类完成。一个NameSample对象记录了文中已发现实体的名字:
ObjectStream<NameSample> sampleStream =
new NameSampleDataStream(lineStream);

通过如下程序执行train函数:
TokenNameFinderModel model = NameFinderME.train(
"en", "person", sampleStream,
Collections.<String, Object>emptyMap(), 100, 5);

下表详细显示了这个函数的参数:
参数 含义
“en” 语言
“person” 实体类型
sampleStream 采样数据
null 资源
100 迭代的次数
5 截止条件
然后模型序列化到一个输出文件:
model.serialize(modelOutputStream);
输出如下。输出有所缩短以节省空间。关于模型创建的基本信息详细描述如下:
Indexing events using cutoff of 5
Computing event counts… done. 53 events
Indexing… done.
Sorting and merging events… done. Reduced 53 events to 46.
Done indexing.
Incorporating indexed data for training…
done.
Number of Event Tokens: 46
Number of Outcomes: 2
Number of Predicates: 34
…done.
Computing model parameters …
Performing 100 iterations.
1: … loglikelihood=-36.73680056967707 0.05660377358490566
2: … loglikelihood=-17.499660626361216 0.9433962264150944
3: … loglikelihood=-13.216835449617108 0.9433962264150944
4: … loglikelihood=-11.461783667999262 0.9433962264150944
5: … loglikelihood=-10.380239416084963 0.9433962264150944
6: … loglikelihood=-9.570622475692486 0.9433962264150944
7: … loglikelihood=-8.919945779143012 0.9433962264150944

99: … loglikelihood=-3.513810438211968 0.9622641509433962
100: … loglikelihood=-3.507213816708068 0.9622641509433962
评估模型
模型可以使用TokenNameFinderEvaluator类进行评估。评估使用标记的示例文本。对于这个简单的示例,先创建包含如下文本的en-ner-person.eval文件:
<START:person> Bill <END> went to the farm to see <START:person> Sally
<END>.
Unable to find <START:person> Sally <END> he went to town.
There he saw <START:person> Fred <END> who had seen <START:person>
Sally <END> at the book store with <START:person> Mary <END>.

接下来的代码用来实施评估。先前的模型用作TokenNameFinderEvaluator构造函数的参数。基于评估文件创建NameSampleDataStream实例。用TokenNameFinderEvaluator类的evaluate函数进行评估。
TokenNameFinderEvaluator evaluator =
new TokenNameFinderEvaluator(new NameFinderME(model));
lineStream = new PlainTextByLineStream(
new FileInputStream("en-ner-person.eval"), "UTF-8");
sampleStream = new NameSampleDataStream(lineStream);
evaluator.evaluate(sampleStream);

为了判断模型在测试数据中表现,执行getFMeasure函数,结果如下:
FMeasure result = evaluator.getFMeasure();
System.out.println(result.toString());
如下的输出显示了准确度,查全率和F-测量结果。找到的实体有50%与测试数据精确匹配。查全率定义为语料库中相同位置找到的实体百分比。性能测量是统一两者的方式,定义为F1 = 2*精确度*查全率/(查全率+精确度)。
Precision: 0.5
Recall: 0.25
F-Measure: 0.3333333333333333
为创建一个更好的模型,数据和测试集应尽量大。注意这里只论述了训练和测试POS模型的基本方法。
总结
NER涉及实体检测和实体分类。一般的类别包括名字,地理位置和事物。许多应用都用它为搜索引擎提供技术支持、解决引用以及寻找文本的含义。这个过程经常用于下游任务。
我们探究了几个实现NER的技术。正则表达式是一个同时支持核心Java类和NLP APIs的方法。这个技术在很多应用中都有用,而且有很多可用的正则表达式库。
基于字典的方法在一些应用中也很有效。但有时需要大量精力完善字典。我们使用LingPipe的MapDictionary类来解释这个方法。
训练的模型也能被用来实现NER。我们测试了一些模型并论述了如何使用OpenNLP NameFinderME来训练模型。这个过程和之前训练过程相似。
下一章,我们将学习如何检测词性如名词、形容词和介词。
最后,想感谢z老师对我的信任与支持鼓励。
zerof,stay hungry stay foolish!

你可能感兴趣的:(机器学习,信号处理,NER)