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

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

Pandas経由でHiveQLを実行してDataFrameに簡単に入れる方法

Hive経由で集計した値を、Pandasからスムーズに使うための方法を紹介します。 "スムーズ"に、というのは、「CSVを経由しない」と言い換えてもらって大丈夫です

準備

ライブラリとして、DropboxPyHive と Clouderaの impyla が必要です。

PyHiveを使っている理由は、必要な手続きが短いのと、PEP-0249に準拠しているからで、impylaを使っている理由は、as_pandasというユーティリティ関数を使いたいだけです。 なので、必須でないといえば必須でないです。

Anacondaを使っている場合は、下記の手順でインストールできます。

pip install impyla
conda install -c https://conda.binstar.org/blaze pyhive

コード

from pyhive import hive
from impala.util import as_pandas

conn = hive.connect(host="localhost")
cursor = conn.cursor()
cursor.execute("SELECT * FROM table")
df = as_pandas(cursor)

df.describe()

で、df にPandasのDataFrameオブジェクトが入ります(impylaの代わりにhiveになっているだけ)。 そんじゃーね!

アメブロでソースコードとかを投稿する方法、またはGithub Flavored Markdownで投稿する方法

お疲れ様です(?)

私の所属する会社には「アメーバブログ」というものがあり、せっかくなら愛着のある自社製品を使いたいところですが、残念なことに プログラマー向けの機能は全然足りません*1。ということで、弊社のプログラマーは、プログラミング系の話題ははてなブログに書いたり、github.ioに書いたり、qiitaに書いたりする人が多い。

しかし! 公式ブログはさすがにアメブロなので、アメブロでもソースコードを投稿しやすくする方法 というのを考案してみました

事前準備

アメブロの投稿画面を「パワーアップした新エディタ」にしてください

f:id:yamitzky:20140511124810p:plain

設定方法は、投稿画面の「パワーアップした新エディタを使おう」をクリックするか、「基本設定>記事投稿画面>新エディタ」です。

アメブロを使わずに書く

タイトルからして真っ向から反抗している感じがしますが、アメブロを使わずに書きます

ソースコードを綺麗に書けるエディタなら何でもいいですが、例えばGithub Flavored Markdownなエディタを使って、まず記事を書きます。

