ScipyCon-2020-笔记-全-
ScipyCon 2020 笔记(全)
课程 P1:dabl - 人机协同的自动化机器学习 🚀
在本课程中,我们将学习一个名为 dabl 的 Python 库。它的全称是“数据分析基线库”,旨在简化机器学习工作流程,帮助数据科学家和初学者快速构建稳健的原型模型。我们将重点了解 dabl 如何通过自动化数据清洗、可视化、模型构建和解释等步骤,让人能够更高效地参与到整个机器学习循环中。
工作流程概览
机器学习通常始于任务定义,这是一个关键步骤,决定了如何将问题转化为机器学习问题。之后,流程包括数据收集、数据清洗、探索性分析与可视化、模型构建、离线模型评估,以及最终的在线评估或业务逻辑内评估。
然而,这并非一个线性流程,而是一个反馈循环。在完成任何一步后,都可能需要返回之前的步骤进行调整。例如,在可视化阶段发现数据质量问题后,可能需要返回修改数据收集方式;或者在模型构建后发现模型无法处理某些数据特性,从而需要重新清洗数据。
许多数据科学家倾向于将大量精力集中在模型构建和超参数调优上。虽然现有工具使这一过程充满乐趣,但本课程将探讨如何让整个流程更加顺畅。真正的提升往往来自于对整个流程的迭代优化,而不仅仅是模型本身。
现有工具的挑战
目前,数据探索、可视化和模型构建等步骤仍不够简便。以使用 seaborn 绘制标准分类数据集的可视化图表为例,即使对于非常标准的绘图任务,也需要编写多行涉及数据类型选择、数据重塑等操作的代码,过程略显复杂。
同样,使用 scikit-learn 构建一个包含缺失值处理、分类变量编码和超参数调优的逻辑回归模型,也需要编写相当多的样板代码。这增加了快速迭代的难度。
另一方面,虽然存在如 auto-sklearn 这样的优秀自动化机器学习框架,但它们的目标通常是在较大的计算预算下寻找最佳模型。例如,在一个小型数据集上运行可能需要一小时。这对于需要快速验证想法、迭代整个工作流的初期阶段来说,时间成本可能过高。
dabl 的解决方案
为了给每个步骤提供易于使用的解决方案,我开发了 dabl 库。它的目标是让用户能够轻松、快速地获得一个良好且稳健的机器学习问题原型,从而可以便捷地在数据清洗、收集、模型评估和解释等环节进行迭代。
dabl 为工作流中的每个步骤提供了工具函数:
- 数据收集:由用户负责,dabl 假设数据已加载到
pandas DataFrame中。 dabl.clean:执行基础的机器学习数据清洗。dabl.plot:提供可视化和探索性分析。dabl.SimpleClassifier/dabl.AnyClassifier:执行快速、高效的自动化机器学习。dabl.explain:尝试解释已构建的模型。
最重要的是,dabl 帮助你思考模型如何融入实际用例,这是数据科学家常常投入不足的环节。
数据清洗与预处理
dabl 提供了两种数据清洗与预处理的使用方式。

第一种是使用 dabl.clean 函数。以一个相对标准但有些“脏”的回归数据集(Ames 房价数据集)为例,它包含82列,有分类变量、连续变量、ID和缺失值等。调用 dabl.clean 会创建一个新的 DataFrame,并报告连续型、分类型列的数量,检测并处理奇怪的字符串或缺失值编码,以及识别近乎恒定或类似索引的无用列。你可以覆盖某些类型检测,并获得一个干净的 DataFrame 以供后续使用。
第二种是使用 dabl.detect_types 和 dabl.Preprocessor 类。Preprocessor 类提供了一个 scikit-learn 转换器,能自动组装一个进行所有预处理的 ColumnTransformer 和 Pipeline。这大大减少了需要编写的样板代码,同时让你能完全控制想要使用的模型类型,从而轻松创建 scikit-learn 管道而无需担心预处理细节。
可视化与探索性分析

dabl 的可视化工具主要通过一个函数 dabl.plot 来提供。
以下是如何使用 dabl.plot 进行可视化的步骤:
- 加载数据到
DataFrame。 - 将
DataFrame传递给plot函数。 - 指定目标列的名称。


dabl 支持两种接口:一种是 scikit-learn 风格的 (X, y),其中 y 是单独的数组或 pandas Series;另一种是单个 DataFrame 加上目标列名。调用 plot 函数后,它会自动为分类变量和连续变量绘图。
对于分类特征,dabl 使用马赛克图进行展示。图中条形的高度表示每个类别中的样本数量,宽度则表示不同类别之间的平衡。例如,在一个二分类数据集中,可以清晰看到每个类别内两个类别的比例。

对于多分类数据集,dabl 提供类别直方图,可以在一个子图矩阵中展示某个特征在所有类别上的单变量分布,使图表更加紧凑。dabl 会自动限制绘图数量,并按其统计显著性进行排序。
dabl 还能绘制成对关系图。当特征数量较多时,它不会展示所有成对组合(那样会过于繁杂),而是尝试找出最有趣的成对图。其原理是评估一个浅层决策树分类器仅基于这两个特征对数据集进行分类的效果,效果越好,通常意味着特征组合的区分度越明显。默认会显示信息量最大的四对特征。
此外,dabl 还会进行主成分分析绘图,显示解释方差,以及使用线性判别分析进行监督降维并可视化。LDA 有时能发现原始数据中不明显的、能很好区分类别的线性投影。
快速原型与模型搜索
在模型构建方面,dabl 提供了两种主要方式。

首先是 SimpleClassifier,它包含一系列几乎能瞬时运行的模型。以下是这些模型的列表:
- 虚拟模型:预测最常见类别,作为基准线。
- 高斯朴素贝叶斯
- 决策树桩
- 线性模型(如逻辑回归)
这些模型按运行时间排序,首先尝试最快的。运行后,你就能立即获得一个合理的基线模型,并可以继续进行模型解释。
如果需要更复杂的模型,可以使用 AnyClassifier。它会在一个预设的模型组合上进行搜索,包括:
- 更多线性模型
- 随机森林
- 梯度提升
- 一些核方法

目前不包含神经网络,以避免引入深度学习库依赖。AnyClassifier 使用连续减半法来高效搜索这个组合。该方法首先在数据子集上评估所有候选模型,淘汰表现最差的一半,然后对剩余模型增加训练数据量,重复此过程,直到使用全部数据。这比随机搜索或网格搜索更高效。
这个模型组合本身是通过在大量基准数据集(如 OpenML CC18)上进行超参数优化后,选取一组既多样又高性能的模型构建而成的。研究表明,即使使用很少的候选模型,这种组合方法也能取得很好的效果。
模型评估与解释
构建模型后(无论是 SimpleClassifier 还是 AnyClassifier),可以调用 dabl.explain 进行模型评估与解释。
dabl.explain 首先提供一系列标准评估指标,包括:
scikit-learn风格的分类报告(精确度、召回率、F1分数)- 混淆矩阵
- ROC 曲线与精确率-召回率曲线
接着,它会提供模型解释:
- 对于线性模型,展示最重要的系数。
- 对于树模型,提供基于不纯度的特征重要性(在训练集上计算)。
- 同时,也会提供基于排列的特征重要性(在测试集上计算),这通常更稳健。
- 最后,还会生成部分依赖图,以展示特征对预测结果的影响。
未来展望与使用方式
dabl 库未来的目标包括创建更省时、更快的模型组合,支持设定时间预算,进行模型压缩,在复杂模型之上构建更易解释的模型,以及改进模型检查功能。
如果你想尝试 dabl,可以通过 pip install dabl 进行安装,并查阅其网站获取文档和教程。如果你在使用中遇到任何问题或有反馈,非常欢迎通过社交媒体、电子邮件或项目问题追踪器与我联系。

课程总结

在本课程中,我们一起学习了 dabl 这个旨在简化机器学习工作流程的 Python 库。我们了解了它如何通过 dabl.clean 简化数据清洗,通过 dabl.plot 进行自动化探索性可视化,通过 SimpleClassifier 和 AnyClassifier 快速构建模型原型并进行高效搜索,以及通过 dabl.explain 对模型进行评估和解释。dabl 的核心思想是降低每个步骤的复杂度,让数据科学家能够更快速地在整个“人机协同”的循环中迭代,从而更早地关注模型如何融入实际业务与科学问题。
课程 P10:Ray - 一个可扩展的Python与机器学习系统 🚀
在本节课中,我们将学习 Ray,这是一个用于扩展 Python 应用和机器学习应用的开源分布式系统。我们将了解 Ray 的核心概念、API 以及它如何简化分布式计算。
Ray 是一个由加州大学伯克利分校的 AMP 实验室和 RISE 实验室孵化的项目,它继承了 Spark、Mesos 等知名系统的传统。Ray 主要包含三个部分:一个用于分布式计算的简单库、一个构建在其上的高级库生态系统,以及一套用于在各种平台上启动集群的工具。
Ray 的定位与生态系统
上一节我们介绍了 Ray 的基本概念,本节中我们来看看 Ray 在整个技术栈中的位置以及它的生态系统。
Ray 在本质上类似于 Dask、PySpark 等工具。它不是一个像 Kubernetes 那样的集群编排工具,而是可以运行在 Kubernetes、AWS 等平台之上。Ray 也不是 NumPy、Pandas 或 TensorFlow 的替代品,而是一个可以用来扩展这些库应用的工具。
Ray 的生态系统如下图所示:

在底层(蓝色部分)是 Ray 的核心 API,它提供了将 Python 函数和类转化为分布式任务的能力。在此之上,构建了用于超参数调优、训练、强化学习等的高级库,如 Tune 和 RLlib。
为什么需要新的分布式系统?🤔
分布式系统并非新事物,从高性能计算到大数据,再到深度学习,已有许多专用工具。然而,当前的应用趋势是这些原本独立的工作负载开始重叠。
例如,强化学习结合了深度学习与高性能模拟;在线学习需要应用持续与环境交互并学习。这些复合型应用需要一个能够同时支持多种工作负载的通用分布式系统,这正是 Ray 的设计目标。
Ray 核心 API 详解
理解了 Ray 的定位后,我们来看看它的核心 API 是如何工作的。Ray API 非常简单,核心思想是通过装饰器将普通的 Python 函数和类转化为分布式组件。
分布式函数
以下是两个普通的 Python 函数:
def read_data(filename):
# 读取文件并返回数组
return data
def add_two_arrays(arr1, arr2):
# 将两个数组相加
return arr1 + arr2
通过添加 @ray.remote 装饰器,我们可以将它们转化为远程任务:
import ray
ray.init()
@ray.remote
def read_data(filename):
return data
@ray.remote
def add_two_arrays(arr1, arr2):
return arr1 + arr2
调用时使用 .remote() 方法,它会立即返回一个“未来对象”(Future),而不会阻塞程序:
future1 = read_data.remote("file1.txt")
future2 = read_data.remote("file2.txt")
# 两个任务会并行执行
future3 = add_two_arrays.remote(future1, future2)
# 此任务依赖于前两个任务的结果
result = ray.get(future3)
# 使用 ray.get() 获取实际结果
分布式类(Actor)
除了函数,Ray 还能将类转化为“Actor”,即运行在集群中的有状态服务。
以下是一个简单的计数器类:

class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
return self.value

通过装饰器将其转化为 Actor:

@ray.remote
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
return self.value
创建实例和调用方法:

counter = Counter.remote() # 在集群中创建一个 Actor 实例
future1 = counter.increment.remote() # 调用方法,返回 Future
future2 = counter.increment.remote()
print(ray.get(future1), ray.get(future2)) # 输出 1, 2
你还可以为任务或 Actor 指定资源需求,例如 GPU 或更多内存。
Ray 的最新进展与性能优化
我们已经了解了 Ray 的基本用法,本节将介绍 Ray 在性能和工具方面的最新进展。
Ray 团队在从 0.7 版本升级到 0.8 版本时进行了重新架构,主要目标是提升性能和可靠性。性能提升意味着 Ray 可以像 GRPC 一样快,满足苛刻应用的需求。可靠性方面,通过引入分布式引用计数和垃圾回收,解决了常见的内存溢出问题。

