五、检测词性
以前,我们识别文本的各个部分,如人物、地点和事物。在这一章中,我们将考察寻找词性 ( 词性)的过程。这些是我们在英语中认为是语法元素的部分,如名词和动词。我们会发现,单词的上下文是决定它是什么类型单词的一个重要方面。
我们将研究标记过程,它本质上是将一个 POS 分配给一个标记。这个过程是检测 POS 的核心。我们将简要讨论为什么标记是重要的,然后检查使检测词性变得困难的各种因素。各种自然语言处理(NLP)API 随后被用来说明标记过程。我们还将演示如何训练一个模型来处理专门的文本。
我们将在本章中讨论以下主题:
- 标记过程
- 使用 NLP APIs
标记过程
标记是将描述分配给令牌或部分文本的过程。这种描述称为标签。位置标记是将位置标记分配给令牌的过程。这些标签通常是语法标签,如名词、动词和形容词。例如,考虑下面的句子:
“奶牛跳过了月亮。”
对于这些初始示例中的许多示例,我们将说明 POS tagger 使用 OpenNLP tagger 的结果,这将在本章后面的使用 OpenNLP POS tagger一节中讨论。如果我们在前面的例子中使用这个标记符,我们将得到下面的结果。注意,单词后面是一个正斜杠,然后是它们的 POS 标签。这些标签将很快得到解释:
The/DT cow/NN jumped/VBD over/IN the/DT moon./NN
根据上下文,单词可能有多个相关联的标签。例如,单词 saw 可以是名词,也可以是动词。当一个单词可以被分类成不同的类别时,诸如其位置、其附近的单词或类似信息的信息被用于概率性地确定适当的类别。例如,如果一个单词前面是限定词,后面是名词,那么将这个单词标记为形容词。
一般的标记过程包括标记文本、确定可能的标记和解决不明确的标记。算法用于执行位置识别(标记)。有两种通用方法:
- 基于规则的:基于规则的标签使用一组规则,以及一个单词和可能标签的字典。当一个单词有多个标签时,使用这些规则。规则通常使用前面和/或后面的单词来选择标签。
- 随机标签:随机标签要么基于马尔可夫模型,要么基于线索,要么使用决策树,要么使用最大熵。马尔可夫模型是有限状态机,其中每个状态有两种概率分布。它的目标是为一个句子找到最佳的标签序列。隐马尔可夫模型 ( HMM )也有使用。在这些模型中,状态转换是不可见的。
最大熵标记器使用统计学来确定单词的词性,并且经常使用语料库来训练模型。语料库是用词性标签标注的单词的集合。许多语言都有语料库。这些都需要很大的努力去开发。常用的语料库有宾夕法尼亚树库(https://www.seas.upenn.edu/~pdtb//)或布朗语料库(www . Essex . AC . uk/linguistics/external/clmt/W3C/Corpus _ ling/content/corpora/list/private/Brown/Brown . html
)。
来自 Penn Treebank 语料库的示例说明了 POS 标记,如下所示:
Well/UH what/WP do/VBP you/PRP think/VB about/IN
the/DT idea/NN of/IN ,/, uh/UH ,/, kids/NNS having/VBG
to/TO do/VB public/JJ service/NN work/NN for/IN a/DT
year/NN ?/.
英语传统上有九种词类:名词、动词、冠词、形容词、介词、代词、副词、连词和感叹词。然而,更完整的分析通常需要额外的类别和子类别。已经发现了多达 150 种不同的词类。在某些情况下,可能需要创建新的标签。下表列出了一个简短的列表。这些是我们将在本章中经常使用的标签:
| 部分 | 意为 |
| 神经网络 | 名词,单数,还是复数 |
| 暗行扫描(Dark Trace) | 限定词 |
| 动词 | 动词,基本形式 |
| VBD | 动词,过去式 |
| VBZ | 动词,第三人称单数现在时 |
| 在…里 | 介词或从属连词 |
| NNP | 专有名词,单数 |
| 到 | 到 |
| 姐姐(网络用语)ˌ法官ˌ裁判员(judges) | 形容词 |
下表显示了更全面的列表。本榜单改编自www . ling . upenn . edu/courses/Fall _ 2003/ling 001/Penn _ tree bank _ pos . html
。宾夕法尼亚大学(Penn)树库标签集的完整列表可以在 http://www.comp.leeds.ac.uk/ccalas/tagsets/upenn.html找到。一组标签被称为标签集:
标签 | 描述 | 标签 | 描述 |
---|---|---|---|
抄送 | 并列连词 | PRP 元 | 所有格代名词 |
激光唱片 | 基数 | 铷 | 副词 |
暗行扫描(Dark Trace) | 限定词 | RBR | 副词,比较 |
前妻;前夫 | 存在主义 | 随机阻塞系统(Random Barrage System 的缩写) | 副词,最高级 |
转发 | 外来词 | 菲律宾共和国 | 颗粒 |
在…里 | 介词或从属连词 | 符号 | 标志 |
姐姐(网络用语)ˌ法官ˌ裁判员(judges) | 形容词 | 到 | 到 |
JJR | 形容词,比较级 | 哦 | 感叹词 |
JJS | 形容词,最高级 | 动词 | 动词,基本形式 |
莱索托 | 列表项目标记 | VBD | 动词,过去式 |
医学博士 | 情态的 | VBG | 动词、动名词或现在分词 |
神经网络 | 名词,单数,还是复数 | VBN | 动词,过去分词 |
NNS | Noun, plural | VBP | 动词,非第三人称单数现在时 |
NNP | 专有名词,单数 | VBZ | 动词,第三人称单数现在时 |
NNPS | 专有名词,复数 | 禁水试验 | 疑问限定词 |
太平洋夏季时间 | 前限定词 | 文字处理 | 疑问代词 |
刷卡机 | 所有格结尾 | WP$ | 所有格 wh 代词 |
富含血小板血浆 | 人称代词 | 战时难民事务委员会(War Refugee Board) | 疑问副词 |
人工语料库的开发是劳动密集型的。然而,已经开发了一些统计技术来创建语料库。许多语料库是可用的。第一个是布朗文集(clu.uni.no/icame/manuals/BROWN/INDEX.HTM
)。较新的包括超过 1 亿字的英国国家语料库(【http://www.natcorp.ox.ac.uk/corpus/index.xml】)和美国国家语料库(【http://www.anc.org/】)。
张贴标签的重要性
对句子进行正确的标注可以提高后续处理任务的质量。如果我们知道 sue 是动词而不是名词,那么这可以帮助建立标记之间的正确关系。确定词性、短语、从句以及它们之间的任何关系被称为解析。这与标记化相反,在标记化中,我们只对识别单词元素感兴趣,而不关心它们的意思。
词性标注用于许多下游过程,例如问题分析和分析文本的情感。一些社交媒体网站经常对评估客户交流的情绪感兴趣。文本索引将频繁使用 POS 数据。语音处理可以使用标签来帮助决定如何发音。
POS 难的原因是什么?
一门语言的许多方面都会使词性标注变得困难。大多数英语单词都有两个或更多的相关标签。字典并不总是足以确定一个单词的位置。例如,像比尔和力量这样的词的意思取决于它们的上下文。下面的句子演示了它们如何在同一个句子中作为名词和动词使用。
“比尔用暴力迫使经理将账单撕成两半。”
将 OpenNLP 标记符与此语句一起使用会产生以下输出:
Bill/NNP used/VBD the/DT force/NN to/TO force/VB the/DT manger/NN to/TO tear/VB the/DT bill/NN in/IN two./PRP$
textese 是不同文本形式的组合,包括缩写、标签、表情符号和俚语,在 tweets 和 text 等通信媒体中的使用使得标记句子变得更加困难。例如,下面的消息很难标记:
“阿发克她已经死了!顺便说一句,我们在 BBIAM 聚会上玩得很开心。”
它的对等词是:
“据我所知,她讨厌打扫房子!顺便说一句,我在聚会上玩得很开心。一会儿就回来。”
使用 OpenNLP 标记器,我们将获得以下输出:
AFAIK/NNS she/PRP H8/CD cth!/.
BTW/NNP had/VBD a/DT GR8/CD tym/NN at/IN the/DT party/NN BBIAM./.
在本章后面的使用 MaxentTagger 类标记 textese 一节中,我们将演示 LingPipe 如何处理 textese。下表给出了常见文本术语的简短列表:
| 短语 | 文字 | 短语 | 文字 |
| 据我所知 | 就我所知 | 顺便问一下 | 顺便说一下 |
| 远离键盘 | 离开键盘的 | 你只能靠自己了 | 游乐儿 |
| 谢谢 | THNX 或 THX | 尽快 | 尽快(As Soon As Possible) |
| 今天 | 2 天 | 你这话是什么意思 | WDYMBT |
| 以前 | B4 | 马上回来 | BBIAM |
| 再见 | C U | 不可以 | (cannot)不能 |
| 哈哈的笑 | 倍硬 | 后来 | l8R |
| 大声笑出来 | 英雄联盟 | 另一方面 | 另一方面 |
| 笑得在地上打滚 | 罗夫尔还是 ROTFL | 我不知道 | 我不知道 |
| 伟大的 | GR8 | 打扫房子 | 三己糖酰基鞘氨醇 |
| 此刻 | 异步传输模式 | 管见所及 | 恕我直言 |
There are several lists of textese; a large list can be found at
www.ukrainecalling.com/textspeak.aspx
.
标记化是词性标注过程中的一个重要步骤。如果标记没有正确分割,我们可能会得到错误的结果。还有其他几个潜在的问题,包括:
- 如果我们使用小写字母,那么像山姆这样的词可能会与奖励管理的人或系统(【www.sam.gov】??)混淆
- 我们必须考虑到缩写,如不能,并认识到撇号可以使用不同的字符
- 虽然像反之亦然这样的短语可以被视为一个单位,但它已经被用于英格兰的一个乐队、一部小说的标题和一本杂志的标题
- 我们不能忽视用连字符连接的单词,如首切和首切,它们的意思不同于它们各自的用法
- 有些单词嵌入了数字,比如 iPhone 5S
- 还需要处理 URL 或电子邮件地址等特殊字符序列
一些单词被发现嵌入在引号或括号中,这可能会使它们的意思混淆。考虑下面的例子:
““蓝色”是否正确(事实并非如此)是有争议的。”
“蓝色”可以指蓝色,也可以是一个人的昵称。
这个句子的标签输出如下:
Whether/IN "Blue"/NNP was/VBD correct/JJ or/CC not/RB (it's/JJ not)/NN is/VBZ debatable/VBG
使用 NLP APIs
我们将使用 OpenNLP、Stanford API 和 LingPipe 演示词性标注。每个例子都会用到下面的句子。这是儒勒·凡尔纳的《海底两万里》中第五章的第一句话,出自《冒险的 ??》和《??》。
private String[] sentence = {"The", "voyage", "of", "the",
"Abraham", "Lincoln", "was", "for", "a", "long", "time", "marked",
"by", "no", "special", "incident."};
要处理的文本可能不总是以这种方式定义。有时,句子会以单个字符串的形式出现:
String theSentence = "The voyage of the Abraham Lincoln was for a "
+ "long time marked by no special incident.";
我们可能需要将一个字符串转换成一个字符串数组。有许多技术可以将这个字符串转换成单词数组。以下tokenizeSentence
方法执行该操作:
public String[] tokenizeSentence(String sentence) {
String words[] = sentence.split("S+");
return words;
}
下面的代码演示了此方法的用法:
String words[] = tokenizeSentence(theSentence);
for(String word : words) {
System.out.print(word + " ");
}
System.out.println();
输出如下所示:
The voyage of the Abraham Lincoln was for a long time marked by no special incident.
或者,我们可以使用一个标记器,比如 OpenNLP 的WhitespaceTokenizer
类,如下所示:
String words[] =
WhitespaceTokenizer.INSTANCE.tokenize(sentence);
使用 OpenNLP 位置标签
OpenNLP 提供了几个支持词性标注的类。我们将演示如何使用POSTaggerME
类来执行基本的标记,以及如何使用ChunkerME
类来执行分块。组块包括根据相关单词的类型对它们进行分组。这可以提供对句子结构的额外洞察。我们还将研究一个POSDictionary
实例的创建和使用。
为 POS 标记器使用 OpenNLP POSTaggerME 类
OpenNLP POSTaggerME
类使用最大熵来处理标签。
tagger 根据单词本身和单词的上下文来确定标签的类型。任何给定的单词都可能有多个相关联的标签。标记器使用概率模型来确定要分配的特定标记。
POS 模型是从文件中加载的。en-pos-maxent.bin
模型经常被使用,它基于 Penn TreeBank 标签集。在opennlp.sourceforge.net/models-1.5/
可以找到 OpenNLP 的各种预训练 POS 模型。
我们从一个 try-catch 块开始,处理加载模型时可能产生的任何IOException
,如下所示。
我们将en-pos-maxent.bin
文件用于模型:
try (InputStream modelIn = new FileInputStream(
new File(getModelDir(), "en-pos-maxent.bin"));) {
...
}
catch (IOException e) {
// Handle exceptions
}
接下来,创建POSModel
和POSTaggerME
实例,如下所示:
POSModel model = new POSModel(modelIn);
POSTaggerME tagger = new POSTaggerME(model);
tag
方法现在可以应用于 tagger,使用要处理的文本作为它的参数:
String tags[] = tagger.tag(sentence);
然后显示单词及其标签,如下所示:
for (int i = 0; i<sentence.length; i++) {
System.out.print(sentence[i] + "/" + tags[i] + " ");
}
输出如下。每个单词后面都有它的类型:
The/DT voyage/NN of/IN the/DT Abraham/NNP Lincoln/NNP was/VBD for/IN a/DT long/JJ time/NN marked/VBN by/IN no/DT special/JJ incident./NN
对于任何一个句子,都可能有不止一个可能的标签分配给单词。topKSequences
方法将根据正确的概率返回一组序列。在下面的代码序列中,使用sentence
变量执行topKSequences
方法,然后显示:
Sequence topSequences[] = tagger.topKSequences(sentence);
for (inti = 0; i<topSequences.length; i++) {
System.out.println(topSequences[i]);
}
其输出如下,其中第一个数字代表加权分数,括号内的标签是评分的标签序列:
-0.5563571615737618 [DT, NN, IN, DT, NNP, NNP, VBD, IN, DT, JJ, NN, VBN, IN, DT, JJ, NN]
-2.9886144610050907 [DT, NN, IN, DT, NNP, NNP, VBD, IN, DT, JJ, NN, VBN, IN, DT, JJ, .]
-3.771930515521527 [DT, NN, IN, DT, NNP, NNP, VBD, IN, DT, JJ, NN, VBN, IN, DT, NN, NN]
Ensure that you include the correct Sequence
class. For this example, use import opennlp.tools.util.Sequence;
.
Sequence
类有几个方法,详见下表:
| 方法 | 意为 |
| getOutcomes
| 返回表示句子标签的字符串列表 |
| getProbs
| 返回代表序列中每个标签概率的一组double
变量 |
| getScore
| 返回序列的加权值 |
在下面的序列中,我们使用其中的几个方法来演示它们的作用。对于每个序列,将显示标记及其概率,用正斜杠分隔:
for (int i = 0; i<topSequences.length; i++) {
List<String> outcomes = topSequences[i].getOutcomes();
double probabilities[] = topSequences[i].getProbs();
for (int j = 0; j <outcomes.size(); j++) {
System.out.printf("%s/%5.3f ",outcomes.get(j),
probabilities[j]);
}
System.out.println();
}
System.out.println();
输出如下。每一对行代表一个序列,其中输出已被包装:
DT/0.992 NN/0.990 IN/0.989 DT/0.990 NNP/0.996 NNP/0.991 VBD/0.994 IN/0.996 DT/0.996 JJ/0.991 NN/0.994 VBN/0.860 IN/0.985 DT/0.960 JJ/0.919 NN/0.832
DT/0.992 NN/0.990 IN/0.989 DT/0.990 NNP/0.996 NNP/0.991 VBD/0.994 IN/0.996 DT/0.996 JJ/0.991 NN/0.994 VBN/0.860 IN/0.985 DT/0.960 JJ/0.919 ./0.073
DT/0.992 NN/0.990 IN/0.989 DT/0.990 NNP/0.996 NNP/0.991 VBD/0.994 IN/0.996 DT/0.996 JJ/0.991 NN/0.994 VBN/0.860 IN/0.985 DT/0.960 NN/0.073 NN/0.419
使用 opennlp 重庆
组块的过程包括将一个句子分成几个部分或几个组块。然后可以用标签对这些块进行注释。我们将使用ChunkerME
类来说明这是如何完成的。这个类使用一个加载到ChunkerModel
实例中的模型。ChunkerME
类的chunk
方法执行实际的分块过程。我们还将研究使用chunkAsSpans
方法来返回关于这些块的跨度的信息。这让我们可以看到一个块有多长,以及什么元素组成了这个块。
我们将使用en-pos-maxent.bin
文件为POSTaggerME
实例创建一个模型。我们需要使用这个实例来标记文本,就像我们在本章前面的为 POS taggers 使用 OpenNLP POSTaggerME 类一节中所做的那样。我们还将使用en-chunker.bin
文件创建一个ChunkerModel
实例,与ChunkerME
实例一起使用。
这些模型是使用输入流创建的,如下例所示。我们使用 try-with-resources 块来打开和关闭文件,并处理可能引发的任何异常:
try (
InputStream posModelStream = new FileInputStream(
getModelDir() + "\\en-pos-maxent.bin");
InputStream chunkerStream = new FileInputStream(
getModelDir() + "\\en-chunker.bin");) {
...
} catch (IOException ex) {
// Handle exceptions
}
下面的代码序列创建并使用一个标记来查找句子的位置。然后显示句子及其标签:
POSModel model = new POSModel(posModelStream);
POSTaggerME tagger = new POSTaggerME(model);
String tags[] = tagger.tag(sentence);
for(int i=0; i<tags.length; i++) {
System.out.print(sentence[i] + "/" + tags[i] + " ");
}
System.out.println();
输出如下。我们已经展示了该输出,以便清楚了解分块器的工作原理:
The/DT voyage/NN of/IN the/DT Abraham/NNP Lincoln/NNP was/VBD for/IN a/DT long/JJ time/NN marked/VBN by/IN no/DT special/JJ incident./NN
使用输入流创建一个ChunkerModel
实例。由此创建了ChunkerME
实例,然后使用chunk
方法,如下所示。chunk
方法将使用句子的标记和它的标签来创建一个字符串数组。每个字符串将保存关于令牌及其块的信息:
ChunkerModel chunkerModel = new
ChunkerModel(chunkerStream);
ChunkerME chunkerME = new ChunkerME(chunkerModel);
String result[] = chunkerME.chunk(sentence, tags);
显示了results
数组中的每个标记及其块标签,如下所示:
for (int i = 0; i < result.length; i++) {
System.out.println("[" + sentence[i] + "] " + result[i]);
}
输出如下。该标记用括号括起来,后面是 chunk 标记。下表解释了这些标签:
| 第一部分 |
| B | 标签开头 |
| 我 | 标签的延续 |
| E | 标签结束(如果标签只有一个单词,则不会出现) |
| 第二部分 |
| 公证人 | 名词块 |
| 动词 | 动词组块 |
多个单词组合在一起,如“The voyage
”、the Abraham Lincoln
:
[The] B-NP
[voyage] I-NP
[of] B-PP
[the] B-NP
[Abraham] I-NP
[Lincoln] I-NP
[was] B-VP
[for] B-PP
[a] B-NP
[long] I-NP
[time] I-NP
[marked] B-VP
[by] B-PP
[no] B-NP
[special] I-NP
[incident.] I-NP
如果我们想获得更多关于块的详细信息,我们可以使用ChunkerME
类的chunkAsSpans
方法。这个方法返回一个Span
对象的数组。每个对象代表文本中的一个跨度。
还有几个其他的ChunkerME
类方法可用。这里,我们将举例说明getType
、getStart
和getEnd
方法的使用。getType
方法返回块标签的第二部分,而getStart
和getEnd
方法分别返回原始sentence
数组中标记的开始和结束索引。length
方法返回多个标记的跨度长度。
在下面的序列中,使用sentence
和tags
数组执行chunkAsSpans
方法。然后显示spans
数组。外部的for
循环一次处理一个Span
对象,显示基本的跨度信息。
内部for
循环显示括号内的跨区文本:
Span[] spans = chunkerME.chunkAsSpans(sentence, tags);
for (Span span : spans) {
System.out.print("Type: " + span.getType() + " - "
+ " Begin: " + span.getStart()
+ " End:" + span.getEnd()
+ " Length: " + span.length() + " [");
for (int j = span.getStart(); j < span.getEnd(); j++) {
System.out.print(sentence[j] + " ");
}
System.out.println("]");
}
下面的输出清楚地显示了跨度类型、它在sentence
数组中的位置、它的Length
,以及实际的跨度文本:
Type: NP - Begin: 0 End:2 Length: 2 [The voyage ]
Type: PP - Begin: 2 End:3 Length: 1 [of ]
Type: NP - Begin: 3 End:6 Length: 3 [the Abraham Lincoln ]
Type: VP - Begin: 6 End:7 Length: 1 [was ]
Type: PP - Begin: 7 End:8 Length: 1 [for ]
Type: NP - Begin: 8 End:11 Length: 3 [a long time ]
Type: VP - Begin: 11 End:12 Length: 1 [marked ]
Type: PP - Begin: 12 End:13 Length: 1 [by ]
Type: NP - Begin: 13 End:16 Length: 3 [no special incident. ]
使用 POSDictionary 类
标签字典指定了单词的有效标签。这可以防止标签不适当地应用于单词。此外,一些搜索算法执行得更快,因为它们不必考虑其他不太可能的标签。
在本节中,我们将演示如何:
- 获取标记者的标记字典
- 确定一个单词有哪些标签
- 展示如何改变一个单词的标签
- 向新的标记器工厂添加新的标记字典
与前面的示例一样,我们将使用 try-with-resources 块打开 POS 模型的输入流,然后创建我们的模型和标记器工厂,如下所示:
try (InputStream modelIn = new FileInputStream(
new File(getModelDir(), "en-pos-maxent.bin"));) {
POSModel model = new POSModel(modelIn);
POSTaggerFactory posTaggerFactory = model.getFactory();
...
} catch (IOException e) {
//Handle exceptions
}
获取标记者的标记字典
我们使用了POSModel
类的getFactory
方法来获得一个POSTaggerFactory
实例。我们将使用它的getTagDictionary
方法来获得它的TagDictionary
实例。这里举例说明了这一点:
MutableTagDictionary tagDictionary =
(MutableTagDictionary)posTaggerFactory.getTagDictionary();
MutableTagDictionary
接口扩展了TagDictionary
接口。TagDictionary
接口拥有一个getTags
方法,MutableTagDictionary
接口增加了一个put
方法,允许将标签添加到字典中。这些接口是由POSDictionary
类实现的。
确定单词的标签
要获得给定单词的标签,使用getTags
方法。这将返回由字符串表示的标记数组。然后会显示标签,如下所示:
String tags[] = tagDictionary.getTags("force");
for (String tag : tags) {
System.out.print("/" + tag);
}
System.out.println();
输出如下所示:
/NN/VBP/VB
这意味着“力”这个词可以有三种不同的解释。
更改单词的标签
接口的方法允许我们给一个单词添加标签。该方法有两个参数:单词及其新标签。方法返回包含前面标记的数组。
在下面的例子中,我们用新标签替换旧标签。然后显示旧标签:
String oldTags[] = tagDictionary.put("force", "newTag");
for (String tag : oldTags) {
System.out.print("/" + tag);
}
System.out.println();
以下输出列出了单词的旧标签:
/NN/VBP/VB
这些标签已被新标签替换,如下所示,其中显示了当前标签:
tags = tagDictionary.getTags("force");
for (String tag : tags) {
System.out.print("/" + tag);
}
System.out.println();
我们得到的是以下内容:
/newTag
为了保留旧标签,我们需要创建一个字符串数组来保存旧标签和新标签,然后使用该数组作为put
方法的第二个参数,如下所示:
String newTags[] = new String[tags.length+1];
for (int i=0; i<tags.length; i++) {
newTags[i] = tags[i];
}
newTags[tags.length] = "newTag";
oldTags = tagDictionary.put("force", newTags);
如果我们重新显示当前标签,如此处所示,我们可以看到旧标签被保留,新标签被添加:
/NN/VBP/VB/newTag
When adding tags, be careful to assign the tags in the proper order, as it will influence which tag is assigned.
添加新的标记字典
一个新的标签字典可以被添加到一个POSTaggerFactory
实例中。我们将通过创建一个新的POSTaggerFactory
并添加我们之前开发的tagDictionary
来说明这个过程。首先,我们使用默认构造函数创建一个新工厂,如下面的代码所示。
接下来对新工厂调用setTagDictionary
方法:
POSTaggerFactory newFactory = new POSTaggerFactory();
newFactory.setTagDictionary(tagDictionary);
为了确认已经添加了tag
字典,我们显示了单词"force"
的标签,如下所示:
tags = newFactory.getTagDictionary().getTags("force");
for (String tag : tags) {
System.out.print("/" + tag);
}
System.out.println();
标签是相同的,如下所示:
/NN/VBP/VB/newTag
从文件创建字典
如果我们需要创建一个新的字典,那么一种方法是创建一个包含所有单词及其标签的 XML 文件,然后从该文件创建字典。OpenNLP 通过POSDictionary
类的create
方法支持这种方法。
XML 文件由根元素dictionary
和一系列的元素entry
组成。entry
元素使用tags
属性来指定单词的标签。这个单词作为一个token
元素包含在entry
元素中。使用存储在dictionary.txt
文件中的两个单词的简单示例如下:
<dictionary case_sensitive="false">
<entry tags="JJ VB">
<token>strong</token>
</entry>
<entry tags="NN VBP VB">
<token>force</token>
</entry>
</dictionary>
为了创建字典,我们使用基于输入流的create
方法,如下所示:
try (InputStream dictionaryIn =
new FileInputStream(new File("dictionary.txt"));) {
POSDictionary dictionary =
POSDictionary.create(dictionaryIn);
...
} catch (IOException e) {
// Handle exceptions
}
POSDictionary
类有一个返回迭代器对象的iterator
方法。它的next
方法为字典中的每个单词返回一个字符串。我们可以使用这些方法来显示字典的内容,如下所示:
Iterator<String> iterator = dictionary.iterator();
while (iterator.hasNext()) {
String entry = iterator.next();
String tags[] = dictionary.getTags(entry);
System.out.print(entry + " ");
for (String tag : tags) {
System.out.print("/" + tag);
}
System.out.println();
}
下面的输出显示了我们可以预期的结果:
strong /JJ/VB
force /NN/VBP/VB
使用斯坦福 POS 标签
在这一节中,我们将研究斯坦福 API 支持的两种不同的方法来执行标记。第一种技术使用了MaxentTagger
类。顾名思义,它使用最大熵来寻找位置。我们还将使用这个类来演示一个用于处理 textese 类型文本的模型。第二种方法将对注释器使用管道方法。英语标签使用宾州树库英语 POS 标签集。
使用斯坦福 MaxentTagger
MaxentTagger
类使用一个模型来执行标记任务。API 中捆绑了许多模型,所有模型都带有文件扩展名.tagger
。它们包括英语、中文、阿拉伯语、法语和德语模型。
这里列出了英国型号。前缀wsj
,指的是基于华尔街日报的模型。其他术语指的是用于训练模型的技术。这里不涉及这些概念:
wsj-0-18-bidirectional-distsim.tagger
wsj-0-18-bidirectional-nodistsim.tagger
wsj-0-18-caseless-left3words-distsim.tagger
wsj-0-18-left3words-distsim.tagger
wsj-0-18-left3words-nodistsim.tagger
english-bidirectional-distsim.tagger
english-caseless-left3words-distsim.tagger
english-left3words-distsim.tagger
该示例从文件中读入一系列句子。然后处理每个句子,并显示访问和显示单词和标签的各种方式。
我们从 try-with-resources 块开始处理 IO 异常,如下所示。wsj-0-18-bidirectional-distsim.tagger
文件用于创建MaxentTagger
类的一个实例。
使用MaxentTagger
类的tokenizeText
方法创建HasWord
对象的List
实例的List
实例。这些句子是从sentences.txt
文件中读入的。HasWord
接口表示单词并包含两个方法:一个setWord
和一个word
方法。后一种方法将单词作为字符串返回。每个句子由一个HasWord
对象的List
实例表示:
try {
MaxentTagger tagger = new MaxentTagger(getModelDir() +
"//wsj-0-18-bidirectional-distsim.tagger");
List<List<HasWord>> sentences = MaxentTagger.tokenizeText(
new BufferedReader(new FileReader("sentences.txt")));
...
} catch (FileNotFoundException ex) {
// Handle exceptions
}
sentences.txt
文件包含本书第五章的前四句话在一次冒险中**海底两万里:
The voyage of the Abraham Lincoln was for a long time marked by no special incident.
But one circumstance happened which showed the wonderful dexterity of Ned Land, and proved what confidence we might place in him.
The 30th of June, the frigate spoke some American whalers, from whom we learned that they knew nothing about the narwhal.
But one of them, the captain of the Monroe, knowing that Ned Land had shipped on board the Abraham Lincoln, begged for his help in chasing a whale they had in sight.
添加一个循环来处理sentences
列表中的每个句子。tagSentence
方法返回TaggedWord
对象的List
实例,如下面的代码所示。TaggedWord
类实现了HasWord
接口,并添加了一个tag
方法,该方法返回与单词相关联的标签。如此处所示,toString
方法用于显示每个句子:
List<TaggedWord> taggedSentence =
tagger.tagSentence(sentence);
for (List<HasWord> sentence : sentences) {
List<TaggedWord> taggedSentence=
tagger.tagSentence(sentence);
System.out.println(taggedSentence);
}
输出如下所示:
[The/DT, voyage/NN, of/IN, the/DT, Abraham/NNP, Lincoln/NNP, was/VBD, for/IN, a/DT, long/JJ, --- time/NN, marked/VBN, by/IN, no/DT, special/JJ, incident/NN, ./.]
[But/CC, one/CD, circumstance/NN, happened/VBD, which/WDT, showed/VBD, the/DT, wonderful/JJ, dexterity/NN, of/IN, Ned/NNP, Land/NNP, ,/,, and/CC, proved/VBD, what/WP, confidence/NN, we/PRP, might/MD, place/VB, in/IN, him/PRP, ./.]
[The/DT, 30th/JJ, of/IN, June/NNP, ,/,, the/DT, frigate/NN, spoke/VBD, some/DT, American/JJ, whalers/NNS, ,/,, from/IN, whom/WP, we/PRP, learned/VBD, that/IN, they/PRP, knew/VBD, nothing/NN, about/IN, the/DT, narwhal/NN, ./.]
[But/CC, one/CD, of/IN, them/PRP, ,/,, the/DT, captain/NN, of/IN, the/DT, Monroe/NNP, ,/,, knowing/VBG, that/IN, Ned/NNP, Land/NNP, had/VBD, shipped/VBN, on/IN, board/NN, the/DT, Abraham/NNP, Lincoln/NNP, ,/,, begged/VBN, for/IN, his/PRP$, help/NN, in/IN, chasing/VBG, a/DT, whale/NN, they/PRP, had/VBD, in/IN, sight/NN, ./.]
或者,我们可以使用Sentence
类的listToString
方法将标记的句子转换成一个简单的String
对象。
HasWord
的toString
方法使用第二个参数的值false
来创建结果字符串,如下所示:
List<TaggedWord> taggedSentence =
tagger.tagSentence(sentence);
for (List<HasWord> sentence : sentences) {
List<TaggedWord> taggedSentence=
tagger.tagSentence(sentence);
System.out.println(Sentence.listToString(taggedSentence, false));
}
这产生了更美观的输出:
The/DT voyage/NN of/IN the/DT Abraham/NNP Lincoln/NNP was/VBD for/IN a/DT long/JJ time/NN marked/VBN by/IN no/DT special/JJ incident/NN ./.
But/CC one/CD circumstance/NN happened/VBD which/WDT showed/VBD the/DT wonderful/JJ dexterity/NN of/IN Ned/NNP Land/NNP ,/, and/CC proved/VBD what/WP confidence/NN we/PRP might/MD place/VB in/IN him/PRP ./.
The/DT 30th/JJ of/IN June/NNP ,/, the/DT frigate/NN spoke/VBD some/DT American/JJ whalers/NNS ,/, from/IN whom/WP we/PRP learned/VBD that/IN they/PRP knew/VBD nothing/NN about/IN the/DT narwhal/NN ./.
But/CC one/CD of/IN them/PRP ,/, the/DT captain/NN of/IN the/DT Monroe/NNP ,/, knowing/VBG that/IN Ned/NNP Land/NNP had/VBD shipped/VBN on/IN board/NN the/DT Abraham/NNP Lincoln/NNP ,/, begged/VBN for/IN his/PRP$ help/NN in/IN chasing/VBG a/DT whale/NN they/PRP had/VBD in/IN sight/NN ./.
我们可以使用下面的代码序列来产生相同的结果。word
和tag
方法提取单词及其标签:
List<TaggedWord> taggedSentence =
tagger.tagSentence(sentence);
for (TaggedWord taggedWord : taggedSentence) {
System.out.print(taggedWord.word() + "/" +
taggedWord.tag() + " ");
}
System.out.println();
如果我们只对查找给定标签的特定出现感兴趣,我们可以使用如下序列,它将只列出单数名词(NN
):
List<TaggedWord> taggedSentence =
tagger.tagSentence(sentence);
for (TaggedWord taggedWord : taggedSentence) {
if (taggedWord.tag().startsWith("NN")) {
System.out.print(taggedWord.word() + " ");
}
}
System.out.println();
每个句子都会显示单数名词,如下所示:
NN Tagged: voyage Abraham Lincoln time incident
NN Tagged: circumstance dexterity Ned Land confidence
NN Tagged: June frigate whalers nothing narwhal
NN Tagged: captain Monroe Ned Land board Abraham Lincoln help whale sight
使用 MaxentTagger 类标记 textese
我们可以使用不同的模型来处理可能包含 textese 的 Twitter 文本。文本工程通用架构 ( 门)(【https://gate.ac.uk/wiki/twitter-postagger.html】??)已经为 Twitter 文本开发了一个模型。此处使用模型来处理 textese:
MaxentTagger tagger = new MaxentTagger(getModelDir()
+ "//gate-EN-twitter.model");
在这里,我们使用来自*的MaxentTagger
类的tagString
方法。*本章前面的小节处理 textese:
System.out.println(tagger.tagString("AFAIK she H8 cth!")); System.out.println(tagger.tagString( "BTW had a GR8 tym at the party BBIAM."));
输出如下所示:
AFAIK_NNP she_PRP H8_VBP cth!_NN
BTW_UH had_VBD a_DT GR8_NNP tym_NNP at_IN the_DT party_NN BBIAM._NNP
使用斯坦福管道执行标记
我们在之前的几个例子中使用了斯坦福管道。在这个例子中,我们将使用斯坦福管道来提取 POS 标签。和我们之前的 Stanford 例子一样,我们基于一组注释器创建了一个管道:tokenize
、ssplit
、
和pos
。
这些将标记化,将文本分割成句子,然后找到位置标记:
Properties props = new Properties();
props.put("annotators", "tokenize, ssplit, pos");
StanfordCoreNLP pipeline = new StanfordCoreNLP(props);
为了处理文本,我们将使用theSentence
变量作为Annotator
的输入。然后调用管道的annotate
方法,如下所示:
Annotation document = new Annotation(theSentence);
pipeline.annotate(document);
因为管道可以执行不同类型的处理,所以使用一列CoreMap
对象来访问单词和标签。Annotation
类的get
方法返回句子列表,如下所示:
List<CoreMap> sentences =
document.get(SentencesAnnotation.class);
可以使用get
方法访问CoreMap
对象的内容。该方法的参数是所需信息的类。如下面的代码示例所示,使用TextAnnotation
类访问令牌,使用PartOfSpeechAnnotation
类检索 POS 标记。将显示每个句子的每个单词及其标签:
for (CoreMap sentence : sentences) {
for (CoreLabel token : sentence.get(TokensAnnotation.class)) {
String word = token.get(TextAnnotation.class);
String pos = token.get(PartOfSpeechAnnotation.class);
System.out.print(word + "/" + pos + " ");
}
System.out.println();
}
输出如下所示:
The/DT voyage/NN of/IN the/DT Abraham/NNP Lincoln/NNP was/VBD for/IN a/DT long/JJ time/NN marked/VBN by/IN no/DT special/JJ incident/NN ./.
管道可以使用附加选项来控制标记器的工作方式。例如,默认情况下,使用english-left3words-distsim.tagger
tagger 模型。我们可以使用pos.model
属性指定一个不同的模型,如下所示。还有一个pos.maxlen
属性来控制最大句子长度:
props.put("pos.model",
"C:/.../Models/english-caseless-left3words-distsim.tagger");
有时候,有一个 XML 格式的标记文档是很有用的。StanfordCoreNLP
类的xmlPrint
方法会写出这样一个文档。该方法的第一个参数是要显示的注释器。它的第二个参数是要写入的OutputStream
对象。在以下代码序列中,先前的标记结果被写入标准输出。它包含在一个try...catch
块中,用于处理 IO 异常:
try {
pipeline.xmlPrint(document, System.out);
} catch (IOException ex) {
// Handle exceptions
}
部分结果列表如下。仅显示前两个单词和最后一个单词。每个 token 标签都包含单词、其位置及其 POS 标签:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="CoreNLP-to-HTML.xsl" type="text/xsl"?>
<root>
<document>
<sentences>
<sentence id="1">
<tokens>
<token id="1">
<word>The</word>
<CharacterOffsetBegin>0</CharacterOffsetBegin>
<CharacterOffsetEnd>3</CharacterOffsetEnd>
<POS>DT</POS>
</token>
<token id="2">
<word>voyage</word>
<CharacterOffsetBegin>4</CharacterOffsetBegin>
<CharacterOffsetEnd>10</CharacterOffsetEnd>
<POS>NN</POS>
</token>
...
<token id="17">
<word>.</word>
<CharacterOffsetBegin>83</CharacterOffsetBegin>
<CharacterOffsetEnd>84</CharacterOffsetEnd>
<POS>.</POS>
</token>
</tokens>
</sentence>
</sentences>
</document>
</root>
prettyPrint
方法以类似的方式工作:
pipeline.prettyPrint(document, System.out);
然而,输出并不真的那么漂亮,如此处所示。显示原始句子,后面是每个单词、其位置和标签。输出已被格式化,以便于阅读:
The voyage of the Abraham Lincoln was for a long time marked by no special incident.
[Text=The CharacterOffsetBegin=0 CharacterOffsetEnd=3 PartOfSpeech=DT]
[Text=voyage CharacterOffsetBegin=4 CharacterOffsetEnd=10 PartOfSpeech=NN]
[Text=of CharacterOffsetBegin=11 CharacterOffsetEnd=13 PartOfSpeech=IN]
[Text=the CharacterOffsetBegin=14 CharacterOffsetEnd=17 PartOfSpeech=DT]
[Text=Abraham CharacterOffsetBegin=18 CharacterOffsetEnd=25 PartOfSpeech=NNP]
[Text=Lincoln CharacterOffsetBegin=26 CharacterOffsetEnd=33 PartOfSpeech=NNP]
[Text=was CharacterOffsetBegin=34 CharacterOffsetEnd=37 PartOfSpeech=VBD]
[Text=for CharacterOffsetBegin=38 CharacterOffsetEnd=41 PartOfSpeech=IN]
[Text=a CharacterOffsetBegin=42 CharacterOffsetEnd=43 PartOfSpeech=DT]
[Text=long CharacterOffsetBegin=44 CharacterOffsetEnd=48 PartOfSpeech=JJ]
[Text=time CharacterOffsetBegin=49 CharacterOffsetEnd=53 PartOfSpeech=NN]
[Text=marked CharacterOffsetBegin=54 CharacterOffsetEnd=60 PartOfSpeech=VBN]
[Text=by CharacterOffsetBegin=61 CharacterOffsetEnd=63 PartOfSpeech=IN]
[Text=no CharacterOffsetBegin=64 CharacterOffsetEnd=66 PartOfSpeech=DT]
[Text=special CharacterOffsetBegin=67 CharacterOffsetEnd=74 PartOfSpeech=JJ]
[Text=incident CharacterOffsetBegin=75 CharacterOffsetEnd=83 PartOfSpeech=NN]
[Text=. CharacterOffsetBegin=83 CharacterOffsetEnd=84 PartOfSpeech=.]
使用 LingPipe POS 标签
LingPipe 使用Tagger
接口来支持词性标注。这个接口只有一个方法:tag
。它返回一个Tagging
对象的List
实例。这些对象是单词和它们的标签。该接口由ChainCrf
和HmmDecoder
类实现。
ChainCrf
类使用线性链条件随机场解码和估计来确定标签。HmmDecoder
类使用 HMM 来执行标记。接下来我们将举例说明这个类。
HmmDecoder
类使用tag
方法来确定最可能(最好)的标签。它还有一个tagNBest
方法,对可能的标签进行评分,并返回这个评分标签的迭代器。灵管有三种 POS 型号,可以从alias-i.com/lingpipe/web/models.html
下载。下表列出了这些选项。在我们的演示中,我们将使用 Brown 语料库模型:
| 型号 | 文件 |
| 英语通用文本:布朗语料库 | pos-en-general-brown.HiddenMarkovModel
|
| 英语生物医学文本:MedPost 语料库 | pos-en-bio-medpost.HiddenMarkovModel
|
| 英语生物医学文本:GENIA 语料库 | pos-en-bio-genia.HiddenMarkovModel
|
使用带有 Best_First 标记的 HmmDecoder 类
我们从处理异常的 try-with-resources 块和创建HmmDecoder
实例的代码开始,如下面的代码所示。
从文件中读取模型,然后用作HmmDecoder
构造函数的参数:
try (
FileInputStream inputStream =
new FileInputStream(getModelDir()
+ "//pos-en-general-brown.HiddenMarkovModel");
ObjectInputStream objectStream =
new ObjectInputStream(inputStream);) {
HiddenMarkovModel hmm = (HiddenMarkovModel)
objectStream.readObject();
HmmDecoder decoder = new HmmDecoder(hmm);
...
} catch (IOException ex) {
// Handle exceptions
} catch (ClassNotFoundException ex) {
// Handle exceptions
};
我们将对theSentence
变量进行标记。首先,它需要被标记化。我们将使用一个IndoEuropean
标记器,如下所示。tokenizer
方法要求将文本字符串转换成字符数组。然后,tokenize
方法以字符串的形式返回一个令牌数组:
TokenizerFactory TOKENIZER_FACTORY =
IndoEuropeanTokenizerFactory.INSTANCE;
char[] charArray = theSentence.toCharArray();
Tokenizer tokenizer =
TOKENIZER_FACTORY.tokenizer(
charArray, 0, charArray.length);
String[] tokens = tokenizer.tokenize();
实际的标记是由HmmDecoder
类的tag
方法执行的。然而,这个方法需要一个String
令牌的List
实例。这个列表是使用Arrays
类的asList
方法创建的。Tagging
类保存一系列标记和标签:
List<String> tokenList = Arrays.asList(tokens);
Tagging<String> tagString = decoder.tag(tokenList);
我们现在准备好显示令牌及其标签。下面的循环使用token
和tag
方法分别访问Tagging
对象中的标记和标签。然后显示它们:
for (int i = 0; i < tagString.size(); ++i) {
System.out.print(tagString.token(i) + "/"
+ tagString.tag(i) + " ");
}
输出如下所示:
The/at voyage/nn of/in the/at Abraham/np Lincoln/np was/bedz for/in a/at long/jj time/nn marked/vbn by/in no/at special/jj incident/nn ./.
使用带有 NBest 标记的 HmmDecoder 类
标记过程考虑标记的多种组合。HmmDecoder
类的tagNBest
方法返回反映不同订单可信度的ScoredTagging
对象的迭代器。该方法采用一个令牌列表和一个指定所需最大结果数的数字。
前面的句子不够模糊,不足以说明标签的组合。相反,我们将使用下面的句子:
String[] sentence = {"Bill", "used", "the", "force",
"to", "force", "the", "manager", "to",
"tear", "the", "bill","in", "to."};
List<String> tokenList = Arrays.asList(sentence);
这里显示了使用此方法的一个示例,从结果数的声明开始:
int maxResults = 5;
使用上一节中创建的decoder
对象,我们将tagNBest
方法应用于它,如下所示:
Iterator<ScoredTagging<String>> iterator =
decoder.tagNBest(tokenList, maxResults);
迭代器将允许我们访问五个不同的分数。ScoredTagging
类拥有一个score
方法,该方法返回一个反映它认为自己执行得有多好的值。在下面的代码序列中,一个printf
语句显示了这个分数。接下来是一个循环,其中显示了令牌及其标签。
结果是一个分数,后跟带有标签的单词序列:
while (iterator.hasNext()) {
ScoredTagging<String> scoredTagging = iterator.next();
System.out.printf("Score: %7.3f Sequence: ",
scoredTagging.score());
for (int i = 0; i < tokenList.size(); ++i) {
System.out.print(scoredTagging.token(i) + "/"
+ scoredTagging.tag(i) + " ");
}
System.out.println();
}
输出如下。注意,单词"force"
可以有一个标签nn
、jj
或vb
:
Score: -148.796 Sequence: Bill/np used/vbd the/at force/nn to/to force/vb the/at manager/nn to/to tear/vb the/at bill/nn in/in two./nn
Score: -154.434 Sequence: Bill/np used/vbn the/at force/nn to/to force/vb the/at manager/nn to/to tear/vb the/at bill/nn in/in two./nn
Score: -154.781 Sequence: Bill/np used/vbd the/at force/nn to/in force/nn the/at manager/nn to/to tear/vb the/at bill/nn in/in two./nn
Score: -157.126 Sequence: Bill/np used/vbd the/at force/nn to/to force/vb the/at manager/jj to/to tear/vb the/at bill/nn in/in two./nn
Score: -157.340 Sequence: Bill/np used/vbd the/at force/jj to/to force/vb the/at manager/nn to/to tear/vb the/at bill/nn in/in two./nn
使用 HmmDecoder 类确定标签可信度
可以使用网格结构来执行统计分析,这对于分析可选的单词排序是有用的。这个结构表示向前/向后得分。HmmDecoder
类的tagMarginal
方法返回TagLattice
类的一个实例,它代表一个网格。
我们可以使用ConditionalClassification
类的一个实例来检查网格的每个令牌。在下面的例子中,tagMarginal
方法返回一个TagLattice
实例。使用一个循环来获取网格中每个标记的ConditionalClassification
实例。
我们使用的是在上一节中开发的同一个tokenList
实例:
TagLattice<String> lattice = decoder.tagMarginal(tokenList);
for (int index = 0; index < tokenList.size(); index++) {
ConditionalClassification classification =
lattice.tokenClassification(index);
...
}
ConditionalClassification
类有一个score
和一个category
方法。score
方法返回给定类别的相对分数。category
方法返回这个类别,也就是标签。令牌、其分数及其类别如下所示:
System.out.printf("%-8s",tokenList.get(index));
for (int i = 0; i < 4; ++i) {
double score = classification.score(i);
String tag = classification.category(i);
System.out.printf("%7.3f/%-3s ",score,tag);
}
System.out.println();
输出如下所示:
Bill 0.974/np 0.018/nn 0.006/rb 0.001/nps
used 0.935/vbd 0.065/vbn 0.000/jj 0.000/rb
the 1.000/at 0.000/jj 0.000/pps 0.000/pp$$
force 0.977/nn 0.016/jj 0.006/vb 0.001/rb
to 0.944/to 0.055/in 0.000/rb 0.000/nn
force 0.945/vb 0.053/nn 0.002/rb 0.001/jj
the 1.000/at 0.000/jj 0.000/vb 0.000/nn
manager 0.982/nn 0.018/jj 0.000/nn$ 0.000/vb
to 0.988/to 0.012/in 0.000/rb 0.000/nn
tear 0.991/vb 0.007/nn 0.001/rb 0.001/jj
the 1.000/at 0.000/jj 0.000/vb 0.000/nn
bill 0.994/nn 0.003/jj 0.002/rb 0.001/nns
in 0.990/in 0.004/rp 0.002/nn 0.001/jj
two. 0.960/nn 0.013/np 0.011/nns 0.008/rb
为 OpenNLP POSModel 定型
训练一个 OpenNLP POSModel
类似于前面的训练例子。需要一个训练文件,它应该足够大以提供一个好的样本集。培训文件的每句话必须单独在一行上。每一行都由一个标记、下划线字符和标签组成。
下面的训练数据是使用第五章的前五句话创建的,第五章是《海底两万里中的在一家企业*。虽然这不是一个大的样本集,但它很容易创建,并且足以用于演示目的。它保存在一个名为sample.train
的文件中:*
The_DT voyage_NN of_IN the_DT Abraham_NNP Lincoln_NNP was_VBD for_IN a_DT long_JJ time_NN marked_VBN by_IN no_DT special_JJ incident._NN
But_CC one_CD circumstance_NN happened_VBD which_WDT showed_VBD the_DT wonderful_JJ dexterity_NN of_IN Ned_NNP Land,_NNP and_CC proved_VBD what_WP confidence_NN we_PRP might_MD place_VB in_IN him._PRP$
The_DT 30th_JJ of_IN June,_NNP the_DT frigate_NN spoke_VBD some_DT American_NNP whalers,_, from_IN whom_WP we_PRP learned_VBD that_IN they_PRP knew_VBD nothing_NN about_IN the_DT narwhal._NN
But_CC one_CD of_IN them,_PRP$ the_DT captain_NN of_IN the_DT Monroe,_NNP knowing_VBG that_IN Ned_NNP Land_NNP had_VBD shipped_VBN on_IN board_NN the_DT Abraham_NNP Lincoln,_NNP begged_VBD for_IN his_PRP$ help_NN in_IN chasing_VBG a_DT whale_NN they_PRP had_VBD in_IN sight._NN
我们将演示使用POSModel
类的train
方法创建模型,以及如何将模型保存到文件中。我们从声明POSModel
实例变量开始:
POSModel model = null;
try-with-resources 块打开示例文件:
try (InputStream dataIn = new FileInputStream("sample.train");) {
...
} catch (IOException e) {
// Handle exceptions
}
创建一个PlainTextByLineStream
类的实例,并与WordTagSampleStream
类一起创建一个ObjectStream<POSSample>
实例。这将样本数据转换成train
方法要求的格式:
ObjectStream<String> lineStream =
new PlainTextByLineStream(dataIn, "UTF-8");
ObjectStream<POSSample> sampleStream =
new WordTagSampleStream(lineStream);
train
方法使用它的参数来指定语言、样本流、训练参数和任何需要的字典(本例中没有),如下所示:
model = POSTaggerME.train("en", sampleStream,
TrainingParameters.defaultParams(), null, null);
这个过程的输出是冗长的。以下输出已被缩短以节省空间:
Indexing events using cutoff of 5
Computing event counts... done. 90 events
Indexing... done.
Sorting and merging events... done. Reduced 90 events to 82.
Done indexing.
Incorporating indexed data for training...
done.
Number of Event Tokens: 82
Number of Outcomes: 17
Number of Predicates: 45
...done.
Computing model parameters ...
Performing 100 iterations.
1: ... loglikelihood=-254.98920096505964 0.14444444444444443
2: ... loglikelihood=-201.19283975630537 0.6
3: ... loglikelihood=-174.8849213436524 0.6111111111111112
4: ... loglikelihood=-157.58164262220754 0.6333333333333333
5: ... loglikelihood=-144.69272379986646 0.6555555555555556
...
99: ... loglikelihood=-33.461128002846024 0.9333333333333333
100: ... loglikelihood=-33.29073273669207 0.9333333333333333
为了将模型保存到文件中,我们使用了下面的代码。输出流被创建,POSModel
类的serialize
方法将模型保存到en_pos_verne.bin
文件中:
try (OutputStream modelOut = new BufferedOutputStream(
new FileOutputStream(new File("en_pos_verne.bin")));) {
model.serialize(modelOut);
} catch (IOException e) {
// Handle exceptions
}
摘要
词性标注是一种识别句子语法部分的强大技术。它为下游任务提供了有用的处理,如问题分析和分析文本的情感。当我们在第七章、信息检索中讨论解析时,我们将回到这个主题。
由于大多数语言中存在歧义,标记不是一个简单的过程。越来越多的使用 textese 只会让这个过程变得更加困难。幸运的是,有一些模型可以很好地识别这种类型的文本。然而,随着新术语和俚语的引入,这些模型需要不断更新。
我们研究了 OpenNLP、斯坦福 API 和 LingPipe 在支持标记方面的使用。这些库使用几种不同的方法来标记单词,包括基于规则和基于模型的方法。我们看到了如何使用字典来增强标记过程。
我们简要介绍了模型训练过程。预先标记的样本文本被用作该过程的输入,模型作为输出出现。虽然我们没有解决模型的验证问题,但是这可以通过与我们在前面章节中所完成的相似的方式来完成。
各种 POS 标记方法可以根据许多因素进行比较,例如它们的准确性和运行速度。虽然我们没有在这里讨论这些问题,但是有许多可用的网络资源。一个检验他们跑得多快的对比可以在 http://mattwilkens . com/2008/11/08/evaluating-pos-taggers-speed/找到。
在下一章中,第六章,表示具有特征的文本,我们将研究基于内容对文档进行分类的技术。
六、用特征表示文本
考虑到文本的上下文,文本包含需要提取的特征,但是对机器来说,处理一整节文本以包含上下文是非常困难的。
在这一章中,我们将看到如何使用 N-gram 来呈现文本,以及它们在关联上下文中扮演什么角色。我们将看到单词嵌入,其中单词的表示被转换或映射为数字(实数),以便机器能够以更好的方式理解和处理它们。由于文本的数量,这可能导致高维数的问题。因此,接下来,我们将看到如何以保持上下文的方式降低向量的维数。
在本章中,我们将讨论以下主题:
- N-grams
- 单词嵌入
- 手套
- word2vec
- 降维
- 主成分分析
- 分布式随机邻居嵌入
N-grams
N-grams 是一种概率模型,用于预测下一个单词、文本或字母。它以统计结构捕捉语言,因为机器更擅长处理数字而不是文本。许多公司在拼写纠正和建议、断词或总结文本时使用这种方法。让我们试着去理解它。n-gram 只是一系列单词或字母,主要是单词。考虑句子"This is n-gram model"
,它有四个单词或记号,所以它是一个 4-gram;来自同一文本的 n-gram 将是“这是 n-gram”和“是 n-gram 模型”。两个单词是一个双字母,一个单词是一个单字母。让我们使用 Java 和 OpenNLP 来尝试一下:
String sampletext = "This is n-gram model";
System.out.println(sampletext);
StringList tokens = new StringList(WhitespaceTokenizer.INSTANCE.tokenize(sampletext));
System.out.println("Tokens " + tokens);
NGramModel nGramModel = new NGramModel();
nGramModel.add(tokens,3,4);
System.out.println("Total ngrams: " + nGramModel.numberOfGrams());
for (StringList ngram : nGramModel) {
System.out.println(nGramModel.getCount(ngram) + " - " + ngram);
}
我们从一个字符串开始,使用记号赋予器,我们得到所有的记号。利用nGramModel
,我们计算出 N-grams 中的N;在前面的例子中,它是 3-gram,输出如下:
This is n-gram model
Tokens [This,is,n-gram,model]
Total ngrams: 3
1 - [is,n-gram,model]
1 - [This,is,n-gram]
1 - [This,is,n-gram,model]
如果我们将n-gram
行改为 2,输出如下:
This is n-gram model
Tokens [This,is,n-gram,model]
Total ngrams: 6
1 - [is,n-gram,model]
1 - [n-gram,model]
1 - [This,is,n-gram]
1 - [This,is,n-gram,model]
1 - [is,n-gram]
1 - [This,is]
使用n-gram
,我们可以找到一个单词序列的概率:哪个单词出现在给定单词 x 之前或之后的概率。从前面的二元模型中,我们可以得出结论,model
出现在单词n-gram
之后的概率比其他任何单词都高。
下一步是准备一个频率表,找出接下来会出现的单词;例如,对于二元模型,该表如下所示:
| 字 1 | 字 2 | 计数/频率 |
| 是 | 这 | Fifty-five thousand |
| 是 | 这 | Twenty-five thousand |
| 是 | 这 | Forty-five thousand |
从这个表中,我们可以说在给定的上下文中,单词最有可能出现在单词和之前。这看起来很简单,但是想想有 20,000 或更多单词的文本。在这种情况下,频率表可能需要数十亿个条目。
另一种方式是用概率进行估算,用带单词 w1,w2 的句子 W ,…wn ,我们要从 W 求 wi 的概率将是:
这里, N =总字数和 c() 表示字数。使用概率链规则,它将是这样的:
让我们试着理解我们的句子,“这是 n 元模型”:
P(“这是 n-gram 模型”)= P(“这”)P(“是” | “这”)P(“n-gram”| “这是”)P(“模型” | “这是 n-gram”)
这看起来简单,但对于长句子和计算估计,这并不简单。但是,使用马尔可夫假设,该等式可以被简化,因为马尔可夫假设说一个单词出现的概率取决于前一个单词:
P(“这是 n-gram 模型”)= P(“这”)P(“是” | “这”)P(“n-gram”| “是”)P(“模型” | “n-gram”)
所以,现在,我们可以这样说:
单词嵌入
需要教会计算机处理上下文。比如说,“我喜欢吃苹果。”电脑需要明白,在这里,苹果是一种水果,而不是一家公司。我们希望单词具有相同含义的文本具有相同的表示,或者至少是相似的表示,以便机器可以理解单词具有相同的含义。单词嵌入的主要目的是捕获尽可能多的关于单词的上下文、层次和形态信息。
单词嵌入可以以两种方式分类:
- 基于频率的嵌入
- 基于预测的嵌入
顾名思义,基于频率的嵌入使用计数机制,而基于预测的嵌入使用概率机制。
基于频率的嵌入可以以不同的方式完成,使用计数向量、TD-IDF 向量或同现向量/矩阵。计数向量试图从所有文档中学习。它将学习一个词汇项,并计算它在目标文档中出现的次数。让我们考虑一个非常简单的例子,有两个文档, d1 和 d2 :
- d1 =计数向量,给定总字数
- d2 = Count 函数,返回集合中值的总数
下一步是寻找记号,它们是 [“计数”、“向量”、“给予”、“总计”、" of “、“字”、“返回”、“数字”、“值”、” in “、” set"] 。
给定两个文档和十一个令牌,计数向量或矩阵将如下所示:
| | 计数 | 矢量 | 给 | 总计 | 共 | 字 | 返回 | 号 | 值 | 中的 | 设置 |
| d1 | Two | one | one | one | one | one | Zero | Zero | Zero | Zero | Zero |
| d2 | one | Zero | Zero | one | one | Zero | one | one | one | one | one |
但是,当有很多文档、文本量很大并且有大量文本时,矩阵将很难构造并且包含许多行和列。有时,常用词被删除,如 a、an、the 和 this。
第二种方法是 TF-IDF 载体。 TF 代表词频,IDF 代表逆文档频率。这种方法背后的想法是删除所有文档中常见的、出现频率很高的不必要的单词,但不添加任何意义。这包括诸如 a、an、the、This、that 和 are 等单词。“The”是英语中最常见的单词,因此它会在任何文档中频繁出现。
让我们将 TF 定义为术语在文档中出现的次数/术语在文档中的数量, IDF = log(N/n) ,其中 N 是文档的数量, n 是术语在文档中出现的数量。考虑前面的例子,术语或字数在 d1 中出现两次,在 d2 中出现一次,因此其 TF 计算如下:
- TF(计数/ d1) = 2/7
- TF(计数/d2) = 1/8
- TF(总数/d1) = 1/2
- TF(总数/d2) = 1/2
让我们为单词或 term total 计算 IDF。**总计在两个文档中出现一次,因此 IDF 将为:
IDF(总计)= log(2/2) = 0
所以,如果这个词出现在每个文档中,那么这个词就有可能不太相关,可以忽略。如果该术语出现在一些文档中,而不是所有文档中,则它可能与字数有一些关联:
IDF(计数)= log(3/2) = 0.17609
为了计算 TF-IDF,我们只需将上一步计算的值相乘:
TF-IDF(合计,d1) = 1/2 * 0 = 0
TF-IDF(count,d1) = 2/7 * 0.17609 = 0.0503
另一种方法是使用共现向量或矩阵。它对一起出现的单词起作用,因此将具有相似的上下文,并因此捕获单词之间的关系。它通过决定上下文窗口的长度来工作,上下文窗口定义了要查找的单词的数量。考虑句子“这是单词嵌入的例子。”
当我们说上下文窗口的大小为 2 时,这意味着我们只对给定单词之前和之后的两个单词感兴趣。假设单词是“word”,那么当我们计算它的共现时,将只考虑“word”之前的两个单词和“word”之后的两个单词。这样的表格或矩阵被转换成概率。它有许多优点,因为它保留了单词之间的关系,但是这种矩阵的大小是巨大的。
另一种方法是使用基于预测的嵌入,这可以使用连续单词包 ( CBOW )或跳格模型来完成。CBOW 预测一个词在给定情境、上下文或场景中出现的概率,可以是单个词,也可以是多个词。考虑句子“使用连续单词包的示例单词”这样,上下文就成了 [“样”、“词”、“用”、“连续”、“包”、“的”、"词】] 。这将被输入一个神经网络。现在,它将帮助我们预测给定上下文中的单词。
另一种方法是使用 skip-gram 模型,该模型使用与 CBOW 相同的方法,但其目的是根据上下文预测给定单词的所有其他单词,也就是说,它应该预测给定单词的上下文。
这两种方法都需要理解神经网络,其中输入通过使用权重的隐藏层传递。下一层是使用 softmax 函数计算的输出层,其值与原始值进行比较,原始值可能不同于第一次运行的值,然后计算损失。损失是原始值和预测值之间的差异;然后,这个损失被反向传播,权重被调整,并且该过程被重复,直到损失最小或接近 0。
在接下来的几节中,我们将看到如何使用 word2vec,它是 CBOW 和 skip-gram 模型的组合。
手套
单词表示的全局向量 ( 手套)是单词表示的模型。它属于无监督学习的范畴。它通过开发单词出现的计数矩阵来学习。最初,它从存储几乎所有单词及其共现信息的大矩阵开始,该矩阵存储一些单词在给定文本的序列中出现的频率。Stanford NLP 中提供了对 GloVe 的支持,但 Java 中没有实现。要了解更多关于 GloVe 的信息,请访问 https://nlp.stanford.edu/pubs/glove.pdf。斯坦福手套的简介和一些资源可以在 https://nlp.stanford.edu/projects/glove/找到。为了了解 GloVe 的功能,我们将使用在github.com/erwtokritos/JGloVe
找到的 GloVe 的 Java 实现。
代码还包括测试文件和文本文件。文本文件的内容如下:
human interface computer
survey user computer system response time
eps user interface system
system human system eps
user response time
trees
graph trees
graph minors trees
graph minors survey
I like graph and stuff
I like trees and stuff
Sometimes I build a graph
Sometimes I build trees
GloVe 展示了与上一篇文章相似的单词。从前面的文本中查找类似于graph
的单词的结果如下:
INFO: Building vocabulary complete.. There are 19 terms
Iteration #1 , cost = 0.4109707480627031
Iteration #2 , cost = 0.37748817335537205
Iteration #3 , cost = 0.3563396433036622
Iteration #4 , cost = 0.3483667149265019
Iteration #5 , cost = 0.3434632969758875
Iteration #6 , cost = 0.33917154339742045
Iteration #7 , cost = 0.3304641363014488
Iteration #8 , cost = 0.32717383183159243
Iteration #9 , cost = 0.3240225514512226
Iteration #10 , cost = 0.32196412138868596
@trees
@minors
@computer
@a
@like
@survey
@eps
@interface
@and
@human
@user
@time
@response
@system
@Sometimes
所以,第一个匹配的单词是“树”,然后是“未成年人”,依此类推。它用于测试的代码如下:
String file = "test.txt";
Options options = new Options();
options.debug = true;
Vocabulary vocab = GloVe.build_vocabulary(file, options);
options.window_size = 3;
List<Cooccurrence> c = GloVe.build_cooccurrence(vocab, file, options);
options.iterations = 10;
options.vector_size = 10;
options.debug = true;
DoubleMatrix W = GloVe.train(vocab, c, options);
List<String> similars = Methods.most_similar(W, vocab, "graph", 15);
for(String similar : similars) {
System.out.println("@" + similar);
}
Word2vec
GloVe 是一个基于计数的模型,其中创建了一个矩阵来对单词进行计数,而 word2vec 是一个预测模型,它使用预测和损失调整来查找相似性。它像一个前馈神经网络一样工作,并使用各种技术进行优化,包括随机梯度下降 ( SGD ),这些都是机器学习的核心概念。它在从向量表示中的给定上下文单词预测单词时更有用。我们将使用来自 https://github.com/IsaacChanghau/Word2VecfJava 的 word2vec 的实现。我们还需要来自drive . Google . com/file/d/0 b 7 xkcwpi 5 kdynlnuttlss 21 pqmm/edit 的
GoogleNews-vectors-negative300.bin文件?usp=sharing
,因为它包含针对GoogleNews
数据集的预训练向量,包含 300 个维度的向量,包含 300 万个单词和短语。示例程序将查找相似的单词来删除。以下是输出示例:
loading embeddings and creating word2vec...
[main] INFO org.nd4j.linalg.factory.Nd4jBackend - Loaded [CpuBackend] backend
[main] INFO org.nd4j.nativeblas.NativeOpsHolder - Number of threads used for NativeOps: 2
[main] INFO org.reflections.Reflections - Reflections took 410 ms to scan 1 urls, producing 29 keys and 189 values
[main] INFO org.nd4j.nativeblas.Nd4jBlas - Number of threads used for BLAS: 2
[main] INFO org.nd4j.linalg.api.ops.executioner.DefaultOpExecutioner - Backend used: [CPU]; OS: [Linux]
[main] INFO org.nd4j.linalg.api.ops.executioner.DefaultOpExecutioner - Cores: [4]; Memory: [5.3GB];
[main] INFO org.nd4j.linalg.api.ops.executioner.DefaultOpExecutioner - Blas vendor: [OPENBLAS]
[main] INFO org.reflections.Reflections - Reflections took 373 ms to scan 1 urls, producing 373 keys and 1449 values
done...
kill 1.0000001192092896
kills 0.6048964262008667
killing 0.6003166437149048
destroy 0.5964594483375549
exterminate 0.5908634066581726
decapitate 0.5677944421768188
assassinate 0.5450955629348755
behead 0.532557487487793
terrorize 0.5281200408935547
commit_suicide 0.5269641280174255
0.10049013048410416
0.1868356168270111
降维
单词嵌入现在是自然语言处理的基本构件。GloVe,或者 word2vec,或者任何其他形式的单词嵌入都会生成一个二维矩阵,但是它存储在一维向量中。 Dimensonality 这里指的是这些向量的大小,和词汇量的大小不一样。下图取自 https://nlp.stanford.edu/projects/glove/的,显示了词汇与向量维度的关系:
大维度的另一个问题是在现实世界中使用单词嵌入所需的内存;具有超过一百万个标记的简单的 300 维向量将需要 6 GB 或更多的内存来处理。在真实的 NLP 用例中,使用这么多内存是不实际的。最好的方法是减少维数来减小尺寸。 t 分布随机邻居嵌入 ( t-SNE )和主成分分析 ( PCA )是两种常用的实现降维的方法。在下一节中,我们将看到如何使用这两种算法来实现降维。
主成分分析
主成分分析 ( PCA )是一种线性的确定性算法,试图捕捉数据内的相似性。一旦发现相似性,就可以用它从高维数据中去除不必要的维度。它使用特征向量和特征值的概念。假设你对矩阵有基本的了解,一个简单的例子将帮助你理解特征向量和特征值:
这相当于以下内容:
这是特征向量的情况, 4 是特征值。
PCA 方法很简单。它从数据中减去平均值开始;然后,它找到协方差矩阵,并计算其特征向量和特征值。一旦你有了特征向量和特征值,把它们从高到低排序,这样我们就可以忽略不太重要的部分。如果特征值很小,损耗可以忽略不计。如果您有具有 n 维的数据,并且您计算了 n 个特征向量和特征值,您可以从 n 个特征向量中选择一些,比如说, m 个特征向量,其中 m 将总是小于 n ,因此最终数据集将只有 m 维。
分布式随机邻居嵌入
T-分布式随机邻居嵌入 ( t-SNE ),广泛应用于机器学习中,是一种非线性、非确定性的算法,创建了数千维数据的二维映射。
换句话说,它将高维空间中的数据转换成 2D 平面。SNE 霸王龙试图保留数据中的本地邻居。这是一种非常流行的降维方法,因为它非常灵活,能够在其他算法失败的地方找到数据中的结构或关系。它通过计算对象 i 选择潜在邻居 j 的概率来做到这一点。它将从高维空间中选取相似的对象,因为它比不太相似的对象具有更高的概率。它使用对象之间的欧几里德距离作为相似性度量的基础。t-SNE 使用困惑功能进行微调,并决定如何平衡本地和全球数据。
t-SNE 实现有多种语言版本;我们将使用在 https://github.com/lejon/T-SNE-Java可用的那个。使用git
和mvn
,您可以构建和使用这里提供的例子。执行以下命令:
> git clone https://github.com/lejon/T-SNE-Java.git
> cd T-SNE-Java
> mvn install
> cd tsne-demo
> java -jar target/tsne-demos-2.4.0.jar -nohdr -nolbls src/main/resources/datasets/iris_X.txt
输出如下所示:
TSneCsv: Running 2000 iterations of t-SNE on src/main/resources/datasets/iris_X.txt
NA string is: null
Loaded CSV with: 150 rows and 4 columns.
Dataset types:[class java.lang.Double, class java.lang.Double, class java.lang.Double, class java.lang.Double]
V0 V1 V2 V3
0 5.10000000 3.50000000 1.40000000 0.20000000
1 4.90000000 3.00000000 1.40000000 0.20000000
2 4.70000000 3.20000000 1.30000000 0.20000000
3 4.60000000 3.10000000 1.50000000 0.20000000
4 5.00000000 3.60000000 1.40000000 0.20000000
5 5.40000000 3.90000000 1.70000000 0.40000000
6 4.60000000 3.40000000 1.40000000 0.30000000
7 5.00000000 3.40000000 1.50000000 0.20000000
8 4.40000000 2.90000000 1.40000000 0.20000000
9 4.90000000 3.10000000 1.50000000 0.10000000
Dim:150 x 4
000: [5.1000, 3.5000, 1.4000, 0.2000...]
001: [4.9000, 3.0000, 1.4000, 0.2000...]
002: [4.7000, 3.2000, 1.3000, 0.2000...]
003: [4.6000, 3.1000, 1.5000, 0.2000...]
004: [5.0000, 3.6000, 1.4000, 0.2000...]
.
.
.
145: [6.7000, 3.0000, 5.2000, 2.3000]
146: [6.3000, 2.5000, 5.0000, 1.9000]
147: [6.5000, 3.0000, 5.2000, 2.0000]
148: [6.2000, 3.4000, 5.4000, 2.3000]
149: [5.9000, 3.0000, 5.1000, 1.8000]
X:Shape is = 150 x 4
Using no_dims = 2, perplexity = 20.000000, and theta = 0.500000
Computing input similarities...
Done in 0.06 seconds (sparsity = 0.472756)!
Learning embedding...
Iteration 50: error is 64.67259135061494 (50 iterations in 0.19 seconds)
Iteration 100: error is 61.50118570075227 (50 iterations in 0.20 seconds)
Iteration 150: error is 61.373758889762875 (50 iterations in 0.20 seconds)
Iteration 200: error is 55.78219488135168 (50 iterations in 0.09 seconds)
Iteration 250: error is 2.3581173593529687 (50 iterations in 0.09 seconds)
Iteration 300: error is 2.2349608757095827 (50 iterations in 0.07 seconds)
Iteration 350: error is 1.9906437450336596 (50 iterations in 0.07 seconds)
Iteration 400: error is 1.8958764344779482 (50 iterations in 0.08 seconds)
Iteration 450: error is 1.7360726540960958 (50 iterations in 0.08 seconds)
Iteration 500: error is 1.553250634564741 (50 iterations in 0.09 seconds)
Iteration 550: error is 1.294981722012944 (50 iterations in 0.06 seconds)
Iteration 600: error is 1.0985607573299603 (50 iterations in 0.03 seconds)
Iteration 650: error is 1.0810715645272573 (50 iterations in 0.04 seconds)
Iteration 700: error is 0.8168399675722107 (50 iterations in 0.05 seconds)
Iteration 750: error is 0.7158739920771124 (50 iterations in 0.03 seconds)
Iteration 800: error is 0.6911748222330966 (50 iterations in 0.04 seconds)
Iteration 850: error is 0.6123536061655738 (50 iterations in 0.04 seconds)
Iteration 900: error is 0.5631133416913786 (50 iterations in 0.04 seconds)
Iteration 950: error is 0.5905547118496892 (50 iterations in 0.03 seconds)
Iteration 1000: error is 0.5053631170520657 (50 iterations in 0.04 seconds)
Iteration 1050: error is 0.44752244538411406 (50 iterations in 0.04 seconds)
Iteration 1100: error is 0.40661841893114614 (50 iterations in 0.03 seconds)
Iteration 1150: error is 0.3267394426152807 (50 iterations in 0.05 seconds)
Iteration 1200: error is 0.3393774577158965 (50 iterations in 0.03 seconds)
Iteration 1250: error is 0.37023103950965025 (50 iterations in 0.04 seconds)
Iteration 1300: error is 0.3192975790641602 (50 iterations in 0.04 seconds)
Iteration 1350: error is 0.28140161036965816 (50 iterations in 0.03 seconds)
Iteration 1400: error is 0.30413739839879855 (50 iterations in 0.04 seconds)
Iteration 1450: error is 0.31755361125826165 (50 iterations in 0.04 seconds)
Iteration 1500: error is 0.36301524742916624 (50 iterations in 0.04 seconds)
Iteration 1550: error is 0.3063801941900375 (50 iterations in 0.03 seconds)
Iteration 1600: error is 0.2928584822753138 (50 iterations in 0.03 seconds)
Iteration 1650: error is 0.2867502934852756 (50 iterations in 0.03 seconds)
Iteration 1700: error is 0.470469997545481 (50 iterations in 0.04 seconds)
Iteration 1750: error is 0.4792376115843584 (50 iterations in 0.04 seconds)
Iteration 1800: error is 0.5100126924750723 (50 iterations in 0.06 seconds)
Iteration 1850: error is 0.37855035406353427 (50 iterations in 0.04 seconds)
Iteration 1900: error is 0.32776847081948496 (50 iterations in 0.04 seconds)
Iteration 1950: error is 0.3875134029990107 (50 iterations in 0.04 seconds)
Iteration 1999: error is 0.32560416632168365 (50 iterations in 0.04 seconds)
Fitting performed in 2.29 seconds.
TSne took: 2.43 seconds
这个例子使用了iris_X.txt
,它有 150 行 4 列,所以尺寸是 150 x 4。它试图通过将困惑度设置为 20 并将θ设置为 0.5 来将这些维度减少到 2。它对iris_X.txt
中提供的数据进行迭代,并使用梯度下降,在 2000 次迭代后得出 2D 平面上的图形。该图显示了 2D 平面中数据的聚类,从而有效地降低了维度。对于如何实现这一点的数学方法,有许多关于该主题的论文,维基百科的文章(en . Wikipedia . org/wiki/T-distributed _ random _ neighbor _ embedding
)也对此进行了解释。
摘要
在这一章中,我们讨论了单词嵌入以及它在自然语言处理中的重要性。n-gram 用于显示如何将单词视为向量,以及如何存储单词数以找到相关性。GloVe 和 word2vec 是两种常见的单词嵌入方法,其中单词计数或概率存储在向量中。这两种方法都导致高维数,这在现实世界中是不可行的,尤其是在移动设备或内存较少的设备上。我们已经看到了两种不同的降维方法。在下一章中,第七章,信息检索我们将看到如何从文本等非结构化格式中进行信息检索。
七、信息检索
信息检索 ( IR )处理在非结构化数据中寻找信息。任何没有特定或一般化结构的数据都是非结构化数据,处理这样的数据对机器来说是一个巨大的挑战。非结构化数据的一些例子是本地 PC 或 web 上可用的文本文件、doc 文件、XML 文件等。因此,处理如此大量的非结构化数据并找到相关信息是一项具有挑战性的任务。
我们将在本章中讨论以下主题:
- 布尔检索
- 字典和容错检索
- 向量空间模型
- 评分和术语权重
- 逆文档频率
- TF-IDF 加权
- 信息检索系统的评价
布尔检索
布尔检索处理一种检索系统或算法,其中 IR 查询可以被视为使用操作AND
、OR
和NOT
的术语的布尔表达式。布尔检索模型是一种将文档视为单词并可以使用布尔表达式应用查询词的模型。一个标准的例子是考虑莎士比亚的文集。该查询用于确定包含单词“Brutus”和“Caesar”但不包含“Calpurnia”的戏剧。使用在基于 Unix 的系统上可用的grep
命令,这样的查询是可行的。
当文档大小有限时,这是一个有效的过程,但是要快速处理大的文档或 web 上可用的数据量,并根据出现次数对其进行排序是不可能的。
另一种方法是提前为文档编制索引。方法是创建一个关联矩阵,以二进制形式记录,并标记该术语是否出现在给定的播放中:
| | 安东尼和克娄巴特拉 | 尤利乌斯·凯撒 | 暴风雨 | 哈姆雷特 | 奥赛罗 | 麦克白 |
| 布鲁图斯 | one | one | Zero | Zero | Zero | one |
| 凯撒 | one | one | Zero | one | Zero | Zero |
| 卡尔珀尼亚 | Zero | one | Zero | Zero | Zero | Zero |
| 怜悯 | one | Zero | one | one | one | one |
| 错误 | one | Zero | one | one | one | Zero |
现在,为了回答前面对“布鲁图斯”和“凯撒”而不是“卡尔珀尼亚”的请求,这个查询可以变成 110100 和 110111 和 101111 = 100100,所以答案是安东尼和克利奥帕特拉和哈姆雷特是满足我们查询的戏剧。
前面的矩阵很好,但考虑到语料库很大,它可以增长为任何带有 1 和 0 条目的东西。设想创建一个包含 100 万个文档的 500,000 个术语的矩阵,这将产生一个 500,000 x 100 万个维度的矩阵。如上表所示,矩阵条目将为 0 和 1,因此使用倒排索引。它以字典的形式存储术语和文档列表,如下图所示:
取自 https://nlp.stanford.edu/IR-book/pdf/01bool.pdf
术语中的文档来自一个列表,称为过帐列表,单个文档称为过帐。为了创建这样的结构,文档被标记化,并且所创建的标记通过语言预处理被规范化。一旦形成了规范化的标记,就创建了字典和发布。为了提供排名,还存储了术语的频率,如下图所示:
存储的额外信息对于排名检索模型中的搜索引擎是有用的。为了高效的查询处理,还对发布列表进行排序。使用这种方法,减少了存储需求;回想一下有 1 和 0 的 m x n 矩阵。这也有助于处理布尔查询或检索。
字典和容错检索
字典数据结构存储列表术语词汇表,以及包含给定术语的文档列表,也作为发布。
字典数据结构可以以两种不同的方式存储:使用哈希表或树。当语料库增长时,存储这种数据结构的幼稚方法将导致性能问题。一些信息检索系统使用散列方法,而另一些使用树方法来制作字典。这两种方法各有利弊。
哈希表以整数的形式存储词汇术语,这是通过哈希得到的。哈希表中的查找或搜索更快,因为它是时间常数 O(1) 。如果搜索是基于前缀的搜索,如查找以“abc”开头的文本,那么如果使用散列表来存储术语,将不起作用,因为术语将被散列。不容易找到微小的变异。随着条款的增多,重复使用的成本也越来越高。
基于树方法使用树结构,通常是二叉树,这对于搜索非常有效。它有效地处理前缀库搜索。它比较慢,因为搜索需要花费 O(log M) 的时间。采油树的每次重新平衡都很昂贵。
通配符查询
通配符查询使用*
来指定要搜索的内容。它可以出现在不同的地方,如单词的开头或结尾。搜索词可能以*its
开头,这意味着查找以its
结尾的单词。这种查询称为后缀查询。搜索词可能会在末尾使用*
,比如its*
,表示查找以its
开头的单词。这种查询称为前缀查询。就树而言,前缀查询很容易,因为它们需要我们在its <= t <= itt
之间查找术语。后缀查询需要额外的树来维护向后移动的术语。下一种需要更多操作的查询是中间有*
的查询,比如"fil*er"
、"se*te"
和"pro*cent"
。要解决这样的查询,需要找到"fil*"
和"*er"
,并将两个集合的结果求交集。这是一个昂贵的操作,因为需要在树的两个方向上遍历;这需要一个变通方法来简化它。一种方法是修改查询,使其仅在末尾包含"*"
。permuterm 索引方法为单词添加了一个特殊字符"$"
;例如,术语“你好”可以表示为hello$
、ello$h
、llo$he
、lo$hel
或o$hell
。让我们假设这个查询是针对hel*o
的,那么它将寻找hel
和o
,以o$hel
结束。它只是旋转通配符,使其只出现在末尾。它将 B 树中的所有旋转相加。也很占地方。另一种方法是使用 bigram (k-gram)索引,它比 permuterm 索引更有效。在二元模型索引中,所有的 bigram 都被枚举。例如,“四月是最残酷的一个月”,拆分成 2-grams (bigrams)将如下所示:
$a, ap, pr, ri, il, l$, $i, is, s$, $t, th, he, e$, $c, cr, ru, ue, el, le, es, st, t$, $m, mo, on, nt, h$
$
用来表示学期的开始或结束。它为所有二元模型和包含该二元模型的字典术语维护第二个索引的倒排形式。它检索所有匹配二元模型的帖子,并与整个列表相交。现在,像hel*
这样的查询作为$h
、he
和el
运行。它应用后置过滤器来过滤不相关的结果。它既快又节省空间。
拼写纠正
拼写纠正最好的例子是谷歌。当我们搜索拼写不正确的内容时,它会给出正确的拼写建议,如下图所示:
谷歌上的拼写纠正简单例子
大多数拼写校正算法使用的两个基本原则如下:
- 找到与拼写错误的单词最接近的匹配项。这就要求我们对术语有接近度。
- 如果两个或两个以上的单词是正确的,并且连在一起,请使用最常用的一个。最常见的单词是基于文档中每个术语的计数计算的;选择最高的。
拼写校正的两种具体形式是孤立术语校正和上下文敏感校正。孤立术语校正处理拼写错误。基本上,它检查每个单词的拼写错误;它不考虑句子的上下文。例如,如果遇到单词“form ”,而不是单词“from ”,它会将其视为正确,因为拼写是正确的。上下文敏感校正将查看周围的单词,并可以建议所需的校正,因此它可以建议“形式”而不是“形式”如果给定的句子是“我们从 A 点飞到 B 点”,在这个句子中,单词“form”是错误的,但是拼写是正确的,所以孤立的术语校正将把它视为正确的,而上下文敏感的校正将建议“from”而不是“form”
桑迪克斯
当拼写错误是由听起来像目标术语的查询引起时,需要语音纠正。这主要发生在人名中。这个想法是为每个词生成一个散列,使其与发音相同的单词相同。算法执行语音散列,使得对于相似发音单词的散列是相同的,这被称为 Soundex 算法。它是 1981 年为美国人口普查而发明的。方法如下:
- 将每个要索引的术语转换为四个字符的简化形式。从这些简化形式到原始术语建立倒排索引;称之为 Soundex 指数。
- 对查询词进行同样的操作。
- 当查询要求 Soundex 匹配时,搜索这个 Soundex 索引。
它是许多流行数据库提供的标准算法。Soundex 对信息检索没有太大帮助,但它有自己的应用,其中按人名搜索很重要。
向量空间模型
布尔检索工作良好,但它只给出二进制输出;它表示术语匹配或不在文档中,如果只有有限数量的文档,这很好。如果文档数量增加,生成的结果人类很难遵循。考虑一个搜索词,在一百万个文档中搜索 X,其中一半返回肯定结果。下一阶段是根据某种基础(比如等级或其他机制)对文档进行排序,以显示结果。
如果需要排名,那么文档需要附加某种分数,这是由搜索引擎给出的。对于普通用户来说,编写布尔查询本身是一项困难的任务,他们必须使用 and、or 和 not 进行查询。实时查询可以简单到单个单词查询,也可以复杂到包含多个单词的句子。
向量空间模型可以分为三个阶段:
- 文档索引,从文档中提取术语
- 对索引术语进行加权,从而可以增强检索系统
- 基于查询和相似性度量对文档进行排序
总是有元数据与包含各种类型信息的文档相关联,例如:
- 作者详细信息
- 编成日期
- 文件的格式
- 标题
- 出版日期
- 抽象(尽管不总是)
这些元数据有助于形成查询,例如“搜索作者为 xyz 并发表于 2017 的所有文档”或“搜索标题包含单词 AI 且作者为 ABC 的文档”对于这样的查询,维护参数索引,并且这样的查询被称为参数搜索。区域包含自由文本,如标题,这在参数索引中是不可能的。通常,为每个参数准备一个单独的参数指数。搜索标题或摘要需要区域方法。为每个区域准备了一个单独的索引,如下图所示:
这确保了数据的有效检索和存储。对于字段和区域的布尔查询和检索,它仍然工作得很好。
将一组文档表示为公共向量空间中的向量被称为向量空间模型。
评分和术语权重
术语加权处理评估术语相对于文档的重要性。一个简单的方法是,除了停用词之外,在文档中出现较多的术语是一个重要的术语。可以给每个文档分配 0-1 的分数。分数是显示术语或查询在文档中匹配程度的度量。分数为 0 表示文档中不存在该术语。随着术语在文档中的出现频率增加,分数从 0 向 1 移动。因此,对于给定的术语 X ,三个文档、 d1 、 d2 和 d3 的得分分别为 0.2、0.3 和 0.5,这意味着 d3 中的匹配比 d2 更重要,而 d1 对于总得分最不重要。这同样适用于这些区域。如何给术语分配这样的分数或权重需要从一些训练集中学习,或者连续运行并更新术语的分数。
实时查询将是自由文本的形式,而不是布尔表达式的形式;例如,布尔查询将能够回答某个东西是否看起来像 A 和 B ,但是不能回答 C ,而自由文本查询将检查 A 是否与 B 在一起并且 C 是否不存在。因此,在自由文本中,需要一种评分机制,将每个单独术语的分数相加,并根据文档将权重分配给该术语。最简单的方法是分配一个权重,该权重等于该术语在文档中出现的次数。这种加权方案称为词频,通常记为,其中 tf 为词频, t 为词频, d 为文档。
逆文档频率
如果我们认为所有的术语对于所有的查询都具有相同的重要性,那么它并不适用于所有的查询。如果文档与冰有关,很明显“冰”几乎会出现在所有文档中,很可能出现频率很高。收集频率和文档频率是两个不同的术语,需要加以解释。一个集合包含许多文档。集合频率 ( 比照)显示术语 ( t )在集合中所有文档中出现的频率,而文档频率 ( df )显示 t 在单个文档中出现的频率。所以单词“ice”将具有高的收集频率,因为它被假定出现在集合中的所有文档中。一个简单的想法是,如果这些词的收集频率很高,就降低它们的权重。逆频率定义如下:
这里, N 是集合中的文档总数。经常性术语的 idf 可能较低,而非经常性术语的 idf 可能较高。
TF-IDF 加权
TF-IDF 结合了术语频率 ( TF )和逆文档频率 ( IDF )的方法,为文档中的每个术语生成一个权重,使用以下公式完成:
换句话说,它给文档 d 中的术语 t 分配一个权重,如下所示:
- 如果 term t 在几个文档中出现多次,它将是最高的
- 如果术语 t 在一个文档中出现的次数很少,它将会更低
- 如果术语 t 出现在所有文档中,它将是最低的
- 如果术语 t 没有出现在任何文档中,则为 0
信息检索系统的评价
为了以标准的方式评估一个信息检索系统,需要一个测试集,它应该具有以下内容:
- 一批文件
- 测试所需信息的查询集
- 相关或不相关的二元评估
集合中的文件分为相关和不相关两类。测试文档集合应该有一个合理的大小,这样测试可以有一个合理的范围来找到平均性能。输出的相关性总是相对于所需的信息进行评估,而不是基于查询。换句话说,在结果中有一个查询词并不意味着它是相关的。例如,如果搜索词或查询是“Python”,结果可能显示 Python 编程语言或宠物 Python;两个结果都包含查询术语,但是它是否与用户相关是重要的因素。如果系统包含一个参数化的索引,那么它可以被调优以获得更好的性能,在这种情况下,需要一个单独的测试集合来测试参数。可能发生的情况是,根据也由参数改变的参数,分配的权重是不同的。
有一些标准的测试集可用于信息检索的评估。其中一些如下所列:
- 克兰菲尔德收集了 1398 份空气动力学杂志的摘要和 225 个问题,以及对所有问题的详尽的相关性判断。
- 从 1992 年开始,文本检索会议 ( TREC )维持了一个大的 IR 测试系列用于评估。它由 189 万个文档和 450 个信息需求的相关性判断组成。
- GOV2 拥有 2500 万个网页。
- NTCIR 侧重于东亚语言和跨语言信息检索的测试集。http://ntcir.nii.ac.jp/about/
- 路透社由 806,791 份文件组成。
- 20 新闻组是另一个广泛用于分类的集合。
用于发现检索系统有效性的两个度量是精确度和召回率。Precision 是检索到的相关文档的比例,recall 是找到的相关文档的比例。
摘要
在本章中,我们讲述了如何使用各种技术从非结构化数据中找到信息。我们讨论了布尔检索、字典和容错检索。我们还讲述了通配符查询及其使用方法。简要介绍拼写校正,然后介绍向量空间模型和 TF-IDF 加权,最后介绍信息检索评估。在下一章第八章、文本和文档分类中,我们将讲述如何对文本和文档进行分类。
八、文本和文档分类
在本章中,我们将演示如何使用各种自然语言处理(NLP)API 来执行文本分类。这不要与文本聚类混淆。聚类涉及不使用预定义类别的文本识别。相反,分类使用预定义的类别。在这一章中,我们将着重于文本分类,其中标签被分配给文本以指定其类型。
用于执行文本分类的一般方法从模型的训练开始。该模型被验证,然后用于分类文档。我们将重点关注该流程的培训和使用阶段。
文档可以根据任意数量的属性进行分类,例如主题、文档类型、出版时间、作者、使用的语言和阅读水平。一些分类方法需要人工标记样本数据。
情感分析是一种分类。它关注的是确定文本试图向读者传达什么,通常是以积极或消极的态度。我们将研究几种可用于执行这种类型分析的技术。
我们将在本章中讨论以下主题:
- 如何使用分类
- 理解情感分析
- 文本分类技术
- 使用 API 对文本进行分类
如何使用分类
对文本进行分类有多种用途:
- 垃圾邮件检测
- 作者归属
- 情感分析
- 年龄和性别识别
- 确定文档的主题
- 语言识别
垃圾邮件是大多数电子邮件用户的不幸现实。如果一封电子邮件可以被归类为垃圾邮件,那么它可以被移动到垃圾邮件文件夹。可以分析文本消息,并且可以使用某些属性来将该电子邮件指定为垃圾邮件。这些属性可能包括拼写错误、缺少合适的收件人电子邮件地址以及不标准的 URL。
分类已被用来确定文件的作者。这已经在历史文献上执行过,例如《联邦党人文集》和《?? 原色》这本书,作者是用分类技术识别的。
情感分析是一种确定一段文本的态度的技术。电影评论一直是这种分析的热门领域,但它几乎可以用于任何产品评论。这有助于公司更好地评估他们的产品是如何被感知的。通常,一个消极或积极的属性被分配给文本。情感分析也称为意见提取/挖掘和主观性分析。消费者信心和股票市场的表现可以从推特和其他来源预测。
分类可用于确定文本作者的年龄和性别,并提供对其作者的更多了解。通常,代词、限定词和名词短语的数量被用来识别作者的性别。女性倾向于使用更多的代词,男性倾向于使用更多的限定词。
当我们需要组织大量文档时,确定文本的主题是很有用的。搜索引擎非常关注这种活动,但它也被简单地用于将文档放在不同的类别中——例如,在标签云中。标签云是反映每个单词出现的相对频率的一组单词。
下图是 IBM Word Cloud Generator 生成的标签云的例子(www . softpedia . com/get/Office-Tools/Other-Office-Tools/IBM-Word-Cloud-Generator . shtml
),可以在upload . wikimedia . org/Wikipedia/commons/9/9e/Foundation-l _ Word _ Cloud _ without _ headers _ and _ quotes . png
找到:
使用分类技术来支持对文档所使用的语言的识别。这种分析对于许多 NLP 问题非常有用,在这些问题中我们需要应用特定的语言模型。
理解情感分析
对于情绪分析,我们关心的是谁对某个特定的产品或话题有什么样的感觉。例如,这可以告诉我们,一个特定城市的公民对一个运动队的表现有积极或消极的感觉。他们对团队表现的看法可能和对管理的看法不同。
情感分析可以用于自动确定关于产品的某些方面或属性的情感,然后以某种有意义的方式显示结果。
凯利蓝皮书(www.kbb.com/toyota/camry/2014-toyota-camry/?)对 2014 款凯美瑞的回顾说明了这一点 r=471659652516861060
),如下截图所示:
如果您向下滚动,可以找到关于该型号的专家评论,如下所示:
属性(如总体评分和值)以条形图和数值的形式显示。这些值的计算可以使用情感分析来自动执行。
情感分析可以应用于一个句子、一个子句或者整个文档。情感分析可以是正面的,也可以是负面的,或者它可以是使用数字值的评级,比如 1 到 10。更复杂的态度类型是可能的。
使过程更加复杂的是,在单个句子或文档中,可以针对不同的主题表达不同的情感。
我们如何知道哪些词有哪些类型的情感?这个问题可以用情感词典来回答。在这种情况下,词典是包含不同单词情感的字典。《普通问询者》
(www.wjh.harvard.edu/~inquirer/
)就是这样一部词典。它包含 1915 个被认为是肯定的单词。它还包含一个表示其他属性的单词列表,如痛苦、快乐、力量和动机。还有其他可供使用的词典,如 MPQA 主观性线索词典(mpqa.cs.pitt.edu/
)。
有时,可能需要建立一个词典。这通常是使用半监督学习来完成的,其中使用一些标记的例子或规则来引导词汇构建过程。当正在使用的词典的领域与我们正在处理的问题领域的领域不匹配时,这是很有用的。
我们不仅对获得积极或消极的情绪感兴趣,我们还对确定情绪的属性感兴趣——有时称为目标。考虑下面的例子:
“旅途很艰难,但乘务员做得很好,让我们很舒服。”
这句话包含两种情绪:粗糙和舒适。第一个是负的,第二个是正的。积极情绪的目标或属性是工作,消极情绪的目标是旅行。
文本分类技术
分类与获取一个特定的文档并确定它是否属于其他几个文档组之一有关。有两种对文本进行分类的基本技术:
- 基于规则的分类
- 监督机器学习
基于规则的分类使用单词和其他属性的组合,这些属性是围绕专家制定的规则组织的。这些可能非常有效,但创建它们是一个耗时的过程。
监督机器学习 ( SML )采用一组带注释的训练文档来创建模型。该模型通常被称为分类器。有很多不同的机器学习技术,包括朴素贝叶斯、支持向量机 ( SVM )、k-最近邻。
我们并不关心这些方法是如何工作的,但是感兴趣的读者将会找到无数的扩展这些和其他技术的资料。
使用 API 对文本进行分类
我们将使用 OpenNLP、Stanford API 和 LingPipe 来演示各种分类方法。我们将花更多的时间在 LingPipe 上,因为它提供了几种不同的分类方法。
使用 OpenNLP
DocumentCategorizer
接口指定了可用于支持分类过程的方法。该接口由DocumentCategorizerME
类实现。该课程将使用最大熵框架将文本分类到预定义的类别中。在本节中,我们将执行以下操作:
- 演示如何训练模型
- 说明如何使用该模型
训练 OpenNLP 分类模型
首先,我们必须训练我们的模型,因为 OpenNLP 没有预构建的模型。这个过程包括创建一个训练数据文件,然后使用DocumentCategorizerME
模型来执行实际的训练。创建的模型
通常保存在一个文件中以备后用。
培训文件格式由一系列行组成,每行代表一个文档。这一行的第一个词是类别。类别后面是由空格分隔的文本。下面是一个dog
类别的例子:
dog The most interesting feature of a dog is its ...
为了演示训练过程,我们创建了en-animals.train
文件,其中我们创建了两个类别:猫和狗。对于训练文本,我们使用了维基百科的部分内容。对于狗(en.wikipedia.org/wiki/Dog
,我们用作为宠物部分。对于猫(en.wikipedia.org/wiki/Cats_and_humans
),我们用了宠物段加上驯养品种段的第一段。我们还删除了部分中的数字引用。
下面的代码显示了每一行的第一部分:
dog The most widespread form of interspecies bonding occurs ...
dog There have been two major trends in the changing status of ...
dog There are a vast range of commodity forms available to ...
dog An Australian Cattle Dog in reindeer antlers sits on Santa's lap ...
dog A pet dog taking part in Christmas traditions ...
dog The majority of contemporary people with dogs describe their ...
dog Another study of dogs' roles in families showed many dogs have ...
dog According to statistics published by the American Pet Products ...
dog The latest study using Magnetic resonance imaging (MRI) ...
cat Cats are common pets in Europe and North America, and their ...
cat Although cat ownership has commonly been associated ...
cat The concept of a cat breed appeared in Britain during ...
cat Cats come in a variety of colors and patterns. These are physical ...
cat A natural behavior in cats is to hook their front claws periodically ...
cat Although scratching can serve cats to keep their claws from growing ...
创建训练数据时,使用足够大的样本量非常重要。我们使用的数据不足以进行某些分析。然而,正如我们将看到的,它在正确识别类别方面做得很好。
DoccatModel
类支持文本的分类和归类。使用基于注释文本的train
方法来训练模型。train
方法使用一个表示语言的字符串和一个保存训练数据的ObjectStream<DocumentSample>
实例。DocumentSample
实例保存带注释的文本及其类别。
在下面的示例中,en-animal.train
文件用于训练模型。它的输入流被用来创建一个PlainTextByLineStream
实例,然后被转换成一个ObjectStream<DocumentSample>
实例。然后应用train
方法。代码包含在一个try-with-resources
块中以处理异常。我们还创建了一个输出流,我们将使用它来持久化模型:
DoccatModel model = null;
try (InputStream dataIn =
new FileInputStream("en-animal.train");
OutputStream dataOut =
new FileOutputStream("en-animal.model");) {
ObjectStream<String> lineStream
= new PlainTextByLineStream(dataIn, "UTF-8");
ObjectStream<DocumentSample> sampleStream =
new DocumentSampleStream(lineStream);
model = DocumentCategorizerME.train("en", sampleStream);
...
} catch (IOException e) {
// Handle exceptions
}
输出如下,为了简洁起见,已经缩短了:
Indexing events using cutoff of 5
Computing event counts... done. 12 events
Indexing... done.
Sorting and merging events... done. Reduced 12 events to 12.
Done indexing.
Incorporating indexed data for training...
done.
Number of Event Tokens: 12
Number of Outcomes: 2
Number of Predicates: 30
...done.
Computing model parameters ...
Performing 100 iterations.
1: ... loglikelihood=-8.317766166719343 0.75
2: ... loglikelihood=-7.1439957443937265 0.75
3: ... loglikelihood=-6.560690872956419 0.75
4: ... loglikelihood=-6.106743124066829 0.75
5: ... loglikelihood=-5.721805583104927 0.8333333333333334
6: ... loglikelihood=-5.3891508904777785 0.8333333333333334
7: ... loglikelihood=-5.098768040466029 0.8333333333333334
...
98: ... loglikelihood=-1.4117372921765519 1.0
99: ... loglikelihood=-1.4052738190352423 1.0
100: ... loglikelihood=-1.398916120150312 1.0
使用serialize
方法保存模型,如下面的代码所示。模型被保存到en-animal.model
文件中,就像在前面的try-with-resources block
中打开的一样:
OutputStream modelOut = null;
modelOut = new BufferedOutputStream(dataOut);
model.serialize(modelOut);
使用文档分类器对文本进行分类
一旦创建了一个模型,我们就可以使用DocumentCategorizerME
类对文本进行分类。我们需要读取模型,创建一个DocumentCategorizerME
类的实例,然后调用categorize
方法返回一个概率数组,告诉我们文本最适合哪个类别。
因为我们是从文件中读取,所以需要处理异常,如下所示:
try (InputStream modelIn =
new FileInputStream(new File("en-animal.model"));) {
...
} catch (IOException ex) {
// Handle exceptions
}
通过InputStream
,我们创建了DoccatModel
和DocumentCategorizerME
类的实例,如下所示:
DoccatModel model = new DoccatModel(modelIn);
DocumentCategorizerME categorizer =
new DocumentCategorizerME(model);
使用字符串作为参数调用categorize
方法。这将返回一个 double 值数组,其中每个元素都具有文本属于某个类别的可能性。DocumentCategorizerME
类的getNumberOfCategories
方法返回模型处理的类别数。DocumentCategorizerME
类的getCategory
方法返回给定类别的索引。
我们在下面的代码中使用了这些方法来显示每个类别及其相应的可能性:
double[] outcomes = categorizer.categorize(inputText);
for (int i = 0; i<categorizer.getNumberOfCategories(); i++) {
String category = categorizer.getCategory(i);
System.out.println(category + " - " + outcomes[i]);
}
为了测试,我们使用了维基百科中《绿野仙踪》(【http://en.wikipedia.org/wiki/Toto_%28Oz%29】)中多萝西的狗托托的部分文章。我们用了经典著作的第一句话一节,在这里声明:
String toto = "Toto belongs to Dorothy Gale, the heroine of "
+ "the first and many subsequent books. In the first "
+ "book, he never spoke, although other animals, native "
+ "to Oz, did. In subsequent books, other animals "
+ "gained the ability to speak upon reaching Oz or "
+ "similar lands, but Toto remained speechless.";
为了测试一只猫,我们使用了位于 https://en.wikipedia.org/wiki/Tortoiseshell_cat 的的维基百科文章中玳瑁和印花布部分的第一句话,如下所示:
String calico = "This cat is also known as a calimanco cat or "
+ "clouded tiger cat, and by the abbreviation 'tortie'. "
+ "In the cat fancy, a tortoiseshell cat is patched "
+ "over with red (or its dilute form, cream) and black "
+ "(or its dilute blue) mottled throughout the coat.";
使用toto
的文本,我们得到以下输出。这表明该文本应放在dog
类别中:
dog - 0.5870711529777994
cat - 0.41292884702220056
使用calico
会产生以下结果:
dog - 0.28960436044424276
cat - 0.7103956395557574
我们可以使用getBestCategory
方法只返回最佳类别。此方法使用结果数组并返回一个字符串。getAllResults
方法将以字符串的形式返回所有结果。这两种方法说明如下:
System.out.println(categorizer.getBestCategory(outcomes));
System.out.println(categorizer.getAllResults(outcomes));
输出如下所示:
cat
dog[0.2896] cat[0.7104]
使用斯坦福 API
斯坦福 API 支持几个分类器。我们将研究使用ColumnDataClassifier
类进行一般分类,使用StanfordCoreNLP
管道进行情感分析。斯坦福 API 支持的分类器有时很难使用。通过ColumnDataClassifier
类,我们将演示如何对盒子的大小进行分类。通过管道,我们将说明如何确定短文本短语的正面或负面情绪。分类器可以从www-nlp.stanford.edu/wiki/Software/Classifier
下载。
使用 ColumnDataClassifier 类进行分类
该分类器使用具有多个值的数据来描述数据。在本演示中,我们将使用一个训练文件来创建一个分类器。然后我们将使用一个测试文件来评估分类器的性能。该类使用属性文件来配置创建过程。
我们将创建一个分类器,试图根据它的尺寸来分类一个盒子。有三种可能的类别:小型、中型和大型。盒子的高度、宽度和长度将被表示为浮点数。它们用于描述箱子的特征。
属性文件指定参数信息,并提供有关训练和测试文件的数据。可以指定许多可能的属性。对于这个例子,我们将只使用几个更相关的属性。
我们将使用下面的属性文件,另存为box.prop
。第一组属性处理包含在训练和测试文件中的特性的数量。因为我们使用了三个值,所以指定了三个realValued
列。trainFile
和testFile
属性指定了各自文件的位置和名称:
useClassFeature=true
1.realValued=true
2.realValued=true
3.realValued=true
trainFile=.box.train
testFile=.box.test
培训和测试文件使用相同的格式。每一行由一个类别和其后的定义值组成,每一行由一个制表符分隔。box.train
训练文件包含 60 个条目,而box.test
文件包含 30 个条目。这些文件可以从github . com/packt publishing/Natural-Language-Processing-with-Java-Second-Edition/
或者从 GitHub 资源库下载。下面的代码显示了box.train
文件的第一行。品类小;其高度、宽度和长度分别为2.34
、1.60
和1.50
:
small 2.34 1.60 1.50
创建分类器的代码如下面的代码所示。使用属性文件作为构造函数的参数创建了一个ColumnDataClassifier
类的实例。Classifier
接口的一个实例由makeClassifier
方法返回。这个接口支持三种方法,我们将演示其中的两种。readTrainingExamples
方法从训练文件中读取训练数据:
ColumnDataClassifier cdc =
new ColumnDataClassifier("box.prop");
Classifier<String, String> classifier =
cdc.makeClassifier(cdc.readTrainingExamples("box.train"));
当执行时,我们得到大量的输出。我们将在这一部分讨论更相关的部分。输出的第一部分重复了属性文件的各个部分:
3.realValued = true
testFile = .box.test
...
trainFile = .box.train
下一部分显示数据集的数量,以及关于各种要素的信息,如下所示:
Reading dataset from box.train ... done [0.1s, 60 items].
numDatums: 60
numLabels: 3 [small, medium, large]
...
AVEIMPROVE The average improvement / current value
EVALSCORE The last available eval score
Iter ## evals ## <SCALING> [LINESEARCH] VALUE TIME |GNORM| {RELNORM} AVEIMPROVE EVALSCORE
然后,分类器对数据进行迭代以创建分类器:
Iter 1 evals 1 <D> [113M 3.107E-4] 5.985E1 0.00s |3.829E1| {1.959E-1} 0.000E0 -
Iter 2 evals 5 <D> [M 1.000E0] 5.949E1 0.01s |1.862E1| {9.525E-2} 3.058E-3 -
Iter 3 evals 6 <D> [M 1.000E0] 5.923E1 0.01s |1.741E1| {8.904E-2} 3.485E-3 -
...
Iter 21 evals 24 <D> [1M 2.850E-1] 3.306E1 0.02s |4.149E-1| {2.122E-3} 1.775E-4 -
Iter 22 evals 26 <D> [M 1.000E0] 3.306E1 0.02s
QNMinimizer terminated due to average improvement: | newest_val - previous_val | / |newestVal| < TOL
Total time spent in optimization: 0.07s
此时,分类器就可以使用了。接下来,我们使用测试文件来验证分类器。我们从使用ObjectBank
类的getLineIterator
方法从文本文件中获取一行开始。这个类支持将读入的数据转换成更标准化的形式。getLineIterator
方法以分类器可以使用的格式一次返回一行。此流程的循环如下所示:
for (String line :
ObjectBank.getLineIterator("box.test", "utf-8")) {
...
}
在 for-each 语句中,从该行创建一个Datum
实例,然后使用它的classOf
方法返回预测的类别,如下面的代码所示。Datum
接口支持包含特性的对象。当用作classOf
方法的参数时,由分类器确定的类别被返回:
Datum<String, String> datum = cdc.makeDatumFromLine(line);
System.out.println("Datum: {"
+ line + "]\tPredicted Category: "
+ classifier.classOf(datum));
当执行这个序列时,测试文件的每一行都被处理,并且显示预测的类别,如下面的代码所示。这里只显示了前两行和后两行。分类器能够正确分类所有测试数据:
Datum: {small 1.33 3.50 5.43] Predicted Category: medium
Datum: {small 1.18 1.73 3.14] Predicted Category: small
...
Datum: {large 6.01 9.35 16.64] Predicted Category: large
Datum: {large 6.76 9.66 15.44] Predicted Category: large
为了测试一个单独的条目,我们可以使用makeDatumFromStrings
方法来创建一个Datum
实例。在下面的代码序列中,创建了一个一维字符串数组,其中每个元素表示一个框的数据值。第一个条目“类别”为空。然后,Datum
实例被用作classOf
方法的参数来预测它的类别:
String sample[] = {"", "6.90", "9.8", "15.69"};
Datum<String, String> datum =
cdc.makeDatumFromStrings(sample);
System.out.println("Category: " + classifier.classOf(datum));
此处显示了该序列的输出。它正确地对盒子进行分类:
Category: large
使用斯坦福管道进行情感分析
在本节中,我们将说明如何使用斯坦福 API 来执行情感分析。我们将使用StanfordCoreNLP
管道对不同的文本进行分析。
我们将使用三种不同的文本,如下面的代码中所定义的。review
字符串是来自烂番茄(www.rottentomatoes.com/m/forrest_gump/
)的关于电影阿甘正传的影评:
String review = "An overly sentimental film with a somewhat "
+ "problematic message, but its sweetness and charm "
+ "are occasionally enough to approximate true depth "
+ "and grace. ";
String sam = "Sam was an odd sort of fellow. Not prone "
+ "to angry and not prone to merriment. Overall, "
+ "an odd fellow.";
String mary = "Mary thought that custard pie was the "
+ "best pie in the world. However, she loathed "
+ "chocolate pie.";
为了执行这个分析,我们需要使用一个情感annotator
,如下面的代码所示。这也需要使用tokenize
、ssplit
和parse
标注器。parse
注释器提供了更多关于文本的结构信息,这将在第十章、使用解析器提取关系中详细讨论:
Properties props = new Properties();
props.put("annotators", "tokenize, ssplit, parse, sentiment");
StanfordCoreNLP pipeline = new StanfordCoreNLP(props);
该文本用于创建一个Annotation
实例,然后用作执行实际工作的annotate
方法的参数,如下所示:
Annotation annotation = new Annotation(review);
pipeline.annotate(annotation);
下面的数组包含不同可能情绪的字符串:
String[] sentimentText = {"Very Negative", "Negative",
"Neutral", "Positive", "Very Positive"};
Annotation
类的get
方法返回一个实现CoreMap
接口的对象。在这种情况下,这些对象表示将输入文本拆分成句子的结果,如下面的代码所示。对于每个句子,获得一个代表树形结构的Tree
对象的实例,该树形结构包含对情感文本的解析。getPredictedClass
方法向sentimentText
数组返回一个索引,反映测试的情绪:
for (CoreMap sentence : annotation.get(
CoreAnnotations.SentencesAnnotation.class)) {
Tree tree = sentence.get(
SentimentCoreAnnotations.AnnotatedTree.class);
int score = RNNCoreAnnotations.getPredictedClass(tree);
System.out.println(sentimentText[score]);
}
当使用review
字符串执行代码时,我们得到以下输出:
Positive
这篇课文由三个句子组成。每个的输出如下,显示了每个句子的情感:
Neutral
Negative
Neutral
正文mary
由两个句子组成。每个的输出如下:
Positive
Neutral
使用 LingPipe 对文本进行分类
在本节中,我们将使用 LingPipe 来演示一些分类任务,包括使用训练好的模型进行一般的文本分类、情感分析和语言识别。我们将涵盖以下分类主题:
- 使用
Classified
类训练文本 - 使用其他培训类别的培训模型
- 使用 LingPipe 分类文本
- 使用 LingPipe 执行情感分析
- 识别使用的语言
本节中描述的几个任务将使用以下声明。LingPipe 附带了几个类别的训练数据。categories
数组包含用 LingPipe 打包的类别的名称:
String[] categories = {"soc.religion.christian",
"talk.religion.misc","alt.atheism","misc.forsale"};
DynamicLMClassifier
类用于执行实际的分类。它是使用categories
数组创建的,为它提供要使用的类别的名称。nGramSize
值指定序列中连续项目的数量,这些项目在模型中用于分类目的:
int nGramSize = 6;
DynamicLMClassifier<NGramProcessLM> classifier =
DynamicLMClassifier.createNGramProcess(
categories, nGramSize);
使用分类类训练文本
使用 LingPipe 的一般文本分类包括使用训练文件训练DynamicLMClassifier
类,然后使用该类执行实际的分类。LingPipe 附带了几个训练数据集,可以在名为demos/data/fourNewsGroups/4news-train
的 LingPipe 目录中找到。我们将使用这些数据集来说明培训过程。这个例子是在alias-I . com/lingpipe/demos/tutorial/classify/read-me . html
找到的过程的简化版本。
我们首先声明trainingDirectory
:
String directory = ".../demos";
File trainingDirectory = new File(directory
+ "/data/fourNewsGroups/4news-train");
在trainingDirectory
中,有四个子目录,它们的名称列在categories
数组中。在每个子目录中,都有一系列带有数字名称的文件。这些文件包含处理子目录名称的新闻组(【http://qwone.com/~jason/20Newsgroups/】??)数据。
训练模型的过程包括通过DynamicLMClassifier
类的handle
方法使用每个文件和类别。该方法将使用该文件为该类别创建一个训练实例,然后用该实例扩充模型。这个过程使用嵌套的for
循环。
外部的for
循环使用目录名创建一个File
对象,然后对其应用list
方法。list
方法返回目录中的文件列表。这些文件的名称存储在trainingFiles
数组中,将在内部for
循环中使用:
for (int i = 0; i < categories.length; ++i) {
File classDir =
new File(trainingDirectory, categories[i]);
String[] trainingFiles = classDir.list();
// Inner for-loop
}
内部的for
循环,如下面的代码所示,将打开每个文件并从文件中读取文本。Classification
类表示具有指定类别的分类。它与文本一起用来创建一个Classified
实例。DynamicLMClassifier
类的handle
方法用新信息更新模型:
for (int j = 0; j < trainingFiles.length; ++j) {
try {
File file = new File(classDir, trainingFiles[j]);
String text = Files.readFromFile(file, "ISO-8859-1");
Classification classification =
new Classification(categories[i]);
Classified<CharSequence> classified =
new Classified<>(text, classification);
classifier.handle(classified);
} catch (IOException ex) {
// Handle exceptions
}
}
You can alternatively use the com.aliasi.util.Files
class instead in java.io.File
; otherwise, the readFromFile
method will not be available.
该分类器可以序列化以备后用,如下面的代码所示。AbstractExternalizable
类是一个支持对象序列化的实用类。它有一个静态的compileTo
方法,接受一个Compilable
实例和一个File
对象。它将对象写入文件,如下所示:
try {
AbstractExternalizable.compileTo( (Compilable) classifier,
new File("classifier.model"));
} catch (IOException ex) {
// Handle exceptions
}
分类器的加载将在本章后面的使用 LingPipe 对文本进行分类部分进行说明。
使用其他培训类别
其他新闻组数据可以在 http://qwone.com/~jason/20Newsgroups/找到。这些数据集合可用于为其他模型定型,如下表所列。虽然只有 20 个类别,但它们可以成为有用的培训模型。有三种不同的下载方式。有些已经过排序,有些则删除了重复数据:
| 新闻组 |
| comp.graphics
| sci.crypt
|
| comp.os.ms-windows.misc
| sci.electronics
|
| comp.sys.ibm.pc.hardware
| sci.med
|
| comp.sys.mac.hardware
| sci.space
|
| comp.windows.x
| misc.forsale
|
| rec.autos
| talk.politics.misc
|
| rec.motoXrcycles
| talk.politics.guns
|
| rec.sport.baseball
| talk.politics.mideast
|
| rec.sport.hockey
| talk.religion.misc
|
| alt.atheism
| |
使用 LingPipe 分类文本
为了对文本进行分类,我们将使用DynamicLMClassifier
类的classify
方法。我们将用两个不同的文本序列来演示它的用法:
forSale
:这是来自www.homes.com/for-sale/
,在这里我们使用了第一个完整的句子martinLuther
:这是来自en.wikipedia.org/wiki/Martin_Luther
,我们用的是第二段的第一句话
这些字符串在此处声明:
String forSale =
"Finding a home for sale has never been "
+ "easier. With Homes.com, you can search new "
+ "homes, foreclosures, multi-family homes, "
+ "as well as condos and townhouses for sale. "
+ "You can even search our real estate agent "
+ "directory to work with a professional "
+ "Realtor and find your perfect home.";
String martinLuther =
"Luther taught that salvation and subsequently "
+ "eternity in heaven is not earned by good deeds "
+ "but is received only as a free gift of God's "
+ "grace through faith in Jesus Christ as redeemer "
+ "from sin and subsequently eternity in Hell.";
要重用前一节中序列化的分类器,请使用AbstractExternalizable
类的readObject
方法,如以下代码所示。我们将使用LMClassifier
类而不是DynamicLMClassifier
类。它们都支持classify
方法,但是DynamicLMClassifier
类不容易序列化:
LMClassifier classifier = null;
try {
classifier = (LMClassifier)
AbstractExternalizable.readObject(
new File("classifier.model"));
} catch (IOException | ClassNotFoundException ex) {
// Handle exceptions
}
在下面的代码序列中,我们将应用LMClassifier
类的classify
方法。这将返回一个JointClassification
实例,我们用它来确定最佳匹配:
JointClassification classification =
classifier.classify(text);
System.out.println("Text: " + text);
String bestCategory = classification.bestCategory();
System.out.println("Best Category: " + bestCategory);
对于forSale
文本,我们得到以下输出:
Text: Finding a home for sale has never been easier. With Homes.com, you can search new homes, foreclosures, multi-family homes, as well as condos and townhouses for sale. You can even search our real estate agent directory to work with a professional Realtor and find your perfect home.
Best Category: misc.forsale
对于martinLuther
文本,我们得到以下输出:
Text: Luther taught that salvation and subsequently eternity in heaven is not earned by good deeds but is received only as a free gift of God's grace through faith in Jesus Christ as redeemer from sin and subsequently eternity in Hell.
Best Category: soc.religion.christian
他们都对文本进行了正确的分类。
使用 LingPipe 进行情感分析
情感分析的执行方式与普通文本分类非常相似。一个区别是,它只使用两个类别:积极和消极。
我们需要使用数据文件来训练我们的模型。我们将通过使用为电影开发的情感数据(www . cs . Cornell . edu/people/pabo/movie-review-data/review _ polarity . tar . gz
)来使用在alias-I . com/ling pipe/demos/tutorial/sensation/read-me . html
执行的情感分析的简化版本。这些数据是从 IMDb 电影档案馆中的 1000 条正面和 1000 条负面电影评论中得出的。
这些评论需要下载和提取。将提取一个txt_sentoken
目录及其两个子目录:neg
和pos
。这两个子目录都包含电影评论。尽管这些文件中的一部分可以保留以评估所创建的模型,但是我们将使用所有这些文件来简化解释。
我们将从使用 LingPipe 对文本部分进行分类的中声明的变量的重新初始化开始。categories
数组被设置为两个元素的数组来保存这两个类别。使用新的类别数组和大小为8
的nGramSize
给classifier
变量分配一个新的DynamicLMClassifier
实例:
categories = new String[2];
categories[0] = "neg";
categories[1] = "pos";
nGramSize = 8;
classifier = DynamicLMClassifier.createNGramProcess(
categories, nGramSize);
正如我们前面所做的,我们将基于培训文件中的内容创建一系列实例。我们不会详细检查下面的代码,因为它与使用分类类部分的培训文本中的代码非常相似。主要区别在于只需要处理两个类别:
String directory = "...";
File trainingDirectory = new File(directory, "txt_sentoken");
for (int i = 0; i < categories.length; ++i) {
Classification classification =
new Classification(categories[i]);
File file = new File(trainingDirectory, categories[i]);
File[] trainingFiles = file.listFiles();
for (int j = 0; j < trainingFiles.length; ++j) {
try {
String review = Files.readFromFile(
trainingFiles[j], "ISO-8859-1");
Classified<CharSequence> classified =
new Classified<>(review, classification);
classifier.handle(classified);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
该模型现在可以使用了。我们将使用电影阿甘正传的评论:
String review = "An overly sentimental film with a somewhat "
+ "problematic message, but its sweetness and charm "
+ "are occasionally enough to approximate true depth "
+ "and grace. ";
我们使用classify
方法来执行实际工作。它返回一个Classification
实例,其bestCategory
方法返回最佳类别,如下所示:
Classification classification = classifier.classify(review);
String bestCategory = classification.bestCategory();
System.out.println("Best Category: " + bestCategory);
执行时,我们得到以下输出:
Best Category: pos
这种方法也适用于其他类别的文本。
使用 LingPipe 的语言识别
LingPipe 附带了一个名为langid-leipzig.classifier
的模型,它针对几种语言进行了训练,可以在demos/models
目录中找到。下表包含支持的语言列表。这个模型是使用从莱比锡语料库(【http://corpora.uni-leipzig.de/】)中获得的训练数据开发的。另一个好工具可以在 http://code.google.com/p/language-detection/找到:
| 语言 | 缩写 | 语言 | 缩写 |
| 加泰罗尼亚语 | 猫 | 意大利的 | 它 |
| 丹麦的 | 男高中生 | 日本人 | 治安官 |
| 英语 | 在中 | 韩国的 | 韩国 |
| 爱沙尼亚语 | 电子工程师 | 挪威的 | 不 |
| 芬兰人的 | 船方不负担装货费用 | 索布人的 | 吸收 |
| 法语 | 神父 | 瑞典的 | 如果 |
| 德国人 | (加在动词之前)表示“否定”,“相反”;(加在名词之前构成动词)表示“除去”,“除掉” | 土耳其的 | tr |
为了使用这个模型,我们基本上使用了本章前面的使用 LingPipe 对文本进行分类一节中使用的相同代码。我们从《阿甘正传》(??)的同一个电影评论开始:
String text = "An overly sentimental film with a somewhat "
+ "problematic message, but its sweetness and charm "
+ "are occasionally enough to approximate true depth "
+ "and grace. ";
System.out.println("Text: " + text);
使用langid-leipzig.classifier
文件创建LMClassifier
实例:
LMClassifier classifier = null;
try {
classifier = (LMClassifier)
AbstractExternalizable.readObject(
new File(".../langid-leipzig.classifier"));
} catch (IOException | ClassNotFoundException ex) {
// Handle exceptions
}
使用classify
方法,然后应用bestCategory
方法,以获得最佳的语言匹配,如下所示:
Classification classification = classifier.classify(text);
String bestCategory = classification.bestCategory();
System.out.println("Best Language: " + bestCategory);
输出如下,选择英语作为语言:
Text: An overly sentimental film with a somewhat problematic message, but its sweetness and charm are occasionally enough to approximate true depth and grace.
Best Language: en
以下代码示例使用瑞典语的瑞典语维基百科条目的第一句(sv.wikipedia.org/wiki/Svenska
)作为文本:
text = "Svenska är ett östnordiskt språk som talas av cirka "
+ "tio miljoner personer[1], främst i Finland "
+ "och Sverige.";
如此处所示,输出正确选择了瑞典语:
Text: Svenska är ett östnordiskt språk som talas av cirka tio miljoner personer[1], främst i Finland och Sverige.
Best Language: se
训练可以使用我们在以前的 LingPipe 模型中使用的相同方法进行。执行语言识别时的另一个考虑是文本可以用多种语言书写。这可能会使语言检测过程变得复杂。
摘要
在这一章中,我们讨论了围绕文本分类的问题,并研究了执行这一过程的几种方法。文本的分类对于许多活动都是有用的,例如检测垃圾电子邮件、确定文档的作者、执行性别识别以及执行语言识别。
我们还演示了如何执行情感分析。这种分析关注的是确定一篇文章在本质上是积极的还是消极的。还可以使用该过程来评估其他情感属性。
我们使用的大多数方法都要求我们首先基于训练数据创建一个模型。通常,这个模型需要用一组测试数据来验证。一旦模型被创建,它通常很容易使用。
在下一章中,第九章,主题建模我们将研究解析过程以及它如何有助于从文本中提取关系。