本文主要内容对非技术背景的人来说确实有点无趣,如果你不是技术人员,可以直接拖到底下看总结,还是有一点价值的。
相关旧文 史上最简单的推荐系统设计
其实本文核心价值没有脱离如上旧文,不过重新用实战案例说明一下,可能更有助于理解,并开放源代码。
在我的知识星球里,经常有新人问这样的问题,有什么星球是可以推荐的。这似乎是一个常见的新人话题。
嗯,最近跟知识星球合作,征询吴鲁加老板同意,获取了一些数据练手,简单实现了一下相关推荐。
先说推荐系统测试的结果,以 我个人的知识星球 为例,在这个推荐系统的测算中,相关度最高的星球是哪些呢?以下结果为程序测算,无任何人工干预和修正。
相关度第一名,是亦仁的 “生财有术”。
相关度第二名,是冯大辉老师的“小道消息”。
相关度第三名,是冯大辉老师的”小程序淘金“。
相关度第四名,抖音红利研究小组,嗯,貌似圈主是在我的知识星球里做过推广。
相关度第五名,余弦的慢雾区。
相关度第六名,是鞠海深的"创业直播间",他曾经在我的知识星球里非常活跃,他的知识星球过半数用户与我的知识星球的重合。
相关度第七名,硅谷女网红 angela zhu的 ”嘀嗒嘀嗒“。
相关度第八名,海外营销交流圈。
相关度第九名,明白老师的webscraper精进。
相关度第十名,池老师的 MacTalk的朋友们。
相关度第十一名,冯大辉老师的”招聘/求职/跳槽“。
相关度第十二名,刘老师的计算广告。
相关度第十三名,外贸协会。//讲外贸建站和营销的,星主也是我星球里非常活跃的。
相关度第十四名,投资人子柳。
相关度第十五名,WLJ的创业笔记。
后面就不罗列了。
这个结果,我觉得是非常符合预期的。
所谓符合预期,要特别说明的是,其实前文也提到过,在相关推荐的权值因素里,有两个最简单的数值代表了两个极端,一个是共同的推举数,也就是两个知识星球共同参与的人数,这个很容易理解,共同人数多是不是相关度就好呢,其实不是,如果按照共同人数来计算相关度,那么最高的一定是那些人数众多的热门免费群,包括几个火爆的摄影和美图群都会在这个榜单前列,这显然不是相关推荐理想的结果。 第二个极端是基于在对方星球的人数占比,如果对方星球的人数80%都出现在你的星球里,是不是相关度就极高了呢,但这样会出现一个很严重的问题,推荐出来的一定是一大堆人数稀少的小星球,有些几十人的星球绝大部分用户与我的星球用户重合,但其实价值不大,这些星球可能活跃度很差,而且星球没有什么号召力,所以这也不符合预期。
实际上我做过几轮不同权值的测试,最后发现,十几年前我在百度用的权值土方法,依然是效果最理想的,很好的兼顾了两个因素,既体现了二者的相关价值,又同时削弱了它们的极端影响。
但这个事情其实没结束,如果我作为用户,而非星主,我已经加入了30多个星球,那么系统会如何推荐给我其他未加入的呢。
基于我个人已加入星球的情况,计算出来推荐结果如下
1、池老师的 MacTalk的朋友们,不好意思,一直没有加入。
2、二爷书友会
3、KCon黑客大会(嗯,我加入了灰袍技能,慢雾区这样的社群)
4、抖音红利研究小组
5、每日运营热点案例
6、裂变增长实验室
7、鹅厂公益笔记
8、我滴妈呀 (一个小社群,完全不知道怎么关联出来的,哈哈)
9、创业直播间
10、新媒体玩法大全
当然,前面结果中很多星球我都已经加入了,这里有一些和前面结果明显不同,是因为这是基于我加入的所有星球来判断的,我觉得结果也还是比较满意的,出现了一些之前想不到的星球,但推荐本身不就是为了让你发现自己不知道而又可能感兴趣的东西么。
把结果放前面,是想证明,这个算法的效果,还是可以的。
那么,下面,代码展示,代码逻辑参见旧文
史上最简单的推荐系统设计
前置说明,数据准备,吴鲁加老师要求信息必须严格脱敏,本文章不涉及任何知识星球的数据结构和知识星球的数据获取方法。如何通过知识星球获取数据并整理的过程略,整理后的结果是我做相关推荐的中间结果,有这样几个数据结构。
gcount,每个星球的人数统计
格式为 : 群组id - 用户人数
mcount,每个用户加入的星球数统计
格式为 :用户id - 加入星球数
nmlist,每个用户加入的星球列表
格式为 :用户id - 星球1,星球2,星球3,
glist,每个星球的用户列表
格式为:星球id - 用户1,用户2,用户3,
再次说明,以上为我整理的中间数据,非知识星球原始数据结构。
caoz_data 是我自建的数据分析库,非知识星球数据库,其中group_relate表用于存储相关关系。
表结构为
id 自加1主键
fromgid 星球id
togid 相关星球id
sums 共同用户数
rights 相关权值 //生效的是这个,sums和nrights用于对比不同策略的效果
nrights 对比权值
相关推荐的数据生成代码,该代码完全基于中间数据,不涉及任何与知识星球系统交互的过程。
1
2 set_time_limit(0);
3 error_reporting(E_ALL || ~E_NOTICE);
4 $now=time();
5 function getarr($fname)
6 {
7 $fd=fopen($fname,"r");
8 $seed=0;
9 while ($str=trim(fgets($fd)))
10 {
11 $arr=explode(" - ",$str);
12 $key=$arr[0];
13 $value=$arr[1];
14 $listarr[$key]=$value;
15 $seed++;
16 // if ($seed>2000) break;
17 }
18 return ($listarr);
19 }
20 function putskip($now,$key)
21 {
22 $skip=time()-$now;
23 echo "$key - $skip\n";
24 }
25 $garr=getarr("gcount");
26 $marr=getarr("mcount");
27 $mlarr=getarr("nmlist");
28 $glarr=getarr("glist");
29 putskip($now,"START");
30 require 'conn.php';
31 $seed=0;$seed2=0;
32 mysql_selectdb("caoz_data");
33 foreach ($glarr as $gid=>$value)
34 {
35 $seed++;
36 $seed2++;
37 $v=$garr[$gid];
38 if ($v<5 or $v>1000000) continue;
39 $listarr=explode(",",$value);
40 foreach ($listarr as $key2=>$mid)
41 {
42 if ($marr[$mid]==1 or $marr[$mid]>1000) continue;
43 $gidlist=explode(",",$mlarr[$mid]);
44 foreach ($gidlist as $key3=>$togid)
45 {
46 if (trim($togid)=="") continue;
47 if ($gid==$togid) continue;
48 if ($garr[$togid]>1000000) continue;
49 if ($garr[$togid]<2) continue;
50 $sumlist[$gid][$togid]++;
51 $tmpright=1/(sqrt($marr[$mid]+1)*sqrt($garr[$togid]+1));
52 $tmpright2=1/(50+$garr[$togid]);
53 $rlist[$gid][$togid]+=$tmpright;
54 $nrlist[$gid][$togid]+=$tmpright2;
55 }
56 }
57 $value=$rlist[$gid];
58 $sql="delete from group_relate where fromgid=$gid";
59 mysql_query($sql);
60
61 $tmpseed=0;
62 arsort($value);
63 foreach ($value as $togid=>$rights)
64 {
65 $tmpseed++;
66 $rights=$rlist[$gid][$togid];
67 $rights2=$nrlist[$gid][$togid];
68 if ($tmpseed==1)
69 {
70 $zright=$rights; //归一化
71 $zright2=$rights2;
72 }
73 $tmpsum=$sumlist[$gid][$togid];
74 if ($tmpseed>30) break;
75 if ($tmpsum<2) continue;
76 $rights=$rights/$zright*10000;
77 $nrights=$rights2/$zright2*10000;
78 $sql="insert into group_relate (fromgid,togid,sums,rights,nrights) values ('$gid','$togid','$tmpsum','$rights','$nrights')";
79 mysql_query($sql);
80 //echo "$sql\n";
81 //echo mysql_error();
82 //echo "$gid $togid $rights $tmpsum \n";
83 }
84 unset($rlist[$gid]);
85 unset($nrlist[$gid]);
86 unset($sumlist[$gid]);
87 if ($seed==1000) { putskip($now,$seed2); $seed=0; }
88
89 }
90 ?>
代码只有90行,php写成,而且有些是调试的痕迹也没去掉,简单粗暴,初阶程序员一定能看懂。其中 putskip函数的意义就是,代码执行的时候,我能知道执行到哪里了,这是我做数据分析的一个习惯,要不怎么判断要等多久呢。扣掉这些调试代码,看看还剩多少行。
conn.php就是链接数据库而已,里面是啥就不许问了。
查询就简单了,以下查询均基于我自建的caoz_data数据库,无任何涉及知识星球部分。
1、查询星球相关的
$str="select * from group_relate where fromgid=$gid order by rights desc";
这个不用解释
2、基于用户加入的星球,查询推荐的
($gids 是用户已加入的星球列表)
$str="select distinct(togid) as tgid,sum(rights) as sumrights from group_relate where fromgid in($gids) and togid not in ($gids) group by tgid order by s umrights desc limit 20";
那有人会说,这个SQL开销太大吧,线上跑不起来吧。
其实还好,开销不会很大,如果你对数据库的索引效率有一定理解,结合上面的生成代码逻辑比对,应该能测算这个开销是多少。结合内存缓存来用的话,对于知识星球这种体量的应用,没太大问题。这可以作为课后题,有兴趣的可以分析一下这个SQL开销是怎么测算的,每秒可以支撑多少量级。
补充说一下,前期数据筛选中,剔除了已退出星球的用户和免费试用的用户。前段时间我放了免费试用卡,人数有点多,发现对数据结果的影响还是有点大的,剔除后恢复了正常。筛选数据确实存在一定的策略,但是基于规则,而非为了合理结果人工定向筛选。
其实这个只是一个快速的测试方案,距离理想的结果依然存在一点距离,比如说,可以增加针对用户活跃度的权值,以及针对星球活跃度的权值,以避免推荐的是一些很久不更新的星球。此外,基于续费率的权值,基于星球主活跃度的权值,也都是可以考虑的一些方向,这也是运营团队需要思考的。
毕竟,相关推荐的目的是什么,说白了,用户价值的挖掘。大量数据实践证明,个性推荐的转化效率远高于系统无差别推荐。转化率是最终的目标,而转化率最终也会成为自学习指标,甚至超过以上所有策略。
总结,
其实写这段代码可能有点自爆其丑,你看我写的代码也平平无奇,一点不像高手写出来的东西,这个确实也不是。
我常说自己是经济适用架构师,如果我们把代码抹掉,把我的前文链接抹掉,我只给你看上面列出来的关联结果,是不是觉得这个东西技术上还是有点厉害的。
这才是本文的重点,其实很多时候,用简单的方式,低成本的方式可以实现还不错的效果,第一看你有没有这个意识和行动,第二看你有没有整体评估的思维,只看代码一定很简单,但如果让一个程序员直接设计这个系统,几乎95%都会用很复杂的方式去实现。(当然,对于巨头或一些独角兽企业来说,肯定存在更好的方法,再高的成本也是值得的。这就不是经济适用架构师所能处理的场景了)
有,总比没有强,然后求精,迭代,后续需要更多技术投入,这也是合理的,但如果一开始就卡在实现成本过高,代价过大,很多创业项目是耗不起的。
我以前写 史上最简单的推荐系统设计 的时候,访问量就不高,不知道这篇会怎样。如果你公司有类似业务诉求,而一直苦于实现成本过高的话,本文可以转给有关的业务和技术负责人,请记住是“和”,不是“或”。
独角兽们请忽略,省的我被技术大牛鄙视,这样的东西也敢拿出来现眼?
最后,本文发布前已经交由吴鲁加老师审阅并征得同意。
此外,该系统所涉及数据为个人研究所用,数据所有权为知识星球,鉴于数据保密原则,本人不提供公开查询和数据开放服务,相关需求免开尊口。