1. 程式人生 > >資料探勘演算法-Apriori演算法

資料探勘演算法-Apriori演算法

前言

假設你是商場的一名推銷員,正與一位剛在商店買了麵包的顧客交談。你應該向她推薦什麼產品?你應該想知道你的顧客在購買了麵包之後頻繁的購買的哪些物品,這些資訊是非常有用的。在這種情況下,頻繁模式和關聯規則正是你想要挖掘的知識。

基本概念

頻繁模式(frequent pattern)是指頻繁地出現在資料集中的模式,例如頻繁的同時出現在交易資料集中的商品(比如牛奶和麵包)集合是頻繁項集。
如果我們想象全域是商店中商品的集合,則每種商品可以代表一個布林變數,表示該商品是否被購買過。那麼可以用一個“商品A=>商品B”這種形式的布林向量來表示商品A與商品B的關聯程度。如何去衡量這個關聯程度呢?我們引入兩個測距:支援度與置信度。

支援度(support)代表該關聯規則的重要性,定義為集合中商品A出現且商品B出現的次數除以總的商品個數,即support(A=>B) = P(A U B)。
置信度(confidence)代表該關聯規則的可靠性,定義為集合中商品A出現且商品B出現的次數除以商品A出現的次數,即confidence(A=>B) = P(B | A)。
因為我們認為滿足最小支援度閾值(min_sup)和最小置信度閾值(min_conf)的規則是強規則。這兩個最小閾值是由相關領域的專家人為給定的。

項的集合稱為項集。包含k個項的項集稱為k項集。比如之前提到的麵包和牛奶這些商品就可以理解為項,某次購買中購買了商品的集合{牛奶,麵包,堅果}就可以稱為項集,在本例中也就是3項集。若該項集的支援度與置信度均大於給定的最小值,那麼就認為該項集是頻繁項集,記作Lk。

那麼如何找到這樣強關聯規則呢?由置信度的定義可以得到
confidence(A=>B) = P(B | A) = support(A U B) / support(A) = support_count(A U B) / support_count(A)
也就是說,如果獲得了AUB、A的支援度計數,也就是出現的次數,那麼就可以很容易的獲得規則A=>B的置信度。因此,關聯規則的挖掘就可以演變為 頻繁項集的挖掘。

一般方法

那麼如何挖掘頻繁項集呢?首先可以想到如下演算法:

1.輸入事務集D,每一個事務都是一個項集(比如代表某一次購買的商品集合),以及總的項集合T、結果集C
2.對於T的每一個子集t 遍歷一遍D,統計t在D中出現的次數cnt 如果cnt大於等於給定的閾值 那麼將t加入結果集C中 否則就繼續

假設共有n項,那麼T的子集就有2^n個。上述演算法的複雜度就是O(2^n*size(D))。顯然指數級的複雜度是難以接受的。因此就輪到本文的主角登場了,Apriori演算法(方便稱為 愛普弱演算法,雖然好像不是這麼發音的:D)

Apriori演算法

首先介紹一下Apriori性質:
1.若A是一個頻繁項集,則A的每一個子集都是一個頻繁項集。
該性質具有反單調性,於是我們得到:
2.若A不是一個頻繁項集,那麼它的所有超集也一定不是頻繁的。(超集的意思就是說集合Y的子集為A,且Y不等於A)。
該演算法的主要思想是利用 逐層搜尋迭代的方法,即用k項集來探索(k+1)項集,我們用Lk來代表頻繁k項集,Ck來代表長度為k的候選項集,那麼該演算法的流程可以概括如下:
L1->C2->L2->C3->L3->…->一直到某一集合為空
那麼該演算法顯然可以概括為如下兩個步驟:

Step1.自連線,即如何由Lk產生Ck+1
Step2.剪枝,按如下規則剪枝:
1.根據性質12,如果Ck中的某一k-1子集不在Lk-1中,那麼其一定不是頻繁項集,可以直接將其剪去。
2.1的基礎上,遍歷D對每個候選進行計數,得到的結果再與最小支援度比較來進行剪枝。

對於Step1,首先假定事務或項集中的項按字典序排序。對於Lk中的每兩個項集,規定若其前k-1項都相同,且前者的第k項小於後者的第k項,那麼就把兩者求個並,得到一個k+1項的候選項集。
這麼做的主要方法是為了避免重複。

下面給出Apriori演算法的虛擬碼,然後給出C++和Python的實現。

演算法1 Apriori。使用逐層迭代方法基於候選產生找出頻繁項集
輸入:
    1.D: 事務資料庫
    2.min_sup: 最小支援度閾值
