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を利用した
  • 時系列を無視して、正例と負例それぞれをシャッフルし、それぞれ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など素性値を変えてみる一つとして有用そう。