gensimのDictionaryの中身を簡単にまとめてみた

gensimでLDAやらtfidfをする際に何かと必要になるgensimのDictionaryですが、大抵の場合それらの手法を適用する際にさらっと流されることが多いように感じたのでDictionaryについてのみ纏めました。あくまで内容としては公式サイトの説明を実際に試したものを載せている程度ですが、何かの参考になれば幸いです。(2018/12/25 dfsとnum_nnzを修正)

# 環境
* gensim (3.4.0)
* Python 3.6.5
* Mac 10.13.5

# 辞書作成
とりあえず辞書がないことには始まらないので、公式の例を借りてDictionaryを作成します。

```terminal:Dictionary作成
>>> from gensim.corpora import Dictionary
>>> texts = [['human', 'interface', 'computer']]
>>> dct = Dictionary(texts)
>>> dct
<gensim.corpora.dictionary.Dictionary object at 0x10a00f2e8>
>>> len(dct)
3
>>> dct[1] # 普通にidをキーとして単語を返す辞書として使用可能
'human'
```

この時点で、dctの中には'human','interface','computer'をそれぞれidと対応させたデータが格納されています。
また、引数として`prune_at`というものが存在します。これは一見Dictionaryが保持する単語の限界数を設定しているように見えますが、使用目的としては全くの別物なので注意してください。単語数を制限したい場合は、後述のfilter_extremesやfilter_n_most_frequentなどを上手く使うようにすべきです。基本的にはデフォルトで設定された値のまま、ノータッチで問題ないでしょう。
prune_atはDictionaryの生成or単語データ追加中に、メモリ使用量を制限するために辞書の保持単語数を制限するための引数です。10000文書ごとにprune_atで指定した制限以下になるよう単語を削除し、メモリ使用量を強引に抑えます。メモリとは関係なく単語数を制限するためだけに使えないこともありませんが、挙動としては10000文書ごとに「制限による単語削減」->「文書に含まれる単語の追加」という順で処理されるため、完成したDictionaryの単語数がprune_atで指定した単語数になることはまずありません。必要な場面としては、PCのスペック不足によりあまりメモリを使われたくないくらいだと思います。

# 属性
## token2id
作成したDictionaryのtoken2id属性には、単語->idの辞書データが格納されています。

```terminal:token2id
>>> dct.token2id
{'computer': 0, 'human': 1, 'interface': 2}
>>> dct.token2id['human']
1
```

## id2token
こちらはtoken2idの逆で、id->単語になるような辞書データが格納されています。しかし注意点が一つあり、辞書生成時にはメモリ節約のためにこの辞書データは存在していません。辞書が生成されるのは、こちらで作成したDictionary本体に対して、idをキーに単語を得ようした瞬間のようです。それ以前にid2tokenに接続した場合は、空の辞書が返されます。

```terminal:id2token
>>> dct.id2token
{}
>>> dct[0]
'computer'
>>> dct.token2id
{'computer': 0, 'human': 1, 'interface': 2}
```

## dfs(2018/12/25修正)
Dictionaryの生成と更新に使用した文書を元に、idがキーになっているdf値（単語が出現する文書数）の辞書が格納されています。

```terminal:dfs
>>> dct.dfs
{1: 1, 2: 1, 0: 1}
>>> dct.add_documents([['human']]) # 'human'だけの文書を追加
>>> dct.dfs # 'human'に対応するid：1の単語の出現回数が増加している
{1: 2, 2: 1, 0: 1}
>>> dct.dfs[1] # 辞書なので、もちろんキー指定の取り出しが可能
2
```

## num_docs
Dictionaryの生成と更新に使用された全文書数が格納されています。

```terminal:num_docs
>>> dct.num_docs # 作成時の1文書 + dfsの説明時に追加した1文書
2
```

## num_pos
Dictionaryの生成と更新に使用された全単語数が格納されています。あくまで使用された全単語数であるため、すでに辞書内に存在する単語が更新に用いられた場合もカウントされています。