輸出: L, D中的頻繁項集。
方法:
    (1) L1 = find_frequent_itemsets(D) //首先找出頻繁1項集
    (2) for (k=2; Lk-1 != 空集; k++) {
    (3)   Ck = apriori_gen(Lk-1);
    (4)   for each 事務t∈D {   //掃描D,進行計數
    (5)       Ct = subset(Ck, t);
    (6)       for each 候選c∈Ct
    (7)          c.count++;
    (8)   }
    (9)   Lk = {c(Ck|c.count >= min_sup)}
    (10) }
    (11) return L = 所有Lk的集合

    procedure apriori_gen(Lk-1: frequent(k-1) itemset)
    (1)  for each 項集l1∈Lk-1
    (2)    for each 項集l2∈Lk-1
    (3)      if (l1[1]==l2[1]) && (l1[2]==l2[2]) && ... &&
    (4)          (l1[k-2] == l2[k-2]) && (l1[k-1] < l2[k-1]) {
    (5)          c = l1 link l2;
    (6)          if has_infrequent_subset(c, Lk-1) then
    (7)             delete c;
    (8)          else add c to Ck;
    (9)return Ck;

    producer has_infrequent_subset(c: candidate k itemset; Lk-1: frequent(k-1 itemset))
    (1) for each(k-1) subset sof c
    (2)   if s 不屬於 Lk-1 then
    (3)     return TRUE;
    (4) return FALSE;

C++實現

測試程式碼是自己寫的,只是為了實現測試功能,具體細節優化沒有考慮:

#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
using namespace std;

//N代表總共的項數+1,這裡假設有5項
#define N 6
#define MIN_SUPPORT 2
//L[k]就代表頻繁k項集,C[k]代表長度為k的候選項集
vector<vector<int>> L[N];
vector<vector<int>> C[N];

//用到的函式宣告
vector<vector<int>> find_frequent_1_itemsets(vector<vector<int>> D, int min_support);
vector<vector<int>> aproiri_gen(vector<vector<int>> L);
bool isValid(vector<int> l1, vector<int> l2);
bool has_infrequent_subset(vector<int> c, vector<vector<int>> L);
bool isexisted(vector<int> itemset, vector<int> candi);

void Aproiri(vector<vector<int>> D, int min_support) {
    if (D.size() == 0) return;
    //首先找出頻繁1項集
    L[1] = find_frequent_1_itemsets(D, min_support);
    for (int k = 2; L[k - 1].size() != 0; k++) {
        C[k] = aproiri_gen(L[k - 1]);
        //輸出C[k]測試
        printf("aproiri_gen產生的k候選集C[%d]:\n", k);
        for (int i = 0; i < C[k].size(); i++) {
            for (int j = 0; j < C[k][i].size(); j++) {
                if (j) printf(" ");
                printf("%d", C[k][i][j]);
            }
            printf("\n");
        }
        //掃描D進行計數,進行第二次剪枝
        map<vector<int>, int> cnt;
        for (auto candi : C[k]) {
            for (auto itemset : D) {
                if (isexisted(itemset, candi)) {
                    cnt[candi]++;
                }
            }
            if (cnt[candi] >= min_support) {
                L[k].push_back(candi);
            }
        }
    }

}

vector<vector<int>> find_frequent_1_itemsets(vector<vector<int>> D, int min_support) {
    vector<vector<int>> res;
    vector<int> cnt(N, 0);
    for (auto vec : D) {
        for (auto num : vec) {
            cnt[num]++;
        }
    }
    for (int i = 1; i < N; i++) {
        if (cnt[i] >= min_support) {
            res.push_back(vector<int>{i});
        }
    }
    return res;
}

vector<vector<int>> aproiri_gen(vector<vector<int>> L) {
    vector<vector<int>> res;
    for (int i = 0; i < L.size(); i++) {
        for (int j = 0; j < L.size(); j++) {
            if (i == j) continue;
            vector<int> candidate = L[i];
            if (isValid(L[i], L[j])) {
                candidate.push_back(L[j].back());
            }
            if (!has_infrequent_subset(candidate, L)) {
                res.push_back(candidate);
            }
        }
    }
    return res;
}
//函式isValid,判斷兩個項集是否可以進行自連線產生新的項集
bool isValid(vector<int> l1, vector<int> l2) {
    if (l1.size() != l2.size()) return false;
    int len = l1.size();
    for (int i = 0; i < len - 1; i++) {
        if (l1[i] != l2[i]) return false;
    }
    return l1[len - 1] < l2[len - 1];
}

bool has_infrequent_subset(vector<int> c, vector<vector<int>> L) {

    for (auto ite = c.begin(); ite != c.end(); ite++) {
        vector<int> subset = c;
        auto site = subset.begin() + (ite - c.begin());
        subset.erase(site);
        if (find(L.begin(), L.end(), subset) == L.end()) {
            return true;
        }
    }
    return false;
}

bool isexisted(vector<int> itemset, vector<int> candi) {
    int len = candi.size();
    for (int i = 0; i < len; i++) {
        if (find(itemset.begin(), itemset.end(), candi[i]) == itemset.end())
            return false;
    }
    return true;
}

int main() {
    //假設資料集為{1,2,5}, {2,4}, {2,3}, {1,2,4}, {1,3},
    //      {2,3},{1,3},{1,2,3,5}, {1,2,3}
    vector<vector<int>> D{ { 1, 2, 5 }, { 2, 4 }, { 2, 3 }, { 1, 2, 4 }, { 1, 3 }, { 2, 3 }, { 1, 3 }, { 1, 2, 3, 5 }, { 1, 2, 3 } };
    Aproiri(D, MIN_SUPPORT);
    printf("最終挖掘結果:\n");
    for (int i = 1; i < N; i++) {
        if (L[i].size() == 0) break;
        printf("頻繁%d項集:\n", i);
        for (int j = 0; j < L[i].size(); j++) {
            for (int k = 0; k < L[i][j].size(); k++) {
                if (k) printf(" ");
                printf("%d", L[i][j][k]);
            }
            printf("\n");
        }
    }
    return 0;
}

結果圖:
這裡寫圖片描述

Python實現

我這裡用的是Windows下的Anaconda,直接是傻瓜式安裝,Ipython,Jupyter Notebook直接一步到位,用起來很舒服。貼個安裝教程:
http://blog.csdn.net/zr459927180/article/details/51253087
由於自己對Python不是很熟練,所以決定用Python來實現一下該演算法。
照著C++的程式碼就能寫出來說,不得不說python真是巨tm好使。。

def loadData():
    return [[1,2,5], [2,4], [2,3], [1,2,4], [1,3], [2,3], [1,3], [1,2,3,5], [1,2,3]]
def find_frequent_1_itemsets(D, minsupport):
    L1 = []
    C1 = []
    cnt = {}
    for transcation in D:
        for item in transcation:
            if not [item] in C1:
                C1.append([item])
                cnt[item] = 1
            else:
                cnt[item] += 1
    for transcation in C1:
        for item in transcation:
            if cnt[item] >= minsupport:
                L1.append(transcation)
    L1.sort()
    return L1

def aproiri_gen(L, k):
    res = []
    lenL = len(L)
    for i in range(lenL):
        for j in range(i+1,lenL):
            l1 = L[i][:-1]
            l2 = L[j][:-1]
            if l1 == l2 and L[i][-1] < L[j][-1]:
                candidate = list(set(L[i]).union(set(L[j])))                
                if not has_infrequent_subset(candidate, L):
                    res.append(candidate)
    return res

def has_infrequent_subset(candidate, L):

    for i in range(len(candidate)):
        subset = candidate.copy()
        subset.remove(candidate[i])
        if subset not in L:
            return True
    return False

def compareList(l1, l2):
    for item in l1:
        if item not in l2:
            return False
    return True

def Aproiri(D, minsupport):
    L = []
    L1 = find_frequent_1_itemsets(D, minsupport)
    L.append([])
    L.append(L1)
    for k in range(2, 5):
        Lk = []
        if len(L[k-1]) == 0:
            break
        Ck = aproiri_gen(L[k-1], k-1)
        print("自連結加剪枝後得到的候選Ck:" , Ck)
        print("遍歷D對每個候選計數")
        for candi in Ck:
            cnt = 0
            for transcation in D:
                if compareList(candi, transcation):
                    cnt += 1
            if cnt >= minsupport:
                print ("符合要求的項集: ", candi, "出現次數: ",cnt)
                Lk.append(candi)
        L.append(Lk)
    return L
#test
D = loadData()
L = Aproiri(D, 2)
print (L)

#Output:
自連結加剪枝後得到的候選Ck: [[1, 2], [1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 4], [3, 5], [4, 5]]
遍歷D對每個候選計數
符合要求的項集:  [1, 2] 出現次數:  4
符合要求的項集:  [1, 3] 出現次數:  4
符合要求的項集:  [1, 5] 出現次數:  2
符合要求的項集:  [2, 3] 出現次數:  4
符合要求的項集:  [2, 4] 出現次數:  2
符合要求的項集:  [2, 5] 出現次數:  2
自連結加剪枝後得到的候選Ck: [[1, 2, 3], [1, 2, 5]]
遍歷D對每個候選計數
符合要求的項集:  [1, 2, 3] 出現次數:  2
符合要求的項集:  [1, 2, 5] 出現次數:  2
自連結加剪枝後得到的候選Ck: []
遍歷D對每個候選計數
[[], [[1], [2], [3], [4], [5]], [[1, 2], [1, 3], [1, 5], [2, 3], [2, 4], [2, 5]], [[1, 2, 3], [1, 2, 5]], []]

由頻繁項集產生關聯規則

對於任一得到的頻繁項集,首先得到其所有的非空子集,從中任取兩個子集A和B,那麼根據置信度的公式
confidence(A=>B) = P(B | A) = support(A U B) / support(A) = support_count(A U B) / support_count(A)
就能計算出相應的置信度,如果該置信度大於等於給定的最小置信度,則該規則就可以認為是強關聯規則。