基于内容的推荐

一、概念

基于内容的推荐算法(Content-Based Recommendations ,CB)理论依据主要来自于信息检索和信息过滤,是最早被使用的推荐算法,它的思想非常简单:根据用户过去喜欢的物品(item),为用户推荐和他过去喜欢的物品相似的物品。而关键就在于这里的物品相似性的度量,这才是算法运用过程中的核心。
举个例子来说,在音乐网站或者资讯网站注册过程中用户需要选择自己的兴趣标签,这就是为了把合适的内容推荐给用户。
需要注意的是物品的特征抽取一般很难。如果系统中的物品是文档(如个性化阅读中),那么我们现在可以比较容易地使用信息检索里的方法来”比较精确地”抽取出物品的特征。但很多情况下我们很难从物品中抽取出准确刻画物品的特征,比如电影推荐中物品是电影,社会化网络推荐中物品是人,这些物品属性都不好抽。其实,几乎在所有实际情况中我们抽取的物品特征都仅能代表物品的一些方面,不可能代表物品的所有方面。

二、实现基于内容的推荐

1、执行步骤

step 1: 物品表示(Item Representation)

为每个物品抽取出一些特征(也就是item的content)来表示此物品。
真实应用中的物品往往都会有一些可以描述它的属性。这些属性通常可以分为两种:结构化的属性与非结构化的属性。所谓结构化的属性就是这个属性的意义比较明确,其取值限定在某个范围;而非结构化的属性往往其意义不太明确,取值也没什么限制,不好直接使用。比如在交友网站上,物品就是人,一个物品会有结构化属性如身高、学历、籍贯等,也会有非结构化属性(如item自己写的交友宣言,博客内容等等)。对于结构化数据,我们自然可以拿来就用;但对于非结构化数据(如文章),我们往往要先把它转化为结构化数据后才能在模型里加以使用。

step 2: 特征学习(Profile Learning)

利用一个用户过去喜欢(及不喜欢)的物品的特征数据,来学习出此用户的喜好特征。
假设用户已经对一些物品给出了他的喜好判断,喜欢其中的一部分物品,不喜欢其中的另一部分。那么,这一步要做的就是通过用户过去的这些喜好判断,为他产生一个模型。有了这个模型,我们就可以根据此模型来判断用户是否会喜欢一个新的物品。所以,我们要解决的是一个典型的有监督分类问题,理论上机器学习里的分类算法都可以照搬进这里。

step 3: 生成推荐列表(Recommendation Generation)

通过比较上一步得到的用户画像与候选物品的特征,为此用户推荐一组相关性最大的物品。用户画像与物品属性的相关性可以使用如余弦相似度等度量方法获得。

2、数据示例

ratings.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
userId,movieId,rating,timestamp
1,31,2.5,1260759144
1,1029,3.0,1260759179
1,1061,3.0,1260759182
1,1129,2.0,1260759185
1,1172,4.0,1260759205
1,1263,2.0,1260759151
1,1287,2.0,1260759187
1,1293,2.0,1260759148
1,1339,3.5,1260759125
1,1343,2.0,1260759131
1,1371,2.5,1260759135
1,1405,1.0,1260759203
1,1953,4.0,1260759191
1,2105,4.0,1260759139
1,2150,3.0,1260759194
1,2193,2.0,1260759198
1,2294,2.0,1260759108
1,2455,2.5,1260759113
1,2968,1.0,1260759200
1,3671,3.0,1260759117
2,10,4.0,835355493
2,17,5.0,835355681
2,39,5.0,835355604

movies.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
movieId,title,genres
1,Toy Story (1995),Adventure|Animation|Children|Comedy|Fantasy
2,Jumanji (1995),Adventure|Children|Fantasy
3,Grumpier Old Men (1995),Comedy|Romance
4,Waiting to Exhale (1995),Comedy|Drama|Romance
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
9,Sudden Death (1995),Action
10,GoldenEye (1995),Action|Adventure|Thriller
11,"American President, The (1995)",Comedy|Drama|Romance
12,Dracula: Dead and Loving It (1995),Comedy|Horror
13,Balto (1995),Adventure|Animation|Children
14,Nixon (1995),Drama

数据集下载
链接:https://pan.baidu.com/s/14yGqGzTrlK63SgQujnH-bA
提取码:xol5

3、代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# coding: utf-8
import os
import operator
import sys

#========================================================
# 辅助工具
#========================================================

