1. 程式人生 > >機器學習經典演算法之Apriori

機器學習經典演算法之Apriori

一、 搞懂關聯規則中的幾個概念

關聯規則這個概念,最早是由 Agrawal 等人在 1993 年提出的。在 1994 年 Agrawal 等人又提出了基於關聯規則的 Apriori 演算法,至今 Apriori 仍是關聯規則挖掘的重要演算法。

/*請尊重作者勞動成果,轉載請標明原文連結:*/

/* https://www.cnblogs.com/jpcflyer/p/11146587.html * /

關聯規則挖掘可以讓我們從資料集中發現項與項(item 與 item)之間的關係,它在我們的生活中有很多應用場景,“購物籃分析”就是一個常見的場景,這個場景可以從消費者交易記錄中發掘商品與商品之間的關聯關係,進而通過商品捆綁銷售或者相關推薦的方式帶來更多的銷售量。所以說,關聯規則挖掘是個非常有用的技術。

我舉一個超市購物的例子,下面是幾名客戶購買的商品列表:

 

什麼是支援度呢?

支援度是個百分比,它指的是某個商品組合出現的次數與總次數之間的比例。支援度越高,代表這個組合出現的頻率越大。

在這個例子中,我們能看到“牛奶”出現了 4 次,那麼這 5 筆訂單中“牛奶”的支援度就是 4/5=0.8。

同樣“牛奶 + 麵包”出現了 3 次,那麼這 5 筆訂單中“牛奶 + 麵包”的支援度就是 3/5=0.6。

 

什麼是置信度呢?

它指的就是當你購買了商品 A,會有多大的概率購買商品 B,在上面這個例子中:

置信度(牛奶→啤酒)=2/4=0.5,代表如果你購買了牛奶,有多大的概率會購買啤酒?

置信度(啤酒→牛奶)=2/3=0.67,代表如果你購買了啤酒,有多大的概率會購買牛奶?

我們能看到,在 4 次購買了牛奶的情況下,有 2 次購買了啤酒,所以置信度 (牛奶→啤酒)=0.5,而在 3 次購買啤酒的情況下,有 2 次購買了牛奶,所以置信度(啤酒→牛奶)=0.67。

所以說置信度是個條件概念,就是說在 A 發生的情況下,B 發生的概率是多少。

 

什麼是提升度呢?

我們在做商品推薦的時候,重點考慮的是提升度,因為提升度代表的是“商品 A 的出現,對商品 B 的出現概率提升的”程度。

還是看上面的例子,如果我們單純看置信度 (可樂→尿布)=1,也就是說可樂出現的時候,使用者都會購買尿布,那麼當用戶購買可樂的時候,我們就需要推薦尿布麼?

實際上,就算使用者不購買可樂,也會直接購買尿布的,所以使用者是否購買可樂,對尿布的提升作用並不大。我們可以用下面的公式來計算商品 A 對商品 B 的提升度:

提升度 (A→B)= 置信度 (A→B)/ 支援度 (B)

這個公式是用來衡量 A 出現的情況下,是否會對 B 出現的概率有所提升。

所以提升度有三種可能:

提升度 (A→B)>1:代表有提升;

提升度 (A→B)=1:代表有沒有提升,也沒有下降;

提升度 (A→B)<1:代表有下降。

 

二、 Apriori 的工作原理

明白了關聯規則中支援度、置信度和提升度這幾個重要概念,我們來看下 Apriori 演算法是如何工作的。

首先我們把上面案例中的商品用 ID 來代表,牛奶、麵包、尿布、可樂、啤酒、雞蛋的商品 ID 分別設定為 1-6,上面的資料表可以變為:

Apriori 演算法其實就是查詢頻繁項集 (frequent itemset) 的過程,所以首先我們需要定義什麼是頻繁項集。

頻繁項集就是支援度大於等於最小支援度 (Min Support) 閾值的項集,所以小於最小值支援度的專案就是非頻繁項集,而大於等於最小支援度的項集就是頻繁項集。

項集這個概念,英文叫做 itemset,它可以是單個的商品,也可以是商品的組合。我們再來看下這個例子,假設我隨機指定最小支援度是 50%,也就是 0.5。

 

我們來看下 Apriori 演算法是如何運算的。

首先,我們先計算單個商品的支援度,也就是得到 K=1 項的支援度:

因為最小支援度是 0.5,所以你能看到商品 4、6 是不符合最小支援度的,不屬於頻繁項集,於是經過篩選商品的頻繁項集就變成:

在這個基礎上,我們將商品兩兩組合,得到 k=2 項的支援度:

我們再篩掉小於最小值支援度的商品組合,可以得到:

我們再將商品進行 K=3 項的商品組合,可以得到:

再篩掉小於最小值支援度的商品組合,可以得到:

到這裡,你已經和我模擬了一遍整個 Apriori 演算法的流程,下面我來給你總結下 Apriori 演算法的遞迴流程:

K=1,計算 K 項集的支援度;

篩選掉小於最小支援度的項集;

如果項集為空,則對應 K-1 項集的結果為最終結果。

否則 K=K+1,重複 1-3 步。

 

三、 Apriori 的改進演算法:FP-Growth 演算法

能看到 Apriori 在計算的過程中有以下幾個缺點:

可能產生大量的候選集。因為採用排列組合的方式,把可能的項集都組合出來了;

每次計算都需要重新掃描資料集,來計算每個項集的支援度。

 

所以 Apriori 演算法會浪費很多計算空間和計算時間,為此人們提出了 FP-Growth 演算法,它的特點是:

建立了一棵 FP 樹來儲存頻繁項集。在建立前對不滿足最小支援度的項進行刪除,減少了儲存空間。我稍後會講解如何構造一棵 FP 樹;

整個生成過程只遍歷資料集 2 次,大大減少了計算量。

所以在實際工作中,我們常用 FP-Growth 來做頻繁項集的挖掘,下面我給你簡述下 FP-Growth 的原理。

 

1. 建立項頭表(item header table)

建立項頭表的作用是為 FP 構建及頻繁項集挖掘提供索引。

這一步的流程是先掃描一遍資料集,對於滿足最小支援度的單個項(K=1 項集)按照支援度從高到低進行排序,這個過程中刪除了不滿足最小支援度的項。

項頭表包括了專案、支援度,以及該項在 FP 樹中的連結串列。初始的時候連結串列為空。

 

2. 構造 FP 樹

FP 樹的根節點記為 NULL 節點。

整個流程是需要再次掃描資料集,對於每一條資料,按照支援度從高到低的順序進行建立節點(也就是第一步中項頭表中的排序結果),節點如果存在就將計數 count+1,如果不存在就進行建立。同時在建立的過程中,需要更新項頭表的連結串列。

 

3. 通過 FP 樹挖掘頻繁項集

到這裡,我們就得到了一個儲存頻繁項集的 FP 樹,以及一個項頭表。我們可以通過項頭表來挖掘出每個頻繁項集。

具體的操作會用到一個概念,叫“條件模式基”,它指的是以要挖掘的節點為葉子節點,自底向上求出 FP 子樹,然後將 FP 子樹的祖先節點設定為葉子節點之和。

我以“啤酒”的節點為例,從 FP 樹中可以得到一棵 FP 子樹,將祖先節點的支援度記為葉子節點之和,得到:

你能看出來,相比於原來的 FP 樹,尿布和牛奶的頻繁項集數減少了。這是因為我們求得的是以“啤酒”為節點的 FP 子樹,也就是說,在頻繁項集中一定要含有“啤酒”這個項。你可以再看下原始的資料,其中訂單 1{牛奶、麵包、尿布}和訂單 5{牛奶、麵包、尿布、可樂}並不存在“啤酒”這個項,所以針對訂單 1,尿布→牛奶→麵包這個項集就會從 FP 樹中去掉,針對訂單 5 也包括了尿布→牛奶→麵包這個項集也會從 FP 樹中去掉,所以你能看到以“啤酒”為節點的 FP 子樹,尿布、牛奶、麵包項集上的計數比原來少了 2。

條件模式基不包括“啤酒”節點,而且祖先節點如果小於最小支援度就會被剪枝,所以“啤酒”的條件模式基為空。

同理,我們可以求得“麵包”的條件模式基為:

所以可以求得麵包的頻繁項集為{尿布,麵包},{尿布,牛奶,麵包}。同樣,我們還可以求得牛奶,尿布的頻繁項集,這裡就不再計算展示。

 

四、 如何使用 Apriori 工具包

Apriori 雖然是十大演算法之一,不過在 sklearn 工具包中並沒有它,也沒有 FP-Growth 演算法。這裡教你個方法,來選擇 Python 中可以使用的工具包,你可以通過 https://pypi.org/ 搜尋工具包。

這個網站提供的工具包都是 Python 語言的,你能找到 8 個 Python 語言的 Apriori 工具包,具體選擇哪個呢?建議你使用第二個工具包,即 efficient-apriori。後面我會講到為什麼推薦這個工具包。

首先你需要通過 pip install efficient-apriori 安裝這個工具包。

然後看下如何使用它,核心的程式碼就是這一行:

1 itemsets, rules = apriori(data, min_support,  min_confidence)

其中 data 是我們要提供的資料集,它是一個 list 陣列型別。min_support 引數為最小支援度,在 efficient-apriori 工具包中用 0 到 1 的數值代表百分比,比如 0.5 代表最小支援度為 50%。min_confidence 是最小置信度,數值也代表百分比,比如 1 代表 100%。

 

接下來我們用這個工具包,跑一下前面講到的超市購物的例子。下面是客戶購買的商品列表:

具體實現的程式碼如下:

 1 from efficient_apriori import apriori
 2 # 設定資料集
 3 data = [('牛奶','麵包','尿布'),
 4            ('可樂','麵包', '尿布', '啤酒'),
 5            ('牛奶','尿布', '啤酒', '雞蛋'),
 6            ('麵包', '牛奶', '尿布', '啤酒'),
 7            ('麵包', '牛奶', '尿布', '可樂')]
 8 # 挖掘頻繁項集和頻繁規則
 9 itemsets, rules = apriori(data, min_support=0.5,  min_confidence=1)
10 print(itemsets)
11 print(rules)

執行結果:

1 {1: {('啤酒',): 3, ('尿布',): 5, ('牛奶',): 4, ('麵包',): 4}, 2: {('啤酒', '尿布'): 3, ('尿布', '牛奶'): 4, ('尿布', '麵包'): 4, ('牛奶', '麵包'): 3}, 3: {('尿布', '牛奶', '麵包'): 3}}
2 [{啤酒} -> {尿布}, {牛奶} -> {尿布}, {麵包} -> {尿布}, {牛奶, 麵包} -> {尿布}]

你能從程式碼中看出來,data 是個 List 陣列型別,其中每個值都可以是一個集合。實際上你也可以把 data 陣列中的每個值設定為 List 陣列型別,比如:

1 data = [['牛奶','麵包','尿布'],
2            ['可樂','麵包', '尿布', '啤酒'],
3            ['牛奶','尿布', '啤酒', '雞蛋'],
4            ['麵包', '牛奶', '尿布', '啤酒'],
5            ['麵包', '牛奶', '尿布', '可樂']]

兩者的執行結果是一樣的,efficient-apriori 工具包把每一條資料集裡的項式都放到了一個集合中進行運算,並沒有考慮它們之間的先後順序。因為實際情況下,同一個購物籃中的物品也不需要考慮購買的先後順序。

而其他的 Apriori 演算法可能會因為考慮了先後順序,出現計算頻繁項集結果不對的情況。所以這裡採用的是 efficient-apriori 這個工具包。

 

五、 挖掘導演是如何選擇演員的

在實際工作中,資料集是需要自己來準備的,比如我們要挖掘導演是如何選擇演員的資料情況,但是並沒有公開的資料集可以直接使用。因此我們需要使用之前講到的 Python 爬蟲進行資料採集。

不同導演選擇演員的規則是不同的,因此我們需要先指定導演。資料來源我們選用豆瓣電影。

先來梳理下采集的工作流程。

 

首先我們先在 https://movie.douban.com 搜尋框中輸入導演姓名,比如“甯浩”。

頁面會呈現出來導演之前的所有電影,然後對頁面進行觀察,你能觀察到以下幾個現象:

頁面預設是 15 條資料反饋,第一頁會返回 16 條。因為第一條資料實際上這個導演的概覽,你可以理解為是一條廣告的插入,下面才是真正的返回結果。

每條資料的最後一行是電影的演出人員的資訊,第一個人員是導演,其餘為演員姓名。姓名之間用“/”分割。

有了這些觀察之後,我們就可以編寫抓取程式了。在程式碼講解中你能看出這兩點觀察的作用。抓取程式的目的是為了生成甯浩導演(你也可以抓取其他導演)的資料集,結果會儲存在 csv 檔案中。完整的抓取程式碼如下:

 1 # -*- coding: utf-8 -*-
 2 # 下載某個導演的電影資料集
 3 from efficient_apriori import apriori
 4 from lxml import etree
 5 import time
 6 from selenium import webdriver
 7 import csv
 8 driver = webdriver.Chrome()
 9 # 設定想要下載的導演 資料集
10 director = u'甯浩'
11 # 寫 CSV 檔案
12 file_name = './' + director + '.csv'
13 base_url = 'https://movie.douban.com/subject_search?search_text='+director+'&cat=1002&start='
14 out = open(file_name,'w', newline='', encoding='utf-8-sig')
15 csv_write = csv.writer(out, dialect='excel')
16 flags=[]
17 # 下載指定頁面的資料
18 def download(request_url):
19     driver.get(request_url)
20     time.sleep(1)
21     html = driver.find_element_by_xpath("//*").get_attribute("outerHTML")
22     html = etree.HTML(html)
23     # 設定電影名稱,導演演員 的 XPATH
24     movie_lists = html.xpath("/html/body/div[@id='wrapper']/div[@id='root']/div[1]//div[@class='item-root']/div[@class='detail']/div[@class='title']/a[@class='title-text']")
25     name_lists = html.xpath("/html/body/div[@id='wrapper']/div[@id='root']/div[1]//div[@class='item-root']/div[@class='detail']/div[@class='meta abstract_2']")
26     # 獲取返回的資料個數
27     num = len(movie_lists)
28     if num > 15: # 第一頁會有 16 條資料
29         # 預設第一個不是,所以需要去掉
30         movie_lists = movie_lists[1:]
31         name_lists = name_lists[1:]
32     for (movie, name_list) in zip(movie_lists, name_lists):
33         # 會存在資料為空的情況
34         if name_list.text is None:
35             continue
36         # 顯示下演員名稱
37         print(name_list.text)
38         names = name_list.text.split('/')
39         # 判斷導演是否為指定的 director
40         if names[0].strip() == director and movie.text not in flags:
41             # 將第一個欄位設定為電影名稱
42             names[0] = movie.text
43             flags.append(movie.text)
44             csv_write.writerow(names)
45     print('OK') # 代表這頁資料下載成功
46     print(num)
47     if num >= 14: # 有可能一頁會有 14 個電影
48         # 繼續下一頁
49         return True
50     else:
51         # 沒有下一頁
52         return False
53 
54 # 開始的 ID 為 0,每頁增加 15
55 start = 0
56 while start<10000: # 最多抽取 1 萬部電影
57     request_url = base_url + str(start)
58     # 下載資料,並返回是否有下一頁
59     flag = download(request_url)
60     if flag:
61         start = start + 15
62     else:
63         break
64 out.close()
65 print('finished')

程式碼中涉及到了幾個模組,我簡單講解下這幾個模組。

在引用包這一段,我們使用 csv 工具包讀寫 CSV 檔案,用 efficient_apriori 完成 Apriori 演算法,用 lxml 進行 XPath 解析,time 工具包可以讓我們在模擬後有個適當停留,程式碼中我設定為 1 秒鐘,等 HTML 資料完全返回後再進行 HTML 內容的獲取。使用 selenium 的 webdriver 來模擬瀏覽器的行為。

在讀寫檔案這一塊,我們需要事先告訴 python 的 open 函式,檔案的編碼是 utf-8-sig(對應程式碼:encoding=‘utf-8-sig’),這是因為我們會用到中文,為了避免編碼混亂。

編寫 download 函式,引數傳入我們要採集的頁面地址(request_url)。針對返回的 HTML,我們需要用到之前講到的 Chrome 瀏覽器的 XPath Helper 工具,來獲取電影名稱以及演出人員的 XPath。我用頁面返回的資料個數來判斷當前所處的頁面序號。如果資料個數 >15,也就是第一頁,第一頁的第一條資料是廣告,我們需要忽略。如果資料個數 =15,代表是中間頁,需要點選“下一頁”,也就是翻頁。如果資料個數 <15,代表最後一頁,沒有下一頁。

在程式主體部分,我們設定 start 代表抓取的 ID,從 0 開始最多抓取 1 萬部電影的資料(一個導演不會超過 1 萬部電影),每次翻頁 start 自動增加 15,直到 flag=False 為止,也就是不存在下一頁的情況。

你可以模擬下抓取的流程,獲得指定導演的資料,比如我上面抓取的甯浩的資料。這裡需要注意的是,豆瓣的電影資料可能是不全的,但基本上夠我們用。

有了資料之後,我們就可以用 Apriori 演算法來挖掘頻繁項集和關聯規則,程式碼如下:

 1 # -*- coding: utf-8 -*-
 2 from efficient_apriori import apriori
 3 import csv
 4 director = u'甯浩'
 5 file_name = './'+director+'.csv'
 6 lists = csv.reader(open(file_name, 'r', encoding='utf-8-sig'))
 7 # 資料載入
 8 data = []
 9 for names in lists:
10      name_new = []
11      for name in names:
12            # 去掉演員資料中的空格
13            name_new.append(name.strip())
14      data.append(name_new[1:])
15 # 挖掘頻繁項集和關聯規則
16 itemsets, rules = apriori(data, min_support=0.5,  min_confidence=1)
17 print(itemsets)
18 print(rules)

程式碼中使用的 apriori 方法和開頭中用 Apriori 獲取購物籃規律的方法類似,比如程式碼中都設定了最小支援度和最小置信係數,這樣我們可以找到支援度大於 50%,置信係數為 1 的頻繁項集和關聯規則。

這是最後的執行結果:

1 {1: {('徐崢',): 5, ('黃渤',): 6}, 2: {('徐崢', '黃渤'): 5}}
2 [{徐崢} -> {黃渤}]

 你能看出來,甯浩導演喜歡用徐崢和黃渤,並且有徐崢的情況下,一般都會用黃渤。你也可以用上面的程式碼來挖掘下其他導演選擇演員的規