例えばGFMarkdownEditorとかMarkdown Editorとか、「Github Flavored Markdown javascript」で検索して出てくるものとかがお勧めです。もしくは、これらをForkして自分好みのエディターを作ろう!\(^o^)/*2

f:id:yamitzky:20140511125825p:plain

アメブロに貼り付け

記事を書いたら、エディタのプレビュー画面をコピーして、アメブロにペーストするだけです。

f:id:yamitzky:20140511131320p:plain

これでストレスなく、アメブロに技術ネタを投稿できるようになりました!

*1:ターゲットの問題がありますので、これは至極真っ当なことです

*2:本当はQiitaを使いたいんですが、コード上のcssがオリジナルであると想定されること、そしてそのcssが、Qiita上で使用することを前提に書かれていることを想定されると考えると、この目的に使うべきでないと考えます。

jedi-vimでanacondaのパッケージを補完させる

jedi-vimという、vimで(賢く)Pythonの補完などをしてくれるプラグインがあります。vimPython書くなら必須かも、というレベル。

当たり前(?)の話ですが、pipでインストールしたようなPythonのパッケージ群も、賢く補完してくれます。

しかし、デフォルトで使用するPythonが、システムのPython(/usr/bin/python)らしく、必然的にシステムのpipでインストールしたものしか補完してくれません。

そこで、anacondaのpipでインストールしたものなども補完できるようにしてみました。

やり方

やり方は単純で、~/.vim/ftplugin/python.vimの中に、以下のスクリプトを書くだけ。

python << EOF
import os
import sys

home = os.path.expanduser("~")
path = home + "/anaconda/lib/python2.7/site-packages"
if not path in sys.path:
  sys.path.insert(0, path)
EOF

要するに、anacondaのsite-packagesをsys.pathに追加しているだけです。同じ要領で、パッケージのディレクトリを増やすことも可能でしょう。

(おまけ) virtualenvを使う

virtualenvで管理している場合は(anacondaの場合そんなことできるのかしらんけど)、jmcantrell/vim-virtualenvを使うとvirtualenv環境のpythonが使われるらしい。試してないので知らないけども。

株式会社CyberZで働くことになりました/後輩の方々にお願い

株式会社サイバーエージェントに入社し、早期配属をすることができ、株式会社CyberZで働くことになりました。

CyberZは、サイバーエージェントの子会社で、スマホ向け広告効果計測ツールを作っている会社です。

CyberZで何をしたいかというと、ざっくりと言うとデータに関わる仕事(a.k.a. データサイエンティスト)をしたいな、と思っています。これから、サーバーサイドエンジニアとして成果をぐいぐい残していきながら、データに関する仕事をぐいぐいやりたい、です。だからこそ、CyberZを第一志望で配属希望を出しました。

あんまり話つながってないですが、ついでにお願いしておきますと、アドテクノロジー関連の情報収集の仕方をお伺いしたいです。海外でadtechとかadtechnologyとかで検索してもなかなか良い情報ソースを見つけることができていません・・・。特にニュースレター(メルマガ)なんかあると最高なんですが。

後輩の方々へお願い

すでに面識があって弊社(CA)に内々定をもらっている方、そして面識がなくても内々定もらっている方々へ、お願いというかアドバイスというか。

私はもともとサイバーエージェントの広告代理店部門のデータ分析チームで内定者アルバイトをしていました。したがって、こちらのデータ分析チームがいかに優秀な方々であるか、いかに一緒に働いていて楽しいか、というのを十分知っています(ここ宣伝ですよ!)。

逆の視点から言うと、一緒に働いてみないと、そこで働くことがどれほど素晴らしいか、というのを本当の意味で実感することはできません。もちろん、これから働く先の方と働くのは楽しいだろうな、と感じていますが*1、その確信度は内定者アルバイトをした方が高くなります(ここ、ニュアンスが難しい)。

また別の視点で見ると、弊社内定者は、内定者アルバイトという機会で、知る機会をもらっていることになります。

そこで私から、後輩の方々へお願いなのですが、どうか内定者アルバイトを2箇所以上でやってください。そしたらランチでも飲みでもおごるので、声をかけていただけると大変嬉しいです。

以上、NDAはちゃんと避けていると思うのですがどうでしょうか。。。

*1:だからこその第1希望です

Theanoを使ってPythonで行列演算とロジスティック回帰

TheanoというPython用のライブラリがあります。

ちょっと勉強したので、チュートリアルを日本語に翻訳しつつ、使い方とかを紹介します。

Theanoとは

まずはじめにTheanoとは、について。

Theanoはおそらく「テアノ」と読むのが多分正しいです。ピタゴラス(Pythagoras)の妻です。PythonとPythagorasをかけてるっぽいです。

何ができるかというと、簡単に言えば、行列演算ができます。以下の特徴を持っています(公式サイトより抜粋)

  • 実行スピードの最適化: Theano は g++などを使って式をコンパイルし、CPUやGPU操作に変換します(つまり、pure Pythonなコードよりも速い)
  • symbolicな微分: Theano は勾配を計算するために自動的にsymbolic graphsを組み立てます(訳注:つまり微分に便利だということ)
  • 安定な数値計算のための最適化: Theanoは不安定な数値計算を認識し、安定なアルゴリズムに変換します(訳注:例えばlogsumexpなどを言っていると思います)

なぜこれができるかというと、変数をシンボルとして扱うからだと思います。例えば、--xxにしたり、x * y / xyに変換することなどが明記されています。*1

この、変数を変数のまま保持しておくような考え方は、Theanoの設計を理解する上でちょっと重要な気もします。

また、Deep Learningとかニューラルネットの実装のためにpylearn2というライブラリが使われることもあるようですが、pylearn2はTheanoに依存しています。Theanoは、行列演算と微分の形になるNeural Networkと相性が良さそうですしね。

チュートリアル

(ロジスティック)シグモイドを造ります。

 s(x) = \frac{1}{1+e^{-x}}

で表せますから、これをそのまま数式にしてあげます。

まずは、必要な物をimportします。

import theano.tensor as T
from theano import function

次に、関数の引数に必要な変数を定義します。Theanoは型をちゃんと指定する必要があります(なんとなく、こうすることによって内部的に計算が高速になるメリットがあるんじゃないでしょうか)。

x = T.dscalar() # double型の数値
# x = T.dmatrix() # double型の行列

次に、シグモイド関数の計算式というか、関数の形を定義してあげます。

s = 1 / (1 + T.exp(-x))

このままでは、シグモイド関数sは呼び出し可能ではありません(s(0.5)みたいには呼び出せない)。ということで、呼び出し可能な関数を作ってあげます。

function 関数を使いますが、第1引数は、関数に必要な引数(ない場合は[])。第2引数は、「returnしてほしいもの」言い換えると関数の形とかです。

logistic = function([x], s)

ここで多分コンパイルが走ってる気がします。そしたらあとは呼び出すだけです。

logistic(0) # => array(0.5)

ここで、先ほどのxの型を「行列(dmatrix)」とかにしてあげると、呼び出し方も変わって、

logistic([[0]]) # => array([[0.5]])

となります。また、引数は行列なので、もちろん[[0, 1], [1.5, -1]]みたいな感じで行列を突っ込んで上げても良いです。

また、出力ももう少し拡張性が高いです。例えば、関数の形を配列に突っ込んであげたり。

logistic = function([x], [s, s])
logistic(0) # => [array(0.5), array(0.5)]

値の保持

値をどこかグロバール領域に保持しつつ計算することもできます(shared variables)。チュートリアルのサンプルでは、勾配法でロジスティック回帰を行っていますが、重みを保持し(つまりreturnしない)、ひたすら書き換えることによって実装しています。

このメリットは、グローバル変数と同じく共有しやすいことと(結果として表記上わかりやすくなる)、主に計算上のメリットだそうです。

shared variableを使うにはshared関数と、update引数を使います。

from theano import shared
state = shared(0) # 0は初期値
inc = T.iscalar('inc') # integer型のスカラー
accumulator = function([inc], state, updates=[(state, state+inc)])
# equals function([inc], state, updates={state: state+inc})

functionの引数のうち、2つめは必須ではありません。あくまで実行前の状態をreturnしてくれるというだけです(状態が確認しやすいだけ)。updateの引数は、各shared varibleに対する操作の詳細を示します。(書き込み先のshared variable, 関数の形)というタプルで指定します。まあ意味的には辞書みたいなものなので、dict型でもいいです。

乱数

乱数を使うこともできます。

from theano.tensor.shared_randomstreams import RandomStreams
srng = RandomStreams(seed=234)
rv_u = srng.uniform((2,2))
f = function([], rv_u)
f() # such as => array([[ 0.28179047,  0.23616647], [ 0.5958365 ,  0.1385743 ]])

例えば、box-muller変換を使った標準正規分布は、

rv_X = srng.uniform((1,2))
rv_Y = srng.uniform((1,2))
box_muller = T.sqrt(-2 * T.log(rv_X)) * T.cos(2 * math.pi * rv_Y)
normal = function([], box_muller)()
normal() # => array([[-0.53976774,  1.09561059]])

ただし、同じ乱数は式中で同じ値になるので注意が必要です。

function([], rv_X - rv_X)() #=> array([[ 0.,  0.]])

ロジスティック回帰の実装

説明に疲れてきたので詳細は割愛します、本家のチュートリアルを見てください。

こちらでは、L2正則化つきのロジスティック回帰が実装されています。具体的な微分式を与えず、

gw, gb = T.grad(cost, [w, b])

とすることで誤差の微分(勾配)を求めています。そして、

train = theano.function(
          inputs=[x,y],
          outputs=[prediction, xent],
          updates=((w, w - 0.1 * gw), (b, b - 0.1 * gb)))

という風に、updatesに((w, w - 0.1 * gw), (b, b - 0.1 * gb))としています。これは更新率ηを0.1として、shared variableのwをひたすら書き換える、という感じです。

また、このoutputs引数に指定された[prediction, xent]は全く使われていませんので、[]にしても動くと思います

参考文献

*1:実際にどれくらい認識して数値計算を安定化、もしくは省略してくれるのかはわからないので、結局自力で一番良い計算方法をコードに落としこむことが多いと思いますが。。。

ニューラル言語モデルは何を目的としているのか? 〜 「A Neural Probabilistic Language Model」を途中まで読んだ

word2vecでさんざん遊んだ皆さん、こんにちは。

今日は、word2vecの元になった論文の元になった論文の先行研究になっている論文「A Neural Probabilistic Language Model(Yoshua Bengio)」の紹介です。

word2vecは、単語の素性で足し算・引き算ができたり、単語の類推(アナロジー)ができたり、単語の素性の面白さが注目されています。とは言え、ニューラルネットによる言語モデルは、別に単語の素性で遊ぶために作られたわけではありません

ということで、ニューラルネットによる言語モデルの本家(?)である「確率的ニューラル言語モデル(Bengio先生)」の論文から、「そもそも何を目的にモデリングしているのか」「なぜニューラル言語モデルが必要なのか」というあたりを、紹介したいと思います(主にIntroductionの部分、ということになります。)。

誤訳はないように心がけているつもりですが、何かありましたらご指摘ください。また、「元論文-数式+例え」な感じで書いているので、元論文と合わせて読むと良いと思います。

何を目的にモデリングしているのか

ざっくり言うと「次に来る単語を予測すること」です。これはニューラルネットによる言語モデルに限った話ではありません。統計的言語モデル一般について言っていると思います。

もう少し厳密に言うと、「ある単語列が与えられたとき、次にどんな単語が来るか、の条件付き確率を学習すること」です。論文中でも「統計的言語モデルは、言語における単語の連なりの条件付き確率関数を学習することが目的だ」と書かれています。

数式で書くと、 p( word _ { t } | word _ { t-1 }, word _ { t-2 }, word _ { t-3 }, ..., word _ {t-n + 1})ということです(n:直前の単語何個を扱うか)。

これは、語順をBag-of-Wordsのようには捨てていないことも意味します。例えば「私、は、今日、料理、を、」という単語列の次に続く単語は、「した」とか「する」とか、「したい」とか、多分そういう単語です。間違っても「にんじん」とかじゃないですね。このような「単語列に対して、どういう単語が来そうか」という確率を求めるようなモデルを学習します*1

余談ですが、一方で、LDAとかは異なります。こちらは文書(≠文章)が持つトピック(話題など)の分布を推定したり、トピックごとの単語の出現確率を推定していますが、「Bag-of-Words」という形で文書を扱うので、語順を捨てています。ニューラルネットによる言語モデルとLDAは、「単語の素性を獲得できる」という一面は少しだけ似ていますが、それ以外は大きく異なります。

なぜニューラルネット言語モデルが必要なのか

先ほど述べた統計的言語モデルの文脈の中で、なぜニューラルネットによる言語モデルが必要とされたのか、という話です。

理由としては、ニューラル言語モデルよりも前の統計的言語モデルだと「次元の呪い」が起こり、学習困難だから、と書かれています。

まず、既存(当時)手法の問題点を説明します。例えば、「私、は、今日、料理、を、」の次に来る単語を予測したいとすると、「私、は、今日、料理、を、(何かの単語)」という連なりをたくさん収集して、(「私、は、今日、料理、を、(ある単語)」の発生数÷「私、は、今日、料理、を」の発生数)を計算しなければなりません。この例では5単語の連なりパターンが考えられます。語彙の数が仮に10万あったとすると、単語列のパターンは 100,000^{5} - 1 = 10^{25} - 1ぐらいあります。このような膨大なパターンの中から「私、は、今日、料理、を、(何かの単語)」という奇跡的な連なりを観測して、(何かの単語)がどれくらいの確率でやってくるのか、というのをカウントしなければなりません(もちろん、DBに保存するのもしんどいですね)。

しかしパターン数が膨大すぎると「ほとんどのパターンは発生しない」という問題が起こります。とはいえ、ただ観測されないだけであって、本当に0とは言えません。そこで「ほぼ0」の代わりに「0.1%」みたいな値でスムージングしたり、次元を削減したりします。

次元削減の考えとしては、例えば単純には、「を、」の次に来る単語を予測してあげるようなものにするとよいです。もう少し正確に言えば、n-gramモデルにおけるnを、1とか2とか、より低次なものにして、組み合わせパターンを減らします。ただし、「を、」の次に来る単語を予測できるよりも「私、は、今日、料理、を、」の次に来る単語を予測できる方が嬉しいのは明らかですし、そちらの方がより文脈を汲み取れるはずです。そういったトレードオフの元で、n-gramのnを減らすことになります。

また、スムージングについては言語モデル入門 - Topics Related to Computers and NLPに紹介されています。スムージングによる手法が意外とperplexityが低くてびっくりしました。Kneser-Ney スムージングによる文書生成 - Mi manca qualche giovedi`?も面白い結果がわかると思います。

さて、ニューラル言語モデル(そしてニューラルネットを使った言語モデル)は上の問題をどう解消しているのか、という説明に入ります。

単純な説明としては、「単語列」を「(単語の特徴を十分によく表すような)ベクトル列」として扱います。

これを例示的な説明に変えてみます。「私、は、今日、料理、を、」という列と全く同じ列は発生しにくいけど、似ている列なら発生しやすい、ということを利用します。具体的には「俺、は、明日、料理、を、」とか「彼、は、昨日、料理、を、」とか。そして「俺」「私」「彼」が似たような語順や文脈で使われることが期待されるので、獲得されたこれらの単語の特徴量は似てきます。他の単語も同様です。各単語をただの文字列として扱ったとき、コンピューターには各文字列の関係性はわかりませんが、特徴量ベクトルは類似します*2。結果として、「ある特徴量ベクトル列と似たような特徴量ベクトル列」というふうに一般化されれば、「ある単語列と全く同じ単語列」よりも確率が"スムージング"されます。

また、パラメータの増加の仕方も抑えられます。下の図は、論文に載っていたニューラルネットの構造に、注釈を付け加えたものです。

f:id:yamitzky:20140426101955p:plain

ニューラル言語モデルの中間層の出力はtanh(bias+Hx)で表わせるのですが、xは特徴量ベクトルの列 x = \left  [ C(w _ {t-1}), C(w _ {t-2}), ..., C(w _ {t-n+1}) \right ] という、(特徴量次元)がn個ならんだ行列です。特徴量次元は自分で設定できます(word2vecのときはたかだか100次元ぐらいとかで設定しました)。結果として、Cのパラメータ数(この場合、行列の大きさ)は語彙数に線形に増加し、Hのパラメータ数は予測に使用する単語列の数(n)に線形に増加します。一方で、先ほどの既存手法ではパラメータは vocab^{n} に指数的に増加してしまっていました。結果として、ニューラルネットを使ったモデルの方が次元の呪いを回避できる、という算段です。

なぜ単語の素性が獲得できるのか

ここでいう単語の素性とは「単語ごとを表すベクトル」です。word2vecでは、これを足し引きして、アナロジーとか類似度計算をしていたわけですね。

すごーくざっくり言うと、モデル(ニューラルネット)が特徴量(素性)抽出をするから、と言うといい気がします。

このモデルは、最終的に精度よく「次の単語」を予測できるように、単語の特徴を十分によく表すように圧縮した特徴量ベクトルを獲得します。「エントロピーを下げてくれるような特徴量」と言うといいような気もします。

先ほどの「私、は、今日、料理、を、」の列のたとえに戻ります。例えば、「(無意味な特徴量)、(無意味な特徴量)、(無意味な特徴量)、(無意味な特徴量)、(無意味な特徴量)、」という列の次の単語は、全く予測できるような気がしません。その逆に各単語を十分に表すような良い特徴量が獲得できれば、次に来る単語を予測できるようになります。

ニューラル言語モデルの学習時には、正しく予測できるように、Back propagationによって特徴量が学習され、最終的に良い特徴量(素性)が完成します。

なぜ単語の素性で足し算/引き算ができるのか

ニューラルネットによる単語のベクトル表現の学習 〜 Twitterのデータでword2vecしてみた - 病みつきエンジニアブログについてです。

答えについては、知らないので、誰か教えてください。。。(というのを聞きたくてこのブログを書いたに等しいかも)

ちなみにこのニューラル言語モデルでもそれができるのかも知りません。このモデルができた当時にはそのような話はないような気がしますが、僕は当時小学生だったので知りません。でも多分、Tomas MikolovによるRecurrent Neural Network Language Modelが初出な気がしますが、歴史的経緯はあまり知らないのでご指摘いただけると。。。

終わりに

ニューラルネットは研究で使ったことがないので、誤りを含むかもしれません。何か間違いや不正確な表現、またはわかりにくい表現がありましたら指摘していただけると幸いです。

参考文献

*1:一応、学習結果として、重みの出方で語順が葬り去られることもなくはないと思いますが

*2:明確に文法的特徴がここに置かれるわけではありませんが、特徴量ベクトルの類似度の結果を見ると、同じ品詞のものが似ているとされたりします。例えば「北海道」と「名古屋」など。その反例もありますが、学習データ量が足りないと顕著になります

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:陽に事後確率を推定して、その確率の降順とかで出すこともできる