ハッシュとカーネル

最近Spark使ってて特徴量減らしたいよねってなった時に、
Future Hashingを漠然と使ってたけど、気になったかた勉強。

Hash Trickの前にKernel Trick

どっか違う空間に写像するってことで似てるけど、若干用途が違うカーネルトリックから。
線形空間では分離できないデータを高次元特徴空間に写像して分離可能にしたいと。けど、次元が増えると計算大変。
じゃあ、高次元に写像した時と同じような結果を導き出せる関数使って、楽に計算しようってのがカーネルトリック。

入力空間R^mから特徴空間Fへの写像 \Phiを、
\Phi:R^m → F , x → K(・,x)
とすると、
\langle \Phi (x), \Phi (y)\rangle = \langle K(・,x), K(・,y)\rangle = K(x,y)
となって、特徴空間Fへの写像した後の内積がカーネル関数K() を使って入力空間R^mで楽に計算できる。
このカーネル関数を上手いこと選びましょうってのはまた別の話で。

Hash Trick

で、本題のハッシュトリック。
カーネル関数が線形分離可能にするために高次元に写像することを目指したのに対して、ハッシュ関数はその特徴行列の性質を損なわずに次元を小さくするために使われます。
次元が大きい疎ベクトルを上手いことぎゅっと凝縮して比較的密なベクトルに変換できれば、計算のためのリソースが少なくて済むよねってのがアイデア。

じゃあどうやるかっていうと、7つの特徴カテゴリがある下記のようなデータセットがあるとします。

A1 =[‘mouse’,’black’,-]
A2 =[‘cat’,’tabby’,’mouse’]

で、でもってインデックスの数を4とし、
ハッシュ関数を

H(Animal,’mouse’)=3
H(Color,’black’)=2
H(Animal,’cat’)=0
H(Color,’tabby’)=0
H(Diet,’mouse’)=2

と定義しておくと、
A1は、ハッシュ値1が0個、2が0個、3が1個、4が1個、
同様にA2はハッシュ値1が2個、2が0個、3が1個、4が0個となり、

A1=[0,0,1,1]
A2=[2,0,1,0]

と変換することができます。
ハッシュ値の衝突を避けるために、特徴値の符号を変えたりなどなど工夫がされます。

PysparkのMllibでリッジ回帰

作ったPysparkの環境でMap関数の練習リッジ回帰やってみました。

元ネタはedXのこの講義↓
Scalable Machine Learning | edX

データはMillionSongDatasetから取得できます。

で、結果から先に書きますと、Default値含めてGridsearchしたにも関わらず、
Default値実施した回帰の方が精度が高くなるという鼻血が出そうな結果になってしまってます。


まずはもろもろimport。

#library import
from pyspark.mllib.regression import LinearRegressionWithSGD
from pyspark.mllib.regression import LabeledPoint
from test_helper import Test
import numpy as np

#data load
rawData = sc.textFile('millionsong.txt')

リッジ回帰(デフォルト)

使ったのはLinearRegressionWithSGD

samplePoints = rawData.take(2)


#Create LabeledPoint
def parsePoint(line):

    tokens = line.split(',')
    label = tokens[0]
    features = tokens[1:]
    return LabeledPoint(label, features)

parsedData = rawData.map(parsePoint)
print parsedData.take(3)

[LabeledPoint(2001.0, [0.884123733793,0.610454259079,0.600498416968,0.474669212493,0.247232680947,0.357306088914,0.344136412234,0.339641227335,0.600858840135,0.425704689024,0.60491501652,0.419193351817]), LabeledPoint(2001.0, [0.854411946129,0.604124786151,0.593634078776,0.495885413963,0.266307830936,0.261472105188,0.506387076327,0.464453565511,0.665798573683,0.542968988766,0.58044428577,0.445219373624]), LabeledPoint(2001.0, [0.908982970575,0.632063159227,0.557428975183,0.498263761394,0.276396052336,0.312809861625,0.448530069406,0.448674249968,0.649791323916,0.489868662682,0.591908113534,0.4500023818])]

次にデータをTraning、Validation,Testに分割。

#Devide rawData into Traning, Validation and Test
weights = [.8, .1, .1]
seed = 50
parsedTrainData, parsedValData, parsedTestData = parsedData.randomSplit(weights, seed)
# Fit the model with default values
fitModel = LinearRegressionWithSGD.train(parsedTrainData)
print  fitModel

(weights=[348.391703677,312.158507888,303.514705245,268.768326368,260.265535915,321.082923267,345.636059404,369.96940298,414.587178279,328.611497772,389.972179858,348.42792115], intercept=0.0)

このモデルを元に、実測値と予測値の比較。
誤差は約2年。

# Prediction 
testPoint = parsedTrainData.take(1)[0]
print testPoint.label

testPrediction = fitModel.predict(testPoint.features)
print testPrediction

2001.0
2003.04838193

次に2乗平均平方根誤差(RMSE)を使ったモデル評価を実施。
さらに、RMSEを評価関数としてグリッドサーチでハイパーパラメータを探していきます。

# Define a formula to caluculate RMSE(Root Mean Square Error)
def squaredError(label, prediction):
    ##Calculates the the squared error for a single prediction.
    return (label - prediction)**2

def calcRMSE(labelsAndPreds):
    ##Calculates the root mean squared error for an `RDD` of (label, prediction) tuples.
    return np.sqrt(labelsAndPreds
                   .map(lambda (label, prediction): squaredError(label, prediction))
                   .mean())

#Create new RDD with actual label and predicted label 
labelsAndPreds = parsedValData.map(lambda lp: (lp.label, fitModel.predict(lp.features)))

#Calculation RMSE
rmseValLR1 = calcRMSE(labelsAndPreds)
print rmseValLR1

126.788570325

リッジ回帰(グリッドサーチ編)

デフォルト値で実施したFittingではRMSEは126程度。
でもって、次にグリッドサーチでバリっとハイパーパラメータを探してみます。

##Grid search
# Values to use when training the linear regression model
miniBatchFrac = 1.0  # miniBatchFraction
regType = 'l2'  # regType
useIntercept = True  # intercept

# Seed of minmum RMSE
min_rmseVal = 10**10

#Fix HyperParameters
modelRMSEs = []

for grid_alpha in [1e-5,1.0,10,100]:
    for grid_numIters in [50,100,500]:
        for grid_reg in [1e-5,0.0, 1.0]:
            model = LinearRegressionWithSGD.train(parsedTrainData,
                                                  iterations=grid_numIters, 
                                                  step=grid_alpha,
                                                  regParam=grid_reg)
            
            labelsAndPreds = parsedValData.map(lambda lp: (lp.label, model.predict(lp.features)))
            rmseVal = calcRMSE(labelsAndPreds)
            
            
            
            if rmseVal < min_rmseVal:
                min_rmseVal = rmseVal
                best_alpha = grid_alpha
                best_numIter =grid_numIters
                best_reg = grid_reg
            
print "best_alpha:{},best_numIter:{}, best_reg:{}, best_rmseVal:{}".format(best_alpha, best_numIter, best_reg, min_rmseVal)
best_alpha:1.0,best_numIter:500, best_reg:1e-05, best_rmseVal:117.120806943

こちらはRMSEが117程度と若干下がって、精度が上がっていることが期待出来そう。
で、早速Fittingして、最初のモデルと同様に実測値と予測値の比較。

#Fitting with HyperParameters fixed by grid search 
Final_model = LinearRegressionWithSGD.train(parsedTrainData,
                                           iterations=best_numIter, 
                                           step=best_alpha, 
                                           regParam =best_reg)

#Labels comparison between Final_model and actual data. 
Final_testPoint = parsedTrainData.take(1)[0]
print Final_testPoint.label

Final_Prediction = Final_model.predict(Final_testPoint.features)
print Final_Prediction

2001.0
1969.7210425

誤差30年近くと大幅に精度が悪くなってますね。なんでやろ。
ちょっとすぐには原因わからないので、やり直したら書き直します。

Pysparkでテキストマイニング第一歩 on Amazon EMR with Jupyter notebook

SparkをAmazon EMRに乗せてみたので備忘録的に。

といってもめちゃくちゃ簡単だった。

他にはローカルでSpark立ててしまう、AmazonEC2使うとか選択肢あったけど、
価格見ても高くないし、いつか本格的に使うことを想定して、EMRでの予行演習的な位置づけ。

下記ブログの記事を参考にすれば、
Pyspark+Jupyter Notebook+AmazonEMRの環境はほとんど何も悩まずに構築可能かと。

estrellita.hatenablog.com

もう少し細かく設定したいなどあれば、Amazon EMR Best Practiceを参考に。

で、せっかくなので、PysparkでHelloWorldな感じでWordsCountやってみました。

Words Count

どのチュートリアルもとりあえずWordsCount的なノリだったので、簡単に手を動かしてみました。
今回テキストに使ったのは、シェークスピアのマクベス。
全くもって読んだことないけど、ちょうど良い感じにテキストが公開されていたので採用。
Cacheの"u"をなんとか消したかったけど、いまいちわからん。
結果に影響ないからとりあえず放置。

他の作品も↓ここからダウンロード可能。
Open Source Shakespeare: search Shakespeare's works, read the texts

とりあえず、どんな感じか見てみる。

#Read text
lines = sc.textFile('Macbeth.txt')
lines.take(10)

[u'[Thunder and lightning. Enter three Witches]',
 u'',
 u'    First Witch. When shall we three meet again',
 u'    In thunder, lightning, or in rain? ',
 u'',
 u"    Second Witch. When the hurlyburly's done,",
 u"    When the battle's lost and won. 5",
 u'',
 u'    Third Witch. That will be ere the set of sun. ',
 u'']

#Count Lines
lines.count()
3485

空白行を削除して改めて。

#Count lines after remove empty lines
lines_nonempty = lines.filter( lambda x: len(x) > 0 )
lines_nonempty.count()
2666

文字数えるのに記号が邪魔なのでスペースに変換。
その後、単語ごとに分割。

#Replace symbols to space
a1=lines.map(lambda x: x.replace(',',' ').replace('.',' ').replace('-',' ').replace('?',' ').replace('[',' ').replace(']',' ').lower())


#Devide lines to words
a2 = a1.flatMap(lambda x: x.split())
a2.take(10)
[u'thunder',
 u'and',
 u'lightning',
 u'enter',
 u'three',
 u'witches',
 u'first',
 u'witch',
 u'when',
 u'shall']

数え上げるために、それぞれの単語にフラグ立てて、タプルに。
で、各単語ごとに合計計算してあげる。
さら、KeyとValueを入れ替え。

#Create tuple which has unique words
a3 = a2.map(lambda x: (x, 1))

#Sum of each words
a4 = a3.reduceByKey(lambda x,y: x+y)

#Swap the order
a5 = a4.map(lambda x:(x[1],x[0]))
a5.take(10)

[(1, u'limited'),
 (4, u'glamis'),
 (1, u'pardon'),
 (4, u'child'),
 (99, u'all'),
 (5, u'foul'),
 (1, u'sleek'),
 (50, u'hath'),
 (2, u'protest'),
 (1, u'weal;')]

最後に昇順でSortかけました。

#Sort by the number of words
a6 = a5.sortByKey(ascending=False)
a6.take(10)

[(731, u'the'),
 (565, u'and'),
 (398, u'to'),
 (342, u'of'),
 (314, u'i'),
 (267, u'macbeth'),
 (250, u'a'),
 (225, u'that'),
 (205, u'in'),
 (192, u'my')]

せっかくSpark環境できたので、今後もうちょい複雑なことやっていきます。

