基于 MyAnimeList 数据集的动漫推荐系统

基于 MyAnimeList 数据集的动漫推荐系统

摘要

MyAnimeList 是在 Kaggle 上发布的一个数据集,其中记录了从网站 MyAnimeList.net 爬取的 300000个用户信息, 14000部动漫信息, 以及 80M 条用户评分数据,完整数据解压后大小达 5GB。在进行数据清理,删掉有问题或不完整的数据之后,可以得到一个包含108711个用户信息, 6535部动漫信息, 以及 31M 条用户评分数据的子集。 我们使用基于标签的推荐、协同过滤、隐语义等多种方法,在此数据集上实现动漫的推荐系统,并使用主流的推荐系统评价方法与自己亲身测试的方式对推荐结果进行评价。动漫的推荐相较于电影的推荐,其难度更大,因为动漫本身的娱乐性更大且观众本身的层次区别,其看过的大多数动漫以及评分是否可以作为其审美标准就已经不能确定,有着相似观看历史以及相近评分的用户可能有着截然相反的喜好。最终结果显示各种方法都在推荐上有不错的结果,也适用于不同的推荐需求,基于标签的方式最简单,但效果却也不错,协同过滤、隐语义可以对用户构建出适合其一个人的推荐结果。

数据集的介绍

清理之后的数据包含108711个用户信息, 6535部动漫信息, 以及 31M 条用户评分数据,分为三个文件:

  • animelists_cleaned.csv (2.11GB)
  • users_cleaned.csv (15.02MB)
  • anime_cleaned.csv (6.03MB)

随后,基于这个子集,我们又筛选并剔除了一些用不到的数据项(其中,数据处理的代码封装在 MyAnimeList_DataProcessing.py 中),得到了本次推荐系统所需要的数据,并分为 Test 和 Train 一大一小两个子集。下面对最终得到的数据进行展示。

1
2
3
4
5
6
7
8
9
10
11
import tensorflow as tf
from collections import deque
from MyAnimeList_DataProcessing import *
import matplotlib.pyplot as plt
import scipy as sp
from sklearn.metrics.pairwise import cosine_similarity
from tqdm import tqdm
import time

users, scores = read_dataset(name='test')
animes = read_animes()

用户信息

用户信息包含了用户的生日、地区、性别、平均评分、观看数量等17项数据。筛选之后得到以下八项:

1
users.sample(n = 1)
user_id user_completed user_dropped gender
4525402 29 4 Male
location birth_date stats_mean_score stats_episodes
Kuningan, Indonesia 1993-05-31 7.97 1141

在使用中,用户观看量、放弃量、观看集数将被进行log10处理;

地区数据将会使用地图 API 处理后以国家为单位和性别一起变成onehot向量。

下图是在清理后的数据集中对于用户性别与年龄分布做的一个可视化:

用户性别与年龄分布图

动漫信息

动漫信息包含了名称、年份、类型、制作公司、音乐等 33 项数据。筛选后得到以下 14 项:

1
animes.sample(n = 1)
anime_id type source episodes rating score genre
31658 Movie Manga 1 PG-13 - Teens 13 or older 8.29 Sports, School, Shounen
scored_by popularity members favorites producer studio aired_from_year
32369 1129 73291 473 Bandai Visual Production I.G 2017-01-01

其中集数、评分数、人气值、观看人数、喜欢人数进行log10处理,类别数据做onehot处理。

动漫类型的分布如下:

动漫类型的分布图

评分信息

评分信息包含了用户ID、动漫ID、分数、观看集数、评分时间等5项数据。

1
scores.sample(n = 1)
user_id anime_id my_watched_episodes my_score my_status
4550376 16870 1.0 9.0 2.0

在使用过程中,用户ID和动漫ID都会被重新分配以保证ID的连续,但都会保留映射关系。

模型评估标准

在模型的训练与评估中,我们随机选取10%的数据作为测试数据,剩余90%作为训练数据,按照以下四个标准进行模型的评估。

准确度:

预测评分的准确度,衡量的是算法预测的评分与用户的实际评分的贴近程度。这针对于一些需要用户给物品评分的网站。

预测评分的准确度指标,一般通过以下指标(均方根误差)计算:

$$ RMSE = \frac{\sqrt{\sum_{u,i\in T}(r_{ui}-\hat{r_{ui} })^2}}{|T|} $$
1
2
3
4
# 准确度计算函数
def RMSE(pred, actual):
s = np.sum(np.square(pred-actual))
return np.sqrt(s)/len(actual)

