病みつきエンジニアブログ

機械学習、Python、Scala、JavaScript、などなど

LDAを使って、Twitterでスパムに使われそうな単語を推定する

教師なしLDAでTwitterのスパム判別をしてみる(予備実験編) - 病みつきエンジニアブログ の続きになります!

モチベーション

前回の記事で、LDA(latent Dirichlet allocation)のモデルを獲得したので、獲得したモデルを使って「どんな単語がスパムによく使われるのか?」というのを推定しようと思った。

そんなにちゃんとモチベーションない気がする*1

考えられる使い道は、

  • スパム判定のルールを作ることができる
  • スパム判定されなさそうな単語を選択することができる(スパマー側の気持ちに立ってw)

なのかなあ、あんまり使えなさそう。でも、ここらへんの教師データってちゃんとないから、前者は普通にありかなあと思ったり。

推定方法

{ \displaystyle
p(spam|word) \propto p(spam) \sum_{z} p(word | z) p(z|spam)
}

という式に基いて行っている。順番に説明すると、

  •  p(spam|word)は「ある単語がスパムっぽい確率」
  •  p(spam)は「そもそもスパムが発生する確率」、つまりスパムの事前確率
  •  p(word|z)は「あるトピックにおける単語の発生確率」
  •  p(z|spam)は、「あるトピックがスパムである尤度」

となる(spamは変数なので、non-spamと置き換えても良い)。

このうち、尤度は「1か0」にしている。つまり、 \sum_{z} p(word | z) p(z|spam) は、「スパムっぽいトピックでの単語の発生確率を足しあわせたもの」と言い換えられる。

で、スパムの時、スパムでない時、それぞれ足しあわせた結果を、比較して、大きいほうを採択する。そのときの基準を決めるのが、 p(spam)になり、これをハイパーパラメータとした。

こうするメリットというのがあって、「どれぐらい明らかにスパムっぽかったらスパムと決定するか」というのをパラメータ一つで柔軟に設定できる*2

以上をもう少し簡単に言うと、スパムっぽいトピックでのみ頻出する単語はスパムであるとする。その境界は p(spam)で決まる、と言っているようなもの、かな。

データセット

データは、自分で収集したもので、「URLつきのツイート」50919件

教師なしLDAでTwitterのスパム判別をしてみる(予備実験編) - 病みつきエンジニアブログ とほぼ同じデータになります(LDA回し直したけど)

f:id:yamitzky:20140216225341p:plain

実装

githubに置いた。汚い。

スパム単語の検知はspam_detect.py

実験結果

 p(spam) = 0.1ぐらいにすると*3