def get_ratings(input_file):
'''
读取评分数据
'''
if not os.path.exists(input_file):
return {}
record = {}
avg_ratings = {}
linenum = 0
for line in open(input_file):
if linenum == 0: # 跳过标题行
linenum += 1
continue
item = line.strip().split(",")
if len(item) < 4: # 跳过缺失或异常数据
continue
userid, itemid, rating = item[0], item[1], float(item[2])
if itemid not in record:
record[itemid] = [0, 0]
record[itemid][0] += rating
record[itemid][1] += 1
for itemid in record:
avg_ratings[itemid] = round(record[itemid][0]/record[itemid][1], 3)
return avg_ratings

def get_item_content(avg_ratings, input_file):
'''
读取物品属性数据
'''
if not os.path.exists(input_file):
return {},{}
topk = 100
record = {}
item_content = {} # 物品成分字典
content_sort = {}
# 计算物品成分
linenum = 0
for line in open(input_file, 'r', encoding='UTF-8'):
if linenum ==0:
linenum+=1
continue
item = line.strip().split(',')
if len(item)< 3:
continue
itemid, content_str = item[0], item[-1]
content_list = content_str.strip().split('|')
ratio = round(1/len(content_list), 3) # 算出每个成分的比例
if itemid not in item_content:
item_content[itemid] = {}
for fix_content in content_list:
item_content[itemid][fix_content] = ratio
# 提取用户评分
for itemid in item_content:
for content in item_content[itemid]:
if content not in record:
record[content] = {}
itemid_rating_score = avg_ratings.get(itemid, 0)
record[content][itemid] = itemid_rating_score
# 每种成分的排行
for content in record:
if content not in content_sort:
content_sort[content] = []
for zuhe in sorted(record[content].items(), key=operator.itemgetter(1), reverse=True)[:topk]:
content_sort[content].append(zuhe[0])
return item_content, content_sort

#========================================================
# 核心算法
#========================================================

def get_time_score(timestamp):
'''
计算时间得分
'''
fix_time_stamp = 1476086345
total_sec = 24*60*60
delta = (fix_time_stamp - timestamp)/total_sec/100
return round(1/(1+delta), 3)

def get_up(item_content, input_file):
'''
用户画像
'''
if not os.path.exists(input_file):
return {}
record = {}
up = {}
score_thr = 4.0
topk = 2
linenum = 0
for line in open(input_file):
if linenum == 0:
linenum += 1
continue
item = line.strip().split(',')
if len(item) < 4:
continue
userid, itemid, rating, timestamp = item[0], item[1], float(item[2]), int(item[3])
if rating < score_thr:
continue
if itemid not in item_content:
continue
time_score = get_time_score(timestamp)
if userid not in record:
record[userid] = {}
for fix_cate in item_content[itemid]:
if fix_cate not in record[userid]:
record[userid][fix_cate] = 0
record[userid][fix_cate] += rating * time_score * item_content[itemid][fix_cate] # 每一个类别权重和
for userid in record: #排序
if userid not in up:
up[userid] = []
total_score = 0
for zuhe in sorted(record[userid].items(), key = operator.itemgetter(1), reverse=True)[:topk]: #对类别中的权重进行排序
up[userid].append((zuhe[0], zuhe[1])) # zuhe[0]种类 zuhe[1]权重
total_score += zuhe[1]
for index in range(len(up[userid])):
up[userid][index] = (up[userid][index][0], round(up[userid][index][1]/total_score, 3)) #这里的ratio表征着用户与物品的相近关系,得分越低,差距越大
return up

def recommend(item_sort, up, userid, topk=10):
'''
根据用户画像为指定用户做推荐
'''
if userid not in up:
return {}
recom_result = {}
if userid not in recom_result:
recom_result[userid] = []
for zuhe in up[userid]:
cate = zuhe[0]
ratio = zuhe[1]
num = int(topk*ratio) + 1
if cate not in item_sort:
continue
recom_list = item_sort[cate][:num]
recom_result[userid] += recom_list
return recom_result

#========================================================
# 主程序
#========================================================

if __name__ == "__main__":
avg_ratings = get_ratings("ratings.txt")
item_content, content_sort = get_item_content(avg_ratings, "movies.txt")
# 生成用户画像
up = get_up(item_content, "ratings.txt")
print(up)
# 针对某用户做推荐
print(recommend(content_sort, up, "1"))