Aidemy Tech Blog

機械学習・ディープラーニング関連技術の活用事例や実装方法をまとめる、株式会社アイデミーの技術ブログです。

Twitterのタイムラインからツイートを取得しトピックモデルを生成してみる

はじめまして、研修生ののっぽです。機械学習の勉強を始めてまだ二ヶ月の未熟者ですが、今回は機械学習を用いた簡単なプログラムの実装をしてみようと思います。


突然ですが、皆さんはTwitterを使用したことがありますか?あるいは他のSNSを用いたことはありますか?
Twitter等のSNSでは大きなアカウントであればあるほどログの流れが早く、後から見返すのが大変です。
そこで、pythonを用いて過去のツイートを大量に取得し過去にどのようなことがツイートされていたのかを大まかに知るプログラムを作ってみます。

 

トピックモデルとは

さて、タイトルにもあるトピックモデルとは一体どういう意味なのでしょうか。
トピックモデルとは簡単に言うと「全ての文章には幾つかの話題があり、文章の中身はその話題の何れかから作られている」というモデルのことです。そしてトピックモデルを生成するとは、各文章中の単語を生成元の話題に対してグループに分け、それらの単語がどれだけの確率でどの話題から生成されたのかを推定することになります。
詳しい解説はAidemyの講座にお任せすることとして、早速ツイートを取得してトピックモデルを生成してみましょう。

ツイートを取得する

ツイートの取得に当たって以下の記事を参考にさせていただきました。
qiita.com

TwitterにはAPI制限というものが存在し、タイムラインからのツイートの取得は200(ツイート/回)×15(回) = 3000ツイートが限界です。そのため、API制限に掛かった場合はプログラムを一時的に停止させる必要が有ります。以下に示したプログラムでは一回ツイートを取得するごとに1分停止することで結果的に制限にかからないようにしています。
また、ツイートを遡るために最後に取得した最も古いツイートのIDを一時的に保持しています。

source = []
for j in range(100):   
    params_a = {"since_id" : str(id1),"count":200, "include_entities":"false", "include_rts":"true", "trim_user":"true", "exclude_replies":"true"} 
    req = twitter.get(url, params = params_a)
    if req.status_code == 200:
        timeline = json.loads(req.text)
        i = 0
        print(str(j)) 
        for tweet in timeline:
            source.append(tweet["text"])
            i = i + 1
            if(i == 200):
                id1 = tweet["id"]-200
    else:
        print("error")        
'''
この部分に追加でアカウントを加える
'''
 time.sleep(60)

(上記のコードはプログラムの一部です。実際にはツイート取得数を増やすために、複数アカウントのタイムラインからツイートを取得しています)

以上のプログラムで20000×アカウント数のツイートが取得できることになります。
しかし、そのままではトピックモデルを形成することはできません。そこで得られたツイート群に対して形態素解析を行い必要な単語のみを抽出します。

ツイートを整形する

ツイートの整形(形態素解析)にはMeCabというライブラリを用います。
以下のコードではparserという関数で一つのツイートの形態素解析を、analyzerという関数でリスト内のツイート全てにparser関数をかけています。

import MeCab
mecab = MeCab.Tagger('mecabrc')
mecab.parse('')

#形態素解析を行う
def parser(text):
    mecab.parse('')
    corplist = []
    node = mecab.parseToNode(text)
    while node:
        if(len(node.surface) == 0):
            node = node.next
            continue       
        if (node.feature.split(",")[0] == u'名詞') and ((node.surface[len(node.surface) - 1].encode("utf-8")[0] >= 0xe3) and (node.surface[len(node.surface) - 1].encode("utf-8")[1] >= 0x81) and (node.surface[len(node.surface) - 1].encode("utf-8")[2] >= 0x80)):
            corplist.append(node.surface)
        node = node.next
    return corplist

#データに直接parserをかける
def analyzer(content):
    token = []
    for i in content:
        token_p = parser(i)
        token.append(token_p)
    return token

words = analyzer(source) #単語リストとして保存

parser関数では形態素解析で得られた単語群の中から条件に合うもの(名詞に分類され、単語の初めの文字が英語でないもの)のみを実際にトピックモデルの生成に使うリストに加えています。

これでトピックモデルを生成する準備は整いました。


トピックモデルの生成

さて、後は得られた単語リストからトピックモデルを生成するだけです。トピックモデルの生成にはgensimと呼ばれるライブラリを使います。
トピックモデルを生成するためにはまず辞書と呼ばれる意味のある頻出単語群の作成と、コーパスという辞書中の各単語にそれらがどれだけの頻度で出現したのかを紐付けるリストの作成が必要になります。
以下のコード中のdictionary.filter_extremesでは上で説明した辞書に単語を加える際の条件を付加しています。今回の例で言えば、文章群全体で二回以上出現した単語でありかつ全体の1%以下の出現率である単語が辞書に加えられます。

