SparrowRecys——线上服务
一、线上服务主要内容
- 把候选物品和离线处理好的特征载入到服务器
- 将离线模型上线
- 在线进行模型服务(model serving)
如何做到负载均衡、缓存、推荐服务降级机制:
二、本项目选择服务器——Jetty服务器
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();
}
三、存储模块redis
前言:类似Embedding特征是在离线环境下生成的,而推荐服务器是在线上环境中运行的。那离线特征数据是如何导入到线上让推荐服务器使用?
需要一个中介redis将embedding存入,线上模型推荐再将redis里的embedding取出。
- 分级存储:把越频繁访问的数据放到越快的数据库甚至缓存里,把海量的全量数据放到廉价但是查询速度较慢的数据库中。
SparrowRecsys采取的分级存储策略: - 用户特征总数较大,难以全部存入服务器内存中,将其存入redis里。
- 物品特征总数较小,将其存入服务器内存中。
- HDFS(单机环境下以本机文件系统为例)存储每次处理的全量特征和训练得到的embedding。。
Redis基础知识: - Redis所有数据以key-value形式存储,key只能是字符串,value支持结构有string(字符串)、list(链表)、set(集合)、zset(有序集合) 和 hash(哈希)。
- Redis的QPS峰值很高,还有数据易丢失,不应该把关键业务数据存储在Redis中。
- Redis基本操作:set、get、keys,value的数据类型用到了string.
将Embedding向量存入redis中:
if (saveToRedis) {
//创建redis client
val redisClient = new Jedis(redisEndpoint, redisPort)
val params = SetParams.setParams()
//设置ttl为24小时
params.ex(60 * 60 * 24)
//遍历存储embedding向量
for (movieId <- model.getVectors.keys) {
//key的形式为前缀+movieId,例如i2vEmb:361
//value的形式是由Embedding向量生成的字符串,例如 "0.1693846 0.2964318 -0.13044095 0.37574086 0.55175656 0.03217995 1.327348 -0.81346786 0.45146862 0.49406642"
redisClient.set(redisKeyPrefix + ":" + movieId, model.getVectors(movieId).mkString(" "), params)
}
//关闭客户端连接
redisClient.close()
}
将embedding向量从redis取出:在服务器端,希望将服务器把所有物品Embedding向量阶段性缓存在服务器内部,用户embedding进行实时查询。
过程:先用keys把所有物品embedding向量前缀键找出,依次将embedding向量载入内存。
//创建redis client
Jedis redisClient = new Jedis(REDIS_END_POINT, REDIS_PORT);
//查询出所有以embKey为前缀的数据
Set<String> movieEmbKeys = redisClient.keys(embKey + "*");
int validEmbCount = 0;
//遍历查出的key
for (String movieEmbKey : movieEmbKeys){
String movieId = movieEmbKey.split(":")[1];
Movie m = getMovieById(Integer.parseInt(movieId));
if (null == m) {
continue;
}
//用redisClient的get方法查询出key对应的value,再set到内存中的movie结构中
m.setEmb(parseEmbStr(redisClient.get(movieEmbKey)));
validEmbCount++;
}
redisClient.close();
具体到为用户推荐过程中,利用接口查出用户embedding,与内存中embedding进行相似度计算,得到最终的推荐列表。
本项目存储embedding使用方式为分布式文件系统+Redis+服务器内存。
value除了设置为string格式还能有其他存储结构存储embedding向量数据?
- redis keys命令不能用在生产环境中,如果数量过大效率十分低,导致redis长时间堵塞在keys上。
- Redis value 可以用pb格式存储, 存储上节省空间. 解析起来相比string, cpu的效率也应该会更高
四、召回层
推荐物品规模庞大时,如何快速又准确筛选掉不相关物品,从而节约排序时所消耗的资源。
召回层策略有:
- 单策略召回:制定一条规则或一个简单模型快速召回可能相关物品
- 多路召回:采用不同的策略、特征或简单模型,分别召回一部分候选集,然后把候选集混合在一起供后序排序模型使用策略。
- 基于embedding召回:
public static List<Movie> retrievalCandidatesByEmbedding(User user){
if (null == user){
return null;
}
//获取用户embedding向量
double[] userEmbedding = DataManager.getInstance().getUserEmbedding(user.getUserId(), "item2vec");
if (null == userEmbedding){
return null;
}
//获取所有影片候选集(这里取评分排名前10000的影片作为全部候选集)
List<Movie> allCandidates = DataManager.getInstance().getMovies(10000, "rating");
HashMap<Movie,Double> movieScoreMap = new HashMap<>();
//逐一获取电影embedding,并计算与用户embedding的相似度
for (Movie candidate : allCandidates){
double[] itemEmbedding = DataManager.getInstance().getItemEmbedding(candidate.getMovieId(), "item2vec");
double similarity = calculateEmbeddingSimilarity(userEmbedding, itemEmbedding);
movieScoreMap.put(candidate, similarity);
}
List<Map.Entry<Movie,Double>> movieScoreList = new ArrayList<>(movieScoreMap.entrySet());
//按照用户-电影embedding相似度进行候选电影集排序
movieScoreList.sort(Map.Entry.comparingByValue());
//生成并返回最终的候选集
List<Movie> candidates = new ArrayList<>();
for (Map.Entry<Movie,Double> movieScoreEntry : movieScoreList){
candidates.add(movieScoreEntry.getKey());
}
return candidates.subList(0, Math.min(candidates.size(), size));
}
embedding召回过程:
- 获取用户embedding
- 获取影片候选集:选取10000部热门电影作为候选集,并获取物品(电影)embedding,计算用户embedding和物品embedding之间相似度。
- 根据相似度排序,返回规定大小的候选集。
其中第二步是最耗时的,有办法可以解决吗?
使用局部敏感哈希搜索embedding最近邻:embedding向量
五、离线模型部署到线上
方法主要有:
- 预存推荐结果和embedding结果
- 预训练embedding+轻量级线上模型:
- PMML模型
- Tensorflow Serving
1.预存推荐结果和embedding结果
预存推荐结果:在离线环境下生成对每个用户的推荐结果,再将结果预存到Redis中,再线上环境取出预存数据直接推荐给用户。
优点:线下平台和线上平台完全解耦,线上服务过程没有复杂计算,推荐线上延迟极低
缺点:存储用户和物品的组合推荐结果,用户数量、物品数量规模过大发生组合爆炸,线上数据库无力支撑。
这种推荐方式适合冷启动和热门榜单。
预存embedding结果:
离线训练好embedding,在线上通过相似度运算得到最终推荐结果。本项目通过Item2vec生成物品embedding,再存入Redis中,这就是预存embedding结果的应用。
然而预存embedding结果是在线下计算embedding,这样的方式缺少线上场景特征的引入,表达能力受限。
2.预训练embedding+轻量级线上模型
用深度网络离线训练embedding存入内存数据库,在线上实现逻辑回归或浅层神经网络轻量级模型拟合优化目标。
线上部分从Redis拿到离线生成embedding向量,跟其他特征embedding向量组合在一起,扔到标准的多层神经网络进行预估。不是end-end模型。
3.PMML模型
end-end,不能够支持所有复杂模型
4.Tensorflow Serving
离线使用Tensorflow的Keras接口完成模型构建和训练,再利用 TensorFlow Serving 载入模型,用 Docker 作为服务容器,然后在 Jetty 推荐服务器中发出 HTTP 请求到 TensorFlow Serving,获得模型推断结果,最后推荐服务器利用这一结果完成推荐排序。
基于Docker 的Tensorflow Serving,