TowardsDataScience 2023 博客中文翻译(九十八)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

亲爱的数据科学家,请保持组织有序!

原文:towardsdatascience.com/dear-data-scientist-be-organized-969ef0fdeb5e?source=collection_archive---------4-----------------------#2023-02-03

一个快速指南,帮助你提高组织技能,从而提升你的表现。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 亚历山德雷·罗斯塞托·莱莫斯

·

关注 发布于 Towards Data Science ·8 min read·2023 年 2 月 3 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由 Matthew Kwong 提供,来自 Unsplash

在我们开始之前

首先,我有几个简单的问题要问你:

  • 你是否曾经在自己的笔记本中迷失,不知道需要按什么顺序执行单元,以确保代码顺利运行?

  • 在项目开发过程中,你是否曾因试图记起之前进行的分析及其结果而浪费了宝贵的几分钟甚至几小时?

  • 你是否曾经遇到过快速、准确、轻松地找到你在项目中使用的数据的困难?

  • 你是否在解释你的代码时遇到困难,特别是在长时间未使用它们之后?

  • 当你去解释你的代码或向同事展示一些分析结果时,是否需要几分钟来记住你做了什么或分析结果在哪里?

如果你对这些问题中的任何一个回答是“是”,那么很抱歉地告诉你,你很可能有一个组织问题。

不要羞愧,这比看起来更普遍!

好消息是,这类问题很容易解决,但它们繁琐且需要投入精力。我曾经因为缺乏组织而遭受过很多问题,因此我最终创建了一个指南,帮助我在工作中更加有条理和方法化。

我喜欢将我的组织分为四个主题:

  • 数据组织

  • 文件组织(笔记本)

  • 笔记本结构组织

  • 代码组织

数据组织

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由 Nana Smirnova 提供,来自 Unsplash

数据科学项目涉及使用数据,要么进行一些分析,要么开发机器学习模型,因此数据是这一领域任何项目的基础。

逻辑上,数据访问必须准确,以便在需要时选择正确的数据。然而,当我们处理大型项目或项目持续很长时间时,这项任务可能会变得困难。在开发过程中,可能会生成多个数据库,如果这些数据库没有正确分类,它们可能会导致混淆,从而做出错误的决策。

为了处理这些问题,我喜欢按照以下结构组织我的数据:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数据组织结构示意图(作者提供的图片)

是的,这是一种非常简单和直观的结构,但人们(包括我)往往会把所有的数据保存在同一个文件夹里。现在我喜欢为我参与的每个项目单独设立一个文件夹,并且对于每个项目,我总是有一个单独的原始数据文件夹,包含初始信息,还有一个处理后数据文件夹,包含经过某种预处理后的数据。

保存处理后的数据非常有用,这样你就不必运行整个预处理流程来生成你将用于构建模型或进行分析的数据集。

根据我的经验,我学到一个好习惯是保留在项目中使用的数据版本,因为这样你可以用来比较获得的不同结果,并且确保以前结果和分析的可重复性。然而,在项目结束后的一段时间,最好清理一些旧文件,以减少使用的存储量。

文件组织

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Maksym Kaharlytskyi拍摄,来源于Unsplash

通常,在数据科学项目中,需要完成几个步骤才能达到结果。例如,如果你正在为特定任务开发机器学习模型,一些步骤是相当常见的,如开发将要使用的数据库、探索性数据分析(EDA)、开发预处理管道、模型调整和结果评估。

每个任务通常需要合理数量的代码行来完成,这使得将所有任务放在同一个笔记本中变得不切实际,因为同一个文件中的信息量巨大。即使应用我将在接下来的主题中讨论的技巧,笔记本也很容易变得混乱和杂乱。

另一个值得提到的点是,笔记本可能变得计算开销如此之大,以至于如果尝试按顺序运行所有单元格,内核可能会变得不稳定,从而导致浪费宝贵的时间,甚至是小时。

因此,我一直使用并且在项目中对我帮助很大的文件组织方式具有以下格式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

文件组织结构示例(图片由作者提供)

基本上,我按照时间顺序整理我的笔记本(使用文件名中的数字进行排序),我给每本笔记本中开发的内容起非常清晰和明确的名字,我创建文件夹来存储生成的文件,以便将来更容易找到它们。除了方便信息组织之外,遵循这种结构还清楚地显示了达到结果的步骤和所使用的推理。

再次使用这种组织结构,或类似的结构,是一个极其简单的任务,只需自律即可完成。

接下来的两个步骤是最具挑战性的,因为它们要求你不仅要编写代码,还需要思考并且要有自律。

笔记本结构组织

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Kelly Sikkema拍摄,来源于Unsplash

我所说的笔记本结构组织是指在同一本笔记本的每一部分中明确你的工作内容。对于这个主题,使用 Markdown 将是你最好的朋友。因为你可以使用不同的样式来区分代码的主题和子主题。

对我而言,这部分几乎和写报告一样,你需要明确陈述每个实验的意图、假设、结果和分析。关键是你必须在目标和分析上非常明确,这需要前瞻性思维和客观性。

井然有序的笔记本结构在解释内容时非常有帮助,这在你向同行和上级展示你的分析时尤为重要。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

笔记本结构组织示例(图像由作者提供)

正如你所见,这部分的工作量会根据笔记本的目标而有所不同。然而,我发现这项工作通常会带来更清晰的目标感,并帮助我理解每次分析的结果。这对我帮助很大,特别是在同时处理多个项目时。

代码组织

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Fahrul RaziUnsplash 上的照片

解释这个主题的最佳方式是:像向别人解释一样编写代码。使用大量注释,尽量做到清晰。刚开始这个习惯可能会有点困难,你可能会觉得注释代码有些懒惰,但随着时间的推移,你会变得非常擅长快速有效地注释,做到几乎自动化。尽量直接,节省文字和理解所需的时间。

另一个非常重要的点是对你开发的函数进行注释。如果没有人知道它的作用,那么即使函数非常有用也没用!在这里,强烈推荐使用文档字符串。再次强调,要详细解释函数的功能,但尽量不要使用过多的文字。

我使用的模板基本上包含三部分信息:函数的目的是什么(在信息部分),函数的输入变量是什么(在输入部分),以及函数的响应是什么(如果有的话,在输出部分)。下面是我为我在 Medium 上的另一篇文章制作的函数的示例:遗传算法及其在机器学习中的实用性

注释和组织代码的实践非常有用,这不仅有助于你明确自己的工作,还能使你的代码对你组织中的其他人有用。可重用、制作良好且有条理的代码在团队中非常有价值,因为它们可以节省多个员工在执行重复或类似任务时的时间。

另一个重要点是,如果你在项目中途去度假或请假,如果你的代码组织得很好,你会比需要记住你几天前写的所有代码或原因要更快地接续工作。

总结

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由Glenn Carstens-Peters拍摄,来自Unsplash

在这篇文章中,我展示了在工作中更有条理的几个好处。有条理使你更加高效、清晰和客观,同时也锻炼了你的纪律性和思维能力,因为在写下分析结果或代码中的注释之前,你必须先思考你要写什么。如果你发现自己需要重写某些内容,不要惊讶,这通常是你在锻炼批判性思维,并寻找更高效的表达方式,这也是锻炼这一重要技能的过程。

如果你在职业生涯的早期阶段,更加有条理会对你帮助很大。能够清楚地回答上级提出的问题或清晰地解释你在分析或代码中的目标,这些都是能为你的工作加分的因素。

最后,我的建议是逐步开始,并调整你整理事物的方式,以便找到最适合你的方法。我在这里描述的方法是我为自己调整的,它们对我非常有效,但有时你可能会更适合使用不同的整理方式。不管怎样,有条理只会给你带来好处。

非常感谢你阅读这篇文章,希望它对你有所帮助。

任何评论和建议都非常欢迎。

欢迎通过我的 LinkedIn 联系我,查看我的 GitHub,以及阅读我在 Medium 上的其他文章。

Medium

LinkedIn

GitHub

调试和调整 Amazon SageMaker 训练任务与 SageMaker SSH 帮助工具

原文:towardsdatascience.com/debugging-and-tuning-amazon-sagemaker-training-jobs-with-sagemaker-ssh-helper-51efeb4d03be?source=collection_archive---------6-----------------------#2023-12-27

一个新工具,提升了托管训练工作负载的调试能力

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Chaim Rand

·

关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 12 月 27 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由 James Wainscoat 提供,来自 Unsplash

考虑到过去一年(2023)宣布的所有新 Amazon SageMaker 功能,包括最近的 AWS re:invent,很容易忽视 SageMaker SSH Helper — 一种用于连接到远程 SageMaker 培训环境的新工具。但 有时正是这些安静的增强功能有潜力对你的日常开发产生最大的影响。 在这篇文章中,我们将回顾 SageMaker SSH Helper 并展示它如何提高你 1) 调查和解决培训应用程序中出现的错误的能力,以及 2) 优化其运行时性能。

在 之前的帖子 中,我们详细讨论了云端培训的好处。基于云的管理培训服务,如 Amazon SageMaker,简化了围绕 AI 模型开发的许多复杂问题,并大大提高了对 AI 特定机器和 预训练 AI 模型 的可访问性。要在 Amazon SageMaker 中进行培训,你只需定义一个培训环境(包括实例类型)并指向你希望运行的代码,培训服务将 1) 设置请求的环境,2) 将你的代码传送到培训机器,3) 运行你的培训脚本,4) 将培训输出复制到持久存储中,5) 在培训完成后拆除一切(以便你只需为你需要的部分付费)。听起来很简单…对吧?然而,管理培训也并非没有缺陷,其中之一 — 它对培训环境的有限访问性 — 将在本文中讨论。

免责声明

  1. 请不要将我们对 Amazon SageMaker、SageMaker SSH Helper 或任何其他框架或工具的使用解读为对它们使用的支持。开发 AI 模型有许多不同的方法。对你而言,最佳解决方案将取决于你的项目细节。

  2. 请务必在阅读时核实本文内容,特别是代码示例,与当时可用的最新软件和文档。AI 开发工具的环境在不断变化,我们提到的一些 API 可能会随着时间而变化。

管理培训的缺点 — 培训环境的不可访问性

正如经验丰富的开发者所知道的,应用开发时间的很大一部分实际上花在了调试上。我们的程序很少会“开箱即用”;更多时候,它们需要经过数小时的繁琐调试才能按预期运行。当然,要有效调试,你需要对应用环境有直接访问权限。尝试在没有环境访问权限的情况下调试应用程序,就像尝试修理水龙头却没有扳手一样。

AI 模型开发中的另一个重要步骤是调整训练应用的运行时性能。训练 AI 模型可能成本高昂,我们最大化计算资源利用的能力可能对训练成本产生决定性影响。在之前的一篇文章中,我们描述了分析和优化训练性能的迭代过程。类似于调试,直接访问运行时环境将大大提高并加速我们获得最佳结果的能力。

不幸的是,SageMaker 训练的“开火即忘”特性带来了一个副作用,那就是无法自由连接到训练环境。当然,你可以通过训练作业输出日志和调试打印(即,添加打印、研究输出日志、修改代码,并重复直到解决所有错误并达到所需性能)来调试和优化性能,但这将是一个非常原始且耗时的解决方案。

有一些最佳实践可以解决管理训练工作负载时的调试问题,每种方法都有其优缺点。我们将回顾其中的三种,讨论它们的局限性,然后展示新的SageMaker SSH Helper如何彻底改变游戏规则。

在本地环境中调试

建议在将任务提交到云端之前,先在本地环境中运行几步训练。尽管这可能需要对代码进行一些修改(例如,为了在 CPU 设备上进行训练),但通常是值得的,因为它使你能够识别并修复一些简单的编码错误。显然,这比在云端昂贵的 GPU 机器上发现这些错误更具成本效益。理想情况下,你的本地环境应尽可能类似于 SageMaker 训练环境(例如,使用相同版本的 Python 和 Python 包),但在大多数情况下,这种相似性是有限的。

在 SageMaker Docker 容器中本地调试

第二个选项是拉取 SageMaker 使用的 深度学习容器 (DLC) 镜像,并在你的本地 PC 上的容器中运行你的训练脚本。这种方法可以让你很好地了解 SageMaker 训练环境,包括已安装的包(和包版本)。它在识别缺失的依赖项和解决依赖项冲突方面极为有用。请参见 文档 以了解如何登录并拉取适当的镜像。请注意,SageMaker 的 API 支持通过其 本地模式 功能拉取和训练 DLC。然而,自己运行镜像将使你能够更自由地探索和研究镜像。

在未管理的实例上在云中调试

另一个选择是在云中使用未管理的 Amazon EC2 实例进行训练。这个选项的优势在于可以在与你在 SageMaker 中使用的相同实例类型上运行。这将使你能够重现可能在本地环境中无法重现的问题,例如,与你使用 GPU 资源相关的问题。最简单的方法是使用与 SageMaker 环境(例如相同的操作系统、Python 和 Python 包版本)最相似的 机器镜像 来运行你的实例。或者,你可以拉取 SageMaker 的 DLC 并在远程实例上运行它。然而,尽管这也在云中运行,但运行时环境可能与 SageMaker 的环境有显著差异。SageMaker 在初始化期间配置了一整套系统设置。尝试重现相同环境可能需要相当多的努力。鉴于在云中调试比前两种方法更昂贵,我们的目标应该是在求助于这一选项之前尽可能清理我们的代码。

调试限制

尽管上述每个选项对解决某些类型的错误都很有用,但没有一个能完美地复制 SageMaker 环境。因此,当在 SageMaker 中运行时,你可能会遇到这些方法无法复现的问题,从而无法进行修正。特别是,有许多功能仅在 SageMaker 环境中受支持(例如,SageMaker 的 Pipe inputFast File 模式来访问 Amazon S3 中的数据)。如果你的问题与这些功能相关,你将 无法 在 SageMaker 之外复现它。

调优限制

此外,上述选项未提供有效的性能调优解决方案。运行时性能对环境中的细微变化非常敏感。虽然模拟环境可能提供一些一般优化提示(例如,不同数据增强的比较性能开销),但准确的性能分析只能在 SageMaker 运行时环境中进行。

SageMaker SSH Helper

SageMaker SSH Helper 提供了连接到远程 SageMaker 训练环境的功能。这是通过 AWS SSM 上的 SSH 连接实现的。正如我们将演示的,设置这个功能的步骤非常简单,值得花费精力。 官方文档 包含了关于此工具的价值及其使用方法的详细信息。

示例

在下面的代码块中,我们演示了如何使用 sagemaker-ssh-helper(版本 2.1.0)启用对 SageMaker 训练作业的远程连接。我们传入完整的代码源目录,但将通常的 entry_pointtrain.py)替换为一个新的 run_ssh.py 脚本,该脚本放置在 source_dir 的根目录中。请注意,我们将 SSHEstimatorWrapper 添加到项目依赖项列表中,因为我们的 start_ssh.py 脚本将需要它。或者,我们也可以将 sagemaker-ssh-helper 添加到 requirements.txt 文件中。这里我们将 connection_wait_time_seconds 设置为两分钟。正如我们将看到的,这会影响训练脚本的行为。

from sagemaker.pytorch import PyTorch
from sagemaker_ssh_helper.wrapper import SSHEstimatorWrapper
MINUTE = 60

estimator = PyTorch(
    role='<sagemaker role>',
    entry_point='run_ssh.py',
    source_dir='<path to source dir>',
    instance_type='ml.g5.xlarge',
    instance_count=1,
    framework_version='2.0.1',
    py_version='py310',
    dependencies=[SSHEstimatorWrapper.dependency_dir()]
)

# configure the SSH wrapper. Set the wait time for the connection.
ssh_wrapper = SSHEstimatorWrapper.create(estimator.framework, 
                                    connection_wait_time_seconds=2*MINUTE)

# start job
estimator.fit()

# wait to receive an instance id for the connection over SSM
instance_ids = ssh_wrapper.get_instance_ids()

print(f'To connect run: aws ssm start-session --target {instance_ids[0]}')

和往常一样,SageMaker 服务将分配一个机器实例,建立所请求的环境,下载并解压我们的源代码,并安装所请求的依赖项。此时,运行环境将与我们通常运行训练脚本的环境完全相同。只是现在,我们将运行我们的start_ssh.py脚本,而不是训练:

import sagemaker_ssh_helper
from time import sleep

# setup SSH and wait for connection_wait_time_seconds seconds
# (to give opportunity for the user to connect before script resumes)
sagemaker_ssh_helper.setup_and_start_ssh()

# place any code here... e.g. your training code
# we choose to sleep for two hours to enable connecting in an SSH window
# and running trials there
HOUR = 60*60
sleep(2*HOUR)

setup_and_start_ssh 函数将启动 SSH 服务,然后在我们上面定义的分配时间(connection_wait_time_seconds)内阻塞,以允许 SSH 客户端连接,然后继续执行脚本的其余部分。在我们的例子中,它将睡眠两小时,然后退出训练作业。在这段时间里,我们可以使用aws ssm start-session 命令和由ssh_wrapper 返回的 instance-id(通常以“mi-”前缀开头,代表“管理实例”)连接到机器,尽情玩耍。特别是,我们可以显式运行我们原始的训练脚本(该脚本作为source_dir的一部分上传)并监控训练行为。

我们描述的方法使我们能够在识别和修复错误的同时,迭代运行我们的训练脚本。它还提供了一个优化性能的理想环境——在这个环境中,我们可以 1) 运行几个训练步骤,2) 识别性能瓶颈(例如,使用PyTorch Profiler),3) 调整我们的代码以解决这些瓶颈,然后 4) 重复这个过程,直到我们实现所需的运行时性能。

重要的是,请记住,一旦start_ssh.py 脚本完成,实例将被终止。在为时已晚之前,确保将所有重要文件(例如,代码修改、配置文件跟踪等)复制到持久存储中。

通过 AWS SSM 进行端口转发

我们可以扩展我们的aws ssm start-session 命令,以启用端口转发。这允许您安全地连接到云实例上运行的服务器应用程序。这对于习惯使用TensorBoard Profiler 插件分析运行时性能的开发人员尤其令人兴奋(正如我们所做的)。下面的命令演示了如何通过 AWS SSM 设置端口转发:

aws ssm start-session \
  --target mi-0748ce18cf28fb51b \
  --document-name AWS-StartPortForwardingSession
  --parameters '{"portNumber":["6006"],"localPortNumber":["9999"]}'

使用的其他模式

SageMaker SSH Helper 文档描述了几种使用 SSH 功能的方法。在基础示例中,将 setup_and_start_ssh 命令添加到现有训练脚本的顶部(而不是定义一个专门的脚本)。这使你有时间(根据 connection_wait_time_seconds 设置定义)在训练开始前连接到机器,以便你可以在训练运行时(通过一个独立的进程)监控其行为。

更加高级的示例包括使用 SageMaker SSH Helper 从本地环境中的 IDE 调试运行在 SageMaker 环境中的训练脚本的不同方法。虽然设置更为复杂,但能够从本地 IDE 进行逐行调试的奖励可能非常值得。

其他用例包括在 VPC 中训练、与SageMaker Studio的集成、连接到 SageMaker 推理端点等。请务必查看文档以获取详细信息。

何时使用 SageMaker SSH Helper

考虑到使用 SageMaker SSH Helper 进行调试的优势,你可能会想知道是否有理由使用我们上述描述的三种调试方法。我们认为,尽管你可以在云端进行所有调试,但仍然强烈建议你在本地环境中进行初步开发和实验阶段——尽可能地使用我们描述的前两种方法。只有在你已耗尽本地调试的能力后,才应转到使用 SageMaker SSH Helper 在云端进行调试。你最不希望的就是在一个极其昂贵的云端 GPU 机器上花费数小时清理简单的语法错误。

与调试相反,性能分析和优化的价值不大除非它在目标训练环境中直接进行。因此,建议在使用 SageMaker SSH Helper 的 SageMaker 实例上进行优化工作。

总结

到现在为止,在 Amazon SageMaker 上训练的一个最痛苦的副作用就是失去了对训练环境的直接访问。这限制了我们有效调试和调整训练工作负载的能力。最近发布的 SageMaker SSH Helper 以及对训练环境的直接访问支持,为开发、调试和调整提供了大量新的机会。这些机会可以对你的 ML 开发生命周期的效率和速度产生显著影响。这就是为什么 SageMaker SSH Helper 是我们 2023 年最喜欢的新云 ML 功能之一。

Pytest 教程:单元测试简介

原文:towardsdatascience.com/debugging-made-easy-use-pytest-to-track-down-and-fix-python-code-ecbad62057b8

如何使用 Pytest fixtures 和 mock 进行单元测试

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Egor Howell

·发布于 Towards Data Science ·7 min read·2023 年 4 月 18 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由 Yancy Min 提供,来源于 Unsplash

背景

想象一下,你是一名数据科学家,刚刚开发了一个出色的新模型,这将为公司带来丰厚的利润。接下来的步骤是将其投入生产。你花了几天时间将代码调整为 PEP 标准,应用 linting 等等。最后,你在 GitHub 上创建了一个 pull request,对你的新发布感到兴奋。然后,一位软件工程师问道:‘我看不到任何测试?

这种情况发生在我身上,并且在初级数据科学家中相当频繁。测试 是任何软件项目的核心部分,数据科学也不例外。因此,掌握这个重要概念和工具将对你的职业生涯非常宝贵。在这篇文章中,我深入探讨了测试的必要性以及如何通过使用 Pytest 来轻松进行测试。

什么是测试?

测试是我们自然进行的,通过简单地推断输出是否符合预期,这被称为 exploratory testing。然而,这并不理想,尤其是当你有一个大型代码库和众多步骤时,因为很难检测问题发生的具体位置。

因此,通常会编写代码测试。你会有一些输入和预期输出。这自动化测试过程,并加快调试过程。

最常见和频繁编写的测试是单元测试。这些是测试小块代码的测试,通常是函数和类,以验证该块代码是否按预期工作。

单元测试的一般优点包括:

  • 加快调试和发现问题的速度

  • 更早识别错误

  • 代码更健壮且易于维护

  • 导致更好的代码设计,复杂度更低

单元测试是测试周期中的基础测试,随后是集成测试系统测试

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

软件测试金字塔。图示由作者提供。

Pytest 是什么?

Pytest 是一个易于使用的 Python 包,用于进行单元测试。它是最受欢迎的测试包之一,与 Python 原生的单元测试框架并列。Pytest 相对于其他测试框架有几个优点:

  • 开源

  • 跳过并标记测试

  • 并行测试执行

  • 使用起来非常简单直观

现在让我们开始测试吧!

安装和设置

你可以通过粗体 pip安装pytest,只需输入:

pip install pytest

在你的终端或命令行中。如果你需要特定版本:

pip install pytest==<version>

你可以通过以下命令验证它是否已安装在你的机器上:

pytest --version

最佳实践是将测试放在与主要代码分开的目录中,例如tests/。另一个要求是所有测试文件都以test_*.py为前缀或以*_test.py为后缀,使用蛇形命名法。类似地,所有测试函数和类应以test_Test驼峰命名法)开头。这确保了pytest知道哪些函数、类和文件是测试。

基本示例

让我们来看一个非常简单的例子。

首先,我们将创建一个新的目录pytest-example/,其中包含两个文件:calculations.pytest_calculations.py。在calculations.py文件中,我们将编写以下函数:

def sum(a: float, b: float) -> float:
    """
    Calculate the sum of the two numbers.

    :param a: The first number to be added.
    :param b: The second number to be added.
    :return: The sum of the two numbers.
    """
    return a + b

test_calculations.py文件中,我们编写相应的单元测试:

from calculations import sum

def test_sum():
    assert sum(5, 10) == 15

这个测试可以通过执行以下任意一个命令来运行:

pytest
pytest test_calculations.py

输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来自作者。

好消息,我们的测试通过了!

但是,如果我们的assert不正确:

def test_sum():
    assert sum(5, 10) == 10

输出将是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来自作者。

若干测试

对于不同的函数,可以有几个测试。例如,让我们在calculations.py中添加另一个函数:

def sum(a: float, b: float) -> float:
    """
    Calculate the sum of the two numbers.

    :param a: The first number to be added.
    :param b: The second number to be added.
    :return: The sum of the two numbers.
    """
    return a + b

def multiply(a: float, b: float) -> float:
    """
    Calculate the product of the two numbers.

    :param a: The first number to be added.
    :param b: The second number to be added.
    :return: The product of the two numbers.
    """
    return a * b

然后在 test_calculations.py 中添加 multiply 函数的测试:

from calculations import sum, multiply

def test_sum():
    assert sum(5, 10) == 15

def test_multiply():
    assert multiply(5, 10) == 50

执行 pytest

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来自作者。

两个测试都通过了!

然而,如果你只想运行 test_multiply 函数呢?你只需在执行 pytest 时将该函数名作为参数传递即可:

pytest test_calculations.py::test_multiply 

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来自作者。

正如我们所见,pytest 只运行了 test_multiply,这正是我们所期望的!

如果我们现在想添加一个 divide 函数,最好将它们转换为类:

class Calculations:
    def __init__(self, a: float, b: float) -> None:
        """
        Initialize the Calculation object with two numbers.

        :param a: The first number.
        :param b: The second number.
        """
        self.a = a
        self.b = b

    def sum(self) -> float:
        """
        Calculate the sum of the two numbers.

        :return: The sum of the two numbers.
        """
        return self.a + self.b

    def multiply(self) -> float:
        """
        Calculate the product of the two numbers.

        :return: The product of the two numbers.
        """
        return self.a * self.b

    def divide(self) -> float:
        """
        Calculate the quotient of the two numbers.

        :return: The quotient of the two numbers.
        """
        return self.a / self.b
from calculations import Calculations
import pytest

class TestCalculations:
    def test_sum(self):
        calculations = Calculations(5, 10)
        assert calculations.sum() == 15

    def test_multiply(self):
        calculations = Calculations(5, 10)
        assert calculations.multiply() == 50

    def test_divide(self):
        calculations = Calculations(5, 10)
        assert calculations.divide() == 0.5

Pytest Fixtures

在上面的 TestCalculations 类中,注意到我们多次初始化了 Calculations 类。这并不是最优的,幸运的是,pytestfixtures 来解决这种情况:

from calculations import Calculations
import pytest

@pytest.fixture
def calculations():
    return Calculations(5, 10)

class TestCalculations:
    def test_sum(self, calculations):
        assert calculations.sum() == 15

    def test_multiply(self, calculations):
        assert calculations.multiply() == 50

    def test_divide(self, calculations):
        assert calculations.divide() == 0.5

与其多次初始化Calculations,我们可以将 fixture 作为一个 装饰器 附加,以包含输入数据的信息。

Pytest 参数化

到目前为止,我们只为每个测试函数通过了一个测试用例。然而,可能有多个边缘情况你想要测试和验证。Pytest 通过 parametrize 装饰器使这一过程变得非常简单:

from calculations import Calculations
import pytest

@pytest.fixture
def calculations():
    return Calculations(5, 10)

class TestCalculations:

    @pytest.mark.parametrize("a, b, expected_output",
                             [(1, 3, 4), (10, 50, 60), (100, 0, 100)])
    def test_sum(self, a, b, expected_output):
        assert Calculations(a, b).sum() == expected_output

    def test_multiply(self, calculations):
        assert calculations.multiply() == 50

    def test_divide(self, calculations):
        assert calculations.divide() == 0.5

我们使用了 pytest.mark.parametrize 装饰器来测试 sum 函数的多个输入。输出结果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来自作者。

注意到我们有 5 个测试通过而不是 3 个,这是因为我们对 sum 函数传递了两个额外的测试。

总结与进一步思考

测试,特别是单元测试,是数据科学家必须学习和理解的重要技能,因为它有助于防止 bug 并加快开发速度。在 Python 中,最常见的测试包是 Pytest。这是一个易于使用的框架,具有直观的测试过程。在本文中,我们展示了如何使用 Pytest 的 fixturesparametrize 功能。

本文中使用的完整代码可以在这里找到:

[## Medium-Articles/Software Engineering /pytest-example at main · egorhowell/Medium-Articles

目前你无法执行该操作。你在另一个标签页或窗口中已登录。你在另一个标签页或窗口中已登出…

github.com

另外的事情!

我有一个免费的新闻通讯,Dishing the Data,在其中我分享成为更好数据科学家的每周技巧。

[## 数据分享 | Egor Howell | Substack

如何成为更好的数据科学家。点击阅读由 Egor Howell 发布的 Substack 刊物《数据分享》…

newsletter.egorhowell.com](https://newsletter.egorhowell.com/?source=post_page-----ecbad62057b8--------------------------------)

联系我!

参考文献及进一步阅读

使用 Docker 调试 SageMaker 端点

原文:towardsdatascience.com/debugging-sagemaker-endpoints-with-docker-7a703fae3a26

SageMaker 本地模式的替代方案

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Ram Vegiraju

·发表于Towards Data Science ·阅读时长 6 分钟·2023 年 6 月 16 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来自Unsplash,作者Mohammad Rahmani

启动SageMaker 实时推理的痛点之一是有时很难调试。当创建端点时,需要确保有许多因素得到了妥善处理,以便成功部署。

  • 根据您使用的模型服务器和容器,正确的模型工件文件结构至关重要。本质上,您提供的 model.tar.gz 必须符合模型服务器的格式。

  • 如果您有一个自定义推理脚本,实现了模型的前处理和后处理,您需要确保实现的处理程序与模型服务器兼容,并且代码级别没有脚本错误。

之前我们讨论了 SageMaker 本地模式,但在本文撰写时,本地模式不支持所有 SageMaker 部署可用的托管选项和模型服务器。

为了克服这个限制,我们将探讨如何使用Docker及示例模型,以及如何在 SageMaker 部署之前测试/调试我们的模型工件和推理脚本。在这个具体示例中,我们将利用BART 模型,这是我在上一篇文章中介绍过的,看看如何使用 Docker 托管它。

注意:对于那些刚接触 AWS 的用户,如果你想跟随本文,请确保在以下 链接 上创建一个账户。本文还假设你对 SageMaker 部署有中级了解,我建议你阅读这篇 文章 以更深入地理解部署/推理。对 Docker 有中级了解也将有助于你完全理解这个示例。

SageMaker 托管是如何工作的?

在进入本文的代码部分之前,让我们先看看 SageMaker 实际是如何处理请求的。SageMaker Inference 的核心有两个构件:

  • Container:这建立了模型的运行时环境,它还与您正在使用的模型服务器集成。你可以使用现有的 深度学习容器(DLCs)之一,也可以 构建你自己的容器。

  • 模型工件:在 CreateModel API 调用中,我们指定了一个包含模型数据的 S3 URL,格式为 model.tar.gz(tarball)。这些模型数据被加载到容器上的 opt/ml/model 目录中,这也包括你提供的任何推理脚本。

关键在于容器需要实现一个 Web 服务器,响应端口 8080 上的 /invocations 和 /ping 路径。我们实现的一个 Web 服务器示例是 Flask,在 自定义容器 示例中提供了这些路径。

使用 Docker 时,我们将暴露这个端口,并指向我们的本地脚本和模型工件,这样我们就可以模拟 SageMaker Endpoint 预期的行为。

使用 Docker 进行测试

为了简单起见,我们将使用我上一篇文章中的 BART 示例,你可以从这个 仓库 获取相关工件。在这里,你应该能看到以下文件:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • model.py:这是我们正在使用的推理脚本。在这种情况下,我们使用 DJL Serving,它期望一个包含处理推理的 handler 函数的 model.py。你的推理脚本仍然需要与模型服务器期望的格式兼容。

  • requirements.txt:任何你的 model.py 脚本所需的额外依赖项。对于 DJL Serving,PyTorch 已经预先安装,我们使用 numpy 进行数据处理。

  • serving.properties:这是一个 DJL 特有的文件,你可以在这里定义模型级别的任何配置(例如:每个模型的工作线程数)。

我们已经有了模型工件,现在需要我们将要使用的容器。在这种情况下,我们可以检索现有的 DJL DeepSpeed 镜像。有关 AWS 已提供的镜像的详细列表,请参考这个指南。你也可以在本地构建自己的镜像并指向它。在这种情况下,我们在一个 SageMaker Classic Notebook 实例环境中操作,该环境中也预装了 Docker。

要使用 AWS 提供的现有镜像,我们首先需要登录到 AWS Elastic Container Registry (ECR)以检索镜像,你可以使用以下 shell 命令来完成这一步。

$(aws ecr get-login --region us-east-1 --no-include-email --registry-ids 763104351884)

你应该看到类似于以下的登录成功消息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

登录成功(作者截图)

一旦登录成功,我们可以进入存储模型工件的路径,并运行以下命令来启动模型服务器。如果你还没有检索镜像,这也会从 ECR 中拉取镜像。

docker run \
-v /home/ec2-user/SageMaker:/opt/ml/model \
--cpu-shares 512 \
-p 8080:8080 \
763104351884.dkr.ecr.us-east-1.amazonaws.com/djl-inference:0.21.0-deepspeed0.8.0-cu117 \
serve

这里有几个关键点:

  • 我们暴露了 8080 端口,因为 SageMaker 推理期望如此。

  • 我们还指向现有的镜像。这个字符串取决于你所在的区域和操作的模型。你还可以使用 SageMaker Python SDK 的retrieve image_uri API 调用来识别合适的镜像以进行拉取。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图像正在检索中(作者截图)

在镜像拉取完成后,你会看到模型服务器已启动。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

DJL 服务器已启动(作者截图)

我们还可以通过使用以下 Docker 命令来验证容器是否正在运行。

docker container ls

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

容器已启动(作者截图)

我们看到 API 通过 8080 端口暴露,我们可以通过 curl 向其发送示例请求。注意,我们指定了 SageMaker 容器期望的 /invocations 路径。

curl -X POST http://localhost:8080/invocations -H "Content-type: text/plain"
 "This is a sample test string"

然后我们看到推理返回了请求,并且模型服务器正在跟踪响应并从我们的推理脚本中发出日志语句。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例请求(作者截图)

让我们拆解 model.py,看看是否能通过 Docker 早期捕获到错误。在推理函数中,我添加了一个语法错误的打印语句,并重启模型服务器以查看是否能捕获到这个错误。

def inference(self, inputs):
        """
        Custom service entry point function.

        :param inputs: the Input object holds the text for the BART model to infer upon
        :return: the Output object to be send back
        """

        #sample error
        print("=)

然后我们可以看到,当我们执行 docker run 命令时,模型服务器捕获了这个错误。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

模型服务器捕获的错误(作者截图)

注意,你不仅限于使用 curl 来测试你的容器。我们还可以使用类似于 Python requests 库来与容器进行交互和操作。一个示例请求可能如下所示:

import requests

headers = {
    'Content-type': 'text/plain',
}

response = requests.post('http://localhost:8080/invocations', headers=headers)

利用类似 requests 的工具,你可以对容器进行大规模负载测试。请注意,你运行容器的硬件就是正在被利用的(可以将其视为 SageMaker Endpoint 后面的实例)。

额外资源与结论

## GitHub - RamVegiraju/SageMaker-Docker-Local: 如何使用 Docker 本地测试 SageMaker 推理

如何使用 Docker 本地测试 SageMaker 推理 - GitHub - RamVegiraju/SageMaker-Docker-Local:如何本地测试…

github.com

你可以在上面的链接找到整个示例的代码。使用 SageMaker Inference,你希望避免等待端点创建以捕捉错误的痛苦。通过这种方法,你可以使用任何 SageMaker 容器来测试和调试你的模型工件和推理脚本。

随时欢迎留下反馈或提问,感谢阅读!

如果你喜欢这篇文章,欢迎通过 LinkedIn 与我联系,并订阅我的 Medium Newsletter。如果你是 Medium 的新用户,可以通过我的 Membership Referral注册。

Decent Espresso DE1Pro vs Kim Express:第 2 轮

原文:towardsdatascience.com/decent-espresso-de1pro-vs-kim-express-round-2-80b9324d3fe3?source=collection_archive---------17-----------------------#2023-04-18

咖啡数据科学

更进一步

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Robert McKeon Aloe

·

关注 发表在 Towards Data Science ·5 分钟阅读·2023 年 4 月 18 日

自从回到办公室后,我有时间再次比较 Kim Express 和 Decent Espresso 机器再次。需要说明的是,Kim Express 在办公室。因此,在过去几个月里,我慢慢地收集了一些数据,现在准备分享一些结果。我很高兴自己已经缩小了 Decent 和 Kim 之间的差距,但仍有改进空间。

首先,我对 Decent 的拍摄配置进行了多次修改,特别是泵送和排出配置文件。这个配置文件影响最大,同时,我也在努力改善水输入

起初,我在咖啡饼上使用了一个纸星,后来我转而修改了喷头屏幕,将水流限制在咖啡饼中心的 25%。我也将这些更改应用到了 Kim 上,因此我的拍摄效果总体上有了改善。

数据注意事项

我还没准备好进行真正的配对比较,因为我需要在工作中使用我的研磨机或在家使用我的机器(虽然我还有其他 Kim)。这造成了三个问题:

  1. 对于 Kim,研磨和拍摄之间存在一些延迟(30 分钟到 3 小时),我进行了一些实验以了解研磨年龄如何影响咖啡的味道。确实有差异,但这也允许更高的提取率。

  2. Kim Express 需要更细的研磨。随着喷头屏幕的更改,我将水温从 116°C 降低到了 105°C,但流速非常快,部分原因是研磨非常均匀。

  3. 使用了两台 Kim Express 机器。我经历了一次密封圈故障,因此在更换密封圈时交换了机器。

我们在比较中失去了一些公平性,但我的目标是为每台机器制作最佳的咖啡。

设备/技术

浓缩咖啡机:Decent Espresso Machine 和 Kim Express

咖啡研磨机:Niche Zero 和 Rok

咖啡:自家烘焙咖啡,中度(第一次裂纹后+1 分钟)

拍摄准备:断奏式压实

预浸润:长时间,大约 25 秒

滤篮:20g Wafo Soe Spirit

其他设备:Acaia Pyxis 秤,DiFluid R2 TDS 计)

性能指标

我使用了两组指标来评估技术差异:最终评分和咖啡提取。

最终评分 是对 7 个指标(Sharp, Rich, Syrup, Sweet, Sour, Bitter, 和 Aftertaste)的评分卡的平均值。这些分数当然是主观的,但它们已经根据我的口味进行了校准,并帮助我改进了冲泡。分数存在一定的变化。我的目标是每个指标的一致性,但有时细微差别难以把握。

总溶解固体 (TDS) 是使用折射仪测量的,这个数值与冲泡的输出重量和咖啡的输入重量结合使用,用来确定杯中提取的咖啡百分比,这被称为提取率 (EY)

强度半径 (IR) 被定义为 TDS 与 EY 控制图中原点的半径,因此 IR = sqrt( TDS² + EY²)。这一指标有助于在产量或冲泡比例之间标准化射击性能。

数据

我在 Decent 上进行了 131 次冲泡,在 Kim 上进行了 35 次,涉及 16 次烘焙。从高层次来看,我们可以查看控制图,看到所有冲泡都落在相似的范围内。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所有图片均由作者提供

我们可以将口味加入其中,结果在两台机器之间没有明显差异。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们可以查看每次烘焙,并找出平均 TDS、EY、IR 和口味。这些指标的表现接近。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然而,我们的目标是达到最佳性能。在最大性能方面,DE 似乎表现更好。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从一般统计角度来看,平均水平在口味上表现更佳,而在每次烘焙的最大值中,DE 表现更突出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这两台机器之间,我学到了比我曾经认为的更多。Kim 的主要优势是其群头位于锅炉内部,这带来了一些加热优势。此外,当你打开杠杆让水进入群头时,活塞室允许水蒸发,我想知道这是否使蒸汽预浸更高效。

Kim Express 的主要缺点是如果冲泡过程中出现问题,你不能减慢它的速度。一旦拉下杠杆,冲泡的结果就已经注定,无论是否会出现通道现象。虽然可以做一些小的调整,但 Decent 的流量控制潜力使其在性能上超越 Kim,非常有吸引力。

我想明确一点:我相信 Decent 还能做得更好。我希望 Decent 能大幅度胜出,并且我正在通过我在配置文件更改中的所有实验朝这个目标努力。

如果你喜欢,可以在TwitterYouTubeInstagram上关注我,我会发布关于不同机器上的浓缩咖啡镜头以及相关内容的视频。你也可以在LinkedIn找到我。你还可以在Medium上关注我,并订阅我的内容。

我的进一步阅读

我的书

我的链接

浓缩咖啡文章合集

工作和学校故事集

决策分析与 Python 中的决策树——奥克兰运动员队的案例

原文:towardsdatascience.com/decision-analysis-and-trees-in-python-the-case-of-the-oakland-as-786d746cdfb2

使用 Python 中的决策树来洞察奥克兰运动员队(A’s)迁往拉斯维加斯的决策

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Giovanni Malloy

·发表在数据科学前沿 ·阅读时长 17 分钟·2023 年 5 月 24 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Rick Rodriguez提供,来源于Unsplash

最近,奥克兰运动员队的所有者约翰·费舍尔宣布球队已在内华达州拉斯维加斯购买了近 50 英亩的土地。[1] 这使得奥克兰最后一支职业体育球队的未来岌岌可危。在过去 5 年中,奥克兰见证了金州勇士(NBA)和拉斯维加斯突袭者(NFL)迁往其他城市的新球场(尽管金州勇士只是跨越湾桥迁往旧金山)。虽然奥克兰运动员队管理层的决策过程对我仍然是一个谜,但数据科学和决策分析的结合可以揭示约翰·费舍尔迁往拉斯维加斯的动机。

决策分析对于所有数据科学家都非常重要,因为它是概率和统计模型的高度技术工作与商业决策之间的桥梁。了解商业决策的制定过程有助于框定我们的工作及向非技术观众展示我们的发现,同时提供可行的建议和发现。运筹学与管理科学研究所(INFORMS)甚至有一个专门致力于决策分析的学会

此外,机器学习可以通过解锁概率敏感性分析的见解来帮助推广决策分析的结果。在使用决策分析初步构建分析奥克兰与拉斯维加斯情境的模型后,我们将使用机器学习挖掘可能揭示可操作建议的模式,以便在决策情况发生变化时为 A 队提供帮助。

什么是决策分析?

决策分析是致力于“系统性、定量化和可视化方法来解决和评估重要选择”的研究领域。[2] 它可以在数据较少的环境中成为强大的工具,并帮助个人利用主题领域的专业知识和先前知识来改善复杂决策的预期价值。它被广泛应用于经济学、管理学和政策分析等多个领域。

通常,在决策分析领域,我们采取贝叶斯视角。贝叶斯定理的基本公式如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者创建。

其中 P(A) 是事件 A 发生的概率,P(B) 是事件 B 发生的概率,P(A|B) 是在事件 B 发生的情况下事件 A 发生的概率,而 P(B|A) 是在事件 A 发生的情况下事件 B 发生的概率。通常,P(A) 代表关于 A 发生的先验信念,而 B 代表一些新数据。P(A|B) 是在观察到 B 之后对 A 发生概率的更新后验信念。

例如,假设我们去奥克兰-阿拉米达县体育场看比赛,但我们没有跟踪球员统计数据。我们从以下知识开始:外场手上垒的概率是 0.35,内场手上垒的概率是 0.25,而指定击球手上垒的概率是 0.4。设 A 为下一个击球员是外场手的事件,B 为下一个击球员是内场手的事件,C 为下一个击球员是指定击球手的事件。由于我们知道棒球队的名单,我们已经知道 P(A) = 0.33,P(B) = 0.56,和 P© = 0.11。现在,下一个击球员上场,令我们高兴的是,他成功上垒(事件 D)!根据我们之前的棒球知识,我们知道 P(D|A) = 0.35,P(D|B) = 0.25,和 P(D|C) = 0.4。利用全概率定理,我们可以计算出 P(D) = P(D|A)P(A) + P(D|B)P(B) + P(D|C)P© = 0.3。现在,我们可以更新我们对击球员类型的信念:P(A|D) = 0.39,P(B|D) = 0.47,和 P(C|D) = 0.15。看到球员上垒后,我们现在更倾向于相信这名球员不是内场手。既然你已经调整了心态,让我们继续。

决策分析中的关键工具是决策树(不要与同名的机器学习算法混淆)。[3] 决策树有两个基本组成部分:决策节点和选择节点。[3] 在这篇博客中,我将向你展示如何构建决策树,如何在 Python 中评估它,并理解奥克兰 A 队迁往拉斯维加斯的决策。

决策是什么?

霍华德和阿巴斯将决策定义为“在两个或更多备选方案之间做出选择,并涉及不可撤销的资源分配。”[3] 这是一个宽泛的定义,但在我们以奥克兰 A 队为例的情况下,决策是:运动家棒球队应该留在奥克兰还是迁往拉斯维加斯? 在这种情况下,决策是不可撤销的,因为无论选择哪个城市,他们都会建造一个新体育场。

不确定性是什么?

每个决策都存在不确定性。在是否留在奥克兰还是迁往拉斯维加斯的决策中,A 队不确定新体育场的成本和随后的运营收入:1)他们将获得多少公共资金用于建造新体育场,2)他们将从票务销售中产生多少收入,以及 3)他们将从地方电视合同中产生多少收入。

A 队目前希望在拉斯维加斯建造一座价值 15 亿美元的体育场。[1] 回到 2021 年,该组织曾要求 855 百万美元的公共资金来帮助在奥克兰建造新体育场,尽管之前与市政府和县政府达成了新体育场将由私人资金资助的协议。[1] 因此,我们可以合理地假设,建造体育场的成本在两个地方大致相同。唯一的不确定性是有多少纳税人的钱将用于资助体育场。

票务收入在不同球队之间差异巨大,从 2700 万到 1.31 亿美元不等,中位数约为 7500 万。[4] 奥克兰的票务收入估计为约 5500 万。[4]

MLB 的电视收入通过 MLB 谈判的全国电视合同均匀分配。然而,个别球队电视收入的一个重要组成部分来自地区体育网络(RSN)。球队可以保留来自地方电视合同的大部分收入,尽管仍有大量的收入共享。经过收入共享后,RSN 的电视合同收入从 3600 万到 1.31 亿美元不等,除了最有价值的球队之外,其余球队的收入都少于 6000 万。[4]

多亏了几年前突袭者(NFL)从奥克兰迁往拉斯维加斯,我们知道拉斯维加斯市愿意提供 7.5 亿美元的公共资金来建造一座全新的足球场。[5] 我们还知道,无论是当地人还是游客,都愿意加入并支持一支新的职业球队,因为突袭者在 2021 年以 1.19 亿美元的票务收入领先 NFL。[6]

有些方法超出了本博客的范围,这些方法旨在询问决策者对这些不确定性可能结果及其概率的先验信念。此外,我怀疑 John Fischer 是否准备为我的博客做出评论。因此,在此期间,我将使用从这些网络来源中汇总的信息,提供每个不确定性的一些可能场景。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者创建。

