码迷,mamicode.com
首页 > 其他好文 > 详细

机器学习实战之Apriori

时间:2016-04-22 20:02:21      阅读:236      评论:0      收藏:0      [点我收藏+]

标签:

1. 关联分析

1.1 定义

       关联分析是一种在大规模数据上寻找物品间隐含关系的一种任务.这种关系有2种形式:频繁项集和关联规则.

        (1) 频繁项集(frequent item sets): 经常出现在一起的物品的集合;

        (2) 关联规则(association rules): 暗示两种物品之间可能存在很强的关系.

1.2 量化关联分析是否成功的算法

       支持度和可信度是用来量化关联分析是否成功的方法.

       (1)支持度(support) : 一个项集的支持度被定义为数据集中包含该项集的记录所占的比例. 支持度针对项集而言,可以设置最小支持度,只保留满足最小支持度的项集.以下图中的清 单为例, {豆奶}的支持度为4/5;{豆奶,尿布}的支持度为3/5.

       (2) 可信度或置信度(confidence) : 针对关联规则定义的. 例如: 规则{尿布}->{葡萄酒}的可信度被定义为 "支持度{尿布,葡萄酒}/支持度{尿布}"  . 支持度{尿布,葡萄酒}为3/5 , 支持度{尿布} 为4/5, 即这条规则可信度为3/4. 这意味着对于包含"尿布"的所有记录,我们的规则对其中75%的记录都适用.

技术分享

2. Apriori

2.1 问题: 假设一家商店里只有4种商品:0,1,2,3. 下图显示了所有可能被购买的商品组合:

技术分享

对于单个项集的支持度, 通过遍历每条记录并检查该记录是否包含该项集来计算.但是对于包含N种物品的数据集共有技术分享中项集组合,重复计算上述过程是不现实的.

2.2 Apriori原理

    Apriori原理能够减少计算量.

    其内容是: 若某个项集是频繁的, 那么它的子集也是频繁的; 则其逆否命题也是正确的,即 若一个项集是非频繁的, 则它的所有超集也是非频繁的.

    举例说明: 已知阴影项集{2,3}是非频繁的。利用这个知识,我们就知道项集{0,2,3},{1,2,3}以及{0,1,2,3}也是非频繁的。也就是说,一旦计算出了{2,3}的支持度,知道它是非频繁的后,就可以紧接着排除{0,2,3}、{1,2,3}和{0,1,2,3}。使用该原理可以避免项集数目的指数增长,从而在合理时间内计算出频繁项集.

技术分享

2.3 实现Apriori算法来发现频繁项集

(1) Apriori算法是发现频繁项集的一种方法. 其两个输入参数分别是数据集和最小支持度.

(2) Apriori算法流程:

  • 首先生成所有单个物品的项集列表; 然后扫描交易记录查看哪些项集满足最小支持度要求,去掉不满足最小支持度的项集.
  • 对剩下来的集合进行组合以生成包含两个元素的项集; 然后扫描交易记录查看哪些项集满足最小支持度要求,去掉不满足最小支持度的项集.
  • 上述过程重复直到所有项集都被去掉

(3) Apriori算法生成频繁项集的伪代码

对数据集中的每条交易记录tran:

          对每个候选项集can:

                       检查交易记录tran是否包含候选项集can:

                       若包含,则增加can的计数;

对每个候选项集:

         若该候选项集的支持度不低于最小支持度, 则保留; 反之, 则去掉.

返回所有频繁项集列表

其具体实现代码如下:

<span style="font-size:14px;"><span style="font-size:18px;">#c1是所有单个物品的集合
def createC1(dataSet):
	c1 = []
	for transaction in dataSet:
		for item in transaction:
			if not [item] in c1:
				c1.append([item])
	c1.sort()
	return map(frozenset,c1)  #frozenset是不能改变的集合,用户不能修改

#由ck得到lk的过程,即从ck选出支持度不小于指定值的项构成lk
def scanD(data,ck,minSupport):
	#统计每个候选项集及其对应的出现次数	
	ssCnt = {}
	for transaction in data:
		for can in ck:
			if can.issubset(transaction):
				if not ssCnt.has_key(can):
					ssCnt[can] = 1
				else:
					ssCnt[can] += 1

	#去掉不满足最小支持度的候选项集
	numItems = float(len(data))
	retList = [] #存储满足支持度不低于最小值的项集
	supportData = {} #存储所有项集及其支持度
	for key in ssCnt:
		support = ssCnt[key]/numItems
		if support >= minSupport:
			retList.insert(0,key)
		supportData[key] = support
	return retList,supportData #返回的retlist是ck中满足最小支持度项集的集合</span></span>
(4) Apriori完整的实现

整个Apriori算法的伪代码如下:

当集合中项的个数大于0时:

           构建一个k个项组成的候选项集的列表(k从1开始)

           遍历数据集来筛选并保留候选项集列表中满足最小支持度的项集

           构建由k+1个项组成的候选项集的列表

以下是实现代码:

<span style="font-size:14px;"><span style="font-size:18px;">#由频繁集lk和项集个数生成候选集ck,其中lk中每一项中含有元素k-1个,生成的ck中每一项中含有元素k个
def aprioriGen(lk,k):
	retList = []
	lenlk = len(lk)
	#retList可能会为空
	for i in range(lenlk):
		for j in range(i+1,lenlk): 
		#只有在两项中前k-2个元素一样时,将这两个集合合并(这样合并得到的大小就为k)
						
			l1 = list(lk[i])[:k-2]
			l2 = list(lk[j])[:k-2]
			l1.sort(); l2.sort()			
			if l1 == l2:
				retList.append(lk[i] | lk[j])
	return retList</span></span>
*** 在候选项集生成过程中, 只有在前k-2个项相同时才合并, 原始频繁项集中每一项含有k-1个元素, 为了合成得到每一项大小是k的候选项集列表,只有在前k-2项相同时,最后一项不同时,才有可能得到频繁项集.注意这里不是两两合并, 因为限制了候选项集的大小.
<span style="font-size:14px;"><span style="font-size:18px;">def apriori(dataSet,minSupport = 0.5):
	#单独产生c1,l1	
	c1 = createC1(dataSet)
	data = map(set,dataSet)
	l1,supportData = scanD(data,c1,minSupport) #得到l1,大小为1的频繁项集集合
	
	#由l1得到c2,c2得到l2,依次循环
	l = [l1]
	k = 2
	#当lk为空时,退出循环
	while (len(l[k-2]) > 0):
		ck = aprioriGen(l[k-2],k)
		lk,supportK = scanD(data,ck,minSupport)
		supportData.update(supportK)
		l.append(lk)
		k += 1
	return l,supportData #所有的频繁集及其支持度</span></span>
3. 从频繁项集中挖掘相关规则

3.1 关联规则的量化指标是可信度.

       一条规则P?H的可信度定义为support(P | H)/support(P),其中“|”表示P和H的并集。可见可信度的计算是基于项集的支持度的。
       下图给出一个频繁项集{0,1,2,3}产生的关联规则, 其中阴影区域给出的是低可信度的规则.可以发现如果{0,1,2}?{3}是一条低可信度规则,那么所有其他以3作为后件(箭头右部包含3)的规则均为低可信度的.因为{3}的支持度肯定比包含3的项集的支持度要高, 即分子没变,但是分母增加, 则得到的可信度就降低了.

          结论:若某条规则不满足最小可信度要求, 则其规则的所有子集也不会满足最小可信度要求. 以图4为例,假设规则{0,1,2} ? {3}并不满足最小可信度要求,那么就知道任何左部为{0,1,2}子集的规则也不会满足最小可信度要求。

         利用此结论可以大大减少需要测试的规则数目.

技术分享

