ベイズ推論の事後分布(離散)の更新について

前回の続き
VAEの理解に必要なベイズ学習について - 時給600円

パラメータの事前分布を自分で仮定して、観測データを元により適したパラメータを推定するのがベイズ学習といった話だった。
事前分布として固定値ではなく正規分布といった確率分布を与えてその時にちゃんとパラメータが学習されるのか確認する。

例としてある1枚のコインが存在して、そのコインで100回コイントスをする。
表が10回ほど、裏が90回ほど出たとする。このとき結果から1枚のコインの表が出る確率が求められるか。

100回中10回程度しか表が出てないんだから確率としては { \displaystyle \frac{1}{10} } でいいじゃん^^と自分は思うがベイズ学習で似たような結果が得られるか確認する。

まずは上記の設定に似たコイントスの結果を生成はこんな感じ

true_mu = 0.16
X = np.random.binomial(n=1,p=true_mu,size=100)

# 結果の例
# (array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
#        0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
#        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
#        0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0,
#        0, 0, 0, 0, 0, 0, 0, 0]) )

コイントスは二値の値 { \displaystyle x \in } { 0 , 1 } を取るので、{0,1}を生成してくれるベルヌーイ分布を使う。(1がコインの表とする)
ここではベルヌーイ分布に必要なパラメータ{ \displaystyle \mu }の真の値を 0.16 とした。この値に近いものを観測データから求めるのが目標。

前回の通りにやるとパラメータの推定は i.i.d な観測データ{ \displaystyle X = } {{ \displaystyle x_1,x_2,...,x_N }}としたとき、ベイズの定理から

{ \displaystyle p(\mu |X) = \frac{p(X|\mu )p(\mu)}{p(X)} = p(\mu) \frac{\prod_{n=1}^N p(x_n | \mu)}{p(X)} }


とできる。

タイトル通り事前分布として固定値ではなく確率分布を与える。前提としてベルヌーイ分布のパラメータ{ \displaystyle \mu }の取りうる範囲は (0,1) の間でなくてはいけない。
つまり (0, 1) の間で実数を生成してくれるような確率分布を与えればいいんだな。

ベータ分布なるものは { \displaystyle \mu \in (0,1) }となる実数を生成してくれる連続確率分布らしい。これを使ってみよう。定義は以下のようになっている

{ \displaystyle Beta(\mu | a,b) = \frac{\Gamma (a+b)}{\Gamma (a)\Gamma (b)} \mu^{a-1} (1-\mu)^{b-1} }


a,bは非負の実数でなくてはならず、自分で設定するハイパーパラメータ。 殺傷力ありそうな記号{ \displaystyle \Gamma() }がついた関数はガンマ関数。階乗を表現してるそうだ。

a,bを適当に設定したベータ分布をプロットしてみた結果はこんなんになった

f:id:Owatank:20180412131347p:plain

ふわ^〜

aとbの値が小さいと下に凹んだ形状で、大きいと正規分布みたいな山のような形になるのかな。いや数式見れば形状に説明つくんだろうけど・・・

観測データの見えない生成規則がベルヌーイ分布

{ \displaystyle p(x| \mu) \sim Bern(x | \mu) = {\mu}^x (1-\mu)^{1-x}}

に従うとしているから、この式と上のベータ分布の式を事後分布の右辺に代入してみる

{ \displaystyle p(\mu |X) =  \frac{\Gamma (a+b)}{\Gamma (a)\Gamma (b)} \frac{(\prod_{n=1}^N {\mu}^{x_n} (1-\mu)^{1-x_n} ) \mu^{a-1} (1-\mu)^{b-1} }{p(X)} }



ヴワッ・・・果てしなく汚い式になってしまった・・・
ガンマ関数と分母はパラメータ{ \displaystyle \mu }によらないので、定数Aとおいて見ればこの式は

{ \displaystyle p(\mu |X) =  A { \mu^{(\sum_{n=1}^N x_n + a) -1} (1-\mu)^{(N - \sum_{n=1}^N x_n + b) -1} } }

こんな感じで書けるはずなのだ。ようは{ \displaystyle \mu }{ \displaystyle (1-\mu ) }でまとめられる。
この式の形はベータ分布と似ている。共役性とやらで実は{ \displaystyle p(x| \mu) }をベルヌーイ分布に設定して、事前分布{ \displaystyle p(\mu) }をベータ分布にしたとき事後分布はベータ分布と同じ形の分布になるのだ・・・

何が嬉しいって分母、もとい周辺尤度{ \displaystyle p(X) }を計算しなくて済む。
多分この周辺尤度ってパラメータ{ \displaystyle \mu }が取りうる範囲で積分、つまりは周辺化して得られるはずだから

{ \displaystyle p(X) =  \int p(X,\mu) d\mu = \int p(X|\mu)p(\mu) d\mu }


こうなるはずなんだよな・・・違うかな・・・
積分するものによっては難しいものあるんじゃなかろうか(´・ω・)

気を取り直して観測データの集合Xからパラメータ{ \displaystyle \mu }を推定する{ \displaystyle p(\mu|X) }を計算するには{ \displaystyle p(x| \mu) }をベルヌーイ分布にして、事前分布をベータ分布に設定すれば、ベータ分布のハイパーパラメータに観測データの情報を加えたものから得られることがわかった。

ベータ分布のハイパーパラメータを a = 0.1 , b = 0.1 に設定して、{ \displaystyle \mu }が本当に推定されていくのか見ていく

for i in range(0,len(X),10):
    est_mu = 0
    est_mu = scipy.stats.beta.rvs(np.sum(X[0:i+10])+a[0], len(X[0:i+10]) - np.sum(X[0:i+10]) + b[0])
    print('観測データの数 : {0}件 、 パラメータμの推定値 : {1} '.format(i,est_mu))

f:id:Owatank:20180412150324p:plain

観測するデータを10件ごと増やしていくと50件以降から真のパラメータの値 0.16 に近くなっていくのが若干わかる
学習できていると見ていいんじゃなかろうか

もういっちょ別の確率分布を使ってパラメータの推定をやってみる
ベルヌーイ分布は二値に対してのみだったが、サイコロの目のようなKパターンの確率も推定してみたい
このようなK次元の確率分布としてカテゴリ分布なるものがある

{ \displaystyle Cat(\boldsymbol{s} | \boldsymbol{\pi}) = \prod_{k=1}^K {\pi_k}^{s_k}}

{ \displaystyle \boldsymbol{s} }{ \displaystyle \boldsymbol{s} = (0,0,1,0,..,0) }といった、K次元のうちk番目の値が1で他の値が0といったonehot表記であること、{ \displaystyle \boldsymbol{\pi}}{ \displaystyle \pi_k \in (0,1) }で、かつ{ \displaystyle \sum_{k=1}^K \pi_k = 1}となることが条件