scikit-learnで機械学習:決定木(2)

前回の続きで、Scikit-learnの決定木を使ってみます。

ここから先は難しいことは何もなくて、本家に従いながら走らせてみる。

もろもろimport。

import numpy as np
import pandas as pd
from sklearn import datasets
from sklearn import tree
from sklearn.grid_search import GridSearchCV
from sklearn import cross_validation
import pydot

データの学習

データはフィッシャーのあやめのサンプルデータを使います。

合計3種類、150個を学習用とテスト用にわけました。

df = pd.read_csv('iris_train.csv')
clf = tree.DecisionTreeClassifier()

#Assign an explanatory variable
data_array = ["SepalLength", "SepalWidth", "PetalLength", "PetalWidth"]

#Assign an objective variable
class_array = ["Name"]

clf = clf.fit(df[data_array], df[class_array])

Fittingがされたので、PDFに決定木を描写させます。

with open('iris.dot', 'w') as f:
  f = tree.export_graphviz(clf, out_file=f)

import os
os.unlink("iris.dot")

from sklearn.externals.six import StringIO  
import pydot

dot_data = StringIO() 
tree.export_graphviz(clf, out_file=dot_data)

graph = pydot.graph_from_dot_data(dot_data.getvalue())
graph.write_pdf("iris.pdf") 

で、得られた決定木がこちら。
f:id:tkzs:20150707074704p:plain


この分類器に従って、残り半分のあやめデータを適用してみます。
ちゃんと分類されるんですかね。

分類器をテスト

df_p = pd.read_csv('iris_test.csv')

#Apply test data
result = (df_p[data_array])

print result
['setosa' 'setosa' 'setosa' 'setosa' 'setosa' 'setosa' 'setosa' 'setosa'
 'setosa' 'setosa' 'setosa' 'setosa' 'setosa' 'setosa' 'setosa' 'setosa'
 'setosa' 'setosa' 'setosa' 'setosa' 'setosa' 'setosa' 'setosa' 'setosa'
 'setosa' 'versicolor' 'versicolor' 'versicolor' 'versicolor' 'versicolor'
 'versicolor' 'versicolor' 'versicolor' 'versicolor' 'versicolor'
 'versicolor' 'versicolor' 'versicolor' 'versicolor' 'versicolor'
 'versicolor' 'versicolor' 'versicolor' 'versicolor' 'versicolor'
 'virginica' 'versicolor' 'versicolor' 'versicolor' 'versicolor'
 'virginica' 'virginica' 'virginica' 'virginica' 'virginica' 'virginica'
 'versicolor' 'virginica' 'virginica' 'virginica' 'virginica' 'virginica'
 'virginica' 'virginica' 'virginica' 'virginica' 'virginica' 'virginica'
 'virginica' 'versicolor' 'virginica' 'virginica' 'virginica' 'virginica''virginica’] 


精度は94%(47/50)程度でした。
極々簡単にScikit-learnの動作確認でした。

VagrantとVirtualBoxで環境構築

「なんでこっちで動いて、そっちで動かんねん」を解消するために環境構築備忘録。

VirtualBox:オラクルが提供しているx86仮想化環境ソフトウェア・パッケージ
Vagrant:開発環境の管理ツール。


この二つ組み合わせて、みんなで同じ環境を構築&共有。
色んな方が詳しく解説されているので、参考にしながら環境構築。
VagrantでVirtualBoxをマウントする方法は、色々ありそうだが、
ひとまず下記コードで動いたので様子見。

vagrant up --provider=virtualbox

<参考>
Getting Started - Vagrant Documentation
web帳 | VirtualBoxとVagrantで開発環境を構築
Vagrant と Chef による仮想環境構築の自動化(VirtualBox編) | オブジェクトの広場

scikit-learnで機械学習:決定木(1)

犯罪予測における時空間分析の研究にずいぶん時間を使っていてブログが放置状態でしたが、久々の更新。
だいぶ遠のいていた機械学習の進めます。

Scikit-learnで決定木やってみます。
参考にしたのは、scikit-learnの本家、Decision Treeの項目。

アルゴリズムの理解にはじパタ(p.176-183)を補助で活用。

はじめてのパターン認識

はじめてのパターン認識


ざっと眺めたところどうやってノードを分割するかと、どうやって木の大きさを決めるのかが分析において意味を持ちそうなので、
理論を追いかけながら、手を動かしてみます。

ノードの分割方法

ノード分割の基準になるのは不純度(impurity)と呼ばれる評価関数。
この関数は

\displaystyle
  I (t)= \phi(P(C_1|t),,,P(C_k|t))

を定義し、関数\phi(z_1,z_2,,,,z_K)\sum_{i=0}^n z_iに対して、
(1)\phi()は、すべてのi=1,...,Kにたいして、z_i = 1/Kのとき、すなわちどのクラスの事後分布も一様に等しいとき最大になる。
 ⇛どこで分割しても一緒やんってときが最大値。
(2)\phi()はあるiについてz_i=1となり、jiのとき、すべてz_j=0、すなわちただ一つのクラスに定まる時最小になる。
 ⇛ここで分割すれば良いよねということを最小値で示す
(3)\phi()は、(z_1,z_2,,,,z_K)について対象な関数である。
を満たせば何でも良いとのこと。

scikit-learnではGini impurity と information gainが選べる。
Information gainの方は具体的にどの関数を具体的につかってるかイマイチよくわかんなかったので、
とりあえず、デフォルトで使われてるジニ係数の方で進めてみることにします。

ジニ係数

はじパタ p.182より、

\displaystyle
  I (t)= \sum_{i=1}^K P(C_i|t)lnP(C_i|t))

