唯物是真 @Scaled_Wurm

プログラミング(主にPython2.7)とか機械学習とか

LIBSVMの特徴量の重みを見る - LIBSVMのモデルの読み方

以前LIBSVMで特徴量の重みを見る方法について記事でURLを紹介したのですが、リンク先の記事とコードがなくなっているみたいなので、改めて記事にしておきます。

SVMでの特徴量の重み

非常に単純化して説明すると、SVMは次のような式\mathbf{wx} -bの符号の正負によってデータを分類します。
このとき\mathbf xがデータのベクトルで、\mathbf wが学習された重み、bが学習されたバイアス項になっています。
重みの絶対値が大きな特徴量は識別に大きな影響を与える≒重要な特徴量であると考えることができ、学習結果の分析などで使われています

LIBSVMのモデルの読み方

まず以下の二値分類の例でLIBSVMのモデルファイルの説明をします

svm_type c_svc
kernel_type rbf
gamma 0.000361402
nr_class 2
total_sv 707
rho -1.00298
label -1 1
nr_sv 429 278
SV
0.03206964163371585 0:1 2:1 11:1 40:1 50:2 57:1 85:1 106:1 185:1 190:1 206:1 231:1 393:1 394:1 481:1 1270:1 1329:1 1330:1 1331:1 1332:1
0.07523869983576176 0:1 11:1 17:1 57:2 106:1 163:1 200:1 201:1 473:1 935:1 936:1 1061:1 1256:1 1347:1
0.3 0:1 2:1 7:1 11:1 31:1 149:1 214:1 1012:1 1355:1
以下省略

svm_type

svm_type c_svc

SVMの種類を表しています
LIBSVMの-sのオプションで指定するものです

kernel_type

kernel_type rbf

カーネルのタイプでLIBSVMの-tのオプションで指定できます

gamma

gamma 0.000361402

カーネルのパラメータgammaを表しています

nr_class

nr_class 2

クラス数を表していて、今回は二値分類なので2です

total_sv

total_sv 707

サポートベクターの数

rho

rho -1.00298
SV
0.03206964163371585 0:1 2:1 11:1 40:1 50:2 57:1 85:1 106:1 185:1 190:1 206:1 231:1 393:1 394:1 481:1 1270:1 1329:1 1330:1 1331:1 1332:1
0.07523869983576176 0:1 11:1 17:1 57:2 106:1 163:1 200:1 201:1 473:1 935:1 936:1 1061:1 1256:1 1347:1
0.3 0:1 2:1 7:1 11:1 31:1 149:1 214:1 1012:1 1355:1
以下省略

SVMのバイアス項bを表しています
いわゆるb=-rhoのはず(?)

label

label -1 1

各クラスのラベルを表しています

nr_sv

nr_sv 429 278

クラスごとのサポートベクターの数(おそらく次で説明する係数がそのクラスが選ばれやすくなる符号になっているサポートベクターの数)を表しています。

SV

SV
0.03206964163371585 0:1 2:1 11:1 40:1 50:2 57:1 85:1 106:1 185:1 190:1 206:1 231:1 393:1 394:1 481:1 1270:1 1329:1 1330:1 1331:1 1332:1
0.07523869983576176 0:1 11:1 17:1 57:2 106:1 163:1 200:1 201:1 473:1 935:1 936:1 1061:1 1256:1 1347:1
0.3 0:1 2:1 7:1 11:1 31:1 149:1 214:1 1012:1 1355:1
以下省略

SV以下はすべてサポートベクターを表しています。
空白区切りで、一番最初がそのサポートベクターの係数、2番目以降がサポートベクターに含まれる特徴量の番号とその値を:でくっつけたものになっています。

特徴量の重みの計算

二値分類の場合

サポートベクターを使って特徴量ごとの重みを得ることができます

たとえば特徴量の番号0についての重みは、番号0を含むサポートベクターについて値と係数の積の総和となります。
つまり以下の3つしかない例だと0:1がすべてに含まれているので、番号0についての重みは1 \times 0.03206964163371585+ 1\times0.07523869983576176+1 \times0.3となります

