libsvm (2)

とあるWebのサービスを使いたくなった。それには会員登録して下さいとな。何か有効な情報を 得ようとすると、こちらの情報と引き換えにという作戦。裏では、機械学習がカラカラと 音を立てて動いているに違いない。そして、隙あらば余計な物を買わせようと、虎視眈々と 狙ってる。消費者心理学とか、考慮してるんだろうな。

で、idを決めてパスワードも決める必要がある。初めにの解説を読むと、idは半角英数字と記号 で、8桁から19桁。パスワードもやはり英数字と記号で、6桁から8桁でお願いしますとの事。 更に使える記号は、ハイフン、アンダーバー、ドット、アットマークの4種に限定ですって。

これって、idはメアドを推奨しますって暗に言ってるな。パスワードは8桁じゃ短すぎないか。 どれも、作る側の都合で決めた仕様っぽいぞ。まるで、 なぜマイナポータルはJava必須なのか、開発者側の理屈でユーザー体験がおざなりにと同じじゃんと思ったぞ。

まあ、文句を言っても始まらないで、ユーザー登録にかかったよ。パスワードは英大文字を 混ぜるのが常識と思って、混ぜたら撥ねられた。何、この仕様。セキュリティを知らない馬鹿者 が、自分の都合だけで作ったな。それでいて、パスワードは生年月日は危険ですとか、偉そうに いってるよ。

ええい、こうなったら、idとパスワードを含めて、超いやらしくランダムな文字列にしてあげるかな。それとも、19桁と8桁なら全27桁になるから、これで、好きな英語の歌のフレーズを入力出来そうだな。

ランダム文字なら、とある座右の銘をmd5にかけて、それをsha256ぐらい にかけて、それをbase64エンコードしてやるか。ログインidとパスワードは、計算して算出させましょう。

[ob: ~]$ echo trump | openssl sha512 | openssl base64 | cut -c 13-21
YThmOGFmY
N2VkNjk4Y
ZGZkYTIzO

idは上の2行を連結、パスワードは最後の行を採用とかね。ああ、大文字が混じっているから trでもかませる必要が有るな。

我ながら、馬鹿な事を考えるものだ。

libsvmを使い倒す

と、キャッチャーな見出しで初めてみる。それには、libsvmが喰えるファイルが必要だな。 なんせ、独自のフォーマットですからね。

$ cat train-images.txt |\
  awk '{
    for (i = 1; i <= NF; i++) if ($i > 0) printf("%d:%f%s", i, $i / 255, i < NF ? " " : "");
    printf("\n");
  }' |\
  paste -d' ' train-labels.txt - >train.svm

何方かが書かれた、MNISTの手書き数字をサポートベクタマシン用データに変換する例。 ラベルファイルとフィーチャーファイルが別になってて、それをpasteで合体させてる。 pasteの上手い使い方だな。

オイラーが持ってる、例の血圧データはCSVファイル。これをSVM形式に変換するやつを 書いてみた。CSVファイル、そんなのpythonに任せますってのは無しの方向です。 上のコードを再利用させていただきました。

sakae@debian:~/ML/svm$ cat conv.awk
# conv csv-file to svm-file

BEGIN { FS = "," }
{
    printf("%d ", ($1 % 100) < 12 ? -1 : 1);
    for (i = 2; i <= NF; i++) {
        printf("%d:%d%s", i - 1, $i, i < NF ? " " : "");
    }
    printf("\n");
}

下記のように使います。

sakae@debian:~/ML/svm$ cat z.csv
15111204,126,78,53
15111221,120,64,61
15111304,130,73,55
15111321,128,73,59
15111405,135,71,55
sakae@debian:~/ML/svm$ cat z.csv | awk -f conv.awk
-1 1:126 2:78 3:53
1 1:120 2:64 3:61
-1 1:130 2:73 3:55
1 1:128 2:73 3:59
-1 1:135 2:71 3:55

スケーリングをやってくれる、svm-scaleってコマンドは、ラベルには手を付けない 仕様になってる。(当たり前と言えば当たり前な事)