ベルヌーイ分布でのパラメータ{ \displaystyle \mu}のように、このカテゴリ分布では { \displaystyle \boldsymbol{\pi}} がパラメータになる
次元数をK=6 とし、{ \displaystyle \pi_k = \frac{1}{6} }とすれば等確率のサイコロが表せるんだな

同様に

{ \displaystyle p(\boldsymbol{s} | \boldsymbol{\pi}) \sim Cat(\boldsymbol{s} | \boldsymbol{\pi})}

としたとき事前分布{ \displaystyle p(\boldsymbol{\pi})}はどうすればいいだろう

ベータ分布は{ \displaystyle \mu \in (0,1) }、つまり変数1個しか生成してくれない。同じ (0,1) 区間で実数を多次元で、かつそれらの和が 1 となってくれるようなものを生成してくれる分布はないだろうかと悩むところでディクリレ分布というものがその役割を果たしてくれる。すげえ!!!

{ \displaystyle Dir( \boldsymbol{\pi}| \boldsymbol{\alpha} ) = \frac{\Gamma(\sum_{k=1}^K \alpha_k)}{\prod_{k=1}^K \Gamma(\alpha_k)} \prod_{k=1}^K {\pi_k}^{\alpha_k - 1}}

{ \displaystyle \boldsymbol{\alpha} }の要素{ \displaystyle \alpha_k}は正の実数が条件のハイパーパラメータ

何はともあれ事後分布に代入だ。観測データを{ \displaystyle S =} {{ \displaystyle \boldsymbol{s_1} , \boldsymbol{s_2}, ... , \boldsymbol{s_N}} }とすると

{ \displaystyle p(\boldsymbol{\pi} |S) =  \frac{p(S|\boldsymbol{\pi} )p(\boldsymbol{\pi})}{p(S)} = \frac{(\prod_{n=1}^N Cat(\boldsymbol{s_n} | \boldsymbol{\pi}))Dir( \boldsymbol{\pi}| \boldsymbol{\alpha} )}{p(S)} = \frac{(\prod_{n=1}^N{(\prod_{k=1}^K {\pi_k}^{s_n,k}}))\frac{\Gamma(\sum_{k=1}^K \alpha_k)}{\prod_{k=1}^K \Gamma(\alpha_k)}\prod_{k=1}^K {\pi_k}^{\alpha_k - 1}}{p(S)} }

ヴォエッ・・。こっちも汚くなったが分母とガンマ関数の部分は定数とみなせるのでAとして、総乗の部分はどちらもkのものだからまとめることができる。整理すると

{ \displaystyle p(\boldsymbol{\pi} |S) = A\prod_{k=1}^K {\pi_k}^{\sum_{n=1}^N s_{n,k} + \alpha_k - 1} }

こんな感じで事後分布をディクリレ分布と似た形の式にみることができるのだ。共役性ありがてえ・・・

等確率でないサイコロを作って、そのサイコロの確率、もといパラメータを推定してみる
まずサンプルとなるインチキなサイコロを投げた結果を作る

weights = [0.1,0.1,0.35,0.1,0.25,0.1]
dice = [1,2,3,4,5,6]

obs_dice = np.random.choice(dice,p=weights,size=300)

# カテゴリ分布に投げるためにonehot表記する
obs_dice_onehot = []
t = np.zeros(6)
for d in obs_dice:
    t = np.zeros(6)
    t[d-1] = 1
    obs_dice_onehot.append(t)

np.random.choice()で任意の重みで設定した結果をsizeの数だけくれるようだ。
ここでweights = [0.1,0.1,0.35,0.1,0.25,0.1]が左からサイコロで1がでる確率、2が出る確率、として、これらの重みを結果から推定する

ディクリレ分布に必要なハイパーパラメータ{ \displaystyle \boldsymbol{\alpha}}の値を全て 0.2 とする
データを10件ずつ増やしていってパラメータ { \displaystyle \boldsymbol{\pi}} の推定値がどう変わるか見ていく

for i in range(0,300,10):
    s = [0] * 6
    alpha = [0.2,0.2,0.2,0.2,0.2,0.2]
    for k in range(0,6):
        s[k] = np.sum(list(map(lambda x: x[k], obs_dice_onehot[0:i+10])))
    alpha = np.add(alpha,s)
    p = scipy.stats.dirichlet.rvs(alpha)
    print(i,p)

f:id:Owatank:20180412165541p:plain

10件あたりでもそこそこいい値が出ている気がする。データが増えていくほど値が安定していくようなそうでないような

ベルヌーイ分布なら事前分布をベータ分布に、カテゴリ分布なら事前分布をディクリレ分布にといった共役の事前確率分布を取れば計算がかなり簡単になることがわかった。それでも現実の問題によっては共役でない事前分布を取ることとかあるのだろうか?必ず共役な事前分布をとれって訳ではないしな・・・

これはベイズ学習であって自分が知りたい変分ベイズではない。変分ベイズはこのような手順を踏んではいるけどちょっと違う推定の仕方とかそういうのなんだろうか?(´・ω・)というか試したのは離散であって連続値に対する推定ですらなかった

VAEの理解にはまだ遠い

VAEの理解に必要なベイズ学習について

自己符号化器、初めて知った当初は中間層にて入力の次元数より少ない次元数で表現し、出力層(入力と同じ次元数)で復元を行うすごいやつといったイメージがあった。入力にあえてノイズを加えて出力としてノイズを除去したものを得るデノイジング自己符号化器といったものもあった。

これらを知った時、復元できんの!!!!!!!すげえじゃん!!!!!!と感動した思い出がある。そして近頃VAEなるものを知った。(出たのは2015年頃で今更感はあるけども)

人からVAEの話を聞いた限りでは、VAEは入力されるモノがパラメータ(μ,σ)に従う正規分布によって生成されていると仮定して、そのμとσを推定できれば、入力と似たものを出力として得られる、という認識をもった。
やっぱり正規分布は便利なんだなと思いつつ、自己符号化器は復元だけではなく、入力と似たものの生成もできるのかとワクワクした。
個人的に興味を持ったので、VAEの論文を読んでみた。([1312.6114] Auto-Encoding Variational Bayes)


が・・・・駄目っ・・・・・!

自分が読んだ限り


  • 有向確率モデルとやらで事後分布を持つ潜在変数またはパラメータの近似的な推論と学習を効率的に行いたいということ

  • そのアプローチとして、変分ベイズ(variational Bayesian)を使う。これは事後分布の近似を最適化を含んでいて、その最適化に確率勾配法を用いることでより簡単に最適化が可能ということ。

  • それにより、MCMCといった計算に時間のかかる反復推論スキームを必要としないでシンプルなサンプリングからモデルのパラメータを効率よく学習、そして推論もできるようにしたということ


