gensimでフレーズ抽出

以前このブログで、テキストデータ中のよく連続する単語を検出するコードを紹介しました。
参考: Pythonを使ってよく連続する文字列を検索する

これは単純にある単語の前か後に出現しやすい単語を探すだけのコードだったのですが、実は同じような目的のモデルでもう少しスマートなロジックで実装されたものがgensimにあることがわかったのでそれを紹介します。

なお、今回の記事は以下のバージョンのgensimで動かすことを前提とします。

$ pip freeze | grep gensim
gensim==4.1.2

僕は複数開発環境を持っているのですが、gensim==3.8.0 など、3系の環境と、今使っている4系の環境で細かい挙動が色々異なり少し手こずりました。(会社のMacで動いたコードが私物のMacで動きませんでした。)
今回紹介するモデルに限らず、githubのgensimのリポジトリのWikiにマイグレーションガイドが出てるので、gensimを頻繁に使われる方は一読をお勧めします。
参考: Migrating from Gensim 3.x to 4 · RaRe-Technologies/gensim Wiki · GitHub

前置きが長くなりました。今回紹介するのは、gensimのphrasesです。
ドキュメント: models.phrases – Phrase (collocation) detection — gensim

要は、分かち書き済みの文章から学習して、頻繁に連続する2単語をフレーズとして抽出してくれるモデルです。
「頻繁に連続する」の基準として、僕が以前の記事で紹介したような単純な割合ではなく、論文で提唱されている手法(を元にした関数)を使ってスコアリングし、そのスコアが閾値を超えたらフレーズとして判定するという手法が採られています。(デフォルトで使われるのは1個目の方です。2個目はオプションで使うことができます。)
参考:
– Distributed Representations of Words and Phrases and their Compositionality
– Normalized (Pointwise) Mutual Information in Collocation Extraction” by Gerlof Bouma

今回の記事は使い方をメインで扱いたいので、このスコアリング関数については次の記事で紹介しましょうかね。

早速使っていきましょう。まず学習させるデータの準備です。以前用意したライブドアニュースコーパスを使います。今回はお試しで、そんなたくさんのデータ量いらないので、「ITライフハック」のデータだけ使います。
参考: livedoorニュースコーパスのファイルをデータフレームにまとめる

上記の記事で作ったCSVデータの読み込みと、分かち書きまでやっておきます。

import subprocess
import pandas as pd
import MeCab


# データの読み込み
df = pd.read_csv("./livedoor_news_corpus.csv")
# 今回は"it-life-hack" だけ使う
df = df[df.category=="it-life-hack"].reset_index(drop=True)
# ユニコード正規化とアルファベットの小文字統一
df.text = df.text.str.normalize("NFKC").str.lower()

# 辞書のパス取得
dicdir = subprocess.run(["mecab-config", "--dicdir"], capture_output=True, text=True).stdout.strip()
# 今回は品詞情報も原型変換も行わないので -Owakati で実行する。
tagger = MeCab.Tagger(f"-Owakati -d {dicdir}/ipadic")


# 分かち書きした結果を配列で返す関数
def mecab_tokenizer(text):
    return tagger.parse(text).split()


# 動作確認
print(mecab_tokenizer("すもももももももものうち"))
# ['すもも', 'も', 'もも', 'も', 'もも', 'の', 'うち']

# 分かち書き
df["tokens"] = df.text.apply(mecab_tokenizer)

これで、各テキストを分かち書きして配列にしたものがdf[“tokens”]に入りました。 (scikit-learnの場合は空白区切りの文字列にしますが、gensimの場合は単語を要素とする配列でデータを用意します。)
早速Phrasesモデルを作ります。

デフォルトのスコア関数、閾値はかなり大きめの1000で学習してみます。(あまりたくさんフレーズを見つけられても、この記事ではどうせ紹介できないのでかなり絞っています。デフォルトは10なので、通常の利用では1000は大きすぎです。)

from gensim.models.phrases import Phrases


phrase_model = Phrases(
    sentences=df["tokens"],  # 学習するデータ
    min_count=20,  # 最低何回出現した単語および単語ペアを対象とするか。デフォルト5
    scoring='default',  # スコアリングに用いる関数。 "default", "npmi", もしくは自作の関数を指定。
    threshold=1000,  # スコアが何点を超えたらフレーズとみなすか。でフォルト10.0
)

