推薦系統入門之協同過濾

[TOC]python

協同過濾

介紹

協同過濾包括基於物品的協同過濾和基於用戶的協同過濾兩種不一樣方式。算法

  • 基於物品的協同過濾:給用戶推薦與他以前購買物品類似的其餘物品。
  • 基於用戶的協同過濾:給用戶推薦與他興趣類似的其餘用戶購買的物品

理解類似度

在上面簡單的介紹了協同過濾的機率,咱們發現不管是基於物品的協同過濾仍是基於用戶的協同過濾,都包含一個共同的關鍵詞,就是類似度。類似度衡量了用戶與用戶之間,物品與物品之間的類似程度。在協同過濾中起着相當重要的做用。app

在計算類似度以前,咱們一般都會將物品或者用戶用一個向量來表示,好比下表表示了4個用戶的購買行爲,1表示購買,0表示沒有。則用戶1能夠用向量$[1, 0, 1, 0]$來表示。框架

商品1 商品2 商品3 商品4
用戶1 1 0 1 0
用戶2 0 1 0 0
用戶3 1 1 0 0
用戶4 1 1 0 1
用戶5 0 0 1

常見的類似度計算方法主要有dom

  1. 餘弦類似度

$i$和$j$表示兩個不一樣的用戶向量,則它們之間的餘弦距離能夠用下式來表示爲學習

$$ Simularity(i,j)=\frac{i\cdot j}{||i||\cdot ||j||} $$測試

餘弦距離表徵的是向量之間的夾角大小,夾角越小,餘弦距離約小。spa

  1. 皮爾遜相關係數

相比於餘弦距離,皮爾遜相關係數使用用戶向量的平均值進行了修正,減小了偏置的影響。設計

$$ Simularity(i,j)=\frac{\sum_{p\in P}(R_{i,p}-\bar{R_i})\cdot (R_{j,p}-\bar{R_j})}{\sqrt{\sum_{p\in P}(R_{i,p}-\bar{R_i})^2}\sqrt{\sum_{p\in P}(R_{j,p}-\bar{R_j})^2}} $$日誌

$P$表示全部的物品,$R_{i,p}$表示用戶$i$對物品$p$的評分。$\\bar{R_i}$表示用戶$i$對全部物品評分的平均值。

  1. 基於皮爾遜係數的其餘思路
    上面的皮爾遜相關係數中,使用了用戶的平均評分對類似度進行了修正,這裏一樣對類似度進行了修正,只不過使用的是物品的平均評分。

    $$ Simularity(i,j)=\frac{\sum_{p\in P}(R_{i,p}-\bar{R_p})\cdot (R_{j,p}-\bar{R_p})}{\sqrt{\sum_{p\in P}(R_{i,p}-\bar{R_p})^2}\sqrt{\sum_{p\in P}(R_{j,p}-\bar{R_p})^2}} $$

    $\\bar{R_p}$表示物品$p$的全部評分的平均值。

在類似用戶的計算過程當中,任何合理的類似度計算過程均可以做爲類似用戶計算的標準。

最終結果的排序

基於用戶的協同過濾(UserCF)

有了用戶之間的類似度,咱們就能夠根據類似度來進行排序,取出TopN的最類似的用戶。根據這些類似用戶的已有評分,咱們就可以對目標用戶的偏好進行預測了。

目標用戶的評價預測一般使用的是用戶之間的類似度加權平均

$$ R_{up}=\frac{\sum_{s\in S}(W_{us}\cdot R_{sp})}{\sum_{s\in S}W_{us}} $$

$R_{up}$表示目標用戶$u$對商品$p$的評分,$W_{us}$表示目標用戶$u$和用戶$s$的類似度,$S$表示與用戶$u$類似度最高的TopN用戶列表。

獲得目標用戶對於每一個商品的評分預測後,就能夠對全部的評分預測進行排序,最終決定要向用戶推薦什麼物品了。

基於用戶的協同過濾思路很簡單,就是類似的用戶應該具備相似的興趣偏好,但在技術上存在一些問題

  1. 互聯網平臺上,用戶數每每遠遠大於物品數,這使得每次類似度計算須要耗費大量時間和資源,並且會隨着用戶的增加,所需的資源和時間呈現$n^2$的增加,這一般是沒法接受的。
  2. 用戶的行爲一般是稀疏的,找到與目標用戶類似的用戶是十分困難的,而且不少狀況下找到的類似用戶並沒有關係。這使得UserCF很難應用在正樣本獲取困難的推薦場景,好比說大件商品或奢侈品等等。

基於物品的協同過濾(ItemCF)

因爲上面提到的UserCF中的兩個缺陷,在最初的推薦系統中,都沒有采用UserCF,而是採用了ItemCF。