あとはメソッドあたりのセクションで最適化と思われる手法の数式やその説明とかがビャーっと書いてあった。わかんなかった

この論文を読んだときに、初めて変分ベイズなるものを知った。メソッドの説明に

  • 連続または離散値の変数(入力) x が連続確率変数 z を含む何らかのプロセスで生成されているとする。すなわち入力 x はある条件付き分布{ \displaystyle P_\theta{\ast} (x|z) }から生成されるということ。

  • zとパラメータ{ \displaystyle \theta }観測できない。そのためパラメータ{ \displaystyle \theta }に対する近似最大尤度または最大事後の推定を行う

生成に必要なパラメータが観測できないから、近似及び推定でパラメータを決定するということだな。そしてパラメータ{ \displaystyle \theta }をいい感じに推定できれば、それを用いて実際のデータxに似た人工データを生成できるというのが人から聞いた時の話に繋がるのかな。変数 z を正規分布に見たてるとかか?

大体読んで、変分ベイズとか全くわからん・・・何で事後分布の推定とかが出てくるんだ?と頭を抱えたがこんなことでめげる自分ではない

変分ベイズについて理解を深めればこの論文をより充実して読めるはずと思い、調べることにした。どうやらベイズ学習とはなんぞやというとこから理解をする必要があるっぽい。以下勉強中なので振り返り用のメモ

ベイズ学習という名前からベイズの定理のあの式が出てくる

{ \displaystyle p(\omega | D)p(D) = p(D,\omega ) = p(D|\omega )p(\omega) }

観測データ{ \displaystyle D }を生成するための確率的な法則、確率モデルと呼びこれを考える。確率モデルを作るのに必要なものは観測データ{ \displaystyle D }が何らかのプロセスに従って生成されるとしてその条件付き確率

{ \displaystyle p(D|\omega )}

と、そのプロセスに相当するパラメータ{ \displaystyle \omega }事前分布

{ \displaystyle p(\omega )}

この2つの組{{ \displaystyle p(D|\omega ),p(\omega) }}が必要。言い換えれば観測データはあるプロセスに依存しているとも言える。
等確率のサイコロで言えば、観測データがサイコロを降って出た目で、プロセスの部分は各サイコロの目がでる確率と考えていいかな。

問題はパラメータ{ \displaystyle \omega }観測できない状況のとき。観測できるのは上で言えばサイコロを降って出た目だけ。
ある6面のサイコロをN回降って、出た結果(観測データ)からそのサイコロの各面がでる確率を求めることができるだろうか。

これは確率で言えば、観測データが与えられたもとでのパラメータ{ \displaystyle \omega }の確率

{ \displaystyle p(\omega |D)}

に相当すると解釈できる。事後確率ともいう。これはベイズの定理を使えば
{ \displaystyle p(\omega |D) = \frac{p(D|\omega )p(\omega)}{p(D)}}

右辺を計算すれば事後確率を計算できそうだ

サイコロの各面が出る確率を求めたいのに右辺にはパラメータ{ \displaystyle \omega }に関するものが出てきている。どうするんだというと

自分で指定する

ええ・・・。例えば「多分6面サイコロだし、等確率で考えるのが無難なはず・・・」と仮説を立ててパラメータ{ \displaystyle \omega = \frac{1}{6}}と固定値を与える。
ベイズ学習ではこのようにパラメータに関する仮説、もとい事前分布{ \displaystyle p(\omega )}を自分で与えて観測データを元にその仮説を更新、尤もらしいものにしていく。観測できないパラメータを観測できるデータを元に明らかにするという認識で合っているのだろうか。

事前分布{ \displaystyle p(\omega )}については固定値を与えてあげる他に、正規分布やベータ分布などの確率分布を与えることもできる。VAEでは生成データに必要なパラメータを正規分布で与えるといった文章があった。正規分布に必要な分散パラメータはウィシャート分布から与えるといったこともできるし複雑そう

VAEで生成は画像だけと拘ってしまっていたが、二項分布またはカテゴリ分布を用いればインチキな裏表のコインや6面サイコロなど色々作れそうだ。

長くなったので多分続く

kaggleのメルカリ価格予測コンペの反省とword2vec、Embeddingについて

そういえば年末年始あたりにメルカリのコンペに冬休みの自由研究として参加してました

他のことに追われていたらいつの間にかコンペが終了したので反省という名の手法の振り返りをする

コンペ自体の詳細は以下のリンクから

Mercari Price Suggestion Challenge | Kaggle

何をするコンペだったかというと主催側で商品名、商品の品質(5段階)、商品のカテゴリ名や説明文などが100万件以上あるデータを提供するのでそこから与えられた商品の価格を予測してねっていう感じのコンペ

価格の予測というわけで二値分類とかではないから半教師分類が使えなくて困った(値段予測を10ドル区切りのnクラス分類と置けばゴリ押しできたかも)

まずは自分が行ったデータ分析をば。コードはここ

与えられた訓練用のデータtrain.tsvの欠損値の確認を最初に行うtsvファイルで与えられたので読み込みで最初こけた

# 欠損値の確認
train_df.isnull().sum()

---
train_id                  0
name                      0
item_condition_id         0
category_name          6327
brand_name           632682
price                     0
shipping                  0
item_description          4
dtype: int64

訓練データは140万件ほどある中、brand_nameは63万件も欠損値があることがわかる。
学歴の偏差値でもそうだが、シャネルやルイヴィトンといったブランド名が付与されているだけである程度の価格は検討つきそうという直感があったので、brand_nameが付与されているか否かの二値の値を新たに与えることにした

# brand_nameが存在するなら1を返し、無いなら0を返す
def isBrandname(x,name):
    brand_name = str(x['brand_name'])
    if(brand_name==name):
        return 0
    return 1

fill_nan_brand_name = "Unknown"
train_df['brand_name']=train_df[['brand_name']].fillna(fill_nan_brand_name)
train_df['is_brand_name'] =  train_df.apply(lambda x: isBrandname(x,fill_nan_brand_name), axis=1)

次にcategory_nameの中身を確認した。value_countで中身を見てみると一部でこんな感じ
f:id:Owatank:20180322111441p:plain

カテゴリ数が1って140万もデータあるのになんか嫌だな・・・one-hot絶対したくねえ・・・と思い悩む。
カテゴリの書式はどれも/でsplitできそうだと考えて左から1つsplitしたものを取ってそれをfirst_category_nameという新たなデータとして加えることにする

def get_first_category_name(x):
    category_name = str(x['category_name'])
    return category_name.split('/')[0]

# category_nameの第一カテゴリを抜き取る
# 欠損値は Unknown で埋めておく
fill_nan_category_name = "Unknown"
train_df['category_name']=train_df[['category_name']].fillna(fill_nan_category_name)

train_df['first_category_name'] =  train_df.apply(lambda x: get_first_category_name(x), axis=1)

