gensimのDictionaryオブジェクトに含まれれる単語を出現頻度で絞り込む

最近久々にgensimのトピックモデルを使う機会がありました。
そのとき、出現する単語を頻度で絞り込みたったので方法を調べました。

トピックモデルの方法自体は、既に記事を書いてますのでこちらをご参照ください。
参考: gensimでトピックモデル(LDA)をやってみる

さて、gensimのLDAは、学習するコーパスを(単語, 出現回数) というタプルの配列に変換して読み込ませる必要があり、
その形への変換に、gensim.corpora.dictionary.Dictionaryを使います。
この辞書は、何も指定しないと、1回以上出現した単語を全部学習してしまいます。
それを、Scikit-learnのCountVectorizerで、min_dfを指定したときみたいに、n回以上出現した単語のみ、と足切りしたいというのが今回の記事の目的です。

Dictionaryの語彙学習時に指定できる引数の中に、CountVectorizerのmin_dfに相当するものがなかったので、てっきり指定できないのかと思っていたのですが、
じつは、学習した後に、語彙を絞り込む関数である、filter_extremesが用意されていることがわかりました。

使いかたを説明するために、まず適当な単語の羅列でコーパスを作って、辞書を学習しておきます。


import numpy as np
from gensim.corpora.dictionary import Dictionary

# 単語リスト作成
words = [
    "White",
    "Black",
    "Grey",
    "Red",
    "Orange",
    "Yellow",
    "Green",
    "Purple",
    "Blue",
    "Cyan",
    "Magenta",
]

# 再現性のためシードを固定する
np.random.seed(2)
# 単語を適当に選んで文章データを生成
documents = [
    np.random.choice(words, size=np.random.randint(3, 7)).tolist() for _ in range(10)
]

print(documents)
"""
[['Blue', 'Green', 'Grey'],
 ['Blue', 'Purple', 'Grey', 'Black', 'Yellow', 'Magenta'],
 ['Orange', 'Yellow', 'Purple'],
 ['Green', 'Orange', 'Magenta', 'Red', 'Purple', 'Green'],
 ['Black', 'Magenta', 'Red', 'Yellow', 'Blue'],
 ['Green', 'Red', 'Cyan'],
 ['Grey', 'White', 'Orange', 'Grey', 'Orange', 'Magenta'],
 ['Black', 'Purple', 'Blue', 'Grey', 'Magenta', 'Cyan'],
 ['Blue', 'Purple', 'Black', 'Green'],
 ['Magenta', 'Yellow', 'Cyan']]
"""

# 辞書の作成
dictionary = Dictionary(documents)

# 学習した単語リスト
for word, id_ in dictionary.token2id.items():
    print(f"id: {id_}, 単語: {word}, 出現ドキュメント数: {dictionary.dfs[id_]}, 出現回数: {dictionary.cfs[id_]}")

"""
id: 0, 単語:  Blue,    出現ドキュメント数: 5, 出現回数: 5
id: 1, 単語:  Green,   出現ドキュメント数: 4, 出現回数: 5
id: 2, 単語:  Grey,    出現ドキュメント数: 4, 出現回数: 5
id: 3, 単語:  Black,   出現ドキュメント数: 4, 出現回数: 4
id: 4, 単語:  Magenta, 出現ドキュメント数: 6, 出現回数: 6
id: 5, 単語:  Purple,  出現ドキュメント数: 5, 出現回数: 5
id: 6, 単語:  Yellow,  出現ドキュメント数: 4, 出現回数: 4
id: 7, 単語:  Orange,  出現ドキュメント数: 3, 出現回数: 4
id: 8, 単語:  Red,     出現ドキュメント数: 3, 出現回数: 3
id: 9, 単語:  Cyan,    出現ドキュメント数: 3, 出現回数: 3
id: 10, 単語: White,   出現ドキュメント数: 1, 出現回数: 1
"""

これを4個以上の文章に登場した単語だけに絞りこみたいとすると、
filter_extremes(no_below=4)
を実行すれば良いよいうに思えます。
それでやってみたのがこちら。


dictionary.filter_extremes(no_below=4)
for word, id_ in dictionary.token2id.items():
    print(f"単語: {word}, 出現ドキュメント数: {dictionary.dfs[id_]}")
