協同過濾在推薦系統中的應用

1.概述

前面的博客介紹過如何構建一個推薦系統,以及簡要的介紹了協同過濾的實現。本篇博客,筆者將介紹協同過濾在推薦系統的應用。推薦系統是大數據和機器學習中最常見、最容易理解的應用之一。其實,在日常的生活當中,我們會頻繁的遇到推薦的場景 ,比如你在電商網站購買商品、使用視頻App觀看視頻、在手機上下載各種遊戲等,這些都是使用了推薦技術來個性化你想要的內容和物品。

2.內容

本篇博客將通過以下方式來介紹,通過建立協同過濾模型,利用訂單數據來想用戶推薦預期的物品。步驟如下:

  • 轉換和規範化數據
  • 訓練模型
  • 評估模型性能
  • 選擇最佳模型

2.1 技術選型

完成本篇博客所需要的技術使用Python和機器學習Turicreate來實現。Python所需要的依賴庫如下:

  • pandas和numpy:用於操作數據
  • turicreate:用於進行模型選擇與評估
  • sklearn:用於對數據進行封裝,包括回歸、降維、分類、聚類等。

2.2 加載數據

本次演示的數據源,包含如下:

  • customer_id.csv:列出1000個客戶ID作為輸出推薦;
  • customer_data.csv:物品數據源集。

加載Python依賴庫,實現代碼如下:

import pandas as pd
import numpy as np
import time
import turicreate as tc
from sklearn.model_selection import train_test_split

查看數據集,實現代碼如下:

customers = pd.read_csv('customer_id.csv') 
transactions = pd.read_csv('customer_data.csv')
print(customers.head())
print(transactions.head())

預覽結果如下:

2.3 數據準備

將上述csv中的數據集中,將products列中的每個物品列表分解成行,並計算用戶購買的產品數量。

2.3.1 使用用戶、物品和目標字段創建數據

  • 此表將作為稍後建模的輸入
  • 在本次案例中,使用customerId、productId和purchase_count字段

實現代碼如下:

transactions['products'] = transactions['products'].apply(lambda x: [int(i) for i in x.split('|')])
data = pd.melt(transactions.set_index('customerId')['products'].apply(pd.Series).reset_index(), 
             id_vars=['customerId'],
             value_name='products') \
    .dropna().drop(['variable'], axis=1) \
    .groupby(['customerId', 'products']) \
    .agg({'products': 'count'}) \
    .rename(columns={'products': 'purchase_count'}) \
    .reset_index() \
    .rename(columns={'products': 'productId'})
data['productId'] = data['productId'].astype(np.int64)
print(data.shape)
print(data.head())

預覽截圖如下:

2.3.1 創建虛擬對象

  • 標識用戶是否購買該商品的虛擬人;
  • 如果一個人購買了一個物品,那麼標記purchase_dummy字段為值為1;
  • 可能會有疑問,為什麼需要創建一個虛擬人而不是將其規範化,對每個用戶的購買數量進行規範化是不可行的,因為用戶的購買頻率在現實情況中可能不一樣;但是,我們可以根據所有用戶的購買頻率對商品進行規範化。

實現代碼如下:

def create_data_dummy(data):
    data_dummy = data.copy()
    data_dummy['purchase_dummy'] = 1
    return data_dummy
data_dummy = create_data_dummy(data)
print(data_dummy.head())

預覽結果如下:

 2.3.2 規範化物品

  • 我們通過首先創建一個用戶矩陣來規範每個用戶的購買頻率。

實現代碼如下:

df_matrix = pd.pivot_table(data, values='purchase_count', index='customerId', columns='productId')
print(df_matrix.head())

預覽結果如下:

 矩陣規範化實現代碼如下:

df_matrix_norm = (df_matrix-df_matrix.min())/(df_matrix.max()-df_matrix.min())
print(df_matrix_norm.head())

預覽結果如下:

創建一個表作為模型的輸入,實現代碼如下:

d = df_matrix_norm.reset_index() 
d.index.names = ['scaled_purchase_freq']
data_norm = pd.melt(d, id_vars=['customerId'], value_name='scaled_purchase_freq').dropna()
print(data_norm.shape)
print(data_norm.head())

預覽結果如下:

