《Machine Learning in Action》Chapter 11 Apriori 学习笔记

普遍联系是为唯物辩证法的一个基本观点。在日常生活中,联系是普遍存在的,一些销售冠军经常提到的关于啤酒和尿布的经典案例,便是联系的一种具体体现。经过一个多月的学习,这几天进入到了无监督学习关联分析部分,与之前学习算法不同,这几天感觉到特别吃力(状态不佳,心浮气躁),本来想看完书后再找一些案例来充实一下这部分的内容,考虑了一下,还是先静下心来写篇博客总结一下吧,为接下来进行深入的学习打好基础。

先来了解一些什么是关联分析。

从大规模的数据中分析物品之间的隐含关系就是关联分析(Association Analysis),也可以叫做关联规则学习(Assocation Rule Learning)。要想找出物品之间的关系,首先需要找出物品的组合,然后再分析这些组合,确定物品之间的关系。但是这些操作非常的复杂,通过常规的算法很难达到技术要求,于是聪明的人类发明了Apriori算法,可以帮助我们快速的找到频繁项集合(Frequent Item Set),然后我们就可以从频繁项集中抽取出关联规则,这就是我们想要的东西。

频繁项集就是经常出现在一起的物品的集合,而关联规则则是按时这些物品之间可能存在很强的关系,下面用一个例子来说明:
在这里插入图片描述
这是来自某个商店的商品交易清单,第一列是交易的编号,第二列是商品。

先来学习几个单词吧.
soy milk 豆奶
lettuce 莴苣,生菜(不知道到底是莴苣还是生菜,如果是莴苣的话,外国人会炒着吃吗?)
diaper 尿布(似曾相识的赶脚)
wine 葡萄酒
chard 甜菜(不明所以)
orange juice 橙汁
好了。

从上面的表格中,我们可以直观的看出{soy milk, wine, diaper}是一个频繁项,并且可以找到诸如diaper->wine的关联规则,这意味着,如果有人买了尿布,那他很有可能还会买葡萄酒。

那么,怎么样的集合才能算是频繁的呢?怎么样的规则才能算是有关联呢?或者说是靠谱的呢?我们有支持度(support)和可信度(confidence)两个指标,分别用于频繁项集的发现和关联规则的生成。

某项集支持度被定义为在数据集中包含该项集的记录所占的比例。以上面的表格为例,项集{diaper}的支持度为4/5,项集{diaper, wine}的支持度为3/5。支持度是针对项集所言,因此可以定义一个最小的支持度,只保留一些比较频繁的项集。

假设一条关联规则为{diaper}->{wine},我们将这条关联规则的可信度表示为support({diaper, wine})/support({diaper}),那么我们可以计算出这条规则的可信度的0.75。同样,我们可以设置一个最小的可信度,只保留一些可信度比较高的关联规则。

假设我们有{0,1,2,3}四种物品,物品所有的组合关系是什么样的呢?如下图:
在这里插入图片描述
仅仅是4件物品,其组合关系看起来就比较复杂了,更不用说大的数据集了。但是仔细分析我们会发现如果{0,1}是频繁的,那么{0}和{1}自然也是频繁的。这就是著名的Apriori原理:如果某个项集是频繁的,那么它的子集肯定也是频繁的。我们可以换一种说法,如果某项集的子集不是频繁的,那么该项集注定不会频繁,那么我们就可以有小到大,不断排除一些不可能的组合,得到频繁项集。
在这里插入图片描述
上图中23是不频繁的,那么023,123,0123注定也是不频繁的,可以排除。

在使用Apriori之前,我们需要先创建几个辅助函数。
假设我们已经拥有了一个候选项集,如何得到频繁项集呢?如下:

对于数据集中的每条交易tran
	对于每一个候选项集set
		如果set是tran的子集,则增加set的计数
对于每个候选项集
	计算支持度
	如果对于最小支持度,则保留该项集
返回频繁项集和候选项集的支持度

下面给出代码实现(变量已重新命名,代码基于Python3,与示例代码不同)以及说明:

# 加载数据集
def load_data_arr():
    return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]


# 初始化候选项集,创建元素个数为1的候选项集
def create_set_0(data_arr):
    # 数组
    set_0 = []
    # 对每一条交易记录
    for transaction in data_arr:
    	# 对交易记录中的每件物品
        for item in transaction:
        	# 如果该物品没有出现在候选项集中,则添加
            if not [item] in set_0:
                set_0.append([item])
    set_0.sort()
    # 返回一个冻结的集合,集合冻结后不能再添加或者删除元素
    return map(frozenset, set_0)


# 扫描交易记录集,计算每个候选项集的支持度,返回满足条件的频繁项集以及所有候选项集的支持度
def scan_data_set(data_set_list, set_k, min_support):
    set_k_list = list(set_k)
    # 候选项集计数
    items_cnt = {}
    for transaction in data_set_list:
        for items in set_k_list:
            if items.issubset(transaction):
                if items not in items_cnt:
                    items_cnt[items] = 1
                else:
                    items_cnt[items] += 1
    num_transaction = len(data_set_list)
    # 数组
    return_set_k = []
    # 字典
    support_data = {}
    for items in items_cnt.keys():
        # 计算每个候选项集的支持度
        support = items_cnt[items]/num_transaction
        # 判断项集的支持度是否满足最小支持度
        if support >= min_support:
            return_set_k.append(items)
        # 保存每个候选项集的支持度
        support_data[items] = support
    return return_set_k, support_data

运行代码:

# 加载数据
data_arr = load_data_arr()
# 创建项个数为1 的候选项集
set_0 = create_set_0(data_arr)
# 扫描项个数为1的候选项集,生成项个数为1 的频繁项集,以及所有候选项集的支持度
set_0_scanned, support_data = scan_data_set(list(map(set, data_arr)), set_0, 0.5)

结果输出为:

[frozenset({1}), frozenset({3}), frozenset({2}), frozenset({5})]
{frozenset({1}): 0.5, 
frozenset({3}): 0.75, 
frozenset({4}): 0.25, 
frozenset({2}): 0.75, 
frozenset({5}): 0.75}

由于这个例子比较简单,读者可以根据原始数据集进行计算验证。

到目前为止,我们已经得到项个数为1的频繁项集了,那么如何得到项个数为2,3…的频繁项集呢?这时候就可以使用Apriori算法了,我们基于如果一个项集的子集不是频繁的,那么该项集注定不是频繁的,逐步得到项个数更多的项集,直到所有的项集的都被处理完毕(频繁保留或者不频繁排除)。

Apriori算法的伪代码如下:

当还有未处理完的候选项集
	构建含有k+1个项的候选项集
	扫描该候选项集得到频繁项集以及支持度
	更新频繁项集以及支持度

“当还有未处理完的候选项集”,这句话可能不太好理解,下面给出代码实现,结合代码解释。

# 根据项个数为k+1的频繁项集set_k,生成项个数为k+2的候选项集set_k_plus
def apriori_gen(set_k, k):
    return_set_k_plus = []
    num_set_k = len(set_k)
    for i in range(num_set_k):
        for j in range(i+1, num_set_k):
            l1 = list(set_k[i])[:k]
            l2 = list(set_k[j])[:k]
            l1.sort()
            l2.sort()
            if l1 == l2:
                return_set_k_plus.append(set_k[i] | set_k[j])    #set union
    return return_set_k_plus

先来看apriori_gen(set_k, k)函数,该函数的作用是根据项个数为k+1的频繁项集set_k,生成项个数为k+2的候选项集set_k_plus,k值的设置比较拗口,之所以比较拗口,是为了调用它的函数考虑的,但是作用还是比较明朗的。

我们将频繁项集中的项集两两比较,由于他们的项个数都是相同的,因此我们只比较他们的前k个项,最后一个项不比较,如果前k个项是相同的,那么最后一项肯定不同(频繁项集不重复),那么我么可以考虑将这两个频繁项集合并,k+1+1,得到含有k+2个项的候选项集。