first_category_namevalue_countは次のようになった

f:id:Owatank:20180322112005p:plain

カテゴリを一つ抜き取ったので全体として意味があるものというのは失われてしまった
カテゴリは3つくらいsplitできそうだったので同様にsecond_category_namethird_category_nameも加えた

ここでグのサマーインターンのときに盗み聞きした懇親会での言葉を思い出す。
日付という文字列のデータから日付を得るように、nameから何かしら得ることはできないだろうか

ということでbrand_nameが無いものに対してnameからブランド名を抜き取ってそれをbrand_nameとして与えてあげることにする

brand_dict = dict(train_df['brand_name'].value_counts())

def extract_brandname_from_name(x,b_dict):
    name = str(x['name'])
    name_split_list = name.split(' ')
    if(x['is_brand_name'] != 1):
        for item in name_split_list:
            if(item in b_dict):
                return item
    return str(x['brand_name'])

train_df['new_brand_name'] =  train_df.apply(lambda x: extract_brandname_from_name(x,brand_dict), axis=1)

new_brand_nameという名前で新たに作ったので、これに対しても最初に行ったブランド名があるか無いかの処理を同様に行った
結果としては10万件ほどブランド名があるものが増えた。

だいたいこんな感じでデータの分析を終えた。自分ではこれがまだ精一杯(´・ω・)

分析して新たに作ったデータで予測を行う学習モデルを構築する

# 入力層
input_dim = 8
X = tf.placeholder(tf.float32, shape=[None, input_dim], name="input")
t = tf.placeholder(tf.float32, shape=[None, 1])
# パラメータ1
stddev = np.sqrt(2.0 / input_dim)
input_node_num = 128
W1 = tf.Variable(tf.truncated_normal([input_dim,input_node_num], stddev=stddev))
b1 = tf.Variable(tf.constant(0.1, shape=[input_node_num]))

# パラメータ2
stddev = np.sqrt(2.0 / input_node_num)
hidden1_node_num = 256
W2 = tf.Variable(tf.truncated_normal([input_node_num,hidden1_node_num], stddev=stddev))
b2 = tf.Variable(tf.constant(0.1, shape=[hidden1_node_num]))

# パラメータ3
stddev = np.sqrt(2.0 / hidden1_node_num)
hidden2_node_num = 512
W3 = tf.Variable(tf.truncated_normal([hidden1_node_num,hidden2_node_num], stddev=stddev))
b3 = tf.Variable(tf.constant(0.1, shape=[hidden2_node_num]))

# パラメータ3
stddev = np.sqrt(2.0 / hidden2_node_num)
output_W = tf.Variable(tf.truncated_normal([hidden2_node_num,1], stddev=stddev))
output_b = tf.Variable(tf.constant(0.1, shape=[1]))
keep_prob1 = tf.placeholder(tf.float32) # ドロップアウトする割合
keep_prob2 = tf.placeholder(tf.float32) # ドロップアウトする割合

layer1 = tf.nn.relu(tf.matmul(X,W1) + b1)

layer2 = tf.nn.relu(tf.matmul(layer1,W2) + b2)
layer2_drop = tf.nn.dropout(layer2, keep_prob1)
layer3 = tf.nn.relu(tf.matmul(layer2_drop,W3) + b3)
layer3_drop = tf.nn.dropout(layer3, keep_prob2)

output_layer = tf.matmul(layer3_drop,output_W)+output_b

p = tf.nn.relu(output_layer,name="output")
# 荷重減衰
norm_term = tf.nn.l2_loss(layer1) + tf.nn.l2_loss(layer2) +tf.nn.l2_loss(layer3)
# 正則項
lambda_ = 0.0001
# 損失関数(MSE)
loss = tf.reduce_mean(tf.square(p - t))+ lambda_*norm_term
#loss = tf.reduce_mean(tf.square(p - t))
# 学習アルゴリズム
optimizer = tf.train.AdamOptimizer()
train_step = optimizer.minimize(loss)

いつも通りのMLPで構築してみた。

しかし結果のスコアは0.713ほどだった。(値が小さい方がいいからクッソ悪い)
何故だと悩んで、そういえばitem_description使ってないな・・・と思った。
他の参加者はitem_descriptionを使用しているのだろうか?と思い、カーネルを漁ることに

するとこんな感じのコードを見つけた

tok_raw = Tokenizer()
tok_raw.fit_on_texts(raw_text)

train_df["seq_item_description"] = tok_raw.texts_to_sequences(train_df.item_description.str.lower())
test_df["seq_item_description"] = tok_raw.texts_to_sequences(test_df.item_description.str.lower())
train_df["seq_name"] = tok_raw.texts_to_sequences(train_df.name.str.lower())
test_df["seq_name"] = tok_raw.texts_to_sequences(test_df.name.str.lower())

!?
え、何これは・・・

層の定義においてはこんなだった

emb_name = Embedding(MAX_TEXT, 50)(name)
emb_item_desc = Embedding(MAX_TEXT, 50)(item_desc)

!!?!?

Embeddingって何だ・・・と当時の自分は思ったが、試しにこれらを使ってnameitem_descriptionを特徴として使いつつ、自分で得た新たなデータも加えて学習させてみるかと試して見たところ、スコアが0.4656とハチャメチャ上がった

当時は全くもって謎だったが、最近自然言語処理について調べたりしていたのでここで定義していたEmbeddingはまずEmbed自体の意味が埋め込みという意味で、埋め込みというのは文字を実数のベクトルに変換して特徴として扱えるようにするって感じなのかなと自分で考えた。

だから最初にtok_raw.texts_to_sequencesitem_descriptionの文章を単語に分割したり、ストップワードの排除やステミングといった前処理をかけてあげたりしていたのだ・・・

埋め込みによるベクトルの変換においてはappleなら[1,0,0,0,0,...]といったone-hotのような変換ではなく、apple = 果物+赤+...といった単語の足し算で表現するようにして[1,0,0,1,0,...]としたり、あえて単語に重みをつけて表現することもできるそうだ

最近読んだポアンカレ埋め込みという論文では、word2vecではユークリッド空間による埋め込みを行っているが、双曲線空間の中に埋め込むことで、ユークリッド空間の埋め込みより単語同士の類似性、つまり距離や潜在的な階層表現などにおいてより優れる結果を出せると書いてあった。しかもベクトルによる表現も倹約に表現できるらしい。

双曲線空間すげえ!!!!!!!!!!!

無敵じゃんこんなの・・・概要だけでめちゃくちゃワクワクしたし銀河を感じてしまった・・・
しかしこの論文で出てきた最適化方法あたりでリーマン最適化、測地線、などなど聞いたこともない単語がたくさん出てきて全然わからなかった
どうやら噂の多様体というものを知る必要があるらしい。

