This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

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节对内容有深度思考和提问的同学你们的每个问题都很精彩。在接下来的课程中欢迎你继续畅所欲言把留言区这个工具好好利用起来我们一起进步