协同过滤在推荐系统中的应用

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大数据挖掘从入门到进阶实战》,喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习,在此感谢大家的支持。关注下面公众号,根据提示,可免费获取书籍的教学视频。