3.2 关联规则生成函数

      函数generateRules()是主函数,调用另2个函数.其他两个函数是rulesFromConseq()和calcConf(),分别用于生成候选规则集合以及对规则进行评估(计算可信度)

(1) 函数generateRules()有3个参数:频繁项集列表L、包含那些频繁项集支持数据的字典supportData、最小可信度阈值minConf。函数最后要生成一个包含可信度的规则列表bigRuleList,后面可以基于可信度对它们进行排序。L和supportData正好为函数apriori()的输出。该函数遍历L中的每一个频繁项集,并对每个频繁项集构建只包含单个元素集合的列表H1。代码中的i指示当前遍历的频繁项集包含的元素个数,freqSet为当前遍历的频繁项集(回忆L的组织结构是先把具有相同元素个数的频繁项集组织成列表,再将各个列表组成一个大列表,所以为遍历L中的频繁项集,需要使用两层for循环)。

<span style="font-size:14px;"><span style="font-size:18px;">#关联规则生成函数
def generateRules(L,supportData,minConf=0.7):
	#存储所有关联规则	
	bigRuleList = []

	for i in range(1,len(L)): #一条关联规则至少需要2个元素,故下标从1开始
		for freqSet in L[i]: #frequent为大小为i的项集
			H1 = [] #对每个频繁项集构了,建只包含单个元素的集合,即可以出现在规则右边
			for item in freqSet:
				H1.append(frozenset([item]))
			if (i > 1):
				H1 = calcConf(freqSet,H1,supportData,bigRuleList,minConf)
				#包含三个及以上元素的频繁集
				rulesFromConseq(freqSet,H1,supportData,bigRuleList,minConf)
			else:
				#包含两个元素的频繁集
				calcConf(freqSet,H1,supportData,bigRuleList,minConf)

	return bigRuleList</span></span>
(2)calcConf函数: 计算规则的可信度,并过滤出满足最小可信度要求的规则. 返回值prunedH保存规则列表的右部,这个值将在下一个函数rulesFromConseq()中用到。

函数实现代码如下:

<span style="font-size:14px;"><span style="font-size:18px;">#处理包含两个元素的频繁集,计算规则的可信度,并过滤出满足最小可信度要求的规则
def calcConf(freqSet,H,supportData,brl,minConf=0.7):
	#保存关联规则的列表
	prunedH = []

	for conseq in H:
		#对一个频繁集合,其分母都是一样的,supprot{1,2}/support{1},supprot{1,2}/support{2}
		conf = supportData[freqSet]/supportData[freqSet-conseq]
		if conf >= minConf:
			print freqSet - conseq, '---->',conseq,'  conf:',conf
			brl.append((freqSet-conseq,conseq,conf))
			prunedH.append(conseq)
	return prunedH
</span></span>

(2) rulesFromConseq()函数; 根据当前候选规则集H生成下一层候选规则集 ;

       H为规则右边可能出现的频繁项集, H中元素的长度逐渐增大; 比如 原频繁集包含3个元素,则出现在规则右边的元素可能有1个,2个; 假设规则{0,1,2} ? {3}并不满足最小可信度要求,那么就知道任何左部为{0,1,2}子集的规则也不会满足最小可信度要求,即不考虑{0,1} ? {2,3}。因此我们先从右边只有一个元素时开始,只有存在可以出现在规则右边的元素,右边才有可能出现元素个数为2的情况.


代码如下:

<span style="font-size:14px;"><span style="font-size:18px;">#根据当前候选规则集H生成下一层候选规则集,H是可以出现在规则右边的元素列表
def rulesFromConseq(freqSet,H,supportData,brl,minConf=0.7):
	#可以出现在规则右边的元素个数[1]=1,[1,2]=2,从1个元素增加直到小于freqSet的总个数都行
	m = len(H[0])

	if (len(freqSet) > (m+1)):
		hmp1 = aprioriGen(H,m+1) #产生大小为m+1的频繁集列表,(1,2->3)->(1->2,3),生成下一层H
		hmp1 = calcConf(freqSet,hmp1,supportData,brl,minConf)
		if (len(hmp1) > 1):#规则右边的元素个数还可以增加
			rulesFromConseq(freqSet,hmp1,supportData,brl,minConf)</span></span>

4. FP-growth算法

(1) FP-growth是一种高效发现频繁集的方法;但其不能用于发现关联规则.

    与Apriori对比: FP-growth算法只需要对数据库扫描2次,Apriori算法要对每个潜在的频繁项集扫描一次数据集来判定其是否是频繁的, 因此FP-growth算法要比Apriori速度快.

(2) FP-growth算法性质

   将数据存储在称为FP树的紧凑数据结构中.FP代表频繁模式(frequent pattern).通过链接(link)来链接相似元素,下图是FP树的一个例子.

技术分享

  

      与搜索树不同的是,一个元素项可以在一棵FP树种出现多次。

      FP树会存储项集的出现频率,而每个项集会以路径的方式存储在树中。存在相似元素的集合会共享树的一部分。只有当集合之间完全不同时,树才会分叉。 树节点上给出集合中的单个元素及其在序列中的出现次数,路径会给出该序列的出现次数。

        相似项之间的链接称为节点链接(node link),用于快速发现相似项的位置。

         下面是用于生成上图FP树的原始数据:

 技术分享

   由于设置了最小支持度,故FP树中没有出现p和q.

(3) FP-growth算法流程

  • 构建FP树
  • 利用FP树来挖掘频繁项集

构建FP树,对原始数据集扫描2遍:

  • 第一遍: 对所有元素项的出现次数进行计数
  • 第二遍: 只考虑频繁元素

(4) 存储FP树的结构

设置树结构如下所示:

<span style="font-size:14px;">class treeNode:
    def __init__(self, nameValue, numOccur, parentNode):
        self.name = nameValue
        self.count = numOccur
        self.nodeLink = None
        self.parent = parentNode      #needs to be updated
        self.children = {} 
    
    def inc(self, numOccur):
        self.count += numOccur
        
    def disp(self, ind=1):
        print '  '*ind, self.name, ' ', self.count
        for child in self.children.values():
            child.disp(ind+1)</span>

每个树节点由五个数据项组成:

  • name:节点元素名称,在构造时初始化为给定值
  • count:出现次数,在构造时初始化为给定值
  • nodeLink:指向下一个相似节点的指针,默认为None
  • parent:指向父节点的指针,在构造时初始化为给定值
  • children:指向子节点的字典,以子节点的元素名称为键,指向子节点的指针为值,初始化为空字典

成员函数:

  • inc():增加节点的出现次数值
  • disp():输出节点和子节点的FP树结构

(5) 构建FP树

准备:  需要一个头指针表的数据结构, 用于记录各个元素项的总出现次数和一个指针用于指向FP树中该元素项的地一个节点,这样每个元素项构成一个单链表.如下图所示: 此处使用python的字典作为数据结构来保存头指针表.以元素名称为键, 保存出现的总次数以及一个指向第一个元素的指针.

技术分享

 元素项排序: FP树会合并相同的频繁项集(或是相同的部分), 因此为判断两个项集的相似程度需要对项集中的元素进行排序. 排序基于元素项的总的出现次数来进行.

在第二次遍历数据集时, 依次读入每个项集, 首先去掉其中不满足最小支持度的元素项, 然后再对剩余的元素进行排序.

以下是对示例数据集进行过滤和排序后的结果:

技术分享

构建FP树

在进行准备工作后, 开始构建FP树. 从空集开始, 将频繁项集一个一个的添加到树中.若树中已存在现有元素,则增加现有元素的值; 若不存在, 则向树增加一个分支.下面展示了添加前2条事务的过程:

技术分享


下面是构建FP树的伪代码:

输入:数据集、最小支持度
输出:FP树根节点、头指针表
1. 遍历数据集,统计各单个元素项出现次数,创建头指针表
2. 移除头指针表中不满足最小支持度的元素项
3. 第二次遍历数据集,创建FP树。对每个数据集中的项集:
    3.1 初始化空FP树
    3.2 对每个项集进行过滤和重排序
    3.3 使用这个项集更新FP树,从FP树的根节点开始:
        3.3.1 如果当前项集的第一个元素项存在于FP树当前节点的子节点中,则更新这个子节点的计数值
        3.3.2 否则,创建新的子节点,同时更新头指针表
        3.3.3 对当前项集的其余元素项和当前元素项的对应子节点递归3.3的过程

具体实现代码如下:

<span style="font-size:14px;">#构建fp树
def createTree(dataset,minsup=1):
	#第一次遍历数据集,创建头指针表,存储每个元素项及其出现频度
	headerTable = {}
	for trans in dataset:
		for item in trans:
			headerTable[item] = headerTable.get(item,0) + dataset[trans]
	#删除出现次数不满足最低支持度的元素
	for k in headerTable.keys():
		if headerTable[k] < minsup:
			del(headerTable[k])
 	#元素集合
	freqItemSet = set(headerTable.keys())
	if len(freqItemSet) == 0:
		return None,None

	#扩展头指针表,用于存放计数值和指向每个元素的指针
	for k in headerTable:
		headerTable[k] = [headerTable[k], None]

	#创建根节点
	retTree = treeNode('null set', 1, None)

	#第二次遍历数据集,创建fp树
	for transet, count in dataset.items():
		localD = {} #针对一个项集transet,记录其中每个元素项的全局频率,用于排序
		for item in transet:
			if item in freqItemSet:
				localD[item] = headerTable[item][0]

		if len(localD) > 0:
			#按出现频率对一个项集内的元素排序
			orderedItem = [v[0] for v in sorted(localD.items(), key = lambda p:p[1], reverse=True)]
			#更新fp树			
			updateTree(orderedItem,retTree,headerTable,count)

	return retTree,headerTable
			

#更新树
def updateTree(items,inTree,headerTable,count):
	#该元素是当前intree节点的孩子节点时
	if items[0] in inTree.children:
		inTree.children[items[0]].inc(count)
	else :
		#创建一个新节点
		inTree.children[items[0]] = treeNode(items[0],count,inTree)
		#更新头指针表或前一个相似元素项节点的指针指向该节点
		if headerTable[items[0]][1] == None:
			headerTable[items[0]][1] = inTree.children[items[0]]
		else:
			updateHeader(headerTable[items[0]][1],inTree.children[items[0]])

	if len(items) > 1:
		#继续对此项集剩下来的元素迭代调用updatetree函数,此时父节点变了
		updateTree(items[1::],inTree.children[items[0]],headerTable,count)	

#更新头指针表中的指针,当相同元素出现时
def updateHeader(oldNode,targetNode):
	while (oldNode.nodeLink != None):
		oldNode = oldNode.nodeLink
	oldNode.nodeLink = targetNode	</span>

5. 从一棵FP树中挖掘频繁项集

挖掘思路: 从单元素项集合开始,然后在此基础续航逐步构建更大的集合.整个过程之利用FP树,不需要原始数据集.

从FP树抽取频繁项集的三个基本步骤如下:

  • 从FP树中获得条件模式基
  • 利用条件模式基,构建一个条件FP树
  • 迭代重复步骤(1)(2),直到树包含一个元素项为止.

5.1 抽取条件模式基

从保存在头指针表中的单个频繁元素项开始, 对每一个元素项,获得其对应的条件模式基(conditional pattern base). 条件模式基是以所查找元素项作为结尾的路径集合, 每一条路径都是一条前缀路径.即一条前缀路径是介于所查找元素项与树根节点之间的所有内容.

举例说明:

技术分享

其实现代码如下:

<span style="font-size:14px;">#得到某个元素的前缀路径   fp.findPrefixPath('x',header['x'][1])
def findPrefixPath(basePat, treeNode):
	conPat = {}
	while treeNode != None:
		prefixPath = []
		ascendTree(treeNode,prefixPath)
		if len(prefixPath) > 1:
			conPat[frozenset(prefixPath[1:])] = treeNode.count
		treeNode = treeNode.nodeLink
	return conPat #键为前缀路径, 值为其出现次数

#迭代上溯直到到达根节点
def ascendTree(leafNode,prefixPath):
	if leafNode.parent != None:
		prefixPath.append(leafNode.name)
		ascendTree(leafNode.parent,prefixPath)</span>
5.2 创建条件FP树

对每一个频繁项,都要创建一棵条件FP树.

可以使用刚才发现的条件模式基作为输入数据,并通过相同的建树代码来构建这些树。例如,对于r,即以“{x, s}: 1, {z, x, y}: 1, {z}: 1”为输入,调用函数createTree()获得r的条件FP树;对于t,输入是对应的条件模式基“{z, x, y, s}: 2, {z, x, y, r}: 1”。

代码(直接调用createTree()函数):

1
2
condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
myCondTree, myHead = createTree(condPattBases, minSup)

举例说明,如下是t的条件FP树构建过程: 最初树以空集作为根节点.接下来, 原始的集合{y,x,s,z}中的集合{y,x,z}被添加进来, 因为不满足最小支持度要求, 字符s和r没有加入.单独上,s和r都是频繁的,但是{t,s}/{t,r}是不频繁的.

技术分享

5.3 递归查找频繁项集

递归过程如下所示:

输入:我们有当前数据集的FP树(inTree,headerTable)tree,header = fp.createTree(initset,3)
1. 初始化一个空列表preFix表示前缀,从只有一个元素项开始,即header中的每一项.此时前缀为空
2. 初始化一个空列表freqItemList接收生成的频繁项集(作为输出)
3. 对headerTable中的每个元素basePat(按计数值由小到大),递归:
        3.1 记basePat + preFix为当前频繁项集newFreqSet
        3.2 将newFreqSet添加到freqItemList中
        3.3 计算t的条件FP树(myCondTree、myHead)
        3.4 当条件FP树不为空时,继续下一步;否则退出递归
        3.4 以myCondTree、myHead为新的输入,以newFreqSet为新的preFix,外加freqItemList,递归这个过程


具体实现代码如下:

<span style="font-size:14px;">#递归查找频繁项集
def mineTree(inTree,headerTable,minSup,prefix,freqItemList):
	#对头指针表中的元素项按照其出现频率进行排序,递增
	bigL = [v[0] for v in sorted(headerTable.items(),key=lambda p:p[1])]

	for basepat in bigL:
		newFreqSet = prefix.copy()
		newFreqSet.add(basepat)
		freqItemList.append(newFreqSet)

		conpat = findPrefixPath(basepat,headerTable[basepat][1]) #获得basepat的条件模式基
		mytree,myhead = createTree(conpat,minSup)  #构建basepat的条件FP树

		if myhead != None:
			mineTree(mytree,myhead,minSup,newFreqSet,freqItemList)</span>

输入参数:

  • inTree和headerTable是由createTree()函数生成的数据集的FP树
  • minSup表示最小支持度
  • preFix请传入一个空集合(set([])),将在函数中用于保存当前前缀
  • freqItemList请传入一个空列表([]),将用来储存生成的频繁项集
技术分享

5.4 封装

完整的FP-growth算法:

<span style="font-size:14px;">
def fpGrowth(dataSet, minSup=3):
    initSet = createInitSet(dataSet)
    myFPtree, myHeaderTab = createTree(initSet, minSup)
    freqItems = []
    mineTree(myFPtree, myHeaderTab, minSup, set([]), freqItems)
    return freqItems</span>

参考:

[1]  http://www.cnblogs.com/qwertWZ/p/4510857.html#_label2


机器学习实战之Apriori

标签:

原文地址:http://blog.csdn.net/sinat_17451213/article/details/51205446

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!