Spark ML特征处理局部敏感散列(Locality Sensitive Hashing)

LSH:将向量进行哈希分桶,使得原语义上相似的文本大概率被哈希到同一个桶中,同个桶内的文本可以认为是大概率是相似的。

LSH:局部敏感哈希算法,是一种针对海量高维数据的快速最近邻查找算法,主要有如下用法:

  • 全基因组的相关研究:生物学家经常使用 LSH 在基因组数据库中鉴定相似的基因表达。
  • 大规模的图片搜索: Google 使用 LSH 和 PageRank 来构建他们的图片搜索技术VisualRank。
  • 音频/视频指纹识别:在多媒体技术中,LSH 被广泛用于 A/V 数据的指纹识别。
  • 近似重复的检测: LSH 通常用于对大量文档,网页和其他文件进行去重处理。

在推荐系统等应用中,经常会遇到的一个问题就是面临着海量的高维数据,查找最近邻。如果使用线性查找对于高维数据计算量和耗时都是灾难性的。为了解决这样的问题,出现了一种特殊的hash函数,使得2个相似度很高的数据以较高的概率映射成同一个hash值,而令2个相似度很低的数据以极低的概率映射成同一个hash值。我们把这样的函数,叫做LSH(局部敏感哈希)。

局部敏感散列(LSH)是一类重要的散列技术,常用于大数据集的聚类、近似最近邻搜索和异常值检测。 LSH 的总体思路是使用一个函数族(“LSH family”)将数据点散列到桶中,使得彼此靠近的数据点很有可能在同一个桶中,而距离较远的数据点 彼此远离的很可能在不同的桶中。 LSH 家族的正式定义如下:

在度量空间 (M, d) 中,其中 M 是一个集合,d 是 M 上的距离函数,LSH 族是满足以下属性的函数 h 族:

 

 

 这个 LSH 家族被称为 (r1, r2, p1, p2) 敏感的。

在 Spark 中,不同的 LSH 系列在不同的类中实现(例如 MinHash),并且在每个类中提供了用于特征变换、近似相似连接和近似最近邻的 API。

在 LSH 中,我们将误报定义为一对远处的输入特征(d(p,q)≥r2),它们散列到同一个桶中,我们将误报定义为一对附近的特征(d( p,q)≤r1) 被散列到不同的桶中。

 

 

一、LSH Operations

描述了 LSH 可用于的主要操作类型。 拟合的 LSH 模型具有用于这些操作中的每一个的方法。

1.1 Feature Transformation(特征转换)

特征转换是将散列值添加为新列的基本功能。 这对于降维很有用。 用户可以通过设置 inputCol 和 outputCol 来指定输入和输出列名。

LSH 还支持多个 LSH 哈希表。 用户可以通过设置 numHashTables 来指定哈希表的数量。 这也用于近似相似连接和近似最近邻中的 OR-amplification增加哈希表的数量会提高准确性,但也会增加通信成本和运行时间。

outputCol 的类型是 Seq[Vector],其中数组的维度等于 numHashTables,向量的维度当前设置为 1。

1.2 Approximate Similarity Join(近似相似连接)

近似相似连接采用两个数据集,并近似返回数据集中距离小于用户定义阈值的行对。 近似相似连接既支持连接两个不同的数据集,也支持自连接。 自连接会产生一些重复的对。

近似相似连接接受转换和未转换的数据集作为输入。 如果使用未转换的数据集,它将自动转换。 在这种情况下,哈希签名将被创建为 outputCol。 在joined dataset中,可以在datasetA和datasetB中查询原始数据集。 距离列将添加到输出数据集中,以显示返回的每对行之间的真实距离。

1.3 Approximate Nearest Neighbor Search(近似最近邻搜索)

近似最近邻搜索采用数据集(特征向量)和键(单个特征向量),它近似返回数据集中与向量最近的指定行数。 近似最近邻搜索接受转换和未转换的数据集作为输入。 如果使用未转换的数据集,它将自动转换。 在这种情况下,哈希签名将被创建为 outputCol。 距离列将添加到输出数据集中,以显示每个输出行与搜索键之间的真实距离。

注意:当哈希桶中没有足够的候选者时,近似最近邻搜索将返回少于 k 行。

 

二、LSH Algorithms

2.1 Bucketed Random Projection for Euclidean Distance

Bucketed Random Projection 是欧几里得距离的 LSH 系列。 欧几里得距离定义如下:

 

 

 它的 LSH 系列将特征向量 x 投影到随机单位向量 v 上,并将投影结果分成哈希桶:

 

 

 其中 r 是用户定义的桶长度。 桶长度可用于控制哈希桶的平均大小(以及桶的数量)。 更大的桶长度(即更少的桶)会增加特征被散列到同一个桶的概率(增加真假阳性的数量)。

Bucketed Random Projection 接受任意向量作为输入特征,并支持稀疏向量和密集向量。