重新架构的核心思想是将控制状态从全局存储转移到工作进程内部。这简化了设计,并实现了“直接调用”,允许进程间直接通信,绕过了调度器,从而大幅提升了性能。
以下是性能对比的一些数据:

- 任务吞吐量:在某些微基准测试中,0.8 版相比 0.7 版有数量级的提升。
- 扩展性:在一个 256 CPU 核心的集群上,0.8 版可以达到每秒 25 万个任务,而 0.7 版在较大集群上每秒只能处理约 5 千到 1 万个任务。
集群启动与部署工具 🛠️
开发完应用后,我们需要将其部署到集群上运行。Ray 提供了一套工具,可以轻松在 AWS、GCP、Azure 或 Kubernetes 上启动集群。
以下是一个简单的示例,展示如何将一个在本地运行缓慢的 Scikit-learn 模型训练任务,快速部署到云集群上执行。
-
本地运行(缓慢):
python example.py即使使用本地所有核心(如16核),训练1000个模型仍然非常耗时。
-
启动 AWS 集群:
ray up config.yaml命令会读取
config.yaml文件中的配置(如实例类型、区域、自动伸缩策略)来创建集群。 -
提交任务到集群:
ray submit config.yaml example.py该命令会将脚本和文件复制到集群并执行。原本需要极长时间的任务,在一个10节点的集群上可能仅需几秒钟即可完成。
Ray 仪表板:调试分布式应用的利器 🔍
仅仅能开发分布式应用是不够的,调试往往更加耗时。Ray 仪表板旨在让调试分布式应用像调试本地应用一样简单。
仪表板提供了以下关键功能:
- 错误聚合:当集群中某个任务抛出异常时,你无需 SSH 到各个机器查看日志。异常信息会聚合在仪表板的统一视图中,并清晰地显示出来。
- 日志流:任务的标准输出和标准错误也会被流式传输到仪表板和控制台。
- 逻辑视图与性能剖析:仪表板提供“逻辑视图”,以应用为中心(而非机器为中心)展示集群状态。你可以直接对运行中的 Actor 进行性能剖析,生成火焰图,直观地看到时间消耗在哪些函数上,而无需预先对代码进行任何插桩或修改。
未来展望
Ray 的未来发展集中在以下几个方向:
- 持续的性能优化:性能是通用性的关键,更好的性能意味着能支持更多样化的工作负载。
- 多语言支持:目前主要支持 Python 和 Java,未来会考虑增加更多语言绑定。
- 智能自动伸缩:目标是让用户只需关注应用逻辑,系统自动管理集群资源的伸缩。
- 增强的仪表板与监控:进一步简化分布式应用的调试和生产环境监控体验。
- 完善 Windows 支持:持续改进对 Windows 系统的支持。
总结

本节课中我们一起学习了 Ray 分布式系统。我们了解到 Ray 通过简单的装饰器 API,能将 Python 函数和类轻松转化为分布式任务和 Actor。它拥有丰富的上层库生态,支持超参数调优、训练、强化学习等多种机器学习任务。同时,Ray 提供了强大的集群部署工具和可视化仪表板,极大地简化了分布式应用的开发、部署和调试流程。Ray 的目标是成为一个高性能、通用且易于使用的分布式计算基础框架。
课程 P2:太阳耀斑预测 🌞

在本课程中,我们将学习如何利用机器学习技术预测太阳耀斑。课程内容将涵盖太阳耀斑的基本概念、传统预测方法、机器学习带来的变革以及该领域的未来展望。
什么是太阳耀斑?⚡

首先,我们来了解太阳耀斑。太阳耀斑是太阳大气中剧烈的能量释放现象。
以下是太阳耀斑的三个基本属性。
属性一:频繁性与稀有性
太阳一直在发生耀斑。小耀斑很常见,而像2017年9月6日那样的大耀斑则非常罕见。这是过去15年中最大的耀斑。


属性二:发生在活动区
耀斑发生在太阳活动区。活动区是太阳表面磁场强、结构复杂且快速演变的区域。下图展示了2017年9月大耀斑发生前几天太阳表面(光球层)的磁场图像。

图中,白色区域表示磁场穿出页面,黑色区域表示磁场穿入页面,灰色表示零磁场。

属性三:能量释放模式
耀斑释放能量的方式类似于地震:应力缓慢积累,然后迅速释放。下图显示了太阳的累积X射线辐射。
X轴是时间,Y轴是通量(单位:瓦特/平方米,对数刻度)。两种颜色代表两个不同的波长波段。可以看到,太阳持续输出一个基线水平的X射线辐射,然后在9月6日,这个输出增加了两个数量级,这就是耀斑。
传统的太阳耀斑预测方法 🔍
上一节我们介绍了太阳耀斑的基本属性,本节我们来看看人们传统上如何预测太阳耀斑。
我们可以将预测太阳耀斑类比为预测地球天气。就像我们利用地球上的云图模式来预报今天或明天是否会下雨一样,我们可以利用太阳上的磁场模式来判断今天或明天是否会发生耀斑。

以下是如何从太阳图像数据中识别磁场模式或特征的一个例子。
让我们回到太阳表面的磁场图像,并只关注一个活动区。观察这个区域时,我们注意到图像中白色和黑色部分之间存在一些边界,例如这里和这个圆形区域。在这些地方,磁场线方向几乎相反,穿出页面的磁场线紧邻着穿入页面的磁场线。耀斑通常起源于这些区域。
因此,我们可以做的一件事是:隔离位于这些边界区域上或非常接近的像素,并对它们的值求和。这样,我们就计算出了这个特定活动区在特定时刻通过特定区域的磁场,或者说磁通量。这就是一个特征。
有许多优秀的研究描述了活动区的许多不同特征。



传统预测方法
那么,人们传统上如何使用这些特征来预测太阳耀斑呢?
传统上,大多数研究只使用一两个特征,并结合简单的统计技术。虽然存在一些例外,但很多文献都是如此。
机器学习如何改变格局?🤖
上一节我们了解了传统方法的局限性,本节我们将探讨机器学习带来的变革。
机器学习使我们能够以有意义且最优的方式将大量特征组合起来。现在,我们不再只使用一两个特征,而是可以使用数十个甚至数百个特征。同时,机器学习也让我们能够充分利用像太阳动力学天文台产生的超大型数据集。
接下来,我将介绍我与许多同事合作完成的四个利用机器学习预测太阳耀斑的项目。

项目一:首次尝试(最简单的方法)
这是我们的第一个也是最简单的方法。

在这项研究中,我们获取太阳磁场图像,只关注活动区,并计算了25个特征,例如磁通量或磁场中储存的能量。我们对每一个活动区在每一个时间点都进行了计算。
最终,我们得到了描述2100个活动区的3800万个特征。我们将此问题构建为一个二元分类问题:这个活动区会产生大耀斑吗?是或否?
对于学习算法,我们使用了Scikit Learn中的SVM分类器。我们得到了一些相当不错的结果。
衡量模型性能的方法有很多。我们的准确率为0.92,这看起来不错。但对于稀有事件预测,高准确率可能具有误导性,因为你可以通过总是预测“否”来获得很高的准确率。
因此,我们还计算了一个称为TSS的技能分数,它量化了你比总是预测“否”好多少。这个特定的技能分数范围从-1到1,其中-1是最差的预测(总是预测错),+1意味着总是预测对,0则等同于随机猜测。
我们这项研究的TSS是0.76。从科学角度,我们了解到磁场极度扭曲、卷曲的活动区最有可能发生耀斑。
项目二:直接利用图像数据
在第一次尝试之后,我们想:好吧,我们把所有这些图像数据压缩成了一个数字,而且我们只看了太阳的表面。那么,为什么不直接看图像数据呢?为什么不同时看看太阳的大气层,而不仅仅是表面呢?
这一次,我们在360万张太阳表面和太阳大气各层的图像上使用了卷积核网络。我们最终得到的技能分数是0.81。
但这个技能分数在前一个项目的误差范围内,所以尚不清楚这种方法是否带来了真正的改进。这让我们陷入了思考:如果我们使用了所有这些额外的数据却没有得到显著更好的结果,那问题出在哪里?
首先,我们的太阳表面图像描绘的是矢量场(磁场),而不是像强度那样的标量场。我们的大气层图像在动态范围上跨越了五个数量级。这些数据与我们用手机拍摄的日常图像非常不同。也许CKN并不完全适合处理这些数据。

其次,我们在做后续预测时没有考虑之前的预测。所以,也许我们可以给模型增加一个自回归成分。
最后,我们独立地看待所有活动区。但也许在任何给定时间,日面上的所有活动区都在相互影响。这个概念并不新鲜,它被称为“交感耀斑”,即一个活动区的耀斑会触发另一个活动区的耀斑。也许我们可以研究这一点。
项目三:研究交感耀斑
基于项目二的思考,我们开始了第三次尝试,目前正在进行中,我们开始研究交感耀斑。
以下是我们如何构建问题:假设我们计算了活动区的一个特定特征,比如之前讨论的磁通量。再假设我们跟踪这个特征随时间的变化。那么现在我们有了一个时间序列。由于我们有很多特征,我们就有了很多时间序列。所有这些时间序列只描述一个活动区。
但是,假设我们也同时观察太阳上所有其他活动区的时间序列。当我们使用所有这些活动区的时间序列数据时,我们是否能更好地预测耀斑?还是当我们单独观察每个活动区时,能更好地预测耀斑?
为了进行这项分析,我们使用了statsmodels中的向量自回归模型。我们的初步结果表明,一个活动区的耀斑确实会触发另一个活动区的耀斑。目前我们正在评估这种影响到底有多大。
项目四:着眼于整个日地系统
最后是第四次尝试。这是一个着眼于整个日地系统的大型项目。
在前三种方法中,我们只关注太阳。但太阳是一颗活跃的恒星。它不断以每秒约500公里的速度向太空喷射等离子体,我们称之为太阳风。太阳风与包括地球在内的所有行星相互作用,并塑造了一个太阳系存在于其中的磁泡,称为日球层。
有许多太空任务在日球层的不同位置采集数据。因此,我们不仅有太阳的图像数据,还有大量直接测量数据,用于采样太阳风、地球磁场和地球大气层。
除了所有这些观测数据,我们还有数值模型,可以从磁流体动力学方程的第一性原理出发,预测太阳风如何从太阳传播到地球。
我是由密歇根大学牵头的一个大型50人团队(名为Solstice)的成员。Solstice项目旨在利用机器学习来预测太阳上的爆发事件最终将如何影响地球。这是一个相当困难的问题。
这个问题最困难的部分之一是弄清楚太阳风如何与地球磁场耦合,特别是在地球磁场的向阳面以及地球磁极附近。这个项目几个月前刚刚启动。
我们从中学到了什么?📚
回顾这四种方法以及其他许多利用机器学习进行耀斑预测的优秀研究,我们学到了一些东西。
首先,我们了解到机器学习算法相比传统方法提供了巨大优势。开源科学软件对太阳耀斑预测领域产生了极其积极的影响。
其次,我们了解到增加机器学习模型的复杂性并不一定会提高预测能力。预测能力最大的提升来自于使用越来越多物理意义明确、信息丰富的输入数据,这不等同于简单地使用“更多数据”。
因此,你在识别具有物理意义和信息丰富的特征方面投入的前端工作越多,在预测能力上的回报就越大。
该领域的未来是什么?🚀
在本节中,我确定了三个我认为如果得到解决,可能在该领域带来突破的问题。
以下是这三个关键问题:
- 解决类别不平衡问题:这是许多科学领域的一个大问题。相对于有事件发生(如耀斑、地震、超新星),我们总是有更多无事发生的例子。这是一个非常难以解决的问题,目前没有明确的解决方案。一个潜在的解决方案是使用像生成对抗网络这样的工具来创建逼真的事件合成观测数据。
- 迁移学习:我们都在使用不同的数据集,并从每个数据集中学到不同的东西。例如,有人可以使用2000年代初运行的卫星的历史数据来预测耀斑,而我则可以使用太阳动力学天文台的数据来预测耀斑。但我们如何能够捕获从一个数据集训练中获得的知识,并将其迁移到另一个相关问题上?迁移学习是另一个大的研究领域。我在日球层物理学中看到的一个解决方案是创建大型的、经过相互校准的统一数据集。
- 可解释性问题:机器学习的实际成功并不总是与可解释性的成功齐头并进。可解释性对于科学研究非常重要,因为仅仅说“我能预测耀斑”是不够的。真正的科学问题是:“我们在这里学到了什么物理?” 可解释性元素才是真正的科学成果。我认为前进的方向之一是使用与模型无关的可解释性工具。
我认为,我们作为一个科学共同体,可以通过致力于解决这三个问题来取得重大进展。
总结