我们的决策时间范围是什么?

当然,收入是年度数据,体育场应该使用远超过一年。时间范围可以根据决策的背景和决策者对景观变化可能性的看法而有所不同。从数据科学的角度来看,这与数据漂移相符,其中用于训练模型的数据与当前数据不同。现在,假设这些估计在十年内保持相对稳定,我们将使用 10 年的时间范围以及 3%的折现率来计算我们的年度成本。

决策树是什么样的?

现在我们已经定义了决策树的所有组件,是时候建立树了。概念上,它看起来是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者创建。

方形节点是决策节点,圆形节点是机会节点,三角形节点是终端节点。由于空间限制,图像中无法显示整个树,但每个节点都有相关的概率和值。

我们如何在 Python 中构建模型?

在决策分析中,建立决策树的构造后,我们可以通过“回滚”树来识别最佳决策。在此示例中,我们假设决策者是理性的(即期望值)决策者。因此,我们首先列出终端状态的相关值(如果适用),这将成为我们的累计总额或期望值。在这种情况下,这不适用,因此我们从$0 开始。然后,我们迭代计算终端节点左侧每组节点的期望值,并将其添加到累计总额或期望值中。最后,我们将得到一个留在奥克兰的决策期望值和一个迁移到拉斯维加斯的决策期望值。

让我们从一个简单的基本情况设置开始。我们将创建一个包含所有可能的决策、公款、票务销售和 RSN 收入情景的数据框。

import numpy as np
import pandas as pd

# Create data frame of all possible outcomes
decision_list = ['Oakland', 'Las Vegas']

# First Node
chance_node_stadium_money_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_stadium_money_probabilities_oakland = [0.1, 0.3, 0.6]
chance_node_stadium_money_probabilities_vegas = [0.5, 0.4, 0.1]
chance_node_stadium_money_values = [855, 500, 0]

#Second Node
chance_node_ticket_sales_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_ticket_sales_probabilities_oakland = [0.2, 0.2, 0.6]
chance_node_ticket_sales_probabilities_vegas = [0.3, 0.4, 0.3]
chance_node_ticket_sales_values_per_year = [80, 55, 27]

# Third Node
chance_node_rsn_revenue_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_rsn_revenue_probabilities_oakland = [0.15, 0.5, 0.35]
chance_node_rsn_revenue_probabilities_vegas = [0.1, 0.3, 0.6]
chance_node_rsn_revenue_values_per_year = [60, 45, 36]

# Convert annual values to NPV of 10 year time horizon
time_horizon = 10 # years
discount_rate = 0.03 # per year
chance_node_ticket_sales_values = [val * (1 - (1/((1 + discount_rate)**time_horizon)))/discount_rate for val in chance_node_ticket_sales_values_per_year]
chance_node_rsn_revenue_values = [val * (1 - (1/((1 + discount_rate)**time_horizon)))/discount_rate for val in chance_node_rsn_revenue_values_per_year]

# Create data frame of all possible scenarios
decision_list_list_for_df = []
chance_node_stadium_money_list_for_df = []
chance_node_stadium_money_probability_list_for_df = []
chance_node_stadium_money_value_list_for_df = []
chance_node_ticket_sales_list_for_df = []
chance_node_ticket_sales_probability_list_for_df = []
chance_node_ticket_sales_value_list_for_df = []
chance_node_rsn_revenue_list_for_df = []
chance_node_rsn_revenue_probability_list_for_df = []
chance_node_rsn_revenue_value_list_for_df = []

for i in decision_list:
    for j in range(len(chance_node_stadium_money_scenarios)):
        for k in range(len(chance_node_rsn_revenue_scenarios)):
            for m in range(len(chance_node_rsn_revenue_scenarios)):
                decision_list_list_for_df.append(i)
                chance_node_stadium_money_list_for_df.append(chance_node_stadium_money_scenarios[j])
                chance_node_stadium_money_value_list_for_df.append(chance_node_stadium_money_values[j])
                chance_node_ticket_sales_list_for_df.append(chance_node_ticket_sales_scenarios[k])
                chance_node_ticket_sales_value_list_for_df.append(chance_node_ticket_sales_values[k])
                chance_node_rsn_revenue_list_for_df.append(chance_node_rsn_revenue_scenarios[m])
                chance_node_rsn_revenue_value_list_for_df.append(chance_node_rsn_revenue_values[m])

                if i == 'Oakland':
                    chance_node_stadium_money_probability_list_for_df.append(chance_node_stadium_money_probabilities_oakland[j])
                    chance_node_ticket_sales_probability_list_for_df.append(chance_node_ticket_sales_probabilities_oakland[k])
                    chance_node_rsn_revenue_probability_list_for_df.append(chance_node_rsn_revenue_probabilities_oakland[m])
                elif i == 'Las Vegas':
                    chance_node_stadium_money_probability_list_for_df.append(chance_node_stadium_money_probabilities_vegas[j])
                    chance_node_ticket_sales_probability_list_for_df.append(chance_node_ticket_sales_probabilities_vegas[k])
                    chance_node_rsn_revenue_probability_list_for_df.append(chance_node_rsn_revenue_probabilities_vegas[m])

decision_tree_df = pd.DataFrame(list(zip(decision_list_list_for_df, chance_node_stadium_money_list_for_df,
                                         chance_node_stadium_money_probability_list_for_df,
                                         chance_node_stadium_money_value_list_for_df,
                                         chance_node_ticket_sales_list_for_df,
                                         chance_node_ticket_sales_probability_list_for_df,
                                         chance_node_ticket_sales_value_list_for_df,
                                         chance_node_rsn_revenue_list_for_df,
                                         chance_node_rsn_revenue_probability_list_for_df,
                                         chance_node_rsn_revenue_value_list_for_df)),
                               columns = ['Decision',
                                          'Stadium_Money_Result', 'Stadium_Money_Prob', 'Stadium_Money_Value',
                                          'Ticket_Sales_Result', 'Ticket_Sales_Prob', 'Ticket_Sales_Value', 
                                          'RSN_Revenue_Result', 'RSN_Revenue_Prob', 'RSN_Revenue_Value'])

现在,如果你打印你的决策树,你将得到一个包含 54 行和 10 列的 pandas dataframe。我们可以通过创造性地使用 groupby 和 merge 函数轻松地回滚决策树。我们从为每种决策、体育场资金和票务销售的组合列出 RSN 收入的期望值开始:

decision_tree_df['RSN_EV'] = decision_tree_df['RSN_Revenue_Prob'] * decision_tree_df['RSN_Revenue_Value']

# Consolidate the RSN_EV values
RSN_rollback_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])['RSN_EV'].sum().reset_index()

# Keep the rest of the columns
decision_tree_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])['Stadium_Money_Value', 'Ticket_Sales_Value'].mean().reset_index()

# merge two dataframes
decision_tree_df = pd.merge(decision_tree_df, RSN_rollback_df, on = ['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])

结果表格已经缩小,现在你可以直观地看到回滚的 RSN 收入节点的期望值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者创建

重复处理票务销售。我们有以下代码:

decision_tree_df['Ticket_Sales_RSN_EV'] = decision_tree_df['Ticket_Sales_Prob'] * decision_tree_df['Ticket_Sales_Value'] + decision_tree_df['RSN_EV']

# Consolidate the Ticket Sales and RSN_EV values
ticket_sales_rollback_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])['Ticket_Sales_RSN_EV'].sum().reset_index()

# Keep the rest of the columns
decision_tree_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])['Stadium_Money_Value'].mean().reset_index()