"""
単語: Blue, 出現ドキュメント数: 5
単語: Green, 出現ドキュメント数: 4
単語: Grey, 出現ドキュメント数: 4
単語: Black, 出現ドキュメント数: 4
単語: Purple, 出現ドキュメント数: 5
単語: Yellow, 出現ドキュメント数: 4
"""

Orange/Red/Cyan/White が消えましたね。Orangeは出現回数自体は4でしたが、ドキュメント数が3だったので消えています。
ここで注意なのが、出現ドキュメント数が6だった、Magentaも消えていることです。

これは、filter_extremesのデフォルトの引数が、(no_below=5, no_above=0.5, keep_n=100000, keep_tokens=None) と、
no_above=0.5 も指定されていることに起因します。
つまり、全体の0.5=50%よりも多く出現している単語も一緒に消してしまうわけです。
逆に、no_above だけ指定しても、no_belowは5扱いなので、4文書以下にしか登場しない単語は足切りされます。

この挙動が困る場合は、忘れないように、no_belowとno_aboveを両方指定する必要があります。


# もう一度辞書の作成
dictionary = Dictionary(documents)
dictionary.filter_extremes(no_below=4, no_above=1)
for word, id_ in dictionary.token2id.items():
    print(f"単語: {word}, 出現ドキュメント数: {dictionary.dfs[id_]}")
"""
単語: Blue, 出現ドキュメント数: 5
単語: Green, 出現ドキュメント数: 4
単語: Grey, 出現ドキュメント数: 4
単語: Black, 出現ドキュメント数: 4
単語: Magenta, 出現ドキュメント数: 6
単語: Purple, 出現ドキュメント数: 5
単語: Yellow, 出現ドキュメント数: 4
"""

出現回数で足切りするのではなく、残す単語数を指定したい場合は、keep_n を使えます。
(これにもデフォルト引数が入ってるので気をつけてください。元の単語数が100000を超えていたら、意図せず動作します)

5単語に絞り込むコードはこうなります。
単語は出現頻度が高い順に選ばれます。
no_below や no_aboveも同時に作用するので、これらの設定次第では、keep_nで指定したよりも少ない単語しか残らないことがあります。


# もう一度辞書の作成
dictionary = Dictionary(documents)
dictionary.filter_extremes(no_below=1, no_above=1, keep_n=5)
for word, id_ in dictionary.token2id.items():
    print(f"単語: {word}, 出現ドキュメント数: {dictionary.dfs[id_]}")
"""
単語: Blue, 出現ドキュメント数: 5
単語: Green, 出現ドキュメント数: 4
単語: Grey, 出現ドキュメント数: 4
単語: Magenta, 出現ドキュメント数: 6
単語: Purple, 出現ドキュメント数: 5
"""

あとは、あまり使わなさそうですが、 keep_tokens に単語を指定することで、no_belowや、no_aboveに関係なく、
その単語を残すことができます。


# もう一度辞書の作成
dictionary = Dictionary(documents)
dictionary.filter_extremes(no_below=5, no_above=1, keep_tokens=["White"])
for word, id_ in dictionary.token2id.items():
    print(f"単語: {word}, 出現ドキュメント数: {dictionary.dfs[id_]}")
"""
単語: Blue, 出現ドキュメント数: 5
単語: Magenta, 出現ドキュメント数: 6
単語: Purple, 出現ドキュメント数: 5
単語: White, 出現ドキュメント数: 1
"""

小ネタですが、Dictionaryオブジェクトは、各単語が出現したドキュメント数をdfs, 出現した回数をcfsという変数に保有しています。
filter_extremes を実行すると、dfsの方は単語が絞り込まれた上でidも振り直されるのですが、
cfsは単語が絞り込まれるだけで、idが振り直されません。
(なぜこんな仕様になっているのかは謎です。将来的に修正されるような気がします。)
直前のサンプルコードを動かした時点で、 dfsとcfs の中身を見たものがこちらです。
単語数が4個に減っているのは共通ですが、cfsの方はidが5とか10とか、元のままであることがわかります。


print(dictionary.dfs)
# {0: 5, 2: 5, 1: 6, 3: 1}
print(dictionary.cfs)
# {0: 5, 5: 5, 4: 6, 10: 1}

コメントを残す

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