投机采样 — 直观且全面的解释
原文:
towardsdatascience.com/speculative-sampling-intuitively-and-exhaustively-explained-2daca347dbb9
机器学习 | 自然语言处理 | 数据科学
探索加速语言模型 3 倍的替代策略
·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 12 月 15 日
–
“投机者”由 Daniel Warfield 使用 MidJourney 和 Affinity Design 2 制作。所有图像均由作者提供,除非另有说明。
在本文中,我们将讨论“投机采样”,这是一种使文本生成更快、更经济的策略,同时不妨碍性能。为此,我们将深入探讨语言模型的一些更微妙的方面。
使用投机采样在各种文本生成任务中的实证结果。请注意,在所有情况下,生成时间都显著更快。 来源
首先,我们将讨论一个拖慢现代语言模型的主要问题,然后建立对投机采样如何优雅加速它们的直观理解,最后我们将用 Python 从头实现投机采样。
这对谁有用? 对自然语言处理(NLP)或前沿人工智能进展感兴趣的任何人。
这篇文章的难度如何? 本文中的概念对机器学习爱好者来说是可以接受的,同时足够前沿,能引起经验丰富的数据科学家的兴趣。文末的代码可能对开发者有用。
前提条件: 了解变换器、OpenAI 的 GPT 模型或两者之一可能会有帮助。如果你感到困惑,可以参考这两篇文章:
探索 OpenAI 生成预训练变换器的架构。
towardsdatascience.com ## 变换器 — 直观且详尽的解释
探索现代机器学习的潮流:一步步拆解变换器
towardsdatascience.com
语言模型变得过于庞大
在过去四年中,OpenAI 的 GPT 模型从 2018 年的 1.17 亿参数增长到 2023 年估计的 1.8 万亿参数。这一快速增长在很大程度上归因于在语言建模中,更大的模型效果更好。
模型规模与性能的图表,显示出更大的模型效果更好。来自 我关于 GPT 的文章。 原始来源
因此,过去几年中,行业变成了一场军备竞赛。众多公司在炫酷的显卡上投入了数十亿美元,这让 Fortnite 玩家感到不满。
问题在于,这些模型变得过于庞大。像 ChatGPT 使用的语言模型,需要通过一种称为“自回归生成”的过程逐字生成响应。模型越大,生成逐字输出所需的金钱和时间就越多。
像 GPT 这样的解码器单模型,通过逐步构建输出。模型接受输入“翻译成法语:我是一个经理”,并通过将之前的输出作为输入的一部分,逐字生成响应。这种文本生成方式称为“自回归生成”。 来自我关于 GPT 的文章
OpenAI 的 GPT-4,基于某人在 Twitter 上的泄露信息,使用了一些技术来绕过这个问题。其中之一,就是本文的主题——投机采样。
投机采样简要概述
投机采样(也称为“投机解码”或“投机生成”)在两篇论文中同时被提出,两者都建议通过投机采样将文本生成速度提高约 3 倍:
-
“加速大型语言模型解码的投机采样”,这是 DeepMind 发表的一篇论文,
-
“通过投机解码实现变换器的快速推断”,这是 Google 发表的一篇论文。
尽管两种方法是独立发布的,但功能上是相同的,因此我们将它们视为同义词。
推测采样的基本思想是,较大的语言模型更好,因为有些文本生成的例子是困难的,但不是所有例子。例如,假设你问一个语言模型关于月球的地质组成。为了形成一个连贯的回答,模型不仅需要理解复杂的科学内容,还需要将“a”、“and”和“of”等词放在正确的位置。知道月球由一种叫“Breccias”的物质组成比知道“are”可能会跟在“which”之后要困难。
生成难度的概念性演示。当模型逐字预测响应序列时,一些词难以预测,因为它们需要深入的知识,而一些词则容易预测,因为可以通过简单的语法或上下文线索推断。在这个例子中,红色文本可能比蓝色文本更难预测。
推测采样利用了通过使用两个语言模型的不同难度程度的概念;一个目标模型和一个草稿模型:
-
目标模型是我们试图加速的超大、超智能模型。
-
草稿模型是一个较小、较简单且更快的模型。
这个想法是使用草稿模型来预测序列中的多个词,然后让目标模型确认所有生成的词都是好的。我们可以丢弃所有不一致的部分,从而得到一个与目标模型独立工作时输出的结果相同的输出。
一个具有推测生成的实际例子。在第一行中,草稿模型输出了“日本的基准债券”,但目标模型不同意“债券”,选择了“n”。词语“债券”被替换为“n”,草稿模型在“债券”之后可能预测的任何内容都被丢弃。实际上,这使得推测生成系统可以为目标模型的每次通过输出多个词。source
一个自然的问题
如果你和我一样,可能会感到有些困惑。常见的直觉,以及我在我的变压器文章和我的GPT 文章中传达的直觉,是语言模型逐字预测输出。在这种直觉下,目标模型如何高效地“二次检查”草稿模型的输出并不明显;如果目标模型必须逐个检查预测,那么最初使用草稿模型有什么意义呢?
如果像 GPT 这样的模型逐字输出文本,难道它不能像检查草稿模型的输出那样逐字检查吗?不会,我们将在下一节讨论原因。
推测性采样的思想需要对 Transformer 的确切输出有透彻的理解。有一些通常不相关的细节,但对于推测性采样来说非常相关。
Transformer 的秘密输出,以及推测性采样如何使用它们
正如我在我关于原始 Transformer 架构的文章中讨论的那样,Transformer 的特别之处在于它们能够并行化训练。在 Transformer 之前,像 LSTM 这样的模型必须逐字训练,这是一种缓慢且昂贵的过程。
当像 GPT 这样的模型进行训练时,会将整个输入序列提供给模型,模型被要求预测相同的序列,只是偏移了一个单词。然后,模型被训练以最小化其预测的缺陷。
语言模型(如 GPT)训练过程的一个例子。模型被给定一个输入序列,该序列向右移动一个标记以指示序列的开始,然后模型被要求在一次传递中预测相同的序列。任何错误都用于训练模型。本质上,模型被训练来预测所有下一个单词同时出现。
所以,如果模型可以访问整个输入序列,它是否会通过将每个单词移到一个空间来作弊?不会,这要归功于掩码。
Transformer 使用“掩码”自注意力,这本质上是阻止未来单词的信息到达给定单词的信息。我可能会在专门的文章中详细介绍掩码,这绝对值得深入探讨,但直观理解是:通过将自注意力机制中的某些值设为零,给定单词的预测不会受到未来单词的影响。
使用掩码的训练概念图。实际上,使用掩码时,语言模型被要求同时预测所有下一个单词。
通常,当使用 Transformer 时,我们只关心序列中下一个单词的预测;这就是我们生成文本并促使风险投资家掏钱的方式。然而,技术上讲,模型的输出对于整个序列来说,就好像序列中的下一个单词不存在一样,这是由于模型的训练方式。
基于 Transformer 的语言模型(如 GPT)的真实输出。虽然我们通常只关心最终的最后一个单词预测,但技术上它预测了序列中的所有下一个单词。
这就是目标模型如何快速检查草稿模型的众多预测的方法。如果我们将草稿模型的输出作为输入提供给目标模型,并要求目标模型预测下一个词,我们可以比较序列中每个词的预测值。如果存在差异,我们可以停在那里并使用目标模型的输出。
假设蓝色的文本是由草稿模型生成的,而目标模型不同意序列中某些用红色高亮和下划线标记的词。所有在分歧前生成的草稿都可以接受,而分歧后生成的所有文本必须被拒绝。在第一次分歧时我们使用目标模型的输出。实际上,我们通过目标模型的一次运行生成了“主要由岩石和月壤组成”。
关于这个过程的一点有趣说明。每次我们运行目标模型时,它都会预测序列中的下一个词。目标模型可能会确认草稿模型的所有预测,或者与所有预测不一致。无论如何,目标模型总是会预测一个新词。因此,在草稿模型持续输出错误信息的最坏情况下,整个系统的速度与仅使用目标模型时一样。换句话说,推测采样不会减慢生成速度,它只能使生成速度更快(至少,当它正确实现时)。
序列、标记、标记 ID、逻辑值和概率
这就是理论。在我们深入代码之前,我们应该讨论一些关于变换器如何工作的技术细节。
从语言建模的角度来看,文本被概念化为序列;一个一个接着的“元素”列表。通常这些“元素”可以被概念化为词,但实际上它们比这更抽象。
机器学习模型首先将输入序列拆分为标记,这些标记是构成序列的“元素”。这可以通过多种算法完成,但最终结果是输入序列被分割成原子级的块。这些块可能是单个词、词的一部分、多个词、标点符号、数字或空格。
一个使用名为“sentencepiece”的分词器进行标记化的示例
从分词器提取的每个标记都有一个独特的编号,称为TokenId。通常,变换器风格的模型会为每个 TokenId 学习一个代表向量,该向量随后成为模型的输入。每个 TokenId 都有一个与之关联的向量,模型在训练过程中对该向量进行优化。
相同的标记及其关联的 ID
数据经过模型内部多个自注意力轮次后,数据变成了每个输出一个的抽象向量序列。这有时被称为“最终隐藏状态”。
输入的向量与每个单词干净地对应,通过多个自注意力层。这一过程创建了高度抽象的表示。来自我关于变换器的文章。
这通过一个语言建模头传递,该头将模型的抽象表示转换为与分词器直接对应的表示。对于给定的分词器,有一个固定数量的 TokenIds,语言建模头将模型的输出转换为包含相同数量值的向量。
在变换器完成其任务后,模型的最终隐藏状态通过语言建模头,该头将数据重构为直接对应于模型所训练的分词器的格式。
这些输出被称为logits。通常,“logit”这个术语用来指代模型的未过滤、未处理的真实输出。这通常是被优化的内容。logits 通常通过 softmax 函数进行比较,将 logits 转换为概率。大 logit 值变成大概率,小 logit 值变成小概率。
logits 转换为概率的概念图。
这些概率可以被转换为令牌,然后可以用来构建输出序列。不过,有几种方法可以做到这一点。
-
你可以简单地总是选择使用最高概率的令牌。
-
你可以以加权概率的方式随机选择输出。
-
你可以采用更复杂的策略,例如“top K 采样”。
无论如何,概率变成了一个 tokenId,这个 tokenId 变成了令牌本身,从这些令牌中可以构建输出。
所以,总结一下:
-
Sequence: 这通常用来指代输入和输出文本,但也可以概念化为令牌序列、TokenIds 序列、logits 序列、概率序列等。“序列”可以根据讨论的上下文有几种含义。
-
Token: 文本可以通过分词器分割成原子令牌。这些用于将文本拆分成原子预定义的块。有时这些块与单词干净地对应,有时则不然。
-
TokenId: 每个令牌都有一个对应的 TokenId,这只是一个数字。模型使用这个数字来检索该令牌的学习向量,从而构建模型的输入。
-
Logits 和概率: 在模型完成其操作后,它会输出一系列值。这些值通常会经过 softmax 处理,从而转化为概率。这些概率用于选择输出令牌。
PyTorch 中的猜测性抽样
现在我们理解了 logits、概率和令牌,我们可以开始深入了解猜测性抽样的实际例子。
让我们保持简单:我们将使用最大 logit 值来决定每一步生成哪个令牌。如果草稿模型和目标模型都输出相同的最大值,我们将说它们达成了一致。
完整的代码可以在这里找到:
[## MLWritingAndResearch/SpeculativeSampling.ipynb at main · DanielWarfield1/MLWritingAndResearch
机器学习写作和研究中使用的笔记本示例 - MLWritingAndResearch/SpeculativeSampling.ipynb at…
加载模型
首先,我们需要一个草稿模型和一个目标模型。在这个例子中,我使用的是 T5,它代表“Text to Text Transfer Transformer”。它是一种编码器-解码器风格的变换器(就像我在这篇文章中讨论的那样),与仅解码器模型(就像我在这篇文章中讨论的那样)不同。不过,它有一个解码器,所以它可以满足我们的需求。此外,方便的是,T5 有多种尺寸,经过预训练,并且在 huggingface 上很容易获得。
"""Loading the draft model
"""
from transformers import T5Tokenizer, T5ForConditionalGeneration
#loading the draft model
draft = "google/flan-t5-large"
draft_tokenizer = T5Tokenizer.from_pretrained(draft)
draft_model = T5ForConditionalGeneration.from_pretrained(draft)
"""Loading the target model
"""
#loading the target model
target = "google/flan-t5-xl"
target_tokenizer = T5Tokenizer.from_pretrained(target)
target_model = T5ForConditionalGeneration.from_pretrained(target)
猜测性解码的整个概念依赖于草稿模型和目标模型具有相同的令牌。因此,为了双重确认,我确认了两个模型的分词器行为相似。
"""Ensuring the tokenizers are identical
in order for speculative sampling to work, tokenization for both the draft
and target model must be identical. This is a sanity check to make sure they are.
"""
#tokenizing a test sequence
tokenizer_test = "this, is, some [text] for 1234comparing, tokenizers adoihayyuz"
ex1 = target_tokenizer(prompt, return_tensors="pt").input_ids
ex2 = draft_tokenizer(prompt, return_tensors="pt").input_ids
#zero means all tokenized values are the same, so the tokenizers are
#more than likely identical
print((ex1-ex2).abs().max())
在这种情况下,“0”表示两个分词器的行为相似
构建猜测性抽样
一旦你拥有了模型,你只需要进行一些…猜测性抽样。正如之前提到的,要有效地进行猜测性抽样,你需要一个可以处理并行信息提示的完整架构。在这个例子中,我只是简单地在同一台机器上进行草稿和检查。这并不是一个非常复杂的过程,但确实有一些循环和逻辑需要实现才能使其正常工作。以下是代码:
"""Performing Speculative Sampling
"""
#initializing an empty input to feed to the decoder.
#this is updated each loop with valid generations
decoder_ids = draft_model._shift_right(draft_tokenizer("", return_tensors="pt").input_ids)
#defining input. T5 is an encoder-decoder model, so input and output are handled seperatly
input_ids = draft_tokenizer("Translate to German \n Battle not with monsters, lest ye become a monster, and if you gaze into the abyss, the abyss gazes also into you.", return_tensors="pt").input_ids
#defining the number of draft generations
k = 5
#keeps track of generation information, for later printouts
generated = []
#Generating Text
iter = 0
for _ in range(15):
print('========== Speculative Sampling Iteration {} =========='.format(iter))
iter+=1
#creating a holding place for the generated draft
decoder_ids_draft = decoder_ids.clone()
before_text = draft_tokenizer.decode(decoder_ids_draft[0])
initial_length = decoder_ids.shape[1]
#generating draft
for i in range(k):
#predicting the next token with the draft model
with torch.no_grad():
logits = draft_model(input_ids=input_ids, decoder_input_ids=decoder_ids_draft).logits
genid = torch.argmax(logits, dim=2)[0][-1]
#appending the generated id to the draft
genid = genid.expand(1,1)
decoder_ids_draft = torch.cat((decoder_ids_draft,genid),1)
print('=== Draft Generation')
current_draft = draft_tokenizer.decode(decoder_ids_draft[0])
print('generated draft tokens: {}'.format(decoder_ids_draft))
print('generated draft text: {}'.format(current_draft))
#Generating all next token predictions with the target
logits = target_model(input_ids=input_ids, decoder_input_ids=decoder_ids_draft).logits
genids = torch.argmax(logits, dim=2)[0]
print('=== Target Generation')
current_target = draft_tokenizer.decode(genids)
print('generated target tokens: {}'.format(genids))
print('generated target text: {}'.format(current_target))
#checking draft against target
for i, (dv, tv) in enumerate(zip(decoder_ids_draft[0,1:],genids[:-1])):
#target does not agree with the draft
if dv != tv:
#genids is next word, so this is done to preserve the first token
first_token = decoder_ids[0][:1]
decoder_ids = genids[:i+1]
decoder_ids = torch.cat((first_token,decoder_ids),0)
break
else:
#no disagreements
decoder_ids = genids
print('=== Validated Generation')
current_target = draft_tokenizer.decode(decoder_ids)
print('generated target tokens: {}'.format(decoder_ids))
print('generated target text: {}'.format(current_target))
#expanding dimensions so that the shape of the tensor is the same
decoder_ids = decoder_ids.expand(1,len(decoder_ids))
#logging
numgen = decoder_ids.shape[1] - initial_length
generated.append({'tokens generated': numgen, 'text before': before_text, 'text after': current_target})
一旦得出结论,我们可以观察每个循环生成了多少个令牌。在这个例子中,我们要求模型将一句名言从英语翻译成德语:
每次猜测性抽样的迭代。
如你所见,使用所选择的任务和模型,大多数迭代并没有产生有用的草稿输出。然而在某些例子中,例如第 8 和第 11,草稿模型允许系统在一次目标模型运行中有效地生成五个标记。这个例子中使用的模型相当小。我想象,当处理更大的模型时,草稿模型会更经常地发挥作用。
结论
就这样。推测性采样是一种极其优雅的方法,可以大幅度加快文本生成速度。我们使用一个小型语言模型快速生成输出,然后(通过利用训练期间掩蔽注意力的一个特性)我们可以使用大型语言模型来几乎免费地对这些工作进行双重检查。我们只保留大型模型认同的生成文本,因此最后得到的输出是相同的,只是更快。
关注以获取更多更新!
我描述了 ML 领域的论文和概念,重点是实用和直观的解释。
订阅 Daniel Warfield 的最新邮件,注册后,如果你还没有 Medium 账户,你将创建一个…
medium.com](https://medium.com/@danielwarfield1/subscribe?source=post_page-----2daca347dbb9--------------------------------)
从未预期,总是感激。通过捐赠,你使我能够投入更多时间和资源来创作更频繁和更高质量的文章。了解更多
版权声明: 本文档中的所有图片均由 Daniel Warfield 创建,除非另有说明。你可以将本帖子中的任何图片用于自己的非商业用途,只要你引用了这篇文章,danielwarfield.dev
,或两者都引用。
使用 LLMs 为你的移动应用提供语音和自然语言输入
如何利用 OpenAI GPT-4 功能来导航你的 GUI
·
关注 发表在 Towards Data Science · 14 分钟阅读 · 2023 年 7 月 25 日
–
图片由 Kelly Sikkema 提供,发布于 Unsplash
介绍
大型语言模型(LLM)是一个可以有效处理自然语言的机器学习系统。目前最先进的 LLM 是 GPT-4,它为付费版 ChatGPT 提供支持。在这篇文章中,你将学习如何通过 GPT-4 功能调用,为你的应用程序提供高度灵活的语音解释,与应用程序的图形用户界面(GUI)完全协同。这篇文章旨在为产品负责人、用户体验设计师和移动开发者提供指导。
背景
移动电话(Android 和 iOS)上的数字助手未能普及,有几个原因,其中包括它们有缺陷、功能有限且使用起来往往很麻烦。LLM,特别是 OpenAI GPT-4,拥有更深入理解用户意图的潜力,而不是粗略地模式匹配口语表达,从而有可能带来改变。
Android 有 Google Assistant 的“应用操作”,iOS 有 SiriKit 意图。这些提供了简单的模板来注册你的应用可以处理的语音请求。Google Assistant 和 Siri 在过去几年中已经有了很大改进——甚至超出你的想象。然而,它们的覆盖范围在很大程度上取决于哪些应用实现了对它们的支持。尽管如此,你仍然可以通过语音在 Spotify 上播放你喜欢的歌曲。然而,这些操作系统提供的服务的自然语言解释早于 LLM 在这一领域带来的巨大进步——所以是时候迈出下一步:利用 LLM 的力量使语音输入更可靠和灵活。
尽管我们可以预期操作系统服务(如 Siri 和 Google Assistant)会很快调整策略,以利用 LLM,但我们已经可以使我们的应用程序在不受这些服务限制的情况下使用语音。一旦你掌握了本文中的概念,你的应用也将准备好接入新助手,一旦它们上线。
你选择的 LLM(GPT、PaLM、LLama2、MPT、Falcon 等)确实会影响可靠性,但你将在这里学到的核心原理可以应用于任何 LLM。我们将让用户通过一句话表达他们的需求,从而访问应用程序的全部功能。LLM 将自然语言表达映射到我们应用的导航结构和功能上的函数调用上。这不一定要像机器人一样说出一句完整的句子。LLM 的解释能力允许用户像人类一样说话,使用他们自己的词汇或语言;犹豫、犯错并纠正错误。用户之所以拒绝语音助手,是因为它们经常无法理解他们的意思,而 LLM 的灵活性可以让交互变得更加自然和可靠,从而提高用户的接受度。
为什么现在在你的应用中使用语音输入?
优点:
-
通过一句语音表达来导航到一个界面并提供所有参数
-
浅层学习曲线:用户无需找到数据在应用中的位置或如何操作 GUI
-
免提
-
互补而非不相关(如语音用户界面或 VUI):语音和 GUI 和谐工作。
-
视力障碍的可及性
-
现在:由于自然语言的解释通过 LLM 达到了一个新水平,回应更加可靠
缺点:
-
说话时的隐私
-
准确性/误解
-
仍然相对较慢
-
头脑中的知识与世界中的知识(我能说什么?):用户不知道系统理解和回答哪些口语表达
受益于语音输入的应用示例包括用于汽车或自行车驾驶辅助的应用。一般来说,当用户不能轻松使用双手时,例如在移动中、戴着手套或忙于用手工作的情况下,他们可能不愿意通过触摸精确导航应用。
购物应用也可以受益于此功能,因为用户可以用自己的话表达需求,而不是通过购物界面和设置过滤器来导航。
当将这种方法应用于提高视力障碍人士的可及性时,您可能考虑加入自然语言输出和文本转语音功能。
您的应用
下图展示了一个典型应用的导航结构,以您可能熟悉的火车旅行规划器为例。在顶部,您可以看到触摸导航的默认导航结构。该结构由导航组件控制。所有导航点击都委托给导航组件,后者执行导航操作。底部展示了我们如何利用语音输入来接入这一结构。
使用 LLM 功能调用来启用您的应用的语音功能
用户说出他们的需求,然后语音识别器将语音转换为文本。系统构建一个包含这些文本的提示并发送给 LLM。LLM 以数据的形式回应应用,告诉它哪个界面需要激活以及使用哪些参数。这个数据对象被转换为深层链接并提供给导航组件。导航组件用正确的参数激活正确的界面:在这个例子中,就是用‘阿姆斯特丹’作为参数的‘外出’界面。请注意,这只是一个简化版。我们将在下面详细说明。
许多现代应用程序在底层有一个集中式导航组件。Android 有 Jetpack Navigation,Flutter 有 Router,而 iOS 有 NavigationStack。集中式导航组件支持深度链接,这是一种技术,允许用户直接导航到移动应用中的特定屏幕,而无需经过应用的主屏幕或菜单。为了使本文中的概念有效,导航组件和集中式深度链接并非必需,但它们使实现这些概念更为简单。
深度链接涉及创建一个独特的 (URI) 路径,该路径指向应用中的特定内容或特定部分。此外,这个路径可以包含控制屏幕上深度链接所指向的 GUI 元素状态的参数。
你的应用程序的函数调用
我们通过提示工程技术指示 LLM 将自然语言表达映射到导航功能调用。提示的内容类似于:‘给定以下带参数的函数模板,将以下自然语言问题映射到这些函数模板之一并返回’。
大多数 LLM 都能做到这一点。LangChain 通过 Zero Shot ReAct Agents 有效地利用了这一点,待调用的函数称为 Tools。OpenAI 已经用特别版本(当前为 gpt-3.5-turbo-0613 和 gpt-4–0613)对其 GPT-3.5 和 GPT-4 模型进行了微调,非常擅长此任务,并为此目的设置了特定的 API 条目。本文将采用 OpenAI 的符号表示,但这些概念可以应用于任何 LLM,例如使用提到的 ReAct 机制。此外,LangChain 有一个特定的代理类型 (AgentType.OPENAI_FUNCTIONS),在幕后将 Tools 转换为 OpenAI 函数模板。对于 LLama2,你将能够使用 llama-api 并使用与 OpenAI 相同的语法。
LLM 的函数调用工作如下:
-
你将函数模板的 JSON 架构与用户的自然语言表达作为用户消息一起插入到提示中。
-
LLM 尝试将用户的自然语言表达映射到这些模板之一。
-
LLM 返回结果 JSON 对象,以便你的代码可以进行函数调用。
在本文中,函数定义是 (移动) 应用程序图形用户界面 (GUI) 的直接映射,其中每个函数对应于一个屏幕,每个参数对应于该屏幕上的一个 GUI 元素。发送到 LLM 的自然语言表达返回一个包含函数名称及其参数的 JSON 对象,你可以用来导航到正确的屏幕并在视图模型中触发正确的函数,以便获取正确的数据。该屏幕上相关 GUI 元素的值根据参数进行设置。
这在下图中进行了说明:
将 LLM 功能映射到你的移动应用程序的 GUI
它展示了添加到 LLM 提示中的函数模板的精简版本。要查看用户消息‘我在阿姆斯特丹可以做些什么?’的完整提示,点击这里 (Github Gist)。它包含了你可以从命令行使用或导入到 Postman 中的完整 curl 请求。你需要将你自己的 OpenAI-key放入占位符中以运行它。
没有参数的屏幕
你应用中的一些屏幕没有任何参数,或者至少没有 LLM 需要了解的参数。为了减少令牌使用和杂乱,我们可以将这些屏幕触发器合并到一个单一的函数中,并使用一个参数:要打开的屏幕。
{
"name": "show_screen",
"description": "Determine which screen the user wants to see",
"parameters": {
"type": "object",
"properties": {
"screen_to_show": {
"description": "type of screen to show. Either
'account': 'all personal data of the user',
'settings': 'if the user wants to change the settings of
the app'",
"enum": [
"account",
"settings"
],
"type": "string"
}
},
"required": [
"screen_to_show"
]
}
},
判断触发函数是否需要参数的标准是用户是否有选择:屏幕上是否进行某种形式的搜索或导航,即是否有可以选择的搜索(类似)字段或标签?
如果没有,那么 LLM 不需要知道这些信息,并且屏幕触发可以添加到你应用的通用屏幕触发函数中。这主要是一个关于屏幕目的描述的实验问题。如果你需要更长的描述,考虑给它一个自己的函数定义,以便比通用参数的枚举更分开地强调它的描述。
提示指令指导和修复:
在你提示的系统消息中,你提供了一般性的引导信息。在我们的示例中,了解当前的日期和时间可能很重要,例如,如果你想为明天计划一个旅行。另一个重要的方面是引导其假设性。我们通常更希望 LLM 表现得过于自信,而不是因为不确定性而打扰用户。对于我们的示例应用,一个好的系统消息是:
"messages": [
{
"role": "system",
"content": "The current date and time is 2023-07-13T08:21:16+02:00.
Be very presumptive when guessing the values of
function parameters."
},
函数参数描述可能需要相当多的调整。例如,在计划火车旅行时,trip_date_time
就是一个例子。一个合理的参数描述是:
"trip_date_time": {
"description": "Requested DateTime for the departure or arrival of the
trip in 'YYYY-MM-DDTHH:MM:SS+02:00' format.
The user will use a time in a 12 hour system, make an
intelligent guess about what the user is most likely to
mean in terms of a 24 hour system, e.g. not planning
for the past.",
"type": "string"
},
所以如果现在是 15:00,而用户说他们想在 8 点离开,他们实际上指的是 20:00,除非他们特别提到一天中的时间。上述指令对 GPT-4 的效果相当好。但在某些极端情况下,它仍然会失败。我们可以例如添加额外的参数到函数模板中,以便在我们自己的代码中进行进一步修正。例如,我们可以添加:
"explicit_day_part_reference": {
"description": "Always prefer None! None if the request refers to
the current day, otherwise the part of the day the
request refers to."
"enum": ["none", "morning", "afternoon", "evening", "night"],
}
在你的应用中,你可能会发现一些参数需要后处理以提高其成功率。
系统请求澄清
有时,用户的请求缺乏继续处理所需的信息。可能没有适合处理用户请求的函数。在这种情况下,LLM 会用自然语言响应,你可以通过例如 Toast 的方式展示给用户。
也可能存在这样一种情况,即大型语言模型(LLM)确实识别出了一个潜在的函数调用,但缺乏填充所有必需函数参数的信息。在这种情况下,可以考虑将参数设置为可选。但如果这不可行,LLM 可能会用用户的语言发送自然语言请求,询问缺失的参数。你应该将这段文本展示给用户,例如通过吐司提示或文本转语音,让他们提供缺失的信息(通过语音)。例如,当用户说“我想去阿姆斯特丹”(而你的应用没有通过系统消息提供默认或当前位置)时,LLM 可能会回应“我知道你想要乘火车旅行,你想从哪里出发?”。
这提出了对话历史的问题。我建议你始终在提示中包含用户的最后 4 条消息,以便信息请求可以分多次进行。为了简化起见,可以省略系统的响应,因为在这种用例中,它们往往弊大于利。
语音识别
语音识别是将语音转换为应用中的参数化导航动作的关键部分。当解释的质量较高时,语音识别的质量较差可能会成为最薄弱的环节。手机具有合理质量的内置语音识别,但基于 LLM 的语音识别如Whisper、谷歌的Chirp/USM、Meta 的MMS或DeepGram往往会取得更好的结果,特别是当你可以为你的用例调整它们时。
架构
最好将函数定义存储在服务器上,但它们也可以由应用管理,并随每个请求发送。这两种方式各有利弊。随每个请求发送函数定义更灵活,函数和界面的对齐也可能更容易维护。然而,函数模板不仅包含函数名称和参数,还包含我们可能希望比应用商店更新流程更快更新的描述。这些描述或多或少依赖于 LLM,并且根据实际效果进行设计。你可能会想要用更好的或更便宜的 LLM 来替换当前的 LLM,或者甚至在某些时候动态切换。将函数模板存储在服务器上也可能有一个好处,即如果你的应用在 iOS 和 Android 上都是原生的,那么可以在一个地方维护它们。如果你同时使用 OpenAI 的服务进行语音识别和自然语言处理,那么整个流程的技术大图如下:
使用 Whisper 和 OpenAI 函数调用为你的移动应用启用语音的架构
用户说出他们的请求;它被录制到 m4a 缓冲区/文件(如果你愿意,也可以是 mp3),然后发送到你的服务器,服务器将其转发到 Whisper。Whisper 响应转录内容,你的服务器将其与系统消息和函数模板结合成 LLM 的提示。你的服务器收到原始函数调用 JSON,然后将其处理成应用程序所需的函数调用 JSON 对象。
从函数调用到深度链接
为了说明函数调用如何转换为深度链接,我们取初始示例中的函数调用响应:
"function_call": {
"name": "outings",
"arguments": "{\n \"area\": \"Amsterdam\"\n}"
}
在不同的平台上,这一过程处理得相当不同,并且随着时间的推移,使用了许多不同的导航机制,并且这些机制仍然在使用中。详细的实现细节超出了本文的范围,但大致来说,这些平台在其最新版本中可以采用如下的深度链接:
在 Android 上:
navController.navigate("outings/?area=Amsterdam")
在 Flutter 上:
Navigator.pushNamed(
context,
'/outings',
arguments: ScreenArguments(
area: 'Amsterdam',
),
);
在 iOS 上,事情有些不够标准化,但使用 NavigationStack:
NavigationStack(path: $router.path) {
...
}
然后发出:
router.path.append("outing?area=Amsterdam")
更多关于深度链接的信息可以在这里找到:Android,Flutter,iOS
应用程序的自由文本字段
有两种自由文本输入模式:语音和打字。我们主要讨论了语音,但打字输入的文本字段也是一个选项。自然语言通常相当冗长,因此可能很难与 GUI 交互竞争。然而,GPT-4 通常很擅长从缩写中猜测参数,因此即使是非常简短的缩写打字也常常能被正确解释。
在提示中使用带有参数的函数通常会大大缩小 LLM 的解释上下文。因此,它需要非常少的内容,如果你指示它进行假设则更少。这是一个新的现象,对移动交互具有很大的潜力。在车站到车站规划器的案例中,LLM 在使用本文中示例提示结构时做出了以下解释。你可以使用上述的 prompt gist 亲自尝试。
示例:
‘ams utr’:给我显示从阿姆斯特丹中央车站到乌特勒支中央车站的列车时刻表,从现在起出发。
‘utr ams arr 9’:(假设现在是 13:00)。给我显示从乌特勒支中央车站到阿姆斯特丹中央车站的列车时刻表,要求到达时间在 21:00 之前。
后续交互
就像在 ChatGPT 中一样,你可以通过发送一小段交互历史来细化你的查询:
使用历史记录功能,以下内容也非常有效(假设现在是早上 9:00):
输入:‘ams utr’ 并获得上述答案。然后在下一轮输入‘arr 7’。是的,它实际上可以将其翻译为从阿姆斯特丹中央到乌特勒支中央的旅行,预计在 19:00 之前到达。
我制作了一个关于此的示例网页应用程序,你可以在这里找到相关视频。实际应用程序的链接在描述中。
更新:可以在这里找到这篇文章的继任者,其中包含文本输入,演示视频请见这里。
未来
你可以期待这种深度链接结构处理应用内功能,成为你手机操作系统(Android 或 iOS)的一个重要组成部分。手机上的全球助手将处理语音请求,应用程序可以将其功能暴露给操作系统,以便以深度链接的方式触发。这与 ChatGPT 插件的可用性类似。目前,通过 AndroidManifest 中的意图和 App Actions 以及 iOS 上的 SiriKit intents 已经可以粗略实现。你对这些功能的控制有限,用户需要像机器人一样说话才能可靠地激活它们。毫无疑问,当LLM 驱动的助手接管时,这种情况会随着时间的推移而改善。
VR 和 AR(XR)为语音识别提供了极好的机会,因为用户的双手通常参与其他活动。
可能很快任何人都能运行自己的高质量 LLM。成本将降低,速度在接下来的一年里将迅速增加。LoRA LLMs 很快将出现在智能手机上,这样推理可以在你的手机上进行,从而降低成本并提高速度。此外,竞争也会越来越激烈,包括像 Llama2 这样的开源项目,以及像 PaLM 这样的闭源项目。
最后,模态的协同效应可以超越提供对整个应用程序 GUI 的随机访问。LLM(大语言模型)结合多个来源的能力预示着更好的帮助将会出现。一些有趣的文章:多模态对话,谷歌关于 GUI 和 LLM 的博客,将 GUI 交互解释为语言,LLM 驱动的助手。
结论
在本文中,你学会了如何应用函数调用来为你的应用程序启用语音功能。以提供的 Gist 为出发点,你可以在 Postman 或命令行中进行实验,了解函数调用的强大功能。如果你想在你的应用上运行一个语音启用的 POC(概念验证),我建议将架构部分的服务器代码直接集成到你的应用中。整体来说,这归结为 2 次 HTTP 调用,一些提示构建和实现麦克风录音。根据你的技能和代码库,你将在几天内完成 POC 的搭建。
编程愉快!
除非另有说明,本文中的所有图片均由作者提供。
使用 Python 的速度打字测试项目
原文:
towardsdatascience.com/speed-typing-test-project-with-python-da1a56987a5b
使用 Python 开发速度打字测试项目以评估准确性和打字速度
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 3 月 22 日
–
图片来源 Spencer Davis 于 Unsplash
每个拥有电子设备的个人通常都会在其各自的设备上打字,无论是笔记本电脑、手机还是个人电脑。在现代世界中,通过项目打字是一种更为广泛使用且方便的方法,可以完成各种任务。
就我个人而言,我打字的频率相当高,打字的内容和材料也各不相同。这些内容的范围从在 Medium 上打字的文章、分析和编写数据科学项目,到写重要的邮件,或者只是浏览互联网。虽然写作有助于产生想法和有效思考,但我不能否认我在打字上花费的时间比写作要多。
无论人们是否与我处于同一范围,或者每个人的情况是否不同,保持在打字时的‘A’状态总是一个好主意。在这个项目中,我们将使用 Python 设计一个简单的速度打字测试,以帮助评估你的准确性、错误率和打字速度。
我们将在控制台界面上开发这个项目,并以每秒字数的形式打印错误率和总体得分。对于那些更有好奇心的开发者,我推荐查看使用 Python 开发的高级 GUI 界面,这样可以使项目更具吸引力和美观。我已经在下面提供了相关文章的链接。
使用更现代的 Python 开发接口来开始你的项目
towardsdatascience.com
开发速度测试项目:
在这部分文章中,我们将开发“速度测试”软件,以报告用户打字的分数和错误率。测试应当进行多次,以获得平均评分,从而准确确定真正的数值。
这个项目有多种不同的方法来处理,我将使用最直接、最简单的方法来实现预期的解决方案。我建议安装下面提供的库来简化这个过程。
pip install wonderwords
你可以通过以下链接访问官方 pip 安装 Python 包索引网站,了解更多关于该库的信息,或者通过以下快速入门文档网站了解更多内容。为了让自然语言处理任务更易于理解,我建议查看下面链接提供的我之前关于正则表达式操作符的文章。
理解四种基本正则操作来清理几乎任何类型的数据。
towardsdatascience.com
导入所需的库:
使用 Python 构建我们的速度测试软件的第一步是导入所有必要的库。我们将使用 wonder words 库导入随机句子类,这将允许我们生成随机句子,供用户进行打字测试。我更喜欢使用这个库作为一种更通用的方法,以便每次需要进行速度测试时随机生成不同的段落。
对于那些希望采用不同方法的开发者,如果不想使用之前提到的库,我们可以使用随机库并自己输入句子和创建列表。随机库中的选择功能对于将各种段落选项随机显示给测试者非常有用。时间库模块对跟踪用户的打字速度至关重要。以下是所有所需库导入的列表。
from wonderwords import RandomSentence
import random
import time
随机句子生成:
一旦导入了所有必要的库,我们可以继续下一步。我们将创建一个句子列表和一个作为字符串变量的句子段落。然后我们将创建一个 for 循环,通过这个循环我们将使用“wonder words”库中的随机句子模块。一旦将以下类分配给所需的变量,我们可以生成随机句子。
每个随机生成的句子都被附加到一个列表中,然后转换成一个字符串段落,其中所有包含相应句子的段落被一起存储。这些随机生成的段落将展示给用户。展示后,用户可以进行打字测试,以测试适当的速度和错误率。
sent_list = []
sent_para = ""
for i in range(5):
sent = RandomSentence()
random_sent = sent.sentence()
sent_list.append(random_sent)
sent_para += random_sent + " "
错误率计算:
一旦随机段落存储在一个变量中,我们可以继续定义计算测试者打字错误率的下一步。我使用了一种简单的方法来计算错误率,以便于这个项目的简单性。请注意,这可能不是计算错误率的最有效方法。我将在即将到来的部分中涵盖更多的改进和进一步的发展。
在这种方法中,我们计算句子段落的长度,并在此范围内创建一个循环。然后,我们将比较每次输入的原始句子段落与输入的段落。每次字符不匹配时,错误计数会增加。总错误百分比通过将错误计数除以总长度,并乘以 100 来计算。以下是执行该功能的代码片段。
def error_rate(sent_para, typed_para):
error_count = 0
length = len(sent_para)
for character in range(length):
try:
if sent_para[character] != typed_para[character]:
error_count += 1
except:
error_count += 1
error_percent = error_count/length * 100
return error_percent
最终得分和错误百分比:
在这个项目的最后一步,我们将输入一个句子,表示速度测试即将开始,且必须在规定时间内准确打印出适当的段落,以达到最佳分数。开始时间和结束时间会被记录,直到用户输入了相应的段落。总时间通过从结束时间中减去开始时间来计算。错误率通过本文节中之前定义的函数来计算。
我增加了一个创建 if 循环的额外步骤,以测量输入句子的错误百分比。如果错误百分比超过 50,那么计算出的分数可能不准确,可能需要重新测试。如果错误百分比低于 50%,我们可以报告每秒单词数和总单词数。以下是执行速度测试操作的代码块。
print("Type the below paragraph as quickly as possible with as few mistakes to get a high score: \n")
print(sent_para)
print("\n")
start_time = time.time()
typed_para = input()
end_time = time.time()
time_taken = end_time - start_time
error_percent = error_rate(sent_para, typed_para)
print("\n")
if error_percent > 50:
print(f"Your error rate {error_percent} was quite high and hence your accurate speed could not be computed.")
else:
speed = len(typed_para)/time_taken
print("******YOUR SCORE REPORT******")
print(f"Your speed is {speed} words/sec")
print(f"The error rate is {error_percent}")
速度测试项目现在已完成。读者可以继续进行自己的测试并检查他们的打字速度!然而,对于那些更好奇和感兴趣的开发者,我们将进行一次测试运行。我还会建议一些额外的改进,以使这个项目在即将到来的部分中变得更加有趣和用户友好。
测试运行和额外改进:
作者截图
一旦你在各自的控制台中运行程序(我使用的是 Visual Studio Code 编辑器,但也可以使用命令提示符),你应该能够相应地测试工作代码。在上面的截图中,读者可以注意到由 wonder words 库生成的随机句子,原始段落下面是我输入的段落。
一旦我输入所需段落并按下回车键,我们可以相应地查看评分报告。从上面的截图中,我们可以注意到我的单词速度约为每秒 4.3 个单词,错误率为 0.59\。为了获得更精确的测试分数,我建议运行上述程序并取至少五次测试的平均值。
我们在本节中将讨论的另一个重要话题是进一步增强此项目功能和风格方面的不同改进。一些建议如下:
-
为了更具自定义性地生成句子,使用自己定制的句子和随机选择函数是一个不错的选项。然而,如果读者想更进一步,Open AI 提供了一个用于高级故事集成的绝佳选项。
-
错误率的计算可能略有偏差,因为一个字符错误可能导致多个故障。更好的方法可能是考虑原始段落和输入段落中的所有单词,然后比较两个列表。
-
最后的建议是将整个项目转移到 GUI 界面上,因为控制台界面可能显得平淡。我推荐查看我之前关于 GUI 的文章,以快速了解一些在 Python 中可用的值得注意的 GUI 选项的入门代码。
## Python 开发者的 7 款最佳 UI 图形工具及入门代码
Python 中开发酷炫用户界面技术的七款最佳 UI 图形工具
towardsdatascience.com
结论:
图片来源于 Paul Kansonkho 于 Unsplash
“打字是说话的未来,不要忘记它,是特性的兄弟。”
— Deyth Banger
打字现在是我们生活中的重要部分,是我们不断进行的必要活动。虽然我们打字的次数很多,而且随着时间的推移变得越来越自然,但人们可能会好奇他们打字的速度和准确度。了解这一点的一个好方法是不断提高打字速度,以提高生产力。
在这个项目中,我们开发了一个用 Python 制作的速度测试软件,使我们能够输入建议的特定段落,并在输入错误百分比低于 50 的段落后,获得我们的打字分数和百分比错误。我们还讨论了可以进一步改进和提升这个项目的其他方法。
如果你希望在我的文章发布后第一时间收到通知,请查看以下链接来订阅电子邮件推荐。如果你希望支持其他作者和我,请订阅以下链接。
[## 通过我的推荐链接加入 Medium - Bharath K
阅读 Bharath K 的每个故事(以及 Medium 上成千上万的其他作家的故事)。你的会员费直接支持…
bharath-k1297.medium.com](https://bharath-k1297.medium.com/membership?source=post_page-----da1a56987a5b--------------------------------)
如果你对本文中提到的各个要点有任何疑问,请随时在下方评论区告诉我。我会尽快回复你。查看一些我的其他文章,看看我的观众们还喜欢阅读什么!
讨论一个优秀的 Jupyter Notebooks 替代方案,用于解释数据科学项目
towardsdatascience.com ## 阅读 7 篇最佳研究论文,以开始深度学习项目
七篇经得起时间考验的最佳研究论文,将帮助你创建出色的项目
towardsdatascience.com ## 使用 Python 可视化 CPU、内存和 GPU 工具
分析 CPU、内存使用情况和 GPU 组件,以监控你的 PC 和深度学习项目
towardsdatascience.com
感谢大家坚持看到最后。我希望你们都喜欢阅读这篇文章。祝你们度过美好的一天!
使用 R 树加速你的地理空间数据分析
图片由 Mathias Arlund 提供,来自 Unsplash
学习如何大幅提升空间搜索的性能
·
关注 发表在 Towards Data Science · 8 分钟阅读 · 2023 年 5 月 21 日
–
几年前,我在做一个副项目。我想创建一个推荐本地珍宝的网页应用,例如咖啡馆、书店或隐秘酒吧。这个想法是将所有这些用户可触及的兴趣点显示在地图上。由于数据集中有成千上万的兴趣点,我必须巧妙地过滤出用户指定范围内的数据点。最简单的方法是计算用户与每个兴趣点之间的距离,并丢弃所有超出指定范围的点。特别是对于像我这样的大数据集,这种方法通常会导致较长的处理时间。
当然,必须有更好的方法,因为响应时间在互动应用程序中非常重要。这时我遇到了数据结构R-tree。这些树用于快速的空间访问和搜索。使用 R-tree,我能够快速隔离靠近用户位置的兴趣点,并将其显示在地图上。这大大提升了我的网页应用的响应时间——仅仅四行额外的代码!
在这篇文章中,我解释了 R-trees 是什么以及它们是如何工作的。前两部分通过纽约市的街道树示例进行了说明。第三部分展示了如何在 Python 中使用这种数据结构来加速地理空间数据处理例程。
通过分析纽约市的树木来学习 R-trees
假设我们被要求分析纽约市的社区与其树木健康之间是否存在相关性。NYC 开放数据门户提供了一个街道树普查数据集,包括每棵树的物种、直径、健康状况的感知和地理位置。
[## 2015 街道树普查 - 树木数据 | NYC 开放数据
数据来自 TreesCount! 2015 街道树普查,由志愿者和纽约市公园部组织的工作人员进行…
data.cityofnewyork.us](https://data.cityofnewyork.us/Environment/2015-Street-Tree-Census-Tree-Data/pi5s-9p35?source=post_page-----4f75abdc6025--------------------------------)
首先,我们想统计上东区的街道树数量。下面的伪代码片段遍历数据集trees
,并检查一棵树是否落在upper_east_side
边界内:
total_count, tree_count = 0, 0
for tree in trees:
total_count += 1
if upper_east_side.contains(tree):
tree_count += 1print_results(num_tests=total_count, num_trees=tree_count)
**>>>** Total number of trees tested: 683,788
**>>>** Number of trees in Upper East Side: 8,807
我们发现上东区大约有 9 千棵树。然而,我们必须测试总共 68 万棵树才能得到这个结果。下方的动画可视化了我们测试那些距离目标社区几英里的树,因此可以很容易地被忽略。但我们如何能排除远离的树木以减少昂贵的计算,从而实现显著的性能提升呢?
我们几乎可以免费获得的一项信息是多边形的边界框(可以通过其节点的最小值和最大值来确定)。此外,测试一个点是否落在矩形内非常简单,只需进行四次比较操作(点必须大于或等于左下角,并且小于或等于右上角)。现在,假设 bounding_box
是一个数据集,包含了上东区周围紧密矩形内的所有树木(在下一节中我们将学习如何轻松获得这样的矩形)。考虑到这一点,可以得出:
total_count, tree_count = 0, 0
for tree in bounding_box:
total_count += 1
if upper_east_side.contains(tree):
tree_count += 1print_results(num_tests=total_count, num_trees=tree_count)
**>>>** Total number of trees tested: 10,768
**>>>** Number of trees in Upper East Side: 8,807
动画的右侧演示了我们现在仅测试潜在候选者。这些是位于多边形附近的树,即落在其边界框内的点。通过忽略远离的树木,我们将测试数量从 684k 减少到 11k —— 降低了 60 倍! 在下一节中,我们将看到 R-trees 正是利用了这一点。
(左) 纽约市的所有树木都经过测试 | (右) 仅测试位于上东区边界框内的树木。图像由作者提供,地图数据来自 © Mapbox 和 © OpenStreetMap。
用于空间搜索的数据结构:R-tree
R-trees 是基于树的数据结构,用于高效地创建空间索引。R-tree 通常用于快速空间查询或加速最近邻搜索 [1]。一个常见的应用场景是存储兴趣点的空间信息(例如餐馆、加油站、街道等)。借助 R-trees,可以快速检索到某位置一定距离内的所有兴趣点。反过来,这些结果可以在地图上或导航系统中显示。
R-tree 的基本思想很简单:树的叶节点保存空间数据,而分支节点对应于包含所有子节点的最小边界框。通过这种结构,R-tree 将空间划分为矩形,随着树的增长这些矩形变得更为精细。以下示例对此进行了说明。
(左) R-tree 将曼哈顿划分为多个矩形 | (右) 对应的树结构。图像由作者提供,地图数据来自 © Mapbox 和 © OpenStreetMap。
R 树被查询以获取一个矩形,即我们想要检索包含在此搜索窗口内的所有数据。请记住,每个非叶节点对应一个包含其所有子节点的边界框。为了执行搜索查询,我们只需沿着树的分支移动,并且沿着与给定矩形相交的路径直到到达叶节点。这些叶节点,因此我们的数据点,包含在搜索矩形内并完成查询。下面的动画演示了我们可以通过忽略不符合搜索条件的整个分支大大减少搜索操作的数量。
(左侧) 不与搜索矩形(红色)相交的边界框(黑色)被逐步忽略 | (右侧) 搜索查询通过遵循与搜索矩形相交的路径来完成。图片由作者提供。
Python 中的 R 树
Python 包Rtree
提供了 R 树数据结构的实现,并提供了许多方便的功能,如最近邻搜索、交集搜索或多维索引。
[## Rtree: Spatial indexing for Python - Rtree 0.9.4 documentation
Rtree 是 libspatialindex 的 ctypes Python 包装器,为 Python 提供了许多先进的空间索引功能。
我们可以方便地使用 Python 的包管理器pip安装这个包:pip install Rtree
。
基础知识
在处理像点或多边形这样的几何体之前,我们先了解一下Rtree
包的基本用法。
index
模块帮助我们构建空间索引。这个索引通过插入我们对象的边界框自动构建起来。边界框通过指定它们的左侧、底部、右侧和顶部坐标来定义。请注意,在执行查询时,我们将边界框与一个标识符(在上面的示例中为0
和1
)一起插入。该 ID 将帮助我们在执行查询时识别边界框:
该索引被查询以获取给定矩形内的对象,再次由其左侧、底部、右侧和顶部坐标指定。intersection
方法的结果是在搜索窗口内包含的对象的 ID(示例 1-3)。如果搜索窗口超出我们在索引中的数据范围,结果为空(示例 4)。类似地,我们使用nearest
方法来找到离给定搜索窗口最近的k个对象:
处理点、线和多边形
在前面的部分中,我们看到索引是通过插入对象的边界框来构建的。现在,我们希望继续使用点、线和多边形来处理这些对象。包Shapely提供了一种在 Python 中处理这些类型几何体的简单方法:
上面,我们首先创建一个点,一个线和一个多边形。接下来,这些对象的边界框被使用 ID 0
、1
和2
插入索引中。我们现在查询不同的搜索窗口:
下面的插图显示了几何图形和搜索窗口:
绿色: 点、线和多边形。红色: 搜索窗口。图片由作者提供。
搜索上东区的所有树木
我们终于具备了提取上东区所有树木所需的一切!我们将通过下面的代码片段进行说明,完整版本可以在这里找到。
绿色: 纽约市的树木。蓝色: 上东区。橙色: 上东区的边界框。图片由作者提供,地图数据来自© Mapbox和© OpenStreetMap。
首先,我们使用GeoPandas包加载所有所需的几何图形:
接下来,我们创建一个包含纽约市所有树木的 R 树索引:
现在,我们生成一个潜在候选项的列表,即所有在上东区边界框内的树木:
最后,我们遍历所有潜在的候选项,提取完全位于上东区内的那些:
结论
在这篇文章中,我们了解了 R 树如何通过将底层空间划分为矩形来组织地理信息。这种结构使 R 树在空间查找中极其快速。在我们的纽约市街道树木示例中,使用 R 树将操作次数减少了 60 倍。我们还看到了如何在 Python 中使用 R 树。我们的示例中的加速仅通过四行代码实现:初始化索引(1 行),构建索引(2 行),以及使用intersection
函数找到附近的候选项(1 行)。
那么,为什么 R 树不是无处不在呢?虽然通过减少搜索操作次数节省了时间,但我们在构建索引时却浪费了时间。后者我们必须实际遍历整个数据集。这使得 R 树不适合只需要少量搜索的应用程序或索引经常变化的应用程序(由于树的重新平衡)。
自 1984 年 Antonin Guttman 发明 R 树以来,它们已经取得了长足的进步。如今,它们被广泛应用于各种领域,例如计算机图形学[2]、视频游戏[3]、交通控制系统[4],以及最显著的空间数据管理数据库[5]。也许在你下一次的地理空间数据分析中也会用到!
参考文献
[1] A. Guttman,R-Trees: A Dynamic Index Structure for Spatial Searching(1984),1984 年 ACM SIGMOD 国际数据管理会议论文集,第 47-57 页
[2] D. Feldmann,使用 R 树加速光线追踪(2015),第十届计算机图形学理论与应用国际会议论文集,第 247–257 页
[3] A. Kinziabulatov,在 Unity 中优化 R 树插入:一个类似 Bomberman 的例子(2023),Medium
[4] Y. Manolopoulos, A. Nanopoulos, A. Papadopoulos 和 Y. Theodoridis,R 树:理论与应用(2006),Springer
[5] S. Bressan, J. Küng 和 R. Wagner,数据库与专家系统应用(2006),Springer
数据集
纽约市公园与娱乐部,2015 年街道树木普查——树木数据(2016),纽约市开放数据
纽约市城市规划部,2010 年社区划分区域(NTAs)(2013),纽约市开放数据
加速你的 Python 技能
原文:
towardsdatascience.com/speed-up-your-python-skills-in-2023-e680f4c56f37
七个技巧助你更上一层楼
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 1 月 16 日
–
图片来源 Emile Perron 在 Unsplash
Python 是数据科学领域使用最广泛的编程语言,其受欢迎程度不断增长。近年来,整个数据科学领域也得到了巨大的发展。
在这篇文章中,我们将向你展示七个提高 Python 技能的技巧。通常是那些小细节能产生大的差异。这些提示将丰富你作为数据科学家的生活。因此,我们提供了七个可以立即实践的技巧。保持好奇!
作为数据科学家,你经常需要处理大量的数据。因此,你必须在运行时间和内存方面高效编码。你的 Python 代码也应该结构良好且易于阅读。这些提示将帮助你编写高效且易读的 Python 代码。
提示 1: 加速 NumPy
NumPy 是一个用于高效处理数组的 Python 库。它还提供了快速和优化的矢量化操作。但!它不支持并行处理。作为 NumPy 的替代品,你可以使用 NumExpr。
NumExpr 的性能显著优于 NumPy,因为它支持多线程。此外,它避免了为中间结果分配内存。
首先,你需要安装 NumPy 和 NumExpr 包。例如:
$ pip install numpy numexpr
看看这个例子并尝试一下。
import numpy as np
import numexpr as ne
import timeit
var1 = np.random.random(2**27)
var2 = np.random.random(2**27)
%timeit np.sin(var1) / np.cos(var2)
# 2.73 s
%timeit ne.evaluate("sin(var1) / cos(var2)")
# 566 ms
哇!该语句在 NumExpr 下执行速度大约提高了 5 倍。因此,如果你想加快 NumPy 语句的速度,这为你提供了一种实现方法。
NumExpr 在处理大数组时效果最佳。如果你有一个强大的多核计算机,它也能发挥其最大性能。因此,当这两个条件都满足时,我们推荐使用 NumExpr。对于小规模数组操作,你也可以使用 NumPy,因为性能差异非常小。原因是 NumExpr 将数组操作数拆分成小块,这些小块很容易适应 CPU 缓存。这些小块在 CPU 的可用核心之间分配,从而实现并行执行。
如果你想了解更多关于 NumExpr 的内容,可以查看NumExpr 的 GitHub库。
提示 2:pandas apply()的快速替代方案
pandas 的 apply()函数可以在数据框的一个轴上执行函数。许多程序员将 apply()函数与 lambda 函数结合使用。但你如何提高 apply()函数的性能呢?
你可以使用 swifter 包。这个包将函数非常快速地应用于数据框或序列。pandas 的 apply()函数在一个核心上运行,而 swifter 则提供了多个核心支持。
首先,你需要安装 swifter 包。
$ pip install swifter
安装后,你可以直接尝试它。
import pandas as pd
import numpy as np
import swifter
import timeit
df = pd.DataFrame({'a': np.random.randint(7, 10, 10**7)})
# pandas apply()
%timeit df.apply(lambda x: x**7)
# 54 ms
# swifter.apply()
%timeit df.swifter.apply(lambda x: x**7)
# 37.5 ms
这个简单的例子显示了 swifter.apply()函数的运行时间更快。尤其是在多核的强大计算机上,这一差异尤其明显。如果你在下一个项目中需要性能提升,考虑使用 swifter 包。
提示 3:使用内置的 Python 函数
你经常实现一个函数却不知道它在 Python 中已经存在。尤其是如果你来自其他编程语言,如 C 或 C++。首先,你应该总是检查 Python 内置函数是否已经存在。Python 内置函数比自定义实现要快得多,因此你应该总是使用它们。以下示例演示了这一点。
import numpy as np
from time import perf_counter
result_list = []
company_list = ["Tesla", "Block", "Palantir", "Apple"]
company_list_sample = np.repeat(company_list, 10**7)
start = perf_counter()
for company in company_list_sample:
result_list.append(company.lower())
print(perf_counter()-start)
# 17.13 s
start = perf_counter()
result_list = map(str.lower, company_list_sample)
print(perf_counter()-start)
# 0.97 s
在上面的代码中,我们将一个包含四个条目的列表复制了 1 千万次,因此得到一个包含 4 千万个条目的列表。然后我们将列表中的字符串转换为小写。你可以看到,内置函数的速度约快了 17 倍。特别是在大量数据的情况下,这个提示带来了巨大的性能提升。所以要使用内置函数!
还有许多内置函数,如 min()、max()、all()等。如果你需要特定的 Python 函数,可以自己查找。这很值得!
提示 4:使用列表推导代替循环
程序员经常将列表与循环结合使用来存储计算结果。然而,这种方法在运行时间上效率不高。因此,最好使用列表推导,它具有更好的性能。以下示例显示了性能的差异。
import numpy as np
from time import perf_counter
result_list_loop = []
result_list_com = []
number_round = 10000000
start = perf_counter()
for i in range(number_round):
result_list_loop.append(i*i)
print(perf_counter()-start)
# 1.47 s
start = perf_counter()
result_list_com = [i*i for i in range(number_round)]
print(perf_counter()-start)
# 0.69 s
print(result_list_com[10])
# 100
从这个示例中我们学到了什么?尽可能使用列表推导式。列表推导式在编程中有些争议。一些程序员觉得语法难以阅读,因为一行代码表达了所有语句。在我们看来,语法清晰简洁。这是一个口味问题,但列表推导式的性能更好。
列表推导式以开括号[开始。然后是 for 循环中的计算。接着是循环头,包含三个元素(关键字 for,运行变量,循环长度)。列表推导式以闭括号]结束。一旦你理解了语法,你可以更紧凑地编写 for 循环。
那么在内存使用方面呢?我们如何减少内存空间?如果我们想对大列表进行进一步操作,这一点尤其建议。在我们的示例中,我们在列表中存储了 10000000 个值。但我们是否必须直接保存所有条目,还是仅在需要时才保存它们?
在这些情况下,我们可以使用生成器。生成器在需要时创建一个列表项。因此,生成器需要更少的内存并且运行时间更好。看看下面的示例。
import sys
from time import perf_counter
print(sys.getsizeof(result_list_com), 'bytes')
# 89095160 bytes
start = perf_counter()
result_gen = (i*i for i in range(number_round))
print(perf_counter()-start)
# 0.22 ms
print(sys.getsizeof(result_gen), 'bytes')
# 112 bytes
print(list(result_gen)[10])
# 100
我们可以执行与之前示例相同的所有操作。唯一的区别是我们现在使用的是()而不是[]。我们存储的是生成器而不是列表。这种方法在内存上更为高效。检查一下你是否可以在项目中使用列表推导式或生成器。它们可以提高性能并减少内存使用。
提示 5:使用双星号语法**合并字典
如何合并字典?你可以用一行代码来实现。我们使用了星号语法**。在下面的示例中,你可以看到它是如何工作的。
dict_1 = {'company': 'Tesla', 'founding': 2002}
dict_2 = {'company': 'Tesla', 'founding': 2003, 'CEO': 'Elon Musk'}
dict_merged = {**dict_1, **dict_2}
print(dict_merged)
# {'company': 'Tesla', 'Founding': 2003, 'CEO': 'Elon Musk'}
首先,我们定义两个字典,它们有相同和不同的键值对。特斯拉成立于 2003 年,所以 dict_2 更加最新。如果两个字典包含相同的键但值不同,则使用最后一个字典的值。合并后,新字典包含所有三个键值对。语法简洁紧凑,因此合并非常简单。而且最棒的是,你可以合并三个或更多字典。这个技巧可以节省大量时间。
另一种方法是 update 方法。此方法更新第一个字典,并不创建副本。看看下面的示例。
dict_1 = {'company': 'Tesla', 'founding': 2002}
dict_2 = {'company': 'Tesla', 'founding': 2003, 'CEO': 'Elon Musk'}
dict_1.update(dict_2)
print(dict_1)
# {'company': 'Tesla', 'Founding': 2003, 'CEO': 'Elon Musk'}
update 方法的缺点是你只能使用一个字典进行更新。如果你未来想要合并字典,请记住这个提示。
提示 6:不要导入不必要的模块
你可能听到过这个提示很多次,但它可以显著提高代码的性能。没有必要导入整个库。你通常只需要其中的某些函数。此外,由于必须先导入整个库,所以你的代码启动时间较长。这不应该是这样的。此外,你还必须通过点表示法访问单独的函数。这非常低效,你应该避免使用点表示法。以下示例演示了这一点。
import math
from time import perf_counter
start = perf_counter()
variable = math.exp(7)
print(perf_counter()-start)
# 8.47-05 s
在这个示例中,我们使用了 math.exp()
函数和点表示法。这导致了代码性能不佳。此外,我们导入了整个 math 库,尽管我们只需要 exp()
函数。
from math import exp
from time import perf_counter
start = perf_counter()
variable = exp(7)
print(perf_counter()-start)
# 4.51-05 s
在这个示例中,我们在没有点表示法的情况下导入了 exp()
函数。通过使用这个技巧,我们可以将代码的运行时间减半。哇,太棒了!
提示 7:使用即时编译器
Numba 是一个即时 (jit) 编译器,能够很好地与 NumPy 循环、数组和函数配合使用。装饰器用于指示 Numba 用 Numba 编译特定函数。Numba 将装饰过的函数即时编译为机器代码,以便所有或部分代码以本地机器代码的速度运行。
首先,我们需要通过 pip 安装 Numba。
pip install numba
安装成功后,你可以使用 Numba。请查看以下示例:
import numpy as np
from numba import jit
import timeit
var = np.random.random(10**7)
num_loop = 10000
def foo(var):
result = 0
for i in range(num_loop):
result += 1
result = np.sin(var) + np.cos(var)
return result
%timeit foo(var)
# 154 ms
@jit(nopython=True)
def foo_numba(var):
result = 0
for i in range(num_loop):
result += 1
result = np.sin(var) + np.cos(var)
return result
%timeit foo_numba(var)
# 76.3 ms
你可以看到,上述 foo 函数的装饰器加快了代码的执行速度。装饰器 nopython=True 表明编译将不涉及 Python 解释器。Numba 加速了循环和 NumPy 三角函数的执行。然而,它不能与所有 Python 函数一起使用。以下是 Numba 的优缺点:
缺点:
-
Numba 不支持 pandas。
-
不支持的代码通过解释器执行,并且有额外的 Numba 开销。
-
仅对 M1/Arm64 提供非官方支持。
优点:
-
对 NumPy 数组和函数以及循环的支持非常好。
-
支持 Nvidia CUDA。它可以很好地用于基于 NumPy 的神经网络开发。
缺点和优点表明 Numba 应主要用于 NumPy 操作。此外,你应该总是在开始时检查 Numba 是否适合相应的实现。
结论
在这篇文章中,我们学习了如何提高代码的运行时间和内存效率。学到的教训:
-
NumPy 不支持并行处理。你可以使用 NumExpr 来处理这个问题。
-
Pandas 的
apply()
函数可以通过 swifter 加速。 -
检查是否有内置函数。
-
使用列表推导而不是循环。检查生成器是否适合你的项目。
-
使用双星号语法合并字典**。
-
不要导入不必要的模块。
-
如果你遇到运行时间问题,你可以使用即时编译器。即时编译器可以加速你的代码。
👉🏽 加入我们的每周免费 Magic AI 通讯,获取最新的 AI 更新!
免费订阅 以便在我们发布新故事时收到通知:
[## 每当 Janik 和 Patrick Tinz 发布新内容时,您将收到电子邮件。
每当 Janik 和 Patrick Tinz 发布新内容时,您将收到电子邮件。通过注册,您将创建一个 Medium 账户,如果您还没有的话…
在我们的关于页面了解更多关于我们的信息。不要忘记在X上关注我们。非常感谢您的阅读。如果您喜欢这篇文章,请随意分享。祝您一天愉快!
使用我们的链接注册 Medium 会员,阅读无限制的 Medium 故事。
在 Mozilla Common Voice 上的口语语言识别——音频变换。
·
关注 发表于 Towards Data Science ·5 min read·2023 年 8 月 13 日
–
图片由 Kelly Sikkema 提供,来源于 Unsplash
这是基于 Mozilla Common Voice 数据集的第三篇关于语音语言识别的文章。在 第一部分,我们讨论了数据选择和数据预处理,在 第二部分 中我们分析了几种神经网络分类器的性能。
最终模型达到了 92% 的准确率和 97% 的配对准确率。由于此模型存在较高的方差,通过添加更多数据可能会提高准确率。获取额外数据的一种非常常见的方法是通过对现有数据集执行各种变换来合成数据。
在本文中,我们将考虑 5 种流行的音频数据增强变换:添加噪声、改变速度、改变音调、时间掩蔽和剪切 & 拼接。
教程笔记本可以在 这里 找到。
为了说明,我们将使用来自 Mozilla Common Voice(MCV)数据集的样本 common_voice_en_100040。这是句子 The burning fire had been extinguished。
import librosa as lr
import IPython
signal, sr = lr.load('./transformed/common_voice_en_100040.wav', res_type='kaiser_fast') #load signal
IPython.display.Audio(signal, rate=sr)
原始样本 common_voice_en_100040 来自 MCV。
原始信号波形(作者提供的图像)
添加噪声
添加噪声是最简单的音频增强方法。噪声量由信噪比(SNR)来表征——即最大信号幅度与噪声标准差的比率。我们将生成几个定义为 SNR 的噪声水平,并查看它们如何改变信号。
SNRs = (5,10,100,1000) #Signal-to-noise ratio: max amplitude over noise std
noisy_signal = {}
for snr in SNRs:
noise_std = max(abs(signal))/snr #get noise std
noise = noise_std*np.random.randn(len(signal),) #generate noise with given std
noisy_signal[snr] = signal+noise
IPython.display.display(IPython.display.Audio(noisy_signal[5], rate=sr))
IPython.display.display(IPython.display.Audio(noisy_signal[1000], rate=sr))
通过将噪声 SNR=5 和 SNR=1000 叠加到原始 MCV 样本 common_voice_en_100040 上获取的信号。
几种噪声水平的信号波形(作者提供的图像)
因此,SNR=1000 听起来几乎像未受干扰的音频,而在 SNR=5 时只能区分信号的最强部分。在实践中,SNR 级别是一个超参数,取决于数据集和选择的分类器。
改变速度
改变速度的最简单方法就是假装信号有不同的采样率。然而,这也会改变音调(声音的频率高低)。增加采样率会使声音听起来更高。为了说明这一点,我们将对我们的示例“增加”采样率 1.5 倍:
IPython.display.Audio(signal, rate=sr*1.5)
使用虚假采样率获取的信号用于原始 MCV 样本 common_voice_en_100040(作者生成)。
改变速度而不影响音高更具挑战性。需要使用相位声码器(PV)算法。简言之,输入信号首先被分割成重叠的帧。然后,通过应用快速傅里叶变换(FFT)计算每帧内的频谱。播放速度通过以不同的速率重新合成帧来修改。由于每帧的频率内容未受影响,因此音高保持不变。PV 在帧之间进行插值,并使用相位信息实现平滑。
对于我们的实验,我们将使用来自这个PV 实现的stretch_wo_loop时间伸缩函数。
stretching_factor = 1.3
signal_stretched = stretch_wo_loop(signal, stretching_factor)
IPython.display.Audio(signal_stretched, rate=sr)
通过改变原始 MCV 样本 common_voice_en_100040 的速度获得的信号(由作者生成)。
速度增加后的信号波形(图片由作者提供)
因为我们增加了速度,所以信号的持续时间减少了。然而,可以听到音高没有变化。请注意,当伸缩因子很大时,帧间的相位插值可能效果不好。因此,变换后的音频可能会出现回声伪影。
改变音高
要在不改变速度的情况下改变音高,我们可以使用相同的 PV 时间伸缩,但假装信号具有不同的采样率,以使信号的总持续时间保持不变:
IPython.display.Audio(signal_stretched, rate=sr/stretching_factor)
通过改变原始 MCV 样本 common_voice_en_100040 的音高获得的信号(由作者生成)。
为什么我们还要使用这个 PV,而librosa已经有time_stretch和pitch_shift函数?这些函数会将信号变换回时间域。当你需要后续计算嵌入时,你将浪费时间在冗余的傅里叶变换上。另一方面,很容易修改stretch_wo_loop函数,使其产生傅里叶输出而不进行逆变换。也可以尝试深入librosa代码以获得类似的结果。
时间掩蔽和切割&拼接
这两种变换最初在频率域中提出(Park 等,2019)。其想法是通过使用预计算的频谱进行音频增强以节省 FFT 的时间。为了简单起见,我们将演示这些变换如何在时间域中工作。所列操作可以通过用帧索引替换时间轴轻松转移到频率域。
时间掩蔽
时间掩蔽的想法是遮盖信号中的随机区域。神经网络将更少地学习到无法泛化的信号特定时间变化。
max_mask_length = 0.3 #maximum mask duration, proportion of signal length
L = len(signal)
mask_length = int(L*np.random.rand()*max_mask_length) #randomly choose mask length
mask_start = int((L-mask_length)*np.random.rand()) #randomly choose mask position
masked_signal = signal.copy()
masked_signal[mask_start:mask_start+mask_length] = 0
IPython.display.Audio(masked_signal, rate=sr)
通过对原始 MCV 样本 common_voice_en_100040 应用时间掩蔽变换获得的信号(由作者生成)。
时间掩蔽后的信号波形(掩蔽区域用橙色标示)(图片由作者提供)
Cut & splice
这个想法是用另一个具有相同标签的信号的随机片段替换信号的随机选定区域。实现几乎与时间掩蔽相同,只是用另一个信号的片段代替了掩蔽。
other_signal, sr = lr.load('./common_voice_en_100038.wav', res_type='kaiser_fast') #load second signal
max_fragment_length = 0.3 #maximum fragment duration, proportion of signal length
L = min(len(signal), len(other_signal))
mask_length = int(L*np.random.rand()*max_fragment_length) #randomly choose mask length
mask_start = int((L-mask_length)*np.random.rand()) #randomly choose mask position
synth_signal = signal.copy()
synth_signal[mask_start:mask_start+mask_length] = other_signal[mask_start:mask_start+mask_length]
IPython.display.Audio(synth_signal, rate=sr)
通过对原始 MCV 样本 common_voice_en_100040(由作者生成)应用 cut&splice 变换得到的合成信号。
cut&splice 变换后的信号波形(从其他信号中插入的片段用橙色标示)(图片由作者提供)
下表显示了 AttNN 模型在验证集上对每个变换的准确率及其典型参数值:
Mozilla Common Voice 数据集上每个变换的 AttNN 准确率及其典型参数(图片由作者提供)。
如所见,这些变换没有显著改变我们基于 MCV 的语音识别系统的准确性。然而,这些变换有可能在某些其他数据集上提升性能。最后,在寻找最佳超参数时,逐个尝试这些变换而不是随机/网格搜索是有意义的。之后,可以将有效的变换结合在一起。
Mozilla Common Voice 上的口语语言识别——第一部分。
·
关注 发表在 Towards Data Science ·6 min read·2023 年 8 月 2 日
–
图片由 Sebastian Unrau 在 Unsplash 提供
其中一个最具挑战性的人工智能任务是识别说话者的语言,以便进行后续的语音转文字转换。例如,当同一家人中讲不同语言的人使用同一个语音控制设备,如车库锁或智能家居系统时,这个问题可能会出现。
在这一系列文章中,我们将尝试通过使用Mozilla Common Voice(MCV)数据集来最大化口语语言识别的准确性。特别是,我们将比较几种神经网络模型,这些模型被训练用来区分德语、英语、西班牙语、法语和俄语。
在第一部分中,我们将讨论数据选择、预处理和嵌入。
数据选择
MCV 迄今为止是最大的公开语音数据集,包括多达 112 种语言的短录音(平均时长 = 5.3 秒)。
对于我们的语言识别任务,我们选择了 5 种语言:德语、英语、西班牙语、法语和俄语。对于德语、英语、西班牙语和法语,我们仅考虑 MCV 中标注的口音,即Deutschland Deutsch、United States English、España和Français de France。对于每种语言,我们从验证过的样本中选择一部分成人记录。
我们使用了 40K/5K/5K 的训练/验证/测试划分。为了获得客观评价,我们确保三组之间的说话者(client_id)不重叠。在数据拆分时,我们首先将测试和验证集填充来自表现不佳的说话者的记录,然后将剩余的数据分配到训练集中。这提高了验证/测试集中说话者的多样性,并导致了对泛化误差的更客观估计。为了避免单一说话者在训练集中占主导地位,我们将每个client_id的记录最大数量限制为 2000 个。平均来说,我们得到了每个说话者 26 个记录。我们还确保女性记录的数量与男性记录的数量匹配。最后,如果最终记录数量低于 40K,我们对训练集进行了上采样。最终的记录分布如下面的图所示。
训练集中的类别分布(图像由作者提供)。
带有指示拆分的最终数据框架可以在这里找到。
数据预处理
所有 MCV 音频文件都以.mp3 格式提供。虽然.mp3 非常适合音乐的紧凑存储,但它在音频处理库(如 python 中的 librosa)中支持有限。因此,我们首先需要将所有文件转换为.wav 格式。此外,原始 MCV 采样率为 44kHz。这意味着最大编码频率为 22kHz(根据奈奎斯特定理)。对于口语语言识别任务来说,这样的频率有些过高:例如,在英语中,大多数音素在会话语音中不会超过 3kHz。因此,我们也可以将采样率降低到 16kHz。这不仅会减少文件大小,还会加快嵌入生成的速度。
这两个操作可以通过ffmpeg的一条命令来执行:
ffmpeg -y -nostdin -hide_banner -loglevel error -i $input_file.mp3 -ar 16000 $output_file.wav
特征工程
相关信息通常通过计算嵌入从音频片段中提取。我们将考虑四种或多或少常见的用于语音识别/口语语言识别任务的嵌入:梅尔频谱图、MFCC、RASTA-PLP 和 GFCC。
梅尔频谱图
梅尔频谱图的原理已广泛讨论 在 Medium上。关于梅尔频谱图和 MFCC 的精彩逐步教程也可以在此处找到。
为了获得梅尔频谱图,首先对输入信号进行预加重滤波。然后,对滑动窗口应用于获得的波形进行连续的傅里叶变换。之后,频率尺度被转换为梅尔尺度,这与人类对间隔的感知是线性的。最后,应用一组重叠的三角滤波器的滤波器组到梅尔尺度上的功率谱,以模拟人耳对声音的感知。
MFCC
梅尔系数高度相关,这可能对一些机器学习算法不利(例如,高斯混合模型更方便使用对角协方差矩阵)。 去相关梅尔滤波器组,梅尔频率倒谱系数(MFCC)通过对对数滤波器组能量进行离散余弦变换(DCT)获得。通常只使用前几个 MFCC。确切步骤详见此处。
RASTA-PLP
知觉线性预测(PLP)(Hermansky 和 Hynek,1990)是计算音乐片段嵌入的另一种方法。
PLP 和 MFCC 之间的差异在于滤波器组、等响预加重、强度到响度的转换以及线性预测的应用(Hönig 等,2005)。
PLP 和 MFCC 技术概述(来自 Hönig 等,2005)
有报告称,PLP(Woodland 等,1996)在训练和测试数据之间存在声学不匹配时,比 MFCC 更具鲁棒性。
与 PLP 相比,RASTA-PLP(Hermansky 等,1991)在对数频谱域中执行额外的滤波,这使得该方法对通信通道引入的线性频谱失真更为鲁棒。
GFCC
有报告称,伽玛音调频谱系数(GFCC)比 MFCC 对噪声的敏感性更低(Zhao,2012;Shao,2007)。与 MFCC 相比,伽玛音调滤波器是在等效矩形带宽尺度上计算的(而不是梅尔尺度),并且在计算 DCT 之前应用了立方根操作(而不是对数)。
下图显示了一个示例信号及其不同的嵌入:
示例音频文件及其嵌入(图片由作者提供)。
比较嵌入
为了选择最有效的嵌入,我们训练了 De Andrade 等人(2018 年)提出的注意力 LSTM 网络。由于时间原因,我们只训练了 5000 个剪辑。
下图比较了所有嵌入的验证准确性。
不同嵌入在 5000 个数据集上的表现(图片由作者提供)。
因此,前 13 个滤波器组的 mel 频谱图的表现接近于model_order=13 的 RASTA-PLP。
值得注意的是,mel 频谱图的表现优于 MFCC。这符合之前的说法(见这里,以及这里),即 mel 频谱图是神经网络分类器的更好选择。
另一个观察是,通常系数数量越多,性能会下降。这可能是由于过拟合,因为高阶系数通常代表与说话者相关的特征,这些特征在测试集中(不同说话者被选择)不可泛化。
由于时间限制,我们没有测试任何嵌入组合,尽管之前有观察到它们可能提供更好的准确性。
由于 mel 频谱图的计算速度远快于 RASTA-PLP,我们将在进一步的实验中使用这些嵌入。
在第二部分,我们将运行几个神经网络模型,并选择分类效果最佳的模型。
参考文献
-
De Andrade, Douglas Coimbra, 等. “用于语音命令识别的神经注意力模型。” arXiv 预印本 arXiv:1808.08929(2018 年)。
-
Hermansky, Hynek. “语音的感知线性预测(PLP)分析。” 美国声学学会期刊 87.4(1990 年):1738–1752。
-
Hönig, Florian, 等. “修订感知线性预测(PLP)。” 第九届欧洲语音通信与技术会议。2005 年。
-
Hermansky, Hynek, 等. “RASTA-PLP 语音分析。” IEEE 国际声学、语音与信号处理会议论文集。第 1 卷。1991 年。
-
Shao, Yang, Soundararajan Srinivasan, 和 DeLiang Wang. “在鲁棒说话人识别中引入听觉特征不确定性。” 2007 年 IEEE 国际声学、语音与信号处理会议-ICASSP’07。第 4 卷。IEEE,2007 年。
-
Woodland, Philip C., Mark John Francis Gales, 和 David Pye. “在大词汇量语音识别中提高环境鲁棒性。” 1996 IEEE 国际声学、语音与信号处理会议论文集。第 1 卷. IEEE, 1996.
-
Zhao, Xiaojia, Yang Shao, 和 DeLiang Wang. “基于 CASA 的鲁棒说话人识别。” IEEE 音频、语音与语言处理汇刊 20.5 (2012): 1608–1616.
在 Mozilla Common Voice 上的语音语言识别 — 第二部分:模型。
·
关注 发布于 Towards Data Science · 7 分钟阅读 · 2023 年 8 月 6 日
–
图片由 Jonathan Velasquez 提供,来源于 Unsplash
这是基于 Mozilla Common Voice 数据集的语音语言识别系列文章的第二篇。在 第一部分 中,我们讨论了数据选择并选择了最佳嵌入。现在让我们训练几个模型并选择最佳模型。
模型比较
我们现在将对以下模型在完整数据(40K 样本,请参见第一部分获取更多数据选择和预处理信息)上进行训练和评估:
· 卷积神经网络(CNN)模型。我们简单地将语言分类问题视为 2 维图像的分类。基于 CNN 的分类器在语言识别 TopCoder 比赛中显示了有希望的结果。
CNN 架构(图像由作者提供,使用PlotNeuralNet创建)
· 来自 Bartz 等人 2017 的 CRNN 模型。CRNN 结合了 CNN 的描述能力和 RNN 捕捉时间特征的能力。
CRNN 架构(图像来自 Bartz 等,2017)
· 来自 Alashban 等人 2022 的 CRNN 模型。这只是 CRNN 架构的另一个变体。
· AttNN:来自 De Andrade 等人 2018 的模型。该模型最初用于语音识别,后来在智能博物馆项目中应用于口语语言识别。除了卷积和 LSTM 单元,该模型还有一个后续的注意力块,经过训练以根据其分类相关性对输入序列的部分(即计算傅里叶变换的帧)进行加权。
· CRNN* 模型:与 AttNN 相同的架构,但没有注意力块。
· 时间延迟神经网络(TDNN)模型。我们在这里测试的模型用于生成 Snyder 等人 2018 的口语语言识别的 X-vector 嵌入。在我们的研究中,我们绕过 X-vector 生成,直接训练网络来分类语言。
所有模型均基于相同的训练/验证/测试拆分和相同的梅尔谱图嵌入(前 13 个梅尔滤波器组系数)进行训练。模型可以在这里找到。
验证集上的学习曲线如下图所示(每个“epoch”指的是数据集的 1/8)。
各种模型在 Mozilla Common Voice 数据集上的表现(图像由作者提供)。
下表显示了基于 10 次运行的准确率的均值和标准差。
每个模型的准确性(图像由作者提供)
可以清楚地看到,AttNN、TDNN 和我们的 CRNN* 模型表现相似,其中 AttNN 以 92.4% 的准确率排名第一。另一方面,CRNN(Bartz 等人 2017)、CNN 和 CRNN(Alashban 等人 2022)表现相当逊色,CRNN(Alashban 等人 2022)以仅 58.5% 的准确率位列末尾。
然后我们在训练和验证集上训练了获胜的 AttNN 模型,并在测试集上进行了评估。92.4%的测试准确率(男性 92.4%,女性 92.3%)与验证准确率接近,这表明模型没有在验证集上过拟合。
为了理解评估模型之间的性能差异,我们首先注意到,TDNN 和 AttNN 是专门为语音识别任务设计的,并已针对先前的基准进行了测试。这可能是这些模型表现优异的原因。
AttNN 模型与我们的 CRNN 模型(相同架构但没有注意力块)之间的性能差距证明了注意力机制在口语语言识别中的相关性。接下来的 CRNN 模型(Bartz et al. 2017)尽管架构类似,但表现较差。这可能只是因为默认的模型超参数不适合 MCV 数据集。
CNN 模型不具有特定的记忆机制,紧随其后。严格来说,CNN 有某种记忆的概念,因为计算卷积涉及固定数量的连续帧。因此,由于 CNN 的层次结构,较高层会封装更长时间间隔的信息。实际上,得分第二的 TDNN 模型,可以视为 1-D CNN。因此,如果在 CNN 架构搜索上投入更多时间,CNN 模型的表现可能会接近 TDNN。
Alashban 等人 2022 年的 CRNN 模型意外地显示出最差的准确率。有趣的是,该模型最初设计用于在 MCV 中识别语言,并显示出约 97%的准确率,如原始研究所报告。由于原始代码未公开,因此很难确定这种巨大差异的来源。
成对准确率
在许多情况下,用户通常使用不超过 2 种语言。在这种情况下,更合适的模型性能指标是成对准确率,它仅仅是计算在给定语言对上的准确率,忽略所有其他语言的得分。
测试集中 AttNN 模型的成对准确率如下面的表格所示,混淆矩阵旁边,个别语言的召回率在对角线上。平均成对准确率为 97%。成对准确率总是高于准确率,因为只需要区分 2 种语言。
混淆矩阵(左)和 AttNN 模型的成对准确率(右)(图像由作者提供)。
因此,该模型在德语(de)和西班牙语(es)以及法语(fr)和英语(en)之间的区分能力最佳(98%)。这并不令人惊讶,因为这些语言的语音系统差异很大。
尽管我们使用了 softmax 损失来训练模型,但之前有报道指出,使用tuplemax 损失(Wan et al. 2019)在成对分类中可能获得更高的准确率。
为了研究 tuplemax 损失的影响,我们在 PyTorch 中实现了 tuplemax 损失,并重新训练了我们的模型(详见这里)。下图比较了在验证集上评估时 softmax 损失和 tuplemax 损失对准确率和成对准确率的影响。
使用 softmax 和 tuplemax 损失计算的 AttNN 模型的准确率和成对准确率(作者提供的图片)。
可以观察到,当比较整体准确率(成对 t 检验 p 值=0.002)或成对准确率时,tuplemax 损失的表现更差(成对 t 检验 p 值=0.2)。
实际上,即使原始研究也未能清楚地解释为何 tuplemax 损失应该表现更好。以下是作者提出的例子:
tuplemax 损失的解释(来自于 Wan 等人,2019 年的图片)
损失的绝对值实际上并不意味着太多。通过足够的训练迭代,这个例子可能会用一个或另一个损失函数正确分类。
无论如何,tuplemax 损失并非一种通用解决方案,损失函数的选择应该针对每个特定的问题进行谨慎利用。
结论
我们在 Mozilla Common Voice(MCV)数据集的短音频剪辑中实现了 92%的准确率和 97%的成对准确率,涉及德语、英语、西班牙语、法语和俄语。
在初步研究中,比较了 mel 频谱图、MFCC、RASTA-PLP 和 GFCC 嵌入,我们发现带有前 13 个滤波器组分的 mel 频谱图具有最高的识别准确率。
接下来,我们比较了 5 个神经网络模型的泛化性能:CNN、CRNN(Bartz 等人,2017)、CRNN(Alashban 等人,2022)、AttNN(De Andrade 等人,2018)、CRNN*和 TDNN(Snyder 等人,2018)。在所有模型中,AttNN 展示了最佳性能,突显了 LSTM 和注意力模块在语音语言识别中的重要性。
最后,我们计算了成对准确率并研究了 tuplemax 损失的影响。结果表明,与 softmax 相比,tuplemax 损失同时降低了准确率和成对准确率。
总之,我们的结果为 Mozilla Common Voice 数据集上的语音语言识别建立了新的基准。通过结合不同的嵌入和广泛探索有前景的神经网络架构,例如变压器,未来研究可以取得更好的结果。
在第三部分中,我们将讨论哪些音频转换可能有助于提高模型性能。
参考文献
-
Alashban, Adal A., 等人。“使用卷积递归神经网络进行语音识别系统。” 应用科学 12.18 (2022): 9181。
-
Bartz, Christian 等人。“使用深度卷积递归神经网络进行语言识别。” Neural Information Processing: 24th International Conference, ICONIP 2017, Guangzhou, China, November 14–18, 2017, Proceedings, Part VI 24。Springer International Publishing, 2017 年。
-
De Andrade, Douglas Coimbra 等人。“用于语音命令识别的神经注意力模型。” arXiv 预印本 arXiv:1808.08929(2018 年)。
-
Snyder, David 等人。“使用 x-vectors 进行口语语言识别。” Odyssey。第 2018 卷。2018 年。
-
Wan, Li 等人。“用于语言识别的 Tuplemax 损失。” ICASSP 2019–2019 IEEE 国际声学、语音与信号处理会议(ICASSP)。IEEE, 2019 年。
30 个 SQL 查询通过它们的 Pandas 等效体进行解释
原文:
towardsdatascience.com/sql-for-people-who-love-pandas-a-10-minute-yet-thorough-tutorial-c189de9d417d
SQL 对喜欢 Pandas 的人来说变得更简单了
·发布于Towards Data Science ·阅读时间 10 分钟·2023 年 6 月 9 日
–
图片由我使用 Midjourney 制作
动机
自 1974 年起 SQL 主导了数据世界,而 Pandas 在 2008 年出现,提供了内置可视化和灵活的数据处理等吸引人的功能。它迅速成为数据探索的首选工具,掩盖了 SQL 的光芒。
但不要被迷惑,SQL 仍然保持其地位。它是数据科学领域第二受欢迎且第三增长最快的语言(见这里)。所以,虽然 Pandas 抢占了风头,SQL 仍然是任何数据科学家必备的技能。
让我们看看当你已经了解 Pandas 时学习 SQL 有多么简单。
连接到数据库
设置 SQL 工作区并连接到示例数据库可能非常麻烦。首先,你需要安装你喜欢的 SQL 类型(PostgreSQL、MySQL 等)并下载一个 SQL IDE。在这里进行这些操作会偏离文章的目的,因此我们将使用一个快捷方式。
具体来说,我们将直接在 Jupyter Notebook 中运行 SQL 查询,而无需额外的步骤。我们需要做的只是使用 pip 安装ipython-sql
包:
pip install ipython-sql
安装完成后,启动一个新的 Jupyter 会话,并在笔记本中运行以下命令:
%load_ext sql
一切准备就绪!
为了说明基本的 SQL 语句如何工作,我们将使用Chinook SQLite 数据库,它包含 11 个表。
要将数据集及其 11 个表作为单独的变量加载到我们的环境中,我们可以运行:
%sql sqlite:///data/chinook.db
这个语句以%sql
内联魔法命令开头,这告诉笔记本解释器我们将执行 SQL 命令。接下来是下载的 Chinook 数据库所在的路径。
有效的路径应该始终以 sqlite:///
前缀开头,用于 SQLite 数据库。上面,我们连接到当前目录的 ‘data’ 文件夹中存储的数据库。如果你想传递绝对路径,前缀应该以四个斜杠开头 - sqlite:
如果你希望连接到不同的数据库风格,你可以参考这篇优秀的文章。
初步查看表格
我们在 Pandas 中总是首先使用 .head()
函数来初步查看数据。让我们学习如何在 SQL 中做到这一点:
数据集也允许用于商业用途。
上述查询中的第一个关键字是 SELECT
。它相当于 Pandas 中的括号运算符,用于选择特定列。但 SELECT 关键字后面跟着一个 (星号)。**** 是一个 SQL 操作符,用于从 FROM
关键字之后指定的表中选择所有内容(所有行和列)。LIMIT 用于最小化返回的输出。因此,上述查询等同于 df.head()
函数。
如果你不想选择所有列,你可以在 SELECT 关键字后指定一个或多个列名:
等效的 Pandas 操作是:
tracks[['Name', 'Composer', 'UnitPrice']].head(10)
SQL 中另一个有用的关键字是 DISTINCT
。在任何列名之前添加此关键字将返回其唯一值:
SQL 中的注释是用双短横线写的。
计算行数
就像 Pandas 的 DataFrames 上有 .shape
属性一样,SQL 有 COUNT
函数来显示表中的行数:
%%sql
SELECT COUNT(*) FROM tracks
更有用的信息是计算特定列中唯一值的数量。我们可以通过将 DISTINCT 关键字添加到 COUNT 中来做到这一点:
使用 WHERE 子句过滤结果
仅仅查看和计算行数有点无聊。让我们看看如何基于条件过滤行。
首先,让我们查看价格超过一美元的歌曲:
价格超过一美元的歌曲。
条件语句写在 WHERE 子句中,WHERE 子句总是位于 FROM 和 LIMIT 关键字之间。使用条件与在 Pandas 中类似,但我敢说 SQL 版本更易读。
你也可以在使用条件时使用 COUNT 函数。例如,让我们查看价格在 1 到 10 美元之间的歌曲数量:
正如你所看到的,SQL 版本要易读得多。
价格在 1 到 10 美元之间的歌曲数量。
上面我们使用布尔运算符 AND 链接了两个条件。其他布尔运算符(OR、NOT)也类似使用。
现在,让我们查看所有开票城市为巴黎 或 柏林的发票:
从柏林或巴黎开具的发票
SQL 中的等于运算符只需要一个‘=’(等号)。不等于运算符用‘!=’或‘<>’表示:
账单城市不是柏林或巴黎的发票
使用 BETWEEN 和 IN 进行更简单的过滤
类似的条件非常常用,用简单的布尔运算符编写起来会很麻烦。例如,Pandas 有 .isin()
函数检查一个值是否属于值列表。
如果我们想选择五个城市的发票,我们将不得不编写五个链式条件。幸运的是,SQL 支持类似 .isin()
的 IN 运算符,所以我们不需要:
IN 之后的值列表应该给出为元组,而不是列表。你也可以用 NOT 关键字来否定条件:
对数字列的另一个常见过滤操作是选择范围内的值。为此,可以使用 BETWEEN 关键字,这等同于 pd.Series.between()
:
选择账单金额在 5 到 15 之间的发票。
检查空值
每个数据源都有缺失值,数据库也不例外。就像有几种方法可以在 Pandas 中探索缺失值一样,SQL 中有特定的关键字用于检查空值的存在。下面的查询计算 BillingState 中缺失值的行数:
你可以在 IS 和 NULL 之间添加 NOT 关键字,以丢弃某一列的缺失值:
使用 LIKE 进行更好的字符串匹配
在 WHERE 子句中,我们根据精确的文本值过滤列。但通常,我们可能希望根据模式过滤文本列。在 Pandas 和纯 Python 中,我们会使用正则表达式进行模式匹配,这非常强大,但正则表达式需要时间来掌握。
作为替代,SQL 提供了‘%’通配符作为占位符,可以匹配任意字符 0 次或多次。例如,‘gr%’ 字符串匹配‘great’,‘groom’,‘greed’,‘%ex%’ 匹配任何中间有‘ex’的文本等。让我们看看如何在 SQL 中使用它:
上述查询找到所有以‘B’开头的歌曲。包含通配符的字符串应出现在 LIKE 关键字之后。
现在,让我们找到所有标题中包含‘beautiful’一词的歌曲:
你还可以在 LIKE 旁边使用其他布尔运算符:
SQL 中还有许多其他通配符具有不同的功能。你可以在这里查看完整列表及其用法。
SQL 中的聚合函数
对列进行基本的算术运算也是可能的。这些运算在 SQL 中称为聚合函数,最常见的有AVG、SUM、MIN、MAX
。它们的功能从名称中应当可以清楚地了解到:
聚合函数只会对你使用它们的列给出一个结果。这意味着你不能对一个列进行聚合并选择其他未聚合的列:
你还可以使用WHERE
子句将聚合函数与条件结合使用:
也可以对列和简单数字使用算术运算符,如 +、-、*、/。当作用于列时,操作是逐元素进行的:
关于算术运算,有一点需要注意:如果你仅对整数执行操作,SQL 会认为你期望整数作为答案:
%%sql
SELECT 10 / 3
结果是 3 而不是 3.33…。为了获得浮点结果,你应在查询中使用至少一个浮点数或使用全部浮点数以确保安全:
%%sql
SELECT 10.0 / 3.0
利用这些知识,让我们计算歌曲的平均时长(分钟):
如果你注意到上面的列,它的名字写作“生成该列的查询。”由于这种行为,使用长计算,如计算列的标准差或方差,可能会成为问题,因为列名将和查询本身一样长。
为了避免这种情况,SQL 允许使用别名,类似于 Python 中的导入语句别名。例如:
在SELECT
语句中的单个项后使用as
关键字告诉 SQL 我们正在使用别名。这里是更多的示例:
对于长名称的列,你也可以同样轻松地使用别名。
SQL 中的结果排序
就像 Pandas 有sort_values
方法一样,SQL 通过ORDER BY
子句支持对列进行排序。在子句后传递列名会将结果按升序排列:
我们按作曲家的名字升序排列tracks
表。请注意,ORDER BY
语句应始终在WHERE
子句之后。也可以将两个或多个列传递给ORDER BY
:
你还可以通过在每个列名后传递DESC
关键字来反转排序:
上述查询在按UnitPrice
和Compose
降序排列以及name
升序排列后返回三列(ASC
是默认关键字)。
SQL 中的分组
Pandas 最强大的功能之一是groupby
。你可以用它将表格转变成几乎任何你想要的形状。在 SQL 中,与之非常接近的是GROUP BY
子句,也可以用来实现相同的功能。例如,下面的查询计算了每个流派的歌曲数量:
SQL 中的 GROUP BY 和 Pandas 中的groupby
之间的区别在于 SQL 不允许选择在 GROUP BY 子句中没有给出的列。例如,在上面的查询中添加一个额外的自由列会产生错误:
然而,你可以在 SELECT 语句中选择任意多的列,只要你在这些列上使用了某种聚合函数:
上面的查询包括了我们迄今为止学习的几乎所有主题。我们按专辑 ID 和流派 ID 进行分组,并为每个组计算了歌曲的平均时长和价格。我们也有效地利用了别名。
我们可以通过按平均时长和流派数量排序来使查询更强大:
注意我们在 ORDER BY 子句中如何使用聚合函数的别名。一旦你对列或聚合函数的结果进行了别名,你可以在查询的其余部分只通过别名来引用它们。
使用 HAVING 条件
默认情况下,SQL 不允许在 WHERE 子句中使用聚合函数进行条件过滤。例如,我们想选择歌曲数量大于 100 的流派。让我们用 WHERE 子句尝试一下:
基于聚合函数结果过滤行的正确方法是使用 HAVING 子句:
HAVING 子句通常与 GROUP BY 一起使用。每当你想使用聚合函数过滤行时,HAVING 子句是最合适的选择!
所有图片均由我自己生成。
摘要
到现在为止,你应该已经意识到 SQL 有多强大,以及与 Pandas 相比,它变得多么可读。尽管我们学到了很多,但我们仅仅是触及了表面。
对于练习题,我推荐Data Lemur或者LeetCode,如果你有冒险的心情。
喜欢这篇文章及其奇特的写作风格?想象一下能访问到更多类似的文章,全部由一个才华横溢、迷人、机智的作者(顺便说一下就是我 😃)撰写。
只需支付 4.99$的会员费用,你将不仅可以访问我的故事,还能获得来自 Medium 上最杰出的头脑的知识宝库。如果你使用我的推荐链接,你将获得我的超级感激和一个虚拟的击掌以支持我的工作。
[## 使用我的推荐链接加入 Medium — Bex T.
获取对我所有⚡优质⚡内容的独家访问权限,以及在 Medium 上无限制地访问所有内容。通过为我购买一份来支持我的工作…
ibexorigin.medium.com](https://ibexorigin.medium.com/membership?source=post_page-----c189de9d417d--------------------------------)
SQL 在 Pandas 上——我新的最爱,速度提升 10 倍。
原文:
towardsdatascience.com/sql-on-pandas-usign-duckdb-f7cd238a0a5a
将两者的最佳特点结合起来
·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 3 月 23 日
–
照片由 Akira Hojo 提供,发布在 Unsplash
我热衷于寻找加速分析任务的方法。
我以前写过几篇关于内存使用、运行时间和加速数据密集型任务的技巧的文章。由于我们主要每天使用 Pandas,我不断研究改进性能的方法。
匹配文本中没有完美匹配的部分
towardsdatascience.com
这个令人印象深刻的 Python 库功能丰富且可靠。但速度并不是它的超级能力之一。通过一些优化,我们可以获得一些速度提升。但这些技术也有它们的问题。
在完全更换数据模型之前尝试的一些简单技巧
towardsdatascience.com
像 Polars 这样的新替代品也在获得关注。但我仍然偏爱 Pandas,就像许多其他数据专业人士一样。
这就是为什么我们使用数据库而不是内存中的数据框的原因之一。数据库被设计用来从磁盘检索数据并高效地执行计算。
然而,大多数数据科学项目不需要数据库。这就像用大锤子砸坚果一样。这时,像 SQLite 这样的内存数据库就派上用场了。
在内存中,数据库没有单独的进程运行,也不需要复杂的设置。最新版本的 Python 自带 SQLite。
但是 SQLite 也有其缺点。虽然你可以使用 SQL 查询数据库,但速度上的好处并不显著。
部署机器学习模型并使其对用户或项目的其他组件可用。
towardsdatascience.com
然而,我们有一个现代化的解决方案——DuckDB。像 SQLite 一样,设置非常简单,你可以将数据库和代码一起移植。而且你可以比 Pandas API 和 SQLite 更快。
使用 DuckDB 查询 Pandas dataframe。
DuckDB 是一个独立的数据库。然而,你也可以将任何 dataframe 转换为 DuckDB 表并进行查询。但在做所有这些之前,这里是如何安装它的:
pip install duckdb
这可能会让你感到惊讶。但这就是我们安装 DuckDB 的方式。尽管它是一个数据库,但它是一个 Python 包进行安装。
由于这是一个简单的内存数据库,它没有像 Postgres 和其他常规数据库那样复杂的配置。你可以设置一些参数,你可以在文档中找到这些参数。
假设我们有一个出租车行程时长的数据集。我们需要计算那些起点在经度 -73.95 西侧的行程的平均时长。
如果你跟随这个教程,你可以使用下面的代码生成测试数据集。
import pandas as pd
import numpy as np
# Define the number of rows in the dataset
num_rows = 10000000
# Generate a random longitude for each row
pickup_longitude = np.random.uniform(low=-38.0, high=-94.0, size=num_rows)
# Generate a random trip duration for each row
trip_duration = np.random.normal(loc=10, scale=5, size=num_rows)
# Create a DataFrame with the pickup longitude and trip duration columns
df = pd.DataFrame(
{"pickup_longitude": pickup_longitude, "trip_duration": trip_duration}
)
我们使用了计时装饰器来测量运行所需的时间。
## 我在几乎所有数据科学项目中使用的 5 个 Python 装饰器
装饰器提供了一种新的便捷方式,用于从缓存到发送通知的各种操作。
towardsdatascience.com
...
@timing_decorator
def find_avg_trip_duration_in_the_west():
return df[df['pickup_longitude'] < -73.95]['trip_duration'].mean()
find_avg_trip_duration_in_the_west()
>> Function find_avg_trip_duration_in_the_west took 0.49195261001586914 seconds to run.
>> 9.995189356480168
上面的代码在我的计算机上运行了 0.2 秒。这里是使用 DuckDB API 而不是 Pandas API 的相同查询:
...
import duckdb
@timing_decorator
def find_avg_trip_duration_in_the_west():
return duckdb.execute(
'SELECT AVG(trip_duration) FROM df WHERE pickup_longitude < -73.95'
).df()
>> Function find_avg_trip_duration_in_the_west took 0.05432271957397461 seconds to run.
>> | | avg(trip_duration) |
>> |---:|---------------------:|
>> | 0 | 9.995189|
如你所见,使用 DuckDB API 的速度比原生 Pandas API 快十倍。
DuckDB 与 SQLite 的性能有何不同?
SQLite 是目前最受欢迎的内存数据库。这就是为什么我们在 Python 中默认提供它。
但是 DuckDB 在两个方面表现出色。首先,如果你不需要持久化,你也不必创建它。你可以像查询数据库表一样直接查询你的 Pandas dataframe。这就是我们在前一个示例中所做的。
## 6 个 Python GUI 框架,用于创建桌面、网页和甚至移动应用。
你可以纯粹用 Python 构建美丽的应用程序。
towardsdatascience.com
另一个好处是 DuckDB 仍然比 SQLite 更快。实际上,将本文中的示例复制到 SQLite 上运行的速度比 Pandas 慢。这使得 SQLite 在性能优先时成为最后的选择。但它仍然非常擅长持久化和移植数据及代码。
这是我们示例的 SQLite 版本:
...
import sqlite3
conn = sqlite3.connect("taxi.db")
df.to_sql("trips", conn)
@timing_decorator
def find_avg_trip_duration_in_the_west():
cursor = conn.cursor()
cursor.execute(
"SELECT AVG(trip_duration) FROM trips WHERE pickup_longitude < -73.95"
)
result = cursor.fetchone()[0]
cursor.close()
return result
find_avg_trip_duration_in_the_west()
>> Function find_avg_trip_duration_in_the_west took 0.5150568962097168 seconds to run.
>> 9.995189
如你所见,在 Pandas 上运行 SQLite 的 SQL 需要我们在磁盘上创建一个单独的数据库实例,并将数据插入其中。如果我们使用 DuckDB,这些步骤都是不必要的。
此外,它花费了 0.51 秒,而 Pandas API 只需 0.49 秒。
结论
Pandas 毫无疑问是让 Python 成为数据科学热门选择的奇迹之一。然而,这个成熟的库使得数据整理任务变得缓慢。
在这篇文章中,我们讨论了使用 DuckDB 的简单查询 API 来提高速度。如果你了解 SQL,可以直接用 SQL 查询你的 Pandas 数据框,而不是使用 Pandas API。
尽管 DuckDB 有优点,但它并不是万灵药。
对于大规模项目,你仍然需要可扩展的数据库,比如 Postgres。而且,使用 Pandas API 可能开发更快,且所有懂 Python 的人都能理解它们。
感谢阅读,朋友!如果你喜欢我的文章,让我们在LinkedIn、Twitter 和Medium保持联系。
还不是 Medium 会员?请使用这个链接成为会员,因为对你没有额外费用,我会因推荐你而获得少量佣金。
测试你的智慧的 SQL 谜题
原文:
towardsdatascience.com/sql-riddles-to-test-your-wits-8ce31202ae7f
时间戳、依赖过滤器和表现异常的左连接
·发布于 Towards Data Science ·阅读时长 8 分钟·2023 年 2 月 22 日
–
SQL 是一种看似简单的语言。通过其多种方言,用户可以使用类似英语的语法查询数据库。你看到的就是你得到的……直到你发现不是。
我时不时会遇到一些查询,它们的结果与我预期的完全不同,这教会了我一些语言中的细微差别。我在这篇文章中汇编了三个最近的难题,并将它们以谜题的形式排列,使其更有趣。尝试在阅读每个部分的结尾之前找出答案!
我还包含了快速的 公共表表达式(CTEs) 来生成每个示例中的表格,因此你不需要尝试查询你公司的生产表格!但要真正熟练掌握 SQL,我实际上建议你创建自己的数据库和表格进行练习。查看 这篇文章 了解如何操作。
请注意,所有查询均为 Postgres 语法 —— 在其他方言中你可能会得到不同的结果。最后,必须说明的是,每个查询中的实际数据和主题仅为示例。 🙂
图片由 Akram Huseyn 提供,来源于 Unsplash
谜题 1:时间戳的具体性
假设我们有一个名为 purchases
的表,其中包含购买 ID、金额以及购买时间。假设它看起来是这样的:
图片由作者提供
作为 CTE,它大致看起来像这样。请注意,我们需要指定dt
列是时间戳,以免被解释为字符串。我们只需要为其中一行指定数据类型;其余的会被推断。
WITH purchases(id, amount, dt) AS (
VALUES
(1::bigint, 0.99::float, '2023-02-15 00:00:00 GMT'::timestamp),
(2, 9.99, '2023-02-15 07:15:00 GMT'),
(3, 15.99, '2023-02-15 23:01:15 GMT'),
(4, 7.99, '2023-02-16 14:22:09 GMT')
)
...
现在让我们计算 2 月 15 日的购买总额。我们可以写一个如下的查询:
...
SELECT
SUM(amount) AS sum
FROM purchases
WHERE
dt = '2023-02-15'
我们神秘地收到以下响应。
作者提供的图片
发生了什么?2 月 15 日有三笔购买:ID 1、2 和 3。总和应该是$26.97。然而,只计算了第一笔购买。
提示
如果你将过滤器更改为2023-02-16
,则没有行返回。
答案
dt
列格式是包含日期和时间的时间戳。我们的WHERE
过滤器只指定了日期。Postgres 不会拒绝此查询,而是自动将日期字符串重新格式化为2023-02-15 00:00:00
。这仅匹配表中的第一笔交易,因此我们只计算了一行的总和。
如果我们想选择对应于 2 月 15 日的所有行,我们应该首先将时间戳转换为日期。
SELECT
SUM(amount) AS sum
FROM purchases
WHERE
DATE(dt) = '2023-02-15'
我们现在得到了预期的结果。
作者提供的图片
照片由Womanizer Toys提供,来源于Unsplash
谜题 2:依赖过滤器与独立过滤器
好的,下一个谜题。我们有一个名为users
的表,我们的目标是删除符合任意一个三种条件的所有行。在下表中,例如,假设我们只想返回有职位并且活跃的用户,即那些在过去 28 天内登录过,曾经发过帖,并且不是新账户。
作者提供的图片
换句话说,我们希望我们的查询仅使用 8 号用户,该用户在no_login_l28
、has_never_posted
和is_new_account
上都有 False 值。
让我们从查询的顶部开始。
WITH users(id, no_login_l28, has_never_posted, is_new_account) AS (
VALUES
(1, True, True, True),
(2, True, True, False),
(3, True, False, True),
(4, True, False, False),
(5, False, True, True),
(6, False, True, False),
(7, False, False, True),
(8, False, False, False)
)
SELECT
id
FROM users
WHERE
...
我们应该如何构建查询的WHERE
子句?考虑一下——我们需要小心不要返回任何列为 **False**
的行。
当你准备好时,查看下面的选项。两个是正确的,两个是错误的。
选项 1:多个 **AND NOT**
WHERE
NOT no_login_l28
AND NOT has_never_posted
AND NOT is_new_account
选项 2:多个 **OR NOT**
WHERE
NOT no_login_l28
OR NOT has_never_posted
OR NOT is_new_account
选项 3: **NOT**
+ 分组 **OR**
WHERE
NOT (
no_login_l28
OR has_never_posted
OR is_new_account
)
选项 4: **NOT**
+ 分组 **AND**
WHERE
NOT (
no_login_l28
AND has_never_posted
AND is_new_account
)
提示
条件在过滤器中是分别评估还是一起评估?如果它们一起评估,我们能否将所有条件浓缩为一个True
或False
值?
答案
选项 1. 这个让我有点困惑。我的团队中的一位数据科学家提交了一个包含这个过滤器的 PR,我确信它会提取行 2–7,因为查询只会移除所有三个列的值为 False
的用户。但令我惊讶的是,选项 1 实际上有效 因为三个过滤器是独立评估的。 ✅
选项 2. 这是我最初认为正确的过滤器,因为我没有意识到这些过滤器会被独立评估。但实际上这个过滤器会返回用户 2–8,因为任何在 no_login_l28
、has_never_posted
和 is_new_account
中至少有一个 True
的用户都会被允许通过。 ❌
选项 3. 这是我最初认为过滤器需要这样表达的方式。如果用户在 no_login_l28
、has_never_posted
或 is_new_account
中有 任何 一个 True
,那么第 3 行到第 5 行评估为 True
,NOT
将其翻转为 False
,这些行最终会被排除。这确实有效,我发现这比选项 1 更容易理解,但两者都是有效的。 ✅
选项 4. 这会返回与选项 2 相同的错误结果。第 3 行到第 5 行仅对用户 1 评估为 True
,这意味着当我们用 NOT
取反时,所有剩余用户都会被提取出来。 ❌
Nick Fewings 在 Unsplash 上的照片
谜题 3:左连接像内连接一样工作
看一下下面的查询。我们有两个表,customers
和 reviews
。customers
包含客户 ID 及其在平台上花费的终身金额。
作者提供的图片
reviews
包含客户留下的评论信息:评论 ID、客户 ID 和评论是否被报告为垃圾评论。
作者提供的图片
这是生成两个 CTE 的子查询:
WITH customers(id, total_spend) AS (
VALUES
(100, 1583.49),
(200, 8739.03),
(300, 431.00),
(400, 1.00),
(500, 22.27)
),
reviews(id, customer_id, reported_as_spam) AS (
VALUES
(1, 100, False),
(2, 100, False),
(3, 400, True),
(4, 400, True),
(5, 500, False)
)
...
现在假设我们对客户的总消费与他们写的非垃圾评论数量之间的关系感到好奇。由于不是每个客户都留下了评论,我们希望将 reviews
左连接到 customers
。我们可以这样构建我们的查询:
...
SELECT
c.id,
c.total_spend,
COALESCE(COUNT(r.id), 0) AS n_reviews
FROM customers c
LEFT JOIN reviews r
ON c.id = r.customer_id
WHERE
NOT r.reported_as_spam
GROUP BY
1, 2
ORDER BY
1
准备好了吗?看看结果吧。
作者提供的图片
等一下。用户 200、300 和 400 去哪里了?为什么它们被移除了,我们怎么能把它们找回来呢?
提示
如果你创建一个过滤掉垃圾评论的 reviews
CTE,然后在这个 CTE 上进行连接,我们会得到相同的结果吗?
答案
仔细查看,我们可以看到用户 200 和 300 从未留下任何评论。400 只有垃圾评论,但它们也被完全移除。由于我们进行了左连接,这些用户仍应存在于表中,并且 n_reviews
应为 0。相反,我们的左连接 表现得像内连接。
问题是,**WHERE**
子句是在连接操作之后进行评估的。我们的左连接带来了用户 200 和 300 的reported_as_spam
的空值。然后,WHERE
过滤器移除所有reported_as_spam
为 True 的行,这样用户 400 就被移除。然而,这个过滤器也会移除空值,因此用户 200 和 300 也被移除。
为了正确完成这一点,我们需要在与customers
连接之前预先过滤reviews
。正如提示所述,我们可以为reviews
创建一个 CTE,并在那里进行过滤。但更有效的是,我们可以在连接内部进行过滤。
我们可以通过在LEFT JOIN
块中添加AND NOT r.reported_as_spam
来实现。见下文:
...
SELECT
c.id,
c.total_spend,
COALESCE(COUNT(r.id), 0) AS n_reviews
FROM customers c
LEFT JOIN reviews r
ON c.id = r.customer_id
AND NOT r.reported_as_spam
GROUP BY
1, 2
ORDER BY
1
现在我们得到了预期的结果。
作者提供的图像
由Laura Chouette拍摄,来源于Unsplash
结论
本文分享了三种可能导致意外结果的 SQL 难点:时间戳的具体性、依赖性与独立性过滤器,以及左连接表现得像内连接。我特别提供了简单的示例,以保持对语法的关注,但你可能会在大型复杂查询中遇到类似的 SQL 细微差别。
这些错误可能非常难以识别,尤其是对于包含多个组件的查询。当我对结果感到困惑时,我会尝试将查询拆分成各个部分,并验证每个组件的结果。但是如果有疑问,写一些简单的 CTE 并用测试数据进行验证,确认结果是否符合预期。
祝查询愉快!
Matt