作成したコーパスをLDAという種類のトピックモデルを生成する関数に入れてあげることで、指定したトピック数に対して単語群からトピックそのものとトピックに各単語がどれだけの割合で含まれているかを推定することができます(この節の後半でLDAについて説明をします)。

import gensim
from gensim import corpora, models, similarities

#辞書の作成
dictionary = corpora.Dictionary(words)
dictionary.filter_extremes(no_below=2, no_above=0.01)

# コーパスを作成
corpus = [dictionary.doc2bow(text) for text in words]

#トピックモデルを生成
lda = gensim.models.ldamodel.LdaModel(corpus=corpus, num_topics=100)

# 各トピックの出現頻度上位を取得
topic_top = []
for topic in lda.show_topics(-1, formatted=False):
    topic_top.append([dictionary[int(tag[0])] for tag in topic[1]])
        
# 各トピックの出現頻度上位10位をcsv形式で保存
topic_data = pandas.DataFrame(topic_top)
topic_data.to_csv("topic_words1.csv", encoding="utf-8")

さて、それでは生成されたトピックモデルをcsvファイルに保存し確認してみましょう。

LDAとは

結果に移動する前にLDAについて簡単な説明をします。
LDAとはトピックモデルの中でもベイズ推論の考えを用いて文章の集合から各トピックや単語がどのから生成されたかについての分布を計算するモデルです。
以下にすこし詳しい説明を書きます。
n個の単語の集合をW、語彙(つまり単語の種類)の集合をV、文書集合をDとします。
この時、文書集合D中のある文書dは単語wの集合として表されます。また文書集合DにはK個のトピックφによって構成されるトピック集合Φが存在しトピックφは語彙vの異なる比率によって構成されています。
また文書dそのものはトピックを幾つか含んでいるのでトピックφが文書dを占める比率をθとおきます。
最後に単語がどのトピックに属するかを示す変数であるトピック割り当てZを設定します。
トピックの総数が決まっている時、単語w及びトピック割り当てはカテゴリ分布(ベルヌーイ分布を多次元に拡張したもの)によって生成されます。
{ \displaystyle p(w_{d,n}|z_{d,n,}\Phi) = \prod_{k=1}^{N-1} Cat(w_{d,n}|\phi_k)^{z_{d,n,k}}}
{ \displaystyle p(z_{d,n}|\theta_d) = Cat(z_{d,n}|\theta_d)}
つまり、単語wの生成される確率はトピック集合Φとそれらの割り当てzに依存し、割り当てzの生成される確率は文書dに含まれるトピックの比率θに依存するということです。
この時、トピック比率及びトピックφがそれぞれディリクレ分布から生成されていると仮定すると、結局全体の同時分布は以下の式で表されます。
{\displaystyle p(D,Z,\Phi,\Theta) = p(D|Z,\Phi)p(Z|\Theta)p(\Phi)p(\Theta)}
(ただし、上式において集合の確率は各要素の確率の積で表されるとします)
今求めたいのはp(D,Z,Φ,Θ)に対してp(D)が与えられた際の事後分布
{\displaystyle p(Z,\Phi,\Theta|D) = \frac{p(D,Z,\Phi,\Theta)}{p(D)}}
です。ここで変分推論を適用し先ほど求めた式を用い、潜在変数Zと他のパラメータ分解を仮定することでLDAにおける解析的な更新式が求まります。
{\displaystyle p(Z,\Phi,\Theta|D) \approx q(Z)q(\Theta, \Phi)}
この更新式はΘ及びΦのディリクレ分布のハイパーパラメータα及びβから求まります。
{\displaystyle < ln \phi_{k,v} > = \psi(\hat{\beta_{k,v}}) - \psi(\sum_{v'=1}^{V}\hat{\beta_{k,v'}})}
{\displaystyle < ln \theta_{d,k} > = \psi(\hat{\alpha_{d,k}}) - \psi(\sum_{k'=1}^{K}\hat{\alpha_{d,k'}})}
{\displaystyle < z_{d,n,k} > = \eta_{d,n,k}}
~ただし、変数はそれぞれ以下のように設定しました~
{\displaystyle \hat{\beta_{k,v}} = \sum_{d=1}^{D}\sum_{n=1}^{N} < z_{d,n,k}w_{d,n,k} > + \beta_v}
{\displaystyle \hat{\alpha_{d,k}} = \sum_{n=1}^{N} < z_{d,n,k} > + \alpha_k}
{\displaystyle \eta_{d,n,k} \approx exp({\sum_{v=1}^{V} w_{d,n,v} < ln\phi_{k,v} > + < ln\theta_{d,k} >})} ただし{\displaystyle (\sum_{k=1}^{K}\eta_{d,n,k} = 1)}
~
これらの更新式において崩壊型ギブスサンプリングというpからパラメータΘ,Φを周辺化除去しzの条件付き分布を求めるサンプリング方法によってαとβの更新が可能になります。