レポート
#followmejp
#goen
インフォゼロ
アフィリエイト
月収
4
0
万
円
9
不労所得
構築
方法
ハズレ
残念
チャレンジ
楽天
ラッキー
スクラッチ
総額
ポイント
当たる
抽選
www.rakuten-apps.jp
稼げる
毎月
以上
公開
対応
ツール
関係
3
atq.ck.valuecommerce.com
#ヤフオク
送料
[
a.r10.to
#RakutenIchiba

twitter-lda/spam-words-0.1.txt at master · yamitzky/twitter-lda · GitHub

という感じ。結構スパムに使われそうな単語が並んでいる感じはある。「楽天」とか「ヤフオク」って結構スパムに使われるんですね。別に楽天が悪いわけじゃないけど・・・。

とはいえ、「ツール」とかは、スパムにのみ使われる単語ではないので、これをルールとして採用して、厳密に取り除いたりすると、ちょっときついかも。それでもいいと妥協するか、もう少し賢いやり方を考える必要がある。

今回の場合は「URLつきツイートのコーパス」を使っているので、「全ツイートのコーパス」でやると、もっとそれっぽい単語だけに絞られるかもしれない。なぜなら、"ツール"という単語がスパム以外にも使われれば、「スパムっぽさ」の事後確率がだんだん下がるから。

次回は、「LDAを使って、Twitterのスパムを効果的に取り除く単語を推定する」というテーマで書きます!

補足:LDA使う意味があるのか?

この疑問なんですが、個人的にはあるかなあ、と。

理由としては、たかだか、LDAで生成されるトピック数分(100個とか、もちろん柔軟に決められる)の教師データを作ればよいから。

一方で、スパム単語の教師データ(ルール)をちゃんと作るのは、結構大変です。全単語見るわけにはいかないし、スパムっぽいかどうか判断できない単語が多いし。

このことから、LDAで次元圧縮して必要な教師データ数を絞る、というのは結構ありな戦略なんじゃないかなあと思っていますが、アンサーまさかりをお願い致します。

補足:判別式の計算

算出したいのは  p(spam|word) になる。これが何を表すかというと、「ある単語は、スパムっぽいか否か?(スパムに使われそうか?)」ということ。

で、ベイズの公式から

{ \displaystyle
p(spam|word) \propto p(word|spam) p(spam)
}

となるので、右辺の2つの関数を求めたい。

 p(word|spam) は、

{ \displaystyle
p(word|spam) = \sum_z p(word, z|spam) = \sum_z p(word | z) p(z|spam)  { \displaystyle
 = \sum_z p(word|z) \frac{ p(spam|z) p(z) } { p(spam) }  = \frac{ p(z) }{ p(spam) }  \sum_{z} p(word|z) p(spam|z)

ただしここには、 p(z) はトピック(z)に依存しないという仮定を置いている(全てのトピックは等しく出やすい)し、実際これは正しいはず(LDAの \alphaがどのトピックにも等しく振られるから)。そうすると、これを最初の右辺に戻してあげて

{ \displaystyle
p(word|spam) p(spam) =  p(spam) \frac{ p(z) } { p(spam) }  \sum_{z} p(word|z) p(spam|z)
}

{ \displaystyle
= p(z) \sum_{z} p(word | z) p(spam | z)
}

となる。 p(word|z)は既にLDAで獲得済みなので、あとは、 p(spam|z)、つまり「トピック(z)がスパムかどうか」という確率が推定できればよい。これもベイズの公式を使って

{ \displaystyle
p(spam|z)= \frac{ p(z|spam)p(spam) } { p(z) }
}

これを最初の式に戻すと、

{ \displaystyle
p(spam|word) \propto p(z) \sum_{z} p(word | z) \frac{ p(z|spam) p(spam) } { p(z) }
}

{\displaystyle
= p(spam) \sum_{z} p(word | z) p(z|spam)
}

となり、判別のための式ができた。

このうち尤度p(z|spam)に関しては人力で、スパムかどうかを「0or1」で判断したもの。事前分布p(spam)に関しては、スパムだったら\gamma、スパムじゃなかったら1 - \gammaと、ハイパーパラメータ扱いをした(ちょうど、スパム/スパムじゃないの境界面を引く感じになると思う)。

また、最後の式は、解釈的にも難しくなくて、「スパムのときと非スパムのときの単語の出現確率をそれぞれ足しあわせて、その比率が一定以上だったらスパム」というような判別をやっているだけ、ということになる。

さて、ここまでの数式はド派手に間違えているかもしれないので、ツッコミをお待ちしております(震え声)

補足:注意

トピックに割り当てられる番号はランダムなので、LDAで再度学習すると「このトピック番号はスパムっぽい」という努力は水の泡になる。

したがって、再学習したときにもトラッキングできるような仕組みは作らないといけない。

例えば、1度計算して、その中から明らかにスパムっぽいものを「スパム単語のルール」として決定しておく。そこから、そのスパム単語の確率が p以上のものをスパムトピックとする、等。

*1:目的に基づいた実装じゃなくて、どちらかと言えば思考実験だったので

*2:本来的な事前確率としての意味合いは崩壊している気もする

*3:陽に事後確率を推定して、その確率の降順とかで出すこともできる