0.03206964163371585 0:1 2:1 11:1 40:1 50:2 57:1 85:1 106:1 185:1 190:1 206:1 231:1 393:1 394:1 481:1 1270:1 1329:1 1330:1 1331:1 1332:1
0.07523869983576176 0:1 11:1 17:1 57:2 106:1 163:1 200:1 201:1 473:1 935:1 936:1 1061:1 1256:1 1347:1
0.3 0:1 2:1 7:1 11:1 31:1 149:1 214:1 1012:1 1355:1
重みの表示用のコード(二値分類)

二値分類については以下のようなコードとコマンドを実行すれば素性番号ごとの重みが得られます
なお注意点としては、モデルファイルのlabelの項目に先に出てきたほうが正の符号を持つようになります
重みの符号の正負を反転すれば、正例と負例を反転した場合と等しくなります

cat モデルファイル | python 以下のスクリプトファイル
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import collections

for line in sys.stdin:
    if line == 'SV\n':
        break

weight = collections.defaultdict(lambda: 0)

for line in sys.stdin:
    split = line.split()
    coef = float(split[0])
    for feature in split[1:]:
        number, count = map(int, feature.split(':'))
        weight[number] += coef * count

for num in sorted(weight.keys()):
    print num, weight[num]

多値分類の場合

LIBSVMではone-against-the-restではなくone-against-oneで多値分類しているようです。
つまりすべてのクラスのペアの数だけSVMを作っているので、rhoやサポートベクターの係数の個数などがその分増えます。
このモデルファイルの説明に書いてあるように、例えばクラス1に関するサポートベクターについて、クラス1対2、クラス1対3、クラス1対4……のように複数の係数がサポートベクターの前に記述されることになります

重みの表示用のコード(多値分類)

こちらのコードは重みの絶対値の大きい順に出力するようにしました
二値分類の場合と異なり、対象とする2つのクラスを指定する必要があります
ラベルが指定されなかった場合には最初の2つのラベルを正例、負例とします
ちなみに二値分類のモデルファイルに対しても使えます

cat モデルファイル | python 以下のスクリプトのファイル名 正例のラベル 負例のラベル
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import collections
import logging

def readModelWithoutSV(iterable):
    labels = []
    nums_sv = []

    for line in iterable:
        if line.startswith('label'):
            label_split = line.split()
            for label in label_split[1:]:
                labels.append(label)
        elif line.startswith('nr_sv'):
            nums_sv = map(int, line.split()[1:])
        elif line == 'SV\n':
            break
    return (labels, nums_sv)

def readWeightFromSV(iterable, labels, num_sv, pos_label, neg_label):
    target = set([pos_label, neg_label])

    pos_index = labels.index(pos_label)
    neg_index = labels.index(neg_label)
    sign = 1 if pos_index < neg_index else -1

    num_class = len(labels)
    weight = collections.defaultdict(lambda: 0)

    for i, num in enumerate(nums_sv):
        label = labels[i]
        if label in target:
            target_index = None
            if label == pos_label:
                target_index = neg_index - 1 if pos_index < neg_index else neg_index
            else:
                target_index = pos_index - 1 if neg_index < pos_index else pos_index

            for j in xrange(num):
                line = iterable.next()
                split = line.split()
            
                coef = sign * float(split[target_index])
                for feature in split[num_class - 1:]:
                    number, count = map(int, feature.split(':'))
                    weight[number] += coef * count
        else:
            for j in xrange(num):
                iterable.next()
    return weight

if __name__ == '__main__':
    labels, nums_sv = readModelWithoutSV(sys.stdin)

    pos_label = labels[0]
    neg_label = labels[1]

    if len(sys.argv) == 3:
        pos_label, neg_label = sys.argv[1:]
        if pos_label == neg_label or pos_label not in labels or neg_label not in labels:
            logging.error('Bad arguments')
            sys.exit(1)

    weight = readWeightFromSV(sys.stdin, labels, nums_sv, pos_label, neg_label)

    for num, w in sorted(weight.iteritems(), key=lambda x: abs(x[1]), reverse=True):
        print num, w

出力例(特徴量の番号 重み)

1979 -1.39134279714
2491 1.31164695436
2085 1.27675564916
373 1.25821654077
45 -1.23630551848
700 1.21099955174
17 1.19725787599
1480 1.18789999971
1315 1.17705425682
46 1.15563454227
1401 1.13300518978
494 1.10740066734
571 -1.10550272888
624 -1.09743842367
以下省略

参考

日本語では上の記事が詳しいですが、ソースコードが間違っていそう(特徴量の番号を使ってない!)なのと、多値分類の場合については書いてありません。