目录
学习完机器学习实战的FP-growth,简单的做个笔记。文中部分描述属于个人消化后的理解,仅供参考。
本篇综合了先前的文章,如有不理解,可参考:
所有代码和数据可以访问 我的 github
如果这篇文章对你有一点小小的帮助,请给个关注喔~我会非常开心的~
0. 前言
从大规模的数据集中,寻找不同特征或者物品之间的隐含关系,称为关联分析(association analysis),或者关联规则学习(association rule learning)。
在 Apriori 算法中,寻找频繁项集,需要对每一个可能的频繁项扫描一遍数据集计算支持度,计算量庞大。
在 FP-growth 算法中,寻找频繁项集,只需要扫描两遍数据集,将数据存储在FP树的结构上,然后在FP树上挖掘频繁项集。
- 优点:速度一般要快于 Apriori。
- 缺点:实现比较困难,在某些数据集上性能会下降。
- 适用数据类型:标称型数据。
例如在下述例子中(图源:机器学习实战),右侧是一颗FP树:
事务ID | 事务中的元素项 |
---|---|
001 | r, z, h, j, p |
002 | z, y, x, w, v, u, t, s |
003 | z |
004 | r, x, n, o, s |
005 | y, r, x, z, q, t, p |
006 | y, z, x, e, q, s, t, m |
FP代表频繁模式(Frequent Pattern),一个元素项可以在一颗FP树上出现多次。
树节点上给出了当前节点的路径在数据集中的出现次数,例如{z:5}表示元素{z}在数据集中出现了5次;{y:3}表示路径{y, x, z}在数据集中出现了3次;{s:2}表示路径{s, y, x, z}在数据集中出现了2次。
左侧为头指针表,给出了每个元素在数据集中出现的次数,并由链表通过节点链接(node link)依次链接每个元素。部分元素因为不满足最小支持度的要求,所以不储存在FP树中。
在 FP-growth 算法中,同样采用了 Apriori 算法的思想,如果某个项是非频繁的,那么这个项的所有超集也是非频繁的。
1. 构建FP树
构建FP树的过程只需要扫描两遍数据集。
第一遍扫描,计算每个单个元素的频率,并根据最小支持度,滤除不满足的元素。
第二遍扫描,首先对数据集进行处理,每一条数据按照元素的绝对出现频率排序,并滤除不满足最小支持度的元素。
例如根据上述的头指针表,元素排序为{z:5, x:4, y:3, s:3, r:3, t:3},所以处理后的数据为:
事务ID | 事务中的元素项 | 过滤及排序后的元素 |
---|---|---|
001 | r, z, h, j, p | z, r |
002 | z, y, x, w, v, u, t, s | z, x, y, s, t |
003 | z | z |
004 | r, x, n, o, s | x, s, r |
005 | y, r, x, z, q, t, p |
z, x, y, r, t |
006 | y, z, x, e, q, s, t, m | z, x, y, s, t |
处理后,遍历数据集,将每一条数据插入FP树中,从根节点开始递归添加路径,存在则将数值增加,不存在则创建新的节点。
例如下图所示(图源:机器学习实战),① 根节点不存在子节点{z},所以创建新的子节点{z},递归节点{z},因不存在子节点{r},所以创建新的子节点{r},② 根节点存在子节点{z},所以数值增加,递归节点{z},因不存在子节点{x},所以创建新的子节点{x},递归节点{x},......,如此递归。
2. 从FP树中挖掘频繁项集
一个元素的条件模式基(conditional pattern base),是这个元素所有前缀路径(prefix path)的集合。
前缀路径(prefix path),是当前元素到根节点之间的路径(不包括当前元素和根节点)。
例如下图所示(图源:机器学习实战),{r}的条件模式基是{{z}{z, x, y}{x, s}}:
从FP树挖掘频繁项集的过程可描述为:
- 对于头指针表中的每一个元素,获取其条件模式基
- 根据条件模式基,构建条件FP树(即,将条件模式基当作新的数据集,按照建树的流程,重新构建一棵FP树)
- 继续对于条件FP树的头指针表中的每一个元素,获取其条件模式基
- 继续根据条件模式基,构建条件FP树
- 如此递归过程,直到无法构建出FP树为止
记录频繁项集的过程在创建一棵新的FP树时记录,伪代码如下表示:
关于此处的理解:首先构建了一棵FP树,此时FP树中的单个元素均满足最小支持度(假设有{a}{b}{c}{d}{e}5个元素),遍历其中的每一个元素(假设此时遍历{a}),先将元素{a}加入总的频繁项集,再寻找元素{a}的条件模式基(假设有{c, b}{b, d}{b, c, e, d}),根据这些前缀路径递归构建一棵条件FP树(若这棵树能够构建的起来,说明树中的单个元素也是满足最小支持度的,假设条件FP树中有{b}{c}{d}3个元素,{e}不满足最小支持度),说明{b}{c}{d}这三个元素满足最小支持度,遍历其中的每一个元素(假设此时遍历{b}),复制上一层递归的频繁项集(即,{a}),将当前遍历元素{b}加入复制的频繁项集中(即,构成{a, b}),然后再将{a, b}加入总的频繁项集,再在条件FP树中寻找元素{b}的条件模式基,继续递归构建。因为上一层递归中的频繁项集{a}是一定满足最小支持度的,由这个元素{a}搜寻得到的条件模式基,一定是在数据集中跟{a}有组合的,若能据此构建一棵条件FP树,说明这棵树中的元素{b}{c}{d}也一定满足最小支持度,因这元素{b}与{a}在原始数据集中有组合,且这元素{b}与上一层递归频繁项集{a}均满足最小支持度,所以这元素{b}和{a}的组合{a, b}一定满足最小支持度,且存在在原始数据集中,所以加入总的频繁项集。
3. 实战案例
以下将展示书中案例的代码段,所有代码和数据可以在github中下载:
3.1. FP-growth寻找频繁项集
# coding:utf-8
from numpy import *
"""
FP-growth寻找频繁项集
"""
# 加载数据集
def loadSimpDat():
simpDat = [['r', 'z', 'h', 'j', 'p'],
['z', 'y', 'x', 'w', 'v', 'u', 't', 's'],
['z'],
['r', 'x', 'n', 'o', 's'],
['y', 'r', 'x', 'z', 'q', 't', 'p'],
['y', 'z', 'x', 'e', 'q', 's', 't', 'm']]
return simpDat
# 将数据集转换为set类型
def createInitSet(dataSet):
retDict = {}
for trans in dataSet:
retDict[frozenset(trans)] = 1
return retDict
# 树节点
class treeNode:
# name: 节点名称
# count: 出现次数
# nodeLink: 节点链接
# parent: 父节点
# children: 子节点集
def __init__(self, nameValue, numOccur, parentNode):
self.name = nameValue
self.count = numOccur
self.nodeLink = None
self.parent = parentNode
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)
# 创建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 list(headerTable.keys()):
if headerTable[k] < minSup:
del (headerTable[k])
# 频繁项集
# freqItemSet: {'p', 'v', 'u', 'q', ...}
freqItemSet = set(headerTable.keys())
# 无频繁项就返回
if len(freqItemSet) == 0:
return None, None
# 扩展头指针表
# 添加指向每种类型第一个元素的指针(节点链接)
# headerTable: {'j': [1, None], 'p': [2, None], 'r': [3, None], ...}
for k in headerTable:
headerTable[k] = [headerTable[k], None]
# 创建根节点
retTree = treeNode('Null Set', 1, None)
# 第二次遍历数据集
# 构建FP树
for tranSet, count in dataSet.items():
# tranSet: frozenset({'h', 'p', 'z', 'j', 'r'})
# count: 1
localD = {}
# 如果单个元素是频繁项,则加入localD列表
for item in tranSet:
if item in freqItemSet:
localD[item] = headerTable[item][0]
# localD: {'r': 3, 'j': 1, 'z': 5, 'h': 1, 'p': 2}
if len(localD) > 0:
# 排序
orderedItems = [v[0] for v in sorted(localD.items(), key=lambda p: p[1], reverse=True)]
# 更新FP树
updateTree(orderedItems, retTree, headerTable, count)
return retTree, headerTable
# 更新FP树函数
def updateTree(items, inTree, headerTable, count):
# 判断排序后列表的第一个元素是否已经是根节点的子节点
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]])
# 列表元素长度大于1
# 递归调用更新FP树函数
if len(items) > 1:
updateTree(items[1::], inTree.children[items[0]], headerTable, count)
# 更新头指针表的节点链接的函数
def updateHeader(nodeToTest, targetNode):
# 将元素放在指针链表的最后
while (nodeToTest.nodeLink != None):
nodeToTest = nodeToTest.nodeLink
nodeToTest.nodeLink = targetNode
# 寻找节点basePat的所有前缀路径
# treeNode: 头节点表的basePat的指针指向元素
def findPrefixPath(basePat, treeNode):
condPats = {}
# 有指向的元素
while treeNode != None:
prefixPath = []
# 回溯父节点,寻找前缀路径
# prefixPath: ['r', 't', 'x', 'z']
ascendTree(treeNode, prefixPath)
# 路径长度大于1,不是单个元素
if len(prefixPath) > 1:
# 添加进condPats,记录路径的出现次数
condPats[frozenset(prefixPath[1:])] = treeNode.count
# 继续寻找basePat为结尾的前缀路径
treeNode = treeNode.nodeLink
# condPats: {frozenset({'z'}): 1, frozenset({'s', 'x'}): 1, frozenset({'x', 'z'}): 1}
return condPats
# 单个节点回溯,寻找前缀路径
def ascendTree(leafNode, prefixPath):
if leafNode.parent != None:
prefixPath.append(leafNode.name)
ascendTree(leafNode.parent, prefixPath)
# 根据FP树寻找频繁项集
def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
# 头指针表排序
# bigL: ['h', 'j', 'u', 'v', 'w',...]
bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: p[1][0])]
for basePat in bigL:
newFreqSet = preFix.copy()
newFreqSet.add(basePat)
print('finalFrequent Item: ', newFreqSet)
freqItemList.append(newFreqSet)
# 以basePat为节点的所有前缀路径
# condPattBases: {frozenset({'z', 'r', 'p'}): 1, ...}
condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
print('condPattBases :', basePat, condPattBases)
# 以当前元素的所有前缀路径,创建条件FP树
myCondTree, myHead = createTree(condPattBases, minSup)
print('head from conditional tree: ', myHead)
# 根据条件FP树和条件头指针表,递归创建下一个条件FP树
if myHead != None:
print('conditional tree for: ', newFreqSet)
myCondTree.disp(1)
mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)
if __name__ == '__main__':
# simpDat = loadSimpDat()
# initSet = createInitSet(simpDat)
# retTree, headerTable = createTree(initSet)
# freqItems = []
# mineTree(retTree, headerTable, 3, set([]), freqItems)
# print(freqItems)
parsedDat = [line.split() for line in open('kosarak.dat').readlines()]
initSet = createInitSet(parsedDat)
myFPtree, myHeaderTab = createTree(initSet, 100000)
myFreqList = []
mineTree(myFPtree, myHeaderTab, 100000, set([]), myFreqList)
print(myFreqList)
如果这篇文章对你有一点小小的帮助,请给个关注喔~我会非常开心的~