今まで機械学習って確率統計とか最適化数学あたりが重要なんかなーと思っていたが、自然言語処理において幾何学ってこんなロマンがあるし、こういった問題に対して登場してくるのかと思える良い発見ができるコンペだった。結果はボロクソだったが

ベクトルの射影と内積について

確率統計の本を読んでいて、共分散行列あたりで射影の話が出てきた。
そういえば射影についてのイメージがあやふやだったなと思ったのでメモ
かなり前に読んだフーリエの冒険を参考にした。

f:id:Owatank:20180224140956p:plain

上の画像において{ \displaystyle \vec{B} }{ \displaystyle \vec{A_1} }{ \displaystyle \vec{A_2} }に射影するというのは{ \displaystyle \vec{B} }を射影したい方向のベクトルと直交するように下ろして交わった時のベクトル{ \displaystyle \vec{P_1} }{ \displaystyle \vec{P_2} }が射影ベクトルとなる。

何をしているかといえば{ \displaystyle \vec{B} }{ \displaystyle \vec{A_1} }{ \displaystyle \vec{A_2} }ではどういったもので表されるかみたいなものを{ \displaystyle \vec{P_1} }{ \displaystyle \vec{P_2} }が示している。{ \displaystyle \vec{B} }が卵を表すベクトルで{ \displaystyle \vec{A_1} }がサンドイッチを表すベクトルなら{ \displaystyle \vec{P_1} }はタマゴサンドを表す(卵を使ったサンドイッチのネタならなんでもいいけど)。

なんで直交していないといけないんだろって思ったけど{ \displaystyle \vec{P_1} + \vec{P_2} = \vec{B}}という関係が成り立つから何となく察した。

実際に{ \displaystyle \vec{B} }{ \displaystyle \vec{A_1} }に向かって垂直に下ろしたとして、交わる{ \displaystyle \vec{P_1} }を求めてみる。

f:id:Owatank:20180224142903p:plain

{ \displaystyle \vec{B} }{ \displaystyle \vec{A_1} }に向かって垂直に下ろしたときのベクトルは{ \displaystyle \vec{P_1} - \vec{B}}で表せる。
{ \displaystyle (\vec{P_1} - \vec{B})}{ \displaystyle \vec{A_1} }と垂直だから内積は0になるのはわかる。
そして{ \displaystyle \vec{P_1} }{ \displaystyle \vec{A_1} }の長さが違うだけのものなので(スカラー倍)以下の関係になっている。

{ \displaystyle \vec{P_1} = x\vec{A_1}} (xは定数)


つまり{ \displaystyle \vec{P_1} }は 定数x を求められれば実質求められる。いくつか悪さできそうな関係式{ \displaystyle \vec{A_1} \cdot (\vec{P_1} - \vec{B}) = 0}{ \displaystyle \vec{P_1} = x\vec{A_1}}から

{ \displaystyle \vec{A_1} \cdot (\vec{P_1} - \vec{B}) = 0}{ \displaystyle \vec{P_1} = x\vec{A_1}} を代入して
{ \displaystyle \vec{A_1} \cdot (x\vec{A_1} - \vec{B}) = 0} になる。これを展開して
{ \displaystyle x\vec{A_1} \cdot \vec{A_1} - \vec{A_1} \cdot \vec{B} = 0}つまり{ \displaystyle x\vec{A_1} \cdot \vec{A_1} = \vec{A_1} \cdot \vec{B} }が得られる。
x だけの式に変形して

{ \displaystyle x = \frac{ \vec{A_1} \cdot \vec{B}}{\vec{A_1} \cdot \vec{A_1}} }  { \displaystyle x\vec{A_1} = \frac{ \vec{A_1} \cdot \vec{B}}{\vec{A_1} \cdot \vec{A_1}}\vec{A_1} }

最後両辺に{ \displaystyle \vec{A_1} }をかけたのは{ \displaystyle \vec{P_1} = x\vec{A_1}}の右辺に上記を代入したいため。結果として

{ \displaystyle \vec{P_1} = \frac{ \vec{A_1} \cdot \vec{B}}{\vec{A_1} \cdot \vec{A_1}}\vec{A_1} }が得られる。


ただの定数xは分子も分母も内積な形で表されていたのだ・・・
{ \displaystyle \vec{A_1}}{ \displaystyle \vec{B}}自体が直交していたらxの分子の内積は0になるので射影した値も0になるのがわかる。

中学か高校生の頃にこんな感じの式を見たことはないだろうか?

{ \displaystyle \vec{X}\cdot \vec{Y} = |\vec{X}| |\vec{Y}|cos\theta}{ \displaystyle \vec{X}\cdot \vec{X} = |\vec{X}|^2}


内積ってやつだ。これでさっき得られた式を書き換えてみる。

{ \displaystyle \vec{P_1} = \frac{ \vec{A_1} \cdot \vec{B}}{\vec{A_1} \cdot \vec{A_1}}\vec{A_1} = \frac{ |\vec{A_1}| |\vec{B}| cos\theta}{|\vec{A_1}|^2}\vec{A_1} = |\vec{B}|cos\theta \frac{\vec{A_1}}{|\vec{A_1}|}}

うーん。分子と分母の内積な形のときより得体の知れない形になってしまった。cosを何とかしたい
f:id:Owatank:20180224160416p:plain
三角関数でよくみたやつだ。ん・・・?待てよ・・・?
cosをベクトルの長さから表すことができるんじゃないか!?
f:id:Owatank:20180224161439p:plain

{ \displaystyle cos\theta = \frac{|\vec{P_1}|}{|\vec{B}|}}より{ \displaystyle |\vec{P_1}| = |\vec{B}|cos\theta}、ベクトル{ \displaystyle \vec{P_1}}の長さが得られた。おほ^〜

これらのことから
求めたい射影ベクトル{ \displaystyle \vec{P_1} = \frac{ \vec{A_1} \cdot \vec{B}}{\vec{A_1} \cdot \vec{A_1}}\vec{A_1} }の分母や分子の関係式は{ \displaystyle \vec{P_1} = |\vec{B}|cos\theta \frac{\vec{A_1}}{|\vec{A_1}|}}という得体の知れない関係式に直すことができ、それは f:id:Owatank:20180224163131p:plain
{ \displaystyle \vec{P_1}}{ \displaystyle \vec{A_1}}の方向(単位ベクトル)に{ \displaystyle |\vec{P_1}| = |\vec{B}|cos\theta}の長さだけ進んだベクトルという見方をすることできた。

そもそも射影って何が便利なんだろと考えたけど{ \displaystyle \vec{B}}をりんご、オレンジ、桃の3つの果物を合わせたミックスジュースだとしたらそれを各成分ごとに分解したいといった時に便利なんかなと考えた。いや逆に射影ベクトルからミックスジュースのベクトルを作ることも可能なのか・・・?

