最近在學大數據這門課,課上講到了一個關於尿布與啤酒的故事,說是發現在超市中尿布如果和啤酒放在一起能跟提高銷量,原因是買尿布的多是父親,這些人看到啤酒后就想買(這是什么邏輯)。當然,這個故事被證明是虛構的了信息來源。
不過這個故事引出了一個問題,如果在一群放在不同類目(baskets)中的物品(items)中尋找成對(pair)的物品,且物品在不同類目中出現了至少threshold次,那么應該怎樣做是有效率(空間上)的呢?
最naive的方法對於N個items,需要的操作數是
假設
為了解決占用內存過大問題,引入了Aprior算法。
什么是Apriori 算法
先看下Wikipedia的說明。先驗算法(英語:Apriori algorithm)是關聯式規則中的經典算法之一。在關聯式規則中,一般對於給定的項目集合baskets(例如,零售交易集合,每個集合都列出的單個商品的購買信息),算法通常嘗試在項目集合中找出至少有threshold個相同的子集。先驗算法采用自底向上的處理方法,即頻繁子集每次只擴展一個對象(該步驟被稱為候選集產生),並且候選集由數據進行檢驗。當不再產生符合條件的擴展對象時,算法終止。
看起來好簡單的算法,很快就實現了,但是跑起來慢得要死...最開始的版本,跑了一個晚上也沒有跑完數據,優化后的依然不行,經過再次優化,最終優化版本30s跑完,可是我錯過了Assignment提交的deadline
算法過程
簡單講,Aprior算法就是利用了單調性:如果一個集合I,I中的物品都至少出現了threshold次,那么任意I的子集,不可能出現的次數少於threshold次(threshold在這里是指一個門限值)。
反過來講,如果一個物品i出現次數不到threshold,那么凡是包含了i的集合,都不可能出現超過threshold次。
一般的實現思路是:
- 讀取所有baskets中的數據,並在內存中記錄至少出現了s次的item
- 重新讀取baskets,並只記錄那些成對且至少出現了s次的item
這樣需要的內存就是最常出現的items的平方了
如果找的不是成對,而是n對物品(n-tuple)怎么辦?
一圖勝千言,
該流程迭代到
實踐
有10000個basket,同時有10000個數,第i個basket里放的都是能夠整除i的數,舉個例子:
解決這個問題,我們按照上方提到的算法,先構造k-tuple,將其通過過濾器,獲取符合條件的的k-tuple,然后再將k-tuple構造成(k+1)-tuple,再繼續迭代即可。
舉一個簡單的例子:
實際上,在生成(k+1)-tuple, 和過濾器這一步,有很多細節。如果剪枝剪得不充分,就會時間復雜度特別高,運行起來極其耗時。
根據這個算法,我們需要一個這樣的函數:construct_filter(baskets_set, last_result, length)
這個函數的作用是,將candidate items輸入,構造新的,長度為length的tuple,並對其過濾輸出。其中baskets_set是我們要用於檢查新tuple是否合格的源
第一個問題,如何構造(k+1)-tuple?
我的解決方法是,從
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)
|
一開始我采用的方法是遍歷整個basket,來檢查(k+1)-tuple出現的次數是否超過了threshold。
顯然,這樣的實現有個很大的問題,在過濾時要遍歷整個baskets,不必要的計算使得運行時間幾個小時都跑不完。根據Apriori,
反過來講,如果一個物品i出現次數不到threshold,那么凡是包含了i的集合,都不可能出現超過threshold次。
這里應該剪枝,不應該遍歷整個baskets,而應該遍歷
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))
|
反思
在這次作業中,我耗費了大量時間,錯過了deadline,存在以下問題:
- 小看了這次assignment,在沒用過python的情況下,又沒有正確的分析算法的時間復雜度,導致了各種各樣的小問題
- 沒有及時回顧課上內容,直接開始寫代碼
總而言之,自己過於自信,在自己不熟悉的情況下沒有敬畏之心,在自己沒有構思好整體思路時直接處理細節,導致我在處理過程中跟無頭蒼蠅一樣。
通過這次作業,對python比較熟悉了,不得不說,python的api設計的很符合人的直覺,set的運算也非常好用。