Apache OpenNLPの紹介
1. 概要
Apache OpenNLPは、オープンソースの自然言語処理Javaライブラリです。
名前付きエンティティ認識、文検出、POSタグ付け、トークン化などのユースケース向けのAPIを備えています。
このチュートリアルでは、さまざまなユースケースでこのAPIを使用する方法を見ていきます。
2. Mavenセットアップ
まず、主な依存関係をpom.xmlに追加する必要があります。
org.apache.opennlp
opennlp-tools
1.8.4
最新の安定バージョンはMaven Centralにあります。
3. 文検出
文が何であるかを理解することから始めましょう。
Sentence detection is about identifying the start and the end of a sentence。これは通常、手元の言語によって異なります。 これは「文境界曖昧性除去」(SBD)とも呼ばれます。
場合によっては、sentence detection is quite challenging because of the ambiguous nature of the period character。 通常、ピリオドは文の終わりを示しますが、電子メールアドレス、略語、小数、および他の多くの場所にも表示できます。
ほとんどのNLPタスクと同様に、文の検出には、入力としてトレーニング済みのモデルが必要です。これは、/resourcesフォルダーにあると予想されます。
文の検出を実装するために、モデルをロードして、SentenceDetectorMEのインスタンスに渡します。 次に、テキストをsentDetect()メソッドに渡して、文の境界で分割します。
@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」は、Apache OpenNLPの多くのクラス名で使用され、「最大エントロピー」に基づくアルゴリズムを表します。
4. トークン化
テキストのコーパスをセンテンスに分割できるようになったので、センテンスの詳細な分析を開始できます。
The goal of tokenization is to divide a sentence into smaller parts called tokens。 通常、これらのトークンは単語、数字、または句読点です。
OpenNLPで使用できるトークナイザーには3つのタイプがあります。
4.1. TokenizerMEの使用
この場合、最初にモデルをロードする必要があります。 モデルファイルをhereからダウンロードし、/resourcesフォルダーに入れて、そこからロードできます。
次に、ロードされたモデルを使用してTokenizerMEのインスタンスを作成し、tokenize()メソッドを使用して任意のString:でトークン化を実行します
@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("example is a Spring Resource.");
assertThat(tokens).contains(
"example", "is", "a", "Spring", "Resource", ".");
}
ご覧のとおり、トークナイザーはすべての単語とピリオド文字を個別のトークンとして識別しました。 このトークナイザーは、カスタムのトレーニング済みモデルでも使用できます。
4.2. WhitespaceTokenizer
名前が示すように、このトークナイザーは、区切り文字として空白文字を使用して、文をトークンに単純に分割します。
@Test
public void givenWhitespaceTokenizer_whenTokenize_thenTokensAreDetected()
throws Exception {
WhitespaceTokenizer tokenizer = WhitespaceTokenizer.INSTANCE;
String[] tokens = tokenizer.tokenize("example is a Spring Resource.");
assertThat(tokens)
.contains("example", "is", "a", "Spring", "Resource.");
}
文が空白で分割されていることがわかります。そのため、「Resource」という単語とピリオド文字の2つの異なるトークンではなく、単一のトークンとして「Resource」(最後にピリオド文字)を取得します。
4.3. SimpleTokenizer
このトークナイザーはWhitespaceTokenizerよりも少し洗練されており、文を単語、数字、句読点に分割します。 これはデフォルトの動作であり、モデルは必要ありません。
@Test
public void givenSimpleTokenizer_whenTokenize_thenTokensAreDetected()
throws Exception {
SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE;
String[] tokens = tokenizer
.tokenize("example is a Spring Resource.");
assertThat(tokens)
.contains("example", "is", "a", "Spring", "Resource", ".");
}
5. 名前付きエンティティの認識
トークン化について理解したので、トークン化の成功に基づく最初のユースケースである名前付きエンティティ認識(NER)を見てみましょう。
NERの目標は、特定のテキスト内で人、場所、組織、その他の名前付きのものなどの名前付きエンティティを見つけることです。
OpenNLPは、個人名、日付と時刻、場所、および組織に事前定義されたモデルを使用します。 TokenNameFinderModelを使用してモデルをロードし、 をNameFinderME.のインスタンスに渡す必要があります。次に、find()メソッドを使用して、指定されたテキスト内の名前付きエンティティを検索できます。
@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 spans = Arrays.asList(nameFinderME.find(tokens));
assertThat(spans.toString())
.isEqualTo("[[person, [13..14) person, [20..21) person]");
}
アサーションでわかるように、結果は、テキスト内の名前付きエンティティを構成するトークンの開始インデックスと終了インデックスを含むSpanオブジェクトのリストになります。
6. 品詞タグ付け
入力としてトークンのリストを必要とする別のユースケースは、品詞タグ付けです。
A part-of-speech (POS) identifies the type of a word. OpenNLPは、さまざまな品詞に次のタグを使用します。
-
NN –名詞、単数または質量
-
DT –限定詞
-
VB –動詞、基本形
-
VBD –動詞、過去形
-
VBZ –動詞、三人称単数現在形
-
IN –の前置詞または従属接続詞
-
NNP –固有名詞、単数
-
TO –「to」という単語
-
JJ –形容詞
これらは、ペンツリーバンクで定義されているものと同じタグです。 完全なリストについては、this listを参照してください。
NERの例と同様に、適切なモデルを読み込んでから、トークンのセットでPOSTaggerMEとそのメソッドtag()を使用して、文にタグを付けます。
@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", ".");
}
tag()メソッドは、トークンをPOSタグのリストにマップします。 例の結果は次のとおりです。
-
「ジョン」– NNP(固有名詞)
-
「持っている」– VBZ(動詞)
-
“ a” – DT(決定要因)
-
「姉妹」– NN(名詞)
-
「名前付き」– VBZ(動詞)
-
「ペニー」– NNP(固有名詞)
-
「。」–ピリオド
7. 補題
これで、文にトークンの品詞情報が含まれたので、テキストをさらに分析できます。
時制、性別、気分、またはその他の情報を持つことができるLemmatization is the process of mapping a word formto the base form of the word – also called its “lemma”。
補題は、トークンとその品詞タグを入力として受け取り、単語の補題を返します。 したがって、補題化する前に、文をトークナイザーとPOSタガーに渡す必要があります。
Apache OpenNLPは、2種類の見出し語化を提供します。
-
Statistical –には、特定の単語の見出語を見つけるためのトレーニングデータを使用して構築された見出語モデルが必要です
-
Dictionary-based –には、単語、POSタグ、および対応する補題のすべての有効な組み合わせを含む辞書が必要です。
統計的レンマ化の場合、モデルをトレーニングする必要がありますが、辞書のレンマ化の場合、this one.のような辞書ファイルが必要です。
辞書ファイルを使用したコード例を見てみましょう。
@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");
}
ご覧のとおり、すべてのトークンの補題を取得します。 「O」は、単語が固有名詞であるため、補題を決定できなかったことを示します。 したがって、「ジョン」と「ペニー」の補題はありません。
しかし、文の他の単語の補題を特定しました。
-
持っている
-
a – a
-
姉妹–姉妹
-
名前付き–名前
8. チャンキング
品詞情報もチャンクに不可欠です。つまり、文を名詞グループや動詞グループのような文法的に意味のある単語グループに分割します。
前と同様に、chunk()メソッドを呼び出す前に、文をトークン化し、トークンに品詞タグを使用します。
@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」はチャンクの開始を表し、「I」はチャンクの継続を表し、「O」はチャンクがないことを表します。
この例の出力を解析すると、6つのチャンクが得られます。
-
「彼」–名詞句
-
「reckons」–動詞句
-
「当座預金赤字」–名詞句
-
「ウィルナロー」–動詞句
-
「to」–前置詞句
-
「わずか80億」–名詞句
9. 言語検出
すでに説明したユースケースに加えて、OpenNLP also provides a language detection API that allows to identify the language of a certain text.
言語の検出には、トレーニングデータファイルが必要です。 このようなファイルには、特定の言語の文を含む行が含まれています。 各行には、機械学習アルゴリズムへの入力を提供するための正しい言語のタグが付けられています。
言語検出用のサンプルトレーニングデータファイルは、hereでダウンロードできます。
トレーニングデータファイルをLanguageDetectorSampleStream,にロードして、いくつかのトレーニングデータパラメータを定義し、モデルを作成してから、そのモデルを使用してテキストの言語を検出できます。
@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)));
}
結果は、最も可能性の高い言語と信頼スコアのリストです。
そして、豊富なモデルにより、このタイプの検出で非常に高い精度を達成できます。
5. 結論
ここでは、OpenNLPの興味深い機能から多くのことを探りました。 見出し語化、POSタグ付け、トークン化、文章検出、言語検出などのNLPタスクを実行するいくつかの興味深い機能に注目しました。
いつものように、上記のすべての完全な実装はover on GitHubで見つけることができます。