接着上文(1) 开始。
4. 深入:协同过滤(Collaborative Filtering )
回顾前文,一句话描述协同过滤:根据相似的(‘趣味相投’)人来推荐商品。
稍做扩展就是,要给A推荐商品,那么找到和A趣味相投的B,然后把B喜欢的推荐给A,that's it!。
在本沙盒里面,有用户(User)对电影(Movie)的评分(Rating, 作为属性包含在RATED这个关系里面)。
Rating
先看下Misty Williams这个User都做出哪些评分信息。
// Show all ratings by Misty Williams
MATCH (u:User {name: "Misty Williams"})
MATCH (u)-[r:RATED]->(m:Movie)
RETURN *;
沙盒在一共召回423条信息(仅显示15条)。
Rating demo
再看下他评分的均值(每个人的严格程度不一样)
// Show all ratings by Misty Williams
MATCH (u:User {name: "Misty Williams"})
MATCH (u)-[r:RATED]->(m:Movie)
RETURN avg(r.rating) AS average;
返回:
average
3.534278959810876
接着,我们再看下高于均值的评分都是哪些电影。
MATCH (u:User {name: "Misty Williams"})
MATCH (u)-[r:RATED]->(m:Movie)
WITH u, avg(r.rating) AS average
MATCH (u)-[r:RATED]->(m:Movie)
WHERE r.rating > average
RETURN *;
返回了199个电影。
Misty Williams喜欢的电影
那么,这199个高于评分均值的电影,从某种程度上就可以刻画'Misty Williams'这个人的喜好。
扩展一下,借此,就可以衡量两个人的喜好类似是否类似。
4.1 最简单的协同过滤(Collaborative Filtering )
ok,让我们从一个最简单粗暴的协同过滤开始。
假设: 如果只要A跟B看过/评价过同一个电影,就认为A跟B趣味相投。
推导:B看过/评价过的,而A没有的,就推荐给A。
代码实现:
MATCH (u:User {name: "Cynthia Freeman"})-[:RATED]->(:Movie)<-[:RATED]-(o:User)
MATCH (o)-[:RATED]->(rec:Movie)
WHERE NOT EXISTS( (u)-[:RATED]->(rec) )
RETURN rec.title, rec.year, rec.plot
LIMIT 25
这个假设-推导可谓是粗陋至极,看下效果。
粗暴的协同过滤
后面会用KNN(k近邻算法)来改进。
再来看,如果只考虑用户喜欢的genre,来做协同过滤。如下代码:
MATCH (u:User {name: "Andrew Freeman"})-[r:RATED]->(m:Movie)
WITH u, avg(r.rating) AS mean
MATCH (u)-[r:RATED]->(m:Movie)-[:IN_GENRE]->(g:Genre)
WHERE r.rating > mean
WITH u, g, COUNT(*) AS score
MATCH (g)<-[:IN_GENRE]-(rec:Movie)
WHERE NOT EXISTS((u)-[:RATED]->(rec))
RETURN rec.title AS recommendation, rec.year AS year, COLLECT(DISTINCT g.name) AS genres, SUM(score) AS sscore
ORDER BY sscore DESC LIMIT 10
解释下代码(原理)
1.首先取回Andrew看过的所有电影u,把评分算个均值记为mean。
2.选出高于mean的那些电影以及对应的genres/g,并且路径个数总计为score。
3.把含g中genre的电影全作为候选rec,但是排除掉Andrew已经看过/评价过的。
4.最后把每个电影,对应genres每项对应的score相加成为sscore值。返回前10项。
可以看出,还是很粗糙。
只考虑genre的协同过滤
4.2 基于余弦相似度的‘趣味相投’度衡量
协同过滤,第一个要解决的问题是如何衡量两个人是‘趣味相投’的,英文叫'with the same preferences'.
现在用一个很直截了当的方法:把一个User A所有评价过的电影当做一个向量V_a,每维的值就是评分(rating)。然后两个User A&B的相似度,就可以用V_a, V_b的余弦相似度衡量。
cosine similarity示例代码:
// Most similar users using Cosine similarity
MATCH (p1:User {name: "Cynthia Freeman"})-[x:RATED]->(m:Movie)<-[y:RATED]-(p2:User)
WITH COUNT(m) AS numbermovies, SUM(x.rating * y.rating) AS xyDotProduct,
SQRT(REDUCE(xDot = 0.0, a IN COLLECT(x.rating) | xDot + a^2)) AS xLength,
SQRT(REDUCE(yDot = 0.0, b IN COLLECT(y.rating) | yDot + b^2)) AS yLength,
p1, p2 WHERE numbermovies > 10
RETURN p1.name, p2.name, xyDotProduct / (xLength * yLength) AS sim
ORDER BY sim DESC LIMIT 100;
其中,REDUCE是Cypher里面定义一个func。
REDUCE in Cypher
在这里是得到向量内积。SQRT是开平方。整体就是求2范数。
沙盒效果如下,还不错,召回很多相似度极高的。
Most similar users using Cosine similarity
4.3 基于Pearson相似度的‘趣味相投’度衡量
问题:每个人评分时尺度不一,有的倾向于给较高分,有的则倾向于给较低分。cosine相似度不能很好解决这个。而Pearson相关系数就可以这一问题加以考虑。
先给出公式,如下。
Pearson相似度
实现代码示例:
1. MATCH (u1:User {name:"Cynthia Freeman"})-[r:RATED]->(m:Movie)
2. WITH u1, avg(r.rating) AS u1_mean
3. MATCH (u1)-[r1:RATED]->(m:Movie)<-[r2:RATED]-(u2)
4. WITH u1, u1_mean, u2, COLLECT({r1: r1, r2: r2}) AS ratings WHERE size(ratings) > 10
5. MATCH (u2)-[r:RATED]->(m:Movie)
6. WITH u1, u1_mean, u2, avg(r.rating) AS u2_mean, ratings
7. UNWIND ratings AS r
8. WITH sum( (r.r1.rating-u1_mean) * (r.r2.rating-u2_mean) ) AS nom,
sqrt( sum( (r.r1.rating - u1_mean)^2) * sum( (r.r2.rating - u2_mean) ^2)) AS denom,
u1, u2 WHERE denom <> 0
9. RETURN u1.name, u2.name, nom/denom AS pearson
10. ORDER BY pearson DESC LIMIT 100
对代码解释下。
1~2: 找到Cynthia以及他u1评价过的所有电影,评分均为记为u1_mean。把u1, u1_mean传下去。
3~4: 匹配到一个u2,如果他和u1一起评价过的电影数超过10个以上。并把u1,u1_mean,u2以及u1,u2两者针对每个电影的评分对形成的list记为ratings也传递下去。
5~6: 找出u2所有评价过的电影,以计算评分均值u2_mean。把u1,u1_mean,u2,u2_mean,ratings继续往下传。
7-8: 把ratings这个list展开,每项是r。 针对r进行sum。按Pearson公式,计算分子记为norm,分母记为denom,并且保证denom不为0。 把nom, denom, u1,u2往下传。// 体会unwind的作用。
9-10:按pearson降序返回前100项:u1.name, u2.name, pearson。
沙盒效果如下,仔细对比上面的cosine sim和这个的结果,ranking不一样了。
Most similar users using Pearson similarity
4.4 把协同过滤推荐进行到底!
前文终于把如何寻找好'趣味相投'的人研究了个差不离(当然还是很粗浅,实践中要复杂很多)。接下来就要接着4.1小节完整地把推荐进行到底。
思路:
(1)要给A推荐商品,先找到和A趣味相投的k个人(k个最近邻B1...Bk)
(2)然后把[B1..Bk]喜欢的,但是A还不知道的商品,优选出一部分推荐给A。
衡量两个人趣味相投度用4.3小节的方法,然后选出10个。再从这10个人评价过的电影中,找出评价度高的。那么,就有两个数量:人和人的相关度(Pearson系数),人对电影的评分(rating)需要综合考虑。
代码如下:
MATCH (u1:User {name:"Cynthia Freeman"})-[r:RATED]->(m:Movie)
WITH u1, avg(r.rating) AS u1_mean
MATCH (u1)-[r1:RATED]->(m:Movie)<-[r2:RATED]-(u2)
WITH u1, u1_mean, u2, COLLECT({r1: r1, r2: r2}) AS ratings WHERE size(ratings) > 10
MATCH (u2)-[r:RATED]->(m:Movie)
WITH u1, u1_mean, u2, avg(r.rating) AS u2_mean, ratings
UNWIND ratings AS r
WITH sum( (r.r1.rating-u1_mean) * (r.r2.rating-u2_mean) ) AS nom,
sqrt( sum( (r.r1.rating - u1_mean)^2) * sum( (r.r2.rating - u2_mean) ^2)) AS denom,
u1, u2 WHERE denom <> 0
WITH u1, u2, nom/denom AS pearson
ORDER BY pearson DESC LIMIT 10
MATCH (u2)-[r:RATED]->(m:Movie) WHERE NOT EXISTS( (u1)-[:RATED]->(m) )
RETURN m.title, SUM( pearson * r.rating) AS score
ORDER BY score DESC LIMIT 25
以上代码,就是最后三行是相对于4.3新加的,意思也很明确。就是排除已评价的,综合考虑Pearson和rating为电影的排序用分数,然后选top25。enjoy!
沙盒验证效果如下:
协同过滤推荐结果
至此,本系列学习记录结束,请大家指正。
网友评论