人工データを作る

Mahoutというか,その中にはそれっぽいライブラリが見つからなかったのだけど,科学技術計算の実験系で人工データを作りたいときに困ったので作ってみた.

import java.io.Serializable;
import java.util.Random;
import org.apache.mahout.math.DenseVector;
import org.apache.mahout.math.Vector;
public class SyntheticDataGenerator implements Serializable {
private static final long serialVersionUID = 1L;
private Random[] randoms;
private double[] means;
private double[] stdevs;
public SyntheticDataGenerator(long seed) {
means = new double[1];
means[0] = 0;
stdevs = new double[1];
stdevs[0] = 1;
init(seed, 1, means, stdevs);
}
public SyntheticDataGenerator(long seed, int cardinality, double[] means,
double[] stdevs) {
init(seed, cardinality, means, stdevs);
}
private void init(long seed, int cardinality, double[] means,
double[] stdevs) {
if (cardinality != means.length || cardinality != stdevs.length) {
throw new IllegalArgumentException("Invalid cardinality.");
}
randoms = new Random[cardinality];
for (int i = 0; i < cardinality; i++) {
randoms[i] = new Random(seed + i);
}
this.means = means;
this.stdevs = stdevs;
}
public double nextDouble() {
return nextDouble(0);
}
protected double nextDouble(int i) {
return randoms[i].nextGaussian() * stdevs[i] + means[i];
}
public double[] nextDoubles() {
double[] values = new double[randoms.length];
for (int i = 0; i < randoms.length; i++) {
values[i] = nextDouble(i);
}
return values;
}
public Vector nextVector() {
return new DenseVector(nextDoubles());
}
}

1次元の正規分布に基づく人工データを作りたいときには以下な感じ.

double[] means = new double[1];
double[] stdevs = new double[1];
means[0] = 10; // 平均 10
stdevs[0] = 5; // 標準偏差 5
SyntheticDataGenerator generator = new SyntheticDataGenerator(0, 1, means, stdevs);

あとは,generator.nextDouble() で値を取得していくと指定した分布の乱数が取得できる.そんで,多次元のデータが欲しい場合は,各次元ごとのmeansとstdevsを配列に格納して,generator.nextDoubles() としてやれば配列がとれるし,nextVector()でMahoutのVectorとして取得できる.

Vectorは良いのか?

気になるので,Vectorを使う場合とプリミティブな配列で処理したときのパフォーマンス影響を確認してみる.たぶん,Vectorで利用される実装はDenseVectorとRandomAccessSparseVectorあたりな気がするので,これらと比較する.DenseVectorは内部的にはプリミティブ配列と同様に全次元数分の容量を確保する(内部的にはdouble配列だし).RandomAccessSparseVectorは必要な要素だけを確保する(簡単に言うと内部的にはMapみみたいなイメージだと思う).単純に以下のコードで比較してみた.

public void test_performance() {
int count = 100;
int dim = .../*次元数*/;
int testNum = .../*利用した要素数*/;
testDenseVector(count, dim, testNum);
sleep(5000);
testSparseVector(count, dim, testNum);
sleep(5000);
testArrayVector(count, dim, testNum);
sleep(5000);
testDenseVector(count, dim, testNum);
sleep(5000);
testSparseVector(count, dim, testNum);
sleep(5000);
testArrayVector(count, dim, testNum);
}
private void testArrayVector(int count, int dim, int testNum) {
long time = System.currentTimeMillis();
long oldHeapSize = getHeapSize();
double[][] data = new double[count][];
for (int i = 0; i < count; i++) {
data[i] = new double[dim];
for (int j = 0; j < testNum; j++) {
data[i][j] = j;
}
}
long heapSize = getHeapSize();
System.out.println("array vector: "
+ (System.currentTimeMillis() - time) + "ms, " + heapSize
+ "MB(" + (heapSize - oldHeapSize) + "MB)");
for (int i = 0; i < count; i++) {
for (int j = 0; j < testNum; j++) {
data[i][j] = j;
}
}
}
private void testDenseVector(int count, int dim, int testNum) {
long time = System.currentTimeMillis();
long oldHeapSize = getHeapSize();
DenseVector[] vectors = new DenseVector[count];
for (int i = 0; i < count; i++) {
vectors[i] = new DenseVector(dim);
for (int j = 0; j < testNum; j++) {
vectors[i].setQuick(j, j);
}
}
long heapSize = getHeapSize();
System.out.println("dense vector: "
+ (System.currentTimeMillis() - time) + "ms, " + heapSize
+ "MB(" + (heapSize - oldHeapSize) + "MB)");
for (int i = 0; i < count; i++) {
for (int j = 0; j < testNum; j++) {
vectors[i].setQuick(j, j);
}
}
}
private void testSparseVector(int count, int dim, int testNum) {
long time = System.currentTimeMillis();
long oldHeapSize = getHeapSize();
RandomAccessSparseVector[] vectors = new RandomAccessSparseVector[count];
for (int i = 0; i < count; i++) {
vectors[i] = new RandomAccessSparseVector(dim);
for (int j = 0; j < testNum; j++) {
vectors[i].setQuick(j, j);
}
}
long heapSize = getHeapSize();
System.out.println("sparse vector: "
+ (System.currentTimeMillis() - time) + "ms, " + heapSize
+ "MB(" + (heapSize - oldHeapSize) + "MB)");
for (int i = 0; i < count; i++) {
for (int j = 0; j < testNum; j++) {
vectors[i].setQuick(j, j);
}
}
}
private long getHeapSize() {
final Runtime runtime = Runtime.getRuntime();
return (runtime.totalMemory() - runtime.freeMemory()) / 1000000;
}
private void sleep(long time) {
System.gc();
try {
Thread.sleep(time);
} catch (InterruptedException e) {
}
}