ItemCF是基於物品類似度進行推薦的協同推薦算法,經過計算物品之間的類似度來獲得物品之間的類似矩陣,而後再找到用戶的歷史正反饋物品的類似物品進行排序和推薦。
ItemCF的步驟以下:

  1. 根據全部用戶的歷史數據,構建以用戶(假設用戶數爲m)爲行座標,物品(假設物品數爲n)爲列座標的$m\\times n$維的共現矩陣。
  2. 計算共現矩陣列向量之間的類似度,獲得一個$n\\times n$的一個類似度矩陣。
  3. 獲取用戶的歷史行爲數據中的正反饋物品列表
  4. 利用物品的類似度矩陣,針對目標用戶的歷史行爲中的正反饋物品,找出類似的TopK個物品,組成類似物品集合。
  5. 對類似物品集合,利用類似度分值進行排序,生成最終的推薦列表。

這裏,我用本身通俗的語言來理解它,首先,咱們根據共現矩陣($m \\times n$, m表示用戶數,n表示物品數)計算類似度矩陣,有了類似度矩陣,而後咱們根據用戶的歷史行爲數據獲得正反饋的物品列表(就是用戶的滿意的物品),好比說正反饋的物品列表爲[1, 3, 4]
那麼對於物品庫中的除正反饋物品外的其餘物品,都會計算出一個得分,計算公式以下所示

$$ R_{u,p}=\sum_{h\in H }(W_{h,p}R_{u,h}) $$

$W_{h,p}$表示物品$h$和物品$p$的類似度,$R_{u,h}$表示用戶$u$對物品$h$的評分, $H$表示用戶$u$的正反饋物品列表。

將計算出來的得分進行排序,最終獲得TopN的結果。

UserCF和ItemCF的應用場景

從新回顧一下UserCF和ItemCF的基本概念,一個是基於類似用戶,一個是基於類似物品。

因爲UserCF的固有特性,使其具備自然的社交屬性,對於社交類型的平臺,使用UserCF更容易發現興趣類似的好友。另外社交屬性爲熱點新聞的傳播一樣提供了自然的途徑,由於新聞自己的興趣點比較分散,相比用戶對於不一樣新聞的興趣偏好,新聞的實時性和熱點性顯得更爲重要。UserCF更容易用來發現熱點,跟蹤熱點。

ItemCF更適合興趣變化較爲穩定的應用,好比購物App等,用戶在一段時間內的興趣是相對固定的。

基於協同過濾的新聞推薦系統

這裏以天池平臺上的一個新聞推薦項目來學習推薦系統中的協同過濾算法。

賽題簡介

這次比賽是新聞推薦場景下的用戶行爲預測挑戰賽, 該賽題是以新聞APP中的新聞推薦爲背景, 目的是要求咱們根據用戶歷史瀏覽點擊新聞文章的數據信息預測用戶將來的點擊行爲, 即用戶的最後一次點擊的新聞文章, 這道賽題的設計初衷是引導你們瞭解推薦系統中的一些業務背景, 解決實際問題。

數據概況

該數據來自某新聞APP平臺的用戶交互數據,包括30萬用戶,近300萬次點擊,共36萬多篇不一樣的新聞文章,同時每篇新聞文章有對應的embedding向量表示。爲了保證比賽的公平性,從中抽取20萬用戶的點擊日誌數據做爲訓練集,5萬用戶的點擊日誌數據做爲測試集A,5萬用戶的點擊日誌數據做爲測試集B。具體數據表和參數, 你們能夠參考賽題說明。下面說一下拿到這樣的數據如何進行理解, 來有效的開展下一步的工做。

評價方式理解

理解評價方式, 咱們須要結合着最後的提交文件來看, 根據sample.submit.csv, 咱們最後提交的格式是針對每一個用戶, 咱們都會給出五篇文章的推薦結果,按照點擊機率從前日後排序。 而真實的每一個用戶最後一次點擊的文章只會有一篇的真實答案, 因此咱們就看咱們推薦的這五篇裏面是否有命中真實答案的。好比對於user1來講, 咱們的提交會是:

user1, article1, article2, article3, article4, article5.

假如article1就是真實的用戶點擊文章,也就是article1命中, 則s(user1,1)=1, s(user1,2-4)都是0, 若是article2是用戶點擊的文章, 則s(user,2)=1/2,s(user,1,3,4,5)都是0。也就是score(user)=命中第幾條的倒數。若是都沒中, 則score(user1)=0。 這個是合理的, 由於咱們但願的就是命中的結果儘可能靠前, 而此時分數正比如較高。

$$ score(user) = \sum_{k=1}^5 \frac{s(user, k)}{k} $$

