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

哥不是小蘿莉發表於2020-10-30

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大資料探勘從入門到進階實戰》,喜歡的朋友或同學, 可以在公告欄那裡點選購買連結購買博主的書進行學習,在此感謝大家的支援。關注下面公眾號,根據提示,可免費獲取書籍的教學視訊。

相關文章