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的运算也非常好用。






posted @ 2015-10-01 12:59  一路绝尘  阅读(3016)  评论(0编辑  收藏  举报