【Spark原理系列】 SparseVector原理示例源码分析

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
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BigDataMLApplication

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值