2017年12月,谷歌和麻省理工学院的研究人员发表了一篇关于他们在“学习型指数结构”中的努力的挑衅性研究报告 。 这些研究非常令人兴奋,正如作者在摘要中所述:
“我们相信,通过学习模型取代数据管理系统核心组件的想法对未来的系统设计有着深远的影响,而且这项工作只是提供了可能的一瞥。”
事实上,谷歌和麻省理工学院研究人员提出的结果包括可能表明索引世界中最可敬的中坚力量新竞争的结果:B-树和哈希图。 工程界对机器学习的未来感到ab;不安; 因此这篇研究论文已经在Hacker News,Reddit和全世界工程界的大厅里进行了探讨。
新的研究是重新审视一个领域基础的绝佳机会; 而且作为索引的根本(并且已经被充分研究)的东西往往不是经常性的突破。 本文作为哈希表的简介,简要介绍了什么使得它们快速和慢速,以及直观的机器学习概念,这些概念正在应用于文章中的索引。
(如果您已经熟悉哈希表,碰撞处理策略和哈希函数性能考虑因素;您可能想要跳过或跳过本文并阅读本文末尾链接的三篇文章,以深入了解这些内容主题。)
为了回应谷歌/麻省理工学院合作的结果,Peter Bailis和一个斯坦福研究小组回顾了基础知识,并警告我们不要扔掉我们的算法书 。 Bailis'和他在斯坦福大学的团队重新创建了学习型索引策略,并且通过使用名为Cuckoo Hashing的经典哈希表策略,能够在没有任何机器学习的情况下获得类似结果。
在对谷歌/麻省理工学院合作的单独回应中,托马斯纽曼描述了另一种实现类似于学习指标策略的性能的方式,而不放弃经过良好测试并且很好理解的B树。 当然,这些对话,比较和要求进一步研究的内容,正是谷歌/麻省理工学院团队激动不已的原因; 在他们写的论文中:
“重要的是要指出,我们不主张用学习的索引结构来完全取代传统的索引结构。 相反,我们概述了一种建立索引的新方法,它补充了现有的工作,并且可以说为一个几十年的领域开辟了一个全新的研究方向。“
那么,什么是大惊小怪? 散列图和B-tree是否注定要成为老龄化的大厅? 计算机是否会重写算法教科书? 如果机器学习策略真的比我们所知道和喜爱的通用索引更好,那么它对计算机世界意味着什么呢? 学习指数在什么情况下会超越旧的备用指数?
为了解决这些问题,我们需要了解索引是什么,他们解决了什么问题,以及使索引更适合其他索引。
什么是索引?
索引的核心是让事情更容易找到和检索。 早在计算机发明之前,人类就一直在索引事物。 当我们使用组织良好的文件柜时,我们使用的是索引系统。 全卷百科全书可被视为一种索引策略。 杂货店里的标签过道是一种索引。 任何时候我们都有很多东西,我们需要在集合中找到或识别特定的东西,索引可以用来使事情变得更容易。
亚历山德里亚大图书馆的第一个图书管理员Zenodotus负责组织图书馆的盛大收藏。 他设计的系统包括按照流派将书籍分组入房间,并按字母顺序搁置书本。 他的同行Callimachus走得更远,引入了一个名为pinakes的中央目录,它允许图书管理员查找作者,并确定该作者的每本书在图书馆中的位置。 (你可以在这里阅读更多关于古代图书馆的信息 )。 自从1876年发明了杜威十进制系统以来,图书馆索引中又创造了更多的创新成果。
在亚历山大图书馆,索引被用来将一段信息(书或作者的名字)映射到图书馆内的一个物理位置。 尽管我们的计算机是数字设备,但计算机中的任何特定数据实际上都驻留在至少一个物理位置 。 无论是本文的文本,最近的信用卡交易记录还是惊吓猫的视频,数据都存在于计算机上的某个物理位置。
在RAM和固态硬盘驱动器中,数据存储为通过一系列许多晶体管传输的电压。 在较老的旋转硬盘驱动器中,数据以磁盘格式存储在磁盘的特定圆弧上。 当我们将计算机中的信息编入索引时,我们创建了一些算法,将部分数据映射到计算机中的物理位置。 我们称这个地址为地址 。 在计算机中,被索引的东西总是数据的一部分,索引用于将这些数据映射到它们的地址。
数据库是索引编制的典型用例。 数据库旨在保存大量信息,并且一般而言,我们希望高效地检索这些信息。 搜索引擎的核心是互联网上可用信息的巨大索引。 哈希表,二叉查找树,尝试,B树和布隆过滤器都是索引的形式。
很容易想象在亚历山大大型图书馆的迷宫式大厅里找到具体的东西的挑战,但我们不应该理所当然地认为人类生成的数据的规模呈指数级增长。 互联网上可用的数据量远远超过任何时代的任何单个图书馆的数量,Google的目标是将所有数据都编入索引。 人类为索引创造了许多策略; 在这里,我们研究了所有时间最多产的数据结构之一,这恰好是一个索引结构:散列表。
什么是散列表?
初看起来,哈希表是基于称为哈希函数的简单数据结构。 有许多种散列函数的行为有些不同并且用于不同的目的; 对于下面的部分,我们将只描述散列表中使用的散列函数,而不是加密散列函数,校验和或任何其他类型的散列函数。
散列函数接受一些输入值(例如一个数字或一些文本)并返回一个整数,我们称之为散列码或散列值。 对于任何给定的输入,散列码总是相同的; 这只是意味着散列函数必须是确定性的。
在构建哈希表时,我们首先为哈希表分配一些空间( 在内存或存储空间中 ) - 您可以想象创建一个任意大小的新数组。 如果我们有很多数据,我们可能会使用更大的数组; 如果我们有更少的数据,我们可以使用更小的数组。 任何时候我们想索引一个单独的数据片段,我们都会创建一个键/值对,其中的关键字是关于数据的一些标识信息(例如数据库记录的主键),值是数据本身(整体数据库记录,例如)。
要将值插入散列表中,我们将数据的密钥发送给散列函数。 散列函数返回一个整数(散列码),我们使用该整数(以数组的大小为模)作为我们数组中数值的存储索引。 如果我们想从哈希表中取回值,我们只需重新计算密钥中的哈希代码并从数组中的该位置获取数据。 这个位置是我们数据的物理地址。
在使用杜威小数系统的图书馆中,“关键”是书本所属的一系列分类,“价值”是书本身。 “哈希码”是我们使用杜威十进制过程创建的数值。 例如,关于分析几何的书得到了516.3的“哈希码”。 自然科学是500,数学是510,几何是516,解析几何是516.3。 通过这种方式,杜威十进制系统可以被视为书籍的散列函数; 然后将这些书放在与其散列值对应的一组书架上,并按作者的字母顺序排列在书架内。
我们的比喻不是一个完美的比喻; 与Dewey Decimal数字不同,散列表中用于索引的散列值通常不会提供信息 - 完美的隐喻中,图书馆目录将根据关于该书的一条信息包含每本书的确切位置(可能是其标题,也许是作者的姓氏,也许是它的ISBN号码......),但书籍不会以任何有意义的方式进行分组或排序,除非具有相同 密钥的所有书籍放在同一个书架上,并且可以查找使用密钥的库目录中的货架号。
从根本上说,这个简单的过程全是哈希表。 然而,为了确保基于哈希的索引的正确性和效率,在这个简单的思想的基础上构建了大量的复杂性。
基于哈希的索引的性能考虑
散列表中复杂性和优化的主要来源是散列冲突问题。 当两个或更多个密钥产生相同的散列码时会发生冲突。 考虑这个简单的哈希函数,其中密钥被假定为一个整数:
虽然任何唯一的整数在乘以13时都会产生唯一的结果,但由于鸽子的原理,最终得到的哈希码仍然会重复:如果不将至少两个项目放入同一个桶中,则无法将6件物品放入5个桶中。 因为我们的存储量是有限的,所以我们不得不使用哈希值来模数组的大小,因此我们总会碰到碰撞。
暂时我们将讨论处理这些不可避免的碰撞的流行策略,但首先应该注意的是,散列函数的选择可以增加或减少碰撞的速率 。 想象一下,我们总共有16个存储位置,我们必须在这两个散列函数中进行选择:
在这种情况下,如果我们要散列数字0-32,hash_b会产生28个冲突; 7个冲突分别用于散列值0,4,8和12(前四个插入没有发生冲突,但是后面的每个插入都没有发生)。 然而,hash_a会平均分散碰撞,每个索引碰撞一次,总共碰撞16次。 这是因为在hash_b中,我们乘以(4)的数字是散列表大小的一个因子(16)。 因为我们在hash_a中选择了一个素数,除非我们的表大小是13的倍数,我们不会有我们用hash_b看到的分组问题。
为了看到这个,你可以运行下面的脚本:
这种哈希策略,将输入密钥乘以素数,实际上是相当常见的。 质数减少了输出哈希码与数组大小共有一个公因式的可能性,从而减少了碰撞的可能性。由于哈希表已经存在了相当长的一段时间,因此有很多其他竞争性哈希函数可供选择。
多次移位散列与初始模数策略类似,但是避免了相对昂贵的取模操作,以支持非常快速的移位操作。 MurmurHash和Tabulation Hashing是散列函数的多位移系列的强有力替代品。 对这些散列函数进行基准测试包括检查它们的计算速度,生成的散列代码的分布以及它们处理不同类型数据(例如除整数以外的字符串和浮点数)的灵活性。 有关哈希函数的基准测试套件的示例,请查看SMhasher 。
如果我们选择一个好的散列函数,我们可以降低我们的冲突率并且仍然快速计算散列码。 不幸的是,无论我们选择什么散列函数,最终我们都会碰撞。决定如何处理冲突将对我们的哈希表的整体性能产生重大影响。 两种常见的碰撞处理策略是链接和线性探测 。
链接简单易行。 我们不是在散列表的每个索引处存储单个项目,而是存储链接列表的头部指针。 任何时候,一个项目通过我们的散列函数与一个已经填充的索引相冲突,我们将它添加为链表中的最后一个元素。 查找不再是严格的“恒定时间”,因为我们必须遍历链表来查找任何特定项目。 如果我们的散列函数产生很多冲突,我们将会有很长的链,并且由于更长的查找,哈希表的性能会随着时间的推移而降低。
线性探测在概念上仍然很简单,但实施起来更麻烦。 在线性探测中,散列表中的每个索引仍保留为单个元素。 当索引i发生碰撞时,我们检查索引i + 1是否为空,如果是,我们将数据存储在那里; 如果i + 1也有元素,我们检查i + 2,然后i + 3等等,直到找到一个空插槽。 只要我们找到一个空插槽,我们插入值。 再一次,查找可能不再是严格不变的时间; 如果我们在一个索引中存在多个碰撞,那么在我们找到要找的项目之前,我们最终不得不搜索一系列长项目。 更重要的是,每当我们发生碰撞时,我们都会增加后续碰撞的机会,因为(与链接不同)传入的项目最终会占据一个新的索引。
这可能听起来像链接是更好的选择,但线性探测被广泛接受为具有更好的性能特征。 大部分情况下,这是由于链接列表的缓存利用率差以及阵列的有利缓存利用率。 简短版本是检查链表中的所有链接比检查相同大小的数组的所有索引要慢得多。 这是因为每个索引在数组中物理上相邻 。 但是,在链接列表中,每个新节点在创建时都会被赋予一个位置。 这个新节点不一定与列表中的邻居物理上相邻。 其结果是,在链表中,列表顺序中“彼此相邻”的节点在RAM芯片内的实际位置方面几乎彼此相邻。 由于我们的CPU高速缓存的工作原理,访问相邻内存位置的速度很快,并且随机访问内存位置速度要慢得多。 当然, 长版本有点复杂。
机器学习基础
为了理解机器学习如何被用来重新创建哈希表(和其他索引)的关键特征,值得快速重新审视统计建模的主要思想。 统计模型是一种函数,它接受一些向量作为输入,并返回:标签(用于分类)或数值(用于回归)。 输入向量包含有关数据点的所有相关信息,标签/数值输出是模型的预测。
在预测高中生是否会进入哈佛大学的模型中,向量可能包含学生的GPA,SAT分数,该学生所属的课外俱乐部数量以及与其学业成绩相关的其他数值;该标签将是真/假(因为将进入/不会进入哈佛)。
在预测抵押贷款违约率的模型中,输入向量可能包含信用评分值,信用卡账户数量,延迟付款频率,年收入以及与申请抵押贷款的人的财务状况相关的其他值; 该模型可能会返回0到1之间的数字,表示违约的可能性。
通常,机器学习用于创建统计模型 。 机器学习从业者将大型数据集与机器学习算法相结合,并且在数据集上运行算法的结果是训练有素的模型。 机器学习的核心是创建能够从原始数据自动构建准确模型的算法 而不需要人帮助机器“理解”数据实际表示的内容。 这与其他人工智能形式不同,人类可以广泛检查数据,为计算机提供有关数据含义的线索(例如,通过定义启发式算法),并定义计算机如何使用该数据(例如,使用极小极小或A * )。 然而,在实践中,机器学习经常与经典的非学习技术相结合; 人工智能代理商会经常使用学习和非学习策略来实现其目标。
考虑着名的国际象棋AI“深蓝”和最近广受好评的玩AI“AlphaGo”。 深蓝是完全非学习的AI; 人类计算机程序员与人类国际象棋专家合作创建一个函数,该函数将棋局的状态作为输入(所有棋子的位置,以及该棋手的状态),并返回与该状态的“良好”相关的值对于深蓝。 深蓝从来没有“学过”任何东西 - 人类国际象棋运动员苦苦编纂机器的评估功能。 深蓝的主要特点是树搜索算法,它允许它计算所有可能的移动,以及它的所有对手对这些移动的可能响应,以及将来的许多移动。
AlphaGo还执行树搜索。 就像Deep Blue一样,AlphaGo在每一个可能的移动中都会看到几个动作。 与Deep Blue不同,AlphaGo创建了自己的评估功能,没有Go专家的明确指示。 在这种情况下,评估函数是一个训练有素的模型。AlphaGo的机器学习算法接受Go板的状态(对于每个位置,是否有白色的石头,黑色的石头或没有石头),标签代表哪个玩家赢得了游戏(白色或黑色)。 利用这些信息,在数十万种游戏中,机器学习算法决定了如何评估任何特定的电路板状态。 AlphaGo通过观察数以百万计的例子,教会自己哪种举动将提供最高的胜利可能性。
(这是一个非常重要的简化,就像AlphaGo的工作原理一样,但是心智模型是一个有用的模型。从AlphaGo的 创建者 那里了解更多关于AlphaGo的 信息。)
模型作为指标,从ML标准出发
Google的研究人员在他们的论文中首先提出了索引是模型的前提。 或者至少可以使用机器学习模型作为索引。 理由是:模型是接受一些输入并返回一个标签的机器; 如果输入是关键字,标签是模型对内存地址的估计,那么可以使用模型作为索引。 虽然这听起来很简单,但索引问题显然不适合机器学习。 以下是谷歌团队不得不脱离机器学习规范以实现其目标的一些领域。
通常情况下,机器学习模型会根据其所了解的数据进行培训,并负责对未见数据进行估算。 当我们对数据建立索引时,估计是不可接受的。 索引唯一的工作是实际查找内存中某些数据的确切位置 。 一个开箱即用的神经网络(或其他机器学习器)将不会提供这种精确度。 Google通过跟踪训练期间每个节点所经历的最大(最正)和最小(最负)误差来处理这个问题。 使用这些值作为边界,ML索引可以在这些边界内执行搜索以查找元素的确切位置。
另一个出发点是机器学习从业者通常必须小心避免将他们的模型“过度拟合”到训练数据上; 这样的“过度拟合”模型将对其所训练的数据产生高度准确的预测,但通常会对训练集外的数据执行得非常糟糕。 另一方面,索引从定义上讲是过度拟合的。 训练数据是被索引的数据 ,也使其成为测试数据。由于查找必须发生在索引的实际数据上,所以在这种机器学习应用中,过度拟合更容易被接受。 同时,如果模型过度适合现有数据,则向索引添加项目可能会产生可怕的错误预测; 正如文件中指出的那样:
“[...],这个模型的普遍性和”最后一英里“表现似乎有一个有趣的折衷; “最后一英里”预测越好,可以说,模型过度拟合的能力就越强,而对新数据项的推广能力就越差。“
最后,培训模型通常是该过程中最昂贵的部分。 不幸的是,在广泛的数据库应用程序(和其他索引应用程序)中,向索引添加数据相当普遍。 该团队坦诚对待这个限制:
“到目前为止,我们的结果集中在只读内存数据库系统的索引结构上。 正如我们已经指出的那样,即使没有任何重大修改,当前的设计已经可以替代数据仓库中使用的索引结构,这些索引结构可能每天只更新一次,或者BigTable [ 18 ],其中创建B树批量作为SStable合并过程的一部分。 “ - (SSTable是Google的” BigTable “的 一个关键组件, 在SSTable上的相关阅读)
学习哈希
该论文审查了(除其他之外)使用机器学习模型替代标准哈希函数的可能性。 研究人员有兴趣了解的一个问题是:知道数据的分布能否帮助我们创建更好的索引? 通过我们上面探讨的传统策略(移位乘法,杂音散列,素数乘法...),数据的分布被明确忽略。 每个传入项目都被视为一个独立的值,而不是作为具有宝贵属性的较大数据集的一部分来考虑。 结果是,即使在很多先进的哈希表中,也存在很多浪费的空间。
哈希表的实现具有大约50%的内存利用率是常见的,这意味着哈希表占用了实际需要存储的数据两倍的空间。 换句话说,当我们存储与数组中存储的数量完全一样多的项时,哈希表中的一半地址仍为空。 通过用机器学习模型替换标准哈希表实现中的哈希函数,研究人员发现它们可以显着减少浪费的空间量。
这并不是特别令人意外的结果:通过对输入数据进行训练,学习的散列函数可以更均匀地在一些空间上分布值,因为ML模型已经知道数据的分布! 然而,这是一种潜在的强大方式,可以显着减少基于散列索引所需的存储量。这带来了一个折衷:ML模型比我们上面看到的标准哈希函数要慢一些; 并且需要一个标准哈希函数不需要的训练步骤。
也许使用基于ML的散列函数可以用于有效的内存使用是关键问题但计算能力不是瓶颈的情况。 谷歌/麻省理工学院的研究小组认为数据仓库是一个很好的用例,因为指数已经在一个已经很昂贵的过程中每天重建一次; 使用更多的计算时间来获得显着的内存节省可能是许多数据仓库环境的一个胜利。
但还有一个情节扭曲,进入杜鹃哈希。
布谷鸟哈希
杜鹃哈希于2001年发明,并以杜鹃鸟类家族命名。 杜鹃哈希是链接和线性探测碰撞处理的替代方案(不是替代哈希函数)。 该策略的名称如此,因为在某些杜鹃种类中,准备下蛋的女性会找到一个被占领的巢穴,并将它现有的卵子从它上面取下来以便自己放置。 在杜鹃哈希中,传入的数据会窃取旧数据的地址,就像杜鹃鸟偷走对方的巢穴一样。
以下是它的工作方式:当你创建你的哈希表时,你立即将表分成两个地址空间; 我们会称它们为主要和次要地址空间。 此外,还可以初始化两个独立的散列函数,每个地址空间一个散列函数。 这些散列函数可能非常相似 - 例如它们都可以来自“主要乘数”族,其中每个散列函数使用不同的素数。 我们将这些称为主要和次要散列函数。
最初,插入杜鹃哈希仅使用主哈希函数和主地址空间。 当发生冲突时,新数据会清除旧数据; 然后将旧数据与次散列函数进行散列并放入次地址空间 。
如果该次要地址空间已被占用,则会发生另一次逐出,并将次要地址空间中的数据发送回主地址空间。 因为有可能造成无限的驱逐循环,所以通常设置逐出驱逐的门槛; 如果达到这种驱逐次数,则重建该表,这可能包括为该表分配更多空间和/或选择新的散列函数。
众所周知,这种策略在记忆受限的场景中是有效的。 所谓的“两种选择的力量”允许杜鹃哈希即使在非常高的利用率下也具有稳定的性能(这不是链式或线性探测的真实情况)。
Bailis和他在斯坦福大学的研究团队发现,通过一些优化,杜鹃散列速度可以非常快, 即使在99%的利用率下也能保持高性能。 从本质上讲,布谷哈希可以通过利用两种选择的力量,在没有昂贵的培训阶段的情况下实现“机器学习”哈希函数的高度利用。
索引的下一步是什么?
最终,每个人都对能够学习的索引结构的潜力感到兴奋。 随着越来越多的ML工具的推出,像TPU这样的硬件进步使机器学习工作负载更快,索引可以越来越受益于机器学习策略。 与此同时,像布谷鸟哈希这样的漂亮算法提醒我们,机器学习不是万能的。 融合了机器学习技术和古老理论(如“两种选择的力量”)的令人难以置信的力量的作品将继续推动计算机效率和能力的界限。
索引的基本原理似乎不可能一夜之间被机器学习策略所取代,但自调整索引的想法是一个强大而令人兴奋的概念。 随着我们继续更善于利用机器学习,并且随着我们不断提高计算机处理机器学习工作负载的效率,利用这些进步的新想法必将找到主流使用方式。 下一个DynamoDB或Cassandra可能会很好地利用机器学习策略; PostgreSQL或MySQL的未来实现最终也可能采用这种策略。 最终,这将取决于未来研究的成功,这将继续建立在最先进的非学习策略和“AI革命”的尖端战术上。
出于必要性,许多细节已被掩盖或简化。 好奇的读者应该阅读:
- 学习指标案例(谷歌/麻省理工学院)
- 不要抛弃你的算法Book:可以超越学习指数的经典数据结构(斯坦福大学) ;
- 哈希方法的七维分析及其对查询处理的影响 (萨尔大学)