在本课程中,我们一起学习了太阳耀斑的基本概念,了解了传统的预测方法及其局限性。我们重点探讨了机器学习如何通过整合大量特征和利用大数据集来变革这一领域,并回顾了四个具体的机器学习应用项目。最后,我们展望了该领域未来面临的挑战,包括类别不平衡、迁移学习和模型可解释性等关键问题。通过持续的研究和创新,我们有望更准确地预测太阳活动,并深化对太阳物理的理解。

课程 P3:Geomstats - 机器学习中的黎曼几何 Python 包 🧮
在本节课中,我们将要学习 Geomstats,一个用于机器学习中几何计算的 Python 包。我们将了解其设计动机、核心概念、基本操作、学习算法,并与其他相关库进行比较。
概述:为什么需要 Geomstats? 🎯
Geomstats 被设计用于在流形上进行数据计算。流形可以粗略地定义为可以弯曲的空间,因此本质上是非线性的。许多数据天然存在于流形上,例如地球上的城市坐标、网络中的交互图或大脑连接组的相关矩阵。为了在这些数据上应用机器学习算法,我们需要考虑数据空间的几何结构,即流形的几何结构。

流形上的数据示例 🌍
以下是数据存在于流形上的几个例子:
- 地理数据:例如地球上的城市坐标。数据所在的流形是球面。
- 交互网络图:例如一个混沌实验室中不同成员之间的交互图。数据所在的流形是李群空间。
- 相关矩阵:例如大脑连接组,即不同脑区激活之间的相关性。数据所在的流形是对称正定矩阵空间。
还有许多其他例子,如刚性变换、3D 框架、RN 的子空间等。因此,Geomstats 旨在提供能够考虑数据空间几何结构(即流形的几何结构)的学习算法。
考虑几何结构的必要性:以均值为例 📊
上一节我们看到了流形上数据的例子,本节中我们来看看为什么需要考虑几何结构。让我们以一个非常简单的学习算法——均值计算为例。
在向量空间中,均值 X_bar 的定义是数据点 X_i 的线性组合。然而,流形是非线性的。如果我们在线性算法(如均值)应用于非线性流形(如球面)上的数据,会发生什么?
线性均值可能不属于该流形。例如,在球面上随机采样两个点,它们的线性均值会落在球体内部,而不是球面上。
因此,我们需要为流形上的数据生成学习算法,甚至是均值这样的简单计算。这通过一个称为 弗雷歇均值 的广义定义来实现。弗雷歇均值计算流形上使到所有数据点平方距离之和最小的点。使用这个定义,流形上数据点的弗雷歇均值属于该流形。
在 Geomstats 中,这可以通过 FrechetMean 类轻松计算。该估计器依赖于流形上的距离(即球面上的度量)。拟合数据点后,我们得到的估计值就是弗雷歇均值,它确实位于流形上。
但这只是一个非常简单的学习算法。问题在于,其他学习算法如何推广到流形?如果我们需要推广均值,肯定也需要推广其他学习算法。这就是 Geomstats 的设计目的。
Geomstats 简介 🐍
Geomstats 是一个 Python 包,提供几何统计和几何学习工具,用于在流形上进行数据计算。它被组织成四个主要模块:
geometry模块:包含在流形上计算所需的所有必要操作。learning模块:包含用于流形上数据的学习算法。datasets模块:允许加载流形上的开源数据集。visualization模块:允许我们获取关于流形上这些计算的可视化信息。
Geomstats 还在三个后端实现:NumPy、TensorFlow 和 PyTorch,从而支持 GPU 计算。
Geomstats 有三个主要目标:
- 实践教学:旨在填补许多教科书只教授微分几何理论而很少提供代码或编码练习的空白。
- 普及使用:由于代码被封装在对象中,您可以使用 Geomstats 的几何学习算法,而无需研究算法的所有复杂数学细节。
- 支持研究:通过邀请研究人员提供他们的几何学习代码并将其纳入库中,来支持几何学习领域的研究。
在流形上计算:基本构建块 🧱
我们知道如何在向量空间上计算。在向量空间中,有两个基本元素:点和向量。我们可以将一个向量加到一个点上得到另一个点,也可以减去两个点得到一个向量。我们还可以定义内积(例如欧几里得内积),从而得到向量空间上点之间的距离概念。这些是任何在向量空间上计算的算法的基本构建块。
如果希望算法在流形上工作,我们需要推广这些构建块。因为如果取一个向量并将其加到流形上的一个点上,会得到另一个点,但这个点可能不在流形上。
它们被推广如下:
- 点:我们仍然有流形上的点。
- 切向量:我们不再有普通的向量,而是在流形上某点处有切向量。
- 指数映射:我们不能简单地将向量加到点上,需要使用称为指数映射的加法推广。指数映射接收一个点和切向量,并提供流形上的另一个点。在视觉上,指数映射会从初始点沿着属于流形的路径(测地线)“发射”到终点。
- 对数映射:在向量空间中,我们可以减去两个点得到一个向量。在流形上,这个操作通过对数映射的概念进行推广。我们取两个点,通过“相减”得到一个切向量,这个切向量是从第一个点到第二个点所需的切向量。
以下是一个在 Geomstats 中如何实现这些操作的代码片段:

# 加载流形上的数据,例如地球上的城市坐标
# 取流形上的一个点,例如巴黎
# 通过在巴黎的切空间中定义并投影一个向量,来定义巴黎处的一个切向量
# 使用指数映射(广义加法)尝试将此切向量加到点上
# 计算从基点沿切向量“发射”时在流形上定义的路径(测地线)
# 使用对数映射(广义减法)将一个点减去另一个点以获得切向量
这些操作在球面上进行了说明,因为它易于可视化。但实际上,Geomstats 为 15 种不同的流形实现了这些流形操作,包括不一定易于可视化的流形(例如高维流形)。
流形学习算法示例 🤖
既然我们已经有了在流形上计算的所有构建块,现在让我们看看可以在 Geomstats 中使用哪些几何学习算法。

在向量空间中,我们知道许多学习算法,可以分为两大类:监督学习算法(如逻辑回归)和无监督学习算法(如主成分分析或 K 均值)。现在我们有了推广它们的构建块,我们可以在流形上拥有类似的算法。
监督学习:切空间逻辑回归
在 Geomstats 中,这被实现为切空间逻辑回归。其思想是,即使流形是弯曲的,如果我们站在流形的一个点上,我们可以查看该数据点处的切空间,这现在定义了一个向量空间。因此,常见的做法是将流形上的所有数据点“运输”到切空间,特别是数据的弗雷歇均值处的切空间。现在,流形上的所有点都在切空间上,它们属于一个向量空间,我们可以使用通常的学习算法,例如 scikit-learn 库提供的算法。
假设我们想在 SPD 矩阵流形上设计逻辑回归。具体来说,对于每个连接组,我们希望预测它是精神分裂症患者还是健康对照者的连接组。
以下是实现步骤:
# 从 geomstats 导入预处理模块和切空间对象
# 从 scikit-learn 导入逻辑回归和管道
# 加载连接组数据(流形上的数据)
# 将数据拆分为训练集和测试集
# 在 SPD 流形上定义度量
# 定义管道:首先将所有数据投影到切空间,然后应用逻辑回归
# 像通常在 scikit-learn 管道中一样进行拟合和预测
# 为 SPD 流形上的每个连接组获取标签
无监督学习:切空间主成分分析
在向量空间中,我们可以使用主成分分析来查看数据集内的主要变化。在 Geomstats 中,我们可以使用切空间 PCA。
假设我们想在超球面流形上使用它:
# 导入切空间 PCA
# 实例化球面流形
# 生成该球面上具有随机分布的数据
# 计算数据的弗雷歇均值作为估计
# 在该弗雷歇均值处计算切空间 PCA(即在均值的切空间中进行 PCA)
# Geomstats 将提供沿流形的数据集主要变化分量
无监督学习:黎曼 K 均值
在向量空间中,我们可以使用 K 均值来查找数据簇。让我们看看这在 Geomstats 中如何工作。
假设我们有一个动力学实验室网络,其中每个点代表一个成员,如果成员之间有大量社交互动,则点靠得更近。我们希望看看是否可以将这个成员数据集分成两个互动组。该交互图可以嵌入到双曲空间中。
问题是如何在双曲空间中执行 K=2 的 K 均值。这在 Geomstats 中通过黎曼 K 均值实现。
# 实例化流形(庞加莱圆盘,二维双曲空间的一种特殊情况)
# 加载数据集(空手道图,二维双曲空间上的数据集)
# 将图嵌入此双曲空间
# 使用黎曼 K 均值算法(双曲空间数据的 K 均值推广)
# 指定簇的数量和双曲空间的度量
# 进行拟合和预测,为每个成员分配一个标签(所属的子组)
# 同时计算这两个子组的中心
这些学习算法在特定流形上进行了说明,但您可以将这些学习算法与 Geomstats 中实现的 15 种流形中的任何一种一起使用。
与其他库的比较 ⚖️
现在我们将看看 Geomstats 与其他也执行流形计算的库的比较。这些是执行流形计算的主要库,它们有不同的目标:
- PyRiemann 和 PyQuaternion:专用于特定流形。例如,PyRiemann 计算 SPD 空间上的数据,PyQuaternion 计算旋转流形上的数据。而 Geomstats 实现了许多不同的流形。
- Pymanopt:在流形上执行优化。流形上的优化对于学习算法非常有用。Pymanopt 可以从 Geomstats 调用以实现学习算法本身。相比之下,Geomstats 提供微分几何的基本数学运算和学习算法的封装。
- VTK Geometry:提供流形的非线性统计。它们将自己限制在计算解剖学领域有用的特定流形集。这个包实现得很好,但缺乏持续集成和单元测试。
- McTorch 和 GeoOpt:旨在用于深度学习应用,并专门实现随机自适应优化。相比之下,Geomstats 实现了低层几何操作(而非优化),然后是流形上的学习算法。
我们可以从多个维度比较这些包:
- 实现的流形:Pymanopt 实现了非常多的流形。
- 实现的微分几何操作:各包支持的操作不同。
- 可用后端:Geomstats 和 Pymanopt 提供更广泛的后端支持(NumPy, TensorFlow, PyTorch),而其他一些库专注于单一后端。
- 工程实践:在持续集成和代码覆盖率方面,Geomstats 等库具有良好的单元测试和覆盖率。
总结与社区 👥
本节课中,我们一起学习了 Geomstats 库。让我们再次强调 Geomstats 的目标:

- 实践教学:提供动手实践的几何与学习体验。
- 普及使用:使几何学习算法的使用民主化。
- 支持研究:支持几何学习领域的研究。

Geomstats 库由 Geomstats 团队创建,该团队是围绕核心贡献者的合作者团队。他们经常组织黑客马拉松来为这个库做贡献。

如果您有兴趣为库做贡献并加入社区,可以在 GitHub 上加入他们。

课程 P4:使用 Rapids 进行 GPU 加速的 Python 数据分析 🚀

在本节课中,我们将学习 Rapids,一个用于 GPU 加速数据分析和机器学习的开源库。我们将了解其核心组件、设计理念以及如何利用它来显著提升数据处理和模型训练的速度。
数据处理技术的演进 📈
上一节我们介绍了课程主题,本节中我们来看看数据处理技术的演进历程。
许多开发者可能已经使用过 Hadoop、Spark 等工具,甚至尝试过 GPU 处理。总体而言,我们能够在更短的时间内处理更多数据,但一直存在一个巨大的瓶颈。
这个瓶颈并非计算本身。GPU 的计算速度非常快。真正的瓶颈在于,当连接两个应用程序时,它们有时不共享相同的数据格式。这需要一个复制和转换的步骤。
每次进行复制和转换,我们都在移动大量数据,执行大量额外的计算,而这些计算无助于完成我们的核心任务。因此,通过消除这些复制和转换步骤,我们可以真正释放 GPU 的更多潜力。


我们采用的方法是 Apache Arrow。我们将其内存二进制格式采纳为标准。这样,每个应用程序都能就浮点数数组在内存布局、位序等方面的外观达成一致。
现在我们有了一个通用的交换格式,所有应用程序和数据格式都能认同它。这直接解锁了大量新能力。
Rapids 的愿景与动机 💡

上一节我们讨论了数据交换的瓶颈,本节中我们来看看 Rapids 如何实现其愿景。

Rapids 正是这一理念的实现,同时还包含了许多其他我们将详细探讨的功能。当所有部分协同工作时,我们能够在这些大型数据处理任务上实现几个数量级的加速。
我们的主要动机源于 Kaggle 竞赛和开发新数据科学工作流程的过程,这是一个迭代的过程。你需要不断循环,更新特征集,重新加载数据,选取不同的数据切片。迭代速度越快,进展就越快。这正是推动我们将所有这些组件整合在一起的动力。
Rapids 的核心:加速 Python 数据栈 ⚙️
上一节我们了解了 Rapids 的动机,本节中我们来看看它的核心架构。
我们以开源的 Python 数据栈为起点,这包括用于数据操作的 pandas、用于机器学习的 scikit-learn、用于图分析的 networkX,以及用于分布式计算的 dask。我们的目标是对整个栈进行 GPU 加速。
Rapids 旨在成为 pandas、scikit-learn 和 networkX 的直接替代品。它继承了 PyTorch、TensorFlow 和 MXNet 中已有的优势,并通过为 plotly、PyViz 或 datashader 等工具添加 GPU 能力来加速数据集的可视化。
Dask:通往分布式计算的桥梁 🌉
上一节我们介绍了 Rapids 的整体架构,本节中我们重点看看 Dask 的关键作用。
Dask 是连接单台桌面/笔记本电脑与更大资源池的桥梁。我们可以将 Dask 部署到大型集群或云端,也可以与 Hadoop 或 Spark 结合使用。它是原生 Python,易于连接现有包,拥有相同的训练 API、庞大的开发者社区,易于扩展且非常流行。因此,我们选择与 Dask 合作。


以下是一个示例:你可以向现有代码库添加一些提示给 Dask,告诉它任务定义在哪里以及依赖关系是什么,Dask 将构建任务图,并负责执行该图,确保所有任务尽可能快地完成。
这使得你可以连接所有使用笔记本电脑或台式机编码的用户,将他们的工作放在 Dask 之上,Dask 将扩展资源池,而无需他们大幅更改代码,并涵盖更广泛的计算机类别。例如,集群和大型超级计算机今天就可以运行 Dask。通过处理任务调度、图管理、故障转移处理、节点间数据复制等所有工作,Dask 承担了这一切。你只需专注于编写成功的机器学习算法,Dask 让你能够将其扩展到更大的数据集。
我们还引入了 OpenUCX。这使我们能够利用新的硬件能力。TCP 套接字很好,但速度慢。因此,我们允许用户在可用时连接更好的硬件。现在,Infiniband、共享内存能力甚至 NVLink 都可以用于 Dask 内部的传输。
这意味着,从 TCP 到使用 UCX,我们在合并两个数据帧时达到了约 2 GB/s 的起点。使用 UCX 和 NVLink 处理相同数据,我们立即获得了几乎两倍的性能。我们还可以引入 Infiniband。如果你的节点上有 Infiniband,现在可以将它们连接起来,达到节点间近 12 GB/s 的速度。你可以叠加这些优势,将 NVLink 与 Infiniband 结合使用。NVLink 处理节点内、工作进程内的通信,而 Infiniband 则连接跨节点。最终,在一个完全专用的节点(如拥有 16 个 GPU 的 DGX2)上,使用 UCX 可以达到 37 GB/s 的速度。
这就是我们实现这一愿景的方式:一个易于使用的 Python 数据栈,通过 Rapids 在单节点上加速。一旦完成调试并准备扩展到非常大的数据集,你可以使用带有 UCX 的 Dask,在数百个节点上获得相同的加速,以应对最大的问题。
cuDF:GPU 加速的数据帧操作 🗃️
上一节我们探讨了分布式计算框架,本节中我们深入 Rapids 的数据处理核心组件:cuDF。
cuDF 代表 CUDA DataFrame,其目标是成为 pandas DataFrame 的等效物。它还与一个名为 cuIO 的 I/O 包结合,用于读取数据集、解析数据集和执行数据转换。这里的真正动机是 ETL(提取、转换、加载)。许多数据科学家花费 90% 的时间来了解数据。因此,我们创建 cuDF 是为了减少 ETL 时间,让人们能够专注于数据科学工作流程中更有趣的部分。
以下是我们的技术栈:CUDA 位于底层,CUDA 库位于 CUDA 语言之上。我们在 C++ 中有一组核心原语,连接到 CUDA 库。这通过 Cython 进行包装,最终用户看到的是一个 Python 包。这是一种常见模式,在介绍 Rapids 组件时我们会多次看到。
最终,cuDF 是一个 Python 库。它看起来非常像 pandas,有许多相同的命令,你可以使用相同熟悉的模式来操作数据帧。但与它结合的是一个非常快速的数据 I/O 包。现在,你可以使用 GPU 加速的 CSV 读取器,以快 10 倍的速度将数据加载到内存中。你可以更快地排序数据、采样数据、进行数据清理和插补。你甚至可以使用 Numba 编写自己的自定义用户定义函数,以满足特定需求。
以下是 cuDF 的一些性能基准,展示了其显著的加速效果:

- 数据帧连接:速度提升高达 300 倍。
- 数据帧排序:速度提升高达 300 倍。
- 大数据集分组聚合:在 1000 万或 1 亿行的数据集上,速度提升显著。

这些基准假设我们使用了内存池来减少内存分配的开销,并且是在单个 GPU 上进行的。
但数据帧并不是全部。过去,GPU 上的字符串支持并不理想。我们努力改变了这一点。现在,cuDF 原生支持字符串列,包括正则表达式和许多字符串操作,速度提升达 20 到 30 倍。分类列、字典编码等功能现在在 cuDF 中也得到了很好的支持。
我们提到了数据格式读取器和解析器。CSV 是我们首先实现的,速度提升达 10 倍。我们现在还可以处理 Parquet、ORC、JSON、JSON Lines、Avro 等格式,并且一直在增加更多支持。
超越 ETL:与深度学习框架集成 🔗
上一节我们专注于数据操作,本节中我们看看 Rapids 如何与深度学习生态集成。
ETL 不仅仅是数据帧。如果你的数据是图像或音频呢?我们可以连接这些数据。你可以使用 cuDF 或通用数组格式加载数据,然后通过零拷贝直接传递给 PyTorch、TensorFlow 或 MXNet。这是因为我们采用了用于数组/张量的 DLPack 接口和 CUDA 数组接口。通过使用这些 API,我们能够在无需移动数据的情况下,与深度学习包进行零拷贝数据交换。


我们还展示了在 Dask 中直接替代 NumPy 数组的能力。现在,我们可以用 cuPy 替换 Dask 中作为大型分布式数组块依赖的 NumPy。这些块现在位于 GPU 内存中。这意味着,由于数据在更快的 GPU 内存中,你可以更快地移动和操作数据,在千兆字节大小的数据上获得 100 倍的加速。
对于像 SVD(奇异值分解) 这样的密集计算操作,高效分布并获得良好的加速仍然具有挑战性。Dask 和 cuPy 的结合能够在不到一分钟的时间内对 2000 万行、1000 列的数据执行 SVD。对于最大的数据集,我们已经能够使用 Dask 和 cuPy 运行总计达 3.2 PB 的数组。
cuML:GPU 加速的机器学习 🤖
上一节我们讨论了数据集成,本节中我们转向机器学习的核心组件:cuML。
cuML 是一个类似于 scikit-learn 的库,旨在填补 Python 数据栈中 scikit-learn 的位置。它试图解决这样一个问题:我们拥有大量数据,并且数据集越来越大,但我们如何在固定时间内高效组织数据、清理数据并从中获取洞察?cuML 就是我们的答案。
这是一个熟悉的故事:我们从 CUDA 开始,构建 CUDA 库,从这些库中构建原语,使用这些原语编写新的机器学习算法,然后用 Cython 包装。最终用户看到的是 Python。
以下是一个使用 Moons 数据集进行聚类的示例,这是 scikit-learn 中使用 pandas 的标准示例。你可以采用完全相同的代码,只需将 import 语句中的 pandas 替换为 cuDF,将 scikit-learn 替换为 cuML,就能获得相同的结果,只是速度更快。
以下是 cuML 中模仿 scikit-learn 的一些算法列表:
- 回归算法
- 分类算法
- 推断算法
- 聚类算法
- 分解算法
- 降维算法
- 时间序列算法
- 超参数调优算法
我们一直在寻求增加更多算法。
以下是一些基准测试,比较了单 GPU cuML 与运行在 220 核 CPU 上的 scikit-learn。加速效果因算法和数据大小而异。一般来说,输入 GPU 的数据量越大,看到的加速比就越高,直到达到 GPU 能处理的最大极限。这是因为 GPU 喜欢满载运行,这为它们提供了更多的并行化和数据重用机会。在许多情况下,加速比达到 10 到 20 倍,有些情况下甚至达到 50 到 100 倍。这些加速是真实的,并且它们是 scikit-learn 的直接替代品。
让我们谈谈推断。推断是指获取训练好的模型(无论是神经网络还是随机森林),并尽可能快地运行它以进行预测。我们添加了一个森林推断库。如果你训练了一个随机森林模型或提升树模型,我们可以将其压缩到最基本要素,并在 GPU 上并行运行。现在,我们可以用这个库每秒进行 1 亿次推断,并显示出相对于仅 CPU 推断 20 到 30 倍的加速。
XGBoost 可能是我们集成到 Rapids 中最重要的机器学习包。我们从一开始就拥有它。现在,XGBoost 与 Rapids 建立了非常紧密的连接,能够从 cuDF、cuPy、Numba 或 PyTorch 进行零拷贝数据导入。它已经过重写和改进,我们能够使用比以前少三分之二的内存,并且现在支持 GPU 上的学习排序等新功能。
如果你对使用云服务感兴趣,Rapids 与 Amazon SageMaker、Azure ML 和 Google AI Platform 集成,它们都可以与 Rapids 和 Dask 开箱即用。我们还使用 Ray Tune 在超参数优化方面做了一些出色的工作。如果你有一个需要超参数调优的任务(这在机器学习中很常见),你需要进行扫描,运行几十到几百个任务来寻找最佳参数。这通常需要大量时间。现在,我们能够使用 Dask 在 GPU 上加速这一过程。因为在云端,你按使用时间付费,这直接转化为降低工作成本。对于相同的调优任务,使用 Rapids 与仅使用 CPU 相比,我们能看到成本降低 7 倍。
cuGraph:GPU 加速的图分析 🕸️
上一节我们介绍了机器学习库,本节中我们看看用于图分析的组件:cuGraph。
cuGraph 是 networkX 的类似物,用于图分析。我们一直试图为 networkX 中熟悉的算法提供开箱即用的突破性性能和规模。我们可以采用熟悉易用的 networkX API,添加属性图支持,扩展到数十亿条边,保持 Python 特性,在底层使用 C++ 和 CUDA 运行,并不断添加新功能。
技术栈看起来很熟悉:底层是 CUDA,之上是 CUDA 库,我们有自已的原语,有使用这些原语的算法,用 Cython 包装,然后用户看到顶层的 Python 接口。
以下是 cuGraph 支持的算法列表:
- 社区检测
- 连通分量分析
- 链接分析
- 链接预测
- 图重编号(将非整数 ID 转换为方便计算的整数 ID)
- 图布局可视化(使用 GPU 加速的力导向布局算法)
- 图遍历
- 最短路径算法
- 中介中心性(包括边和顶点)
以下是一些基准测试,展示了 cuGraph 的速度。在常见数据集上,对于 Louvain 模块度、PageRank、广度优先搜索和单源最短路径等算法,与 networkX 相比,我们看到了高达 200 倍 的加速。如果你有非常大的数据集,我们对 PageRank 等算法提供了多 GPU 支持,未来会有更多算法,使你能在这些大数据集上实现每秒数百 GTEPS 的性能。


