在工业级推荐系统中,训练和实现推荐模型的工作量往往连一半都没有,大量的工作都发生在搭建并维护推荐服务器、模型服务模型模块,以及特征和模型参数、数据库等线上服务部分。
工业级推荐服务器具有以下功能:
宏观来讲,高并发推荐服务的整体架构主要由三个重要机制支撑,它们分别是负载均衡、缓存、推荐服务降级机制。
负载均衡是整个推荐服务能够实现高可用、可扩展的基础。当推荐服务支持的业务量达到一定规模的时候,单独依靠一台服务器是不可行的,无论这台服务器的性能有多强大,都不可能独立支撑起高 QPS(Queries Per Second,每秒查询次数)的需求。这时候,我们就需要增加服务器来分担独立节点的压力。既然有多个劳动力在干活,那我们还需要一个“工头”来分配任务,以达到按能力分配和高效率分配的目的,这个“工头”就是所谓的“负载均衡服务器”。
下图就很好地展示了负载均衡的原理。我们可以看到,负载均衡服务器(Load Balancer)处在一个非常重要的位置。因此在实际工程中,负载均衡服务器也经常采用非常高效的 nginx 技术选型,甚至采用专门的硬件级负载均衡设备作为解决方案。
对于高并发带来的负载压力,除“增加劳动力”(负载均衡)外,可从“减少劳动量”角度解决。推荐过程复杂、候选物品规模大时计算资源消耗大,可减少“硬算”次数为服务器减负,如缓存同一用户的推荐结果,预先缓存新用户推荐列表,合理缓存策略能阻挡超 90% 推荐请求。但服务集群和缓存方案也可能面临流量洪峰或软硬件故障,为防推荐服务熔断及“雪崩效应”,需采用“服务降级”机制,即抛弃复杂逻辑,用简单、不耗资源的降级服务应对,如采用基于规则的推荐方法或准备默认推荐列表。
“负载均衡”提升服务能力,“缓存”降低服务压力,“服务降级”机制保证故障时刻的服务不崩溃,压力不传导
以Java嵌入式服务器Jetty为例:
public class RecSysServer {
//主函数,创建推荐服务器并运行
public static void main(String[] args) throws Exception {
new RecSysServer().run();
}
//推荐服务器的默认服务端口6010
private static final int DEFAULT_PORT = 6010;
//运行推荐服务器的函数
public void run() throws Exception{
int port = DEFAULT_PORT;
//绑定IP地址和端口,0.0.0.0代表本地运行
InetSocketAddress inetAddress = new InetSocketAddress("0.0.0.0", port);
//创建Jetty服务器
Server server = new Server(inetAddress);
//创建Jetty服务器的环境handler
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
context.setWelcomeFiles(new String[] { "index.html" });
//添加API,getMovie,获取电影相关数据
context.addServlet(new ServletHolder(new MovieService()), "/getmovie");
//添加API,getuser,获取用户相关数据
context.addServlet(new ServletHolder(new UserService()), "/getuser");
//添加API,getsimilarmovie,获取相似电影推荐
context.addServlet(new ServletHolder(new SimilarMovieService()), "/getsimilarmovie");
//添加API,getrecommendation,获取各类电影推荐
context.addServlet(new ServletHolder(new RecommendationService()), "/getrecommendation");
//设置Jetty的环境handler
server.setHandler(context);
//启动Jetty服务器
server.start();
server.join();
}
创建 Jetty 服务的过程非常简单直观,十几行代码就可以搭建起一套推荐服务。
以Netflix的推荐系统架构为例。Netflix 采用了非常经典的 Offline、Nearline、Online 三层推荐系统架构。架构图中最核心的位置就是图中用红框标出的部分,它们是三个数据库 Cassandra、MySQL 和 EVcache,这三个数据库就是 Netflix 解决特征和模型参数存储问题的钥匙。
没有独立数据库能经济高效地完成存储量大、查询快且应对高 QPS 压力的复杂任务,因此工业级推荐系统普遍采用分级存储,将频繁访问的数据放快速数据库或缓存,海量全量数据放便宜但查询慢的数据库。
对于 MySQL 而言,MySQL 是强一致性的关系型数据库,一般存储关键的、要求强一致性的信息,如物品推荐控制信息、物品分类层级关系、用户注册信息等。这类信息由推荐服务器阶段性拉取,或利用分级缓存阶段性更新,避免频繁访问压垮 MySQL。
分级存储,把越频繁访问的数据放到越快的数据库甚至缓存中,把海量的全量数据放到廉价但是查询速度较慢的数据库中
用户特征总数较大,难以全部载入服务器内存,因此将用户特征载入到 Redis 等内存数据库是合理的选择。
除了 Redis,我们还提到了多种不同的缓存和数据库,如 Cassandra、EVcache、GuavaCache 等等,它们都是业界非常流行的存储特征的工具。
Redis是当今业界最主流的内存数据库。
在Redis中,所有数据都以Key-value的形式存储;同时,所有数据都存储在内存中,磁盘只在持久化备份或恢复数据时起作用。
Redis的特点决定了其特性:QPS峰值可以很高,但数据易丢失。
“召回层”处于推荐系统的线上服务模块之中,推荐服务器从数据库或内存中拿到所有候选物品集合后,会依次经过召回层、排序层、再排序层(也被称为补充算法层),才能够产生用户最终看到的推荐列表。
召回层就是要快速、准确地过滤出相关物品,缩小候选集,排序层则要以提升推荐效果为目标,作出精准的推荐列表排序。再详细一点说,可以从候选集规模、模型复杂程度、特征数量、处理速度、排序精度等几个角度来对比召回层和排序层的特点:
计算速度和召回率是召回层设计中相互矛盾的指标:
单策略召回指的是,通过制定一条规则或者利用一个简单模型来快速地召回可能的相关物品。
单召回策略简单且直观,但难以命中用户的潜在需求。
“多路召回策略”,就是指采用不同的策略、特征或简单模型,分别召回一部分候选集,然后把候选集混合在一起供后续排序模型使用的策略。
各简单策略保证候选集的快速召回,从不同角度设计的策略又能保证召回率接近理想的状态,不至于损害排序效果。所以,多路召回策略是在计算速度和召回率之间进行权衡的结果。
在实现的过程中,为了进一步优化召回效率,还可以通过多线程并行、建立标签 / 特征索引、建立常用召回集缓存等方法来进一步完善它。
多路召回策略也存在一些缺点:
利用物品和用户 Embedding 相似性来构建召回层,是深度学习推荐系统中非常经典的技术方案。其优势包括三方面:
把模型部署在线上环境,并实时进行模型推断(Inference)的过程就是模型服务。
由于各个公司技术栈的特殊性,采用不同的机器学习平台,模型服务的方法会截然不同,不仅如此,使用不同的模型结构和模型存储方式,也会让模型服务的方法产生区别。总的来说,业界主流的模型服务方法有 4 种,分别是预存推荐结果或 Embedding 结果、预训练 Embedding+ 轻量级线上模型、PMML 模型以及 TensorFlow Serving。
对于推荐系统线上服务来说,最简单直接的模型服务方法就是在离线环境下生成对每个用户的推荐结果,然后将结果预存到以 Redis 为代表的线上数据库中。这样,我们在线上环境直接取出预存数据推荐给用户即可。
由于这些优缺点的存在,这种直接存储推荐结果的方式往往只适用于用户规模较小,或者一些冷启动、热门榜单等特殊的应用场景中。
在用户规模较大的场景下,可通过存储 Embedding 替代直接存储推荐结果以减少模型存储空间。具体做法是先离线训练好 Embedding,线上通过相似度运算得出最终推荐结果,像用 Item2vec、Graph Embedding 生成物品 Embedding 存入 Redis 供线上使用,就是预存 Embedding 的模型服务方法的典型应用,此方法因线上推断简单快速,常被业界采用。
不过它也有局限性,完全基于线下计算 Embedding,无法引入线上场景特征,也不能进行复杂模型结构的线上推断,表达能力受限。所以对于复杂模型,需从模型实时线上推断角度改进模型服务方法。
预训练 Embedding+ 轻量级线上模型的模型服务方式指用复杂深度学习网络离线训练生成 Embedding,存入内存数据库,再在线上实现逻辑回归或浅层神经网络等轻量级模型来拟合优化目标。
以阿里的推荐模型 MIMN(Multi-channel user Interest Memory Network,多通道用户兴趣记忆网络)为例,其结构图如下。
左边粉色的部分是复杂模型部分,右边灰色的部分是简单模型部分。左边的部分不管多复杂,它们其实是在线下训练生成的,而右边的部分是一个经典的多层神经网络,它才是真正在线上服务的部分。
图中被虚线框框住的 S(1)-S(m) 和 M(1)-M(m) 这两个数据结构,即离线生成的 Embedding 向量,在 MIMN 模型中是“多通道用户兴趣向量”,它们是连接离线模型和线上模型的接口。线上部分从 Redis 等模型数据库获取这些离线生成的 Embedding 向量,与其他特征的 Embedding 向量组合后输入标准多层神经网络进行预估,这是“预训练 Embedding + 轻量级线上模型”的服务方式。其好处是隔离了离线模型复杂性和线上推断效率要求,离线可使用复杂结构构建模型,只要输出 Embedding 就能供线上推断。
“Embedding + 轻量级模型”方法实用高效,但将模型割裂,并非“End2End(端到端)训练 + End2End 部署”的理想方式。存在一种离线训练完模型后可直接部署的方式,即脱离于平台的通用模型部署方式 PMML。PMML 全称为“预测模型标记语言”(Predictive Model Markup Language),是以 XML 形式表示不同模型结构参数的通用标记语言,在模型上线时,常作为中间媒介连接离线训练平台和线上预测平台。
例子使用 JPMML 作为序列化和解析 PMML 文件的库。JPMML 项目包含 Spark 和 Java Server 两部分,Spark 部分的库可将 Spark MLlib 模型序列化生成 PMML 文件,并保存到线上服务器可访问的数据库或文件系统;Java Server 部分负责解析 PMML 模型,生成预估模型并与业务逻辑整合。Java Server 部分仅进行推断,不涉及模型训练、分布式部署等问题,库较为轻量,能高效完成推断。类似的开源项目 MLeap 也采用 PMML 作为模型转换和上线的媒介,并且 JPMML 和 MLeap 都具备对 Scikit - learn、TensorFlow 等简单模型进行转换和上线的能力。
虽然 PMML 是 End2End 训练 + End2End 部署的理想方式,但 PMML 对复杂结构的深度学习模型表示能力有限,对于深度学习推荐模型而言,可以选用TensorFlow。
上线 TensorFlow 模型需借助 TensorFlow 的原生模型服务模块 TensorFlow Serving。整体上,TensorFlow Serving 和 PMML 类工具工作流程一致,都有模型存储、载入还原和提供服务的过程。
具体而言,TensorFlow 离线将模型序列化存于文件系统,TensorFlow Serving 把模型文件载入服务器并还原推断过程,通过 HTTP 或 gRPC 接口提供服务。在 Sparrow Recsys 项目中,离线用 TensorFlow 的 Keras 接口构建和训练模型,利用 TensorFlow Serving 载入模型,以 Docker 为服务容器,Jetty 推荐服务器向 TensorFlow Serving 发 HTTP 请求获取推断结果,用于完成推荐排序。