假設通過使用者 - 物品相似度進行個性化推薦
使用者和物品的 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】王喆. 深度學習推薦系統