sakae@debian:~/ML/svm$ cat z.csv | awk -f conv.awk > t.txt
sakae@debian:~/ML/svm$ svm-scale t.txt > t.svm
sakae@debian:~/ML/svm$ cat t.svm
-1 1:-0.2 2:1 3:-1
1 1:-1 2:-1 3:1
-1 1:0.333333 2:0.285714 3:-0.5
1 1:0.0666667 2:0.285714 3:0.5
-1 1:1 3:-0.5

変換されたデータを良く見ると、最後の行の2カラム目のデータが省略されてる。これ、 変換した結果がZEROになったから。いわゆる疎な行列として扱い、メモリーとかを 節約したい作戦なんだろうな。

sakae@debian:~/ML/svm$ svm-scale -s xx t.txt > t.svm
sakae@debian:~/ML/svm$ cat xx
x
-1 1
1 120 135
2 64 78
3 53 61

スケールさせた時の範囲をファイルに記録しておける。後で、テストデータをスケールさせる 時、このファイルを使う必要がある。トレーニングとテストと同じスケールになっていないと 意味ないからね。

debianと言う緩い環境から離れて、OpenBSD上でやってみるかな。libsvmに同梱されてる toolsの中のpythonファイル類を/usr/local/binの中に移動。後、python-dirの中にある ファイルは、/usr/local/lib/python3.4/site-packageの中に移動。これで、普通に 扱えるpythonコマンドに昇格だな。

それから、svm-xxxってコマンドも/usr/local/binの下へ。ライブラリィーファイルは/usr/local/libへ配置。なんせ、インストーラーが付いていませんから、手動管理してくださいって いう自由度が高いものになってます。

手始めに、パラメータのチューチングでもやってみるか。

[ob: svm]$ easy.py z.svm
Traceback (most recent call last):
  File "/usr/local/bin/easy.py", line 28, in <module>
    assert os.path.exists(svmscale_exe),"svm-scale executable not found"
AssertionError: svm-scale executable not found

早速怒られた。

[ob: svm]$ sudo vi +28 /usr/local/bin/easy.py
  :
if not is_win32:
        svmscale_exe = "../svm-scale"
        svmtrain_exe = "../svm-train"
        svmpredict_exe = "../svm-predict"
        grid_py = "./grid.py"
        gnuplot_exe = "/usr/bin/gnuplot"

こんなに良い物をlibsvmの中に閉じ込めておくのは、いかがなものかと。まあ、煩雑に バージョンアップが有るなら、相応の対処をしなきゃならんだろうけど、世界の目が集中 してるんで、十分に安定してると思われ。上記の相対PATHを、/usr/local/binに書き換えて おいた。

[ob: svm]$ easy.py z.svm
Scaling training data...
Cross validation...
Best c=32.0, g=0.5 CV rate=96.0219
Training...
Output model: z.svm.model

これ、クロスバリディーションって言ってるんで、トレーニングデータ、テストデータに 分割して、過学習にならないように配慮しながら、cとgのパラメータを変えて、正解率が 上がる点を見つけているんだな。

最良のパラメータの時、正解率は96%になりましたとな。上記の結果が出た後も暫くは 等高線グラフが出てるけど、やがて終了して消えてしまう。

[ob: svm]$ ls z*
z.svm            z.svm.range      z.svm.scale.out
z.svm.model      z.svm.scale      z.svm.scale.png
[ob: svm]$ gpicview z.svm.scale.png

各種のログを残してくれていて、その中に等高線グラフも含まれているんで、後でじっくりと 観察出来るね。

なお、供試データは欲張らない事。データが大きすぎると、探索に膨大な時間がかかってしまう。せいぜい1000行ぐらいまでにしておけ。大きなデータから データの特徴に配慮して切り出してくれる、subset.pyが用意されてるぞ。

[ob: svm]$ easy.py z.svm
Scaling training data...
Cross validation...
Best c=2048.0, g=0.03125 CV rate=93.0059
Training...
Output model: z.svm.model

3217行のデータで探索したら、こんな結果になった。正解率が落ちているぞ。 subset.pyを使って抽出したデータでやってみたら、