さて、これで学習ができました。学習した語彙は vocab プロパティが持っています。

phrase_model.vocab
"""
{'マイクロンジャパン': 2,
 'は': 12486,
 'マイクロンジャパン_は': 1,
 '、': 21839,
 'は_、': 3765,
 '従来': 138,
 '、_従来': 54,
 'の': 25248,
 '従来_の': 68,
# 以下略
"""

単語とその単語の出現回数に加えて、アンダーバーで二つの単語を繋いだbi-gram について、その出現回数の辞書となっています。(4系のgensimではvocabが単純な辞書ですが、実は3系では違ったのですよ。gensimオリジナルの型でしたし、単語はエンコーディングされていました。)

このモデルが結果的に見つけてくれたフレーズは、export_phrases()メソッドで取得することができます。(これも3系4系で挙動が違うメソッドです。)

phrase_model.export_phrases()
"""
{'ガ_ジェット': 1793.2403746097816,
 'インター_フェイス': 1398.7857142857142,
 '池田_利夫': 1409.4339100346021,
 '岡本_奈知': 1444.5520523497917,
 'ジャム_ハウス': 1377.0331304935767,
 'エヌプラス_copyright': 1409.4339100346021,
 'all_rights': 1259.6684809500248,
 'rights_reserved': 1393.045143638851,
 '上倉_賢': 1367.7015873015873,
 'キン_ドル': 1062.89667445223,
# 以下略
"""

見つけたフレーズと、そのフレーズのスコアの辞書として結果が得られます。

あの単語と、この単語の組み合わせのスコアって何点だったのかな?と思ったら、scoringメソッドで調べられます。気になるフレーズが検出されなかったら見てみましょう。

引数は結構たくさん渡す必要あります。まずヘルプ見てみましょう。

phrase_model.scoring?
"""
Signature:
phrase_model.scoring(
    worda_count,
    wordb_count,
    bigram_count,
    len_vocab,
    min_count,
    corpus_word_count,
)
"""

試しに、「キン_ドル」で1062.89… であることを見ておきましょうかね。worda_count とかは先に述べた通り、vocabから拾ってこれます。コーパスの単語数頭の情報はモデルが持ってるのでそこからとりましょう。

phrase_model.scoring(
    phrase_model.vocab["キン"],
    phrase_model.vocab["ドル"],
    phrase_model.vocab["キン_ドル"],
    len(phrase_model.vocab),
    phrase_model.min_count,
    phrase_model.corpus_word_count
)
# 1062.89667445223

学習に使ったデータとは別のテキストから、学習済みのフレーズを検索することもできます。

sample_data = [
    ['アマゾン', 'の', '新しい', 'ガ', 'ジェット'],
    ['新しい', 'キン', 'ドル', 'を', '買い', 'まし', 'た'],
]

print(phrase_model.find_phrases(sample_data))
# {'ガ_ジェット': 1793.2403746097816, 'キン_ドル': 1062.89667445223}

また、次のようにdictのようにモデルを使うと、渡されたデータ内で見つけたフレーズを _ で連結してくれます。結果がジェネレーターで帰ってくるので、listを使って配列にしてからprintしました。これはとても便利な機能なのですが、このような辞書的な呼び出し方ではなく、transformか何か名前のあるメソッドにしてほしかったですね。

print(list(phrase_model[sample_data]))
# [['アマゾン', 'の', '新しい', 'ガ_ジェット'], ['新しい', 'キン_ドル', 'を', '買い', 'まし', 'た']]

ちなみに、 _ だと不都合がある場合は、モデル学習時に delimiter 引数で違う文字を使うこともできます。

スコア関数を変えたり、閾値を変えたらり、また、スコア関数の中でmin_countなども使われていますので、この辺の値を変えることで結果は大きく変わります。なかなか面白いので色々試してみましょう。

また、このモデルを重ねがけするように使うことで、3単語以上からなるフレーズを抽出することもできます。(閾値などの調整に少々コツが必要そうですが。)
そのような応用もあるので、なかなか面白いモデルだと思います。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です