最近在学大数据这门课,课上讲到了一个关于尿布与啤酒的故事,说是发现在超市中尿布如果和啤酒放在一起能跟提高销量,原因是买尿布的多是父亲,这些人看到啤酒后就想买(这是什么逻辑)。当然,这个故事被证明是虚构的了信息来源。
不过这个故事引出了一个问题,如果在一群放在不同类目(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的运算也非常好用。