以前LIBSVMで特徴量の重みを見る方法について記事でURLを紹介したのですが、リンク先の記事とコードがなくなっているみたいなので、改めて記事にしておきます。
SVMでの特徴量の重み
非常に単純化して説明すると、線形カーネルのSVMは次のような式の符号の正負によってデータを分類します。
このときがデータのベクトルで、が学習された重み、が学習されたバイアス項になっています。
重みの絶対値が大きな特徴量は識別に大きな影響を与える≒重要な特徴量であると考えることができ、学習結果の分析などで使われています
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のバイアス項を表しています
いわゆるのはず(?)
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番目以降がサポートベクターに含まれる特徴量の番号とその値を:でくっつけたものになっています。
特徴量の重みの計算
二値分類の場合
線形カーネルのSVMの場合サポートベクターを使って特徴量ごとの重みを得ることができます
たとえば特徴量の番号0についての重みは、番号0を含むサポートベクターについて値と係数の積の総和となります。
つまり以下の3つしかない例だと0:1がすべてに含まれているので、番号0についての重みはとなります
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 以下省略
参考
日本語では上の記事が詳しいですが、ソースコードが間違っていそう(特徴量の番号を使ってない!)なのと、多値分類の場合については書いてありません。