推荐系统(1)——先做一个出来(先实战,后理论)

前言

先实现推荐系统,再细说理论

注:看全章节之前需要有python基础和数学基础,数学知识要求不高,了解微积分、线性代数就行啦

说起来推荐系统总能让人觉得这个是一个非常高大上的东西,让想去了解学习的人触不可及,在网上搜到很多关于推荐系统的文章大多是原理居多,容易让人觉得云里雾里,劝退很多人。
其实在实际去实现的过程中遇到原理性问题就可以自发去了解了,这样就避免了很多人直接先学各种数学原理的迷茫和不知所措。
所以本章节不说原理,原理部分在后面的章节再细说,上来就说原理容易劝退大家,所以~直接打开jupyter去做一个简单的推荐系统实现它吧!
全文代码使用同一个数据集,命名贯通,亲测可用!!!


数据源自互联网

一、数据下载

制作一个音乐推荐系统,数据集共提供两个文件,TXT文件和DB文件
链接:https://pan.baidu.com/s/1OBduA9kXsHx1scdwD9XeFA?pwd=z8uo
提取码:z8uo
–来自百度网盘超级会员V1的分享

二、数据读取

1.数据导入

txt格式文件导入

此文件中保存的是用户ID、歌曲ID、播放次数

#导入数据处理相关库
import pandas as pd
import numpy as np
#数据读取(读取用户,歌曲,播放量)
data_user = pd.read_csv('train_triplets.txt', sep='\t', names=['user','song_id','play_count'])
#查看数据大小(0.4亿多条数据)
print(data_user.shape)
data_user.head(10)

(48373586, 3)

usersong_idplay_count
0b80344d063b5ccb3212f76538f3d9e43d87dca9eSOAKIMP12A8C1309951
1b80344d063b5ccb3212f76538f3d9e43d87dca9eSOAPDEY12A81C210A91
2b80344d063b5ccb3212f76538f3d9e43d87dca9eSOBBMDR12A8C13253B2
3b80344d063b5ccb3212f76538f3d9e43d87dca9eSOBFNSP12AF72A0E221
4b80344d063b5ccb3212f76538f3d9e43d87dca9eSOBFOVM12A58A7D4941
5b80344d063b5ccb3212f76538f3d9e43d87dca9eSOBNZDC12A6D4FC1031
6b80344d063b5ccb3212f76538f3d9e43d87dca9eSOBSUJE12A6D4F8CF52
7b80344d063b5ccb3212f76538f3d9e43d87dca9eSOBVFZR12A6D4F8AE31
8b80344d063b5ccb3212f76538f3d9e43d87dca9eSOBXALG12A8C13C1081
9b80344d063b5ccb3212f76538f3d9e43d87dca9eSOBXHDL12A81C204C01

注意:这个表大小为1.1G,其中play_count是int64类型,为节省内存可以转化为int16,可以压缩到800MB,电脑内存大的小伙伴无需考虑。


db文件导入(数据库文件)

此文件中保存的是歌曲信息

#sqlite3是一个连接数据库文件的python库,而且不需要本地安装数据库
import sqlite3
#使用.connect () 函数连接数据库,返回一个Connection对象,我们就是通过这个对象与数据库进行交互。
conn = sqlite3.connect('track_metadata.db')
#创建一个游标对象,该对象的.execute()方法可以执行sql命令,让我们能够进行数据操作。
cur = conn.cursor()
#查询数据库中所有的表名
cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
#获取查询结果
cur.fetchall()

[(‘songs’,)]

输出释义:数据库中只有一个表,表名为songs

#抽取数据库中表名为songs的所有文件
song_data = pd.read_sql(con=conn, sql='select * from songs')
#使用数据库后,需要关闭游标和链接
cur.close()
conn.close()
#数据展示(100万条数据)
print(song_data.shape)
song_data.head(5)

(1000000, 14)