今更だけど内積って二つのベクトルが同じ向きではないときに、どっちかのベクトルの方向に合わせて({ \displaystyle |\vec{B}|cos\theta}のような処理)をして二つの長さを掛け合わせるといったイメージでいいのかな

内積もそうだけど距離というか位相というか連続や隣の概念のようなものについても勉強すべきだなと思った。

参考アンド宣伝
フーリエの冒険
フーリエの冒険
フーリエの冒険

機械学習の勉強を始めて1年ほど経ったので振り返る

あまりこういうことするのは好きではないが、これからも勉強を続けていってどれくらい成長できたか確認したいと思ったので、タイトル通り機械学習の勉強を始めて1年ほど経ったので振り返ることにした。

始めた発端

学部2年の10月頃、大学の授業にあまり追われなくなり暇になってたところ先輩から「はじめてのパターン認識」という本を貸してくれたので読むことにした。
わからないところも多かったけど、パターン認識って(プロトタイプとかからの)距離から分類したり、勾配とか使うんだなーくらいのイメージが付いた。
授業もあったしノートに写しながら読んでいたので、読み終わるのに2ヶ月くらいかかった記憶がある。

その後1〜2月頃(うろ覚え)にTensorflowを使ったディープラーニングのアルバイトもどきをやってみないかと声をかけられたのでやることにした。
このアルバイトではライブラリの使い方や導入方法、簡単な回帰や分類の記事を書いたり(バージョンの変化による)修正したりするものだった。
始めた頃はコードで定義されている損失関数やソフトマックスってなんだ?みたいな雰囲気でディープラーニングをやっている状態で今覚えばかなりひどかった。

3月

Tensorflowで構築したモデルをAndroidアプリとして動かしたいと言われリファレンスを読みまくる日々に追われた。
この記事を参考にしてアヤメの分類をアプリとして動かしたり
f:id:Owatank:20180211114241p:plain
この方法だとc++でなぜかコードを書かないといけなくて辛かった。
その後モルガンさんの素晴らしい記事を見つけて、おかげでモデルのファイルを読み込んで入力値を渡してあげるだけで済むようになって咽び泣いた。

春休みに入って時間もできたので真面目にディープラーニングの仕組みみたいなものについて勉強しようかなと思い始める。
何から始めればいいか悩んでいたところ何故か会社に「ゼロから作るDeep Learning」という本が転がっていたのでこっそり借りて読んだ。
はじパタを事前に読んでいたおかげかサクサク読めたしTensorflowを使ってネットワークの構築で定義していたtf.layers.conv2dのパラメータの意味や、tf.nn.dropoutとかの意味がわかっていってめっちゃ楽しかった。伏線回収しているような気分だった。

4月~8月

魚の本のおかげで、もっとディープラーニングについてしっかり勉強したいなと思うようになって次に研究室に置いてあった青くてイルカの絵が載っている岡谷さんの「深層学習」という本を読んだ。自己符号化器の内容が読んでいて一番ワクワクした。
かなり授業を取ったので勉強に充てる時間が全くなくて半分以上読む頃には夏休みに入っていた。

それとは別で物体検出というものに興味を持って、SSDという手法を知る。自分で実装してみたいと思いまず仕組みを知ろうと思った。SSDの行なっていることを知るにはRCNNSPP-netなどの論文から読まなくてはいけないらしく初めて論文を読んだ。英語力がスカスカで一つ読むのに2週間以上かかった。

論文を読んで思ったのは、論文内で定義されている損失関数などの数式の意味があまり理解できなかったということ。
ディープラーニングの仕組みではなく数学の基礎的なものをやり直そうかなと思い始めた。

9月

初めての機械学習インターンに参加した。割とボロボロな結果だったけど、機械学習にしてもディープラーニングにしても、必要なデータは自分で整形したり特徴量の絞り込みをしたりしなくてはいけないということを痛感する。

今までMNISTやアヤメのデータセットなど用意されたデータでやっていたのでこれはかなり自分の中では大きかった。

10月~11月

9月のインターンのこともあり、「戦略的データサイエンス」という本を借りて読んでいった。この本のおかげで、エントロピーなるものや混同行列に決定木など色々なことを知ることができた。
またデータセットの整形、前処理ということにも慣れようと思い、kagglのフリーのデータセットpandasというライブラリを使ってデータの操作の練習をした。「Pythonによるデータ分析入門」という本が図書館にあったのでちまちま借りた。

11月の最後に某リクのデータサイエンティストイベントなるものがあり何故か参加できた。学部生が自分だけで辛かった。
所属している研究室は卒業単位が取れないと研究させてくれない(研究スペースすら割り当てられてなかったけど)らしいので、面接のときに研究内容を話すことが全くできなかったし、面接に来てくれた各企業の人からは「いや絶対院に行ったほうがいいよ」と言われて進路相談を受けている気分だった。優秀な人を取りに来ているのにすごく申し訳なかった。

他の参加者からエントリーシートがスカスカじゃんとか、googleに就職したいとか書いておこうよwとか今の時期から機械学習の勉強してるみたいだけどブームすぎたらどうするの?とかイキられて言われて懇親会のピザがしょっぱかった。
旧帝大修士や博士というだけで学歴ビーム喰らっているのにさらにオーバーキルされた気分だった。

それでも企業の人やイキリ修士、博士の意見から、機械学習の勉強をするのはいいとして、一体どういったことをしたいのか(研究としても仕事としても)ということを考えようと思った。今まではわかると楽しいからやっていたという感じで何がしたいかは全くなかった。

12月

4月からの研究テーマや将来何しようかなと考えつつ、数学的な基礎を固めようと思い「プログラミングのための線形代数」という本を読んだ。
1年生の頃は単位のためと適当にやっていた線形代数だったけど基底や固有値固有ベクトルの存在などのありがたさがわかってくると興奮した。
kagglのメルカリコンペにこっそり参加した。

1月

「スパース性に基づく機械学習」という本を帰省している間に読もうと思ったが見事に数式の部分で躓いたので「プログラミングのための確率統計」を読むことにした。読んでいくと「深層学習」の本で載っていた白色化の式の意味とかがわかってやっぱ基礎がガバガバと再認識した。

自然言語処理について触ろうと思ってWORD2VECなどの論文を読んだいたら、ユークリッド空間やリーマン勾配、測地線とか謎の単語が出て来て!?となった。なんで言語処理なのにこんなものが出てくるんだ・・・多様体って何だ・・・・
論文でボコボコにされたが単語の類似性などを保ったまま実数値のベクトルに変換?するには幾何学の考えが必要なんだなと思った。
それまで機械学習の勉強って線形代数や確率統計、最適化数学とかをやっとけばいいのかな〜とかのんきに考えていたが、他の数学の分野を知っておくと考えをこういった手法のアプローチが提案できるのかと感動した。 というか幾何学にものすごい興味を持った。