可视化:洞察大规模数据 👁️
上一节我们探讨了图分析,本节中我们看看 Rapids 如何帮助可视化大规模数据。
通常,数据集如此之大,以至于在概念上理解它们都很困难,而可视化是发现数据中隐藏有趣部分的关键。我们长期以来一直拥有 cuCrossfilter,用于数据帧中任何数据的 GPU 加速交叉过滤。这允许你构建仪表板来查看数据,开销很低,只需编写很少几行代码,你就能快速可视化和理解数据集。
PyViz 允许你导入大型数据集并快速在屏幕上渲染,以便查看并更快获得洞察。由于与 cuDF 的集成以及零拷贝能力,我们能够在这些大型数据集上实现 10 倍到 100 倍的加速。
Plotly Dash 是另一个例子,它使用 Bokeh 并通过与 Rapids 的连接,能够展示一个包含 2010 年人口普查中每个人(即 3 亿个数据点)的演示,我们可以可视化、交互、切片、进行地理查询等,所有操作都在一个高度交互的 UI 中完成。

总结与生态系统 🌍

在本节课中,我们一起学习了 Rapids,一个旨在成为机器学习一切事务中心的 GPU 加速开源库。
Rapids 连接到 I/O、可视化、分析、特征工程和数据分区。它既能向上扩展(单节点多 GPU),也能向外扩展(多节点)。我们正努力让 Rapids 可用,帮助每个人将更多时间花在实际工作上,减少在 ETL 上的时间。
我们并非独自完成这项工作。我们有一个优秀的社区与我们合作,贡献代码,保持这是一个开源项目,供所有人使用。我们在工业界有许多采用者,将其用于自己的工作流程,并从这个开源软件中获得了巨大价值。
我们还提到了 BlazingSQL,这是一个基于 cuDF 构建的 GPU 加速 SQL 引擎,可以运行完整的 TPCH 查询集,例如直接从 Amazon S3 存储桶加载数据,无需任何特殊的数据准备。
如果你想开始使用 Rapids,有以下几种方式:
- Docker 容器
- Conda 源下载
- Google Colab
- GitHub(获取最新夜间构建版)
- 教程 Notebook 和端到端数据分析示例工作流
- 安装助手(交互式界面)
- 详细文档、Slack 频道、Google 网上论坛和 Stack Overflow 支持
主要代码获取途径包括 GitHub、Anaconda Cloud 和 NVIDIA GPU Cloud(NGC)上的预构建容器。


希望你能享受使用 Rapids 的过程!
课程 P5:JAX - 加速机器学习研究 🚀

在本节课中,我们将学习 JAX 项目。JAX 是 Google Research 推出的一个工具,旨在为 Python 提供可组合的函数变换,以加速机器学习研究。
什么是 JAX? 🤔
JAX 是一个来自 Google Research 的项目。它提供了一个工具,用于加速机器学习研究。这个工具应该是一个可组合的函数变换系统,基于 Python。
动机:为何需要 JAX? 💡
为了理解 JAX 的价值,可以设想一个场景:你想在 Python 中从头实现一个深度神经网络。虽然市面上有许多专为神经网络设计的工具,但如果你想以一种高性能且可扩展的方式从头实现,会有些困难。
例如,你可以使用 NumPy 来实现神经网络的核心功能。下面是一个简单的全连接神经网络及其预测函数:
def predict(params, inputs):
for W, b in params:
outputs = np.dot(inputs, W) + b
inputs = np.tanh(outputs)
return outputs
这个模型需要一个损失函数来进行优化。损失函数可能如下所示:
def loss(params, inputs, targets):
preds = predict(params, inputs)
return np.sum((preds - targets) ** 2)
这就是深度学习。但要真正实现“深度”,将其扩展到大量数据和非常大的模型,你需要比单纯使用 NumPy 更高效的方法。
NumPy 的局限性 🚧
为了获得高性能,深度学习通常需要在 GPU 和 TPU 等加速硬件上运行。这对于标准的 NumPy 来说很困难。
其次,由于参数数量通常很大,我们需要某种自动微分来进行梯度下降等优化。这在 NumPy 中同样困难。
此外,还有操作融合或编译的需求。例如,在计算预测值与目标值的平方差之和时,NumPy 会创建差值数组、平方差值数组,最后再求和。如果能将这些操作融合成一个,避免实例化所有中间数组,效率会更高。
最后,能够并行化计算,在不同计算节点间分发数据也是理想的功能。
JAX 的解决方案 ⚙️
JAX 的理念是提供一个类似 NumPy 的 API,但附带了进行大规模深度学习所需的所有附加功能。

关键区别在于,我们将导入语句从 import numpy as np 改为 import jax.numpy as jnp。jax.numpy 不是 NumPy,但它提供了一个与 NumPy 具有相同抽象集的 API。
运行此代码时,JAX 可以将其编译为 XLA(加速线性代数库,也是 TensorFlow 的底层技术之一),并在 GPU 和 TPU 等芯片上运行。仅仅通过导入 jax.numpy 而不是 numpy,我们就获得了在加速硬件上运行的第一步能力。
但 JAX 的功能不止于此。
JAX 的核心功能 ✨
上一节我们介绍了 JAX 的基本理念,本节中我们来看看它的几个核心功能。
以下是 JAX 提供的一些高阶函数示例:
- 自动微分:使用 JAX 中的
grad函数,我们可以计算一个新函数来评估损失函数的梯度。JAX 了解损失函数和预测函数中的所有操作,并知道如何解析地计算其梯度。from jax import grad grad_loss = grad(loss) - 向量化:如果我们想将梯度函数映射到多个输入上,可以使用
vmap。from jax import vmap batched_grad = vmap(grad_loss) - 即时编译:我们可以使用
jit编译函数,它接收一个输入并返回一个包装了已编译 XLA 代码的函数。编译会将诸如平方差等操作融合在一起,使它们一次性完成。from jax import jit compiled_loss = jit(loss) - 并行化:如果你想跨多个芯片并行化,可以将
vmap改为pmap,以跨并行架构进行映射,并使用分布式数据和计算。from jax import pmap parallel_batched_grad = pmap(grad_loss)
本质上,JAX 可以被视为一个可扩展的系统,用于对 Python 和 NumPy 代码进行可组合的函数变换。你编写看起来像 NumPy 的代码,然后添加一些高阶函数来编译或计算梯度等。通过这种方式,你可以从这些可组合的构建块中创建高效的程序,而无需依赖专门构建的系统来获得高效计算。
演示:JAX 实战 🎬
让我们通过一个演示来看看 JAX 的实际应用。
类似 NumPy 的 API
如果你习惯使用 NumPy 库,可以这样做:
import numpy as np
x = np.random.randn(2000, 2000)
JAX 基本上可以做同样的事情。我们导入 jax.numpy 并创建一个 JAX 数组:
import jax.numpy as jnp
y = jnp.array(x) # 创建一个设备数组 (Device Array)
这个设备数组将原本在 CPU 内存中的数组转移到了 GPU 内存中,以便 GPU 进行操作。就像我们使用 NumPy 一样,我们可以用 JAX 做所有相同的事情:元素级运算、广播运算、线性代数等。所有这些操作都在 GPU 上计算,因此速度更快。
即时编译 (JIT)
即时编译是一种将你编写的 JAX NumPy 代码转换为 GPU 上更高效的融合代码的方法。
考虑一个函数:
def f(x):
for _ in range(10):
x = x - 0.1 * x
return x
直接计算 f(y) 时,所有操作都是顺序执行的,相对较慢。JAX 提供了 jit 操作符:
from jax import jit
g = jit(f)
jit 操作会评估函数内部的代码,并返回其编译版本。计算 g(y) 会得到与 f(y) 相同的结果,但速度要快得多(例如,从 6.9 毫秒减少到 282 微秒)。这非常强大,因为它允许你使用 NumPy 原语来表达函数,甚至无需考虑内存管理、操作顺序和操作融合等问题。你只需编写有意义的代码,然后 JAX 的 JIT 会负责将这些操作融合在一起并进行编译,使其高效运行。
自动微分 (Autograd)
自动微分是深度学习中快速优化模型的关键。假设有一个函数:
def f(x):
return x * jnp.sin(x)
手动计算其梯度需要应用链式法则。但对于复杂的机器学习模型,手动计算不切实际。JAX 提供了 grad 对象:
from jax import grad
grad_f_jax = grad(f)
grad 接收一个函数 f,并返回一个计算其梯度的函数。JAX 不是通过有限差分法,而是通过查看函数及其组成的操作,本质上自动应用链式法则来解析地计算梯度。这意味着对于神经网络预测和损失函数,你可以快速计算整个过程的梯度,从而优化模型。
向量化 (Vectorization)
向量化允许你将为单个输入编写的代码快速转换为处理多个输入的代码。
假设有一个函数:
def square(x):
return jnp.sum(x ** 2)
要在向量序列上应用此函数,在 Python 中可以使用列表推导式,但这相对低效。JAX 提供了 vmap:
from jax import vmap
map_square = vmap(square)
vmap 会查看这个操作,并创建编译代码,使得原本需要多次函数调用的计算,变成一次批处理函数调用。这可以更快地完成,无需在用户和 GPU 之间进行任何数据往返。
vmap、grad 和 jit 是 JAX 提供的一些函数,它们让你能够用这个库做出非常惊人的事情。
JAX 的工作原理 🛠️
上一节我们体验了 JAX 的强大功能,本节中我们来探讨一下其底层原理。JAX 是如何在传递任意函数时进行 JIT 编译的?这与其他 JIT 库(如 Numba)有所不同。
关键在于,JAX 利用 Python 的动态特性。它向函数传递一个抽象值(追踪器,Tracer),这个值本身不是数组,但知道数组在特定操作下的行为。JAX 使用这个数组来表征函数的行为,并获取一个操作列表,然后将其发送给 XLA 进行编译。

例如,对于一个用 JAX 知道如何变换的底层原语构建的函数 log2,JAX 会传入一个代表数组的追踪器。追踪器不会实际计算任何值,而是记录对该值执行了何种操作(如对数运算)。通过这种方式,JAX 构建了一个函数实际操作的紧凑中间表示(IR),然后将其编译为 XLA。这就是 JAX 能够编译你编写的任何黑盒函数的原因。
与 Numba 等直接对 Python 字节码进行更复杂转换的工具相比,JAX 的 JIT 编译范围有限,但它对于数值计算和机器学习研究来说非常有用且高效。