```terminal:num_pos
>>> dct.num_pos # dfs説明時の'human'も含めた「'human', 'interface', 'computer', 'human'」で全4単語．
4
>>> len(dct) # Dictionaryデータに被りは存在しないので，こちらは3単語．
3
```

## num_nnz(2018/12/25修正)
誰だnum_nnsとかいう存在しない名前を書いていた奴は。……はい、私です。いや本当にお恥ずかしい限りでした。
num_nnzはコーパスに含まれる各文書に出現する単語の種類の合計値を表しています。
下の例の最初では、1つ目の文章に[a,b,c]の3種類、2つ目の文書に[a,c]の2種類が含まれているので、$3+2=5$です。文書数が1つの場合は、`dct.num_nnz=len(dct)`になるようです。
そもそも公式のリファレンスをみたら、全文書でBOWのゼロじゃない部分の総数って思いっきり書かれてました。なぜ記事を書いた時の私はそれが読めていなかったのでしょうね……。

```terminal:num_nnz
>>> texts=[['a','a','b','c'],['a','a','c']]
>>> dct = Dictionary(texts)
>>> dct.num_nnz
5
>>>
>>> texts=[['a','a','b','c']]
>>> dct.num_nnz
5
>>> texts=[['a','a','b','c']]
>>> dct = Dictionary(texts)
>>> dct.num_nnz
3
>>> len(dct)
3
```


# 関数
## add_documents(documents, prune_at=2000000)
Dictionaryを更新するために用いる関数です。入力に用いるdocumentsはDictionary生成時と同様のフォーマットを用います。下の例は公式の使用例を拝借しました。

```terminal:add_documents
>>> corpus = ["máma mele maso".split(), "ema má máma".split()]
>>> dct = Dictionary(corpus)
>>> len(dct)
5
>>> dct.token2id
{'maso': 0, 'mele': 1, 'máma': 2, 'ema': 3, 'má': 4}
>>> dct.add_documents([["this", "is", "sparta"], ["just", "joking"]])
>>> len(dct)
10
>>> dct.token2id
{'maso': 0, 'mele': 1, 'máma': 2, 'ema': 3, 'má': 4, 'is': 5, 'sparta': 6, 'this': 7, 'joking': 8, 'just': 9}
```

## compactify()
Dictionaryから単語を削除した場合などに、idを振り直すための関数です。しかし単語を削除するような関数を用いた場合には、勝手に内部でこの関数が呼び出されているため、基本的にこちら側からこの関数を使うことはないと思います。一応実行してみましたが、特に変化はありませんでした。

```terminal:compactify()
>>> dct.token2id
{'maso': 0, 'mele': 1, 'máma': 2, 'ema': 3, 'má': 4, 'is': 5, 'sparta': 6, 'this': 7, 'joking': 8, 'just': 9}
>>> dct.compactify()
>>> dct.token2id # 結果変わらず．
{'maso': 0, 'mele': 1, 'máma': 2, 'ema': 3, 'má': 4, 'is': 5, 'sparta': 6, 'this': 7, 'joking': 8, 'just': 9}
```

## doc2bow(document, allow_update=False, return_missing=False)
documentをbag-of-words形式に変換してくれる関数であり、gensimで処理するときには何かと入り用な大変お世話になる関数です。崇めましょう。
引数として`allow_update`と`return_missing`が存在しています。効果はそれぞれ以下の通りです。

* allow_update: Dictionary内に存在しない語がdocument内に含まれていた場合、その語を新たにDictionaryに追加し、新単語も含めたBoWを返す
* return_missing: Dictionary内に存在しない語がdocument内に含まれていた場合、その語をキーにして何回出現したかが収納された辞書を返す
* 2つを併用した場合: 単語がDictionaryに追加され新単語も含めたBoWと追加された単語の出現回数が収納された辞書を返す