假如article1就是真實的用戶點擊文章,也就是article1命中, 則s(user1,1)=1, s(user1,2-4)都是0, 若是article2是用戶點擊的文章, 則s(user,2)=1/2,s(user,1,3,4,5)都是0。也就是score(user)=命中第幾條的倒數。若是都沒中, 則score(user1)=0。 這個是合理的, 由於咱們但願的就是命中的結果儘可能靠前, 而此時分數正比如較高。

好了,在前面的基礎和對賽題有一個簡要的介紹以後,咱們開始思考如何使用代碼來實現它。

這裏只使用了基於物品的協同過濾進行文章的推薦。

首先導入一些必要的庫

import pandas as pd
import numpy as np
import os
import random
random.seed(0)
import collections
from tqdm.notebook import tqdm

首先簡單的查看一下數據。

data_path = 'data/train_click_log.csv'
df = pd.read_csv(data_path, sep=',')
df.head()

image.png

print('user count -> ', df['user_id'].unique().size)
print('article count -> ', df['click_article_id'].unique().size)
user count -> 200000
article count -> 31116

讀取數據

# 讀取全部用戶點擊數據
def get_all_user_click(root_dir, offline=True, n_sample=10000):
    """read user click data from file
    
    Params:
        root_dir(str): click data root directory
        
        offline(bool): denote current environment, only train click data will be used when in offline
        otherwise all click data will be used.
        
        n_sample(int): sample how many user from user click data, if used for test your model. 
        only valid when offline is True
        
    Returns:
        df(pandas.Dataframe): a dataframe after drop duplicate values.
    """
    
    df = pd.read_csv(os.path.join(root_dir, 'train_click_log.csv'), sep=',')
    if not offline:
        df = pd.concat([df, pd.read_csv(os.path.join(root_dir, 'test_click_log.csv'), sep=',')], axis=0)
    else:
        uni_vals = df['user_id'].unique()
        if uni_vals.size > n_sample:
            sampled_users = random.choices(uni_vals, k=n_sample)
            df = df.loc[df['user_id'].isin(sampled_users), :]
            
    df.dropduplicates(subset=['user_id', 'click_article_id', 'click_timestamp'], inplace=True)
    
    return df



# 創建一個字典,鍵爲用戶id,值爲他所點擊的文章列表
def df_to_item_time_dict(df):
    """get user item-time dictionary from dataframe
    
    Returns:
        a dict, it is like {user1: [(article_id, click_timestamp), ...], ...}
    """
    df = df.sort_values(by='click_timestamp')
    
    # 按照用戶id分組,並取出click_article_id和click_timestamp
    user_item_time_dicts = df.groupby('user_id')['click_article_id', 'click_timestamp']
    
    # group 以後的結果
    """
    user_id    click_article_id     click_timestamp
    xxx         xxx                 xxx
                xxx                 xxx
                xxx                 xxx

    xxx         xxx                 xxx
                xxx                 xxx
    
    ...
    """
    user_item_time_dicts = user_item_time_dicts.apply(lambda x: list(zip(x['click_article_id'], x['click_timestamp']))).reset_index()
    
    user_item_time_dicts = dict(zip(user_item_time_dicts['user_id'], user_item_time_dicts[0]))
    
    return user_item_time_dicts

# 獲得最熱門的n篇文章
def get_topn_articles(df, n):
    """get top n articles based on occur count
    """
    return df['click_article_id'].value_counts().index[:n]
# 計算類似度矩陣
def calc_sim_mat(user_item_time_dicts):
    """calculate item simularity matrix, here use dict to save silularity matrix
    
    Params:
        user_item_time_dicts(dict): {key: [(item, time)]}
    Returns:
        a dict, denote simularity matrix
    
    """
    item_sim_mat_dict = {}
    
    item_cnt_dict = collections.defaultdict(int)
    
    """
    {
        user1: [(article_id, click_timestamp), ...],
        user2: [],
        
    }
    """
    for user, item_time_list in tqdm(user_item_time_dicts):
        
        for item_i, _ in item_time_list:
            
            item_cnt_dict[item] += 1
            item_sim_mat_dict[item_i] = {}
            for item_j, _ in item_time_list:
                
                if item_j not in item_sim_mat_dict[item_i]:
                    item_sim_mat_dict[item_i][item_j] = 0
                    
                # 用戶已點擊過的文章不會被推薦,即得分爲0
                if item_i == item_j:
                    continue
                    
                #  item_sim_mat_dict[item_i][item_j] ----> 文章j和文章i共同出如今一個用戶的點擊列表的次數
                item_sim_mat_dict[item_i][item_j] += 1
                
            # 下降熱門文章的權重           
            item_sim_mat_dict[item_i][item_j] /= np.log(len(item_time_list) + 1) 
    
    for item_i, item_i_related in tqdm(item_sim_mat_dict.items()):
        for item_j, w_ij in item_i_related.items():
            
            # 計算餘弦類似度
            item_sim_mat_dict[item_i][item_j] /= (np.sqrt(item_cnt_dict[item_i]) * np.sqrt(item_cnt_dict[item_j]))
      
    return item_sim_mat_dict