%spark
// 特征处理局部敏感散列 —— —— BucketedRandomProjectionLSH
// 输入是密集或稀疏向量,每个向量代表欧几里得距离空间中的一个点。 输出将是可配置维度的向量。 同一维度的哈希值由相同的哈希函数计算得出。
import org.apache.spark.ml.feature.BucketedRandomProjectionLSH
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions.col

val dfA = spark.createDataFrame(Seq(
  (0, Vectors.dense(1.0, 1.0)),
  (1, Vectors.dense(1.0, -1.0)),
  (2, Vectors.dense(-1.0, -1.0)),
  (3, Vectors.dense(-1.0, 1.0))
)).toDF("id", "features")

val dfB = spark.createDataFrame(Seq(
  (4, Vectors.dense(1.0, 0.0)),
  (5, Vectors.dense(-1.0, 0.0)),
  (6, Vectors.dense(0.0, 1.0)),
  (7, Vectors.dense(0.0, -1.0))
)).toDF("id", "features")

val key = Vectors.dense(1.0, 0.0)

val brp = new BucketedRandomProjectionLSH()
// 每个哈希桶的长度,较大的桶降低了误报率。 
// 桶的数量将是(输入向量的最大 L2 范数)/桶长度。
// 如果输入向量被归一化,1-10 倍 pow(numRecords, -1/inputDim) 将是一个合理的值 
  .setBucketLength(2.0)
// LSH OR-amplification 中使用的哈希表数量的参数。
// LSH OR-amplification 可用于降低假阴性率。 此参数的较高值会降低假阴性率,但会增加计算复杂性。 
  .setNumHashTables(3)
//   随机种子的参数。 
//   .setSeed()
  .setInputCol("features")
  .setOutputCol("hashes")

// 拟合数据训练转换器
val model = brp.fit(dfA)

// 1、特征转换
println("哈希值存储在“哈希”列中的哈希数据集: ")
model.transform(dfA).show(false)

// 2、计算输入行的局部敏感哈希,然后执行近似相似性连接。 
// 可以通过传入已经转换的数据集来避免计算哈希,例如 `model.approxSimilarityJoin(transformedA,transformedB,1.5)` 
// approxSimilarityJoin方法:连接两个数据集以近似找到距离小于阈值的所有行对。 如果 outputCol 缺失,该方法将转换数据; 如果 outputCol 存在,它将使用 outputCol。
println("在小于 1.5 的欧几里得距离上近似加入 dfA 和 dfB: ")
model.approxSimilarityJoin(dfA, dfB, 1.5, "EuclideanDistance")
  .select(col("datasetA.id").alias("idA"),
    col("datasetB.id").alias("idB"),
    col("EuclideanDistance")).show()

// 3、计算输入行的局部敏感哈希,然后执行近似最近邻搜索。 
// 可以通过传入已经转换的数据集来避免计算哈希,例如`model.approxNearestNeighbors(transformedA, key, 2)` 
// 给定一个大数据集和一个项目,大约找到与该项目最近距离的 k 个项目。 如果 outputCol 缺失,该方法将转换数据; 如果 outputCol 存在,它将使用 outputCol。
println("近似搜索 dfA 的 2 个最近邻居: ")
model.approxNearestNeighbors(dfA, key, 2).show(false)

输出:
哈希值存储在“哈希”列中的哈希数据集: 
+---+-----------+-----------------------+
|id |features   |hashes                 |
+---+-----------+-----------------------+
|0  |[1.0,1.0]  |[[0.0], [0.0], [-1.0]] |
|1  |[1.0,-1.0] |[[-1.0], [-1.0], [0.0]]|
|2  |[-1.0,-1.0]|[[-1.0], [-1.0], [0.0]]|
|3  |[-1.0,1.0] |[[0.0], [0.0], [-1.0]] |
+---+-----------+-----------------------+

在小于 1.5 的欧几里得距离上近似加入 dfA 和 dfB: 
+---+---+-----------------+
|idA|idB|EuclideanDistance|
+---+---+-----------------+
|  1|  4|              1.0|
|  0|  6|              1.0|
|  1|  7|              1.0|
|  3|  5|              1.0|
|  0|  4|              1.0|
|  3|  6|              1.0|
|  2|  7|              1.0|
|  2|  5|              1.0|
+---+---+-----------------+

近似搜索 dfA 的 2 个最近邻居: 
+---+----------+-----------------------+-------+
|id |features  |hashes                 |distCol|
+---+----------+-----------------------+-------+
|0  |[1.0,1.0] |[[0.0], [0.0], [-1.0]] |1.0    |
|1  |[1.0,-1.0]|[[-1.0], [-1.0], [0.0]]|1.0    |
+---+----------+-----------------------+-------+

 

2.2 MinHash for Jaccard Distance

MinHash 是 Jaccard 距离的 LSH 系列,其中输入特征是自然数集。 两个集合的 Jaccard 距离由它们的交集和并集的基数定义:

MinHash 将随机散列函数 g 应用于集合中的每个元素,并取所有散列值中的最小值:

 

 

 MinHash 的输入集表示为二进制向量,其中向量索引表示元素本身,向量中的非零值表示该元素在集合中的存在。 虽然支持密集和稀疏向量,但通常建议使用稀疏向量以提高效率。 例如 Vectors.sparse(10, Array[(2, 1.0), (3, 1.0), (5, 1.0)]) 表示空间中有 10 个元素。 该集合包含元素 2、元素 3 和元素 5。所有非零值都被视为二进制“1”值。

注意:空集不能被 MinHash 转换,这意味着任何输入向量必须至少有 1 个非零条目。

%spark
// 特征处理局部敏感散列 —— —— MinHashLSH
// Jaccard 距离的 LSH 类。
// 输入可以是密集或稀疏向量,但如果是稀疏的,则效率更高。 
// 例如 Vectors.sparse(10, Array((2, 1.0), (3, 1.0), (5, 1.0))) 表示空间中有 10 个元素。 该集合包含元素 2、3 和 5。此外,任何输入向量必须至少有 1 个非零索引,并且所有非零值都被视为二进制“1”值。 
import org.apache.spark.ml.feature.MinHashLSH
import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions.col

val dfA = spark.createDataFrame(Seq(
  (0, Vectors.sparse(6, Seq((0, 1.0), (1, 1.0), (2, 1.0)))),
  (1, Vectors.sparse(6, Seq((2, 1.0), (3, 1.0), (4, 1.0)))),
  (2, Vectors.sparse(6, Seq((0, 1.0), (2, 1.0), (4, 1.0))))
)).toDF("id", "features")

val dfB = spark.createDataFrame(Seq(
  (3, Vectors.sparse(6, Seq((1, 1.0), (3, 1.0), (5, 1.0)))),
  (4, Vectors.sparse(6, Seq((2, 1.0), (3, 1.0), (5, 1.0)))),
  (5, Vectors.sparse(6, Seq((1, 1.0), (2, 1.0), (4, 1.0))))
)).toDF("id", "features")

val key = Vectors.sparse(6, Seq((1, 1.0), (3, 1.0)))

val mh = new MinHashLSH()
// LSH OR-amplification 中使用的哈希表数量的参数。
// LSH OR-amplification 可用于降低假阴性率。 此参数的较高值会降低假阴性率,但会增加计算复杂性。 
  .setNumHashTables(5)
//   随机种子的参数。 
//   .setSeed()
  .setInputCol("features")
  .setOutputCol("hashes")

val model = mh.fit(dfA)

// 1、特征转换
println("哈希值存储在“哈希”列中的哈希数据集: ")
model.transform(dfA).show()

// 计算输入行的局部敏感哈希,然后执行近似相似连接。
// 我们可以通过传入已经转换的数据集来避免计算哈希,例如 `model.approxSimilarityJoin(transformedA,transformedB,0.6)` 
println("在小于 0.6 的 Jaccard 距离上近似加入 dfA 和 dfB: ")
model.approxSimilarityJoin(dfA, dfB, 0.6, "JaccardDistance")
  .select(col("datasetA.id").alias("idA"),
    col("datasetB.id").alias("idB"),
    col("JaccardDistance")).show()

// 计算输入行的局部敏感哈希,然后执行近似最近邻搜索。
// 我们可以通过传入已经转换的数据集来避免计算哈希,例如
// `model.approxNearestNeighbors(transformedA, key, 2)`
// 当没有找到足够的近似近邻候选时,它可能返回少于 2 行。 
println("近似搜索 dfA 的 2 个最近邻居: ")
model.approxNearestNeighbors(dfA, key, 2).show()

输出:
哈希值存储在“哈希”列中的哈希数据集: 
+---+--------------------+--------------------+
| id|            features|              hashes|
+---+--------------------+--------------------+
|  0|(6,[0,1,2],[1.0,1...|[[2.25592966E8], ...|
|  1|(6,[2,3,4],[1.0,1...|[[2.25592966E8], ...|
|  2|(6,[0,2,4],[1.0,1...|[[2.25592966E8], ...|
+---+--------------------+--------------------+

在小于 0.6 的 Jaccard 距离上近似加入 dfA 和 dfB: 
+---+---+---------------+
|idA|idB|JaccardDistance|
+---+---+---------------+
|  1|  4|            0.5|
|  0|  5|            0.5|
|  1|  5|            0.5|
|  2|  5|            0.5|
+---+---+---------------+

近似搜索 dfA 的 2 个最近邻居: 
+---+--------------------+--------------------+-------+
| id|            features|              hashes|distCol|
+---+--------------------+--------------------+-------+
|  1|(6,[2,3,4],[1.0,1...|[[2.25592966E8], ...|   0.75|
+---+--------------------+--------------------+-------+

 

posted @ 2022-03-07 17:08  干了这瓶老干妈  阅读(439)  评论(0编辑  收藏  举报
Live2D