大家好,欢迎来到IT知识分享网。
第1章 算法概述
本书涵盖了理解、分类、选择和实现各种重要算法所需的信息。除了阐述这些算法的逻辑,本书还讲解适用于各种算法的数据结构、开发环境和生产环境。我们着重讲述现代机器学习算法,因为它们的重要性与日俱增。本书在讲解算法逻辑的同时,还提供实例来展示如何使用算法求解日常生活中的实际问题。
本章整体阐述算法基础。先介绍理解不同算法如何工作所需的基本概念。总述人们最初如何用算法以数学的形式表达特定类型的问题,还提到不同算法的局限性。接着讲述描述算法逻辑的各种方法。由于本书用Python编写算法,之后说明如何设置环境以运行书中给出的例子。然后讨论如何用不同方法量化算法性能,并与其他算法进行比较。最后,本章讨论验证算法的特定实现的各种方法。
1.1 什么是算法
简而言之,算法是求解问题的计算规则集,每条规则都执行某种计算。它旨在根据精确定义的指令,为任何有效的输入产生对应的输出结果。在英语词典(如American Heritage Dictionary)中查找算法这个词,你将得到这个概念的定义如下:
“算法是由无歧义指令构成的有限集合,它在给定的一组初始条件下按预定顺序执行,直到满足给定的可识别的结束条件,以实现某种目的。”
算法设计致力于设计一种最高效的数学步骤来有效地求解实际问题。以所得的数学步骤为基础,可以开发一个可复用且通用性更强的数学方案来求解更广泛的类似问题。
算法的各个阶段
图1-1展示了开发、部署和使用算法的各个阶段。
图1-1
可以看到,整个过程始于从问题表述中了解算法的设计需求,明确需要完成的事项细节。一旦问题被明确表述,就可以进入开发阶段。
开发阶段由两个阶段构成:
■设计阶段:设计阶段要构思算法的架构、逻辑和实现细节并形成文档。设计算法时,既要考虑算法的准确性,又要考虑算法的性能。为给定问题设计算法时,很多时候最终会得到多个候选算法。算法设计阶段是一个迭代的过程,需要对各种候选算法进行比较。有些算法简单而快速,但可能会牺牲一些准确性。其他算法可能非常准确,但由于其复杂度高,可能需要大量的运行时间。在这些复杂的算法中,也有一些算法比其他算法更高效。在做出选择之前,应该仔细研究候选算法的所有内在平衡因素。特别是,为复杂问题设计高效算法非常重要。恰当设计的算法是一个有效的解决方案,它不仅具有令人满意的性能,还具有令人信服的准确性。
■编码阶段:编码阶段将设计好的算法转化为计算机程序。重要的是,实际程序必须实现设计阶段提出的所有逻辑和结构。
算法的设计阶段和编码阶段本质上是一个迭代过程。设计出同时满足功能性需求和非功能性需求的算法,往往需要花费大量的时间和精力。算法的功能性需求明确刻画了给定输入数据对应的正确输出结果,而非功能需求主要是指在给定数据规模上的性能需求。本章稍后将讨论算法的验证和性能分析。算法验证就是验证算法是否满足其功能性需求,而性能分析则是验证算法是否满足其主要的非功能性需求,亦即性能需求。
一旦将选用的算法用编程语言进行了代码设计和代码实现,就可以部署算法的代码了。部署算法需要设计代码运行的实际生产环境,其设计需要根据算法的数据和处理需求来展开。例如,并行算法需要恰当地选择集群中计算机节点的数量,以确保算法被高效执行;数据密集型算法则需要设计数据传递管道、数据缓存策略和数据存储策略。生产环境的设计将在第13章和第14章中更加详细地讨论。一旦生产环境被设计和实现,算法就可以部署了。之后,算法就接收并处理输入数据,并按照要求生成输出结果。
1.2 描述算法逻辑
在设计算法时,找出描述算法细节的各种不同方法非常重要。这些方法既要能描述算法的逻辑,又要能描述算法的结构。一般来说,如同房屋建造一样,算法的结构描述应先于算法实现完成,这一点很重要。对于更复杂的分布式算法来说,预先规划好算法运行时在集群上的逻辑分布对高效的迭代式算法设计过程非常重要。接下来,通过伪代码和执行计划来实现和讨论算法逻辑和算法结构。
1.2.1 理解伪代码
描述算法逻辑最简单的方式是写伪代码,也就是用半结构化的方式给出算法的高层次描述。在用伪代码编写算法逻辑前,先用通俗易懂的自然语言描述算法的主要流程。然后,将这种描述转化为伪代码,也就是用与算法逻辑和算法流程紧密关联的结构化方式来改写这种自然语言描述。写得好的算法伪代码应该能够合理地描述算法高层次步骤的细节,尽管有时包含这种细节的伪代码与算法的主要流程和结构无关。图1-2展示了这些步骤的先后关系。
图1-2
注意,伪代码写好之后(参见下一个小节),就可以用编程语言为我们选择的算法编写代码了。
伪代码实例
下面展示了一个名为SRPMP的资源分配算法的伪代码。在集群计算中,很多情况下需要在一组可用资源上运行并行任务,这些资源统称为资源池。该算法将任务分配到资源上,并创建一个映射集合Ω。请注意,给出的伪代码描述了算法的逻辑和流程,这些逻辑和流程在下一个段落中阐述:
现在逐行解析这个算法:
1. 算法运行时开始建立映射。此时,映射集合Ω是空的。
2. 选择第一个分区作为T1任务的资源池(参见伪代码第3行)。Television Rating Point(TRPS)不断地针对每个任务Ti调用Rheumatoid Arthritis(RA)算法,为其选择一个分区作为资源池。
3. RA算法返回为任务Ti选择的资源集,表示为ωi(参见伪代码第5行)。
4. Ti和ωi被添加到映射集合Ω中(参见伪代码第6行)。
5. Ti的状态由STATE 0: Idle/Mapping变为STATE 1: Idle/Mapped(参见伪代码第7行)。
6. 注意,第一轮选择时,k=1,第一个分区被选中。对于随后的每轮选择,k值都会增加,直到k>q。
7. 如果k大于q,则k被重置为1(参见伪代码第9行和第10行)。
8. 重复上述过程,直到所有任务和资源集合之间建立起映射,并将其存储在映射集Ω中。
9. 每个任务一旦被映射阶段映射到一组资源上,该任务就在对应资源上被执行。
1.2.2 使用代码片段
随着Python等简单但功能强大的编程语言的流行,一种替代伪代码的方法逐渐流行起来,那就是直接以某种简化的形式使用编程语言来表示算法的逻辑。与伪代码一样,这种被选定的代码避免了使用详细完整的代码,而是抓住了所提出的算法的重要逻辑和结构。这种被选定的代码有时称为代码片段。在本书中将尽可能使用代码片段代替伪代码,因为它们可以省略一个额外步骤。例如,让我们看一个Python函数的简单代码片段,该代码片段可用于交换两个变量:
注意,代码片段并不总能代替伪代码。在伪代码中,我们有时会把多行代码抽象为一行伪代码来表达算法的逻辑,避免算法逻辑被不必要的代码细节所干扰。
1.2.3 制定执行计划
伪代码和代码片段并不总是能够详细叙述与更复杂的分布式算法相关的所有逻辑。例如,分布式算法在运行时通常需要划分为具有先后顺序的不同代码阶段。找到正确策略将较大的问题划分为顺序正确的代码阶段使得阶段数最优,对于算法的高效执行至关重要。
我们需要找到一种方法来表示这种策略,从而完整地表示算法的逻辑和结构。执行计划是详细说明算法如何被细分为一组任务的方法之一。一个任务可以是mapper或reducer,这些任务可以被组合在一个块,也就是阶段中。图1-3展示了在算法执行前由Apache Spark生成的执行计划。它详细说明了为执行我们的算法而创建的作业将被具体划分为哪些运行时任务。注意,图中五个任务被分为两个不同的阶段:Stage 11和Stage 12。
图1-3
1.3 Python包简介
算法设计好后需要用编程语言来按照设计进行实现。本书中选择编程语言Python。之所以选择它,是因为Python是一种灵活且开源的编程语言。对于越来越重要的云计算基础设施,如亚马逊网络服务(AWS)、微软Azure和谷歌云平台(GCP),Python也是其首选语言。
Python官方主页是https://www.python.org/,其中包含了安装说明和对你可能有所帮助的初学者指南。
如果你以前没有使用过Python,最好浏览一下初学者指南,以进行自学。对Python的基本了解将有助于你更好地理解本书所介绍的概念。
在本书中,我希望你能够使用Python 3的最新版本。在编写本书时,最新的版本是3.7.3,我们将用它运行本书中的练习。
1.3.1 Python包
Python是一种通用语言。在设计时,它仅带有最低限度的功能。根据使用Python的具体目的,你还可以安装附加包。安装附加包最简单的方法是通过pip安装程序。pip命令可以用来安装附加包:
已经安装的包需要定期更新,以获得最新的功能,这可以通过使用upgrade标识来实现:
另一个用于科学计算的Python发行版是Anaconda,它可以从http://continuum.io/downloads下载。
除了使用pip命令安装新包,对于Anaconda发行版,我们还可以选择使用如下命令安装新包:
如果要更新现有的软件包,Anaconda发行版提供了另一个选项,可以使用以下命令:
有各种各样的Python包可供选择,在接下来的小节中将介绍一些与算法相关的比较重要的包。
SciPy生态系统
Scientific Python (SciPy)——发音为sigh pie——是一组为科学界创建的Python包。它包含许多函数,包括各种随机数生成器、线性代数程序和优化器。SciPy是一个全面的软件包,并且随着时间的推移,人们开发了许多扩展,以根据自己的需求定制和扩展该软件包。
以下是属于这个生态系统的主要的包:
■NumPy:对于算法来说,创建多维数据结构(如数组和矩阵)的能力非常重要。NumPy提供了一组数组和矩阵数据类型,这些数据类型对于统计和数据分析非常重要。关于NumPy的详细信息可以在http://www.numpy.org/找到。
■scikit-learn:这个机器学习扩展是SciPy最受欢迎的扩展之一。scikit-learn提供了一系列重要的机器学习算法,包括分类、回归、聚类和模型验证。可以在http://scikit-learn了解更多关于scikit-learn的细节。
■pandas:pandas是一个开源软件库。它包含了表格型的复杂数据结构,该数据结构在各种算法中广泛用于输入、输出和处理表格数据。pandas库中包含了许多有用的函数,它还提供了高度优化后的性能。想要找到更多关于pandas的细节,可以查看http://pandas.pydata.org/。
■Matplotlib:Matplotlib提供了强大的可视化工具。数据可以通过折线图、散点图、柱状图、直方图、饼图等形式呈现。要获得更多信息,请访问https://matplotlib.org/。
■Seaborn:Seaborn类似于在R语言中流行的ggplot2库。它基于Matplotlib,并且提供了优秀的界面用于绘制出色的统计图形。更多详情请访问https://seaborn.pydata.org/。
■iPython:iPython是一个增强的交互式控制台,旨在方便编写、测试和调试Python代码。
■Running Python programs:交互式的编程模式对代码的学习和实验很有用。Python程序可以保存在一个以.py为扩展名的文本文件中,并且可以从控制台运行该文件。
1.3.2 通过Jupyter Notebook执行Python
运行Python程序的另一种方式是通过Jupyter Notebook。Jupyter Notebook提供了一个基于浏览器的用户界面来开发代码。本书使用Jupyter Notebook来展示代码示例。能够使用文字和图形对代码进行注释和描述的能力,使其成为展示和解释算法的完美工具,以及学习的绝佳工具。
要启动notebook,需要启动Juypter-notebook程序,然后打开浏览器,导航到http://localhost:8888(如图1-4所示)。
图1-4
请注意,一个Jupyter Notebook页面由被称为单元格的不同的块组成。
1.4 算法设计技术
算法是求解实际问题的数学方案。我们在设计和微调算法时,需要牢记以下三个设计的关注点:
■第一点:算法是否能产生我们预期的结果?
■第二点:这是获取预期结果的最佳方法吗?
■第三点:算法在更大的数据集上表现怎么样?
在为问题设计解决方案前,最好先理解问题本身的复杂度。例如,如果将问题描述为其需求和复杂度,则有助于为其设计恰当的求解方案。一般来说,算法可以根据问题的特点分为以下几种类型:
■数据密集型算法:数据密集型算法旨在处理大量数据,其处理需求相对简单。压缩大型文件的算法就是数据密集型算法一个典型例子。对于这类算法,待处理数据的规模远大于处理引擎(单节点或集群)的内存规模,因此可能需要开发一个迭代处理设计来根据要求高效地处理数据。
■计算密集型算法:计算密集型算法具有大量计算需求而不涉及大量数据。一个简单的例子就是寻找大素数的算法。寻找恰当策略将算法划分为不同的阶段,使得其中至少有一些阶段是可以并行化的,这是最大限度地提升算法性能的关键。
■既是数据密集型算法,也是计算密集型算法:有些算法需要处理大量数据,同时也有大量计算需求。实时视频信号上的情感分析算法就是这种算法的一个很好的例子,其中数据和计算需求都很大。这类算法是最耗费资源的算法,需要对算法进行精心设计,并对可用资源进行智能分配。
更深入地研究问题的数据和计算将有助于刻画问题的复杂度和计算需求。下面讨论这方面的内容。
1.4.1 数据维度
将算法从问题的数据维度上归类,我们需要查看数据的体积(volume)、速度(velocity)和多样性(variety),这三个方面被称为数据的3V,其定义如下:
■体积:指算法将要处理的数据的预期规模;
■速度:指使用该算法时新数据生成的预期速度,它可以为零;
■多样性:量化了所设计算法预计要处理多少种不同类型的数据。
图1-5更加详细地展示了数据的3V,其中心是最简单的数据,体积小且多样性和速度都很低。逐渐远离中心时,数据复杂度逐渐增加,这可以从三个维度中的一个或多个维度上增加。例如,在速度维度上,最简单的是批处理过程,其次是周期性处理过程,然后是近实时处理过程,最后是实时处理过程。实时处理从数据速度维度上看是最复杂的情况。例如,由一组监控摄像头收集的实时视频信号的集合具有体积大、速度快和多样性高的特点,因此可能需要恰当的设计才能有效地存储和处理数据;而由Excel创建的单个简单.csv文件则具有体积小、速度慢和多样性低的特点。
图1-5
例如,如果输入数据仅有一个简单的csv文件,则数据体积小,速度和多样性都低。另一方面,如果输入数据是监控摄像头的实时信号,则数据体积大,速度和多样性也高,在为其设计算法的时候应该牢记这一点。
1.4.2 计算维度
问题的计算维度与待求解问题的处理需求和计算需求有关。算法的处理需求确定了其应采取何种设计才最有效。例如,深度学习算法一般都需要大量的处理能力。这意味着,深度学习算法都应尽可能采用多节点并行架构。
实例
假定要对视频展开情感分析,也就是将视频的不同部分恰当地用人类的悲伤、幸福、恐惧、喜悦、挫折和狂喜等情绪进行标记。这是一项计算密集型的工作,需要大量的计算能力。如图1-6所示,为了对算法的计算维度进行设计,我们将处理工作分为五个任务,由此构成两个阶段。所有的数据转换和准备工作都在三个mapper中完成。为了实现这个目的,我们将视频分为三个不同的部分,统称为切片(split)。在执行mapper后,将处理后的视频输入到两个称为reducer的聚合器中。为了完成所需的情感分析,这两个reducer根据情感对视频进行分组。最后,将结果合并在输出中。
图1-6
注意,mapper数量将直接转化为算法运行时的并行度。mapper和reducer的最佳数量取决于数据的特性、所用算法的类型和可用资源的数量。
1.5 性能分析
算法的性能分析是算法设计的重要部分。估计算法性能的方法之一是分析算法的复杂度。
复杂度理论研究算法的复杂程度。任何有用的算法应该具备以下三个关键特征:
■算法应该是正确的。算法如果无法给你正确的答案,则对你毫无用处。
■好算法应该是易懂的。如果世界上最好的算法对你来说太复杂,你无法在计算机上实现,则它对你也毫无用处。
■好算法应该是高效的。即使算法能产生正确结果,但是如果它需要花费一千年或者10亿TB的内存,那么它的用处也不大。
有两种方法用于量化分析算法的复杂度:
■空间复杂度分析:估计算法运行对内存的需求。
■时间复杂度分析:估计算法的运行时间。
1.5.1 空间复杂度分析
空间复杂度分析就是估计算法在处理输入数据时所需的内存量。在处理输入数据的同时,算法需要在内存中存储一些临时的数据结构。算法的设计方式将会影响这些数据结构的数量、类型和规模。在分布式计算时代,需要处理的数据量越来越大,空间复杂度分析变得日益重要。这些数据结构的规模、类型和数量将决定对于底层硬件的内存需求。分布式计算中使用的现代内存数据结构——如弹性分布式数据集(Resilient Distributed Dataset,RDD)——需要高效的资源分配机制,使其能够感知算法在不同执行阶段的内存需求。
空间复杂度分析是高效算法设计中必须完成的工作。如果在设计特定算法时没有展开空间复杂度分析,临时数据结构可用的内存不足,则可能会触发不必要的磁盘溢出,从而大大影响算法的性能和效率。
本章仅深入讨论时间复杂度。空间复杂度将在第13章中进行更详细的讨论,届时将对运行时内存需求比较复杂的大规模分布式算法进行处理。
1.5.2 时间复杂度分析
时间复杂度分析就是依据算法结构来估计算法完成其指定工作所需的时间。与空间复杂度不同,时间复杂度并不取决于运行算法的任何硬件设施,而是完全取决于算法本身的结构。时间复杂度分析的总体目标是试图回答下列重要问题:算法是否具有良好的可扩展性?算法在处理更大规模数据集时性能如何变化?
为回答这些问题,我们需要确定数据规模的增加对算法性能的影响,并确保设计的算法不仅准确,而且具有良好的可扩展性。在当今“大数据”的世界里,算法处理更大规模数据集时的性能变得越来越重要。
在很多情况下,设计算法的方法可能不止一种。此时,时间复杂度分析的目的如下:
“给定某个问题和多种算法,从时间效率来看,使用哪种算法效率最高?”
计算算法的时间复杂度有以下两种基本方法:
■实现算法后的分析方法:这种方法先分别实现各种候选算法,再对其性能进行比较。
■实现算法前的理论方法:这种方法在运行算法前用数学方法近似计算每个算法的性能。
理论方法的优点是它仅依赖于算法本身的结构,而不依赖于将用于运行该算法的实际硬件、运行算法时所选择的相关软件和用于实现该算法的编程语言。
1.5.3 性能评估
典型算法的性能都取决于作为输入提供给它的数据的类型。例如,如果在待求解问题中数据已经排序,则该算法执行的速度可能会快得惊人。如果用排序后的输入作为基准来对特定算法进行测试,则将得到不真实的、过好的性能测试结果,而这个结果在大多数情况下并不能够反映算法的实际性能。为了处理算法对输入数据的这种依赖性,我们在进行性能分析时需要考虑各种不同情况。
最好复杂度
在最好复杂度中,输入数据经过组织,能够得到算法的最佳性能。最好复杂度分析得出算法性能的上界。
最坏复杂度
评估算法性能的第二种方法是尝试找到算法在给定条件下完成工作所需的最大可能时间。最坏复杂度分析非常有用,因为我们可以保证无论在何种条件下算法的性能总是优于所得的分析结果。在评估算法在处理更大规模数据集的复杂问题时,最坏复杂度分析特别有用。最坏复杂度给出了算法性能的下界。
平均复杂度
平均复杂度分析先将各种可能的输入划分为不同的组,然后从每组中选择具有代表性的一个输入来分析算法性能,最后计算出算法在各组输入上的平均性能。
平均复杂度并不总是准确结果,因为它需要考虑算法输入的所有不同组合和可能性,但这并不总是容易做到的。
1.5.4 选择算法
你怎么知道哪种方案更好?你怎么知道哪种算法运行得更快?时间复杂度和大O记号(本章后面会讨论)为回答这种问题提供了有效手段。
为了理解它的作用,我们举一个简单的例子:对一个数字列表进行排序。有几种可用的算法可以完成这项工作,问题是如何选择合适的算法。
首先,易于观察到的事实是,如果列表中数字为数不多,则我们选择哪种算法来排序数字列表根本无关紧要。例如,如果列表中只有10个数字(n=10),则我们选择哪种算法均不要紧,因为即使是设计得非常糟糕的算法,所花费的时间可能也不会超过几微秒。但是,如果列表规模高达100万,则选择合适的算法就会产生区别。一个写得很差的算法甚至可能需要几个小时才能完成列表排序任务,而一个设计较好的算法则可能仅用几秒就完成了任务。因此,在大规模输入数据集上,投入时间和精力展开性能分析,选择合理设计的算法来高效地完成要求的任务是非常有意义的。
1.5.5 大O记号
大O记号用于量化表示各种算法在输入规模增长时的性能,它是最坏复杂度分析中最常用的方法之一。下面讨论不同种类的大O记号。
常数时间复杂度(O(1))
如果算法运行时间是独立于输入数据规模的相同值,则称其运行时间是常数时间,表示为O(1)。例如,考虑访问数组的第n个元素,无论数组的规模多大,花费常数时间即可获得结果。再如,下面的函数以复杂度O(1)返回数组的第一个元素:
其输出结果显示为图1-7。
图1-7
■使用push向栈内添加新的元素,或使用pop从栈中删除元素。无论栈的规模多大,添加和删除元素都花费常数时间。
■访问哈希表中的元素(参见第2章的相关讨论)。
■桶排序(参见第2章的相关讨论)。
线性时间复杂度(O(n))
如果算法的执行时间与输入规模成正比,则称该算法具有线性时间复杂度,表示为O(n)。例如,考虑下面对一维数据结构中所有元素求和的算法:
注意算法的主循环。算法中主循环的迭代次数随着n值的增加而线性增加,导致了O(n)的复杂度,图1-8展示了算法的运行实例。
图1-8
其他一些数组操作的例子如下:
■查找元素
■找出数组中所有元素的最小值
平方时间复杂度(O(n2))
如果算法的执行时间与输入规模的平方成正比,则称该算法的运行时间为平方时间。例如,考虑下面对二维数组求和的简单函数:
注意,在内层循环嵌套外层主循环中,这种嵌套结构使得前面代码的时间复杂度为O(n2)(如图1-9所示)。
图1-9
另一个例子是冒泡排序算法(参见第2章的相关讨论)。
对数时间复杂度(O(log n))
如果算法的执行时间与输入规模的对数成正比,则称该算法的运行时间为对数时间。在时间复杂度为O(log n)的算法中,随着算法的每一轮迭代,输入规模都会以常数倍数递减。例如,二分查找是对数时间复杂度,该算法用于从一维数据结构(如Python列表)中查找特定元素,它要求数据结构内的元素按降序进行排序。下面的代码将二分查找算法实现为一个名为searchBinary的函数:
主循环利用了列表有序这一事实。算法中每轮迭代都将列表二等分,直到得到结果(如图1-10所示)。
图1-10
定义完函数后,图1-10的第11行和第12行中通过查找特定元素对其进行了测试。二分查找算法将在第3章中进一步讨论。
注意,在所介绍的四种类型的大O记号中,O(n2)表示最差性能,O(log n)表示最佳性能。事实上,O(log n)可以视为任何算法性能的黄金标准(尽管并非总是能够达到)。另一方面,O(n2)并不像O(n3)那样糟糕,但由于时间复杂度限制了它们实际可以处理的数据规模,因此属于这类时间复杂度的算法仍然不能用于大数据。
降低算法复杂度的一种方法是在算法的准确度上进行折中,从而得到一种称为近似算法的算法。
算法性能评估的整个过程本质上是迭代进行的,如图1-11所示。
图1-11
1.6 验证算法
验证算法指的是确保它是为待求解问题找到了一个数学求解方案。验证过程应该在尽可能多的输入值和输入类型上检验求解结果。
1.6.1 精确算法、近似算法和随机算法
验证算法要依据算法的类型展开,因为对不同类型的算法,其验证技术也不同。我们先区分确定型算法和随机算法。
确定型算法在特定输入上始终产生完全相同的输出结果。但是,在某些类型的算法中,随机数序列也被当作输入,这些随机数使得算法每次运行时产生的输出都不同。在第6章中将要详细介绍的k-means聚类算法就是这类算法的一个例子,如图1-12所示。
图1-12
算法也可以分为如下两种类型,分类的依据是简化算法逻辑,使其运行速度更快时所采用的假设或近似:
■精确算法:精确算法预计在不引入任何假设或近似的情况下产生精确解。
■近似算法:当问题的复杂度过大,在给定的资源下难以处理时,我们会通过一些假设来简化问题。基于这些简化或假设的算法称为近似算法,它并不能给我们提供完全精确的解。
让我们通过一个例子来理解精确算法和近似算法之间的区别。1930年,人们提出了著名的旅行商问题:为特定的旅行商找出最短路线,让他能够沿该路线访问城市列表中的每个城市之后返回出发点(这就是问题被命名为旅行商问题的原因)。寻找解决方案时,第一个想法就是生成所有城市的排列组合,然后选择路线最短的排列组合。这种解决方案的复杂度是O(n!),其中n是城市的数量。显然,城市数量超过30之后,时间复杂度就变得无法处理了。
如果城市数量超过30,那么降低复杂度的方法之一就是引入一些近似和假设。
对于近似算法来说,在需求分析时设置好期望的准确度很重要。验证近似算法就是要验证结果的误差是否在可接受的范围内。
1.6.2 可解释性
算法在临界条件下使用时,能够在需要时解释每一个结果背后的原因变得很重要。这是很有必要的,因为这能够确保基于算法结果得出的决策不会带来偏差。
有些特征会直接或间接用于得到某种特定决策,能够准确地识别出这些特征的能力,称为算法的可解释性。算法在临界条件下使用时,需要评估算法是否存在偏差和偏见。如果算法可能影响到与人们生活相关的决策,则算法的伦理分析将会成为验证过程中的标准环节。
对于处理深度学习的算法,很难实现算法的可解释性。例如,如果某个算法用于拒绝某些人的抵押贷款申请,则透明度和解释原因的能力都很重要。
算法可解释性是一个活跃的研究领域。最近发展起来的有效技术之一是局部可理解的模型无关解释(Local Interpretable Model-Agnostic Explanation, LIME),参见2016年第22届国际计算机学会知识发现和数据挖掘国际会议(ACM SIGKDD)论文集。LIME基于如下概念,对每个输入实例均做出各种细微改变,然后尽力映射出该实例的局部决策边界,它可以量化每种细微改变对该实例的影响。
1.7 小结
本章学习算法基础。首先,我们了解了开发算法的不同阶段,讨论了算法设计过程中用于描述算法逻辑的不同方法;然后,学习了如何设计算法和两种不同的算法性能分析方法。最后,我们学习了验证算法涉及的各个不同方面。
经过本章的学习,我们应该能够理解算法的伪代码,理解开发和部署算法的不同阶段。此外,我们还学会了如何使用大O记号来估计算法的性能。
下一章讨论算法中用到的数据结构。我们先讨论Python中可用的数据结构,然后考虑如何用这些数据结构来创建栈、队列和树等更复杂的数据结构,它们将用于复杂算法的开发。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/40458.html