社内Slackをテキスト解析してみた話


※この記事は[VOYAGE GROUP エンジニアブログ Advent Calendar 2014](http://tech.voyagegroup.com/archives/7941760.html)の14日目の記事です．

こんにちは，VOYAGE GROUPにて新卒マエショリストをさせて頂いております[@a_macbee](https://twitter.com/a_macbee)です．
Advent Calendar何を書こうかと散々迷った挙句，そうだSlackのテキストをちょちょっと解析してまとめたら面白いんじゃないかと思い立ち，色々頑張った挙句特に面白いことは出来なかったけど，とりあえずこの汗と涙の結晶を紹介しようかなと思います（前置きが長い）

## Slackのdumpデータについて
 以下のようなURLにアクセスすることで，権限さえあれば簡単にデータをexportすることができます．
https://[team名].slack.com/services/export

今回は「2014/12/09 22:03」時点でのSlackデータをexportして利用しています．(圧縮状態で34.3MB)
データはチャンネル毎にフォルダ分けされており，日毎にjsonファイル形式で保存されています．

```data/baseball/2014-11-03.json
[
    {
        "type": "message",
        "user": "U025RMJ6L",
        "text": "\u4eca\u65e5\u306f\u91ce\u7403\u898b\u308b\u4f1a\u306e\u65e5\u3060\n<https://****>",
        "ts": "1414463972.000141"
    },
    {
        "type": "message",
        "user": "U027MNCC5",
        "text": "jpi \u85e4\u6ce2",
        "ts": "1414464310.000142"
    },
    {
        "text": "<*******.png>",
        "username": "jewelpet",
        "bot_id": "B02C7FLJB",
        "attachments": [
            {
                "from_url": "*******.png",
                "fallback": "426x640px image",
                "image_url": "*******.png",
                "image_width": 426,
                "image_height": 640,
                "image_bytes": 77257,
                "id": 1
            }
        ],
        "type": "message",
        "subtype": "bot_message",
        "ts": "1414464310.000143"
    },
    ...
]
```

各チャンネルの情報やユーザの情報は，それぞれ`channels.json`，`users.json`ファイルに格納されています．


## 前処理
もちろんデータをそのまま使おうとすると大変なことになるので，前処理をする必要があります．
全部の発言データ(※`text`キーに対応する)をNFKCで正規化したり特殊文字除いたりといった一般的な前処理に加えて，Slackデータ特有の前処理も行いましたので，以下にまとめます．

- チャンネル／ユーザの選別
	- チャンネル
		- 既にアーカイブされているチャンネルは扱わない
		- ユーザが2名以下のチャンネルは扱わない
	- ユーザ
		- 既に削除されているユーザの発言は扱わない
		- Botの発言は扱わない
- Slackにjoinしたり，topicを変更した際に自動的に書かれるメッセージは扱わない (e.g. `@amacbee has joined the channel`)
	- Tips: それらのデータには `subtype` キーが設定されています
- URLを削除したり，user_idやchannnel_idを名前に置き換え
	- Tips: データ中では`< ... >`に囲まれて出てきます
	- Tips: `<@user_id>`，`<#channel_id>`，それ以外はURLと判断して削除
- Botに何かしらの処理を行わせる命令メッセージは削除
	- 社内には，愛されBot☆jewelpetが存在します（参考：[Slackで役に立たないBotを運用するときの知見とか](http://mint.hateblo.jp/entry/2014/12/11/233922)）
- \`\`\`...\`\`\`で囲まれたメッセージは，2chテキストチックなメッセージであったり，プログラムのコードであったりする事が多いため除く
- 複数行にまたがる発言は，1行1メッセージとして扱うことにする

こんな感じでようやく前処理が終わりました．
以上のような処理を挟んでも，以下の様な発言は扱えていません＼(^o^)／（出典：社内Slack #photo）

```
ﾊﾟｼｬｯ　　　　ﾊﾟｼｬｯ
　　　ﾊﾟｼｬｯ
　　　 　 ∧_∧　ﾊﾟｼｬｯ
ﾊﾟｼｬｯ　  (　 )】Σ
.　 　 　/　 /┘ 　　ﾊﾟｼｬｯ
 　　　 ノ￣ゝ
　　　　　　　　ミ∧_∧　ﾊﾟｼｬｯ　ﾊﾟｼｬｯ
　　　　　　　ミ (/【◎】
　　　　　.　 ミ　/　 /┘
　　　　　　　　ノ￣ゝ
```

最終的に，320,165発言が集められました．
結構な重労働だったため，この時点で私のやる気は風前の灯火です←

## 形態素解析処理
### 辞書の用意
さて，集めた発言を形態素に分割します．今回は，形態素解析器としてMeCabを用いることにしました．
SlackではWeb上の口語文体や顔文字が非常に多く使われているため，MeCabをデフォルトのままで利用すると形態素分割精度が恐ろしいほど悪くなります．
そこで，今回は以下の記事を参考に顔文字辞書，はてなキーワード辞書，2ch辞書を解析用辞書として新たに追加してみたり，デフォルトのipadicに対していくつかパッチをあててます（Wikipedia辞書もやろうと思ったけどめんどくさｋｒｙ）

- 参考：[wikipediaのデータや顔文字辞書からmecabのユーザ辞書を作成するフレームワーク](http://blog.livedoor.jp/techblog/archives/65828235.html)
- 参考：[部首タグ付き顔文字辞書](http://www.haroperi.info/emoticon/annotated.html)
- 参考：[単語の追加方法](http://mecab.googlecode.com/svn/trunk/mecab/doc/dic.html)
- 参考：[はてなキーワードからMeCabのユーザ辞書を作る](http://d.hatena.ne.jp/stellaqua/20100326/1269589253)

### 単語の登録
チャンネル名，ユーザ名，一部の名詞（e.g. ジュエルペット(※社内Botの名前)，VOYAGE GROUP）を新しい単語として辞書に追加しています．

## Slackデータをテキスト解析
ようやく解析処理が出来る！長かった！！辛かった！！！

### word2vecを使ってみる
今年の前半に大いに盛り上がったword2vecを使って，Slackデータを解析してみます．
word2vecとは，ニューラルネットワーク（CBOW，Skip-gram）のオープンソース実装で単語同士の意味計算を可能にしたツールです．例えば，"king-man+woman"を計算すると，"queen"という単語が得られたりします．より詳細な解説については，以下のサイト等を御覧ください．

- 参考：[自然言語処理に新風を巻き起こしたWord2Vecとは何か](http://business.nikkeibp.co.jp/article/bigdata/20141110/273649/)
- 参考：[自然言語処理の最新手法"word2vec"で艦これ加賀さんから乳を引いてみる](http://antibayesian.hateblo.jp/entry/2014/03/10/001532)
- 参考：[青空文庫のデータを使って、遅ればせながらword2vecと戯れてみた](http://tjo.hatenablog.com/entry/2014/06/19/233949)

早速使ってみましょう！
今回は，Googleが提供するword2vecのツールを利用します．

```
$ svn checkout http://word2vec.googlecode.com/svn/trunk/
$ cd trunk
$ make
```

Slackデータを利用して学習を行います．データは1行1メッセージ形式になっており，これを形態素解析器を利用して分かち書きし，messages.wakatiという名前で保存しています．

```
$ cat messages.txt | mecab -u 追加した辞書達 -Owakati > messages.wakati
```

早速学習してみます！

```
$ ./word2vec -train messages.wakati -output vectors.bin -cbow 1 -size 200 -window 8 -negative 25 -hs 0 -sample 1e-4 -threads 20 -binary 1 -iter 15
```

学習は大体1〜2分程度で終わります．爆速！
それぞれのオプションの意味は以下の通リです．

- train: 学習用データとなるファイル名
- output: 学習結果を出力するファイル名
- threads: 並列で行う学習の数
- binary: =1の場合，出力がバイナリ形式になる
- cbow: =0の場合，学習モデルにCBOWではなくSkip-gramを利用する
- size: ベクトルの次元数
- window: 文脈として考える単語数
- hs: =1の場合，高速化手法として階層的ソフトマックスを利用
- sample: =1e-3の場合，頻出語をランダムに削除する．削除頻度は高めになる

### distance
`distance`コマンドを利用して，ある単語に類似した（=コサイン距離が近い）単語を得ることが出来ます．

```
$ ./distance vectors.bin
```

実際に実行した結果を見てみましょう．
VOYAGE GROUP内にあります，社内BAR「ajito」に類似した単語は以下になります（括弧内はコサイン距離です）

```
1. ajiting (0.576895)
2. オーストラリア (0.556273)
3. アンタークティカ (0.552122)
4. ジパング (0.549927)
5. アジト (0.547126)
```

「ajiting」が一番類似しているという結果になりました．社内では，AJITOで何かしらの活動をすることを #ajiting とよんでいます． see also: https://twitter.com/ajiting
2位から4位までは社内の会議室名がランクインしました．結構うまくとれている気がします．

社内のアイドルBot「jewelpet」に類似した単語は以下になります．

```
1. 馬鹿 (0.422769)
2. jewelbot (0.421701)
3. ジュエルペット (0.415403)
4. パカー (0.386684)  # jewelpetの口癖です
5. wwwwwwww (0.375883)
```

愛され・・・てます、ね！

### word-analogys
word-analogyツールを利用して，例えば，appleと林檎の関係が，orangeと何の関係に似ているのかといった類推を行うことが出来ます．訳語探しなどに便利です．

```
$ ./word-analogy vectors.bin
```


実際に実行した結果を見てみましょう．
営業にとっての勉強は，エンジニアにとって何にあたるのでしょうか？

```
1. tips (0.505426)
2. 交流 (0.437485)
3. 懇親 (0.428733)
4. 知る (0.401476)
5. 読書 (0.374354)
```

何らかのtipsだったり，交流/懇親がエンジニアにとっての勉強らしい．なるほど．

ついでに私 (a_macbee) にとっての酒は，VOYAGE GROUPにとっての何？・・・というのを出した結果が以下になります．

```
1. 水 (0.457294)
2. 品切れ (0.449639)
3. 樽 (0.443636)
4. 飲む (0.432374)
5. 飲め (0.427002)
```

こうして，自分自身ですら知らなかった新しい知見を教えてもらえたりします←
word2vec面白いですね！

### おまけ
Pythonの`gensim`というパッケージを利用すると簡単にword2vecで遊べます（以下にサンプルコードを載っけておきます）

```src/word2vec.py
from gensim.models import word2vec
import os

messages = word2vec.Text8Corpus(os.path.join(os.path.dirname(__file__), "../data/messages.wakati"))
model = word2vec.Word2Vec(messages, size=200)

def calculate_similalities(positive, negative, topn=5):
    results = model.most_similar(positive=positive, negative=negative, topn=topn)
    for result in results:
        print("単語: {0}, コサイン距離: {1}".format(result[0], result[1]))
```

## 最後に
解析が手抜きっぽさ出ててすみません．時間内に投稿するためにはこれが限界でした＼(^o^)／ｗ

前処理サボったり形態素解析器デフォルトのままで使ったりしてもある程度面白い結果出るんじゃないかな〜と思っていた時代が私にもありました←
でも世の中そんなにうまくいかないですね．前処理超大事．
9割9分の労力が前処理に割かれた感じになりました（あるある
（※気力がわいたら）Slackのデータを前処理する部分をツール化するかもしれません（※気力がわいたら）

明日の担当は同期で一番面白いエンジニア[@blackstar240](https://twitter.com/blackstar240)です！
私の100倍面白い記事を書くと思いますので，皆様お楽しみに！！！