Spark SparseVector原理示例源码分析
原理
Spark中的SparseVector
是一个表示稀疏向量的类。它使用两个数组来存储非零元素的索引和对应的值,从而节省了内存空间。
构造函数SparseVector(size: Int, indices: Array[Int], values: Array[Double])
接收三个参数:向量的大小size
、非零元素的索引数组indices
和对应的值数组values
。通过这些参数,可以创建一个稀疏向量对象。
在内部实现中,SparseVector
使用了压缩稀疏列(Compressed Sparse Column)的数据结构。索引数组indices
存储非零元素的索引,值数组values
存储非零元素的值。这两个数组的长度相等,且按照索引的升序排列。
SparseVector
提供了一系列方法来处理稀疏向量。例如,toString
方法用于将稀疏向量转换为字符串表示形式,toArray
方法将稀疏向量转换为密集数组,apply
方法用于获取指定索引位置的值,foreachActive
方法用于遍历非零元素并执行指定操作,等等。
通过使用SparseVector
,可以高效地表示和操作大规模稀疏向量,节省内存空间,并且支持常见的向量操作和线性代数运算。
方法总结
方法 | 描述 |
---|---|
SparseVector 构造函数 | 创建一个稀疏向量对象,参数包括向量的大小、索引数组和值数组。需要满足一定的条件,如大小必须大于等于0,索引数组和值数组长度必须相等,并且索引数组的维度不能超过向量的大小。 |
toString | 将稀疏向量转换为字符串表示形式。 |
toArray | 将稀疏向量转换为密集数组。 |
copy | 复制稀疏向量。 |
asBreeze | 将稀疏向量转换为 Breeze 稀疏向量对象。 |
apply | 获取稀疏向量指定索引位置的值。如果索引不存在于索引数组中,则返回0.0。 |
foreachActive | 遍历稀疏向量的非零元素,并对每个非零元素执行指定操作。 |
equals | 判断稀疏向量是否与另一个对象相等。 |
hashCode | 计算稀疏向量的哈希码。 |
numActives | 获取稀疏向量的非零元素数量。 |
numNonzeros | 获取稀疏向量的非零值数量。 |
toSparseWithSize | 根据指定的非零元素数量创建一个新的稀疏向量,如果指定的非零元素数量与当前向量的非零元素数量相同,则返回当前向量。 |
argmax | 找到稀疏向量中的最大元素的索引。如果向量为空,则返回-1;如果所有元素都是零,则返回0。如果最大的非零元素非正且存在未激活的元素,则找到第一个零元素的索引。 |
slice | 根据给定的索引创建向量的切片。传入的索引数组可以是无序的,该方法会按照给定索引的顺序排列切片中的元素。注意:该方法需要进一步讨论,如果假设索引已排序,则可以进行优化。 |
unapply | 提取稀疏向量对象的大小、索引数组和值数组。 |
示例说明
package org.example
import org.apache.spark.ml.linalg.SparseVector
object SparseVectorTest extends App{
// SparseVector 构造函数
val sv = new SparseVector(5, Array(0, 2, 4), Array(1.0, 2.0, 3.0))
println(sv) // 输出:(5,[0,2,4],[1.0,2.0,3.0])
// toString 方法
val str = sv.toString
println(str) // 输出:(5,[0,2,4],[1.0,2.0,3.0])
// toArray 方法
val arr = sv.toArray
println(arr.mkString(", ")) // 输出:0.0, 1.0, 0.0, 2.0, 3.0
// copy 方法
val copy = sv.copy
println(copy) // 输出:(5,[0,2,4],[1.0,2.0,3.0])
// private method asBreeze 方法
// val breezeVector = sv.asBreeze
// println(breezeVector) // 输出:(5,[0,2,4],[1.0,2.0,3.0])
// apply 方法
val value = sv.apply(2)
println(value) // 输出:0.0
// foreachActive 方法
sv.foreachActive { (index, value) =>
println(s"Index: $index, Value: $value")
}
// 输出:
// Index: 0, Value: 1.0
// Index: 2, Value: 2.0
// Index: 4, Value: 3.0
// equals 方法
val otherVector = new SparseVector(5, Array(0, 2, 4), Array(1.0, 2.0, 3.0))
val isEqual = sv.equals(otherVector)
println(isEqual) // 输出:true
// hashCode 方法
val hashCode1 = sv.hashCode()
println(hashCode) // 输出:-997097006
// numActives 方法
val numActives = sv.numActives
println(numActives) // 输出:3
// numNonzeros 方法
val numNonzeros = sv.numNonzeros
println(numNonzeros) // 输出:3
// private method toSparseWithSize 方法
// val sparseWithSize = sv.toSparseWithSize(3)
// println(sparseWithSize) // 输出:(5,[0,2,4],[1.0,2.0,3.0])
// argmax 方法
val maxIndex = sv.argmax
println(maxIndex) // 输出:4
// private method slice 方法
// val selectedIndices = Array(2, 4)
// val slicedVector = sv.slice(selectedIndices)
// println(slicedVector) // 输出:(2,[1,2],[2.0,3.0])
// unapply 方法
val SparseVector(size, indices, values) = sv
println(size) // 输出:5
println(indices.mkString(", ")) // 输出:0, 2, 4
println(values.mkString(", ")) // 输出:1.0, 2.0, 3.0
}
中文源码
SparseVector
/**
* 通过索引数组和值数组表示的稀疏向量。
*
* @param size 向量的大小。
* @param indices 索引数组,假设严格递增。
* @param values 值数组,必须与索引数组长度相同。
*/
@Since("2.0.0")
class SparseVector @Since("2.0.0") (
override val size: Int,
@Since("2.0.0") val indices: Array[Int],
@Since("2.0.0") val values: Array[Double]) extends Vector {
// 验证数据的有效性
{
require(size >= 0, "请求的稀疏向量的大小必须不小于0。")
require(indices.length == values.length, "稀疏向量要求索引的维度与值的维度匹配。提供了" +
s"${indices.length}个索引和${values.length}个值。")
require(indices.length <= size, s"提供了${indices.length}个索引和值,超过了指定的向量大小${size}。")
if (indices.nonEmpty) {
require(indices(0) >= 0, s"发现负索引:${indices(0)}。")
}
var prev = -1
indices.foreach { i =>
require(prev < i, s"索引$i跟随$prev,并且不是严格递增的。")
prev = i
}
require(prev < size, s"索引$prev超出了向量的大小$size。")
}
override def toString: String =
s"($size,${indices.mkString("[", ",", "]")},${values.mkString("[", ",", "]")})"
override def toArray: Array[Double] = {
val data = new Array[Double](size)
var i = 0
val nnz = indices.length
while (i < nnz) {
data(indices(i)) = values(i)
i += 1
}
data
}
override def copy: SparseVector = {
new SparseVector(size, indices.clone(), values.clone())
}
private[spark] override def asBreeze: BV[Double] = new BSV[Double](indices, values, size)
override def apply(i: Int): Double = {
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException(s"索引$i超出了范围[0, $size)")
}
val j = util.Arrays.binarySearch(indices, i)
if (j < 0) 0.0 else values(j)
}
override def foreachActive(f: (Int, Double) => Unit): Unit = {
var i = 0
val localValuesSize = values.length
val localIndices = indices
val localValues = values
while (i < localValuesSize) {
f(localIndices(i), localValues(i))
i += 1
}
}
override def equals(other: Any): Boolean = super.equals(other)
override def hashCode(): Int = {
var result: Int = 31 + size
val end = values.length
var k = 0
var nnz = 0
while (k < end && nnz < Vectors.MAX_HASH_NNZ) {
val v = values(k)
if (v != 0.0) {
val i = indices(k)
result = 31 * result + i
val bits = java.lang.Double.doubleToLongBits(v)
result = 31 * result + (bits ^ (bits >>> 32)).toInt
nnz += 1
}
k += 1
}
result
}
override def numActives: Int = values.length
override def numNonzeros: Int = {
var nnz = 0
values.foreach { v =>
if (v != 0.0) {
nnz += 1
}
}
nnz
}
private[linalg] override def toSparseWithSize(nnz: Int): SparseVector = {
// 如果指定的非零元素数量与当前向量的非零元素数量相同,则返回当前向量
if (nnz == numActives) {
this
} else {
val ii = new Array[Int](nnz)
val vv = new Array[Double](nnz)
var k = 0
foreachActive { (i, v) =>
// 只保留非零元素的索引和值
if (v != 0.0) {
ii(k) = i
vv(k) = v
k += 1
}
}
new SparseVector(size, ii, vv)
}
}
override def argmax: Int = {
if (size == 0) {
-1
} else if (numActives == 0) {
0
} else {
// 找到最大的非零元素
var maxIdx = indices(0)
var maxValue = values(0)
var maxJ = 0
var j = 1
val na = numActives
while (j < na) {
val v = values(j)
if (v > maxValue) {
maxValue = v
maxIdx = indices(j)
maxJ = j
}
j += 1
}
// 如果最大的非零元素非正且存在未激活的元素,则找到第一个零元素的索引
if (maxValue <= 0.0 && na < size) {
if (maxValue == 0.0) {
// 如果在maxIdx之前存在未激活的元素,则找到它并返回其索引
if (maxJ < maxIdx) {
var k = 0
while (k < maxJ && indices(k) == k) {
k += 1
}
maxIdx = k
}
} else {
// 如果最大的非零值为负数,则找到并返回第一个未激活的索引
var k = 0
while (k < na && indices(k) == k) {
k += 1
}
maxIdx = k
}
}
maxIdx
}
}
/**
* 根据给定的索引创建向量的切片。
* @param selectedIndices 无序的索引列表,用于指定切片中的元素。
* 注意:这不会进行边界检查。
* @return 新的SparseVector,其中的值按照给定索引的顺序排列。
*
* 注意:在将其公开之前,需要讨论该API。如果我们有一个假设索引已排序的版本,应该进行优化。
*/
private[spark] def slice(selectedIndices: Array[Int]): SparseVector = {
var currentIdx = 0
val (sliceInds, sliceVals) = selectedIndices.flatMap { origIdx =>
val iIdx = java.util.Arrays.binarySearch(this.indices, origIdx)
val i_v = if (iIdx >= 0) {
Iterator((currentIdx, this.values(iIdx)))
} else {
Iterator()
}
currentIdx += 1
i_v
}.unzip
new SparseVector(selectedIndices.length, sliceInds.toArray, sliceVals.toArray)
}
}
@Since("2.0.0")
object SparseVector {
@Since("2.0.0")
def unapply(sv: SparseVector): Option[(Int, Array[Int], Array[Double])] =
Some((sv.size, sv.indices, sv.values))
}
object Vectors
/**
* 用于创建 [[org.apache.spark.ml.linalg.Vector]] 的工厂方法。
* 由于Scala默认导入了`scala.collection.immutable.Vector`,因此这里没有使用`Vector`作为名称。
*/
@Since("2.0.0")
object Vectors {
/**
* 从值数组创建一个密集向量。
*/
@varargs
@Since("2.0.0")
def dense(firstValue: Double, otherValues: Double*): Vector =
new DenseVector((firstValue +: otherValues).toArray)
// 使用一个虚拟的implicit避免与@varargs生成的方法签名冲突。
/**
* 从双精度数组创建一个密集向量。
*/
@Since("2.0.0")
def dense(values: Array[Double]): Vector = new DenseVector(values)
/**
* 通过索引数组和值数组创建一个稀疏向量。
*
* @param size 向量大小。
* @param indices 索引数组,必须严格递增。
* @param values 值数组,长度必须与indices相同。
*/
@Since("2.0.0")
def sparse(size: Int, indices: Array[Int], values: Array[Double]): Vector =
new SparseVector(size, indices, values)
/**
* 使用无序的(索引,值)对创建一个稀疏向量。
*
* @param size 向量大小。
* @param elements 向量元素,以(索引,值)对的形式给出。
*/
@Since("2.0.0")
def sparse(size: Int, elements: Seq[(Int, Double)]): Vector = {
val (indices, values) = elements.sortBy(_._1).unzip
new SparseVector(size, indices.toArray, values.toArray)
}
/**
* 使用无序的(索引,值)对创建一个稀疏向量,适用于Java环境。
*
* @param size 向量大小。
* @param elements 向量元素,以(索引,值)对的形式给出。
*/
@Since("2.0.0")
def sparse(size: Int, elements: JavaIterable[(JavaInteger, JavaDouble)]): Vector = {
sparse(size, elements.asScala.map { case (i, x) =>
(i.intValue(), x.doubleValue())
}.toSeq)
}
/**
* 创建一个全零向量。
*
* @param size 向量大小
* @return 全零向量
*/
@Since("2.0.0")
def zeros(size: Int): Vector = {
new DenseVector(new Array[Double](size))
}
/**
* 从breeze向量创建一个向量实例。
*/
private[spark] def fromBreeze(breezeVector: BV[Double]): Vector = {
breezeVector match {
case v: BDV[Double] =>
if (v.offset == 0 && v.stride == 1 && v.length == v.data.length) {
new DenseVector(v.data)
} else {
new DenseVector(v.toArray) // 无法直接使用底层数组,因此创建一个新数组
}
case v: BSV[Double] =>
if (v.index.length == v.used) {
new SparseVector(v.length, v.index, v.data)
} else {
new SparseVector(v.length, v.index.slice(0, v.used), v.data.slice(0, v.used))
}
case v: BV[_] =>
sys.error("不支持的Breeze向量类型:" + v.getClass.getName)
}
}
/**
* 返回该向量的p-范数。
* @param vector 输入向量。
* @param p 范数。
* @return L^p^空间中的范数。
*/
@Since("2.0.0")
def norm(vector: Vector, p: Double): Double = {
require(p >= 1.0, "为了计算向量的p-范数,要求指定p>=1。您指定的p=" + p + "。")
val values = vector match {
case DenseVector(vs) => vs
case SparseVector(n, ids, vs) => vs
case v => throw new IllegalArgumentException("不支持的向量类型:" + v.getClass)
}
val size = values.length
if (p == 1) {
var sum = 0.0
var i = 0
while (i < size) {
sum += math.abs(values(i))
i += 1
}
sum
} else if (p == 2) {
var sum = 0.0
var i = 0
while (i < size) {
sum += values(i) * values(i)
i += 1
}
math.sqrt(sum)
} else if (p == Double.PositiveInfinity) {
var max = 0.0
var i = 0
while (i < size) {
val value = math.abs(values(i))
if (value > max) max = value
i += 1
}
max
} else {
var sum = 0.0
var i = 0
while (i < size) {
sum += math.pow(math.abs(values(i)), p)
i += 1
}
math.pow(sum, 1.0 / p)
}
}
/**
* 返回两个向量之间的平方距离。
* @param v1 第一个向量。
* @param v2 第二个向量。
* @return 两个向量之间的平方距离。
*/
@Since("2.0.0")
def sqdist(v1: Vector, v2: Vector): Double = {
require(v1.size == v2.size, s"向量维度不匹配:Dim(v1)=${v1.size},Dim(v2)=${v2.size}。")
var squaredDistance = 0.0
(v1, v2) match {
case (v1: SparseVector, v2: SparseVector) =>
val v1Values = v1.values
val v1Indices = v1.indices
val v2Values = v2.values
val v2Indices = v2.indices
val nnzv1 = v1Indices.length
val nnzv2 = v2Indices.length
var kv1 = 0
var kv2 = 0
while (kv1 < nnzv1 || kv2 < nnzv2) {
var score = 0.0
if (kv2 >= nnzv2 || (kv1 < nnzv1 && v1Indices(kv1) < v2Indices(kv2))) {
score = v1Values(kv1)
kv1 += 1
} else if (kv1 >= nnzv1 || (kv2 < nnzv2 && v2Indices(kv2) < v1Indices(kv1))) {
score = v2Values(kv2)
kv2 += 1
} else {
score = v1Values(kv1) - v2Values(kv2)
kv1 += 1
kv2 += 1
}
squaredDistance += score * score
}
case (v1: SparseVector, v2: DenseVector) =>
squaredDistance = sqdist(v1, v2)
case (v1: DenseVector, v2: SparseVector) =>
squaredDistance = sqdist(v2, v1)
case (DenseVector(vv1), DenseVector(vv2)) =>
var kv = 0
val sz = vv1.length
while (kv < sz) {
val score = vv1(kv) - vv2(kv)
squaredDistance += score * score
kv += 1
}
case _ =>
throw new IllegalArgumentException("不支持的向量类型:" + v1.getClass +
" 和 " + v2.getClass)
}
squaredDistance
}
/**
* 检查稀疏/密集向量之间的相等性。
*/
private[ml] def equals(
v1Indices: IndexedSeq[Int],
v1Values: Array[Double],
v2Indices: IndexedSeq[Int],
v2Values: Array[Double]): Boolean = {
val v1Size = v1Values.length
val v2Size = v2Values.length
var k1 = 0
var k2 = 0
var allEqual = true
while (allEqual) {
while (k1 < v1Size && v1Values(k1) == 0) k1 += 1
while (k2 < v2Size && v2Values(k2) == 0) k2 += 1
if (k1 >= v1Size || k2 >= v2Size) {
return k1 >= v1Size && k2 >= v2Size // 检查末尾对齐
}
allEqual = v1Indices(k1) == v2Indices(k2) && v1Values(k1) == v2Values(k2)
k1 += 1
k2 += 1
}
allEqual
}
/** 计算哈希码时使用的非零元素的最大数量。 */
private[linalg] val MAX_HASH_NNZ = 128
trait Vector
/**
* 表示一个数字向量,其索引类型为Int,值类型为Double。
*
* @note 用户不应该实现此接口。
*/
@Since("2.0.0")
sealed trait Vector extends Serializable {
/**
* 向量的大小。
*/
@Since("2.0.0")
def size: Int
/**
* 将实例转换为双精度数组。
*/
@Since("2.0.0")
def toArray: Array[Double]
override def equals(other: Any): Boolean = {
other match {
case v2: Vector =>
if (this.size != v2.size) return false
(this, v2) match {
case (s1: SparseVector, s2: SparseVector) =>
Vectors.equals(s1.indices, s1.values, s2.indices, s2.values)
case (s1: SparseVector, d1: DenseVector) =>
Vectors.equals(s1.indices, s1.values, 0 until d1.size, d1.values)
case (d1: DenseVector, s1: SparseVector) =>
Vectors.equals(0 until d1.size, d1.values, s1.indices, s1.values)
case (_, _) => util.Arrays.equals(this.toArray, v2.toArray)
}
case _ => false
}
}
/**
* 返回向量的哈希码值。哈希码基于向量的大小和其前128个非零元素,使用类似`java.util.Arrays.hashCode`的哈希算法。
*/
override def hashCode(): Int = {
// 这是一个参考实现。它在foreachActive中调用return,这会导致速度较慢。
// 子类应该使用优化的实现来重写它。
var result: Int = 31 + size
var nnz = 0
this.foreachActive { (index, value) =>
if (nnz < Vectors.MAX_HASH_NNZ) {
// 忽略稀疏向量和密集向量之间的显式0进行比较
if (value != 0) {
result = 31 * result + index
val bits = java.lang.Double.doubleToLongBits(value)
result = 31 * result + (bits ^ (bits >>> 32)).toInt
nnz += 1
}
} else {
return result
}
}
result
}
/**
* 将实例转换为breeze向量。
*/
private[spark] def asBreeze: BV[Double]
/**
* 获取第i个元素的值。
* @param i 索引
*/
@Since("2.0.0")
def apply(i: Int): Double = asBreeze(i)
/**
* 创建此向量的深拷贝。
*/
@Since("2.0.0")
def copy: Vector = {
throw new NotImplementedError(s"copy is not implemented for ${this.getClass}.")
}
/**
* 对密集向量和稀疏向量的所有有效元素应用函数`f`。
*
* @param f 函数,接受两个参数,第一个参数是类型为`Int`的向量索引,第二个参数是类型为`Double`的相应值。
*/
@Since("2.0.0")
def foreachActive(f: (Int, Double) => Unit): Unit
/**
* 活跃条目的数量。 "活跃条目"是明确存储的元素,无论其值如何。请注意,非活跃条目的值为0。
*/
@Since("2.0.0")
def numActives: Int
/**
* 非零元素的数量。这将扫描所有活跃值并计算非零元素的数量。
*/
@Since("2.0.0")
def numNonzeros: Int
/**
* 将此向量转换为删除所有显式零的稀疏向量。
*/
@Since("2.0.0")
def toSparse: SparseVector = toSparseWithSize(numNonzeros)
/**
* 将此向量转换为删除所有显式零的稀疏向量,当大小已知时使用。
* 当已经计算了非零元素的数量时,可以使用此方法来避免重新计算。例如:
* {{{
* val nnz = numNonzeros
* val sv = toSparse(nnz)
* }}}
*
* 如果`nnz`未指定,则会抛出[[java.lang.ArrayIndexOutOfBoundsException]]。
*/
private[linalg] def toSparseWithSize(nnz: Int): SparseVector
/**
* 将此向量转换为密集向量。
*/
@Since("2.0.0")
def toDense: DenseVector = new DenseVector(this.toArray)
/**
* 返回一个以密集或稀疏格式表示的向量,以占用较少的存储空间。
*/
@Since("2.0.0")
def compressed: Vector = {
val nnz = numNonzeros
// 一个密集向量需要8 * size + 8字节,而一个稀疏向量需要12 * nnz + 20字节。
if (1.5 * (nnz + 1.0) < size) {
toSparseWithSize(nnz)
} else {
toDense
}
}
/**
* 找到最大元素的索引。如果存在多个最大元素,返回第一个。如果向量长度为0,则返回-1。
*/
@Since("2.0.0")
def argmax: Int
}