作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
马哈茂德Ridwan
验证专家 在工程
13 的经验

Mahmud是一名软件开发人员,拥有多年的经验和效率诀窍, 可伸缩性, 稳定的解.

分享

推荐引擎(有时也被称为推荐系统)是一种工具,它可以让 算法开发人员 在给定的物品列表中预测用户可能喜欢或不喜欢的东西. 推荐引擎是一个非常有趣的替代搜索字段, 因为推荐引擎可以帮助用户发现他们可能不会遇到的产品或内容. 这使得推荐引擎成为Facebook等网站和服务的重要组成部分, YouTube, 亚马逊, 和更多的.

推荐引擎的理想工作方式有两种. 它可以依赖于用户喜欢的物品的属性, which are analyzed to determine what 其他的 the 用户 may like; or, 它可以依赖于其他用户的好恶, 然后,推荐引擎使用它来计算用户之间的相似度指数,并相应地向他们推荐商品. 也可以将这两种方法结合起来构建一个更健壮的推荐引擎. 然而,像所有其他与信息相关的问题一样,选择一个 适合该问题的算法 要解决.

构建推荐引擎

在本教程中, 我们将引导您完成构建一个基于协作和记忆的推荐引擎的过程. 这个推荐引擎会根据用户喜欢和不喜欢的内容向他们推荐电影, 就像之前提到的第二个例子一样. 在这个项目中,我们将使用基本的集合操作、一点数学和节点.js / CoffeeScript. 可以找到与本教程相关的所有源代码 在这里.

集合与方程

在实现基于协作记忆的推荐引擎之前, 我们必须首先了解这样一个系统背后的核心思想. 对于这个引擎,每个项目和每个用户都只是标识符. 因此, 我们不会使用电影的任何其他属性(例如, 演员, 导演, 类型, 等.)作参考,以作出建议. 两个用户之间的相似性使用-1之间的十进制数表示.0和1.0. 我们称这个数字为相似性指数. 最后, 用户喜欢电影的可能性将使用-1之间的另一个十进制数字表示.0和1.0. 现在我们已经用简单的术语围绕这个系统建立了世界模型, 我们可以使用一些优雅的数学方程来定义这些标识符和数字之间的关系.

在我们的推荐算法中,我们将维护一些集合. 每个用户都有两组:一组是用户喜欢的电影,另一组是用户不喜欢的电影. 每部电影还将有两组与之相关联的用户:一组喜欢这部电影的用户, 还有一组用户不喜欢这部电影. 在生成建议的阶段, 将产生许多集合——大多数是其他集合的并集或交集. 我们还将为每个用户提供建议和类似用户的有序列表.

为了计算相似性指数,我们将使用Jaccard指数公式的一个变体. 最初被称为“公社系数”(由Paul Jaccard创造), 该公式比较两个集合并产生0到1之间的简单小数统计量.0:

相似性指数

该公式包括将任一集合中的公共元素的数量除以两个集合中所有元素的数量(只计数一次). 两个相同集合的Jaccard索引总是为1, 而两个没有公共元素的集合的Jaccard索引总是产生0. 现在我们知道了如何比较两个集合,让我们考虑一个可以用来比较两个用户的策略. 如前所述, 用户, 从系统的角度来看, 三样东西:标识符, 一套喜欢的电影, 还有一组不喜欢的电影. 如果我们只根据用户喜欢的电影集来定义用户的相似度指数, 我们可以直接使用Jaccard指数公式:

雅卡指数公式

在这里, U1和U2是我们比较的两个用户, L1和L2是U1和U2喜欢的电影集合分别. 现在, 如果你仔细想想, 喜欢同一部电影的两个用户是相似的, 那么不喜欢同一部电影的两个用户也应该是相似的. 我们在这里稍微修改一下方程:

修改equasion

而不是只考虑公式分子上常见的类, 我们现在也添加了一些常见的不喜欢. 在分母中,我们取用户喜欢或不喜欢的所有物品的数量. 既然我们已经以一种独立的方式考虑了好恶, 我们还应该考虑两个用户的偏好截然相反的情况. 两个用户一个喜欢一部电影,另一个不喜欢这部电影的相似度指数不应该为0:

两个用户的相似度指数

这是一个很长的公式! 但这很简单,我保证. 它和之前的公式很相似只是分子上有一点不同. 我们现在从两个用户的共同喜欢和不喜欢的数量中减去他们相互冲突的喜欢和不喜欢的数量. 这将导致相似性指数公式的值范围在-1之间.0和1.0. 两个品味相同的用户的相似指数为1.而两个电影品味完全不同的用户的相似度指数为-1.0.

