GensimPy3を使って小説家になろうのトピックモデルを解析


[gensim](http://radimrehurek.com/gensim/)というトピックモデルを扱うPythonのライブラリがある。公式ではPythonのバージョン 2.5 <= Python < 3.0 にしか対応していない。

しかしSamantpさんが[gensimPy3](https://github.com/samantp/gensimPy3)というライブラリを公開している。gensimをforkしてPython3.3に対応させたものだ。

今回はこのgensimPy3を使ってしょとうさんの[小説家になろうのランキングをトピックモデルで解析(gensim)](http://sucrose.hatenablog.com/entry/2013/04/27/225218) と同じ事ができるか実験した。

※gensimの使い方参考 http://yuku-tech.hatenablog.com/entry/20110623/1308810518


## GensimPy3のインストール

 https://github.com/samantp/gensimPy3

からソースコードをcloneしてくる。

```terminal
git clone git@github.com:samantp/gensimPy3.git
```
で、

```terminal
python setup.py test
```

したら

```
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa3 in position 3973: invalid start byte
```

なるエラーが出て不安になった。でも無視してインストールした。

```terminal
python setup.py install
```

なぜかSyntaxErrorが出たりしたけれどもインストールには成功した。


## 環境の違い
しょとうさんの記事 http://sucrose.hatenablog.com/entry/2013/04/27/225218 との違いを以下に記す。

- Python3.3.1 on pyenvを使用
- gensimはgensimPy3を使用
- [BeautifulSoup](http://www.crummy.com/software/BeautifulSoup/bs4/doc/)のかわりに[pyquery](https://pypi.python.org/pypi/pyquery)を使用
- urllib2のかわりに[requests](http://www.python-requests.org/en/latest/)を使用

pyqueryはJQueryと同じ記法のセレクタが使えるので、Chromeの開発者ツールのConsoleを使って試行錯誤しやすい。便利。

## ソースコード

```python:topic_model_in_narou.py
# -*- coding: utf-8 -*-
import requests
from pyquery import PyQuery as pq
import gensim
import pdb


def fetch_narou_ranking_html():
    r = requests.get('http://yomou.syosetu.com/rank/list/type/total_total/')
    r.encoding = 'utf-8'
    return r.text


def collect_tags(d):
    d_novels = d('.s')
    tags = []
    for d_novel in d_novels:
        d_tag_name_list = d_novel.findall('a')
        tags_in_a_novel = [d_tag_name.text for d_tag_name in d_tag_name_list]
        tags.append(tags_in_a_novel)
    return tags


if __name__ == "__main__":
    html = fetch_narou_ranking_html()
    d = pq(html.encode('utf-8'))
    tags = collect_tags(d)

    dictionary = gensim.corpora.Dictionary(tags)
    dictionary.filter_extremes(3)
    corpus = [dictionary.doc2bow(text) for text in tags]
    lda = gensim.models.ldamodel.LdaModel(corpus=corpus, num_topics=10, id2word=dictionary)
    for x in lda.show_topics(-1, 5):
        print(x)

```

このコードでモデルを作ってトピック表示ができた。結果は以下のとおり。

```result
0.115*ファンタジー + 0.068*魔法 + 0.041*チート + 0.034*ハーレム + 0.028*転生
0.173*魔法 + 0.095*ファンタジー + 0.039*ダーク + 0.033*トリップ + 0.027*転生
0.106*転生 + 0.087*ハーレム + 0.074*主人公最強 + 0.063*チート + 0.052*ファンタジー
0.079*ファンタジー + 0.069*チート + 0.062*恋愛 + 0.059*転生 + 0.041*異世界トリップ
0.088*ファンタジー + 0.063*異世界トリップ + 0.051*ハーレム + 0.039*冒険 + 0.039*OVL文庫大賞応募作
0.105*チート + 0.103*ファンタジー + 0.062*主人公最強 + 0.058*魔法 + 0.044*転生
0.099*ファンタジー + 0.089*転生 + 0.045*主人公最強 + 0.045*最強 + 0.034*魔法
0.051*魔法 + 0.051*成り上がり + 0.051*モンスター + 0.039*VRMMO + 0.039*シリアス
0.140*ファンタジー + 0.077*チート + 0.054*転生 + 0.043*魔法 + 0.038*冒険
0.168*ファンタジー + 0.073*魔法 + 0.052*恋愛 + 0.026*冒険 + 0.026*戦争
```

うん、ファンタジーばっかり……。同じジャンルが多すぎてトピックに分けられていない。pixiv小説あたりからデータ取ってきたほうがよさそう。

## 追記
dictionary.filter_extremes(no_below=5, no_above=0.5, keep_n=100000)関数の値を変えてフィルタリングすればファンタジーばっかりな状況を変えられるかもしれないと考えて、修正を加えた。

```python:topic_model_in_narou.py
# -*- coding: utf-8 -*-
import requests
from pyquery import PyQuery as pq
import gensim
import pdb


def fetch_narou_ranking_html():
    r = requests.get('http://yomou.syosetu.com/rank/list/type/total_total/')
    r.encoding = 'utf-8'
    return r.text


def collect_tags(d):
    d_novels = d('.s')
    tags = []
    for d_novel in d_novels:
        d_tag_name_list = d_novel.findall('a')
        tags_in_a_novel = [d_tag_name.text for d_tag_name in d_tag_name_list]
        tags.append(tags_in_a_novel)
    return tags


if __name__ == "__main__":
    html = fetch_narou_ranking_html()
    d = pq(html.encode('utf-8'))
    tags = collect_tags(d)

    dictionary = gensim.corpora.Dictionary(tags)
    dictionary.filter_extremes(no_below=5, no_above=0.05, keep_n=10000)  # 変更
    corpus = [dictionary.doc2bow(text) for text in tags]
    lda = gensim.models.ldamodel.LdaModel(corpus=corpus, num_topics=20, id2word=dictionary)
    for x in lda.show_topics(-1, 5):
        print(x)
```

no_aboveの値を0.05にした。全体の5%以上に出現しているタグはカウントしないことにした。

gensim公式サイト http://radimrehurek.com/gensim/corpora/dictionary.html

結果がこちら。

```result
0.166*成長 + 0.133*コメディ + 0.100*バトル + 0.100*成り上がり + 0.067*なろうコン大賞
0.106*SF + 0.054*トリップ + 0.054*ＶＲＭＭＯ + 0.054*なろうコン大賞 + 0.054*成り上がり
0.142*ドラゴン + 0.142*ダーク + 0.073*奴隷 + 0.073*貴族 + 0.073*バトル
0.120*魔法使い/魔女 + 0.081*精霊 + 0.081*エルフ + 0.081*獣人 + 0.081*成り上がり
0.136*勇者 + 0.136*コメディー + 0.092*迷宮 + 0.092*最強 + 0.092*ＶＲＭＭＯ
0.140*異世界召喚 + 0.106*国家/民族 + 0.071*ＶＲＭＭＯ + 0.053*溺愛 + 0.036*奴隷
0.153*騎士 + 0.078*勘違い + 0.078*中世 + 0.078*魔法使い/魔女 + 0.078*異世界召喚
0.125*召喚 + 0.125*冒険者 + 0.125*獣人 + 0.125*最強 + 0.125*ご都合主義
0.151*勇者 + 0.091*モンスター + 0.091*美形 + 0.061*戦争 + 0.061*溺愛
0.163*モンスター + 0.122*友情 + 0.082*貴族 + 0.082*成り上がり + 0.082*最強
0.260*精霊 + 0.054*貴族 + 0.054*コメディー + 0.054*シリアス + 0.054*国家/民族
0.143*冒険者 + 0.096*バトル + 0.096*シリアス + 0.096*内政 + 0.049*成り上がり
0.189*コメディ + 0.143*勘違い + 0.096*VRMMORPG + 0.096*コメディー + 0.049*高校生
0.147*ドラゴン + 0.118*最強 + 0.060*エルフ + 0.060*バトル + 0.060*戦争
0.173*奴隷 + 0.088*トリップ + 0.088*魔術 + 0.045*成長 + 0.045*モンスター
0.173*内政 + 0.088*勇者 + 0.088*トリップ + 0.045*最強 + 0.045*奴隷
0.130*シリアス + 0.088*バトル + 0.088*高校生 + 0.088*戦記 + 0.088*異世界転移
0.143*スキル + 0.107*テンプレ + 0.072*戦争 + 0.072*成り上がり + 0.072*魔術
0.206*戦争 + 0.070*内政 + 0.070*中世 + 0.070*召喚 + 0.070*国家/民族
0.130*奴隷 + 0.130*ギルド + 0.088*貴族 + 0.088*召喚 + 0.045*戦争
```

やっぱりファンタジーばっかりでした……。

でもよーく見ると「シリアス, バトル, 高校生, 戦記, 異世界転移」や「SF, トリップ, VRMMO, なろうコン大賞, 成り上がり」「戦争, 内政, 中世, 召喚, 国家／民族」のような、ちょっとは別ジャンルに見えそうなものもあるので、初期設定のdictionary.filter_extremes()処理を行ったときよりはましになった。