用亲和性分析方法推荐电影
親和性分析面臨的問題
親和性分析比分類更具探索性,因為通常我們無法拿到像在很多分類任務中所用的那樣完整 的數據集。例如,在電影推薦任務中,我們拿到的是不同用戶對不同電影的評價。但是,每個用 戶不可能評價過所有電影,這就給親和性分析帶來一個不容忽視的大難題。如果用戶沒有評價過一部電影,是因為他們不喜歡這部電影(據此就不推薦給他們),還是因為他們出于別的原因還沒有評價?
思考數據集中類似潛在問題該怎么解決,這將幫助我們提升推薦算法的準確性。
另外一個問題在于https://mp.csdn.net/mdeditor/81013001,像這個鏈接中說的親和性測試方法過于簡單粗暴(用排列組合枚舉的辦法)。從排列組合的知識出發,我們知道,對于每一個商品,只存在買或者不買兩種情況。于是,所有可能的規則數量是2n?12n?1。數據集有5個特征,可能的規則就有31條;有10個特征,可能的規則就有1023條;僅僅有100個特征,規則數就能達到30位數字。即使計算能力大幅提升也未 必能趕上在線商品的增長速度。因此,與其跟計算機過不去,不如尋找更加聰明的算法。
Apriori算法介紹
Apriori算法是經典的親和性分析算法,它只從數據集中頻繁出現的商品中選出共同出現的商品組成頻繁項集(frequent itemset)避免了上述復雜度呈指數級增長的問題。一旦找到頻繁項集,生成關聯規則就很容易了。
Apriori算法的一個重要參數就是最小支持度。比如,要生成包含商品A、B的頻繁項集(A, B), 要求支持度至少為30,那么A和B都必須至少在數據集中出現30次。更大的頻繁項集也要遵守該 項約定,比如要生成頻繁項集(A, B, C, D),那么子集(A, B, C)必須是頻繁項集(當然D自己也 要滿足最小支持度標準)。
生成頻繁項集后,將不再考慮其他可能的卻不夠頻繁的項集(這樣的集合有很多),從而大 大減少測試新規則所需的時間。
其他親和性分析算法有Eclat和頻繁項集挖掘算法(FP-growth)。從數據挖掘角度看,這些 算法比起基礎的Apriori算法有很多改進,性能也有進一步提升。
在這個電影推薦過程中,我們主要分兩步走。第一步,找出頻繁項集。第二步,根據置信度選取關聯規則。可以設定最小置信度,返回一部分規則,或者返回所有規則,讓用戶自己選。
數據集介紹
首先,我們下載電影評分數據集:http://grouplens.org/datasets/movielens/
這個數據集是美國蘇明達州一個研究團隊公開的。下載ml-100k.zip這個數據集。
接著,我們導入數據集
import os data_folder = os.path.join(os.path.expanduser("~"), "Data", "ml-100k") print(data_folder) ratings_filename = os.path.join(data_folder, "u.data") print(ratings_filename)輸出結果是:
C:\Users\mi\Data\ml-100k
C:\Users\mi\Data\ml-100k\u.data
MovieLens數據集非常規整,但是有幾點跟pandas.read_csv方法的默認設置有出入,所以 要調整參數設置。第一個問題是數據集每行的幾個數據之間用制表符而不是逗號分隔。其次,沒 有表頭,這表示數據集的第一行就是數據部分,我們需要手動為各列添加名稱。
加載數據集時,把分隔符設置為制表符,告訴pandas不要把第一行作為表頭(header=None), 設置好各列的名稱。
import pandas as pd''' pd.csv文件中參數: names:指定列名,如果文件中不包含header的行,應該顯性表示header=None delimiter: 指定分割符,默認是’,’C引擎不能自動檢測分隔符,但Python解析引擎可以 header: 指定第幾行作為列名(忽略注解行),如果沒有指定列名,默認header=0; 如果指定了列名header=None ''' all_ratings = pd.read_csv(ratings_filename, delimiter="\t", header=None, names = ["UserID", "MovieID", "Rating", "Datetime"]) all_ratings["Datetime"] = pd.to_datetime(all_ratings['Datetime'],unit='s') all_ratings[0:5]這里我們再介紹一下to_datatime方法
利用 pandas 的to_datetime 方法,把 “date” 列的字符類型數據解析成 datetime 對象。unit=’s’是指時間精度精確到秒(unit of the arg (D,s,ms,us,ns))
結果如下:
稀疏矩陣
這其實是一個稀疏矩陣的形式。對于這個數據而言,每一行代表每一個用戶給每一個電影打一個分數。數據集中有1000名用戶和1700部電影,這就意味著整個矩陣很大。不可能所有用戶對所有電影都打分,大部分矩陣是空的。我們用上述方式也可以表示整個數據,但相比較而言,更為緊湊。
任何沒有出現在數據集中的用戶和電影組合表示它們實際上是不存在的。這比起在內存中保存大量的0,節省了很多空間。這種格式叫作稀疏矩陣(sparse matrix)。根據經驗來說,如果數 據集中60%或以上的數據為0,就應該考慮使用稀疏矩陣,從而節省不少空間。
數據處理
首先要確定用戶是不是喜歡某一部電影。為此創建新特征Favorable,若用戶喜歡該電影,值為True。
# Not all reviews are favourable! Our goal is "other recommended books", so we only want favourable reviews all_ratings["Favorable"] = all_ratings["Rating"] > 3 all_ratings[10:15]可以看到數據發生了新的變化:
我們可以看到userID為1的用戶給5部電影打分的情況:
從數據集中選取一部分數據用作訓練集,這能有效減少搜索空間,提升Apriori算法的速度。我們取UserID為前200的用戶的打分數據,然后只取Favorite的部分
ratings = all_ratings[all_ratings['UserID'].isin(range(200))] # We start by creating a dataset of each user's favourable reviews favorable_ratings = ratings[ratings["Favorable"]] favorable_ratings[:5]在生成項集時,需要搜索用戶喜歡的電影。因此,接下來,我們需要知道每個用戶各喜歡哪些電影,按照User ID進行分組,并遍歷每個用戶看過的每一部電影。
# We are only interested in the reviewers who have more than one review favorable_reviews_by_users = dict((k, frozenset(v.values)) for k, v in favorable_ratings.groupby("UserID")["MovieID"]) len(favorable_reviews_by_users)輸出結果是:199
這里介紹一下pandas的groupby技術:
groupby的作用就是拆分合并,或者說的更具體一點是(分組、應用、聚合)。這里我們把favorable_ratings中的數據中的“UserID”和“MovieID”拿出來然后分組聚合后添加到新的字典favorable_reviews_by_user中
frozenset是不可變集合的意思,一旦創建就不能修改。另外,集合比列表速度快。
我們可以用下面代碼查看最受用戶歡迎的5部電影:
# Find out how many movies have favourable ratings num_favorable_by_movie = ratings[["MovieID", "Favorable"]].groupby("MovieID").sum() num_favorable_by_movie.sort_values("Favorable", ascending=False)[:5]
這段代碼的第一行表示的將ratings中的“MovieID”和“Favorable”拿出來重新分組,然后按照“MovieID”進行聚合,也就是說有相同的“MovieID”放在一起。之后用sum把Favorable為True的進行累加。
第二行用sort_values方法對列進行排序,ascending(上升)=False是按照降序排序,(即輸出排名最高的前5個)。ascending默認是True
Apriori算法實現
我們把發現的頻繁項集保存到以項集長度為鍵的字典中,便于根據長度查找,這樣就可以找 到最新發現的頻繁項集。
我們還需要確定項集要成為頻繁項集所需的最小支持度。這個值需要根據數據集的具體情況 來設定,可自行嘗試其他值,建議每次只改動10個百分點,即使這樣你可能也會發現算法運行時 間變動很大!
我們先來實現Apriori算法的第一步,為每一部電影生成只包含它自己的項集,檢測它是否夠頻繁。電影編號使用frozenset,后面要用到集合操作。此外,它們也可以用作字典的鍵(普通集合不可以)
import sys frequent_itemsets = {} # itemsets are sorted by length min_support = 50# k=1 candidates are the isbns with more than min_support favourable reviews frequent_itemsets[1] = dict((frozenset((movie_id,)), row["Favorable"])for movie_id, row in num_favorable_by_movie.iterrows()if row["Favorable"] > min_support)接著,用一個函數來實現發現新的頻繁項集,創建超集,檢測頻繁程度。
from collections import defaultdictdef find_frequent_itemsets(favorable_reviews_by_users, k_1_itemsets, min_support):#進行字典初始化counts = defaultdict(int)# 遍歷所有用戶和打分情況for user, reviews in favorable_reviews_by_users.items():# 遍歷前面找出的項集,判斷它們是否是當前評分項集的子集。# 如果是,表明用戶已經 為子集中的電影打過分for itemset in k_1_itemsets:if itemset.issubset(reviews):# 遍歷用戶打過分卻沒有出現在項集里的電影,用它生成超集,更新該項集的計數for other_reviewed_movie in reviews - itemset:current_superset = itemset | frozenset((other_reviewed_movie,))counts[current_superset] += 1# 最后檢測達到支持度要求的項集,看它的頻繁程度夠不夠,并返回其中的頻繁項集。 return dict([(itemset, frequency) for itemset, frequency in counts.items() if frequency >= min_support])創建循環,運行Apriori算法,存儲算法運行過程中發現的新項集。循環體中,k表示即將發 現的頻繁項集的長度,用鍵k - 1可以從frequent_itemsets字典中獲取剛發現的頻繁項集。新 發現的頻繁項集以長度為鍵,將其保存到字典中。
如果在上述循環中沒能找到任何新的頻繁項集,就跳出循環(輸出信息,告知我們沒能找到 長度為k的頻繁項集)。
如果確實找到了頻繁項集,我們也讓程序輸出信息,告知我們它會再次運行。因為算法運行 時間很長,所以每隔一段時間輸出一下狀態是很有必要的!
最后,循環結束,我們對只有一個元素的項集不再感興趣——它們對生成關聯規則沒有用 處——生成關聯規則至少需要兩個項目。刪除長度為1的項集。
sys.stdout.flush() for k in range(2, 20):# Generate candidates of length k, using the frequent itemsets of length k-1# Only store the frequent itemsetscur_frequent_itemsets = find_frequent_itemsets(favorable_reviews_by_users, frequent_itemsets[k-1],min_support)if len(cur_frequent_itemsets) == 0:print("Did not find any frequent itemsets of length {}".format(k))sys.stdout.flush()breakelse:print("I found {} frequent itemsets of length {}".format(len(cur_frequent_itemsets), k))#print(cur_frequent_itemsets)sys.stdout.flush()frequent_itemsets[k] = cur_frequent_itemsets # We aren't interested in the itemsets of length 1, so remove those del frequent_itemsets[1]用sys.stdout.flush()方法,確保代碼還在運行時,把緩沖區內容輸出 到終端。有時,在位于筆記本文件特定格子的大型循環中,代碼結束運行前不會 輸出,用flush方法可以保證及時輸出。但是,該方法不宜過多使用——flush 操作(輸出也是)所帶來的計算開銷會拖慢程序的運行速度。
抽取關聯規則
最后一步我們要抽取關聯規則。頻繁項集是一組達到最小支持度的項目,而關聯規則包含了前提和結論。我們定義關聯規則如下:如果一個用戶喜歡前提中的所有電影,那么他也會結論中的電影。
下面代碼通過遍歷不同長度的頻繁項集,來為每個項集生成關聯規則。
# Now we create the association rules. First, they are candidates until the confidence has been tested# 通過遍歷不同長度的頻繁項集,為每個項集生成規則 candidate_rules = [] for itemset_length, itemset_counts in frequent_itemsets.items():for itemset in itemset_counts.keys():# 遍歷項集中的每一部電影,把它作為結論。項集中的其他電影作為前提,用前提和結 論組成備選規則for conclusion in itemset:premise = itemset - set((conclusion,))candidate_rules.append((premise, conclusion)) print(candidate_rules[:5])輸出結果如下:
[(frozenset({79}), 258), (frozenset({258}), 79), (frozenset({50}), 64), (frozenset({64}), 50), (frozenset({127}), 181)]
計算置信度
接下來,計算每條規則的置信度。
首先創造兩個字典,用來存儲規則應驗(正例)和規則不適用(反例)的次數。
# Now, we compute the confidence of each of these rules. This is very similar to what we did in chapter 1 correct_counts = defaultdict(int) incorrect_counts = defaultdict(int) for user, reviews in favorable_reviews_by_users.items():for candidate_rule in candidate_rules:premise, conclusion = candidate_rule# 看用戶是否喜歡前提中的所有電影if premise.issubset(reviews):# 符合前提條件,看用戶是否喜歡結論中的電影。如果是的話,適用規則,反之,規則不適用if conclusion in reviews:correct_counts[candidate_rule] += 1else:incorrect_counts[candidate_rule] += 1 rule_confidence = {candidate_rule: correct_counts[candidate_rule] / float(correct_counts[candidate_rule] + incorrect_counts[candidate_rule])for candidate_rule in candidate_rules}對置信度進行排序后輸出置信度最高的前五條規則:
from operator import itemgetter sorted_confidence = sorted(rule_confidence.items(), key=itemgetter(1), reverse=True)for index in range(5):print("Rule #{0}".format(index + 1))(premise, conclusion) = sorted_confidence[index][0]print("Rule: If a person recommends {0} they will also recommend {1}".format(premise, conclusion))print(" - Confidence: {0:.3f}".format(rule_confidence[(premise, conclusion)]))print("")結果如下:
Rule #1
Rule: If a person recommends frozenset({64, 56, 98, 50, 7}) they will also recommend 174
- Confidence: 1.000
Rule #2
Rule: If a person recommends frozenset({98, 100, 172, 79, 50, 56}) they will also recommend 7
- Confidence: 1.000
。。。。。。
顯然這樣結果中只有電影編號,沒有顯示電影名稱,很不好。下載的數據集中的u.items 文件里存儲了電影名稱和編號(還有體裁等信息)。
用pandas從u.items文件加載電影名稱信息。關于該文件和類別的更多信息請見數據集中的 README文件。u.items文件為CSV格式,但是用豎線分隔數據。讀取時需要指定分隔符,設置表頭和編碼格式。每一列的名稱是從README文件中找到的。
# Even better, we can get the movie titles themselves from the dataset movie_name_filename = os.path.join(data_folder, "u.item") movie_name_data = pd.read_csv(movie_name_filename, delimiter="|", header=None, encoding = "mac-roman") movie_name_data.columns = ["MovieID", "Title", "Release Date", "Video Release", "IMDB", "<UNK>", "Action", "Adventure","Animation", "Children's", "Comedy", "Crime", "Documentary", "Drama", "Fantasy", "Film-Noir","Horror", "Musical", "Mystery", "Romance", "Sci-Fi", "Thriller", "War", "Western"]創建一個電影名稱獲取函數:
def get_movie_name(movie_id):# 在數據框movie_name_data中查找電影編號,找到后,只返回電影名稱列的數據title_object = movie_name_data[movie_name_data["MovieID"] == movie_id]["Title"] # 我們用title_object的values屬性獲取電影名稱(不是存儲在title_object中的 Series對象)。我們只對第一個值感興趣title = title_object.values[0]return titleget_movie_name(4)結果如下:’Get Shorty (1995)’
調整之后,就可以輸出比較不錯的結果了
for index in range(5):print("Rule #{0}".format(index + 1))(premise, conclusion) = sorted_confidence[index][0]premise_names = ", ".join(get_movie_name(idx) for idx in premise)conclusion_name = get_movie_name(conclusion)print("Rule: If a person recommends {0} they will also recommend {1}".format(premise_names, conclusion_name))print(" - Confidence: {0:.3f}".format(rule_confidence[(premise, conclusion)]))print("")輸出結果如下:
Rule #1
Rule: If a person recommends Shawshank Redemption, The (1994), Pulp Fiction (1994), Silence of the Lambs, The (1991), Star Wars (1977), Twelve Monkeys (1995) they will also recommend Raiders of the Lost Ark (1981)
- Confidence: 1.000
Rule #2
Rule: If a person recommends Silence of the Lambs, The (1991), Fargo (1996), Empire Strikes Back, The (1980), Fugitive, The (1993), Star Wars (1977), Pulp Fiction (1994) they will also recommend Twelve Monkeys (1995)
- Confidence: 1.000
……
看起來效果就好很多
參考資料
《Python數據挖掘入門與實踐》
總結
以上是生活随笔為你收集整理的用亲和性分析方法推荐电影的全部內容,希望文章能夠幫你解決所遇到的問題。