GCとかの都合とかもあってスリープとかもろもろ入れておく.

まず,100,000次元で10,000要素を使った場合は

dense vector: 103ms, 80MB(80MB)
sparse vector: 133ms, 37MB(37MB)
array vector: 92ms, 80MB(80MB)

次に,100,000次元で20,000要素を使った場合は

dense vector: 113ms, 80MB(80MB)
sparse vector: 237ms, 67MB(67MB)
array vector: 99ms, 80MB(80MB)

さらに,要素数を25,000にすると

dense vector: 114ms, 80MB(80MB)
sparse vector: 388ms, 110MB(110MB)
array vector: 108ms, 80MB(80MB)

で最後に100,000要素を利用すると

dense vector: 130ms, 80MB(80MB)
sparse vector: 1469ms, 376MB(376MB)
array vector: 110ms, 80MB(80MB)

という感じだった.

というわけで,メモリ的には,次元数の20%以下の利用で済むのであれば,RandomAccessSparseVectorで,それ以上ならDenseVectorが良いかな(時間的なことを考えると,10%くらいでもよいのかも).プリミティブ配列とDenseVectorに大きな差はないけど,若干早いような感じかね.プリミティブ配列で処理するかは,10%の速度向上をとるか,利便性をとるかのどちらが必要かを考えて判断するべきかね.

Tasteについて思うこと

ここ数カ月くらい、研究の実験でMahoutのTasteを結構使ってみました。個人的な感想ですが、フレームワークとしては使えるけど、富豪(?)でない人が利用するのは厳しい気がしました。ここでいう富豪とは最新のPCを数十や数百台とか使って問題解決できる人のことを指してます。最新のPCを数台とかで計算するようなときにはTasteの中身の実装は非効率と感じています(富豪でもコストパフォーマンスを気にする人は微妙かもな)。機械学習みたいな、科学技術計算的な問題を解こうとすると、基本は激しいループ処理なのでその中でnew ~とかでインスタンスを作ったり、インスタンスの配列やListやMapなどで何かしだすと終わりません…。MahoutのVectorもベクトルの計算するには便利なのですけど、富豪でないとちょっと厳しい気がしてきています。Javaだと、インスタンスの破棄をGCに期待することになり、メモリは消費されるし、GCスレッドも負荷が高くなるし、とかなりやられました(パラレルGC、CMS、G1とかも試したけど、CMSが一番良かった気がする)。そんなわけで、始めはTasteに乗っかって作っていましたが、最終的にはインターフェースは同じ感じだけど、中身の実装は作りこんでいった感じ(Mahoutが実装したKDDCupDataModelだと10G以上のメモリが必要っぽいけど,実装しなおすと6Gくらいでさくっと扱えるようになったりします)。改善して行った点は、インスタンスの生成やListとかMapの利用とかもできる限りやめて、基本はプリミティブな値やその配列とかでやる感じにしました(Javaっぽくないけど)。そんな感じの改善をしていくことで、数十~数百倍くらいのパフォーマンスが改善できた気がしてます。というわけで、富豪でない私みたいな人用に、プリミティブな配列とかでベクトルや行列の計算ができるようなものを作る必要があるのかな、と考え始めてます…。