過去の自分のツイートをトピック分析してみた

この記事はklisアドベントカレンダー7日目の記事です。
また私が所属しているAutoscale, Inc.のアドベントカレンダーも兼ねています。

はじめまして、FooQoo [(<span>@FooQoo56</span>)](https://twitter.com/FooQoo56) と申します。
現在slisのM2に所属していてツイートデータを対象にしたデータマイニング手法について研究しています。
また[**Autoscale, Inc.**](https://autoscale.jp/)でスマートで効率的なTwitterアカウント運用ツール[**SocialDog**](https://social-dog.net/)の開発も行なっています。
こんな感じで今の私にとってTwitterは最も身近な存在かなと思ったので、2018年の集大成としてTwitterを題材に記事にさせていただきました。

さてタイトルの通り、今回は**過去の自分のツイートのトピック分析**をやりました。
**ツイート** かつ **トピック分析** 、なんともカビの生えたようなタイトルですがそこはご容赦いただきまして...

# トピック分析について

そもそもトピック分析って何って方のために軽く触れておきます。
トピック分析とは、大量の文書に対してそれぞれの文書に含まれるトピック(例えばスポーツや経済など)を分析することです。
あらかじめラベルが振られていないテキストデータが大量にある場合、それらのテキストを複数の大まかな集合(トピック)にまとめることによって文書を理解する助けになります。
このとき文書に含まれるトピックが何であるかわかってないケースでは、トピック自体をテキストデータから推定してあげる必要があります。
この問題を解決するために、**トピックモデル**と呼ばれるものが提案されました。
このトピックモデルの中で最も有名な手法である[LDA](http://www.jmlr.org/papers/volume3/blei03a/blei03a.pdf)では、文書中の単語が文書に含まれるトピックによって生成されることを確率的なモデルによって表現しています。

今回の記事ではトピックモデルを使って私自身のツイートのトピック分析を行なってみました。
ただ単純にLDAを使ったトピック分析では面白みにかけるので、少し工夫してみます。

# 工夫したところ
今回のエントリーでは以下の点について工夫してみました。

- LDAではなくBiterm topic modelを使いました
    - BTMの最新手法の実装も公開しています！
- 記事をみてくれた方がすぐに実践できるようにstep by stepで紹介します

## Biterm topic model
**BTM**(Biterm topic model)は、ツイートのような文書長の短いテキストに対して、LDAよりも一貫性の高いトピックを抽出することができる手法です。
かねてよりLDAでは短文書データに対して、トピックの質が悪くなっていることが報告されています。[[Tang et al., 2014]](http://proceedings.mlr.press/v32/tang14.pdf)
これはLDAのトピックの推論において、文書ごとの各トピックの出現回数が使われていることが原因の一つであり、短文書データではこれが十分に学習できないためトピックの質が悪くなってしまうと考えられます。
この問題を解決する手法がBTMで、文書をbitermと呼ばれる非順序の2単語対に変換し、それぞれのbitermのトピックをコーパス全体で共有することによって、LDAと比較してトピックが十分に学習されるため、より質の高いトピックを抽出することができるようになります。

今回はこのBTMの推論を行うプログラムを、BTMのオンラインな推論アルゴリズムである**Stochastic collapsed variational bayesian zero** [[Awaya et al., 2017](https://ieeexplore.ieee.org/document/7727629)]で実装しました。
本記事では、この実装を使ってトピック分析を行います。

# ツイートのトピック分析をしてみよう
ここからが本編です。はじめに今回のトピック分析の流れについて説明します。

1. Twitterから自分のツイートを取得する
2. 取得したツイートに対して前処理を行う
3. 前処理を行なったテキストデータのトピックをBTMで分析する

step by stepで進んでいきましょう！

## 1. Twitterから自分のツイートを取得する

APIを使うとOauthの認証が必要でとても手間がかかるため、TwitterのWebページからダウンロードするのが楽チンです。
[Twitterのヘルプページ](https://help.twitter.com/ja/managing-your-account/how-to-download-your-twitter-archive)のガイドの通りに行うと、ツイート履歴の入ったzipファイルがダウンロードできます。

このファイルを解凍したフォルダの中に**tweet.csv**というファイルがあります。
このファイルには、これまでの自分のツイートの本文(**text**)の他に投稿日時(**timestamp**)やツイートした端末(**source**)が記載されているので、そのままトピックの分析には使えません。
そこで次の前処理のstepではこのファイルを読み込み、学習に適した形にした新たなファイルを生成します。

## 2. 取得したツイートに対して前処理を行う

前処理用のスクリプトを[Gist](https://gist.github.com/FooQoo/c028e522d99b3209f58d035a89c802ee)で公開しました。
スクリプトの内容は以下のものとなっています。

```python:pretweet.py
'''
requirement : janome, pandas
'''

import re
from janome.tokenizer import Tokenizer
import unicodedata
from html import unescape
import pandas as pd

class Pretweet(object):
    def __init__(self):
        self.tokenizer = Tokenizer()
        
    def cleaning(self, text):
        text = ' ' + unescape(text) + ' '

        stopwords = []
        
        if text[1:3] == 'RT':
            stopwords.append('RT')

        stopwords += re.findall(r'\s@[a-zA-Z1-9]+\s', text)
        stopwords += re.findall(r'\s#\w+\s', text)
        stopwords += [g[0] for g in re.findall(r'((https?|ftp)(:\/\/[-_\.!~*\'()a-zA-Z0-9;\/?:\@&=\+\$,%#]+))', text)]
        
        return re.sub('(' + '|'.join(stopwords) + ')', '', text)[1:-1]
        
    def annotation(self, text):
        return [(token.base_form,  token.part_of_speech.split(',')) for token in self.tokenizer.tokenize(text)]
    
    def normalization(self, text):
        return unicodedata.normalize("NFKC", text)
        
    def is_not_my_stopword(self, word, attr):
        if attr[0] == '名詞' and attr[1] in ['固有名詞', '一般',  'サ変接続']:
            if (re.match(u'[一-龥ぁ-んァ-ンa-zA-Zａ-ｚＡ-Ｚ1-9１-９]', word) and len(word) >= 2) or (re.match(u'[一-龥]', word) and len(word) == 1):
                return True
        
        return False
                
    def segmentation(self, text, cleaning=True, cleaning_pos=True, normalization=True):
        text = self.cleaning(text) if cleaning else text
        
        segment = []
        for word, attr in self.annotation(text):
            
            word = self.normalization(word) if normalization else word
            
            if cleaning_pos and self.is_not_my_stopword(word, attr):
                segment.append(self.normalization(word))
                
        return ' '.join(segment)
    
if __name__ == "__main__":
    pretweet = Pretweet()
    
    lines = pd.read_csv('tweets.csv', encoding = 'utf-8').text.tolist()
    
    with open('processed.txt', 'w', encoding='utf-8') as f:
        for line in lines:
            f.write('{}\n'.format(pretweet.segmentation(line)))
````

こちらはオリジナルの前処理で以下のストップワードをフィルタリングして、単語をスペースで分ち書きにしたテキストファイルを出力しています。

**ストップワード一覧**

- ユーザ名(<span>@</span>username)・ハッシュタグ(<span>#</span>hashtag)・リツイートタグ(RT)
- URL
- 固有名詞・一般名詞・サ変接続名詞以外の品詞を持つ単語
- ひらがな・カタカナ・アルファベット・数字・漢字以外の文字が含まれる単語
- ひらがな・カタカナ・アルファベット・数字のいずれかで構成され単語長が1のもの

要するに、固有名詞・一般名詞・サ変接続名詞のひらがな・カタカナ・アルファベット・数字・漢字のいずれかを含む単語のみ考慮する感じですね！(ただし漢字以外の場合は単語長1のものを除く)

形態素解析のライブラリは有名なMecabではなく[janome](http://mocobeta.github.io/janome/)を採用しました。
こちらは `pip install janome` で簡単にinstallでき、導入コストが極めて低くオススメです！

## 3.前処理を行なったテキストデータのトピックをBTMで分析する

ついに、トピック分析にたどり着きました。
今回の記事に執筆にともない、**pybtm** という実装を[GitHub](https://github.com/FooQoo/pybtm)で公開しました！

`run.sh` のスクリプトの変数 `input_dir` に先ほど前処理を行なったツイートのパスを入力し、このファイルを実行することでトピック分析を行うことができます。

```sh:run.sh
# run an toy example for BTM
mkdir -p ./output
# 以下に前処理したファイルのパスを記載
input_dir=path/to/your/file
output_dir=./output/

b2w_dir=./output/b2w.txt
i2w_dir=./output/i2w.txt
biterms_dir=./output/biterms.txt
num_topic=20
minibatchsize=10
iteration=100

echo "================= Index Docs ==============="
python indexDocs.py $input_dir $output_dir
wc -l ./output/*

echo "=============== Topic Learning ============="
python train.py $b2w_dir $i2w_dir $biterms_dir $num_topic $minibatchsize $iteration
```

それでは私のツイートを使った実行結果を見てみましょう。

```
$ time sh run.sh
================= Index Docs ===============
   16175 ./output/b2w.txt
   16175 ./output/biterms.txt
    4109 ./output/i2w.txt
   36459 total
=============== Topic Learning =============
Training progress 100%

0.063 [('男', 0.046), ('音楽', 0.034), ('人', 0.025), ('演奏', 0.023), ('飯', 0.023)]

0.058 [('人', 0.056), ('データ', 0.018), ('利用', 0.012), ('パズドラ', 0.012), ('研究', 0.012)]

0.055 [('自分', 0.062), ('意味', 0.019), ('友人', 0.016), ('久々', 0.014), ('予定', 0.013)]

0.055 [('アニメ', 0.046), ('神', 0.024), ('TV', 0.024), ('主題歌', 0.02), ('OP', 0.019)]

0.053 [('マン', 0.024), ('徳', 0.022), ('定期', 0.015), ('amp', 0.014), ('例', 0.014)]

0.051 [('曲', 0.048), ('闇', 0.018), ('恐竜', 0.017), ('親', 0.014), ('応援', 0.013)]

0.051 [('先生', 0.048), ('最高', 0.04), ('企画', 0.014), ('月', 0.014), ('プレゼン', 0.013)]

0.051 [('天気', 0.016), ('時', 0.015), ('紹介', 0.015), ('本舗', 0.014), ('HOME', 0.014)]

0.051 [('ライブ', 0.042), ('大学', 0.032), ('授業', 0.024), ('一般', 0.02), ('発売', 0.02)]

0.05 [('ライブ', 0.031), ('曲', 0.028), ('キクチリョウタ', 0.021), ('出演', 0.02), ('月', 0.016)]

0.049 [('研究', 0.039), ('話', 0.025), ('課題', 0.022), ('汗', 0.019), ('ニュース', 0.017)]

0.048 [('カラオケ', 0.022), ('相手', 0.018), ('性格', 0.017), ('歌', 0.016), ('仕事', 0.016)]

0.047 [('レベル', 0.031), ('友達', 0.022), ('バイト', 0.022), ('人', 0.021), ('研究', 0.021)]

0.047 [('汗', 0.031), ('あと', 0.027), ('件', 0.025), ('麺', 0.018), ('声', 0.016)]

0.047 [('気', 0.035), ('番組', 0.025), ('心', 0.018), ('女', 0.018), ('人', 0.014)]

0.047 [('高専', 0.028), ('月', 0.022), ('気持ち', 0.02), ('人', 0.018), ('理解', 0.018)]

0.047 [('謝罪', 0.025), ('海外', 0.023), ('本気', 0.023), ('指導', 0.021), ('人間', 0.021)]

0.046 [('list', 0.022), ('ウナギ', 0.02), ('出演', 0.019), ('覚醒', 0.018), ('テレビ', 0.017)]

0.046 [('勉強', 0.027), ('ゅら', 0.026), ('曲', 0.018), ('ツイート', 0.018), ('CM', 0.015)]

0.038 [('fake', 0.027), ('企業', 0.019), ('店員', 0.019), ('福', 0.018), ('検討', 0.018)]

sh run.sh  27.48s user 0.67s system 96% cpu 29.155 total
```

およそ4000ツイートに対して、30秒弱で処理が終わりました。
それぞれの行の一番左の値が各トピックの全体に対する割合、単語のタプルの右の値がそのトピック中に単語が生成される確率(簡単に言えばトピックに占める単語の割合)になっています。
全体をみた感じだと、わかりやすいトピックもあればよくわからないトピックも含まれているので、引き続き実装も見直していかないとなぁという気持ちになっています。

わかりやすい例だと、
`0.055 [('アニメ', 0.046), ('神', 0.024), ('TV', 0.024), ('主題歌', 0.02), ('OP', 0.019)]` 
は何かしらの神アニメの主題歌について語っているツイートが存在していたのでしょうか。

他には、
`0.05 [('ライブ', 0.031), ('曲', 0.028), ('キクチリョウタ', 0.021), ('出演', 0.02), ('月', 0.016)]` 
では私の好きなシンガーソングライターのキクチリョウタさん([<span>@ryotakikuchi</span>] (https://twitter.com/ryotakikuchi))に関するツイートがいくつか存在していたということがわかりました。

あとは、**ライブ**・**演奏**・**音楽**・**曲** など大学時代に音楽活動をしていた名残のトピックがいくつかありました。これまで自分が生きてきた足跡がトピックという形で残っていたんですね(しみじみ

# まとめ

今回の記事では過去の自分のツイートのトピック分析を行いました。
トピック分析では、ツイートのような文書長の短いテキストデータに強い**BTM**を使い、それにともなってBTMの実装とツイートの前処理を行うスクリプトを公開しました。

分析結果はわかりやすいトピックからわかりにくいものまであり中途半端にはなってしまいましたが、思いがけず自分の人生について振り返ることができたので悪くはなかったと思います。

実装はGitHubやGistで公開しているのでみなさんも興味があれば是非自分のツイートのトピック分析をしてみてください！