157-深度学习推荐系统实战

开篇词｜从0开始搭建一个深度学习推荐系统

你好，我是王喆，在一家硅谷的科技公司 Roku 担任机器学习工程师。

Roku 是美国最大的视频流媒体平台，在美国智能电视市场占比超过 1/3。我作为推荐系统的架构负责人，在最近两年多的时间里，跟团队一起搭建了一整套深度学习推荐系统，相比于传统推荐系统，它在播放时长、点击率等效果指标上取得了 30% 以上的提升。

在这个过程中，我和团队踩过不少“坑”，也总结了很多经验。其中最宝贵的一点是，只有建立起深度学习推荐系统的知识体系，从系统的层面考虑问题，我们才能够实现整体效果上的优化。与此同时，我还发现越来越多的在校生和刚入行的工程师，想要熟悉或者是在深度学习推荐系统领域取得进一步的发展，但经常因为缺少系统的学习机会而困在一两个难点上，停滞不前。

比如说，一些对深度学习推荐系统非常感兴趣的在校生，很希望能有前辈带领他们熟悉业界的热门技术，让他们在进入相关领域前就积攒起足够的工程经验。这种想法是非常好的。我也面试超过两三百位应届生了，以我的经验，最让企业青睐的应届生就是动手能力强、有实习经验、有业界项目经验的。但这种锻炼自己的机会却很难找到。

再比如说，一些已经进入推荐、广告、搜索相关部门，但是时间不长，或者正要转行进入这些行业的工程师。他们想要自主承担起某个模块的开发任务，期待扩展自己的技术视野和格局，向更高级的职位进发。但这需要他们建立一套成体系的知识和实践框架。

我是一个十分赞同开源思想并且乐于分享知识的人。既然有那么多同行都有这样的需求，我为什么不把我建立行业知识体系的过程分享出来呢？所以在 2020 年初，我发布了新书《深度学习推荐系统》，希望这本书能帮助一些同学建立深度学习推荐系统的知识体系。令我没想到的是，这本书在半年之内就重印了 7 次，销量接近 2 万册，豆瓣评分达到了 9.3，这对于一本技术书来说是非常难得的。

新书获得肯定的同时，我也收到了很多读者的反馈，最多的一点是“书中的知识非常体系化，帮助我建立了整个行业的知识蓝图，但如果能有配套的实践项目就更完美了”。

就像 Linux 之父 Linus Torvalds 说的那句话：“Talk is cheap. Show me the code.”，实践对于工程师来说永远是最重要的。所以我就趁热打铁，在极客时间上开设了一门更偏重实战的技术专栏。我想这是一次绝佳的机会，与你一同从“0”开始，搭建一个“工业级”的“深度学习”推荐系统，做到知识和实践两手抓！

深度学习的浪潮，推荐系统的时代

在跟你聊了开设这门课程的初心之后，我觉得很有必要把我们的视野放得更宽一点，看看我们这个时代到底是怎么被推荐系统影响着，推荐系统又是怎么在深度学习的浪潮之中“乘风破浪”的。

毫无疑问，推荐系统从来没有像现在这样影响着我们的生活。想上网购物，天猫、京东的推荐系统会帮你挑选商品；想了解资讯，头条、知乎的推荐系统会为你准备感兴趣的新闻和知识；想消遣放松，抖音、快手的推荐系统会为你奉上让你欲罢不能的短视频。

而驱动这些巨头进行推荐服务的，都是基于深度学习的推荐模型。

2013 年，百度率先在广告系统中应用了深度学习，2015 到 2020 年，阿里提出并应用了从 MLR 到 DIEN 等一系列的深度学习模型。国外的互联网巨头也不逞多让，从最早的 Google 的 Word2vec，到 2015 年 YouTube 的深度学习推荐系统，再到之后的 Facebook、Amazon、微软等等，几乎所有头部公司的成功应用，让深度学习如后浪般席卷了推荐系统业界，将传统的推荐模型彻底取代。

更让人感叹的是，字节跳动在 2020 年 1 月 5 日发布的一份《抖音数据报告》中宣布，抖音日活用户突破了 4 亿。要知道，这距离抖音 2016 年 9 月上线仅过去了 3 年多一点的时间。作为一个几乎完全由推荐系统驱动的应用，这样的增长速度是惊人的，是前所未有的。而字节跳动技术团队曾经披露的，深度学习在推荐算法、视频内容理解、自然语言处理等方向上的应用，则又一次向我们印证了深度学习的强大实力。

深度学习时代，推荐工程师的职业发展方向在哪？

随着推荐系统的快速发展，在深度学习时代，推荐工程师又该如何选择自己的职业发展方向呢？

推荐工程师的工作，本质上是利用一切可能的技术手段来提升推荐系统的效果，从而不断达到甚至超越企业的商业目标。

举个例子，2019 年，阿里著名的千人千面系统驱动了天猫“双 11”2684 亿元的成交额。假设我们通过改进天猫的商品推荐功能，让平台整体的转化率提升 1%，那么在 2684 亿元成交额的基础上，我们就能再增加 26.84 亿元。 也就是说，推荐工程师仅通过优化推荐技术，就创造了 26.84 亿元的价值。这无疑是这个职位最大的魅力所在，也是它能够支撑起百万年薪最重要的原因。

1% 听起来很小，但是想要在一个成熟的推荐系统上，找到能够提升的突破点并不容易，这需要我们动用所有的领域知识储备。一位推荐工程师优秀与否，也就是在这个时候体现出来的。

在所有业界巨头的推荐引擎都由深度学习驱动的今天，作为一名推荐系统从业者，我们不应该止步于，或者说满足于继续使用协同过滤、矩阵分解这类传统方法，而应该加深对深度学习模型的理解，加强对大数据平台的熟悉程度，培养结合业务和模型的技术直觉，提高我们整体的技术格局，这些都是我们取得成功的关键。

可能你最近经常听说“算法工程师的知识更新太快，一不小心就处在被淘汰的边缘”。但我始终坚信一点，人才的分布都是金字塔式的，与其抱怨金字塔底座为什么这么宽，不如努力提高自己的知识储备、工程能力和技术视野，哪怕只是向金字塔的塔尖前进了一步，我们也超越了最宽的那个底座，不是吗。

因此，我希望在这门课里与你一起建立的，是深度学习推荐系统的整体架构。这对我来说也是个不小的挑战，因为它几乎需要我输出所有的工作经验和知识储备。当然，对你来说肯定也是个挑战，因为一定有很多新知识需要补充到自己的知识框架中。但我相信我们的目标一定是一致的，就是在深度学习时代，在推荐系统这个行业开拓视野、站稳脚跟。

这门课是怎么设计的？

一句话来说，这门课程是一门知识与实践并重的课程，通过解决 30+ 个深度学习推荐系统问题，不仅能串联起深度学习推荐系统的知识体系，还能帮你实打实地收获一套经过实践验证过的开源代码，从而让你也能实现一个工业级的深度学习推荐系统。

注意了，这里面有几个关键词，分别是“知识体系”“深度学习”“工业级”“实战”。没错，我们的课程就是围绕这几个关键词展开的。我遵循一个经典推荐系统的框架，把课程分为 6 个部分，分别是“基础架构篇”“特征工程篇”“线上服务篇”“推荐模型篇”“效果评估篇”“前沿拓展篇”，其中的每节课，我们都会着重解决一个技术难点。

基础架构篇：从 0 出发，建立深度学习推荐系统的知识体系

在开始学习这门课之前，我对你的要求有两个，一是有一定的编程基础，二是有基本的机器学习概念知识。在此基础上，我们通过基础架构篇的学习，就能建立起深度学习推荐系统的完整知识架构，做到“心中有蓝图、心中有高楼”。

具体来说，在基础架构篇中，我会详细讲解我们要从 0 开始实现的推荐系统，Sparrow RecSys 的主要功能和技术架构。由于缺少工业级的实验环境，Sparrow RecSys 不可能是一个真正的工业级推荐系统，但是它的每一行代码都是严谨的，其中的每个方法都是经过业界验证的主流方法。并且，我们还会使用到 Spark、Flink、TensorFlow 这些业界目前最流行的机器学习和大数据框架，麻雀虽小，但五脏俱全。

特征工程篇：又快又好，用心准备推荐系统的“食材”

在特征工程篇中，我会和你一起讨论推荐系统会用到的特征，以及主要的特征处理方式，并且把它们都实践在 Spark 上。除此之外，我还会讲解深度学习中非常流行的 Embedding、Graph Embedding 技术。

我们可以把特征工程看作是为推荐系统准备“食材”的过程。所以我希望通过这部分的学习，你不仅能够成为一名合格的“备菜”师傅，更能够利用学到的 Embedding 方法，来实现 Sparrow Recsys 中的相似电影推荐功能，在实践中快速成长起来。

线上服务篇：实践出真知，掌握搭建工业级推荐系统的核心技能

一个工业级推荐系统和实验室 Demo 的最大区别就在于线上服务部分。在这一篇中，我们要实打实地搭建一个推荐服务器，它包括了服务器、存储、缓存、模型服务等相关知识。相信通过这部分的学习，你能初步掌握 Jetty Server、Spark、Redis，这些工程领域的核心技能。

推荐模型篇：深度学习推荐系统上的明珠

如果让我挑出深度学习对传统推荐系统最大的改进，毫无疑问是深度学习在推荐模型上的应用，甚至我们称它为“推荐系统上的明珠”也不为过，所以这一部分可以说是整门课程的重中之重了。我们将一起学习深度学习推荐模型的原理和实现方法，主要包括 Embedding+MLP 、Wide&Deep、PNN 等深度学习模型的架构和 TensorFlow 实现，以及注意力机制、序列模型、增强学习等相关领域的前沿进展。

效果评估篇：建立成体系的推荐系统评估机制

在效果评估篇中，我们要重点学习效果评估的主要方法和指标。但对一个成熟的推荐系统来说，仅熟悉这些是不够的，我还期望你能通过这一篇的学习建立起包括线下评估、线上 AB 测试、评估反馈闭环等整套的评估体系，真正能够用业界的方法而不是实验室的指标来评价一个推荐系统。

前沿拓展篇：融会贯通，追踪业界前沿

在完成整体的知识积累之后，我们在通过这一篇的学习，将通过业界巨头们的深度学习推荐系统方案进行融会贯通。我会重点讲解 YouTube、阿里巴巴、微软、Pinterest 等一线公司的深度学习应用，帮助你追踪业界发展的最新趋势，并且找到自己技术道路上的方向。

所有的点穿成线、连成面，就组成了我们希望掌握的深度学习推荐系统架构。与此同时，我们也会在课程完成后，搭建起一个完整的推荐系统，再收获一份亲眼看见自己学习成果的成就感。

最后我想说，No Magic，不要期望一门课程就能够让你成为业界专家。如果你是完全没有推荐系统基础的新人，这门课程能够让你入门推荐系统，初步掌握深度学习推荐系统各模块的相关知识和业界实践。如果你是行业老兵，这门课能让你查漏补缺，在技术视野和格局上有所提高。但推荐系统的每个模块都有着极深的技术纵深，不管是 TensorFlow 还是 Spark，还是 Redis、Flink，它们中的每一个都需要我们持续性的钻研才能够成为领域专家。在技术专家的道路上，这仅仅是个开始，当然，我也很荣幸能帮你开启这个既有魅力又有挑战性的技术领域。

最后，关于深度学习推荐系统，希望你能在这里畅所欲言，提出你的困惑和疑问。也欢迎多多给我留言，你们的鼓励是我的动力。如果你身边也有想要学习深度学习推荐系统的同学，也别忘了把这个课程分享给他。很高兴能与你一起开启深度学习推荐系统的学习之路，愿与你在攀登行业塔尖的路上共勉。

01｜深度学习推荐系统的经典技术架构长啥样？

你好，我是王喆。从今天开始，我们正式开始学习“深度学习推荐系统”了。在开始之前，我想先问你一个问题：当你开始学习一个全新领域的时候，你想做的第一件事情是什么？

当然每个人可能都有自己的答案，但对于我自己来说，我最想搞明白的是两个问题。一个是，这个领域到底要解决什么问题？第二个是，这个领域有没有一个非常高角度的思维导图，让我能够了解这个领域有哪些主要的技术，做到心中有数？

针对“深度学习推荐系统”这个领域啊，可能还会有第三个问题，为什么我们要一直强调“深度学习”，深度学习到底给推荐系统带来了什么革命性的影响？相信听完了这一节课，你心中的这三个问题也都能迎刃而解。

推荐系统要解决的根本问题是什么？

在开篇词中我们提到，推荐系统的应用已经渗透到购物、娱乐、学习等生活的方方面面，虽然商品推荐、视频推荐、新闻推荐这些推荐场景可能完全不同，既然它们都被称为“推荐系统”，解决的本质问题一定是相通的，遵循着共通的逻辑框架。

推荐系统要解决的问题用一句话总结就是，在“信息过载”的情况下，用户如何高效获取感兴趣的信息。

因此，推荐系统正是在“浩如烟海的互联网信息”和“用户的兴趣点”之间，搭建起的一座桥梁。那这座桥是怎么一步步搭建起来的呢？下面，我们先来看看，推荐系统比较抽象的逻辑架构是什么样的，再一步步搭建起它的技术架构，让你对推荐系统有一个整体上的印象。

推荐系统的逻辑架构

从推荐系统的根本问题出发，我们可以清楚地知道，推荐系统要处理的其实是“人”和“信息”之间的关系问题。也就是基于“人”和“信息”，构建出一个找寻感兴趣信息的方法。

这里“信息”的定义非常多样，它在不同场景下的具体含义也千差万别。比如说，在商品推荐中指的是“商品信息”，在视频推荐中指的是“视频信息”，在新闻推荐中指的是“新闻信息”，为了方便，我们可以把它们统称为“物品信息”。

而从“人”的角度出发，为了更可靠地推测出“人”的兴趣点，推荐系统希望能利用大量与“人”相关的信息，这类信息包括历史行为、人口属性、关系网络等，它们可以被统称为“用户信息”。

此外，在具体的推荐场景中，用户的最终选择一般会受时间、地点、用户的状态等一系列环境信息的影响，这些环境信息又可以被称为“场景信息”或“上下文信息”。

清楚了这些信息的定义，推荐系统要处理的问题就可以被形式化地定义为：对于某个用户U（User），在特定场景C（Context）下，针对海量的“物品”信息构建一个函数 ，预测用户对特定候选物品I（Item）的喜好程度，再根据喜好程度对所有候选物品进行排序，生成推荐列表的问题。

这样一来，我们就可以抽象出推荐系统的逻辑框架了。虽然这个逻辑框架还比较简单，但我们正是在此基础上，对各模块进行细化和扩展，才产生了推荐系统的整个技术体系。

图1 推荐系统逻辑架构

深度学习对推荐系统的革命

有了推荐系统的逻辑架构，我就能回答开头的第三个问题“深度学习到底给推荐系统带来了什么革命性的影响？”。

在推荐系统逻辑架构图中，居于中心位置的是一个抽象函数f(U,I,C)，它负责“猜测”用户的心，为用户可能感兴趣的物品打分，从而得出最终的推荐物品列表。在推荐系统中，这个函数一般被称为“推荐系统模型”（今后简称“推荐模型”）。

深度学习应用于推荐系统，能够极大地增强推荐模型的拟合能力和表达能力。简单来说，就是让推荐模型“猜的更准”，更能抓住用户的“心”。这么说你可能还没有一个清晰的概念，接下来，我们再从模型结构的角度出发，来比较一下传统机器学习推荐模型和深度学习推荐模型的区别，让你有一个更清晰的认识。

我在下面给出了一张模型结构对比图，它对比了传统的矩阵分解模型和深度学习矩阵分解模型的区别。我们先忽略细节不谈，你第一眼看上去有什么感觉？是不是觉得深度学习模型变得更复杂了，一层又一层，层数增加了很多。

你的感觉一点儿都没错，其实正是因为深度学习复杂的模型结构，让深度学习模型具备了理论上拟合任何函数的能力。如果说 *f(U,I,C)这个推荐函数具有一个最优的表达形式，那传统的机器学习模型只能够拟合出f(U,I,C)* 这个推荐函数的近似形式，而深度学习模型则可以最大程度地接近这个最优形式。

图2 传统的矩阵分解模型和深度学习矩阵分解模型的对比图
来源：《Neural collaborative filtering》

除此之外，深度学习模型非常灵活的模型结构还让它具备了一个无法替代的优势，就是我们可以让深度学习模型的神经网络模拟很多用户兴趣的变迁过程，甚至用户做出决定的思考过程。比如阿里巴巴的深度学习模型——深度兴趣进化网络（如图 3），它利用了三层序列模型的结构，模拟了用户在购买商品时兴趣进化的过程，如此强大的数据拟合能力和对用户行为的理解能力，是传统机器学习模型不具备的。

图3 阿里巴巴的深度兴趣进化网络
来源：《Deep Interest Evolution Network for Click-Through Rate Prediction》

但是，深度学习对推荐系统的革命影响还远不止这些。近几年，由于深度学习模型的结构复杂度大大提高，使通过训练使模型收敛所需的数据量大大增加，这也反向推动了推荐系统大数据平台的发展，让推荐系统相关的大数据存储、处理、更新模块也一同迈入了“深度学习时代”。

讲了这么多深度学习对推荐系统的影响，我们似乎还没看到一个完整的深度学习推荐系统架构。别着急，下面，我们就来讲一讲，经典的深度学习推荐系统的技术架构是什么样的。

深度学习推荐系统的技术架构

讲之前啊，我还要说明一点，深度学习推荐系统的架构与经典的推荐系统架构其实是一脉相承的，它对经典推荐系统架构中某些特定模块进行了改进，使之能够支持深度学习的应用。所以，我会先讲经典的推荐系统架构，再讲深度学习对它们的改进。

在实际的推荐系统中，工程师需要着重解决的问题有两类。

一类问题与数据和信息相关，即“用户信息”“物品信息”“场景信息”分别是什么？如何存储、更新和处理数据？

另一类问题与推荐系统算法和模型相关，即推荐系统模型如何训练、预测，以及如何达成更好的推荐效果？

一个工业级推荐系统的技术架构其实也是按照这两部分展开的，其中“数据和信息”部分逐渐发展为推荐系统中融合了数据离线批处理、实时流处理的数据流框架；“算法和模型”部分则进一步细化为推荐系统中，集训练（Training）、评估（Evaluation）、部署（Deployment）、线上推断（Online Inference）为一体的模型框架。基于此，我们就能总结出推荐系统的技术架构图。

图4 推荐系统技术架构示意图

在图 4 中，我把推荐系统的技术架构分成了“数据部分”和“模型部分”。那它们的工作内容和作用分别是什么呢？深度学习对于这两部分的影响又有哪些呢？下面，我来一一讲解。

第一部分：推荐系统的数据部分

推荐系统的“数据部分”主要负责的是“用户”“物品”“场景”信息的收集与处理。根据处理数据量和处理实时性的不同，我们会用到三种不同的数据处理方式，按照实时性的强弱排序的话，它们依次是客户端与服务器端实时数据处理、流处理平台准实时数据处理、大数据平台离线数据处理。

在实时性由强到弱递减的同时，三种平台的海量数据处理能力则由弱到强。因此，一个成熟推荐系统的数据流系统会将三者取长补短，配合使用。我们也会在今后的课程中讲到具体的例子，比如使用 Spark 进行离线数据处理，使用 Flink 进行准实时数据处理等等。

大数据计算平台通过对推荐系统日志，物品和用户的元数据等信息的处理，获得了推荐模型的训练数据、特征数据、统计数据等。那这些数据都有什么用呢？具体说来，大数据平台加工后的数据出口主要有 3 个：

生成推荐系统模型所需的样本数据，用于算法模型的训练和评估。

生成推荐系统模型服务（Model Serving）所需的“用户特征”，“物品特征”和一部分“场景特征”，用于推荐系统的线上推断。

生成系统监控、商业智能（Business Intelligence，BI）系统所需的统计型数据。

可以说，推荐系统的数据部分是整个推荐系统的“水源”，我们只有保证“水源”的持续、纯净，才能不断地“滋养”推荐系统，使其高效地运转并准确地输出。在深度学习时代，深度学习模型对于“水源”的要求更高了，首先是水量要大，只有这样才能保证我们训练出的深度学习模型能够尽快收敛；其次是“水流”要快，让数据能够尽快地流到模型更新训练的模块，这样才能够让模型实时的抓住用户兴趣变化的趋势，这就推动了大数据引擎 Spark，以及流计算平台 Flink 的发展和应用。

第二部分：推荐系统的模型部分

推荐系统的“模型部分”是推荐系统的主体。模型的结构一般由“召回层”、“排序层”以及“补充策略与算法层”组成。

其中，“召回层”一般由高效的召回规则、算法或简单的模型组成，这让推荐系统能快速从海量的候选集中召回用户可能感兴趣的物品。“排序层”则是利用排序模型对初筛的候选集进行精排序。而“补充策略与算法层”，也被称为“再排序层”，是在返回给用户推荐列表之前，为兼顾结果的“多样性”“流行度”“新鲜度”等指标，结合一些补充的策略和算法对推荐列表进行一定的调整，最终形成用户可见的推荐列表。

从推荐系统模型接收到所有候选物品集，到最后产生推荐列表，这一过程一般叫做“模型服务过程”。为了生成模型服务过程所需的模型参数，我们需要通过模型训练（Model Training）确定模型结构、结构中不同参数权重的具体数值，以及模型相关算法和策略中的参数取值。

模型的训练方法根据环境的不同，可以分为“离线训练”和“在线更新”两部分。其中，离线训练的特点是可以利用全量样本和特征，使模型逼近全局最优点，而在线更新则可以准实时地“消化”新的数据样本，更快地反应新的数据变化趋势，满足模型实时性的需求。

除此之外，为了评估推荐系统模型的效果，以及模型的迭代优化，推荐系统的模型部分还包括“离线评估”和“线上 A/B 测试”等多种评估模块，用来得出线下和线上评估指标，指导下一步的模型迭代优化。

我们刚才说过，深度学习对于推荐系统的革命集中在模型部分，那具体都有什么呢？我把最典型的深度学习应用总结成了 3 点：

深度学习中 Embedding 技术在召回层的应用。作为深度学习中非常核心的 Embedding 技术，将它应用在推荐系统的召回层中，做相关物品的快速召回，已经是业界非常主流的解决方案了。

不同结构的深度学习模型在排序层的应用。排序层（也称精排层）是影响推荐效果的重中之重，也是深度学习模型大展拳脚的领域。深度学习模型的灵活性高，表达能力强的特点，这让它非常适合于大数据量下的精确排序。深度学习排序模型毫无疑问是业界和学界都在不断加大投入，快速迭代的部分。

增强学习在模型更新、工程模型一体化方向上的应用。增强学习可以说是与深度学习密切相关的另一机器学习领域，它在推荐系统中的应用，让推荐系统可以在实时性层面更上一层楼。

小结

这节课，我带你熟悉了深度学习推荐系统的技术架构，虽然涉及的内容非常多，但如果没有记住的话，你也完全不用慌张，只需要在心中留下这个框架的印象就可以了。你完全可以把这节课的内容当作整个课程的技术索引，让它成为属于你自己的一张知识图谱。

形象点来说，你可以把这节课程的内容想象成是一颗知识树，它有根，有干、有枝、有叶，还有花。

其中，推荐系统的根就是推荐系统要解决的根本性问题：在“信息过载”情况下，用户怎么高效获取感兴趣的信息。

而推荐系统的干就是推荐系统的逻辑架构：对于某个用户U（User），在特定场景C（Context）下，针对海量的“物品”构建一个函数 ，预测用户对特定候选物品I（Item）的喜好程度的过程。

枝和叶就是推荐系统的各个技术模块，以及各模块的技术选型。技术模块撑起了推荐系统的技术架构，技术选型又让我们可以在技术架构上实现各种细节，开枝散叶。

最后，深度学习在推荐系统的应用无疑是当前推荐系统技术架构上的明珠，它就像是这颗大树上开出的花，是最精彩的点睛之笔。深度学习的模型结构复杂，数据拟合能力和表达能力更强，能够让推荐模型更好的模拟用户的兴趣变迁过程，甚至是做决定的过程。而深度学习的发展，也推动着推荐系统数据流部分的革命，让它能够更快、更强地处理推荐系统相关的数据。

好了，这节课我们就讲到这里，希望你能牢牢记住深度学习推荐系统的架构，播撒下这一粒种子，然后跟随我后面的课程让它长大成一颗属于你自己的参天大树。

图5 推荐系统的技术架构像树一样生发而出

课后思考

下面是 Netflix 的推荐系统的经典架构图，你能结合本节课讲的推荐系统技术架构，说出 Netflix 架构图中哪些是数据部分，哪些是模型部分吗？

图6 Netflix架构图示意图

这样的深度学习推荐系统和你想的一样吗？如果今天的课程对你有帮助，也欢迎你把它转发出去！我们下节课见！

02｜Sparrow RecSys：我们要实现什么样的推荐系统？

你好，我是王喆。

上节课，我们明确了推荐系统要解决的基本问题，清楚了深度学习推荐系统的技术架构，这节课我们开始走进实战。

作为程序员，我相信你肯定听过，甚至可能还很认同 Linux 之父 Linus Torvalds 的那句话“Talk is cheap.Show me the code.”。我也一样，所以只讲解理论知识不是这门课的风格，我希望你通过这门课的学习，不仅能构建出一棵深度学习推荐系统的知识树，还能动手实现出一个看得见、摸得着、能操作、能修改的推荐系统。

所以今天，你跟着我的讲解，只需要花三十分钟的时间，就能将一套完整的深度学习推荐系统，Sparrow RecSys（随着课程的进行，我们会逐渐补充新的模块），在你自己的电脑上运行起来。这也是我们这门课最终要实现的深度学习推荐系统。

废话不多说，直接运行

废话不多说，我们先把 Sparrow RecSys 安装运行起来。因为我已经把项目相关的所有代码（代码还会随着课程进行持续更新）、数据都整理到 GitHub 的开源项目中，所以你不需要额外安装任何的支持软件，也不需要额外下载任何数据。

这样，整个安装过程就跟“把大象装进冰箱“一样，只需要三步，就是打开冰箱门，把大象装进去，关上冰箱门。“翻译”成咱们的过程就是，从 GitHub 中 clone 代码，在本地以 maven project 的形式安装，运行 RecSysServer 主函数启动推荐服务器。接下来，我们详细地解释一下这三个步骤。

首先，从 GitHub 中 clone 代码。这里，我直接给出了 Sparrow Recsys 开源项目的地址：https://github.com/wzhe06/SparrowRecSys。点击之后，你需要使用git clone https://github.com/wzhe06/SparrowRecSys.git命令，或者从 Web 端下载的方式，把代码下载到本地。

然后，你可以在本地以 maven project 的形式安装，也就是导入项目到 IDE。我推荐你使用 IntelliJ IDEA 为本项目的 IDE。这样，我们直接使用 IDEA，打开本地的 Sparrow Recsys 项目根目录就能导入项目。不过有一点需要注意，如果项目没有自动识别为 maven project，你还需要右键点击 pom.xml 文件，选择将该项目设置为 maven project 才能进行后面的操作。

最后，运行 RecSysServer。等到所有库文件自动下载完毕，项目编译完毕后，我们找到项目的主函数com.wzhe.sparrowrecsys.online.RecSysServer，右键点击运行。因为推荐服务器默认运行在 6010 端口，所以我们打开浏览器，输入http://localhost:6010/，就能看到整个推荐系统的前端效果了。

图1 Sparrow Recsys的主页

如果通过上面的步骤，你的浏览器显示出了由多个电影列表组成的 Sparrow Recsys 的主页，那么恭喜你，你已经拥有了这套深度学习推荐系统。

而且我相信，你把 Sparrow Recsys 这只“大象”装到自己冰箱里的时间，不会超过 30 分钟。但第一次见面的热情过后，你会不但想知其然，还想知其所以然，那接下来我就和你说说 Sparrow Recsys 的来历，以及功能和架构。而且在接下来的课程中，我会以它为例来给你讲透深度学习推荐系统。

“麻雀虽小，五脏俱全”的 Sparrow Recsys

Sparrow RecSys，全称 Sparrow Recommender System，中文名“麻雀推荐系统”，名字取自“麻雀虽小，五脏俱全”之意。

你第一眼见到它，可能认为它像个 Demo 或者玩具。虽然它不可能真正具备一个工业级深度学习推荐系统的全部功能，但我希望它是一颗能够成长为参天大树的种子，一只未来有可能大鹏展翅的雏鸟。在投入一定的精力改造、拓展之后，它甚至有可能支撑起一个规模互联网公司的推荐系统框架。这就是我设计 Sparrow RecSys 的初衷。我也希望你能够在实现 Sparrow RecSys 的过程中，快速领略深度学习推荐系统的主要模块和主流技术，并且找到乐趣、找到成就感。

那么 Sparrow Recsys 到底实现了哪些功能呢？它又包含了哪些深度学习推荐系统的关键技术呢？下面，我会为你一一讲解。

Sparrow Recsys 的功能有哪些

Sparrow RecSys 是一个电影推荐系统，视频推荐是我最熟悉的领域，这也是我以电影推荐作为切入点的原因。像所有经典的推荐系统一样，它具备“相似推荐”“猜你喜欢”等经典的推荐功能，在页面设置上，主要由“首页”“电影详情页”和“为你推荐页”组成。

首先，是 Sparrow RecSys 的首页。

图2 Sparrow RecSys的首页

Sparrow RecSys 的首页由不同类型的电影列表组成，当用户首次访问首页时，系统默认以历史用户的平均打分从高到低排序，随着当前用户不断为电影打分，系统会对首页的推荐结果进行个性化的调整，比如电影类型的排名会进行个性化调整，每个类型内部的影片也会进行个性化推荐。

其次，是电影详情页。

图3 电影详情页

你可以看到电影详情页除了罗列出电影的一些基本信息，最关键的部分是相似影片的推荐。相似内容推荐是几乎所有推荐系统非常重要的功能，传统的推荐系统基本依赖于基于内容（Content based）的推荐方法，而我们这门课程会更多地讲解基于深度学习 Embedding 的相似内容推荐方法。

最后，是为你推荐页。

图4 为你推荐页

这一部分也是整个推荐系统中最重要的部分，是用户的个性化推荐页面。这个页面会根据用户的点击、评价历史进行个性化推荐。这几乎是所有推荐系统最经典和最主要的应用场景。我希望在这门课程中，你能够动手完成个性化推荐中的每个关键步骤，包括但不限于特征的处理、候选集的召回、排序层主要模型等等。

Sparrow Recsys 的数据从哪来？

知道了 Sparrow RecSys 的功能之后，你肯定想问，“老师，咱们的数据从哪来呀？”。既然 Sparrow RecSys 是一个开源项目，那么 Sparrow RecSys 的数据源肯定也是开源和免费的，它的数据源来自于著名的电影开源数据集movieLens。

为了方便你调试，咱们这门课程的教学数据集对 movieLens 数据集进行了精简，只留下了 1000 部电影。如果希望在全量数据集上进行推荐，你可以去 MovieLens 的官方网站下载全量数据，它一共包含了 27000 部电影。

MovieLens 的数据集包括三部分，分别是 movies.csv（电影基本信息数据）、ratings.csv（用户评分数据）和 links.csv（外部链接数据）。下面，我就具体说说它们分别长什么样。

1. movies.csv（电影基本信息数据）

movies 表是电影的基本信息表，它包含了电影 ID（movieId）、电影名（title）、发布年份以及电影类型（genres）等基本信息。

图5 电影基本信息数据

MovieLens 20M Dataset 包含了 2016 年前的约 13 万部电影，我们课程的实验数据集从中抽取了前 1000 部电影。电影数据集是我们推荐的主体，其中分类、发布年份、电影名称等信息也将是推荐模型可以利用的重要特征。

2. ratings.csv（用户评分数据）

ratings 表包含了用户 ID（userId）、电影 ID（movieId）、评分（rating）和时间戳（timestamp）等信息。

图6 用户评分数据

MovieLens 20M Dataset 包含了 2000 万条评分数据，我们课程的实验数据集从中抽取了约 104 万条评论数据。评论数据集是之后推荐模型训练所需的训练样本来源，也是我们分析用户行为序列、电影统计型特征的原始数据。

3. links.csv（外部链接数据）

links 表包含了电影 ID（movieId）、IMDB 对应电影 ID（imdbId）、TMDB 对应电影 ID（tmdbId）等信息。其中，imdb 和 tmdb 是全球最大的两个电影数据库。因为 links 表包含了 movieLens 电影和这两个数据库 id 之间的对应关系，所以，我们可以根据这个对应关系来抓取电影的其他相关信息，这也为我们大量拓展推荐系统特征提供了可能。

图7 外部链接数据

此外，MovieLens 的数据集中还包含了 tags.csv，它用于记录用户为电影打的标签，由于课程中暂时没有使用标签数据，我就展开说了。

Sparrow Recsys 涵盖的技术点

清楚了 Sparrow Recsys 的功能和数据，你肯定迫不及待地想知道 Sparrow Recsys 会使用哪些技术，可以实现哪些模型。

那我们直接来看下面这张 Sparrow Recsys 的技术架构图。你会发现，它其实就是我们用具体的技术选型，把上节课的深度学习推荐系统架构图给填上得到的。所以，Sparrow Recsys 就是深度学习推荐系统架构的一个实现。

图8 Sparrow Recsys的推荐系统架构

你可以看到，它一共分为三个模块，分别是数据、模型和前端。其中每个部分都用业界推荐系统的主流技术，比如数据部分我们会用 Spark，Flink 进行样本和特征的处理，模型部分我们会使用 TensorFlow 训练深度神经网络、Wide&Deep、PNN 等模型。

图9 Sparrow Recsys中的技术点

我想啊，你在看到这么多的技术点和技术平台之后，肯定想问，我们的课程能把它们都讲完、讲透吗？这是个好问题，我也有必要在这里说清楚。

从中，我们可以总结出 Sparrow Recsys 中具体用到的技术点。

其实推荐系统是一个应用属性很强的领域，想把推荐系统学好，我们就必须去学习各式各样相关的平台、技术，所以我们这门课的涉及面非常广。但你也不用因为要学这么多的技术而感到惊慌，因为我们没有必要去深究每个平台内部的原理、优化的方法，我们当好一个使用者就好。

举个例子你就明白了，我们处理数据需要用到 Spark，但我们有必要成为 Spark 的专家吗？其实不用。因为即使你已经走上了工作岗位，也有平台架构部的同事能够提供 Spark 的很多技术支持。所以学习这门课程，我们大可抱着一个使用者，而不是开发者、维护者的心态去使用不同的技术平台。当然，如果你想成为某个细分方向的专家，比如 Spark 的专家、Flink 的专家等等，我相信极客时间上肯定还有很不错的课程供你学习。

所以希望你能够通过 Sparrow Recsys 认识到主流深度学习推荐系统都使用了哪些技术，让自己有一个全面的认识，建立自己的知识广度。如果还想深入钻研某个方向，也可以由此开始，努力成为一个领域的专家。

小结

这堂课，我带你熟悉了我们将要实现的推荐系统 Sparrow Recsys，它将是我们深度学习推荐系统这门课的落地项目和实现范例。希望有这个真实可用的推荐系统作为支撑，这门课可以同时兼顾概念讲解和代码实战，也让我们接下来的共同合作能够更好。

从开篇词到这一节课，我们从推荐系统要解决的核心问题，生发出深度学习推荐系统的技术架构，再到让技术架构实实在在地落地到 Sparrow Recsys 这个开源项目上。我想你已经可以感受到架构篇的学习过程，其实就是一个从抽象到具体，从形而上到形而下的过程。

那在搭建起这整门课程的框架之后，接下来我们将会一起深入到技术细节，以及深度学习的实践中，一起去体验深度学习浪潮之巅的推荐系统知识，期待继续与你同行！

课后思考

当你把 Sparrow Recsys 在自己的电脑上安装运行起来之后，对照着上节课的深度学习推荐系统架构图，你能试着说出每个模块的代码属于架构图中的哪一部分吗？

你觉得对于一个电影推荐系统来说，什么数据对生成用户个性化推荐结果最有帮助？

好啦，快按照这节的方法把 Sparrow Recsys 运行起来吧！课后的两个问题也并不困难，相信你肯定可以回答出来。今天就讲到这里了，我们下节课见！

03｜深度学习基础：你打牢深度学习知识的地基了吗？

你好，我是王喆。

今天，我想用一节课的时间，带你梳理巩固一下深度学习的相关基础知识。打好基础之后，我们再去学习深度学习推荐系统的技术细节，就能更加得心应手了。

具体来说，我会从一个基本的神经元开始，讲到多神经元组成的神经网络，再到结构各异的深度学习网络，最后再讲一讲深度学习和推荐系统是怎么结合的。这样，从 0 到 1 带你体会深度学习网络生长的整个过程。

是不是已经迫不及待想要开始今天的课程啦？接下来，我们就一起“钻”进一个神经元里面，跟它一起成长吧。

一切要从一个神经元开始

上中学的时候，你肯定在生物课上学到过，神经元是我们神经系统的最基本单元，我们的大脑、小脑、脊髓都是由神经元组成的。比如，大脑大概包含了 1000 亿个神经元！正是这些小小的神经元之间互相连接合作，让大脑能够完成非常复杂的学习任务，这是一个多么神奇的过程！

于是，计算机科学家们就有一个设想，是不是我们也能从神经元出发，创造出一个人造大脑，来帮我们完成各种不同的任务呢？这其中当然也包括我们课程要讲的推荐任务。事实上，随着近十年深度学习网络的快速发展，这个设想已经被成功应用到图像识别、语音处理、推荐搜索等多个领域了！那组成这个“人造大脑”的基础，也就是神经元到底是什么样子的呢？

下面这张图就是一个神经元的结构示意图，它大致由树突、细胞体（图中细胞核和周围的部分）、轴突、轴突末梢等几部分组成。树突是神经元的输入信号通道，它的功能是将其他神经元的动作电位（可以当作一种信号）传递至细胞体。

图1 神经元示意图

在接收到其他神经元的信号后，细胞体会根据这些信号改变自己的状态，变成“激活态”还是“未激活态”。具体的状态取决于输入信号，也取决于神经细胞本身的性质（抑制或加强）。当信号量超过某个阈值时，细胞体就会被激活，产生电脉冲。电脉冲会沿着轴突传播，并通过轴突末梢传递到其它神经元。

我上面讲的这些是神经元工作的生物学过程，那如果要用一个神经元来解决推荐问题，具体又该怎么做呢？举个例子，我们可以假设其他神经元通过树突传递过来的信号就是推荐系统用到的特征，有的信号可能是“用户性别是男是女”，有的信号可能是“用户之前有没有点击过这个物品”等等。细胞体在接收到这些信号的时候，会做一个简单的判断，然后通过轴突输出一个信号，这个输出信号大小代表了用户对这个物品的感兴趣程度。这样一来，我们就可以用这个输出信号给用户做推荐啦。

看起来用神经元来完成推荐任务还是很有希望的，但在实际应用里面，我们还得把生物结构的神经元抽象成一个数学形式，这样我们才能用程序来实现它。图 2 就是这样的一个抽象结构，这个神经元的结构很简单，只有两个传递输入信号的树突。

图2 神经元的抽象结构

我们可以看到，图 2 中的 x1、x2就相当于两个树突传递的输入信号，蓝圈内的结构相当于神经元的细胞体，细胞体用某种方式处理好输入信号之后，就通过右面的轴突输出信号 y。因为输入输出都很简单，所以我想现在你的疑问，肯定聚焦在细胞体对输入信号的处理方式上了，我们可以把细胞体的数学结构放大一点看看。

图3 基于Sigmoid激活函数的神经元

其实细胞体中的计算就做了两件事情，一件事情是把输入信号 x1、x2各自乘以一个权重 w1、w2，再把各自的乘积加起来之后输入到一个叫“激活函数”的结构里。

图 3 中的激活函数是 sigmoid 激活函数，它的数学定义是：f(z)=1+e−z1​ 。它的函数图像就是图 3 中的 S 型曲线，它的作用是把输入信号从（-∞，+∞）的定义域映射到（0，1）的值域（因为在点击率预测，推荐问题中，往往是要预测一个从 0 到 1 的概率）。再加上 sigmoid 函数有处处可导的优良数学形式，方便之后的梯度下降学习过程，所以它成为了经常使用的激活函数。

当然，激活函数的种类有很多种，比较流行的还有 tanh、ReLU 等，在训练神经元或者神经网络的时候，我们可以尝试多种激活函数，根据效果来做最终的决定。

什么是神经网络？

不过，单神经元由于受到简单结构的限制，预测能力并不强，因此在解决复杂问题时，我们经常会用多神经元组成一个网络，这样它就具有更强的拟合数据的能力了，这也就是我们常说的神经网络。比如说，下图就向我们展示了一个由输入层、两神经元隐层和单神经元输出层组成的简单神经网络。

图4 简单神经网络示意图

其中，每个蓝色神经元的构造都和刚才的单神经元构造相同，h1和 h2神经元的输入是由 x1和 x2组成的特征向量，而神经元 o1的输入则是由 h1和 h2输出组成的输入向量。这是一个最简单的三层神经网络，在深度学习的发展过程中，正是因为研究人员对神经元不同的连接方式的探索，才衍生出各种不同特性的深度学习网络，让深度学习模型的家族树枝繁叶茂。在后面课程的学习中，我们也会深入讲解各种不同的网络结构，相信你对这句话的理解也会随着学习的推进而更加深刻。

神经网络是怎么学习的？

清楚了神经网络的结构之后，更重要的问题是我们该如何训练一个神经网络。也就是说，我们怎么得到图 5 中 x1到 h1、h2的权重 w1、w3，以及图中其他的权重呢？

图5 神经网络中的权重

这里需要用到神经网络的重要训练方法，前向传播（Forward Propagation）和反向传播（Back Propagation）。前向传播的目的是在当前网络参数的基础上得到模型对输入的预估值，也就是我们常说的模型推断过程。比如说，我们要通过一位同学的体重、身高预测 TA 的性别，前向传播的过程就是给定体重值 71，身高值 178，经过神经元 h1、h2和 o1的计算，得到一个性别概率值，比如说 0.87，这就是 TA 可能为男性的概率。

在得到预估值之后，我们就可以利用损失函数（Loss Function）计算模型的损失。比如我们采用绝对值误差（Absolute Loss）作为我们的损失函数，如果这位同学的真实性别是男，那真实的概率值就是 1，根据公式 2 的绝对值误差定义，这次预测的损失就是|1-0.87| = 0.13。

l1​(yi​,y^​i​)=∣yi​−y^​i​∣

我们常说“知错能改，善莫大焉”，神经网络的学习更是践行了这句话。发现了预测值和真实值之间的误差（Loss），我们就要用这个误差来指导权重的更新，让整个神经网络在下次预测时变得更准确。最常见的权重更新方式就是梯度下降法，它是通过求取偏导的形式来更新权重的。比如，我们要更新权重 w5，就要先求取损失函数到 w5 的偏导 ∂w5​∂Lo1​​。从数学角度来看，梯度的方向是函数增长速度最快的方向，那么梯度的反方向就是函数下降最快的方向，所以让损失函数减小最快的方向就是我们希望梯度 w5 更新的方向。这里我们再引入一个超参数α，它代表了梯度更新的力度，也称为学习率。好，现在我们可以写出梯度更新的公式了：w5t+1​=w5t​−α∗∂w5​∂Lo1​​。公式中的 w5当然可以换成其他要更新的参数，公式中的 t 代表着更新的次数。

对输出层神经元来说（图中的 o1），我们可以直接利用梯度下降法计算神经元相关权重（即图 5 中的权重 w5和 w6）的梯度，从而进行权重更新，但对隐层神经元的相关参数（比如 w1），我们又该如何利用输出层的损失进行梯度下降呢？

答案是“利用求导过程中的链式法则（Chain Rule）”。通过链式法则我们可以解决梯度逐层反向传播的问题。最终的损失函数到权重 w1的梯度是由损失函数到神经元 h1输出的偏导，以及神经元 h1输出到权重w1的偏导相乘而来的。也就是说，最终的梯度逐层传导回来，“指导”权重w1的更新。

∂w1​∂Lo1​​=∂h1​∂Lo1​​⋅∂w1​∂h1​​

具体在计算的时候，我们需要根据具体的问题明确最终损失函数的形式，以及每层神经元激活函数的形式，再根据具体的函数形式进行偏导的计算。这部分的学习需要一定的微积分基础，如果你觉得不是很好理解，也不用担心，因为对于大部分的机器学习库来说，梯度反向传播的过程已经被实现并且封装好了，直接调用就可以了，但是原理我们还是有必要了解的。

到这里，神经网络相关的基本知识我们就讲完了，前面讲了这么多，我想再带你做个总结。神经元是神经网络中的基础结构，它参照生物学中的神经元构造，抽象出带有输入输出和激活函数的数学结构。而神经网络是通过将多个神经元以串行、并行、全连接等方式连接起来形成的网络，神经网络的训练方法是基于链式法则的梯度反向传播。

但是细心的你可能会问了，搭建深度学习推荐系统跟神经网络到底有什么关系呢？

神经网络与深度学习的关系是什么？

想要搞清楚这个问题，我得先给你讲点儿神经网络和深度学习的历史。

其实学术界对于人工神经网络的研究非常早，可以追溯上世纪 60 年代，但由于计算机发展本身的限制，我们没有办法完成神经网络学习所需的大量计算。直到上世纪 80 年代，Hinton 等人提出了反向传播算法（BP 算法），神经网络才能够完成类似字母识别的简单任务。

时间到了 2012 年，Hinton 的学生 Alex Krizhevsky 提出了一个用于图像识别的深度神经网络结构 AlexNet，在当年著名的图像识别大赛 ImageNet 上，它以碾压第二名的成绩荣获桂冠。也正是从那时起，人们突然意识到，原来用深层的神经网络可以完成这么复杂的任务。深度学习的浪潮从此被正式引爆了！

图6 著名的深度学习模型AlexNet

结合上图你可以看到，跟我们刚才讲的简单神经网络相比，AlexNet 无论从深度，还是每一层的神经元数量来说，都大大增加了，并且神经元的连接方式和种类也更加丰富。

那知道了这些，我们就可以回答刚才提出的问题了。深度学习可以说是神经网络的延伸和发展，它极大地丰富神经网络的结构种类，让它能够处理各类复杂问题。

这个时候，你可能又会问：好像深度学习相比传统的神经网络没什么革命性的创新呀，为啥到了 2012 年才取得这么大的突破呢？这是个好问题，我觉得主要有三个原因。

一是算力的极大提高。 到了 2012 年，随着 GPU 大量应用于机器学习领域，神经网络的训练速度也有了量级上的提高，时至今日，OpenAI 刚发布的语言模型 GPT-3 居然有高达 1750 亿个参数，这放在十年前是完全不可想像的。

二是数据的极大丰富。 之前神经网络面临的一大问题是，在训练数据量较小的情况下，模型难以收敛。但随着越来越多成熟的大数据开源平台，以及越来越多丰富的开源数据集的出现，即使神经网络的结构变得越来越复杂，我们也完全可以凭借海量的训练数据使它完全收敛。

三是深度学习理论的进一步发展。 虽然深度学习是站在“神经网络”这一巨人的肩膀上发展起来的，但专家们也取得了非常多的理论创新，就比如，pooling 层的加入和成功应用，各类更适合深度学习的梯度下降方法的提出等等，这些都让深度学习的应用成为了可能。

深度学习是如何应用在推荐系统中的？

前面我们介绍的都是通用的深度学习知识，但深度学习又是如何应用在推荐系统中的呢？下面，我就着重来说说这一点。

在刚才讲解神经元原理的时候，我讲到用单个神经元可以预测用户对物品感兴趣的程度。事实上，无论是单个神经元，还是结构非常复杂的深度学习网络，它们在推荐系统场景下要解决的问题都是一样的，就是预测用户对某个物品的感兴趣程度，这个感兴趣程度往往是一个概率，最典型的就是点击率、播放率、购买概率等。

所以在深度学习时代，我们使用深度学习模型替代了传统的推荐模型，目的就是让它作出更准确的推荐。但像上节课提出的，深度学习的革命要求我们对算力、数据都作出大幅度的调整，而这些调整因为涉及到分布式计算平台、深度学习平台，以及线上的模型服务部分，所以我们需要在推荐系统的整体架构上都作出不小的改造，让它适应深度学习时代的需求。这也是我们学习这门课的目的所在。

小结

今天，我带你学习了深度学习的基础知识。我们从神经元学到了神经网络，再到训练神经网络的方法，以及神经网络和深度学习的关系。今天的知识点比较密集，而且每个知识点之间都是层层递进的。为了方便你记忆，我把本节课的重点整理成了一张表格，帮你巩固所学。

这些知识是未来我们搭建深度学习推荐系统的基础。我希望你能够在动手实践之前完全掌握它。从下节课开始，我们就会进入深度学习推荐系统技术细节和实战环节，你准备好了吗？

课后思考

你觉得都有哪些因素影响着深度学习网络的结构？深度学习模型是越深越好吗？为什么？

欢迎在留言区分享你的答案和疑惑，如果你的朋友也想了解神经网络和深度学习的基本知识，也欢迎你把这节课分享给他。好了，今天的内容就到这里了，我们下节课见！

国庆策划 | 关于深度学习推荐系统，我有这些资料想推荐给你

你好，我是王喆。明天就是国庆和中秋假期了，这里我提前祝你节日快乐！

专栏上线以来，通过跟同学们在留言区讨论，我发现同学们的基础差别很大。有的同学已经是其他领域的资深工程师，希望借这门课丰富一下知识体系。有的同学确实是 0 基础的新手，可能对于机器学习的一些基本概念都不是特别清楚。

所以，我特意准备了一些适合不同阶段学习的参考书目和一些基础的实践项目，希望你能借着国庆假期查漏补缺，为我们后面的学习打好基础。

虽然这些参考书适合的学习阶段不同，但它们有两个特点，一是这些书，我都读过很多遍，它们在我学习、工作生涯的某个阶段让我受益匪浅；二是这些书都非常易读易懂，不是那种诘屈聱牙的“不适合人类阅读”的技术书。好，我们先来看看都有哪些书吧。

技术参考书，贵精不贵多

如果你在学习第 3 节课的时候有一些困难，就说明你对机器学习的一些基础概念还不太清楚，我希望你能继续巩固机器学习的基础知识，这里我推荐三本书，你根据自己的偏好和知识基础选择一本就好啦。

第一本书是南京大学周志华老师的《机器学习》。

这本书也称“西瓜书”，它的内容比较偏向传统机器学习，深度学习的内容也有，但不是重点。它的特点就是内容非常全面、详尽，语言也流畅易懂。所以，我把它推荐给机器学习基础不太好的同学，希望能够帮助你巩固基础。

第二本书是复旦大学邱锡鹏老师的《神经网络与深度学习》。

这本书也叫“蒲公英书”，它更偏向介绍神经网络和深度学习，对深度学习的介绍非常全面、详尽，但又不故作高深，是我们入门深度学习非常好的选择。

第三本书是诸葛越和 hulu 机器学习团队的《百面机器学习》。

这本书是我在 hulu 时跟 hulu 的机器学习团队一同编写的，也称“葫芦书”。它重在讲解一些机器学习领域关键的知识点，如果你正在准备算法岗位的知识性面试，它会是你极佳的选择。这本书比较适合有一些机器学习基础，但还不够深入的同学来查漏补缺。

除此之外，如果你在学习推荐系统技术架构时还有一些疑惑的话，我再给你推荐两本推荐系统领域的书籍，你可以利用它们进一步来丰富推荐系统的知识。

在我刚工作的时候，项亮的这本《推荐系统实践》让我受益匪浅，它介绍了经典的协同过滤、矩阵分解方法，还有推荐系统可以利用的数据，以及基本的评测方法等等。时至今日，其中经典的推荐系统知识仍然可以让我们受用。如果你想了解经典的推荐系统算法、技术架构，可以尝试读一读它。

这里，我还想给你推荐一本我的新书，《深度学习推荐系统》，它是今年年初出版的。我也看到很多同学在留言区提问说“老师，咱们的专栏和这本书的区别在哪呀？”。这里，我统一回复一下。

这本书当然跟咱们专栏有着千丝万缕的联系，因为它们共享了同样的知识框架。不过，咱们的专栏重在实践，这本书注重介绍知识，二者呈互补的关系。在学习这门课的同时，你也可以购买这本书，进一步拓展自己在深度学习推荐系统这个领域的知识面。

除了这些和深度学习推荐系统非常相关的书籍之外，我还想给你推荐两本课外书，这两本书是我非常喜欢的计算机领域的“闲书”。如果假期有时间的话，我非常推荐你读一读，它们可以帮助我们建立更全面的计算机思维和系统设计理念。当然，如果你都读过，也欢迎在留言区和我分享你的看法。

首先是吴军老师的《数学之美》，这本书在我刚工作的时候给了我很多的灵感，书中的内容涉及了机器学习、人工智能、信息论、自然语言处理等等重要的计算机科学子领域。虽然涉及面非常广，但是讲得非常透，生动有趣的语言也让我完全没有阅读压力。所以，我把它推荐给想拓展知识面的同学。

计算机经典书籍有很多，但最让我感到惊喜的是这本《程序员修炼之道》。我读这本书是两年前，因为已经有了很多大项目的开发和架构经验，所以读这本书时候，很多地方我都会会心一笑，太有感触了。书里面介绍了很多重构、架构、系统设计、程序员哲学相关的经验知识。因为推荐系统毫无疑问是一个复杂的大系统，所以有些时候，我们总要以系统的眼光去看待推荐系统的问题，这本书涉及的理念就至关重要了。

更关键的是，我非常喜欢它的副标题，“通向务实的最高境界”。我认为这是“返璞归真”的程序员的最高境界，也是我们作为一名工程师应该遵循的思考方式

初识实践工具，走好入门第一步

咱们这门课之后的实战环节，还会涉及很多推荐系统相关的工具，比如 Spark、TensorFlow、Redis、Jetty Server 等等。为了在之后的学习中避免一些上手的困难，我建议你先熟悉一下 Spark、TensorFlow 和 Redis 这三个工具。如果你对这些工具完全没有概念，可以通过我在下面介绍进行初步的了解。

首先是 Spark。它是业界最流行的分布式计算平台，如果你还没有相关经验的话，我建议你按照我给出的三步来学习。首先，你可以通过这篇文章（如何用形象的比喻描述大数据的技术生态？Hadoop、Hive、Spark 之间是什么关系？）来了解一下大数据的生态，然后，你可以通过 Spark 的官方教程尝试写一个 Spark Hello World 程序（在我们的 SparrowRecSys 里面新建一个 Scala 文件就可以）。因为咱们这门课大量使用了 Spark 的机器学习库 Spark MLlib，所以最后可以通过这个官方教程来做一些初步的了解。

其次是 TensorFlow。它是我们这门课要使用的训练深度学习模型的平台。Keras 是一套 TensorFlow 支持的 API，因为它的易用性，我们主要利用 Keras API 来实现我们的推荐模型。所以，我们第一步可以先看一篇介绍 TensorFlow 和 Keras 的基本概念的文章，对它们有一个初步的认识，再通过给 TensorFlow 的 Keras 接口写一个 Hello World项目做一个基本的上手实践。最后，如果你还有时间，可以通过TensorFlow 官方教程进一步熟悉 TensorFlow 的其他功能。

最后是 Redis。Redis 是我们这门课要频繁使用的内存数据库，用来存储模型所需线上特征。你可以先在官网熟悉一下Redis的基本介绍，然后下载安装它，最后尝试使用 Redis 内置客户端 redis-cli，来执行几条基本的Redis命令。

相信熟悉了深度学习的基础知识和基本工具之后，你学习起来专栏后续的课程就会更加得心应手。对于完全 0 基础的同学，我可以这样说，如果你能通过我列出的书单和项目列表打牢基础，学习后面的课程肯定完全没有问题！

最后，我也想问问你，你在进行推荐、广告、搜索这些领域的学习的时候，还阅读过哪些非常不错的技术书？欢迎也在留言区分享出来，我们可以一起交流读后感，也可以让更多的同学受益。最后，再次祝你假期快乐，我们国庆之后再会！

推荐阅读

Spark：

1.大数据生态的介绍

2.写一个 Spark Hello World 程序

3.Spark 的机器学习库 MLlib 官方文档

TensorFlow：

1.TensorFlow 和最常用的接口 Keras 推荐阅读

2.利用 TensorFlow 的 Keras 接口写一个 hello world

3.TensorFlow 官方教程

Redis：

1.Redis 官方简介

2.基本的 Redis 命令

国庆策划 | 深度学习推荐系统基础，你掌握了多少？

你好，我是王喆。

今天是国庆假期的第二天，我在基础架构篇的基础上，设计了一套测试题，希望你能在假期里试做一下，温故知新，巩固所学。

在这套测试题中，有 10 道选择题，每道题 10 分，满分为 100。这些内容全部来自我们之前专栏所讲的内容，都是基础知识，希望能帮助你查缺补漏。

最后呢，我还为你准备了两道实践题，这两道题为选做。如果你对自己有更高的要求，我希望你可以 show me the code！当然，在答题和实践中遇到任何问题，都可以在留言区评论，我们一起来探讨。还等什么，点击下面的按钮开始测试吧！

选择题

问答题

第一题：通过阅读 SparrowRecSys 的代码，你能找到首页中每行内部的电影都是按照什么排序的吗？

第二道题：如果你已经发现了排序的规则，你能在推荐服务器的代码里添加一个叫“热度”（popularity）的排序规则，然后让首页按照“热度”排序吗（💡补充：热度的定义是“这个电影被评价的次数”）？

我相信这是熟悉 movielens 数据、推荐服务器代码的一个非常好的机会，那在实现过程中的任何问题，都欢迎你留言和我交流！我们 10 月 9 日再见！

04｜特征工程：推荐系统有哪些可供利用的特征？

你好，我是王喆。基础架构篇我们已经讲完了，你掌握得怎么样？希望你已经对深度学习推荐系统有了一个初步的认识。

从这节课开始，我们将会开启一个新的模块，特征工程篇。

如果说整个推荐系统是一个饭馆，那么特征工程就是负责配料和食材的厨师，推荐模型这个大厨做的菜好不好吃，大厨的厨艺肯定很重要，但配料和食材作为美食的基础也同样重要。而且只有充分了解配料和食材的特点，我们才能把它们的作用发挥到极致。

今天，我们就先来讲讲特征工程，说说到底什么是特征工程，构建特征工程的基本原则是什么，以及推荐系统中常用的特征有哪些。相信通过这节课的学习，能让你更好地利用起推荐系统相关的数据提升推荐的效果。

什么是特征工程

在第一节课中我们学习过，推荐系统就是利用“用户信息”“物品信息”“场景信息”这三大部分有价值数据，通过构建推荐模型得出推荐列表的工程系统。

图1 特征工程部分在推荐系统中的位置

在这个系统之中，特征工程就是利用工程手段从“用户信息”“物品信息”“场景信息”中提取特征的过程。这个过程说起来容易，但实际做起来其实困难重重。

比如说，一个网站或者 APP 每天收集起来的用户日志，采集来的站外信息，自己公司员工编辑添加的结构化数据那么多，那么庞杂，怎么才能挑出那些对推荐有用的特征呢？

再比如从“推荐模型”的角度来说，一个机器学习模型的输入，往往是一个数值型的向量。那用户性别，用户行为历史这些根本不是数字的信息怎么处理成一个模型可用的数值向量呢？

我们这节课先聚焦第一个问题，“怎么挑出有用特征”，下节课我们再解决第二个问题。都说“理论指导实践”，在展开讲有哪些有用的特征之前，我们先看一看构建特征工程有哪些原则或者规律可以遵循。

构建推荐系统特征工程的原则

我给推荐系统中的特征下了一个比较抽象的定义，特征其实是对某个行为过程相关信息的抽象表达。为什么这么说呢？因为一个行为过程必须转换成某种数学形式才能被机器学习模型所学习，为了完成这种转换，我们就必须将这些行为过程中的信息以特征的形式抽取出来。

我们来举个最简单的例子，用户的性别有三个，男、女、未知。但推荐模型没办法直接认识这三个类别，它是一个只认识数字的“严重偏科理工男”，所以我们就需要把它转换成 1、2、3（为了方便你理解，这里我用的是一个最简单的方法，不一定是最合适的）这样的数字代号它才能处理。

但是，这种从具体行为信息转化成抽象特征的过程，往往会造成信息的损失。为什么这么说呢？

一是因为具体的推荐行为和场景中包含大量原始的场景、图片和状态信息，保存所有信息的存储空间过大，我们根本无法实现。

二是因为具体的推荐场景中包含大量冗余的、无用的信息，把它们都考虑进来甚至会损害模型的泛化能力。比如说，电影推荐中包含了大量的影片内容信息，我们有没有必要把影片的所有情节都当作特征放进推荐模型中去学习呢？其实没有必要，或者说收效甚微。

这其实也是我们构建推荐系统特征工程的原则：尽可能地让特征工程抽取出的一组特征，能够保留推荐环境及用户行为过程中的所有“有用“信息，并且尽量摒弃冗余信息。

接下来，我们就结合一个实际的例子，说一说在电影推荐这个场景下，我们该怎么贯彻特征工程原则来挑选特征。

现在，你就可以先把自己当成是一个用户，假设你正在选择看哪部电影。想一想在这个选择过程中，你都会受什么因素影响呢？如果是我的话，可能影响我的因素有 6 个，把它们按照重要性由高到低排序就是，电影类型我是否感兴趣、电影是不是大片、导演和演员我是否喜欢、电影海报是否吸引人、我是否已经观看过该影片以及我当时的心情。

那站在一个工程师的角度，我们能不能用某些特征把这些要素表达出来呢？我尝试用表格的形式把它们特征化的方法列举了出来：

图2 电影推荐的要素和特征化方式

我们详细来讲一个要素，比如，如何知道一个用户是否对这个电影的类型（动作、喜剧、爱情等）感兴趣。一般来说，我们会利用这个用户的历史观看记录来分析他已有的兴趣偏好，这个兴趣偏好可能是每个电影类型的概率分布，比如动作 45%、喜剧 30%、爱情 25%。也可能是一个通过 Embedding 技术学出来的用户兴趣向量。

这个时候，我们就可以根据这个电影本身的特征，计算出用户对电影的感兴趣程度了。对于其他的特征，我们也都可以通过类似的分析，利用日志、元数据等信息计算得出。

不过，并不是所有的要素都能特征化。比如，“自己当时的心情”这个要素就被我们无奈地舍弃了，这是因为我们很难找到可用的信息，更别说抽取出特征了；再比如，“电影海报是否吸引人“这个要素，我们可以利用一些图像处理的方法提取出海报中的某些要点（比如海报中有哪些演员？是什么风格？），但想面面俱到地提取出海报中所有的图像要素，几乎是不可能的。

因此，在已有的、可获得的数据基础上，“尽量”保留有用信息是现实中构建特征工程的原则。

推荐系统中的常用特征

前面我以电影推荐为例，讲解了特征工程的基本原则，互联网中的推荐系统当然不仅限于电影推荐，短视频、新闻、音乐等等都是经典的推荐场景，那么它们常用的特征之间有没有共性呢？确实是有的，推荐系统中常用的特征有五大类，下面我一一来说。

1. 用户行为数据

用户行为数据是推荐系统最常用，也是最关键的数据。用户的潜在兴趣、用户对物品的真实评价都包含在用户的行为历史中。用户行为在推荐系统中一般分为显性反馈行为（Explicit Feedback）和隐性反馈行为（Implicit Feedback）两种，在不同的业务场景中，它们会以不同的形式体现。具体是怎么表现的呢？你可以看我下面给出的几个例子。

图3 不同业务场景下用户行为数据的例子

对用户行为数据的使用往往涉及对业务的理解，不同的行为在抽取特征时的权重不同，而且一些跟业务特点强相关的用户行为需要推荐工程师通过自己的观察才能发现。

在当前的推荐系统特征工程中，隐性反馈行为越来越重要，主要原因是显性反馈行为的收集难度过大，数据量小。在深度学习模型对数据量的要求越来越大的背景下，仅用显性反馈的数据不足以支持推荐系统训练过程的最终收敛。所以，能够反映用户行为特点的隐性反馈是目前特征挖掘的重点。

2. 用户关系数据

互联网本质上就是人与人、人与信息之间的连接。如果说用户行为数据是人与物之间的“连接”日志，那么用户关系数据就是人与人之间连接的记录。就像我们常说的那句话“物以类聚，人以群分”，用户关系数据毫无疑问是非常值得推荐系统利用的有价值信息。

用户关系数据也可以分为“显性”和“隐性”两种，或者称为“强关系”和“弱关系”。如图 4 所示，用户与用户之间可以通过“关注”“好友关系”等连接建立“强关系”，也可以通过“互相点赞”“同处一个社区”，甚至“同看一部电影”建立“弱关系”。

图4 社交网络关系的多样性

在推荐系统中，利用用户关系数据的方式也是多种多样的，比如可以将用户关系作为召回层的一种物品召回方式；也可以通过用户关系建立关系图，使用 Graph Embedding 的方法生成用户和物品的 Embedding；还可以直接利用关系数据，通过“好友”的特征为用户添加新的属性特征；甚至可以利用用户关系数据直接建立社会化推荐系统。

3. 属性、标签类数据

推荐系统中另外一大类特征来源是属性、标签类数据，这里我把属性类和标签类数据归为一组进行讨论，是因为它们本质上都是直接描述用户或者物品的特征。属性和标签的主体可以是用户，也可以是物品。它们的来源非常多样，大体上包含图 5 中的几类。

图5 属性、标签类数据的分类和来源

用户、物品的属性、标签类数据是最重要的描述型特征。成熟的公司往往会建立一套用户和物品的标签体系，由专门的团队负责维护，典型的例子就是电商公司的商品分类体系；也可以有一些社交化的方法由用户添加。图 6 就是豆瓣的“添加收藏”页面，在添加收藏的过程中，用户需要为收藏对象打上对应的标签，这是一种常见的社交化标签添加方法。

图6 豆瓣的“添加收藏”页面

在推荐系统中使用属性、标签类数据，一般是通过 Multi-hot 编码的方式将其转换成特征向量，一些重要的属性标签类特征也可以先转换成 Embedding，比如业界最新的做法是将标签属性类数据与其描述主体一起构建成知识图谱（Knowledge Graph），在其上施以 Graph Embedding 或者 GNN（Graph Neural Network，图神经网络）生成各节点的 Embedding，再输入推荐模型。这里提到的不同的特征处理方法我们都会在之后的课程中详细来讲。

4. 内容类数据

内容类数据可以看作属性标签型特征的延伸，同样是描述物品或用户的数据，但相比标签类特征，内容类数据往往是大段的描述型文字、图片，甚至视频。

一般来说，内容类数据无法直接转换成推荐系统可以“消化”的特征，需要通过自然语言处理、计算机视觉等技术手段提取关键内容特征，再输入推荐系统。例如，在图片类、视频类或是带有图片的信息流推荐场景中，我们往往会利用计算机视觉模型进行目标检测，抽取图片特征，再把这些特征（要素）转换成标签类数据供推荐系统使用。

图7 利用计算机视觉模型进行目标检测，抽取图片特征

而文字信息则更多是通过自然语言处理的方法提取关键词、主题、分类等信息，一旦这些特征被提取出来，就跟处理属性、标签类特征的方法一样，通过 Multi-hot 编码，Embedding 等方式输入推荐系统进行训练。

5. 场景信息（上下文信息）

最后一大类是场景信息，或称为上下文信息（Context），它是描述推荐行为产生的场景的信息。最常用的上下文信息是“时间”和通过 GPS、IP 地址获得的“地点”信息。根据推荐场景的不同，上下文信息的范围极广，除了我们上面提到的时间和地点，还包括“当前所处推荐页面”“季节”“月份”“是否节假日”“天气”“空气质量”“社会大事件”等等。

场景特征描述的是用户所处的客观的推荐环境，广义上来讲，任何影响用户决定的因素都可以当作是场景特征的一部分。但在实际的推荐系统应用中，由于一些特殊场景特征的获取极其困难，我们更多还是利用时间、地点、推荐页面这些易获取的场景特征。

小结

这节课我们一起进入推荐系统中一个非常重要的模块，特征工程模块的学习。推荐系统中可用的特征非常多，但它们基本上可被划分到“用户行为”“用户关系”“属性标签”“内容数据”“场景信息”这五个类别，而且挑选特征的方法也遵循着“保留有用信息，摒弃冗余信息”的原则。

就像本节开头说的一样，特征工程是准备食材的过程，准备食材的好坏直接影响到能不能做出好菜。同时，要准备的食材也和我们要做什么菜紧密相连。所以针对不同的推荐系统，我们也要针对它们的业务特点，因地制宜地挑选合适的特征，抓住业务场景中的关键信息。这才是特征工程中不变的准则，以及我们应该在工作中不断积累的业务经验。

从工程的角度来说，除了特征的挑选，特征工程还包括大量的数据预处理、特征转换、特征筛选等工作，下节课我们就一起学习一下特征处理的主要方法，提升一下我们“处理食材”的技巧！

课后思考

如果你是一名音乐 APP 的用户，你觉得在选歌的时候，有哪些信息是影响你做决定的关键信息？那如果再站在音乐 APP 工程师的角度，你觉得有哪些关键信息是可以被用来提取特征的，哪些是很难被工程化的？

欢迎在留言区畅所欲言，留下你的思考和疑惑。如果今天的内容你都学会了，那不妨也把这节课转发出去。今天的内容就到这里了，我们下节课见！

05｜特征处理：如何利用Spark解决特征处理问题？

你好，我是王喆。

上节课，我们知道了推荐系统要使用的常用特征有哪些。但这些原始的特征是无法直接提供给推荐模型使用的，因为推荐模型本质上是一个函数，输入输出都是数字或数值型的向量。那么问题来了，像动作、喜剧、爱情、科幻这些电影风格，是怎么转换成数值供推荐模型使用的呢？用户的行为历史又是怎么转换成数值特征的呢？

而且，类似的特征处理过程在数据量变大之后还会变得更加复杂，因为工业界的数据集往往都是 TB 甚至 PB 规模的，这在单机上肯定是没法处理的。那业界又是怎样进行海量数据的特征处理呢？这节课，我就带你一起来解决这几个问题。

业界主流的大数据处理利器 -Spark

既然要处理海量数据，那选择哪个数据处理平台就是我们首先要解决的问题。如果我们随机采访几位推荐系统领域的程序员，问他们在公司用什么平台处理大数据，我想最少有一半以上会回答是 Spark。作为业界主流的大数据处理利器，Spark 的地位毋庸置疑。所以，今天我先带你了解一下 Spark 的特点，再一起来看怎么用 Spark 处理推荐系统的特征。

Spark 是一个分布式计算平台。所谓分布式，指的是计算节点之间不共享内存，需要通过网络通信的方式交换数据。Spark 最典型的应用方式就是建立在大量廉价的计算节点上，这些节点可以是廉价主机，也可以是虚拟的 docker container（Docker 容器）。

理解了 Spark 的基本概念，我们来看看它的架构。从下面 Spark 的架构图中我们可以看到，Spark 程序由 Manager node（管理节点）进行调度组织，由 Worker Node（工作节点）进行具体的计算任务执行，最终将结果返回给 Drive Program（驱动程序）。在物理的 worker node 上，数据还会分为不同的 partition（数据分片），可以说 partition 是 Spark 的基础数据单元。

图1 Spark架构图

Spark 计算集群能够比传统的单机高性能服务器具备更强大的计算能力，就是由这些成百上千，甚至达到万以上规模的工作节点并行工作带来的。

那在执行一个具体任务的时候，Spark 是怎么协同这么多的工作节点，通过并行计算得出最终的结果呢？这里我们用一个任务来解释一下 Spark 的工作过程。

这个任务并不复杂，我们需要先从本地硬盘读取文件 textFile，再从分布式文件系统 HDFS 读取文件 hadoopFile，然后分别对它们进行处理，再把两个文件按照 ID 都 join 起来得到最终的结果。

这里你没必要执着于任务的细节，只要清楚任务的大致流程就好。在 Spark 平台上处理这个任务的时候，它会将这个任务拆解成一个子任务 DAG（Directed Acyclic Graph，有向无环图），再根据 DAG 决定程序各步骤执行的方法。从图 2 中我们可以看到，这个 Spark 程序分别从 textFile 和 hadoopFile 读取文件，再经过一系列 map、filter 等操作后进行 join，最终得到了处理结果。

图2 某Spark程序的任务有向无环图

其中，最关键的过程是我们要理解哪些是可以纯并行处理的部分，哪些是必须 shuffle（混洗）和 reduce 的部分。

这里的 shuffle 指的是所有 partition 的数据必须进行洗牌后才能得到下一步的数据，最典型的操作就是图 2 中的 groupByKey 操作和 join 操作。以 join 操作为例，我们必须对 textFile 数据和 hadoopFile 数据做全量的匹配才可以得到 join 后的 dataframe（Spark 保存数据的结构）。而 groupByKey 操作则需要对数据中所有相同的 key 进行合并，也需要全局的 shuffle 才能完成。

与之相比，map、filter 等操作仅需要逐条地进行数据处理和转换，不需要进行数据间的操作，因此各 partition 之间可以完全并行处理。

此外，在得到最终的计算结果之前，程序需要进行 reduce 的操作，从各 partition 上汇总统计结果，随着 partition 的数量逐渐减小，reduce 操作的并行程度逐渐降低，直到将最终的计算结果汇总到 master 节点（主节点）上。可以说，shuffle 和 reduce 操作的触发决定了纯并行处理阶段的边界。

图3 被shuffle操作分割的DAG stages

除此之外，我还想强调的是，shuffle 操作需要在不同计算节点之间进行数据交换，非常消耗计算、通信及存储资源，因此 shuffle 操作是 spark 程序应该尽量避免的。

说了这么多，这里我们再用一句话总结 Spark 的计算过程：Stage 内部数据高效并行计算，Stage 边界处进行消耗资源的 shuffle 操作或者最终的 reduce 操作。

清楚了 Spark 的原理，相信你已经摩拳擦掌期待将 Spark 应用在推荐系统的特征处理上了。下面，我们就进入实战阶段，用 Spark 处理我们的 Sparrow Recsys 项目的数据集。在开始学习之前，我希望你能带着这 2 个问题，边学边思考： 经典的特征处理方法有什么？以及 Spark 是如何实现这些特征处理方法的？

如何利用 One-hot 编码处理类别型特征

广义上来讲，所有的特征都可以分为两大类。第一类是类别、ID 型特征（以下简称类别型特征）。拿电影推荐来说，电影的风格、ID、标签、导演演员等信息，用户看过的电影 ID、用户的性别、地理位置信息、当前的季节、时间（上午，下午，晚上）、天气等等，这些无法用数字表示的信息全都可以被看作是类别、ID 类特征。第二类是数值型特征。简单来说就是能用数字直接表示的特征就是数值型特征，典型的包括用户的年龄、收入、电影的播放时长、点击量、点击率等。

我们进行特征处理的目的，是把所有的特征全部转换成一个数值型的特征向量，对于数值型特征，这个过程非常简单，直接把这个数值放到特征向量上相应的维度上就可以了。但是对于类别、ID 类特征，我们应该怎么处理它们呢？

这里我们就要用到 One-hot 编码（也被称为独热编码），它是将类别、ID 型特征转换成数值向量的一种最典型的编码方式。它通过把所有其他维度置为 0，单独将当前类别或者 ID 对应的维度置为 1 的方式生成特征向量。这怎么理解呢？我们举例来说，假设某样本有三个特征，分别是星期、性别和城市，我们用 [Weekday=Tuesday, Gender=Male, City=London] 来表示，用 One-hot 编码对其进行数值化的结果。

图4 One-hot编码特征向量

从图 4 中我们可以看到，Weekday 这个特征域有 7 个维度，Tuesday 对应第 2 个维度，所以我把对应维度置为 1。而 Gender 分为 Male 和 Female，所以对应的 One-hot 编码就有两个维度，City 特征域同理。

除了这些类别型特征外，ID 型特征也经常使用 One-hot 编码。比如，在我们的 SparrowRecsys 中，用户 U 观看过电影 M，这个行为是一个非常重要的用户特征，那我们应该如何向量化这个行为呢？其实也是使用 One-hot 编码。假设，我们的电影库中一共有 1000 部电影，电影 M 的 ID 是 310（编号从 0 开始），那这个行为就可以用一个 1000 维的向量来表示，让第 310 维的元素为 1，其他元素都为 0。

下面，我们就看看 SparrowRecsys 是如何利用 Spark 完成这一过程的。这里，我们使用 Spark 的机器学习库 MLlib 来完成 One-hot 特征的处理。

其中，最主要的步骤是，我们先创建一个负责 One-hot 编码的转换器，OneHotEncoderEstimator，然后通过它的 fit 函数完成指定特征的预处理，并利用 transform 函数将原始特征转换成 One-hot 特征。实现思路大体上就是这样，具体的步骤你可以参考我下面给出的源码：

def oneHotEncoderExample(samples:DataFrame): Unit ={
  //samples样本集中的每一条数据代表一部电影的信息，其中movieId为电影id
  val samplesWithIdNumber = samples.withColumn("movieIdNumber", col("movieId").cast(sql.types.IntegerType))

  //利用Spark的机器学习库Spark MLlib创建One-hot编码器
  val oneHotEncoder = new OneHotEncoderEstimator()
    .setInputCols(Array("movieIdNumber"))
    .setOutputCols(Array("movieIdVector"))
    .setDropLast(false)

  //训练One-hot编码器，并完成从id特征到One-hot向量的转换
  val oneHotEncoderSamples =      oneHotEncoder.fit(samplesWithIdNumber).transform(samplesWithIdNumber)
  //打印最终样本的数据结构
  oneHotEncoderSamples.printSchema()
  //打印10条样本查看结果
  oneHotEncoderSamples.show(10)

_（参考 com.wzhe.sparrowrecsys.offline.spark.featureeng.FeatureEngineering__中的oneHotEncoderExample函数）_

One-hot 编码也可以自然衍生成 Multi-hot 编码（多热编码）。比如，对于历史行为序列类、标签特征等数据来说，用户往往会与多个物品产生交互行为，或者一个物品被打上多个标签，这时最常用的特征向量生成方式就是把其转换成 Multi-hot 编码。在 SparrowRecsys 中，因为每个电影都是有多个 Genre（风格）类别的，所以我们就可以用 Multi-hot 编码完成标签到向量的转换。你可以自己尝试着用 Spark 实现该过程，也可以参考 SparrowRecsys 项目中 multiHotEncoderExample 的实现，我就不多说啦。

数值型特征的处理 - 归一化和分桶

下面，我们再好好聊一聊数值型特征的处理。你可能会问了，数值型特征本身不就是数字吗？直接放入特征向量不就好了，为什么还要处理呢？

实际上，我们主要讨论两方面问题，一是特征的尺度，二是特征的分布。

特征的尺度问题不难理解，比如在电影推荐中有两个特征，一个是电影的评价次数 fr，一个是电影的平均评分 fs。评价次数其实是一个数值无上限的特征，在 SparrowRecsys 所用的 movieLens 数据集上，fr 的范围一般在[0,10000]之间。对于电影的平均评分来说，因为我们采用了 5 分为满分的评分，所以特征 fs 的取值范围在[0,5]之间。

由于 fr 和 fs 两个特征的尺度差距太大，如果我们把特征的原始数值直接输入推荐模型，就会导致这两个特征对于模型的影响程度有显著的区别。如果模型中未做特殊处理的话，fr 这个特征由于波动范围高出 fs 几个量级，可能会完全掩盖 fs 作用，这当然是我们不愿意看到的。为此我们希望把两个特征的尺度拉平到一个区域内，通常是[0,1]范围，这就是所谓归一化。

归一化虽然能够解决特征取值范围不统一的问题，但无法改变特征值的分布。比如图 5 就显示了 Sparrow Recsys 中编号在前 1000 的电影平均评分分布。你可以很明显地看到，由于人们打分有“中庸偏上”的倾向，因此评分大量集中在 3.5 的附近，而且越靠近 3.5 的密度越大。这对于模型学习来说也不是一个好的现象，因为特征的区分度并不高。

图5 电影的平均评分分布

这该怎么办呢？我们经常会用分桶的方式来解决特征值分布极不均匀的问题。所谓“分桶（Bucketing）”，就是将样本按照某特征的值从高到低排序，然后按照桶的数量找到分位数，将样本分到各自的桶中，再用桶 ID 作为特征值。

在 Spark MLlib 中，分别提供了两个转换器 MinMaxScaler 和 QuantileDiscretizer，来进行归一化和分桶的特征处理。它们的使用方法和之前介绍的 OneHotEncoderEstimator 一样，都是先用 fit 函数进行数据预处理，再用 transform 函数完成特征转换。下面的代码就是 SparrowRecSys 利用这两个转换器完成特征归一化和分桶的过程。

def ratingFeatures(samples:DataFrame): Unit ={
  samples.printSchema()
  samples.show(10)

  //利用打分表ratings计算电影的平均分、被打分次数等数值型特征
  val movieFeatures = samples.groupBy(col("movieId"))
    .agg(count(lit(1)).as("ratingCount"),
      avg(col("rating")).as("avgRating"),
      variance(col("rating")).as("ratingVar"))
      .withColumn("avgRatingVec", double2vec(col("avgRating")))

  movieFeatures.show(10)

  //分桶处理，创建QuantileDiscretizer进行分桶，将打分次数这一特征分到100个桶中
  val ratingCountDiscretizer = new QuantileDiscretizer()
    .setInputCol("ratingCount")
    .setOutputCol("ratingCountBucket")
    .setNumBuckets(100)

  //归一化处理，创建MinMaxScaler进行归一化，将平均得分进行归一化
  val ratingScaler = new MinMaxScaler()
    .setInputCol("avgRatingVec")
    .setOutputCol("scaleAvgRating")

  //创建一个pipeline，依次执行两个特征处理过程
  val pipelineStage: Array[PipelineStage] = Array(ratingCountDiscretizer, ratingScaler)
  val featurePipeline = new Pipeline().setStages(pipelineStage)

  val movieProcessedFeatures = featurePipeline.fit(movieFeatures).transform(movieFeatures)
  //打印最终结果
  movieProcessedFeatures.show(

_（参考 com.wzhe.sparrowrecsys.offline.spark.featureeng.FeatureEngineering中的ratingFeatures函数）_

当然，对于数值型特征的处理方法还远不止于此，在经典的 YouTube 深度推荐模型中，我们就可以看到一些很有意思的处理方法。比如，在处理观看时间间隔（time since last watch）和视频曝光量（#previous impressions）这两个特征的时，Youtube 模型对它们进行归一化后，又将它们各自处理成了三个特征（图 6 中红框内的部分），分别是原特征值 x，特征值的平方x^2，以及特征值的开方，这又是为什么呢？

图6 YouTube推荐模型（来源：Deep Neural Networks for YouTube Recommendations）

其实，无论是平方还是开方操作，改变的还是这个特征值的分布，这些操作与分桶操作一样，都是希望通过改变特征的分布，让模型能够更好地学习到特征内包含的有价值信息。但由于我们没法通过人工的经验判断哪种特征处理方式更好，所以索性把它们都输入模型，让模型来做选择。

这里其实自然而然地引出了我们进行特征处理的一个原则，就是特征处理并没有标准答案，不存在一种特征处理方式是一定好于另一种的。在实践中，我们需要多进行一些尝试，找到那个最能够提升模型效果的一种或一组处理方式。

小结

这节课我们介绍了推荐系统中特征处理的主要方式，并利用 Spark 实践了类别型特征和数值型特征的主要处理方法，最后我们还总结出了特征处理的原则，“特征处理没有标准答案，需要根据模型效果实践出真知”。

针对特征处理的方法，深度学习和传统机器学习的区别并不大，TensorFlow、Pytorch 等深度学习平台也提供了类似的特征处理函数。在今后的推荐模型章节我们会进一步用到这些方法。

最后，我把这节课的主要知识点总结成了一张表格，你可以利用它巩固今天的重点知识。

这节课是我们的第一堂实战课，对于还未进入到工业界的同学，相信通过这节课的实践，也能够一窥业界的大数据处理方法，增强自己的工程经验，让我们一起由此迈入工业级推荐系统的大门吧！

课后思考

请你查阅一下 Spark MLlib 的编程手册，找出 Normalizer、StandardScaler、RobustScaler、MinMaxScaler 这个几个特征处理方法有什么不同。

你能试着运行一下 SparrowRecSys 中的 FeatureEngineering 类，从输出的结果中找出，到底哪一列是我们处理好的 One-hot 特征和 Multi-hot 特征吗？以及这两个特征是用 Spark 中的什么数据结构来表示的呢？

这就是我们这节课的全部内容了，你掌握得怎么样？欢迎你把这节课转发出去。下节课我们将讲解一种更高阶的特征处理方法，它同时也是深度学习知识体系中一个非常重要的部分，我们到时候见！

06｜Embedding：所有人都在谈的Embedding技术到底是什么？

你好，我是王喆。今天我们聊聊 Embedding。

说起 Embedding，我想你肯定不会陌生，至少经常听说。事实上，Embedding 技术不仅名气大，而且用 Embedding 方法进行相似物品推荐，几乎成了业界最流行的做法，无论是国外的 facebook、Airbnb，还是在国内的阿里、美团，我们都可以看到 Embedding 的成功应用。因此，自从深度学习流行起来之后，Embedding 就成为了深度学习推荐系统方向最火热的话题之一。

但是 Embedding 这个词又不是很好理解，你甚至很难给它找出一个准确的中文翻译，如果硬是翻译成“嵌入”、“向量映射”，感觉也不知所谓。所以索性我们就还是用 Embedding 这个叫法吧。

那这项技术到底是什么，为什么它在推荐系统领域这么重要？最经典的 Embedding 方法 Word2vec 的原理细节到底啥样？这节课，我们就一起来聊聊这几个问题。

什么是 Embedding？

简单来说，Embedding 就是用一个数值向量“表示”一个对象（Object）的方法，我这里说的对象可以是一个词、一个物品，也可以是一部电影等等。但是“表示”这个词是什么意思呢？用一个向量表示一个物品，这句话感觉还是有点让人费解。

这里，我先尝试着解释一下：一个物品能被向量表示，是因为这个向量跟其他物品向量之间的距离反映了这些物品的相似性。更进一步来说，两个向量间的距离向量甚至能够反映它们之间的关系。这个解释听上去可能还是有点抽象，那我们再用两个具体的例子解释一下。

图 1 是 Google 著名的论文 word2vec 中的例子，它利用 word2vec 这个模型把单词映射到了高维空间中，每个单词在这个高维空间中的位置都非常有意思，你看图 1 左边的例子，从 king 到 queen 的向量和从 man 到 woman 的向量，无论从方向还是尺度来说它们都异常接近。这说明什么？这说明词 Embedding 向量间的运算居然能够揭示词之间的性别关系！比如 woman 这个词的词向量可以用下面的运算得出：

Embedding(woman)=Embedding(man)+[Embedding(queen)-Embedding(king)]

同样，图 1 右的例子也很典型，从 walking 到 walked 和从 swimming 到 swam 的向量基本一致，这说明词向量揭示了词之间的时态关系！

这就是 Embedding 技术的神奇之处。

图1 词向量例子

你可能会觉得词向量技术离推荐系统领域还是有一点远，那 Netflix 应用的电影 Embedding 向量方法，就是一个非常直接的推荐系统应用。从 Netflix 利用矩阵分解方法生成的电影和用户的 Embedding 向量示意图中，我们可以看出不同的电影和用户分布在一个二维的空间内，由于 Embedding 向量保存了它们之间的相似性关系，因此有了这个 Embedding 空间之后，我们再进行电影推荐就非常容易了。具体来说就是，我们直接找出某个用户向量周围的电影向量，然后把这些电影推荐给这个用户就可以了。这就是 Embedding 技术在推荐系统中最直接的应用。

图2 电影-用户向量例子

Embedding 技术对深度学习推荐系统的重要性

事实上，我一直把 Embedding 技术称作深度学习的“基础核心操作”。在推荐系统领域进入深度学习时代之后，Embedding 技术更是“如鱼得水”。那为什么 Embedding 技术对于推荐系统如此重要，Embedding 技术又在特征工程中发挥了怎样的作用呢？针对这两个问题，我主要有两点想和你深入聊聊。

首先，Embedding 是处理稀疏特征的利器。 上节课我们学习了 One-hot 编码，因为推荐场景中的类别、ID 型特征非常多，大量使用 One-hot 编码会导致样本特征向量极度稀疏，而深度学习的结构特点又不利于稀疏特征向量的处理，因此几乎所有深度学习推荐模型都会由 Embedding 层负责将稀疏高维特征向量转换成稠密低维特征向量。所以说各类 Embedding 技术是构建深度学习推荐模型的基础性操作。

其次，Embedding 可以融合大量有价值信息，本身就是极其重要的特征向量 。 相比由原始信息直接处理得来的特征向量，Embedding 的表达能力更强，特别是 Graph Embedding 技术被提出后，Embedding 几乎可以引入任何信息进行编码，使其本身就包含大量有价值的信息，所以通过预训练得到的 Embedding 向量本身就是极其重要的特征向量。

因此我们才说，Embedding 技术在深度学习推荐系统中占有极其重要的位置，熟悉并掌握各类流行的 Embedding 方法是构建一个成功的深度学习推荐系统的有力武器。这两个特点也是我们为什么把 Embedding 的相关内容放到特征工程篇的原因，因为它不仅是一种处理稀疏特征的方法，也是融合大量基本特征，生成高阶特征向量的有效手段。

经典的 Embedding 方法，Word2vec

提到 Embedding，就一定要深入讲解一下 Word2vec。它不仅让词向量在自然语言处理领域再度流行，更关键的是，自从 2013 年谷歌提出 Word2vec 以来，Embedding 技术从自然语言处理领域推广到广告、搜索、图像、推荐等几乎所有深度学习的领域，成了深度学习知识框架中不可或缺的技术点。Word2vec 作为经典的 Embedding 方法，熟悉它对于我们理解之后所有的 Embedding 相关技术和概念都是至关重要的。下面，我就给你详细讲一讲 Word2vec 的原理。

什么是 Word2vec？

Word2vec 是“word to vector”的简称，顾名思义，它是一个生成对“词”的向量表达的模型。

想要训练 Word2vec 模型，我们需要准备由一组句子组成的语料库。假设其中一个长度为 _T_ 的句子包含的词有w1,w2……wt ，并且我们假定每个词都跟其相邻词的关系最密切。

根据模型假设的不同，Word2vec 模型分为两种形式，CBOW 模型（图 3 左）和 Skip-gram 模型（图 3 右）。其中，CBOW 模型假设句子中每个词的选取都由相邻的词决定，因此我们就看到 CBOW 模型的输入是 wt 周边的词，预测的输出是 wt。Skip-gram 模型则正好相反，它假设句子中的每个词都决定了相邻词的选取，所以你可以看到 Skip-gram 模型的输入是 wt，预测的输出是 wt 周边的词。按照一般的经验，Skip-gram 模型的效果会更好一些，所以我接下来也会以 Skip-gram 作为框架，来给你讲讲 Word2vec 的模型细节。

图3 Word2vec的两种模型结构CBOW和Skip-gram

Word2vec 的样本是怎么生成的？

我们先来看看训练 Word2vec 的样本是怎么生成的。 作为一个自然语言处理的模型，训练 Word2vec 的样本当然来自于语料库，比如我们想训练一个电商网站中关键词的 Embedding 模型，那么电商网站中所有物品的描述文字就是很好的语料库。

我们从语料库中抽取一个句子，选取一个长度为 2_c_+1（目标词前后各选 _c_ 个词）的滑动窗口，将滑动窗口由左至右滑动，每移动一次，窗口中的词组就形成了一个训练样本。根据 Skip-gram 模型的理念，中心词决定了它的相邻词，我们就可以根据这个训练样本定义出 Word2vec 模型的输入和输出，输入是样本的中心词，输出是所有的相邻词。

为了方便你理解，我再举一个例子。这里我们选取了“Embedding 技术对深度学习推荐系统的重要性”作为句子样本。首先，我们对它进行分词、去除停用词的过程，生成词序列，再选取大小为 3 的滑动窗口从头到尾依次滑动生成训练样本，然后我们把中心词当输入，边缘词做输出，就得到了训练 Word2vec 模型可用的训练样本。

图4 生成Word2vec训练样本的例子

Word2vec 模型的结构是什么样的？

有了训练样本之后，我们最关心的当然是 Word2vec 这个模型的结构是什么样的。我相信，通过第 3 节课的学习，你已经掌握了神经网络的基础知识，那再理解 Word2vec 的结构就容易多了，它的结构本质上就是一个三层的神经网络（如图 5）。

图5 Word2vec模型的结构

它的输入层和输出层的维度都是 V，这个 V 其实就是语料库词典的大小。假设语料库一共使用了 10000 个词，那么 V 就等于 10000。根据图 4 生成的训练样本，这里的输入向量自然就是由输入词转换而来的 One-hot 编码向量，输出向量则是由多个输出词转换而来的 Multi-hot 编码向量，显然，基于 Skip-gram 框架的 Word2vec 模型解决的是一个多分类问题。

隐层的维度是 N，N 的选择就需要一定的调参能力了，我们需要对模型的效果和模型的复杂度进行权衡，来决定最后 N 的取值，并且最终每个词的 Embedding 向量维度也由 N 来决定。

最后是激活函数的问题，这里我们需要注意的是，隐层神经元是没有激活函数的，或者说采用了输入即输出的恒等函数作为激活函数，而输出层神经元采用了 softmax 作为激活函数。

你可能会问为什么要这样设置 Word2vec 的神经网络，以及我们为什么要这样选择激活函数呢？因为这个神经网络其实是为了表达从输入向量到输出向量的这样的一个条件概率关系，我们看下面的式子：

p(wO​∣wI​)=∑i=1V​exp(vwi​′​⊤vwI​​)exp(vwO​′​vwI​​)​

这个由输入词 WI 预测输出词 WO 的条件概率，其实就是 Word2vec 神经网络要表达的东西。我们通过极大似然的方法去最大化这个条件概率，就能够让相似的词的内积距离更接近，这就是我们希望 Word2vec 神经网络学到的。

当然，如果你对数学和机器学习的底层理论没那么感兴趣的话，也不用太深入了解这个公式的由来，因为现在大多数深度学习平台都把它们封装好了，你不需要去实现损失函数、梯度下降的细节，你只要大概清楚他们的概念就可以了。

如果你是一个理论派，其实 Word2vec 还有很多值得挖掘的东西，比如，为了节约训练时间，Word2vec 经常会采用负采样（Negative Sampling）或者分层 softmax（Hierarchical Softmax）的训练方法。关于这一点，我推荐你去阅读Word2vec Parameter Learning Explained这篇文章，相信你会找到最详细和准确的解释。

怎样把词向量从 Word2vec 模型中提取出来？

在训练完 Word2vec 的神经网络之后，可能你还会有疑问，我们不是想得到每个词对应的 Embedding 向量嘛，这个 Embedding 在哪呢？其实，它就藏在输入层到隐层的权重矩阵 WVxN 中。我想看了下面的图你一下就明白了。

图6 词向量藏在Word2vec的权重矩阵中

你可以看到，输入向量矩阵 WVxN 的每一个行向量对应的就是我们要找的“词向量”。比如我们要找词典里第 i 个词对应的 Embedding，因为输入向量是采用 One-hot 编码的，所以输入向量的第 i 维就应该是 1，那么输入向量矩阵 WVxN 中第 i 行的行向量自然就是该词的 Embedding 啦。

细心的你可能也发现了，输出向量矩阵 W‘也遵循这个道理，确实是这样的，但一般来说，我们还是习惯于使用输入向量矩阵作为词向量矩阵。

在实际的使用过程中，我们往往会把输入向量矩阵转换成词向量查找表（Lookup table，如图 7 所示）。例如，输入向量是 10000 个词组成的 one hot 向量，隐层维度是 300 维，那么输入层到隐层的权重矩阵为 10000x300 维。在转换为词向量 Lookup table 后，每行的权重即成了对应词的 Embedding 向量。如果我们把这个查找表存储到线上的数据库中，就可以轻松地在推荐物品的过程中使用 Embedding 去计算相似性等重要的特征了。

图7 Word2vec的Lookup table

Word2vec 对 Embedding 技术的奠基性意义

Word2vec 是由谷歌于 2013 年正式提出的，其实它并不完全是原创性的，学术界对词向量的研究可以追溯到 2003 年，甚至更早的时期。但正是谷歌对 Word2vec 的成功应用，让词向量的技术得以在业界迅速推广，进而使 Embedding 这一研究话题成为热点。毫不夸张地说，Word2vec 对深度学习时代 Embedding 方向的研究具有奠基性的意义。

从另一个角度来看，Word2vec 的研究中提出的模型结构、目标函数、负采样方法、负采样中的目标函数在后续的研究中被重复使用并被屡次优化。掌握 Word2vec 中的每一个细节成了研究 Embedding 的基础。从这个意义上讲，熟练掌握本节课的内容是非常重要的。

Item2Vec：Word2vec 方法的推广

在 Word2vec 诞生之后，Embedding 的思想迅速从自然语言处理领域扩散到几乎所有机器学习领域，推荐系统也不例外。既然 Word2vec 可以对词“序列”中的词进行 Embedding，那么对于用户购买“序列”中的一个商品，用户观看“序列”中的一个电影，也应该存在相应的 Embedding 方法。

图8 不同场景下的序列数据

于是，微软于 2015 年提出了 Item2Vec 方法，它是对 Word2vec 方法的推广，使 Embedding 方法适用于几乎所有的序列数据。Item2Vec 模型的技术细节几乎和 Word2vec 完全一致，只要能够用序列数据的形式把我们要表达的对象表示出来，再把序列数据“喂”给 Word2vec 模型，我们就能够得到任意物品的 Embedding 了。

Item2vec 的提出对于推荐系统来说当然是至关重要的，因为它使得“万物皆 Embedding”成为了可能。对于推荐系统来说，Item2vec 可以利用物品的 Embedding 直接求得它们的相似性，或者作为重要的特征输入推荐模型进行训练，这些都有助于提升推荐系统的效果。

小结

这节课，我们一起学习了深度学习推荐系统中非常重要的知识点，Embedding。Embedding 就是用一个数值向量“表示”一个对象的方法。通过 Embedding，我们又引出了 Word2vec，Word2vec 是生成对“词”的向量表达的模型。其中，Word2vec 的训练样本是通过滑动窗口一一截取词组生成的。在训练完成后，模型输入向量矩阵的行向量，就是我们要提取的词向量。最后，我们还学习了 Item2vec，它是 Word2vec 在任意序列数据上的推广。

我把这些重点的内容以表格的形式，总结了出来，方便你随时回顾。

这节课，我们主要对序列数据进行了 Embedding 化，那如果是图结构的数据怎么办呢？另外，有没有什么好用的工具能实现 Embedding 技术呢？接下来的两节课，我就会一一讲解图结构数据的 Embedding 方法 Graph Embedding，并基于 Spark 对它们进行实现。

课后思考

在我们通过 Word2vec 训练得到词向量，或者通过 Item2vec 得到物品向量之后，我们应该用什么方法计算他们的相似性呢？你知道几种计算相似性的方法？

如果你身边的朋友正对 Embedding 技术感到疑惑，也欢迎你把这节课分享给 TA，我们下节课再见！

07｜Embedding进阶：如何利用图结构数据生成Graph Embedding？

你好，我是王喆。

上一节课，我们一起学习了 Embedding 技术。我们知道，只要是能够被序列数据表示的物品，都可以通过 Item2vec 方法训练出 Embedding。但是，互联网的数据可不仅仅是序列数据那么简单，越来越多的数据被我们以图的形式展现出来。这个时候，基于序列数据的 Embedding 方法就显得“不够用”了。但在推荐系统中放弃图结构数据是非常可惜的，因为图数据中包含了大量非常有价值的结构信息。

那我们怎么样才能够基于图结构数据生成 Embedding 呢？这节课，我们就重点来讲讲基于图结构的 Embedding 方法，它也被称为 Graph Embedding。

互联网中有哪些图结构数据？

可能有的同学还不太清楚图结构中到底包含了哪些重要信息，为什么我们希望好好利用它们，并以它们为基础生成 Embedding？下面，我就先带你认识一下互联网中那些非常典型的图结构数据（如图 1）。

图1 互联网图结构数据

事实上，图结构数据在互联网中几乎无处不在，最典型的就是我们每天都在使用的社交网络（如图 1-a）。从社交网络中，我们可以发现意见领袖，可以发现社区，再根据这些“社交”特性进行社交化的推荐，如果我们可以对社交网络中的节点进行 Embedding 编码，社交化推荐的过程将会非常方便。

知识图谱也是近来非常火热的研究和应用方向。像图 1-b 中描述的那样，知识图谱中包含了不同类型的知识主体（如人物、地点等），附着在知识主体上的属性（如人物描述，物品特点），以及主体和主体之间、主体和属性之间的关系。如果我们能够对知识图谱中的主体进行 Embedding 化，就可以发现主体之间的潜在关系，这对于基于内容和知识的推荐系统是非常有帮助的。

还有一类非常重要的图数据就是行为关系类图数据。这类数据几乎存在于所有互联网应用中，它事实上是由用户和物品组成的“二部图”（也称二分图，如图 1c）。用户和物品之间的相互行为生成了行为关系图。借助这样的关系图，我们自然能够利用 Embedding 技术发掘出物品和物品之间、用户和用户之间，以及用户和物品之间的关系，从而应用于推荐系统的进一步推荐。

毫无疑问，图数据是具备巨大价值的，如果能将图中的节点 Embedding 化，对于推荐系统来说将是非常有价值的特征。那下面，我们就进入正题，一起来学习基于图数据的 Graph Embedding 方法。

基于随机游走的 Graph Embedding 方法，Deep Walk

我们先来学习一种在业界影响力比较大，应用也很广泛的 Graph Embedding 方法，Deep Walk，它是 2014 年由美国石溪大学的研究者提出的。它的主要思想是在由物品组成的图结构上进行随机游走，产生大量物品序列，然后将这些物品序列作为训练样本输入 Word2vec 进行训练，最终得到物品的 Embedding。因此，DeepWalk 可以被看作连接序列 Embedding 和 Graph Embedding 的一种过渡方法。下图 2 展示了 DeepWalk 方法的执行过程。

图2 DeepWalk方法的过程

接下来，我就参照图 2 中 4 个示意图，来为你详细讲解一下 DeepWalk 的算法流程。

首先，我们基于原始的用户行为序列（图 2(a)），比如用户的购买物品序列、观看视频序列等等，来构建物品关系图（图 2(b)）。从中，我们可以看出，因为用户 U1 先后购买了物品 A 和物品 B，所以产生了一条由 A 到 B 的有向边。如果后续产生了多条相同的有向边，则有向边的权重被加强。在将所有用户行为序列都转换成物品相关图中的边之后，全局的物品相关图就建立起来了。

然后，我们采用随机游走的方式随机选择起始点，重新产生物品序列（图 2©）。其中，随机游走采样的次数、长度等都属于超参数，需要我们根据具体应用进行调整。

最后，我们将这些随机游走生成的物品序列输入图 2(d) 的 Word2vec 模型，生成最终的物品 Embedding 向量。

在上述 DeepWalk 的算法流程中，唯一需要形式化定义的就是随机游走的跳转概率，也就是到达节点 vi后，下一步遍历 vi的邻接点 vj 的概率。如果物品关系图是有向有权图，那么从节点 vi跳转到节点 vj的概率定义如下：

P(vj​∣vi​)={∑j∈N+​(Vi​)​Mij​​,mij​​vj​∈N+​(vi​) 0,​e_ij∈/​ε​

其中，N+(vi) 是节点 vi所有的出边集合，Mij是节点 vi到节点 vj边的权重，即 DeepWalk 的跳转概率就是跳转边的权重占所有相关出边权重之和的比例。如果物品相关图是无向无权重图，那么跳转概率将是 (式 1) 的一个特例，即权重 Mij将为常数 1，且 N+(vi) 应是节点 vi所有“边”的集合，而不是所有“出边”的集合。

再通过随机游走得到新的物品序列，我们就可以通过经典的 word2vec 的方式生成物品 Embedding 了。当然，关于 word2vec 的细节你可以回顾上一节课的内容，这里就不再赘述了。

在同质性和结构性间权衡的方法，Node2vec

2016 年，斯坦福大学的研究人员在 DeepWalk 的基础上更进一步，他们提出了 Node2vec 模型。Node2vec 通过调整随机游走跳转概率的方法，让 Graph Embedding 的结果在网络的同质性（Homophily）和结构性（Structural Equivalence）中进行权衡。进一步可以把不同的 Embedding 输入推荐模型，让推荐系统学习到不同的网络结构特点。

我这里所说的网络的“同质性”指的是距离相近节点的 Embedding 应该尽量近似，如图 3 所示，节点 u 与其相连的节点 s1、s2、s3、s4 的 Embedding 表达应该是接近的，这就是网络“同质性“的体现。在电商网站中，同质性的物品很可能是同品类、同属性，或者经常被一同购买的物品。

而“结构性”指的是结构上相似的节点的 Embedding 应该尽量接近，比如图 3 中节点 u 和节点 s6 都是各自局域网络的中心节点，它们在结构上相似，所以它们的 Embedding 表达也应该近似，这就是“结构性”的体现。在电商网站中，结构性相似的物品一般是各品类的爆款、最佳凑单商品等拥有类似趋势或者结构性属性的物品。

图3 网络的BFS和 DFS示意图

理解了这些基本概念之后，那么问题来了，Graph Embedding 的结果究竟是怎么表达结构性和同质性的呢？

首先，为了使 Graph Embedding 的结果能够表达网络的“结构性”，在随机游走的过程中，我们需要让游走的过程更倾向于 BFS（Breadth First Search，宽度优先搜索），因为 BFS 会更多地在当前节点的邻域中进行游走遍历，相当于对当前节点周边的网络结构进行一次“微观扫描”。当前节点是“局部中心节点”，还是“边缘节点”，亦或是“连接性节点”，其生成的序列包含的节点数量和顺序必然是不同的，从而让最终的 Embedding 抓取到更多结构性信息。

而为了表达“同质性”，随机游走要更倾向于 DFS（Depth First Search，深度优先搜索）才行，因为 DFS 更有可能通过多次跳转，游走到远方的节点上。但无论怎样，DFS 的游走更大概率会在一个大的集团内部进行，这就使得一个集团或者社区内部节点的 Embedding 更为相似，从而更多地表达网络的“同质性”。

那在 Node2vec 算法中，究竟是怎样控制 BFS 和 DFS 的倾向性的呢？

其实，它主要是通过节点间的跳转概率来控制跳转的倾向性。图 4 所示为 Node2vec 算法从节点 t 跳转到节点 v 后，再从节点 v 跳转到周围各点的跳转概率。这里，你要注意这几个节点的特点。比如，节点 t 是随机游走上一步访问的节点，节点 v 是当前访问的节点，节点 x1、x2、x3是与 v 相连的非 t 节点，但节点 x1还与节点 t 相连，这些不同的特点决定了随机游走时下一次跳转的概率。

图4 Node2vec的跳转概率

这些概率我们还可以用具体的公式来表示，从当前节点 v 跳转到下一个节点 x 的概率 πvx​=αpq​(t,x)⋅ωvx​ ，其中 wvx 是边 vx 的原始权重，αpq​(t,x) 是 Node2vec 定义的一个跳转权重。到底是倾向于 DFS 还是 BFS，主要就与这个跳转权重的定义有关了。这里我们先了解一下它的精确定义，我再作进一步的解释：

αpq(t,x)=​⎩⎨⎧​p1​ 1q1​​ 如果 dtx​=0 如果 dtx​=1 如果 dtx​=2​

αpq​(t,x) 里的 dtx是指节点 t 到节点 x 的距离，比如节点 x1其实是与节点 t 直接相连的，所以这个距离 dtx就是 1，节点 t 到节点 t 自己的距离 dtt就是 0，而 x2、x3这些不与 t 相连的节点，dtx就是 2。

此外，αpq​(t,x) 中的参数 p 和 q 共同控制着随机游走的倾向性。参数 p 被称为返回参数（return parameter），p 越小，随机游走回节点 t 的可能性越大，Node2vec 就更注重表达网络的结构性。参数 q 被称为进出参数（in-out parameter），q 越小，随机游走到远方节点的可能性越大，Node2vec 更注重表达网络的同质性。反之，当前节点更可能在附近节点游走。你可以自己尝试给 p 和 q 设置不同大小的值，算一算从 v 跳转到 t、x1、x2和 x3的跳转概率。这样一来，应该就不难理解我刚才所说的随机游走倾向性的问题啦。

Node2vec 这种灵活表达同质性和结构性的特点也得到了实验的证实，我们可以通过调整 p 和 q 参数让它产生不同的 Embedding 结果。图 5 上就是 Node2vec 更注重同质性的体现，从中我们可以看到，距离相近的节点颜色更为接近，图 5 下则是更注重结构性的体现，其中结构特点相近的节点的颜色更为接近。

图5 Node2vec实验结果

毫无疑问，Node2vec 所体现的网络的同质性和结构性，在推荐系统中都是非常重要的特征表达。由于 Node2vec 的这种灵活性，以及发掘不同图特征的能力，我们甚至可以把不同 Node2vec 生成的偏向“结构性”的 Embedding 结果，以及偏向“同质性”的 Embedding 结果共同输入后续深度学习网络，以保留物品的不同图特征信息。

Embedding 是如何应用在推荐系统的特征工程中的？

到这里，我们已经学习了好几种主流的 Embedding 方法，包括序列数据的 Embedding 方法，Word2vec 和 Item2vec，以及图数据的 Embedding 方法，Deep Walk 和 Node2vec。那你有没有想过，我为什么要在特征工程这一模块里介绍 Embedding 呢？Embedding 又是是怎么应用到推荐系统中的呢？这里，我就来做一个统一的解答。

第一个问题不难回答，由于 Embedding 的产出就是一个数值型特征向量，所以 Embedding 技术本身就可以视作特征处理方式的一种。只不过与简单的 One-hot 编码等方式不同，Embedding 是一种更高阶的特征处理方法，它具备了把序列结构、网络结构、甚至其他特征融合到一个特征向量中的能力。

而第二个问题的答案有三个。Embedding 在推荐系统中的应用方式大致有三种，分别是“直接应用”、“预训练应用”和“End2End 应用”。

其中，“直接应用”最简单，就是在我们得到 Embedding 向量之后，直接利用 Embedding 向量的相似性实现某些推荐系统的功能。典型的功能有，利用物品 Embedding 间的相似性实现相似物品推荐，利用物品 Embedding 和用户 Embedding 的相似性实现“猜你喜欢”等经典推荐功能，还可以利用物品 Embedding 实现推荐系统中的召回层等。当然，如果你还不熟悉这些应用细节，也完全不用担心，我们在之后的课程中都会讲到。

“预训练应用”指的是在我们预先训练好物品和用户的 Embedding 之后，不直接应用，而是把这些 Embedding 向量作为特征向量的一部分，跟其余的特征向量拼接起来，作为推荐模型的输入参与训练。这样做能够更好的把其他特征引入进来，让推荐模型作出更为全面且准确的预测。

第三种应用叫做“End2End 应用”。看上去这是个新的名词，它的全称叫做“end to end training”，也就是端到端训练。不过，它其实并不神秘，指的是我们不预先训练 Embedding，而是把 Embedding 的训练与深度学习推荐模型结合起来，采用统一的、端到端的方式一起训练，直接得到包含 Embedding 层的推荐模型。这种方式非常流行，比如图 6 就展示了三个包含 Embedding 层的经典模型，分别是微软的 Deep Crossing，UCL 提出的 FNN 和 Google 的 Wide&Deep。它们的实现细节我们也会在后续课程里面介绍，你这里只需要了解这个概念就可以了。

图6 带有Embedding层的深度学习模型

小结

这节课我们一起学习了 Graph Embedding 的两种主要方法，分别是 Deep Walk 和 Node2vec，并且我们还总结了 Embedding 技术在深度学习推荐系统中的应用方法。

学习 Deep Walk 方法关键在于理解它的算法流程，首先，我们基于原始的用户行为序列来构建物品关系图，然后采用随机游走的方式随机选择起始点，重新产生物品序列，最后将这些随机游走生成的物品序列输入 Word2vec 模型，生成最终的物品 Embedding 向量。

而 Node2vec 相比于 Deep Walk，增加了随机游走过程中跳转概率的倾向性。如果倾向于宽度优先搜索，则 Embedding 结果更加体现“结构性”。如果倾向于深度优先搜索，则更加体现“同质性”。

最后，我们介绍了 Embedding 技术在深度学习推荐系统中的三种应用方法，“直接应用”“预训练”和“End2End 训练”。这些方法各有特点，它们都是业界主流的应用方法，随着课程的不断深入，我会带你一步一步揭开它们的面纱。

老规矩，在课程的最后，我还是用表格的方式总结了这次课的关键知识点，你可以利用它来复习巩固。

至此，我们就完成了所有 Embedding 理论部分的学习。下节课，我们再一起进入 Embedding 和 Graph Embedding 的实践部分，利用 Sparrow Recsys 的数据，使用 Spark 实现 Embedding 的训练，希望你到时能跟我一起动起手来！

课后思考

对于 Embedding 预训练和 Embedding End2End 训练两种应用方法，你能说出它们之间的优劣点吗？

你能尝试对比一下 Embedding 预训练和 Embedding End2End 训练这两种应用方法，说出它们之间的优缺点吗？

欢迎在留言区分享你的思考和答案，如果这节 Graph Embedding 的课程让你有所收获，那不妨也把这节课分享给你的朋友们，我们下节课见！

08｜Embedding实战：如何使用Spark生成Item2vec和Graph Embedding？

你好，我是王喆。

前面两节课，我们一起学习了从 Item2vec 到 Graph Embedding 的几种经典 Embedding 方法。在打好了理论基础之后，这节课就让我们从理论走向实践，看看到底如何基于 Spark 训练得到物品的 Embedding 向量。

通过特征工程部分的实践，我想你已经对 Spark 这个分布式计算平台有了初步的认识。其实除了一些基本的特征处理方法，在 Spark 的机器学习包 Spark MLlib 中，还包含了大量成熟的机器学习模型，这其中就包括我们讲过的 Word2vec 模型。基于此，这节课我们会在 Spark 平台上，完成 Item2vec 和基于 Deep Walk 的 Graph Embedding 的训练。

对其他机器学习平台有所了解的同学可能会问，TensorFlow、PyTorch 都有很强大的深度学习工具包，我们能不能利用这些平台进行 Embedding 训练呢？当然是可以的，我们也会在之后的课程中介绍 TensorFlow 并用它实现很多深度学习推荐模型。但是 Spark 作为一个原生的分布式计算平台，在处理大数据方面还是比 TensorFlow 等深度学习平台更具有优势，而且业界的很多公司仍然在使用 Spark 训练一些结构比较简单的机器学习模型，再加上我们已经用 Spark 进行了特征工程的处理，所以，这节课我们继续使用 Spark 来完成 Embedding 的实践。

首先，我们来看看怎么完成 Item2vec 的训练。

Item2vec：序列数据的处理

我们知道，Item2vec 是基于自然语言处理模型 Word2vec 提出的，所以 Item2vec 要处理的是类似文本句子，观影序列之类的序列数据。那在真正开始 Item2vec 的训练之前，我们还要先为它准备好训练用的序列数据。在 movieLens 数据集中，有一张叫 rating（评分）的数据表，里面包含了用户对看过电影的评分和评分的时间。既然时间和评分历史都有了，我们要用的观影序列自然就可以通过处理 rating 表得到啦。

图1 movieLens数据集中的rating评分表

不过，在使用观影序列编码之前，我们还要再明确两个问题。一是 movieLens 这个 rating 表本质上只是一个评分的表，不是真正的“观影序列”。但对用户来说，当然只有看过这部电影才能够评价它，所以我们几乎可以把评分序列当作是观影序列。二是我们是应该把所有电影都放到序列中，还是只放那些打分比较高的呢？

这里，我是建议对评分做一个过滤，只放用户打分比较高的电影。为什么这么做呢？我们要思考一下 Item2vec 这个模型本质上是要学习什么。我们是希望 item2vec 能够学习到物品之间的近似性。既然这样，我们当然是希望评分好的电影靠近一些，评分差的电影和评分好的电影不要在序列中结对出现。

好，那到这里我们明确了样本处理的思路，就是对一个用户来说，我们先过滤掉他评分低的电影，再把他评论过的电影按照时间戳排序。这样，我们就得到了一个用户的观影序列，所有用户的观影序列就组成了 Item2vec 的训练样本集。

那这个过程究竟该怎么在 Spark 上实现呢？其实很简单，我们只需要明白这 5 个关键步骤就可以实现了：

读取 ratings 原始数据到 Spark 平台。

用 where 语句过滤评分低的评分记录。

用 groupBy userId 操作聚合每个用户的评分记录，DataFrame 中每条记录是一个用户的评分序列。

定义一个自定义操作 sortUdf，用它实现每个用户的评分记录按照时间戳进行排序。

把每个用户的评分记录处理成一个字符串的形式，供后续训练过程使用。

具体的实现过程，我还是建议你来参考我下面给出的代码，重要的地方我也都加上了注释，方便你来理解。

def processItemSequence(sparkSession: SparkSession): RDD[Seq[String]] ={
  //设定rating数据的路径并用spark载入数据
  val ratingsResourcesPath = this.getClass.getResource("/webroot/sampledata/ratings.csv")
  val ratingSamples = sparkSession.read.format("csv").option("header", "true").load(ratingsResourcesPath.getPath)

  //实现一个用户定义的操作函数(UDF)，用于之后的排序
  val sortUdf: UserDefinedFunction = udf((rows: Seq[Row]) => {
    rows.map { case Row(movieId: String, timestamp: String) => (movieId, timestamp) }
      .sortBy { case (movieId, timestamp) => timestamp }
      .map { case (movieId, timestamp) => movieId }
  })

  //把原始的rating数据处理成序列数据
  val userSeq = ratingSamples
    .where(col("rating") >= 3.5)  //过滤掉评分在3.5一下的评分记录
    .groupBy("userId")            //按照用户id分组
    .agg(sortUdf(collect_list(struct("movieId", "timestamp"))) as "movieIds")     //每个用户生成一个序列并用刚才定义好的udf函数按照timestamp排序
    .withColumn("movieIdStr", array_join(col("movieIds"), " "))
                //把所有id连接成一个String，方便后续word2vec模型处理

  //把序列数据筛选出来，丢掉其他过程数据
  userSeq.select("movieIdStr").rdd.map(r => r.getAs[String]("movieIdStr").split(" ").toSeq)

通过这段代码生成用户的评分序列样本中，每条样本的形式非常简单，它就是电影 ID 组成的序列，比如下面就是 ID 为 11888 用户的观影序列：

296 380 344 588 593 231 595 318 480 110 253 288 47 364 377 589 410 597 539 39 160 266 350 553 337 186 736 44 158 551 293 780 353 368 858

Item2vec：模型训练

训练数据准备好了，就该进入我们这堂课的重头戏，模型训练了。手写 Item2vec 的整个训练过程肯定是一件让人比较“崩溃”的事情，好在 Spark MLlib 已经为我们准备好了方便调用的 Word2vec 模型接口。我先把训练的代码贴在下面，然后再带你一步步分析每一行代码是在做什么。

def trainItem2vec(samples : RDD[Seq[String]]): Unit ={
    //设置模型参数
    val word2vec = new Word2Vec()
    .setVectorSize(10)
    .setWindowSize(5)
    .setNumIterations(10)

  //训练模型
  val model = word2vec.fit(samples)

  //训练结束，用模型查找与item"592"最相似的20个item
  val synonyms = model.findSynonyms("592", 20)
  for((synonym, cosineSimilarity) <- synonyms) {
    println(s"$synonym $cosineSimilarity")
  }

  //保存模型
  val embFolderPath = this.getClass.getResource("/webroot/sampledata/")
  val file = new File(embFolderPath.getPath + "embedding.txt")
  val bw = new BufferedWriter(new FileWriter(file))
  var id = 0
  //用model.getVectors获取所有Embedding向量
  for (movieId <- model.getVectors.keys){
    id+=1
    bw.write( movieId + ":" + model.getVectors(movieId).mkString(" ") + "\n")
  }
  bw.close()

从上面的代码中我们可以看出，Spark 的 Word2vec 模型训练过程非常简单，只需要四五行代码就可以完成。接下来，我就按照从上到下的顺序，依次给你解析其中 3 个关键的步骤。

首先是创建 Word2vec 模型并设定模型参数。我们要清楚 Word2vec 模型的关键参数有 3 个，分别是 setVectorSize、setWindowSize 和 setNumIterations。其中，setVectorSize 用于设定生成的 Embedding 向量的维度，setWindowSize 用于设定在序列数据上采样的滑动窗口大小，setNumIterations 用于设定训练时的迭代次数。这些超参数的具体选择就要根据实际的训练效果来做调整了。

其次，模型的训练过程非常简单，就是调用模型的 fit 接口。训练完成后，模型会返回一个包含了所有模型参数的对象。

最后一步就是提取和保存 Embedding 向量，我们可以从最后的几行代码中看到，调用 getVectors 接口就可以提取出某个电影 ID 对应的 Embedding 向量，之后就可以把它们保存到文件或者其他数据库中，供其他模块使用了。

在模型训练完成后，我们再来验证一下训练的结果是不是合理。我在代码中求取了 ID 为 592 电影的相似电影。这部电影叫 Batman 蝙蝠侠，我把通过 item2vec 得到相似电影放到了下面，你可以从直观上判断一下这个结果是不是合理。

图2 通过Item2vec方法找出的电影Batman的相似电影

当然，因为 SparrowRecsys 在演示过程中仅使用了 1000 部电影和部分用户评论集，所以我们得出的结果不一定非常准确，如果你有兴趣优化这个结果，可以去 movieLens 下载全部样本进行重新训练。

Graph Embedding：数据准备

到这里，我相信你已经熟悉了 Item2vec 方法的实现。接下来，我们再来说说基于随机游走的 Graph Embedding 方法，看看如何利用 Spark 来实现它。这里，我们选择 Deep Walk 方法进行实现。

图3 Deep Walk的算法流程

在 Deep Walk 方法中，我们需要准备的最关键数据是物品之间的转移概率矩阵。图 3 是 Deep Walk 的算法流程图，转移概率矩阵表达了图 3（b）中的物品关系图，它定义了随机游走过程中，从物品 A 到物品 B 的跳转概率。所以我们首先来看一下如何利用 Spark 生成这个转移概率矩阵。

//samples 输入的观影序列样本集
def graphEmb(samples : RDD[Seq[String]], sparkSession: SparkSession): Unit ={
  //通过flatMap操作把观影序列打碎成一个个影片对
  val pairSamples = samples.flatMap[String]( sample => {
    var pairSeq = Seq[String]()
    var previousItem:String = null
    sample.foreach((element:String) => {
      if(previousItem != null){
        pairSeq = pairSeq :+ (previousItem + ":" + element)
      }
      previousItem = element
    })
    pairSeq
  })
  //统计影片对的数量
  val pairCount = pairSamples.countByValue()
  //转移概率矩阵的双层Map数据结构
  val transferMatrix = scala.collection.mutable.Map[String, scala.collection.mutable.Map[String, Long]]()
  val itemCount = scala.collection.mutable.Map[String, Long]()

  //求取转移概率矩阵
  pairCount.foreach( pair => {
    val pairItems = pair._1.split(":")
    val count = pair._2
    lognumber = lognumber + 1
    println(lognumber, pair._1)

    if (pairItems.length == 2){
      val item1 = pairItems.apply(0)
      val item2 = pairItems.apply(1)
      if(!transferMatrix.contains(pairItems.apply(0))){
        transferMatrix(item1) = scala.collection.mutable.Map[String, Long]()
      }

      transferMatrix(item1)(item2) = count
      itemCount(item1) = itemCount.getOrElse[Long](item1, 0) + count
    }

生成转移概率矩阵的函数输入是在训练 Item2vec 时处理好的观影序列数据。输出的是转移概率矩阵，由于转移概率矩阵比较稀疏，因此我没有采用比较浪费内存的二维数组的方法，而是采用了一个双层 map 的结构去实现它。比如说，我们要得到物品 A 到物品 B 的转移概率，那么 transferMatrix(itemA)(itemB) 就是这一转移概率。

在求取转移概率矩阵的过程中，我先利用 Spark 的 flatMap 操作把观影序列“打碎”成一个个影片对，再利用 countByValue 操作统计这些影片对的数量，最后根据这些影片对的数量求取每两个影片之间的转移概率。

在获得了物品之间的转移概率矩阵之后，我们就可以进入图 3（c）的步骤，进行随机游走采样了。

Graph Embedding：随机游走采样过程

随机游走采样的过程是利用转移概率矩阵生成新的序列样本的过程。这怎么理解呢？首先，我们要根据物品出现次数的分布随机选择一个起始物品，之后就进入随机游走的过程。在每次游走时，我们根据转移概率矩阵查找到两个物品之间的转移概率，然后根据这个概率进行跳转。比如当前的物品是 A，从转移概率矩阵中查找到 A 可能跳转到物品 B 或物品 C，转移概率分别是 0.4 和 0.6，那么我们就按照这个概率来随机游走到 B 或 C，依次进行下去，直到样本的长度达到了我们的要求。

根据上面随机游走的过程，我用 Scala 进行了实现，你可以参考下面的代码，在关键的位置我也给出了注释：

//随机游走采样函数
//transferMatrix 转移概率矩阵
//itemCount 物品出现次数的分布
def randomWalk(transferMatrix : scala.collection.mutable.Map[String, scala.collection.mutable.Map[String, Long]], itemCount : scala.collection.mutable.Map[String, Long]): Seq[Seq[String]] ={
  //样本的数量
  val sampleCount = 20000
  //每个样本的长度
  val sampleLength = 10
  val samples = scala.collection.mutable.ListBuffer[Seq[String]]()

  //物品出现的总次数
  var itemTotalCount:Long = 0
  for ((k,v) <- itemCount) itemTotalCount += v

  //随机游走sampleCount次，生成sampleCount个序列样本
  for( w <- 1 to sampleCount) {
    samples.append(oneRandomWalk(transferMatrix, itemCount, itemTotalCount, sampleLength))
  }

  Seq(samples.toList : _*)
}

//通过随机游走产生一个样本的过程
//transferMatrix 转移概率矩阵
//itemCount 物品出现次数的分布
//itemTotalCount 物品出现总次数
//sampleLength 每个样本的长度
def oneRandomWalk(transferMatrix : scala.collection.mutable.Map[String, scala.collection.mutable.Map[String, Long]], itemCount : scala.collection.mutable.Map[String, Long], itemTotalCount:Long, sampleLength:Int): Seq[String] ={
  val sample = scala.collection.mutable.ListBuffer[String]()

  //决定起始点
  val randomDouble = Random.nextDouble()
  var firstElement = ""
  var culCount:Long = 0
  //根据物品出现的概率，随机决定起始点
  breakable { for ((item, count) <- itemCount) {
    culCount += count
    if (culCount >= randomDouble * itemTotalCount){
      firstElement = item
      break
    }
  }}

  sample.append(firstElement)
  var curElement = firstElement
  //通过随机游走产生长度为sampleLength的样本
  breakable { for( w <- 1 until sampleLength) {
    if (!itemCount.contains(curElement) || !transferMatrix.contains(curElement)){
      break
    }
    //从curElement到下一个跳的转移概率向量
    val probDistribution = transferMatrix(curElement)
    val curCount = itemCount(curElement)
    val randomDouble = Random.nextDouble()
    var culCount:Long = 0
    //根据转移概率向量随机决定下一跳的物品
    breakable { for ((item, count) <- probDistribution) {
      culCount += count
      if (culCount >= randomDouble * curCount){
        curElement = item
        break
      }
    }}
    sample.append(curElement)
  }}
  Seq(sample.toList : _

通过随机游走产生了我们训练所需的 sampleCount 个样本之后，下面的过程就和 Item2vec 的过程完全一致了，就是把这些训练样本输入到 Word2vec 模型中，完成最终 Graph Embedding 的生成。你也可以通过同样的方法去验证一下通过 Graph Embedding 方法生成的 Embedding 的效果。

小结

这节课，我们运用 Spark 实现了经典的 Embedding 方法 Item2vec 和 Deep Walk。它们的理论知识你应该已经在前两节课的学习中掌握了，这里我就总结一下实践中应该注意的几个要点。

关于 Item2vec 的 Spark 实现，你应该注意的是训练 Word2vec 模型的几个参数 VectorSize、WindowSize、NumIterations 等，知道它们各自的作用。它们分别是用来设置 Embedding 向量的维度，在序列数据上采样的滑动窗口大小，以及训练时的迭代次数。

而在 Deep Walk 的实现中，我们应该着重理解的是，生成物品间的转移概率矩阵的方法，以及通过随机游走生成训练样本过程。

最后，我还是把这节课的重点知识总结在了一张表格中，希望能帮助你进一步巩固。

这里，我还想再多说几句。这节课，我们终于看到了深度学习模型的产出，我们用 Embedding 方法计算出了相似电影！对于我们学习这门课来说，它完全可以看作是一个里程碑式的进步。接下来，我希望你能总结实战中的经验，跟我继续同行，一起迎接未来更多的挑战！

课后思考

上节课，我们在讲 Graph Embedding 的时候，还介绍了 Node2vec 方法。你能尝试在 Deep Walk 代码的基础上实现 Node2vec 吗？这其中，我们应该着重改变哪部分的代码呢？

欢迎把你的思考和答案写在留言区，如果你掌握了 Embedding 的实战方法，也不妨把它分享给你的朋友吧，我们下节课见！

答疑 | 基础架构篇+特征工程篇常见问题解答

你好，我是王喆。

到今天为止，基础架构篇和特征工程篇我们都学完了。这段时间，我收到了同学们的很多留言，也看到了大家在学习和实践过程中的很多疑问。今天这节课，我挑了 10 道典型的问题，想和你好好讨论一下，希望可以帮助你解决困惑。

实战项目安装、操作类的问题

我们在第 2 节课讲了 SparrowRecys 项目的安装方法，不过，我在留言区看到，大家在安装的时候，还是会遇到很多问题。这里我整理出了两类典型的问题，我们一起看看。

问题 1：因为没有项目经验，想知道把 SparrowRecys 项目 git clone 到本地之后，怎么运行这个 Maven project？

这里我再重新描述一下整个安装和执行的过程，详细来说一共有 6 步：

1. 安装 IDEA。到这个地址下载 IDE，安装 IDEA 后，打开 IDEA；

2. 在 IDEA 中打开项目。选择 File->Open-> 选择 git clone 到的项目根目录，就可以把项目导入到 IDEA；

3. 配置 maven project。我们在 IDEA 的项目结构树的 pom.xml 上点击右键，设置为 maven project（最新的 IDE 版本也可能不用）就可以了；

4. 配置 SDK。SparrowRecsys 使用了 Java8，Scala2.11 的编译环境，你可以在 File->Project Structure->Project 中配置 Java SDK，并在 Global Libraries 中配置 Scala SDK；

5. 运行推荐服务器。我们找到类文件 class RecSysServer（com.wzhe.sparrowrecsys.online.RecSysServer），右键点击 -> run；

6. 打开 SparrowRecsys 首页，在浏览器中输入http://localhost:6010/ ，当看到 SparrowRecSys 首页的时候，就说明你整个配置和安装成功了。

问题 2：在项目中没有找到“为你推荐页”，也没有看到一些项目介绍中提到的推荐算法，是我安装过程中出错了吗？

这里我再强调说明一下，你没有安装错，SparrowRecsys 这个项目是随着课程的进展逐渐完善起来的。所以如果在你学习的时候课程还未完结，SparrowRecsys 中可能会缺少课程还未进行到的模块。比如为你推荐这个功能是在课程的“推荐模型篇”中加入的，所以具体的内容我也会在之后的课程中再加入。但课程已经讲解过或提到过的部分，一定会在 SparrowRecsys 中有对应的实践代码。

课程相关的知识误区

除了安装问题之外，我还发现同学们在学习的过程中对某些知识点有疑惑，那下面我就帮同学们来解答一些典型的疑惑。

问题 3：网上资料的大部分观点认为协同过滤这样的传统方法应该是在召回层，但我们课程中把协同过滤放在了排序（精排）层，这是为什么呢？

这是个好问题。我们知道，五六年前的传统推荐系统不少还在使用协同过滤作为主排序模型。但这几年它就被慢慢淘汰了，排序层变成了以深度学习推荐模型为主的复杂模型。但因为协同过滤类算法比较简单，线上计算过程也很高效，比如矩阵分解之后可以进行 embedding 快速召回，所以放在召回层也完全适用。

在这门课程中，我们总结的推荐系统架构是一个比较经典的架构，但你也没必要认为它就是无法改变的真理。在实际应用场景之中，我希望你能根据业务特点灵活运用。

问题 4：像多模态或者是通过其它预训练方法得到的向量，直接加到推荐排序模型作为特征的话，感觉没什么效果，我理解是预训练学习的目标和排序学习目标并不一致。这个问题老师是怎么看的？

首先，我觉得这是一个很好的业务实践的问题。多模态指的是在推荐系统中引入视频、图片、语音等多种不同形式的数据和特征，希望来提升推荐效果。

在实际的业务应用里，确实存在多模态特征效果不强的问题。结合我的实践经验，我会觉得问题根源还是因为目前多模态的技术本质上还处于比较初期的阶段。

比如用一些 CV 的技术去处理视频图像，识别出一些物品，比如视频里有汽车、有树木、有人物之类。但你要说这些物品对于最终的推荐效果到底有没有影响，比如说视频中出现汽车到底对用户的点击率影响有多大，我觉得还是比较微弱。在视频推荐中，这可能远不及知名演员一个要素的影响大。

当然，我一直强调所有的效果都要跟业务场景紧密结合起来，所以多模态到底有没有作用，根本无法一概而论，还是跟你的使用方法和对业务的理解强关联。比如在短视频推荐中，如果你能精确识别出视频中的明星是哪位，再用它作为推荐特征，我想肯定对最终的推荐效果有正向影响。

问题 5：对训练数据中的某项特征进行平方或者开方，是为了改变训练数据的分布。训练数据的分布被改变后，训练出来的模型岂不是不能正确拟合训练数据了？

这个也是一个常见的误区，如果你有这样的问题，说明你还没有弄明白特征的分布和训练数据的分布之间的关系。

对训练数据中的某个特征进行开方或者平方操作，本质上是改变了特征的分布，并不是训练数据的分布。特征的分布和训练数据的分布没有本质的联系，只要你不改变训练数据 label 的分布，最终预测出的结果都应该是符合数据本身分布的。因为你要预测的是 label，并不是特征本身。而且在最终的预测过程中，这些开方、平方的特征处理操作是在模型推断过程中复现的，本质上可以看作是模型的一部分，所以不存在改变数据分布的问题。

问题 6：“为了使 Graph Embedding 的结果能够表达网络的‘结构性’，在随机游走的过程中，我们需要让游走的过程更倾向于 BFS（Breadth First Search，宽度优先搜索）”。这里应该是 DFS 吧？并且同质性是使用 BFS。

这是第 7 讲中的一个知识点，这个疑问非常地常见，因为 BFS，DFS 与结构性、同质性的关系本身确实有一点反直觉。这也是我们在学习 Node2vec 模型的时候经常会有的问题，也推荐其他有疑问的同学关注一下。

在这里，我需要再强调一下，课程中的描述是完全正确的，也就是为了使 Graph Embedding 的结果能够表达网络的“结构性”，在随机游走的过程中，我们需要让游走的过程更倾向于 BFS；为了表达“同质性”，需要倾向于 DFS。我们一定要厘清它们之间的正确关系。

这里，我也直接把Node2vec 原论文中的论述贴在了文稿里，你直接参考原文，我觉得会理解得更深刻一些。

We observe that BFS and DFS strategies play a key role in producing representations that reflect either of the above equivalences.

In particular, the neighborhoods sampled by BFS lead to embeddings that correspond closely to structural equivalence.

The opposite is true for DFS which can explore larger parts of the network as it can move further away from the source node u (with sample size k being fixed).

In DFS, the sampled nodes more accurately reflect a macro-view of the neighborhood which is essential in inferring communities based on homophily.

参考译文：

我们观察到，BFS 和 DFS 策略在产生向量表达时发挥着关键的作用。特别是通过 BFS 采样得到的邻域节点使生成的相应 Embedding 更接近结构性一致。而对于 DFS 来说，情况恰恰相反，由于 DFS 可以进一步采样到远离节点 u（样本大小 k 固定）的部分，因此可以探索更大范围的网络。在 DFS 中，采样的节点可以更准确地反映邻域的宏观视图，这对于推断社区的同质性至关重要。

关于推荐系统的深入思考

解决了一些常见的知识性的疑问，我们再来看看一些关于课程具体内容的延伸思考。我觉得这些问题都提得都很好，说明同学们学习的时候都有在认真思考，同时，我也鼓励大家都带着问题来学习，把自己的思考分享出来，这也能帮助到更多的同学。

问题 7：老师，我注意到 Flink 最近更新比较频繁，号称可以做到流批一体分析，甚至 ETL 领域好像也可以用起来。那我们在设计系统架构的时候直接用 Flink 取代 Spark，让 ETL 和实时部分统一到一个架构上是否可行呢？

其实这也是大数据工程师们一直追求的批流一体的 Kappa 架构。

在 Kappa 架构的实践中，工程师们遇到的困难也不少。一是一些历史遗留问题，比如当前很多公司的数据体系大部分是建立在 Spark 基础上的，直接用 Flink 完全替代肯定有风险，所以很多公司还沿用着批流混合的 Lambda 架构。

另外是 Spark 和 Flink 发展的问题，Flink 在进化的同时 Spark 也在发展，比如 Spark 最近发展的 Structured Streaming 就是为了跟 Flink 竞争，而且 Spark 本身的社区成熟程度和这么多年的积累还是超过目前的 Flink 的，所以也难说 Flink 会完全替代 Spark。

但毫无疑问，批流一体是未来的方向，业内的工程师们也都在往这个方向努力。但我个人觉得 Spark 和 Flink 会长期共存、共同发展。

问题 8：老师，请问关于大数据数据出口的那部分内容，请问实时的用户推荐请求也是会先经过大数据处理，生成可供线上推理的数据吗？就是针对文中大数据出口的第二点。

这是第一节课的课后留言，你可以先回忆一下第一节的内容，然后再听我讲。在推荐服务器做线上推断时，实时用户请求里面包含的特征一般是直接在服务器内部提取出来的，所以肯定不需要再在数据流中走一遍。

但是线上请求数据最终还是会落盘，生成日志数据。这个过程中，一些流处理和批处理的平台会对这些数据做进一步处理，生成今后可供我们使用的特征以及训练用样本。

问题 9：王老师，在线预测的时候，模型所需的特征是直接从数据库读取，还是在线实时组装？我在想如果只是用户或者物品自身的特征的话，可以从数据库读取，但如果是用户和物品的交叉特征的话，是不是必须实时组装？

非常好的点。一般来说如果组合特征可以在线处理，最好能够在线处理，因为组合特征有组合爆炸问题，为了节约宝贵的存储资源，我们一般不直接存储。

但对于一些不得不存储的组合特征，比如用户 x 物品的曝光、点击记录，如果线上模型需要的话，还是要存储到数据库中的，因为这些特征你没办法在线组合。

问题 10：为什么深度学习的结构特点不利于稀疏特征向量的处理呢？

首先，我想说这个问题问得太好了，如果不解决这个问题，那整个 Embedding 技术的意义就没有了，所以我也希望你能好好思考一下这个问题。

一方面，如果我们深入到神经网络的梯度下降学习过程就会发现，特征过于稀疏会导致整个网络的收敛非常慢，因为每一个样本的学习只有极少数的权重会得到更新，这在样本数量有限的情况下会导致模型不收敛。另一个方面，One-hot 类稀疏特征的维度往往非常地大，可能会达到千万甚至亿的级别，如果直接连接进入深度学习网络，那整个模型的参数数量会非常庞大，这对于一般公司的算力开销都是吃不消的。

所以基于上面两个原因，我们往往先通过 Embedding 把原始稀疏特征稠密化，然后再输入复杂的深度学习网络进行训练，这相当于把原始特征向量跟上层复杂深度学习网络做一个隔离。

好了，这节课就到这里。非常感谢前 8 讲对内容有深度思考和提问的同学，你们的每个问题都很精彩。在接下来的课程中，欢迎你继续畅所欲言，把留言区这个工具好好利用起来，我们一起进步！

09｜线上服务：如何在线上提供高并发的推荐服务？

你好，我是王喆。今天开始，我们进入线上服务篇的学习。

很多同学提起推荐系统，首先想到的是那些结构“华丽”，发展迅速的推荐模型。但事实上，在一个实际的工业级推荐系统中，训练和实现推荐模型的工作量往往连一半都没有。大量的工作都发生在搭建并维护推荐服务器、模型服务模块，以及特征和模型参数数据库等线上服务部分。

同时，由于线上服务模块是直接服务用户，产生推荐结果的模块，如果一旦发生延迟增加甚至服务宕机的情况，就会产生公司级别的事故。因此毫不夸张地说，线上服务实际上是推荐系统中最关键的一个模块。

线上服务如果写得不好，不仅杂乱无章，而且难以升级维护。因此，为了让你掌握搭建起一个支持深度学习的、稳定可扩展的推荐服务的方法，在这一模块中，我们会依次来讲线上服务器、特征存储、模型服务等模块的知识。

今天，我们先聚焦线上服务器，一起搭建直接产生推荐结果的服务接口。在这个过程中，我们按照先了解、后思考、再实践的顺序，依次解决这 3 个关键问题：

一个工业级的推荐服务器内部究竟都做了哪些事情？

像阿里、字节、腾讯这样级别的公司，它们的推荐系统是怎么承接住每秒百万甚至上千万的推荐请求的？

我们自己该如何搭建一个工业级推荐服务器的雏形呢？

工业级推荐服务器的功能

首先，我们来解决第一个问题，一个工业级的推荐服务器内部究竟做了哪些事情？要回答这个问题，我们要先回到专栏的出发点，在推荐系统的技术架构图上找到推荐系统线上服务模块的位置。只有我们心中有全局，学习才能有重点。图 1 中红色的部分就是我们要详细来讲的线上服务模块。

图1 推荐系统技术架构图

可以看到，线上服务模块的功能非常繁杂，它不仅需要跟离线训练好的模型打交道，把离线模型进行上线，在线进行模型服务（Model Serving），还需要跟数据库打交道，把候选物品和离线处理好的特征载入到服务器。

而且线上服务器内部的逻辑也十分地复杂，不仅包括了一些经典的过程，比如召回层和排序层，还包括一些业务逻辑，比如照顾推荐结果多样性，流行度的一些硬性的混合规则，甚至还包括了一些 AB 测试相关的测试代码。

高并发推荐服务的整体架构

我刚才说的就是线上服务的技术框架了，可以说，想要把线上服务写好难度并不小，更何况在面对高 QPS 的压力下，事情还会变得更复杂。接下来，我们就来看第二个问题，说一说阿里、字节、腾讯这样级别的公司，使用了哪些策略来承接住每秒百万甚至是上千万推荐请求的。

说实话，想彻底讲清楚这个问题并不容易，因为大厂关于甚高并发具体的解决方案是集整个集团的技术精英打造的，而且维护一个高可用的服务集群的工作也不是一个算法工程师的主要工作方向。但这里，我还是希望你能够从宏观的角度了解高并发的主要解决方案，因为它是一个工业级推荐系统的重要组成部分，也是我们在与架构组配合工作时应有的知识储备。

宏观来讲，高并发推荐服务的整体架构主要由三个重要机制支撑，它们分别是负载均衡、缓存、推荐服务降级机制。下面，我们一一来看。

首先是负载均衡。它是整个推荐服务能够实现高可用、可扩展的基础。当推荐服务支持的业务量达到一定规模的时候，单独依靠一台服务器是不可行的，无论这台服务器的性能有多强大，都不可能独立支撑起高 QPS（Queries Per Second，每秒查询次数）的需求。这时候，我们就需要增加服务器来分担独立节点的压力。既然有多个劳动力在干活，那我们还需要一个“工头”来分配任务，以达到按能力分配和高效率分配的目的，这个“工头”就是所谓的“负载均衡服务器”。

下图就很好地展示了负载均衡的原理。我们可以看到，负载均衡服务器（Load Balancer）处在一个非常重要的位置。因此在实际工程中，负载均衡服务器也经常采用非常高效的 nginx 技术选型，甚至采用专门的硬件级负载均衡设备作为解决方案。

图2 高并发情况下的负载均衡服务器（来源：GitHub）

这个时候，有的同学可能会问，“负载均衡”解决高并发的思路是“增加劳动力”，那我们能否从“减少劳动量”的角度来解决高并发带来的负载压力呢？这是一个非常好的角度。要知道，推荐过程特别是基于深度学习的推荐过程往往是比较复杂的，进一步来说当候选物品规模比较大的时候，产生推荐列表的过程其实非常消耗计算资源，服务器的“劳动量”非常大。这个时候，我们就可以通过减少“硬算”推荐结果的次数来给推荐服务器减负，那具体怎么做呢？

比如说，当同一个用户多次请求同样的推荐服务时，我们就可以在第一次请求时把 TA 的推荐结果缓存起来，在后续请求时直接返回缓存中的结果就可以了，不用再通过复杂的推荐逻辑重新算一遍。再比如说，对于新用户来说，因为他们几乎没有行为历史的记录，所以我们可以先按照一些规则预先缓存好几类新用户的推荐列表，等遇到新用户的时候就直接返回。

因此，在一个成熟的工业级推荐系统中，合理的缓存策略甚至能够阻挡掉 90% 以上的推荐请求，大大减小推荐服务器的计算压力。

但不管是再强大的服务集群，还是再有效的缓存方案，也都有可能遭遇特殊时刻的流量洪峰或者软硬件故障。在这种特殊情况下，为了防止推荐服务彻底熔断崩溃，甚至造成相关微服务依次崩溃的“雪崩效应”，我们就要在第一时间将问题控制在推荐服务内部，而应对的最好机制就是“服务降级”。

所谓“服务降级”就是抛弃原本的复杂逻辑，采用最保险、最简单、最不消耗资源的降级服务来度过特殊时期。比如对于推荐服务来说，我们可以抛弃原本的复杂推荐模型，采用基于规则的推荐方法来生成推荐列表，甚至直接在缓存或者内存中提前准备好应对故障时的默认推荐列表，做到“0”计算产出服务结果，这些都是服务降级的可行策略。

总之，“负载均衡”提升服务能力，“缓存”降低服务压力，“服务降级”机制保证故障时刻的服务不崩溃，压力不传导，这三点可以看成是一个成熟稳定的高并发推荐服务的基石。

搭建一个工业级推荐服务器的雏形

那说了这么多，这对我们搭建一个工业级推荐服务器有什么实际帮助呢？

相信你肯定听说过一句话，算法工程师是“面试造火箭，工作拧螺丝”。说实话，这确实反映了算法岗面试的一些不合理之处，但也不是说造火箭的知识不应该掌握。要给一个火箭拧螺丝，真不是说会拧螺丝就可以了，还真是得清楚火箭的构造是什么样的，否则螺丝你是拧上了，但地方拧错了，照样会让火箭出事故。

我们刚才讲的大厂处理高并发服务的方法就是“造火箭”，理解了这些方法，我们再来看实际工作中“拧螺丝”的技巧，就能做到有的放矢。下面，我们就一起在 Sparrow Recsys 里面实践一下搭建推荐服务器的过程，看看如何一步步拧螺丝，搭建起一个可用的推荐服务器。当然，它肯定无法直接具备负载均衡这些企业级服务的能力，但我可以保证，它可以作为一个工业级推荐服务器的雏形。让你以此为起点，逐渐把它扩展成为一个成熟的推荐服务。

首先，我们要做的就是选择服务器框架。这里，我们选择的服务器框架是 Java 嵌入式服务器 Jetty。为什么我们不选择其他的服务器呢？原因有三个。

第一，相比于 Python 服务器的效率问题，以及 C++ 服务器的开发维护难度，Java 服务器在效率和开发难度上做到了一个权衡。而且互联网上有大量开源 Java 项目可以供我们直接融合调用，所以 Java 服务器开发的扩展性比较好。第二，相比 Tomcat 等其他 Java 服务器，Jetty 是嵌入式的，它更轻量级，没有过多 J2EE 的冗余功能，可以专注于建立高效的 api 推荐服务。而 Tomcat 更适用于搭建一整套的 J2EE 项目。第三，相比于基于 Nodejs、Go 这样的服务器，Java 社区更成熟和主流一些，应用范围更广。

当然，每一种技术选择都有它的优势，C++ 的效率更高，Python 更便捷，Go 的上升势头也愈发明显，我们只要清楚 Jetty 是企业级服务的选择之一就够了，我们接下来的服务器端实践也是基于 Jetty 开展的。

作为一款嵌入式服务器框架，Jetty 的最大优势是除了 Java 环境外，你不用配置任何其他环境，也不用安装额外的软件依赖，你可以直接在 Java 程序中创建对外服务的 http api，之后在 IDE 中运行或者打 Jar 包运行就可以了。下面就是我们 Sparrow Recsys 中创建推荐服务器的代码，我已经在所有关键的地方添加了注释，你可以逐句解读一下。

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 服务的过程非常简单直观，十几行代码就可以搭建起一套推荐服务。当然，推荐服务的主要业务逻辑并不在这里，而是在每个注册到 Jetty Context 中的 Servlet 服务中。这里我们用其中最简单的 Servlet 服务 MovieService，来看一看 Jetty 中的 Servlet 服务是怎么写的。

//MovieService需要继承Jetty的HttpServlet
public class MovieService extends HttpServlet {
    //实现servlet中的get method
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws IOException {
        try {
            //该接口返回json对象，所以设置json类型
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_OK);
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Access-Control-Allow-Origin", "*");

            //获得请求中的id参数，转换为movie id
            String movieId = request.getParameter("id");
            //从数据库中获取该movie的数据对象
            Movie movie = DataManager.getInstance().getMovieById(Integer.parseInt(movieId));

            if (null != movie) {
                //使用fasterxml.jackson库把movie对象转换成json对象
                ObjectMapper mapper = new ObjectMapper();
                String jsonMovie = mapper.writeValueAsString(movie);
                //返回json对象
                response.getWriter().println(jsonMovie);
            }else {
                response.getWriter().println("");
            }

        } catch (Exception e) {
            e.printStackTrace();
            response.getWriter().println("");
        }
    }

熟悉了这个 Servlet 服务，其他服务就依葫芦画瓢就可以啦。唯一的不同就是其中的业务逻辑。如果你已经从 github 上下载了 sparrow recsys 项目把它运行起来，并且在浏览器中输入 http://localhost:6010/getmovie?id=1，就可以看到 getmovie 接口的返回对象了。

小结

这节课我们既学习了怎么“造火箭”，又实践了怎么“拧螺丝”。对于一个合格的算法工程师来说，这两方面缺一不可。

“造火箭”的知识包括工业级推荐服务器的具体功能，以及实现工业级高并发推荐服务的主要机制。其中，推荐服务器的具体功能主要有：模型服务、数据库接口、推荐模块逻辑、补充业务逻辑等等，而工业级高并发推荐服务的主要机制有负载均衡、缓存和服务降级。

“拧螺丝”的技能我们也掌握了不少，我们利用 Jetty 实践并搭建起了我们 SparrowRecSys 的推荐服务接口。这个过程中，我们需要重点关注的是，每个注册到 Jetty Context 的 Servlet 服务中的主要业务逻辑，只要掌握了一个，在实际工作中我们就能举一反三了。老规矩，我今天继续用表格的形式帮你整理了这节课的主要知识点，你可以看看。

好了，推荐服务器的相关内容我就先讲到这里，下节课我会继续讲解线上服务的另一个主要的组成部分，存储模块。

课后思考

在一个高并发的推荐服务集群中，负载均衡服务器的作用至关重要，如果你是负载均衡服务器的策略设计师的话，你会怎么实现这个“工头”的调度策略，让它能够公平又高效的完成调度任务呢？（比如是按每个节点的能力分配？还是按照请求本身的什么特点来分配？如何知道什么时候应该扩展节点，什么时候应该关闭节点？）

欢迎把你的思考和答案写在留言区，也欢迎你把这节课分享给你的朋友，我们下节课见！

10｜存储模块：如何用Redis解决推荐系统特征的存储问题？

你好，我是王喆。今天，我们来解决系统特征的存储问题。

在特征工程篇我们说过，在推荐系统这个大饭馆中，特征工程就是负责配料和食材的厨师，那我们上堂课搭建的推荐服务器就是准备做菜的大厨。配料和食材准备好了，做菜的大厨也已经开火热锅了，这时候我们得把食材及时传到大厨那啊。这个传菜的过程就是推荐系统特征的存储和获取过程。

可是我们知道，类似 Embedding 这样的特征是在离线环境下生成的，而推荐服务器是在线上环境中运行的，那这些离线的特征数据是如何导入到线上让推荐服务器使用的呢？

今天，我们先以 Netflix 的推荐系统架构为例，来讲一讲存储模块在整个系统中的位置，再详细来讲推荐系统存储方案的设计原则，最后以 Redis 为核心搭建起 SparrowRecsys 的存储模块。

推荐系统存储模块的设计原则

你还记得，我曾在第 1 节的课后题中贴出过 Netflix 推荐系统的架构图（如图 1）吗？Netflix 采用了非常经典的 Offline、Nearline、Online 三层推荐系统架构。架构图中最核心的位置就是我在图中用红框标出的部分，它们是三个数据库 Cassandra、MySQL 和 EVcache，这三个数据库就是 Netflix 解决特征和模型参数存储问题的钥匙。

图1 Netflix推荐系统架构中的特征与模型数据库

你可能会觉得，存储推荐特征和模型这件事情一点儿都不难啊。不就是找一个数据库把离线的特征存起来，然后再给推荐服务器写几个 SQL 让它取出来用不就行了吗？为什么还要像 Netflix 这样兴师动众地搞三个数据库呢？

想要搞明白这个问题，我们就得搞清楚设计推荐系统存储模块的原则。对于推荐服务器来说，由于线上的 QPS 压力巨大，每次有推荐请求到来，推荐服务器都需要把相关的特征取出。这就要求推荐服务器一定要“快”。

不仅如此，对于一个成熟的互联网应用来说，它的用户数和物品数一定是巨大的，几千万上亿的规模是十分常见的。所以对于存储模块来说，这么多用户和物品特征所需的存储量会特别大。这个时候，事情就很难办了，又要存储量大，又要查询快，还要面对高 QPS 的压力。很不幸，没有一个独立的数据库能经济又高效地单独完成这样复杂的任务。

因此，几乎所有的工业级推荐系统都会做一件事情，就是把特征的存储做成分级存储，把越频繁访问的数据放到越快的数据库甚至缓存中，把海量的全量数据放到便宜但是查询速度较慢的数据库中。

举个不恰当的例子，如果你把特征数据放到基于 HDFS 的 HBase 中，虽然你可以轻松放下所有的特征数据，但要让你的推荐服务器直接访问 HBase 进行特征查询，等到查询完成，这边用户的请求早就超时中断了。而 Netflix 的三个数据库正好满足了这样分级存储的需求。

图2 分级存储的设计

比如说，Netflix 使用的 Cassandra，它作为流行的 NoSQL 数据库，具备大数据存储的能力，但为支持推荐服务器高 QPS 的需求，我们还需要把最常用的特征和模型参数存入 EVcache 这类内存数据库。而对于更常用的数据，我们可以把它们存储在 Guava Cache 等服务器内部缓存，甚至是服务器的内存中。总之，对于一个工程师来说，我们经常需要做出技术上的权衡，达成一个在花销和效果上平衡最优的技术方案。

而对于 MySQL 来说，由于它是一个强一致性的关系型数据库，一般存储的是比较关键的要求强一致性的信息，比如物品是否可以被推荐这种控制类的信息，物品分类的层级关系，用户的注册信息等等。这类信息一般是由推荐服务器进行阶段性的拉取，或者利用分级缓存进行阶段性的更新，避免因为过于频繁的访问压垮 MySQL。

总的来说，推荐系统存储模块的设计原则就是“分级存储，把越频繁访问的数据放到越快的数据库甚至缓存中，把海量的全量数据放到廉价但是查询速度较慢的数据库中”。

SparrowRecsys 的存储系统方案

那在我们要实现的 SparrowRecsys 中，存储模块的设计原则又是怎么应用的呢？

在 SparrowRecsys 中，我们把存储模块的设计问题进行了一些简化，避免由于系统设计得过于复杂导致你不易上手。

我们使用基础的文件系统保存全量的离线特征和模型数据，用 Redis 保存线上所需特征和模型数据，使用服务器内存缓存频繁访问的特征。

在实现技术方案之前，对于问题的整体分析永远都是重要的。我们需要先确定具体的存储方案，这个方案必须精确到哪级存储对应哪些具体特征和模型数据。

存储的工具已经知道了，那特征和模型数据分别是什么呢？这里，我们直接应用特征工程篇为 SparrowRecsys 准备好的一些特征就可以了。我把它们的具体含义和数据量级整理成了表格，如下：

图3 特征和模型数据

根据上面的特征数据，我们一起做一个初步的分析。首先，用户特征的总数比较大，它们很难全部载入到服务器内存中，所以我们把用户特征载入到 Redis 之类的内存数据库中是合理的。其次，物品特征的总数比较小，而且每次用户请求，一般只会用到一个用户的特征，但为了物品排序，推荐服务器需要访问几乎所有候选物品的特征。针对这个特点，我们完全可以把所有物品特征阶段性地载入到服务器内存中，大大减少 Redis 的线上压力。

最后，我们还要找一个地方去存储特征历史数据、样本数据等体量比较大，但不要求实时获取的数据。这个时候分布式文件系统（单机环境下以本机文件系统为例）往往是最好的选择，由于类似 HDFS 之类的分布式文件系统具有近乎无限的存储空间，我们可以把每次处理的全量特征，每次训练的 Embedding 全部保存到分布式文件系统中，方便离线评估时使用。

经过上面的分析，我们就得到了具体的存储方案，如下表：

图4 SparrowRecsys的存储方案

此外，文件系统的存储操作非常简单，在 SparrowRecsys 中就是利用 Spark 的输出功能实现的，我们就不再重点介绍了。而服务器内部的存储操作主要是跟 Redis 进行交互，所以接下来我们重点介绍 Redis 的特性以及写入和读取方法。

你需要知道的 Redis 基础知识

Redis 是当今业界最主流的内存数据库，那在使用它之前，我们应该清楚 Redis 的两个主要特点。

一是所有的数据都以 Key-value 的形式存储。 其中，Key 只能是字符串，value 可支持的数据结构包括 string(字符串)、list(链表)、set(集合)、zset(有序集合) 和 hash(哈希)。这个特点决定了 Redis 的使用方式，无论是存储还是获取，都应该以键值对的形式进行，并且根据你的数据特点，设计值的数据结构。

二是所有的数据都存储在内存中，磁盘只在持久化备份或恢复数据时起作用。这个特点决定了 Redis 的特性，一是 QPS 峰值可以很高，二是数据易丢失，所以我们在维护 Redis 时要充分考虑数据的备份问题，或者说，不应该把关键的业务数据唯一地放到 Redis 中。但对于可恢复，不关乎关键业务逻辑的推荐特征数据，就非常适合利用 Redis 提供高效的存储和查询服务。

在实际的 SparrowRecsys 的 Redis 部分中，我们用到了 Redis 最基本的操作，SET、GET 和 KEYS，value 的数据类型用到了 string。

SparrowRecsys 中的 Redis 部分的实践流程

Redis 的实践流程还是符合我们“把大象装冰箱”的三部曲，只不过，这三步变成了安装 Redis，把数据写进去，把数据读出来。下面，我们来逐一来讲。

首先是安装 Redis。 Redis 的安装过程在 linux/Unix 环境下非常简单，你参照官方网站的步骤依次执行就好。而 Windows 环境下的安装过程稍复杂一些，你可以参考这篇文章进行安装。

在启动 Redis 之后，如果没有特殊的设置，Redis 服务会默认运行在 6379 端口，没有特殊情况保留这个默认的设置就可以了，因为我们的 SparrowRecSys 也是默认从 6379 端口存储和读取 Redis 数据的。

然后是运行离线程序，通过 jedis 客户端写入 Redis。 在 Redis 运行起来之后，我们就可以在离线 Spark 环境下把特征数据写入 Redis。这里我们以第 8 节课中生成的 Embedding 数据为例，来实现 Redis 的特征存储过程。

实际的过程非常简单，首先我们利用最常用的 Redis Java 客户端 Jedis 生成 redisClient，然后遍历训练好的 Embedding 向量，将 Embedding 向量以字符串的形式存入 Redis，并设置过期时间（ttl）。具体实现请参考下面的代码（代码参考 com.wzhe.sparrowrecsys.offline.spark.featureeng.Embedding 中的 trainItem2vec 函数）：

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()
}

最后是在推荐服务器中把 Redis 数据读取出来。

在服务器端，根据刚才梳理出的存储方案，我们希望服务器能够把所有物品 Embedding 阶段性地全部缓存在服务器内部，用户 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 进行相似度的计算，就可以得到最终的推荐列表了。

如果你已经安装好了 Redis，我非常推荐你运行 SparrowRecsys 中 offline 部分 Embedding 主函数，先把物品和用户 Embedding 生成并且插入 Redis（注意把 saveToRedis 变量改为 true）。然后再运行 online 部分的 RecSysServer，看一下推荐服务器有没有正确地从 Redis 中读出物品和用户 Embedding 并产生正确的推荐结果（注意，记得要把 util.Config 中的 EMB_DATA_SOURCE 配置改为 DATA_SOURCE_REDIS）。

当然，除了 Redis，我们还提到了多种不同的缓存和数据库，如 Cassandra、EVcache、GuavaCache 等等，它们都是业界非常流行的存储特征的工具，你有兴趣的话也可以在课后查阅相关资料进行进一步的学习。在掌握了我们特征存储的基本原则之后，你也可以在业余时间尝试思考一下每个数据库的不同和它们最合适的应用场景。

小结

今天我们学习了推荐系统存储模块的设计原则和具体的解决方案，并且利用 SparrowRecsys 进行了实战。

在设计推荐系统存储方案时，我们一般要遵循“分级存储”的原则，在开销和性能之间取得权衡。在 SparrowRecsys 的实战中，我们安装并操作了内存数据库 Redis，你要记住 Redis 的特点“Key-value 形式存储”和“纯内存数据库”。在具体的特征存取过程中，我们应该熟悉利用 jedis 执行 SET，GET 等 Redis 常用操作的方法。

最后，我也把重要的知识点总结在了文稿中，你可以再回顾一下。

对于搭建一套完整的推荐服务来说，我们已经迈过了两大难关，分别是用 Jetty Server 搭建推荐服务器问题，以及用 Redis 解决特征存储的问题。下节课，我们会一起来挑战线上服务召回层的设计。

课后思考

你觉得课程中存储 Embedding 的方式还有优化的空间吗？除了 string，我们是不是还可以用其他 Redis value 的数据结构存储 Embedding 数据，那从效率的角度考虑，使用 string 和使用其他数据结构的优缺点有哪些？为什么？

欢迎把你的思考和答案写在留言区，也欢迎你把这节课分享给你的朋友，我们下节课见！

11｜召回层：如何快速又准确地筛选掉不相关物品？

你好，我是王喆。今天，我们来一起学习推荐系统中非常重要的一个模块，召回层。

为了弄清楚召回层是什么，我们先试着解决一下这个问题：如果你是一名快手的推荐工程师，你的任务是从 500 万个候选短视频中，为一名用户推荐 10 个他最感兴趣的。你会怎么做？

我想最直接最暴力的做法，就是对这 500 万个短视频挨个打分、排序，取出得分最高的 10 个推荐给用户。如果这个打分的算法非常靠谱的话，我们肯定能够选出用户最感兴趣的 Top 10 视频。但这个过程会涉及一个非常棘手的工程问题：如果利用比较复杂的推荐模型，特别是深度学习推荐模型，对 500 万个短视频打分，这个过程是非常消耗计算资源的。

而且你要知道，这还只是计算了一个用户的推荐结果，在工业级的线上服务中，每秒可是有几十万甚至上百万的用户同时请求服务器，逐个候选视频打分产生的计算量，是任何集群都承受不了的。

那在推荐物品候选集规模非常大的时候，我们该如何快速又准确地筛选掉不相关物品，从而节约排序时所消耗的计算资源呢？这其实就是推荐系统召回层要解决的问题。今天，我就从三个召回层技术方案入手，带你一起来解决这个问题。

召回层和排序层的功能特点

在前面的课程中我提到过学习推荐系统的一个主要原则，那就是“深入细节，不忘整体”。对于召回层，我们也应该清楚它在推荐系统架构中的位置。

从技术架构的角度来说，“召回层”处于推荐系统的线上服务模块之中，推荐服务器从数据库或内存中拿到所有候选物品集合后，会依次经过召回层、排序层、再排序层（也被称为补充算法层），才能够产生用户最终看到的推荐列表。既然线上服务需要这么多“层”才能产生最终的结果，不同层之间的功能特点有什么区别呢？

图1 推荐系统的召回和排序阶段及其特点

其实从这节课开头的问题出发，你应该已经对召回层和排序层的功能特点有了初步的认识，召回层就是要快速、准确地过滤出相关物品，缩小候选集，排序层则要以提升推荐效果为目，作出精准的推荐列表排序。

再详细一点说，我们可以从候选集规模、模型复杂程度、特征数量、处理速度、排序精度等几个角度来对比召回层和排序层的特点：

图2 召回层和排序层的特点

需要注意的是，在我们设计召回层时，计算速度和召回率其实是两个矛盾的指标。怎么理解呢？比如说，为了提高计算速度，我们需要使召回策略尽量简单，而为了提高召回率或者说召回精度，则要求召回策略能够尽量把用户感兴趣的物品囊括在内，这又要求召回策略不能过于简单，导致召回物品无法满足排序模型的要求。

推荐工程师们就是在这样的矛盾中逐渐作出新的尝试，推动着召回层的设计方案不断向前发展。下面，我们就详细学习一下三个主要的召回方法，以及它们基于 SparrowRecSys 的代码实现。

如何理解“单策略召回”方法？

你会发现，今天我多次提到一个关键字，快。那怎么才能让召回层“快”起来呢？我们知道，召回层慢的原因是模型复杂，算法计算量大，那我们能不能反其道而行之，用一些简单直观的策略来实现召回层呢？当然是可以的，这就是所谓的单策略召回。

单策略召回指的是，通过制定一条规则或者利用一个简单模型来快速地召回可能的相关物品。 这里的规则其实就是用户可能感兴趣的物品的特点，我们拿 SparrowRecSys 里面的电影推荐为例。在推荐电影的时候，我们首先要想到用户可能会喜欢什么电影。按照经验来说，很有可能是这三类，分别是大众口碑好的、近期非常火热的，以及跟我之前喜欢的电影风格类似的。

基于其中任何一条，我们都可以快速实现一个单策略召回层。比如在 SparrowRecSys 中，我就制定了这样一条召回策略：如果用户对电影 A 的评分较高，比如超过 4 分，那么我们就将与 A 风格相同，并且平均评分在前 50 的电影召回，放入排序候选集中。

基于这条规则，我实现了如下的召回层：

//详见SimilarMovieFlow class
public static List<Movie> candidateGenerator(Movie movie){
    ArrayList<Movie> candidates = new ArrayList<>();
    //使用HashMap去重
    HashMap<Integer, Movie> candidateMap = new HashMap<>();
    //电影movie包含多个风格标签
    for (String genre : movie.getGenres()){
        //召回策略的实现
        List<Movie> oneCandidates = DataManager.getInstance().getMoviesByGenre(genre, 100, "rating");
        for (Movie candidate : oneCandidates){
            candidateMap.put(candidate.getMovieId(), candidate);
        }
    }
    //去掉movie本身
    if (candidateMap.containsKey(movie.getMovieId())){
        candidateMap.remove(movie.getMovieId());
    }
    //最终的候选集
    return new ArrayList<>(candidateMap.values());
}

单策略召回是非常简单直观的，正因为简单，所以它的计算速度一定是非常快的。但我想你应该也发现了其中的问题，就是它有很强的局限性。因为大多数时候用户的兴趣是非常多元的，他们不仅喜欢自己感兴趣的，也喜欢热门的，当然很多时候也喜欢新上映的。这时候，单一策略就难以满足用户的潜在需求了，那有没有更全面的召回策略呢？

如何理解“多路召回”方法

为了让召回的结果更加全面，多路召回方法应运而生了。

所谓“多路召回策略”，就是指采用不同的策略、特征或简单模型，分别召回一部分候选集，然后把候选集混合在一起供后续排序模型使用的策略。

其中，各简单策略保证候选集的快速召回，从不同角度设计的策略又能保证召回率接近理想的状态，不至于损害排序效果。所以，多路召回策略是在计算速度和召回率之间进行权衡的结果。

这里，我们还是以电影推荐为例来做进一步的解释。下面是我给出的电影推荐中常用的多路召回策略，包括热门电影、风格类型、高分评价、最新上映以及朋友喜欢等等。除此之外，我们也可以把一些推断速度比较快的简单模型（比如逻辑回归，协同过滤等）生成的推荐结果放入多路召回层中，形成综合性更好的候选集。具体的操作过程就是，我们分别执行这些策略，让每个策略选取 Top k 个物品，最后混合多个 Top k 物品，就形成了最终的多路召回候选集。整个过程就如下所示：

图3 常见的多路召回策略

在 SparrowRecsys 中，我们就实现了由风格类型、高分评价、最新上映，这三路召回策略组成的多路召回方法，具体代码如下：

public static List<Movie> multipleRetrievalCandidates(List<Movie> userHistory){
    HashSet<String> genres = new HashSet<>();
    //根据用户看过的电影，统计用户喜欢的电影风格
    for (Movie movie : userHistory){
        genres.addAll(movie.getGenres());
    }
    //根据用户喜欢的风格召回电影候选集
    HashMap<Integer, Movie> candidateMap = new HashMap<>();
    for (String genre : genres){
        List<Movie> oneCandidates = DataManager.getInstance().getMoviesByGenre(genre, 20, "rating");
        for (Movie candidate : oneCandidates){
            candidateMap.put(candidate.getMovieId(), candidate);
        }
    }
    //召回所有电影中排名最高的100部电影
    List<Movie> highRatingCandidates = DataManager.getInstance().getMovies(100, "rating");
    for (Movie candidate : highRatingCandidates){
        candidateMap.put(candidate.getMovieId(), candidate);
    }
    //召回最新上映的100部电影
    List<Movie> latestCandidates = DataManager.getInstance().getMovies(100, "releaseYear");
    for (Movie candidate : latestCandidates){
        candidateMap.put(candidate.getMovieId(), candidate);
    }
    //去除用户已经观看过的电影
    for (Movie movie : userHistory){
        candidateMap.remove(movie.getMovieId());
    }
    //形成最终的候选集
    return new ArrayList<>(candidateMap.values());
}

在实现的过程中，为了进一步优化召回效率，我们还可以通过多线程并行、建立标签 / 特征索引、建立常用召回集缓存等方法来进一步完善它。

不过，多路召回策略虽然能够比较全面地照顾到不同的召回方法，但也存在一些缺点。比如，在确定每一路的召回物品数量时，往往需要大量的人工参与和调整，具体的数值需要经过大量线上 AB 测试来决定。此外，因为策略之间的信息和数据是割裂的，所以我们很难综合考虑不同策略对一个物品的影响。

那么，是否存在一个综合性强且计算速度也能满足需求的召回方法呢？

基于 Embedding 的召回方法

在第 5 节和第 6 节课中，我们已经介绍了多种离线生成物品 Embedding 的方案。事实上，利用物品和用户 Embedding 相似性来构建召回层，是深度学习推荐系统中非常经典的技术方案。我们可以把它的优势总结为三方面。

一方面，多路召回中使用的“兴趣标签”“热门度”“流行趋势”“物品属性”等信息都可以作为 Embedding 方法中的附加信息（Side information），融合进最终的 Embedding 向量中 。因此，在利用 Embedding 召回的过程中，我们就相当于考虑到了多路召回的多种策略。

另一方面，Embedding 召回的评分具有连续性。我们知道，多路召回中不同召回策略产生的相似度、热度等分值不具备可比性，所以我们无法据此来决定每个召回策略放回候选集的大小。但是，Embedding 召回却可以把 Embedding 间的相似度作为唯一的判断标准，因此它可以随意限定召回的候选集大小。

最后，在线上服务的过程中，Embedding 相似性的计算也相对简单和直接。通过简单的点积或余弦相似度的运算就能够得到相似度得分，便于线上的快速召回。

在 SparrowRecsys 中，我们也实现了基于 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));
}

这里，我再带你简单梳理一下整体的实现思路。总的来说，我们通过 3 步生成了最终的候选集。第一步，我们获取用户的 Embedding。第二步，我们获取所有物品的候选集，并且逐一获取物品的 Embedding，计算物品 Embedding 和用户 Embedding 的相似度。第三步，我们根据相似度排序，返回规定大小的候选集。

在这 3 步之中，最主要的时间开销在第 2 步，虽然它的时间复杂度是线性的，但当物品集过大时（比如达到了百万以上的规模），线性的运算也可能造成很大的时间开销。那有没有什么方法能进一步缩小 Embedding 召回层的运算时间呢？这个问题我们留到下节课来讨论。

小结

今天，我们一起讨论了推荐系统中召回层的功能特点和实现方法。并且重点讲解了单策略召回、多路召回，以及深度学习推荐系统中常用的基于 Embedding 的召回。

为了方便你对比它们之间的技术特点，我总结了一张表格放在了下面，你可以看一看。

总的来说，关于召回层的重要内容，我总结成了一个特点，三个方案。

特点就是召回层的功能特点：召回层要快速准确地过滤出相关物品，缩小候选集。三个方案指的是实现召回层的三个技术方案：简单快速的单策略召回、业界主流的多路召回、深度学习推荐系统中最常用的 Embedding 召回。

这三种方法基本囊括了现在业界推荐系统的主流召回方法，希望通过这节课的学习，你能掌握这一关键模块的实现方法。

相信你也一定发现了，召回层技术的发展是循序渐进的，因此我希望你不仅能够学会应用它们，更能够站在前人的技术基础上，进一步推进它的发展，这也是工程师这份职业最大的魅力。

课后思考

你能根据我今天讲的内容在 SparrowRecsys 中实现一个多线程版本的多路召回策略吗？

你觉得对于 Embedding 召回来说，我们怎么做才能提升计算 Embedding 相似度的速度？

你理解的召回层也是这样吗？欢迎把你的思考和答案写在留言区。如果有收获，我也希望你能把这节课分享给你的朋友们。

12 | 局部敏感哈希：如何在常数时间内搜索Embedding最近邻？

你好，我是王喆。

在深度学习推荐系统中，我们经常采用 Embedding 召回这一准确又便捷的方法。但是，在面对百万甚至更高量级的候选集时，线性地逐一计算 Embedding 间的相似度，往往会造成极大的服务延迟。

这个时候，我们要解决的问题就是，如何快速找到与一个 Embedding 最相似的 Embedding？这直接决定了召回层的执行速度，进而会影响推荐服务器的响应延迟。

今天，我们就一起来学习一下业界解决近似 Embedding 搜索的主要方法，局部敏感哈希。

推荐系统中的“快速”Embedding 最近邻搜索问题

在深度学习推荐系统中，我们经常会使用 Embedding 方法对物品和用户进行向量化。在训练物品和用户的 Embedding 向量时，如果二者的 Embedding 在同一个向量空间内（如图 1），我们就可以通过内积、余弦、欧式距离等相似度计算方法，来计算它们之间的相似度，从而通过用户 - 物品相似度进行个性化推荐，或者通过物品 - 物品相似度进行相似物品查找。

图1 用户和电影的Embedding向量空间

假设用户和物品的 Embeding 都在一个 k 维的 Embedding 空间中，物品总数为 n，那么遍历计算一个用户和所有物品向量相似度的时间复杂度是多少呢？不难算出是 O(k×n)。虽然这一复杂度是线性的，但物品总数 n 达到百万甚至千万量级时，线性的时间复杂度也是线上服务不能承受的。

换一个角度思考这个问题，由于用户和物品的 Embedding 同处一个向量空间内，因此召回与用户向量最相似的物品 Embedding 向量这一问题，其实就是在向量空间内搜索最近邻的过程。如果我们能够找到高维空间快速搜索最近邻点的方法，那么相似 Embedding 的快速搜索问题就迎刃而解了。

使用“聚类”还是“索引”来搜索最近邻？

遇到最近邻搜索的问题，我想大部分同学直觉上肯定会想到两种解决方案，一种是聚类，我们把相似的点聚类到一起，不就可以快速地找到彼此间的最近邻了吗？另一种是索引，比如，我们通过某种数据结构建立基于向量距离的索引，在查找最近邻的时候，通过索引快速缩小范围来降低复杂度。这两种想法可不可行呢？我们一一尝试一下。

对于聚类问题，我想最经典的算法当属 K-means。它完成聚类的过程主要有以下几步：

随机指定 k 个中心点；

每个中心点代表一个类，把所有的点按照距离的远近指定给距离最近的中心点代表的类；

计算每个类包含点的平均值作为新的中心点位置；

确定好新的中心点位置后，迭代进入第 2 步，直到中心点位置收敛，不再移动。

到这里，整个 K-means 的迭代更新过程就完成了，你可以看下图 2。

图2 三中心点的K-means算法迭代过程

如果我们能够在离线计算好每个 Embedding 向量的类别，在线上我们只需要在同一个类别内的 Embedding 向量中搜索就可以了，这会大大缩小了 Embedding 的搜索范围，时间复杂度自然就下降了。

但这个过程还是存在着一些边界情况。比如，聚类边缘的点的最近邻往往会包括相邻聚类的点，如果我们只在类别内搜索，就会遗漏这些近似点。此外，中心点的数量 k 也不那么好确定，k 选的太大，离线迭代的过程就会非常慢，k 选的太小，在线搜索的范围还是很大，并没有减少太多搜索时间。所以基于聚类的搜索还是有一定局限性的，解决上面的问题也会增加过多冗余过程，得不偿失。

既然聚类有局限性，那索引能不能奏效呢？我们这里可以尝试一下经典的向量空间索引方法 kd-tree（k-dimension tree）。与聚类不同，它是为空间中的点 / 向量建立一个索引。这该怎么理解呢？

举个例子，你可以看下图 3 中的点云，我们先用红色的线把点云一分为二，再用深蓝色的线把各自片区的点云一分为二，以此类推，直到每个片区只剩下一个点，这就完成了空间索引的构建。如果我们能够把这套索引“搬”到线上，就可以利用二叉树的结构快速找到邻接点。比如，希望找到点 q 的 m 个邻接点，我们就可以先搜索它相邻子树下的点，如果数量不够，我们可以向上回退一个层级，搜索它父片区下的其他点，直到数量凑够 m 个为止。

图3 kd-tree索引

听上去 kd-tree 索引似乎是一个完美的方案，但它还是无法完全解决边缘点最近邻的问题。对于点 q 来说，它的邻接片区是右上角的片区，但是它的最近邻点却是深蓝色切分线下方的那个点。所以按照 kd-tree 的索引方法，我们还是会遗漏掉最近邻点，它只能保证快速搜索到近似的最近邻点集合。而且 kd-tree 索引的结构并不简单，离线和在线维护的过程也相对复杂，这些都是它的弊端。

那有没有更“完美”的解决方法呢？

局部敏感哈希的基本原理及多桶策略

为了“拯救”我们推荐系统的召回层，“局部敏感哈希”（Locality Sensitive Hashing，LSH）这一方法横空出世，它用简洁而高效的方法几乎完美地解决了这一问题。那它是怎么做到的呢？我们先来看看局部敏感哈希的基本原理。

1. 局部敏感哈希的基本原理

局部敏感哈希的基本思想是希望让相邻的点落入同一个“桶”，这样在进行最近邻搜索时，我们仅需要在一个桶内，或相邻的几个桶内的元素中进行搜索即可。如果保持每个桶中的元素个数在一个常数附近，我们就可以把最近邻搜索的时间复杂度降低到常数级别。

那么，如何构建局部敏感哈希中的“桶”呢？下面，我们以基于欧式距离的最近邻搜索为例，来解释构建局部敏感哈希“桶”的过程。

首先，我们要弄清楚一个问题，如果将高维空间中的点向低维空间进行映射，其欧式相对距离是不是会保持不变呢？以图 4 为例，图 4 中间的彩色点处在二维空间中，当我们把二维空间中的点通过不同角度映射到 a、b、c 这三个一维空间时，可以看到原本相近的点，在一维空间中都保持着相近的距离。而原本远离的绿色点和红色点在一维空间 a 中处于接近的位置，却在空间 b 中处于远离的位置。

因此我们可以得出一个定性的结论：欧式空间中，将高维空间的点映射到低维空间，原本接近的点在低维空间中肯定依然接近，但原本远离的点则有一定概率变成接近的点。

图4 高维空间点向低维空间映射

利用低维空间可以保留高维空间相近距离关系的性质，我们就可以构造局部敏感哈希“桶”。对于 Embedding 向量来说，由于 Embedding 大量使用内积操作计算相似度，因此我们也可以用内积操作来构建局部敏感哈希桶。假设 v 是高维空间中的 k 维 Embedding 向量，x 是随机生成的 k 维映射向量。那我们利用内积操作可以将 v 映射到一维空间，得到数值 h(v)=v⋅x。

而且，我们刚刚说了，一维空间也会部分保存高维空间的近似距离信息。因此，我们可以使用哈希函数 h(v) 进行分桶，公式为：hx,b(v)=⌊wx⋅v+b​] 。其中， ⌊⌋ 是向下取整操作， w 是分桶宽度，b 是 0 到 w 间的一个均匀分布随机变量，避免分桶边界固化。

不过，映射操作会损失部分距离信息，如果我们仅采用一个哈希函数进行分桶，必然存在相近点误判的情况，因此，我们可以采用 m 个哈希函数同时进行分桶。如果两个点同时掉进了 m 个桶，那它们是相似点的概率将大大增加。通过分桶找到相邻点的候选集合后，我们就可以在有限的候选集合中通过遍历找到目标点真正的 K 近邻了。

刚才我们讲的哈希策略是基于内积操作来制定的，内积相似度也是我们经常使用的相似度度量方法，事实上距离的定义有很多种，比如“曼哈顿距离”“切比雪夫距离”“汉明距离”等等。针对不同的距离定义，分桶函数的定义也有所不同，但局部敏感哈希通过分桶方式保留部分距离信息，大规模降低近邻点候选集的本质思想是通用的。

2. 局部敏感哈希的多桶策略

刚才我们讲到了可以使用多个分桶函数的方式来增加找到相似点的概率。那你可能有疑问，如果有多个分桶函数的话，具体应该如何处理不同桶之间的关系呢？这就涉及局部敏感哈希的多桶策略。

假设有 A、B、C、D、E 五个点，有 h1和 h2两个分桶函数。使用 h1来分桶时，A 和 B 掉到了一个桶里，C、D、E 掉到了一个桶里；使用 h2来分桶时，A、C、D 掉到了一个桶里，B、E 在一个桶。那么请问如果我们想找点 C 的最近邻点，应该怎么利用两个分桶结果来计算呢？

如果我们用“且”（And）操作来处理两个分桶结果之间的关系，那么结果是这样的，找到与点 C 在 h1函数下同一个桶的点，且在 h2函数下同一个桶的点，作为最近邻候选点。我们可以看到，满足条件的点只有一个，那就是点 D。也就是说，点 D 最有可能是点 C 的最近邻点。因此，用且操作作为多桶策略，可以最大程度地减少候选点数量。但是，由于哈希分桶函数不是一个绝对精确的操作，点 D 也只是最有可能的最近邻点，却不一定是最近邻点，因此，“且”操作其实也增大了漏掉最近邻点的概率。

那如果我们采用“或”（Or）操作作为多桶策略，又会是什么情况呢？具体操作就是，我们找到与点 C 在 h1函数下同一个桶的点，或在 h2函数下同一个桶的点。这个时候，我们可以看到候选集中会有三个点，分别是 A、D、E。这样一来，虽然我们增大了候选集的规模，减少了漏掉最近邻点的可能性，但增大了后续计算的开销。

当然，局部敏感哈希的多桶策略还可以更加复杂，比如使用 3 个分桶函数分桶，把同时落入两个桶的点作为最近邻候选点等等。

那么，我们到底应该选择“且”操作还是“或”操作，以及到底该选择使用几个分桶函数，每个分桶函数分几个桶，这还是一个工程上的权衡问题。我虽然不能给出具体的最佳数值，但可以给你一些取值的建议：

点数越多，我们越应该增加每个分桶函数中桶的个数；相反，点数越少，我们越应该减少桶的个数；

Embedding 向量的维度越大，我们越应该增加哈希函数的数量，尽量采用且的方式作为多桶策略；相反，Embedding 向量维度越小，我们越应该减少哈希函数的数量，多采用或的方式作为分桶策略。

最后，我们再回头来解决课程开头提出的问题，局部敏感哈希能在常数时间得到最近邻的结果吗？答案是可以的，如果我们能够精确的控制每个桶内的点的规模是 C，假设每个 Embedding 的维度是 N，那么找到最近邻点的时间开销将永远在 O(C⋅N) 量级。采用多桶策略之后，假设分桶函数数量是 K，那么时间开销也在 O(K⋅C⋅N) 量级，这仍然是一个常数。

局部敏感哈希实践

现在，我们已经知道了局部敏感哈希的基本原理和多桶策略，接下来我们一起进入实践环节，利用 SparrowRecsys 训练好的物品 Embedding，来实现局部敏感哈希的快速搜索吧。为了保证跟 Embedding 部分的平台统一，这一次我们继续使用 Spark MLlib 完成 LSH 的实现。

在将电影 Embedding 数据转换成 dense Vector 的形式之后，我们使用 Spark MLlib 自带的 LSH 分桶模型 BucketedRandomProjectionLSH（我们简称 LSH 模型）来进行 LSH 分桶。其中最关键的部分是设定 LSH 模型中的 BucketLength 和 NumHashTables 这两个参数。其中，BucketLength 指的就是公式 2 中的分桶宽度 w，NumHashTables 指的是多桶策略中的分桶次数。

清楚了模型中的关键参数，执行的过程就跟我们讲过的其他 Spark MLlib 模型一样了，都是先调用 fit 函数训练模型，再调用 transform 函数完成分桶的过程，具体的实现你可以参考下面的代码。

def embeddingLSH(spark:SparkSession, movieEmbMap:Map[String, Array[Float]]): Unit ={
  //将电影embedding数据转换成dense Vector的形式，便于之后处理
  val movieEmbSeq = movieEmbMap.toSeq.map(item => (item._1, Vectors.dense(item._2.map(f => f.toDouble))))
  val movieEmbDF = spark.createDataFrame(movieEmbSeq).toDF("movieId", "emb")

  //利用Spark MLlib创建LSH分桶模型
  val bucketProjectionLSH = new BucketedRandomProjectionLSH()
    .setBucketLength(0.1)
    .setNumHashTables(3)
    .setInputCol("emb")
    .setOutputCol("bucketId")
  //训练LSH分桶模型
  val bucketModel = bucketProjectionLSH.fit(movieEmbDF)
  //进行分桶
  val embBucketResult = bucketModel.transform(movieEmbDF)

  //打印分桶结果
  println("movieId, emb, bucketId schema:")
  embBucketResult.printSchema()
  println("movieId, emb, bucketId data result:")
  embBucketResult.show(10, truncate = false)

  //尝试对一个示例Embedding查找最近邻
  println("Approximately searching for 5 nearest neighbors of the sample embedding:")
  val sampleEmb = Vectors.dense(0.795,0.583,1.120,0.850,0.174,-0.839,-0.0633,0.249,0.673,-0.237)
  bucketModel.approxNearestNeighbors(movieEmbDF, sampleEmb, 5).show(truncate = false)
}

为了帮助你更加直观的看到分桶操作的效果，我把使用 LSH 模型对电影 Embedding 进行分桶得到的五个结果打印了出来，如下所示：

+-------+-----------------------------+------------------+
|movieId|emb                          |bucketId          |
+-------+-----------------------------+------------------------+
|710    |[0.04211471602320671,..]     |[[-2.0], [14.0], [8.0]] |
|205    |[0.6645985841751099,...]     |[[-4.0], [3.0], [5.0]]  |
|45     |[0.4899883568286896,...]     |[[-6.0], [-1.0], [2.0]] |
|515    |[0.6064003705978394,...]     |[[-3.0], [-1.0], [2.0]] |
|574    |[0.5780771970748901,...]     |[[-5.0], [2.0], [0.0]]  |
+-------+-----------------------------+------------------------+

你可一看到在 BucketId 这一列，因为我们之前设置了 NumHashTables 参数为 3，所以每一个 Embedding 对应了 3 个 BucketId。在实际的最近邻搜索过程中，我们就可以利用刚才讲的多桶策略进行搜索了。

事实上，在一些超大规模的最近邻搜索问题中，索引、分桶的策略还能进一步复杂。如果你有兴趣深入学习，我推荐你去了解一下Facebook 的开源向量最近邻搜索库 FAISS，这是一个在业界广泛应用的开源解决方案。

小结

本节课，我们一起解决了“Embedding 最近邻搜索”问题。

事实上，对于推荐系统来说，我们可以把召回最相似物品 Embedding 的问题，看成是在高维的向量空间内搜索最近邻点的过程。遇到最近邻问题，我们一般会采用聚类和索引这两种方法。但是聚类和索引都无法完全解决边缘点最近邻的问题，并且对于聚类来说，中心点的数量 k 也并不好确定，而对于 kd-tree 索引来说，kd-tree 索引的结构并不简单，离线和在线维护的过程也相对复杂。

因此，解决最近邻问题最“完美”的办法就是使用局部敏感哈希，在每个桶内点的数量接近时，它能够把最近邻查找的时间控制在常数级别。为了进一步提高最近邻搜索的效率或召回率，我们还可以采用多桶策略，首先是基于“且”操作的多桶策略能够进一步减少候选集规模，增加计算效率，其次是基于“或”操作的多桶策略则能够提高召回率，减少漏掉最近邻点的可能性。

最后，我在下面列出了各种方法的优缺点，希望能帮助你做一个快速的复盘。

课后思考

如果让你在推荐服务器内部的召回层实现最近邻搜索过程，你会怎样存储和使用我们在离线产生的分桶数据，以及怎样设计线上的搜索过程呢？

欢迎你在留言区写出你的答案，更欢迎你把这一过程的实现提交 Pull Request 到 SparrowResys 项目，如果能够被采纳，你将成为这一开源项目的贡献者之一。我们下节课再见！

13 | 模型服务：怎样把你的离线模型部署到线上？

你好，我是王喆。今天我们来讨论“模型服务”（Model Serving）。

在实验室的环境下，我们经常使用 Spark MLlib、TensorFlow、PyTorch 这些流行的机器学习库来训练模型，因为不用直接服务用户，所以往往得到一些离线的训练结果就觉得大功告成了。但在业界的生产环境中，模型需要在线上运行，实时地根据用户请求生成模型的预估值。这个把模型部署在线上环境，并实时进行模型推断（Inference）的过程就是模型服务。

模型服务对于推荐系统来说是至关重要的线上服务，缺少了它，离线的模型只能在离线环境里面“干着急”，不能发挥功能。但是，模型服务的方法可谓是五花八门，各个公司为了部署自己的模型也是各显神通。那么，业界主流的模型服务方法都有哪些，我们又该如何选择呢？

今天，我就带你学习主流的模型服务方法，并通过 TensorFlow Serving 把你的模型部署到线上。

业界的主流模型服务方法

由于各个公司技术栈的特殊性，采用不同的机器学习平台，模型服务的方法会截然不同，不仅如此，使用不同的模型结构和模型存储方式，也会让模型服务的方法产生区别。总的来说，那业界主流的模型服务方法有 4 种，分别是预存推荐结果或 Embedding 结果、预训练 Embedding+ 轻量级线上模型、PMML 模型以及 TensorFlow Serving。接下来，我们就详细讲讲这些方法的实现原理，通过对比它们的优缺点，相信你会找到最合适自己业务场景的方法。

预存推荐结果或 Embedding 结果

对于推荐系统线上服务来说，最简单直接的模型服务方法就是在离线环境下生成对每个用户的推荐结果，然后将结果预存到以 Redis 为代表的线上数据库中。这样，我们在线上环境直接取出预存数据推荐给用户即可。

这个方法的优缺点都非常明显，我把它们总结在了下图中，你可以看看。

图1 预存推荐结果优缺点对比

由于这些优缺点的存在，这种直接存储推荐结果的方式往往只适用于用户规模较小，或者一些冷启动、热门榜单等特殊的应用场景中。

那如果在用户规模比较大的场景下，我们该怎么减少模型存储所需的空间呢？我们其实可以通过存储 Embedding 的方式来替代直接存储推荐结果。具体来说就是，我们先离线训练好 Embedding，然后在线上通过相似度运算得到最终的推荐结果。

在前面的课程中，我们通过 Item2vec、Graph Embedding 等方法生成物品 Embedding，再存入 Redis 供线上使用的过程，这就是预存 Embedding 的模型服务方法的典型应用。

由于，线上推断过程非常简单快速，因此，预存 Embedding 的方法是业界经常采用的模型服务手段。但它的局限性同样存在，由于完全基于线下计算出 Embedding，这样的方式无法支持线上场景特征的引入，并且无法进行复杂模型结构的线上推断，表达能力受限。因此对于复杂模型，我们还需要从模型实时线上推断的角度入手，来改进模型服务的方法。

预训练 Embedding+ 轻量级线上模型

事实上，直接预存 Embedding 的方法让模型表达能力受限这个问题的产生，主要是因为我们仅仅采用了“相似度计算”这样非常简单的方式去得到最终的推荐分数。既然如此，那我们能不能在线上实现一个比较复杂的操作，甚至是用神经网络来生成最终的预估值呢？当然是可行的，这就是业界很多公司采用的“预训练 Embedding+ 轻量级线上模型”的模型服务方式。

详细一点来说，这样的服务方式指的是“用复杂深度学习网络离线训练生成 Embedding，存入内存数据库，再在线上实现逻辑回归或浅层神经网络等轻量级模型来拟合优化目标”。

口说无凭，接下来，我们就来看一个业界实际的例子。我们先来看看下面这张模型结构图，这是阿里的推荐模型 MIMN（Multi-channel user Interest Memory Network，多通道用户兴趣记忆网络）的结构。神经网络，才是真正在线上服务的部分。

仔细看这张图你会注意到，左边粉色的部分是复杂模型部分，右边灰色的部分是简单模型部分。看这张图的时候，其实你不需要纠结于复杂模型的结构细节，你只要知道左边的部分不管多复杂，它们其实是在线下训练生成的，而右边的部分是一个经典的多层神经网络，它才是真正在线上服务的部分。

图2 阿里的MIMN模型 （出自Practice on Long Sequential User Behavior Modeling for Click-Through Rate Prediction）

这两部分的接口在哪里呢？你可以看一看图中连接处的位置，有两个被虚线框框住的数据结构，分别是 S(1)-S(m) 和 M(1)-M(m)。它们其实就是在离线生成的 Embedding 向量，在 MIMN 模型中，它们被称为“多通道用户兴趣向量”，这些 Embedding 向量就是连接离线模型和线上模型部分的接口。

线上部分从 Redis 之类的模型数据库中拿到这些离线生成 Embedding 向量，然后跟其他特征的 Embedding 向量组合在一起，扔给一个标准的多层神经网络进行预估，这就是一个典型的“预训练 Embedding+ 轻量级线上模型”的服务方式。

它的好处显而易见，就是我们隔离了离线模型的复杂性和线上推断的效率要求，离线环境下，你可以尽情地使用复杂结构构建你的模型，只要最终的结果是 Embedding，就可以轻松地供给线上推断使用。

利用 PMML 转换和部署模型

虽然 Embedding+ 轻量级模型的方法既实用又高效，但它还是把模型进行了割裂，让模型不完全是 End2End（端到端）训练 +End2End 部署这种最“完美”的方式。那有没有能够在离线训练完模型之后什么都不用做，直接部署模型的方式呢？当然是有的，也就是我接下来要讲的脱离于平台的通用模型部署方式，PMML。

PMML 的全称是“预测模型标记语言”(Predictive Model Markup Language, PMML)，它是一种通用的以 XML 的形式表示不同模型结构参数的标记语言。在模型上线的过程中，PMML 经常作为中间媒介连接离线训练平台和线上预测平台。

这么说可能还比较抽象。接下来，我就以 Spark MLlib 模型的训练和上线过程为例，来和你详细解释一下，PMML 在整个机器学习模型训练及上线流程中扮演的角色。

图3 Spark模型利用PMML的上线过程

图 3 中的例子使用了 JPMML 作为序列化和解析 PMML 文件的 library（库），JPMML 项目分为 Spark 和 Java Server 两部分。Spark 部分的 library 完成 Spark MLlib 模型的序列化，生成 PMML 文件，并且把它保存到线上服务器能够触达的数据库或文件系统中，而 Java Server 部分则完成 PMML 模型的解析，生成预估模型，完成了与业务逻辑的整合。

JPMML 在 Java Server 部分只进行推断，不考虑模型训练、分布式部署等一系列问题，因此 library 比较轻，能够高效地完成推断过程。与 JPMML 相似的开源项目还有 MLeap，同样采用了 PMML 作为模型转换和上线的媒介。

事实上，JPMML 和 MLeap 也具备 Scikit-learn、TensorFlow 等简单模型的转换和上线能力。我把JPMML和MLeap的项目地址放在这里，感兴趣的同学可以进一步学习和实践。

TensorFlow Serving

既然 PMML 已经是 End2End 训练 +End2End 部署这种最“完美”的方式了，那我们的课程中为什么不使用它进行模型服务呢？这是因为对于具有复杂结构的深度学习模型来说，PMML 语言的表示能力还是比较有限的，还不足以支持复杂的深度学习模型结构。由于咱们课程中的推荐模型篇，会主要使用 TensorFlow 来构建深度学习推荐模型，这个时候 PMML 的能力就有点不足了。想要上线 TensorFlow 模型，我们就需要借助 TensorFlow 的原生模型服务模块，也就是 TensorFlow Serving 的支持。

从整体工作流程来看，TensorFlow Serving 和 PMML 类工具的流程一致，它们都经历了模型存储、模型载入还原以及提供服务的过程。在具体细节上，TensorFlow 在离线把模型序列化，存储到文件系统，TensorFlow Serving 把模型文件载入到模型服务器，还原模型推断过程，对外以 HTTP 接口或 gRPC 接口的方式提供模型服务。

再具体到咱们的 SparrowRecsys 项目中，我们会在离线使用 TensorFlow 的 Keras 接口完成模型构建和训练，再利用 TensorFlow Serving 载入模型，用 Docker 作为服务容器，然后在 Jetty 推荐服务器中发出 HTTP 请求到 TensorFlow Serving，获得模型推断结果，最后推荐服务器利用这一结果完成推荐排序。

图4 SparrowRecsys项目模型服务部分的架构

实战搭建 TensorFlow Serving 模型服务

好了，清楚了模型服务的相关知识，相信你对各种模型服务方法的优缺点都已经了然于胸了。刚才我们提到，咱们的课程选用了 TensorFlow 作为构建深度学习推荐模型的主要平台，并且选用了 TensorFlow Serving 作为模型服务的技术方案，它们可以说是整个推荐系统的核心了。那为了给之后的学习打下基础，接下来，我就带你搭建一个 TensorFlow Serving 的服务，把这部分重点内容牢牢掌握住。

总的来说，搭建一个 TensorFlow Serving 的服务主要有 3 步，分别是安装 Docker，建立 TensorFlow Serving 服务，以及请求 TensorFlow Serving 获得预估结果。为了提高咱们的效率，我希望你能打开电脑跟着我的讲解和文稿里的指令代码，一块儿来安装。

1. 安装 Docker

TensorFlow Serving 最普遍、最便捷的服务方式就是使用 Docker 建立模型服务 API。为了方便你后面的学习，我再简单说说 Docker。Docker 是一个开源的应用容器引擎，你可以把它当作一个轻量级的虚拟机。它可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中，然后发布到任何流行的操作系统，比如 Linux/Windows/Mac 的机器上。Docker 容器相互之间不会有任何接口，而且容器本身的开销极低，这就让 Docker 成为了非常灵活、安全、伸缩性极强的计算资源平台。

因为 TensorFlow Serving 对外提供的是模型服务接口，所以使用 Docker 作为容器的好处主要有两点，一是可以非常方便的安装，二是在模型服务的压力变化时，可以灵活地增加或减少 Docker 容器的数量，做到弹性计算，弹性资源分配。Docker 的安装也非常简单，我们参考官网的教程，像安装一个普通软件一样下载安装就好。

安装完 Docker 后，你不仅可以通过图形界面打开并运行 Docker，而且可以通过命令行来进行 Docker 相关的操作。那怎么验证你是否安装成功了呢？只要你打开命令行输入 docker --version 命令，它能显示出类似“Docker version 19.03.13, build 4484c46d9d”这样的版本号，就说明你的 Docker 环境已经准备好了。

2. 建立 TensorFlow Serving 服务

Docker 环境准备好之后，我们就可以着手建立 TensorFlow Serving 服务了。

首先，我们要利用 Docker 命令拉取 TensorFlow Serving 的镜像:

# 从docker仓库中下载tensorflow/serving镜像
docker pull tensorflow/serving

然后，我们再从 TenSorflow 的官方 GitHub 地址下载 TensorFlow Serving 相关的测试模型文件：

# 把tensorflow/serving的测试代码clone到本地
git clone https://github.com/tensorflow/serving
# 指定测试数据的地址
TESTDATA="$(pwd)/serving/tensorflow_serving/servables/tensorflow/testdata"

最后，我们在 Docker 中启动一个包含 TensorFlow Serving 的模型服务容器，并载入我们刚才下载的测试模型文件 half_plus_two：

# 启动TensorFlow Serving容器，在8501端口运行模型服务api
docker run -t --rm -p 8501:8501 \
    -v "$TESTDATA/saved_model_half_plus_two_cpu:/models/half_plus_two" \
    -e MODEL_NAME=half_plus_two \
    tensorflow/serving &

在命令执行完成后，如果你在 Docker 的管理界面中看到了 TenSorflow Serving 容器，如下图所示，就证明 TensorFlow Serving 服务被你成功建立起来了。

图5 TensorFlow Serving容器的Docker启动管理界面

3. 请求 TensorFlow Serving 获得预估结果

最，我们再来验证一下是否能够通过 HTTP 请求从 TensorFlow Serving api 中获得模型的预估结果。我们可以通过 curl 命令来发送 HTTP POST 请求到 TensorFlow Serving 的地址，或者利用 Postman 等软件来组装 POST 请求进行验证。

# 请求模型服务API
curl -d '{"instances": [1.0, 2.0, 5.0]}' \
    -X POST http://localhost:8501/v1/models/half_plus_two:predict

如果你看到了下图这样的返回结果，就说明 TensorFlow Serving 服务已经成功建立起来了。

# 返回模型推断结果如下
# Returns => { "predictions": [2.5, 3.0, 4.5] }

如果对这整个过程还有疑问的话，你也可以参考 TensorFlow Serving 的官方教程。

不过，有一点我还想提醒你，这里我们只是使用了 TensorFlow Serving 官方自带的一个测试模型，来告诉你怎么准备环境。在推荐模型实战的时候，我们还会基于 TensorFlow 构建多种不同的深度学习模型，到时候 TensorFlow Serving 就会派上关键的用场了。

那对于深度学习推荐系统来说，我们只要选择 TensorFlow Serving 的模型服务方法就万无一失了吗？当然不是，它也有需要优化的地方。在搭建它的过程会涉及模型更新，整个 Docker Container 集群的维护，而且 TensorFlow Serving 的线上性能也需要大量优化来提高，这些工程问题都是我们在实践过程中必须要解决的。但是，它的易用性和对复杂模型的支持，还是让它成为上线 TensorFlow 模型的第一选择。

小结

业界主流的模型服务方法有 4 种，分别是预存推荐结果或 Embeding 结果、预训练 Embeding+ 轻量级线上模型、利用 PMML 转换和部署模型以及 TensorFlow Serving。

它们各有优缺点，为了方便你对比，我把它们的优缺点都列在了表格中，你可以看看。

我们的之后的课程会重点使用 TensorFlow Serving，它是 End2End 的解决方案，使用起来非常方便、高效，而且它支持绝大多数 TensorFlow 的模型结构，对于深度学习推荐系统来说，是一个非常好的选择。但它只支持 TensorFlow 模型，而且针对线上服务的性能问题，需要进行大量的优化，这是我们在使用时需要重点注意的。

在实践部分，我们一步步搭建起了基于 Docker 的 TensorFlow Serving 服务，这为我们之后进行深度学习推荐模型的上线打好了基础。整个搭建过程非常简单，相信你跟着我的讲解就可以轻松完成。

课后思考

我们今天讲了如此多的模型服务方式，你能结合自己的经验，谈一谈你是如何在自己的项目中进行模型服务的吗？除了我们今天说的，你还用过哪些模型服务的方法？

欢迎在留言区分享你的经验，也欢迎你把这节课分享出去，我们下节课见！

14 | 融会贯通：SparrowRecSys中的电影相似推荐功能是如何实现的？

你好，我是王喆。

课程进行到这里，推荐系统架构的大部分知识点，包括特征工程、Embedding 模型，到推荐服务的搭建，线上推荐过程的实现，我们都已经学习并且实践过了。如果你坚持跟着我一起学下来的话，可以说已经是“武功小成”了。

为了帮你巩固所学，今天，我就带你从头到尾地实现一个完整的推荐功能，相似电影推荐，来帮助你打通推荐系统的“任督二脉”。

“清点技能库”，看看我们已有的知识储备有哪些

在开始实现相似电影推荐功能之前，我想先带着你一起清点一下自己的技能库。我喜欢把推荐的过程比喻成做菜的过程，接下来，我就按照做菜的 4 个关键步骤，带你回顾一下前面学过的重点知识。

第一步，准备食材。 准备食材的过程就是我们准备推荐所需特征的过程。在特征工程篇中，我们不仅学会了怎么挑选“食材”，怎么处理“食材”，而且还实践了“备菜”的高级技能 Embedding 技术。具体来说就是，我们能够利用物品序列数据，通过 Item2vec 方法训练出 Embedding，也能够使用 Deep Walk 和 Node2vec 把图结构数据生成 Graph Embedding。

总的来说，因为 Embedding 技术的本质就是利用了物品之间的相关性，所以 Embedding 是做好“相似推荐”这盘菜的关键。

第二步，食材下锅。 备好了菜，在正式开炒之前，我们肯定要把食材下锅。在推荐系统中“食材下锅”的过程有两个，一是把线上推荐所用的特征存储到数据库中，在之前的课程中我们已经实践过使用 Redis 作为特征数据库的方法，另一个是把模型部署到模型服务模块，我们也已讲过了预训练 Embedding，Embedding 加轻量级线上模型，TensorFlow Serving 等多种模型服务方式，这节课我们将采用预训练 Embedding 的方式进行模型服务。

第三步，做菜技术。 “做菜的技术”说的是推荐服务器线上推荐的整个流程是否合理。那回到推荐系统中就是指，召回层要快速准确，模型排序部分要精确。这些具体的实现都影响着最终的推荐效果。

对于召回层来说，我们已经学过单策略召回、多路召回和基于 Embedding 的召回。对于排序来说，我们会主要利用 Embedding 相似度来排序，后续我们还会学习基于多种推荐模型的排序。

最后是菜品上桌的过程 ，也就是把推荐的结果呈现给用户的过程。这节课，我会带你一起实现这个过程。提前“剧透”一下，在 SparrowRecsys 中，我们会先利用 JavaScript 异步请求推荐服务 API 获取推荐结果，再利用 JavaScript+HTML 把结果展现给用户”。因为，这一部分内容不是推荐系统的重点，所以我们这里只要做到界面清爽、逻辑清晰就可以了。

相信到这里，各位“大厨”已经准备好了所要用到的技能，下面就让我们一起来实现 SparrowRecSys 中的相似电影推荐功能吧！

如何实现相似电影推荐功能？

在正式开始相似电影推荐功能之前，我们先来看看我总结的 SparrowRecsys 相似电影推荐功能的详细技术架构图。细心的你可能已经发现了，这个架构图就是 SparrowRecsys 架构的精简版。因为我们还没有学习深度学习推荐模型和模型评估的相关知识，所以把重点聚焦在已经学过的知识上就可以了。

图1 Sparrow Recsys 相似电影推荐功能的技术架构图

接下来，我就结合这个技术架构图，带你一步步地实现其中的每一个模块。并且，我还会给你讲解一些项目中没有实现的其他业界主流方法，如果你还学有余力，希望你能抓住这个机会，来扩展一下自己的知识面。

1. 数据和模型部分

数据和模型部分的实现，其实和我们第 8 节课讲的 Embedding 的实战思路是一样的，我们可以选用 Item2vec、Deep Walk 等不同的 Embedding 方法，来生成物品 Embedding 向量。考虑到大数据条件下，数据处理与训练的一致性，在 SparrowRecsys 中，我们会采用 Spark 进行数据处理，同时选择 Spark MLlib 进行 Embedding 的训练。这部分内容的代码，你可以参考项目中的 _com.wzhe.sparrowrecsys.offline.spark.embedding.__Embedding_ 对象，它定义了所有项目中用到的 Embedding 方法。

对于一些比较复杂的 Embedding 方案，比如特征种类很多，网络结构也更多样化的 Embedding 模型，业界也多采用 Spark 进行原始数据处理，生成训练样本后交由 TensorFlow、PyTorch 训练的方案。

但是不论训练平台是怎样的，Embedding 方法的产出都是一致的，就是物品 ID 对应的 Embedding 向量。那为了方便线上服务使用，我们还需要在生成 Embedding 后，把它们存入某个高可用的数据库。SparrowRecsys 选择了最主流的内存数据库 Redis 作为实现方案，这一部分的具体实现，你可以参照 com.wzhe.sparrowrecsys.offline.spark.embedding.Embedding 对象中 trainItem2vec 函数的 Redis 存储操作。当然，业界也会使用 Cassandra+ 缓存，RocksDB 等不同的存储方案来实现 Embedding 向量的高效读取，但我们现阶段只要学会 Redis 存储和读取操作就够用了。

到这里，Redis 成为了连接线下和线上的关键节点，那我们的线上服务部分又是怎么利用 Redis 中的 Embedding 数据进行相似电影推荐的呢？

2. 线上服务部分

线上服务部分是直接接收并处理用户推荐请求的部分，从架构图的最左边到最右边，我们可以看到三个主要步骤：候选物品库的建立、召回层的实现、排序层的实现。我们逐个来讲一讲。

首先是候选物品库的建立。SparrowRecsys 中候选物品库的建立采用了非常简单的方式，就是直接把 MovieLens 数据集中的物品数据载入到内存中。但对于业界比较复杂的推荐业务来说，候选集的选取往往是有很多条件的， 比如物品可不可用，有没有过期，有没有其他过滤条件等等，所以，工业级推荐系统往往会通过比较复杂的 SQL 查询，或者 API 查询来获取候选集。

第二步是召回层的实现。我们在11 课曾经详细学习了召回层的技术，这里终于可以学以致用了。因为物品的 Embedding 向量已经在离线生成，所以我们可以自然而然的使用 Embedding 召回的方法来完成召回层的实现。同时，SparrowRecsys 也实现了基于物品 metadata（元信息）的多路召回方法，具体的实现你可以参照 com.wzhe.sparrowrecsys.online.recprocess.SimilarMovieProcess 类中的 multipleRetrievalCandidates 函数和 retrievalCandidatesByEmbedding 函数。

第三步是排序层的实现。根据 Embedding 相似度来进行“相似物品推荐”，是深度学习推荐系统最主流的解决方案，所以在 SparrowRecsys 中，我们当然也是先根据召回层过滤出候选集，再从 Redis 中取出相应的 Embedding 向量，然后计算目标物品和候选物品之间的相似度，最后进行排序就可以了。

这里“相似度”的定义是多样的，可以是余弦相似度，也可以是内积相似度，还可以根据你训练 Embedding 时定义的不同相似度指标来确定。因为在 Word2vec 中，相似度的定义是内积相似度，所以, 这里我们也采用内积作为相似度的计算方法。同样，具体的实现，你可以参照 com.wzhe.sparrowrecsys.online.recprocess.SimilarMovieProcess 类中的 ranker 函数。

经历了这三个主要的线上服务步骤，SparrowRecsys 就可以向用户返回推荐列表了。所以接下来，我们要解决的问题就是，怎么把这些结果通过前端页面展示给用户。

3. 前端部分

SparrowRecsys 的前端部分采用了最简单的 HTML+AJAX 请求的方式。AJAX 的全称是 Asynchronous JavaScript and XML，异步 JavaScript 和 XML 请求。它指的是不刷新整体页面，用 JavaScript 异步请求服务器端，更新页面中部分元素的技术。当前流行的 JavaScript 前端框架 React、Vue 等等也大多是基于 AJAX 来进行数据交互的。

但前端毕竟不是我们课程的重点，你知道我在上面提到的基本原理就可以了。如果你已经在本地的 6010 端口运行起了 SparrowRecsys，那直接点击这个链接：http://localhost:6010/movie.html?movieId=589 ， 就可以看到电影《终结者 2》的详情页面和相似电影推荐结果了（如图 2）。

图2 终结者2的相似电影推荐结果

相似电影推荐的结果和初步分析

到这里，我相信你已经串联起来了 SparrowRecsys 相似电影推荐的所有实现，看到了推荐结果。那么问题来了，推荐结果的好坏到底是如何判断的呢？关于这个问题，我们也会在后面的“模型评估篇”中进行系统性的学习。不过，这里我也想先跟你聊聊这个话题，让你对它有一个大体认识，这对你建立后续的模型评估体系是非常有帮助的。

首先提醒你的是，SparrowRecsys 开源项目中自带的 MovieLens 数据集是经过我采样后的缩小集，所以基于这个数据集训练出的模型的准确性和稳定性是比较低的。如果你有兴趣的话可以去MovieLens 官网选择 MovieLens 20M Dataset 下载并重新训练，相信会得到更准确的推荐结果。

其次，针对相似物品推荐这个推荐场景，我们其实很难找到一个统一的衡量标准。比如，你能说出《功夫熊猫》这部电影是跟《玩具总动员》更相近，还是跟《飞屋环游记》更相近吗？好在，工程师们还是总结出了一些有效的评估方法。这里，我挑出了 3 个最常用的来给你讲讲。

方法一：人肉测试（SpotCheck）。 在一种 Embedding 结果新鲜出炉的时候，你作为创造它们的工程师，应该第一时间做一个抽样测试，看一看基于 Embedding 的相似推荐结果是不是符合你自己的常识。比如说，我在 Embedding 训练完之后，随便在 SparrowRecsys 中翻了翻，看到了两个页面，一个是儿童电影《Free Willy》（《人鱼童话》）的相似电影推荐页面（图 3 左），另一个是著名动画电影《Toy Story》（《玩具总动员》）的相似电影推荐页面（图 3 右）。

图3 随机测试

直观上来看，《Free Willy》的推荐结果就非常不错，因为你可以看到相似电影中都是适合儿童看的，甚至这些电影都和动物相关。但是《玩具总动员》就不一样了，它的相似电影里不仅有动画片，还有《真实的谎言》（《True Lies》）、《阿甘正传》这类明显偏成人的电影。这明显不是一个非常好的推荐结果。

为什么会出现这样的结果呢？我们来做一个推测。事实上，《玩具总动员》本身是一部非常流行的电影，跟它近似的也都是类似《真实的谎言》、《阿甘正传》这类很热门的电影。这就说明了一个问题，热门电影其实很容易跟其他大部分电影产生相似性，因为它们会出现在大多数用户的评分序列中。

针对这个问题，其实仅利用基于用户行为序列的 Embedding 方法是很难解决的。这需要我们引入更多内容型特征进行有针对性的改进，比如电影类型、海报风格，或者在训练中有意减少热门电影的样本权重，增大冷门电影的样本权重等等。总的来说，遇到推荐结果不合理的情况，我们需要做更多的调查研究，发掘这些结果出现的真实原因，才能找到改进方向。

方法二：指定 Ground truth（可以理解为标准答案）。 虽然我们说，相似影片的 Ground truth 因人而异。但如果只是为了进行初步评估，我们也可以指定一些比较权威的验证集。比如，对于相似影片来说，我们可以利用 IMDB 的 more like this 的结果去做验证我们的相似电影结果。当然要补充说明的是，要注意有些 Ground truth 数据集的可用范围，不能随意在商业用途中使用未经许可的数据集。

方法三：利用商业指标进行评估。 既然相似影片比较难以直接衡量，那我们不如换一个角度，来思考一下做相似影片这个功能的目的是什么。对于一个商业网站来说，无非是提高点击率，播放量等等。因此，我们完全可以跃过评估相似度这样一个过程，直接去评估它的终极商业指标。

举个例子，我们可以通过上线一个新的相似电影模型，让相似电影这个功能模块的点击率提高，假设提高了 5%，那这就是一个成功的模型改进。至于相似电影到底有没有那么“相似”，我们反而不用那么纠结了。

小结

这节课，我们使用 Embedding 方法准备好了食材，使用 Redis 把食材下锅，做菜的步骤稍微复杂一点，分为建立候选集、实现召回层、实现排序层这 3 个步骤。最后我们用 HTML+Ajax 的方式把相似电影推荐这盘菜呈现出来。

既然有做菜的过程，当然也有品菜的阶段。针对相似物品推荐这一常见的功能，我们可以使用人肉测试、Ground truth 和商业指标评估这三种方法对得到的结果进行评估。也希望你能够在实际的业务场景中活学活用，用评估结果指导模型的下一步改进。

我希望，通过这节课的总结和实战，能让你融会贯通的厘清我们学过的知识。所以我把你需要掌握的重要知识点，总结在了一张图里，你可以利用它复习巩固。

好了，那到这里，我们线上服务篇的内容就全部结束了。通过这一篇的学习，我相信你已经清楚了推荐系统的全部技术架构，以及深度学习核心技术 Embedding 的运用方法。

但我要说的是，盛宴还未开始，下一篇我们将进入深度推荐模型的学习和实践。我曾经说过，深度推荐模型是深度学习推荐系统这个王冠上的明珠，正是它对推荐模型的革命，让深度学习的浪潮席卷推荐系统领域。希望你再接再厉，让我们一起把这颗明珠摘下吧！

课后思考

刚才我说到，《玩具总动员》的相似电影推荐结果并不好，我认为可能是因为热门电影的头部效应造成的。你认同这一观点吗？你觉得还有其他可能的原因吗？如果让你去做一些 Embedding 方法上的改进，你还有什么好的想法吗？

欢迎把你的成果和优化想法分享到留言区，也欢迎你能把这节课转发出去，让更多人从我们的实践中受益，我们下节课见！

答疑 | 线上服务篇留言问题详解

你好，我是王喆。

今天是专栏的第二次答疑加餐时间，第一次答疑我已经对基础篇和特征工程篇中常见的问题进行了解答，所以这节课我们重点来看看线上服务篇中很有代表性的课后思考题和留言问题，我会对它们进行一些补充回答，希望对你有帮助。

关于项目的开源精神

在开始回答问题之前，我想先跟你聊一聊我们 SparrowRecsys 项目的开源精神。在课程一开始我就说过，SparrowRecsys 这个项目是我们的一个种子项目，它肯定不完美，但我希望它是一个工业级推荐系统的雏形。在学习的过程中，我希望能够跟你一起完善它，让它的羽翼逐渐丰满起来。

让我很高兴的是，已经有不少同学投身到改进 SparrowRecsys 的队伍中来，比如 GitHub ID 叫dxzmpk的同学添加了Node2vec 模型的代码，还有 GitHub ID 叫 jason-wang1 的同学添加了多路召回多线程版本的代码，还有更多的同学修改了项目中的 Bug，优化了一些实现，感谢你们的投入！

我是开源精神的坚定拥护者，我也相信在我们的共同努力下，SparrowRecsys 未来能够发展成为在业界有影响力的开源项目。所以在这里我呼吁同学们能够多参与进来，多提 Pull Request，让我们共同成为项目的第一批原作者。

好，下面我们进入问题解答的环节。

《03｜深度学习基础：你打牢深度学习知识的地基了吗？》

思考题 1：哪些因素影响着深度学习网络的结构？深度学习模型是越深越好吗？

这两个问题我们分开来看，先看看影响深度学习网络结构的因素。在业界的应用中，影响深度学习网络结构的因素非常多。不过，我认为可以总结出二类最主要的因素。

第一类：业务场景中用户行为的特点。很多模型结构的实现是为了模拟用户行为的特点，比如注意力机制的引入是用来模拟用户的注意力行为特点，序列模型是用来模拟用户兴趣变迁，特征交叉层是为了让用户和物品的相关特征进行交叉等等。

第二类：数据规模、算力的制约。这一点“一天”同学回答得非常有价值，在实际的业界应用中，数据规模大起来之后，我们往往不能够随意选择复杂模型，而是要在数据规模，平台算力的制约下，尽量选择效果最优的模型结构。

我们再来看第二个问题，深度学习模型是越深越好吗？

这个答案是否定的。深度学习模型变深之后，并不总是能够提高模型效果，有时候深度的增加对模型效果的贡献是微乎其微的。而且模型复杂之后的负面影响也非常多，比如训练时间加长，收敛所需数据和训练轮数增加，模型不一定稳定收敛，模型过拟合的风险增加等等。所以在模型深度的选择上，我们要在尽量保证效果的前提下，选择结构较简单的方案。

借助这道思考题，我希望能帮助你更好地理解深度学习的特点，以及实际应用中的一些经验。

《09｜线上服务：如何在线上提供高并发的推荐服务？》

思考题 2：在一个高并发的推荐服务集群中，负载均衡服务器的作用至关重要，如果你是负载均衡服务器的策略设计师，你会怎么实现这个“工头”的调度策略，让它能够公平又高效地完成调度任务呢？（比如是按每个节点的能力分配？还是按照请求本身的什么特点来分配？如何知道什么时候应该扩展节点，什么时候应该关闭节点？）

负载均衡的策略其实有多种选择，比如“smjccj”同学的回答就很专业，他说可以进行源地址哈希，或根据服务器计算能力加权随机分配。这是一个很好的答案，这里我再补充一下。通常来说，常用的负载均衡的策略有三种，分别是轮询调度、哈希调度和一致性哈希调度。我们一一来看。

轮询调度就是以轮询的方式依次把请求调度到不同的服务器。在服务器的算力等硬件配置不同的时候，我们还可以为每个服务器设定权重，按权重比例为能力强的服务器分配更多的请求。

而哈希调度指的是通过某个哈希函数把 key 分配给某个桶，这里 key 可以是请求中的用户 ID，物品 ID 等 ID 型信息，桶的总数就是服务器的总数。这样一来，我们就可以把某个用户的请求分配给某个服务器处理。这么做的好处是可以让一个 key 落在固定的服务器节点上，有利于节约服务器内部缓存的使用。

哈希方式的缺点在于无法高效处理故障点，一旦某个点有故障需要减少桶的数量，或者在 QPS 增大时需要增加服务器，整个分配过程就被完全打乱。因此，一致性哈希调度就是更好的解决方案，简单来说就是使用哈希环来解决计算节点的增加和减少的问题，具体的实现我推荐你参考《一致性哈希算法的理解与实践》这篇文章。

留言问题 1：在一个成熟的工业级推荐系统中，每个用户请求的时间、地点、context 都不一样，缓存策略是怎么工作的，才能把这些数据大部分都缓存起来？

这里，同学们一定要理解缓存的意义。如果请求中的变量每次都不一样，那我们确实就没有必要进行缓存了，因为每次返回的结果都是不同的。但真实情况往往不是这样，我们其实可以在具体的业务场景中挖掘出巨大的优化空间。

比如，电商网站存在着大量没有购买记录的新用户，我们其实可以根据这些新用户有限的特征把他们分成少量的几个类别，对一个类别内的用户展示同样的推荐结果。这样，我们就没必要每次都请求复杂的推荐模型了。

再比如，同一个用户有可能多次请求同一个页面，如果推荐系统对这些操作进行了缓存，就不用对每次重复的请求重复计算推荐结果了，在处理完首次请求之后，面对之后的重复请求，推荐系统直接返回缓存结果就可以了。当然，推荐系统具体能存储多少用户缓存，也取决于硬件配置，也取决于缓存的过期时间，这些都需要我们灵活进行配置。

留言问题 2：推荐系统中的冷启动策略指的是什么？

冷启动是推荐系统一定要考虑的问题。它是指推荐系统在没有可用信息，或者可用信息很少的情形下怎么做推荐的问题，冷启动可以分为用户冷启动和物品冷启动两类。

用户冷启动是指用户没有可用的行为历史情况下的推荐问题。一般来说，我们需要清楚在没有推荐历史的情况下，还有什么用户特征可以使用，比如注册时的信息，访问 APP 时可以获得的地点、时间信息等等，根据这些有限的信息，我们可以为用户做一个聚类，为每类冷启动用户返回合适的推荐列表。当然，我们也可以利用可用的冷启动特征，来构建一个较简单的冷启动推荐模型，去解决冷启动问题。

对于物品冷启动来说，主要处理的是新加入系统的物品，它们没有跟用户的交互信息。所以，针对物品冷启动，我们除了用类似用户冷启动的方式解决它以外，还可以通过物品分类等信息找到一些相似物品，如果这些相似物品已经具有了预训练的 Embedding，我们也可以采用相似物品 Embedding 平均的方式，来快速确定冷启动物品的 Embedding，让它们通过 Embedding 的方式参与推荐过程。

《11｜召回层：如何快速又准确地筛选掉不相关物品？》

留言问题 3：用户的多兴趣标签怎么与物品的标签进行最优匹配？当物品的标签有多层时，如何利用上一层的标签？

这个问题最简单的做法，就是把用户的兴趣标签和物品对应的标签都转换成 Multi-hot 向量，然后，我们就可以计算出用户和物品的相似度了。

除此之外，我们也可以进一步计算每个兴趣标签的TF-IDF 值，为标签分配权重后，再把它们转换成 Multi-hot 向量，这样我们也可以计算出用户和物品的相似度。

如果标签有多层，我们也可以把多层标签全部放到 Multi-hot 向量中，再把高层标签的权重适当降低，这也是可行的思路之一。

留言问题 4：在电商领域下，如何解决 EGES 训练非常慢的问题？

这是一个非常好的业界实践问题。这里，我先给同学们解释一下，什么是 EGES。EGES 指的是阿里提出的一种 Graph Embedidng 方法，全称是 Enhanced Graph Embedding with Side Information，补充信息增强图 Embedding。它是一种融合了经典的 Deep Walk Graph Embedding 结果和其他特征的 Embedding 方法。

针对 EGES 的训练比较慢的问题，我这里有两条建议可供同学们参考。

第一条是我们可以把商品 Embedding 进行预训练，再跟其他 side information 特征一起输入 EGES，不用直接在 EGES 中加 Embedding 层进行 End2End 训练。

第二条是我们可以把商品进行聚类后再输入 EGES 网络，比如非常类似的商品，可以用一个商品聚类 id 替代，当作一个商品来处理。事实上，这种方法往往可以大幅减少商品数量的量级，AirBnb 就曾经非常成功地应用了该方法，用一些特征的组合来代替一类商品或用户，不仅大幅加快训练速度，而且推荐效果也没有受到影响。

《12｜局部敏感哈希：如何在常数时间内搜索 Embedding 最近邻？》

留言问题 5：在用 Item2vec 等方法生成物品 Embedding 后，用户的 Embedding 是怎么生成的呢？ 物品和用户在同一个向量空间，这是怎么保证的呢？

在咱们的项目里，用户 Embedding 的生成方法是很直观的，就是对用户评论过的高分电影 Embedding 取平均值得到的。这相当于说，用户 Embedding 是在物品 Embedding 向量空间中进行运算得到的，那它们肯定是在一个向量空间内，我们也就可以使用相似度计算来求取相似度。

因此，只要是利用用户历史的 Item Embedding 生成的用户 Embedding，都是在一个向量空间内，这些生成方式包括 average pooling、sum pooling、attention 等等。

但是如果用户 Embedding 和物品 Embedding 是分别独立生成的，或者说是通过一个模型中没有直接关系的两个 Embedidng 层生成的，那么它们就不在一个向量空间内了。注意啦，这个时候，我们不能直接求用户和物品之间的相似度，只能求用户 - 用户的相似度，和物品 - 物品的相似度。

留言问题 6：“在局部敏感哈希的函数中，b 是 0 到 w 间的一个均匀分布随机变量，是为了避免分桶边界固化”。这是什么意思呢？是说可以通过调整 b 来形成另外一个个 Hash 函数？

首先，我要说这个局部敏感哈希相关的问题非常好，推荐其他同学也关注一下。

说回到这个问题，如果我们总是固定分桶的边界，很容易让边界两边非常接近的点被分到两个桶里，这是我们不想看到的。所以，这里我们就可以通过随机调整 b 的方式，来生成多个 Hash 函数，在进行多个 Hash 函数的分桶之后，再采用或的方式对分桶结果进行组合查找最近邻向量，就可以一定程度避免这些边界点的问题。

好了，这节课的答疑就到这里，非常感谢同学们的积极提问和思考。希望在接下来的课程里，你也能多多参与进来，与我一起完善 SparrowRecsys 项目的代码，共同进步！

15 | 协同过滤：最经典的推荐模型，我们应该掌握什么？

你好，我是王喆。今天我们要开启推荐模型篇的学习。

推荐模型篇是整个课程中最重要的一个模块，因为推荐模型直接决定了最终物品排序的结果，它的好坏也直接影响着推荐效果的优劣。而且，从某种意义上讲，推荐系统的整体架构都是围绕着推荐模型搭建的，用于支持推荐模型的上线、训练、评估、服务。因此，我一直把推荐模型称作“推荐系统这个皇冠上的明珠”。

而提起推荐模型，我们就不能不提协同过滤算法。它可能是推荐系统自诞生以来最经典的算法，且没有之一。虽然我们课程的主题是“深度学习”推荐系统，但协同过滤以及它后续衍生出来的各类模型，都与深度学习推荐模型有着千丝万缕的联系。因此，在进入深度学习模型之前，掌握协同过滤及其衍生模型是非常有必要的。

今天，我就来给你讲讲经典协同过滤和它的衍生模型矩阵分解的原理，以及相关的 Spark 实现。

协同过滤算法的基本原理

我在特征工程篇曾经提到过：“用户行为数据是推荐系统最常用，也是最关键的数据。用户的潜在兴趣、用户对物品的评价好坏都反映在用户的行为历史中”。

而协同过滤算法，就是一种完全依赖用户和物品之间行为关系的推荐算法。我们从它的名字“协同过滤”中，也可以窥探到它背后的原理，就是 “协同大家的反馈、评价和意见一起对海量的信息进行过滤，从中筛选出用户可能感兴趣的信息”。

这么说可能还是太抽象了，接下来，我们就一起看一个电商场景下的例子。通过分析这个例子，你就能搞清楚协同过滤算法的推荐过程了。这个电商推荐系统从得到原始数据到生成最终推荐分数，全过程一共可以总结为 6 个步骤，如下所示。下面，我们一一来讲。

图1 协同过滤的过程 （来自《深度学习推荐系统》）

首先，我们可以看到，电商网站的商品库里一共有 4 件商品：一个游戏机、一本小说、一本杂志，以及一台电视机。假设，现在有一名用户 X 访问了这个电商网站，电商网站的推荐系统需要决定是否推荐电视机给用户 X。

为了进行这项预测，推荐系统可以利用的数据有用户 X 对其他商品的历史评价数据，以及其他用户对这些商品的历史评价数据。我在图 1(b) 中用绿色“点赞”的标志表示好评，用红色“踩”的标志表示了差评。这样一来，用户、商品和评价记录就构成了带有标识的有向图。

接下来，为了方便计算，我们将有向图转换成矩阵的形式。这个矩阵表示了物品共同出现的情况，因此被称为“共现矩阵”。其中，用户作为矩阵行坐标，物品作为列坐标，我们再把“点赞”和“踩”的用户行为数据转换为矩阵中相应的元素值。这里，我们将“点赞”的值设为 1，将“踩”的值设为 -1，“没有数据”置为 0（如果用户对物品有具体的评分，那么共现矩阵中的元素值可以取具体的评分值，没有数据时的默认评分也可以取评分的均值）。

你发现了吗，生成共现矩阵之后，推荐问题就转换成了预测矩阵中问号元素（图 1(d) 所示）的值的问题。由于在“协同”过滤算法中，推荐的原理是让用户考虑与自己兴趣相似用户的意见。因此，我们预测的第一步就是找到与用户 X 兴趣最相似的 n（Top n 用户，这里的 n 是一个超参数）个用户，然后综合相似用户对“电视机”的评价，得出用户 X 对“电视机”评价的预测。

从共现矩阵中我们可以知道，用户 B 和用户 C 由于跟用户 X 的行向量近似，被选为 Top n（这里假设 n 取 2）相似用户，接着在图 1(e) 中我们可以看到，用户 B 和用户 C 对“电视机”的评价均是负面的。因为相似用户对“电视机”的评价是负面的，所以我们可以预测出用户 X 对“电视机”的评价也是负面的。在实际的推荐过程中，推荐系统不会向用户 X 推荐“电视机”这一物品。

到这里，协同过滤的算法流程我们就说完了。也许你也已经发现了，这个过程中有两点不严谨的地方，一是用户相似度到底该怎么定义，二是最后我们预测用户 X 对“电视机”的评价也是负面的，这个负面程度应该有一个分数来衡量，但这个推荐分数该怎么计算呢？

计算用户相似度

首先，我们来解决计算用户相似度的问题。计算用户相似度其实并不是什么难事，因为在共现矩阵中，每个用户对应的行向量其实就可以当作一个用户的 Embedding 向量。相信你早已经熟悉 Embedding 相似度的计算方法，那我们这里依葫芦画瓢就可以知道基于共现矩阵的用户相似度计算方法啦。

最经典的方法就是利用余弦相似度了，它衡量了用户向量 i 和用户向量 j 之间的向量夹角大小。夹角越小，余弦相似度越大，两个用户越相似，它的定义如下：

$sim(i,j)=cos(imath,jmath​)=∣imath∣×∥j​∣vecimath⋅jmath​​$

除了最常用的余弦相似度之外，相似度的定义还有皮尔逊相关系数、欧式距离等等。咱们课程主要使用的是余弦相似度，因此你只要掌握它就可以了，其他的定义我这里不再多说了。

用户评分的预测

接下来，我们再来看看推荐分数的计算。在获得 Top n 个相似用户之后，利用 Top n 用户生成最终的用户 u 对物品 p 的评分是一个比较直接的过程。这里，我们假设的是“目标用户与其相似用户的喜好是相似的”，根据这个假设，我们可以利用相似用户的已有评价对目标用户的偏好进行预测。最常用的方式是，利用用户相似度和相似用户评价的加权平均值，来获得目标用户的评价预测，公式如下所示。

Ru,p​=∑s∈S​wu,s​∑sϵS​(wu,s​⋅Rs,p​)​

其中，权重 wu,s​ 是用户 u 和用户 s 的相似度，Rs,p​ 是用户 s 对物品 p 的评分。

在获得用户 u 对不同物品的评价预测后，最终的推荐列表根据评价预测得分进行排序即可得到，到这里，我们就完成了协同过滤的全部推荐过程。

矩阵分解算法的原理

虽然说协同过滤是目前公认的最经典的推荐算法，但我们还是可以轻松找出它的缺点，那就是共现矩阵往往非常稀疏，在用户历史行为很少的情况下，寻找相似用户的过程并不准确。于是，著名的视频流媒体公司 Netflix 对协同过滤算法进行了改进，提出了矩阵分解算法，加强了模型处理稀疏矩阵的能力。

这里，我们还是用一个直观的例子来理解一下什么叫做矩阵分解。这次我从 Netflix 的矩阵分解论文中截取了两张示意图（图 2），来比较协同过滤和矩阵分解的原理。

图2 “协同过滤（左a）”和“矩阵分解（右b）”的原理图

如图 2(a) 所示，协同过滤算法找到用户可能喜欢的视频的方式很直观，就是利用用户的观看历史，找到跟目标用户 Joe 看过同样视频的相似用户，然后找到这些相似用户喜欢看的其他视频，推荐给目标用户 Joe。

矩阵分解算法则是期望为每一个用户和视频生成一个隐向量，将用户和视频定位到隐向量的表示空间上（如图 2(b) 所示），距离相近的用户和视频表明兴趣特点接近，在推荐过程中，我们就应该把距离相近的视频推荐给目标用户。例如，如果希望为图 2(b) 中的用户 Dave 推荐视频，我们可以找到离 Dave 的用户向量最近的两个视频向量，它们分别是《Ocean’s 11》和《The Lion King》，然后我们可以根据向量距离由近到远的顺序生成 Dave 的推荐列表。

这个时候你肯定觉得，矩阵分解不就是相当于一种 Embedding 方法嘛。没错，矩阵分解的主要过程，就是先分解协同过滤生成的共现矩阵，生成用户和物品的隐向量，再通过用户和物品隐向量间的相似性进行推荐。

那这个过程的关键就在于如何分解这个共现矩阵了。从形式上看，矩阵分解的过程是直观的，就是把一个 mxn 的共现矩阵，分解成一个 mxk 的用户矩阵和 kxn 的物品矩阵相乘的形式（如图 3）。

图3 矩阵分解示意图

有了用户矩阵和物品矩阵，用户隐向量和物品隐向量就非常好提取了。用户隐向量就是用户矩阵相应的行向量，而物品隐向量就是物品矩阵相应的列向量。

那关键问题就剩下一个，也就是我们该通过什么方法把共现矩阵分解开呢？最常用的方法就是梯度下降。梯度下降的原理我们在第 3 节课学习过，简单来说就是通过求取偏导的形式来更新权重。梯度更新的公式是 (wt+1=wt−α∗∂w∂L​)。为了实现梯度下降，最重要的一步是定义损失函数 L，定义好损失函数我们才能够通过求导的方式找到梯度方向，这里我们就给出矩阵分解损失函数的定义如下。

这个目标函数里面，rui​ 是共现矩阵里面用户 u 对物品 i 的评分，qi​ 是物品向量，pu​ 是用户向量。通过目标函数的定义我们可以看到，我们要求的物品向量和用户向量，是希望让物品向量和用户向量之积跟原始的评分之差的平方尽量小。简单来说就是，我们希望用户矩阵和物品矩阵的乘积尽量接近原来的共现矩阵。

在通过训练得到用户隐向量和物品隐向量之后，在服务器内部的推荐过程跟我们之前讲过的 Embedding 推荐是一样的，你也已经在 SparrowRecSys 里面实践过，是这方面的专家了，我就不再多说了。

矩阵分解算法的 Spark 实现

基础知识学完，接下来又到了 show me the code 的时间了。这里，我们继续使用 Spark 实现矩阵分解算法。我把关键的代码放在了下面，你可以参考一下。

我们可以看到，因为 Spark MLlib 已经帮我们封装好了模型，所以矩阵分解算法实现起来非常简单，还是通过我们熟悉的三步来完成，分别是定义模型，使用 fit 函数训练模型，提取物品和用户向量。但是有一点我们需要注意，就是在模型中，我们需要在模型中指定训练样本中用户 ID 对应的列 userIdInt 和物品 ID 对应的列 movieIdInt，并且两个 ID 列对应的数据类型需要是 Int 类型的。

// 建立矩阵分解模型
val als = new ALS()
  .setMaxIter(5)
  .setRegParam(0.01)
  .setUserCol("userIdInt")
  .setItemCol("movieIdInt")
  .setRatingCol("ratingFloat")

//训练模型
val model = als.fit(training)

//得到物品向量和用户向量
model.itemFactors.show(10, truncate = false)
model.userFactors.show(10, truncate = false

其实，矩阵分解算法得出的结果，你完全可以把它当作 Embedding 来处理。具体怎么做呢？在讲 Redis 的时候，我们就已经实现过物品 Embedding 和用户 Embedding 的存储和线上预估的过程了，你可以直接参考它。最后，我建议你利用矩阵分解后的用户和物品隐向量，仿照其他 Embedding 的实现，在 SparrowRecSys 中动手实现一下线上部署的过程，这样你就可以看到矩阵分解模型的实际效果了。

小结

这节课我们一起学习了协同过滤算法，以及它的后续算法矩阵分解。作为最经典的推荐算法，我们应该好好地掌握并且实践它。

总结来说，协同过滤是一种协同大家的反馈、评价和意见，对海量的信息进行过滤，从中筛选出用户感兴趣信息的一种推荐算法。它的实现过程主要有三步，先根据用户行为历史创建共现矩阵，然后根据共现矩阵查找相似用户，再根据相似用户喜欢的物品，推荐目标用户喜欢的物品。

但是协同过滤处理稀疏矩阵的能力比较差，因此，矩阵分解算法被提出了，它通过分解共现矩阵，生成用户向量矩阵和物品向量矩阵，进而得到用户隐向量和物品隐向量。你可以完全把最后的结果当作用户 Embedding 和物品 Embedding 来处理。

针对这节课的重要知识点，我把它们都列在了下面的表格里，你可以看看。

课后思考

1. 基于协同过滤算法，你能找到进行相似“物品”推荐的方法吗？

2. 在 MovieLens 数据集中，不同用户对物品打分的标准不尽相同。比如说，有的人可能爱打高分，评价的影片得分都在 4 分以上，有的人爱打低分，大部分影片都在 3 分以下。你觉得这样的偏好对于推荐结果有影响吗？我们能不能在算法中消除这种偏好呢？

关于矩阵分解算法的实现你学会了吗？欢迎把你的疑问和思考分享到留言区，也欢迎你能把这节课转发出去，我们下节课见！

16 | 深度学习革命：深度学习推荐模型发展的整体脉络是怎样的？

你好，我是王喆。今天，我们要开始学习激动人心的深度推荐模型部分了。

当下，几乎所有互联网巨头的推荐业务中，都有对深度学习推荐模型的落地和应用。比如说，从早期微软的 Deep Crossing、Google 的 Wide&Deep、阿里的 MLR，到现在影响力非常大的模型 DIN、DIEN，YouTube 的深度推荐模型等等。因此，对于我们算法工程师来说，紧跟业界的脚步，去了解和掌握深度学习推荐模型是非常必要的。

那你可能想问了，深度学习推荐模型这么多，发展这么快，也没有一个统一的模板，我们该学哪个，怎么学呢？我想说的是，算法工程师的工作是一个持续优化和迭代的过程，如果想要追求更好的推荐效果，我们的思路不应该只局限于某一个被成功应用的模型，而是应该把眼光放得更高、更宽，去思考这些成功的推荐模型在业界下一步的发展方向是什么？有没有哪些其他的模型结构的思路可以借鉴。这些都是你在这个岗位上取得持续成功的关键。

那怎么才能做到这一点呢？我认为，只有建立起一个比较全面的深度学习模型知识库，我们才能在工作中做出正确的技术选择，以及为模型的下一步改进方向找到思路。

因此，这节课，我想和你深入聊一聊业界影响力非常大的深度学习推荐模型，以及它们之间的发展关系，带你从整体上建立起深度学习推荐模型的发展脉络。这不仅是我们建立行业知识储备的必需，也为后面实现深度推荐模型打下了基础。

深度学习对推荐系统的影响详解

在第一节课中，我们曾说过，深度学习给推荐系统带来了革命性的影响，能够显著提升推荐系统的效果，原因主要有两点，一是深度学习极大地增强了推荐模型的拟合能力，二是深度学习模型可以利用模型结构模拟用户兴趣的变迁、用户注意力机制等不同的用户行为过程。接下来，我们就结合这两点，来说说深度学习模型到强在哪里。

1. 深度学习模型的强拟合能力

首先，我们来说说深度学习模型的强拟合能力。上一节课我们学习了经典的推荐算法，矩阵分解。在矩阵分解模型的结构（图 1 左）中，用户 One-hot 向量和物品 One-hot 向量分居两侧，它们会先通过隐向量层转换成用户和物品隐向量，再通过点积的方式交叉生成最终的打分预测。

但是，点积这种特征向量交叉的方式毕竟过于简单了，在数据模式比较复杂的情况下，往往存在欠拟合的情况。而深度学习就能大大加强模型的拟合能力，比如在 NeuralCF（神经网络协同过滤）模型中，点积层被替换为多层神经网络，理论上多层神经网络具备拟合任意函数的能力，所以我们通过增加神经网络层的方式就能解决模型欠拟合的问题了。

图1 矩阵分解模型示意图

如果你不知道什么是欠拟合、正确拟合和过拟合的现象，可能就无法理解神经网络到底解决了什么问题。这里我就带你看一张很经典的示意图，来详细聊聊这三种现象 。

“欠拟合”指的是模型复杂度低，无法很好地拟合训练集数据的现象。 就像图 2（左）展示的那样，模型曲线没办法“准确”地找到正负样本的分界线，而深度学习模型就可以大大增加模型的“非线性”拟合能力，像图 2（中）一样找到更加合适的分类面，更准确地完成分类任务。当然，过分复杂的深度学习模型存在着“过拟合”的风险，“过拟合”是指模型在训练集上的误差很小，但在测试集上的误差较大的现象，就像图 2（右）一样，模型曲线过分精确地刻画分界线而忽略了对噪声的容忍能力，这对于未知样本的预估来说往往是不利的。

图2拟合能力示意图（图片来自Geeksforgeeks）

2. 深度学习模型结构的灵活性

说完了深度学习模型的强拟合能力，我们再来看看它的灵活性。这里，你可能会有疑问了，灵活性和深度学习模型模拟用户行为有什么关系呢？我们先接着往下看。

如果你读过一些深度学习相关的论文，肯定会发现，每篇论文中的模型结构都不尽相同，就像图 3 展示的那样，它们有的是好多层，有的像一个串是串在一起的，而有的像一张网，拥有多个输入和多个输出，甚至还有的像金字塔，会从输入到输出逐层变窄。

虽然，模型结构的复杂性让我们难以掌握它们的规律，但也正是因为深度模型拥有这样的灵活性，让它能够更轻松地模拟人们的思考过程和行为过程，让推荐模型像一个无所不知的超级大脑一样，把用户猜得更透。

图3 不同深度学习模型的结构（图片来自ResearchGate）

这其中典型的例子就是阿里巴巴的模型 DIN（深度兴趣网络）和 DIEN（深度兴趣进化网络）。它们通过在模型结构中引入注意力机制和模拟兴趣进化的序列模型，来更好地模拟用户的行为。

图4 DIN模型（左）和DIEN的模型（右）示意图

我们重点关注图 4 的 DIN 模型，它在神经网络中增加了一个叫做“激活单元“的结构，这个单元就是为了模拟人类的注意力机制。举个例子来说，我们在购买电子产品，比如说笔记本电脑的时候，更容易拿之前购买电脑的经验，或者其他电子产品的经验来指导当前的购买行为，很少会借鉴购买衣服和鞋子的经验。这就是一个典型的注意力机制，我们只会注意到相关度更高的历史购买行为，而 DIN 模型就是模拟了人类这样的注意力特点。

DIN 模型的改进版 DIEN 模型就更厉害了，它不仅引入了注意力机制，还模拟了用户兴趣随时间的演化过程。我们来看那些彩色的层，这一层层的序列结构模拟的正是用户兴趣变迁的历史，通过模拟变迁的历史，DIEN 模型可以更好地预测下一步用户会喜欢什么。

这些通过改变模型结构来模拟用户行为的做法不胜枚举。很多重要的深度学习模型的改进动机正是基于这样的原理。也正是因为这样的灵活性，正确、全面地掌握不同深度学习模型的特点，以及它们之间的发展关系变得异常重要，只有这样，我们才能做到在实践中有的放矢、灵活应用。

深度学习推荐模型的演化关系图

说了这么多，我们到底该怎么掌握不同深度学习模型之间的关系呢？这里，我梳理出了一张深度学习模型 5 年内的发展过程图，图中的每一个节点都是一个重要的模型结构，节点之间的连线也揭示了不用模型间的联系。

接下来，我就带你梳理一下图中重要模型的原理，以及不同模型间的关系。而在之后的课程中，我们还会进一步学习重点模型的技术细节，并且基于 TensorFlow 对它们进行实现。

图5 主流深度学习推荐模型的演化图谱

首先，我们来看整个演化图最中心部分，这是深度学习最基础的结构，我们叫它“多层神经网络”或者“多层感知机”，简称 MLP（MultiLayer Perceptron）。多层感知机的原理我们在第 3 节课中讲过，它就像一个黑盒，对输入的特征进行深度地组合交叉，然后输出对兴趣值的预测。其他的深度推荐模型全都是在多层感知机的基础上，进行结构上的改进而生成的，所以“多层感知机”是整个演化图的核心。

从多层感知机向上，还有一个重点模型我们需要知道，那就是 Deep Crossing。Deep Crossing 实际上是一类经典深度学习模型的代表，相比于 MLP，Deep Crossing 在原始特征和 MLP 之间加入了 Embedding 层。这样一来，输入的稀疏特征先转换成稠密 Embedding 向量，再参与到 MLP 中进行训练，这就解决了 MLP 不善于处理稀疏特征的问题。可以这么说，Embedding+MLP 的结构是最经典，也是应用最广的深度学习推荐模型结构。

从 MLP 向下，我们看到了 Google 提出的推荐模型 Wide&Deep。它把深层的 MLP 和单层的神经网络结合起来，希望同时让网络具备很好的“记忆性”和“泛化性”。对“记忆性”和“泛化性”这两个名词陌生的同学也不用着急，我们后面的课程会专门来讲解 Wide&Deep。

Wide&Deep 提出以来，凭借着“易实现”“易落地”“易改造”的特点，获得了业界的广泛应用。围绕着 Wide&Deep 还衍生出了诸多变种，比如，通过改造 Wide 部分提出的 Deep&Cross 和 DeepFM，以及通过改造 Deep 部分提出的 AFM，NFM 等等。总之，Wide&Deep 是业界又一得到广泛应用的深度推荐模型。

除此之外，我们还可以看到经典的深度学习模型跟其他机器学习子领域的交叉。这里，我给你举 3 个比较著名的例子：第 1 个是深度学习和注意力机制的结合，诞生了阿里的深度兴趣网络 DIN，浙大和新加坡国立提出的 AFM 等等；；第 2 个是把序列模型引入 MLP+Embedding 的经典结构，诞生了阿里的深度兴趣进化网络 DIEN；第 3 个是把深度学习和强化学习结合在一起，诞生了微软的深度强化学习网络 DRN，以及包括美团、阿里在内的非常有价值的业界应用。

看了这诸多模型的演进过程，你肯定想问模型的演化有什么规律可循吗？接下来，我就把我总结出的，关于模型改进的 4 个方向告诉你 。

一是改变神经网络的复杂程度。 从最简单的单层神经网络模型 AutoRec，到经典的深度神经网络结构 Deep Crossing，它们主要的进化方式在于增加了深度神经网络的层数和结构复杂度。

二是改变特征交叉方式。 这种演进方式的要点在于大大提高了深度学习网络中特征交叉的能力。比如说，改变了用户向量和物品向量互操作方式的 NeuralCF，定义了多种特征向量交叉操作的 PNN 等等。

三是把多种模型组合应用。 组合模型主要指的就是以 Wide&Deep 模型为代表的一系列把不同结构组合在一起的改进思路。它通过组合两种甚至多种不同特点、优势互补的深度学习网络，来提升模型的综合能力。

四是让深度推荐模型和其他领域进行交叉。 我们从 DIN、DIEN、DRN 等模型中可以看出，深度推荐模型无时无刻不在从其他研究领域汲取新的知识。事实上，这个过程从未停歇，我们从今年的推荐系统顶会 Recsys2020 中可以看到，NLP 领域的著名模型 Bert 又与推荐模型结合起来，并且产生了非常好的效果。一般来说，自然语言处理、图像处理、强化学习这些领域都是推荐系统经常汲取新知识的地方。

总的来说，深度学习推荐模型的发展快、思路广。但每种模型都不是无本之木，它们的发展脉络都有迹可循，而想要掌握好这些模型，在实际工作中做到拿来就用，我们就需要让这些模型脉络图像知识树一样扎根在心中，再通过不断地实践来掌握技术细节。

小结

这节课，我们通过学习深度学习对推荐系统的影响要素，以及经典深度学习模型之间的关系，初步建立起了深度学习模型的知识库。

我们知道，深度学习能够提升推荐系统的效果有两个关键因素，分别是它的“强拟合能力”和“结构的灵活性”。

对于“强拟合能力”来说，深度学习模型可以大大增加模型的“非线性”拟合能力，对复杂数据模型进行更准确的分类，避免“欠拟合”现象的发生，从而提升推荐效果。

对于“结构的灵活性”来说，深度学习模型可以通过灵活调整自身的结构，更轻松恰当地模拟人们的思考过程和行为过程，把用户猜得更透。

而整个深度学习推荐模型的演化过程，是从最经典的多层神经网络向不同方向开枝散叶，比如结合协同过滤发展出了 NerualCF，加入 Embedding 层发展出以 Deep Crossing 为代表的 Embedding+MLP 的结构，以及把深度神经网络和单层网络结合起来发展出 Wide&Deep 模型等等。

在这节课里，我们可以先忽略每个模型的细节，着重建立一个整体的知识框架，在之后的课程中，我不仅会带你一一揭晓它们的技术细节，还会利用 TensorFlow 实现其中几个经典的模型。期待继续与你一起学习！

最后，我还是把这节课的重点知识梳理成了表格的形式，你可以借助它来复习巩固。

课后思考

有的同学说，深度学习这么流行，我把一些经典的深度模型结构实现好，肯定能提升我们公司推荐系统的效果，你觉得这种观点有问题吗？你觉得除了模型结构，还有哪些影响推荐效果的因素？为什么？

好啦，关于深度学习模型的知识库你建立起来了吗？欢迎把你的疑问和思考分享到留言区，也欢迎你能把这节课转发出去，我们下节课见！

17 | Embedding+MLP：如何用TensorFlow实现经典的深度学习模型？

你好，我是王喆。

今天我们正式进入深度学习模型的实践环节，来一起学习并实现一种最经典的模型结构，Embedding+MLP。它不仅经典，还是我们后续实现其他深度学习模型的基础，所以你一定要掌握好。

这里面的 Embedding 我们已经很熟悉了，那什么叫做 MLP 呢？它其实是 Multilayer perceptron，多层感知机的缩写。感知机是神经元的另外一种叫法，所以多层感知机就是多层神经网络。

讲到这里啊，我想你脑海中已经有这个模型结构的大致图像了。今天，我就以微软著名的深度学习模型 Deep Crossing 为例，来给你详细讲一讲 Embedding+MLP 模型的结构和实现方法。

Embedding+MLP 模型的结构

图 1 展示的就是微软在 2016 年提出的深度学习模型 Deep Crossing，微软把它用于广告推荐这个业务场景上。它是一个经典的 Embedding+MLP 模型结构，我们可以看到，Deep Crossing 从下到上可以分为 5 层，分别是 Feature 层、Embedding 层、Stacking 层、MLP 层和 Scoring 层。

接下来，我就从下到上来给你讲讲每一层的功能是什么，以及它们的技术细节分别是什么样的。

图1 经典的Embedding+MLP模型结构（图片来自 Deep Crossing - Web-Scale Modeling without Manually Crafted Combinatorial Features）

我们先来看 Feature 层。Feature 层也叫做输入特征层，它处于 Deep Crossing 的最底部，作为整个模型的输入。仔细看图 1 的话，你一定会发现不同特征在细节上的一些区别。比如 Feature#1 向上连接到了 Embedding 层，而 Feature#2 就直接连接到了更上方的 Stacking 层。这是怎么回事呢？

原因就在于 Feature#1 代表的是类别型特征经过 One-hot 编码后生成的特征向量，而 Feature#2 代表的是数值型特征。我们知道，One-hot 特征太稀疏了，不适合直接输入到后续的神经网络中进行训练，所以我们需要通过连接到 Embedding 层的方式，把这个稀疏的 One-hot 向量转换成比较稠密的 Embedding 向量。

接着，我们来看 Embedding 层。Embedding 层就是为了把稀疏的 One-hot 向量转换成稠密的 Embedding 向量而设置的，我们需要注意的是，Embedding 层并不是全部连接起来的，而是每一个特征对应一个 Embedding 层，不同 Embedding 层之间互不干涉。

那 Embedding 层的内部结构到底是什么样子的呢？我想先问问你，你还记得 Word2vec 的原理吗？Embeding 层的结构就是 Word2vec 模型中从输入神经元到隐层神经元的部分（如图 2 红框内的部分）。参照下面的示意图，我们可以看到，这部分就是一个从输入层到隐层之间的全连接网络。

图2 Word2vec模型中Embedding层的部分

一般来说，Embedding 向量的维度应远小于原始的稀疏特征向量，按照经验，几十到上百维就能够满足需求，这样它才能够实现从稀疏特征向量到稠密特征向量的转换。

接着我们来看 Stacking 层。Stacking 层中文名是堆叠层，我们也经常叫它连接（Concatenate）层。它的作用比较简单，就是把不同的 Embedding 特征和数值型特征拼接在一起，形成新的包含全部特征的特征向量。

再往上看，MLP 层就是我们开头提到的多层神经网络层，在图 1 中指的是 Multiple Residual Units 层，中文叫多层残差网络。微软在实现 Deep Crossing 时针对特定的问题选择了残差神经元，但事实上，神经元的选择有非常多种，比如我们之前在深度学习基础知识中介绍的，以 Sigmoid 函数为激活函数的神经元，以及使用 tanh、ReLU 等其他激活函数的神经元。我们具体选择哪种是一个调参的问题，一般来说，ReLU 最经常使用在隐层神经元上，Sigmoid 则多使用在输出神经元，实践中也可以选择性地尝试其他神经元，根据效果作出最后的决定。

不过，不管选择哪种神经元，MLP 层的特点是全连接，就是不同层的神经元两两之间都有连接。就像图 3 中的两层神经网络一样，它们两两连接，只是连接的权重会在梯度反向传播的学习过程中发生改变。

图3 全连接神经网络

MLP 层的作用是让特征向量不同维度之间做充分的交叉，让模型能够抓取到更多的非线性特征和组合特征的信息，这就使深度学习模型在表达能力上较传统机器学习模型大为增强。

最后是 Scoring 层，它也被称为输出层。虽然深度学习模型的结构可以非常复杂，但最终我们要预测的目标就是一个分类的概率。如果是点击率预估，就是一个二分类问题，那我们就可以采用逻辑回归作为输出层神经元，而如果是类似图像分类这样的多分类问题，我们往往在输出层采用 softmax 这样的多分类模型。

到这里，我们就讲完了 Embedding+MLP 的五层结构。它的结构重点用一句话总结就是，对于类别特征，先利用 Embedding 层进行特征稠密化，再利用 Stacking 层连接其他特征，输入 MLP 的多层结构，最后用 Scoring 层预估结果。

Embedding+MLP 模型的实战

现在，我们从整体上了解了 Embedding+MLP 模型的结构，也许你对其中的细节实现还有些疑问。别着急，下面我就带你用 SparrowRecsys 来实现一个 Embedding+MLP 的推荐模型，帮你扫清这些疑问。

实战中，我们按照构建推荐模型经典的步骤，特征选择、模型设计、模型实现、模型训练和模型评估这 5 步来进行实现。

首先，我们来看看特征选择和模型设计。

特征选择和模型设计

在上一节的实践准备课程中，我们已经为模型训练准备好了可用的训练样本和特征。秉着“类别型特征 Embedding 化，数值型特征直接输入 MLP”的原则，我们选择 movieId、userId、movieGenre、userGenre 作为 Embedding 化的特征，选择物品和用户的统计型特征作为直接输入 MLP 的数值型特征，具体的特征选择你可以看看下面的表格：

选择好特征后，就是 MLP 部分的模型设计了。我们选择了一个三层的 MLP 结构，其中前两层是 128 维的全连接层。我们这里采用好评 / 差评标签作为样本标签，因此要解决的是一个类 CTR 预估的二分类问题，对于二分类问题，我们最后一层采用单个 sigmoid 神经元作为输出层就可以了。

当然了，我知道你肯定对这一步的细节实现有很多问题，比如为什么要选三层的 MLP 结构，为什么要选 sigmoid 作为激活函数等等。其实，我们对模型层数和每个层内维度的选择是一个超参数调优的问题，这里的选择不能保证最优，我们需要在实战中需要根据模型的效果进行超参数的搜索，找到最适合的模型参数。

Embedding+MLP 模型的 TensorFlow 实现

确定好了特征和模型结构，万事俱备，只欠实现了。下面，我们就看一看利用 TensorFlow 的 Keras 接口如何实现 Embedding+MLP 的结构。总的来说，TensorFlow 的实现有七个步骤。因为这是我们课程中第一个 TensorFlow 的实现，所以我会讲得详细一些。而且，我也把全部的参考代码放在了 SparrowRecsys 项目 TFRecModel 模块的 EmbeddingMLP.py，你可以结合它来听我下面的讲解。

我们先来看第一步，导入 TensorFlow 包。 如果你按照实战准备一的步骤配置好了 TensorFlow Python 环境，就可以成功地导入 Tensorflow 包。接下来，你要做的就是定义好训练数据的路径 TRAIN_DATA_URL 了，然后根据你自己训练数据的本地路径，替换参考代码中的路径就可以了。

import tensorflow as tf

TRAIN_DATA_URL = "file:///Users/zhewang/Workspace/SparrowRecSys/src/main/resources/webroot/sampledata/modelSamples.csv"
samples_file_path = tf.keras.utils.get_file("modelSamples.csv", TRAIN_DATA_URL)

第二步是载入训练数据，我们利用 Tensorflow 自带的 CSV 数据集的接口载入训练数据。注意这里有两个比较重要的参数，一个是 label_name，它指定了 CSV 数据集中的标签列。另一个是 batch_size，它指定了训练过程中，一次输入几条训练数据进行梯度下降训练。载入训练数据之后，我们把它们分割成了测试集和训练集。

def get_dataset(file_path):
    dataset = tf.data.experimental.make_csv_dataset(
        file_path,
        batch_size=12,
        label_name='label',
        na_value="?",
        num_epochs=1,
        ignore_errors=True)
    return dataset

# sample dataset size 110830/12(batch_size) = 9235
raw_samples_data = get_dataset(samples_file_path)

test_dataset = raw_samples_data.take(1000)
train_dataset = raw_samples_data.skip(1000)

第三步是载入类别型特征。 我们用到的类别型特征主要有这三类，分别是 genre、userId 和 movieId。在载入 genre 类特征时，我们采用了 tf.feature_column.categorical_column_with_vocabulary_list 方法把字符串型的特征转换成了 One-hot 特征。在这个转换过程中我们需要用到一个词表，你可以看到我在开头就定义好了包含所有 genre 类别的词表 genre_vocab。

在转换 userId 和 movieId 特征时，我们又使用了 tf.feature_column.categorical_column_with_identity 方法把 ID 转换成 One-hot 特征，这个方法不用词表，它会直接把 ID 值对应的那个维度置为 1。比如，我们输入这个方法的 movieId 是 340，总的 movie 数量是 1001，使用这个方法，就会把这个 1001 维的 One-hot movieId 向量的第 340 维置为 1，剩余的维度都为 0。

为了把稀疏的 One-hot 特征转换成稠密的 Embedding 向量，我们还需要在 One-hot 特征外包裹一层 Embedding 层，你可以看到 tf.feature_column.embedding_column(movie_col, 10) 方法完成了这样的操作，它在把 movie one-hot 向量映射到了一个 10 维的 Embedding 层上。

genre_vocab = ['Film-Noir', 'Action', 'Adventure', 'Horror', 'Romance', 'War', 'Comedy', 'Western', 'Documentary',
               'Sci-Fi', 'Drama', 'Thriller',
               'Crime', 'Fantasy', 'Animation', 'IMAX', 'Mystery', 'Children', 'Musical']

GENRE_FEATURES = {
    'userGenre1': genre_vocab,
    'userGenre2': genre_vocab,
    'userGenre3': genre_vocab,
    'userGenre4': genre_vocab,
    'userGenre5': genre_vocab,
    'movieGenre1': genre_vocab,
    'movieGenre2': genre_vocab,
    'movieGenre3': genre_vocab
}

categorical_columns = []
for feature, vocab in GENRE_FEATURES.items():
    cat_col = tf.feature_column.categorical_column_with_vocabulary_list(
        key=feature, vocabulary_list=vocab)
    emb_col = tf.feature_column.embedding_column(cat_col, 10)
    categorical_columns.append(emb_col)

movie_col = tf.feature_column.categorical_column_with_identity(key='movieId', num_buckets=1001)
movie_emb_col = tf.feature_column.embedding_column(movie_col, 10)
categorical_columns.append(movie_emb_col)

user_col = tf.feature_column.categorical_column_with_identity(key='userId', num_buckets=30001)
user_emb_col = tf.feature_column.embedding_column(user_col, 10)
categorical_columns.append(user_emb_c

第四步是数值型特征的处理。 这一步非常简单，我们直接把特征值输入到 MLP 内，然后把特征逐个声明为 tf.feature_column.numeric_column 就可以了，不需要经过其他的特殊处理。

numerical_columns = [tf.feature_column.numeric_column('releaseYear'),
                   tf.feature_column.numeric_column('movieRatingCount'),
                     tf.feature_column.numeric_column('movieAvgRating'),
                     tf.feature_column.numeric_column('movieRatingStddev'),
                     tf.feature_column.numeric_column('userRatingCount'),
                     tf.feature_column.numeric_column('userAvgRating'),
                     tf.feature_column.numeric_column('userRatingStddev')]

第五步是定义模型结构。 这一步的实现代码也非常简洁，我们直接利用 DenseFeatures 把类别型 Embedding 特征和数值型特征连接在一起形成稠密特征向量，然后依次经过两层 128 维的全连接层，最后通过 sigmoid 输出神经元产生最终预估值。

preprocessing_layer = tf.keras.layers.DenseFeatures(numerical_columns + categorical_columns)

model = tf.keras.Sequential([
    preprocessing_layer,
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dense(1, activation='sigmoid'),
])

第六步是定义模型训练相关的参数。 在这一步中，我们需要设置模型的损失函数，梯度反向传播的优化方法，以及模型评估所用的指标。关于损失函数，我们使用的是二分类问题最常用的二分类交叉熵，优化方法使用的是深度学习中很流行的 adam，最后是评估指标，使用了准确度 accuracy 作为模型评估的指标。

model.compile(
    loss='binary_crossentropy',
    optimizer='adam',
    metrics=['accuracy'])

第七步是模型的训练和评估。 TensorFlow 模型的训练过程和 Spark MLlib 一样，都是调用 fit 函数，然后使用 evaluate 函数在测试集上进行评估。不过，这里我们要注意一个参数 epochs，它代表了模型训练的轮数，一轮代表着使用所有训练数据训练一遍，epochs=10 代表着训练 10 遍。

model.fit(train_dataset, epochs=10)

test_loss, test_accuracy = model.evaluate(test_dataset)

print('\n\nTest Loss {}, Test Accuracy {}'.format(test_loss, test_accuracy)

如果一切顺利的话，你就可以看到模型的训练过程和最终的评估结果了。从下面的训练输出中你可以看到，每轮训练模型损失（loss）的变化过程和模型评估指标 accruacy 的变化过程。你肯定会发现，随着每轮训练的 loss 减小，accruacy 会变高。换句话说，每轮训练都会让模型结果更好，这是我们期望看到的。需要注意的是，理论上来说，我们应该在模型 accuracy 不再变高时停止训练，据此来确定最佳的 epochs 取值。但如果模型收敛的时间确实过长，我们也可以设置一个 epochs 最大值，让模型提前终止训练。

Epoch 1/10
8236/8236 [==============================] - 20s 2ms/step - loss: 2.7379 - accuracy: 0.5815
Epoch 2/10
8236/8236 [==============================] - 21s 3ms/step - loss: 0.6397 - accuracy: 0.6659
Epoch 3/10
8236/8236 [==============================] - 21s 3ms/step - loss: 0.5550 - accuracy: 0.7179
Epoch 4/10
8236/8236 [==============================] - 21s 2ms/step - loss: 0.5209 - accuracy: 0.7431
Epoch 5/10
8236/8236 [==============================] - 21s 2ms/step - loss: 0.5010 - accuracy: 0.7564
Epoch 6/10
8236/8236 [==============================] - 20s 2ms/step - loss: 0.4866 - accuracy: 0.7641
Epoch 7/10
8236/8236 [==============================] - 20s 2ms/step - loss: 0.4770 - accuracy: 0.7702
Epoch 8/10
8236/8236 [==============================] - 21s 2ms/step - loss: 0.4688 - accuracy: 0.7745
Epoch 9/10
8236/8236 [==============================] - 20s 2ms/step - loss: 0.4633 - accuracy: 0.7779
Epoch 10/10
8236/8236 [==============================] - 20s 2ms/step - loss: 0.4580 - accuracy: 0.7800
1000/1000 [==============================] - 1s 1ms/step - loss: 0.5037 - accuracy: 0.7473

Test Loss 0.5036991238594055, Test Accuracy 0.747250020503997

最终的模型评估需要在测试集上进行，从上面的输出中我们可以看到，最终的模型在测试集上的准确度是 0.7472，它意味着我们的模型对 74.72% 的测试样本作出了正确的预测。当然了，模型的评估指标还是很多，我们会在之后的模型评估篇中进行详细的讲解。

小结

这节课是我们深度学习模型实践的第一课，我们要掌握两个重点内容，一是 Embedding+MLP 的模型结构，二是 Embedding+MLP 模型的 TensorFlow 实现。

Embedding+MLP 主要是由 Embedding 部分和 MLP 部分这两部分组成，使用 Embedding 层是为了将类别型特征转换成 Embedding 向量，MLP 部分是通过多层神经网络拟合优化目标。具体来说，以微软的 Deep Crossing 为例，模型一共分为 5 层，从下到上分别是 Feature 层、Embedding 层、Stacking 层、MLP 层和 Scoring 层。

在 TensorFlow 实践部分，我们利用上节课处理好的特征和训练数据，实现了 SparrowRecsys 项目中的第一个深度学习模型。在实践过程中，我们要重点掌握类别型特征的处理方法，模型的定义方式和训练方式，以及最后的模型评估方法。

我也把这些重点知识总结在了一张表格里，你可以利用它来认真回顾。

今天，我们一起完成了 Embedding MLP 模型的实现。在之后的课程中，我们会进一步实现其他深度学习模型，通过模型的评估进行效果上的对比。另外，我们也会利用训练出的深度学习模型完成 SparrowRecsys 的猜你喜欢功能，期待与你一起不断完善我们的项目。

课后思考

在我们实现的 Embedding+MLP 模型中，也有用户 Embedding 层和物品 Embedding 层。你觉得从这两个 Embedding 层中，抽取出来的用户和物品 Embedding，能直接用来计算用户和物品之间的相似度吗？为什么？

欢迎把你的思考和疑惑写在留言区，也欢迎你把这节课转发给希望用 TensorFlow 实现深度推荐模型的朋友，我们下节课见！

18｜Wide&Deep：怎样让你的模型既有想象力又有记忆力？

你好，我是王喆。

今天，我们来学习一个在业界有着巨大影响力的推荐模型，Google 的 Wide&Deep。可以说，只要掌握了 Wide&Deep，我们就抓住了深度推荐模型这几年发展的一个主要方向。那 Wide&Deep 模型是什么意思呢？我们把它翻译成中文就是“宽并且深的模型”。

这个名字看起来好像很通俗易懂，但你真的理解其中“宽”和“深”的含义吗？上一节课我们讲过 Embedding+MLP 的经典结构，因为 MLP 可以有多层神经网络的结构，所以它是一个比较“深”的模型，但 Wide&Deep 这个模型说的“深”和 MLP 是同样的意思吗？“宽”的部分又是什么样的呢？宽和深分别有什么不同的作用呢？以及我们为什么要把它们结合在一起形成一个模型呢？

这节课，就让我们就带着这诸多疑问，从模型的结构开始学起，再深入到 Wide&Deep 在 Google 的应用细节中去，最后亲自动手实现这个模型。

Wide&Deep 模型的结构

首先，我们来看看 Wide&Deep 模型的结构，理解了结构再深入去学习细节，我们才能掌握得更好。

图1 Wide&Deep模型结构
（出自Wide & Deep Learning for Recommender Systems ）

上图就是 Wide&Deep 模型的结构图了，它是由左侧的 Wide 部分和右侧的 Deep 部分组成的。Wide 部分的结构太简单了，就是把输入层直接连接到输出层，中间没有做任何处理。Deep 层的结构稍复杂，但我相信你也不会陌生，因为它就是我们上节课学习的 Embedding+MLP 的模型结构。

知道了 Wide&Deep 模型的结构之后，我们先来解决第一个问题，那就是 Google 为什么要创造这样一个混合式的模型结构呢？这我们还得从 Wide 部分和 Deep 部分的不同作用说起。

简单来说，Wide 部分的主要作用是让模型具有较强的“记忆能力”（Memorization），而 Deep 部分的主要作用是让模型具有“泛化能力”（Generalization），因为只有这样的结构特点，才能让模型兼具逻辑回归和深度神经网络的优点，也就是既能快速处理和记忆大量历史行为特征，又具有强大的表达能力，这就是 Google 提出这个模型的动机。

那么问题又来了，所谓的“记忆能力”和“泛化能力”到底指什么呢？这我们就得好好聊一聊了，因为理解这两种能力是彻底理解 Wide&Deep 模型的关键。

模型的记忆能力

所谓的 “记忆能力”，可以被宽泛地理解为模型直接学习历史数据中物品或者特征的“共现频率”，并且把它们直接作为推荐依据的能力 。 就像我们在电影推荐中可以发现一系列的规则，比如，看了 A 电影的用户经常喜欢看电影 B，这种“因为 A 所以 B”式的规则，非常直接也非常有价值。

但这类规则有两个特点：一是数量非常多，一个“记性不好”的推荐模型很难把它们都记住；二是没办法推而广之，因为这类规则非常具体，没办法或者说也没必要跟其他特征做进一步的组合。就像看了电影 A 的用户 80% 都喜欢看电影 B，这个特征已经非常强了，我们就没必要把它跟其他特征再组合在一起。

现在，我们就可以回答开头的问题了，为什么模型要有 Wide 部分？就是因为 Wide 部分可以增强模型的记忆能力，让模型记住大量的直接且重要的规则，这正是单层的线性模型所擅长的。

模型的泛化能力

接下来，我们来谈谈模型的“泛化能力”。“泛化能力”指的是模型对于新鲜样本、以及从未出现过的特征组合的预测能力。 这怎么理解呢？我们还是来看一个例子。假设，我们知道 25 岁的男性用户喜欢看电影 A，35 岁的女性用户也喜欢看电影 A。如果我们想让一个只有记忆能力的模型回答，“35 岁的男性喜不喜欢看电影 A”这样的问题，这个模型就会“说”，我从来没学过这样的知识啊，没法回答你。

这就体现出泛化能力的重要性了。模型有了很强的泛化能力之后，才能够对一些非常稀疏的，甚至从未出现过的情况作出尽量“靠谱”的预测。

回到刚才的例子，有泛化能力的模型回答“35 岁的男性喜不喜欢看电影 A”这个问题，它思考的逻辑可能是这样的：从第一条知识，“25 岁的男性用户喜欢看电影 A“中，我们可以学到男性用户是喜欢看电影 A 的。从第二条知识，“35 岁的女性用户也喜欢看电影 A”中，我们可以学到 35 岁的用户是喜欢看电影 A 的。那在没有其他知识的前提下，35 岁的男性同时包含了合适的年龄和性别这两个特征，所以他大概率也是喜欢电影 A 的。这就是模型的泛化能力。

事实上，我们学过的矩阵分解算法，就是为了解决协同过滤“泛化能力”不强而诞生的。因为协同过滤只会“死板”地使用用户的原始行为特征，而矩阵分解因为生成了用户和物品的隐向量，所以就可以计算任意两个用户和物品之间的相似度了。这就是泛化能力强的另一个例子。

从上节课中我们学过深度学习模型有很强的数据拟合能力，在多层神经网络之中，特征可以得到充分的交叉，让模型学习到新的知识。因此，Wide&Deep 模型的 Deep 部分，就沿用了上节课介绍的 Embedding+MLP 的模型结构，来增强模型的泛化能力。

好，清楚了记忆能力和泛化能力是什么之后，让我们再回到 Wide&Deep 模型提出时的业务场景上，去理解 Wide&Deep 模型是怎么综合性地学习到记忆能力和泛化能力的。

Wide&Deep 模型的应用场景

Wide&Deep 模型是由 Google 的应用商店团队 Google Play 提出的，在 Google Play 为用户推荐 APP 这样的应用场景下，Wide&Deep 模型的推荐目标就显而易见了，就是应该尽量推荐那些用户可能喜欢，愿意安装的应用。那具体到 Wide&Deep 模型中，Google Play 团队是如何为 Wide 部分和 Deep 部分挑选特征的呢？下面，我们就一起来看看。

图2 Google Play Wide&Deep模型的细节
（出自Wide & Deep Learning for Recommender Systems ）

我们先来看看图 2，它补充了 Google Play Wide&Deep 模型的细节，让我们可以清楚地看到各部分用到的特征是什么。我们先从右边 Wide 部分的特征看起。这部分很简单，只利用了两个特征的交叉，这两个特征是“已安装应用”和“当前曝光应用”。这样一来，Wide 部分想学到的知识就非常直观啦，就是希望记忆好“如果 A 所以 B”这样的简单规则。在 Google Play 的场景下，就是希望记住“如果用户已经安装了应用 A，是否会安装 B”这样的规则。

接着，我们再来看看左边的 Deep 部分，它就是一个非常典型的 Embedding+MLP 结构了。我们看到其中的输入特征很多，有用户年龄、属性特征、设备类型，还有已安装应用的 Embedding 等等。我们把这些特征一股脑地放进多层神经网络里面去学习之后，它们互相之间会发生多重的交叉组合，这最终会让模型具备很强的泛化能力。

比如说，我们把用户年龄、人口属性和已安装应用组合起来。假设，样本中有 25 岁男性安装抖音的记录，也有 35 岁女性安装抖音的记录，那我们该怎么预测 25 岁女性安装抖音的概率呢？这就需要用到已有特征的交叉来实现了。虽然我们没有 25 岁女性安装抖音的样本，但模型也能通过对已有知识的泛化，经过多层神经网络的学习，来推测出这个概率。

总的来说，Wide&Deep 通过组合 Wide 部分的线性模型和 Deep 部分的深度网络，取各自所长，就能得到一个综合能力更强的组合模型。

Wide&Deep 模型的 TensorFlow 实现

在理解了 Wide&Deep 模型的原理和技术细节之后，就又到了“show me the code”的环节了。接下来，我们就动手在 SparrowRecsys 上实现 Wide&Deep 模型吧！

通过上节课的实战，我相信你已经熟悉了 TensorFlow 的大部分操作，也清楚了载入训练数据，创建 TensorFlow 所需的 Feature column 的方法。因此，Wide&Deep 模型的实践过程中，我们会重点关注定义模型的部分。

这里，我们也会像上节课一样，继续使用 TensorFlow 的 Keras 接口来构建 Wide&Deep 模型。具体的代码如下：

# wide and deep model architecture
# deep part for all input features
deep = tf.keras.layers.DenseFeatures(numerical_columns + categorical_columns)(inputs)
deep = tf.keras.layers.Dense(128, activation='relu')(deep)
deep = tf.keras.layers.Dense(128, activation='relu')(deep)
# wide part for cross feature
wide = tf.keras.layers.DenseFeatures(crossed_feature)(inputs)
both = tf.keras.layers.concatenate([deep, wide])
output_layer = tf.keras.layers.Dense(1, activation='sigmoid')(both)
model = tf.keras.Model(inputs, output_layer)

从代码中我们可以看到，在创建模型的时候，我们依次配置了模型的 Deep 部分和 Wide 部分。我们先来看 Deep 部分，它是输入层加两层 128 维隐层的结构，它的输入是类别型 Embedding 向量和数值型特征，实际上这跟上节课 Embedding+MLP 模型所用的特征是一样的。

Wide 部分其实不需要有什么特殊操作，我们直接把输入特征连接到了输出层就可以了。但是，这里我们要重点关注一下 Wide 部分所用的特征 crossed_feature。

movie_feature = tf.feature_column.categorical_column_with_identity(key='movieId', num_buckets=1001)
rated_movie_feature = tf.feature_column.categorical_column_with_identity(key='userRatedMovie1', num_buckets=1001)
crossed_feature = tf.feature_column.crossed_column([movie_feature, rated_movie_feature], 10000)

在生成 crossed_feature 的过程中，我其实仿照了 Google Play 的应用方式，生成了一个由“用户已好评电影”和“当前评价电影”组成的一个交叉特征，就是代码中的 crossed_feature，设置这个特征的目的在于让模型记住好评电影之间的相关规则，更具体点来说就是，就是让模型记住“一个喜欢电影 A 的用户，也会喜欢电影 B”这样的规则。

当然，这样的规则不是唯一的，需要你根据自己的业务特点来设计， 比如在电商网站中，这样的规则可以是，购买了键盘的用户也会购买鼠标。在新闻网站中，可以是打开过足球新闻的用户，也会点击 NBA 新闻等等。

在 Deep 部分和 Wide 部分都构建完后，我们要使用 concatenate layer 把两部分连接起来，形成一个完整的特征向量，输入到最终的 sigmoid 神经元中，产生推荐分数。

总的来说，在我们上一节的 Embedding MLP 模型基础上实现 Wide&Deep 是非常方便的，Deep 部分基本没有变化，我们只需要加上 Wide 部分的特征和设置就可以了。Wide&Deep 的全部相关代码，我都实现在了 SparrowRecsys 的 WideNDeep.py 文件中，你可以直接参考源代码。但我更希望，你能尝试设置不同的特征，以及不同的参数组合，来真实地体验一下深度学习模型的调参过程。

小结

这节课，我们一起实现了业界影响力非常大的深度学习模型 Wide&Deep，它是由 Wide 部分和 Deep 部分组成的。其中，Wide 部分主要是为了增强模型的“记忆能力”，让模型记住“如果 A，那么 B”这样的简单但数量非常多的规则。Deep 部分是为了增强模型的“泛化能力”，让模型具备对于稀缺样本、以及从未出现过的特征组合的预测能力。Wide&Deep 正是通过这样取长补短的方式，让模型的综合能力提升。

在具体实践的时候，我们继续使用 TensorFlow 的 Keras 接口实现了 Wide&Deep 模型。相比上节课 Embedding MLP 模型的实现，我们新加入了“用户已好评电影”和“当前评价电影”组成的交叉特征 crossed_feature，让 Wide 部分学习“一个喜欢电影 A 的用户，也会喜欢电影 B”这样的规则。

好了，这就是我们这节课的主要内容，同样，我也把重要的知识点总结在了表格里，你可以利用它来巩固复习。

课后思考

对于 Deep 部分来说，你觉得我们一股脑地把所有特征都扔进 MLP 中去训练，这样的方式有没有什么改进的空间？比如说，“用户喜欢的电影风格”和“电影本身的风格”这两个特征，我们能不能进一步挖掘出它们之间的相关性，而不是简单粗暴地扔给神经网络去处理呢？

欢迎把你的思考和疑问写在留言区，如果的你朋友也正在为 Wide&Deep 模型的实现而困扰，欢迎你把这节课转发给他，我们下节课见！

19｜NeuralCF：如何用深度学习改造协同过滤？

你好，我是王喆，今天，我们来学习协同过滤的深度学习进化版本，NeuralCF。

在第 15 节课里，我们学习了最经典的推荐算法，协同过滤。在前深度学习的时代，协同过滤曾经大放异彩，但随着技术的发展，协同过滤相比深度学习模型的弊端就日益显现出来了，因为它是通过直接利用非常稀疏的共现矩阵进行预测的，所以模型的泛化能力非常弱，遇到历史行为非常少的用户，就没法产生准确的推荐结果了。

虽然，我们可以通过矩阵分解算法增强它的泛化能力，但因为矩阵分解是利用非常简单的内积方式来处理用户向量和物品向量的交叉问题的，所以，它的拟合能力也比较弱。这该怎么办呢？不是说深度学习模型的拟合能力都很强吗？我们能不能利用深度学习来改进协同过滤算法呢？

当然是可以的。2017 年，新加坡国立的研究者就使用深度学习网络来改进了传统的协同过滤算法，取名 NeuralCF（神经网络协同过滤）。NeuralCF 大大提高了协同过滤算法的泛化能力和拟合能力，让这个经典的推荐算法又重新在深度学习时代焕发生机。这节课，我们就一起来学习并实现 NeuralCF！

NeuralCF 模型的结构

在学习 NeuralCF 之前，我们先来简单回顾一下协同过滤和矩阵分解的原理。协同过滤是利用用户和物品之间的交互行为历史，构建出一个像图 1 左一样的共现矩阵。在共现矩阵的基础上，利用每一行的用户向量相似性，找到相似用户，再利用相似用户喜欢的物品进行推荐。

图1 矩阵分解算法的原理

矩阵分解则进一步加强了协同过滤的泛化能力，它把协同过滤中的共现矩阵分解成了用户矩阵和物品矩阵，从用户矩阵中提取出用户隐向量，从物品矩阵中提取出物品隐向量，再利用它们之间的内积相似性，进行推荐排序。如果用神经网络的思路来理解矩阵分解，它的结构图就是图 2 这样的。

图2 矩阵分解的神经网络化示意图

图 2 中的输入层是由用户 ID 和物品 ID 生成的 One-hot 向量，Embedding 层是把 One-hot 向量转化成稠密的 Embedding 向量表达，这部分就是矩阵分解中的用户隐向量和物品隐向量。输出层使用了用户隐向量和物品隐向量的内积作为最终预测得分，之后通过跟目标得分对比，进行反向梯度传播，更新整个网络。

把矩阵分解神经网络化之后，把它跟 Embedding+MLP 以及 Wide&Deep 模型做对比，我们可以一眼看出网络中的薄弱环节：矩阵分解在 Embedding 层之上的操作好像过于简单了，就是直接利用内积得出最终结果。这会导致特征之间还没有充分交叉就直接输出结果，模型会有欠拟合的风险。针对这一弱点，NeuralCF 对矩阵分解进行了改进，它的结构图是图 3 这样的。

图3 NeuralCF的模型结构图 （出自论文Neural Collaborative Filtering）

我想你一定可以一眼看出它们的区别，那就是 NeuralCF 用一个多层的神经网络替代掉了原来简单的点积操作。这样就可以让用户和物品隐向量之间进行充分的交叉，提高模型整体的拟合能力。

NeuralCF 模型的扩展，双塔模型

有了之前实现矩阵分解和深度学习模型的经验，我想你理解起来 NeuralCF 肯定不会有困难。事实上，NeuralCF 的模型结构之中，蕴含了一个非常有价值的思想，就是我们可以把模型分成用户侧模型和物品侧模型两部分，然后用互操作层把这两部分联合起来，产生最后的预测得分。

这里的用户侧模型结构和物品侧模型结构，可以是简单的 Embedding 层，也可以是复杂的神经网络结构，最后的互操作层可以是简单的点积操作，也可以是比较复杂的 MLP 结构。但只要是这种物品侧模型 + 用户侧模型 + 互操作层的模型结构，我们把它统称为“双塔模型”结构。

图 4 就是一个典型“双塔模型”的抽象结构。它的名字形象地解释了它的结构组成，两侧的模型结构就像两个高塔一样，而最上面的互操作层则像两个塔尖搭建起的空中走廊，负责两侧信息的沟通。

图4 双塔模型结构
（出自论文 Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations）

对于 NerualCF 来说，它只利用了用户 ID 作为“用户塔”的输入特征，用物品 ID 作为“物品塔”的输入特征。事实上，我们完全可以把其他用户和物品相关的特征也分别放入用户塔和物品塔，让模型能够学到的信息更全面。比如说，YouTube 在构建用于召回层的双塔模型时，就分别在用户侧和物品侧输入了多种不同的特征，如图 5 所示。

图5 YouTube双塔召回模型的架构
（出自论文 Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations）

我们看到，YouTube 召回双塔模型的用户侧特征包括了用户正在观看的视频 ID、频道 ID（图中的 seed features）、该视频的观看数、被喜欢的次数，以及用户历史观看过的视频 ID 等等。物品侧的特征包括了候选视频的 ID、频道 ID、被观看次数、被喜欢次数等等。在经过了多层 ReLU 神经网络的学习之后，双塔模型最终通过 softmax 输出层连接两部分，输出最终预测分数。

看到这里，你可能会有疑问，这个双塔模型相比我们之前学过的 Embedding MLP 和 Wide&Deep 有什么优势呢？其实在实际工作中，双塔模型最重要的优势就在于它易上线、易服务。为什么这么说呢？

你注意看一下物品塔和用户塔最顶端的那层神经元，那层神经元的输出其实就是一个全新的物品 Embedding 和用户 Embedding。拿图 4 来说，物品塔的输入特征向量是 x，经过物品塔的一系列变换，生成了向量 u(x)，那么这个 u(x) 就是这个物品的 Embedding 向量。同理，v(y) 是用户 y 的 Embedding 向量，这时，我们就可以把 u(x) 和 v(y) 存入特征数据库，这样一来，线上服务的时候，我们只要把 u(x) 和 v(y) 取出来，再对它们做简单的互操作层运算就可以得出最后的模型预估结果了！

所以使用双塔模型，我们不用把整个模型都部署上线，只需要预存物品塔和用户塔的输出，以及在线上实现互操作层就可以了。如果这个互操作层是点积操作，那么这个实现可以说没有任何难度，这是实际应用中非常容易落地的，也是工程师们喜闻乐见的，这也正是双塔模型在业界巨大的优势所在。

正是因为这样的优势，双塔模型被广泛地应用在 YouTube、Facebook、百度等各大公司的推荐场景中，持续发挥着它的能量。

NeuralCF 的 TensorFlow 实现

熟悉了 NerualCF 和双塔模型的结构之后，我们就可以使用 TensorFlow 来实现它们了。通过之前 Embedding+MLP 模型以及 Wide&Deep 模型的实现，我想你对 TensorFlow 中读取数据，定义特征，训练模型的过程肯定已经驾轻就熟了。我们只要更改之前代码中模型定义的部分，就可以实现 NeuralCF。具体的代码你可以参考 SparrowRecsys 项目中的 NeuralCF.py，我只贴出了 NeuralCF 模型部分的实现。下面，我们重点讲解一下它们的实现思路。

# neural cf model arch two. only embedding in each tower, then MLP as the interaction layers
def neural_cf_model_1(feature_inputs, item_feature_columns, user_feature_columns, hidden_units):
    # 物品侧特征层
    item_tower = tf.keras.layers.DenseFeatures(item_feature_columns)(feature_inputs)
    # 用户侧特征层
    user_tower = tf.keras.layers.DenseFeatures(user_feature_columns)(feature_inputs)
    # 连接层及后续多层神经网络
    interact_layer = tf.keras.layers.concatenate([item_tower, user_tower])
    for num_nodes in hidden_units:
        interact_layer = tf.keras.layers.Dense(num_nodes, activation='relu')(interact_layer)
    # sigmoid单神经元输出层
    output_layer = tf.keras.layers.Dense(1, activation='sigmoid')(interact_layer)
    # 定义keras模型
    neural_cf_model = tf.keras.Model(feature_inputs, output_layer)
    return neural_cf_model

你可以看到代码中定义的生成 NeuralCF 模型的函数，它接收了四个输入变量。其中 feature_inputs 代表着所有的模型输入， item_feature_columns 和 user_feature_columns 分别包含了物品侧和用户侧的特征。在训练时，如果我们只在 item_feature_columns 中放入 movie_id ，在 user_feature_columns 放入 user_id， 就是 NeuralCF 的经典实现了。

通过 DenseFeatures 层创建好用户侧和物品侧输入层之后，我们会再利用 concatenate 层将二者连接起来，然后输入多层神经网络进行训练。如果想要定义多层神经网络的层数和神经元数量，我们可以通过设置 hidden_units 数组来实现。

除了经典的 NeuralCF 实现，我还基于双塔模型的原理实现了一个 NeuralCF 的双塔版本。你可以参考下面的模型定义。与上面的经典 NerualCF 实现不同，我把多层神经网络操作放到了物品塔和用户塔内部，让塔内的特征进行充分交叉，最后使用内积层作为物品塔和用户塔的交互层。具体的步骤你可以参考下面代码中的注释，实现过程很好理解，我就不再赘述了。

# neural cf model arch one. embedding+MLP in each tower, then dot product layer as the output
def neural_cf_model_2(feature_inputs, item_feature_columns, user_feature_columns, hidden_units):
    # 物品侧输入特征层
    item_tower = tf.keras.layers.DenseFeatures(item_feature_columns)(feature_inputs)
    # 物品塔结构
    for num_nodes in hidden_units:
        item_tower = tf.keras.layers.Dense(num_nodes, activation='relu')(item_tower)
    # 用户侧输入特征层
    user_tower = tf.keras.layers.DenseFeatures(user_feature_columns)(feature_inputs)
    # 用户塔结构
    for num_nodes in hidden_units:
        user_tower = tf.keras.layers.Dense(num_nodes, activation='relu')(user_tower)
    # 使用内积操作交互物品塔和用户塔，产生最后输出
    output = tf.keras.layers.Dot(axes=1)([item_tower, user_tower])
    # 定义keras模型
    neural_cf_model = tf.keras.Model(feature_inputs, output)
    return neural_cf_model

在实现了 Embedding MLP、Wide&Deep 和 NeuralCF 之后，相信你可以感觉到，实现甚至创造一个深度学习模型并不难，基于 TensorFlow 提供的 Keras 接口，我们可以根据我们的设计思路，像搭积木一样实现模型的不同结构，以此来验证我们的想法，这也正是深度推荐模型的魅力和优势。相信随着课程的进展，你不仅对这一点能够有更深刻的感受，同时，你设计和实现模型的能力也会进一步加强。

小结

这节课，我们首先学习了经典推荐算法协同过滤的深度学习进化版本 NerualCF。相比于矩阵分解算法，NeuralCF 用一个多层的神经网络，替代了矩阵分解算法中简单的点积操作，让用户和物品隐向量之间进行充分的交叉。这种通过改进物品隐向量和用户隐向量互操作层的方法，大大增加了模型的拟合能力。

利用 NerualCF 的思想，我们进一步学习了双塔模型。它通过丰富物品侧和用户侧的特征，让模型能够融入除了用户 ID 和物品 ID 外更丰富的信息。除此之外，双塔模型最大的优势在于模型服务的便捷性，由于最终的互操作层是简单的内积操作或浅层神经网络。因此，我们可以把物品塔的输出当作物品 Embedding，用户塔的输出当作用户 Embedding 存入特征数据库，在线上只要实现简单的互操作过程就可以了。

最后，我们继续使用 TensorFlow 实现了 NerualCF 和双塔模型，相信你能进一步感受到利用 TensorFlow 构建深度学习模型的便捷性，以及它和传统推荐模型相比，在模型结构灵活性上的巨大优势。

为了帮助你复习，我把刚才说的这些重点内容总结在了一张图里，你可以看看。

课后思考

对于我们这节课学习的双塔模型来说，把物品侧的 Embedding 和用户侧的 Embedding 存起来，就可以进行线上服务了。但如果我们把一些场景特征，比如当前时间、当前地点加到用户侧或者物品侧，那我们还能用这种方式进行模型服务吗？为什么？

欢迎把你的思考和疑惑写在留言区，也欢迎你把这节课转发出去，我们下节课见！

20 | DeepFM：如何让你的模型更好地处理特征交叉？

你好，我是王喆。

前面几节课，我们学习了 Embedding MLP、Wide&Deep、NerualCF 等几种不同的模型结构。你有没有深入思考过这样一个问题，就是这几种模型都是怎么处理特征交叉这个问题的呢？

比如说，模型的输入有性别、年龄、电影风格这几个特征，在训练样本中我们发现有 25 岁男生喜欢科幻电影的样本，有 35 岁女生喜欢看恐怖电影的样本，那你觉得模型应该怎么推测“25 岁”的女生喜欢看的电影风格呢？

事实上，这类特征组合和特征交叉问题非常常见，而且在实际应用中，特征的种类还要多得多，特征交叉的复杂程度也要大得多。解决这类问题的关键，就是模型对于特征组合和特征交叉的学习能力，因为它决定了模型对于未知特征组合样本的预测能力，而这对于复杂的推荐问题来说，是决定其推荐效果的关键点之一。

但无论是 Embedding MLP，还是 Wide&Deep 其实都没有对特征交叉进行特别的处理，而是直接把独立的特征扔进神经网络，让它们在网络里面进行自由组合，就算是 NeuralCF 也只在最后才把物品侧和用户侧的特征交叉起来。那这样的特征交叉方法是高效的吗？深度学习模型有没有更好的处理特征交叉的方法呢？

这节课，我们就一起来解决这些问题。同时，我还会基于特征交叉的思想，带你学习和实现一种新的深度学习模型 DeepFM。

为什么深度学习模型需要加强特征交叉的能力？

不过，在正式开始今天的课程之前，我还想和你再深入聊聊，为什么深度学习需要加强特征交叉的能力。我们刚才说 Embedding MLP 和 Wide&Deep 模型都没有针对性的处理特征交叉问题，有的同学可能就会有疑问了，我们之前不是一直说，多层神经网络有很强的拟合能力，能够在网络内部任意地组合特征吗？这两个说法是不是矛盾了？

在进入正题前，我就带你先扫清这个疑问。我们之前一直说 MLP 有拟合任意函数的能力，这没有错，但这是建立在 MLP 有任意多层网络，以及任意多个神经元的前提下的。

在训练资源有限，调参时间有限的现实情况下，MLP 对于特征交叉的处理其实还比较低效。因为 MLP 是通过 concatenate 层把所有特征连接在一起成为一个特征向量的，这里面没有特征交叉，两两特征之间没有发生任何关系。

这个时候，在我们有先验知识的情况下，人为地加入一些负责特征交叉的模型结构，其实对提升模型效果会非常有帮助。比如，在我们 Sparrow RecSys 项目的训练样本中其实有两个这样的特征，一个是用户喜欢的电影风格，一个是电影本身的风格，这两个特征明显具有很强的相关性。如果我们能让模型利用起这样的相关性，肯定会对最后的推荐效果有正向的影响。

既然这样，那我们不如去设计一些特定的特征交叉结构，来把这些相关性强的特征，交叉组合在一起，这就是深度学习模型要加强特征交叉能力的原因了。

善于处理特征交叉的机器学习模型 FM

扫清了这个疑问，接下来，我们就要进入具体的深度学习模型的学习了，不过，先别着急，我想先和你聊聊传统的机器学习模型是怎么解决特征交叉问题的，看看深度学习模型能不能从中汲取到“养分”。

说到解决特征交叉问题的传统机器学习模型，我们就不得不提一下，曾经红极一时的机器学习模型因子分解机模型（Factorization Machine）了，我们可以简称它为 FM。

图1 FM的神经网络化结构
（出自论文 DeepFM: A Factorization-Machine based Neural Network for CTR Prediction）

首先，我们看上图中模型的最下面，它的输入是由类别型特征转换成的 One-hot 向量，往上就是深度学习的常规操作，也就是把 One-hot 特征通过 Embedding 层转换成稠密 Embedding 向量。到这里，FM 跟其他深度学习模型其实并没有区别，但再往上区别就明显了。

FM 会使用一个独特的层 FM Layer 来专门处理特征之间的交叉问题。你可以看到，FM 层中有多个内积操作单元对不同特征向量进行两两组合，这些操作单元会把不同特征的内积操作的结果输入最后的输出神经元，以此来完成最后的预测。

这样一来，如果我们有两个特征是用户喜爱的风格和电影本身的风格，通过 FM 层的两两特征的内积操作，这两个特征就可以完成充分的组合，不至于像 Embedding MLP 模型一样，还要 MLP 内部像黑盒子一样进行低效的交叉。

深度学习模型和 FM 模型的结合 DeepFM

这个时候问题又来了，FM 是一个善于进行特征交叉的模型，但是我们之前也讲过，深度学习模型的拟合能力强啊，那二者之间能结合吗？

学习过 Wide&Deep 结构之后，我们一定可以快速给出答案，我们当然可以把 FM 跟其他深度学习模型组合起来，生成一个全新的既有强特征组合能力，又有强拟合能力的模型。基于这样的思想，DeepFM 模型就诞生了。

DeepFM 是由哈工大和华为公司联合提出的深度学习模型，我把它的架构示意图放在了下面。

图2 DeepFM模型架构图
（出自论文 DeepFM: A Factorization-Machine based Neural Network for CTR Prediction）

结合模型结构图，我们可以看到，DeepFM 利用了 Wide&Deep 组合模型的思想，用 FM 替换了 Wide&Deep 左边的 Wide 部分，加强了浅层网络部分特征组合的能力，而右边的部分跟 Wide&Deep 的 Deep 部分一样，主要利用多层神经网络进行所有特征的深层处理，最后的输出层是把 FM 部分的输出和 Deep 部分的输出综合起来，产生最后的预估结果。这就是 DeepFM 的结构。

如何使用元素积的操作进行特征交叉？

接下来我们再思考一个问题，FM 和 DeepFM 中进行特征交叉的方式，都是进行 Embedding 向量的点积操作，那是不是说特征交叉就只能用点积操作了？

答案当然是否定的。事实上还有很多向量间的运算方式可以进行特征的交叉，比如模型 NFM（Neural Factorization Machines，神经网络因子分解机），它就使用了新的特征交叉方法。下面，我们一起来看一下。

图 3 就是 NFM 的模型架构图，相信已经看了这么多模型架构图的你，一眼就能看出它跟其他模型的区别，也就是 Bi-Interaction Pooling 层。那这个夹在 Embedding 层和 MLP 之间的层到底做了什么呢？

图3 NFM的模型架构图
（出自论文Neural Factorization Machines for Sparse Predictive Analytics）

Bi-Interaction Pooling Layer 翻译成中文就是“两两特征交叉池化层”。假设 Vx 是所有特征域的 Embedding 集合，那么特征交叉池化层的具体操作如下所示。

fPI​(Vx​)=i=1∑n​j=i+1∑n​xi​vi​⊙xj​vj​

其中 ⊙ 运算代表两个向量的元素积（Element-wise Product）操作，即两个长度相同的向量对应维相乘得到元素积向量。其中，第 k 维的操作如下所示。

(Vi​⊙Vj​)K​=vik​vjk​

在进行两两特征 Embedding 向量的元素积操作后，再求取所有交叉特征向量之和，我们就得到了池化层的输出向量。接着，我们再把该向量输入上层的多层全连接神经网络，就能得出最后的预测得分。

总的来说，NFM 并没有使用内积操作来进行特征 Embedding 向量的交叉，而是使用元素积的操作。在得到交叉特征向量之后，也没有使用 concatenate 操作把它们连接起来，而是采用了求和的池化操作，把它们叠加起来。

看到这儿，你肯定又想问，元素积操作和点积操作到底哪个更好呢？还是那句老话，我希望我们能够尽量多地储备深度学习模型的相关知识，先不去关注哪个方法的效果会更好，至于真实的效果怎么样，交给你去在具体的业务场景的实践中验证。

DeepFM 的 TensorFlow 实战

接下来，又到了 TensorFlow 实践的时间了，今天我们将要实现 DeepFM 模型。有了之前实现 Wide&Deep 模型的经验，我想你实现起 DeepFM 也不会困难。跟前几节课一样，实践过程中的特征处理、模型训练评估的部分都是相同的，我也就不再重复了，我们重点看模型定义的部分。我把这部分的代码也放在了下面，你可以结合它来看我的讲解。

item_emb_layer = tf.keras.layers.DenseFeatures([movie_emb_col])(inputs)
user_emb_layer = tf.keras.layers.DenseFeatures([user_emb_col])(inputs)
item_genre_emb_layer = tf.keras.layers.DenseFeatures([item_genre_emb_col])(inputs)
user_genre_emb_layer = tf.keras.layers.DenseFeatures([user_genre_emb_col])(inputs)

# FM part, cross different categorical feature embeddings
product_layer_item_user = tf.keras.layers.Dot(axes=1)([item_emb_layer, user_emb_layer])
product_layer_item_genre_user_genre = tf.keras.layers.Dot(axes=1)([item_genre_emb_layer, user_genre_emb_layer])
product_layer_item_genre_user = tf.keras.layers.Dot(axes=1)([item_genre_emb_layer, user_emb_layer])
product_layer_user_genre_item = tf.keras.layers.Dot(axes=1)([item_emb_layer, user_genre_emb_layer])

# deep part, MLP to generalize all input features
deep = tf.keras.layers.DenseFeatures(deep_feature_columns)(inputs)
deep = tf.keras.layers.Dense(64, activation='relu')(deep)
deep = tf.keras.layers.Dense(64, activation='relu')(deep)

# concatenate fm part and deep part
concat_layer = tf.keras.layers.concatenate([product_layer_item_user, product_layer_item_genre_user_genre,
                                            product_layer_item_genre_user, product_layer_user_genre_item, deep], axis=1)
output_layer = tf.keras.layers.Dense(1, activation='sigmoid')(concat_layer)

model = tf.keras.Model(inputs, output_lay)

在整个实践的过程中，有两个地方需要我们重点注意，一个是 FM 部分的构建，另一个是 FM 部分的输出和 Deep 输出的连接。

在构建 FM 部分的时候，我们先为 FM 部分选择了 4 个用于交叉的类别型特征，分别是用户 ID、电影 ID、用户喜欢的风格和电影自己的风格。接着，我们使用 Dot layer 把用户特征和电影特征两两交叉，这就完成了 FM 部分的构建。

而 Deep 部分的实现，其实和我们之前实现过的 Wide&Deep 模型的 Deep 部分完全一样。只不过，最终我们会使用 concatenate 层，去把 FM 部分的输出和 Deep 部分的输出连接起来，输入到输出层的 sigmoid 神经元，从而产生最终的预估分数。那关于 DeepFM 的全部代码，你可以参照 SparrowRecsys 项目中的 DeepFM.py 文件。

小结

DeepFM 模型在解决特征交叉问题上非常有优势，它会使用一个独特的 FM 层来专门处理特征之间的交叉问题。具体来说，就是使用点积、元素积等操作让不同特征之间进行两两组合，再把组合后的结果输入的输出神经元中，这会大大加强模型特征组合的能力。因此，DeepFM 模型相比于 Embedding MLP、Wide&Deep 等模型，往往具有更好的推荐效果。

实现 DeepFM 模型的过程并不困难，我们主要记住三点就可以了：

它是由 FM 和 Deep 两部分组成的；

在实现 FM 部分特征交叉层的时候，我们使用了多个 Dot Product 操作单元完成不同特征的两两交叉；

Deep 部分则与 Wide&Deep 模型一样，负责所有输入特征的深度拟合，提高模型整体的表达能力

刚才说的重点知识，我都整理在了下面的表格中，你可以看一看。

好了，到今天这节课，我们已经在 SparrowRecsys 中实现了四个深度学习模型，相信你对 TensorFlow 的 Keras 接口也已经十分熟悉了。我希望你不只满足于读懂、用好 SparrowRecsys 中实现好的模型，而是真的在课后自己多去尝试不同的特征输入，不同的模型结构，甚至可以按照自己的理解和思考去改进这些模型。

因为深度学习模型结构没有标准答案，我们只有清楚不同模型之间的优缺点，重点汲取它们的设计思想，才能在实际的工作中结合自己遇到的问题，来优化和改造已有的模型。也只有这样，你们才能成为一名能真正解决实际问题的算法工程师。

课后思考

你觉得除了点积和元素积这两个操作外，还有没有其他的方法能处理两个 Embedding 向量间的特征交叉？

关于深度学习中特征交叉问题的处理方法，你是不是学会了？欢迎把你的思考和疑问写在留言区，如果你的朋友也对 DeepFM 这个模型感兴趣，那不妨也把这节课转发给他，我们下节课见！

21｜注意力机制、兴趣演化：推荐系统如何抓住用户的心？

你好，我是王喆。

近几年来，注意力机制、兴趣演化序列模型和强化学习，都在推荐系统领域得到了广泛的应用。它们是深度学习推荐模型的发展趋势，也是我们必须要储备的前沿知识。

作为一名算法工程师，足够的知识储备是非常重要的，因为在掌握了当下主流的深度学习模型架构（Embedding MLP 架构、Wide&Deep 和 DeepFM 等等）之后，要想再进一步提高推荐系统的效果，就需要清楚地知道业界有哪些新的思路可以借鉴，学术界有哪些新的思想可以尝试，这些都是我们取得持续成功的关键。

所以，我会用两节课的时间，带你一起学习这几种新的模型改进思路。今天我们先重点关注注意力机制和兴趣演化序列模型，下节课我们再学习强化学习。

什么是“注意力机制”？

“注意力机制”来源于人类天生的“选择性注意”的习惯。最典型的例子是用户在浏览网页时，会有选择性地注意页面的特定区域，而忽视其他区域。

比如，图 1 是 Google 对大量用户进行眼球追踪实验后，得出的页面注意力热度图。我们可以看到，用户对页面不同区域的注意力区别非常大，他们的大部分注意力就集中在左上角的几条搜索结果上。

那说了这么多，“注意力机制”对我们构建推荐模型到底有什么价值呢？

图1 Google搜索结果的注意力热度图

价值是非常大的。比如说，我们要做一个新闻推荐的模型，让这个模型根据用户已经看过的新闻做推荐。那我们在分析用户已浏览新闻的时候，是把标题、首段、全文的重要性设置成完全一样比较好，还是应该根据用户的注意力不同给予不同的权重呢？当然，肯定是后者比较合理，因为用户很可能都没有注意到正文最后的几段，如果你分析内容的时候把最后几段跟标题、首段一视同仁，那肯定就把最重要的信息给淹没了。

事实上，近年来，注意力机制已经成功应用在各种场景下的推荐系统中了。其中最知名的，要数阿里巴巴的深度推荐模型，DIN（Deep Interest Network，深度兴趣网络）。接下来，我们就一起来学习一下 DIN 的原理和模型结构。

深度兴趣网络 DIN 的原理和结构

DIN 模型的应用场景是阿里最典型的电商广告推荐。对于付了广告费的商品，阿里会根据模型预测的点击率高低，把合适的广告商品推荐给合适的用户，所以 DIN 模型本质上是一个点击率预估模型。

注意力机制是怎么应用在 DIN 模型里的呢？回答这个问题之前，我们得先看一看 DIN 在应用注意力机制之前的基础模型是什么样的，才能搞清楚注意力机制能应用在哪，能起到什么作用。

下面的图 2 就是 DIN 的基础模型 Base Model。我们可以看到，Base Model 是一个典型的 Embedding MLP 的结构。它的输入特征有用户属性特征（User Proflie Features）、用户行为特征（User Behaviors）、候选广告特征（Candidate Ad）和场景特征（Context Features）。

用户属性特征和场景特征我们之前也已经讲过很多次了，这里我们重点关注用户的行为特征和候选广告特征，也就是图 2 中彩色的部分。

图2 阿里Base模型的架构图
(出自论文 Deep Interest Network for Click-Through Rate Prediction)

我们可以清楚地看到，用户行为特征是由一系列用户购买过的商品组成的，也就是图上的 Goods 1 到 Goods N，而每个商品又包含了三个子特征，也就是图中的三个彩色点，其中红色代表商品 ID，蓝色是商铺 ID，粉色是商品类别 ID。同时，候选广告特征也包含了这三个 ID 型的子特征，因为这里的候选广告也是一个阿里平台上的商品。

我们之前讲过，在深度学习中，只要遇到 ID 型特征，我们就构建它的 Embedding，然后把 Embedding 跟其他特征连接起来，输入后续的 MLP。阿里的 Base Model 也是这么做的，它把三个 ID 转换成了对应的 Embedding，然后把这些 Embedding 连接起来组成了当前商品的 Embedding。

完成了这一步，下一步就比较关键了，因为用户的行为序列其实是一组商品的序列，这个序列可长可短，但是神经网络的输入向量的维度必须是固定的，那我们应该怎么把这一组商品的 Embedding 处理成一个长度固定的 Embedding 呢？图 2 中的 SUM Pooling 层的结构就给出了答案，就是直接把这些商品的 Embedding 叠加起来，然后再把叠加后的 Embedding 跟其他所有特征的连接结果输入 MLP。

但这个时候问题又来了，SUM Pooling 的 Embedding 叠加操作其实是把所有历史行为一视同仁，没有任何重点地加起来，这其实并不符合我们购物的习惯。

举个例子来说，候选广告对应的商品是“键盘”，与此同时，用户的历史行为序列中有这样几个商品 ID，分别是“鼠标”“T 恤”和“洗面奶”。从我们的购物常识出发，“鼠标”这个历史商品 ID 对预测“键盘”广告点击率的重要程度应该远大于后两者。从注意力机制的角度出发，我们在购买键盘的时候，会把注意力更多地投向购买“鼠标”这类相关商品的历史上，因为这些购买经验更有利于我们做出更好的决策。

好了，现在我们终于看到了应用注意力机制的地方，那就是用户的历史行为序列。阿里正是在 Base Model 的基础上，把注意力机制应用在了用户的历史行为序列的处理上，从而形成了 DIN 模型。那么，DIN 模型中应用注意力机制的方法到底是什么呢？

我们可以从下面的 DIN 模型架构图中看到，与 Base Model 相比，DIN 为每个用户的历史购买商品加上了一个激活单元（Activation Unit），这个激活单元生成了一个权重，这个权重就是用户对这个历史商品的注意力得分，权重的大小对应用户注意力的高低。

图3 阿里DIN模型的架构图
(出自论文 Deep Interest Network for Click-Through Rate Prediction)

那现在问题就只剩下一个了，这个所谓的激活单元，到底是怎么计算出最后的注意力权重的呢？为了搞清楚这个问题，我们需要深入到激活单元的内部结构里面去，一起来看看图 3 右上角激活单元的详细结构。

它的输入是当前这个历史行为商品的 Embedding，以及候选广告商品的 Embedding。我们把这两个输入 Embedding，与它们的外积结果连接起来形成一个向量，再输入给激活单元的 MLP 层，最终会生成一个注意力权重，这就是激活单元的结构。简单来说，激活单元就相当于一个小的深度学习模型，它利用两个商品的 Embedding，生成了代表它们关联程度的注意力权重。

到这里，我们终于抽丝剥茧地讲完了整个 DIN 模型的结构细节。如果你第一遍没理解清楚，没关系，对照着 DIN 模型的结构图，反复再看几遍我刚才讲的细节，相信你就能彻底消化吸收它。

注意力机制对推荐系统的启发

注意力机制的引入对于推荐系统的意义是非常重大的，它模拟了人类最自然，最发自内心的注意力行为特点，使得推荐系统更加接近用户真实的思考过程，从而达到提升推荐效果的目的。

从“注意力机制”开始，越来越多的对深度学习模型结构的改进是基于对用户行为的深刻观察而得出的。由此，我也想再次强调一下，一名优秀的算法工程师应该具备的能力，就是基于对业务的精确理解，对用户行为的深刻观察，得出改进模型的动机，进而设计出最合适你的场景和用户的推荐模型。

沿着这条思路，阿里的同学们在提出 DIN 模型之后，并没有停止其推荐模型演化的进程，而是又在 2019 年提出了 DIN 模型的演化版本，也就是深度兴趣进化网络 DIEN（Deep Interest Evolution Network），那这个 DIEN 到底在 DIN 基础上做了哪些改进呢？

兴趣进化序列模型

无论是电商购买行为，还是视频网站的观看行为，或是新闻应用的阅读行为，特定用户的历史行为都是一个随时间排序的序列。既然是和时间相关的序列，就一定存在前后行为的依赖关系，这样的序列信息对于推荐过程是非常有价值的。为什么这么说呢？

我们还拿阿里的电商场景举个例子。对于一个综合电商来说，用户兴趣的迁移其实是非常快的。比如，上一周一位用户在挑选一双篮球鞋，这位用户上周的行为序列都会集中在篮球鞋这个品类的各个商品上，但在他完成这一购物目标后，这一周他的购物兴趣就可能变成买一个机械键盘，那这周他所有的购买行为都会围绕机械键盘这个品类展开。

因此，如果我们能让模型预测出用户购买商品的趋势，那肯定会对提升推荐效果有益的。而 DIEN 模型，就正好弥补了 DIN 模型没有对行为序列进行建模的缺陷，它围绕兴趣进化这个点进一步对 DIN 模型做了改进。

图 4 就是 DIEN 模型的架构图，这个模型整体上仍然是一个 Embedding MLP 的模型结构。与 DIN 不同的是，DIEN 用“兴趣进化网络”也就是图中的彩色部分替换掉了原来带有激活单元的用户历史行为部分。这部分虽然复杂，但它的输出只是一个 h’(T) 的 Embedding 向量，它代表了用户当前的兴趣向量。有了这个兴趣向量之后，再把它与其他特征连接在一起，DIEN 就能通过 MLP 作出最后的预测了。

图4 DIEN模型的架构图 （出自论文 Deep Interest Evolution Network for Click-Through Rate Prediction）

好了，现在问题的焦点就在，DIEN 模型是如何生成这个兴趣向量的。关键就在于 DIEN 模型中彩色部分的三层兴趣进化网络，下面，我就按照从下到上的顺序，给你讲讲它们的名称和作用。

最下面一层是行为序列层（Behavior Layer，浅绿色部分）。它的主要作用和一个普通的 Embedding 层是一样的，负责把原始的 ID 类行为序列转换成 Embedding 行为序列。

再上一层是兴趣抽取层（Interest Extractor Layer，浅黄色部分）。它的主要作用是利用 GRU 组成的序列模型，来模拟用户兴趣迁移过程，抽取出每个商品节点对应的用户兴趣。

最上面一层是兴趣进化层（Interest Evolving Layer，浅红色部分）。它的主要作用是利用 AUGRU(GRU with Attention Update Gate) 组成的序列模型，在兴趣抽取层基础上加入注意力机制，模拟与当前目标广告（Target Ad）相关的兴趣进化过程，兴趣进化层的最后一个状态的输出就是用户当前的兴趣向量 h’(T)。

不知道你发现了吗，兴趣抽取层和兴趣进化层都用到了序列模型的结构，那什么是序列模型呢？直观地说，图 5 就是一个典型的序列模型的结构，它和我们之前看到的多层神经网络的结构不同，序列模型是“一串神经元”，其中每个神经元对应了一个输入和输出。

那在 DIEN 模型中，神经元的输入就是商品 ID 或者前一层序列模型的 Embedding 向量，而输出就是商品的 Embedding 或者兴趣 Embedding，除此之外，每个神经元还会与后续神经元进行连接，用于预测下一个状态，放到 DIEN 里，就是为了预测用户的下一个兴趣。这就是序列模型的结构和作用。

图5 RNN模型的经典结构

至于上面提到过的 GRU 序列模型，它其实是序列模型的一种，根据序列模型神经元结构的不同，最经典的有RNN、LSTM、GRU这 3 种。这里我们就不展开讲了，对理论感兴趣的同学，可以点击我给出的超链接，参考这几篇论文做更深入的研究。

图6 序列模型中的不同单元结构

事实上，序列模型已经不仅在电商场景下，成功应用于推测用户的下次购买行为，在 YouTube、Netflix 等视频流媒体公司的视频推荐模型中，序列模型也用来推测用户的下次观看行为（Next Watch）。除此之外，音乐类应用也非常合适使用序列模型来预测用户的音乐兴趣变化。所以，掌握 DIEN 模型的架构对于拓宽我们的技术视野非常有帮助。

小结

注意力机制和兴趣演化序列模型的加入，让推荐系统能够更好地抓住用户的心。

对于注意力机制来说，它主要模拟了人类注意力的天性。具体到阿里的 DIN 模型上，它利用激活单元计算出用户对于不同历史商品的注意力权重，针对当前广告商品，作出更有针对性的预测。

而序列模型更注重对序列类行为的模拟和预测。典型的例子是 DIEN 模型对用户购买兴趣进化过程的模拟。DIEN 模型可以应用的场景非常广泛，包括电商平台的下次购买，视频网站的下次观看，音乐 APP 的下一首歌曲等等。

总的来说，注意力机制的引入是对经典深度学习模型的一次大的改进，因为它改变了深度学习模型对待用户历史行为“一视同仁”的弊端。而序列模型则把用户行为串联起来，让用户的兴趣随时间进行演化，这也是之前的深度学习模型完全没有考虑到的结构。

最后，我把今天的重要概念总结在了表格中，方便你及时查看和复习。

课后思考

DIN 使用了一个结构比较复杂的激活单元来计算注意力权重，你觉得有没有更简单、更实用的方式来生成注意力权重呢？其实计算注意力权重就是为了计算历史行为物品和广告物品的相关性，在这个过程中，你觉得能不能利用到特征交叉的知识呢？为什么？

欢迎把你的思考和疑问写在留言区，如果你的朋友们也在关注注意力机制和兴趣演化序列模型的发展，那不妨也把这节课转发给他们，我们下节课见！

22｜强化学习：让推荐系统像智能机器人一样自主学习

你好，我是王喆。这节课我们继续来讲深度推荐模型发展的前沿趋势，来学习强化学习（Reinforcement Learning）与深度推荐模型的结合。

强化学习也被称为增强学习，它在模型实时更新、用户行为快速反馈等方向上拥有巨大的优势。自从 2018 年开始，它就被大量应用在了推荐系统中，短短几年时间内，微软、美团、阿里等多家一线公司都已经有了强化学习的成功应用案例。

虽然，强化学习在推荐系统中的应用是一个很复杂的工程问题，我们自己很难在单机环境下模拟，但理解它在推荐系统中的应用方法，是我们进一步改进推荐系统的关键点之一，也是推荐系统发展的趋势之一。

所以这节课，我会带你重点学习这三点内容：一是强化学习的基本概念；二是，我会以微软的 DRN 模型为例，帮你厘清强化学习在推荐系统的应用细节；三是帮助你搞清楚深度学习和强化学习的结合点究竟在哪。

强化学习的基本概念

强化学习的基本原理，简单来说，就是一个智能体通过与环境进行交互，不断学习强化自己的智力，来指导自己的下一步行动，以取得最大化的预期利益。

事实上，任何一个有智力的个体，它的学习过程都遵循强化学习所描述的原理。比如说，婴儿学走路就是通过与环境交互，不断从失败中学习，来改进自己的下一步的动作才最终成功的。再比如说，在机器人领域，一个智能机器人控制机械臂来完成一个指定的任务，或者协调全身的动作来学习跑步，本质上都符合强化学习的过程。

为了把强化学习技术落地，只清楚它的基本原理显然是不够的，我们需要清晰地定义出强化学习中的每个关键变量，形成一套通用的技术框架。对于一个通用的强化学习框架来说，有这么六个元素是必须要有的：

智能体（Agent）：强化学习的主体也就是作出决定的“大脑”；

环境（Environment）：智能体所在的环境，智能体交互的对象；

行动（Action）：由智能体做出的行动；

奖励（Reward）：智能体作出行动后，该行动带来的奖励；

状态（State）：智能体自身当前所处的状态；

目标（Objective）：指智能体希望达成的目标。

为了方便记忆，我们可以用一段话把强化学习的六大要素串起来：一个智能体身处在不断变化的环境之中，为了达成某个目标，它需要不断作出行动，行动会带来好或者不好的奖励，智能体收集起这些奖励反馈进行自我学习，改变自己所处的状态，再进行下一步的行动，然后智能体会持续这个“行动 - 奖励 - 更新状态”的循环，不断优化自身，直到达成设定的目标。

这就是强化学习通用过程的描述，那么，对于推荐系统而言，我们能不能创造这样一个会自我学习、自我调整的智能体，为用户进行推荐呢？事实上，微软的 DRN 模型已经实现这个想法了。下面，我就以 DRN 模型为例，来给你讲一讲在推荐系统中，强化学习的六大要素都是什么，强化学习具体又是怎样应用在推荐系统中的。

强化学习推荐系统框架

强化学习推荐模型 DRN（Deep Reinforcement Learning Network，深度强化学习网络）是微软在 2018 年提出的，它被应用在了新闻推荐的场景上，下图 1 是 DRN 的框架图。事实上，它不仅是微软 DRN 的框架图，也是一个经典的强化学习推荐系统技术框图。

图1 深度强化学习推荐系统框架

从这个技术框图中，我们可以清楚地看到强化学习的六大要素。接下来，我就以 DRN 模型的学习过程串联起所有要素，来和你详细说说这六大要素在推荐系统场景下分别指的是什么，以及每个要素的位置和作用。

在新闻的推荐系统场景下，DRN 模型的第一步是初始化推荐系统，主要初始化的是推荐模型，我们可以利用离线训练好的模型作为初始化模型，其他的还包括我们之前讲过的特征存储、推荐服务器等等。

接下来，推荐系统作为智能体会根据当前已收集的用户行为数据，也就是当前的状态，对新闻进行排序这样的行动，并在新闻网站或者 App 这些环境中推送给用户。

用户收到新闻推荐列表之后，可能会产生点击或者忽略推荐结果的反馈。这些反馈都会作为正向或者负向奖励再反馈给推荐系统。

推荐系统收到奖励之后，会根据它改变、更新当前的状态，并进行模型训练来更新模型。接着，就是推荐系统不断重复“排序 - 推送 - 反馈”的步骤，直到达成提高新闻的整体点击率或者用户留存等目的为止。

为了方便你进行对比，我也把这六大要素在推荐系统场景下的定义整理在了下面，你可以看一看。

到这里，你有没有发现强化学习推荐系统跟传统推荐系统相比，它的主要特点是什么？其实，就在于强化学习推荐系统始终在强调“持续学习”和“实时训练”。它不断利用新学到的知识更新自己，做出最及时的调整，这也正是将强化学习应用于推荐系统的收益所在。

我们现在已经熟悉了强化学习推荐系统的框架，但其中最关键的部分“智能体”到底长什么样呢？微软又是怎么实现“实时训练”的呢？接下来，就让我们深入 DRN 的细节中去看一看。

深度强化学习推荐模型 DRN

智能体是强化学习框架的核心，作为推荐系统这一智能体来说，推荐模型就是推荐系统的“大脑”。在 DRN 框架中，扮演“大脑”角色的是 Deep Q-Network (深度 Q 网络，DQN)。其中，Q 是 Quality 的简称，指通过对行动进行质量评估，得到行动的效用得分，来进行行动决策。

图2 DQN的模型架构图

DQN 的网络结构如图 2 所示，它就是一个典型的双塔结构。其中，用户塔的输入特征是用户特征和场景特征，物品塔的输入向量是所有的用户、环境、用户 - 新闻交叉特征和新闻特征。

在强化学习的框架下，用户塔特征向量因为代表了用户当前所处的状态，所以也可被视为状态向量。物品塔特征向量则代表了系统下一步要选择的新闻，我们刚才说了，这个选择新闻的过程就是智能体的“行动”，所以物品塔特征向量也被称为行动向量。

双塔模型通过对状态向量和行动向量分别进行 MLP 处理，再用互操作层生成了最终的行动质量得分 Q(s,a)，智能体正是通过这一得分的高低，来选择到底做出哪些行动，也就是推荐哪些新闻给用户的。

其实到这里为止，我们并没有看到强化学习的优势，貌似就是套用了强化学习的概念把深度推荐模型又解释了一遍。别着急，下面我要讲的 DRN 学习过程才是强化学习的精髓。

DRN 的学习过程

DRN 的学习过程是整个强化学习推荐系统框架的重点，正是因为可以在线更新，才使得强化学习模型相比其他“静态”深度学习模型有了更多实时性上的优势。下面，我们就按照下图中从左至右的时间轴，来描绘一下 DRN 学习过程中的重要步骤。

图3 DRN的学习过程

我们先来看离线部分。DRN 根据历史数据训练好 DQN 模型，作为智能体的初始化模型。

而在线部分根据模型更新的间隔分成 n 个时间段，这里以 t1 到 t5 时间段为例。首先在 t1 到 t2 阶段，DRN 利用初始化模型进行一段时间的推送服务，积累反馈数据。接着是在 t2 时间点，DRN 利用 t1 到 t2 阶段积累的用户点击数据，进行模型微更新（Minor update）。

最后在 t4 时间点，DRN 利用 t1 到 t4 阶段的用户点击数据及用户活跃度数据，进行模型的主更新（Major update）。时间线不断延长，我们就不断重复 t1 到 t4 这 3 个阶段的操作。

这其中，我要重点强调两个操作，一个是在 t4 的时间点出现的模型主更新操作，我们可以理解为利用历史数据的重新训练，用训练好的模型来替代现有模型。另一个是 t2、t3 时间点提到的模型微更新操作，想要搞清楚它到底是怎么回事，还真不容易，必须要牵扯到 DRN 使用的一种新的在线训练方法，Dueling Bandit Gradient Descent algorithm（竞争梯度下降算法）。

DRN 的在线学习方法：竞争梯度下降算法

我先把竞争梯度下降算法的流程图放下了下面。接下来，我就结合这个流程图，来给你详细讲讲它的过程和它会涉及的模型微更新操作。

图4 DRN的在线学习过程

DRN 的在线学习过程主要包括三步，我带你一起来看一下。

第一步，对于已经训练好的当前网络 Q，对其模型参数 W 添加一个较小的随机扰动，得到一个新的模型参数，这里我们称对应的网络为探索网络 Q~。

在这一步中，由当前网络 Q 生成探索网络 ，产生随机扰动的公式 1 如下：

ΔW=α⋅rand(−1,1)⋅W

其中，α 是一个探索因子，决定探索力度的大小。rand(-1,1) 产生的是一个[-1,1]之间的随机数。

第二步，对于当前网络 Q 和探索网络 Q~，分别生成推荐列表 L 和 L~，再将两个推荐列表用间隔穿插（Interleaving）的方式融合，组合成一个推荐列表后推送给用户。

最后一步是实时收集用户反馈。如果探索网络 Q～生成内容的效果好于当前网络 Q，我们就用探索网络代替当前网络，进入下一轮迭代。反之，我们就保留当前网络。

总的来说，DRN 的在线学习过程利用了“探索”的思想，其调整模型的粒度可以精细到每次获得反馈之后，这一点很像随机梯度下降的思路：虽然一次样本的结果可能产生随机扰动，但只要总的下降趋势是正确的，我们就能够通过海量的尝试最终达到最优点。DRN 正是通过这种方式，让模型时刻与最“新鲜”的数据保持同步，实时地把最新的奖励信息融合进模型中。模型的每次“探索”和更新也就是我们之前提到的模型“微更新”。

到这里，我们就讲完了微软的深度强化学习模型 DRN。我们可以想这样一个问题：这个模型本质上到底改进了什么？从我的角度来说，它最大的改进就是把模型推断、模型更新、推荐系统工程整个一体化了，让整个模型学习的过程变得更高效，能根据用户的实时奖励学到新知识，做出最实时的反馈。但同时，也正是因为工程和模型紧紧地耦合在一起，让强化学习在推荐系统中的落地并不容易。

既然，说到了强化学习的落地，这里我还想再多说几句。因为涉及到了模型训练、线上服务、数据收集、实时模型更新等几乎推荐系统的所有工程环节，所以强化学习整个落地过程的工程量非常大。这不像我们之前学过的深度学习模型，只要重新训练一下它，我们就可以改进一个模型结构，强化学习模型需要工程和研究部门通力合作才能实现。

在这个过程中，能不能有一个架构师一样的角色来通盘协调，就成为了整个落地过程的关键点。有一个环节出错，比如说模型在做完实时训练后，模型参数更新得不及时，那整个强化学习的流程就被打乱了，整体的效果就会受到影响。

所以对我们个人来说，掌握强化学习模型的框架，也就多了一个发展的方向。那对于团队来说，如果强化学习能够成功落地，也一定证明了这个团队有着极强的合作能力，在工程和研究方向上都有着过硬的实践能力。

小结

强化学习是近来在学术界和业界都很火的话题，它起源于机器人领域。这节课，我们要重点掌握强化学习的通用过程，以及它在深度学习中的应用细节。

简单来说，强化学习的通用过程就是训练一个智能体，让它通过与环境进行交互，不断学习强化自己的智力，并指导自己的下一步行动，以取得最大化的预期利益。这也让强化学习在模型实时更新，用户行为快速反馈等方向上拥有巨大的优势。

但强化学习的落地并不容易，整个落地过程的工程量非常大。现阶段，我们只需要以微软的 DRN 模型作为参考，重点掌握强化学习在推荐系统领域的应用细节就可以了。

一个是 DRN 构建了双塔模型作为深度推荐模型，来得出“行动得分”。第二个是 DRN 的更新方式，它利用“微更新”实时地学习用户的奖励反馈，更新推荐模型，再利用阶段性的“主更新”学习全量样本，更新模型。第三个是微更新时的方法，竞争梯度下降算法，它通过比较原网络和探索网络的实时效果，来更新模型的参数。

为了方便你复习，我们把这节课的重点知识总结在了下面的表格中，你可以看看。

课后思考

DRN 的微更新用到了竞争梯度下降算法，你觉得这个算法有没有弊端？你还知道哪些可以进行模型增量更新或者实时更新的方法吗？

欢迎把你的思考和疑问写在留言区，如果你的朋友们也在关注强化学习在推荐系统上的发展，那不妨也把这节课转发给他们，我们下节课见！

23｜ 实战：如何用深度学习模型实现Sparrow RecSys的个性化推荐功能？

你好，我是王喆。

今天又是一堂实战课。在这节课里，我会带你利用我们现阶段掌握的所有知识，来实现 SparrowRecSys 中“猜你喜欢”的功能。具体来说，我们会根据一位用户的历史行为，为 TA 推荐可能喜欢的电影。这个功能几乎会用到所有的推荐系统模块，包括离线的特征工程、模型训练以及线上的模型服务和推荐逻辑的实现。

如果说完成了第 14 节课的“相似电影”功能，还只是你“武功小成”的标志，那啃透了这节课的实践，就代表你掌握了推荐系统技术框架中的大部分内容，你就能在推荐系统的实际工作中做到“驾轻就熟”啦。

“清点技能库”，看看我们已有的知识储备有哪些

正式开始实践之前，我们还是先来清点一次自己的技能库。看看经过推荐模型篇的学习，我们技能库中的“兵器”又增加了多少，哪些可以用来实现“猜你喜欢”这个功能。下面，我就按照从离线到线上，由数据到模型的顺序，为你依次梳理一下特征工程、模型离线训练、模型服务、推荐服务器逻辑这四大部分的技能点。

1. 模型特征工程

特征工程是所有机器学习项目的起点，咱们的推荐模型也不例外。为了训练推荐模型，我们需要准备好模型所需的样本和特征。此外，在进行模型线上推断的时候，推荐服务器也需要线上实时拼装好包含了用户特征、物品特征、场景特征的特征向量，发送给推荐模型进行实时推断。

在“模型实战准备二”这节课，我们就通过 Spark 处理好了 TensorFlow 训练所需的训练样本，并把 Spark 处理好的特征插入了 Redis 特征数据库，供线上推断使用。不熟悉这部分内容的同学，最好再复习一下相关内容，把这把武器装进自己的技能库。

2. 模型离线训练

为了在线上做出尽量准确或者说推荐效果尽量好的排序，我们需要在离线训练好排序所用的推荐模型。

我们在这一篇中学习和实践的所有深度推荐模型，都是围绕着这个目的展开的。虽然这些深度推荐模型的结构各不相同，但它们的输入、输出都是一致的，输入是由不同特征组成的特征向量，输出是一个分数，这个分数的高低代表了这组特征对应的用户对物品的喜好程度。

具体实践的时候，我们在 TensorFlow 平台上实现了 Embedding MLP、Wide&Deep、NeuralCF、双塔模型、DeepFM 等几种不同的深度推荐模型，它们中的任何一个都可以支持“猜你喜欢”的排序功能。

在实际的工业级系统中，我们会通过离线、在线等不同的评估手段来挑出最好的那个模型，去支持真实的应用场景。在 SparrowRecsys 中，我们以 NeuralCF 模型为例，实现“猜你喜欢”功能。其他模型的上线方法与 NeuralCF 几乎一致，唯一的区别是，对于不同的模型来说，它们在模型服务的部分需要载入不同的模型文件，并且在线上预估的部分也要传入模型相应的输入特征。

3. 模型服务

模型服务是推荐系统中连接线上环境和线下环境的纽带之一（另一个关键的纽带是特征数据库）。

在离线训练好模型之后，为了让模型在线上发挥作用，做出实时的推荐排序，我们需要通过模型服务的模块把推荐模型部署上线。我们曾经在第 13 节课中详细介绍过主流的模型服务方法，它们是“预存推荐结果”，“预训练 Embedding+ 轻量级线上模型”，“利用 PMML 转换和部署模型”以及“TensorFlow Serving”。因为我们这一篇的深度学习模型都是基于 TensorFlow 训练的，所以这节课我们也会采用 TensorFlow Serving 作为模型服务的方式。

4. 推荐服务器内部逻辑实现

模型服务虽然可以做到“猜你喜欢”中电影的排序，但要进行排序，仍然需要做大量的准备工作，比如候选集的获取，召回层的构建，特征的获取和拼装等等。这些推荐逻辑都是在推荐服务器内部实现的。推荐服务器就像推荐系统的线上的心脏，是所有线上模块的核心。

我们曾经在“相似电影”功能中实现过整套的推荐逻辑，今天我们重点关注其中不同的部分，就是特征的拼装，以及从推荐服务器内部请求模型服务 API 的方法。

至此，我们准备好了自己的技能库。接下来，就让我们使出十八般武艺，来打造“猜你喜欢”这个推荐功能吧。

“猜你喜欢”推荐功能的技术架构

与“相似电影”功能一样，“猜你喜欢”相关的技术架构同样是由数据模型部分、线上部分和前端部分组成的。我们先来看看整个功能的技术架构图，再来说说每部分的具体实现细节。下图 1 就是“猜你喜欢”功能的技术架构图，接下来，你就跟着我，按照从左上到右下的顺序，一起随着数据流的走向过一遍这个架构图吧。

图1 “猜你喜欢”功能的技术架构图

首先，我们来看数据和模型部分。左上角是我们使用的数据集 MovieLens，它经过 Spark 的处理之后，会生成两部分数据，分别从两个出口出去，特征部分会存入 Redis 供线上推断时推荐服务器使用，样本部分则提供给 TensorFlow 训练模型。

TensorFlow 完成模型训练之后，会导出模型文件，然后模型文件会载入到 TensorFlow Serving 中，接着 TensorFlow Serving 会对外开放模型服务 API，供推荐服务器调用。

接下来，我们再看推荐服务器部分。在这部分里，基于 MovieLens 数据集生成的候选电影集合会依次经过候选物品获取、召回层、排序层这三步，最终生成“猜你喜欢”的电影推荐列表，然后返回给前端，前端利用 HTML 和 JavaScript 把它们展示给用户。

整个过程中，除了排序层和 TensorFlow Serving 的实现，其他部分我们都已经在之前的实战中一一实现过。所以今天，我们会重点讲解推荐服务器排序层和 TensorFlow Serving 的实现。

排序层 +TensorFlow Serving 的实现

在推荐服务器内部，经过召回层之后，我们会得到几百量级的候选物品集。最后我们到底从这几百部电影中推荐哪些给用户，这个工作就交由排序层来处理。因为排序的工作是整个推荐系统提高效果的重中之重，在业界的实际应用中，往往交由评估效果最好的深度推荐模型来处理。整个的排序过程可以分为三个部分：

准备线上推断所需的特征，拼接成 JSON 格式的特征样本；

把所有候选物品的特征样本批量发送给 TensorFlow Serving API；

根据 TensorFlow Serving API 返回的推断得分进行排序，生成推荐列表。

接下来，我们就详细来讲讲这三步中的实现重点。

首先，第一步的实现重点在于特征样本的拼接。因为实践例子里，我们选用了 NeuralCF 作为排序模型，而 NerualCF 所需的特征只有 userId 和 itemId ，所以特征是比较好准备的。我们下面看一下如何拼接特征形成模型推断所需的样本。详细的代码，你可以参考 com.wzhe.sparrowrecsys.online.recprocess.RecForYouProcess。

/**
 * call TenserFlow serving to get the NeuralCF model inference result
 * @param user              input user
 * @param candidates        candidate movies
 * @param candidateScoreMap save prediction score into the score map
 */
public static void callNeuralCFTFServing(User user, List<Movie> candidates, HashMap<Movie, Double> candidateScoreMap){
    if (null == user || null == candidates || candidates.size() == 0){
        return;
    }
    //保存所有样本的JSON数组
    JSONArray instances = new JSONArray();
    for (Movie m : candidates){
        JSONObject instance = new JSONObject();
        //为每个样本添加特征，userId和movieId
        instance.put("userId", user.getUserId());
        instance.put("movieId", m.getMovieId());
        instances.put(instance);
    }
    JSONObject instancesRoot = new JSONObject();
    instancesRoot.put("instances", instances);
    //请求TensorFlow Serving API
    String predictionScores = asyncSinglePostRequest("http://localhost:8501/v1/models/recmodel:predict", instancesRoot.toString());
    //获取返回预估值
    JSONObject predictionsObject = new JSONObject(predictionScores);
    JSONArray scores = predictionsObject.getJSONArray("predictions");
    //将预估值加入返回的map
    for (int i = 0 ; i < candidates.size(); i++){
        candidateScoreMap.put(candidates.get(i), scores.getJSONArray(i).getDouble(0));
    }
}

在代码中，我们先把 userId 和 movieId 加入了 JSON 格式的样本中，然后再把样本加入到 Json 数组中。接下来，我们又以 http post 请求的形式把这些 JSON 样本发送给 TensorFlow Serving 的 API，进行批量预估。在收到预估得分后，保存在候选集 map 中，供排序层进行排序。

第二步的重点在于如何建立起 TensorFlow Serving API。事实上，我们通过第 13 讲模型服务的实践部分，已经能够搭建起一个测试模型的 API 了。

想要搭建起我们自己的 TensorFlow Serving API，只需要把之前载入的测试模型文件换成我们自己的模型文件就可以了。这里，我就以 NerualCF 模型为例，带你看一看模型文件是如何被导出和导入的。

首先是模型的导出。在 NeuralCF 的 TensorFlow 实现中，我们已经把训练好的模型保存在了 model 这个结构中，接下来需要调用 tf.keras.models.save_model 这一函数来把模型序列化。

从下面的代码中你可以看到，这一函数需要传入的参数有要保存的 model 结构，保存的路径，还有是否覆盖路径 overwrite 等等。其中，我们要注意的是保存路径。你可以看到，我在保存路径中加上了一个模型版本号 002，这对于 TensorFlow Serving 是很重要的，因为 TensorFlow Serving 总是会找到版本号最大的模型文件进行载入，这样做，就保证了我们每次载入的都是最新训练的模型。详细代码请你参考 NeuralCF.py。

tf.keras.models.save_model(
    model,
  "file:///Users/zhewang/Workspace/SparrowRecSys/src/main/resources/webroot/modeldata/neuralcf/002",
    overwrite=True,
    include_optimizer=True,
    save_format=None,
    signatures=None,
    options=None
)

其次是模型的导入，导入命令非常简单就是 TensorFlow Serving API 的启动命令，我们直接看下面命令中的参数。

docker run -t --rm -p 8501:8501     -v "/Users/zhewang/Workspace/SparrowRecSys/src/main/resources/webroot/modeldata/neuralcf:/models/recmodel"     -e MODEL_NAME=recmodel     tensorflow/serving &

这里面最重要的参数，就是指定载入模型的路径和预估 url，而载入路径就是我们刚才保存模型的路径，“/Users/zhewang/Workspace/SparrowRecSys/src/main/resources/webroot/modeldata/neuralcf”。但是在这里，我们没有加模型的版本号。这是为什么呢？因为版本号是供 TensorFlow Serving 查找最新模型用的，TensorFlow Serving 在模型路径上会自动找到版本号最大的模型载入，因此不可以在载入路径上再加上版本号。

除此之外，冒号后的部分“/models/recmodel”指的是 TensorFlow Serving API 在这个模型上的具体 url，刚才我们是通过请求 http://localhost:8501/v1/models/recmodel:predict 获取模型预估值的，请求连接中的 models/recmodel 就是在这里设定的。

在正确执行上面的命令后，我们就可以在 Docker 上运行起 TensorFlow Serving 的 API 了。

最后，我们来看第三步的实现重点：获取返回得分和排序。 我们先来看一下 TensorFlow Serving API 的返回得分格式。它的返回值也是一个 JSON 数组的格式，数组中每一项对应着之前发送过去的候选电影样本，所以我们只要把返回的预估值赋给相应的样本，然后按照预估值排序就可以了。详细的过程你也可以参考 com.wzhe.sparrowrecsys.online.recprocess.RecForYouProcess 中全部排序层的代码。

{
    "predictions": [[0.824034274], [0.86393261], [0.921346784], [0.957705915], [0.875154734], [0.905113697], [0.831545711], [0.926080644], [0.898158073]...
    ]
}

如果你已经正确建立起了 Redis 和 TensorFlow Serving API 服务，并且已经分别导入了特征数据和模型文件，我们就可以启动 SparrowRecsys Server，查看“猜你喜欢”的结果了。图 2 是用户 ID 为 6 的用户在 NerualCF 模型下的推荐结果，注意通过在连接中设置 model 变量为 nerualcf，来决定产生结果的模型。

通过用户的评分历史（User Watched Movies）我们可以看到该用户偏向于观看动作类的电影，同时夹杂着一些爱情片和动画片，而在下方的“猜你喜欢”（Recommended For You）的结果中，我们也可以看到 SparrowRecsys 为他推荐的电影也包含了这三类电影。有兴趣的话，你可以多在 SparrowRecsys 里面逛一逛，看看推荐的结果在你眼中是不是合理。

图2 猜你喜欢功能的推荐结果

小结

今天我们通过实现“猜你喜欢”功能串联起了我们之前所有学过的知识。希望在你看到推荐结果的时候，有种“武功大成，驾轻就熟”的感觉。要知道，这里面所有的代码都是你曾经学习过的，这里面每个结果都是你通过自己的所学生成的。希望你能在这里为自己鼓掌，这是一个不小的里程碑。

下面我们再重点总结一下今天实践用到的技术。首先，我们利用 Spark 对 MovieLens 原始数据进行了处理，生成了训练样本和特征，样本供 TensorFlow 进行模型训练，特征存入 Redis 供线上推断使用。

在 TensorFlow 平台上，我们以 NeuralCF 模型为例，训练并导出了 NeuralCF 的模型文件。然后使用 TensorFlow Serving 载入模型文件，建立线上模型服务 API。推荐服务器的排序层从 Redis 中取出用户特征和物品特征，组装好 JSON 格式的特征数据，发送给 TensorFlow Serving API，再根据返回的预估分数进行排序，最终生成“猜你喜欢”的推荐列表。

虽然我们实现了猜你喜欢的功能，但是课程进行到这里你一定会有一个疑问：我们的推荐结果到底是好还是坏呢？我们总不能总是人肉去查看结果好坏吧，这样效率又低，又不准确。没错，推荐系统的效果评估是有一套非常完整的评估体系的，别着急，从下一篇的模型评估篇开始，我们就会系统性地讲解推荐系统的评估方法，期待继续与你同行。

课后思考

推荐系统的特征预处理是一项很重要的工作，比如一些连续特征的归一化，分桶等等。那么这些预处理的过程，我们应该放在线上部分的哪里完成呢？是在 Tensorflow Serving 的部分，还是在推荐服务器内部，还是在离线部分完成？你有什么好的想法吗？

期待在留言区看到你的想法和思考，我们下一节课见！

模型实战准备（一） | TensorFlow入门和环境配置

你好，我是王喆。

我在留言区看到，很多同学对 TensorFlow 的用法还不太熟悉，甚至可能还没接触过它。但我们必须要熟练掌握 TensorFlow，因为接下来，它会成为我们课程主要使用的工具和平台。为此，我特意准备了两堂模型实战准备课，来帮助你掌握 TensorFlow 的基础知识，以及为构建深度学习模型做好准备。

这节课，我们先来学习 TensorFlow 的环境配置，讲讲什么是 TensorFlow，怎么安装 TensorFlow，以及怎么在 TensorFlow 上构建你的第一个深度学习模型。下节课，我们再来学习模型特征和训练样本的处理方法。

什么是 TensorFlow？

TensorFlow 是由 Google Brain 团队开发的深度学习平台，于 2015 年 11 月首次发布，目前最新版本是 2.3。因为 TensorFlow 自从 2.0 版本之后就发生了较大的变化，所以咱们这门课程会使用 TensorFlow2.3 作为实践版本。

TensorFlow 这个名字还是很有意思的，翻译过来是“张量流动”，这非常准确地表达了它的基本原理，就是根据深度学习模型架构构建一个有向图，让数据以张量的形式在其中流动起来。

这里的张量（Tensor）其实是指向量的高维扩展，比如向量可以看作是张量的一维形式，矩阵可以看作张量在二维空间上的特例。在深度学习模型中，大部分数据是以矩阵甚至更高维的张量表达的，为了让这些张量数据流动起来，每一个深度学习模型需要根据模型结构建立一个由点和边组成的有向图，图里面的点代表着某种操作，比如某个激活函数、某种矩阵运算等等，而边就定义了张量流动的方向。

这么说还是太抽象，我们来看一个简单 TensorFlow 任务的有向图。从图中我们可以看出，向量 b、矩阵 W 和向量 x 是模型的输入，紫色的节点 MatMul、Add 和 ReLU 是操作节点，分别代表了矩阵乘法、向量加法、ReLU 激活函数这样的操作。模型的输入张量 W、b、x 经过操作节点的变形处理之后，在点之间传递，这就是 TensorFlow 名字里所谓的张量流动。

图1 一个简单的TensorFlow有向图

事实上，任何复杂模型都可以抽象为操作有向图的形式。这样做不仅有利于操作的模块化，还可以厘清各操作间的依赖关系，有利于我们判定哪些操作可以并行执行，哪些操作只能串行执行，为并行平台能够最大程度地提升训练速度打下基础。

说起 TensorFlow 的并行训练，就不得不提 GPU，因为 GPU 拥有大量的计算核心，所以特别适合做矩阵运算这类易并行的计算操作。业界主流的 TensorFlow 平台都是建立在 CPU+GPU 的计算环境之上的。但咱们这门课程，因为考虑到大多数同学都是使用个人电脑进行学习的，所以所有的项目实践都建立在 TensorFlow CPU 版本上，我在这里先给你说明一下。

TensorFlow 的运行环境如何安装？

知道了 TensorFlow 的基本概念之后，我们就可以开始着手配置它的运行环境了。我先把这门课使用的环境版本告诉你，首先，TensorFlow 我推荐 2.3 版本也就是目前的最新版本 ，Python 我推荐 3.7 版本，其实 3.5-3.8 都可以，我建议你在学习过程中不要随意更换版本，尽量保持一致。

虽然Python 环境和TensorFlow 环境有自己的官方安装指南，但是安装起来也并不容易，所以接下来，我会带你梳理一遍安装中的重点步骤。一般来说，安装 TensorFlow 有两种方法，一种是采用 Docker+Jupyter 的方式，另一种是在本地环境安装 TensorFlow 所需的 python 环境和所需依赖库。其中，Docker+Jupyter 的方式比较简单，我们先来看看它。

最简单的方法，Docker+Jupyter

经过第 13 节模型服务的实践，我们已经把 Docker 安装到了自己的电脑上。这里，它就可以再次派上用场了。因为 TensorFlow 官方已经为我们准备好了它专用的 Docker 镜像，你只要运行下面两行代码，就可以拉取并运行最新的 TensorFlow 版本，还能在http://localhost:8888/ 端口运行起 Jupyter Notebook。

docker pull tensorflow/tensorflow:latest  # Download latest stable image
docker run -it -p 8888:8888 tensorflow/tensorflow:latest-jupyter  # Start Jupyter server

如果你已经能够在浏览器中打开 Jupyter 了，就在这个 Notebook 上开始你“肆无忌惮”地尝试吧。在之后的实践中，我们也可以把 SparrowRecsys 中的 TensorFlow 代码 copy 到 Notebook 上直接执行。因为不用操心非常复杂的 python 环境配置的过程，而且 docker 的运行与你的主系统是隔离的，如果把它“玩坏了”，我们再重新拉取全新的镜像就好，所以这个方式是最快捷安全的方式。

在 IDEA 中调试你的 TensorFlow 代码

不过，Docker+Jupyter 的方式虽然非常方便，但我们在实际工作中，还是会更多地使用 IDE 来管理和维护代码。所以，掌握 IDEA 或者 PyCharm 这类 IDE 来调试 TensorFlow 代码的方法，对我们来说也非常重要。

比如说，SparrowRecSys 中就包含了 TensorFlow 的 Python 代码，我把所有 Python 和 TensorFlow 相关的代码都放在了 TFRecModel 模块。那我们能否利用 IDEA 一站式地调试 SparrowRecSys 项目中的 Python 代码和之前的 Java、Scala 代码呢？当然是可以的，要实现这一操作，我们需要进行三步的配置，分别是安装 IDEA 的 Python 编译器插件，安装本地的 Python 环境，以及配置 IDEA 的 Python 环境。下面，我们一步一步地详细说一说。

首先是安装 IDEA 的 Python 编译器插件。

因为 IDEA 默认不支持 Python 的编译，所以我们需要为它安装 Python 插件。具体的安装路径是点击顶部菜单的 IntelliJ IDEA -> Preferences -> Plugins -> 输入 Python -> 选择插件 Python Community Edition 进行安装。

图2 为IDEA安装Python插件

接着是安装本地 Python 环境。

使用过 Python 的同学可能知道，由于 Python 的流行版本比较多，不同软件对于 Python 环境的要求也不同，所以我们直接更改本地的 Python 环境比较危险，因此，我推荐你使用Anaconda来创建不同 Python 的虚拟环境，这样就可以为咱们的 SparrowRecsys 项目，专门创建一个使用 Python3.7 和支持 TensorFlow2.3 的虚拟 Python 环境了。

具体的步骤我推荐你参考 Anaconda 的官方 TensorFlow 环境安装指导。

这里，我再带你梳理一下其中的关键步骤。首先，我们去[Anaconda 的官方地址] (https://www.anaconda.com/products/individual) 下载并安装 Anaconda（最新版本使用 Python3.8，你也可以去历史版本中安装 Python3.7 的版本）。然后，如果你是 Windows 环境，就打开 Anaconda Command Prompt，如果是 Mac 或 Linux 环境，你就打开 terminal。都打开之后，你跟着我输入下面的命令：

conda create -n tf tensorflow
conda activate tf

第一条命令会创建一个名字为 tf 的 TensorFlow 环境，第二条命令会让 Anaconda 为我们在这个 Python 环境中准备好所有 Tensorflow 需要的 Python 库依赖。接着，我们直接选择 TensorFlow 的 CPU 版本。不过，如果是有 GPU 环境的同学，可以把命令中的tensorflow替换为tensorflow-gpu，来安装支持 GPU 的版本。到这里，我们就利用 Anaconda 安装好了 TensorFlow 的 Python 环境。

最后是配置 IDEA 的项目 Python 环境。

现在 IDEA 的 Python 插件有了，本地的 TensorFlow Python 环境也有了，接下来，我们就要在 IDEA 中配置它的 Python 环境。这个配置的过程主要可以分成三步，我们一起来看看。

第一步，在 IDEA 中添加项目 Python SDK。你直接按照我给出的这个路径配置就可以了：File->Project Structure -> SDKs -> 点击 + 号 ->Add Python SDK ，这个路径在操作界面的显示如图 3。

图3 添加Python SDK

添加完 Python SDK，接下来，我们配置 Conda Environment 为项目的 Python SDK，IDEA 会自动检测到系统的 Conda 环境相关路径，你选择按照自动填充的路径就好，具体的操作你可以看下图 4。

图4 选择Conda Enviroment为IDEA Python环境

最后，我们为 TFRecModel 模块配置 Python 环境。我们选择 Project Structure Modules 部分的 tfmodel 模块，在其上点击右键来 add python。

图5 为tfmodel模块配置Python环境

设置好的 tfmode 模块的 Python 环境应该如图 6 所示。

图6 配置好的Python环境

如果你按照上面的步骤，那 Python 和 TensorFlow 的环境应该已经配置好了，但我们到底怎么验证一下所有的配置是否成功呢？下面，我们就来写一个测试模型来验证一下。

如何在 TensorFlow 上构建你的第一个深度学习模型？

这里，我们选择了 TensorFlow 官方教程上的例子作为我们深度学习模型的“初体验”，你可以参考 https://www.tensorflow.org/tutorials/quickstart/beginner?hl=zh_cn 来构建这个模型。这里，我再对其中的关键内容做一个补充讲解。

我先把测试模型的代码放到了下面，你可以边看代码边参考我的讲解。首先，我们要清楚的是这个测试模型做了一件什么事情。它要处理的数据集是一个手写数字的 MNIST 数据集，所以模型其实是一个多分类模型，它的输入是这个手写数字的图片（一个 28*28 维的图片），输出是一个 0-9 的多分类概率模型。

图7 MNIST数据集

其次，我们要清楚如何定义这个深度学习模型的结构。从代码中我们可以看出，测试模型使用了 TensorFlow 的 Keras 接口，这样就可以很清晰方便地定义一个三层神经网络了，它包括 28*28 的输入层，128 维的隐层，以及由 softmax 构成的多分类输出层。

之后是模型的训练，这里有 3 个参数的设定是很关键的，分别是 compile 函数中的 optimizer、loss，以及 fit 函数中的 epochs。其中，optimizer 指的是模型训练的方法，这里我们采用了深度学习训练中经常采用的 adam 优化方法，你也可以选择随机梯度下降（SGD），自动变更学习率的 AdaGrad，或者动量优化 Momentum 等等。

loss 指的是损失函数，因为这个问题是一个多分类问题，所以这个测试模型，我们使用了多分类交叉熵（Sparse Categorical Crossentropy）作为损失函数。

epochs 指的是训练时迭代训练的次数，单次训练指的是模型训练过程把所有训练数据学习了一遍。这里 epochs=5 意味着模型要反复 5 次学习训练数据，以便模型收敛到一个局部最优的位置。

最后是模型评估的过程，因为在构建模型时，我们选择了准确度（Accuracy）作为评估指标，所以 model.evaluate 函数会输出准确度的结果。

import tensorflow as tf
//载入MINST数据集
mnist = tf.keras.datasets.mnist
//划分训练集和测试集
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

//定义模型结构和模型参数
model = tf.keras.models.Sequential([
    //输入层28*28维矩阵
    tf.keras.layers.Flatten(input_shape=(28, 28)),
    //128维隐层，使用relu作为激活函数
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dropout(0.2),
    //输出层采用softmax模型，处理多分类问题
    tf.keras.layers.Dense(10, activation='softmax')
])
//定义模型的优化方法(adam)，损失函数(sparse_categorical_crossentropy)和评估指标(accuracy)
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

//训练模型，进行5轮迭代更新(epochs=5）
model.fit(x_train, y_train, epochs=5)
//评估模型
model.evaluate(x_test,  y_test, verbose=2

到这里，我们就介绍完了整个测试模型的全部流程。事实上，之后利用 TensorFlow 构建深度学习推荐模型的过程也是非常类似的，只是把其中特征处理、模型结构、训练方法进行了替换，整体的结构并没有变化。所以，理解测试模型的每一步对我们来说是非常重要的。

说回我们的测试模型，如果 Python 和 TensorFlow 的环境都配置正确的话，在 IDEA 中执行测试模型程序，你会看到 5 轮训练过程，每一轮的准确度指标，以及最终模型的评估结果。

Epoch 1/5
1875/1875 [==============================] - 1s 527us/step - loss: 0.3007 - accuracy: 0.9121
Epoch 2/5
1875/1875 [==============================] - 1s 516us/step - loss: 0.1451 - accuracy: 0.9575
Epoch 3/5
1875/1875 [==============================] - 1s 513us/step - loss: 0.1075 - accuracy: 0.9670
Epoch 4/5
1875/1875 [==============================] - 1s 516us/step - loss: 0.0873 - accuracy: 0.9729
Epoch 5/5
1875/1875 [==============================] - 1s 517us/step - loss: 0.0748 - accuracy: 0.9767
313/313 - 0s - loss: 0.0800 - accuracy: 0.9743

小结

这节课，我们一起学习了 TensorFlow 的基础知识，搭建起了 TensorFlow 的使用环境，并且编写了第一个基于 Keras 的深度学习模型。

TensorFlow 的基本原理，就是根据深度学习模型架构构建一个有向图，让数据以张量的形式在其中流动起来。

而安装 TensorFlow 有两种方法，一种是采用 Docker+Jupyter 的方式，另一种是在本地环境安装 TensorFlow 所需的 python 环境和所需依赖库。其中，Docker+Jupyter 的方式比较简单，而利用 IDEA 来调试 TensorFlow 代码的方法比较常用，我们要重点掌握。

想要实现在 IDEA 中调试 TensorFlow 代码，我们需要进行三步配置，分别是安装 IDEA 的 python 编译器插件，安装本地的 Python 环境，以及配置 IDEA 的 Python 环境。配置好了 Python 和 TensorFlow 的环境之后，我们还要验证一下是不是所有的配置都成功了。

在测试模型中，我们牢牢记住实现它的四个主要步骤，分别是载入数据、定义模型、训练模型和评估模型。当然，我把这些关键知识点总结在了下面的知识表格里，你可以看看。

课后思考

这是一堂实践课，所以今天的课后题就是希望你能完成 TensorFlow 的环境配置。在配置、试验过程中遇到任何问题，你都可以在留言区提问，我会尽力去解答，也欢迎同学们分享自己的实践经验，互相帮助。我们下节课见！

模型实战准备（二） | 模型特征、训练样本的处理

你好，我是王喆，欢迎来到模型实战准备第二课。

这节课，我们来讲实战中所需的模型特征和训练样本的处理。为什么我们要专门花一节课的时间讲这些呢？因为在推荐模型篇中，我们的重点在于学习模型结构的原理和实现，而要实现并且训练这些模型，我们就必须先解决训练所需的样本和特征的处理的问题。

这节课我们先来把模型实战的准备工作做完。具体来说，今天，我会带你用 Spark 来处理深度学习模型训练所需的样本和特征，再把特征存储到 Redis 中，供模型线上服务时调用。

为什么选择 Spark 为 TensorFlow 处理特征和样本？

这个时候，你可能会有疑问，我们的深度学习模型将在 TensorFlow 上进行训练，为什么要用 Spark 处理样本？可不可以直接让 TensorFlow 解决所有的事情呢？这是一个好问题，在我们学习具体的技术之前，先解决这个架构上的疑问是很有必要的。

在业界的实践中，我们需要记住一个原则，就是让合适的平台做合适的事情。比如说，数据处理是 Spark 的专长，流处理是 Flink 的专长，构建和训练模型是 TensorFlow 的专长。在使用这些平台的时候，我们最好能够用其所长，避其所短。这也是一个业界应用拥有那么多个模块、平台的原因。

你可能想说，TensorFlow 也可以处理数据啊。没错，但是它并不擅长分布式的并行数据处理，在并行数据处理能力上，TensorFlow 很难和动辄拥有几百上千个节点的 Spark 相比。那在面对海量数据的时候，如果我们能够利用 Spark 进行数据清洗、数据预处理、特征提取的话，最好的方案就是让 Spark 发挥它的长处，承担“繁重”但相对简单的样本和特征处理的工作，为 TensorFlow 减轻负担。

物品和用户特征都有哪些？

既然我们决定用 Spark 进行样本和特征处理，那下一个问题又接踵而来，我们能从 MovieLens 的数据集中抽取出什么特征呢？这就要用到我们在特征工程篇中学到的，关于推荐系统特征以及相关特征处理方法的知识，如果你记得还不够扎实，我建议你可以先回去复习一下。

MovieLens 数据集中，可供我们提取特征的数据表有两个，分别是 movies 表和 ratings 表，它们的数据格式如下：

图1 电影基本数据movies表（左），用户评分数据ratings表（右）

接下来，我们按照“物品特征”“用户特征”“场景特征”，这三大类推荐系统特征的顺序，来看一看从这两张表中能提取出什么样的特征。

“物品特征”在我们的项目里指的就是电影特征了，从 movies 表中我们可以提取出电影的基本信息，包括 movieId、title（电影名）、releaseYear（发布年份）和 genre（风格类型）。除此之外，我们还可以利用 ratings 表为每个电影提取出一些统计类的特征，包括电影的平均评分、评分标准差等等。

接下来是“用户特征”，乍一看，从 movies 和 ratings 表中，除了 userId，我们好像找不到其他直接可用的用户信息了。这个时候，千万不要忘了我们之前提到过的，用户特征最重要的部分就是历史行为特征，所以，从用户的评分历史中，我们其实可以提取出非常多有价值的特征。比如，我们可以根据 ratings 表的历史联合 movies 表的电影信息，提取出用户统计类特征，它包括用户评分总数、用户平均评分、用户评分标准差、用户好评电影的发布年份均值、用户好评电影的发布年份标准差、用户最喜欢的电影风格，以及用户好评电影 ID 等等。

最后是“场景特征”，我们可用的场景特征就一个，那就是评分的时间戳，我们把它作为代表时间场景的特征放到特征工程中。

好了，到这儿，我们就梳理完了所有可用的特征，我把它们总结在了下面的表格里，供你参考。

图2 所有可用的特征汇总表

用 Spark 来提取这些特征的总体实现会比较琐碎，所以我就不把全部代码贴在这里了，你可以参考 SparrowRecsys 项目中的 com.wzhe.sparrowrecsys.offline.spark.featureeng.FeatureEngForRecModel 对象，里面包含了所有特征工程的代码。这里，我们只讲几个有代表性的统计型特征的处理方法。

val movieRatingFeatures = samplesWithMovies3.groupBy(col("movieId"))
  .agg(count(lit(1)).as("movieRatingCount"),
   avg(col("rating")).as("movieAvgRating"),
   stddev(col("rating")).as("movieRatingStddev"))

计算统计型特征的典型方法，就是利用 Spark 中的 groupBy 操作，将原始评分数据按照 movieId 分组，然后用 agg 聚合操作来计算一些统计型特征。比如，在上面的代码中，我们就分别使用了 count 内置聚合函数来统计电影评价次数（movieRatingCount），用 avg 函数来统计评分均值（movieAvgRating），以及使用 stddev 函数来计算评价分数的标准差（movieRatingStddev）。

特征处理具体过程，我们就讲完了。不过，这里我还想和你多分享一些我的经验。一般来说，我们不会人为预设哪个特征有用，哪个特征无用，而是让模型自己去判断，如果一个特征的加入没有提升模型效果，我们再去除这个特征。就像我刚才虽然提取了不少特征，但并不是说每个模型都会使用全部的特征，而是根据模型结构、模型效果有针对性地部分使用它们。在接下来的课程中，我们还会详细探讨不同模型对这些特征的具体使用方法。

最终的训练样本是什么样的？

特征提取之后就到了训练模型的步骤，为了训练模型，我们还需要生成模型所需的训练样本。这里我们需要明确两件事情，一是样本从哪里来，二是样本的标签是什么。这两件事情都跟我们训练模型的目标有关系。

对于一个推荐模型来说，它的根本任务是预测一个用户 U 对一个物品 I 在场景 C 下的喜好分数。所以在训练时，我们要为模型生成一组包含 U、I、C 的特征，以及最终真实得分的样本。在 SparrowRecsys 中，这样的样本就是基于评分数据 ratings，联合用户、物品特征得来的。

其中，用户特征和物品特征都需要我们提前生成好，然后让它们与 ratings 数据进行 join 后，生成最终的训练样本，具体的实现也在 FeatureEngForRecModel 中，你可以先参考我在下面贴出的关键代码。这样，我们就解决了第一个关键问题。

//读取原始ratings数据
val ratingSamples = spark.read.format("csv").option("header", "true").load(ratingsResourcesPath.getPath)
//添加样本标签
val ratingSamplesWithLabel = addSampleLabel(ratingSamples)
//添加物品（电影）特征
val samplesWithMovieFeatures = addMovieFeatures(movieSamples, ratingSamplesWithLabel)
//添加用户特征
val samplesWithUserFeatures = addUserFeatures(samplesWithMovieFeatures)

接着，我们来看第二个关键问题，也就是样本的标签是什么，对于 MovieLens 数据集来说，用户对电影的评分是最直接的标签数据，因为它就是我们想要预测的用户对电影的评价，所以 ratings 表中的 0-5 的评分数据自然可以作为样本的标签。

但对于很多应用来说，我们基本上不可能拿到它们的评分数据，更多的是点击、观看、购买这些隐性的反馈数据，所以业界更多使用 CTR 预估这类解决二分类问题的模型去解决推荐问题。

为了让我们的实践过程更接近真实的应用场景，我也对 MovieLens 数据集进行了进一步处理。具体来说就是，把评分大于等于 3.5 分的样本标签标识为 1，意为“喜欢”，评分小于 3.5 分的样本标签标识为 0，意为“不喜欢”。这样一来，我们可以完全把推荐问题转换为 CTR 预估问题。

如何在生成样本时避免引入“未来信息”？

训练模型所需要的训练样本我们已经得到了，但是，在处理训练样本的时候，还有一个问题我们一定要注意，那就是引入未来信息（Future Information）的问题，这也是我们在实际工作中经常会遇到的问题。

什么叫做未来信息呢？如果我们在 t 时刻进行模型预测，那么 t+1 时刻的信息就是未来信息。这个问题在模型线上服务的时候是不存在的，因为未来的事情还未发生，我们不可能知道。但在离线训练的时候，我们就容易犯这样的错误。比如说，我们利用 t 时刻的样本进行训练，但是使用了全量的样本生成特征，那么这些特征就包含了 t+1 时刻的未来信息，这就是一个典型的引入未来信息的错误例子。

这样说可能还是有点抽象，我们用刚才讲过的特征举个例子。刚才我们说到有一个用户特征叫做用户平均评分（userAvgRating），我们通过把用户评论过的电影评分取均值得到它。假设，一个用户今年评论过三部电影，分别是 11 月 1 日评价电影 A，评分为 3 分，11 月 2 日评价电影 B，评分为 4 分，11 月 3 日评价电影 C，评分为 5 分。如果让你利用电影 B 这条评价记录生成样本，样本中 userAvgRating 这个特征的值应该取多少呢？

有的同学会说，应该取评价过的电影评分的均值啊，（3+4+5）/3=4 分，应该取 4 分啊。这就错了，因为在样本 B 发生的时候，样本 C 还未产生啊，它属于未来信息，你怎么能把 C 的评分也加进去计算呢？而且样本 B 的评分也不应该加进去，因为 userAvgRating 指的是历史评分均值，B 的评分是我们要预估的值，也不可以加到历史评分中去，所以正确答案是 3 分，我们只能考虑电影 A 的评分。

因此，在处理历史行为相关的特征的时候，我们一定要考虑未来信息问题。类似的还有用户评分总数、用户评分标准差、用户最喜欢的电影风格、用户好评电影 ID 等一系列特征。

那在 Spark 中，我们应该如何处理这些跟历史行为相关的特征呢？这就需要用到 window 函数了。比如说，我在生成 userAvgRating 这个特征的时候，是使用下面的代码生成的：

withColumn("userAvgRating", avg(col("rating"))
  .over(Window.partitionBy("userId")
    .orderBy(col("timestamp")).rowsBetween(-100, -1)))

我们可以看到，代码中有一个 over(Window.partitionBy(“userId”).orderBy(col(“timestamp”))) 操作，它的意思是，在做 rating 平均这个操作的时候，我们不要对这个 userId 下面的所有评分取平均值，而是要创建一个滑动窗口，先把这个用户下面的评分按照时间排序，再让这个滑动窗口一一滑动，滑动窗口的位置始终在当前 rating 前一个 rating 的位置。这样，我们再对滑动窗口内的分数做平均，就不会引入未来信息了。

类似的操作，我使用在了所有与历史行为有关的特征中，你也可以在 SparrowRecsys 的源码中看到。

如何把特征数据存入线上供模型服务用？

在生成好特征和训练样本之后，还有一个问题需要我们解决，那就是特征的线上存储问题。因为训练样本是供离线训练使用的，而线上模型推断过程是要使用线上特征的。

好在，特征数据库 Redis 已经为我们提供了解决办法。我们把用户特征和物品特征分别存入 Redis，线上推断的时候，再把所需的用户特征和物品特征分别取出，拼接成模型所需的特征向量就可以了。

FeatureEngForRecModel 中的 extractAndSaveUserFeaturesToRedis 函数给出了详细的 Redis 操作，我把其中的关键操作放在了下面。

val userKey = userFeaturePrefix + sample.getAs[String]("userId")
val valueMap = mutable.Map[String, String]()
valueMap("userRatedMovie1") = sample.getAs[String]("userRatedMovie1")
valueMap("userRatedMovie2") = sample.getAs[String]("userRatedMovie2")
...
valueMap("userAvgRating") = sample.getAs[String]("userAvgRating")
valueMap("userRatingStddev") = sample.getAs[String]("userRatingStddev")

redisClient.hset(userKey, JavaConversions.mapAsJavaMap(valueMap))

我们可以看到，代码中使用了 Redis 一个新的操作 hset，它的作用是把一个 Map 存入 Redis。这样做有什么好处呢？对于这里的用户特征来说，Map 中存储的就是特征的键值对，又因为这个 Map 本身是 userId 的值，所以，每个 userId 都拥有一组用户特征。这样一来，我们就可以在推荐服务器内部，通过 userId 来取出所有对应的用户特征了。当然，物品特征的储存方式是一样的。

到这里，我们完成了所有特征工程相关的准备工作，为之后的模型训练也做好了充足的准备。

小结

这节课，我们选择 Spark 作为特征和样本处理的平台，是因为 Spark 更擅长海量数据的分布式处理，为 TensorFlow 减轻数据处理的负担。在选择具体特征的过程中，我们遵循了“物品特征”“用户特征”“场景特征”这三大类特征分类方式，基于 MovieLens 的 ratings 表和 movies 表完成了特征抽取。

在样本处理过程中，我们选用评分和基于评分生成的好评差评标识作为样本标签，并基于 ratings 表的每条数据，通过联合物品和用户数据生成训练样本。在训练样本的生成中，要特别注意“未来信息”的问题，利用 Spark 中的 window 函数滑动生成历史行为相关特征。最后我们利用 Redis 的 hset 操作把线上推断用的特征存储 Redis。

这些重点内容，我也都总结在了下面的表格里，你可以看一看。

课后思考

为了避免引入未来信息，咱们课程中讲了基于 userId 的 window 函数方法，你觉得还有哪些方法也能避免引入未来信息吗？

欢迎把你的思考和答案写在留言区，如果你觉得今天的内容对你有所帮助，也欢迎你分享给你的朋友或同事。我们下节课见！

特别加餐 | “银弹”不可靠，最优的模型结构该怎么找？

你好，我是王喆。

推荐模型篇的课程到现在已经接近尾声了，我们也已经学习并且实践了六种深度学习推荐模型。最近，我发现很多同学会在留言区提出类似这样的问题：

老师，我的 Wide&Deep 模型在我的数据集上效果不好怎么办？

老师，是不是 DeepFM 模型的效果会比 NeuralCF 好？

老师，DIEN 模型是不是现在效果最好的模型？

其实，我完全理解同学们提问题的心情，就是希望在工作中通过不断地改进模型找到一个最强的模型，来尽快地提升推荐效果，击败当前的模型。我们团队中所有的新人也几乎都有这样的想法。

但是真的存在一个万能的模型结构，能在所有推荐系统上都达成最好的推荐效果吗？这节课，我希望我们能够放缓脚步，务虚一点，好好思考一下到底什么才是最好的模型结构，以及算法工程师正确的工作方法是什么。

有解决推荐问题的“银弹”吗？

在软件工程领域的著作《人月神话》中，作者 Brooks 凭借自己在 IBM 管理 2000 人完成大型项目的经验，得出了一个结论：没有任何技术或管理上的进展， 能够独立地许诺十年内使软件系统项目生产率、 可靠性或简洁性获得数量级上的进步。

Brooks 用“没有银弹”来形容这个结论。在欧洲的古老传说中，银色的子弹是能够一击杀死狼人这种怪物的特效武器。我们也可以把银弹理解成是最好的解决办法。“没有银弹”这个结论让很多期待寻找大型软件开发“捷径”的管理者和工程师们深感失望。但距离人月神话出版已经 45 年的今天，我们找到“银弹”了吗？

很遗憾，不仅在大型软件开发这个领域，“没有银弹”的观念深入人心，而且我要说的是，在推荐系统领域，也同样不存在能够一劳永逸解决问题的“银弹”，或者说根本不存在一种模型结构，它能够一击解决推荐效果问题，做到总是最优。

为什么这么讲呢？我们拿阿里的 DIEN 模型做一个例子。

我们在21 讲曾经详细介绍过 DIEN 模型，这里再做一个简要的回顾。DIEN 模型的整体结构是一个加入了 GRU 序列模型的深度学习网络。其中，序列模型部分主要负责利用用户的历史行为序列，来预测用户下一步的购买兴趣，模型的其他部分则是 Embedding MLP 的结构，用来把用户的兴趣向量跟其他特征连接后进行预测。

由于阿里巴巴在业界巨大的影响力，DIEN 模型自 2019 年提出以来，就被很多从业者认为是解决推荐问题的“银弹”，并纷纷进行尝试。

但是在应用的过程中，DIEN 并没有体现出“银弹”的效果。在这个时候，大家又都习惯于从自身上找原因，比如说，“是不是 Embedding 层的维度不够”，“是不是应该再增加兴趣演化层的状态数量”，“是不是哪个模型参数没有调好”等等。包括我自己在实践的过程中，也会因为 DIEN 并没有产生预期的推荐效果提升，而去思考是不是因为我们没有完整的复现 DIEN 模型。

说了这么多，我想强调的是，所有提出类似问题的同行都默认了一个前提假设，就是在阿里巴巴的推荐场景下能够提高效果的 DIEN 模型，在其他应用场景下应该同样有效。然而，这个假设真的合理吗？DIEN 模型是推荐系统领域的“银弹”吗？当然不是。接下来，我就带你一起分析一下。

既然 DIEN 的要点是模拟并表达用户兴趣进化的过程，那模型应用的前提必然是应用场景中存在着“兴趣进化”的过程。阿里巴巴的电商场景下，因为用户的购买兴趣在不同时间点有变化，所以有着明显的兴趣进化趋势。

比如说，用户在购买了电脑后，就有一定概率会购买电脑周边产品，用户在购买了某些类型的服装后，就会有一定概率选择与其搭配的其他服装。这些都是兴趣进化的直观例子，也是阿里巴巴的电商场景下非常典型的情况。

除此之外，我们还发现，在阿里巴巴的应用场景下，用户的兴趣进化路径能够被整个数据流近乎完整的保留。作为中国最大的电商集团，阿里巴巴各产品线组成的产品矩阵几乎能够完整地抓住用户购物过程中兴趣迁移的过程。当然，用户有可能去京东、拼多多等电商平台购物，从而打断阿里巴巴的兴趣进化过程，但从统计意义上讲，大量用户的购物链条还是可以被阿里巴巴的数据体系捕获的。

这样一来，我们就总结出了 DIEN 有效的前提是应用场景满足两个条件，一是应用场景存在“兴趣的进化”。二是用户兴趣的进化过程能够被数据完整捕获。如果二者中有一个条件不成立，DIEN 就很可能不会带来较大的收益。

为什么这么说呢？还是以我自身的实践经历为例，我现在工作的公司 Roku 是北美最大的视频流媒体平台，在使用的过程中，用户既可以选择我们自己的频道和内容，也可以选择观看 Netflix、YouTube，或者其他流媒体频道的内容。但是，一旦用户进入 Netflix 或者其他第三方应用，我们就无法得到应用中的具体数据了。

在这样的场景下，我们仅能获取用户一部分的观看、点击数据，而且这部分的数据仅占用户全部数据的 10% 左右，用户的整个行为过程我们无法完全获取到，那么谈何构建起整个兴趣进化链条呢？

另外一点也很关键，通过分析我们发现，长视频用户的兴趣点相比电商而言其实是非常稳定的。电商用户可以今天买一套衣服，明天买一套化妆品，兴趣的变化过程非常快。但你很难想象长视频用户今天喜欢看科幻电影，明天就喜欢看爱情片，绝大多数用户喜好非常稳定地集中在一个或者几个兴趣点上。这也是序列模型并不能给我们公司提高效果的另一个主要原因。

总的来说，通过 DIEN 的例子我们可以得出，到底怎样的模型结构是最优的模型结构，跟你的业务特点和数据特点强相关。因此，在模型结构的选择上，没有“银弹”，没有最优，只有最合适。

在工作中避免学生思维

那么有没有可供参考的方法论来指导模型结构的选择，或者说更广义一点，指导各种模型参数的调优呢？当然是有的，但在谈这个方法论之前，我想先纠正一个工作中非常有害的思维方式，特别是对于刚工作一两年的同学，我们最应该纠正的就是工作中的学生思维。

“学生思维”最典型的表现就是总是在寻求一个问题的标准答案。因为在我们的学生时代，所有的题目都是有标准答案的，会就是会，不会就是不会。

但在工作中就不一样了。举个例子来说，在讲 Embedding 的部分，很多同学问我 Embedding 到底应该取多少维？在实际的工作中，这就是一个典型的没有标准答案的问题。实际工作中，它应有的决策链条应该是下面这样的：

先取一个初始值，比如说 10 维来尝试一下效果；

以 10 维 Embedding 的效果作为 baseline，进行一定程度的参数调优，比如尝试 5 维和 20 维的 Embedding，比较它跟 10 维的效果，确定更好的维度数；

如果项目时间和硬件条件允许，我们还可以尝试 fine tunning（精细调参），直到找到最优的维度设置；

在上线前再次评估 Embedding 线上存储所需存储量的限制，如果线上存储的可用空间有限，我们可以通过适当降低维度数缩小 Embedding 所需的存储空间。

你从这个过程中肯定能够体会到，所谓 Embedding 的维度到底取多少这个问题。根本没有标准答案，最优的答案跟你的数据量，Embedding 模型本身的结构都有关系，甚至还要受到工程环境的制约。

类似的问题还有“局部敏感哈希到底要选择几个桶”，“召回层的 TopN 中，N 到底应该选择多少”，“Wide&Deep 模型中可不可以加入新的用户特征”，“要不要在模型中加入正则化项，Drop out”等等。这些问题都没有标准答案，你只有通过实践中的尝试才能得到最优的设定。

其实这也是我在讲解这门课时一直秉承的原则，我希望把业界的主流方法告诉你，期望你建立起来的是一套知识体系和方法论，而不是一套能让你一劳永逸的模型参数，因为“银弹”并不存在。

算法工程师正确的工作方法

我们刚刚否定了“学生思维”，那该怎么建立起一套正确的算法工程师思维呢？下面是我总结出的一套通用的算法工程师的工作流程，虽然不能说我一定掌握了“正确”的方法，但这些工作方法都是从我多年的工作经验中总结出来的，也都得到了验证，你完全可以借助它们来完善自己的工作方法。

问题提出 ： 清楚领导提出的问题，或者自己发现问题。

数据和业务探索 ： 在动手解决这个问题之前，我们一定要花时间弄清楚业务的相关逻辑，并且动手用一些脚本程序弄清楚自己可利用数据的数据量、数据特点、提取出一些特征并分析特征和标签之间的相关性。

初始解决方案 ： 根据探索结果提出初始解决方案。

解决方案调优 ：在初始解决方案之上，进行技术选型和参数调优，确定最终的解决方案。

工程落地调整 ：针对工程上的限制调整技术方案，尽量做到能简勿繁，能稳定不冒险。

生产环境上线 ： 进行最终的调整之后，在生产环境上线。

迭代与复盘 ： 根据生产环境的结果进行迭代优化，并复盘这个过程，继续发现问题，解决问题。

最后我还想补充一点，我一直认为，做算法工程师，首先要有扎实全面的技术功底，但更重要的其实是自信和务实的精神，不迷信所谓的权威模型，不试图寻找万能的参数，从业务出发，从用户的真实行为出发，才能够构建出最适合你业务场景的推荐模型 。

小结

在解决推荐问题上，我认为是没有“银弹”的，特别是在模型结构这个点上，我们必须综合考虑自己的业务特点和数据特点，在实践中不断进行参数调优，才能找到最合适自己业务场景的模型。

事实上，不仅仅是推荐问题，对于其他问题来说，我也不建议同学们去追求所谓的“银弹”。换句话说，我们必须要尽量避免学生思维，不要总是试图去寻找标准答案，而是应该尽可能多地掌握业界的主流技术手段，丰富自己的“武器库”，建立自己的知识框架。这样，我们才能在面对实际工作中复杂问题的时候，找到最合适的兵器。

除此之外，作为一名算法工程师，我建议你在工作中按照“问题提出”，“数据和业务探索”，“提出初始解决方案”，“解决方案调优”，“工程落地调整”，“生产环境上线”，“迭代与复盘”的顺序，去完成整个的项目周期。这是能帮助你快速建立起正确方法论的有效途径。

总的来说，算法工程师是一份极有挑战的工作，相比研发岗有非常确定的项目目标，算法的优化工作往往需要我们自己去找到那个可以提升的点，自己去找到那组最合适的参数，并且可以完成生产环境的工程实现。这给了我们极大的灵活性，也给了我们不小的业绩压力。希望这节课可以帮助你纠正一些误区，与你共勉。

课后思考

推荐模型的研究可谓层出不穷，很多同学都热衷于追求实现最前沿的技术，最 fancy 的模型，你这样的吗？你认可这种现象吗？

欢迎把你的想法写在留言区，我们一起讨论！

24 | 离线评估：常用的推荐系统离线评估方法有哪些？

你好，我是王喆。今天我们要进入一个全新的章节，模型评估篇。

在推荐系统这个行业，所有人都在谈效果。就像我们在学习推荐模型篇的时候，你肯定也有过这样的疑问：

DIEN 这个模型的效果到底怎么样啊？

我们用深度学习来构建模型到底能让推荐系统效果提高多少啊？

DeepFM 的效果是不是会比 Wide&Deep 好呢？

那这个所谓的“效果”到底指的是什么呢？我们一般用什么方法来衡量这个“效果”呢？我们又应该如何根据效果评估的结果来更新模型呢？这就是模型评估篇要解决的问题。

在所有推荐系统的评估方法中，离线评估是最常用、最基本的。顾名思义，“离线评估”就是我们将模型部署于线上环境之前，在离线环境下进行的评估。由于不用部署到生产环境，“离线评估”没有线上部署的工程风险，也不会浪费宝贵的线上流量资源，而且具有测试时间短，可多组并行，以及能够利用丰富的线下计算资源等诸多优点。

因此，在模型上线之前，进行大量的离线评估是验证模型效果最高效的手段。这节课，我们就来讲讲离线评估的主要方法，以及怎么在 Spark 平台上实现离线评估。

离线评估的主要方法

离线评估的基本原理是在离线环境下，将数据集分为“训练集”和“测试集”两部分，“训练集”用来训练模型，“测试集”用于评估模型。但是如何划分测试集和训练集，其实这里面有很多学问。我总结了一下，常用的离线评估方法主要有五种，分别是：Holdout 检验、交叉检验、自助法、时间切割、离线 Replay。接下来，我们一一来看。

Holdout 检验、交叉检验和自助法

首先，我们来看 Holdout 检验。 Holdout 检验是最基础，最常用的离线评估方法，它将原始的样本集合随机划分为训练集和测试集两部分，所以 Holdout 检验的关键词就是“随机”。举例来说，对于一个推荐模型，我们可以把样本按照 70%-30% 的比例随机分成两部分。其中，70% 的样本用于模型的训练，30% 的样本用于模型的评估。

虽然 Holdout 检验很简单实用，但它的缺点也很明显，就是评估的结果有一定随机性，因为训练集和验证集的划分是随机的，所以如果只进行少量的 Holdout 检验，得到的评估指标会存在一定的波动。那为了消除这种随机性，我们就要使用“交叉检验”的方法。

为了进行交叉检验，我们需要先将全部样本划分成 k 个大小相等的样本子集，然后依次遍历这 k 个子集，每次把当前遍历到的子集作为验证集，其余所有的子集作为训练集，这样依次进行 k 次模型的训练和评估。最后，我们再将所有 k 次评估指标的平均值作为最终的评估指标。在我们的实践中，k 经常取 10，也就是依次进行 10 次检验然后取指标均值。

不管是 Holdout 检验还是交叉检验，都是基于划分训练集和测试集的方法进行模型评估的。然而，当样本规模比较小时，将样本集进行划分会让训练集进一步减小，这往往会影响模型的训练效果。那有没有能维持训练集样本规模的验证方法呢？

“自助法”就可以在一定程度上解决这个问题。我这里所说的自助法（Bootstrap）是基于自助采样的检验方法，它的主要过程是：对于总数为 n 的样本集合，我们先进行 n 次有放回地随机抽样，得到大小为 n 的训练集。在 n 次采样过程中，有的样本会被重复采样，有的样本没有被抽出过，我们再将这些没有被抽出的样本作为验证集进行模型验证，这就是自助法的验证过程。

虽然自主法能够保持训练集的规模，但是它的缺点也很明显，它其实改变了原有数据的分布，有可能让模型产生一定程度的偏差。至于，到底是自助采样增加训练样本规模的收益大，还是数据分布被改变带来的损失大，这就需要我们在实践中进行验证了。

时间切割

说完了前三种方法，我们再来看时间切割法。在“模型实战准备（二）”那节课里，我们曾经讲过一个概念，叫“未来信息”。它是说，如果我们在 t 时刻进行模型预测，那么 t+1 时刻的信息就是未来信息。在构建特征工程的时候，我们要避免引入“未来信息”。

其实，在进行模型评估的时候，我们同样不应该在训练集中包含“未来”的样本。怎么理解这句话呢？比如，我们所有的样本数据分布在 t0到 tn这样的时间轴上，如果训练样本是通过随机采样得到的，那么训练数据也会分布在 t0到 tn上，同样，测试数据也会分布在 t0到 tn上。

如果你细想，这个事情其实是有点反常理的。因为训练模型的时候，我们已经使用了 tn这个时间窗口的数据，结果你却用它来预测 t0的事件，这不是很荒谬吗？这就相当于你有一个时光机，已经穿越到了明天，知道股票会涨，结果你又穿越回来，预测说明天股票会涨，这哪是预测呢？这就是“作弊”。

为了防止这类“信息穿越”导致的模型作弊现象发生，我们一般会使用时间切割的方案去划分训练集和测试集，它的做法很简单。比如，你一共处理了 30 天的样本，从第 25 天末开始切割，前 25 天的样本作为训练集，后 5 天的样本作为测试集，这样我们就从根源上切断了引入“未来信息”的可能。当然切割的比例到底如何，也需要根据你的实践来定，一般来说我们控制训练集跟测试集的比例在 3:1 到 10:1 之间，比例太小训练样本不够，比例太大测试结果不够稳定。

离线 Replay

时间切割的方法虽然能避免“信息穿越”，但也不是没有缺点的。它的缺点就在于整个评估过程是静态的，模型不会随着评估的进行而更新，这显然是不符合事实的。就拿我们刚才举的例子来说，用前 25 天的数据做训练集，用后 5 天的数据做测试集。如果在生产环境中，模型是日更新的，那后 5 天的评测过程就不准确，因为在离线测试中，我们并没有在后 5 天的评测过程中做到日更模型。

那怎么解决这个问题呢？我们也可以在离线状态下对线上更新过程进行仿真，让整个评估过程“动”起来。业界把这样离线仿真式的评估方式叫做离线 Replay。

下图就是动态的 Replay 评估法与静态的时间分割评估法的对比示意图。我们可以看到，“Replay 评估方法”先根据产生时间对测试样本，由早到晚地进行排序，再让模型根据样本时间的先后进行预测。在模型更新的时间点上，模型需要增量学习更新时间点前的测试样本，更新模型后，再继续评估更新点之后的样本。

图1 静态时间分割评估与动态Replay评估 （出自《深度学习推荐系统》）

你应该也发现了，Replay 评估的过程更接近于真实的线上环境，因为它在线下还原了模型在线上的更新、预估过程。这也让 Replay 方法的评估结果更加权威可信，毕竟，我们最终的目标是让模型在线上产生更好的效果。

当然，Replay 评估方法也有弊端，因为它需要在评估过程中不断更新模型，这让评估过程的工程实现难度加大，因为包含了模型训练的时间，所以整个评估过程的总时长也会加长，影响评估和调参的效率。到底是要评估的准确性，还是要评估的效率，这又是一个需要权衡的问题，我们需要根据自己工程上的侧重点进行选择。

基于 Spark 的离线评估方法实践

熟悉了离线环节的主要模型评估方法，就又到了实践的环节。其实，无论是基于 Python 的 TensorFlow 还是基于 Scala 的 Spark，都有很多支持离线评估的库，这里我们选择了 Spark 进行实践，主要是因为在业界数据集很大的情况下，Spark 在分布式环境下划分训练集和测试集的效率是最高的。

下面，我就来看一下如何使用 Spark 实现 Holdout 检验、交叉检验和时间切割评估法。至于另外两种方法，由于自助法不太常用，离线 Replay 又涉及过多的附加模块，我们暂时就不在项目里实现。

实现 Holdout 检验的时候，我们要清楚如何利用 Spark 随机划分测试集和训练集。它的关键代码只有下面这一行，就是利用 randomSplit 函数把全量样本 samples 按比例分割成 trainingSamples 和 testSamples。在 Spark 的后端，这个 randomSplit 函数会在各个节点分布式执行，所以整个执行效率是非常高的。源代码你可以参考 com.wzhe.sparrowrecsys.offline.spark.featureeng.FeatureEngForRecModel 中的 splitAndSaveTrainingTestSamples 函数。

val Array(trainingSamples, testSamples) = samples.randomSplit(Array(0.9, 0.1))

实现交叉检验的过程相对比较复杂，好在，Spark 已经提供了交叉检验的接口可以直接使用，我们直接看一下这部分的关键代码。

val cv = new CrossValidator()
  .setEstimator(modelPipeline)
  .setEvaluator(new BinaryClassificationEvaluator)
  .setEstimatorParamMaps(paramGrid)
  .setNumFolds(10)  // Use 3+ in practice
val cvModel = cv.fit(training)

这段代码中有三个关键参数，一是 setEstimator，这是我们要评估的对象，它需要把我们构建的模型 pipeline 设置进去；二是 setEvaluator，它用来设置评估所用的方法和指标；三是 setNumFolds，它设置的是交叉检验中 k 的值，也就是把样本分成多少份用于交叉检验。本质上 Spark 的 CrossValidator 其实是通过交叉检验来选择模型的最优参数，但也可以通过模型中 cvModel.avgMetrics 参数查看模型的评估指标。

接下来，我们来实现时间切割方法。既然是要按时间划分，如果你知道样本的时间跨度，直接用 where 语句就可以把训练集和测试集划分开了，这也是我最推荐的方法，因为它最高效，不用专门判断切割点。

如果你不知道样本的时间跨度，就要按照时间求取样本的分位数。具体来说就是，通过 Spark 的 approxQuantile 函数，我们可以找到划分样本集为 8:2 的训练集和测试集的时间戳的值。那么接下来我们根据这个值通过 where 语句划分就可以了。我把这个过程的关键代码贴到了下面，供你参考。完整的源代码，你可以参考 com.wzhe.sparrowrecsys.offline.spark.featureeng.FeatureEngForRecModel 中的 splitAndSaveTrainingTestSamplesByTimeStamp 函数。

//找到时间切割点
val quantile = smallSamples.stat.approxQuantile("timestampLong", Array(0.8), 0.05)
val splitTimestamp = quantile.apply(0)
//切割样本为训练集和测试集
val training = smallSamples.where(col("timestampLong") <= splitTimestamp).drop("timestampLong")
val test = smallSamples.where(col("timestampLong") > splitTimestamp).drop("timestampLong")

小结

这节课，我们学习了五种主流的推荐模型离线评估方法，它们分别是 Holdout 检验、交叉检验、自助法、时间切割和离线 Replay。

其中，Holdout 检验最简单常用，它通过随机划分的方式把样本集划分成训练集和测试集。而交叉检验的评估效果更加稳定准确，它通过划分样本集为 k 份，再进行 k 次评估取平均的方式得到最终的评估指标。

自助法是为了解决样本量过少而提出的，它可以通过有放回采样的方式扩充训练集，但有改变数据本身分布的风险。而时间切割法在某个时间点上把样本分成前后两份，分别用于模型训练和评估，避免引入未来信息。最后是离线 Replay，它通过仿真线上模型更新过程来进行评估，是最接近线上环境的离线评估方法，但实现起来比较复杂。

总之，各种评估方法都有优有劣，你需要根据实践中的侧重点选择使用，我把它们的优缺点也总结在了文稿的表格里，方便你进行对比。

这节课我们讲了评估模型效果的方法之一，离线评估。但我们并没有具体来讲“效果”的衡量指标到底是什么。别着急，下节课我们就来学习推荐系统主要使用的效果评估指标，也会利用这节课学习到的评估方法来生成这些指标。

课后思考

你觉得离线 Replay 这个方法，跟我们之前讲过的增强学习有什么相似之处吗？你知道它们两个还有什么更深层次的关系吗？

期待在留言区看到你的发现和思考，我们下节课见！

25 | 评估指标：我们可以用哪些指标来衡量模型的好坏？

你好，我是王喆。今天，我们来学习推荐模型的评估指标。

上节课，我们讲了五种评估方法，清楚了它们都是怎么把样本分割为训练集和测试集的。但是只分割样本是远远不够的，为了比较模型效果的好坏，还得用一些指标进行衡量。就像我们工作中经常说，我的模型提高了“一个点”的效果，那所谓的“一个点”指的是什么呢？它其实说的就是，我们的模型在一些经典的推荐指标上提升了 1% 的效果，这节课我就带你来捋一捋这些经典的推荐评估指标。

低阶评估指标

我按照指标计算的难易程度，和评估的全面性，把推荐系统的评估指标可以分成低阶评估指标和高阶评估指标两大类。对于低阶评估指标来说，准确率、精确率与召回率、对数损失、均方根误差，这四个指标在推荐模型评估中最常用，计算起来也最容易。所以，我们就先来学习一下这几个低阶评估指标的具体含义。

1. 准确率

准确率 (Accuracy) 是指分类正确的样本占总样本个数的比例，公式 1 就是：Accuracy=ntotal ​ncorrect ​​。

其中， ncorrect 是正确分类的样本个数， ntotal 是样本的总数。

准确率是分类任务中非常直观的评价指标，可解释性也很强，但它也存在明显的缺陷，就是当不同类别的样本比例非常不均衡的时候，占比大的类别往往成为影响准确率的最主要因素。比如，负样本占 99%，那么分类器把所有样本都预测为负样本也可以获得 99% 的准确率。

在之前的课程中，我们经常把推荐问题看作是一个点击率预估型的分类问题。这个时候，我们就可以用准确率来衡量推荐模型的好坏。但在实际的推荐场景中，我们往往会生成一个推荐列表，而不是用所谓的分类正不正确来衡量最终的效果，那我们该怎么评估一个推荐列表的效果呢？这个时候，我们就会利用到精确率和召回率这两个指标。

2. 精确率与召回率

我这里所说的精确率（Precision）指的是分类正确的正样本个数占分类器判定为正样本个数的比例，召回率（Recall）是分类正确的正样本个数占真正的正样本个数的比例。

在推荐列表中，通常没有一个确定的阈值来把预测结果直接判定为正样本或负样本，而是采用 Top N 排序结果的精确率（Precision@N）和召回率（Recall@N）来衡量排序模型的性能。具体操作，就是认为模型排序的前 N 个结果就是模型判定的正样本，然后分别计算 Precision@N 和 Recall@N。

事实上，精确率和召回率其实是矛盾统一的一对指标。这是什么意思呢？就是，为了提高精确率，模型需要尽量在“更有把握”时把样本预测为正样本，但此时，我们往往会因为过于保守而漏掉很多“没有把握”的正样本，导致召回率降低。

那有没有一个指标能综合地反映精确率和召回率的高低呢？其实是有的，那就是 F1-score。F1-score 的定义是精确率和召回率的调和平均值，具体的定义你可以看看下面的公式 2。F1-score 的值越高，就证明模型在精确率和召回率的整体表现上越好。

F1= precision + recall 2⋅ precision ⋅ recall ​

3. 对数损失

接着，我们来说一说对数损失（Logloss）这个评估指标。

首先，在一个二分类问题中，对数损失函数的定义就是下面的公式 3。

−N1​i=1∑N​(yi​logPi​+(1−yi​)log(1−Pi​))

在这个公式中，yi​ 是输入实例 xi​ 的真实类别, pi​ 是预测输入实例 xi​ 是正样本的概率，N 是样本总数。

而面对多分类问题的时候，对数损失函数定义就变成了下面公式 4 的样子：

 Multi-LogLoss =−n1​i=1∑n​j=1∑m​yi,j​log(pi,j​)

如果你仔细看公式就会发现，二分类和多分类模型的 Logloss 其实就是我们之前讲过的逻辑回归和 Softmax 模型的损失函数，而大量深度学习模型的输出层正是逻辑回归或 Softmax，因此，采用 Logloss 作为评估指标能够非常直观地反映模型损失函数的变化。所以在训练模型的过程中，我们在每一轮训练中都会输出 Logloss，来观察模型的收敛情况。

4. 均方根误差

刚才我们说的准确率、精确率、召回率、LogLoss 都是针对分类模型指定的指标。分类模型就是指预测某个样本属于哪个类别的模型，最典型的就是点击率预估模型。除了这类分类模型以外，还有回归模型，它是用来预测一个连续值，比如预测某个用户对某个电影会打多少分，这就是一个回归模型。

那我们对于回归模型有什么合适的评估指标吗？对于回归模型来说，最常用的评估指标就是均方根误差（RMSE，Root Mean Square Error）。它的公式是求预测值跟真实值之间差值的均方根：

RMSE=n∑i=1n​(yi​−y^​l​)2​​

这个公式中，yi​ 是第 i 个样本点的真实值，y^​l​ 是第 i 个样本点的预测值，n 是样本点的个数。那么均方根误差越小，当然就证明这个回归模型预测越精确。

总的来说，我们刚才说的这四个评估指标，虽然在推荐系统中最常用，计算起来也最简单，但它们反应的结果还不够精确和全面。

比如说，精确率和召回率可以反应模型在 Top n 个排序结果上的表现，但我们要知道，在真正的推荐问题中，n 的值是变化的，因为用户可能会通过不断的翻页、下滑来拉取更多的推荐结果，这就需要有更高阶的评估指标来衡量模型在不同数量推荐结果上的综合性能。所以，我们接下来再讲几个非常流行，也非常权威的高阶评估指标。

高阶评估指标

那在高阶评估指标部分，我会给你讲 P-R 曲线、ROC 曲线、平均精度均值，这三个最常用的评估指标。

1. P-R 曲线

首先，我要说的是 P-R 曲线，这里的 P 就是我们之前学过的精确率 Precision，R 就是召回率 Recall。刚才我们说了，为了综合评价一个推荐模型的好坏，不仅要看模型在一个 Top n 值下的精确率和召回率，还要看到模型在不同 N 取值下的表现，甚至最好能绘制出一条 n 从 1 到 N，准确率和召回率变化的曲线。这条曲线就是 P-R 曲线。

P-R 曲线的横轴是召回率，纵轴是精确率。对于一个推荐模型来说，它的 P-R 曲线上的一个点代表“在某一阈值下，模型将大于该阈值的结果判定为正样本，将小于该阈值的结果判定为负样本时，整体结果对应的召回率和精确率”。整条 P-R 曲线是通过从高到低移动正样本阈值生成的。如图 1 所示，它画了两个测试模型，模型 A 和模型 B 的对比曲线。其中，实线代表模型 A 的 P-R 曲线，虚线代表模型 B 的 P-R 曲线。

从图中我们可以看到，在召回率接近 0 时，模型 A 的精确率是 0.9，模型 B 的精确率是 1。这说明模型 B 预测的得分前几位的样本全部是真正的正样本，而模型 A 即使是得分最高的几个样本也存在预测错误的情况。

然而，随着召回率的增加，两个模型的精确率整体上都有所下降。特别是当召回率在 0.6 附近时，模型 A 的精确率反而超过了模型 B。这就充分说明了，只用一个点的精确率和召回率是不能全面衡量模型性能的，只有通过 P-R 曲线的整体表现，才能对模型进行更全面的评估。

虽然 P-R 曲线能全面衡量模型的性能，但是它总归是一条曲线，不是一个数字，我们很难用它直接来判断模型的好坏。那有没有一个指标能用来衡量 P-R 曲线的优劣呢？当然是有的，这个指标就是 AUC(Area Under Curve)，曲线下面积。顾名思义，AUC 指的是 P-R 曲线下的面积大小，因此计算 AUC 值只需要沿着 P-R 曲线横轴做积分。AUC 越大，就证明推荐模型的性能越好。

2. ROC 曲线

接着，我们再来介绍第二个高阶指标，ROC 曲线，它也是一个非常常用的衡量模型综合性能的指标。ROC 曲线的全称是 the Receiver Operating Characteristic 曲线，中文名为“受试者工作特征曲线”。ROC 曲线最早诞生于军事领域，而后在医学领域应用甚广，“受试者工作特征曲线”这一名称也正是来源于医学领域。

ROC 曲线的横坐标是 False Positive Rate（FPR，假阳性率），纵坐标是 True Positive Rate （TPR，真阳性率）。这两个名字读上去就有点拗口，我们还是通过它们的定义来理解一下 ：

FPR=NFP​,TPR=PTP​

在公式中，P 指的是真实的正样本数量，N 是真实的负样本数量；TP 指的是 P 个正样本中被分类器预测为正样本的个数，FP 指的是 N 个负样本中被分类器预测为正样本的个数。但我估计你看了这个定义，可能还是不好理解这个 ROC 曲线是怎么得到的。没关系，我们真正去画一条 ROC 曲线，你就明白了。

和 P-R 曲线一样，ROC 曲线也是通过不断移动模型正样本阈值生成的。假设测试集中一共有 20 个样本，模型的输出如下表所示，表中第一列为样本序号，Class 为样本的真实标签，Score 为模型输出的样本为正的概率，样本按照预测概率从高到低排序。在输出最终的正例、负例之前，我们需要指定一个阈值，并且设定预测概率大于该阈值的样本会被判为正例，小于该阈值的会被判为负例。

比如，我们指定 0.9 为阈值，那么只有第一个样本会被预测为正例，其他全部都是负例。这里的阈值也被称为 “截断点”。

接下来，我们要做的就是动态地调整截断点，从最高的得分开始（实际上是从正无穷开始，对应着 ROC 曲线的零点），逐渐调整到最低得分。每一个截断点都会对应一个 FPR 和 TPR 的值，在 ROC 图上绘制出每个截断点对应的位置，再连接每个点之后，我们就能得到最终的 ROC 曲线了。那么 ROC 曲线上的点具体应该怎么确定呢？

我们来看几个例子，当截断点选择为正无穷的时候，模型会把全部样本预测为负例，那 FP 和 TP 必然都为 0，FPR 和 TPR 也都为 0，因此曲线的第一个点就是 (0,0) 。当把截断点调整为 0.9 的时候，模型预测 1 号样本为正样本，并且这个样本也确实是正样本。因此，在 20 个样本中，当 TP=1，所有正例数量 P=10 的时候，TPR=TP/P=1/10。

我们还可以看到，这个例子里没有预测错的正样本，也就是说当 FP=0，负样本总数 N=10 的时候，FPR=FP/N=0/10=0，对应着 ROC 图上的点 (0,0.1)。

其实，还有一种更直观的绘制 ROC 曲线的方法。首先，我们根据样本标签统计出正负样本的数量，假设正样本数量为 P，负样本数量为 N。然后，我们把横轴的刻度间隔设置为 1/N，纵轴的刻度间隔设置为 1/P。接着，我们再根据模型输出的预测概率对样本进行从高到低的排序。

最后，依次遍历样本。同时，从零点开始绘制 ROC 曲线，每遇到一个正样本就沿纵轴方向绘制一个刻度间隔的曲线，每遇到一个负样本就沿横轴方向绘制一个刻度间隔的曲线，直到遍历完所有样本，曲线最终停在 (1,1) 这个点，整个 ROC 曲线就绘制完成了。

在绘制完 ROC 曲线后，我们也可以像 P-R 曲线一样，计算出 ROC 曲线的 AUC，AUC 越高，推荐模型的效果就越好。

3. 平均精度均值

最后，我们来说平均精度均值 mAP（mAP，mean average precision）这个高阶指标，它除了在推荐系统中比较常用，在信息检索领域也很常用。mAP 其实是对平均精度（AP，average precision）的再次平均，因此在计算 mAP 前，我们需要先学习什么是平均精度 AP。

假设，推荐系统对某一用户测试集的排序结果是 1, 0, 0, 1, 1, 1。其中，1 代表正样本，0 代表负样本。接下来，我们就按照之前学过的方法，计算这个序列中每个位置上的 precision@N。你可以自己先试着计算一下，也可以直接看我下面计算好的结果。

每个位置的precision@N值

计算平均精度 AP 的时候，我们只取正样本处的 precision 进行平均，根据得到的表格 AP =（1/1 + 2/4 + 3/5 + 4/6）/4 = 0.6917。接下来，我们再来看什么是 mAP。

如果推荐系统对测试集中的每个用户都进行样本排序，那么每个用户都会计算出一个 AP 值，再对所有用户的 AP 值进行平均，就得到了 mAP。也就是说，mAP 是对精确度平均的平均。

这里就需要注意了，mAP 的计算方法和 P-R 曲线、ROC 曲线的计算方法是完全不同的，因为 mAP 需要对每个用户的样本进行分用户排序，而 P-R 曲线和 ROC 曲线均是对全量测试样本进行排序。这一点在实际操作中是需要注意的。

合理选择评估指标

到这里，这节课的 7 个评估指标我们就讲完了。如果你是第一次接触它们，可能现在已经有点茫然了。事实上，除了这些评估指标，还有很多其他的推荐系统指标，比如归一化折扣累计收益（Normalized Discounted Cumulative Gain,NDCG）、覆盖率（Coverage）、多样性（Diversity）等等。那面对这么多评估指标，你肯定想问，我们应该怎么选择它们呢？

很可惜，这次又是一个开放式的问题，评估指标的选择同样没有标准答案。但我还是会把一些经验性的选择总结出来，希望能够帮助到你。

比如，在对推荐模型的离线评估中，大家默认的权威指标是 ROC 曲线的 AUC。但 AUC 评估的是整体样本的 ROC 曲线，所以我们往往需要补充分析 mAP，或者对 ROC 曲线进行一些改进，我们可以先绘制分用户的 ROC，再进行用户 AUC 的平均等等。

再比如，在评估 CTR 模型效果的时候，我们可以采用准确率来进行初步的衡量，但我们很有可能会发现，不管什么模型，准确率都在 95% 以上。仔细查看数据我们会发现，由于现在电商点击率、视频点击率往往都在 1%-10% 之间。也就是说，90% 以上都是负样本，因此准确率这个指标就不能够精确地反应模型的效果了。这时，我们就需要加入精确率和召回率指标进行更精确的衡量，比如我们采用了 Precision@20 和 Recall@20 这两个评估指标，但它终究只衡量了前 20 个结果的精确率和召回率。

如果我们要想看到更全面的指标，就要多看看 Precision@50 和 Recall@50，Precision@100 和 Recall@100，甚至逐渐过渡到 P-R 曲线。

总的来说，评估指标的选择不是唯一的，而是一个动态深入，跟你评测的“深度”紧密相关的过程。而且，在真正的离线实验中，虽然我们要通过不同角度评估模型，但也没必要陷入“完美主义”和“实验室思维”的误区，选择过多指标评估模型，更没有必要为了专门优化某个指标浪费过多时间。

离线评估的目的在于快速定位问题，快速排除不可行的思路，为线上评估找到“靠谱”的候选者。因此，我们根据业务场景选择 2~4 个有代表性的离线指标，进行高效率的离线实验才是离线评估正确的“打开方式”。

小结

这节课，我们重点介绍了模型离线评估中使用的评估指标。我把它们分成了两部分，简单直接的低阶评估指标，还有复杂全面的高阶评估指标。

低阶评估指标主要包括准确率，精确率，召回率和均方根误差。准确率是指分类正确的样本占总样本个数的比例，精确率指的是分类正确的正样本个数占分类器判定为正样本个数的比例，召回率是分类正确的正样本个数占真正的正样本个数的比例，而均方根误差的定义是预测值跟真实值之间差值的均方根。

高阶指标包括 P-R 曲线，ROC 曲线和平均精度均值。P-R 曲线的横坐标是召回率，纵坐标是精确率；ROC 曲线的横坐标是假阳性率，纵坐标是真阳性率。P-R 曲线和 ROC 曲线的绘制都不容易，我希望你能多看几遍我在课程中讲的例子，巩固一下。最后是平均精度均值 mAP，这个指标是对每个用户的精确率均值的再次平均。

最后，为了方便你记忆和对比，我也把所有指标的概念都总结在了文稿的表格里，你可以去看看。

课后问题

对于我们今天学到的 P-R 曲线和 ROC 曲线，你觉得它们的优缺点分别是什么呢？在正负样本分布极不均衡的情况下，你觉得哪个曲线的表现会更稳定、更权威一点？

期待在留言区看到你对这节课的思考，我们下节课见！

26 | 在线测试：如何在推荐服务器内部实现A/B测试？

你好，我是王喆。这节课我们来聊一聊推荐系统的线上 A/B 测试。

上两节课，我们进行了推荐系统离线评估方法和指标的学习。但是无论采用哪种方法，离线评估终究无法还原线上的所有变量。比如说，视频网站最想要提高的指标是用户观看时长，在离线评估的环境下可以模拟出这个指标吗？显然是非常困难的。即使能够在离线环境下生成这样一个指标，它是否能真实客观地反映线上效果，这也要打一个问号。

所以，对于几乎所有的互联网公司来说，线上 A/B 测试都是验证新模型、新功能、新产品是否能够提升效果的主要测试方法。这节课，我们就来讲一讲线上 A/B 测试，希望通过今天的课程，能帮助你了解到 A/B 测试的基本原理，A/B 测试的分层和分桶方法，以及怎么在 SparrowRecSys 的推荐服务器中实现 A/B 测试模块。

如何理解 A/B 测试？

A/B 测试又被称为“分流测试”或“分桶测试”，它通过把被测对象随机分成 A、B 两组，分别对它们进行对照测试的方法得出实验结论。具体到推荐模型测试的场景下，它的流程是这样的：先将用户随机分成实验组和对照组，然后给实验组的用户施以新模型，给对照组的用户施以旧模型，再经过一定时间的测试后，计算出实验组和对照组各项线上评估指标，来比较新旧模型的效果差异，最后挑选出效果更好的推荐模型。

好了，现在我们知道了什么是线上 A/B 测试。那它到底有什么优势，让几乎所有的互联网公司主要使用它来确定模型最终的效果呢？你有想过这是什么原因吗？我总结了一下，主要有三点原因。接下来，我们就一起来聊聊。

首先，离线评估无法完全还原线上的工程环境。 一般来讲，离线评估往往不考虑线上环境的延迟、数据丢失、标签数据缺失等情况，或者说很难还原线上环境的这些细节。因此，离线评估环境只能说是理想状态下的工程环境，得出的评估结果存在一定的失真现象。

其次，线上系统的某些商业指标在离线评估中无法计算。 离线评估一般是针对模型本身进行评估的，无法直接获得与模型相关的其他指标，特别是商业指标。像我们上节课讲的，离线评估关注的往往是 ROC 曲线、PR 曲线的改进，而线上评估却可以全面了解推荐模型带来的用户点击率、留存时长、PV 访问量这些指标的变化。

其实，这些指标才是最重要的商业指标，跟公司要达成的商业目标紧密相关，而它们都要由 A/B 测试进行更全面准确的评估。

最后是离线评估无法完全消除数据有偏（Data Bias）现象的影响。 什么叫“数据有偏”呢？因为离线数据都是系统利用当前算法生成的数据，因此这些数据本身就不是完全客观中立的，它是用户在当前模型下的反馈。所以说，用户本身有可能已经被当前的模型“带跑偏了”，你再用这些有偏的数据来衡量你的新模型，得到的结果就可能不客观。

正是因为离线评估存在这三点硬伤，所以我们必须利用线上 A/B 测试来确定模型的最终效果。明确了这一点，是不是让我们的学习更有方向了？接下来，我们再深入去学习一下 A/B 测试的核心原则和评估指标。

A/B 测试的“分桶”和“分层”原则

刚才，我们说 A/B 测试的原理就是把用户分桶后进行对照测试。这听上去好像没什么难的，但其实我们要考虑的细节还有很多，比如到底怎样才能对用户进行一个公平公正的分桶呢？如果有多组实验在同时做 A/B 测试，怎样做才能让它们互不干扰？

下面，我就来详细的讲一讲 A/B 测试的“分桶”和“分层”的原则，告诉你让 A/B 测试公平且高效的执行方法长什么样。

在 A/B 测试分桶的过程中，我们需要注意的是样本的独立性和分桶过程的无偏性。这里的“独立性”指的是同一个用户在测试的全程只能被分到同一个桶中。“无偏性”指的是在分桶过程中用户被分到哪个实验桶中应该是一个纯随机的过程。

举个简单的例子，我们把用户 ID 是奇数的用户分到对照组，把用户 ID 是偶数的用户分到实验组，这个策略只有在用户 ID 完全是随机生成的前提下才能说是无偏的，如果用户 ID 的奇偶分布不均，我们就无法保证分桶过程的无偏性。所以在实践的时候，我们经常会使用一些比较复杂的 Hash 函数，让用户 ID 尽量随机地映射到不同的桶中。

说完了分桶，那什么是分层呢？要知道，在实际的 A/B 测试场景下，同一个网站或应用往往要同时进行多组不同类型的 A/B 测试。比如，前端组正在进行不同 App 界面的 A/B 测试的时候，后端组也在进行不同中间件效率的 A/B 测试，同时算法组还在进行推荐场景 1 和推荐场景 2 的 A/B 测试。这个时候问题就来了，这么多 A/B 测试同时进行，我们怎么才能让它们互相不干扰呢？

你可能会说，这还不简单，我们全都并行地做这些实验，用户都不重叠不就行了。这样做当然可以，但非常低效。你如果在工作中进行过 A/B 测试的话肯定会知道，线上测试资源是非常紧张的，如果不进行合理的设计，很快所有流量资源都会被 A/B 测试占满。

为了解决这个问题，我们就要用到 A/B 测试的分层原则了。Google 在一篇关于实验测试平台的论文《Overlapping Experiment Infrastructure: More, Better, Faster Experimentation》中，详细介绍了 A/B 测试分层以及层内分桶的原则。

如果你没看过这篇论文，没有关系，你记住我总结出来的这句话就够了：层与层之间的流量“正交”，同层之间的流量“互斥”。它是什么意思呢？接下来，我就针对这句话做个详细的解释。

首先，我们来看层与层之间的流量“正交”，它指的是层与层之间的独立实验的流量是正交的，一批实验用的流量穿越每层实验时，都会再次随机打散，然后再用于下一层的实验。

这么说好像还是太抽象，我们来看下面的示意图。假设，在一个 X 层的实验中，流量被随机平均分为 X1（蓝色）和 X2（白色）两部分。当它们穿越到 Y 层的实验之后，X1 和 X2 的流量会被随机且均匀地分配给 Y 层的两个桶 Y1 和 Y2。

如果 Y1 和 Y2 的 X 层流量分配不均匀，那么 Y 层的样本就是有偏的，Y 层的实验结果就会被 X 层的实验影响，也就无法客观地反映 Y 层实验组和对照组变量的影响。

层与层之间流量正交示例

理解了第一句话，我们再来看看什么叫同层之间的流量“互斥”。这里的“互斥”具体有 2 层含义：

如果同层之间进行多组 A/B 测试，不同测试之间的流量不可以重叠，这是第一个“互斥”；

一组 A/B 测试中实验组和对照组的流量是不重叠的，这是第二个“互斥”。

在基于用户的 A/B 测试中，“互斥”的含义可以被进一步解读为，不同实验之间以及 A/B 测试的实验组和对照组之间的用户是不重叠的。特别是对推荐系统来说，用户体验的一致性是非常重要的。也就是说我们不可以让同一个用户在不同的实验组之间来回“跳跃”，这样会严重损害用户的实际体验，也会让不同组的实验结果相互影响。因此在 A/B 测试中，保证同一用户始终分配到同一个组是非常有必要的。

A/B 测试的“正交”与“互斥”原则共同保证了 A/B 测试指标的客观性，而且由于分层的存在，也让功能无关的 A/B 测试可以在不同的层上执行，充分利用了流量资源。

在清楚了 A/B 测试的方法之后，我们要解决的下一个问题就是，怎么选取线上 A/B 测试的指标。

线上 A/B 测试的评估指标

一般来说，A/B 测试是模型上线前的最后一道测试，通过 A/B 测试检验的模型会直接服务于线上用户，来完成公司的商业目标。因此，A/B 测试的指标应该与线上业务的核心指标保持一致。这就需要我们因地制宜地制定最合适的推荐指标了。

具体怎么做呢？实际也不难，那在实际的工作中，我们需要跟产品、运营团队多沟通，在测试开始之前一起制定大家都认可的评估指标。为了方便你参考，我在下表中列出了电商类推荐模型、新闻类推荐模型、视频类推荐模型的主要线上 A/B 测试评估指标，你可以看一看。

看了这些指标，我想你也发现了，线上 A/B 测试的指标和离线评估的指标（诸如 AUC、F1- score 等），它们之间的差异非常大。这主要是因为，离线评估不具备直接计算业务核心指标的条件，因此退而求其次，选择了偏向于技术评估的模型相关指标，但公司更关心的是能够驱动业务发展的核心指标，这也是 A/B 测试评估指标的选取原则。

总的来说，在具备线上环境条件时，利用 A/B 测试验证模型对业务核心指标的提升效果非常有必要。从这个意义上讲，线上 A/B 测试的作用是离线评估永远无法替代的。

SparrowRecSys 中 A/B 测试的实现方法

搞清楚了 A/B 测试的主要方法，下一步就让我们一起在 SparrowRecSys 上实现一个 A/B 测试模块，彻底掌握它吧！

既然是线上测试，那我们肯定需要在推荐服务器内部来实现这个 A/B 测试的模块。模块的基本框架不难实现，就是针对不同的 userId，随机分配给不同的实验桶，每个桶对应着不同的实验设置。

比较方便的是，我们可以直接在上一篇刚实现过的“猜你喜欢”功能上进行实验。实验组的设置是算法 NerualCF，对照组的设置是 Item2vec Embedding 算法。接下来，我们说一下详细的实现步骤。

首先，我们在 SparrowRecSys 里面建立了一个 ABTest 模块，它负责为每个用户分配实验设置。其中，A 组使用的模型 bucketAModel 是 emb，代表着 Item2vec Embedding 算法，B 组使用的模型 bucketBModel 是 Nerualcf。除此之外，我们还给不在 A/B 测试的用户设置了默认模型 emb，默认模型是不在实验范围内的用户的设置。

模型设置完，就到了分配实验组的阶段。这里，我们使用 getConfigByUserId 函数来确定用户所在的实验组。具体怎么做呢？因为这个函数只接收 userId 作为唯一的输入参数，所以我们利用 userId 的 hashCode 把数值型的 ID 打散，然后利用 userId 的 hashCode 和 trafficSplitNumber 这个参数进行取余数的操作，根据余数的值来确定 userId 在哪一个实验组里。

你可能对 trafficSplitNumber 这个参数的作用还不熟悉，我来进一步解释一下。这个参数的含义是把我们的全部用户分成几份。这里，我们把所有用户分成了 5 份，让第 1 份用户参与 A 组实验，第 2 份用户参与 B 组实验，其余用户继续使用系统的默认设置。这样的操作就是分流操作，也就是把流量划分之后，选取一部分参与 A/B 测试。

public class ABTest {
    final static int trafficSplitNumber = 5;
    final static String bucketAModel = "emb";
    final static String bucketBModel = "nerualcf";
    final static String defaultModel = "emb";
    public static String getConfigByUserId(String userId){
        if (null == userId || userId.isEmpty()){
            return defaultModel;
        }
        if(userId.hashCode() % trafficSplitNumber == 0){
            System.out.println(userId + " is in bucketA.");
            return bucketAModel;
        }else if(userId.hashCode() % trafficSplitNumber == 1){
            System.out.println(userId + " is in bucketB.");
            return bucketBModel;
        }else{
            System.out.println(userId + " isn't in AB test.");
            return defaultModel;
        }
    }
}

上面是 A/B 测试模块的主要实现。在实际要进行 A/B 测试的业务逻辑中，我们需要调用 A/B 测试模块来获得正确的实验设置。比如，我们这次选用了猜你喜欢这个功能进行 A/B 测试，就需要在相应的实现 RecForYoService 类中添加 A/B 测试的代码，具体的实现如下：

if (Config.IS_ENABLE_AB_TEST){
    model = ABTest.getConfigByUserId(userId);
}
//a simple method, just fetch all the movie in the genre
List<Movie> movies = RecForYouProcess.getRecList(Integer.parseInt(userId), Integer.parseInt(size), model);

我们可以看到，这里的实现非常简单，就是调用 ABTest.getConfigByUserId 函数获取用户对应的实验设置，然后把得到的参数 model 传入后续的业务逻辑代码。需要注意的是，我设置了一个全局的 A/B 测试使能标识 Config.IS_ENABLE_AB_TEST，你在测试这部分代码的时候，要把这个使能标识改为 true。

上面就是经典的 A/B 测试核心代码的实现。在实际的应用中，A/B 测试的实现当然要更复杂一些。比如，不同实验的设置往往是存储在数据库中的，需要我们从数据库中拿到它。再比如，为了保证分组时的随机性，我们往往会创建一些复杂的 hashCode 函数，保证能够均匀地把用户分到不同的实验桶中。但整个 A/B 测试的核心逻辑没有变化，你完全可以参考我们今天的实现过程。

小结

这节课，我们讲解了线上 A/B 测试的基本原理和评估指标，并且在 SparrowRecsys 上实现了 A/B 测试的模块。我带你从 A/B 测试的定义和优势、设计原则以及在线评估指标这三个方面回顾一下。

A/B 测试又叫“分流测试”或“分桶测试”，它把被测对象随机分成 A、B 两组，通过对照测试的方法得出实验结论。相比于离线评估，A/B 测试有三个优势：

实验环境就是线上的真实生产环境；

可以直接得到线上的商业指标；

不受离线数据“数据有偏”现象的影响。

在 A/B 测试的设计过程中，我们要遵循层与层之间的流量“正交”，同层之间的流量“互斥”这一设计原则，这样才能既正确又高效地同时完成多组 A/B 测试。除此之外，在线上评估指标的制定过程中，我们要尽量保证这些指标与线上业务的核心指标保持一致，这样才能更加准确地衡量模型的改进，有没有帮助到公司的业务发展，是否达成了公司的商业目标。

为了方便你复习，我把一些核心的知识点总结在了表格中，你可以看一看。

课后思考

今天讲的 A/B 测试的分层和分桶的原则你都理解了吗？如果我们在测试模型的时候，一个实验是在首页测试新的推荐模型，另一个实验是在内容页测试新的推荐模型，你觉得这两个实验应该放在同一层，还是可以放在不同的层呢？为什么？

期待在留言区看到你的思考，如果有其他疑问也欢迎你随时提出来，我会一一解答，我们下节课见！

27 | 评估体系：如何解决A/B测试资源紧张的窘境？

你好，我是王喆。

我们在进行推荐系统评估时经常会遇到两类问题。

一类是在做线上 A/B 测试的时候，流量经常不够用，要排队等别人先做完测试之后才能进行自己的测试。线上 A/B 测试资源紧张的窘境，会大大拖慢我们试验的新思路，以及迭代优化模型的进度。

另一类是，离线评估加上在线评估有那么多种测试方法，在实际工作中，我们到底应该选择哪一种用来测试，还是都要覆盖到呢？

其实，这两个问题的答案是有深刻联系的，并不是孤立的。我认为最好的解决办法就是，建立起一套推荐系统的评估体系，用它来解决不同评估方法的配合问题，以及线上 A/B 测试资源紧张的问题。这节课，我就带你一起来厘清如何建立起一整套推荐系统评估体系。

什么是推荐系统的评估体系？

首先，什么是评估体系呢？我先给它下一个定义，推荐系统的评估体系指的是，由多种不同的评估方式组成的、兼顾效率和正确性的，一套用于评估推荐系统的解决方案。一个成熟的推荐系统评估体系应该综合考虑评估效率和正确性，可以利用很少的资源，快速地筛选出效果更好的模型。

那对一个商业公司来说，最公正也是最合理的评估方法就是进行线上测试，来评估模型是否能够更好地达成公司或者团队的商业目标。但是，正如我们开头所说，线上 A/B 测试要占用宝贵的线上流量资源，这些有限的线上测试机会远远不能满足算法工程师改进模型的需求。所以如何有效地把线上和离线测试结合起来，提高测试的效率，就是我们迫切的需求。

那我们该怎么去构建起一整套评估体系呢？图 1 就是一个典型的评估体系示意图。从图中我们可以看到，处于最底层的是传统的离线评估方法，比如 Holdout 检验、交叉检验等，往上是离线 Replay 评估方法，再往上是一种叫 Interleaving 的线上测试方法，我们等会还会详细介绍，最后是线上 A/B 测试。

图1 推荐系统的评测体系
（出自《深度学习推荐系统》）

这四层结构共同构成完整的评估体系，做到了评估效率和评估正确性之间的平衡，越是底层的方法就会承担越多筛选掉改进思路的任务，这时候“评估效率”就成了更关键的考虑因素，那对于“正确性”的评估，我们反而没有多么苛刻的要求了。

总的来说，离线评估由于有着更多可供利用的计算资源，可以更高效、快速地筛选掉那些“不靠谱”的模型来改进思路，所以被放在了第一层的位置。

随着候选模型被一层层筛选出来，越接近正式上线的阶段，评估方法对评估“正确性”的要求就越严格。因此，在模型正式上线前，我们应该以最接近真实产品体验的 A/B 测试，来做最后的模型评估，产生最具说服力的在线指标之后，才能够进行最终的模型上线，完成模型改进的迭代过程。

讲了这么多，你可能会觉得，道理没问题，但工作中真的是这样吗？不如，我们来看个例子。下图就是一个很形象的工作中的模型筛选过程。

假设，现在有 30 个待筛选的模型，如果所有模型都直接进入线上 A/B 测试的阶段进行测试，所需的测试样本是海量的，由于线上流量有限，测试的时间会非常长。但如果我们把测试分成两个阶段，第一个阶段先进行初筛，把 30 个模型筛选出可能胜出的 5 个，再只对这 5 个模型做线上 A/B 测试，所需的测试流量规模和测试时间长度都会大大减少。这里的初筛方法，就是我们在评估体系中提到的离线评估、离线 Replay 和在线 Interleaving 等方法。

图2 模型的筛选过程
（图片出自The Netflix Tech Blog）

到这里，我想你已经清楚了什么是推荐系统的评估体系，以及评估体系是有哪些方法组成的。但在这些组成方法中，我们还有两点要重点注意：一个是离线 Replay 这个方法，虽然我们之前讲过离线 Replay 的原理，但是对于它的相关工程架构还没有讲过；第二个是上面提到过的线上 Interleaving 方法。 下面，我就借着流媒体巨头 Netflix 的实践方案，来讲解一下离线 Replay 和在线 Interleaving 的细节。

Netflix 的 Replay 评估方法实践

借着下图 3，我们来回顾一下，第 24 课学过的离线 Replay 方法的原理：离线 Replay 通过动态的改变测试时间点，来模拟模型的在线更新过程，让测试过程更接近真实线上环境。

图3 静态时间分割评估与动态Replay评估
（出自《深度学习推荐系统》）

但是在 Replay 方法的实现过程中，存在一个很棘手的工程问题，就是我们总提到的“未来信息”问题，或者叫做“特征穿越”问题。因此在 Replay 过程中，每次模型更新的时候，我们都需要用历史上“彼时彼刻”的特征进行训练，否则训练和评估的结果肯定是不准确的。

我来举个例子，假设 Replay 方法要使用 8 月 1 日到 8 月 31 日的样本数据进行重放，这些样本中包含一个特征，叫做“历史 CTR”，这个特征只能通过历史数据来计算生成。

比如说，8 月 20 日的样本就只能够使用 8 月 1 日到 8 月 19 日的数据来生成“历史 CTR”这个特征，绝不能使用 8 月 20 日以后的数据来生成这个特征。在评估过程中，如果我们为了工程上的方便，使用了 8 月 1 日到 8 月 31 日所有的样本数据生成这个特征，供所有样本使用，之后再使用 Replay 的方法进行评估，那我们得到的结论必然是错误的。

那么问题来了，在工程上，为了方便按照 Replay 方法进行模型评估，我们应该怎么去建立一套数据处理的架构，支持这种历史特征的复现呢？接下来，我们就看一看 Netflix 是怎么解决这个问题的。

Netflix 为了进行离线 Replay 的实验，建立了一整套从数据生成到数据处理再到数据存储的数据处理架构，并给它起了一个很漂亮的名字，叫做时光机（Time Machine）。

下图 4 就是时光机的架构，图中最主要的就是 Snapshot Jobs（数据快照）模块。它是一个每天执行的 Spark 程序，它做的主要任务就是把当天的各类日志、特征、数据整合起来，形成当天的、供模型训练和评估使用的样本数据。它还会以日期为目录名称，将样本快照数据保存在分布式文件系统 S3 中（Snapshots），再对外统一提供 API（Batch APIs），供其他模型在训练和评估的时候按照时间范围方便地获取。

图4 Netflix的离线评估数据流架构——时光机
（出自The Netflix Tech Blog ）

这个 Snapshot Jobs 主任务的源数据是从哪来的呢？你可以重点关注它上方的 Context Set 模块和左边的 Prana 模块。接下来，我再详细和你说说这两个模块的任务。

Context Set 模块负责保存所有的历史当天的环境信息。 环境信息主要包括两类：一类是存储在 Hive 中的场景信息，比如用户的资料、设备信息、物品信息等数据；另一类是每天都会发生改变的一些统计类信息，包括物品的曝光量、点击量、播放时长等信息。

Prana 模块负责处理每天的系统日志流。 系统日志流指的是系统实时产生的日志，它包括用户的观看历史（Viewing History）、用户的推荐列表（My List）和用户的评价（Ratings）等。这些日志从各自的服务（Service）中产生，由 Netflix 的统一数据接口 Prana 对外提供服务。

因此，Snapshot Jobs 这个核心模块每天的任务就是，通过 Context Set 获取场景信息，通过 Prana 获取日志信息，再经过整合处理、生成特征之后，保存当天的数据快照到 S3。

在生成每天的数据快照后，使用 Replay 方法进行离线评估就不再是一件困难的事情了，因为我们没有必要在 Replay 过程中进行烦琐的特征计算，直接使用当天的数据快照就可以了。

在时光机这个架构之上，使用某个时间段的样本进行一次 Replay 评估，就相当于直接穿越到了彼时彼刻，用当时的日志和特征进行模型训练和评估，就像进行了一次时光旅行（Time Travel）一样。

Interleaving 评估方法是什么

讲完了离线 Replay 的工程实现方法，我们再来聊一聊什么是 Interleaving 在线评估方法。

那 Interleaving 评估方法提出的意义是什么呢？主要有两方面：首先，它是和 A/B 测试一样的在线评估方法，能够得到在线评估指标；其次，它提出的目的是为了比传统的 A/B 测试用更少的资源，更快的速度得到在线评估的结果。

清楚了 Interleaving 评估方法提出的意义，我们就可以更好地理解 Interleaving 方法的具体细节了。下面，我们对比 A/B 测试，来看看 Interleaving 方法的具体实现过程。

在传统的 A/B 测试中，我们会把用户随机分成两组。一组接受当前的推荐模型 A 的推荐结果，这一组被称为对照组 。另一组接受新的推荐模型 B 的推荐结果，这组被成为实验组。

在 Interleaving 方法中，不再需要两个不同组的用户，只需要一组用户，这些用户会收到模型 A 和模型 B 的混合结果。也就是说，用户会在一个推荐列表里同时看到模型 A 和模型 B 的推荐结果。在评估的过程中，Interleaving 方法通过分别累加模型 A 和模型 B 推荐物品的效果，来得到模型 A 和 B 最终的评估结果。

下图可以帮助我们更形象地对比 A/B 测试和 Interleaving 方法。

图5 传统A/B测试和Interleaving方法的比较
（出自The Netflix Tech Blog ）

那你可能想问了，在使用 Interleaving 方法进行测试的时候，我们该怎么保证对模型 A 和模型 B 的测试是公平的呢？如果有一个模型的结果总排在第一位，这对另一个模型不就不公平了吗？

这个问题很好，我们确实需要考虑推荐列表中位置偏差的问题，要想办法避免来自模型 A 或者模型 B 的物品总排在第一位。因此，我们需要以相等的概率让模型 A 和模型 B 产生的物品交替领先。这就像在野球场打球的时候，两个队长会先通过扔硬币的方式决定谁先选人，再交替来选择队员。

理解了原理，我们再结合下面的图示，来进一步理解 Interleaving 方法混合模型 A 和 B 结果的过程。和刚才说的野球场选人的过程一样，我们先选模型 A 或者模型 B 的排名第一的物品作为最终推荐列表的第一个物品，然后再交替选择，直到填满整个推荐列表。所以，最后得到的列表会是 ABABAB，或者 BABABA 这样的顺序，而且这两种形式出现的概率应该是相等的，这样才能保证两个模型的公平性。

图6 Interleaving方法中推荐列表的生成方法

最后，我们要清楚推荐列表中的物品到底是由模型 A 生成的，还是由模型 B 生成的，然后统计出所有模型 A 物品的综合效果，以及模型 B 物品的综合效果，然后进行对比。这样，模型评估过程就完成了。

总的来说，Interleaving 的方法由于不用进行用户分组，因此比传统 A/B 测试节约了一半的流量资源。但是 Interleaving 方法能彻底替代传统 A/B 测试吗？其实也不能，在测试一些用户级别而不是模型级别的在线指标时，我们就不能用 Interleaving 方法。

比如用户的留存率，用户从试用到付费的转化率等，由于 Interleaving 方法同时使用了对照模型和实验模型的结果，我们就不清楚到底是哪个模型对这些结果产生了贡献。但是在测试 CTR、播放量、播放时长这些指标时，Interleaving 就可以通过累加物品效果得到它们。这个时候，它就能很好地替代传统的 A/B 测试了。

到这里，我们就形成了一个完整、高效且准确的评估系统。希望你能从整体的角度重新审视一遍这个体系中的每个方法，如果有不清楚的，再好好回顾一下我讲的知识点。

小结

这节课，我们利用之前讲过的知识，总结出了推荐系统的评估体系。这个评估体系由传统离线评估、离线 Replay、线上 Interleaving，以及线上 A/B 测试四个层级组成。这四个层级由下到上评估效率逐渐降低，但是评估的准确性逐渐升高，它们共同组成一个能够高效筛选候选模型的评估体系。

针对这个评估体系中的两个要点，离线 Replay 实践和 Interleaving 方法，我们又深入学习了它们的工程架构和实现细节。

其中，离线 Replay 借鉴了 Netflix 时光机的经验，这个时光机的数据流体系通过融合日志流和场景信息数据，生成天级别的数据快照，并对外提供统一的 API，供模型训练和评估使用，使用时就像做了一次时光旅行。

对于 Interleaving 方法，我们应该清楚它实现的三个要点：

它不进行用户分组；

它的实验推荐列表是通过间隔地选择模型 A 和模型 B 的推荐物品得到的；

为了保证它的公平性，我们要从模型 A 或者模型 B 中随机选择第一个物品，就像野球场选人一样完成推荐列表的生成。

还是老习惯，我把这节课的重要知识点总结在了下面的表格里，方便你及时回顾。

这节课也是我们模型评估篇的最后一节课，希望通过整个模型评估篇的学习，你不仅能够熟悉起每一种评估方法，而且能够清楚它们之间的区别和联系，形成一个高效的评估体系。相信它会加快你模型迭代的速度，对你的实际工作产生非常积极的影响！

课后思考

在 Interleaving 方法中，推荐列表是由模型 A 和模型 B 的结果共同组成的，那如果模型 A 和模型 B 的结果中有重叠怎么办？是保留模型 A 的结果还是模型 B 的结果呢？你有什么好的想法吗？

今天讲的评估体系，你知道怎么建立了吗？欢迎把你的思考和疑问写在留言区，不妨也把这节课分享给你的朋友们，我们下节课见！

特别加餐｜TensorFlow的模型离线评估实践怎么做？

你好，我是王喆。

上两节课，我们学习了离线评估的主要方法以及离线评估的主要指标。那这些方法和指标具体是怎么使用的，会遇到哪些问题呢？我们之前实现的深度学习模型的效果怎么样呢？

这节课，我们直接进入实战，在 TensorFlow 环境下评估一下我们之前实现过的深度学习模型。一方面这能帮助我们进一步加深对离线评估方法和指标的理解，另一方面，也能检验一下我们自己模型的效果。

训练集和测试集的生成

离线评估的第一步就是要生成训练集和测试集，在这次的评估实践中，我会选择最常用的 Holdout 检验的方式来划分训练集和测试集。划分的方法我们已经在第 23 课里用 Spark 实现过了，就是调用 Spark 中的 randomSplit 函数进行划分，具体的代码你可以参考 FeatureEngForRecModel 对象中的 splitAndSaveTrainingTestSamples 函数。

这里我们按照 8:2 的比例把全量样本集划分为训练集和测试集，再把它们分别存储在SparrowRecSys/src/main/resources/webroot/sampledata/trainingSamples.csv和SparrowRecSys/src/main/resources/webroot/sampledata/testSamples.csv路径中。

在 TensorFlow 内部，我们跟之前载入数据集的方式一样，调用get_dataset方法分别载入训练集和测试集就可以了。

TensorFlow 评估指标的设置

在载入训练集和测试集后，我们需要搞清楚如何在 TensorFlow 中设置评估指标，并通过这些指标观察模型在每一轮训练上的效果变化，以及最终在测试集上的表现。这个过程听起来还挺复杂，好在，TensorFlow 已经为我们提供了非常丰富的评估指标，这让我们可以在模型编译阶段设置 metrics 来指定想要使用的评估指标。

具体怎么做呢？我们一起来看看下面的代码，它是设置评估指标的一个典型过程。首先，我们在 model complie 阶段设置准确度（Accuracy）、ROC 曲线 AUC（tf.keras.metrics.AUC(curve='ROC')）、PR 曲线 AUC（tf.keras.metrics.AUC(curve='PR')），这三个在评估推荐模型时最常用的指标。

同时，在训练和评估过程中，模型还会默认产生损失函数 loss 这一指标。在模型编译时我们采用了 binary_crossentropy 作为损失函数，所以这里的 Loss 指标就是我们在上一节课介绍过的二分类问题的模型损失 Logloss。

在设置好评估指标后，模型在每轮 epoch 结束后都会输出这些评估指标的当前值。在最后的测试集评估阶段，我们可以调用 model.evaluate 函数来生成测试集上的评估指标。具体的实现代码，你可以参考 SparrowRecsys 项目中深度推荐模型相关的代码。

# compile the model, set loss function, optimizer and evaluation metrics
model.compile(
    loss='binary_crossentropy',
    optimizer='adam',
    metrics=['accuracy', tf.keras.metrics.AUC(curve='ROC'), tf.keras.metrics.AUC(curve='PR')])
# train the model
model.fit(train_dataset, epochs=5)
# evaluate the model
test_loss, test_accuracy, test_roc_auc, test_pr_auc = model.evaluate(test_dataset)

在执行这段代码的时候，它的输出是下面这样的。从中，我们可以清楚地看到每一轮训练的 Loss、Accuracy、ROC AUC、PR AUC 这四个指标的变化，以及最终在测试集上这四个指标的结果。

Epoch 1/5
8236/8236 [==============================] - 60s 7ms/step - loss: 3.0724 - accuracy: 0.5778 - auc: 0.5844 - auc_1: 0.6301
Epoch 2/5
8236/8236 [==============================] - 55s 7ms/step - loss: 0.6291 - accuracy: 0.6687 - auc: 0.7158 - auc_1: 0.7365
Epoch 3/5
8236/8236 [==============================] - 56s 7ms/step - loss: 0.5555 - accuracy: 0.7176 - auc: 0.7813 - auc_1: 0.8018
Epoch 4/5
8236/8236 [==============================] - 56s 7ms/step - loss: 0.5263 - accuracy: 0.7399 - auc: 0.8090 - auc_1: 0.8305
Epoch 5/5
8236/8236 [==============================] - 56s 7ms/step - loss: 0.5071 - accuracy: 0.7524 - auc: 0.8256 - auc_1: 0.8481

1000/1000 [==============================] - 5s 5ms/step - loss: 0.5198 - accuracy: 0.7427 - auc: 0.8138 - auc_1: 0.8430
Test Loss 0.5198314250707626, Test Accuracy 0.7426666617393494, Test ROC AUC 0.813848614692688, Test PR AUC 0.8429719805717468

总的来说，随着训练的进行，模型的 Loss 在降低，而 Accuracy、Roc AUC、Pr AUC 这几个指标都在升高，这证明模型的效果随着训练轮数的增加在逐渐变好。

最终，我们就得到了测试集上的评估指标。你会发现，测试集上的评估结果相比训练集有所下降，比如 Accuracy 从 0.7524 下降到了 0.7427，ROC AUC 从 0.8256 下降到了 0.8138。这是非常正常的现象，因为模型在训练集上都会存在着轻微过拟合的情况。

如果测试集的评估结果相比训练集出现大幅下降，比如下降幅度超过了 5%，就说明模型产生了非常严重的过拟合现象，我们就要反思一下是不是在模型设计过程中出现了一些问题，比如模型的结构对于这个问题来说过于复杂，模型的层数或者每层的神经元数量过多，或者我们要看一看是不是需要加入 Dropout，正则化项来减轻过拟合的风险。

除了观察模型自己的效果，在模型评估阶段，我们更应该重视不同模型之间的对比，这样才能确定我们最终上线的模型，下面我们就做一个模型效果的横向对比。

模型的效果对比

在推荐模型篇，我们已经实现了 EmbeddingMLP、NerualCF、Wide&Deep 以及 DeepFM 这四个深度学习模型，后来还有同学添加了 DIN 的模型实现。

那接下来，我们就利用这节课的模型评估方法，来尝试对比一下这几个模型的效果。首先，我直接把这些模型在测试集上的评估结果记录到了表格里，当然，我更建议你利用 SparrowRecsys 项目中的代码，自己来计算一下，多实践一下我们刚才说的模型评估方法。

通过上面的比较，我们可以看出，Embedding MLP 和 Wide&Deep 模型在我们的 MovieLens 这个小规模数据集上的效果最好，它们两个的指标也非常接近，只不过是在不同指标上有细微的差异，比如模型 Loss 指标上 Wide&Deep 模型好一点，在 Accuracy、ROC AUC、PR AUC 指标上 Embedding MLP 模型好一点。

遇到这种情况，我们该如何挑出更好的那个模型呢？一般我们会在两个方向上做尝试：一是做进一步的模型调参，特别是对于复杂一点的 Wide&Deep 模型，我们可以尝试通过参数的 Fine Tuning（微调）让模型达到更好的效果；二是如果经过多次尝试两个模型的效果仍比较接近，我们就通过线上评选出最后的胜出者。

说完了效果好的指标，不知道你有没有注意到一个反常的现象，那就是模型 DeepFM 的评估结果非常奇怪，怎么个奇怪法呢？理论上来说，DeepFM 的表达能力是最强的，可它现在展示出来的评估结果却最差。这种情况就极有可能是因为模型遇到了过拟合问题。为了验证个想法，我们再来看一下 DeepFM 在训练集上的表现，如下表所示：

我们很惊讶地发现，DeepFM 在测试集上的表现比训练集差了非常多。毫无疑问，这个模型过拟合了。当然，这里面也有我们数据的因素，因为我们采用了一个规模很小的采样过的 MovieLens 数据集，在训练复杂模型时，小数据集往往更难让模型收敛，并且由于训练不充分的原因，模型中很多参数其实没有达到稳定的状态，因此在测试集上的表现往往会呈现出比较大的随机性。

通过 DeepFM 模型效果对比的例子，也再一次印证了我们在“最优的模型结构该怎么找？”那节课的结论：推荐模型没有银弹，每一个业务，每一类数据，都有最合适的模型结构，并不是说最复杂的，最新的模型结构就是最好的模型结构，我们需要因地制宜地调整模型和参数，这才是算法工程师最大的价值所在。

小结

这节实践课，我们基于 TensorFlow 进行了深度推荐模型的评估，整个实践过程可以分成三步。

第一步是导入 Spark 分割好的训练集和测试集。

第二步是在 TensorFlow 中设置评估指标，再在测试集上调用 model.evaluate 函数计算这些评估指标。在实践过程中，我们要清楚有哪些 TensorFlow 的指标可以直接调用。那在这节课里，我们用到了最常用的 Loss、Accuracy、ROC AUC、PR AUC 四个指标。

第三步是根据四个深度推荐模型的评估结果，进行模型效果的对比。通过对比的结果我们发现 Embedding MLP 和 Wide&Deep 的效果是最好的。同时，我们也发现，本该表现很好的 DeepFM 模型，它的评估结果却比较差，原因是模型产生了非常严重的过拟合问题。

因此，在实际工作中，我们需要通过不断调整模型结构、模型参数，来找到最合适当前业务和数据集的模型。

课后思考

1. 除了这节课用到的 Loss、Accuracy、ROC AUC、PR AUC 这四个指标，你在 TensorFlow 的实践中还会经常用到哪些评估指标呢？ 你能把这些常用指标以及它们特点分享出来吗？（你可以参考 TensorFlow 的官方Metrics 文档 ）

2. 你认为 DeepFM 评估结果这么差的原因，除了过拟合，还有什么更深层次的原因呢？可以尝试从模型结构的原理上给出一些解释吗？

期待在留言区看到你对 DeepFM 模型的思考和使用评估指标的经验，我们下节课见！

28 | 业界经典：YouTube深度学习推荐系统的经典架构长什么样？

你好，我是王喆。今天我们一起来开启前沿拓展篇的学习。

如果你是跟着课程安排学到这里的，我可以很自信地说，你几乎已经掌握了推荐系统的全部重点知识。从数据特征的处理，模型构建，到模型的评估和上线，再到推荐服务器的实现，你的知识广度已经覆盖了推荐系统领域的全部要点。但要想成为一名合格的推荐工程师，我们还需要做两件事情，一件是追踪前沿，另一件是融会贯通。

因此，在这一篇中，我会通过详细讲解几个一线大厂的推荐系统解决方案，来帮你追踪行业的热点、创新点。它们既包括一些推荐模型的业界实现，如 YouTube 和 Pinterest 的推荐模型，也包括推荐系统的工程落地方案，如 Flink 的经典应用和美团对于强化学习的落地方案。最后，我还会对算法工程师的所需能力做一个全面的总结。

今天，我们今天先来学习 YouTube 的经典深度学习推荐系统架构。YouTube 这套深度学习解决方案，已经经典到可以成为一个业界标杆式的方案了，也是我在国内外和同学、同事们交流、讨论的时候经常会提到的方案。

话不多说，我们正式开始今天的学习吧！

YouTube 推荐系统架构

提起 YouTube，我想你肯定不会陌生，作为全球最大的视频分享网站，YouTube 平台中几乎所有的视频都来自 UGC（User Generated Content，用户原创内容），这样的内容产生模式有两个特点：

一是其商业模式不同于 Netflix，以及国内的腾讯视频、爱奇艺这样的流媒体，这些流媒体的大部分内容都是采购或自制的电影、剧集等头部内容，而 YouTube 的内容都是用户上传的自制视频，种类风格繁多，头部效应没那么明显；

二是由于 YouTube 的视频基数巨大，用户难以发现喜欢的内容。

这样的内容特点简直就是深度学习推荐系统最适合扎根的土壤，所以 YouTube 也是最早落地深度学习的一线公司。那 YouTube 的深度学习推荐系统架构长什么样呢？

图1 YouTube推荐系统整体架构

上图就是 YouTube 在 2016 年发布的推荐系统架构。我们可以看到，为了对海量的视频进行快速、准确的排序，YouTube 也采用了经典的召回层 + 排序层的推荐系统架构。

它的推荐过程可以分成二级。第一级是用候选集生成模型（Candidate Generation Model）完成候选视频的快速筛选，在这一步，候选视频集合由百万降低到几百量级，这就相当于经典推荐系统架构中的召回层。第二级是用排序模型（Ranking Model）完成几百个候选视频的精排，这相当于经典推荐系统架构中的排序层。

无论是候选集生成模型还是排序模型，YouTube 都采用了深度学习的解决方案。下面，就让我们详细讲讲这两个深度学习模型是如何构建起来的。

候选集生成模型

首先，是用于视频召回的候选集生成模型，它的模型架构如下图所示。

图2 YouTube候选集生成模型架构

我们一起自下而上地好好看一看这个模型的结构。

最底层是它的输入层，输入的特征包括用户历史观看视频的 Embedding 向量，以及搜索词的 Embedding 向量。对于这些 Embedding 特征，YouTube 是利用用户的观看序列和搜索序列，采用了类似 Item2vec 的预训练方式生成的。

当然，我们也完全可以采用 Embedding 跟模型在一起 End2End 训练的方式来训练模型。至于预训练和 End2End 训练这两种方式孰优孰劣，我们也探讨过很多次了，你可以自己再深入思考一下。

除了视频和搜索词 Embedding 向量，特征向量中还包括用户的地理位置 Embedding、年龄、性别等特征。这里我们需要注意的是，对于样本年龄这个特征，YouTube 不仅使用了原始特征值，还把经过平方处理的特征值也作为一个新的特征输入模型。

这个操作其实是为了挖掘特征非线性的特性，当然，这种对连续型特征的处理方式不仅限于平方，其他诸如开方、Log、指数等操作都可以用于挖掘特征的非线性特性。具体使用哪个，需要我们根据实际的效果而定。

确定好了特征，跟我们之前实践过的深度学习模型一样，这些特征会在 concat 层中连接起来，输入到上层的 ReLU 神经网络进行训练。

三层 ReLU 神经网络过后，YouTube 又使用了 softmax 函数作为输出层。值得一提的是，这里的输出层不是要预测用户会不会点击这个视频，而是要预测用户会点击哪个视频，这就跟我们之前实现过的深度推荐模型不一样了。

比如说，YouTube 上有 100 万个视频，因为输出层要预测用户会点击哪个视频，所以这里的 sofmax 就有 100 万个输出。因此，这个候选集生成模型的最终输出，就是一个在所有候选视频上的概率分布。为什么要这么做呢？它其实是为了更好、更快地进行线上服务，这一点我们等会再详细讲。

总的来讲，YouTube 推荐系统的候选集生成模型，是一个标准的利用了 Embedding 预训练特征的深度推荐模型，它遵循我们之前实现的 Embedding MLP 模型的架构，只是在最后的输出层有所区别。

候选集生成模型独特的线上服务方法

好，现在我们就详细说一说，为什么候选集生成模型要用“视频 ID”这个标签，来代替“用户会不会点击视频”这个标签作为预测目标。事实上，这跟候选集生成模型独特的线上服务方式紧密相关。

图3 模型服务部分示意图

细心的同学可能已经留意到，架构图左上角的模型服务（Serving）方法与模型训练方法完全不同。在候选集生成模型的线上服务过程中，YouTube 并没有直接采用训练时的模型进行预测，而是采用了一种最近邻搜索的方法，我们曾经在第 12 课详细讲过基于 Embedding 的最近邻搜索方法，不记得的同学可以先去回顾一下。

具体来说，在模型服务过程中，网络结构比较复杂，如果我们对每次推荐请求都端到端地运行一遍模型，处理一遍候选集，那模型的参数数量就会巨大，整个推断过程的开销也会非常大。

因此，在通过“候选集生成模型”得到用户和视频的 Embedding 后，我们再通过 Embedding 最近邻搜索的方法，就可以提高模型服务的效率了。这样一来，我们甚至不用把模型推断的逻辑搬上服务器，只需要将用户 Embedding 和视频 Embedding 存到特征数据库就行了。再加上可以使用局部敏感哈希这类快速 Embedding 查找方法，这对于百万量级规模的候选集生成过程的效率提升是巨大的。

那么问题又来了，这里的用户 Embedding 和视频 Embedding 到底是从哪里来的呢？这个问题的答案就是，候选集生成模型为什么要用视频 ID 作为多分类输出的答案了。我们再仔细看一下图 2 的架构，架构图中从 softmax 向模型服务模块画了个箭头，用于代表视频 Embedding 向量的生成。

由于最后的输出层是 softmax，而这个 softmax 层的参数本质上就是一个 m x n 维的矩阵，其中 m 指的是最后一层红色的 ReLU 层的维度 m，n 指的是分类的总数，也就是 YouTube 所有视频的总数 n。因此，视频 Embedding 就是这个 m x n 维矩阵的各列向量。

这样的 Embedding 生成方法其实和 word2vec 中词向量的生成方法是相同的，你也可以参考第 6 节课的内容来理解它。

清楚了视频 Embedding 的生成原理，用户 Embedding 的生成就非常好理解了，因为输入的特征向量全部都是用户相关的特征，一个物品和场景特征都没有，所以在使用某用户 u 的特征向量作为模型输入时，最后一层 ReLU 层的输出向量就可以当作该用户 u 的 Embedding 向量。

在模型训练完成后，逐个输入所有用户的特征向量到模型中，YouTube 就可以得到所有用户的 Embedding 向量，之后就可以把它们预存到线上的特征数据库中了。

在预测某用户的视频候选集时，YouTube 要先从特征数据库中拿到该用户的 Embedding 向量，再在视频 Embedding 向量空间中，利用局部敏感哈希等方法搜索该用户 Embedding 向量的 K 近邻，这样就可以快速得到 k 个候选视频集合。这就是整个候选集生成模型的训练原理和服务过程。

到这里，你一定已经体会到了咱们前沿拓展篇案例分析的作用，通过一个 YouTube 候选集生成模型的原理分析，我们就已经把第 6 课的 Embedding、第 10 课的特征数据库、第 12 课的局部敏感哈希，以及第 17 课的 Embedding MLP 模型都回顾了一遍。

如果你喜欢这种通过学习业界实践方案，把知识串联起来的方式，可以给我留言反馈，我也会在之后的课程中多采用这样的方式。

排序模型

通过候选集生成模型，YouTube 已经得到了几百个候选视频的集合了，下一步就是利用排序模型进行精排序。下图就是 YouTube 深度学习排序模型的架构，我们一起来看一看。

图3 YouTube的深度学习排序模型的架构

第一眼看上去，你可能会认为排序模型的网络结构与候选集生成模型没有太大区别，在模型结构上确实是这样的，它们都遵循 Embedding MLP 的模型架构。但是我们来看其中的细节，特别是输入层和输出层的部分，它们跟候选集生成模型还是有很大不同的，这就是我们要重点关注的。

我们先看输入层，相比于候选集生成模型需要对几百万候选集进行粗筛，排序模型只需对几百个候选视频进行排序，因此可以引入更多特征进行精排。具体来说，YouTube 的输入层从左至右引入的特征依次是：

impression video ID embedding：当前候选视频的 Embedding；

watched video IDs average embedding：用户观看过的最后 N 个视频 Embedding 的平均值；

language embedding：用户语言的 Embedding 和当前候选视频语言的 Embedding；

time since last watch：表示用户上次观看同频道视频距今的时间；

#previous impressions：该视频已经被曝光给该用户的次数；

上面 5 个特征中，前 3 个 Embedding 特征的含义很好理解，我就不细说了。第 4 个特征和第 5 个特征，因为很好地引入了 YouTube 工程师对用户行为的观察，所以我来重点解释一下。

第 4 个特征 time since last watch 说的是用户观看同类视频的间隔时间。如果从用户的角度出发，假如某用户刚看过“DOTA 比赛经典回顾”这个频道的视频，那他很大概率会继续看这个频道的其他视频，该特征就可以很好地捕捉到这一用户行为。

第 5 个特征 #previous impressions 说的是这个视频已经曝光给用户的次数。我们试想如果一个视频已经曝光给了用户 10 次，用户都没有点击，那我们就应该清楚，用户对这个视频很可能不感兴趣。所以 #previous impressions 这个特征的引入就可以很好地捕捉到用户这样的行为习惯，避免让同一个视频对同一用户进行持续的无效曝光，尽量增加用户看到新视频的可能性。

把这 5 类特征连接起来之后，需要再经过三层 ReLU 网络进行充分的特征交叉，然后就到了输出层。这里我们要重点注意，排序模型的输出层与候选集生成模型又有所不同。不同主要有两点：一是候选集生成模型选择了 softmax 作为其输出层，而排序模型选择了 weighted logistic regression（加权逻辑回归）作为模型输出层；二是候选集生成模型预测的是用户会点击“哪个视频”，排序模型预测的是用户“要不要点击当前视频”。

那么问题来了，YouTube 为什么要这么做呢？

其实，排序模型采用不同输出层的根本原因就在于，YouTube 想要更精确地预测用户的观看时长，因为观看时长才是 YouTube 最看中的商业指标，而使用 Weighted LR 作为输出层，就可以实现这样的目标。

这是怎么做到的呢？在 Weighted LR 的训练中，我们需要为每个样本设置一个权重，权重的大小，代表了这个样本的重要程度。为了能够预估观看时长，YouTube 将正样本的权重设置为用户观看这个视频的时长，然后再用 Weighted LR 进行训练，就可以让模型学到用户观看时长的信息。

这是因为观看时长长的样本更加重要，严格一点来说，就是观看时长长的样本被模型预测的为正样本的概率更高，这个概率与观看时长成正比，这就是使用 Weighted LR 来学习观看时长信息的基本原理。

最后，我们再聊一聊排序模型的模型服务方法。我刚才讲过了，候选集生成模型是可以直接利用用户 Embedding 和视频 Embedding 进行快速最近邻搜索的。那排序模型还能这样做吗？

这就不可以了，原因有两点：一是因为我们的输入向量中同时包含了用户和视频的特征，不再只是单纯的用户特征。这样一来，用户 x 物品特征的组合过多，就无法通过预存的方式保存所有模型结果；二是因为排序模型的输出层不再是预测视频 ID，所以我们也无法拿到视频 Embedding。因此对于排序模型，我们必须使用 TensorFlow Serving 等模型服务平台，来进行模型的线上推断。

到这里，我们就讲完了 YouTube 推荐模型的全部细节。如果你有任何疑惑的地方，可以在留言区提问，同时我也建议你多看几遍这节课的内容，因为这个解决方案真的是太经典了。

小结

好了，这节课的内容讲完了，我们再总结一下 YouTube 推荐系统的重点知识。

YouTube 推荐系统的架构是一个典型的召回层加排序层的架构，其中候选集生成模型负责从百万候选集中召回几百个候选视频，排序模型负责几百个候选视频的精排，最终选出几十个推荐给用户。

候选集生成模型是一个典型的 Embedding MLP 的架构，我们要注意的是它的输出层，它是一个多分类的输出层，预测的是用户点击了“哪个”视频。在候选集生成模型的 serving 过程中，需要从输出层提取出视频 Embedding，从最后一层 ReLU 层得到用户 Embedding，然后利用最近邻搜索快速得到候选集。

排序模型同样是一个 Embedding MLP 的架构，不同的是，它的输入层包含了更多的用户和视频的特征，输出层采用了 Weighted LR 作为输出层，并且使用观看时长作为正样本权重，让模型能够预测出观看时长，这更接近 YouTube 要达成的商业目标。

好了，这些关键知识点，我也总结在了下面的表格中，希望它能帮助你加深记忆。

在这节课结束前，关于 YouTube 的推荐模型我还想多说几句。事实上，YouTube 的推荐系统论文中还包含了更多的细节，业界真正好的论文并不多，YouTube 的这篇《Deep Neural Networks for YouTube Recommendations》绝对是不可多得的一篇，我甚至推荐大家逐句来读，抓住每一个细节。

当然，你也可以在我的书《深度学习推荐系统》中的相应章节找到更多的实现细节。这些内容让我曾经受益匪浅，相信也会对你有所帮助。

课后思考

YouTube 的排序模型和候选集生成模型，都使用了平均池化这一操作，来把用户的历史观看视频整合起来。你能想到更好的方法来改进这个操作吗？

期待在留言区看到你的思考和总结，我们下节课见！

29 | 图神经网络：Pinterest是如何应用图神经网络的？

你好，我是王喆。

互联网中到处都是图结构的数据，比如我们熟悉的社交网络，最近流行的知识图谱等等，这些数据中包含着大量的关系信息，这对推荐系统来说是非常有帮助的。

为了能更好地利用这些信息进行推荐，各大巨头可谓尝试了各种办法，比如我们之前学过的 DeepWalk、Node2Vec 这些非常实用的 Graph Embedding 方法。但是技术的发展永无止境，最近两年，GNN（Graph Nerual Netwrok，图神经网络）毫无疑问是最火热、最流行的基于图结构数据的建模方法。严格一点来说，图神经网络指的就是可以直接处理图结构数据的神经网络模型。

在诸多 GNN 的解决方案中，著名的社交电商巨头 Pinterest 对于 GraphSAGE 的实现和落地又是最为成功的，在业界的影响力也最大。所以，这节课我们就学一学 GraphSAGE 的技术细节，看一看 Pinterest 是如何利用图神经网络进行商品推荐的。

搭桥还是平推？技术途径上的抉择

在正式开始 GraphSAGE 的讲解之前，我想先给你讲一讲 DeepWalk、Node2vec 这些 Graph Embedding 方法和 GNN 之间的关系，这有助于我们理解 GNN 的原理。

我们这里简单回顾一下 DeepWalk 和 Node2vec 算法的基本流程，如下面的图 1 所示。它们在面对像图 1b 这样的图数据的时候，其实没有直接处理图结构的数据，而是走了一个取巧的方式，先把图结构数据通过随机游走采样，转换成了序列数据，然后再 用诸如 Word2vec 这类序列数据 Embedding 的方法生成最终的 Graph Embedding。

图1 基于随机游走的Graph Embedding算法

我把这类 Graph Embedding 的方法归类为基于随机游走的间接性 Graph Embedding 方法。它其实代表了我们在解决一类技术问题时的思路，就是面对一个复杂问题时，我们不直接解决它，而是“搭一座桥”，通过这座桥把这个复杂问题转换成一个简单问题，因为对于简单问题，我们有非常丰富的处理手段。这样一来，这个复杂问题也就能简单地解决了。显然，基于随机游走的 Graph Embedding 方法就是这样一种“搭桥”的解决方案。

但搭桥的过程中难免会损失一些有用的信息，比如用随机游走对图数据进行抽样的时候，虽然我们得到的序列数据中还包含了图结构的信息，但却破坏了这些信息原始的结构。

正因为这样，很多研究者、工程师不满足于这样搭桥的方式，而是希望造一台“推土机”，把这个问题平推过去，直接解决它。GNN 就是这样一种平推解决图结构数据问题的方法，它直接输入图结构的数据，产生节点的 Embedding 或者推荐结果。当然，不同研究者打造这台推土机的方式各不相同，我们今天要重点介绍的 GraphSAGE，就是其中最著名的一台，也最具参考价值。

GraphSAGE 的主要步骤

下面，我们就来详细讲一讲 GraphSAGE 的细节。GraphSAGE 的全称叫做 Graph Sample and Aggregate，翻译过来叫“图采样和聚集方法”。其实这个名称就很好地解释了它运行的过程，就是先“采样”、再“聚集”。

这时候问题又来了，这里的“采样”还是随机游走的采样吗？要是还通过采样才能得到样本，我们造的还能是“推土机”吗，不就又变成搭桥的方式了吗？别着急，等我讲完 GraphSAGE 的细节，你就明白了。

图2 GraphSAGE的主要过程
（出自论文 Inductive Representation Learning on Large Graphs）

GraphSAGE 的过程如上图所示，主要可以分为 3 步：

在整体的图数据上，从某一个中心节点开始采样，得到一个 k 阶的子图，示意图中给出的示例是一个二阶子图；

有了这个二阶子图，我们可以先利用 GNN 把二阶的邻接点聚合成一阶的邻接点（图 1-2 中绿色的部分），再把一阶的邻接点聚合成这个中心节点（图 1-2 中蓝色的部分）；

有了聚合好的这个中心节点的 Embedding，我们就可以去完成一个预测任务，比如这个中心节点的标签是被点击的电影，那我们就可以让这个 GNN 完成一个点击率预估任务。

这就是 GraphSAGE 的主要步骤，你看了之后可能还是觉得有点抽象。那接下来，我们再结合下图 3 推荐电影的例子，来看一看 GraphSAGE 是怎么工作的。

图3 GraphSAGE示例

首先，我们要利用 MovieLens 的数据得到电影间的关系图，这个关系图可以是用用户行为生成（这个方法我们在第 7 节课讲过），它也可以是像生成知识图谱一样来生成，比如，两部电影拥有同一个演员就可以建立一条边，拥有相同的风格也可以建立一条边，规则我们可以自己定。

在这个由电影作为节点的关系图上，我们随机选择一个中心节点。比如，我们选择了玩具总动员（Toy Story）作为中心节点，这时再向外进行二阶的邻接点采样，就能生成一个树形的样本。

经过多次采样之后，我们会拥有一批这样的子图样本。这时，我们就可以把这些样本输入 GNN 中进行训练了。GNN 的结构我会在下一小节详细来讲，这里我们只要清楚，这个 GNN 既可以预测中心节点的标签，比如点击或未点击，也可以单纯训练中心节点的 Embedding 就够了。

总的来说，GraphSAGE 的主要步骤就是三个“抽样 - 聚合 - 预测”。

GraphSAGE 的模型结构

现在，我们关注的重点就变成了 GraphSAGE 的模型结构到底怎么样？它到底是怎么把一个 k 阶的子图放到 GNN 中去训练，然后生成中心节点的 Embedding 的呢？接下来，我就结合 GraphSAGE 的模型结构来和你详细讲一讲。

这里，我们还是以二阶的 GraphSAGE 为例，因为超过二阶的结构只是进一步延伸这个模型，没有更多特别的地方，所以我们理解二阶的模型结构就足够了。

图4 GraphSAGE的模型结构

上图中处理的样本是一个以点 A 为中心节点的二阶子图，从左到右我们可以看到，点 A 的一阶邻接点包括点 B、点 C 和点 D，从点 B、C、D 再扩散一阶，可以看到点 B 的邻接点是点 A 和点 C，点 C 的邻接点是 A、B、E、F，而点 D 的邻接点是点 A。

清楚了样本的结构，我们再从右到左来看一看 GraphSAGE 的训练过程。这个 GNN 的输入是二阶邻接点的 Embedding，二阶邻接点的 Embedding 通过一个叫 CONVOLVE 的操作生成了一阶邻接点的 Embedding，然后一阶邻接点的 Embedding 再通过这个 CONVOLVE 的操作生成了目标中心节点的 Embedding，至此完成了整个训练。

这个过程实现的关键就在于这个叫 CONVOLVE 的操作，那它到底是什么呢？

CONVOLVE 的中文名你肯定不会陌生，就是卷积。但这里的卷积并不是严格意义上的数学卷积运算，而是一个由 Aggregate 操作和 Concat 操作组成的复杂操作。这里，我们要重点关注图 4 中间的部分，它放大了 CONVOLVE 操作的细节。

这个 CONVOLVE 操作是由两个步骤组成的：第一步叫 Aggregate 操作，就是图 4 中 gamma 符号代表的操作，它把点 A 的三个邻接点 Embedding 进行了聚合，生成了一个 Embedding hN(A)；第二步，我们再把 hN(A) 与点 A 上一轮训练中的 Embedding hA 连接起来，然后通过一个全联接层生成点 A 新的 Embedding。

第二步实现起来很简单，但第一步中的 Aggregate 操作到底是什么呢？搞清楚这个，我们就搞清楚了 GraphSAGE 的所有细节。

事实上，Aggregate 操作我们也不陌生，它其实就是把多个 Embedding 聚合成一个 Embedding 的操作，我们在推荐模型篇中也讲过很多次了。比如，我们最开始使用的 Average Pooling，在 DIN 中使用过的 Attention 机制，在序列模型中讲过的基于 GRU 的方法，以及可以把这些 Embedding 聚合起来的 MLP 等等。Aggregate 操作非常多，如果你要问具体用哪个，我还是那句老话，实践决定最终结构。

到这里，我们就抽丝剥茧地讲清楚了 GraphSAGE 的每个模型细节。如果你还有疑惑，再回头多看几遍 GraphSAGE 的模型结构图，结合我刚才的讲解，相信不难理解。

GraphSAGE 的预测目标

不过，在讲 GraphSAGE 的主要步骤的时候，我们还留下了一个“小尾巴”没有讲，就是说 GraphSAGE 既可以预测中心节点的标签，比如点击或未点击，又可以单纯地生成中心节点的 Embedding。要知道预测样本标签这个事情是一个典型的有监督学习任务，而生成节点的 Embedding 又是一个无监督学习任务。

那 GraphSAGE 是怎么做到既可以进行有监督学习，又能进行无监督学习的呢？要想让 GraphSAGE 做到这一点，关键就看你怎么设计它的输出层了。

我们先来说说有监督的情况，为了预测中心节点附带的标签，比如这个标签是点击或未点击，我们就需要让 GraphSAGE 的输出层是一个 Logistic Regression 这样的二分类模型，这个输出层的输入，就是我们之前通过 GNN 学到的中心节点 Embedding，输出当然就是预测标签的概率了。这样，GraphSAGE 就可以完成有监督学习的任务了。

而对于无监督学习，那就更简单了。这是因为，我们的输出层就完全可以仿照第 6 节课Word2vec 输出层的设计，用一个 softmax 当作输出层，预测的是每个点的 ID。这样一来，每个点 ID 对应的 softmax 输出层向量就是这个点的 Embedding，这就和 word2vec 的原理完全一致了。如果你仔细学了 YouTube 的候选集生成模型的话，就会知道这和视频向量的生成方式也是一样的。

GraphSAGE 在 Pinterest 推荐系统中的应用

GraphSAGE 我们讲了这么多，那 Pinterest 到底是怎么在它的推荐系统中应用 GNN 的呢？我这就来讲一讲。

由于 GraphSAGE 是 Pinterest 和斯坦福大学联合提出的，所以 Pinterest 对于 GNN 的应用也是直接在 GraphSAGE 的基础上进行的，只是给这个 GNN 取了个换汤不换药的新名字，PinSAGE。

Pinterest 这个网站的主要功能是为用户提供各种商品的浏览、推荐、收藏的服务，那么所谓的 Pin 这个动作，其实就是你收藏了一个商品到自己的收藏夹。因此，所有的 Pin 操作就连接起了用户、商品和收藏夹，共同构成了一个它们之间的关系图。PinSAGE 就是在这个图上训练并得到每个商品的 Embedding 的。

而 PinSAGE Embedding 的具体应用场景，其实跟我们14 课中实现的功能一样，就是商品的相似推荐。只不过之前业界更多地使用 Item2vec、DeepWalk 这些方法，来生成用于相似推荐的物品 Embedding，在 GNN 流行起来之后，大家就开始尝试使用 GNN 生成的物品 Embedding 进行相似推荐。

那么，PinSAGE 在 Pinterest 场景下的效果到底怎么样呢？Pinterest 给出了一些例子如图 5 所示，我们可以判断一下。

图5 PinSAGE在Pinterest上应用的例子

我们先看图 5 左边的例子，因为它给出的是一个种子发芽的图片，我们就推测它应该是一个卖绿植或者绿植种子的商家。接下来，我们再来判断左边通过四种不同算法找到的相似图片是不是合理。其中，PinSAGE 是 Pinterest 实际用于推荐系统中的算法，其他三个 Visual、Annot、Pixie 都是效果测试中的对比算法。

我们看到通过第一个算法 Visual 找到的图片，虽然看上去和原来的图片比较相似，但前两个图片居然都是食品照片，这显然不相关。第二个算法 Annot 中的树木，以及第三个算法 Pixie 中的辣椒和西兰花，显然都跟绿植种子有很遥远的差距。相比之下，PinSAGE 找到的图片就很合理了，它找到的全都是种子发芽或者培育绿植的图片，这就非常合乎用户的逻辑了。

要知道，在 PinSAGE 应用的构成中，它没有直接分析图片内容，而只是把图片当作一个节点，利用节点和周围节点的关系生成的图片 Embedding。因此，这个例子可以说明，PinSAGE 某种程度上理解了图片的语义信息，而这些语义信息正是埋藏在 Pinterest 的商品关系图中。可见，PinSAGE 起到了多么神奇的数据挖掘的作用。

小结

这节课，我们讲解了图神经网络的经典方法 GraphSAGE，我们抽丝剥茧地把 GraphSAGE 的细节全部剖开了。关于 GraphSAGE，我们重点要记住它的特点和主要步骤。

首先，GraphSAGE 是目前来说最经典的 GNN 解决方案。因此，它具有 GNN 最显著的特点，那就是它可以直接处理图数据，不需要把图数据转换成更简单的序列数据，再用序列数据 Embedding 方法进行处理。

其次，GraphSAGE 的主要步骤是三步“采样 - 聚合 - 预测”。其中，采样是指在整体图数据上随机确定中心节点，采样 k 阶子图样本。聚合是指利用 GNN 把 k 阶子图样本聚合成中心节点 Embedding。预测是指利用 GNN 做有监督的标签预测或者直接生成节点 Embedding。

在这三步之中，重点在于聚合的 GNN 结构，它使用 CONVOLVE 操作把邻接点 Embedding 聚合起来，跟中心节点上一轮的 Embedding 连接后，利用全连接层生成新的 Embedding。

为了方便你及时回顾，我也把这节课中的重要知识点总结了下面的表格中，你可以看看。

课后思考

使用 GraphSAGE 是为了生成每个节点的 Embedding，那我们有没有办法在 GraphSAGE 中加入物品的其他特征，如物品的价格、种类等等特征，让最终生成的物品 Embedding 中包含这些物品特征的信息呢？

期待在留言区看到你对 GraphSAGE 的思考，我们下节课见！

30 | 流处理平台：Flink是如何快速识别用户兴趣，实现实时推荐的？

你好，我是王喆。

刚刚结束的 2020 年双 11 活动，让技术圈出现了一个非常劲爆的新闻，就是阿里基于 Flink，实现了数据的批流一体处理，每秒能够处理 40 亿条的巨量数据。这也是业界首次在这么大规模的数据洪峰之上，实现数据流的实时处理。

也正是因为实时数据流处理功能的实现，让阿里的推荐系统引擎能够在双 11 期间做出更快速的反应，实时抓住用户的兴趣，给出更准确的推荐。

这节课，我就带你揭开阿里使用流处理平台 Flink 的面纱，来重点解决这 3 个问题：

为什么说实时性是影响推荐系统效果的关键因素？

到底什么是批流一体的数据处理体系？

业界流行的 Flink 到底是怎么实现数据流处理的？

为什么实时性是影响推荐系统效果的关键因素？

周星驰的电影《功夫》里有一句著名的台词“天下武功，无坚不摧，唯快不破”。如果说推荐模型的架构是那把“无坚不摧”的“玄铁重剑”，那么推荐系统的实时性就是“唯快不破”的“柳叶飞刀”。那这把柳叶飞刀到底是怎么发挥作用的呢？我说个切身的场景你就明白了。

假设，你正在手机上刷抖音，你刚刚看了一个精彩的足球进球视频，感觉意犹未尽，还想看更多这样的视频。这个时候，抖音的推荐系统肯定不会让你失望，它很快会接收到“你观看了精彩进球视频”的信号，快速地抓到你的兴趣点，然后，它会迅速做出反馈，给你推送更多类似的视频。

那我们也可以试想一下，如果抖音的推荐系统实时性不够的话，会发生什么呢？可能是你看完了精彩进球的视频之后，推荐系统还跟什么都没发生一样，依然按部就班地给你推荐一些原本就设定好的不相关视频。如果是这样的话，抖音又怎么能让你欲罢不能，刷了又想刷呢？

这个例子就充分说明了，推荐系统只有拥有实时抓住用户新兴趣点的能力，才能让你的用户“离不开你”。

什么是批流一体的数据处理体系？

那作为推荐系统工程师，我们就要思考，到底怎样才能实现用户兴趣的快速提取呢？这就不得不提到推荐系统的数据体系。

我们之前讲的数据处理，无论是数据的预处理，还是特征工程，大部分是在 Spark 平台上完成的。Spark 平台的特点是，它处理的数据都是已经落盘的数据。也就是说，这些数据要么是在硬盘上，要么是在分布式的文件系统上，然后才会被批量地载入到 Spark 平台上进行运算处理，这种批量处理大数据的架构就叫做批处理大数据架构。它的整体结构图如图 1 所示。

图1 传统批处理大数据架构

但批处理架构的特点就是慢，数据从产生到落盘，再到被 Spark 平台重新读取处理，往往要经历几十分钟甚至几小时的延迟。如果推荐系统是建立在这样的数据处理架构上，还有可能实时地抓住用户的新兴趣点吗？肯定是没有希望了。

那怎么办呢？我们能不能在数据产生之后就立马处理它，而不是等到它落盘后再重新处理它呢？当然是可以的，这种在数据产生后就直接对数据流进行处理的架构，就叫做流处理大数据架构。

我们从图 2 的流处理架构示意图中可以看到，它和批处理大数据架构相比，不仅用流处理平台替换掉了分布式批处理 Map Reduce 计算平台，而且在数据源与计算平台之间，也不再有存储系统这一层。这就大大提高了数据处理的速度，让数据的延迟可以降低到几分钟级别，甚至一分钟以内，这也让实时推荐成为了可能。

图2 流处理大数据架构

但是，流处理平台也不是十全十美的。由于流处理平台是对数据流进行直接处理，它没有办法进行长时间段的历史数据的全量处理，这就让流处理平台无法应用在历史特征的提取，模型的训练样本生成这样非常重要的领域。

那是不是说，根本就没有能够同时具有批处理、流处理优势的解决方案吗？当然是有的，这就是我们在一开始说的，批流一体的大数据架构，其中最有代表性的就是 Flink。

如下图 3 所示，批流一体的大数据架构最重要的特点，就是在流处理架构的基础上添加了数据重播的功能。

我们怎么理解这个数据重播功能呢？它指的是在数据落盘之后，还可以利用流处理平台同样的代码，进行落盘数据的处理，这就相当于进行了一遍重播。这样不就实现了离线环境下的数据批处理了吗？而且由于流处理和批处理使用的是一套代码，因此完美保证了代码维护的一致性，是近乎完美的数据流解决方案。

图3 批流一体大数据架构

既然批流一体的大数据架构这么完美，为什么我们很少听说有实现这套方案的公司呢？以我个人的实践经验来看，这主要是因为它实现起来有下面两个难点。

大批成熟的互联网公司已经在 Spark 等批处理平台上，构建起了整套的数据体系，要想完全迁移到批流一体的数据体系上，有着非常沉重的技术负担。

批流一体的解决方案还很理想化，因为我们在实际处理特征的时候，很难让批处理和流处理完全共享一套代码。

比如，我们在流处理中可以很方便地计算出点击量、曝光量这类方便累计的指标，但如果遇到比较复杂的特征，像是用户过去一个月的平均访问时长，用户观看视频的进度百分比等等，这些指标就很难在流处理中计算得到了。这是因为计算这类特征所需的数据时间跨度大，计算复杂，流处理难以实现。

因此，在对待流处理平台时，我们的态度应该是，取其所长。更具体点来说就是，在需要实时计算的地方发挥它的长处，但也没有必要过于理想主义，强调一切应用都应该批流一体，这反而会为我们增加过多的技术负担。

Flink 是如何处理数据流的？

现在，我们已经清楚流处理平台的特点和优势了，但 Flink 平台到底是怎么进行流数据处理的呢？

我们先来认识 Flink 中两个最重要的概念，数据流（DataStream）和窗口（Window）。数据流其实就是消息队列，从网站、APP 这些客户端中产生的数据，被发送到服务器端的时候，就是一个数据消息队列，而流处理平台就是要对这个消息队列进行实时处理。

就像下图 4 所示的那样，上面是来自三个用户的数据，其中一个一个紫色的点就是一条条数据，所有紫色的点按时间排列就形成了一个消息队列。

图4 数据流和窗口

知道了什么是消息队列，Flink 会怎么处理这个消息队列里的数据呢？答案很简单，就是随着时间的流失，按照时间窗口来依次处理每个时间窗口内的数据。

比如图 4 中的数据流就被分割成了 5 个时间窗口，每个窗口的长度假设是 5 分钟，这意味着每积攒够 5 分钟的数据，Flink 就会把缓存在内存中的这 5 分钟数据进行一次批处理。这样，我们就可以算出数据流中涉及物品的最新 CTR，并且根据用户最新点击的物品来更新用户的兴趣向量，记录特定物品曝光给用户的次数等等。

除了上面例子中的固定窗口以外，Flink 还提供了多种不同的窗口类型，滑动窗口（Sliding Window）也是我们经常会用到的。

滑动窗口的特点是在两个窗口之间留有重叠的部分，Flink 在移动窗口的时候，不是移动 window size 这个长度，而是移动 window slide 这个长度，window slide 的长度要小于 window size。因此，窗口内部的数据不仅包含了数据流中新进入的 window slide 长度的数据，还包含了上一个窗口的老数据，这部分数据的长度是 window size-window slide。

图5 Flink中的滑动窗口

那滑动窗口这种方式有什么用呢？它最典型的用处就是做一些数据的 JOIN 操作。比如我们往往需要通过 JOIN 连接一个物品的曝光数据和点击数据，以此来计算 CTR，但是你要知道，曝光数据肯定是在点击数据之前到达 Flink 的。

那如果在分窗的时候，恰好把曝光数据和点击数据分割在了两个窗口怎么办呢？那点击数据就不可能找到相应的曝光数据了。这个时候，只要我们使用滑动窗口，这个问题就迎刃而解了。因为两个窗口重叠的部分给我们留了足够的余量来进行数据 JOIN，避免数据的遗漏。

事实上，除了固定窗口和滑动窗口，Flink 还提供了更丰富的窗口操作，比如基于会话的 Session Window，全局性的 Global Window。除此之外，Flink 还具有数据流 JOIN，状态保存特性 state 等众多非常有价值的操作，想继续学习的同学可以在课后参考 Flink 的官方文档 。我们这节课，只要清楚 Flink 的核心概念数据流和时间窗口就可以了，因为它反映了流处理平台最核心的特点。

Flink 数据流处理实践

接下来，就又到了实践的环节。我们要继续在 SparrowRecsys 项目上利用 Flink 实现一个特征更新的应用。因为没有真实的数据流环境，所以我们利用 MoviesLens 的 ratings 表来模拟一个用户评分的数据流，然后基于这个数据流，利用 Flink 的时间窗口操作，来实时地提取出用户最近的评分电影，以此来反映用户的兴趣。

下面就是 SparrowRecsys 中相关的代码（详细代码：com.sparrowrecsys.nearline.flink.RealTimeFeature）。你可以看到，我首先定义了一个评分的数据流 ratingStream，然后在处理 ratingStream 的时候，是把 userId 作为 key 进行处理。

接着，我又利用到了两个函数 timeWindow 和 reduce。利用 timeWindow 函数，我们可以把处理的时间窗口设置成 1s，再利用 reduce 函数，把每个时间窗口到期时触发的操作设置好。

在完成了 reduce 操作后，我们再触发 addSink 函数中添加的操作，进行数据存储、特征更新等操作。

DataStream<Rating> ratingStream = inputStream.map(Rating::new);
ratingStream.keyBy(rating -> rating.userId)
        .timeWindow(Time.seconds(1))
        .reduce(
                (ReduceFunction<Rating>) (rating, t1) -> {
                    if (rating.timestamp.compareTo(t1.timestamp) > 0){
                        return rating;
                    }else{
                        return t1;
                    }
                }
        ).addSink(new SinkFunction<Rating>() {
    @Override
    public void invoke(Rating value, Context context) {
        System.out.println("userId:" + value.userId + "\tlatestMovieId:" + value.latestMovieId);
    }
});

看完了这些操作之后，你知道我们应该怎么把用户最近的高分电影评价历史，实时反映到推荐结果上了吗？其实很简单，我们的用户 Embedding 是通过平均用户的高分电影 Embedding 得到的，我们只需要在得到新的高分电影后，实时地更新用户 Embedding 就可以了，然后在推荐过程中，用户的推荐列表自然会发生实时的变化。这就是 SparrowRecsys 基于 Flink 的实时推荐过程。

小结

这节课我们讲了流处理平台 Flink 特点，并且通过 Flink 的实践清楚了，利用流处理平台提高推荐系统实时性的方法。

Flink 是最具代表性的批流一体的大数据平台。它的特点就是，让批处理和流处理共用一套代码，从而既能批量处理已落盘的数据，又能直接处理实时数据流。从理论上来说，是近乎完美的数据流解决方案。

而 Flink 提高推荐系统实时性的原理可以理解为是，用户数据进入数据流，也就是数据消息队列后，会被分割成一定时长的时间窗口，之后 Flink 会按照顺序来依次处理每个时间窗口内的数据，计算出推荐系统需要的特征。这个处理是直接在实时数据流上进行的，所以相比原来基于 Spark 的批处理过程，实时性有了大幅提高。

为了方便你复习，我把这节课的核心概念总结在了下面的表格里，希望你能记住它们。

至于 Flink 的实时性实践，我们要记住，利用 Flink 我们可以实时地获取到用户刚刚评价过的电影，然后通过实时更新用户 Embedding，就可以实现 SparrowRecsys 的实时推荐了。

课后思考

你觉得实时性是不是对所有推荐系统都非常重要？比如对于抖音、快手这类短视频应用，还有优酷、Netflix 这类长视频应用，实时性对哪个更重要一些？为什么？

Flink 要加强的往往是数据的实时性，特征的实时性，你觉得模型训练的实时性重要吗？模型训练的实时性发挥的作用和特征实时性有什么不同呢？

期待在留言区看到你对 Flink 的思考和疑惑，我们下节课见！

31｜模型迭代：阿里巴巴是如何迭代更新推荐模型的？

你好，我是王喆。

前两天，我问了我团队中推荐工程师们一个问题，“你们觉得在推荐系统领域工作，最大的挑战是什么？”。有的人说是模型调参，有的人说是工程落地，有的人说是找到下一步改进的思路。那现在听到这个问题的你，认为会是什么呢？

其实，刚才的这些回答都很好。不过，我的想法和大家稍有不同，我认为工作中最大的挑战就是，不断地优化整个推荐系统的架构，做到持续的效果提升和高质量的模型迭代。这是因为，从我们成为一名算法工程师的第一天开始就在不断面临挑战，不仅要跟别人赛跑，也要跟自己赛跑，而超越过去的自己往往是最难的。

那这节课，我就以阿里妈妈团队的经验为例，带你看一看业界一线的团队是如何实现模型的持续迭代创新，做到好上加好的。在学习这节课的时候，我再给你一点小建议，不要纠结于每个模型的细节，而是要多体会阿里的工程师是如何定位创新点的。

阿里妈妈推荐系统的应用场景

阿里妈妈是阿里巴巴集团的营销广告团队，他们负责了阿里大部分广告推荐系统的工作。下图 1 就是一个阿里妈妈典型的广告推荐场景。

这是我在手机淘宝上搜索了“篮球鞋”之后，App 给我推荐的一个商品列表。其中，第一个搜索结果的左上角带着 HOT 标识，左下角还有一个比较隐蔽的灰色“广告”标识。那带有这两个标志的，就是阿里妈妈广告推荐系统推荐的商品啦。

图1 阿里妈妈广告推荐的场景

如果让你来构建这个广告推荐模型，你觉得应该考虑的关键点是什么？

我们可以重点从它的商业目标来思考。阿里妈妈的广告大部分是按照 CPC（Cost Per Click）付费的，也就是按照点击效果付费的，所以构建这个模型最重要的一点就是要准确预测每个广告商品的点击率，这样才能把高点击率的商品个性化地推荐给特定的用户，让整个广告系统的收益更高。

因此，模型怎么预测出用户爱点击的广告种类就是重中之重了。那预测广告就一定要分析和利用用户的历史行为，因为这些历史行为之中包含了用户最真实的购买兴趣。

而阿里妈妈推荐模型的主要进化和迭代的方向，也是围绕着用户历史行为的处理展开的。下面，我们就来一起看一看阿里妈妈深度推荐模型的进化过程。

阿里妈妈深度推荐模型的进化过程

阿里妈妈深度推荐模型的起点被称为 Base Model，它是一个基础版本的模型结构，就是我们之前讲过的 Embedding MLP 的结构，它的结构如图 2 所示。

图2 Base Model的架构和用户历史行为样例

Base Model 的输入特征包括用户特征、广告特征、场景特征等等，我们最需要关注的，就是它处理用户行为特征的原理，也就是图中彩色的部分。

Base Model 使用了 SUM Pooling 层将用户的所有历史购买商品 Embedding 进行了加和。这样一来，Base Model 就会一视同仁、不分重点地把用户的历史行为都加起来，形成一个固定的用户兴趣向量。

比如说，如果一位女性用户购买过连衣裙、奶粉、婴儿车，她的兴趣向量就是全部商品 Embedding 的平均。

这当然是一种很粗糙的历史行为处理方式，自然就成为了阿里的工程师需要重点改进的方向。那具体该怎么改进呢？我们在第 21 节课讲过，就是使用注意力机制来计算每个历史行为和当前要预测的候选广告物品之间的关系，关系紧密的权重大，关系疏远的权重低。

图3 DIN的模型架构和相应的用户历史行为样例

这样一来，如果我们要预测这位用户购买另一套女装时的 CTR，在历史行为中，购买连衣裙这类商品的权重就会高，而购买奶粉这类不相关商品的权重就会低，就像图 3 中商品下面的进度条一样。

那通过这个例子我们又一次证明了，引入注意力机制是非常符合我们购买习惯的一种模型改进方式。

但是，模型的改进到这里就结束了吗？当然不是，我们虽然引入了注意力机制，但还没有考虑商品的购买时间啊。

举个例子来说，你前一天在天猫上买了一个键盘，今天你又在天猫上闲逛。那对推荐系统来说，是你前一天的购买经历比较重要，还是一年前的更重要呢？当然是前一天的，因为很可能因为你前一天购买了键盘，所以今天购买鼠标或者其他电脑周边的概率变大了，而这些连续事件之间的相关性，对模型来说是非常有用的知识。

于是，DIN 模型就进一步进化到了 DIEN 模型。图 4 就很好地展示了 DIEN 的模型结构和它对应的用户历史行为样例。

图4 DIEN的模型架构和相应的用户历史行为样例

其实，本质上所有的用户历史行为都是序列式的，时间维度是天然附着在每个历史商品之上的属性。既然我们在之前的模型中把这个属性丢掉了，就一定存在着信息的浪费和知识的丢失。因此在改进模型时，这自然成为了一个突破点。

DIEN 正是在模型中加入了一个三层的序列模型结构，来处理用户的历史行为序列，进而抽取出用户的购买兴趣演化序列，最后预测出用户当前的购买兴趣。但是这次改进之后，模型的迭代之路似乎变得越来越难了，似乎算法工程师们把该想到的都想到了。

算法工程师这条职业道路确实是这样的，它是一个自己跟自己较劲的过程，上一轮大幅提高的优化效果，反而可能会成为这一轮改进的阻碍。这个时候，我们很可能会陷入阶段性的怀疑状态，怀疑自己还能不能找到更好的改进点。

这种自我怀疑是正常的，但更重要的是，我们能不能在阶段性的怀疑过后，重新开始冷静地分析问题，找到还没有利用过的信息资源，或者补上之前的某个技术短板，从而在现有系统中“扣出”下一个增长点。

这也是我们要从阿里工程师的工作中学习和借鉴的，下面，我们就来看看他们是怎么继续优化模型的。

2019 年，阿里的工程师们提出了新的模型改进思路，也就是多通道兴趣记忆网络 MIMN。因为他们发现，DIEN 虽然存在着序列模型的结构，但所有的历史行为都是在一条线上演化的，而事实上，用户的兴趣演化过程完全可以多条并行。这该怎么理解呢？

我们还以样例中的这位女性用户为例，她购买女装的兴趣演化路径，完全可以跟购买婴儿物品的演化路径平行，她的兴趣向量也可以是多个，而不仅仅局限于一个。

图5 MIMN的模型架构和相应的用户历史行为样例

正是基于这样的模型改进动机，MIMN 相比于 DIEN 加入了多条平行的兴趣演化路径，最终为 Embedding MLP 的主模型提供了多个课选择的兴趣向量。当然，具体选择哪个，我们还是要根据当前兴趣与目标广告商品之间的注意力得分而定。通过引入兴趣演化的多通道，MIMN 进一步加强了模型的表达能力，让模型挖掘数据价值的能力更强。

说了这么多，这几个模型到底有没有提升阿里妈妈推荐系统的效果呢？我们来看一看表 1 的离线评估结果。从中我们可以看到，阿里采用了 ROC AUC 作为评估指标，正如期望的那样，随着模型的一步步改进，模型的效果在淘宝数据集和亚马逊数据集上都逐渐增强。

表1 离线评估结果

当然，我建议所有论文中的实验效果你都应该辩证地来看，可以作为一个参考值，但不能全部相信。因为就像我常说的在，模型结构不存在最优，只要你的数据、业务特点和阿里不一样，就不能假设这个模型在你的场景下具有同样的效果，所以根据自己的数据特点来改进模型才最重要。

阿里妈妈的其他创新点

这个时候，有的同学可能会问了，阿里妈妈的同学们只是瞄着用户历史这个点去优化吗？感觉这一点的优化空间早晚会穷尽啊。我要说这个观察角度太好了，作为一名算法工程师，技术的格局和全面的视野都是非常重要的，技术的空间那么大，我们不能只盯着一个点去改进，要适当地跳出局限你的空间，去找到其他的技术短板。这样一来，我们往往会发现更好的迭代方向。

所以，阿里的工程师们除了对用户历史进行挖掘，也针对商品图片信息进行了建模，也就是所谓的 DICM（Deep Image CTR Model）。

图6 DICM的模型架构

图 6 中 DICM 的模型结构很好理解，它还是采用了 Embedding MLP 的主模型架构，只不过引入了用户历史购买过的商品的图片信息，以及当前广告商品的图片信息。这些商品图片还是以 Embedding 的形式表示，然后输入后续 MLP 层进行处理。

因为要引入图片信息，所以在 MLP 层之前，还需要利用一个预训练好的 CV 模型来生成这些图片的 Embedding 表达。简单来说，就是一个预训练图片 Embedding+MLP 的模型结构。

除此之外，阿里团队还在很多方向上都做了研究。比如说，同时处理 CTR 预估和 CVR 预估问题的多目标优化模型 ESMM（Entire Space Multi-Task Model），基于用户会话进行会话内推荐的 DSIN（Deep Session Interest Network ），可以在线快速排序的轻量级深度推荐模型 COLD(Computing power cost-aware Online and Lightweight Deep pre-ranking system) 等等。

我们可以用百花齐放来形容阿里团队的发展过程。你如果感兴趣，可以按照我提供的论文地址来进一步追踪学习。

我们能学到什么？

到这里，我带你从业务方面梳理了，业界一线团队进行模型迭代的过程。那我们到底能从阿里妈妈团队学到或者说借鉴到什么呢？为了帮助你彻底梳理清楚，我把阿里妈妈模型的发展过程分为横向和纵向这两个维度绘制了在下面。

图7 阿里妈妈推荐模型的发展过程

横向的维度是推荐主模型的发展过程，从基础的深度模型一路走来，沿着挖掘用户行为信息的思路不断深化，我把这种发展思路叫做模型的深化演进思路。

但是深化演进的过程是一个不断挑战自己的过程，虽然我们说数据中埋藏着无穷无尽的宝藏，不可能让我们挖完，但是挖掘的难度一直在不断加大。

就像刚开始通过用深度学习模型替代传统机器学习模型，我们能取得 10% 这个量级的收益，加入注意力机制，加入序列模型这样大的结构改进，我们能取得 1%-3% 这个量级的收益，到后来，更加复杂的改进，可能只能取得 0.5% 甚至更低的收益。这个时候，我们就需要把视野拓宽，从其他角度看待推荐问题，找到新的切入点。

这方面，阿里给我们做了很好的示范。比如图 7 纵向的发展过程就是阿里团队对于一些独立问题的解决方法：多目标、多模态，包括我们之前提到的可以进行实时服务和在线学习在 COLD 模型。这些都是独立的技术点，但是它们对于推荐效果的提高可能丝毫不弱于主排序模型的改进。这种从不同角度改进推荐模型的思路，我称之为广度扩展。

推荐系统是一个非常复杂的系统，系统中成百上千个不同的模块共同围成了这个系统，它最终的效果也是由这几百上千个模块共同决定的。在通往资深推荐工程师的路上，你的任务是敏锐地发现这些模块中的短板，而不是总盯着最长的那块木板改进，只有高效地解决短板问题，我们才能取得最大的收益。这也是我们这门课为什么一直强调要系统性地建立知识体系的原因。

小结

今天，我带你总结了阿里妈妈团队推荐模型的发展过程。我们可以从两个维度来看阿里广告推荐模型的演进。

首先是深化演进这个迭代思路上，阿里妈妈对用户的历史行为进行了深入理解和不断挖掘，这让阿里妈妈模型从最初的 Embedding MLP 结构，到引入了注意力机制的 DIN，序列模型的 DIEN，再到能够利用多通道序列模型学习出用户多个兴趣向量的 MIMN，整个发展是一脉相承的。

其次，阿里妈妈还一直在践行“广度拓展”的模型迭代思路，从多目标、多模态，以及在线学习等多个角度改进推荐模型，实现了推荐系统的整体效果的提高。

最后，我把阿里妈妈模型演进过程中所有改进模型的特点，都总结在了下面，希望可以帮助你加深印象。

课后思考

最后的最后，我希望咱们再来做一个开放型的练习：如果我让你在 MIMN 的基础上进一步改进模型，你觉得还有什么可供挖掘的用户行为信息吗？或者说还有什么方式能够进一步利用好用户行为信息吗？

期待在留言区看到你的创意和思考，我们下节课见！

32 | 强化学习案例：美团是如何在推荐系统中落地强化学习的？

你好，我是王喆。今天我们来聊一聊美团的强化学习落地案例。

我们在第 22 讲中学过强化学习的基本原理、优点，以及微软的强化学习模型 DRN，但我们也说了强化学习在推荐系统中落地会有一个难点：因为强化学习涉及模型训练、线上服务、数据收集、实时模型更新等几乎推荐系统的所有工程环节，所以强化学习整个落地过程的工程量非常大，需要工程和研究部门通力合作才能实现。

即使很难，但业界依然有成功落地强化学习的案例，典型的就是美团在“猜你喜欢”功能中的应用。美团曾在官方博客中详细介绍过这一落地方案的技术细节，我们这节课就借助这个方案，来好好学习一下强化学习的实践。我也希望你能通过这个案例，串联起我们学过的所有推荐系统知识。

美团的强化学习应用场景

“猜你喜欢”是美团这个团购 App 中流量最大的推荐展位，产品形态是信息流。从图 1 的 App 截图中你可以看到，“猜你喜欢”列表中推荐了用户可能喜欢的餐厅，用户可以通过下滑和翻页的方式实现与 App 的多轮交互，在这个过程中，用户还可能发生点击、购买等多种行为。

图1 美团首页“猜你喜欢”场景

强化学习的应用场景就藏在用户和美团 App 的多轮交互之中。如果推荐系统能够在用户每次翻页的时候都考虑到用户刚刚发生的行为，就能够提供实时性更强的推荐体验。图 2 是美团的同学统计的用户翻页次数的分布图，我们可以看到，多轮交互确实是美团 App 中非常常见的用户场景，这也说明强化学习还是非常有用武之地的。

图2 “猜你喜欢”展位用户翻页情况统计

美团的强化学习建模方法

清楚了美团为什么要使用强化学习，我们就要开始思考强化学习是怎么应用到这样的场景之上的。

通过 22 课讲的学习，我们知道了强化学习有六个要素，分别是：智能体、环境、行动、奖励、目标和状态。清楚这六个要素在“猜你喜欢”功能中的具体含义，以及建模过程，我们就能知道美团 App 是怎么应用强化学习了。

那接下来，我就带你依次看一看美团是怎么针对这六个要素进行建模的。

首先是“智能体”，它指的就是美团的推荐系统，这其中最重要的部分自然就是强化学习的模型，这个我们等会再详细讲。“环境”我们刚才介绍过了，指的是美团 App 这个“猜你喜欢”的推荐场景。“行动”也不难理解，就是推荐系统选出商铺的列表，然后推荐给用户这个动作。接下来的三个要素是整个系统的重点，就是“奖励”、“目标”和“状态”。下面，我们一一来看它们的具体含义。

“奖励”指的是推荐系统把推荐列表推送给用户之后，用户在这个列表之上的反馈。对于美团来说，“猜你喜欢”展位的核心优化指标是点击率和下单率，这里的奖励就可以被定义成推荐列表中物品的点击次数和下单次数的加权和。如下面的公式所示，其中的 wc 和 wp 分别是点击次数和下单次数的权重，这两个超参数可以根据你对它们的重视程度进行调整。

r=wc​∗∑Iclick​+wp​∗∑Ipay​

有了奖励的定义，那么整个强化学习过程的目标就很好定义了，就是多轮交互后的奖励期望之和最大，它的形式化表达如下所示：

你可以看到，公式中的 k 指的是交互的轮数，r 指的是我们在上面定义的奖励函数，Gamma 是一个 0-1 之间的损失系数。综合来看这个目标函数的含义就是，在状态 s 的基础上，采取动作 a 让之后 k 轮的奖励之和期望最大。在强化学习模型参数的更新过程中，美团就是根据这个目标函数，并且通过梯度下降的方式进行参数的更新。

然后是状态的定义，在推荐系统的强化学习模型中，状态实质上代表了用户的意图、兴趣和所处场景，所以它在整个搭建过程中非常重要。

那我们如何来表达这个要素呢？美团设计了图 3 所示的网络结构来提取状态的 Embedding 表达，这个状态 Embedding 就是当前推荐系统所处的状态。

这个网络主要分为两个部分：第一个部分是把用户实时行为序列的 Item Embedding 作为输入（就是翻页前点击下单等动作对应的物品），使用一维 CNN 层学习用户实时意图的表达，也就是图中的彩色部分；另一部分是传统的特征工程，它使用了特征值组成的稠密向量来表示用户所处的时间、地点、场景，以及使用兴趣 Embedding 来代表更长时间周期内用户的行为习惯。

图3 状态建模网络结构

这里比较有意思的是第一个部分，它采用了单层 CNN，也就是卷积神经网络来学习用户的实时意图表达，这是我们之前没有遇到过的处理手法。

那它是怎么做的呢？它首先把用户交互获得的 Item Embedding 组成了一个矩阵，然后在这个矩阵之上使用了两个不同尺度的卷积层，再对卷积层中的每个行向量进行池化操作，生成两个代表了用户实时意图的 Embedding，把它们与第二部分的场景特征向量连接后，再经过全连接层，生成最终代表状态的 State Embedding。

强化学习这 6 大要素是什么、怎么实现，我们已经搞清楚了，但我们之前还留了一个问题，就是智能体部分的强化学习模型的结构到底什么样。下面，我们就来解决这个疑问。图 4 就是这个强化学习模型的框图。

我们看到，这个模型跟强化学习模型 DRN 一样，都采用了 DQN 的模型框架，也都分成了两个部分，一个部分叫做 Advantage 函数部分 A(s,a)，它与状态、动作都相关，这里面的 s 就是状态，a 就是动作，另一部分叫做 Value Net，它只与状态相关。最终的模型输出得分 Q(s,a) = V(s) + A(s,a)，就是把这两部分的得分融合起来。

图4 美团强化学习模型

这个框图中的状态网络是预训练好的，也就是说在线上推荐的过程中，状态网络是不会进行在线学习的，那么强化学习动态学习的参数到底是什么呢？它学习的其实是 V(s) 和 A(s,a) 这两部分的融合参数，以及跟 Action 相关的参数。

V(s) 和 A(s,a) 这两个函数的具体细节，美团并没有披露，但是一种典型的做法就是采用便于学习的线性模型来构建这两个函数。比如，对于 V(s) 这部分来说，输入是状态 Embedding 向量，那么我们就可以使用一个 LR 的结构生成 Value Net 的得分。

同理，A(s,a) 也一样，我们可以通过另一个 LR 结构把状态向量和动作相关的特征综合起来，生成一个 Advantage 函数得分。最后，我们再通过加权和的方式把 Value Net 得分和 Advantage 函数得分加起来。

总的来说，整个美团强化学习方案的执行过程可以分成 4 步：

根据历史数据预训练 State Embedding 网络，初始化 Value Net 和 Advantage 函数的相关参数；

在用户打开 APP 时，根据初始化模型来生成推荐列表；

随着用户的不断交互，产生了一些实时的交互数据，这些数据经过 State Embedding 网络的在线推断过程，生成用户的实时 State Embedding，从而实时地影响用户每一轮交互的结果；

State Embedding 网络不进行在线更新，Value Net 和 Advantage 函数部分进行在线学习来更新相关参数，让强化学习框架具备实时学习的能力。

以上就是美团强化学习方案的理论部分，但是我们知道，强化学习落地最难的地方在于工程与模型的紧密融合，那下面就让我们看一看美团是如何落地这套方案的。

美团强化学习的工程架构

图 5 就是美团强化学习方案相关的工程架构。从整体上看，它确实像一整个推荐系统的框图，跟我们的 SparrowRecys 项目一样，包含了线上（Online）部分、近线（Nearline）部分和离线（Offline）部分。其中的模块也非常复杂，包含了数据流、模型训练、模型服务、线上推荐等多个模块。

虽然它复杂，但你也不用着急，它里面每个模块之间都有很强的逻辑关系。接下来，你就和我随着数据的整体流向，一起来过一遍这整个架构图吧。

图5 实时更新的强化学习框架

我们从最上方的 Log Collector 开始，它收集了推荐系统产生的各种日志，包括曝光、点击、下单，以及与这些行为相关的特征。

这些数据经过消息缓存系统 Kafka 收集之后，会形成数据流来到 Online Joiner 模块，那么 Online Joiner 正是从 Kafka 的数据流中实时收集特征和用户反馈，处理成包含了特征和标签的样本，再分别把样本数据输出到下游的 Kafka 和离线的 HDFS，来分别支持模型的在线和离线更新。

对于在线部分，Kafka 中的数据继续流向了一个叫 Experience Collector 的模块。这个模块是对样本做进一步处理，主要是把离散开的一个个样本点做进一步的整合，生成 App 中展现的推荐列表，然后以列表的形式“喂”给模型进行训练。

到这里，数据流的部分我们就基本讲完了，熟悉 Flink 的同学肯定会说，这部分基于数据流的处理过程不就是 Flink 最擅长的吗？没错，Online Joiner、Experience Collector 这些模块确实是最适合运行在 Flink 这类流处理平台之上。虽然美团没有具体透漏他们的流处理平台，但我们只需要知道这些近线的数据流操作都会运行在 Flink、Spark Streaming 这类流处理平台就可以了。

再说回到整个流程中，生成好的样本数据就来到了训练这一环节。美团使用了 TensorFlow 作为深度学习模型的训练平台。

当然，整个模型的训练也分为两部分。一部分是离线的训练，刚才我们提到的 State 网络、Item Embedding 等都是通过离线训练生成的。另一部分需要在线训练，比如 Advantage 和 Value 函数相关的参数，这部分并不是在 TensorFlow 中训练的，而是利用实时数据流中的样本进行在线学习，并实时调整相关参数值。

虽然 TensorFlow 的深度模型部分是在离线预训练好的，但是深度学习模型的线上服务效率问题一直是工程的难点。那美团是怎么解决的呢？和我们的项目一样，美团同样采用了 TensorFlow Serving 作为模型服务的方式，但它进行了 2 点优化。

首先是剥离 State 模型的 Embedding 层，它采取预训练的方式生成 Item Embedding，减小模型的整体体积，方便上线。这些预训练的 Embedding 会预存在内存数据库 Tair 中。Tair 的具体使用方法你不用特别关心，只要知道它是类似 Redis 的 Key-value 内存数据库就可以了。

其次是优化了模型更新过程。你可以看到 Version Controller 的模块，它是负责深度学习模型的版本管理的，在训练好一个新的模型之后，就需要把新模型进行上线。

但是在模型上线的时候，TensorFlow Serving 往往会在一两分钟内，产生响应时间骤然增加的现象，产生这种现象的原因主要有两点：一是 TensorFlow Serving 的模型加载和请求共用一个线程池，导致切换模型阻塞请求的处理；二是 TensorFlow 模型的计算图初始化要等到新模型接收第一次请求后才开始。

美团分别针对这两点进行了优化：针对第一点，切分了模型加载部分和请求处理部分的线程池，使之互不干扰；针对第二点，采用了 warm up，也就是预热初始化的方式进行解决，在真实请求到来之前，先用一些预热的请求让模型完成计算图加载的过程。

到这里，我们就讲完了美团的整个强化学习解决方案，从数据流的产生，近线的数据处理，到离线的模型训练，在线的模型学习更新、模型服务，强化学习需要整个系统通盘配合，才能够完成落地。通过今天的讲解，我相信你对强化学习的落地难点应该有了更深刻的理解，如果之后遇到这样的考验，也希望你能好好利用这些内容。

小结

这节课我们一起学习了美团的强化学习方案。

在美团的建模方法中，我们要关注强化学习的六要素。其中，智能体指的是美团的强化学习推荐模型，环境是“猜你喜欢”这个推荐场景，行动指的是强化学习模型生成的推荐列表，奖励是由点击和下单共同组成的奖励函数，目标是最大化多轮交互过程的奖励综合，状态是由深度学习网络生成的状态 Embedding。

在工程落地方案中，我们要清楚整个数据流的流向。推荐系统产生日志以后，它首先会被日志流系统 Kafka 收集，然后依次流经 Online Joiner 模块进行数据处理，生成训练样本，再由 Experience Collector 生成推荐列表样本，接着 TensorFlow 模型会根据列表样本进行离线训练，以及 Advantage 函数和 Value 函数利用相关参数进行在线学习，最后，等 TensorFlow 模型训练完成后通过 TensorFlow Serving 进行模型更新和服务。

同时，针对 TensorFlow Serving 延迟大，更新时效率低的特点，美团也采取了很多改进措施，比如剥离 Embedding 层、切分线程池，以及模型预热等等。

课后思考

你觉得我们在离线训练中使用的随机梯度下降之类的方法能用到在线学习上吗？除此之外，你还知道哪些在线学习的方法？

欢迎把你的经验和思考写在留言区，我们下节课见！

33｜技术权衡：解决方案这么多，哪个最合适？

你好，我是王喆。

在实际的工作中，我们经常会面临一些技术上的抉择。比如说，在设计推荐系统的时候，我们是应该用模型 A 还是用模型 B，是用 TensorFlow 还是用 PyTorch，是用 Redis 还是用 EVCache 呢？从理论上来说，其实选择哪个方案都可以，但在工程落地中，不同的方案往往对系统整体的开销，整个团队的工作量，甚至最终的推荐效果都有着非常大的影响。

我想这也是很多算法工程师的困惑：在工程落地环节，解决方案这么多，我们到底该选哪个？

今天，我们就一起来探讨一下技术权衡的问题，看看能不能在理论知识和工程落地之间找到一条最优的路径。

工程师职责的本质

“工程”和“理论”之间的权衡是所有工程师都要考虑的问题，对这个问题的思考方式决定了你具备的是“工程思维”还是“研究思维”，抑或是“学生思维”。推荐系统是一个工程性极强，以技术落地为首要目标的领域，因此，“工程思维”对推荐系统工程师是最重要的。

事实上，无论是算法工程师，还是研发工程师，甚至是设计电动汽车、神舟飞船、长征火箭的工程师，他们的职责都相同，那就是在现有实际条件的制约下，以工程完成和技术落地为目标，寻找并实现最优的解决方案。这里面有一个词最关键，那就是“制约”。我们该怎么理解这个制约呢？

比如说，在机器学习的理论层面，一切事情都是理想化的，就像模型结构是精确定义的，训练过程是严格推导的，但在实际条件下，一个模型的训练过程会受算力条件、数据质量、工程时间限制等多重条件的制约。这也是我们不得不面临诸多选择和权衡的原因。

再比如说，我有些同学在航天领域工作，最近趁着嫦娥五号任务圆满完成的热点，我咨询了他们一个问题，说“咱们国家什么时候能实现载人登月啊？”他们这帮航天工程师的回答也很严谨，说“制约咱们国家载人登月的主要是火箭的运力问题，现在的长征五号火箭，近地轨道运载能力是 25 吨，美国阿波罗登月计划使用的土星五号火箭的轨道运载能力是 140 吨，整整差了 5 倍多。”

图1 土星五号和长征5号

你看，所有的工程师都在受客观条件的制约，那怎么办呢？他们又讲了，要么就是一步步来，先攻克运载火箭技术，再像阿波罗计划一样，一发火箭把登月舱、返回舱、航天员等等一起送到月球；要么就是用现有的长征五号，发射四到五枚火箭，在太空中完成登月舱、返回舱、载人飞船的组装。

第一个计划的优势是一次性搞定，但周期长，需要等待火箭技术的攻克。第二个计划的优势是现在就可以着手开始做，但因为要发射四到五枚火箭，整个任务的容错率低，失败的风险大。

所以你看，航天工作者们发射火箭也成天抱怨运力不够，就像我们训练模型总说算力不够一样。那怎么办，日子就不过了吗？当然不是，我们工程师就是要在这样的制约条件下，又快又好地解决问题、完成任务，这就是你的职责所在。

那推荐系统中“现有实际条件的制约”都有什么呢？我们经常会遇到这 3 种：

软硬件环境的制约

研发周期的制约

实际业务逻辑和应用场景的制约

正是因为有这些制约的存在，一名工程师不可能像学术界的研究者一样不断尝试新的技术，做更多探索性的创新，也正是因为工程师永远以“技术落地”为目标，而不是炫耀自己的新模型、新技术是否走在业界前沿，所以在前沿理论和工程现实之间做权衡是一名工程师应该具有的基本素质。

那这个技术权衡具体该怎么做呢？下面，我就借用三个实际的案例帮助你体会一下。

Redis 容量和模型上线方式之间的权衡

对线上推荐系统来说，为了进行在线服务，需要将特征和模型参数存储到线上数据库或者线上服务器内存中。为了保证这两部分数据的实时性，很多公司采用内存数据库的方式实现，就像我们在第 10 讲中讲的一样，Redis 这类内存数据库自然是主流的选择。

但 Redis 需要占用大量内存资源，而内存资源相比存储资源和计算资源又比较稀缺和昂贵的资源。因此，无论是 AWS（Amazon Web Services，亚马逊网络服务平台）、阿里云，还是自建数据中心，实现 Redis 模块的成本都比较高，自然 Redis 的容量就成了制约推荐系统模型上线方式的关键因素。

在这个制约因素下，我们要考虑两方面的事情：

模型的参数规模要尽量小。特别是对深度学习推荐系统而言，模型的参数量级较传统模型有了几个量级的提升，所以我们更应该着重考虑模型的参数规模；

因为要考虑线上预估延迟和特征存储空间有限的情况，所以线上预估所需的特征数量不能无限制地增加，要根据重要性做一定的删减。

因此，在实际上线推荐系统的时候，我们必然需要进行一定的取舍：舍弃一些次要的要素，关注主要的矛盾。具体怎么做呢？这里，我结合自己的经验，把整个取舍的思考过程做了梳理，一共可以分成四步，你可以作为参考。

首先，对于千万量级甚至更高量级的特征向量维度（理论上模型参数的数量级也在千万量级）来说，因为线上服务很难支持这种级别的数据量，所以我们在上线模型时关注模型的稀疏性和复杂度，通过舍弃一定的模型预测准确度来换取上线的速度和资源消耗。

明确了工程权衡的目标，我们就要思考怎么提高模型的稀疏性，降低模型的复杂度。提高稀疏性的常见方法是，加入 L1 正则化项，或者采用 FTRL 这类稀疏性强的训练方法让大部分参数归零，从而减少模型体积。

对于降低复杂度，我们可以通过减少神经网络的层数，以及每层内神经元的数量来实现。当然，这个方法只有在不明显降低模型效果的前提下，才是可行的工程策略。

在明确了多种备选方案之后，如果还是无法确定哪种技术效果更佳，我们就需要实现所有备选方案以及方案间的各种组合，进行离线和线上的效果测试。

最后，我们要根据测试数据确定最终的技术途径、技术方案，完成最终上线。

以上就是模型侧的“瘦身”方法，在线特征的“瘦身”方法当然也可以采用同样的思路。首先采用“主成分分析”等方法进行特征筛选，在不显著降低模型效果的前提下减少所用的特征。针对不好取舍的特征，进行离线评估和线上 A/B 测试，最终达到工程上可以接受的水平。

研发周期和平台迁移之间的权衡

除了硬件条件的限制，研发周期的制约同样是不可忽视的因素。这就需要工程师对于项目有整体的把控，以及对研发周期有预估。在产品迭代日益迅速的互联网领域，没人愿意成为拖累整个团队的最慢的一个环节。接下来，我就以一个平台迁移的例子，来给你讲一讲如何在研发周期的制约下完成技术决策。

比如，公司希望把机器学习平台从 Spark MLlib 整体迁移到 TensorFlow 上，毫无疑问，这是顺应深度学习浪潮的技术决策，是非常正确的决定。但由于 Spark 平台自身的特性，它的编程语言、模型训练方式都和 TensorFlow 有很大的差别，因此整个迁移必然要经历一个较长的研发周期。

我就经历过很多次公司产品和技术平台的大规模升级，很常见的情况是，在保证平台升级正常进行的同时，我们还需要兼顾日常的开发进度，去实现一些新的产品需求。这就是工程师需要做出权衡的时候了，也是最考验工程师架构能力的时候。

图2 TensorFlow vs Spark

在这样的情况下，一般来说有 2 种可行的技术方案：

集中团队的力量完成 Spark 到 TensorFlow 的迁移，在新平台上进行新模型和新功能的研发。

让团队一部分成员利用成熟稳定的 Spark 平台，快速满足产品需求，为 TensorFlow 的迁移、调试、试运行留足充分的时间。同时，让另一部分成员全力完成 TensorFlow 的相关工作，尽量保证新平台的成熟和可靠。

不过，单纯从技术角度考虑，既然团队已经决定迁移到 TensorFlow 平台了，理论上就没必要再花时间利用 Spark 平台研发新模型，否则到时候还要进行二次开发。但是，再成熟的平台也需要整个团队磨合调试较长时间，绝不可能刚迁移 TensorFlow 就让它支持重要的业务逻辑。而且，技术平台的升级换代，应该作为技术团队的内部项目，最好对其他团队是透明的，不应该成为减缓对业务支持的直接理由。

因此，从工程进度和风险角度考虑，第 2 个技术途径更符合工程实际的需求。

冷启动等业务逻辑对推荐模型的制约

最后一类制约是来自业务逻辑，或者说应用场景的，最常见的如物品冷启动问题，这类业务问题往往制约了模型的更新和应用的方式。

之前，我们讲了很多种生成物品 Embedding 的方法，但在任何公司业务中，物品都不是一成不变的，就像视频网站要不断添加新视频，电商网站会不断加入新的商品。这个时候，新物品的 Embedding 该如何生成呢？

对于 Item2vec、DeepWalk 这类基于物品节点的 Embedding 模型来说，数据中必须包含新物品的 ID，才能够生成它相应的 Embedding。这就要求我们必须加快模型的更新速度，让模型尽快学习新物品的数据。这就是业务场景的特点在倒逼我们改进模型的实时性。这个时候，我们就又要思考改进 Embedding 模型实时性的方法了。

这里，我总结出了下面三个备选方案。

方案一：从模型训练 pipeline 的各个环节入手，看看哪些环节可以优化。比如，我们可以优化 Spark 数据预处理的过程，加快预处理速度，优化 Embedding 上线的过程，让 Embedding 从产生到上线的过程更紧凑。

方案二：从模型的复杂度入手，看一看能否在不显著伤及效果的前提下，通过降低模型的复杂度来降低训练时间，进而加快模型更新的速度。

方案三：跳出 End2End 训练 Embedding 模型的限制，看看能不能通过其他策略性的手段来生成临时性的新物品 Embedding。

其中，前两个方案需要我们从细节入手来优化工程上的实现，而第三个则要求我们有更灵活的处理思路。这一点也是我想和你多聊一聊的。

我们在改进推荐系统的时候，不能总陷进机器学习的固定思维里，认为一定要用一些机器学习模型或者深度学习网络去解决问题，我把这样的思维叫做“技术洁癖”。我们应该清楚的是工程师的第一任务是解决问题，而不是搭一个精致好看的技术玩具，所以在推荐模型的基础上，用一些策略性的手段来修补一些边界情况，“bad case”是可以去尝试的。

回到这个物品 Embedding 冷启动的例子上，策略性地生成新物品的 Embedding 就是很好的补充方式，房屋短租行业的巨头 Airbnb 就给我们提供了一个很好的参考例子。

在 Airbnb 平台上，如果一个新的出租屋发布了，但模型还没有学到它的 Embedding，Airbnb 的推荐系统会怎么做呢？它会通过三个步骤生成新出租屋的 Embedding。

第一步根据新房屋的属性，比如租金价格、房屋类型、面积、几房几厅等特征，来找到和它相似的一批房屋；第二步，在这些相似房屋中找到离它最近的三个；第三步，通过平均这三个最近房屋的 Embedding 来生成新房屋的 Embedding。

就像图 3 展示的这样，颜色相近的点代表着 Embedding 非常接近的房屋，如果一个新房屋落在了图中的某个位置，Airbnb 就可以根据它的地理位置和属性，找到相近的房屋并且给它涂上相似的颜色。

图3 Airbnb房屋聚类的例子

所以我们经常会看到，一些推荐系统中的补充策略往往可以高效地解决一些推荐模型无法解决的棘手问题。

当然，在我们日常工作中技术间的权衡无时无刻不在发生，小到一个变量取什么名字好，一行代码应该怎么写，大到一个平台应该怎么重构，一个模型应该如何构建。我们这节课的例子当然无法囊括所有的情况，只是希望能帮助你建立起正确的进行技术权衡的思路，做到抓住主矛盾点，做出有利于主要目标的取舍。

小结

这节课，我们通过三个例子一起探讨了工程落地中技术权衡的问题，希望能帮助你进一步加深了对工程师思维本质的理解。

事实上，对任何领域来说，工程师思维的本质都是“在现有实际条件的制约下，以工程完成和技术落地为目标，寻找并实现最优的解决方案”。

在推荐系统领域，典型的制约条件有三类：“软硬件环境的制约”，“研发周期的制约”，以及“实际业务逻辑和应用场景的制约”。在这些制约条件下，我认为的技术方案间的权衡之道，就是“抓住主矛盾点，列出备选方案，通过分析对比，作出有利于主要目标的取舍。”

课后思考

这节课，我们提到了物品冷启动问题的解决方案，你觉得基于 Embedding，还有哪些好的冷启动解决方案呢？如果让你来解决的话，在实践中你会作出哪些取舍，倾向于哪种选择呢？

期待在留言区看到你的分享和思考，我们下节课见！

结束语｜深度学习时代需要什么样的推荐工程师？

你好，我是王喆。

今天是 2021 年的第一天，也是咱们课程的最后一讲，课程开始前，我要对你说一句，很荣幸能陪你一起度过这几个月的学习时间。

我相信，在这门课中，只要你认真学习、亲手实践，就一定能够搭建起深度学习推荐系统的知识框架，并且收获一套亲手实现的、包含工业级推荐系统各个模块的开源项目 SparrowRecSys。这是咱们这门课的目标，也是你作为推荐工程师对深度学习方向的一次探索。

但要想成为一名优秀的推荐工程师，这一路上，包括我自己，也还在不断地学习和前行着。虽然我不能给你一张魔法船票，让你直接到达技术专家的彼岸，但我希望能通过这门课给你一个坚实的基础和宽广的视野。

在这最后一课，我想结合自己近十年的从业经验，包括国内和硅谷互联网公司的工作经历，跟你谈一谈，这个时代到底需要什么样的推荐工程师，我们应该继续朝什么方向持续努力。

一名推荐工程师的“自我修养”

在我刚进入推荐系统这个行业的时候，我心中对这个行业的初步印象是，认为这个行业就是一群搞机器学习的研究者在不断地改进模型、训练模型和提高效果，所以，我卯足了劲要成为那个能提出新的模型架构，让全公司都采用我模型的“年轻人”。

当我工作了三年之后，我终于明白，模型的工作固然重要，但它仅仅是推荐系统的一个部分，还有其他很多重要的子系统、子模块可以进行深入研究。当我工作了五年之后，我又意识到，原来能够全盘考虑推荐系统的模型、工程、业务，对它们进行整体优化的工程师才是最优秀的那一拨人。

工作八年后，我开始思考一个优秀推荐工程师的“自我修养”到底是什么，换句话说就是他该具备什么能力。下面我就把我的思考分享给你。

我认为一名推荐工程师的技术能力基本可以拆解成 4 个维度：知识、工具、逻辑、业务。我们一一来看。

“知识”代表了推荐工程师应该具备的推荐系统和机器学习领域的相关知识，比如我在推荐模型篇中就强调过，我们一定要建立起自己的推荐模型演化框架，厘清各个模型之间的关系，从整体上建立起推荐系统的知识体系。

而“工具”指的是我们的工程实践能力，掌握推荐系统相关的工程实践工具的使用方法。我在这门课程中讲了很多推荐系统相关的工具、平台，包括 Spark、TensorFlow、Flink、Redis 等等，就是希望你能够具备推荐系统知识的落地能力，把脑海中的专业领域知识转化成实际的能够支持公司业务的推荐系统。

“逻辑”指的是我们作为一名程序员的基本功，包括算法和数据结构基础，以及梳理高质量系统框架的能力。像是我们在第一课中总结过的深度学习推荐系统架构图，以及在第 14 课、23 课这些实践章节中总结过的各种架构图，就是逻辑设计能力的体现。

至于“业务”，我想是推荐工程师跟其他后端工程师区别最大的地方了。在业务维度，推荐工程师需要把自己当成半个产品经理，从业务场景的角度来思考用户到底喜欢什么，他们的习惯到底什么样。只有这样，我们才能够不断发现改进模型的动机，持续地迭代模型。这一点，我想你通过 32 课中阿里模型的演进过程，肯定也有所体会。

推荐工程师能力模型

推荐工程师职位的面试要求

除了推荐工程师的能力维度，我还想跟你聊一聊推荐工程师职位的面试要求，因为这也是很多在校同学和刚入行的工程师们非常感兴趣的话题。

虽然，不同的招聘职位和职级对面试者的能力要求有所不同，但各个公司也不外乎是从“知识”、“工具”、“逻辑”、“业务”，这四个维度对面试者进行考察。接下来，我们就依次来看一看面试官都是怎么考察这四项能力的。

1. 对知识的考察

面试官最关心的就是你对这个领域的知识储备，以及你对一些技术细节的理解。一般来说，他们会按照由广入深，层层递进地方式来考察你的掌握程度。

比如说，在知识广度方面，面试官可能会让你说说主流的推荐模型有哪些，主流的 Embedding 方法有哪些，然后，再根据你的回答挑一到两点继续深入。

假设，你有提到你对 DIN 模型比较熟悉，那么接下来的问题可能是：

注意力机制具体指的是什么？

DIN 中注意力单元的具体结构是什么？

能否写出注意力单元的形式化定义，并推导它的梯度下降更新过程？

所以，在面试之前，对于你自己熟悉的领域，一定要深入理解每一个细节，切忌浅尝辄止，临时抱佛脚。

2. 对逻辑的考察

面试官对“逻辑思维能力”的考察方法就是做题，一般来说也会用两种方式。

一种是直接出算法面试题，让你写一种排序算法，完成二叉树的构建、遍历、反转等等操作，或者任意类似的 LeetCode 面试题。这是所有 CS 相关职位都会检查的，因为一名推荐工程师首先需要是一名合格的、基础牢固的工程师。

第二种方式是让你去设计一个推荐系统的工程架构，可能是根据某个推荐场景，设计一套合适的特征工程和推荐模型等等。这类系统设计题更能检查你严谨、全面的逻辑思考能力。

对于逻辑能力的积累是一个长期的过程，不仅要求你在学生阶段打牢自己 CS 相关的知识基础，而且最好能在工作中面临实际问题的时候，多思考系统设计的问题，即使这部分功能不属于你的工作范畴，你也可以多想一想有哪些可行的设计方案，不断锻炼自己系统设计的能力。

3. 对“工具”使用能力的考察

其实，对工具的考察跟公司职位的实际需求最相关，特别是一些比较高级的职位。

如果这个职位要求更强的大数据处理能力，面试官可能会考察你对 Spark 的运用情况，以及对 Spark 背后 Map Reduce 过程原理的理解情况；如果这个职位希望招一位对 TensorFlow，XGBoost 等建模工具应用熟练的同学，面试官就会重点考察你 TensorFlow 的基本概念，以及怎么处理训练过程中遇到的主要问题。

对于这部分，我的建议是，你在了解所有工具基本使用方法的同时，一定要注意一到两个工具的深入实践和积累，争取成为一两个方向的专家。这样，你才能在一些高级职位的竞争中脱颖而出。

4. 对“业务”理解能力的考察

如果说前三项能力是一名合格推荐工程师的必备条件，那么业务能力则是你在合格基础上变得优秀的宝贵素质。只有对业务有深刻理解，我们才能够从完成项目的阶段进化到发现项目、领导项目的阶段，所以越高的职位，对业务理解能力的考察就越深刻。

比如：对于 DIN 来说，最关键的点其实并不是用注意力机制来解决这个问题，而是发现用户行为历史和目标商品的相关性。对于 Wide&Deep 来说，最重要的也并不是这个模型结构，而是如何设计 Deep 特征和 Wide 特征，从而发挥出这个模型最大的“功力”；对于模型服务的部分，最重要的是如何设计它才既能满足业务需求，又能实现工程上的要求。

除此之外，推荐系统中遇到的一些常见问题，如冷启动、多目标优化、探索与利用，也是面试常问的业务问题，你必须能说出自己的思考，充分地分析具体问题，提出有针对性的解决方案。

“业务”方面的提高，虽然是一个长期的过程，但我还是要建议你，在平时的工作中，不要总是想着完成一个个工程任务，按部就班的执行上级的命令，而是多思考任务背后的逻辑，多提出自己的见解，这样才能真正的提高自己解决业务问题的能力。

总的来说，“知识”和“逻辑”是一名推荐工程师的基本素质，“工具”和“业务”是影响你能不能获得高端职位的关键。但不管你处于职业生涯的任何阶段，都要注意平时的不断思考和持续学习，从“广度”和“深度”两个方面，不断提升自己的综合能力，这样才能在职业生涯的阶梯上不断前行。

什么样的机器学习人才最紧缺？

清楚了能力要求，了解了面试形式，最后我们再展望一下未来，让我们预测一下接下来这几年，业界最需要怎样的机器学习人才。

说是预测，其实也不能说是十分准确，因为我跟业界的很多非常资深的工程师，技术 leader 探讨的过程中发现，大家对于这个问题的看法其实是惊人一致的。下面，让我们先来看看业界的技术专家们都是怎么说的。

首先是阿里的资深技术专家朱小强给我的书写序时的总结，他说：

技术发展将从依赖深度学习算法单点突破收割技术红利，开始转向更为复杂的、系统性的技术体系推进，进一步创造技术红利。这里，关键性的技术破局点是算法与系统架构的协同设计（algo-system co-design）。

所以朱小强的观点是，单纯依靠深度学习模型改进就能大幅提高效果的时代已经过去了，现在我们只有把算法和工程协同设计，才能取得进一步的突破。

第二位是前 Google、Facebook 高级工程师，现在的新浪微博广告核心技术负责人詹盈，他对这个问题的看法是：

成熟的算法工程师不应仅满足于基本的建模和调参，他们对于业务和产品有较为深刻的理解，并且在实践中能够拥有足够的数据分析能力去指导建模方案的完善，并确确实实地带来客观的业务收益。这一类高端的算法人才是真的稀缺，还远达不到“内卷”的程度。

可以说，詹盈的看法和我对算法工程师能力的总结高度一致：只有综合了算法能力、业务能力、工程能力，你才能成为一名高端的算法工程师。

最后是我的前老板，Hulu 的全球副总裁诸葛越的话，她说：

算法工程师这个群体普遍都会遇到的是，算法研究能力和工程能力的权衡问题。在其中任何一个方向上成为专家都没有问题，企业都会有合适的位置给到你。但如果你想走到更高的职级，就需要不断拓宽与现有工作相关的技术栈。随着近两年算法岗位的成熟，有一种说法重新被大家认同，那就是“算法工程师首先是一名工程师”，可见对算法工程师工程能力的重视已经是普遍的观点了。

越姐从一个技术管理者的角度表达了自己的观点，结论也和前几位工程师一样：只有算法研究能力和工程能力都突出的人，才会在现在愈加成熟的业界环境下取得更高的职位。

最后，我想用自己说的话再给这个问题做一个总结：未来 3-5 年业界最需要的是：能够站在机器学习“工程体系”的高度之上，综合考虑“业务特点”、“模型结构”、“工程限制”、“问题目标”的算法工程师。在算法工程师的发展之路上，切忌“好高骛远”，只追求“好看好听”的先进技术的学生思维，回到工程师思维上来，回到解决问题的本质上来，才能够逐渐成为一名优秀的算法工程师。

写在最后

好了，如果你学习到这里，我要告诉你，这是本门课程的最后一段话。这门课程，我们一直在聊技术、学实践，一直没有机会跟你聊一聊内心的话，所以借着最后的机会我想跟你“谈谈心”。

说实话，推荐工程师这条职业道路并不是一条容易的道路，同行们常说这是一个时刻处在淘汰边缘的职业，我们时刻面临着这样那样的挑战。比如说，当你在谈一个流行的技术点时，它已经进入了过时的倒计时；当你 coding 一天回家，还是勉为其难再多看一篇 paper 的时候，也许你也曾和我一样觉得不如就随波逐流算了；当你苦心作出一个模型的尝试却没有效果的时候，感觉自己的工作几乎是要从零开始。

但我还要说，这是一个充满魅力和激情的工作，它不是在简单地完成一个机械式的任务，而是用在你的观察、你的思考、你的智力，以及你的经验去实现一个个目标，当你真正突破这些目标的时候，我相信你会体会到比一道编程题 AC 大 100 倍的成就感，毫无疑问，它值得你为之付出一个长长的职业生涯。

这几个月里，你可能是在晚上回家打开笔记本继续学习，或者是在上班的路上听我的录音，又或者是在周末通过咱们的课程为自己充电，不管是用哪种方式学习，我都要说一声：“谢谢你的支持和信任”，能陪伴你走过这几个月的学习历程是我的荣幸。

今天的最后没有思考题，但如果你对这门课、这个职业，对未来和前途，有任何的感悟，我都欢迎你写下来，总结给自己看，也与这门课一同走过的同学们共勉。

好了，同学们，虽然我们的课程结束了，但我相信你的职业生涯从这里开始会更加精彩！

最后的最后，我还为你准备了一份毕业调查问卷，题目不多，希望你能花两分钟的时间填一下。一起走过了这些时间，期待听到你对我和这个课程的反馈和建议！

期末考试 | “深度学习推荐系统”100分试卷等你来挑战！

你好，我是王喆。

《深度学习推荐系统》这门课程到这里就正式完结了。在发布了结束语之后，我陆续收到了很多同学的留言和反馈，非常感谢你一直以来的认真学习和支持！

为了帮助你检验自己的学习效果，我特别给你准备了一套结课测试题。这套测试题共有 20 道题目，包括 12 道单选题和 8 道多选题，满分 100 分，核心考点都出自前面讲到的所有重要知识，希望可以帮助你进行一场自测。点击下面按钮，就可以开始测试啦。

除此之外，我还特意准备了一份问卷，想听听你的声音和反馈，以便我后面对课程进行更好地优化。同时，填写问卷还有机会获得礼物或者是课程阅码哦。