简体   繁体   English

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

[英]Efficient modification of the rows of a sparse matrix in Java

I need to "normalize" a large sparse matrix so that the sum of the entries in each row is 1. For rows that have some non-zero entries, each entry is divided by the row sum.我需要“规范化”一个大的稀疏矩阵,以便每行中的条目总和为 1。对于具有一些非零条目的行,每个条目除以行总和。 For rows that are all zeros, each entry is replaced by 1/numberOfColumns (making the matrix considerably less sparse).对于全为零的行,每个条目都替换为 1/numberOfColumns(使矩阵的稀疏程度大大降低)。

If it matters, my matrix is square, and symmetric.如果重要的话,我的矩阵是正方形的,而且是对称的。 I'm working first with a "small" test sample, where my matrix is about 32K x 32K in size -- but we ultimately need this to work with much larger matrices.我首先使用“小”测试样本,其中我的矩阵大小约为 32K x 32K——但我们最终需要它来处理更大的矩阵。 Both time and memory efficiency will be important (with memory a bit more critical than time).时间和内存效率都很重要(内存比时间更重要)。 Before normalization about 1% of the entries are non-zero.在标准化之前,大约 1% 的条目是非零的。

After normalizing the nxn matrix, it needs to be multiplied by a series of nxk dense matrices, where k is a small number (<10)将nxn矩阵归一化后,需要乘以一系列nxk密集矩阵,其中k是一个小数(<10)

I'm currently using the la4j matrix package, but I'm not in any way tied to it.我目前正在使用 la4j matrix 包,但我与它没有任何关系。

I've included my normalization code below.我在下面包含了我的规范化代码。 It's horribly slow, in my 32K x 32K test case.在我的 32K x 32K 测试用例中,它非常慢。 I know from a bit of experimentation that the piece that takes the most time is re-setting the rows of the T matrix (my t.setRow lines below).我从一些实验中了解到,花费最多时间的部分是重新设置 T 矩阵的行(下面是我的 t.setRow 行)。

Three questions:三个问题:

1) Is there a better package or better approach to doing this efficiently? 1)是否有更好的包装或更好的方法来有效地做到这一点?

2) Should I not be using a sparse matrix representation for this? 2)我不应该为此使用稀疏矩阵表示吗?

3) Would it be best to not replace the zero rows but just keep track of which rows were zeros, and then hack the matrix multiplication accordingly? 3)最好不要替换零行,而只是跟踪哪些行是零,然后相应地修改矩阵乘法?

Thanks in advance!提前致谢!

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);
        }
    }

Update 1更新 1

Here's what I tried that did speed things up quite a bit.这是我尝试过的方法,确实加快了速度。 I haven't tried Vladimir's suggestions yet but I will add those when I get back to this project next week.我还没有尝试过 Vladimir 的建议,但我下周回到这个项目时会添加这些建议。 Now I am keeping a boolean array of which rows are zero rows, and should be treated as uniform rows during the matrix multipication, which is now done with my hackMultiply method.现在我保留了一个布尔数组,其中的行为零行,并且在矩阵乘法过程中应该被视为统一行,现在使用我的 hackMultiply 方法完成。 I would appreciate Vladimir's (or others') additional advice on avoiding the getRow and setRow in that method.我很感激弗拉基米尔(或其他人)关于在该方法中避免getRowsetRow额外建议。 Note that m2 (the second multiplicand) and m3 (the result matrix) are not sparse, if that matters.请注意,如果重要的话,m2(第二个被乘数)和 m3(结果矩阵)不是稀疏的。 Thanks in advance!提前致谢!

    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;
}

Update 2更新 2

I really appreciate all the help from @Vladimir and @mikera.我非常感谢@Vladimir 和@mikera 的所有帮助。 For what it's worth, the version using la4j with the hackMultiply approach is pretty much identical in time use to the version using vectorz version 0.26.0.就其价值而言,使用la4jhackMultiply方法的版本在时间使用上与使用vectorz版的版本几乎相同。 It looks like the upgrades in 0.27.0 will give it a slight edge, but I haven't tried it yet.看起来 0.27.0 中的升级会给它一点优势,但我还没有尝试过。 For memory use, the la4j version with the hack is considerably better than the vectorz version, at least the way I have the code written right now.对于内存使用,带有 hack 的 la4j 版本比 vectorz 版本好得多,至少我现在编写代码的方式是这样。 I suspect the 0.27.0 release may help there as well.我怀疑 0.27.0 版本也可能对此有所帮助。

