Apriori算法

  Apriori算法用于关联分析,其目标包括两个:发现频繁项集,发现关联规则。首先需要发现频繁项集,然后才能发现关联规则。本文Apriori部分的代码来自《机器学习实战》,有需要可以看看。


发现频繁项集

  频繁项集指那些经常出现在一起的集合。若某个项集是频繁项集,则它的所有子集也是频繁的。反之,若一个项集是非频繁项集,则它的所有超集也是非频繁的。Apriori利用这个原理,避免计算非频繁项集及其所有超集的支持度。
  给定数据集,用Apriori发现频繁项集,只有一个输入参数:最小支持度。Apriori初始生成所有单个物品的候选集,然后扫描数据集,滤掉小于最小支持度的候选集,接着把剩下的集合进行组合,生成包含两个元素的候选集,如此持续下去,直到所有候选集被消去。
  频繁项集的量化指标是支持度,频繁项集必须满足支持度需求。

# 提取数据集的单元素集合(大小为1的候选集的集合)
def createC1(dataSet):
    C1 = set()
    for transaction in dataSet:
        for item in transaction:
            C1.add(item)
    C1 = list(C1)
    C1.sort()
    # 用frozenset是为了能作为key
    return map(lambda x:frozenset([x]), C1)

# 计算Ck中每项的支持度并过滤
def scanD(D, Ck, minSupport):
    ssCnt = {}
    for tid in D:
        for can in Ck:
            if can.issubset(tid):
                if not ssCnt.has_key(can):
                    ssCnt[can] = 1
                else:
                    ssCnt[can] += 1
    retList = [] # Lk
    supportData = {} # 支持度(出现比例)
    for key in ssCnt:
        support = ssCnt[key] / float(len(D))
        if support >= minSupport:
            retList.insert(0, key) # 阈值过滤
        supportData[key] = support
    return retList, supportData

  createC1()用于初始化,构建集合C1,它的每个元素都是大小为1的候选集,顾名思义,Ck的每个元素大小为k。scanD()用于扫描数据集,判断C1里的每一个候选集是否满足最小支持度需求,过滤掉,然后剩下的构成L1L1中的元素后续会进行组合,构成C2,然后过滤成L2,如此循环往复。

# 构造下一个候选集Ck 
def aprioriGen(Lk, k):
    retList = []
    lenLk = len(Lk)
    for i in range(lenLk):
        for j in range(i+1, lenLk):
            L1 = list(Lk[i])[:k-2]; L2 = list(Lk[j])[:k-2]
            L1.sort(); L2.sort()
            if L1 == L2: # 如果它们前k-2项相同
                retList.append(Lk[i] | Lk[j]) # 合并
    return retList

def apriori(dataSet, minSupport=0.5):
    C1 = createC1(dataSet)
    D = map(set, dataSet)
    L1, supportData = scanD(D, C1, minSupport)
    L = [L1]
    k = 2
    while len(L[k-2])>0:
        Ck = aprioriGen(L[k-2], k)
        Lk, supK = scanD(D, Ck, minSupport) # 扫描并过滤
        supportData.update(supK)
        L.append(Lk)
        k += 1
    return L, supportData

  aprioriGen()接收Lk,创建候选集Ck,比如输入{0}、{1}、{2},就会输出{0,1}、{0,2}、{1,2}。注意这里有一个判断,若输入的两个元素(集合)有k-2项相同才合并,这个做法的目的是减少遍历次数。主函数是apriori(),它将持续生成Ck,然后过滤生成Lk,一直到无法继续为止。


挖掘关联规则

  关联规则源于频繁项集,任意一个频繁项集都能产生若干条关联规则,其中只有一部分成立。例如{“啤酒”,“奶粉”},可能有一条规则“啤酒”→“奶粉”,但是反之不一定成立。
  关联规则的量化指标是可信度,规则P→H的可信度定义为 support(P∪H)/support(P)。对于任何一个频繁项集,其产生的所有关联规则,都要计算可信度,把低可信度的规则去掉。然而有时候频繁项集的元素个数太多,可能产生很多关联规则,为了减少计算复杂度,利用这个规律:若某条规则不满足最小可信度需求,则该规则的所有子集也不满足最小可信度需求。

# 计算可信度
def calcConf(freqSet, H, supportData, br1, minConf=0.7):
    prunedH = []
    for conseq in H:
        conf = supportData[freqSet] / supportData[freqSet - conseq]
        if conf >= minConf: # 过滤
            # print "{0} --> {1} conf:{2}".format(freqSet - conseq, conseq, conf)
            br1.append((freqSet - conseq, conseq, conf))
            prunedH.append(conseq)
    return prunedH

