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,81 @@
<audio id="audio" title="46 | 缓存系统:如何通过哈希表和队列实现高效访问?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/87/a9/87b57eb1b94ed154444584548a8752a9.mp3"></audio>
你好,我是黄申。
经过前三大模块的学习,我带你纵览了数学在各个计算机编程领域的重要应用。离散数学是基础数据结构和编程算法的基石,而概率统计论和线性代数,是很多信息检索和机器学习算法的核心。
因此,今天开始,我会综合性地运用之前所讲解的一些知识,设计并实现一些更有实用性的核心模块或者原型系统。通过这种基于案例的讲解,我们可以融汇贯通不同的数学知识,并打造更加高效、更加智能的计算机系统。首先,让我们从一个缓存系统入手,开始综合应用篇的学习。
## 什么是缓存系统?
缓存Cache是计算机系统里非常重要的发明之一它在编程领域中有非常非常多的应用。小到电脑的中央处理器CPU、主板、显卡等硬件大到大规模的互联网站点都在广泛使用缓存来提升速度。而在网站的架构设计中一般不会像PC电脑那样采用高速的缓存介质而是采用普通的服务器内存。但是网站架构所使用的内存容量大得多至少是数个吉字节 GB
我们可以把缓存定义为数据交换的缓冲区。它的读取速度远远高于普通存储介质,可以帮助系统更快地运行。当某个应用需要读取数据时,会优先从缓存中查找需要的内容,如果找到了则直接获取,这个效率要比读取普通存储更高。如果缓存中没有发现需要的内容,再到普通存储中寻找。
理解了缓存的概念和重要性之后,我们来看下缓存设计的几个主要考量因素。
第一个因素是**硬件的性能**。缓存的应用场景非常广泛,因此没有绝对的条件来定义何种性能可以达到缓存的资格,我们只要确保以高速读取介质可以充当相对低速的介质的缓冲。
第二个因素是**命中率**。缓存之所以能提升访问速度主要是因为能从高速介质读取这种情况我们称为“命中”Hit。但是高速介质的成本是非常昂贵的而且一般也不支持持久化存储因此放入数据的容量必须受到限制只能是全局信息的一部分。那么一定是有部分数据无法在缓存中读取而必须要到原始的存储中查找这种情况称之为“错过”Missed
我们通常使用能够在缓存中查找到数据的次数($|H|$),除以整体的数据访问次数($|V|$)计算命中率。如果命中率高,系统能够频繁地获取已经在缓存中驻留的数据,速度会明显提升。
接下来的问题就是,如何在缓存容量有限的情况下,尽可能的提升命中率呢?人们开始研究缓存的淘汰算法,通过某种机制将缓存中可能无用的数据剔除,然后向剔除后空余的空间中补充将来可能会访问的数据。
最基本的策略包括最少使用LFULeast Frequently Used策略和最久未用LRULeast Recently Used策略。LFU会记录每个缓存对象被使用的频率并将使用次数最少的对象剔除。LRU会记录每个缓存对象最近使用的时间并将使用时间点最久远的对象给剔除。很显然我们都希望缓存的命中率越高越好。
第三个因素是**更新周期**。虽然缓存处理的效率非常高,但是,被访问的数据不会一成不变,对于变化速度很快的数据,我们需要将变动主动更新到缓存中,或者让原有内容失效,否则用户将读取到过时的内容。在无法及时更新数据的情况下,高命中率反而变成了坏事,轻则影响用户交互的体验,重则会导致应用逻辑的错误。
为了方便你的理解,我使用下面这张图,来展现这几个主要因素之间的关系,以及缓存系统的工作流程。
<img src="https://static001.geekbang.org/resource/image/d9/5b/d92decec85a79493f9ef1f87f3add05b.png" alt="">
## 如何设计一个缓存系统?
了解这些基本概念之后我们就可以开始设计自己的缓存系统了。今天我重点讲解如何使用哈希表和队列来设计一个基于最久未用LRU策略的缓存。
从缓存系统的工作流程可以看出首先我们需要确认某个被请求的数据是不是存在于缓存系统中。对于这个功能哈希表是非常适合的。第2讲我讲过哈希的概念我们可以通过哈希值计算快速定位加快查找的速度。不论哈希表中有多少数据读取、插入和删除操作只需要耗费接近常量的时间也就是O (1)的时间复杂度 ,这正好满足了缓存高速运作的需求。
在第18讲我讲了用数组和链表来构造哈希表。在很多编程语言中哈希表的实现采用的是链地址哈希表。这种方法的主要思想是先分配一个很大的数组空间而数组中的每一个元素都是一个链表的头部。随后我们就可以根据哈希函数算出的哈希值也叫哈希的key找到数组的某个元素及对应的链表然后把数据添加到这个链表中。之所以要这样设计是因为存在哈希冲突。所以我们要尽量找到一个合理的哈希函数减少冲突发生的机会提升检索的效率。
接下来我们来聊聊缓存淘汰的策略。这里我们使用LRU最久未用策略。在这种策略中系统会根据数据最近一次的使用时间来排序使用时间最久远的对象会被淘汰。考虑到这个特性我们可以使用队列。我在讲解广度优先搜索策略时谈到了队列。这是一种先进先出的数据结构先进入队列的元素会优先得到处理。如果充分利用队列的特点我们就很容易找到上一次使用时间最久的数据具体的实现过程如下。
第一,根据缓存的大小,设置队列的最大值。通常的做法是使用缓存里所能存放数据记录量的上限,作为队列里结点的总数的上限,两者保持一致。
第二,每次访问一个数据后,查看是不是已经存在一个队列中的结点对应于这个数据。如果不是,创造一个对应于这个数据的队列结点,加入队列尾部。如果是,把这个数据所对应的队列结点重新放入队列的尾部。需要注意,这一点是至关重要的。因为这种操作可以保证上一次访问时间最久的数据,所对应的结点永远在队列的头部。
第三,如果队列已满,我们就需要淘汰一些缓存中的数据。由于队列里的结点和缓存中的数据记录量是一致的,所以队列里的结点数达到上限制,也就意味着缓存也已经满了。刚刚提到,由于第二点的操作,我们只需要移除队列头部的结点就可以了。
综合上述关于哈希表和队列的讨论,我们可以画出下面这张框架图。
<img src="https://static001.geekbang.org/resource/image/2a/28/2a74d2c598a2aa9952b70e350c49f828.png" alt="">
从这张图可以看到我们使用哈希表来存放需要被缓存的内容然后使用队列来实现LRU策略。每当数据请求进来的时候缓存系统首先检查数据记录是不是已经存在哈希表中。如果不存在那么就返回没有查找到不会对哈希表和队列进行任何的改变如果已经存在就直接从哈希表读取并返回。
与此同时,在队列中进行相应的操作,标记对应记录最后访问的次序。队列头部的结点,对应即将被淘汰的记录。如果缓存或者说队列已满,而我们又需要插入新的缓存数据,那么就需要移除队列头部的结点,以及它所对应的哈希表结点。
接下来我们结合这张图以请求数据记录175为例详细看看在这个框架中每一步是如何运作的。
这里的哈希表所使用的散列函数非常简单是把数据的所有位数加起来再对一个非常大的数值例如10^6求余。那么175的哈希值就是(1+7+5)/10^6=13。通过哈希值13找到对应的链表然后进行遍历找到记录175。这个时候我们已经完成了从缓存中获取数据。
不过由于使用了LRU策略来淘汰旧的数据所以还需要对保存访问状态的队列进行必要的操作。检查队列我们发现表示175的结点已经在队列之中了说明最近这条数据已经被访问过所以我们要把这个结点挪到队列的最后让它远离被淘汰的命运。
我们再来看另一个例子假设这次需要获取的是数据记录1228这条记录并不在缓存之中因此除了从低速介质返回获取的记录我们还要把这个数据记录放入缓存区并更新保存访问状态的队列。和记录175不同的是1228最近没有被访问过所以我们需要在队列的末尾增加一个表示1201的结点。这个时候队列已经满了我们需要让队列头部的结点73出队然后把相应的记录73从哈希表中删除最后把记录1228插入到哈希表中作为缓存。
## 总结
当今的计算机系统中缓存扮演着非常重要的角色小到CPU大到互联网站点我们都需要使用缓存来提升系统性能。基于哈希的数据结构可以帮助我们快速的获取数据所以非常适合运用在缓存系统之中。
不过缓存都需要相对昂贵的硬件来实现因此大小受到限制。所以我们需要使用不同的策略来淘汰不经常使用的内容取而代之一些更有可能被使用的内容增加缓存的命中率进而提升缓存的使用效果。为了实现这个目标人们提出了多种淘汰的策略包括LRU和LFU。
综合上面两点,我们提出一种结合哈希表和队列的缓存设计方案。哈希表负责快速的存储和更新被缓存的内容,而队列负责管理最近被访问的状态,并告知系统哪些数据是要被淘汰并从哈希表中移除。
## 思考题
请根据今天所讲解的设计思想尝试编码实现一个基于LRU淘汰策略的缓存。哈希表部分可以直接使用编程语言所提供的哈希类数据结构。
欢迎留言和我分享,也欢迎你在留言区写下今天的学习笔记。你可以点击“请朋友读”,把今天的内容分享给你的好友,和他一起精进。