我们假设k=0,那么该函数的作用就是根据项个数为1的频繁项集set_k,生成项个数为2的候选项集set_k_plus。读者可以自己验证一下。

下面给出重量级Apriori算法的实现。

def apriori(data_arr, min_support=0.5):
	# 创建项个数为1的候选项集
    set_0 = create_set_0(data_arr)
    data_set_list = list(map(set, data_arr))
    # 计算项个数为1的频繁项集
    set_0_scanned, all_support = scan_data_set(data_set_list, set_0, min_support)
    # 将项个数为1的频繁项集放入到总的频繁项集中,下标为0
    s = [set_0_scanned]
    k = 0
    while len(s[k]) > 0:
    	# 根据项个数为k+1的频繁项集set_k,生成项个数为k+2的候选项集set_k_plus
        set_k_plus = apriori_gen(s[k], k)
        # 计算项个数为k+2的频繁项集set_k_plus_scanned
        set_k_plus_scanned, k_plus_support 
        	= scan_data_set(data_set_list, set_k_plus, min_support)
        # 更新
        all_support.update(k_plus_support)
        s.append(set_k_plus_scanned)
        k += 1
    return s, all_support

如果读者对while len(s[k]) > 0有疑问,可以试着自己模拟运行一下。

运行代码:

data_arr = load_data_arr()
s, all_support = apriori(data_arr, 0.5)

执行结果:

[
	[frozenset({1}), frozenset({3}), frozenset({2}), frozenset({5})], 
	[frozenset({1, 3}), frozenset({2, 3}), frozenset({3, 5}), frozenset({2, 5})], 
	[frozenset({2, 3, 5})], 
	[]
]

frozenset({1}) : 0.5
frozenset({3}) : 0.75
frozenset({4}) : 0.25
frozenset({2}) : 0.75
frozenset({5}) : 0.75
frozenset({1, 3}) : 0.5
frozenset({2, 3}) : 0.5
frozenset({3, 5}) : 0.5
frozenset({2, 5}) : 0.75
frozenset({1, 2}) : 0.25
frozenset({1, 5}) : 0.25
frozenset({2, 3, 5}) : 0.5

可以改变最小支持度,看频繁项集有什么变化。

到目前为止,我们已经得到频繁项集以及候选项集的支持度,那么就可以着手关联规则的发掘了。

从前面商店的购物清单的例子中我们可以发现,{diaper,wine}是一个频繁项集,那么就可能有两条关联规则,diaper->wine和wine->diaper,前面我们已经给出了关联规则可信度的计算公式,再加上我们已经求得了支持度,那么计算关联规则的可信度应该是一件比较简单的事情。

可以,我们现在面临一个问题,即如何从一个频繁项集中找出所有可能的关联规则呢?我们以频繁项集{0,1,2,3}为例,构造所有的关联规则,如下图。
在这里插入图片描述
哇,又是一个这么复杂的图,我们现在分析一下这张图,再总结计算方法。

注意看每一条规则的右侧,在第0层,右侧的项数是0(空集),在第1层,右侧的项数是1,在第2层,右侧的项数是2,在第三层,右侧的项数是3。这种规律是否似曾相识?是的,在前面,生成频繁项集的时候,也是由小到大,不断地合并而成的。关联规则的右侧也是满足这种规律的,那么我们可以从小到大,不断的合并关联规则的右侧,生成所有的关联规则。那关联规则的左侧怎么生成呢?通过频繁项集减去规则右侧即规则左侧。上述做法叫做分级法。

再注意看图中的阴影部分,如果012->3不成立,那么下面与之关联的3条规则也不成立。即如果一条规则不可信,那么由该规则的右侧合并(与其他可信规则的右侧合并)出的规则同样不可信。利用这个属性,我们可以大大减少需要测试的关联规则。

(原书写的是,如果一条规则不可信,那么该条规则的子集同样是不可信的。这个子集有些不好理解)

