最近久々に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}