View File

@@ -0,0 +1,87 @@
<audio id="audio" title="47 | 搜索引擎(上):如何通过倒排索引和向量空间模型,打造一个简单的搜索引擎?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a7/6d/a776bceee319f64640d78c663be7176d.mp3"></audio>
你好,我是黄申。
上一节,我们充分利用了哈希表时间复杂度低的特点,设计了一个简单的缓存系统。在实际项目中,哈希表或者类似的哈希数据结构,有着更为广泛的运用。比如,搜索引擎中的倒排索引,也是基于哈希表结构来设计的。这种倒排索引可以大大提升数据对象的检索效率。
除了搜索的效率,搜索引擎另一个需要考虑的问题是相关性,也就是说,我们需要保证检索出来的信息是满足用户需求的。最简单的基于倒排索引的实现,属于一种布尔排序模型,它只考虑了单词是不是出现在文档之中,如果出现了就返回相应的文档,否则就不返回,对应于布尔模型中的真假值。在这种实现中,只要出现了相关搜索词的文档都会被检索出来,因此相关性比较差。对于这点,我们可以利用向量空间模型,来衡量文档和用户查询之间的相似程度,确保两者是相关的。不过,向量空间模型需要涉及两两之间的比较,时间复杂度比较高。
考虑到上述两点,今天,我们就以文档检索为例,参照倒排索引加向量空间模型的设计思路,设计一个简单的搜索引擎。
## 搜索引擎的设计框架
之前在讲解向量空间模型的时候,我们介绍了信息检索的基础知识,而我们平时经常使用的搜索引擎,就是一种典型的信息检索系统。在讲解如何结合倒排索引和向量空间模型之前,我们先来看,常见的文本搜索引擎都由哪些模块组成。
文本搜索系统的框架通常包括2个重要模块**离线的预处理**和**在线的查询**。离线预处理也就是我们通常所说的“索引”阶段,包括数据获取、文本预处理、词典和倒排索引的构建、相关性模型的数据统计等。数据的获取和相关性模型的数据统计这两步,根据不同的应用场景,必要性和处理方式有所不同。可是,文本预处理和倒排索引构建这两个步骤,无论在何种应用场景之中都是必不可少的,所以它们是离线阶段的核心。之前我们讲过,常规的文本预处理是指针对文本进行分词、移除停用词、取词干、归一化、扩充同义词和近义词等操作。
在第17讲里我讲解了如何使用倒排索引把文档集转换为从关键词到文档的这种查找关系。有了这种“倒排”的关系我们可以很高效地根据给定的单词找到出现过这个单词的文档集合。
倒排索引是典型的牺牲空间来换取时间的方法。我们假设文章总数是k每篇文章的单词数量是m查询中平均的关键词数量是l那么倒排索引可以把时间复杂度从O(k×logm)降到O(l)。但是,如果使用倒排索引,就意味着除了原始数据,我们还需要额外的存储空间来放置倒排索引。因此,如果我们的字典里,不同的词条总数为$n_1$,每个单词所对应的文章平均数为$n_2$那么空间复杂度就是O($n_1$×$n_2$)。
在文本的离线处理完毕后,我们来看在线的文本查询。这个过程相对简单。
查询一般都会使用和离线模块一样的预处理,词典也是沿用离线处理的结果。当然,也可能会出现离线处理中未曾出现过的新词,我们一般会忽略或给予非常小的权重。在此基础上,系统根据用户输入的查询条件,在倒排索引中快速检出文档,并进行相关性的计算。
不同的相关性模型有不同的计算方式。最简单的布尔模型只需要计算若干匹配条件的交集向量空间模型VSM则需要计算查询向量和待查文档向量的余弦夹角而语言模型需要计算匹配条件的贝叶斯概率等等。
综合上述的介绍,我使用下面这张图来展示搜索引擎的框架设计。
<img src="https://static001.geekbang.org/resource/image/5f/71/5fd4e7baf56b4ee938aa586289965571.png" alt="">
## 倒排索引的设计
我们之前已经把倒排索引的概念讲清楚了。不过到具体设计的时候,除了从关键词到文档这种“倒排”的关系,还有其它两个要点值得考虑:第一个是倒排索引里具体存储什么内容,第二个就是多个关键词的查询结果如何取交集。我们下面一个个来看。
首先我们来聊聊倒排索引里具体存放的内容。
从倒排索引的概念我们很容易就想到使用哈希表、尤其是基于链式地址法的哈希表来实现倒排索引。哈希的键key就是文档词典的某一个词条value就是一个链表链表是出现这个词条的所有文档之集合而链表的每一个结点就表示出现过这个词条的某一篇文档。这种最简单的设计能够帮助我们判断哪些文档出现过给定的词条因此它可以用于布尔模型。但是如果我们要实现向量空间模型或者是基于概率的检索模型就需要很多额外的信息比如词频tf、词频-逆文档频率tf-idf、词条出现的条件概率等等。
另外有些搜索引擎需要返回匹配到的信息摘要nippet因此还需要记住词条出现的位置。这个时候最简单的倒排索引就无法满足我们的需求了。我们要在倒排索引中加入更多的信息。每个文档列表中存储的不仅仅是文档的ID还有其他额外的信息。我使用下面这张图展示了一个示例帮助你理解这种新的设计。
<img src="https://static001.geekbang.org/resource/image/b4/2d/b4efd06d8eb915d8f68a0ebc4fb6852d.png" alt="">
其中ID字段表示文档的IDtf字段表示词频tfidf字段表示词频-逆文档频率而prob表示这个词条在这篇文档中出现的条件概率。
好了,下面我们来看,如何确定出现所有多个关键词的文档。
由于倒排索引本身的特性,我们可以很快知道某一个词条对应的文档,也就是说查找出现某一个词条的所有文档是很容易的。可是,如果用户的查询包含多个关键词,那么该如何利用倒排索引,查找出现多个词条的所有文档呢?
还记得我讲解分治法时所提到的归并排序吗在这里我们可以借鉴其中的合并步骤。假设有两个词条a和ba对应的文档列表是Ab对应的文档列表是B而A和B这两个列表中的每一个元素都包含了文档的ID。
首先我们根据文档的ID分别对这两个列表进行从小到大的排序然后依次比较两个列表的文档ID如果当前的两个ID相等就表示这个ID所对应的文档同时包含了a和b两个关键词所以是符合要求的进行保留然后两个列表都拿出下一个ID进行之后的对比。如果列表A的当前ID小于列表B的当前ID那么表明A中的这个ID一定不符合要求跳过它然后拿出A中的下一个ID和B进行比较。同样如果是列表B的第一个ID更小那么就跳过B中的这个ID拿出B中的下一个ID和A进行比较。依次类推直到遍历完所有A和B中的ID。
我画了张图来进一步解释这个过程。
<img src="https://static001.geekbang.org/resource/image/ba/89/ba281eeeb565405d95bedcc5a908ae89.png" alt="">
基于这种两两比较的过程,我们可以推广到比较任意多的列表。此外,在构建倒排索引的时候,我们可以事先对每个词条的文档列表进行排序,从而避免了查询时候的排序过程,达到提升搜索效率的目的。
## 向量空间和倒排索引的结合
有了倒排索引的高效查询,向量空间的实现就不难了。还记得之前我们讲解的向量空间模型吗?这个模型假设所有的对象都可以转化为向量,然后使用向量间的距离(通常是欧氏距离)或者是向量间的夹角余弦来表示两个对象之间的相似程度。
在文本搜索引擎中我们使用向量来表示每个文档以及用户的查询而向量的每个分量由每个词条的tf-idf构成最终用户查询和文档之间的相似度或者说相关性由文档向量和查询向量的夹角余弦确定。如果能获取这个查询和所有文档之间的相关性得分那么我们就能对文档进行排序返回最相关的那些。不过当文档集合很大的时候这个操作的复杂度会很高。你可以观察一下这个夹角余弦的公式。
<img src="https://static001.geekbang.org/resource/image/47/58/47a3eb754bc98caab86dc554b4cf7558.png" alt="">
如果文档中词条的平均数量是n查询中词条的平均数量是m那么计算某个查询和某个文档之间的夹角余弦时间复杂度是O(n×m)。如果整个被索引的文档集合有k个文档那么计算某个查询和所有文档之间的夹角余弦时间复杂度就变为O(n×m×k)。
实际上很多文档并没有出现查询中的关键词条所以计算出来的夹角余弦都是0而这些计算都是可以完全避免的解决方案就是倒排索引。通过倒排索引我们挑选出那些出现过查询关键词的文档并仅仅针对这些文档进行夹角余弦的计算那么计算量就会大大减少。
此外我们之前设计的倒排索引也已经保存了tf-idf这种信息因此可以直接利用从倒排索引中取出的tf-idf值计算夹角余弦公式的分子部分。至于分母部分它包含了用户查询向量的和文档向量的L2范数。通常查询向量所包含的非0分量很少L2范数计算是很快的。而每篇文档的L2范数在文档没有更新的情况下是不变的因此我们可以在索引阶段就计算好并保持在额外的数据结构之中。
## 小结
目前,以搜索引擎为代表的信息检索技术已经相当成熟,无论是大型的互联网系统,还是小型的手机操作系统,都支持高效率的搜索。而搜索引擎最重要的核心就是及时性和相关性。及时性确保用户可以快速找到信息,而相关性确保所找到的信息是用户真正需要的。
在文本搜索中,倒排索引通过一种称为“索引”的过程,把文档到词条的关系,转化为词条到文档的逆关系,这样对于任何给定的关键词,我们可以很快的找到哪些文档包含这个关键词条。所以,倒排索引是搜索引擎提升及时性中非常关键的一步。倒排索引非常适合使用哈希表,特别是链地址型的哈希表来实现。
向量空间模型可以作为文本搜索的相关性模型。但是,它的计算需要把查询和所有的文档进行比较,时间复杂度太高,影响了及时性。这个时候,我们可以利用倒排索引,过滤掉绝大部分不包含查询关键词的文档。
## 思考题
请根据今天所讲解的设计思想,使用你熟悉的编程语言,来实现一个基于倒排索引和向量空间模型的文本搜索引擎。
欢迎留言和我分享,也欢迎你在留言区写下今天的学习笔记。你可以点击“请朋友读”,把今天的内容分享给你的好友,和他一起精进。

View File

@@ -0,0 +1,132 @@
<audio id="audio" title="48 | 搜索引擎(下):如何通过查询的分类,让电商平台的搜索结果更相关?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/43/b3/436b949dd7e4d9134c67f68004ef68b3.mp3"></audio>
你好,我是黄申。
上一节,我给你阐述了如何使用哈希的数据结构设计倒排索引,并使用倒排索引加速向量空间模型的计算。倒排索引提升了搜索执行的速度,而向量空间提升了搜索结果的相关性。
可是,在不同的应用场景,搜索的相关性有不同的含义。无论是布尔模型、向量空间模型、概率语言模型还是其他任何更复杂的模型,都不可能“一招鲜,吃遍天”。今天,我就结合自己曾经碰到的一个真实案例,为你讲解如何利用分类技术,改善搜索引擎返回结果的相关性。
你可能会觉得奇怪,这分类技术,不是监督式机器学习中的算法吗?它和信息检索以及搜索技术有什么关系呢?且听我慢慢说来。
## 电商搜索的难题
我曾经参与过一个电商的商品搜索项目。有段时间,用户时常反馈这么一个问题,那就是关键词搜索的结果非常不精准。比如搜索“牛奶”,会出现很多牛奶巧克力,甚至连牛奶色的连衣裙,都跑到搜索结果的前排了,用户体验非常差。但是,巧克力和连衣裙这种商品标题里确实存在“牛奶”的字样,如果简单地把“牛奶”字眼从巧克力和服饰等商品标题里去除,又会导致搜索“牛奶巧克力”或者“牛奶连衣裙”时无法展示相关的商品,这肯定也是不行的。
这种搜索不精确的情况十分普遍,还有很多其他的例子,比如搜索“橄榄油”的时候会返回热门的“橄榄油发膜”或“橄榄油护手霜”,搜索“手机”的时候会返回热门的“手机壳”和“手机贴膜”。另外,商品的品类也在持续增加,因此也无法通过人工运营来解决。
为了解决这个问题首先我们来分析一下产生问题的主要原因。目前多数的搜索引擎实现所采用都是类似向量空间模型的相关性模型。所以在进行相关性排序的时候系统主要考虑的因素都是关键词的tf-idf、文档的长短、查询的长短等因素。这种方式非常适合普通的文本检索在各大通用搜索引擎里也被证明是行之有效的方法之一。但是经过我们的分析这种方式并不适合电子商务的搜索平台主要原因包括这样几点
第一点,商品的标题都非常短。电商平台上的商品描述,包含的内容太多,有时还有不少广告宣传,这些不一定是针对产品特性的信息,如果进入了索引,不仅加大了系统计算的时间和空间复杂度,还会导致较低的相关性。所以,商品的标题、名称和主要的属性成为搜索索引关注的对象,而这些内容一般短小精悍,不需要考虑其长短对于相关性衡量的影响。
第二点关键词出现的位置、词频对相关性意义不大。如上所述正是由于商品搜索主要关注的是标题等信息浓缩的字段因此某个关键词出现的位置、频率对于相关性的衡量影响非常小。如果考虑了这些反而容易被别有用心的卖家利用进行不合理的关键词搜索优化SEO导致最终结果的质量变差。
第三点,用户的查询普遍比较短。在电商平台上,顾客无需太多的关键词就能定位大概所需,因此查询的字数多少对于相关性衡量也没有太大意义。
因此,电商的搜索系统不能局限于关键词的词频、出现位置等基础特征,更应该从其他方面来考虑。
既然最传统的向量空间模型无法很好的解决商品的搜索,那么我们应该使用什么方法进行改进呢?回到我们之前所发现的问题,实际上主要纠结在一个“分类”的问题上。例如,顾客搜索“牛奶”字眼的时候,系统需要清楚用户是期望找到饮用的牛奶,还是牛奶味的巧克力或饼干。从这个角度出发考虑,我们很容易就考虑到了,是不是可以首先对用户的查询,进行一个基于商品目录的分类呢?如果可以,那么我们就能知道把哪些分类的商品排在前面,从而提高返回商品的相关性。
## 查询的分类
说到查询的分类,我们有两种方法可以尝试。第一种方法是在商品分类的数据上,运用朴素贝叶斯模型构建分类器。第二种方法是根据用户的搜索行为构建分类器。
在第一种方法中,商品分类数据和朴素贝叶斯模型是关键。电商平台通常会使用后台工具,让运营人员构建商品的类目,并在每个类目中发布相应的商品。这个商品的类目,就是我们分类所需的类别信息。由于这些商品属于哪个类目是经过人工干预和确认的,因此数据质量通常比较高。我们可以直接使用这些数据,构造朴素贝叶斯分类器。这里我们快速回顾一下朴素贝叶斯的公式。
<img src="https://static001.geekbang.org/resource/image/92/1e/929ef79bdb208063ad744c285e51231e.png" alt="">
之前我们提到过,商品文描中噪音比较多,因此通常我们只看商品的标题和重要属性。所以,上述公式中的$f_1f_2……f_k$,表示来自商品标题和属性的关键词。
相对于第一种方法,第二种方法更加巧妙。它的核心思想是观察用户在搜词后的行为,包括点击进入的详情页、把商品加入收藏或者是添加到购物车,这样我们就能知道,顾客最为关心的是哪些类目。
举个例子当用户输入关键词“咖啡”如果经常浏览和购买的品类是国产冲饮咖啡、进口冲饮咖啡和咖啡饮料那么这3个分类就应该排在更前面然后将其它虽然包含咖啡字眼但是并不太相关的分类统统排在后面。需要注意的是这种方法可以直接获取P(C|f),而无需通过贝叶斯理论推导。
上述这两种方法各有优劣。第一种方法的优势在有很多的人工标注作为参考,因此不愁没有可用的数据。可是分类的结果受到商品分布的影响太大。假设服饰类商品的数量很多,而且有很多服饰都用到了“牛奶”的字眼,那么根据朴素贝叶斯分类模型的计算公式,“牛奶”这个词属于服饰分类的概率还是很高。第二种方法正好相反,它的优势在于经过用户行为的反馈,我们可以很精准地定位到每个查询所期望的分类,甚至在一定程度上解决查询季节性和个性化的问题。但是这种方法过度依赖用户的使用,面临一个“冷启动”的问题,也就说在搜索系统投入使用的初期,无法收集足够的数据。
考虑到这两个方法的特点,我们可以把它们综合起来使用,最简单的就是线性加和。
$P(C|query)=w_1·P_1(C|query)+w_2·P_2(C|query)$
其中,$P_1$和$P_2$分别表示根据第一种方法和第二种方法获得的概率,而权重$w_1$和$w_2$分别表示第一种方法和第二种方法的权重,可以根据需要设置。通常在一个搜索系统刚刚起步的时候,可以让$w_1$更大。随着用户不断的使用,我们就可以让$w_2$更大,让用户的参与使得系统更智能。
## 查询分类和搜索引擎的结合
一旦我们可以对商品查询进行更加准确地分类,那么就可以把这个和普通的搜索引擎结合起来。我使用下面的框架图来展示整个流程。
<img src="https://static001.geekbang.org/resource/image/1a/bf/1ae2bca6c15f8c53937a7d825e78cdbf.png" alt="">
从这张图可以看到,我们使用商品目录打造一个初始版本的查询分类器。随着用户不断的使用这个搜索引擎,我们收集用户的行为日志,并使用这个日志改善查询的分类器,让它变得更加精准,然后再进一步优化搜索引擎的相关性。
我以Elasticsearch为例讲一下如何利用分类的结果改变搜索的排序。
Elasticsearch是一个基于Lucene的搜索服务器是流行的企业级搜索引擎之一目前最新版已经更新到6.6.x。Elasticsearch是基于Lucene的架构很多要素都是一脉相承的例如文档和字段的概念、相关性的模型、各种模式的查询等。也正是这个原因Elasticsearch默认的排序也采取了类似向量空间模型的方式。如果这种默认排序并不适用于商品搜索那么我们要如何修改呢
为了充分利用查询分类的结果首先要达到这样的目标对于给定的查询所有命中的结果的得分都是相同的。至少有两种做法修改默认的Similarity类的实现或者是使用过滤查询Filter Query
统一了基本的排序得分之后,我们就可以充分利用用户的行为数据,指导搜索引擎进行有针对性的排序改变,最终提升相关性。这里需要注意的是,由于这里排序的改变依赖于用户每次输入的关键词,因此不能在索引的阶段完成。
例如在搜索“牛奶巧克力”的时候理想的是将巧克力排列在前而搜索“巧克力牛奶”的时候理想的是将牛奶排列在前所以不能简单地在索引阶段就利用文档提升Document Boosting或字段提升Field Boosting
对于Elasticsearch而言它有个强大的Boost功能这个功能可以在查询阶段根据某个字段的值动态地修改命中结果的得分。假设我们有一个用户查询“米”根据分类结果我们知道“米”属于“大米”分类的概率为0.85属于“饼干”和“巧克力”分类的概率都为0.03。根据这个分类数据下面我使用了一段伪代码展示了加入查询分类后的Elasticsearch查询。
```
{
&quot;query&quot;: {
&quot;bool&quot;: {
&quot;must&quot;: {
&quot;match_all&quot;: {
}
},
&quot;should&quot;: [
{
&quot;match&quot;: {
&quot;category_name&quot;: {
&quot;query&quot;: &quot;大米&quot;,
&quot;boost&quot;: 0.85
}
}
},
{
&quot;match&quot;: {
&quot;category_name&quot;: {
&quot;query&quot;: &quot;饼干&quot;,
&quot;boost&quot;: 0.03
}
}
},
{
&quot;match&quot;: {
&quot;category_name&quot;: {
&quot;query&quot;: &quot;巧克力&quot;,
&quot;boost&quot;: 0.03
}
}
}
],
&quot;filter&quot;: {
&quot;term&quot;: {&quot;listing_title&quot; : &quot;米&quot;}
}
}
}
}
```
其中最主要的部分是增加了should的查询针对最主要的3个相关分类进行了boost操作。如果使用这个查询进行搜索你就会发现属于“大米”分类的商品排到了前列更符合用户的预期而且这完全是在没有修改索引的前提下实现的。
## 小结
相关性模型是搜索引擎非常核心的模块,它直接影响了搜索结果是不是满足用户的需求。我们之前讲解的向量空间模型、概率语言模型等常见的模型,逐渐成为了主流的相关性模型。不过这些模型通常适用于普通的文本检索,并没有针对每个应用领域进行优化。
在电商平台上,搜索引擎是帮助用户查找商品的好帮手。可是,直接采用向量空间模型进行排序往往效果不好。这主要是因为索引的标题和属性都很短,我们无法充分利用关键词的词频、逆文档频率等信息。考虑到搜索商品的时候,商品的分类对于用户更为重要,所以我们在设计相关性排序的时候需要考虑这个信息。
为了识别用户对哪类商品更感兴趣,我们可以对用户输入的查询进行分类。用于构建分类器的数据,可以是运营人员发布的商品目录信息,也可以是用户使用之后的行为日志。我们可以根据搜索系统运行的情况,赋予它们不同的权重。
如果我们可以对查询做出更为准确的分类那么就可以使用这个分类的结果来对原有搜索结果进行重新排序。现在的开源搜索引擎例如Elasticsearch都支持动态修改排序结果为我们结合分类器和搜索引擎提供了很大的便利。
## 思考题
通过用户行为反馈的数据,构建查询分类的时候,我们把整个查询作为了一个单词或者词组来处理。也就是说直接获取了$P(C|f)$的值。如果我们把这个查询看做多个词的组合,也就是说获取的是$P(C|f1,f2,…,fn)$,那我们可以如何改进这个基于用户行为反馈的分类模型呢?
欢迎留言和我分享,也欢迎你在留言区写下今天的学习笔记。你可以点击“请朋友读”,把今天的内容分享给你的好友,和他一起精进。

