dots.のタグ付け

皆さんこんにちは。@best_not_bestです。
私は現在、[dots.](http://eventdots.jp/)の開発を担当しています。今回はこのシステム内の形態素解析を使ったタグ付けを説明したいと思います。

## dots.とは？

以下の記事を参考ください。
[PHP - Laravel 5でサービスをフルリニューアルした話 - Qiita](http://qiita.com/best_not_best/items/c07115529dc688819833)

## タグ付け

タグ付けについても上記記事に概要が書かれています。
現在は、イベント本文をphp-mecabで形態素解析して抽出した単語をタグとして付けています。辞書ファイル（の元となるCSVファイル）には

```
ANDROID,,,-7408,名詞,一般,*,*,*,*,ANDROID,,
ANSIBLE,,,-7408,名詞,一般,*,*,*,*,ANSIBLE,,
APPGOAT,,,-7408,名詞,一般,*,*,*,*,APPGOAT,,
...
```

のように、内部のツールでタグとして登録された単語が登録されています。
タグ一覧は以下のページをご覧ください。
[タグ一覧（イベント）｜IT勉強会・セミナー・イベント情報 - dots. [ドッツ]](http://eventdots.jp/event/taglist)

抽出された単語がただ付くだけですので、イベントに趣旨から外れた単語が付くこともあります。
今回の記事では、いくつかの手法を使ってより良い効果が出るか（イベントに相応しいタグが付くか）を検証していきたいと思います。

## 検証する手法

- TF-IDF  
cf. [形態素解析と検索APIとTF-IDFでキーワード抽出](http://chalow.net/2005-10-12-1.html)

## 環境

- MacBook Pro 15-inch
- OS X Yosemite 10.10.5
- Python 2.7.9
  - mecab-python 0.996
  - scikit-learn 0.17

## 前置き

今回対象とするイベントは「登録元がdots.」のイベントに絞っています。
（http://eventdots.jp/calendar で「d.」マークが付いているイベント）
それらの「イベント内容」の部分の文章を解析対象にしています。
イベント内容はテキストファイルとしてローカルに保存しておきます。

## ディレクトリ構成

```
extract_words.py
calc_tf_idf.py
dic
 └─ morphological_keyword.dic
event_description（イベントごとのイベント内容（<イベントID>.txt））
 ├─ 103839.txt
 ・・・
 └─ 95484.txt
event_description_words（イベント内容から抽出された単語（<イベントID>.txt））
 ├─ 103839.txt
 ・・・
 └─ 95484.txt
```

## 単語を抽出

イベント内容からHTMLタグを除去後、形態素解析し、「名詞」かつ「文字数が2つ以上」の単語を抜き出します。辞書は前出の物を使用しています。

```py:extract_words.py
#!/usr/bin/env python
# -*- coding: UTF-8 -*-

import os
import MeCab
import sgmllib

INPUT_DIR_PATH = './event_description/'
OUTPUT_DIR_PATH = './event_description_words/'

class Stripper(sgmllib.SGMLParser):
    def __init__(self):
        sgmllib.SGMLParser.__init__(self)

    def strip(self, some_html):
        self.theString = ""
        self.feed(some_html)
        self.close()
        return self.theString

    def handle_data(self, data):
        self.theString += data

def split_to_words(text):
    tagger = MeCab.Tagger('-u ./dic/morphological_keyword.dic')
    mecab_result = tagger.parse(text)
    info_of_words = mecab_result.split('\n')
    words = []
    for info in info_of_words:
        if info == 'EOS' or info == '':
            break

        info_elems = info.split('\t')
        if not info_elems[1].startswith('名詞'):
            continue

        if len(unicode(info_elems[0], 'utf-8')) <= 1:
            continue

        words.append(info_elems[0])
    return words

if __name__ == '__main__':
    stripper = Stripper()
    files = os.listdir(INPUT_DIR_PATH)
    for file in files:
        input_data = open((INPUT_DIR_PATH + file), 'r')
        output_data = open((OUTPUT_DIR_PATH + file), 'w')
        html = input_data.read()
        text = stripper.strip(html)
        words = split_to_words(text)
        output_data.write('\n' . join(words))

        input_data.close()
        output_data.close()
```

以下のコマンドで実行します。

```
$ python extract_words.py
```

## TF-IDFを計算

```py:calc_tf_idf.py
#!/usr/bin/env python
# -*- coding: UTF-8 -*-

import os
from sklearn.feature_extraction.text import TfidfVectorizer

INPUT_DIR_PATH = './event_description_words/'

class Tfidf:
    def token_dict(self):
        dic = {}
        files = os.listdir(INPUT_DIR_PATH)
        for file in files:
            shakes = open((INPUT_DIR_PATH + file), 'r')
            text = shakes.read()
            lowers = text.lower()
            dic[file] = lowers
            shakes.close()
        return dic

    def tokenize(self, text):
        words = text.rstrip().split("\n")
        return list(set(words))

    def analyze(self):
        dic = {}
        token_dic = self.token_dict()
        tfidf = TfidfVectorizer(tokenizer = self.tokenize, max_df = 30)
        tfs = tfidf.fit_transform(token_dic.values())
        feature_names = tfidf.get_feature_names()

        i = 0
        for k, v in token_dic.items():
            d = dict(zip(feature_names, tfs[i].toarray()[0]))
            score = [(x, d[x]) for x in sorted(d, key = lambda x:-d[x])]
            dic[k] = score[:50]
            i += 1

        return dic

if __name__ == '__main__':
    tfidf = Tfidf()
    dic = tfidf.analyze()
    for txt in dic:
        print txt
        words = []
        for word in dic[txt]:
            words.append(word[0])

        print ' ' . join(words)
```

以下のコマンドで実行します。

```
$ python calc_tf_idf.py
```

## 結果

いくつか抜粋します。

http://eventdots.jp/event/265074

```text:265074.txt
データ・ビジュアライゼーション リエンジニアリング 実演 正之
セールスコンサルティングマネージャー ビジュアライゼーション 並木
エキスパート 啓蒙 imageboxleft 最先端 世界中 疑問 難易 そこ 要望
エバンジェリスト ヒント 不定期 0068 可視 トレンド 体験 membership
木曜 datascientist 重要 tableau 創出 実務 90 設立 多く 協会 製品
メンバー 成長 サイエンティスト 時代 社団 ビッグ お客様 簡単 支援
コミュニケーション サイエンス ビジネス 作成 同意 ユーザー
```

http://eventdots.jp/event/238419

```text:238419.txt
スター 関東学院大学 通販 客員 義介 albert 明治大学 階層 非常勤 人間 04
山川 会長 初級 総合 講師 不定期 グローバル 研究所 程度 0068 大学院
membership 木曜 datascientist 具体 大手 創出 実務 協会 基礎 問題
サイエンティスト 時代 社団 ビッグ 研究 コミュニケーション サイエンス
ビジネス 同意 法人 クラ opacity 会員 うえ 上記 一般 ソリューション
クエリプラン
```

http://eventdots.jp/event/571126

```text:571126.txt
外形 soa locari infrasturecture 野瀬 松木 package mercari マスコット
かい yoshifumiyamaguchi 中国語 シス tatsuhiko greg nginx
オライリー・ジャパン 拓也 lua mackerel 語学 erlang 敏感 82148 kubo
shogo オーム社 中国 3960 担兼 モバイルオンラインゲーム コミケ check
roseberry ｜@ tenntenn gopher engineering 4437 訳書 cubicdaiya 上田
継続 山口 ,　# 社内外 youtube songmu management ファイヤーサイドチャット
```

http://eventdots.jp/event/572909

```text:572909.txt
カフェイン カフェラテ 優雅 最低限 店員 好物 store シルバーアクセ スペック
エレキ 感じ strong アクセサリー ギター ・アプリ 実機 radius 料理 desc
雑貨 挫折 不足 小野寺 140 85 project 独学 隆史 習得 メタル wifi 本格
お菓子 地図 大丈夫 表示 コア フリー playground kit 前回 app どこ 転職
edited 趣味 xcode 電源 ec ipad
```

http://eventdots.jp/event/573112

```text:573112.txt
回帰 従来 ブラウザゲーム doraemonsss リックテレコム コスト マイナビ 打破
cocos |@ webgl )） 清水 tks スマートフォンゲーム リッチ shimizu アプ )」
ブラウザ サル コン unity クロス スムーズ 歴史 ネイティブ 表現 将来 移行
エンジン 近年 ほか ガイド ケース 実行 執筆 android 書籍 パフォーマンス
学習 iphone windows 手法 注目 ノート 大手 mac javascript 問題
```

http://eventdots.jp/event/567494

```text:567494.txt
node 渋谷 js ソリューション クエリプラン 明確 tatakaba 在学 ♪」 日本人
クラウドソリューションアーキテクト 事務 一樹 デザイナー tnaruto -』
tenntenn サイエンス kintone 棟梁 basics 容認 asami 商学部 当時 0050 所持
アンケート マークアップエンジニア second 279 委員 修士 拓生 deferred 和人
三平 引き マクロ キャリア specialist スペシャリスト affiliates 課長 子供
キャリー mattani 方々 和之 中身
```

http://eventdots.jp/event/575693

```text:575693.txt
極上 コワーキングスペース 辣腕 コーディング もくもく 方々 ソリューション
クエリプラン 明確 tatakaba 在学 ♪」 日本人 クラウドソリューションアーキテクト
事務 一樹 デザイナー tnaruto -』 tenntenn サイエンス kintone 棟梁 basics
容認 asami 商学部 当時 0050 所持 アンケート マークアップエンジニア second
279 委員 修士 拓生 deferred 和人 三平 引き マクロ キャリア specialist
スペシャリスト affiliates 課長 子供 キャリー mattani
```

なんかイマイチですね・・・。
この手法はdots.のタグ付けには向いていないのかもしれません。

## 参考

[textmining - 青空文庫の作品から TF-IDF を指標として特徴となる語彙を抽出する - Qiita](http://qiita.com/ynakayama/items/234ad00ae520030217ab)
[Python で HTML タグを取り除く方法](http://www.codereading.com/codereading/python/strip-html-tags.html)