上述步驟可以組合成下面定義的函數,實現代碼如下 :

def normalize_data(data):
    df_matrix = pd.pivot_table(data, values='purchase_count', index='customerId', columns='productId')
    df_matrix_norm = (df_matrix-df_matrix.min())/(df_matrix.max()-df_matrix.min())
    d = df_matrix_norm.reset_index()
    d.index.names = ['scaled_purchase_freq']
    return pd.melt(d, id_vars=['customerId'], value_name='scaled_purchase_freq').dropna()

上面,我們規範化了用戶的購買歷史記錄,從0到1(1是一個物品的最多購買次數,0是該物品的0個購買計數)。

2.4 拆分用於訓練用的數據集

  • 將數據分割成訓練集和測試集是評估預測建模的一個重要部分,在這種情況下使一個協作過濾模型。通過,我們使用較大部分的數據用於訓練,而較小的部分用於測試;
  • 我們將訓練集和測試集佔比拆分為80% : 20%;
  • 訓練部分將用於開發預測模型,而另外一部分用於評估模型的性能。

拆分函數實現如下:

def split_data(data):
    '''
    Splits dataset into training and test set.
    
    Args:
        data (pandas.DataFrame)
        
    Returns
        train_data (tc.SFrame)
        test_data (tc.SFrame)
    '''
    train, test = train_test_split(data, test_size = .2)
    train_data = tc.SFrame(train)
    test_data = tc.SFrame(test)
    return train_data, test_data

現在我們有了是三個數據集,分別是購買計數、購買虛擬數據和按比例的購買計數,這裡我們將每個數據集分開進行建模,實現代碼如下:

train_data, test_data = split_data(data)
train_data_dummy, test_data_dummy = split_data(data_dummy)
train_data_norm, test_data_norm = split_data(data_norm)
print(train_data)

這裡打印訓練結果數據,預覽結果如下:

2.5 使用Turicreate庫來構建模型

在運行更加複雜的方法(比如協同過濾)之前,我們應該運行一個基線模型來比較和評估模型。由於基線通常使用一種非常簡單的方法,因此如果在這種方法之外使用的技術顯示出相對較好的準確性和複雜性,則應該選擇這些技術。

Baseline Model是機器學習領域的一個術語,簡而言之,就是使用最普遍的情況來做結果預測。比如,猜硬幣遊戲,最簡單的策略就是一直選擇正面或者反面,這樣從預測的模型結果來看,你是有50%的準確率的。

一種更複雜但是更常見的預測購買商品的方法就是協同過濾。下面,我們首先定義要在模型中使用的變量,代碼如下:

# constant variables to define field names include:
user_id = 'customerId'
item_id = 'productId'
users_to_recommend = list(customers[user_id])
n_rec = 10 # number of items to recommend
n_display = 30 # to display the first few rows in an output dataset

Turicreate使我們非常容易去調用建模技術,因此,定義所有模型的函數如下:

def model(train_data, name, user_id, item_id, target, users_to_recommend, n_rec, n_display):
    if name == 'popularity':
        model = tc.popularity_recommender.create(train_data, 
                                                    user_id=user_id, 
                                                    item_id=item_id, 
                                                    target=target)
    elif name == 'cosine':
        model = tc.item_similarity_recommender.create(train_data, 
                                                    user_id=user_id, 
                                                    item_id=item_id, 
                                                    target=target, 
                                                    similarity_type='cosine')
    elif name == 'pearson':
        model = tc.item_similarity_recommender.create(train_data, 
                                                    user_id=user_id, 
                                                    item_id=item_id, 
                                                    target=target, 
                                                    similarity_type='pearson')
    recom = model.recommend(users=users_to_recommend, k=n_rec)
    recom.print_rows(n_display)
    return model

2.5.1 使用Popularity Model作為Baseline

  • Popularity Model採用最受歡迎的物品進行推薦,這些物品在用戶中銷量是最高的;
  • 訓練數據用於模型選擇。

購買計數實現代碼如下:

name = 'popularity'
target = 'purchase_count'
popularity = model(train_data, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
print(popularity)

截圖如下:

購買虛擬人代碼如下:

name = 'popularity'
target = 'purchase_dummy'
pop_dummy = model(train_data_dummy, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
print(pop_dummy)

截圖如下:

 按比例購買計數實現代碼如下:

name = 'popularity'
target = 'scaled_purchase_freq'
pop_norm = model(train_data_norm, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
print(pop_norm)

截圖如下:

2.6 協同過濾模型

根據用戶如何在協作購買物品的基礎上推薦相似的物品。例如,如果用戶1和用戶2購買了類似的物品,比如用戶1購買的X、Y、Z,用戶2購買了X、Y、Y,那麼我們可以向用戶2推薦物品Z。

2.6.1 原理

  • 創建一個用戶-物品矩陣,其中索引值表示唯一的用戶ID,列值表示唯一的物品ID;
  • 創建相似矩陣,這個作用是用於計算一個物品和另外一個物品的相似度,這裡我們使用餘弦相似度或者皮爾森相似度。
  1. 要計算物品X和物品Y之間的相似性,需要查看對這兩個物品進行評級的所有用戶,例如,用戶1和用戶2都對物品X和Y進行了評級
  2. 然後,我們在(用戶1,用戶2)的用戶空間中創建兩個物品向量,V1表示物品X,V2表示物品Y,然後找出這些向量之間的餘弦值。餘弦值為1的零角度或者重疊向量表示完全相似(或者每個用戶,所有物品都有相同的評級),90度的角度意味着餘弦為0或者沒有相似性。

2.6.2 餘弦相似度

公式如下:

 

 

 購買計數代碼如下:

name = 'cosine'
target = 'purchase_count'
cos = model(train_data, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
print(cos)

截圖如下:

 

 購買虛擬人代碼如下:

name = 'cosine'
target = 'purchase_dummy'
cos_dummy = model(train_data_dummy, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
print(cos_dummy)

截圖如下:

 

 按比例購買計數,實現代碼如下:

name = 'cosine' 
target = 'scaled_purchase_freq' 
cos_norm = model(train_data_norm, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
print(cos_norm)

截圖如下:

 2.6.3 皮爾森相似度

  • 相似性是兩個向量之間的皮爾遜係數。

  • 計算公式如下:

 購買計數實現代碼:

name = 'pearson'
target = 'purchase_count'
pear = model(train_data, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
print(pear)

截圖如下:

 購買虛擬人實現代碼:

name = 'pearson'
target = 'purchase_dummy'
pear_dummy = model(train_data_dummy, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
print(pear_dummy)

截圖如下:

 按比例購買計數:

name = 'pearson'
target = 'scaled_purchase_freq'
pear_norm = model(train_data_norm, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
print(pear_norm)

截圖如下:

 2.7 模型訓練

在評價推薦引擎時,我們可以使用RMSE和精準召回的概念。

  • RMSE(Root Mean Squared Errors)
  1. 測量預測值的誤差;
  2. RMSE值越小,結果越好。
  • 召回
  1. 用戶購買的物品中實際推薦的比例是多少;
  2. 如果一個用戶購買了5種物品,而推薦列表決定展示其中的3種,那麼召回率為60%。
  • 準確率
  1. 在所有推薦的物品中,有多少用戶真正喜歡;
  2. 如果向用戶推薦了5種物品,而用戶購買了其中的4種,那麼準確率為80%。

為何召回和準確度如此重要呢?

  • 考慮一個案例,我們推薦所有的物品。這樣我們的用戶一定會涵蓋他們喜歡和購買的物品。這種情況下,我們的召回率為100%,這樣是否意味着我們的模型是最好的呢?
  • 我們必須考慮準確率,如果我們推薦300件物品,但用戶喜歡,而且購買了3件,那麼準確率是1%,這個非常低的準確率表明,儘管他們的召回率很高,但是這個模型並不是很好。
  • 因此,我們最終的目標是優化召回率和準確率,讓他們儘可能的接近1。

下面,我們為模型求值創建初識可調用變量,實現代碼如下:

models_w_counts = [popularity, cos, pear]
models_w_dummy = [pop_dummy, cos_dummy, pear_dummy]
models_w_norm = [pop_norm, cos_norm, pear_norm]
names_w_counts = ['Popularity Model on Purchase Counts', 'Cosine Similarity on Purchase Counts', 'Pearson Similarity on Purchase Counts']
names_w_dummy = ['Popularity Model on Purchase Dummy', 'Cosine Similarity on Purchase Dummy', 'Pearson Similarity on Purchase Dummy']
names_w_norm = ['Popularity Model on Scaled Purchase Counts', 'Cosine Similarity on Scaled Purchase Counts', 'Pearson Similarity on Scaled Purchase Counts']

然後,讓我們比較一下我們基於RMSE和精準召回特性構建的所有模型,代碼如下:

eval_counts = tc.recommender.util.compare_models(test_data, models_w_counts, model_names=names_w_counts)
eval_dummy = tc.recommender.util.compare_models(test_data_dummy, models_w_dummy, model_names=names_w_dummy)
eval_norm = tc.recommender.util.compare_models(test_data_norm, models_w_norm, model_names=names_w_norm)

評估結果輸出如下:

3.總結

  • 協同過濾:我們可以看到,協同過濾算法比Popularity Model更加適合購買數量。實際上,Popularity Model並沒有提供任何個性化設置,因為它只向每個用戶提供相同的推薦項目列表;
  • 精準召回:綜上所述,我們可以看到購買數量 > 購買虛擬 > 標準化購買計數的精準率和召回率。然而,由於標準化購買數據的推薦分數為0且不變,所以我們選擇了虛擬的,實際上,虛擬模型和標準模型化數據模型的RMSE差別不大;
  • RMSE:由於使用皮爾森相似度的RMSE比餘弦相似度結果高,所以我們選擇較小的均方誤差模型,在這種情況下,就是選擇餘弦相似度模型。

完成實例代碼如下:

import pandas as pd
import numpy as np
import time
import turicreate as tc
from sklearn.model_selection import train_test_split

customers = pd.read_csv('customer_id.csv') 
transactions = pd.read_csv('customer_data.csv')
# print(customers.head())
# print(transactions.head())
transactions['products'] = transactions['products'].apply(lambda x: [int(i) for i in x.split('|')])
data = pd.melt(transactions.set_index('customerId')['products'].apply(pd.Series).reset_index(), 
             id_vars=['customerId'],
             value_name='products') \
    .dropna().drop(['variable'], axis=1) \
    .groupby(['customerId', 'products']) \
    .agg({'products': 'count'}) \
    .rename(columns={'products': 'purchase_count'}) \
    .reset_index() \
    .rename(columns={'products': 'productId'})
data['productId'] = data['productId'].astype(np.int64)
# print(data.shape)
# print(data.head())

def create_data_dummy(data):
    data_dummy = data.copy()
    data_dummy['purchase_dummy'] = 1
    return data_dummy
data_dummy = create_data_dummy(data)
# print(data_dummy.head())

df_matrix = pd.pivot_table(data, values='purchase_count', index='customerId', columns='productId')
# print(df_matrix.head())

df_matrix_norm = (df_matrix-df_matrix.min())/(df_matrix.max()-df_matrix.min())
# print(df_matrix_norm.head())

# create a table for input to the modeling  
d = df_matrix_norm.reset_index() 
d.index.names = ['scaled_purchase_freq']
data_norm = pd.melt(d, id_vars=['customerId'], value_name='scaled_purchase_freq').dropna()
# print(data_norm.shape)
# print(data_norm.head())

def normalize_data(data):
    df_matrix = pd.pivot_table(data, values='purchase_count', index='customerId', columns='productId')
    df_matrix_norm = (df_matrix-df_matrix.min())/(df_matrix.max()-df_matrix.min())
    d = df_matrix_norm.reset_index()
    d.index.names = ['scaled_purchase_freq']
    return pd.melt(d, id_vars=['customerId'], value_name='scaled_purchase_freq').dropna()

def split_data(data):
    '''
    Splits dataset into training and test set.
    
    Args:
        data (pandas.DataFrame)
        
    Returns
        train_data (tc.SFrame)
        test_data (tc.SFrame)
    '''
    train, test = train_test_split(data, test_size = .2)
    train_data = tc.SFrame(train)
    test_data = tc.SFrame(test)
    return train_data, test_data

train_data, test_data = split_data(data)
train_data_dummy, test_data_dummy = split_data(data_dummy)
train_data_norm, test_data_norm = split_data(data_norm)
# print(train_data)

# constant variables to define field names include:
user_id = 'customerId'
item_id = 'productId'
users_to_recommend = list(customers[user_id])
n_rec = 10 # number of items to recommend
n_display = 30 # to display the first few rows in an output dataset

def model(train_data, name, user_id, item_id, target, users_to_recommend, n_rec, n_display):
    if name == 'popularity':
        model = tc.popularity_recommender.create(train_data, 
                                                    user_id=user_id, 
                                                    item_id=item_id, 
                                                    target=target)
    elif name == 'cosine':
        model = tc.item_similarity_recommender.create(train_data, 
                                                    user_id=user_id, 
                                                    item_id=item_id, 
                                                    target=target, 
                                                    similarity_type='cosine')
    elif name == 'pearson':
        model = tc.item_similarity_recommender.create(train_data, 
                                                    user_id=user_id, 
                                                    item_id=item_id, 
                                                    target=target, 
                                                    similarity_type='pearson')
    recom = model.recommend(users=users_to_recommend, k=n_rec)
    recom.print_rows(n_display)
    return model

name = 'popularity'
target = 'purchase_count'
popularity = model(train_data, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
# print(popularity)

name = 'popularity'
target = 'purchase_dummy'
pop_dummy = model(train_data_dummy, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
# print(pop_dummy)

name = 'popularity'
target = 'scaled_purchase_freq'
pop_norm = model(train_data_norm, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
# print(pop_norm)

name = 'cosine'
target = 'purchase_count'
cos = model(train_data, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
# print(cos)

name = 'cosine'
target = 'purchase_dummy'
cos_dummy = model(train_data_dummy, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
# print(cos_dummy)

name = 'cosine' 
target = 'scaled_purchase_freq' 
cos_norm = model(train_data_norm, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
# print(cos_norm)

name = 'pearson'
target = 'purchase_count'
pear = model(train_data, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
# print(pear)

name = 'pearson'
target = 'purchase_dummy'
pear_dummy = model(train_data_dummy, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
# print(pear_dummy)

name = 'pearson'
target = 'scaled_purchase_freq'
pear_norm = model(train_data_norm, name, user_id, item_id, target, users_to_recommend, n_rec, n_display)
# print(pear_norm)

models_w_counts = [popularity, cos, pear]
models_w_dummy = [pop_dummy, cos_dummy, pear_dummy]
models_w_norm = [pop_norm, cos_norm, pear_norm]
names_w_counts = ['Popularity Model on Purchase Counts', 'Cosine Similarity on Purchase Counts', 'Pearson Similarity on Purchase Counts']
names_w_dummy = ['Popularity Model on Purchase Dummy', 'Cosine Similarity on Purchase Dummy', 'Pearson Similarity on Purchase Dummy']
names_w_norm = ['Popularity Model on Scaled Purchase Counts', 'Cosine Similarity on Scaled Purchase Counts', 'Pearson Similarity on Scaled Purchase Counts']

eval_counts = tc.recommender.util.compare_models(test_data, models_w_counts, model_names=names_w_counts)
eval_dummy = tc.recommender.util.compare_models(test_data_dummy, models_w_dummy, model_names=names_w_dummy)
eval_norm = tc.recommender.util.compare_models(test_data_norm, models_w_norm, model_names=names_w_norm)

# Final Output Result 
# final_model = tc.item_similarity_recommender.create(tc.SFrame(data_dummy), user_id=user_id, item_id=item_id, target='purchase_dummy', similarity_type='cosine')
# recom = final_model.recommend(users=users_to_recommend, k=n_rec)
# recom.print_rows(n_display)

# df_rec = recom.to_dataframe()
# print(df_rec.shape)
# print(df_rec.head())

View Code

4.結束語

這篇博客就和大家分享到這裡,如果大家在研究學習的過程當中有什麼問題,可以加群進行討論或發送郵件給我,我會盡我所能為您解答,與君共勉!

另外,博主出書了《Kafka並不難學》和《Hadoop大數據挖掘從入門到進階實戰》,喜歡的朋友或同學, 可以在公告欄那裡點擊購買鏈接購買博主的書進行學習,在此感謝大家的支持。關注下面公眾號,根據提示,可免費獲取書籍的教學視頻。