# merge two dataframes
decision_tree_df = pd.merge(decision_tree_df, ticket_sales_rollback_df, on = ['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])

结果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者创建。

最后,重复进行公共资金贡献的标准计算:

decision_tree_df['Stadium_Money_Ticket_Sales_RSN_EV'] = decision_tree_df['Stadium_Money_Prob'] * decision_tree_df['Stadium_Money_Value'] + decision_tree_df['Ticket_Sales_RSN_EV']

# Consolidate the Stadium Money, Ticket Sales, and RSN_EV values
decision_tree_df = decision_tree_df.groupby(['Decision'])['Stadium_Money_Ticket_Sales_RSN_EV'].sum().reset_index()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者创建

在这里,我们可以看到模型计算出在 10 年的时间范围内,留在奥克兰的预期价值为 47 亿美元,而搬到拉斯维加斯的预期价值为 52 亿美元。

我们如何将模型进行概括?

当然,我们的数据和模型中都有不确定性,我们可以测试许多不同的情景。自然,我们可能会定义一些阈值或情景,在这些情况下,决策从留在奥克兰变为搬到拉斯维加斯(或反之)。这些决策点可以作为决策者的“业务规则”集合,并帮助我们作为数据科学家从分析中提取可操作的建议。

有很多方法可以实现这一目标,但在本博客中,我们将使用机器学习元建模。元建模涉及开发一个比原始数学或模拟模型更快(有时更简单)的模型,该模型采用相同的输入并产生非常相似的输出[7]。在这种情况下,我们将使用概率敏感性分析来测试决策分析决策树的大量参数空间,并记录每个参数集的结果决策。然后,我们将使用参数集作为特征,并将结果决策作为标签来训练机器学习决策树分类模型。机器学习模型的好处在于它可以揭示复杂的关系,这些关系仅靠多变量敏感性分析是难以解读的。我们的希望是,能从一个浅层树中获得足够的准确性,以描述 A 应该留在奥克兰还是搬到拉斯维加斯的情景。

首先,我们开始设计一个概率敏感性分析。对于这个例子,我们将假设机会节点的美元值保持不变,但各种结果的概率会有所不同。由于我们知道概率将在 0 到 1 之间变化,我们将假设所有情景概率是等可能的,并使用均匀分布进行建模,最小值为 0,最大值为 1。经过三次从均匀分布中抽样(分别对应乐观、中性和悲观情景),我们将结果归一化,使三个概率的总和为 1。

# Number of simulations
n_sim = 5000

# Track scenarios
oakland_stadium_money_probabilities_optimistic_list = []
oakland_stadium_money_probabilities_neutral_list = []
oakland_stadium_money_probabilities_pessimistic_list = []

oakland_ticket_sales_probabilities_optimistic_list = []
oakland_ticket_sales_probabilities_neutral_list = []
oakland_ticket_sales_probabilities_pessimistic_list = []

oakland_rsn_revenue_probabilities_optimistic_list = []
oakland_rsn_revenue_probabilities_neutral_list = []
oakland_rsn_revenue_probabilities_pessimistic_list = []

vegas_stadium_money_probabilities_optimistic_list = []
vegas_stadium_money_probabilities_neutral_list = []
vegas_stadium_money_probabilities_pessimistic_list = []

vegas_ticket_sales_probabilities_optimistic_list = []
vegas_ticket_sales_probabilities_neutral_list = []
vegas_ticket_sales_probabilities_pessimistic_list = []

vegas_rsn_revenue_probabilities_optimistic_list = []
vegas_rsn_revenue_probabilities_neutral_list = []
vegas_rsn_revenue_probabilities_pessimistic_list = []

oakland_EV_list = []
vegas_EV_list = []

decision_list = []

# Create data frame of all possible outcomes
decision_list = ['Oakland', 'Las Vegas']

# First Node
chance_node_stadium_money_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_stadium_money_values = [855, 500, 0]

#Second Node
chance_node_ticket_sales_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_ticket_sales_values_per_year = [80, 55, 27]

# Third Node
chance_node_rsn_revenue_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_rsn_revenue_values_per_year = [60, 45, 36]

# Convert annual values to NPV of 10 year time horizon
time_horizon = 10 # years
discount_rate = 0.03 # per year
chance_node_ticket_sales_values = [val * (1 - (1/((1 + discount_rate)**time_horizon)))/discount_rate for val in chance_node_ticket_sales_values_per_year]
chance_node_rsn_revenue_values = [val * (1 - (1/((1 + discount_rate)**time_horizon)))/discount_rate for val in chance_node_rsn_revenue_values_per_year]

# Run the probabilistic sensitivity analysis n_sim times
for n in range(n_sim):

    ## Set up tree
    #First node
    chance_node_stadium_money_probabilities_oakland = np.random.uniform(0,1,3)
    chance_node_stadium_money_probabilities_oakland = chance_node_stadium_money_probabilities_oakland / np.sum(chance_node_stadium_money_probabilities_oakland)

    chance_node_stadium_money_probabilities_vegas = np.random.uniform(0,1,3)
    chance_node_stadium_money_probabilities_vegas = chance_node_stadium_money_probabilities_vegas / np.sum(chance_node_stadium_money_probabilities_vegas)

    #Second Node
    chance_node_ticket_sales_probabilities_oakland = np.random.uniform(0,1,3)
    chance_node_ticket_sales_probabilities_oakland = chance_node_ticket_sales_probabilities_oakland / np.sum(chance_node_ticket_sales_probabilities_oakland)

    chance_node_ticket_sales_probabilities_vegas = np.random.uniform(0,1,3)
    chance_node_ticket_sales_probabilities_vegas = chance_node_ticket_sales_probabilities_vegas / np.sum(chance_node_ticket_sales_probabilities_vegas)

    # Third Node
    chance_node_rsn_revenue_probabilities_oakland = np.random.uniform(0,1,3)
    chance_node_rsn_revenue_probabilities_oakland = chance_node_rsn_revenue_probabilities_oakland / np.sum(chance_node_rsn_revenue_probabilities_oakland)

    chance_node_rsn_revenue_probabilities_vegas = np.random.uniform(0,1,3)
    chance_node_rsn_revenue_probabilities_vegas = chance_node_rsn_revenue_probabilities_vegas / np.sum(chance_node_rsn_revenue_probabilities_vegas)

    # Evaluate Tree
    # Create data frame of all possible scenarios
    decision_list_list_for_df = []
    chance_node_stadium_money_list_for_df = []
    chance_node_stadium_money_probability_list_for_df = []
    chance_node_stadium_money_value_list_for_df = []
    chance_node_ticket_sales_list_for_df = []
    chance_node_ticket_sales_probability_list_for_df = []
    chance_node_ticket_sales_value_list_for_df = []
    chance_node_rsn_revenue_list_for_df = []
    chance_node_rsn_revenue_probability_list_for_df = []
    chance_node_rsn_revenue_value_list_for_df = []

    for i in decision_list:
        for j in range(len(chance_node_stadium_money_scenarios)):
            for k in range(len(chance_node_rsn_revenue_scenarios)):
                for m in range(len(chance_node_rsn_revenue_scenarios)):
                    decision_list_list_for_df.append(i)
                    chance_node_stadium_money_list_for_df.append(chance_node_stadium_money_scenarios[j])
                    chance_node_stadium_money_value_list_for_df.append(chance_node_stadium_money_values[j])
                    chance_node_ticket_sales_list_for_df.append(chance_node_ticket_sales_scenarios[k])
                    chance_node_ticket_sales_value_list_for_df.append(chance_node_ticket_sales_values[k])
                    chance_node_rsn_revenue_list_for_df.append(chance_node_rsn_revenue_scenarios[m])
                    chance_node_rsn_revenue_value_list_for_df.append(chance_node_rsn_revenue_values[m])

                    if i == 'Oakland':
                        chance_node_stadium_money_probability_list_for_df.append(chance_node_stadium_money_probabilities_oakland[j])
                        chance_node_ticket_sales_probability_list_for_df.append(chance_node_ticket_sales_probabilities_oakland[k])
                        chance_node_rsn_revenue_probability_list_for_df.append(chance_node_rsn_revenue_probabilities_oakland[m])
                    elif i == 'Las Vegas':
                        chance_node_stadium_money_probability_list_for_df.append(chance_node_stadium_money_probabilities_vegas[j])
                        chance_node_ticket_sales_probability_list_for_df.append(chance_node_ticket_sales_probabilities_vegas[k])
                        chance_node_rsn_revenue_probability_list_for_df.append(chance_node_rsn_revenue_probabilities_vegas[m])

    decision_tree_df = pd.DataFrame(list(zip(decision_list_list_for_df, chance_node_stadium_money_list_for_df,
                                             chance_node_stadium_money_probability_list_for_df,
                                             chance_node_stadium_money_value_list_for_df,
                                             chance_node_ticket_sales_list_for_df,
                                             chance_node_ticket_sales_probability_list_for_df,
                                             chance_node_ticket_sales_value_list_for_df,
                                             chance_node_rsn_revenue_list_for_df,
                                             chance_node_rsn_revenue_probability_list_for_df,
                                             chance_node_rsn_revenue_value_list_for_df)),
                                   columns = ['Decision',
                                              'Stadium_Money_Result', 'Stadium_Money_Prob', 'Stadium_Money_Value',
                                              'Ticket_Sales_Result', 'Ticket_Sales_Prob', 'Ticket_Sales_Value', 
                                              'RSN_Revenue_Result', 'RSN_Revenue_Prob', 'RSN_Revenue_Value'])
    decision_tree_df['RSN_EV'] = decision_tree_df['RSN_Revenue_Prob'] * decision_tree_df['RSN_Revenue_Value']

    # Consolidate the RSN_EV values
    RSN_rollback_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])['RSN_EV'].sum().reset_index()

    # Keep the rest of the columns
    decision_tree_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])['Stadium_Money_Value', 'Ticket_Sales_Value'].mean().reset_index()

    # merge two dataframes
    decision_tree_df = pd.merge(decision_tree_df, RSN_rollback_df, on = ['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])

    decision_tree_df['Ticket_Sales_RSN_EV'] = decision_tree_df['Ticket_Sales_Prob'] * decision_tree_df['Ticket_Sales_Value'] + decision_tree_df['RSN_EV']

    # Consolidate the Ticket Sales and RSN_EV values
    ticket_sales_rollback_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])['Ticket_Sales_RSN_EV'].sum().reset_index()

    # Keep the rest of the columns
    decision_tree_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])['Stadium_Money_Value'].mean().reset_index()

    # merge two dataframes
    decision_tree_df = pd.merge(decision_tree_df, ticket_sales_rollback_df, on = ['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])

    decision_tree_df['Stadium_Money_Ticket_Sales_RSN_EV'] = decision_tree_df['Stadium_Money_Prob'] * decision_tree_df['Stadium_Money_Value'] + decision_tree_df['Ticket_Sales_RSN_EV']

    # Consolidate the Stadium Money, Ticket Sales, and RSN_EV values
    decision_tree_df = decision_tree_df.groupby(['Decision'])['Stadium_Money_Ticket_Sales_RSN_EV'].sum().reset_index()

    # Fill out lists for meta-model inputs
    oakland_stadium_money_probabilities_optimistic_list.append(chance_node_stadium_money_probabilities_oakland[0])
    oakland_stadium_money_probabilities_neutral_list.append(chance_node_stadium_money_probabilities_oakland[1])
    oakland_stadium_money_probabilities_pessimistic_list.append(chance_node_stadium_money_probabilities_oakland[2])

    oakland_ticket_sales_probabilities_optimistic_list.append(chance_node_ticket_sales_probabilities_oakland[0])
    oakland_ticket_sales_probabilities_neutral_list.append(chance_node_ticket_sales_probabilities_oakland[1])
    oakland_ticket_sales_probabilities_pessimistic_list.append(chance_node_ticket_sales_probabilities_oakland[2])

    oakland_rsn_revenue_probabilities_optimistic_list.append(chance_node_rsn_revenue_probabilities_oakland[0])
    oakland_rsn_revenue_probabilities_neutral_list.append(chance_node_rsn_revenue_probabilities_oakland[1])
    oakland_rsn_revenue_probabilities_pessimistic_list.append(chance_node_rsn_revenue_probabilities_oakland[2])

    vegas_stadium_money_probabilities_optimistic_list.append(chance_node_stadium_money_probabilities_vegas[0])
    vegas_stadium_money_probabilities_neutral_list.append(chance_node_stadium_money_probabilities_vegas[1])
    vegas_stadium_money_probabilities_pessimistic_list.append(chance_node_stadium_money_probabilities_vegas[2])

    vegas_ticket_sales_probabilities_optimistic_list.append(chance_node_ticket_sales_probabilities_vegas[0])
    vegas_ticket_sales_probabilities_neutral_list.append(chance_node_ticket_sales_probabilities_vegas[1])
    vegas_ticket_sales_probabilities_pessimistic_list.append(chance_node_ticket_sales_probabilities_vegas[2])

    vegas_rsn_revenue_probabilities_optimistic_list.append(chance_node_rsn_revenue_probabilities_vegas[0])
    vegas_rsn_revenue_probabilities_neutral_list.append(chance_node_rsn_revenue_probabilities_vegas[1])
    vegas_rsn_revenue_probabilities_pessimistic_list.append(chance_node_rsn_revenue_probabilities_vegas[2])

    oakland_EV_list.append(decision_tree_df['Stadium_Money_Ticket_Sales_RSN_EV'][0])
    vegas_EV_list.append(decision_tree_df['Stadium_Money_Ticket_Sales_RSN_EV'][1])

    print(n)

现在我们可以将结果放入一个新的数据框中,以便用于训练我们的机器学习模型:

decision_tree_psa_data_df = pd.DataFrame(list(zip(oakland_stadium_money_probabilities_optimistic_list, 
                                         oakland_stadium_money_probabilities_neutral_list,
                                         oakland_stadium_money_probabilities_pessimistic_list,
                                         oakland_ticket_sales_probabilities_optimistic_list,
                                         oakland_ticket_sales_probabilities_neutral_list,
                                         oakland_ticket_sales_probabilities_pessimistic_list,
                                         oakland_rsn_revenue_probabilities_optimistic_list,
                                         oakland_rsn_revenue_probabilities_neutral_list,
                                         oakland_rsn_revenue_probabilities_pessimistic_list,
                                         vegas_stadium_money_probabilities_optimistic_list,
                                         vegas_stadium_money_probabilities_neutral_list, 
                                         vegas_stadium_money_probabilities_pessimistic_list,
                                         vegas_ticket_sales_probabilities_optimistic_list,
                                         vegas_ticket_sales_probabilities_neutral_list,
                                         vegas_ticket_sales_probabilities_pessimistic_list,
                                         vegas_rsn_revenue_probabilities_optimistic_list,
                                         vegas_rsn_revenue_probabilities_neutral_list, 
                                         vegas_rsn_revenue_probabilities_pessimistic_list,
                                         oakland_EV_list, vegas_EV_list)),
                                   columns = ['oakland_stad_mon_prob_optimistic',
                                              'oakland_stad_mon_prob_neutral',
                                              'oakland_stad_mon_prob_pessimistic',
                                              'oakland_ticket_sales_prob_optimistic',
                                              'oakland_ticket_sales_prob_neutral',
                                              'oakland_ticket_sales_prob_pessimistic',
                                              'oakland_rsn_rev_prob_optimistic',
                                              'oakland_rsn_rev_prob_neutral',
                                              'oakland_rsn_rev_prob_pessimistic',
                                              'vegas_stad_mon_prob_optimistic',
                                              'vegas_stad_mon_prob_neutral',
                                              'vegas_stad_mon_prob_pessimistic',
                                              'vegas_ticket_sales_prob_optimistic',
                                              'vegas_ticket_sales_prob_neutral',
                                              'vegas_ticket_sales_prob_pessimistic',
                                              'vegas_rsn_rev_prob_optimistic',
                                              'vegas_rsn_rev_prob_neutral',
                                              'vegas_rsn_rev_prob_pessimistic',
                                              'oakland_EV', 'vegas_EV'])

# Add decision based on EV
decision_tree_psa_data_df['decision'] = 'Oakland'
decision_tree_psa_data_df.loc[decision_tree_psa_data_df['vegas_EV'] > decision_tree_psa_data_df['oakland_EV'],'decision'] = 'Las Vegas'

我们将使用sci-kit learn 包来训练一个基本的机器学习决策树。由于输入数据是 0 到 1 之间的概率,并且我们使用的是基于树的模型,所以不需要进行特征缩放或工程。为了博客的可视化目的,我将树的深度限制为 3。然而,树的深度越大,越有可能实现更高的准确度。

from sklearn.datasets import load_iris
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import train_test_split
from sklearn import tree

#Features
X = decision_tree_psa_data_df.drop(['oakland_EV', 'vegas_EV', 'decision'], axis = 1)
#labels
y = decision_tree_psa_data_df['decision']

# split into train (70%) and test set (30%)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=32)

# Create decision tree model with maximum depth of 3 to keep recommendation managable
dec_tree_model = tree.DecisionTreeClassifier(random_state=32, max_depth = 3, class_weight = 'balanced')
dec_tree_model = dec_tree_model.fit(X_train, y_train)

我们的模型最终得到了一个不错但不完美的 AUC,接近 0.8。 (AUC 是基于真正和假正率来衡量模型准确性的一种方法。有关模型准确性度量的更多信息,请查看我之前关于评估 ESPN 幻想足球预测分数准确性的博客这里。) 这对于我们继续进行练习来说足够尊重。当然,还有很多方法可以提高决策树分类器的准确性,包括增加最大深度、超参数调整或运行更多的模拟以增加数据量。

from sklearn.metrics import roc_auc_score
roc_auc_score(y_test, dec_tree_model.predict_proba(X_test)[:, 1])

现在我们对性能满意,可以直观地检查训练的决策树。树中的每个分裂表示一组业务规则的另一个维度。在打印的树中的每个框(或叶子)中,第一行将表示模型用于分割数据的规则,第二行是基尼指数,它描述了叶子中的类别分布(其中 0.5 表示每个类别的数量相等,0 或 1 表示只有一个类别),第三行显示每个类别的样本数量,第四行显示模型分配给该叶子中所有样本的标签。我们可以打印出结果树如下:

# Plot decision tree results to see how decisions were made
import matplotlib.pyplot as plt
fig = plt.figure(figsize = (14,14))
tree.plot_tree(dec_tree_model, filled = True, feature_names = X.columns, fontsize = 8, class_names = ['Las Vegas', 'Oakland'])
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者创建。

从我们的机器学习决策树中,我们可以看到,A 队是否应该留在奥克兰或迁移到拉斯维加斯的分类首先依赖于乐观的 RSN 收入概率,其次是与奥克兰票务销售相关的概率。

拉斯维加斯可能是首选目的地,当:

  • 拉斯维加斯的乐观 RSN 收入的概率大于 0.4(除非奥克兰的乐观 RSN 收入的概率大于 0.341 并且奥克兰的乐观票务销售的概率大于 0.355)

  • 或者乐观的 RSN 收入在奥克兰的概率小于或等于 0.468,并且乐观的票务销售在拉斯维加斯的概率大于 0.438。

有趣的是,尽管媒体对新球场的公共或私人资金进行了一番喧哗,我们的模型却指向了 RSN 收入和票务销售。这种差异可能是由于我们 10 年的时间范围,或者可能是组织寻找一个经过 MLB 批准的借口来离开奥克兰。不管怎样,这种方法突显了数据科学团队可以向决策者提供的一个重要见解,以便为商业战略提供信息。 这种方法可以将你的模型从有趣的理论练习转变为改变 C-suite 中的思维。

我们如何验证机器学习模型?

鉴于我们正在尝试提供一个极其重要的决策信息,确保我们的模型对输入数据的差异或标签的不平衡类集具有鲁棒性非常重要。为了考虑后者,你会注意到,我们在创建机器学习模型时包含了 class_weight = ‘balanced’。为了考虑前者以及模型验证,我们可以使用交叉验证得分来查看其他训练/测试分割性能指标是什么:

# 10-fold cross-validation scores
cross_val_score(dec_tree_model, X, y, cv=10)

输出如下:array([0.724, 0.722, 0.718, 0.72 , 0.722, 0.708, 0.732, 0.726, 0.76, 0.702]),这告诉我们,在 10 种不同的训练/测试分割中,我们的模型表现相似。

我们学到了什么?

有了这一点,我们已经从关于 A 队棒球队搬迁的商业问题,回溯决策分析决策树模型,揭示为什么 A 队可能会前往拉斯维加斯,再到利用机器学习决策树将我们的结果推广成管理层可以用来决定是否重新定位的可消化商业规则。希望你能利用类似的方法或方法来通知你自己组织或日常生活中的决策者。

参考文献

[1] Sutelan, E, 亚特兰大运动员拉斯维加斯搬迁时间表:球场挫折,资金失败通往 A 队离开奥克兰的道路 (2023), 体育新闻

[2] Kenton, W, 决策分析(DA):定义、用途和示例 (2022), Investopedia

[3] Howard, R. 和 Abbas, A, 决策分析基础 (2014)

[4] Morss, E., 大联盟棒球财务:数字告诉我们什么 (2019), Morss 全球金融

[5] Greer, J., 为什么突袭者队迁移到拉斯维加斯?解释 2020 年从奥克兰迁移到罪恶之城的球队 (2020), 体育新闻

[6] Andre, D. 报告:突袭者队在 2021 年 NFL 票务收入中排名第一 (2022), Fox 5 拉斯维加斯

[7] Malloy, G. 和 Brandeau, M. 何时大规模预防对流行病控制具有成本效益?决策方法的比较 (2022), 医学决策制定

对我的内容感兴趣吗?请考虑在 Medium 上关注我

所有代码和数据可以在 GitHub 上找到: gspmalloy/oakland_as_decision_trees: 我博客“决策分析和 Python 中的决策树——奥克兰 A 队的案例”的代码 (github.com)

在 Twitter 上关注我:@malloy_giovanni

你认为 A 队应该留在奥克兰吗?搬到拉斯维加斯?还是尝试其他城市?你使用机器学习进行元建模的体验如何?我很想听听你的想法!通过评论保持讨论的进行。

决策科学与设计的结合

原文:towardsdatascience.com/decision-science-meets-design-fb30eaa0ded9

深入探讨通过深度强化学习解决生成设计问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Houssame E. Hsain

·发表于Towards Data Science ·阅读时间约 9 分钟·2023 年 10 月 27 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Igor Omilaev提供,来源于Unsplash

过去几十年间,设计过程发生了巨大的变化。曾经由人类直觉、判断和审美偏好驱动的领域,现在被计算方法和数据驱动过程所增强。这一过渡通过数据科学与设计的交集得到了体现,这是一个精确与创造力相遇的交汇点。

数据驱动技术在设计中的实用性在其子领域生成设计中得到了很好的展示,这是一种使用计算算法根据预定义标准生成多个设计变体的方法。然而,随着这些设计问题变得越来越复杂和多维,需要更复杂的技术来寻找令人满意的解决方案。这时,决策科学,特别是强化学习,就发挥了作用。

将决策科学应用于生成设计

设计的核心不仅仅是创造,而是一系列有目的的决策,这些决策导致了创造的形成。

决策科学的基本原则是通过评估在特定背景下可用选项的预测或已知后果来做出明智的选择。它包含定量统计方法与优化过程的结合。当应用于生成设计时,决策科学可以帮助确定哪些设计决策或决策序列可以改善某个配置或设计实例。这个过程需要三个组成部分:

  • 评估设计: 评估每种变体的性能或质量,以了解每个设计选择对预期结果的贡献

  • **优化:**综合设计选择序列,以产生可行且令人满意的设计变体

  • **情景分析:**通过在不同的背景和约束下做出设计决策来探索各种设计可能性

将生成设计问题框架化为马尔可夫决策过程(MDPs)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

简单的马尔可夫决策过程(插图由作者制作)

在深入探讨生成设计中的深度强化学习(DRL)之前,将这些设计问题框架化为马尔可夫决策过程(MDPs)是至关重要的。但什么是 MDP?

MDPs 是一种数学框架,用于建模在结果部分由概率动态和部分由决策者行为决定的设置中的决策过程。它包括以下主要组成部分:

  • **状态(S):**表示不同的情景或条件。

  • **行动(A):**表示每个状态下可用的选择。

  • **过渡(P):**表示在采取行动后,从一个状态转移到另一个状态的概率。

  • **奖励(R):**表示在某状态下采取行动后的反馈或结果。

在生成设计的背景下,我们可以将状态视为设计配置,将行动视为设计修改,将过渡视为从一个初始设计配置转移到另一个的可能性。奖励则是向设计师传达设计实例性能度量的反馈,并指导整个设计过程。

通过深度强化学习(DRL)解决生成设计问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

强化学习训练闭环(插图由作者制作)

强化学习(RL)的目标是通过试错过程学习执行任务的最佳行动策略。在我们的上下文中,代理即适应性设计策略,通过采取行动(修改设计)并根据结果(效率或性能)获得奖励或惩罚,从环境中学习。

在处理设计问题中的大状态和行动空间时,挑战就会出现。这时,深度学习,特别是深度强化学习(DRL),变得非常宝贵。DRL 将 RL 的决策能力与深度学习的强大函数逼近能力结合起来。简单来说,它利用神经网络预测在大型和复杂设计场景中应采取的最佳行动。

深度强化学习(DRL)实践:优化建筑物在地形上的布局

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将建筑物质量放置在地形上,并计算所需的开挖和填充体积(动画由作者制作)

考虑在不平坦地形上建造建筑物的挑战。设计师可能需要考虑将建筑物放置在减少土方(挖掘和填充)量的地方。地形中的每一个可能位置代表一个动作,产生的挖填量代表奖励(或在这种情况下的惩罚)。

我们将通过一个工作流程展示如何训练 DRL 代理在地形上放置建筑物,同时最小化所需的挖填量。

定义观察和动作空间

我们首先定义了 DRL 代理的动作空间。代理控制建筑质量的三个参数:其 x 和 y 坐标以及旋转角度(theta)。我们使用 3 维离散动作空间表示。至于观察空间,使用包含建筑位置的地形图像帧来表示我们环境的状态。

import numpy as np
import torch

# 3-dim action space
param1_space = np.linspace(start=0.1, stop=0.9, num=17)
param2_space = np.linspace(start=0.1, stop=0.9, num=17)
param3_space = np.linspace(start=0, stop=160, num=17)

# Define action space
param1_space = torch.from_numpy(param1_space)
param2_space = torch.from_numpy(param2_space)
param3_space = torch.from_numpy(param3_space)

奖励函数

代理的主要目标是最小化建造建筑物所需的土方。为此,奖励函数根据挖填量对代理进行惩罚。

代理在每一步都会收到一个等同于放置建筑所需的挖填量的惩罚值。在多建筑设置中,如果建筑质量在训练过程中与任何之前定位的建筑质量相交,还会有额外的-5 惩罚。奖励信号在Rhinoceros 3D Grasshopper环境中根据以下代码计算:

# Grasshopper reward computation code
try:
    from ladybug_rhino.grasshopper import all_required_inputs
except ImportError as e:
    raise ImportError('\nFailed to import ladybug_rhino:\n\t{}'.format(e))

if all_required_inputs(ghenv.Component):
    reward = 0
    reward -= Soil_volume / 1000
    done = False

    bInter_relationList = [list(i) for i in bInter_relation.Branches]

    if len(bInter_relationList[0]) > 1:
        for i in bInter_relationList[0]:
            # building mass is inside some previously placed one
            if i == 0:
                reward -= 5
            # building mass intersects with some previously placed one
            elif i == 1:
                reward -= 5
        # compensate for self-intersection
        reward += 5

代理与环境之间的连接

在建立了观察和动作空间以及奖励函数之后,有必要促进 DRL 代理与 Grasshopper 模拟环境之间的互动。这是通过使用套接字进行协调的,这是一种流行的进程间通信方法。

# Define Socket connection between Grasshopper and RL agent in Python
import socket

HOST = '127.0.0.1'
timeout = 20

def done_from_gh_client(socket):
    socket.listen()
    conn, _ = socket.accept()
    with conn:
        return_byt = conn.recv(5000)
    return_str = return_byt.decode() 

    return eval(return_str)

def reward_from_gh_client(socket):
    socket.listen()
    conn, _ = socket.accept()
    with conn:
        return_byt = conn.recv(5000)
    return_str = return_byt.decode()
    if return_str == 'None':
        return_float = 0
    else:
        return_float = float(return_str) 

    return return_float

def fp_from_gh_client(socket):
    socket.listen()
    conn, _ = socket.accept()
    with conn:
        return_byt = conn.recv(5000)
    fp = return_byt.decode()

    return fp

def send_ep_count_to_gh_client(socket, message):
    message_str = str(message)
    message_byt = message_str.encode()

    socket.listen()
    conn, _ = socket.accept()
    with conn:
        conn.send(message_byt)

def send_to_gh_client(socket, message):
    message_str = ''
    for item in message:
        listToStr = ' '.join(map(str, item))
        message_str = message_str + listToStr + '\n'

    message_byt = message_str.encode()
    socket.listen()
    conn, _ = socket.accept()
    with conn:
        conn.send(message_byt)

DRL 演员评论家模型定义

在定义了各种通信实用函数后,我们定义并初始化 DRL 模型和 ADAM 优化器用于训练:

import torch 
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.autograd import Variable
from torch.distributions import Categorical

# Actor Critic Model Architecture
def enc_block(in_c, out_c, BN=True):
    if BN:
        conv = nn.Sequential(
            nn.Conv2d(in_c, out_c, kernel_size=4, stride=2, 
                      padding=1, bias=True),
            nn.BatchNorm2d(out_c),
            nn.LeakyReLU(negative_slope=0.2, inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        return conv
    else:
        conv = nn.Sequential(
            nn.Conv2d(in_c, out_c, kernel_size=4, stride=2, 
                      padding=1, bias=True),
            nn.LeakyReLU(negative_slope=0.2, inplace=True)
        )
        return conv

class GRUpolicy(nn.Module):
    def __init__(self, n_gru_layers, hidden_size, lin_size1, lin_size2, 
                  enc_size1, enc_size2, enc_size3):
        super(GRUpolicy, self).__init__()

        #critic
        self.critic_enc1 = enc_block(3, enc_size1, BN=False)
        self.critic_enc2 = enc_block(enc_size1, enc_size2, BN=True)
        self.critic_enc3 = enc_block(enc_size2, enc_size3, BN=True)
        self.critic_enc4 = enc_block(enc_size3, 128, BN=True)

        self.critic_linear1 = nn.Linear(512, lin_size1)
        self.critic_linear2 = nn.Linear(lin_size1, lin_size2)
        self.critic_linear3 = nn.Linear(lin_size2, 1)

        # actor
        self.gru1 = nn.GRU(4, hidden_size, n_gru_layers, batch_first=True)
        self.gru2 = nn.GRU(4, hidden_size, n_gru_layers, batch_first=True)
        self.gru3 = nn.GRU(4, hidden_size, n_gru_layers, batch_first=True)
        self.actor_linear = nn.Linear(hidden_size, 17)

    def forward(self, state):
        state = Variable(state.unsqueeze(0))

        # critic
        enc = self.critic_enc1(state)
        enc = self.critic_enc2(enc)
        enc = self.critic_enc3(enc)
        enc = self.critic_enc4(enc)

        value = F.relu(self.critic_linear1(torch.flatten(enc)))
        value = F.relu(self.critic_linear2(value))
        value = self.critic_linear3(value)

        # actor
        seq = torch.reshape(enc, (1, 128, 4))

        out1, h_1 = self.gru1(seq)
        out_s1 = torch.squeeze(out1[:, -1, :])
        out_l1 = self.actor_linear(out_s1)
        prob1 = F.softmax(out_l1, dim=-1)
        dist1 = Categorical(prob1)

        out2, h_2 = self.gru2(seq, h_1)  
        out_s2 = torch.squeeze(out2[:, -1, :])
        out_l2 = self.actor_linear(out_s2)
        prob2 = F.softmax(out_l2, dim=-1)
        dist2 = Categorical(prob2)

        out3, _ = self.gru3(seq, h_2)
        out_s3 = torch.squeeze(out3[:, -1, :])
        out_l3 = self.actor_linear(out_s3)
        prob3 = F.softmax(out_l3, dim=-1)
        dist3 = Categorical(prob3)

        return value, dist1, dist2, dist3 

# Set device
is_cuda = torch.cuda.is_available()
device = torch.device('cuda' if is_cuda else 'cpu')
print(f'Used Device: {device}')

# Initialize DRL model
actorcritic = GRUpolicy(config.n_gru_layers, config.hidden_size, 
                        config.lin_size1, config.lin_size2, 
                        config.enc_size1, config.enc_size2, 
                        config.enc_size3).to(device)

# Initialize optimizer 
ac_optimizer = optim.Adam(actorcritic.parameters(), lr=config.lr, weight_decay = 1e-6)

代理架构在 GRUpolicy 类中定义。它是一种演员-评论家架构。演员在给定状态下提供动作的概率分布,而评论家估计该状态的价值,即从该状态开始并遵循代理策略的期望回报。

代理训练

一旦定义了代理与环境之间的套接字连接,模型架构得以实现,并且模型的一个实例被初始化,我们就准备好训练 DRL 代理以正确地在地形上放置建筑物质量。

实验的核心是训练循环。在这里,代理在多个训练回合中反复与环境互动,遵循以下步骤:

  • 一旦代理收到当前状态观察,它会根据其当前策略选择一个动作。
# model forward pass
value, dist1, dist2, dist3 = actorcritic.forward(state) 

# get action from probability distributions
param1 = param1_space[dist1.sample()]
param2 = param2_space[dist2.sample()]
param3 = param3_space[dist3.sample()] 

action = [param1, param2, param3]
  • 智能体随后将动作发送到 Grasshopper,并获得结果奖励和新状态。
# Send action through socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, 8080))
    s.settimeout(timeout)
    send_to_gh_client(s, action)

# Send episode count through socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, 8083))
    s.settimeout(timeout)
    send_ep_count_to_gh_client(s, episode)

####### Awaiting Grasshopper script response #######

# Receive observation from Grasshopper Client
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, 8084))
    s.settimeout(timeout)
    fp = fp_from_gh_client(s)

# Receive Reward from Grasshopper Client
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, 8081))
    s.settimeout(timeout)
    reward = reward_from_gh_client(s)