召回率:

$$ Recall = \frac{\sum_{u\in U}|R(u) \cap T(u)|}{\sum_{u\in U}|T(u)|} $$ $R(u)$是根据用户在训练集上的行为给用户做出的推荐列表,$T(u)$是用户在测试集上的行为列表。
1
2
3
4
5
6
7
# 召回率计算函数
def recall(R, T):
rs = set(R)
ts = set(T)
if len(ts):
return len(rs&ts)/len(ts)
return None

覆盖率:

描述一个推荐系统对物品的发掘能力。最简单的定义是,推荐系统推荐出来的物品标签占总物品标签的比例。覆盖率是内容提供者关心的指标,覆盖率为100%的推荐系统可以将每个物品都推荐给至少一个用户。

用信息熵表示的覆盖率公式如下:

$$ Coverage = H = -\sum_{i=1}^n p(i)\log_2{p(i)} $$

在商品推荐的推荐系统中,推荐商品的覆盖范围越大越好,因为同一种商品顾客不可能买太多。

但是在动漫作品的推荐系统中,我们认为标签的覆盖的范围应该越小越好,因为用户有兴趣影视作品的类别是有限的,在动漫中更是极端,可能有的用户只喜欢某一类的作品,所以在标签的类别上,我们希望覆盖率尽量小。而在作品的类型(TV、电影、OVA)上,观众有可能因为平时接触的不多被某一类型局限,比如只看TV动画或是动画电影,所以在这里,我们希望覆盖率更大。

1
2
3
4
5
# 覆盖率计算函数
def coverage(pred):
pred = pred+0.001
pred = pred/pred.sum()
return -np.sum(pred * np.log2(pred))

基于标签相似度的推荐

方法介绍

这个方法主要基于标签的相似度进行动漫作品的推荐,对每部动漫的标签的one-hot向量进行距离的计算,根据动漫之间的相似度进行推荐。

距离计算公式使用欧式距离:

$$ Euclidean = \sqrt{\sum_{i=1}^n (A_i-B_i)^2} $$

或者余弦距离:

$$ Cosine = \frac{\sum_{i=1}^n A_i * B_i}{\sqrt{\sum_{i=1}^n (A_i)^2}*\sqrt{\sum_{i=1}^n (B_i)^2}} $$

在实验中,两种方式的效果并没有太大的优劣差别。所以可以综合考虑,叠加使用。

本方法需要用户给定几部喜欢的动漫,而不是给看过的所有动漫评分。系统基于每一部给出的动漫计算与其他动漫的相似度。随后取前100部相似的动漫按照综合评分由高到低排列,并且删除综合评分过低的动漫。

方法实现

1
2
3
4
def cosined(x,y):
return 1 - np.dot(x,y)/(np.linalg.norm(x)*np.linalg.norm(y))
def euclideand(x,y):
return np.sqrt(np.square(x-y).sum())
1
2
3
4
5
6
7
8
9
10
11
# 对 animes 进行处理
aid_convert.columns = ['anime_id','anime_id_to']
for name in ['producer', 'studio', 'genre']:
g = animes[name]
g[g.isnull()] = 'None'
animes[name] = g
animes = apply_numerate(animes, numerate_fixed(animes, 'type'))
animes = apply_numerate(animes, numerate_fixed(animes, 'source'))
animes = apply_numerate(animes, listed_to_onehot(animes,'genre', length=50))
animes = apply_numerate(animes, listed_to_onehot(animes,'studio', length=500))
animes = apply_numerate(animes, aid_convert, sort=['anime_id']).reset_index()
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
def distance(n1, n2, distancef = cosined):
row1 = animes.loc[n1]
row2 = animes.loc[n2]

a1 = np.zeros([21])
a1[row1.type] = 1
a1[row1.source + 6] = 1
a2 = np.zeros([21])
a2[row2.type] = 1
a2[row2.source + 6] = 1