track_idtitlesong_idreleaseartist_idartist_mbidartist_namedurationartist_familiarityartist_hotttnesssyeartrack_7digitalidshs_perfshs_work
0TRMMMYQ128F932D901Silent NightSOQMMHC12AB0180CB8Monster Ballads X-MasARYZTJS1187B98C555357ff05d-848a-44cf-b608-cb34b5701ae5Faster Pussy cat252.055060.6498220.39403220037032331-10
1TRMMMKD128F425225DTanssi vaanSOVFVAK12A8C1350D9KarkuteilläARMVN3U1187FB3A1EB8d7ef530-a6fd-4f8f-b2e2-74aec765e0f9Karkkiautomaatti156.551380.4396040.35699219951514808-10
2TRMMMRX128F93187D9No One Could EverSOGTUKN12AB017F4F1ButterARGEKB01187FB507503d403d44-36ce-465c-ad43-ae877e65adc4Hudson Mohawke138.970980.6436810.43750420066945353-10
3TRMMMCH128F425532CSi Vos QuerésSOBNYVR12A8C13558CDe CuloARNWYLR1187B9B2F9C12be7648-7094-495f-90e6-df4189d68615Yerba Brava145.057510.4485010.37234920032168257-10
4TRMMMWA128F426B589Tangle Of AspensSOHSBXH12A8C13B0DFRene Ablaze Presents Winter SessionsAREQDTE1269FB37231Der Mystic514.298320.0000000.00000002264873-10

2.数据合并与清洗

根据抽取的数据进行合并
两个表都有song的ID,所以使用song_id进行主键进行关联

#去除song_id相同的歌曲,避免合并后数据量变大
song_data.drop_duplicates(['song_id'],inplace=True)
# data_user 与 data_song 以 song_id 为主键进行左连接
data = data_user.merge(song_data, on='song_id', how='left')
#去除掉无用的字段比如歌曲作者家乡,其他无用的ID什么的
data.drop(['track_id','artist_mbid','artist_id','duration',
		   'artist_familiarity','artist_hotttnesss','track_7digitalid',
 	 	   'shs_perf','shs_work'],axis=1,inplace=True)
#缺失值检查
def check_null_data(df):
    total = df.isnull().sum().sort_values(ascending=False)
    percent =(df.isnull().sum()/df.isnull().count()).sort_values(ascending=False)
    missing_data = pd.concat([total, percent], axis=1, keys=['Total', 'Percent'])
    return missing_data
print(data.shape)
#随机抽取100万检查,全部检查运行太慢
check_null_data(data.sample(n=1000000,axis=0))

(48373586, 17)

TotalPercent
user00.0
artist_name00.0
year00.0
song_id00.0
release00.0
title00.0
play_count00.0

这数据真的可以,没有缺失值


三、数据分析

1.歌曲热度排行

查看播放量前15的热门歌曲

# 统计每首歌的播放总数
song_play_count = data.groupby(['title'])['play_count'].sum()
# 播放总数倒序排序
song_play_rank = pd.DataFrame(song_play_count).sort_values('play_count',ascending=False).reset_index()
# 歌曲热度top15绘图
import matplotlib.pyplot as plt
pic_data = song_play_rank.iloc[:15,:]
plt.figure(figsize=(8,8))
plt.bar(pic_data.title,pic_data.play_count,alpha=0.5)
plt.xticks(rotation='vertical')
plt.ylabel('listen count')
plt.title('Most popular songs')
plt.show()

播放量排行


2.用户播放量分布

# 统计每个用户的播放量
user_play_count = data.groupby(['user'])['title'].count()
# 绘制分布密度图
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(20,6))
plt.subplot(1,2,1)
ax = sns.kdeplot(user_play_count, color="Red", shade=True)
plt.subplot(1,2,2)
plt.xlim(0,400)
ax = sns.kdeplot(user_play_count, color="Red", shade=True)
plt.show()

在这里插入图片描述

由于左图密度图呈现右长尾分布,所以截去长尾后的分布在右图可见
从图中可以看出,大部分用户的播放量大概在25首左右

其他数据探索可以自行探索,比如最受欢迎的发行,最受欢迎的歌手、用户播放量分布等,接下来主要讲述推荐算法


四、推荐算法

1.基于排行榜的推荐

基于排行榜的推荐是最简单推荐,顾名思义就是把每一首歌曲按照已听用户数排序进行推荐(热歌排行)。缺点是这样做不会体现用户的独特性,用户的偏好等,每个用户所推荐的内容都是一样的。好处就是会给用户推荐当下最流行的歌曲,可以这个算法完全可以用在未产生信息的新用户上,可以称为冷启动,在后面的章节会具体具体讲述冷启动如何实现。