I'm the author of the la4j library.我是 la4j 库的作者。 I do see several places of improvement in your code.我确实在您的代码中看到了几个改进的地方。 Here is my advice:这是我的建议:

Calling getRow (as well as setRow ) is always a bad idea (especially for sparse matrices), since it launches a full-copying of the matrix row.调用getRow (以及setRow )总是一个坏主意(尤其是对于稀疏矩阵),因为它会启动矩阵行的完整复制。 I would suggest you to avoid such calls.我建议你避免这样的电话。 Thus, w/o getRow / setRow code should looks like:因此,没有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);
    }
  }
}

Please, try this.请试试这个。 I didn't compile it, but it should work.我没有编译它,但它应该可以工作。

Update更新

Using a boolean array in order to keep track of the same rows is a fantastic idea.使用布尔数组来跟踪相同的行是一个绝妙的主意。 The main bottle-neck here is the loop:这里的主要瓶颈是循环:

for (int j = 0; j < n; j++) {
  t.set(i, j, uniformWeight);
}

Here we're completely ruining the performance/footprint of the sparse matrice since assigning the entire row to the same value.这里我们完全破坏了稀疏矩阵的性能/足迹,因为将整行分配给相同的值。 So, I would say, combining these two ideas together: avoid getRow/setRow + extra array with flags (I would use BitSet instead, it is much efficien in terms of footprint) should give you an awesome performance.所以,我想说,将这两个想法结合在一起:避免getRow/setRow + 带有标志的额外数组(我会使用 BitSet,它在占用空间方面效率更高)应该会给你一个很棒的性能。

Thank you for using the la4j library, and please, report any issues with performance/functional to mail-list, or GitHub page.感谢您使用 la4j 库,请向邮件列表或 GitHub 页面报告性能/功能方面的任何问题。 All references are available here: http://la4j.org .所有参考资料均可在此处获得: http : //la4j.org

If you're not tied to la4j, the Vectorz package (which I maintain) has some tools to do these kinds of operations very efficiently.如果您不依赖于 la4j, Vectorz包(我维护的)有一些工具可以非常有效地执行这些类型的操作。 This is possible because of two features:这是可能的,因为有两个特点:

  • Sparse storage of data数据的稀疏存储
  • Lightweight mutable "views", so you can mutate rows of a matrix as vectors in-place轻量级可变“视图”,因此您可以将矩阵的行就地变异为向量

The strategy I would use is:我会使用的策略是:

  • Create a VectorMatrixMN to store a matrix as a collection of sparse vector rows创建一个VectorMatrixMN以将矩阵存储为稀疏向量行的集合
  • Use a SparseIndexedVector for each row, which is an efficient format for mostly-zero data对每一行使用SparseIndexedVector ,这是一种适用于几乎为零的数据的有效格式

Normalising the rows of the matrix can then be done with the following code:然后可以使用以下代码对矩阵的行进行归一化:

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));
    }
}

Note that this code is modifying the rows in-place, so you don't need to do anything like "setRow" to get the data back in the matrix.请注意,此代码正在就地修改行,因此您无需执行“setRow”之类的任何操作即可将数据恢复到矩阵中。

Using this configuration with a 32,000 x 32,000 sparse matrix and a density of 100 non-zero values per row, I timed this at less than 32ms to normalise the whole matrix with this code (ie about 10ns per non-zero element == 0.03ns per matrix element : so you are clearly getting big benefits by exploiting the sparsity).将此配置与 32,000 x 32,000 稀疏矩阵和每行 100 个非零值的密度一起使用,我将其定时为小于 32ms 以使用此代码对整个矩阵进行归一化(即每个非零元素约 10ns == 0.03ns每个矩阵元素:因此您显然通过利用稀疏性获得了很大的好处)。

You could also optionally use a ZeroVector for rows that are all-zero (these will be even faster, but impose some extra constraints since ZeroVectors are immutable.....)您还可以选择对全为零的行使用ZeroVector (这些会更快,但会施加一些额外的约束,因为 ZeroVectors 是不可变的.....)

EDIT:编辑:

I've coded a complete example that demonstrates using sparse matrices for a use case very similar to this question:我编写了一个完整的示例,演示了如何将稀疏矩阵用于与此问题非常相似的用例:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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