推荐系统(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)
user | song_id | play_count | |
---|---|---|---|
0 | b80344d063b5ccb3212f76538f3d9e43d87dca9e | SOAKIMP12A8C130995 | 1 |
1 | b80344d063b5ccb3212f76538f3d9e43d87dca9e | SOAPDEY12A81C210A9 | 1 |
2 | b80344d063b5ccb3212f76538f3d9e43d87dca9e | SOBBMDR12A8C13253B | 2 |
3 | b80344d063b5ccb3212f76538f3d9e43d87dca9e | SOBFNSP12AF72A0E22 | 1 |
4 | b80344d063b5ccb3212f76538f3d9e43d87dca9e | SOBFOVM12A58A7D494 | 1 |
5 | b80344d063b5ccb3212f76538f3d9e43d87dca9e | SOBNZDC12A6D4FC103 | 1 |
6 | b80344d063b5ccb3212f76538f3d9e43d87dca9e | SOBSUJE12A6D4F8CF5 | 2 |
7 | b80344d063b5ccb3212f76538f3d9e43d87dca9e | SOBVFZR12A6D4F8AE3 | 1 |
8 | b80344d063b5ccb3212f76538f3d9e43d87dca9e | SOBXALG12A8C13C108 | 1 |
9 | b80344d063b5ccb3212f76538f3d9e43d87dca9e | SOBXHDL12A81C204C0 | 1 |
注意:这个表大小为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_id | title | song_id | release | artist_id | artist_mbid | artist_name | duration | artist_familiarity | artist_hotttnesss | year | track_7digitalid | shs_perf | shs_work | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | TRMMMYQ128F932D901 | Silent Night | SOQMMHC12AB0180CB8 | Monster Ballads X-Mas | ARYZTJS1187B98C555 | 357ff05d-848a-44cf-b608-cb34b5701ae5 | Faster Pussy cat | 252.05506 | 0.649822 | 0.394032 | 2003 | 7032331 | -1 | 0 |
1 | TRMMMKD128F425225D | Tanssi vaan | SOVFVAK12A8C1350D9 | Karkuteillä | ARMVN3U1187FB3A1EB | 8d7ef530-a6fd-4f8f-b2e2-74aec765e0f9 | Karkkiautomaatti | 156.55138 | 0.439604 | 0.356992 | 1995 | 1514808 | -1 | 0 |
2 | TRMMMRX128F93187D9 | No One Could Ever | SOGTUKN12AB017F4F1 | Butter | ARGEKB01187FB50750 | 3d403d44-36ce-465c-ad43-ae877e65adc4 | Hudson Mohawke | 138.97098 | 0.643681 | 0.437504 | 2006 | 6945353 | -1 | 0 |
3 | TRMMMCH128F425532C | Si Vos Querés | SOBNYVR12A8C13558C | De Culo | ARNWYLR1187B9B2F9C | 12be7648-7094-495f-90e6-df4189d68615 | Yerba Brava | 145.05751 | 0.448501 | 0.372349 | 2003 | 2168257 | -1 | 0 |
4 | TRMMMWA128F426B589 | Tangle Of Aspens | SOHSBXH12A8C13B0DF | Rene Ablaze Presents Winter Sessions | AREQDTE1269FB37231 | Der Mystic | 514.29832 | 0.000000 | 0.000000 | 0 | 2264873 | -1 | 0 |
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)
Total | Percent | |
---|---|---|
user | 0 | 0.0 |
artist_name | 0 | 0.0 |
year | 0 | 0.0 |
song_id | 0 | 0.0 |
release | 0 | 0.0 |
title | 0 | 0.0 |
play_count | 0 | 0.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
title | score | rank | |
---|---|---|---|
220061 | Sehr kosmisch | 110479 | 1 |
279267 | Undo | 90479 | 2 |
63471 | Dog Days Are Over (Radio Edit) | 90444 | 3 |
303140 | You’re The One | 89287 | 4 |
209422 | Revelry | 80666 | 5 |
219526 | Secrets | 79260 | 6 |
108691 | Horn Concerto No. 4 in E flat K495: II. Romanc… | 69487 | 7 |
83577 | Fireflies | 64933 | 8 |
105587 | Hey_ Soul Sister | 63809 | 9 |
270293 | Tive Sim | 58621 | 10 |
183334 | OMG | 53260 | 11 |
280663 | Use Somebody | 52080 | 12 |
68052 | Drop The World | 51022 | 13 |
159669 | Marry Me | 47734 | 14 |
39032 | Canada | 46384 | 15 |
40952 | Catch You Baby (Steve Pitron & Max Sanna Radio… | 46077 | 16 |
261462 | The Scientist | 45558 | 17 |
133035 | Just Dance | 42745 | 18 |
45648 | Clocks | 42733 | 19 |
202513 | Pursuit Of Happiness (nightmare) | 41811 | 20 |
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)
user | song_id | play_count | title | release | artist_name | year | |
---|---|---|---|---|---|---|---|
0 | bb85bb79612e5373ac714fcd4469cabeb5ed94e1 | SOCMHGT12A8C138D8A | 7 | Heard Them Stirring | Fleet Foxes | Fleet Foxes | 2008 |
1 | bb85bb79612e5373ac714fcd4469cabeb5ed94e1 | SOGFKJE12A8C138D6A | 9 | Sun It Rises | Fleet Foxes | Fleet Foxes | 2008 |
2 | bb85bb79612e5373ac714fcd4469cabeb5ed94e1 | SOJAMXH12A8C138D9B | 9 | Meadowlarks | Fleet Foxes | Fleet Foxes | 2008 |
3 | bb85bb79612e5373ac714fcd4469cabeb5ed94e1 | SOJWBZK12A58A78AF7 | 10 | Tiger Mountain Peasant Song | Fleet Foxes | Fleet Foxes | 2008 |
4 | bb85bb79612e5373ac714fcd4469cabeb5ed94e1 | SOMMJUQ12AF72A5931 | 8 | Your Protector | Fleet Foxes | Fleet Foxes | 2008 |
#建立用户—物品(音乐)矩阵
user_music_matric = pd.crosstab(data_song_user_recom.user,data_song_user_recom.song_id)
user_music_matric.head(5)
SOAACPJ12A81C21360 | SOAAFAC12A67ADF7EB | SOAAFYH12A8C13717A | SOAAROC12A6D4FA420 | SOAATLI12A8C13E319 | SOAAUKC12AB017F868 | SOAAVUV12AB0186646 | SOAAWEE12A6D4FBEC8 | SOABHYV12A6D4F6D0F | SOABJBU12A8C13F63F | … | SOZXTUT12A6D4F6D03 | SOZYBGN12A8C13A93C | SOZYDZR12A8C13F4F0 | SOZYNFV12AB0186910 | SOZYSDT12A8C13BFD7 | SOZYUGZ12A8AE472AC | SOZZHQT12AB018B714 | SOZZLZN12A8AE48D6D | SOZZTCU12AB0182C58 | SOZZTNF12A8C139916 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
002b63a7e2247de6d62bc62f253474edc7dd044c | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | … | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
003a5e3285141b1a54edbc51fbfa1cc922023aae | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | … | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
008065e52b59a094dcfca17d361c5ae4b4eb767f | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | … | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0086dd8bbc9290ff2cafc04b807b87aec719aa36 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | … | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0091e0326c4c034cc04be6454742912845740a1f | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | … | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
上面表图就是数据集的用户—物品(音乐)矩阵,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)
SOAACPJ12A81C21360 | SOAAFAC12A67ADF7EB | SOAAFYH12A8C13717A | SOAAROC12A6D4FA420 | SOAATLI12A8C13E319 | SOAAUKC12AB017F868 | SOAAVUV12AB0186646 | SOAAWEE12A6D4FBEC8 | SOABHYV12A6D4F6D0F | SOABJBU12A8C13F63F | … | SOZXTUT12A6D4F6D03 | SOZYBGN12A8C13A93C | SOZYDZR12A8C13F4F0 | SOZYNFV12AB0186910 | SOZYSDT12A8C13BFD7 | SOZYUGZ12A8AE472AC | SOZZHQT12AB018B714 | SOZZLZN12A8AE48D6D | SOZZTCU12AB0182C58 | SOZZTNF12A8C139916 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
SOAACPJ12A81C21360 | 1.000000 | 0.00 | 0.00 | 0.000000 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | … | 0.000000 | 0.000000 | 0.00000 | 0.0 | 0.000000 | 0.000000 | 0.00 | 0.0 | 0.000000 | 0.041667 |
SOAAFAC12A67ADF7EB | 0.000000 | 1.00 | 0.00 | 0.000000 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.000000 | 0.006211 | … | 0.000000 | 0.041667 | 0.10000 | 0.0 | 0.013514 | 0.000000 | 0.05 | 0.0 | 0.000000 | 0.000000 |
SOAAFYH12A8C13717A | 0.000000 | 0.00 | 1.00 | 0.000000 | 0.0 | 0.0 | 0.053571 | 0.052632 | 0.058824 | 0.006369 | … | 0.000000 | 0.000000 | 0.00000 | 0.0 | 0.014286 | 0.000000 | 0.00 | 0.0 | 0.000000 | 0.040000 |
SOAAROC12A6D4FA420 | 0.000000 | 0.00 | 0.00 | 1.000000 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.040816 | 0.005814 | … | 0.035714 | 0.028571 | 0.00000 | 0.0 | 0.075000 | 0.030303 | 0.00 | 0.0 | 0.033333 | 0.000000 |
SOAATLI12A8C13E319 | 0.000000 | 0.00 | 0.00 | 0.000000 | 1.0 | 0.0 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | … | 0.000000 | 0.000000 | 0.00000 | 0.0 | 0.000000 | 0.000000 | 0.00 | 0.0 | 0.000000 | 0.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代表着该用户对物品的喜爱程度
Ben | Tom | John | Fred | |
---|---|---|---|---|
season1 | 5 | 5 | 0 | 5 |
season2 | 5 | 0 | 3 | 4 |
season3 | 3 | 4 | 0 | 3 |
season4 | 0 | 0 | 5 | 3 |
season5 | 5 | 4 | 4 | 5 |
season6 | 5 | 4 | 5 | 5 |
对矩阵进行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
user | song_id | rating |
---|---|---|
bb85bb79612e5373ac714fcd4469cabeb5ed94e1 | SOCMHGT12A8C138D8A | 7.399577 |
bb85bb79612e5373ac714fcd4469cabeb5ed94e1 | SOGFKJE12A8C138D6A | 9.513742 |
bb85bb79612e5373ac714fcd4469cabeb5ed94e1 | SOJAMXH12A8C138D9B | 9.513742 |
bb85bb79612e5373ac714fcd4469cabeb5ed94e1 | SOJWBZK12A58A78AF7 | 10.570825 |
bb85bb79612e5373ac714fcd4469cabeb5ed94e1 | SOMMJUQ12AF72A5931 | 8.456660 |
… | … | … |
739ee4c555b0558cbf2c75abc0782f3f10941c35 | SOZRYWL12A67ADD512 | 1.986755 |
739ee4c555b0558cbf2c75abc0782f3f10941c35 | SOZVCRW12A67ADA0B7 | 5.298013 |
739ee4c555b0558cbf2c75abc0782f3f10941c35 | SOZVMYF12A8C132646 | 0.662252 |
739ee4c555b0558cbf2c75abc0782f3f10941c35 | SOZVUCT12A8C1424BE | 1.324503 |
739ee4c555b0558cbf2c75abc0782f3f10941c35 | SOZVVRE12A8C143150 | 7.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_pukQ[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_meanp_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 = columnsdef 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-LFP = dict(zip(self.users_ratings.index,np.random.rand(len(self.users_ratings), self.number_LatentFactors).astype(np.float32)))# Item-LFQ = dict(zip(self.items_ratings.index,np.random.rand(len(self.items_ratings), self.number_LatentFactors).astype(np.float32)))return P, Qdef 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 Qv_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, Qdef predict(self, uid, iid):# 如果uid或iid不在,我们使用全剧平均分作为预测结果返回if uid not in self.users_ratings.index or iid not in self.items_ratings.index:return self.globalMeanp_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':breakelse:print(lfm.predict(uid, iid))
如果说想要每一个用户返回一个推荐列表(从高到低),只需要固定用户ID,循环遍历歌曲ID,输出全部分数后排序,然后去除掉用户听过的歌曲即可。
如果你的代码运行到这,那么恭喜你,你已经得到了你自己制作的第一个推荐算法了~
推荐系统(1)——先做一个出来(先实战,后理论)相关推荐
- 《看聊天记录都学不会Python到游戏实战?太菜了吧》(8)我们开始做一个数字小游戏吧
本系列文章将会以通俗易懂的对话方式进行教学,对话中将涵盖了新手在学习中的一般问题.此系列将会持续更新,包括别的语言以及实战都将使用对话的方式进行教学,基础编程语言教学适用于零基础小白,之后实战课程也将 ...
- 徒弟做了一个Python的实战小项目——银行系统
国际惯例:实践是检验真理的唯一标准. 众所周知,在编程过程中理论知识再充实也需要通过项目的炼金石.下面给大家看一下我徒弟做的一个小项目实战要求,是做一个银行系统,就是我们去银行办业务时候会有个自助的A ...
- GUI实战|Python做一个文档图片提取软件
大家好,本文将进一步讲解如何用Python提取PDF与Word中图片,并结合之前讲解过的GUI框架PysimpleGUI,做一个多文件图片提取软件,效果如下: 本文主要将分为以下部分讲解: PDF.W ...
- 10分钟做一个新闻问答web站点[iVX低代码实战]
一.创建首页 进入到iVX线上编辑器后,选择相对定位. 点击创建后进入到 IDE 之中: 我们在第一步中首先创建一个首页.点击左侧组件栏中的页面组件,创建一个页面: 接着重命名该页面为Home,在页面 ...
- python实战扫码下载_实例:用 Python 做一个扫码工具
原标题:实例:用 Python 做一个扫码工具 来自公众号: 新建文件夹X 链接:https://blog.csdn.net/ZackSock/article/details/108610957Pyt ...
- vue实战案例:用学过的知识做一个小demo
学过了前面11个章节的知识,可以说你已经对vue2.0的基础知识有了一定程度的掌握,虽然在真正开发过程中,一些知识的使用会稍有不同,但是别慌,我们会把那部分内容在进阶系列,比如:单文件组件,过渡效果, ...
- MoviePy - 中文文档4-MoviePy实战案例-给MoviePy Logo做一个闪动的阴影效果
回到目录 给MoviePy Logo做一个闪动的阴影效果 一起交流,一起进步,群内提问答疑 QQ群:MoviePy中文 :819718037 回到目录
- CSS3 做一个旋转的立体3D正方形 动效核心【前端就业课 第二阶段】CSS 零基础到实战(07)
若是大一学子或者是真心想学习刚入门的小伙伴可以私聊我,若你是真心学习可以送你书籍,指导你学习,给予你目标方向的学习路线,无套路,博客为证. 一.transform-origin transform-o ...
- Python做一个Kindle电子书下载助手,真香!
哈喽,大家好,我是菜鸟哥! 大家有没有想过把亚马逊网站上的Kindle电子书下载到自己的电脑上? 今天分享的项目可以帮大家实现这一目的.该项目用Python开发,简单.好用.开源. 下面分享下项目的使 ...
最新文章
- ACS AAA Tacacs+
- 用java发送邮件(黄海已测试通过)
- leetcode107. 二叉树的层次遍历 II
- JavaScript 插入元素到数组的方法汇总
- 对象创建从农业社会到共产主义的发展
- limesurvey php5.2,Limesurvey二次开发(接入CAS统一身份认证)随笔
- 知识库构建前沿:自动和半自动知识提取
- 英剧推荐【IT狂人】
- esxi - with nvidia geforce 10 titan xp card
- 将 libVLC 视频渲染到 QWidget 中
- nacos启动成功无法访问
- 【Nginx】Nginx配置实例-反向代理
- 回文子串是什么意思?
- 微信读书从本地导入书籍失败
- 求csdn积分啊呜呜呜
- 直线度测量告别手工测量时代
- 百度云盘和谐下载和云播
- 已知三个点坐标求 三角形面积 || 求任意多边形面积公式||判断点在直线的左侧还是右侧
- 数学分析教程史济怀练习9.10
- python中point什么意思_在Python中创建一个Point类
热门文章
- 什么是反射以及反射的作用
- 使用 HTML5 和 CSS3 创建现代 Web 站点
- 网易七鱼客服对接记录以及Vue项目里使用
- 第01章 Spring-Boot 应用文件application配置
- 【机器学习】一文详解GBDT、Xgboost、Boosting与Bagging之间的区别
- Linux中使用命令分类型统计系统光盘中rpm包数量
- Visial Studio中“变量已被优化掉 因而不可用”的解决方案
- PHP For循环字母A-Z当超过26个字母时输出AA,AB,AC
- HTML小黄人吃球球GO域名跳转源码
- Linux上安装git