共起関係を力学モデルで描画

Twitterの投稿文の共起関係を分析して力学モデルで表示してみました。
単なる数値の羅列ではなく、グラフ表示することで視覚的にイメージできるようにしてみました。
本稿では次の日付のTwitter投稿文を対象としました。

- 2011年3月11日
- 2019年7月18日

# 共起とは
　自然言語処理における共起とは、ある単語Aとある単語Bが同時に出現する場合に、単語Aと単語Bは共起関係にあるといいます。共起関係にある単語は何らかの意味的な近さがあるとみなすことができます。
　ただし、単語Aと単語Bが極端に離れているとその関係性は薄れてしまうので、たとえば、単語n件離れている場合などと定義することもあります。

　本稿では、Twitterの投稿（＝高々140文字）を対象とするので、1投稿中に出現した単語同士に共起関係があることにします。（改行や句読点は無視して、同一投稿にある単語は共起関係だと判断します）

# 力学モデルとは
　n件の単語の関係（例えば共起頻度を元に計算された共起強度）からノード間の距離を定義して、2次元上にグラフ表現しようとしても、正確に描画することはできません。どこかにゆがみが出てきてしまいます。
　それを可能な限り元の関係性を保持して2次元上に表現する方法の一つとして力学モデルによる描画があります。
　単語間の関係の強さに対して、ばね・電磁力・重力などによる計算を施して、安定する位置を決めています。
　詳しくは次の説明などを参照してください

- [力学モデル (グラフ描画アルゴリズム) - wikipedia](https://ja.wikipedia.org/wiki/%E5%8A%9B%E5%AD%A6%E3%83%A2%E3%83%87%E3%83%AB_(%E3%82%B0%E3%83%A9%E3%83%95%E6%8F%8F%E7%94%BB%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0))

# 対象とするデータ
　ここで扱うのはTwitterのAPI「Sample realtime Tweets」を使って取得したデータです。（任意のユーザのタイムラインに出てきたデータではありません）
　全Tweetの一部（1%以下）のデータを収集することができます。定量的な分析には向きませんが、定性的な分析をするには十分な量のデータを得ることができます。
　収集には、次のコードを使っています。

- [michihosokawa/GetTwitterStream - Github](https://github.com/michihosokawa/GetTwitterStream)

　APIの公式ドキュメントは次になります

- [Sample realtime Tweets](https://developer.twitter.com/en/docs/tweets/sample-realtime/api-reference/get-statuses-sample)

　このAPIでは世界中のデータが流れてきます。日本語の情報を見たいので、日本語だけを抜き出します。日本語文を抜き出しす方法は次の記事を参照してください。

- [Pythonで文の日本語判定を行う - qiita](https://qiita.com/MichiHosokawa/items/83da0ad58905c79c867d)

# 形態素解析
　以下のモジュールを使用しました。辞書は最終的にはneologdを使用しました。

- mecab-0.996
- mecab-ipadic-2.7.0-20070801
- mecab-ipadic-neologd  (2019-07-22版)

形態素解析システムMeCabのインストールに関しては次の記事が参考になります。

- [MeCabとNEologdのインストール方法(CentOS6/CentOS7)](https://qiita.com/bokotomo/items/3e9d78a4bc94c0314346)
- [MeCabをUTF-8でインストールしたい。](https://qiita.com/junpooooow/items/0a7d13addc0acad10606)

# 共起データの作成手順
1. 「Sample realtime Tweets」APIでTwitterの投稿を収集しておきます。  
※Streamデータなので後から特定日時のデータを収集することはできません。  
※本稿では事前に取得していたデータを使っています。  
2. 得られたデータから本文のみを抜き出します。  
さらに、本文に含まれる不要な情報（@で始まるユーザ名、URL、タグなど）を除去します。
これらが残ったままだと形態素解析結果にノイズが残ってしまい、正しい共起情報が抽出できません。  
※アルファベット文字列を強引に形態素解析した結果として出現する単語（未知語/未登録語）は、Twitterの投稿文140文字に対してかなりの割合を占めることになるので無視できません。可能な限り除去します。  
3. 各行（各投稿）ごとに形態素解析を行い、名詞のみを取り出します。また、名詞の中でも不要なもの（数詞など）は除外します。そのほかに明らかに誤解析になる単語があれば、除外しておきます。  
4. 単語の頻度、共起の頻度を求めます。  
5. 共起強度を示す値を計算します。  
例えば、出現頻度1000件の単語同士の共起頻度100件と、出現頻度1万件の単語同士の共起頻度100件を同列に扱うわけにはいきません。そこで、単純な共起頻度ではなく、共起強度を示す指標としてJaccard係数を採用しました。  
また、すべての情報をグラフ表示しても雑然とするだけなので、閾値を設定してそれより小さい値は表示対象外とします。単語の頻度や共起強度で判定しています。  
※この辺りは、対象とするコーパスや解決すべき問題によって、個別の調整が必要になります。  
6. JSONデータに変換し、Javascriptに埋め込みます。

# 力学モデルで描画
ここでは[d3.js](https://github.com/d3/d3/wiki)の[Force-Directed Graph](https://observablehq.com/@d3/force-directed-graph)を使います。
d3.jsの具体的な使い方は、次を参考にしました

- [d3.jsでスゴイっぽい図(force layout)を作ってみたら思ったより簡単だった件](https://qiita.com/shoki_kitajima/items/34ad6e2209fde5b4dedc)

※なお、d3.jsのバージョンの違いによって、力学モデルの仕様が違うので注意が必要です。

## ソースコード
ソースコードは次に格納してあります。

- [共起関係を力学モデルで描画 - Github](https://github.com/michihosokawa/ForceDirectedGraphWithCollocation)

# 描画例

## 20110311
![20110311.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132967/0205da63-84e7-3d92-2828-2c2537abc6f4.png)

2011年3月11日14時46分18秒（日本時間）に[東日本大震災](https://ja.wikipedia.org/wiki/%E6%9D%B1%E6%97%A5%E6%9C%AC%E5%A4%A7%E9%9C%87%E7%81%BD)が発生しました。
Twitter上では、その直前までいつもと同じやり取りが行われていましたが、その時刻以降内容は一変します。
上記のグラフを見てもわかるようにほかの情報が入ってくる余地はありません。
いくつかのクラスタに分かれているのが見て取れます。これは、数値列眺めただけではわからない情報で、可視化することによって認識できたことです。もちろん、これらを機械的にクラスタ分析する手法は存在しますが、本稿では扱いません。

## 20190718
![20190718.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132967/6c5c4e37-82e3-7565-9be6-c98a73cae4d2.png)

2019年7月18日の午前10時半ごろ、[京都アニメーション放火事件](https://ja.wikipedia.org/wiki/%E4%BA%AC%E9%83%BD%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E6%94%BE%E7%81%AB%E4%BA%8B%E4%BB%B6)が発生しました。
こちらは、地震のように直接体感できる事象ではないので、多くの人たちはニュース情報として耳にしたのだろうと推測できます。
グラフを見ると、明らかなクラスタが存在しています。他方のクラスタは、連日発信されている情報のようで、前後の日付でも見ることができました。

# 備考

現状、対象単語を出現頻度の上位300件に限定しています。もう少し単語数を増やすことで、他のクラスタも見えてくるかもしれません。
また、連日同じような投稿が頻出しているような場合には、何らかの操作で除外した方がよいのかもしれません。

このあたりをまた考えてみたいと思います。