幾何学の勉強は位相空間論、多様体論、双曲幾何とたくさんあるし一つ学ぶのにすごい時間がかかるそうだ。
そのおかげか世界が広くなったように感じてまだまだ薄っぺらいままで全然成長できてないんだと実感した。
学ぶべきこと、やりたいことは沢山あるし自由な時間は4月から少なくなっていくけど、自分がどうなりたいかを考えてやっていきたい

自然言語処理に触れてみる(メルカリのデータセットでカテゴリ分類)

よく聞く自然言語処理をやっとこさ触ってみることにした。

自然言語処理自体については図書館で借りた戦略的データサイエンスという本がちょっとだけ触れていてくれたのでこれを元に触ってみることにする
www.oreilly.co.jp

今までSVMでも決定木でも学習に必要な特徴量は大体離散値か実数値で、文字列を特徴量として渡したことがない。画像も数値の集まりで表現したし、正解ラベルも分類ならdogは1、catは2とか離散値かone-hot表現に直した

よくあるニュースの説明文からカテゴリ分類をするといったものも、説明文を数値、特徴ベクトルに変換して学習している。

その変換アプローチとして簡単かつ低コストなのがbag-of-word(BoW)と呼ばれるもの。これは全てのドキュメントを独立した単語の集まりと見なす(文法、単語の順序、文構造、句読点とかは無視する)
このアプローチではドキュメントに含まれる単語は全てドキュメントにとって重要なキーワード(特徴)になり得るとみる

あるドキュメントにおける特徴値はどんなものになるかというと全ての単語をトークンとして扱って、あるドキュメントにそのトークンが含まれているかどうかをベクトルで表現する。

用語の出現するか否かの区別に加えて、用語が使用された回数でさらに区別をしようということで用語の出現頻度を数える。ドキュメント内に存在する用語の重要性が出現回数に応じて高くなるときとかの重み付けに有効らしい。用語出現頻度(Term Frequency,TF)と呼ばれる

長いドキュメントの方が多くの単語を持っているから用語出現頻度の値は長いドキュメントの方が大きいはず。でも長いドキュメントだからといって短いドキュメントよりある用語が重要な意味を持つとは限らない

そのため、用語出現頻度がドキュメントの長さに依存しないようにドキュメント内の総単語数で用語の出現回数を割るといった正規化を施すこともあるらしい。

自前で雑に実装したもの

import collections
import re

def TF(desc,terms,norm=False):
    tf_dict = {}
    d = desc.lower()
    for term in terms:
        i = 0
        for m in re.finditer(term,d):
            i += 1
        if i > 0 and norm==False:
            tf_dict[term] = i
        elif i > 0 and norm==True:
            tf_dict[term] = i/len(set(d.split(' ')))
    return tf_dict

正規化の方はsetでドキュメントの長さを取ってしまったけど重複ありならいらなかったかもしれない・・・

頻度のリストを作る際に、まず用語に以下のような処理を施してあげるのが基本

  • 大文字と小文字の正規化 単語は全て小文字に変換する。これはJsonJSONといった単語を同じ用語と見なすため

  • ステミング
    英単語において動詞announceannouncesannouncedannouncingと変化する。これらはannouc + 接頭辞に分解できるため、接頭辞を削除してannoucという用語として扱うといったことをする。名詞も複数形は単数形に直してあげる

  • ストップワードの除去
    ストップワードとは英語において出現回数の多い一般的な単語のことを指す。これらは自然言語処理の対象外になる単語だそうだ。theとかamとかandとかそこらへん。最近でたItとかいうホラー映画のタイトルみたいにストップワードが重要な意味を持つこともあるから対象によって除去は変わってきそうだ


この前処理はgensimという便利なライブラリのgensim.parsing.preprocess_stringというメソッドを使うことで上記の処理を一気にすることができる

pre_itemname = []
# gensim.parsing.preprocess_string(文字列)は以下の関数を実行した結果を返してくれる
# strip_tags() : <a>hoge</hoge>といったhtmlのタグを消してくれる
# strip_punctuation() : ,や!といった記号を消してくれる
# strip_multiple_whitespaces() : \rや\nといった空白文字を消してくれる
# strip_numeric() : 文字列の中にある数字を消してくれる
# remove_stopwords() : am や but といったストップワードと呼ばれる単語を削除してくれる
# strip_short() : 一定の長さを持たない単語を削除する(デフォルトでは3)
# stem_text() : ステミング(useやuseful,usingなら -> us)を行ってくれる
for i in itemname:
    pre_itemname.append(gensim.parsing.preprocess_string(i))

データとしてkaggleのコンペにメルカリの価格予測のデータを使うことにする。規約見た限り大丈夫なはず。
f:id:Owatank:20180204161831p:plain

処理の結果としては以下のようになった
f:id:Owatank:20180206112721p:plain
どことなく重要な単語が消えてるようなそうでないような・・・まあいいや

自分で実装したTFを適用してみる。結果はこうなった
f:id:Owatank:20180206113305p:plain
商品のタイトルだから単一ドキュメントに重複する単語が登場するのは滅多にないかな

単一のドキュメント内での用語の出現頻度を測定することとは別に解析対象のコーパス(ドキュメントの集まり)において、ある用語がどれだけ一般的かといったものも測定する必要がある。

例えば、ある用語が出現するドキュメントの数が少ないほど、その用語は重要かもしれない。ある用語tに関する希少性は逆文書頻度(IDF)と呼ばれる式で計算できる

{ \displaystyle IDF(t) = 1+log_{2}\frac{総ドキュメント数}{用語tを含むドキュメント数} }

logの中身から、分母(用語tを含むドキュメント数)が小さいほど、IDFの値は大きくなることがわかる

まずこれを計算するにはコーパスからある用語がいくつ出現しているかを調べなくてはいけない

def get_terms_dict(doc):    
    d = collections.defaultdict(int)
    for i in doc:
        # 重複はカウントしない
        terms = set(i)
        for term in terms:
            d[term] += 1
    return d

コーパスでの用語出現頻度のリストを作ってIDFを計算する

def IDF(terms_dict,term,doc_num):
    return (1+np.log2(doc_num/terms_dict[term]))

f:id:Owatank:20180206114931p:plain

TFとIDFを組み合わせたTFIDFと呼ばれるテキストの表現方法(重み付けとも)が有名らしい

{ \displaystyle TFIDF(t,d) = TF(t,d)\ast IDF(t) }

tは用語、dは単一ドキュメントを指す。数式から、TFIDFは単一ドキュメントに対する値であることがわかる。
自分で実装したもの

def TFIDF(tf,idf):
    tfidf_dict = {}
    tf_list = list(tf.items())
    for t in tf_list:
        tmp_dict = {}
        for k,v in t[1].items():
            tmp_dict[k] = v*idf[k]
        tfidf_dict[t[0]] = tmp_dict
    return tfidf_dict