现在我们知道了如何根据两个用户的电影品味来比较他们, 在开始实现我们自制的推荐引擎算法之前,我们必须探索另一个公式:

推荐引擎算法

让我们把这个方程分解一下. 我们所说的 P (U, M) 是用户的可能性吗 U 喜欢这部电影 M. ZLZD 用户的相似度指数之和是多少 U 所有喜欢或不喜欢这部电影的用户 M分别. |毫升| + | | 表示喜欢或不喜欢该电影的用户总数 M. 结果 P (U, M) 产生一个介于-1之间的数字.0和1.0.

就是这样. 在下一节中, 我们可以使用这些公式开始实现基于记忆的协同推荐引擎.

构建推荐引擎

我们将把这个推荐引擎构建为一个非常简单的节点.js应用程序. 在前端也会有很少的工作, 主要是一些HTML页面和表单(我们将使用Bootstrap使页面看起来整洁). 在服务器端,我们将使用CoffeeScript. 应用程序将有一些GET和POST路由. 尽管我们在应用程序中有用户的概念, 我们不会有任何详细的注册/登录机制. 为持久性, 我们将使用通过NPM提供的Bourne包,它使应用程序能够将数据存储在纯JSON文件中, 并对它们执行基本的数据库查询. 我们将使用快递.Js来简化管理路由和处理程序的过程.

在这一点上,如果你是新的 节点.js开发,你可能想克隆 GitHub库 这样就更容易理解本教程. 与任何其他节点一样.的项目,我们将开始 创建包.json文件 并安装此项目所需的一组依赖包. 如果您使用的是克隆的存储库,那么包.Json文件应该已经在那里了, 从这里安装依赖需要你执行" $ NPM install ". 这将安装包中列出的所有包.json文件.

的节点.这个项目需要的Js包是:

我们将通过将所有相关方法拆分为四个独立的CoffeeScript类来构建推荐引擎, 每个都将存储在“lib/engine”下:engine, 评定等级, 相似导线, 和建议. 类Engine将负责为推荐引擎提供一个简单的API, 并将其他三个类绑定在一起. 评定等级将负责跟踪喜欢和不喜欢(作为评定等级类的两个独立实例). 相似和建议将负责确定和跟踪相似的用户,并为用户推荐项目分别.

追踪喜好

让我们首先从我们的评级者类开始. 这是一个简单的例子:

类评定等级
	constructor: (@engine, @kind) ->
	add: (用户, 项,做) ->
	remove: (用户, 项,做) ->
	项sByUser: (用户,完成) ->
	用户sByItem: (项,做) ->

如本教程前面所述, 我们将有一个喜欢的评定等级实例, 另一个是不喜欢的. 为了记录用户喜欢某件商品,我们将它们传递给“评定等级#add()”. 类似地,要删除评级,我们将它们传递给“评定等级#remove()”.

因为我们使用Bourne作为无服务器数据库解决方案, 我们将把这些评级存储在一个名为"./ db - # {@kind}.Json ",其中kind为"喜欢"或"不喜欢". 我们将在评定等级实例的构造函数中打开数据库:

constructor: (@engine, @kind) ->
	@db =新伯恩”./ db - # {@kind}.json”

这将使添加评级记录变得像在我们的“评定等级#add()”方法中调用Bourne数据库方法一样简单:

@db.插入 用户:用户, 项目:项目, (err) =>

删除它们也是类似的(“db.删除“db”而不是“db”.插入”). 然而, 在我们添加或删除一些东西之前, 我们必须确保它不存在于数据库中. 理想情况下,对于真实的数据库,我们可以将其作为单个操作来完成. 与伯恩, we have to do a manual check first; 和, 一旦插入或删除完成, 我们需要确保为这个用户重新计算了相似度索引, 然后生成一组新的建议. “评定等级#add()”和“评定等级#remove()”方法看起来像这样:

add: (用户, 项,做) ->
	@db.find 用户:用户, 项目:项目, (err, res) =>
		如果res.长度 > 0
			返回(完成)

		@db.插入 用户:用户, 项目:项目, (err) =>
			异步.系列(
				(done) =>
					@engine.相似导线.更新用户,完成
				(done) =>
					@engine.建议.更新用户,完成
			),完成

remove: (用户, 项,做) ->
	@db.delete 用户:用户, 项目:项目, (err) =>
		异步.系列(
			(done) =>
				@engine.相似导线.更新用户,完成
			(done) =>
				@engine.建议.更新用户,完成
		),完成