def item_cf_recommend(user_id, user_item_time_dicts, item_sim_mat_dict, sim_n, recall_n, topn_items):
    """recall article according to item silularity matrix
    
    Params:
        user_id(str): user id, a unique value represent a user
        user_item_dicts(dict): 
        item_sim_mat_dict(dict):
        sim_n(int):
        recall_n(int):
        topn_items(list):
    
    Returns:
        
    
    """
    
    # 用戶歷史點擊記錄
    user_his_items = {item for item, _ in user_item_time_dicts[user_id]}
    
    item_rank = {}
    
    # 遍歷用戶每個點擊文章, 每一篇文章都獲得 recall_n 個最類似文章(除了它自己)
    for i, item in enumerate(user_his_items):
        
        # 從類似矩陣中取出item與全部物品的類似度,獲得一個列向量item_related_sim_vector
        # 將類似度從高到低排序       
        item_related_sim_vector = sorted(item_sim_mat_dict[item].items(), key=lambda x: x[1], reverse=True)
        
        # 取出前 recall_n 個item
        for item_j, w_ij in  item_related_sim_vector[:sim_n]:
            if item_j in user_his_items or item_j in recall_items:
                continue
                
            
            item_rank.setdefault(item_j, 0)
            
            item_rank[item_j] += w_ij
    
    # 若是取出的類似文章數小於須要召回的數目,使用熱門文章進行填充
    if len(item_rank) < recall_n:
        # 
        for i, item in enumerate(topn_items):
            
            if item in item_rank:
                continue
                
            # 讓熱門文章排在高類似度文章後面
            item_rank[item] = -1 - i
            
    
    # 根據取出來的文章按權重從高到低進行排序, 而後從中選出 recall_n 篇文章
    item_rank = sorted(item_rank.items(), key=lambda x: x[1], reverse=True)[:recall_n]
    
    return item_rank
all_df = get_all_user_click(root_dir='data', offline=False)

user_item_time_dict = df_to_item_time_dict(all_df)

item_sim_mat_dict = calc_sim_mat(user_item_time_dict)

sim_n = 10

recall_n = 10

hot_items = get_topn_articles(all_df, recall_n)

user_recalled_items = {}

for user_id in tqdm(all_df['user_id'].unique()):
    user_recalled_items[user] = item_cf_recommend(user_id, user_item_time_dict, item_sim_mat_dict,\
                                                 sim_n, recall_n, hot_items)
user_item_list = []
for user_id, items in user_recalled_items.items():
    for item, score in items:
        user_item_list.append([user_id, item, score])
    

recall_df = pd.DataFrame(user_item_list, columns=['user_id', 'click_article_id', 'pred_score'])
def generate_submit_df(recall_df, topn=5, save_dir='.'):
    recall_df = recall_df.sort_values(by=['user_id', 'pred_score'])
    
    # rank 對排名
    recall_df['rank'] = recall_df.groupby(['user_id'])['pred_score'].rank(ascending=False, method='first')
    
    # 判斷是否是每一個用戶都有5篇文章及以上
    tmp = recall_df.groupby('user_id').apply(lambda x: x['rank'].max())
    assert tmp.min() >= topk

    submit = recall_df.loc[all_df['rank'] <= topk, :].set_index(['user_id', 'rank']).unstack(-1).reset_index()
    
    submit.columns = [int(col) if isinstance(col, int) else col for col in submit.columns.droplevel(0)]
    
    # 按照提交格式定義列名
    submit = submit.rename(columns={'': 'user_id', 1: 'article_1', 2: 'article_2', 
                                                  3: 'article_3', 4: 'article_4', 5: 'article_5'})
    
    save_name = save_dir + '/' + datetime.today().strftime('%m-%d') + '.csv'
    submit.to_csv(save_name, index=False, header=True)
    
    
# 獲取測試集
test_click_df = pd.read_csv(os.path.join(root_path, 'testA_click_log.csv'), sep=',')
uni_user_ids = test_click_df['user_id'].unique()

# 從全部的召回數據中將測試集中的用戶選出來
test_recalled = recall_df[recall_df['user_id'].isin(uni_user_ids)]

# 生成提交文件
generate_submit_df(test_recalled, topn=5)

總結

上面的項目使用了ItemCF的思路構建了一個基本的推薦系統框架,基本的過程以下

  1. 根據用戶的歷史點擊數據,計算文章之間的類似度矩陣。爲了不稀疏矩陣的。

Reference

[1] 推薦系統實戰

[2] 天池新聞推薦入門賽之【賽題理解+Baseline】Task01

相關文章
相關標籤/搜索