1. 概述

Apache OpenNLP 是一个开源的自然语言处理 Java 库。它提供了一系列用于处理常见 NLP 任务的 API,包括命名实体识别、句子检测、词性标注和分词等功能。

本教程将带你了解如何使用这个 API 实现不同的 NLP 任务。

2. Maven 配置

首先,我们需要在 pom.xml 中添加核心依赖:

<dependency>
    <groupId>org.apache.opennlp</groupId>
    <artifactId>opennlp-tools</artifactId>
    <version>1.8.4</version>
</dependency>

最新稳定版本可在 Maven Central 获取。

部分功能需要预训练模型,你可以从这里下载预定义模型,详细信息参考官方文档

3. 句子检测

我们先从基础功能开始:理解什么是句子。

句子检测的核心任务是识别句子的起始和结束位置,这通常取决于目标语言的特性。这个过程也称为"句子边界消歧"(Sentence Boundary Disambiguation,SBD)。

⚠️ 实际应用中的挑战:由于句点(.)的多义性,检测变得复杂。它可能表示句子结束,也可能出现在邮箱、缩写、小数等场景中。

与大多数 NLP 任务一样,我们需要预训练模型(假设存放在 /resources 目录):

@Test
public void givenEnglishModel_whenDetect_thenSentencesAreDetected() 
  throws Exception {

    String paragraph = "This is a statement. This is another statement." 
      + "Now is an abstract word for time, "
      + "that is always flying. And my email address is [email protected].";

    InputStream is = getClass().getResourceAsStream("/models/en-sent.bin");
    SentenceModel model = new SentenceModel(is);

    SentenceDetectorME sdetector = new SentenceDetectorME(model);

    String sentences[] = sdetector.sentDetect(paragraph);
    assertThat(sentences).contains(
      "This is a statement.",
      "This is another statement.",
      "Now is an abstract word for time, that is always flying.",
      "And my email address is [email protected].");
}

💡 "ME" 后缀表示基于"最大熵"(Maximum Entropy)的算法实现。

4. 分词处理

现在我们能将文本分割成句子,接下来深入分析句子结构。

分词的目标是将句子拆解为更小的单元(称为词元)。这些词元通常是单词、数字或标点符号。

OpenNLP 提供三种分词器,各有适用场景:

4.1. 使用 TokenizerME

需要预训练模型(从这里下载):

@Test
public void givenEnglishModel_whenTokenize_thenTokensAreDetected() 
  throws Exception {
 
    InputStream inputStream = getClass()
      .getResourceAsStream("/models/en-token.bin");
    TokenizerModel model = new TokenizerModel(inputStream);
    TokenizerME tokenizer = new TokenizerME(model);
    String[] tokens = tokenizer.tokenize("Baeldung is a Spring Resource.");
 
    assertThat(tokens).contains(
      "Baeldung", "is", "a", "Spring", "Resource", ".");
}

✅ 优势:能准确识别单词和标点符号(如句点)

4.2. WhitespaceTokenizer

简单粗暴的空白字符分割:

@Test
public void givenWhitespaceTokenizer_whenTokenize_thenTokensAreDetected() 
  throws Exception {
 
    WhitespaceTokenizer tokenizer = WhitespaceTokenizer.INSTANCE;
    String[] tokens = tokenizer.tokenize("Baeldung is a Spring Resource.");
 
    assertThat(tokens)
      .contains("Baeldung", "is", "a", "Spring", "Resource.");
  }

❌ 局限:无法处理标点符号粘连(如 "Resource." 会被视为整体)

4.3. SimpleTokenizer

更智能的默认分词器(无需模型):

@Test
public void givenSimpleTokenizer_whenTokenize_thenTokensAreDetected() 
  throws Exception {
 
    SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE;
    String[] tokens = tokenizer
      .tokenize("Baeldung is a Spring Resource.");
 
    assertThat(tokens)
      .contains("Baeldung", "is", "a", "Spring", "Resource", ".");
  }

✅ 特点:自动识别单词、数字和标点符号

5. 命名实体识别(NER)

基于分词结果,我们可以实现更高级的功能:识别文本中的特定实体。