总结 📝

本节课中,我们一起学习了 JAX 项目。我们了解到 JAX 是一个用于加速机器学习研究的 Python 库,它通过提供类似 NumPy 的 API 以及可组合的函数变换(如 jit、grad、vmap、pmap),使得代码能够高效运行在 GPU/TPU 等加速硬件上。JAX 的工作原理是通过追踪抽象值来构建计算图并编译到 XLA。尽管其 JIT 范围有限,但它已被广泛应用于从分子动力学到机器人控制等各种研究项目中,是进行高效科学计算的强大工具。

课程 P6:从演化数据流中学习 🚀
在本课程中,我们将学习一种不同于传统批量学习的机器学习范式——流学习。我们将探讨其核心概念、面临的挑战、关键算法以及如何利用工具进行实践。
概述:什么是标准机器学习?🤔
标准机器学习通常基于批量数据,因此也被称为批量学习。其流程是:首先收集并存储一段时间内的数据,然后利用这些数据训练一个数学模型,最后部署该模型以对新的未知数据进行预测。
这种方法在许多应用中表现出色。然而,在某些现实场景中,它可能面临挑战。
流学习的挑战与需求 ⚡
当数据持续生成而非一次性可用,或者数据随时间变化时,批量学习方法可能难以应对。例如,在新冠疫情期间,消费者的在线搜索行为在短时间内发生剧变,导致许多基于历史数据训练的自动供应链系统无法适应,从而引发混乱。
为了处理这类情况,我们引入了流学习作为替代方案。在流学习中,我们假设数据是无限的,并且必须以在线方式维护模型。这意味着我们需要:
- 处理单样本训练集。
- 即时地增量式整合数据。
- 保持资源高效,因为数据量是无限的。
- 能够检测变化并适应它们。
流学习通常基于以下四个要求进行:
- 单样本处理与单次检查:由于存储海量数据不可行,我们通常一次只处理一个数据样本,并且只检查它一次。
- 有限的内存使用:面对无限的数据流,必须严格控制内存占用。
- 有限的处理时间:必须能够在新数据生成时快速处理,以免丢失信息。
- 随时可预测:模型必须始终保持就绪状态,能够随时进行预测,这与批量学习有显著区别。
流学习的流程可以可视化如下:模型始终处于就绪状态,从流中接收数据。当收到带标签的数据时,它用其来训练或更新自身内部状态;当需要预测时,它接收数据并返回预测结果,然后继续等待更多数据。
核心算法:从决策树到霍夫丁树 🌳
为了具体说明流学习如何工作,我们以决策树分类器为例。在批量学习中,决策树通过递归归纳构建:查看所有数据,选择最重要的属性进行分割,最终形成树结构。其关键在于需要看到所有数据。
然而,在流学习中,我们无法一次性获得所有数据,只能一次看到一个样本。那么如何构建决策树呢?
解决方案是 VFDT(Very Fast Decision Tree),也称为霍夫丁树。其核心思想是增量式地扩展或分割节点。算法从一个根节点开始,随着数据样本的流入,它会在该节点积累数据。在某个时刻,根据积累的信息决定是否进行分割。
这里的关键技巧在于:如何判断何时积累了足够的数据以做出正确的分割决策?霍夫丁界 为此提供了理论保证。它帮助我们确定进行节点扩展所需的正确样本量,确保在拥有足够统计信心时才进行分割。
霍夫丁树保留了批量决策树的许多优点:低方差、低过拟合。更重要的是,它是渐近接近于批量模型的——当数据量足够大时,流学习模型将非常接近在全部数据上训练的批量模型,而这在流学习场景中(数据量巨大)是常见的。
应对变化:概念漂移与检测 🎯
在动态且不断演化的环境中,数据分布可能随时间改变,这种现象称为概念漂移。我们的目标是在发生分布变化时发出警报。漂移检测的一个重要应用是监测模型性能的变化。
以下是几种常见的漂移类型:
- 突然漂移:从一个概念突然切换到另一个概念。
- 渐进漂移:在过渡期间,两个概念会同时存在并交互。
- 增量漂移:在从一个概念过渡到另一个概念的过程中,存在一些中间概念。
- 重复性漂移:概念发生变化后又回到原来的概念。
在漂移检测中,一个重要挑战是避免将异常值误判为概念漂移。
一个流行的漂移检测方法是 ADWIN(Adaptive Windowing)。它维护一个自适应窗口,该窗口包含两个子窗口。ADWIN 会持续监测这两个子窗口内数据的平均值。随着新数据的到来,它会寻找一个分割点,使得两个子窗口的平均值尽可能相似。如果发现两个子窗口的平均值出现显著差异,就意味着检测到了分布变化,从而触发警报。
这个过程是完全在线的,并且能以对数级的内存和更新时间复杂度运行,这对于需要恒定内存和亚线性处理时间的流学习应用来说非常高效。
流学习方法 vs. 批量方法:一个对比实验 📊
本节我们通过一个实验来展示在特定场景下使用流学习方法的优势。实验使用一个包含三次突然漂移的合成数据集,并比较两种算法:
- XGBoost:一种基于批量学习的集成方法(树的集合)。
- 自适应随机森林:随机森林的流学习版本,也是一种集成方法。
在实验中,批量模型 XGBoost 使用部分数据预先训练,然后部署到数据流上。可以看到:
- 在初始概念与训练数据一致时,XGBoost 表现良好。
- 第一次漂移发生后,性能急剧下降,因为模型无法识别新数据。
- 第二次漂移后,性能有所回升,可能因为新概念与旧概念有相似之处。
- 第三次漂移后,性能再次变差。
另一方面,流学习模型自适应随机森林的表现是:
- 开始时需要学习,但很快达到良好性能。
- 遇到第一次漂移时,性能下降,但能快速恢复。
- 后续遇到漂移时,表现出相同的行为模式。
这个对比凸显了适应性和快速反应能力的重要性。在批量学习模型部署中,一旦出现性能下降,往往需要人工干预并重新触发整个批量学习流程,步骤繁琐。而流学习模型则能自动、持续地适应变化。


流学习模型的评估方法 📈
评估流学习方法时,我们需要考虑一些特殊因素。主要有两种评估方法:
- 留出法:类似于批量学习,保留一个独立的测试集。我们从数据流中存储一部分数据作为测试集,并定期用它对模型进行测试。
- 预检验评估:也称为“测试再训练”法,这是流学习中非常流行的方法。其关键在于顺序很重要:我们依次处理每个样本,先用它进行测试,然后再用它进行训练。这种方法利用了数据流中的所有数据。
实践工具:Scikit-Multiflow 🛠️
为了简化流学习的实验设计和实现,我们引入 Scikit-Multiflow,这是一个用于 Python 数据流的机器学习库。
Scikit-Multiflow 的目标是:
- 易于设计和运行实验。
- 易于扩展现有方法。
- 面向有经验的用户,学习曲线平缓,兼容 Jupyter Notebook。
其当前版本包含多种功能,如数据生成器、流学习方法、变化检测器、评估器等。
以下是使用 Scikit-Multiflow 的两个简单演示:
演示一:分类任务与预检验评估
我们使用一个合成数据流和一个朴素贝叶斯分类器。预检验评估可以通过一个简单的循环实现:
while 有数据且样本数 < 2000:
# 1. 从流中获取一个样本(特征X, 标签y)
# 2. 使用当前模型对X进行预测(测试阶段)
# 3. 将预测结果与真实y比较,计算指标(如准确率)
# 4. 用该样本(X, y)增量训练模型(训练阶段)
Scikit-Multiflow 将这个过程封装成了 PrequentialEvaluator 类,可以方便地比较多个模型(如朴素贝叶斯 vs. SGD分类器)的性能,并动态绘制准确率、运行时间、模型大小等指标。
演示二:概念漂移检测与性能影响
我们生成一个包含三个不同分布阶段的数据流。使用 ADWIN 检测器在循环中处理每个样本,它能成功在分布变化的边界点(第1000和第2000个样本附近)发出警报。
接着,我们比较两种树模型在真实漂移数据集上的表现:
- 霍夫丁树:基础版本。
- 自适应霍夫丁树:改进版本,它在树的节点中使用 ADWIN 来检测局部变化。如果发现某个节点的性能发生变化,它会创建替代分支,并在其表现更好时最终替换原分支。
实验结果显示,自适应霍夫丁树在漂移发生后恢复更快,整体性能更优。有趣的是,它的最终模型内存占用也更小,这表明模型内部结构根据变化进行了动态调整和优化。
总结与要点 ✅
本节课我们一起学习了以下核心内容:
- 流学习是标准批量学习的重要替代方案,尤其适用于数据持续生成且非平稳(即存在概念漂移)的场景。
- Scikit-Multiflow 是一个强大的 Python 库,用于数据流上的机器学习。它的两大优点是易于设计运行实验以及易于扩展。
流学习使我们能够构建出能够持续学习、适应变化并随时提供预测的智能系统,是处理现代动态数据的关键技术。


Scikit-Multiflow 是一个开源项目,可通过 GitHub、PIP、Conda 获取,也提供 Docker 镜像,支持 Linux、Mac OS 和 Windows。我们欢迎社区的贡献和合作。
机器学习模型部署与服务化教程 P7 🚀

