Dropbox 正在构建功能,以帮助用户专注于重要的事情。搜索您的内容可能很繁琐,因此我们构建了内容建议,以便在需要时更轻松地找到所需的文件。
我们已经使用现代机器学习(ML)技术构建了此功能,但是从这里开始的过程始于一个简单的问题:人们如何找到他们的文件?哪些类型的行为模式是最常见的?我们假设以下两类是最普遍的:
- 最近使用的文件:您需要的文件通常是您最近使用的文件。当然,这些会随着时间的推移而变化,但最近的过去通常是近期的良好指标。这还可以包括您的文件,这些文件最近有其他人的活动,而不仅仅是您。例如,一位同事刚刚编写了报告的初稿,并与您共享,以便您可以对其进行编辑。即使您尚未打开它,最近与您共享的事实也是一个强烈的暗示,您可能想要立即对其进行编辑。
- 常用文件:您的另一类文件是您一次又一次地返回的文件。这可能包括您的个人待办事项列表、每周会议记录或团队目录。这里与上一个类别有一些重叠 - 如果您正在努力处理下周到期的报告,您可能会非常频繁地打开该报告,并且它也将是您访问过的最新文件之一。
启发式
从对用户访问的文件类型的基本了解开始,我们使用一组简单的启发式方法构建了一个系统,这些启发式方法尝试捕获我们上面描述的行为的手动定义规则。以下是最成功的:
- 新近度:我们可以按时间倒序呈现您的文件,即最新的文件在先。我们今天已经在 dropbox.com 向用户展示了这一点,这是一个很好的改进基准。
- 频率:上周最常用的文件可能与去年的文件不同。为简单起见,我们从这些选项之间的中间地带开始,例如,一个月。我们计算上个月每个文件的访问次数,并显示计数最高的文件。
- 频率:上述两个选项的组合是一种称为频率的启发式方法。它会查找最近有一些活动和/或经常被访问的文件,从而将符合这两个条件的文件排名更高。我们根据分配给每个访问的权重来衰减它很久以前。例如,在过去一周内访问一个文件五次,与你几个月前访问十次的文件相比,你更有可能很快再次使用它。
部署和改进启发式算法
从启发式方法开始,允许人们使用相当简单的实现启动,并根据可理解的行为开始记录用户反应。在我们的例子中,我们能够使用这些简单的启发式方法来支持内容建议的第一个版本。这意味着我们可以专注于构建此功能所需的所有其他部分 - 设计,前端代码,后端代码,为每个用户获取候选文件列表的代码,获取每个文件访问时间相关元数据的代码等。
日志记录对于此初始版本至关重要。一旦我们向某些用户推出了一项功能(我们经常使用我们的功能门控系统Stormcrow对一小部分用户进行初始实验),我们就可以开始看到他们发现这些建议有用的频率,并使用它来帮助我们确定下一步该做什么。我们还可以比较不同的变体(例如,不同的启发式),以查看它们相对于彼此的表现。
在我们的例子中,当我们第一次与用户一起测试试探法时,我们发现了很多问题。例如,有时用户可能只有一个他们最近处理过的文件,在此之前的很长一段时间内没有其他文件。但是,由于我们始终显示前三个建议,因此用户会感到困惑,为什么最近的文件与其他完全不相关的文件一起显示。我们通过阈值启发式方法解决了这个问题:仅当文件的分数高于阈值时才显示文件。
我们可以通过查看显示的建议的已记录数据集、每个建议的分数以及用户单击的建议来选择阈值。通过离线设置不同的阈值,我们可以在精度(单击所显示结果的哪一部分)和召回率(显示的单击结果的一部分)之间进行权衡。在我们的例子中,我们有兴趣提高精度,以避免误报,因此选择了一个相当高的阈值。任何启发式分数低于该值的文件都不会被建议。
我们发现的另一个问题是,这些建议有时包括由用户计算机上安装的程序(如病毒扫描程序或临时文件)访问的文件,但不包括通过用户的直接操作访问的文件。我们创建了一个过滤器,以便在我们的建议中排除此类文件。我们还发现了其他类别的用户行为,这些用户行为尚未包含在最初的启发式方法中。特别是,某些文件以定期方式访问,而我们的启发式方法无法捕获这些文件。这些文档可以是季度文章或每月会议文档等文档。
起初,我们能够在试探法中添加其他逻辑来处理这些问题,但随着代码开始变得越来越复杂,我们决定准备开始构建 ML 模型的第一个版本。机器学习允许我们直接从用户的行为模式中学习,因此我们不需要维护不断增长的规则列表。
机器学习模型 v1
设计ML系统的一种方法是从我们希望系统在预测时的运行方式向后工作。在本例中,我们想要一个相当标准的预测管道:
内容建议系统的 ML 预测管道
步骤如下:
- 获取候选文件:对于每个用户,我们需要一组候选文件进行排名。由于Dropbox用户可能拥有数千甚至数百万个文件,因此对所有文件进行排名将非常昂贵 - 而且也不是很有用,因为很多文件很少被访问。相反,我们可以限制为用户与之交互的最新文件,而不会显着降低准确性。
- 获取信号:对于每个候选文件,我们需要获取与该文件相关的我们感兴趣的原始信号。其中包括其历史记录(打开、编辑、共享等)、用户处理过的文件以及文件的其他属性(如文件类型和大小)。我们还包括有关当前用户和“上下文”的信号(例如,用户所在的当前时间和设备类型),以便结果变得更加个性化,而无需为每个用户训练单独的模型。该模型使用来自大量用户的活动进行训练,这可以保护它免受表示任何给定用户的操作(或揭示行为)的偏见。对于第一个版本,我们只使用基于活动的信号(例如访问历史记录),而不是基于内容的信号(例如文档中的关键字)。这样做的好处是,我们可以一视同仁地处理所有文件,而不必根据文件类型计算不同类型的信号。将来,如果需要,我们可以添加基于内容的信号。
- 编码特征向量:由于大多数ML算法只能对极其简单的输入形式(例如浮点数向量)进行操作,因此我们将原始信号编码为所谓的特征向量。对于不同类型的输入,有一些标准的方法可以做到这一点,我们根据需要进行了调整。
- 分数:我们终于准备好进行实际的排名了。我们将每个文件的特征向量传递给排名算法,获取每个文件的分数,并按该分数进行排序。然后,在向用户显示排名靠前的结果之前,将再次检查权限。
当然,对于每个用户及其文件,这一切都必须非常快速地发生,因为用户正在等待页面加载。我们花了相当多的时间来优化管道的不同部分。幸运的是,我们可以利用这样一个事实,即这些步骤可以为每个候选文件独立完成,从而使我们能够并行化整个过程。此外,我们已经有一个快速数据库,用于获取最近文件及其信号的列表(步骤1和2)。事实证明,这足以满足我们的延迟预算,而无需显着优化步骤3和4。
训练模型
现在我们知道系统在生产中将如何运行,我们需要弄清楚如何训练模型。我们最初将问题描述为二元分类:我们要确定给定文件现在是否将被打开(“正”)或不(“负”)。我们可以将此事件的预测概率用作分数来对结果进行排名。对于训练管道,一般准则是尝试使训练方案尽可能接近预测方案。因此,我们确定了以下步骤,与我们的预测管道紧密匹配。
- 获取候选文件:作为整个系统的输入,正确执行此步骤对于最终准确性至关重要,尽管看起来很简单,但由于多种原因,它是最具挑战性的步骤之一。
- 从哪里获得正面示例:我们的日志记录为我们提供了打开的历史文件列表。然而,我们应该把哪个开场算作我们的正面例子呢?来自此功能的启发式版本的数据,还是来自常规 Dropbox 文件打开的数据?前者是更“相关”的候选者,因为我们知道用户在我们将部署此模型的上下文中打开了这些文件;然而,仅在来自单个上下文的数据上训练的模型在更广泛的上下文中使用时可能会受到隧道视觉的影响。另一方面,更一般的文件历史记录更能代表所有类型的用户行为,但可能包括更多的噪音。我们最初使用前一种方法,因为日志更容易处理,但是一旦我们有了训练管道,就切换到两者的混合(重点放在后者上),因为结果要好得多。
- 从哪里获得负面示例:从理论上讲,用户Dropbox中未打开的每个文件都是负面的!但是,请记住我们的准则(“让您的训练场景尽可能接近您的预测场景”),我们使用正文件打开时的最近文件列表作为可能的否定数集,因为这与预测时将发生的事情最接近。由于负片列表将比正数集大得多(ML系统通常不能很好地做到这一点),因此我们将负片进行子采样,仅将负数采样为比正数大的一小部分因子。
- 获取信号:这与预测场景类似,只是它需要访问历史数据,因为我们需要在每个训练示例时出现的信号。为了促进这一点,我们有一个Spark集群,它可以对历史数据进行操作。例如,我们的信号之一是“新近秩”,它是最近打开的文件列表中文件的排名,按上次访问时间排序。对于历史数据,这意味着重建此列表在给定时间点的外观,以便我们可以计算出正确的排名。
- 编码功能:同样,我们将其与生产中完全相同。在我们的迭代训练过程中,我们从非常简单的数据编码开始,并根据需要随着时间的推移使它们更加复杂。
- 训练:这里的主要决定是使用哪种ML算法。我们从一个非常简单的选择开始:线性支持向量机(SVM)。这些具有训练速度极快,易于理解的优点,并且带有许多成熟和优化的实现。
此过程的输出是一个经过训练的模型,它只是一个包含每个特征维度(即浮点数)的权重系数的向量。在这个项目的过程中,我们尝试了许多不同的模型:在不同的输入数据上训练,使用不同的信号集,以各种方式编码,以及使用不同的分类器训练参数。
请注意,对于此初始版本,我们针对所有用户训练了一个全局分类器。使用线性分类器,这足以捕获通用行为,例如文件使用的新近度和频率,但不能适应每个用户的偏好。在本文的后面部分,我们将介绍 ML 系统的下一个主要迭代,它可以做得更好。
指标和迭代
一旦我们有了一个有效的训练和预测管道,我们就必须弄清楚如何将最好的系统交付给用户。然而,“最好”到底是什么意思呢?在 Dropbox,我们的 ML 工作以产品为导向:首先,我们希望改善用户的产品体验,而我们构建的任何 ML 都是为了满足这一点。因此,当我们谈论改进系统时,最终我们希望能够衡量用户的利益。
我们从定义产品指标开始。就内容建议而言,主要目标是参与。我们使用几个指标来跟踪这一点,所以我们决定从简单的开始:如果我们的建议有帮助,我们希望用户更多地点击它们。通过计算用户点击建议的次数除以向用户显示建议的次数,可以直接衡量这一点。(这也称为点击率或点击率。为了获得统计显著性,我们将在一两周内向用户子集显示来自不同模型的建议,然后比较不同变体的CTR。从理论上讲,我们可以每隔几周发布一次模型,并随着时间的推移慢慢提高参与度。
在实践中,我们遇到了一些主要问题(以及一些次要问题)。首先是如何将由于产品设计的变化与ML模型的变化而导致的点击率的增加(或下降)归因于此。大量的研究和行业案例研究表明,即使是用户体验(UX)中看似很小的变化也会导致用户行为发生重大变化(因此A / B测试的流行)。因此,当一个工程师团队专注于改进 ML 模型时,另一个由工程师、设计师和产品经理组成的团队则专注于改进功能的设计。此设计迭代的一些高级示例如下所示:
鉴于我们可能会为新版本更改UX和ML模型,我们如何知道哪些更改影响了点击率(以及影响了多少)?事实上,这个问题比我们最初想象的还要复杂。早期,我们发现在相当多的情况下,我们向用户建议正确的文件,但他们没有从建议部分单击它们;相反,他们会以其他方式导航到该文件,然后从那里打开它们。虽然造成这种情况的原因有很多——更熟悉现有的文件访问方式,缺乏关键的上下文信息(例如父文件夹名称,我们在设计迭代中添加了这些信息)等——但最终结果是,我们不能依赖CTR数字作为模型准确性的唯一衡量标准。我们需要找到一个代理指标,可以更直接地捕获模型的准确性,并尽可能独立于UX。
切换到代理指标还有另一个令人信服的理由:迭代速度。运行A / B测试几周听起来并不长,但这只是每个实验完全运行所需的时间。之后,我们必须分析结果,提出一系列改进建议,实施它们,最后训练新模型。所有这些都意味着发布新版本可能需要一个多月的时间。为了取得重大进展,我们需要大幅缩短这段时间,理想情况下,我们可以“离线”衡量,而无需向用户发布。
命中率
我们的训练过程让我们测量了保留的训练数据集的各种数量,例如精度和召回率(如前所述)或分类器的准确性,但是当我们实际向用户启动模型时,我们没有发现这些指标与CTR之间的强相关性。相反,我们提出了一个满足我们所有标准的“命中率”指标。对于任何给定的建议,我们检查了用户是否在随后的一小时内访问了该文件,无论他们如何到达该文件。如果是这样,我们将其视为“命中”。然后,我们可以计算每个建议的“命中率”(命中建议的百分比)和每个会话命中率(至少有一个建议命中的会话百分比)。我们可以在线和离线(通过查看用户行为的历史日志)来衡量这一点。
事实证明,该指标不仅对于更快地迭代ML,而且对于诊断UX问题都是无价的。例如,当我们第一次从将内容建议显示为文件列表转变为一组缩略图时,我们预计点击率会提高。我们不仅在较小的垂直空间中显示文件,还显示缩略图,以便用户更轻松地识别文件。然而,点击率实际上下降了。我们尝试了几种不同的设计迭代,并使用我们的命中率指标来验证建议的质量是否不受指责。我们发现了许多已纠正的问题,例如前面提到的缺少文件夹名称。
现在,我们既有产品指标又有代理指标来衡量模型的准确性,我们可以在改进系统的UX和ML方面取得快速进展。但是,并非所有的改进都来自查看这些指标。如果您将指标视为衡量我们绩效的“广泛但肤浅”的指标,那么将“深层但狭义”的衡量标准视为补充信息来源也是有帮助的。在这种情况下,这意味着要详细查看对一小部分数据(属于我们自己团队成员的数据子集)的建议。
通过广泛的内部测试,我们发现了许多其他问题,包括许多影响管道开始的问题:我们使用了哪些数据。以下是一些有趣的示例:
- 短促点击:我们发现,我们的训练数据包括用户打开文件夹中的文件,然后使用向左和向右箭头键滚动浏览该文件夹中的其他文件的情况。所有这些最终都会被计为正训练样本,即使这种行为不适用于主页,我们一次只显示几张图片。因此,我们设计了一种简单的方法来标记这些“短点击”,并从我们的训练数据中过滤掉它们。
- 最近的文件活动:常用的 Dropbox 功能是自动将屏幕截图保存到 Dropbox。来到主页的用户会期望立即看到这些内容,但是我们的功能管道的响应速度不够快,无法选择这些内容。通过调整各种组件的延迟,我们也能够将这些组件包含在结果中。
- 新创建的文件夹:类似地,用户希望看到新创建的文件夹显示在主页上,因为这些文件夹通常是专门为将文件移入而创建的。在这种情况下,我们不得不暂时使用启发式方法来检测此类文件夹并将其合并到建议中,因为我们对文件夹的信号类型要有限得多(并且与文件不同)。
机器学习模型 v2
有了这些新收集到的关于我们当前系统在哪些方面做得不好的知识,我们开始在整个培训管道中进行重大改进。我们从训练数据中过滤掉了短点击,以及某些其他类别的文件,这些文件被虚假地建议给用户。我们开始将其他类型的信号集成到训练过程中,这可能有助于移动我们的命中率指标。我们重新设计了功能编码步骤,以更有效地从原始信号中提取相关信息。
事实证明,Dropbox的另一个机器学习投资领域对于改进内容建议非常重要:学习常见类型实体的嵌入。嵌入是一种将一组离散对象(例如每个 Dropbox 用户或文件)表示为浮点数的紧凑向量的方法。这些可以被视为高维空间中的向量,其中常用的距离度量(如余弦或欧几里得距离)捕获对象的语义相似性。例如,我们希望具有相似行为的用户或具有相似活动模式的文件的嵌入在嵌入空间中“接近”。这些嵌入可以从我们在Dropbox的各种信号中学习,然后作为输入应用于任何ML系统。对于内容建议,这些嵌入可显著提高准确性。
最后,我们将分类器升级到神经网络。我们的网络目前还不是很深,但仅仅通过能够对输入特征的组合进行非线性操作,它就比线性分类器具有巨大的优势。例如,如果一些用户倾向于在早上在手机上打开PDF文件,下午在桌面上打开PowerPoint文件,那么使用线性模型(没有广泛的功能组合或增强)很难捕获,但神经网络可以很容易地拾取。
我们还稍微改变了我们构建训练问题的方式。我们没有采用二元分类,而是改用学习到排名(LTR)公式。这类方法更适合我们的问题场景。LTR 不会针对是否单击文件进行优化(完全独立于其他预测),而是针对单击的文件排名高于未单击的文件进行优化。这具有以改进最终结果的方式重新分配输出分数的效果。
通过所有这些改进,我们能够显著提高命中率并提高整体点击率。我们还为将来的其他改进奠定了基础。
确认
该项目是产品、基础架构和机器学习团队之间高度协作的跨团队工作。虽然这里有太多的人需要单独列出,但我们想特别赞扬Ian Baker构建了系统的大部分内容(以及改进了管道中的几乎所有其他部分),以及Ermo Wei领导了大部分ML迭代工作。