# Receive done from Grasshopper Client
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, 8082))
    s.settimeout(timeout)
    done = done_from_gh_client(s)
  • 然后,它会根据接收到的阶段性奖励更新其策略。
# compute loss functions
returns = []
for t in reversed(range(len(rewards))):
    Qval = rewards[t] + config.gamma * Qval
    returns.insert(0, Qval)

returns = torch.cat(returns).detach()
values = torch.cat(values)
log_probs = torch.cat(log_probs)

advantage = returns - values

actor_loss = -(log_probs * advantage.detach()).mean() 
critic_loss = 0.5 * advantage.pow(2).mean() 
ac_loss = actor_loss + critic_loss - config.beta * entropy

# update actor critic
ac_optimizer.zero_grad()
ac_loss.backward()
ac_optimizer.step()

这个过程会持续进行几个训练迭代,直到智能体收敛到一个最优的建筑质量布置,以最小化地形切割和填充体积。这个由奖励反馈指导的迭代试错学习过程,结果产生了优化的设计配置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

DRL 智能体收敛到一个最小化必要切割和填充体积的建筑位置。地形中的渐变着色代表了其坡度(动画作者提供)

通过这个实验,你可以看到 DRL 智能体如何被调整以应对现实世界中的设计挑战。这种由 DRL 驱动的生成设计方法,展示了数据科学与设计交汇处未来探索的有希望的途径。

所有关于这个实验的实现和细节,包括在 Grasshopper 中的环境实现以及相关的 Rhinoceros 3D 文件,都可以在 GitHub 上的 CutnFill_DeepRL 库中找到。

结论

通过将生成设计问题框架设为 MDP,并利用深度强化学习的力量,我们可以更广泛地探索设计空间,更客观地评估设计,并更有效地优化它们。像 DRL 这样的计算技术在设计实践中变得越来越普遍。数据科学与设计的融合预示着一个未来,在这个未来中,美观且经过严谨信息化的设计方案将被自动生成和评估,从而实现一个快速有效的迭代设计过程,其中人类和机器协作,产生智能设计解决方案。

决策树回归器——Scikit Learn 的可视化指南

原文:towardsdatascience.com/decision-tree-regressor-a-visual-guide-with-scikit-learn-2aa9e01f5d7f

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 niko photos 提供,发布在 Unsplash

无需数学知识了解决策树

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Angela and Kezhan Shi

·发布在 Towards Data Science ·5 分钟阅读·2023 年 3 月 27 日

在这篇文章中,我们将使用 Python 中的 scikit-learn 实现 DecisionTreeRegressor,以可视化该模型的工作原理。我们不会使用任何数学术语,而是通过可视化来展示决策树回归器的工作原理及一些超参数的影响。

在这个背景下,决策树回归器通过将特征变量切分成小区域来预测一个连续的目标变量,每个区域将有一个预测值。我们将从一个连续变量开始,然后是两个连续变量。我们不会使用分类变量,因为对于决策树,当进行切分时,连续变量最终会像分类数据一样处理。

我们将主要研究两个直观易懂的超参数的影响:max depth 和 min_samples_leaf。其他超参数类似,主要思想是限制规则的大小。

一个非线性变量

我们使用一些简单的数据,只有特征变量 x。

# Import the required libraries
from sklearn.tree import DecisionTreeRegressor
import numpy as np
# Define the dataset
X = np.array([[1], [3], [4], [7], [9], [10], [11], [13], [14], [16]])
y = np.array([3, 4, 3, 15, 17, 15, 18, 7, 3, 4])

我们可以在一个 (x,y) 图中可视化数据

import matplotlib.pyplot as plt

# Plot the dataset with the decision tree splits
plt.figure(figsize=(10,6))
plt.scatter(X, y, color='blue')
plt.show()

在下图中,对于第一次切分,我们可以直观地猜测出两种可能的分割,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

决策树回归器可视化——作者提供的图像

现在,决策树回归器模型准确地决定了哪个分割更好。我们指定参数 max_depth=1,以仅获得一个分割:

from sklearn.tree import DecisionTreeRegressor

# Fit the decision tree model
model = DecisionTreeRegressor(max_depth=1)
model.fit(X, y)

# Generate predictions for a sequence of x values
x_seq = np.arange(0, 17, 0.1).reshape(-1, 1)
y_pred = model.predict(x_seq)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

决策树回归器可视化——作者提供的图像

如果我们决定得到 4 个区域,我们可以尝试 max_depth=2,并得到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

决策树回归器可视化 — 作者提供的图片

然后我们可以通过下面的图片可视化 max_depth 超参数对最终模型的影响:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

决策树回归器可视化 — 作者提供的图片

我们也可以用另一个超参数 min_samples_leaf 绘制相同的图形,它是最终区域(我们称之为叶子,因为在树的分支末端,我们找到叶子)的最小观察数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

决策树回归器可视化 — 作者提供的图片

一个“线性”特征

决策树回归器是一个非线性回归器。我们可以从之前的示例中看到它如何表现/建模数据。对于“线性”数据会发生什么呢?

让我们以这个完美线性数据的简单例子开始:

import numpy as np

X=np.arange(1,13,1).reshape(-1,1)
y=np.concatenate((np.arange(1,12,1),12), axis=None)

plt.scatter(X,y)

你可以看到关系非常简单:y = x!