在本教程中,我们将学习机器学习模型部署与服务化的核心概念、常见方法及其挑战,并介绍一个名为 Ray Serve 的新框架,它旨在解决现有方案的痛点。
什么是模型服务化?🤔
模型服务化,也称为模型推理或预测服务,是机器学习生命周期中的一个关键阶段。构建和部署机器学习驱动的应用通常包含三个主要步骤:
- 模型开发:收集和分析数据,迭代改进模型。
- 模型训练:将模型投入训练管道,从生产数据中学习。
- 模型推理:将训练好的模型部署到预测服务中,供用户应用调用。
我们本次讨论的重点是最后一个阶段——模型推理与服务化。
模型服务化的核心需求 📋
一个典型的模型服务系统需要满足一系列独特的需求。以下是七个基本要求:
1. 支持多模型复用
在实际应用中,我们通常需要同时服务多种模型。这些模型可能版本不同、算法不同,甚至架构也不同。模型服务器应能处理不同模型间的复用。
2. 支持透明扩展与负载均衡
机器学习模型通常计算密集且耗时。为了避免请求堆积,服务系统应能透明地将模型扩展到多个副本,并通过负载均衡实现请求的并行处理。
3. 支持批处理以提高效率
尽管模型计算量大,但它们可以利用硬件并行性(如向量化指令、多线程)来高效处理批量输入。对一批图像进行推理的成本,通常远低于对单个图像进行多次独立推理。
4. 支持版本管理与无缝部署
在生产中,我们经常需要基于新数据重新训练模型或尝试不同超参数。因此,服务系统需要支持零停机部署、渐进式发布、回滚以及A/B测试等功能。
5. 集成预处理与后处理逻辑
模型通常接收和输出数值张量,但这并非用户期望的格式。输入需要验证和转换,输出需要解释。因此,预处理和后处理逻辑对模型服务至关重要。
6. 优化性能与成本
由于模型计算成本高、吞吐量相对较低,为了处理大量数据,我们需要在成本、性能、延迟和吞吐量之间进行权衡。服务系统应允许调整这些参数,并帮助优化成本效益。
7. 高可用性与可观测性
系统需要具备监控和可观测性工具,确保服务稳定运行,并在出现故障时能够快速恢复。
当前常见的模型服务化方法 🔧
目前主要有两种部署模型的方法,它们各有优劣。
方法一:嵌入到传统Web服务器中
这种方法通常将模型包装在传统的Web服务器(如使用Python的Flask框架)中。
工作原理简述:
- HTTP请求到达Flask服务器。
- 服务器将请求分发到不同的API端点。
- 其中一个端点处理器负责处理模型评估。
- 输入数据被转换后,送入模型进行评估。
- 输出结果被封装并返回。
优点:
- 简单直观:易于构建和理解,适合演示或概念验证。
- 控制力强:开发者对请求处理和模型服务方式有完全的控制权。
缺点与挑战:
- 扩展性差:难以处理并发请求或多个模型同时服务。
- 生命周期管理难:模型通常作为全局变量加载,缺乏对启动、初始化和生命周期的精细控制。
- 稳定性风险:一个模型崩溃可能导致整个Web服务器宕机。
- 内存问题:在多进程部署中,多个模型副本会迅速消耗大量内存。
- 难以构建复杂流水线:现实应用往往涉及预处理、后处理、多模型组合(如集成学习、级联推理)等复杂逻辑,在简单Web服务器中实现这些非常困难。
方法二:卸载到外部专用服务
当简单Web服务器无法满足需求时,人们转向第二种方法:将预测任务卸载到外部专用服务。
工作原理简述:
- Web服务器接收HTTP请求。
- 服务器执行部分预处理(如验证、解析、特定业务逻辑转换)。
- 将处理后的中间数据(如数值张量)通过RPC发送给外部推理服务(如TensorFlow Serving)。
- 外部服务执行核心模型推理。
- 推理结果返回给Web服务器。
- Web服务器执行后处理,并将最终结果封装为HTTP响应。
代表服务: TensorFlow Serving, ONNX Runtime, NVIDIA TensorRT, AWS SageMaker。
优点:
- 关注点分离:将计算密集的推理任务与业务逻辑分离。
- 专业化:专用服务通常针对性能进行了高度优化。
缺点与挑战:
- 操作复杂性高:需要单独管理和调优多个服务。
- 逻辑割裂:模型推理逻辑与核心业务逻辑分离,一方的变更需要同步到另一方,增加了维护成本。
- 学习曲线陡峭:需要学习新的API和定制系统。
- 调试困难:问题排查涉及多个进程和服务,不再是一个简单的单进程应用。
- 配置繁琐:需要处理RPC、外部服务配置、编解码甚至加密等问题。
新一代解决方案:Ray Serve 框架 🎯
为了克服上述方法的缺点,我们开发了 Ray Serve。它是一个基于分布式运行时 Ray 构建的框架,旨在简化机器学习模型服务化。
Ray Serve 的核心优势:
- 轻松扩展:利用Ray系统,可以轻松、透明地将模型扩展到数百个核心。
- 端到端控制:像Web服务器一样,给予开发者对请求处理的完全控制权。
- 独立隔离与扩展:将每个模型作为独立组件进行隔离,并可以独立扩展每个模型。Ray Serve负责负载均衡和管理不同的模型副本。
- 原生流水线支持:允许用户轻松组合多个模型,在Python中构建和连接复杂的处理流水线。
- 可编程性:作为一个可编程的服务系统,允许在运行时修改其行为。
- 开箱即用的监控:提供内置的监控和可观测性工具。
Ray Serve API 简介
定义模型
模型可以通过简单的Python类来定义。在 __init__ 方法中加载模型,在 __call__ 方法中定义请求处理逻辑。Ray Serve暴露了类似Flask的请求接口,易于理解和使用。
import ray
from ray import serve

@serve.deployment
class MyModel:
def __init__(self):
# 在此加载你的模型
self.model = load_your_model()
async def __call__(self, request):
# 获取请求数据
data = await request.json()
# 预处理、推理、后处理
result = self.model.predict(data["input"])
return {"prediction": result}
核心概念:后端与端点
- 后端:与模型实现相关联,是版本化的、可扩展的单元。你可以指定副本数量。
MyModel.deploy() # 创建后端并部署 - 端点:用户可调用的逻辑服务,与HTTP路由关联,并且可以连接到多个后端。端点可以将流量按比例分配到不同的后端(用于A/B测试),或者将请求串联到不同后端(用于流水线处理)。

配置即代码
在Ray Serve中,配置语言就是Python。你使用熟悉的Python代码来配置整个运行系统,而不是复杂的YAML文件。
部署与容错
由于构建在Ray之上,Ray Serve可以部署在从笔记本电脑到Kubernetes集群的任何环境。Ray的分布式运行时确保了系统的高可用性和容错性,组件故障会自动重启和重连。
总结 📝
本节课我们一起学习了机器学习模型服务化的核心内容:
- 模型服务化是ML生命周期中将训练好的模型投入生产使用的关键阶段。
- 服务化系统需满足多模型支持、扩展性、批处理、版本管理、预处理/后处理集成以及成本优化等独特需求。
- 当前两种主流方法各有局限:嵌入Web服务器简单但难以扩展和构建复杂流水线;卸载到外部服务功能强大但引入了操作复杂性和逻辑割裂。
- Ray Serve 作为一个新兴框架,试图融合两者的优点:它提供了类似Web服务器的开发体验和端到端控制能力,同时具备了分布式系统轻松扩展和专业化管理的优势。通过Python API,它让构建、组合和部署生产级模型服务变得更加简单。
你可以通过 pip install ray[serve] 来尝试Ray Serve,并查阅其文档和社区资源以了解更多。

希望本教程能帮助你理解机器学习模型服务化的挑战与解决方案。
机器学习特征选择教程 P8:优化人与机器以推动科学 🧠⚙️
在本教程中,我们将学习机器学习中的特征选择。特征选择是构建高效、准确且易于解释模型的关键步骤。我们将探讨三种主要方法,并了解如何根据具体问题选择合适的方法。
动机:为什么需要特征选择?🤔
机器学习遵循一个核心原则:垃圾进,垃圾出。高性能模型的关键往往不在于复杂的算法,而在于一个精心构建的特征空间。从初始数据集中剔除无关特征,同时保留相关特征,并非易事。目前尚无系统的方法,但如果不进行选择,直接使用大量特征训练模型会导致两个问题:一是计算成本高昂,二是可能导致过拟合,使模型学到虚假的、随机的规律。



特征选择的目标与益处 🎯
特征选择是主动减少特征空间的过程,其思想类似于奥卡姆剃刀原理:简单的解决方案比复杂的更可能是正确的。我们的目标是用比现有更少的特征来寻找数据的表示和关系。
这带来三大益处:
- 模型训练更快,得到更具成本效益的预测器。
- 过拟合风险更低,从而提升预测器性能。
- 模型更易于解释,有助于更好地理解数据中蕴含的关系。
特征选择的两种视角 👓

进行特征选择主要有两种动机:
- 以模型性能为唯一目标:目标是构建一个优秀的预测器,不关心模型如何做出区分。
- 以模型可解释性为目标:目标是基于找到的相关特征获得洞察,例如了解疾病的成因。


这两种方法并非必须取舍。有些特征选择方法既能保证良好的准确性,又具有可解释性,并能帮助生成关于目标与特征关系的假设。本教程将聚焦于这类方法。

特征选择的三种主要方法 🗂️
上一节我们介绍了特征选择的动机和目标,本节中我们来看看实现这些目标的具体方法。主要有三种类型的特征选择方法:
1. 过滤法 (Filter Methods) 🧹
过滤法在不进行任何学习的情况下,直接从数据中提取特征。它通常作为数据预处理步骤,其核心思想是为每个特征分配一个启发式分数,以过滤掉无用的特征。驱动这些方法的问题是:这个特征是否包含足够的信息?
以下是过滤法的两种主要类别:
- 单变量方法:根据某些标准(如方差)对单个特征进行排名,然后选择排名前 N 的特征。这种方法擅长移除恒定或近似恒定的特征,但可能留下冗余变量,因为它不考虑特征之间的相互关系。
- 多变量方法:考察变量之间的相互关系,因此擅长移除重复的、高度相关的特征。衡量相关性的方法有很多,例如皮尔逊相关系数(衡量线性关系强度)或斯皮尔曼相关系数(衡量单调关联程度)。

何时使用:过滤法适合作为高效的初始步骤,快速过滤数据,特别是在算法运行时间是瓶颈时。由于这些方法独立于模型,因此选出的特征可用于任何机器学习模型或其他特征选择方法。
代码示例:
# 单变量过滤:选择K个最佳特征(基于互信息)
from sklearn.feature_selection import SelectKBest, mutual_info_regression
selector = SelectKBest(score_func=mutual_info_regression, k=10)
X_new = selector.fit_transform(X, y)

# 多变量过滤:基于皮尔逊相关系数移除高相关特征
import pandas as pd
corr_matrix = X.corr().abs()
upper_tri = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
to_drop = [column for column in upper_tri.columns if any(upper_tri[column] > 0.95)]
X_reduced = X.drop(to_drop, axis=1)
2. 包装法 (Wrapper Methods) 🎁
包装法基于给定算法的性能质量来评估每个特征子集,旨在找到最佳的可能特征子集。由于它们能检测变量间的交互作用并直接优化预测性能,因此在特征选择上往往表现更好,但随着特征空间增长,计算成本会变得非常高昂。


以下是两种常见的包装法策略:
- 前向选择:从零特征开始,每次迭代添加一个最能改进模型的特征,直到添加特征不再提升性能为止。
- 后向消除:从所有特征开始,每次迭代移除最不重要的特征,直到移除特征不再提升性能为止。
局限性:除了计算成本高,前向选择可能添加一个初始有用但加入其他特征后变得无用的特征;后向消除则可能发生相反的情况。Scikit-learn 没有直接提供前向选择或后向消除算法,但提供了类似的递归特征消除算法。

3. 嵌入法 (Embedded Methods) 🔧
嵌入法的动机是:能否将特征选择过程作为模型训练过程本身的一部分? 这些方法不将学习与特征选择分开,而是在训练模型的同时进行特征选择和特征排名。
优势:
- 类似包装法,会考虑特征间的交互作用。
- 相对较快(至少比包装法快)。
- 通常比单纯的过滤法更准确,且不易过拟合。
注意:由于它们依赖于特定分类器或回归器进行选择,因此产生的特征子集如果更换模型可能不再有效。基于树的算法(如随机森林)是嵌入法的典型例子,它们会迭代地丢弃重要性最低的数据部分。
代码示例:
from sklearn.ensemble import RandomForestRegressor
rf = RandomForestRegressor()
rf.fit(X_train, y_train)
# 获取特征重要性排名
importances = rf.feature_importances_
混合方法与递归特征消除 (RFE) 🥗

了解了三种基本方法后,我们可以将它们组合成混合方法,以获取最佳的特征子集。组合方式由工程师决定,例如可以先使用过滤法消除恒定和重复特征,然后对剩余特征使用包装法。混合方法的优势在于取长补短,从而可能实现更高的性能、更低的计算成本和更稳健的模型。
递归特征消除 (RFE) 是一个典型的混合方法示例。其工作流程如下:
- 在所有特征上训练模型并评估其性能。
- 获取特征重要性,删除最不重要的特征,在剩余特征上重新训练模型。
- 使用评估指标计算新模型的性能,测试指标下降是否超过某个阈值。如果下降未超过阈值,则认为该特征不重要,可以移除。
- 算法持续删除最不重要的特征,直到所有特征被移除。特征的排名与其被消除的顺序相反。
RFE 看起来有点像后向消除,但主要区别在于:后向消除首先消除所有特征以确定重要性,而 RFE 则从机器学习模型推导出的重要性中获取信息。

如何选择特征选择方法?❓
现在我们已经了解了所有可用的方法,你可能会问:如何选择? 最终,没有“最佳”的特征选择方法,你必须尝试不同的模型和不同的特征子集,以找出最适合你数据的方法。
以下因素可以帮助缩小搜索范围:
- 模型拟合过程的计算成本:如果成本高,应避免计算密集的包装法。
- 你试图学习的模型的复杂度:如果关系非常复杂(非线性),简单的过滤或目标相关性分析可能无法捕捉。
- 你的回归或分类方法是否允许嵌入式特征选择:如果不允许,则无法使用嵌入法。

