Feature Hashingを試す
はじめに
Feature Hashingについて気になったことがあったので試してみた。
Feature Hashingとは
ベクトルの作り方
いくつか提案されているが、各素性のhash値を計算してmod Mをとったインデクスの所に入れるものとしては主に2つがあるようなので、メモしておく。
Shiらの方法
- Shiら(2009)
- 値をunsigned sumする
- φ_i (x) = Σ_{ j:h(j)=i } x_j
- h : ハッシュ関数
ちょっとした実験
ハッシュ関数による違いみたいなものがどのぐらいあるのか気になったので、試してみる。
liblinear形式とかの「a:b」のaの数字部分を文字列として扱ってFeatureHashingするみたいなことをしても別に問題ないはずだけど、本当に問題ないのか気になったので、一緒に試してみる。
news20.binaryのデータを使う。
http://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/binary.html#news20.binary
ハッシュ関数は書きやすそうなものとして、naiveな感じのものとFNV-1とmurmurhashで試してみる(深い意味はなし)。
Weinbergerらの方法でベクトルを作る。
Mの値は、2のベキ乗を使った方がよいかもしれないが、50万、10万、5万、1万、5000で見てみる。
liblinear-1.94を使用、オプションは「-v 2」以外全部デフォルト(C=1,L2正則化L2Loss SVC dual)。
$ ./a.out 10000 < news20.binary > news20.binary.10000 $ train -v 2 news20.binary.10000
コード
#include <iostream> #include <sstream> #include <string> #include <vector> #include <map> #include <cmath> class FeatureHashing { std::map<int,std::map<std::string,int> > tbl; unsigned int hash(const std::string& str) { return hash_murmur(str); //return hash_FNV1(str); //return hash_naive(str); } //murmurhash // http://en.wikipedia.org/wiki/MurmurHash unsigned int hash_murmur(const std::string& str) { const char *key = str.c_str(); unsigned int len = str.length(); static const unsigned int c1 = 0xcc9e2d51; static const unsigned int c2 = 0x1b873593; static const unsigned int r1 = 15; static const unsigned int r2 = 13; static const unsigned int m = 5; static const unsigned int n = 0xe6546b64; unsigned int hash = 123456789U; const int nblocks = len / 4; const unsigned int *blocks = (const unsigned int *) key; int i; for (i = 0; i < nblocks; i++) { unsigned int k = blocks[i]; k *= c1; k = (k << r1) | (k >> (32 - r1)); k *= c2; hash ^= k; hash = ((hash << r2) | (hash >> (32 - r2))) * m + n; } const unsigned char *tail = (const unsigned char *) (key + nblocks * 4); unsigned int k1 = 0; switch (len & 3) { case 3: k1 ^= tail[2] << 16; case 2: k1 ^= tail[1] << 8; case 1: k1 ^= tail[0]; k1 *= c1; k1 = (k1 << r1) | (k1 >> (32 - r1)); k1 *= c2; hash ^= k1; } hash ^= len; hash ^= (hash >> 16); hash *= 0x85ebca6b; hash ^= (hash >> 13); hash *= 0xc2b2ae35; hash ^= (hash >> 16); return hash; } //FNV-1 // http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function unsigned int hash_FNV1(const std::string& str){ unsigned int hash; hash = 2166136261U; for(int i=0; i<str.length(); i++){ hash = (16777619U * hash) ^ (str[i]); } return hash; } //naive hash unsigned int hash_naive(const std::string& str){ unsigned int h = 0; for(int i=0; i<str.length(); i++){ h = h * 31 + str[i]; } return h; } //binary hash(string -> {1,-1}) int binary_hash(const std::string& str){ unsigned int h = 0; for(int i=0; i<str.length(); i++){ h = h * 37 + str[i]; } return (h % 2 == 1) ? 1 : -1; } public: FeatureHashing() { } std::vector<double> vectorize(int M, const std::map<std::string,double>& fts){ std::vector<double> x(M,0); for(std::map<std::string,double>::const_iterator i=fts.begin(); i!=fts.end(); ++i){ unsigned int h = hash(i->first); int xi = binary_hash(i->first); x[ h % M ] += xi * i->second; tbl[ h % M ][i->first] = 1; } return x; } void dump(int M){ int n = 0; int zero = 0; int sum = 0; for(std::map<int, std::map<std::string,int> >::iterator i=tbl.begin(); i!=tbl.end(); ++i){ //std::cout << (i->second).size() << std::endl; n++; sum += (i->second).size(); if((i->second).size() == 0) zero++; } std::cerr << "N : " << n << std::endl; std::cerr << "Zero : " << zero << std::endl; std::cerr << "Ave. of hash collision : " << (sum/(double)M) << std::endl; } }; std::vector<std::string> split(const std::string &str, const char c){ std::vector<std::string> ret; std::string tmp = ""; for(int i=0; i<str.length(); i++){ if(tmp != "" && str[i] == c){ ret.push_back(tmp); tmp = ""; } else if(str[i] != c){ tmp += str[i]; } } if(tmp != "") ret.push_back(tmp); return ret; } int main(int argc, char** argv){ int M = 0; if(argc == 2){ std::stringstream ss(argv[1]); ss >> M; } if(M == 0) return 1; std::cerr << "M = " << M << std::endl; FeatureHashing fh; std::string line; int cnt = 0; while(std::getline(std::cin, line)){ std::cerr << "\r" << cnt++; std::vector<std::string> sp = split(line, ' '); std::map<std::string,double> mp; for(int i=1; i<sp.size(); i++){ double val; std::vector<std::string> fsp = split(sp[i], ':'); std::stringstream ss(fsp[1]); ss >> val; mp[fsp[0]] = val; } std::vector<double> fvec = fh.vectorize(M, mp); std::cout << sp[0]; for(int i=0; i<fvec.size(); i++){ if(fabs(fvec[i])<1e-8) continue; std::cout << " " << i+1 << ":" << fvec[i]; } std::cout << std::endl; } std::cerr << std::endl; fh.dump(M); return 0; }
結果
表の一番上は何もしない場合の結果(ベースライン)。
naiveな感じの
Mの大きさ | 使ったハッシュ値の種類数 | 2-fold cv Accuracy |
---|---|---|
(1,355,191) | - | (95.5791%) |
500,000 | 466,615 | 95.094% |
100,000 | 100,000 | 94.5439% |
50,000 | 50,000 | 93.8188% |
10,000 | 10,000 | 91.3983% |
5,000 | 5,000 | 88.7678% |
FNV-1
Mの大きさ | 使ったハッシュ値の種類数 | 2-fold cv Accuracy |
---|---|---|
(1,355,191) | - | (95.5791%) |
500,000 | 467,764 | 95.4441% |
100,000 | 100,000 | 94.979% |
50,000 | 50,000 | 94.3989% |
10,000 | 10,000 | 91.4183% |
5,000 | 5,000 | 88.7828% |
murmurhash
Mの大きさ | 使ったハッシュ値の種類数 | 2-fold cv Accuracy | |
---|---|---|---|
(1,355,191) | - | (95.5791%) | |
500,000 | 467,088 | 95.5391% | |
100,000 | 100,000 | 95.059% | |
50,000 | 50,000 | 94.7139% | |
10,000 | 10,000 | 91.6183% | |
5,000 | 5,000 | 88.6777% |
(M=50万のとき、使われないハッシュ値ができてる)
次元が1/10ぐらいになってもAccuracyがほとんど減ってない。
ハッシュ関数がかわると大きく変わるかなと思ったけど、そんなに大きな変化はしないみたい。
参考
- http://en.wikipedia.org/wiki/Feature_hashing
- http://hunch.net/~jl/projects/hash_reps/
- http://alex.smola.org/papers/2009/Weinbergeretal09.pdf
- http://www.slideshare.net/pfi/pfi-seminar-20120315
- http://www.ss.cs.tut.ac.jp/nlp2011/nlp2010_tutorial_okanohara.pdf
- http://www.cs.cornell.edu/~jlmo/nips11.pdf