欣灌费研仍在继续,不能返校只好在家做毕设,毕设的内容是利用地泼雷妞技术优化推荐算法。
悲剧的是地泼雷妞不会,推荐系统算法也不懂,包含推荐系统的东西倒是用过不少(某宝,某东,某音)。
只好从最基础的开始学,先学推荐算法。
打开Microsoft Edge,搜索推荐系统总结,得到该文章《推荐系统干货总结》,如获至宝。
文章中推荐了一本书《推荐系统实践》,我决定先从这本书开始。
电子书链接:https://pan.baidu.com/s/12BPDnGPe7jgXlqTXgT8i1g ,提取码:mnve
本文是我的学习笔记,对应此书2.4.1小节,
原理就不介绍了,直接上数据集和代码。
不懂原理的,先看这个 基于用户的协同过滤推荐算法原理和实现
下面进入正题
数据集
采用GroupLens提供的MovieLens数据集。 MovieLens数据集有3个不同的版本,书中选用中等大小的数据集。该数据集包含6000多用户对4000多部电影的100万条评分。该数据集是一个评分数据集,用户可以给电影评5个不同等级的分数(1~5分)。
数据集来源 https://grouplens.org/datasets/movielens/1m/
你要是实在特别懒的话,这里有百度网盘链接,https://pan.baidu.com/s/1QaOW5_1quMRDsBobD6CIsw ,提取码:o1rt
数据集中包含4个文件,movies.dat,users.dat,ratings.dat,README
movies.dat
1::Toy Story (1995)::Animation|Children's|Comedy 2::Jumanji (1995)::Adventure|Children's|Fantasy 3::Grumpier Old Men (1995)::Comedy|Romance 4::Waiting to Exhale (1995)::Comedy|Drama 5::Father of the Bride Part II (1995)::Comedy 6::Heat (1995)::Action|Crime|Thriller 7::Sabrina (1995)::Comedy|Romance 8::Tom and Huck (1995)::Adventure|Children's 9::Sudden Death (1995)::Action 10::GoldenEye (1995)::Action|Adventure|Thriller .... ....
每行数据由三部分构成 MovieID::Title::Genres (Genres可能有多个)
users.dat
1::F::1::10::48067 2::M::56::16::70072 3::M::25::15::55117 4::M::45::7::02460 5::M::25::20::55455 6::F::50::9::55117 7::M::35::1::06810 8::M::25::12::11413 9::M::25::17::61614 10::F::35::1::95370 .... ....
每行数据由四部分构成 UserID::Gender::Age::Occupation::Zip-code
ratings.dat
1::1193::5::978300760 1::661::3::978302109 1::914::3::978301968 1::3408::4::978300275 1::2355::5::978824291 1::1197::3::978302268 1::1287::5::978302039 1::2804::5::978300719 1::594::4::978302268 1::919::4::978301368 .... .... 每行数据由四部分构成 UserID::MovieID::Rating::Timestamp
看到这,你可能还对数据内容即格式有疑问,实际上我这个算法是最基本的算法,只需知道有多少部电影,有多少个用户,每个用户对那些电影评过分即可,想深入了解数据的自己去看README文档,看懂了告诉我Zip-code是什么意思
Code
代码来源:https://github.com/Lockvictor/MovieLens-RecSys
Lockvictor 大哥写了一个类 class UserBasedCF()来实现算法,类中包括这几个函数
def __init__(self) 初始化变量 def loadfile(filename) 读取文件到一个生成器里面 def generate_dataset(self, filename, pivot=0.7) 得到训练集,测试集,及其他参数 def calc_user_sim(self) 计算用户相似度 def recommend(self, user) 给用户做出推荐列表 def evaluate(self) 评价算法
来看一下我认为这段代码中比较值得注意的几个点
def __init__(self)
def __init__(self):
self.trainset = {}
self.testset = {}
self.n_sim_user = 20
self.n_rec_movie = 10
self.user_sim_mat = {}
self.movie_popular = {}
self.movie_count = 0
print('Similar user number = %d' % self.n_sim_user, file=sys.stderr)
print('recommended movie number = %d' %
self.n_rec_movie, file=sys.stderr)
这个初始化函数中很多变量采用了字典类型的数据结构,对字典不熟悉的点这里,Python 字典(Dictionary)
print()函数中 file=sys.stderr 的意思是输出的字体为红色,红色字体本意是报错用的,不知道作者咋想的。
def loadfile(filename)
def loadfile(filename):
''' load a file, return a generator. '''
fp = open(filename, 'r')
for i, line in enumerate(fp):
yield line.strip('\r\n')
if i % 100000 == 0:
print('loading %s(%s)' % (filename, i), file=sys.stderr)
fp.close()
print('load %s succ' % filename, file=sys.stderr)
注意代码中的yield,yield 的作用就是把loadfile()函数变成一个 generator,这个generator中存放了所有行的数据。
深入了解yield点这里,Python yield 使用浅析
def generate_dataset(self, filename, pivot=0.7)
这段代码太长,不复制了。
参数pivot=0.7意思是随机切分数据集时,训练集70%,测试集30%
调用这个函数之后,可以得到
self.trainset = {user1:{movie1:rating, movie2:rating ...} user2:{...}...} 训练集 self.testset = {user1:{movie1:rating, movie2:rating ...} user2:{...}...} 测试集
两层的字典,第一层为用户ID,第二层中KEY为用户看过的电影ID,VALUE为用户对这部电影的评分。
def calc_user_sim(self)
调用这个函数之后,可以得到
movie2users 传说中的倒查表 self.user_sim_mat 用户相似度矩阵 self.movie_popular 每个电影的流行度 self.movie_count 训练集中电影总数
这四个变量如何得到的详见代码,不可能看不懂的
def recommend(self, user)
函数返回值 return sorted(rank.items(), key=itemgetter(1), reverse=True)[0:N]
这个函数是什么意思呢,听我解释,它干了这么个事
举个例子,对于测试集用的用户A,由相似度矩阵,找到与其相似度最高的前20个用户,
然后找出这20个用户看过而A没看过的电影 movie1,movie2,movie3,movie1,.....
注意到前面出现了movie1出现了两次,因为这20个用户中有人看过相同的电影,把相同的电影对应的用户相似度相加
得到{movie:similarity_factor...},然后找到similarity_factor排名前10的推荐给A用户。
认真看代码能看懂。
def evaluate(self)
这个函数涉及到了召回率和准确率
precision = hit / (1.0 * rec_count) recall = hit / (1.0 * test_count)
hit 是测试集中用户的评分列表里有,给用户的推荐列表里有,而测试集中用户的评分列表里没有的电影总数。
就是你推荐了用户没有看过的又 喜欢的电影总数。
所以举个例子,准确率就是,你推荐了100个,hit 了10个,那准确率就是10%
而召回率就是,你理论上可以推荐100个让用户 hit 的电影,而你只推荐了这100个里面的20个,召回率就是20%
其他的指标覆盖率等等,详见代码。
下面附上完整代码
# -*- coding: utf-8 -*-
'''
Created on 2015-06-22
@author: Lockvictor
'''
import sys
import random
import math
import os
from operator import itemgetter
random.seed(0)
class UserBasedCF(object):
''' TopN recommendation - User Based Collaborative Filtering '''
def __init__(self):
self.trainset = {}
self.testset = {}
self.n_sim_user = 20
self.n_rec_movie = 10
self.user_sim_mat = {}
self.movie_popular = {}
self.movie_count = 0
print('Similar user number = %d' % self.n_sim_user, file=sys.stderr)
print('recommended movie number = %d' %
self.n_rec_movie, file=sys.stderr)
@staticmethod
def loadfile(filename):
''' load a file, return a generator. '''
fp = open(filename, 'r')
for i, line in enumerate(fp):
yield line.strip('\r\n')
if i % 100000 == 0:
print('loading %s(%s)' % (filename, i), file=sys.stderr)
fp.close()
print('load %s succ' % filename, file=sys.stderr)
def generate_dataset(self, filename, pivot=0.7):
''' load rating data and split it to training set and test set '''
trainset_len = 0
testset_len = 0
for line in self.loadfile(filename):
user, movie, rating, _ = line.split('::')
# split the data by pivot
if random.random() < pivot:
self.trainset.setdefault(user, {})
self.trainset[user][movie] = int(rating)
trainset_len += 1
else:
self.testset.setdefault(user, {})
self.testset[user][movie] = int(rating)
testset_len += 1
print('split training set and test set succ', file=sys.stderr)
print('train set = %s' % trainset_len, file=sys.stderr)
print('test set = %s' % testset_len, file=sys.stderr)
def calc_user_sim(self):
''' calculate user similarity matrix '''
# build inverse table for item-users
# key=movieID, value=list of userIDs who have seen this movie
print('building movie-users inverse table...', file=sys.stderr)
movie2users = dict()
for user, movies in self.trainset.items():
print(user,movies)
for movie in movies:
# inverse table for item-users
if movie not in movie2users:
movie2users[movie] = set()
movie2users[movie].add(user)
# count item popularity at the same time
if movie not in self.movie_popular:
self.movie_popular[movie] = 0
self.movie_popular[movie] += 1
print('build movie-users inverse table succ', file=sys.stderr)
# save the total movie number, which will be used in evaluation
self.movie_count = len(movie2users)
print('total movie number = %d' % self.movie_count, file=sys.stderr)
# count co-rated items between users
usersim_mat = self.user_sim_mat
print('building user co-rated movies matrix...', file=sys.stderr)
for movie, users in movie2users.items():
for u in users:
for v in users:
if u == v:
continue
usersim_mat.setdefault(u, {})
usersim_mat[u].setdefault(v, 0)
usersim_mat[u][v] += 1
print('build user co-rated movies matrix succ', file=sys.stderr)
# calculate similarity matrix
print('calculating user similarity matrix...', file=sys.stderr)
simfactor_count = 0
PRINT_STEP = 2000000
for u, related_users in usersim_mat.items():
for v, count in related_users.items():
usersim_mat[u][v] = count / math.sqrt(
len(self.trainset[u]) * len(self.trainset[v]))
simfactor_count += 1
if simfactor_count % PRINT_STEP == 0:
print('calculating user similarity factor(%d)' %
simfactor_count, file=sys.stderr)
print('calculate user similarity matrix(similarity factor) succ',
file=sys.stderr)
print('Total similarity factor number = %d' %
simfactor_count, file=sys.stderr)
def recommend(self, user):
''' Find K similar users and recommend N movies. '''
K = self.n_sim_user
N = self.n_rec_movie
rank = dict()
watched_movies = self.trainset[user]
for similar_user, similarity_factor in sorted(self.user_sim_mat[user].items(),
key=itemgetter(1), reverse=True)[0:K]:
for movie in self.trainset[similar_user]:
if movie in watched_movies:
continue
# predict the user's "interest" for each movie
rank.setdefault(movie, 0)
rank[movie] += similarity_factor
# return the N best movies
return sorted(rank.items(), key=itemgetter(1), reverse=True)[0:N]
def evaluate(self):
''' print evaluation result: precision, recall, coverage and popularity '''
print('Evaluation start...', file=sys.stderr)
N = self.n_rec_movie
# varables for precision and recall
hit = 0
rec_count = 0
test_count = 0
# varables for coverage
all_rec_movies = set()
# varables for popularity
popular_sum = 0
for i, user in enumerate(self.trainset):
if i % 500 == 0:
print('recommended for %d users' % i, file=sys.stderr)
test_movies = self.testset.get(user, {})
rec_movies = self.recommend(user)
for movie, _ in rec_movies:
if movie in test_movies:
hit += 1
all_rec_movies.add(movie)
popular_sum += math.log(1 + self.movie_popular[movie])
rec_count += N
test_count += len(test_movies)
precision = hit / (1.0 * rec_count)
recall = hit / (1.0 * test_count)
coverage = len(all_rec_movies) / (1.0 * self.movie_count)
popularity = popular_sum / (1.0 * rec_count)
print('precision=%.4f\trecall=%.4f\tcoverage=%.4f\tpopularity=%.4f' %
(precision, recall, coverage, popularity), file=sys.stderr)
if __name__ == '__main__':
ratingfile = os.path.join('ml-1m', 'ratings.dat')
usercf = UserBasedCF()
usercf.generate_dataset(ratingfile)
usercf.calc_user_sim()
usercf.evaluate()