最近在個性化推薦系統的優化過程中遇到一些問題,大致描述如下:目前在我們的推薦系統中,各個推薦策略召回的item相對較為固定,這樣就會導致一些問題,用戶在多個推薦場景(如果多個推薦場景下使用了相同的召回策略)、多次請求時得到的結果也較為固定,對流量的利用效率會有所降低;尤其對於行為較少的用戶,用來作為trigger的行為數據本身就很少,這樣就使得召回item同質化較為嚴重,使得第一個問題更加明顯。
目前的解決方法是,在推薦策略的召回階段加入一定的隨機機制,使得用戶在多個場景、多次請求時能后給用戶展示相似但不完全雷同的結果 。所以問題就轉化為在N個召回結果(召回結果需要適當地擴大)中隨機抽樣出K個結果,兩個難點:
1. N的值很大時,直接在N個數中取K個數實際是比較慢的,再加上我們這里還要求是不重復的采樣,這就導致每次產生的隨機數采樣的結果與之前采樣的某一個結果一致就需要重新進行采樣,這就導致線上計算的性能會受到影響,這個影響隨着N的增加會越來越嚴重。所以我們需要有一種時間復雜度較小的采樣算法,如O(N)的時間復雜度。
2. 對於推薦策略召回的結果,其實每個item是具有不同的權重(相似度)的,所以我們也可以利用到這部分信息,即在抽樣時並不是等概率采樣,而是帶權重的概率采樣。
對於第一個問題,我們可以使用蓄水池算法來解決。首先先看這個問題的簡化版,即從n個數中隨機采樣出1個數。
解法:我們總是選擇第一個對象,以1/2的概率選擇第二個,以1/3的概率選擇第三個,以此類推,以1/m的概率選擇第m個對象。當該過程結束時,每一個對象具有相同的選中概率,即1/n,證明如下。
證明:第m個對象最終被選中的概率P=選擇m的概率*其后面所有對象不被選擇的概率,即
再來看對應的蓄水池抽樣問題,即從n個數中隨機采樣k個數。可以類似的思路解決。先把讀到的前k個對象放入“水庫”,對於第k+1個對象開始,以k/(k+1)的概率選擇該對象,以k/(k+2)的概率選擇第k+2個對象,以此類推,以k/m的概率選擇第m個對象(m>k)。如果m被選中,則隨機替換水庫中的一個對象。最終每個對象被選中的概率均為k/n,證明如下。
證明:第m個對象被選中的概率=選擇m的概率*(其后元素不被選擇的概率+其后元素被選擇的概率*不替換第m個對象的概率),即
實際代碼實現還是比較簡單的:
1 List<Map<String, Object>> sampleList = new ArrayList<>(); 2 for (int i=0; i<sampleNum; ++i) { 3 sampleList.add(rawList.get(i)); 4 } 5 for (int i=sampleNum; i<rawListSize; ++i) { 6 int j = r.nextInt(i+1); 7 if (j < sampleNum) { 8 sampleList.remove(j); 9 sampleList.add(rawList.get(i)); 10 } 11 }
再來看看第二個問題,這就涉及到了帶權重的概率抽樣問題了。那有沒有在蓄水池算法基礎上的帶權重概率的抽樣算法呢?當然是有的,想要詳細了解的可以直接看paper《Weighted random sampling with a reservoir》。
首先對於每個樣本,都具有一個權重Wi,我們可以針對這個權重值做一個變換作為每個樣本的得分:sampleScore = random(0, 1)^(1/Wi)。然后采樣過程與之前的一致,也是對每個樣本進行順序讀取。對前k個樣本維護一個最小堆(針對sampleScore排序),然后對於后續的樣本,每次來一個樣本,都將這個新樣本的sampleScore與之前的最小樣本的sampleScore進行比較,如果比最小sampleScore要大,則推出這個最小值,壓入這個新樣本並繼續維護這個最小堆,直到所有樣本都被遍歷過一次。
具體的代碼實現如下:
Comparator<Map<String, Object>> cmp = new Comparator<Map<String, Object>>() { public int compare(Map<String, Object> e1, Map<String, Object> e2) { return Double.compare((double)e1.get(sampleScoreField), (double)e2.get(sampleScoreField)); } }; PriorityQueue<Map<String, Object>> pq = new PriorityQueue<>(sampleNum, cmp); for (int i=0; i<sampleNum; ++i) { Map<String, Object> item = rawList.get(i); double sampleScore = Math.pow(r.nextDouble(), 1.0/(0.001+MapUtils.getDoubleValue(item, weightField, 0.0))); item.put(sampleScoreField, sampleScore); pq.add(item); } for (int i=sampleNum; i<rawListSize; ++i) { Map<String, Object> item = rawList.get(i); double sampleScore = Math.pow(r.nextDouble(), 1.0/(0.001+MapUtils.getDoubleValue(item, weightField, 0.0))); item.put(sampleScoreField, sampleScore); Map<String, Object> minItem = pq.peek(); if (sampleScore > (double)minItem.get(sampleScoreField)) { pq.remove(); pq.add(item); } }
以上。
版權聲明:
本文由笨兔勿應所有,發布於http://www.cnblogs.com/bentuwuying。如果轉載,請注明出處,在未經作者同意下將本文用於商業用途,將追究其法律責任。