LSH-區域性敏感雜湊

魚與魚發表於2022-04-17

假設通過使用者 - 物品相似度進行個性化推薦

使用者和物品的 Embedding 都在一個 \(k\) 維的 Embedding 空間中,物品總數為 \(n\),計算一個使用者和所有物品向量相似度的時間複雜度是$ O(k*n)$

直覺的解決方案

  • 基於聚類
  • 基於索引

基於聚類的思想

優點:

離線計算好每個 Embedding 向量的類別,線上上我們只需要在同一個類別內的 Embedding 向量中搜尋就可以。

缺點:

  • 存在著一些邊界情況,比如,聚類邊緣的點的最近鄰往往會包括相鄰聚類的點,如果我們只在類別內搜尋,就會遺漏這些近似點

  • 中心點的數量 k 也不那麼好確定,k 選得太大,離線迭代的過程就會非常慢,k 選得太小,線上搜尋的範圍還是很大

基於索引

Kd-tree(K-dimension tree)

首先,構建索引,然後用二叉樹搜尋

比如,希望找到點 q 的 m 個鄰接點,我們就可以先搜尋它相鄰子樹下的點,如果數量不夠,我們可以向上回退一個層級,搜尋它父片區下的其他點,直到數量湊夠 m 個為止

缺點:

  • 無法完全解決邊緣點最近鄰的問題。對於點 q 來說,它的鄰接片區是右上角的片區,但是它的最近鄰點卻是深藍色切分線下方的那個點。

區域性敏感雜湊

基本原理

區域性敏感雜湊的基本思想是希望讓相鄰的點落入同一個“桶”,這樣在進行最近鄰搜尋時,我們僅需要在一個桶內,或相鄰幾個桶內的元素中進行搜尋即可。如果保持每個桶中的元素個數在一個常數附近,我們就可以把最近鄰搜尋的時間複雜度降低到常數級別。

把二維空間中的點通過不同角度對映到 a、b、c 這三個一維空間時,可以看到原本相近的點,在一維空間中都保持著相近的距離。而原本遠離的綠色點和紅色點在一維空間 a 中處於接近的位置,卻在空間 b 中處於遠離的位置。

歐式空間中,將高維空間的點對映到低維空間,原本接近的點在低維空間中肯定依然接近,但原本遠離的點則有一定概率變成接近的點

內積相似度是經常使用的相似度度量方法,還有曼哈頓距離,切比雪夫距離,漢明距離 等。假如 用內積操作來構建區域性敏感雜湊桶

假設 \(v\) 是高維空間中的 $k $ 維 Embedding 向量,$x $ 是隨機生成的 \(k\) 維對映向量。那我們利用內積操作可以將 $ v $ 對映到一維空間,得到數值$ h(v)=v⋅x$。

使用雜湊函式 \(h(v)\) 進行分桶,公式為:$h^{x,b}(v)= ⌊\frac{x⋅v+b}{w}⌋ $ 。其中, \(⌊⌋\) 是向下取整操作, \(w\) 是分桶寬度,\(b\)\(0\)\(w\) 間的一個均勻分佈隨機變數,避免分桶邊界固化。

防止相鄰的點每次都落在不同的桶

如果總是固定邊界,很容易讓邊界兩邊非常接近的點總是被分到兩個桶裡。這是我們不想看到的。 所以隨機調整b,生成多個hash函式,並且採用或的方式組合,就可以一定程度避免這些邊界點的問題。

採用 m 個雜湊函式同時進行分桶。如果兩個點同時掉進了 m 個桶,那它們是相似點的概率將大大增加。

用幾個hash函式?怎麼組合多個hash函式?

多桶策略

關於怎麼組合多個hash函式,沒有固定做法,可以用不同的組合策略.

  • 點數越多,我們越應該增加每個分桶函式中桶的個數;相反,點數越少,我們越應該減少桶的個數;

  • Embedding 向量的維度越大,我們越應該增加雜湊函式的數量,儘量採用且的方式作為多桶策略;相反,Embedding 向量維度越小,我們越應該減少雜湊函式的數量,多采用或的方式作為分桶策略。

一些經驗值設定

每個桶取多少點跟線上想尋找top N的規模有關係。比如召回層想召回1000個物品,那麼N就是1000,那麼桶內點數的規模就維持在1000-5000的級別是比較合適的

Embedding在實踐中超過100維後,增加維度的作用就沒那麼明顯了,通常取10-50維就足夠了

hash函式數量的初始判斷,有個經驗公式:Embedding維數開4次方。後續,調參按照2的倍數進行調整,例如:2,4,8,16

區域性敏感雜湊實踐

基於spark BucketedRandomProjectionLSH

def embeddingLSH(spark:SparkSession, movieEmbMap:Map[String, Array[Float]]): Unit ={
  //將電影embedding資料轉換成dense Vector的形式,便於之後處理
  val movieEmbSeq = movieEmbMap.toSeq.map(item => (item._1, Vectors.dense(item._2.map(f => f.toDouble))))
  val movieEmbDF = spark.createDataFrame(movieEmbSeq).toDF("movieId", "emb")


  //利用Spark MLlib建立LSH分桶模型
  val bucketProjectionLSH = new BucketedRandomProjectionLSH()
    .setBucketLength(0.1)
    .setNumHashTables(3)
    .setInputCol("emb")
    .setOutputCol("bucketId")
  //訓練LSH分桶模型
  val bucketModel = bucketProjectionLSH.fit(movieEmbDF)
  //進行分桶
  val embBucketResult = bucketModel.transform(movieEmbDF)
  
  //列印分桶結果
  println("movieId, emb, bucketId schema:")
  embBucketResult.printSchema()
  println("movieId, emb, bucketId data result:")
  embBucketResult.show(10, truncate = false)
  
  //嘗試對一個示例Embedding查詢最近鄰
  println("Approximately searching for 5 nearest neighbors of the sample embedding:")
  val sampleEmb = Vectors.dense(0.795,0.583,1.120,0.850,0.174,-0.839,-0.0633,0.249,0.673,-0.237)
  bucketModel.approxNearestNeighbors(movieEmbDF, sampleEmb, 5).show(truncate = false)
}

refenences:

【1】王喆. 深度學習推薦系統

【2】Facebook 最近鄰搜尋庫 FAISS

相關文章