```terminal:doc2bow
>>> dct.token2id
{'maso': 0, 'mele': 1, 'ema': 2, 'má': 3, 'is': 4, 'sparta': 5, 'this': 6, 'joking': 7, 'just': 8}
>>> dct.doc2bow(["this", "is", "máma"]) # 'máma'は登録していないため返ってこない．
[(4, 1), (6, 1)]
>>> dct.doc2bow(["this", "is", "máma","máma"],return_missing=True) # 'máma'は登録していなかったので，辞書が返される．
([(4, 1), (6, 1)], {'máma': 2})
>>> dct.doc2bow(["this", "is"],return_missing=True) # 存在しない語が含まれていない場合は空の辞書が返される．
([(4, 1), (6, 1)], {})
>>> dct.doc2bow(["this", "is", "máma","máma"],allow_update=True) # 新しく'máma'がDictionaryに登録され，その状態でBoWが返される．
[(4, 1), (6, 1), (9, 2)]
>>> dct.token2id
{'maso': 0, 'mele': 1, 'ema': 2, 'má': 3, 'is': 4, 'sparta': 5, 'this': 6, 'joking': 7, 'just': 8, 'máma': 9}
>>> dct.doc2bow(["this", "is", "máma","máma",'mko'],allow_update=True,return_missing=True) # 併用した場合は存在しなかった語が返されるが，Dictionaryも更新される．
([(4, 1), (6, 1), (9, 2), (10, 1)], {'mko': 1})
```

## doc2idx(document, unknown_word_index=-1)
入力したドキュメントをDictionary内のidに置き換える関数です。あくまでid表示に置き換えるだけなので、BoWのように出現回数は関係ありません。`unknown_word_index`によってDictionary内に存在しない語が入力された場合の変換内容を変更することができます。デフォルトでは-1に設定されます。

```terminal:doc2idx
>>> dct.token2id
{'maso': 0, 'mele': 1, 'ema': 2, 'má': 3, 'is': 4, 'sparta': 5, 'this': 6, 'joking': 7, 'just': 8, 'máma': 9, 'mko': 10}
>>> dct.doc2idx(['this','is','sparta','I','am','sparta'])
[6, 4, 5, -1, -1, 5]
>>> dct.doc2idx(['this','is','sparta','I','am','sparta'],unknown_word_index='hoge') # 文字列に置き換えることも可能
[6, 4, 5, 'hoge', 'hoge', 5]
>>> dct.doc2idx(['this','is','sparta','I','am','sparta'],unknown_word_index=[]) # 割となんでもいいっぽい
[6, 4, 5, [], [], 5]
```

## filter_extremes(no_below=5, no_above=0.5, keep_n=100000, keep_tokens=None)
Dictionaryに登録した単語データを、単語出現回数や出現文書頻度、辞書最大保存数によってフィルタリングすることができる関数です。事前処理としてこれも何かと使用頻度が高いので、一日一回くらい拝んでおきましょう。引数として`no_below`,`no_above`,`keep_n`,`keep_tokens`が存在しています。それぞれの効果は以下の通りです。keep_nのデフォルト値に気づかずフィルタをした結果、単語がめちゃくちゃ減ってるようなことがあった（実体験）ので注意しましょう。
また、keep_nはDictionaryの長さでバッサリと単語数を制限しているため、keep_tokensによる保護が無意味になります。