为简洁起见,我们将跳过检查错误的部分. 在文章中这样做可能是合理的, 但是并不是忽略实际代码中的错误的借口.

另外两种方法, 该类的“评定等级#项sByUser()”和“评定等级#用户sByItem()”将涉及执行它们的名称所暗示的操作-查找由用户评分的物品和对物品评分的用户分别. 例如,当评定等级实例化为 Kind =“喜欢”,“评定等级#项sByUser()”将查找用户已评分的所有项目.

寻找相似的用户

下一节课:相似. 这个类将帮助我们计算和跟踪用户之间的相似度指数. 如前所述, 计算两个用户之间的相似度需要分析他们喜欢和不喜欢的物品集. 要做到这一点, 我们将依靠评定等级实例来获取相关项的集合, 然后使用相似度指数公式确定某些用户对的相似度指数.

寻找相似的用户

就像我们上节课一样,评定等级,我们将把所有内容放入名为"./ db-相似导线.我们将在评定等级的构造函数中打开该文件. 类将有一个方法" 相似导线#byUser() ", 这将使我们通过简单的数据库查找来查找与给定用户相似的用户:

@db.findOne 用户:用户, (err, {others}) =>

然而, 该类中最重要的方法是“相似导线#update()”,它的工作原理是获取一个用户并计算与之相似的其他用户的列表, 并将列表存储在数据库中, 以及它们的相似度指数. 首先要找到用户的喜好:

异步.汽车
	用户Likes: (done) =>
		@engine.喜欢.项sByUser用户,完成
	用户Dis喜欢: (done) =>
		@engine.不喜欢.项sByUser用户,完成
, (err, {用户Likes, 用户Dis喜欢}) =>
	项目= _.平([用户Likes 用户Dis喜欢])

我们还找到了所有给这些项目打分的用户:

异步.map 项s, (项,做) =>
	异步.地图(
		@engine.喜欢
		@engine.不喜欢
	], (评定等级,做) =>
		评定等级.用户sByItem 项,完成
	,做
, (err, others) =>

下一个, 对于每一个其他用户, 我们计算相似度索引并将其全部存储在数据库中:

异步.map others, (other,做) =>
	异步.汽车
		otherLikes: (done) =>
			@engine.喜欢.项sByUser其他,完成
		otherDis喜欢: (done) =>
			@engine.不喜欢.项sByUser其他,完成
	, (err, {otherLikes, otherDis喜欢}) =>
		做空,
			其他用户:
			相似:(_.十字路口(用户Likes otherLikes).长度+ _.十字路口(用户Dis喜欢 otherDis喜欢).长度_.十字路口(用户Likes otherDis喜欢).长度_.十字路口(用户Dis喜欢 otherLikes).长度)/ _.union(用户Likes, otherLikes, 用户憎,other憎).长度

, (err, others) =>
	@db.插入
		用户:用户
		其他:其他
	,做

在上面的代码片段中, 您将注意到,我们有一个与相似性指数公式本质上相同的表达式, 雅卡德指数公式的一种变体.

生成的建议

下一节课,建议,是所有预测发生的地方. 与类相似导线一样,我们依赖于另一个名为“./ db-建议.Json”,在构造函数中打开.

提出建议和建议

该类将有一个方法“建议#forUser()”来查找给定用户的计算建议:

forUser: (用户,完成) ->
	@db.findOne 用户:用户, (err, {建议}={suggestion: []}) ->
		无效,建议

计算这些结果的方法是“Suggestions#update()”. 这个方法,像“相似导线#update()”一样,将把用户作为参数. 该方法首先列出与给定用户相似的所有用户, 以及给定用户未评分的所有项目:

@engine.相似导线.byUser 用户, (err, others) =>
	异步.汽车 
		喜欢: (done) =>
			@engine.喜欢.项sByUser用户,完成
		不喜欢: (done) =>
			@engine.不喜欢.项sByUser用户,完成
		项s: (done) =>
			异步.map others, (other,做) =>
				异步.地图(
					@engine.喜欢
					@engine.不喜欢
				], (评定等级,做) =>
					评定等级.项sByUser其他.用户,完成
				,做
			,做
	, (err, {喜欢, 不喜欢, 项s}) =>
		项目= _.区别_.独特的(_.把项目弄平),喜欢,不喜欢

一旦我们有了所有其他用户和未评级的项目列出, 我们可以通过删除任何以前的推荐集来开始计算新的推荐集, 迭代每一项, 并根据现有信息计算用户喜欢它的可能性:

@db.delete 用户:用户, (err) =>
异步.map 项s, (项,做) =>
		异步.汽车
			likers: (done) =>
				@engine.喜欢.用户sByItem 项,完成
			dislikers: (done) =>
				@engine.不喜欢.用户sByItem 项,完成
		, (err, {likers, dislikers}) =>
			分子= 0
			对于其他人来说…….没有_.Flatten([喜欢,不喜欢]),用户
				其他= _.findW在这里(others, 其他用户:)
				如果其他?
				分子+= other.相似

				做空,
					项目:项目
					质量:分子/ _.联盟(liker dislikers).长度

	, (err, 建议) =>

一旦完成,我们将其保存回数据库:

@db.插入
	用户:用户
	建议:建议
,做

公开库API

Engine类内部, 我们把所有的东西都绑定在一个整洁的类似api的结构中,以便于从外部世界访问:

类引擎
	constructor: ->
		@喜欢 = new 评定等级 @, '喜欢'
		@不喜欢= new 评定等级 @, '不喜欢'
		@ 相似导线 = new 相似导线 @
		@建议=新的建议@

一旦我们实例化了一个Engine对象:

e =新发动机

我们可以很容易地添加或删除喜欢和不喜欢:

e.喜欢.add 用户, 项, (err) ->
e.不喜欢.add 用户, 项, (err) ->

我们也可以开始更新用户相似度指数和建议:

e.相似导线.update 用户, (err) ->
e.建议.update 用户, (err) ->

最后, 从各自的引擎类(以及所有其他类)导出此引擎类非常重要。.咖啡”文件:

模块.出口=引擎

然后,通过创建“索引”从包中导出引擎.Coffee”文件,只有一行:

模块.出口=要求./引擎”

创建用户界面

能够使用本教程中的推荐引擎算法, 我们希望通过网络提供一个简单的用户界面. 为此,我们在“web”中生成一个Express应用程序.文件和处理一些路由:

电影=要求./数据/电影.json的

引擎=要求./lib/engine”
e =新的Eengine 

App = 表达()

应用程序.设置'views', "#{__dirname}/views"
应用程序.设置'view engine', '玉'

应用程序.路线(/刷新)
.post(({query}, res, next) ->
	异步.系列(
		(done) =>
			e.相似导线.更新查询.用户,完成

		(done) =>
			e.建议.更新查询.用户,完成

	], (err) =>
		res.重定向”/?用户= #{查询.用户}”
)

应用程序.路线(' / ')
.post(({query}, res, next) ->
	如果查询.Unset是“yes”
		e.喜欢.删除查询.用户查询.movie, (err) =>
			res.重定向”/?用户= #{查询.用户}”

	其他的
		e.不喜欢.删除查询.用户查询.movie, (err) =>
			e.喜欢.添加查询.用户查询.movie, (err) =>
				如果犯错?
					返回下一个错误

				res.重定向”/?用户= #{查询.用户}”
)

应用程序.路线(' /不喜欢')
.post(({query}, res, next) ->
	如果查询.Unset是“yes”
		e.不喜欢.删除查询.用户查询.movie, (err) =>
			res.重定向”/?用户= #{查询.用户}”

	其他的
		e.喜欢.删除查询.用户查询.movie, (err) =>
			e.不喜欢.添加查询.用户查询.movie, (err) =>
				res.重定向”/?用户= #{查询.用户}”
)

应用程序.路线(“/”)
.get(({query}, res, next) ->
	异步.汽车
		喜欢: (done) =>
			e.喜欢.项sByUser查询.用户,完成

		不喜欢: (done) =>
			e.不喜欢.项sByUser查询.用户,完成

		建议: (done) =>
			e.建议.我们查询.用户, (err, 建议) =>
				Done null, _.地图_.sortBy(建议, (suggestion) -> -suggestion.weight), (suggestion) =>
					_.findW在这里电影,id:建议.项

	, (err, {喜欢, 不喜欢, 建议}) =>
		res.呈现“指数”,
			电影:电影
			用户:查询.用户
			喜欢,喜欢
			不喜欢:不喜欢
			建议:建议...4]
)

在这个应用程序中,我们处理四条路由. 索引路由“/”是我们通过呈现一个Jade模板来提供前端HTML的地方. 生成模板需要一个电影列表, 当前用户名, 用户的好恶, 以及给用户的四大建议. Jade模板的源代码不在本文中,但是可以从 GitHub库.