[ob: svm]$ easy.py z.svm
Scaling training data...
Cross validation...
Best c=32768.0, g=0.00048828125 CV rate=93.0556
Training...
Output model: z.svm.model

こんな結果が。データに捉えどころが無いって事かな。なんかcが振り切っている予感が するぞ。このデータは、3年半分のデータだから、体調が徐々に変化してるのかな。

wine

統計的機械学習入門を見つつ、ワイン でも楽しみましょ。

とか言ってるけど、この機械学習を始める機会になった、node.jsでなんたらってのが回を 重ねて、 機械にワインの味は判定できるか? その2で、node.js用のsvmを使い速いだろうってやってたんだ。svmが何たるか知らなかったけど、そのまま Wine Quality Data Set から落としてきたデータで試そうも、フォーマットが変わっていて、面倒だったので、そのまま うっちゃっていたんだ。

時は流れ、周囲を見渡せるようになった今、改めてワインに酔ってみましょとなった訳。

機械学習の取っ掛かりは、データの調査から。落としてきたデータは、赤ワインと白ワインの 2本だて。面倒なので、両者を混ぜ合わせて、ロゼワインにしちゃおうかと思ったけど、 ワインソムリエの田崎真也さんに、馬鹿な事をするなと窘めるられそうなので、すんでの所で 思いとどまったよ。

[ob: wine]$ head -n 3 red.csv
"fixed acidity";"volatile acidity";"citric acid";"residual sugar";"chlorides";"free sulfur dioxide";"total sulfur dioxide";"density";"pH";"sulphates";"alcohol";"quality"
7.4;0.7;0;1.9;0.076;11;34;0.9978;3.51;0.56;9.4;5
7.8;0.88;0;2.6;0.098;25;67;0.9968;3.2;0.68;9.8;5

冒頭にフィーチャの説明が書いてある。ワインのフレーバーとか品質がどの列にあるかの説明だな。機械学習で使う教師データに相当するものは、一番最後になるんだな。 2行目からは、セミコロン区切りのデータが並んでいる。各ワイナリーから供試されたものだな。

これをsvmで扱えるように変換する必要が有るな。前に作ったawkスクリプトをチョイ変更。

[ob: wine]$ cat conv.awk
BEGIN { FS = ";" }
{
    printf("%d ", $NF);
    for (i = 1; i < NF; i++) {
        printf("%d:%f%s", i , $i, i < NF ? " " : "");
    }
    printf("\n");
}

これをそのまま使うと、

0 1:0.000000 2:0.000000 3:0.000000 4:0.000000 5:0.000000 6:0.000000 7:0.000000 8:0.000000 9:0.000000 10:0.000000 11:0.000000
5 1:7.400000 2:0.700000 3:0.000000 4:1.900000 5:0.076000 6:11.000000 7:34.000000 8:0.997800 9:3.510000 10:0.560000 11:9.400000
5 1:7.800000 2:0.880000 3:0.000000 4:2.600000 5:0.098000 6:25.000000 7:67.000000 8:0.996800 9:3.200000 10:0.680000 11:9.800000

ヘッダー行が数値ではないのに、awkは何のエラー(警告)も出さずに変換したよ。文字列は、 数値のZEROになるんか。ちょいと怖い仕様だな。ユーザーが気を付けるしかない。

wine for octave

後は、お決まりの手法。スケールしてレーニングして、鑑定するってなるんだけど、 どうも鑑定で良いスコアが得られない。ワインは嗜好品だから、万人が一致する事は無い だろうって言う論法を持ち出すか。

[ob: wine]$ awk -f conv.awk red.csv > t
[ob: wine]$ svm-scale t > w.svm
[ob: wine]$ octave-cli -q
octave:1> [y, x] = libsvmread('w.svm');
octave:2> cv = svmtrain(y, x, '-c 1 -g 0.07');
*
optimization finished, #iter = 574
nu = 0.726641
obj = -911.830280, rho = -1.127341
nSV = 966, nBSV = 953

octave:3> [p, a, d] = svmpredict(y, x, cv);
Accuracy = 58.474% (935/1599) (classification)

それとも、下記のoctave用のパラメータチューニングスクリプトで、舌を鍛えるか。

bestcv = 0;
for log2c = -5:2:15,
  for log2g = -15:2:3,
    cmd = ['-q -v 5 -c ', num2str(2^log2c), ' -g ', num2str(2^log2g)];
    cv = svmtrain(y, x,  cmd);
    if (cv >= bestcv),
      bestcv = cv; bestc = 2^log2c; bestg = 2^log2g;
    end
    fprintf('%g %g %g (best c=%g, g=%g, rate=%g)\n', log2c, log2g, cv, bestc, bestg, bestcv);
  end
end

結果は、随分吟味した末に、下記のようになった。全数検査してたんで時間がかかったんだな。 こういう場合は、抜き取り検査でいいのかな。

Cross Validation Accuracy = 42.5891%
-5 -15 42.5891 (best c=0.03125, g=3.05176e-05, rate=42.5891)
Cross Validation Accuracy = 42.5891%
-5 -13 42.5891 (best c=0.03125, g=0.00012207, rate=42.5891)
  :
Cross Validation Accuracy = 63.6023%
15 3 63.6023 (best c=2, g=8, rate=66.0413)

それにしても、node.js下のsvmで、98%のスコアだったってのが信じられないな。

on scikit-learn

色々試験するには、ちゃんとしてる実験室がいいな。設備も揃っているし。Debianに戻りました。 そして、ワインのデータを取り込むpython pandaさんです。

def get_wine(file):
    a = pd.read_csv(file, delimiter=';')
    return a.ix[:,:-1], a.ix[:,-1]

一応、動作確認。

In [27]: x,y=get_wine('red.csv')

In [28]: !head -n 2 red.csv
"fixed acidity";"volatile acidity";"citric acid";"residual sugar";"chlorides";"\free sulfur dioxide";"total sulfur dioxide";"density";"pH";"sulphates";"alcohol\";"quality"
7.4;0.7;0;1.9;0.076;11;34;0.9978;3.51;0.56;9.4;5

冒頭にビックリを付けると、本物もshellを呼べるのは良い兆候です。

In [29]: x.ix[0,:]
Out[29]:

fixed acidity            7.4000
volatile acidity         0.7000
citric acid              0.0000
residual sugar           1.9000
chlorides                0.0760
free sulfur dioxide     11.0000
total sulfur dioxide    34.0000
density                  0.9978
pH                       3.5100
sulphates                0.5600
alcohol                  9.4000
Name: 0, dtype: float64

In [30]: y[0]
Out[30]:
5

どうやら正しく読み込んでくれてます。ワインのpHが3.5って、随分と酸性なんだな。他の ワインはどうなんだろう? 一番酸っぱいのは、2.7だった。これって硫酸だか塩酸ぐらいでは なかろうか? こんなの飲んで大丈夫か? あの人に聞いてみたいぞ。

tips

なんか、新しいのが出たみたい。 Python向けのグラフィカルなシェル「IPython 5.3」リリース 良く使うんで、重いのは御免こうむりたいぞ。

で、コピペしたのを実行出来るとな。某Webに出てたpythonとのセッション記録。 これをコピペしようとすると、冒頭のプロンプトが邪魔になって、一行づつしか出来ない。 最後の結果はいいけど、入力した部分をペターとコピーして貼り付けたい。

 >>> import random
 >>> mylist=['apple','orange','passion fruit','banana']
 >>> random.shuffle(mylist)
 >>> mylist
 ['orange', 'passion fruit', 'banana', 'apple']

そんな時は、cpasteっていうのが便利。

In [7]: cpaste
Pasting code; enter '--' alone on the line to stop or use Ctrl-D.
: >>> import random
 >>> mylist=['apple','orange','passion fruit','banana']
 >>> random.shuffle(mylist)
 >>> mylist:::
:--
Out[7]: ['apple', 'banana', 'orange', 'passion fruit']

張り付けてて、最後に、ダッシュを2つ入力するか、Ctrl-Dすればおk。プロンプトを 取り除いて、実行してくれる。