return distancef(a1,a2), distancef(row1.genre, row2.genre)
def SimilarItemRecomand(items, alpha = 1, beta = 1, distancef = euclideand):
array = None
for item in tqdm(items):
li = []
for i in range(6535):
a, b = distance(item, i, distancef = distancef)
li.append(alpha*a + beta*b)
if array is not None:
array += np.array(li)
else:
array = np.array(li)
p = pd.DataFrame(data = {'anime_id_to':range(6535), 'distance':array})
p = pd.merge(p, aid_convert, on = ['anime_id_to']).loc[:,['anime_id', 'distance']]
p.columns = ['anime_id','distance']
p = pd.merge(p, animes_, on=['anime_id']).loc[:,['anime_id', 'distance', 'title_japanese', 'type', 'score','genre']]
return p.sort_values(['distance'])[:100].sort_values(['score'], ascending=False)
1
2
3
4
5
6
titles = ['STEINS;GATE','CLANNAD','蟲師','狼と香辛料','新世界より']
items = []
for title in titles:
items.append(aid_convert[aid_convert.anime_id == animes_[animes_.title_japanese == title].anime_id.values[0]].anime_id_to.values[0])
out = SimilarItemRecomand(items, distancef = cosined)
out[out.score > 7.5]

输出:
基于标签相似度的推荐输出结果

协同过滤模型

模型介绍

基于用户的协同过滤算法的关键是找到相同偏好的用户,找到了偏好最近的几个用户,他们偏好的物品便是要给你推荐的目标。而基于物品的协同过滤算法的关键是计算其它物品和历史物品的相似度,相似度最近的几个物品便是要推荐的物品。(换句话说,协同过滤算法的关键是解决相似度问题)。

协同过滤示意图

相似度计算

相似度计算主要有三个经典算法:余弦定理相似性度量、欧氏距离相似度度量和杰卡德相似性度量。而我选择了余弦定理相似性度量:

$$ Cosine = \frac{\sum_{i=1}^n A_i * B_i}{\sqrt{\sum_{i=1}^n (A_i)^2}*\sqrt{\sum_{i=1}^n (B_i)^2}} $$

预测值计算

根据之前算好的物品之间的相似度,接下来利用加权求和对用户未打分的物品进行预测。

用过对用户 u 已打分的物品的分数进行加权求和,权值为各个物品与物品 i 的相似度,然后对所有物品相似度的和求平均,计算得到用户 u 对物品 i 打分,公式如下:

$$ P_{u,i} = \frac{\sum_{all-similar-items, N}(S_{i,N}*R_{u,N})}{\sum_{all-similar-items, N}|S_{i,N}|} $$

模型实现

1
2
3
4
5
6
7
8
9
10
11
12
13
merged = scores_train.merge(apply_numerate(animes_,aid_convert),on = ['anime_id']).loc[:,['user_id', 'title_japanese', 'my_score']]
piv = merged.pivot_table(index=['user_id'], columns=['title_japanese'], values='my_score')
piv_norm = piv.apply(lambda x: (x-np.mean(x))/(np.max(x)-np.min(x)), axis=1)
# 删除所有只包含表示未评分用户的零的列
piv_norm.fillna(0, inplace=True)
piv_norm = piv_norm.T
piv_norm = piv_norm.loc[:, (piv_norm != 0).any(axis=0)]
# 我们的数据需要采用稀疏矩阵格式,以便下列函数读取
piv_sparse = sp.sparse.csr_matrix(piv_norm.values)
item_similarity = cosine_similarity(piv_sparse)
user_similarity = cosine_similarity(piv_sparse.T)
item_sim_df = pd.DataFrame(item_similarity, index = piv_norm.index, columns = piv_norm.index)
user_sim_df = pd.DataFrame(user_similarity, index = piv_norm.columns, columns = piv_norm.columns)
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
def top_animes(anime_name):
count = 1
print('Similar shows to {} include:\n'.format(anime_name))
for item in item_sim_df.sort_values(by = anime_name, ascending = False).index[1:11]:
print('No. {}: {}'.format(count, item))
count +=1
def top_users(user):

if user not in piv_norm.columns:
return('No data available on user {}'.format(user))

print('Most Similar Users:\n')
sim_values = user_sim_df.sort_values(by=user, ascending=False).loc[:,user].tolist()[1:11]
sim_users = user_sim_df.sort_values(by=user, ascending=False).index[1:11]
zipped = zip(sim_users, sim_values,)
for user, sim in zipped:
print('User #{0}, Similarity value: {1:.2f}'.format(user, sim))
def predicted_rating(anime_name, user):
sim_users = user_sim_df.sort_values(by=user, ascending=False).index[1:1000]
user_values = user_sim_df.sort_values(by=user, ascending=False).loc[:,user].tolist()[1:1000]
rating_list = []
weight_list = []
for j, i in enumerate(sim_users):
rating = piv.loc[i, anime_name]
similarity = user_values[j]
if np.isnan(rating):
continue
elif not np.isnan(rating):
rating_list.append(rating*similarity)
weight_list.append(similarity)
return sum(rating_list)/sum(weight_list)
1
top_animes('新世界より')
Similar shows to 新世界より include:

No. 1: サイコパス
No. 2: 絶園のテンペスト
No. 3: 刀語
No. 4: 凪のあすから
No. 5: リトルバスターズ!
No. 6: 輪るピングドラム
No. 7: リトルバスターズ!~Refrain~
No. 8: ガッ活!
No. 9: 蟲師
No. 10: 革命機ヴァルヴレイヴ
1
top_animes('夏目友人帳')
Similar shows to 夏目友人帳 include:

No. 1: 続 夏目友人帳
No. 2: 夏目友人帳 参
No. 3: 夏目友人帳 肆
No. 4: 夏目友人帳 新作OVA いつかゆきのひに
No. 5: 夏目友人帳 伍
No. 6: 蟲師
No. 7: 夏目友人帳 陸
No. 8: ちはやふる
No. 9: 蛍火の杜へ
No. 10: 坂道のアポロン
1
predicted_rating('凪のあすから',8711)
4.786221734246446

基于邻域模型(协同过滤的一种)的推荐

方法介绍

$S_{ij}(k)$ : 对 item i、j 都评过分的用户集合 $n_{ij}$ : 对 item i、j 都评过分的用户数量 皮尔森(pearson)相关系数: $$ p_{ij} = \frac{\sum_{u\in S_{ij}(k)}(r_{ui} - \bar{r_i})(r_{uj} - \bar{r_j})}{\sqrt{\sum_{u\in S_{ij}(k)}(r_{ui} - \bar{r_i})^2 \sum_{u\in S_{ij}(k)}(r_{uj} - \bar{r_j})^2}} $$ 收缩的相关系数(实践证明收缩的效果会更好): $$ {Sim}_{ij} = \frac{n_{ij}}{n_{ij} + \lambda_2}p_{ij} $$ 我们的目标是预测 $r_{ui}$,即用户u没有评分过的物品i。根据物品相似度,我们提取与物品i最相似的k个的物品(用户u有过评分的),并将这k个物品的集合表示为$S_{ij}(k)$。那么我们的预测$\hat{r_{ui}}$则为在baseline的基础上,加上k个物品的加权平均, 即预测值: $$ \hat{r_{ui}} = b_{ui} + \frac{\sum_{j\in S(u)-\{i\}}{Sim_{ij}}(r_{uj} - b_{uj})}{\sum_{j\in S(u)-\{i\}}{Sim_{ij}}} $$

这个计算方法的缺点:

  • 这个方法不能被正式的模型证明
  • 两个物品之间的相似度的衡量在没有考虑整个邻域集的相互作用下是否适合
  • 式中权重的计算方法太依赖于邻域集,但是有的物品可能邻域集为空(如用户u没有对与物品i相似的任何物品有过评分)。

方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bias_u = np.fromfile('bias_user.mat').reshape([8712])
bias_i = np.fromfile('bias_item.mat').reshape([6536])
p_mat = np.fromfile('pearson-no_null.mat').reshape([6536,6536])
r_mat = np.fromfile('r.mat').reshape([8712,6536])
n_mat = np.fromfile('n.mat').reshape([6536,6536])
l2 = 100

sim_mat = p_mat*(n_mat/(n_mat+l2))

sr = np.array(range(6536), dtype=np.int32)
def r_pred(u, i):
su = sr[~np.isnan(r_mat[u])]
ou = (sim_mat[i]*(r_mat[u]-bias_i))[su].sum()
od = sim_mat[i][su].sum()
return ou/od# + bias_u[u]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 给出指定用户的推荐
uid = 8711
aid_convert.columns = ['anime_id_to','anime_id']

result = []
for i in range(6535):
result.append((i, r_pred(uid, i)))

p = pd.DataFrame(data = result)

p.columns = ['anime_id', 'pred']

p = pd.merge(p, aid_convert, on = ['anime_id']).loc[:,['anime_id_to', 'pred']]
p.columns = ['anime_id','pred']
p = pd.merge(p, animes_, on=['anime_id']).loc[:,['anime_id', 'pred', 'title_japanese', 'type', 'score','genre']]
p.sort_values('pred', ascending=False)[:200]

