Apriori算法


最近在學大數據這門課,課上講到了一個關於尿布與啤酒的故事,說是發現在超市中尿布如果和啤酒放在一起能跟提高銷量,原因是買尿布的多是父親,這些人看到啤酒后就想買(這是什么邏輯)。當然,這個故事被證明是虛構的了信息來源

不過這個故事引出了一個問題,如果在一群放在不同類目(baskets)中的物品(items)中尋找成對(pair)的物品,且物品在不同類目中出現了至少threshold次,那么應該怎樣做是有效率(空間上)的呢?

最naive的方法對於N個items,需要的操作數是  o(n2)   的時間復雜度,會生成n(n - 1)/2個pairs。問題是,如果物品數量很多呢?假如物品是商品(沃爾瑪的商品類是100k的l量級)。
假設 105  items, 那么pairs的數量就是 105(1051)/2=5109       。可以算下,如果每個pair用4-byte整數表示,總共需要 21010   (20GB)內存...不過實驗室的機器內存40G,這點東西還是存的下

為了解決占用內存過大問題,引入了Aprior算法。


什么是Apriori 算法

先看下Wikipedia的說明。先驗算法(英語:Apriori algorithm)是關聯式規則中的經典算法之一。在關聯式規則中,一般對於給定的項目集合baskets(例如,零售交易集合,每個集合都列出的單個商品的購買信息),算法通常嘗試在項目集合中找出至少有threshold個相同的子集。先驗算法采用自底向上的處理方法,即頻繁子集每次只擴展一個對象(該步驟被稱為候選集產生),並且候選集由數據進行檢驗。當不再產生符合條件的擴展對象時,算法終止。

看起來好簡單的算法,很快就實現了,但是跑起來慢得要死...最開始的版本,跑了一個晚上也沒有跑完數據,優化后的依然不行,經過再次優化,最終優化版本30s跑完,可是我錯過了Assignment提交的deadline


算法過程

簡單講,Aprior算法就是利用了單調性:如果一個集合I,I中的物品都至少出現了threshold次,那么任意I的子集,不可能出現的次數少於threshold次(threshold在這里是指一個門限值)。
反過來講,如果一個物品i出現次數不到threshold,那么凡是包含了i的集合,都不可能出現超過threshold次。

一般的實現思路是:

  1. 讀取所有baskets中的數據,並在內存中記錄至少出現了s次的item
  2. 重新讀取baskets,並只記錄那些成對且至少出現了s次的item

這樣需要的內存就是最常出現的items的平方了


如果找的不是成對,而是n對物品(n-tuple)怎么辦?

一圖勝千言, Ci 是備選i-tuple items, Li 是選出來合格的i-teple items,

該流程迭代到 Ci 為空為止


實踐

有10000個basket,同時有10000個數,第i個basket里放的都是能夠整除i的數,舉個例子: basket12    = {1,2,3,4,6,12}。那么,在所有basket中出現過100次及以上的組合(長度至少3個數)有哪些?舉個例子:(1,2,4)在所有的basket中出現了超過100次,且長度為3個數,它是一個我們要的答案

解決這個問題,我們按照上方提到的算法,先構造k-tuple,將其通過過濾器,獲取符合條件的的k-tuple,然后再將k-tuple構造成(k+1)-tuple,再繼續迭代即可。
舉一個簡單的例子: C1 ={{a}{b}{c}{d}{e}}=> L1 ={a,b,c,d} =>  C2 ={{a,b},{a,c},{a,d},{b,c},{b,d},{c,d}}=> L2 ={{a,b},{a,c},{c,d}}=> C3 ={{a,b,c},{a,c,d}}=> L3 ={{a,b,c}}
實際上,在生成(k+1)-tuple, 和過濾器這一步,有很多細節。如果剪枝剪得不充分,就會時間復雜度特別高,運行起來極其耗時。
根據這個算法,我們需要一個這樣的函數:construct_filter(baskets_set, last_result, length)
這個函數的作用是,將candidate items輸入,構造新的,長度為length的tuple,並對其過濾輸出。其中baskets_set是我們要用於檢查新tuple是否合格的源

第一個問題,如何構造(k+1)-tuple?
我的解決方法是,從 Ck 中取出所有的元素,組成為一個set,記為element,然后遍歷k-tuple和element, 這些k-tuple加上element中的元素,構成(k+1)-tuple。這里source是為了改進性能而增加的一個dict,后文為提到。

1
2
3
4
5
6
7
8
for atuple in last_result:
     for index in range ( len (atuple)):
         candidate_set.add(atuple[index])
         if atuple[index] in source.keys():
             source[atuple[index]].add(atuple)
         else :
             source[atuple[index]] = set ()
             source[atuple[index]].add(atuple)
第二個問題,如何過濾出現頻率沒有達到門限的(k+1)-tuple?
一開始我采用的方法是遍歷整個basket,來檢查(k+1)-tuple出現的次數是否超過了threshold。

顯然,這樣的實現有個很大的問題,在過濾時要遍歷整個baskets,不必要的計算使得運行時間幾個小時都跑不完。根據Apriori,

反過來講,如果一個物品i出現次數不到threshold,那么凡是包含了i的集合,都不可能出現超過threshold次。

這里應該剪枝,不應該遍歷整個baskets,而應該遍歷 Ck 中,各個元素出現過的的basket,這樣每次要遍歷的basket_set會越來越小,速度會有很大的提高。第一次改善,將所有element曾出現過的baskets存存到了baskets_set,驗證的時候遍歷baskets_set。不過這樣依然要跑不知多少個小時。所以我又加強了一下剪枝,用了python的dict做了一個映射,每個被選取作為(k+1)-tuple的第k+1位數,都在dict中對應的曾出現過的basket。這樣遍歷時,只需要從dict找到要遍歷的basket即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
for atuple in last_result:
     # shrinke the set
     temp_set =  candidate_set - set (atuple)
     # construct new (k+1)tuple
     temp_list = []
     for num in temp_set:
         # make sure this tuple never checked before
         temp_list = list (atuple)
         temp_list.append(num)
         temp_list.sort()
         current_tuple = tuple (temp_list)
         if current_tuple not in history:
             history.add(current_tuple)
         else :
             continue
         # count the frequent via basket_set
         # find the source of num:source[num] is a set
         temp_basket_set = set ()
         for aset in basket_set[atuple]:
             if num in baskets[aset]:
                 temp_basket_set.add(aset)
         if len (temp_basket_set) > threshold:
             result.append(current_tuple)
             new_basket_set[current_tuple] = temp_basket_set
             print ( "one answer:" + str (current_tuple))

這里還要注意的是,(k+1)-tuple可能之前就處理過,因此這里用了一個history詞典來記錄,若曾經處理過,則直接跳過。

執行速度從原來不知道少個小時變成了30s。咋一看用了dict增加了內存使用,但是由於沒有遍歷所有的baskets,其實內存占用並不多。至此,這個題目我算是完整的解決了。

反思

在這次作業中,我耗費了大量時間,錯過了deadline,存在以下問題:

  1. 小看了這次assignment,在沒用過python的情況下,又沒有正確的分析算法的時間復雜度,導致了各種各樣的小問題
  2. 沒有及時回顧課上內容,直接開始寫代碼

總而言之,自己過於自信,在自己不熟悉的情況下沒有敬畏之心,在自己沒有構思好整體思路時直接處理細節,導致我在處理過程中跟無頭蒼蠅一樣。

通過這次作業,對python比較熟悉了,不得不說,python的api設計的很符合人的直覺,set的運算也非常好用。







免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM