大家熟知推荐系统在电商、社交网络、音乐、广告等上的应用,但是对于聚合阅读如何做推荐呢?TMT 是由一群极客发起的技术博客,本文介绍了这群 Tech Geek 给读者推荐文章的方法。
我们正处于信息过载的时代,很多公司都遇到了信息过载的问题。推荐系统应运而生,它不仅可以从大数据中提取价值,而且还能有效的避免信息过载。众所周知,数据科学的目标是从大数据中创造价值。
然而,数据科学也应该满足第二个目标即避免信息过载。
一个典型的可以满足这两个目标的系统就是推荐引擎。不仅是像 Amazon 这样的网上商店,同样的还有流媒体服务如 Netflix 公司也遭到了信息过载的困扰。客户可能很容易迷失在其庞大的(百万)产品或电影中。推荐引擎通过呈现给用户可能的选择帮助用户从繁多的产品中缩小选择范围。
当然这些推荐引擎可以随机的展现给用户一些可能的选择,但这并没有真正降低信息过载。因此,这些推荐引擎通过应用统计科学来给用户展现更匹配他们期望的结果。例如,一个观看了《Frozen》的 Netflix 用户会从 Pixar 看到类似的儿童电影推荐结果。
Netflix 的推荐系统示例
TMT 目前拥有超过 50 个类别的文章,覆盖了众多类别的主题,从数据科学到 ReactJS 编程。浏览完网站上所有的文章是非常耗时的,尤其是在文章的数量一直增长的情况下。同时读者很可能仅仅对他们所熟悉的领域的文章感兴趣。如果一个推荐引擎能够筛选出用户感兴趣的文章,那么它就可以被认为是能从数据中创造价值并避免信息过载的那一类系统。
两种类型的推荐系统
粗略地讲,我们可以将推荐引擎划分为两种类型:协同过滤和基于内容的推荐。
就像维基百科说的:协同过滤是一个使用涉及多个代理,观点和数据源之间的协作技术模式来过滤信息或模式的过程。在 TMT 案例中,这意味着在多个读者中找到一些规律。如果一群读者对特定系列中的文章感兴趣,当一个读者开始读这个系列中的一篇文章,那么他有很大的概率会对这个系列中的其他文章感兴趣。因此,可以基于其他用户的阅读行为来给类似的用户进行推荐。
基于内容的推荐引擎是不同的,因为它们是以产品的属性进行推荐。在我们的案例中产品就是 TMT 的文章,属性则是文章中的关键字。如果一个用户在读的文章中包含 'Google Analytics' 和 'Tag Manager' 的关键字,那么这个用户很有可能也喜欢包含这些关键字的其他文章。因此,基于内容的推荐引擎会推荐包含这些关键字的文章。
值得注意的是最近从 "Geek" 到 "TheMarketingTechnologist" 的升级中,一个非常简单的基于内容过滤推荐方法被集成到 TMT 中。就是在每篇文章的下面有 5 篇其他相关的文章展示给用户供他们继续阅读。推荐的文章就是包含用户在读文章中任意一个标签的最新发表的 5 篇文章。在这个简单的例子中,标签可以当作产品的一个属性。
协同过滤和基于内容的推荐背后的原理
这两类系统各有利弊。基于内容的推荐系统将推荐的文章限定在基于一组特征类似的文章中。例如,在给包含 'Google Analytics' 这样具体特征的文章进行推荐的时候会在其他文章中查找类似的关键字。
然而,一个包含类似的分析具 'Snowplow' 这样具体特征的文章,却不太可能被推荐出来。实际上用户可能对这两类文章都感兴趣,因为他们都属于分析这个主题。因此,基于内容的推荐系统并不善于发现隐藏的模式。协同过滤推荐系统在寻找隐藏模式这点上是优于基于内容的推荐系统。协同过滤着眼于用户的阅读行为,不专门关注用户阅读文章的内容。所以如果一个用户阅读了关于数据科学和转化率优化 (CRO),尽管那些 CRO 的文章的内容和数据科学是截然不同的,协同过滤推荐系统也会向数据科学的读者们推荐关于 CRO 的文章。协同过滤的最大的缺点是,它需要大量的历史用户阅读行为数据,以便找到这些模式。基于内容的推荐可以在不给或者少量的历史数据的条件下完成,因此更容易实现。
协同过滤推荐系统的准备前提
在接下来的两篇博客中,我们将会实现一个基于内容推荐系统和基于协同过滤算法的推荐系统,并分析推荐结果。然而,为了能够实现目标,我们首先需要准备一些前提物料。对于协同过滤推荐系统这意味着我们需要实现得到用户阅读文章行为的方法。我们的同事 Erik Driessen 已经实现了通过使用谷歌分析 (Google Analytics) 并且分配客户端 ID 来记录用户阅读行为的方法。Simo Ahava 在他的博客《在谷歌分析中存储客户端 ID》解释了如何有效的实现它。
此外,由于 Erik 已经在文章的内容上增加了 'Enhanced Ecommerce' 插件,这样我们就可以追踪到用户是否完整的阅读了这篇文章。最后,在谷歌分析中可以创建自定义报表,展示客户端 ID 和他们已经阅读过的文章。
需要注意的是,目前跨设备的解决方案尚未实现。因此,如果用户使用不同的设备继续阅读文章或着中间清除了 cookie,后续的行为将不能和他前期的阅读行为联系起来。
谷歌分析中一个自定义的用户层面的阅读行为报表示例
基于内容的推荐系统的准备前提
在基于内容的推荐中,显然我们会需要所有 TMT 文章内容。有多种实现的方法。其中的一个将是直接从数据库中提取这些文章的文本。然而,因为我们是 Geek,通过编写一个 Python 脚本,可以自动检索文章和相应的指标如作者和类别,这将是一件更酷的事情。因此,我们创建了从 TMT 网站复制文章的 Python 脚本,具体分为两个步骤。步骤 1 和步骤 2 的代码在这里
1. 创建一个 TMT 全量文章的列表
可以使用 Python 的 urllib2 库加载网页源代码。用 urllib2.urlopen 这个命令可以加载我们自己 TMT 博客的前端页面的源代码。这个首页总是展现最新发表的 10 篇文章。使用 BeautifulSoup 库,我们可以很容易通过在 DOM 中搜索并提取所有带 class ="post" 的 article 元素,并将它们存储在一个 Pandas dataframe 中。此外,在每个这样的元素内部,我们还可以通过搜索相应的 DOM 元素来提取作者和标签等元素。
因为只有十篇最新的博客文章在首页显示,为了获取更多博客文章,我们还需要检查在页面底部是否有 Older posts > 按钮。同样的,可以通过搜索相应的 DOM 元素,如 class="older-posts" 来完成。在更早博客文章区块中可以通过使用 get 函数从 href 属性值中提取到下一个页面的链接。在每个页面上我们重复上述过程。最后,我们将得到一个包含所有文章的名称、标签、作者和指向文章内容链接的数据帧。
2. 检索每篇文章内容
在步骤 1 中,我们保存了一个到每一篇文章的链接,所以我们可以下载每篇文章的全部内容。但是存在一个特殊的问题,即每篇博客文章的内容是通过 JavaScript 加载的。因此,如果我们使用的 urllib2 加载文章的静态资源,我们将无法得到文章的内容。为了执行 JavaScript 代码来加载文章内容,我们实际需要通过 Web 浏览器进行渲染出文章的内容。幸运的是,这可以通过使用流行的 Selenium 库来达到目的。使用 Python 的几行代码并结合 Selenium 库可以打开 Firefox 浏览器,定位到相应的 URL 并渲染页面。然后就可以在 DOM 中搜索我们想要的信息,例如博客文章的内容。
需要注意的是,因为使用 Selenium 执行所有的 JavaScript 代码,这也意味着谷歌分析的代码也会被执行。因此,明智的做法是采取措施防止产生脏数据。例如,在 GA 过滤器列表加入您的 IP 地址或者通过谷歌安装插件。还要注意的是,实际上并不需要在 Firefox 浏览器中把每个渲染出来页面呈现在我们面前。你也可以使用一个轻量级的驱动程序,如 Phan'tom'JS 这使得可以在后台渲染页面而减少视觉上额外的开销。
这就是截至到现在建立推荐系统需要的准备前提。
下面将介绍度量推荐结果相似度的两种方法,包括欧几里德距离和余弦相似法。还将详细介绍计算博客相似度时对博客文章向量化的三个步骤:分词、词频统计、加权。直接在这个向量化后的高维度空间中进行推荐效果不一定是最优的,通过使用 SVD 或 PCA 降维算法可以得到更好的推荐效果。
基于内容的推荐系统背后的概念
目标是为我们的读者推荐 TMT 上的其他文章。我们假设读者已经完整的看过一篇喜欢的文章,并希望阅读更多类似的文章。因此,基于用户的历史阅读行为,我们想要打造一个基于内容的、可以推荐新的类似文章的推荐系统。
为了得到准确和有用的推荐结果,我们希望用一个数学量化的方法来找到最好的推荐结果。否则,我们的网站会减少对读者的吸引力,进而他们会离开 TMT。因此,我们希望找到那些彼此相似的文章, 从而它们就是“彼此相近”的。也就是说,这些文章之间的“距离”是近的,一个较小的距离意味着更多的相似性。
一篇 TMT 文章在 2 维空间可视化的例子
那么,这个距离的概念究竟是什么?
假设我们可以将任意一篇 TMT 的文章映射为 2 维空间中的一个点。上面的图提供了一个将 76 篇 TMT 文章映射在 2 维空间中的例子。此外,我们假设两点越接近它们彼此就越相似。因此,如果把用户在读的文章当作 2 维空间中的一个点,则在这个空间中离这个点距离越近的点代表的那些文章可以被视为好的推荐结果,因为它们是相似的。这些点之间的距离有多远可以使用欧几里德距离公式来计算。在 2 维空间这个距离的公式可以简单地归纳为勾股定理。
需要注意的是,这个距离公式也适用于更高维度的空间,例如 100 维空间(尽管超过 3 维空间的时候我们已经很难想象了)。
欧几里德距离公式
余弦相似性是另外一种计算两点的相似性的公式。
想象一下,为每篇文章分配一个它倾向的方向。余弦相似函数使用的是两篇文章指向的方向差,即两篇文章方向之间的角度差。例如,在 2 维空间中,一篇文章方向是正北向和另外一篇文章是正西向,则他们在方向上的角度差是 -90 度。把这种角度差归一化在区间 [-1,1],其中 1 表示的是相同的方向,因此是完美的相似,-1 表示完全相反的方向,因此是完全没有相似性的可言的。
我们使用余弦相似性量度 TMT 文章的相似度是因为文章之间的方向比文章之间的精确距离更重要。此外,我们尝试了两个度量指标(欧氏距离和余弦相似性)并且余弦相似性度量指标表现的更好。
两个新的挑战
上面我们介绍了度量文章之间相似性的方法的概念。然而,现在两个新的挑战出现了:
我们如何把每篇文章映射成 2 维空间的点?
空间中的两点间的距离提供了有关文章的相似性的指标, 那么我们如何绘制这些代表文章的点之间的距离?
一篇文章可通过在 2 维空间中分配给它的坐标来绘制,例如一个 x 和 y 坐标。这意味着首先需要把我们的文章转换为数字格式,然后将其归约为两个值,例如 x 和 y 坐标。因此,我们首先要阐述一种通过应用特征提取算法来科学的量化 TMT 文章中文本的方法。
需要注意的是,我们将在下面讨论的处理文本的特征提取方法是专门为处理 TMT 中的文章而设计的。你可以想象一下,如果你正在构建一个针对电话的基于内容的推荐的系统,你可能需要一种不同的算法来将电话的属性(内容)转换为数字格式。
将 TMT 文章变换为数字格式
TMT 的文章是由大量的单词组成的短语和标点符号组成,需要被变换为数字向量,这个变换的过程中不允许损失文章的内容。我们希望最好是一个固定大小的向量。
为什么我们想要固定大小?回想上面提到的 2 维空间的例子。将 2 维空间中的点和 100 维空间中的点进行比较是很荒谬的,对吧?此外,如果我们讨论两篇文章某些方面的相似性就需要用相同的方式来描述它们。
为了得到固定大小的向量我们将创建一些特征,例如那些文章可以测量的属性。在文本分析领域中,特征往往指的是单词、短语、数字或符号。所有的文章都可以被一个相同的特征集合来量化并表示,进而所有的文章就被变换为固定大小的数字向量。将文本变换为数字向量的整个过程称为特征提取,通常分为 3 个步骤:分词,计数和加权。
文本向量化的例子
1. 分词
在特征数字化的第一步是分词。在文本分析中,分词被认为是从大量的文本中得到有意义的词汇单元。例如,物理速度可以用米 / 秒来表示。在文本分析中,文本的长字符串可以用词汇单元来表示。这些词汇单元通常对应单词。因此,获取句子 I am happy, because the sun shines 词汇单元的一种简单的分词方法就是通过空格进行分割。用这种分割方法得到 7 个词汇单元, 例如,I, am, happy,, because, the, sun, shines。分词之后,原始的句子就可以用这些词汇单元来表达了。 这种简单的分词方法引入了新的问题:
第一,这种方法不会过滤掉任何标点符号,从而导致了 happy, 这个词汇单元是以逗号结尾的。这也就表明 happy, 和 happy 是两个不同的词汇单元,尽管他们实际上代表的是同一个单词。因此,我们需要过滤掉所有的标点符号,因为标点符号几乎和我们的文章中单词的含义是不相关的。需要注意的是,在其他场景中,标点符号可能和单词的含义是相关的。例如,在分析 Twitter 中的消息时,标点符号也是很重要的,因为它们通常被用来创建可以表达很多情感的笑脸。笑脸这个例子强调了每种数据源需要有自己的特征提取方法的事实。
第二,利用简单的分词方法,可能会获得的分词结果包含 works 和 working。然而这些都是同一个单词的不同形式,例如 to work。当一个单词是另一个单词的复数形式时,同样也是上面的这种情况。在我们的基于内容的推荐系统中,我们假设这些单词不同的形式代表相同的含义。因此,这些单词可以归约到它们的词干形式并当作同一个词汇。为了达成这个目标,我们需要一种词干提取算法,可以把每个单词变换成其词干形式。需要注意的是这种词干提取算法每种语言都是专用的。幸运的是,已经有一些可用的免费程序库, 例如 NLTK 这个库可以帮我们完成这个工作。
第三,通常文本分析的存在的问题是如何处理单词的组合或者单词的否定语意。例如,如果仅仅用单独的单词 Google 和 Analytics 并不能表明我们正在谈论的是 Google Analytics 这个产品。因此,我们需要创建两个或三个连续单词的短语,分别称为二元的 bi-gram 和三元的 tri-gram。I like big data 这个句子,可以被转换为 I, like, big,data, I like, like big, big data, I like big 和 like big data 这些词组。
需要注意的是,这种分词方法并没有把单词所在的位置以及顺序考虑进来。例如,分词之后就不能确定单词 big 在原始的句子或文章出现的位置了。同时,单词自身也没有携带它前面和后面单词的任何信息。
因此,我们在分词的过程中丢失了原始句子的某些信息。关键点是在保存一系列分词的同时也采集尽可能多的句子的原始信息。用我们的分词算法将会丢失原始句子的结构信息。有其他会把单词在文章中出现的位置和顺序考虑进来的分词算法。
例如,Part-Of-Speech (POS)方法还增加了额外的标注信息,例如词性,如词汇是否是一个动词,形容词,名词或专有名词。然而,我们假设词性标注不会大大增加我们推荐引擎的推荐效果,因为词语在句子中的顺序在推荐的时候不是很重要。
2. 词频统计
在第二步骤中,需要对每篇文章中的每个词汇单元的频率进行统计。这些词频数据将用于下一步骤,为文章中的词汇加权。此外这些词汇的计数信息将被用于稍后执行初步的特征选择,例如用来减少特征的维度。注意,文本分析的典型特性是,大多数的词汇仅在几篇文章中使用。因此,大多数的词汇在一篇文章中出现的频率为零。
3. 加权
在最后步骤中,使用前面步骤得到的词汇和词频把所有文章转换为数字格式。把每篇文章编码成一个数字向量,向量的元素代表的就是步骤 1 得到的词汇单元。此外,词汇加权过程使用从第 2 步得到的词频。当给词汇赋完权重之后,它不在被认为是词汇而是被当作一个特征。因此,一个特征代表一个词汇,文章特征的的值是由加权方法确定的权重。
有多种可选的特征加权方法:
最基础的加权方法就是特征频率 (FF).FF 加权方法简单的使用文章中每个词汇单元出现的频率作为它的权重。例如,给定词汇的集合{mad, happy},句子 I am not mad, but happy, very happy,加权得到的结果向量就是 [1,2].
Feature Presence (FP) 是一种类似的基础加权方法。在 FP 算法中,一个词汇被简单的赋予一个二元变量,1 代表这个词汇在这篇文章中出现,0 则相反。前面的例句用 FP 算法得到的结果向量是 [1,1],因为这两个词汇均在这个句子中出现了。使用 FP 加权算法一个额外的优势就是我们将会得到一个二元数据集,不会遇到扩展性的问题。扩展性问题可能在算法中遇到,例如在复杂计算中像计算特征值或 Hessian 矩阵时。
一种更加复杂的特征加权算法就是词频和逆文档频率 (TF-IDF) 加权算法。这个方法使用两种类型的评分,例如词频评分和逆文档频率评分。词频评分就是计算这个词汇在这篇文章中出现的频率。逆文档频率评分可以由总的文章数目除以包含该词语文章的数目,再将得到的商取对数得到。当这两个评分相乘时,如果得到的值比较大,表示该词语在一小部分文章里频繁地出现很多次,如果值较小则表示该词语在很多文章里都有出现。
在我们的基于内容的推荐系统中,我们将使用 FP 加权算法,因为这个方法非常快并且在推荐效果上不会比其他加权方法差。此外,用它得到的稀疏矩阵,在计算的时候有额外的优势。
降维
应用上述特征推荐方法后,我们会得到一个特征列表,用这些特征可以把每篇 TMT 文章数字化。我们可以在这个高维空间中计算出任意两篇文章间的相似度,但我们不倾向于这么做。因为有些特征不太可能表达文章之间的相似度,就像 is 和 a,可从特征集合中移除掉,进而显著减少特征的数量,并提高的推荐结果的质量。此外,更小的数据集提高了推荐系统的速度。
文档频率 (DF) 选择是用于降维最简单的特征选择方法,是很多文本分析问题的必需。我们首先删除最常见的英文停用词,例如 the,a,is 等在判断文章之间相似度时并不能提供太多信息的单词。做完这些,那些有着很高和很低的文档频率的特征也会被从特征集合中移除掉,因为这些特征也不太可能在判别文章相似度时起太大的作用。
回想一下,在本文的开头,我们可以想象在 2 维空间中的文章。然而,应用 DF 后我们仍处于一个非常高维的空间。因此,我们打算使用一种算法将高维空间降到 2 维空间,这让我们可以巧妙地将文章可视化。此外,在 2 维空间中很容易理解推荐系统的原理,因为距离的概念是很直观的。用来把我们带回了 2 维空间的算法是奇异值分解(SVD)。另外一种不同的算法是主成分分析(PCA)。我们在这篇文章中不会全面的介绍这些算法是如何工作的。总之,两者的本质都是找到最有意义的基,用这组基我们可以重造原始数据集和捕获尽可能多的原始方差。幸运的是,scikit-learn 已经包含了内置版本的 SVD 和 PCA 算法,因此我们可以很容易使用。
还有很多用来降维的方法,如信息增益和 X 平方准则或随机映射,为了简单起见,我们坚持使用 IDF 特征选择方法和 PCA 降维方法。
一个用 SVD 方法对 Iris 数据降维的例子。用 SVD 将从 3 维降到 2 维,并且不会丢失太多的信息。
标记推荐结果
应用所有上述步骤之后,所有的 TMT 文章被归约成一个 2 维空间的坐标。对于任意两篇文章,现在可以计算两个坐标之间的距离。我们现在唯一需要的一个函数,这个函数可以将当前的文章作为输入,返回与这篇文章固定数量的距离最小的 TMT 文章!使用欧氏距离公式这个函数是很容易实现的。
让我们构造一些场景来测试我们的基于内容的推荐系统。假设我们是刚刚读完 Caching $http requests in AngularJS 这篇文章的读者, 基于内容的推荐系统提供了以下 TMT 文章推荐结果供我们继续:Angular 2: Where have my factories, services, constants and values gone? 听起来很合理,对吧?下表提供的更多场景中的推荐结果。
总结
在最终的推荐系统中,我们使用 SVD 算法将维度减少到 30 的而不是 2。这样做是因为当我们将其降低到一个 2 维空间,很多特征的信息将会丢失。因此,文章的相似性量度指标也是不太可信的。我们使用 2 维空间只是做可视化的。我们还对 TMT 文章的标题和标签应用了特征提取算法。这显著的改善了推荐结果的质量。
我们的推荐系统的参数是直观选择的,并没有做优化。