“/喜欢”和“/不喜欢”路由是我们接受POST请求记录用户喜欢和不喜欢的地方. 如果有必要,这两种路由都通过首先删除任何冲突的评级来添加评级. 例如, 如果用户喜欢他们以前不喜欢的东西,处理程序会首先删除“不喜欢”评级. 这些路线还允许用户“不喜欢”或“不喜欢”一个项目,如果需要的话.

最后,“/refresh”路由允许用户根据需要重新生成他们的推荐集. 尽管如此,每当用户对某项商品进行评级时,都会自动执行此操作.

试驾

如果您尝试通过本文从头开始实现此应用程序, 在测试它之前,您需要执行最后一个步骤. 您需要创建一个“.Json " file at " data/movies.Json”,并填充一些电影数据,像这样:

您可能需要复制 GitHub库,其中预先填充了一些电影名称和缩略图url.

一旦所有源代码准备好并连接在一起, 启动服务器进程需要调用以下命令:

$ NPM start

假设一切顺利,您应该看到终端上出现以下文本:

收听5000台

因为我们还没有实现任何真正的用户身份验证系统, 原型应用程序仅依赖于访问“http://localhost:5000”后选择的用户名。. 输入用户名后, 然后提交表格, 你应该被带到另一个有两个部分的页面:“推荐电影”和“所有电影”。. 由于我们缺乏基于协作记忆的推荐引擎最重要的元素(数据), 我们将无法向这个新用户推荐任何电影.

此时此刻, 您应该打开另一个浏览器窗口到“http://localhost:5000”,并在那里以不同的用户登录. 作为第二个用户喜欢和不喜欢一些电影. 返回到第一个用户的浏览器窗口并对一些电影进行评级. 确保你至少为两个用户评价了几个共同的电影. 您应该立即开始看到推荐.

改进

在本算法教程中,我们构建了一个原型推荐引擎. 当然有很多方法可以改进这个引擎. 本节将简要介绍一些需要改进的领域,以便大规模使用. 然而, 在可伸缩性方面, 稳定, 其他类似的属性也是必需的, 您应该始终求助于使用经过时间考验的解决方案. 就像文章的其他部分一样, 这里的想法是提供一些关于推荐引擎如何工作的见解. 而不是讨论当前方法的明显缺陷(例如我们实现的一些方法中的竞争条件), 改进将在更高的层次上讨论.

这里的一个非常明显的改进是使用真正的数据库,而不是我们基于文件的解决方案. 基于文件的解决方案可能在小规模的原型中工作得很好, 但对于实际使用来说,这根本不是一个合理的选择. Redis是众多选择之一. Redis速度很快,而且已经做到了 特殊的功能 这在处理类似集合的数据结构时非常有用.

我们可以简单解决的另一个问题是,每当用户对电影进行评分或更改评分时,我们都会计算新的推荐. 而不是实时地重新计算, 我们应该为用户对这些推荐更新请求进行排队,并在后台执行它们——也许可以设置定时刷新间隔.

除了这些“技术”选择, 还可以做出一些战略选择来改进这些建议. 随着项目和用户数量的增长, 生成推荐将变得越来越昂贵(在时间和系统资源方面). 通过只选择一部分用户来生成推荐,可以加快这一过程, 而不是每次都处理整个数据库. 例如, 如果这是一个餐厅推荐引擎, 可以将类似用户集限制为只包含居住在同一城市或州的用户.

其他改进可能涉及采用混合方法, 基于协作过滤和基于内容过滤的推荐在哪里生成. 这对于诸如电影之类的内容尤其有用, 内容的属性在哪里定义得很好. 网飞公司, 例如, 走这条路, 根据其他用户的活动和电影的属性来推荐电影.

结论

基于内存的协同推荐引擎算法是一个非常强大的东西. 我们在本文中试验的方法可能比较原始, 但它也很简单:很容易理解, 而且很容易构建. 它可能远非完美, 但是推荐引擎的健壮实现, 例如:推荐人, 是建立在相似的基本思想之上的吗.

就像大多数涉及大量数据的计算机科学问题一样, 获得正确的推荐有很多关于 选择正确的算法 以及要处理的内容的适当属性. 我希望这篇文章能让您对基于协作记忆的推荐引擎在使用过程中发生的情况有所了解.

聘请Toptal这方面的专家.
现在雇佣
马哈茂德Ridwan

马哈茂德Ridwan

验证专家 在工程
13 的经验

达卡,达卡区,孟加拉国

2014年1月16日成为会员

作者简介

Mahmud是一名软件开发人员,拥有多年的经验和效率诀窍, 可伸缩性, 稳定的解.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 隐私政策.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 隐私政策.

Toptal开发者

加入总冠军® 社区.