View File

@@ -0,0 +1,163 @@
<audio id="audio" title="49 | 推荐系统(上):如何实现基于相似度的协同过滤?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b8/fe/b8d32976306e32dc0b75f87a6191f2fe.mp3"></audio>
你好,我是黄申。
个性化推荐这种技术在各大互联网站点已经普遍使用了系统会根据用户的使用习惯主动提出一些建议帮助他们发现一些可能感兴趣的电影、书籍或者是商品等等。在这方面最经典的案例应该是美国的亚马逊电子商务网站它是全球最大的B2C电商网站之一。在公司创立之初最为出名的就是其丰富的图书品类以及相应的推荐技术。亚马逊的推荐销售占比可以达到整体销售的30%左右。可见,对于公司来说,推荐系统也是销售的绝好机会。因此,接下来的两节,我会使用一个经典的数据集,带你进行推荐系统核心模块的设计和实现。
## MovieLens数据集
在开始之前我们先来认识一个知名的数据集MovieLens。你可以在它的[主页](http://files.grouplens.org/datasets/movielens/)查看详细的信息。这个数据集最核心的内容是多位用户对不同电影的评分,此外,它也包括了一些电影和用户的属性信息,便于我们研究推荐结果是不是合理。因此,这个数据集经常用来做推荐系统、或者其他机器学习算法的测试集。
时至今日这个数据集已经延伸出几个不同的版本有不同的数据规模和更新日期。我这里使用的是一个最新的小规模数据集包含了600位用户对于9000部电影的约10万条评分最后更新于2018年9月。你可以在这里下载[http://files.grouplens.org/datasets/movielens/ml-latest-small.zip](http://files.grouplens.org/datasets/movielens/ml-latest-small.zip)。
解压了这个zip压缩包之后你会看到readme文件和四个csv文件ratings、movies、links和tags。其中最重要的是ratings它包含了10万条评分每条记录有4个字段包括userId、movieId、rating、timestamp。userId表示每位用户的idmovieId是每部电影的ID
rating是这位用户对这部电影的评分取值为0-5分。timestamp是时间戳。而movies包含了电影的主要属性信息title和genres分别表示电影的标题和类型一部电影可以属于多种类型。links和tags则包含了电影的其他属性信息。我们的实验主要使用ratings和movies里的数据。
## 设计的整体思路
有了用于实验的数据接下来就要开始考虑如何设计这个推荐系统。我在第38期讲解了什么是协同过滤推荐算法、基于用户的协同过滤和基于物品的协同过滤。这一节我们就以协同过滤为基础分别实现基于用户和物品的过滤。
根据协同过滤算法的核心思想,整个系统可以分为三个大的步骤。
第一步,用户评分的标准化。因为有些用户的打分比较宽松,而有些用户打分则比较挑剔。所以,我们需要使用标准化或者归一化,让不同用户的打分具有可比性,这里我会使用**z分数**标准化。
第二步,衡量和其他用户或者物品之间的相似度。我们这里的物品就是电影。在基于用户的过滤中,我们要找到相似的用户。在基于物品的过滤中,我们要找到相似的电影。我这里列出计算用户之间相似度$us$和物品之间相似度$is$的公式。之前我们讲过,这些都可以通过矩阵操作来实现。
<img src="https://static001.geekbang.org/resource/image/8e/ec/8e002cc0e287f675221e42df5c249fec.png" alt=""><img src="https://static001.geekbang.org/resource/image/3c/2b/3c04082d6f87f9268eceb65cb624892b.png" alt="">
我们以基于用户的过滤为例。假设我们使用夹角余弦来衡量相似度,那么我们就可以采用用户评分的矩阵点乘自身的转置来计算余弦夹角。用户评分的矩阵$X$中,每一行是某位用户的行向量,每个分量表示这位用户对某部电影的打分。而矩阵$X$的每一列是某个用户的列向量,每个分量表示用户对某部电影的打分。
我们假设$XX$的结果为矩阵$Y$,那么$y_{i,j}$就表示用户$i$和用户$j$这两者喜好度向量的点乘结果,它就是夹角余弦公式中的分子。如果$i$等于$j$,那么这个计算值也是夹角余弦公式分母的一部分。从矩阵的角度来看,$Y$中任何一个元素都可能用于夹角余弦公式的分子,而对角线上的值会用于夹角余弦公式的分母。因此,我们可以利用$Y$来计算任何两个用户之间的相似度。
之前我们使用了一个示例讲解过对于基于用户的协同过滤,如何计算矩阵$Y$,以及如何使用$Y$来计算余弦夹角,我这里列出来给你参考。
<img src="https://static001.geekbang.org/resource/image/79/2b/79e7065dade1b1a4d38639bb9d2cea2b.png" alt=""><img src="https://static001.geekbang.org/resource/image/9d/42/9ddfe8b7874d9d708fa367ccca967942.png" alt="">
第三步根据相似的用户或物品给出预测的得分p。
<img src="https://static001.geekbang.org/resource/image/0a/29/0ace6bd5295a83ab6da8f5357a755929.png" alt=""><img src="https://static001.geekbang.org/resource/image/8e/23/8e170042ec88bdf48908ea0bdc2d7423.png" alt="">
之前我们也解释过如何使用矩阵操作来实现这一步。还是以基于用户的过滤为例。假设通过第二步,我们已经得到用户相似度矩阵$US$$US$和评分矩阵$X$的点乘结果为矩阵$USP$。沿用前面的示例,结果就是下面这样。
<img src="https://static001.geekbang.org/resource/image/65/1f/65ff214ce18ccc12193bf17cd1ec201f.png" alt="">
然后对$US$按行求和,获得矩阵$USR$。
<img src="https://static001.geekbang.org/resource/image/28/0e/28c5469d3fcacda96604f66dfc883a0e.png" alt="">
最终,我们使用$USP$和$USR$的元素对应除法,就可以求得任意用户对任意电影的评分矩阵$P$。
<img src="https://static001.geekbang.org/resource/image/eb/c8/eba06795db612c7be96118b9fa93cfc8.png" alt="">
有了这个设计的思路下面我们就可以使用Python进行实践了。
## 核心Python代码
在实现上述设计的三个主要步骤之前我们还需要把解压后的csv文件加载到数组并转为矩阵。下面我列出了主要的步骤和注释。需要注意的是由于这个数据集中的用户和电影ID都是从1开始而不是从0开始所以需要减去1才能和Python数组中的索引一致。
```
import pandas as pd
from numpy import *
# 加载用户对电影的评分数据
df = pd.read_csv(&quot;/Users/shenhuang/Data/ml-latest-small/ratings.csv&quot;)
# 获取用户的数量和电影的数量
user_num = df[&quot;userId&quot;].max()
movie_num = df[&quot;movieId&quot;].max()
# 构造用户对电影的二元关系矩阵
user_rating = [[0.0] * movie_num for i in range(user_num)]
i = 0
for index, row in df.iterrows(): # 获取每行的index、row
# 由于用户和电影的ID都是从1开始为了和Python的索引一致减去1
userId = int(row[&quot;userId&quot;]) - 1
movieId = int(row[&quot;movieId&quot;]) - 1
# 设置用户对电影的评分
user_rating[userId][movieId] = row[&quot;rating&quot;]
# 显示进度
i += 1
if i % 10000 == 0:
print(i)
# 把二维数组转化为矩阵
x = mat(user_rating)
print(x)
```
加载了数据之后,第一步就是对矩阵中的数据,以行为维度,进行标准化。
```
# 标准化每位用户的评分数据
from sklearn.preprocessing import scale
# 对每一行的数据,进行标准化
x_s = scale(x, with_mean=True, with_std=True, axis=1)
print(&quot;标准化后的矩阵:&quot;, x_s)
```
第二步是计算表示用户之间相似度的矩阵US。其中y变量保存了矩阵X左乘转置矩阵X的结果。而利用y变量中的元素我们很容易就可以得到不同向量之间的夹角余弦。
```
# 获取XX'
y = x_s.dot(x_s.transpose())
print(&quot;XX'的结果是'&quot;, y)
# 获得用户相似度矩阵US
us = [[0.0] * user_num for i in range(user_num)]
for userId1 in range(user_num):
for userId2 in range(user_num):
# 通过矩阵Y中的元素计算夹角余弦
us[userId1][userId2] = y[userId1][userId2] / sqrt((y[userId1][userId1] * y[userId2][userId2]))
```
在最后一步中,我们就可以进行基于用户的协同过滤推荐了。需要注意的是,我们还需要使用元素对应的除法来实现归一化。
```
# 通过用户之间的相似度计算USP矩阵
usp = mat(us).dot(x_s)
# 求用于归一化的分母
usr = [0.0] * user_num
for userId in range(user_num):
usr[userId] = sum(us[userId])
# 进行元素对应的除法,完成归一化
p = divide(usp, mat(usr).transpose())
```
我们可以来看一个展示推荐效果的例子。在原始的评分数据中我们看到ID为1的用户并没有对ID为2的电影进行评分。而在最终的矩阵P中我们可以看系统对用户1给电影2的评分做出了较高的预测换句话说系统认为用户1很可能会喜好电影2。进一步研究电影的标题和类型我们会发现用户1对《玩具总动员》1995年这类冒险类和动作类的题材更感兴趣所以推荐电影2《勇敢者的游戏》1995年也是合理的。
## 总结
在今天的内容中,我通过一个常用的实验数据,设计并实现了最简单的基于用户的协同过滤。我们最关心的是这个数据中,用户对电影的评分。有了这种二元关系,我们就能构建矩阵,并通过矩阵的操作来发现用户或物品之间的相似度,并进行基于用户或者物品的协同过滤。对于最终的计算结果,你可以尝试分析针对不同用户的推荐,看看协同过滤推荐的效果是不是合理。
在你分析推荐结果的时候可能会参考movie.csv这个文件中所描述的电影类型。这些电影类型都是一开始人工标注好的。那么有没有可能在没有这种标注数据的情况下在一定程度上自动分析哪些电影属于同一个或者近似的类型呢如果可以有没有可能在这种自动划分电影类型的基础之上给出电影的推荐呢下一节我会通过SVD奇异值分解来进行这个方向的尝试。
## 思考题
今天我使用Python代码实现了基于用户的协同过滤。类似地我们也可以采用矩阵操作来实现基于物品的协同过滤请使用你擅长的语言来实现试试。
欢迎留言和我分享,也欢迎你在留言区写下今天的学习笔记。你可以点击“请朋友读”,把今天的内容分享给你的好友,和他一起精进。

View File

@@ -0,0 +1,209 @@
<audio id="audio" title="50 | 推荐系统如何通过SVD分析用户和物品的矩阵" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ee/4f/eecc7c8a556c409ae30bc299141ee84f.mp3"></audio>
你好,我是黄申。
上一节我们讲了如何使用矩阵操作实现基于用户或者物品的协同过滤。实际上推荐系统是个很大的课题你可以尝试不同的想法。比如对于用户给电影评分的案例是不是可以使用SVD奇异值的分解来分解用户评分的矩阵并找到“潜在”的电影主题呢如果在一定程度上实现这个目标那么我们可以通过用户和主题以及电影和主题之间的关系来进行推荐。今天我们继续使用MovieLens中的一个数据集尝试Python代码中的SVD分解并分析一些结果所代表的含义。
## SVD回顾以及在推荐中的应用
在实现SVD分解之前我们先来回顾一下SVD的主要概念和步骤。如果矩阵$X$是对称的方阵,那么我们可以求得这个矩阵的特征值和特征向量,并把矩阵$X$分解为特征值和特征向量的乘积。
假设我们求出了矩阵$X$的$n$个特征值$λ_1λ_2λ_n$,以及这$n$个特征值所对应的特征向量$v_1v_2v_n$,那么矩阵$X$可以表示为:
其中,$V$是这$n$个特征向量所张成的$n×n$维矩阵,而$Σ$是这$n$个特征值为主对角线的$n×n$维矩阵。这个过程就是特征分解Eigendecomposition
如果我们会把$V$的这$n$个特征向量进行标准化处理,那么对于每个特征向量$V_i$,就有$||V_{i}||_{2}=1$,而这表示$V_iV_i=1$,此时$V$的$n$个特征向量为标准正交基,满足$VV=I$ 也就是说,$V$为酉矩阵,有$V=V^{-1}$ 。这样一来,我们就可以把特征分解表达式写作:
可是,如果矩阵$X$不是对称的方阵那么我们不一定能得到有实数解的特征分解。但是SVD分解可以避免这个问题。
我们可以把$X$的转置$X$和$X$做矩阵乘法,得到一个$n×n$维的对称方阵$XX$,并对这个对称方阵进行特征分解。分解的时候,我们得到了矩阵$XX$的$n$个特征值和对应的$n$个特征向量$v$,其中所有的特征向量叫作$X$的右奇异向量。通过所有右奇异向量我们可以构造一个$n×n$维的矩阵$V$。
类似地,如果我们把$X$和$X$做矩阵乘法,那么会得到一个$m×m$维的对称方阵$XX$。由于$XX$也是方阵,因此我们同样可以对它进行特征分解,并得到矩阵$XX$的$m$个特征值和对应的$m$个特征向量$u$,其中所有的特征向量向叫作$X$的左奇异向量。通过所有左奇异向量我们可以构造一个$m×m$的矩阵$U$。
现在,包含左右奇异向量的$U$和$V$都求解出来了,只剩下奇异值矩阵$Σ$了。$Σ$除了对角线上是奇异值之外其他位置的元素都是0所以我们只需要求出每个奇异值$σ$就可以了。之前我们已经推导过,$σ$可以通过两种方式获得。第一种方式是计算下面这个式子:
其中$v_i$和$u_i$都是列向量。一旦我们求出了每个奇异值$σ$,那么就能得到奇异值矩阵$Σ$。
第二种方式是通过$XX$矩阵或者$XX$矩阵的特征值之平方根,来求奇异值。计算出每个奇异值$σ$,那么就能得到奇异值矩阵$Σ$了。
通过上述几个步骤,我们就能把一个$mxn$维的实数矩阵,分解成$X=UΣV$的形式。那么这种分解对于推荐系统来说,又有怎样的意义呢?
之前我讲过在潜在语义分析LSA的应用场景下分解之后所得到的奇异值$σ$,对应一个语义上的“概念”,而$σ$值的大小表示这个概念在整个文档集合中的重要程度。$U$中的左奇异向量表示了每个文档和这些语义“概念”的关系强弱,$V$中的右奇异向量表示每个词条和这些语义“概念”的关系强弱。
最终SVD分解把原来的“词条-文档”关系,转换成了“词条-语义概念-文档”的关系。而在推荐系统的应用场景下对用户评分矩阵的SVD分解能够帮助我们找到电影中潜在的“主题”比如科幻类、动作类、浪漫类、传记类等等。
分解之后所得到的奇异值$σ$对应了一个“主题”,$σ$值的大小表示这个主题在整个电影集合中的重要程度。$U$中的左奇异向量表示了每位用户对这些“主题”的喜好程度,$V$中的右奇异向量表示每部电影和这些“主题”的关系强弱。
最终SVD分解把原来的“用户-电影”关系,转换成了“用户-主题-电影”的关系。有了这种新的关系,即使我们没有人工标注的电影类型,同样可以使用更多基于电影主题的推荐方法,比如通过用户对电影主题的评分矩阵,进行基于用户或者电影的协同过滤。
接下来我会使用同样一个MovieLens的数据集一步步展示如何通过Python语言对用户评分的矩阵进行SVD分解并分析一些结果的示例。
## Python中的SVD实现和结果分析
和上节的代码类似首先我们需要加载用户对电影的评分。不过由于非并行SVD分解的时间复杂度是3次方数量级而空间复杂度是2次方数量级所以对硬件资源要求很高。这里为了节省测试的时间我增加了一些语句只取大约十分之一的数据。
```
import pandas as pd
from numpy import *
# 加载用户对电影的评分数据
df_ratings = pd.read_csv(&quot;/Users/shenhuang/Data/ml-latest-small/ratings.csv&quot;)
# 获取用户的数量和电影的数量这里我们只取前1/10来减小数据规模
user_num = int(df_ratings[&quot;userId&quot;].max() / 10)
movie_num = int(df_ratings[&quot;movieId&quot;].max() / 10)
# 构造用户对电影的二元关系矩阵
user_rating = [[0.0] * movie_num for i in range(user_num)]
i = 0
for index, row in df_ratings.iterrows(): # 获取每行的index、row
# 由于用户和电影的ID都是从1开始为了和Python的索引一致减去1
userId = int(row[&quot;userId&quot;]) - 1
movieId = int(row[&quot;movieId&quot;]) - 1
# 我们只取前1/10来减小数据规模
if (userId &gt;= user_num) or (movieId &gt;= movie_num):
continue
# 设置用户对电影的评分
user_rating[userId][movieId] = row[&quot;rati
```
之后,二维数组转为矩阵,以及标准化矩阵的代码和之前是一致的。
```
# 把二维数组转化为矩阵
x = mat(user_rating)
# 标准化每位用户的评分数据
from sklearn.preprocessing import scale
# 对每一行的数据,进行标准化
x_s = scale(x, with_mean=True, with_std=True, axis=1)
print(&quot;标准化后的矩阵:&quot;, x_s
```
Python的numpy库已经实现了一种SVD分解我们只调用一个函数就行了。
```
# 进行SVD分解
from numpy import linalg as LA
u,sigma,vt = LA.svd(x_s, full_matrices=False, compute_uv=True)
print(&quot;U矩阵&quot;, u)
print(&quot;Sigma奇异值&quot;, sigma)
print(&quot;V矩阵&quot;, vt)
```
最后输出的Sigma奇异值大概是这样的
```
Sigma奇异值 [416.56942602 285.42546812 202.25724866 ... 79.26188177 76.35167406 74.96719708]
```
最后几个奇异值不是0说明我们没有办法完全忽略它们不过它们相比最大的几个奇异值还是很小的我们可以去掉这些值来求得近似的解。
为了验证一下SVD的效果我们还可以加载电影的元信息包括电影的标题和类型等等。我在这里使用了一个基于哈希的Python字典结构来存储电影ID到标题和类型的映射。
```
# 加载电影元信息
df_movies = pd.read_csv(&quot;/Users/shenhuang/Data/ml-latest-small/movies.csv&quot;)
dict_movies = {}
for index, row in df_movies.iterrows(): # 获取每行的index、row
dict_movies[row[&quot;movieId&quot;]] = &quot;{0},{1}&quot;.format(row[&quot;title&quot;], row[&quot;genres&quot;])
print(dict_movies)
```
我刚刚提到,分解之后所得到的奇异值$σ$对应了一个“主题”,$σ$值的大小表示这个主题在整个电影集合中的重要程度而V中的右奇异向量表示每部电影和这些“主题”的关系强弱。所以我们可以对分解后的每个奇异值通过$V$中的向量找找看哪些电影和这个奇异值所对应的主题更相关然后看看SVD分解所求得的电影主题是不是合理。比如我们可以使用下面的代码来查看和向量$Vt1$,相关的电影主要有哪些。
```
# 输出和某个奇异值高度相关的电影,这些电影代表了一个主题
print(max(vt[1,:]))
for i in range(movie_num):
if (vt[1][i] &gt; 0.1):
print(i + 1, vt[1][i], dict_movies[i + 1])
```
需要注意的是向量中的电影ID和原始的电影ID差1所以在读取dict_movies时需要使用(i + 1)。这个向量中最大的分值大约是0.173所以我把阈值设置为0.1并输出了所有分值大于0.1的电影,电影列表如下:
```
0.17316444479201024
260 0.14287410901699643 Star Wars: Episode IV - A New Hope (1977),Action|Adventure|Sci-Fi
1196 0.1147295905497075 Star Wars: Episode V - The Empire Strikes Back (1980),Action|Adventure|Sci-Fi
1198 0.15453176747222075 Raiders of the Lost Ark (Indiana Jones and the Raiders of the Lost Ark) (1981),Action|Adventure
1210 0.10411193224648774 Star Wars: Episode VI - Return of the Jedi (1983),Action|Adventure|Sci-Fi
2571 0.17316444479201024 Matrix, The (1999),Action|Sci-Fi|Thriller
3578 0.1268370902126096 Gladiator (2000),Action|Adventure|Drama
4993 0.12445203514448012 Lord of the Rings: The Fellowship of the Ring, The (2001),Adventure|Fantasy
5952 0.12535012292041953 Lord of the Rings: The Two Towers, The (2002),Adventure|Fantasy
7153 0.10972312192709989 Lord of the Rings: The Return of the King, The (2003),Action|Adventure|Drama|Fantasy
```
从这个列表可以看出,这个主题是关于科幻或者奇幻类的动作冒险题材。
使用类似的代码和同样的阈值0.1,我们来看看和向量$Vt5$,相关的电影主要有哪些。
```
# 输出和某个奇异值高度相关的电影,这些电影代表了一个主题
print(max(vt[5,:]))
for i in range(movie_num):
if (vt[5][i] &gt; 0.1):
print(i + 1, vt[5][i], dict_movies[i + 1])
```
电影列表如下:
```
0.13594520920117012
21 0.13557812349701226 Get Shorty (1995),Comedy|Crime|Thriller
50 0.11870851441884082 Usual Suspects, The (1995),Crime|Mystery|Thriller
62 0.11407971751480048 Mr. Holland's Opus (1995),Drama
168 0.10295400456394468 First Knight (1995),Action|Drama|Romance
222 0.12587492482374366 Circle of Friends (1995),Drama|Romance
261 0.13594520920117012 Little Women (1994),Drama
339 0.10815473505804706 While You Were Sleeping (1995),Comedy|Romance
357 0.11108191756350501 Four Weddings and a Funeral (1994),Comedy|Romance
527 0.1305895737838763 Schindler's List (1993),Drama|War
595 0.11155774544755555 Beauty and the Beast (1991),Animation|Children|Fantasy|Musical|Romance|IMAX
```
从这个列表可以看出这个主题更多的是关于剧情类题材。就目前所看的两个向量来说SVD在一定程度上区分了不同的电影主题你也可以使用类似的方式查看更多的向量以及对应的电影名称和类型。
## 总结
在今天的内容中我们回顾了SVD奇异值分解的核心思想解释了如何通过$XX$和$XX$这两个对称矩阵的特征分解,求得分解后的$U$矩阵、$V$矩阵和$Σ$矩阵。另外我们也解释了在用户对电影评分的应用场景下SVD分解后的$U$矩阵、$V$矩阵和$Σ$矩阵各自代表的意义,其中$Σ$矩阵中的奇异值表示了SVD挖掘出来的电影主题$U$矩阵中的奇异向量表示用户对这些电影主题的评分,而$V$矩阵中的奇异向量表示了电影和这些主题的相关程度。
我们还通过Python代码实践了这种思想在推荐算法中的运用。从结果的奇异值和奇异向量可以看出SVD分解找到了一些MovieLens数据集上的电影主题。这样我们就可以把用户针对电影的评分转化为用户针对主题的评分。由于主题通常远远小于电影所以SVD的分解也帮助我们实现了降低特征维度的目的。
SVD分解能够找到一些“潜在的“因素例如语义上的概念、电影的主题等等。虽然这样操作可以降低特征维度去掉一些噪音信息但是由于SVD分解本身的计算量也很大所以从单次的执行效率来看SVD往往无法起到优化的作用。在这种情况下我们可以考虑把它和一些监督式的学习相结合使用一次分解的结果构建分类器提升日后的执行效率。
## 思考题
刚才SVD分解实验中得到的$U$矩阵,是用户对不同电影主题的评分矩阵。请你使用这个$U$矩阵,进行基于用户或者基于主题(物品)的协同过滤。
欢迎留言和我分享,也欢迎你在留言区写下今天的学习笔记。你可以点击“请朋友读”,把今天的内容分享给你的好友,和他一起精进。

View File

@@ -0,0 +1,77 @@
<audio id="audio" title="51 | 综合应用篇答疑和总结:如何进行个性化用户画像的设计?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e7/d3/e773e974ae5ee310454304f4c41c20d3.mp3"></audio>
你好,我是黄申。今天是综合应用篇的答疑和总结。
在这个模块中,我们讲述了不同数学思想在系统设计和实现中的综合运用。相对于前面几个模块,综合应用的内容更注重实践,也更加有趣。大家对这些内容也提出了很多值得思考的问题。今天,我会讲解其中一个问题,如何进行个性化用户画像的设计?。最后,我也会照例对整个应用篇进行一个总结。
## 个性化用户画像的设计
如今是个性化的时代,互联网和人工智能技术正在把这点推向极致。无论是主动搜索还是进行浏览,用户都希望看到针对自己的结果。
举个例子A品牌的奶瓶在全网是非常畅销的可是对于一位5岁儿子的妈妈来说儿子早已过了喝奶瓶的阶段所以在她输入A品牌后返回“奶瓶”肯定不合适。同时如果她一直在购买A品牌的儿童洗衣液那么返回A品牌的洗衣液就更合理顾客体验也会更好这就是**品类的个性化**。
从另一个场景来看这位妈妈没有输入A品牌而是输入了“儿童洗衣液”如果是A品牌的洗衣液产品排在首页而不是她所陌生的其他品牌用户体验也会更好这就是**品牌的个性化**。
在进行个性化设计之前,最关键的问题是,如何收集和运用顾客的行为数据。
第48节我在讲解查询分类的时候介绍了如何利用用户的搜索行为。而实践中用户个人的行为涉及面更为广泛需要更多细致的分析。通常我们将相应的工程称为“用户画像”。为了让你更好地理解这里我给出一个较为全面的设计概述。
### 如何通过数据生成用户标签?
开发用户画像,首先要解决的问题是:哪些用户数据可以收集,以及如何通过这些数据生成用户标签。
最基本的原始数据包括网站浏览、购物、位置、气候、设备等信息。除了这些原始的数据,我们还可以结合人工的运营,生成一些包含语义的用户标签。这里的用户标签,或者说属性标签,是一个具有语义的标签,用于描述一组用户的行为特征。例如,“美食达人”“数码玩家”“白领丽人”“理财专家”等。对于标签的定义,按照概率统计篇和线性代数篇所介绍的机器学习方法论,既可以考虑采用监督式的分类方法,也可以采用非监督式的聚类方法。
分类的好处在于,可以让人工运营向计算机系统输入更多的先验知识,也可以让标签的制定和归类更为精准。从操作的层面考虑,又可以细分为基于人工规则和基于标注数据。人工规则是指由运营人员指定分类的主要规则。
例如运营人员指定最近1个月至少购买过2次以上母婴产品消费额在500元以上的为“辣妈” 标签。这里规则就相当于直接产生类似决策树的分类模型,它的优势在于具有很强的可读性,便于人们的理解和沟通。但是,如果用户的行为特征过于繁多,运营人员往往很难甄别出哪些具有代表性。这时如果仍然使用规则,那么就不容易确定规则的覆盖面或者是精准度。
另一种方法是使用标注数据通过训练样本来构建分类器。例如通过运营人员挑选一些有代表性的用户对他们的特征进行人工标注然后输入给系统。之后让系统根据分类技术来学习模型可以使用决策树、朴素贝叶斯NBNaive Bayes或支持向量机SVMSupport Vector Machine等等。
不过除了决策树的模型其余模型产生的人群分组可能会缺乏可读性内容很难向业务方解释其结果。一种缓解的办法是让系统根据数据挖掘中的特征选择技术包括我们之前讲解的信息增益IGInformation Gain、开方检验CHI等来确定这组人群应该有怎样的特征并将其作为标签。
除了分类,我们也可以使用非监督式的聚类。这种方法中,运营人员参与最少,完全利用用户之间的相似度来确定,相似度同样可以基于各种用户的特征和向量空间模型来衡量。其问题也在于结果缺乏解释性,只能通过特征选择等技术来挑选具有代表性的标签。
如果我们比较一下分类和聚类的方法会发现分类的技术比较适合业务需求明确、运营人员充足、针对少量高端顾客的管理其精准性可以提升VIP顾客服务的品质。而聚类更适合大规模用户群体的管理甚至是进行在线的AB测试其对精准性要求不高但是数据的规模比较大对系统的数据处理能力有一定要求。
无论是哪种方法,只要我们能获取比较准确的用户标签,那么我们就可以给出用户的画像,刻画他们的主要行为特征。下面我们来看看基于用户画像,可以进行哪些个性化的服务。首先是在搜索中增加个性化因素,相比普通的搜索,个性化的搜索可以投用户之所好,增加搜索结果的点击率、商品的购买转化率等等。具体来说,我们可以在下面这几点下功夫:
第一点个性化的排序根据用户经常浏览的品类和属性对搜索结果中的项目进行个性化的排序开头提到的5岁儿子妈妈的案例体现了这点的核心思想。
第二点,个性化的搜索词推荐。例如,一位体育迷搜索“足球”的时候,我们可以给出“足球新闻”“冠军杯”等相关搜索。而在一位彩票用户搜索“足球“的时候,我们可以给出”足球彩票“等相关搜索。
第三点,个性化的搜索下拉提示。例如,经常购买儿童洗衣液的用户,输入儿童用品的品牌后,在搜索下拉框中优先提示该品牌的儿童洗衣液。
除了搜索个性化还可以运用在推荐系统、电子邮件营销EDMEmail Direct Marketing、移动App的推送等等。对于推荐系统来说在用户画像完善的前提下我们能更准确地找到相似的用户和物品从而进行效果更好的基于用户或基于物品的协同过滤。相对于传统的线下营销电子邮件营销不再受限于印刷和人力成本完全可以做到因人而异的精准化定向投放。
比如系统根据品类、品牌、节日或时令分为不同的主题进行推送。运营人员甚至只用制定模板和规则然后让系统根据用户画像的特征自动的填充模板并最终生成电子邮件的内容。另外随着移动端逐渐占据互联网市场的主导地位掌上设备的App推送变成了另一个重要的营销渠道。从技术层面上看它可以采用和电子邮件营销类似的解决方案。不过内容的运营要考虑到移动设备屏幕尺寸和交互方式的特性并进行有针对性的优化。
有了上述这些设计理念和模块,我们需要一个整体的框架来整合它们。我在这里画了一张框架图,供你参考。
<img src="https://static001.geekbang.org/resource/image/34/4f/34f550e2650e64065d0f104410ce3e4f.png" alt="">
这种架构包括行为数据的收集和分析、聚类、分类、构建画像、缓存等几个主要模块。随着数据规模的不断扩大我们可以选择一些分布式系统来存储用户画像数据并使用缓存系统来升数据查询的效率为前端的搜索、推荐、EDM和App推送等应用提供服务。当然我们还可以利用行为数据的跟踪进一步分析这套画像系统的质量和效果形成一个螺旋式上升的优化闭环。
综合来看,用户画像也许概念上并不复杂,可是一旦落实到技术实施,我们需要综合很多不同领域的知识。从用户标签的角度来说,可能涉及的领域包括监督式和非监督式的机器学习算法,以及相关的特征选择。从系统集成的角度来说,可能涉及的领域包括分布式、缓存、信息检索和推荐系统。这些内容我们在之前的各个模块都有介绍,今天我通过用户画像的设计进行了知识的串联。当然,我这里讲解的方案也只是一种参考,你可以结合自身的需求来进一步的设计和实现。相信经过一定量的项目实践和经验积累,你对这些内容的综合性运用会更加得心应手。
## 综合应用篇总结
在综合应用篇之前,我们分别从基础模块、概率统计模块和线性代数模块出发,详细阐述了不同编程技术背后的数学知识。在综合应用这个模块,我们又从几个非常实用的案例出发,讲解了如何结合不同的编程技术,设计并架构大型的系统,最终为商业需求提供解决方案。
如今的数据系统越来越庞大系统设计时常常会用到缓存系统来提升记录查找的效率。对缓存系统的强烈需求也催生了很多开源的项目例如Memcached和Redis等等这些系统都已经相当成熟。而在这个模块我们同时使用了哈希函数和队列实现了一个最简单的缓存系统。哈希函数确保了查找的高效率而队列则实现了LRU的淘汰策略。通过这两点你就能理解缓存设计的基本原理和方法。
和缓存类似搜索引擎的倒排索引也使用了哈希表结构来提高查询效率。当然倒排索引的功能不仅限于数据对象的快速定位。它本身还能存放很多额外的信息包括词频tf、tfidf、关键词出现的位置等等。在这个模块中我展示了如何利用这些信息实现更为复杂的相关性模型例如向量空间模型、概率语言模型等等。另外倒排索引可以帮助我们过滤掉完全无关的数据大大降低这些模型的计算量。
除了基本的及时性和相关性,搜索引擎还应该按照不同应用的需求进行优化。例如,电商平台的搜索,就和通用型的搜索不一样,对于电商搜索来说,用户更加关注的是商品的品类。我讲解了如何根据商品目录和用户行为反馈,构建查询的分类器。这样,当用户进行搜索的时候,系统首先对用户输入的关键词进行分类,弄清楚用户最感兴趣的品类是哪些,然后再优化商品的排序,最终增加商品搜索结果的相关性。
和搜索引擎同样重要的是推荐引擎。有的时候用户自己不会输入想要查询的关键词而是喜好不断地浏览网页。这个时候推荐技术起到了很关键的作用它可以主动地为用户提供他们可能感兴趣的内容。在这个领域协同过滤是非常经典的算法。我通过代码的实践给你讲解了如何通过矩阵操作实现基于用户和基于物品的过滤。除此之外我们还探讨了如何使用SVD对用户和物品之间的关系进行分解帮助我们找到隐藏在用户和物品之间的潜在因素比如电影的主题。
无论是设计搜索还是推荐系统,我们都可以加入个性化的元素,而这点往往是提升业务的关键。今天,我讲解了用户画像的原理、用户标签的设计和实现、以及如何使用用户画像来给搜索和推荐系统加入个性化。而这个整体方案涉及的技术面是相当广的,你可以结合之前的各期专栏,对每一个环节进行消化和理解。
## 思考题
对各种知识的综合应用对个人能力要求很高,却也是最重要的。我想听你说说,在平时的开发项目中,你有没有结合使用本专栏所讲的不同知识点的经历?能不能和我们说说你在这方面的心得体会?
欢迎留言和我分享,也欢迎你在留言区写下今天的学习笔记。你可以点击“请朋友读”,把今天的内容分享给你的好友,和他一起精进。