Python × GiNZAで固有名詞を抽出してみる
はじめに
こんにちは。エンジニアのアルバイトをしている大学生です!
ここ最近は業務で「社内の知見を整理しよう」みたいなプロジェクトに携わっています。
先日、上記プロジェクトの一環としてドキュメントから社内用語を洗い出す作業があったのですが、ドキュメントの量が多くめんどくさかったので、ディープラーニングを使って固有名詞を洗い出してみました。
今回の記事は、その備忘録になります。
対象読者
- 文章から固有名詞を抽出したい方
- Pythonの文法について基本的な理解がある方
実装
単語抽出・品詞の推定にはGiNZA[1][2]という自然言語処理モデルを使いました。モデルはgenerate_tokens
関数で使用し、メインの処理は最初のtext2propns
関数に書いています。
def text2propns(text, min_nouns_length=2):
"""文章から固有名詞を抽出する。
"""
nouns = [] # 一般名詞のリスト
propns = [] # 固有名詞のリスト
for token in generate_tokens(text):
pos = token.pos_ # 単語の品詞
# 一般名詞のとき
if pos == "NOUN":
nouns.append(token.orth_)
# 固有名詞のとき
elif pos == "PROPN":
propns.append(token.orth_)
commit_nouns(propns, nouns, min_nouns_length)
# 上記以外のとき
else:
commit_nouns(propns, nouns, min_nouns_length)
commit_nouns(propns, nouns, min_nouns_length)
propns = format_list(propns)
return propns
def generate_tokens(text):
"""文章から抽出した単語のイテレータを生成する。
"""
ginza = spacy.load("ja_ginza") # GiNZA
doc = ginza(text)
for sent in doc.sents:
for token in sent:
yield token
def commit_nouns(propns, nouns, min_nouns_length=2):
"""条件を満たす一般名詞を固有名詞に登録する。
"""
# 名詞が一定数連続しているとき
if len(nouns) >= min_nouns_length:
propn = "".join(nouns) # 連続した一般名詞を結合して固有名詞とする
propns.append(propn)
nouns.clear() # 登録した一般名詞を削除
def format_list(array):
"""リストの要素を一意にしてソートする。
"""
array = set(array) # リストの要素を一意にする
array = list(array)
array = sorted(array)
return array
Google Colaboratory[3]で動かしたい人は、最初に以下のコードを実行してください。
GiNZAをインポートできます。
!pip install -U ginza ja-ginza
import spacy
解説
大まかな方針として、以下のような流れで固有名詞を抽出しています。
- モデルで単語のリストを取得する。
- 固有名詞を固有名詞のリスト
propns
に追加する。 - 連続する一般名詞を固有名詞とみなして
propns
に追加する。 - 得られた固有名詞のリストを綺麗にする。
3.のステップを挟んだ理由は、GiNZAの固有名詞と自分の固有名詞の感覚にずれがあったからです。例えば「ポテトサラダ」は一般名詞を並べただけですが、個人的には立派な一つのメニューとして固有名詞であると考えます 🥔 🥗
1. モデルで単語のリストを取得する
この処理は以下の関数で行っています。モデルが全部やってくれるので特に何もしていません。ネストするとコードが見にくくなるので、関数に切り出しているだけです。
def generate_tokens(text):
"""文章から抽出した単語のイテレータを生成する。
"""
ginza = spacy.load("ja_ginza") # GiNZA
doc = ginza(text)
for sent in doc.sents:
for token in sent:
yield token
ちなみに取得しているのはリストではなくイテレータですが、ここでは同じものという解釈でOKです。
propns
に追加する
2. 固有名詞を固有名詞のリストこの処理は、text2propns
の条件分岐で行っています。なお、GiNZAの仕様としてtoken.pos_
に単語の品詞が、token.orth_
に分解した単語が入っています。
# 固有名詞のとき
elif pos == "PROPN":
propns.append(token.orth_)
commit_nouns(propns, nouns, min_nouns_length)
commit_nouns
については次で解説します。
propns
に追加する
3. 連続する一般名詞を固有名詞とみなしてこの処理ではまずtext2propns
の条件分岐で、連続した一般名詞のリストnouns
を作ります。
# 一般名詞のとき
if pos == "NOUN":
nouns.append(token.orth_)
続いて、commit_nouns
でnouns
を1つの文字列として結合し、固有名詞として登録します。
def commit_nouns(propns, nouns, min_nouns_length=2):
"""条件を満たす一般名詞を固有名詞に登録する。
"""
# 名詞が一定数連続しているとき
if len(nouns) >= min_nouns_length:
propn = "".join(nouns) # 連続した一般名詞を結合して固有名詞とする
propns.append(propn)
nouns.clear() # 登録した一般名詞を削除
引数のmin_nouns_length
では、固有名詞とみなすために必要な一般名詞の数を指定できます(一般名詞が2回以上連続した場合に固有名詞とする、など)。値を1以下にした場合は、ただの一般名詞を固有名詞として認識します。
4. 得られた固有名詞のリストを綺麗にする
format_list
では名詞のソート・重複排除を行っています。
重複排除にはPythonのset
を使いました。
def format_list(array):
"""リストの要素を一意にしてソートする。
"""
array = set(array) # リストの要素を一意にする
array = list(array)
array = sorted(array)
return array
実行
それでは動かしてみましょう!
この記事のタイトルから固有名詞を抽出してみます。
text = "Python × GiNZAで固有名詞を抽出してみる"
text2propns(text, min_nouns_length=2) # ['固有名詞']
「固有名詞」という固有名詞が抽出されました。ややこしい。
「Python」と「GiNZA」は一般名詞らしいですね。
単語の漏れを極力避けたい場合には、min_nouns_length=1
にすると良いと思います。
text = "Python × GiNZAで固有名詞を抽出してみる"
text2propns(text, min_nouns_length=1) # ['GiNZA', 'Python', '固有名詞']
果たして「固有名詞」は固有名詞なのだろうか 🤔
まぁ今回求めているものには近いので、ヨシ!
おわりに
実用的にはPDFやWordから文字を取得したりして、それを上記のコードに突っ込むと良いと思います。あとはモデルが誤認識したり名詞が変なところで連続したりすることがあるので、最終的には人の目で確認した方が安全です。
「じゃあ意味ないじゃん!」なんてことはなく、ドキュメントを血眼で目grep
していくよりは全然楽だし早いです。所詮人も見落とすし。
ちなみに「ポテトサラダ」は一般名詞らしい 🥔 🥗
text = "ポテトサラダ"
text2propns(text, min_nouns_length=2) # []
Discussion