在《关联规则(一):Apriori算法和FP Tree算法原理总结》中介绍了Apriori算法和FP Tree算法,这两个算法都是挖掘频繁项集的。而今天我们要介绍的PrefixSpan算法也是关联算法,PrefixSpan 算法是裴健教授和韩家炜教授在 2004 年提出的序列模式算法。它是挖掘频繁序列模式的,因此要解决的问题目标与普通的关联算法稍有不同。
一、项集数据和序列数据
首先我们看看项集数据和序列数据有什么不同,如下图所示。
![CSDN图标](https://i-blog.csdnimg.cn/blog_migrate/167628e34d6cd5d467f214c611c593c5.png)
左边的数据集就是项集数据,在Apriori和FP Tree算法中我们也已经看到过了,每个项集数据由若干项组成,这些项没有时间上的先后关系。而右边的序列数据则不一样,它是由若干数据项集组成的序列。比如第一个序列<a(abc)(ac)d(cf)>,它由a,abc,ac,d,cf共5个项集数据组成,并且这些项集有时间上的先后关系。对于多于一个项的项集我们要加上括号,以便和其他的项集分开。同时由于项集内部是不区分先后顺序的,为了方便数据处理,我们一般将序列数据内所有的项集内部按字母顺序排序。
注:序列模式的序列是指项集是有相互顺序的,但项集内部是没有顺序的。
二、子序列与频繁序列
了解了序列数据的概念,我们再来看看什么是子序列。子序列和我们数学上的子集的概念很类似,也就是说,如果某个序列 A A A所有的项集在序列 B B B中的项集都可以找到,则 A A A就是 B B B的子序列。当然,如果用严格的数学描述,子序列是这样的:
对于序列 a 1 , a 2 , . . . a n a_1,a_2,...a_n a1,a2,...an和序列 b 1 , b 2 , . . . b m b_1,b_2,...b_m b1,b2,...bm,如果存在数字序列 1 ≤ j 1 ≤ j 2 ≤ . . . ≤ j n ≤ m 1 \leq j_1 \leq j_2 \leq ... \leq j_n \leq m 1≤j1≤j2≤...≤jn≤m, 满足 a 1 ⊆ b j 1 , a 2 ⊆ b j 2 . . . a n ⊆ b j n a_1 \subseteq b_{j_1}, a_2 \subseteq b_{j_2}...a_n \subseteq b_{j_n} a1⊆bj1,a2⊆bj2...an⊆bjn,则称 A A A是 B B B的子序列。当然反过来说, B B B就是 A A A的超序列。
而频繁序列则和我们的频繁项集很类似,也就是频繁出现的子序列。比如对于下图,支持度阈值定义为50%,也就是需要出现两次的子序列才是频繁序列。而子序列<(ab)c>是频繁序列,因为它是图中的第一条数据和第三条序列数据的子序列,对应的位置用蓝色标示。
![CSDN图标](https://i-blog.csdnimg.cn/blog_migrate/1a0aceaef1aa585067e8957d94222925.png)
三、PrefixSpan算法的一些概念
PrefixSpan算法的全称是Prefix-Projected Pattern Growth,即前缀投影的模式挖掘。里面有前缀和投影两个词。那么我们首先看看什么是PrefixSpan算法中的前缀prefix。
在PrefixSpan算法中的前缀prefix通俗意义讲就是序列数据前面部分的子序列。如果用严格的数学描述,前缀是这样的:对于序列 A = a 1 , a 2 , . . . a n A={a_1,a_2,...a_n} A=a1,a2,...an和序列 B = b 1 , b 2 , . . . b m B={b_1,b_2,...b_m} B=b1,b2,...bm, n ≤ m n \leq m n≤m,满足 a 1 = b 1 , a 2 = b 2 . . . a n − 1 = b n − 1 a_1 =b_1 , a_2 = b_2...a_{n-1} = b_{n-1} a1=b1,a2=b2...an−1=bn−1,而 a n ⊆ b n a_n \subseteq b_n an⊆bn,则称 A A A是 B B B的前缀。比如对于序列数据B=<a(abc)(ac)d(cf)>,而A=<a(abc)a>,则A是B的前缀。当然B的前缀不止一个,比如<a(ab)> 也是B的前缀。
看了前缀,我们再来看前缀投影,其实前缀投影这儿就是我们的后缀,有前缀就有后缀嘛。前缀加上后缀就可以构成一个我们的序列。下面给出前缀和后缀的例子。对于某一个前缀,序列里前缀后面剩下的子序列即为我们的后缀。如果前缀最后的项是项集的一部分,则用一个“_”来占位表示。
下面这个例子展示了序列<a(abc)(ac)d(cf)>的一些前缀和后缀,还是比较直观的。要注意的是,如果前缀的末尾不是一个完全的项集,则需要加一个占位符。
在PrefixSpan算法中,相同前缀对应的所有后缀的结合我们称为前缀对应的投影数据库。
![CSDN图标](https://i-blog.csdnimg.cn/blog_migrate/992eca2e6cdc901877fd88b521cb1889.png)
四、PrefixSpan算法思想
现在我们来看看PrefixSpan算法的思想,PrefixSpan算法的目标是挖掘出满足最小支持度的频繁序列。那么怎么去挖掘出所有满足要求的频繁序列呢。回忆Aprior算法,它是从频繁1项集出发,一步步的挖掘2项集,直到最大的K项集。PrefixSpan算法也类似,它从长度为1的前缀开始挖掘序列模式,搜索对应的投影数据库得到长度为1的前缀对应的频繁序列,然后递归的挖掘长度为2的前缀所对应的频繁序列,。。。以此类推,一直递归到不能挖掘到更长的前缀挖掘为止。
比如对应于我们第二节的例子,支持度阈值为50%。里面长度为1的前缀包括<a> ,<b> ,<c> ,<d> ,<e> ,<f> ,<g> ,我们需要对这7个前缀分别递归搜索找各个前缀对应的频繁序列。如下图所示,每个前缀对应的后缀也标出来了。由于g只在序列4出现,支持度计数只有1,因此无法继续挖掘。我们的长度为1的频繁序列为<a> ,<b> ,<c> ,<d> ,<e> ,<f>。去除所有序列中的g,即第4条记录变成<e(af)cbc>。
![CSDN图标](https://i-blog.csdnimg.cn/blog_migrate/5b19cf480af90a326ae21cd05f57d3a3.png)
现在我们开始挖掘频繁序列,分别从长度为1的前缀开始。这里我们以d为例子来递归挖掘,其他的节点递归挖掘方法和d一样。
方法如下图,首先我们对d的后缀进行计数,得到{a:1, b:2, c:3, d:0, e:1, f:1,_f:1}。注意f和_f是不一样的,因为前者是在和前缀d不同的项集,而后者是和前缀d同项集。由于此时a,d,e,f,_f都达不到支持度阈值,因此我们递归得到的前缀为d的2项频繁序列为<db> 和<dc> 。
接着我们分别递归db和dc为前缀所对应的投影序列。首先看db前缀,此时对应的投影后缀只有<_c(ae)>,此时_c,a,e支持度均达不到阈值,因此无法找到以db为前缀的频繁序列。现在我们来递归另外一个前缀dc。以dc为前缀的投影序列为<_f>, <(bc)(ae)>,< b>,此时我们进行支持度计数,结果为{b:2, a:1, c:1, e:1, _f:1},只有b满足支持度阈值,因此我们得到前缀为dc的三项频繁序列为< dcb>。
我们继续递归以< dcb>为前缀的频繁序列。由于前缀< dcb>对应的投影序列<(_c)ae>支持度全部不达标,因此不能产生4项频繁序列。至此以d为前缀的频繁序列挖掘结束,产生的频繁序列为< d>< db>< dc>< dcb>。
![CSDN图标](https://i-blog.csdnimg.cn/blog_migrate/39feb75d36b9f2f519949d46741d3101.png)
同样的方法可以得到其他以<a> ,<b> ,<c> ,<e> ,<f>为前缀的频繁序列。
在这里,我插入一个知识点,即封闭序列模式 closed sequential pattern 。定义为:如果一个序列模式不存在序列模式的支持度相同的超集,则该序列模式称为封闭序列模式。
举个简单的例子,考虑4个序列:
a b c d e
a b d
b e a
b c d e
设定minsup = 2。
b c 是一种频繁的序列模式,因为它出现在两个序列中(支持度2)。 b c 不是一个封闭的序列模式,因为它包含在一个更大的序列模式中 b c d中,因此 b c d有同样的支持度。然而 b c d也不是一个封闭的序列模式,因为它包含在一个更大的序列模式中 b c d e 且有同样的支持度。 b c d e 是一个封闭的序列模式,因为它没有包含在具有相同支持的任何其他序列模式中。
序列模式挖掘给出了序列挖掘很好的介绍。值得阅读!
五、PrefixSpan算法流程
下面我们对PrefixSpan算法的流程做一个归纳总结。
输入:序列数据集S和支持度阈值
α
\alpha
α
输出:所有满足支持度要求的频繁序列集
1)找出所有长度为1的前缀和对应的投影数据库
2)对长度为1的前缀进行计数,将支持度低于阈值 α \alpha α的前缀对应的项从数据集S删除,同时得到所有的频繁1项序列, i = 1 i=1 i=1.
3)对于每个长度为 i i i满足支持度要求的前缀进行递归挖掘:
a) 找出前缀所对应的投影数据库。如果投影数据库为空,则递归返回。
b) 统计对应投影数据库中各项的支持度计数。如果所有项的支持度计数都低于阈值 α \alpha α,则递归返回。
c) 将满足支持度计数的各个单项和当前的前缀进行合并,得到若干新的前缀。
d) 令 i = i + 1 i=i+1 i=i+1,前缀为合并单项后的各个前缀,分别递归执行第3步。
六、PrefixSpan算法小结
PrefixSpan算法由于不用产生候选序列,且投影数据库缩小的很快,内存消耗比较稳定,作频繁序列模式挖掘的时候效果很高。比起其他的序列挖掘算法比如GSP,FreeSpan有较大优势,因此是在生产环境常用的算法。
PrefixSpan运行时最大的消耗在递归的构造投影数据库。如果序列数据集较大,项数种类较多时,算法运行速度会有明显下降。因此有一些PrefixSpan的改进版算法都是在优化构造投影数据库这一块。比如使用伪投影计数。
当然使用大数据平台的分布式计算能力也是加快PrefixSpan运行速度一个好办法。比如Spark的MLlib就内置了PrefixSpan算法。
不过scikit-learn始终不太重视关联算法,一直都不包括这一块的算法集成。
七、在pyspark下实践PrefixSpan算法
7.1 pyspark的PrefixSpan算法的参数
Spark的MLlib中内置了PrefixSpan算法,但是可惜的是,在ML中却没有PrefixSpan算法。因此,我们在这里介绍一下MLlib中PrefixSpan算法的参数:
类:FreqSequence
该类用于表示频繁序列,数据结构为 (sequence, freq) 元组
类:pyspark.mllib.fpm.PrefixSpan
方法: train(data, minSupport=0.1, maxPatternLength=10, maxLocalProjDBSize=32000000)
-
data:输入数据集,每个样本代表一个序列
-
minSupport:最小支持度,任何出现次数大于 minSupport*size-of-the-dataset 的模式都会被输出,默认为
0.1 -
maxPatternLength:序列的最大长度,默认为 10
-
maxLocalProjDBSize:本地处理前,数据库允许的最大样本数量,若超过此数量,会执行另一个分布式 prefix growth
迭代。默认 32000000
类: pyspark.mllib.fpm.PrefixSpanModel
方法: freqSequences()
返回该模型的频繁序列集
7.2 代码
我们用本机的spark环境测试一下官方给的代码,见参考文献【4】。
为了与上述的原理部分分析比照,我们使用和原理篇一样的数据集,一样的支持度阈值50%,同时将最长频繁序列程度设置为4,来训练数据。代码如下:
from pyspark import SparkContext
from pyspark import SparkConf
from pyspark.mllib.fpm import PrefixSpan
sc = SparkContext("local","testing")
data = [
[['a'],["a", "b", "c"], ["a","c"],["d"],["c", "f"]],
[["a","d"], ["c"],["b", "c"], ["a", "e"]],
[["e", "f"], ["a", "b"], ["d","f"],["c"],["b"]],
[["e"], ["g"],["a", "f"],["c"],["b"],["c"]]
]
rdd = sc.parallelize(data, 2)
model = PrefixSpan.train(rdd, 0.5,4)
sorted(model.freqSequences().collect())
输出的结果为:
[FreqSequence(sequence=[['a']], freq=4),
FreqSequence(sequence=[['a'], ['a']], freq=2),
FreqSequence(sequence=[['a'], ['b']], freq=4),
FreqSequence(sequence=[['a'], ['b'], ['a']], freq=2),
FreqSequence(sequence=[['a'], ['b'], ['c']], freq=2),
FreqSequence(sequence=[['a'], ['b', 'c']], freq=2),
FreqSequence(sequence=[['a'], ['b', 'c'], ['a']], freq=2),
FreqSequence(sequence=[['a'], ['c']], freq=4),
FreqSequence(sequence=[['a'], ['c'], ['a']], freq=2),
FreqSequence(sequence=[['a'], ['c'], ['b']], freq=3),
FreqSequence(sequence=[['a'], ['c'], ['c']], freq=3),
FreqSequence(sequence=[['a'], ['d']], freq=2),
FreqSequence(sequence=[['a'], ['d'], ['c']], freq=2),
FreqSequence(sequence=[['a'], ['f']], freq=2),
FreqSequence(sequence=[['b']], freq=4),
FreqSequence(sequence=[['b'], ['a']], freq=2),
FreqSequence(sequence=[['b'], ['c']], freq=3),
FreqSequence(sequence=[['b'], ['d']], freq=2),
FreqSequence(sequence=[['b'], ['d'], ['c']], freq=2),
FreqSequence(sequence=[['b'], ['f']], freq=2),
FreqSequence(sequence=[['b', 'a']], freq=2),
FreqSequence(sequence=[['b', 'a'], ['c']], freq=2),
FreqSequence(sequence=[['b', 'a'], ['d']], freq=2),
FreqSequence(sequence=[['b', 'a'], ['d'], ['c']], freq=2),
FreqSequence(sequence=[['b', 'a'], ['f']], freq=2),
FreqSequence(sequence=[['b', 'c']], freq=2),
FreqSequence(sequence=[['b', 'c'], ['a']], freq=2),
FreqSequence(sequence=[['c']], freq=4),
FreqSequence(sequence=[['c'], ['a']], freq=2),
FreqSequence(sequence=[['c'], ['b']], freq=3),
FreqSequence(sequence=[['c'], ['c']], freq=3),
FreqSequence(sequence=[['d']], freq=3),
FreqSequence(sequence=[['d'], ['b']], freq=2),
FreqSequence(sequence=[['d'], ['c']], freq=3),
FreqSequence(sequence=[['d'], ['c'], ['b']], freq=2),
FreqSequence(sequence=[['e']], freq=3),
FreqSequence(sequence=[['e'], ['a']], freq=2),
FreqSequence(sequence=[['e'], ['a'], ['b']], freq=2),
FreqSequence(sequence=[['e'], ['a'], ['c']], freq=2),
FreqSequence(sequence=[['e'], ['a'], ['c'], ['b']], freq=2),
FreqSequence(sequence=[['e'], ['b']], freq=2),
FreqSequence(sequence=[['e'], ['b'], ['c']], freq=2),
FreqSequence(sequence=[['e'], ['c']], freq=2),
FreqSequence(sequence=[['e'], ['c'], ['b']], freq=2),
FreqSequence(sequence=[['e'], ['f']], freq=2),
FreqSequence(sequence=[['e'], ['f'], ['b']], freq=2),
FreqSequence(sequence=[['e'], ['f'], ['c']], freq=2),
FreqSequence(sequence=[['e'], ['f'], ['c'], ['b']], freq=2),
FreqSequence(sequence=[['f']], freq=3),
FreqSequence(sequence=[['f'], ['b']], freq=2),
FreqSequence(sequence=[['f'], ['b'], ['c']], freq=2),
FreqSequence(sequence=[['f'], ['c']], freq=2),
FreqSequence(sequence=[['f'], ['c'], ['b']], freq=2)]
我们可以看到:d开头的频繁项集就是< d>< db>< dc>< dcb>,跟我们理论分析的一样。
八、讨论
现在我们已经可以求出了序列数据集的频繁项集了,我们在实际中应该如何有效的利用我们挖掘出的结果呢?只有有效利用了这个结果,我们才真正地把这个算法给落地。因此,对时间序列时序关联规则的进一步挖掘是十分必要的。
接下来的一段时间,我先搞一些预研,有些经验了,再来分享。
参考文献
【2】用Spark学习FP Tree算法和PrefixSpan算法
本文主要转自前2篇博客。
【4】PrefixSpan