NBSVMを試す
はじめに
S. Wang & C. D. Manning, Baselines and Bigrams: Simple, Good Sentiment and Topic Classificatioin
Naive Bayes素性を利用したSVM(NBSVM)なるものを試してみる。
SVM with NB features(NBSVM)
- Log-count ratio r = log( (p / ||p||_1) / (q / ||q||_1) )
- 正例カウントベクトル p = α + Σ_{i:y_i=1} f_i
- 負例カウントベクトル q = α + Σ_{i:y_i=-1} f_i
- f_i : 各事例iにおける素性ベクトル
- α : スムージング用パラメータ
- モデル w' = (1-β) * w~ + β * w
- w~ : ||w||_1 / |V|
- β : 補間パラメータ(0〜1)
実験ではliblinearを利用して、前処理としてLog-count ratioの計算、モデル学習後に補間モデルの計算、をしている模様(MATLAB)。
使用したデータ
- LIBSVMに置いてあるnews20.binaryを利用した
- http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary.html#news20.binary
- 正例はsci.*, comp.*, misc.forsalesの10カテゴリ、負例はそれ以外の10カテゴリ
- 素性については詳細が論文に書かれているが、0/1バイナリ素性を事例ごとにノーマライズ(単位ベクトル化)してあるらしい
- 時系列を無視して、正例と負例それぞれをシャッフルし、それぞれ8000件ずつを学習用、残りを評価用にした
- 正例数 : 学習8000 + 評価1999
- 負例数 : 学習8000 + 評価1997
コード
liblinear形式のデータをそれぞれ直すためにスクリプトを準備。非常に雑。
学習データからLCRを計算(calc_lcr.pl)
#!/usr/bin/perl use strict; use warnings; my $alpha = shift; my %pos_count_vector; my %neg_count_vector; while(<>){ chomp; my @line = split(/\s+/); for(my $i=1; $i<@line; $i++){ my ($id, $val) = split(/:/, $line[$i]); if($line[0] > 0){ $pos_count_vector{$id}++; }else{ $neg_count_vector{$id}++; } } } my %pos_p; my %neg_p; my $sum; $sum = 0; foreach my $id (keys %pos_count_vector){ $sum += $alpha + $pos_count_vector{$id}; $pos_p{$id} = log($alpha + $pos_count_vector{$id}); } foreach my $id (keys %pos_count_vector){ $pos_p{$id} -= log($sum); } $sum = 0; foreach my $id (keys %neg_count_vector){ $sum += $alpha + $neg_count_vector{$id}; $neg_p{$id} = log($alpha + $neg_count_vector{$id}); } foreach my $id (keys %neg_count_vector){ $neg_p{$id} -= log($sum); } my @ids = grep{ our %h; ++$h{$_} < 2 }(keys %pos_p, keys %neg_p); foreach my $id (sort {$a <=> $b} @ids){ my $p = exists $pos_p{$id} ? $pos_p{$id} : 0; my $n = exists $neg_p{$id} ? $neg_p{$id} : 0; print $id, "\t", ($p - $n), "\n"; }
liblinear形式データをlcrデータを使って素性値置き換え(data2lcr.pl)
#!/usr/bin/perl use strict; use warnings; my $lcr_file = shift; my %lcr; open(IN, "<", $lcr_file) or die; while(<IN>){ chomp; my ($id, $val) = split(/\t/); $lcr{$id} = $val; } while(<>){ chomp; my @line = split(/\s+/); print $line[0]; my $sz = 0; for(my $i=1; $i<@line; $i++){ my ($id, $val) = split(/:/, $line[$i]); my $new_val = exists $lcr{$id} ? $lcr{$id} : 0; $sz += $new_val * $new_val; } $sz = sqrt($sz); for(my $i=1; $i<@line; $i++){ my ($id, $val) = split(/:/, $line[$i]); my $new_val = exists $lcr{$id} ? $lcr{$id} : 0; print " ", $id, ":", (($sz<1e-8)?$new_val:($new_val/$sz)); } print "\n"; }
学習したモデルを修正(model_modif.pl)
#!/usr/bin/perl use strict; use warnings; my $beta = shift; my @w; my $sum = 0; my $flg = 0; while(<>){ chomp; if($_ eq 'w'){ $flg = 1; next; } if($flg == 0){ print $_, "\n"; }else{ push @w, $_; $sum += abs($_); } } print "w\n"; foreach my $val (@w){ my $new_val = (1.0 - $beta) * ($sum / scalar(@w)) + $beta * $val; print $new_val, "\n"; }
結果
liblinearのパラメータは、s=2を用いる以外は全部デフォルトでAccuracyをみてみる。
(s=1だとバイナリ素性のときイテレーション回数最大値までいってしまうため)
#!/bin/zsh #正例負例 #grep "^+1" news20.binary > news20.binary.pos #grep "^-1" news20.binary > news20.binary.neg #データをシャッフル #perl -MList::Util=shuffle -e 'print shuffle(<>)' < news20.binary.neg >news20.binary.neg.shuf #perl -MList::Util=shuffle -e 'print shuffle(<>)' < news20.binary.pos >news20.binary.pos.shuf #データの前半と後半を、学習用と評価用に分ける head -8000 news20.binary.pos.shuf > news20.binary.pos_train head -8000 news20.binary.neg.shuf > news20.binary.neg_train tail -n +8001 news20.binary.pos.shuf > news20.binary.pos_test tail -n +8001 news20.binary.neg.shuf > news20.binary.neg_test #学習用と評価用データ cat news20.binary.pos_train news20.binary.neg_train > train cat news20.binary.pos_test news20.binary.neg_test > test #LCRの計算とデータの素性値を置換 perl ./bin/calc_lcr.pl 0.25 < train > train.lcr perl ./bin/data2lcr.pl train.lcr < train > train.new perl ./bin/data2lcr.pl train.lcr < test > test.new #学習 ./liblinear/liblinear-1.94/train -s 2 train.new for i in {0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1}; do perl ./bin/model_modif.pl $i < train.new.model > model.beta$i; done #評価 for i in {0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1}; do echo -n $i"\t"; ./liblinear/liblinear-1.94/predict test.new model.beta$i out; done #ベースラインの計算(素性値を0/1のバイナリ値にした場合) perl ./bin/data2binary.pl < train > train.binary perl ./bin/data2binary.pl < test > test.binary ./liblinear/liblinear-1.94/train -s 2 train.binary ./liblinear/liblinear-1.94/predict test.binary train.binary.model out #ベースラインの計算(素性値を0/1のバイナリ値にしたあと、normalize(単位ベクトル化)) ./liblinear/liblinear-1.94/train -s 2 train ./liblinear/liblinear-1.94/predict test train.model out
- バイナリ素性
- Accuracy = 95.5205% (3817/3996)
- バイナリ素性+単位ベクトル化
- Accuracy = 96.5215% (3857/3996)
- α=0.25でLCR値+単位ベクトル化
- β=0.0 : Accuracy = 4.02903% (161/3996)
- β=0.1 : Accuracy = 11.8118% (472/3996)
- β=0.2 : Accuracy = 34.1592% (1365/3996)
- β=0.3 : Accuracy = 69.8699% (2792/3996)
- β=0.4 : Accuracy = 90.6156% (3621/3996)
- β=0.5 : Accuracy = 95.8959% (3832/3996)
- β=0.6 : Accuracy = 97.1221% (3881/3996)
- β=0.7 : Accuracy = 97.3724% (3891/3996)
- β=0.8 : Accuracy = 97.3974% (3892/3996)
- β=0.9 : Accuracy = 97.3974% (3892/3996)
- β=1.0 : Accuracy = 97.3724% (3891/3996)
ベースラインよりも良い結果が得られているっぽい。(パラメータのβが1.0じゃないところで最大値になっている)
データにも依るかもしれないけど、ノーマライズやTFIDFなど素性値を変えてみる一つとして有用そう。