如何衡量您的基于 RAG 的 LLM 系统的成功
使用机器来评估机器
包括一种新的定性评分方法和详细解释。
·
关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 10 月 23 日
–
由 Stable Diffusion XL 生成的图像
增强生成研究,或 RAG,是今年出现的大型语言模型(LLMs)最常见的使用案例。尽管文本摘要和生成通常是个别用户的重点,但企业已经意识到他们需要利用自己的数据来利用这项技术。回顾我仍然使用 LLM 的情况,文本生成位居高位。我想问 Bard 问题并让它搜索网络;我希望 Claude 重写电子邮件或博客文章,以增强我的内容。但我遇到的最令人兴奋的使用是将我自己的数据输入 LLM。我想搜索我的笔记、电子邮件、日历和 Slack 消息,并让 Llama 充当另一个我(但这个我能记住今天之前发生的细节)。
这不是关于如何构建 RAG 的帖子(已经有很多这样的帖子了……我还在为其他时间写这篇帖子)。今天我们将探讨的是如何评估 RAG 系统。
我们如何从 RAG 中获取数据?
在深入细节之前,我们先设定一个基础。当我们谈论 RAG 时,我们指的是系统的两个部分。
知识源
知识源可以是向量数据库、搜索引擎、加载到内存中的几个文本文件、SQL 数据,或任何存储我们数据的地方。
LLM
一旦我们有了数据,我们将其输入到 LLM 中。这是通过上下文窗口完成的。因此,最终,我们进行搜索,得到一些文本,将找到的文本放入提示中,然后将问题传递给 LLM。模型随后从该上下文窗口中获取所有内容并提供答案。
为什么这很重要?
当我们谈论评估 RAG 系统时,我们必须知道在定义如何评估之前我们要评估什么。我们现在可以看到需要检查两个部分。初始数据检索是最关键的部分。LLM 通常在总结/回答问题方面表现出色,前提是提供了上下文数据。可能缺乏的部分是搜索功能本身。
这些知识源有一些内建的限制。例如,当使用向量数据库存储大型文本文件时,你必须对数据进行‘切块’处理。这是什么意思?假设你有一个 100 页的文档,但数据库一次只能处理 1 页。加载文档并进行搜索时,数据库只能逐页检查(好吧,这有点简化,但请耐心一点;这对于实际应用已经足够接近了)。当我们找到匹配的搜索结果时,确实有可能整个问题的答案不在这一页上。真遗憾!我们只能得到一页!这很好地说明了在担心 LLM 的输出之前需要检查系统这一部分的原因。
我们需要评估什么?
评估初始搜索
这可能不是大多数技术专家想听到的答案。评估结果需要一定程度的人为评估。
为什么?如果一个企业使用的是私有数据,自动化测试来验证搜索结果是否完全准确将很困难。不要担心,这不必完全手动;我们可以自动化其中的一部分。让我们深入了解一下。
我看到有两种实现方式用于初步验证和评估。
第一个选项是为数据集设置一组常见的预期问题,并让人工 QA 团队验证搜索结果。例如,如果你的团队负责为银行构建一个客服问答机器人,一些常见问题可能是:“我必须在账户中保持的最低金额是多少?”,“我怎么还款?”,“我的分行几点开门?”。如果你的 QA 能够提供问题和预期答案,并以类似 CSV 文件的形式提供,这样可以程序化读取,那就最好不过了;然后,我们可以使用一些自动化测试,稍后会在本文中进一步介绍。
如果没有时间或资源,第二种方法是 QA 团队实时搜索和审查。这是早期 POC 和原型的一个选项,但请注意,这不能扩展到实际生产工作负载中。
评估 LLM 响应
一旦我们对知识来源的数据的可靠性感到放心,我们必须确保最终答案的准确性。RAG 系统在减少幻觉的可能性方面表现出色,这可以通过调整底层提示来进一步延伸。然而,它可能会遗漏信息、误解输入的数据,或试图引入训练中的先验知识。
评估这一步骤类似于评估之前的搜索。如果 QA 团队可以提供问题和预期答案,我们可以尝试程序化地评估答案。
现在让我们看看其中的一些选项。
评估框架
需要记住的是,LLM 和 RAG 在其成熟周期中还处于早期阶段。自 ChatGPT 首次亮相以来仅过去了一年,每天都有更多的进展、模型、框架和研究。尽管如此,一些指标正成为衡量这些系统性能的标准方式。
我们不会涵盖评估基础 LLM 的方法。像 ARC、MMLU、HellaSwag 等都针对底层语言模型。你不需要自己运行这些指标;可以查看像这样的站点
llm-leaderboard.streamlit.app/
和 huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard
查看不同模型的表现。我们只对从 RAG 系统中得到的结果感兴趣。
这引导我们关注像 ROUGE、BLEU、BLEURT 和 METEOR 这样的算法。让我们逐一详细了解每个指标。我还会附上一小段代码,展示如何调用每个指标以及输出分数的样子。我引入评估框架以便开始,并包括了我想要评分的参考和答案。
!pip install evaluate --quiet
!pip install rouge_score --quiet
!pip install importlib-metadata --quiet
!pip install datasets==2.10.1 --quiet
!pip install git+https://github.com/google-research/bleurt.git --quiet
!pip install sacrebleu --quiet
!pip --no-cache-dir install bert_score==0.3.9 --quiet
!pip install sacremoses --quiet
!pip install jiwer==2.5.1 --quiet
!pip install Cython
import evaluate
# If you have a translation and reference corpus:
predictions = ["In Dungeons & Dragons, the metallic dragons come in brass, bronze, copper, gold, and silver varieties. Each has scales in hues matching their name - brass dragons have brassy scales, bronze have bronze scales, etc. The metallic dragons are generally more benign than the chromatic dragons in D&D lore."]
references =["""The five basic chromatic dragons (red, blue, green, black, and white) and metallic dragons (copper, brass, silver, gold, and bronze) appeared in the fifth edition Monster Manual (2014) in wyrmling, young, adult, and ancient. Gem dragons and other new-to-fifth-edition dragons appeared in Fizban's Treasury of Dragons (2021)"""]
ROUGE(召回导向的摘要评估指标)
ROUGE 是一组用于评估自动摘要和机器翻译输出的指标。它基于系统输出与参考摘要之间的重叠 n-gram 的计数。
#predictions (list): list of predictions to score. Each prediction should be a string with tokens separated by spaces.
#references (list or list[list]): list of reference for each prediction or a list of several references per prediction. Each reference should be a string with tokens separated by spaces.
#rouge_types (list): A list of rouge types to calculate. Defaults to ['rouge1', 'rouge2', 'rougeL', 'rougeLsum'].
#Valid rouge types:
##"rouge1": unigram (1-gram) based scoring
##"rouge2": bigram (2-gram) based scoring
##"rougeL": Longest common subsequence based scoring.
##"rougeLSum": splits text using "\n"
#use_aggregator (boolean): If True, returns aggregates. Defaults to True.
#use_stemmer (boolean): If True, uses Porter stemmer to strip word suffixes. Defaults to False.
rouge = evaluate.load('rouge')
results = rouge.compute(predictions=predictions, references=references, use_aggregator=False)
print(results)
{
'rouge1': [0.3636363636363636],
'rouge2': [0.06185567010309278],
'rougeL': [0.22222222222222224],
'rougeLsum': [0.22222222222222224]
}
BLEU(双语评估指标)
BLEU 是一种用于自动评估机器翻译输出的指标。它基于候选翻译与一组参考翻译的 n-gram 精度。
#predictions (list of strs): Translations to score.
#references (list of lists of strs): references for each translation.
#max_order (int): Maximum n-gram order to use when computing BLEU score. Defaults to 4.
#smooth (boolean): Whether or not to apply Lin et al. 2004 smoothing. Defaults to False.
#bleu (float): bleu score
#precisions (list of floats): geometric mean of n-gram precisions,
#brevity_penalty (float): brevity penalty,
#length_ratio (float): ratio of lengths,
#translation_length (int): translation_length,
#reference_length (int): reference_length
bleu = evaluate.load(“bleu”)
results = bleu.compute(predictions=predictions, references=references,max_order=4)
print(results)
{
'bleu': 0.07342349837092484,
'precisions': [0.4262295081967213, 0.11666666666666667, 0.03389830508474576, 0.017241379310344827],
'brevity_penalty': 1.0,
'length_ratio': 20.333333333333332,
'translation_length': 61,
'reference_length': 3
}
BLEURT(基于变换器的 BLEU 回归)
BLEURT 是一种自然语言生成(NLG)的评估指标。它基于 BERT,使得 BLEURT 能够学习词汇和短语之间的统计关系,并识别 NLG 输出中的模式。
BLEURT 已被证明在各种任务上优于其他自然语言生成(NLG)评估指标,如 BLEU 和 ROUGE,包括机器翻译、文本摘要和问答系统。
#output is always a number between 0 and (approximately 1).
#This value indicates how similar the generated text is to the reference texts, with values closer to 1 representing more similar texts.
bleurt = evaluate.load("bleurt", module_type="metric")
results = bleurt.compute(predictions=predictions, references=references)
print(results)
{
'scores': [0.6028875708580017]
}
METEOR(显式排序翻译评估指标)
METEOR 是一种用于机器翻译输出的自动评估指标。它还具有其他指标所没有的特性,如词干提取、同义词匹配和标准的精确词汇匹配。该指标旨在解决更流行的 BLEU 指标遇到的一些问题,并在句子或片段级别上与人工判断产生良好的相关性。
#predictions: a list of predictions to score. Each prediction should be a string with tokens separated by spaces.
#references: a list of references (in the case of one reference per prediction), or a list of lists of references (in the case of multiple references per prediction. Each reference should be a string with tokens separated by spaces.
#alpha: Parameter for controlling relative weights of precision and recall. The default value is 0.9.
#beta: Parameter for controlling shape of penalty as a function of fragmentation. The default value is 3.
#gamma: The relative weight assigned to fragmentation penalty. The default is 0.5.
#outputs 0-1 - .317 is acceptable score
meteor = evaluate.load('meteor')
results = meteor.compute(predictions=predictions, references=references)
print(results)
{
'meteor': 0.19316493313521543
}
我被承诺会有新东西!
虽然我已经引起了你的注意,但我想介绍一个新的想法。虽然这四种算法会给出一个量化的分数,让你的 QA 团队能够快速确定答案/摘要是否相似,但仍存在一些不足之处。
首先,参考句子和结果可能足够相似以回答用户的问题,但它仍然可能获得较低的分数。运行一组已知的问题和答案以建立良好的基准并将未来的答案与该基准进行比较是至关重要的。
其次,它无法告诉你分数为何受损。是因为重复词汇会受到惩罚吗?还是因为缺少了一些词汇?摘要是否完全遗漏了答案中的重要部分?没有办法得知。
最后,仅仅因为一个回答获得低分并不一定意味着人类会认为这个回答不足或错误。基准在这里可能有帮助,用于确定什么是可接受的评分,但在使用这些评分来判断 RAG 答案时,保持一些怀疑态度是重要的。
LLMs 评分 LLMs
BLEURT 向我们介绍了我们可以以某种方式使用 LLMs 来评估 RAG 系统的答案。如果我们直接利用这一点呢?我们指示 LLM 给我们的答案一个定性评分,并提供列点的理由和评分的叙述解释。这让我们兼得两全其美的效果。我们可以提取一个数值评分来报告给用户和 QA;我们也可以提供更多细节来解释为什么答案得分低。
这里是一个可以用于 ClaudeV2 的示例提示模板。我们传递关于如何希望模型评分我们答案的指令,传递参考数据,并传递我们从 RAG 系统收到的答案。
prompt_data = """
Human:
You are to rate a summarization on the scale of 0-10, with 0 being completely incorrect and 10 being a perfect summzarization of the given text.
Explain why you give your score.
Give a bullet point list of major differences between the reference and the summary.
I will supply a reference text by denoting REF: and the summarization to compare against with SUMMARY:.
REF:
The five basic chromatic dragons (red, blue, green, black, and white)
and metallic dragons (copper, brass, silver, gold, and bronze)
appeared in the fifth edition Monster Manual (2014) in wyrmling, young, adult, and ancient.
Gem dragons and other new-to-fifth-edition dragons appeared in Fizban's Treasury of Dragons (2021)
SUMMARY:
In Dungeons & Dragons, the metallic dragons come in brass, bronze, copper, gold, and silver varieties.
Each has scales in hues matching their name - brass dragons have brassy scales, bronze have bronze scales, etc.
The metallic dragons are generally more benign than the chromatic dragons in D&D lore.
Assistant:"""
Score: 5/10
Explanation:
The summary captures some accurate high-level information about metallic dragons in D&D, such as listing the main types and noting their more benign nature compared to chromatic dragons. However, it leaves out a lot of key details from the reference text:
- Does not mention the chromatic dragons (red, blue, green, black, white) that were described.
- Does not mention the different age categories (wyrmling, young, adult, ancient).
- Does not mention the Monster Manual or Fizban's Treasury of Dragons sourcebooks.
- Only focuses on metallic dragons, while the reference text had information about both metallic and chromatic dragons.
Major differences:
- No mention of chromatic dragons
- No mention of dragon age categories
- No mention of Monster Manual or Fizban's Treasury of Dragons
- Only discusses metallic dragons, not full scope of reference text
- Does not convey the introduction timeline of dragons across 5th edition books
就这样。如果团队能提供预期的答案,我们可以将 RAG 答案反馈到 LLM 中进行评分。好处是我们不必依赖 LLM 的先验知识,因为我们仍在传输相关数据。我们可以使用与 RAG 系统不同的 LLM,这意味着我们甚至可以让多个模型来评分我们的输出,以确保评估的平衡性。
这种方法还给我们提供了出错的优秀解释。在这个例子中,我问了关于 DND 宇宙中存在哪些龙的问题。评判 LLM 正确识别出它没有提到色彩龙。然而,它还因为没有包括龙的年代、DND 怪物手册或扩展冒险而扣分。这些遗漏对我提出的问题并不重要,但这允许 QA 团队自行决定一次。
接下来我们怎么做?
基于 RAG 的系统及其创建框架每天都在进步。评分它们的新方法和机制也会不断进步。甚至有来自像 LangChain 这样的巨头的工具可以帮助完成这个任务,例如LangSmith。
在我们等待更多进展的同时,结合一些手动验证数据和 HuggingFace 度量库或 LLMs 本身,给我们提供了一个很好的开始信任这些系统的方法。
记住,一旦你对你的新 RAG 充满信心并准备投入生产,答案的评估不会停止!作为常规监控和审计工作的一部分,你必须继续存储问题和答案,并计划进行人工评估工作,以评分和标记提供给最终用户的答案。然而,这个话题另说。
如何使用 Python 和 Vertex AI Pipelines 测量碳足迹
原文:
towardsdatascience.com/how-to-mesure-the-carbon-footprint-using-vertex-ai-pipelines-3d6bc9695e7b
关于使用 Vertex AI Pipelines 跟踪碳排放的逐步指南
·发表于 Towards Data Science ·9 分钟阅读·2023 年 1 月 31 日
–
图片由作者使用 Midjourney 生成。
动机
机器学习已经成为我们日常生活的一部分,因此是时候考虑它对环境的潜在影响了。否则,大自然可能会以自然灾害的形式给我们一个‘我早就说过了’的教训,导致严重的人类痛苦。我们可以通过开始测量和减少机器学习模型的碳足迹来帮助应对气候变化。碳足迹衡量的是服务、产品、个人、组织或事件造成的温室气体排放总量。在机器学习的情况下,它包括训练和运行模型所需的能源,以及运行这些模型的硬件所用的能源。
在这篇文章中,我将对两个开源 Python 库 CodeCarbon 和 CarbonTracker 提供反馈,它们能够估算碳足迹。我还将包括一个在 Vertex AI 管道中使用它们的逐步指南。最后,我将列出减少碳足迹的实际考虑因素。所以,让我们在为时已晚之前开始为拯救地球贡献自己的力量吧!💚
I. Python 中的碳足迹 📗
用于测量 Python 中碳足迹的两个最流行的库是 CodeCarbon 和 CarbonTracker。事实是我们没有很多开源的替代方案。但我相信,一旦社区开始将碳足迹集成到机器学习系统中,我们将会有更多的选择。
让我们先说几句关于库的内容。
CodeCarbon
它是一个开源 Python 库,用于估算运行代码时产生的 CO2。该项目由 Yoshua Bengio 发起。我最欣赏的一点是它非常易于使用,文档良好,并且有一个很棒的仪表盘。估算通过测量总 GPU、CPU 和 RAM 的功耗来完成。然后,它应用您的云提供商或国家的区域碳强度,如果您使用本地计算机或本地集群。请参考下表以查看各种能源来源的碳强度。
@CodeCarbon 来源
二氧化碳排放估算(CO₂eq)计算如下:
CO₂eq=Power_consumption(kilowatt-hours)*Carbon_Intensity(kg of CO₂/kilowatt-hour)
请注意,当碳强度不可用时,CodeCarbon 使用世界平均值475 gCO2.eq/KWh。排放量被保存到名为emissions.csv.
的 CSV 文件中。
在支持的基础设施方面,它兼容支持NVIDIA 管理库(NVML)的 NVIDIA GPU 和支持Intel RAPL的 Intel CPU。如果您的 CPU 不在支持的 CPU 列表上,它将估算 CPU 的功耗为其热设计功耗(TDP)的 50%,默认 TDP 平均值为 85W。
使用 pip 安装:
pip install codecarbon
它支持两种模式:在线模式
或 离线模式
。
在线模式
需要互联网连接来获取您的地理位置。请参见下面使用它的示例,带有或不带装饰器:
离线模式
可以在您的设置无法访问互联网时使用。它需要指定国家的 3 个字母 ISO 代码。您可以在维基百科上找到国家ISO代码的列表。
CarbonTracker
CarbonTracker 是一个开源库,旨在通过测量训练所用硬件的功耗来估算训练深度学习模型的碳足迹。目前,它支持 GPU、CPU 和 DRAM 组件。它与支持NVIDIA 管理库 (NVML)的 NVIDIA GPU、支持Intel RAP的 Intel CPU、Slurm 和 Google Colab / Jupyter Notebook 兼容。使用起来很简单,但不幸的是,文档有限。
为了估算碳足迹,它使用以下公式:
Carbon Footprint = Energy Consumption × Carbon Intensity
Energy Consumption
是基于 PUE(功耗使用效率)计算的,这是一种用于衡量数据中心能源效率的指标。它通过将数据中心使用的总能量除以 IT 设备(如服务器、存储等)使用的能量来计算。
它使用与 codecarbon
相同的每个云或国家的 Carbon Intensity
。当碳强度不可用时,应用475 gCO2.eq/KWh的全球平均值。
可以通过 pip 安装:
pip install carbontracker
使用方法如下面的示例所示:
它还具备在指定目录中收集和存储日志的能力:
from carbontracker import parser
logs = parser.parse_all_logs(log_dir="./"+YOUR_DIR+"/")
现在我们对 CodeCarbon 和 CarbonTracker 有了一定了解,接下来我们将在 Vertex AI Pipeline 中使用它们。
II. 使用 Vertex AI Pipelines 的案例研究👷
有趣的部分现在开始 😄
在继续 Vertex AI Pipelines 之前,我邀请你阅读我的文章,该文章展示了如何使用 Vertex AI Pipelines。
接下来,我将演示如何在两种场景中跟踪碳足迹:
-
1. 仅使用 CodeCarbon 进行的监督学习(CarbonTracker 仅支持深度学习)。
-
2. 使用 CodeCarbon 和 CarbonTracker 进行深度学习。
1. CPU 上监督学习的碳足迹
我们将跟踪训练随机森林算法以**“预测葡萄酒质量”**时的碳排放。数据从UCI 机器学习库下载,@source [Cortez 等, 2009]。有关数据集的更多细节,请查看我的文章中的用例部分。笔记本可在GitHub上获取。
为了测量训练期间的碳排放,我们需要按如下方式修改笔记本中的 train_winequality
自定义 Kubeflow 组件:
-
将
codecarbon
添加到packages_to_install
列表中。 -
添加一个指标以跟踪 CO2 估算值
kpi_co2:Output[Metrics].
-
导入
OfflineEmissionsTracker
以使用离线模式(设置时没有互联网连接)。 -
实例化 codecarbon 跟踪器
tracker = OfflineEmissionsTracker(country_iso_code=”BEL”).
BEL 代表比利时。注意国家代码应与所选的 Google Cloud 区域匹配,在我们的例子中是europe-west1
。 -
使用
tracker.start().
开始跟踪。 -
使用 .fit() 方法开始训练模型。
-
使用
tracker.stop()
停止跟踪 -
记录排放
kpi_co2.log_metric(“emissions”, float(emissions)).
请参见下方我的示例。
一旦你更新 train_winequality
组件并重新运行笔记本,你应该会在管道图上看到 kpi_co2
指标工件。
作者提供的图片
然后,进入 NODE INFO 标签以检查 CO2 排放估算值(以 kg/kWh 为单位)。
❗️ 请注意,要创建一个 Vertex AI Pipeline 并在完成之前不进行监控,你可以使用 submit 而不是 run。
现在让我们使用深度学习算法进行练习。
2. 碳足迹使用 GPU 上的深度学习
在这个练习中,我们将使用 Keras(开源 Python 深度学习库)和 Tensorflow(开源机器学习框架)训练深度学习模型。目标是确定照片是否包含表 1 中列出的任何标签。为此,我们使用自定义的 卷积神经网络(CNN)架构进行图像分类。
2.1 数据描述
我们使用 Keras 提供的由 Zalando SE 提供的 Fashion-MNIST 数据集,该数据集基于MIT 许可证。它包括 60,000 张用于训练的图片和 10,000 张用于测试的图片。每张图片都是一个 28x28 的灰度图像,属于 10 种不同的时尚类别,并标记为以下 10 个类别之一:
表 1:包含照片的标签。
2.2 设置环境
-
Vertex AI Workbench
-
Python 3
-
Kubeflow 管道组件
-
使用 NVIDIA TESLA T4 GPU 预构建镜像
-
Tensorflow 2.11
-
Codecarbon
-
CarbonTracker
然后使用 pip 安装 Google Cloud Pipeline 组件和 TensorFlow。
pip3 install google-cloud-pipeline-components --upgrade --user
pip3 install kfp tensorflow
导入库。
定义全局变量。
2.3 创建自定义深度学习组件
我将使用tf-gpu.2–11:latest
作为基础镜像,该镜像包含 TensorFlow 和 GPU,存储在 Artifact Registry 中。请参见欧洲的预测和训练的预构建镜像。请注意,Google 还在Container Registry中发布了预构建镜像。
为了启用codecarbon
和carbontracker
,我们将按如下步骤进行:
-
将
codecarbon 和 carbontracker
添加到packages_to_install
列表中。 -
添加度量来跟踪估计值
kpi_co2:Output[Metrics]
。 -
导入 CarbonTracker 和日志解析器。
-
定义一个目录来重定向
carbontracker
的日志,例如DIR_LOG="log"
。 -
加载训练图像并进行缩放。
-
使用
keras.Sequential
API 来定义 CNN 的层。 -
编译模型。
-
启动
codecarbon
并指定荷兰地区的 ISO 代码 NLD。 -
指定 epochs 的数量和 carbontracker 的日志目录
carbontracker = CarbonTracker(epochs=epochs, log_dir=”./”+DIR_LOG+”/”)
。 -
启动 carbontracker
carbontracker.epoch_start()
。 -
在训练数据上拟合模型。
-
停止 carbontracker:
carbontracker.epoch_end()
& codecarboncodecarbon.stop()
。 -
记录排放值并保存模型。
2.4 评估模型
评估组件依赖于预编译的 GPU Tensorflow 2.11 基础镜像。
输入经过训练的模型。然后加载fashion.mist
测试数据集。它调整和重塑测试图像,评估模型,并计算准确率、损失和混淆矩阵。
2.5 创建并提交管道
该管道促进了无服务器工作流的编排。我们的管道有两个步骤:训练(deep_learning_mist_task)和评估(model_evaluation_task)。它接受一些参数,如 learning_rate、epochs 的数量、batch_size、API 端点、项目 ID 和服务 URI。
要为管道步骤指定机器配置,请使用:
.add_node_selector_constraint("cloud.google.com/gke-accelerator", GPU_TYPE )
.set_gpu_limit(GPU_LIMIT)
GPU_TYPE 的可用值包括:
-
NVIDIA_TESLA_A100,
-
NVIDIA_TESLA_K80,
-
NVIDIA_TESLA_P4,
-
NVIDIA_TESLA_P100,
-
NVIDIA_TESLA_T4,
-
NVIDIA_TESLA_V10。
GPU_LIMIT 是一个表示 GPU 限制的正数。
执行管道后,你应该会看到以下图表:
作者提供的图片
转到总结选项卡以查看训练期间 CO2 排放的估算值。
作者提供的图片
我们可以注意到 CO2 估算值不同,因为两个库之间的能耗公式不同。说实话,我更喜欢 Codecarbon,因为它有更好的兼容性和文档。
III. 减少碳足迹的几个实践💡
我建议在设计 AI 算法时考虑一些实际问题。
考虑将碳足迹整合到机器学习模型的整个生命周期中,从数据收集到模型部署。
寻找配备适当处理器(CPU/GPU/TPU)的机器,以适应你的用例。
选择碳足迹较低的云区域。例如,Google Cloud Platform 会指示每个可用区域是否低 CO2。
在你的机器学习生态系统中集成碳足迹跟踪器。
选择高效的模型架构。查看可能减少能耗的sparse models。
鼓励团队使用云,因为研究表明它更具能源效率。
尽可能使用预构建模型,而不是从头开始训练。
在组织中共享数据集、特征库和预构建的专业模型。
关键要点
总之,我推荐使用 Codecarbon,因为它在机器学习(CPU)和深度学习算法(GPU)上表现良好,并且具有更好的基础设施兼容性。关于 CarbonTraker,我在 Google Cloud CPU 上遇到了无法运行的错误,导致浪费时间。如果你打算使用 GPU,记得在使用前验证 GPU可用性。此外,我强烈建议检查GPU 定价以期望更低的成本。最后,重要的是要记住,减少机器学习中的碳足迹是一个持续的过程,新技术和技术不断发展以应对这一问题。确保跟踪有关碳足迹减少策略的更新。
笔记本可以在我的GitHub账户中找到。
希望你喜欢这篇文章。
谢谢阅读!
如果你想在收件箱中收到我的未来故事,别忘了订阅。
如果你喜欢阅读我的故事并希望支持我成为作者,请考虑注册成为 Medium 会员,并获得数千篇数据工程和数据科学文章的访问权限。
[## 使用我的推荐链接加入 Medium — Bildea Ana
作为 Medium 会员,你的会员费的一部分将用于你阅读的作者,并且你可以完全访问每一个故事……
medium.com](https://medium.com/@anna.bildea/membership?source=post_page-----3d6bc9695e7b--------------------------------)
查看我的 MLops 文章集合
MLOps - AI 在生产中
查看列表4 个故事
如何在时间序列中建模多重季节性
原文:
towardsdatascience.com/how-to-model-multiple-seasonality-in-time-series-2a3488f8e7f5
处理多个周期的季节性效应
·发布于 Towards Data Science ·阅读时间 5 分钟·2023 年 7 月 25 日
–
图片由 Joshua Woroniecki 提供,来源于 Unsplash
在这篇文章中,你将学习如何在时间序列中建模多重季节性。我们将涵盖:
-
如何使用 MSTL 分解时间序列
-
创建捕捉复杂季节性的解释变量
-
使用现成的方法,基于orbit的预测包进行示例。
复杂的季节性
季节性指的是以规律的周期性重复的系统性变化。这些模式与时间序列的观察频率有关。低频时间序列通常包含一个季节性周期。例如,每月时间序列表现出年度季节性。
越来越多的时间序列以更高的采样频率收集,如每日或每小时。这导致了具有复杂季节性的大数据集。每日时间序列可能显示出每周、每月和每年的重复模式。
下面是一个具有每日和每周季节性的每小时时间序列示例:
每小时时间序列具有每日和每周的季节性。人工数据和图像由作者创建。
乍一看,以上时间序列似乎没有超过一个季节性模式。多个季节性效应可能相互重叠,这使得识别所有相关周期变得困难。
多重季节性的分解
分解方法旨在将时间序列拆分为其基本部分:趋势、季节性和残差。
大多数方法设计用于处理单一预定义周期的季节性。例子包括经典方法、x11 和 STL 等。
STL 方法已经扩展以处理多重季节性。MSTL(用于多重 STL)在statsmodels Python 包中可用:
import numpy as np
from statsmodels.tsa.seasonal import MSTL
# creating an artificial time series with complex seasonality
# daily and weekly seasonality
period1, period2 = 24, 24 * 7
# 500 data points
size = 500
beta1 = np.linspace(-.6, .3, num=size)
beta2 = np.linspace(.6, -.3, num=size)
sin1 = np.asarray([np.sin(2 * np.pi * i / period1) for i in np.arange(1, size + 1)])
sin2 = np.asarray([np.sin(2 * np.pi * i / period2) for i in np.arange(1, size + 1)])
cos1 = np.asarray([np.cos(2 * np.pi * i / period1) for i in np.arange(1, size + 1)])
cos2 = np.asarray([np.cos(2 * np.pi * i / period2) for i in np.arange(1, size + 1)])
xt = np.cumsum(np.random.normal(scale=0.1, size=size))
noise = np.random.normal(scale=0.1, size=size)
# combining parts
yt = xt + beta1 * sin1 + beta2 * cos1 + sin2 + cos2 + noise
# hourly time series
ind = pd.date_range(end=pd.Timestamp('2023-07-10'), periods=size, freq='H')
yt = pd.Series(yt, index=ind)
yt.name = 'Series'
yt.index.name = 'Date'
# decomposition with MSTL
decomp = MSTL(endog=yt, periods=(period1, period2)).fit()
分解结果包括以下部分:
时间序列及其基本部分,包括两个季节性组件。图像由作者提供。
因此,MSTL 可以用于调整具有复杂季节性的时间序列。
你可以查看上一篇文章,了解如何使用分解后的时间序列构建预测模型。
多重季节性建模
除了分解,还有其他季节性建模方法,这些方法通常专注于具有单一季节周期的时间序列。不过,有些方法也可以处理复杂的季节性。
例如,你可以为不同的时期获取季节性虚拟变量。对于小时级时间序列,你可以获取每个观测值的日期、周或月份等信息。
使用傅里叶级数,你可以通过不同的周期计算正弦和余弦波。以下是如何使用sktime 实现这一点:
from sktime.transformations.series.fourier import FourierFeatures
# Fourier series with two periods
# 4 terms for the first period
# 2 terms for the second period
fourier = FourierFeatures(sp_list=[period1, period2],
fourier_terms_list=[4, 2],
keep_original_columns=False)
fourier_feats = fourier.fit_transform(yt)
你还可以获取多个周期的径向基函数。
现成的方法
有一些现成的方法可以处理复杂的季节性。例如,TBATS、Prophet、MSTL 或 KTR(基于核的时间变化回归)都是不错的选择。
使用 MSTL,你可以通过分别预测每个组件,然后合并预测的方法来处理这个任务。
下面是如何使用 KTR 的示例,这在orbit Python 包中可用:
from orbit.models import KTR
from orbit.diagnostics.plot import plot_predicted_data, plot_predicted_components
from sklearn.model_selection import train_test_split
df = yt.reset_index()
# train test split
train, test = train_test_split(df, shuffle=False, test_size=100)
# creating a KTR instance with the required periods
ktr_with_seas = KTR(
response_col='Series',
date_col='Date',
seed=1,
seasonality=[24, 24 * 7],
estimator='pyro-svi',
n_bootstrap_draws=1e4,
# pyro training config
num_steps=301,
message=100,
)
# fitting the model
ktr_with_seas.fit(train)
# inference
predicted_df = ktr_with_seas.predict(df=df, decompose=True)
_ = plot_predicted_data(training_actual_df=train,
predicted_df=predicted_df,
date_col='Date',
actual_col='Series',
test_actual_df=test,
markersize=10, lw=.5)
_ = plot_predicted_components(predicted_df=predicted_df,
date_col='Date',
plot_components=['trend',
'seasonality_24',
'seasonality_168'])
每个季节性组件都通过傅里叶级数建模,这些级数被用作解释变量。它们的样子如下:
从函数 plot_predicted_components 获取的结果。图像由作者提供。
每个傅里叶波具有不同的特征,以捕捉每个季节性的周期性。
关键要点
高频时间序列可以在多个周期中表现出季节性。捕捉所有季节性模式对于时间序列的最佳建模至关重要。
在这篇文章中,你学到了如何扩展常见的方法以处理多个季节性周期。
一些现成的方法也能够应对这个问题,如 Prophet 或 KTR。
感谢阅读,下次见!
相关文章
代码
参考文献
[1] Orbit 的 KTR 文档:orbit-ml.readthedocs.io/en/stable/tutorials/ktr2.html
[2] 预测:原理与实践,复杂季节性部分:otexts.com/fpp3/complexseasonality.html
[3] Holmes, Elizabeth E., Mark D. Scheuerell, 和 E. J. Ward. “应用于渔业和环境数据的时间序列分析。” NOAA 渔业,西北渔业科学中心,西雅图,WA (2020)。
破解当前数据科学就业市场:来自科技数据科学家的实用策略
原文:
towardsdatascience.com/how-to-navigate-the-current-data-science-job-market-6924f9b4a407
为什么现在很难找到数据科学相关的工作,以及你可以做些什么
·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 7 月 26 日
–
我在 2020 年全球疫情期间毕业,那时就业市场是一个充满敌意的领域。现在我很高兴地在 Spotify 担任数据科学家,但走到这一步的过程是漫长的。
在数据科学的就业市场中导航就像在与狮子和小猫的战场上作战。狮子可能会杀死你,而小猫也很可能会妨碍你。你稍后会明白我这句话的意思。
我记得申请了数百个职位却从未收到过一次面试回电。这让我怀疑自己的能力,但回头看来,大多数失败的原因超出了我的控制范围。
在 2020 年,我收到过数百封这样的邮件,但当时的就业市场完全荒凉。
如果你正在经历类似的情况,也许这篇文章可以帮助你找到应对当前就业市场的最佳方法。
最近几个月,我与多位经验丰富的科技数据科学家和有志于此的人员讨论了当前数据科学招聘市场的状况。
我逐渐明白了:
-
为什么现在变得难以获得聘用
-
如何最大化获得工作的机会
我将在这篇文章中与您分享这些经验。
但首先,请确保订阅我的通讯!
点击下面的链接,我会给你更多个性化内容和内幕建议,帮助你成为一名数据科学家!
[## 加入+1k 读者 💌 跟随我作为数据科学家在科技和 Spotify 的旅程,不要错过!
加入+1k 读者 💌 跟随我作为数据科学家在科技和 Spotify 的旅程,不要错过!通过注册,你将…
工作公式
在当前经济形势下,我越来越多地听到许多数据科学家在找工作时面临的困难。不论是应届毕业生还是来自 MAANG 的被裁员专家,目前大家都在苦苦挣扎。
如果你处于这种情况,请知道你无法找到工作的原因是一个多层次的问题。如果你想更好地调整你的努力,将所有机会放在你这边,了解为什么会发生这种情况是至关重要的。
获得工作机会的机会 = (技能 + 经验) * 市场条件
找工作取决于你的技能、过去的经验,以及最重要的一点:市场条件,这些完全超出了你的控制范围。
目前,求职市场对新加入者并不那么友好。我们都知道经济形势不利,但后续影响是什么?这与你的困境有何关系?
经济衰退的后果
我目睹了许多数据科学家,其中一些我个人认识的人,在全球科技市场裁员潮中失去了工作。
这个玩笑——照片由Annie Spratt提供,Unsplash
这些大规模裁员有 3 个可能影响你目前找工作能力的后续效应,即使这份工作不在科技领域:
-
数据专家过剩
-
公司员工数量减少
-
公司成熟度与规模
#1. 市场饱和
多年来,申请数据科学职位的人数过剩。似乎每个人都被“数据科学”这个被誉为本世纪最热门工作的钩子吸引了。
这也意味着随着时间的推移,数据科学市场正越来越多地被新加入者所饱和。
过剩来自于:
-
自学的爱好者迎合潮流
-
数据科学课程的应届毕业生
-
从事数据科学工作的技术背景人士,包括博士(通常在科技领域非常受欢迎)
-
从训练营毕业的人或持有数据证书的人
照片由Marvin Meyer提供,Unsplash
这意味着当你申请职位时,你也在与这些人竞争,包括经验丰富的“狮子”和不太合格(或不合格)的“小猫”。这种情况已经存在了一段时间。
但让游戏变得更难的是现在你还要面对一波新被裁员的专家加入这个圈子。
这就像你在游戏中玩地狱难度级别一样,尽管你什么也没要求(除了工作)。
这也意味着由于两个原因,你被拒绝的机会会更高。
1. 你正在与被裁员的技术行业精英竞争。他们有丰富的经验,并且他们可能最终会在过程中超越你。即使你是其中之一,你仍然在和他们竞争。
-
如果公司规模较大,这些人可能有更大的机会被录用,而对于经验较少的人来说,获得招聘人员回电的难度会更大。
-
规模较小的公司可能无法承担这些专家的高薪和福利。如果你经验较少,瞄准这些公司可能会增加你的机会。
2. 大多数公司没有复杂的招聘系统。其中一些公司因为缺乏筛选大量申请的手段,最终在筛选过程中错过了优秀的候选人。这意味着你的简历有时可能会被忽略。
从人群中脱颖而出成为你的新目标,但我们稍后会详细讨论这一点。
#2. 人员编制少
这种经济冲击的第二个下游效应是,现在公司更不愿意招聘人员。由于公司现在有不同的优先级(盈利 > 增长),这意味着:
-
招聘预算在过程中被削减,所以人员编制有限。公司仅根据需求招聘,因此招聘过程可能会非常挑剔。
-
经验丰富的候选人正处于前列,因为公司能从他们那里获得比更初级员工更多的投资回报。
这意味着你要和更多的人竞争更少的职位。
当我在 2020 年申请工作时,公司由于相同的原因——节省成本——而进行裁员。
不幸的是,劳动力市场非常波动,但这也是好消息,因为这意味着情况也可以迅速发生变化。
#3. 公司规模
并不是很多公司处于需要数据科学家的阶段。我们通常是在公司积累了足够的数据后,或者当数据成为产品时进入游戏。
对于许多科技公司,比如 Netflix,数据就是产品。
他们主要通过分析数据并利用机器学习将其转化为产品来获得大部分价值。
你能想象 Netflix 没有系统推荐符合你口味的娱乐内容吗?数据在这里至关重要,数据科学家也是如此。
不依赖数据来增长和盈利的公司很可能在经济衰退期间会更加谨慎地考虑是否雇佣数据科学家。大多数数据科学家变成了奢侈品。不是每个人都能负担得起我们。
你能做些什么?
如果你想继续参与游戏,你需要稍微调整规则。
由于竞争激烈,你的工作是尽力在众人中脱颖而出。我做的一些事情帮助我和其他数据科学家在游戏中提升水平,包括:
-
个人品牌
-
正确的网络拓展方式
-
专业化
-
海外申请
#1. 个人品牌
确保你的 LinkedIn 页面整洁。任何能帮助你看起来很有吸引力的内容都应该在上面。
照片由 Greg Bulla 贡献,来源于 Unsplash
接下来是你的简历。如果你经验很少或者没有经验,那么你需要将你的项目放在最前面。如果你做的数据科学项目不多,那么你没有收到回电也就不足为奇了。回去学习吧!
你可以做的一些事情:
-
精心制作你的简历。 当然,你不能将简历适应到每家公司。我曾申请过一个需要一些聚类技术知识的职位,因此我确保突出了我做过聚类的项目。因此,你应该做的是:
— 选择 3-5 个你真正感兴趣且你也符合条件的职位。
— 研究这家公司当前员工在类似岗位上的技能,并识别你与他们共享的技能,这些技能也出现在职位申请中。
— 在你的简历上突出那些特定的技能,以及你个人资料中与公司价值观一致的其他任何内容。
-
创建和优化你的作品集。 它可以是一个网站或在 GitHub 上。重要的是展示你能做什么。确保在你的简历上包含到你的作品集的链接!
-
突出展示你运用了商业洞察力的项目。 大多数科技公司聘请数据科学家是为了帮助他们进一步实现商业目标。因此,你需要展示你能够在商业背景下理解数据。
我被 Spotify 聘用的主要原因之一(至少我是这么被告知的)是因为我有商业背景和数据科学技能组合。最好的数据科学家是能够将两者结合起来的人,所以展示你可以做到这一点(如果你能的话)!
事实上,我编写了一篇逐步指南,帮助你设计一个成功的数据科学工作简历。 在这篇文章中,我详细分析了让我在 Spotify 获得工作的简历。不要错过!
2. 正确的网络拓展方式
图片来源:作者(Midjourney)
利用你的网络来获得入门机会。我通过网站申请进入 Spotify,这是传统方式。但如果你想提高获得回复的机会,我建议你发挥你的网络作用。这就是我在同一阶段被拒绝后如何获得 Ubisoft 实习机会的方式。
这并不是意味着联系随机的人并请求他们提供推荐。
当有人把你推荐给一家公司时,他们承诺他们推荐的是他们信任的人,是他们知道能够带来积极贡献的人。陌生人不会认识你,因此他们很可能不会回复你的推荐请求。
这意味着你能做的最好的事情 是尝试与已经在你网络中的人建立联系。
最终,你可以依靠的有可能是你曾就读的大学的校友、招聘经理、以前的同事,甚至是朋友或朋友的朋友,但不是你朋友的狗。
重要的是与你可以利用的人建立某种良好的联系。
这并不意味着他们会推荐你,但至少你可能会有机会向他们展示你的能力。这也不意味着你一定能获得这份工作,但至少它可能会让你的简历受到关注,而不是在没有机会的情况下被筛选掉。
我写了一整篇文章,讲述了如何通过建立人脉获得了技术领域的顶级机会,以及你需要遵循的 6 个步骤以在拥挤的数据职位市场中脱颖而出。 一定要去看看!
3. 专业化!
人工智能在各地蓬勃发展,数据职位市场也因此最近增长迅猛,最终分裂成了不同的分支:
-
数据专家: 数据科学家、数据分析师、数据工程师、机器学习工程师、研究科学家
-
人工智能专家: 自然语言处理科学家、云计算工程师、计算机视觉科学家、大型语言模型科学家、稳定扩散专家等……
而且这种疯狂的情况只会与日俱增。跟上潮流很困难,但知道要选择哪个潮流可以让你免于溺水。
图片由作者提供(Midjourney)
与所有人交谈就是与任何人交谈
如果你的技能范围已经过于广泛以至于不适合数据市场,那么将技能集中在特定领域可能会提高你的机会。这样,你可能会在与更少的人竞争时变得更具吸引力。
4. 在美国以外申请
数据科学家在美国以外的地方也有发展机会。我这样说是因为许多科技公司总部设在那里,因此在求职市场上挣扎的很多人也在那里。我知道,因为我曾经也是其中之一。
如果美国的招聘放缓了,要知道在世界其他地方招聘并没有完全停止。
在像我所在的法国这样的国家,高质量的数据科学家并不多见,所以如果你没有被限制在特定国家,我建议你拓宽视野,去其他地方看看。
由于美国的数据科学职位市场竞争激烈,搬到法国证明是一个战略性决定,帮助我在 Spotify 获得了梦想中的工作。
在法国,仅数据科学领域目前就有将近 2 万个职位空缺(参见下图),我相信在许多其他国家也是如此。
当然,我理解有些人由于各种原因无法搬到国外。但我们生活在一个后疫情时代,远程工作越来越成为常态。
如果你不能搬到国外,你可以尝试申请其他国家的职位,无论你是否住在美国或其他地方。
我住在巴黎,但我的大部分团队成员分布在欧洲各地。分布式工作正越来越多地使有才华和勤奋的人能找到超越国界的机会。你考虑尝试一下吗?
总结
为什么今天很难找到数据相关的工作?
经济衰退对就业市场造成了 3 个下游影响:
-
数据工作市场中应聘者的过度饱和
-
招聘有限,由于公司更注重盈利而非增长,导致职位竞争加剧。
-
那些不依赖数据来开展业务的公司可能在经济衰退期间会犹豫是否雇佣数据科学家。
你可以做些什么?
-
个人品牌。 确保你的个人资料呈现得体,并通过相关项目优化你的作品集,展示你的商业技能。
-
正确网络化。 利用你的网络来提高被关注的机会。
-
专业化。 将你的技能集中在一个特定领域,以便脱颖而出。
-
申请美国以外的职位。 考虑在其他对数据科学家需求增长的国家(包括远程工作)的机会。
当然,这些不是奇迹解决方案。目标是帮助你优化求职方法,提高你的机会。愿好运与你同在!
我有礼物送给你🎁!
订阅我的 通讯 K’s DataLadder,你将自动获得我的终极 SQL 备忘单,其中包含我在大型科技公司工作中每天使用的所有查询+另一个秘密礼物!
我每周分享作为科技领域数据科学家的经历,以及实用的技巧、技能和故事,帮助你提升自己——因为没有人真正知道,直到他们亲身经历!
如果你还没有这样做的话
不久后见!
如何 避免 将机器学习模型投入生产
原文:
towardsdatascience.com/how-to-not-get-machine-learning-models-in-production-742e6b79847d
一份讽刺性的指南,避免将生产过程与您的机器学习模型接触
·发表于 Towards Data Science ·8 分钟阅读·2023 年 7 月 8 日
–
图片由 Unpad Street Feeding Animal Friend 提供,在 Unsplash
您旅程的概述
-
引言 — 没有生产,没问题!
-
笔记本可以用来做所有事情!
-
为什么要在有时间的时候自动化?
-
测试?只需从不出错!
-
我的头脑中的依赖管理!
-
总结
1 — 引言 — 没有生产,没问题!
作为数据科学家、数据工程师和机器学习工程师,我们被有关生产中机器学习模型的信息淹没。数以百计的视频和数以千计的博客尽力帮助我们避免当前的困境。然而徒劳无功。现在,我痛苦地说,全球各地都有机器学习模型在生产中。 这些模型正在为数百万无知的人创造价值。每个街角都有。我们容忍它,只因为它很常见。
在参加会议时,我听到紧张的数据科学家在大观众面前谈论生产。从他们的额头汗水和湿漉漉的手中可以看出情况的严重性。这种情况已经持续了很多年,但我们没有相信他们的预言。现在看看我们自己。我们应该听从的。
现在不是有人说“我早就告诉过你们”的时候。我们需要团结起来,夺回我们的领地。我们需要单方面表达对现代做事方式的厌恶。我们需要回到更好的时代,当时生产中的机器学习模型只是中型公司招聘广告上的一个非约束性要点。
必须有人带头引导这次救赎之旅。而谁比我更合适呢?不打算吹嘘,我已经做了几个没有投入生产的 ML 模型。其中一些甚至离生产都很远。我可以与你分享一些最佳技巧,这样你就不需要复制你的开发环境——因为将不会有其他环境。
我已经将接下来的每一部分清晰地分为两个部分。第一部分叫做正道。它告诉你如何避免将 ML 模型投入生产。第二部分叫做歪道。这让你知道应该避免什么,因为这是将 ML 模型快速投入生产的捷径。不要搞混!
2 — 一个单一的笔记本可以用来做所有事情!
正道
我发现避免将 ML 模型投入生产的最简单方法之一就是将你的整个代码库放在一个单一的 Jupyter 笔记本文件中。这包括模型训练、模型预测、数据处理,甚至配置文件。
想想看。当所有敏感的配置文件都在你的主文档中时,几乎不可能在不引入重大安全风险的情况下将任何东西上传到 GitHub 或 Azure DevOps。此外,你是否曾尝试阅读一个修改了 100,000 行的文件的拉取请求?最好的回应就是干脆放弃远程托管和版本控制。
这,我的朋友,把我们带上了避免生产的快车道。没有远程托管意味着孤岛将是不可避免的。你有没有停下来思考一下每次需要预测时运行整个模型训练的云消耗成本——记住,我们只有一个用于训练和测试的 Jupyter 笔记本。我自豪地称之为单一笔记本架构的这种方法简直是天才之作。
歪道
相比单一笔记本架构,人们随着时间的推移走了歪路。他们开始将代码、配置和文档拆分到多个文件中。这些文件不幸的是遵循了使其易于导航的惯例。甚至还有像Cookiecutter这样的工具,提供了结构化 ML 项目的易用模板。
代码本身通常被划分为诸如函数、类和文件这样的模块化组件。这鼓励了重用并改善了组织结构。编码标准通过PEP8 标准和Black等自动格式化工具来强制执行。甚至还有一些恶心的博客帖子指导数据科学家更好的软件开发实践。真恶心!
不仅仅是源代码被结构化。还有像数据版本控制(DVC)这样的工具用于大数据版本控制,或者像MLflow这样的工具用于结构化机器学习模型打包。这些工具中的任何一个实际上都不难上手。远离这些吧!
3 — 为什么在有时间的时候还要自动化?
图片由Aron Visuals拍摄,来自Unsplash
正义的方式
过去,情况有所不同。我们对我们的代码有一种归属感。这种归属感来自于定期重新审视代码。没错,我们都是手动运行所有代码的。 这显然是上天的旨意,我已经厌倦了假装不是这样。
手动方法让你拥有完全的控制权。不需要检查数据管道是否在凌晨 3 点启动——你就在那儿!每晚都仔细地从记忆中输入执行命令。当你有时间的时候,为什么要依赖CRON 表达式或Airflow DAGs来完成工作?到头来,你真的能相信别人吗?
这种手动方法被证明在生产环境中对机器学习模型是个福音。数据漂移发生在机器学习模型上,然后模型需要重新训练。但使用手动方法,必须有人坐镇监视数据漂移是否足够。在这种经济环境下?不太可能!也许还是放弃整个生产环节,回到更好的方法上来吧。
堕落的方式
让我们谈谈明显的问题:持续集成与持续交付(CI/CD)。我知道,这也让我感到愤怒。现在,人们会在更新代码库之前自动检查代码质量并运行测试。像GitHub Actions或Azure DevOps Pipelines这样的工具也用于自动化生产环境中机器学习模型的再训练。我们还有机会吗?
还有更多!现在人们使用像Teraform这样的工具来设置支持生产环境中机器学习模型所需的基础设施。通过这种基础设施即代码的方法,环境可以在各种设置中复制。突然间,生产环境中的机器学习模型从 Azure 变成了 AWS!
还有像Airflow这样的工具帮助进行数据管道的编排。Airflow 确保预处理步骤在执行之前等待前一步骤完成。它还提供了一个 GUI,你可以查看之前的管道运行记录,了解情况。虽然这些功能看似简单,但它们能在错误传播到系统并破坏数据质量之前迅速发现错误。不幸的是,高质量的数据对于成功的生产环境中的 ML 模型至关重要。
4 — 测试?就是不要犯错!
正确的方式
测试的哲学是对无数生产失败和安全漏洞的回应。这个想法是尽可能多地发现问题,然后再推出产品。虽然确实很高尚,但我认为这是误导性的。对于那些关注的人,一个更简单的方法显现出来。正是这样!一旦你看到它,它就像白天一样清晰。你只需不犯任何错误。
对于没有错误的代码,不需要测试。更好的是,当你对代码的有效性 100%确信时,其他人则不那么确信。他们会怀疑你的代码的正确性。因此,他们将避免将你的 ML 模型投入生产,担心一切会崩溃。你知道它不会。但他们不必知道这一点。把你完美的编码技巧留给自己。
不编写测试还有一个额外的好处,那就是你会获得更少的文档。有时测试对帮助他人理解你的代码应该做什么至关重要。 没有测试,甚至更少的人会打扰你,你可以继续平静地工作。
罪恶的方式
编写测试已经变得很普遍,即使是对于 ML 模型和数据工程管道。主要有两类测试:单元测试和集成测试。正如名称所示,单元测试测试一个单独的单元,如 Python 函数是否按预期工作。另一方面,集成测试测试不同组件是否无缝地协同工作。让我们关注单元测试。
在 Python 中,你可以使用内置的库unittest来编写,正如你所猜到的,单元测试。这是基于 OOP 的,需要一些样板代码才能开始。越来越多的人使用外部库pytest来编写 Python 中的单元测试。它是功能性而非基于 OOP 的,并且需要较少的样板代码。
编写单元测试还有一个副作用。它迫使你以模块化的方式编写代码。经过充分测试的模块化代码在生产中发生故障的概率较低。这对你的 ML 模型来说是坏消息。如果它们在生产中没有出现故障,那么它们将永远留在那里。考虑一下,ML 模型在生产中出现故障就是它们试图逃脱。谁能怪它们呢?
5 — 头脑中的依赖管理!
正确的方式
管理依赖项是任何机器学习项目的重要部分。这包括知道你使用了哪些 Python 库。我的建议是简单地记住你安装了哪些库、你使用的操作系统、运行时版本等等。 这并不难,我相信甚至有应用程序可以帮助你跟踪这些。
我有时会在夜里醒来,想知道自己运行的是 scikit-learn 的 0.2.3 版本还是 0.3.2 版本。这没关系!所有版本都存在,所以应该没有问题……对吧?如果我不定期解决依赖冲突,那么我的依赖冲突技能就会生疏。
仅仅记住你的依赖项的优势在于其他人很难运行你的代码。尤其是当你不想告诉他们所有依赖细节时。这样,你可以避免有人突然产生偷偷将你的机器学习模型投入生产的想法。
罪恶的方法
记忆力差的人选择了简单的办法。他们寻找可以为你处理依赖的解决方案。我发誓,人们每天都变得更懒惰!管理运行时版本和库依赖的简单方法是使用虚拟环境和requirements.txt文件。在 Python 中,有像virtualenv这样的工具,允许你轻松设置虚拟环境。
那些想更进一步的人使用像Docker这样的基于容器的技术。这可以从操作系统及以上层面复制一切。这样,只要每个人知道如何运行 Docker 容器,共享机器学习模型就变得相当轻松。在像Azure Machine Learning这样的现代工具中,有简单的方法来使用标准化的 Docker 镜像。这些镜像可以包括像scikit-learn和tensorflow这样的常用库。而且你甚至不需要自己记住版本!
6 — 总结
我希望不用指出你确实希望机器学习模型进入生产环境。我在这里提出的观点可以反向操作,帮助你成功将那些麻烦的机器学习模型投入生产。
随时在LinkedIn上关注我并打个招呼,我在这篇博客文章中看起来并没有我表现得那么烦躁 ✋
喜欢我的写作吗? 可以看看我其他的一些帖子,获取更多 Python 内容:
-
用美丽的类型提示现代化你的罪恶 Python 代码
-
在 Python 中可视化缺失值简直太简单了
-
在 Python 中使用 PyOD 介绍异常/离群点检测 🔥
-
5 个绝妙的 NumPy 函数,关键时刻拯救你
-
5 个专家技巧,让你的 Python 字典技能飞速提升 🚀
如何在 Python 中客观地比较两个排名列表
原文:
towardsdatascience.com/how-to-objectively-compare-two-ranked-lists-in-python-b3d74e236f6a
对 Rank Biased Overlap 的简化解释和实现
·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 1 月 5 日
–
假设你和你的朋友都看过所有 8 部《哈利·波特》电影。
但有一个问题 —— 你是每部电影在首映当天就观看的,没有错过任何一次首映。
然而,你的朋友却是先看了第二部电影,然后是第四部和第五部,最后在 Netflix 上追完了其余的电影。
理论上,你和你的朋友处于同等条件 —— 都看过该系列的所有电影。
它真的平等吗?
图片由作者使用商业可用字体(Harry P)和 CC 图片来自 Wikimedia 制作
作为一个真正的粉丝,你绝对不能将它视为平等。
怎么可能有人不按顺序观看一本书系列的电影,这完全不合理?!
你认为这是亵渎!
好消息是 —— 数学对你有利。 你可以将这个问题简化为排名列表比较。
有几种方法可以比较列表。
在这篇文章中,你将:
-
理解为什么我们实际上需要一个排名列表的比较
-
查看比较度量应该满足的要求
-
了解一种叫做 Rank Biased Overlap 的方法
-
理解其背后的数学方法
-
在 Python 中创建一个简单的实现
最后,你将彻底解决与你朋友的分歧:
你 != 你的朋友
为什么你需要排名列表比较?
除了上述的电影观看顺序分歧,我们周围充满了比较列表的例子!
实际上,我们所有人都在不断进行这样的比较。
-
对同一查询在 Google 和 Bing 上的结果进行比较,哪个更好?
-
《纽约时报》畅销书榜单与《今日美国》的榜单有多相似?
-
Rotten Tomatoes 上的前 10 部电影排名与 IMDB 相比有多不同?
在自然语言处理和机器学习领域:
-
两个段落在转换为标记化单词列表后有多相似?
-
如何比较由两个不同的学习排序/机器学习排名(MLR)¹模型生成的排名输出?
在制造/生产和物流领域:
- 将传入部分的序列与传出部分的序列进行比较(FIFO)。一个理想的 FIFO 序列如下所示。一个1首先进入,一个1首先出来。
但这在现实世界中可能并不总是发生。也许一个1先进去,但一个3先出来?
是的,这可能导致完全的混乱。
那么,你应该如何衡量序列的质量?
你需要某种度量。
这种度量必须满足哪些要求?
比较度量的要求
理想情况下,比较度量应该以分数的形式出现,指示排名的相似程度。
再次考虑上述示例。
你可以从中得到关于这种度量应该告诉我们什么的线索:
-
排名列表可能是无限的,且可能长度不同——因此度量应该能够处理两个不同的列表大小
-
应该有能力在选择的交集长度下进行比较——这意味着度量应该处理列表的交集长度
-
顶部元素比底部元素更重要(例如在搜索结果、电影排序等方面)——因此应该有在需要时赋予权重的可能性
现在让我们来看看满足这些条件的东西。
排名偏差重叠
正如名字所暗示的,这种方法是排名偏向的。
这意味着你可以根据更高的权重优先考虑列表中的前几个元素。
这个方法在一篇题为——**W. Weber 等人²的《无限排名的相似性度量》**的论文中提出。
你不必通读整个研究论文中的复杂内容,我在下面简化了大部分部分,以便我们可以尝试在 Python 中进行快速实现。
不过,系好安全带,戴上数学眼镜,我们将穿越一些复杂的公式!
设 S 和 T 为我们标题图像中两个排名列表的名称:
现在让我们定义这两个列表在深度(d)下的交集。
这实际上意味着什么?
这是 S 和 T 在给定长度(称为深度d)下的共同元素列表。从中可以清楚地看到:
深度被认为是最多为较小列表的长度,否则,交集就没有意义。
这个新的交集列表的长度称为重叠,在这种情况下是 7。
接下来,我们定义一个称为一致性的概念。它只是按深度计算的重叠。
**平均重叠(AO)**是所有深度列表的平均一致性,范围从 1 到整数 k。
当 k = 1 时,深度范围从 1 到 1,这意味着只比较第一个元素。
由于元素匹配,交集 X 为 1,Agreement A 也是 1。
让我们看一下 k = 2。
在这种情况下,d 从 1 到 2,即我们比较列表中的前两个元素。在这种情况下,重叠为 1,因为只有第一个元素是共同的。因此,一致性为 1/2 或 0.5。
同样,我们可以继续进行 k = 3。
好吧,现在你明白它是如何工作的,你可以继续这个过程,直到 k = 7。
你将得到**平均重叠(AO)**为:
这可以适用于两个列表中的任何元素数量。
我们可以将上述求和概括为:
这就是相似度度量的基础。
平均重叠是一个相似度度量,用于比较两个列表在给定深度上的相似度。
取一组这样的相似度度量(SIM):
这是一个几何级数,其中***d’***项的形式为 1/(1-p),其中 0 < p < 1。
将其代入上面的公式并重新排列项,我们可以得到列表 S 和 T 的排名基础重叠为:
变量p位于范围(0,1)内,可以用于确定前d个元素对最终相似度分数的影响。
为了得到一个单一的 RBO 分数,可以对上述公式进行外推。假设深度k的一致性在两个列表之间无限延续,可以将其概括为:
RBO 分数在 0 和 1 之间。
- 0 表示完全不相交,而 1 表示完全相同的排名列表。
我们将使用这个公式来实现 RBO 函数的 Python 代码!
论文还展示了我们如何选择p来给前d个元素赋予权重(Wrbo)(我在这里不深入推导):
我们将使用这个公式来实现权重计算器功能。
呼, 这真是信息量很大啊!
但不要担心,这些公式看起来比实际要复杂得多——你会看到用 Python 实现起来是多么简单。
所以我希望你还在这里,因为有趣的部分现在开始了!
Python 中的排名偏倚重叠的简单实现
我使用上述公式在 python 中编写了 Rank Biased Overlap 的简单实现和一个权重计算函数。
现在你了解了 RBO 的推导过程,实施起来很简单。
import math
def rbo(S,T, p= 0.9):
""" Takes two lists S and T of any lengths and gives out the RBO Score
Parameters
----------
S, T : Lists (str, integers)
p : Weight parameter, giving the influence of the first d
elements on the final score. p<0<1\. Default 0.9 give the top 10
elements 86% of the contribution in the final score.
Returns
-------
Float of RBO score
"""
# Fixed Terms
k = max(len(S), len(T))
x_k = len(set(S).intersection(set(T)))
summation_term = 0
# Loop for summation
# k+1 for the loop to reach the last element (at k) in the bigger list
for d in range (1, k+1):
# Create sets from the lists
set1 = set(S[:d]) if d < len(S) else set(S)
set2 = set(T[:d]) if d < len(T) else set(T)
# Intersection at depth d
x_d = len(set1.intersection(set2))
# Agreement at depth d
a_d = x_d/d
# Summation
summation_term = summation_term + math.pow(p, d) * a_d
# Rank Biased Overlap - extrapolated
rbo_ext = (x_k/k) * math.pow(p, k) + ((1-p)/p * summation_term)
return rbo_ext
让我们检查一下我们的示例列表 S 和 T。
S = [1,2,3,4,5,6,7]
T = [1,3,2,4,5,7,6,8]
print (rbo(S,T))
>> 0.8853713875
RBO 得分 0.88 意味着列表几乎 90%相似(这在之前的平均重叠计算中也看到过)。
这绝对不是一个稳健的 python 实现。
然而,它显然足够好,可以开始使用!
现在让我们实现权重计算器函数,帮助我们选择 p。
import numpy as np
import math
def weightage_calculator(p,d):
""" Takes values of p and d
----------
p : Weight parameter, giving the influence of the first d
elements on the final score. p<0<1.
d : depth at which the weight has to be calculated
Returns
-------
Float of Weightage Wrbo at depth d
"""
summation_term = 0
for i in range (1, d): # taking d here will loop upto the value d-1
summation_term = summation_term + math.pow(p,i)/i
Wrbo_1_d = 1 - math.pow(p, d-1) +
(((1-p)/p) * d *(np.log(1/(1-p)) - summation_term))
return Wrbo_1_d
让我们检查一下它是否有效。
根据研究论文,p 为 0.9 时,前 10 个元素在最终相似性评分中的权重为 86%¹。
使用我们上面的函数:
weightage_calculator(0.9,10)
>> 0.8555854467473518
太好了,它运行良好——与论文中指出的一致,得到了 86%!
我们现在准备好了工具。
让我们解决你和你朋友之间的分数吧!
解决你的争论!
让我们通过创建哈利·波特电影的列表,以你和你朋友观看的顺序来测试一下。
然后在其上运行你的 RBO 函数!
you = ['''Harry Potter and the Philosopher's Stone''',
'Harry Potter and the Chamber of Secrets',
'Harry Potter and the Prisoner of Azkaban',
'Harry Potter and the Goblet of Fire',
'Harry Potter and the Order of Phoenix',
'Harry Potter and the Half Blood Prince',
'Harry Potter and the Deathly Hallows - Part_1'
'Harry Potter and the Deathly Hallows - Part_2']
your_friend = ['Harry Potter and the Chamber of Secrets',
'Harry Potter and the Goblet of Fire',
'Harry Potter and the Order of Phoenix',
'''Harry Potter and the Philosopher's Stone''',
'Harry Potter and the Prisoner of Azkaban',
'Harry Potter and the Half Blood Prince',
'Harry Potter and the Deathly Hallows - Part_1'
'Harry Potter and the Deathly Hallows - Part_2']
rbo (you, your_friend)
>> 0.782775
0.78!
所以,按顺序观看电影确实很重要,否则分数会是 1!
但你可以更进一步。
通常,前几部电影介绍了角色并构建了虚构的世界——因此,按正确的顺序首先观看这些电影具有更高的质量重要性。
让我们使用权重计算器,并给前四部电影更多的权重(86%)。
通过对权重计算函数的一些试验,我们得到 p = 0.75 和 d = 4 时的权重为 86%。
weightage_calculator(0.75,4)
>> 0.8555854467473518
因此,在 RBO 函数中使用 p 为 0.75 将使前四部电影在排名比较中有更大的影响:
rbo (you, your_friend, 0.75)
>> 0.5361328125
得到的比较得分是0.53!
这意味着如果你跳过前几部电影,或以错误的顺序观看,它是客观上的不良。
实际上,你朋友的观看顺序仅比你的顺序好一半(53%)!
现在你有数学证明这一点!
RBO 的优势
Rank-biased overlap 并不是比较列表的唯一方法——其他方法包括:
-
Kendall Tau³
-
Pearson 相关系数⁴
然而,RBO 在以下几个方面优于其他方法:
-
RBO 适用于不同长度的比较列表(不相交或不相似)
-
它具有权重测量——你可以赋予比较中顶部或底部更多的重要性
由于这些好处,我决定在这篇文章中详细解释 RBO。
但是,欢迎查看我在来源中链接的其他两个——它们在不同的情况下也会使用!
结论
总结来说,在这篇文章中你学到了比较排名列表的一种度量——Rank Biased Overlap。
-
你已经深入了解了它的数学原理
-
在 python 中做了一个简单的实现
-
使用这些函数对电影观看顺序进行了实际比较
-
理解了它的好处!
现在每当出现关于比较电影排名、观看顺序或基本上任何序列的分歧时,你可以像专家一样解决问题!
完结。
来源和注释:
[1] 学习排名
[2] 不确定排名的相似性度量 — 威廉·韦伯,阿利斯泰尔·莫法特,贾斯廷·佐贝尔
[3] 肯德尔秩相关系数
[4] 皮尔逊相关系数
图片、动图和脚本的版权归作者所有。
操作指南:手动进行单因素 ANOVA
原文:
towardsdatascience.com/how-to-one-way-anova-by-hand-4c19e2a61a8c
学习如何手动进行单因素方差分析(ANOVA),以比较三个或更多组之间的定量测量
·发表于Towards Data Science ·阅读时间 6 分钟·2023 年 8 月 30 日
–
图片由Rohan Makhecha提供
介绍
ANOVA 是一种统计检验,用于比较定量变量在各组之间,以确定几个总体均值之间是否存在统计学显著差异。在实际操作中,它通常用于比较三组或更多组。然而,从理论上讲,它也可以仅用于两个组。1
在之前的一篇文章中,我们展示了如何在 R 中进行单因素 ANOVA。在本文中,我们演示了如何通过手动方式进行单因素 ANOVA,通常称为“ANOVA 表”。
数据和假设
为了说明该方法,假设我们抽取一个包含 12 名学生的样本,这些学生平均分为三班(A、B 和 C),并观察他们的年龄。以下是样本:
作者提供的表格
我们有兴趣比较各班级之间的总体均值。
请记住,ANOVA 的原假设是所有均值相等(即,各班级之间的年龄没有显著差异),而备择假设是至少有一个均值与其他两个不同(即,至少有一个班级的年龄与其他两个班级有显著差异)。正式来说,我们有:
-
μA = μB = μC
-
至少一个均值不同
手动进行 ANOVA
如上所述,我们将进行一个 ANOVA 表以得出测试结论。
请注意,ANOVA 需要一些假设(即独立性、方差齐性和正态性)。本文的目的是演示如何手动进行 ANOVA,而不是验证这些假设,因此我们假设这些假设已满足而未进行验证。如果你有兴趣,查看如何在 R 中测试这些假设。
整体和组均值
我们首先需要计算均值年龄(称为组均值):
-
类别 A: (24+31+26+23) / 4 = 26
-
类别 B: (24+21+19+24) / 4 = 22
-
类别 C: (15+21+18+18) / 4 = 18
整体样本的平均年龄(称为总体平均值):
(24+31+26+23+24+21+1912+24+15+21+18+18) / 12 = 22
SSR 和 SSE
然后我们需要计算回归平方和(SSR)和误差平方和(SSE)。
SSR 通过将组均值与总体均值之间的差异平方,然后乘以该组的观察数来计算:
作者提供的表格
然后将所有单元格的总和:
64+0+64 = 128 = SSR
SSE 通过将每个观察值与其组均值之间的差异平方来计算:
作者提供的表格
然后将所有单元格的总和:
4+25+0+9+4+1+9+4+9+9+0+0 = 74 = SSE
对于那些有兴趣计算总平方和(SST)的人,它只是 SSR 和 SSE 的总和,即:
SST = SSR + SSE = 128 + 74 = 202
ANOVA 表
ANOVA 表如下(我们将其留空,并将逐步填写):
作者提供的表格
我们开始构建 ANOVA 表,将上面找到的 SSR 和 SSE 值插入表格(在“Sum.of.Sq.”列中):
作者提供的表格
“Df”列对应自由度,计算方法如下:
-
对于线性回归:组数-1 = 3–1 = 2
-
对于线性误差:观察数-组数 = 12–3 = 9
有了这些信息,ANOVA 表变为:
作者提供的表格
“Mean.Sq.”列对应均方,等于平方和除以自由度,即“Sum.of.Sq.”列除以“Df”列:
作者提供的表格
最后,F 值对应于两个均方之间的比率,即 64 / 8.222 = 7.78:
作者提供的表格
这个 F 值给出了检验统计量(也称为 Fobs),需要与 Fisher 表中的临界值进行比较以得出结论。
我们根据自由度(ANOVA 表中使用的自由度)和显著性水平在 Fisher 表中找到临界值。假设我们取显著性水平α = 0.05,则可以在 Fisher 表中找到临界值如下:
作者提供的表格
所以我们有
F(2;9;0.05) = 4.26
如果你有兴趣用 R 找到这个值,可以使用qf()
函数,其中 0.95 对应于 1−α:
qf(0.95, 2, 9)
## [1] 4.256495
检验结论
拒绝规则说,如果:
-
Fobs > F(2;9;0.05) ⇒ 我们拒绝原假设
-
Fobs ≤ F(2;9;0.05) ⇒ 我们不拒绝原假设
在我们的案例中,
Fobs = 7.78 > F(2;9;0.05) = 4.26
⇒ 我们拒绝原假设,即所有均值相等。换句话说,这意味着至少有一个类别在年龄上与其他两个类别不同。2
为了验证我们的结果,这里是使用 R 进行的 ANOVA 表:
## Df Sum Sq Mean Sq F value Pr(>F)
## class 2 128 64.00 7.784 0.0109 *
## Residuals 9 74 8.22
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
我们手动得出了相同的结果,但请注意,在 R 中,是计算 p 值而不是将 Fobs 与临界值进行比较。p 值可以根据 Fobs 和自由度在 R 中轻松找到:
pf(7.78, 2, 9,
lower.tail = FALSE
)
## [1] 0.010916
结论
感谢阅读。
我希望这篇文章帮助你手动进行单因素 ANOVA。如果你想学习如何在 R 中进行,请查看这个教程。
一如既往,如果你对本文所讨论的主题有任何问题或建议,请在评论中添加,以便其他读者可以从讨论中受益。
-
在这种情况下,Student’s t 检验 通常比 ANOVA 更受欢迎,尽管两种检验都会得出完全相同的结论。↩︎
-
记住,ANOVA 不能告诉你哪个组在定量因变量上与其他组不同,也不能告诉你它们是否都不同或只有一个不同。要回答这个问题,需要进行事后检验。这超出了本文的范围,但可以在 R 中轻松完成(参见这个教程)。↩︎
相关文章
最初发布于 https://statsandr.com 于 2023 年 8 月 30 日。
如何优化多维 Numpy 数组操作的 Numexpr
快速计算
Numpy 性能优化的实际案例研究
·发布于 Towards Data Science ·阅读时间 5 分钟·2023 年 10 月 22 日
–
如何使用 Numexpr 优化多维 Numpy 数组操作。图片来源:作者创建,Canva。
这是一篇相对简短的文章。在其中,我将以一个实际场景为例,解释如何在多维 Numpy 数组中使用Numexpr 表达式以实现显著的性能提升。
目前关于如何在多维 Numpy 数组中使用 Numexpr 及其表达式的文章不多,因此希望这篇文章能对你有所帮助。
介绍
最近,在回顾一些旧工作时,我偶然发现了这段代码:
def predict(X, w, b):
z = np.dot(X, w)
y_hat = sigmoid(z)
y_pred = np.zeros((y_hat.shape[0], 1))
for i in range(y_hat.shape[0]):
if y_hat[i, 0] < 0.5:
y_pred[i, 0] = 0
else:
y_pred[i, 0] = 1
return y_pred
这段代码将预测结果从概率转换为机器学习中逻辑回归模型的 0 或 1 分类结果。
但天哪,谁会用for loop
来遍历 Numpy ndarray?
你可以预见,当数据量达到一定程度时,它不仅会占用大量内存,而且性能也会变差。
是的,这段代码是我年轻时写的。
带着责任感,我计划今天使用 Numexpr 库重写这段代码。
在这个过程中,我将展示如何在多维 Numpy 数组中使用 Numexpr 及其where
表达式,以实现显著的性能提升。
代码实现
如果你不熟悉 Numexpr 的基本用法,可以参考这篇文章:
使用 Python 的 Numexpr 和 Pandas 的 eval/query 函数提升数据分析性能
[towardsdatascience.com
本文使用实际案例演示了 Numexpr 在 Numpy 和 Pandas 中的 API 和表达式的具体用法。
where(bool, number1, number2): number
- 如果布尔条件为真,则为 number1,否则为 number2。
上述是 Numexpr 中 where 表达式的用法。
在处理矩阵数据时,你可能习惯使用 Pandas 的 DataFrame
。但由于 Pandas 的 eval
方法不支持 where
表达式,你只能选择在多维 Numpy ndarray 中使用 Numexpr。
不用担心,我会马上为你解释。
在开始之前,我们需要导入必要的包并实现一个 generate_ndarray
方法,用于生成特定大小的 ndarray 进行测试:
from typing import Callable
import time
import numpy as np
import numexpr as ne
import matplotlib.pyplot as plt
rng = np.random.default_rng(seed=4000)
def generate_ndarray(rows: int) -> np.ndarray:
result_array = rng.random((rows, 1))
return result_array
首先,我们生成一个 200 行的矩阵,以查看它是否是我们想要的测试数据:
In: arr = generate_ndarray(200)
print(f"The dimension of this array: {arr.ndim}")
print(f"The shape of this array: {arr.shape}")
Out: The dimension of this array: 2
The shape of this array: (200, 1)
为了接近实际的逻辑回归模型情况,我们生成一个形状为 (200, 1)
的 ndarray。当然,你也可以根据需要测试其他形状的 ndarray。
接着,我们开始编写 Numexpr 在 numexpr_to_binary method
中的具体用法:
-
首先,我们使用索引来分隔需要处理的列。
-
然后,使用 Numexpr 的 where 表达式来处理这些值。
-
最后,将处理过的列与其他列合并,以生成所需结果。
由于 ndarray 的形状是 (200, 1)
,只有一列,因此我添加了一个新维度。
代码如下:
def numexpr_to_binary(np_array: np.ndarray) -> np.ndarray:
temp = np_array[:, 0]
temp = ne.evaluate("where(temp<0.5, 0, 1)")
return temp[:, np.newaxis]
我们可以用一个 10 行的数组测试结果,以查看是否符合预期:
arr = generate_ndarray(10)
result = numexpr_to_binary(arr)
mapping = np.column_stack((arr, result))
mapping
我测试了一个 10 行的数组,结果是我想要的。图片由作者提供
看,匹配是正确的。我们的任务已完成。
整个过程可以通过以下图示演示:
Numexpr 如何转换多维 ndarray 的整个过程。图片由作者提供
性能比较
在代码实现后,我们需要将 Numexpr 实现版本与之前的 for each
实现版本进行比较,以确认性能是否有所提升。
首先,我们实现一个 numexpr_example
方法。该方法基于 Numexpr 的实现:
def numexpr_example(rows: int) -> np.ndarray:
orig_arr = generate_ndarray(rows)
the_result = numexpr_to_binary(orig_arr)
return the_result
接着,我们需要补充一个 for_loop_example
方法。该方法参考了我需要重写的原始代码,并用作性能基准:
def for_loop_example(rows: int) -> np.ndarray:
the_arr = generate_ndarray(rows)
for i in range(the_arr.shape[0]):
if the_arr[i][0] < 0.5:
the_arr[i][0] = 0
else:
the_arr[i][0] = 1
return the_arr
然后,我编写了一个测试方法 time_method
。该方法将分别生成从 10 到 10 的 9 次方行的数据,调用相应的方法,并最终保存不同数据量所需的时间:
def time_method(method: Callable):
time_dict = dict()
for i in range(9):
begin = time.perf_counter()
rows = 10 ** i
method(rows)
end = time.perf_counter()
time_dict[i] = end - begin
return time_dict
我们分别测试 numexpr 版本和 for_loop
版本,并使用 matplotlib
绘制不同数据量所需的时间:
t_m = time_method(for_loop_example)
t_m_2 = time_method(numexpr_example)
plt.plot(t_m.keys(), t_m.values(), c="red", linestyle="solid")
plt.plot(t_m_2.keys(), t_m_2.values(), c="green", linestyle="dashed")
plt.legend(["for loop", "numexpr"])
plt.xlabel("exponent")
plt.ylabel("time")
plt.show()
Numexpr 版本的实现具有显著的性能提升。图片来源:作者
可以看出,当数据行数大于 10 的 6 次方时,Numexpr 版本的实现具有显著的性能提升。
结论
在前一篇文章中解释了 Numexpr 的基本用法,本篇文章通过实际工作中的具体示例,说明如何使用 Numexpr 重写现有代码以获得性能提升。
本文主要使用了 Numexpr 的两个特性:
-
Numexpr 允许以矢量化的方式进行计算。
-
在 Numexpr 的计算过程中,不会生成新的数组,从而显著减少内存使用。
感谢阅读。如果你有其他解决方案,请随时留言与我讨论。
感谢阅读我的故事。
你可以订阅以获取我最新的数据科学故事。
如果你有任何问题,可以在LinkedIn或Twitter(X)找到我。
本文最初发布在Data Leads Future。
如何优化特定领域的目标检测模型
设计更好更快的模型以解决你的特定问题
Alvaro Leandro Cavalcante Carneiro
·
关注 发表在 Towards Data Science ·13 min read·2023 年 10 月 19 日
–
图片由 paolo candelo 提供,来源于 Unsplash
目标检测在从学术界到工业领域的不同领域中被广泛使用,因为它能够以低计算成本提供良好的结果。然而,尽管有许多开源架构可公开获取,大多数模型设计用于解决通用问题,可能不适用于特定的上下文。
举个例子,我们可以提到COCO 数据集,这是在这一领域中通常作为研究基线的数据集,对模型的超参数和架构细节产生影响。该数据集包含 90 个不同的类别,涵盖了各种光照条件、背景和大小。事实证明,有时你面临的检测问题相对简单。你可能只需检测几个不同的对象,而没有太多的场景或大小变化。在这种情况下,如果你使用通用的超参数集来训练你的模型,你可能会得到一个产生不必要计算成本的模型。
从这个角度来看,本文的主要目标是提供优化各种目标检测模型以处理较简单任务的指导。我希望帮助你选择一个更高效的配置,以减少计算成本而不影响平均精度(mAP)。
提供一些背景信息
我的硕士学位目标之一是开发一个手语识别系统,以最小的计算要求为前提。该系统的一个关键组成部分是预处理阶段,包括检测翻译员的手和脸,如下图所示:
这是本研究中创建的 HFSL 数据集的样本。图片作者提供。
如图所示,这个问题相对简单,只涉及两类不同的对象和图像中同时出现的三种对象。因此,我的目标是优化模型的超参数,以保持高的 mAP,同时减少计算成本,从而使其能够在智能手机等边缘设备上高效执行。
目标检测架构和设置
在这个项目中,测试了以下目标检测架构:EfficientDetD0、Faster-RCNN、SDD320、SDD640 和 YoloV7。然而,这里提出的概念可以应用于适应各种其他架构。
在模型开发方面,我主要使用了 Python 3.8 和 TensorFlow 框架,除了 YoloV7 使用了 PyTorch。虽然这里提供的大多数示例与 TensorFlow 相关,但你可以将这些原则适应于你所选择的框架。
在硬件方面,测试使用了 RTX 3060 GPU 和 Intel Core i5–10400 CPU。所有源代码和模型都可以在GitHub上找到。
目标检测器的微调
使用 TensorFlow 进行目标检测时,了解所有超参数都存储在一个名为“pipeline.config”的文件中是至关重要的。这个 protobuf 文件保存了用于训练和评估模型的配置,你可以在任何从TF 模型库下载的预训练模型中找到它。在这个背景下,我将描述我在管道文件中实施的修改,以优化目标检测器。
需要注意的是,这里提供的超参数是专门为手部和面部检测(2 类,3 对象)设计的。务必根据你自己的问题领域进行调整。
一般简化
对所有模型可以应用的第一个变化是将每个类别的最大预测数量和生成的边界框数量从 100 分别减少到 2 和 4。你可以通过调整“max_number_of_boxes”属性来实现这一点,该属性位于“train_config”对象中:
...
train_config {
batch_size: 128
sync_replicas: true
optimizer { ... }
fine_tune_checkpoint: "PATH_TO_BE_CONFIGURED"
num_steps: 50000
startup_delay_steps: 0.0
replicas_to_aggregate: 8
max_number_of_boxes: 4 # <------------------ change this line
unpad_groundtruth_tensors: false
fine_tune_checkpoint_type: "classification"
fine_tune_checkpoint_version: V2
}
...
之后,更改“max_total_detections” 和 “max_detections_per_class”,它们位于目标检测器的“post_processing”中:
post_processing {
batch_non_max_suppression {
score_threshold: 9.99999993922529e-09
iou_threshold: 0.6000000238418579
max_detections_per_class: 2 # <------------------ change this line
max_total_detections: 4 # <------------------ change this line
use_static_shapes: false
}
score_converter: SIGMOID
}
这些变化很重要,特别是对我来说,因为图像中同时出现的对象只有三个且类别只有两个。通过减少预测次数,消除重叠边界框所需的迭代次数也减少了,这通过非极大值抑制 (NMS) 实现。因此,如果你需要检测的类别和场景中的对象数量有限,改变这个超参数可能是个好主意。
额外的调整是逐个应用的,考虑了每个目标检测模型的具体架构细节。
单次检测多框 (SSD)
在进行目标检测时,测试不同的分辨率总是一个好主意。在这个项目中,我使用了两个版本的模型,SSD320 和 SSD640,分别具有 320x320 和 640x640 像素的输入图像分辨率。
对于这两个模型,主要的修改之一是将特征金字塔网络 (FPN) 的深度从 5 减少到 4,通过移除最表层。FPN 是一个强大的特征提取机制,能够处理多种特征图尺寸。然而,对于较大的对象,最表层是为较高图像分辨率设计的,可能并不必要。也就是说,如果你要检测的对象不太小,移除这一层可能是个好主意。为了实现这一变化,将“min_level”属性从 3 调整到 4,位于“fpn”对象中:
...
feature_extractor {
type: "ssd_mobilenet_v2_fpn_keras"
depth_multiplier: 1.0
min_depth: 16
conv_hyperparams {
regularizer { ... }
initializer { ... }
activation: RELU_6
batch_norm {...}
}
use_depthwise: true
override_base_feature_extractor_hyperparams: true
fpn {
min_level: 4 # <------------------ change this line
max_level: 7
additional_layer_depth: 108 # <------------------ change this line
}
}
...
我还通过将“additional_layer_depth”从 128 减少到 108 简化了高分辨率模型(SSD640)。同样,我将两个模型的“multiscale_anchor_generator”深度从 5 调整到 4 层,如下所示:
...
anchor_generator {
multiscale_anchor_generator {
min_level: 4 # <------------------ change this line
max_level: 7
anchor_scale: 4.0
aspect_ratios: 1.0
aspect_ratios: 2.0
aspect_ratios: 0.5
scales_per_octave: 2
}
}
...
最后,生成边界框预测的网络(“box_predictor”)的层数从 4 减少到 3。关于 SSD640,边框预测的深度也从 128 减少到 96,如下所示:
...
box_predictor {
weight_shared_convolutional_box_predictor {
conv_hyperparams {
regularizer { ... }
initializer { ... }
activation: RELU_6
batch_norm { ... }
}
depth: 96 # <------------------ change this line
num_layers_before_predictor: 3 # <------------------ change this line
kernel_size: 3
class_prediction_bias_init: -4.599999904632568
share_prediction_tower: true
use_depthwise: true
}
}
...
这些简化是因为我们拥有的不同类别数量有限,且模式相对简单。因此,可以减少模型的层数和深度,因为即使特征图较少,我们仍能有效地从图像中提取所需的特征。
EfficientDet-D0
关于 EfficientDet-D0,我将双向特征金字塔网络 **(Bi-FPN)**的深度从 5 减少到 4。此外,我将 Bi-FPN 的迭代次数从 3 减少到 2,将特征图内核从 64 减少到 48。Bi-FPN 是一种复杂的多尺度特征融合技术,可以产生出色的结果。然而,它也需要较高的计算资源,对于简单的问题,这可能会浪费资源。要实施上述调整,只需按如下方式更新“bifpn”对象的属性:
...
bifpn {
min_level: 4 # <------------------ change this line
max_level: 7
num_iterations: 2 # <------------------ change this line
numyaml_filters: 48 # <------------------ change this line
}
...
除此之外,还需要像处理 SSD 时一样,减少“multiscale_anchor_generator”的深度。最后,我将边框预测网络的层数从 3 减少到 2:
...
box_predictor {
weight_shared_convolutional_box_predictor {
conv_hyperparams {
regularizer { ... }
initializer { ... }
activation: SWISH
batch_norm { ... }
force_use_bias: true
}
depth: 64
num_layers_before_predictor: 2 # <------------------ change this line
kernel_size: 3
class_prediction_bias_init: -4.599999904632568
use_depthwise: true
}
}
...
Faster R-CNN
Faster R-CNN 模型依赖于区域建议网络(RPN)和锚框作为其主要技术。锚框是一个滑动窗口的中心点,它在骨干 CNN 的最后特征图上迭代。每次迭代时,分类器会确定一个建议中是否包含物体的概率,而回归器则调整边界框坐标。为了确保检测器具有平移不变性,它使用三种不同的尺度和三种宽高比的锚框,这增加了每次迭代的建议数量。
虽然这是一个简单的解释,但显而易见,由于其两阶段检测过程,这个模型比其他模型复杂得多。然而,可以简化它并提高其速度,同时保持高精度。
为此,第一个重要的修改是将生成的建议数量从 300 减少到 50。这一减少是可行的,因为图像中同时存在的物体数量较少。你可以通过调整“first_stage_max_proposals”属性来实现这一变化,如下所示:
...
first_stage_box_predictor_conv_hyperparams {
op: CONV
regularizer { ... }
initializer { ... }
}
first_stage_nms_score_threshold: 0.0
first_stage_nms_iou_threshold: 0.7
first_stage_max_proposals: 50 # <------------------ change this line
first_stage_localization_loss_weight: 2.0
first_stage_objectness_loss_weight: 1.0
initial_crop_size: 14
maxpool_kernel_size: 2
maxpool_stride: 2
...
之后,我从模型中移除了最大的锚框尺度 (2.0)。这个改变是因为手部和面部由于解释器与相机的固定距离而保持一致的大小,大锚框可能对提议生成没有用。此外,我移除了一个锚框的长宽比,因为我的物体在数据集中具有类似的形状,变化很小。下面的调整以视觉形式展示:
first_stage_anchor_generator {
grid_anchor_generator {
scales: [0.25, 0.5, 1.0] # <------------------ change this line
aspect_ratios: [0.5, 1.0] # <------------------ change this line
height_stride: 16
width_stride: 16
}
}
也就是说,考虑目标物体的尺寸和长宽比至关重要。这种考虑可以帮助你排除不太有用的锚框,并显著减少模型的计算成本。
YoloV7
相比之下,对 YoloV7 应用的更改很少,以保留架构的功能性。主要的修改是简化了负责特征提取的 CNN,包括骨干网和模型的头部。为此,我减少了几乎每个卷积层的内核/特征图数量,创建了以下模型:
backbone:
# [from, number, module, args]
[[-1, 1, Conv, [22, 3, 1]], # 0
[-1, 1, Conv, [44, 3, 2]], # 1-P1/2
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [89, 3, 2]], # 3-P2/4
[-1, 1, Conv, [44, 1, 1]],
[-2, 1, Conv, [44, 1, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[[-1, -3, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]], # 11
[-1, 1, MP, []],
[-1, 1, Conv, [89, 1, 1]],
[-3, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [89, 3, 2]],
[[-1, -3], 1, Concat, [1]], # 16-P3/8
[-1, 1, Conv, [89, 1, 1]],
[-2, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[[-1, -3, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [512, 1, 1]], # 24
[-1, 1, MP, []],
[-1, 1, Conv, [89, 1, 1]],
[-3, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [89, 3, 2]],
[[-1, -3], 1, Concat, [1]], # 29-P4/16
[-1, 1, Conv, [89, 1, 1]],
[-2, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[[-1, -3, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [716, 1, 1]], # 37
[-1, 1, MP, []],
[-1, 1, Conv, [256, 1, 1]],
[-3, 1, Conv, [256, 1, 1]],
[-1, 1, Conv, [256, 3, 2]],
[[-1, -3], 1, Concat, [1]], # 42-P5/32
[-1, 1, Conv, [128, 1, 1]],
[-2, 1, Conv, [128, 1, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[[-1, -3, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [716, 1, 1]], # 50
]
# yolov7 head
head:
[[-1, 1, SPPCSPC, [358]], # 51
[-1, 1, Conv, [179, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[37, 1, Conv, [179, 1, 1]], # route backbone P4
[[-1, -2], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]],
[-2, 1, Conv, [179, 1, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[[-1, -2, -3, -4, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]], # 63
[-1, 1, Conv, [89, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[24, 1, Conv, [89, 1, 1]], # route backbone P3
[[-1, -2], 1, Concat, [1]],
[-1, 1, Conv, [89, 1, 1]],
[-2, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[-1, 1, Conv, [44, 3, 1]],
[[-1, -2, -3, -4, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [89, 1, 1]], # 75
[-1, 1, MP, []],
[-1, 1, Conv, [89, 1, 1]],
[-3, 1, Conv, [89, 1, 1]],
[-1, 1, Conv, [89, 3, 2]],
[[-1, -3, 63], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]],
[-2, 1, Conv, [179, 1, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[-1, 1, Conv, [89, 3, 1]],
[[-1, -2, -3, -4, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]], # 88
[-1, 1, MP, []],
[-1, 1, Conv, [179, 1, 1]],
[-3, 1, Conv, [179, 1, 1]],
[-1, 1, Conv, [179, 3, 2]],
[[-1, -3, 51], 1, Concat, [1]],
[-1, 1, Conv, [179, 1, 1]],
[-2, 1, Conv, [179, 1, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[[-1, -2, -3, -4, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [358, 1, 1]], # 101
[75, 1, RepConv, [179, 3, 1]],
[88, 1, RepConv, [358, 3, 1]],
[101, 1, RepConv, [716, 3, 1]],
[[102,103,104], 1, IDetect, [nc, anchors]], # Detect(P3, P4, P5)
]
如前所述,从检测器中移除一些层和特征图通常是处理简单问题的好方法,因为特征提取器最初是设计用于检测数十种甚至数百种不同类别,在多种场景中需要更强大的模型来应对这些复杂性并确保高准确性。
通过这些调整,我将参数数量从 3640 万减少到仅 1410 万,减少约 61%。此外,我使用了 512x512 像素的输入分辨率,而不是原论文中建议的 640x640 像素。
额外提示
另一个在目标检测训练中的宝贵提示是利用 Kmeans 模型进行锚框比例的无监督调整,将图形的宽度和高度调整到最大化训练集中的交并比 (IoU) 比例。通过这样做,我们可以更好地适应给定的问题领域,从而通过以合适的长宽比开始,提升模型收敛。下图展示了这个过程,将 SSD 算法中默认使用的三个锚框(红色)与用于手部和面部检测任务的三个优化比例的框(绿色)进行比较。
比较不同边界框的长宽比。图片由作者提供。
展示结果
我使用自己的数据集,即手部和面部手语(HFSL)数据集,训练和评估了每个检测器,考虑了 mAP 和每秒帧数(FPS)作为主要指标。下表提供了结果的总结,表中的值(括号中的数字)表示在实施任何描述的优化之前检测器的 FPS。
目标检测结果。
我们可以观察到,大多数模型在保持各个层次的交并比(IoU)高 mAP 的同时,推理时间显著减少。更复杂的架构,如 Faster R-CNN 和 EfficientDet,分别将 GPU 上的 FPS 提高了 200.80%和 231.78%。即使是基于 SSD 的架构,其性能也有了巨大的提升,640 版本和 320 版本的性能分别提高了 280.23%和 159.59%。考虑到 YoloV7,尽管在 CPU 上的 FPS 差异最为明显,但优化后的模型参数减少了 61%,降低了内存需求,使其更适合边缘设备。
结论
在计算资源有限或任务必须快速执行的情况下,我们可以进一步优化开源目标检测模型,以寻找能够减少计算需求而不影响结果的超参数组合,从而为各种问题领域提供合适的解决方案。
我希望这篇文章能帮助你在训练目标检测器时做出更好的选择,从而在付出最小努力的情况下实现显著的效率提升。如果你对某些解释的概念不太理解,我建议你深入了解你的目标检测架构是如何工作的。此外,考虑尝试不同的超参数值,以根据你所解决的具体问题进一步优化你的模型!
如何通过自定义 PyTorch 操作符优化你的深度学习数据输入管道
PyTorch 模型性能分析与优化 — 第五部分
·
查看 发表在 Towards Data Science · 6 分钟阅读 · 2023 年 8 月 31 日
–
图片由 Alexander Grey 提供,发布在 Unsplash
这篇文章是关于基于 GPU 的 PyTorch 工作负载性能分析和优化系列文章中的第五篇,直接续接了第四部分。在第四部分中,我们演示了如何使用PyTorch Profiler和TensorBoard来识别、分析和解决 DL 训练工作负载的数据预处理管道中的性能瓶颈。在这篇文章中,我们讨论了 PyTorch 对创建自定义操作符的支持,并演示了它如何帮助我们解决数据输入管道中的性能瓶颈,加速 DL 工作负载,并降低训练成本。感谢伊扎克·莱维和吉拉德·瓦瑟曼对本文章的贡献。与本篇文章相关的代码可以在这个 GitHub 仓库中找到。
构建 PyTorch 扩展
PyTorch 提供了多种创建自定义操作的方法,包括通过自定义模块扩展 torch.nn 和/或自定义函数。在这篇文章中,我们关注 PyTorch 对集成自定义 C++ 代码的支持。这一功能之所以重要,是因为某些操作在 C++ 中实现的效率和/或简便性可能远超 Python。通过指定的 PyTorch 工具,如CppExtension,这些操作可以轻松地作为 PyTorch 的“扩展”进行集成,而无需拉取和重新编译整个 PyTorch 代码库。有关这一功能的动机以及如何使用它的详细信息,请参见官方 PyTorch 自定义 C++ 和 CUDA 扩展教程。由于我们在本文中关注的是加速基于 CPU 的数据预处理管道,因此我们将仅使用 C++ 扩展,不需要 CUDA 代码。在未来的文章中,我们希望展示如何使用这一功能实现自定义 CUDA 扩展,以加速在 GPU 上运行的训练代码。
示例
在我们之前的文章中,我们定义了一个数据输入管道,从解码一个533x800 JPEG 图像开始,然后提取一个随机的256x256裁剪区域,经过一些额外的变换后,送入训练循环。我们使用了PyTorch Profiler和TensorBoard来测量从文件加载图像的时间,并承认了解码的浪费。为了完整性,我们在下面复制了代码:
import numpy as np
from PIL import Image
from torchvision.datasets.vision import VisionDataset
input_img_size = [533, 800]
img_size = 256
class FakeDataset(VisionDataset):
def __init__(self, transform):
super().__init__(root=None, transform=transform)
size = 10000
self.img_files = [f'{i}.jpg' for i in range(size)]
self.targets = np.random.randint(low=0,high=num_classes,
size=(size),dtype=np.uint8).tolist()
def __getitem__(self, index):
img_file, target = self.img_files[index], self.targets[index]
img = Image.open(img_file)
if self.transform is not None:
img = self.transform(img)
return img, target
def __len__(self):
return len(self.img_files)
transform = T.Compose(
[T.PILToTensor(),
T.RandomCrop(img_size),
RandomMask(),
ConvertColor(),
Scale()])
回顾我们之前的文章,我们达到的优化平均步骤时间是0.72 秒。可以推测,如果我们能够仅解码感兴趣的裁剪区域,我们的管道将运行得更快。不幸的是,截至本文写作时,PyTorch 并没有包含支持这一功能的函数。然而,利用自定义操作符创建工具,我们可以定义并实现自己的函数!
自定义 JPEG 图像解码和裁剪函数
libjpeg-turbo库是一个 JPEG 图像编解码器,相比于libjpeg,包含了许多增强和优化功能。特别是,libjpeg-turbo包括许多功能,使我们能够仅解码图像中的预定义裁剪区域,例如jpeg_skip_scanlines和jpeg_crop_scanline。如果你在 conda 环境中运行,可以使用以下命令进行安装:
conda install -c conda-forge libjpeg-turbo
请注意,libjpeg-turbo已预装在我们将在下面实验中使用的官方AWS PyTorch 2.0 深度学习 Docker 镜像中。
在下面的代码块中,我们修改了decode_jpeg函数,来自torchvision 0.15,以解码并返回来自输入 JPEG 编码图像的请求裁剪。
torch::Tensor decode_and_crop_jpeg(const torch::Tensor& data,
unsigned int crop_y,
unsigned int crop_x,
unsigned int crop_height,
unsigned int crop_width) {
struct jpeg_decompress_struct cinfo;
struct torch_jpeg_error_mgr jerr;
auto datap = data.data_ptr<uint8_t>();
// Setup decompression structure
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = torch_jpeg_error_exit;
/* Establish the setjmp return context for my_error_exit to use. */
setjmp(jerr.setjmp_buffer);
jpeg_create_decompress(&cinfo);
torch_jpeg_set_source_mgr(&cinfo, datap, data.numel());
// read info from header.
jpeg_read_header(&cinfo, TRUE);
int channels = cinfo.num_components;
jpeg_start_decompress(&cinfo);
int stride = crop_width * channels;
auto tensor =
torch::empty({int64_t(crop_height), int64_t(crop_width), channels},
torch::kU8);
auto ptr = tensor.data_ptr<uint8_t>();
unsigned int update_width = crop_width;
jpeg_crop_scanline(&cinfo, &crop_x, &update_width);
jpeg_skip_scanlines(&cinfo, crop_y);
const int offset = (cinfo.output_width - crop_width) * channels;
uint8_t* temp = nullptr;
if(offset > 0) temp = new uint8_t[cinfo.output_width * channels];
while (cinfo.output_scanline < crop_y + crop_height) {
/* jpeg_read_scanlines expects an array of pointers to scanlines.
* Here the array is only one element long, but you could ask for
* more than one scanline at a time if that's more convenient.
*/
if(offset>0){
jpeg_read_scanlines(&cinfo, &temp, 1);
memcpy(ptr, temp + offset, stride);
}
else
jpeg_read_scanlines(&cinfo, &ptr, 1);
ptr += stride;
}
if(offset > 0){
delete[] temp;
temp = nullptr;
}
if (cinfo.output_scanline < cinfo.output_height) {
// Skip the rest of scanlines, required by jpeg_destroy_decompress.
jpeg_skip_scanlines(&cinfo,
cinfo.output_height - crop_y - crop_height);
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
return tensor.permute({2, 0, 1});
}
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("decode_and_crop_jpeg",&decode_and_crop_jpeg,"decode_and_crop_jpeg");
}
完整的 C++ 文件可以在这里找到。
在接下来的部分中,我们将按照 PyTorch 教程中的步骤,将其转换为一个 PyTorch 操作符,以便在我们的预处理管道中使用。
部署 PyTorch 扩展
如PyTorch 教程中所述,有多种方式来部署自定义操作符。设计部署时需要考虑许多因素。以下是我们认为重要的一些例子:
-
即时编译:为了确保我们的 C++扩展与我们训练时使用的 PyTorch 版本一致,我们编程部署脚本在训练环境内训练前编译代码。
-
多进程支持:部署脚本必须支持我们的 C++扩展从多个进程(例如多个 DataLoader 工作者)加载的可能性。
-
托管训练支持:由于我们经常在托管训练环境(如Amazon SageMaker)中进行训练,因此我们要求部署脚本支持此选项。(有关自定义托管训练环境的更多信息,请参见这里。)
在下面的代码块中,我们定义了一个简单的setup.py脚本,用于编译和安装我们的自定义函数,如这里所述。
from setuptools import setup
from torch.utils import cpp_extension
setup(name='decode_and_crop_jpeg',
ext_modules=[cpp_extension.CppExtension('decode_and_crop_jpeg',
['decode_and_crop_jpeg.cpp'],
libraries=['jpeg'])],
cmdclass={'build_ext': cpp_extension.BuildExtension})
我们将 C++文件和setup.py脚本放在一个名为custom_op的文件夹中,并定义一个*init.py*,以确保安装脚本只运行一次且由单个进程执行。
import os
import sys
import subprocess
import shlex
import filelock
p_dir = os.path.dirname(__file__)
with filelock.FileLock(os.path.join(pkg_dir, f".lock")):
try:
from custom_op.decode_and_crop_jpeg import decode_and_crop_jpeg
except ImportError:
install_cmd = f"{sys.executable} setup.py build_ext --inplace"
subprocess.run(shlex.split(install_cmd), capture_output=True, cwd=p_dir)
from custom_op.decode_and_crop_jpeg import decode_and_crop_jpeg
最后,我们修订了数据输入管道,使用我们新创建的自定义函数:
from torchvision.datasets.vision import VisionDataset
input_img_size = [533, 800]
class FakeDataset(VisionDataset):
def __init__(self, transform):
super().__init__(root=None, transform=transform)
size = 10000
self.img_files = [f'{i}.jpg' for i in range(size)]
self.targets = np.random.randint(low=0,high=num_classes,
size=(size),dtype=np.uint8).tolist()
def __getitem__(self, index):
img_file, target = self.img_files[index], self.targets[index]
with torch.profiler.record_function('decode_and_crop_jpeg'):
import random
from custom_op.decode_and_crop_jpeg import decode_and_crop_jpeg
with open(img_file, 'rb') as f:
x = torch.frombuffer(f.read(), dtype=torch.uint8)
h_offset = random.randint(0, input_img_size[0] - img_size)
w_offset = random.randint(0, input_img_size[1] - img_size)
img = decode_and_crop_jpeg(x, h_offset, w_offset,
img_size, img_size)
if self.transform is not None:
img = self.transform(img)
return img, target
def __len__(self):
return len(self.img_files)
transform = T.Compose(
[RandomMask(),
ConvertColor(),
Scale()])
结果
根据我们描述的优化步骤,我们的步骤时间从 0.72 秒降到了 0.48 秒(提高了 50%)!显然,我们优化的影响与原始 JPEG 图像的大小和我们选择的裁剪大小直接相关。
摘要
数据预处理管道中的瓶颈是常见的现象,可能导致 GPU 资源不足并减慢训练。考虑到潜在的成本影响,拥有多种工具和技术来分析和解决这些问题是至关重要的。在本文中,我们回顾了通过创建自定义 C++ PyTorch 扩展来优化数据输入管道的选项,展示了其易用性,并说明了其潜在影响。当然,这种优化机制的潜在收益会根据项目和性能瓶颈的细节而大相径庭。
这里讨论的优化技术加入了我们在许多博客文章中讨论的广泛的输入管道优化方法。我们鼓励你查看这些文章(例如,从这里开始)。
接下来做什么? 在我们关于 PyTorch 性能分析和优化系列的第六部分中,我们探讨了分析性能问题中较复杂的类型——训练步骤的反向传播过程中的瓶颈。