f:id:Owatank:20180206120226p:plain
TFの値を正規化していないからか、基本的に1や2とかの値が多いため、IDF値のまんまな物が多い・・・

こうすることで用語が登場しているかどうかの0と1の特徴ベクトルではなく重み付けを行ったもので学習データとして使うことができる
    -= ∧_∧
  -=≡ ( ´∀`)
    -=( つ┯つ
   -=≡/  / //
  -=≡(__)/ )
   -= (◎) ̄))

gensimのライブラリを使ってさっきのTFIDFの値が合っているか、その後学習データでカテゴリの分類をしてみる

まずコーパス内での用語出現回数リストは次のように作成する

itemname_corpora_dict = gensim.corpora.Dictionary(pre_itemname)

TFIDF値を計算するには先にドキュメント(前処理済み)をBoWに変換してから行う

bow_dict = {}
for i in range(0,len(pre_itemname)):
    bow_dict[i] = itemname_corpora_dict.doc2bow(pre_itemname[i])

tfidf_model = gensim.models.TfidfModel(bow_dict.values(),normalize=False)
tfidf_corpus = tfidf_model[bow_dict.values()]

自分で作ったTFIDFのものと結果を比較する。適当に2個持ってきた結果
f:id:Owatank:20180206122854p:plain

なんかgensimの方はIDFの計算において1が加算されていないっぽい?そんなことしたらlogの中身が1のとき0の値になるけどいいんだろうか
多分計算式としてはそれ以外は合ってるかな

この重み付けされたBoWのベクトル(商品の名前)で、カテゴリ分類をしてみる。

コーパス内にある用語の数(次元数)が19797と多いので次の考え方を考慮して絞り込みをする

  • 用語はレアすぎてはいけないということ
    解析対象のコーパスのどっかのドキュメント内に滅多に使われない単語が登場しているとする。この用語が重要かどうかは利用次第で重要度は変わってくる。クラスタリングのためならあまり使われない単語は分類の基準にはなれず重要度は低くなる。なので、ある用語が出現するドキュメントの数に下限を設定して、その加減を超えたものを用語として扱うといった制限を付ける。

  • 用語は一般的すぎてもいけないということ
    ストップワードのようにコーパスにある全てのドキュメントに出現するような用語は分類、クラスタリングといった点では区別に使えない。上とは反対にある用語が出現するドキュメントの数(または割合)に任意の上限を設けて、過度に出現する用語を取り除く

# レアすぎる単語、一般的すぎる単語の除去
item_corpora_dict.filter_extremes(no_below=3,no_above=0.5)

実行した結果、コーパス内にある用語の数は7174となった。それでもまだ多い気がする
これは一種の次元削減として見てもいいのかな

次にTFIDFを正規化ありで実行し、重み付けされた特徴ベクトルを得る

tfidf_model = gensim.models.TfidfModel(bow_dict.values(),normalize=True)
tfidf_corpus = tfidf_model[bow_dict.values()]

feature_vec = gensim.matutils.corpus2dense(list(tfidf_corpus), num_terms=len(itemname_corpora_dict))
feature_vec = feature_vec.T

分類のための学習を行う

# TFIDFで重み付けしたもので分類
from sklearn.grid_search import GridSearchCV
from sklearn.cross_validation import StratifiedKFold
from sklearn.linear_model import SGDClassifier
clf = GridSearchCV(SGDClassifier(penalty="l2"), param_grid={'alpha':[0.01,0.05,0.001,0.0001]})

# StratifiedKFold(y, n_folds, shuffle=True) 交差検証用にデータを分割してくれる
for i, (itr, ite) in enumerate(StratifiedKFold(y_test,n_folds=5, shuffle=True)):
    clf.fit(X_train[itr], y_train[itr])
    train_pred = clf.predict(X_train[ite])
    train_acc = metrics.accuracy_score(train_pred,y_train[ite])
    print("{0} finished. test accuracy : {1}".format(i+1,train_acc))

今回はGridSearchCVStratifiedKFoldというメソッドを使ってみた。前者は最適なパラメータを探してくれて、後者は交差検証のためのメソッドらしい

f:id:Owatank:20180206143448p:plain

最終的なテストデータでのAccuracy0.78095だった。

同様のことを重み付けしていないただのBoWでのベクトルで行なったが、こっちでのテストデータのAccuracy0.7827だった。
重み付けしていない方が誤差レベルだけど値が大きいことからここでの問題に対して用語の重み付けはそんなに意味を成さなかったのかな(((((;`Д´)≡⊃)`Д)、;'.・

今回やったもののコードはここ

github.com

PythonとGo言語でSlackからTogglとスプレッドシートを操作するbotを作った

機械学習に全然関係ないです

togglという時間を計測したり分析してくれるサービスがありますね
最近勉強時間を測って振り返ってみようと思い使っているが、スマホやブラウザから操作すると大抵タイマーをストップするのを忘れて作業時間が43時間になったりすることがよくある

正直タイマーのスタートやストップのために一々スマホアプリやwebブラウザを開くのは面倒で困る。
という訳でいつもよく見るslackやtwitterから操作すれば面倒に感じないのでは?と思い勉強の息抜きも兼ねてtogglのタイマーを操作してくれるslackbotを作ることにした。すでにそういうものがある?まあ・・・うん・・・・

最初はpythonで作ったけど後からgo言語でも作った
以下を参考にした

Real Time Messaging APIで秘書botを作ってみる - adish intelligence
golang で始める Slack bot 開発 - at kaneshin
GitHub - toggl/toggl_api_docs: Documentation for the Toggl API

やっていることは、特定のキーワードに反応して対応する操作を行い、メッセージを返すといったもの
例えばstartと入力すればtogglのタイマーをスタートする、add desc hogeと打てばそのtogglのタイムエントリーにhogeという説明文を追加するとか
個人のtogglのAPIトークンを使って操作するので同チャンネル内の他の人からは操作できないように、メッセージの送信者のメールアドレスを確認して特定のユーザーなら操作を行うといったものにしておいた

f:id:Owatank:20180128143706p:plain f:id:Owatank:20180128143316p:plain

それとは別に計測した時間をgoogleスプレッドシートに書き込んでくれる機能も盛り込んだ
用意しておいたスプレッドシートから対応する日付の行を持ってきていい感じに出力してシートを更新したりする

f:id:Owatank:20180128144313p:plain

コードはここ(追記: Goの方パターン諸々綺麗に直しといた)
Go言語のほう
Pythonのほう

開発は全然したことないから関数名、パターンだったり例外処理だったりと雑なのだ
Pythonをここ最近触っていたせいか初めてgoを触ったせいなのか色々とギャップがひどかった。Pythonのが色々とゴリ押しで書けるところがあるからか...