LDAは確率モデルであり拡張性が高いため、広く使われています。

結果

以下の画像は作成したTwitterの4つのサンプルアカウント(フォローに法則性のないアカウント,アニメ・ドラマ・ゲーム等の内容をつぶやくユーザーの多いアカウント,数学・物理・情報等の学問についてつぶやくユーザーが多いアカウント,僕自身のアカウント)から生成されたトピックを30個分取り出したものになります。

...可もなく不可もなくといったところでしょうか
f:id:noppo_eeic:20180422034431p:plain


これらのトピックのようにうまく単語をまとめられたものもありますが、
f:id:noppo_eeic:20180422034251p:plainf:id:noppo_eeic:20180422034319p:plainf:id:noppo_eeic:20180422034345p:plain

中にはとんちんかんなトピックもあります。
Twitterでは多くの著者と多くの話題が混在しているためうまくモデルを推定するのが難しいのかもしれません。


そこでモデルのパラメータであるトピック数と辞書の作成に用いたパラメータである単語の最低出現数(no_below)についてパラメータの適解を探索します。

モデルの評価に用いるのはperplexityと言う指標でトピックからある単語が生成される確率の逆数によって表されます。つまり、この数値が小さければ小さいほどモデルの予測性能は向上するのでうまく構築出来たと言うことになります。

はじめにトピック数及びに単語の最低出現数ついて大まかに探索することで大体のperplexityの傾向を探ったところ、今回はトピック数はだいたい200程度、最低出現数はだいたい18程度で極小値に収束することがわかりました。
そこでさらに細かく全探索していきます。

dictlist = []
for i in range(10):
    dictionary = corpora.Dictionary(words)
    dictionary.filter_extremes(no_below=i + 15, no_above=0.01)
    dictlist.append(dictionary)
    print(i)
corplist = []
for i in range(10):
    print(i)
    corpus = [dictlist[i].doc2bow(text) for text in words]
    corplist.append(corpus)

for k in range(10):
    for i in range(10):
        lda = gensim.models.ldamodel.LdaModel(corpus=corplist[i], num_topics=k + 215)
        perplist.append(np.exp2(-lda.log_perplexity(corpus)))
        print(np.exp2(-lda.log_perplexity(corpus)))
        print("i:",i)
        print("k:",k)

全探索をしたところ、トピック数219, 最低単語出現数19でperplexityの極小値25.48が得られました。
このパラメータを用いてもう一度トピックモデルを構築したところ以下のようになりました。
f:id:noppo_eeic:20180424051430p:plain

ちょっとは改善されているのでしょうか...?

新しく取得したツイートのトピック

では、取得したツイートのトピックモデルを用いて新たに取得したツイートがどのトピックに属しているのかを確認してみましょう。
先ほど用いたアニメ、ドラマ等のアカウントをフォローしているサンプルアカウントのタイムラインから以下のツイートが得られました。

[映画ニュース] 松坂桃李、役所広司の“魂”が込められた「孤狼の血」キーアイテムを継承!

この時、以下のコードによって新しく取得したツイート内のどのような単語が既存のトピックに反応するのか確認してみると、

print(test_words[1][2])
for i in range(len(test_words)):
    print("tweetnumber:", i)
    for j in range(len(test_words[i])):
        try:
            print(test_words[i][j], "id is", dictionary.token2id[test_words[i][j]])
        except KeyError:
            print(test_words[i][j], "is not found")
松坂桃李 id is 448
役所広司 id is 447

と、俳優の名前を含むトピックが既に存在することがわかり、

c = [(447,1), (448, 1)]
for (tpc, prob) in lda.get_document_topics(c):
    print(str(tpc) + ': '+str(prob))

146: 0.668155

と、146番目のトピックが約67%の確率で属することがわかりました。146番目のトピックは以下のようになっており

f:id:noppo_eeic:20180425211340p:plain

例えば俳優の名前なら「江口洋介」と言った単語がのちに続く可能性があることがわかります。

元ツイートを確認してみると

「江口洋介」さんが関わっていることがわかりますね。

最後に

今回はpythonを用いて身近な題材について機械学習のプログラムを実装してみました。機械学習というとハードルが高く見えるかもしれませんが、ライブラリが充実しているので簡単なものであれば誰にでも作れてしまうものです。
とはいえ、精度や学習速度など奥が深い分野であることもまた事実です。僕もトピック数の決定や評価法などまだまだ改良点があるのでまた挑戦してみようと思います。
ご清覧ありがとうございました


~今回作成したプログラムはこちらに置いておきます~
github.com

~またLDAの説明に関して以下の本を参考にさせていただきました。より詳しく機械学習について知りたい方にとてもおすすめの本です。

機械学習スタートアップシリーズ ベイズ推論による機械学習入門 (KS情報科学専門書)

機械学習スタートアップシリーズ ベイズ推論による機械学習入門 (KS情報科学専門書)