🥗

Python × GiNZAで固有名詞を抽出してみる

2023/01/02に公開

はじめに

こんにちは。エンジニアのアルバイトをしている大学生です!
ここ最近は業務で「社内の知見を整理しよう」みたいなプロジェクトに携わっています。

先日、上記プロジェクトの一環としてドキュメントから社内用語を洗い出す作業があったのですが、ドキュメントの量が多くめんどくさかったので、ディープラーニングを使って固有名詞を洗い出してみました。

今回の記事は、その備忘録になります。

対象読者

  • 文章から固有名詞を抽出したい方
  • 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

解説

大まかな方針として、以下のような流れで固有名詞を抽出しています。

  1. モデルで単語のリストを取得する。
  2. 固有名詞を固有名詞のリストpropnsに追加する。
  3. 連続する一般名詞を固有名詞とみなしてpropnsに追加する。
  4. 得られた固有名詞のリストを綺麗にする。

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です。

2. 固有名詞を固有名詞のリストpropnsに追加する

この処理は、text2propnsの条件分岐で行っています。なお、GiNZAの仕様としてtoken.pos_に単語の品詞が、token.orth_に分解した単語が入っています。

# 固有名詞のとき
elif pos == "PROPN":
    propns.append(token.orth_)
    commit_nouns(propns, nouns, min_nouns_length)

commit_nounsについては次で解説します。

3. 連続する一般名詞を固有名詞とみなしてpropnsに追加する

この処理ではまずtext2propnsの条件分岐で、連続した一般名詞のリストnounsを作ります。

# 一般名詞のとき
if pos == "NOUN":
    nouns.append(token.orth_)

続いて、commit_nounsnounsを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していくよりは全然楽だし早いです。所詮人も見落とすし。

ちなみに「ポテトサラダ」は一般名詞らしい 🥔 🥗

potesara.py
text = "ポテトサラダ"
text2propns(text, min_nouns_length=2)  # []
脚注
  1. megagonlabs/ginza: A Japanese NLP Library using spaCy as framework based on Universal Dependencies ↩︎

  2. spaCyとGiNZAを使った日本語自然言語処理 - Qiita ↩︎

  3. Colaboratory へようこそ - Colaboratory ↩︎

GitHubで編集を提案
株式会社アクティブコア

Discussion