本記事は、日本語の自然言語処理ライブラリである GiNZA の紹介記事です。 Qiitaの記事 と GiNZA examples - GitHub の二箇所に同じものを公開しています。
記事を書いた経緯
想定する読者
- 自然言語処理ってどんなことができるの?という初学者の方
- 筆者もまだまだ初学者ですが GiNZA は簡単にすごいことができるのでぜひ見ていってください。
- Pythonを学びたての方やこれから学ぼうとしている方
- Python学習のモチベーションになると思います。
- MeCab などの形態素解析器を使ったことはあるが GiNZA は初めて聞いたという方
- 簡単に比較できるものではありませんが新たに GiNZA を知る価値はあると思います。
GiNZA は 日本語の 自然言語処理ライブラリです。 もともと spaCy という自然言語処理のフレームワークがあり、英語など主要な言語に対応していました。 GiNZA は言わば spaCy の日本語対応版です。 詳細については GiNZAの公開ページ をご覧ください。
GiNZAを選ぶ理由
日本語の形態素解析器として有名なものに MeCab があります(形態素解析って何?という方は Web茶まめ ©国立国語研究所 にて実行してみてください)。 GiNZA も同様に日本語の文を分かち書きすることができます。単に日本語を分かち書きしたいだけなら MeCab の方が圧倒的に速いです。 それでも GiNZA には次のメリットがあると思います。
なお、決して MeCab をディスっているわけではないことを強調しておきます。状況や目的によって最適な選択は変わります。 MeCab はすでに長期間使用されており、高速というだけでなく高い信頼性、そして豊富な実践例があります。 また、最新の語彙に随時対応し続ける NEologd や、国立国語研究所が開発した UniDic を利用することができるのも MeCab のメリットだと思います。
ここで紹介するコードは GitHubホストランナーの仮想環境 のubuntu-latest, macos-latestとPython 3.6, 3.7, 3.8の組み合わせ(計6通り)で動作検証しています。
動作検証結果
オンライン
Pythonに親しみのない方や手っ取り早く動作環境がほしい方向けにオンラインの実行環境を用意しています。ブラウザで GiNZA examples - Repl.it を開いて Run
をクリックしてください。セットアップ完了までに5分程度かかります。
ローカル環境
git clone https://github.com/poyo46/ginza-examples.git
cd ginza-examples
poetry install
もし poetry
が未導入であれば $ pip install poetry
でインストールしてください。
ソースコードを開く
import sys
from typing import List
from pprint import pprint
import spacy
import ginza
nlp = spacy.load('ja_ginza')
def tokenize(text: str) -> List[List[str]]:
"""
日本語文を形態素解析する。
Parameters
----------
text : str
解析対象の日本語テキスト。
Returns
-------
List[List[str]]
形態素解析結果。
Notes
-----
* Token 属性の詳細については次のリンク先をご覧ください。
https://spacy.io/api/token#attributes
* Token.lemma_ の値は SudachiPy の Morpheme.dictionary_form() です。
* Token.ent_type_ の詳細については次のリンク先をご覧ください。
http://liat-aip.sakura.ne.jp/ene/ene8/definition_jp/html/enedetail.html
"""
doc = nlp(text)
attrs_list = []
for token in doc:
token_attrs = [
token.i, # トークン番号
token.text, # テキスト
token.lemma_, # 基本形
ginza.reading_form(token), # 読みカナ
token.pos_, # 品詞
token.tag_, # 品詞詳細
ginza.inflection(token), # 活用情報
token.ent_type_ # 固有表現
]
attrs_list.append([str(a) for a in token_attrs])
return attrs_list
EXAMPLE_TEXT = '田中部長に伝えてください。'
EXAMPLE_SCRIPT = f'python examples/token_information.py {EXAMPLE_TEXT}'
ATTRS = [
'i', 'text', 'lemma_', 'reading_form', 'pos_', 'tag_',
'inflection', 'ent_type_'
]
if __name__ == '__main__':
if len(sys.argv) > 1:
input_text = sys.argv[1]
pprint(tokenize(input_text))
else:
print('Please run as follows: \n$ ' + EXAMPLE_SCRIPT)
実行
python examples/token_information.py 田中部長に伝えてください。
結果
i | text | lemma_ | reading_form | pos_ | tag_ | inflection | ent_type_ |
---|---|---|---|---|---|---|---|
0 | 田中 | 田中 | タナカ | PROPN | 名詞-固有名詞-人名-姓 | Person | |
1 | 部長 | 部長 | ブチョウ | NOUN | 名詞-普通名詞-一般 | Position_Vocation | |
2 | に | に | ニ | ADP | 助詞-格助詞 | ||
3 | 伝え | 伝える | ツタエ | VERB | 動詞-一般 | 下一段-ア行,連用形-一般 | |
4 | て | て | テ | SCONJ | 助詞-接続助詞 | ||
5 | ください | くださる | クダサイ | AUX | 動詞-非自立可能 | 五段-ラ行,命令形 | |
6 | 。 | 。 | 。 | PUNCT | 補助記号-句点 |
※結果は見やすいように加工しています。
説明を開く
日本語の文を単語ごとに分け、各単語の解析結果を表示しています。
Token.pos_
は Universal Part-of-speech Tags と呼ばれるもので、言語に依存せず全世界的に単語の品詞を表そうというものです(Part-of-speech = 品詞)。
Token.ent_type_
は固有表現と呼ばれるもので、例えば人名には Person
が、料理名には Dish
が割り当てられます。詳細な定義については こちら をご覧ください。
Token
の他の属性については spaCy API doc をご覧ください。
応用 この解析結果を使って例えば次のようなことができます。
- 文中に含まれる単語から動詞と形容詞の原形を抽出する。
- 文中に含まれる食べ物を抽出してカウントする。
- 文中の個人情報をマスキングする。
ソースコードを開く
import sys
from typing import List
import spacy
nlp = spacy.load('ja_ginza')
def get_sentences(text: str) -> List[str]:
"""
文のリストに分割したテキストを取得する。
Parameters
----------
text : str
分割対象の日本語テキスト。
Returns
-------
List[str]
文のリスト。結合すると `text` に一致する。
See Also
--------
https://spacy.io/api/doc#sents
"""
doc = nlp(text)
sentences = [s.text for s in doc.sents]
return sentences
EXAMPLE_TEXT = 'はい、そうです。ありがとうございますよろしくお願いします。'
EXAMPLE_SCRIPT = f'python examples/split_text.py {EXAMPLE_TEXT}'
if __name__ == '__main__':
if len(sys.argv) > 1:
input_text = sys.argv[1]
print('\n'.join(get_sentences(input_text)))
else:
print('Please run as follows: \n$ ' + EXAMPLE_SCRIPT)
実行
python examples/split_text.py はい、そうです。ありがとうございますよろしくお願いします。
結果
はい、そうです。
ありがとうございます
よろしくお願いします。
ソースコードを開く
import sys
from pathlib import Path
import spacy
from spacy import displacy
nlp = spacy.load('ja_ginza')
def visualize(text: str) -> None:
"""
日本語文の文法的構造を解析し、可視化する。
Parameters
----------
text : str
解析対象の日本語テキスト。
Notes
-----
実行後、 ブラウザで http://localhost:5000 を開くと画像が表示される。
"""
doc = nlp(text)
displacy.serve(doc, style='dep')
def save_as_image(text: str, path) -> None:
"""
日本語文の文法的構造を解析し、結果を画像として保存する。
Parameters
----------
text : str
解析対象の日本語テキスト。
path
保存先のファイルパス。
Notes
-----
画像はSVG形式で保存される。
"""
doc = nlp(text)
svg = displacy.render(doc, style='dep')
with open(path, mode='w') as f:
f.write(svg)
EXAMPLE_TEXT = 'あのラーメン屋にはよく行く。'
EXAMPLE_SCRIPT = f'python examples/displacy.py {EXAMPLE_TEXT}'
if __name__ == '__main__':
if len(sys.argv) > 1:
input_text = sys.argv[1]
save_to = Path(__file__).with_name('displacy.svg')
save_as_image(input_text, save_to)
visualize(input_text)
else:
print('Please run as follows: \n$ ' + EXAMPLE_SCRIPT)
実行
python examples/displacy.py あのラーメン屋にはよく行く。
結果
Using the 'dep' visualizer
Serving on http://0.0.0.0:5000 ...
ブラウザで http://localhost:5000 を開くと解析結果が表示されます。同時に、サンプルコードでは画像を examples/displacy.svg に保存しています。
LexRankアルゴリズムを用いて抽出型要約を実行します。抽出型要約とは、元の文から重要な文を(無加工で)抽出するものです。サンプル文として 『走れメロス』 を用意しました。
ソースコードを開く
import sys
import re
from typing import Tuple, List
from pathlib import Path
import numpy as np
import spacy
from sumy.summarizers.lex_rank import LexRankSummarizer
nlp = spacy.load('ja_ginza')
def preprocess(text: str) -> str:
"""
要約の前処理を実施する。
* 全角スペースや括弧、改行を削除する。
* !?を。に置換する
Parameters
----------
text : str
日本語のテキスト。
Returns
-------
str
前処理が実施されたtext
"""
text = re.sub('[ 「」『』【】\r\n]', '', text)
text = re.sub('[!?]', '。', text)
text = text.strip()
return text
def lexrank_scoring(text: str) -> Tuple[List[str], np.ndarray]:
"""
LexRankアルゴリズムによって文に点数をつける。
この点数は文の重要度とみなすことができる。
Parameters
----------
text : str
分析対象のテキスト。
Returns
-------
List[str]
text を文のリストに分解したもの。
np.ndarray
文のリストに対応する重要度のリスト。
"""
doc = nlp(text)
# 文のリストと単語のリストをつくる
sentences = []
corpus = []
for sent in doc.sents:
sentences.append(sent.text)
tokens = []
for token in sent:
# 文に含まれる単語のうち, 名詞・副詞・形容詞・動詞に限定する
if token.pos_ in ('NOUN', 'ADV', 'ADJ', 'VERB'):
# ぶれをなくすため, 単語の見出し語 Token.lemma_ を使う
tokens.append(token.lemma_)
corpus.append(tokens)
# sentences = [文0, 文1, ...]
# corpus = [[文0の単語0, 文0の単語1, ...], [文1の単語0, 文1の単語1, ...], ...]
# sumyライブラリによるLexRankスコアリング
lxr = LexRankSummarizer()
tf_metrics = lxr._compute_tf(corpus)
idf_metrics = lxr._compute_idf(corpus)
matrix = lxr._create_matrix(corpus, lxr.threshold, tf_metrics, idf_metrics)
scores = lxr.power_method(matrix, lxr.epsilon)
# scores = [文0の重要度, 文1の重要度, ...]
return sentences, scores
def extract(sentences: List[str], scores: np.ndarray, n: int) -> List[str]:
"""
スコアの高い順にn個の文を抽出する。
Parameters
----------
sentences : List[str]
文のリスト。
scores : np.ndarray
スコアのリスト。
n : int
抽出する文の数。
Returns
-------
List[str]
sentencesから抽出されたn個の文のリスト。
"""
assert len(sentences) == len(scores)
# scoresのインデックスリスト
indices = range(len(scores))
# スコアの大きい順に並べ替えたリスト
sorted_indices = sorted(indices, key=lambda i: scores[i], reverse=True)
# スコアの大きい順からn個抽出したリスト
extracted_indices = sorted_indices[:n]
# インデックスの並び順をもとに戻す
extracted_indices.sort()
# 抽出されたインデックスに対応する文のリストを返す
return [sentences[i] for i in extracted_indices]
def get_summary(path, n) -> List[str]:
with open(path, mode='rt', encoding='utf-8') as f:
text = f.read()
text = preprocess(text)
sentences, scores = lexrank_scoring(text)
return extract(sentences, scores, n)
EXAMPLE_PATH = Path(__file__).with_name('data') / 'run_melos.txt'
N = 15
EXAMPLE_SCRIPT = f'python examples/lexrank_summary.py examples/data/run_melos.txt {N}'
if __name__ == '__main__':
if len(sys.argv) > 2:
input_path = sys.argv[1]
input_n = int(sys.argv[2])
extracted_sentences = get_summary(input_path, input_n)
print('\n'.join(extracted_sentences))
else:
print('Please run as follows: \n$ ' + EXAMPLE_SCRIPT)
実行
python examples/lexrank_summary.py examples/data/run_melos.txt 15
結果
人を、信ずる事が出来ぬ、というのです。
三日のうちに、私は村で結婚式を挙げさせ、必ず、ここへ帰って来ます。
そうして身代りの男を、三日目に殺してやるのも気味がいい。
ものも言いたくなくなった。
そうして、少し事情があるから、結婚式を明日にしてくれ、と頼んだ。
あれが沈んでしまわぬうちに、王城に行き着くことが出来なかったら、あの佳い友達が、私のために死ぬのです。
何をするのだ。
けれども、今になってみると、私は王の言うままになっている。
王は、ひとり合点して私を笑い、そうして事も無く私を放免するだろう。
私を、待っている人があるのだ。
死んでお詫び、などと気のいい事は言って居られぬ。
メロス。
その人を殺してはならぬ。
メロスが帰って来た。
メロスだ。
LexRankアルゴリズムによって抽出された、重要度の高い上位 15 文です。重要度のスコアは一度だけ計算すればよいため、抽出する文の数を変更したい場合は lexrank_scoring の結果を再利用すると速いです。
ソースコードを開く
import sys
from typing import List
from pprint import pprint
import spacy
import numpy as np
nlp = spacy.load('ja_ginza')
def similarity_matrix(texts: List[str]) -> List[List[np.float64]]:
"""
テキスト同士の類似度を計算する。
Parameters
----------
texts : str
日本語文のリスト。
Returns
-------
List[List[np.float64]]
文同士の類似度。
Notes
-----
spaCy の Doc.similarity (https://spacy.io/api/doc#similarity) を使っている。
"""
docs = [nlp(text) for text in texts]
rows = [[a.similarity(b) for a in docs] for b in docs]
return rows
EXAMPLE_TEXTS = [
'今日はとても良い天気です。',
'昨日の天気は大雨だったのに。',
'ラーメンを食べました。'
]
EXAMPLE_SCRIPT = f'python examples/similarity.py ' + ' '.join(EXAMPLE_TEXTS)
if __name__ == '__main__':
if len(sys.argv) > 2:
input_texts = sys.argv[1:]
pprint(similarity_matrix(input_texts))
else:
print('Please run as follows: \n$ ' + EXAMPLE_SCRIPT)
実行
python examples/similarity.py 今日はとても良い天気です。 昨日の天気は大雨だったのに。 ラーメンを食べました。
結果
今日はとても良い天気です。 | 昨日の天気は大雨だったのに。 | ラーメンを食べました。 | |
---|---|---|---|
今日はとても良い天気です。 | 1.0 | 0.9085916084662856 | 0.7043564497093551 |
昨日の天気は大雨だったのに。 | 0.9085916084662856 | 1.0 | 0.7341796340817486 |
ラーメンを食べました。 | 0.7043564497093551 | 0.7341796340817486 | 1.0 |
※結果は見やすいように加工しています。
説明を開く
spaCy の Doc.similarity() を利用して文同士の類似度を計算しています。自分自身との類似度は1で、類似度の値が大きいほど似ているということです。
GiNZA そのものは MIT License で利用できます。詳しくは ライセンス条項 をご覧ください。
筆者の Qiitaの記事 および GiNZA examples - GitHub も同様に MIT License で利用できます。
- GiNZA を利用できる言語はPythonのみです。しかしフレームワークである spaCy にはJavaScript版やR版など Python以外の言語での実装 があるため、すごい人たちが移植してくれることを期待します。
- 単語のネガティブ・ポジティブを数値化する Token.sentiment は現時点で実装されていませんが、 GiNZA 開発者の方から直々にコメントをいただき、今後実装を計画していただけるとのことです。
ご意見・ご要望などは随時受け付けています。 Qiitaの記事 へコメント、または GitHubのIssues へ投稿をお願いします。