在给出代码之前,我们先分析一下生成关联规则的开始和结束。假设某频繁项集有m个项,那么规则右侧的项数应该是1~m-1,这就是程序开始结束条件。

下面给出代码:

# 计算规则的可信度
def calculate_conf(freq_item, H, support_data, big_rules_list, min_conf=0.5):
    pruned_H = []
    for consequence in H:
        # 通过支持度计算可信度
        # 规则的左侧通过作差求得
        conf = support_data[freq_item]/support_data[freq_item - consequence]
        if conf >= min_conf:
            big_rules_list.append((freq_item - consequence, consequence, conf))
            # 需要返回可信的规则的右侧,以便再次进行合并,同时抛弃不可信规则的右侧
            pruned_H.append(consequence)
    return pruned_H


# 从序列中提取规则
# H是规则的右侧
def rules_from_consequence(freq_item, H, support_data, big_rules_list, min_conf=0.5):
    m = len(H[0])
    # 原书代码中此处没有等号,是不对的,读者可以结合前面给出的结束条件,判断此处是否需要添加等号
    if len(freq_item) >= (m+1):
    	# 下面的两行代码,原书给出的顺序是错误的,应该先计算可信度,再合并
    	# 读者可以根据计算出的频繁项集和支持度手动计算一下,可以发现原执行顺序会漏掉一部分规则
        hmp1 = calculate_conf(freq_item, H, support_data, big_rules_list, min_conf)
        hmp1 = apriori_gen(hmp1, m - 1)
        # 下面这行代码使没有用的,因为合并右侧后,肯定是需要去提取规则并计算规则可信度的。
        # if len(hmp1) > 1:
        rules_from_consequence(freq_item, hmp1, support_data, big_rules_list, min_conf)


# 生成关联规则
def generate_rules(s, support_data, min_conf=0.5):
    big_rules_list = []
    for i in range(1, len(s)):
        for freq_set in s[i]:
            h1 = [frozenset([item]) for item in freq_set]
            if i > 1:
            	# 项数大于2,至少为3,可以组合项,提取规则
                rules_from_consequence(freq_set, h1, support_data, big_rules_list, min_conf)
            else:
                calculate_conf(freq_set, h1, support_data, big_rules_list, min_conf)
    return big_rules_list

执行程序:

L, supportData = apriori(load_data_arr())
bigRuleList = generate_rules(L, supportData, 0.5)

执行结果:

(frozenset({3}), frozenset({1}), 0.6666666666666666)
(frozenset({1}), frozenset({3}), 1.0)
(frozenset({3}), frozenset({2}), 0.6666666666666666)
(frozenset({2}), frozenset({3}), 0.6666666666666666)
(frozenset({5}), frozenset({3}), 0.6666666666666666)
(frozenset({3}), frozenset({5}), 0.6666666666666666)
(frozenset({5}), frozenset({2}), 1.0)
(frozenset({2}), frozenset({5}), 1.0)
(frozenset({3, 5}), frozenset({2}), 1.0)
(frozenset({2, 5}), frozenset({3}), 0.6666666666666666)
(frozenset({2, 3}), frozenset({5}), 1.0)
(frozenset({5}), frozenset({2, 3}), 0.6666666666666666)
(frozenset({3}), frozenset({2, 5}), 0.6666666666666666)
(frozenset({2}), frozenset({3, 5}), 0.6666666666666666)

此结果与书中给出的结果不一致,但是经过作者手动计算,此结果是正确的,读者可以自行验证。

至此,本文的主要内容就结束了,下面简单的回顾一下本文的主要内容。

首先是频繁项集关联规则,这是两个重要的主题;
其次是支持度可信度,这是两个重要的指标;
著名的Apriori算法,生成候选项集,计算支持度,得到频繁项集;
可信度的计算公式
生成关联规则,计算可信度,得到感兴趣的关联规则。

本文基于一个小小的数据集,后期会在一个较大的数据集上运行本文的算法,敬请期待!

文中若有不足之处,欢迎各位读者指正,谢谢!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值