最终,这仍将归结为尝试不同的算法,找到最适合你和你的数据的那一个。
实战案例:从原料到功能 (Feedstock to Function) 🔬
为了演示一个完整的工作流程,我们来看一个名为“从原料到功能”的项目。该项目旨在生物衍生航空燃料开发的早期阶段预测其特性。我们试图预测的两个特性是沸点和产烟指数。

特征:我们使用分子描述符,即对分子化学结构进行数学编码的方式(如碳原子数、环数等)。初始通过 Mordred 软件生成了 1800 个描述符,我们手动将其筛选至 821 个(沸点)和 940 个(产烟指数)。
目标:由于我们的目标包括生成关于分子结构与这些特性之间潜在关系的假设,因此我们在整个过程中都注重保持可解释性。

工作流程:
- 手动检查:删除与项目范围明显不符的描述符(如用于制药应用的)。
- 数据预处理与过滤:
- 移除对所有化合物具有相同值的描述符。
- 移除相关系数高于某个阈值的描述符(消除高度共线性)。
- 尝试多种方法进行精细筛选(针对剩余的 500-700 个描述符):
- 尝试了后向消除和 RFE,但计算成本太高。
- 转而使用更严格的相关性阈值分析(皮尔逊和斯皮尔曼)。
- 尝试了随机森林特征重要性、LASSO 回归,最后再次尝试 RFE(此时特征已减少,计算可行)。
- 确定最佳特征数:绘制特征数量与模型性能的关系图,观察随着添加排名较低的特征,性能如何变化。这帮助我们决定最终纳入特征集的特征数量。
结果:
- 沸点:描述符从 821 个减少到约 300 个,准确率有小幅提升。虽然提升不大,但意义重大,因为我们需要分析的关系维度大大降低。
- 产烟指数:描述符从 940 个锐减到 7 个,同时准确率大幅提升。这已经为我们理解驱动产烟指数估计的因素提供了重要见解。
总结 📝


本节课中,我们一起学习了机器学习中的特征选择。
- 我们探讨了三种主要方法:过滤法、包装法和嵌入法,以及结合它们优点的混合方法。
- 我们了解到,在准确性和可解释性之间不一定需要取舍。通过案例看到,我们可以将超过 900 个特征减少到 7 个,同时提高准确性并更好地解释模型。
- 记住,你的数据可以暗示哪种特征选择方法可能最有效。同时,考虑计算成本、试图建模的关系复杂度以及所使用的模型类型,也能为你优化特征选择搜索提供线索。
- 尽管你的数据或模型可能与我们示例中的不同,但这个案例可以指导你如何处理类似的特征选择问题。优化不仅在于机器本身,也在于作为机器学习工程师的你,从而更好地推动你所从事的领域前进。
课程 P9:Pandera - Pandas DataFrame 的统计式数据验证 🧪
在本课程中,我们将学习如何使用 Pandera 库对 Pandas DataFrame 进行统计式数据验证。我们将探讨数据验证的理论基础、Pandera 的核心概念,并通过一个真实世界的数据集案例来演示其应用。
概述
数据验证是数据科学和机器学习工作流中至关重要的一环。它确保数据在处理和分析前满足特定的假设和质量要求。Pandas 是 Python 中处理表格数据的标准工具,而 Pandera 则是一个专门为 Pandas DataFrame 设计的数据验证库。
数据验证的核心是定义一个验证函数 V,它接收数据 X 作为输入,并输出 True 或 False。为了使验证有意义,函数 V 必须是满射的,即至少存在一个 X 值映射到 True,另一个映射到 False。
公式:V: X -> {True, False},且 ∃ x1, x2 ∈ X 使得 V(x1) = True 和 V(x2) = False。

为什么需要数据验证?

上一节我们介绍了数据验证的基本概念,本节中我们来看看为什么在实际工作中需要它。
数据验证有以下几个实际好处:
- 提高可调试性:复杂的数据处理管道难以推理和调试。明确的验证规则可以帮助快速定位问题。
- 保障数据质量:高质量的数据对于支持业务决策、科学发现和生产环境中的预测至关重要。
- 提升代码可维护性:清晰的验证规则可以作为代码的文档,帮助未来的你或其他开发者理解数据管道的预期行为。
数据验证的类型
了解了数据验证的必要性后,我们可以从不同角度对验证规则进行分类。

一种分类方式是将其分为技术性检查和领域特定检查。
- 技术性检查:关注数据结构本身的属性。例如,检查“收入”列是否为数值型。
- 领域特定检查:关注所研究主题的特定属性。例如,检查“年龄”变量是否在 0 到 120 岁之间。
另一种分类方式是从统计学的角度,分为确定性检查和概率性检查。
- 确定性检查:表达硬编码的逻辑规则。例如,
mean(age) > 30。 - 概率性检查:明确地结合了随机性和分布变异性。例如,
mean(age)的 95% 置信区间在 30 到 40 岁之间。
这引出了统计类型安全的概念,它类似于逻辑类型安全,但应用于数据的分布属性,以确保对这些数据进行的统计操作是有效的。例如,检查训练样本是否独立同分布,或检查训练集和测试集是否来自同一分布。
Pandera 简介
现在,我们来看看 Pandera 如何将上述理论付诸实践。Pandera 是一个“契约式设计”的数据验证库,它提供了一个直观的 API 来表达 DataFrame 的模式(Schema)。
其核心设计是模式函数 S,它接收一个验证函数和数据作为输入,如果验证通过则返回数据本身,否则抛出一个异常。
代码示例:
import pandera as pa
# 定义一个简单的模式
schema = pa.DataFrameSchema({
“column_a”: pa.Column(int, checks=pa.Check.gt(0)),
})
# 验证数据
valid_data = pd.DataFrame({“column_a”: [1, 2, 3]})
schema.validate(valid_data) # 返回 valid_data
# 无效数据会抛出异常
invalid_data = pd.DataFrame({“column_a”: [-1, 0, 1]})
# schema.validate(invalid_data) # 会抛出 SchemaError

这种设计支持组合性。对于一个数据处理函数 F,我们可以用多种方式将模式验证组合进去:在 F 处理前验证原始数据,验证 F 的输出,或者在处理前后都进行验证(“数据验证三明治”模式)。


案例研究:分析“致命遭遇”数据集

理论部分介绍完毕,让我们通过一个真实案例来具体学习 Pandera 的应用。我们将使用“致命遭遇”(Fatal Encounters)数据集,这是一个记录美国执法过程中导致死亡的遭遇的数据库。

本分析的核心问题是:哪些因素最能预测法院将案件裁定为“意外”?

以下是分析的主要步骤:


1. 数据读取与初步探索
首先,我们读取数据并查看其结构。

代码示例:
import pandas as pd
url = “https://raw.githubusercontent.com/.../fatal_encounters.csv”
df_raw = pd.read_csv(url)
print(df_raw.head())
2. 定义基础模式并清理列名


在进一步分析前,我们先清理列名,并定义一个最小化的模式来确保我们关心的列存在。
代码示例:
import pandera as pa

# 定义我们感兴趣的列
minimal_schema = pa.DataFrameSchema({
“Dispositions/Exclusions”: pa.Column(str, nullable=False),
“Age”: pa.Column(str),
“Gender”: pa.Column(str),
“Race”: pa.Column(str),
“Cause of death”: pa.Column(str),
})

def clean_columns(df: pd.DataFrame) -> pd.DataFrame:
df_clean = (df
.rename(columns=lambda x: x.strip().replace(“ “, “_”).replace(“/”, “_”))
.pipe(minimal_schema.validate) # 在方法链末尾验证
)
return df_clean

3. 探索数据并定义详细模式
使用 pandas-profiling 等工具进行探索性数据分析后,我们可以定义一个更丰富的训练数据模式。
代码示例:
training_schema = pa.DataFrameSchema({
“age”: pa.Column(float, checks=pa.Check.in_range(0, 120), coerce=True),
“gender”: pa.Column(str, checks=pa.Check.isin([“Male”, “Female”, “Unknown”])),
“race”: pa.Column(str, checks=pa.Check.isin([“White”, “Black”, “Hispanic”, …])),
“cause_of_death”: pa.Column(str, checks=pa.Check.isin([“Gunshot”, “Vehicle”, …])),
“dispositions_exclusions”: pa.Column(str, nullable=False),
})
# 可以将模式保存为 YAML 以便检查
training_schema.to_yaml(“training_schema.yaml”)

4. 数据清洗与“验证三明治”
接下来,我们清洗数据(如规范化字符串、转换类型、派生目标变量)。我们使用 check_input 和 check_output 装饰器来确保清洗函数的输入和输出符合模式,形成“验证三明治”。
代码示例:
from pandera import check_input, check_output


@check_input(training_schema)
@check_output(training_schema.remove_columns([“dispositions_exclusions”]))
def clean_data(df: pd.DataFrame) -> pd.DataFrame:
# … 数据清洗逻辑,例如:
# - 清理字符串
# - 规范化年龄(将“25 years”转为 25.0)
# - 从 dispositions_exclusions 列派生目标列 `disposition_accidental`
# - 过滤掉“未报告”、“未知”、“待定”或“自杀”的记录
return df_cleaned
5. 数据拆分与模型构建


将数据拆分为训练集和测试集,并构建一个机器学习模型(例如随机森林分类器)。Pandera 的 SeriesSchema 和模式修改方法(如 remove_columns)在这里非常有用。
代码示例:
target_schema = pa.SeriesSchema(int, checks=pa.Check.isin([0, 1]))
feature_schema = training_schema.remove_columns([“dispositions_exclusions”, “disposition_accidental”])

@check_output((feature_schema, target_schema, feature_schema, target_schema), [0, 1, 2, 3])
def split_training_data(df):
from sklearn.model_selection import train_test_split
X = df.drop(“disposition_accidental”, axis=1)
y = df[“disposition_accidental”]
return train_test_split(X, y, test_size=0.2)
我们甚至可以利用定义好的 feature_schema 来帮助构建 sklearn 的 ColumnTransformer。

6. 模型评估与解释
训练模型后,我们使用 SHAP 值来解释哪些特征对预测“意外”裁定最重要。然后,我们可以基于这些解释创建模型审计模式,以验证模型行为是否符合我们的假设(例如,通过 t 检验验证某个二元特征是否显著影响 SHAP 值)。
代码示例:
# 创建模型审计模式(示例)
model_audit_schema = pa.DataFrameSchema({
“feature_gunshot”: pa.Column(float, checks=pa.Check.less_than(0)), # 假设枪击导致更低概率
“feature_vehicle”: pa.Column(float, checks=pa.Check.greater_than(0)), # 假设车辆导致更高概率
})
# 对模型解释结果进行验证
# model_audit_schema.validate(shap_values_df)

Pandera 的高级功能与未来展望
在案例中我们体验了 Pandera 的核心功能,本节简要介绍一些实验性功能和未来规划。

- 模式推断:可以从一个现有的 DataFrame 自动推断出一个模式。
inferred_schema = pa.infer_schema(df) - 模式序列化:可以将模式写入 YAML 文件或 Python 脚本,便于共享和版本控制。
- 未来方向:计划开发面向机器学习领域的模式,能够区分目标变量和特征变量,并可能用于生成测试数据。
总结与要点
本节课中,我们一起学习了使用 Pandera 进行 Pandas DataFrame 统计式数据验证的全过程。
以下是四个关键要点:
- 数据验证是达到多种目的的手段:包括可重复性、可读性、可维护性和统计类型安全。
- 数据验证是一个迭代过程:需要在数据探索、领域知识获取和编写验证代码之间不断循环。
- Pandera 模式是可执行的契约:它们在运行时强制执行 DataFrame 的统计属性,并能灵活地与数据处理和分析逻辑交织在一起。
- Pandera 不是完全自动化的:用户仍需负责识别管道中需要测试的关键部分,并定义数据被视为有效的契约条件。

通过将明确的验证规则融入你的数据工作流,你可以更快地调试问题,更自信地构建模型,并创建出更健壮、更易于协作的代码。


浙公网安备 33010602011771号