如果我们使用经典的决策树可视化,你可以立即看到模型的表现。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,我们可以创建下面相同的图形。有时,人们会惊讶地看到决策树将完美的线分割成区域,并为预测提供几个值,即使对于线性数据集也是如此。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后人们会评论说这个模型根本不适合这个数据集,我们不应该使用它。

现在,事实是你无法提前知道数据集的行为。这就是“无免费午餐定理”的全部内容。

在实际操作中,你可以应用几种模型,如线性回归和决策树。如果有一个模型显著优于另一个模型,那么你可以对数据的线性与非线性行为做出结论。

两个连续特征

对于两个连续变量,我们需要创建一个 3D 图。

首先,让我们生成一些数据。

import numpy as np
from sklearn.tree import DecisionTreeRegressor
import plotly.graph_objs as go
from plotly.subplots import make_subplots

# Define the data
X = np.array([[1, 2], [3, 4], [4, 5], [7, 2], [9, 5], [10, 4], [11, 3], [13, 5], [14, 3], [16, 1],
              [10, 10], [16, 10], [12, 10]])
y = np.array([3, 4, 3, 15, 17, 15, 18, 7, 3, 4,8,10,13])

然后可以创建模型:

# Fit the decision tree model
model = DecisionTreeRegressor(max_depth=3)
model.fit(X, y)

最后,我们可以使用 Plotly 创建 3D 图。

# Create an interactive 3D plot with Plotly
fig = make_subplots(rows=1, cols=1, specs=[[{'type': 'surface'}]])

fig.add_trace(go.Surface(x=x_seq, y=y_seq, z=z_seq, colorscale='Viridis', showscale=True,opacity = 0.5),
              row=1, col=1)

fig.add_trace(go.Scatter3d(x=X[:, 0], y=X[:, 1], z=y, mode='markers', marker=dict(size=5, color='red')),
              row=1, col=1)

fig.update_layout(title='Decision Tree with Max Depth = {}'.format(max_depth),
                  scene=dict(xaxis_title='x1', yaxis_title='x2', zaxis_title='Predicted Y'))

fig.show()

我们可以比较不同深度的值,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

决策树回归器可视化 — 作者提供的图片

如果你用 Python 创建一个图形,你可以操控它以从不同角度查看可视化效果。

结论

可视化模型对简单数据集的预测是理解模型如何工作的一个极好的方法。

对于决策树来说,它们通过树状规则的可视化已经相当直观。经典的 x, y(和 z)可视化可以作为补充。

我们也可以看到模型是高度非线性的。而且数据集不需要任何缩放。

我写关于机器学习和数据科学的文章,并以清晰的方式解释复杂的概念。请通过下面的链接关注我并获取我的文章完整访问权限:medium.com/@angela.shi/membership

Excel 中的决策树回归

原文:towardsdatascience.com/decision-tree-regressor-in-excel-2d29d16df1db

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由Kevin Young提供,来源于Unsplash

面向机器学习初学者的逐步指南

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Angela 和 Kezhan Shi

·发布于数据科学探索 ·6 分钟阅读·2023 年 3 月 23 日

我正在写一系列关于使用 Excel 实现机器学习算法的文章,这是一个了解这些算法工作原理而无需编程的绝佳工具。

在本文中,我们将一步步实现决策树回归算法。

我将使用 Google 表格演示实现过程。如果您希望访问这个表格以及我开发的其他表格——例如梯度下降的线性回归、逻辑回归、带反向传播的神经网络、KNN、K 均值等——请考虑在 Ko-fi 上支持我。您可以在以下链接找到所有这些资源:ko-fi.com/s/4ddca6dff1

一个简单的数据集上的简单决策树

让我们使用一个只有一个连续特征的简单数据集。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Excel 中简单数据集的决策树回归——作者提供的图像

我们可以直观地猜测,对于第一次分裂,有两个可能的值,一个在 5.5 左右,另一个在 12 左右。现在的问题是,我们选择哪个?

为了确定这一点,我们可以查看使用 DecisionTreeRegressor 估计器的 scikit learn 的结果。下图显示了第一次分裂是 5.5,因为它导致了最低的平方误差。这到底意味着什么?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

简单的决策树回归——作者提供的图像

这正是我们要找出的:如何通过在 Excel 中实现第一次分裂来确定其值?一旦确定第一次分裂的值,我们可以对随后的分裂应用相同的过程。这就是为什么我们将只在 Excel 中实现第一次分裂。

决策树回归器的算法原理

决策树算法的三步骤

我写了一篇文章来始终区分机器学习的三个步骤,以有效地学习它,让我们将这个原则应用于决策树回归器:

  • 1. 模型: 这里的模型是一组规则,值得注意的是,它与基于数学函数的模型不同,例如线性回归中,我们可以将模型写成 y=aX+b,其中参数 a 和 b 需要确定。而决策树模型则是非参数的。

  • 2. 模型拟合: 对于决策树,我们也称这一过程为完全生长一棵树。在决策树回归器的情况下,叶节点将仅包含一个观察值,因此 MSE 为零。

  • 3. 模型调整: 对于决策树,我们也称之为剪枝,包括优化超参数,如叶节点中的最小观察数和最大深度。

训练过程

生长一棵树包括递归地将输入数据划分为越来越小的块或区域。对于每个区域,可以计算预测值。在回归的情况下,预测值是该区域的目标变量的平均值。

在构建过程的每一步,算法选择特征和分裂值,以最大化一个标准,而对于回归器,这通常是实际值与预测值之间的均方误差(MSE)。

调整或剪枝

剪枝过程可以看作是从完全生长的树中删除节点和叶子,或者也可以等同于说当满足某个标准时(如最大深度或每个叶节点中的最小样本数)构建过程停止。这些就是可以通过调整过程优化的超参数。

下面是一些具有不同最大深度值的树的示例。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

不同最大深度的决策树回归— 作者提供的图像

推理过程

一旦决策树回归器建立完成,它可以通过应用规则和从根节点遍历到与输入特征值对应的叶节点来预测新输入实例的目标变量。

对于输入实例的预测目标值是落在同一叶节点中的训练样本的目标值的均值。

在 Excel 中实现第一次分裂

以下是我们将遵循的步骤:

  • 列出所有可能的分裂

  • 对于每一个分割,我们将计算 MSE(均方误差)

  • 我们将选择最小化 MSE 的分割作为最优下一个分割

所有可能的分割

首先,我们需要列出所有可能的分割,这些分割是两个连续值的平均值。不需要测试更多的值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Excel 中的决策树回归可能的分割 — 作者图片

每个可能分割的 MSE 计算

作为起点,我们可以在任何分割之前计算均方误差(MSE)。这也意味着预测只是 y 的平均值。而 MSE 相当于 y 的标准差。

现在,目标是找到一个分割,使得分割后的 MSE 低于之前的值。可能分割不会显著提高性能(或降低 MSE),那么最终的树将会很简单,即 y 的平均值。

对于每一个可能的分割,我们可以计算 MSE(均方误差)。下图显示了第一个可能分割的计算结果,即 x = 2。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Excel 中的决策树回归所有可能分割的 MSE — 作者图片

我们可以查看计算的详细信息:

  1. 将数据集切割成两个区域:以 x=2 为例,我们确定了两个可能性 x<2 或 x>2,因此 x 轴被分成两部分。

  2. 计算预测值:对于每一部分,我们计算 y 的平均值。这是 y 的潜在预测值。

  3. 计算误差:然后我们将预测值与实际的 y 值进行比较

  4. 计算平方误差:对于每个观察值,我们可以计算平方误差。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Excel 中的决策树回归所有可能分割 — 作者图片

最优分割

对于每一个可能的分割,我们做相同的操作以获得 MSE。在 Excel 中,我们可以复制并粘贴公式,唯一变化的是 x 的可能分割值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Excel 中的决策树回归分割 — 作者图片

然后我们可以将 MSE 绘制在 y 轴上,将可能的分割绘制在 x 轴上,现在我们可以看到 x=5.5 时 MSE 最小,这正是通过 Python 代码得到的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Excel 中的决策树回归 MSE 的最小化 — 作者图片

你可以做的练习

现在,你可以使用 Google Sheet 进行操作:

  • 你可以修改数据集

  • 你可以引入一个分类特征

  • 你可以尝试寻找下一个分割

  • 你可以改变标准,不仅使用 MSE,还可以使用绝对误差、泊松误差或 friedman_mse,正如 DecisionTreeRegressor 文档中所示

  • 你可以将目标变量更改为二进制变量,通常这会变成一个分类任务,但 0 或 1 也是数字,因此标准 MSE 仍然适用。但如果你想创建一个合适的分类器,你必须应用通常的标准EntroyGini。我会很快发布另一篇关于决策树分类器的文章,敬请关注。

结论

使用 Excel,可以进行一次分割,以深入了解决策树回归器如何工作。尽管我们没有创建完整的树,但这仍然很有趣,因为最重要的部分是找到所有可能分割中的最佳分割。

我写关于机器学习和数据科学的文章,并以清晰的方式解释复杂概念。请通过下面的链接关注我,并获得对我的文章的全部访问权限:medium.com/@angela.shi/membership

分类决策树——完整示例

原文:towardsdatascience.com/decision-trees-for-classification-complete-example-d0bc17fcf1c2?source=collection_archive---------1-----------------------#2023-01-01

关于如何构建分类决策树的详细示例

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Datamapu

·

关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 1 月 1 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Fabrice Villard 提供,来自 Unsplash

本文解释了我们如何使用决策树来解决分类问题。在解释重要术语后,我们将为一个简单的示例数据集构建一个决策树。

介绍

决策树是一种决策支持工具,它使用树状模型展示决策及其可能的后果,包括随机事件结果、资源成本和效用。这是一种仅包含条件控制语句的算法展示方式。

传统上,决策树是手动绘制的,但可以通过机器学习进行学习。它们可用于回归和分类问题。在这篇文章中,我们将重点关注分类问题。让我们考虑以下示例数据:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例数据(由作者构建)

使用这个简化的例子,我们将预测一个人是否会成为宇航员,取决于他们的年龄、是否喜欢狗以及是否喜欢重力。在讨论如何构建决策树之前,让我们看一下我们示例数据的最终决策树。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例数据的最终决策树

我们可以跟踪路径来做出决定。例如,我们可以看到,不喜欢重力的人不会成为宇航员,与其他特征无关。另一方面,我们也可以看到,喜欢重力和喜欢狗的人将会成为宇航员,与年龄无关。

在详细讨论如何构建这棵树之前,让我们定义一些重要的术语。

术语

根节点

顶级节点。做出的第一个决策。在我们的例子中,根节点是“喜欢重力”。

分支

分支代表子树。我们的例子有两个分支。例如,一个分支是从“喜欢狗”开始的子树,另一个是从“年龄 < 40.5”开始的子树。

节点

一个节点代表进一步(子)节点的切分。在我们的例子中,节点有“喜欢重力”、“喜欢狗”和“年龄 < 40.5”。

叶子节点

叶子节点位于分支的末端,即不再进行切分。它们代表每个动作的可能结果。在我们的例子中,叶子节点由“是”和“否”表示。

父节点

一个在(子)节点之前的节点称为父节点。在我们的例子中,“喜欢重力”是“喜欢狗”的父节点,而“喜欢狗”是“年龄 < 40.5”的父节点。

子节点

一个节点在另一个节点下方称为子节点。在我们的例子中,“喜欢狗”是“喜欢重力”的子节点,而“年龄 < 40.5”是“喜欢狗”的子节点。

切分

将一个节点划分为两个(子)节点的过程。

剪枝

移除父节点的(子)节点称为剪枝。树通过切分生长,通过剪枝缩小。在我们的例子中,如果我们移除节点“年龄 < 40.5”,我们将对树进行剪枝。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

决策树插图

我们还可以观察到,决策树允许我们混合数据类型。我们可以在同一棵树中使用数值数据(“年龄”)和分类数据(“喜欢狗”、“喜欢重力”)。

创建决策树

创建决策树中最重要的步骤是对数据的 分裂。我们需要找到一种方法将数据集 (D) 分裂为两个数据集 (D_1) 和 (D_2)。可以使用不同的标准来寻找下一个分裂,概览见例如 这里。我们将集中于其中一个标准:基尼不纯度,它是一个用于分类目标变量的标准,也是 Python 库 scikit-learn 使用的标准。

基尼不纯度

数据集 D 的基尼不纯度计算如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其中 n = n_1 + n_2 表示数据集 (D) 的大小,并且

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

D_1D_2D 的子集,𝑝_𝑗 是在给定节点上样本属于类别 𝑗 的概率,𝑐 是类别的数量。基尼不纯度越低,节点的同质性越高。纯节点的基尼不纯度为零。为了使用基尼不纯度来分裂决策树,需要执行以下步骤。

  1. 对于每个可能的分裂,计算每个子节点的基尼不纯度

  2. 计算每个分裂的基尼不纯度,作为子节点基尼不纯度的加权平均值

  3. 选择基尼不纯度值最低的分裂

重复步骤 1–3 直到不能再分裂为止。

为了更好地理解这一点,让我们来看一个例子。

第一个示例:具有两个二元特征的决策树

在为整个数据集创建决策树之前,我们将首先考虑一个子集,该子集仅考虑两个特征:“喜欢重力”和“喜欢狗”。

我们首先需要决定哪个特征将作为 根节点。我们通过仅用一个特征来预测目标,然后选择基尼不纯度最低的特征作为根节点。也就是说,在我们的案例中,我们构建了两个浅层树,只有根节点和两个叶子。在第一个案例中,我们使用“喜欢重力”作为根节点,在第二个案例中使用“喜欢狗”。然后我们计算两个树的基尼不纯度。这些树的样子如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供

这些树的基尼不纯度计算如下:

案例 1:

数据集 1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数据集 2:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基尼不纯度是两者的加权均值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

案例 2:

数据集 1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数据集 2:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基尼不纯度是两者的加权均值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

即,第一个案例的基尼不纯度较低,是选择的拆分。在这个简单的示例中,只剩下一个特征,我们可以构建最终的决策树。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

只考虑特征‘喜欢重力’和‘喜欢狗’的最终决策树

第二个示例:添加一个数值变量

到现在为止,我们只考虑了数据集的一个子集——分类变量。现在我们将添加数值变量‘年龄’。拆分的标准相同。我们已经知道‘喜欢重力’和‘喜欢狗’的基尼不纯度。数值变量的基尼不纯度计算类似,但决策需要更多计算。需要执行以下步骤

  1. 按数值变量(‘年龄’)对数据框进行排序

  2. 计算邻近值的均值

  3. 计算这些均值的所有拆分的基尼不纯度

这又是我们的数据,按年龄排序,左侧给出了邻近值的均值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

按年龄排序的数据集。左侧显示了年龄的邻近值的均值。

我们得到以下可能的拆分。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

年龄的可能拆分及其基尼不纯度。

我们可以看到,所有可能的‘年龄’拆分的基尼不纯度都高于‘喜欢重力’和‘喜欢狗’的基尼不纯度。当使用‘喜欢重力’时,基尼不纯度最低,即这是我们的根节点和第一次拆分。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

树的第一次拆分。‘喜欢重力’是根节点。

子集数据集 2 已经是纯净的,即这个节点是一个叶子节点,无需进一步拆分。左侧的分支,数据集 1 不是纯净的,可以进一步拆分。我们像之前一样计算每个特征的基尼不纯度:‘喜欢狗’和‘年龄’。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数据集 2 的可能拆分。

我们看到最低的基尼不纯度是由“喜欢狗”的拆分给出的。我们现在可以构建我们的最终决策树。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最终决策树。

使用 Python

在 Python 中,我们可以使用 scikit-learn 方法DecisionTreeClassifier来构建分类决策树。请注意,scikit-learn 还提供了DecisionTreeRegressor,这是一个用于回归的决策树方法。假设我们的数据存储在数据框‘df’中,我们可以使用‘fit’方法进行训练:

from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier()
X = df['age', 'likes dogs', 'likes graviy']
y = df['going_to_be_an_astronaut']
clf.fit(X,y)

我们可以使用‘plot_tree’方法可视化生成的树。这与我们构建的树相同,只是分割标准用‘<=’代替了‘<’,而‘true’和‘false’路径的方向相反。也就是说,外观上存在一些差异。

plot_tree(clf, feature_names=[‘age’,‘likes_dogs’,‘likes_gravity’], fontsize=8);

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用 scikit-learn 生成的决策树。

决策树的优缺点

在使用决策树时,了解其优缺点很重要。以下是一些优缺点的列表,但这个列表并不完全。

优势

  • 决策树直观、易于理解和解释。

  • 决策树不受异常值和缺失值的影响。

  • 数据不需要进行缩放。

  • 数值数据和分类数据可以结合使用。

  • 决策树是非参数算法。

缺点

  • 过拟合是一个常见问题。剪枝可能有助于克服这个问题。

  • 虽然决策树可以用于回归问题,但它们不能真正预测连续变量,因为预测必须以类别形式分隔。

  • 训练决策树相对昂贵。

结论

在这篇文章中,我们讨论了一个简单但详细的示例,说明了如何为分类问题构建决策树,以及如何利用它进行预测。创建决策树的关键步骤是找到将数据分成两个子集的最佳分割方式。常用的方法是基尼不纯度。这也被 Python 中的 scikit-learn 库所使用,该库在实际中常用于构建决策树。重要的是要记住决策树的局限性,其中最突出的就是过拟合的倾向。

参考文献

  • 克里斯·尼科尔森,《决策树》(2020),pathmind — A.I. Wiki,《AI、机器学习和深度学习中的重要主题初学者指南》https://wiki.pathmind.com/decision-tree。

  • 阿比谢克·夏尔马,4 种简单的方式来拆分决策树 (2020),analyticsvidhya

除非另有说明,所有图片均为作者所用。

在这里找到更多数据科学和机器学习的文章:

[## 更多

数据科学和机器学习博客

datamapu.com](https://datamapu.com/?source=post_page-----d0bc17fcf1c2--------------------------------) [## 订阅 Pumaline 发布的内容时会收到电子邮件。

订阅 Pumaline 发布的内容时会收到电子邮件。通过注册,如果你还没有 Medium 账号,将会创建一个…

medium.com [## Pumaline

嗨,我喜欢学习和分享关于数据科学和机器学习的知识。

www.buymeacoffee.com

决策树:介绍与直观理解

原文:towardsdatascience.com/decision-trees-introduction-intuition-dac9592f4b7f

使用 Python 做数据驱动决策

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Shaw Talebi

·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 2 月 10 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 niko photos 提供,来源于 Unsplash,并配有思考的表情符号。

这是关于决策树系列文章中的第一篇。在这篇文章中,我介绍了决策树,并描述了如何使用数据来生成它们。文章最后包含了示例 Python 代码,展示了如何创建和使用决策树来帮助进行医学预测。

重点:

  • 决策树是一种广泛使用且直观的机器学习技术,用于解决预测问题。

  • 我们可以从数据中生成决策树。

  • 超参数调整可以用来帮助避免过拟合问题。

什么是决策树?

决策树是一种广泛使用且直观的机器学习技术。通常,它们用于解决预测问题。例如,预测明天的天气预报或估算一个人患心脏病的概率。

决策树通过一系列是非问题进行工作,这些问题用于缩小可能的选择范围并得出结果。下面展示了一个简单的决策树示例。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例决策树预测我是否会喝茶或咖啡。图片由作者提供。

如上图所示,决策树由通过有向边连接的节点组成。决策树中的每个节点对应于一个基于预测变量的条件语句。

上面显示的决策树的顶部是根节点,它设置了数据记录的初始分裂。在这里,我们评估时间是否在下午 4 点之后。每个可能的响应(是或否)在树中遵循不同的路径。

如果是,我们沿着左侧分支前进,最终到达一个叶节点(也称为终端节点)。在这种类型的节点上不需要进一步分裂来确定结果。在这种情况下,我们选择茶而不是咖啡,以便能在合理的时间上床睡觉。

相反,如果时间是下午 4 点或更早,我们会沿着右侧分支前进,最终到达一个所谓的分裂节点。这些节点进一步分割数据记录,基于条件语句进行划分。接下来,我们评估昨晚的睡眠时间是否超过 6 小时。如果是,我们继续选择茶,但如果不是,我们则选择咖啡☕️。

使用决策树

在实际操作中,我们通常不会像刚才那样使用决策树(即查看决策树并跟随特定数据记录)。相反,我们让计算机为我们评估数据。我们只需将所需数据以表格形式提供给计算机即可。

下面是一个示例。这里我们有两个变量的表格数据:时间和前一晚的睡眠小时数(蓝色列)。然后,使用上述决策树,我们可以为每条记录分配适当的含咖啡因饮料(绿色列)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入数据的示例表格及其生成的决策树预测。图片由作者提供。

决策树的图形视图

另一种思考决策树的方法是图形化(这是我个人对决策树的直观理解。)

想象一下,我们将示例决策树中的两个预测变量绘制在一个二维图上。然后,我们可以将决策树的分裂表示为将图划分为不同区域的线条。这样,我们可以通过简单地查看数据点所在的象限来确定饮料选择。

从直观上看,这就是决策树的作用。将预测空间划分为不同的部分,并为每个部分分配一个标签(或概率)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

决策树对茶或咖啡的预测的图形视图。图片由作者提供。

如何构建决策树?

决策树是一种直观的数据划分方法。然而,使用数据手动绘制一个合适的决策树可能并不容易。在这种情况下,我们可以使用机器学习策略来学习适用于给定数据集的“最佳”决策树。

数据可以用于在一种称为训练的优化过程中构建决策树。训练需要一个训练数据集,其中的预测变量预先标记了目标值。

一种标准的训练决策树的策略使用被称为贪婪搜索的方法。这是一种在优化中流行的技术,我们通过找到局部最优解来简化更复杂的优化问题,而不是全局最优解。(我在之前的一篇关于 因果发现的文章中给出了贪婪搜索的直观解释。)

在决策树的情况下,贪婪搜索确定每个可能的分裂选项的增益,然后选择提供最大增益的那个选项[1,2]。这里的“增益”由分裂准则决定,这可以基于几种不同的量度,例如基尼 impurity信息增益、**均方误差 (MSE)**等。这个过程会递归地重复,直到决策树完全生成。

例如,如果使用基尼 impurity,数据记录会递归地分成两个组,以使结果组的加权平均 impurity 最小化。这个分裂过程可以继续,直到所有数据分区都是纯净的,即每个分区中的所有数据记录都对应于一个单一的目标值。

尽管这意味着决策树可以是完美的估计器,但这种方法可能会导致过拟合。训练好的决策树在与训练数据集有显著不同的数据上表现不佳

超参数调优

对抗过拟合问题的一种方法是超参数调优。超参数限制决策树增长的值

常见的决策树超参数包括最大分裂次数、最小叶子节点大小和分裂变量的数量。设置决策树超参数的关键结果限制树的大小,这有助于避免过拟合并提高泛化能力。

替代训练策略

虽然我上述描述的训练过程在决策树中广泛使用,但我们可以使用其他替代方法。

剪枝——一种这样的方式叫做剪枝[3]。从某种意义上讲,剪枝是决策树生长的反面。我们不是从根节点开始递归地添加节点,而是从完全生成的树开始,逐步去除节点。

虽然剪枝过程可以通过多种方式进行,但通常会删除那些不会显著增加模型误差的节点。这是一种替代超参数调优来限制树的生长以避免过拟合的方法[3]。

最大似然——我们可以使用最大似然框架训练决策树[4]。虽然这种方法不太为人知,但它基于一个强大的理论框架。它允许我们使用信息准则如 AIC 和 BIC 来客观优化树中的参数数量及其性能,这有助于避免广泛的超参数调优。

示例代码:使用决策树进行脓毒症生存预测

现在,了解了决策树的基本概念以及如何从数据中构建决策树后,让我们使用 Python 深入探讨一个具体的例子。在这里,我们将使用来自UCI 机器学习库的数据集来训练一个决策树,以预测患者是否会生存,基于他们的年龄、性别和经历的脓毒症发作次数[5,6]。

对于决策树的训练,我们将使用 sklearn Python 库[7]。此示例的代码可以在GitHub 仓库中自由获取。

我们从导入一些有用的库开始。

# import modules
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from sklearn.model_selection import train_test_split
from sklearn import tree
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
from sklearn.metrics import precision_score, recall_score, f1_score
from imblearn.over_sampling import SMOTE

接下来,我们从.csv 文件中加载数据并进行一些数据准备。

# read data from csv
df = pd.read_csv('raw/s41598-020-73558-3_sepsis_survival_primary_cohort.csv')

# look at data distributions
plt.rcParams.update({'font.size': 16})

# plot histograms
df.hist(figsize=(12,8))

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数据集中每个变量的直方图。图片由作者提供。

请注意,在右下角的直方图中,我们有更多的存活记录而非死亡记录。这被称为不平衡数据集。对于一个简单的决策树分类器,从不平衡的数据中学习可能会导致决策树过度预测多数类

为了处理这种情况,我们可以通过过采样少数类来使数据更平衡。一种方法是使用一种叫做合成少数类过采样技术SMOTE)的技术。虽然我会在未来的文章中详细介绍 SMOTE,但目前只需知道这有助于我们平衡数据并改进决策树模型即可。

# Balance data using SMOTE

# define predictor and target variable names
X_var_names = df.columns[:3]
y_var_name = df.columns[3]

# create predictor and target arrays
X = df[X_var_names]
y = df[y_var_name]

# oversample minority class using smote
X_resampled, y_resampled = SMOTE().fit_resample(X, y)

# plot resulting outcome histogram
y_resampled.hist(figsize=(6,4))
plt.title('hospital_outcome_1alive_0dead \n (balanced)')

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

SMOTE 之后的结果直方图。图片由作者提供。

数据准备的最后一步是将我们的重采样数据拆分为训练和测试数据集。训练数据将用于构建决策树,而测试数据将用于评估其性能。这里我们使用 80–20 的训练-测试拆分。

# create train and test datasets
X_train, X_test, y_train, y_test = \
      train_test_split(X_resampled, y_resampled, test_size=0.2, random_state=0)

现在有了训练数据,我们可以创建我们的决策树。Sklearn 使这一过程非常简单,仅用两行代码,我们就能得到一个决策树。

# Training
clf = tree.DecisionTreeClassifier(random_state=0)
clf = clf.fit(X_train, y_train)

让我们来看一下结果。

# Display decision tree
plt.figure(figsize=(24,16))

tree.plot_tree(clf)
plt.savefig('visuals/fully_grown_decision_tree.png',facecolor='white',bbox_inches="tight")
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

完全生长的决策树。图片由作者提供。

不用说,这是一棵非常大的决策树,这可能会使解释结果变得困难。然而,让我们暂时搁置这一点,评估模型的性能。

为了评估性能,我们使用混淆矩阵它显示了真正例(TP)、真负例(TN)、假正例(FP)和假负例(FN)的数量

我不会在这里讨论混淆矩阵,但目前我们希望的是对角线上的数字要大,而非对角线上的数字要小

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

完全生长的决策树的混淆矩阵。(左)训练数据集。(右)测试数据集。图片由作者提供。

我们可以从混淆矩阵中获取数据,并计算三个不同的性能指标:精确度、召回率和 f1-score**。** 简单来说,精确度 = TP / (TP + FP),召回率 = TP / (TP + FN),f1-score 是精确度和召回率的调和均值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

完全成长的决策树的三个性能指标。 图片由作者提供。

生成这些结果的代码如下。

# Function to plot confusion matrix and print precision, recall, and f1-score
def evaluateModel(clf, X, y):

    # confusion matrix
    y_pred = clf.predict(X)
    cm = confusion_matrix(y, y_pred)
    cm_disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['dead', 'alive'])
    cm_disp.plot()

    # print metrics
    print("Precision = " + str(np.round(precision_score(y, y_pred),3)))
    print("Recall = " + str(np.round(recall_score(y, y_pred),3)))
    print("F1 = " + str(np.round(f1_score(y, y_pred),3)))

超参数调整

虽然决策树在这里使用的数据上表现不错,但仍然存在可解释性的问题。查看之前展示的决策树,临床医生从决策树的逻辑中提取有意义的见解将是具有挑战性的。

这就是超参数调整可以发挥作用的地方。为了使用 sklearn 完成这一点,我们只需在决策树训练步骤中添加输入参数即可。

在这里我们将尝试设置 max_depth = 3。

# train model with max_depth set to 3
clf_tuned = tree.DecisionTreeClassifier(random_state=0, max_depth=3)
clf_tuned = clf_tuned.fit(X_train, y_train)

现在,让我们看看结果决策树。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

调整后的决策树,max_depth=3。 图片由作者提供。

由于我们限制了树的最大深度,我们可以清楚地看到这里发生了哪些分裂。

我们再次使用混淆矩阵和之前相同的三个性能指标来评估模型的表现。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

超参数调整决策树的混淆矩阵。 (左)训练数据集。 (右)测试数据集。 图片由作者提供

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

超参数调整后的决策树的三个性能指标。 图片由作者提供。

虽然完全成长的树可能看起来比超参数调整的树更可取,但这回到了过拟合的讨论。是的,完全成长的树在当前数据上的表现更好,但我不认为这适用于其他数据。

换句话说,虽然简单的决策树在这里的表现较差,但它可能比完全成长的树更具泛化能力。

这个假设可以通过将每个模型应用于 GitHub repo 中的其他两个数据集来进行测试。

[## YouTube-Blog/decision-tree/decision_tree at main · ShawhinT/YouTube-Blog

代码补充了 YouTube 视频和 Medium 上的博客文章。 - YouTube-Blog/decision-tree/decision_tree at main ·…

github.com](https://github.com/ShawhinT/YouTube-Blog/tree/main/decision-tree/decision_tree?source=post_page-----dac9592f4b7f--------------------------------)

决策树集成

虽然超参数调优可以提高决策树的泛化能力,但在性能方面仍有不足。在上面的例子中,经过超参数调优后,决策树仍然将训练数据**35%**的时间标记错误,这在讨论生死问题时(如此处的例子所示)是一个大问题。

解决这个问题的一种流行方法是使用多个决策树而不是单一的决策树来进行预测。这些被称为决策树集成,将成为本系列的下一篇文章的主题。

## 10 决策树比 1 个更好

解析 bagging、boosting、随机森林和 AdaBoost

towardsdatascience.com

资源

联系: 我的网站 | 预约电话

社交媒体: YouTube 🎥 | LinkedIn | Twitter

支持: 请我喝咖啡 ☕️

[## 免费获取我写的每个新故事

免费获取我写的每个新故事 P.S. 我不会将你的邮箱与任何人分享。通过注册,你将创建一个…

shawhin.medium.com](https://shawhin.medium.com/subscribe?source=post_page-----dac9592f4b7f--------------------------------)

[1] 分类与回归树 由 Breiman 等人著

[2] 决策树:Kotsiantis, S. B. 最近的概述

[3] Esposito 等人对剪枝决策树方法的比较分析

[4] Su 等人提出的最大似然回归树

[5] Dua, D. 和 Graff, C. (2019). UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加利福尼亚州尔湾: 加州大学信息与计算机科学学院。(CC BY 4.0)

[6] Chicco 和 Jurman 通过年龄、性别和脓毒症发作次数来预测脓毒症患者的生存率

[7] Scikit-learn: Python 中的机器学习,Pedregosa 等人,JMLR 12,2825–2830 页,2011 年。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值