本文介绍了推荐算法的相关原理,主要分为两种UCF和ICF;介绍了隐语义模型;以及通过协同过滤和矩阵分解两种方式构建音乐推荐系统。本文是对阿里云大学人工智能学习路线中的机器学习实战课程的学习笔记和总结。
文章目录
前言
1. 推荐系统算法原理
1.1 相关公式
- 调和平均值
- 加权平均值
- 方差
- 协方差
- 皮尔逊相关系数(Pearson Correlation Coefficient)
- 欧几里得距离(Euclidean Distance)
- Cosine相似度(Cosine Similarity)
注意:
- 如果两个变量的变化趋势一致,即其中一个大于自身的期望值时另外一个也大于自身的期望值,那么两个变量之间的协方差是正值;
- 如果两个变量的变化趋势相反,即其中一个变量大于自身的期望值时另外一个却小于自身的期望值,那么两个变量之间的协方差是负值。
- 如果X与Y是统计独立的,那么二者之间的协方差就是0,因为两个独立的随机变量满足 。
- 反过来并不成立。即如果X与Y的协方差为0,二者并不一定是统计独立的。
- 协方差矩阵计算的是不同维度之间的协方差,而不是不同样本之间的。
1.2 皮尔逊相关系数
相关系数越接近于0,相关程度越弱。
不同相关系数的两个不同维度数据的分布情况。
1.3 相似度计算
在进行推荐时,我们需要选择特征,下表中列出了几种特征形式。
用户行为 | 类型 | 特征 | 作用 |
---|---|---|---|
评分 | 显式 | 整数量化的偏好,可能的取值是[0,n),n一般取值为5或者是10。 | 通过用户对物品的评分,可以较精确的得到用户的偏好 |
投票 | 显式 | 布尔量化的偏好,取值是0或1 | 通过用户对物品的投票,可以精确的得到用户的偏好 |
转发 | 显式 | 布尔量化的偏好,取值是0或1 | 通过用户对物品的找可以精确的得到用户的偏好。如果是站内,同时可以推理得到被转发人的偏好(不精确)。 |
保存书签 | 显式 | 布尔量化的偏好,取值是0或1 | 通过用户对物品的找票,可以精确的得到用户的偏好。 |
标记标签 | 显式 | 一些单词,需要对单词进行分析,得到偏好 | 通过分析用户的标签,可以得到用户对项目的理解,同时可以分析出用户的情感:喜欢还是讨厌。 |
评论 | 显式 | 一段文字,需要进行文本分析,得到偏好 | 通过分析用户的评论,可以得到用户的情感:喜欢还是讨厌。 |
假设五个用户对商品1和商品2的评分分别为:[(6.5, 3.3), (2.6, 5.8), (6.3, 3.6), (5.8, 3.4), (3.1, 5.2)],我们在直角坐标系中绘制图像,如下图所示,那么两个用户之间的相似度就可定义为按照某种距离公式计算的距离大小。比如与用户A相似度最高的是B,如果这两个人平时都喜欢吃相似囗味的菜,突然餐厅今天出了一道新的菜品,A觉得这道新菜的味道不错,那么B喜欢这道菜的概率一定也很大,因为A和B在平日里都有相似的饮食偏好。那么我们在推荐的时候就可以按照这个思路去推荐菜品,其他领域的推荐类似。
邻居的选择:
- 固定数量的邻居
- 基于相似度门槛的邻居(常用)
1.4 协同过滤算法
协同过滤算法(collaborative filtering,CF),是一种基于类别的推荐算法。其实可以用一句谚语来解释协同过滤算法:物以类聚,人以群分。协同过滤最核心的理念就是找出爱好相同的人者属性相似的物。这里有一个潜在设定就是,爱好相同的人,他们对特定产品的偏好性是近似的。这样的场景在我们的生活中也有很多体现,例如两个人A和B,平时都喜欢吃相似囗味的菜,突然餐厅今天出了一道新的菜品,A觉得这道新菜的味道不错,那么B喜欢这道菜的概率一定也很大,因为A和B在平日里都有相似的饮食偏好。
1.5 基于用户的协同过滤 UCF(user based)
实例:亚马逊、淘宝、京东等电商平台的“买过它的用户还购买了…”,微博“关注他的用户还关注了…”。
看一个例子:
用户\物品 | 物品A | 物品B | 物品C | 物品D |
---|---|---|---|---|
用户A | √ | √ | 推荐 | |
用户B | √ | |||
用户C | √ | √ | √ |
上表中,可以看出A与C有相似的购买行为,在决定推荐商品D给哪些用户时,发现用户C已经买了,用户A还没卖,因为用户A与C有相似的购买行为,此时给用户A推荐商品D,而不给用户B推荐。
1.5.1 UCF 要解决的问题
- 已知用户评分矩阵Matrix R(一般都是非常稀疏的)
- 推断矩阵中空格empty cell处的值。
1.5.2 UCF 存在的难题
- 对于一个新用户,很难找到邻居用户。
- 对于一个物品,所有最近的邻居都在其上没有多少打分。
1.5.3 基础解决方案
- 相似度计算最好使用皮尔逊相似度。
- 考虑共同打分物品的数目,如乘上 (n为共同打分数;N为指定阈值)。
- 对打分进行归一化处理。防止打分偏差较大。
- 设置一个相似度阈值。以阈值为半径画圆,落在范围内的为相似用户。
1.5.3 UCF 不流行的原因
1.稀疏问题。
2.如果用户数量很多,计算开销很大。
3. 人是善变的。南阎浮提众生,性识无定。
1.6 基于物品的协同过滤 ICF(item based)
实例:沃尔玛尿布与啤酒的故事。
看一个例子:
用户\物品 | 物品A | 物品B | 物品C |
---|---|---|---|
用户A | √ | √ | |
用户B | √ | √ | √ |
用户C | √ | 推荐 |
1.6.1 ICF的优势
- 计算性能高,通常用户数量远大于物品数量;
- 可预先计算保留,物品并不善变;
1.6.2 ICF实例
1.7 冷启动问题
1.7.1 用户冷启动问题
回顾前言中的第一张图片,所谓冷启动通俗的理解就是,新注册用户,什么数据都未知,这时候要给用户推荐,应该怎么处理。
- 引导用户把自己的一些属性表达出来;
- 利用现有的开放数据平台;
- 根据用户注册属性;
- 推荐排行榜单(大众心理,常用)。
1.7.2 物品冷启动问题
- 文本分析;
- 主题模型;
- 打标签;
- 推荐排行榜单。
1.8 UCF 与 ICF 对比
UserCF | ltemCF | |
---|---|---|
性能 | 适用于用户较少的场合,如果用户过多,计算用户相似度矩阵适用于物品数明显小于用户数的场合,如果物品很多,计算物品相似度矩阵的的代价较大 | 适用于物品数明显小于用户数的场合,如果物品很多,计算物品相似度矩阵的代价较大 |
领域 | 时效性要求高,用户个性化兴趣要求不高 | 长尾物品丰富,用户个性化需求强烈 |
实时性 | 用户有新行为,不一定需要推荐结果立变化 | 用户有新行为,一定会导致推荐结果的实时变化 |
冷启动 | 在新用户对少的物品产生行为后,不能立即对他进行个性化推,因为用户相似度是离线计算的;新物品上线后一段时间,一旦有用户对物品产生行为,就可以将新物品推荐给其他用户 | 新用户只要对一个物品产生行为,就能推荐相关物品给他,但无注在不离线更新物品相似度表的情况下将新物品推荐给用户;(但是新的item到来也同样是冷启动问题) |
推荐理由 | 很难提供令用户信服的推荐解释 | 可以根据用户历史行为归纳推荐理由 |
1.9 应用场景
基于用户的推荐 | 基于物品的准荐 |
---|---|
实时新闻 | 图书 |
突发情况 | 电子商务 |
电影 |
2. 隐语义模型
在做推荐的时候,需要找到用户与商品之间的联系,那么如何找,这就需要隐语义模型,可以类比神经网络,相当于把用户和物品分别转换成隐语义,然后再进行组合。
- 从数据出发,进行个性化准荐;
- 用户和物品之间有着隐含的联系;
- 隐含因子让计算机能理解就好;
- 将用户和物品通过中介隐含因子联系起来。
2.1 构造目标函数
表示用户(User)对当前物品(Item)的评分值,
表示用户与评分之间的关系,
表示物品与评分之间的关系。构造目标函数C,其中P,Q为要求解的参数,带
的那两项表示正则化项,为了防止过拟合。
2.2 选择优化算法
使用梯度下降算法迭代求解,其中
为学习率。
隐语义模型负样本选择:
- 对每个用户,要保证正负样本的平衡(数目相似);
- 选取那些很热门,而用户却没有行为的物品;
- 对于用户一物品集 ,如果 是正样本,则有 ,负样本则 。
2.3 选择模型参数
选择好模型参数之后,进行训练,训练完成得到的是矩阵P和Q,通过矩阵运算(矩阵乘)得到矩阵R,此时R中每一个元素都有值,不要忘记R的含义,R代表用户(User)对物品(Iterm)的评分,因此,有了R,我们就可以知道用户对原来数据中却是部分的用户可能评分,也就知道了用户的喜好程度,因此可以依据此来做推荐。简单来说就是,将用户、物品、评分组成的三维空间,拆分映射到二维空间,通过隐语义模型训练求解分量之间的关系,注意此处依然是不同维度数据之间的关系,求解出来之后,再通过矩阵乘求出R,映射回三维空间,此时可以知道用户对所有物品的评分,也就可以用来做预测。矩阵P和矩阵Q的可解释性很差,跟深度学习训练得到的模型差不多。
2.4 协同过滤VS隐语义
原理:协同过滤基于统计;隐语义基于建模。
空间复杂度:隐语义模型较小。
实时推荐依旧难,目前离线计算多。
2.5 评估指标
令
是根据用户在训练集上的行为给用户作出的推荐列表,
是用户在测试集上的行为列表。
覆盖率尽可能的满足客户的需求,即类别数要多,比如一个用户买了同品牌的卫衣,推荐的时候尽可能的推荐裤子,鞋子,周边等等,也可用熵值表示,种类越多,越混轮,熵值越大。覆盖率越广。
再回顾一下西瓜书中的查全率和查准率的定义。
2. 案例实战:音乐推荐
2.1 数据处理
- 导入必要的包
import pandas as pd
import numpy as np
import time
import sqlite3
- 读取数据
data_home = 'D:/AliyunEDU/Part3-01-Recommendation/SampleCode/'
triplet_dataset = pd.read_csv(filepath_or_buffer=data_home+'train_triplets.txt',
sep='\t', header=None,
names=['user','song','play_count'])
# 数据文件2.79GB,4837万条数据,读取比较费劲,如果觉得很慢的话,可以设置`nrows`参数,设置读取行数。
- 查看读取的数据
triplet_dataset.head(n=5)
输出:
user song play_count
0 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOAKIMP12A8C130995 1
1 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOAPDEY12A81C210A9 1
2 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOBBMDR12A8C13253B 2
3 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOBFNSP12AF72A0E22 1
4 b80344d063b5ccb3212f76538f3d9e43d87dca9e SOBFOVM12A58A7D494 1
- 分别统计每一个用户的播放总量
output_dict = {}
with open(data_home+'train_triplets.txt') as f:
# enumerate() 用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标。
for line_number, line in enumerate(f):
user = line.split('\t')[0] # split() 通过指定分隔符对字符串进行切片
play_count = int(line.split('\t')[2])
if user in output_dict:
play_count +=output_dict[user]
output_dict.update({user:play_count})
output_dict.update({user:play_count})
output_list = [{'user':k,'play_count':v} for k,v in output_dict.items()]
play_count_df = pd.DataFrame(output_list) # 转换成pandas的DataFrame格式
play_count_df = play_count_df.sort_values(by = 'play_count', ascending = False) # 排名
play_count_df.to_csv(path_or_buf='user_playcount_df.csv', index = False) # 保存
- 保存
play_count_df = pd.read_csv(filepath_or_buffer='user_playcount_df.csv')
song_count_df = pd.read_csv(filepath_or_buffer='song_playcount_df.csv')
- 取其中一部分数(已经按大小排好序的了,这些应该是比较重要的数据),作为实验数据。
total_play_count = sum(song_count_df.play_count)
print ((float(play_count_df.head(n=100000).play_count.sum())/total_play_count)*100)
play_count_subset = play_count_df.head(n=100000) #统计播放占比
(float(song_count_df.head(n=30000).play_count.sum())/total_play_count)*100 #统计歌曲占比
- 取10W个用户,3W首歌
song_count_subset = song_count_df.head(n=30000)
user_subset = list(play_count_subset.user)
song_subset = list(song_count_subset.song)
- 过滤掉其他数据
triplet_dataset = pd.read_csv(filepath_or_buffer=data_home+'train_triplets.txt',sep='\t',
header=None, names=['user','song','play_count'])
triplet_dataset_sub = triplet_dataset[triplet_dataset.user.isin(user_subset) ]
del(triplet_dataset)
triplet_dataset_sub_song = triplet_dataset_sub[triplet_dataset_sub.song.isin(song_subset)]
del(triplet_dataset_sub)
triplet_dataset_sub_song.to_csv(path_or_buf=data_home+'triplet_dataset_sub_song.csv', index=False)
- 加入音乐详细信息
# 数据库文件转DataFrame
conn = sqlite3.connect(data_home+'track_metadata.db')
cur = conn.cursor()
cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
cur.fetchall()
track_metadata_df = pd.read_sql(con=conn, sql='select * from songs')
track_metadata_df_sub = track_metadata_df[track_metadata_df.song_id.isin(song_subset)]
track_metadata_df_sub.to_csv(path_or_buf=data_home+'track_metadata_df_sub.csv', index=False)
- 现有数据
triplet_dataset_sub_song = pd.read_csv(filepath_or_buffer=data_home+'triplet_dataset_sub_song.csv',encoding = "ISO-8859-1")
track_metadata_df_sub = pd.read_csv(filepath_or_buffer=data_home+'track_metadata_df_sub.csv',encoding = "ISO-8859-1")
- 清洗数据
# 去掉无用的和重复的数据
del(track_metadata_df_sub['track_id'])
del(track_metadata_df_sub['artist_mbid'])
track_metadata_df_sub = track_metadata_df_sub.drop_duplicates(['song_id'])
triplet_dataset_sub_song_merged = pd.merge(triplet_dataset_sub_song, track_metadata_df_sub, how='left', left_on='song', right_on='song_id')
triplet_dataset_sub_song_merged.rename(columns={'play_count':'listen_count'},inplace=True)
del(triplet_dataset_sub_song_merged['song_id'])
del(triplet_dataset_sub_song_merged['artist_id'])
del(triplet_dataset_sub_song_merged['duration'])
del(triplet_dataset_sub_song_merged['artist_familiarity'])
del(triplet_dataset_sub_song_merged['artist_hotttnesss'])
del(triplet_dataset_sub_song_merged['track_7digitalid'])
del(triplet_dataset_sub_song_merged['shs_perf'])
del(triplet_dataset_sub_song_merged['shs_work'])
- 查看最流行的歌曲
popular_songs = triplet_dataset_sub_song_merged[['title','listen_count']].groupby('title').sum().reset_index()
popular_songs_top_20 = popular_songs.sort_values('listen_count', ascending=False).head(n=20)
import matplotlib.pyplot as plt; plt.rcdefaults()
import numpy as np
import matplotlib.pyplot as plt
objects = (list(popular_songs_top_20['title']))
y_pos = np.arange(len(objects))
performance = list(popular_songs_top_20['listen_count'])
plt.bar(y_pos, performance, align='center', alpha=0.5)
plt.xticks(y_pos, objects, rotation='vertical')
plt.ylabel('Item count')
plt.title('Most popular songs')
plt.show()
- 最受欢迎的歌手
popular_artist = triplet_dataset_sub_song_merged[['artist_name','listen_count']].groupby('artist_name').sum().reset_index()
popular_artist_top_20 = popular_artist.sort_values('listen_count', ascending=False).head(n=20)
objects = (list(popular_artist_top_20['artist_name']))
y_pos = np.arange(len(objects))
performance = list(popular_artist_top_20['listen_count'])
plt.bar(y_pos, performance, align='center', alpha=0.5)
plt.xticks(y_pos, objects, rotation='vertical')
plt.ylabel('Item count')
plt.title('Most popular Artists')
plt.show()
2.2 基于物品的协同过滤
- 推荐系统
def create_popularity_recommendation(train_data, user_id, item_id):
#Get a count of user_ids for each unique song as recommendation score
train_data_grouped = train_data.groupby([item_id]).agg({user_id: 'count'}).reset_index()
train_data_grouped.rename(columns = {user_id: 'score'},inplace=True)
#Sort the songs based upon recommendation score
train_data_sort = train_data_grouped.sort_values(['score', item_id], ascending = [0,1])
#Generate a recommendation rank based upon score
train_data_sort['Rank'] = train_data_sort['score'].rank(ascending=0, method='first')
#Get the top 10 recommendations
popularity_recommendations = train_data_sort.head(20)
return popularity_recommendations
- 基于歌曲相似度的推荐
#song_count_subset = song_count_df.head(n=5000)
song_count_subset = song_count_df.head(n=500)
user_subset = list(play_count_subset.user)
song_subset = list(song_count_subset.song)
triplet_dataset_sub_song_merged_sub = triplet_dataset_sub_song_merged[triplet_dataset_sub_song_merged.song.isin(song_subset)]
triplet_dataset_sub_song_merged_sub.head()
输出:
user song listen_count title release artist_name year
0 d6589314c0a9bcbca4fee0c93b14bc402363afea SOADQPP12A67020C82 12 You And Me Jesus Tribute To Jake Hess Jake Hess 2004
1 d6589314c0a9bcbca4fee0c93b14bc402363afea SOAFTRR12AF72A8D4D 1 Harder Better Faster Stronger Discovery Daft Punk 2007
2 d6589314c0a9bcbca4fee0c93b14bc402363afea SOANQFY12AB0183239 1 Uprising Uprising Muse 0
3 d6589314c0a9bcbca4fee0c93b14bc402363afea SOAYATB12A6701FD50 1 Breakfast At Tiffany's Home Deep Blue Something 1993
4 d6589314c0a9bcbca4fee0c93b14bc402363afea SOBOAFP12A8C131F36 7 Lucky (Album Version) We Sing. We Dance. We Steal Things. Jason Mraz & Colbie Caillat 0
train_data, test_data = train_test_split(triplet_dataset_sub_song_merged_sub, test_size = 0.30, random_state=0)
is_model = Recommenders.item_similarity_recommender_py()
is_model.create(train_data, 'user', 'title')
user_id = list(train_data.user)[7]
user_items = is_model.get_user_items(user_id)
#Recommend songs for the user using personalized model
is_model.recommend(user_id)
输出
user_id song score rank
0 dbb1dc38adb46ec7cb404856564e756632a534b1 Revelry 0.129473 1
1 dbb1dc38adb46ec7cb404856564e756632a534b1 OMG 0.117629 2
2 dbb1dc38adb46ec7cb404856564e756632a534b1 Drop The World 0.116160 3
3 dbb1dc38adb46ec7cb404856564e756632a534b1 Fireflies 0.113310 4
4 dbb1dc38adb46ec7cb404856564e756632a534b1 Marry Me 0.113148 5
5 dbb1dc38adb46ec7cb404856564e756632a534b1 Sehr kosmisch 0.112280 6
6 dbb1dc38adb46ec7cb404856564e756632a534b1 Pursuit Of Happiness (nightmare) 0.111999 7
7 dbb1dc38adb46ec7cb404856564e756632a534b1 Hey_ Soul Sister 0.111086 8
8 dbb1dc38adb46ec7cb404856564e756632a534b1 The Only Exception (Album Version) 0.110231 9
9 dbb1dc38adb46ec7cb404856564e756632a534b1 I CAN'T GET STARTED 0.108167 10
2.3 基于矩阵分解(SVD)
import math as mt
from scipy.sparse.linalg import * #used for matrix multiplication
from scipy.sparse.linalg import svds
from scipy.sparse import csc_matrix
def compute_svd(urm, K):
U, s, Vt = svds(urm, K)
dim = (len(s), len(s))
S = np.zeros(dim, dtype=np.float32)
for i in range(0, len(s)):
S[i,i] = mt.sqrt(s[i])
U = csc_matrix(U, dtype=np.float32)
S = csc_matrix(S, dtype=np.float32)
Vt = csc_matrix(Vt, dtype=np.float32)
return U, S, Vt
def compute_estimated_matrix(urm, U, S, Vt, uTest, K, test):
rightTerm = S*Vt
max_recommendation = 250
estimatedRatings = np.zeros(shape=(MAX_UID, MAX_PID), dtype=np.float16)
recomendRatings = np.zeros(shape=(MAX_UID,max_recommendation ), dtype=np.float16)
for userTest in uTest:
prod = U[userTest, :]*rightTerm
estimatedRatings[userTest, :] = prod.todense()
recomendRatings[userTest, :] = (-estimatedRatings[userTest, :]).argsort()[:max_recommendation]
return recomendRatings
K=50
urm = data_sparse
MAX_PID = urm.shape[1]
MAX_UID = urm.shape[0]
U, S, Vt = compute_svd(urm, K)
uTest = [4,5,6,7,8,873,23]
uTest_recommended_items = compute_estimated_matrix(urm, U, S, Vt, uTest, K, True)
for user in uTest:
print("Recommendation for user with user id {}". format(user))
rank_value = 1
for i in uTest_recommended_items[user,0:10]:
song_details = small_set[small_set.so_index_value == i].drop_duplicates('so_index_value')[['title','artist_name']]
print("The number {} recommended song is {} BY {}".format(rank_value, list(song_details['title'])[0],list(song_details['artist_name'])[0]))
rank_value+=1
输出:
Recommendation for user with user id 23
The number 1 recommended song is Garden Of Eden BY Guns N' Roses
The number 2 recommended song is Don't Speak BY John Dahlbäck
The number 3 recommended song is Master Of Puppets BY Metallica
The number 4 recommended song is TULENLIEKKI BY M.A. Numminen
The number 5 recommended song is Bring Me To Life BY Evanescence
The number 6 recommended song is Kryptonite BY 3 Doors Down
The number 7 recommended song is Make Her Say BY Kid Cudi / Kanye West / Common
The number 8 recommended song is Night Village BY Deep Forest
The number 9 recommended song is Better To Reign In Hell BY Cradle Of Filth
The number 10 recommended song is Xanadu BY Olivia Newton-John;Electric Light Orchestra
完整代码和数据集,请访问阿里云下载,或者评论区留邮箱。
参考:
从零构建音乐推荐系统:https://edu.aliyun.com/course/1893
阿里云PAI机器学习实战:https://edu.aliyun.com/course/26
均值、方差、协方差:https://www.zybuluo.com/spiritnotes/note/297176
pearson correlation:https://www.spss-tutorials.com/pearson-correlation-coefficient/