* no_below: 「出現文書数≥指定値」になるような語のみを保持する(同一文書内での出現頻度は関係なし）
* no_above: 「出現文書数/全文書数≤指定値」になるような語のみを保持する(同一文書内での出現頻度は関係なし）
* keep_n: 辞書のid小さい順に指定値個のデータを保持する
* keep_tokens: 削除条件を満たしていても、指定した語をDictionary内に保持し続ける。ただし、**keep_nによる削除は防げない**

```terminal:filter_extremes
>>> corpus=[['dog','cat','rabbit'],['dog','cat','mouse','cat'],['dog','rabbit','bird','bird','bird'],['dog']] # 4 document
>>> dct = Dictionary(corpus)
>>> dct.token2id
{'cat': 0, 'dog': 1, 'rabbit': 2, 'mouse': 3, 'bird': 4}
>>> dct.filter_extremes(no_below=2, no_above=1.0) # 「出現文書数≥2」の語を保持．no_adoveは1(=100%)を指定しているので効果なし．
>>> dct.token2id # 1文書にしか含まれていない'mouse'と'bird'が削除された．
{'cat': 0, 'dog': 1, 'rabbit': 2}
>>> dct = Dictionary(corpus)
>>> dct.token2id
{'cat': 0, 'dog': 1, 'rabbit': 2, 'mouse': 3, 'bird': 4}
>>> dct.filter_extremes(no_below=1, no_above=0.5) # 「出現文書数/全文書数≤0.5」の語を保持．no_belowは1回以上出現する語を指定しているため効果なし．
>>> dct.token2id # 'dog'は4/4=1なので削除 'cat'は2/4=0.5なので保持される．
{'cat': 0, 'rabbit': 1, 'mouse': 2, 'bird': 3}
>>> dct = Dictionary(corpus)
>>> dct.token2id
{'cat': 0, 'dog': 1, 'rabbit': 2, 'mouse': 3, 'bird': 4}
>>> dct.filter_extremes(no_below=1, no_above=1.0, keep_n=3) # 3単語目までを保持
>>> dct.token2id # 4番目以降の'mouse'と'bird'は削除されている
{'cat': 0, 'dog': 1, 'rabbit': 2}
>>> dct = Dictionary(corpus)
>>> dct.token2id
{'cat': 0, 'dog': 1, 'rabbit': 2, 'mouse': 3, 'bird': 4}
>>> dct.filter_extremes(no_below=2, no_above=1.0,keep_tokens=['mouse','bird'])
>>> dct.token2id # 削除条件を満たしているが，指定した'mouse','bird'は削除されない
{'cat': 0, 'dog': 1, 'rabbit': 2, 'mouse': 3, 'bird': 4}
>>> dct = Dictionary(corpus)
>>> dct.token2id
{'cat': 0, 'dog': 1, 'rabbit': 2, 'mouse': 3, 'bird': 4}
>>> dct.filter_extremes(no_below=1, no_above=1.0, keep_n=3, keep_tokens=['mouse','bird'])
>>> dct.token2id # keep_nは削除する語を確認していないため，keep_tokensが無意味
{'cat': 0, 'dog': 1, 'rabbit': 2}
```

## filter_n_most_frequent(remove_n)
出現文書数の多い単語順に、指定した単語数だけDictionary内から単語を削除する関数です。同じ出現文書数の語が複数ある場合は、同じ出現文書数の中からidの若い順に削除されます。（pythonのsortedは同値の順番は保つので、おそらくそうなるはず。間違ってたらごめんなさい）

```terminal:filter_n_most_frequent
>>> corpus = [["máma", "mele", "maso"], ["ema", "má", "máma"]]
>>> dct = Dictionary(corpus)
>>> dct.token2id
{'maso': 0, 'mele': 1, 'máma': 2, 'ema': 3, 'má': 4}
>>> dct.filter_n_most_frequent(2)
>>> dct.token2id # 出現頻度の最も多い'máma'が削除＋同じ出現頻度中から最もidの若い'maso'が削除
{'mele': 0, 'ema': 1, 'má': 2}
```

## filter_tokens(bad_ids=None, good_ids=None)
Dictionaryに対し、指定したidの語のみを削除or保持するような操作を行う関数です。引数には`bad_ids`,`good_ids`があり、役割はそれぞれ以下の通りです。併用はややこしくなるだけなので、やめたほうが良さげかなぁと。そもそも併用するような場面がない気もしますけど。

* bad_ids: 指定したidの語をDictionaryから削除
* good_ids: 指定したidの語のみを保持（＝それ以外は削除）
* 順序としてはbad_ids->good_idsという順で処理が行われている

```terminal:filter_tokens
>>> corpus = [["máma", "mele", "maso"], ["ema", "má", "máma"]]
>>> dct = Dictionary(corpus)
>>> dct.token2id
{'maso': 0, 'mele': 1, 'máma': 2, 'ema': 3, 'má': 4}
>>> dct.filter_tokens(bad_ids=[2,4]) # id=2,4を指定して削除
>>> dct.token2id # 元のid=2,4だった'máma'と'má'だけが削除されている
{'maso': 0, 'mele': 1, 'ema': 2}
>>> dct = Dictionary(corpus)
>>> dct.token2id
{'maso': 0, 'mele': 1, 'máma': 2, 'ema': 3, 'má': 4}
>>> dct.filter_tokens(good_ids=[2,4]) # id=2,4を指定して保持
>>> dct.token2id # 元のid=2,4だった'máma'と'má'のみが保持されている
{'máma': 0, 'má': 1}
>>> dct = Dictionary(corpus)
>>> dct.token2id
{'maso': 0, 'mele': 1, 'máma': 2, 'ema': 3, 'má': 4}
>>> dct.filter_tokens(bad_ids=[0,4],good_ids=[0,3]) # 一応併用した場合
>>> dct.token2id # 先に'maso'と'má'を削除した後に'maso'と'ema'のみを保持しているため，'ema'しかのこらない
{'ema': 0}
```

## from_corpus(corpus, id2word=None)　※staticメソッド
BoWのコーパスからDictionaryを生成するための関数です。BoWから作成しているため、idと単語の対応を表す辞書を入力しない場合はidのままDictionaryに格納されます。

```terminal:from_corpus
>>> corpus = [["máma", "mele", "maso"], ["ema", "má", "máma"]]
>>> dct = Dictionary(corpus)
>>> new_corpus = [dct.doc2bow(c) for c in corpus] # コーパスをBoW化
>>> new_corpus
[[(0, 1), (1, 1), (2, 1)], [(2, 1), (3, 1), (4, 1)]]
>>> new_dic = Dictionary.from_corpus(new_corpus) # BoWから新しいDictionaryを生成
>>> new_dic.token2id # id2wordを指定していないのでid表示のまま
{'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
>>> dct[1] # 元のdctのid2token辞書生成
'mele'
>>> dct.id2token
{0: 'maso', 1: 'mele', 2: 'máma', 3: 'ema', 4: 'má'}
>>> new_dct = Dictionary.from_corpus(new_corpus, id2word= dct.id2token) # id2wordを指定して辞書を作成
>>> new_dct.token2id # 今度はidから単語に変更されている
{'maso': 0, 'mele': 1, 'máma': 2, 'ema': 3, 'má': 4}
```

## from_documents(documents)　※staticメソッド
新しいDictionaryクラスを生成して返す関数。挙動としては普通にDictionaryを生成するのと変わらない（内部でDictionary(documents)をreturnしてるだけ）ため、わざわざこれを使う機会は無い……と思う。

```terminal:from_documents
>>> texts = [['human', 'interface', 'computer']]
>>> dct = Dictionary.from_documents(texts)
>>> dct.token2id
{'computer': 0, 'human': 1, 'interface': 2}
```

## get(k[,d])
通常のpythonの辞書で使うgetと同じ。指定したキーがない場合もエラーを起こさず、こちらでキーがない場合の返り値も指定することができる。

```terminal:get
>>> dct.token2id
{'computer': 0, 'human': 1, 'interface': 2}
>>> dct.get(1,None) # 1は存在するので'human'が返ってくる
'human'
>>> dct.get(9,'hoge') # 9は存在しないので指定した'hoge'が返ってくる
'hoge'
>>> dct.get(9) # 9は存在しない＆返り値を指定してないので何も返ってこない(Noneが返ってくる）
```

## items(), iteritems(), iterkeys(), itervalues(),keys()
これらもpythonの辞書で使える各関数と同じ（だと思う）。

```terminal:items()
>>> for (item,iteritem,iterkey,itervalue,key) in zip(dct.items(),dct.iteritems(),dct.iterkeys(),dct.itervalues(),dct.keys()):
...     print(item,iteritem,iterkey,itervalue,key)
... 
(0, 'computer') (0, 'computer') 0 computer 0
(1, 'human') (1, 'human') 1 human 1
(2, 'interface') (2, 'interface') 2 interface 2
```

## save(fname_or_handle, separately=None, sep_limit=10485760, ignore=frozenset([]), pickle_protocol=2), load(fname)
Dictionaryのセーブとロードを行うための関数です。この関数を使ってDictionaryを保存した場合、生成されたファイルは中を見ても意味がわからないものになります。saveにはファイル名指定の他に複数の引数が存在しますが、読んでも自分の理解が足りずにちんぷんかんぷんだった＆gensimにお任せしたほうが良さそうだったのでここでは触れないことにします。一応自分も9MBのDictionaryを保存したりしていますが、自分の環境でその程度なら全く問題ないので基本的には気にする必要はないのかなぁと思っています。

```terminal:save,load
>>> texts = [['human', 'interface', 'computer']]
>>> dct = Dictionary(texts)
>>> dct.token2id
{'computer': 0, 'human': 1, 'interface': 2}
>>> dct.save('dct.dict')
>>> new_dct = Dictionary.load('dct.dict')
>>> new_dct.token2id
{'computer': 0, 'human': 1, 'interface': 2}
```

## save_as_text(fname, sort_by_word=True),load_from_text(fname)
通常のsave&loadで作成されるファイルが開いても読めない形式だったのに対し、こちらを使用した場合は人間の目でも理解可能なtxt形式で出力されます。sort_by_wordをFalseにした場合は、辞書のid順とは異なる順で出力されるようです。性能としては通常のsave()の方が良いので、基本的にはsave()load()を使用してDictionaryの確認や別形式で読み込む必要がある場合などに用いると良いとのこと。
txtファイルの内容としては「id\t単語\t出現文書数」です。

```terminal:save_as_text,load_from_text
>>> texts = [['human', 'interface', 'computer']]
>>> dct = Dictionary(texts)
>>> dct.token2id
{'computer': 0, 'human': 1, 'interface': 2}
>>> dct.save_as_text('dct.txt')
>>> new_dct = Dictionary.load_from_text('dct.txt')
>>> new_dct.token2id
{'computer': 0, 'human': 1, 'interface': 2}
```

```text:dct.txt
1
0	computer	1
1	human	1
2	interface	1

```

## merge_with(other)
Dictionaryに対し、異なるDictionaryを結合することのできる関数です。mergeする際の返り値に関しては、あまりよくわからなかったのでパスです。情報求む

```terminal:merge_with
>>> corpus_1, corpus_2 = [["a", "b", "c"]], [["a", "f", "f"]]
>>> dct_1, dct_2 = Dictionary(corpus_1), Dictionary(corpus_2)
>>> dct_1.token2id
{'a': 0, 'b': 1, 'c': 2}
>>> dct_2.token2id
{'a': 0, 'f': 1}
>>> transformer = dct_1.merge_with(dct_2)
>>> dct_1.token2id
{'a': 0, 'b': 1, 'c': 2, 'f': 3}
```

#まとめ
何かと利用されるけど、gensimのDictionary自体について日本語で書かれたページはないっぽい……？と思ったのでとりあえずまとめてみました。ただイマドキのgoogle先生を使えば、あるいはちょっと頑張れば大体は読めそうな分量の英語で公式ドキュメントが書かれてるので、割と無駄に時間を使っただけのような。最悪ソースコードを読めば日本語も英語も関係ないですし。~~これやっぱり需要がないから今までなかっただけでは……？~~うん、まあ、最初にも書きましたが、何かの参考にでもなれば幸いです。
万が一内容に間違い等がありましたら、容赦なくご指摘ください。

# 参考サイト
* [gensim: corpora.dictionary – Construct word&lt;-&gt;id mappings](https://radimrehurek.com/gensim/corpora/dictionary.html)
* [gensim/dictionary.py at develop · RaRe-Technologies/gensim](https://github.com/RaRe-Technologies/gensim/blob/develop/gensim/corpora/dictionary.py)