JubatusでQiita記事を推薦してくれる日報アプリ作ってみた

# 概要
研修の一環 + 個人的な興味でJubatusを使ってみよう！
という趣旨で、Qiita記事を推薦してくれる機能を（知り合いが作った）日報アプリに（勝手に）実装してみました。

ユーザが日報を投稿すると、Jubatus（＋Mecab）が日報内容を解析し、類似したQiitaの記事をおすすめしてくれるような機能です。

Gitリポジトリ：https://github.com/kawasaki-kk/Introduction_mysite/tree/qiita

![recommend_qiita_red.png](https://qiita-image-store.s3.amazonaws.com/0/142563/7a00ee80-4715-fd30-1875-5c443979fb74.png)


# 1. システム構成
以下の環境で実装しました。

* CentOS 7
* Python 3.5.2
* jubatus 0.9.1
* mecab-python3 0.7

処理の流れは以下のようなイメージです。
![archtecture.png](https://qiita-image-store.s3.amazonaws.com/0/142563/3236aa0d-a006-c3cd-ecba-b67ffe4e165c.png)


1. ユーザが日報を投稿すると、日報アプリはその投稿内容をそのままJubatus（recommender）サーバに投げます。
2. Jubatusサーバでは、
  * 投稿内容をMecabを用いて形態素解析
  * 特徴ベクトルを生成
  * 類似する特徴ベクトルを持つQiita記事の情報を日報アプリに返す。

といったシンプルな流れになってます。

# 2. 環境構築
## Python 3.5.2環境の構築
Pyenvを使います。Pyenv自体のインストール方法は調べれば、素晴らしい記事がたくさんヒットするので省略。
python 3.5.2のインストール、デフォルトで使用するバージョンを変更

```shell-session
pyenv install 3.5.2
pyenv global 3.5.2
python -V
```
Python 3.5.2になってればOK。
pipのアップグレード

```shell-session
pip install -U pip
```

## Jubatusのインストール
[Jubatus公式HP](http://jubat.us/ja/quickstart.html#red-hat-enterprise-linux-6-2-64-bit)が参考になります。
リポジトリの追加、パッケージインストール、Pythonのクライアントパッケージのインストール

```shell-session
sudo rpm -Uvh http://download.jubat.us/yum/rhel/7/stable/x86_64/jubatus-release-7-2.el7.x86_64.rpm
sudo yum install jubatus jubatus-client
pip install jubatus
```
import jubatusでエラーを吐かなければ成功

## Mecabのインストール
[Python3で形態素解析エンジンMeCabを使えるようにする(2016年3月版)](http://qiita.com/grachro/items/4fbc9bf8174c5abb7bdd)を参考に、Mecab本体、辞書、Python3バインディングをインストールする。

## Qiita記事の収集
学習データとして、Qiita記事を取得しておく必要があります。
取得には、[Qiitaの投稿をガーッと取得するバッチ処理用QiitaAPI Pythonラッパー](http://qiita.com/tag1216/items/7e23630d97293e35ea4c#_reference-6a490306bbc181488c3cAPI)を利用させて頂きました。

~~しかし、Qiita API v2では、10,000件しか集めることができず、タグから記事を取得することもできませんでした。~~
~~学習データとして10,000件では少ないですが、ひとまず今回は手を打ちました。~~
~~今後、以下を検討する必要があります（未対応）。~~

~~* Qiita API v1を利用して、タグ取得 → 記事取得~~
~~* スクレイピング~~
[追記 2016/11/01]
コメントにてtagtag1216さんから情報を頂きました。
Qiita API v2で記事取得の際、requestsのparamsに以下のように値を設定することで、指定した日時以前の記事を取得することができます。

```Python3:Introduction_mysite/qiita/qiita_api/qiita2.py_80行目あたり
params = {"per_page": self.per_page, "page": self.page, "query":"created:<2016-05-01"}
```

以上で、必要な環境の構築は完了です。


# 3. 機能詳細
Jubatus側の機能は、基本的に[Jubatus公式のRecommenderチュートリアル](http://jubat.us/ja/tutorial/recommender_python.html)を拡張する形で実装しました。
日報アプリは、[Djangoで作成された知り合いのアプリ](https://github.com/hayashizakitakaaki/Introduction_mysite)をほぼそのまま使用しています。

Djangoのフレームワークに沿って作成されたファイルが沢山ありますが、私がメインで手を加えたのは
Introduction_mysite/qiita/* のみです。

Jubatusのメインコードとなる

* settings.py
* yuba_update.py
* yuba_analyze.py

を見ていきたいと思います。


## Jubatus
### Recommenderの設定
各項目の詳細は[データ変換 - Jubatus](http://jubat.us/ja/fv_convert.html)を参考。

```Python3:Introduction_mysite/dailyreport/qiita/settings.json
{
  "method": "inverted_index",
  "converter": {
    "string_filter_types": {},
    "string_filter_rules": [],
    "num_filter_types": {},
    "num_filter_rules": [],
    "string_types": {},
    "string_rules": [],
    "num_types": {},
    "num_rules": [
      {"key" : "*", "type" : "num"}
    ]
  },
  "parameter": {}
}
```

methodは、とりえずinverted_index。
今回は、フィルターや特徴抽出を自作しました（get_AllNounsという関数）。これはもちろん、内部の自然言語的な処理も学習するためであり、知らないで作っちゃったという訳ではありません。

自作した特徴抽出器は、文書中に出現する名詞とその名詞の出現数を返してくれます。ので、num_rulesに、全てのkeyに対して値をそのまま重みとするように設定しました。


### 学習
事前に収集したQiita記事のjsonファイルを読み込み、key値をファイル名、valueを記事本文中の各名詞の出現回数として、Datumにnum_valuesを作成します。
作成したDatumをrecommenderサーバにアップロードして、学習を行います。

```Python3:Introduction_mysite/dailyreport/qiita/yuba_update.py
# -*- coding: utf-8 -*-

from jubatus.recommender import client
# from jubatus.recommender import types
from jubatus.common import Datum

from services import load_json, get_all_files
from mecab import get_AllNouns

SERVER_IP = "127.0.0.1"
SERVER_PORT = 9199
NAME = "recommender_qiita"
DATA_FILE_DIR = "./data/items/"

if __name__ == '__main__':
    # Jubatus recommenderサーバに接続して、
    # DATA_FILE_DIRで指定したディレクトリ内の全ファイル.jsonから
    # keyをファイル名、特徴量をファイルのbodyに出現する名詞とその出現回数の辞書
    # として、サーバにアップロードする

    # サーバに接続
    recommender = client.Recommender(SERVER_IP, SERVER_PORT, NAME)

    for file_name in get_all_files(DATA_FILE_DIR):
        # 指定したディレクトリ内の全ファイルを読み込んで、
        data = load_json(DATA_FILE_DIR, file_name)
        print(data["title"])
        d = Datum(
            get_AllNouns(data["body"])  # body内に出現する名詞と、その出現回数の辞書をDatumとして作成
        )

        # サーバにアップロード
        recommender.update_row(file_name, d)
```

前述しましたが、get_AllNouns関数は、記事本文に出現する名詞とその出現回数を抽出する特徴抽出器です。
{"名詞": 出現回数, ...}という形で出現回数の辞書を返します。


### 類似Qiita記事の推薦
学習済みのrecommenderサーバに対して、日報の本文の各名詞出現回数をクエリとして投げ、recommenderサーバから類似するQiita記事の情報を取得し、返します。
返ってくる情報は、以下です。

* 記事タイトル
* 日報本文とQiita記事の類似度スコア
* 記事URL
* 記事のタグ

```Python3:Introduction_mysite/dailyreport/qiita/yuba_analyze.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
from jubatus.recommender import client
# from jubatus.recommender import types
from jubatus.common import Datum

# デバッグにシェルから読んだ時用と、Djangoから読んだ時用。
if __name__ == '__main__':
    from services import load_json
    from mecab import get_AllNouns
else:
    from qiita.services import load_json
    from qiita.mecab import get_AllNouns

NAME = "recommender_qiita"


def recommend_Qiita(content, recommend_num=4, learned_file_name=""):
    # 日報本文を受け取り、出現する名詞とその出現回数のDatumをクエリとして、
    # Jubatus recomenderから類似記事を取得する。

    # content: 日報本文
    # recommend_num: おすすめする記事数
    # learned_file_name: 事前に保存しておいた学習モデルのファイル名

    # return 類似記事のタイトル、類似度スコア、url、tag情報

    # Jubatus recommenderサーバに接続
    recommender = client.Recommender("127.0.0.1", 9199, NAME)
    if learned_file_name:
        recommender.load(learned_file_name)  # 保存した学習モデルを読み込む

    # 日報本文からDatum作成
    d = Datum(get_AllNouns(content))
    # recommend_numで指定した数、類似記事の情報を取得
    similars = recommender.similar_row_from_datum(d, recommend_num)

    data = []
    for similar in similars:
        # 類似記事のid＝ファイル名から、類似記事のデータをロード
        print("similar:", similar)
        item = load_json(
            os.path.join(
                os.path.dirname(os.path.abspath(__file__)),
                "data/items"),
            similar.id)
        # データから、各情報をappend
        data.append({
            "q_title": item["title"],
            "score": similar.score,
            "url": item["url"],
            "tags": item["tags"]
        })
    return data


if __name__ == '__main__':
    # デバッグ用。

    from pprint import pprint
    recommender = client.Recommender("127.0.0.1", 9199, NAME)

    # with open('') as f:
    # nippo = f.read().split('\n')
    nippo = ["日報テスト", "Pythonが好きだ。" * 100]

    data = recommend_Qiita(nippo[1])
    # print(sr)

    print("nippo ", nippo[0],  " is similar to :")
    for item in data:
        pprint([item["q_title"], item["score"], item["url"], item["tags"]])
```

あとはrecommend_Qiitaを日報アプリから呼んでやって、返ってきたQiita記事の情報をhtml上に表示してやるだけです。

* Introduction_mysite/dailyreport/views.pyの107行目
* Introduction_mysite/dailyreport/templates/dailyreport/daily_detail.htmlの最後の方

に該当部分を記述していますが、Djangoのお話しなので省略。


# 4. 課題
## 推薦精度が悪い
日報と全然関係ないQiita記事が推薦されることがあります。
原因は多々あると思います。

1. 学習データ不足
1. 特徴量が、単純に名詞の出現回数（bug-of-word）のみ
1. アルゴリズムが適切か検証していない

1.を改善するだけでも、だいぶ精度は上がりそう


# 5. まとめ
Jubatus recommenderを用いて、投稿された日報と類似するQiita記事を推薦してくれる機能を開発しました。
名詞の出現回数を特徴量としてQiita記事から学習モデルを生成、投稿された日報と類似する特徴量を持つQiita記事を推薦する、という流れでした。
課題としては、学習データ（Qiita記事）をもっと増やすことでした。


# 参考
* [Jubatus](http://jubat.us/ja/quickstart.html#red-hat-enterprise-linux-6-2-64-bit)
* [Python3で形態素解析エンジンMeCabを使えるようにする(2016年3月版)](http://qiita.com/grachro/items/4fbc9bf8174c5abb7bdd
pip install mecab-python3)
* [Qiitaの投稿をガーッと取得するバッチ処理用QiitaAPI Pythonラッパー](http://qiita.com/tag1216/items/7e23630d97293e35ea4c#_reference-6a490306bbc181488c3cAPI)