[英]Efficient modification of the rows of a sparse matrix in Java
我需要“规范化”一个大的稀疏矩阵,以便每行中的条目总和为 1。对于具有一些非零条目的行,每个条目除以行总和。 对于全为零的行,每个条目都替换为 1/numberOfColumns(使矩阵的稀疏程度大大降低)。
如果重要的话,我的矩阵是正方形的,而且是对称的。 我首先使用“小”测试样本,其中我的矩阵大小约为 32K x 32K——但我们最终需要它来处理更大的矩阵。 时间和内存效率都很重要(内存比时间更重要)。 在标准化之前,大约 1% 的条目是非零的。
将nxn矩阵归一化后,需要乘以一系列nxk密集矩阵,其中k是一个小数(<10)
我目前正在使用 la4j matrix 包,但我与它没有任何关系。
我在下面包含了我的规范化代码。 在我的 32K x 32K 测试用例中,它非常慢。 我从一些实验中了解到,花费最多时间的部分是重新设置 T 矩阵的行(下面是我的 t.setRow 行)。
三个问题:
1)是否有更好的包装或更好的方法来有效地做到这一点?
2)我不应该为此使用稀疏矩阵表示吗?
3)最好不要替换零行,而只是跟踪哪些行是零,然后相应地修改矩阵乘法?
提前致谢!
SparseMatrix t = new CRSMatrix(n, n);
// in between non-zero entries in t are added one at a time
double uniformWeight = (double) 1 / n; // used when the rowSum is zero
for (int i = 0; i < n; i++) {
Vector row = t.getRow(i);
double rowSum = row.sum();
if (rowSum > 0) {
row = row.divide(rowSum);
t.setRow(i,row);
} else {
row.assign(uniformWeight); // Assigns all elements of this vector to given value
t.setRow(i, row);
}
}
更新 1
这是我尝试过的方法,确实加快了速度。 我还没有尝试过 Vladimir 的建议,但我下周回到这个项目时会添加这些建议。 现在我保留了一个布尔数组,其中的行为零行,并且在矩阵乘法过程中应该被视为统一行,现在使用我的 hackMultiply 方法完成。 我很感激弗拉基米尔(或其他人)关于在该方法中避免getRow
和setRow
额外建议。 请注意,如果重要的话,m2(第二个被乘数)和 m3(结果矩阵)不是稀疏的。 提前致谢!
boolean[] zeroRows = new boolean[n]; // all values default to false
for (int i = 0; i < n; i++) {
Vector row = t.getRow(i); // haven't had a chance to try Vladimir's
double rowSum = row.sum(); // improvements here yet
if (rowSum > 0) {
row = row.divide(rowSum); // or here
t.setRow(i,row); // ...
} else {
// instead of setting to uniform weight, just keep track
zeroRows[i] = true;
}
}
/**
* Multiplies the given matrices m1 (Sparse) and m2, with the following modifications
* For every row in m1 for which the corrresponding entry in zeroRows is true
* use uniformVector instead of the actual contents of the row for the multiplication
* (Meant for the case when the replaced rows are all zeros, and we don't want
* to actually replace them in the SparseMatrix because then it wouldn't be sparse
* anymore.)
*
* @param m1 First multiplicand
* @param m2 Second multiplicand
* @param zeroRows boolean array indicating which rows of m1 should be logically replaced by uniformVector
* @param uniformVector vector to replace all the zeroRows with during the multiplication
* @return the result of the hacked multiplication
*/
public static Matrix hackMultiply(SparseMatrix m1, Matrix m2, boolean[] zeroRows, Vector uniformVector) {
// for "efficiency" I'm assuming I get passed in things of appropriate sizes, no checking
int a = m1.rows();
int b = m2.columns();
int c = m1.columns(); // must == m2.rows() for the matrix multiplication to work
Matrix m3 = new Basic2DMatrix(a,b);
Vector v1;
for (int i = 0; i < a; i++) {
if (zeroRows[i]) {
v1 = uniformVector;
} else {
v1 = m1.getRow(i);
}
m3.setRow(i, v1.multiply(m2));
}
return m3;
}
更新 2
我非常感谢@Vladimir 和@mikera 的所有帮助。 就其价值而言,使用la4j
和hackMultiply
方法的版本在时间使用上与使用vectorz
版的版本几乎相同。 看起来 0.27.0 中的升级会给它一点优势,但我还没有尝试过。 对于内存使用,带有 hack 的 la4j 版本比 vectorz 版本好得多,至少我现在编写代码的方式是这样。 我怀疑 0.27.0 版本也可能对此有所帮助。
我是 la4j 库的作者。 我确实在您的代码中看到了几个改进的地方。 这是我的建议:
调用getRow
(以及setRow
)总是一个坏主意(尤其是对于稀疏矩阵),因为它会启动矩阵行的完整复制。 我建议你避免这样的电话。 因此,没有getRow
/ setRow
代码应该如下所示:
SparseMatrix t = new CRSMatrix(n, n);
double uniformWeight = (double) 1 / n; // used when the rowSum is zero
for (int i = 0; i < n; i++) {
double rowSum = t.foldRow(i, Matrices.asSumAccumulator(0.0));
if (rowSum > 0.0) {
MatrixFunction divider = Matrices.asDivFunction(rowSum);
for (int j = 0; j < n; j++) {
// TODO: I should probably think about `updateRow` method
// in order to avoid this loop
t.update(i, j, divider);
}
} else {
for (int j = 0; j < n; j++) {
t.set(i, j, uniformWeight);
}
}
}
请试试这个。 我没有编译它,但它应该可以工作。
更新
使用布尔数组来跟踪相同的行是一个绝妙的主意。 这里的主要瓶颈是循环:
for (int j = 0; j < n; j++) {
t.set(i, j, uniformWeight);
}
这里我们完全破坏了稀疏矩阵的性能/足迹,因为将整行分配给相同的值。 所以,我想说,将这两个想法结合在一起:避免getRow/setRow
+ 带有标志的额外数组(我会使用 BitSet,它在占用空间方面效率更高)应该会给你一个很棒的性能。
感谢您使用 la4j 库,请向邮件列表或 GitHub 页面报告性能/功能方面的任何问题。 所有参考资料均可在此处获得: http : //la4j.org 。
如果您不依赖于 la4j, Vectorz包(我维护的)有一些工具可以非常有效地执行这些类型的操作。 这是可能的,因为有两个特点:
我会使用的策略是:
VectorMatrixMN
以将矩阵存储为稀疏向量行的集合SparseIndexedVector
,这是一种适用于几乎为零的数据的有效格式然后可以使用以下代码对矩阵的行进行归一化:
VectorMatrixMN m = ....
for (int i=0; i<SIZE; i++) {
AVector row=m.getRow(i);
double sum=row.elementSum();
if (sum>0) {
row.divide(sum);
} else {
m.setRow(i, new RepeatedElementVector(SIZE,1.0/SIZE));
}
}
请注意,此代码正在就地修改行,因此您无需执行“setRow”之类的任何操作即可将数据恢复到矩阵中。
将此配置与 32,000 x 32,000 稀疏矩阵和每行 100 个非零值的密度一起使用,我将其定时为小于 32ms 以使用此代码对整个矩阵进行归一化(即每个非零元素约 10ns == 0.03ns每个矩阵元素:因此您显然通过利用稀疏性获得了很大的好处)。
您还可以选择对全为零的行使用ZeroVector
(这些会更快,但会施加一些额外的约束,因为 ZeroVectors 是不可变的.....)
编辑:
我编写了一个完整的示例,演示了如何将稀疏矩阵用于与此问题非常相似的用例:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.