二値変数だけからなるロジスティック回帰

説明変数が複数の二値変数からなり、目的変数が二値をとるロジスティック回帰を考える。

\(\bf{w}\)をウェイト、\(\bf{x}_n\)\(t_n\)を学習データとする。確率的勾配降下法を用いれば、以下のよく見る式で都度重みを更新できる。 ただし、\(y_n\)\(\bf{w}^{(i)}\)による\(\bf{x}_n\)に対する予測値で、\(\eta\)は1未満の正の定数。

\[\bf{w}^{(i+1)} = \bf{w}^{(i)} - \eta ( y_n - t_n ) \bf{x}_n \]

二値を0、1とすれば、説明変数は疎ベクトルとなる可能性がある。この場合、ハッシュで表すと空間効率が良い。 定数項を含めるため、\(\bf{w}\)\(\bf{x}\)にそれぞれ“bias”という項を設け、常に\(1\)で入力するようにする。

例えば説明変数が2次元の場合、取りうる値は\((0, 0), (1, 0), (0, 1), (1, 1)\)の4つしかないので、 それぞれの点での値とマッチするようにフィッティングされる。\(N\)次元の場合は、\(2^N\)個の等式ができるが、 パラメータ\(\bf{w}\)の数は定数項を入れて\(N + 1\)個であるので、対数尤度を最大にするようにパラメータを 決めなければならないというのは通常のロジスティック回帰と変わらない。

perlでさくっと実装すれば以下のような感じ。説明変数が10010次元あり(よって、ウェイトは定数項を入れて10011次元)、それぞれの入力に対して1となる 確率を予測している。

use strict;
use warnings;
use List::Util qw(shuffle);

sub logit ($) {
    1 / (1 + exp(- $_[0]));
}

sub dot ($$) {
    my ($w, $x) = @_;
    my $p = 0;
    (exists $w->{$_} and $p += $w->{$_} * $x->{$_}) for keys %$x;
    $p;
}

sub predict ($$) {
    my ($w, $x) = @_;
    logit(dot($w, $x));
}

sub train ($$$$) {
    my ($w, $x, $t, $eta) = @_;
    my $pred = predict($w, $x);
    for (keys %$x) {
        $w->{$_} -= ($pred - $t) * $eta * $x->{$_};
    }
}

# Use random grouping
# {
#     my %groups = map { $_ => int(rand(10)) } 0 .. 9999;
#     sub group_of_subgroup ($) { $groups{$_[0]} }
# }
sub group_of_subgroup ($) { int($_[0] / 1000) }

sub build_subgroup ($) {
    my $n = shift;
    my $rate = int($n / 1000) + 1;
    my $group = group_of_subgroup $n;
    ($rate, {
        "subgroup$n"  => 1,
        "group$group" => 1,
        bias          => 1,
    });
}

sub build_subgroup_samples {
    my $n = shift;
    my ($rate, $vec) = build_subgroup $n;
    map {
        [$vec, $_ <= $rate ? 1 : 0];
    } 1 .. 100;
}

my @all_samples = map { build_subgroup_samples $_ } 0 .. 9999;

my %weight;
for (1 .. 10) {
    print "ITERATION $_\n";
    my $eta = 0.1 - 0.005 * $_;
    for (shuffle @all_samples) {
        my ($vec, $t) = @$_;
        train \%weight, $vec, $t, $eta;
    }
}

# Predict for groups
for (0 .. 9) {
    printf "group=%d, RATE=%3.2f %%\n", $_, 100 * (predict \%weight, {"group$_" => 1, bias => 1});
}

# Predict for subgroups
for (0 .. 9) {
    my $subgroup = $_ * 1000 + 1;
    printf "subgroup=%4d, RATE=%3.2f %%\n", $subgroup, 100 * (predict \%weight, {"subgroup$subgroup" => 1, "group${\ group_of_subgroup $subgroup}" => 1, bias => 1});
}

# Predict for unknown subgroups
printf "UNKNOWN RATE=%3.2f %%\n", 100 * (predict \%weight, {bias => 1});
__END__
ITERATION 1
ITERATION 2
ITERATION 3
ITERATION 4
ITERATION 5
ITERATION 6
ITERATION 7
ITERATION 8
ITERATION 9
ITERATION 10
group=0, RATE=1.19 %
group=1, RATE=1.81 %
group=2, RATE=3.58 %
group=3, RATE=4.65 %
group=4, RATE=4.66 %
group=5, RATE=8.93 %
group=6, RATE=7.78 %
group=7, RATE=9.33 %
group=8, RATE=9.75 %
group=9, RATE=11.19 %
subgroup=   1, RATE=1.19 %
subgroup=1001, RATE=1.81 %
subgroup=2001, RATE=3.56 %
subgroup=3001, RATE=4.64 %
subgroup=4001, RATE=4.63 %
subgroup=5001, RATE=9.10 %
subgroup=6001, RATE=7.80 %
subgroup=7001, RATE=9.53 %
subgroup=8001, RATE=9.87 %
subgroup=9001, RATE=11.34 %
UNKNOWN RATE=6.62 %

ここで自分がひっかかっていたのが、説明変数に従属的な関係がある場合。 例えばこのコードだと、subgroupが決まるとgroupが自動的に決まるため、groupの値が一部死に体となる。 が、先ほど書いたようにパラメータ数より等式のほうが圧倒的に大きいので、一部が無視されても実際は問題とはならない。 例えば二次元の場合で第二要素が1の時は必ず第一要素が1となるという制約が入って \((0, 0), (1, 0), (1, 1)\) の値しか 登場しないとしても、パラメータ3つに関して等式が3本となるので、パラメータを決定するには十分な情報がある。

もう一点、逐次学習するに当たって、学習データにおいてgroupの登場回数(1となっているベクトルの件数)が1つにつき10万回あるのに対し、subgroupの登場回数は100回なので、 subgroupについては十分に学習されないのではないかという懸念もあった。これも結果を見る限りは大丈夫そうだ。 登場回数は少なくても、目的変数が\(1\)となった場合には現時点のズレを元に大きく重みが増えることを考えれば自然に思える。 また、登場回数が少ないということは目的変数が\(0\)になって重みが減算される機会も少ないと言えそうだ。