This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
<audio id="audio" title="28 | 业界经典YouTube深度学习推荐系统的经典架构长什么样" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/61/61/61d588cff12fd6709a50bf1cb0340c61.mp3"></audio>
你好,我是王喆。今天我们一起来开启前沿拓展篇的学习。
如果你是跟着课程安排学到这里的,我可以很自信地说,你几乎已经掌握了推荐系统的全部重点知识。从数据特征的处理,模型构建,到模型的评估和上线,再到推荐服务器的实现,你的知识广度已经覆盖了推荐系统领域的全部要点。但要想成为一名合格的推荐工程师,我们还需要做两件事情,一件是追踪前沿,另一件是融会贯通。
因此在这一篇中我会通过详细讲解几个一线大厂的推荐系统解决方案来帮你追踪行业的热点、创新点。它们既包括一些推荐模型的业界实现如YouTube和Pinterest的推荐模型也包括推荐系统的工程落地方案如Flink的经典应用和美团对于强化学习的落地方案。最后我还会对算法工程师的所需能力做一个全面的总结。
今天我们今天先来学习YouTube的经典深度学习推荐系统架构。YouTube这套深度学习解决方案已经经典到可以成为一个业界标杆式的方案了也是我在国内外和同学、同事们交流、讨论的时候经常会提到的方案。
话不多说,我们正式开始今天的学习吧!
## YouTube推荐系统架构
提起YouTube我想你肯定不会陌生作为全球最大的视频分享网站YouTube平台中几乎所有的视频都来自UGCUser Generated Content用户原创内容这样的内容产生模式有两个特点
- 一是其商业模式不同于Netflix以及国内的腾讯视频、爱奇艺这样的流媒体这些流媒体的大部分内容都是采购或自制的电影、剧集等头部内容而YouTube的内容都是用户上传的自制视频种类风格繁多头部效应没那么明显
- 二是由于YouTube的视频基数巨大用户难以发现喜欢的内容。
这样的内容特点简直就是深度学习推荐系统最适合扎根的土壤所以YouTube也是最早落地深度学习的一线公司。那YouTube的深度学习推荐系统架构长什么样呢
<img src="https://static001.geekbang.org/resource/image/47/05/47c0fa06ffc18912b027fe920ac30905.jpg" alt="" title="图1 YouTube推荐系统整体架构">
上图就是YouTube在2016年发布的推荐系统架构。我们可以看到为了对海量的视频进行快速、准确的排序YouTube也采用了经典的召回层+排序层的推荐系统架构。
**它的推荐过程可以分成二级。第一级是用候选集生成模型Candidate Generation Model完成候选视频的快速筛选在这一步候选视频集合由百万降低到几百量级这就相当于经典推荐系统架构中的召回层。第二级是用排序模型Ranking Model完成几百个候选视频的精排这相当于经典推荐系统架构中的排序层。**
无论是候选集生成模型还是排序模型YouTube都采用了深度学习的解决方案。下面就让我们详细讲讲这两个深度学习模型是如何构建起来的。
## 候选集生成模型
首先,是用于视频召回的候选集生成模型,它的模型架构如下图所示。
<img src="https://static001.geekbang.org/resource/image/69/cf/6968873184cf93194aa476398c2e35cf.jpg" alt="" title="图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”这个标签来代替“用户会不会点击视频”这个标签作为预测目标。事实上这跟候选集生成模型独特的线上服务方式紧密相关。
<img src="https://static001.geekbang.org/resource/image/e9/69/e9a20bc7260296e09078509e3f42df69.jpg" alt="" title="图3 模型服务部分示意图">
细心的同学可能已经留意到架构图左上角的模型服务Serving方法与模型训练方法完全不同。在候选集生成模型的线上服务过程中YouTube并没有直接采用训练时的模型进行预测而是采用了一种最近邻搜索的方法我们曾经在[第12讲](https://time.geekbang.org/column/article/301739)详细讲过基于Embedding的最近邻搜索方法不记得的同学可以先去回顾一下。
具体来说,在模型服务过程中,网络结构比较复杂,如果我们对每次推荐请求都端到端地运行一遍模型,处理一遍候选集,那模型的参数数量就会巨大,整个推断过程的开销也会非常大。
** 因此在通过“候选集生成模型”得到用户和视频的Embedding后我们再通过Embedding最近邻搜索的方法就可以提高模型服务的效率了。这样一来我们甚至不用把模型推断的逻辑搬上服务器只需要将用户Embedding和视频Embedding存到特征数据库就行了。**再加上可以使用局部敏感哈希这类快速Embedding查找方法这对于百万量级规模的候选集生成过程的效率提升是巨大的。
那么问题又来了这里的用户Embedding和视频Embedding到底是从哪里来的呢这个问题的答案就是候选集生成模型为什么要用视频ID作为多分类输出的答案了。我们再仔细看一下图2的架构架构图中从softmax向模型服务模块画了个箭头用于代表视频Embedding向量的生成。
由于最后的输出层是softmax而这个softmax层的参数本质上就是一个m x n维的矩阵其中m指的是最后一层红色的ReLU层的维度mn指的是分类的总数也就是YouTube所有视频的总数n。因此视频Embedding就是这个m x n维矩阵的各列向量。
这样的Embedding生成方法其实和word2vec中词向量的生成方法是相同的你也可以参考[第6讲](https://time.geekbang.org/column/article/295939)的内容来理解它。
清楚了视频Embedding的生成原理用户Embedding的生成就非常好理解了因为输入的特征向量全部都是用户相关的特征一个物品和场景特征都没有所以在使用某用户u的特征向量作为模型输入时最后一层ReLU层的输出向量就可以当作该用户u的Embedding向量。
在模型训练完成后逐个输入所有用户的特征向量到模型中YouTube就可以得到所有用户的Embedding向量之后就可以把它们预存到线上的特征数据库中了。
在预测某用户的视频候选集时YouTube要先从特征数据库中拿到该用户的Embedding向量再在视频Embedding向量空间中利用局部敏感哈希等方法搜索该用户Embedding向量的K近邻这样就可以快速得到k个候选视频集合。这就是整个候选集生成模型的训练原理和服务过程。
到这里你一定已经体会到了咱们前沿拓展篇案例分析的作用通过一个YouTube候选集生成模型的原理分析我们就已经把第6讲的Embedding、[第10讲](https://time.geekbang.org/column/article/299326)的特征数据库、第12讲的局部敏感哈希以及[第17讲](https://time.geekbang.org/column/article/309846)的Embedding MLP模型都回顾了一遍。
如果你喜欢这种通过学习业界实践方案,把知识串联起来的方式,可以给我留言反馈,我也会在之后的课程中多采用这样的方式。
## 排序模型
通过候选集生成模型YouTube已经得到了几百个候选视频的集合了下一步就是利用排序模型进行精排序。下图就是YouTube深度学习排序模型的架构我们一起来看一看。
<img src="https://static001.geekbang.org/resource/image/28/1a/28e0acbb64760670ee94d015025da81a.jpg" alt="" title="图3 YouTube的深度学习排序模型的架构">
第一眼看上去你可能会认为排序模型的网络结构与候选集生成模型没有太大区别在模型结构上确实是这样的它们都遵循Embedding MLP的模型架构。但是我们来看其中的细节特别是输入层和输出层的部分它们跟候选集生成模型还是有很大不同的这就是我们要重点关注的。
我们先看输入层相比于候选集生成模型需要对几百万候选集进行粗筛排序模型只需对几百个候选视频进行排序因此可以引入更多特征进行精排。具体来说YouTube的输入层从左至右引入的特征依次是
1. impression video ID embedding当前候选视频的Embedding
1. watched video IDs average embedding用户观看过的最后N个视频Embedding的平均值
1. language embedding用户语言的Embedding和当前候选视频语言的Embedding
1. time since last watch表示用户上次观看同频道视频距今的时间
1. #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要达成的商业目标。
好了,这些关键知识点,我也总结在了下面的表格中,希望它能帮助你加深记忆。
<img src="https://static001.geekbang.org/resource/image/60/2e/60509d00835ac95ayya3bdeb17f0532e.jpeg" alt="">
在这节课结束前关于YouTube的推荐模型我还想多说几句。事实上YouTube的推荐系统论文中还包含了更多的细节业界真正好的论文并不多YouTube的这篇《Deep Neural Networks for YouTube Recommendations》绝对是不可多得的一篇我甚至推荐大家逐句来读抓住每一个细节。
当然,你也可以在我的书《深度学习推荐系统》中的相应章节找到更多的实现细节。这些内容让我曾经受益匪浅,相信也会对你有所帮助。
## 课后思考
YouTube的排序模型和候选集生成模型都使用了平均池化这一操作来把用户的历史观看视频整合起来。你能想到更好的方法来改进这个操作吗
期待在留言区看到你的思考和总结,我们下节课见!

View File

@@ -0,0 +1,123 @@
<audio id="audio" title="29 | 图神经网络Pinterest是如何应用图神经网络的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d7/ae/d72b01f17b2e18bbd2bab18e291e05ae.mp3"></audio>
你好,我是王喆。
互联网中到处都是图结构的数据,比如我们熟悉的社交网络,最近流行的知识图谱等等,这些数据中包含着大量的关系信息,这对推荐系统来说是非常有帮助的。
为了能更好地利用这些信息进行推荐各大巨头可谓尝试了各种办法比如我们之前学过的DeepWalk、Node2Vec这些非常实用的Graph Embedding方法。但是技术的发展永无止境最近两年GNNGraph Nerual Netwrok图神经网络毫无疑问是最火热、最流行的基于图结构数据的建模方法。严格一点来说图神经网络指的就是可以直接处理图结构数据的神经网络模型。
在诸多GNN的解决方案中著名的社交电商巨头Pinterest对于GraphSAGE的实现和落地又是最为成功的在业界的影响力也最大。所以这节课我们就学一学GraphSAGE的技术细节看一看Pinterest是如何利用图神经网络进行商品推荐的。
## 搭桥还是平推?技术途径上的抉择
在正式开始GraphSAGE的讲解之前我想先给你讲一讲DeepWalk、Node2vec这些Graph Embedding方法和GNN之间的关系这有助于我们理解GNN的原理。
我们这里简单回顾一下DeepWalk和Node2vec算法的基本流程如下面的图1所示。它们在面对像图1b这样的图数据的时候其实没有直接处理图结构的数据而是走了一个取巧的方式先把图结构数据通过随机游走采样转换成了序列数据然后再 用诸如Word2vec这类序列数据Embedding的方法生成最终的Graph Embedding。
<img src="https://static001.geekbang.org/resource/image/1f/ed/1f28172c62e1b5991644cf62453fd0ed.jpeg" alt="" title="图1 基于随机游走的Graph Embedding算法">
我把这类Graph Embedding的方法归类为基于随机游走的间接性Graph Embedding方法。它其实代表了我们在解决一类技术问题时的思路**就是面对一个复杂问题时,我们不直接解决它,而是“搭一座桥”,通过这座桥把这个复杂问题转换成一个简单问题,因为对于简单问题,我们有非常丰富的处理手段。这样一来,这个复杂问题也就能简单地解决了。**显然基于随机游走的Graph Embedding方法就是这样一种“搭桥”的解决方案。
但搭桥的过程中难免会损失一些有用的信息,比如用随机游走对图数据进行抽样的时候,虽然我们得到的序列数据中还包含了图结构的信息,但却破坏了这些信息原始的结构。
正因为这样很多研究者、工程师不满足于这样搭桥的方式而是希望造一台“推土机”把这个问题平推过去直接解决它。GNN就是这样一种平推解决图结构数据问题的方法它直接输入图结构的数据产生节点的Embedding或者推荐结果。当然不同研究者打造这台推土机的方式各不相同我们今天要重点介绍的GraphSAGE就是其中最著名的一台也最具参考价值。
## GraphSAGE的主要步骤
下面我们就来详细讲一讲GraphSAGE的细节。GraphSAGE的全称叫做Graph Sample and Aggregate翻译过来叫“图采样和聚集方法”。其实这个名称就很好地解释了它运行的过程就是先“采样”、再“聚集”。
这时候问题又来了这里的“采样”还是随机游走的采样吗要是还通过采样才能得到样本我们造的还能是“推土机”吗不就又变成搭桥的方式了吗别着急等我讲完GraphSAGE的细节你就明白了。
<img src="https://static001.geekbang.org/resource/image/c1/25/c1d474d94b5d1dd56591ef55d58d5d25.jpeg" alt="" title="图2 GraphSAGE的主要过程 [br](出自论文 Inductive Representation Learning on Large Graphs">
GraphSAGE的过程如上图所示主要可以分为3步
1. 在整体的图数据上从某一个中心节点开始采样得到一个k阶的子图示意图中给出的示例是一个二阶子图
1. 有了这个二阶子图我们可以先利用GNN把二阶的邻接点聚合成一阶的邻接点图1-2中绿色的部分再把一阶的邻接点聚合成这个中心节点图1-2中蓝色的部分
1. 有了聚合好的这个中心节点的Embedding我们就可以去完成一个预测任务比如这个中心节点的标签是被点击的电影那我们就可以让这个GNN完成一个点击率预估任务。
这就是GraphSAGE的主要步骤你看了之后可能还是觉得有点抽象。那接下来我们再结合下图3推荐电影的例子来看一看GraphSAGE是怎么工作的。
<img src="https://static001.geekbang.org/resource/image/2c/b3/2c4791fe07870eef1d4bcb6abe71e9b3.jpeg" alt="" title="图3 GraphSAGE示例">
首先我们要利用MovieLens的数据得到电影间的关系图这个关系图可以是用用户行为生成这个方法我们在[第7讲](https://time.geekbang.org/column/article/296672)中讲过),它也可以是像生成知识图谱一样来生成,比如,两部电影拥有同一个演员就可以建立一条边,拥有相同的风格也可以建立一条边,规则我们可以自己定。
在这个由电影作为节点的关系图上我们随机选择一个中心节点。比如我们选择了玩具总动员Toy Story作为中心节点这时再向外进行二阶的邻接点采样就能生成一个树形的样本。
经过多次采样之后我们会拥有一批这样的子图样本。这时我们就可以把这些样本输入GNN中进行训练了。GNN的结构我会在下一小节详细来讲这里我们只要清楚这个GNN既可以预测中心节点的标签比如点击或未点击也可以单纯训练中心节点的Embedding就够了。
总的来说,**GraphSAGE的主要步骤就是三个“抽样-聚合-预测”**。
## GraphSAGE的模型结构
现在我们关注的重点就变成了GraphSAGE的模型结构到底怎么样它到底是怎么把一个k阶的子图放到GNN中去训练然后生成中心节点的Embedding的呢接下来我就结合GraphSAGE的模型结构来和你详细讲一讲。
这里我们还是以二阶的GraphSAGE为例因为超过二阶的结构只是进一步延伸这个模型没有更多特别的地方所以我们理解二阶的模型结构就足够了。
<img src="https://static001.geekbang.org/resource/image/e8/c0/e8d640076241bd415da8b782f8c256c0.jpg" alt="" title="图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讲](https://time.geekbang.org/column/article/295939)中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讲](https://time.geekbang.org/column/article/303641)中实现的功能一样就是商品的相似推荐。只不过之前业界更多地使用Item2vec、DeepWalk这些方法来生成用于相似推荐的物品Embedding在GNN流行起来之后大家就开始尝试使用GNN生成的物品Embedding进行相似推荐。
那么PinSAGE在Pinterest场景下的效果到底怎么样呢Pinterest给出了一些例子如图5所示我们可以判断一下。
<img src="https://static001.geekbang.org/resource/image/9c/84/9c694cc8c041c237e541d64d9e370684.jpg" alt="" title="图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。
为了方便你及时回顾,我也把这节课中的重要知识点总结了下面的表格中,你可以看看。
<img src="https://static001.geekbang.org/resource/image/13/b9/1300b834c6c356e8fbbe17d69d1b1db9.jpeg" alt="">
## 课后思考
使用GraphSAGE是为了生成每个节点的Embedding那我们有没有办法在GraphSAGE中加入物品的其他特征如物品的价格、种类等等特征让最终生成的物品Embedding中包含这些物品特征的信息呢
期待在留言区看到你对GraphSAGE的思考我们下节课见

View File

@@ -0,0 +1,139 @@
<audio id="audio" title="30 | 流处理平台Flink是如何快速识别用户兴趣实现实时推荐的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/49/62/49693d204eb1b2104b6501a892617562.mp3"></audio>
你好,我是王喆。
刚刚结束的2020年双11活动让技术圈出现了一个非常劲爆的新闻就是阿里基于Flink实现了数据的批流一体处理每秒能够处理40亿条的巨量数据。这也是业界首次在这么大规模的数据洪峰之上实现数据流的实时处理。
也正是因为实时数据流处理功能的实现让阿里的推荐系统引擎能够在双11期间做出更快速的反应实时抓住用户的兴趣给出更准确的推荐。
这节课我就带你揭开阿里使用流处理平台Flink的面纱来重点解决这3个问题
- 为什么说实时性是影响推荐系统效果的关键因素?
- 到底什么是批流一体的数据处理体系?
- 业界流行的Flink到底是怎么实现数据流处理的
## 为什么实时性是影响推荐系统效果的关键因素?
周星驰的电影《功夫》里有一句著名的台词“天下武功,无坚不摧,唯快不破”。如果说推荐模型的架构是那把“无坚不摧”的“玄铁重剑”,那么推荐系统的实时性就是“唯快不破”的“柳叶飞刀”。那这把柳叶飞刀到底是怎么发挥作用的呢?我说个切身的场景你就明白了。
假设,你正在手机上刷抖音,你刚刚看了一个精彩的足球进球视频,感觉意犹未尽,还想看更多这样的视频。这个时候,抖音的推荐系统肯定不会让你失望,它很快会接收到“你观看了精彩进球视频”的信号,快速地抓到你的兴趣点,然后,它会迅速做出反馈,给你推送更多类似的视频。
那我们也可以试想一下,如果抖音的推荐系统实时性不够的话,会发生什么呢?可能是你看完了精彩进球的视频之后,推荐系统还跟什么都没发生一样,依然按部就班地给你推荐一些原本就设定好的不相关视频。如果是这样的话,抖音又怎么能让你欲罢不能,刷了又想刷呢?
这个例子就充分说明了,**推荐系统只有拥有实时抓住用户新兴趣点的能力,才能让你的用户“离不开你”**。
## 什么是批流一体的数据处理体系?
那作为推荐系统工程师,我们就要思考,到底怎样才能实现用户兴趣的快速提取呢?这就不得不提到推荐系统的数据体系。
我们之前讲的数据处理无论是数据的预处理还是特征工程大部分是在Spark平台上完成的。Spark平台的特点是它处理的数据都是已经落盘的数据。也就是说这些数据要么是在硬盘上要么是在分布式的文件系统上然后才会被批量地载入到Spark平台上进行运算处理这种批量处理大数据的架构就叫做批处理大数据架构。它的整体结构图如图1所示。
<img src="https://static001.geekbang.org/resource/image/15/78/153c832b255ce750537698fc5866a878.jpg" alt="" title="图1 传统批处理大数据架构">
但批处理架构的特点就是慢数据从产生到落盘再到被Spark平台重新读取处理往往要经历几十分钟甚至几小时的延迟。如果推荐系统是建立在这样的数据处理架构上还有可能实时地抓住用户的新兴趣点吗肯定是没有希望了。
那怎么办呢?我们能不能在数据产生之后就立马处理它,而不是等到它落盘后再重新处理它呢?当然是可以的,这种在数据产生后就直接对数据流进行处理的架构,就叫做流处理大数据架构。
我们从图2的流处理架构示意图中可以看到它和批处理大数据架构相比不仅用流处理平台替换掉了分布式批处理Map Reduce计算平台而且在数据源与计算平台之间也不再有存储系统这一层。这就大大提高了数据处理的速度让数据的延迟可以降低到几分钟级别甚至一分钟以内这也让实时推荐成为了可能。
<img src="https://static001.geekbang.org/resource/image/dc/b1/dc38fbc4e98fc348a3a6bd58d2bc51b1.jpg" alt="" title="图2 流处理大数据架构">
但是,流处理平台也不是十全十美的。由于流处理平台是对数据流进行直接处理,它没有办法进行长时间段的历史数据的全量处理,这就让流处理平台无法应用在历史特征的提取,模型的训练样本生成这样非常重要的领域。
那是不是说根本就没有能够同时具有批处理、流处理优势的解决方案吗当然是有的这就是我们在一开始说的批流一体的大数据架构其中最有代表性的就是Flink。
如下图3所示**批流一体的大数据架构最重要的特点,就是在流处理架构的基础上添加了数据重播的功能**。
我们怎么理解这个数据重播功能呢?它指的是在数据落盘之后,还可以利用流处理平台同样的代码,进行落盘数据的处理,这就相当于进行了一遍重播。这样不就实现了离线环境下的数据批处理了吗?而且由于流处理和批处理使用的是一套代码,因此完美保证了代码维护的一致性,是近乎完美的数据流解决方案。
<img src="https://static001.geekbang.org/resource/image/7c/7b/7c6a35831867bfyy3140b619616d7c7b.jpg" alt="" title="图3 批流一体大数据架构">
既然批流一体的大数据架构这么完美,为什么我们很少听说有实现这套方案的公司呢?以我个人的实践经验来看,这主要是因为它实现起来有下面两个难点。
- 大批成熟的互联网公司已经在Spark等批处理平台上构建起了整套的数据体系要想完全迁移到批流一体的数据体系上有着非常沉重的技术负担。
- 批流一体的解决方案还很理想化,因为我们在实际处理特征的时候,很难让批处理和流处理完全共享一套代码。
比如,我们在流处理中可以很方便地计算出点击量、曝光量这类方便累计的指标,但如果遇到比较复杂的特征,像是用户过去一个月的平均访问时长,用户观看视频的进度百分比等等,这些指标就很难在流处理中计算得到了。这是因为计算这类特征所需的数据时间跨度大,计算复杂,流处理难以实现。
因此,**在对待流处理平台时,我们的态度应该是,取其所长**。更具体点来说就是,在需要实时计算的地方发挥它的长处,但也没有必要过于理想主义,强调一切应用都应该批流一体,这反而会为我们增加过多的技术负担。
## Flink是如何处理数据流的
现在我们已经清楚流处理平台的特点和优势了但Flink平台到底是怎么进行流数据处理的呢
我们先来认识Flink中两个最重要的概念数据流DataStream和窗口Window。数据流其实就是消息队列从网站、APP这些客户端中产生的数据被发送到服务器端的时候就是一个数据消息队列而流处理平台就是要对这个消息队列进行实时处理。
就像下图4所示的那样上面是来自三个用户的数据其中一个一个紫色的点就是一条条数据所有紫色的点按时间排列就形成了一个消息队列。
[<img src="https://static001.geekbang.org/resource/image/21/15/2151181244e9b21b03029333294f9515.jpg" alt="" title="图4 数据流和窗口">](https://ci.apache.org/projects/flink/flink-docs-stable/dev/stream/operators/windows.html)
知道了什么是消息队列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。
[<img src="https://static001.geekbang.org/resource/image/eb/10/eb6af5fcb0ff8088f31f2bc5776eae10.jpg" alt="" title="图5 Flink中的滑动窗口">](https://ci.apache.org/projects/flink/flink-docs-stable/dev/stream/operators/windows.html)
那滑动窗口这种方式有什么用呢它最典型的用处就是做一些数据的JOIN操作。比如我们往往需要通过JOIN连接一个物品的曝光数据和点击数据以此来计算CTR但是你要知道曝光数据肯定是在点击数据之前到达Flink的。
那如果在分窗的时候恰好把曝光数据和点击数据分割在了两个窗口怎么办呢那点击数据就不可能找到相应的曝光数据了。这个时候只要我们使用滑动窗口这个问题就迎刃而解了。因为两个窗口重叠的部分给我们留了足够的余量来进行数据JOIN避免数据的遗漏。
事实上除了固定窗口和滑动窗口Flink还提供了更丰富的窗口操作比如基于会话的Session Window全局性的Global Window。除此之外Flink还具有数据流JOIN状态保存特性state等众多非常有价值的操作想继续学习的同学可以在课后参考Flink的[官方文档](https://ci.apache.org/projects/flink/flink-docs-release-1.12/) 。我们这节课只要清楚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&lt;Rating&gt; ratingStream = inputStream.map(Rating::new);
ratingStream.keyBy(rating -&gt; rating.userId)
.timeWindow(Time.seconds(1))
.reduce(
(ReduceFunction&lt;Rating&gt;) (rating, t1) -&gt; {
if (rating.timestamp.compareTo(t1.timestamp) &gt; 0){
return rating;
}else{
return t1;
}
}
).addSink(new SinkFunction&lt;Rating&gt;() {
@Override
public void invoke(Rating value, Context context) {
System.out.println(&quot;userId:&quot; + value.userId + &quot;\tlatestMovieId:&quot; + value.latestMovieId);
}
});
```
看完了这些操作之后你知道我们应该怎么把用户最近的高分电影评价历史实时反映到推荐结果上了吗其实很简单我们的用户Embedding是通过平均用户的高分电影Embedding得到的我们只需要在得到新的高分电影后实时地更新用户Embedding就可以了然后在推荐过程中用户的推荐列表自然会发生实时的变化。这就是SparrowRecsys基于Flink的实时推荐过程。
## 小结
这节课我们讲了流处理平台Flink特点并且通过Flink的实践清楚了利用流处理平台提高推荐系统实时性的方法。
Flink是最具代表性的批流一体的大数据平台。它的特点就是让批处理和流处理共用一套代码从而既能批量处理已落盘的数据又能直接处理实时数据流。从理论上来说是近乎完美的数据流解决方案。
而Flink提高推荐系统实时性的原理可以理解为是用户数据进入数据流也就是数据消息队列后会被分割成一定时长的时间窗口之后Flink会按照顺序来依次处理每个时间窗口内的数据计算出推荐系统需要的特征。这个处理是直接在实时数据流上进行的所以相比原来基于Spark的批处理过程实时性有了大幅提高。
为了方便你复习,我把这节课的核心概念总结在了下面的表格里,希望你能记住它们。
<img src="https://static001.geekbang.org/resource/image/ba/b1/baff4cbda249a76a7204c023ce5cc6b1.jpeg" alt="">
至于Flink的实时性实践我们要记住利用Flink我们可以实时地获取到用户刚刚评价过的电影然后通过实时更新用户Embedding就可以实现SparrowRecsys的实时推荐了。
## 课后思考
1. 你觉得实时性是不是对所有推荐系统都非常重要比如对于抖音、快手这类短视频应用还有优酷、Netflix这类长视频应用实时性对哪个更重要一些为什么
1. Flink要加强的往往是数据的实时性特征的实时性你觉得模型训练的实时性重要吗模型训练的实时性发挥的作用和特征实时性有什么不同呢
期待在留言区看到你对Flink的思考和疑惑我们下节课见

View File

@@ -0,0 +1,127 @@
<audio id="audio" title="31模型迭代阿里巴巴是如何迭代更新推荐模型的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/94/82/94872ff602a25716eab48yya10823782.mp3"></audio>
你好,我是王喆。
前两天,我问了我团队中推荐工程师们一个问题,“你们觉得在推荐系统领域工作,最大的挑战是什么?”。有的人说是模型调参,有的人说是工程落地,有的人说是找到下一步改进的思路。那现在听到这个问题的你,认为会是什么呢?
其实,刚才的这些回答都很好。不过,我的想法和大家稍有不同,我认为工作中最大的挑战就是,**不断地优化整个推荐系统的架构,做到持续的效果提升和高质量的模型迭代**。这是因为,从我们成为一名算法工程师的第一天开始就在不断面临挑战,不仅要跟别人赛跑,也要跟自己赛跑,而超越过去的自己往往是最难的。
那这节课,我就以阿里妈妈团队的经验为例,带你看一看业界一线的团队是如何实现模型的持续迭代创新,做到好上加好的。在学习这节课的时候,我再给你一点小建议,不要纠结于每个模型的细节,而是要多体会阿里的工程师是如何定位创新点的。
## 阿里妈妈推荐系统的应用场景
阿里妈妈是阿里巴巴集团的营销广告团队他们负责了阿里大部分广告推荐系统的工作。下图1就是一个阿里妈妈典型的广告推荐场景。
这是我在手机淘宝上搜索了“篮球鞋”之后App给我推荐的一个商品列表。其中第一个搜索结果的左上角带着HOT标识左下角还有一个比较隐蔽的灰色“广告”标识。那带有这两个标志的就是阿里妈妈广告推荐系统推荐的商品啦。
<img src="https://static001.geekbang.org/resource/image/4c/4a/4c798d74bc1b57bf34cd4c8a6c2c0e4a.jpeg" alt="" title="图1 阿里妈妈广告推荐的场景">
如果让你来构建这个广告推荐模型,你觉得应该考虑的关键点是什么?
我们可以重点从它的商业目标来思考。阿里妈妈的广告大部分是按照CPCCost Per Click付费的也就是按照点击效果付费的所以构建这个模型最重要的一点就是要准确预测每个广告商品的点击率这样才能把高点击率的商品个性化地推荐给特定的用户让整个广告系统的收益更高。
因此,模型怎么预测出用户爱点击的广告种类就是重中之重了。那预测广告就一定要分析和利用用户的历史行为,因为这些历史行为之中包含了用户最真实的购买兴趣。
而阿里妈妈推荐模型的主要进化和迭代的方向,也是围绕着用户历史行为的处理展开的。下面,我们就来一起看一看阿里妈妈深度推荐模型的进化过程。
## 阿里妈妈深度推荐模型的进化过程
阿里妈妈深度推荐模型的起点被称为Base Model它是一个基础版本的模型结构就是我们之前讲过的Embedding MLP的结构它的结构如图2所示。
<img src="https://static001.geekbang.org/resource/image/55/1a/554381f409e804e94cd903451d42cd1a.jpg" alt="" title="图2 Base Model的架构和用户历史行为样例">
Base Model的输入特征包括用户特征、广告特征、场景特征等等我们最需要关注的就是它处理用户行为特征的原理也就是图中彩色的部分。
Base Model使用了SUM Pooling层将用户的所有历史购买商品Embedding进行了加和。这样一来Base Model就会一视同仁、不分重点地把用户的历史行为都加起来形成一个固定的用户兴趣向量。
比如说如果一位女性用户购买过连衣裙、奶粉、婴儿车她的兴趣向量就是全部商品Embedding的平均。
这当然是一种很粗糙的历史行为处理方式,自然就成为了阿里的工程师需要重点改进的方向。那具体该怎么改进呢?我们在[第21讲](https://time.geekbang.org/column/article/313736)中讲过,就是使用注意力机制来计算每个历史行为和当前要预测的候选广告物品之间的关系,关系紧密的权重大,关系疏远的权重低。
<img src="https://static001.geekbang.org/resource/image/16/67/16c234b41538b5cd11be2ce56a450467.jpg" alt="" title="图3 DIN的模型架构和相应的用户历史行为样例">
这样一来如果我们要预测这位用户购买另一套女装时的CTR在历史行为中购买连衣裙这类商品的权重就会高而购买奶粉这类不相关商品的权重就会低就像图3中商品下面的进度条一样。
那通过这个例子我们又一次证明了,引入注意力机制是非常符合我们购买习惯的一种模型改进方式。
但是,模型的改进到这里就结束了吗?当然不是,我们虽然引入了注意力机制,但还没有考虑商品的购买时间啊。
举个例子来说,你前一天在天猫上买了一个键盘,今天你又在天猫上闲逛。那对推荐系统来说,是你前一天的购买经历比较重要,还是一年前的更重要呢?当然是前一天的,因为很可能因为你前一天购买了键盘,所以今天购买鼠标或者其他电脑周边的概率变大了,而这些连续事件之间的相关性,对模型来说是非常有用的知识。
于是DIN模型就进一步进化到了DIEN模型。图4就很好地展示了DIEN的模型结构和它对应的用户历史行为样例。
<img src="https://static001.geekbang.org/resource/image/9f/ac/9fa4fc593f266fe55b5fda2b5f7927ac.jpg" alt="" title="图4 DIEN的模型架构和相应的用户历史行为样例">
其实,本质上所有的用户历史行为都是序列式的,时间维度是天然附着在每个历史商品之上的属性。既然我们在之前的模型中把这个属性丢掉了,就一定存在着信息的浪费和知识的丢失。因此在改进模型时,这自然成为了一个突破点。
DIEN正是在模型中加入了一个三层的序列模型结构来处理用户的历史行为序列进而抽取出用户的购买兴趣演化序列最后预测出用户当前的购买兴趣。但是这次改进之后模型的迭代之路似乎变得越来越难了似乎算法工程师们把该想到的都想到了。
算法工程师这条职业道路确实是这样的,它是一个自己跟自己较劲的过程,上一轮大幅提高的优化效果,反而可能会成为这一轮改进的阻碍。这个时候,我们很可能会陷入阶段性的怀疑状态,怀疑自己还能不能找到更好的改进点。
这种自我怀疑是正常的,但更重要的是,我们能不能在阶段性的怀疑过后,重新开始冷静地分析问题,找到还没有利用过的信息资源,或者补上之前的某个技术短板,从而在现有系统中“扣出”下一个增长点。
这也是我们要从阿里工程师的工作中学习和借鉴的,下面,我们就来看看他们是怎么继续优化模型的。
2019年阿里的工程师们提出了新的模型改进思路也就是多通道兴趣记忆网络MIMN。因为他们发现DIEN虽然存在着序列模型的结构但所有的历史行为都是在一条线上演化的而事实上用户的兴趣演化过程完全可以多条并行。这该怎么理解呢
我们还以样例中的这位女性用户为例,她购买女装的兴趣演化路径,完全可以跟购买婴儿物品的演化路径平行,她的兴趣向量也可以是多个,而不仅仅局限于一个。
<img src="https://static001.geekbang.org/resource/image/f9/a0/f97e680938097f7e9b4c3f1dfdc460a0.jpg" alt="" title="图5 MIMN的模型架构和相应的用户历史行为样例">
正是基于这样的模型改进动机MIMN相比于DIEN加入了多条平行的兴趣演化路径最终为Embedding MLP的主模型提供了多个课选择的兴趣向量。当然具体选择哪个我们还是要根据当前兴趣与目标广告商品之间的注意力得分而定。通过引入兴趣演化的多通道MIMN进一步加强了模型的表达能力让模型挖掘数据价值的能力更强。
说了这么多这几个模型到底有没有提升阿里妈妈推荐系统的效果呢我们来看一看表1的离线评估结果。从中我们可以看到阿里采用了ROC AUC作为评估指标正如期望的那样随着模型的一步步改进模型的效果在淘宝数据集和亚马逊数据集上都逐渐增强。
<img src="https://static001.geekbang.org/resource/image/yy/cd/yycfdb9a682a37fa93c0b22fe2c9dbcd.jpeg" alt="" title="表1 离线评估结果">
当然,我建议所有论文中的实验效果你都应该辩证地来看,可以作为一个参考值,但不能全部相信。因为就像我常说的在,**模型结构不存在最优,只要你的数据、业务特点和阿里不一样,就不能假设这个模型在你的场景下具有同样的效果,所以根据自己的数据特点来改进模型才最重要**。
## 阿里妈妈的其他创新点
这个时候,有的同学可能会问了,阿里妈妈的同学们只是瞄着用户历史这个点去优化吗?感觉这一点的优化空间早晚会穷尽啊。我要说这个观察角度太好了,作为一名算法工程师,技术的格局和全面的视野都是非常重要的,技术的空间那么大,我们不能只盯着一个点去改进,要适当地跳出局限你的空间,去找到其他的技术短板。这样一来,我们往往会发现更好的迭代方向。
所以,阿里的工程师们除了对用户历史进行挖掘,也针对商品图片信息进行了建模,也就是所谓的 DICMDeep Image CTR Model
<img src="https://static001.geekbang.org/resource/image/23/bc/23a5ce459ab84b0ce0e736ede5ed9ebc.jpg" alt="" title="图6 DICM的模型架构">
图6中DICM的模型结构很好理解它还是采用了Embedding MLP的主模型架构只不过引入了用户历史购买过的商品的图片信息以及当前广告商品的图片信息。这些商品图片还是以Embedding的形式表示然后输入后续MLP层进行处理。
因为要引入图片信息所以在MLP层之前还需要利用一个预训练好的CV模型来生成这些图片的Embedding表达。简单来说就是一个预训练图片Embedding+MLP的模型结构。
除此之外阿里团队还在很多方向上都做了研究。比如说同时处理CTR预估和CVR预估问题的多目标优化模型ESMM[Entire Space Multi-Task Model](https://arxiv.org/pdf/1804.07931.pdf)基于用户会话进行会话内推荐的DSIN[Deep Session Interest Network](https://arxiv.org/pdf/1905.06482.pdf) 可以在线快速排序的轻量级深度推荐模型COLD(Computing power cost-aware Online and Lightweight Deep pre-ranking system)等等。
我们可以用百花齐放来形容阿里团队的发展过程。你如果感兴趣,可以按照我提供的论文地址来进一步追踪学习。
## 我们能学到什么?
到这里,我带你从业务方面梳理了,业界一线团队进行模型迭代的过程。那我们到底能从阿里妈妈团队学到或者说借鉴到什么呢?为了帮助你彻底梳理清楚,我把阿里妈妈模型的发展过程分为横向和纵向这两个维度绘制了在下面。
<img src="https://static001.geekbang.org/resource/image/e1/c5/e1d01c75d10543ebde10f658d73dd2c5.jpg" alt="" title="图7 阿里妈妈推荐模型的发展过程">
横向的维度是推荐主模型的发展过程,从基础的深度模型一路走来,沿着挖掘用户行为信息的思路不断深化,我把这种发展思路叫做模型的深化演进思路。
但是深化演进的过程是一个不断挑战自己的过程,虽然我们说数据中埋藏着无穷无尽的宝藏,不可能让我们挖完,但是挖掘的难度一直在不断加大。
就像刚开始通过用深度学习模型替代传统机器学习模型我们能取得10%这个量级的收益加入注意力机制加入序列模型这样大的结构改进我们能取得1%-3%这个量级的收益到后来更加复杂的改进可能只能取得0.5%甚至更低的收益。这个时候,我们就需要把视野拓宽,从其他角度看待推荐问题,找到新的切入点。
这方面阿里给我们做了很好的示范。比如图7纵向的发展过程就是阿里团队对于一些独立问题的解决方法多目标、多模态包括我们之前提到的可以进行实时服务和在线学习在COLD模型。这些都是独立的技术点但是它们对于推荐效果的提高可能丝毫不弱于主排序模型的改进。这种从不同角度改进推荐模型的思路我称之为广度扩展。
推荐系统是一个非常复杂的系统,系统中成百上千个不同的模块共同围成了这个系统,它最终的效果也是由这几百上千个模块共同决定的。在通往资深推荐工程师的路上,你的任务是敏锐地发现这些模块中的短板,而不是总盯着最长的那块木板改进,只有高效地解决短板问题,我们才能取得最大的收益。这也是我们这门课为什么一直强调要系统性地建立知识体系的原因。
## 小结
今天,我带你总结了阿里妈妈团队推荐模型的发展过程。我们可以从两个维度来看阿里广告推荐模型的演进。
首先是深化演进这个迭代思路上阿里妈妈对用户的历史行为进行了深入理解和不断挖掘这让阿里妈妈模型从最初的Embedding MLP结构到引入了注意力机制的DIN序列模型的DIEN再到能够利用多通道序列模型学习出用户多个兴趣向量的MIMN整个发展是一脉相承的。
其次,阿里妈妈还一直在践行“广度拓展”的模型迭代思路,从多目标、多模态,以及在线学习等多个角度改进推荐模型,实现了推荐系统的整体效果的提高。
最后,我把阿里妈妈模型演进过程中所有改进模型的特点,都总结在了下面,希望可以帮助你加深印象。
<img src="https://static001.geekbang.org/resource/image/8c/83/8c39dea9f734fd6d1933b1ffb1c73183.jpeg" alt="">
## 课后思考
最后的最后我希望咱们再来做一个开放型的练习如果我让你在MIMN的基础上进一步改进模型你觉得还有什么可供挖掘的用户行为信息吗或者说还有什么方式能够进一步利用好用户行为信息吗
期待在留言区看到你的创意和思考,我们下节课见!

View File

@@ -0,0 +1,128 @@
<audio id="audio" title="32 | 强化学习案例:美团是如何在推荐系统中落地强化学习的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6f/ce/6f58283b91c57658a72035e0739417ce.mp3"></audio>
你好,我是王喆。今天我们来聊一聊美团的强化学习落地案例。
我们在[第22讲](https://time.geekbang.org/column/article/315254)中学过强化学习的基本原理、优点以及微软的强化学习模型DRN但我们也说了强化学习在推荐系统中落地会有一个难点因为强化学习涉及模型训练、线上服务、数据收集、实时模型更新等几乎推荐系统的所有工程环节所以强化学习整个落地过程的工程量非常大需要工程和研究部门通力合作才能实现。
即使很难,但业界依然有成功落地强化学习的案例,典型的就是美团在“猜你喜欢”功能中的应用。美团曾在官方博客中详细介绍过这一[落地方案](https://tech.meituan.com/2018/11/15/reinforcement-learning-in-mt-recommend-system.html)的技术细节,我们这节课就借助这个方案,来好好学习一下强化学习的实践。我也希望你能通过这个案例,串联起我们学过的所有推荐系统知识。
## 美团的强化学习应用场景
“猜你喜欢”是美团这个团购App中流量最大的推荐展位产品形态是信息流。从图1的App截图中你可以看到“猜你喜欢”列表中推荐了用户可能喜欢的餐厅用户可以通过下滑和翻页的方式实现与App的多轮交互在这个过程中用户还可能发生点击、购买等多种行为。
<img src="https://static001.geekbang.org/resource/image/ce/dc/cef09f967937d6d0d7aeab3f26c8a0dc.jpg" alt="" title="图1 美团首页“猜你喜欢”场景">
强化学习的应用场景就藏在用户和美团App的多轮交互之中。如果推荐系统能够在用户每次翻页的时候都考虑到用户刚刚发生的行为就能够提供实时性更强的推荐体验。图2是美团的同学统计的用户翻页次数的分布图我们可以看到多轮交互确实是美团App中非常常见的用户场景这也说明强化学习还是非常有用武之地的。
<img src="https://static001.geekbang.org/resource/image/5a/4e/5ae0e505314b8856c88815f7c252f34e.jpg" alt="" title="图2 “猜你喜欢”展位用户翻页情况统计">
## 美团的强化学习建模方法
清楚了美团为什么要使用强化学习,我们就要开始思考强化学习是怎么应用到这样的场景之上的。
通过22讲的学习我们知道了强化学习有六个要素分别是智能体、环境、行动、奖励、目标和状态。清楚这六个要素在“猜你喜欢”功能中的具体含义以及建模过程我们就能知道美团App是怎么应用强化学习了。
那接下来,我就带你依次看一看美团是怎么针对这六个要素进行建模的。
首先是“智能体”它指的就是美团的推荐系统这其中最重要的部分自然就是强化学习的模型这个我们等会再详细讲。“环境”我们刚才介绍过了指的是美团App这个“猜你喜欢”的推荐场景。“行动”也不难理解就是推荐系统选出商铺的列表然后推荐给用户这个动作。接下来的三个要素是整个系统的重点就是“奖励”、“目标”和“状态”。下面我们一一来看它们的具体含义。
“奖励”指的是推荐系统把推荐列表推送给用户之后用户在这个列表之上的反馈。对于美团来说“猜你喜欢”展位的核心优化指标是点击率和下单率这里的奖励就可以被定义成推荐列表中物品的点击次数和下单次数的加权和。如下面的公式所示其中的wc和wp分别是点击次数和下单次数的权重这两个超参数可以根据你对它们的重视程度进行调整。
$$<br>
r=w_{c} * \sum I_{c l i c k}+w_{p} * \sum I_{p a y}<br>
$$
有了奖励的定义,那么整个强化学习过程的目标就很好定义了,就是多轮交互后的奖励期望之和最大,它的形式化表达如下所示:
<img src="https://static001.geekbang.org/resource/image/f4/b0/f40f64bd0157787b8a25e738c73225b0.jpeg" alt="">
你可以看到公式中的k指的是交互的轮数r指的是我们在上面定义的奖励函数Gamma是一个0-1之间的损失系数。综合来看这个目标函数的含义就是在状态s的基础上采取动作a让之后k轮的奖励之和期望最大。在强化学习模型参数的更新过程中美团就是根据这个目标函数并且通过梯度下降的方式进行参数的更新。
然后是状态的定义,在推荐系统的强化学习模型中,状态实质上代表了用户的意图、兴趣和所处场景,所以它在整个搭建过程中非常重要。
那我们如何来表达这个要素呢美团设计了图3所示的网络结构来提取状态的Embedding表达这个状态Embedding就是当前推荐系统所处的状态。
**这个网络主要分为两个部分第一个部分是把用户实时行为序列的Item Embedding作为输入就是翻页前点击下单等动作对应的物品使用一维CNN层学习用户实时意图的表达也就是图中的彩色部分另一部分是传统的特征工程它使用了特征值组成的稠密向量来表示用户所处的时间、地点、场景以及使用兴趣Embedding来代表更长时间周期内用户的行为习惯。**
<img src="https://static001.geekbang.org/resource/image/86/62/8601398477b0463ef7da4ef02d529f62.jpg" alt="" title="图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),就是把这两部分的得分融合起来。
<img src="https://static001.geekbang.org/resource/image/8b/a2/8b73738da62ce1b121782eb1d5d666a2.jpeg" alt="" title="图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步
<li>
根据历史数据预训练State Embedding网络初始化Value Net和Advantage函数的相关参数
</li>
<li>
在用户打开APP时根据初始化模型来生成推荐列表
</li>
<li>
随着用户的不断交互产生了一些实时的交互数据这些数据经过State Embedding网络的在线推断过程生成用户的实时State Embedding从而实时地影响用户每一轮交互的结果
</li>
<li>
State Embedding网络不进行在线更新Value Net和Advantage函数部分进行在线学习来更新相关参数让强化学习框架具备实时学习的能力。
</li>
以上就是美团强化学习方案的理论部分,但是我们知道,强化学习落地最难的地方在于工程与模型的紧密融合,那下面就让我们看一看美团是如何落地这套方案的。
## 美团强化学习的工程架构
图5就是美团强化学习方案相关的工程架构。从整体上看它确实像一整个推荐系统的框图跟我们的SparrowRecys项目一样包含了线上Online部分、近线Nearline部分和离线Offline部分。其中的模块也非常复杂包含了数据流、模型训练、模型服务、线上推荐等多个模块。
虽然它复杂,但你也不用着急,它里面每个模块之间都有很强的逻辑关系。接下来,你就和我随着数据的整体流向,一起来过一遍这整个架构图吧。
<img src="https://static001.geekbang.org/resource/image/53/d4/539624204ffb2b17f25yy50cf7bea5d4.jpg" alt="" title="图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层、切分线程池以及模型预热等等。
## 课后思考
你觉得我们在离线训练中使用的随机梯度下降之类的方法能用到在线学习上吗?除此之外,你还知道哪些在线学习的方法?
欢迎把你的经验和思考写在留言区,我们下节课见!

View File

@@ -0,0 +1,129 @@
<audio id="audio" title="33技术权衡解决方案这么多哪个最合适" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/17/757b0a1fa0aa32afc330d9687cd38317.mp3"></audio>
你好,我是王喆。
在实际的工作中我们经常会面临一些技术上的抉择。比如说在设计推荐系统的时候我们是应该用模型A还是用模型B是用TensorFlow还是用PyTorch是用Redis还是用EVCache呢从理论上来说其实选择哪个方案都可以但在工程落地中不同的方案往往对系统整体的开销整个团队的工作量甚至最终的推荐效果都有着非常大的影响。
我想这也是很多算法工程师的困惑:在工程落地环节,解决方案这么多,我们到底该选哪个?
今天,我们就一起来探讨一下技术权衡的问题,看看能不能在理论知识和工程落地之间找到一条最优的路径。
## 工程师职责的本质
“工程”和“理论”之间的权衡是所有工程师都要考虑的问题,对这个问题的思考方式决定了你具备的是“工程思维”还是“研究思维”,抑或是“学生思维”。推荐系统是一个工程性极强,以技术落地为首要目标的领域,因此,“工程思维”对推荐系统工程师是最重要的。
事实上,无论是算法工程师,还是研发工程师,甚至是设计电动汽车、神舟飞船、长征火箭的工程师,他们的职责都相同,那就是**在现有实际条件的制约下,以工程完成和技术落地为目标,寻找并实现最优的解决方案。**这里面有一个词最关键,那就是“制约”。我们该怎么理解这个制约呢?
比如说,在机器学习的理论层面,一切事情都是理想化的,就像模型结构是精确定义的,训练过程是严格推导的,但在实际条件下,一个模型的训练过程会受算力条件、数据质量、工程时间限制等多重条件的制约。这也是我们不得不面临诸多选择和权衡的原因。
再比如说我有些同学在航天领域工作最近趁着嫦娥五号任务圆满完成的热点我咨询了他们一个问题说“咱们国家什么时候能实现载人登月啊”他们这帮航天工程师的回答也很严谨说“制约咱们国家载人登月的主要是火箭的运力问题现在的长征五号火箭近地轨道运载能力是25吨美国阿波罗登月计划使用的土星五号火箭的轨道运载能力是140吨整整差了5倍多。”
[<img src="https://static001.geekbang.org/resource/image/52/bb/5206160f389c023852a5980dafd095bb.jpeg" alt="" title="图1 土星五号和长征5号">](https://www.bilibili.com/video/av71595679/)
你看,所有的工程师都在受客观条件的制约,那怎么办呢?他们又讲了,要么就是一步步来,先攻克运载火箭技术,再像阿波罗计划一样,一发火箭把登月舱、返回舱、航天员等等一起送到月球;要么就是用现有的长征五号,发射四到五枚火箭,在太空中完成登月舱、返回舱、载人飞船的组装。
第一个计划的优势是一次性搞定,但周期长,需要等待火箭技术的攻克。第二个计划的优势是现在就可以着手开始做,但因为要发射四到五枚火箭,整个任务的容错率低,失败的风险大。
所以你看,航天工作者们发射火箭也成天抱怨运力不够,就像我们训练模型总说算力不够一样。那怎么办,日子就不过了吗?当然不是,我们工程师就是要在这样的制约条件下,又快又好地解决问题、完成任务,这就是你的职责所在。
那推荐系统中“现有实际条件的制约”都有什么呢我们经常会遇到这3种
- 软硬件环境的制约
- 研发周期的制约
- 实际业务逻辑和应用场景的制约
正是因为有这些制约的存在,一名工程师不可能像学术界的研究者一样不断尝试新的技术,做更多探索性的创新,也正是因为工程师永远以“技术落地”为目标,而不是炫耀自己的新模型、新技术是否走在业界前沿,所以在前沿理论和工程现实之间做权衡是一名工程师应该具有的基本素质。
那这个技术权衡具体该怎么做呢?下面,我就借用三个实际的案例帮助你体会一下。
## Redis容量和模型上线方式之间的权衡
对线上推荐系统来说为了进行在线服务需要将特征和模型参数存储到线上数据库或者线上服务器内存中。为了保证这两部分数据的实时性很多公司采用内存数据库的方式实现就像我们在第10讲中讲的一样Redis这类内存数据库自然是主流的选择。
但Redis需要占用大量内存资源而内存资源相比存储资源和计算资源又比较稀缺和昂贵的资源。因此无论是AWSAmazon Web Services亚马逊网络服务平台、阿里云还是自建数据中心实现Redis模块的成本都比较高自然Redis的容量就成了制约推荐系统模型上线方式的关键因素。
在这个制约因素下,我们要考虑两方面的事情:
1. 模型的参数规模要尽量小。特别是对深度学习推荐系统而言,模型的参数量级较传统模型有了几个量级的提升,所以我们更应该着重考虑模型的参数规模;
1. 因为要考虑线上预估延迟和特征存储空间有限的情况,所以线上预估所需的特征数量不能无限制地增加,要根据重要性做一定的删减。
因此,在实际上线推荐系统的时候,我们必然需要进行一定的取舍:**舍弃一些次要的要素,关注主要的矛盾**。具体怎么做呢?这里,我结合自己的经验,把整个取舍的思考过程做了梳理,一共可以分成四步,你可以作为参考。
首先,对于千万量级甚至更高量级的特征向量维度(理论上模型参数的数量级也在千万量级)来说,因为线上服务很难支持这种级别的数据量,所以我们在上线模型时关注模型的稀疏性和复杂度,通过舍弃一定的模型预测准确度来换取上线的速度和资源消耗。
明确了工程权衡的目标我们就要思考怎么提高模型的稀疏性降低模型的复杂度。提高稀疏性的常见方法是加入L1正则化项或者采用FTRL这类稀疏性强的训练方法让大部分参数归零从而减少模型体积。
对于降低复杂度,我们可以通过减少神经网络的层数,以及每层内神经元的数量来实现。当然,这个方法只有在不明显降低模型效果的前提下,才是可行的工程策略。
在明确了多种备选方案之后,如果还是无法确定哪种技术效果更佳,我们就需要实现所有备选方案以及方案间的各种组合,进行离线和线上的效果测试。
最后,我们要根据测试数据确定最终的技术途径、技术方案,完成最终上线。
以上就是模型侧的“瘦身”方法在线特征的“瘦身”方法当然也可以采用同样的思路。首先采用“主成分分析”等方法进行特征筛选在不显著降低模型效果的前提下减少所用的特征。针对不好取舍的特征进行离线评估和线上A/B测试最终达到工程上可以接受的水平。
## 研发周期和平台迁移之间的权衡
除了硬件条件的限制,研发周期的制约同样是不可忽视的因素。这就需要工程师对于项目有整体的把控,以及对研发周期有预估。在产品迭代日益迅速的互联网领域,没人愿意成为拖累整个团队的最慢的一个环节。接下来,我就以一个平台迁移的例子,来给你讲一讲如何在研发周期的制约下完成技术决策。
比如公司希望把机器学习平台从Spark MLlib整体迁移到TensorFlow上毫无疑问这是顺应深度学习浪潮的技术决策是非常正确的决定。但由于Spark平台自身的特性它的编程语言、模型训练方式都和TensorFlow有很大的差别因此整个迁移必然要经历一个较长的研发周期。
我就经历过很多次公司产品和技术平台的大规模升级,很常见的情况是,在保证平台升级正常进行的同时,我们还需要兼顾日常的开发进度,去实现一些新的产品需求。这就是工程师需要做出权衡的时候了,也是最考验工程师架构能力的时候。
<img src="https://static001.geekbang.org/resource/image/57/b2/57b98830369yy8d95fe173a585046fb2.jpeg" alt="" title="图2 TensorFlow vs Spark">
在这样的情况下一般来说有2种可行的技术方案
1. 集中团队的力量完成Spark到TensorFlow的迁移在新平台上进行新模型和新功能的研发。
1. 让团队一部分成员利用成熟稳定的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平台上如果一个新的出租屋发布了但模型还没有学到它的EmbeddingAirbnb的推荐系统会怎么做呢它会通过三个步骤生成新出租屋的Embedding。
第一步根据新房屋的属性比如租金价格、房屋类型、面积、几房几厅等特征来找到和它相似的一批房屋第二步在这些相似房屋中找到离它最近的三个第三步通过平均这三个最近房屋的Embedding来生成新房屋的Embedding。
就像图3展示的这样颜色相近的点代表着Embedding非常接近的房屋如果一个新房屋落在了图中的某个位置Airbnb就可以根据它的地理位置和属性找到相近的房屋并且给它涂上相似的颜色。
<img src="https://static001.geekbang.org/resource/image/8d/2a/8df08c853314f54e20c6a02fc21f672a.jpeg" alt="" title="图3 Airbnb房屋聚类的例子">
所以我们经常会看到,一些推荐系统中的补充策略往往可以高效地解决一些推荐模型无法解决的棘手问题。
当然,在我们日常工作中技术间的权衡无时无刻不在发生,小到一个变量取什么名字好,一行代码应该怎么写,大到一个平台应该怎么重构,一个模型应该如何构建。我们这节课的例子当然无法囊括所有的情况,只是希望能帮助你建立起正确的进行技术权衡的思路,做到抓住主矛盾点,做出有利于主要目标的取舍。
## 小结
这节课,我们通过三个例子一起探讨了工程落地中技术权衡的问题,希望能帮助你进一步加深了对工程师思维本质的理解。
事实上,对任何领域来说,工程师思维的本质都是“**在现有实际条件的制约下,以工程完成和技术落地为目标,寻找并实现最优的解决方案**”。
在推荐系统领域,典型的制约条件有三类:“软硬件环境的制约”,“研发周期的制约”,以及“实际业务逻辑和应用场景的制约”。在这些制约条件下,我认为的技术方案间的权衡之道,就是“抓住主矛盾点,列出备选方案,通过分析对比,作出有利于主要目标的取舍。”
## 课后思考
这节课我们提到了物品冷启动问题的解决方案你觉得基于Embedding还有哪些好的冷启动解决方案呢如果让你来解决的话在实践中你会作出哪些取舍倾向于哪种选择呢
期待在留言区看到你的分享和思考,我们下节课见!

View File

@@ -0,0 +1,126 @@
<audio id="audio" title="01 | 技术架构:深度学习推荐系统的经典技术架构长啥样?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/54/15/5415875b7dd714872cb00959ebf5b415.mp3"></audio>
你好,我是王喆。从今天开始,我们正式开始学习“深度学习推荐系统”了。在开始之前,我想先问你一个问题:当你开始学习一个全新领域的时候,你想做的第一件事情是什么?
当然每个人可能都有自己的答案,但对于我自己来说,我最想搞明白的是两个问题。一个是,这个领域到底要解决什么问题?第二个是,这个领域有没有一个非常高角度的思维导图,让我能够了解这个领域有哪些主要的技术,做到心中有数?
针对“深度学习推荐系统”这个领域啊,可能还会有第三个问题,为什么我们要一直强调“深度学习”,深度学习到底给推荐系统带来了什么革命性的影响?相信听完了这一节课,你心中的这三个问题也都能迎刃而解。
## 推荐系统要解决的根本问题是什么?
在开篇词中我们提到,推荐系统的应用已经渗透到购物、娱乐、学习等生活的方方面面,虽然商品推荐、视频推荐、新闻推荐这些推荐场景可能完全不同,既然它们都被称为“推荐系统”,解决的本质问题一定是相通的,遵循着共通的逻辑框架。
推荐系统要解决的问题用一句话总结就是,**在“信息过载”的情况下,用户如何高效获取感兴趣的信息。**
因此,推荐系统正是在“浩如烟海的互联网信息”和“用户的兴趣点”之间,搭建起的一座桥梁。那这座桥是怎么一步步搭建起来的呢?下面,我们先来看看,推荐系统比较抽象的逻辑架构是什么样的,再一步步搭建起它的技术架构,让你对推荐系统有一个整体上的印象。
## 推荐系统的逻辑架构
从推荐系统的根本问题出发,我们可以清楚地知道,推荐系统要处理的其实是“**人**”和“**信息**”之间的关系问题。也就是基于“人”和“信息”,构建出一个找寻感兴趣信息的方法。
这里“信息”的定义非常多样,它在不同场景下的具体含义也千差万别。比如说,在商品推荐中指的是“商品信息”,在视频推荐中指的是“视频信息”,在新闻推荐中指的是“新闻信息”,为了方便,我们可以把它们统称为“物品信息”。
而从“人”的角度出发,为了更可靠地推测出“人”的兴趣点,推荐系统希望能利用大量与“人”相关的信息,这类信息包括历史行为、人口属性、关系网络等,它们可以被统称为“用户信息”。
此外,在具体的推荐场景中,用户的最终选择一般会受时间、地点、用户的状态等一系列环境信息的影响,这些环境信息又可以被称为“场景信息”或“上下文信息”。
清楚了这些信息的定义,推荐系统要处理的问题就可以被形式化地定义为:**对于某个用户**U**User在特定场景**C**Context针对海量的“物品”信息构建一个函数 ,预测用户对特定候选物品**I**Item的喜好程度再根据喜好程度对所有候选物品进行排序生成推荐列表的问题**。
这样一来,我们就可以抽象出推荐系统的逻辑框架了。虽然这个逻辑框架还比较简单,但我们正是在此基础上,对各模块进行细化和扩展,才产生了推荐系统的整个技术体系。
<img src="https://static001.geekbang.org/resource/image/c7/07/c75969c5fcc6e5e374a87d4b4b1d5d07.jpg" alt="" title="图1 推荐系统逻辑架构">
## 深度学习对推荐系统的革命
有了推荐系统的逻辑架构,我就能回答开头的第三个问题“**深度学习到底给推荐系统带来了什么革命性的影响?**”。
在推荐系统逻辑架构图中,居于中心位置的是一个抽象函数**f(U,I,C)**,它负责“猜测”用户的心,为用户可能感兴趣的物品打分,从而得出最终的推荐物品列表。在推荐系统中,这个函数一般被称为“推荐系统模型”(今后简称“推荐模型”)。
深度学习应用于推荐系统,能够极大地增强推荐模型的拟合能力和表达能力。简单来说,就是让推荐模型“猜的更准”,更能抓住用户的“心”。这么说你可能还没有一个清晰的概念,接下来,我们再从模型结构的角度出发,来比较一下传统机器学习推荐模型和深度学习推荐模型的区别,让你有一个更清晰的认识。
我在下面给出了一张模型结构对比图,它对比了传统的矩阵分解模型和深度学习矩阵分解模型的区别。我们先忽略细节不谈,你第一眼看上去有什么感觉?是不是觉得深度学习模型变得更复杂了,一层又一层,层数增加了很多。
你的感觉一点儿都没错,其实正是**因为深度学习复杂的模型结构,让深度学习模型具备了理论上拟合任何函数的能力**。如果说**f(U,I,C)** 这个推荐函数具有一个最优的表达形式,那传统的机器学习模型只能够拟合出**f(U,I,C)** 这个推荐函数的近似形式,而深度学习模型则可以最大程度地接近这个最优形式。
<img src="https://static001.geekbang.org/resource/image/3d/7e/3d786a8a7ab6141edaeb2383858b137e.jpg" alt="" title="图2 传统的矩阵分解模型和深度学习矩阵分解模型的对比图[br]来源《Neural collaborative filtering》">
除此之外,深度学习模型非常灵活的模型结构还让它具备了一个无法替代的优势,就是我们可以**让深度学习模型的神经网络模拟很多用户兴趣的变迁过程,甚至用户做出决定的思考过程**。比如阿里巴巴的深度学习模型——深度兴趣进化网络如图3它利用了三层序列模型的结构模拟了用户在购买商品时兴趣进化的过程如此强大的数据拟合能力和对用户行为的理解能力是传统机器学习模型不具备的。
<img src="https://static001.geekbang.org/resource/image/b2/b9/b2606096aa4ff97461dd91b87d748db9.jpg" alt="" title="图3 阿里巴巴的深度兴趣进化网络 [br]来源《Deep Interest Evolution Network for Click-Through Rate Prediction》">
但是,深度学习对推荐系统的革命影响还远不止这些。近几年,由于深度学习模型的结构复杂度大大提高,使通过训练使模型收敛所需的数据量大大增加,这也反向推动了推荐系统大数据平台的发展,让推荐系统相关的大数据存储、处理、更新模块也一同迈入了“深度学习时代”。
讲了这么多深度学习对推荐系统的影响,我们似乎还没看到一个完整的深度学习推荐系统架构。别着急,下面,我们就来讲一讲,经典的深度学习推荐系统的技术架构是什么样的。
## 深度学习推荐系统的技术架构
讲之前啊,我还要说明一点,深度学习推荐系统的架构与经典的推荐系统架构其实是一脉相承的,它对经典推荐系统架构中某些特定模块进行了改进,使之能够支持深度学习的应用。所以,我会先讲经典的推荐系统架构,再讲深度学习对它们的改进。
在实际的推荐系统中,工程师需要着重解决的问题有两类。
- 一类问题与数据和信息相关,即“用户信息”“物品信息”“场景信息”分别是什么?如何存储、更新和处理数据?
- 另一类问题与推荐系统算法和模型相关,即推荐系统模型如何训练、预测,以及如何达成更好的推荐效果?
一个工业级推荐系统的技术架构其实也是按照这两部分展开的其中“数据和信息”部分逐渐发展为推荐系统中融合了数据离线批处理、实时流处理的数据流框架“算法和模型”部分则进一步细化为推荐系统中集训练Training、评估Evaluation、部署Deployment、线上推断Online Inference为一体的模型框架。基于此我们就能总结出推荐系统的技术架构图。
<img src="https://static001.geekbang.org/resource/image/a8/c1/a87530cf45fb76480bf5b60b9feb60c1.jpg" alt="" title="图4 推荐系统技术架构示意图">
在图4中我把推荐系统的技术架构分成了“数据部分”和“模型部分”。那它们的工作内容和作用分别是什么呢深度学习对于这两部分的影响又有哪些呢下面我来一一讲解。
### 第一部分:推荐系统的数据部分
推荐系统的“数据部分”主要负责的是“用户”“物品”“场景”信息的收集与处理。根据处理数据量和处理实时性的不同,我们会用到三种不同的数据处理方式,按照实时性的强弱排序的话,它们依次是**客户端与服务器端实时数据处理、流处理平台准实时数据处理、大数据平台离线数据处理**。
在实时性由强到弱递减的同时三种平台的海量数据处理能力则由弱到强。因此一个成熟推荐系统的数据流系统会将三者取长补短配合使用。我们也会在今后的课程中讲到具体的例子比如使用Spark进行离线数据处理使用Flink进行准实时数据处理等等。
大数据计算平台通过对推荐系统日志物品和用户的元数据等信息的处理获得了推荐模型的训练数据、特征数据、统计数据等。那这些数据都有什么用呢具体说来大数据平台加工后的数据出口主要有3个
1. 生成推荐系统模型所需的样本数据,用于算法模型的训练和评估。
1. 生成推荐系统模型服务Model Serving所需的“用户特征”“物品特征”和一部分“场景特征”用于推荐系统的线上推断。
1. 生成系统监控、商业智能Business IntelligenceBI系统所需的统计型数据。
可以说,推荐系统的数据部分是整个推荐系统的“水源”,我们只有保证“水源”的持续、纯净,才能不断地“滋养”推荐系统,使其高效地运转并准确地输出。在深度学习时代,深度学习模型对于“水源”的要求更高了,**首先是水量要大**,只有这样才能保证我们训练出的深度学习模型能够尽快收敛;**其次是“水流”要快**让数据能够尽快地流到模型更新训练的模块这样才能够让模型实时的抓住用户兴趣变化的趋势这就推动了大数据引擎Spark以及流计算平台Flink的发展和应用。
### 第二部分:推荐系统的模型部分
推荐系统的“模型部分”是推荐系统的主体。模型的结构一般由“召回层”、“排序层”以及“补充策略与算法层”组成。
其中,“召回层”一般由高效的召回规则、算法或简单的模型组成,这让推荐系统能快速从海量的候选集中召回用户可能感兴趣的物品。“排序层”则是利用排序模型对初筛的候选集进行精排序。而“补充策略与算法层”,也被称为“再排序层”,是在返回给用户推荐列表之前,为兼顾结果的“多样性”“流行度”“新鲜度”等指标,结合一些补充的策略和算法对推荐列表进行一定的调整,最终形成用户可见的推荐列表。
从推荐系统模型接收到所有候选物品集,到最后产生推荐列表,这一过程一般叫做“**模型服务过程**”。为了生成模型服务过程所需的模型参数我们需要通过模型训练Model Training确定模型结构、结构中不同参数权重的具体数值以及模型相关算法和策略中的参数取值。
模型的训练方法根据环境的不同,可以分为“离线训练”和“在线更新”两部分。其中,离线训练的特点是可以利用全量样本和特征,使模型逼近全局最优点,而在线更新则可以准实时地“消化”新的数据样本,更快地反应新的数据变化趋势,满足模型实时性的需求。
除此之外为了评估推荐系统模型的效果以及模型的迭代优化推荐系统的模型部分还包括“离线评估”和“线上A/B测试”等多种评估模块用来得出线下和线上评估指标指导下一步的模型迭代优化。
我们刚才说过深度学习对于推荐系统的革命集中在模型部分那具体都有什么呢我把最典型的深度学习应用总结成了3点
1. 深度学习中Embedding技术在召回层的应用。作为深度学习中非常核心的Embedding技术将它应用在推荐系统的召回层中做相关物品的快速召回已经是业界非常主流的解决方案了。
1. 不同结构的深度学习模型在排序层的应用。排序层(也称精排层)是影响推荐效果的重中之重,也是深度学习模型大展拳脚的领域。深度学习模型的灵活性高,表达能力强的特点,这让它非常适合于大数据量下的精确排序。深度学习排序模型毫无疑问是业界和学界都在不断加大投入,快速迭代的部分。
1. 增强学习在模型更新、工程模型一体化方向上的应用。增强学习可以说是与深度学习密切相关的另一机器学习领域,它在推荐系统中的应用,让推荐系统可以在实时性层面更上一层楼。
## 小结
这节课,我带你熟悉了深度学习推荐系统的技术架构,虽然涉及的内容非常多,但如果没有记住的话,你也完全不用慌张,只需要在心中留下这个框架的印象就可以了。你完全可以把这节课的内容当作整个课程的技术索引,让它成为属于你自己的一张知识图谱。
形象点来说,你可以把这节课程的内容想象成是一颗知识树,它有根,有干、有枝、有叶,还有花。
其中,推荐系统的根就是推荐系统要解决的根本性问题:在“信息过载”情况下,用户怎么高效获取感兴趣的信息。
而推荐系统的干就是推荐系统的逻辑架构:对于某个用户**U**User在特定场景**C**Context针对海量的“物品”构建一个函数 ,预测用户对特定候选物品**I**Item的喜好程度的过程。
枝和叶就是推荐系统的各个技术模块,以及各模块的技术选型。技术模块撑起了推荐系统的技术架构,技术选型又让我们可以在技术架构上实现各种细节,开枝散叶。
最后,深度学习在推荐系统的应用无疑是当前推荐系统技术架构上的明珠,它就像是这颗大树上开出的花,是最精彩的点睛之笔。深度学习的模型结构复杂,数据拟合能力和表达能力更强,能够让推荐模型更好的模拟用户的兴趣变迁过程,甚至是做决定的过程。而深度学习的发展,也推动着推荐系统数据流部分的革命,让它能够更快、更强地处理推荐系统相关的数据。
好了,这节课我们就讲到这里,希望你能牢牢记住深度学习推荐系统的架构,播撒下这一粒种子,然后跟随我后面的课程让它长大成一颗属于你自己的参天大树。
<img src="https://static001.geekbang.org/resource/image/0e/a1/0e269ebf95dcb772ed31f9c28cef2aa1.jpeg" alt="" title="图5 推荐系统的技术架构像树一样生发而出">
## 课后思考
下面是Netflix的推荐系统的经典架构图你能结合本节课讲的推荐系统技术架构说出Netflix架构图中哪些是数据部分哪些是模型部分吗
<img src="https://static001.geekbang.org/resource/image/41/c0/4189db7d4yy86a74903dd2acb30742c0.jpg" alt="" title="图6 Netflix架构图示意图">
这样的深度学习推荐系统和你想的一样吗?如果今天的课程对你有帮助,也欢迎你把它转发出去!我们下节课见!

View File

@@ -0,0 +1,124 @@
<audio id="audio" title="02 | Sparrow RecSys我们要实现什么样的推荐系统" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9a/a7/9a74c445ed975ecdc0a67100c6abaca7.mp3"></audio>
你好,我是王喆。
上节课,我们明确了推荐系统要解决的基本问题,清楚了深度学习推荐系统的技术架构,这节课我们开始走进实战。
作为程序员我相信你肯定听过甚至可能还很认同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](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/`,就能看到整个推荐系统的前端效果了。
<img src="https://static001.geekbang.org/resource/image/29/e0/291a34c2e4cd379434efd2e3aed777e0.jpg" alt="" title="图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的首页**
<img src="https://static001.geekbang.org/resource/image/29/e0/291a34c2e4cd379434efd2e3aed777e0.jpg" alt="" title="图2 Sparrow RecSys的首页">
Sparrow RecSys 的首页由不同类型的电影列表组成,当用户首次访问首页时,系统默认以历史用户的平均打分从高到低排序,随着当前用户不断为电影打分,系统会对首页的推荐结果进行个性化的调整,比如电影类型的排名会进行个性化调整,每个类型内部的影片也会进行个性化推荐。
**其次,是电影详情页。**
<img src="https://static001.geekbang.org/resource/image/b1/46/b12e82af7b561f2d657da25b04960946.jpg" alt="" title="图3 电影详情页">
你可以看到电影详情页除了罗列出电影的一些基本信息最关键的部分是相似影片的推荐。相似内容推荐是几乎所有推荐系统非常重要的功能传统的推荐系统基本依赖于基于内容Content based的推荐方法而我们这门课程会更多地讲解基于深度学习Embedding的相似内容推荐方法。
**最后,是为你推荐页。**
<img src="https://static001.geekbang.org/resource/image/55/65/55d0f2b3395d30b9ecac47a615686765.jpg" alt="" title="图4 为你推荐页">
这一部分也是整个推荐系统中最重要的部分,是用户的个性化推荐页面。这个页面会根据用户的点击、评价历史进行个性化推荐。这几乎是所有推荐系统最经典和最主要的应用场景。我希望在这门课程中,你能够动手完成个性化推荐中的每个关键步骤,包括但不限于特征的处理、候选集的召回、排序层主要模型等等。
## Sparrow Recsys的数据从哪来
知道了Sparrow RecSys的功能之后你肯定想问“老师咱们的数据从哪来呀”。既然Sparrow RecSys是一个开源项目那么Sparrow RecSys的数据源肯定也是开源和免费的它的数据源来自于著名的电影开源数据集[MovieLens](https://grouplens.org/datasets/movielens/)。
为了方便你调试咱们这门课程的教学数据集对MovieLens数据集进行了精简只留下了1000部电影。如果希望在全量数据集上进行推荐你可以去MovieLens的官方网站下载全量数据它一共包含了27000部电影。
MovieLens的数据集包括三部分分别是 **movies.csv电影基本信息数据、ratings.csv**用户评分数据)**和 links.csv**(外部链接数据)。下面,我就具体说说它们分别长什么样。
### 1. movies.csv电影基本信息数据
movies表是电影的基本信息表它包含了电影IDmovieId、电影名title、发布年份以及电影类型genres等基本信息。
<img src="https://static001.geekbang.org/resource/image/f3/7a/f3579fbb04e430d5322fcd58fb410c7a.jpeg" alt="" title="图5 电影基本信息数据">
MovieLens 20M Dataset包含了2016年前的约13万部电影我们课程的实验数据集从中抽取了前1000部电影。电影数据集是我们推荐的主体其中分类、发布年份、电影名称等信息也将是推荐模型可以利用的重要特征。
### 2. ratings.csv用户评分数据
ratings表包含了用户IDuserId、电影IDmovieId、评分rating和时间戳timestamp等信息。
<img src="https://static001.geekbang.org/resource/image/6c/ec/6c1d79ba6f9ea7017b7b0f60d3798aec.jpeg" alt="" title="图6 用户评分数据">
MovieLens 20M Dataset包含了2000万条评分数据我们课程的实验数据集从中抽取了约104万条评论数据。评论数据集是之后推荐模型训练所需的训练样本来源也是我们分析用户行为序列、电影统计型特征的原始数据。
### 3. links.csv外部链接数据
links表包含了电影IDmovieId、IMDB对应电影IDimdbId、TMDB对应电影IDtmdbId等信息。其中imdb和tmdb是全球最大的两个电影数据库。因为links表包含了MovieLens电影和这两个数据库ID之间的对应关系所以我们可以根据这个对应关系来抓取电影的其他相关信息这也为我们大量拓展推荐系统特征提供了可能。
<img src="https://static001.geekbang.org/resource/image/95/07/951f3fb63de288774c404c215756cf07.jpeg" alt="" title="图7 外部链接数据">
此外MovieLens的数据集中还包含了tags.csv它用于记录用户为电影打的标签由于课程中暂时没有使用标签数据我就展开说了。
## Sparrow Recsys涵盖的技术点
清楚了Sparrow Recsys的功能和数据你肯定迫不及待地想知道Sparrow Recsys会使用哪些技术可以实现哪些模型。
那我们直接来看下面这张Sparrow Recsys的技术架构图。你会发现它其实就是我们用具体的技术选型把上节课的深度学习推荐系统架构图给填上得到的。所以Sparrow Recsys就是深度学习推荐系统架构的一个实现。
<img src="https://static001.geekbang.org/resource/image/8c/9e/8cee6a7eeebda9745bfbe1b6yy18c59e.jpg" alt="" title="图8 Sparrow Recsys的推荐系统架构">
你可以看到它一共分为三个模块分别是数据、模型和前端。其中每个部分都用业界推荐系统的主流技术比如数据部分我们会用SparkFlink进行样本和特征的处理模型部分我们会使用TensorFlow训练深度神经网络、Wide&amp;Deep、PNN等模型。
<img src="https://static001.geekbang.org/resource/image/2f/02/2f89e7a0fb0f130f2ec8e75d3848f802.jpeg" alt="" title="图9 Sparrow Recsys中的技术点">
我想啊你在看到这么多的技术点和技术平台之后肯定想问我们的课程能把它们都讲完、讲透吗这是个好问题我也有必要在这里说清楚。从中我们可以总结出Sparrow Recsys中具体用到的技术点。
其实推荐系统是一个应用属性很强的领域,想把推荐系统学好,我们就必须去学习各式各样相关的平台、技术,所以我们这门课的涉及面非常广。但你也不用因为要学这么多的技术而感到惊慌,因为我们没有必要去深究每个平台内部的原理、优化的方法,我们当好一个使用者就好。
举个例子你就明白了我们处理数据需要用到Spark但我们有必要成为Spark的专家吗其实不用。因为即使你已经走上了工作岗位也有平台架构部的同事能够提供Spark的很多技术支持。所以学习这门课程我们大可抱着一个使用者而不是开发者、维护者的心态去使用不同的技术平台。当然如果你想成为某个细分方向的专家比如Spark的专家、Flink的专家等等我相信极客时间上肯定还有很不错的课程供你学习。
所以希望你能够通过Sparrow Recsys认识到主流深度学习推荐系统都使用了哪些技术让自己有一个全面的认识建立自己的知识广度。如果还想深入钻研某个方向也可以由此开始努力成为一个领域的专家。
## 小结
这堂课我带你熟悉了我们将要实现的推荐系统Sparrow Recsys它将是我们深度学习推荐系统这门课的落地项目和实现范例。希望有这个真实可用的推荐系统作为支撑这门课可以同时兼顾概念讲解和代码实战也让我们接下来的共同合作能够更好。
从开篇词到这一节课我们从推荐系统要解决的核心问题生发出深度学习推荐系统的技术架构再到让技术架构实实在在地落地到Sparrow Recsys这个开源项目上。我想你已经可以感受到架构篇的学习过程其实就是一个从抽象到具体从形而上到形而下的过程。
那在搭建起这整门课程的框架之后,接下来我们将会一起深入到技术细节,以及深度学习的实践中,一起去体验深度学习浪潮之巅的推荐系统知识,期待继续与你同行!
## 课后思考
1. 当你把Sparrow Recsys在自己的电脑上安装运行起来之后对照着上节课的深度学习推荐系统架构图你能试着说出每个模块的代码属于架构图中的哪一部分吗
1. 你觉得对于一个电影推荐系统来说,什么数据对生成用户个性化推荐结果最有帮助?
好啦快按照这节的方法把Sparrow Recsys运行起来吧课后的两个问题也并不困难相信你肯定可以回答出来。今天就讲到这里了我们下节课见

View File

@@ -0,0 +1,115 @@
<audio id="audio" title="03 | 深度学习基础:你打牢深度学习知识的地基了吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d4/f1/d47a52f61cfe3d10362807d9b3535ef1.mp3"></audio>
你好,我是王喆。
今天,我想用一节课的时间,带你梳理巩固一下深度学习的相关基础知识。打好基础之后,我们再去学习深度学习推荐系统的技术细节,就能更加得心应手了。
具体来说我会从一个基本的神经元开始讲到多神经元组成的神经网络再到结构各异的深度学习网络最后再讲一讲深度学习和推荐系统是怎么结合的。这样从0到1带你体会深度学习网络生长的整个过程。
是不是已经迫不及待想要开始今天的课程啦?接下来,我们就一起“钻”进一个神经元里面,跟它一起成长吧。
## 一切要从一个神经元开始
上中学的时候你肯定在生物课上学到过神经元是我们神经系统的最基本单元我们的大脑、小脑、脊髓都是由神经元组成的。比如大脑大概包含了1000亿个神经元正是这些小小的神经元之间互相连接合作让大脑能够完成非常复杂的学习任务这是一个多么神奇的过程
于是,计算机科学家们就有一个设想,是不是我们也能从神经元出发,创造出一个人造大脑,来帮我们完成各种不同的任务呢?这其中当然也包括我们课程要讲的推荐任务。事实上,随着近十年深度学习网络的快速发展,这个设想已经被成功应用到图像识别、语音处理、推荐搜索等多个领域了!那组成这个“人造大脑”的基础,也就是神经元到底是什么样子的呢?
下面这张图就是一个神经元的结构示意图,它大致由**树突**、**细胞体**(图中细胞核和周围的部分)、**轴突、轴突末梢**等几部分组成。**树突**是神经元的输入信号通道,它的功能是将其他神经元的动作电位(可以当作一种信号)传递至细胞体。
<img src="https://static001.geekbang.org/resource/image/86/f9/86892b5dfbf5e37e355c5dee84c846f9.jpeg" alt="" title="图1 神经元示意图">
在接收到其他神经元的信号后,**细胞体**会根据这些信号改变自己的状态,变成“激活态”还是“未激活态”。具体的状态取决于输入信号,也取决于神经细胞本身的性质(抑制或加强)。当信号量超过某个阈值时,细胞体就会被激活,产生电脉冲。电脉冲会沿着**轴突**传播,并通过**轴突末梢**传递到其它神经元。
我上面讲的这些是神经元工作的生物学过程,那如果要用一个神经元来解决推荐问题,具体又该怎么做呢?举个例子,我们可以假设其他神经元通过树突传递过来的信号就是推荐系统用到的特征,有的信号可能是“用户性别是男是女”,有的信号可能是“用户之前有没有点击过这个物品”等等。细胞体在接收到这些信号的时候,会做一个简单的判断,然后通过轴突输出一个信号,这个输出信号大小代表了用户对这个物品的感兴趣程度。这样一来,我们就可以用这个输出信号给用户做推荐啦。
看起来用神经元来完成推荐任务还是很有希望的但在实际应用里面我们还得把生物结构的神经元抽象成一个数学形式这样我们才能用程序来实现它。图2就是这样的一个抽象结构这个神经元的结构很简单只有两个传递输入信号的树突。
<img src="https://static001.geekbang.org/resource/image/3e/b7/3eb558f5b5f41df5b5644841cb1694b7.jpeg" alt="" title="图2 神经元的抽象结构">
我们可以看到图2中的x<sub>1</sub>、x<sub>2</sub>就相当于两个树突传递的输入信号蓝圈内的结构相当于神经元的细胞体细胞体用某种方式处理好输入信号之后就通过右面的轴突输出信号y。因为输入输出都很简单所以我想现在你的疑问肯定聚焦在细胞体对输入信号的处理方式上了我们可以把细胞体的数学结构放大一点看看。
<img src="https://static001.geekbang.org/resource/image/c2/54/c28b5eee53a2b4e70233cdf232873e54.jpeg" alt="" title="图3 基于Sigmoid激活函数的神经元">
其实细胞体中的计算就做了两件事情一件事情是把输入信号x<sub>1</sub>、x<sub>2</sub>各自乘以一个权重w<sub>1</sub>、w<sub>2</sub>,再把各自的乘积加起来之后输入到一个叫“激活函数”的结构里。
图3中的激活函数是sigmoid激活函数它的数学定义是$f(z)=\frac{1}{1+\mathrm{e}^{-z}}$ 。它的函数图像就是图3中的S型曲线它的作用是把输入信号从-∞,+∞的定义域映射到01的值域因为在点击率预测推荐问题中往往是要预测一个从0到1的概率。再加上sigmoid函数有处处可导的优良数学形式方便之后的梯度下降学习过程所以它成为了经常使用的激活函数。
当然激活函数的种类有很多种比较流行的还有tanh、ReLU等在训练神经元或者神经网络的时候我们可以尝试多种激活函数根据效果来做最终的决定。
## 什么是神经网络?
不过,单神经元由于受到简单结构的限制,预测能力并不强,因此在解决复杂问题时,我们经常会用多神经元组成一个网络,这样它就具有更强的拟合数据的能力了,这也就是我们常说的**神经网络**。比如说,下图就向我们展示了一个由输入层、两神经元隐层和单神经元输出层组成的简单神经网络。
<img src="https://static001.geekbang.org/resource/image/dc/c6/dc2e967466dc1d05fe7e443b8400b6c6.jpeg" alt="" title="图4 简单神经网络示意图">
其中每个蓝色神经元的构造都和刚才的单神经元构造相同h<sub>1</sub>和h<sub>2</sub>神经元的输入是由x<sub>1</sub>和x<sub>2</sub>组成的特征向量而神经元o<sub>1</sub>的输入则是由h<sub>1</sub>和h<sub>2</sub>输出组成的输入向量。这是一个最简单的三层神经网络,在深度学习的发展过程中,正是因为研究人员对神经元不同的连接方式的探索,才衍生出各种不同特性的深度学习网络,让深度学习模型的家族树枝繁叶茂。在后面课程的学习中,我们也会深入讲解各种不同的网络结构,相信你对这句话的理解也会随着学习的推进而更加深刻。
## 神经网络是怎么学习的?
清楚了神经网络的结构之后更重要的问题是我们该如何训练一个神经网络。也就是说我们怎么得到图5中x<sub>1</sub>到h<sub>1</sub>、h<sub>2</sub>的权重w<sub>1</sub>、w<sub>3</sub>,以及图中其他的权重呢?
<img src="https://static001.geekbang.org/resource/image/35/92/356aeb6e93f4292a89e5b57a4a385d92.jpeg" alt="" title="图5 神经网络中的权重">
这里需要用到神经网络的重要训练方法,**前向传播Forward Propagation<strong>和**反向传播Back Propagation</strong>。前向传播的目的是在当前网络参数的基础上得到模型对输入的预估值也就是我们常说的模型推断过程。比如说我们要通过一位同学的体重、身高预测TA的性别前向传播的过程就是给定体重值71身高值178经过神经元h<sub>1</sub>、h<sub>2</sub>和o<sub>1</sub>的计算得到一个性别概率值比如说0.87这就是TA可能为男性的概率。
在得到预估值之后我们就可以利用损失函数Loss Function计算模型的损失。比如我们采用绝对值误差Absolute Loss作为我们的损失函数如果这位同学的真实性别是男那真实的概率值就是1根据公式2的绝对值误差定义这次预测的损失就是|1-0.87| = 0.13。
$$l_{1}\left(y_{i}, \hat{y}_{i}\right)=\left|y_{i}-\hat{y}_{i}\right|$$
我们常说“知错能改善莫大焉”神经网络的学习更是践行了这句话。发现了预测值和真实值之间的误差Loss我们就要用这个误差来指导权重的更新让整个神经网络在下次预测时变得更准确。最常见的权重更新方式就是梯度下降法它是通过求取偏导的形式来更新权重的。比如我们要更新权重w<sub>5</sub>就要先求取损失函数到w5的偏导$\frac{\partial L_{o 1}}{\partial w_{5}}$。从数学角度来看梯度的方向是函数增长速度最快的方向那么梯度的反方向就是函数下降最快的方向所以让损失函数减小最快的方向就是我们希望梯度w5更新的方向。这里我们再引入一个超参数α它代表了梯度更新的力度也称为学习率。好现在我们可以写出梯度更新的公式了$w_{5}^{t+1}=w_{5}^{t}-\alpha * \frac{\partial L_{o 1}}{\partial w_{5}}$。公式中的w<sub>5</sub>当然可以换成其他要更新的参数公式中的t代表着更新的次数。
对输出层神经元来说(图中的 o<sub>1</sub>我们可以直接利用梯度下降法计算神经元相关权重即图5中的权重w<sub>5</sub>和w<sub>6</sub>的梯度从而进行权重更新但对隐层神经元的相关参数比如w<sub>1</sub>),我们又该如何利用输出层的损失进行梯度下降呢?
答案是“利用求导过程中的链式法则Chain Rule”。通过链式法则我们可以解决梯度逐层反向传播的问题。最终的损失函数到权重w<sub>1</sub>的梯度是由损失函数到神经元h<sub>1</sub>输出的偏导以及神经元h<sub>1</sub>输出到权重w<sub>1</sub>的偏导相乘而来的。也就是说最终的梯度逐层传导回来“指导”权重w<sub>1</sub>的更新。
$$\frac{\partial L_{o 1}}{\partial w_{1}}=\frac{\partial L_{o 1}}{\partial h_{1}} \cdot \frac{\partial h_{1}}{\partial w_{1}}$$
具体在计算的时候,我们需要根据具体的问题明确最终损失函数的形式,以及每层神经元激活函数的形式,再根据具体的函数形式进行偏导的计算。这部分的学习需要一定的微积分基础,如果你觉得不是很好理解,也不用担心,因为对于大部分的机器学习库来说,梯度反向传播的过程已经被实现并且封装好了,直接调用就可以了,但是原理我们还是有必要了解的。
到这里,神经网络相关的基本知识我们就讲完了,前面讲了这么多,我想再带你做个总结。**神经元是神经网络中的基础结构,它参照生物学中的神经元构造,抽象出带有输入输出和激活函数的数学结构。而神经网络是通过将多个神经元以串行、并行、全连接等方式连接起来形成的网络,神经网络的训练方法是基于链式法则的梯度反向传播。**
但是细心的你可能会问了,搭建深度学习推荐系统跟神经网络到底有什么关系呢?
## 神经网络与深度学习的关系是什么?
想要搞清楚这个问题,我得先给你讲点儿神经网络和深度学习的历史。
其实学术界对于人工神经网络的研究非常早可以追溯上世纪60年代但由于计算机发展本身的限制我们没有办法完成神经网络学习所需的大量计算。直到上世纪80年代Hinton等人提出了反向传播算法BP算法神经网络才能够完成类似字母识别的简单任务。
时间到了2012年Hinton的学生Alex Krizhevsky提出了一个用于图像识别的深度神经网络结构AlexNet在当年著名的图像识别大赛ImageNet上它以碾压第二名的成绩荣获桂冠。也正是从那时起人们突然意识到原来用深层的神经网络可以完成这么复杂的任务。深度学习的浪潮从此被正式引爆了
<img src="https://static001.geekbang.org/resource/image/93/3e/9399bb223f60eec8b2fd9fc32d09303e.jpg" alt="" title="图6 著名的深度学习模型AlexNet">
结合上图你可以看到跟我们刚才讲的简单神经网络相比AlexNet无论从深度还是每一层的神经元数量来说都大大增加了并且神经元的连接方式和种类也更加丰富。
那知道了这些,我们就可以回答刚才提出的问题了。深度学习可以说是神经网络的延伸和发展,它极大地丰富神经网络的结构种类,让它能够处理各类复杂问题。
这个时候你可能又会问好像深度学习相比传统的神经网络没什么革命性的创新呀为啥到了2012年才取得这么大的突破呢这是个好问题我觉得主要有三个原因。
**一是算力的极大提高。** 到了2012年随着GPU大量应用于机器学习领域神经网络的训练速度也有了量级上的提高时至今日OpenAI刚发布的语言模型GPT-3居然有高达1750亿个参数这放在十年前是完全不可想像的。
**二是数据的极大丰富。** 之前神经网络面临的一大问题是,在训练数据量较小的情况下,模型难以收敛。但随着越来越多成熟的大数据开源平台,以及越来越多丰富的开源数据集的出现,即使神经网络的结构变得越来越复杂,我们也完全可以凭借海量的训练数据使它完全收敛。
**三是深度学习理论的进一步发展。** 虽然深度学习是站在“神经网络”这一巨人的肩膀上发展起来的但专家们也取得了非常多的理论创新就比如pooling层的加入和成功应用各类更适合深度学习的梯度下降方法的提出等等这些都让深度学习的应用成为了可能。
## 深度学习是如何应用在推荐系统中的?
前面我们介绍的都是通用的深度学习知识,但深度学习又是如何应用在推荐系统中的呢?下面,我就着重来说说这一点。
在刚才讲解神经元原理的时候,我讲到用单个神经元可以预测用户对物品感兴趣的程度。事实上,无论是单个神经元,还是结构非常复杂的深度学习网络,它们在推荐系统场景下要解决的问题都是一样的,就是**预测用户对某个物品的感兴趣程度,这个感兴趣程度往往是一个概率,最典型的就是点击率、播放率、购买概率等**。
所以在深度学习时代,我们使用深度学习模型替代了传统的推荐模型,目的就是让它作出更准确的推荐。但像上节课提出的,深度学习的革命要求我们对算力、数据都作出大幅度的调整,而这些调整因为涉及到分布式计算平台、深度学习平台,以及线上的模型服务部分,所以我们需要在推荐系统的整体架构上都作出不小的改造,让它适应深度学习时代的需求。这也是我们学习这门课的目的所在。
## 小结
今天,我带你学习了深度学习的基础知识。我们从神经元学到了神经网络,再到训练神经网络的方法,以及神经网络和深度学习的关系。今天的知识点比较密集,而且每个知识点之间都是层层递进的。为了方便你记忆,我把本节课的重点整理成了一张表格,帮你巩固所学。
<img src="https://static001.geekbang.org/resource/image/e3/a7/e3ba1a60e19832aec4c2152c14e501a7.jpeg" alt="">
这些知识是未来我们搭建深度学习推荐系统的基础。我希望你能够在动手实践之前完全掌握它。从下节课开始,我们就会进入深度学习推荐系统技术细节和实战环节,你准备好了吗?
## 课后思考
你觉得都有哪些因素影响着深度学习网络的结构?深度学习模型是越深越好吗?为什么?
欢迎在留言区分享你的答案和疑惑,如果你的朋友也想了解神经网络和深度学习的基本知识,也欢迎你把这节课分享给他。好了,今天的内容就到这里了,我们下节课见!

View File

@@ -0,0 +1,93 @@
<audio id="audio" title="国庆策划 | 关于深度学习推荐系统,我有这些资料想推荐给你" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4c/65/4c79fe03a45b2f78c1fbc93f98394c65.mp3"></audio>
你好,我是王喆。明天就是国庆和中秋假期了,这里我提前祝你节日快乐!
专栏上线以来通过跟同学们在留言区讨论我发现同学们的基础差别很大。有的同学已经是其他领域的资深工程师希望借这门课丰富一下知识体系。有的同学确实是0基础的新手可能对于机器学习的一些基本概念都不是特别清楚。
所以,我特意准备了一些适合不同阶段学习的参考书目和一些基础的实践项目,希望你能借着国庆假期查漏补缺,为我们后面的学习打好基础。
虽然这些参考书适合的学习阶段不同,但它们有两个特点,一是这些书,我都读过很多遍,它们在我学习、工作生涯的某个阶段让我受益匪浅;二是这些书都非常易读易懂,不是那种诘屈聱牙的“不适合人类阅读”的技术书。好,我们先来看看都有哪些书吧。
## 技术参考书,贵精不贵多
如果你在学习[第3节课](https://time.geekbang.org/column/article/291245)的时候有一些困难,就说明你对机器学习的一些基础概念还不太清楚,我希望你能继续巩固机器学习的基础知识,这里我推荐三本书,你根据自己的偏好和知识基础选择一本就好啦。
**第一本书是南京大学周志华老师的《机器学习》。**
<img src="https://static001.geekbang.org/resource/image/42/01/42254cfaea1cb4d1f4701d4yy5d72001.jpg" alt="">
这本书也称“西瓜书”,它的内容比较偏向传统机器学习,深度学习的内容也有,但不是重点。它的特点就是内容非常全面、详尽,语言也流畅易懂。所以,我把它推荐给机器学习基础不太好的同学,希望能够帮助你巩固基础。
**第二本书是复旦大学邱锡鹏老师的《神经网络与深度学习》。**
<img src="https://static001.geekbang.org/resource/image/79/b0/79415dyye34a8997abf3f2bfa1f592b0.jpg" alt="">
这本书也叫“蒲公英书”,它更偏向介绍神经网络和深度学习,对深度学习的介绍非常全面、详尽,但又不故作高深,是我们入门深度学习非常好的选择。
**第三本书是诸葛越和hulu机器学习团队的《百面机器学习》。**
<img src="https://static001.geekbang.org/resource/image/be/8f/bef7d9a7cf91840732ae93238dd4068f.jpg" alt="">
这本书是我在hulu时跟hulu的机器学习团队一同编写的也称“葫芦书”。它重在讲解一些机器学习领域关键的知识点如果你正在准备算法岗位的知识性面试它会是你极佳的选择。这本书比较适合有一些机器学习基础但还不够深入的同学来查漏补缺。
除此之外,如果你在学习推荐系统技术架构时还有一些疑惑的话,我再给你推荐两本推荐系统领域的书籍,你可以利用它们进一步来丰富推荐系统的知识。
<img src="https://static001.geekbang.org/resource/image/34/ab/344bb940aca63yya4cd617d885a8c7ab.jpg" alt="">
在我刚工作的时候,**项亮的这本《推荐系统实践》**让我受益匪浅,它介绍了经典的协同过滤、矩阵分解方法,还有推荐系统可以利用的数据,以及基本的评测方法等等。时至今日,其中经典的推荐系统知识仍然可以让我们受用。如果你想了解经典的推荐系统算法、技术架构,可以尝试读一读它。
<img src="https://static001.geekbang.org/resource/image/9f/91/9f74436347b19bd473dfc71e9530a291.jpg" alt="">
这里,我还想给你推荐一本我的新书,**《深度学习推荐系统》**,它是今年年初出版的。我也看到很多同学在留言区提问说“老师,咱们的专栏和这本书的区别在哪呀?”。这里,我统一回复一下。
这本书当然跟咱们专栏有着千丝万缕的联系,因为它们共享了同样的知识框架。不过,咱们的专栏重在实践,这本书注重介绍知识,二者呈互补的关系。在学习这门课的同时,你也可以购买这本书,进一步拓展自己在深度学习推荐系统这个领域的知识面。
除了这些和深度学习推荐系统非常相关的书籍之外,我还想给你推荐两本课外书,这两本书是我非常喜欢的计算机领域的“闲书”。如果假期有时间的话,我非常推荐你读一读,它们可以帮助我们建立更全面的计算机思维和系统设计理念。当然,如果你都读过,也欢迎在留言区和我分享你的看法。
<img src="https://static001.geekbang.org/resource/image/34/1c/34585b20d15e6a4449e4ebdc69e6671c.jpg" alt="">
**首先是吴军老师的《数学之美》,**这本书在我刚工作的时候给了我很多的灵感,书中的内容涉及了机器学习、人工智能、信息论、自然语言处理等等重要的计算机科学子领域。虽然涉及面非常广,但是讲得非常透,生动有趣的语言也让我完全没有阅读压力。所以,我把它推荐给想拓展知识面的同学。
<img src="https://static001.geekbang.org/resource/image/4e/ac/4e85ea6bbdb96a025cb319019ca01aac.jpg" alt="">
计算机经典书籍有很多,但最让我感到惊喜的是这本《**程序员修炼之道**》。我读这本书是两年前,因为已经有了很多大项目的开发和架构经验,所以读这本书时候,很多地方我都会会心一笑,太有感触了。书里面介绍了很多重构、架构、系统设计、程序员哲学相关的经验知识。因为推荐系统毫无疑问是一个复杂的大系统,所以有些时候,我们总要以系统的眼光去看待推荐系统的问题,这本书涉及的理念就至关重要了。
更关键的是,我非常喜欢它的副标题,“通向务实的最高境界”。我认为这是“返璞归真”的程序员的最高境界,也是我们作为一名工程师应该遵循的思考方式
## 初识实践工具,走好入门第一步
咱们这门课之后的实战环节还会涉及很多推荐系统相关的工具比如Spark、TensorFlow、Redis、Jetty Server等等。为了在之后的学习中避免一些上手的困难我建议你先熟悉一下Spark、TensorFlow和Redis这三个工具。如果你对这些工具完全没有概念可以通过我在下面介绍进行初步的了解。
首先是Spark。它是业界最流行的分布式计算平台如果你还没有相关经验的话我建议你按照我给出的三步来学习。首先你可以通过[这篇文章](https://www.zhihu.com/question/27974418)如何用形象的比喻描述大数据的技术生态Hadoop、Hive、Spark 之间是什么关系来了解一下大数据的生态然后你可以通过Spark的[官方教程](https://spark.apache.org/docs/2.4.3/quick-start.html)尝试写一个Spark Hello World程序在我们的SparrowRecSys里面新建一个Scala文件就可以。因为咱们这门课大量使用了Spark的机器学习库Spark MLlib所以最后可以通过[这个官方教程](https://spark.apache.org/docs/2.4.3/ml-guide.html)来做一些初步的了解。
其次是TensorFlow。它是我们这门课要使用的训练深度学习模型的平台。Keras是一套TensorFlow支持的API因为它的易用性我们主要利用Keras API来实现我们的推荐模型。所以我们第一步可以先看一篇[介绍TensorFlow和Keras的基本概念的文章](https://blog.csdn.net/li528405176/article/details/83857286)对它们有一个初步的认识再通过给TensorFlow的Keras接口写一个Hello World[项目](https://www.tensorflow.org/tutorials/quickstart/beginner)做一个基本的上手实践。最后,如果你还有时间,可以通过[TensorFlow官方教程](https://www.tensorflow.org/tutorials)进一步熟悉TensorFlow的其他功能。
最后是Redis。Redis是我们这门课要频繁使用的内存数据库用来存储模型所需线上特征。你可以先在官网熟悉一下[Redis](http://www.redis.cn/)[的基本介绍](http://www.redis.cn/)然后下载安装它最后尝试使用Redis内置客户端redis-cli来执行几条[基本的](http://www.redis.cn/download.html)[Redis](http://www.redis.cn/download.html)[命令](http://www.redis.cn/download.html)。
相信熟悉了深度学习的基础知识和基本工具之后你学习起来专栏后续的课程就会更加得心应手。对于完全0基础的同学我可以这样说如果你能通过我列出的书单和项目列表打牢基础学习后面的课程肯定完全没有问题
最后,我也想问问你,你在进行推荐、广告、搜索这些领域的学习的时候,还阅读过哪些非常不错的技术书?欢迎也在留言区分享出来,我们可以一起交流读后感,也可以让更多的同学受益。最后,再次祝你假期快乐,我们国庆之后再会!
## 推荐阅读
Spark
1.[大数据生态的介绍](https://www.zhihu.com/question/27974418)
2.[写一个Spark Hello World程序](https://spark.apache.org/docs/2.4.3/quick-start.html)
3.[Spark的机器学习库MLlib官方文档](https://spark.apache.org/docs/2.4.3/ml-guide.html)
TensorFlow
1.[TensorFlow和最常用的接口Keras推荐阅读](https://blog.csdn.net/li528405176/article/details/83857286)
2.[利用TensorFlow的Keras接口写一个hello world](https://www.tensorflow.org/tutorials/quickstart/beginner)
3.[TensorFlow官方教程](https://www.tensorflow.org/tutorials)
Redis
1.[Redis官方简介](http://www.redis.cn/)
2.[基本的Redis命令](http://www.redis.cn/download.html)

View File

@@ -0,0 +1,21 @@
<audio id="audio" title="国庆策划 | 深度学习推荐系统基础,你掌握了多少?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6a/86/6ace9e254d8bf5e394c33990394f1786.mp3"></audio>
你好,我是王喆。
今天是国庆假期的第二天,我在基础架构篇的基础上,设计了一套测试题,希望你能在假期里试做一下,温故知新,巩固所学。
在这套测试题中,有 10 道选择题,每道题 10 分,满分为 100。这些内容全部来自我们之前专栏所讲的内容都是基础知识希望能帮助你查缺补漏。
最后呢我还为你准备了两道实践题这两道题为选做。如果你对自己有更高的要求我希望你可以show me the code当然在答题和实践中遇到任何问题都可以在留言区评论我们一起来探讨。还等什么点击下面的按钮开始测试吧
## 选择题
[<img src="https://static001.geekbang.org/resource/image/28/a4/28d1be62669b4f3cc01c36466bf811a4.png" alt="">](http://time.geekbang.org/quiz/intro?act_id=216&amp;exam_id=666)
## 问答题
第一题通过阅读SparrowRecSys的代码你能找到首页中每行内部的电影都是按照什么排序的吗
第二道题如果你已经发现了排序的规则你能在推荐服务器的代码里添加一个叫“热度”popularity的排序规则然后让首页按照“热度”排序吗💡补充热度的定义是“这个电影被评价的次数”
我相信这是熟悉movielens数据、推荐服务器代码的一个非常好的机会那在实现过程中的任何问题都欢迎你留言和我交流我们10月9日再见

View File

@@ -0,0 +1,87 @@
<audio id="audio" title="开篇词 | 从0开始搭建一个深度学习推荐系统" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5b/c0/5b51f46c972f2b3e53ea7416eddeaec0.mp3"></audio>
你好我是王喆在一家硅谷的科技公司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&amp;Deep、PNN等深度学习模型的架构和TensorFlow实现以及注意力机制、序列模型、增强学习等相关领域的前沿进展。
效果评估篇:建立成体系的推荐系统评估机制
在效果评估篇中我们要重点学习效果评估的主要方法和指标。但对一个成熟的推荐系统来说仅熟悉这些是不够的我还期望你能通过这一篇的学习建立起包括线下评估、线上AB测试、评估反馈闭环等整套的评估体系真正能够用业界的方法而不是实验室的指标来评价一个推荐系统。
前沿拓展篇:融会贯通,追踪业界前沿
在完成整体的知识积累之后我们在通过这一篇的学习将通过业界巨头们的深度学习推荐系统方案进行融会贯通。我会重点讲解YouTube、阿里巴巴、微软、Pinterest等一线公司的深度学习应用帮助你追踪业界发展的最新趋势并且找到自己技术道路上的方向。
所有的点穿成线、连成面,就组成了我们希望掌握的深度学习推荐系统架构。与此同时,我们也会在课程完成后,搭建起一个完整的推荐系统,再收获一份亲眼看见自己学习成果的成就感。
<img src="https://static001.geekbang.org/resource/image/06/2e/066c5f56f4e0a5e8d4648e0cfb85e72e.jpg" alt="">
最后我想说No Magic不要期望一门课程就能够让你成为业界专家。如果你是完全没有推荐系统基础的新人这门课程能够让你入门推荐系统初步掌握深度学习推荐系统各模块的相关知识和业界实践。如果你是行业老兵这门课能让你查漏补缺在技术视野和格局上有所提高。但推荐系统的每个模块都有着极深的技术纵深不管是TensorFlow还是Spark还是Redis、Flink它们中的每一个都需要我们持续性的钻研才能够成为领域专家。在技术专家的道路上这仅仅是个开始当然我也很荣幸能帮你开启这个既有魅力又有挑战性的技术领域。
最后,关于深度学习推荐系统,希望你能在这里畅所欲言,提出你的困惑和疑问。也欢迎多多给我留言,你们的鼓励是我的动力。如果你身边也有想要学习深度学习推荐系统的同学,也别忘了把这个课程分享给他。**很高兴能与你一起开启深度学习推荐系统的学习之路,愿与你在攀登行业塔尖的路上共勉**。

View File

@@ -0,0 +1,130 @@
<audio id="audio" title="15 | 协同过滤:最经典的推荐模型,我们应该掌握什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a8/72/a8d1eb864a1886d6fe6b74d37ff10672.mp3"></audio>
你好,我是王喆。今天我们要开启推荐模型篇的学习。
推荐模型篇是整个课程中最重要的一个模块,因为推荐模型直接决定了最终物品排序的结果,它的好坏也直接影响着推荐效果的优劣。而且,从某种意义上讲,推荐系统的整体架构都是围绕着推荐模型搭建的,用于支持推荐模型的上线、训练、评估、服务。因此,我一直把**推荐模型称作“推荐系统这个皇冠上的明珠”**。
而提起推荐模型,我们就不能不提协同过滤算法。它可能是推荐系统自诞生以来最经典的算法,且没有之一。虽然我们课程的主题是“深度学习”推荐系统,但协同过滤以及它后续衍生出来的各类模型,都与深度学习推荐模型有着千丝万缕的联系。因此,在进入深度学习模型之前,掌握协同过滤及其衍生模型是非常有必要的。
今天我就来给你讲讲经典协同过滤和它的衍生模型矩阵分解的原理以及相关的Spark实现。
## 协同过滤算法的基本原理
我在特征工程篇曾经提到过:**“用户行为数据是推荐系统最常用,也是最关键的数据。用户的潜在兴趣、用户对物品的评价好坏都反映在用户的行为历史中”**。
而协同过滤算法,就是一种完全依赖用户和物品之间行为关系的推荐算法。我们从它的名字“协同过滤”中,也可以窥探到它背后的原理,就是 **“协同大家的反馈、评价和意见一起对海量的信息进行过滤,从中筛选出用户可能感兴趣的信息”**。
这么说可能还是太抽象了接下来我们就一起看一个电商场景下的例子。通过分析这个例子你就能搞清楚协同过滤算法的推荐过程了。这个电商推荐系统从得到原始数据到生成最终推荐分数全过程一共可以总结为6个步骤如下所示。下面我们一一来讲。
<img src="https://static001.geekbang.org/resource/image/39/42/3960001f6c049652160cb16ff3ddee42.jpg" alt="" title="图1 协同过滤的过程 (来自《深度学习推荐系统》)">
首先我们可以看到电商网站的商品库里一共有4件商品一个游戏机、一本小说、一本杂志以及一台电视机。假设现在有一名用户X访问了这个电商网站电商网站的推荐系统需要决定是否推荐电视机给用户X。
为了进行这项预测推荐系统可以利用的数据有用户X对其他商品的历史评价数据以及其他用户对这些商品的历史评价数据。我在图1(b)中用绿色“点赞”的标志表示好评,用红色“踩”的标志表示了差评。这样一来,用户、商品和评价记录就构成了带有标识的有向图。
接下来为了方便计算我们将有向图转换成矩阵的形式。这个矩阵表示了物品共同出现的情况因此被称为“共现矩阵”。其中用户作为矩阵行坐标物品作为列坐标我们再把“点赞”和“踩”的用户行为数据转换为矩阵中相应的元素值。这里我们将“点赞”的值设为1将“踩”的值设为-1“没有数据”置为0如果用户对物品有具体的评分那么共现矩阵中的元素值可以取具体的评分值没有数据时的默认评分也可以取评分的均值
你发现了吗生成共现矩阵之后推荐问题就转换成了预测矩阵中问号元素图1(d)所示的值的问题。由于在“协同”过滤算法中推荐的原理是让用户考虑与自己兴趣相似用户的意见。因此我们预测的第一步就是找到与用户X兴趣最相似的nTop n用户这里的n是一个超参数个用户然后综合相似用户对“电视机”的评价得出用户X对“电视机”评价的预测。
从共现矩阵中我们可以知道用户B和用户C由于跟用户X的行向量近似被选为Top n这里假设n取2相似用户接着在图1(e)中我们可以看到用户B和用户C对“电视机”的评价均是负面的。因为相似用户对“电视机”的评价是负面的所以我们可以预测出用户X对“电视机”的评价也是负面的。在实际的推荐过程中推荐系统不会向用户X推荐“电视机”这一物品。
到这里协同过滤的算法流程我们就说完了。也许你也已经发现了这个过程中有两点不严谨的地方一是用户相似度到底该怎么定义二是最后我们预测用户X对“电视机”的评价也是负面的这个负面程度应该有一个分数来衡量但这个推荐分数该怎么计算呢
### 计算用户相似度
首先我们来解决计算用户相似度的问题。计算用户相似度其实并不是什么难事因为在共现矩阵中每个用户对应的行向量其实就可以当作一个用户的Embedding向量。相信你早已经熟悉Embedding相似度的计算方法那我们这里依葫芦画瓢就可以知道基于共现矩阵的用户相似度计算方法啦。
最经典的方法就是利用余弦相似度了它衡量了用户向量i和用户向量j之间的向量夹角大小。夹角越小余弦相似度越大两个用户越相似它的定义如下
除了最常用的余弦相似度之外,相似度的定义还有皮尔逊相关系数、欧式距离等等。咱们课程主要使用的是余弦相似度,因此你只要掌握它就可以了,其他的定义我这里不再多说了。
### 用户评分的预测
接下来我们再来看看推荐分数的计算。在获得Top n个相似用户之后利用Top n用户生成最终的用户$u$对物品$p$的评分是一个比较直接的过程。这里,我们假设的是“目标用户与其相似用户的喜好是相似的”,根据这个假设,我们可以利用相似用户的已有评价对目标用户的偏好进行预测。最常用的方式是,利用用户相似度和相似用户评价的加权平均值,来获得目标用户的评价预测,公式如下所示。
$$<br>
\mathrm{R}_{u, p}=\frac{\sum_{s \epsilon S}\left(w_{u, s} \cdot R_{s, p}\right)}{\sum_{s \in S} w_{u, s}}<br>
$$
其中,权重$w_{u, s}$是用户$u$和用户$s$的相似度,$R_{s, p}$ 是用户$s$对物品$p$的评分。
在获得用户$u$对不同物品的评价预测后,最终的推荐列表根据评价预测得分进行排序即可得到。到这里,我们就完成了协同过滤的全部推荐过程。
## 矩阵分解算法的原理
虽然说协同过滤是目前公认的最经典的推荐算法但我们还是可以轻松找出它的缺点那就是共现矩阵往往非常稀疏在用户历史行为很少的情况下寻找相似用户的过程并不准确。于是著名的视频流媒体公司Netflix对协同过滤算法进行了改进提出了矩阵分解算法加强了模型处理稀疏矩阵的能力。
这里我们还是用一个直观的例子来理解一下什么叫做矩阵分解。这次我从Netflix的矩阵分解论文中截取了两张示意图图2来比较协同过滤和矩阵分解的原理。
<img src="https://static001.geekbang.org/resource/image/63/f8/63f52fe38288a9b31c2d8e7640f8c4f8.jpg" alt="" title="图2 “协同过滤左a”和“矩阵分解右b”的原理图">
如图2(a)所示协同过滤算法找到用户可能喜欢的视频的方式很直观就是利用用户的观看历史找到跟目标用户Joe看过同样视频的相似用户然后找到这些相似用户喜欢看的其他视频推荐给目标用户Joe。
矩阵分解算法则是期望为每一个用户和视频生成一个隐向量将用户和视频定位到隐向量的表示空间上如图2(b)所示距离相近的用户和视频表明兴趣特点接近在推荐过程中我们就应该把距离相近的视频推荐给目标用户。例如如果希望为图2(b)中的用户Dave推荐视频我们可以找到离Dave的用户向量最近的两个视频向量它们分别是《Oceans 11》和《The Lion King》然后我们可以根据向量距离由近到远的顺序生成Dave的推荐列表。
这个时候你肯定觉得矩阵分解不就是相当于一种Embedding方法嘛。没错**矩阵分解的主要过程,就是先分解协同过滤生成的共现矩阵,生成用户和物品的隐向量,再通过用户和物品隐向量间的相似性进行推荐**。
那这个过程的关键就在于如何分解这个共现矩阵了。从形式上看矩阵分解的过程是直观的就是把一个mxn的共现矩阵分解成一个mxk的用户矩阵和kxn的物品矩阵相乘的形式如图3
<img src="https://static001.geekbang.org/resource/image/60/fb/604b312899bff7922528df4836c10cfb.jpeg" alt="" title="图3 矩阵分解示意图">
有了用户矩阵和物品矩阵,用户隐向量和物品隐向量就非常好提取了。用户隐向量就是用户矩阵相应的行向量,而物品隐向量就是物品矩阵相应的列向量。
那关键问题就剩下一个,也就是我们该通过什么方法把共现矩阵分解开呢?最常用的方法就是梯度下降。梯度下降的原理我们在[第3讲](https://time.geekbang.org/column/article/291245)学习过,简单来说就是通过求取偏导的形式来更新权重。梯度更新的公式是 $(w^{t+1}=w^{t}-\alpha * \frac{\partial L}{\partial w})$。为了实现梯度下降,最重要的一步是定义损失函数$L$,定义好损失函数我们才能够通过求导的方式找到梯度方向,这里我们就给出矩阵分解损失函数的定义如下。
<img src="https://static001.geekbang.org/resource/image/30/3e/3034f1205b8f9ce0d2f736957ff1933e.jpeg" alt="">
这个目标函数里面,${r}_{u i}$ 是共现矩阵里面用户$u$对物品$i$的评分,${q}_{i}$ 是物品向量,${p}_{u}$ 是用户向量K是所有用户评分物品的全体集合。通过目标函数的定义我们可以看到我们要求的物品向量和用户向量是希望让物品向量和用户向量之积跟原始的评分之差的平方尽量小。简单来说就是我们希望用户矩阵和物品矩阵的乘积尽量接近原来的共现矩阵。
在通过训练得到用户隐向量和物品隐向量之后在服务器内部的推荐过程跟我们之前讲过的Embedding推荐是一样的你也已经在Sparrow RecSys里面实践过是这方面的专家了我就不再多说了。
## 矩阵分解算法的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(&quot;userIdInt&quot;)
.setItemCol(&quot;movieIdInt&quot;)
.setRatingCol(&quot;ratingFloat&quot;)
//训练模型
val model = als.fit(training)
//得到物品向量和用户向量
model.itemFactors.show(10, truncate = false)
model.userFactors.show(10, truncate = false
```
其实矩阵分解算法得出的结果你完全可以把它当作Embedding来处理。具体怎么做呢在讲Redis的时候我们就已经实现过物品Embedding和用户Embedding的存储和线上预估的过程了你可以直接参考它。最后我建议你利用矩阵分解后的用户和物品隐向量仿照其他Embedding的实现在Sparrow RecSys中动手实现一下线上部署的过程这样你就可以看到矩阵分解模型的实际效果了。
## 小结
这节课我们一起学习了协同过滤算法,以及它的后续算法矩阵分解,它是最经典的推荐算法。
总结来说,协同过滤是一种协同大家的反馈、评价和意见,对海量的信息进行过滤,从中筛选出用户感兴趣信息的一种推荐算法。它的实现过程主要有三步,先根据用户行为历史创建共现矩阵,然后根据共现矩阵查找相似用户,再根据相似用户喜欢的物品,推荐目标用户喜欢的物品。
但是协同过滤处理稀疏矩阵的能力比较差因此矩阵分解算法被提出了它通过分解共现矩阵生成用户向量矩阵和物品向量矩阵进而得到用户隐向量和物品隐向量。你可以完全把最后的结果当作用户Embedding和物品Embedding来处理。
针对这节课的重要知识点,我把它们都列在了下面的表格里,你可以看看。
<img src="https://static001.geekbang.org/resource/image/5f/12/5f02442573af2202a85eb3e4bb895212.jpeg" alt="">
## 课后思考
1.基于协同过滤算法,你能找到进行相似“物品”推荐的方法吗?
2.在MovieLens数据集中不同用户对物品打分的标准不尽相同。比如说有的人可能爱打高分评价的影片得分都在4分以上有的人爱打低分大部分影片都在3分以下。你觉得这样的偏好对于推荐结果有影响吗我们能不能在算法中消除这种偏好呢
关于矩阵分解算法的实现你学会了吗?欢迎把你的疑问和思考分享到留言区,也欢迎你能把这节课转发出去,我们下节课见!

View File

@@ -0,0 +1,104 @@
<audio id="audio" title="16 | 深度学习革命:深度学习推荐模型发展的整体脉络是怎样的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6c/cd/6c78a245f1eebb50f2860c5d3f75fbcd.mp3"></audio>
你好,我是王喆。今天,我们要开始学习激动人心的深度推荐模型部分了。
当下几乎所有互联网巨头的推荐业务中都有对深度学习推荐模型的落地和应用。从早期微软的Deep Crossing、Google的Wide&amp;Deep、阿里的MLR到现在影响力非常大的模型DIN、DIENYouTube的深度推荐模型等等。因此对于算法工程师来说紧跟业界的脚步去了解和掌握深度学习推荐模型是非常必要的。
那你可能想问了,深度学习推荐模型这么多,发展这么快,也没有一个统一的模板,我们该学哪个,怎么学呢?我想说的是,算法工程师的工作是一个持续优化和迭代的过程,如果想要追求更好的推荐效果,我们的思路不应该只局限于某一个被成功应用的模型,而是应该把眼光放得更高、更宽,去思考这些成功的推荐模型在业界下一步的发展方向是什么?有没有哪些其他的模型结构的思路可以借鉴。这些都是你在这个岗位上取得持续成功的关键。
那怎么才能做到这一点呢?我认为,只有建立起一个比较全面的深度学习模型知识库,我们才能在工作中做出正确的技术选择,为模型的下一步改进方向找到思路。
因此,这节课,我想和你深入聊一聊业界影响力非常大的深度学习推荐模型,以及它们之间的发展关系,带你从整体上建立起深度学习推荐模型的发展脉络。这不仅是我们建立行业知识储备的必需,也为我们后面实现深度推荐模型打下了基础。
## 深度学习对推荐系统的影响详解
在第一节课中,我们曾说过,**深度学习给推荐系统带来了革命性的影响**,能够显著提升推荐系统的效果,原因主要有两点,一是深度学习极大地增强了推荐模型的拟合能力,二是深度学习模型可以利用模型结构模拟用户兴趣的变迁、用户注意力机制等不同的用户行为过程。接下来,我们就结合这两点,来说说深度学习模型到强在哪里。
### 1. 深度学习模型的强拟合能力
首先我们来说说深度学习模型的强拟合能力。上一节课我们学习了经典的推荐算法矩阵分解。在矩阵分解模型的结构图1左用户One-hot向量和物品One-hot向量分居两侧它们会先通过隐向量层转换成用户和物品隐向量再通过点积的方式交叉生成最终的打分预测。
但是点积这种特征向量交叉的方式毕竟过于简单了在数据模式比较复杂的情况下往往存在欠拟合的情况。而深度学习就能大大加强模型的拟合能力比如在NeuralCF神经网络协同过滤模型中点积层被替换为多层神经网络理论上多层神经网络具备拟合任意函数的能力所以我们通过增加神经网络层的方式就能解决模型欠拟合的问题了。
<img src="https://static001.geekbang.org/resource/image/70/09/7063d223da013845534d3c84b7ab9409.jpg" alt="" title="图1 矩阵分解模型示意图
">
如果你不知道什么是欠拟合、正确拟合和过拟合的现象,可能就无法理解神经网络到底解决了什么问题。这里我就带你看一张很经典的示意图,来详细聊聊这三种现象 。
**“欠拟合”指的是模型复杂度低,无法很好地拟合训练集数据的现象。** 就像图2展示的那样模型曲线没办法“准确”地找到正负样本的分界线而深度学习模型就可以大大增加模型的“非线性”拟合能力像图2一样找到更加合适的分类面更准确地完成分类任务。当然过分复杂的深度学习模型存在着“过拟合”的风险**“过拟合”是指模型在训练集上的误差很小,但在测试集上的误差较大的现象**就像图2一样模型曲线过分精确地刻画分界线而忽略了对噪声的容忍能力这对于未知样本的预估来说往往是不利的。
[<img src="https://static001.geekbang.org/resource/image/88/87/885010f805fb5290ec53f4fd6667e087.jpg" alt="" title="图2拟合能力示意图图片来自Geeksforgeeks">](https://www.geeksforgeeks.org/regularization-in-machine-learning/)
### 2. 深度学习模型结构的灵活性
说完了深度学习模型的强拟合能力,我们再来看看它的灵活性。这里,你可能会有疑问了,灵活性和深度学习模型模拟用户行为有什么关系呢?我们先接着往下看。
如果你读过一些深度学习相关的论文肯定会发现每篇论文中的模型结构都不尽相同就像图3展示的那样它们有的是好多层有的像一个串是串在一起的而有的像一张网一样拥有多个输入和多个输出甚至还有的像金字塔会从输入到输出逐层变窄。
虽然,模型结构的复杂性让我们难以掌握它们的规律,但也正是因为深度模型拥有这样的灵活性,让它能够更轻松地模拟人们的思考过程和行为过程,让推荐模型像一个无所不知的超级大脑一样,把用户猜得更透。
[<img src="https://static001.geekbang.org/resource/image/60/26/60d7f9aaa4562251c1dyy78712a2fe26.jpg" alt="" title="图3 不同深度学习模型的结构图片来自ResearchGate">](https://www.researchgate.net/figure/The-structures-of-different-deep-learning-models_fig2_340123883)
这其中典型的例子就是阿里巴巴的模型DIN深度兴趣网络和DIEN深度兴趣进化网络。它们通过在模型结构中引入注意力机制和模拟兴趣进化的序列模型来更好地模拟用户的行为。
<img src="https://static001.geekbang.org/resource/image/20/32/202cfa968b1aa6fa4349722bb4ab4332.jpg" alt="" title="图4 DIN模型和DIEN的模型示意图">
我们重点关注图4的DIN模型它在神经网络中增加了一个叫做“激活单元“的结构这个单元就是为了模拟人类的注意力机制。举个例子来说我们在购买电子产品比如说笔记本电脑的时候更容易拿之前购买电脑的经验或者其他电子产品的经验来指导当前的购买行为很少会借鉴购买衣服和鞋子的经验。这就是一个典型的注意力机制我们只会注意到相关度更高的历史购买行为而DIN模型就是模拟了人类的注意力特点。
DIN模型的改进版DIEN模型就更厉害了它不仅引入了注意力机制还模拟了用户兴趣随时间的演化过程。我们来看那些彩色的层这一层层的序列结构模拟的正是用户兴趣变迁的历史通过模拟变迁的历史DIEN模型可以更好地预测下一步用户会喜欢什么。
这些通过改变模型结构来模拟用户行为的做法不胜枚举,很多重要的深度学习模型的改进动机也是基于这样的原理。也正是因为这样的灵活性,正确、全面地掌握不同深度学习模型的特点,以及它们之间的发展关系变得异常重要,只有这样,我们才能在实践中做到有的放矢、灵活应用。
## 深度学习推荐模型的演化关系图
说了这么多我们到底该怎么掌握不同深度学习模型之间的关系呢这里我梳理出了一张深度学习模型5年内的发展过程图图中的每一个节点都是一个重要的模型结构节点之间的连线也揭示了不用模型间的联系。
接下来我就带你梳理一下图中重要模型的原理以及不同模型间的关系。而在之后的课程中我们还会进一步学习重点模型的技术细节并且基于TensorFlow对它们进行实现。
<img src="https://static001.geekbang.org/resource/image/10/c5/10e8105911823d96348dc7288d4d26c5.jpg" alt="" title="图5 主流深度学习推荐模型的演化图谱">
首先我们来看整个演化图最中心部分这是深度学习最基础的结构我们叫它“多层神经网络”或者“多层感知机”简称MLPMultiLayer Perceptron。多层感知机的原理我们在[第3讲](https://time.geekbang.org/column/article/291245)中讲过,它就像一个黑盒,会对输入的特征进行深度地组合交叉,然后输出对兴趣值的预测。其他的深度推荐模型全都是在多层感知机的基础上,进行结构上的改进而生成的,所以**“多层感知机”是整个演化图的核心**。
从多层感知机向上还有一个重点模型我们需要知道那就是Deep Crossing。Deep Crossing实际上是一类经典深度学习模型的代表相比于MLPDeep Crossing在原始特征和MLP之间加入了Embedding层。这样一来输入的稀疏特征先转换成稠密Embedding向量再参与到MLP中进行训练这就解决了MLP不善于处理稀疏特征的问题。可以说**Embedding+MLP的结构是最经典也是应用最广的深度学习推荐模型结构**。
从MLP向下我们看到了Google提出的推荐模型Wide&amp;Deep。它把深层的MLP和单层的神经网络结合起来希望同时让网络具备很好的“记忆性”和“泛化性”。对“记忆性”和“泛化性”这两个名词陌生的同学也不用着急我们后面的课程会专门来讲解Wide&amp;Deep。
Wide&amp;Deep提出以来凭借着“易实现”“易落地”“易改造”的特点获得了业界的广泛应用。围绕着Wide&amp;Deep还衍生出了诸多变种比如通过改造Wide部分提出的Deep&amp;Cross和DeepFM通过改造Deep部分提出的AFM、NFM等等。总之**Wide&amp;Deep是业界又一得到广泛应用的深度推荐模型**。
除此之外我们还可以看到经典的深度学习模型跟其他机器学习子领域的交叉。这里我给你举3个比较著名的例子第1个是深度学习和注意力机制的结合诞生了阿里的深度兴趣网络DIN浙大和新加坡国立提出的AFM等等第2个是把序列模型引入MLP+Embedding的经典结构诞生了阿里的深度兴趣进化网络DIEN第3个是把深度学习和强化学习结合在一起诞生了微软的深度强化学习网络DRN以及包括[美团](https://tech.meituan.com/2018/11/15/reinforcement-learning-in-mt-recommend-system.html)、[阿里](https://102.alibaba.com/downloadFile.do?file=1517812754285/reinforcement_learning.pdf)在内的非常有价值的业界应用。
看了这诸多模型的演进过程,你肯定想问模型的演化有什么规律可循吗?接下来,我就把我总结出的,关于模型改进的四个方向告诉你 。
**一是改变神经网络的复杂程度。** 从最简单的单层神经网络模型AutoRec到经典的深度神经网络结构Deep Crossing它们主要的进化方式在于增加了深度神经网络的层数和结构复杂度。
**二是改变特征交叉方式。** 这种演进方式的要点在于大大提高了深度学习网络中特征交叉的能力。比如说改变了用户向量和物品向量互操作方式的NeuralCF定义了多种特征向量交叉操作的PNN等等。
**三是把多种模型组合应用。** 组合模型主要指的就是以Wide&amp;Deep模型为代表的一系列把不同结构组合在一起的改进思路。它通过组合两种甚至多种不同特点、优势互补的深度学习网络来提升模型的综合能力。
**四是让深度推荐模型和其他领域进行交叉。** 我们从DIN、DIEN、DRN等模型中可以看出深度推荐模型无时无刻不在从其他研究领域汲取新的知识。事实上这个过程从未停歇我们从今年的推荐系统顶会Recsys2020中可以看到NLP领域的著名模型Bert又与推荐模型结合起来并且产生了非常好的效果。一般来说自然语言处理、图像处理、强化学习这些领域都是推荐系统经常汲取新知识的地方。
总的来说,深度学习推荐模型的发展快、思路广,但每种模型都不是无本之木,它们的发展脉络都有迹可循。想要掌握好这些模型,在实际工作中做到拿来就用,我们就需要让这些模型脉络图像知识树一样扎根在心中,再通过不断地实践来掌握技术细节。
## 小结
这节课,我们通过学习深度学习对推荐系统的影响要素,以及经典深度学习模型之间的关系,初步建立起了深度学习模型的知识库。
我们知道,深度学习能够提升推荐系统的效果有两个关键因素,分别是它的“强拟合能力”和“结构的灵活性”。
对于“强拟合能力”来说,深度学习模型可以大大增加模型的“非线性”拟合能力,对复杂数据模型进行更准确的分类,避免“欠拟合”现象的发生,从而提升推荐效果。
对于“结构的灵活性”来说,深度学习模型可以通过灵活调整自身的结构,更轻松恰当地模拟人们的思考过程和行为过程,把用户猜得更透。
而整个深度学习推荐模型的演化过程是从最经典的多层神经网络向不同方向开枝散叶比如结合协同过滤发展出了NerualCF加入Embedding层发展出以Deep Crossing为代表的Embedding+MLP的结构以及把深度神经网络和单层网络结合起来发展出Wide&amp;Deep模型等等。
在这节课我们可以先忽略每个模型的细节着重建立一个整体的知识框架。之后的课程中我不仅会带你一一揭晓它们的技术细节还会利用TensorFlow实现其中几个经典的模型。期待继续与你一起学习
最后,我还是把这节课的重点知识梳理成了表格的形式,你可以借助它来复习巩固。
<img src="https://static001.geekbang.org/resource/image/be/ce/be5a662d3abd6a8dd5411038718d6cce.jpeg" alt="">
## 课后思考
有的同学说,深度学习这么流行,我把一些经典的深度模型结构实现好,肯定能提升我们公司推荐系统的效果,你觉得这种观点有问题吗?你觉得除了模型结构,还有哪些影响推荐效果的因素?为什么?
好啦,关于深度学习模型的知识库你建立起来了吗?欢迎把你的疑问和思考分享到留言区,也欢迎你能把这节课转发出去,我们下节课见!

View File

@@ -0,0 +1,253 @@
<audio id="audio" title="17 | Embedding+MLP如何用TensorFlow实现经典的深度学习模型" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8f/db/8f34e48405693c86345902528f4d41db.mp3"></audio>
你好,我是王喆。
今天我们正式进入深度学习模型的实践环节来一起学习并实现一种最经典的模型结构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层。
接下来,我就从下到上来给你讲讲每一层的功能是什么,以及它们的技术细节分别是什么样的。
<img src="https://static001.geekbang.org/resource/image/50/71/5076071d3c69d3a9fff848a9e631f371.jpeg" alt="" title="图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红框内的部分。参照下面的示意图我们可以看到这部分就是一个从输入层到隐层之间的全连接网络。
<img src="https://static001.geekbang.org/resource/image/8a/29/8a26d9a531ae8bef89f3730388f59a29.jpeg" alt="" title="图2 Word2vec模型中Embedding层的部分">
一般来说Embedding向量的维度应远小于原始的稀疏特征向量按照经验几十到上百维就能够满足需求这样它才能够实现从稀疏特征向量到稠密特征向量的转换。
接着我们来看Stacking层。Stacking层中文名是堆叠层我们也经常叫它连接Concatenate层。它的作用比较简单就是把不同的Embedding特征和数值型特征拼接在一起形成新的包含全部特征的特征向量。
再往上看MLP层就是我们开头提到的多层神经网络层在图1中指的是Multiple Residual Units层中文叫多层残差网络。微软在实现Deep Crossing时针对特定的问题选择了残差神经元但事实上神经元的选择有非常多种比如我们之前在深度学习基础知识中介绍的以Sigmoid函数为激活函数的神经元以及使用tanh、ReLU等其他激活函数的神经元。我们具体选择哪种是一个调参的问题一般来说ReLU最经常使用在隐层神经元上Sigmoid则多使用在输出神经元实践中也可以选择性地尝试其他神经元根据效果作出最后的决定。
不过不管选择哪种神经元MLP层的特点是全连接就是不同层的神经元两两之间都有连接。就像图3中的两层神经网络一样它们两两连接只是连接的权重会在梯度反向传播的学习过程中发生改变。
<img src="https://static001.geekbang.org/resource/image/7a/99/7a2b22c106c454af3db2edaed555e299.jpeg" alt="" title="图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的数值型特征具体的特征选择你可以看看下面的表格
<img src="https://static001.geekbang.org/resource/image/af/94/af3fabdcb119c9e06ddc0f7225bbc094.jpg" alt="">
选择好特征后就是MLP部分的模型设计了。我们选择了一个三层的MLP结构其中前两层是128维的全连接层。我们这里采用好评/差评标签作为样本标签因此要解决的是一个类CTR预估的二分类问题对于二分类问题我们最后一层采用单个sigmoid神经元作为输出层就可以了。
当然了我知道你肯定对这一步的细节实现有很多问题比如为什么要选三层的MLP结构为什么要选sigmoid作为激活函数等等。其实我们对模型层数和每个层内维度的选择是一个超参数调优的问题这里的选择不能保证最优我们需要在实战中需要根据模型的效果进行超参数的搜索找到最适合的模型参数。
### Embedding+MLP模型的TensorFlow实现
确定好了特征和模型结构就万事俱备只欠实现了。下面我们就看一看利用TensorFlow的Keras接口如何实现Embedding+MLP的结构。总的来说TensorFlow的实现有七个步骤。因为这是我们课程中第一个TensorFlow的实现所以我会讲得详细一些。而且我也把全部的参考代码放在了Sparrow Recsys项目TFRecModel模块的EmbeddingMLP.py你可以结合它来听我下面的讲解。
**我们先来看第一步导入TensorFlow包。** 如果你按照实战准备一的步骤配置好了TensorFlow Python环境就可以成功地导入TensorFlow包。接下来你要做的就是定义好训练数据的路径TRAIN_DATA_URL了然后根据你自己训练数据的本地路径替换参考代码中的路径就可以了。
```
import tensorflow as tf
TRAIN_DATA_URL = &quot;file:///Users/zhewang/Workspace/SparrowRecSys/src/main/resources/webroot/sampledata/modelSamples.csv&quot;
samples_file_path = tf.keras.utils.get_file(&quot;modelSamples.csv&quot;, 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=&quot;?&quot;,
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实践部分我们利用上节课处理好的特征和训练数据实现了Sparrow Recsys项目中的第一个深度学习模型。在实践过程中我们要重点掌握类别型特征的处理方法模型的定义方式和训练方式以及最后的模型评估方法。
我也把这些重点知识总结在了一张表格里,你可以利用它来认真回顾。
<img src="https://static001.geekbang.org/resource/image/4e/67/4e34e77589d386c8924542794dyy1867.jpg" alt="">
今天我们一起完成了Embedding MLP模型的实现。在之后的课程中我们会进一步实现其他深度学习模型通过模型的评估进行效果上的对比。另外我们也会利用训练出的深度学习模型完成Sparrow Recsys的猜你喜欢功能期待与你一起不断完善我们的项目。
## 课后思考
在我们实现的Embedding+MLP模型中也有用户Embedding层和物品Embedding层。你觉得从这两个Embedding层中抽取出来的用户和物品Embedding能直接用来计算用户和物品之间的相似度吗为什么
欢迎把你的思考和疑惑写在留言区也欢迎你把这节课转发给希望用TensorFlow实现深度推荐模型的朋友我们下节课见

View File

@@ -0,0 +1,117 @@
<audio id="audio" title="18Wide&Deep怎样让你的模型既有想象力又有记忆力" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/39/92/392da33b177987db0364c30d3efbc292.mp3"></audio>
你好,我是王喆。
今天我们来学习一个在业界有着巨大影响力的推荐模型Google的Wide&amp;Deep。可以说只要掌握了Wide&amp;Deep我们就抓住了深度推荐模型这几年发展的一个主要方向。那Wide&amp;Deep模型是什么意思呢我们把它翻译成中文就是“宽并且深的模型”。
这个名字看起来好像很通俗易懂但你真的理解其中“宽”和“深”的含义吗上一节课我们讲过Embedding+MLP的经典结构因为MLP可以有多层神经网络的结构所以它是一个比较“深”的模型但Wide&amp;Deep这个模型说的“深”和MLP是同样的意思吗“宽”的部分又是什么样的呢宽和深分别有什么不同的作用呢以及我们为什么要把它们结合在一起形成一个模型呢
这节课就让我们就带着这诸多疑问从模型的结构开始学起再深入到Wide&amp;Deep在Google的应用细节中去最后亲自动手实现这个模型。
## Wide&amp;Deep模型的结构
首先我们来看看Wide&amp;Deep模型的结构理解了结构再深入去学习细节我们才能掌握得更好。
<img src="https://static001.geekbang.org/resource/image/fb/e0/fb17112c951ebb2a515f12dace262de0.jpg" alt="" title="图1 Wide&amp;Deep模型结构 [br]出自Wide &amp; Deep Learning for Recommender Systems ">
上图就是Wide&amp;Deep模型的结构图了它是由左侧的Wide部分和右侧的Deep部分组成的。Wide部分的结构太简单了就是把输入层直接连接到输出层中间没有做任何处理。Deep层的结构稍复杂但我相信你也不会陌生因为它就是我们上节课学习的Embedding+MLP的模型结构。
知道了Wide&amp;Deep模型的结构之后我们先来解决第一个问题那就是Google为什么要创造这样一个混合式的模型结构呢这我们还得从Wide部分和Deep部分的不同作用说起。
简单来说Wide部分的主要作用是让模型具有较强的“记忆能力”Memorization而Deep部分的主要作用是让模型具有“泛化能力”Generalization因为只有这样的结构特点才能让模型兼具逻辑回归和深度神经网络的优点也就是既能快速处理和记忆大量历史行为特征又具有强大的表达能力这就是Google提出这个模型的动机。
那么问题又来了所谓的“记忆能力”和“泛化能力”到底指什么呢这我们就得好好聊一聊了因为理解这两种能力是彻底理解Wide&amp;Deep模型的关键。
### 模型的记忆能力
**所谓的 “记忆能力”,可以被宽泛地理解为模型直接学习历史数据中物品或者特征的“共现频率”,并且把它们直接作为推荐依据的能力 。** 就像我们在电影推荐中可以发现一系列的规则比如看了A电影的用户经常喜欢看电影B这种“因为A所以B”式的规则非常直接也非常有价值。
但这类规则有两个特点一是数量非常多一个“记性不好”的推荐模型很难把它们都记住二是没办法推而广之因为这类规则非常具体没办法或者说也没必要跟其他特征做进一步的组合。就像看了电影A的用户80%都喜欢看电影B这个特征已经非常强了我们就没必要把它跟其他特征再组合在一起。
现在我们就可以回答开头的问题了为什么模型要有Wide部分就是因为Wide部分可以增强模型的记忆能力让模型记住大量的直接且重要的规则这正是单层的线性模型所擅长的。
### 模型的泛化能力
接下来,我们来谈谈模型的“泛化能力”。**“泛化能力”指的是模型对于新鲜样本、以及从未出现过的特征组合的预测能力。** 这怎么理解呢我们还是来看一个例子。假设我们知道25岁的男性用户喜欢看电影A35岁的女性用户也喜欢看电影A。如果我们想让一个只有记忆能力的模型回答“35岁的男性喜不喜欢看电影A”这样的问题这个模型就会“说”我从来没学过这样的知识啊没法回答你。
这就体现出泛化能力的重要性了。模型有了很强的泛化能力之后,才能够对一些非常稀疏的,甚至从未出现过的情况作出尽量“靠谱”的预测。
回到刚才的例子有泛化能力的模型回答“35岁的男性喜不喜欢看电影A”这个问题它思考的逻辑可能是这样的从第一条知识“25岁的男性用户喜欢看电影A“中我们可以学到男性用户是喜欢看电影A的。从第二条知识“35岁的女性用户也喜欢看电影A”中我们可以学到35岁的用户是喜欢看电影A的。那在没有其他知识的前提下35岁的男性同时包含了合适的年龄和性别这两个特征所以他大概率也是喜欢电影A的。这就是模型的泛化能力。
事实上,我们学过的矩阵分解算法,就是为了解决协同过滤“泛化能力”不强而诞生的。因为协同过滤只会“死板”地使用用户的原始行为特征,而矩阵分解因为生成了用户和物品的隐向量,所以就可以计算任意两个用户和物品之间的相似度了。这就是泛化能力强的另一个例子。
从上节课中我们学过深度学习模型有很强的数据拟合能力在多层神经网络之中特征可以得到充分的交叉让模型学习到新的知识。因此Wide&amp;Deep模型的Deep部分就沿用了上节课介绍的Embedding+MLP的模型结构来增强模型的泛化能力。
清楚了记忆能力和泛化能力是什么之后让我们再回到Wide&amp;Deep模型提出时的业务场景上去理解Wide&amp;Deep模型是怎么综合性地学习到记忆能力和泛化能力的。
## Wide&amp;Deep模型的应用场景
Wide&amp;Deep模型是由Google的应用商店团队Google Play提出的在Google Play为用户推荐APP这样的应用场景下Wide&amp;Deep模型的推荐目标就显而易见了就是应该尽量推荐那些用户可能喜欢愿意安装的应用。那具体到Wide&amp;Deep模型中Google Play团队是如何为Wide部分和Deep部分挑选特征的呢下面我们就一起来看看。
<img src="https://static001.geekbang.org/resource/image/4b/be/4b2f89d82768ba851e3f7392b17df2be.jpg" alt="" title="图2 Google Play Wide&amp;Deep模型的细节 [br]出自Wide &amp; Deep Learning for Recommender Systems ">
我们先来看看图2它补充了Google Play Wide&amp;Deep模型的细节让我们可以清楚地看到各部分用到的特征是什么。我们先从右边Wide部分的特征看起。这部分很简单只利用了两个特征的交叉这两个特征是“已安装应用”和“当前曝光应用”。这样一来Wide部分想学到的知识就非常直观啦就是希望记忆好“如果A所以B”这样的简单规则。在Google Play的场景下就是希望记住“如果用户已经安装了应用A是否会安装B”这样的规则。
接着我们再来看看左边的Deep部分它就是一个非常典型的Embedding+MLP结构了。我们看到其中的输入特征很多有用户年龄、属性特征、设备类型还有已安装应用的Embedding等等。我们把这些特征一股脑地放进多层神经网络里面去学习之后它们互相之间会发生多重的交叉组合这最终会让模型具备很强的泛化能力。
比如说我们把用户年龄、人口属性和已安装应用组合起来。假设样本中有25岁男性安装抖音的记录也有35岁女性安装抖音的记录那我们该怎么预测25岁女性安装抖音的概率呢这就需要用到已有特征的交叉来实现了。虽然我们没有25岁女性安装抖音的样本但模型也能通过对已有知识的泛化经过多层神经网络的学习来推测出这个概率。
总的来说Wide&amp;Deep通过组合Wide部分的线性模型和Deep部分的深度网络取各自所长就能得到一个综合能力更强的组合模型。
## Wide&amp;Deep模型的TensorFlow实现
在理解了Wide&amp;Deep模型的原理和技术细节之后就又到了“show me the code”的环节了。接下来我们就动手在Sparrow Recsys上实现Wide&amp;Deep模型吧
通过上节课的实战我相信你已经熟悉了TensorFlow的大部分操作也清楚了载入训练数据创建TensorFlow所需的Feature column的方法。因此Wide&amp;Deep模型的实践过程中我们会重点关注定义模型的部分。
这里我们也会像上节课一样继续使用TensorFlow的Keras接口来构建Wide&amp;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&amp;Deep是非常方便的Deep部分基本没有变化我们只需要加上Wide部分的特征和设置就可以了。Wide&amp;Deep的全部相关代码我都实现在了Sparrow Recsys的WideNDeep.py文件中你可以直接参考源代码。但我更希望你能尝试设置不同的特征以及不同的参数组合来真实地体验一下深度学习模型的调参过程。
## 小结
这节课我们一起实现了业界影响力非常大的深度学习模型Wide&amp;Deep它是由Wide部分和Deep部分组成的。其中Wide部分主要是为了增强模型的“记忆能力”让模型记住“如果A那么B”这样的简单但数量非常多的规则。Deep部分是为了增强模型的“泛化能力”让模型具备对于稀缺样本、以及从未出现过的特征组合的预测能力。Wide&amp;Deep正是通过这样取长补短的方式让模型的综合能力提升。
在具体实践的时候我们继续使用TensorFlow的Keras接口实现了Wide&amp;Deep模型。相比上节课Embedding MLP模型的实现我们新加入了“用户已好评电影”和“当前评价电影”组成的交叉特征crossed_feature让Wide部分学习“一个喜欢电影A的用户也会喜欢电影B”这样的规则。
好了,这就是我们这节课的主要内容,同样,我也把重要的知识点总结在了表格里,你可以利用它来巩固复习。
<img src="https://static001.geekbang.org/resource/image/1d/12/1d5985d8e9b7d92a87baa80f619a8a12.jpeg" alt="">
## 课后思考
对于Deep部分来说你觉得我们一股脑地把所有特征都扔进MLP中去训练这样的方式有没有什么改进的空间比如说“用户喜欢的电影风格”和“电影本身的风格”这两个特征我们能不能进一步挖掘出它们之间的相关性而不是简单粗暴地扔给神经网络去处理呢
欢迎把你的思考和疑问写在留言区如果的你朋友也正在为Wide&amp;Deep模型的实现而困扰欢迎你把这节课转发给他我们下节课见

View File

@@ -0,0 +1,122 @@
<audio id="audio" title="19NeuralCF如何用深度学习改造协同过滤" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/61/d6/610fb4a963d9962d01c6fd59663606d6.mp3"></audio>
你好我是王喆今天我们来学习协同过滤的深度学习进化版本NeuralCF。
在[第15讲](https://time.geekbang.org/column/article/305182)里,我们学习了最经典的推荐算法,协同过滤。在前深度学习的时代,协同过滤曾经大放异彩,但随着技术的发展,协同过滤相比深度学习模型的弊端就日益显现出来了,因为它是通过直接利用非常稀疏的共现矩阵进行预测的,所以模型的泛化能力非常弱,遇到历史行为非常少的用户,就没法产生准确的推荐结果了。
虽然,我们可以通过矩阵分解算法增强它的泛化能力,但因为矩阵分解是利用非常简单的内积方式来处理用户向量和物品向量的交叉问题的,所以,它的拟合能力也比较弱。这该怎么办呢?不是说深度学习模型的拟合能力都很强吗?我们能不能利用深度学习来改进协同过滤算法呢?
当然是可以的。2017年新加坡国立的研究者就使用深度学习网络来改进了传统的协同过滤算法取名NeuralCF神经网络协同过滤。NeuralCF大大提高了协同过滤算法的泛化能力和拟合能力让这个经典的推荐算法又重新在深度学习时代焕发生机。这节课我们就一起来学习并实现NeuralCF
## NeuralCF模型的结构
在学习NeuralCF之前我们先来简单回顾一下协同过滤和矩阵分解的原理。协同过滤是利用用户和物品之间的交互行为历史构建出一个像图1左一样的共现矩阵。在共现矩阵的基础上利用每一行的用户向量相似性找到相似用户再利用相似用户喜欢的物品进行推荐。
<img src="https://static001.geekbang.org/resource/image/60/fb/604b312899bff7922528df4836c10cfb.jpeg" alt="" title="图1 矩阵分解算法的原理">
矩阵分解则进一步加强了协同过滤的泛化能力它把协同过滤中的共现矩阵分解成了用户矩阵和物品矩阵从用户矩阵中提取出用户隐向量从物品矩阵中提取出物品隐向量再利用它们之间的内积相似性进行推荐排序。如果用神经网络的思路来理解矩阵分解它的结构图就是图2这样的。
<img src="https://static001.geekbang.org/resource/image/e6/bd/e61aa1d0d6c75230ff75c2fb698083bd.jpg" alt="" title="图2 矩阵分解的神经网络化示意图">
图2 中的输入层是由用户ID和物品ID生成的One-hot向量Embedding层是把One-hot向量转化成稠密的Embedding向量表达这部分就是矩阵分解中的用户隐向量和物品隐向量。输出层使用了用户隐向量和物品隐向量的内积作为最终预测得分之后通过跟目标得分对比进行反向梯度传播更新整个网络。
把矩阵分解神经网络化之后把它跟Embedding+MLP以及Wide&amp;Deep模型做对比我们可以一眼看出网络中的薄弱环节矩阵分解在Embedding层之上的操作好像过于简单了就是直接利用内积得出最终结果。这会导致特征之间还没有充分交叉就直接输出结果模型会有欠拟合的风险。针对这一弱点NeuralCF对矩阵分解进行了改进它的结构图是图3这样的。
<img src="https://static001.geekbang.org/resource/image/5f/2c/5ff301f11e686eedbacd69dee184312c.jpg" alt="" title="图3 NeuralCF的模型结构图 出自论文Neural Collaborative Filtering">
我想你一定可以一眼看出它们的区别那就是NeuralCF用一个多层的神经网络替代掉了原来简单的点积操作。这样就可以让用户和物品隐向量之间进行充分的交叉提高模型整体的拟合能力。
## NeuralCF模型的扩展双塔模型
有了之前实现矩阵分解和深度学习模型的经验我想你理解起来NeuralCF肯定不会有困难。事实上NeuralCF的模型结构之中蕴含了一个非常有价值的思想就是我们可以把模型分成用户侧模型和物品侧模型两部分然后用互操作层把这两部分联合起来产生最后的预测得分。
这里的用户侧模型结构和物品侧模型结构可以是简单的Embedding层也可以是复杂的神经网络结构最后的互操作层可以是简单的点积操作也可以是比较复杂的MLP结构。但只要是这种物品侧模型+用户侧模型+互操作层的模型结构,我们把它统称为“双塔模型”结构。
图4就是一个典型“双塔模型”的抽象结构。它的名字形象地解释了它的结构组成两侧的模型结构就像两个高塔一样而最上面的互操作层则像两个塔尖搭建起的空中走廊负责两侧信息的沟通。
<img src="https://static001.geekbang.org/resource/image/66/cf/66606828b2c80a5f4ea28d60762e82cf.jpg" alt="" title="图4 双塔模型结构 [br](出自论文 Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations">
对于NerualCF来说它只利用了用户ID作为“用户塔”的输入特征用物品ID作为“物品塔”的输入特征。事实上我们完全可以把其他用户和物品相关的特征也分别放入用户塔和物品塔让模型能够学到的信息更全面。比如说YouTube在构建用于召回层的双塔模型时就分别在用户侧和物品侧输入了多种不同的特征如图5所示。
<img src="https://static001.geekbang.org/resource/image/e2/87/e2603a22ec91a9f00be4b73feyy1f987.jpg" alt="" title="图5 YouTube双塔召回模型的架构 [br](出自论文 Sampling-Bias-Corrected Neural Modeling for Large Corpus Item Recommendations">
我们看到YouTube召回双塔模型的用户侧特征包括了用户正在观看的视频ID、频道ID图中的seed features、该视频的观看数、被喜欢的次数以及用户历史观看过的视频ID等等。物品侧的特征包括了候选视频的ID、频道ID、被观看次数、被喜欢次数等等。在经过了多层ReLU神经网络的学习之后双塔模型最终通过softmax输出层连接两部分输出最终预测分数。
看到这里你可能会有疑问这个双塔模型相比我们之前学过的Embedding MLP和Wide&amp;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&amp;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&amp;Deep和NeuralCF之后相信你可以感觉到实现甚至创造一个深度学习模型并不难基于TensorFlow提供的Keras接口我们可以根据我们的设计思路像搭积木一样实现模型的不同结构以此来验证我们的想法这也正是深度推荐模型的魅力和优势。相信随着课程的进展你不仅对这一点能够有更深刻的感受同时你设计和实现模型的能力也会进一步加强。
## 小结
这节课我们首先学习了经典推荐算法协同过滤的深度学习进化版本NerualCF。相比于矩阵分解算法NeuralCF用一个多层的神经网络替代了矩阵分解算法中简单的点积操作让用户和物品隐向量之间进行充分的交叉。这种通过改进物品隐向量和用户隐向量互操作层的方法大大增加了模型的拟合能力。
利用NerualCF的思想我们进一步学习了双塔模型。它通过丰富物品侧和用户侧的特征让模型能够融入除了用户ID和物品ID外更丰富的信息。除此之外双塔模型最大的优势在于模型服务的便捷性由于最终的互操作层是简单的内积操作或浅层神经网络。因此我们可以把物品塔的输出当作物品Embedding用户塔的输出当作用户Embedding存入特征数据库在线上只要实现简单的互操作过程就可以了。
最后我们继续使用TensorFlow实现了NerualCF和双塔模型相信你能进一步感受到利用TensorFlow构建深度学习模型的便捷性以及它和传统推荐模型相比在模型结构灵活性上的巨大优势。
为了帮助你复习,我把刚才说的这些重点内容总结在了一张图里,你可以看看。
<img src="https://static001.geekbang.org/resource/image/91/5f/9196a80181f41ba4a96bb80e6286c35f.jpeg" alt="">
## 课后思考
对于我们这节课学习的双塔模型来说把物品侧的Embedding和用户侧的Embedding存起来就可以进行线上服务了。但如果我们把一些场景特征比如当前时间、当前地点加到用户侧或者物品侧那我们还能用这种方式进行模型服务吗为什么
欢迎把你的思考和疑惑写在留言区,也欢迎你把这节课转发出去,我们下节课见!

View File

@@ -0,0 +1,144 @@
<audio id="audio" title="20 | DeepFM如何让你的模型更好地处理特征交叉" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/32/8e/3295ff2e57ed7aaa90a885abe533c58e.mp3"></audio>
你好,我是王喆。
前面几节课我们学习了Embedding MLP、Wide&amp;Deep、NerualCF等几种不同的模型结构。你有没有深入思考过这样一个问题这几种模型都是怎么处理特征交叉这个问题的
比如说模型的输入有性别、年龄、电影风格这几个特征在训练样本中我们发现有25岁男生喜欢科幻电影的样本有35岁女生喜欢看恐怖电影的样本那你觉得模型应该怎么推测“25岁”的女生喜欢看的电影风格呢
事实上,这类特征组合和特征交叉问题非常常见,而且在实际应用中,特征的种类还要多得多,特征交叉的复杂程度也要大得多。**解决这类问题的关键,就是模型对于特征组合和特征交叉的学习能力,因为它决定了模型对于未知特征组合样本的预测能力,而这对于复杂的推荐问题来说,是决定其推荐效果的关键点之一。**
但无论是Embedding MLP还是Wide&amp;Deep其实都没有对特征交叉进行特别的处理而是直接把独立的特征扔进神经网络让它们在网络里面进行自由组合就算是NeuralCF也只在最后才把物品侧和用户侧的特征交叉起来。那这样的特征交叉方法是高效的吗深度学习模型有没有更好的处理特征交叉的方法呢
这节课我们就一起来解决这些问题。同时我还会基于特征交叉的思想带你学习和实现一种新的深度学习模型DeepFM。
## 为什么深度学习模型需要加强处理特征交叉的能力?
不过在正式开始今天的课程之前我还想和你再深入聊聊为什么深度学习需要加强处理特征交叉的能力。我们刚才说Embedding MLP和Wide&amp;Deep模型都没有针对性的处理特征交叉问题有的同学可能就会有疑问了我们之前不是一直说多层神经网络有很强的拟合能力能够在网络内部任意地组合特征吗这两个说法是不是矛盾了
在进入正题前我就带你先扫清这个疑问。我们之前一直说MLP有拟合任意函数的能力这没有错但这是建立在MLP有任意多层网络以及任意多个神经元的前提下的。
在训练资源有限调参时间有限的现实情况下MLP对于特征交叉的处理其实还比较低效。因为MLP是通过concatenate层把所有特征连接在一起成为一个特征向量的这里面没有特征交叉两两特征之间没有发生任何关系。
这个时候在我们有先验知识的情况下人为地加入一些负责特征交叉的模型结构其实对提升模型效果会非常有帮助。比如在我们Sparrow RecSys项目的训练样本中其实有两个这样的特征一个是用户喜欢的电影风格一个是电影本身的风格这两个特征明显具有很强的相关性。如果我们能让模型利用起这样的相关性肯定会对最后的推荐效果有正向的影响。
既然这样,那我们不如去设计一些特定的特征交叉结构,来把这些相关性强的特征,交叉组合在一起,这就是深度学习模型要加强特征交叉能力的原因了。
## 善于处理特征交叉的机器学习模型FM
扫清了这个疑问,接下来,我们就要进入具体的深度学习模型的学习了,不过,先别着急,我想先和你聊聊传统的机器学习模型是怎么解决特征交叉问题的,看看深度学习模型能不能从中汲取到“养分”。
说到解决特征交叉问题的传统机器学习模型我们就不得不提一下曾经红极一时的机器学习模型因子分解机模型Factorization Machine我们可以简称它为FM。
<img src="https://static001.geekbang.org/resource/image/6b/a2/6b2868995e486943ea90cfc51c2bc0a2.jpg" alt="" title="图1 FM的神经网络化结构 [br](出自论文 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&amp;Deep结构之后我们一定可以快速给出答案我们当然可以把FM跟其他深度学习模型组合起来生成一个全新的既有强特征组合能力又有强拟合能力的模型。基于这样的思想DeepFM模型就诞生了。
DeepFM是由哈工大和华为公司联合提出的深度学习模型我把它的架构示意图放在了下面。
<img src="https://static001.geekbang.org/resource/image/d0/19/d0df6ed3958byyd9529efceebba64419.png" alt="" title="图2 DeepFM模型架构图 [br](出自论文 DeepFM: A Factorization-Machine based Neural Network for CTR Prediction">
结合模型结构图我们可以看到DeepFM利用了Wide&amp;Deep组合模型的思想用FM替换了Wide&amp;Deep左边的Wide部分加强了浅层网络部分特征组合的能力而右边的部分跟Wide&amp;Deep的Deep部分一样主要利用多层神经网络进行所有特征的深层处理最后的输出层是把FM部分的输出和Deep部分的输出综合起来产生最后的预估结果。这就是DeepFM的结构。
## 特征交叉新方法:元素积操作
接下来我们再思考一个问题FM和DeepFM中进行特征交叉的方式都是进行Embedding向量的点积操作那是不是说特征交叉就只能用点积操作了
答案当然是否定的。事实上还有很多向量间的运算方式可以进行特征的交叉比如模型NFMNeural Factorization Machines神经网络因子分解机它就使用了新的特征交叉方法。下面我们一起来看一下。
图3就是NFM的模型架构图相信已经看了这么多模型架构图的你一眼就能看出它跟其他模型的区别也就是Bi-Interaction Pooling层。那这个夹在Embedding层和MLP之间的层到底做了什么呢
<img src="https://static001.geekbang.org/resource/image/a2/0c/a2c0f6751f64f50e3c628bf86cd9b00c.jpg" alt="" title="图3 NFM的模型架构图 [br]出自论文Neural Factorization Machines for Sparse Predictive Analytics">
Bi-Interaction Pooling Layer翻译成中文就是“两两特征交叉池化层”。假设Vx是所有特征域的Embedding集合那么特征交叉池化层的具体操作如下所示。
$$<br>
f_{\mathrm{PI}}\left(V_{x}\right)=\sum_{i=1}^{n} \sum_{j=i+1}^{n} x_{i} \boldsymbol{v}_{i} \odot \boldsymbol{x}_{j} \boldsymbol{v}_{j}<br>
$$
其中$\odot$运算代表两个向量的元素积Element-wise Product操作即两个长度相同的向量对应维相乘得到元素积向量。其中第k维的操作如下所示。
$$<br>
\left(V_{i} \odot V_{j}\right)_{K}=v_{i k} v_{j k}<br>
$$
在进行两两特征Embedding向量的元素积操作后再求取所有交叉特征向量之和我们就得到了池化层的输出向量。接着我们再把该向量输入上层的多层全连接神经网络就能得出最后的预测得分。
总的来说NFM并没有使用内积操作来进行特征Embedding向量的交叉而是使用元素积的操作。在得到交叉特征向量之后也没有使用concatenate操作把它们连接起来而是采用了求和的池化操作把它们叠加起来。
看到这儿,你肯定又想问,元素积操作和点积操作到底哪个更好呢?还是那句老话,我希望我们能够尽量多地储备深度学习模型的相关知识,先不去关注哪个方法的效果会更好,至于真实的效果怎么样,交给你去在具体的业务场景的实践中验证。
## DeepFM的TensorFlow实战
接下来又到了TensorFlow实践的时间了今天我们将要实现DeepFM模型。有了之前实现Wide&amp;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&amp;Deep模型的Deep部分完全一样。只不过最终我们会使用concatenate层去把FM部分的输出和Deep部分的输出连接起来输入到输出层的sigmoid神经元从而产生最终的预估分数。那关于DeepFM的全部代码你可以参照SparrowRecsys项目中的DeepFM.py文件。
## 小结
DeepFM模型在解决特征交叉问题上非常有优势它会使用一个独特的FM层来专门处理特征之间的交叉问题。具体来说就是使用点积、元素积等操作让不同特征之间进行两两组合再把组合后的结果输入的输出神经元中这会大大加强模型特征组合的能力。因此DeepFM模型相比于Embedding MLP、Wide&amp;Deep等模型往往具有更好的推荐效果。
实现DeepFM模型的过程并不困难我们主要记住三点就可以了
- 它是由FM和Deep两部分组成的
- 在实现FM部分特征交叉层的时候我们使用了多个Dot Product操作单元完成不同特征的两两交叉
- Deep部分则与Wide&amp;Deep模型一样负责所有输入特征的深度拟合提高模型整体的表达能力
刚才说的重点知识,我都整理在了下面的表格中,你可以看一看。
<img src="https://static001.geekbang.org/resource/image/4d/76/4dbb2c9760199311b38b32a15daba176.jpeg" alt="">
好了到今天这节课我们已经在SparrowRecsys中实现了四个深度学习模型相信你对TensorFlow的Keras接口也已经十分熟悉了。我希望你不只满足于读懂、用好SparrowRecsys中实现好的模型而是真的在课后自己多去尝试不同的特征输入不同的模型结构甚至可以按照自己的理解和思考去改进这些模型。
因为深度学习模型结构没有标准答案,我们只有清楚不同模型之间的优缺点,重点汲取它们的设计思想,才能在实际的工作中结合自己遇到的问题,来优化和改造已有的模型。也只有这样,你们才能成为一名能真正解决实际问题的算法工程师。
## 课后思考
你觉得除了点积和元素积这两个操作外还有没有其他的方法能处理两个Embedding向量间的特征交叉
关于深度学习中特征交叉问题的处理方法你是不是学会了欢迎把你的思考和疑问写在留言区如果你的朋友也对DeepFM这个模型感兴趣那不妨也把这节课转发给他我们下节课见

View File

@@ -0,0 +1,117 @@
<audio id="audio" title="21注意力机制、兴趣演化推荐系统如何抓住用户的心" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a1/21/a105760f4e79103861027ce0c8b81421.mp3"></audio>
你好,我是王喆。
近几年来,注意力机制、兴趣演化序列模型和强化学习,都在推荐系统领域得到了广泛的应用。它们是深度学习推荐模型的发展趋势,也是我们必须要储备的前沿知识。
作为一名算法工程师足够的知识储备是非常重要的因为在掌握了当下主流的深度学习模型架构Embedding MLP架构、Wide&amp;Deep和DeepFM等等之后要想再进一步提高推荐系统的效果就需要清楚地知道业界有哪些新的思路可以借鉴学术界有哪些新的思想可以尝试这些都是我们取得持续成功的关键。
所以,我会用两节课的时间,带你一起学习这几种新的模型改进思路。今天我们先重点关注注意力机制和兴趣演化序列模型,下节课我们再学习强化学习。
## 什么是“注意力机制”?
**“注意力机制”来源于人类天生的“选择性注意”的习惯**。最典型的例子是用户在浏览网页时,会有选择性地注意页面的特定区域,而忽视其他区域。
比如图1是Google对大量用户进行眼球追踪实验后得出的页面注意力热度图。我们可以看到用户对页面不同区域的注意力区别非常大他们的大部分注意力就集中在左上角的几条搜索结果上。
那么,“注意力机制”对我们构建推荐模型到底有什么价值呢?
[<img src="https://static001.geekbang.org/resource/image/3a/5f/3a3cb86da13876d679c16f4ec955645f.jpg" alt="" title="图1 Google搜索结果的注意力热度图">](https://www.researchgate.net/figure/Heat-Map-Golden-Triangle-pattern-shown-by-the-heat-map-of-how-users-focus-on-a_fig1_267025472)
价值是非常大的。比如说,我们要做一个新闻推荐的模型,让这个模型根据用户已经看过的新闻做推荐。那我们在分析用户已浏览新闻的时候,是把标题、首段、全文的重要性设置成完全一样比较好,还是应该根据用户的注意力不同给予不同的权重呢?当然,肯定是后者比较合理,因为用户很可能都没有注意到正文最后的几段,如果你分析内容的时候把最后几段跟标题、首段一视同仁,那肯定就把最重要的信息给淹没了。
事实上近年来注意力机制已经成功应用在各种场景下的推荐系统中了。其中最知名的要数阿里巴巴的深度推荐模型DINDeep 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中彩色的部分。
<img src="https://static001.geekbang.org/resource/image/c1/6e/c1dbcc00dd17166ec53ca9e46b04006e.jpg" alt="" title="图2 阿里Base模型的架构图[br] (出自论文 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这个激活单元生成了一个权重这个权重就是用户对这个历史商品的注意力得分权重的大小对应用户注意力的高低。
<img src="https://static001.geekbang.org/resource/image/ed/30/edd028f815ef3577553ffe045f042330.jpg" alt="" title="图3 阿里DIN模型的架构图[br] (出自论文 Deep Interest Network for Click-Through Rate Prediction)">
那现在问题就只剩下一个了这个所谓的激活单元到底是怎么计算出最后的注意力权重的呢为了搞清楚这个问题我们需要深入到激活单元的内部结构里面去一起来看看图3右上角激活单元的详细结构。
它的输入是当前这个历史行为商品的Embedding以及候选广告商品的Embedding。我们把这两个输入Embedding与它们的外积结果连接起来形成一个向量再输入给激活单元的MLP层最终会生成一个注意力权重这就是激活单元的结构。简单来说**激活单元就相当于一个小的深度学习模型它利用两个商品的Embedding生成了代表它们关联程度的注意力权重**。
到这里我们终于抽丝剥茧地讲完了整个DIN模型的结构细节。如果你第一遍没理解清楚没关系对照着DIN模型的结构图反复再看几遍我刚才讲的细节相信你就能彻底消化吸收它。
### 注意力机制对推荐系统的启发
注意力机制的引入对于推荐系统的意义是非常重大的,它模拟了人类最自然,最发自内心的注意力行为特点,使得推荐系统更加接近用户真实的思考过程,从而达到提升推荐效果的目的。
从“注意力机制”开始,越来越多对深度学习模型结构的改进是基于对用户行为的深刻观察而得出的。由此,我也想再次强调一下,**一名优秀的算法工程师应该具备的能力,就是基于对业务的精确理解,对用户行为的深刻观察,得出改进模型的动机,进而设计出最合适你的场景和用户的推荐模型。**
沿着这条思路阿里的同学们在提出DIN模型之后并没有停止推荐模型演化的进程他们又在2019年提出了DIN模型的演化版本也就是深度兴趣进化网络DIENDeep Interest Evolution Network。这个DIEN到底在DIN的基础上做了哪些改进呢
## 兴趣进化序列模型
无论是电商购买行为,还是视频网站的观看行为,或是新闻应用的阅读行为,特定用户的历史行为都是一个随时间排序的序列。既然是和时间相关的序列,就一定存在前后行为的依赖关系,这样的序列信息对于推荐过程是非常有价值的。为什么这么说呢?
我们还拿阿里的电商场景举个例子。对于一个综合电商来说,用户兴趣的迁移其实是非常快的。比如,上一周一位用户在挑选一双篮球鞋,这位用户上周的行为序列都会集中在篮球鞋这个品类的各个商品上,但在他完成这一购物目标后,这一周他的购物兴趣就可能变成买一个机械键盘,那这周他所有的购买行为都会围绕机械键盘这个品类展开。
因此如果我们能让模型预测出用户购买商品的趋势肯定会对提升推荐效果有益。而DIEN模型正好弥补了DIN模型没有对行为序列进行建模的缺陷它围绕兴趣进化这个点进一步对DIN模型做了改进。
图4就是DIEN模型的架构图这个模型整体上仍然是一个Embedding MLP的模型结构。与DIN不同的是DIEN用“兴趣进化网络”也就是图中的彩色部分替换掉了原来带有激活单元的用户历史行为部分。这部分虽然复杂但它的输出只是一个h'(T)的Embedding向量它代表了用户当前的兴趣向量。有了这个兴趣向量之后再把它与其他特征连接在一起DIEN就能通过MLP作出最后的预测了。
<img src="https://static001.geekbang.org/resource/image/56/a0/56c93e11aa3503852c37cd18db740da0.jpg" alt="" title="图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里就是为了预测用户的下一个兴趣。这就是序列模型的结构和作用。
[<img src="https://static001.geekbang.org/resource/image/f6/37/f69c0f565b5fa31023b727192d3b1d37.jpg" alt="" title="图5 RNN模型的经典结构 ">](https://towardsdatascience.com/introduction-to-recurrent-neural-networks-rnn-with-dinosaurs-790e74e3e6f6)
至于上面提到过的GRU序列模型它其实是序列模型的一种根据序列模型神经元结构的不同最经典的有[RNN](https://arxiv.org/pdf/1406.1078.pdf)、[LSTM](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.676.4320&amp;rep=rep1&amp;type=pdf)、[GRU](https://arxiv.org/pdf/1412.3555.pdf)这3种。这里我们就不展开讲了对理论感兴趣的同学可以点击我给出的超链接参考这几篇论文做更深入的研究。
[<img src="https://static001.geekbang.org/resource/image/be/02/bed695e253a49e6f08a6627782ec9202.jpg" alt="" title="图6 序列模型中的不同单元结构">](http://dprogrammer.org/rnn-lstm-gru)
事实上序列模型已经不仅在电商场景下成功应用于推测用户的下次购买行为在YouTube、Netflix等视频流媒体公司的视频推荐模型中序列模型也用来推测用户的下次观看行为Next Watch。除此之外音乐类应用也非常适合使用序列模型来预测用户的音乐兴趣变化。所以掌握DIEN模型的架构对于拓宽我们的技术视野非常有帮助。
## 小结
注意力机制和兴趣演化序列模型的加入,让推荐系统能够更好地抓住用户的心。
对于注意力机制来说它主要模拟了人类注意力的天性。具体到阿里的DIN模型上它利用激活单元计算出用户对于不同历史商品的注意力权重针对当前广告商品作出更有针对性的预测。
而序列模型更注重对序列类行为的模拟和预测。典型的例子是DIEN模型对用户购买兴趣进化过程的模拟。DIEN模型可以应用的场景非常广泛包括电商平台的下次购买视频网站的下次观看音乐App的下一首歌曲等等。
总的来说,注意力机制的引入是对经典深度学习模型的一次大的改进,因为它改变了深度学习模型对待用户历史行为“一视同仁”的弊端。而序列模型则把用户行为串联起来,让用户的兴趣随时间进行演化,这也是之前的深度学习模型完全没有考虑到的结构。
最后,我把今天的重要概念总结在了表格中,方便你及时查看和复习。
<img src="https://static001.geekbang.org/resource/image/c3/e0/c38e2d57f570c0e3ea5c0d616397b2e0.jpeg" alt="">
## 课后思考
DIN使用了一个结构比较复杂的激活单元来计算注意力权重你觉得有没有更简单、更实用的方式来生成注意力权重呢其实计算注意力权重就是为了计算历史行为物品和广告物品的相关性在这个过程中你觉得能不能利用到特征交叉的知识呢为什么
欢迎把你的思考和疑问写在留言区,如果你的朋友们也在关注注意力机制和兴趣演化序列模型的发展,那不妨也把这节课转发给他们,我们下节课见!

View File

@@ -0,0 +1,132 @@
<audio id="audio" title="22强化学习让推荐系统像智能机器人一样自主学习" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9a/63/9aac277df3d25b66870664f5df053063.mp3"></audio>
你好我是王喆。这节课我们继续来讲深度推荐模型发展的前沿趋势来学习强化学习Reinforcement Learning与深度推荐模型的结合。
强化学习也被称为增强学习它在模型实时更新、用户行为快速反馈等方向上拥有巨大的优势。自从2018年开始它就被大量应用在了推荐系统中短短几年时间内[微软](http://www.personal.psu.edu/~gjz5038/paper/www2018_reinforceRec/www2018_reinforceRec.pdf)、[美团](https://tech.meituan.com/2018/11/15/reinforcement-learning-in-mt-recommend-system.html)、[阿里](https://arxiv.org/abs/1803.00710)等多家一线公司都已经有了强化学习的成功应用案例。
虽然,强化学习在推荐系统中的应用是一个很复杂的工程问题,我们自己很难在单机环境下模拟,但理解它在推荐系统中的应用方法,是我们进一步改进推荐系统的关键点之一,也是推荐系统发展的趋势之一。
所以这节课我会带你重点学习这三点内容一是强化学习的基本概念二是我会以微软的DRN模型为例帮你厘清强化学习在推荐系统的应用细节三是帮助你搞清楚深度学习和强化学习的结合点究竟在哪。
## 强化学习的基本概念
强化学习的基本原理,简单来说,就是**一个智能体通过与环境进行交互,不断学习强化自己的智力,来指导自己的下一步行动,以取得最大化的预期利益**。
事实上,任何一个有智力的个体,它的学习过程都遵循强化学习所描述的原理。比如说,婴儿学走路就是通过与环境交互,不断从失败中学习,来改进自己的下一步的动作才最终成功的。再比如说,在机器人领域,一个智能机器人控制机械臂来完成一个指定的任务,或者协调全身的动作来学习跑步,本质上都符合强化学习的过程。
为了把强化学习技术落地,只清楚它的基本原理显然是不够的,我们需要清晰地定义出强化学习中的每个关键变量,形成一套通用的技术框架。对于一个通用的强化学习框架来说,有这么六个元素是必须要有的:
- **智能体Agent**:强化学习的主体也就是作出决定的“大脑”;
- **环境Environment**:智能体所在的环境,智能体交互的对象;
- **行动Action**:由智能体做出的行动;
- **奖励Reward**:智能体作出行动后,该行动带来的奖励;
- **状态State**:智能体自身当前所处的状态;
- **目标Objective**:指智能体希望达成的目标。
为了方便记忆,我们可以用一段话把强化学习的六大要素串起来:一个**智能体**身处在不断变化的**环境**之中,为了达成某个**目标**,它需要不断作出**行动**,行动会带来好或者不好的**奖励**,智能体收集起这些奖励反馈进行自我学习,改变自己所处的**状态**,再进行下一步的行动,然后智能体会持续这个“**行动-奖励-更新状态**”的循环,不断优化自身,直到达成设定的目标。
这就是强化学习通用过程的描述那么对于推荐系统而言我们能不能创造这样一个会自我学习、自我调整的智能体为用户进行推荐呢事实上微软的DRN模型已经实现这个想法了。下面我就以DRN模型为例来给你讲一讲在推荐系统中强化学习的六大要素都是什么强化学习具体又是怎样应用在推荐系统中的。
## 强化学习推荐系统框架
强化学习推荐模型DRNDeep Reinforcement Learning Network深度强化学习网络是微软在2018年提出的它被应用在了新闻推荐的场景上下图1是DRN的框架图。事实上它不仅是微软DRN的框架图也是一个经典的强化学习推荐系统技术框图。
<img src="https://static001.geekbang.org/resource/image/2f/27/2ff55be8dea34e992bcb09e1f3c39a27.jpg" alt="" title="图1 深度强化学习推荐系统框架">
从这个技术框图中我们可以清楚地看到强化学习的六大要素。接下来我就以DRN模型的学习过程串联起所有要素来和你详细说说这六大要素在推荐系统场景下分别指的是什么以及每个要素的位置和作用。
在新闻的推荐系统场景下DRN模型的第一步是初始化推荐系统主要初始化的是推荐模型我们可以利用离线训练好的模型作为初始化模型其他的还包括我们之前讲过的特征存储、推荐服务器等等。
接下来推荐系统作为智能体会根据当前已收集的用户行为数据也就是当前的状态对新闻进行排序这样的行动并在新闻网站或者App这些环境中推送给用户。
用户收到新闻推荐列表之后,可能会产生点击或者忽略推荐结果的反馈。这些反馈都会作为正向或者负向奖励再反馈给推荐系统。
推荐系统收到奖励之后,会根据它改变、更新当前的状态,并进行模型训练来更新模型。接着,就是推荐系统不断重复“排序-推送-反馈”的步骤,直到达成提高新闻的整体点击率或者用户留存等目的为止。
为了方便你进行对比,我也把这六大要素在推荐系统场景下的定义整理在了下面,你可以看一看。
<img src="https://static001.geekbang.org/resource/image/ea/2b/eac0fea51d5033a35332cd81f653202b.jpeg" alt="">
到这里,你有没有发现强化学习推荐系统跟传统推荐系统相比,它的主要特点是什么?其实,就在于强化学习推荐系统始终在强调“持续学习”和“实时训练”。它不断利用新学到的知识更新自己,做出最及时的调整,这也正是将强化学习应用于推荐系统的收益所在。
我们现在已经熟悉了强化学习推荐系统的框架但其中最关键的部分“智能体”到底长什么样呢微软又是怎么实现“实时训练”的呢接下来就让我们深入DRN的细节中去看一看。
## 深度强化学习推荐模型DRN
智能体是强化学习框架的核心作为推荐系统这一智能体来说推荐模型就是推荐系统的“大脑”。在DRN框架中扮演“大脑”角色的是Deep Q-Network (深度Q网络DQN)。其中Q是Quality的简称指通过对行动进行质量评估得到行动的效用得分来进行行动决策。
<img src="https://static001.geekbang.org/resource/image/75/8a/75d38d425b1a72350ce85e8b676d928a.jpg" alt="" title="图2 DQN的模型架构图">
DQN的网络结构如图2所示它就是一个典型的双塔结构。其中用户塔的输入特征是用户特征和场景特征物品塔的输入向量是所有的用户、环境、用户-新闻交叉特征和新闻特征。
在强化学习的框架下,用户塔特征向量因为代表了用户当前所处的状态,所以也可被视为**状态向量**。物品塔特征向量则代表了系统下一步要选择的新闻,我们刚才说了,这个选择新闻的过程就是智能体的“行动”,所以物品塔特征向量也被称为**行动向量**。
双塔模型通过对状态向量和行动向量分别进行MLP处理再用互操作层生成了最终的行动质量得分Q(s,a),智能体正是通过这一得分的高低,来选择到底做出哪些行动,也就是推荐哪些新闻给用户的。
其实到这里为止我们并没有看到强化学习的优势貌似就是套用了强化学习的概念把深度推荐模型又解释了一遍。别着急下面我要讲的DRN学习过程才是强化学习的精髓。
### DRN的学习过程
DRN的学习过程是整个强化学习推荐系统框架的重点正是因为可以在线更新才使得强化学习模型相比其他“静态”深度学习模型有了更多实时性上的优势。下面我们就按照下图中从左至右的时间轴来描绘一下DRN学习过程中的重要步骤。
<img src="https://static001.geekbang.org/resource/image/96/2a/96509410e50e019e5077f6a2a909292a.jpg" alt="" title="图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的在线学习方法竞争梯度下降算法
我先把竞争梯度下降算法的流程图放在了下面。接下来,我就结合这个流程图,来给你详细讲讲它的过程和它会涉及的模型微更新操作。
<img src="https://static001.geekbang.org/resource/image/c9/14/c94b7122f71d8b7845ffca3fd239e314.jpg" alt="" title="图4 DRN的在线学习过程">
DRN的在线学习过程主要包括三步我带你一起来看一下。
第一步对于已经训练好的当前网络Q对其模型参数**W**添加一个较小的随机扰动得到一个新的模型参数这里我们称对应的网络为探索网络Q~。
在这一步中由当前网络Q生成探索网络 产生随机扰动的公式1如下
$$<br>
\Delta \mathrm{W}=\alpha \cdot \operatorname{rand}(-1,1) \cdot W<br>
$$
其中,$\alpha$是一个探索因子决定探索力度的大小。rand(-1,1)产生的是一个[-1,1]之间的随机数。
第二步对于当前网络Q和探索网络 Q~分别生成推荐列表L和 L~再将两个推荐列表用间隔穿插Interleaving的方式融合组合成一个推荐列表后推送给用户。
最后一步是实时收集用户反馈。如果探索网络Q生成内容的效果好于当前网络Q我们就用探索网络代替当前网络进入下一轮迭代。反之我们就保留当前网络。
总的来说DRN的在线学习过程利用了“探索”的思想其调整模型的粒度可以精细到每次获得反馈之后这一点很像随机梯度下降的思路虽然一次样本的结果可能产生随机扰动但只要总的下降趋势是正确的我们就能够通过海量的尝试最终达到最优点。DRN正是通过这种方式让模型时刻与最“新鲜”的数据保持同步实时地把最新的奖励信息融合进模型中。模型的每次“探索”和更新也就是我们之前提到的模型“微更新”。
到这里我们就讲完了微软的深度强化学习模型DRN。我们可以想这样一个问题这个模型本质上到底改进了什么从我的角度来说它最大的改进就是把模型推断、模型更新、推荐系统工程整个一体化了让整个模型学习的过程变得更高效能根据用户的实时奖励学到新知识做出最实时的反馈。但同时也正是因为工程和模型紧紧地耦合在一起让强化学习在推荐系统中的落地并不容易。
既然,说到了强化学习的落地,这里我还想再多说几句。因为涉及到了模型训练、线上服务、数据收集、实时模型更新等几乎推荐系统的所有工程环节,所以强化学习整个落地过程的工程量非常大。这不像我们之前学过的深度学习模型,只要重新训练一下它,我们就可以改进一个模型结构,强化学习模型需要工程和研究部门通力合作才能实现。
在这个过程中,能不能有一个架构师一样的角色来通盘协调,就成为了整个落地过程的关键点。有一个环节出错,比如说模型在做完实时训练后,模型参数更新得不及时,那整个强化学习的流程就被打乱了,整体的效果就会受到影响。
所以对我们个人来说,掌握强化学习模型的框架,也就多了一个发展的方向。那对于团队来说,如果强化学习能够成功落地,也一定证明了这个团队有着极强的合作能力,在工程和研究方向上都有着过硬的实践能力。
## 小结
强化学习是近来在学术界和业界都很火的话题,它起源于机器人领域。这节课,我们要重点掌握强化学习的通用过程,以及它在深度学习中的应用细节。
简单来说,强化学习的通用过程就是训练一个智能体,让它通过与环境进行交互,不断学习强化自己的智力,并指导自己的下一步行动,以取得最大化的预期利益。这也让强化学习在模型实时更新,用户行为快速反馈等方向上拥有巨大的优势。
但强化学习的落地并不容易整个落地过程的工程量非常大。现阶段我们只需要以微软的DRN模型作为参考重点掌握强化学习在推荐系统领域的应用细节就可以了。
一个是DRN构建了双塔模型作为深度推荐模型来得出“行动得分”。第二个是DRN的更新方式它利用“微更新”实时地学习用户的奖励反馈更新推荐模型再利用阶段性的“主更新”学习全量样本更新模型。第三个是微更新时的方法竞争梯度下降算法它通过比较原网络和探索网络的实时效果来更新模型的参数。
为了方便你复习,我们把这节课的重点知识总结在了下面的表格中,你可以看看。
<img src="https://static001.geekbang.org/resource/image/a6/15/a62dc5c0837f25bb8dd9bc2aa46e5c15.jpeg" alt="">
## 课后思考
DRN的微更新用到了竞争梯度下降算法你觉得这个算法有没有弊端你还知道哪些可以进行模型增量更新或者实时更新的方法吗
欢迎把你的思考和疑问写在留言区,如果你的朋友们也在关注强化学习在推荐系统上的发展,那不妨也把这节课转发给他们,我们下节课见!

View File

@@ -0,0 +1,178 @@
<audio id="audio" title="23 实战如何用深度学习模型实现Sparrow RecSys的个性化推荐功能" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d9/7d/d936d8cfa7e89ee9cf3c719592defe7d.mp3"></audio>
你好,我是王喆。
今天又是一堂实战课。在这节课里我会带你利用我们现阶段掌握的所有知识来实现SparrowRecSys中“猜你喜欢”的功能。具体来说我们会根据一位用户的历史行为为TA推荐可能喜欢的电影。这个功能几乎会用到所有的推荐系统模块包括离线的特征工程、模型训练以及线上的模型服务和推荐逻辑的实现。
如果说完成了[第14讲](https://time.geekbang.org/column/article/303641)的“相似电影”功能,还只是你“武功小成”的标志,那啃透了这节课的实践,就代表你掌握了推荐系统技术框架中的大部分内容,你就能在推荐系统的实际工作中做到“驾轻就熟”啦。
## “清点技能库”,看看我们已有的知识储备有哪些
正式开始实践之前,我们还是先来清点一次自己的技能库。看看经过推荐模型篇的学习,我们技能库中的“兵器”又增加了多少,哪些可以用来实现“猜你喜欢”这个功能。下面,我就按照从离线到线上,由数据到模型的顺序,为你依次梳理一下特征工程、模型离线训练、模型服务、推荐服务器逻辑这四大部分的技能点。
### 1. 模型特征工程
特征工程是所有机器学习项目的起点,咱们的推荐模型也不例外。为了训练推荐模型,我们需要准备好模型所需的样本和特征。此外,在进行模型线上推断的时候,推荐服务器也需要线上实时拼装好包含了用户特征、物品特征、场景特征的特征向量,发送给推荐模型进行实时推断。
在“[模型实战准备二](https://time.geekbang.org/column/article/308812)”这一讲我们就通过Spark处理好了TensorFlow训练所需的训练样本并把Spark处理好的特征插入了Redis特征数据库供线上推断使用。不熟悉这部分内容的同学最好再复习一下相关内容把这把武器装进自己的技能库。
### 2. 模型离线训练
为了在线上做出尽量准确或者说推荐效果尽量好的排序,我们需要在离线训练好排序所用的推荐模型。
我们在这一篇中学习和实践的所有深度推荐模型,都是围绕着这个目的展开的。虽然这些深度推荐模型的结构各不相同,但它们的输入、输出都是一致的,输入是由不同特征组成的特征向量,输出是一个分数,这个分数的高低代表了这组特征对应的用户对物品的喜好程度。
具体实践的时候我们在TensorFlow平台上实现了Embedding MLP、Wide&amp;Deep、NeuralCF、双塔模型、DeepFM等几种不同的深度推荐模型它们中的任何一个都可以支持“猜你喜欢”的排序功能。
在实际的工业级系统中我们会通过离线、在线等不同的评估手段来挑出最好的那个模型去支持真实的应用场景。在SparrowRecsys中我们以NeuralCF模型为例实现“猜你喜欢”功能。其他模型的上线方法与NeuralCF几乎一致唯一的区别是对于不同的模型来说它们在模型服务的部分需要载入不同的模型文件并且在线上预估的部分也要传入模型相应的输入特征。
### 3. 模型服务
模型服务是推荐系统中连接线上环境和线下环境的纽带之一(另一个关键的纽带是特征数据库)。
在离线训练好模型之后,为了让模型在线上发挥作用,做出实时的推荐排序,我们需要通过模型服务的模块把推荐模型部署上线。我们曾经在[第13讲](https://time.geekbang.org/column/article/303430)中详细介绍过主流的模型服务方法它们是“预存推荐结果”“预训练Embedding+轻量级线上模型”“利用PMML转换和部署模型”以及“TensorFlow Serving”。因为我们这一篇的深度学习模型都是基于TensorFlow训练的所以这节课我们也会采用TensorFlow Serving作为模型服务的方式。
### 4. 推荐服务器内部逻辑实现
模型服务虽然可以做到“猜你喜欢”中电影的排序,但要进行排序,仍然需要做大量的准备工作,比如候选集的获取,召回层的构建,特征的获取和拼装等等。这些推荐逻辑都是在推荐服务器内部实现的。推荐服务器就像推荐系统的线上的心脏,是所有线上模块的核心。
我们曾经在“相似电影”功能中实现过整套的推荐逻辑今天我们重点关注其中不同的部分就是特征的拼装以及从推荐服务器内部请求模型服务API的方法。
至此,我们准备好了自己的技能库。接下来,就让我们使出十八般武艺,来打造“猜你喜欢”这个推荐功能吧。
## “猜你喜欢”推荐功能的技术架构
与“相似电影”功能一样“猜你喜欢”相关的技术架构同样是由数据模型部分、线上部分和前端部分组成的。我们先来看看整个功能的技术架构图再来说说每部分的具体实现细节。下图1 就是“猜你喜欢”功能的技术架构图,接下来,你就跟着我,按照从左上到右下的顺序,一起随着数据流的走向过一遍这个架构图吧。
<img src="https://static001.geekbang.org/resource/image/64/ee/642ca5a4260959fcce69b97000c3c4ee.jpg" alt="" title="图1 “猜你喜欢”功能的技术架构图">
首先我们来看数据和模型部分。左上角是我们使用的数据集MovieLens它经过Spark的处理之后会生成两部分数据分别从两个出口出去特征部分会存入Redis供线上推断时推荐服务器使用样本部分则提供给TensorFlow训练模型。
TensorFlow完成模型训练之后会导出模型文件然后模型文件会载入到TensorFlow Serving中接着TensorFlow Serving会对外开放模型服务API供推荐服务器调用。
接下来我们再看推荐服务器部分。在这部分里基于MovieLens数据集生成的候选电影集合会依次经过候选物品获取、召回层、排序层这三步最终生成“猜你喜欢”的电影推荐列表然后返回给前端前端利用HTML和JavaScript把它们展示给用户。
整个过程中除了排序层和TensorFlow Serving的实现其他部分我们都已经在之前的实战中一一实现过。所以今天我们会重点讲解推荐服务器排序层和TensorFlow Serving的实现。
## 排序层+TensorFlow Serving的实现
在推荐服务器内部,经过召回层之后,我们会得到几百量级的候选物品集。最后我们到底从这几百部电影中推荐哪些给用户,这个工作就交由排序层来处理。因为排序的工作是整个推荐系统提高效果的重中之重,在业界的实际应用中,往往交由评估效果最好的深度推荐模型来处理。整个的排序过程可以分为三个部分:
<li>
准备线上推断所需的特征拼接成JSON格式的特征样本
</li>
<li>
把所有候选物品的特征样本批量发送给TensorFlow Serving API
</li>
<li>
根据TensorFlow Serving API返回的推断得分进行排序生成推荐列表。
</li>
接下来,我们就详细来讲讲这三步中的实现重点。
**首先,第一步的实现重点在于特征样本的拼接**。因为实践例子里我们选用了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&lt;Movie&gt; candidates, HashMap&lt;Movie, Double&gt; 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(&quot;userId&quot;, user.getUserId());
instance.put(&quot;movieId&quot;, m.getMovieId());
instances.put(instance);
}
JSONObject instancesRoot = new JSONObject();
instancesRoot.put(&quot;instances&quot;, instances);
//请求TensorFlow Serving API
String predictionScores = asyncSinglePostRequest(&quot;http://localhost:8501/v1/models/recmodel:predict&quot;, instancesRoot.toString());
//获取返回预估值
JSONObject predictionsObject = new JSONObject(predictionScores);
JSONArray scores = predictionsObject.getJSONArray(&quot;predictions&quot;);
//将预估值加入返回的map
for (int i = 0 ; i &lt; 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,
&quot;file:///Users/zhewang/Workspace/SparrowRecSys/src/main/resources/webroot/modeldata/neuralcf/002&quot;,
overwrite=True,
include_optimizer=True,
save_format=None,
signatures=None,
options=None
)
```
其次是模型的导入导入命令非常简单就是TensorFlow Serving API的启动命令我们直接看下面命令中的参数。
```
docker run -t --rm -p 8501:8501 -v &quot;/Users/zhewang/Workspace/SparrowRecSys/src/main/resources/webroot/modeldata/neuralcf:/models/recmodel&quot; -e MODEL_NAME=recmodel tensorflow/serving &amp;
```
这里面最重要的参数就是指定载入模型的路径和预估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中全部排序层的代码。
```
{
&quot;predictions&quot;: [[0.824034274], [0.86393261], [0.921346784], [0.957705915], [0.875154734], [0.905113697], [0.831545711], [0.926080644], [0.898158073]...
]
}
```
如果你已经正确建立起了Redis和TensorFlow Serving API服务并且已经分别导入了特征数据和模型文件我们就可以启动Sparrow Recsys Server查看“猜你喜欢”的结果了。图2是用户ID为6的用户在NerualCF模型下的[推荐结果](http://localhost:6010/user.html?id=6&amp;model=nerualcf)注意通过在连接中设置model变量为nerualcf来决定产生结果的模型。
通过用户的评分历史User Watched Movies我们可以看到该用户偏向于观看动作类的电影同时夹杂着一些爱情片和动画片而在下方的“猜你喜欢”Recommended For You的结果中我们也可以看到Sparrow Recsys为他推荐的电影也包含了这三类电影。有兴趣的话你可以多在Sparrow Recsys里面逛一逛看看推荐的结果在你眼中是不是合理。
<img src="https://static001.geekbang.org/resource/image/7f/7b/7f0d71486bf8ae09b18abf8a54db777b.png" alt="" title="图2 猜你喜欢功能的推荐结果">
## 小结
今天我们通过实现“猜你喜欢”功能串联起了我们之前所有学过的知识。希望在你看到推荐结果的时候,有种“武功大成,驾轻就熟”的感觉。要知道,这里面所有的代码都是你曾经学习过的,这里面每个结果都是你通过自己的所学生成的。希望你能在这里为自己鼓掌,这是一个不小的里程碑。
下面我们再重点总结一下今天实践用到的技术。首先我们利用Spark对MovieLens原始数据进行了处理生成了训练样本和特征样本供TensorFlow进行模型训练特征存入Redis供线上推断使用。
在TensorFlow平台上我们以NeuralCF模型为例训练并导出了NeuralCF的模型文件。然后使用TensorFlow Serving载入模型文件建立线上模型服务API。推荐服务器的排序层从Redis中取出用户特征和物品特征组装好JSON格式的特征数据发送给TensorFlow Serving API再根据返回的预估分数进行排序最终生成“猜你喜欢”的推荐列表。
虽然我们实现了猜你喜欢的功能,但是课程进行到这里你一定会有一个疑问:我们的推荐结果到底是好还是坏呢?我们总不能总是人肉去查看结果好坏吧,这样效率又低,又不准确。没错,推荐系统的效果评估是有一套非常完整的评估体系的,别着急,从下一篇的模型评估篇开始,我们就会系统性地讲解推荐系统的评估方法,期待继续与你同行。
<img src="https://static001.geekbang.org/resource/image/93/3c/9364b714305ba0b26791db2805d5983c.jpg" alt="">
## 课后思考
推荐系统的特征预处理是一项很重要的工作比如一些连续特征的归一化分桶等等。那么这些预处理的过程我们应该放在线上部分的哪里完成呢是在Tensorflow Serving的部分还是在推荐服务器内部还是在离线部分完成你有什么好的想法吗
期待在留言区看到你的想法和思考,我们下一节课见!

View File

@@ -0,0 +1,179 @@
<audio id="audio" title="模型实战准备(一) | TensorFlow入门和环境配置" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/be/59/bee51c9da7622cf5e0cf66a03b9d3a59.mp3"></audio>
你好,我是王喆。
我在留言区看到很多同学对TensorFlow的用法还不太熟悉甚至可能还没接触过它。但我们必须要熟练掌握TensorFlow因为接下来它会成为我们课程主要使用的工具和平台。为此我特意准备了两堂模型实战准备课来帮助你掌握TensorFlow的基础知识以及为构建深度学习模型做好准备。
这节课我们先来学习TensorFlow的环境配置讲讲什么是TensorFlow怎么安装TensorFlow以及怎么在TensorFlow上构建你的第一个深度学习模型。下节课我们再来学习模型特征和训练样本的处理方法。
## 什么是TensorFlow
TensorFlow 是由 Google Brain团队开发的深度学习平台于2015年11月首次发布目前最新版本是2.3。因为TensorFlow自从2.0版本之后就发生了较大的变化所以咱们这门课程会使用TensorFlow2.3作为实践版本。
TensorFlow这个名字还是很有意思的翻译过来是“张量流动”这非常准确地表达了它的基本原理就是根据深度学习模型架构构建一个有向图让数据以[张量](https://www.youtube.com/watch?v=f5liqUk0ZTw)的形式在其中流动起来。
这里的张量Tensor其实是指向量的高维扩展比如向量可以看作是张量的一维形式矩阵可以看作张量在二维空间上的特例。在深度学习模型中大部分数据是以矩阵甚至更高维的张量表达的为了让这些张量数据流动起来每一个深度学习模型需要根据模型结构建立一个由点和边组成的有向图图里面的点代表着某种操作比如某个激活函数、某种矩阵运算等等而边就定义了张量流动的方向。
这么说还是太抽象我们来看一个简单TensorFlow任务的有向图。从图中我们可以看出向量b、矩阵W和向量x是模型的输入紫色的节点MatMul、Add和ReLU是操作节点分别代表了矩阵乘法、向量加法、ReLU激活函数这样的操作。模型的输入张量W、b、x经过操作节点的变形处理之后在点之间传递这就是TensorFlow名字里所谓的张量流动。
<img src="https://static001.geekbang.org/resource/image/b8/f7/b80df3e43650d81ebca2608de27fdff7.jpeg" alt="" title="图1 一个简单的TensorFlow有向图">
事实上,任何复杂模型都可以抽象为操作有向图的形式。这样做不仅有利于操作的模块化,还可以厘清各操作间的依赖关系,有利于我们判定哪些操作可以并行执行,哪些操作只能串行执行,为并行平台能够最大程度地提升训练速度打下基础。
说起TensorFlow的并行训练就不得不提GPU因为GPU拥有大量的计算核心所以特别适合做矩阵运算这类易并行的计算操作。业界主流的TensorFlow平台都是建立在CPU+GPU的计算环境之上的。但咱们这门课程因为考虑到大多数同学都是使用个人电脑进行学习的所以所有的项目实践都建立在TensorFlow CPU版本上我在这里先给你说明一下。
## TensorFlow的运行环境如何安装
知道了TensorFlow的基本概念之后我们就可以开始着手配置它的运行环境了。我先把这门课使用的环境版本告诉你首先[TensorFlow我推荐2.3版本](https://www.tensorflow.org/)也就是目前的最新版本 [Python我推荐3.7版本](https://www.python.org/)其实3.5-3.8都可以,我建议你在学习过程中不要随意更换版本,尽量保持一致。
虽然[Python环境](https://www.tensorflow.org/install/pip)和[TensorFlow环境](https://www.tensorflow.org/install)有自己的官方安装指南但是安装起来也并不容易所以接下来我会带你梳理一遍安装中的重点步骤。一般来说安装TensorFlow有两种方法一种是采用Docker+Jupyter的方式另一种是在本地环境安装TensorFlow所需的python环境和所需依赖库。其中Docker+Jupyter的方式比较简单我们先来看看它。
### 最简单的方法Docker+Jupyter
经过[第13讲](https://time.geekbang.org/column/article/303430)模型服务的实践我们已经把Docker安装到了自己的电脑上。这里它就可以再次派上用场了。因为TensorFlow官方已经为我们准备好了它专用的Docker镜像你只要运行下面两行代码就可以拉取并运行最新的TensorFlow版本还能在[http://localhost:8888/](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 -&gt; Preferences -&gt; Plugins -&gt; 输入Python -&gt; 选择插件 Python Community Edition进行安装。
<img src="https://static001.geekbang.org/resource/image/e3/bb/e394886105fyyc1d5da5f4a497d002bb.jpg" alt="" title="图2 为IDEA安装Python插件">
**接着是安装本地Python环境。**
使用过Python的同学可能知道由于Python的流行版本比较多不同软件对于Python环境的要求也不同所以我们直接更改本地的Python环境比较危险因此我推荐你使用[Anaconda](https://www.anaconda.com/)来创建不同Python的虚拟环境这样就可以为咱们的SparrowRecsys项目专门创建一个使用Python3.7和支持TensorFlow2.3的虚拟Python环境了。
具体的步骤我推荐你参考Anaconda的[官方TensorFlow环境安装指导](https://docs.anaconda.com/anaconda/user-guide/tasks/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-&gt;Project Structure -&gt; SDKs -&gt;点击+号-&gt;Add Python SDK 这个路径在操作界面的显示如图3。
<img src="https://static001.geekbang.org/resource/image/99/bd/996c5740f59752918154c825ab0b01bd.jpg" alt="" title="图3 添加Python SDK">
添加完Python SDK之后我们配置Conda Environment为项目的Python SDK。IDEA会自动检测到系统的Conda环境相关路径你选择按照自动填充的路径就好具体的操作你可以看下图4。
<img src="https://static001.geekbang.org/resource/image/62/cf/6220bfb6ce685d4bb2d5688e358a75cf.jpg" alt="" title="图4 选择Conda Enviroment为IDEA Python环境">
最后我们为TFRecModel模块配置Python环境。我们选择Project Structure Modules部分的TFRecModel模块在其上点击右键来Add Python。
<img src="https://static001.geekbang.org/resource/image/16/1a/164165823e10fede6114e4ac5244e71a.jpg" alt="" title="图5 为TFRecModel模块配置Python环境">
设置好的TFRecModel模块的Python环境应该如图6所示。
<img src="https://static001.geekbang.org/resource/image/47/c2/47105cd66e322260ae455a68cc9fc8c2.jpg" alt="" title="图6 配置好的Python环境">
如果你按照上面的步骤那Python和TensorFlow的环境应该已经配置好了但我们到底怎么验证一下所有的配置是否成功呢下面我们就来写一个测试模型来验证一下。
## 如何在TensorFlow上构建你的第一个深度学习模型
这里我们选择了TensorFlow官方教程上的例子作为我们深度学习模型的“初体验”你可以参考[https://tensorflow.google.cn/tutorials/quickstart/beginner?hl=zh_cn](https://tensorflow.google.cn/tutorials/quickstart/beginner?hl=zh_cn)来构建这个模型。这里,我再对其中的关键内容做一个补充讲解。
我先把测试模型的代码放到了下面你可以边看代码边参考我的讲解。首先我们要清楚的是这个测试模型做了一件什么事情。因为它要处理的数据集是一个手写数字的MNIST数据集所以模型其实是一个多分类模型它的输入是这个手写数字的图片一个28*28维的图片输出是一个0-9的多分类概率模型。
<img src="https://static001.geekbang.org/resource/image/0c/44/0cfa5a28c6006d47e25fb6f1f7877f44.jpg" alt="" title="图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的环境之后我们还要验证一下是不是所有的配置都成功了。
在测试模型中,我们要牢牢记住实现它的四个主要步骤,分别是载入数据、定义模型、训练模型和评估模型。当然,我把这些关键知识点总结在了下面的知识表格里,你可以看看。
<img src="https://static001.geekbang.org/resource/image/d9/33/d906029b35925dd56c5c7270b330ce33.jpeg" alt="">
## 课后思考
这是一堂实践课所以今天的课后题就是希望你能完成TensorFlow的环境配置。在配置、试验过程中遇到任何问题你都可以在留言区提问我会尽力去解答也欢迎同学们分享自己的实践经验互相帮助。我们下节课见

View File

@@ -0,0 +1,145 @@
<audio id="audio" title="模型实战准备(二) | 模型特征、训练样本的处理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6a/16/6ayyb82b166cb52af679fb562dc4e016.mp3"></audio>
你好,我是王喆,欢迎来到模型实战准备第二课。
这节课,我们来讲实战中所需的模型特征和训练样本的处理。为什么我们要专门花一节课的时间讲这些呢?因为在推荐模型篇中,我们的重点在于学习模型结构的原理和实现,而要实现并且训练这些模型,我们就必须先解决训练所需的样本和特征的处理问题。
这节课我们先来把模型实战的准备工作做完。具体来说今天我会带你用Spark来处理深度学习模型训练所需的样本和特征再把特征存储到Redis中供模型线上服务时调用。
## 为什么选择Spark为TensorFlow处理特征和样本
这个时候你可能会有疑问我们的深度学习模型将在TensorFlow上进行训练为什么要用Spark处理样本可不可以直接让TensorFlow解决所有的事情呢这是一个好问题在我们学习具体的技术之前先解决这个架构上的疑问是很有必要的。
在业界的实践中,我们需要记住一个原则,就是**让合适的平台做合适的事情**。比如说数据处理是Spark的专长流处理是Flink的专长构建和训练模型是TensorFlow的专长。在使用这些平台的时候我们最好能够用其所长避其所短。这也是一个业界应用拥有那么多个模块、平台的原因。
你可能想说TensorFlow也可以处理数据啊。没错但是它并不擅长分布式的并行数据处理在并行数据处理能力上TensorFlow很难和动辄拥有几百上千个节点的Spark相比。那在面对海量数据的时候如果我们能够利用Spark进行数据清洗、数据预处理、特征提取的话最好的方案就是让Spark发挥它的长处承担“繁重”但相对简单的样本和特征处理的工作为TensorFlow减轻负担。
## 物品和用户特征都有哪些?
既然我们决定用Spark进行样本和特征处理那下一个问题又接踵而来我们能从MovieLens的数据集中抽取出什么特征呢这就要用到我们在特征工程篇中学到的关于推荐系统特征以及相关特征处理方法的知识如果你记得还不够扎实我建议你可以先回去复习一下。
MovieLens数据集中可供我们提取特征的数据表有两个分别是movies表和ratings表它们的数据格式如下
<img src="https://static001.geekbang.org/resource/image/87/b8/87cd18e09550522e85ef81a8f30869b8.jpg" alt="" title="图1 电影基本数据movies表用户评分数据ratings表">
接下来,我们按照“物品特征”“用户特征”“场景特征”,这三大类推荐系统特征的顺序,来看一看从这两张表中能提取出什么样的特征。
“物品特征”在我们的项目里指的就是电影特征了从movies表中我们可以提取出电影的基本信息包括movieId、title电影名、releaseYear发布年份和genre风格类型。除此之外我们还可以利用ratings表为每个电影提取出一些统计类的特征包括电影的平均评分、评分标准差等等。
接下来是“用户特征”。乍一看从movies和ratings表中除了userId我们好像找不到其他直接可用的用户信息了。这个时候千万不要忘了我们之前提到过的用户特征最重要的部分就是历史行为特征。
所以从用户的评分历史中我们其实可以提取出非常多有价值的特征。比如我们可以根据ratings表的历史联合movies表的电影信息提取出用户统计类特征它包括用户评分总数、用户平均评分、用户评分标准差、用户好评电影的发布年份均值、用户好评电影的发布年份标准差、用户最喜欢的电影风格以及用户好评电影ID等等。
最后是“场景特征”。我们可用的场景特征就一个,那就是评分的时间戳,我们把它作为代表时间场景的特征放到特征工程中。
好了,到这儿,我们就梳理完了所有可用的特征,我把它们总结在了下面的表格里,供你参考。
<img src="https://static001.geekbang.org/resource/image/ae/d5/ae45f7d9020dcb406291b8519d0312d5.jpeg" alt="" title="图2 所有可用的特征汇总表">
用Spark来提取这些特征的总体实现会比较琐碎所以我就不把全部代码贴在这里了你可以参考SparrowRecsys项目中的com.wzhe.sparrowrecsys.offline.spark.featureeng.FeatureEngForRecModel对象里面包含了所有特征工程的代码。这里我们只讲几个有代表性的统计型特征的处理方法。
```
val movieRatingFeatures = samplesWithMovies3.groupBy(col(&quot;movieId&quot;))
.agg(count(lit(1)).as(&quot;movieRatingCount&quot;),
avg(col(&quot;rating&quot;)).as(&quot;movieAvgRating&quot;),
stddev(col(&quot;rating&quot;)).as(&quot;movieRatingStddev&quot;))
```
计算统计型特征的典型方法就是利用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(&quot;csv&quot;).option(&quot;header&quot;, &quot;true&quot;).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(&quot;userAvgRating&quot;, avg(col(&quot;rating&quot;))
.over(Window.partitionBy(&quot;userId&quot;)
.orderBy(col(&quot;timestamp&quot;)).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](&quot;userId&quot;)
val valueMap = mutable.Map[String, String]()
valueMap(&quot;userRatedMovie1&quot;) = sample.getAs[String](&quot;userRatedMovie1&quot;)
valueMap(&quot;userRatedMovie2&quot;) = sample.getAs[String](&quot;userRatedMovie2&quot;)
...
valueMap(&quot;userAvgRating&quot;) = sample.getAs[String](&quot;userAvgRating&quot;)
valueMap(&quot;userRatingStddev&quot;) = sample.getAs[String](&quot;userRatingStddev&quot;)
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。
这些重点内容,我也都总结在了下面的表格里,你可以看一看。
<img src="https://static001.geekbang.org/resource/image/48/4d/48e15afe48dba3d35bef1e6f69aee84d.jpg" alt="">
## 课后思考
为了避免引入未来信息咱们课程中讲了基于userId的window函数方法你觉得还有哪些方法也能避免引入未来信息吗
欢迎把你的思考和答案写在留言区,如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事。我们下节课见!

View File

@@ -0,0 +1,96 @@
<audio id="audio" title="特别加餐 | “银弹”不可靠,最优的模型结构该怎么找?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1a/f1/1a5c171f40ce8405065c20debc6bf6f1.mp3"></audio>
你好,我是王喆。
推荐模型篇的课程到现在已经接近尾声了,我们也已经学习并且实践了六种深度学习推荐模型。最近,我发现很多同学会在留言区提出类似这样的问题:
- 老师我的Wide&amp;Deep模型在我的数据集上效果不好怎么办
- 老师是不是DeepFM模型的效果会比NeuralCF好
- 老师DIEN模型是不是现在效果最好的模型
其实,我完全理解同学们提问题的心情,就是希望在工作中通过不断地改进模型找到一个最强的模型,来尽快地提升推荐效果,击败当前的模型。我们团队中所有的新人也几乎都有这样的想法。
但是真的存在一个万能的模型结构,能在所有推荐系统上都达成最好的推荐效果吗?这节课,我希望我们能够放缓脚步,务虚一点,好好思考一下到底什么才是最好的模型结构,以及算法工程师正确的工作方法是什么。
## 有解决推荐问题的“银弹”吗?
在软件工程领域的著作《人月神话》中作者Brooks凭借自己在IBM管理2000人完成大型项目的经验得出了一个结论没有任何技术或管理上的进展 能够独立地许诺十年内使软件系统项目生产率、 可靠性或简洁性获得数量级上的进步。
Brooks用“没有银弹”来形容这个结论。在欧洲的古老传说中银色的子弹是能够一击杀死狼人这种怪物的特效武器。我们也可以把银弹理解成是最好的解决办法。“没有银弹”这个结论让很多期待寻找大型软件开发“捷径”的管理者和工程师们深感失望。但距离人月神话出版已经45年的今天我们找到“银弹”了吗
很遗憾,不仅在大型软件开发这个领域,“没有银弹”的观念深入人心,而且我要说的是,在推荐系统领域,也同样不存在能够一劳永逸解决问题的“银弹”,或者说根本不存在一种模型结构,它能够一击解决推荐效果问题,做到总是最优。
为什么这么讲呢我们拿阿里的DIEN模型做一个例子。
我们在[第21讲](https://time.geekbang.org/column/article/313736)曾经详细介绍过DIEN模型这里再做一个简要的回顾。DIEN模型的整体结构是一个加入了GRU序列模型的深度学习网络。其中序列模型部分主要负责利用用户的历史行为序列来预测用户下一步的购买兴趣模型的其他部分则是Embedding MLP的结构用来把用户的兴趣向量跟其他特征连接后进行预测。
由于阿里巴巴在业界巨大的影响力DIEN模型自2019年提出以来就被很多从业者认为是解决推荐问题的“银弹”并纷纷进行尝试。
但是在应用的过程中DIEN并没有体现出“银弹”的效果。在这个时候大家又都习惯于从自身上找原因比如说“是不是Embedding层的维度不够”“是不是应该再增加兴趣演化层的状态数量”“是不是哪个模型参数没有调好”等等。包括我自己在实践的过程中也会因为DIEN并没有产生预期的推荐效果提升而去思考是不是因为我们没有完整的复现DIEN模型。
说了这么多我想强调的是所有提出类似问题的同行都默认了一个前提假设就是在阿里巴巴的推荐场景下能够提高效果的DIEN模型在其他应用场景下应该同样有效。然而这个假设真的合理吗DIEN模型是推荐系统领域的“银弹”吗当然不是。接下来我就带你一起分析一下。
既然DIEN的要点是模拟并表达用户兴趣进化的过程那模型应用的前提必然是应用场景中存在着“兴趣进化”的过程。阿里巴巴的电商场景下因为用户的购买兴趣在不同时间点有变化所以有着明显的兴趣进化趋势。
比如说,用户在购买了电脑后,就有一定概率会购买电脑周边产品,用户在购买了某些类型的服装后,就会有一定概率选择与其搭配的其他服装。这些都是兴趣进化的直观例子,也是阿里巴巴的电商场景下非常典型的情况。
除此之外,我们还发现,在阿里巴巴的应用场景下,用户的兴趣进化路径能够被整个数据流近乎完整的保留。作为中国最大的电商集团,阿里巴巴各产品线组成的产品矩阵几乎能够完整地抓住用户购物过程中兴趣迁移的过程。当然,用户有可能去京东、拼多多等电商平台购物,从而打断阿里巴巴的兴趣进化过程,但从统计意义上讲,大量用户的购物链条还是可以被阿里巴巴的数据体系捕获的。
这样一来我们就总结出了DIEN有效的前提是应用场景满足两个条件一是应用场景存在“兴趣的进化”。二是用户兴趣的进化过程能够被数据完整捕获。如果二者中有一个条件不成立DIEN就很可能不会带来较大的收益。
为什么这么说呢还是以我自身的实践经历为例我现在工作的公司Roku是北美最大的视频流媒体平台在使用的过程中用户既可以选择我们自己的频道和内容也可以选择观看Netflix、YouTube或者其他流媒体频道的内容。但是一旦用户进入Netflix或者其他第三方应用我们就无法得到应用中的具体数据了。
在这样的场景下我们仅能获取用户一部分的观看、点击数据而且这部分的数据仅占用户全部数据的10%左右,用户的整个行为过程我们无法完全获取到,那么谈何构建起整个兴趣进化链条呢?
另外一点也很关键,通过分析我们发现,长视频用户的兴趣点相比电商而言其实是非常稳定的。电商用户可以今天买一套衣服,明天买一套化妆品,兴趣的变化过程非常快。但你很难想象长视频用户今天喜欢看科幻电影,明天就喜欢看爱情片,绝大多数用户喜好非常稳定地集中在一个或者几个兴趣点上。这也是序列模型并不能给我们公司提高效果的另一个主要原因。
总的来说通过DIEN的例子我们可以得出**到底怎样的模型结构是最优的模型结构,跟你的业务特点和数据特点强相关。因此,在模型结构的选择上,没有“银弹”,没有最优,只有最合适。**
## 在工作中避免学生思维
那么有没有可供参考的方法论来指导模型结构的选择,或者说更广义一点,指导各种模型参数的调优呢?当然是有的,但在谈这个方法论之前,我想先纠正一个工作中非常有害的思维方式,特别是对于刚工作一两年的同学,我们最应该纠正的就是工作中的学生思维。
**“学生思维”最典型的表现就是总是在寻求一个问题的标准答案**。因为在我们的学生时代,所有的题目都是有标准答案的,会就是会,不会就是不会。
但在工作中就不一样了。举个例子来说在讲Embedding的部分很多同学问我Embedding到底应该取多少维在实际的工作中这就是一个典型的没有标准答案的问题。实际工作中它应有的决策链条应该是下面这样的
1. 先取一个初始值比如说10维来尝试一下效果
1. 以10维Embedding的效果作为baseline进行一定程度的参数调优比如尝试5维和20维的Embedding比较它跟10维的效果确定更好的维度数
1. 如果项目时间和硬件条件允许我们还可以尝试fine tunning精细调参直到找到最优的维度设置
1. 在上线前再次评估Embedding线上存储所需存储量的限制如果线上存储的可用空间有限我们可以通过适当降低维度数缩小Embedding所需的存储空间。
你从这个过程中肯定能够体会到所谓Embedding的维度到底取多少这个问题。根本没有标准答案最优的答案跟你的数据量Embedding模型本身的结构都有关系甚至还要受到工程环境的制约。
类似的问题还有“局部敏感哈希到底要选择几个桶”“召回层的TopN中N到底应该选择多少”“Wide&amp;Deep模型中可不可以加入新的用户特征”“要不要在模型中加入正则化项Drop out”等等。这些问题都没有标准答案你只有通过实践中的尝试才能得到最优的设定。
其实这也是我在讲解这门课时一直秉承的原则,我希望把业界的主流方法告诉你,**期望你建立起来的是一套知识体系和方法论**,而不是一套能让你一劳永逸的模型参数,因为“银弹”并不存在。
## 算法工程师正确的工作方法
我们刚刚否定了“学生思维”,那该怎么建立起一套正确的算法工程师思维呢?下面是我总结出的一套通用的算法工程师的工作流程,虽然不能说我一定掌握了“正确”的方法,但这些工作方法都是从我多年的工作经验中总结出来的,也都得到了验证,你完全可以借助它们来完善自己的工作方法。
1. **问题提出** 清楚领导提出的问题,或者自己发现问题。
1. **数据和业务探索** 在动手解决这个问题之前,我们一定要花时间弄清楚业务的相关逻辑,并且动手用一些脚本程序弄清楚自己可利用数据的数据量、数据特点、提取出一些特征并分析特征和标签之间的相关性。
1. **初始解决方案** 根据探索结果提出初始解决方案。
1. **解决方案调优** :在初始解决方案之上,进行技术选型和参数调优,确定最终的解决方案。
1. **工程落地调整** :针对工程上的限制调整技术方案,尽量做到能简勿繁,能稳定不冒险。
1. **生产环境上线** 进行最终的调整之后,在生产环境上线。
1. **迭代与复盘** 根据生产环境的结果进行迭代优化,并复盘这个过程,继续发现问题,解决问题。
最后我还想补充一点,我一直认为,**做算法工程师,首先要有扎实全面的技术功底,但更重要的其实是自信和务实的精神,不迷信所谓的权威模型,不试图寻找万能的参数,从业务出发,从用户的真实行为出发,才能够构建出最适合你业务场景的推荐模型** 。
## 小结
在解决推荐问题上,我认为是没有“银弹”的,特别是在模型结构这个点上,我们必须综合考虑自己的业务特点和数据特点,在实践中不断进行参数调优,才能找到最合适自己业务场景的模型。
事实上,不仅仅是推荐问题,对于其他问题来说,我也不建议同学们去追求所谓的“银弹”。换句话说,我们必须要尽量避免学生思维,不要总是试图去寻找标准答案,而是应该尽可能多地掌握业界的主流技术手段,丰富自己的“武器库”,建立自己的知识框架。这样,我们才能在面对实际工作中复杂问题的时候,找到最合适的兵器。
除此之外,作为一名算法工程师,我建议你在工作中按照“问题提出”,“数据和业务探索”,“提出初始解决方案”,“解决方案调优”,“工程落地调整”,“生产环境上线”,“迭代与复盘”的顺序,去完成整个的项目周期。这是能帮助你快速建立起正确方法论的有效途径。
总的来说,算法工程师是一份极有挑战的工作,相比研发岗有非常确定的项目目标,算法的优化工作往往需要我们自己去找到那个可以提升的点,自己去找到那组最合适的参数,并且可以完成生产环境的工程实现。这给了我们极大的灵活性,也给了我们不小的业绩压力。希望这节课可以帮助你纠正一些误区,与你共勉。
## 课后思考
推荐模型的研究可谓层出不穷很多同学都热衷于追求实现最前沿的技术最fancy的模型你这样的吗你认可这种现象吗
欢迎把你的想法写在留言区,我们一起讨论!

View File

@@ -0,0 +1,119 @@
<audio id="audio" title="24 | 离线评估:常用的推荐系统离线评估方法有哪些?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f1/df/f15b259d4e853yyd741924d8d00b39df.mp3"></audio>
你好,我是王喆。今天我们要进入一个全新的章节,模型评估篇。
在推荐系统这个行业,所有人都在谈效果。就像我们在学习推荐模型篇的时候,你肯定也有过这样的疑问:
- DIEN这个模型的效果到底怎么样啊
- 我们用深度学习来构建模型到底能让推荐系统效果提高多少啊?
- DeepFM的效果是不是会比Wide&amp;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次采样过程中有的样本会被重复采样有的样本没有被抽出过我们再将这些没有被抽出的样本作为验证集进行模型验证这就是自助法的验证过程。
虽然自主法能够保持训练集的规模,但是它的缺点也很明显,它其实改变了原有数据的分布,有可能让模型产生一定程度的偏差。至于,到底是自助采样增加训练样本规模的收益大,还是数据分布被改变带来的损失大,这就需要我们在实践中进行验证了。
### 时间切割
说完了前三种方法,我们再来看时间切割法。在“[模型实战准备(二)](https://time.geekbang.org/column/article/308812)”那节课里我们曾经讲过一个概念叫“未来信息”。它是说如果我们在t时刻进行模型预测那么 t+1 时刻的信息就是未来信息。在构建特征工程的时候,我们要避免引入“未来信息”。
其实在进行模型评估的时候我们同样不应该在训练集中包含“未来”的样本。怎么理解这句话呢比如我们所有的样本数据分布在t<sub>0</sub>到t<sub>n</sub>这样的时间轴上如果训练样本是通过随机采样得到的那么训练数据也会分布在t<sub>0</sub>到t<sub>n</sub>同样测试数据也会分布在t<sub>0</sub>到t<sub>n</sub>上。
如果你细想这个事情其实是有点反常理的。因为训练模型的时候我们已经使用了t<sub>n</sub>这个时间窗口的数据结果你却用它来预测t<sub>0</sub>的事件,这不是很荒谬吗?这就相当于你有一个时光机,已经穿越到了明天,知道股票会涨,结果你又穿越回来,预测说明天股票会涨,这哪是预测呢?这就是“作弊”。
为了防止这类“信息穿越”导致的模型作弊现象发生我们一般会使用时间切割的方案去划分训练集和测试集它的做法很简单。比如你一共处理了30天的样本从第25天末开始切割前25天的样本作为训练集后5天的样本作为测试集这样我们就从根源上切断了引入“未来信息”的可能。当然切割的比例到底如何也需要根据你的实践来定一般来说我们控制训练集跟测试集的比例在3:1到10:1之间比例太小训练样本不够比例太大测试结果不够稳定。
### 离线Replay
时间切割的方法虽然能避免“信息穿越”但也不是没有缺点的。它的缺点就在于整个评估过程是静态的模型不会随着评估的进行而更新这显然是不符合事实的。就拿我们刚才举的例子来说用前25天的数据做训练集用后5天的数据做测试集。如果在生产环境中模型是日更新的那后5天的评测过程就不准确因为在离线测试中我们并没有在后5天的评测过程中做到日更模型。
那怎么解决这个问题呢?我们也可以在离线状态下对线上更新过程进行仿真,让整个评估过程“动”起来。**业界把这样离线仿真式的评估方式叫做离线Replay。**
下图就是动态的Replay评估法与静态的时间分割评估法的对比示意图。我们可以看到“Replay评估方法”先根据产生时间对测试样本由早到晚地进行排序再让模型根据样本时间的先后进行预测。在模型更新的时间点上模型需要增量学习更新时间点前的测试样本更新模型后再继续评估更新点之后的样本。
<img src="https://static001.geekbang.org/resource/image/4a/fe/4a530c53ef61ca5fee367a58070c32fe.png" alt="" title="图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(&quot;timestampLong&quot;, Array(0.8), 0.05)
val splitTimestamp = quantile.apply(0)
//切割样本为训练集和测试集
val training = smallSamples.where(col(&quot;timestampLong&quot;) &lt;= splitTimestamp).drop(&quot;timestampLong&quot;)
val test = smallSamples.where(col(&quot;timestampLong&quot;) &gt; splitTimestamp).drop(&quot;timestampLong&quot;)
```
## 小结
这节课我们学习了五种主流的推荐模型离线评估方法它们分别是Holdout检验、交叉检验、自助法、时间切割和离线Replay。
其中Holdout检验最简单常用它通过随机划分的方式把样本集划分成训练集和测试集。而交叉检验的评估效果更加稳定准确它通过划分样本集为k份再进行k次评估取平均的方式得到最终的评估指标。
自助法是为了解决样本量过少而提出的它可以通过有放回采样的方式扩充训练集但有改变数据本身分布的风险。而时间切割法在某个时间点上把样本分成前后两份分别用于模型训练和评估避免引入未来信息。最后是离线Replay它通过仿真线上模型更新过程来进行评估是最接近线上环境的离线评估方法但实现起来比较复杂。
总之,各种评估方法都有优有劣,你需要根据实践中的侧重点选择使用,我把它们的优缺点也总结在了文稿的表格里,方便你进行对比。
<img src="https://static001.geekbang.org/resource/image/19/fd/198bea8ba6ae2ecfe2d65617a6efe7fd.jpeg" alt="">
这节课我们讲了评估模型效果的方法之一,离线评估。但我们并没有具体来讲“效果”的衡量指标到底是什么。别着急,下节课我们就来学习推荐系统主要使用的效果评估指标,也会利用这节课学习到的评估方法来生成这些指标。
## 课后思考
你觉得离线Replay这个方法跟我们之前讲过的增强学习有什么相似之处吗你知道它们两个还有什么更深层次的关系吗
期待在留言区看到你的发现和思考,我们下节课见!

View File

@@ -0,0 +1,166 @@
<audio id="audio" title="25 | 评估指标:我们可以用哪些指标来衡量模型的好坏?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d5/7f/d57f931540d997717e99c3a6f9f8a17f.mp3"></audio>
你好,我是王喆。今天,我们来学习推荐模型的评估指标。
上节课我们讲了五种评估方法清楚了它们都是怎么把样本分割为训练集和测试集的。但是只分割样本是远远不够的为了比较模型效果的好坏还得用一些指标进行衡量。就像我们工作中经常说我的模型提高了“一个点”的效果那所谓的“一个点”指的是什么呢它其实说的就是我们的模型在一些经典的推荐指标上提升了1%的效果,这节课我就带你来捋一捋这些经典的推荐评估指标。
## 低阶评估指标
我按照指标计算的难易程度,和评估的全面性,把推荐系统的评估指标可以分成低阶评估指标和高阶评估指标两大类。对于低阶评估指标来说,准确率、精确率与召回率、对数损失、均方根误差,这四个指标在推荐模型评估中最常用,计算起来也最容易。所以,我们就先来学习一下这几个低阶评估指标的具体含义。
### 1. 准确率
准确率 (Accuracy)是指分类正确的样本占总样本个数的比例公式1就是$\text {Accuracy}=\frac{n_{\text {correct }}}{n_{\text {total }}}$。
其中, 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的值越高就证明模型在精确率和召回率的整体表现上越好。
$$<br>
\mathrm{F} 1=\frac{2 \cdot \text { precision } \cdot \text { recall }}{\text { precision }+\text { recall }}<br>
$$
### 3. 对数损失
接着我们来说一说对数损失Logloss这个评估指标。
首先在一个二分类问题中对数损失函数的定义就是下面的公式3。<br>
$$<br>
-\frac{1}{N} \sum_{i=1}^{N}\left(y_{i} \log P_{\mathrm{i}}+\left(1-y_{i}\right) \log \left(1-P_{i}\right)\right)<br>
$$
在这个公式中,$y_{i}$是输入实例 $x_{i}$ 的真实类别, $p_{i}$是预测输入实例 $x_{i}$ 是正样本的概率,$N$是样本总数。
而面对多分类问题的时候对数损失函数定义就变成了下面公式4的样子
$$<br>
\text { Multi-LogLoss }=-\frac{1}{n} \sum_{i=1}^{n} \sum_{j=1}^{m} y_{i, j} \log \left(p_{i, j}\right)<br>
$$
如果你仔细看公式就会发现二分类和多分类模型的Logloss其实就是我们之前讲过的逻辑回归和Softmax模型的损失函数而大量深度学习模型的输出层正是逻辑回归或Softmax因此采用Logloss作为评估指标能够非常直观地反映模型损失函数的变化。所以在训练模型的过程中我们在每一轮训练中都会输出Logloss来观察模型的收敛情况。
### 4. 均方根误差
刚才我们说的准确率、精确率、召回率、LogLoss都是针对分类模型指定的指标。分类模型就是指预测某个样本属于哪个类别的模型最典型的就是点击率预估模型。除了这类分类模型以外还有回归模型它是用来预测一个连续值比如预测某个用户对某个电影会打多少分这就是一个回归模型。
那我们对于回归模型有什么合适的评估指标吗?对于回归模型来说,最常用的评估指标就是**均方根误差**RMSERoot Mean Square Error。它的公式是求预测值跟真实值之间差值的均方根
$$<br>
\mathrm{RMSE}=\sqrt{\frac{\sum_{i=1}^{n}\left(y_{i}-\hat{y}_{l}\right)^{2}}{n}}<br>
$$
这个公式中,$y_{i}$是第i个样本点的真实值$ \hat{y}_{l}$是第i个样本点的预测值n是样本点的个数。那么均方根误差越小当然就证明这个回归模型预测越精确。
总的来说,我们刚才说的这四个评估指标,虽然在推荐系统中最常用,计算起来也最简单,但它们反应的结果还不够精确和全面。
比如说精确率和召回率可以反应模型在Top n个排序结果上的表现但我们要知道在真正的推荐问题中n的值是变化的因为用户可能会通过不断的翻页、下滑来拉取更多的推荐结果这就需要有更高阶的评估指标来衡量模型在不同数量推荐结果上的综合性能。所以我们接下来再讲几个非常流行也非常权威的高阶评估指标。
## 高阶评估指标
那在高阶评估指标部分我会给你讲P-R曲线、ROC曲线、平均精度均值这三个最常用的评估指标。
### 1. P-R曲线
首先我要说的是P-R曲线这里的P就是我们之前学过的精确率PrecisionR就是召回率Recall。刚才我们说了为了综合评价一个推荐模型的好坏不仅要看模型在一个Top n值下的精确率和召回率还要看到模型在不同N取值下的表现甚至最好能绘制出一条n从1到N准确率和召回率变化的曲线。这条曲线就是P-R曲线。
P-R曲线的横轴是召回率纵轴是精确率。对于一个推荐模型来说它的P-R曲线上的一个点代表“在某一阈值下模型将大于该阈值的结果判定为正样本将小于该阈值的结果判定为负样本时整体结果对应的召回率和精确率”。整条P-R曲线是通过从高到低移动正样本阈值生成的。如图1所示它画了两个测试模型模型A和模型B的对比曲线。其中实线代表模型A的P-R曲线虚线代表模型B的P-R曲线。
<img src="https://static001.geekbang.org/resource/image/27/40/27c1669b30da6817fc7275354fc1ff40.jpg" alt="">
从图中我们可以看到在召回率接近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 RateFPR假阳性率纵坐标是True Positive Rate TPR真阳性率。这两个名字读上去就有点拗口我们还是通过它们的定义来理解一下
$$<br>
\mathrm{FPR}=\frac{\mathrm{FP}}{N}, T P R=\frac{\mathrm{TP}}{P}<br>
$$
在公式中P指的是真实的正样本数量N是真实的负样本数量TP指的是P个正样本中被分类器预测为正样本的个数FP指的是N个负样本中被分类器预测为正样本的个数。但我估计你看了这个定义可能还是不好理解这个ROC曲线是怎么得到的。没关系我们真正去画一条ROC曲线你就明白了。
和P-R曲线一样ROC曲线也是通过不断移动模型正样本阈值生成的。假设测试集中一共有20个样本模型的输出如下表所示表中第一列为样本序号Class为样本的真实标签Score为模型输出的样本为正的概率样本按照预测概率从高到低排序。在输出最终的正例、负例之前我们需要指定一个阈值并且设定预测概率大于该阈值的样本会被判为正例小于该阈值的会被判为负例。
比如我们指定0.9为阈值,那么只有第一个样本会被预测为正例,其他全部都是负例。这里的阈值也被称为 “截断点”。
<img src="https://static001.geekbang.org/resource/image/4c/66/4c7f89a6717e0d272527a77a5fe64266.jpeg" alt="">
接下来我们要做的就是动态地调整截断点从最高的得分开始实际上是从正无穷开始对应着ROC曲线的零点逐渐调整到最低得分。每一个截断点都会对应一个FPR和TPR的值在ROC图上绘制出每个截断点对应的位置再连接每个点之后我们就能得到最终的ROC曲线了。那么ROC曲线上的点具体应该怎么确定呢
我们来看几个例子当截断点选择为正无穷的时候模型会把全部样本预测为负例那FP和TP必然都为0FPR和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)。
<img src="https://static001.geekbang.org/resource/image/a5/e6/a54e03043e1dca53a47d601c7b2e51e6.jpg" alt="">
其实还有一种更直观的绘制ROC曲线的方法。首先我们根据样本标签统计出正负样本的数量假设正样本数量为P负样本数量为N。然后我们把横轴的刻度间隔设置为1/N纵轴的刻度间隔设置为1/P。接着我们再根据模型输出的预测概率对样本进行从高到低的排序。
最后依次遍历样本。同时从零点开始绘制ROC曲线每遇到一个正样本就沿纵轴方向绘制一个刻度间隔的曲线每遇到一个负样本就沿横轴方向绘制一个刻度间隔的曲线直到遍历完所有样本曲线最终停在 (1,1) 这个点整个ROC曲线就绘制完成了。
在绘制完ROC曲线后我们也可以像P-R曲线一样计算出 ROC曲线的AUCAUC越高推荐模型的效果就越好。
### 3. 平均精度均值
最后我们来说平均精度均值mAPmAPmean average precision这个高阶指标它除了在推荐系统中比较常用在信息检索领域也很常用。mAP其实是对平均精度APaverage precision的再次平均因此在计算mAP前我们需要先学习什么是平均精度AP。
假设推荐系统对某一用户测试集的排序结果是1, 0, 0, 1, 1, 1。其中1代表正样本0代表负样本。接下来我们就按照之前学过的方法计算这个序列中每个位置上的precision@N。你可以自己先试着计算一下,也可以直接看我下面计算好的结果。
<img src="https://static001.geekbang.org/resource/image/f9/bb/f91acb00e50aa1f273cc1610148953bb.jpeg" alt="" title="每个位置的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@50Precision@100和Recall@100甚至逐渐过渡到P-R曲线。
总的来说,评估指标的选择不是唯一的,而是一个动态深入,跟你评测的“深度”紧密相关的过程。而且,在真正的离线实验中,虽然我们要通过不同角度评估模型,但也没必要陷入“完美主义”和“实验室思维”的误区,选择过多指标评估模型,更没有必要为了专门优化某个指标浪费过多时间。
离线评估的目的在于快速定位问题快速排除不可行的思路为线上评估找到“靠谱”的候选者。因此我们根据业务场景选择2~4个有代表性的离线指标进行高效率的离线实验才是离线评估正确的“打开方式”。
## 小结
这节课,我们重点介绍了模型离线评估中使用的评估指标。我把它们分成了两部分,简单直接的低阶评估指标,还有复杂全面的高阶评估指标。
低阶评估指标主要包括准确率,精确率,召回率和均方根误差。**准确率是指分类正确的样本占总样本个数的比例,精确率指的是分类正确的正样本个数占分类器判定为正样本个数的比例****召回率是分类正确的正样本个数占真正的正样本个数的比例,而均方根误差**的定义是预测值跟真实值之间差值的均方根。
高阶指标包括P-R曲线ROC曲线和平均精度均值。P-R曲线的横坐标是召回率纵坐标是精确率ROC曲线的横坐标是假阳性率纵坐标是真阳性率。P-R曲线和ROC曲线的绘制都不容易我希望你能多看几遍我在课程中讲的例子巩固一下。最后是平均精度均值mAP这个指标是对每个用户的精确率均值的再次平均。
最后,为了方便你记忆和对比,我也把所有指标的概念都总结在了文稿的表格里,你可以去看看。
<img src="https://static001.geekbang.org/resource/image/e1/1a/e1a0566473b367633f0d18346608661a.jpeg" alt="">
## 课后问题
对于我们今天学到的P-R曲线和ROC曲线你觉得它们的优缺点分别是什么呢在正负样本分布极不均衡的情况下你觉得哪个曲线的表现会更稳定、更权威一点
期待在留言区看到你对这节课的思考,我们下节课见!

View File

@@ -0,0 +1,149 @@
<audio id="audio" title="26 | 在线测试如何在推荐服务器内部实现A/B测试" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/95/ff/955624bf0f8874eda7c6a4e48yyd3cff.mp3"></audio>
你好我是王喆。这节课我们来聊一聊推荐系统的线上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层实验组和对照组变量的影响。
<img src="https://static001.geekbang.org/resource/image/e0/7b/e0da06ee473e3f551ac2cyy987957d7b.jpeg" alt="" title="层与层之间流量正交示例">
理解了第一句话我们再来看看什么叫同层之间的流量“互斥”。这里的“互斥”具体有2层含义
1. 如果同层之间进行多组A/B测试不同测试之间的流量不可以重叠这是第一个“互斥”
1. 一组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测试评估指标你可以看一看。
<img src="https://static001.geekbang.org/resource/image/eb/48/eb1c9db619f6ec5b8e62fc2a81419948.jpeg" alt="">
看了这些指标我想你也发现了线上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 = &quot;emb&quot;;
final static String bucketBModel = &quot;nerualcf&quot;;
final static String defaultModel = &quot;emb&quot;;
public static String getConfigByUserId(String userId){
if (null == userId || userId.isEmpty()){
return defaultModel;
}
if(userId.hashCode() % trafficSplitNumber == 0){
System.out.println(userId + &quot; is in bucketA.&quot;);
return bucketAModel;
}else if(userId.hashCode() % trafficSplitNumber == 1){
System.out.println(userId + &quot; is in bucketB.&quot;);
return bucketBModel;
}else{
System.out.println(userId + &quot; isn't in AB test.&quot;);
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&lt;Movie&gt; 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测试有三个优势
1. 实验环境就是线上的真实生产环境;
1. 可以直接得到线上的商业指标;
1. 不受离线数据“数据有偏”现象的影响。
在A/B测试的设计过程中我们要遵循层与层之间的流量“正交”同层之间的流量“互斥”这一设计原则这样才能既正确又高效地同时完成多组A/B测试。除此之外在线上评估指标的制定过程中我们要尽量保证这些指标与线上业务的核心指标保持一致这样才能更加准确地衡量模型的改进有没有帮助到公司的业务发展是否达成了公司的商业目标。
为了方便你复习,我把一些核心的知识点总结在了表格中,你可以看一看。
<img src="https://static001.geekbang.org/resource/image/2b/0e/2b470d744b02c013b0a4bb00748b010e.jpeg" alt="">
## 课后思考
今天讲的A/B测试的分层和分桶的原则你都理解了吗如果我们在测试模型的时候一个实验是在首页测试新的推荐模型另一个实验是在内容页测试新的推荐模型你觉得这两个实验应该放在同一层还是可以放在不同的层呢为什么
期待在留言区看到你的思考,如果有其他疑问也欢迎你随时提出来,我会一一解答,我们下节课见!

View File

@@ -0,0 +1,125 @@
<audio id="audio" title="27 | 评估体系如何解决A/B测试资源紧张的窘境" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cd/87/cd720c74b71b85962d3649yy64d9dc87.mp3"></audio>
你好,我是王喆。
我们在进行推荐系统评估时经常会遇到两类问题。
一类是在做线上A/B测试的时候流量经常不够用要排队等别人先做完测试之后才能进行自己的测试。线上A/B测试资源紧张的窘境会大大拖慢我们试验的新思路以及迭代优化模型的进度。
另一类是,离线评估加上在线评估有那么多种测试方法,在实际工作中,我们到底应该选择哪一种用来测试,还是都要覆盖到呢?
其实,这两个问题的答案是有深刻联系的,并不是孤立的。我认为最好的解决办法就是,建立起一套**推荐系统的评估体系用它来解决不同评估方法的配合问题以及线上A/B测试资源紧张的问题。这节课我就带你一起来厘清如何建立起一整套推荐系统评估体系。**
## 什么是推荐系统的评估体系?
首先,什么是评估体系呢?我先给它下一个定义,**推荐系统的评估体系指的是,由多种不同的评估方式组成的、兼顾效率和正确性的,一套用于评估推荐系统的解决方案**。一个成熟的推荐系统评估体系应该综合考虑评估效率和正确性,可以利用很少的资源,快速地筛选出效果更好的模型。
那对一个商业公司来说最公正也是最合理的评估方法就是进行线上测试来评估模型是否能够更好地达成公司或者团队的商业目标。但是正如我们开头所说线上A/B测试要占用宝贵的线上流量资源这些有限的线上测试机会远远不能满足算法工程师改进模型的需求。所以如何有效地把线上和离线测试结合起来提高测试的效率就是我们迫切的需求。
那我们该怎么去构建起一整套评估体系呢图1就是一个典型的评估体系示意图。从图中我们可以看到处于最底层的是传统的离线评估方法比如Holdout检验、交叉检验等往上是离线Replay评估方法再往上是一种叫Interleaving的线上测试方法我们等会还会详细介绍最后是线上A/B测试。
<img src="https://static001.geekbang.org/resource/image/yy/92/yy44dd15a4e727c8b6eec89fb187e392.jpg" alt="" title="图1 推荐系统的评测体系 [br](出自《深度学习推荐系统》)">
这四层结构共同构成完整的评估体系,做到了评估效率和评估正确性之间的平衡,越是底层的方法就会承担越多筛选掉改进思路的任务,这时候“评估效率”就成了更关键的考虑因素,那对于“正确性”的评估,我们反而没有多么苛刻的要求了。
总的来说,离线评估由于有着更多可供利用的计算资源,可以更高效、快速地筛选掉那些“不靠谱”的模型来改进思路,所以被放在了第一层的位置。
随着候选模型被一层层筛选出来越接近正式上线的阶段评估方法对评估“正确性”的要求就越严格。因此在模型正式上线前我们应该以最接近真实产品体验的A/B测试来做最后的模型评估产生最具说服力的在线指标之后才能够进行最终的模型上线完成模型改进的迭代过程。
讲了这么多,你可能会觉得,道理没问题,但工作中真的是这样吗?不如,我们来看个例子。下图就是一个很形象的工作中的模型筛选过程。
假设现在有30个待筛选的模型如果所有模型都直接进入线上A/B测试的阶段进行测试所需的测试样本是海量的由于线上流量有限测试的时间会非常长。但如果我们把测试分成两个阶段第一个阶段先进行初筛把30个模型筛选出可能胜出的5个再只对这5个模型做线上A/B测试所需的测试流量规模和测试时间长度都会大大减少。这里的初筛方法就是我们在评估体系中提到的离线评估、离线Replay和在线Interleaving等方法。
[<img src="https://static001.geekbang.org/resource/image/da/1b/da57fcb9287b31ec436a4dce87f11c1b.jpg" alt="" title="图2 模型的筛选过程 [br]图片出自The Netflix Tech Blog">](https://netflixtechblog.com/interleaving-in-online-experiments-at-netflix-a04ee392ec55)
到这里,我想你已经清楚了什么是推荐系统的评估体系,以及评估体系是有哪些方法组成的。但在这些组成方法中,我们还有两点要重点注意:**一个是离线Replay这个方法虽然我们之前讲过离线Replay的原理但是对于它的相关工程架构还没有讲过第二个是上面提到过的线上Interleaving方法。** 下面我就借着流媒体巨头Netflix的实践方案来讲解一下离线Replay和在线Interleaving的细节。
## Netflix的Replay评估方法实践
借着下图3我们来回顾一下[第24讲](https://time.geekbang.org/column/article/317319)学过的离线Replay方法的原理离线Replay通过动态的改变测试时间点来模拟模型的在线更新过程让测试过程更接近真实线上环境。
<img src="https://static001.geekbang.org/resource/image/cb/e4/cb05ba1a3a975f9d824df60bdaca7ee4.jpg" alt="" title="图3 静态时间分割评估与动态Replay评估 [br](出自《深度学习推荐系统》)">
但是在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再对外统一提供APIBatch APIs供其他模型在训练和评估的时候按照时间范围方便地获取。
[<img src="https://static001.geekbang.org/resource/image/3e/64/3e41699be3fa13709c7e898c4f07bf64.jpg" alt="" title="图4 Netflix的离线评估数据流架构——时光机 [br]出自The Netflix Tech Blog ">](https://netflixtechblog.com/distributed-time-travel-for-feature-generation-389cccdd3907)
这个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方法。
[<img src="https://static001.geekbang.org/resource/image/e2/29/e2257304c4e450138f81ea9460ddef29.jpg" alt="" title="图5 传统A/B测试和Interleaving方法的比较 [br]出自The Netflix Tech Blog ">](https://netflixtechblog.com/interleaving-in-online-experiments-at-netflix-a04ee392ec55)
那你可能想问了在使用Interleaving方法进行测试的时候我们该怎么保证对模型A和模型B的测试是公平的呢如果有一个模型的结果总排在第一位这对另一个模型不就不公平了吗
这个问题很好我们确实需要考虑推荐列表中位置偏差的问题要想办法避免来自模型A或者模型B的物品总排在第一位。因此我们需要以相等的概率让模型A和模型B产生的物品交替领先。这就像在野球场打球的时候两个队长会先通过扔硬币的方式决定谁先选人再交替来选择队员。
理解了原理我们再结合下面的图示来进一步理解Interleaving方法混合模型A和B结果的过程。和刚才说的野球场选人的过程一样我们先选模型A或者模型B的排名第一的物品作为最终推荐列表的第一个物品然后再交替选择直到填满整个推荐列表。所以最后得到的列表会是ABABAB或者BABABA这样的顺序而且这两种形式出现的概率应该是相等的这样才能保证两个模型的公平性。
<img src="https://static001.geekbang.org/resource/image/ff/40/ffacf8e910e56233c3a9d004b8c22d40.jpg" alt="" title="图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中随机选择第一个物品就像野球场选人一样完成推荐列表的生成。
还是老习惯,我把这节课的重要知识点总结在了下面的表格里,方便你及时回顾。
<img src="https://static001.geekbang.org/resource/image/75/12/7591c3bb54dd16caccb71cbdf995d012.jpeg" alt="">
这节课也是我们模型评估篇的最后一节课,希望通过整个模型评估篇的学习,你不仅能够熟悉起每一种评估方法,而且能够清楚它们之间的区别和联系,形成一个高效的评估体系。相信它会加快你模型迭代的速度,对你的实际工作产生非常积极的影响!
## 课后思考
在Interleaving方法中推荐列表是由模型A和模型B的结果共同组成的那如果模型A和模型B的结果中有重叠怎么办是保留模型A的结果还是模型B的结果呢你有什么好的想法吗
今天讲的评估体系,你知道怎么建立了吗?欢迎把你的思考和疑问写在留言区,不妨也把这节课分享给你的朋友们,我们下节课见!

View File

@@ -0,0 +1,107 @@
<audio id="audio" title="特别加餐TensorFlow的模型离线评估实践怎么做" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/77/e7/7704d444f9fe7741752e41586dc4a6e7.mp3"></audio>
你好,我是王喆。
上两节课,我们学习了离线评估的主要方法以及离线评估的主要指标。那这些方法和指标具体是怎么使用的,会遇到哪些问题呢?我们之前实现的深度学习模型的效果怎么样呢?
这节课我们直接进入实战在TensorFlow环境下评估一下我们之前实现过的深度学习模型。一方面这能帮助我们进一步加深对离线评估方法和指标的理解另一方面也能检验一下我们自己模型的效果。
## 训练集和测试集的生成
离线评估的第一步就是要生成训练集和测试集在这次的评估实践中我会选择最常用的Holdout检验的方式来划分训练集和测试集。划分的方法我们已经在[第23讲](https://time.geekbang.org/column/article/317114)里用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曲线AUCtf.keras.metrics.AUC(curve='ROC')、PR曲线AUCtf.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.7427ROC AUC从0.8256下降到了0.8138。这是非常正常的现象,因为模型在训练集上都会存在着轻微过拟合的情况。
如果测试集的评估结果相比训练集出现大幅下降比如下降幅度超过了5%就说明模型产生了非常严重的过拟合现象我们就要反思一下是不是在模型设计过程中出现了一些问题比如模型的结构对于这个问题来说过于复杂模型的层数或者每层的神经元数量过多或者我们要看一看是不是需要加入Dropout正则化项来减轻过拟合的风险。
除了观察模型自己的效果,在模型评估阶段,我们更应该重视不同模型之间的对比,这样才能确定我们最终上线的模型,下面我们就做一个模型效果的横向对比。
## 模型的效果对比
在推荐模型篇我们已经实现了EmbeddingMLP、NerualCF、Wide&amp;Deep以及DeepFM这四个深度学习模型后来还有同学添加了DIN的模型实现。
那接下来我们就利用这节课的模型评估方法来尝试对比一下这几个模型的效果。首先我直接把这些模型在测试集上的评估结果记录到了表格里当然我更建议你利用SparrowRecsys项目中的代码自己来计算一下多实践一下我们刚才说的模型评估方法。
<img src="https://static001.geekbang.org/resource/image/06/c3/067c96ed1a4d59b1e2a8d610cb6888c3.jpeg" alt="">
通过上面的比较我们可以看出Embedding MLP和Wide&amp;Deep模型在我们的MovieLens这个小规模数据集上的效果最好它们两个的指标也非常接近只不过是在不同指标上有细微的差异比如模型Loss指标上Wide&amp;Deep模型好一点在Accuracy、ROC AUC、PR AUC指标上Embedding MLP模型好一点。
遇到这种情况我们该如何挑出更好的那个模型呢一般我们会在两个方向上做尝试一是做进一步的模型调参特别是对于复杂一点的Wide&amp;Deep模型我们可以尝试通过参数的Fine Tuning微调让模型达到更好的效果二是如果经过多次尝试两个模型的效果仍比较接近我们就通过线上评选出最后的胜出者。
说完了效果好的指标不知道你有没有注意到一个反常的现象那就是模型DeepFM的评估结果非常奇怪怎么个奇怪法呢理论上来说DeepFM的表达能力是最强的可它现在展示出来的评估结果却最差。这种情况就极有可能是因为模型遇到了过拟合问题。为了验证个想法我们再来看一下DeepFM在训练集上的表现如下表所示
<img src="https://static001.geekbang.org/resource/image/e6/41/e659147e8da20228ed722e3f38eea641.jpeg" alt="">
我们很惊讶地发现DeepFM在测试集上的表现比训练集差了非常多。毫无疑问这个模型过拟合了。当然这里面也有我们数据的因素因为我们采用了一个规模很小的采样过的MovieLens数据集在训练复杂模型时小数据集往往更难让模型收敛并且由于训练不充分的原因模型中很多参数其实没有达到稳定的状态因此在测试集上的表现往往会呈现出比较大的随机性。
通过DeepFM模型效果对比的例子也再一次印证了我们在[“最优的模型结构该怎么找?”](https://time.geekbang.org/column/article/315620)那节课的结论:推荐模型没有银弹,每一个业务,每一类数据,都有最合适的模型结构,并不是说最复杂的,最新的模型结构就是最好的模型结构,我们需要因地制宜地调整模型和参数,这才是算法工程师最大的价值所在。
## 小结
这节实践课我们基于TensorFlow进行了深度推荐模型的评估整个实践过程可以分成三步。
第一步是导入Spark分割好的训练集和测试集。
第二步是在TensorFlow中设置评估指标再在测试集上调用model.evaluate函数计算这些评估指标。在实践过程中我们要清楚有哪些TensorFlow的指标可以直接调用。那在这节课里我们用到了最常用的Loss、Accuracy、ROC AUC、PR AUC四个指标。
第三步是根据四个深度推荐模型的评估结果进行模型效果的对比。通过对比的结果我们发现Embedding MLP和Wide&amp;Deep的效果是最好的。同时我们也发现本该表现很好的DeepFM模型它的评估结果却比较差原因是模型产生了非常严重的过拟合问题。
因此,在实际工作中,我们需要通过不断调整模型结构、模型参数,来找到最合适当前业务和数据集的模型。
## 课后思考
1.除了这节课用到的Loss、Accuracy、ROC AUC、PR AUC这四个指标你在TensorFlow的实践中还会经常用到哪些评估指标呢 你能把这些常用指标以及它们特点分享出来吗你可以参考TensorFlow的官方[Metrics文档](https://tensorflow.google.cn/api_docs/python/tf/keras/metrics)
2.你认为DeepFM评估结果这么差的原因除了过拟合还有什么更深层次的原因呢可以尝试从模型结构的原理上给出一些解释吗
期待在留言区看到你对DeepFM模型的思考和使用评估指标的经验我们下节课见

View File

@@ -0,0 +1,119 @@
<audio id="audio" title="04 | 特征工程:推荐系统有哪些可供利用的特征?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b5/50/b58aa6e0cbb347c8674332148707fe50.mp3"></audio>
你好,我是王喆。基础架构篇我们已经讲完了,你掌握得怎么样?希望你已经对深度学习推荐系统有了一个初步的认识。
从这节课开始,我们将会开启一个新的模块,特征工程篇。
如果说整个推荐系统是一个饭馆,那么特征工程就是负责配料和食材的厨师,推荐模型这个大厨做的菜好不好吃,大厨的厨艺肯定很重要,但配料和食材作为美食的基础也同样重要。而且只有充分了解配料和食材的特点,我们才能把它们的作用发挥到极致。
今天,我们就先来讲讲特征工程,说说到底**什么是特征工程,构建特征工程的基本原则是什么,以及推荐系统中常用的特征有哪些。**相信通过这节课的学习,能让你更好地利用起推荐系统相关的数据提升推荐的效果。
## 什么是特征工程
在[第一讲](https://time.geekbang.org/column/article/288917)中我们学习过,推荐系统就是利用“用户信息”“物品信息”“场景信息”这三大部分有价值数据,通过构建推荐模型得出推荐列表的工程系统。
<img src="https://static001.geekbang.org/resource/image/8d/7a/8d5c1c9dc5ca3a55057981b7418d907a.jpeg" alt="" title="图1 特征工程部分在推荐系统中的位置">
在这个系统之中,**特征工程就是利用工程手段从“用户信息”“物品信息”“场景信息”中提取特征的过程。**这个过程说起来容易,但实际做起来其实困难重重。
比如说一个网站或者App每天收集起来的用户日志采集来的站外信息自己公司员工编辑添加的结构化数据那么多那么庞杂怎么才能挑出那些对推荐有用的特征呢
再比如从“推荐模型”的角度来说,一个机器学习模型的输入,往往是一个数值型的向量。那用户性别,用户行为历史这些根本不是数字的信息怎么处理成一个模型可用的数值向量呢?
我们这节课先聚焦第一个问题,“怎么挑出有用特征”,下节课我们再解决第二个问题。都说“理论指导实践”,在展开讲有哪些有用的特征之前,我们先看一看构建特征工程有哪些原则或者规律可以遵循。
## 构建推荐系统特征工程的原则
我给推荐系统中的特征下了一个比较抽象的定义,**特征其实是对某个行为过程相关信息的抽象表达**。为什么这么说呢?因为一个行为过程必须转换成某种数学形式才能被机器学习模型所学习,为了完成这种转换,我们就必须将这些行为过程中的信息以特征的形式抽取出来。
我们来举个最简单的例子用户的性别有三个男、女、未知。但推荐模型没办法直接认识这三个类别它是一个只认识数字的“严重偏科理工男”所以我们就需要把它转换成1、2、3为了方便你理解这里我用的是一个最简单的方法不一定是最合适的这样的数字代号它才能处理。
但是,这种从具体行为信息转化成抽象特征的过程,往往会造成信息的损失。为什么这么说呢?
一是因为具体的推荐行为和场景中包含大量原始的场景、图片和状态信息,保存所有信息的存储空间过大,我们根本无法实现。
二是因为具体的推荐场景中包含大量冗余的、无用的信息,把它们都考虑进来甚至会损害模型的泛化能力。比如说,电影推荐中包含了大量的影片内容信息,我们有没有必要把影片的所有情节都当作特征放进推荐模型中去学习呢?其实没有必要,或者说收效甚微。
这其实也是我们构建推荐系统特征工程的原则:**尽可能地让特征工程抽取出的一组特征,能够保留推荐环境及用户行为过程中的所有“有用“信息,并且尽量摒弃冗余信息**。
接下来,我们就结合一个实际的例子,说一说在电影推荐这个场景下,我们该怎么贯彻特征工程原则来挑选特征。
现在你就可以先把自己当成是一个用户假设你正在选择看哪部电影。想一想在这个选择过程中你都会受什么因素影响呢如果是我的话可能影响我的因素有6个把它们按照重要性由高到低排序就是**电影类型我是否感兴趣、电影是不是大片、导演和演员我是否喜欢、电影海报是否吸引人、我是否已经观看过该影片以及我当时的心情**。
那站在一个工程师的角度,我们能不能用某些特征把这些要素表达出来呢?我尝试用表格的形式把它们特征化的方法列举了出来:
<img src="https://static001.geekbang.org/resource/image/af/5d/af921c7e81984281621729f6e75c1b5d.jpeg" alt="" title="图2 电影推荐的要素和特征化方式">
我们详细来讲一个要素比如如何知道一个用户是否对这个电影的类型动作、喜剧、爱情等感兴趣。一般来说我们会利用这个用户的历史观看记录来分析他已有的兴趣偏好这个兴趣偏好可能是每个电影类型的概率分布比如动作45%、喜剧30%、爱情25%。也可能是一个通过Embedding技术学出来的用户兴趣向量。
这个时候,我们就可以根据这个电影本身的特征,计算出用户对电影的感兴趣程度了。对于其他的特征,我们也都可以通过类似的分析,利用日志、元数据等信息计算得出。
不过,并不是所有的要素都能特征化。比如,“自己当时的心情”这个要素就被我们无奈地舍弃了,这是因为我们很难找到可用的信息,更别说抽取出特征了;再比如,“电影海报是否吸引人“这个要素,我们可以利用一些图像处理的方法提取出海报中的某些要点(比如海报中有哪些演员?是什么风格?),但想面面俱到地提取出海报中所有的图像要素,几乎是不可能的。
因此,**在已有的、可获得的数据基础上,“尽量”保留有用信息是现实中构建特征工程的原则**。
## 推荐系统中的常用特征
前面我以电影推荐为例,讲解了特征工程的基本原则,互联网中的推荐系统当然不仅限于电影推荐,短视频、新闻、音乐等等都是经典的推荐场景,那么它们常用的特征之间有没有共性呢?确实是有的,推荐系统中常用的特征有五大类,下面我一一来说。
### 1. 用户行为数据
用户行为数据User Behavior Data是推荐系统最常用也是最关键的数据。用户的潜在兴趣、用户对物品的真实评价都包含在用户的行为历史中。用户行为在推荐系统中一般分为显性反馈行为Explicit Feedback和隐性反馈行为Implicit Feedback两种在不同的业务场景中它们会以不同的形式体现。具体是怎么表现的呢你可以看我下面给出的几个例子。
<img src="https://static001.geekbang.org/resource/image/75/06/7523075958d83e9bd08966b77ea23706.jpeg" alt="" title="图3 不同业务场景下用户行为数据的例子">
对用户行为数据的使用往往涉及对业务的理解,不同的行为在抽取特征时的权重不同,而且一些跟业务特点强相关的用户行为需要推荐工程师通过自己的观察才能发现。
在当前的推荐系统特征工程中,隐性反馈行为越来越重要,主要原因是显性反馈行为的收集难度过大,数据量小。在深度学习模型对数据量的要求越来越大的背景下,仅用显性反馈的数据不足以支持推荐系统训练过程的最终收敛。所以,能够反映用户行为特点的隐性反馈是目前特征挖掘的重点。
### 2. 用户关系数据
互联网本质上就是人与人、人与信息之间的连接。如果说用户行为数据是人与物之间的“连接”日志那么用户关系数据User Relationship Data就是人与人之间连接的记录。就像我们常说的那句话“物以类聚人以群分”用户关系数据毫无疑问是非常值得推荐系统利用的有价值信息。
用户关系数据也可以分为“显性”和“隐性”两种或者称为“强关系”和“弱关系”。如图4所示用户与用户之间可以通过“关注”“好友关系”等连接建立“强关系”也可以通过“互相点赞”“同处一个社区”甚至“同看一部电影”建立“弱关系”。
<img src="https://static001.geekbang.org/resource/image/3c/3c/3c4f81f660b101a80a97137c6b89523c.jpeg" alt="" title="图4 社交网络关系的多样性">
在推荐系统中利用用户关系数据的方式也是多种多样的比如可以将用户关系作为召回层的一种物品召回方式也可以通过用户关系建立关系图使用Graph Embedding的方法生成用户和物品的Embedding还可以直接利用关系数据通过“好友”的特征为用户添加新的属性特征甚至可以利用用户关系数据直接建立社会化推荐系统。
### 3. 属性、标签类数据
推荐系统中另外一大类特征来源是属性、标签类数据这里我把属性类数据Attribute Data和标签类数据Label Data归为一组进行讨论是因为它们本质上都是直接描述用户或者物品的特征。属性和标签的主体可以是用户也可以是物品。它们的来源非常多样大体上包含图5中的几类。
<img src="https://static001.geekbang.org/resource/image/ba/69/ba044e0033b513d996633de77e11f969.jpeg" alt="" title="图5 属性、标签类数据的分类和来源">
用户、物品的属性、标签类数据是最重要的描述型特征。成熟的公司往往会建立一套用户和物品的标签体系由专门的团队负责维护典型的例子就是电商公司的商品分类体系也可以有一些社交化的方法由用户添加。图6就是豆瓣的“添加收藏”页面在添加收藏的过程中用户需要为收藏对象打上对应的标签这是一种常见的社交化标签添加方法。
<img src="https://static001.geekbang.org/resource/image/ba/b4/ba77ffaf72284c397896e8222fd8ffb4.jpeg" alt="" title="图6 豆瓣的“添加收藏”页面">
在推荐系统中使用属性、标签类数据一般是通过Multi-hot编码的方式将其转换成特征向量一些重要的属性标签类特征也可以先转换成Embedding比如业界最新的做法是将标签属性类数据与其描述主体一起构建成知识图谱Knowledge Graph在其上施以Graph Embedding或者GNNGraph Neural Network图神经网络生成各节点的Embedding再输入推荐模型。这里提到的不同的特征处理方法我们都会在之后的课程中详细来讲。
### 4. 内容类数据
内容类数据Content Data可以看作属性标签型特征的延伸同样是描述物品或用户的数据但相比标签类特征内容类数据往往是大段的描述型文字、图片甚至视频。
一般来说,内容类数据无法直接转换成推荐系统可以“消化”的特征,需要通过自然语言处理、计算机视觉等技术手段提取关键内容特征,再输入推荐系统。例如,在图片类、视频类或是带有图片的信息流推荐场景中,我们往往会利用计算机视觉模型进行目标检测,抽取图片特征,再把这些特征(要素)转换成标签类数据供推荐系统使用。
<img src="https://static001.geekbang.org/resource/image/f0/c2/f0033a7b1747ed467088d9df0f5f62c2.jpeg" alt="" title="图7 利用计算机视觉模型进行目标检测,抽取图片特征">
而文字信息则更多是通过自然语言处理的方法提取关键词、主题、分类等信息一旦这些特征被提取出来就跟处理属性、标签类特征的方法一样通过Multi-hot编码Embedding等方式输入推荐系统进行训练。
### 5. 场景信息(上下文信息)
最后一大类是场景信息或称为上下文信息Context Information它是描述推荐行为产生的场景的信息。最常用的上下文信息是“时间”和通过GPS、IP地址获得的“地点”信息。根据推荐场景的不同上下文信息的范围极广除了我们上面提到的时间和地点还包括“当前所处推荐页面”“季节”“月份”“是否节假日”“天气”“空气质量”“社会大事件”等等。
场景特征描述的是用户所处的客观的推荐环境,广义上来讲,任何影响用户决定的因素都可以当作是场景特征的一部分。但在实际的推荐系统应用中,由于一些特殊场景特征的获取极其困难,我们更多还是利用时间、地点、推荐页面这些易获取的场景特征。
## 小结
这节课我们一起进入推荐系统中一个非常重要的模块,特征工程模块的学习。推荐系统中可用的特征非常多,但它们基本上可被划分到“用户行为”“用户关系”“属性标签”“内容数据”“场景信息”这五个类别,而且挑选特征的方法也遵循着“保留有用信息,摒弃冗余信息”的原则。
就像本节开头说的一样,特征工程是准备食材的过程,准备食材的好坏直接影响到能不能做出好菜。同时,要准备的食材也和我们要做什么菜紧密相连。所以针对不同的推荐系统,我们也要针对它们的业务特点,因地制宜地挑选合适的特征,抓住业务场景中的关键信息。这才是特征工程中不变的准则,以及我们应该在工作中不断积累的业务经验。
从工程的角度来说,除了特征的挑选,特征工程还包括大量的数据预处理、特征转换、特征筛选等工作,下节课我们就一起学习一下特征处理的主要方法,提升一下我们“处理食材”的技巧!
## 课后思考
如果你是一名音乐App的用户你觉得在选歌的时候有哪些信息是影响你做决定的关键信息那如果再站在音乐App工程师的角度你觉得有哪些关键信息是可以被用来提取特征的哪些是很难被工程化的
欢迎在留言区畅所欲言,留下你的思考和疑惑。如果今天的内容你都学会了,那不妨也把这节课转发出去。今天的内容就到这里了,我们下节课见!

View File

@@ -0,0 +1,179 @@
<audio id="audio" title="05 | 特征处理如何利用Spark解决特征处理问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ba/77/ba2036d6db50ca66f00c39e6f3c8db77.mp3"></audio>
你好,我是王喆。
上节课,我们知道了推荐系统要使用的常用特征有哪些。但这些原始的特征是无法直接提供给推荐模型使用的,因为推荐模型本质上是一个函数,输入输出都是数字或数值型的向量。那么问题来了,像动作、喜剧、爱情、科幻这些电影风格,是怎么转换成数值供推荐模型使用的呢?用户的行为历史又是怎么转换成数值特征的呢?
而且类似的特征处理过程在数据量变大之后还会变得更加复杂因为工业界的数据集往往都是TB甚至PB规模的这在单机上肯定是没法处理的。那业界又是怎样进行海量数据的特征处理呢这节课我就带你一起来解决这几个问题。
## 业界主流的大数据处理利器Spark
既然要处理海量数据那选择哪个数据处理平台就是我们首先要解决的问题。如果我们随机采访几位推荐系统领域的程序员问他们在公司用什么平台处理大数据我想最少有一半以上会回答是Spark。作为业界主流的大数据处理利器Spark的地位毋庸置疑。所以今天我先带你了解一下Spark的特点再一起来看怎么用Spark处理推荐系统的特征。
Spark是一个分布式计算平台。所谓分布式指的是计算节点之间不共享内存需要通过网络通信的方式交换数据。Spark最典型的应用方式就是建立在大量廉价的计算节点上这些节点可以是廉价主机也可以是虚拟的Docker ContainerDocker容器
理解了Spark的基本概念我们来看看它的架构。从下面Spark的架构图中我们可以看到Spark程序由Manager Node管理节点进行调度组织由Worker Node工作节点进行具体的计算任务执行最终将结果返回给Drive Program驱动程序。在物理的Worker Node上数据还会分为不同的partition数据分片可以说partition是Spark的基础数据单元。
<img src="https://static001.geekbang.org/resource/image/4a/9b/4ae1153e4daee39985c357ed796eca9b.jpeg" alt="" title="图1 Spark架构图">
Spark计算集群能够比传统的单机高性能服务器具备更强大的计算能力就是由这些成百上千甚至达到万以上规模的工作节点并行工作带来的。
那在执行一个具体任务的时候,**Spark是怎么协同这么多的工作节点通过并行计算得出最终的结果呢**这里我们用一个任务来解释一下Spark的工作过程。
这个任务并不复杂我们需要先从本地硬盘读取文件textFile再从分布式文件系统HDFS读取文件hadoopFile然后分别对它们进行处理再把两个文件按照ID都join起来得到最终的结果。
这里你没必要执着于任务的细节只要清楚任务的大致流程就好。在Spark平台上处理这个任务的时候会将这个任务拆解成一个子任务DAGDirected Acyclic Graph有向无环图再根据DAG决定程序各步骤执行的方法。从图2中我们可以看到这个Spark程序分别从textFile和hadoopFile读取文件再经过一系列map、filter等操作后进行join最终得到了处理结果。
<img src="https://static001.geekbang.org/resource/image/01/fd/01524cdf0ff7f64bcf86c656dd5470fd.jpeg" alt="" title="图2 某Spark程序的任务有向无环图">
其中最关键的过程是我们要理解哪些是可以纯并行处理的部分哪些是必须shuffle混洗和reduce的部分。
这里的shuffle指的是所有partition的数据必须进行洗牌后才能得到下一步的数据最典型的操作就是图2中的groupByKey操作和join操作。以join操作为例我们必须对textFile数据和hadoopFile数据做全量的匹配才可以得到join后的dataframeSpark保存数据的结构。而groupByKey操作则需要对数据中所有相同的key进行合并也需要全局的shuffle才能完成。
与之相比map、filter等操作仅需要逐条地进行数据处理和转换不需要进行数据间的操作因此各partition之间可以完全并行处理。
此外在得到最终的计算结果之前程序需要进行reduce的操作从各partition上汇总统计结果随着partition的数量逐渐减小reduce操作的并行程度逐渐降低直到将最终的计算结果汇总到master节点主节点上。可以说shuffle和reduce操作的触发决定了纯并行处理阶段的边界。
<img src="https://static001.geekbang.org/resource/image/6e/13/6e50b4010c27fac81acb0b230516e113.jpeg" alt="" title="图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编码对其进行数值化的结果。
<img src="https://static001.geekbang.org/resource/image/94/15/94f78685d98671648638e330a461ab15.jpeg" alt="" title="图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(&quot;movieIdNumber&quot;, col(&quot;movieId&quot;).cast(sql.types.IntegerType))
//利用Spark的机器学习库Spark MLlib创建One-hot编码器
val oneHotEncoder = new OneHotEncoderEstimator()
.setInputCols(Array(&quot;movieIdNumber&quot;))
.setOutputCols(Array(&quot;movieIdVector&quot;))
.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的密度越大。这对于模型学习来说也不是一个好的现象,因为特征的区分度并不高。
<img src="https://static001.geekbang.org/resource/image/56/4e/5675f0777bd9275b5cdd8aa166cebd4e.jpeg" alt="" title="图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(&quot;movieId&quot;))
.agg(count(lit(1)).as(&quot;ratingCount&quot;),
avg(col(&quot;rating&quot;)).as(&quot;avgRating&quot;),
variance(col(&quot;rating&quot;)).as(&quot;ratingVar&quot;))
.withColumn(&quot;avgRatingVec&quot;, double2vec(col(&quot;avgRating&quot;)))
movieFeatures.show(10)
//分桶处理创建QuantileDiscretizer进行分桶将打分次数这一特征分到100个桶中
val ratingCountDiscretizer = new QuantileDiscretizer()
.setInputCol(&quot;ratingCount&quot;)
.setOutputCol(&quot;ratingCountBucket&quot;)
.setNumBuckets(100)
//归一化处理创建MinMaxScaler进行归一化将平均得分进行归一化
val ratingScaler = new MinMaxScaler()
.setInputCol(&quot;avgRatingVec&quot;)
.setOutputCol(&quot;scaleAvgRating&quot;)
//创建一个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`,以及特征值的开方,这又是为什么呢?
<img src="https://static001.geekbang.org/resource/image/69/ae/69f2abc980b8d8448867b58468729eae.jpeg" alt="" title="图6 YouTube推荐模型来源Deep Neural Networks for YouTube Recommendations">
其实,无论是平方还是开方操作,改变的还是这个特征值的分布,这些操作与分桶操作一样,都是希望通过改变特征的分布,让模型能够更好地学习到特征内包含的有价值信息。但由于我们没法通过人工的经验判断哪种特征处理方式更好,所以索性把它们都输入模型,让模型来做选择。
这里其实自然而然地引出了我们进行特征处理的一个原则,就是**特征处理并没有标准答案**,不存在一种特征处理方式是一定好于另一种的。在实践中,我们需要多进行一些尝试,找到那个最能够提升模型效果的一种或一组处理方式。
## 小结
这节课我们介绍了推荐系统中特征处理的主要方式并利用Spark实践了类别型特征和数值型特征的主要处理方法最后我们还总结出了特征处理的原则“特征处理没有标准答案需要根据模型效果实践出真知”。
针对特征处理的方法深度学习和传统机器学习的区别并不大TensorFlow、PyTorch等深度学习平台也提供了类似的特征处理函数。在今后的推荐模型章节我们会进一步用到这些方法。
最后,我把这节课的主要知识点总结成了一张表格,你可以利用它巩固今天的重点知识。
<img src="https://static001.geekbang.org/resource/image/b3/7b/b3b8c959df72ce676ae04bd8dd987e7b.jpeg" alt="">
这节课是我们的第一堂实战课,对于还未进入到工业界的同学,相信通过这节课的实践,也能够一窥业界的大数据处理方法,增强自己的工程经验,让我们一起由此迈入工业级推荐系统的大门吧!
## 课后思考
<li>
请你查阅一下Spark MLlib的编程手册找出Normalizer、StandardScaler、RobustScaler、MinMaxScaler这个几个特征处理方法有什么不同。
</li>
<li>
你能试着运行一下SparrowRecSys中的FeatureEngineering类从输出的结果中找出到底哪一列是我们处理好的One-hot特征和Multi-hot特征吗以及这两个特征是用Spark中的什么数据结构来表示的呢
</li>
这就是我们这节课的全部内容了,你掌握得怎么样?欢迎你把这节课转发出去。下节课我们将讲解一种更高阶的特征处理方法,它同时也是深度学习知识体系中一个非常重要的部分,我们到时候见!

View File

@@ -0,0 +1,130 @@
<audio id="audio" title="06 | Embedding基础所有人都在谈的Embedding技术到底是什么" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f0/c2/f0f2680861f53cda4183c4f87f0cc9c2.mp3"></audio>
你好我是王喆。今天我们聊聊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技术的神奇之处。
<img src="https://static001.geekbang.org/resource/image/19/0a/19245b8bc3ebd987625e36881ca4f50a.jpeg" alt="" title="图1 词向量例子">
你可能会觉得词向量技术离推荐系统领域还是有一点远那Netflix应用的电影Embedding向量方法就是一个非常直接的推荐系统应用。从Netflix利用矩阵分解方法生成的电影和用户的Embedding向量示意图中我们可以看出不同的电影和用户分布在一个二维的空间内由于Embedding向量保存了它们之间的相似性关系因此有了这个Embedding空间之后我们再进行电影推荐就非常容易了。具体来说就是我们直接找出某个用户向量周围的电影向量然后把这些电影推荐给这个用户就可以了。这就是Embedding技术在推荐系统中最直接的应用。
<img src="https://static001.geekbang.org/resource/image/da/4c/da7e73faacc5e6ea1c02345386bf6f4c.jpeg" alt="" title="图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的句子包含的词有w<sub>1</sub>,w<sub>2</sub>……w<sub>t</sub>,并且我们假定每个词都跟其相邻词的关系最密切。
根据模型假设的不同Word2vec模型分为两种形式CBOW模型图3左和Skip-gram模型图3右。其中CBOW模型假设句子中每个词的选取都由相邻的词决定因此我们就看到CBOW模型的输入是w<sub>t</sub>周边的词预测的输出是w<sub>t</sub>。Skip-gram模型则正好相反它假设句子中的每个词都决定了相邻词的选取所以你可以看到Skip-gram模型的输入是w<sub>t</sub>预测的输出是w<sub>t</sub>周边的词。按照一般的经验Skip-gram模型的效果会更好一些所以我接下来也会以Skip-gram作为框架来给你讲讲Word2vec的模型细节。
<img src="https://static001.geekbang.org/resource/image/f2/8a/f28a06f57e4aeb5f826df466cbe6288a.jpeg" alt="" title="图3 Word2vec的两种模型结构CBOW和Skip-gram">
### Word2vec的样本是怎么生成的
我们先来看看**训练Word2vec的样本是怎么生成的。** 作为一个自然语言处理的模型训练Word2vec的样本当然来自于语料库比如我们想训练一个电商网站中关键词的Embedding模型那么电商网站中所有物品的描述文字就是很好的语料库。
我们从语料库中抽取一个句子选取一个长度为2c+1目标词前后各选c个词的滑动窗口将滑动窗口由左至右滑动每移动一次窗口中的词组就形成了一个训练样本。根据Skip-gram模型的理念中心词决定了它的相邻词我们就可以根据这个训练样本定义出Word2vec模型的输入和输出输入是样本的中心词输出是所有的相邻词。
为了方便你理解我再举一个例子。这里我们选取了“Embedding技术对深度学习推荐系统的重要性”作为句子样本。首先我们对它进行分词、去除停用词的过程生成词序列再选取大小为3的滑动窗口从头到尾依次滑动生成训练样本然后我们把中心词当输入边缘词做输出就得到了训练Word2vec模型可用的训练样本。
<img src="https://static001.geekbang.org/resource/image/e8/1f/e84e1bd1f7c5950fb70ed63dda0yy21f.jpeg" alt="" title="图4 生成Word2vec训练样本的例子">
### Word2vec模型的结构是什么样的
有了训练样本之后我们最关心的当然是Word2vec这个模型的结构是什么样的。我相信通过第3节课的学习你已经掌握了神经网络的基础知识那再理解Word2vec的结构就容易多了它的结构本质上就是一个三层的神经网络如图5
<img src="https://static001.geekbang.org/resource/image/99/39/9997c61588223af2e8c0b9b2b8e77139.jpeg" alt="" title="图5 Word2vec模型的结构
">
它的输入层和输出层的维度都是V这个V其实就是语料库词典的大小。假设语料库一共使用了10000个词那么V就等于10000。根据图4生成的训练样本这里的输入向量自然就是由输入词转换而来的One-hot编码向量输出向量则是由多个输出词转换而来的Multi-hot编码向量显然基于Skip-gram框架的Word2vec模型解决的是一个多分类问题。
隐层的维度是NN的选择就需要一定的调参能力了我们需要对模型的效果和模型的复杂度进行权衡来决定最后N的取值并且最终每个词的Embedding向量维度也由N来决定。
最后是激活函数的问题这里我们需要注意的是隐层神经元是没有激活函数的或者说采用了输入即输出的恒等函数作为激活函数而输出层神经元采用了softmax作为激活函数。
你可能会问为什么要这样设置Word2vec的神经网络以及我们为什么要这样选择激活函数呢因为这个神经网络其实是为了表达从输入向量到输出向量的这样的一个条件概率关系我们看下面的式子
$$p\left(w_{O} \mid w_{I}\right)=\frac{\exp \left(v_{w_{O}}^{\prime}{v}_{w_{I}}\right)}{\sum_{i=1}^{V} \exp \left(v_{w_{i}}^{\prime}{ }^{\top} v_{w_{I}}\right)}$$
这个由输入词WI预测输出词WO的条件概率其实就是Word2vec神经网络要表达的东西。我们通过极大似然的方法去最大化这个条件概率就能够让相似的词的内积距离更接近这就是我们希望Word2vec神经网络学到的。
当然,如果你对数学和机器学习的底层理论没那么感兴趣的话,也不用太深入了解这个公式的由来,因为现在大多数深度学习平台都把它们封装好了,你不需要去实现损失函数、梯度下降的细节,你只要大概清楚他们的概念就可以了。
如果你是一个理论派其实Word2vec还有很多值得挖掘的东西比如为了节约训练时间Word2vec经常会采用负采样Negative Sampling或者分层softmaxHierarchical Softmax的训练方法。关于这一点我推荐你去阅读[《Word2vec Parameter Learning Explained》](https://github.com/wzhe06/Reco-papers/blob/master/Embedding/%5BWord2Vec%5D%20Word2vec%20Parameter%20Learning%20Explained%20%28UMich%202016%29.pdf)这篇文章,相信你会找到最详细和准确的解释。
### 怎样把词向量从Word2vec模型中提取出来
在训练完Word2vec的神经网络之后可能你还会有疑问我们不是想得到每个词对应的Embedding向量嘛这个Embedding在哪呢其实它就藏在输入层到隐层的权重矩阵WVxN中。我想看了下面的图你一下就明白了。
<img src="https://static001.geekbang.org/resource/image/0d/72/0de188f4b564de8076cf13ba6ff87872.jpeg" alt="" title="图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去计算相似性等重要的特征了。
<img src="https://static001.geekbang.org/resource/image/1e/96/1e6b464b25210c76a665fd4c34800c96.jpeg" alt="" title="图7 Word2vec的Lookup table">
### Word2vec对Embedding技术的奠基性意义
Word2vec是由谷歌于2013年正式提出的其实它并不完全是原创性的学术界对词向量的研究可以追溯到2003年甚至更早的时期。但正是谷歌对Word2vec的成功应用让词向量的技术得以在业界迅速推广进而使Embedding这一研究话题成为热点。毫不夸张地说Word2vec对深度学习时代Embedding方向的研究具有奠基性的意义。
从另一个角度来看Word2vec的研究中提出的模型结构、目标函数、负采样方法、负采样中的目标函数在后续的研究中被重复使用并被屡次优化。掌握Word2vec中的每一个细节成了研究Embedding的基础。从这个意义上讲熟练掌握本节课的内容是非常重要的。
## Item2VecWord2vec方法的推广
在Word2vec诞生之后Embedding的思想迅速从自然语言处理领域扩散到几乎所有机器学习领域推荐系统也不例外。既然Word2vec可以对词“序列”中的词进行Embedding那么对于用户购买“序列”中的一个商品用户观看“序列”中的一个电影也应该存在相应的Embedding方法。
<img src="https://static001.geekbang.org/resource/image/d8/07/d8e3cd26a9ded7e79776dd31cc8f4807.jpeg" alt="" title="图8 不同场景下的序列数据">
于是微软于2015年提出了Item2Vec方法它是对Word2vec方法的推广使Embedding方法适用于几乎所有的序列数据。Item2Vec模型的技术细节几乎和Word2vec完全一致只要能够用序列数据的形式把我们要表达的对象表示出来再把序列数据“喂”给Word2vec模型我们就能够得到任意物品的Embedding了。
Item2vec的提出对于推荐系统来说当然是至关重要的因为它使得“万物皆Embedding”成为了可能。对于推荐系统来说Item2vec可以利用物品的Embedding直接求得它们的相似性或者作为重要的特征输入推荐模型进行训练这些都有助于提升推荐系统的效果。
## 小结
这节课我们一起学习了深度学习推荐系统中非常重要的知识点Embedding。Embedding就是用一个数值向量“表示”一个对象的方法。通过Embedding我们又引出了Word2vecWord2vec是生成对“词”的向量表达的模型。其中Word2vec的训练样本是通过滑动窗口一一截取词组生成的。在训练完成后模型输入向量矩阵的行向量就是我们要提取的词向量。最后我们还学习了Item2vec它是Word2vec在任意序列数据上的推广。
我把这些重点的内容以表格的形式,总结了出来,方便你随时回顾。
<img src="https://static001.geekbang.org/resource/image/0f/7b/0f0f9ffefa0c610dd691b51c251b567b.jpeg" alt="">
这节课我们主要对序列数据进行了Embedding化那如果是图结构的数据怎么办呢另外有没有什么好用的工具能实现Embedding技术呢接下来的两节课我就会一一讲解图结构数据的Embedding方法Graph Embedding并基于Spark对它们进行实现。
## 课后思考
在我们通过Word2vec训练得到词向量或者通过Item2vec得到物品向量之后我们应该用什么方法计算他们的相似性呢你知道几种计算相似性的方法
如果你身边的朋友正对Embedding技术感到疑惑也欢迎你把这节课分享给TA我们下节课再见

View File

@@ -0,0 +1,117 @@
<audio id="audio" title="07 | Embedding进阶如何利用图结构数据生成Graph Embedding" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ea/50/ea3f2fc11cc1952419478432b08bcf50.mp3"></audio>
你好,我是王喆。
上一节课我们一起学习了Embedding技术。我们知道只要是能够被序列数据表示的物品都可以通过Item2vec方法训练出Embedding。但是互联网的数据可不仅仅是序列数据那么简单越来越多的数据被我们以图的形式展现出来。这个时候基于序列数据的Embedding方法就显得“不够用”了。但在推荐系统中放弃图结构数据是非常可惜的因为图数据中包含了大量非常有价值的结构信息。
那我们怎么样才能够基于图结构数据生成Embedding呢这节课我们就重点来讲讲基于图结构的Embedding方法它也被称为Graph Embedding。
## 互联网中有哪些图结构数据?
可能有的同学还不太清楚图结构中到底包含了哪些重要信息为什么我们希望好好利用它们并以它们为基础生成Embedding下面我就先带你认识一下互联网中那些非常典型的图结构数据如图1
<img src="https://static001.geekbang.org/resource/image/54/91/5423f8d0f5c1b2ba583f5a2b2d0aed91.jpeg" alt="" title="图1 互联网图结构数据">
事实上,图结构数据在互联网中几乎无处不在,最典型的就是我们每天都在使用的**社交网络**如图1-a。从社交网络中我们可以发现意见领袖可以发现社区再根据这些“社交”特性进行社交化的推荐如果我们可以对社交网络中的节点进行Embedding编码社交化推荐的过程将会非常方便。
**知识图谱**也是近来非常火热的研究和应用方向。像图1b中描述的那样知识图谱中包含了不同类型的知识主体如人物、地点等附着在知识主体上的属性如人物描述物品特点以及主体和主体之间、主体和属性之间的关系。如果我们能够对知识图谱中的主体进行Embedding化就可以发现主体之间的潜在关系这对于基于内容和知识的推荐系统是非常有帮助的。
还有一类非常重要的图数据就是**行为关系类图数据**。这类数据几乎存在于所有互联网应用中它事实上是由用户和物品组成的“二部图”也称二分图如图1c。用户和物品之间的相互行为生成了行为关系图。借助这样的关系图我们自然能够利用Embedding技术发掘出物品和物品之间、用户和用户之间以及用户和物品之间的关系从而应用于推荐系统的进一步推荐。
毫无疑问图数据是具备巨大价值的如果能将图中的节点Embedding化对于推荐系统来说将是非常有价值的特征。那下面我们就进入正题一起来学习基于图数据的Graph Embedding方法。
## 基于随机游走的Graph Embedding方法Deep Walk
我们先来学习一种在业界影响力比较大应用也很广泛的Graph Embedding方法Deep Walk它是2014年由美国石溪大学的研究者提出的。它的主要思想是在由物品组成的图结构上进行随机游走产生大量物品序列然后将这些物品序列作为训练样本输入Word2vec进行训练最终得到物品的Embedding。因此DeepWalk可以被看作连接序列Embedding和Graph Embedding的一种过渡方法。下图2展示了DeepWalk方法的执行过程。
<img src="https://static001.geekbang.org/resource/image/1f/ed/1f28172c62e1b5991644cf62453fd0ed.jpeg" alt="" title="图2 DeepWalk方法的过程">
接下来我就参照图2中4个示意图来为你详细讲解一下DeepWalk的算法流程。
首先我们基于原始的用户行为序列图2a比如用户的购买物品序列、观看视频序列等等来构建物品关系图图2b。从中我们可以看出因为用户U<sub>i</sub>先后购买了物品A和物品B所以产生了一条由A到B的有向边。如果后续产生了多条相同的有向边则有向边的权重被加强。在将所有用户行为序列都转换成物品相关图中的边之后全局的物品相关图就建立起来了。
然后我们采用随机游走的方式随机选择起始点重新产生物品序列图2c。其中随机游走采样的次数、长度等都属于超参数需要我们根据具体应用进行调整。
最后我们将这些随机游走生成的物品序列输入图2d的Word2vec模型生成最终的物品Embedding向量。
在上述DeepWalk的算法流程中唯一需要形式化定义的就是随机游走的跳转概率也就是到达节点v<sub>i</sub>下一步遍历v<sub>i</sub> 的邻接点v<sub>j</sub> 的概率。如果物品关系图是有向有权图那么从节点v<sub>i</sub> 跳转到节点v<sub>j</sub> 的概率定义如下:
$$P\left(v_{j} \mid v_{i}\right)=\left\{\begin{array}{ll}\frac{M_{i j}}{\sum_{j \in N_{+}\left(V_{i}\right)}}, m_{i j} &amp; v_{j} \in N_{+}\left(v_{i}\right) \\\ 0, &amp; \mathrm{e}_{i j} \notin \varepsilon\end{array}\right.$$
其中N+(v<sub>i</sub>)是节点v<sub>i</sub>所有的出边集合M<sub>ij</sub>是节点v<sub>i</sub>到节点v<sub>j</sub>边的权重即DeepWalk的跳转概率就是跳转边的权重占所有相关出边权重之和的比例。如果物品相关图是无向无权重图那么跳转概率将是上面这个公式的一个特例即权重M<sub>ij</sub>将为常数1且N+(v<sub>i</sub>)应是节点v<sub>i</sub>所有“边”的集合,而不是所有“出边”的集合。
再通过随机游走得到新的物品序列我们就可以通过经典的Word2vec的方式生成物品Embedding了。当然关于Word2vec的细节你可以回顾上一节课的内容这里就不再赘述了。
## 在同质性和结构性间权衡的方法Node2vec
2016年斯坦福大学的研究人员在DeepWalk的基础上更进一步他们提出了Node2vec模型。Node2vec通过调整随机游走跳转概率的方法让Graph Embedding的结果在网络的**同质性**Homophily和**结构性**Structural Equivalence中进行权衡可以进一步把不同的Embedding输入推荐模型让推荐系统学习到不同的网络结构特点。
我这里所说的网络的**“同质性”指的是距离相近节点的Embedding应该尽量近似**如图3所示节点u与其相连的节点s<sub>1</sub>、s<sub>2</sub>、s<sub>3</sub>、s<sub>4</sub>的Embedding表达应该是接近的这就是网络“同质性”的体现。在电商网站中同质性的物品很可能是同品类、同属性或者经常被一同购买的物品。
而**“结构性”指的是结构上相似的节点的Embedding应该尽量接近**比如图3中节点u和节点s<sub>6</sub>都是各自局域网络的中心节点它们在结构上相似所以它们的Embedding表达也应该近似这就是“结构性”的体现。在电商网站中结构性相似的物品一般是各品类的爆款、最佳凑单商品等拥有类似趋势或者结构性属性的物品。
<img src="https://static001.geekbang.org/resource/image/e2/82/e28b322617c318e1371dca4088ce5a82.jpeg" alt="" title="图3 网络的BFS和 DFS示意图">
理解了这些基本概念之后那么问题来了Graph Embedding的结果究竟是怎么表达结构性和同质性的呢
首先为了使Graph Embedding的结果能够表达网络的“**结构性**”,在随机游走的过程中,我们需要让游走的过程更倾向于**BFSBreadth First Search宽度优先搜索**因为BFS会更多地在当前节点的邻域中进行游走遍历相当于对当前节点周边的网络结构进行一次“微观扫描”。当前节点是“局部中心节点”还是“边缘节点”亦或是“连接性节点”其生成的序列包含的节点数量和顺序必然是不同的从而让最终的Embedding抓取到更多结构性信息。
而为了表达“**同质性**”,随机游走要更倾向于**DFSDepth First Search深度优先搜索**才行因为DFS更有可能通过多次跳转游走到远方的节点上。但无论怎样DFS的游走更大概率会在一个大的集团内部进行这就使得一个集团或者社区内部节点的Embedding更为相似从而更多地表达网络的“同质性”。
那在Node2vec算法中究竟是怎样控制BFS和DFS的倾向性的呢
其实它主要是通过节点间的跳转概率来控制跳转的倾向性。图4所示为Node2vec算法从节点t跳转到节点v后再从节点v跳转到周围各点的跳转概率。这里你要注意这几个节点的特点。比如节点t是随机游走上一步访问的节点节点v是当前访问的节点节点x<sub>1</sub>、x<sub>2</sub>、x<sub>3</sub>是与v相连的非t节点但节点x<sub>1</sub>还与节点t相连这些不同的特点决定了随机游走时下一次跳转的概率。
<img src="https://static001.geekbang.org/resource/image/6y/59/6yyec0329b62cde0a645eea8dc3a8059.jpeg" alt="" title="图4 Node2vec的跳转概率">
这些概率我们还可以用具体的公式来表示从当前节点v跳转到下一个节点x的概率$\pi_{v x}=\alpha_{p q}(t, x) \cdot \omega_{v x}$ 其中wvx是边vx的原始权重$\alpha_{p q}(t, x)$是Node2vec定义的一个跳转权重。到底是倾向于DFS还是BFS主要就与这个跳转权重的定义有关了。这里我们先了解一下它的精确定义我再作进一步的解释
$$\alpha_{p q(t, x)=}\left\{\begin{array}{ll}\frac{1}{p} &amp; \text { 如果 } d_{t x}=0\\\ 1 &amp; \text { 如果 } d_{t x}=1\\\frac{1}{q} &amp; \text { 如果 } d_{t x}=2\end{array}\right.$$
$\alpha_{p q}(t, x)$里的d<sub>tx</sub>是指节点t到节点x的距离比如节点x<sub>1</sub>其实是与节点t直接相连的所以这个距离d<sub>tx</sub>就是1节点t到节点t自己的距离d<sub>tt</sub>就是0而x<sub>2</sub>、x<sub>3</sub>这些不与t相连的节点d<sub>tx</sub>就是2。
此外,$\alpha_{p q}(t, x)$中的参数p和q共同控制着随机游走的倾向性。参数p被称为返回参数Return Parameterp越小随机游走回节点t的可能性越大Node2vec就更注重表达网络的结构性。参数q被称为进出参数In-out Parameterq越小随机游走到远方节点的可能性越大Node2vec更注重表达网络的同质性。反之当前节点更可能在附近节点游走。你可以自己尝试给p和q设置不同大小的值算一算从v跳转到t、x<sub>1</sub>、x<sub>2</sub>和x<sub>3</sub>的跳转概率。这样一来,应该就不难理解我刚才所说的随机游走倾向性的问题啦。
Node2vec这种灵活表达同质性和结构性的特点也得到了实验的证实我们可以通过调整p和q参数让它产生不同的Embedding结果。图5上就是Node2vec更注重同质性的体现从中我们可以看到距离相近的节点颜色更为接近图5下则是更注重结构性的体现其中结构特点相近的节点的颜色更为接近。
<img src="https://static001.geekbang.org/resource/image/d2/3a/d2d5a6b6f31aeee3219b5f509a88903a.jpeg" alt="" title="图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 CrossingUCL提出的FNN和Google的Wide&amp;Deep。它们的实现细节我们也会在后续课程里面介绍你这里只需要了解这个概念就可以了。
<img src="https://static001.geekbang.org/resource/image/e9/78/e9538b0b5fcea14a0f4bbe2001919978.jpg" alt="" title="图6 带有Embedding层的深度学习模型">
## 小结
这节课我们一起学习了Graph Embedding的两种主要方法分别是Deep Walk和Node2vec并且我们还总结了Embedding技术在深度学习推荐系统中的应用方法。
学习Deep Walk方法关键在于理解它的算法流程首先我们基于原始的用户行为序列来构建物品关系图然后采用随机游走的方式随机选择起始点重新产生物品序列最后将这些随机游走生成的物品序列输入Word2vec模型生成最终的物品Embedding向量。
而Node2vec相比于Deep Walk增加了随机游走过程中跳转概率的倾向性。如果倾向于宽度优先搜索则Embedding结果更加体现“结构性”。如果倾向于深度优先搜索则更加体现“同质性”。
最后我们介绍了Embedding技术在深度学习推荐系统中的三种应用方法“直接应用”“预训练”和“End2End训练”。这些方法各有特点它们都是业界主流的应用方法随着课程的不断深入我会带你一步一步揭开它们的面纱。
老规矩,在课程的最后,我还是用表格的方式总结了这次课的关键知识点,你可以利用它来复习巩固。
<img src="https://static001.geekbang.org/resource/image/d0/e6/d03ce492866f9fb85b4fbf5fa39346e6.jpeg" alt="">
至此我们就完成了所有Embedding理论部分的学习。下节课我们再一起进入Embedding和Graph Embedding的实践部分利用Sparrow Recsys的数据使用Spark实现Embedding的训练希望你到时能跟我一起动起手来
## 课后思考
你能尝试对比一下Embedding预训练和Embedding End2End训练这两种应用方法说出它们之间的优缺点吗
欢迎在留言区分享你的思考和答案如果这节Graph Embedding的课程让你有所收获那不妨也把这节课分享给你的朋友们我们下节课见

View File

@@ -0,0 +1,287 @@
<audio id="audio" title="08 | Embedding实战如何使用Spark生成Item2vec和Graph Embedding" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/86/10/86b4355e3748c990fcca80f14d7f3110.mp3"></audio>
你好,我是王喆。
前面两节课我们一起学习了从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表得到啦。
<img src="https://static001.geekbang.org/resource/image/36/c0/36a2cafdf3858b18a72e4ee8d8202fc0.jpeg" alt="" title="图1 movieLens数据集中的rating评分表">
不过在使用观影序列编码之前我们还要再明确两个问题。一是MovieLens这个rating表本质上只是一个评分的表不是真正的“观影序列”。但对用户来说当然只有看过这部电影才能够评价它所以我们几乎可以把评分序列当作是观影序列。二是我们是应该把所有电影都放到序列中还是只放那些打分比较高的呢
这里我是建议对评分做一个过滤只放用户打分比较高的电影。为什么这么做呢我们要思考一下Item2vec这个模型本质上是要学习什么。我们是希望Item2vec能够学习到物品之间的近似性。既然这样我们当然是希望评分好的电影靠近一些评分差的电影和评分好的电影不要在序列中结对出现。
那到这里我们明确了样本处理的思路就是对一个用户来说我们先过滤掉他评分低的电影再把他评论过的电影按照时间戳排序。这样我们就得到了一个用户的观影序列所有用户的观影序列就组成了Item2vec的训练样本集。
那这个过程究竟该怎么在Spark上实现呢其实很简单我们只需要明白这5个关键步骤就可以实现了
1. 读取ratings原始数据到Spark平台
1. 用where语句过滤评分低的评分记录
1. 用groupBy userId操作聚合每个用户的评分记录DataFrame中每条记录是一个用户的评分序列
1. 定义一个自定义操作sortUdf用它实现每个用户的评分记录按照时间戳进行排序
1. 把每个用户的评分记录处理成一个字符串的形式,供后续训练过程使用。
具体的实现过程,我还是建议你来参考我下面给出的代码,重要的地方我也都加上了注释,方便你来理解。
```
def processItemSequence(sparkSession: SparkSession): RDD[Seq[String]] ={
//设定rating数据的路径并用spark载入数据
val ratingsResourcesPath = this.getClass.getResource(&quot;/webroot/sampledata/ratings.csv&quot;)
val ratingSamples = sparkSession.read.format(&quot;csv&quot;).option(&quot;header&quot;, &quot;true&quot;).load(ratingsResourcesPath.getPath)
//实现一个用户定义的操作函数(UDF),用于之后的排序
val sortUdf: UserDefinedFunction = udf((rows: Seq[Row]) =&gt; {
rows.map { case Row(movieId: String, timestamp: String) =&gt; (movieId, timestamp) }
.sortBy { case (movieId, timestamp) =&gt; timestamp }
.map { case (movieId, timestamp) =&gt; movieId }
})
//把原始的rating数据处理成序列数据
val userSeq = ratingSamples
.where(col(&quot;rating&quot;) &gt;= 3.5) //过滤掉评分在3.5一下的评分记录
.groupBy(&quot;userId&quot;) //按照用户id分组
.agg(sortUdf(collect_list(struct(&quot;movieId&quot;, &quot;timestamp&quot;))) as &quot;movieIds&quot;) //每个用户生成一个序列并用刚才定义好的udf函数按照timestamp排序
.withColumn(&quot;movieIdStr&quot;, array_join(col(&quot;movieIds&quot;), &quot; &quot;))
//把所有id连接成一个String方便后续word2vec模型处理
//把序列数据筛选出来,丢掉其他过程数据
userSeq.select(&quot;movieIdStr&quot;).rdd.map(r =&gt; r.getAs[String](&quot;movieIdStr&quot;).split(&quot; &quot;).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&quot;592&quot;最相似的20个item
val synonyms = model.findSynonyms(&quot;592&quot;, 20)
for((synonym, cosineSimilarity) &lt;- synonyms) {
println(s&quot;$synonym $cosineSimilarity&quot;)
}
//保存模型
val embFolderPath = this.getClass.getResource(&quot;/webroot/sampledata/&quot;)
val file = new File(embFolderPath.getPath + &quot;embedding.txt&quot;)
val bw = new BufferedWriter(new FileWriter(file))
var id = 0
//用model.getVectors获取所有Embedding向量
for (movieId &lt;- model.getVectors.keys){
id+=1
bw.write( movieId + &quot;:&quot; + model.getVectors(movieId).mkString(&quot; &quot;) + &quot;\n&quot;)
}
bw.close()
```
从上面的代码中我们可以看出Spark的Word2vec模型训练过程非常简单只需要四五行代码就可以完成。接下来我就按照从上到下的顺序依次给你解析其中3个关键的步骤。
首先是创建Word2vec模型并设定模型参数。我们要清楚Word2vec模型的关键参数有3个分别是setVectorSize、setWindowSize和setNumIterations。其中setVectorSize用于设定生成的Embedding向量的维度setWindowSize用于设定在序列数据上采样的滑动窗口大小setNumIterations用于设定训练时的迭代次数。这些超参数的具体选择就要根据实际的训练效果来做调整了。
其次模型的训练过程非常简单就是调用模型的fit接口。训练完成后模型会返回一个包含了所有模型参数的对象。
最后一步就是提取和保存Embedding向量我们可以从最后的几行代码中看到调用getVectors接口就可以提取出某个电影ID对应的Embedding向量之后就可以把它们保存到文件或者其他数据库中供其他模块使用了。
在模型训练完成后我们再来验证一下训练的结果是不是合理。我在代码中求取了ID为592电影的相似电影。这部电影叫Batman蝙蝠侠我把通过Item2vec得到相似电影放到了下面你可以从直观上判断一下这个结果是不是合理。
<img src="https://static001.geekbang.org/resource/image/3a/10/3abdb9b411615487031bf03c07bf5010.jpeg" alt="" title="图2 通过Item2vec方法找出的电影Batman的相似电影">
当然因为Sparrow Recsys在演示过程中仅使用了1000部电影和部分用户评论集所以我们得出的结果不一定非常准确如果你有兴趣优化这个结果可以去movieLens下载全部样本进行重新训练。
## Graph Embedding数据准备
到这里我相信你已经熟悉了Item2vec方法的实现。接下来我们再来说说基于随机游走的Graph Embedding方法看看如何利用Spark来实现它。这里我们选择Deep Walk方法进行实现。
<img src="https://static001.geekbang.org/resource/image/1f/ed/1f28172c62e1b5991644cf62453fd0ed.jpeg" alt="" title="图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 =&gt; {
var pairSeq = Seq[String]()
var previousItem:String = null
sample.foreach((element:String) =&gt; {
if(previousItem != null){
pairSeq = pairSeq :+ (previousItem + &quot;:&quot; + 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 =&gt; {
val pairItems = pair._1.split(&quot;:&quot;)
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) &lt;- itemCount) itemTotalCount += v
//随机游走sampleCount次生成sampleCount个序列样本
for( w &lt;- 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 = &quot;&quot;
var culCount:Long = 0
//根据物品出现的概率,随机决定起始点
breakable { for ((item, count) &lt;- itemCount) {
culCount += count
if (culCount &gt;= randomDouble * itemTotalCount){
firstElement = item
break
}
}}
sample.append(firstElement)
var curElement = firstElement
//通过随机游走产生长度为sampleLength的样本
breakable { for( w &lt;- 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) &lt;- probDistribution) {
culCount += count
if (culCount &gt;= 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的实现中我们应该着重理解的是生成物品间的转移概率矩阵的方法以及通过随机游走生成训练样本过程。
最后,我还是把这节课的重点知识总结在了一张表格中,希望能帮助你进一步巩固。
<img src="https://static001.geekbang.org/resource/image/02/a7/02860ed1170d9376a65737df1294faa7.jpeg" alt="">
这里我还想再多说几句。这节课我们终于看到了深度学习模型的产出我们用Embedding方法计算出了相似电影对于我们学习这门课来说它完全可以看作是一个里程碑式的进步。接下来我希望你能总结实战中的经验跟我继续同行一起迎接未来更多的挑战
## 课后思考
上节课我们在讲Graph Embedding的时候还介绍了Node2vec方法。你能尝试在Deep Walk代码的基础上实现Node2vec吗这其中我们应该着重改变哪部分的代码呢
欢迎把你的思考和答案写在留言区如果你掌握了Embedding的实战方法也不妨把它分享给你的朋友吧我们下节课见

View File

@@ -0,0 +1,112 @@
<audio id="audio" title="答疑 | 基础架构篇+特征工程篇常见问题解答" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0a/a2/0aa8999bb1a3756298e7e1c1dc50aea2.mp3"></audio>
你好,我是王喆。
到今天为止基础架构篇和特征工程篇我们都学完了。这段时间我收到了同学们的很多留言也看到了大家在学习和实践过程中的很多疑问。今天这节课我挑了10道典型的问题想和你好好讨论一下希望可以帮助你解决困惑。
## 实战项目安装、操作类的问题
我们在第2节课讲了Sparrow Recys项目的安装方法不过我在留言区看到大家在安装的时候还是会遇到很多问题。这里我整理出了两类典型的问题我们一起看看。
**问题1因为没有项目经验想知道把Sparrow Recys项目git clone到本地之后怎么运行这个Maven project**
这里我再重新描述一下整个安装和执行的过程详细来说一共有6步
1.安装IDEA。到[这个地址](https://www.jetbrains.com/idea/download/#section=mac)下载IDE安装IDEA后打开IDEA
2.在IDEA中打开项目。选择File-&gt;Open-&gt;选择git clone到的项目根目录就可以把项目导入到IDEA
3.配置maven project。我们在IDEA的项目结构树的pom.xml上点击右键设置为maven project最新的IDE版本也可能不用就可以了
4.配置SDK。Sparrow Recsys使用了Java8Scala2.11的编译环境你可以在File-&gt;Project Structure-&gt;Project中配置Java SDK并在Global Libraries中配置Scala SDK
5.运行推荐服务器。我们找到类文件 class RecSysServercom.wzhe.sparrowrecsys.online.RecSysServer右键点击-&gt; run
6.打开Sparrow Recsys首页在浏览器中输入[http://localhost:6010/](http://localhost:6010/) 当看到Sparrow RecSys首页的时候就说明你整个配置和安装成功了。
** 问题2在项目中没有找到“为你推荐页”也没有看到一些项目介绍中提到的推荐算法是我安装过程中出错了吗**
这里我再强调说明一下你没有安装错Sparrow Recsys这个项目是随着课程的进展逐渐完善起来的。所以如果在你学习的时候课程还未完结Sparrow Recsys中可能会缺少课程还未进行到的模块。比如为你推荐这个功能是在课程的“推荐模型篇”中加入的所以具体的内容我也会在之后的课程中再加入。但课程已经讲解过或提到过的部分一定会在Sparrow Recsys中有对应的实践代码。
## 课程相关的知识误区
除了安装问题之外,我还发现同学们在学习的过程中对某些知识点有疑惑,那下面我就帮同学们来解答一些典型的疑惑。
** 问题3网上资料的大部分观点认为协同过滤这样的传统方法应该是在召回层但我们课程中把协同过滤放在了排序精排这是为什么呢**
这是个好问题。我们知道五六年前的传统推荐系统不少还在使用协同过滤作为主排序模型但这几年它就被慢慢淘汰了排序层变成了以深度学习推荐模型为主的复杂模型。不过因为协同过滤类算法比较简单线上计算过程也很高效比如矩阵分解之后可以进行embedding快速召回所以放在召回层也完全适用。
在这门课程中,我们总结的推荐系统架构是一个比较经典的架构,但你也没必要认为它就是无法改变的真理。在实际应用场景之中,我希望你能根据业务特点灵活运用。
** 问题4像多模态或者是通过其它预训练方法得到的向量直接加到推荐排序模型作为特征的话感觉没什么效果我理解是预训练学习的目标和排序学习目标并不一致。这个问题老师是怎么看的**
首先,我觉得这是一个很好的业务实践的问题。多模态指的是在推荐系统中引入视频、图片、语音等多种不同形式的数据和特征,希望以此来提升推荐效果。
在实际的业务应用里,确实存在多模态特征效果不强的问题。结合我的实践经验,我会觉得问题根源是因为,目前多模态的技术本质上还处于比较初期的阶段。
比如说我们可以用一些CV的技术去处理视频图像识别出其中的汽车、树木、人物等等。但你要说这些物品对最终的推荐效果到底有没有影响比如说视频中出现汽车到底对用户的点击率影响有多大我觉得还是比较微弱的它可能远不及知名演员这个要素的影响大。
当然,我一直强调所有的效果都要跟业务场景紧密结合起来,所以多模态到底有没有作用,根本无法一概而论,还是跟你的使用方法和对业务的理解强关联。比如在短视频推荐中,如果你能精确识别出视频中的明星是哪位,再用它作为推荐特征,我想肯定对最终的推荐效果有正向影响。
** 问题5对训练数据中的某项特征进行平方或者开方是为了改变训练数据的分布。训练数据的分布被改变后训练出来的模型岂不是不能正确拟合训练数据了**
这个也是一个常见的误区,如果你有这样的问题,说明你还没有弄明白特征的分布和训练数据的分布之间的关系。
对训练数据中的某个特征进行开方或者平方操作本质上是改变了特征的分布并不是训练数据的分布。特征的分布和训练数据的分布没有本质的联系只要你不改变训练数据label的分布最终预测出的结果都应该是符合数据本身分布的。因为你要预测的是label并不是特征本身。而且在最终的预测过程中这些开方、平方的特征处理操作是在模型推断过程中复现的本质上可以看作是模型的一部分所以不存在改变数据分布的问题。
**问题6“为了使 Graph Embedding 的结果能够表达网络的‘结构性’,在随机游走的过程中,我们需要让游走的过程更倾向于 BFSBreadth First Search宽度优先搜索”。这里应该是DFS吧并且同质性是使用BFS。**
这是[第7讲](https://time.geekbang.org/column/article/296672)中的一个知识点这个疑问非常地常见因为BFS、DFS与结构性、同质性的关系本身确实有一点反直觉。这也是我们在学习Node2vec模型的时候经常会有的问题也推荐其他有疑问的同学关注一下。
在这里,我需要再强调一下,课程中的描述是完全正确的,也就是为了使 Graph Embedding 的结果能够表达网络的“结构性”,在随机游走的过程中,我们需要让游走的过程更倾向于 BFS为了表达“同质性”需要倾向于DFS。我们一定要厘清它们之间的正确关系。
这里,我直接把[Node2vec原论文](https://github.com/wzhe06/Reco-papers/blob/master/Embedding/%5BNode2vec%5D%20Node2vec%20-%20Scalable%20Feature%20Learning%20for%20Networks%20%28Stanford%202016%29.pdf)中的论述贴在了下面,你直接参考原文,会理解得更深刻一些。
>
<p>We observe that BFS and DFS strategies play a key role in producing representations that reflect either of the above equivalences.<br>
&nbsp;<br>
In particular, the neighborhoods sampled by BFS lead to embeddings that correspond closely to structural equivalence.<br>
&nbsp;<br>
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).<br>
&nbsp;<br>
In DFS, the sampled nodes more accurately reflect a macro-view of the neighborhood which is essential in inferring communities based on homophily.<br>
&nbsp;<br>
参考译文:<br>
我们观察到BFS和DFS策略在产生向量表达时发挥着关键的作用。特别是通过BFS采样得到的邻域节点使生成的相应Embedding更接近结构性一致。而对于DFS来说情况恰恰相反由于DFS可以进一步采样到远离节点u样本大小k固定的部分因此可以探索更大范围的网络。在DFS中采样的节点可以更准确地反映邻域的宏观视图这对于推断社区的同质性至关重要。</p>
## 关于推荐系统的深入思考
解决了一些常见的知识性的疑问,我们再来看看一些关于课程具体内容的延伸思考。我觉得这些问题都提得都很好,说明同学们学习的时候都有在认真思考,同时,我也鼓励大家都带着问题来学习,把自己的思考分享出来,这也能帮助到更多的同学。
**问题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节对内容有深度思考和提问的同学你们的每个问题都很精彩。在接下来的课程中欢迎你继续畅所欲言把留言区这个工具好好利用起来我们一起进步

View File

@@ -0,0 +1,171 @@
<audio id="audio" title="09 | 线上服务:如何在线上提供高并发的推荐服务?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4e/e8/4e21a14b43df0cacb624e01e839426e8.mp3"></audio>
你好,我是王喆。今天开始,我们进入线上服务篇的学习。
很多同学提起推荐系统,首先想到的是那些结构“华丽”,发展迅速的推荐模型。但事实上,在一个实际的工业级推荐系统中,训练和实现推荐模型的工作量往往连一半都没有。大量的工作都发生在搭建并维护推荐服务器、模型服务模块,以及特征和模型参数数据库等线上服务部分。
同时,由于线上服务模块是直接服务用户,产生推荐结果的模块,如果一旦发生延迟增加甚至服务宕机的情况,就会产生公司级别的事故。因此毫不夸张地说,线上服务实际上是推荐系统中最关键的一个模块。
线上服务如果写得不好,不仅杂乱无章,而且难以升级维护。因此,为了让你掌握搭建起一个支持深度学习的、稳定可扩展的推荐服务的方法,在这一模块中,我们会依次来讲线上服务器、特征存储、模型服务等模块的知识。
今天我们先聚焦线上服务器一起搭建直接产生推荐结果的服务接口。在这个过程中我们按照先了解、后思考、再实践的顺序依次解决这3个关键问题
1. 一个工业级的推荐服务器内部究竟都做了哪些事情?
1. 像阿里、字节、腾讯这样级别的公司,它们的推荐系统是怎么承接住每秒百万甚至上千万的推荐请求的?
1. 我们自己该如何搭建一个工业级推荐服务器的雏形呢?
## 工业级推荐服务器的功能
首先我们来解决第一个问题一个工业级的推荐服务器内部究竟做了哪些事情要回答这个问题我们要先回到专栏的出发点在推荐系统的技术架构图上找到推荐系统线上服务模块的位置。只有我们心中有全局学习才能有重点。图1中红色的部分就是我们要详细来讲的线上服务模块。
<img src="https://static001.geekbang.org/resource/image/c1/ed/c16ef5cbebc41008647425083b7b38ed.jpeg" alt="" title="图1 推荐系统技术架构图">
可以看到线上服务模块的功能非常繁杂它不仅需要跟离线训练好的模型打交道把离线模型进行上线在线进行模型服务Model Serving还需要跟数据库打交道把候选物品和离线处理好的特征载入到服务器。
而且线上服务器内部的逻辑也十分地复杂不仅包括了一些经典的过程比如召回层和排序层还包括一些业务逻辑比如照顾推荐结果多样性流行度的一些硬性的混合规则甚至还包括了一些AB测试相关的测试代码。
## 高并发推荐服务的整体架构
我刚才说的就是线上服务的技术框架了可以说想要把线上服务写好难度并不小更何况在面对高QPS的压力下事情还会变得更复杂。接下来我们就来看第二个问题说一说阿里、字节、腾讯这样级别的公司使用了哪些策略来承接住每秒百万甚至是上千万推荐请求的。
说实话,想彻底讲清楚这个问题并不容易,因为大厂关于甚高并发具体的解决方案是集整个集团的技术精英打造的,而且维护一个高可用的服务集群的工作也不是一个算法工程师的主要工作方向。但这里,我还是希望你能够从宏观的角度了解高并发的主要解决方案,因为它是一个工业级推荐系统的重要组成部分,也是我们在与架构组配合工作时应有的知识储备。
宏观来讲,高并发推荐服务的整体架构主要由三个重要机制支撑,它们分别是**负载均衡、缓存、推荐服务降级机制。**下面,我们一一来看。
首先是负载均衡。它是整个推荐服务能够实现高可用、可扩展的基础。当推荐服务支持的业务量达到一定规模的时候单独依靠一台服务器是不可行的无论这台服务器的性能有多强大都不可能独立支撑起高QPSQueries Per Second每秒查询次数的需求。这时候我们就需要增加服务器来分担独立节点的压力。既然有多个劳动力在干活那我们还需要一个“工头”来分配任务以达到按能力分配和高效率分配的目的这个“工头”就是所谓的“负载均衡服务器”。
下图就很好地展示了负载均衡的原理。我们可以看到负载均衡服务器Load Balancer处在一个非常重要的位置。因此在实际工程中负载均衡服务器也经常采用非常高效的nginx技术选型甚至采用专门的硬件级负载均衡设备作为解决方案。
[<img src="https://static001.geekbang.org/resource/image/a2/e1/a2daf129556bc3b9fd7dcde4230db8e1.jpeg" alt="" title="图2 高并发情况下的负载均衡服务器来源GitHub">](https://github.com/dmytrostriletskyi/heroku-load-balancer)
这个时候,有的同学可能会问,“负载均衡”解决高并发的思路是“增加劳动力”,那我们能否从“减少劳动量”的角度来解决高并发带来的负载压力呢?这是一个非常好的角度。要知道,推荐过程特别是基于深度学习的推荐过程往往是比较复杂的,进一步来说,当候选物品规模比较大的时候,产生推荐列表的过程其实非常消耗计算资源,服务器的“劳动量”非常大。这个时候,我们就可以通过减少“硬算”推荐结果的次数来给推荐服务器减负,那具体怎么做呢?
比如说当同一个用户多次请求同样的推荐服务时我们就可以在第一次请求时把TA的推荐结果缓存起来在后续请求时直接返回缓存中的结果就可以了不用再通过复杂的推荐逻辑重新算一遍。再比如说对于新用户来说因为他们几乎没有行为历史的记录所以我们可以先按照一些规则预先缓存好几类新用户的推荐列表等遇到新用户的时候就直接返回。
因此在一个成熟的工业级推荐系统中合理的缓存策略甚至能够阻挡掉90%以上的推荐请求,大大减小推荐服务器的计算压力。
但不管再强大的服务集群,再有效的缓存方案,也都有可能遭遇特殊时刻的流量洪峰或者软硬件故障。在这种特殊情况下,为了防止推荐服务彻底熔断崩溃,甚至造成相关微服务依次崩溃的“雪崩效应”,我们就要在第一时间将问题控制在推荐服务内部,而应对的最好机制就是“服务降级”。
所谓“服务降级”就是抛弃原本的复杂逻辑采用最保险、最简单、最不消耗资源的降级服务来渡过特殊时期。比如对于推荐服务来说我们可以抛弃原本的复杂推荐模型采用基于规则的推荐方法来生成推荐列表甚至直接在缓存或者内存中提前准备好应对故障时的默认推荐列表做到“0”计算产出服务结果这些都是服务降级的可行策略。
**总之,“负载均衡”提升服务能力,“缓存”降低服务压力,“服务降级”机制保证故障时刻的服务不崩溃,压力不传导**,这三点可以看成是一个成熟稳定的高并发推荐服务的基石。
## 搭建一个工业级推荐服务器的雏形
那说了这么多,这对我们搭建一个工业级推荐服务器有什么实际帮助呢?
相信你肯定听说过一句话,算法工程师是“面试造火箭,工作拧螺丝”。说实话,这确实反映了算法岗面试的一些不合理之处,但也不是说造火箭的知识不应该掌握。要给一个火箭拧螺丝,真不是说会拧螺丝就可以了,还真是得清楚火箭的构造是什么样的,否则螺丝你是拧上了,但地方拧错了,照样会让火箭出事故。
我们刚才讲的大厂处理高并发服务的方法就是“造火箭”理解了这些方法我们再来学学实际工作中“拧螺丝”的技巧就能做到有的放矢。下面我们就一起在Sparrow Recsys里面实践一下搭建推荐服务器的过程看看如何一步步拧螺丝搭建起一个可用的推荐服务器。当然它肯定无法直接具备负载均衡这些企业级服务的能力但我可以保证它可以作为一个工业级推荐服务器的雏形。让你以此为起点逐渐把它扩展成为一个成熟的推荐服务。
首先我们要做的就是选择服务器框架。这里我们选择的服务器框架是Java嵌入式服务器Jetty。为什么我们不选择其他的服务器呢原因有三个。
第一相比于Python服务器的效率问题以及C++服务器的开发维护难度Java服务器在效率和开发难度上做到了一个权衡而且互联网上有大量开源Java项目可以供我们直接融合调用所以Java服务器开发的扩展性比较好。第二相比Tomcat等其他Java服务器Jetty是嵌入式的它更轻量级没有过多J2EE的冗余功能可以专注于建立高效的API推荐服务。而Tomcat更适用于搭建一整套的J2EE项目。第三相比于基于Node.js、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(&quot;0.0.0.0&quot;, port);
//创建Jetty服务器
Server server = new Server(inetAddress);
//创建Jetty服务器的环境handler
ServletContextHandler context = new ServletContextHandler();
context.setContextPath(&quot;/&quot;);
context.setWelcomeFiles(new String[] { &quot;index.html&quot; });
//添加APIgetMovie获取电影相关数据
context.addServlet(new ServletHolder(new MovieService()), &quot;/getmovie&quot;);
//添加APIgetuser获取用户相关数据
context.addServlet(new ServletHolder(new UserService()), &quot;/getuser&quot;);
//添加APIgetsimilarmovie获取相似电影推荐
context.addServlet(new ServletHolder(new SimilarMovieService()), &quot;/getsimilarmovie&quot;);
//添加APIgetrecommendation获取各类电影推荐
context.addServlet(new ServletHolder(new RecommendationService()), &quot;/getrecommendation&quot;);
//设置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(&quot;application/json&quot;);
response.setStatus(HttpServletResponse.SC_OK);
response.setCharacterEncoding(&quot;UTF-8&quot;);
response.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;);
//获得请求中的id参数转换为movie id
String movieId = request.getParameter(&quot;id&quot;);
//从数据库中获取该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(&quot;&quot;);
}
} catch (Exception e) {
e.printStackTrace();
response.getWriter().println(&quot;&quot;);
}
}
```
熟悉了这个Servlet服务其他服务就依葫芦画瓢就可以啦。唯一的不同就是其中的业务逻辑。如果你已经从GitHub上下载了Sparrow Recsys项目把它运行起来并且在浏览器中输入http://localhost:6010/getmovie?id=1就可以看到getMovie接口的返回对象了。
## 小结
这节课我们既学习了怎么“造火箭”,又实践了怎么“拧螺丝”。对于一个合格的算法工程师来说,这两方面缺一不可。
“造火箭”的知识包括工业级推荐服务器的具体功能,以及实现工业级高并发推荐服务的主要机制。其中,推荐服务器的具体功能主要有:模型服务、数据库接口、推荐模块逻辑、补充业务逻辑等等,而工业级高并发推荐服务的主要机制有负载均衡、缓存和服务降级。
“拧螺丝”的技能我们也掌握了不少我们利用Jetty实践并搭建起了我们SparrowRecSys的推荐服务接口。这个过程中我们需要重点关注的是每个注册到Jetty Context的Servlet服务中的主要业务逻辑只要掌握了一个在实际工作中我们就能举一反三了。
老规矩,我今天继续用表格的形式帮你整理了这节课的主要知识点,你可以看看。
<img src="https://static001.geekbang.org/resource/image/9f/df/9f756f358d1806dc9b3463538567d7df.jpeg" alt="">
好了,推荐服务器的相关内容我就先讲到这里,下节课我会继续讲解线上服务的另一个主要的组成部分,存储模块。
## 课后思考
在一个高并发的推荐服务集群中,负载均衡服务器的作用至关重要,如果你是负载均衡服务器的策略设计师的话,你会怎么实现这个“工头”的调度策略,让它能够公平又高效的完成调度任务呢?(比如是按每个节点的能力分配?还是按照请求本身的什么特点来分配?如何知道什么时候应该扩展节点,什么时候应该关闭节点?)
欢迎把你的思考和答案写在留言区,也欢迎你把这节课分享给你的朋友,我们下节课见!

View File

@@ -0,0 +1,151 @@
<audio id="audio" title="10 | 存储模块如何用Redis解决推荐系统特征的存储问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5c/53/5ce2aee6c8770ea6e48d0a19e76b1e53.mp3"></audio>
你好,我是王喆。今天,我们来解决系统特征的存储问题。
在特征工程篇我们说过,在推荐系统这个大饭馆中,特征工程就是负责配料和食材的厨师,那我们上堂课搭建的推荐服务器就是准备做菜的大厨。配料和食材准备好了,做菜的大厨也已经开火热锅了,这时候我们得把食材及时传到大厨那啊。这个传菜的过程就是推荐系统特征的存储和获取过程。
可是我们知道类似Embedding这样的特征是在离线环境下生成的而推荐服务器是在线上环境中运行的那这些**离线的特征数据是如何导入到线上让推荐服务器使用的呢?**
今天我们先以Netflix的推荐系统架构为例来讲一讲存储模块在整个系统中的位置再详细来讲推荐系统存储方案的设计原则最后以Redis为核心搭建起Sparrow Recsys的存储模块。
## 推荐系统存储模块的设计原则
你还记得,我曾在[第1讲的课后题](https://time.geekbang.org/column/article/288917)中贴出过Netflix推荐系统的架构图如图1Netflix采用了非常经典的Offline、Nearline、Online三层推荐系统架构。架构图中最核心的位置就是我在图中用红框标出的部分它们是三个数据库Cassandra、MySQL和EVcache这三个数据库就是Netflix解决特征和模型参数存储问题的钥匙。
<img src="https://static001.geekbang.org/resource/image/bc/ca/bc6d770cb20dfc90cc07168d626fd7ca.jpg" alt="" title="图1 Netflix推荐系统架构中的特征与模型数据库">
你可能会觉得存储推荐特征和模型这件事情一点儿都不难啊。不就是找一个数据库把离线的特征存起来然后再给推荐服务器写几个SQL让它取出来用不就行了吗为什么还要像Netflix这样兴师动众地搞三个数据库呢
想要搞明白这个问题我们就得搞清楚设计推荐系统存储模块的原则。对于推荐服务器来说由于线上的QPS压力巨大每次有推荐请求到来推荐服务器都需要把相关的特征取出。这就要求推荐服务器一定要“快”。
不仅如此对于一个成熟的互联网应用来说它的用户数和物品数一定是巨大的几千万上亿的规模是十分常见的。所以对于存储模块来说这么多用户和物品特征所需的存储量会特别大。这个时候事情就很难办了又要存储量大又要查询快还要面对高QPS的压力。很不幸没有一个独立的数据库能**经济又高效**地单独完成这样复杂的任务。
因此,几乎所有的工业级推荐系统都会做一件事情,就是把特征的存储做成分级存储,把越频繁访问的数据放到越快的数据库甚至缓存中,把海量的全量数据放到便宜但是查询速度较慢的数据库中。
举个不恰当的例子如果你把特征数据放到基于HDFS的HBase中虽然你可以轻松放下所有的特征数据但要让你的推荐服务器直接访问HBase进行特征查询等到查询完成这边用户的请求早就超时中断了而Netflix的三个数据库正好满足了这样分级存储的需求。
<img src="https://static001.geekbang.org/resource/image/03/78/0310b59276fde9eeec5d9cd946fef078.jpeg" alt="" title="图2 分级存储的设计">
比如说Netflix使用的Cassandra它作为流行的NoSQL数据库具备大数据存储的能力但为支持推荐服务器高QPS的需求我们还需要把最常用的特征和模型参数存入EVcache这类内存数据库。而对于更常用的数据我们可以把它们存储在Guava Cache等服务器内部缓存甚至是服务器的内存中。总之对于一个工程师来说我们经常需要做出技术上的权衡达成一个在花销和效果上平衡最优的技术方案。
而对于MySQL来说由于它是一个强一致性的关系型数据库一般存储的是比较关键的要求强一致性的信息比如物品是否可以被推荐这种控制类的信息物品分类的层级关系用户的注册信息等等。这类信息一般是由推荐服务器进行阶段性的拉取或者利用分级缓存进行阶段性的更新避免因为过于频繁的访问压垮MySQL。
总的来说,推荐系统存储模块的设计原则就是“**分级存储,把越频繁访问的数据放到越快的数据库甚至缓存中,把海量的全量数据放到廉价但是查询速度较慢的数据库中**”。
## SparrowRecsys的存储系统方案
那在我们要实现的SparrowRecsys中存储模块的设计原则又是怎么应用的呢
在SparrowRecsys中我们把存储模块的设计问题进行了一些简化避免由于系统设计得过于复杂导致你不易上手。
我们使用基础的文件系统保存全量的离线特征和模型数据用Redis保存线上所需特征和模型数据使用服务器内存缓存频繁访问的特征。
在实现技术方案之前,对于问题的整体分析永远都是重要的。我们需要先确定具体的存储方案,这个方案必须精确到哪级存储对应哪些具体特征和模型数据。
存储的工具已经知道了那特征和模型数据分别是什么呢这里我们直接应用特征工程篇为SparrowRecsys准备好的一些特征就可以了。我把它们的具体含义和数据量级整理成了表格如下
<img src="https://static001.geekbang.org/resource/image/d9/2a/d9cf4b8899ff4442bc7cd87f502a9c2a.jpeg" alt="" title="图3 特征和模型数据">
根据上面的特征数据我们一起做一个初步的分析。首先用户特征的总数比较大它们很难全部载入到服务器内存中所以我们把用户特征载入到Redis之类的内存数据库中是合理的。其次物品特征的总数比较小而且每次用户请求一般只会用到一个用户的特征但为了物品排序推荐服务器需要访问几乎所有候选物品的特征。针对这个特点我们完全可以把所有物品特征阶段性地载入到服务器内存中大大减少Redis的线上压力。
最后我们还要找一个地方去存储特征历史数据、样本数据等体量比较大但不要求实时获取的数据。这个时候分布式文件系统单机环境下以本机文件系统为例往往是最好的选择由于类似HDFS之类的分布式文件系统具有近乎无限的存储空间我们可以把每次处理的全量特征每次训练的Embedding全部保存到分布式文件系统中方便离线评估时使用。
经过上面的分析,我们就得到了具体的存储方案,如下表:
<img src="https://static001.geekbang.org/resource/image/34/63/34958066e8704ea2780d7f8007e18463.jpeg" alt="" title="图4 SparrowRecsys的存储方案">
此外文件系统的存储操作非常简单在SparrowRecsys中就是利用Spark的输出功能实现的我们就不再重点介绍了。而服务器内部的存储操作主要是跟Redis进行交互所以接下来我们重点介绍Redis的特性以及写入和读取方法。
## 你需要知道的Redis基础知识
Redis是当今业界最主流的内存数据库那在使用它之前我们应该清楚Redis的两个主要特点。
**一是所有的数据都以Key-value的形式存储。** 其中Key只能是字符串value可支持的数据结构包括string(字符串)、list(链表)、set(集合)、zset(有序集合)和hash(哈希)。这个特点决定了Redis的使用方式无论是存储还是获取都应该以键值对的形式进行并且根据你的数据特点设计值的数据结构。
**二是所有的数据都存储在内存中,磁盘只在持久化备份或恢复数据时起作用**。这个特点决定了Redis的特性一是QPS峰值可以很高二是数据易丢失所以我们在维护Redis时要充分考虑数据的备份问题或者说不应该把关键的业务数据唯一地放到Redis中。但对于可恢复不关乎关键业务逻辑的推荐特征数据就非常适合利用Redis提供高效的存储和查询服务。
在实际的Sparrow Recsys的Redis部分中我们用到了Redis最基本的操作set、get和keysvalue的数据类型用到了string。
## Sparrow Recsys中的Redis部分的实践流程
Redis的实践流程还是符合我们“把大象装冰箱”的三部曲只不过这三步变成了安装Redis把数据写进去把数据读出来。下面我们来逐一来讲。
**首先是安装Redis。** Redis的安装过程在linux/Unix环境下非常简单你参照[官方网站的步骤](http://www.redis.cn/download.html)依次执行就好。Windows环境下的安装过程稍复杂一些你可以参考[这篇文章](https://www.cnblogs.com/liuqingzheng/p/9831331.html)进行安装。
在启动Redis之后如果没有特殊的设置Redis服务会默认运行在6379端口没有特殊情况保留这个默认的设置就可以了因为我们的Sparrow RecSys也是默认从6379端口存储和读取Redis数据的。
**然后是运行离线程序通过jedis客户端写入Redis。** 在Redis运行起来之后我们就可以在离线Spark环境下把特征数据写入Redis。这里我们以[第8讲([https://time.geekbang.org/column/article/296932](https://time.geekbang.org/column/article/296932))中生成的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 &lt;- model.getVectors.keys) {
//key的形式为前缀+movieId例如i2vEmb:361
//value的形式是由Embedding向量生成的字符串例如 &quot;0.1693846 0.2964318 -0.13044095 0.37574086 0.55175656 0.03217995 1.327348 -0.81346786 0.45146862 0.49406642&quot;
redisClient.set(redisKeyPrefix + &quot;:&quot; + movieId, model.getVectors(movieId).mkString(&quot; &quot;), params)
}
//关闭客户端连接
redisClient.close()
}
```
**最后是在推荐服务器中把Redis数据读取出来。**
在服务器端根据刚才梳理出的存储方案我们希望服务器能够把所有物品Embedding阶段性地全部缓存在服务器内部用户Embedding则进行实时查询。这里我把缓存物品Embedding的代码放在了下面。
你可以看到它的实现的过程也并不复杂就是先用keys操作把所有物品Embedding前缀的键找出然后依次将Embedding载入内存。
```
//创建redis client
Jedis redisClient = new Jedis(REDIS_END_POINT, REDIS_PORT);
//查询出所有以embKey为前缀的数据
Set&lt;String&gt; movieEmbKeys = redisClient.keys(embKey + &quot;*&quot;);
int validEmbCount = 0;
//遍历查出的key
for (String movieEmbKey : movieEmbKeys){
String movieId = movieEmbKey.split(&quot;:&quot;)[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等等它们都是业界非常流行的存储特征的工具你有兴趣的话也可以在课后查阅相关资料进行进一步的学习。在掌握了我们特征存储的基本原则之后你也可以在业余时间尝试思考一下每个数据库的不同和它们最合适的应用场景。
## 小结
今天我们学习了推荐系统存储模块的设计原则和具体的解决方案并且利用Sparrow Recsys进行了实战。
在设计推荐系统存储方案时我们一般要遵循“分级存储”的原则在开销和性能之间取得权衡。在Sparrow Recsys的实战中我们安装并操作了内存数据库Redis你要记住Redis的特点“Key-value形式存储”和“纯内存数据库”。在具体的特征存取过程中我们应该熟悉利用jedis执行SETGET等Redis常用操作的方法。
最后,我也把重要的知识点总结在了下面,你可以再回顾一下。
<img src="https://static001.geekbang.org/resource/image/5f/08/5f76090e7742593928eaf118d72d2b08.jpeg" alt="">
对于搭建一套完整的推荐服务来说我们已经迈过了两大难关分别是用Jetty Server搭建推荐服务器问题以及用Redis解决特征存储的问题。下节课我们会一起来挑战线上服务召回层的设计。
## 课后思考
你觉得课程中存储Embedding的方式还有优化的空间吗除了string我们是不是还可以用其他Redis value的数据结构存储Embedding数据那从效率的角度考虑使用string和使用其他数据结构的优缺点有哪些为什么
欢迎把你的思考和答案写在留言区,也欢迎你把这节课分享给你的朋友,我们下节课见!

View File

@@ -0,0 +1,195 @@
<audio id="audio" title="11 | 召回层:如何快速又准确地筛选掉不相关物品?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e5/e5/e5b2a2a0da3c6cf28fec224d0e9677e5.mp3"></audio>
你好,我是王喆。今天,我们来一起学习推荐系统中非常重要的一个模块,召回层。
为了弄清楚召回层是什么,我们先试着解决一下这个问题:**如果你是一名快手的推荐工程师你的任务是从500万个候选短视频中为一名用户推荐10个他最感兴趣的。你会怎么做**
我想最直接最暴力的做法就是对这500万个短视频挨个打分、排序取出得分最高的10个推荐给用户。如果这个打分的算法非常靠谱的话我们肯定能够选出用户最感兴趣的Top 10视频。但这个过程会涉及一个非常棘手的工程问题如果利用比较复杂的推荐模型特别是深度学习推荐模型对500万个短视频打分这个过程是非常消耗计算资源的。
而且你要知道,这还只是计算了一个用户的推荐结果,在工业级的线上服务中,每秒可是有几十万甚至上百万的用户同时请求服务器,逐个候选视频打分产生的计算量,是任何集群都承受不了的。
那**在推荐物品候选集规模非常大的时候,我们该如何快速又准确地筛选掉不相关物品,从而节约排序时所消耗的计算资源呢?**这其实就是推荐系统召回层要解决的问题。今天,我就从三个召回层技术方案入手,带你一起来解决这个问题。
## 召回层和排序层的功能特点
在前面的课程中我提到过学习推荐系统的一个主要原则,那就是“深入细节,不忘整体”。对于召回层,我们也应该清楚它在推荐系统架构中的位置。
从技术架构的角度来说,“召回层”处于推荐系统的**线上服务模块**之中,推荐服务器从数据库或内存中拿到所有候选物品集合后,会依次经过召回层、排序层、再排序层(也被称为补充算法层),才能够产生用户最终看到的推荐列表。既然线上服务需要这么多“层”才能产生最终的结果,不同层之间的功能特点有什么区别呢?
<img src="https://static001.geekbang.org/resource/image/b1/6b/b1fd054eb2bbe0ec1237fc316byye66b.jpeg" alt="" title="图1 推荐系统的召回和排序阶段及其特点">
其实从这节课开头的问题出发,你应该已经对召回层和排序层的功能特点有了初步的认识,召回层就是要**快速**、准确地过滤出相关物品,缩小候选集,排序层则要以提升推荐效果为目标,作出精准的推荐列表排序。
再详细一点说,我们可以从候选集规模、模型复杂程度、特征数量、处理速度、排序精度等几个角度来对比召回层和排序层的特点:
<img src="https://static001.geekbang.org/resource/image/55/7e/5535a3d83534byy54ab201e865ec4a7e.jpeg" alt="" title="图2 召回层和排序层的特点">
需要注意的是,在我们设计召回层时,计算速度和召回率其实是两个矛盾的指标。怎么理解呢?比如说,为了提高计算速度,我们需要使召回策略尽量简单,而为了提高召回率或者说召回精度,让召回策略尽量把用户感兴趣的物品囊括在内,这又要求召回策略不能过于简单,否则召回物品就无法满足排序模型的要求。
推荐工程师们就是在这样的矛盾中逐渐做出新的尝试推动着召回层的设计方案不断向前发展。下面我们就详细学习一下三个主要的召回方法以及它们基于SparrowRecSys的代码实现。
## 如何理解“单策略召回”方法?
你会发现,今天我多次提到一个关键字,快。那怎么才能让召回层“快”起来呢?我们知道,排序层慢的原因是模型复杂,算法计算量大,那我们能不能反其道而行之,用一些简单直观的策略来实现召回层呢?当然是可以的,这就是所谓的**单策略召回**。
**单策略召回指的是,通过制定一条规则或者利用一个简单模型来快速地召回可能的相关物品。** 这里的规则其实就是用户可能感兴趣的物品的特点我们拿SparrowRecSys里面的电影推荐为例。在推荐电影的时候我们首先要想到用户可能会喜欢什么电影。按照经验来说很有可能是这三类分别是大众口碑好的、近期非常火热的以及跟我之前喜欢的电影风格类似的。
基于其中任何一条我们都可以快速实现一个单策略召回层。比如在SparrowRecSys中我就制定了这样一条召回策略如果用户对电影A的评分较高比如超过4分那么我们就将与A风格相同并且平均评分在前50的电影召回放入排序候选集中。
基于这条规则,我实现了如下的召回层:
```
//详见SimilarMovieFlow class
public static List&lt;Movie&gt; candidateGenerator(Movie movie){
ArrayList&lt;Movie&gt; candidates = new ArrayList&lt;&gt;();
//使用HashMap去重
HashMap&lt;Integer, Movie&gt; candidateMap = new HashMap&lt;&gt;();
//电影movie包含多个风格标签
for (String genre : movie.getGenres()){
//召回策略的实现
List&lt;Movie&gt; oneCandidates = DataManager.getInstance().getMoviesByGenre(genre, 100, &quot;rating&quot;);
for (Movie candidate : oneCandidates){
candidateMap.put(candidate.getMovieId(), candidate);
}
}
//去掉movie本身
if (candidateMap.containsKey(movie.getMovieId())){
candidateMap.remove(movie.getMovieId());
}
//最终的候选集
return new ArrayList&lt;&gt;(candidateMap.values());
}
```
单策略召回是非常简单直观的,正因为简单,所以它的计算速度一定是非常快的。但我想你应该也发现了其中的问题,就是它有很强的局限性。因为大多数时候用户的兴趣是非常多元的,他们不仅喜欢自己感兴趣的,也喜欢热门的,当然很多时候也喜欢新上映的。这时候,单一策略就难以满足用户的潜在需求了,那有没有更全面的召回策略呢?
## 如何理解“多路召回”方法
为了让召回的结果更加全面,多路召回方法应运而生了。
**所谓“多路召回策略”,就是指采用不同的策略、特征或简单模型,分别召回一部分候选集,然后把候选集混合在一起供后续排序模型使用的策略。**
其中,各简单策略保证候选集的快速召回,从不同角度设计的策略又能保证召回率接近理想的状态,不至于损害排序效果。所以,多路召回策略是在计算速度和召回率之间进行权衡的结果。
这里我们还是以电影推荐为例来做进一步的解释。下面是我给出的电影推荐中常用的多路召回策略包括热门电影、风格类型、高分评价、最新上映以及朋友喜欢等等。除此之外我们也可以把一些推断速度比较快的简单模型比如逻辑回归协同过滤等生成的推荐结果放入多路召回层中形成综合性更好的候选集。具体的操作过程就是我们分别执行这些策略让每个策略选取Top K个物品最后混合多个Top K物品就形成了最终的多路召回候选集。整个过程就如下所示
<img src="https://static001.geekbang.org/resource/image/c6/e6/c6cdccbb76a85f9d1bbda5c0e030dee6.jpeg" alt="" title="图3 常见的多路召回策略">
在SparrowRecsys中我们就实现了由风格类型、高分评价、最新上映这三路召回策略组成的多路召回方法具体代码如下
```
public static List&lt;Movie&gt; multipleRetrievalCandidates(List&lt;Movie&gt; userHistory){
HashSet&lt;String&gt; genres = new HashSet&lt;&gt;();
//根据用户看过的电影,统计用户喜欢的电影风格
for (Movie movie : userHistory){
genres.addAll(movie.getGenres());
}
//根据用户喜欢的风格召回电影候选集
HashMap&lt;Integer, Movie&gt; candidateMap = new HashMap&lt;&gt;();
for (String genre : genres){
List&lt;Movie&gt; oneCandidates = DataManager.getInstance().getMoviesByGenre(genre, 20, &quot;rating&quot;);
for (Movie candidate : oneCandidates){
candidateMap.put(candidate.getMovieId(), candidate);
}
}
//召回所有电影中排名最高的100部电影
List&lt;Movie&gt; highRatingCandidates = DataManager.getInstance().getMovies(100, &quot;rating&quot;);
for (Movie candidate : highRatingCandidates){
candidateMap.put(candidate.getMovieId(), candidate);
}
//召回最新上映的100部电影
List&lt;Movie&gt; latestCandidates = DataManager.getInstance().getMovies(100, &quot;releaseYear&quot;);
for (Movie candidate : latestCandidates){
candidateMap.put(candidate.getMovieId(), candidate);
}
//去除用户已经观看过的电影
for (Movie movie : userHistory){
candidateMap.remove(movie.getMovieId());
}
//形成最终的候选集
return new ArrayList&lt;&gt;(candidateMap.values());
}
```
在实现的过程中,为了进一步优化召回效率,我们还可以通过多线程并行、建立标签/特征索引、建立常用召回集缓存等方法来进一步完善它。
不过多路召回策略虽然能够比较全面地照顾到不同的召回方法但也存在一些缺点。比如在确定每一路的召回物品数量时往往需要大量的人工参与和调整具体的数值需要经过大量线上AB测试来决定。此外因为策略之间的信息和数据是割裂的所以我们很难综合考虑不同策略对一个物品的影响。
那么,是否存在一个综合性强且计算速度也能满足需求的召回方法呢?
## 基于Embedding的召回方法
在[第5讲](https://time.geekbang.org/column/article/295300)和[第6讲](https://time.geekbang.org/column/article/295939)中我们已经介绍了多种离线生成物品Embedding的方案。事实上利用物品和用户Embedding相似性来构建召回层是深度学习推荐系统中非常经典的技术方案。我们可以把它的优势总结为三方面。
一方面多路召回中使用的“兴趣标签”“热门度”“流行趋势”“物品属性”等信息都可以作为Embedding方法中的附加信息Side Information融合进最终的Embedding向量中 。因此在利用Embedding召回的过程中我们就相当于考虑到了多路召回的多种策略。
另一方面Embedding召回的评分具有连续性。我们知道多路召回中不同召回策略产生的相似度、热度等分值不具备可比性所以我们无法据此来决定每个召回策略放回候选集的大小。但是Embedding召回却可以把Embedding间的相似度作为唯一的判断标准因此它可以随意限定召回的候选集大小。
最后在线上服务的过程中Embedding相似性的计算也相对简单和直接。通过简单的点积或余弦相似度的运算就能够得到相似度得分便于线上的快速召回。
在SparrowRecsys中我们也实现了基于Embedding的召回方法。我具体代码放在下面你可以参考一下。
```
public static List&lt;Movie&gt; retrievalCandidatesByEmbedding(User user){
if (null == user){
return null;
}
//获取用户embedding向量
double[] userEmbedding = DataManager.getInstance().getUserEmbedding(user.getUserId(), &quot;item2vec&quot;);
if (null == userEmbedding){
return null;
}
//获取所有影片候选集(这里取评分排名前10000的影片作为全部候选集)
List&lt;Movie&gt; allCandidates = DataManager.getInstance().getMovies(10000, &quot;rating&quot;);
HashMap&lt;Movie,Double&gt; movieScoreMap = new HashMap&lt;&gt;();
//逐一获取电影embedding并计算与用户embedding的相似度
for (Movie candidate : allCandidates){
double[] itemEmbedding = DataManager.getInstance().getItemEmbedding(candidate.getMovieId(), &quot;item2vec&quot;);
double similarity = calculateEmbeddingSimilarity(userEmbedding, itemEmbedding);
movieScoreMap.put(candidate, similarity);
}
List&lt;Map.Entry&lt;Movie,Double&gt;&gt; movieScoreList = new ArrayList&lt;&gt;(movieScoreMap.entrySet());
//按照用户-电影embedding相似度进行候选电影集排序
movieScoreList.sort(Map.Entry.comparingByValue());
//生成并返回最终的候选集
List&lt;Movie&gt; candidates = new ArrayList&lt;&gt;();
for (Map.Entry&lt;Movie,Double&gt; movieScoreEntry : movieScoreList){
candidates.add(movieScoreEntry.getKey());
}
return candidates.subList(0, Math.min(candidates.size(), size));
}
```
这里我再带你简单梳理一下整体的实现思路。总的来说我们通过三步生成了最终的候选集。第一步我们获取用户的Embedding。第二步我们获取所有物品的候选集并且逐一获取物品的Embedding计算物品Embedding和用户Embedding的相似度。第三步我们根据相似度排序返回规定大小的候选集。
在这三步之中最主要的时间开销在第二步虽然它的时间复杂度是线性的但当物品集过大时比如达到了百万以上的规模线性的运算也可能造成很大的时间开销。那有没有什么方法能进一步缩小Embedding召回层的运算时间呢这个问题我们留到下节课来讨论。
## 小结
今天我们一起讨论了推荐系统中召回层的功能特点和实现方法。并且重点讲解了单策略召回、多路召回以及深度学习推荐系统中常用的基于Embedding的召回。
为了方便你对比它们之间的技术特点,我总结了一张表格放在了下面,你可以看一看。
<img src="https://static001.geekbang.org/resource/image/2f/80/2fc1eyyefd964f7b65715de6f896c480.jpeg" alt="">
总的来说,关于召回层的重要内容,我总结成了**一个特点,三个方案**。
特点就是召回层的功能特点召回层要快速准确地过滤出相关物品缩小候选集。三个方案指的是实现召回层的三个技术方案简单快速的单策略召回、业界主流的多路召回、深度学习推荐系统中最常用的Embedding召回。
这三种方法基本囊括了现在业界推荐系统的主流召回方法,希望通过这节课的学习,你能掌握这一关键模块的实现方法。
相信你也一定发现了,召回层技术的发展是循序渐进的,因此我希望你不仅能够学会应用它们,更能够站在前人的技术基础上,进一步推进它的发展,这也是工程师这份职业最大的魅力。
## 课后思考
1. 你能根据我今天讲的内容在SparrowRecsys中实现一个多线程版本的多路召回策略吗
1. 你觉得对于Embedding召回来说我们怎么做才能提升计算Embedding相似度的速度
你理解的召回层也是这样吗?欢迎把你的思考和答案写在留言区。如果有收获,我也希望你能把这节课分享给你的朋友们。

View File

@@ -0,0 +1,170 @@
<audio id="audio" title="12 | 局部敏感哈希如何在常数时间内搜索Embedding最近邻" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/66/c8/66de9a763d1f86cf617a0006f01371c8.mp3"></audio>
你好,我是王喆。
在深度学习推荐系统中我们经常采用Embedding召回这一准确又便捷的方法。但是在面对百万甚至更高量级的候选集时线性地逐一计算Embedding间的相似度往往会造成极大的服务延迟。
这个时候,我们要解决的问题就是,**如何快速找到与一个Embedding最相似的Embedding**这直接决定了召回层的执行速度,进而会影响推荐服务器的响应延迟。
今天我们就一起来学习一下业界解决近似Embedding搜索的主要方法局部敏感哈希。
## 推荐系统中的“快速”Embedding最近邻搜索问题
在深度学习推荐系统中我们经常会使用Embedding方法对物品和用户进行向量化。在训练物品和用户的Embedding向量时如果二者的Embedding在同一个向量空间内如图1我们就可以通过内积、余弦、欧式距离等相似度计算方法来计算它们之间的相似度从而通过用户-物品相似度进行个性化推荐,或者通过物品-物品相似度进行相似物品查找。
<img src="https://static001.geekbang.org/resource/image/7f/54/7f7f9647565848d0d530d27d96927654.jpeg" alt="" title="图1 用户和电影的Embedding向量空间">
假设用户和物品的Embeding都在一个$k$维的Embedding空间中物品总数为$n$,那么遍历计算一个用户和所有物品向量相似度的时间复杂度是多少呢?不难算出是$O(k×n)$。虽然这一复杂度是线性的,但物品总数$n$达到百万甚至千万量级时,线性的时间复杂度也是线上服务不能承受的。
换一个角度思考这个问题由于用户和物品的Embedding同处一个向量空间内因此**召回与用户向量最相似的物品Embedding向量这一问题其实就是在向量空间内搜索最近邻的过程**。如果我们能够找到高维空间快速搜索最近邻点的方法那么相似Embedding的快速搜索问题就迎刃而解了。
## 使用“聚类”还是“索引”来搜索最近邻?
遇到最近邻搜索的问题,我想大部分同学直觉上肯定会想到两种解决方案,**一种是聚类**,我们把相似的点聚类到一起,不就可以快速地找到彼此间的最近邻了吗?**另一种是索引**,比如,我们通过某种数据结构建立基于向量距离的索引,在查找最近邻的时候,通过索引快速缩小范围来降低复杂度。这两种想法可不可行呢?我们一一尝试一下。
对于聚类问题我想最经典的算法当属K-means。它完成聚类的过程主要有以下几步
1. 随机指定k个中心点
1. 每个中心点代表一个类,把所有的点按照距离的远近指定给距离最近的中心点代表的类;
1. 计算每个类包含点的平均值作为新的中心点位置;
1. 确定好新的中心点位置后迭代进入第2步直到中心点位置收敛不再移动。
到这里整个K-means的迭代更新过程就完成了你可以看下图2。
<img src="https://static001.geekbang.org/resource/image/5d/90/5d93557a390be7dabc82ffdd6baebc90.jpeg" alt="" title="图2 三中心点的K-means算法迭代过程">
如果我们能够在离线计算好每个Embedding向量的类别在线上我们只需要在同一个类别内的Embedding向量中搜索就可以了这会大大缩小了Embedding的搜索范围时间复杂度自然就下降了。
但这个过程还是存在着一些边界情况。比如聚类边缘的点的最近邻往往会包括相邻聚类的点如果我们只在类别内搜索就会遗漏这些近似点。此外中心点的数量k也不那么好确定k选得太大离线迭代的过程就会非常慢k选得太小在线搜索的范围还是很大并没有减少太多搜索时间。所以基于聚类的搜索还是有一定局限性的解决上面的问题也会增加过多冗余过程得不偿失。
既然聚类有局限性那索引能不能奏效呢我们这里可以尝试一下经典的向量空间索引方法Kd-treeK-dimension tree。与聚类不同它是为空间中的点/向量建立一个索引。这该怎么理解呢?
举个例子你可以看下图3中的点云我们先用红色的线把点云一分为二再用深蓝色的线把各自片区的点云一分为二以此类推直到每个片区只剩下一个点这就完成了空间索引的构建。如果我们能够把这套索引“搬”到线上就可以利用二叉树的结构快速找到邻接点。比如希望找到点q的m个邻接点我们就可以先搜索它相邻子树下的点如果数量不够我们可以向上回退一个层级搜索它父片区下的其他点直到数量凑够m个为止。
<img src="https://static001.geekbang.org/resource/image/df/3f/dfb2c271d9eaa3a29054d2aea24b5e3f.jpeg" alt="" title="图3 Kd-tree索引">
听上去Kd-tree索引似乎是一个完美的方案但它还是无法完全解决边缘点最近邻的问题。对于点q来说它的邻接片区是右上角的片区但是它的最近邻点却是深蓝色切分线下方的那个点。所以按照Kd-tree的索引方法我们还是会遗漏掉最近邻点它只能保证快速搜索到近似的最近邻点集合。而且Kd-tree索引的结构并不简单离线和在线维护的过程也相对复杂这些都是它的弊端。那有没有更“完美”的解决方法呢
## 局部敏感哈希的基本原理及多桶策略
为了“拯救”我们推荐系统的召回层“局部敏感哈希”Locality Sensitive Hashing,LSH这一方法横空出世它用简洁而高效的方法几乎完美地解决了这一问题。那它是怎么做到的呢
### 1. 局部敏感哈希的基本原理
局部敏感哈希的基本思想是希望让相邻的点落入同一个“桶”,这样在进行最近邻搜索时,我们仅需要在一个桶内,或相邻几个桶内的元素中进行搜索即可。如果保持每个桶中的元素个数在一个常数附近,我们就可以把最近邻搜索的时间复杂度降低到常数级别。
那么,如何构建局部敏感哈希中的“桶”呢?下面,我们以基于欧式距离的最近邻搜索为例,来解释构建局部敏感哈希“桶”的过程。
首先我们要弄清楚一个问题如果将高维空间中的点向低维空间进行映射其欧式相对距离是不是会保持不变呢以图4为例图4中间的彩色点处在二维空间中当我们把二维空间中的点通过不同角度映射到a、b、c这三个一维空间时可以看到原本相近的点在一维空间中都保持着相近的距离。而原本远离的绿色点和红色点在一维空间a中处于接近的位置却在空间b中处于远离的位置。
因此我们可以得出一个定性的结论:**欧式空间中,将高维空间的点映射到低维空间,原本接近的点在低维空间中肯定依然接近,但原本远离的点则有一定概率变成接近的点。**
<img src="https://static001.geekbang.org/resource/image/d9/55/d9476e92e9a6331274e18abc416db955.jpeg" alt="" title="图4 高维空间点向低维空间映射">
利用低维空间可以保留高维空间相近距离关系的性质我们就可以构造局部敏感哈希“桶”。对于Embedding向量来说由于Embedding大量使用内积操作计算相似度因此我们也可以用内积操作来构建局部敏感哈希桶。假设$v$是高维空间中的k维Embedding向量$x$是随机生成的k维映射向量。那我们利用内积操作可以将$v$映射到一维空间,得到数值$h(v)=v·x$。
而且,我们刚刚说了,一维空间也会部分保存高维空间的近似距离信息。因此,我们可以使用哈希函数$h(v)$进行分桶,公式为:$h^{x, b}(v)=\left\lfloor\frac{x \cdot v+b}{w}\right]$ 。其中, ⌊⌋ 是向下取整操作, $w$是分桶宽度,$b$是0到w间的一个均匀分布随机变量避免分桶边界固化。
不过映射操作会损失部分距离信息如果我们仅采用一个哈希函数进行分桶必然存在相近点误判的情况因此我们可以采用m个哈希函数同时进行分桶。如果两个点同时掉进了m个桶那它们是相似点的概率将大大增加。通过分桶找到相邻点的候选集合后我们就可以在有限的候选集合中通过遍历找到目标点真正的K近邻了。
刚才我们讲的哈希策略是基于内积操作来制定的,内积相似度也是我们经常使用的相似度度量方法,事实上距离的定义有很多种,比如“曼哈顿距离”“切比雪夫距离”“汉明距离”等等。针对不同的距离定义,分桶函数的定义也有所不同,但局部敏感哈希通过分桶方式保留部分距离信息,大规模降低近邻点候选集的本质思想是通用的。
### 2. 局部敏感哈希的多桶策略
刚才我们讲到了可以使用多个分桶函数的方式来增加找到相似点的概率。那你可能有疑问,如果有多个分桶函数的话,具体应该如何处理不同桶之间的关系呢?这就涉及局部敏感哈希的多桶策略。
假设有A、B、C、D、E五个点有h<sub>1</sub>和h<sub>2</sub>两个分桶函数。使用h<sub>1</sub>来分桶时A和B掉到了一个桶里C、D、E掉到了一个桶里使用h<sub>2</sub>来分桶时A、C、D掉到了一个桶里B、E在一个桶。那么请问如果我们想找点C的最近邻点应该怎么利用两个分桶结果来计算呢
如果我们用“且”And操作来处理两个分桶结果之间的关系那么结果是这样的找到与点C在h<sub>1</sub>函数下同一个桶的点且在h<sub>2</sub>函数下同一个桶的点作为最近邻候选点。我们可以看到满足条件的点只有一个那就是点D。也就是说点D最有可能是点C的最近邻点。
用“且”操作作为多桶策略可以最大程度地减少候选点数量。但是由于哈希分桶函数不是一个绝对精确的操作点D也只是最有可能的最近邻点不是一定的最近邻点因此“且”操作其实也增大了漏掉最近邻点的概率。
那如果我们采用“或”Or操作作为多桶策略又会是什么情况呢具体操作就是我们找到与点C在h<sub>1</sub>函数下同一个桶的点或在h<sub>2</sub>函数下同一个桶的点。这个时候我们可以看到候选集中会有三个点分别是A、D、E。这样一来虽然我们增大了候选集的规模减少了漏掉最近邻点的可能性但增大了后续计算的开销。
当然局部敏感哈希的多桶策略还可以更加复杂比如使用3个分桶函数分桶把同时落入两个桶的点作为最近邻候选点等等。
那么,我们到底应该选择“且”操作还是“或”操作,以及到底该选择使用几个分桶函数,每个分桶函数分几个桶呢?这些都还是工程上的权衡问题。我虽然不能给出具体的最佳数值,但可以给你一些取值的建议:
1. 点数越多,我们越应该增加每个分桶函数中桶的个数;相反,点数越少,我们越应该减少桶的个数;
1. Embedding向量的维度越大我们越应该增加哈希函数的数量尽量采用且的方式作为多桶策略相反Embedding向量维度越小我们越应该减少哈希函数的数量多采用或的方式作为分桶策略。
最后,我们再回头来解决课程开头提出的问题,局部敏感哈希能在常数时间得到最近邻的结果吗?答案是可以的,如果我们能够精确地控制每个桶内的点的规模是$C$假设每个Embedding的维度是$N$,那么找到最近邻点的时间开销将永远在$O(C·N)$量级。采用多桶策略之后,假设分桶函数数量是$K$,那么时间开销也在$O(K·C·N)$量级,这仍然是一个常数。
## 局部敏感哈希实践
现在我们已经知道了局部敏感哈希的基本原理和多桶策略接下来我们一起进入实践环节利用Sparrow Recsys训练好的物品Embedding来实现局部敏感哈希的快速搜索吧。为了保证跟Embedding部分的平台统一这一次我们继续使用Spark MLlib完成LSH的实现。
在将电影Embedding数据转换成dense Vector的形式之后我们使用Spark MLlib自带的LSH分桶模型BucketedRandomProjectionLSH我们简称LSH模型来进行LSH分桶。其中最关键的部分是设定LSH模型中的BucketLength和NumHashTables这两个参数。其中BucketLength指的就是分桶公式中的分桶宽度wNumHashTables指的是多桶策略中的分桶次数。
清楚了模型中的关键参数执行的过程就跟我们讲过的其他Spark MLlib模型一样了都是先调用fit函数训练模型再调用transform函数完成分桶的过程具体的实现你可以参考下面的代码。
```
def embeddingLSH(spark:SparkSession, movieEmbMap:Map[String, Array[Float]]): Unit ={
//将电影embedding数据转换成dense Vector的形式便于之后处理
val movieEmbSeq = movieEmbMap.toSeq.map(item =&gt; (item._1, Vectors.dense(item._2.map(f =&gt; f.toDouble))))
val movieEmbDF = spark.createDataFrame(movieEmbSeq).toDF(&quot;movieId&quot;, &quot;emb&quot;)
//利用Spark MLlib创建LSH分桶模型
val bucketProjectionLSH = new BucketedRandomProjectionLSH()
.setBucketLength(0.1)
.setNumHashTables(3)
.setInputCol(&quot;emb&quot;)
.setOutputCol(&quot;bucketId&quot;)
//训练LSH分桶模型
val bucketModel = bucketProjectionLSH.fit(movieEmbDF)
//进行分桶
val embBucketResult = bucketModel.transform(movieEmbDF)
//打印分桶结果
println(&quot;movieId, emb, bucketId schema:&quot;)
embBucketResult.printSchema()
println(&quot;movieId, emb, bucketId data result:&quot;)
embBucketResult.show(10, truncate = false)
//尝试对一个示例Embedding查找最近邻
println(&quot;Approximately searching for 5 nearest neighbors of the sample embedding:&quot;)
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](https://github.com/facebookresearch/faiss),这是一个在业界广泛应用的开源解决方案。
## 小结
本节课我们一起解决了“Embedding最近邻搜索”问题。
事实上对于推荐系统来说我们可以把召回最相似物品Embedding的问题看成是在高维的向量空间内搜索最近邻点的过程。遇到最近邻问题我们一般会采用聚类和索引这两种方法。但是聚类和索引都无法完全解决边缘点最近邻的问题并且对于聚类来说中心点的数量k也并不好确定而对于Kd-tree索引来说Kd-tree索引的结构并不简单离线和在线维护的过程也相对复杂。
因此,解决最近邻问题最“完美”的办法就是使用局部敏感哈希,在每个桶内点的数量接近时,它能够把最近邻查找的时间控制在常数级别。为了进一步提高最近邻搜索的效率或召回率,我们还可以采用多桶策略,首先是基于“且”操作的多桶策略能够进一步减少候选集规模,增加计算效率,其次是基于“或”操作的多桶策略则能够提高召回率,减少漏掉最近邻点的可能性。
最后,我在下面列出了各种方法的优缺点,希望能帮助你做一个快速的复盘。
<img src="https://static001.geekbang.org/resource/image/40/b1/40yy632948cdd9090fe34d3957307eb1.jpeg" alt="">
## 课后思考
如果让你在推荐服务器内部的召回层实现最近邻搜索过程,你会怎样存储和使用我们在离线产生的分桶数据,以及怎样设计线上的搜索过程呢?
欢迎你在留言区写出你的答案更欢迎你把这一过程的实现提交Pull Request到Sparrow Resys项目如果能够被采纳你将成为这一开源项目的贡献者之一。我们下节课再见

View File

@@ -0,0 +1,169 @@
<audio id="audio" title="13 | 模型服务:怎样把你的离线模型部署到线上?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/74/6c/7458ddfd30943baa11d09f10a6ba7b6c.mp3"></audio>
你好我是王喆。今天我们来讨论“模型服务”Model Serving
在实验室的环境下我们经常使用Spark MLlib、TensorFlow、PyTorch这些流行的机器学习库来训练模型因为不用直接服务用户所以往往得到一些离线的训练结果就觉得大功告成了。但在业界的生产环境中模型需要在线上运行实时地根据用户请求生成模型的预估值。这个把模型部署在线上环境并实时进行模型推断Inference的过程就是模型服务。
模型服务对于推荐系统来说是至关重要的线上服务,缺少了它,离线的模型只能在离线环境里面“干着急”,不能发挥功能。但是,模型服务的方法可谓是五花八门,各个公司为了部署自己的模型也是各显神通。那么,业界主流的模型服务方法都有哪些,我们又该如何选择呢?
今天我就带你学习主流的模型服务方法并通过TensorFlow Serving把你的模型部署到线上。
## 业界的主流模型服务方法
由于各个公司技术栈的特殊性采用不同的机器学习平台模型服务的方法会截然不同不仅如此使用不同的模型结构和模型存储方式也会让模型服务的方法产生区别。总的来说那业界主流的模型服务方法有4种分别是预存推荐结果或Embedding结果、预训练Embedding+轻量级线上模型、PMML模型以及TensorFlow Serving。接下来我们就详细讲讲这些方法的实现原理通过对比它们的优缺点相信你会找到最合适自己业务场景的方法。
### 预存推荐结果或Embedding结果
对于推荐系统线上服务来说最简单直接的模型服务方法就是在离线环境下生成对每个用户的推荐结果然后将结果预存到以Redis为代表的线上数据库中。这样我们在线上环境直接取出预存数据推荐给用户即可。
这个方法的优缺点都非常明显,我把它们总结在了下图中,你可以看看。
<img src="https://static001.geekbang.org/resource/image/f7/78/f71c27199778404d97c7f228635ea278.jpeg" alt="" title="图1 预存推荐结果优缺点对比">
由于这些优缺点的存在,这种直接存储推荐结果的方式往往只适用于用户规模较小,或者一些冷启动、热门榜单等特殊的应用场景中。
那如果在用户规模比较大的场景下我们该怎么减少模型存储所需的空间呢我们其实可以通过存储Embedding的方式来替代直接存储推荐结果。具体来说就是我们先离线训练好Embedding然后在线上通过相似度运算得到最终的推荐结果。
在前面的课程中我们通过Item2vec、Graph Embedding等方法生成物品Embedding再存入Redis供线上使用的过程这就是预存Embedding的模型服务方法的典型应用。
由于线上推断过程非常简单快速因此预存Embedding的方法是业界经常采用的模型服务手段。但它的局限性同样存在由于完全基于线下计算出Embedding这样的方式无法支持线上场景特征的引入并且无法进行复杂模型结构的线上推断表达能力受限。因此对于复杂模型我们还需要从模型实时线上推断的角度入手来改进模型服务的方法。
### 预训练Embedding+轻量级线上模型
事实上直接预存Embedding的方法让模型表达能力受限这个问题的产生主要是因为我们仅仅采用了“相似度计算”这样非常简单的方式去得到最终的推荐分数。既然如此那我们能不能在线上实现一个比较复杂的操作甚至是用神经网络来生成最终的预估值呢当然是可行的这就是业界很多公司采用的“预训练Embedding+轻量级线上模型”的模型服务方式。
详细一点来说,这样的服务方式指的是“**用复杂深度学习网络离线训练生成Embedding存入内存数据库再在线上实现逻辑回归或浅层神经网络等轻量级模型来拟合优化目标**”。
口说无凭接下来我们就来看一个业界实际的例子。我们先来看看下面这张模型结构图这是阿里的推荐模型MIMNMulti-channel user Interest Memory Network多通道用户兴趣记忆网络的结构。神经网络才是真正在线上服务的部分。
仔细看这张图你会注意到,左边粉色的部分是复杂模型部分,右边灰色的部分是简单模型部分。看这张图的时候,其实你不需要纠结于复杂模型的结构细节,你只要知道左边的部分不管多复杂,它们其实是在线下训练生成的,而右边的部分是一个经典的多层神经网络,它才是真正在线上服务的部分。
<img src="https://static001.geekbang.org/resource/image/1e/53/1e0c2a6c404786b709c5177f7d337553.jpg" alt="" title="图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在整个机器学习模型训练及上线流程中扮演的角色。
<img src="https://static001.geekbang.org/resource/image/83/8b/835f47b8c7eac3e18711c8c6e22dbd8b.jpeg" alt="" title="图3 Spark模型利用PMML的上线过程">
图3中的例子使用了JPMML作为序列化和解析PMML文件的libraryJPMML项目分为Spark和Java Server两部分。Spark部分的library完成Spark MLlib模型的序列化生成PMML文件并且把它保存到线上服务器能够触达的数据库或文件系统中而Java Server部分则完成PMML模型的解析生成预估模型完成了与业务逻辑的整合。
JPMML在Java Server部分只进行推断不考虑模型训练、分布式部署等一系列问题因此library比较轻能够高效地完成推断过程。与JPMML相似的开源项目还有MLeap同样采用了PMML作为模型转换和上线的媒介。
事实上JPMML和MLeap也具备Scikit-learn、TensorFlow等简单模型的转换和上线能力。我把[JPMML](https://github.com/jpmml)和[MLeap](https://github.com/combust/mleap)的项目地址放在这里,感兴趣的同学可以进一步学习和实践。
### TensorFlow Serving
既然PMML已经是End2End训练+End2End部署这种最“完美”的方式了那我们的课程中为什么不使用它进行模型服务呢这是因为对于具有复杂结构的深度学习模型来说PMML语言的表示能力还是比较有限的还不足以支持复杂的深度学习模型结构。由于咱们课程中的推荐模型篇会主要使用TensorFlow来构建深度学习推荐模型这个时候PMML的能力就有点不足了。想要上线TensorFlow模型我们就需要借助TensorFlow的原生模型服务模块也就是TensorFlow Serving的支持。
从整体工作流程来看TensorFlow Serving和PMML类工具的流程一致它们都经历了模型存储、模型载入还原以及提供服务的过程。在具体细节上TensorFlow在离线把模型序列化存储到文件系统TensorFlow Serving把模型文件载入到模型服务器还原模型推断过程对外以HTTP接口或gRPC接口的方式提供模型服务。
再具体到咱们的Sparrow Recsys项目中我们会在离线使用TensorFlow的Keras接口完成模型构建和训练再利用TensorFlow Serving载入模型用Docker作为服务容器然后在Jetty推荐服务器中发出HTTP请求到TensorFlow Serving获得模型推断结果最后推荐服务器利用这一结果完成推荐排序。
<img src="https://static001.geekbang.org/resource/image/88/f4/882b2c61f630084e74427b724f64eef4.jpg" alt="" title="图4 Sparrow Recsys项目模型服务部分的架构">
## 实战搭建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的安装也非常简单我们参考[官网的教程](https://www.docker.com/get-started),像安装一个普通软件一样下载安装就好。
安装完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=&quot;$(pwd)/serving/tensorflow_serving/servables/tensorflow/testdata&quot;
```
最后我们在Docker中启动一个包含TensorFlow Serving的模型服务容器并载入我们刚才下载的测试模型文件half_plus_two
```
# 启动TensorFlow Serving容器在8501端口运行模型服务API
docker run -t --rm -p 8501:8501 \
-v &quot;$TESTDATA/saved_model_half_plus_two_cpu:/models/half_plus_two&quot; \
-e MODEL_NAME=half_plus_two \
tensorflow/serving &amp;
```
在命令执行完成后如果你在Docker的管理界面中看到了TenSorflow Serving容器如下图所示就证明TensorFlow Serving服务被你成功建立起来了。
<img src="https://static001.geekbang.org/resource/image/35/3c/3539eccb2a57573a75902738c148fe3c.jpg" alt="" title="图5 TensorFlow Serving容器的Docker启动管理界面">
### 3. 请求TensorFlow Serving获得预估结果
最后我们再来验证一下是否能够通过HTTP请求从TensorFlow Serving API中获得模型的预估结果。我们可以通过curl命令来发送HTTP POST请求到TensorFlow Serving的地址或者利用Postman等软件来组装POST请求进行验证。
```
# 请求模型服务API
curl -d '{&quot;instances&quot;: [1.0, 2.0, 5.0]}' \
-X POST http://localhost:8501/v1/models/half_plus_two:predict
```
如果你看到了下图这样的返回结果就说明TensorFlow Serving服务已经成功建立起来了。
```
# 返回模型推断结果如下
# Returns =&gt; { &quot;predictions&quot;: [2.5, 3.0, 4.5] }
```
如果对这整个过程还有疑问的话你也可以参考TensorFlow Serving的[官方教程](https://www.tensorflow.org/tfx/serving/docker)。
不过有一点我还想提醒你这里我们只是使用了TensorFlow Serving官方自带的一个测试模型来告诉你怎么准备环境。在推荐模型实战的时候我们还会基于TensorFlow构建多种不同的深度学习模型到时候TensorFlow Serving就会派上关键的用场了。
那对于深度学习推荐系统来说我们只要选择TensorFlow Serving的模型服务方法就万无一失了吗当然不是它也有需要优化的地方。在搭建它的过程会涉及模型更新整个Docker Container集群的维护而且TensorFlow Serving的线上性能也需要大量优化来提高这些工程问题都是我们在实践过程中必须要解决的。但是它的易用性和对复杂模型的支持还是让它成为上线TensorFlow模型的第一选择。
## 小结
业界主流的模型服务方法有4种分别是预存推荐结果或Embeding结果、预训练Embeding+轻量级线上模型、利用PMML转换和部署模型以及TensorFlow Serving。
它们各有优缺点,为了方便你对比,我把它们的优缺点都列在了表格中,你可以看看。
<img src="https://static001.geekbang.org/resource/image/51/52/51f65a9b9e10b0808338388e20217d52.jpeg" alt="">
我们之后的课程会重点使用TensorFlow Serving它是End2End的解决方案使用起来非常方便、高效而且它支持绝大多数TensorFlow的模型结构对于深度学习推荐系统来说是一个非常好的选择。但它只支持TensorFlow模型而且针对线上服务的性能问题需要进行大量的优化这是我们在使用时需要重点注意的。
在实践部分我们一步步搭建起了基于Docker的TensorFlow Serving服务这为我们之后进行深度学习推荐模型的上线打好了基础。整个搭建过程非常简单相信你跟着我的讲解就可以轻松完成。
## 课后思考
我们今天讲了如此多的模型服务方式,你能结合自己的经验,谈一谈你是如何在自己的项目中进行模型服务的吗?除了我们今天说的,你还用过哪些模型服务的方法?
欢迎在留言区分享你的经验,也欢迎你把这节课分享出去,我们下节课见!

View File

@@ -0,0 +1,109 @@
<audio id="audio" title="14 | 融会贯通Sparrow RecSys中的电影相似推荐功能是如何实现的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cc/57/cc197c38ac9e8343faac60d8f9b30457.mp3"></audio>
你好,我是王喆。
课程进行到这里推荐系统架构的大部分知识点包括特征工程、Embedding模型到推荐服务的搭建线上推荐过程的实现我们都已经学习并且实践过了。如果你坚持跟着我一起学下来的话可以说已经是“武功小成”了。
为了帮你巩固所学,今天,我就带你从头到尾地实现一个完整的推荐功能,**相似电影推荐**,来帮助你打通推荐系统的“任督二脉”。
## “清点技能库”,看看我们已有的知识储备有哪些
在开始实现相似电影推荐功能之前,我想先带着你一起清点一下自己的技能库。我喜欢把推荐的过程比喻成做菜的过程,接下来,我就按照做菜的四个关键步骤,带你回顾一下前面学过的重点知识。
**第一步,准备食材。** 准备食材的过程就是我们准备推荐所需特征的过程。在特征工程篇中我们不仅学会了怎么挑选“食材”怎么处理“食材”而且还实践了“备菜”的高级技能Embedding技术。具体来说就是我们能够利用物品序列数据通过Item2vec方法训练出Embedding也能够使用Deep Walk和Node2vec把图结构数据生成Graph Embedding。
总的来说因为Embedding技术的本质就是利用了物品之间的相关性所以Embedding是做好“相似推荐”这盘菜的关键。
**第二步,食材下锅。** 备好了菜在正式开炒之前我们肯定要把食材下锅。在推荐系统中“食材下锅”的过程有两个一是把线上推荐所用的特征存储到数据库中在之前的课程中我们已经实践过使用Redis作为特征数据库的方法另一个是把模型部署到模型服务模块我们也已讲过了预训练EmbeddingEmbedding加轻量级线上模型TensorFlow Serving等多种模型服务方式这节课我们将采用预训练Embedding的方式进行模型服务。
**第三步,做菜技术。** “做菜的技术”说的是推荐服务器线上推荐的整个流程是否合理。那回到推荐系统中就是指,召回层要快速准确,模型排序部分要精确。这些具体的实现都影响着最终的推荐效果。
对于召回层来说我们已经学过单策略召回、多路召回和基于Embedding的召回。对于排序来说我们会主要利用Embedding相似度来排序后续我们还会学习基于多种推荐模型的排序。
**最后是菜品上桌的过程** 也就是把推荐的结果呈现给用户的过程。这节课我会带你一起实现这个过程。提前“剧透”一下在Sparrow Recsys中我们会先利用JavaScript异步请求推荐服务API获取推荐结果再利用JavaScript+HTML把结果展现给用户”。因为这一部分内容不是推荐系统的重点所以我们这里只要做到界面清爽、逻辑清晰就可以了。
相信到这里,各位“大厨”已经准备好了所要用到的技能,下面就让我们一起来实现**Sparrow RecSys中的相似电影推荐功能吧**
## 如何实现相似电影推荐功能?
在正式开始相似电影推荐功能之前我们先来看看我总结的Sparrow Recsys相似电影推荐功能的详细技术架构图。细心的你可能已经发现了这个架构图就是Sparrow Recsys架构的精简版。因为我们还没有学习深度学习推荐模型和模型评估的相关知识所以把重点聚焦在已经学过的知识上就可以了。
<img src="https://static001.geekbang.org/resource/image/f4/4f/f408eeeeb04bccd127a6726b8bf91d4f.jpg" alt="" title="图1 Sparrow Recsys 相似电影推荐功能的技术架构图">
接下来,我就结合这个技术架构图,带你一步步地实现其中的每一个模块。并且,我还会给你讲解一些项目中没有实现的其他业界主流方法,如果你还学有余力,希望你能抓住这个机会,来扩展一下自己的知识面。
### 1. 数据和模型部分
数据和模型部分的实现,其实和我们[第8讲](https://time.geekbang.org/column/article/296932)讲的Embedding的实战思路是一样的我们可以选用Item2vec、Deep Walk等不同的Embedding方法来生成物品Embedding向量。考虑到大数据条件下数据处理与训练的一致性在Sparrow Recsys中我们会采用Spark进行数据处理同时选择Spark MLlib进行Embedding的训练。这部分内容的代码你可以参考项目中的`_com.wzhe.sparrowrecsys.offline.spark.embedding.__Embedding_`对象它定义了所有项目中用到的Embedding方法。
对于一些比较复杂的Embedding方案比如特征种类很多网络结构也更多样化的Embedding模型业界也多采用Spark进行原始数据处理生成训练样本后交由TensorFlow、PyTorch训练的方案。
但是不论训练平台是怎样的Embedding方法的产出都是一致的就是物品ID对应的Embedding向量。那为了方便线上服务使用我们还需要在生成Embedding后把它们存入某个高可用的数据库。Sparrow Recsys选择了最主流的内存数据库Redis作为实现方案这一部分的具体实现你可以参照`com.wzhe.sparrowrecsys.offline.spark.embedding.Embedding`对象中trainItem2vec函数的Redis存储操作。当然业界也会使用Cassandra+缓存RocksDB等不同的存储方案来实现Embedding向量的高效读取但我们现阶段只要学会Redis存储和读取操作就够用了。
到这里Redis成为了连接线下和线上的关键节点那我们的线上服务部分又是怎么利用Redis中的Embedding数据进行相似电影推荐的呢
### 2. 线上服务部分
线上服务部分是直接接收并处理用户推荐请求的部分,从架构图的最左边到最右边,我们可以看到三个主要步骤:候选物品库的建立、召回层的实现、排序层的实现。我们逐个来讲一讲。
首先是候选物品库的建立。Sparrow Recsys中候选物品库的建立采用了非常简单的方式就是直接把MovieLens数据集中的物品数据载入到内存中。但对于业界比较复杂的推荐业务来说候选集的选取往往是有很多条件的 比如物品可不可用有没有过期有没有其他过滤条件等等所以工业级推荐系统往往会通过比较复杂的SQL查询或者API查询来获取候选集。
第二步是召回层的实现。我们在[第11讲](https://time.geekbang.org/column/article/299494)曾经详细学习了召回层的技术这里终于可以学以致用了。因为物品的Embedding向量已经在离线生成所以我们可以自然而然的使用Embedding召回的方法来完成召回层的实现。同时Sparrow Recsys也实现了基于物品metadata元信息的多路召回方法具体的实现你可以参照`com.wzhe.sparrowrecsys.online.recprocess.SimilarMovieProcess`类中的multipleRetrievalCandidates函数和retrievalCandidatesByEmbedding函数。
第三步是排序层的实现。根据Embedding相似度来进行“相似物品推荐”是深度学习推荐系统最主流的解决方案所以在Sparrow Recsys中我们当然也是先根据召回层过滤出候选集再从Redis中取出相应的Embedding向量然后计算目标物品和候选物品之间的相似度最后进行排序就可以了。
这里“相似度”的定义是多样的可以是余弦相似度也可以是内积相似度还可以根据你训练Embedding时定义的不同相似度指标来确定。因为在Word2vec中相似度的定义是内积相似度所以,这里我们也采用内积作为相似度的计算方法。同样具体的实现你可以参照com.wzhe.sparrowrecsys.online.recprocess.SimilarMovieProcess类中的ranker函数。
经历了这三个主要的线上服务步骤Sparrow Recsys就可以向用户返回推荐列表了。所以接下来我们要解决的问题就是怎么把这些结果通过前端页面展示给用户。
### 3. 前端部分
Sparrow Recsys的前端部分采用了最简单的HTML+AJAX请求的方式。AJAX的全称是Asynchronous JavaScript and XML异步JavaScript和XML请求。它指的是不刷新整体页面用JavaScript异步请求服务器端更新页面中部分元素的技术。当前流行的JavaScript前端框架React、Vue等等也大多是基于AJAX来进行数据交互的。
但前端毕竟不是我们课程的重点你知道我在上面提到的基本原理就可以了。如果你已经在本地的6010端口运行起了Sparrow Recsys那直接点击这个链接[http://localhost:6010/movie.html?movieId=589](http://localhost:6010/movie.html?movieId=589) 就可以看到电影《终结者2》的详情页面和相似电影推荐结果了如图2
<img src="https://static001.geekbang.org/resource/image/a3/yy/a36a1ba15f4c464c84797fc87caf85yy.jpg" alt="" title="图2 终结者2的相似电影推荐结果">
## 相似电影推荐的结果和初步分析
到这里我相信你已经串联起来了Sparrow Recsys相似电影推荐的所有实现看到了推荐结果。那么问题来了推荐结果的好坏到底是如何判断的呢关于这个问题我们也会在后面的“模型评估篇”中进行系统性的学习。不过这里我也想先跟你聊聊这个话题让你对它有一个大体认识这对你建立后续的模型评估体系是非常有帮助的。
首先提醒你的是Sparrow Recsys开源项目中自带的MovieLens数据集是经过我采样后的缩小集所以基于这个数据集训练出的模型的准确性和稳定性是比较低的。如果你有兴趣的话可以去[MovieLens官网](https://grouplens.org/datasets/movielens/)选择**MovieLens 20M Dataset**下载并重新训练,相信会得到更准确的推荐结果。
其次,针对相似物品推荐这个推荐场景,我们其实很难找到一个统一的衡量标准。比如,你能说出《功夫熊猫》这部电影是跟《玩具总动员》更相近,还是跟《飞屋环游记》更相近吗?好在,工程师们还是总结出了一些有效的评估方法。这里,我挑出了三个最常用的来给你讲讲。
**方法一人肉测试SpotCheck。** 在一种Embedding结果新鲜出炉的时候你作为创造它们的工程师应该第一时间做一个抽样测试看一看基于Embedding的相似推荐结果是不是符合你自己的常识。比如说我在Embedding训练完之后随便在Sparrow Recsys中翻了翻看到了两个页面一个是儿童电影《Free Willy》《人鱼童话》的相似电影推荐页面图3左另一个是著名动画电影《Toy Story》《玩具总动员》的相似电影推荐页面图3右
<img src="https://static001.geekbang.org/resource/image/43/87/43be7ebfd05yye98f9c432d4bb113987.jpg" alt="" title="图3 随机测试">
直观上来看《Free Willy》的推荐结果就非常不错因为你可以看到相似电影中都是适合儿童看的甚至这些电影都和动物相关。但是《玩具总动员》就不一样了它的相似电影里不仅有动画片还有《真实的谎言》《True Lies》、《阿甘正传》这类明显偏成人的电影。这明显不是一个非常好的推荐结果。
为什么会出现这样的结果呢?我们来做一个推测。事实上,《玩具总动员》本身是一部非常流行的电影,跟它近似的也都是类似《真实的谎言》、《阿甘正传》这类很热门的电影。这就说明了一个问题,热门电影其实很容易跟其他大部分电影产生相似性,因为它们会出现在大多数用户的评分序列中。
针对这个问题其实仅利用基于用户行为序列的Embedding方法是很难解决的。这需要我们引入更多内容型特征进行有针对性的改进比如电影类型、海报风格或者在训练中有意减少热门电影的样本权重增大冷门电影的样本权重等等。总的来说遇到推荐结果不合理的情况我们需要做更多的调查研究发掘这些结果出现的真实原因才能找到改进方向。
**方法二指定Ground truth可以理解为标准答案。** 虽然我们说相似影片的Ground truth因人而异。但如果只是为了进行初步评估我们也可以指定一些比较权威的验证集。比如对于相似影片来说我们可以利用IMDB的more like this的结果去做验证我们的相似电影结果。当然要补充说明的是要注意有些Ground truth数据集的可用范围不能随意在商业用途中使用未经许可的数据集。
**方法三:利用商业指标进行评估。** 既然相似影片比较难以直接衡量,那我们不如换一个角度,来思考一下做相似影片这个功能的目的是什么。对于一个商业网站来说,无非是提高点击率,播放量等等。因此,我们完全可以跃过评估相似度这样一个过程,直接去评估它的终极商业指标。
举个例子我们可以通过上线一个新的相似电影模型让相似电影这个功能模块的点击率提高假设提高了5%,那这就是一个成功的模型改进。至于相似电影到底有没有那么“相似”,我们反而不用那么纠结了。
## 小结
这节课我们使用Embedding方法准备好了食材使用Redis把食材下锅做菜的步骤稍微复杂一点分为建立候选集、实现召回层、实现排序层这3个步骤。最后我们用HTML+Ajax的方式把相似电影推荐这盘菜呈现出来。
既然有做菜的过程当然也有品菜的阶段。针对相似物品推荐这一常见的功能我们可以使用人肉测试、Ground truth和商业指标评估这三种方法对得到的结果进行评估。也希望你能够在实际的业务场景中活学活用用评估结果指导模型的下一步改进。
我希望,通过这节课的总结和实战,能让你融会贯通的厘清我们学过的知识。所以我把你需要掌握的重要知识点,总结在了一张图里,你可以利用它复习巩固。
<img src="https://static001.geekbang.org/resource/image/dc/9f/dcbb6cf20283ee362235255841b00c9f.jpg" alt="">
好了那到这里我们线上服务篇的内容就全部结束了。通过这一篇的学习我相信你已经清楚了推荐系统的全部技术架构以及深度学习核心技术Embedding的运用方法。
但盛宴还未开始,下一篇我们将进入深度推荐模型的学习和实践。我曾经说过,深度推荐模型是深度学习推荐系统这个王冠上的明珠,正是它对推荐模型的革命,让深度学习的浪潮席卷推荐系统领域。希望你再接再厉,让我们一起把这颗明珠摘下吧!
## 课后思考
刚才我说到《玩具总动员》的相似电影推荐结果并不好我认为可能是因为热门电影的头部效应造成的。你认同这一观点吗你觉得还有其他可能的原因吗如果让你去做一些Embedding方法上的改进你还有什么好的想法吗
欢迎把你的成果和优化想法分享到留言区,也欢迎你能把这节课转发出去,让更多人从我们的实践中受益,我们下节课见!

View File

@@ -0,0 +1,109 @@
<audio id="audio" title="答疑 | 线上服务篇留言问题详解" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b9/cb/b9b773d5c6892244568f7b615dee96cb.mp3"></audio>
你好,我是王喆。
今天是专栏的第二次答疑加餐时间,第一次答疑我已经对基础篇和特征工程篇中常见的问题进行了解答,所以这节课我们重点来看看线上服务篇中很有代表性的课后思考题和留言问题,我会对它们进行一些补充回答,希望对你有帮助。
## 关于项目的开源精神
在开始回答问题之前我想先跟你聊一聊我们SparrowRecsys项目的开源精神。在课程一开始我就说过SparrowRecsys这个项目是我们的一个种子项目它肯定不完美但我希望它是一个工业级推荐系统的雏形。在学习的过程中我希望能够跟你一起完善它让它的羽翼逐渐丰满起来。
让我很高兴的是已经有不少同学投身到改进SparrowRecsys的队伍中来比如GitHub ID叫[dxzmpk](https://github.com/dxzmpk)的同学添加了[Node2vec模型的代码](https://github.com/wzhe06/SparrowRecSys/pull/14)还有GitHub ID叫jason-wang1的同学添加了[多路召回多线程版本的代码](https://github.com/wzhe06/SparrowRecSys/pull/13)还有更多的同学修改了项目中的Bug优化了一些实现感谢你们的投入
我是开源精神的坚定拥护者我也相信在我们的共同努力下SparrowRecsys未来能够发展成为在业界有影响力的开源项目。所以在这里我呼吁同学们能够多参与进来多提Pull Request让我们共同成为项目的第一批原作者。
好,下面我们进入问题解答的环节。
## [《03深度学习基础你打牢深度学习知识的地基了吗》](https://time.geekbang.org/column/article/291245)
**思考题1哪些因素影响着深度学习网络的结构深度学习模型是越深越好吗**
这两个问题我们分开来看,先看看影响深度学习网络结构的因素。在业界的应用中,影响深度学习网络结构的因素非常多。不过,我认为可以总结出二类最主要的因素。
第一类:业务场景中用户行为的特点。很多模型结构的实现是为了模拟用户行为的特点,比如注意力机制的引入是用来模拟用户的注意力行为特点,序列模型是用来模拟用户兴趣变迁,特征交叉层是为了让用户和物品的相关特征进行交叉等等。
第二类:数据规模、算力的制约。这一点“一天”同学回答得非常有价值,在实际的业界应用中,数据规模大起来之后,我们往往不能够随意选择复杂模型,而是要在数据规模,平台算力的制约下,尽量选择效果最优的模型结构。
我们再来看第二个问题,深度学习模型是越深越好吗?
这个答案是否定的。深度学习模型变深之后,并不总是能够提高模型效果,有时候深度的增加对模型效果的贡献是微乎其微的。而且模型复杂之后的负面影响也非常多,比如训练时间加长,收敛所需数据和训练轮数增加,模型不一定稳定收敛,模型过拟合的风险增加等等。所以在模型深度的选择上,我们要在尽量保证效果的前提下,选择结构较简单的方案。
借助这道思考题,我希望能帮助你更好地理解深度学习的特点,以及实际应用中的一些经验。
## [《09线上服务如何在线上提供高并发的推荐服务》](https://time.geekbang.org/column/article/299155)
思考题2在一个高并发的推荐服务集群中负载均衡服务器的作用至关重要如果你是负载均衡服务器的策略设计师你会怎么实现这个“工头”的调度策略让它能够公平又高效地完成调度任务呢比如是按每个节点的能力分配还是按照请求本身的什么特点来分配如何知道什么时候应该扩展节点什么时候应该关闭节点
负载均衡的策略其实有多种选择比如“smjccj”同学的回答就很专业他说可以进行源地址哈希或根据服务器计算能力加权随机分配。这是一个很好的答案这里我再补充一下。通常来说常用的负载均衡的策略有三种分别是轮询调度、哈希调度和一致性哈希调度。我们一一来看。
轮询调度就是以轮询的方式依次把请求调度到不同的服务器。在服务器的算力等硬件配置不同的时候,我们还可以为每个服务器设定权重,按权重比例为能力强的服务器分配更多的请求。
而哈希调度指的是通过某个哈希函数把key分配给某个桶这里key可以是请求中的用户ID物品ID等ID型信息桶的总数就是服务器的总数。这样一来我们就可以把某个用户的请求分配给某个服务器处理。这么做的好处是可以让一个key落在固定的服务器节点上有利于节约服务器内部缓存的使用。
哈希方式的缺点在于无法高效处理故障点一旦某个点有故障需要减少桶的数量或者在QPS增大时需要增加服务器整个分配过程就被完全打乱。因此一致性哈希调度就是更好的解决方案简单来说就是使用哈希环来解决计算节点的增加和减少的问题具体的实现我推荐你参考[《一致性哈希算法的理解与实践》](%E4%B8%80%E8%87%B4%E6%80%A7%E5%93%88%E5%B8%8C%E7%AE%97%E6%B3%95%E7%9A%84%E7%90%86%E8%A7%A3%E4%B8%8E%E5%AE%9E%E8%B7%B5/)这篇文章。
留言问题1在一个成熟的工业级推荐系统中每个用户请求的时间、地点、context都不一样缓存策略是怎么工作的才能把这些数据大部分都缓存起来
<img src="https://static001.geekbang.org/resource/image/a8/y7/a896f3ee7258d3b1ec5e6178b9da0yy7.jpg" alt="">
这里,同学们一定要理解缓存的意义。如果请求中的变量每次都不一样,那我们确实就没有必要进行缓存了,因为每次返回的结果都是不同的。但真实情况往往不是这样,我们其实可以在具体的业务场景中挖掘出巨大的优化空间。
比如,电商网站存在着大量没有购买记录的新用户,我们其实可以根据这些新用户有限的特征把他们分成少量的几个类别,对一个类别内的用户展示同样的推荐结果。这样,我们就没必要每次都请求复杂的推荐模型了。
再比如,同一个用户有可能多次请求同一个页面,如果推荐系统对这些操作进行了缓存,就不用对每次重复的请求重复计算推荐结果了,在处理完首次请求之后,面对之后的重复请求,推荐系统直接返回缓存结果就可以了。当然,推荐系统具体能存储多少用户缓存,也取决于硬件配置,也取决于缓存的过期时间,这些都需要我们灵活进行配置。
留言问题2推荐系统中的冷启动策略指的是什么
<img src="https://static001.geekbang.org/resource/image/c1/18/c19dc90720b21f317450aa7c94b66d18.jpeg" alt="">
冷启动是推荐系统一定要考虑的问题。它是指推荐系统在没有可用信息,或者可用信息很少的情形下怎么做推荐的问题,冷启动可以分为用户冷启动和物品冷启动两类。
用户冷启动是指用户没有可用的行为历史情况下的推荐问题。一般来说我们需要清楚在没有推荐历史的情况下还有什么用户特征可以使用比如注册时的信息访问APP时可以获得的地点、时间信息等等根据这些有限的信息我们可以为用户做一个聚类为每类冷启动用户返回合适的推荐列表。当然我们也可以利用可用的冷启动特征来构建一个较简单的冷启动推荐模型去解决冷启动问题。
对于物品冷启动来说主要处理的是新加入系统的物品它们没有跟用户的交互信息。所以针对物品冷启动我们除了用类似用户冷启动的方式解决它以外还可以通过物品分类等信息找到一些相似物品如果这些相似物品已经具有了预训练的Embedding我们也可以采用相似物品Embedding平均的方式来快速确定冷启动物品的Embedding让它们通过Embedding的方式参与推荐过程。
## [《11召回层如何快速又准确地筛选掉不相关物品》](https://time.geekbang.org/column/article/299494)
留言问题3用户的多兴趣标签怎么与物品的标签进行最优匹配当物品的标签有多层时如何利用上一层的标签
<img src="https://static001.geekbang.org/resource/image/d7/26/d74e828bee4ddde80a2110b75e852026.jpeg" alt="">
这个问题最简单的做法就是把用户的兴趣标签和物品对应的标签都转换成Multi-hot向量然后我们就可以计算出用户和物品的相似度了。
除此之外,我们也可以进一步计算每个兴趣标签的[TF-IDF值](https://baike.baidu.com/item/tf-idf/8816134?fr=aladdin)为标签分配权重后再把它们转换成Multi-hot向量这样我们也可以计算出用户和物品的相似度。
如果标签有多层我们也可以把多层标签全部放到Multi-hot向量中再把高层标签的权重适当降低这也是可行的思路之一。
留言问题4在电商领域下如何解决EGES训练非常慢的问题
<img src="https://static001.geekbang.org/resource/image/7b/d8/7b6f322d2d0cb95c730a33e08a9d86d8.jpeg" alt="">
这是一个非常好的业界实践问题。这里我先给同学们解释一下什么是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最近邻》](https://time.geekbang.org/column/article/301739)
留言问题5在用Item2vec等方法生成物品Embedding后用户的Embedding是怎么生成的呢 物品和用户在同一个向量空间,这是怎么保证的呢?
<img src="https://static001.geekbang.org/resource/image/d7/02/d74505c44e579c2aac7f8990b50d8102.jpeg" alt="">
在咱们的项目里用户Embedding的生成方法是很直观的就是对用户评论过的高分电影Embedding取平均值得到的。这相当于说用户Embedding是在物品Embedding向量空间中进行运算得到的那它们肯定是在一个向量空间内我们也就可以使用相似度计算来求取相似度。
因此只要是利用用户历史的Item Embedding生成的用户Embedding都是在一个向量空间内这些生成方式包括average pooling、sum pooling、attention等等。
但是如果用户Embedding和物品Embedding是分别独立生成的或者说是通过一个模型中没有直接关系的两个Embedidng层生成的那么它们就不在一个向量空间内了。注意啦这个时候我们不能直接求用户和物品之间的相似度只能求用户-用户的相似度,和物品-物品的相似度。
留言问题6“在局部敏感哈希的函数中b是0到w间的一个均匀分布随机变量是为了避免分桶边界固化”。这是什么意思呢是说可以通过调整b来形成另外一个个Hash函数
<img src="https://static001.geekbang.org/resource/image/40/db/40559a19901eb73aa9a7871b7cb973db.jpeg" alt="">
首先,我要说这个局部敏感哈希相关的问题非常好,推荐其他同学也关注一下。
说回到这个问题如果我们总是固定分桶的边界很容易让边界两边非常接近的点被分到两个桶里这是我们不想看到的。所以这里我们就可以通过随机调整b的方式来生成多个Hash函数在进行多个Hash函数的分桶之后再采用或的方式对分桶结果进行组合查找最近邻向量就可以一定程度避免这些边界点的问题。
好了这节课的答疑就到这里非常感谢同学们的积极提问和思考。希望在接下来的课程里你也能多多参与进来与我一起完善SparrowRecsys项目的代码共同进步

View File

@@ -0,0 +1,12 @@
你好,我是王喆。
《深度学习推荐系统》这门课程到这里就正式完结了。在发布了结束语之后,我陆续收到了很多同学的留言和反馈,非常感谢你一直以来的认真学习和支持!
为了帮助你检验自己的学习效果,我特别给你准备了一套结课测试题。这套测试题共有 20 道题目包括12道单选题和8道多选题满分 100 分,核心考点都出自前面讲到的所有重要知识,希望可以帮助你进行一场自测。点击下面按钮,就可以开始测试啦。
[<img src="https://static001.geekbang.org/resource/image/28/a4/28d1be62669b4f3cc01c36466bf811a4.png" alt="">](http://time.geekbang.org/quiz/intro?act_id=358&amp;exam_id=970)
除此之外,我还特意准备了一份问卷,想听听你的声音和反馈,以便我后面对课程进行更好地优化。同时,填写问卷还有机会获得礼物或者是课程阅码哦。
[<img src="https://static001.geekbang.org/resource/image/a6/28/a687f6da2e4a405af0b83c82932a9a28.jpg" alt="">](https://jinshuju.net/f/Fstsjz)

View File

@@ -0,0 +1,131 @@
<audio id="audio" title="结束语|深度学习时代需要什么样的推荐工程师?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/28/e1/2811bcc50f0edfd7d42234d9a90090e1.mp3"></audio>
你好,我是王喆。
今天是2021年的第一天也是咱们课程的最后一讲课程开始前我要对你说一句很荣幸能陪你一起度过这几个月的学习时间。
我相信在这门课中只要你认真学习、亲手实践就一定能够搭建起深度学习推荐系统的知识框架并且收获一套亲手实现的、包含工业级推荐系统各个模块的开源项目SparrowRecSys。这是咱们这门课的目标也是你作为推荐工程师对深度学习方向的一次探索。
但要想成为一名优秀的推荐工程师,这一路上,包括我自己,也还在不断地学习和前行着。虽然我不能给你一张魔法船票,让你直接到达技术专家的彼岸,但我希望能通过这门课给你一个坚实的基础和宽广的视野。
在这最后一课,我想结合自己近十年的从业经验,包括国内和硅谷互联网公司的工作经历,跟你谈一谈,**这个时代到底需要什么样的推荐工程师,我们应该继续朝什么方向持续努力**。
## 一名推荐工程师的“自我修养”
在我刚进入推荐系统这个行业的时候,我心中对这个行业的初步印象是,认为这个行业就是一群搞机器学习的研究者在不断地改进模型、训练模型和提高效果,所以,我卯足了劲要成为那个能提出新的模型架构,让全公司都采用我模型的“年轻人”。
当我工作了三年之后,我终于明白,模型的工作固然重要,但它仅仅是推荐系统的一个部分,还有其他很多重要的子系统、子模块可以进行深入研究。当我工作了五年之后,我又意识到,原来能够全盘考虑推荐系统的模型、工程、业务,对它们进行整体优化的工程师才是最优秀的那一拨人。
工作八年后,我开始思考一个优秀推荐工程师的“自我修养”到底是什么,换句话说就是他该具备什么能力。下面我就把我的思考分享给你。
我认为一名推荐工程师的技术能力基本可以拆解成4个维度知识、工具、逻辑、业务。我们一一来看。
“知识”代表了推荐工程师应该具备的推荐系统和机器学习领域的相关知识,比如我在推荐模型篇中就强调过,我们一定要建立起自己的推荐模型演化框架,厘清各个模型之间的关系,从整体上建立起推荐系统的知识体系。
而“工具”指的是我们的工程实践能力掌握推荐系统相关的工程实践工具的使用方法。我在这门课程中讲了很多推荐系统相关的工具、平台包括Spark、TensorFlow、Flink、Redis等等就是希望你能够具备推荐系统知识的落地能力把脑海中的专业领域知识转化成实际的能够支持公司业务的推荐系统。
“逻辑”指的是我们作为一名程序员的基本功包括算法和数据结构基础以及梳理高质量系统框架的能力。像是我们在第一课中总结过的深度学习推荐系统架构图以及在第14课、23课这些实践章节中总结过的各种架构图就是逻辑设计能力的体现。
至于“业务”我想是推荐工程师跟其他后端工程师区别最大的地方了。在业务维度推荐工程师需要把自己当成半个产品经理从业务场景的角度来思考用户到底喜欢什么他们的习惯到底什么样。只有这样我们才能够不断发现改进模型的动机持续地迭代模型。这一点我想你通过32课中阿里模型的演进过程肯定也有所体会。
<img src="https://static001.geekbang.org/resource/image/f8/80/f895cb9a1b686b98bf19b5f958968880.jpg" alt="" title="推荐工程师能力模型">
## 推荐工程师职位的面试要求
除了推荐工程师的能力维度,我还想跟你聊一聊推荐工程师职位的面试要求,因为这也是很多在校同学和刚入行的工程师们非常感兴趣的话题。
虽然,不同的招聘职位和职级对面试者的能力要求有所不同,但各个公司也不外乎是从“知识”、“工具”、“逻辑”、“业务”,这四个维度对面试者进行考察。接下来,我们就依次来看一看面试官都是怎么考察这四项能力的。
### 1. 对知识的考察
面试官最关心的就是你对这个领域的知识储备,以及你对一些技术细节的理解。一般来说,他们会按照由广入深,层层递进地方式来考察你的掌握程度。
比如说在知识广度方面面试官可能会让你说说主流的推荐模型有哪些主流的Embedding方法有哪些然后再根据你的回答挑一到两点继续深入。
假设你有提到你对DIN模型比较熟悉那么接下来的问题可能是
- 注意力机制具体指的是什么?
- DIN中注意力单元的具体结构是什么
- 能否写出注意力单元的形式化定义,并推导它的梯度下降更新过程?
所以,**在面试之前,对于你自己熟悉的领域,一定要深入理解每一个细节,切忌浅尝辄止,临时抱佛脚**。
### 2. 对逻辑的考察
面试官对“逻辑思维能力”的考察方法就是做题,一般来说也会用两种方式。
一种是直接出算法面试题让你写一种排序算法完成二叉树的构建、遍历、反转等等操作或者任意类似的LeetCode面试题。这是所有CS相关职位都会检查的因为一名推荐工程师首先需要是一名合格的、基础牢固的工程师。
第二种方式是让你去设计一个推荐系统的工程架构,可能是根据某个推荐场景,设计一套合适的特征工程和推荐模型等等。这类系统设计题更能检查你严谨、全面的逻辑思考能力。
对于逻辑能力的积累是一个长期的过程不仅要求你在学生阶段打牢自己CS相关的知识基础而且最好能在工作中面临实际问题的时候多思考系统设计的问题即使这部分功能不属于你的工作范畴你也可以多想一想有哪些可行的设计方案不断锻炼自己系统设计的能力。
### 3. 对“工具”使用能力的考察
其实,对工具的考察跟公司职位的实际需求最相关,特别是一些比较高级的职位。
如果这个职位要求更强的大数据处理能力面试官可能会考察你对Spark的运用情况以及对Spark背后Map Reduce过程原理的理解情况如果这个职位希望招一位对TensorFlowXGBoost等建模工具应用熟练的同学面试官就会重点考察你TensorFlow的基本概念以及怎么处理训练过程中遇到的主要问题。
对于这部分,我的建议是,**你在了解所有工具基本使用方法的同时,一定要注意一到两个工具的深入实践和积累,争取成为一两个方向的专家。这样,你才能在一些高级职位的竞争中脱颖而出**。
### 4. 对“业务”理解能力的考察
如果说前三项能力是一名合格推荐工程师的必备条件,那么业务能力则是你在合格基础上变得优秀的宝贵素质。只有对业务有深刻理解,我们才能够从完成项目的阶段进化到发现项目、领导项目的阶段,所以越高的职位,对业务理解能力的考察就越深刻。
比如对于DIN来说最关键的点其实并不是用注意力机制来解决这个问题而是发现用户行为历史和目标商品的相关性。对于Wide&amp;Deep来说最重要的也并不是这个模型结构而是如何设计Deep特征和Wide特征从而发挥出这个模型最大的“功力”对于模型服务的部分最重要的是如何设计它才既能满足业务需求又能实现工程上的要求。
除此之外,推荐系统中遇到的一些常见问题,如冷启动、多目标优化、探索与利用,也是面试常问的业务问题,你必须能说出自己的思考,充分地分析具体问题,提出有针对性的解决方案。
“业务”方面的提高,虽然是一个长期的过程,但我还是要建议你,在平时的工作中,不要总是想着完成一个个工程任务,按部就班的执行上级的命令,而是多思考任务背后的逻辑,多提出自己的见解,这样才能真正的提高自己解决业务问题的能力。
总的来说,“知识”和“逻辑”是一名推荐工程师的基本素质,“工具”和“业务”是影响你能不能获得高端职位的关键。但不管你处于职业生涯的任何阶段,都要注意平时的不断思考和持续学习,从“广度”和“深度”两个方面,不断提升自己的综合能力,这样才能在职业生涯的阶梯上不断前行。
## 什么样的机器学习人才最紧缺?
清楚了能力要求,了解了面试形式,最后我们再展望一下未来,让我们预测一下接下来这几年,业界最需要怎样的机器学习人才。
说是预测其实也不能说是十分准确因为我跟业界的很多非常资深的工程师技术leader探讨的过程中发现大家对于这个问题的看法其实是惊人一致的。下面让我们先来看看业界的技术专家们都是怎么说的。
首先是阿里的资深技术专家朱小强给我的书写序时的总结,他说:
>
技术发展将从依赖深度学习算法单点突破收割技术红利开始转向更为复杂的、系统性的技术体系推进进一步创造技术红利。这里关键性的技术破局点是算法与系统架构的协同设计algo-system co-design
所以朱小强的观点是,单纯依靠深度学习模型改进就能大幅提高效果的时代已经过去了,现在我们只有把算法和工程协同设计,才能取得进一步的突破。
第二位是前Google、Facebook高级工程师现在的新浪微博广告核心技术负责人詹盈他对这个问题的看法是
>
成熟的算法工程师不应仅满足于基本的建模和调参,他们对于业务和产品有较为深刻的理解,并且在实践中能够拥有足够的数据分析能力去指导建模方案的完善,并确确实实地带来客观的业务收益。这一类高端的算法人才是真的稀缺,还远达不到“内卷”的程度。
可以说,詹盈的看法和我对算法工程师能力的总结高度一致:只有综合了算法能力、业务能力、工程能力,你才能成为一名高端的算法工程师。
最后是我的前老板Hulu的全球副总裁诸葛越的话她说
>
算法工程师这个群体普遍都会遇到的是,算法研究能力和工程能力的权衡问题。在其中任何一个方向上成为专家都没有问题,企业都会有合适的位置给到你。但如果你想走到更高的职级,就需要不断拓宽与现有工作相关的技术栈。随着近两年算法岗位的成熟,有一种说法重新被大家认同,那就是“算法工程师首先是一名工程师”,可见对算法工程师工程能力的重视已经是普遍的观点了。
越姐从一个技术管理者的角度表达了自己的观点,结论也和前几位工程师一样:只有算法研究能力和工程能力都突出的人,才会在现在愈加成熟的业界环境下取得更高的职位。
最后,我想用自己说的话再给这个问题做一个总结:**未来3-5年业界最需要的是能够站在机器学习“工程体系”的高度之上综合考虑“业务特点”、“模型结构”、“工程限制”、“问题目标”的算法工程师。在算法工程师的发展之路上切忌“好高骛远”只追求“好看好听”的先进技术的学生思维回到工程师思维上来回到解决问题的本质上来才能够逐渐成为一名优秀的算法工程师。**
## 写在最后
好了,如果你学习到这里,我要告诉你,这是本门课程的最后一段话。这门课程,我们一直在聊技术、学实践,一直没有机会跟你聊一聊内心的话,所以借着最后的机会我想跟你“谈谈心”。
说实话推荐工程师这条职业道路并不是一条容易的道路同行们常说这是一个时刻处在淘汰边缘的职业我们时刻面临着这样那样的挑战。比如说当你在谈一个流行的技术点时它已经进入了过时的倒计时当你coding一天回家还是勉为其难再多看一篇paper的时候也许你也曾和我一样觉得不如就随波逐流算了当你苦心作出一个模型的尝试却没有效果的时候感觉自己的工作几乎是要从零开始。
但我还要说这是一个充满魅力和激情的工作它不是在简单地完成一个机械式的任务而是用在你的观察、你的思考、你的智力以及你的经验去实现一个个目标当你真正突破这些目标的时候我相信你会体会到比一道编程题AC大100倍的成就感毫无疑问它值得你为之付出一个长长的职业生涯。
这几个月里,你可能是在晚上回家打开笔记本继续学习,或者是在上班的路上听我的录音,又或者是在周末通过咱们的课程为自己充电,不管是用哪种方式学习,我都要说一声:“谢谢你的支持和信任”,能陪伴你走过这几个月的学习历程是我的荣幸。
今天的最后没有思考题,但如果你对这门课、这个职业,对未来和前途,有任何的感悟,我都欢迎你写下来,总结给自己看,也与这门课一同走过的同学们共勉。
好了,同学们,虽然我们的课程结束了,但我相信你的职业生涯从这里开始会更加精彩!
最后的最后,我还为你准备了一份毕业调查问卷,题目不多,希望你能花两分钟的时间填一下。一起走过了这些时间,期待听到你对我和这个课程的反馈和建议!
[<img src="https://static001.geekbang.org/resource/image/a6/28/a687f6da2e4a405af0b83c82932a9a28.jpg" alt="">](https://jinshuju.net/f/Fstsjz)