spark 稀疏矩陣儲存詳細揭祕
spark mllib模組中,矩陣的表示位於org.apache.spark.mllib.linalg包的Matrices中。而Matrix的表示又分兩種方式:dense與sparse。在實際場景應用場景中,因為大資料本身的稀疏性,sparse的方式比dense的方式使用更為頻繁。而網路上大部分的資料對與sparse方式解釋不是很清晰,本人也花了一些時間來理解,所以特此記錄。
1.稀疏矩陣的一些表達方式
1.1 coo
這種方式構造稀疏矩陣很容易理解,需要三個等長陣列,alues陣列存放矩陣中的非0元素,row indices存放非0元素的行座標,column indices存放非0元素的列座標。
這種方式的優點:
1.容易構造
2.可以快速地轉換成其他形式的稀疏矩陣
3.支援相同的(row,col)座標上存放多個值
缺點如下:
1.構建完成後不允許再插入或刪除元素
2.不能直接進行科學計算和切片操作
1.2 csr_matrix
csr_matrix同樣由3個數組組成,values儲存非0元素,column indices儲存非0元素的列座標,row offsets依次儲存每行的首元素在values中的座標,如果某行全是0則對應的row offsets值為-1。
這種方式的優點:
1.高效地按行切片
2.快速地計算矩陣與向量的內積
3.高效地進行矩陣的算術執行,CSR + CSR、CSR * CSR等
缺點:
1.按列切片很慢(考慮CSC)
2.一旦構建完成後,再往裡面新增或刪除元素成本很高
1.3 csc_matrix
csr是基於行,則csc是基於列,特點自然跟csr_matrix類似。
1.4 dia_matrix
對角線儲存法,按對角線方式存,列代表對角線,行代表行。省略全零的對角線。
適用場景:
如果原始矩陣就是一個對角性很好的矩陣那壓縮率會非常高,比如下圖,但是如果是隨機的那效率會非常糟糕。
當然還有根據各種場景合適的儲存方式,這裡就不再一一列舉了。
2.spark中的稀疏矩陣表達
mllib中,稀疏矩陣可以用Matrices.sparse
來生成。
先看看SparseMatrix的原始碼:
class SparseMatrix @Since("1.3.0") (
@Since("1.2.0") val numRows: Int,
@Since ("1.2.0") val numCols: Int,
@Since("1.2.0") val colPtrs: Array[Int],
@Since("1.2.0") val rowIndices: Array[Int],
@Since("1.2.0") val values: Array[Double],
@Since("1.3.0") override val isTransposed: Boolean) extends Matrix {
require(values.length == rowIndices.length, "The number of row indices and values don't match! " +
s"values.length: ${values.length}, rowIndices.length: ${rowIndices.length}")
// The Or statement is for the case when the matrix is transposed
require(colPtrs.length == numCols + 1 || colPtrs.length == numRows + 1, "The length of the " +
"column indices should be the number of columns + 1. Currently, colPointers.length: " +
s"${colPtrs.length}, numCols: $numCols")
require(values.length == colPtrs.last, "The last value of colPtrs must equal the number of " +
s"elements. values.length: ${values.length}, colPtrs.last: ${colPtrs.last}")
/**
* Column-major sparse matrix.
* The entry values are stored in Compressed Sparse Column (CSC) format.
* ...
上面程式碼的註釋中,透露了一個很重要的資訊:spark mllib中的稀疏矩陣是通過CSC的格式儲存的!
在spark shell中測試一下:
scala> import org.apache.spark.mllib.linalg.Matrices
import org.apache.spark.mllib.linalg.Matrices
scala> val mx = Matrices.sparse(2,4, Array(0, 1, 2, 3, 4),Array(0, 1, 1, 1), Array(9, 8, 6, 5))
mx: org.apache.spark.mllib.linalg.Matrix =
2 x 4 CSCMatrix
(0,0) 9.0
(1,1) 8.0
(1,2) 6.0
(1,3) 5.0
根據原始碼可以看出,構造一個稀疏矩陣需要5個引數,分別為行、列,colPtrs為每列的首元素在values中的座標,rowIndices為每個元素的行座標,values為對應的值。
3.如果有每行/每列均為0怎麼辦
CSC這種方式在儲存矩陣的時候,有三個陣列。最開始的時候,我想到一個問題:如果只存這三個陣列,能完整表示這個稀疏矩陣麼?
答案是不能。為什麼呢?這個問題經過作者深入思考,原因如下。
比如上面的矩陣為一個2*4的矩陣,具體形式為:
9 0 0 0
0 8 6 5
如果給上面矩陣新增一行全0行,
9 0 0 0
0 8 6 5
0 0 0 0
大家會發現這個稀疏矩陣按CSC的方式儲存,對應的三個陣列與上面的矩陣是一樣的!
因為colPtrs為矩陣的列數+1,所以如果只存三個陣列,只能確定矩陣的列數,無法確定矩陣的行數,矩陣往後面追加再多的全0行,對應的三個陣列都是一樣的!
所以在構造方法中,才會要求指定矩陣的行數與列數!
我們可以在spark shell中再嘗試一下:
scala> val mx = Matrices.sparse(3,4, Array(0, 1, 2, 3, 4),Array(0, 1, 1, 1), Array(9, 8, 6, 5))
mx: org.apache.spark.mllib.linalg.Matrix =
3 x 4 CSCMatrix
(0,0) 9.0
(1,1) 8.0
(1,2) 6.0
(1,3) 5.0
scala> val mx = Matrices.sparse(4,4, Array(0, 1, 2, 3, 4),Array(0, 1, 1, 1), Array(9, 8, 6, 5))
mx: org.apache.spark.mllib.linalg.Matrix =
4 x 4 CSCMatrix
(0,0) 9.0
(1,1) 8.0
(1,2) 6.0
(1,3) 5.0
我們不改變後面三個陣列的值,只改變建構函式中的行數,可以看出就構造除了不同的稀疏矩陣!