输出:
邻域模型输出结果

隐语义模型

隐语义模型是近年来推荐系统领域较为热门的话题,它主要是根据隐含特征将用户与物品联系起来。对于一个给定的类,选择这个类中的哪些物品进行推荐,如何确定物品在某个类别中的权重,是推荐系统需要解决的问题,但在实际运用中存在以下困难:

  • 难以把握分类的粒度
  • 难以给一个物品多个类别
  • 难以给出多维度的分类
  • 难以确定一个物品在某一分类中的权重

模型介绍

隐语义模型是基于用户的行为数据进行自动聚类的,能反应用户对物品的分类意见;我们可以指定将物品聚类的类别数k,k越大,则粒度越细,模型会自动学习到物品在某一类别的权重。也有论文将隐语义模型与 AutoEncoder 相结合,以获取更好的效果,和可解释的特征。

隐语义模型是根据如下公式来计算用户 u 对物品 i 的兴趣度:

$$ Preference(u,i) = r_{ui} = p_w^T q_i = \sum_{f=1}^r p_{w,k}q_{i,k} $$

隐语义模型示意图

模型实现

在使用此模型的过程中,我们只需要用到数据集中的 score 部分,模型使用 Tensorflow 实现。我拿10万个用户的子集和9千个用户的子集都进行过训练,在结果上相差不明显。

参数及迭代器设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
tf.reset_default_graph()
my.user_id = 999999 # 我自己的数据
users = users.append(dict(
user_id = 999999,
gender = 'Male',
),ignore_index=True)
user_num = len(users) # 用户数量
item_num = len(animes) # 动漫总数
dim = 16 # 物品聚类的类别数
BATCH_SIZE = 1000
EPOCHS = 100
MODEL_SAVE_PATH = "save_report/"
MODEL_NAME = "model_saved"
def clip(x): # 把输出结果限制在指定范围内
return np.clip(x, 0.0, 10.0)

scores_train, scores_test, uid_convert = scores_modify(pd.concat([scores, my]),
users, aid_convert)

iter_train = ShuffleIterator([scores_train.user_id,scores_train.anime_id,
scores_train.my_score],batch_size=BATCH_SIZE)

iter_test = OneEpochIterator([scores_test.user_id,scores_test.anime_id,
scores_test.my_score],batch_size=-1)

模型的代码实现

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
user_batch = tf.placeholder(tf.int32, shape=[None], name="id_user")
item_batch = tf.placeholder(tf.int32, shape=[None], name="id_item")
rate_batch = tf.placeholder(tf.float32, shape=[None])


# 一个全局的 bias
bias_global = tf.get_variable("bias_global", shape=[])
# 用户bias 与 动漫bias
w_bias_user = tf.get_variable("embd_bias_user", shape=[user_num])
w_bias_item = tf.get_variable("embd_bias_item", shape=[item_num])
# 用户权重与动漫权重,上图中的 P、Q 矩阵
w_user = tf.get_variable("embd_user", shape=[user_num, dim],
initializer=tf.truncated_normal_initializer(stddev=0.02))
w_item = tf.get_variable("embd_item", shape=[item_num, dim],
initializer=tf.truncated_normal_initializer(stddev=0.02))


# 使用 embedding_lookup 获取每个 Batch 所需的 bias
bias_user = tf.nn.embedding_lookup(w_bias_user, user_batch, name="bias_user")
bias_item = tf.nn.embedding_lookup(w_bias_item, item_batch, name="bias_item")
# 使用 embedding_lookup 获取每个 Batch 所需的权重
embd_user = tf.nn.embedding_lookup(w_user, user_batch, name="embedding_user")
embd_item = tf.nn.embedding_lookup(w_item, item_batch, name="embedding_item")

# 根据公式计算结果
infer = tf.reduce_sum(tf.multiply(embd_user, embd_item), 1)
infer = tf.add(infer, bias_global)
infer = tf.add(infer, bias_user)
infer = tf.add(infer, bias_item, name="svd_inference")
regularizer = tf.add(tf.nn.l2_loss(embd_user), tf.nn.l2_loss(embd_item),
name="svd_regularizer")