def rulesFromConseq(freqSet, H, supportData, br1, minConf=0.7):
    m = len(H[0])
    if len(freqSet) > m+1:
        Hmp1 = aprioriGen(H, m+1)
        Hmp1 = calcConf(freqSet, Hmp1, supportData, br1, minConf)
        if len(Hmp1)>1:
            rulesFromConseq(freqSet, Hmp1, supportData, br1, minConf)

def generateRules(L, supportData, minConf=0.7):
    bigRuleList = []
    for i in range(1, len(L)):
        for freqSet in L[i]:
            H1 = [frozenset([item]) for item in freqSet]
            if i>1:
                rulesFromConseq(freqSet, H1, supportData, bigRuleList, minConf)
            else:
                calcConf(freqSet, H1, supportData, bigRuleList, minConf)
    return bigRuleList

  generateRules()是主函数,它有三个输入:频繁项集列表L、支持度表supportData和最小可信度minConf,输出一个包含可信度的规则列表bigRuleList。它会遍历L中每一个频繁项集,对每个频繁项集创建只包含单个元素集合的列表H1,如果每个项集只有1个元素,直接用calcConf()计算可信度,否则用rulesFromConseq()进行合并。


示例:发现毒蘑菇的相似特征

  Apriori还可以拿来做分类。我们不寻找所有的频繁项集,只对包含label的项集感兴趣。也就是说我们需要找到一些关联规则P→H,在H中包含label。这样我们就可以遍历所有样本,若P是样本的子集,就将对应的置信度作为样本是对应label的概率。当匹配的P很多时,可以取平均值。这种做法具有可解释性,可以理解为,“特征搭配为xxx和xxx时,平均有xx%的概率是正例”。

蘑菇数据可以在这里这里找到。

为了保证label被包含在H中,需要对calConf()函数做一点改动。

def calcConf(freqSet, H, supportData, br1, minConf=0.7):
    prunedH = []
    for conseq in H:
        conf = supportData[freqSet] / supportData[freqSet - conseq]
        if conf >= minConf:
            # 仅当label被包含在H中
            if "1" in conseq or "0" in conseq:
                # print "{0} --> {1} conf:{2}".format(freqSet - conseq, conseq, conf)
                br1.append((freqSet - conseq, conseq, conf))
                prunedH.append(conseq)
    return prunedH

主函数

import apriori
import time
import numpy as np

# 读取训练集
with open("./data/agaricus_train.csv", "rb") as f:
    dataSet = [line[:-1].split(',') for line in f.readlines()]

# L中的每一个元素都至少在25%的样本中出现过
L, suppData = apriori.apriori(dataSet, 0.25) # 阈值越小,越慢

# 生成规则,每个规则的置信度至少是0.6
bigRuleList = apriori.generateRules(L, suppData, 0.6)

# P→H,根据P集合的大小排序
bigRuleList = sorted(bigRuleList, key=lambda x:len(x[0]), reverse=True)

# 读取测试集
with open("./data/agaricus_test.csv", "rb") as f:
    dataSet = [line[:-1].split(',') for line in f.readlines()]
labels = np.array([int(x[0]) for x in dataSet])

scores = []
for line in dataSet:
    tmp = []
    for item in bigRuleList:
        if item[0].issubset(set(line)):
            if "1" in item[1]:
                tmp.append(float(item[2]))
            # 因为是预测“为1的概率”,所以要用1减
            if "0" in item[1]:
                tmp.append(1 - float(item[2]))
    scores.append(np.mean(tmp)) # 求取均值

# 用置信度作为预测概率
scores = map(lambda x:x>0.5, scores)
scores = np.array(scores, dtype='int')
print sum(np.equal(scores, labels)), len(labels), sum(np.equal(scores, labels))/float(len(labels))

输出

1427 1611 0.8857852265673495

虽然88.6%的识别率很低,但将最小支持度进一步调低,应该还可以提高识别率,此外,也可以将这个置信度当做一维特征,而不是单纯作为预测概率。

Apriori的缺点

k k <script type="math/tex" id="MathJax-Element-6">k</script>,Apriori都要遍历一次整个数据集,而当支持度变低,满足条件的频繁项集变多,这个时间消耗就进一步增加。

完整实验代码

https://github.com/SongDark/Apriori


参考资料
Frequent Itemset Mining Dataset Repository
UCI mushroom
mushroom 分训练集测试集