作者丨gongyouliu
来源 | 转载自大数据与人工智能(ID:ai-big-data)
【导语】:作者在上一篇文章《基于内容的推荐算法》中介绍了基于内容的推荐算法的实现原理。在本篇文章中作者会介绍一个具体的基于内容的推荐算法的实现案例。该案例是作者在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)、(2)是剔除掉无效的
值,(3)是给出的当
在0到2020年之间的一个计算公式,
值越大,最终的相似度越大。这里相似度与
无关(所以严格来说,不叫做相似度,而是贡献度,下面类似,不再说明,直接用贡献度来描述)在其他条件相同的情况下,越是近期拍摄的视频权重越大。
标题
假设两个视频
,
和
分别是这两个视频的标题,首先我们可以通过分词或者关键词提取算法将
和
提取关键词得到对应的关键词,假设如下:
其中
分别是
提取关键词后的集合,那么我们可以用如下Jacard相似度来计算
之间的相似度:
这里涉及到很多NLP方面的技术和领域知识。首先我们需要有视频行业相关语料,才能够保证分词准确,另外标题可能是杂乱无章的,比如很多电影标题中包含粤语版、国语版、新传、第三季等对相似度价值不大的词或者词组,这些都需要借助规则或者NLP技术做预处理,才能得到较好的关键词提取效果,最终才会有较好的相似度计算效果。
标签
对于标签,可以采用跟上面标题一样的计算方法,因为标题提取关键词后的每个关键词可以看成是一个标签。这里不再细说怎么计算了。
地域
假设两个视频
,
和
分别是这两个视频的出品地,我们可以用如下公式来计算这两个视频在地域维度的相似度。
这个公式可以考虑得更复杂一点,将地区按照语言、地域等分成几类,比如北美、东欧、东南亚等,当两个视频的地域完全一样时相似度为1,当两个视频在同一个地区分类内,相似度为0.5(或者其他大于0小于1的值,根据业务经验开发人员自己定义),否则为0,这样会更加精确合理。
豆瓣评分
假设两个视频
,
和
分别是这两个视频的豆瓣评分(豆瓣评分是0到10之间),下面公式给出视频
在豆瓣评分这个维度的贡献度,评分越高,贡献度越大。
是否获奖
假设两个视频
,
和
分别是这两个视频所获的奖项,那么可以简单用下面公式来计算视频
在获奖这个维度上的贡献,当然计算公式可以更加复杂,对不同奖项区别对待,级别更高的奖项可以给出更高的贡献度。
导演与演员
通常情况下,视频一般只有一个导演,导演的相似度可以类似地域一样的方法计算。而演职员可以采用跟标签类似的方法计算相似度。
上面给出了几个内容维度怎么计算两个视频在该维度的相似度(或者贡献度),我们在实际项目中用到的维度比这个更多,但是计算原理类似,这里不再一一列举。另外,具体的计算和处理逻辑也会更复杂,比如对于标签相似度,标签之间是有相似关系的,比如惊悚和恐怖,它们是相似的。上面方法没有考虑到标签之间的这种相似关系而是将他们看成不同的标签,计算相似性多少有点简单。
实际上,我们项目中用到了数学中等价类的思想,将很相似的标签看成是同一类的,在同一类的标签之间是有相似度的,这样如果一个电影的标签是恐怖,另外一个电影的标签是惊悚,按照上面的计算方法相似度是0,而按照我们等价类的思路,相似度是大于0的。同时,对于某些类别的视频我们加了很多规则性的东西,更好地适应不同类别内容的相似计算。像这类提升效果的处理还有很多,这里不再细说。
问题与难点
该项目最早(2012年底)是采用Java来开发的,写一个单机程序,当时视频量还比较少,也没有这么多视频类别,基本可以支撑,当后面加入越来越多的视频类别,每类视频数量也越来越多时,单机计算的性能就出现瓶颈了。
当时采用的应对方案是将视频按照类别分成几组,每一组采用一个Java线程计算,虽然某种程度上可以做到并行计算,但是每个视频类型的数量及增长速度是不一样的,人工按照类型拆开分布不够均匀,问题比较多。
回顾老的设计面临的问题,我们来总结一下,基于该算法的相似视频推荐主要难点有如下几个:
数据量大,增速快
前面讲到我们长短视频加起来大概有12个类别,类别多,总量有几百万条视频。对于短视频来说,特别是资讯、体育等,每天都有大量(万级)新增的节目。
第一次全量计算所有节目的相似度时,由于需要计算的节目太多,必须采用分布式计算,否则计算太慢,单机可能要花几个月时间才能完全算完。
需要实时计算
在我们的APP上,短视频一般按照时间排列,新的短视频放在最前面,用户更容易看到。所以对于新增的视频节目,我们需要实时计算出相似的视频,否则只能用默认推荐顶替相似推荐,效果肯定不会太好。
计算与某个视频最相似的视频需要遍历所有视频
我们一般只关联推荐同一大类的视频,但是各个类别数量是极不均衡的,电影1-2万部,资讯大概有上百万。在我们为某个资讯计算与它最相似的N个资讯时,我们需要遍历所有其他的资讯才能找到与它最相似的N个,怎么设计这个遍历过程对计算时间有很大影响。
更新已经计算的视频的相似度
对于新入库的视频A,我们需要计算它的关联推荐,同时,对于某个已经计算好的视频B,如果新加入的视频A与B的相似度高于B原来计算好的topN相似的某个视频的相似度,这时需要更新B的相似度列表,将A添加进去,同时删除原来B的相似列表中相似度最低的视频。每个新视频的加入都可能影响很多已经计算过相似度的视频,怎么在短时间内快捷地查找出这类需要更新相似度列表的视频也是一个挑战。
上面我们对相似视频推荐的算法原理及难点做了比较细致的讲解。为了实现这些算法和克服难点,我们基于Erlang语言完美地解决了这些问题,在讲怎么利用Erlang语言来从工程上实现上面的算法之前,我们先简单对Erlang语言做一个初略的介绍,方便读者更好地理解后续算法的架构和工程实现原理。
为什么要用Erlang语言开发
Erlang语言简介
Erlang是一种通用的面向并发的编程语言,它由瑞典电信设备制造商爱立信所辖的CS-Lab开发,目的是创造一种可以应对大规模并发事件的编程语言和运行环境。Erlang问世于1987年,经过十年的发展,于1998年发布开源版本。Erlang是运行于虚拟机的解释性语言。在编程范型上,Erlang属于多重范型编程语言,涵盖函数式、并发式及分布式。
Erlang是一个结构化、动态类型编程语言,内建并行计算支持。最初是由爱立信专门为通信应用设计的,比如控制交换机或者变换协议等,因此非常适合于构建分布式、实时并行计算系统。使用Erlang编写出的应用程序运行时通常由成千上万个轻量级进程组成,并通过消息传递相互通讯。进程间上下文切换对于Erlang来说非常简单,比起C程序的线程切换要高效得多。
Erlang语言的特性
Erlang语言虽然开发于上世纪80年代,但是很多思想是非常超前的,在当前云计算时代具有非常实用的价值,算是在多如牛毛的编程语言大军中独树一帜。这些特性也是我选择利用Erlang语言开发相似视频推荐系统的主要原因之一。
下面我们列举一些Erlang语言的主要特性:
(1) 函数式编程及部分语法特性
Erlang是一个函数式编程语言,即可以将函数作为参数传入别的的函数,并且可以作为函数的返回值。
Erlang语法也比较特殊,通过递归来实现迭代逻辑,没有其他语言的while和for循环结构。Erlang的变量跟数学中类似,只能单次赋值,不可重复赋不同值。Erlang的模式匹配能力也非常强大。Erlang内置了很多数据类型及操作函数,辅助更好地进行函数式编程。
(2) 并发模型
Erlang是一个高并发语言,天生支持高并发,Erlang基于Actor的并发编程模型,进程间通信通过消息传递进行,高效自然可靠。Erlang语言将并发模式作为自己的核心特性,非常方便构建分布式处理逻辑,从一开始设计之初就充分利用多核处理器性能,非常适合在现代服务器上构建分布式应用。
(3) 跨平台
Erlang语言与java类似,采用虚拟机来解释执行代码,Erlang的beam虚拟机负责对代码进行解释执行,因此具备跨平台特性,一次编译到处运行。
(3) 错误处理
Erlang是一个高容错的编程框架,它对错误处理有两个设计哲学:让另外一个程序来解决错误,如果出错就让程序崩溃并重新启动。第一个设计哲学将错误”外包“给另外一个专门的程序监控和处理,这样原来的程序将核心放到处理逻辑上,这个监控程序可以放在另外一台机器上,如果原来程序所在机器挂了,监控程序也可以发现问题。
基于第二个设计哲学,既然处理逻辑和处理错误的程序分离了,如果处理逻辑的程序挂了(一般也是遇到偶发的情况或者传入非法参数等原因挂掉),处理错误的程序就可以让它重新启动,这样系统又可以正常运行了。这个哲学跟我们熟知的重启可以解决90%以上问题不谋而合。
(4) 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的各种行为机制,这样只需要实现核心的接口,进程间的调用、监控这些能力行为机制很容易帮我们做到。
(5) 内嵌的Mnesia数据库
Mnesia是内嵌入Erlang的一款容错的、分布式可拓展的交易型数据库,数据按照表来组织,类似于关系型数据库,数据可以选择存在内存或者磁盘中,并且有一套自己的非常方便的查询语言,可以对数据进行方便快捷的读写查询等操作。
为什么选择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个模块的核心功能和实现细节。
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的相似推荐列表了。
视频id | 推荐列表最小的相似度 |
---|---|
id_1 | s_1 |
id_2 | s_2 |
...... | ...... |
id_k | s_k |
实际上可以做很多简化,比如我们可以先求出上表中第二列的最小值
(见下面公式),我们只保留A与其他视频相似度大于
的视频,其他视频直接丢弃(其实很多视频可以丢弃,毕竟很多视频跟A是没有任何相似度的),而不会影响更新计算逻辑,这些相似度大于
的视频才是可能需要更新推荐列表的视频。
Master节点除了上面5个核心模块外,还维护两个关键的数据结构:
A :living_nodes:记录集群目前可用的Slaver节点,如有有节点加入或者挂掉会更新该数据。
B:need_computing_id: 记录哪些视频还没有计算相关视频推荐列表,针对这些视频在任务分配模块中分派给Slaver节点计算,分配出去后将该视频从待计算列表中删除,避免重复计算。如果有新视频加入,新视频的id会写入该列表。
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(或者很小),我们就没必要计算它的其他维度的相似度了,直接将该视频丢弃计算下一个。这些优化点还很多,这里不一一描述。
Riak集群:负责最终相似推荐结果的存储
Riak是基于Erlang语言开发的一个分布式的key-value存储系统,可以非常容易地水平扩展,非常适合大规模的数据存储,是整个相似视频推荐系统的最终计算结果的存储模块,所有视频的相似推荐列表都存在Riak集群中。Slaver的worker计算完一个视频的相似度后会直接插入Riak。
相应请求模块:基于客户端用户请求,给出推荐结果
当用户在客户端访问某个视频的详情页时,客户端向服务端发送请求,请求响应模块根据用户请求从Riak集群中将该节目的相似列表取出来,并将其他需要的信息(如标题、演职员、海报图等在前端展示需要用到的节目metadata信息,这些信息我们存在Redis集群中)填充完整后返回给用户。
请求响应模块是基于Cowboy (一款基于Erlang开发的高性能轻量级接口服务器)来开发的。从前面的介绍中可以知道,Cowboy除了从Riak中获取推荐列表外,还需要从Redis中获取节目的metadata信息做填充。
核心亮点
到此为止,我们基本讲完了相似视频推荐的核心算法原理与基于Erlang实现的工程架构,该系统是作者在15年开发的,一直在作者公司的两个产品中使用到现在,其中一个产品目前还是用的该算法(另外一个产品基于Spark平台做了重构,整体效果更好),该算法在服务公司业务的4年中,虽然视频种类和数量有了非常大的增长,但是系统一直比较稳定,也能够很好地应对视频量的增长,并且效果还不错,这得益于Erlang良好的容错机制及该系统较好的分布式扩展能力。
在这里我们回顾一下该系统的亮点,也让我们可以更好地理解它的价值。
分布式可拓展能力
该系统采用Master/Slaver架构,可以通过水平地增加服务器来拓展该系统的计算能力,同时当全量计算完后,后面就是增量计算了,增量计算相对没有那么大的计算量,不需要这么多计算资源,我们可以缩减部分服务器节省开支。
可以实时对新增加的视频做计算
Master的Data sync模块近实时监控媒资库MySQL,如果有新视频加入,马上将该视频同步到Mnesia中,并分派给Slaver进行计算,在分钟级内新视频就可以完成计算,这样基本可以有效避免新入库视频的相似推荐冷启动问题。
系统很稳定
该系统一般很少出问题,这得益于Erlang的OTP框架,我们的Master和Slaver服务都是基于OTP框架来实现的,每个进程都有一个supervisor进程,当进程挂掉后,supervisor会重启该进程,避免了因为偶尔的故障或者异常数据导致的系统崩溃,最终让我们的系统非常稳定。
分将计算过程解耦合,抽象为最大堆来维护topN最相似的视频列表
将计算相似性计算抽象为一个计算模块,每个worker通过维护一个最大堆,可以非常有效地解决最相似的topN计算问题。同时,针对新闻、体育等时效性强的视频类型,我们还可以只取最近一段时间内的视频来计算相似度,进一步减少计算量。
充分利用多核能力
每个Slaver可以启动多个worker进行计算,充分利用现代服务器多核的能力,大大加速了计算过程。
除了上述优点外,我们通过配置文件来定义各种参数(比如一个Slaver可以启动多少个worker),可以方便地对参数进行调整,系统启动时会首先解析这些参数。我们也可以一键启动Master节点和所有的Slaver节点,整个配置和启动过程跟Spark比较类似。该系统可以看成基于Erlang语言开发的具备特定功能的类Spark的小型分布式计算平台。
未来优化方向
虽然该系统有很多优点,但由于个人能力及精力有限,并且对Erlang的了解还没有达到炉火纯青的阶段,该系统还有非常多的地方是可以做优化的。下面对可能的优化点进行说明,作为后面努力的方向。
算法本身的优化
该系统基于视频内容的简单向量空间模型来计算相似度,虽然算法原理简单,但是由于视频的metadata比较杂乱,相似性效果受到数据质量好坏的严重影响。同时,向量空间模型是一个比较简单的模型,无法获得更复杂的特征表示。可行的优化点是,我们可以基于metadata数据或者用户行为数据做嵌入,为每个视频构建一个稠密的特征向量表示,该系统可以通过稠密向量的相似度来计算视频的相似性。其他各类计算视频相似度的方法都是可以使用的。
目前我们的计算相似度算法和整个系统还是耦合比较紧密的,通过优化是可以将计算相似性做成可插拔的组件的,这样就可以方便更换计算引擎。
另外,我们的向量空间模型各个维度的权重是根据人工经验自定义的,比较主观,其实是可以
利用用户点击反馈机制自动化学习最优参数的,这样可能效果会更好。
调度策略的优化
当前该框架的调度策略还非常粗暴,对于每个需要计算相似推荐的视频,直接从所有Slaver中先过滤出有空余资源的worker,将任务分配给第一个空闲的worker。针对每个视频都要从头过滤一遍,效率很低。
更好的方式是每个Slaver节点维护一个待计算相似度的固定长度的视频队列,当队列中待计算的视频都计算完了相似度,Slaver主动向Master申请待计算的视频。这样将主动权放到Slaver上,减少原来分配方案中毫无意义的轮询,同时也减轻了Master节点的压力。
部署方式的优化
目前的系统虽然部署非常容易,只要在每台服务器上安装Erlang,将该项目编译好,将编译后的工程代码分发到每台服务器上统一的目录下,修改每台服务器上的配置文件(实际上所有Slaver上配置是一样的,跟Master上略有不同)就可以启动了。
更好的方式可以利用docker将工程构建在容器之上,利用mesos或者kubernetes等来管理工程运行,可以更好地做到集群监控和资源弹性伸缩。
错误监控与问题排查的优化
目前该项目运行过程中会打少量的日志记录,但对于各个模块中可能存在的错误信息并未捕获并记录下来,对于问题的发现和排查不是很友好。虽然Erlang很稳定,但是偶尔出一些问题是在所难免的,这一块的优化也是未来可行的方向之一。
数据同步的优化
目前是由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流式计算引擎来开发。
参考文献:
1.http://highscalability.com/blog/2014/3/31/how-whatsapp-grew-to-nearly-500-million-users-11000-cores-an.html
2.Programming Erlang: Software for a Concurrent World (Second Edition) Joe Armstrong
(*本文为 AI科技大本营转载文章,转载请联系原作者)