learning_rate=0.1
reg=0.1
cost_l2 = tf.nn.l2_loss(tf.subtract(infer, rate_batch)) #数据上的损失
penalty = tf.constant(reg, dtype=tf.float32, shape=[], name="l2") #正则化的损失
cost = tf.add(cost_l2, tf.multiply(regularizer, penalty))

train_op = tf.train.FtrlOptimizer(learning_rate).minimize(cost)
init_op = tf.global_variables_initializer()

训练代码

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
saver = tf.train.Saver()
samples_per_batch = len(scores_train) // BATCH_SIZE
with tf.Session() as sess:
sess.run(init_op)
# break
merged = tf.summary.merge_all()
writer = tf.summary.FileWriter('logs/%s'%str(time.clock()),sess.graph)
print("%s\t%s\t%s\t%s" % ("Epoch", "Train Error", "Val Error", "Elapsed Time"))
errors = deque(maxlen=samples_per_batch)
start = time.time()
for i in range(EPOCHS * samples_per_batch):
users, items, rates = next(iter_train)
_, pred_batch = sess.run([train_op, infer],
feed_dict={user_batch: users,
item_batch: items,
rate_batch: rates,})
pred_batch = clip(pred_batch)
errors.append(np.power(pred_batch - rates, 2))
if i % samples_per_batch == 0:
train_err = np.sqrt(np.mean(errors))
test_err2 = np.array([])
for users, items, rates in iter_test:
pred_batch = sess.run(infer,
feed_dict={user_batch: users,
item_batch: items,
})
pred_batch = clip(pred_batch)
test_err2 = np.append(test_err2, np.power(pred_batch - rates, 2))
end = time.time()

