推荐系统入门之协同过滤

[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

相关文章
相关标签/搜索