NER 的目标是定位文本中的命名实体,如人名、地点、组织机构等。

OpenNLP 提供预训练模型支持多种实体类型:

@Test
public void 
  givenEnglishPersonModel_whenNER_thenPersonsAreDetected() 
  throws Exception {

    SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE;
    String[] tokens = tokenizer
      .tokenize("John is 26 years old. His best friend's "  
        + "name is Leonard. He has a sister named Penny.");

    InputStream inputStreamNameFinder = getClass()
      .getResourceAsStream("/models/en-ner-person.bin");
    TokenNameFinderModel model = new TokenNameFinderModel(
      inputStreamNameFinder);
    NameFinderME nameFinderME = new NameFinderME(model);
    List<Span> spans = Arrays.asList(nameFinderME.find(tokens));

    assertThat(spans.toString())
      .isEqualTo("[[0..1) person, [13..14) person, [20..21) person]");
}

输出结果为 Span 对象列表,标识实体在词元数组中的位置范围。

6. 词性标注(POS)

另一个依赖分词结果的核心功能是词性标注。

词性标注用于识别单词的语法类型,OpenNLP 使用宾夕法尼亚树库标签体系:

标签 含义 示例
NN 名词(单数/不可数) book
DT 限定词 the, a
VB 动词原形 go
VBD 动词过去式 went
VBZ 动词第三人称单数 goes
IN 介词/从属连词 in, of
NNP 专有名词 John
TO 不定式标记 to
JJ 形容词 good

完整标签列表参考宾夕法尼亚树库文档

实现代码:

@Test
public void givenPOSModel_whenPOSTagging_thenPOSAreDetected() 
  throws Exception {
 
    SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE;
    String[] tokens = tokenizer.tokenize("John has a sister named Penny.");

    InputStream inputStreamPOSTagger = getClass()
      .getResourceAsStream("/models/en-pos-maxent.bin");
    POSModel posModel = new POSModel(inputStreamPOSTagger);
    POSTaggerME posTagger = new POSTaggerME(posModel);
    String tags[] = posTagger.tag(tokens);
 
    assertThat(tags).contains("NNP", "VBZ", "DT", "NN", "VBN", "NNP", ".");
}

标注结果示例:

  1. "John" → NNP(专有名词)
  2. "has" → VBZ(动词)
  3. "a" → DT(限定词)
  4. "sister" → NN(名词)
  5. "named" → VBN(动词过去分词)
  6. "Penny" → NNP(专有名词)
  7. "." → 句点

7. 词形还原(Lemmatization)

基于词性标注结果,我们可以进行更深层的文本分析。

词形还原将变形词(含时态/性别等)还原为词典基本形式,例如:

  • "running" → "run"
  • "better" → "good"

OpenNLP 提供两种实现方式:

类型 特点 适用场景
统计型 需要训练模型 处理未见过的词汇
字典型 依赖预定义字典(示例 高精度处理已知词汇

字典型实现示例:

@Test
public void givenEnglishDictionary_whenLemmatize_thenLemmasAreDetected() 
  throws Exception {

    SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE;
    String[] tokens = tokenizer.tokenize("John has a sister named Penny.");

    InputStream inputStreamPOSTagger = getClass()
      .getResourceAsStream("/models/en-pos-maxent.bin");
    POSModel posModel = new POSModel(inputStreamPOSTagger);
    POSTaggerME posTagger = new POSTaggerME(posModel);
    String tags[] = posTagger.tag(tokens);
    InputStream dictLemmatizer = getClass()
      .getResourceAsStream("/models/en-lemmatizer.dict");
    DictionaryLemmatizer lemmatizer = new DictionaryLemmatizer(
      dictLemmatizer);
    String[] lemmas = lemmatizer.lemmatize(tokens, tags);

    assertThat(lemmas)
      .contains("O", "have", "a", "sister", "name", "O", "O");
}

结果解读:

  • "has" → "have"(动词还原)
  • "named" → "name"(过去分词还原)
  • "O" 表示专有名词(如 John/Penny)无法还原

8. 组块分析(Chunking)

词性标注的另一个重要应用是组块分析。

组块分析将句子按语法结构分组,形成名词短语、动词短语等有意义的单元。

实现流程:

  1. 分词 → 2. 词性标注 → 3. 组块分析
@Test
public void 
  givenChunkerModel_whenChunk_thenChunksAreDetected() 
  throws Exception {

    SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE;
    String[] tokens = tokenizer.tokenize("He reckons the current account 
      deficit will narrow to only 8 billion.");

    InputStream inputStreamPOSTagger = getClass()
      .getResourceAsStream("/models/en-pos-maxent.bin");
    POSModel posModel = new POSModel(inputStreamPOSTagger);
    POSTaggerME posTagger = new POSTaggerME(posModel);
    String tags[] = posTagger.tag(tokens);

    InputStream inputStreamChunker = getClass()
      .getResourceAsStream("/models/en-chunker.bin");
    ChunkerModel chunkerModel
     = new ChunkerModel(inputStreamChunker);
    ChunkerME chunker = new ChunkerME(chunkerModel);
    String[] chunks = chunker.chunk(tokens, tags);
    assertThat(chunks).contains(
      "B-NP", "B-VP", "B-NP", "I-NP", 
      "I-NP", "I-NP", "B-VP", "I-VP", 
      "B-PP", "B-NP", "I-NP", "I-NP", "O");
}

输出标签说明:

  • B-:组块开始(如 B-NP 表示名词短语开始)
  • I-:组块延续(如 I-NP 表示名词短语延续)
  • O:不属于任何组块

示例句子组块结果:

  1. "He" → 名词短语
  2. "reckons" → 动词短语
  3. "the current account deficit" → 名词短语
  4. "will narrow" → 动词短语
  5. "to" → 介词短语
  6. "only 8 billion" → 名词短语

9. 语言检测

OpenNLP 还提供文本语言识别功能,适用于多语言场景处理。

实现步骤:

  1. 准备训练数据(示例文件
  2. 训练语言检测模型
  3. 预测文本语言
@Test
public void 
  givenLanguageDictionary_whenLanguageDetect_thenLanguageIsDetected() 
  throws FileNotFoundException, IOException {
 
    InputStreamFactory dataIn
     = new MarkableFileInputStreamFactory(
       new File("src/main/resources/models/DoccatSample.txt"));
    ObjectStream lineStream = new PlainTextByLineStream(dataIn, "UTF-8");
    LanguageDetectorSampleStream sampleStream
     = new LanguageDetectorSampleStream(lineStream);
    TrainingParameters params = new TrainingParameters();
    params.put(TrainingParameters.ITERATIONS_PARAM, 100);
    params.put(TrainingParameters.CUTOFF_PARAM, 5);
    params.put("DataIndexer", "TwoPass");
    params.put(TrainingParameters.ALGORITHM_PARAM, "NAIVEBAYES");

    LanguageDetectorModel model = LanguageDetectorME
      .train(sampleStream, params, new LanguageDetectorFactory());

    LanguageDetector ld = new LanguageDetectorME(model);
    Language[] languages = ld
      .predictLanguages("estava em uma marcenaria na Rua Bruno");
    assertThat(Arrays.asList(languages))
      .extracting("lang", "confidence")
      .contains(
        tuple("pob", 0.9999999950605625),
        tuple("ita", 4.939427661577956E-9), 
        tuple("spa", 9.665954064665144E-15),
        tuple("fra", 8.250349924885834E-25)));
}

输出结果包含:

  • 语言代码(如 "pob" 表示巴西葡萄牙语)
  • 置信度分数(越接近 1 越可靠)

💡 使用高质量训练数据时,检测准确率可达 99%+

10. 总结

我们系统探索了 OpenNLP 的核心功能:

  • ✅ 基础处理:句子检测、分词
  • ✅ 高级分析:命名实体识别、词性标注
  • ✅ 深度处理:词形还原、组块分析
  • ✅ 实用工具:语言检测

这些功能为构建复杂的 NLP 应用提供了坚实基础。完整实现代码可在 GitHub 获取。


原始标题:Intro to Apache OpenNLP | Baeldung