def popularity_recommendation(data, user_id, item_id, n=20):
    #统计每个歌曲被多少用户听过
    data_train = pd.DataFrame(data.groupby([item_id])[user_id].count()).reset_index()
    #根据用户数量进行倒叙排序
    data_train = data_train.sort_values(by=user_id,ascending=False).iloc[:n,:]
    #推荐排名
    data_train['rank'] = [x for x in range(1,n+1)]
    data_train.columns = [item_id, 'score', 'rank']
    return data_train

#n=20指的是推荐20首歌曲
recommender = popularity_recommendation(data=data, user_id='user', item_id='title', n=20)
recommender
titlescorerank
220061Sehr kosmisch1104791
279267Undo904792
63471Dog Days Are Over (Radio Edit)904443
303140You’re The One892874
209422Revelry806665
219526Secrets792606
108691Horn Concerto No. 4 in E flat K495: II. Romanc…694877
83577Fireflies649338
105587Hey_ Soul Sister638099
270293Tive Sim5862110
183334OMG5326011
280663Use Somebody5208012
68052Drop The World5102213
159669Marry Me4773414
39032Canada4638415
40952Catch You Baby (Steve Pitron & Max Sanna Radio…4607716
261462The Scientist4555817
133035Just Dance4274518
45648Clocks4273319
202513Pursuit Of Happiness (nightmare)4181120

2.推荐入门——协同过滤

先介绍一下什么是协同过滤,一定不要被这个名字吓到,我们从字面来理解一下这个词的含义,首先协同就是一起的意思,过滤就是筛选剔除掉的意思,因此这个推荐算法的本质思想就是用户一起(通过与网站的互动)来筛选出满足自己需求的物品,剔除掉不感兴趣的物品。
在这里插入图片描述
一般说的协同过滤算法主要有基于用户的协同过滤算法和基于物品的协同过滤算法。

  • 基于用户的协同过滤:比如说要给你推荐物品,就找到和你有相似兴趣的其他用户,把他们喜欢的但是你没听过的物品推荐给你。
  • 基于物品的协同过滤:比如说你喜欢吃水煮鱼,然后水煮鱼和水煮肉的相似度很高,那就把水煮肉推荐给你。
  • 基于内容的协同过滤:比如说你在CSDN上搜索文章,点开一篇文章后,把网页拖到最后,你会发现推荐给你的文章都是和你正在看的这篇文章的大致都是同一个内容方向的。

基于物品(item-based)的协同过滤(ItemCF算法)

计算物品(音乐)相似度

  • 首先我们要针对某一个用户进行推荐,那必然得先得到他都听过哪些歌曲,通过这些已被听过的歌曲跟整个数据集中的歌曲进行对比,看哪些歌曲跟用户已听过的比较类似,推荐的就是这些类似的。

比如说,小明听了A、B两首歌,在他没听过的C、D、E、F、G、H等歌曲如何按照最优推荐推荐给他呢?

这里引入一个简单的相似计算——杰卡德相似度

按照下图中的计算方法就是Jaccard相似系数,相似系的值的大小反映了对AB歌曲相似的相似度。A,B,C,D,E等代表歌曲,其中A,B是某用户听过的歌曲,C,D,E等是用户没有听过的歌曲,A/C的相似度为1/7,B/C的相似度为1/3,C的推荐分数为两项之和,按照这个分值从高到低排序即可排列出推荐顺序。

在这里插入图片描述

Jaccard(C/A) = 交集(听过C歌曲的3000人和听过A歌曲的5000人)/ 并集(听过C歌曲的3000人和听过A歌曲的5000人)Jaccard(i/j) = 交集(听过I歌曲的m人和听过j歌曲的n人)/ 并集(听过i歌曲的m人和听过j歌曲的n人)

说白了就是如果两个歌曲很相似,那其受众应当是一致的,交集/并集的比例应该比较大,如果两个歌曲没啥相关性,其值应当就比较小了。

在这里我们选取一部分数据进行实验(全量的话将会创造很大的用户—物品矩阵,电脑会歇菜):

#选取一部分数据进行推荐
#根据歌曲播放量选取比放量前5000的歌曲
song_nums = 5000
song_recom = pd.DataFrame(data.groupby('song_id')['play_count'].sum()).reset_index().sort_values(by='play_count',ascending=False).head(n=song_nums)
data_song_recom = data[data.song_id.isin(song_recom.song_id)]
#根据筛选后的再选取用户播放量前1000名(根据自己电脑性能选择,这里只做实验,所以只选了1000)
user_nums = 1000
user_recom = pd.DataFrame(data_song_recom.groupby('user')['play_count'].sum()).reset_index().sort_values(by='play_count',ascending=False).head(n=1000)
data_song_user_recom = data_song_recom[data_song_recom.user.isin(user_recom.user)]
print(data_song_user_recom.shape)
#重置index
data_song_user_recom = data_song_user_recom.reset_index().drop(['index'],axis=1)
data_song_user_recom

(118747, 7)

usersong_idplay_counttitlereleaseartist_nameyear
0bb85bb79612e5373ac714fcd4469cabeb5ed94e1SOCMHGT12A8C138D8A7Heard Them StirringFleet FoxesFleet Foxes2008
1bb85bb79612e5373ac714fcd4469cabeb5ed94e1SOGFKJE12A8C138D6A9Sun It RisesFleet FoxesFleet Foxes2008
2bb85bb79612e5373ac714fcd4469cabeb5ed94e1SOJAMXH12A8C138D9B9MeadowlarksFleet FoxesFleet Foxes2008
3bb85bb79612e5373ac714fcd4469cabeb5ed94e1SOJWBZK12A58A78AF710Tiger Mountain Peasant SongFleet FoxesFleet Foxes2008
4bb85bb79612e5373ac714fcd4469cabeb5ed94e1SOMMJUQ12AF72A59318Your ProtectorFleet FoxesFleet Foxes2008

#建立用户—物品(音乐)矩阵
user_music_matric = pd.crosstab(data_song_user_recom.user,data_song_user_recom.song_id)
user_music_matric.head(5)
SOAACPJ12A81C21360SOAAFAC12A67ADF7EBSOAAFYH12A8C13717ASOAAROC12A6D4FA420SOAATLI12A8C13E319SOAAUKC12AB017F868SOAAVUV12AB0186646SOAAWEE12A6D4FBEC8SOABHYV12A6D4F6D0FSOABJBU12A8C13F63FSOZXTUT12A6D4F6D03SOZYBGN12A8C13A93CSOZYDZR12A8C13F4F0SOZYNFV12AB0186910SOZYSDT12A8C13BFD7SOZYUGZ12A8AE472ACSOZZHQT12AB018B714SOZZLZN12A8AE48D6DSOZZTCU12AB0182C58SOZZTNF12A8C139916
002b63a7e2247de6d62bc62f253474edc7dd044c00000010010000000000
003a5e3285141b1a54edbc51fbfa1cc922023aae00000000000000000000
008065e52b59a094dcfca17d361c5ae4b4eb767f00000000010000000000
0086dd8bbc9290ff2cafc04b807b87aec719aa3600000000000000000000
0091e0326c4c034cc04be6454742912845740a1f00000000000000000000

上面表图就是数据集的用户—物品(音乐)矩阵,index列是用户ID,columns行是歌曲ID,其中0代表该用户没有听过这个音乐,1代表用户听过这个音乐,可以看出这个矩阵是一个非常稀疏的矩阵,因为不可能每首歌每个用户都有听过。

杰卡德相似度计算

#导入计算杰卡德相似度的API——jaccard_score
from sklearn.metrics import jaccard_score
#计算歌曲id为SOAAVUV12AB0186646和SOABJBU12A8C13F63F的杰卡德相似度
jaccard_score(user_music_matric['SOAAVUV12AB0186646'],user_music_matric['SOABJBU12A8C13F63F'])

0.06914893617021277

from sklearn.metrics.pairwise import pairwise_distances
#计算物品(音乐)之间的杰卡德相似度
music_similar = 1-pairwise_distances(data.T.values,metric='jaccard')
music_similar = pd.DataFrame(music_similar,index=user_music_matric.columns,columns=user_music_matric.columns)
music_similar.head(5)
SOAACPJ12A81C21360SOAAFAC12A67ADF7EBSOAAFYH12A8C13717ASOAAROC12A6D4FA420SOAATLI12A8C13E319SOAAUKC12AB017F868SOAAVUV12AB0186646SOAAWEE12A6D4FBEC8SOABHYV12A6D4F6D0FSOABJBU12A8C13F63FSOZXTUT12A6D4F6D03SOZYBGN12A8C13A93CSOZYDZR12A8C13F4F0SOZYNFV12AB0186910SOZYSDT12A8C13BFD7SOZYUGZ12A8AE472ACSOZZHQT12AB018B714SOZZLZN12A8AE48D6DSOZZTCU12AB0182C58SOZZTNF12A8C139916
SOAACPJ12A81C213601.0000000.000.000.0000000.00.00.0000000.0000000.0000000.0000000.0000000.0000000.000000.00.0000000.0000000.000.00.0000000.041667
SOAAFAC12A67ADF7EB0.0000001.000.000.0000000.00.00.0000000.0000000.0000000.0062110.0000000.0416670.100000.00.0135140.0000000.050.00.0000000.000000
SOAAFYH12A8C13717A0.0000000.001.000.0000000.00.00.0535710.0526320.0588240.0063690.0000000.0000000.000000.00.0142860.0000000.000.00.0000000.040000
SOAAROC12A6D4FA4200.0000000.000.001.0000000.00.00.0000000.0000000.0408160.0058140.0357140.0285710.000000.00.0750000.0303030.000.00.0333330.000000
SOAATLI12A8C13E3190.0000000.000.000.0000001.00.00.0000000.0000000.0000000.0000000.0000000.0000000.000000.00.0000000.0000000.000.00.0000000.000000

可以看出矩阵的对角线为1,也就是说歌曲自己对自己的相似度为1

#为每个歌曲找到最相似的N首歌曲
#创建歌曲推荐列表
topN_music = {}
#找到最相似的3首歌曲
N = 2
for i in music_similar.index:
    #在寻找相似度歌曲中,首先删除自己(自己和自己的相似度为1)
    _df = music_similar[i].drop(i)
    #在寻找相似度歌曲中,相似度降序排序
    _df_sorted = _df.sort_values(ascending=False)
    #找出相似度前N首歌曲
    topN_music[i] = list(_df_sorted.index)[:N]
print(topN_music)

{‘SOAACPJ12A81C21360’: [‘SOMMLDP12A8C13BA46’, ‘SOOWNLO12A6D4F7A3C’],
‘SOAAFAC12A67ADF7EB’: [‘SOKEUYU12A67ADF7E6’, ‘SOINIUZ12A67ADF6D8’],
‘SOAAFYH12A8C13717A’: [‘SOTKTQG12A6BD5294E’, ‘SOVDUYI12A8C139EBE’],
‘SOAAROC12A6D4FA420’: [‘SOQXDXM12A8C134E8E’, ‘SOFINSL12AF729F063’],
‘SOAATLI12A8C13E319’: [‘SOHUAVP12A6BD50521’, ‘SOGGXNH12AB018D2AC’],
‘SOAAUKC12AB017F868’: [‘SOXQIUR12A8AE4654A’, ‘SOBCXCW12A8C13BFDD’],
‘SOAAVUV12AB0186646’: [‘SONQJCU12A8C144398’, ‘SOOLWEJ12AB0186DA4’],
‘SOAAWEE12A6D4FBEC8’: [‘SOAJWRM12A8C13CF2B’, ‘SOLNZIU12AB01896D2’],
…}

从上面的输出结果可以看出,对于每一首歌曲都能自动推荐相似度最大的两首歌,基于物品推荐的协同过滤就介绍到此。上面的算法可以固化到函数,输入歌曲名字,返回推荐列表,这里就不做过多介绍。

基于用户(user-based)的协同过滤(UserCF)

#计算用户之间的杰卡德相似度
user_similar = 1-pairwise_distances(data.values,metric='jaccard')
user_similar = pd.DataFrame(user_similar,index=user_user_matric.columns,columns=user_user_matric.columns)
user_similar.head(5)
#为每个歌曲找到最相似的N首歌曲
#创建歌曲推荐列表
topN_user = {}
#找到最相似的3首歌曲
N = 2
for i in user_similar.index:
    #在寻找相似度歌曲中,首先删除自己(自己和自己的相似度为1)
    _df = user_similar[i].drop(i)
    #在寻找相似度歌曲中,相似度降序排序
    _df_sorted = _df.sort_values(ascending=False)
    #找出相似度前N首歌曲
    topN_user[i] = list(_df_sorted.index)[:N]
print(topN_user)

计算用户之间的相似度和计算物品之间相似度是相同的,只是将
pairwise_distances(data.T.values,metric=‘jaccard’)
改成
pairwise_distances(data.values,metric=‘jaccard’)即可

上面两个示例仅能表示用户有没有听过歌曲(0、1)做的推荐,用户听过的歌曲不一定就是用户喜欢的,所以并没有考虑用户对歌曲的喜欢程度


基于模型(model-based)的协同过滤 (ModelCF算法)

通过上面的算法,我们已经知道了基于用户和基于物品的协同过滤,算法也比较简单,但是在上面的协同过滤思想中,有一个致命的问题就是内存占用问题,因为上面我们仅仅做了5000首歌曲和1000个用户之间的协同过滤算法,在实际的项目应用中遇到的item和user都是千万级甚至是亿级的,我们不可能创建出一个这么大的user—items矩阵的,所以针对这个问题,我们需要对这些矩阵进行分解。

SVD矩阵分解

假设一个user-item矩阵 ,m表示user个数,n表示item个数,那可以得到样本的协方差矩阵C,将C进行矩阵分解,得到一组正交特征向量及对应的特征值。就是下面这个图的样子。
在这里插入图片描述

到这里,怎么用SVD做推荐的原理就看的很明白了。 就是先根据user-item的共现矩阵,进行分解后得到user的特征向量矩阵奇异值特征矩阵及item的特征向量矩阵。然后就可以用这些向量矩阵来表示每一个user或item,这就跟embedding原理类似。

假设我们有一个矩阵,该矩阵每一列代表一个user,每一行代表一个item

  • 这里出现了新的矩阵展现方式,不再是0、1数据了,其中的0~5代表着该用户对物品的喜爱程度
BenTomJohnFred
season15505
season25034
season33403
season40053
season55445
season65455

对矩阵进行SVD分解,将得到USV

from scipy.sparse.linalg import svds
U, s, Vt = svds(A)

在这里插入图片描述
可以将这三个矩阵抽取前两列拿出相乘
在这里插入图片描述
重新计算得到A2矩阵
在这里插入图片描述
在这里插入图片描述

重新计算 USV的结果得到A2 来比较下A2和A的差异,看起来差异是有的,但是并不大,所以我们可以近似来代替,起到了一定的内存缓冲,减小了空间和时间。

在这里插入图片描述
在这里插入图片描述

知道Bob的坐标后计算他与其他用户的余弦相似度即可找到与他最相似的用户,进而把他的相似用户喜欢的物品推荐给Bob

但是!! SVD仍然不适合做推荐,为什么呢?

  • SVD做奇异值分解,是基于共现矩阵,和上面说的基于用户、物品的协同过滤一样的,我们仍然不能将这么大的共现矩阵载入到内存进行分解。
  • 共现矩阵的处理要经过初始化,对于缺失值多的矩阵如何填充缺失值是一个问题,在上面这个案例中,把没有用户使用过物品的分数打分为0分,但是在实际代入矩阵的含义中,0分其实代表了用户很不喜欢这个物品,这就造成了解释上的歧义,所以到底把用户没有使用过的物品打多少分就成为了一个问题。
LFM矩阵分解(隐语义模型)

LFM矩阵分解就解决了上述的两个问题,一个是内存占用问题,LFM不会加载这个大矩阵到内存中,并且在缺失值上可以任然保持缺失,无需填补。因为这个算法不是在用户-物品矩阵上操刀的。

  • 还是假设一个user-item矩阵,m表示user个数,n表示item个数,初始化两个user矩阵P与item矩阵Q,即每一个user、item都初始化一个向量表示,A矩阵格子中已有的值就是一条训练数据,用Vuser*Vitem 去拟合,格子中没有的值不用管。这样就成功解决了共现矩阵的问题以及初始化的问题。

在这里插入图片描述

LFM与SVD不一样,SVD是使用数学拆解方法使得分解得到的三个矩阵相乘可以得到原矩阵,但是LFM是模型方法,先创造出P、Q矩阵,使得P、Q矩阵乘积约等于A矩阵,这里就要不断对P、Q矩阵进行梯度下降算法的优化。这里有两种优化方法,使用交替最小二乘法或者随机梯度下降发法得到最佳的用户矩阵和物品矩阵。可以使得两个矩阵乘积近似于原矩阵,但不能完全相等,肯定会有损失。这就需要在精度与空间/时间之间做出抉择,想要迭代后的最终损失越小,PQ矩阵的就越大。

#从表中选取字段'user','song_id','play_count'
dataset = data_song_user_recom[['user','song_id','play_count']]
#计算每个用户的总共播放量
_data = pd.DataFrame(data_song_user_recom.groupby('user')['play_count'].sum()).reset_index()
_data.columns=['user','user_play_count']
#把用户的总共播放量并到刚刚抽取的表中
dataset = dataset.merge(_data,on='user',how='left')
#计算每个用户对自己听过的音乐打分(单首播放量/总播放量)
dataset['rating'] = dataset['play_count']/dataset['user_play_count']
#数据仅留下user、song_id、rating三个字段即可
dataset.drop(['play_count','user_play_count'],axis=1,inplace=True)
dataset
usersong_idrating
bb85bb79612e5373ac714fcd4469cabeb5ed94e1SOCMHGT12A8C138D8A7.399577
bb85bb79612e5373ac714fcd4469cabeb5ed94e1SOGFKJE12A8C138D6A9.513742
bb85bb79612e5373ac714fcd4469cabeb5ed94e1SOJAMXH12A8C138D9B9.513742
bb85bb79612e5373ac714fcd4469cabeb5ed94e1SOJWBZK12A58A78AF710.570825
bb85bb79612e5373ac714fcd4469cabeb5ed94e1SOMMJUQ12AF72A59318.456660
739ee4c555b0558cbf2c75abc0782f3f10941c35SOZRYWL12A67ADD5121.986755
739ee4c555b0558cbf2c75abc0782f3f10941c35SOZVCRW12A67ADA0B75.298013
739ee4c555b0558cbf2c75abc0782f3f10941c35SOZVMYF12A8C1326460.662252
739ee4c555b0558cbf2c75abc0782f3f10941c35SOZVUCT12A8C1424BE1.324503
739ee4c555b0558cbf2c75abc0782f3f10941c35SOZVVRE12A8C1431507.284768

这里采用的是随机梯度下降法拟合PQ矩阵

#分组聚合
users_ratings = dataset.groupby('user').agg([list])
items_ratings = dataset.groupby('song_id').agg([list])
# 计算全局平均分(当输入的user_id和song_id不在原来数据中,直接返回全局平均分)
global_mean = dataset['rating'].mean()
# 初始化P Q  
# User-LF  10 代表 隐含因子个数是10个(随机梯度下降的随机体现在这里)
P = dict(zip(users_ratings.index,np.random.rand(len(users_ratings),10).astype(np.float32)))
# Item-LF
Q = dict(zip(items_ratings.index,np.random.rand(len(items_ratings),10).astype(np.float32)))
#梯度下降优化损失函数
for i in range(1,21):
    print('*'*10,'迭代次数',i,'*'*10)
    for uid,iid,real_rating in dataset.itertuples(index = False):
        #遍历 用户 物品的评分数据 通过用户的id 到用户矩阵中获取用户向量
        v_puk = P[uid]
        # 通过物品的uid 到物品矩阵里获取物品向量
        v_qik = Q[iid]
        #计算损失
        error = real_rating-np.dot(v_puk,v_qik)
        # 0.02学习率 0.01正则化系数
        v_puk += 0.02*(error*v_qik-0.01*v_puk)
        v_qik += 0.02*(error*v_puk-0.01*v_qik)
        
        P[uid] = v_puk
        Q[iid] = v_qik

********** 迭代次数 1 **********
********** 迭代次数 2 **********
********** 迭代次数 3 **********
********** 迭代次数 4 **********
********** 迭代次数 5 **********
********** 迭代次数 6 **********
********** 迭代次数 7 **********
********** 迭代次数 8 **********
********** 迭代次数 9 **********
********** 迭代次数 10 **********
********** 迭代次数 11 **********
********** 迭代次数 12 **********
********** 迭代次数 13 **********
********** 迭代次数 14 **********
********** 迭代次数 15 **********
********** 迭代次数 16 **********
********** 迭代次数 17 **********
********** 迭代次数 18 **********
********** 迭代次数 19 **********
********** 迭代次数 20 **********

def predict(uid, iid):
    # 如果uid或iid不在,我们使用全剧平均分作为预测结果返回
    if uid not in users_ratings.index or iid not in items_ratings.index:
        print('haha')
        return global_mean
    p_u = P[uid]
    q_i = Q[iid]

    return np.dot(p_u, q_i)
predict('bb85bb79612e5373ac714fcd4469cabeb5ed94e1', 'SOCMHGT12A8C138D8A')

0.18866214

将上面的核心代码包装成函数

#将上面的核心代码包装成函数
'''
LFM Model
'''
import pandas as pd
import numpy as np

# 评分预测    1-5
class LFM(object):

    def __init__(self, alpha, reg_p, reg_q, number_LatentFactors=10, number_epochs=10, columns=["uid", "iid", "rating"]):
        self.alpha = alpha # 学习率
        self.reg_p = reg_p    # P矩阵正则
        self.reg_q = reg_q    # Q矩阵正则
        self.number_LatentFactors = number_LatentFactors  # 隐式类别数量
        self.number_epochs = number_epochs    # 最大迭代次数
        self.columns = columns

    def fit(self, dataset):
        '''
        fit dataset
        :param dataset: uid, iid, rating
        :return:
        '''

        self.dataset = pd.DataFrame(dataset)

        self.users_ratings = dataset.groupby(self.columns[0]).agg([list])[[self.columns[1], self.columns[2]]]
        self.items_ratings = dataset.groupby(self.columns[1]).agg([list])[[self.columns[0], self.columns[2]]]

        self.globalMean = self.dataset[self.columns[2]].mean()

        self.P, self.Q = self.sgd()

    def _init_matrix(self):
        '''
        初始化P和Q矩阵,同时为设置0,1之间的随机值作为初始值
        :return:
        '''
        # User-LF
        P = dict(zip(
            self.users_ratings.index,
            np.random.rand(len(self.users_ratings), self.number_LatentFactors).astype(np.float32)
        ))
        # Item-LF
        Q = dict(zip(
            self.items_ratings.index,
            np.random.rand(len(self.items_ratings), self.number_LatentFactors).astype(np.float32)
        ))
        return P, Q

    def sgd(self):
        '''
        使用随机梯度下降,优化结果
        :return:
        '''
        P, Q = self._init_matrix()

        for i in range(self.number_epochs):
            print("iter%d"%i)
            error_list = []
            for uid, iid, r_ui in self.dataset.itertuples(index=False):
                # User-LF P
                ## Item-LF Q
                v_pu = P[uid] #用户向量
                v_qi = Q[iid] #物品向量
                err = np.float32(r_ui - np.dot(v_pu, v_qi))

                v_pu += self.alpha * (err * v_qi - self.reg_p * v_pu)
                v_qi += self.alpha * (err * v_pu - self.reg_q * v_qi)
                
                P[uid] = v_pu 
                Q[iid] = v_qi

                # for k in range(self.number_of_LatentFactors):
                #     v_pu[k] += self.alpha*(err*v_qi[k] - self.reg_p*v_pu[k])
                #     v_qi[k] += self.alpha*(err*v_pu[k] - self.reg_q*v_qi[k])

                error_list.append(err ** 2)
            print(np.sqrt(np.mean(error_list)))
        return P, Q

    def predict(self, uid, iid):
        # 如果uid或iid不在,我们使用全剧平均分作为预测结果返回
        if uid not in self.users_ratings.index or iid not in self.items_ratings.index:
            return self.globalMean

        p_u = self.P[uid]
        q_i = self.Q[iid]
        return np.dot(p_u, q_i)

    def test(self,testset):
        '''预测测试集数据'''
        for uid, iid, real_rating in testset.itertuples(index=False):
            try:
                pred_rating = self.predict(uid, iid)
            except Exception as e:
                print(e)
            else:
                yield uid, iid, real_rating, pred_rating

#训练LFM模型
lfm = LFM(0.1, 0.01, 0.01, 10, 100, ["user", "song_id", "rating"])
lfm.fit(dataset)
#制作小工具,输入用户ID和歌曲ID即可返回用户对此首歌曲的预测评分(用户未听歌之前)
while True:
    uid = input("uid: ")
    iid = input("iid: ")
    if uid=='q' and iid=='q':
        break
    elseprint(lfm.predict(uid, iid))

如果说想要每一个用户返回一个推荐列表(从高到低),只需要固定用户ID,循环遍历歌曲ID,输出全部分数后排序,然后去除掉用户听过的歌曲即可。

如果你的代码运行到这,那么恭喜你,你已经得到了你自己制作的第一个推荐算法了~

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值