作者在上一篇文章《基于内容的推荐算法》中介绍了基于内容的推荐算法的实现原理。在本篇文章中作者会介绍一个具体的基于内容的推荐算法的实现案例。
该案例是作者在 2015 年基于 Erlang 语言开发的相似视频推荐系统,从开发完成就一直在公司多个产品线中使用,该算法目前一直还在使用中,已经四年了,效果还相当不错。
本文会从视频相似推荐系统简介、算法原理及实现细节、问题与难点、为什么用 Erlang 语言开发、系统架构与工程实现、核心亮点、未来优化方向、个人收获与感悟等 8 个方面来讲解。
通过学习本文读者可以深入了解基于向量空间模型的相似推荐算法原理及实现细节、对 Erlang 语言特性也会有基本的了解、同时对实现一个简单高效的 Master/Slaver 架构的分布式计算框架的原理和工程细节有基本的概念。
一、视频相似推荐系统简介
作者所在公司从事智能电视/智能机顶盒上的视频业务(主要产品是电视猫),由于智能电视上主要依赖遥控器操作,所以操控不是很方便,产品对推荐系统的依赖会更大。而相似推荐就是为每个视频关联一组相似(或者有一定关联关系)的视频。具体产品形态见下图 1 和图 2:
图 1:电视猫电影详情页的相似影片
图 2:电视猫奇趣短视频的关联推荐
我们的产品包括长视频(电影、电视剧、动漫、少儿、纪录片、综艺 6 种)和短视频(资讯、奇趣、游戏、体育、戏曲、音乐 6 种)两大类,我们需要为每类视频类型的节目关联一组相似推荐(在同一类别中做推荐,电影的相关推荐只能是电影、体育的关联推荐是体育等)。
具体怎么计算相似呢?我们是基于视频的 metadata 信息来计算两个视频之间的相似度,利用相似度从高到低来排序,获取某个视频最相似的 topN 作为关联或者相似推荐。
这里提一下,虽然长视频和短视频采用同一套算法体系,但是由于视频类型不一样,前端的产品形态是不一样的。由于短视频单片时长较短,一般几分钟就播完了,所以短视频的关联推荐采用的是信息流的方式,播放原视频,它关联的视频会作为信息流播放,这样整体用户体验会好很多(这就是上面图 1 和图 2 虽然都是相似推荐但是产品形态不一样的原因)。
相信通过上面的介绍,大家对视频相似推荐的产品形态比较清楚了。在下面一节,我们来讲解具体的算法实现细节。
二、算法原理及具体实现细节
视频行业一般是具备结构化信息的,一般视频公司会有 CMS(Content Management System,内容管理系统) ,该系统一般会包含媒资库,媒资库中针对每个节目会有标题、演职员、导演、标签、评分、地域等维度数据,这类数据一般存放在关系型数据库(如 MySQL)中。这类数据,我们可以将一个字段(也是一个特征)作为向量的一个维度,这时用向量表示视频,每个维度的值不一定是数值,但是形式还是向量化的形式,即所谓的 向量空间模型 ( Vector Space Model,简称 VSM )。这时我们可以通过如下的方式计算两个视频之间的相似度。
假设两个视频的向量表示分别为:
这时这两个视频的相似度可以采用如下公式计算:
其中 代表的是向量的两个分量 之间的相似度。可以采用 Jacard 相似度等各种方法计算两个分量之间的相似度。上面公式中还可以针对不同的分量采用不同的权重策略,见下面公式,其中 是第 t 个分量(特征)的权重,具体权重的数值可以根据对业务的理解来人工设置,或者利用机器学习算法来训练学习得到。
有了上面的计算公式,我们就知道该怎么计算两个视频的相似度了。但是上面公式中未解决的问题是,对于某一个具体的维度,我们该怎么计算相似度呢?
上式中的 、 分别代表两个节目第 i 个维度的值,可以是数值、字符串等。下面我们列举一下针对不同的分量怎么计算分量之间的相似度。
1. 年代
假设两个视频 的年代分别是 和 ,计算在年代这个维度的相似度可以采用如下分段函数表示:
其中(1)、(2)是剔除掉无效的 值,(3)是给出的当 在 0 到 2020 年之间的一个计算公式, 值越大,最终的相似度越大。这里相似度与 无关( 所以严格来说,不叫做相似度,而是贡献度,下面类似,不再说明,直接用贡献度来描述) 在其他条件相同的情况下,越是近期拍摄的视频权重越大。
2. 标题
假设两个视频 和 分别是这两个视频的标题,首先我们可以通过分词或者关键词提取算法将 和 提取关键词得到对应的关键词,假设如下:
其中 分别是 提取关键词后的集合,那么我们可以用如下 Jacard 相似度来计算 之间的相似度:
这里涉及到很多 NLP 方面的技术和领域知识。首先我们需要有视频行业相关语料,才能够保证分词准确,另外标题可能是杂乱无章的,比如很多电影标题中包含粤语版、国语版、新传、第三季等对相似度价值不大的词或者词组,这些都需要借助规则或者 NLP 技术做预处理,才能得到较好的关键词提取效果,最终才会有较好的相似度计算效果。
3. 标签
对于标签,可以采用跟上面标题一样的计算方法,因为标题提取关键词后的每个关键词可以看成是一个标签。这里不再细说怎么计算了。
4. 地域
假设两个视频 和 分别是这两个视频的出品地,我们可以用如下公式来计算这两个视频在地域维度的相似度。
这个公式可以考虑得更复杂一点,将地区按照语言、地域等分成几类,比如北美、东欧、东南亚等,当两个视频的地域完全一样时相似度为 1,当两个视频在同一个地区分类内,相似度为 0.5(或者其他大于 0 小于 1 的值,根据业务经验开发人员自己定义),否则为 0,这样会更加精确合理。
5. 豆瓣评分
假设两个视频 和 分别是这两个视频的豆瓣评分(豆瓣评分是 0 到 10 之间),下面公式给出视频 在豆瓣评分这个维度的 贡献度 ,评分越高,贡献度越大。
6. 是否获奖
假设两个视频 和 分别是这两个视频所获的奖项,那么可以简单用下面公式来计算视频 在获奖这个维度上的 贡献 ,当然计算公式可以更加复杂,对不同奖项区别对待,级别更高的奖项可以给出更高的贡献度。
7. 导演与演职员
通常情况下,视频一般只有一个导演,导演的相似度可以类似地域一样的方法计算。而演职员可以采用跟标签类似的方法计算相似度。
上面给出了几个内容维度怎么计算两个视频在该维度的相似度(或者贡献度),我们在实际项目中用到的维度比这个更多,但是计算原理类似,这里不再一一列举。另外,具体的计算和处理逻辑也会更复杂,比如对于标签相似度,标签之间是有相似关系的,比如惊悚和恐怖,它们是相似的。上面方法没有考虑到标签之间的这种相似关系而是将他们看成不同的标签,计算相似性多少有点简单。实际上,我们项目中用到了数学中等价类的思想,将很相似的标签看成是同一类的,在同一类的标签之间是有相似度的,这样如果一个电影的标签是恐怖,另外一个电影的标签是惊悚,按照上面的计算方法相似度是 0,而按照我们等价类的思路,相似度是大于 0 的。同时,对于某些类别的视频我们加了很多规则性的东西,更好地适应不同类别内容的相似计算。像这类提升效果的处理还有很多,这里不再细说。
三、问题与难点
该项目最早(2012 年底)是采用 Java 来开发的,写一个单机程序,当时视频量还比较少,也没有这么多视频类别,基本可以支撑,当后面加入越来越多的视频类别,每类视频数量也越来越多时,单机计算的性能就出现瓶颈了。当时采用的应对方案是将视频按照类别分成几组,每一组采用一个 Java 线程计算,虽然某种程度上可以做到并行计算,但是每个视频类型的数量及增长速度是不一样的,人工按照类型拆开分布不够均匀,问题比较多。回顾老的设计面临的问题,我们来总结一下,基于该算法的相似视频推荐主要难点有如下几个:
1. 数据量大,增速快
前面讲到我们长短视频加起来大概有 12 个类别,类别多,总量有几百万条视频。对于短视频来说,特别是资讯、体育等,每天都有大量(万级)新增的节目。
第一次全量计算所有节目的相似度时,由于需要计算的节目太多,必须采用分布式计算,否则计算太慢,单机可能要花几个月时间才能完全算完。
2. 需要实时计算
在我们的 APP 上,短视频一般按照时间排列,新的短视频放在最前面,用户更容易看到。所以对于新增的视频节目,我们需要实时计算出相似的视频,否则只能用默认推荐顶替相似推荐,效果肯定不会太好。
3. 计算与某个视频最相似的视频需要遍历所有视频
我们一般只关联推荐同一大类的视频,但是各个类别数量是极不均衡的,电影 1-2 万部,资讯大概有上百万。在我们为某个资讯计算与它最相似的 N 个资讯时,我们需要遍历所有其他的资讯才能找到与它最相似的 N 个,怎么设计这个遍历过程对计算时间有很大影响。
4. 更新已经计算的视频的相似度
对于新入库的视频 A,我们需要计算它的关联推荐,同时,对于某个已经计算好的视频 B,如果新加入的视频 A 与 B 的相似度高于 B 原来计算好的 topN 相似的某个视频的相似度,这时需要更新 B 的相似度列表,将 A 添加进去,同时删除原来 B 的相似列表中相似度最低的视频。每个新视频的加入都可能影响很多已经计算过相似度的视频,怎么在短时间内快捷地查找出这类需要更新相似度列表的视频也是一个挑战。
上面我们对相似视频推荐的算法原理及难点做了比较细致的讲解。为了实现这些算法和克服难点,我们基于 Erlang 语言完美地解决了这些问题,在讲怎么利用 Erlang 语言来从工程上实现上面的算法之前,我们先简单对 Erlang 语言做一个初略的介绍,方便读者更好地理解后续算法的架构和工程实现原理。
四、为什么要用 Erlang 语言开发
1. Erlang 语言简介
Erlang 是一种通用的面向并发的编程语言,它由瑞典电信设备制造商爱立信所辖的 CS-Lab 开发,目的是创造一种可以应对大规模并发事件的编程语言和运行环境。Erlang 问世于 1987 年,经过十年的发展,于 1998 年发布开源版本。Erlang 是运行于虚拟机的解释性语言。在编程范型上,Erlang 属于多重范型编程语言,涵盖函数式、并发式及分布式。
Erlang 是一个结构化、动态类型编程语言,内建并行计算支持。最初是由爱立信专门为通信应用设计的,比如控制交换机或者变换协议等,因此非常适合于构建分布式、实时并行计算系统。使用 Erlang 编写出的应用程序运行时通常由成千上万个轻量级进程组成,并通过消息传递相互通讯。进程间上下文切换对于 Erlang 来说非常简单,比起 C 程序的线程切换要高效得多。
2. Erlang 语言的特性
Erlang 语言虽然开发于上世纪 80 年代,但是很多思想是非常超前的,在当前云计算时代具有非常实用的价值,算是在多如牛毛的编程语言大军中独树一帜。这些特性也是我选择利用 Erlang 语言开发相似视频推荐系统的主要原因之一。下面我们列举一些 Erlang 语言的主要特性:
(1) 函数式编程及部分语法特性
Erlang 是一个函数式编程语言,即可以将函数作为参数传入别的的函数,并且可以作为函数的返回值。
Erlang 语法也比较特殊,通过递归来实现迭代逻辑,没有其他语言的 while 和 for 循环结构。Erlang 的变量跟数学中类似,只能单次赋值,不可重复赋不同值。Erlang 的模式匹配能力也非常强大。Erlang 内置了很多数据类型及操作函数,辅助更好地进行函数式编程。
(2) 并发模型
Erlang 是一个高并发语言,天生支持高并发,Erlang 基于 Actor 的并发编程模型,进程间通信通过消息传递进行,高效自然可靠。Erlang 语言将并发模式作为自己的核心特性,非常方便构建分布式处理逻辑,从一开始设计之初就充分利用多核处理器性能,非常适合在现代服务器上构建分布式应用。
(3) 跨平台
Erlang 语言与 java 类似,采用虚拟机来解释执行代码,Erlang 的 beam 虚拟机负责对代码进行解释执行,因此具备跨平台特性,一次编译到处运行。
(4) 错误处理
Erlang 是一个高容错的编程框架,它对错误处理有两个设计哲学:让另外一个程序来解决错误,如果出错就让程序崩溃并重新启动。第一个设计哲学将错误”外包“给另外一个专门的程序监控和处理,这样原来的程序将核心放到处理逻辑上,这个监控程序可以放在另外一台机器上,如果原来程序所在机器挂了,监控程序也可以发现问题。基于第二个设计哲学,既然处理逻辑和处理错误的程序分离了,如果处理逻辑的程序挂了(一般也是遇到偶发的情况或者传入非法参数等原因挂掉),处理错误的程序就可以让它重新启动,这样系统又可以正常运行了。这个哲学跟我们熟知的重启可以解决 90%以上问题不谋而合。
(5) OTP 框架
OTP 是包装在 Erlang 中的一组库程序。OTP 构成 Erlang 的行为机制(behaviors),用于编写服务器、有限状态机、事件管理器。不仅如此,OTP 的应用行为(the application behavior)允许程序员把写好的 Erlang 代码打包成一个单独的应用程序;监测行为(the supervisor behavior )允许程序员创建树状结构的进程依赖链,使得某个进程死后,它的监控进程(父进程)会重新启动它而复活。
OTP 提供大量通用的库程序,可以轻松创建具有高度容错、热机换码等功能的高质量高效的程序。你可以通过 OTP 获得如下好处:
a 通用服务器、有限状态机、事件管理器;
b 标准化应用程序结构;
c 代码热机更换;
d 监测树行为机制,让你的进程永不”罢工“。
OTP 是在 Erlang 之上构建系统平台的标准方式。大型 Erlang 项目,如 ejabberd, CouchDB 等,都是基于 OTP 开发的。我们的视频推荐系统也大量利用 OTP 的各种行为机制,这样只需要实现核心的接口,进程间的调用、监控这些能力行为机制很容易帮我们做到。
(6) 内嵌的 Mnesia 数据库
Mnesia 是内嵌入 Erlang 的一款容错的、分布式可拓展的交易型数据库,数据按照表来组织,类似于关系型数据库,数据可以选择存在内存或者磁盘中,并且有一套自己的非常方便的查询语言,可以对数据进行方便快捷的读写查询等操作。
3. 为什么选择 Erlang 语言来开发相似视频推荐系统
有了上面对 Erlang 语言的简单介绍,我们在这里简单介绍一下该项目采用 Erlang 语言来开发的主要原因:
(1) Erlang 语言有比较牛的互联网应用
大家耳熟能详的互联网软件,如 CouchBase 、 CouchDB (apache 基金会上的一款文档型数据库,类似 MongoDB), RabbitMQ (消息队列中间件),还有基于 XMPP 协议的 IM 开源软件 ejabberd 等非常流行的软件都是基于 Erlang 语言开发的,它们在工业界有大量应用案例。
另外大家可能知道 whatsApp 背后只有 50 多位工程师支撑近 5 亿的月活用户规模,在 2014 年被 Facebook 以 190 亿美元收购。而该公司背后用到的编程语言正是 Erlang。当时,作者看到这个消息时是非常震惊的,对 Erlang 语言越发佩服了。
(2) Erlang 语言的几个特性非常适合该项目
前面对 Erlang 语言的特性做了简单描述,这些特性是构建相似视频推荐系统的核心基础,我们在这里重点讲一下对我们开发该系统非常重要的一些特性。
Erlang 屏蔽了跨服务器交互的细节,内嵌了跨服务器访问的 rpc 及网络交换函数,跨服务器交互跟与本地交互基本一样,所以非常适合开发分布式程序,能够快速扩展。
Erlang 自带非常多的数据处理函数,方便对 Set、List、Map、字符串等结构的各类操作。Erlang 包含 ETS、DETS 等 key-value 的分布式数据结构以及嵌入式数据库 Mnesia,非常方便对数据进行读写等操作。
Erlang 的 OTP 框架和错误处理机制也是非常的强大,适合开发稳定高效的应用程序。
正是 Erlang 有这些成功的软件产品、优秀的应用案例及非常有意思的特性,让作者对 Erlang 崇拜不已,跃跃欲试,最终决定采用 Erlang 来开发相似视频推荐系统。对 Erlang 语言有兴趣的读者可以看参考文献 2,Erlang 的作者写的一本全面介绍 Erlang 编程的书,非常值得一读。
五、系统架构与工程实现
前面对相似视频的算法实现细节及 Erlang 的特性做了完整的介绍,在本节我们就来详细讲解怎么基于 Erlang 的一些特性从工程上实现一个高效的分布式的 Master/Slaver 架构的相似视频推荐系统。
首先我们给出相似视频推荐的架构图(见下面图 3),再针对每个模块详细说明实现的细节。
图 3:基于 Erlang 语言的相似视频推荐架构图
另外,整个项目的工程目录如下图,这里简单解释一下:conf 是配置文件相关目录,doc 是文档相关目录,ebin 是编译文件目录,log 是日志目录,Mnesia.helios@Platform-recommended-couchbase11 是 Mnesia 数据存储目录,out 是输出相关目录,RelevanceRecommend.* 、similarity_computing.app 是工程启动及配置相关文件,src 是源码。
图 4:基于 Erlang 语言的相似视频推荐工程目录结构
相似视频推荐采用主流的 Master/Slaver 架构,主要包括 Master、Slaver、Riak Cluster、Cowboy server 4 个核心部分,其中 Master、Slaver 是整个相似视频推荐算法的核心。Master 主要负责任务的分配、跟 Slaver 保持联系、并且从 MySQL 中将 metadata 同步到 Mnesia 中,而 Slaver 主要负责相似度计算,计算完后将推荐结果插入 Riak 集群中。我们在下面分别介绍这 4 个模块的核心功能和实现细节。
1. Master 节点模块与功能
Master 节点主要负责任务分派,数据同步,处理节点加入及退出等异常情况。Master 包含 4 个主要组件,如上图,各个组件的功能如下:
(1) data sync 模块
该模块负责将需要计算相似性的视频从 MySQL(媒资库)同步到 Slaver 的 Mnesia 集群中,Slaver 计算时直接从本地 Mnesia 读取数据来进行相似计算。由于需要参与计算的字段是较少的(媒资库字段很多,我们只选择同步对计算相似度有价值的字段),这里我们采用 Mnesia 的内存存储,将所有数据存在内存中,方便计算程序更快地从 Mnesia 读取需要参与计算的视频 metadata,提升计算速度。
该模块不光可以具备批量读取 MySQL 所有数据的能力(项目第一次跑的时候需要全量计算),同时还需要实时监控媒资库的变化,如有新视频加入,马上(在秒级内)将新视频同步到 Mnesia 中。
(2) add node 模块
当加入新节点时,通过重启 Master 节点,Master 节点与新节点建立联系,识别出新节点,并把新节点加入计算集群,同时将 Mnesia 上的数据均匀分配到所有节点(包括新加入的节点)、给新节点分派计算任务(如果当前还有未计算过视频相似度的视频的话)。
(3) heartbeat 模块
Master 节点定期(几秒钟)向所有 Slaver 节点发送心跳信号,通过该信号探测 Slaver 是否活着,如果一段时间后 Slaver 无任何响应,Master 会认为该 Slaver 挂掉了,这时会将该挂掉的 Slaver 从计算列表中删除,后续新的计算任务不再分配给该 Slaver。
(4) Task allocation 模块
Slaver 节点上启动多个(一般可以设置为该服务器核数的 1-2 倍,如 4 核机器,可以启动 4-8 个进程进行计算,有效利用多核计算能力)进程进行计算,等待 Master 节点来分配任务。Master 节点定期跟 Slaver 节点通讯,轮询各个 Slaver 节点,了解 Slaver 节点是否空闲,如有空闲并且现在还有未完成的计算任务,那么 Master 将新的计算任务分配给该 slaver 进行计算。
(5) similarity update 模块
如果新加入的视频 A 比视频 B 相似列表中相似度最低的视频相似度更大,这时我们就需要更新视频 B 的相似列表,将 A 添加进去,同时将 B 原来相似列表中相似度最低的剔除掉(见下面图 5)。
图 5:如果新视频相似度大于某个视频的相似列表,需要更新相似列表
那么怎么更新老视频的相似推荐列表呢?一般来说,我们可以采用如下的方法,该方法也非常简单,容易理解。
在 Master 节点维护一张视频 id 和它的相似列表最小相似度的表(见下表,表中的视频都是已经计算完相似推荐的视频)。新加入的视频 A 计算完相似视频后(在计算 topN 相似度时,保存 A 跟每个其他视频的相似度),将 A 视频与每个其他视频的相似度与下表比对,假设 A 与 id_1 的相似度大于 s_1,那么就需要更新 id_1 的相似推荐列表了。
实际上可以做很多简化,比如我们可以先求出上表中第二列的最小值 (见下面公式),我们只保留 A 与其他视频相似度大于 的视频,其他视频直接丢弃(其实很多视频可以丢弃,毕竟很多视频跟 A 是没有任何相似度的),而不会影响更新计算逻辑,这些相似度大于 的视频才是可能需要更新推荐列表的视频。
Master 节点除了上面 5 个核心模块外,还维护两个关键的数据结构:
A :living_nodes:记录集群目前可用的 Slaver 节点,如有有节点加入或者挂掉会更新该数据。
B: need_computing_id: 记录哪些视频还没有计算相关视频推荐列表,针对这些视频在任务分配模块中分派给 Slaver 节点计算,分配出去后将该视频从待计算列表中删除,避免重复计算。如果有新视频加入,新视频的 id 会写入该列表。
2. Slaver:主要负责计算任务
Slaver 节点只有一个核心模块,即 计算模块 ,负责根据 Master 节点指派的任务进行相似计算。当 Master 将某个视频的计算任务分配到 Slaver 时,Slaver 从 Mnesia 读取这个视频的 metadata 信息,并计算该视频与该视频所在 group(如电影组)的所有其他视频的相似度,将相似度最大的 TopN 存到 Riak 集群中。
这里我们对核心的计算过程进行更详细的讲解,让大家知道我们是怎么解决 TopN 最相似视频的计算问题的。在下面图 6 中每个 Slaver 节点中有 4 个 worker(工作进程,负责进行相似计算),每个 worker 维护一个最大堆(最大堆中保留的元素个数就是我们需要计算的 TopN 的相似视频数),最大堆负责保留最相似的 N 个视频及相似度。当 Master 将某个视频 A 分配给 worker1 时,worker1 先从 Mnesia 集群中将所有与 A 在同一类别(如电影)中的所有视频取出来,循环计算与 A 的相似度,计算完一个就丢给最大堆,当所有的视频与 A 的相似度都计算完后,这些相似视频都丢给了 worker1 维护的最大堆,根据最大堆的性质,最终最大堆中留下的视频(及相似度)就是与 A 最相似的 N 个相似视频了。
图 6:Slaver 中进行相似计算过程与逻辑
上面的最大堆是基于 Erlang 的 ETS 数据结构及相关操作构建的,它非常高效,也是我们整个计算引擎的核心子模块,可以自动对丢入的{(videoid(视频 Id),similarity_score(相似度))}结构进行排序,保留最相似的 N 个。上面提到 worker 中包含的计算部分,即是基于我们在第二部分中的公式进行计算的。当某个视频最相似的 TopN 计算完成后,worker 会将推荐列表插入 Riak 集群,供前端接口调用。
这里面也是有很多可优化点的,比如对于新闻或者体育等时效性很强的视频,我们可以只从 Mnesia 中选取一段时间内(比如过去 1 天)的视频来计算相似度,这样就不需要计算相似度时跟 Mnesia 中同一组中的所有其他视频计算相似度,这样大大节省了计算时间。再比如,如果计算相似度中标签的权重最大,我们计算视频 A 与其他视频相似度时,如果 A 与其他视频的标签相似度为 0(或者很小),我们就没必要计算它的其他维度的相似度了,直接将该视频丢弃计算下一个。这些优化点还很多,这里不一一描述。
3. Riak 集群:负责最终相似推荐结果的存储
Riak 是基于 Erlang 语言开发的一个分布式的 key-value 存储系统,可以非常容易地水平扩展,非常适合大规模的数据存储,是整个相似视频推荐系统的最终计算结果的存储模块,所有视频的相似推荐列表都存在 Riak 集群中。Slaver 的 worker 计算完一个视频的相似度后会直接插入 Riak。
4. 响应请求模块:基于客户端用户请求,给出推荐结果
当用户在客户端访问某个视频的详情页时,客户端向服务端发送请求,请求响应模块根据用户请求从 Riak 集群中将该节目的相似列表取出来,并将其他需要的信息(如标题、演职员、海报图等在前端展示需要用到的节目 metadata 信息,这些信息我们存在 Redis 集群中)填充完整后返回给用户。
请求响应模块是基于 Cowboy (一款基于 Erlang 开发的高性能轻量级接口服务器)来开发的。从前面的介绍中可以知道,Cowboy 除了从 Riak 中获取推荐列表外,还需要从 Redis 中获取节目的 metadata 信息做填充。
六、核心亮点
到此为止,我们基本讲完了相似视频推荐的核心算法原理与基于 Erlang 实现的工程架构,该系统是作者在 15 年开发的,一直在作者公司的两个产品中使用到现在,其中一个产品目前还是用的该算法(另外一个产品基于 Spark 平台做了重构,整体效果更好),该算法在服务公司业务的 4 年中,虽然视频种类和数量有了非常大的增长,但是系统一直比较稳定,也能够很好地应对视频量的增长,并且效果还不错,这得益于 Erlang 良好的容错机制及该系统较好的分布式扩展能力。在这里我们回顾一下该系统的亮点,也让我们可以更好地理解它的价值。
1. 分布式可拓展能力
该系统采用 Master/Slaver 架构,可以通过水平地增加服务器来拓展该系统的计算能力,同时当全量计算完后,后面就是增量计算了,增量计算相对没有那么大的计算量,不需要这么多计算资源,我们可以缩减部分服务器节省开支。
2.可以实时对新增加的视频做计算
Master 的 Data sync 模块近实时监控媒资库 MySQL,如果有新视频加入,马上将该视频同步到 Mnesia 中,并分派给 Slaver 进行计算,在分钟级内新视频就可以完成计算,这样基本可以有效避免新入库视频的相似推荐冷启动问题。
3.系统很稳定
该系统一般很少出问题,这得益于 Erlang 的 OTP 框架,我们的 Master 和 Slaver 服务都是基于 OTP 框架来实现的,每个进程都有一个 supervisor 进程,当进程挂掉后,supervisor 会重启该进程,避免了因为偶尔的故障或者异常数据导致的系统崩溃,最终让我们的系统非常稳定。
4.将计算过程解耦合,抽象为最大堆来维护 topN 最相似的视频列表
将计算相似性计算抽象为一个计算模块,每个 worker 通过维护一个最大堆,可以非常有效地解决最相似的 topN 计算问题。同时,针对新闻、体育等时效性强的视频类型,我们还可以只取最近一段时间内的视频来计算相似度,进一步减少计算量。
5. 充分利用多核能力
每个 Slaver 可以启动多个 worker 进行计算,充分利用现代服务器多核的能力,大大加速了计算过程。
除了上述优点外,我们通过配置文件来定义各种参数(比如一个 Slaver 可以启动多少个 worker),可以方便地对参数进行调整,系统启动时会首先解析这些参数。我们也可以一键启动 Master 节点和所有的 Slaver 节点,整个配置和启动过程跟 Spark 比较类似。该系统可以看成基于 Erlang 语言开发的具备特定功能的类 Spark 的小型分布式计算平台。
七、未来优化方向
虽然该系统有很多优点,但由于个人能力及精力有限,并且对 Erlang 的了解还没有达到炉火纯青的阶段,该系统还有非常多的地方是可以做优化的。下面对可能的优化点进行说明,作为后面努力的方向。
1. 算法本身的优化
该系统基于视频内容的简单向量空间模型来计算相似度,虽然算法原理简单,但是由于视频的 metadata 比较杂乱,相似性效果受到数据质量好坏的严重影响。同时,向量空间模型是一个比较简单的模型,无法获得更复杂的特征表示。可行的优化点是,我们可以基于 metadata 数据或者用户行为数据做嵌入,为每个视频构建一个稠密的特征向量表示,该系统可以通过稠密向量的相似度来计算视频的相似性。其他各类计算视频相似度的方法都是可以使用的。
目前我们的计算相似度算法和整个系统还是耦合比较紧密的,通过优化是可以将计算相似性做成可插拔的组件的,这样就可以方便更换计算引擎。
另外,我们的向量空间模型各个维度的权重是根据人工经验自定义的,比较主观,其实是可以
利用用户点击反馈机制自动化学习最优参数的,这样可能效果会更好。
2. 调度策略的优化
当前该框架的调度策略还非常粗暴,对于每个需要计算相似推荐的视频,直接从所有 Slaver 中先过滤出有空余资源的 worker,将任务分配给第一个空闲的 worker。针对每个视频都要从头过滤一遍,效率很低。
更好的方式是每个 Slaver 节点维护一个待计算相似度的固定长度的视频队列,当队列中待计算的视频都计算完了相似度,Slaver 主动向 Master 申请待计算的视频。这样将主动权放到 Slaver 上,减少原来分配方案中毫无意义的轮询,同时也减轻了 Master 节点的压力。
3. 部署方式的优化
目前的系统虽然部署非常容易,只要在每台服务器上安装 Erlang,将该项目编译好,将编译后的工程代码分发到每台服务器上统一的目录下,修改每台服务器上的配置文件(实际上所有 Slaver 上配置是一样的,跟 Master 上略有不同)就可以启动了。
更好的方式可以利用 docker 将工程构建在容器之上,利用 mesos 或者 kubernetes 等来管理工程运行,可以更好地做到集群监控和资源弹性伸缩。
4. 错误监控与问题排查的优化
目前该项目运行过程中会打少量的日志记录,但对于各个模块中可能存在的错误信息并未捕获并记录下来,对于问题的发现和排查不是很友好。虽然 Erlang 很稳定,但是偶尔出一些问题是在所难免的,这一块的优化也是未来可行的方向之一。
5. 数据同步的优化
目前是由 Master 节点的 data sync 模块直接监控 MySQL(媒资库),从中将数据同步到 Mnesia 集群的。这一块是可以直接采用消息队列(如 RabbitMQ)解耦的,data sync 只需要监控消息队列中某个 topic 是否有新节目进来,有新的话就同步到 Mnesia 中,这比直接监控 MySQL 高效得多。
后面作者会考虑基于上面几点优化思路对该项目进行优化,同时对代码结构进行调整,让代码看起来更加优雅,如果有机会的话是希望可以将该项目开源的。
八、个人收获与感悟
到此为止,关于利用 Erlang 语言开发分布式视频相似推荐系统的介绍就讲完了。在最后作者简单说下自己做这个项目后的收获和感悟。
作者在 2015 年花了近半年时间,算是一边学习 Erlang 语言(在这之前看过 Erlang,但是看得不太懂)一边开发了该项目。该项目一共 5000 行左右代码,虽然不是很多,但是对于像 Erlang 这类简洁的语言来说,也不算少(如果用 Java 实现,估计要几万行,还很难实现分布式计算)。在整个开发过程中,最大的收获有如下 3 点:
新学习了一门比较有意思的函数式编程语言,对 Erlang 的特性有了比较深入的了解;
对于分布式计算有了更深刻的认识,这个项目相当于独立实现了一个小型的分布式计算引擎,对于深刻认识 Spark、Hadoop 的原理是非常有帮助的;
独立完成了一个较大的工程,并且实现了一个基于内容的相似视频推荐系统。
做完该项目对个人来说确实是非常有帮助的。但是从整个团队来说,这样做未必是好事,利用一个很小众的语言来开发一整套系统,为以后埋下了很大的隐患,如果人员离职,很难招聘到 Erlang 的开发人员,新人很难独立维护这套系统,风险极大。作为团队管理者,应该避免这种情况发生,最好还是利用主流的技术栈,避免留下无穷后患。
当时作者管理经验还不足、对技术的理解还不够深入,所以自告奋勇的亲自开发了该系统。经过这几年的积累和成长,对技术和管理有了更深的感悟。如果作为一名技术管理者,让我再一次选择,我可能不会用 Erlang 来开发该系统,而会采用 Spark 流式计算引擎来开发。
参考文献
Programming Erlang: Software for a Concurrent World (Second Edition) Joe Armstrong
评论