print("%02d\t%.3f\t\t%.3f\t\t%.3f secs" % (i // samples_per_batch, train_err, np.sqrt(np.mean(test_err2)), end - start))
start = end
saver.save(sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME))

一组预测样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uid_base = np.ones([item_num])
items = np.array(range(item_num))
with tf.Session() as sess:
saver = tf.train.import_meta_graph(MODEL_SAVE_PATH+'/model_saved.meta')
saver.restore(sess, tf.train.latest_checkpoint(MODEL_SAVE_PATH))
pred_batch = sess.run(infer,
feed_dict={user_batch: uid_base*8711,
item_batch: items})
pred_batch = clip(pred_batch)
aid_convert.columns = ['anime_id_to','anime_id']
p = pd.DataFrame(data = {'anime_id':items,'pred':pred_batch})
p = pd.merge(p, aid_convert, on = ['anime_id']).loc[:,['anime_id_to', 'pred']]
p.columns = ['anime_id','pred']
p = pd.merge(p, animes_, on=['anime_id']).loc[:,['anime_id', 'pred', 'title_japanese', 'type', 'score','genre']]
p[p.pred > 8].sort_values('pred', ascending=False)

输出:
隐语义模型输出结果

可解释性

隐语义为每个用户和动漫都计算了给定维度的特征,我们将这些特征在 Tensorboard 中进行可视化,希望观察这些特征是否可以得到解释,或是这些特征之间的距离能不能作为两个物体间相似度的衡量标准。

下图是对不同 Type 的动漫标上不同颜色之后的可视化结果:

动漫按照不同 Type 标色

可见这些特征无法对不同的类别进行区分。

特征之间的距离能不能作为两动漫之间的相似度呢?我们选取了一些机战动画进行显示,发现他们在各个地方都有分布,随后我们对《夏目友人帐》整个系列的作品在图上标注出来,如果特征可以作为相似度标准,那么同系列作品的距离必定会比较相近,选取这部作品的原因是因为它出了多达6季的TV动画以及好多OVA,观看用户也多。

下图是结果:

《夏目友人帐》系列的位置

我们也拿其它相近作品作了测试,也对用户特征进行了可视化,标注其国籍、性别等标签,都得到了相同的结果,所以我们认为这个基本的隐语义模型所学习得到的特征没有可解释性。

模型评价

根据评价标准的测试

隐语义模型的测试:

  • 在整个测试集上进行,包含了 245948 个用户评分数据。

基于标签相似度的测试:

  • 随机取2-4部动漫作品作为喜欢的作品输入进行推荐,计算结果的覆盖率,重复100次。

协同过滤因为性能问题没有进行测试。

\ RMSE 召回率 覆盖率(type) 覆盖率(genre)
基于标签相似度 x x 0.4680 3.8169
邻域模型 0.0047 0.4511 2.0199 3.4869
协同过滤 x x x x
隐语义模型 0.0039 0.5614 1.6189 3.6029

从上述数据我们可以看出,邻域模型和隐语义模型在准确度伤的表现都不错,召回率都在50%左右,这个数据怎么样其实很难确定,因为难以找到类似动漫作品推荐系统的例子,更没有办法找到基于同一个数据集的案例,所以也就没有办法进行比较。在覆盖率的表现上,基于标签相似度的模型覆盖率最小,毕竟它本身就是以这个为目标的,可喜可贺的是,对于动画标签的覆盖率,三者都差不多,甚至邻域模型和隐语义模型的覆盖率更小,这充分证明了邻域模型和隐语义模型充分学习到了用户的喜好(不过基于标签相似度的模型在推荐的时候有按照评分排序,所以失去了一部分覆盖率)。

亲自测试的结论

光看冷冰冰的测试结果的数据,我们可能没法真的从用户的角度评价推荐系统的好坏,我作为一个动漫宅,记录了自己看过的所有动漫并给其中的一大部分评过分,所以我就把这些数据都给模型,让每个模型给我推荐一些动漫,自己主观评价一下其表现,前面的一些输出其实就是对我的推荐。

基于标签相似度的模型

从之前的输出中可以看到,对于我输入的动漫,模型可以仅仅根据标签相似性,找出其同系列的一些作品(如果有的话,比如《虫师》、《CLANNAD》、《狼与香辛料》)给出的推荐中不乏我看过且比较喜欢的作品,但效果算不上惊艳。

协同过滤模型

协同过滤模型虽然没有办法在完整的测试集上做测试,但是在小范围的尝试中,它对动漫的相似性把握得非常棒,针对《夏目友人帐》它找出的10个最相近的动漫中,几乎包揽了它的整个系列,但是系列的推荐不是推荐系统要做的事情,我希望找到的是我没听说过的动漫,然而协同过滤模型在这方面也做的极其出色,对于我输入的,我最喜欢的动漫《来自新世界》,它给出的10个最相近的动漫中,除了我没看过的5部作品,剩下的我看过的5部都是我给予过较高评价的动漫,特别是《虫师》我第二喜欢的作品居然也在内。之后我还去搜寻了一下我没看过的5部作品的简介,准备把它们补起来。但是其缺点也很明显,我觉得它对评分的预测并不准确。

邻域模型

邻域模型在数据表现上还是不错的,但是其实现较为麻烦,计算效率也不高,可能是训练也没有完全到位,或是模型本身的缺陷,其推荐结果也不是很出色,它的输出中虽然我评价高的作品不多,但是鉴于其推荐类别的广度,我觉得还是可以接受的。但是!它给我推荐的第一步作品居然是里番!这个不能忍,差评!

隐语义模型

隐语义模型是我觉得比较美观与优雅的一个模型了,虽然可解释性差,不能直接给出两部动漫的相似性,但在评分预测上还是可以的,输出的结果比较全面,也包含了许多我喜欢的作品。这个模型给我的推荐也几乎包含了我所喜欢看的动漫的各个类别,不得不说是一个优秀的结果。

共同的问题:除了基于标签相似度的模型之外,剩下3个可以给出评分预测的模型对【我,《来自新世界》】的评分预测都只有6.x分,甚至远远低于该动漫的平均打分,于是我又让它们预测协同过滤模型给出的10个与《来自新世界》最相近的动漫。这些模型也都预测我给它们的评分非常低。可以说,这些模型可以对用户的整体偏好作出了解,但却很难知道用户真正喜欢什么。

总结

在实现的四个模型中,协同过滤模型和隐语义模型表现最棒,虽然协同过滤模型给了我惊艳的感觉,但两个模型的应用场景还是有所区别的。如果想通过你心中的某一部“神作”去寻找其它优秀的作品,那么协同过滤模型将会非常好用,如果你希望模型对你有一个整体的认识,并针对你所有的喜好进行推荐,那么隐语义模型的表现则非常优秀。从使用复杂程度来看,隐语义模型需要你的整体数据,显得较为麻烦,但是给出的推荐也更加全面,适合自己。

参考文献

【1】推荐系统系列之隐语义模型

【2】Collaborative Variational Autoencoder for Recommender Systems

【3】受限玻尔兹曼机与推荐系统

【4】推荐系统总结MF->PMF->CTR->CDL->CNN

【5】推荐系统之隐语义模型(LFM)

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×