最新要闻
- 【天天速看料】苹果推出跨年优惠 其实一点也没便宜
- 时讯:株洲卡丁车比赛事故致车手死亡后续 车手俱乐部声讨场地方
- 全球今日讯!彻底重构安卓内存底层!一加11将全球首发“内存基因重组”技术
- 天天微资讯!马斯克被曝内部圈子缺乏持不同观点的人:全是马屁精
- 全球滚动:K60发布后 倪飞发声:努比亚Z50才是旗舰焊门员
- 世界观点:RTX 4090玩游戏性能过剩 外星人懂了:将推500Hz高刷显示器
- 苹果股价三连跌创一年多来最低:2022累计蒸发27%
- 性能仅比3080略强?RTX 4070 Ti售价曝光 7199元你还可能买不到
- 全球快资讯:6年过去了:一代神卡GTX 1060依然值得入手
- 天天微头条丨平价神器!新iPad mini曝光:苹果加量不加价、还在密谋折叠屏惊喜
- 环球快看点丨马斯克赔哭 公司股价腰斩!消息称特斯拉上海工厂又要减产 卖不动国人不敢兴趣?
- 速看:中国GPU欲弯道超车:国产显卡向AMD、NV发起冲击
- 【播资讯】华硕发布灵耀AX小魔方Pro分布式路由:双频3000M 449元
- 威联通发布TS-1655 NAS:可塞入16块硬盘 超级别墅
- 全球热文:男子嫌妻子开车水平差 醉驾被查:这下5年内别嫌弃了
- 一家比苹果还赚钱的日本企业:日本人自己都不知道
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
天天速讯:「实操」结合图数据库、图算法、机器学习、GNN 实现一个推荐系统
本文是一个基于 NebulaGraph 上图算法、图数据库、机器学习、GNN 的推荐系统方法综述,大部分介绍的方法提供了 Playground 供大家学习。
【资料图】
基本概念
推荐系统诞生的初衷是解决互联网时代才面临的信息量过载问题,从最初的 Amazon 图书推荐、商品推荐,到电影、音乐、视频、新闻推荐,如今大多数网站、App 中都有至少一个基于推荐系统生成的供用户选择的物品列表界面。而这些物品的推荐基本都是基于用户喜好、物品的特征、用户与物品交互历史和其他相关上下文去做的。
一个推荐系统会包含以下几个部分:
- 数据、特征的处理
- 从特征出发,生成推荐列表
- 过滤、排序推荐列表
这其中,过滤的核心方法主要有两种:基于内容的过滤 Content-Based Filtering、与协同过滤 Collaborative Filtering,相关论问介绍可参考延伸阅读 1、2。
基于内容的过滤
内容过滤方法的本质是给用户的偏好做画像,同时对所有待推荐的物品计算特征,做用户的画像与待推荐物品特征之间的距离运算,过滤得到相近的物品。。
内容过滤的方法的好处有:
- 清晰的可解释性,无论是对用户的画像分析,还是对物品的运算本身天然带来了排序、过滤的可解释性;
- 用户数据输入的独立性,对指定的待推荐用户来说,只需要单独分析他们的画像和历史评分就足够了;
- 规避新物品冷启动问题,对于新的添加的物品,即使没有任何历史的用户评价,也可以做出推荐;
同时,基于内容过滤的推荐也有以下弊端:
- 特征提取难度,比如:照片、视频等非纯文本数据,它们的特征提取依赖领域专家知识。举个例子,电影推荐系统需要抽出导演、电影分类等领域知识作为特征;
- 难以打破舒适圈,发掘用户的潜在新兴趣点;
- 新用户冷启动数据缺失问题,新用户个人信息少难以形成用户画像,进而缺少做物品画像、特种距离运算的输入;
基于协同过滤
协同过滤的本质是结合用户与系统之间的交互行为去推荐物品。
协同过滤的方法又分为基于记忆memory-based 的协同过滤与基于模型model-based 的协同过滤。
基于记忆的协同过滤主要有物品与物品之间的协同过滤 ItemCF 和用户与用户之间的协同过滤 UserCF。ItemCF 简单来说是,推荐和用户之前选择相似的物品,即:根据行为找物品之间的相似性;UserCF 则推荐与用户有共同爱好的人所喜欢的物品,即:根据行为找用户之间的相似性。
基于模型的协同过滤主要根据用户喜好的历史信息、利用统计与机器学习方法训练模型,对新用户的偏好进行推理。
协同过滤的方法的好处有:
- 无需对非结构化物品进行特征分析,因为协同过滤关注的是用户和物品之间的协同交互,这绕过了对物品领域知识处理的需求;
- 对用户的个性化定制程度更强、更细,基于行为的分析使得对用户偏好的划分本质上是连续的(相比来说,对用户做画像的方法则是离散的),这样的推荐结果会更加“千人千面”。同时,也会蕴含内容过滤、有限的画像角度之下的“惊喜”推荐。
但,它的缺点有以下方面:
- 有新用户和新物件上的冷启动问题,因为它们身上都缺少历史喜好行为的信息;
我们总结一下,两种过滤方式各有利弊,也存在互补的地方。比如,新物件的冷启动上,基于内容的过滤有优势;对于个性化、推荐惊喜度方面,协同过滤有优势。所以,在实操中,推荐系统大多演变都比上面的归类复杂得多,而且常常伴随着多种方法的融合。
基于图的个性推荐
图技术、图数据库技术在推荐系统中的应用是多方面的,在本章节中我们会从图数据库的出发点上给出多种应用例子。
建立图谱
在开始之前,我简单地介绍下本文使用的图数据集。
为了给出更接近实际情况的例子,我从两个公开的数据集 OMDB 和 MovieLens 中分别抽取了所需信息,组成了一个既包含电影的卡司(导演、演员)和类型,又包含用户对电影评分记录的知识图谱。
Schema 如下:
- 顶点:
- user(user_id)
- movie(name)
- person(name, birthdate)
- genre(name)
- 边:
- watched(rate(double))
- with_genre
- directed_by
- acted_by
这个数据的准备、ETL 过程会在另外的文章里详细介绍,在进入下一章节之前,我们可以用 Nebula-Up 一键搭起一个测试的 NebulaGraph 单机集群,再参考数据集的 GitHub 仓库,一键导入所需数据,数据集参考延伸阅读 4。
具体操作步骤:
- 用 Nebula-Up 安装 NebulaGraph;
- 克隆 movie-recommendation-dataset;
- 导入数据集 NebulaGraph;
curl -fsSL nebula-up.siwei.io/install.sh | bashgit clone https://github.com/wey-gu/movie-recommendation-dataset.git && cd movie-recommendation-datasetdocker run --rm -ti \ --network=nebula-net \ -v ${PWD}:/root/ \ -v ${PWD}/dbt_project/to_nebulagraph/:/data \ vesoft/nebula-importer:v3.2.0 \ --config /root/nebula-importer.yaml
基于内容的过滤
CBF,内容过滤的思想是利用领域知识、历史记录、元数据分别对用户和物件做画像、打标签,最终根据用户的标签与待推荐物件之间的距离评分进行排序给出相关推荐。
对用户的画像不涉及其他用户的信息,但是输入的特征可能来源于元数据(生日、国籍、性别、年龄)、历史记录(评论、打分、购买、浏览)等等,在这些基础之上对用户进行标签标注、分类、聚类。
对物件的画像输入的特征可能是基于语言处理(NLP、TF-IDF、LFM)、专家标注、多媒体处理(视觉到文字再 NLP、音频风格处理、音频到文字再 NLP)等。
有了用户画像与物件的画像特征、对用户涉及的画像进行相关画像物件中新对象的近似度计算,再评分加权,就可以获得最终的推荐排序了。其中的近似度计算常见的有 KNN、余弦相似度、Jaccard 等算法。
CBF 的方法中没有限定具体实现方式。如前边介绍,可能是基于机器学习、Elasticsearch、图谱等不同方法。为切合本章的主题,这里我给出一个基于图数据库、图谱上的 CBF 的例子,做一个电影推荐系统,能让读者理解这个方法的思想。同时,也能熟悉图数据库、知识图谱的方法。
实操部分的用户特征直接利用历史电影评价记录,而推荐物件「电影」的画像则来自于领域中的知识。这些知识有:电影风格、电影的卡司、导演。近似度算法则采用图谱中基于关系的 Jaccard 相似度算法。
Jaccard Index
Jaccard Index 是一个描述两个集合距离的定义公式,非常简单、符合直觉地取两者的交集与并集测度的比例,它的定义记为:
这里,我们把交集理解为 A 与 B 共同连接的点(有共同的导演、电影类型、演员),并集理解为这几种关系下与 A 或者 B 直连的所有点,而测度就直接用数量表示。
CBF 方法在 NebulaGraph 中的实现
CBF 方法分如下四步:
- 找出推荐用户评分过的电影;
- 从用户评分过的电影,经由导演卡司、电影类型找到新的待推荐电影;
- 对看过的电影与新的电影,藉由导演、卡司、电影类型的关系,在图上做 Jaccard 相似性运算,得出每一对看过的电影和待推荐新电影之间的 Jaccard 系数;
- 把用户对看过电影的评分作为加权系数,针对其到每一个新电影之间的 Jaccard 系数加权评分,获得排序后的推荐电影列表;
// 用户 u_124 看过的电影MATCH (u:`user`)-[:watched]->(m:`movie`) WHERE id(u) == "u_124"WITH collect(id(m)) AS watched_movies_id// 根据电影的标注关系找到备选推荐电影,刨除看过的,把评分、交集关联链路的数量传下去MATCH (u:`user`)-[e:watched]->(m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)<-[:directed_by|acted_by|with_genre]-(recomm:`movie`)WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_idWITH (e.rate - 2.5)/2.5 AS rate, m, recomm, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 50// 计算 Jaccard index-------------------------------------------------// 针对每一对 m 和 recomm:// 开始计算看过的电影,集合 a 的部分MATCH (m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)WITH rate, id(m) AS m_id, recomm, intersection_size, COLLECT(id(intersection)) AS set_a// 计算推荐电影,集合 b 的部分MATCH (recomm:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)WITH rate, m_id, id(recomm) AS recomm_id, set_a, intersection_size, COLLECT(id(intersection)) AS set_b// 得到并集数量WITH rate, m_id, recomm_id, toFloat(intersection_size) AS intersection_size, toSet(set_a + set_b) AS A_U_B// 得到每一对 m 和 recomm 的 Jaccard index = A_N_B/A_U_BWITH rate, m_id, recomm_id, intersection_size/size(A_U_B) AS jaccard_index// 计算 Jaccard index-------------------------------------------------// 得到每一个被推荐的电影 recomm_id,经由不同看过电影推荐链路的相似度 = 评分 * jaccard_indexWITH recomm_id, m_id, (rate * jaccard_index) AS score// 对每一个 recomm_id 按照 m_id 加权求得相似度的和,为总的推荐程度评分,降序排列WITH recomm_id, sum(score) AS sim_score ORDER BY sim_score DESC WHERE sim_score > 0RETURN recomm_id, sim_score LIMIT 50
上边查询的执行结果截取出来是:
recomm_id | sim_score |
---|---|
1891 | 0.2705882352941177 |
1892 | 0.22278481012658227 |
1894 | 0.15555555555555556 |
808 | 0.144 |
1895 | 0.13999999999999999 |
85 | 0.12631578947368421 |
348 | 0.12413793103448277 |
18746 | 0.11666666666666668 |
628 | 0.11636363636363636 |
3005 | 0.10566037735849057 |
可视化分析
我们把上面过程中的部分步骤查询修改一下为 p=xxx
的方式,并渲染出来,会更加方便理解。
例子 1,用户 u_124
看过的、评分过的电影:
// 用户 u_124 看过的电影MATCH p=(u:`user`)-[:watched]->(m:`movie`) WHERE id(u) == "u_124"RETURN p
例子 2,找到用户 u_124
看过的那些电影在相同的演员、导演、电影类型的关系图谱上关联的所有其他电影:
// 用户 u_124 看过的电影MATCH (u:`user`)-[:watched]->(m:`movie`) WHERE id(u) == "u_124"WITH collect(id(m)) AS watched_movies_id // 根据电影的标注关系找到备选推荐电影,刨除看过的,把评分、交集关联链路的数量传下去MATCH p=(u:`user`)-[e:watched]->(m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)<-[:directed_by|acted_by|with_genre]-(recomm:`movie`)WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_idRETURN p, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 500
可以看到用户 u_124
看过电影经由演员、类型扩散出好多新的电影
在得到待推荐电影以及推荐路径后,通过 Jaccard 系数与用户路径第一条边的评分综合评定之后,得到了最终的结果。
这里,我们把结果再可视化一下:取得它们和用户之间的路径并渲染出来。
// 用户 u_124 看过的电影MATCH (u:`user`)-[:watched]->(m:`movie`) WHERE id(u) == "u_124"WITH collect(id(m)) AS watched_movies_id// 根据电影的标注关系找到备选推荐电影,刨除看过的,把评分、交集关联链路的数量传下去MATCH (u:`user`)-[e:watched]->(m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)<-[:directed_by|acted_by|with_genre]-(recomm:`movie`)WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_idWITH (e.rate - 2.5)/2.5 AS rate, m, recomm, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 50// 计算 Jaccard index-------------------------------------------------// 针对每一对 m 和 recomm:// 开始计算看过的电影,集合 a 的部分MATCH (m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)WITH rate, id(m) AS m_id, recomm, intersection_size, COLLECT(id(intersection)) AS set_a// 计算推荐电影,集合 b 的部分MATCH (recomm:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)WITH rate, m_id, id(recomm) AS recomm_id, set_a, intersection_size, COLLECT(id(intersection)) AS set_b// 得到并集数量WITH rate, m_id, recomm_id, toFloat(intersection_size) AS intersection_size, toSet(set_a + set_b) AS A_U_B// 得到每一对 m 和 recomm 的 Jaccard index = A_N_B/A_U_BWITH rate, m_id, recomm_id, intersection_size/size(A_U_B) AS jaccard_index// 计算 Jaccard index-------------------------------------------------// 得到每一个被推荐的电影 recomm_id,经由不同看过电影推荐链路的相似度 = 评分 * jaccard_indexWITH recomm_id, m_id, (rate * jaccard_index) AS score// 对每一个 recomm_id 按照 m_id 加权求得相似度的和,为总的推荐程度评分,降序排列WITH recomm_id, sum(score) AS sim_score ORDER BY sim_score DESC WHERE sim_score > 0WITH recomm_id, sim_score LIMIT 10WITH COLLECT(recomm_id) AS recomm_idsMATCH p = (u:`user`)-[e:watched]->(m:`movie`)-[:directed_by|acted_by|with_genre]->(intersection)<-[:directed_by|acted_by|with_genre]-(recomm:`movie`)WHERE id(u) == "u_124" AND id(recomm) in recomm_idsRETURN p
哇,我们可以很清晰地看到推荐的理由路径:喜欢星战的用户通过多条共同的类型、演员、导演的边引导出未观看的几部星战电影。这其实就是 CBF 的优势之一:天然具有较好的可解释性。
基于记忆的协同过滤
前边我们提过了,协同过滤主要可以分为两种:
- User-User CF 基于多个用户对物件的历史行为,判定用户之间的相似性,再根据相似用户的选择推荐新的物件;
- Item-Item CF 判断物件之间的相似性,给用户推荐他喜欢的物品相似的物品。
这里,ItemCF 看起来和前边的 CBF 有些类似,核心区别在于 CBF 找到相似物件的方式是基于物件的“内容”本身,是领域知识的画像,而 ItemCF 的协同则是考虑用户对物件的历史行为。
ItemCF
这个方法分如下四步:
- 找出推荐用户评分过的电影;
- 经由用户的高评分电影,找到其他给出高评分用户所看过的新的高评分电影;
- 通过用户的评分对看过的电影与新的电影在图上做 Jaccard 相似性运算,得出每一对看过的电影和待推荐新电影之间的 Jaccard 系数;
- 把用户对看过电影的评分作为加权系数,针对其到每一个新电影之间的 Jaccard 系数加权评分,获得排序后的推荐电影列表。
// 用户 u_124 看过的并给出高评分的电影MATCH (u:`user`)-[e:watched]->(m:`movie`) WHERE id(u) == "u_124" AND e.rate > 3WITH collect(id(m)) AS watched_movies_id// 根据同样也看过这些电影,并给出高评分的用户,得出待推荐的电影MATCH (u:`user`)-[e:watched]->(m:`movie`)<-[e0:watched]-(intersection:`user`)-[e1:watched]->(recomm:`movie`)WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_id AND e0.rate >3 AND e1.rate > 3WITH (e.rate - 2.5)/2.5 AS rate, m, recomm, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 50// 计算 Jaccard index-------------------------------------------------// 针对每一对 m 和 recomm:// 开始计算看过的电影,集合 a 的部分MATCH (m:`movie`)<-[e0:watched]-(intersection:`user`)WHERE e0.rate >3WITH rate, id(m) AS m_id, recomm, intersection_size, COLLECT(id(intersection)) AS set_a// 计算推荐电影,集合 b 的部分MATCH (recomm:`movie`)<-[e1:watched]-(intersection:`user`)WHERE e1.rate >3WITH rate, m_id, id(recomm) AS recomm_id, set_a, intersection_size, COLLECT(id(intersection)) AS set_b// 得到并集数量WITH rate, m_id, recomm_id, toFloat(intersection_size) AS intersection_size, toSet(set_a + set_b) AS A_U_B// 得到每一对 m 和 recomm 的 Jaccard index = A_N_B/A_U_BWITH rate, m_id, recomm_id, intersection_size/size(A_U_B) AS jaccard_index// 计算 Jaccard index-------------------------------------------------// 得到每一个被推荐的电影 recomm_id,经由不同看过电影推荐链路的相似度 = 评分 * jaccard_indexWITH recomm_id, m_id, (rate * jaccard_index) AS score// 对每一个 recomm_id 按照 m_id 加权求得相似度的和,为总的推荐程度评分,降序排列,只取正值WITH recomm_id, sum(score) AS sim_score ORDER BY sim_score DESC WHERE sim_score > 0RETURN recomm_id, sim_score LIMIT 50
结果:
recomm_id | sim_score |
---|---|
832 | 0.8428369424692955 |
114707 | 0.7913842214590154 |
64957 | 0.6924673321504288 |
120880 | 0.5775219768736295 |
807 | 0.497532028328161 |
473 | 0.4748322300870322 |
52797 | 0.2311965559170528 |
12768 | 0.19642857142857142 |
167058 | 0.19642857142857142 |
可视化分析 ItemCF
同样,我们把整个过程中的部分步骤查询修改一下为 p=xxx
的方式,并渲染出来,看看有什么有意思的的洞察。
步骤 1 的例子,找出推荐用户评分过的电影:
// 用户 u_124 看过的并给出高评分的电影MATCH p=(u:`user`)-[e:watched]->(m:`movie`) WHERE id(u) == "u_124" AND e.rate > 3RETURN p
它们是:
步骤 2 的例子,经由用户的高评分电影,找到其他给出高评分用户所看过的新的高评分电影,修改结果为路径:
// 用户 u_124 看过的并给出高评分的电影MATCH (u:`user`)-[e:watched]->(m:`movie`) WHERE id(u) == "u_124" AND e.rate > 3WITH collect(id(m)) AS watched_movies_id// 根据同样也看过这些电影,并给出高评分的用户,得出待推荐的电影MATCH p=(u:`user`)-[e:watched]->(m:`movie`)<-[e0:watched]-(intersection:`user`)-[e1:watched]->(recomm:`movie`)WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_id AND e0.rate >3 AND e1.rate > 3WITH p, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 200
可以看到待推荐的电影在路径的右边末端,中间连接着的都是其他用户的推荐记录,它的模式和 CBF 真的很像,只不过关联的关系不是具体的内容,而是行为。
步骤 3 的例子,在得出每一对看过的电影和待推荐新电影之间的 Jaccard 系数之后,把用户对看过电影的评分作为加权系数,针对其到每一个新电影之间的 Jaccard 系数加权评分,获得排序后的推荐电影列表。这里改造下最终的查询为路径,并渲染前 500 条路径:
// 用户 u_124 看过的并给出高评分的电影MATCH (u:`user`)-[e:watched]->(m:`movie`) WHERE id(u) == "u_124" AND e.rate > 3WITH collect(id(m)) AS watched_movies_id// 根据同样也看过这些电影,并给出高评分的用户,得出待推荐的电影MATCH (u:`user`)-[e:watched]->(m:`movie`)<-[e0:watched]-(intersection:`user`)-[e1:watched]->(recomm:`movie`)WHERE id(u) == "u_124" AND NOT id(recomm) IN watched_movies_id AND e0.rate >3 AND e1.rate > 3WITH (e.rate - 2.5)/2.5 AS rate, m, recomm, size(COLLECT(intersection)) AS intersection_size ORDER BY intersection_size DESC LIMIT 50// 计算 Jaccard index-------------------------------------------------// 针对每一对 m 和 recomm:// 开始计算看过的电影,集合 a 的部分MATCH (m:`movie`)<-[e0:watched]-(intersection:`user`)WHERE e0.rate >3WITH rate, id(m) AS m_id, recomm, intersection_size, COLLECT(id(intersection)) AS set_a// 计算推荐电影,集合 b 的部分MATCH (recomm:`movie`)<-[e1:watched]-(intersection:`user`)WHERE e1.rate >3WITH rate, m_id, id(recomm) AS recomm_id, set_a, intersection_size, COLLECT(id(intersection)) AS set_b// 得到并集数量WITH rate, m_id, recomm_id, toFloat(intersection_size) AS intersection_size, toSet(set_a + set_b) AS A_U_B// 得到每一对 m 和 recomm 的 Jaccard index = A_N_B/A_U_BWITH rate, m_id, recomm_id, intersection_size/size(A_U_B) AS jaccard_index// 计算 Jaccard index-------------------------------------------------// 得到每一个被推荐的电影 recomm_id,经由不同看过电影推荐链路的相似度 = 评分 * jaccard_indexWITH recomm_id, m_id, (rate * jaccard_index) AS score// 对每一个 recomm_id 按照 m_id 加权求得相似度的和,为总的推荐程度评分,降序排列,只取正值WITH recomm_id, sum(score) AS sim_score ORDER BY sim_score DESC WHERE sim_score > 0WITH recomm_id LIMIT 10WITH COLLECT(recomm_id) AS recomm_idsMATCH p = (u:`user`)-[e:watched]->(m:`movie`)<-[e0:watched]-(intersection:`user`)-[e1:watched]->(recomm:`movie`)WHERE id(u) == "u_124" AND id(recomm) in recomm_idsRETURN p limit 500
可以看出最推荐的两部电影几乎所有人看过,且是给过中高评分的用户共同看过的电影:
关于”高评分“
这里有个优化点,“高评分”其实是高于 3 的评分。这样的设定会有失公允,更合理的方式是针对每一个用户,取得这个用户所有评分的平均值,再取得与平均值相差的比例或者绝对值判定高低。此外,在通过 Jaccard 相似性判断每一部看过的电影和对应推荐电影的相似性时,并没有考虑这层关联关系:
(m:`movie`)<-[e0:watched]-(intersection:`user`)-[e1:watched]->(recomm:`movie`)
e0 与 e1 的评分数值的作用因素只过滤了低评分的关系,可以做进一步优化。
Pearson Correlation Coefficient
皮尔逊积矩相关系数 Pearson Correlation Coefficient,就是考虑了关系中的数值进行相似性运算的方法。
Pearson Correlation Coefficient,缩写 PCC。其定义为:
相比 Jaccard Index,它把对象之间关系中的数值与自身和所有对象的数值的平均值的差进行累加运算,在考虑了数值比重的同时考虑了数值基于对象自身的相对差异。
UserCF
下面,我们就利用 Pearson Correlation Coefficient 来举例 UserCF 方法。
基于用户的协同过滤方法分如下四步:
- 找出和推荐用户同样给出评分过的电影的用户;
- 运算 Pearson Correlation Coefficient 得到和推荐用户兴趣接近的用户;
- 通过兴趣接近用户得到高评分未观看电影;
- 根据观看用户的 Pearson Correlation Coefficient 加权,排序得推荐电影列表;
// 找出和用户 u_2 看过相同电影的用户, 得电影评分MATCH (u:`user`)-[e0:watched]->(m:`movie`)<-[e1:watched]-(u_sim:`user`)WHERE id(u) == "u_2" AND id(u_sim) != "u_2"WITH u, u_sim, collect(id(m)) AS watched_movies_id, COLLECT({e0: e0, e1: e1}) AS e// 计算 u_2 和这些用户的 pearson_ccMATCH (u:`user`)-[e0:watched]->(m:`movie`)WITH u_sim, watched_movies_id, avg(e0.rate) AS u_mean, eMATCH (u_sim:`user`)-[e1:watched]->(recomm:`movie`)WITH u_sim, watched_movies_id, u_mean, avg(e1.rate) AS u_sim_mean, e AS e_UNWIND e_ AS eWITH sum((e.e0.rate - u_mean) * (e.e1.rate - u_sim_mean) ) AS numerator, sqrt(sum(exp2(e.e0.rate - u_mean)) * sum(exp2(e.e1.rate - u_sim_mean))) AS denominator, u_sim, watched_movies_id WHERE denominator != 0// 取 pearson_cc 最大的 50 个相似用户WITH u_sim, watched_movies_id, numerator/denominator AS pearson_cc ORDER BY pearson_cc DESC LIMIT 50// 取相似用户给出高评分的新电影,根据相似用户个数对用户相似程度 pearson_cc 加权,获得推荐列表MATCH (u_sim:`user`)-[e1:watched]->(recomm:`movie`)WHERE NOT id(recomm) IN watched_movies_id AND e1.rate > 3RETURN recomm, sum(pearson_cc) AS sim_score ORDER BY sim_score DESC LIMIT 50
结果:
recomm | sim_score |
---|---|
("120880" :movie{name: "I The Movie"}) | 33.018868012270396 |
("167058" :movie{name: "We"}) | 22.38531462958867 |
("12768" :movie{name: "We"}) | 22.38531462958867 |
("55207" :movie{name: "Silence"}) | 22.342886447570585 |
("170339" :movie{name: "Silence"}) | 22.342886447570585 |
("114707" :movie{name: "Raid"}) | 21.384280909249796 |
("10" :movie{name: "Star Wars"}) | 19.51546960750133 |
("11" :movie{name: "Star Wars"}) | 19.515469607501327 |
("64957" :movie{name: "Mat"}) | 18.142639694676603 |
("187689" :movie{name: "Sin"}) | 18.078111338733557 |
可视化分析 UserCF
再看看 UserCF 的可视化结果吧:
步骤 1 的例子,找出和推荐用户同样给出评分过的电影的用户:
// 找出和用户 u_2 看过相同电影的用户MATCH p=(u:`user`)-[e0:watched]->(m:`movie`)<-[e1:watched]-(u_sim:`user`)WHERE id(u) == "u_2" AND id(u_sim) != "u_2"RETURN p
步骤 2 的例子,运算 Pearson Correlation Coefficient 得到和推荐用户兴趣接近的用户,输出这些接近的用户:
// 找出和用户 u_2 看过相同电影的用户, 得电影评分MATCH (u:`user`)-[e0:watched]->(m:`movie`)<-[e1:watched]-(u_sim:`user`)WHERE id(u) == "u_2" AND id(u_sim) != "u_2"WITH u, u_sim, collect(id(m)) AS watched_movies_id, COLLECT({e0: e0, e1: e1}) AS e// 计算 u_2 和这些用户的 pearson_ccMATCH (u:`user`)-[e0:watched]->(m:`movie`)WITH u_sim, watched_movies_id, avg(e0.rate) AS u_mean, eMATCH (u_sim:`user`)-[e1:watched]->(recomm:`movie`)WITH u_sim, watched_movies_id, u_mean, avg(e1.rate) AS u_sim_mean, e AS e_UNWIND e_ AS eWITH sum((e.e0.rate - u_mean) * (e.e1.rate - u_sim_mean) ) AS numerator, sqrt(sum(exp2(e.e0.rate - u_mean)) * sum(exp2(e.e1.rate - u_sim_mean))) AS denominator, u_sim, watched_movies_id WHERE denominator != 0// 取 pearson_cc 最大的 50 个相似用户WITH u_sim, watched_movies_id, numerator/denominator AS pearson_cc ORDER BY pearson_cc DESC LIMIT 50RETURN u_sim
我们给它们标记一下颜色:
步骤 3 的例子,通过兴趣接近用户得到高评分未观看电影,根据观看用户的 Pearson Correlation Coefficient 加权,排序得推荐电影列表。我们把结果输出为这些相似用户的高评分电影路径:
// 找出和用户 u_2 看过相同电影的用户, 得电影评分MATCH (u:`user`)-[e0:watched]->(m:`movie`)<-[e1:watched]-(u_sim:`user`)WHERE id(u) == "u_2" AND id(u_sim) != "u_2"WITH u, u_sim, collect(id(m)) AS watched_movies_id, COLLECT({e0: e0, e1: e1}) AS e// 计算 u_2 和这些用户的 pearson_ccMATCH (u:`user`)-[e0:watched]->(m:`movie`)WITH u_sim, watched_movies_id, avg(e0.rate) AS u_mean, eMATCH (u_sim:`user`)-[e1:watched]->(recomm:`movie`)WITH u_sim, watched_movies_id, u_mean, avg(e1.rate) AS u_sim_mean, e AS e_UNWIND e_ AS eWITH sum((e.e0.rate - u_mean) * (e.e1.rate - u_sim_mean) ) AS numerator, sqrt(sum(exp2(e.e0.rate - u_mean)) * sum(exp2(e.e1.rate - u_sim_mean))) AS denominator, u_sim, watched_movies_id WHERE denominator != 0// 取 pearson_cc 最大的 50 个相似用户WITH u_sim, watched_movies_id, numerator/denominator AS pearson_cc ORDER BY pearson_cc DESC LIMIT 50// 取相似用户给出高评分的新电影,根据相似用户个数对用户相似程度 pearson_cc 加权,获得推荐列表MATCH p=(u_sim:`user`)-[e1:watched]->(recomm:`movie`)WHERE NOT id(recomm) IN watched_movies_id AND e1.rate > 3WITH p, recomm, sum(pearson_cc) AS sim_score ORDER BY sim_score DESC LIMIT 50RETURN p
得到的结果,我们增量渲染到画布上可以得到:这些 UserCF 推荐而得的电影在路径末端:
在可视化中,相似用户的高评分未观看电影被推荐的思想是不是一目了然了呢?
混合的方法
在真实的应用场景里,达到最好效果的方法通常是结合不同的协同方法,这样既可以让不同角度的有用信息得到充分利用,又能弥补单一方法在不同数据量、不同阶段的弱点。
具体的结合方法在工程上千差万别,这里就不做展开。不过,抛砖引玉我们来看一种基于模型的混合方式。
基于模型的方法
上面讲过基于内容的(电影的领域知识、关系)、协同的(用户与用户、用户与电影之间的交互关系)的算法来直接进行相关推荐。但实际上,它们也可以作为机器学习的输入特征,用统计学的方法得到一个模型,用来预测用户可能喜欢的物件(电影),这就是基于模型的方法。
基于模型的方法可以很自然地把以上几种过滤方法作为特征,本质上这也是混合过滤方法的一种实现。
基于模型、机器学习的推荐系统方法有很多,这里着重同图、图数据库相关的方法,讲解其中基于图神经网络 GNN 方法。
GNN 的方法可以将图谱中的内容信息(导演、演员、类型)和协同信息(用户-用户、电影-电影、用户-电影之间的相互关系)以知识的方式嵌入,并且方法中的消息传递方式保有了图中的局部性(locality),这使得它可能成为一个非常新颖、有效的推荐系统模型方法。
GNN + 图数据库的推荐系统
为什么需要图数据库?
GNN 的方法中,图数据库只是一个可选项,而我给的 GNN 方法的示例中,图数据库的关键作用是它带来了实时性的可能。
一个实时性推荐系统要求在秒级响应下利用 GNN 训练模型从近实时的输入数据中进行推理,这给我们提出的要求是:
- 输入数据可以实时、近实时获取;
- 推理运算可以实时完成;
而利用归纳型 Inductive model 的模型从图数据库中实时获取新的数据子图作为推理输入是一个满足这样要求可行的设计方式。
下边是简单的流程图:
- 左边是模型训练,在图谱的二分图(user 和 item)之上建模
- 用户和物件(movie)之间的关系除了交互关系之外,还有预先处理的关系,这些关系被查询获得后再写回图数据库中供后续消费使用,关系有:
- 用户间“相似”关系
- 物件间不同(共同演员、类型、导演)关系
- 利用 Nebula-DGL 将图中需要的点、边序列化为图深度学习框架 DGL 可以消费的对象
- 在 DGL 中分割训练、测试、验证集,利用 PinSAGE 模型训练
- 导出模型给推荐系统使用
- 用户和物件(movie)之间的关系除了交互关系之外,还有预先处理的关系,这些关系被查询获得后再写回图数据库中供后续消费使用,关系有:
- 右边是导出的模型作为推理接口的推荐系统
- 基于图库的实时图谱上一直会有实时的数据更新,节点增减
- 当给定的用户推荐请求过来的时候,图库中以该用户为起点的子图会被获取(1.)、作为输入发送给推理接口(2.)
- 推理接口把子图输入给之前训练的模型,获得该用户在子图中关联的新物件中的评分排序(3.)作为推荐结果
由于篇幅关系,这里不做端到端的实例代码展示,后续有机会我会出个 demo。
推荐系统可解释性
在结束本章之前,最后举一个图数据库在推荐系统中的典型应用:推荐理由。
下图是美团、大众点评中的一个常见的搜索、推荐结果。现在推荐系统的复杂度非常高,一方面由实现方式的特性决定,另一方面最终推荐由多个协同系统组合生成最终排名,这使得系统很难对推荐结果进行解释。
得益于被推荐用户和物件、以及他们的各种各样画像最终形成的知识图谱,我们只需要在图谱中对推荐结果进行“路径查找”就可以获得很有意义的解释,像是如下截图的“在北京喜欢北京菜的山东老乡都说这家店很赞”就是这样获得的解释。
图片来源:美团案例分享
可解释性的例子
回到咱们电影推荐的图谱上,前边的算法中我们获得过用户 u_124
的推荐电影 1891
(星球大战),那么我们可以通过这一个查询获得它的推荐解释:
FIND NOLOOP PATH FROM "u_124" TO "1891" over * BIDIRECT UPTO 4 STEPS yield path as `p` | LIMIT 20
我们可以很快获得 20 条路径:
(root@nebula) [moviegraph]> FIND NOLOOP PATH FROM "u_124" TO "1891" over * BIDIRECT UPTO 4 STEPS yield path as `p` | LIMIT 20+-----------------------------------------------------------------------------------------------------+| p |+-----------------------------------------------------------------------------------------------------+| <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_49")<-[:with_genre@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_17")<-[:with_genre@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_10281")<-[:with_genre@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_4")<-[:acted_by@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_3")<-[:acted_by@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_24342")<-[:acted_by@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_2")<-[:acted_by@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("832")-[:with_genre@0 {}]->("g_1110")<-[:with_genre@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_1110")<-[:with_genre@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_13463")<-[:acted_by@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_12248")<-[:acted_by@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("47981")-[:with_genre@0 {}]->("g_10219")<-[:with_genre@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_6")<-[:acted_by@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("497")-[:with_genre@0 {}]->("g_104")<-[:with_genre@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("120880")-[:with_genre@0 {}]->("g_104")<-[:with_genre@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_104")<-[:with_genre@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:acted_by@0 {}]->("p_130")<-[:acted_by@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("497")-[:with_genre@0 {}]->("g_50")<-[:with_genre@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11635")-[:with_genre@0 {}]->("g_50")<-[:with_genre@0 {}]-("1891")> || <("u_124")-[:watched@0 {}]->("11")-[:with_genre@0 {}]->("g_50")<-[:with_genre@0 {}]-("1891")> |+-----------------------------------------------------------------------------------------------------+Got 20 rows (time spent 267151/278139 us)Wed, 09 Nov 2022 19:05:56 CST
我们在结果可视化中可以很容易看出这个推荐的结果可以是:
- 曾经喜欢的星战电影的大部分演职人员都也参与了这部和同样是“奥斯卡获奖”且“经典”的电影。
总结
图数据库可作为推荐系统中信息的最终形式,尽管在很多推荐实现中,图库不一定是最终落地系统方案的选择,但图数据库所带来的可视化洞察的潜力还是非常大的。
同时,构建的综合知识图谱上的解释、推理能力与一些实时要求高的图方法中,比如:GNN的基于模型方法上能起到带来独一无二的作用。
延伸阅读
- Content-based Recommender Systems: State of the Art and Trends
- A DYNAMIC COLLABORATIVE FILTERING SYSTEM VIA A WEIGHTED CLUSTERING APPROACH
- Nebula-UP:https://github.com/wey-gu/nebula-up
- 数据集仓库:https://github.com/wey-gu/movie-recommendation-dataset
- Jaccard Index:https://en.wikipedia.org/wiki/Jaccard_index
- Pearson Correlation Coefficient:https://en.wikipedia.org/wiki/Pearson_correlation_coefficient
- DGL 流行的图深度学习矿建,项目官网:https://www.dgl.ai
- Nebula-DGL:https://github.com/wey-gu/nebula-dgl
- PinSAGE:https://arxiv.org/abs/1806.01973
- 美团案例分享:https://tech.meituan.com/2021/04/01/nebula-graph-practice-in-meituan.html
谢谢你读完本文(///▽///)
要来近距离体验一把图数据库吗?现在可以用用 NebulaGraph Cloud 来搭建自己的图数据系统哟,快来节省大量的部署安装时间来搞定业务吧~ NebulaGraph 阿里云计算巢现 30 天免费使用中,点击链接来用用图数据库吧~
想看源码的小伙伴可以前往 GitHub 阅读、使用、(з)-☆ star 它 -> GitHub;和其他的 NebulaGraph 用户一起交流图数据库技术和应用技能,留下「你的名片」一起玩耍呢~
-
天天速讯:「实操」结合图数据库、图算法、机器学习、GNN 实现一个推荐系统
本文是一个基于NebulaGraph上图算法、图数据库、机器学习、GNN的推荐系统方法综述,大部分介绍的方法提...
来源: -
Python爬虫实战,requests+openpyxl模块,爬取小说数据并保存txt文档(附源码)
前言今天给大家介绍的是Python爬取小说数据并保存txt文档,在这里给需要的小伙伴们代码,并且给出一点小...
来源: -
关注:面试官问:为啥不建议使用 Select *?请你大声地回答他!!
作者:小目标青年来源:https: blog csdn net qq_35387940 article details 125921218前言不建议使用select*这几个字眼,做开发的
来源: 天天速讯:「实操」结合图数据库、图算法、机器学习、GNN 实现一个推荐系统
天天短讯!HTML 常用标签 tag
Python爬虫实战,requests+openpyxl模块,爬取小说数据并保存txt文档(附源码)
关注:面试官问:为啥不建议使用 Select *?请你大声地回答他!!
即时:DAG任务调度系统 Taier 演进之道,探究DataSourceX 模块
【天天速看料】苹果推出跨年优惠 其实一点也没便宜
时讯:株洲卡丁车比赛事故致车手死亡后续 车手俱乐部声讨场地方
全球今日讯!彻底重构安卓内存底层!一加11将全球首发“内存基因重组”技术
天天微资讯!马斯克被曝内部圈子缺乏持不同观点的人:全是马屁精
全球滚动:K60发布后 倪飞发声:努比亚Z50才是旗舰焊门员
世界观点:RTX 4090玩游戏性能过剩 外星人懂了:将推500Hz高刷显示器
苹果股价三连跌创一年多来最低:2022累计蒸发27%
性能仅比3080略强?RTX 4070 Ti售价曝光 7199元你还可能买不到
全球快资讯:6年过去了:一代神卡GTX 1060依然值得入手
天天微头条丨平价神器!新iPad mini曝光:苹果加量不加价、还在密谋折叠屏惊喜
环球快看点丨马斯克赔哭 公司股价腰斩!消息称特斯拉上海工厂又要减产 卖不动国人不敢兴趣?
速看:中国GPU欲弯道超车:国产显卡向AMD、NV发起冲击
【天天新要闻】实现小程序onShow第一次页面加载不执行
Java String类为什么用final修饰
天天通讯!Spring IOC官方文档学习笔记(五)之bean的作用域
【播资讯】华硕发布灵耀AX小魔方Pro分布式路由:双频3000M 449元
威联通发布TS-1655 NAS:可塞入16块硬盘 超级别墅
全球热文:男子嫌妻子开车水平差 醉驾被查:这下5年内别嫌弃了
一家比苹果还赚钱的日本企业:日本人自己都不知道
快讯:一文带你入门Transformer
全球观焦点:飞机票+高铁票可以一起买了:仅一笔支付 支持30个城市
今日要闻!听吐了!一对夫妻决定众筹买下《圣诞曲》版权将其下架
观点:国际空间站宇宙飞船发生泄漏 俄罗斯公布原因:外部机械性损坏
AcWing. 1165.单词环
全球热推荐:绚丽又冷静!酷冷至尊莫比乌斯风扇评测:马力全开也只有60分贝
当前资讯!剧情澎湃!张艺谋新片《满江红》定档春节 双男主沈腾+易烊千玺
【天天报资讯】字节掐准了商家命门
全球即时看![数据结构]单向链表的翻转(C语言)
【世界速看料】诺基亚、爱立信宣布退出 俄罗斯移动网路恐将倒退30年
速读:日系车笑了:国人把丰田买成了全球第一
每日速递:差价800元 Redmi K60与K60 Pro如何选?这三点不同
《王者荣耀》吕布FMVP皮肤来了:2023年第一款皮肤
观点:与微念和解 停更531天的李子柒何时复出?回应来了
全球播报:FreeSWITCH编译加载新模块
python学习: fire库的使用教程
可能是Redmi最帅手机!Redmi K60 Pro亮相
焦点速讯:卢伟冰:Redmi K60 Pro干掉了电竞手机 将被市场淘汰
支付宝迎来“史诗级”更新:深色模式终于开启测试
天降横财!土耳其在黑海发现580亿立方米天然气
焦点讯息:日本N多影院设备陈旧 无法播放《阿凡达2》48帧版:影迷大规模退票
window10/window11不能登录微软账户等
焦点速递!cmd命令curl的简单使用以及通过ip查所对应地址的方法
环球今亮点!Gateway
相当差劲!中汽研公布新一批碰撞测试:合创Z03仅获三星
博主称餐厅虚假营销:抓起帝王蟹扯腿吃掉
世界通讯!十年N饭称AMD显卡驱动有一个优点 就是不太稳定
全球消息!轻松瓦解重油污 奥妙桂花青梅香型洗洁精4 x 1.1KG 29.9元
鹤岗铁路开通运营:“绿巨人”开到中国最北端!时速160公里
比304更昂贵的316L不锈钢 富光真空保温杯年末清仓:多款式多容量可选
当前信息:清幽雅致 索泰RTX 4080 AMP EXTREME AIRO月白图赏
焦点播报:豪华超跑质感!Redmi K60冠军版官宣
焦点热议:AcWing361. 观光奶牛
即时:10 种超好用的 MyBatis 写法,同事都说好用!
焦点!【记录贴】项目经理的进阶日常:靠年终总结获得了核心项目的机会
Atcoder Grand Contest AGC 060 D Same Descent Set 题解 (容斥,多项式)
焦点精选!英国工程师警告:电动车可能导致停车场坍塌 原因意想不到
全球新动态:荣耀平板V8 Pro预装MagicOS 7.0 赵明:跨设备非常强 将与苹果竞争
天天微头条丨我国成功发射高分十一号04星 首次实现中继星数据双向高速互传
太阴间了!荷兰一车企推出特斯拉Model 3定制灵车
环球热头条丨荣耀赵明:荣耀笔记可记录瞬间灵感 未来将在多端流转
Java基本数据类型
环球焦点!易基因|深度综述:癌症中RNA修饰机制的遗传和表观遗传失调(m6A+m1A+m5C+ψ)
焦点速讯:2.4折发车:361° 新款休闲老爹鞋99元官方大促
谷歌旗舰Pixel 7遭品控问题:带壳使用后摄玻璃依旧碎裂
焦点热议:驾校学车被撞伤获赔23万 学员用不用担责?法院如此判决
环球热讯:奔驰GLS国产平替 北京越野BJ90山河经典版上市:47.80万起
第五章 --------------------加载和编译XAML
天天短讯!Netty中8大组件详解(EventLoop、Channel、ChannelFuture、Future、 Promise、Handler 、 Pipe
全球热点!贯穿汽车用户全生命周期,火山引擎数智平台能帮车企做这些事!
全球播报:当项目经理看世界杯决赛时…
在Windows中利用WSL2安装禅道17.7
观热点:纽约州进入紧急状态 美国暴风雪有多可怕?官方告诫市民别出门否则会冻死
快消息!稳住iPhone 14 Pro 富士康给员工发留任奖金:最高5000元
黑客携4亿推特用户数据勒索马斯克:想破财消灾还是被罚款?
世界看点:驾特斯拉Model X车祸 5个多月林志颖仍在康复:期待复工
一家三口发烧用洋葱退热成功 医生科普:低烧有用、高烧不建议
【全球新视野】工信部拟规定:APP应可便捷卸载 不得默认自动续订
当前关注:终于不用费劲算优惠了:天猫年货节取消跨店满减 直接降价
最新资讯:雷军刚投的激光雷达:120米以外就能探测到路面轮胎
环球讯息:李国庆称逃离北上广去二线商机无限 快反思降降价:看好杭州合肥长沙等
43mm大喇叭!荣耀亲选迪士尼便携蓝牙音箱上架 149元
天天讯息:从发现SQL注入到ssh连接
学习下Redis内存模型
环球观天下!更冷了!南方多地将有大到暴雪:湿冷“魔法攻击”来了
【世界独家】张艺谋新作 电影《满江红》官宣阵容:沈腾、易烊千玺双男主
每日热闻!专家:波司登万元羽绒服面料很低廉
焦点消息!文化输出网红要复出了?李子柒成子柒文化实控人:微念创始人退出公司
天天快讯:多语种录音转译!讯飞开放式办公耳机iFLYBUDS Air上市 899元
HTC G18上市价格是多少?HTC G18手机参数
小米3什么时候出的?小米3后盖怎么打开?
win7系统怎么设置防火墙?win7系统一键还原按哪个键?
显示器驱动程序停止响应的原因有哪些?电脑显示驱动程序出现问题怎么办?
洗衣机怎么消毒?洗衣机消毒方法有哪些?
科特迪瓦是哪个国家?科特迪瓦特产有哪些?
起初婚礼放鞭炮是为了什么?起初婚礼上放鞭炮是什么原因?