繁体   English   中英

Java中对稀疏矩阵行的有效修改

[英]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 方法完成。 我很感激弗拉基米尔(或其他人)关于在该方法中避免getRowsetRow额外建议。 请注意,如果重要的话,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 的所有帮助。 就其价值而言,使用la4jhackMultiply方法的版本在时间使用上与使用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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM