詳解十大經典數據挖掘算法之——Apriori

本文始發於我的公衆號:TechFlow,原創不易,求個關注web


今天是機器學習專題的第19篇文章,咱們來看經典的Apriori算法。算法

Apriori算法號稱是十大數據挖掘算法之一,在大數據時代威風無兩,哪怕是沒有據說過這個算法的人,對於那個著名的啤酒與尿布的故事也耳熟能詳。但遺憾的是,隨着時代的演進,大數據這個概念很快被機器學習、深度學習以及人工智能取代。即便是拉攏投資人的創業者也不多會講到這個故事了,雖然時代的變遷使人唏噓,可是這並不妨礙它是一個優秀的算法。安全

咱們來簡單回顧一下這個故事,聽說在美國的沃爾瑪超市當中,啤酒和尿布常常被擺放在同一個貨架當中。若是你仔細觀察就會以爲很奇怪,啤酒和尿布不管是從應用場景仍是商品自己的屬性來分都不該該被放在一塊兒,爲何超市要這麼擺放呢?數據結構

看似不合理的現象背後每每都有更深層次的緣由,聽說是沃爾瑪引進了一種全新的算法,它分析了全部顧客在超市消費的記錄,而後計算商品之間的關聯性,發現這兩件商品的關聯性很是高。也就是說有大量的顧客會同時購買啤酒和尿布這兩種商品,因此通過數據的分析,沃爾瑪下令將這兩個商品放在同一個貨架上進行銷售。果真這麼一搞以後,兩種商品的銷量都提高了app

這個在背後分析數據,出謀劃策充當軍師同樣決策的算法就是Apriori算法。機器學習

關聯分析與條件機率

咱們先把這個故事的真假放在一邊,先來分析一下故事背後折射出來的信息。咱們把問題進行抽象,沃爾瑪超市當中的商品種類大概有數萬種,咱們的算法作的實際上是根據售賣數據計算分析這些種類之間的相關性。專業術語叫作關聯分析,這個從字面上很好理解。但從關聯分析這個角度出發,咱們會有些不同的洞見。編輯器

咱們以前都學過條件機率,咱們是否是能夠用條件機率來反應兩個物品之間的關聯性呢?好比,咱們用A表示一種商品,B表示另一種商品,咱們徹底能夠根據全部訂單的狀況計算出P(A|B)和P(B|A),也就是說用戶在購買了A的狀況下會購買B的機率以及在購買B的狀況下會購買A的機率。這樣作看起來也很科學啊,爲何不這樣作呢,還要引入什麼新的算法呢?ide

這也就是算法必要性問題,這個問題解決不了,咱們好像會很難說服本身去學習一門新的算法。其實回答這個問題很簡單,就是成本。大型超市當中的商品通常都有幾萬種,而這幾萬種商品的銷量差別巨大。一些熱門商品好比水果、蔬菜的銷量多是冷門商品,好比冰箱、洗衣機的上千倍甚至是上萬倍。若是咱們要計算兩兩商品之間的相關性顯然是一個巨大的開銷,由於對於每兩個商品的組合,咱們都須要遍歷一遍整個數據集,拿到商品之間共同銷售的記錄,從而計算條件機率。學習

咱們假設商品的種類數是一萬,超市的訂單量也是一萬好了,那麼兩兩商品之間的組合就有一億條,若是再乘上每次計算須要遍歷一次整個數據集,那麼整個運算的複雜度大概會是一萬億。若是再考慮多個商品的組合,那這個數字更加可怕。測試

但實際上一個大型超市訂單量確定不是萬級別的,至少也是十萬或者是百萬量級甚至更多。因此這個計算的複雜度是很是龐大的,若是考慮計算帶來的開銷,這個問題在商業上就是不可解的。由於即便算出來結果帶來的收益也遠遠沒法負擔付出的計算代價,這個計算代價可能比不少人想得大得多,即便是使用現成的雲計算服務,也會帶來極爲昂貴的開銷。若是考慮數據安全,不能使用其餘公司的計算服務的話,那麼本身維護這些數據和人工帶來的消耗也是常人不可思議的。

若是想要得出切實可行的結果,那麼優化算法必定是必須的,不然可能沒有一家超市願意付出這樣的代價。

在咱們介紹Apriori算法以前,咱們能夠先試着本身思考一下這個問題的解法。我真的試着想過,可是我沒有獲得很好的答案,對比一下Apriori算法我才發現,這並不是是我我的的問題,而是由於咱們的思惟有誤區。

若是你作過LeetCode,學過算法導論和數據結構,那麼你在思考問題的時候,每每會不由自主地從最優解以及最佳解的方向出發。反應在這個問題當中,你可能會傾向於找到全部高關聯商品,或者是計算出全部商品對之間的關聯性。可是在這個問題當中,前提可能就是錯的。由於答案的完備性和複雜度之間每每是掛鉤的,找出全部的答案必然會帶來更多的開銷,並且落實在實際當中,犧牲一些完備性也是有道理的。由於對於超市而言,更加關注高銷量的商品,好比電冰箱和洗衣機,即便得出結論它們和某些商品關聯性很高對超市來講也沒有太大意義,由於電冰箱和洗衣機一天總共也賣不出多少臺。

你仔細思考就會發現這個問題和算法的背景比咱們一開始想的和理解的要深入得多,因此讓咱們帶着一點點敬畏之心來看看這個算法的詳細吧。

頻繁項集與關聯規則

在咱們具體瞭解算法的原理以前,咱們先來熟悉兩個術語。第一個屬於叫作頻繁項集,英文是frequent item sets。這個翻譯很接地氣,咱們直接看字面意思應該就能理解。意思是常常會出如今一塊兒的物品的集合。第二個屬於叫作關聯規則,也就是兩個物品之間可能存在很強的關聯關係。

用啤酒和尿布的故事舉個例子,好比不少人常常一塊兒購買啤酒和尿布,那麼啤酒和尿布就常常出如今人們的購物單當中。因此啤酒和尿布就屬於同一個頻繁項集,而一我的買了啤酒頗有可能還會購買尿布,啤酒和尿布之間就存在一個關聯規則。表示它們之間存在很強的內在聯繫。

有了頻繁項集和關聯規則咱們會作什麼事情?很簡單會去計算它們的機率

對於一個集合而言,咱們要考慮的是整個集合出現的機率。在這個問題場景當中,它的計算很是簡單。即用集合當中全部元素一塊兒出現的次數,除以全部的數據條數。這個機率也有一個術語,叫作支持度,英文稱做support。

對於一個關聯規則而言,它指的是A物品和B物品之間的內在關係,其實也就是條件機率。因此A->B關聯規則的機率就是P(AB)/P(A)和條件機率的公式同樣,不過在這個問題場景當中,也有一個術語,叫作置信度,英文是confidence。

其實confidence也好,support也罷,咱們均可以簡單地理解成出現的機率。這是一個計算機率的模型,能夠認爲是條件機率運算的優化。其中關聯規則是基於頻繁項集的,因此咱們能夠先把關聯規則先放一放,先來主要看頻繁項集的求解過程。既然頻繁項集的支持度本質上也是一個機率,那麼咱們就可使用一個閾值來進行限制了。好比咱們規定一個閾值是0.5,那麼凡是支持度小於0.5的集合就不用考慮了。咱們先用這個支持度過一遍全體數據,找出知足支持度限制的單個元素的集合。以後當咱們尋找兩個元素的頻繁項集的時候,它的候選集就再也不是全體商品了,而只有那些包含單個元素的頻繁項集。

同理,若是咱們要尋找三項的頻繁項集,它的候選集就是含有兩項元素的頻繁項集,以此類推。表面上看,咱們是把候選的範圍限制在了頻繁項集內從而簡化了運算。其實它背後有一個很深入的邏輯,即不是頻繁項集的集合,必定不可能構成其餘的頻繁項集。好比電冰箱天天的銷量很低,它和任何商品都不可能構成頻繁項集。這樣咱們就能夠排除掉全部那些不是頻繁項集的全部狀況,大大減小了運算量。

上圖當中的23不是頻繁項集,那麼對應的123和023顯然也都不是頻繁項集。其實咱們把這些非頻繁的項集去掉,剩下的就是頻繁項集。因此咱們從正面或者是反面理解均可以,邏輯的內核是同樣的。

Apriori算法及實現

其實Apriori的算法精髓就在上面的表述當中,也就是根據頻繁項集尋找新的頻繁項集。咱們整理一下整個算法的流程,而後一點點用代碼來實現它,對照代碼和流程很容易就搞清楚了。

首先,咱們來建立一批假的數據用來測試:

def create_dataset():
    return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]

下面咱們要生成只有一個項的全部集合。這一步很好理解,咱們須要對全部有交易的商品生成一個清單,也就是將全部交易記錄中的商品購買記錄進行去重。因爲咱們生成的結果在後序會做爲dict的key,而且咱們知道set也是可變對象,也是不能夠做爲dict中的key的。因此咱們要作一點轉換,將它轉換成frozenset,它能夠認爲是不能夠修改的set。

def individual_components(dataset):
    ret = []
    for data in dataset:
        for i in data:
            ret.append((i))
    # 將list轉化成set便是去重操做
    ret = set(ret)
    return [frozenset((i, )) for i in ret]

執行事後,咱們會獲得這樣一個序列:

[frozenset({1}), frozenset({2}), frozenset({3}), frozenset({4}), frozenset({5})]

上面的這個序列是長度爲1的全部集合,咱們稱它爲C1,這裏的C就是component,也就是集合的意思。下面咱們要生成的f1,也就是長度爲1的頻繁集合。頻繁集合的選取是根據最小支持度過濾的,因此咱們下面要實現的就是計算Ck中每個集合的支持度,而後過濾掉那些支持度不知足要求的集合。這個應該也很好理解:

def filter_components_with_min_support(dataset, components, min_support): # 咱們將數據集中的每一條轉化成set # 一方面是爲了去重,另外一方面是爲了進行集合操做 dataset = list(map(set, dataset)) # 記錄每個集合在數據集中的出現次數 components_dict = defaultdict(int) for data in dataset: for i in components: # 若是集合是data的子集 # 也就是data包含這個集合 if i <= data: components_dict[i] += 1
rows = len(dataset)
frequent_components = []
supports = {}
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> k,v <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> components_dict.items():
    <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 支持度就是集合在數據集中的出現次數除以數據總數</span>
    support = v / rows
    <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 保留知足支持度要求的數據</span>
    <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">if</span> support &gt;= min_support:
        frequent_components.append(k)
    supports[k] = support
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> frequent_components, supports
rows = len(dataset) frequent_components = [] supports = {} <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> k,v <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> components_dict.items(): <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 支持度就是集合在數據集中的出現次數除以數據總數</span> support = v / rows <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 保留知足支持度要求的數據</span> <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">if</span> support &gt;= min_support: frequent_components.append(k) supports[k] = support <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> frequent_components, supports

咱們將支持度設置成0.5來執行一下,會獲得如下結果:

能夠發現數據中的4被過濾了,由於它只出現了1次,支持度是0.25,達不到咱們設置的閾值,和咱們的預期一致。

如今咱們有了方法建立長度爲1的項集,也有了方法根據支持度過濾非頻繁的項集,接下來要作的已經很明顯了,咱們要根據長度爲1的頻繁項集生成長度爲2的候選集,而後再利用上面的方法過濾獲得長度爲2的頻繁項集,再經過長度爲2的頻繁項集生成長度爲3的候選集,如此往復,直到全部的頻繁項集都被挖掘出來爲止。

根據這個思路,咱們接下來還有兩個方法要作,一個是根據長度爲n的頻繁項集生成長度n+1候選集的方法,另外一個方法是利用這些方法挖掘全部頻繁項集的方法。

咱們先來看根據長度爲n的項集生成n+1候選集的方法,這個也很好實現,咱們只須要用全部元素依次加入現有的集合當中便可。

def generate_next_componets(components): # 獲取集合當中全部單個的元素 individuals = individual_components(components) storage = set() for i in individuals: for c in components: # 若是i已經在集合中了則跳過 if i <= c: continue cur = i | c # 不然合併,加入存儲 if cur not in storage: storage.add(cur)
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> list(storage)
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> list(storage)

這些方法都有了以後,剩下的就很好辦了,咱們只須要重複調用上面的方法,直到找不到更長的頻繁項集爲止。咱們直接來看代碼:

def apriori(dataset, min_support):
    # 生成長度1的候選集合
    individuals = individual_components(dataset)
    # 生成長度爲1的頻繁項集
    f1, support_dict = filter_components_with_min_support(dataset, individuals, min_support)
    frequent = [f1]
    while True:
        # 生成長度+1的候選集合
        next_components = generate_next_componets(frequent[-1])
        # 根據支持度篩選出頻繁項集
        components, new_dict = filter_components_with_min_support(dataset, next_components, min_support)
        # 若是篩選結果爲空,說明不存在更長的頻繁項集了
        if len(components) == 0:
            break
        # 更新結果
        support_dict.update(new_dict)
        frequent.append(components)
    return frequent, support_dict

最後,咱們運行一下這個方法查看一下結果:

紅色框中就是咱們從數據集合當中挖掘出的頻繁項集了。在一些場景當中咱們除了想要知道頻繁項集以外,可能還會想要知道關聯規則,看看哪些商品之間存在隱形的強關聯。咱們根據相似的思路能夠設計出算法來實現關聯規則的挖掘。

關聯規則

理解了頻繁項集的概念以後再來算關聯規則就簡單了,咱們首先來看一個很簡單的變形。因爲咱們須要計算頻繁項集之間的置信度,也就是條件機率。咱們都知道P(A|B) = P(AB) / P(B),這個是條件機率的基本公式。這裏的P(A) = 出現A的數據條數/ 總條數,其實也就是A的支持度。因此咱們能夠用支持度來計算置信度,因爲剛剛咱們在計算頻繁項集的時候算出了全部頻繁項集的支持度,因此咱們能夠用這份數據來計算置信度,這樣會簡單不少。

咱們先來寫出置信度的計算公式,它很是簡單:

def calculate_confidence(comp, subset, support_dict):
    return float(support_dict[comp]) / support_dict[comp-subset]

這裏的comp表示集合,subset表示咱們要推斷的項。也就是咱們挖掘的是comp-item這個集合與subset集合之間的置信度。

接着咱們來看候選規則的生成方法,它和前面生成候選集合的邏輯差很少。咱們拿到頻繁項集以後,扣除其中的一個子集,將它做爲一個候選的規則。

def generate_rules(components, subset): all_set = [] # 生成全部子集,也就是長度更小的頻繁項集的集合 for st in subset: all_set += st
rules = []
<span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 遍歷全部子集</span>
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> i <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> all_set:
    <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 遍歷頻繁項集</span>
    <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> comp <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> components:
        <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 若是子集關係成立,則生成了一條候選規則</span>
        <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">if</span> i &lt;= comp:
            rules.append((comp, i))
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> rules
rules = [] <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 遍歷全部子集</span> <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> i <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> all_set: <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 遍歷頻繁項集</span> <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> comp <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> components: <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 若是子集關係成立,則生成了一條候選規則</span> <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">if</span> i &lt;= comp: rules.append((comp, i)) <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> rules

最後,咱們把上面兩個方法串聯在一塊兒,先生成全部的候選規則,再根據置信度過濾掉符合條件的關聯規則。利用以前頻繁項集時候生成的數據,很容易實現這點。

def mine_rules(frequent, support_dict, min_confidence):
    rules = []
    # 遍歷長度大於等於2的頻繁項集
    for i in range(1, len(frequent)):
        # 使用長度更小的頻繁項集做爲本身,構建候選規則集
        candidate_rules = generate_rules(frequent[i], frequent[:i])
        # 計算置信度,根據置信度進行過濾
        for comp, item in candidate_rules:
            confidence = calculate_confidence(comp, item, support_dict)
            if confidence >= min_confidence:
                  rules.append([comp-item, item, confidence])
    return rules

咱們運行一下這個方法,看一下結果:

從結果來看還不錯,咱們挖掘出了全部的關聯規則。要注意一點A->B和B->A是兩條不一樣的規則,這並不重複。舉個簡單的例子,買乒乓拍的人每每都會買乒乓球,可是買乒乓球的人卻並不必定會買乒乓拍,由於乒乓拍比乒乓球貴得多。並且乒乓球是消耗品,乒乓拍不是。因此乒乓拍能夠關聯乒乓球,但反之不必定成立。

結尾、昇華

到這裏,Apriori算法和它的應用場景就講完了。這個算法的原理並不複雜,代碼也不困難,沒有什麼高深的推導或者是晦澀的運算,可是算法背後的邏輯並不簡單。怎麼樣爲一個複雜的場景涉及簡單的指標?怎麼樣縮小咱們計算的範圍?怎麼樣衡量數據的價值?其實這些並非空穴來風,顯然算法的設計者是付出了大量思考的。

若是咱們順着解法出發去試着倒推當時設計者的思考過程,你會發現看似簡單的問題背後其實並不簡單,看似天然而然的道理,也並不天然,這些看似尋常的背後都隱藏着邏輯,這些背後的思考和邏輯,纔是算法真正寶貴的部分。

今天的文章就到這裏,原創不易,須要你的一個關注,你的舉手之勞對我來講很重要。

相關文章
相關標籤/搜索