\sum_{i=1}^K P(C_i|t)lnP(C_i|t)はノードtにおける誤り率。
この誤り率が一番下がる分割を選びたい。
従って、p_L=データがLに分類される確立、p_R=がRに分類される確率とすると、分割がsとしたとき、

\displaystyle
  \Delta I (s, t)= I(t) - (p_LI (t_L)+p_RI (t_R))

が最大になるsを求めることになる。

木の大きさを決める

木をどこまで成長させるかには、色々な方法論があるっぽいです。

scikit-learnにおいては、主に下記のパラメータによって制御する。
max_depth : 木の大きさ(深さ)の上限
min_split : ノードにおけるデータ数の下限

またはじパタの中では誤り率と木の複雑さから構成されるコストという概念が紹介され、
誤り率をR(T)終端ノード数を|T|とすると、

\displaystyle
 R_\alpha (T)=  \sum R (T) + \alpha|T|

という関数でコストが定義されています。
で、この関数は\alphaが正則化パラメータの役割を果たしていて、誤り率と木の複雑さのどちらに重きをおくかを制御できると。

直感的であることが売りの決定木であれば、深さとデータ数で剪定するので良いのかもですね。

次回、実際にデータを用いてscikit-learnを走らせてみます。

一般化線形モデル(GLM)とニューラルネットって一緒やんね (2)

前回の続きで、一般化線形モデル(GLM)と多層パーセプトロンの比較です。

一般化線形モデル(GLM)とニューラルネットって一緒やんね (1) - KAZ log


機械学習の観点から:多層パーセプトロン

ニューラルネット、パーセプトロンについては下記に非常にわかりやすくまとめて下さっているので、是非ご参考に下さい。
第3回 単純パーセプトロン · levelfour/machine-learning-2014 Wiki · GitHub
第3回 多層パーセプトロン · levelfour/machine-learning-2014 Wiki · GitHub


単純パーセプトロンだと線形分離可能、つまり直線でデータを分けきれる場合のみ、識別関数のパラメーターは収束します。
もし線形分離不可のデータを与えると、健気にパラメーターを求め続けたりもして可愛げあります。

続きを読む