ベイズ推論の事後分布(離散)の更新について
前回の続き
VAEの理解に必要なベイズ学習について - 時給600円
パラメータの事前分布を自分で仮定して、観測データを元により適したパラメータを推定するのがベイズ学習といった話だった。
事前分布として固定値ではなく正規分布といった確率分布を与えてその時にちゃんとパラメータが学習されるのか確認する。
例としてある1枚のコインが存在して、そのコインで100回コイントスをする。
表が10回ほど、裏が90回ほど出たとする。このとき結果から1枚のコインの表が出る確率が求められるか。
100回中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]) )
コイントスは二値の値 { 0 , 1 } を取るので、{0,1}を生成してくれるベルヌーイ分布を使う。(1がコインの表とする)
ここではベルヌーイ分布に必要なパラメータの真の値を 0.16 とした。この値に近いものを観測データから求めるのが目標。
前回の通りにやるとパラメータの推定は i.i.d な観測データ {}としたとき、ベイズの定理から
とできる。
タイトル通り事前分布として固定値ではなく確率分布を与える。前提としてベルヌーイ分布のパラメータの取りうる範囲は (0,1) の間でなくてはいけない。
つまり (0, 1) の間で実数を生成してくれるような確率分布を与えればいいんだな。
ベータ分布なるものは となる実数を生成してくれる連続確率分布らしい。これを使ってみよう。定義は以下のようになっている
a,bは非負の実数でなくてはならず、自分で設定するハイパーパラメータ。 殺傷力ありそうな記号がついた関数はガンマ関数。階乗を表現してるそうだ。
a,bを適当に設定したベータ分布をプロットしてみた結果はこんなんになった
ふわ^〜
aとbの値が小さいと下に凹んだ形状で、大きいと正規分布みたいな山のような形になるのかな。いや数式見れば形状に説明つくんだろうけど・・・
観測データの見えない生成規則がベルヌーイ分布
に従うとしているから、この式と上のベータ分布の式を事後分布の右辺に代入してみる
ヴワッ・・・果てしなく汚い式になってしまった・・・
ガンマ関数と分母はパラメータによらないので、定数Aとおいて見ればこの式は
こんな感じで書けるはずなのだ。ようはとでまとめられる。
この式の形はベータ分布と似ている。共役性とやらで実はをベルヌーイ分布に設定して、事前分布をベータ分布にしたとき事後分布はベータ分布と同じ形の分布になるのだ・・・
何が嬉しいって分母、もとい周辺尤度を計算しなくて済む。
多分この周辺尤度ってパラメータが取りうる範囲で積分、つまりは周辺化して得られるはずだから
こうなるはずなんだよな・・・違うかな・・・
積分するものによっては難しいものあるんじゃなかろうか(´・ω・)
気を取り直して観測データの集合Xからパラメータを推定するを計算するにはをベルヌーイ分布にして、事前分布をベータ分布に設定すれば、ベータ分布のハイパーパラメータに観測データの情報を加えたものから得られることがわかった。
ベータ分布のハイパーパラメータを a = 0.1 , b = 0.1 に設定して、が本当に推定されていくのか見ていく
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))
観測するデータを10件ごと増やしていくと50件以降から真のパラメータの値 0.16 に近くなっていくのが若干わかる
学習できていると見ていいんじゃなかろうか
もういっちょ別の確率分布を使ってパラメータの推定をやってみる
ベルヌーイ分布は二値に対してのみだったが、サイコロの目のようなKパターンの確率も推定してみたい
このようなK次元の確率分布としてカテゴリ分布なるものがある
はといった、K次元のうちk番目の値が1で他の値が0といったonehot表記であること、はで、かつとなることが条件
ベルヌーイ分布でのパラメータのように、このカテゴリ分布では がパラメータになる
次元数をK=6 とし、とすれば等確率のサイコロが表せるんだな
同様に
としたとき事前分布はどうすればいいだろう
ベータ分布は、つまり変数1個しか生成してくれない。同じ (0,1) 区間で実数を多次元で、かつそれらの和が 1 となってくれるようなものを生成してくれる分布はないだろうかと悩むところでディクリレ分布というものがその役割を果たしてくれる。すげえ!!!
の要素は正の実数が条件のハイパーパラメータ
何はともあれ事後分布に代入だ。観測データを { }とすると
ヴォエッ・・。こっちも汚くなったが分母とガンマ関数の部分は定数とみなせるのでAとして、総乗の部分はどちらもkのものだからまとめることができる。整理すると
こんな感じで事後分布をディクリレ分布と似た形の式にみることができるのだ。共役性ありがてえ・・・
等確率でないサイコロを作って、そのサイコロの確率、もといパラメータを推定してみる
まずサンプルとなるインチキなサイコロを投げた結果を作る
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が出る確率、として、これらの重みを結果から推定する
ディクリレ分布に必要なハイパーパラメータの値を全て 0.2 とする
データを10件ずつ増やしていってパラメータ の推定値がどう変わるか見ていく
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)
10件あたりでもそこそこいい値が出ている気がする。データが増えていくほど値が安定していくようなそうでないような
ベルヌーイ分布なら事前分布をベータ分布に、カテゴリ分布なら事前分布をディクリレ分布にといった共役の事前確率分布を取れば計算がかなり簡単になることがわかった。それでも現実の問題によっては共役でない事前分布を取ることとかあるのだろうか?必ず共役な事前分布をとれって訳ではないしな・・・
これはベイズ学習であって自分が知りたい変分ベイズではない。変分ベイズはこのような手順を踏んではいるけどちょっと違う推定の仕方とかそういうのなんだろうか?(´・ω・)というか試したのは離散であって連続値に対する推定ですらなかった
VAEの理解にはまだ遠い
VAEの理解に必要なベイズ学習について
自己符号化器、初めて知った当初は中間層にて入力の次元数より少ない次元数で表現し、出力層(入力と同じ次元数)で復元を行うすごいやつといったイメージがあった。入力にあえてノイズを加えて出力としてノイズを除去したものを得るデノイジング自己符号化器といったものもあった。
これらを知った時、復元できんの!!!!!!!すげえじゃん!!!!!!と感動した思い出がある。そして近頃VAEなるものを知った。(出たのは2015年頃で今更感はあるけども)
人からVAEの話を聞いた限りでは、VAEは入力されるモノがパラメータ(μ,σ)に従う正規分布によって生成されていると仮定して、そのμとσを推定できれば、入力と似たものを出力として得られる、という認識をもった。
やっぱり正規分布は便利なんだなと思いつつ、自己符号化器は復元だけではなく、入力と似たものの生成もできるのかとワクワクした。
個人的に興味を持ったので、VAEの論文を読んでみた。([1312.6114] Auto-Encoding Variational Bayes)
が・・・・駄目っ・・・・・!
自分が読んだ限り
有向確率モデルとやらで事後分布を持つ潜在変数またはパラメータの近似的な推論と学習を効率的に行いたいということ
そのアプローチとして、変分ベイズ(variational Bayesian)を使う。これは事後分布の近似を最適化を含んでいて、その最適化に確率勾配法を用いることでより簡単に最適化が可能ということ。
それにより、MCMCといった計算に時間のかかる反復推論スキームを必要としないでシンプルなサンプリングからモデルのパラメータを効率よく学習、そして推論もできるようにしたということ
あとはメソッドあたりのセクションで最適化と思われる手法の数式やその説明とかがビャーっと書いてあった。わかんなかった
この論文を読んだときに、初めて変分ベイズなるものを知った。メソッドの説明に
連続または離散値の変数(入力) x が連続確率変数 z を含む何らかのプロセスで生成されているとする。すなわち入力 x はある条件付き分布から生成されるということ。
zとパラメータは観測できない。そのためパラメータに対する近似最大尤度または最大事後の推定を行う
生成に必要なパラメータが観測できないから、近似及び推定でパラメータを決定するということだな。そしてパラメータをいい感じに推定できれば、それを用いて実際のデータxに似た人工データを生成できるというのが人から聞いた時の話に繋がるのかな。変数 z を正規分布に見たてるとかか?
大体読んで、変分ベイズとか全くわからん・・・何で事後分布の推定とかが出てくるんだ?と頭を抱えたがこんなことでめげる自分ではない。
変分ベイズについて理解を深めればこの論文をより充実して読めるはずと思い、調べることにした。どうやらベイズ学習とはなんぞやというとこから理解をする必要があるっぽい。以下勉強中なので振り返り用のメモ
観測データを生成するための確率的な法則、確率モデルと呼びこれを考える。確率モデルを作るのに必要なものは観測データが何らかのプロセスに従って生成されるとしてその条件付き確率
と、そのプロセスに相当するパラメータの事前分布
この2つの組{}が必要。言い換えれば観測データはあるプロセスに依存しているとも言える。
等確率のサイコロで言えば、観測データがサイコロを降って出た目で、プロセスの部分は各サイコロの目がでる確率と考えていいかな。
問題はパラメータが観測できない状況のとき。観測できるのは上で言えばサイコロを降って出た目だけ。
ある6面のサイコロをN回降って、出た結果(観測データ)からそのサイコロの各面がでる確率を求めることができるだろうか。
これは確率で言えば、観測データが与えられたもとでのパラメータの確率
に相当すると解釈できる。事後確率ともいう。これはベイズの定理を使えば
右辺を計算すれば事後確率を計算できそうだ
サイコロの各面が出る確率を求めたいのに右辺にはパラメータに関するものが出てきている。どうするんだというと
自分で指定する
ええ・・・。例えば「多分6面サイコロだし、等確率で考えるのが無難なはず・・・」と仮説を立ててパラメータと固定値を与える。
ベイズ学習ではこのようにパラメータに関する仮説、もとい事前分布を自分で与えて観測データを元にその仮説を更新、尤もらしいものにしていく。観測できないパラメータを観測できるデータを元に明らかにするという認識で合っているのだろうか。
事前分布については固定値を与えてあげる他に、正規分布やベータ分布などの確率分布を与えることもできる。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
で中身を見てみると一部でこんな感じ
カテゴリ数が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_name
のvalue_count
は次のようになった
カテゴリを一つ抜き取ったので全体として意味があるものというのは失われてしまった
カテゴリは3つくらいsplitできそうだったので同様にsecond_category_name
とthird_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
って何だ・・・と当時の自分は思ったが、試しにこれらを使ってname
とitem_description
を特徴として使いつつ、自分で得た新たなデータも加えて学習させてみるかと試して見たところ、スコアが0.4656とハチャメチャ上がった
当時は全くもって謎だったが、最近自然言語処理について調べたりしていたのでここで定義していたEmbedding
はまずEmbed
自体の意味が埋め込みという意味で、埋め込みというのは文字を実数のベクトルに変換して特徴として扱えるようにするって感じなのかなと自分で考えた。
だから最初にtok_raw.texts_to_sequences
でitem_description
の文章を単語に分割したり、ストップワードの排除やステミングといった前処理をかけてあげたりしていたのだ・・・
埋め込みによるベクトルの変換においてはapple
なら[1,0,0,0,0,...]
といったone-hotのような変換ではなく、apple = 果物+赤+...
といった単語の足し算で表現するようにして[1,0,0,1,0,...]
としたり、あえて単語に重みをつけて表現することもできるそうだ
最近読んだポアンカレ埋め込みという論文では、word2vecではユークリッド空間による埋め込みを行っているが、双曲線空間の中に埋め込むことで、ユークリッド空間の埋め込みより単語同士の類似性、つまり距離や潜在的な階層表現などにおいてより優れる結果を出せると書いてあった。しかもベクトルによる表現も倹約に表現できるらしい。
双曲線空間すげえ!!!!!!!!!!!
無敵じゃんこんなの・・・概要だけでめちゃくちゃワクワクしたし銀河を感じてしまった・・・
しかしこの論文で出てきた最適化方法あたりでリーマン最適化、測地線、などなど聞いたこともない単語がたくさん出てきて全然わからなかった
どうやら噂の多様体というものを知る必要があるらしい。
今まで機械学習って確率統計とか最適化数学あたりが重要なんかなーと思っていたが、自然言語処理において幾何学ってこんなロマンがあるし、こういった問題に対して登場してくるのかと思える良い発見ができるコンペだった。結果はボロクソだったが
ベクトルの射影と内積について
確率統計の本を読んでいて、共分散行列あたりで射影の話が出てきた。
そういえば射影についてのイメージがあやふやだったなと思ったのでメモ
かなり前に読んだフーリエの冒険を参考にした。
上の画像においてをとに射影するというのはを射影したい方向のベクトルと直交するように下ろして交わった時のベクトルとが射影ベクトルとなる。
何をしているかといえばはやではどういったもので表されるかみたいなものを、が示している。が卵を表すベクトルでがサンドイッチを表すベクトルならはタマゴサンドを表す(卵を使ったサンドイッチのネタならなんでもいいけど)。
なんで直交していないといけないんだろって思ったけどという関係が成り立つから何となく察した。
実際にをに向かって垂直に下ろしたとして、交わるを求めてみる。
をに向かって垂直に下ろしたときのベクトルはで表せる。
はと垂直だから内積は0になるのはわかる。
そしてはの長さが違うだけのものなので(スカラー倍)以下の関係になっている。
つまりは 定数x を求められれば実質求められる。いくつか悪さできそうな関係式とから
は を代入して
になる。これを展開して
つまりが得られる。
x だけの式に変形して
最後両辺にをかけたのはの右辺に上記を代入したいため。結果として
ただの定数xは分子も分母も内積な形で表されていたのだ・・・
と自体が直交していたらxの分子の内積は0になるので射影した値も0になるのがわかる。
中学か高校生の頃にこんな感じの式を見たことはないだろうか?
内積ってやつだ。これでさっき得られた式を書き換えてみる。
うーん。分子と分母の内積な形のときより得体の知れない形になってしまった。cosを何とかしたい
三角関数でよくみたやつだ。ん・・・?待てよ・・・?
cosをベクトルの長さから表すことができるんじゃないか!?
より、ベクトルの長さが得られた。おほ^〜
これらのことから
求めたい射影ベクトルの分母や分子の関係式はという得体の知れない関係式に直すことができ、それは
はの方向(単位ベクトル)にの長さだけ進んだベクトルという見方をすることできた。
そもそも射影って何が便利なんだろと考えたけどをりんご、オレンジ、桃の3つの果物を合わせたミックスジュースだとしたらそれを各成分ごとに分解したいといった時に便利なんかなと考えた。いや逆に射影ベクトルからミックスジュースのベクトルを作ることも可能なのか・・・?
今更だけど内積って二つのベクトルが同じ向きではないときに、どっちかのベクトルの方向に合わせて(のような処理)をして二つの長さを掛け合わせるといったイメージでいいのかな
内積もそうだけど距離というか位相というか連続や隣の概念のようなものについても勉強すべきだなと思った。
機械学習の勉強を始めて1年ほど経ったので振り返る
あまりこういうことするのは好きではないが、これからも勉強を続けていってどれくらい成長できたか確認したいと思ったので、タイトル通り機械学習の勉強を始めて1年ほど経ったので振り返ることにした。
始めた発端
学部2年の10月頃、大学の授業にあまり追われなくなり暇になってたところ先輩から「はじめてのパターン認識」という本を貸してくれたので読むことにした。
わからないところも多かったけど、パターン認識って(プロトタイプとかからの)距離から分類したり、勾配とか使うんだなーくらいのイメージが付いた。
授業もあったしノートに写しながら読んでいたので、読み終わるのに2ヶ月くらいかかった記憶がある。
その後1〜2月頃(うろ覚え)にTensorflowを使ったディープラーニングのアルバイトもどきをやってみないかと声をかけられたのでやることにした。
このアルバイトではライブラリの使い方や導入方法、簡単な回帰や分類の記事を書いたり(バージョンの変化による)修正したりするものだった。
始めた頃はコードで定義されている損失関数やソフトマックスってなんだ?みたいな雰囲気でディープラーニングをやっている状態で今覚えばかなりひどかった。
3月
Tensorflowで構築したモデルをAndroidアプリとして動かしたいと言われリファレンスを読みまくる日々に追われた。
この記事を参考にしてアヤメの分類をアプリとして動かしたり
この方法だとc++でなぜかコードを書かないといけなくて辛かった。
その後モルガンさんの素晴らしい記事を見つけて、おかげでモデルのファイルを読み込んで入力値を渡してあげるだけで済むようになって咽び泣いた。
春休みに入って時間もできたので真面目にディープラーニングの仕組みみたいなものについて勉強しようかなと思い始める。
何から始めればいいか悩んでいたところ何故か会社に「ゼロから作るDeep Learning」という本が転がっていたのでこっそり借りて読んだ。
はじパタを事前に読んでいたおかげかサクサク読めたしTensorflowを使ってネットワークの構築で定義していたtf.layers.conv2d
のパラメータの意味や、tf.nn.dropout
とかの意味がわかっていってめっちゃ楽しかった。伏線回収しているような気分だった。
4月~8月
魚の本のおかげで、もっとディープラーニングについてしっかり勉強したいなと思うようになって次に研究室に置いてあった青くてイルカの絵が載っている岡谷さんの「深層学習」という本を読んだ。自己符号化器の内容が読んでいて一番ワクワクした。
かなり授業を取ったので勉強に充てる時間が全くなくて半分以上読む頃には夏休みに入っていた。
それとは別で物体検出というものに興味を持って、SSD
という手法を知る。自分で実装してみたいと思いまず仕組みを知ろうと思った。SSDの行なっていることを知るにはRCNN
やSPP-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でドキュメントの長さを取ってしまったけど重複ありならいらなかったかもしれない・・・
頻度のリストを作る際に、まず用語に以下のような処理を施してあげるのが基本
大文字と小文字の正規化 単語は全て小文字に変換する。これは
Json
とJSON
といった単語を同じ用語と見なすためステミング
英単語において動詞announce
はannounces
、announced
、announcing
と変化する。これらは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のコンペにメルカリの価格予測のデータを使うことにする。規約見た限り大丈夫なはず。
処理の結果としては以下のようになった
どことなく重要な単語が消えてるようなそうでないような・・・まあいいや
自分で実装したTFを適用してみる。結果はこうなった
商品のタイトルだから単一ドキュメントに重複する単語が登場するのは滅多にないかな
単一のドキュメント内での用語の出現頻度を測定することとは別に解析対象のコーパス(ドキュメントの集まり)において、ある用語がどれだけ一般的かといったものも測定する必要がある。
例えば、ある用語が出現するドキュメントの数が少ないほど、その用語は重要かもしれない。ある用語t
に関する希少性は逆文書頻度(IDF)と呼ばれる式で計算できる
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]))
TFとIDFを組み合わせたTFIDFと呼ばれるテキストの表現方法(重み付けとも)が有名らしい
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
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個持ってきた結果
なんか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))
今回はGridSearchCV
とStratifiedKFold
というメソッドを使ってみた。前者は最適なパラメータを探してくれて、後者は交差検証のためのメソッドらしい
最終的なテストデータでのAccuracy
は0.78095
だった。
同様のことを重み付けしていないただのBoWでのベクトルで行なったが、こっちでのテストデータのAccuracy
は0.7827
だった。
重み付けしていない方が誤差レベルだけど値が大きいことからここでの問題に対して用語の重み付けはそんなに意味を成さなかったのかな(((((;`Д´)≡⊃)`Д)、;'.・
今回やったもののコードはここ
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トークンを使って操作するので同チャンネル内の他の人からは操作できないように、メッセージの送信者のメールアドレスを確認して特定のユーザーなら操作を行うといったものにしておいた
それとは別に計測した時間をgoogleスプレッドシートに書き込んでくれる機能も盛り込んだ
用意しておいたスプレッドシートから対応する日付の行を持ってきていい感じに出力してシートを更新したりする
コードはここ(追記: Goの方パターン諸々綺麗に直しといた)
Go言語のほう
Pythonのほう
開発は全然したことないから関数名、パターンだったり例外処理だったりと雑なのだ
Pythonをここ最近触っていたせいか初めてgoを触ったせいなのか色々とギャップがひどかった。Pythonのが色々とゴリ押しで書けるところがあるからか...