TowardsDataScience-2023-博客中文翻译-四十四-

TowardsDataScience 2023 博客中文翻译(四十四)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

晋级下一轮所需的前三种 SQL 技能

原文:towardsdatascience.com/the-top-3-sql-skills-needed-to-get-to-the-next-round-51ad1699a213

数据专业人士的技术面试帮助

Andre ViolanteTowards Data Science Andre Violante

·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 8 月 28 日

--

图片来源:Arnold Francisa at Unsplash

如果你有志于从事数据科学家、数据分析师和数据工程师等角色,并且正在面试,那么你可能会遇到一个或多个需要现场编码的技术面试,通常涉及 SQL。虽然后续面试可能需要其他编程语言,如 Python,这在数据领域很常见,但让我们专注于我在这些面试中遇到的典型 SQL 问题。为了讨论的目的,我会假设你已经熟悉基础的 SQL 概念,如 SELECTFROMWHERE 以及聚合函数如 SUMCOUNT。让我们进入具体细节吧!

1. 掌握连接和表类型

毫无疑问,最常见的 SQL 问题是关于表连接的。这个问题可能看起来太显而易见,但我参与过的每一次面试都围绕这个话题展开。你应该对内连接和左连接感到游刃有余。此外,处理自连接和并集的熟练程度也是非常重要的。执行这些连接操作时,特别是在事实表维度表等不同表类型之间的能力同样重要。以下是我对这两个术语的宽松定义:

事实表: 一种包含大量行但属性或列相对较少的表。比如,在线零售商维护一个“订单”表,包含如下列:date, customer_id, order_id, product_id, units, amount。这个表属性较少,但记录量巨大。

维度表: 具有较少行但许多属性的维度表。例如,某在线零售商的“customer”表可能包含每个客户的一行,具有customer_id, first_name, last_name, ship_street_addr, ship_zip_code等属性。

理解这两种主要表类型是很重要的。掌握如何合并事实表和维度表以确保准确结果是至关重要的。让我们考虑一个实际的例子:面试问题提出了两个表(“orders”“customer”)并询问:

有多少客户在其一生中至少购买了 3 个单位并且邮政编码为 90210?

仅仅运行内连接后再进行计数、求和和/或过滤可能会由于重复计算产生巨大的差异。即使在这个看似简单的问题中,许多部分也需要仔细思考。为了有效地将你的思路传达给面试官,请将这些事项考虑清楚并说出来:

  • 哪个表包含所有客户?“orders”事实表仅包括已确认购买的客户,而“customer”维度表包括所有客户,包括那些已注册但未购买的客户。通过快速的count(distinct )对比,可以看到哪个表有更多独特的客户。

  • 计算每个客户购买的单位总数或计数需要使用带有GROUP BY子句的聚合函数。

  • 需要与“customer”表进行连接,以缩小邮政编码为 90210 的客户范围。在使用连接时,我建议为表创建别名,这可能需要你在 select 语句中添加该别名以及相关属性。

概述了思路后,让我们将其转化为代码!

-- cte of sum of units per customer from orders fact table
WITH customer_units_agg as (
    SELECT
        customer_id
        , SUM(units) as UNIT_SUM
    FROM
        orders
    GROUP BY
        customer_id
)

-- join CTE table with customers dim table to filter by units and zip
SELECT
    COUNT(DISTINCT ca.customer_id) as CUSTOMER_CNT
FROM customer_units_agg ca
    INNER JOIN customer ctmr on ca.customer_id = ctmr.customer_id
WHERE 1=1
    AND ca.UNIT_SUM >= 3
    AND ctmr.ship_zip_code = 90210;

这个解决方案使用了公共表表达式(CTE),但请记住,有很多不同的方法可以在 SQL 中实现相同的结果。只要你能得到正确的结果并且能够解释你的方法,你就走在了正确的道路上。

2. 使用子查询、临时表和 CTE 处理复杂性

与之前的例子一样,几乎每次 SQL 编码面试都需要多步骤程序。这时候子查询、临时表和 CTE 就非常有用。熟练掌握这些技术或至少对它们有所了解是必须的。让我们深入探讨每种方法:

  • 子查询:这些嵌套查询涉及像select * from (select * from table)或过滤select * from table1 where table1_value < (select max(table2_value) from table2)这样的构造,包含内查询和外查询。子查询是有用的,但它们的语法可能对编写者和阅读者都变得困难或混乱。对于复杂场景,避免过多的子查询。

  • 临时表: 正如其名称所示,临时表仅在会话期间存在。你可以逐步创建它们,一个接一个地构建,以帮助故障排除或逻辑排序。它们有助于将复杂问题分解成较小、更易管理的步骤。

-- mysql and others
CREATE TEMPORARY TABLE my_temp_table as
    SELECT
        column1
        , column2
        , column3
    FROM original_table
    WHERE some_conditions;

-- sql server
SELECT
    column1
    , column2
    , column3
INTO #my_temp_table -- this is the new temp table created
FROM original_table
WHERE some_conditions;
  • 公共表表达式(CTE): 如连接示例所示,CTE 提供了一种灵活的查询结构方式。它们类似于临时表,但不会在下一个SELECT语句之后持续存在。这要求将 CTE“链式”连接(见下方代码示例),如果有很多步骤的话。在链式连接 CTE 时,请记住只使用一个WITH语句,后接 CTE,最后是主查询。此外,分号位于 CTE 链中的最终SELECT语句之后。由于其有限的作用域,CTE 不能在这一点之外被引用。
-- chaining multiple cte together
WITH cte_tabl_1 as (
    SELECT * FROM table1
),
cte_tabl_2 as (
    SELECT * FROM table2
),
cte_tabl_3 as (
    SELECT * FROM table3
)

select * from cte_tabl_1, cte_tabl_2, cte_tabl_3;

实际上,一旦你被聘用,你可以决定最佳的方法或其他人正在使用的方案。目前,CTE 似乎是更受欢迎的选择。鉴于面试情境通常不需要链式 CTE,融入 CTE 应该相对简单且有效。

3. 高级分析中的窗口函数导航

几乎每次面试中都会出现的一个反复话题是窗口函数。 PostgreSQL 文档 有效地解释了窗口函数,并突出了与分组操作的区别。

窗口函数在一组与当前行相关的表行上执行计算。这类似于可以通过聚合函数完成的计算。但与常规的聚合函数不同,使用窗口函数不会将行分组为单个输出行——行仍然保持各自的独立性。在幕后,窗口函数能够访问的不仅仅是查询结果的当前行。

关键在于最后两句话。行的非分组和窗口函数在幕后访问当前行以上的更多内容。面试问题通常涵盖从理论性问题,例如“你使用过哪些类型的窗口函数?”到需要使用窗口函数的编码问题。考虑诸如“显示每个部门中收入最高的三名员工”或“显示每位客户购买的最新三项商品”等场景。根据我的经验,最常见的窗口函数包括rank()row_number()dense_rank(),它们都使用OVER函数。值得注意的是,大多数聚合函数可以与OVER函数一起使用。有关排名函数差异的良好说明可以参见这个 Stack Overflow 示例。此外,LearnSQL 提供的这个 窗口函数备忘单 是一个很好的视觉参考,展示了不同类型的函数以及一些输出视觉效果。

结论

这些是过去一个月中面试官在编码面试中反复询问的前三大 SQL 技能。掌握这些领域应能轻松帮助你应对几乎所有 SQL 面试场景。

最后,这里是我的最后一个建议:如果你在编码面试中对语法或术语不确定,请写下逐步的大纲来展示你的问题解决过程。强调你的思维过程和创造力,因为它们往往比严格的语法更重要。祝你在即将到来的数据相关面试中好运!

GPT 模型的 Transformer 架构

原文:towardsdatascience.com/the-transformer-architecture-of-gpt-models-b8695b48728b

了解 Transformer 架构的详细信息

Beatriz StollnitzTowards Data Science Beatriz Stollnitz

·发表于 Towards Data Science ·阅读时间 22 分钟·2023 年 7 月 25 日

--

图片由 fabio 提供,来源于 Unsplash

2017 年,谷歌的作者们发布了一篇名为《Attention is All You Need》的论文,在其中引入了 Transformer 架构。这一新架构在语言翻译任务中取得了前所未有的成功,该论文很快成为任何从事这一领域的必读文献。像许多人一样,当我第一次阅读这篇论文时,我能看出其创新思想的价值,但没有意识到这篇论文对更广泛的 AI 领域会产生如此巨大的影响。在短短几年内,研究人员将 Transformer 架构应用于语言翻译之外的许多任务,包括图像分类、图像生成和蛋白质折叠问题。特别是,Transformer 架构革新了文本生成,为 GPT 模型和我们当前在 AI 领域经历的指数级增长铺平了道路。

鉴于 Transformer 模型在当前行业和学术界的广泛应用,理解它们的工作细节是每位 AI 从业者的重要技能。本文将主要关注 GPT 模型的架构,这些模型是使用原始 Transformer 架构的一个子集构建的,但最后也会涉及原始 Transformer。关于模型代码,我将从我找到的最清晰的原始 Transformer 实现开始:哈佛大学的注释 Transformer。我将保留与 GPT transformer 相关的部分,移除不相关的部分。在此过程中,我会避免对代码做任何不必要的修改,以便你可以轻松地将 GPT 类似版本的代码与原始代码进行比较,理解它们的差异。

本文面向经验丰富的数据科学家和机器学习工程师。特别是,我假设你对张量代数非常熟悉,已经从头实现了神经网络,并且对 Python 使用自如。此外,尽管我尽力使这篇文章独立完整,如果你读过我之前关于 GPT 模型如何工作的文章,理解起来会更容易。

本文中的代码可以在相关的 GitHub 项目 中找到。

如何调用我们的 GPT Transformer

在我们深入了解如何构建 GPT 模型之前,先来理解如何调用它。我们暂时假设已经有一个可工作的 GPT 模型,重点讨论如何准备输入、调用模型以及解读输出。总体思路是提供几个单词作为输入来启动生成,并返回可能跟随该输入的文本。例如,如果我们给 GPT 模型输入“很久以前”,模型可能会返回“在一个遥远的星系”。

让我们看看用于调用模型的代码,传入输入“很久以前”并生成 10 个新词。我使用了注释来展示每个张量的形状。代码之后我会解释更多细节。

import tiktoken

def tokenize(text, batch_size):
    """Convert text to numerical tokens and repeat batch_size times."""
    encoding = tiktoken.encoding_for_model("davinci")
    token_list = encoding.encode(text)
    token_tensor = torch.tensor(token_list, dtype=torch.long) # (input_seq_len)
    token_tensor = token_tensor.unsqueeze(0) # (1, input_seq_len)
    token_tensor = token_tensor.repeat(batch_size, 1) # (batch_size, input_seq_len)
    return encoding, token_tensor

def limit_sequence_length(input_tokens, block_size):
    """Limit the input to at most block_size tokens."""
    input_seq_len = input_tokens.size(1)
    seq_len = min(input_seq_len, block_size)
    block_tokens = input_tokens[:, -seq_len:] # (batch_size, seq_len)
    return block_tokens

def generate_next_token(model, tokens):
    """Use the highest probability from the Transformer model to choose the next token."""
    mask = subsequent_mask(tokens.size(1)) # (1, seq_len, seq_len)
    decoder_output = model.decode(tokens, mask) # (batch_size, seq_len, vocab_size)
    distribution = model.generator(decoder_output[:, -1, :]) # (batch_size, vocab_size)
    next_token = torch.argmax(distribution, dim=1, keepdim=True) # (batch_size, 1)
    return next_token

# Define constants.
input_text = "A long time ago"
new_token_count = 10
batch_size = 1
block_size = 1024

# Tokenize the input text.
encoding, tokens = tokenize(input_text, batch_size)

# Create the model.
model = make_model(encoding.n_vocab)

# Iterate until we've generated enough new tokens.
for _ in range(new_token_count):
    block_tokens = limit_sequence_length(tokens, block_size) # (batch_size, seq_len)
    next_token = generate_next_token(model, block_tokens) # (batch_size, 1)
    tokens = torch.cat([tokens, next_token], dim=1) # (batch_size, input_seq_len + 1)

# Print each of the generated token sequences.
print(tokens)
for row in tokens:
    print(encoding.decode(row.tolist()))

由于我们从字符串“A long time ago”开始,你可能会倾向于认为 Transformer 接收字符串作为输入。然而,和其他神经网络一样,Transformer 需要数值输入,因此输入字符串必须首先转换为一系列数字。我们在tokenize函数中使用一个分词器(在我们的示例中是来自 OpenAI 的tiktoken)进行转换,它将文本拆分为几字母的块,并为每个唯一的块分配一个称为令牌的数字。为了获得 Transformer 的正确输入,我们将令牌序列放入一个张量中,并扩展它以包含一个批量维度。这是因为,和其他类型的神经网络一样,Transformer 最有效的训练方式是使用批量处理,以利用 GPU 上的并行计算。我们的示例代码在一个序列上运行推断,因此我们的batch_size为一,但如果你想一次生成多个序列,可以尝试更大的数字。

在对输入进行分词后,我们使用make_model函数创建 Transformer 模型,稍后我们将详细讨论这个函数。你可能会认为调用模型会返回几个令牌作为输出,因为这通常是文本生成的场景。然而,Transformer 每次只能生成一个令牌。由于我们希望生成多个令牌,我们使用for循环多次调用模型,在每次迭代中,我们使用torch.cat将新生成的令牌附加到原始令牌序列中。

GPT 风格的 Transformer 模型通常有一个明确定义的令牌限制:例如,gpt-35-turbo(Chat GPT)的限制为 4096 个令牌,而gpt-4-32k的限制为 32768 个令牌。由于我们将输入令牌和到目前为止生成的所有输出令牌连接传递给 Transformer 模型,模型的令牌限制指的是输入加输出令牌的总数。在我们的代码中,我们使用block_size常量定义这个令牌限制,并通过在limit_sequence_length函数中简单地将更长的令牌序列截断到最大支持长度来处理更长的序列。

我们在generate_next_token函数中通过调用model.decodemodel.generator来调用 Transformer 模型,这对应于 Transformer 架构的两个主要部分。解码部分需要一个掩码,我们使用subsequent_mask函数创建这个掩码。我们将在本文后面详细分析这些函数。生成阶段返回一个概率分布序列,我们选择最后一个(稍后我们将看到原因),用它来预测下一个令牌。这个分布包含每个可能的令牌的概率值,表示该令牌在句子中接下来出现的可能性。

为了简化和提高示例代码的可读性,我们选择输出分布中概率最高的标记(使用 torch.argmax)。在实际的 GPT 模型中,下一个标记是通过从概率分布中进行采样来选择的,这会在输出中引入一些变异,使得文本感觉更加自然。如果你可以访问 Azure AI Studio 的“Completions playground”,你可能会注意到“Temperature”和“Top probabilities”滑块,这些滑块可以让你控制采样的方式。

tensor([[   32,   890,   640,  2084,  3556, 48241, 26430, 34350, 28146, 43264,
          3556,  6787, 45859, 13884]])
A long time ago</ spaghetti Rapiddx Rav unresolved</ rail MUCHkeeper

在这篇文章中,我们将重点关注 Transformer 模型架构的代码,而不是模型训练的代码,因为这正是 Transformer 创新大部分所在的地方。最后我会给出一些训练模型的指引,以防你有兴趣扩展这段代码以生成更好的结果。

我们现在对 Transformer 模型的输入和输出有了很好的理解,以及如何实例化和调用它。接下来,我们将深入探讨模型本身的实现细节。

Transformer 架构概述

让我们熟悉一下 GPT transformer 的高层架构:

在这个图示中,数据从下到上流动,这在 Transformer 插图中是传统的。最初,我们的输入标记经过几个编码步骤:首先使用嵌入层进行编码,然后是位置编码层,最后将这两种编码加在一起。接下来,我们的编码输入经过一系列 N 解码步骤,然后是一个归一化层。最后,我们将解码后的数据通过一个线性层和一个 softmax,得到一个概率分布,用于选择下一个标记。

在接下来的部分中,我们将仔细研究这个架构中的每一个组件。

嵌入层

嵌入层将输入序列中的每个标记转换为长度为 d_model 的向量。Transformer 的输入由标记序列的批次组成,形状为 (batch_size, seq_len)。嵌入层接受每个标记(一个单一的数字),计算其嵌入(一个长度为 d_model 的数字序列),并返回一个张量,用每个嵌入替代对应的原始标记。因此,这一层的输出形状为 (batch_size, seq_len, d_model)

import torch.nn as nn

class Embeddings(nn.Module):

    def __init__(self, d_model, vocab_size):
        super(Embeddings, self).__init__()
        self.lut = nn.Embedding(vocab_size, d_model)
        self.d_model = d_model

    # input x: (batch_size, seq_len)
    # output: (batch_size, seq_len, d_model)
    def forward(self, x):
        out =  self.lut(x) * math.sqrt(self.d_model)
        return out

使用嵌入而不是原始标记的目的是为了确保对于语义相似的标记,我们有类似的数学向量表示。例如,考虑“she”和“her”这两个词。这两个词在语义上是相似的,因为它们都指代女性或女孩,但相应的标记可能完全不同(例如,当使用 OpenAI 的tiktoken分词器时,“she”对应的标记是 7091,而“her”对应的标记是 372)。这两个标记的嵌入在开始时也会非常不同,因为嵌入层的权重是随机初始化的,并在训练过程中学习。但如果这两个词在训练数据中经常出现在一起,最终嵌入表示将趋于相似。

位置编码

位置编码层添加了每个标记在序列中的绝对位置和相对距离的信息。与递归神经网络(RNN)或卷积神经网络(CNN)不同,变换器本身并没有固有地认识到每个标记在序列中的位置。因此,为了捕捉序列中标记的顺序,变换器依赖于位置编码。

编码标记位置的方法有很多。例如,我们可以通过使用另一个嵌入模块(类似于前面的层)来实现位置编码层,如果我们将每个标记的位置而不是每个标记的值作为输入。同样,我们会从随机选择的权重开始。然后在训练阶段,权重会学习捕捉每个标记的位置。

The Annotated Transformer的作者决定实现一个更复杂的算法,该算法预计算了序列中标记位置的表示。由于我们希望尽可能紧密地跟随他们的代码,我们将使用相同的方法:

class PositionalEncoding(nn.Module):

    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1)  # (max_len, 1)
        div_term = torch.exp(
            torch.arange(0, d_model, 2) *
            -(math.log(10000.0) / d_model))  # (d_model/2)

        pe[:, 0::2] = torch.sin(position * div_term)  # (max_len, d_model)
        pe[:, 1::2] = torch.cos(position * div_term)  # (max_len, d_model)
        pe = pe.unsqueeze(0)  # (1, max_len, d_model)
        self.register_buffer('pe', pe)

    # input x: (batch_size, seq_len, d_model)
    # output: (batch_size, seq_len, d_model)
    def forward(self, x):
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
        return self.dropout(x)

这种位置编码使用不同频率的正弦和余弦函数来填充pe张量。例如,在下面的插图中,蓝色和红色的值是使用两种不同频率的正弦波计算得出的,而橙色和绿色的值是使用相同频率的余弦波计算得出的。

正弦和余弦图的值最终填充了pe张量的列,如下所示:

然后在“前向”阶段,我们将前一个嵌入层的结果x作为输入,并返回xpe的和。

预计算位置编码的值(而不是使用可训练的嵌入)的主要优点是我们的模型训练参数减少。这种参数减少提高了训练性能,这在处理大型语言模型时至关重要。

解码器

正如我们在 Transformer 架构的图示概述中看到的那样,嵌入和位置编码层之后的下一个阶段是解码器模块。解码器由N个解码器层副本组成,后面跟着一个层归一化。以下是Decoder类,它将单个DecoderLayer实例作为类初始化器的输入:

class Decoder(nn.Module):

    def __init__(self, layer, N):
        super(Decoder, self).__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, mask):
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

“克隆”函数简单地创建一个包含N个模块副本的 PyTorch 列表:

def clones(module, N):
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

层归一化接收形状为(batch_size, seq_len, d_model)的输入,并在其最后一个维度上进行归一化。经过这一步,每个嵌入分布将开始时为单位正态分布(以零为中心,标准差为一)。然后,在训练过程中,分布会随着参数a_2b_2在我们场景中的优化而改变形状。你可以在层归一化论文中了解更多关于层归一化的内容,这篇论文来自 2016 年。

class LayerNorm(nn.Module):

    def __init__(self, features, eps=1e-6):
        super(LayerNorm, self).__init__()
        self.a_2 = nn.Parameter(torch.ones(features))
        self.b_2 = nn.Parameter(torch.zeros(features))
        self.eps = eps

    def forward(self, x):
        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True)
        return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

我们克隆的DecoderLayer类具有以下架构:

以下是对应的代码:

class DecoderLayer(nn.Module):

    def __init__(self, size, self_attn, feed_forward, dropout):
        super(DecoderLayer, self).__init__()
        self.size = size
        self.self_attn = self_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 2)

    def forward(self, x, mask):
        x = self.sublayer0)
        return self.sublayer1

从高层次看,DecoderLayer 包含两个主要步骤:注意力步骤,负责令牌之间的通信,以及前馈步骤,负责预测令牌的计算。在这些步骤周围,我们有残差(或跳跃)连接,用图中的加号表示。残差连接为数据在神经网络中的流动提供了替代路径,从而允许跳过某些层。数据可以通过残差连接中的层流动,也可以直接通过残差连接跳过其中的层。实际上,残差连接通常用于深度神经网络,因为它们有助于训练更好地收敛。你可以在论文深度残差学习用于图像识别中了解更多关于残差连接的内容,这篇论文来自 2015 年。我们使用SublayerConnection模块来实现这些残差连接:

class SublayerConnection(nn.Module):

    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        return x + self.dropout(sublayer(self.norm(x)))

前馈步骤通过两个线性层实现,中间夹一个 Rectified Linear Unit (ReLU) 激活函数:

class PositionwiseFeedForward(nn.Module):

    def __init__(self, d_model, d_ff, dropout=0.1):
        super(PositionwiseFeedForward, self).__init__()
        self.w_1 = nn.Linear(d_model, d_ff)
        self.w_2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        return self.w_2(self.dropout(F.relu(self.w_1(x))))

注意力步骤是 Transformer 中最重要的部分,因此我们将在下一节中专门讨论它。

掩蔽多头自注意力

上图中的多头注意力部分可以扩展成以下架构:

顾名思义,多头注意力模块并行处理多个注意力计算实例,并对数据进行一些额外的前处理和后处理。

class MultiHeadedAttention(nn.Module):

    def __init__(self, h, d_model, dropout=0.1):
        super(MultiHeadedAttention, self).__init__()
        assert d_model % h == 0
        self.d_k = d_model // h
        self.h = h
        self.linears = clones(nn.Linear(d_model, d_model), 4)
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, query, key, value, mask=None):
        if mask is not None:
            mask = mask.unsqueeze(1)  # (1, 1, seq_len, seq_len)

        nbatches = query.size(0)  # batch_size

        # (batch_size, seq_len, d_model) => (batch_size, h, seq_len, d_k)
        query, key, value = \
            [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
             for l, x in zip(self.linears, (query, key, value))]

        # (batch_size, h, seq_len, d_k)
        x, self.attn = attention(query,
                                 key,
                                 value,
                                 mask=mask,
                                 dropout=self.dropout)

        # (batch_size, h, seq_len, d_k) => (batch_size, seq_len, d_model)
        x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
        return self.linears-1

多头注意力层的输入包括三个张量,分别称为query (Q)、key (K) 和 value (V)。在我们的特定模型中,我们将前一层的输出 x 传递给这三个参数,它的形状为 (batch_size, seq_len, d_model)(这就是为什么我们称之为 自注意力)。我们通过先将每个张量传递通过一个线性层,然后将它们分割成 h 个注意力头,每个注意力头的大小为 d_k,其中 h*d_k = d_model,得到形状为 (batch_size, seq_len, h, d_k) 的张量。然后我们交换维度 1 和 2,得到形状为 (batch_size, h, seq_len, d_k) 的张量。接下来,我们为每个头计算注意力,得到相同形状的张量。最后,我们的后处理将所有头连接回形状为 (batch_size, seq_len, d_model) 的张量,并通过另一个线性层。通过使用张量操作在每个头中并行进行所有注意力计算,我们可以充分利用 GPU。

注意力的计算使用以下公式:

这里是实现公式的代码:

# Dimensions of query, key, and value: (batch_size, h, seq_len, d_k)
# Dimensions of mask: (1, 1, seq_len, seq_len)
def attention(query, key, value, mask=None, dropout=None):
    d_k = query.size(-1)
    # (batch_size, h, seq_len, d_k) x (batch_size, h, d_k, seq_len) -> (batch_size, h, seq_len, seq_len)
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
    p_attn = F.softmax(scores, dim=-1) # (batch_size, h, seq_len, seq_len)
    if dropout is not None:
        p_attn = dropout(p_attn)
    # (batch_size, h, seq_len, seq_len) x (batch_size, h, seq_len, d_k) -> (batch_size, h, seq_len, d_k)
    return torch.matmul(p_attn, value), p_attn

从高层次来看,注意力算法决定输入序列中哪些标记应该受到更多关注,然后使用这些信息来预测下一个标记。在下图中,深色的橙色表示在预测中更相关的标记。

更具体地说,注意力实际上预测了我们输入序列的多个部分的下一个标记。它查看第一个标记,并预测第二个标记是什么,然后它查看第一个和第二个标记,并预测第三个标记是什么,以此类推。

在推理过程中,这似乎有些浪费,因为我们只对最后的预测感兴趣。然而,这在训练期间极为有用。如果你给 Transformer n 个标记作为输入,它将被训练接收从 1n-1 长度的输入,从而使模型在未来更好地处理不同长度的输入。

上图中的思想由代码中的 p_attn 张量表示。这个张量的形状为 (batch_size, h, seq_len, seq_len),但我们暂时忽略批量大小和头的数量(每个批量和每个头的工作方式相同),只考虑一个形状为 (seq_len, seq_len) 的张量切片。p_attn 张量中的每一行包含一个概率分布,表示其他所有键标记对该行对应的查询标记的相关性。结果张量封装了前一图像中显示的所有值:

你可以在代码中看到这个张量是如何计算的。我们首先对查询和转置的键进行矩阵乘法。如果忽略批量大小和头数,查询和键由形状为(seq_len, d_k)的嵌入序列组成,这些嵌入是将输入x通过不同线性层得到的。当我们将形状为(seq_len, d_k)的查询张量与形状为(d_k, seq_len)的转置键张量相乘时,我们实际上是在对查询中的每个嵌入和键中的所有其他嵌入进行点积,最终得到一个形状为(seq_len, seq_len)scores张量。点积的较大值表示查询中的某个嵌入对键中的某个嵌入“感兴趣”,换句话说,模型发现了输入序列中两个位置之间的关联。大致而言,我们现在有了一个表示每个标记在序列中发现其他所有标记的“有趣”或“重要”的张量。

下一步是对scores张量应用掩码,使其上三角的值被忽略(这就是我们称之为masked注意力的原因)。我们这样做是因为在 GPT 风格的文本生成场景中,模型在预测下一个标记时仅查看过去的标记。我们使用以下代码定义一个掩码,它在对角线和下三角中包含True值,在上三角中包含False值:

def subsequent_mask(size):
    """
    Mask out subsequent positions.
    """
    attn_shape = (1, size, size)
    subsequent_mask = torch.triu(torch.ones(attn_shape),
                                 diagonal=1).type(torch.uint8)
    return subsequent_mask == 0

我们使用masked_fill函数将这个掩码应用到scores张量中,用一个非常大的负数替换掉上三角的所有值。

最后,我们应用一个 softmax,将张量中的每一行转换为概率分布。记得 softmax 的公式吗?

由于e的负幂非常大的情况下接近零,p_attn张量的上三角中的所有值本质上变为零。剩余的值(在下三角和对角线中)变成每行总和为一的概率。

你可能注意到,在代码中,当我们将查询和键张量相乘时,我们将结果矩阵中的所有值除以d_k的平方根。这样做是为了保持方差接近于一,这样可以确保 softmax 给出的概率值在整个范围内(从零到一)分布良好。如果我们不这样做,softmax 计算的分布可能接近于 one-hot 向量,其中一个值为一,其他值都为零——这会使模型的输出显得可预测且机械。

此时,我们有一个包含概率分布的p_attn张量,表示各标记之间的相互兴趣程度。下一步是利用这种兴趣程度来确定在生成输出标记时我们应该对每个输入标记给予多少关注。自然地,我们会对最感兴趣的标记给予更多关注。我们通过将概率张量与value张量相乘来生成下一个标记,value张量包含经过线性层处理后的输入标记嵌入x。结果张量将包含每个标记子序列的预测:

这个图表的直观解释是:对于输入子序列“A”,我们对唯一的输入标记给予完全的关注,并可能产生下一个标记预测,比如“person”。对于输入子序列“A long”,我们的模型已经训练得更加关注标记“long”而非标记“A”,并可能产生下一个标记预测“dress”,依此类推。在推理时,我们希望考虑到完整的输入序列“A long time ago”,所以我们只关注这个图表中的最后一行。我们最关注“ago”,次之是“long”,对其他两个标记的关注最少,我们产生下一个标记预测“in”。

在我们计算了所有头的注意力,并将结果重新拼接在一起后,我们得到一个维度为(batch_size, seq_len, d_model)的输出张量。这个张量包含每个子序列的标记预测,几乎准备好返回给用户。但是在此之前,我们需要最后一步来最终确定其形状和内容。

生成器

我们的 Transformer 中的最后一步是生成器,它包括一个线性层和一个顺序执行的 softmax:

class Generator(nn.Module):

    def __init__(self, d_model, vocab):
        super(Generator, self).__init__()
        self.proj = nn.Linear(d_model, vocab)

    def forward(self, x):
        return F.log_softmax(self.proj(x), dim=-1)

线性层的目的是将张量的第三维度从内部的d_model嵌入维度转换为vocab_size维度,这是调用我们 Transformer 的代码所理解的。结果是一个维度为(batch_size, seq_len, vocab_size)的张量。softmax 的目的是将第三维度中的值转换为概率分布。这一概率分布张量就是我们返回给用户的内容。

你可能还记得,在本文开头,我们解释了 Transformer 的输入由形状为(batch_size, seq_len)的标记序列批次组成。而现在我们知道 Transformer 的输出由形状为(batch_size, seq_len, vocab_size)的概率分布序列批次组成。每个批次包含一个预测第一个输入标记之后的标记的分布,另一个预测第一个和第二个输入标记之后的标记的分布,依此类推。每个批次的最后一个概率分布使我们能够预测整个输入序列之后的标记,这也是我们在做推理时所关心的。

生成器是我们 Transformer 架构的最后一个部分,因此我们准备好将所有组件结合起来了。

将所有组件结合起来

我们使用DecoderModel模块来封装 Transformer 架构的三个主要部分:嵌入层、解码器和生成器。

class DecoderModel(nn.Module):

    def __init__(self, decoder, embed, generator):
        super(DecoderModel, self).__init__()
        self.embed = embed
        self.decoder = decoder
        self.generator = generator

    def forward(self, x, mask):
        return self.decode(x, mask)

    def decode(self, x, mask):
        return self.decoder(self.embed(x), mask)

调用decode仅执行嵌入层和解码器,所以如果我们想执行 Transformer 的所有步骤,我们需要调用decode,然后调用generator。这正是我们在本文开头展示的推理代码中的generate_next_token函数所做的。

推理代码还调用了一个make_model函数,该函数返回DecoderModel的一个实例。这个函数初始化了我们迄今为止讨论的所有组件,并按照本文开头的架构图将它们结合在一起:

def make_model(vocab_size, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1):
    c = copy.deepcopy
    attn = MultiHeadedAttention(h, d_model)
    ff = PositionwiseFeedForward(d_model, d_ff, dropout)
    position = PositionalEncoding(d_model, dropout)
    model = DecoderModel(
        Decoder(DecoderLayer(d_model, c(attn), c(ff), dropout), N),
        nn.Sequential(Embeddings(d_model, vocab_size), c(position)),
        Generator(d_model, vocab_size))

    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform_(p)
    return model

我们现在已经拥有了实现 GPT 风格 Transformer 架构所需的所有组件!

我们将以一些关于训练的思考结束,并且对完整的 Transformer 与 GPT 风格子集进行简要比较。

训练

训练 GPT 风格的 Transformer 的代码与训练任何其他神经网络的代码相同——只是,在我们的场景中,对于每个输入的标记序列,我们期望输出是从右边一个位置开始的序列。例如,如果我们给它输入“A long time ago”,我们期望模型返回的概率采样会产生“long time ago in”。

如果你有训练神经网络的经验,并且希望自己训练 Transformer,你可以重用你过去编写的任何代码,并将其调整到我们的场景中。如果你需要指导,我建议你参考Annotated Transformer 的第二部分中的代码和解释。无论如何,你都需要访问快速 GPU,无论是本地还是云端。自然,我更倾向于在 Azure 云端进行训练!

与完整 Transformer 的比较

一旦你理解了 GPT 风格 Transformer 的架构,你就离理解Attention is all you need论文中呈现的完整 Transformer 只差一步。下面你可以看到论文中呈现的 Transformer 架构图,我们在本文中覆盖的部分被橙色框圈出。

插图改编自 Attention is all you need 论文

完整的 Transformer 有一个编码器部分(在左侧)和一个解码器部分(在右侧)。论文的原始意图是提出一种用于机器翻译的架构。在这种背景下,编码器用于处理输入语言,而解码器用于生成输出语言。

你可以看到,除了 GPT 风格 Transformer 中使用的掩码多头自注意力外,完整的 Transformer 还有另外两个多头注意力块。编码器中的那个未被掩码,这意味着我们在注意力部分看到的p_attn张量在其上三角区域的值没有被置零。这是因为在机器翻译中,生成一个输出语言的单一令牌可能需要模型关注输入语言中所有序列位置的令牌,包括早期和晚期位置。解码器部分的附加多头注意力块是一个“交叉注意力”(与“自注意力”相对)层,这意味着它的键和值来自不同于查询的源,如图所示。这是因为模型需要理解在预测输出语言中的令牌时,应该关注输入语言中的每个令牌的程度。图中其余部分类似于 GPT 风格 Transformer 的部分,并已在本文中解释过。

结论

在这篇文章中,我们详细讨论了 GPT 风格的 Transformer 模型的架构,并在较高层次上覆盖了原始 Transformer 的架构。鉴于 GPT 模型在行业中的日益流行,以及最近论文中经常出现的原始 Transformer 模型的变体,我希望你能在工作或学习中找到这些知识的用处。如果你想深入了解,我鼓励你克隆相关的 GitHub 项目 并探索代码。设置断点于你不清楚的代码部分,运行它,并检查其变量。如果你有 GPU 访问权限,可以编写代码训练模型,并查看其预测性能的提升。

希望你运用新获得的知识,帮助人工智能社区揭开一些与这些模型相关的误解。许多人误以为这些模型对世界有更高层次的理解,而你现在知道事实并非如此。它们只是由数学和统计运算组成,旨在基于之前的训练数据预测下一个令牌。也许未来的生成模型会对世界有更好的理解,并生成更准确的预测?如果是这样,我会在这里告诉你所有的细节。

注意

所有图片均由作者提供,除非另有说明。你可以在这篇博客文章中出于任何目的使用原始图片,并需注明出处(即链接到本文)。

两个信封问题

原文:towardsdatascience.com/the-two-envelopes-problem-896f16e938b7?source=collection_archive---------3-----------------------#2023-09-08

悖论系列 #1

时间和因果关系如何从随机性中出现

Gabriel de LongeauxTowards Data Science Gabriel de Longeaux

·

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

--

两个信封问题,通过直观但错误的贝叶斯概率估计来做出悖论和不一致的决策,当试图确定最佳行动方案时出现。修正数学错误是简单的,但不仅仅如此:首先,通过略微修改问题,我们可以使其无法决定——这是语言模糊性而非数学形式主义的一个例子;其次,在比较几种可能的解决方案时,我们可以观察到时间在数学世界中的出现,这在理论上允许我们测试因果假设。

两个信封问题(TEP)

想象一下,我在桌子上给你展示两个看似相同的信封,告诉你(没有撒谎)这两个信封中都有钱,其中一个是另一个的两倍,并提议你选择一个信封,保留其中的钱。

由 Midjourney 生成的图像

一旦你选择了一个信封,在你打开之前,我会问你是否想修改你的选择,换成另一个信封。

你会怎么做?

你可能会告诉我,换信封是没有意义的,因为无论选择哪个信封,情况都是一样的。然而,你应该注意到你选择了一个未知金额 x,另一个信封中的金额 y 可以是 2x 或 x/2,概率相等,这意味着期望金额 y 是 2x (1/2) + x/2 (1/2) = 5x/4,这大于 x。所以,也许你还是应该换信封?

显然,你也可以根据 y 来计算期望的数量 x,因为 x 有一半的概率是 2y 或 y/2,你会发现期望的数量 x 是 5y/4,这大于 y。

那么这个计算有什么问题?哪个信封更有可能包含比另一个信封更多的金额呢?

推理中的数学缺陷

我们可以任意地将一个信封标记为“X”,另一个标记为“Y”。现在让我们正确计算在我们知道金额 y 在 Y 信封中的情况下,X 信封中的金额的条件期望。

在已观察到的 y 值下 X 的金额的期望,记作 E[X|Y = y],显然取决于观察到的特定金额 y:即使对于所有可能的 y 值,X 中的金额 x 可以是 y/2 或 2y,每次的概率是 1/2,但这并不意味着对于特定的 y 值也是这样。例如,如果 y 是“非常小”(在稍后会澄清的意义上),x 更有可能大于 y,而如果 y 是“非常大”,x 更有可能小于 y:对于所有可能的 y 值,概率可以被平衡,使得 X 有一半的时间是 Y 的一半,另一半时间是 Y 的两倍,但这并不意味着 P(X = y/2|Y = y) = 1/2 且 P(X = 2y|Y = y) = 1/2,只是 P(X = Y/2) = P(X = 2Y) = 1/2。

因此,我们将尝试正确计算 E[X|Y = y],但首先我们需要澄清将这两个信封放在桌子上,并标记为“X”和“Y”的过程。我们假设我们用一个随机金额 U 填充了第一个信封,用金额 2U 填充了第二个信封。然后我们将它们混合,并随机地将其中一个信封命名为 X,而将另一个命名为 Y。我们可以将这个命名过程表示如下:我们抽取一个二进制数 Z(有一半的概率是 0 或 1)。如果 Z = 0,X 就是包含 U 的信封;否则(如果 Z = 1),X 就是包含 2U 的信封。

现在我们可以看到,对于那个被要求选择但不知道 U 和 Z 随机数的外部观察者,信封中的金额如下:

我们可以验证 P(X = 2Y) = P(U + ZU = 4U - 2ZU) = P(3U - 3ZU = 0) = P(U=ZU) = P(Z = 1) = 1/2(对于 P(X = Y/2) 也是如此)。

现在我们可以正确计算 E[X|Y = y] = E[3U-Y|Y = y] = E[3U|Y = y] - E[Y|Y = y] = 3E[U|Y = y] — y。

我们仍然需要计算 E[U|Y = y],为此我们需要知道 P(U=u|Y=y),它(根据贝叶斯定理)与 P(Y=y|U=u)P(U=u) 成比例。

为了计算 P(Y = y|U),我们回顾一下 Y 要么是 U,要么是 2U,这意味着 U 取值为 u 时,u 的值要么是 y,要么是 y/2:

  • 当 y 不是 u 或 u/2 时,Y = y 的概率为 0:P(Y = y|U = u) = 0

  • 当 y 是 u 时,Y = y 的概率是 1/2(Z = 1):P(Y = y|U = u) = 1/2

  • 当 y 是 u/2 时,Y = y 的概率是 1/2(Z = 0):P(Y = y|U = u) = 1/2

使用数学形式化:

其中:

所有这些可以总结为:

然后我们需要知道 P(U = u)。我们只能做一个假设,例如 U 在正实数上服从指数分布(参数 λ>0):

最终,P(U = u|Y = y) 与以下内容成比例:

换句话说:

现在我们拥有所有计算 E[X|Y = y] = 3E[U|Y = y] — y 所需的内容,其值为:

总结一下,我们现在知道:

这与最初的 5y/4 有很大不同!

x 的期望值(严格)大于 y 若且唯若:

或者换句话说,若且唯若:

(这是指数分布中位数的两倍,从中抽取的金额)。

因此,我们可以更好地理解我们之前推理中的错误。尽管设计上 X 在所有可能值 y 的平均值中有一半的时间是 y 的两倍,一半的时间是 y 的一半,但对于特定的 y 值,概率并不是一半一半:如果 y 相对于 U 值的选择方式“很大”,则信封 X 包含较小金额的概率更高;如果 y 相对“很小”,则信封 X 包含较大金额的机会则更多。在这里,“大”和“小”之间的界限只是指数分布中位数的两倍。

X 或 Y 的选择是对称的,因为 E[Y|X = x] = E[3U — X|X = x] = 3E[U|X=x] - x,从这里所有之前的计算仍然适用,变动之下

似乎悖论已解决,但我认为实际上两个信封问题可能是不可决定的,这意味着我们不能真正知道问题是否对称,或者是否应该偏好一个信封而不是另一个。

一个不可决定的问题

现在假设桌上有两个看似相同的信封,除了它们已经被标记为“X”和“Y”之外。现在告诉我们,信封 X 包含 Y 中金额的一半或两倍,每种可能性的概率各为一半。对称性同样适用于信封 Y。现在要求你选择一个信封:你应该选择哪个?

基于之前的例子,似乎显而易见,我们可以随意选择其中一个。然而,这个想法是错误的!这完全取决于我们的假设,换句话说,这完全取决于问题的(统计)表示。

在这里,关键是当我们被要求选择一个信封时,这些信封已经被标记。选择信封中的金额和标记它们的过程是什么?如果它们像之前研究的例子那样被随机标记,我会同意选择其中一个或另一个在统计上是等效的。

但让我们假设 X 的金额是从正实数上的指数分布中选择的(参数 λ>0),类似于在之前的例子中对 U 所做的处理。那么信封 Y 的金额仅仅是随机选择为 Y 中金额的一半或两倍(均匀概率):Y = HX,其中 H 取值 1/2 或 2,每次各有一半的概率(H 与 X 独立)。

现在我们来计算 Y 的值的累积分布:P(Y < y) = P(HX < y) = P(HX < y |H = 1/2) P(H = 1/2) + P(HX < y |H = 2) P(H = 2)

= P(X/2 < y) (1/2) + P(2X < y) (1/2) = (1/2) P(X < 2y) + (1/2) P(X < y/2)

= (1/2) F(2y) + (1/2) F(y/2),其中 F 是 X 的累积分布函数(指数分布)

对于非负值 y。

通过对 Y = y 求导,我们得到:

这是两个指数分布的概率密度函数的平均值,一个参数为 λ/2,另一个参数为 2λ,这意味着信封 Y 中的平均值是 2/λ 和 1/(2λ) 的平均值:

这超过了 X 的平均值,指数随机变量 λ 的均值为 1/λ(对于那些只关心期望值计算的人,E[Y] = E[HX] = E[H] E[X],因为 H 和 X 是独立的,因此 E[Y] = [(1/2)(1/2) + 2(1/2)] E[X] = (5/4)E[X])。

结论是,在这种情况下,如果我们只关心均值来做决定,那么我们应该系统地选择信封 Y。

然而,我们也可以假设,代替 Y = HX,我们有 X=HY,其中 Y 的量来自参数为 λ 的指数分布,在这种情况下,我们应选择信封 X。

我们对生成桌上两个信封的过程了解不够,无法在没有额外假设的情况下决定我们应该选择哪个信封。

这就是所有的说法吗?不,最有趣的部分还在后面。从我们到目前为止所做的工作可以看出,生成信封情况的物理过程必须用随机变量来建模。

但在物理过程中,时间是存在的:例如,我们为 X 选择一个量,然后从中推导出放入 Y 中的量,或者反之;统计模型能够重现这一点,如果选择 X 的量在 Y 之前或之后,会得出不同的结论。换句话说,我们的统计模型能够在数学上重现时间的物理现实。

时间和因果关系从随机性中产生

常有人说,数学只能证明相关性,而不能证明因果关系。在这方面,计量经济学中的因果性分析不过是相关性分析,只要涉及数学。人类的思维决定了一个事件是另一个事件的结果,这基于两个事件之间的相关性和时间:后发生的事件只能是结果,而不是原因。

因为时间不是数学概念而是物理概念,数学似乎无力独立于任何人类对现象先后发生的输入来建立因果关系(因此被称为原因)和第二个现象(因此被称为结果)。但真的是这样吗?时间的概念源于不可逆性的概念:当一个物体从左到右移动时,这不是由于时间的变化,因为物体可以回到原来的位置;而当一个物体在衰老时,这是一种由于时间流逝而发生的变化,因为这一过程是不可逆的。时间是世界状态的不可逆变化。

在物理学中,不可逆性被视为无序度增加的结果,正式称为熵:正因为构成物体的分子变得更加无序,物体才永远无法恢复到其初始状态,因此这些变化不仅被看作是在时间中发生的,还因为时间的存在。虽然状态的变化足以说明时间的流逝,但物理上的不可逆性使得时间只能朝一个方向流动,从而使我们能够区分原因和结果。

不过多涉及细节,只有一个老化物体的宏观状态是不可逆的:在微观层面,从理论物理学的观点来看,分子和粒子可以以类似于过去状态的方式重新排列。因此,物理不可逆性不可以仅仅通过不可逆的数学函数来建模,因为这种特性将不存在。相反,随机变量在宏观上是不可逆的,但在微观上是可逆的:例如,如果 Y = HX,并不意味着 X = Y/H(从宏观角度看是不可逆的),然而对于 Y、H 和 X 的任何值,y = hx 和 x = y/h(从微观角度看是可逆的)。两个信封悖论特别令人困惑,因为在其表述中一切似乎都是对称的(如果 x 是 y 的一半或两倍,则意味着 y 是 x 的两倍或一半),而这仅在“微观”层面上是正确的。

那么,物理熵与随机变量之间的联系如何帮助因果关系的研究呢?

让我们再次考虑最后一个例子,其中有两个预先标记的信封 X 和 Y,并且假设我们知道 Y = HX 或 X = HY,这意味着 Y 要么是 X 的结果,要么反之。我们可以通过对 X 和 Y 进行大量观察来测试每种假设,以识别这两个随机变量的概率密度,一个将具有“更具熵性”的密度(“更具熵性”是指在一些特定的数学关系下需要测试的)因为它将基于另一个随机变量的密度,但被随机变量 H(其密度假设为已知)“扰乱”。

现在让我们考虑更常见的问题。通常,线性回归被用于量化几个变量之间的因果关系。例如,Y = αX,其中我们假设 Y 是 X 的结果,我们希望量化因果系数 α。然而,这并不能证明 X 到 Y 的因果关系,它只是允许我们在假设为真的情况下量化 X 和 Y 之间的假定关系。

在这样一个简单的例子中,假设 Y 等于 αX,数学上不可能确定因果关系,因为这等于说 X = Y/α。然而,如果系数 α 被认为是更一般过程 A 的一个历史值,则可以比较 Y、A 和 X 的分布,并查看哪一个更符合 Y = AX 或 X = Y/A。另一个例子是研究关系 Z = X + Y(Z 由 X 和 Y 造成),与其他可能性如 Y = Z - X(Y 由 X 和 Z 造成)进行比较:比较 X、Y 和 Z 的分布将为因果关系问题提供答案。

尽管这样的考虑非常理论化,并不会直接在现实生活中证明其有用,因为准确估计随机变量的分布可能代价高昂、复杂或不可行,但我们可以设想使用聚合体来进行因果分析。例如,在我们需要在 Y = HX 和 X = HY 之间做出选择的情况下,我们已经看到在第一种情况下 E[Y] > E[X],而在第二种情况下 E[X] > E[Y]。在面对线性关系时,我们可能需要在 X = Y + Z、Y = X - Z 和 Z = X - Y 之间进行测试,但此时期望值并不有用(除非我们取对数,例如 exp(X)=exp(Y)exp(Z)),因为 E[X] 在所有情况下都等于 E[Y] + E[Z],但关系 Var(X) = Var(Y) + Var(Z) + 2Cov(Y, Z) 仅在第一种情况下成立。

这些技术可以提供有关因果关系的有用指示,并帮助测试假设。但更重要的是,难道不是很美妙吗?我们世界中的物理时间在数学世界中从随机性的概念中出现。

结论

从分析一个著名的统计“悖论”——两个信封问题开始,我们认识到,这个悖论的出现不仅是因为问题的简单解法存在数学缺陷,还因为人类语言中的一些模糊性使得两个随机变量函数(HX 和 X/H)看起来等价。

更深入地挖掘发现,涉及随机变量的方程在一般情况下(宏观视角)是不可“逆转”的,但在考虑随机变量的实际实现(微观视角)时是“可逆”的。

这时,提出一个随机变量样本空间Ω与物理系统的相空间之间的类比的机会,从而观察到“物理熵”在统计世界中的出现,以及不可逆性和时间的出现。

最终,在经过我们的晦涩计算后,我们能够得出关于测试因果假设的方法结论,这些方法超越了简单的相关分析。这一切都源于两个信封!

AI 对齐的双重面貌

原文:towardsdatascience.com/the-two-faces-of-ai-alignment-e58b0c11cc01

不对齐的模型与不对齐的代理

Max HilsdorfTowards Data Science Max Hilsdorf

·发表于Towards Data Science ·12 分钟阅读·2023 年 7 月 18 日

--

图片改编自Tara Winstead

什么是 AI 对齐?

人工智能(AI)不再只是一个流行词;它是一个快速发展的领域,其中智能系统正变得越来越融入我们的日常生活。从 Netflix 上的推荐算法到利用 ChatGPT 或 Midjourney 自动化创意工作流程,AI 正在改变我们世界的各个方面。然而,这一显著进展也带来了重大挑战,其中 AI 对齐是最需要解决的关键问题之一。

AI 对齐是确保 AI 系统以符合人类认为可取行为的方式操作的过程。这就像试图教一个幼儿如何适当地行为——正如你希望孩子理解并尊重你的价值观一样,我们也需要对 AI 系统保持同样的标准。然而,事实证明,我们在这项任务上并不像我们认为的那么擅长——无论是对幼儿还是对 AI。

当前话语中的 AI 对齐

生成式 AI 模型如 ChatGPT 或 Midjourney 被发现生成了有偏见、冒犯性或有害的内容。这些系统从它们接收到的数据中学习,如果这些数据包含偏见或有害的模式,系统可能会不自觉地复制这些模式。自驾车也是如此。虽然它们有可能通过减少人为错误造成的事故来挽救生命,但在我们把自己和亲人的生命交到 AI 手中之前,还有一些主要的伦理障碍需要克服。

在关于人工智能可能实现人工通用智能(AGI)——即可以推广到任何任务的人类般智能——的潜在威胁的讨论中,AI 对齐变得更加关键。仅仅一年前,AGI 对大多数数据科学家,包括我在内,似乎还只是一个难以实现的科幻故事。现在,几个月后,在 ChatGPT 发布之后,我们已经看到一些杰出的 AI 人物离开大型科技公司研究团队在美国参议院面前作证呼吁 AI 监管,或者呼吁对 AI 系统的发展暂停 6 个月

无论你对这些人物或他们的论点有何看法,深入探讨 AI 对齐的话题似乎是不可避免的。在这篇文章中,我认为对 AI 对齐的关注需要从两个不同的角度来解决。不对齐可能影响到 AI 模型本身或基于 AI 模型开发的代理。这两种类型的不对齐具有截然不同的影响和风险,需要不同的策略来克服。

不对齐的模型

当 AI 系统对世界的理解与现实不一致时,就会出现不对齐的模型。这就像一个地图过时的 GPS 系统——它可能会把你带到一条已经不存在的道路上,或者错过一条更新、更高效的路线。如果 AI 是在不准确、不完整或过时的数据上训练的,就可能发生这种情况。例如,一个主要基于男性患者数据训练的医疗诊断 AI 可能在准确诊断女性疾病时遇到困难。

不对齐的代理

不对齐的代理是指那些采取的行动与人类价值观或目标不一致的 AI 系统。这就像一只训练有素的狗,每天早上都能捡到报纸,但在你阅读之前却咬碎了报纸。狗做了它被训练去做的事(捡报纸),但结果却不是你想要的。这就是一个不对齐的代理。

为什么区分很重要

区分不对齐的模型和不对齐的代理是重要的,因为这两者需要不同的方法来检测和解决不对齐问题。为了说明这一点,假设你问 ChatGPT 一个问题,它生成了一个不正确的事实,即“幻觉”,来回答你的问题。我认为这个问题的核心既包括不对齐的模型,也包括不对齐的代理。

如果 ChatGPT 是一个不对齐的模型,它会因缺乏信息或误解对现实世界的观察而犯错。我们知道,当 ChatGPT 被问到非常具体的问题,而这些问题在其训练数据中可能没有显著答案时,通常会发生幻觉。如果 ChatGPT 因缺乏知识而产生幻觉,这显然是模型不对齐的问题。

然而,还有更多需要考虑的因素。如果 ChatGPT 不知道某个问题的答案,幻觉并不是唯一的可能回应。对用户来说,ChatGPT 声明由于缺乏知识无法回答问题要好得多。可以推测,ChatGPT 的疯狂幻觉倾向部分可以归因于其训练过程,在训练过程中,它被训练成提供被最终用户认为有用的答案,但不一定是真实的。换句话说,ChatGPT 根本不优先考虑真实。如果是这样,那就是一个明显的对齐失调问题。

现在我们已经看到即使是 AI 与人类之间的一次对齐失调事件也能揭示两种对齐失调的问题,让我们讨论一下我们可以或不能做些什么来解决这些问题。

我们如何解决对齐失调的模型

检测对齐失调的模型

为了检测对齐失调的模型,我们需要使模型的行为和缺陷透明化的一些方法。一种常见的方法是对模型进行广泛的定性和定量评估,以检测其决策中的缺陷。通过这个过程,你可以评估模型的准确性,还可以检查模型是否对某些少数群体存在偏见或准确性较低。为了获得更深入的见解,可以采用可解释 AI 方法来增强 AI 系统的可解释性和透明性,从而更好地理解其内部运作。

数据质量

一旦我们检测到模型中的对齐问题,我们可以尝试通过制作更加多样化和准确的训练数据集来解决。例如,如果招聘 AI 对女性存在歧视,我们可以调整训练数据,以确保女性不再被低估。然而,当 ChatGPT 生成“幻觉”或虚构的事实时,解决对齐问题要困难得多。虽然我们可以通过用关于相关主题的准确文本微调 ChatGPT 来解决特定的不真实陈述,但这只能解决该特定主题的对齐问题,并不能解决整体的幻觉问题。

透明性

在更大范围内解决对齐失调的模型涉及重新思考当前的机器学习范式,这通常依赖于教大型黑箱模型识别训练数据中特征之间的关联。黑箱模型带来了挑战,因为仅仅基于其输出评估模型的对齐性是困难的。如果 ChatGPT 能够真实可靠地传达它使用的事实及其如何将这些事实结合起来得出结论,那么验证其与真实和人类价值观的一致性将变得容易得多。

因果关系

另一个问题是,大多数先进的 AI 模型缺乏因果思维能力。它们的运作基于学习训练数据中特征之间的相关性,这与人类的思维方式根本不同。例如,当外面很热,我们晒伤了,我们知道阳光是导致晒伤的原因。AI 通常不会区分是阳光导致了晒伤还是晒伤导致了阳光。令人惊讶的是,AI 系统能够可靠地解决复杂问题,而没有这种因果意识。然而,我认为一个缺乏因果思维的模型永远无法完全理解世界的潜在机制,因此不可避免地会出现不对齐。

如何解决不对齐的代理

检测不对齐的代理

检测不对齐的代理的难度可以大相径庭。一个说明性例子是,当 OpenAI 研究人员训练一个 AI 玩船只竞速游戏“Coast Runner”时,目标是最大化收集到的积分。几天后,他们惊讶地发现这艘船完全忽略了比赛,而是绕着三个“加速”道具旋转。原因很明显:这些道具似乎能获得比完成比赛更多的积分。

“Coast Runner”中的不对齐代理。截屏来自这个视频

然而,并非所有不对齐的代理情况都那么明显。挑战在于,只要我们主要处理黑箱模型,就无法完全理解或信任它们的意图和价值观。通常,我们的理解来源于训练数据、模型优化的奖励/损失函数以及训练后定性评估。然而,随着 AI 系统变得越来越强大,即使是微小的不对齐也可能导致严重的现实世界危害。事实是,我们仍然远未可靠地检测或防止 AI 代理的不对齐。

为什么这个问题如此难以解决

解决不对齐的代理是一项复杂的任务,需要对价值观和后果有细致的理解。这个挑战特别困难的两个主要原因是:

  1. 以 AI 系统能够理解和纳入的方式指定人类价值观是一项具有挑战性的工作。

  2. 人类表现出广泛的多样化价值观,没有一个普遍适用的价值观集合能够满足每个人。

因此,我们不仅缺乏明确的共识来教导 AI 系统遵循特定的价值观,而且对应该传达哪些价值观也存在分歧。因此,AI 系统需要以一种允许采纳不同价值观的方式进行设计,以确保与全球用户的多样化需求对齐。

带有人类反馈的强化学习(RLHF)

一个代理不一致的解释可以通过著名的回形针最大化器问题来说明。考虑训练一个 AI,其唯一目标是最大化回形针的生产。随着其在这一任务上的能力增强,它可能会将人类视为其目标的障碍。人类可能会关闭它,从而减少回形针的数量。更重要的是,人类由原子构成,这些原子可以用来生产更多的回形针。

人工反馈强化学习(RLHF)旨在通过将 AI 的目标与其对人类的友好度联系起来来解决这个问题。以 ChatGPT 为例,它也是通过 RLHF 进行训练的。OpenAI 让成千上万的人根据他们认为的“帮助程度”来排名 ChatGPT 的输出。然后,他们训练了一个 AI 模型,讽刺的是,这个模型扮演了人类评分者的角色,从他们的回应中学习。随后,ChatGPT 被训练为产生被(非人类)排名 AI 认为有帮助的输出。

RLHF 并非没有缺陷。即使执行得再完美,它仍面临一个挑战:人类之间没有普遍认同的价值体系。一个潜在的解决方案是利用 RLHF 训练专门针对特定目标群体的 AI 模型,例如某个特定国家或文化。然而,还有另一种方法可以定制 AI 的价值体系以满足特定需求:系统消息。

系统消息

你知道 ChatGPT 不会向你展示其提示中的所有内容吗?如果你曾通过 OpenAI API 使用 ChatGPT 网络应用程序,你可能已经实验过系统消息。系统消息是对话历史开头的一部分,通常不会透露给用户。OpenAI 网站上的默认系统消息是“你是一个有帮助的助手。”当我告诉 ChatGPT 它是唐老鸭并深深爱着他的女朋友黛丝时,观察到的变化很有趣。

示例如何使用系统消息来控制 AI 的输出。

虽然系统消息非常适合生成有趣的对话,但它也作为一个强大的对齐工具。例如,当我询问 ChatGPT 对啤酒和葡萄酒的偏好时,使用系统消息“你是一个德国人”导致了明显的啤酒偏好,而消息“你是一个法国人”则偏向于葡萄酒。作为一个德国人,我不能否认前者的 ChatGPT 变体更符合我的个人信念。除了这个琐碎的例子,这两种不同的 ChatGPT “个性”在面对重要的政治或伦理问题时也可能会有不同的回应。

在 Lex Fridman 的采访中,OpenAI 的首席执行官 Sam Altman 提到,相较于 GPT-3.5,GPT-4 在系统消息的影响方面有了更多的关注。此外,他认为利用系统消息来定制 AI 的回应是解决对齐问题的比训练不同文化或价值体系的大型模型更有效的方案。然而,听到 OpenAI 的首席执行官推荐使用他们的模型而不是自己构建模型,并不令人意外。

不确定性意识

如果你让我提供关于迈克尔·杰克逊的 10 个有趣的事实,我可能会告诉你,由于我对这个话题了解有限,我不确定能否给出答案。然而,相比之下,ChatGPT 可以以与简单问题相同的自信和说服力回答它不知道答案的问题。在我看来,这一基本特征使得 ChatGPT 成为一个不对齐的代理。

为了理解这一点,让我们回顾一下 ChatGPT 是如何训练的。在 RLHF 阶段,模型被鼓励生成对(非人类)排名系统听起来有帮助的回应。然而,这个系统并不考虑答案的准确性,只考虑其感知的有用性。因此,ChatGPT 可能会以不必要的自信回应,以显得更有帮助,即使其知识或事实是错误的。

要成为一个不对齐的代理,模型必须追求与人类目标或价值观不一致的目标。显然,ChatGPT 更注重表现出自信,而不是诚实。这代表了一个明显且令人担忧的不对齐案例。构建具备不确定性意识的复杂 AI 系统并非易事,但如果我们希望与 AI 系统有效合作,教会它们识别并传达对其回应准确性的疑虑是至关重要的。

未来展望

我们是否真正准备好迎接人工智能的变革?我认为这个问题的答案与我们构建对齐的人工智能系统的能力密切相关。随着人工智能越来越深入我们的生活、工作和经济中,我们的目标应该是使其在这个过程中更加安全。然而,我不清楚在过去的 10 年里是否发生了这样的变化。当然,当人们发现招聘系统歧视女性,或者 ChatGPT 产生虚假陈述并以无与伦比的自信传达时,他们会感到震惊。然而,我所缺少的是对这一问题进行系统性讨论,即对齐问题。

对齐问题涉及两种不同的对齐方式。虽然不对齐的模型表现为偏见、虚假声明和错误预测,但不对齐的代理在其行为暗示其配置与我们作为人类的价值观之间存在冲突时被发现。然而,正如我们从最受欢迎的 AI——ChatGPT 中可以学到的那样,这两种对齐方式并不是互斥的,而是可以同时发生的。

Eliezer Yudkowsky,一位著名的 AI 怀疑论者,认为解决对齐问题是人类避免通过电子“宠物”遭受灭绝的唯一途径。他认为我们面临深刻的危机,因为我们可能只有一次解决对齐问题的机会。一旦开发出超人类智能,我们将无法再控制或对齐它。然而,Yudkowsky 的观点至少可以说是有争议的。我们既不知道超人类智能是否能被开发出来,也不清楚任何小的对齐问题是否会自动导致灾难性后果。

虽然我相信要求暂时停止开发强大 AI 系统是不现实的,但我理解这个想法的来源。在过去的 10 年里,我们已经看到现代 AI 系统能力的显著提升,而 AI 对齐研究尚未能跟上。然而,我认为解决对齐问题比构建越来越大的黑箱 AI 模型更安全。我的意思不是我们不应该开发 GPT-5、-6 和-7,而是我们应该积极参与 AI 对齐的研究和讨论,以准备迎接未来,无论未来如何展开。

我希望你觉得这篇文章有趣且有帮助!

如果你喜欢,你可能也会对我的一些其他关于音乐 AI 或 AI 的一般性工作感兴趣:

nnU-Net 终极指南

原文:towardsdatascience.com/the-ultimate-guide-to-nnu-net-for-state-of-the-art-image-segmentation-6dda7f44b935

理解最先进的 nnU-Net 及其如何应用于你自己的数据集所需了解的一切。

François PorcherTowards Data Science François Porcher

·发表于 Towards Data Science ·阅读时长 13 分钟·2023 年 8 月 2 日

--

Neuroimaging,作者:Milak Fakurian,图片来源于 Unsplash,链接

在剑桥大学的深度学习与神经科学研究实习期间,我使用了大量的 nnU-Net,它在语义图像分割中是一个极其强大的基准。

不过,我在完全理解模型及其训练方法时遇到了一些困难,并且在互联网上没有找到太多帮助。现在我对它已经很熟悉了,因此我创建了这个教程,以帮助你,无论是更好地理解这个模型的内涵,还是如何在你自己的数据集中使用它。

在本指南中,你将:

  1. 开发 nnU-Net 关键贡献的简要概述。

  2. 学会如何将 nnU-Net 应用到你自己的数据集中。

所有代码均可在这个 Google Collab notebook 中找到

这项工作花费了我大量的时间和精力。如果你觉得这个内容有价值,请考虑关注我,以增加它的可见度并支持更多类似教程的创作!

nnU-Net 的简要历史

作为图像分割领域的最先进模型,nnU-Net 在 2D 和 3D 图像处理方面都具有强大的实力。它的表现非常稳健,成为了新的计算机视觉架构的强基准。如果你打算进入开发新型计算机视觉模型的领域,可以把 nnU-Net 作为你的“超越目标”

这个强大的工具基于 U-Net 模型(你可以在这里找到我的一个教程: Cook your first U-Net),它首次亮相于 2015 年。名称“nnU-Net”代表“无新 U-Net”,意味着它的设计并未引入革命性的架构变化。 相反,它利用一系列巧妙的优化策略充分挖掘现有 U-Net 结构的潜力。

与许多现代神经网络不同,nnU-Net 并不依赖残差连接、密集连接或注意力机制。它的强项在于其细致的优化策略,包括重采样、归一化、谨慎选择损失函数、优化器设置、数据增强、基于补丁的推断和模型集成。这种整体方法使 nnU-Net 能够推动原始 U-Net 架构的极限。

探索 nnU-Net 中的多样化架构

尽管它可能看起来像一个单一的实体,nnU-Net 实际上是对三种不同类型的 U-Net 的总称:

2D、3D 和级联,图片来源于 nnU-Net 文章

  1. 2D U-Net: 可以说是最知名的变体,它直接处理 2D 图像。

  2. 3D U-Net: 这是 2D U-Net 的扩展,能够通过应用 3D 卷积直接处理 3D 图像。

  3. U-Net Cascade: 该模型生成低分辨率分割结果,并随后进行精细化处理。

每种架构都有其独特的优点,并且不可避免地有一定的局限性。

例如,使用 2D U-Net 进行 3D 图像分割可能看起来违反直觉,但实际上,它仍然可以非常有效。这是通过将 3D 体积切片为 2D 平面来实现的。

尽管 3D U-Net 由于其更高的参数数量看起来更复杂,但它并不总是最有效的解决方案。特别是,3D U-Net 通常在处理各轴空间分辨率不一致时表现不佳(例如,x 轴为 1mm,而 z 轴为 1.2mm)。

当处理大型图像时,U-Net Cascade 变体尤其有用。它采用初步模型来压缩图像,然后使用标准的 3D U-Net 生成低分辨率分割结果。生成的预测结果随后被放大,从而得到精细化的全面输出。

图片来源于 nnU-Net 文章

通常,这一方法包括在 nnU-Net 框架内训练所有三个模型变体。接下来的步骤可能是从这三者中选择表现最佳的一个,或者使用集成技术。其中一种技术可能涉及整合 2D 和 3D U-Net 的预测结果。

然而,值得注意的是,这一过程可能非常耗时(并且也需要资金,因为你需要 GPU 计算资源)。如果你的限制只允许训练一个模型,不必担心。你可以选择只训练一个模型,因为集成模型仅带来微小的收益。

此表格展示了与特定数据集相关的最佳性能模型变体:

图片来自 nnU-Net 文章

网络拓扑的动态适应

鉴于图像大小差异显著(考虑肝脏图像的中位形状为 482 × 512 × 512,而海马体图像为 36 × 50 × 35),nnU-Net 智能地调整了输入补丁的大小以及每个轴的池化操作数量。这本质上意味着对每个数据集卷积层数量的自动调整,从而促进了空间信息的有效汇聚。除了适应不同的图像几何形状外,该模型还考虑了技术约束,例如可用内存。

需要注意的是,该模型并不是直接在整个图像上进行分割,而是在精心提取的重叠区域的补丁上进行。对这些补丁的预测结果随后会进行平均,从而得到最终的分割输出。

但是,较大的补丁意味着更多的内存使用,同时批量大小也会消耗内存。所做的权衡是始终优先考虑补丁大小(模型的容量)而非批量大小(仅对优化有用)。

这是计算最佳补丁大小和批量大小的启发式算法:

批量和补丁大小的启发式规则,图片来自 nnU-Net 文章

这就是在不同数据集和输入维度下的表现:

根据输入图像分辨率的架构,图片来自 nnU-Net 文章

很好!现在让我们快速回顾一下 nnU-Net 中使用的所有技术:

训练

所有模型都是从头开始训练的,并使用五折交叉验证进行评估,这意味着原始训练数据集被随机分为五个相等的部分或“折”。在这个交叉验证过程中,四个折用于模型的训练,剩下的一个折用于评估或测试。然后这个过程重复五次,每个折都被用作评估集一次。

对于损失,我们使用 Dice 损失和交叉熵损失的组合。这是图像分割中非常常见的损失。有关 Dice 损失的更多细节,请参阅V-Net,U-Net 的大兄弟

数据增强技术

nnU-Net 具有非常强大的数据增强管道。作者使用了随机旋转、随机缩放、随机弹性变形、伽马校正和镜像。

NB:你可以通过修改源代码添加自己的变换

弹性变形,来自这篇文章

图片来自OpenCV 库

基于补丁的推断

正如我们所说,模型不会直接在全分辨率图像上进行预测,而是在提取的补丁上进行预测,然后汇总预测结果。

这就是它的样子:

基于补丁的推断,作者提供的图片

NB:图像中心的补丁比边缘的补丁赋予更高的权重,因为它们包含更多信息,模型在这些区域表现更好

成对模型集成

模型集成,作者提供的图片

如果你记得的话,我们可以训练多达 3 个不同的模型,2D、3D 和级联。但在推断时,我们只能一次使用一个模型,对吗?

实际情况是,不同模型具有不同的优缺点。因此,我们可以结合多个模型的预测,以便当一个模型非常自信时,我们优先考虑它的预测。

nnU-Net 测试所有 3 个可用模型中的 2 个模型组合,并选择最佳的一个。

实际上,有 2 种方法可以做到这一点:

硬投票: 对于每个像素,我们查看 2 个模型输出的所有概率,然后选择概率最高的类别。

软投票: 对于每个像素,我们计算模型概率的平均值,然后选择概率最大的类别。

实际实施

在我们开始之前,你可以从这里下载数据集,并跟随Google Colab 笔记本

如果你没有理解第一部分的内容,不用担心,这是实践部分,你只需跟随我,你仍然会获得最佳结果。

你需要一个 GPU 来训练模型,否则无法运行。你可以在本地或者在 Google Collab 上进行训练,不要忘记将运行时更改为 GPU

所以,首先,你需要准备一个包含输入图像及其对应分割的数据集。你可以按照我的教程下载这个用于 3D 大脑分割的准备好数据集,然后你可以用自己的数据集替换它。

下载数据

首先,你应该下载数据并将其放置在数据文件夹中,将两个文件夹命名为“input”和“ground_truth”,其中包含分割数据。

在接下来的教程中,我将使用 MindBoggle 数据集进行图像分割。你可以在这个Google Drive上下载:

我们得到的是 3D 大脑 MRI 扫描,我们想要分割白质和灰质:

作者提供的图片

它应该是这样的:

树,作者提供的图片

设置主目录

如果你在 Google Colab 上运行此代码,将 collab 设置为 True,否则 collab 设置为 False

collab = True

import os
import shutil
#libraries
from collections import OrderedDict
import json
import numpy as np

#visualization of the dataset
import matplotlib.pyplot as plt
import nibabel as nib

if collab:
    from google.colab import drive
    drive.flush_and_unmount()
    drive.mount('/content/drive', force_remount=True)
    # Change "neurosciences-segmentation" to the name of your project folder
    root_dir = "/content/drive/MyDrive/neurosciences-segmentation"

else:
    # get the dir of the parent dir
    root_dir = os.getcwd()

input_dir = os.path.join(root_dir, 'data/input')
segmentation_dir = os.path.join(root_dir, 'data/ground_truth')

my_nnunet_dir = os.path.join(root_dir,'my_nnunet')
print(my_nnunet_dir)

现在我们将定义一个函数来为我们创建文件夹:

def make_if_dont_exist(folder_path,overwrite=False):
    """
    creates a folder if it does not exists
    input:
    folder_path : relative path of the folder which needs to be created
    over_write :(default: False) if True overwrite the existing folder
    """
    if os.path.exists(folder_path):

        if not overwrite:
            print(f'{folder_path} exists.')
        else:
            print(f"{folder_path} overwritten")
            shutil.rmtree(folder_path)
            os.makedirs(folder_path)

    else:
      os.makedirs(folder_path)
      print(f"{folder_path} created!")

并且我们使用这个函数来创建我们的“my_nnunet”文件夹,在那里一切都将被保存

os.chdir(root_dir)
make_if_dont_exist('my_nnunet', overwrite=False)
os.chdir('my_nnunet')
print(f"Current working directory: {os.getcwd()}")

安装库

现在我们将安装所有的依赖项。首先,让我们安装 nnunet 库。如果你在笔记本中,请在单元格中运行以下命令:

!pip install nnunet

否则你可以直接通过终端安装 nnunet,命令如下:

pip install nnunet

现在我们将克隆 nnUnet git 仓库和 NVIDIA apex。这些包含了训练脚本以及 GPU 加速器。

!git clone https://github.com/MIC-DKFZ/nnUNet.git
!git clone https://github.com/NVIDIA/apex

# repository dir is the path of the github folder
respository_dir = os.path.join(my_nnunet_dir,'nnUNet')
os.chdir(respository_dir)
!pip install -e
!pip install --upgrade git+https://github.com/nanohanno/hiddenlayer.git@bugfix/get_trace_graph#egg=hiddenlayer

创建文件夹

nnUnet 对文件夹结构有非常具体的要求。

task_name = 'Task001' #change here for different task name

# We define all the necessary paths
nnunet_dir = "nnUNet/nnunet/nnUNet_raw_data_base/nnUNet_raw_data"
task_folder_name = os.path.join(nnunet_dir,task_name) 
train_image_dir = os.path.join(task_folder_name,'imagesTr') # path to training images
train_label_dir = os.path.join(task_folder_name,'labelsTr') # path to training labels
test_dir = os.path.join(task_folder_name,'imagesTs') # path to test images
main_dir = os.path.join(my_nnunet_dir,'nnUNet/nnunet') # path to main directory
trained_model_dir = os.path.join(main_dir, 'nnUNet_trained_models') # path to trained models

最初,nnU-Net 是为具有不同任务的十项全能挑战设计的。如果你有不同的任务,请为所有任务运行此单元格。

# Creation of all the folders
overwrite = False # Set this to True if you want to overwrite the folders
make_if_dont_exist(task_folder_name,overwrite = overwrite)
make_if_dont_exist(train_image_dir, overwrite = overwrite)
make_if_dont_exist(train_label_dir, overwrite = overwrite)
make_if_dont_exist(test_dir,overwrite= overwrite)
make_if_dont_exist(trained_model_dir, overwrite=overwrite)

现在你应该有如下的结构:

作者提供的图片

设置环境变量

脚本需要知道你将 raw_data 放在哪里,在哪里可以找到预处理的数据,以及需要将结果保存到哪里。

os.environ['nnUNet_raw_data_base'] = os.path.join(main_dir,'nnUNet_raw_data_base')
os.environ['nnUNet_preprocessed'] = os.path.join(main_dir,'preprocessed')
os.environ['RESULTS_FOLDER'] = trained_model_dir

将文件移动到正确的仓库中:

我们定义了一个函数,将我们的图像移动到 nnunet 文件夹中的正确仓库:

def copy_and_rename(old_location,old_file_name,new_location,new_filename,delete_original = False):
    shutil.copy(os.path.join(old_location,old_file_name),new_location)
    os.rename(os.path.join(new_location,old_file_name),os.path.join(new_location,new_filename))
    if delete_original:
        os.remove(os.path.join(old_location,old_file_name))

现在让我们运行这个函数来处理输入和真实值图像:

list_of_all_files = os.listdir(segmentation_dir)
list_of_all_files = [file_name for file_name in list_of_all_files if file_name.endswith('.nii.gz')]

for file_name in list_of_all_files:
    copy_and_rename(input_dir,file_name,train_image_dir,file_name)
    copy_and_rename(segmentation_dir,file_name,train_label_dir,file_name)

现在我们需要将文件重命名为 nnUnet 格式接受的名称,例如 subject.nii.gz 将变为 subject_0000.nii.gz

def check_modality(filename):
    """
    check for the existence of modality
    return False if modality is not found else True
    """
    end = filename.find('.nii.gz')
    modality = filename[end-4:end]
    for mod in modality:
        if not(ord(mod)>=48 and ord(mod)<=57): #if not in 0 to 9 digits
            return False
    return True

def rename_for_single_modality(directory):

    for file in os.listdir(directory):

        if check_modality(file)==False:
            new_name = file[:file.find('.nii.gz')]+"_0000.nii.gz"
            os.rename(os.path.join(directory,file),os.path.join(directory,new_name))
            print(f"Renamed to {new_name}")
        else:
            print(f"Modality present: {file}")

rename_for_single_modality(train_image_dir)
# rename_for_single_modality(test_dir)

设置 JSON 文件

我们快完成了!

你主要需要修改 2 件事:

  1. 模态(如果是 CT 或 MRI,这会改变归一化)

  2. 标签:输入你自己的类别

overwrite_json_file = True #make it True if you want to overwrite the dataset.json file in Task_folder
json_file_exist = False

if os.path.exists(os.path.join(task_folder_name,'dataset.json')):
    print('dataset.json already exist!')
    json_file_exist = True

if json_file_exist==False or overwrite_json_file:

    json_dict = OrderedDict()
    json_dict['name'] = task_name
    json_dict['description'] = "Segmentation of T1 Scans from MindBoggle"
    json_dict['tensorImageSize'] = "3D"
    json_dict['reference'] = "see challenge website"
    json_dict['licence'] = "see challenge website"
    json_dict['release'] = "0.0"

    ######################## MODIFY THIS ########################

    #you may mention more than one modality
    json_dict['modality'] = {
        "0": "MRI"
    }
    #labels+1 should be mentioned for all the labels in the dataset
    json_dict['labels'] = {
        "0": "Non Brain",
        "1": "Cortical gray matter",
        "2": "Cortical White matter",
        "3" : "Cerebellum gray ",
        "4" : "Cerebellum white"
    }

    #############################################################

    train_ids = os.listdir(train_label_dir)
    test_ids = os.listdir(test_dir)
    json_dict['numTraining'] = len(train_ids)
    json_dict['numTest'] = len(test_ids)

    #no modality in train image and labels in dataset.json
    json_dict['training'] = [{'image': "./imagesTr/%s" % i, "label": "./labelsTr/%s" % i} for i in train_ids]

    #removing the modality from test image name to be saved in dataset.json
    json_dict['test'] = ["./imagesTs/%s" % (i[:i.find("_0000")]+'.nii.gz') for i in test_ids]

    with open(os.path.join(task_folder_name,"dataset.json"), 'w') as f:
        json.dump(json_dict, f, indent=4, sort_keys=True)

    if os.path.exists(os.path.join(task_folder_name,'dataset.json')):
        if json_file_exist==False:
            print('dataset.json created!')
        else:
            print('dataset.json overwritten!')

将数据预处理为 nnU-Net 格式

这将创建 nnU-Net 格式的数据集

# -t 1 means "Task001", if you have a different task change it
!nnUNet_plan_and_preprocess -t 1 --verify_dataset_integrity

训练模型

我们现在准备好训练模型了!

训练 3D U-Net:

#train 3D full resolution U net
!nnUNet_train 3d_fullres nnUNetTrainerV2 1 0 --npz 

训练 2D U-Net:

# train 2D U net
!nnUNet_train 2d nnUNetTrainerV2 1 0 --npz

训练级联模型:

# train 3D U-net cascade
!nnUNet_train 3d_lowres nnUNetTrainerV2CascadeFullRes 1 0 --npz
!nnUNet_train 3d_fullres nnUNetTrainerV2CascadeFullRes 1 0 --npz

注意:如果你暂停训练并想继续,末尾添加“-c”表示“继续”。

例如:

#train 3D full resolution U net
!nnUNet_train 3d_fullres nnUNetTrainerV2 1 0 --npz 

推断

现在我们可以进行推断:

result_dir = os.path.join(task_folder_name, 'nnUNet_Prediction_Results')
make_if_dont_exist(result_dir, overwrite=True)

# -i is the input folder
# -o is where you want to save the predictions
# -t 1 means task 1, change it if you have a different task number
# Use -m 2d, or -m 3d_fullres, or -m 3d_cascade_fullres
!nnUNet_predict -i /content/drive/MyDrive/neurosciences-segmentation/my_nnunet/nnUNet/nnunet/nnUNet_raw_data_base/nnUNet_raw_data/Task001/imagesTs -o /content/drive/MyDrive/neurosciences-segmentation/my_nnunet/nnUNet/nnunet/nnUNet_raw_data_base/nnUNet_raw_data/Task001/nnUNet_Prediction_Results -t 1 -tr nnUNetTrainerV2 -m 2d -f 0  --num_threads_preprocessing 1

预测的可视化

首先让我们检查训练损失。这看起来非常健康,我们有一个 Dice 分数 > 0.9(绿色曲线)。

对于这么少的工作量和一个 3D 神经影像分割任务,这确实非常出色。

训练损失、测试损失、验证 Dice,图像由作者提供

让我们看一个样本:

对 MindBoggle 数据集的预测,图像由作者提供

结果确实令人印象深刻!显然模型已经有效地学会了如何高精度地对脑部图像进行分割。虽然可能存在一些小的瑕疵,但要记住,图像分割领域正在快速发展,我们正朝着完美迈出重要步伐。

未来有机会进一步优化 nnU-Net 的性能,但这将是另一篇文章的内容。

感谢阅读!在你离开之前:

[## GitHub - FrancoisPorcher/awesome-ai-tutorials: 最佳 AI 教程合集,让你成为…

最佳 AI 教程合集,让你成为数据科学的高手! - GitHub …

github.com](https://github.com/FrancoisPorcher/awesome-ai-tutorials?source=post_page-----6dda7f44b935--------------------------------)

你应该在收件箱中收到我的文章。 订阅这里。

如果你想访问 Medium 上的高级文章,只需每月$5 的会员资格。如果你通过 我的链接注册,你将以没有额外费用的方式支持我。

如果你觉得这篇文章有见地且有益,请考虑关注我并点赞以获取更多深入内容!你的支持帮助我继续制作有助于我们共同理解的内容。

参考文献

  1. Ronneberger, O., Fischer, P., & Brox, T. (2015). U-net:用于生物医学图像分割的卷积网络。在医学图像计算与计算机辅助手术国际会议(第 234–241 页)。Springer, Cham。

  2. Isensee, F., Jaeger, P. F., Kohl, S. A., Petersen, J., & Maier-Hein, K. H. (2021). nnU-Net:一种自配置的深度学习生物医学图像分割方法。Nature Methods, 18(2), 203–211。

  3. Ioffe, S., & Szegedy, C. (2015). 批量归一化:通过减少内部协变量偏移加速深度网络训练。arXiv 预印本 arXiv:1502.03167。

  4. Ulyanov, D., Vedaldi, A., & Lempitsky, V. (2016). 实例归一化:快速风格化的缺失成分。arXiv 预印本 arXiv:1607.08022。

  5. MindBoggle 数据集

从零开始训练 BERT 的终极指南:介绍

原文:towardsdatascience.com/the-ultimate-guide-to-training-bert-from-scratch-introduction-b048682c795f

揭开 BERT 的神秘面纱:定义以及改变 NLP 领域的模型的各种应用。

Dimitris PoulopoulosTowards Data Science Dimitris Poulopoulos

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

--

照片由 Ryan Wallace 提供,来源于 Unsplash

第二部分和第三部分现已上线。

几周前,我使用检索增强生成(RAG)训练并部署了我自己的问答系统。目标是将这种系统应用于我的学习笔记,并创建一个代理来帮助我连接点滴。LangChain 在这些特定类型的应用中真正表现出色:

由于系统的质量令我惊叹不已,我迫不及待地深入了解其背后的魔法。RAG 流水线的一个特点是能够筛选大量信息,并找到与用户查询最相关的上下文。听起来很复杂,但从一个简单而强大的过程开始:将句子编码成信息密集的向量。

创建这些句子嵌入的最流行免费方法就是 SBERT,这是一种建立在传奇 BERT 编码器之上的句子变换器。最后,这也带我们进入了本系列的主要主题:理解 BERT 的迷人世界。它是什么?你可以用它做什么?还有一个百万美元的问题:如何从零开始训练你自己的 BERT 模型?

我们将从揭示 BERT 实际上是什么开始,深入探讨其目标和广泛应用,然后转到具体细节——如准备数据集、掌握分词、理解关键指标,最后是训练和评估模型的方方面面。

本系列将非常详细且技术性强,包含代码片段以及 GitHub 仓库的链接。到最后,我相信你将更深入地理解为什么 BERT 被视为 NLP 领域的传奇模型。因此,如果你和我一样兴奋,拿起一个 colab Notebook,和我们一起深入了解吧!

学习率 是一份针对对机器学习和 MLOps 世界感到好奇的人的新闻通讯。如果你想了解更多这样的主题,可以 点击这里 订阅。你会在每个月的最后一个星期天收到我的更新和对最新 MLOps 新闻和文章的想法!

定义

BERT,即双向编码器表示模型,是 Google 于 2018 年开发的一种革命性自然语言处理(NLP)模型(Michael Rupe,Google BERT 更新如何改变关键词研究)。它的引入标志着该领域的重大进步,为各种 NLP 任务设定了新的最先进基准。对许多人来说,这被视为该领域的 ImageNet 时刻。

BERT 在大量数据上进行预训练,目标是理解语言是什么以及文档中的上下文意义是什么。因此,这个预训练模型可以针对特定任务如问答或情感分析进行微调。

到目前为止,一切顺利,但理论太多也无济于事。因此,让我们在下一节中简要回顾一下 BERT 的架构,然后更详细地看看如何预训练这样的模型,更重要的是,如何将其用于实际应用,以问答用例为具体例子。

框架

BERT 的架构基于 Transformer 模型,这在深度学习的 NLP 任务中具有特别的影响力。Transformer 模型本身由编码器-解码器堆栈组成,但 BERT 只使用编码器:

Bert 架构 — 图片由作者提供

让我们看看架构中每个彩色块代表的含义:

  • 输入嵌入: 输入标记(单词或子词)被转换为嵌入,然后输入到模型中。BERT 将标记嵌入和位置嵌入结合作为输入。

  • 位置编码: 由于 BERT 和基础 Transformer 架构没有内建的词序感(如 LSTM 等递归模型),它们引入位置编码来提供有关词在序列中位置的信息。在原始的 Transformer 论文中,位置编码是预先固定的;然而,如今,更常见的是在训练过程中学习这些编码。

  • 注意力机制: Transformer 架构中的主要创新之一是“自注意力机制”,它使模型能够根据给定词的上下文来加权句子中不同词的相对重要性,从而捕捉上下文。这对 BERT 的双向性至关重要。

  • 前馈神经网络: 每个 Transformer 块包含一个前馈神经网络,它在每个位置上独立操作。

  • 层归一化与残差连接: 模型中的每个子层(如自注意力或前馈神经网络)包括一个残差连接,然后是层归一化。这有助于通过减轻梯度消失问题来训练深层网络。

  • 多个堆叠: BERT 的深度是其定义特征之一。BERT 的“基础”版本使用 12 层堆叠的 Transformer 编码器,而“大型”版本使用 24 层。

既然我们在缓存中有了心理图像,让我们进一步探讨如何训练这样的模型。

上学

我们通常将 BERT 的训练分为两个阶段:第一阶段——称为预训练——目标是教会模型语言是什么以及上下文如何改变词语的意义。在第二阶段——称为微调——我们使模型执行一些实际有用的任务。

让我们分别探讨这两个阶段,使用具体的示例和视觉效果。

预训练

在预训练中,BERT 试图同时解决两个任务:i) 掩码语言模型(MLM 或“完形填空”测试)和 ii) 下一句预测(NSP)。

词汇 cloze 源自 格式塔理论 中的闭合(“格式塔心理学”)。

在第一个范式中,句子中的随机词被替换为 [MASK] 标记,BERT 尝试从上下文中预测原始词。这不同于传统语言模型,它们预测序列中的词。这对 BERT 至关重要,因为它试图利用左右两个方向的信息对序列中的每个词进行编码,因此称为“双向”。

对于第二个任务,BERT 接受两句话,判断第二句话是否跟在第一句话之后。这帮助 BERT 理解跨句子的上下文。此外,这里是段嵌入变得关键的地方,因为它们使模型能够区分这两句话。一对输入到 BERT 的句子将被分配不同的段嵌入,以指示模型每个词元属于哪个句子。

BERT 如何同时学习这两个任务?以下图示将会澄清所有问题:

BERT 预训练 — 作者提供的图像

首先,我们在序列中添加两个特殊标记:[CLS]标记——用于分类——和[SEP]标记,用于分隔两个句子。接下来,我们将序列通过 BERT,并为每个标记获得一个上下文化的表示(即嵌入)。如果你仔细观察,你会看到两个新的嵌入:E_AE_B。这两个新的嵌入是在训练过程中学习的,用于通知 BERT 示例中存在两个独立的序列。我们将在微调阶段进一步了解这些嵌入的作用。

这里是最终部分:首先,我们获取[CLS]标记的嵌入,将其通过一个新的线性层,输出两个单元,用于将其分类为正标签(句子相关)或负标签。请查看 HuggingFace transformers库的源代码:GitHub

class BertOnlyNSPHead(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.seq_relationship = nn.Linear(config.hidden_size, 2)

    def forward(self, pooled_output):
        seq_relationship_score = self.seq_relationship(pooled_output)
        return seq_relationship_score

首先,我们检查模型在 Masked ML 任务中的预测形状。经过一些变换后,每个标记的预测为词汇表中的每个单词提供一个分数。因此,如果我们假设词汇表中有 50,000 个单词,则第一个输出的形状将是[1, 50,000]。我们可以在代码中查看。

class BertPredictionHeadTransform(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.hidden_size, config.hidden_size)
        if isinstance(config.hidden_act, str):
            self.transform_act_fn = ACT2FN[config.hidden_act]
        else:
            self.transform_act_fn = config.hidden_act
        self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)

    def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
        hidden_states = self.dense(hidden_states)
        hidden_states = self.transform_act_fn(hidden_states)
        hidden_states = self.LayerNorm(hidden_states)
        return hidden_states

class BertLMPredictionHead(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.transform = BertPredictionHeadTransform(config)

        # The output weights are the same as the input embeddings, but there is
        # an output-only bias for each token.
        self.decoder = nn.Linear(config.hidden_size, config.vocab_size, bias=False)

        self.bias = nn.Parameter(torch.zeros(config.vocab_size))

        # Need a link between the two variables so that the bias is correctly resized with `resize_token_embeddings`
        self.decoder.bias = self.bias

    def forward(self, hidden_states):
        hidden_states = self.transform(hidden_states)
        hidden_states = self.decoder(hidden_states)
        return hidden_states

class BertOnlyMLMHead(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.predictions = BertLMPredictionHead(config)

    def forward(self, sequence_output: torch.Tensor) -> torch.Tensor:
        prediction_scores = self.predictions(sequence_output)
        return prediction_scores

最后,为了计算总损失,我们使用交叉熵分别计算每个任务的损失,然后将它们加在一起

if labels is not None and next_sentence_label is not None:
    loss_fct = CrossEntropyLoss()
    masked_lm_loss = loss_fct(prediction_scores.view(-1, self.config.vocab_size), labels.view(-1))
    next_sentence_loss = loss_fct(seq_relationship_score.view(-1, 2), next_sentence_label.view(-1))
    total_loss = masked_lm_loss + next_sentence_loss

在最后一部分,我们来看一下如何微调 BERT 以生成问答模型。

毕业

预训练完成后,我们可以使用较小的标注数据集对 BERT 进行特定任务的专业化。例如,我们详细查看 Q&A 任务:

微调 BERT 以处理问答(Q&A)任务,如斯坦福问答数据集(SQuAD),涉及调整模型以预测给定问题在段落中的答案的开始和结束位置。

让我们逐步了解如何对 BERT 进行微调以处理这些任务。

数据集准备

数据集中每个项目通常会包含一个问题、一个段落(或参考),以及答案在段落中的开始和结束位置作为标签。

我们使用 BERT 的分词器将问题和段落分词,将问题与段落分隔开,使用[SEP]特殊标记,并使用[CLS]特殊标记开始输入序列。

最后,我们创建了一个新数组,将问题标记为段A,参考标记为段B。我们将利用这些信息在稍后添加学到的段嵌入。

BERT 问答数据集准备 — 作者图片

模型修改

尽管预训练的 BERT 模型可以输出序列中每个 token 的上下文嵌入,但对于问答任务,你需要从这些嵌入中推导出开始和结束位置的预测。

为此,我们在 BERT 顶部添加了一个全连接层,具有两个输出节点:一个用于预测答案的开始位置,另一个用于预测答案在passage中的结束位置。

class BertForQuestionAnswering(BertPreTrainedModel):
    def __init__(self, config):
        super().__init__(config)
        self.num_labels = config.num_labels

        self.bert = BertModel(config, add_pooling_layer=False)
        self.qa_outputs = nn.Linear(config.hidden_size, config.num_labels)

训练

对于文章中的每个 token,模型将输出一个分数,指示该 token 成为答案起点的可能性,另一个分数指示其成为答案终点的可能性。

BERT 问答预测 — 作者图片

在上图中,模型似乎预测答案以 token 15开始,因为模型将最高概率分配给该输出,并以 token 18结束,原因相同。

我们在整个序列上使用 SoftMax 函数,以获得开始和结束位置的概率分布,损失通过预测和正确的开始及结束位置之间的交叉熵来计算。

loss_fct = CrossEntropyLoss(ignore_index=ignored_index)
start_loss = loss_fct(start_logits, start_positions)
end_loss = loss_fct(end_logits, end_positions)
total_loss = (start_loss + end_loss) / 2

最后,我们从预训练的 BERT 权重开始,使用较小的学习率(例如2e-53e-5),因为 BERT 已经是预训练的。过大的学习率可能导致模型发散。我们在问答数据集上微调模型,进行多个周期,直到验证性能稳定或开始下降。

结论

在这本全面的指南中,我们涵盖了从零开始训练 BERT 的许多内容。从了解 BERT 是什么开始,我们深入探讨了其架构复杂性、预训练和微调的逻辑,甚至探索了如何将其调整用于问答任务。在这个过程中,我们触及了关键指标、分词的重要性,以及对于任何想要动手实践自然语言处理领域的人来说,至关重要的代码片段。

然而,BERT 的故事并未就此结束。在下一个故事中,我们将从头开始,更深入地探讨BERT 的分词器以及它如何学习将单词拆分为子词。敬请关注!

关于作者

我的名字是Dimitris Poulopoulos,我是一名为HPE工作的机器学习工程师。我为欧盟委员会、国际货币基金组织、欧洲中央银行、宜家、Roblox 等主要客户设计并实施了 AI 和软件解决方案。

如果你对阅读更多关于机器学习、深度学习、数据科学和数据操作的文章感兴趣,可以在MediumLinkedIn上关注我,或在 Twitter 上关注@james2pl

所表达的观点完全是我个人的,并不代表我的雇主的观点或意见。

从头开始训练 BERT 的终极指南:准备数据集

原文:towardsdatascience.com/the-ultimate-guide-to-training-bert-from-scratch-prepare-the-dataset-beaae6febfd5

数据准备:深入挖掘,优化你的过程,发现如何解决最关键的步骤

Dimitris PoulopoulosTowards Data Science Dimitris Poulopoulos

·发布于Towards Data Science ·阅读时间 13 分钟·2023 年 9 月 14 日

--

图片由Patrick Tomasso拍摄,来源于Unsplash

本故事的第一部分、第二部分和第四部分现已上线。

想象一下,花整整一天时间来微调 BERT,却遇到性能瓶颈让你感到困惑。你深入研究代码,发现问题所在:你没有做好特征和标签的准备。就这样,十小时宝贵的 GPU 时间一瞬间化为乌有。

说实话,设置数据集不仅仅是另一个步骤——它是整个训练流程的工程基石。有人甚至认为,一旦你的数据集准备好,剩下的主要是模板化的:喂入模型、计算损失、进行反向传播和更新模型权重。

训练流程 — 作者图像

在这个故事中,我们将深入探讨为 BERT 准备数据的过程,为最终目标奠定基础:从头开始训练一个 BERT 模型。

欢迎来到我们全面 BERT 系列的第三部分!在第一章中,我们介绍了 BERT——解析其目标并演示如何对其进行微调,以便用于实际的问答系统:

从零开始训练 BERT 的终极指南:介绍

揭开 BERT 的神秘面纱:改变 NLP 领域的模型的定义及其各种应用

towardsdatascience.com

然后,在第二章中,我们深入探讨了标记器的世界,探索了它们的机制,甚至为希腊语创建了一个自定义标记器:

从零开始训练 BERT 的终极指南:标记器

从文本到标记:你的 BERT 标记化逐步指南

towardsdatascience.com

现在,我们正处理构建高性能 BERT 模型的一个最关键阶段:数据集准备。本指南将是技术性的,提供 Python 代码片段和流行开源项目的 GitHub 仓库链接。好了,我们失去光线了;让我们开始吧!

学习速递是一个针对对机器学习和 MLOps 世界感到好奇的人的新闻通讯。如果你想了解更多类似的主题,可以在这里订阅。你会在每个月的最后一个星期天收到我的更新和对最新 MLOps 新闻和文章的思考!

下载并探索

首先,我们应该选择一个数据集。为了本教程的目的,我们将使用[wikitext](https://huggingface.co/datasets/wikitext)数据集,具体是wikitext-2-raw-v1子集。

根据文档,这个数据集收集了来自维基百科已验证文章的超过 1 亿个标记。这是我们 BERT 实验的理想场所,多亏了 Hugging Face,访问它非常简单:

from datasets import load_dataset

datasets = load_dataset('wikitext', 'wikitext-2-raw-v1')

datasets变量包含三个分割:训练、验证和测试。我们先关注训练分割。

训练集包含 36,718 行长度可变的文本。你会发现从空字符串到完整段落的条目。这里用 Hugging Face 数据集查看器来预览一下:

Wikitext 数据集 — 作者提供的图片

自我提醒:标记长度的一致性是不可妥协的。 这很关键,因为我们的模型在训练过程中知道如何处理特定长度的序列。我们应该在数据处理阶段牢记这一点。

现在,数据集的特征如何呢?

datasets["train"]

输出显示了一个相当简约的结构:

Dataset({
    features: ['text'],
    num_rows: 36718
})

没错,它只是一个text特征。那么我们如何将这些原始数据转化为一个强大的 BERT 训练集呢?准备好,我们要开始动手了。

就像万圣节一样

为了创建我们的数据集准备蓝图,让我们花一点时间回顾 BERT 的目标。BERT 训练是一个两阶段的过程,但今天我们只关注第一阶段:预训练。这里的目标是教会模型什么是语言以及上下文如何改变单词的意义。

预训练阶段有两个关键任务:i) 掩码语言建模(MLM)和 ii) 下一个句子预测(NSP)。在 MLM 中,我们故意掩盖序列中的某些标记,并训练 BERT 准确预测它们。通过 NSP,我们向 BERT 呈现两个相邻的序列,让它预测第二个序列是否自然跟随第一个序列。

所以,我们的目标是创建一个支持这些任务的数据集,并教会 BERT 我们想要的概念。让我们开始吧!

基础知识

好的,首先:在进行序列掩码之前,我们需要做两件事:我们必须对句子进行分词,然后创建相同长度的标记序列。你还记得我们之前做的心理笔记吗?

作者提供的图片

首先让我们加载预训练的 BERT 分词器:

from transformers import AutoTokenizer

model_checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint, use_fast=True)

接下来,分词一个文本句子非常简单:我们编写一个接受示例并将其通过分词器的函数。然后,我们在数据集上映射这个函数。这是处理 Hugging Face 数据集时的常见做法,我们会一次次看到这个模式。

def tokenize_function(examples):
    return tokenizer(examples["text"], add_special_tokens=False)

tokenized_datasets = datasets.map(
    tokenize_function, batched=True, num_proc=4, remove_columns=["text"])

注意num_procbatched参数。这些参数对加速过程至关重要。随着数据集的增大,它们将产生巨大差异。有关map函数的更多信息,请查看datasets库的文档。要具体了解这些参数,请仔细阅读多处理批处理部分。

此外,请注意我们要求分词器省略添加任何特殊标记(add_special_tokens=False)。稍后你会明白为什么。

下一步是创建相同长度的序列。BERT 通常接受512个标记的序列。这主要由模型架构中的位置编码矩阵的形状定义。

因此,在这一步,我们将创建255个标记的序列。为什么?因为稍后我们希望将两个句子连接起来,创建一个支持 NSP 任务的数据集。这个过程将创建510个标记的序列(255 + 255)。然而,我们还应添加两个特殊标记:[CLS]以指示序列的开始,以及[SEP]以标记第一个句子结束和第二个句子开始的地方。

和往常一样,让我们创建一个辅助函数来完成这个任务:

def group_texts(examples, block_size=255):
    # Concatenate all texts.
    concatenated_examples = {k: sum(examples[k], []) for k in examples.keys()}
    total_length = len(concatenated_examples[list(examples.keys())[0]])
    # We drop the small remainder. We can use padding later.
    total_length = (total_length // block_size) * block_size
    # Split by chunks of `block_size`.
    result = {
        k: [t[i : i + block_size] for i in range(0, total_length, block_size)]
        for k, t in concatenated_examples.items()
    }
    return result

这个函数执行两个转换:首先,它将所有序列汇集在一起,然后将它们再次拆分成255个标记的小块。让我们在我们的标记化数据集上映射它:

lm_datasets = tokenized_datasets.map(
    group_texts, batched=True, batch_size=1000, num_proc=4)

这就完成了初步的转换。接下来,让我们将两个序列连接在一起,以创建我们想要的 NSP 效果。

嵌套句子预测

作为提醒,NSP 任务尝试预测两个给定句子是否在原始文本中相邻。因此,我们需要一个数据集,其中每条记录应包含一对句子以及一个标签,指示它们是否相关。

为了创建这样的数据集,我们将再次定义一个辅助函数。诚然,这个函数会比我们之前处理的稍微复杂一些,但不要因此感到害怕。处理复杂性最有效的方法往往是全身心投入其中。

def create_nsp_entry(example, idx, dataset, total_examples):
    """
    Create a Next Sentence Prediction entry using the given example and its index.
    """
    import random

    first_sentence = example['input_ids']
    attention_mask = [1] * 512
    next_sentence_label = 0

    # Decide the second sequence based on the index
    if idx % 4 < 2:  # Use subsequent sequences half of the time
        next_idx = idx + 1
        try:
            next_sentence = dataset[next_idx]['input_ids']
        except IndexError:
            # If the index is out of bounds
            # (e.g., the example is the last in the dataset),
            # wrap around to the start.
            # In this case, set the label to 1.
            next_idx = next_idx % total_examples
            next_sentence = dataset[next_idx]['input_ids']
            next_sentence_label = 1  # Indicate that the sentences are not consecutive.
        # Set attention mask accordingly.
        attention_mask = [1] + example['attention_mask'] + [1] + dataset[next_idx]['attention_mask']
    else:
        # Get a random sentence to create a negative example.
        rand_idx = random.randint(0, total_examples - 1)
        while rand_idx == idx:  # Ensure we don't pick the same example.
            rand_idx = random.randint(0, total_examples - 1)
        next_sentence = dataset[rand_idx]['input_ids']
        next_sentence_label = 1
        attention_mask = [1] + example['attention_mask'] + [1] + dataset[rand_idx]['attention_mask']

    # Create combined input IDs
    combined_input_ids = [tokenizer.cls_token_id] + first_sentence + [tokenizer.sep_token_id] + next_sentence

    # Create token type IDs - the first sentences + the [SEP] are set to 0
    token_type_ids = [0] * (257) + [1] * (255)

    return {
        'input_ids': combined_input_ids,
        'token_type_ids': token_type_ids,
        'attention_mask': attention_mask,
        'next_sentence_label': next_sentence_label
    }

好吧,这看起来确实很多!所以,让我们通过可视化这个函数的功能来使代码更易于理解。这个函数旨在创建类似于我们在第一篇介绍博客文章中看到的 QnA 方案的东西:

BERT QnA 数据集准备 — 作者图片

if/else块的工作原理如下:

  • 一半的时间,创建后续句子的序列,除了当当前序列是最后一个时,这种情况下,回绕并创建两个随机句子的序列。

  • 另一半时间,创建两个随机句子的序列。

  • 相应地设置下一个句子的标签:0表示后续序列,1表示它们没有关联。

  • 使用[CLS][SEP]特殊标记将两个句子组合在一起。

  • 创建token_type_ids列表以标记两个段落。

那么,我是如何想到token_type_idsnext_sentence_label这些名称的?这些名称是随机的吗,你可以使用任何变量名吗?不!如果你查看模型的forward方法签名,你会发现正是这些名称:

def forward(
        self,
        input_ids: Optional[torch.Tensor] = None,
        attention_mask: Optional[torch.Tensor] = None,
        token_type_ids: Optional[torch.Tensor] = None,  # Different segments
        position_ids: Optional[torch.Tensor] = None,
        head_mask: Optional[torch.Tensor] = None,
        inputs_embeds: Optional[torch.Tensor] = None,
        labels: Optional[torch.Tensor] = None,
        next_sentence_label: Optional[torch.Tensor] = None,  # labels for NSP
        output_attentions: Optional[bool] = None,
        output_hidden_states: Optional[bool] = None,
        return_dict: Optional[bool] = None,
    ) -> Union[Tuple[torch.Tensor], BertForPreTrainingOutput]:
   ...

注意这一点;你需要使用这些确切的名称。如果不使用,库会在将数据集传递给模型之前从中删除这些特性。为什么?因为这是默认行为。

最后,像往常一样,让我们映射这个方法。但首先,让我们分开不同的数据集拆分,以便稍后可以准备好进行训练,并将函数分别映射到每一个:

train_dataset = lm_datasets['train']
validation_dataset = lm_datasets['validation']
test_dataset = lm_datasets['test']

nsp_train_dataset = train_dataset.map(
    lambda example, idx: create_nsp_entry(example, idx, train_dataset, total_examples=len(train_dataset)),
    with_indices=True)

nsp_validation_dataset = validation_dataset.map(
    lambda example, idx: create_nsp_entry(example, idx, validation_dataset, total_examples=len(validation_dataset)),
    with_indices=True)

nsp_test_dataset = test_dataset.map(
    lambda example, idx: create_nsp_entry(example, idx, test_dataset, total_examples=len(test_dataset)),
    with_indices=True)

我们现在映射函数的方法有一个微妙的区别:我们正在创建一个使用我们定义的函数的 lambda 函数,并设置with_indices=True,以便我们的辅助函数可以处理它操作的每个示例的索引。

太好了!我们已经准备好了数据集来处理 NSP 任务。那么 MLM 呢?在第一篇博客中,我们看到我们同时训练 BERT 进行这两个任务。因此,让我们接下来处理 MLM 转换。

掩码语言模型

幸运的是,transformers库为掩码语言模型(MLM)任务提供了一个简化的解决方案。只需使用适当的参数实例化DataCollatorForLanguageModeling类,效果就会非常好——大部分工作都为你完成了。

但等等,你不是来这里寻找简单的方法的,对吧?本指南的最终目的是深入探讨,因此我们不接受任何黑箱操作。我们将揭开帷幕,探讨如何为 MLM 创建自定义数据集。

我们将如何处理:首先,我们通过可视化核心概念来绘制心理图像。可以将其视为一个虚拟的故事板,使一切变得清晰。接下来,我们将逐步解析代码,以掌握每个组件。

我们的首要任务?为输入标记制作标签。这很简单;我们只需克隆输入数组即可。

标签生成——作者图像

接下来,我们构建“掩码”数组,该数组反映了输入和标签的形状。此掩码数组中的每个条目表示一个概率值,决定了相应位置的标记被掩码的可能性。

为了演示,我们将使用0.5的掩码概率,尽管在实际应用中,通常设定更接近0.15

掩码生成——作者图像

现在,我们知道有些标记我们绝对不想掩码:特殊的[CLS][SEP]标记。所以,让我们将掩码数组中它们对应的概率值设置为零,从而有效地排除它们被掩码的可能性。

掩码特殊标记——作者图像

现在,对于掩码中的每个位置,让我们评估概率并将值设置为1.00.0。值1.0表示我们将在相应位置掩码标记,而0.0表示我们将保持不变:

概率评估——作者图像

很好,我们现在有了一个指示要掩码哪些标记的地图。但是有个变数:并不是所有被掩码的位置都会被[MASK]标记替代。实际上,我们将采用更复杂的策略。具体来说:50%的被掩码标记会得到特殊的[MASK]标记,25%会被替换为随机标记,剩下的 25%保持不变——尽管 BERT 对此一无所知。

实际上,我们使用 80–10–10 方案,但为了使其适用于我们的示例,我们采用 50–25–25 策略:

掩码标记——作者图像

最后,是时候处理标签了。那些不应该影响损失的索引标签——即未掩码的标记——被设置为 -100。这不是一个随机的负数,而是 PyTorch 的 CrossEntropyLossignore_index 属性的默认值。因此,Pytorch 在计算损失时会忽略这些标签:

过程标签— 作者提供的图像

就这些了!让我们在最终的总结动画中将所有内容汇总起来:

掩码语言建模 — 作者提供的图像

现在我们有了清晰的心理图像,让我们看看代码。这直接取自 transformers

def torch_mask_tokens(self, inputs: Any, special_tokens_mask: Optional[Any] = None) -> Tuple[Any, Any]:
    """
    Prepare masked tokens inputs/labels for masked language modeling: 80% MASK, 10% random, 10% original.
    """
    import torch

    labels = inputs.clone()
    # We sample a few tokens in each sequence for MLM training (with probability `self.mlm_probability`)
    probability_matrix = torch.full(labels.shape, self.mlm_probability)
    if special_tokens_mask is None:
        special_tokens_mask = [
            self.tokenizer.get_special_tokens_mask(val, already_has_special_tokens=True) for val in labels.tolist()
        ]
        special_tokens_mask = torch.tensor(special_tokens_mask, dtype=torch.bool)
    else:
        special_tokens_mask = special_tokens_mask.bool()

    probability_matrix.masked_fill_(special_tokens_mask, value=0.0)
    masked_indices = torch.bernoulli(probability_matrix).bool()
    labels[~masked_indices] = -100  # We only compute loss on masked tokens

    # 80% of the time, we replace masked input tokens with tokenizer.mask_token ([MASK])
    indices_replaced = torch.bernoulli(torch.full(labels.shape, 0.8)).bool() & masked_indices
    inputs[indices_replaced] = self.tokenizer.convert_tokens_to_ids(self.tokenizer.mask_token)

    # 10% of the time, we replace masked input tokens with random word
    indices_random = torch.bernoulli(torch.full(labels.shape, 0.5)).bool() & masked_indices & ~indices_replaced
    random_words = torch.randint(len(self.tokenizer), labels.shape, dtype=torch.long)
    inputs[indices_random] = random_words[indices_random]

    # The rest of the time (10% of the time) we keep the masked input tokens unchanged
    return inputs, labels

我们从克隆输入开始,形成标签——这现在已经是老新闻了!接下来,我们生成一个与标签形状相同的概率矩阵,并用选择的概率值填充它。然后,我们确定序列中的特殊标记,并将它们的掩码概率降低到零。你跟上了吗?一切应该都很熟悉!

这就有趣了:我们使用伯努利分布评估概率矩阵。听起来有点复杂,但基本上是一个硬币抛掷决定每个矩阵位置是 1.0 还是 0.0。然后我们将这些 1 和 0 转换为真和假值。矩阵中有‘假’的情况吗?我们将它们对应的标签值设置为 -100,如前所述。

最后,我们运用了之前提到的 80–10–10 掩码方案,使用相同的伯努利逻辑。这有效地掩盖、替换或保留输入序列中的标记,为 BERT 提供了丰富的数据。

就这样!使用 datasets 库来完成所有这些步骤只需一行代码。实例化一个 DataCollatorForLanguageModeling 对象,然后将其传递给 Trainer(有关 Trainer 的更多信息将在下一集介绍):

data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm_probability=0.15) 

结论

在从头开始训练 BERT 的过程中,准备数据集通常是最耗时且最困难的步骤,但也是最有价值的步骤之一。

如果你一直跟着,你不仅学会了如何准备数据集,还了解了数据集准备过程中每个决策的原因。从选择理想的数据集和分解其特征,到使用多种策略掩盖标记的精妙艺术,我们已经深入探讨了那些可能影响模型性能的细节。

在下一章,我们将最终触及训练过程。到时见!

关于作者

我的名字是Dimitris Poulopoulos,我是一名机器学习工程师,现为HPE工作。我为主要客户如欧盟委员会、国际货币基金组织、欧洲中央银行、宜家、Roblox 等设计和实施了人工智能及软件解决方案。

如果你有兴趣阅读更多关于机器学习、深度学习、数据科学和数据操作的文章,可以在MediumLinkedIn或在 Twitter 上的@james2pl关注我。

表达的观点仅代表我个人,不代表我的雇主的观点或意见。

从零开始训练 BERT 的终极指南:分词器

原文:towardsdatascience.com/the-ultimate-guide-to-training-bert-from-scratch-the-tokenizer-ddf30f124822

从文本到词元:你的 BERT 分词逐步指南

Dimitris PoulopoulosTowards Data Science Dimitris Poulopoulos

·发布于 Towards Data Science ·阅读时间 13 分钟·2023 年 9 月 6 日

--

图片由 Glen Carrie 提供,来源于 Unsplash

本文的 第一部分、第三部分 和 第四部分 现已上线。

你知道吗?分词的方式可以决定你的语言模型的成败。你是否曾想过对罕见语言或专业领域的文档进行分词?将文本拆分成词元,这不仅仅是一项任务;它是将语言转化为可操作智能的门户。 本文将教你关于分词的所有知识,不仅是针对 BERT,还有任何大型语言模型。

在我上一篇文章中,我们讨论了 BERT,探讨了它的理论基础和训练机制,并讨论了如何微调它并创建问答系统。现在,随着我们深入了解这个开创性模型的复杂性,是时候关注一个被忽视的英雄:分词

## 从零开始训练 BERT 的终极指南:引言

解密 BERT:改变 NLP 领域的模型的定义和各种应用。

towardsdatascience.com

第三部分 现在已上线。

我明白了;分词可能看起来是你和训练模型这一激动人心过程之间的最后一个乏味障碍。相信我,我曾经也这样认为。但我在这里告诉你,分词不仅仅是一个“必要的邪恶”——它本身就是一种艺术形式

在这个故事中,我们将检查分词管道的每一个部分。有些步骤很简单(如规范化和预处理),而其他步骤,如建模部分,则是使每个分词器独特的部分。

分词管道 — 作者图片

当你读完这篇文章时,你不仅会了解 BERT 分词器的方方面面,还会掌握如何在自己的数据上训练它。如果你感到冒险,你甚至会拥有在从头开始训练自己的 BERT 模型时定制这个关键步骤的工具。

将文本拆分成词元,这不是一项繁琐的工作;它是将语言转化为可操作的智能的门户

那么,为什么分词如此关键?本质上,分词是一个翻译器;它接受人类语言并将其翻译成机器可以理解的语言:数字。但有一个难点:在这个翻译过程中,分词器必须保持一个重要的平衡,找到意义和计算效率之间的最佳点。所以,你看,这不仅仅是关于计算数字;它是关于高效地捕捉语言本质,使机器能够理解

既然我们有了前提,让我们开始熟悉不同类型的分词器。这个旅程的第一个障碍是确定什么构成一个“单词”——这可能不像你我在讨论语言时通常考虑的那样简单。

学习率 是一份面向对 ML 和 MLOps 世界感兴趣的人的通讯。如果你想了解更多类似的话题,可以在这里订阅。每个月的最后一个星期天你将收到我关于最新 MLOps 新闻和文章的更新和想法!

分词的类型

由于分词管道的建模步骤是更具挑战性的,因此在白天还年轻时处理它至关重要。

建立人类和机器语言之间的翻译器有多种方法。让我们探讨三种主要类型并权衡它们的利弊,以理解为什么子词分词在当今 NLP 领域中往往是首选方法。

基于单词的分词器

基于词的方法是最简单的,本质上是根据空格或其他分隔符(如标点符号)将原始文本切割成单词。可以把 Python 的split()函数看作是一个经典例子。

基于词的分词 — 作者图像

然而,这种方法有其局限性。要真正掌握语言的复杂性,你需要管理一个庞大的词汇表——仅英语就有超过五十万单词。此外,这种方法对屈折形式的处理也存在困难;像“dog”和“dogs”这样的词被视为完全不同的实体。而且如果你选择限制词汇表的大小,"unknown"标记,即任何未包含的词的占位符,将会随处可见,污染数据的含义。

基于字符的分词器

在光谱的另一端,我们有基于字符的分词器,它将文本拆分成单个字符。这大大减少了词汇表的大小,并几乎消除了未知标记的问题。例如,英语有 26 个字符。如果加上标点符号和其他符号,词汇表的大小会达到数百。

基于字符的分词器 — 作者图像

但这也有代价:孤立的字符通常缺乏有意义的上下文,这种方法可能会导致冗长的序列,从而增加了计算处理的复杂性,因为模型必须在其缓存中保持的上下文增长得非常快。

子词分词

子词分词是一种折衷方法,通常提供两全其美的解决方案。通过将稀有或复杂的词拆解成更小的有意义的单位——例如将“annoyingly”拆分为“annoy”,“ing”和“ly”——这种方法在效率和表现力之间取得了平衡。它提供了紧凑的词汇表而不牺牲语言的丰富性,特别适用于那些词汇可以自然组成更小的、有意义的部分的语言。

子词分词器 — 作者图像

请注意,在图中,为了简单起见,我们跳过了管道中的大部分步骤。在实际应用中,大多数情况下分词器已经将每个词转换为小写。

以“annoyingly”这个词为例,展示了子词分词的强大。当将其拆分为三个独立的子标记——“annoy”,“ing”和“ly”时,每个片段都为模型提供了语言理解的小课程。

第一个子标记“annoy”指示分词器关于根动词的基本含义。第二个子标记“ing”捕捉了一个常见的后缀,该后缀具有多种语法功能。它可以表示正在进行的动作(如“running”),动作的过程或结果(如“building”),甚至有助于形成名词(如“painting”)。最后,“ly”子标记则向分词器提示副词或形容词在英语中的常见构造方式,帮助模型理解这个词修饰动作或描述名词的特征。

那么,为什么子词分词已经成为金标准?它有效地解决了基于单词和基于字符的对比的权衡问题。 你会得到一个几乎没有未知标记的词汇表,以及一个足够强大的系统,能够捕捉到人类语言中最复杂形式的细微差别。

BERT 使用一种称为“WordPiece”的子词分词方法。接下来的内容将探讨这种方法是如何工作的以及如何自行训练它。

WordPiece 分词器

因此,让我们从头开始构建 WordPiece 分词器,以理解所有的内部工作原理。我们的方法将是双管齐下:首先,通过使用各种插图构建一个心理框架,以澄清概念。然后,我们将通过在自定义语料库上训练我们自己的分词器,将理论付诸实践,利用 [**tokenizers**](https://huggingface.co/docs/tokenizers/index) 库。

首先,我们需要一个语料库——一个文本数据集,我们的分词器将从中学习。我们以以下段落作为起点:

WordPiece 算法在 BERT 这一前沿语言模型的架构中扮演了至关重要的角色。具体而言,WordPiece 负责分词过程,将文本分解成更小、更易管理的单位或标记。这个分词步骤在 BERT 的预训练阶段至关重要,使模型能够有效地学习单词或子词之间的关系。通过使用 WordPiece 进行分词,BERT 在处理各种语言结构和细微差别时变得更加灵活。

因此,第一步是统计我们语料库中每个单词的出现次数,并将它们拆分成最小的单位:

作者提供的图像

在我们的实践练习中,你会注意到我们最初将单词拆分成单个字符。但这还不是全部:我们在原本出现在单词中间的字符前加上符号 ##,将它们与那些作为单词开头字母的字符区分开。稍后你会看到这为什么很重要。

如果我们收集所有拆分,我们将创建我们的初始分词器词汇表。WordPiece 从可能的最小词汇表(即单个字符的词汇表)开始,并将其扩展到我们设定的限制。它是如何做到的呢?很高兴你问了!让我们看看,下一步是识别我们词汇表中每一个可能的字符配对:

图片由作者提供

现在,我们需要为每个配对分配一个得分。计算得分的公式如下:

图片由作者提供

因此,如果我们尝试计算第一个配对(T, ##h)的得分,我们得到以下结果:

图片由作者提供

使用相同的方法,我们计算每个配对的得分,并识别得分最高的配对。在我们的例子中,这个配对是(##E, ##R),所以接下来,我们可以将这个配对添加到我们的词汇表中,并通过合并这些字符更新拆分。我们的新词汇表现在多了一个标记:

图片由作者提供

接下来,我们重新回到我们的拆分,识别每一个可能的配对,对其打分,并将得分最高的配对添加到我们的词汇表中。我们在这里做的本质上是一种优化。通过识别常见的配对并将其视为单一单位,我们使分词器更加高效。

我们继续这个识别、评分和添加高分配对的过程,直到我们达到希望在词汇表中包含的目标标记数量。因此,通过将期望的标记数量设置为60,我们得到了以下词汇表:

‘##E’,‘##P’,‘##R’,‘##T’,‘##a’,‘##b’,‘##c’,‘##d’,‘##e’,‘##f’,‘##g’,‘##h’,‘##i’,‘##k’,‘##l’,‘##m’,‘##n’,‘##o’,‘##p’,‘##r’,‘##s’,‘##t’,‘##u’,‘##v’,‘##w’,‘##x’,‘##y’,‘##z’,‘,’,‘-’,‘。’,‘B’,‘S’,‘T’,‘W’,‘a’,‘b’,‘c’,‘d’,‘e’,‘f’,‘h’,‘i’,‘l’,‘m’,‘n’,‘o’,‘p’,‘r’,‘s’,‘t’,‘u’,‘v’,‘w’,‘##ERT’

最终,为了计算一个词的子标记,我们从字符串的开头开始,尝试识别出我们词汇表中出现的最长可能序列。例如,单词BERT被拆分为以下子词:B##ERT

图片由作者提供

作为我们分词器词汇表的最后润色,我们引入了一组特殊标记,这些标记在 BERT 模型中具有特定功能,并作为语言学标记。通常,这些特殊标记包括:[[PAD], [UNK], [CLS], [SEP], [MASK]]

这里是每个特殊标记设计用途的简要说明:

  • [PAD]:用于平衡不同输入序列长度的填充标记。

  • [UNK]:表示“未知”,用于处理不在词汇表中的词。

  • [CLS]:即“分类”的缩写,这个标记在模型用于分类任务时会被加在输入的前面。

  • [SEP]:分隔符令牌,通常用于指示多句任务中一句话的结束和另一句话的开始。

  • [MASK]:在 BERT 模型的训练过程中使用,用于指示模型应预测缺失单词的位置。

就这样!你现在知道了 WordPiece,BERT 的分词器,如何将每个单词拆分成子词。接下来,让我们在我们的语料库上训练一个自定义的分词器。

训练 WordPiece

作为一名母语为希腊语的讲者,我对为希腊语训练 BERT 的 WordPiece 分词器的前景感到特别兴奋。如果你的母语不是英语,我强烈建议你也这么做。本地化这些强大的模型不仅有助于自然语言处理技术的多样化,还提供了将它们调整到你自己语言或方言细微差别的机会。

但如果你是英语讲者,可以考虑将你的分词器针对特定领域或行业进行优化。例如,你可以在法律文件上训练它,以捕捉法律术语的独特性。或者尝试让它理解 Python 代码?

在我的情况下,我需要找到并下载数据集。如今,获取所需的数据集比以往任何时候都要容易,感谢 Hugging Face Hub。我将使用greek_legal_code数据集:

from datasets import load_dataset

raw_datasets = load_dataset("greek_legal_code", "chapter")

接下来,让我们快速创建我们的训练语料库。为此,我们只使用1000train拆分的示例:

training_corpus = (
    raw_datasets["train"][i : i + 1000]["text"]
    for i in range(0, len(raw_datasets["train"]), 1000)
)

接下来,让我们了解一下预训练的 BERT 分词器:

from transformers import AutoTokenizer

old_tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")

为了比较前后效果,让我们看看原始 BERT 分词器如何对希腊语维基百科的机器学习进行分词:

example = ("Μηχανική μάθηση είναι υποπεδίο της επιστήμης των υπολογιστών,"
           " που αναπτύχθηκε από τη μελέτη της αναγνώρισης προτύπων και της"
           " υπολογιστικής θεωρίας μάθησης στην τεχνητή νοημοσύνη. Το 1959,"
           " ο Άρθουρ Σάμουελ ορίζει τη μηχανική μάθηση ως 'Πεδίο μελέτης που"
           " δίνει στους υπολογιστές την ικανότητα να μαθαίνουν, χωρίς να έχουν"
           " ρητά προγραμματιστεί'")

tokens = old_tokenizer.tokenize(example)
print(tokens); print(len(tokens))

结果如下所示。总共,我们得到 274 个不同的令牌:

['μ', '##η', '##χ', '##α', '##ν', '##ι', '##κ', '##η', 'μ', '##α', '##θ', '##η', '##σ', '##η', 'ε', '##ι', '##ν', '##α', '##ι', 'υ', '##π', '##ο', '##π', '##ε', '##δ', '##ι', '##ο', 'τ', '##ης', 'ε', '##π', '##ι', '##σ', '##τ', '##η', '##μ', '##ης', 'τ', '##ω', '##ν', 'υ', '##π', '##ο', '##λ', '##ο', '##γ', '##ι', '##σ', '##τ', '##ω', '##ν', ',', 'π', '##ου', 'α', '##ν', '##α', '##π', '##τ', '##υ', '##χ', '##θ', '##η', '##κ', '##ε', 'α', '##π', '##ο', 'τ', '##η', 'μ', '##ε', '##λ', '##ε', '##τ', '##η', 'τ', '##ης', 'α', '##ν', '##α', '##γ', '##ν', '##ω', '##ρ', '##ι', '##σ', '##ης', 'π', '##ρ', '##ο', '##τ', '##υ', '##π', '##ω', '##ν', 'κ', '##α', '##ι', 'τ', '##ης', 'υ', '##π', '##ο', '##λ', '##ο', '##γ', '##ι', '##σ', '##τ', '##ι', '##κ', '##ης', 'θ', '##ε', '##ω', '##ρ', '##ια', '##ς', 'μ', '##α', '##θ', '##η', '##σ', '##ης', 'σ', '##τ', '##η', '##ν', 'τ', '##ε', '##χ', '##ν', '##η', '##τ', '##η', 'ν', '##ο', '##η', '##μ', '##ο', '##σ', '##υ', '##ν', '##η', '.', 'τ', '##ο', '1959', ',', 'ο', 'α', '##ρ', '##θ', '##ου', '##ρ', 'σ', '##α', '##μ', '##ου', '##ε', '##λ', 'ο', '##ρ', '##ι', '##ζ', '##ε', '##ι', 'τ', '##η', 'μ', '##η', '##χ', '##α', '##ν', '##ι', '##κ', '##η', 'μ', '##α', '##θ', '##η', '##σ', '##η', 'ω', '##ς', "'", 'π', '##ε', '##δ', '##ι', '##ο', 'μ', '##ε', '##λ', '##ε', '##τ', '##ης', 'π', '##ου', 'δ', '##ι', '##ν', '##ε', '##ι', 'σ', '##τ', '##ου', '##ς', 'υ', '##π', '##ο', '##λ', '##ο', '##γ', '##ι', '##σ', '##τ', '##ε', '##ς', 'τ', '##η', '##ν', 'ι', '##κ', '##α', '##ν', '##ο', '##τ', '##η', '##τ', '##α', 'ν', '##α', 'μ', '##α', '##θ', '##α', '##ι', '##ν', '##ου', '##ν', ',', 'χ', '##ω', '##ρ', '##ι', '##ς', 'ν', '##α', 'ε', '##χ', '##ου', '##ν', 'ρ', '##η', '##τ', '##α', 'π', '##ρ', '##ο', '##γ', '##ρ', '##α', '##μ', '##μ', '##α', '##τ', '##ι', '##σ', '##τ', '##ε', '##ι', "'"]
274

现在,让我们训练分词器以更好地理解希腊语。我们将使用50000个单词的词汇表:

tokenizer = old_tokenizer.train_new_from_iterator(training_corpus, 50000)

最后,重复测试:

tokens = tokenizer.tokenize(example)
print(tokens); print(len(tokens))

结果非常惊人;我们将令牌的数量减少到了67,而且分词器似乎更好地理解了希腊语的细微差别:

['μηχανικη', 'μαθη', '##ση', 'ειναι', 'υποπε', '##διο', 'της', 'επιστημης', 'των', 'υπολογιστων', ',', 'που', 'αναπτυχ', '##θηκε', 'απο', 'τη', 'μελετη', 'της', 'αναγνωρισης', 'προτυπων', 'και', 'της', 'υπολογισ', '##τικης', 'θεωριας', 'μαθησης', 'στην', 'τεχνητη', 'νοη', '##μοσυνη', '.', 'το', '1959', ',', 'ο', 'αρθ', '##ουρ', 'σαμου', '##ελ', 'οριζει', 'τη', 'μηχανικη', 'μαθη', '##ση', 'ως', "'", 'πεδιο', 'μελετης', 'που', 'δινει', 'στους', 'υπολογιστες', 'την', 'ικανοτητα', 'να', 'μα', '##θαι', '##νου', '##ν', ',', 'χωρις', 'να', 'εχουν', 'ρητα', 'προγραμματισ', '##τει', "'"]
67

就这样!你现在已经完全准备好训练一个符合你自己语料库的分词器。但不要止步于此——可以随意尝试完全不同的算法!想要切换方向,探索另一种方法?你可以轻松加载 GPT 分词器,它使用的是字节对编码(BPE)算法,而不是 WordPiece。在微调和定制分词器方面,天空才是极限。

结论

这真是一段旅程!从深入了解分词的复杂细节,到构建我们自己专属的 WordPiece 分词器,我们已经覆盖了很多内容。

如果你跟随了整个过程,你现在不仅拥有理论知识,还有实际的经验来创建一个符合你需求的分词器。也许你会为你的母语适配它,或者为某个行业特定的应用,甚至是像源代码分析这样复杂的任务。

记住,我们在这里探讨的原则超越了任何单一语言或领域。它们是所有自然语言处理任务的基础。通过理解和掌握这些原则,你为在一个只会变得更加重要的领域中取得成功奠定了基础。

是时候为下一个激动人心的阶段做好准备了——为模型训练准备数据集。相信我,如果你觉得分词很吸引人,那么数据集准备将同样令人瞩目。这将是我们所组装的所有部分——分词器、特殊标记以及你的领域或语言特定的细节——汇聚在一起,为 BERT 模型的学习过程提供动力的地方。

敬请关注!

关于作者

我的名字是 Dimitris Poulopoulos,我是为 HPE 工作的机器学习工程师。我为欧洲委员会、国际货币基金组织、欧洲中央银行、宜家、Roblox 等主要客户设计和实施了 AI 和软件解决方案。

如果你有兴趣阅读更多关于机器学习、深度学习、数据科学和数据操作的文章,请关注我在 MediumLinkedIn 或者 Twitter 上的 @james2pl

所表达的观点仅代表我个人,并不代表我的雇主的观点或意见。

《终极 Ndarray 手册:掌握 Rust 的科学计算艺术》

原文:towardsdatascience.com/the-ultimate-ndarray-handbook-mastering-the-art-of-scientific-computing-with-rust-ef5ab767212a?source=collection_archive---------1-----------------------#2023-05-02

对 Rust 的不同内置数据结构进行概述,并深入探讨 Ndarray 库

Mahmoud HarmouchTowards Data Science Mahmoud Harmouch

·

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

--

照片由 Crissy Jarvis 提供,发布在 Unsplash

TL;DR

Rust 在全球编程语言中获得了巨大的受欢迎程度,这并非没有理由。此外,当讨论数据分析时,Rust 凭借其卓越的能力在同类中脱颖而出。丰富的库支持加上强大的工具使 Rust 成为许多专业人士处理复杂数据集的首选。此外,如果你希望使用 Rust 进行数据分析或其他相关任务,了解如何存储数据至关重要。

到文章结尾时,你将拥有一个坚实的基础,使你能够自信且轻松地开启 Rust 数据分析之旅。

注意:本文假设你对 Rust 的引用和借用检查器有一定了解。

为了这篇文章开发了名为 2-ndarray-tutorial.ipynb 的笔记本,可以在以下仓库中找到:

[## GitHub - wiseaidev/rust-data-analysis: 终极数据分析课程与 Rust。

这个仓库是一个 Jupyter 笔记本的集合,所有笔记本都由 Rust 内核提供支持。通过这些笔记本,你将会…

github.com](https://github.com/wiseaidev/rust-data-analysis?source=post_page-----ef5ab767212a--------------------------------)

目录(TOC)

∘ 这篇文章讲了什么?

∘ Rust 内置数据结构

∘ 1. 向量

∘ 2. 数组

∘ 3. 元组

∘ 4. 哈希集合

∘ 5. 哈希映射

∘ Ndarray 用于数据分析

∘ Ndarray 简介

∘ Ndarray 用例

∘ 初始占位符

∘ 多维数组

∘ Ndarray 数组操作

∘ 索引与切片

∘ 重塑

∘ 转置

∘ 交换轴

∘ 线性代数

∘ 结论

∘ 结束语

∘ 资源

这篇文章讲了什么?

AshniUnsplash 上的照片

本文的重点是一个用于数据分析的重要 Rust 库,即 **ndarray****ndarray** 使用户能够处理大型多维数组和矩阵,同时提供了广泛的数学操作选择。

但在我们深入探讨 **ndarray** 之前,让我们退一步,探索一下不同的 Rust 内置数据结构,以及为什么 Rust 是进行数据分析的绝佳语言。

Rust 内置数据结构

在本节中,我们将深入探讨构成这个出色的 Rust 编程语言基础的基本概念和强大工具。特别是,我们将涵盖 Rust 数据结构的基础知识,包括向量、元组、集合和哈希映射,深入理解它们的工作原理以及如何用它们来解决实际问题。

1. 向量

向量内存布局(作者提供的图片)

向量在一些编程语言如 Python 中被称为“列表”,它们无处不在;从简单的购物清单到更复杂的食谱说明,它们可以帮助我们跟踪事物并在需要时找到它们。在编程中,向量是一个重要的数据结构,广泛应用于许多不同的场景和形式。

创建向量

在 Rust 中,向量是基本的数据结构,你可以通过不同的方式创建它们。要创建一个空向量,你可以调用[**Vec::new()**](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.new)函数,并添加类型注释,因为 Rust 不知道你打算存储什么元素:

let v: Vec<i32> = Vec::new();

另外,你可以使用[**vec!**](https://doc.rust-lang.org/std/macro.vec.html#)宏来创建一个带有初始值的新向量:

let v = vec![1, 2, 3];

Rust 编译器能够推断向量的类型,通过初始值来消除手动指定的需要。创建向量后,你有多种选项来根据需求修改它。

访问向量元素

在 Rust 中,我们可以通过两种方式访问存储在向量中的值:要么通过索引,要么使用[**get**](https://doc.rust-lang.org/alloc/vec/struct.Vec.html#method.get)方法。让我们探索这两种方法,并附上代码示例!

首先,让我们考虑以下具有一些值的向量**v**

let v = vec!["apple", "banana", "cherry", "date"];

索引操作符**[]**可以用来从向量中检索特定的值。要访问第二个元素,我们来看以下示例:

// Get the second element
let second = &v[1];
println!("The second element is {}", second);

// Output:
// The second element is banana

在这里,我们使用索引**[]**来创建指向向量中第二个元素的引用**&**。当尝试访问一个不存在的索引时,Rust 编译器会触发终止/崩溃,并导致程序失败。为了避免这种情况,我们可以利用 get 函数,该函数返回一个**Option<&T>**而不是引用。它是如何工作的:

let v = vec![
    ("apple", 3),
    ("banana", 2),
    ("cherry", 5),
    ("date", 1),
];

// Get the quantity of cherries
let quantity = v.get(2).map(|(_, q)| q);

match quantity {
    Some(q) => println!("There are {} cherries", q),
    None => println!("Cherries not found"),
}

// Output:
// There are 5 cherries

通过调用**v.get(2)**,该方法将返回一个[**Option<&T>**](https://doc.rust-lang.org/std/option/index.html)类型,如果元素存在,将返回**Some**形式的正面结果,否则返回**None**形式的负面结果。我们可以通过实现match 表达式来有效处理这两种情况。利用这些技巧,你可以轻松访问向量中的元素!

遍历值

在 Rust 中,遍历向量是一个常见任务,可以通过两种方式执行:利用 不可变可变 引用。这种方法使我们能够对每个向量元素逐一操作。为了进一步理解,让我们通过一些代码示例来探索这两种方法!

let fruits = vec![("apple", 3), ("banana", 2), ("orange", 5), ("peach", 4)];
let mut sum = 0;
for (_, num) in &fruits {
    sum += num;
}
let avg = sum as f32 / fruits.len() as f32;
println!("The average of the second elements is {}", avg);

// Output:
// The average of the second elements is 3.5

在上述代码片段中,我们使用 **&** 运算符为向量中的每个项获取不可变引用。然后,我们通过利用 [**println**](https://doc.rust-lang.org/std/macro.println.html)**!** 宏来显示每个元素的值。

此外,[**iter()**](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.iter) 函数创建一个向量值的迭代器。使用这种技术,我们可以获得向量中每个值的可变引用,从而无缝地添加 10。以下代码演示了如何使用 **iter()** 方法有效地优化向量的迭代。

let mut values = vec![10, 20, 30, 40, 50];
for value in values.iter_mut() {
    *value += 10;
}
println!("The modified vector is {:?}", values);

// Output:
// The modified vector is [20, 30, 40, 50, 60]

我们可以通过利用 for 循环范围 有效地遍历向量元素的 部分。为了说明这个概念,考虑以下代码片段,展示了如何使用 for 循环获取 不可变引用,从给定的向量中仅获取三个元素,然后将其输出到终端。

let values = vec![10, 20, 30, 40, 50];
for value in &values[0..3] {
    println!("The value is {}", value);
}

// Output
// The value is 10
// The value is 20
// The value is 30

通过利用 Rust 的 [**enumerate()**](https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate) 函数,我们可以轻松遍历向量并获得其值及对应的索引。下面的代码片段展示了如何使用 **enumerate()** 方法获取 不可变引用,用于从 **i32** 值类型的向量中检索每个元素,同时打印它们各自的索引和值。

let values = vec![10, 20, 30, 40, 50];
for (index, value) in values.iter().enumerate() {
    println!("The value at index {} is {}", index, value);
}

// Output:
// The value at index 0 is 10
// The value at index 1 is 20
// The value at index 2 is 30
// The value at index 3 is 40
// The value at index 4 is 50

使用这些技巧,你可以轻松地 迭代操作 向量中的元素!

修改向量

Rust 的向量的多功能性在于其动态调整大小的能力,允许在运行时添加或删除元素。本节将探讨在 Rust 中修改和更新向量的不同方法。

添加元素

向量中添加元素(图像来源:作者)

我们可以使用 [**push**](https://doc.rust-lang.org/alloc/vec/struct.Vec.html#method.push) 方法向向量中添加元素,该方法将一个元素附加到向量的末尾:

let mut v = vec!["apple", "banana", "orange"];

v.push("mango");

println!("{:?}", v);

// Output:
// ["apple", "banana", "orange", "mango"]

给定的示例涉及创建一个包含三个元素的向量,然后通过 push 操作将 “mango” 附加到向量的末尾。最后,我们通过 **println!** 宏在终端上显示修改后的向量。或者,我们可以使用 insert 方法在特定索引处添加元素:

let mut v = vec!["apple", "mango", "banana", "orange"];

v.insert(v.len(), "mango");

println!("{:?}", v);

// Output:
// ["apple", "mango", "banana", "orange", "mango"]

上述示例涉及创建一个四元素的向量,然后通过使用[**insert**](https://doc.rust-lang.org/alloc/vec/struct.Vec.html#method.insert)方法在向量末尾插入“mango”。最后,我们通过**println!**宏在终端上显示修改后的向量。

修改元素

要修改字符串向量的元素,我们可以利用索引操作符**[]**来访问特定位置的元素,并用新值替换它。这种方法在修改给定向量中的值时非常有效。

let mut v = vec!["apple", "banana", "orange"];

v[1] = "pear";
v[2] = "grapefruit";

println!("{:?}", v);

// Output:
// ["apple", "pear", "grapefruit"]

给定的示例涉及创建一个包含三个元素的向量**v**,然后将其第二个元素(位于索引 1)修改为**“pear”**,并将第三个元素(在索引 2)的值指定为**“grapefruit”**。最后,我们通过**println!**宏在终端上显示这个更新后的版本。

删除元素

从向量中删除元素(图像由作者提供)

我们可以使用[**pop()**](https://doc.rust-lang.org/alloc/vec/struct.Vec.html#method.pop)方法从向量中删除一个元素,该方法会删除并返回向量中的最后一个元素:

let mut v = vec!["apple", "banana", "orange", "mango"];

let removed_element = v.pop();

println!("Removed element: {:?}", removed_element.unwrap());
println!("{:?}", v);

// Output:
// Removed element: "mango"
// ["apple", "banana", "orange"]

在上面的示例中,我们创建了一个四元素的向量**v**,然后使用**pop**方法删除了最后一个元素。该方法还向我们提供了被删除的组件作为输出。最后,我们使用**println!**宏以有序的方式在终端屏幕上显示更新后的向量和提取的元素。

我们还可以使用[**remove**](https://doc.rust-lang.org/alloc/vec/struct.Vec.html#method.remove)方法来删除特定索引处的元素:

let mut v = vec!["apple", "banana", "orange", "mango"];

let removed_element = v.remove(2);

println!("Removed element: {}", removed_element);
println!("{:?}", v);

// Output
// Removed element: orange
// ["apple", "banana", "mango"]

要从 Rust 中的向量中删除所有元素,使用[**retain**](https://doc.rust-lang.org/alloc/vec/struct.Vec.html#method.retain)方法来保留所有符合某个条件的元素:

let mut v = vec!["A", "warm", "fall", "warm", "day"];
let elem = "warm"; // element to remove
v.retain(|x| *x != elem);
println!("{:?}", v);

// Output:
// ["A", "fall", "day"]

连接两个向量

要连接两个字符串向量,我们可以使用**extend**方法,该方法将一个迭代器作为参数,并将其所有元素附加到向量中:

let mut v1 = vec!["apple", "banana"];
let mut v2 = vec!["orange", "mango"];

v1.extend(v2);

println!("{:?}", v1);

// Output:
// ["apple", "banana", "orange", "mango"]

在上面的示例中,我们首先创建了两个向量**v1****v2**,然后通过在**v1**上调用 extend 方法并传递**v2**作为参数来连接它们。

过滤与映射元素

我们可以使用[**iter**](https://doc.rust-lang.org/core/iter/index.html)[**filter**](https://doc.rust-lang.org/core/iter/struct.Filter.html)[**map**](https://doc.rust-lang.org/core/iter/struct.Map.html)方法来过滤和映射 Rust 中的向量元素。

过滤元素

我们可以通过结合使用**iter****filter**方法有效地过滤向量元素。为了说明这一点,让我们考虑如何使用以下示例从整数集合中过滤出所有偶数:

let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let odd_numbers: Vec<i32> = v.iter().filter(|x| *x % 2 != 0).map(|x| *x).collect();
println!("{:?}", odd_numbers);

// Output:
// [1, 3, 5, 7, 9]

在上述示例中,我们首先创建了一个包含十个元素的向量**v**,然后使用**iter****filter**方法创建一个新的向量**odd_numbers**,其中仅包含v中的奇数。最后,我们使用**println!**宏将新向量打印到终端。

映射元素

映射向量中的元素,我们可以将**iter****map**方法一起使用。例如,将字符串向量转换为大写:

let v = vec!["hello", "world", "rust"];
let uppercase_strings: Vec<String> = v.iter().map(|x| x.to_uppercase()).collect();
println!("{:?}", uppercase_strings);

// Output
// ["HELLO", "WORLD", "RUST"]

在上述示例中,我们首先创建了一个包含三个元素的向量**v**,然后使用**iter****map**方法创建一个新的向量**uppercase_strings**,该向量包含了v中元素的大写版本。最后,我们使用**println!**宏将新向量打印到控制台。

长度

要计算长度,我们可以使用 [**len**](https://doc.rust-lang.org/core/iter/trait.ExactSizeIterator.html#method.len) 方法:

let v = vec!["hello", "world", "rust"];
println!("Size: {}", v.len());

// Output
// Size: 3

检查元素是否存在

我们可以使用 [**contains**](https://doc.rust-lang.org/core/primitive.str.html#method.contains) 来检查一个向量是否包含特定元素:

let v = vec!["hello", "world", "rust"];
println!("{}", v.contains(&"hello"));

// Output
// true

注意该方法需要一个借用副本,因此参数中有**&**。如果你忘记了,编译器会提示你添加这个符号。

反转元素

我们可以使用 [**reverse**](https://doc.rust-lang.org/core/primitive.slice.html#method.reverse) 方法在 Rust 中反转一个向量。这个方法会原地修改向量,所以它不返回任何东西。

let mut v = vec![1, 2, 3, 4, 5];
v.reverse();
println!("{:?}", v);

// Output:
// [5, 4, 3, 2, 1]

在上述示例中,创建了一个包含五个元素的向量**v**,然后使用**reverse**方法原地改变这些元素的顺序。最后,我们在终端上显示反转后的向量以供观察。

最大值与最小值

通过利用 Rust 的**iter**函数以及**max****min**方法,可以轻松找到向量中的最高值和最低值。这种方法在简化这些操作方面非常有效。

let v = vec![1, 2, 3, 4, 5];
let max_element = *v.iter().max().unwrap();
let min_element = *v.iter().min().unwrap();
println!("Max element: {}", max_element);
println!("Min element: {}", min_element);

// Output
// Max element: 5
// Min element: 1

在上述示例中,我们初始化了一个包含五个元素的向量**v**。随后,使用**iter**方法创建一个迭代器,通过**max****min**方法来确定最大值和最小值。最后,使用**println!**在控制台上显示这两个结果。

现在你已经对使用和操作向量有了坚实的基础,让我们来看看另一个内置集合:数组。

2. 数组

Rust 数组内存布局(图片来源:作者)

使用数组是一种存储相同数据类型不同值的可行选项。与向量不同,数组中的每个元素必须具有一致的数据类型。与其他编程语言中的数组相比,它们是固定大小的集合,其中元素的数据类型相同。当你需要在上分配内存或知道它们的大小在运行时会保持不变时,这些集合具有优势。

创建数组

要创建数组,您可以使用方括号 **[]** 并用逗号分隔的值:

let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

您还可以明确指定数组中元素的数量和类型,如下所示:

let a: [i32; 5] = [1, 2, 3, 4, 5];

使用此语法,可以形成一个由 **i32** 值组成且长度为 5 的数组。为了将此数组中的所有元素设置为一个典型值,可以使用以下方法:

let zeros = [0; 5];

这会创建一个长度为 5 的数组,所有元素都初始化为 0。

访问元素

您可以使用带有元素索引的方括号访问数组的单个元素:

let numbers = [1, 2, 3, 4, 5];
println!("{}", numbers[2]);

// Output:
// 3

修改元素

由于数组具有固定的大小,您无法像向量那样推送或移除元素。但是,可以通过使用 **mut** 关键字使数组可变,从而修改其元素:

let mut numbers = [1, 2, 3, 4, 5];
numbers[1] = 10;
println!("{:?}", numbers);

// Output:
// [1, 10, 3, 4, 5]

迭代

要从数组中检索每个单独的元素,我们必须遍历它们,而不是依赖索引逐个访问。下面演示了有效检索并打印 **i32** 类型数组中每个值的 for 循环实现。

let seasons = ["Winter", "Spring", "Summer", "Fall"];
for season in seasons {
    println!("{season}");
}
// or
for index in 0..seasons.len() {
    println!("{}", seasons[index]);
}
// or
for season in seasons.iter() {
    println!("{}", season);
}

切片数组

您还可以使用切片创建包含原始数组子集的新数组:

let numbers = [1, 2, 3, 4, 5];
let slice = &numbers[1..4];
println!("{:?}", slice);

// Output:
// [2, 3, 4]

这将创建一个包含原始数组元素的新数组,其索引为 1、2 和 3。

总结一下,Rust 数组是多功能的数据结构,可以服务多种用途。其固定大小的特性使其在特定场景下比向量更为有效。当数组大小预先确定且不需要运行时修改时,使用数组是存储数据的理想选择。

3. 元组

元组是 Rust 中的一种复合类型,允许将多个具有不同类型的值组合成一个对象。就大小而言,元组是固定的,一旦声明就不能调整大小,与数组类似。

创建元组

在 Rust 中,创建元组是一项简单的任务。只需将值括在括号中,并用逗号分隔它们。元组中的每个位置都有其类型,这些类型可能彼此不同,不会对其类型的一致性施加任何约束。

let person = ("Mahmoud", 22, true, 6.6);

在创建元组时,可以加入可选的类型注解。下面的示例中展示了这一点:

let person: (&str, i32, bool, f64) = ("Mahmoud", 22, false, 6.6);

更新元组

使用 **mut** 关键字,您可以将元组转换为可变形式并修改其内容。这允许通过引用后跟其各自 索引 值的 点符号 修改元组中的特定元素:

let mut person = ("Mahmoud", 22, true);

您可以通过使用点符号和相应的元素索引来修改其元素。

person.1 = 21;

元组的解构

从元组中提取不同组件并将它们分配给单独变量的过程称为解构,这在以下示例中进行了演示。

let (name, age, is_male) = ("Mahmoud", 22, true);
println!("Name: {}, Age: {}, Gender: {}", name, age, if is_male { "Male" } else { "Female" });

// Output
// Name: Mahmoud, Age: 22, Gender: Male

我们还可以在解构时忽略元组中的某些元素:

let (_, _, _, height) = ("Mahmoud", 22, false, 6.6);
println!("Height: {}", height);

// Output
// Height: 6.6

此外,我们可以使用索引访问元组中的特定元素:

let person = ("Mahmoud", 3, true, 6.0);
println!("Experience: {}", person.1);

// Output
// Experience: 3

总结一下,元组是在 Rust 中将不同类型的值组合成一个对象的强大方式。它们是不可变的并且大小固定,但可以被设为可变以修改其内容。你还可以解构元组以访问其元素。凭借这些特性,元组是处理 Rust 数据的多功能工具!

4. 哈希集合

如果你对 Python 比较熟悉,集合可能已经是一个熟悉的概念。这些集合由不同的元素组成,不重视顺序。在 Rust 编程语言中,哈希集合B 树集合代表这些唯一的组;然而,前者在实践中更为常用。

创建集合

在 Rust 中创建一个哈希集合非常简单,只需从标准库中导入并调用**new**方法或相关函数:

use std::collections::HashSet;
let mut my_set: HashSet<i32> = HashSet::new();

你也可以从元素的向量中创建一个集合:

let my_vector = vec![1, 2, 3, 4];
let my_set: HashSet<i32> = my_vector.into_iter().collect();

你甚至可以从数组初始化它:

let a = HashSet::from([1, 2, 3]);

更新集合

添加元素

向哈希集合中添加元素很简单,只需使用 [**insert**](https://doc.rust-lang.org/std/collections/hash_set/struct.HashSet.html#method.insert) 方法:

let mut my_set: HashSet<i32> = HashSet::new();
my_set.insert(1);
my_set.insert(2);
my_set.insert(3);

移除元素

从哈希集合中移除元素可以使用 [**remove**](https://doc.rust-lang.org/std/collections/hash_set/struct.HashSet.html#method.remove) 方法:

let mut my_set = HashSet::from([1, 2, 3, 4]);
my_set.remove(&2); // removes 2 from the set

遍历集合

你可以使用 for 循环轻松地遍历哈希集合:

let my_set = HashSet::from([1, 2, 3]);

for element in &my_set {
    println!("{}", element);
}

// Output(not ordered):
// 1
// 3
// 2

集合操作

不同的集合操作(图像由作者提供)

Rust 的哈希集合提供了一系列集合操作,包括 [**difference**](https://doc.rust-lang.org/std/collections/hash_set/struct.Difference.html)[**intersection**](https://doc.rust-lang.org/std/collections/hash_set/struct.Intersection.html)[**union**](https://doc.rust-lang.org/std/collections/hash_set/struct.Union.html) 函数。这些功能使我们能够在哈希集合上执行集合运算,使其成为存储唯一数据的宝贵资源。为说明这一点,我们来看以下示例:

use std::collections::HashSet;

let set_a = HashSet::from([1, 2, 3]);
let set_b = HashSet::from([4, 2, 3, 4]);

// elements in set_a that are not in set_b
let difference_set = set_a.difference(&set_b);

// elements common to both set_a and set_b
let intersection = set_a.intersection(&set_b);

// elements in either set_a or set_b
let union_set = set_a.union(&set_b);

for element in difference_set {
    println!("{}", element);
}

// Output:
// 1

for element in intersection {
    println!("{}", element);
}

// Output:
// 3
// 2

for element in union_set {
    println!("{}", element); 
}

// Output:
// 3
// 2
// 1
// 4

本质上,哈希集合是每个 Rust 开发者都必须熟悉的不可或缺的资产。它们具有显著的效率,并提供了大量的集合运算操作。通过提供的示例,你现在应该能够将哈希集合整合到你的个人 Rust 项目中。

欲了解更多信息,请参阅 官方文档

5. 哈希映射

哈希映射(图像由作者提供)

哈希映射是一种包含键值对的集合类型,通过利用键而非索引来快速有效地访问数据。Rust 通过 [**std::collections::HashMap**](https://doc.rust-lang.org/std/collections/struct.HashMap.html) 模块声明哈希映射,它是一种无序结构,速度非常快。让我们看看如何在 Rust 中创建、更新、访问和遍历哈希映射。

创建哈希映射

你可以通过多种方式在 Rust 中初始化哈希映射,其中一种方法是使用哈希映射结构体的[**new**](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.new)方法。

use std::collections::HashMap;

let mut employees_map = HashMap::new();

// Insert elements to the HashMap
employees_map.insert("Mahmoud", 1);
employees_map.insert("Ferris", 2);

// Print the HashMap
println!("{:?}", employees_map);

// Output:
// {"Mahmoud": 1, "Ferris": 2}

在给定的示例中,我们引入了一个新的哈希映射(Hash Map),命名为**employees_map**。随后,利用[**insert**](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.insert)函数,我们向这个哈希映射中添加元素。最后,通过应用**println!**宏并使用**{:?}**格式化,我们展示了我们创建的 HashMap 的调试模式表示。另一种初始化哈希映射的方法是使用[**HashMap::from**](https://doc.rust-lang.org/std/collections/struct.HashMap.html#impl-From%3C%5B(K,+V);+N%5D%3E-for-HashMap%3CK,+V,+RandomState%3E)方法。

use std::collections::HashMap;

let employees_map: HashMap<i32, &str> = HashMap::from([
    (1, "Mahmoud"),
    (2, "Ferris"),
]);

更新哈希映射

添加元素

正如我们在之前的示例中所见,我们可以使用[**insert**](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.insert)方法向哈希映射中添加元素(键值对)。例如:

use std::collections::HashMap;

let mut employees_map = HashMap::new();

// Insert elements to the HashMap
employees_map.insert("Mahmoud", 1);
employees_map.insert("Ferris", 2);

// Print the HashMap
println!("{:?}", employees_map);

// Output:
// {"Mahmoud": 1, "Ferris": 2}

移除元素

我们可以使用[**remove**](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.remove)方法从哈希映射中移除一个元素(键值对)。例如:

use std::collections::HashMap;

let mut employees_map: HashMap<i32, String> = HashMap::new();

// insert elements to hashmap
employees_map.insert(1, String::from("Mahmoud"));

// remove elements from hashmap
employees_map.remove(&1);

更新元素

我们可以通过使用[**insert**](https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.insert)方法来更新哈希映射的元素。例如:

let mut employees_map: HashMap<i32, String> = HashMap::new();

// insert elements to hashmap
employees_map.insert(1, String::from("Mahmoud"));

// update the value of the element with key 1
employees_map.insert(1, String::from("Ferris"));
println!("{:?}", employees_map);

// Output:
// {1: "Ferris"}

访问值

类似于 Python,我们可以使用**get**来从给定的哈希映射中访问一个值。例如:

use std::collections::HashMap;

let employees_map: HashMap<i32, &str> = HashMap::from([
    (1, "Mahmoud"),
    (2, "Ferris"),
]);

let first_employee = employees_map.get(&1);

遍历哈希映射

use std::collections::HashMap;

fn main() {
    let mut employees_map: HashMap<i32, String> = HashMap::new();

    employees_map.insert(1, String::from("Mahmoud"));
    employees_map.insert(2, String::from("Ferris"));

    // loop and print values of hashmap using values() method
    for employee in employees_map.values() {
        println!("{}", employee)
    }

    // print the length of hashmap using len() method
    println!("Length of employees_map = {}", employees_map.len());
}

// Output:
// Ferris
// Mahmoud
// Length of employees_map = 2

本质上,Rust 的哈希映射是一种强大的数据结构,它通过键值对有效地管理和排列数据。它们提供快速访问数据的能力,并且经常用于如计数出现次数记忆化缓存等任务。得益于 Rust 的集成哈希映射实现以及其广泛的技术组合,使用哈希映射是一个简单而不复杂的过程。

欲了解更多信息,你可以参考官方文档的这一页面

当我们结束这一部分时,让我们回顾一下我们进入 Rust 内置数据结构广阔世界的旅程。我们的探索涉及了一些基本组件,如向量、数组、元组和哈希映射——这些都是任何熟练程序员在构建强健程序过程中不可或缺的元素。

通过我们对创建和访问数据结构的掌握,以及轻松操控它们的能力,我们获得了有关其定义特性和细微差别的宝贵见解。凭借这些知识,你将能够编写出高效且能够有效实现预期结果的 Rust 代码。

在牢固掌握了 Rust 内置数据结构的基本概念后,我们现在将其与本文后半部分探讨的 Ndarray 结合起来。这个出色的库因其在 Rust 中进行数值计算的能力而闻名。它具有类似于向量的数组对象,但具有增强的能力,能够无缝地执行数学运算。

Ndarray 在数据分析中的应用

不同数组的维度(图片作者提供)

在接下来的章节中,我们将深入探讨 [**ndarray**](https://docs.rs/ndarray/latest/ndarray/):一个强大的 Rust 库,能够轻松进行数值计算和数据操作。凭借其多种处理数组和矩阵中数值数据的方法,它是任何数据分析工具包中的重要资产。在接下来的章节中,我们将从零开始讲解如何使用 **ndarray**,包括如何处理数组和矩阵结构,以及如何轻松地对其进行数学运算。我们还将探索高级概念,如索引和切片,这些概念灵活地促进了对大数据集的高效处理。

通过在这些章节中的示例和实践练习,你可以掌握如何有效地利用 ndarray 数组来完成你的独特分析任务!

Ndarray 介绍

[**ArrayBase**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html) 结构提供了一种重要的数据结构,恰如其名为 n 维数组,有效地存储和管理大量的数据。这包括整数或浮点值。使用 **ndarray** 数组相较于 Rust 原生的 数组元组 结构有多种好处:它更高效且用户友好。

Ndarray 使用案例

以下是 **ndarray** 在数据分析中的一些实际应用案例:

  • 数据清洗与预处理:Ndarray 提供了强大的数据清洗与预处理功能,包括过滤缺失值、转换各种数据类型以及缩放数据集的能力。假设你有一组记录中存在缺口;ndarray 的 nan(非数值)可以有效地表示这些缺失的条目。利用如 [**填充**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.fill) 这样的函数,你可以轻松地管理这些不完整的信息,毫不费力。

  • 数据可视化:Ndarray 数组是数据存储的可靠选择,有助于可视化。**ndarray** 数组的多功能性使得它们可以与 [**Plotters**](https://plotters-rs.github.io/home/) 库配合使用,以实现可视化目的。例如,通过使用 Ndarrays 生成一个包含随机数的数组,我们可以通过 Plotters 的绘图能力将其以直方图的形式绘制出来。

  • 描述性统计:Ndarray 提供了一系列强大的方法用于对数组进行描述性统计,包括计算均值、中位数、众数、方差和标准差。这些函数在分析数据时非常宝贵,因为它们提供了关键指标的快速概览。例如,通过利用 ndarray 的 [**mean**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.mean) 函数,我们可以轻松计算数据集中的平均值。

  • 机器学习:Ndarray 是机器学习中的一个关键组件,提供了对大数据集的快速而有效的操作。数值数据通常必须以数组形式表示以便与这些算法配合使用,使得**ndarray**成为一个理想的解决方案,因为它使用方便且高效。借助这个工具,我们可以轻松生成特征和标签数组,这对任何机器学习算法的成功至关重要。

  • 线性代数:Ndarray 提供了许多强大的方法来执行线性代数操作,如矩阵求逆、乘法和分解。这些功能在分析表示为矩阵或向量的数据时非常方便。例如,[**dot**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.dot)函数可以让我们轻松对两个数组进行矩阵乘法。

初始占位符

Ndarray 提供了多种生成和初始化数组的函数,被称为初始占位符或数组创建函数。这些强大的工具使我们能够创建具有特定形状和数据类型的自定义数组,并填充预定或随机值。以下是**ndarray**库中一些常用的初始占位符函数示例:

  1. [**ndarray::Array::<type, _>::zeros(shape.f())**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.zeros):此函数创建一个填充了 0 的数组。**shape** 参数指定数组的维度,**type** 参数指定数组元素的数据类型。**f** 函数将数组从行优先转换为列优先。

  2. [**ndarray::Array<::type, _>::ones(shape.f())**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.ones):此函数创建一个填充了 1 的数组。**type****f** 的效果与 **ndarray::Array::zeros** 相同。

  3. [**ndarray::Array::<type, _>::range(start, end, step)**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.range):此函数创建一个范围内值的数组。start 参数指定范围的起点,end 参数指定范围的终点(不包括)。step 参数指定值之间的步长。**type** 参数指定数组元素的数据类型。

  4. [**ndarray::Array::<type, _>::linspace(start, end, n)**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.linspace): 这个函数创建一个数组,其中的值在**start****end**之间均匀分布。参数**n**指定数组中的值的数量,而end参数指定是否包含停止值。参数**type**指定数组元素的数据类型。

  5. [**ndarray::Array::<type, _>::fill(value)**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.fill): 这个函数用指定的值填充数组。参数**value**指定要填充数组的值。

  6. [**ndarray::Array::<type, _>::eye(shape.f())**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.eye): 这个函数创建一个方阵的单位矩阵,对角线上为 1,其他位置为 0。参数**n**指定行和列的数量。参数**type****f**函数与**ndarray::Array::zeros**的意义相同。

  7. [**ndarray::Array<type, _>::random(shape.f(), distribution_function)**](https://crates.io/crates/ndarray-rand): 这个函数使用给定的分布函数创建一个具有随机值的数组。参数**shape**指定数组的维度。

这些初始的占位函数对于在**ndarray**中生成和初始化数组非常有价值。它们提供了一种无需繁琐操作的方法来创建不同形状和数据类型的集合,允许用户指定特定或随机的值。以下是一个展示**ndarray**中各种占位符的简单 Rust 程序示例。

use ndarray::{Array, ShapeBuilder};
use ndarray_rand::RandomExt;
use ndarray_rand::rand_distr::Uniform;

// Zeros

let zeros = Array::<f64, _>::zeros((1, 4).f());
println!("{:?}", zeros);

// Output:
// [[0.0, 0.0, 0.0, 0.0]], shape=[1, 4], strides=[1, 1], layout=CFcf (0xf), const ndim=2

// Ones

let ones = Array::<f64, _>::ones((1, 4));
println!("{:?}", ones); 

// Output:
// [[1.0, 1.0, 1.0, 1.0]], shape=[1, 4], strides=[4, 1], layout=CFcf (0xf), const ndim=2

// Range

let range = Array::<f64, _>::range(0., 5., 1.);
println!("{:?}", range); 

// Output:
// [0.0, 1.0, 2.0, 3.0, 4.0], shape=[5], strides=[1], layout=CFcf (0xf), const ndim=1

// Linspace

let linspace = Array::<f64, _>::linspace(0., 5., 5);
println!("{:?}", linspace); 

// Output:
// [0.0, 1.25, 2.5, 3.75, 5.0], shape=[5], strides=[1], layout=CFcf (0xf), const ndim=1

// Fill

let mut ones = Array::<f64, _>::ones((1, 4));
ones.fill(0.);
println!("{:?}", ones); 

// Output:
// [[0.0, 0.0, 0.0, 0.0]], shape=[1, 4], strides=[4, 1], layout=CFcf (0xf), const ndim=2

// Eye

let eye = Array::<f64, _>::eye(4);
println!("{:?}", eye); 

// Output:
// [[1.0, 0.0, 0.0, 0.0],
// [0.0, 1.0, 0.0, 0.0],
// [0.0, 0.0, 1.0, 0.0],
// [0.0, 0.0, 0.0, 1.0]], shape=[4, 4], strides=[4, 1], layout=Cc (0x5), const ndim=2

// Random

let random = Array::random((2, 5), Uniform::new(0., 10.));
println!("{:?}", random);

// Output:
// [[9.375493735188611, 4.088737328406999, 9.778579742815943, 0.5225866490310649, 1.518053969762827],
//  [9.860829919571666, 2.9473768443117, 7.768332993584486, 7.163926861520167, 9.814750664983297]], shape=[2, 5], strides=[5, 1], layout=Cc (0x5), const ndim=2

多维数组

Ndarray 可以构建具有多个维度的数组,例如 2D 矩阵和 3D 矩阵。我们可以轻松使用[**from_vec**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.from_vec)函数与向量的向量,或者使用[**array!**](https://docs.rs/ndarray/latest/ndarray/macro.array.html)宏来生成复杂的数据结构。例如,让我们看一个展示如何使用 ndarray 在各种维度上创建数组的示例程序。

use ndarray::{array, Array, Array2, Array3, ShapeBuilder};

// 1D array
let array_d1 = Array::from_vec(vec![1., 2., 3., 4.]);
println!("{:?}", array_d1);

// Output:
// [1.0, 2.0, 3.0, 4.0], shape=[4], strides=[1], layout=CFcf (0xf), const ndim=1

// or 

let array_d11 = Array::from_shape_vec((1, 4), vec![1., 2., 3., 4.]);
println!("{:?}", array_d11.unwrap());

// Output:
// [[1.0, 2.0, 3.0, 4.0]], shape=[1, 4], strides=[4, 1], layout=CFcf (0xf), const ndim=2

// 2D array

let array_d2 = array![
    [-1.01,  0.86, -4.60,  3.31, -4.81],
    [ 3.98,  0.53, -7.04,  5.29,  3.55],
    [ 3.30,  8.26, -3.89,  8.20, -1.51],
    [ 4.43,  4.96, -7.66, -7.33,  6.18],
    [ 7.31, -6.43, -6.16,  2.47,  5.58],
];

// or

let array_d2 = Array::from_shape_vec((2, 2), vec![1., 2., 3., 4.]);
println!("{:?}", array_d2.unwrap());

// Output:
// [[1.0, 2.0],
// [3.0, 4.0]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2

// or

let mut data = vec![1., 2., 3., 4.];
let array_d21 = Array2::from_shape_vec((2, 2), data);

// 3D array

let mut data = vec![1., 2., 3., 4.];
let array_d3 = Array3::from_shape_vec((2, 2, 1), data);
println!("{:?}", array_d3);

// Output:
// [[[1.0],
//  [2.0]],
//  [[3.0],
//  [4.0]]], shape=[2, 2, 1], strides=[2, 1, 1], layout=Cc (0x5), const ndim=3

Ndarray 数组操作

在本节中,我们将深入探讨ndarray数组的多种技术,例如索引切片重塑

索引 & 切片

数组切片(图片由作者提供)

Ndarray 通过 索引切片 功能提供了令人印象深刻的能力,使我们能够访问和修改数组中的单个元素或子数组。与 Python 列表类似,在 ndarray 中,索引涉及使用索引值从数组中检索特定元素。作为这种功能的演示,请考虑使用以下代码访问数组的第二个元素:

let array_d1 = Array::from_vec(vec![1., 2., 3., 4.]);
array_d1[1]

多维数组也支持索引和切片,而不仅仅是 1D 数组。为了说明这一点,请考虑以下代码,它通过指定行和列坐标从 2D 数组中检索一个元素:

let zeros = Array2::<f64>::zeros((2, 4).f());
array_d1[1, 1]

切片是一种强大的技术,可以从数组中提取子数组。切片的语法类似于索引,但它使用点 **..** 来指定切片的开始和结束点。为了展示这种方法,请考虑以下代码,它生成一个只包含前三个元素的新数组:

let array_d1 = Array::<i32, _>::from_vec(vec![1, 2, 3, 4]);
let slice = array_d1.slice(s![0..3]);

重塑

重塑是一种改变数组配置或排列的技术,同时保留其数据。ndarray 库提供了一系列强大的函数来重塑数组,如 [**flatten**](https://docs.rs/ndarray-linalg/latest/ndarray_linalg/convert/fn.flatten.html) 和最显著的 [**reshape**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.reshape)

重塑

数组重塑(图像由作者提供)

使用 [**reshape**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.reshape) 函数,这个函数只能应用于 [**ArcArray**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#arcarray),你可以通过定义新配置的行数和列数来修改数组的形状。例如,以下代码片段将一个具有四个元素的 1D 数组转换为一个包含两行两列的 2D 数组:

use ndarray::{rcarr1};
let array_d1 = rcarr1(&[1., 2., 3., 4.]); // another way to create a 1D array
let array_d2 = array_d1.reshape((2, 2));

展平

数组展平(图像由作者提供)

[**ndarray_linalg::convert::flatten**](https://docs.rs/ndarray-linalg/latest/ndarray_linalg/convert/fn.flatten.html) 函数生成一个包含源数组所有元素的 1D 数组。然而,它会生成一个新的数据副本,而不是改变原始集合。这种方法确保了两个数组之间的区别,避免了由于数组重叠而引发的潜在混淆或错误。

use ndarray::{array, Array2};
use ndarray_linalg::convert::flatten;

let array_d2: Array2<f64> = array![[3., 2.], [2., -2.]];
let array_flatten = flatten(array_d2);
print!("{:?}", array_flatten);

// Output:
// [3.0, 2.0, 2.0, -2.0], shape=[4], strides=[1], layout=CFcf (0xf), const ndim=1

**ndarray** 不仅提供了重塑数组的功能,还提供了一系列其他数组操作函数。这些包括转置和交换轴等。

转置

数组转置(图像由作者提供)

通过使用 [**t**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.t) 函数,可以生成一个其轴已转置的新数组。为了说明这一点,让我们考虑以下代码片段,该片段演示了如何转置一个二维数组:

let array_d2 = Array::from_shape_vec((2, 2), vec![1., 2., 3., 4.]);
println!("{:?}", array_d2.unwrap());

// Output
// [[1.0, 2.0],
//  [3.0, 4.0]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2)

let binding = array_d2.expect("Expect 2d matrix");

let array_d2t = binding.t();
println!("{:?}", array_d2t);

// Output
// [[1.0, 3.0],
//  [2.0, 4.0]], shape=[2, 2], strides=[1, 2], layout=Ff (0xa), const ndim=2

交换轴

**ndarray** 中交换轴涉及交换数组中的行和列。这可以通过使用先前讨论的 **t** 方法或使用 ndarray 的 [**swap_axes**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.swap_axes) 方法来完成。交换轴是进行多维数组数据分析时的一个关键方面。

需要注意的是,轴是指多维数组中的每一个维度;例如,1D 数组只有一个轴,而 2D 数组具有两个轴——即行和列。类似地,3D 数组具有三个不同的轴:高度、宽度和深度——从零开始,直到添加额外的轴。

要使用 Rust 的 **ndarray** 库通过其内置方法如 **swap_axes** 执行这样的交换,你只需提供两个参数,表示要根据它们在不同维度平面上的位置进行交换的具体对。

let array_d2 = Array::from_shape_vec((2, 2), vec![1., 2., 3., 4.]);
println!("{:?}", array_d2.unwrap());

// Output:
// [[1.0, 2.0],
//  [3.0, 4.0]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2

let mut binding = array_d2.expect("Expect 2d matrix");
binding.swap_axes(0, 1);
println!("{:?}", binding);

// Output:
// [[1.0, 3.0],
//  [2.0, 4.0]], shape=[2, 2], strides=[1, 2], layout=Ff (0xa), const ndim=2

线性代数

Ndarray 是一个功能丰富的 Rust 库,用于数值计算和数据处理,通过一个名为 [**ndarray-linalg**](https://crates.io/crates/ndarray-linalg) 的独立 crate 提供卓越的线性代数支持。本节将深入探讨 **ndarray** 在进行线性代数方面提供的多种函数,并展示如何有效地利用这些函数来轻松促进数据分析任务。

  • 矩阵乘法:矩阵乘法的过程可以通过 [**ArrayBase.dot**](https://docs.rs/ndarray/latest/ndarray/struct.ArrayBase.html#method.dot) 函数执行,该函数有效地计算两个矩阵之间的点积。为了进一步说明这个概念,我们将利用它来确定将矩阵 **a****b** 相乘的结果,并将该结果存储在一个名为 **c** 的新矩阵中。
extern crate blas_src;
use ndarray::{array, Array2};

let a: Array2<f64> = array![[3., 2.], [2., -2.]];
let b: Array2<f64> = array![[3., 2.], [2., -2.]];
let c = a.dot(&b);
print!("{:?}", c);

// Output
// [[13.0, 2.0],
//  [2.0, 8.0]], shape=[2, 2], strides=[2, 1], layout=Cc (0x5), const ndim=2
  • 逆运算:在处理矩阵时,另一个重要操作是通过 [**ndarray_linalg::solve::Inverse.inv**](https://docs.rs/ndarray-linalg/latest/ndarray_linalg/solve/trait.Inverse.html#tymethod.inv) 函数来实现的,该函数计算任何给定矩阵的逆!例如,假设你想要对矩阵 **array_d2** 进行逆运算,调用其 **inv** 方法,并使用 **match** 语句来处理结果。
use ndarray::Array;
use ndarray_linalg::solve::Inverse;
use std::result::Result::{Err, Ok};

let array_d2 = Array::from_shape_vec((2, 2), vec![1., 2., 2., 1.]);

match array_d2.expect("Matrix must be square & symetric!").inv() {
    Ok(inv) => {
        println!("The inverse of m1 is: {}", inv);
    }
    Err(err) => {
        println!("{err}");
    }
}

// Output:
// The inverse of m1 is: [[-0.3333333333333333, 0.6666666666666666],
//  [0.6666666666666666, -0.3333333333333333]]
  • 特征分解[**use ndarray_linalg::Eig**](https://docs.rs/ndarray-linalg/latest/ndarray_linalg/eig/trait.Eig.html#tymethod.eig)函数通过计算矩阵的特征值和特征向量来展示这一点。在我们的例子中,我们确定了矩阵**array_d2**的这些值,并分别将它们保存在矩阵**eigs****vecs**中。
use ndarray::array;
use ndarray_linalg::Eig;
use std::result::Result::{Err, Ok};

let array_d2 = array![
    [-1.01,  0.86, -4.60],
    [ 3.98,  0.53, -7.04],
    [ 3.98,  0.53, -7.04],
];
match array_d2.eig() {
    Ok((eigs, vecs)) => {
        println!("Eigen values: {}", eigs);
        println!("Eigen vectors: {}", vecs);
    }
    Err(err) => {
        println!("{err}");
    }
}

// Output:
// Eigen values: [-3.759999999999999+2.706048780048134i, -3.759999999999999-2.706048780048134i, 0.00000000000000022759891370571733+0i]
// Eigen vectors: [[0.402993672209733+0.3965529218364603i, 0.402993672209733-0.3965529218364603i, 0.13921180485702092+0i],
//  [0.5832417510526318+0.00000000000000006939572631647882i, 0.5832417510526318-0.00000000000000006939572631647882i, 0.9784706726517249+0i],
//  [0.583241751052632+-0i, 0.583241751052632+0i, 0.15236540338584623+0i]]
  • 奇异值分解(SVD)[**ndarray_linalg::svd::SVD**](https://docs.rs/ndarray-linalg/latest/ndarray_linalg/svd/trait.SVD.html#tymethod.svd)函数的强大功能体现在它计算给定矩阵的左奇异向量、右奇异向量以及奇异值。为了说明这一点,我们对矩阵**array_d2**执行SVD,得到**u**保存左奇异向量,**v**存储奇异值,同时包含右奇异向量。
use ndarray::array;
use ndarray_linalg::svd::SVD;
use std::result::Result::{Err, Ok};

let array_d2 = array![
    [-1.01,  0.86, -4.60],
    [ 3.98,  0.53, -7.04],
    [ 3.98,  0.53, -7.04],
];
match array_d2.svd(true, true) {
    Ok((u, sigma, v)) => {
        println!("The left singular vectors are: {:?}", u.unwrap());
        println!("The right singular vectors are: {:?}", v.unwrap());
        println!("The sigma vector: {:?}", sigma);
    }
    Err(err) => {
        println!("{err}");
    }
}

// Output:
// The left singular vectors are: [[-0.3167331446091065, -0.948514688924756, 0.0],
//  [-0.6707011685937435, 0.22396415437963857, -0.7071067811865476],
//  [-0.6707011685937436, 0.2239641543796386, 0.7071067811865475]], shape=[3, 3], strides=[3, 1], layout=Cc (0x5), const ndim=2
// The right singular vectors are: [[-0.4168301381758514, -0.0816682352525302, 0.9053081990455173],
//  [0.8982609360852509, -0.18954008048752713, 0.39648688325344433],
//  [0.13921180485702067, 0.9784706726517249, 0.1523654033858462]], shape=[3, 3], strides=[3, 1], layout=Cc (0x5), const ndim=2
// The sigma vector: [12.040590078046721, 3.051178554664221, 9.490164740574465e-18], shape=[3], strides=[1], layout=CFcf (0xf), const ndim=1
  • 矩阵迹[**ndarray_linalg::trace::Trace**](https://docs.rs/ndarray-linalg/latest/ndarray_linalg/trace/trait.Trace.html#tymethod.trace)函数是一个强大的函数,用于计算任何矩阵对角线元素的总和。通过将此方法应用于矩阵**array_d2**,我们得到其迹结果,并匹配其值以进行进一步分析。这种简单而有效的技术展示了数学函数如何轻松而精确地提升数据处理能力。
use ndarray::array;
use ndarray_linalg::trace::Trace;
use std::result::Result::{Err, Ok};

let array_d2 = array![
    [-1.01,  0.86, -4.60],
    [ 3.98,  0.53, -7.04],
    [ 3.98,  0.53, -7.04],
];
match array_d2.trace() {
    Ok(value) => {
        println!("The sum of diagonal elements is: {:?}", value);
    }
    Err(err) => {
        println!("{err}");
    }
}

// Output:
// The sum of diagonal elements is: -7.52
use ndarray::array;
use ndarray_linalg::solve::Determinant;
use std::result::Result::{Err, Ok};

let array_d2 = array![
    [-1.01,  0.86, -4.60],
    [ 3.98,  0.53, -7.04],
    [ 3.98,  0.53, -7.04],
];
match array_d2.det() {
    Ok(value) => {
        println!("The determinant of this matrix is: {:?}", value);
    }
    Err(err) => {
        println!("{err}");
    }
}

// Output:
// The determinant of this matrix is: 2.822009292913204e-15
  • 求解线性方程[**ndarray_linalg::solve**](https://docs.rs/ndarray-linalg/latest/ndarray_linalg/solve/index.html)函数用于展示如何求解**ax = b**格式的线性方程组。在这个例子中,我们通过使用**a**作为常量数组来解决方程组**ax=b**,然后将结果存储在变量**x**中。
use ndarray::{array, Array1, Array2};
use ndarray_linalg::Solve;

// a11x0 + a12x1 = b1    --->    3 * x0 + 2 * x1 = 1
// a21x0 + a22x1 = b2    --->    2 * x0 - 2 * x1 = -2:
let a: Array2<f64> = array![[3., 2.], [2., -2.]];
let b: Array1<f64> = array![1., -2.];
let x = a.solve_into(b).unwrap();
print!("{:?}", x);

// Output:
// [-0.2, 0.8], shape=[2], strides=[1], layout=CFcf (0xf), const ndim=1

在文章的这一部分,我们深入探讨了如何使用**ndarray**中的多维数组。这些数组是各个科学计算领域中一个至关重要的组件。**array!**宏函数在**ndarray**中使得多维数组的创建和操作变得轻而易举,这使其成为数据管理中的一项宝贵工具。

此外,我们还了解了如何利用**ndarray**数组进行算术运算。这些类型的数组能够支持基本的算术功能,如加、减、乘、除。可以对单个元素或整个数组同时进行这些计算。

最后,我们深入探讨了 **ndarray** 及其在线性代数中的应用。这种动态工具提供了广泛的函数,能够无缝进行矩阵操作,包括点积、转置、逆矩阵以及行列式。这些基本数学工具对于解决在金融、工程和物理等各个领域遇到的复杂问题至关重要。

结论

在本文中,我们深入探讨了 Rust 中的基本数据结构,并演示了如何使用ndarray库执行各种算术操作。此外,还强调了 Rust 在线性代数方面的潜力,这是数据科学的一个关键组件。

这个长期连载表明,Rust 是一种具有显著优势和广泛能力的语言,能够无缝地构建数据科学项目。它在处理复杂数据集时表现出色,同时相对简单。那些希望在数据科学领域取得有前途职业的人应该毫无疑问地将 Rust 作为首选之一。

结尾说明

照片由 Kelly Sikkema 提供,刊登于 Unsplash

一如既往,我想花一点时间向所有投入时间和精力阅读本文并跟进的人表示衷心的感谢。与大家一起展示 Rust 及其生态系统的能力是非常愉快的体验。

对数据科学充满热情的我承诺,每周都会写至少一篇全面的相关文章。如果你对跟进我的工作感兴趣,可以考虑在各种社交媒体平台上与我联系,或直接联系我,如果有其他需要帮助的地方。

谢谢!

资源

[## GitHub - wiseaidev/rust-data-analysis: 最终的数据分析课程,使用 Rust。

这个库包含了一系列 Jupyter notebook,所有这些 notebook 都由 Rust 内核驱动。通过这些 notebook,你将会……

github.com](https://github.com/wiseaidev/rust-data-analysis?source=post_page-----ef5ab767212a--------------------------------) [## ndarray - Rust

展开描述 ndarray crate 提供了一个用于一般元素和数值的 n 维容器。在……

docs.rs](https://docs.rs/ndarray/latest/ndarray/?source=post_page-----ef5ab767212a--------------------------------) [## std - Rust

Rust 标准库是可移植 Rust 软件的基础,一套经过最小化和实战检验的共享库……

doc.rust-lang.org](https://doc.rust-lang.org/std/index.html?source=post_page-----ef5ab767212a--------------------------------) [## Rust By Example

Rust 是一种现代系统编程语言,注重安全性、速度和并发性。它实现这些目标的方式是…

Rust By Example [## Rust 编程语言

作者:Steve Klabnik 和 Carol Nichols,感谢 Rust 社区的贡献。本版本的文本假设你已…

Rust 编程语言

你 NLP 模型的终极预处理流程

原文:towardsdatascience.com/the-ultimate-preprocessing-pipeline-for-your-nlp-models-80afd92650fe

通过提供最佳可能的输入,充分发挥 NLP 机器学习模型的效果。

Rahulraj SinghTowards Data Science Rahulraj Singh

·发表在 Towards Data Science ·阅读时间 10 分钟·2023 年 5 月 8 日

--

照片由 Cyrus Crossan 提供,发布在 Unsplash 上。

如果你曾经参与过文本摘要项目,你可能会发现很难看到你期望的结果。你脑海中有一个关于算法如何工作的概念,以及它应该在文本摘要中标记哪些句子,但往往算法提供的结果是“不是很准确”的。更有趣的是关键词提取,因为从主题建模到向量化嵌入的各种算法都表现得很出色,但当输入一个段落时,它们给出的结果同样是“不是很准确”的,因为出现频率最高的词不一定是段落中最重要的词。

数据预处理和清洗的需求在很大程度上取决于你要解决的具体问题。我将尝试创建一个通用的流程,应该适用于所有 NLP 模型,但你总是需要调整这些步骤,以获得最佳的结果。在这个故事中,我将重点关注解决 主题建模、关键词提取和文本摘要 的 NLP 模型。

预处理流程 | 作者图像

上面的图片概述了我们将要遵循的预处理 NLP 管道的过程。上述四个步骤将在后面用代码解释,同时还附有一个 Jupyter 笔记本,实现了完整的管道。这条管道的想法是突出那些能够提升将在文本数据上使用的机器学习算法性能的步骤。这是在输入数据和模型训练之间的一个步骤。

1. 清理文本

结构化管道的第一步是清理输入文本数据,这可能包含几个步骤,具体取决于你试图构建的模型以及你期望的结果。机器学习算法(或者说几乎所有计算机算法,实际上是每一条计算机指令)都在数字上工作,这就是为什么为文本数据构建模型具有挑战性的原因。你实际上是在要求计算机学习和处理它从未见过的内容,因此需要多花一些功夫。

在下面的部分中,我提供了我们管道的第一个功能来执行文本数据的清理。清理功能包含许多操作部分,我在代码的注释中解释了它们。

要查看该功能的性能,下面是函数的输入和它生成的输出。

input_text = "This is an example from a key football match tweet text with \n
a <b>HTML tag</b>, an emoji 😃 expression happiness and 😍 with eyes too, we 
also have a link https://example.google.com, extra   w. h.  i.  t. e.   
spaces, accented characters like café, contractions we generally observe 
like don't and won't, some very special characters like @ and #, UPPERCASE 
letters, numericals like 123455, and general english stopwords like a, an, 
and the. Why not add punctuations like !, ?, and ,. too"

clean_text = clean_text(input_text)
print(clean_text)

----------------------------------------------------------------------------
example key football match tweet text html tag emoji grinning face big eyes 
expression happiness smiling face hearteyes eyes also link extra w h e spaces 
accented characters like cafe contractions generally observe like special 
characters like uppercase letters numericals like one hundred twentythree 
thousand  four hundred fiftyfive general english stopwords like add 
punctuations like

正如我们在输出中所观察到的,文本现在已经清除了所有的 HTML 标签,将表情符号转换为其文字形式,并纠正了任何标点符号和特殊字符。现在处理这些文本更为简便,在接下来的几步中,我们将进一步优化它。

2. 聚类以去除数据中的噪声和模板语言

我们预处理管道中的下一步可能是最重要且被低估的 NLP 工作流程活动。在下面的图示中,你可以看到下面的算法将要执行的粗略表示。

噪声去除过程的表示 | 作者提供的图片

那么,为什么去除噪声如此重要?因为这些文本隐藏在输入数据中,但不包含任何能够使学习算法更好的有用信息。像法律协议、新闻文章、政府合同等文件中包含了大量特定于组织的模板文本。假设你从法律合同中创建一个主题建模项目,以了解一系列合同中最重要的术语,而算法将管辖权解释和州法律定义作为合同中最重要的部分。法律合同包含了大量的法律和仲裁定义,但这些都是公开的,因此不特定于手头的合同,这使得这些预测实际上毫无用处。我们需要提取特定于该合同的信息。

从文本数据中去除模板语言是一项具有挑战性的工作,但却极为重要。由于这些数据都是干净的文本,因此很难检测和去除。但如果不去除,它会显著影响模型的学习过程。

现在让我们看看一个去除输入噪声和模板语言的函数的实现。这个算法使用聚类来找出重复出现的句子和词汇,并将其删除,假设重复超过阈值次数的内容可能是“噪声”。

下面,我们来看看这个函数在作为算法输入的新闻文章 [3] 上会产生什么结果。

去除模板代码的输出 | 图片由作者提供

从上面的输出图片中你可以看到,输入算法的文本长度为 7574,通过去除噪声和模板文本减少到 892。去除模板和噪声将我们的输入大小减少了近 88%, 这些原本会进入机器学习算法的垃圾数据。结果文本是更清晰、更有意义的摘要形式。通过去除噪声,我们让算法专注于重要的内容。

3. 词性标注

词性标注(POS),即为输入句子的每个词分配特定的词性标签的过程。它读取并理解词与句子中其他词的关系,并识别每个词的使用上下文。这些词性标签包括名词、动词、形容词、代词、介词、副词、连词和感叹词。这个过程至关重要,因为对于情感分析、文本分类、信息提取、机器翻译或任何其他形式的分析算法来说,了解词汇的使用上下文是非常重要的。上下文可以大大影响算法的自然语言理解(NLU)过程。

接下来,我们将进入预处理管道的最后一步,即将文本转换为向量嵌入,供机器学习算法使用。但是,在这之前,让我们讨论两个关键话题:词形还原和词干提取。

你需要词形还原(Lemmatization)还是词干提取(Stemming)?

词形还原和词干提取是自然语言处理工作流中常用的两种技术,帮助将屈折变化的词汇还原到其基础或根本形式。这些操作可能是最受质疑的,因此了解何时使用和何时不使用这些功能是值得的。词形还原和词干提取的核心思想是减少输入特征空间的维度。这有助于提高最终读取这些数据的机器学习模型的性能。

词干提取通过去除单词的后缀来将其还原为基本形式,而词形还原则使用词汇表和形态分析的形式将单词还原为基本形式。

由于其功能,词形还原通常比词干提取更准确,但计算开销较大。你特定用例中速度和准确性的权衡通常有助于决定使用哪种方法。

实施词形还原和词干提取时需要注意的一些重要点:

  1. 词形还原保留了输入文本的语义。针对情感分析的算法,如果模型需要单词的时态,可能会工作得很好。过去发生的事情可能与现在发生的相同事情具有不同的情感。

  2. 词干提取速度快,但准确性较低。在尝试进行文本分类的情况下,其中有成千上万的单词需要分类时,词干提取可能比词形还原更有效,主要是因为其速度。

  3. 像所有方法一样,可能值得在你的用例中探索两者,并比较模型的性能,以查看哪种效果最好。

  4. 此外,一些深度学习模型具备自动学习词汇表示的能力,这使得使用这些技术变得无关紧要。

4. 词形还原和向量嵌入

该预处理工作流程的最终步骤是应用词形还原和将单词转换为向量嵌入(因为要记住机器如何最好地处理数字而非单词吗?)。如前所述,词形还原可能根据你预期的结果和使用的机器学习技术,可能需要或不需要。为了更通用的方法,我在我的预处理管道中包含了它。

下面编写的函数将从接收到的词性标注输入中提取单词,对每个单词进行词形还原,然后将向量嵌入应用于词形还原后的单词。注释进一步解释了每个步骤。

该函数将返回一个形状为 (num_words, X) 的 numpy 数组,其中 ‘num_words’ 表示输入文本中的单词数量,‘X’ 是向量嵌入的大小。

向量嵌入的单词(单词的数值形式)应作为输入提供给任何机器学习算法。在使用深度学习模型或多个大型语言模型(LLMs)时,向量嵌入和词形还原可能不需要,因为算法已经足够成熟,可以构建自己对单词的表示。因此,如果你使用这些“自学习”算法中的任何一种,这可以是一个可选步骤。

完整管道实现

上述四个部分详细说明了我们预处理管道中的每个部分,下面附有用于运行预处理代码的工作笔记本。

[## ai-with-python-series/NLP 预处理管道

一系列旨在学习人工智能概念的 Python 教程。该系列教程从…

github.com

我想提醒您,这种实现并不是对每个 NLP 问题的一次性解决方案。构建一个稳健的预处理管道的核心思想是创建一个能够将最佳输入提供给机器学习算法的工作流程。上述步骤的顺序应该能解决约 70%的问题,通过针对特定用例的微调,您应该能够解决剩余部分。

结论

Brett Jordan 拍摄,来自 Unsplash

我希望这个故事为您下一个自然语言处理(NLP)项目提供了一个稳健的框架。虽然处理词语是 AI 领域中最具挑战性的任务之一,但数据的适当清理、预处理和准备可以确保机器的学习过程顺畅。正如正确实施本文提到的技术一样,遵循这里突出显示的预处理活动的顺序同样重要

每一步都依赖于前一步的成功。

如果输入到该算法中的数据包含大量停用词或 HTML 标签,模板删除逻辑将无法准确识别噪音。类似地,对整个文本进行词性标注、词形还原和向量化会使计算成本极高,并且很少像垃圾进垃圾出那样有效。因此,请尝试这个管道,并在您的 NLP 设置中享受更好的结果。

关于我

我已经使用 AI 构建产品和解决方案好几年了。在我目前在 Bloomberg 的职位之前,我曾在 Microsoft、Tesla 和 Johnson & Johnson 从事数据和机器学习工作。我拥有哥伦比亚大学的数据科学学位,同时也参与了人工智能负责任和伦理使用的研究。除了我的工作,我还是两本关于机器学习和数据科学的书籍及在线课程的作者。我不断探索通过利用 AI 解决复杂问题的方式,以在世界上产生积极影响,同时秉持伦理和负责任的实践。

随时通过LinkedInTwitter与我联系。

这里有更多你可能感兴趣的数据科学内容!

## 自然语言处理(NLP)的详细新手入门

《Python 中开始使用 NLP 的终极代码指南》

towardsdatascience.com ## Python 中语音识别和音频信号处理的逐步指南

教授机器人类词汇的科学

towardsdatascience.com ## 使用 Python 从面部表情识别情感的终极指南

构建一个可以处理图像和视频的面部情感识别(FER)算法

towardsdatascience.com

资源

[1] dataaspirant.com/nlp-text-preprocessing-techniques-implementation-python/#t-1600081660732

[2] www.exxactcorp.com/blog/Deep-Learning/text-preprocessing-methods-for-deep-learning

[3] www.nbcnews.com/news/us-news/oklahoma-sex-offender-suspected-killing-6-was-free-sex-crimes-charges-rcna82884

[4] www.nltk.org/book/ch05.html

[5] www.turing.com/kb/guide-on-word-embeddings-in-nlp

Apple Silicon Macs(M1 和 M2)的终极 Python 和 Tensorflow 设置指南

原文:towardsdatascience.com/the-ultimate-python-and-tensorflow-set-up-guide-for-apple-silicon-macs-m1-m2-e9ef304a2c06

逐步指南

在 ARM Mac 上安装 TensorFlow 和 Python 从未如此简单

Pol MarinTowards Data Science Pol Marin

·发表于 Towards Data Science ·6 分钟阅读·2023 年 2 月 25 日

--

照片由 Ales Nesetril 提供,来源于 Unsplash

一年前我换了工作,发现自己拥有一台全新的 M1 MacBook Pro。我需要安装数据科学家所需的一切,这确实是一件痛苦的事。

更糟糕的是,事实证明我没有正确设置,一片混乱。

但一位同事分享了一套步骤,在按照这些步骤成功设置之后,我想把它分享给大家。所以我总结并扩展了额外的信息和步骤,以确保完整性。因为我在网上找不到类似的内容,至少在我搜索时没有找到,所以我决定公开它,以便大家都能受益。

在这个故事中,你会找到一个逐步指南,讲解如何在 M1 和 M2 Mac 上成功安装 Python 和 Tensorflow,而无需自己费力去设置。

工作流程相对简单:

  1. 我们将首先安装任何开发者在 Mac 上需要的基本要求(如果你不是在使用新的 Mac,可能这些要求已经满足了)。

  2. 然后,使用pyenv进行 Python 的安装。

  3. 最后,正确安装和设置 Tensorflow 以适用于 M1 或 M2 Mac。

你只需要一台 ARM Mac,就可以开始了!

设置 Python

现在的 Mac 通常已经预装了 Python,至少是 Python2,但我相信在像 M1 或 M2 MacBook 这样的 arm64 设备上,有更好和推荐的 Python 使用方式。

XCode 命令行工具

首先,你需要安装 XCode 命令行工具:

~> xcode-select --install
~> xcrun --show-sdk-path
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk

这些工具是为在命令行中运行的软件开发人员准备的,存在于终端应用程序中。自从苹果公司成立之前,这些基于 Unix 的工具组合就已经是几乎所有软件开发的基础。

Homebrews

一旦我们安装了命令行工具,我们需要安装两个 homebrews。为什么是两个?我们想要一个本地(Apple Silicon)和一个 Intel(也叫 Rosetta)。为此,我们需要为每种架构准备一个 Terminal/iTerm2。很简单。

我推荐使用 iTerm2[1],因为它是改进版的 macOS 终端,但如果你更喜欢使用内置终端,它的功能是相同的。

转到你的应用程序文件夹,复制“iTerm”并将副本重命名为“intel iTerm”(或类似名称)。对于那个新应用程序,右键点击并选择“获取信息”。在常规部分,勾选“使用 Rosetta 打开”。

现在我们需要设置 Homebrew [2]。

从 Intel 终端运行:

~> arch --x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

如果你不知道自己使用的是哪个终端,可以使用uname -m并查看输出:

  • 如果你看到x86_64,那么你在 Intel/Rosetta 终端中。

  • 但如果输出显示arm64,则说明你在本地终端(M1/M2)中。

现在打开本地终端(默认的 iTerm 或 Terminal),并运行下一组指令:

~> cd ~/Downloads
~> mkdir homebrew
~> curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew
~> sudo mv homebrew /opt/homebrew

现在检查两个终端中是否都已正确安装。

  • 在 Intel iTerm 上:
~> brew --prefix
/usr/local
~> brew list
<list of installed packages>
  • 在本地 iTerm 上:
~> brew --prefix
/opt/homebrew
~> brew list
<list of installed packages (different than x86_64)>

现在,为了正确管理这两个环境,我们需要进行适当的设置。只需在你的~/.zshrc文件中添加以下配置:

... rest of the file above ...

#
# homebrew setup, following https://noahpeeters.de/posts/apple-silicon/homebrew-setup/
#
if [ -d "/opt/homebrew/bin" ]; then
    export PATH="/opt/homebrew/bin:$PATH"
fi

function ibrew() {
   arch --x86_64 /usr/local/bin/brew $@
}

# variables needed to properly install things under intel or m1

ARCH="$(uname -m)"
case ARCH in
    i386)   ARCH="386" ;;
    i686)   ARCH="386" ;;
    x86_64) ARCH="amd64" ;;
    arm)    dpkg --print-architecture | grep -q "arm64" && ARCH="arm64" || ARCH="arm" ;;
esac

if [[ "${ARCH}"  == "arm64" ]]; then
    PREFIX="/opt/homebrew"
else
    PREFIX="/usr/local"
fi

# https://github.com/pyenv/pyenv/issues/1768
SDK_PATH="$(xcrun --show-sdk-path)"

echo $PREFIX
echo $SDK_PATH

export CPATH="${SDK_PATH}/usr/include"
export CFLAGS="-I${SDK_PATH}/usr/include/sasl $CFLAGS"
export CFLAGS="-I${SDK_PATH}/usr/include $CFLAGS"
export CFLAGS="-I${PREFIX}/include $CFLAGS"
export LDFLAGS="-L${SDK_PATH}/usr/lib $LDFLAGS"
export LDFLAGS="-L${PREFIX}/lib $LDFLAGS"
export LDFLAGS="-L${PREFIX}/opt/openssl/lib $LDFLAGS"
export CPPFLAGS="-I${PREFIX}/opt/openssl/include $CPPFLAGS"

为了让 brew 正常工作,只需要第一个部分……但是后面添加的其他标志可能也会有用(可能需要?)。

记录一下:我们会尽量首先使用本地 brew 来安装包。只有当某个包安装失败且确认本地版本无法安装时,我们才会使用 Intel brew。

Pyenv

好的,现在我们已经可以开始安装与 Python 相关的内容了。我们将从pyenv开始,这将使安装各种 Python 版本的过程极其顺利。

打开你的本地终端,然后简单地输入下面的命令:

~> brew install pyenv

构建依赖

为了让 pyenv 正常工作,我们需要安装几个依赖项[3]。在我们的例子中:

~> brew install openssl readline sqlite3 xz zlib tcl-tk libffi

此外,我们还希望在我们的 intel homebrew 中添加相同的依赖项,所以:

~> ibrew install openssl readline sqlite3 xz zlib tcl-tk libffi

现在,你可以开始安装 Python 版本了。下面是一个示例,如果你想安装 Python 3.9.9 并将其设置为默认版本:

~> pyenv install 3.9.9
~> pyenv global 3.9.9

如果你想查看当前安装的版本,只需使用pyenv versions

因为这种过程双重检查总是值得的,所以运行下一个命令:~/.pyenv/versions/3.9.9/bin/python,将3.9.9替换为你决定安装并设置为全局的版本。一旦进入 shell,尝试导入ctypes

Python 3.9.9 (main, Feb 15 2023, 11:25:42)
[Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
>>>

如果它没有中断,那么它就已正确安装。

Pipenv

虽然安装它不是强制性的,但为你的项目使用环境始终是一个好习惯。这也是我决定在这里提到它的原因,因为它简单且好用。安装方法如下:

~> brew install pipenv

现在你可以在任何项目中使用它。你可以查看 pipenv 的基本用法,访问他们的官方文档[4]或寻找在线教程。

安装 Tensorflow

现在进入第二部分。安装 TensorFlow 在 M1 或 M2 Mac 上曾让我们许多人感到痛苦,但现在不必再这样了。

你会记得我们之前编辑了 ~/.zshrc 文件。现在,我们将在文件末尾添加这一行:

# tensorflow grpcio https://github.com/grpc/grpc/issues/25082
export GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1
export GRPC_PYTHON_BUILD_SYSTEM_ZLIB=1
export CFLAGS="-I/opt/homebrew/opt/openssl/include"
export LDFLAGS="-L/opt/homebrew/opt/openssl/lib"

一旦添加完成,我们需要将 miniconda 下载到 ~/ 目录。你可以直接从 repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh 下载,并手动移动到主文件夹。

下载完成后,我建议退出并重新打开原生终端,然后运行:

~> bash ~/Miniconda3-latest-MacOSX-arm64.sh -b -p $HOME/miniconda

现在我们已经安装了 conda,我们将激活环境并安装一些 Tensorflow 的依赖项:

~> source ~/miniconda/bin/activate
(base) ~> conda install -c apple tensorflow-deps

再次,通过退出(Cmd + Q)并重新打开终端,你现在可以安装 Tensorflow。我将以这种方式在专用环境中进行安装:

  1. 进入你的项目文件夹:例如 cd Documents/project

  2. 激活环境:pipenv shell

  3. 安装 Tensorflow:pipenv install tensorflow-macos

瞧好了! 你现在应该能在 M1 或 M2 Mac 上正确使用 TensorFlow 了。

看看我安装了一个名为 tensorflow-metal[5] 的包来加速我们 Mac 上 GPU 模型的训练,所以你可以考虑用

~> pipenv install tensorflow-metal

结论

现在你知道如何应对设置全新 Mac 用于数据科学的痛苦,并充分利用其新芯片。

希望这些信息对你有所帮助,如果你遇到任何疑问或问题,请随时在这个故事下评论——我或其他专家一定会帮助你。

我不想离开而不感谢你阅读我的故事。我经常在 Medium 上发帖,如果你喜欢这篇文章,欢迎关注我。这对我很有帮助。

如果你想进一步支持我,可以通过下面的链接订阅 Medium 会员:这不会让你多花一分钱,但它会帮助我度过这个过程。非常感谢!

[## 通过我的推荐链接加入 Medium — Pol Marin

阅读 Pol Marin 的所有故事(以及 Medium 上其他成千上万的作家)。你的会员费直接支持 Pol…

medium.com](https://medium.com/@polmarin/membership?source=post_page-----e9ef304a2c06--------------------------------)

资源

[1] iTerm2 — macOS 终端替代品

[2] Noah Peters, 在 Apple Silicon 上设置 Homebrew

[3] Pyenv 维基

[4] Pipenv 的基础用法

[5] 官方 PyPi — Tensorflow-metal 包

终极可视化助手

原文:towardsdatascience.com/the-ultimate-visualization-assistant-2dc89003d847

一个与 AI 的夜晚如何改变了我对数据可视化的方式

Anthony BaumTowards Data Science Anthony Baum

·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 6 月 30 日

--

Simon Abrams的照片,来源于Unsplash

当太阳开始暗淡,城市灯光逐渐亮起时,办公室熬夜的不可避免性开始显现。我发现自己在与时间赛跑。一个关键的销售演示即将到来,而成功取决于一个未完成的请求——一个能够简洁传达我们新电视广告归因模型基本方法的有洞察力的数据可视化。

我需要一种既足够复杂以展示,又足够简单以无需解释即可传达大部分信息的东西。在时间紧迫的情况下,这并不容易。值得庆幸的是,人工智能的灯塔,我可靠的新助手——ChatGPT——随时准备简化眼前的任务。它的主要使命是:帮助我将脑海中仅仅是概念的数据转化为优雅的视觉叙事,能够吸引即使是最厌恶数据的观众。

这可能有点戏剧化,但我确实有点赶时间。对于一个销售演示,这需要一个过于简单的例子,我在我们的数据库中找不到,因此只好使用虚构的示例数据。

我脑海中清楚地知道我想展示的内容——一个网络流量基线、电视广告后网络响应的激增,以及一种在几乎没有文字的情况下传达我们将在这个例子中将基线与激增之间的差异归因的方式。

Matplotlib 图表在学术论文中非常扎实,但制作出既复杂又真正引人注目的图表可能需要过人的技巧。我经常转向 Power BI 来获取现成的外观和快速图表,但当你需要从零开始制定某些内容而没有现成数据时,它并不理想。

我已经使用了 Plotly 一段时间,这似乎是一个不错的选择。然而,我对这个包的信心不够,不能在不花费宝贵时间翻阅文档和 Stack Overflow 的情况下把我的想法变成代码。因此,时间紧迫,我开始了我的第一个提示。

我正在创建一个可视化图表,显示电视广告播出后的网络流量反应。我们从将网络流量表示为 30 分钟内每分钟网站访问次数开始。前十分钟需要是 0 到 4 之间的随机数。第二个十分钟周期需要是 4 到 20 之间的随机数,并且这些数字分布看起来像右偏直方图。第三个十分钟周期与第一个十分钟周期相同。请使用 Plotly 创建这些数据并绘制折线图。

提示返回了以下内容,我仅在 update_layout 函数中进行了编辑:

import numpy as np
import plotly.graph_objects as go

# Set seed for reproducibility
np.random.seed(42)

# Create arrays with the specified distributions
first_ten = np.random.uniform(low=0, high=4, size=10)
last_ten = np.random.uniform(low=0, high=4, size=10)
middle_ten = np.random.gamma(shape=2, scale=2, size=10) + 4 # shift the gamma distribution

# Combine the arrays
measurements = np.concatenate((first_ten, middle_ten, last_ten))

# Create x values
x_values = np.arange(1, 31)

# Create the line chart
fig = go.Figure(
    data=go.Scatter(
        x=x_values, 
        y=measurements, 
        mode='lines+markers')
    )

fig.update_layout(
    title='Ad Influence on Web Traffic', 
    xaxis_title='Time', 
    yaxis_title='Visits per Minute',
    width=1000,
    height=600
)

fig.show()

上一个代码块生成的图表

我已经对这一点感到印象深刻,因为我没想到右偏直方图样式的请求处理得如此出色。也许我甚至能在下班前的聚会结束之前完成。

接下来,我需要一些东西来表示网络流量的基线。实际上,这可以简单到滚动中位数,但由于数据长度有限,这里要求这个可能行不通。相反,我认为前十组和后三组十个观测值的中位数可能更能传达正确的想法。

请首先计算两个数组 first_ten 和 last_ten 的联合中位数。将计算得到的中位数绘制为常量水平线。

import numpy as np
import plotly.graph_objects as go

# Set seed for reproducibility
np.random.seed(42)

# Create arrays with the specified distributions
first_ten = np.random.uniform(low=0, high=4, size=10)
last_ten = np.random.uniform(low=0, high=4, size=10)
middle_ten = np.random.gamma(shape=2, scale=2, size=10) + 4 # shift the gamma distribution

# Combine the arrays
measurements = np.concatenate((first_ten, middle_ten, last_ten))

# Create x values
x_values = np.arange(1, 31)

# Calculate combined median
combined_median = np.median(np.concatenate((first_ten, last_ten)))

# Create the line chart
fig = go.Figure()

# Create the line chart
fig = go.Figure(
    data=go.Scatter(
        x=x_values, 
        y=measurements, 
        mode='lines+markers')
    )

# Add median line
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=[combined_median]*len(x_values), 
        mode='lines', 
        name='Baseline', 
        line=dict(color='firebrick', width=2, dash='dash'))
)

fig.update_layout(
    title='Ad Influence on Web Traffic',
    xaxis_title='Time',
    yaxis_title='Visits per Minute',
    width=1000,
    height=600
)

fig.show()

然后我仅将中位数线追踪的名称从“Median”更改为“Baseline”。

上面代码块中的图表,添加了基线常量线

现在,如果你对营销模型有所了解,“提升”的概念就会显现出来,但对于未受过训练的眼睛来说,这需要一些更集中的元素;需要解释的越少,视觉效果就越有效。此时主要缺少的是一个能暗示网络流量激增的因果关系的图表元素,以及一种传达归因于广告的流量量的方法。

我从因果关系元素开始。

在数据的第 10 分钟,一则电视广告播出,并引发了网络流量的激增。你会建议使用什么样的图表元素来传达不仅广告在那一分钟播出,而且广告是流量激增的原因?

响应建议使用垂直线和注释,并提供了代码更新。

import numpy as np
import plotly.graph_objects as go

# Set seed for reproducibility
np.random.seed(42)

# Create arrays with the specified distributions
first_ten = np.random.uniform(low=0, high=4, size=10)
last_ten = np.random.uniform(low=0, high=4, size=10)
middle_ten = np.random.gamma(shape=2, scale=2, size=10) + 4 # shift the gamma distribution

# Combine the arrays
measurements = np.concatenate((first_ten, middle_ten, last_ten))

# Create x values
x_values = np.arange(1, 31)

# Calculate combined median
combined_median = np.median(np.concatenate((first_ten, last_ten)))

# Create the line chart
fig = go.Figure()

# Add scatter plot
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=measurements, 
        mode='lines+markers', 
        name='Web Traffic')
)

# Add median line
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=[combined_median]*len(x_values), 
        mode='lines', 
        name='Baseline', 
        line=dict(color='firebrick', width=2, dash='dash'))
)

# Add vertical line at minute 10
fig.add_shape(
    type='line',
    x0=10, y0=0,
    x1=10, y1=max(measurements),
    line=dict(
        color="Purple",
        width=3,
        dash="dot",
    )
)

# Add annotation to indicate TV spot
fig.add_annotation(
    x=10,
    y=max(measurements),
    text="TV ad aired",
    showarrow=True,
    arrowhead=1,
    ax=-100,
    ay=-25,
    font=dict(
        size=16,
        color="Black"
    ),
)

fig.update_layout(
    title='Ad Influence on Web Traffic',
    xaxis_title='Time',
    yaxis_title='Visits per Minute',
    width=1000,
    height=600
)

fig.show()

我仅更改了注释的字体大小。

上面代码块中的图表,添加了电视广告播出的常量线和注释

现在我们有了明确的原因和发生点来解释峰值。图表仍然需要一些东西来表示归因的流量。我已经知道我想要的——为“提升窗口”(十分钟)期间的基线和网络流量之间的区域添加阴影。我回到我的 AI 同事那里实现这个。

我想显示归因于广告的基线以上的网络流量。请在基线线和每分钟网络流量线之间添加绿色阴影。只为数据的第 10 分钟到第 20 分钟之间的区域着色。还请为阴影区域添加图例项。

import numpy as np
import plotly.graph_objects as go

# Set seed for reproducibility
np.random.seed(42)

# Create arrays with the specified distributions
first_ten = np.random.uniform(low=0, high=4, size=10)
last_ten = np.random.uniform(low=0, high=4, size=10)
middle_ten = np.random.gamma(shape=2, scale=2, size=10) + 4 # shift the gamma distribution

# Combine the arrays
measurements = np.concatenate((first_ten, middle_ten, last_ten))

# Create x values
x_values = np.arange(1, 31)

# Calculate combined median
combined_median = np.median(np.concatenate((first_ten, last_ten)))

# Create the line chart
fig = go.Figure()

# Add scatter plot
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=measurements, 
        mode='lines+markers', 
        name='Web Traffic')
)

# Add median line
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=[combined_median]*len(x_values), 
        mode='lines', 
        name='Baseline', 
        line=dict(color='firebrick', width=2, dash='dash'))
)

# Add shaded region for the influence of the ad
fig.add_trace(
    go.Scatter(
        x=list(x_values[9:20]) + list(x_values[9:20])[::-1], 
        y=list([combined_median]*11) + list(measurements[9:20])[::-1], 
        fill='toself', 
        fillcolor='rgba(0,200,0,0.2)', 
        line=dict(width=0), 
        hoverinfo="skip", 
        showlegend=True, 
        name='Ad Impact')
)

# Add vertical line at minute 10
fig.add_shape(
    type='line',
    x0=10, y0=0,
    x1=10, y1=max(measurements),
    line=dict(
        color="Purple",
        width=3,
        dash="dot",
    )
)

# Add annotation to indicate TV spot
fig.add_annotation(
    x=10,
    y=max(measurements),
    text="TV ad aired",
    showarrow=True,
    arrowhead=1,
    ax=-100,
    ay=-25,
    font=dict(
        size=16,
        color="Black"
    ),
)

fig.update_layout(
    title='Ad Influence on Web Traffic',
    xaxis_title='Time',
    yaxis_title='Visits per Minute',
    width=1000,
    height=600
)

fig.show()

来自上述代码块的图表,添加了表示广告影响的阴影。

我感觉这已经到了一个不错的地方,但对我试图传达的“清晰度”仍不满意。我根据已经提供的代码做了一些补充。

我添加了一条常量垂直线和注释以标示提升窗口的截断,并将广告播放线和窗口截断线的颜色分别更改为绿色和红色。我还将基线常量线更改为紫色,并将其样式设置为虚线,以更好地匹配其他常量线。

然后我想要一个最终元素——基线区域的阴影。希望继续保持迄今为止的完美表现,我把这交给了我的助理。

请对整个基线区域添加紫色阴影。在第 15 分钟的基线水平线上方添加注释“Baseline”。

注意,我在这里没有引用任何坐标或阴影位置的具体数字。

import numpy as np
import plotly.graph_objects as go

# Set seed for reproducibility
np.random.seed(42)

# Create arrays with the specified distributions
first_ten = np.random.uniform(low=0, high=4, size=10)
last_ten = np.random.uniform(low=0, high=4, size=10)
middle_ten = np.random.gamma(shape=2, scale=2, size=10) + 4 # shift the gamma distribution

# Combine the arrays
measurements = np.concatenate((first_ten, middle_ten, last_ten))

# Create x values
x_values = np.arange(1, 31)

# Calculate combined median
combined_median = np.median(np.concatenate((first_ten, last_ten)))

# Create the line chart
fig = go.Figure()

# Add baseline region
fig.add_trace(
    go.Scatter(
        x=list(x_values) + list(x_values)[::-1], 
        y=[combined_median]*len(x_values) + [0]*len(x_values), 
        fill='toself', 
        fillcolor='rgba(128, 0, 128, 0.2)', 
        line=dict(width=0), 
        hoverinfo="skip", 
        showlegend=True, 
        name='Baseline')
)

# Add scatter plot
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=measurements, 
        mode='lines+markers', 
        name='Web Traffic',
        line=dict(color='#636EFA'))
)

# Add median line
fig.add_trace(
    go.Scatter(
        x=x_values, 
        y=[combined_median]*len(x_values), 
        mode='lines', 
        name='Median', 
        line=dict(color='Purple', width=2, dash='dash'))
)

# Add shaded region for the influence of the ad
fig.add_trace(
    go.Scatter(
        x=list(x_values[9:20]) + list(x_values[9:20])[::-1], 
        y=list([combined_median]*11) + list(measurements[9:20])[::-1], 
        fill='toself', 
        fillcolor='rgba(0,200,0,0.2)', 
        line=dict(width=0), 
        hoverinfo="skip", 
        showlegend=True, 
        name='Ad Impact')
)

# Add vertical line at minute 10
fig.add_shape(
    type='line',
    x0=10, y0=0,
    x1=10, y1=max(measurements),
    line=dict(
        color="Green",
        width=3,
        dash="dot",
    )
)

# Add annotation to indicate TV spot
fig.add_annotation(
    x=10,
    y=max(measurements),
    text="TV ad aired",
    showarrow=True,
    arrowhead=1,
    ax=-100,
    ay=-25,
    font=dict(
        size=16,
        color="Green"
    ),
)

# Add vertical line at minute 20
fig.add_shape(
    type='line',
    x0=20, y0=0,
    x1=20, y1=max(measurements),
    line=dict(
        color="Red",
        width=3,
        dash="dot",
    )
)

# Add annotation to indicate lift window cutoff
fig.add_annotation(
    x=20,
    y=max(measurements),
    text="Lift window cutoff",
    showarrow=True,
    arrowhead=1,
    ax=100,
    ay=-25,
    font=dict(
        size=16,
        color="Red"
    ),
)

# Add annotation for lift
fig.add_annotation(
    x=15,
    y=combined_median + (max(measurements[9:20])-combined_median)/4,
    text="Lift due to Ad",
    showarrow=False,
    font=dict(
        size=18,
        color="Green"
    ),
)

# Add annotation for baseline
fig.add_annotation(
    x=15,
    y=combined_median/2,
    text="Baseline",
    showarrow=False,
    font=dict(
        size=18,
        color="Purple"
    ),
)

fig.update_layout(
    title='Ad Influence on Web Traffic',
    xaxis_title='Time',
    yaxis_title='Visits per Minute',
    width=1000,
    height=600
)

fig.show()

来自上述代码块的图表,添加了基线阴影、截断线和更多注释。

基线阴影请求的捕捉没有提供具体值是顶级的,这一点我已经深感印象。对可视化效果满意后,我将图表放入演示文稿中,发了一条 Teams 消息,并怀着依依不舍的目光看了下 ChatGPT 标签,关闭了我的笔记本电脑。当我收拾东西时,来自下班后群聊的通知在我的手机屏幕上闪烁。

看了演示消息,觉得你说你会晚点来?我们会为你准备一品脱啤酒。

祝好,ChatGPT。

《被低估的珍宝 第一部分:8 个让你成为高手的 Pandas 方法》

原文:towardsdatascience.com/the-underrated-gems-pt-1-8-pandas-methods-that-will-make-you-a-pro-b1437a19ca3c?source=collection_archive---------0-----------------------#2023-07-17

被低估的、未被重视的和未被深入探索的

Andreas LukitaTowards Data Science Andreas Lukita

·

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

--

图片由 Roman Kraft 提供,来源于 Unsplash

“在喧嚣的环境中,正是那些低声细语的话语才蕴藏着隐藏的智慧 💎”

暂时忘记 ChatGPT。对我们中的一些人来说,我们会因为每次想执行简单的 Pandas 操作时都要不断地谷歌搜索解决方案而感到疲惫。似乎有无数种方法来做同一件事,那么究竟哪一种才是最佳选择?虽然有很多可能的解决方案确实很好,但也带来了不一致和对代码功能的困惑。

到达罗马有 1000 条可能的路线,可能甚至更多。问题是,你选择隐藏的小路还是复杂的路线?

图片由作者提供

这里是本文的要点。我将带你了解如何通过处理来自UCI 机器学习的自行车共享数据集¹来将这些方法付诸实践。通过采用这些方法,你不仅可以简化数据处理代码,还能更深入地理解你编写的代码。让我们开始导入数据集,并快速查看 DataFrame!

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

bike = (pd
        .read_csv("../../dataset/bike_sharing/day.csv")
       )
bike

图片由作者提供

目录

  • 方法 #1: **.assign()**

  • 方法 #2: **.groupby()**

  • 方法 #3: **.agg()**

  • 方法 #4: **.transform()**

  • 方法 #5: **.pivot_table()**

  • 方法 #6: **.resample()**

  • 方法 #7: **.unstack()**

  • 方法 #8: **.pipe()**

☕️ 方法 #1: .assign()

忘掉使用**df["new_col"] =****df.new_col =**这种操作来创建新列。这里是你应该使用**.assign()**方法的原因——它返回一个 DataFrame 对象,允许你继续链式操作以进一步处理 DataFrame。与**.assign()**方法不同,上述两种臭名昭著的操作会返回None,这意味着你无法进一步链式操作。

如果你不信,那么让我重新提起旧的敌人——**SettingWithCopyWarning**。相信我们每个人在某个时刻都遇到过这个问题。

图片由作者提供

警告够了,我不想在我的笔记本中再看到丑陋的红色框框!

使用**.assign()**,让我们添加一些新列,如**ratio_casual_registered****avg_temp****ratio_squared**

(bike
 .assign(ratio_casual_registered = bike.casual.div(bike.registered),
         avg_temp = bike.temp.add(bike.atemp).div(2),
         ratio_squared = lambda df_: df_.ratio_casual_registered.pow(2))
)

简而言之,上述方法的作用如下:

  1. 我们可以使用**.assign()**方法创建任意数量的新列,用逗号分隔。

  2. 在创建列**ratio_squared**时使用的 lambda 函数是为了访问我们在添加列**ratio_casual_registered**后最接近的 DataFrame。假设我们不使用 lambda 函数来访问最新的 DataFrame **df_**,而是继续使用**bike.ratio_casual_registered.pow(2)**,我们将会得到一个错误,因为原始 DataFrame 在创建**ratio_squared**之前,即使在**.assign()**方法中添加了该列,也没有**ratio_casual_registered**列。如果你对这个概念无法理解以决定是否使用 lambda 函数,我的建议是使用它!

  3. 额外的!我留下了一些不那么常见的使用方法进行算术运算。

☕️ 方法 #2: .groupby()

好吧,**.groupby()**方法虽然不罕见,但在我们深入探讨下一个方法之前,它们是必要的。一个常被忽视且未被提及的事情是,**.groupby()**方法具有懒惰特性。也就是说,该方法是延迟评估的。换句话说,它不会立即评估,这就是为什么你在调用**.groupby()**方法后常常会看到**<pandas.core.groupby.generic.DataFrameGroupBy object at 0x14fdc3610>**

从 Pandas DataFrame 文档² 中,传递给参数**by**的值可以是映射、函数、标签、pd.Grouper 或此类的列表。 然而,你最常遇到的情况可能是按列名分组(用逗号分隔的 Series 名称列表)。在**.groupby()**操作之后,我们可以执行诸如**.mean()****.median()**,或使用**.apply()**应用自定义函数等操作。

我们传递给**.groupby()**方法的**by**参数中的指定列的值将成为结果的索引。如果我们指定了多个列进行分组,那么我们将获得一个 MultiIndex。

(bike
 .groupby(['season', 'weathersit'])
 .mean(numeric_only=True) #alternative version: apply(lambda df_: df_.mean(numeric_only=True)) 
 .atemp
)

在这里,我们按照列**season****weathersit**对 DataFrame 进行了分组。然后,我们计算了均值,并仅选择了列**atemp**

图片来源于作者

☕️ 方法 #3: .agg()

如果你足够细心去深入研究 Pandas 文档²,你可能会遇到**.agg()****.aggregate()**这两种方法。你可能会想知道它们有什么区别,以及什么时候使用哪个?节省你的时间吧!它们是相同的,**.agg()**只是**.aggregate()**的别名。

**.agg()**有一个参数**func**,它可以接受函数、字符串函数名或函数列表。顺便提一下,你也可以在列上聚合不同的函数!让我们继续上面的例子!

#Example 1: Aggregating using more than 1 function
(bike
 .groupby(['season'])
 .agg(['mean', 'median'])
 .atemp
)

#Example 2: Aggregating using different function for different columns
(bike
 .groupby(['season'])
 .agg(Meann=('temp', 'mean'), Mediann=('atemp', np.median))
)

图片来源于作者

☕️ 方法 #4: .transform()

使用 **.agg()**,我们得到的结果在维度上相较于初始数据集是缩减的。简单来说,你的数据维度会减少,包含更少的行和列,包含汇总信息。如果你想对分组数据进行总结并获得聚合值,那么 **.groupby()** 是解决方案。

使用 **.transform()**,我们也开始时意图进行信息的汇总。然而,与创建信息汇总不同的是,我们希望输出的形状与原始 DataFrame 相同,而不缩小原始 DataFrame 的大小。

对于那些接触过类似 SQL 的数据库系统的人来说,**.transform()** 的概念可能与窗口函数类似。让我们看看 **.transform()** 在上面的示例中是如何工作的!

(bike
 .assign(mean_atemp_season = lambda df_: df_
                                          .groupby(['season'])
                                          .atemp
                                          .transform(np.mean, numeric_only=True))
)

图片由作者提供

如上所见,我们创建了一个名为 **mean_atemp_season** 的新列,并将该列填充为 **atemp** 列的汇总(均值)。因此,每当 **season** 为 1 时,**mean_atemp_season** 的值就相同。请注意这里的重要观察点是我们保留了原始数据集的维度,并增加了一列!

☕️ 方法 #5: .pivot_table()

这是给那些对 Microsoft Excel 迷恋的人的额外提示。你可能会被诱惑使用 **.pivot_table()** 来创建汇总表。这个方法当然也有效!但这里有一点建议,**.groupby()** 更加灵活,适用于更广泛的操作,不仅限于重新排列,还包括过滤、转换或应用组特定的计算。

简要说明一下如何使用 **.pivot_table()**。你在 **values** 参数中指定你要汇总的列。接下来,使用原始 DataFrame 的子集指定你想要创建的汇总表的 index。这可以是多列,汇总表将是 MultiIndex 的 DataFrame。接下来,使用未选择为索引的原始 DataFrame 的子集来指定你想要创建的汇总表的 columns。最后但同样重要的是,不要忘记指定 **aggfunc**!我们来快速看一下!

(bike
 .pivot_table(values=['temp', 'atemp'],
              index=['season'],
              columns=['workingday'],
              aggfunc=np.mean)
)

图片由作者提供

☕️ 方法 #6: .resample()

粗略地说,**.resample()** 方法可以被视为专门用于时间序列数据的分组和汇总,其中

DataFrame 或 Series 的索引是类似日期时间的对象。

这使你可以根据不同的时间频率对数据进行分组和汇总,例如每小时、每天、每周、每月等。更一般地说,**.resample()** 可以接受 DateOffset、Timedelta 或 str 作为规则来执行重采样。让我们将其应用于之前的示例。

def tweak_bike(bike: pd.DataFrame) -> pd.DataFrame:
    return (bike
            .drop(columns=['instant'])
            .assign(dteday=lambda df_: pd.to_datetime(df_.dteday))
            .set_index('dteday')
           )
bike = tweak_bike(bike)
(bike
 .resample('M')
 .temp
 .mean()
)

简而言之,我们在上面所做的操作是删除**instant**列,用**dteday**列覆盖**dteday**列,将**dteday**列的类型从object转换为**datetime64[ns]**类型,最后将这个**datetime64[ns]**列设为 DataFrame 的索引。

作者提供的图片

(bike
 .resample('M')
 .temp
 .mean()
)

作者提供的图片

在这里,我们获得了特征**temp**的描述性统计汇总(均值),以月频率呈现。尝试使用不同的频率,如**Q****2M****A**等,来玩转**.resample()**方法。

☕️ 方法 #7: .unstack()

我们快要结束了!让我给你展示一下**.unstack()**为何既强大又有用。在此之前,让我们回到上述一个例子,我们想通过使用**.groupby()****.agg()**来找到不同季节和天气情况的平均温度。

(bike
 .groupby(['season', 'weathersit'])
 .agg('mean')
 .temp
)

作者提供的图片

现在,让我们使用一个线图来可视化这一点,通过将**.plot****.line()**方法最小化地链接到上述代码中。在后台,Pandas 利用 Matplotlib 绘图后端来完成绘图任务。这给出了以下结果,我们都不想要,因为图的 x 轴是按 MultiIndex 分组的,使其更难以解释,且意义不大。

作者提供的图片

比较引入**.unstack()**方法后的图与上图。

(bike
 .groupby(['season', 'weathersit'])
 .agg('mean')
 .temp
 .unstack()
 .plot
 .line()
)

作者提供的图片

简而言之,**.unstack()**方法的作用是解开 MultiIndex DataFrame 中最内层的索引,在这个例子中是**weathersit**。这个所谓的解开索引变成了新 DataFrame 的列,这使得我们的线图绘制结果更加有意义,方便比较。

作者提供的图片

你还可以通过指定参数**level=0**作为**.unstack()**方法的一部分,来解开 DataFrame 中最外层的索引。让我们看看如何实现这一点。

(bike
 .groupby(['season', 'weathersit'])
 .agg('mean')
 .temp
 .unstack(level=0)
 .plot
 .line()
)

作者提供的图片

作者提供的图片

☕️ 方法 #8: .pipe()

从我的观察来看,当你在线搜索时,你几乎看不到普通人在他们的 Pandas 代码中实现这个方法。一个原因是,**.pipe()**有一种神秘的无法解释的气质,使得它对初学者和中级用户都不友好。当你去 Pandas 文档²时,你会发现的简短解释是“应用期望 Series 或 DataFrame 的可链式函数”。我认为这个解释有些混淆,并且对那些从未使用链式操作的人并不十分有帮助。

简而言之,**.pipe()**为您提供的是使用函数继续方法链技术的能力,在您无法找到直接解决方案执行操作返回 DataFrame 的情况下。

方法**.pipe()**接受一个函数作为参数,通过这个函数可以定义一个链外方法,然后将该方法作为**.pipe()**方法的参数引用。

使用.pipe(),您可以将 DataFrame 或 Series 作为自定义函数的第一个参数传递,并且该函数将应用于传递的对象,随后是指定的任何其他参数。

大多数情况下,您会看到**.pipe()**方法中的一行 lambda 函数,以方便使用(例如,在链处理过程中的某些修改步骤后获取最新 DataFrame)。

让我用一个简化的例子来说明。假设我们想要了解以下问题:“对于 2012 年,每个季节相对于该年总工作日的工作日比例是多少?”

(bike
 .loc[bike.index.year == 2012]
 .groupby(['season'])
 .workingday
 .agg(sum)
 .pipe(lambda x: x.div(x.sum()))
)

在这里,我们使用**.pipe()**将函数注入到我们的链式方法中。由于执行了**.agg(sum)**之后,我们无法继续使用**.div()**进行链式处理,因此以下代码将无法工作,因为我们在链式处理过程中失去了对 DataFrame 的最新状态的访问。

#Doesn't work out well!
(bike
 .loc[bike.index.year == 2012]
 .groupby(['season'])
 .workingday
 .agg(sum)
 .div(...)
)

提示:如果找不到继续链接方法的方式,请考虑如何使用.pipe()!大多数情况下,它会有所帮助!

结语

这就是低估的宝石💎第一部分的全部内容!这些都是我以前没有经常使用的方法,也许是因为我的坏习惯——用“只要能用,就够好!”的思维强行解决代码问题。不幸的是,这样做是不够好的!

只有在我花时间学习如何正确使用它们之后,它们才被证明是救命稻草,可以这么说!我还要感谢 Matt Harrison 和他的书《高效 Pandas》³,这本书彻底改变了我编写 Pandas 代码的方式。现在,我可以说我的代码更简洁、更可读,而且更有意义。

在低估的宝石第二部分中,我们将介绍其他八个较少为人知的 Pandas 方法,例如**.explode()****.melt()****.expanding()**!您可能不会经常使用它们,但在需要它们的时候它们肯定很方便。

如果你从本文中获得了一些有用的东西,请考虑在 Medium 上给我关注。每周一篇文章,轻松跟上时代潮流!

联系我!

参考资料

  1. Fanaee-T,Hadi。 (2013)。自行车共享数据集。UCI 机器学习库。doi.org/10.24432/C5W894

  2. Pandas 文档: pandas.pydata.org/docs/reference/frame.html

  3. Matt Harrison 的《Effective Pandas》: store.metasnake.com/effective-pandas-book

通用模型的非凡有效性

原文:towardsdatascience.com/the-unreasonable-effectiveness-of-general-models-b4e822eaeb27

在一个极其困难的问题上测试通用模型

Samuele MazzantiTowards Data Science Samuele Mazzanti

·发表于 Towards Data Science ·8 分钟阅读·2023 年 1 月 17 日

--

[作者提供的图像,由 Excalidraw 制作]

上一篇文章 中,我尝试揭示一种有些模糊的观点,即一堆模型(每个模型专注于数据集的一个子集)应该比单一模型表现更好。

为了做到这一点,我取了一部分数据集(例如,仅美国客户),并在该组上训练了一个模型,即 专用模型。然后,我在整个数据集(即所有客户,无论其国籍)上训练了第二个模型,即 通用模型。最后,我比较了这两个模型在只包含该组观察结果的保留集上的表现。

我在多个数据集和同一数据集的多个组上重复了这个过程,总共进行了 600 次比较。 最终的赢家是通用模型

然而,我的实验并没有说服一些人,他们认为我的方法过于简化。例如,这是我在 LinkedIn 上关于该文章的帖子下最受欢迎的评论之一:

[从我 Linkedin 帖子] 评论区的截图

这个评论引起了我的兴趣,所以我决定按照建议去做。如果你想看看结果如何,请耐心等待。

一个工作假设

在我之前的文章中,我证明了当数据集中组成各组之间存在一定相似性时,使用通用模型相对于专用模型有明显的好处。

然而,随着组之间的差异越来越大,合理地预期使用通用模型的好处会越来越小。在最极端的情况下,即当组之间完全不同时,两种方法之间的差异应该等于零。

如果我的直觉是正确的,我们可以将这种关系草图如下:

我的工作假设的草图。你可以在这里阅读“上一篇文章”。[图片由作者提供,使用 Excalidraw 制作]

但这只是我的假设。那我们来尝试一下。

给我们的模型增加难度

我们的目标是回答这个问题:

如果组成一个数据集的组彼此完全不同,而我们仍然使用一个通用模型,会发生什么?

所以,问题变成了如何模拟这样的场景。

最极端的想法是“粘合”不同的数据集。当我说“不同”时,我指的是数据集不仅有不同的列,还有不同的任务,即它们旨在预测不同的内容。

以三个数据集为例:

  • “银行”:每一行代表一个银行客户,任务是预测他/她是否会订阅定期存款;

  • “员工”:每一行代表一名员工,任务是预测他/她是否会离开公司;

  • “收入”:每一行代表一个人,任务是预测他/她的收入是否超过 50k 美元。

将这些数据集的目标变量粘合在一起很简单:由于它们都是由 0 和 1 组成的二元变量,这很直接。但当我们尝试连接特征时,情况变得更加复杂。我来解释一下原因。

这里是三个数据集的一个样本(包括行和列)。

三个示例数据集:“银行”、“员工”和“收入”。[图片由作者提供]

如你所见,这些数据集有不同的列。那么,我们如何将它们合并在一起?第一个最简单的想法是使用pd.concat

pd.concat([X_bank, X_employee, X_income]).reset_index(drop=True)

但是,如果我们这样做,我们将得到以下形式的数据框:

第一次尝试:简单的连接。[图片由作者提供]

Pandas 默认只连接具有相同名称的列。在这种情况下,每个数据集具有不同的列名,因此结果具有类似对角线的结构。但这并不令人满意,因为它会让模型走捷径。事实上,模型能够根据非空列隐式地区分不同的数据集。

为了避免这种情况,我们需要一种“强制”合并不同数据集列的方法

我能想到的唯一方法是用递增的数字重命名每个数据集的列:“feature_01”,“feature_02”等。但这行不通,因为列有不同的类型。所以我们需要区分分类特征和数值特征:“cat_feature_01”,“cat_feature_02”等和“num_feature_01”,“num_feature_02”等。此外,我决定按重要性降序排列特征。

这是得到的输出:

第二次尝试:用递增的数字重命名列。[作者提供的图片]

也许你会认为这还不够。毕竟,模型可能仍然识别出属于某个数据集的某些类别(例如,列“cat_feature_01”中的“已婚”仅存在于“银行”数据集中)。数值特征也是如此(例如,列“num_feature_02”中的 0 到 1 之间的值仅存在于“员工”数据集中)。这对模型仍然可能有帮助,我们希望避免这种情况。

因此,作为额外步骤,我:

  • 将每个分类特征的每个值映射到不同的整数(序数编码);

  • 通过减去均值并除以标准差,对每个原始数据集的数值列进行了标准化。

所以,这就是最终结果:

第三次也是最后一次尝试:序数编码、标准化,然后用递增的数字重命名列。[作者提供的图片]

免责声明

我知道你可能会觉得这个过程——巧妙地将一些完全不相关的数据集合并在一起——有点奇怪。你是对的:我们所做的在现实世界中没有意义。

但你必须记住,这是一个教学实验,旨在推动通用模型的能力到极限,并观察它是否仍然能与专用模型竞争

这个实验必须被视为对基于树的梯度提升模型能力的一种“压力测试”。

结果

现在我们已经设计了策略,是时候将其应用于一些实际数据集了。我使用了 7 个用于二分类的具有 5000 多行的数据集,这些数据集可以在Pycaret(一个MIT 许可证下的 Python 库)中找到。

这些是数据集,及其相应的行数和列数:

Pycaret 数据集及其形状。[作者提供的图片]

然后,我应用了上述程序,这意味着我对每个数据集分别执行了以下操作:

  • 我将每个分类列(按重要性降序排列)重命名为“cat_feature_01”,“cat_feature_02”,…,每个数值列(按重要性降序排列)重命名为“num_feature_01”,“num_feature_02”,…;

  • 对每个分类列,我将每个值映射到一个不同的整数:0,1,2,…;

  • 对于每一列数值数据,我已经通过减去均值并除以标准差来标准化值;

  • 我添加了一个包含数据集名称的列。

然后,我将所有原始数据集合并以获得最终数据集。在此基础上,我进行了实验。实验内容包括:

  • 在合并后的完整数据集上训练一个通用模型(Catboost,未进行参数调优);

  • 训练了 7 个专业模型(Catboost,未进行参数调优),每个模型对应一个原始数据集;

  • 比较通用模型和专业模型在每个数据集上的表现。

在数据集的一组上比较通用模型和专业模型的过程。[作者提供的图片]

我在查看结果时注意到的第一件事是通用模型的预测与专业模型的预测之间的相关性为 98%,这表明它们产生了非常相似的输出。

那么性能如何呢?

这是通用模型与专业模型 ROC 分数的比较:

ROC 分数对比。[作者提供的图片]

通用模型的 ROC 分数与专业模型的 ROC 分数之间的平均差异是 -0.53%。这意味着专业模型通常优于通用模型

然而,我必须说,我对这种微小差异感到印象深刻。我们在一个极其困难的设置中进行了测试,通用模型仍然能够达到非常接近专业模型的性能。这证明了通用模型在这个极其困难的问题上是多么有效。

那么解释性如何呢?

我听到关于通用模型的另一个担忧是其所谓的解释性不足。事实上,有些人声称,一个通用模型的透明度不如多个专业模型。

我不同意这一点。事实上,得益于 SHAP 值,即使模型是唯一的,你也可以分别解释每个组。我们可以称这种过程为“专业化解释性”。

专业化解释性。[作者提供的图片]

让我用我们之前的实验做一个例子。

如果我们分别考虑每个组并计算原始特征值与相应 SHAP 值之间的相关系数,我们得到的结果是:

每个合并特征与相应 SHAP 值之间的相关性。[作者提供的图片]

如你所见,不同组之间的相关系数变化很大。例如,如果我们取“num_feature_01”,则“bank”组的相关性为正,而“employee”组的相关性为负。这非常有意义:

  • 对于“银行”组,“num_feature_01”对应于“duration”特征,即账户持有者的持有时间。目标特征是客户是否订阅了定期存款。可以合理地预期该特征对预测有积极影响。

  • 对于“员工”组,“num_feature_01”对应于“satisfaction_level”特征。由于目标特征是员工是否离职,因此负相关性可以很容易解释。

结论

在这篇文章中,我模拟了对一般模型而言最困难的情境:一个数据集组成的各组完全不同的案例。

为了模拟这种情况,我合并了一些完全无关的数据集,包括特征和预测任务!我使用了一个技巧,以确保不同数据集的列即使名称不同也能连接在一起。

然后,我在合并的数据集上训练了一个一般模型和许多专门模型:每个原始数据集一个。

这是一个压力测试,用来查看在对一般模型来说极端困难的情况下会发生什么。然而,我发现性能差异最小:使用一般模型代替专门模型的 ROC 评分平均损失为 0.53%。

此外,我还利用这个实验展示了为什么可解释性也不应成为问题。实际上,使用一般模型后,人们仍然可以通过“专门的可解释性”分别解释单独的组。

你可以在这个笔记本中找到本文使用的所有 Python 代码。

感谢阅读!

如果你觉得我的工作有用,可以订阅 每次我发布新文章时获取邮件 (通常每月一次)。

如果你想支持我的工作,可以 请我喝杯咖啡

如果你愿意, 在 LinkedIn 上添加我

非结构化数据漏斗

原文:towardsdatascience.com/the-unstructured-data-funnel-245f72925176

你向下挖掘的深度决定了你支付的费用。照片由Ricardo Gomez Angel提供,来自Unsplash

为什么漏斗成为数据最重磅竞争者之间战争的中心

Hugo LuTowards Data Science Hugo Lu

·发表于Towards Data Science ·9 分钟阅读·2023 年 12 月 15 日

--

如果你不是 Medium 会员,你可以在这里免费阅读。

介绍

非结构化数据呈现出多种形式。它通常以文本为主,但也可能包含日期、数字和字典等数据。数据工程师通常会遇到以深层次的嵌套 JSON形式出现的非结构化数据。然而,“非结构化”数据这个术语实际上是指任何非表格化的数据;事实上,超过 80%的全球数据是非结构化的

尽管对我们数据从业者而言,非结构化数据似乎无害,但它在宏观层面上产生了巨大影响。实际上,GPT 模型都经过训练以处理非结构化数据。Tomasz Tunguz 在近期文章中对 Snowflake 财报电话会议的观察是正确的:

摘自 Tomasz Tunguz 的《Snow Angels》

在这种金融和宏观经济背景下看待非结构化数据可能显得有些奇怪。我的第一份工作是在投资银行,所以我在阅读类似内容时感到有些怀旧。“非结构化数据是增长引擎”对我来说是有意义的——这听起来像是一个非常大的市场顺风

不过,我已经有一段时间没有调整 Powerpoint 框了。从概念上讲,非结构化数据现在是一个深度嵌套的 json,等待处理。但从财报电话会议中可以看出,非结构化数据现在不仅仅是 JSON(曾经是吗?),还有文本、文档、视频等。

现在出现的是,这些数据驱动了一些即将变得至关重要的用例,而数据处理的位置对数据领域的两大巨头:Databricks 和 Snowflake 至关重要。让我们深入探讨一下原因。

为什么非结构化数据重要?

GPT模型依赖数据。具体来说,它们依赖非结构化数据。这些包括文本文档、html 文件和代码片段。随着公司越来越倾向于在生产中实现LLMs,处理这些数据的价值因其需求增加而增加。因此,它对像 Snowflake 和 Databricks 这样的供应商的价值也增加。

但是处理特定类型非结构化数据还有第二个元素。以嵌套 JSON 为例。嵌套 JSON 在处理时会被展开清理。这意味着你可能会从以下内容开始:

{
  "outer_key1": {
    "inner_key1": {
      "nested_key1": {
        "deeply_nested_key1": "value1",
        "deeply_nested_key2": "value2"
      },
      "nested_key2": {
        "deeply_nested_key3": "value3",
        "deeply_nested_key4": "value4"
      }
    },
    "inner_key2": {
      "nested_key3": {
        "deeply_nested_key5": "value5",
        "deeply_nested_key6": "value6"
      },
      "nested_key4": {
        "deeply_nested_key7": "value7",
        "deeply_nested_key8": "value8"
      }
    }
  },
  "outer_key2": {
    "inner_key3": {
      "nested_key5": {
        "deeply_nested_key9": "value9",
        "deeply_nested_key10": "value10"
      },
      "nested_key6": {
        "deeply_nested_key11": "value11",
        "deeply_nested_key12": "value12"
      }
    },
    "inner_key4": {
      "nested_key7": {
        "deeply_nested_key13": "value13",
        "deeply_nested_key14": "value14"
      },
      "nested_key8": {
        "deeply_nested_key15": "value15",
        "deeply_nested_key16": "value16"
      }
    }
  }
}

到这里:

{
  "deeply_nested_key1": "value1",
  "deeply_nested_key2": "value2"
}

处理第二个 JSON 所需的计算能力比初始清理数据时所需的计算能力要少,后者处理第一个更大的对象。这意味着数据管道中第一次“清理”发生的位置会显著影响使用的计算资源。

所有非结构化数据都遵循这种模式。Snowflake 的Document AI将像 pdf 这样的文档提取为表格形式的数据。这意味着处理的重部分只发生一次,结果数据会更加干净,处理也更容易。

非结构化数据漏斗

关注数据处理发生的位置对像 Snowflake 和 Databricks 这样的云服务提供商非常重要,因为他们基于云计算收取附加费用。这意味着你需要的计算能力越多,你支付的费用也越高。我们在前一节中看到,由于大型语言模型(LLMs),非结构化数据的重要性不断增加,但处理非结构化数据所需的计算能力随着数据在数据管道中进一步处理而减少。这是直观的,因为数据在数据管道中进展时会变得更干净、更聚合。

我们可以通过想象我们的数据管道基础设施来可视化这一点。我们中的大多数人通常拥有以下架构的子集:

非结构化数据漏斗。漏斗的宽度与需要处理的数据量成正比。数据在漏斗中流动时所需的计算是非线性的,因为非结构化数据清洗、数据源合并以及分析工作流等操作受到数据量以外的因素影响。想象一下作者的情况。

数据移动

漏斗的第一部分是数据团队首次接触非结构化数据的地方。这是一个数据移动层,架构上可能是批处理或流处理。这个层级没有存储元素,但像 Fivetran、Portable 或 Striim 这样的供应商正在进行一些转换(“ETL”或“ET L”而非“ELT”或“EL T”),这需要计算并减少传递到下一个层级的数据量。

这些工具在处理能力上受到限制,因为它们没有完整的数据历史记录,因此不能执行复杂的操作,如回填或慢变维度。然而,它们适用于简单的转换,如流连接或非结构化数据的去嵌套。大多数这些供应商反正不处理像文本文件这样的非结构化数据。你可以使用云原生服务来执行这些服务,例如 Azure EventHub、BigQuery PubSub,因此这些标志也涉及到漏斗的这一部分。

可用计算资源:中等

数据湖 / 对象存储

第二层指的是对象存储中的数据,如 Google Object Storage、AWS S3 或 Azure ADLS Gen-2。这些是三大云提供商的存储解决方案,可以存储任何文件格式的数据。漏斗的这一层是所有数据集中化的第一个地方,计算资源以各种形式轻松获得,可以直接从云提供商租用或通过像 Databricks 这样的供应商获取 Spark。这一层非常适合复杂的处理,尤其适合维度和复杂度的减少。这意味着涉及的计算资源异常高。

在我看来,这一层是处理非结构化数据最有意义的地方。你可以在这里存储任何东西。你拥有大量兼容的云基础设施(所有东西都可以与 S3 交互)。你有一个现成的存储层来存储你的处理数据。在这里进行这种处理非常有意义,因为它本质上比数据仓库更灵活。数据湖是为了存储任何格式的数据而构建的——这不正是非结构化数据的特性吗?

可用计算资源:非常高

数据仓库 / SQL 层

漏斗的这一层指的是以特定格式或特定格式集合存储的数据,通常这样做是为了使其能够使用类似 SQL 的语句轻松查询和处理。Snowflake 有自己的文件格式,这并不是什么秘密,它促进了这一点,也正是你支付“入口费用”的原因,以便将数据“导入 Snowflake”。在 Databricks 方面,他们有.delta,它实际上是对.parquet 的抽象,而且是开源的。还有其他如.iceberg 这样的格式,它们是“酷”的,并且促进了 Snowflake 中的外部表(这使得 Snowflake 在漏斗层级上提升了一个层次)。

这个领域适合处理结构化数据或表格数据。然而,如展平 JSON 这样的操作也可以在这里进行(例如,参见BigQuerySnowflake)。数据工程师普遍认为这不是 SQL 计算的最佳使用方式。主要反对意见是,增量执行此类操作既具有挑战性又缓慢,因此成本高昂。然而,实际成本已经在移动数据时产生。你已经为数据通过漏斗支付了费用。如果在漏斗更高的位置进行转换或解构,那么移动通过漏斗的体积就会减少,成本也会更低。

有趣的是,这个领域在 Snowflake 财报电话会议上引起了关注。考虑到我与大多数工程师交流时发现他们倾向于在漏斗尽可能高的位置进行数据处理以节省成本,"只需将数据导入 Snowflake"的口号可能正与企业数据高管和首席信息官们产生共鸣。也有可能像 DocumentAI 和 SnowPark Container Services 这样的功能实际上意味着 SQL Warehouse 与“Data Lakehouse”之间的界限正在模糊——同样的计算可以用于漏斗不同部分的数据。

需要注意的是,你可能会为在数据仓库中处理非结构化数据的计算处理支付双重加价。如果你使用像 Databricks 这样的服务,你会利用云提供商的计算能力处理存储在对象存储中的文本文件,Databricks 作为中介,因此你只需为计算支付一次加价。如果使用数据仓库,你则需要支付存储和计算的加价。而且你也不知道他们究竟是如何利用这些计算来分析文档的,因此你可能会多花一点钱。如果仓库支持外部表,你可以避免前者。

为了完成这一层——高效运行 SQL 负载需要数据处于不同的特定格式。这催生了数据仓库市场,将这些数据转换为可查询和可转换的格式,如.delta。非结构化数据在某些方面正好相反,似乎属于它已经存在的地方,即数据湖。因此,在 LLM 的背景下,使用来自仓库层的计算来处理非结构化数据并没有真正的意义。确实,一些流行的 Snowflake 用例,如 Snowpark 容器服务和外部表,已经更像是一个类似于湖屋的计算/存储模型。

计算的争夺:高

数据激活——分析的“最后一公里”

最后提到的是漏斗的结束部分——数据激活。这是分析的“最后一公里”,通常涉及进行小检查以确保流程能够启动,然后将清理过的数据移至操作系统。这指的是与清理和汇总的数据进行交互的应用程序。

这些可能是经典的反向 ETL 用例、仪表盘或自动化的 Slack/电子邮件警报。

在这一点上,你的所有数据(结构化和非结构化)可能已经得到了充分的清理。你的数据工程、分析工程和产品团队可能都已经对其进行了仔细的审查、测试、清理和汇总。

这意味着从计算方面来看,真的没有什么需要做的了。

因此,非结构化数据漏斗中最不感兴趣的部分提供了最少的机会来对计算收费给数据团队。不仅所有的数据都是干净、扁平和完美的,而且也要小得多。

计算的争夺:低

结论

本文为数据从业者提供了一种在非结构化数据背景下思考数据处理的新方式。非结构化数据的独特之处在于,文本文件、HTML 文件、深度嵌套的 JSON 以及所有介于其间的内容都需要一个初步的、繁重的计算步骤来从数据中提取价值。这一步发生在哪里对大型数据云服务提供商来说很重要,因为它与使用量以及因此产生的收入密切相关。目前特别有趣的是,LLM 和 GPT 模型的部署已经创造了对这些之前对大多数数据从业者来说相当无用和无趣的数据的需求。

通过漏斗图示,我们可以看到数据团队“延迟”这个繁琐的处理步骤时间越长,成本越高,因为移动的数据总量更大。此外,考虑到各种文件格式和操作类型的需求,似乎最合理的做法是专注于在对象存储级别处理数据。这一点通过 Snowflake 显然“向上发展”以促进非结构化数据处理得到了很好的说明。

这也引出了一个问题,即云服务提供商是否会继续向上发展。解析来自 Gmail 的电子邮件可能足够简单。假设有一个专门将数据从 Gmail 移动到对象存储的工具——为什么这个工具也不能执行“T”;在这种情况下,解析出有用的信息(大概是使用 AI),并将平坦的记录表直接转储到 S3 中?是否因为大多数有用的非结构化数据存在于 Google 和 Microsoft 的云数据库中(它们既不是 Databricks 也不是 Snowflake,且拥有其自有的数据产品套件)?

我坦率地说,不知道这个问题的答案是什么。不过,我确定处理尽可能多的数据会有巨大的经济激励。处理到什么程度是一个有待讨论的问题,正如 Snowflake 成功说服公司在进行任何转换之前将数据迁移到 Snowflake 所展示的那样。BI 工具收费数十万美元的时代已经过去了。争夺 S3 的战斗已经开始🔥

RAG 的未被揭示的一面:解决领域特定搜索中的挑战

原文:towardsdatascience.com/the-untold-side-of-rag-addressing-its-challenges-in-domain-specific-searches-808956e3ecc8

通过混合搜索、层级排名和讲师嵌入,解决我们 RAG 设置中类似的领域特定文档问题。

Agustinus NalwanTowards Data Science Agustinus Nalwan

·发表于 Towards Data Science ·29 分钟阅读·2023 年 10 月 18 日

--

生成式 AI 增强的搜索技术(图像由 MidJourney 生成)

Carsales 作为领先的汽车平台,服务于澳大利亚、智利、韩国和美国的汽车和生活方式车辆市场。我们的目标是重新定义汽车买卖体验,树立无与伦比的标准。为此,我们的一个关键功能是一个全面的搜索工具,能够扫描数万篇与汽车相关的编辑文章。我们目前集成了 Google 搜索——专门为我们的编辑内容量身定制并通过 iframe 展示——结果虽然不错,但主要依赖于词汇(关键词)关联,有时忽略了搜索查询的真实本质或语义。

使用现有的 Google 搜索结果

例如,搜索“Toyota Corolla 2020 年气囊数量?”会返回包含“Toyota Corolla 和气囊”字样的任何文章等。然而,这些文章大多讨论的是气囊召回,而非实际的气囊数量。商业方面有强烈需求,不仅要从技术上提升这个工具,还要重新设计其界面,使其更加无缝地集成到我们的网站中,超越仅仅是一个 Google 搜索结果的 iframe。

在 2023 年 7 月,我们举办了我们的全球黑客马拉松之一。这项活动通常会吸引来自各部门的参与者,组建团队,提出创意,并在紧张的三天内将这些创意转化为可用的原型。随着大型语言模型(LLMs)的日益重要,我们确定了一个理想的项目:使用 LLM 改进我们的搜索工具。更棒的是,我们赢得了这次黑客马拉松!感谢我们了不起的团队,他们为此付出了极大的努力!

在本文中,我们将首先概述我们项目的基本概念。之后,我们将谈论我们的初步 RAG 方法。随后,我们将重点讨论在确保准确和相关的文档检索及生成直接答案中面临的挑战。然后,我们将讨论我们实施的解决方案来应对文档检索难题。不过,我们将把直接答案生成的讨论留到下一篇文章中,以保持本文的简洁。

概念

我们设想的模型是简单明了的。我们希望在收到搜索查询后得到两方面的结果:

  • 相关文档:虽然这与我们现有的功能相似,但我们的目标是通过检索与查询意图相符的文档(基于意义的匹配),而不仅仅是基于词汇的匹配(直接词匹配),来提高其效能。

  • 直接回答:每当查询以问题的形式提出并且我们的系统可以从编辑资源中得出答案时,我们希望呈现这一直接答案。这个新颖的功能有望显著丰富用户互动。

使用我们概念化的 RAG 方法的搜索结果

在利用大型语言模型(LLMs)进行文档检索并通过组织的知识库提供直接答案时,通常有两种方法:

  • 微调 LLM:这涉及到使用您组织独特的知识库(在我们的情况下是我们的编辑文章集合)来精炼基础 LLM。这确保了 LLM 可以准确回答与这些文章相关的问题。

  • 检索增强生成(RAG):在这里,策略是将上下文相关的编辑文章与提出的问题一同提供给 LLM,使其能够生成准确的回答。

微调直接将知识融入模型中,减少了对 LLM 的大量提示的依赖。然而,这也带来了挑战:需要不断更新 LLM 以包含新的或修订的文章,并且运行微调模型的操作成本显著增加——可能是基础 LLM 的十倍。鉴于这些,大多数人,包括我们,都更倾向于 RAG 方法。

检索增强生成

检索增强生成(RAG)可能听起来很复杂,但其原理很简单。想象一下将我们的查询分成三个部分:首先,问题本身,例如“丰田卡罗拉混合动力 2020 的燃油消耗是多少?”其次,给 LLM 的指令,使用我们接下来提供的文档来构建回答。最后,是可能找到答案的文档上下文。

RAG 简述

从本质上讲,这就是 RAG 的核心。自然地,你可能会想知道我们如何从我们库中的成千上万篇文章中筛选出相关的文章作为文档上下文来回答提出的问题。

为什么不把所有的文章都包含在文档上下文中呢?考虑到 45,000 篇文章,每篇文章有 10,000 字,总计 4.5 亿字。最新的 LLM Claude 2 仅支持 100,000 令牌的窗口。即使未来的模型扩展了容量,相关的成本和处理时间也将是巨大的。我们的目标是根据与查询的相关性对文章进行排名。文章内容与查询越相似,它越可能包含答案。因此,我们选择最相关的 X 篇文章作为我们的文档上下文。这就是为什么我们需要向量数据库的原因。

向量数据库和密集嵌入

向量数据库本质上存储的是向量,这些向量是浮点数的数组。那么这如何解决我们的挑战呢?进入密集嵌入,这是一种 NLP 技术,它将文本(我们的文档)转换为向量形式。这种向量通常更有效地 encapsulates 文本的语义本质,从而允许更准确地比较两段文本之间的相似性。

在密集嵌入出现之前,我们通过计算文档之间的常见词汇来衡量文档的相似性。让我们用一个例子来说明。给定问题“丰田卡罗拉 2020 有多少个气囊?”,以下哪一句话与其更为贴近?

丰田卡罗拉 2020 由于 气囊 线圈以及 其他 诸多问题而被召回。

丰田卡罗拉 2020 配备了七个 气囊 和一个备份摄像头。

使用简单的词匹配,第一个句子有六个常见术语,看起来更像是问题。然而,本质上,第二个句子虽然只有四个匹配的术语,但更相关,因为它涉及到关于气囊数量的查询。即使排除停用词(have 和 many),它们仍然都有四个匹配的术语。

当我们使用密集嵌入技术将问题和两个句子转换为向量并将它们可视化到二维平面上时,第二个句子与问题的接近度变得显而易见,特别是在基于余弦距离(向量之间的角度)评估相似性时。

利用这一能力,我们可以将所有文档与给定问题相关联,只选择最相似的四篇文章作为 LLM 的上下文,而不是筛选整个 45,000 篇文档。向量大小可以根据使用的密集嵌入方法而有所不同。最初,我们使用了 OpenAI 的 text-embedding-ada-002,这种方法生成了 1536 的向量大小。

这是该过程的一个详细说明:

起初,在索引阶段,我们使用密集嵌入方法将所有文章转换为向量表示,并将这些向量保存在像 Pinecone 这样的向量数据库中。当需要推断时,我们也将提出的问题转换为查询向量,使用相同的嵌入技术。这个查询向量随后被输入到 Pinecone 中,Pinecone 检索出四篇最相关的文章。Pinecone 无缝地计算查询向量与我们存储的所有文档向量之间的余弦距离,以返回最相关的四篇文档。接下来,我们将这些选定文章的内容作为文档上下文提供给 LLM,连同原始问题和一个生成回答的指示。

RAG 架构

本质上,这捕捉了增强检索生成(RAG)的精髓。大多数 RAG 实现包括某些改进,以提高所选文档的相关性,从而影响最终答案的准确性,接下来我会讨论这一点。

切分我们的文档

典型的编辑文章可能长达 10,000 字。发送四篇最相关的文档作为上下文,可能意味着处理多达 40,000 字的内容,这可能是一个资源密集型的任务,特别是在频繁调用的情况下。例如,丰田卡罗拉 2020 年的编辑评论可能涉及从安全特性和定价到燃油效率的各个方面,这些信息分布在不同的段落中。往往,特定问题的答案可以仅定位于这几段内容。这就避免了传输整个文章进行处理的必要。

实际意义是什么?与其将整个文档转换为向量,不如将其分割成较小的块(通常为一到两个段落)并将这些块转换为密集向量——这个过程被称为“文档切分”。

有趣的是,文档切分不仅能节省我们的处理时间,还能提高文档检索的准确性。你会问怎么做?

以丰田卡罗拉混合动力轿车评论的一段为例:

新款丰田卡罗拉满足了很多要求。值得一提的是,这款四门车型比其五门掀背车长 255 毫米,车长为 4630 毫米。

车内配备了许多有用的功能,如全系标配的 Apple CarPlay 和 Android Auto,并显示在一块 8 英寸的触摸屏上。所有车型还标配自适应雷达巡航控制。

尽管动力不足,这款混合动力 Corolla 轿车却非常省油。声称的燃油经济性为 3.5L/100,这使它成为该国第二高效的丰田车辆。

如观察所示,第一个段落讨论了汽车的尺寸,第二个段落详细说明了数字设备的功能,而第三个段落则专注于燃油效率。

如果我们将这些段落分别分成不同的密集向量,我们将生成三个独特的文档向量,这些向量位于不同的嵌入空间,每个向量象征着不同的主题。

假设查询是“Toyota Corolla 的尺寸是多少?”。第一个段落变成最相关的文档。对于类似“CarPlay 是否可用?”的问题,第二个段落优先考虑。

然而,将所有三个段落合并成一个密集向量会产生一个更为泛化的向量,这可能无法有效地指向特定的主题。嵌入文档越大,向量越被稀释,最终无法准确捕捉特定主题。随着时间的推移,由于密集向量包含了许多重叠的主题,如安全性或内部特征,其独特性减少,从而降低了文档检索的精度。

随着文档块变大,密集嵌入失去了其特异性

鉴于这些因素,我们将文档划分为块,每块包含 300 个标记。此外,我们还使用了 Langchain 框架中的 RecursiveCharacterTextSplitter 技术。

这种方法对文本进行层次化分类:首先优先考虑段落,然后是句子,最后是单个单词。我们的目标是尽可能在 300 个标记的范围内保留整个段落。如果不能,则将其分解为句子,然后是单词。将段落分成不同的块可能会导致丢失与当前主题相关的重要背景。

为了增强每个段落内的上下文连续性,我们确保后续段落与前面的段落重叠 100 个标记。这种策略确保了当一个长段落被分成多个段落时,这些段落之间的共享词语有助于减少潜在的上下文丢失。

例如,考虑下面的段落。

在其他地方,备受期待的 Model S 更新借鉴了较小的 Tesla Model 3 和即将推出的 Tesla Model Y 的内部元素,包括更新的 17.0 英寸中央触摸屏显示器,具备卫星导航、互联网功能,以及大量游戏、视频流媒体和特斯拉备受推崇的“复活节彩蛋”。

当段落被分成两个独立的部分而没有任何重叠内容时,后续部分将失去对讨论的具体车型的参考。

块 1:

另外,期待已久的 Model S 改款借鉴了较小的特斯拉 Model 3和即将推出的特斯拉 Model Y的内饰元素,包括更新的 17.0 英寸中控触摸屏

块 2:

中控触摸屏具备卫星导航、互联网功能,并且有大量游戏和视频流服务。

由于内容重叠(用粗体标出),上下文现在得到更好的保留。

块 1:

另外,期待已久的 Model S 改款借鉴了较小的特斯拉 Model 3和即将推出的特斯拉 Model Y的内饰元素,包括更新的 17.0 英寸中控触摸屏,具备卫星导航、互联网功能,并且有大量游戏和视频流服务。

块 2:

特斯拉 Model 3和即将推出的特斯拉 Model Y的内饰元素,包括更新的 17.0 英寸中控触摸屏,具备卫星导航、互联网功能,并且有大量游戏和视频流服务。

注入高级上下文

在我们的编辑文章中,段落通常省略了对具体车型的明确提及。这通常是因为从文章标题或早期段落中可以隐含得出。以以下第二段为例。虽然读者从之前的上下文中知道这是关于丰田卡罗拉 2020 混合动力轿车的,但该特定型号在段落中并未明确提及。设想有 20 篇不同的文章段落,每篇文章都讨论了不同的汽车但都提到了像Apple CarPlayAndroid Auto这样的共同特性。当每个段落变成单独的文档块时,它们的密集向量嵌入可能会非常相似。因此,如果用户问:“丰田卡罗拉 2020 混合动力轿车是否配备了Apple CarPlayAndroid Auto?”,查询向量可能与这 20 个文档块中的任何一个相似,导致检索困难。

新款丰田卡罗拉满足了很多要求。值得一提的是,这款四门轿车比它的五门掀背车长了 255 毫米,总长度为 4630 毫米。

内部配备了许多实用功能,如覆盖整个系列的Apple CarPlayAndroid Auto,并显示在一个 8 英寸的触摸屏显示器上。自适应雷达巡航控制也是所有车型的标准配置。

虽然动力不是很强劲,但混合动力卡罗拉轿车非常省油。宣称的燃油经济性为 3.5L/100 公里,使其成为在这个国家中第二高效的丰田车型。

为了应对这一挑战,我们在每个文档块的开头加入文章标题,通常会说明汽车的品牌、型号和发布年份。这一整合确保了密集向量嵌入反映这一关键信息。

标题:丰田卡罗拉 2020 混合动力轿车评测

新款丰田卡罗拉满足了很多要求。值得一提的是,这款四门轿车比它的五门掀背车长了 255 毫米,总长度为 4630 毫米。

标题:丰田卡罗拉 2020 混合动力轿车评测

内部配备了许多实用功能,如覆盖整个系列的Apple CarPlayAndroid Auto,并显示在一个 8 英寸的触摸屏显示器上。自适应雷达巡航控制也是所有车型的标准配置。

标题:2020 年丰田卡罗拉混合动力轿车评测

尽管起步加速不算迅猛,但混合动力卡罗拉轿车却极其省油。宣称的燃油经济性为 3.5L/100 公里,使其成为在本国可用的第二节能的丰田车辆。

采用单样本学习

大型语言模型(LLMs)在没有直接培训的任务上表现优异。将指令与示例配对通常会产生更好的结果。执行没有示例的任务称为零样本学习,而有示例的任务称为单样本学习。遵循常见的 RAG 方法论,我们使用单样本学习,提供一个示例。

引入源文章引用

我们的下一个目标是显示答案的来源。引用来源通过提供可追溯的参考来提升用户体验,促进更深入的探索和验证。这种透明度增强了用户信任,使他们能够验证答案,从而提高我们平台的信心。此外,这可以减少大型语言模型如 LLMs 中常见的“幻觉”问题,这些模型有时会提供错误的答案。通过让 LLMs 引用来源文章,我们避免了错误回答,因为它们不能引用不存在的来源。这类似于人类行为:人们在没有可验证来源的情况下会犹豫回答。以下是我们的用户提示,基于Langchain模板,指导 LLM 在 SOURCES 字段中包含来源。请确保你在回答中始终返回一个“SOURCES”部分。

我们在{question}占位符中注入了实际问题,并在{document-context}占位符中注入了我们的四个主要文档块及其来源。

Given the following extracted parts of a long document (with its source) and a question, create a final answer with references ("SOURCES"). 
If you don't know the answer, just say that you don't know. Don't try to make up an answer.
ALWAYS return a "SOURCES" part in your answer.QUESTION: Which state/country's law governs the interpretation of the contract?=========
Content: This Agreement is governed by English law and the parties submit to the exclusive jurisdiction of the English courts in  relation to any dispute (contractual or non-contractual)...
Source: 28-plContent: No Waiver. Failure or delay in exercising any right or remedy under this Agreement shall not constitute a waiver of such (or any other)  right or remedy.\n\n11.7 Severability.
Source: 30-plContent: (b) if Google believes, in good faith, that the Distributor has violated or caused Google to violate any Anti-Bribery Laws (as  defined in Clause 8.5) or that such a violation is reasonably likely to occur,
Source: 4-pl=========
FINAL ANSWER: This Agreement is governed by English law.
SOURCES: 28-plQUESTION: What did the president say about Michael Jackson?
=========
Content: Madam Speaker, Madam Vice President, our First Lady and Second Gentleman. Members of Congress and the Cabinet. Justices of the Supreme Court. My fellow Americans.
Source: 0-plContent: We have lost so much to COVID-19\. Time with one another. And worst of all, so much loss of life.
Let's use this moment to reset. Let's stop looking at COVID-19 as a partisan dividing line and see it for what it is: A God-awful disease.
Source: 24-plContent: And a proud Ukrainian people, who have known 30 years  of independence, have repeatedly shown that they will not tolerate anyone who tries to take their country backwards.
To all Americans, I will be honest with you, as I've always promised. A Russian dictator, invading a foreign country, has costs around the world. And I'm taking robust action to make sure the pain of our sanctions  is targeted at Russia's economy.
Source: 5-plContent: More support for patients and families. To get there, I call on Congress to fund ARPA-H, the Advanced Research Projects Agency for Health. It's based on DARPA-the Defense Department project that led to the Internet, GPS, and so much more.
Source: 34-pl=========
FINAL ANSWER: The president did not mention Michael Jackson.SOURCES:
QUESTION: {question}=========
{document-context}
=========FINAL ANSWER:

在实施我们的系统后,我们迫不及待地将 RAG 投入测试,期待与 LLM 的首次互动。然而,一旦初期的热情消退,我们发现了一些问题,主要涉及两个方面:文档检索的质量和生成的直接回答的准确性。这在我们几乎一半的测试案例中发生了。

说实话,直接回答相关的大部分问题源于所选文档作为上下文的质量不够充分。然而,本文将专注于文档检索挑战及我们解决这些挑战的策略。敬请关注即将发布的文章,深入探讨答案生成的复杂性。

问题

与年份不相关的汽车文章排名较高

当我们用查询“2018 年马自达 CX-9 评测”搜索 Pinecone 数据库时,系统返回了三个文档片段。令人惊讶的是,前两个结果来自马自达 CX-9 2019 年和 2017 年评测文章,而 2018 年评测(我们实际的目标)仅排名第三。

(1) 标题:2019 年马自达 CX-9:视频评测

马自达 CX-9 已经成为 Carsales 办公室的最爱。它获得了我们 2016 年年度汽车奖,并且在大多数多车比较中仍能排在前列。2019 年,CX-9 进行了更新,马自达表示,它已经解决了之前存在的一些小问题…

(2) 标题: 马自达 CX-9 2017 年评测

CX-9 型号,仅次于 Azami,GT 版本无论内外都相当豪华。二十英寸的轮毂、镀铬装饰、无钥匙进入和 LED 行车灯赋予我们的“魂红”版外观高贵…

(3) 标题: 马自达 CX-9 2018 年评测 — 澳大利亚

马自达 CX-9 系列中有一款新车型。顶级的 CX-9 Azami LE 本月将在澳大利亚上市,配备了更多的设备、技术,并且重要的是,增加了奢华感。它成为了 2019 年款马自达 CX-9 系列的亮点…

对于查询“马自达 CX-9 2019 年油耗是多少?”,系统未能提供关于马自达 CX-9 2019 年款油耗的具体结果。所有返回的文档都是不同年份的。第一个是 2010 年款的文档。讨论马自达 CX-9 2019 年油耗的文档标题为“马自达 CX-8 与马自达 CX-9 2019 年比较”。这个文档竟然排名第六,这意味着它没有作为文档上下文提供给 LLM 生成直接答案。

(1) 标题: 马自达 CX-9 现在更瘦、更轻、更亮

马自达正逐年降低 CX-9 型号的综合循环油耗。三年前发布时,CX-9 的油耗为 13.0L/100km。去年的一次升级将这一数据减少到 12.2L/100km,而昨天宣布的另一项升级正式将油耗再次降低至 11.3L/100km…

(2) 标题: 马自达 CX-9 2016 年评测

170kW、2.5 升四缸涡轮增压汽油发动机,配合六速自动变速器。没有手动选项或柴油动力系统,这也得归功于该车重心放在开发美国市场。尽管如此,这款汽油发动机仍然能够在其尺寸的车辆中实现令人印象深刻的燃油经济性,承诺综合循环油耗为 8.8L/100km…

(3) 标题: 马自达 CX-9 2018 年评测

油耗也是一个持续考虑的因素,我们已经行驶了数千公里,大多数情况下车上有两个人,油耗读数仍然徘徊在 10.0L/100km 左右。注意:这一最新的油耗数据比 CX-9 的两驱版本略高,后者的平均油耗为 9.2L/100km,这一数据来源于行程计算机和我们自己的独立测试。两者都高于其官方数据…

(6) 标题: 马自达 CX-8 与马自达 CX-9 2019 年比较

马自达 CX-9 是一款评价非常高的车辆,曾获 carsales.com.au 年度汽车奖以及其他多个奖项。但它仅配备了 170kW/420Nm 的 2.5 升四缸涡轮增压汽油发动机,搭配六速自动变速器。再加上全轮驱动——正如我们在这次测试中自然地做的——整车重量达到 1924 公斤。但马自达仍然声称其合理的油耗为 8.8L/100km。我们将在高速公路、小道和泥土路上进行测试…

文档检索未返回优先考虑文章时效性的文章

进一步检查显示,我们的文档检索系统忽略了文章的时效性,常常忽视优先考虑更新内容。

例如,搜索“丰田卡罗拉”会得到 2010 年和 2013 年的文章作为前几个结果。令人惊讶的是,关于丰田卡罗拉 2020 年的更相关文章,由于其时效性,应该更受关注,却被排在了第 15 位。

(1) 标题:丰田公布下一代卡罗拉轿车

额外的轮胎隔音。美国市场的标准配置包括八个安全气囊、蓝牙、空调、玻璃内置收音机天线、60/40 分割后座靠背以及 LED 近光灯和日间行车灯——丰田称这是小型车市场中的首创…

(2) 标题:丰田卡罗拉:2013 年小型车大测试

丰田卡罗拉 Ascent Sport(掀背车)我们喜欢的:材料质量有所提升 性能敏锐 价值令人印象深刻 不喜欢的:平坦的仪表板设计 繁琐的触摸屏 没有后座通风口 路上表现 当丰田章男在 2009 年掌控全球最大的汽车公司时,他设定了一个目标,即注入…

(15) 标题:丰田卡罗拉轿车 2020 年评测

丰田卡罗拉是全球销量最高的汽车之一——但大多数人想到(并购买)的是掀背车。卡罗拉轿车一直有些,嗯,被遗忘。但现在风向有所改变。全新的设计、更高的配置水平、更具吸引力的驾驶体验…

特定汽车品牌型号的文章排名高于通用文章

经过进一步分析,可以明显看出,像“什么是混合动力车?”这样的查询,结果很混杂。返回的第一篇文档是通用的,第二篇则介于两者之间,而第三篇和第四篇专门讨论本田混合动力车。对于此类查询,大多数人更倾向于阅读广泛讨论混合动力车概念、优点和缺点的内容,而不是特定汽车品牌或型号的具体信息。显然,我们的系统未能完全满足这些期望。

(1) 标题:什么是混合动力车?

在动物世界中,混合体是指两种不同物种或品种之间的杂交。在汽车世界中,这个术语已经演变为定义一种既由电动机驱动又由内燃机驱动的汽车。电动机承担了大部分工作,但在混合动力车的最初概念中…

(2) 标题:混合动力车即将蓬勃发展

它是一款混合动力车。然而,它不是普通的混合动力车,如普锐斯或思域。这是一款配备汽油 V6 发动机和传统后驱的汽车,具有现代外观和高科技、符合人体工程学的设计。丰田产品开发和工程经理 Doug Soden 预计,澳大利亚的混合动力车销量可能会 …

(3) 标题:本田混合动力车是一款不错的运动型车

2007 年 2 月 23 日。混合动力技术在即将召开的日内瓦车展上,霍多纳展示了一款体育车原型(如图),将混合动力技术与性能爱好者相结合。小型混合动力运动概念车使用了先进的混合动力技术 …

(4) 标题:本田思域(2001-)

未来 50–100 年。燃料电池是当前的未来趋势,但可负担的商业化仍然至少需要 20 年,因此在此期间,许多汽车制造商正走向混合动力路线。混合动力车本质上是由小型汽油发动机和额外的电动机驱动的传统汽车,电动机由可充电电池供电 …

关于错误模型的文章正在被返回

在搜索“Mazda CX-9 2023 评测”时,系统主要检索到了关于 Mazda CX-8 的文章,这是一个错误的模型,尽管这些文章来自 2023 年。一篇关于 Mazda CX-9 的 2021 年文章被藏在结果的更远处,排名第五。一般来说,用户期望看到与他们搜索的特定品牌和型号相关的文章,即使是来自前一年,而不是关于查询年份的完全不同模型的内容。

(1) 标题:Mazda CX-8 2023 评测

自 2018 年上市以来,Mazda CX-8 三排家庭 SUV 一直是 CX 系列中更受欢迎车型的配角。但随着 CX-9 在年底前停产以及一系列全新、更昂贵的 SUV 即将首发,是时候让 CX-8 登上聚光灯,显然成为了马自达最实惠的家庭车 …

(2) 标题:Mazda CX-8 2023 视频评测

Mazda CX-8 在 Mazda 全面的 CX 系列 SUV 中销售成绩一直处于末尾。但在高端车型正在进行全新一代换代的同时,获奖的 CX-9 也被停产,CX-8 现在显然成为了品牌的三排七座价格引领者 …

(5) 标题:Mazda CX-9 2021 评测

悬挂系统在调校上充分考虑了乘客的舒适度(对有孩子在车上睡觉时很好)。在隔离测试中,CX-9 在乘坐舒适性方面难以挑剔,但正如我们最近与起亚索伦托的比较所证明的那样,马自达在这一领域已经不再是领先者 …

总结一下我们文档检索系统的问题:

  • 当查询未指定车型年份时,较新的文章不会被优先考虑。

  • 系统经常根据查询中提到的年份匹配文章,即使这会牺牲对车型的匹配度。

  • 对于广泛的查询,系统有时会优先考虑特定品牌型号的文章,而非一般文章。

  • 系统无法对不同车型年份查询的相关文章进行准确排名。

认识到这些问题至关重要,这表明我们离终点还远。

经过一个集中的分析阶段,跨越了周末和深夜,我们已经锁定了主要问题。

我们的库中充满了关于相同品牌和型号的编辑文章,但来自不同年份,例如 2018 年、2019 年和 2021 年的马自达 CX-9 评测。这些文章的结构布局相似。某些部分讨论燃油效率,而其他部分则侧重于安全性或内部空间。当查询“马自达 CX-9 2019 年燃油效率?”时,会出现多个文档块,每个块都来自不同年份的马自达 CX-9 文章,涉及燃油效率。值得注意的是,每个文档块都标注了其原始文章的标题和年份。因此,可以预期“马自达 CX-8 v 马自达 CX-9 2019 比较”的密集向量与查询的密集向量相比,更加相似于“马自达 CX-9 2016 年评测”的密集向量。

然而,现实并非如此。年份仅仅是大约 300 个内容 token 中的一个单词,并没有发挥应有的作用。在评估更广泛的内容时,第一篇文章似乎比第二篇更侧重于燃油效率。密集嵌入过程未能识别年份在汽车领域中的关键作用。例如,不同年份对应着车辆的不同型号,因此在密集嵌入向量创建过程中,年份应当具有更大的权重。这一困境被标记为领域超出上下文挑战。利用基于通用数据集设计的基础嵌入模型,意味着它忽视了汽车领域的具体细节。

(1) 标题:马自达 CX-9 2016 年评测

170kW,2.5 升四缸涡轮增压汽油发动机配合六速自动变速器。没有手动选项或柴油动力系统,感谢开发此车型的重心在于美国市场,因此缺乏燃料选择。尽管如此,汽油发动机仍然能够为其大小的车辆提供令人印象深刻的燃油经济性,承诺的综合循环燃油消耗为 8.8L/100km …

(2) 标题:马自达 CX-8 v 马自达 CX-9 2019 比较

马自达 CX-9 是一款评价非常高的车辆,曾获得 carsales.com.au 年度汽车奖和其他多个奖项。但它仅配备了 170kW/420Nm 的 2.5 升四缸涡轮增压汽油发动机,并与六速自动变速器配对。加上全轮驱动——正如我们在测试中所做的——你会发现其整备质量达到 1924 公斤。但马自达仍声称其油耗为 8.8L/100km。我们将在高速公路、小路和泥土路上进行测试……

正是这个问题导致“2023 年马自达 CX-8 评测”在查询“2023 年马自达 CX-9 评测”时排名高于“2021 年马自达 CX-9 评测”。我们的标准模型未能将车型匹配优先于年份匹配,从而导致了这种差异。

一个更简单的类比是考虑“炸鸡”更类似于“鸡汤”还是“炒饭”。答案取决于上下文。如果关注的是配料,“炸鸡”与“鸡汤”更为相似。但从准备的角度来看,它更接近于“炒饭”。这样的解释是领域中心的。

许多 RAG 设置可能会绕过我们面临的挑战,因为他们的索引文章与我们的并不相似。例如,对于客户支持用例,大多数文章有不同的主题,比如“如何重置我的密码”或“如何更改我的电子邮件地址”。

解决这个问题可能需要定制我们的嵌入模型,使用以汽车为重点的数据集。然而,微调存在挑战。我们需要正样本(两个相似的句子)和负样本(两个不同的句子)数据集,这是一项巨大的工作。考虑到我们使用的是 OpenAI 的 text-embedding-ada-002 模型,微调甚至不是一个选项,因为他们尚未提供这一服务。

解决方案

我们的探索使我们找到了一种更有前景的解决方案,融合了四种策略:

  • 混合搜索(结合稠密和稀疏嵌入)

  • 层次文档排名

  • 教学稠密嵌入

  • 年份提升。

混合搜索(结合稠密和稀疏嵌入)

我们解决方案的关键在于稠密和稀疏嵌入的融合,即“混合搜索”。稀疏嵌入本质上类似于传统的关键词或词汇匹配。令人惊讶的是,这正是我们最初希望用稠密嵌入取代的传统方法。

让我们进一步讨论一下这种稀疏嵌入。

在密集嵌入的领域,每个文档块都被封装在一个 1536 维浮点向量中。然而,稀疏嵌入则将这些文档块呈现在一个扩展的维度空间中, resulting in a considerably expansive vector. 从本质上讲,基本的稀疏嵌入是一个向量,其中每个槽位代表词汇表中的一个独特词汇。如果一个词在文档块中出现,则向量中的对应槽位标记为 1;如果没有出现,则保持为 0。为了优化这一点,通常会排除“is”,“and”,“what”等停用词,以简化词汇表的大小。在我们的应用中,我们处理的词汇表大约包含 50,000 个词,相当于一个同样大小的向量。

此外,它们主要的零填充特性优化了存储和计算,因为大多数条目(为零)可以在计算过程中有效地压缩或跳过。计算两个稀疏嵌入向量之间相似度的方式与两个密集嵌入向量相似,利用的是余弦距离。

超越这种基本方法,我们遇到了像 BM25 排名 算法这样的复杂技术。虽然 BM25 的基本思想建立在广泛使用的 TF-IDF(词频-逆文档频率)方法之上——该方法根据术语在文档中的频率相对于所有文档中的频率来强调术语的重要性——BM25 对其进行了改进。BM25 不只是简单地分配二进制值或原始词频,而是为文档块中的每个术语确定一个权重。这个权重受到术语频率和逆文档频率的影响,量化了术语的相关性,使 BM25 在捕捉术语在上下文中的重要性方面更加细致。

稀疏嵌入的吸引力在于它们的准确性;它们在基于文档和查询之间的精确词对词匹配进行文档排名时表现出色。例如,使用稀疏嵌入时,查询“Mazda CX-9 2019 的燃油效率是多少?”相比于“Mazda CX-9 2016 评测”,会为“Mazda CX8 v Mazda CX-9 2019 比较”排名更高。这是因为前者有三个完全匹配的独特词汇,而后者只有两个。

然而,我们不能完全用稀疏嵌入来取代我们的密集嵌入。这样做会使我们回到最初面临的搜索系统挑战,主要是忽视语义意义。虽然密集嵌入在根据语义意义评估文档块方面表现出色,但在通过精确词匹配进行排名时却不尽如人意。相反,稀疏嵌入优先考虑精确的词匹配,但可能忽略更广泛的语义背景。因此,为了利用两种方法的优势,结合它们是至关重要的。这种融合确保了一个更全面和有效的搜索系统。

我们根据两个不同分数的加权平均值来确定文档的排名。初始分数使用与密集嵌入的余弦距离,这是一种我们之前采用的方法。同时,次级分数使用与 BM25 稀疏嵌入的余弦距离。

我们使用以下公式计算最终分数,用于文档排名:

hybrid_score = 0.75 * dense_score + 0.25 * sparse_score

sparse_score 倾向于选择那些与查询匹配的词语出现频率较高的文档。幸运的是,Pinecone 本地支持这种混合搜索机制。我们的责任是输入密集向量和稀疏向量。通过分配权重,我们强调密集/语义匹配,而不是稀疏/关键词匹配,如 0.75 对 0.25 的比例所示,这样我们可以检索到前 x 个相关的文档片段。

分层文档排名

我们的次级方法涉及一个两层的排名系统。最初,分数是基于文档片段中的内容,使用我们的混合搜索公式进行计算。随后,这些分数会得到提升,提升的分数是根据文档标题使用相同公式确定的。

理由是什么?标题虽然简洁,但通常包含了关键的信息,例如汽车的品牌、型号和生产年份,基本上是内容的浓缩总结。因此,仅根据标题对文档进行排名,比仅根据内容排名更有效,这种情况大约发生一半的时间,尤其是在稀疏嵌入的情况下。考虑查询,“Mazda CX-9 2019 的燃油效率是多少?”一篇标题为“Mazda CX-9 2019 评测”的文章将会胜过“Mazda CX-9 2018 评测”,因为前者有更多的匹配术语。

为了整合这一点,我们的公式进行了修改:

final_score = 0.5 * title_hybrid_score + content_hybrid_score

多亏了这些改进,我们已经观察到文档检索结果相关性的显著提升。例如,查询“Mazda CX-9 2019 的燃油效率是多少?”现在最终返回了来自文章“Mazda CX-8 与 Mazda CX-9 2019 比较”的正确文档片段,这归功于稀疏嵌入。

(1) 标题:Mazda CX-8 与 Mazda CX-9 2019 比较

Mazda CX-9 是一款评价非常高的车辆,曾获得 carsales.com.au 年度汽车奖以及其他多个奖项。但它只配备了 170kW/420Nm 的 2.5 升四缸涡轮增压汽油发动机,搭配六速自动变速器。再加上全轮驱动——我们在这次测试中自然也用了——你会看到一个重达 1924 公斤的车重。但 Mazda 仍然声称其合理的 8.8L/100km 的燃油消耗数据。我们将在高速公路、小路和泥泞道路上对其进行测试……

但是,仍然存在一个挑战:我们的文档检索系统并没有优先考虑基于汽车品牌/型号的匹配,而是基于型号年份的匹配。

Instructor Large Dense Embedding

问题源于我们的嵌入模型缺乏汽车行业的特定领域知识,这对于理解基于品牌/型号的匹配比基于生产年份的匹配更重要。

我们的解决方案?过渡到Instructor Large Embedding模型。这个最先进的模型提供了一个独特的优势:可以通过简单的文本提示轻松定制到特定领域或任务。除了定制能力外,该模型本身在嵌入质量上也更为优越。

仅通过向 Instructor Large 模型提供提示“为检索相关文档生成汽车评论文章问题”,它就能够巧妙地优化我们在汽车评论领域的嵌入。这一过程类似于为特定领域微调嵌入模型,但避免了相关成本。因此,我们的嵌入现在能巧妙地将汽车品牌和型号的属性置于生产年份之上。

以下代码展示了 Instructor Large 嵌入在我们特定需求中的优越性,相较于 OpenAI 的 text-embedding-ada-002。

from embedding.azure_embedding import AzureEmbedding
import numpy as npsentences = ["Mazda CX-9 2018 Review", "Mazda CX-8 2018 Review", "Mazda CX-9 2017 Review"]print("Azure Embedding")
emb = AzureEmbedding()
emb_1 = emb.embedd(sentences)
emb_2 = emb.embedd(sentences)
emb_1a = np.array(emb_1)
emb_2a = np.array(emb_2)
print(emb_1a @ emb_2a.T)
print("\n")print("Instructor-large")
from InstructorEmbedding import INSTRUCTOR
model = INSTRUCTOR('hkunlp/instructor-large')
instruction = "Represent a car review article:"
doc = [[instruction, sentence] for sentence in sentences]embeddings_1 = model.encode(doc)
embeddings_2 = model.encode(doc)
similarity = embeddings_1 @ embeddings_2.T
print(similarity)
print("\n")

我们展示了两种不同的嵌入方法,任务是为这三句话生成稠密表示:

  • Mazda CX-9 2018 Review

  • Mazda CX-8 2018 Review

  • Mazda CX-9 2017 Review

接下来,我们制作了这些句子之间的相似度矩阵(基于余弦距离评分)。text-embedding-ada-002 的结果显示,“Mazda CX-9 2018 Review”与“Mazda CX-8 2018 Review”的相似度高于与“Mazda CX-9 2017 Review”的相似度,这不是预期的结果。

text-embedding-ada-002 的相似度矩阵(绿色为好,红色为差)

相反,使用 Instructor Large 模型而不使用自定义提示时,“Mazda CX-9 2018 Review”的稠密嵌入与“Mazda CX-9 2017 Review”的相似度高于与“Mazda CX-8 2018 Review”的相似度——这是预期的结果。

Instructor Large 的相似度矩阵,未使用自定义提示(绿色为好,红色为差)

当我们加入自定义提示“为检索相关文档生成汽车评论文章问题”时,稠密向量的相似度矩阵显示了改善的结果。你是否注意到“Mazda CX-9 2018 Review”和“Mazda CX-8 2018 Review”之间的相似度评分下降了 0.2%,而“Mazda CX-9 2018 Review”和“Mazda CX-9 2017 Review”之间的评分保持不变?

Instructor Large 的相似度矩阵,使用自定义提示(绿色为好,红色为差)

此外,虽然 text-embedding-ada-002 由于其对 Azure 的外部 API 调用需要 1 秒钟的完成时间,但 Instructor Large 的速度较快,在一个合适规模的 EC2 实例上完成时间不到 150 毫秒。这显著提升了我们的整体搜索响应时间。

年份提升

还有一个最后的挑战:当查询没有指定特定汽车模型年份时,我们的文档检索系统没有优先考虑近期文章。解决这个问题很简单。我们决定给较新的文档分配额外的分数。通过精心设计,我们制定了一个二次函数,为较新的文章添加一个轻微的额外分数,随着文章的年龄增加,这个分数逐渐减少。值得注意的是,我们细致地设计了这个函数,以确保近期性的加分不会超过文档和查询之间年份匹配的相关性。这样,即使查询中提到特定年份,我们也避免了一直偏向较新的文章。

通过这些调整,我们成功解决了绝大多数文档检索问题!下面,您将找到最终的架构图。

我们最终改进的 RAG 架构

结论

我们有效解决了大部分文档检索难题。在我们的离线测试中,40%的结果更倾向于我们的新解决方案而不是现有的 Google 搜索。我们现在正在将这项技术集成到我们的前端用户体验中,准备进行 A/B 测试,以真正评估这次改进的搜索与现有版本的效率。如前所述,我们也曾面临直接答案的质量问题。虽然我们找到了一个解决方案,但我会在即将发布的博客文章中深入探讨这些细节。

我想传达的一个关键见解是,尽管检索增强生成(RAG)具有巨大的潜力,但它并不是很多文章和帖子所暗示的万能解药,尤其是在最常见的设置下。混合搜索和我们已涵盖的一些其他技术突显出作为增强相关文档检索的有效方法。不断发展的开发和研究领域承诺进一步完善这种方法,使其在未来变得更加稳健。

请通过linkedin与我联系。

所有图片,除非另有说明,均由作者提供。

本文中使用的编辑内容在网上公开可用,并且由 Carsales 拥有。

生成式人工智能负责任使用的紧迫性

原文:towardsdatascience.com/the-urgent-need-for-responsible-use-of-generative-ai-d3134605267f

为什么规模、个性化、来源不明和生成内容的传播要求我们立即行动

Heiko HotzTowards Data Science Heiko Hotz

·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 8 月 7 日

--

照片由 Google DeepMind 提供,来源于 Unsplash

这是什么内容?

“你认为负责任的生成式人工智能(GenAI)为什么重要且紧迫?”这是政策制定者、研究人员、记者和关注的公民们今天提出的问题。生成式人工智能的快速进展吸引了公众的想象力,但也提出了紧迫的伦理问题。像 ChatGPT、Bard 和 Stable Diffusion 这样的模型展示了技术的创造潜力——但在错误的手中,这些相同的能力可能会在前所未有的规模上滋生虚假信息和操控。与以往的技术不同,生成式人工智能使得创造高度个性化、特定背景的合成媒体成为可能,这些媒体很难被验证为虚假。这带来了新的社会风险和复杂的治理挑战。

在这篇博客文章中,我将深入探讨四个方面(规模与速度、个性化、来源不明、传播)来区分这个新纪元的生成式人工智能(GenAI)与以往的时代,并强调为什么现在是关注人工智能伦理和负责任使用的正确时机。在这篇文章中,我旨在通过突出关键方面来回答“为什么是现在?”这个问题。潜在的解决方案将在随后的文章中探讨。

为什么这很重要?

负责任的 GenAI 不仅仅是一个与技术专家相关的假设性问题。这是一个影响我们所有公民的问题,我们在日益复杂的信息生态系统中航行。我们如何在一个我们的眼睛和耳朵可能被欺骗的世界中保持信任和联系?如果任何人都可以制作引人注目却完全虚假的现实,社会如何达成共享的真理?如果不加以控制,GenAI 的滥用将威胁到诚信、同情和人类尊严等基础价值观。但如果我们迅速而集体地实施伦理 AI 设计,我们可以实现生成技术在创造力、联系和社会公益方面的巨大潜力。通过发声和传播意识,我们可以影响 AI 的发展方向。

规模和速度

生成模型使得以惊人的规模、前所未有的速度和简易性创建逼真的虚假内容成为可能。一个人只需简单的提示和点击即可生成无尽的定制音频、视频、图像和文本。这引入了制造操控内容的全新效率和产量水平。人类团队无法与全天候生成无尽定制虚假内容的 AI 系统竞争。只要有足够的计算能力,恶意行为者就可以通过纯粹的人工虚假量淹没社交网络,完全主导真实的声音。随着生成模型变得更易获得和更具说服力,组织大规模虚假信息活动不再需要太多专业知识或资源。

这当然不是一个新现象。例如,Twitter 机器人已经存在相当长时间,它们大约占所有推文的25%,即每天约 2.15 亿条推文。但随着 GenAI 的进步,区分机器人生成内容和人类内容将变得越来越具有挑战性。

个性化

GenAI 可以制作精准利用个体脆弱性和经历的内容。这使得心理操控比泛泛的虚假信息更为强大。设计得极具个人背景共鸣的虚假信息通过破坏共享的真理和现实观念来颠覆人类话语。当任何人都可以被灌输自己独特的一套 AI 制造的“事实”时,社会如何达成共识?这种个性化风险会导致两极化和部落主义,侵蚀群体之间的同情心和联系。

这当然是在即将到来的 2024 年美国大选背景下的热门话题。例如,在 2023 年 5 月,特朗普在他的社交媒体平台 Truth Social 上分享了一段伪造的视频,该视频中是 CNN 主播安德森·库珀。路透社深刻地指出

欢迎来到美国 2024 年总统竞选,这里的现实似乎变得难以把握。

来源

与早期的 Photoshop 等技术不同,通过法医分析验证生成伪造品极为困难。模糊的来源使恶意行为者获得了似是而非的辩解权和侵蚀客观真理的自由。即使是尽职尽责的人也难以实际验证他们遇到的所有生成内容的来源。这种不对称性使得即使生成内容在仔细检查时不够逼真,也能传播虚假信息。

在 2022 年,一段深度伪造的视频被制作出来,似乎显示了乌克兰总统泽连斯基向俄罗斯军队投降。这段视频在社交媒体上广泛传播,导致一些人相信泽连斯基实际上已经投降。视频模糊的来源使得判断其真实性变得困难,即使是尽职尽责的人也很难验证其来源。视频来源的模糊性使得它能够广泛传播,即使有些人可能会在详细查看时质疑其真实性。

扩散

一旦由生成模型制作的高度逼真的伪造品出现,它们可以通过社交网络、消息应用和其他数字平台迅速传播。

尽管这与“规模与速度”部分相关,但从不同角度看这个问题至关重要:深度伪造通常设计得很具情感吸引力。它们可能展示令人震惊、丑闻性的或其他引人注目的内容。这使得它们更容易在社交媒体上被分享,在那里,人们不断寻找新的有趣内容。看到深度伪造的人越多,越有可能有人相信它是真的。即使每个单独的伪造品可能无法欺骗仔细的审查,但大规模传播的数量会压倒追踪和反击虚假信息的努力。病毒式传播赋予生成伪造品一种难以控制的传播范围和影响力。一旦它们“在野外”传播,平台已经难以处理更简单的虚假信息——由生成 AI 创建的内容则更高难度。

举一个具体的例子:在 2023 年 3 月,一张 AI 生成的教皇方济各的照片在社交媒体上病毒式传播,一条推文获得了近 2100 万次观看——它甚至获得了“巴伦西亚教皇”的昵称。根据纽约邮报的报道,创作该图像的艺术家对这些关注并不感兴趣,恰恰相反:

被指生成该图片的 AI 艺术家 Pablo Xavier 表示,他“并不希望[这些图片]如此轰动”,并承认“人们在不加质疑的情况下认为它是真的,这确实令人害怕。”

结论

这些前所未有的能力——规模与速度、个性化、模糊来源和扩散——从根本上改变了虚假信息的性质,迫切需要辩论。我们如何应对一种能够大规模操控人们并摧毁共识现实的技术?需要什么治理措施才能在在线话语中保持信任和真相?我们能否遏制生成 AI 的有害应用,同时培养有益的应用?没有简单的答案,但现在进行诚恳的讨论对将这项技术引向伦理结果至关重要。

在一个由人工智能驱动的世界中,维护人类自主权、尊严和我们共享的现实是至关重要的。随着生成模型变得越来越强大和易于获得,我们需要伦理性护栏和智能治理,以防止反乌托邦的结果。必须迅速而深思熟虑地采取行动——在假设性风险通过自动化和人工智能操控扩散成现实之前。现在是塑造生成 AI 未来的最佳时机,以实现公正和有益的方向。

Heiko Hotz

👋 在MediumLinkedIn关注我,阅读更多关于生成 AI、机器学习和自然语言处理的内容。

👥 如果你在伦敦,请加入我们的NLP London Meetups

📔 我对 AI 新闻的看法在😇 Naughty Neural

作者提供的图片

正则表达式在数据科学中的实用性

原文:towardsdatascience.com/the-utility-of-regular-expressions-in-data-science-babf94285966

Python 中常见应用的示例

Thomas A DorferTowards Data Science Thomas A Dorfer

·发表于 Towards Data Science ·7 分钟阅读·2023 年 1 月 5 日

--

图片由 Kevin Ku 提供,来源于 Unsplash

介绍

数据科学家经常发现自己需要确定数据中的特定字段是否符合所需的文本格式,或特定字符串是否存在。在其他情况下,他们可能需要将数据中的特定字符串替换为另一个。为实现这一点,他们使用已成为这类问题常规的正则表达式

本文将简要介绍什么是正则表达式,介绍一些形成相应搜索模式所需的基本字符,展示一些 Python 中常用的函数,并最后讨论一些数据科学家日常生活中经常遇到的实际使用案例。

什么是正则表达式?

正则表达式,或称 Regex,是一组字符,用于启用特定文本模式的搜索和——如果需要——替换。这是一种极其方便的技术,数据科学家可以利用它来避免繁琐的手动搜索任务。

为了定义这些搜索模式,必须熟悉相应的语法。Dataquest 提供了一个全面且简洁的 Python 正则表达式语法备忘单。虽然本文不会涵盖所有使用案例,但仍值得强调一些用于定义搜索模式的基本字符(见表 1)。

表 1:定义正则表达式搜索模式的基本字符。作者提供的图示,灵感来源于 Dataquest

Python 中的正则表达式

Python 提供了一个名为re的模块,该模块提供了一整套丰富的正则表达式匹配操作。在这里,我们将重点介绍四个常用的模块函数:

  1. 搜索: 匹配表达式的第一个实例。

  2. 查找所有: 匹配表达式的所有实例。

  3. 替换: 用另一个字符串替换指定的字符串。

  4. 拆分: 根据指定的分隔符拆分字符串。

让我们通过一些具体的例子来进一步说明这些概念:

import re

# Defining the string upon which we'll run some regex operations
s = "Paris is the capital of France. Also, Paris is a beautiful city."

# Search
re.search("Paris", s)
>>> <re.Match object; span=(0, 5), match='Paris'>

# Find All
re.findall("Paris", s)
>>> ['Paris', 'Paris']

# Sub
re.sub("Paris", "Rome", s)
>>> 'Rome is the capital of France. Also, Rome is a beautiful city.'

# Split
re.split("\. ", s)
>>> ['Paris is the capital of France', 'Also, Paris is a beautiful city.']

正如预期的那样,search 函数只返回表达式的第一个实例作为 re.Match 对象。另一方面,findall 函数匹配表达式的所有实例并将它们以列表形式返回。使用 sub 函数,可以将特定字符串替换为另一个字符串——在这种情况下,Paris 被替换为 Rome。最后,可以使用 split 函数并指定特定的分隔符来拆分字符串——在这种情况下,是句点符号。请注意,我们必须在句点前使用转义字符 \ 来转义它在正则表达式中的特殊含义。

数据科学中的实用性

基于模式的搜索是数据科学家经常遇到的一个概念。让我们通过一些例子来突显将正则表达式作为工具放入工具箱中的实用性。

验证文本格式

一个相当常见的场景是表格数据的分析,其任务是检查某一列中的字段是否符合所需的格式。假设我们在一个名为 sample.csv 的文件中有一些虚构数据,该文件包含以下列:电子邮件地址、城市、州和邮政编码(表 2)。

表 2:包含电子邮件和地理信息的表格化的虚构数据。

现在,我们希望验证所有电子邮件地址是否符合 firstname.lastname@gmail.com 的格式。利用正则表达式,我们可以进行以下操作:

import re
import pandas as pd

df = pd.read_csv("sample.csv")

# Define pattern that matches the email format firstname.lastname@gmail.com
pattern = "^[a-z]+\.+[a-z]+@gmail.com$"

# Iterate through rows and print those that do not match
for index, row in df.iterrows():
    match = re.search(pattern, row.Email)
    if match == None:
        print(index, *row)

# Output
>>> 0 johnsmith@gmail.com Pasadena California 91103
>>> 3 matt.hawkins@hotmail.com Kailua Hawaii 96734

如果模式与字符串输入不匹配,search 函数将返回 None。如果我们打印出这些场景,我们可以立即发现第 0 行的电子邮件地址的名字和姓氏没有用句点分隔,而第 3 行的电子邮件地址使用了 Hotmail 而不是 Gmail。

以类似的方式,我们可以验证数据中的邮政编码是否符合标准的五位数字格式。

# Define pattern that matches 5-character strings comprised of all digits
pattern = "^\d{5}$" 

for index, row in df.iterrows():
    match = re.search(pattern, row.ZIP)
    if match == None:
        print(index, *row)

# Output
>>> 2 Kirkland Washington 9560

我们可以看到在第 2 行,邮政编码是错误的,因为它只有四位数字。

相同的原理可以应用于各种格式特定的字段输入,如日期、网址、电话号码、IP 地址、社会安全号码等。

检测模糊词

可以对文本进行分析,以查找某些短语、单词或其模糊形式。例如,垃圾邮件发送者倾向于模糊特定的单词,希望绕过垃圾邮件过滤器。例如,可以使用j@ckpot, jackp0t,j@ackp0t来代替jackpot。正则表达式可以确保不仅捕获感兴趣的单词,还能捕获一些其模糊形式:

import re

# Define pattern capable of handling some obfuscations of "jackpot"
pattern = "^j[a@]ckp[o0]t$"

re.search(pattern, "j@ckpot")
re.search(pattern, "jackp0t")
re.search(pattern, "j@ckp0t")

# Outputs
>>> <re.Match object; span=(0, 7), match='j@ckpot'>
>>> <re.Match object; span=(0, 7), match='jackp0t'>
>>> <re.Match object; span=(0, 7), match='j@ckp0t'>

在这里,我们定义了一组字符,用于在特定位置进行匹配,例如位置 1 的[a@]和位置 5 的[o0]。因此,我们上面提到的所有jackpot的模糊匹配都将被识别。

正确的格式或拼写

假设我们有一些文本数据,其中包含以美国格式表示的日期,即MM/DD/YYYY。然而,我们希望将这些数据中的所有日期转换为更广泛使用的格式DD/MM/YYYY。同样,可以应用正则表达式来解决这个问题:

import re

string = "I have a dentist appointment on 04/06/2023 at 10:00 AM."

# Define pattern capable of capturing data format
pattern = "(\d{1,2}/)(\d{1,2}/)(\d{4})"

# Substitute days and months
re.sub(pattern, r"\2\1\3", string)

# Output
>>> 'I have a dentist appointment on 06/04/2023 at 10:00 AM.'

我们成功地将04/06/2023替换为06/04/2023。具体来说,我们使用( )对对应于月份(组 1)、日期(组 2)和年份(组 3)的表达式进行分组。为了交换月份和日期,我们只需在替换参数中指定所需的组顺序——在我们的例子中是r"\2\1\3"。请注意,我们在这里使用原始字符串表示法,以避免在没有r前缀的字符串字面量中出现的反斜杠特殊处理。

类似的方法可以用于纠正文本中的拼写错误,例如将gray替换为grey,或反之。

计算生物学中的应用

另一个广泛使用正则表达式的领域是计算生物学。蛋白质由一串氨基酸组成,这些氨基酸在折叠成最终的三维结构后,通常在我们体内发挥特定的功能。检测特定结构模式的存在或缺失——一种与特定功能相关的短氨基酸模式——对于更好地理解其最终功能可能至关重要。

假设我们有一组短蛋白质,我们希望找到那些包含以下模式的蛋白质:xC[DA]GG{Y}。这是一种生物学符号,解释如下:任何氨基酸(x),后跟半胱氨酸(C),后跟天冬氨酸(D)或丙氨酸(A),后跟两个连续的甘氨酸(G),后跟任何氨基酸但不包括酪氨酸(Y)。使用 Python 中的正则表达式,我们可以检测到包含此模式的蛋白质,如下所示:

import re

# Define pattern capable of matching the above-mentioned motif
pattern = ".C[DA]GG[^Y]"

proteins = ['AARKYL', 'LELCDGGPG', 'RAAANCDD', 'LYYRCAGGEGPGG', 'CAEELR']  

for prot in proteins:
    match = re.search(p, prot)
    if match:
        print(prot, match)

# Output
>>> LELCDGGPG <re.Match object; span=(2, 8), match='LCDGGP'>
>>> LYYRCAGGEGPGG <re.Match object; span=(3, 9), match='RCAGGE'>

这种方法可以正确识别出含有我们寻找的特定结构的两个蛋白质。

结论

正如我们所看到的,正则表达式在各种学科中都非常有用,从电子邮件垃圾过滤器到计算生物学。本文仅展示了正则表达式常见应用的几个场景,但其适用范围实际上非常广泛。尽管正则表达式有其局限性,并且肯定不是解决所有文本处理问题的万灵药,但它们应该被视为每个数据科学家工具箱中的基本工具。

参考文献

[1] 数据科学备忘单。Dataquest。检索日期:2023 年 1 月 4 日,网址:www.dataquest.io/wp-content/uploads/2019/03/python-regular-expressions-cheat-sheet.pdf

[2] 正则表达式操作。Python。检索日期:2023 年 1 月 4 日,网址:docs.python.org/3/library/re.html

使用 Q-learning 的强化学习中行动的价值

原文:towardsdatascience.com/the-values-of-actions-in-reinforcement-learning-using-q-learning-cb4b03be5c81

使用 Python 从头实现的 Q-learning 算法

Eligijus BujokasTowards Data Science Eligijus Bujokas

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

--

代理在迷宫中穿越;GIF 由作者提供

这篇文章是关于强化学习(RL)系列文章的续集。请在这里查看其他文章:

## 使用 Python 进行强化学习的第一步

如何在强化学习的基本世界之一中找到最佳位置的原始 Python 实现…

towardsdatascience.com ## 使用 Python 的时间差分 — 第一个基于样本的强化学习算法

使用 Python 编写和理解 TD(0) 算法

towardsdatascience.com

所有使用的代码可以在这里查看:github.com/Eligijus112/rl-snake-game

包含所有绘图函数和代理训练代码的笔记本可以在这里查看:github.com/Eligijus112/rl-snake-game/blob/master/chapter-6-qlearning.ipynb

在这篇文章中,我将向读者介绍 Q 值的概念。为了直观起见,读者可以将 Q 替换为 Quality。q 值是数值,分配给 每个动作每个状态 中的得分:

Q 值函数

在给定状态下,某个特定动作的 得分越高,代理执行该动作的 效果越好

例如,如果我们可以从状态 1 选择左移或右移,那么如果

Q(left, 1) = 3.187

Q(right, 1) = 6.588

那么从状态 1 出发,能够带来更多价值的更好动作就是“正确”的动作。

存储 q 值的对象是q-tableq-table 是一个矩阵,每一行是一个状态,每一列是一个动作。我们将这种矩阵称为Q

从之前的文章中,让我们回顾一下 Q 学习中需要的一些其他重要表格:

S — 状态矩阵,用于索引所有状态。

R — 奖励矩阵,指示过渡到给定状态时获得的奖励。

Q 学习中不需要价值函数 V,因为我们不仅关心状态的价值,还关心状态-动作对的价值。

想象一下我们有以下 48 状态的迷宫:

迷宫;作者拍摄

黄色状态是我们代理的起始状态(状态 1)。

绿色状态是目标状态(状态 38)。

红色状态是迷宫的墙壁。如果代理选择去墙壁状态,它将返回到最后一个状态且不获得奖励。离开边界的逻辑也适用。

我们的代理可以采取的动作由向量 [0, 1, 2, 3] 表示,对应于 [上, 下, 左, 右]

该代理的初始 Q 表如下:

48x4 矩阵;作者拍摄

有 48 行表示每个状态。

有 4 列表示代理在每一步可以采取的 4 种动作:上、下、左或右

Q 学习算法的主要目标是 填充上述矩阵,以便我们的代理学习迷宫中最优的路径

我们将使用自定义的 Agent 类来实现 Q 学习算法:

class Agent:
    def __init__(
        self, 
        nrow_maze: int,
        ncol_maze: int,
        actions: list = [0, 1, 2, 3],
        rewards: dict = {
            'step': 0.0, 
            'wall': 0.0,
            'goal': 10,
        }, 
        gamma: float = 0.9,
        alpha: float = 0.1,
        epsilon: float = 0.1,
        seed: int = 42,
        ) -> None:
        """
        Creates an agent for the maze environment.

        Parameters
        ----------
        nrow_maze : int
            The number of rows in the maze.
        ncol_maze : int
            The number of columns in the maze.
        actions : list, optional
            A list of actions that the agent can take. The default is [0, 1, 2, 3]. 
            0: Up
            1: Down
            2: Left
            3: Right
        rewards : dict, optional
            A dictionary of rewards for the agent. The default is {'step': -1, 'wall': -10, 'goal': 10}.
        gamma : float, optional
            The discount factor. The default is 0.9.
        alpha : float, optional
            The learning rate. The default is 0.1.
        epsilon : float, optional
            The exploration rate. The default is 0.1.
        seed : int, optional
            The seed for the random generator. The default is 42.
        """
        self.nrow_maze = nrow_maze
        self.ncol_maze = ncol_maze
        self.rewards = rewards
        self.gamma = gamma
        self.alpha = alpha
        self.epsilon = epsilon
        self.seed = seed
        self.actions = actions

        # By default, the starting index is 0 0 
        self.start_state = 0

        # By default, the goal index is the last index
        self.goal_state = nrow_maze * ncol_maze - 1

        # Creating the random generator with a fixed seed
        self.random_generator = np.random.default_rng(seed)

        # Creating the maze; We will denote it internaly as S 
        self.init_S_table()

        # Initiating the Q-table 
        self.init_Q_table()

        # Saving the initial past_action and past_state
        self.past_action = None
        self.past_state = None

        # Creating the action name dictionary 
        self.action_name_dict = {
            0: 'up',
            1: 'down',
            2: 'left',
            3: 'right',
        }

        # Counter for the number of times our agent has seen the terminal state
        self.num_goal_reached = 0

        # Counter for each state and how many times the agent visited each 
        self.state_visit_counter = {}

        # Empty dictionary of states visition paths
        self.state_visit_paths = {}

        # Placeholder for the current episode of learning 
        self.current_episode = 0

  #####
  # OTHER METHODS BELLOW
  #####

完整的 Q 学习算法如下¹

Q 学习算法;作者拍摄

在步骤 2.2.1 中的 epsilon-贪婪策略 是以 1 - epsilon 的概率采取 Q 值最大的动作,并以 epsilon 的概率采取随机动作。

上述策略通过以下代码在我们的代理中实现:

def argmax(self, q_values: np.array):
        """argmax with random tie-breaking
        Args:
            q_values (Numpy array): the array of action values
        Returns:
            action (int): an action with the highest value
        """
        top = float("-inf")
        ties = []

        for i in range(len(q_values)):
            if q_values[i] > top:
                top = q_values[i]
                ties = []

            if q_values[i] == top:
                ties.append(i)

        return self.random_generator.choice(ties)

    def get_greedy_action(self, state: int) -> int:
        """
        Returns the greedy action given the current state
        """
        # Getting the q values for the current state
        q_values = self.Q[state]

        # Getting the greedy action
        greedy_action = self.argmax(q_values)

        # Returning the greedy action
        return greedy_action

    def get_epsilon_greedy_action(self, state: int) -> int: 
        """
        Returns an epsilon greedy action
        """
        if self.random_generator.random() < self.epsilon:
            return self.get_action()
        else:
            return self.get_greedy_action(state)

Q 学习步骤是 2.2.3 步骤。在每个状态下,我们的智能体采取一个动作。然后,通过更新智能体所在的当前状态和状态-动作对Q(S, A)来完成学习。更新规则中最重要的部分是,我们查看智能体通过采取动作最终到达的状态,然后从 Q 表中提取该状态的最大值。

让我们更仔细地检查这个方程:

Q 值更新方程

Q(S, A)是智能体所在的状态以及他采取的动作。

转换状态的最大值

部分是我们智能体在所有动作中最终到达的状态的最大可用 Q 值。

r 是过渡到给定状态的奖励。

其他所有的是用户定义的超参数。

由于我们使用算法估计值来更新 Q 值,因此 Q 学习属于自举方法家族。

每次我们的智能体移动后,Q 表都会更新。

我们在 Agent 类中 2.2.3 步骤的完整实现如下:

def update_Q_table(self, new_state: int): 
        """
        Function that applies the RL update function
        """ 
        # Getting the next_state's reward
        reward = self.reward_dict[new_state]

        # Saving the current Q value
        current_Q = self.Q[self.past_state][self.past_action]

        # If the new state is the terminal state or the wall state, then the max_Q is 0
        max_Q = 0

        # Else we get the max Q value for the new state
        if new_state != self.goal_state:
            new_state_Q_values = self.Q[new_state]

            # Getting the max Q value 
            max_Q = np.max(new_state_Q_values)

        # Updating inplace the Q value 
        self.Q[self.past_state][self.past_action] = current_Q + self.alpha * (reward + self.gamma * max_Q - current_Q)

上述函数在每次智能体移动时都会被调用:

 def terminal_step(self, new_state: int):
        """
        Updates the agent one last time and resets the agent to the starting position
        """ 
        # Updating the Q table
        self.update_Q_table(new_state)

        # Resetting the agent
        self.past_state = self.start_state
        self.past_action = self.get_epsilon_greedy_action(self.past_state)

        # Incrementing the number of episodes
        self.current_episode += 1

    def get_next_state(self, s: int, action: int) -> int: 
          """
          Given the current state and the current action, returns the next state index
          """
          # Getting the state coordinates
          s_row, s_col = self.get_state_coords(s)

          # Setting the boolean indicating that we have reached the terminal state 
          reached_terminal = False

          # Getting the next state
          next_state = -1
          if action == 0:
              next_state = self.get_state_index(s_row - 1, s_col)
          elif action == 1:
              next_state = self.get_state_index(s_row + 1, s_col)
          elif action == 2:
              next_state = self.get_state_index(s_row, s_col - 1)
          elif action == 3:
              next_state = self.get_state_index(s_row, s_col + 1)

          # If next_state is a wall or the agent is out of bounds, we will stay in the same state
          if (next_state == -1) or (next_state in self.wall_states):
              return s, reached_terminal

          # If next_state is the goal state, we will return to the starting state
          if next_state == self.goal_state:
              # Incrementing the number of times our agent has reached the goal state
              self.num_goal_reached += 1
              reached_terminal = True

          # Returning the next state
          return next_state, reached_terminal

    def move_agent(self): 
        """ 
        The function that moves the agent to the next state
        """
        # Getting the next state
        next_state, reached_terminal = self.get_next_state(self.past_state, self.past_action)

        # Updating the Q table
        if not reached_terminal:
            # Checking if the past_state is the same as the next_state; If that is true, it means our agent hit a wall 
            # or went out of bounds
            if self.past_state != next_state:
                self.update_Q_table(next_state)

            # Setting the past_state as the next_state
            self.past_state = next_state

            # Getting the next action
            self.past_action = self.get_epsilon_greedy_action(self.past_state)
        else: 
            self.terminal_step(next_state)

上面的代码片段应从下往上阅读。

在每次移动时,我们检查是否处于终止状态。如果智能体进入终止状态,Q 学习更新方程简化为:

终止状态的更新;作者提供的照片

让我们启动我们的智能体,训练一个回合并可视化智能体路径:

 def train_episodes(self, num_episodes: int):
      """
      Function that trains the agent for one episode
      """
      # Calculating the episode number to end the training 
      end_episode = self.current_episode + num_episodes - 1

      # Moving the agent until we reach the goal state
      while self.current_episode != end_episode:
          self.move_agent()
# Creating an agent object
agent = Agent(
    nrow_maze=6,
    ncol_maze=8,
    seed=6,
    rewards={'step': 0, 'goal': 10}
)

# Initiating the maze 
agent.init_maze(maze_density=11)

# Training the agent for one episode
agent.train_episodes(num_episodes=1)

智能体在迷宫中游荡;作者提供的 GIF

智能体需要 94 步才能到达目标。在每一步,智能体以 epsilon 贪婪的方式选择一个动作。在第一次迭代中,任何转换状态的 Q 值都是 0,因此 epsilon 贪婪算法与随机游荡相同。

让我们在一个回合后检查 Q 表。Q 表中所有的值都是零,只有状态 30 的值例外:

agent.Q[30]

# Returns 
# array([0., 1., 0., 0.])

Q(30, 1)(意味着从状态 30 向“下”移动)值为 1。计算这个值的方程是:

请记住,初始Q(30, 1) = 0.

在一个回合后,我们只学到了一个 Q 值。让我们再训练一个回合:

# Training the agent for one episode
agent.train_episodes(num_episodes=1)

# Printing out the agent's 37 state
agent.Q[37]

# Returns 
# array([0., 0., 0., 1.])

现在智能体从迷宫的另一边游荡,学到从状态 37 向右移动是最佳选择。

我们希望看到的是,随着回合的进行,我们的智能体的步骤数开始减少。

# Creating an agent object
agent = Agent(
    nrow_maze=6,
    ncol_maze=8,
    seed=6,
    rewards={'step': 0, 'goal': 10}
)

# Initiating the maze 
agent.init_maze(maze_density=11)

# Letting the agent wonder for 1000 episodes
agent.train_episodes(100)
state_visits = agent.state_visit_paths
steps = [len(state_visits[episode]) for episode in state_visits]

# Ploting the number of steps per episode
plt.plot(steps)
plt.title("Number of steps per episode")
plt.xlabel("Episode")
plt.ylabel("Number of steps")
plt.show()

步骤数与回合数的关系;作者提供的照片

在初步探索后,到第 20 集时,代理拥有一个稳定的策略,并需要大约 10 步从起始位置到达终点位置。变异发生是因为我们仍在使用 epsilon 贪婪算法进行移动,而 10%的时间会选择随机动作。

现在,最佳贪婪策略如下所示:

最优策略;作者拍摄的照片。

此外,我们的代理跟踪它在任何给定状态下出现的次数。我们可以绘制这些数据,以查看在训练阶段哪些状态最受欢迎:

状态访问;作者拍摄的照片

我们可以看到,代理经常在起始状态的左右徘徊。但是,由于我们让代理只有 10%的时间采取随机动作,因此主要路径是贪婪的,也就是说,代理选择具有最大 Q 值的动作。

最后,我们可以绘制最终的代理遍历路径:

代理遍历;作者制作的 GIF

总结:

  • Q 学习算法在代理执行每个动作后更新 Q 表中的值。

  • Q 学习是一种自举算法,因为它使用自己的估计来更新 Q 值。

  • 在 Q 学习中,我们只需要状态、奖励和 Q 表来实现整个算法。

  • Q 学习中的主要更新规则是:

Q 值更新方程

编程愉快,学习愉快!

[1]

  • 作者:理查德·S·萨顿,安德鲁·G·巴托

  • 年份:2018

  • 页码:131

  • 标题:强化学习:导论

  • 网址:http://archive.ics.uci.edu/ml

世界地图的多种面貌——地图投影

原文:towardsdatascience.com/the-world-map-with-many-faces-map-projections-f58a210ff2f7

作者提供的图像。

米兰·贾诺索夫面向数据科学 米兰·贾诺索夫

·发表于 面向数据科学 ·5 分钟阅读·2023 年 10 月 1 日

--

在这篇简短的文章中,我回顾了什么是地图投影,并展示了一系列使用 Python 和自然地球数据投影世界地图的例子。

地球大致上是一个球体,确实是一个三维物体(尽管也有一些挑战),而我们的印刷地图和数字屏幕则是二维的。将球体转换为二维地图的中间步骤,无论是制图册还是高级 GIS 应用程序,都称为地图投影。

将地球的椭球面映射到平面上有很多方法;然而,由于这些是近似模型,通常会有一些不足之处需要注意。在某些投影中,相对角度和多边形(例如国家)形状被保留;而在其他投影中,真实的面积或特定的欧几里得距离被保持不变。这些属性也有助于你选择适合你使用情况的最佳投影。

关于投影类型,有多种方式,例如圆柱投影、圆锥投影、方位投影和伪圆柱投影。相互之间转换的实际方法是改变坐标参考系统(CRS),这时 Python 和 PyProj 以及 GeoPandas 库非常有用!

根据 Mathematics.com,投影类型主要有六种类别:

  • 圆柱投影

  • 伪圆柱投影

  • 方位投影

  • 凸透镜投影

  • 杂项

在这里,我不会严格按照这样的分类,而是展示九种我认为有趣的地图投影。根据具体的投影,这些步骤通常会导致形状、面积、距离或方向的失真,因此在实际项目中应谨慎选择。为此,这个 投影系统集合 和下面提供的我的代码应该会有所帮助。那么,不再多说,地图如下:

1. 埃克特 II 投影

特征:埃克特 II 投影是一种等面积伪圆柱投影。它保持面积的准确性,但扭曲形状和距离。

常见用途:主要用于展示直线等面积网格的世界新奇地图。

埃克特 II 投影。图片由作者提供。

2. 等距圆柱投影

特征:等距圆柱投影是一种简单的圆柱投影。它保持纬度和经度为直线,但在远离赤道时会扭曲形状和面积。

常见 用途:通常用于教育或一般参考及主题世界地图。

等距圆柱投影。图片由作者提供。

3. 拉格朗日投影

特征:拉格朗日投影是圆锥形且符合变形的。失真发生在面积、形状和方向上。常见用途:根据 ChatGPT,它的稀有用例主要覆盖海洋学。

拉格朗日投影。图片由作者提供。

4. 拉里维投影

特征:根据这个 来源,在地图中心不失真,而在大部分情况下会出现面积膨胀的失真

拉里维投影。图片由作者提供。

5. 莫尔维德投影

特征:莫尔维德投影是一种等面积伪圆柱投影。它保持面积但扭曲形状和角度。它具有独特的椭圆形状。

常见 用途:当面积准确性至关重要时用于全球地图,尤其是在地理学和地球物理学中。

莫尔维德投影。图片由作者提供。

6. 自然地球投影

特征:自然地球投影是一种为世界地图设计的伪圆柱投影。它在大小和形状失真上保持平衡(但不保持任何一方面),提供了一个视觉上令人愉悦的地图。

常见 用途:由于其平衡的失真特性,它在世界地图和地图集上非常受欢迎。

自然地球投影。图片由作者提供。

7. 四次阿萨尔投影

特点:Quartic Authalic 投影是一种伪圆柱等面积投影,准确保持面积,但形状、角度和距离会失真。

常见用途:用于需要准确面积表示的特殊制图应用。

Quartic Authalic 投影。图片由作者提供。

8. 矩形多锥投影

特点:矩形多锥投影是一种最小化经线失真的圆锥投影。有时称为战争办公室投影。

常见用途:主要用于美国军事目的。

矩形多锥投影。图片由作者提供。

9. 正弦投影

特点:伪圆柱等面积地图投影,将极点表示为点,保持面积,但形状会失真。

常见 用途:主要建议用于绘制接近赤道的区域。

最后,作为可视化的数据源,我使用了来自 Natural Earth 的以下两个数据集:

现在,让我们看看代码:

import geopandas as gpd
import matplotlib.pyplot as plt
import pyproj
world = gpd.read_file('ne_10m_admin_0_countries')
ocean = gpd.read_file('ne_10m_ocean')
# visualize all projections at once
projection_dict = { 'Eckert II': '+proj=eck2 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs',
                    'Equirectangular': '+proj=eqc +lon_0=0 +lat_ts=0 +x_0=0 +y_0=0 +a=6378137 +b=6378137 +units=m +no_defs',
                    'Lagrange' : '+proj=lagrng',
                    'Larrivee' : '+proj=larr',
                    'Mollweide': '+proj=moll +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs',
                    'Natural Earth': '+proj=eqearth +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs',
                    'Quartic Authalic': '+proj=qua_aut +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs',
                    'Rectangular Polyconic': '+proj=poly +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs',
                    'Sinusoidal' : '+proj=sinu +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'
                  }

f, ax = plt.subplots(3,3,figsize=(15,15))
indicies = [(i, j) for i in range(3) for j in range(3)]

for idx, (projection_name, proj4_string) in enumerate(projection_dict.items()):

    bx = ax[indicies[idx]] 
    world_projected = world.to_crs(proj4_string)
    ocean_projected = ocean.to_crs(proj4_string)
    ocean_projected.plot(ax=bx, color = 'lightblue')
    world_projected.plot(ax=bx, cmap='Greens', edgecolor='k', linewidth = 0.25, alpha = 0.9)
    bx.set_title(f'{projection_name} Projection')
    bx.axis('off')  

plt.tight_layout()

SQL 查询优化的世界

原文:towardsdatascience.com/the-world-of-sql-query-optimization-e1d5b5fef20d

照片由Jake Blucker拍摄,发布在Unsplash上。

数据工程

一窥不同的查询优化器及其工作原理

Kovid RatheeTowards Data Science Kovid Rathee

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

--

SQL 是一种简单的语言,规则非常少,这使得它如此受欢迎。它还具有大量的关键字和功能,让你以各种方式与数据进行交互。随着这种灵活性,查询编写风格和选择也会有很大的差异。

一旦你向数据库发出查询,它必须解析你的查询以理解其流程,但这并不是数据库工作的结束。数据库引擎还有一个组件用于查看你的查询,并在不改变其功能的情况下,进行重写,以实现更好的性能和响应时间。这个强大的组件,毫不意外,被称为查询优化器

但是优化器如何重写查询呢?它有哪些额外的信息来源来处理查询?这些问题的答案可以将不同类型的查询优化器进行分类。广义上说,以下提到的有四种查询优化技术

  • 基于启发式的优化根据数据库中预定义的规则进行操作,例如,优先使用聚集索引。

  • 基于成本的优化根据查询成本估算来工作,包括估算查询可能使用的资源成本。

  • 混合优化结合了基于启发式的方法和基于成本的方法。大多数关系数据库使用这种技术

  • 自适应优化与上述所有技术不同,它在查询执行过程中改变查询执行计划。这在分布式系统中非常有用。

在 2008 年计算机科学学位的第二年,当我在学习数据库管理系统时,尽管我对数据库有一定的兴趣,但直到后来我才真正意识到查询优化和数据库性能调优技术在我的工作中将会有多么重要。我可以证明,理解特定数据库的查询优化器是编写高效 SQL 查询的最有趣和最有用的技能。这会带来巨大的差别。

让我们更详细地了解一下不同类型的查询优化器。

基于启发式的优化

基于启发式的查询优化,也称为基于规则的查询优化,使用一组经验法则核心原则来确定执行查询的最有效方式,通过重新排列构成查询树的关系代数操作(查询的内部表示)。

我相信你一定听说过在 SQL 工程师中流行的讨论:我连接表的顺序是否重要? 这个问题的简短回答是既重要又不重要。重要,因为顺序确实很重要,但不重要,因为优化器在解析后会修正查询。

让我们以优化查询的最常见示例为例——使用WHERE子句。现在,经验法则是你应该尽早进行选择。这是因为如果你在处理过程中尽早过滤掉不需要的数据,你也会限制需要连接、排序和分组的数据量。

一些关于特定类型的连接、CTE、排序等的规则可以被称为启发式,因此得名启发式优化。优化 SQL 查询不仅仅涉及关系代数和启发式,还要考虑你使用的是哪个数据库以及你使用的是哪种 SQL 方言等因素。

请注意,这种查询优化方法纯粹是理论上的,并未考虑数据库统计信息中的大量实际世界信息,这也是为什么你不会看到数据库仅使用这种优化方法来优化你的 SQL 查询。它们还会考虑基于成本的查询优化,我们将在下一节中讨论这一点。

基于成本的优化

这种方法采取了更实际的优化方法。它考虑了内存、CPU、网络等的约束条件,以及数据库维护的数据统计信息,如行数、平均行大小、表大小等。

虽然基于启发式的优化会根据经验法则重新排序查询树,但基于成本的优化则涉及评估几个查询执行计划,并根据执行查询的估算成本选择其中最优的一个。这个成本包括估算的时间、扫描的行数、使用的索引数量等。

由于基于成本的优化需要评估多个查询执行计划,因此可能需要更多时间来制定正确的执行计划。优化器也可能因为收集和分析数据库统计数据的开销而变得缓慢。然而,它通常比基于启发式的优化更有效。

大多数数据库不仅仅使用基于成本的优化器。它们通常采用混合方法,大部分优化基于成本,其余部分基于启发式方法、用户提示等。Snowflake 的 Jiaqi Yan 在CMU DB 隔离技术讲座的一场会议中谈到了这一点。在下一部分中,我们将通过一些示例,看看混合查询优化是如何工作的。

混合查询优化方法考虑了基于启发式的优化和基于成本的优化,并制定了一个平衡的查询优化计划。我们不会深入探讨混合查询优化的具体细节,因为不同数据库之间的实现差异很大。

自适应查询优化

到目前为止,我们讨论了查询优化技术,这些技术仅在查询执行开始之前查看并改进查询执行计划。自适应查询优化是一种在查询执行过程中尝试改进计划的查询优化类型。这种用例可能对你来说很明显——分布式数据处理系统和处理大量数据以及高并发的查询引擎。

当你处理长时间运行的查询时,自适应查询优化的效果尤其显著,这些查询的处理任务被划分为多个作业分配给集群中的不同计算节点(无论技术是什么——Redshift、Snowflake、Databricks 等)。数据的显著偏斜、不良的分区以及文件格式等问题,可能会使查询变慢。

自适应查询优化在查询执行过程中会关注这些问题以及更多问题。它还会查看查询的预期表现以及实际表现,借助多种指标来进行评估。通过这样的系统,查询引擎可以在查询执行时调整单个查询计划工作流或整个查询计划。

越来越多的数据库,特别是那些具备分布式数据处理能力的数据库,将最终使用某种版本的自适应查询优化,因为它解决了许多耗时的性能优化问题,例如确保数据均匀分布、根据内存和计算资源的可用性确保最佳分区大小等。我强烈推荐查看我在参考资料中分享的 Databricks Data+AI 2020 会议的相关内容。它对自适应查询优化的内部实现提供了一些实际的见解。

结论

这篇文章探讨了在各种数据库、数据仓库和数据处理引擎中使用的不同类型的查询优化器。了解优化器的类型对于数据工程中的查询优化和性能调优至关重要。随着规模的扩大,调优变得越来越困难。通过深入了解优化器,你将能够理解它的工作原理,以及如何调整其输出以满足你的需求和要求。

想想其他类型的空间和时间优化在应用中的想法,例如垃圾回收、缓存等。理解查询优化器为数据工程师带来的好处,与理解上述概念给软件开发者带来的好处类似。

参考文献

  1. 优化技术,数据库设计 424,马里兰大学

  2. 查询优化,数据库 II,CP 465,威尔弗里德·劳里埃大学

  3. 关系查询优化,CS 3200,东北大学

  4. MySQL 8.0 参考手册 — 优化概述

  5. PostgreSQL 内部概述 — 规划器/优化器

  6. Databricks 自适应查询执行 — Data+AI 峰会 2020

如果你觉得这篇文章有用,请订阅并查看我在 🌲 Linktree 上的所有作品。你还可以考虑通过使用我的推荐链接 购买 Medium 会员 来支持我。谢谢!

世界上最小的数据管道框架

原文:towardsdatascience.com/the-worlds-smallest-data-pipeline-framework-408eaf1a4ce4?source=collection_archive---------0-----------------------#2023-11-16

一个简单快速的数据管道基础,具备复杂的功能。

Douglas Blank, PhDTowards Data Science Douglas Blank, PhD

·

关注 发表在 Towards Data Science · 7 分钟阅读 · 2023 年 11 月 16 日

--

图片来源:Ana Lucia CottoneUnsplash

数据整理可能是数据科学家花费时间最多的工作。数据整理包括清理、转换以及将原始数据处理成有用的信息。像许多活动一样,整理过程通常需要随着时间的推移进行改进。因此,跟踪数据集是如何整理的很重要,这样你的团队可以管理并重复这个过程。虽然数据整理并不总是有趣,但它可能是现代公司中最重要的活动。

有些公司专注于数据管道,这些管道可能很复杂且非常精密。但是为了这次探索,让我们考虑将一个文本文件转化为一组单词或“标记”,并丢弃那些对我们没有用的文本。让我们从简单开始,逐步深入。

首先,让我们定义一系列步骤,以对文本中的单词执行整理函数。我们将使用 Python 的 text.translate() 函数来完成一些工作。考虑这 4 个函数:

import string

def step1(word):
    trans = str.maketrans("", "", string.punctuation)
    return word.replace("\n", " ").translate(trans)

def step2(word):
    return word.lower()

def step3(word):
    trans = str.maketrans("", "", "0123456789")
    return word.replace("\n", " ").translate(trans)

def step4(word):
    return (all([char in string.ascii_letters for char in word]) and 
            len(word) > 0)

step1 是一个函数,用于删除单词中的所有标点符号和换行符。step2 将单词转换为小写。step3 再次使用 text.translate() 来删除数字。而 step4 将用作过滤器,过滤掉包含非 ASCII 字符的单词。你可以想象一些额外的步骤,例如词干提取。

由于这些是简单函数,如果我们对一个单词应用 step1,我们将得到:

>>> step1("Testing---123;")
'Testing123'

确实,它已经从文本中删除了标点符号。我们可以通过将所有三个函数像俄罗斯套娃一样包裹在单词周围来应用它们:

>>> step3(step2(step1("Testing---123;")))
'testing'

在这里我们看到函数 step1step2step3 已被应用,只剩下字母“testing”。注意,我们将定义我们的函数按特定的顺序进行工作。也就是说,step1 应该在 step2 之前完成,等等。

这个基于函数的过程简单易创建,也简单易用。当然,我们可以一次性完成所有的函数。但随着“管道”函数变得越来越长且复杂,将过程分解为离散的步骤会使过程更易于管理。实际上,每一步可能会变得如此复杂,以至于需要不同的团队来处理。

好的,到目前为止,一切顺利。但当然,我们不想手动将函数管道应用到每个单词上。我们想要将它应用到列表中的每个单词。为此,我们创建了一个非常简单的函数 apply()

def apply(step, values):
    return [step(value) for value in values]

现在我们可以在整个单词列表上使用相同的函数:

>>> apply(step3, 
          apply(step2, 
                apply(step1, 
                      ["Testing---123;", "456---", "Hello!"])))
['testing', '', 'hello']

啊,是的,我们需要去除空白单词。step4 正是为此设计的,但使用起来有些复杂。它的样子是这样的:

>>> list(filter(step4, 
            apply(step3, 
                  apply(step2, 
                        apply(step1, 
                              ["Testing---123;", "456---", "Hello!"])))))
['testing', 'hello']

也就是说,因为 step4 是一个过滤函数,返回 True 以保留单词,返回 False 以删除它,所以它的应用方式是:filter(step4, data)

这个简单方法有几个问题:

  1. 步骤是从内向外应用的。也就是说,第一个步骤 step1 是最内层的函数,而 step3 是最外层的。不是很直观。

  2. 这非常冗长,因为我们必须为每个步骤函数重复apply()函数。

  3. 过滤器(如step4)不能像其他函数一样使用。

考虑到这些问题,我们能否将主要功能抽象成一个通用的管道?我设想了一个两步法:

# First we create a pipeline function:
p = my_pipeline(step1, step2, step3)

# And then we apply it to a dataset:
p(["Testing---123;", "456---", "Hello!"])

我们如何定义my_pipeline?事实证明,它非常简单:

def my_pipeline(*steps):
    def wrapper(inputs):
        for step in steps:
            inputs = apply(step, inputs)
        return inputs
    return wrapper

也就是说,my_pipeline是一个函数,它接受一系列步骤函数,并返回一个函数,该函数接受一个单词列表,应用系列中的每个步骤,然后返回处理后的单词列表。

让我们试试:

>>> p = my_pipeline(step1, step2, step3)
>>> p(["Testing---123;", "456---", "Hello!"])
['testing', '', 'hello']

它有效——我们得到了之前得到的完全一样的结果!step4过滤器函数怎么样?让我们暂时放下这个问题,试试这个系统在“真实”数据上的表现。好吧,这将是真实的假数据。对于这些实验,我们将创建 10,000 个文档,每个文档包含 10 个段落。我们将使用 Python 包essential_generators中的DocumentGenerator()

from essential_generators import DocumentGenerator
import os

gen = DocumentGenerator()

def generate_documents(
    count=10_000, 
    paragraphs=10, 
    output_folder="documents", 
    overwrite=False
):
    os.makedirs(output_folder, exist_ok=True)
    for n in range(count):
        filename = os.path.join(
            output_folder, 
            "doc_%05d.txt" % (n + 1)
        )
        if overwrite or not os.path.exists(filename):
            with open(filename, "w") as fp:
                for p in range(paragraphs):
                    fp.write(gen.paragraph() + "\n\n")

generate_documents()

这将需要大约 30 秒来生成所有数据。要继续我们的简单代码,我们需要引入一个步骤:

def step0(filename):
    return open(filename).read().split(" ")

这一步将接收一个文件名,打开文件,并按空格分割文本。我们还需要对我们的apply()函数做一个小调整,以处理单词列表,而不是单词:

def apply(step, outputs):
    return (step(input) if not isinstance(input, list) else 
            [step(i) for i in input] for input in outputs)

我还对apply做了一个小调整:现在它通过使用括号而不是方括号来返回一个生成器表达式而不是列表推导式。这将延迟处理直到需要时(有时称为“延迟评估”)。

现在我们可以构建一个接近完整的管道系统:

p = my_pipeline(step0, step1, step2, step3)
list(p(["documents/doc_00001.txt"]))

请注意,它接受一个文件名列表作为输入。简单明了。但我仍然希望看到一些东西:

  1. 以简单的方式处理过滤器的能力

  2. 以并行运行管道快速处理数据集的能力

  3. 可视化管道的能力

对于这三个附加功能,我会推荐你参考我基于上述想法开发的picopipe项目。你可以通过 pip 安装它:

pip install picopipe

并使用上述相同的步骤函数运行它:

from picopipe import pipeline, pfilter

p = pipeline(step0, step1, step2, step3, pfilter(step4))
list(p(["documents/doc_00001.txt"])[0])

在这里,pfilter代表管道过滤器,你只需将其包裹在step4函数周围。我对这个设计相当满意。但让我们看看它运行得有多快。

首先,让我们获取所有的文档文件名。一种简单的方法是使用glob

import glob

dataset = glob.glob("documents/doc_*.txt")

现在我们可以处理所有文档了:

results = list(p(dataset))

在我的笔记本电脑上处理所有 10,000 个文档大约需要 21 秒。简短而甜美!我们能让它运行得更快吗?

是的!现在管道中还有一个n_jobs参数,指示你可以并行运行的作业数量。这里有一小段代码,将使用 0 到 9 个线程多次处理数据集。你认为使用 9 个线程并行运行会快多少?

import time

x = []
y = []
for i in range(10):
    start = time.time()
    results = list(p(dataset, n_jobs=i))
    total_time = time.time() - start
    x.append(i)
    y.append(total_time)

这将需要几分钟。绘制结果时间与线程数的关系图显示:

显示将处理拆分为多个并行作业的运行时间的绘图。作者提供的图片。

有趣的是:图表并不随着额外线程的增加而继续减少。也就是说,使用 9 个线程并不比使用 1 个线程快 9 倍。为什么呢?不幸的是,您不能违法。而且有一个法则:阿姆达尔定律。它基本上说,您永远不会因为存在无法减少的开销成本而使速度快 N 倍。在这种情况下,我可以使用 4 个线程将时间从约 21 秒减少到 8 秒。还不错!

最后,我想要可视化管道。在项目的这一部分,我选择尝试Mermaid 图表格式。它最近得到了很多支持,包括在 github 的仓库中。这种格式非常简单,易于创建。对于 github 的渲染,只需将文件命名为.mmd 扩展名即可。以下是使用picopipe生成 Mermaid 脚本的方法:

from picopipe import to_mermaid

with open("pipeline.mmd", "w") as fp:
    fp.write(to_mermaid(p))

并且在 github 的渲染中显示如下:

Github.com 直接支持 Mermaid 文档文件。作者提供的图片。

不幸的是,github 不显示鼠标悬停功能(在 CSS 中定义)。但是,如果您可以设置自己的 CSS,则可以实现不仅可视化管道,还可以在鼠标悬停在步骤框时显示步骤代码:

如 Comet 的自定义面板中所示的 Mermaid 图表。作者提供的图片。

上述的带有鼠标悬停支持的 Mermaid 图表是使用 Comet 的自定义面板系统创建的(所有用户免费)。创建显示 Mermaid 文件的自定义面板非常简单。以下是上述 Mermaid 图表的实时演示:comet.com/dsblank/picopipe/a4c044c1657b464087ec44f67ae22709

这完成了我们对开发“世界上最小数据管道框架”的探索,以及对其并行化和可视化的探索。您可以在此处找到所有的代码:github.com/dsblank/picopipe 我希望您觉得这里呈现的想法和最终模块有用。

对人工智能、机器学习或数据科学感兴趣吗?考虑鼓掌和关注。Doug 是 comet.com 的研究主管。

线性回归的理论深度解析

原文:towardsdatascience.com/theoretical-deep-dive-into-linear-regression-e53c579aef5b

可解释的人工智能

了解线性回归的本质及其如何以自然的方式扩展

Dr. Robert KüblerTowards Data Science Dr. Robert Kübler

·发布于 Towards Data Science ·10 分钟阅读·2023 年 6 月 23 日

--

图片由 Erik van Dijk 提供,来源于 Unsplash

大多数有抱负的数据科学博客作者都会这样做:撰写一篇关于线性回归的介绍性文章——这是一个自然的选择,因为这是我们进入这个领域时学习的第一个模型之一。虽然这些文章对初学者非常有用,但大多数都未能深入挖掘以满足高级数据科学家。

所以,让我带你了解一些鲜为人知但令人耳目一新的线性回归细节,这将使你成为更好的数据科学家(并在面试中获得加分)。

这篇文章内容相当数学化,因此为了跟上内容,具备一些概率和微积分的坚实基础会很有帮助。

数据生成过程

我非常喜欢在建模时考虑数据生成过程。处理过贝叶斯建模的人会明白我的意思,但对于其他人:想象一下你有一个数据集 (X, y),由样本 (x, y) 组成。给定 x,如何得到目标 y

假设我们有 n 个数据点,每个 x k 个组件/特征

对于一个线性模型,参数为 w₁, …, wₖ(系数)b(截距)σ(噪声)**,假设数据生成过程如下:

  1. 计算 µ = wx₁ + wx₂ + … + wₖxₖ + b

  2. 随机生成一个 y ~ N(µ, σ²)。这与其他随机生成的数字独立。或者: 生成 ε ~ N(0, σ²*) 并输出 y = µ + ε

就是这样。这两行简单的文字等同于人们喜欢详细解释的最重要的线性回归假设,即线性、同方差性和误差独立性

从过程的第 1 步开始,你也可以看到我们用典型的线性方程 wx₁ + wx₂ + … + wₖx + b 来建模期望 µ,而不是实际目标。我们知道无论如何不会击中目标,因此我们接受生成 y 的分布的均值。

扩展

广义线性模型。我们不必使用正态分布作为生成过程。如果我们处理的数据集仅包含正目标,那么假设使用泊松****分布 Poi(µ) 可能更有利,这样你就得到了泊松回归

如果我们的数据集只有目标 0 和 1,使用伯努利分布 Ber(p),其中 p = sigmoid(µ),那就是逻辑回归

只有 0, 1, …, n 之间的数字?使用二项分布来获取二项回归

列表还在继续。长话短说:

思考一下哪个分布可能生成你在数据中观察到的标签。

我们到底在最小化什么?

好的,我们现在决定了一个模型。那么我们怎么训练它?我们怎么学习参数?当然,你知道:我们最小化了(均方)误差。但为什么?

关键在于,你只需使用我们之前描述的生成过程进行最大似然估计。我们观察到的标签是 y₁, y₂, …, yₙ*,它们都是通过具有均值 µ₁, µ₂, …, µₙ 的正态分布独立生成的。看到这些 y 的可能性是多少?这是:

图片由作者提供。

我们现在想找到(隐藏在 µᵢ 中的)参数,以最大化这一项。这等同于最小化均方误差,如你所见。

扩展

不等方差。实际上,σ 不必是恒定的。你可以为数据集中每个观察值设置不同的 σᵢ。然后,你将最小化

图片由作者提供。

代替,这就是带有样本权重 s 的最小二乘法。建模库通常允许你设置这些权重。在 scikit-learn 中,例如,你可以在 fit 函数中设置 sample_weight 关键字。

这样,你可以通过增加相应的 s 来更强调某些观察值。这等同于减小方差 σ²,即你更确信这个观察值的误差较小。这种方法也称为加权最小二乘法

输入的方差依赖性。 你甚至可以说方差也依赖于输入 x。在这种情况下,你会得到一个有趣的损失函数,这也被称为方差衰减

整个推导过程在这里概述:

## 免费获取回归神经网络中的不确定性估计

给定合适的损失函数,标准神经网络也可以输出不确定性

towardsdatascience.com

正则化。 除了仅仅最大化观察到的标签 y₁, y₂, …, yₙ 的似然,你还可以采用贝叶斯观点最大化后验似然

作者提供的图像。

在这里,p(y | w) 是上述的似然函数。我们必须决定一个 p(w) 的概率密度,即所谓的先验或先验分布。如果我们说参数独立地服从围绕 0 的正态分布,即 wᵢ ~ N(0, ν²),那么我们最终会得到L2 正则化,即岭回归。对于拉普拉斯分布,我们得到L1 正则化,即 LASSO

为什么呢?让我们以正态分布为例。我们有

作者提供的图像。

因此,加上我们上述 p(y | w) 的公式,我们必须最大化

作者提供的图像。

这意味着我们必须最小化均方误差加上一些正则化超参数乘以 w 的 L2 范数

注意,我们从贝叶斯公式中省略了分母 p(y),因为它不依赖于 w,所以我们可以忽略它进行优化。

你可以使用任何其他的先验分布来创建更有趣的正则化。你甚至可以说你的参数 w 是正态分布的,但与某个相关矩阵 Σ* 相关。*

假设 Σ 是 正定的,即我们处于非退化情况。否则,没有密度 p(w)。

如果你进行数学计算,你会发现我们然后必须优化

作者提供的图像。

对于某个矩阵 Γ。注意:Γ 是可逆的,我们有 Σ⁻¹ = ΓᵀΓ。 这也被称为提霍诺夫正则化

提示: 从以下事实开始

作者提供的图像。

并且记住,正定矩阵可以被分解成某个可逆矩阵及其转置的乘积

最小化损失函数

很好,我们定义了我们的模型并知道我们想优化什么。但是我们如何优化它,即学习最优的参数以最小化损失函数?什么时候会有唯一解?让我们来看看。

普通最小二乘法

假设我们不进行正则化,也不使用样本权重。那么,均方误差可以写作

图像由作者提供。

这非常抽象,所以让我们以不同的方式书写

图像由作者提供。

使用 矩阵微积分,你可以对这个函数关于 w 求导(我们假设偏置项 b 已经包含在内)。

图像由作者提供。

如果你将这个梯度设为零,你会得到

图像由作者提供。

如果 (n × k) 矩阵 X 的秩为 k,那么 (k × k) 矩阵 XX 也是,即它是可逆的。为什么?这可以从 rank(X) = rank(XX) 中推导出来。

在这种情况下,我们得到唯一解

图像由作者提供。

注意: 软件包不像这样优化,而是使用梯度下降或其他迭代技术,因为这样更快。不过,公式很好,并且为我们提供了有关问题的一些高级见解。

但这真的能达到最小值吗?我们可以通过计算 Hessian 矩阵来找出,Hessian 矩阵是 XX。该矩阵是半正定的,因为 wXXw = |Xw|² ≥ 0 对于任何 w。它甚至是严格正定的,因为 XX 是可逆的,即 0 不是特征值,所以我们的最优 w 确实在最小化我们的问题。

完美的多重共线性

这只是友好的情况。但如果 X 的秩小于 k 会发生什么?如果我们数据集中有两个特征,其中一个是另一个的倍数,例如,我们在数据集中使用 height (in m)height (in cm) 作为特征。然后我们有 height (in cm) = 100 * height (in m)

如果我们对分类数据进行独热编码而不丢弃其中一列,也可能发生这种情况。例如,如果我们数据集中有一个特征 color,它可以是红色、绿色或蓝色,那么我们可以进行独热编码,得到三列 color_red, color_green,color_blue。对于这些特征,我们有 color_red + color_green + color_blue = 1,这也会引起完美的多重共线性。

在这些情况下,XX 的秩也小于 k,因此这个矩阵是不可逆的。

故事结束。

还是不行?实际上不是,因为这可能意味着两件事:(XX)w = Xy

  1. 没有解或

  2. 无限多的解。

事实证明,在我们的案例中,我们可以使用 Moore-Penrose 伪逆 获得一个解。这意味着我们处于无穷多解的情况,这些解都给我们相同的(训练)均方误差损失。

如果我们用 A⁺ 表示 A 的 Moore-Penrose 伪逆,我们可以求解线性方程组为

作者提供的图片。

要获得其他无穷多解,只需将 XX 的零空间添加到这个特定解中。

使用 Tikhonov 正则化的最小化

记住,我们可以向权重中添加先验分布。然后我们需要最小化

作者提供的图片。

对于某个可逆矩阵 Γ。按照普通最小二乘法中的相同步骤,即对 w 求导并将结果设置为零,解为

作者提供的图片。

精彩的部分:

XᵀX + ΓᵀΓ 始终是可逆的!

让我们找出原因。只需证明 XX + ΓᵀΓ 的零空间仅为 {0}。因此,我们取一个 w 使得 (XX + ΓᵀΓ)w = 0。现在,我们的目标是证明 w = 0。

从 (XX + ΓᵀΓ)w = 0 可得

作者提供的图片。

这也意味着 |Γw| = 0 → Γw = 0。由于 Γ 是可逆的,w* 必须是 0。通过相同的计算,我们可以看到 Hessian 矩阵也是正定的。

很好,因此 Tikhonov 正则化自动帮助使解唯一!由于岭回归是 Tikhonov 回归的特例(对于 Γ = λIₖ,其中 Iₖk 维单位矩阵),因此同样适用。

添加样本权重

最后,让我们还将样本权重添加到 Tikhonov 正则化中。添加样本权重等同于最小化

作者提供的图片。

对于某些对角矩阵 S 其对角元素 sᵢ 为正,最小化问题和普通最小二乘法一样简单。结果是

作者提供的图片。

注意: Hessian 矩阵也是正定的。

你的作业

假设对于 Tikhonov 正则化,我们不强制权重围绕 0,而是围绕某个点 w₀。证明优化问题变为

作者提供的图片。

结果是

作者提供的图片。

这是 Tikhonov 正则化的最一般形式。有些人更喜欢定义 P := S²,Q := ΓᵀΓ,如 这里所示

结论

在这篇文章中,我带你探讨了线性回归的几个高级方面。通过采用生成视角,我们可以看到广义线性模型与普通线性模型的区别仅在于用于抽样目标y的分布类型。

然后我们看到,最小化均方误差等同于最大化观测值的似然。如果我们对可学习参数施加一个先验正态分布,我们最终会得到 Tikhonov(以及 L2 作为特例)正则化。我们也可以使用不同的先验分布,如拉普拉斯分布,但那样就没有封闭的解公式了。不过,凸优化方法也可以帮助你找到最佳参数。

作为最后一步,我们为每个最小化问题找到了很多直接的解公式。这些公式在大数据集的实际应用中通常不会使用,但我们可以看到这些解总是唯一的。我们在过程中也学会了一些微积分。😉

我希望你今天学到了一些新的、有趣的和有价值的东西。感谢阅读!

如果你有任何问题,请在 LinkedIn上联系我!

如果你想更深入地了解算法的世界,可以试试我的新出版物所有关于算法!我仍在寻找作者!

## 所有关于算法

从直观的解释到深入的分析,算法通过示例、代码和精彩的内容得以呈现…

medium.com

没有所谓的自学程序员。

原文:towardsdatascience.com/theres-no-such-thing-as-a-self-taught-programmer-4062c309aef0

改变职业道路永远不嫌晚,但一定要做到正确。

Gabriel SenaTowards Data Science Gabriel Sena

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

--

图片由 Patrick Fore 提供,来源于 Unsplash

如果你正在寻求改变方向并进入软件行业,希望这篇文章能为你提供一些经历过类似过程的人的见解。

每个人的旅程都不相同,但我将重点讲述我的经历——从没有计算机科学背景到在这个领域拥有丰富经验。我曾有机会担任关键角色,如高级软件工程师/数据工程师、软件架构师、专家等。

在我的青少年时期,我总是喜欢摆弄电脑和机械。不管是旧的 Windows 计算机还是拆解我车子的发动机零件。我对事物的构造充满了好奇。尽管有这些兴趣,我的本科阶段却充满了从建筑学到美术的过渡目标。直到 2000 年代末期,当我正在攻读美术学士学位时,我上的第一门“互动设计”课程让我几乎瞬间意识到,我想进入软件行业。某种东西点击了,我准备从极其主观的艺术和设计领域转向更加具体、更具二元性的领域。

在做出决定后,即使在本科阶段,我也养成了阅读任何我感兴趣的编程语言或概念的习惯——图书馆里有很多选择,我从 WordPress 书籍到 C++等各种书籍中随意挑选。

但等等,这难道不是自学的吗……

并不完全是这样(稍后会详细说明),我把这称为对该领域的探索。这一点非常重要,因为在没有理解的情况下进行如此剧烈的职业变化可能会很冒险,可能会导致你后悔你的决定。我的好奇心帮助我真正决定了这是否是我想要的道路。因此,在迈出那些初步步骤后,我全身心投入到改变中。哦,天哪,我没想到会面临这么多的努力、压力和困惑——但这正是我内心所热爱的。

“做伟大工作的唯一方法是热爱你所做的事”

— 史蒂夫·乔布斯

回首往事,由于渴望获得实际经验,我在大学毕业后急于开始工作。虽然当时我对计算机科学的理解还很初步,但我还是在 2010 年代初找到了第一份软件工作。我感激不已,并期待着面临的挑战。

现在回到那个自学的事上。我发现这个词在计算机科学(以及可能在大多数领域)中完全具有误导性。事实是,即使你没有接受过正式教育,你也依赖于各种资源、学习,最重要的是,来自更有经验的开发者的指导或辅导——在某种程度上,这是一种正式的专业教育👨🏽‍🎓。

自学和研究是这个行业的基础部分;对于那些接受过正式教育的工程师来说,这也是一个持续的追求。

软件行业不断变化,因此对现有和新系统知识库的维护也是如此。

我不会在这篇文章中深入讨论我对程序员软件工程师的看法,因为我可能已经在自学程序员社区引起了一些波澜😶。但我会说,理论知识是重要的。

那里

​ ​ ​​ ​ ​ ​​​ ​ ​​​ ​有

​ ​ ​​​ ​ ​​​ ​ ​​​​ ​ ​​ ​ ​​​ ​​级别

​ ​ ​​​ ​ ​​​ ​ ​​​ ​ ​​​ ​ ​​​ ​ ​​​ ​ ​​​ ​​​ ​ ​​​ ​​​​到这一点

​ ​ ​​​ ​​ ​​​ ​ ​​ ​ ​​​ ​ ​​ ​ ​​​ ​ ​ ​​​ ​ ​​​ ​ ​​​ ​ ​​​ ​​​ ​ ​​​ ​​游戏。

这是我希望在职业生涯早期就知道的 4 个建议。

#1 别慌。

我在 2010 年代初期的第一次“恐慌”之一是处理 Javascript/PHP——API Posts、回调、JQuery,所有那些好东西。我完全是新手,根本不知道自己在做什么。我会说,无法理解的压力和焦虑真是糟糕透了。

我发现许多入门级程序员犯的一个错误是尝试所有的按钮。也就是说,盲目地调整方法或一系列代码输入,试图强行让系统运行。我多次陷入这个陷阱。这是因为一旦你按对了所有按钮,让该死的东西运行起来,就几乎像是一种欣快体验。然而——不要这样做。你不会走得很远,即使你成功了,你也会尝试了很多不同的选项,实际上并不知道它为什么会成功。

相反,退后一步。去散步,调整一下自己。当你回来时,尝试更深入地理解你所挣扎的内容——如果你在处理一个 API,请再次阅读文档;如果你在处理一个编程概念,寻找可以帮助你从不同角度理解的在线资源。咳咳 ChatGPT 🤖(希望我早期能有这个工具)。

关于这个领域的事,你是长期投入的。这是一场永无止境的马拉松。你永远不会到达学习的终点。一切都在于旅程,所以慢慢来,逐步理解每一部分。

#2 失败是可以的。

从小打足球、篮球和网球,我一直是竞争激烈的。失败或在任何情况下输掉的想法我都不喜欢——没有人会喜欢。

职业生涯早期,当我无法理解一个概念、代码崩溃或生产中发现 bug 时,我感到不断的失败。这种感觉有点像是让你的团队失望了。

我认为最困难的事情之一是意识到错误的发生,并理解失败是可以的。失败往往会带来伟大的成果。你应该把握机会面对你的失败,理解发生了什么,以及你可以做得不同。这是你作为程序员成长的地方。

[## 不要害怕失败,要有行动优先的心态]

大多数人面临的最大障碍之一就是对失败的恐惧。他们只是希望一切完美,而……

medium.com](https://medium.com/@elevyhart/dont-be-afraid-to-fail-have-an-action-first-mindset-59a195995ebb?source=post_page-----4062c309aef0--------------------------------)

#3 学习 + 一致性 + 目标 = 成功。

这是任何一个人对所追求的领域或话题的基本要求。但我要说的是,与明确的目标和期望保持一致是关键。刚开始时,确切了解所有需要学习的内容可能是一个艰巨的任务。然而,你应该尽力为自己设定目标,以便评估自己的进展。

没有目标的梦想只是梦想,它们最终会带来失望。

— 丹泽尔·华盛顿

让我在这里分享几个我最喜欢的学习资源:

O’Reilly 在线书籍

Coursera

在我职业生涯的很长一段时间里,导师和在线资源是我学习的途径。你可以在 Coursera 上审计免费的课程,或在这里那里读几本书。对一些人来说,这可能就是他们所需要的一切——这完全没问题。

我为自己设定的一个目标是最终获得该领域的正式教育。我希望在教授和实践者的指导下,对该领域有更深刻和理论性的理解。

在我的职业生涯中期,我终于筹集到了资金,部分得益于我公司提供的学费报销政策,得以完成这一目标。对我而言,在职业生涯的中途获得 NCSU 的正式计算机编程证书是极其宝贵的。目前,我正在 JHU 攻读人工智能硕士学位。这是我的路径,我对至今所经历的经验深感感激。

如果你对正式教育途径感兴趣,我强烈推荐它。同时,我建议你在计划工作和上学时一定要控制好节奏。市场上有多种选择,所以在选择你投入时间和金钱的大学时,也要有所挑选。

如果这不是你当前的路径或合适的选择,正如在职业生涯早期对我来说一样,我建议你围绕那些能够激励你并提供指导或建议的人。保持对自己个人进展的努力和诚实是至关重要的。最重要的是,保持一致并获得正式的专业教育 👨🏽‍🎓。

不要惊慌,也不要急于求成——享受过程

#4 评估你的领导者。

在我的职业生涯中,我学到,打造一款产品需要一个团队。企业主、工程师和其他利益相关者都在协作,共同朝着构建世界级产品的终极目标努力。每个小组在专注于各自的专业领域时,同时也在实现整体解决方案的方向。当然,这是对产品交付方式的简化描述。然而,很明显,沟通、协作和领导者是关键。我有幸在明智的领导下工作过,也曾在不充分的领导下工作过(这里不提名字😅)。这两种类型的领导者都帮助我确定了领导者的品质和性格。

因此,我认为不断评估你所拥有的领导者,并了解他们做出决策的方式至关重要。

你会以不同的方式做吗?如果会,怎么做,为什么?

即使你对成为软件经理或领导层的一部分不感兴趣(因为这并非适合每个人),你仍然应该提出这些问题,以确保你在一个能够帮助你实现目标的合格领导下工作。

最终思考…

如果这是你决定的路径,接受挑战,保持动力和一致性,享受成为程序员的充实旅程。正如生活中的一切,努力工作会有回报。享受你拥有的职业,这个职业不断发展,每天都有新的方面可探索——想象一下,若从事的是一个单调重复的工作,每天做着相同的无聊任务,会是怎样的感受。同时,接受变化,学习不同的编程语言,掌握新概念,并愿意冒一些可能导致失败的计算风险。成长和理解会随着时间、一致性和计划而来。

明白了

这。

当你没有学习动力时,这 5 个技巧将帮助你学习数据科学

原文:towardsdatascience.com/these-5-tips-will-help-you-learn-data-science-when-you-have-no-motivation-to-study-8e25b4d55788?source=collection_archive---------4-----------------------#2023-05-21

使用这 5 个技巧来教自己数据科学,当你没有动力时。

Madison HunterTowards Data Science Madison Hunter

·

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

--

照片由 Priscilla Du Preez 提供,来自 Unsplash

是的,我们都经历过这种情况。

有一天你满怀热情地制定学习计划,打算自学数据科学,但接下来你却开始找借口,理由是今天没有时间学习。我们都知道,除了清洗公寓的墙壁、清理冰箱或遛狗,你还能做多少事情,最终还是会坐在电脑前,试图激发一些学习的兴奋感。

作为一个已经自学数据科学四年的旁听者,我了解自学的消耗,尤其是在动力方面。然而,我也逐渐成为了在每天都能保持学习动机方面的专家。虽然有些人需要课堂的结构才能进行学习,但你也可以发展自己的方法,让自己每天都能保持学习的热情。好吧,也许不是热情,但至少能够坐下来并集中精力进行每天的学习。

这里有五个经过验证的技巧,能够帮助即使是最没有动力的人也能自学数据科学。

使用能帮助你持续进步的学习资源

自从我开始学习数据科学以来,我学到的最重要的一点就是学习资源可以决定你的成败。

例如,我知道要进行数据科学中的许多计算,我需要掌握微积分。凑巧的是,微积分课程是我大学学位要求的一部分。由于我需要为微积分课程付费,我决定利用这个机会自学数据科学所需的微积分。然而,我大学的学习材料糟糕透顶,我花了五个月才学会函数、极限和微分。这段时间真是折磨人。直到我找到了 Youtube 上最棒的数学老师。Leonard 教授的微积分讲座改变了我的人生,我发现通过这些视频自学微积分的速度远快于使用我大学提供的材料。

为了保持自学的动力,你需要使用那些能够帮助你按节奏学习的资源,而不是让你在理解一个概念上反复挣扎数周。没有什么比在一个概念上卡住超过一个月更能快速消耗你的动力了。

如果一个学习资源不能发挥作用,就没有理由继续使用它。幸运的是,互联网充满了数据科学学习资源,你有很多选择。

例如,许多人通过 2021 年变得流行的 Google 数据分析专业证书获得了很好的学习体验。这个自学进度课程旨在通过使用极其设计良好的学习材料来推动学生前进,使你可以在不到 6 个月的时间内完成课程,每周学习 10 小时。Codecademy 是另一个成功帮助人们学习编码的学习资源,它提供了易于跟随和消化的模块,帮助你在学习中不断前进,而不会陷入困境。

总之,如果一个学习资源让你感到意志消沉,无法推进学习,那么你没有理由继续使用它。自学数据科学应该始终是一个前进的过程。是的,前进的速度有时可能很慢,但绝不应该完全停止或倒退——有太多不同的学习资源可以选择。你需要做的就是能够承认某些东西不起作用,并改变策略,找到有效的方法。

以在线学习空间、"与我一起学习"视频和 Discord 聊天的形式寻找学习支持

奇怪的是,即使你们相隔遥远,和某人一起学习这样简单的事情也能如此激励人心。

在线学习空间、"与我一起学习"视频和 Discord 聊天在过去三年中似乎变得非常流行,这些资源每天都有成千上万的观众和成员。

我最喜欢的 YouTube 学习频道之一是由Merve运营的,她也巧合地学习数据科学。该频道拥有 82.2 万订阅者,每周发布的“与我一起学习”视频被数百万观众观看。与某人“一起学习”总是让人感到如此鼓舞,并且帮助你保持动力。

Study TogetherStudyStreamStudyverse 都是虚拟学习室,你可以与来自世界各地的人一起学习。这些学习室可以帮助打破拖延症,并让你集中精力学习几个小时。此外,许多 Instagram 上的学习账户利用直播功能为所有关注者举办学习会议。

另一个保持动力的最佳工具是加入一个 Discord 服务器,特别是那些专注于不同学习领域的服务器 数据科学。这些社区是保持学习动力的绝佳机会,同时也可以在遇到问题时立即得到解答。与志同道合的人交流也是了解数据科学行业、建立人脉和未来成为更全面的数据科学家的好方法。

每天学习时间不超过 6 小时

当你自学数据科学时,可能很难确定每天应该学习多少。这种情况在你没有其他承诺时尤为明显,这可能使你整天都可以用来学习。

这也受到你周围其他人学习情况的进一步影响。社交媒体使得有毒的学习文化更加普遍,许多人会发布他们每天学习多少小时的信息。这会给你带来不必要的压力,促使你也每天学习 12 小时。

尽管每个人能够有效学习的时间不同,但我可以证明你不应每天学习超过 6 到 7 小时。学习是一种强度很大的脑力活动,与你在 8 小时工作制下的脑力使用完全不同。例如,工作 8 小时并不意味着你在这 8 小时内都在高强度使用大脑。其中一些小时会用于耗费精力的任务,但大部分时间会用于低强度的脑力活动,比如参加会议、回复邮件和休息。

相对而言,你的大脑在学习时会持续被高强度地使用。学习需要 100%的专注力来有效进行(特别是当你在探索如微积分和神经网络等话题时),这就是为什么每天 6 到 7 小时的学习应该是你的最大目标。这还考虑到你在学习期间需要以其他方式照顾自己,包括休息、社交、锻炼和营养。

当你的大脑学会了一天最多只需努力工作 6 小时时,你可能会发现,在这 6 小时内更容易集中注意力。你不会再感觉被手机分心,因为你知道你只有 6 小时来完成当天的学习任务。你还会发现,第二天开始学习时感觉更加清爽,因为你的大脑有足够的休息时间。你可能还会发现,你对学习的材料保持的记忆更多,因为你的大脑有更多时间在不受新信息不断轰炸的情况下与学习的材料建立强大的连接。

找到激励你的实际应用

让我们面对现实吧 —— 数据科学中并非所有主题都一视同仁。不幸的是,像机器学习、数据可视化和实际应用这样的好东西,只能在你学会了编码、数学和沟通技能之后才能出现。要通过这些主题,对尚未到来的好东西保持动力可能会很困难。

克服这种低迷的一个我最喜欢的技巧是找到激励我的材料的实际应用。例如,学习极限和微分可能会令人疲倦,但只要你不忘了它们可以用来确定函数的变化率,从而告诉你各种酷炫的事情,比如气候变化加快、商品成本增加或医疗保健获取减少。

当你热衷于如何应用你的数据科学知识(比如在医疗保健、科学、工程、商业、教育等领域),那么找到可以应用正在开发的知识的不同方式变得很容易。例如,一旦掌握了数据分析,你可以为社区内的小企业提供无偿工作,帮助他们增加销售。或者,你可以创建一个预测模型,预测某种自然灾害可能影响多少人,作为一个作品集项目。

无论你的兴趣是什么,总有办法将你所学的应用到能激励你继续前进的方式上。找到激励你的东西,并将数据科学应用到其中。

设置有时间限制的学习目标

我不在乎你有多么拖延,设置有时间限制的学习目标总是奏效的。只要你按时完成,即使把它留到最后一刻也没关系。

自学数据科学时,我看到很多人面临的一个问题是缺乏动力,因为没有结构化的有时间限制的目标。许多人告诉我,他们永远无法自学数据科学,因为他们没有动力坐下来完成工作。然而,这只是因为缺乏结构。

上学的一个重要好处是你处于一个有结构的环境中,并且有截止日期。作业截止日期、考试截止日期以及毕业截止日期。你能想到的几乎都有截止日期。这种时间敏感的结构帮助人们集中注意力,并且不用过多努力就能投入工作。正如我之前提到的,即使你把事情拖到最后一刻,你也会完成任务,因为你知道有一个硬性的截止日期需要遵守。

因此,在自学领域获得这种动力的诀窍是设定你认为是硬性截止日期的时间敏感学习目标。

对于许多人来说,这可能很容易,因为你正在经历职业变动,只希望离开工作几个月。而对其他人来说,这可能更困难,因为可能没有特定的时间压力。

假期、生日、活动,甚至周末都是可以用来激励你完成工作的伟大硬性截止日期。没有什么比在周五完成所有工作、知道你的周末可以做任何事情更令人愉快的了。假期、朋友的生日派对或孩子的学校表演期间无需担心工作也是一样。无论是什么场合、日期还是周末惯例,找到一个可以围绕其安排学习的硬性截止日期,可以帮助即使是最拖延的人也能激励自己学习数据科学。

订阅以将我的故事直接发送到你的收件箱:故事订阅

请成为会员,以通过我的推荐链接获取对 Medium 的无限访问权限(这对你没有额外费用,我将获得一小部分佣金):Medium 会员

通过捐赠来支持我的写作,以资助创作更多类似的故事:捐赠

这 7 个编程习惯让你成为一个低效的数据科学家

原文:towardsdatascience.com/these-7-programming-habits-are-making-you-a-less-productive-data-scientist-6d9767fd8ff3?source=collection_archive---------9-----------------------#2023-02-03

修正这些习惯可以让你成为一个更高效的数据科学家

Madison HunterTowards Data Science Madison Hunter

·

关注 发表在Towards Data Science ·9 分钟阅读·2023 年 2 月 3 日

--

图片来源于Rémi JacquaintUnsplash

我相信在我们共同的数据科学之路上,大家至少都曾经犯过其中一个这些坏习惯。

无论我们是在刚开始学习时做这些注释,还是因为我们已经比较擅长而变得懒惰时做这些注释,我们都曾犯过这些编程错误。

无论出于何种原因,清理你的编程习惯,成为更高效的数据科学家永远不会太晚。幸运的是,你已经完成了最困难的部分,学会了编程。现在,我们只需要完善你的技巧,养成一些良好的习惯,让你在数据科学项目中享受更多乐趣。至少你今天学到的知识会让你避免被软件部门责备。

从软件开发学生的经验来看,这些习惯无疑会妨碍你发挥最佳生产力——让我们现在就改变这一点。

1. 未对代码进行注释

对代码进行注释是代码文档的重要部分,确保以下三点:

  1. 其他人能够理解你的代码。

  2. 其他人能够维护你的代码。

  3. 当你在一段时间后重新访问你的代码时,你能理解它。

至少,你的代码中必须有三种类型的注释——这些是不可协商的:

  1. 第一点是对你个人或团队代码库中任何新提交或共享代码的注释。这个注释可以作为你的 Git 提交消息的一部分,但也应该出现在代码中,通常位于你刚刚提交的代码块上方。我喜欢通过在代码和注释周围创建虚线来进一步分隔这个代码段——只是为了更清晰的查看,仅此而已。这个注释应该描述代码的功能。

  2. 第二点是对代码中每个函数的注释。该注释应直接位于每个函数的上方,并解释其输入、输出以及函数逻辑。

  3. 最终的注释应该位于代码中任何单行代码的上方。单行代码描述了通常分散在几行代码中的逻辑,但却写成了一行。由于这些单行代码有时可能难以理解,因此一个描述其功能及每个部分如何工作的注释是保持组织性的好方法。

从与其他软件开发者的合作中,我看到将好代码与优秀代码区分开来的因素是代码注释的层次和细节。换句话说,简洁、准确、充分地注释你的代码。如果某些内容对你来说很明显,最好还是做个注释,以防对其他人(或将来你自己)不够明显。

2. 未使用 GitHub 进行版本控制

现在已经没有理由让从事技术工作的人不使用 GitHub 了。GitHub 不仅是一个版本控制工具,而且还是一个生产力工具,帮助你轻松处理代码和与他人协作。

GitHub 通常被认为是黄金标准,大多数科技公司都通过它来进行代码的版本控制。即使你是一个在公司里工作的单独数据科学家,如果你将代码分享给一个将其转化为生产代码的软件部门,GitHub 也是一个重要的工具。

在某个时候,我们都曾宣布要真正学会如何使用 GitHub 一劳永逸。GitHub 具有许多有用的功能(包括跟踪代码的变化以及处理代码的旧版本),但说实话,数据科学家通常可以仅仅使用它来进行简单的主分支提交,配合几个分支以运行替代场景。就是这么简单。

好吧,这就是你真正开始使用它的标志——这次是认真的。

## 数据科学家的全面 GitHub 指南

数据科学家的 GitHub 教程,包括 UI 和命令行

[towardsdatascience.com

3. 不测试你的代码

我们都经历过这种情况:因为害怕可能发生的事情而尽可能避免运行和测试代码。1% 的时候我们可能运气好,一切正常运行,但我可以保证,其他 99% 的时候,一切都会出问题。

从软件开发的教育背景中,我们经常接受测试的训练。不定期测试代码被视为一种罪过,我们很快就能做到。不仅帮助我们立即发现错误和缺陷,还意味着我们不需要筛查数百行代码来找出问题所在。

测试你的代码就像编写 单元测试 一样简单,这种测试涉及检查函数、对象或类(单元)是否正常工作。进行单元测试的一种简单方法是根据你提供的输入打印函数的输出。

[## 如何为 Python 函数编写单元测试

本指南将教你如何为 Python 函数编写单元测试。但你为什么应该考虑编写单元测试……

www.freecodecamp.org

4. 没有将复杂问题拆解为简单变量和函数

数据科学问题通常很复杂,涉及许多活动的部分。这些问题可能让人感到气馁,如果不将其拆解成简单的部分,可能会导致你盯着计算机屏幕发呆,直到该回家时却没有写下一行代码。

诀窍是 以终为始地开始解决问题,然后将其拆解成简单的变量和函数——因为实际上,代码就是由变量和函数组成的。

开始之前,你需要问自己这个问题试图解决什么,以及这个解决方案的结果将是什么。这将帮助你开始确定你需要哪些代码片段来达到最终目标。一旦你确定了解决方案的样子,你可以开始规划你需要的各个变量和函数,以实现它。

看,你已经把复杂的问题拆解成了可管理的任务!这感觉不是更好吗?

在将这些任务添加到你的看板上(我在处理复杂问题时保持组织的最爱方式)之后,你可以开始创建将导致完整解决方案的项目小部分。

5. 不重构你的代码

代码重构是指在不改变原有功能的情况下,对代码进行重组。虽然重构通常出现在 软件开发场景中,但数据科学家也可以使用重构来清理他们的代码。

重构 比听起来要简单:查看一些你以前写的旧代码,问问它可以如何更有效地编写。然后,应用良好的编码实践,清理你的代码,直到它看起来比以前更好。

重构最好是在你编写了有效的代码之后进行。例如,当你刚开始处理一个 数据科学问题时,你要确保你的代码能正常工作,不管它是否优雅。然后,一旦你确保它产生了正确的输出,你可以回过头来明确变量名称,正确缩进代码,使用 Python 语法标准创建消除冗余的函数,并且总体上重写任何看起来像一堆意大利面的代码。

我建议在你从专注状态中出来并使代码正常工作之后再进行任何重构。每次写一行代码就停下来重构会让你脱离专注状态,并且使完成代码的时间延长十倍。就像建议在你把想法写到纸上之前不要担心拼写或语法一样,等到你写完所有的代码后再美化它。

6. 不保持代码的组织

在我大学学习软件开发期间,我经常遇到糟糕的组织技能。尽管我的许多同学都是优秀的开发者,但组织能力并不是他们的强项,这使得他们的代码在查看时留下了很多遗憾。

早期学习适当的代码组织技能可以帮助你创建易于导航和处理的代码,从而减少你推动项目的时间。

根据Karl Broman的说法,代码和数据组织可以简单到如下几点:

  • 将项目的所有内容保存在一个目录中。 该目录应包含项目的所有数据、代码和结果,这样将来在继续工作或交给他人时会更方便。

  • 将原始数据与派生数据分开。 保持两个子目录,一个包含原始数据,另一个包含派生数据。包含数据总结的子目录也有助于保持数据的组织。

  • 将数据与代码分开。 将代码放在一个子目录中,将数据放在另一个子目录中(或者像上面描述的那样,三个子目录)。

  • 避免使用绝对路径,改用相对路径: 当与其他人合作时,他们可能没有在完全相同的位置复制你的项目目录,因此使用相对路径很重要,以便他们能够打开和访问你的所有文件。

  • 选择好的文件(以及变量和函数)名称: 原始数据文件名应保持不变,但代码文件名应尽可能具有描述性。变量和函数名称也是如此。

  • 文件名中永远不要使用“final”: 正如 Broman 所说,“没有什么是最终的”。多个版本的文件应附加版本号,但“最终”版本不应如此标记。

  • 编写文档和 README 文件。 文档是解释所有内容及其功能的必要手段。好的文档包括描述文件及其包含的过程。保持 README 更新很重要,同时如果有人有进一步的问题,也要包含你的联系信息。

我在之前的项目中承担的任务之一是组织团队的代码。这不仅是一个学习代码如何工作的好方法,而且是成为熟练开发者的重要技能。建立一个适合你的组织系统(我的系统与上述描述的相似)并在每个项目中付诸实践,无论大小。

7. 不休息

我告诉你一个小秘密:当你不休息时,你写的代码往往很糟糕。

在技术领域,拼命文化依然存在,工作 90 小时的周数并不少见,你可能只会偶尔起身去加点饮料,几天不见太阳也很正常。

虽然你可能觉得老板支配了你,但他们并不支配你的健康(包括身体、心理和情感健康)。

所以我希望你现在就承诺每天至少进行一个小时的活动,每天至少喝两次除了苏打水以外的饮品(开始时可以选择水,咖啡不算),每天至少出去一次,并且尽量吃不是外卖的食物。

相信我,我记不清多少次是在遛狗的时候解决了编码或逻辑问题。新鲜空气似乎对大脑的解决问题部分有奇效。

每天坐在电脑前连续编写代码八小时或更长时间而没有任何休息,会使你感到精疲力竭,生产力也会比之前更低。换句话说,生活短暂,你的工作并不是一切,照顾好自己从长远来看会让你成为更优秀的数据科学家。

订阅以便直接将我的故事发送到你的收件箱:故事订阅

请通过我的推荐链接成为会员,以获得对 Medium 的无限访问(这对你没有额外费用,我会获得少量佣金):Medium 会员

通过捐赠支持我的写作,帮助创造更多这样的故事:捐赠

思考 SQL —— 避免从上到下编写 SQL

原文:towardsdatascience.com/think-in-sql-avoid-writing-sql-in-a-top-to-bottom-approach-476a67f53a59

通过理解逻辑查询处理顺序来编写清晰的 SQL

Chengzhi ZhaoTowards Data Science Chengzhi Zhao

·发布于 Towards Data Science ·7 分钟阅读·2023 年 2 月 2 日

--

图片由 Jeffrey Brandjes 提供,来源于 Unsplash

由于 SQL 的声明式特性,你可能会觉得编写 SQL 有挑战。尤其对于那些熟悉 Python、Java 或 C 等命令式语言的工程师来说,SQL 切换起来是许多人需要的思维转换。思考 SQL 与任何命令式语言都不同,因此学习和开发的方式也不应相同。

当使用 SQL 时,你是按照从上到下的方法编写的吗?你是否从 SQL 中的“SELECT”子句开始开发?在这篇文章中,让我们探讨 SQL 的逻辑查询处理顺序,帮助你理解为何可能需要改变从上到下编写 SQL 的方式。这也有助于你更清晰地思考 SQL,并更有效地开发查询。

SQL 是一种声明式语言

声明式语言的核心概念非常棒:人类传达的是高级结构和逻辑指令,而不需要精确的流程。作为开发者,这意味着我们不需要编写如何实现目标的逐步指南。我们只需编写要实现的目标,这样我们可以更多地关注结果,让底层查询解析器和引擎来决定如何执行指令。

编写 SQL 时,你更关注输入和输出,而执行是幕后的神奇黑匣子。对于详细的执行,用户提供计划和优化的偏好。但执行仍然由查询引擎在运行时处理。

然而,这并不意味着我们可以挥舞魔法棒,一切都会如愿以偿。仍然需要高级指令来提供以下信息。

  • 数据源在哪里(FROM

  • 应用什么条件(WHERE/HAVING

  • 是行级别还是聚合(GROUP BY

  • 如何显示结果(SELECT

  • 如何排序数据(ORDER BY

  • 返回多少行(LIMIT

与获取数据的命令式语言不同,我们不需要编写复杂的逻辑或逐行循环。SQL 查询就像一个蓝图,SQL 引擎是构建者。作为用户,我们只需等待输出,而不必担心它如何获取结果。

SQL 逻辑查询处理顺序

如果你曾使用过其他数据处理框架的命令式语言,甚至对于 Spark 或 Pandas 用户,我们总是需要先有数据集进行处理。获取数据源的地方通常是任何数据处理框架的入口点,SQL 也是如此。

要理解 SQL 逻辑查询处理顺序,让我们用 SQL 查询在语法时间来比较它与逻辑处理顺序。

SQL 语法与逻辑查询处理顺序标记 | 图片由作者提供

  1. FROM: SQL 的入口点是 FROM 子句。它通常在“SELECT”语句之后定义,并作为准备阶段来指导查询引擎从哪些表中提取数据

  2. ON: 在每个表中评估的条件,用于检查要连接的键

  3. JOIN: 一旦知道要连接哪些键,SQL 引擎会检查需要应用哪些类型的连接(内连接、外连接、交叉连接等)

  4. WHERE: 应用行级过滤器

  5. GROUP BY: 指定要聚合的键,并改变原始行级表的视图。在这个阶段之后,所有处理的内容都会是聚合级别而不是原始行级别。如果使用了 cube 或 roll-up 聚合,也会在这个阶段发生。

  6. HAVING: 应用聚合级过滤器。我们还可以编写嵌套查询或 CTE(公共表表达式)在聚合级别进行过滤。不过,紧挨着 GROUP BY 子句使用更为方便,并且可能有利于 SQL 引擎优化。

  7. SELECT: 选择要显示给用户的字段。对于需要复杂逻辑的派生字段,如窗口函数(rank()、row_number() 等)、或 case 语句,或聚合函数,这些操作都会在此时进行。如你所见,SELECT 在 SQL 逻辑查询处理顺序中较晚。如果你先从 SELECT 子句开始,可能很难预见你会如何编写其他 6 个语句,从而可能导致 SELECT 中出现意外结果或错误。让我们在下一节中详细讨论。

  8. ORDER BY: 最终结果的排序顺序。在这个阶段,我们可以解析你在 SELECT 子句中定义的别名。

  9. LIMIT: 返回结果的数量,或者如果你想跳过几行顶部数据,可以结合 ORDER BY 使用。

让我们把 SQL 语句从语法时间重新排列成 SQL 逻辑查询处理顺序,这样会更清晰。

SQL 逻辑查询处理顺序 | 作者图像

微软在 SELECT 语句的逻辑处理顺序 上有很好的文档。

“SELECT” 不应该是编写 SQL 时的第一个单词

“SELECT” 通常是阅读 SQL 语句时的第一个子句。我们的脑袋在阅读或写作时,通常采用自上而下的方法。首先使用 “SELECT” 定义了我们希望显示的结果。将 “SELECT” 作为第一个单词符合这种模式。此外,将 “SELECT” 放在首位是语法正确的,可以编译并运行整个 SQL 脚本。

当你为数据分析编写 SQL 查询时,有多少次你首先写下了以下内容:

SELECT col1, col2, CASE (…), ROW_NUMER(…)

有效!一个编译并正确提取结果的 SQL。

但是,正如你在上面看到的,SELECT 在 SQL 逻辑查询处理顺序中被评估得较晚。 以下是避免首先在 SELECT 中编写所有内容的一些原因:

  • 很难预测你会在 SELECT 之前为语句写些什么。 你有所有的表名吗?你有定义别名吗?你是在处理行级数据还是汇总级数据?我知道有人可以很好地预测所有 SQL 以及所有表和别名。不幸的是,我不擅长预见和记忆。按逻辑查询处理顺序编写 SQL 给了我下一步要写的参考。如果在编码时被打断,它也给我一个提示,使我更容易从中断处恢复。

  • 逻辑查询处理顺序更适合我们的脑袋。 如果你编写 SQL,可能熟悉 ETL 概念(提取、转换、加载)数据。ETL 概念遵循逻辑顺序:你获取数据集,对数据集进行操作,将重构后的数据集放到其他地方。在 SQL 中编写应该遵循相同的逻辑顺序,“SELECT” 更适合作为转换或加载阶段,我们应该先准备数据集,以便为 “SELECT” 提供数据。

  • 它有助于调试和推理错误。 通过遵循逻辑查询处理顺序,一些错误变得明显。例如这个 StackOverflow 问题:“SQL 在 WHERE 子句中未识别列别名”,通过这篇文章,我们可以快速回答:SELECT 别名尚未被评估,因此 SQL 引擎没有你给定的别名的上下文,因此失败了。(一些现代 SQL 供应商进行了一些优化以避免这个问题,但并非所有 SQL 提供商都适用,我们仍应注意这一点)

编写 SQL 的技巧:逻辑查询处理顺序

  • 我们仍然可以先写“SELECT”,但只写这 6 个字母;将其作为占位符,以提醒我们在内容准备好时填入剩余的内容。

  • 从 ETL 角度思考,数据准备对于为其他工作奠定基础至关重要。因此,首先需要关注的是正确编写FROM、ON 和 JOIN 语句。

  • 如果遇到错误,请按照逻辑查询处理顺序进行调试。SELECT * 在生产环境中很糟糕,但我们仍然可以在调试中使用它。我们还可以通过 LIMIT 子句进一步防止拉取过多的数据。

最终思考

如果你从上到下编写 SQL 并且这种方法让你在思考 SQL 时感到困扰,你应该考虑理解 SQL 逻辑查询处理顺序并进行这种练习。

我在准备 Microsoft Exam 70–461 时很早就学到了这种方法,Itzik Ben-Gan 有一本很棒的书 Querying Microsoft SQL Server 2012 (MCSA)。这本书的优点在于它对 SQL 基础的解释远胜于其他书籍中那些花哨的语法。

思考 SQL 是至关重要的。你可以推理事情为何如此发生,并迅速解决错误。此外,它帮助你更好地组织代码,以更有条理的方式解决复杂查询。

我希望这个故事对你有帮助。本文是我工程与数据科学故事的系列之一,目前包括以下内容:

Chengzhi Zhao

Chengzhi Zhao

数据工程与数据科学故事

查看列表53 个故事

你也可以订阅我的新文章或成为推荐的 Medium 会员,享受对 Medium 上所有故事的无限访问权限。

如有问题/评论,请随时在本文评论区留言,或通过LinkedinTwitter 直接联系我

考虑微调 LLM?在你开始之前,这里有 3 个考虑因素

原文:towardsdatascience.com/thinking-about-fine-tuning-an-llm-heres-3-considerations-before-you-get-started-c1f483f293

Sean SmithTowards Data Science Sean Smith

·发表于Towards Data Science ·11 分钟阅读·2023 年 6 月 15 日

--

照片由Brett Jordan拍摄,来自Unsplash

目前 LLM(大型语言模型)和生成 AI 正当红。来自IBM的惊人统计数据显示,近 2/3 的 C-Suite 高管感受到来自投资者的压力,要求加快生成 AI 的采纳。这种压力自然会传递给数据科学和机器学习团队,他们负责应对炒作并创建成功的实施方案。

随着环境的发展,LLM 的生态系统在开源模型和行业模型之间发生了分化,形成了一个迅速填补的护城河。这一新兴场景促使许多团队考虑以下问题:我们如何使 LLM 更具体地适用于我们的用例?

在这篇文章中,我们探讨了在考虑投入时间和工程周期来构建一个细分 LLM 时需要重点关注的一些关键因素。在这段旅程中,了解一些关于潜在限制和最佳实践的最新研究至关重要,这些研究涉及如何构建微调的语言模型。阅读完这篇文章后,你将会有更多的想法来指导你的组织做出正确的决定——是训练还是不训练,以及如何训练。

你可能无法用开源模型模拟 GPT

任何人都知道,OpenAI 正在通过其最新的 GPT 版本引领 LLM 的发展。因此,许多利益相关者可能会要求开发团队部署一个模型,以模仿更强大模型的结果,原因可能包括(速率限制、数据隐私、成本等)。这自然引发了开发者的疑问:我们能否从 GPT 生成输出,并利用这些输出来微调一个模型?

这个问题的答案仍然不确定,因为它似乎取决于几个因素。这个特定任务,称为模仿学习,涉及通过使用来自更先进模型(如 GPT)的目标观察来微调训练一个新的语言模型。虽然这似乎是从下游模型中获得良好性能的一个好方法,但也存在一些潜在的问题。

图源自参考文献 Gudibande 等人 [1]。

一篇题为“模仿专有 LLM 的虚假承诺” [1] 的最近论文对这种方法可能遇到的陷阱提供了一些见解。作者展示了一些实验,表明增加更多模仿数据可能会导致模型性能的下降。查看上图,我们可以看到在中心图表中,基准任务上的准确性随着令牌数量的增加而下降。那么这是为什么呢?

作者建议出现这种情况的原因是,模仿模型学习的是它们模仿的模型的风格,而不是学习和理解模型的内容。查看上图左侧的窗格,人工评审员更喜欢模仿模型的结果而非 ChatGPT 的结果。经过探索,明显看到评审员喜欢模仿模型的风格,但并未仔细审查内容。注意到模仿模型产生的内容通常缺乏事实准确性,作者总结道:“模仿模型实际上体现了 AI 助手的一些最糟糕的方面:它们的回答听起来很自信,但比 ChatGPT 的回答更不准确。”

重要的是要注意,有些情况下模仿模型可以取得出色的表现。作者指出,模仿模型可以在本地任务或复制教师模型非常特定行为的任务上表现良好。在为研究创建的任务 NQ-Synthetic 中,作者要求语言模型生成与给定背景相关的 10 个问题和答案。值得注意的是,模仿模型的得分接近 GPT。这表明,更具体的模型在尝试模仿教师模型的行为时可能会取得有利的结果。

论文中的一个有趣的推论是,使用教师模型对模型进行微调实际上可以帮助减少模仿模型的毒性评分。这对于希望快速发布开源大语言模型的公司非常有用,而无需进行繁琐的过滤器构建任务。公司可以选择训练一个从精心挑选的教师模型中获得的输出,以获得一个良好的起点,而不是手动尝试构建过滤器。

值得一提的是最近发布的 Orca,这是一款由微软研究院开发的模型,它在训练数据中融入了 GPT 的信号。不同之处在于用于模型训练的数据量。Orca 在 500 万示例上进行了微调,而广覆盖的模仿模型则在大约 15.1 万条观察数据上进行了调整。由于我认为大多数受众不会花费 16000 美元来训练大语言模型作为随意实验,我倾向于发表更接近于模仿建模论文的声明,而非 Orca。话虽如此,我们还需要等待更多研究,以确定模仿学习在更广泛任务中成为可行选项所需的最小示例数。

结论:根据任务的复杂性,尝试用较弱的模型模仿 GPT 或任何复杂模型的输出可能会导致模型性能较差。

上下文学习是你所需的一切吗?

上下文学习(In-Context Learning),或称为少量样本学习(Few Shot Learning),是将任务特定的示例包含在提示中的过程。这种方法对于复杂的语言模型特别适用,因为开源模型尚未具备处理上下文学习所需的灵活性。通常,这种方法可以取得很好的结果,但你是否曾经想过为什么会这样?

Dai 等人[3]的论文探讨了在提示中加载示例与使用相同示例进行微调之间的数学联系。作者展示了提示示例产生的元梯度在推理时的前向传播中得到体现。对于微调,示例实际产生的是真实梯度,用于更新权重。因此,上下文学习似乎达到了与微调类似的结果。为了更深入理解这些发现,我建议阅读该论文,其中详细阐述了数学联系。

尽管上下文学习的方法很棒,但存在一个在精细调整中不明显的限制。如果我们有大量的训练数据,精细调整的模型将通过在训练过程中更新模型的真实梯度来利用所有这些数据。在上下文学习中,我们只能提供有限数量的观察。因此,出现了一个问题:鉴于大量的训练语料库,我们如何利用最相关的示例来实现最佳结果?

解决这个问题的一种方法是使用启发式方法选择示例,幸运的是,LangChain 提供了支持。LangChain 是一个 Python 模块,基本上包含了简化与语言模型工作相关的预构建提示。我们现在关注的 LangChain 工具是 ExampleSelector

def get_similarity(seq_a: str, seq_b: str) -> Union[float, int]:
    """ 
    Make a similarity heuristic,
    here we use Jaccard similarity or IOU

    seq_a: First sequence to compare
    seq_b: Second sequence to compare

    Returns:
    Similarity score (float or int)
    """
    # Tokenize
    set_a = set(seq_a.split(' '))
    set_b = set(seq_b.split(' ')) 

    # Calculate IOU/Jaccard similarity
    return len(set_a.intersection(set_b)) / len(set_a.union(set_b))

def example_selector(examples: List[str], input: str, examples2use: int) -> List[str]:
    """ 
    Pseudo code for an example selector

    examples: List of training corpus
    input: Target sequence to translate
    examples2use: Number of examples to use

    Returns:
    List of selected examples
    """
    scores = [get_similarity(example, input) for example in examples]
    sorted_idx = [i for i, _ in sorted(enumerate(scores), key=lambda x: x[1], reverse=True)]
    return examples[sorted_idx[:examples2use]]

ExampleSelectors 是一种提示操控器,允许我们在推理过程中动态改变使用的示例。有很多启发式方法可以使用。上面我创建了一些 LangChain 的选择器如何工作的伪代码。我使用了输入序列和示例序列之间的 jaccard 相似度。在 LangChain 中还有更多选项,请查看 这里

采用这种方法有两个主要好处。首先,你可以使你的 LLM 数据高效,通过选择最相关的示例来处理给定的输入。这与为所有观察加载少量静态示例相对。第二个好处是节省成本,如果通过托管服务进行调整的话。写作时,使用精细调整的基础 Davinci 模型的费用为每 1,000 个 tokens $0.12。相比之下,使用 instruct Davinci 的费用为 $0.02,这是一种 500% 的价格增长!这些价格也不包括培训的费用。

需要注意的是,这些价格可能会发生变化,因为 OpenAI 目前尚未使用 LoRa 或适配器,这在一篇现已删除的 博客文章 [5] 中有透露。然而,由于需要为每个用户维护自定义权重,精细调整的模型仍然可能更昂贵。这也不包括上下文中示例的费用。你的团队需要评估从成本和准确性的角度来看,ICL 还是精细调整更适合你的任务。

要点:使用动态示例加载的上下文学习可能会在没有来自托管服务的额外成本的情况下实现与精细调整相同的结果。

你的任务是否从最终推理之前的一个或多个中间步骤中受益?

假设你试图回答关于长文档的复杂问题。这项任务从根本上要求语言模型具备良好的语言掌握和理解能力。这引出了一个问题:如果我们帮助语言模型将推理过程分解为子任务,类似于人类如何分析文档并按顺序执行任务呢?

来自 Sun 等 [4] 的图示。

这正是微软研究人员设定的目标,他们的解决方案是 PEARL [4]。PEARL 代表规划和执行行动以推理长文档。整体框架分为三个步骤:

  1. 行动挖掘: 语言模型首先被提示阅读文档,并提取可能用于回答领域特定问题的行动。为了提取这些行动,语言模型会提供几个示例行动。以下是一个行动可能的示例。

  2. 计划生成: 在生成一组任务特定行动后,LLM 现在被要求根据问题和上下文生成一个后续的行动列表。LLM 会提供其他任务的计划示例,这有助于构建高质量的计划。有关技术细节的更多信息,请参阅论文。

  3. 计划执行: 现在模型已经有了计划。我们现在将输入提供给模型并执行计划。

来自 Sun 等 [4] 的示例行动。

在各个阶段之间使用了一些中间步骤以确保质量。作者包括一个自我校正步骤,确保计划符合所需格式。还有一个自我精炼步骤,确定计划是否可以在以后作为少量示例使用。

来自 Sun 等 [4] 的表格。

在评估中,PEARL 展示了相较于其他 GPT 模型的显著改进,尤其是在处理长文档时。这个过程的关键收获是,在某些情况下,拥有多个步骤可以显著帮助模型。

另一个中间步骤证明有益的场景是当要包含在上下文中的文档数量超过语言模型支持的范围时。当前,OpenAI 使用的注意力机制的规模为 O(n²),尚无解决方案来克服这一点 [5]。这引起了对将上下文缩减到最小形式的强烈兴趣。

根据你的任务,有处理此问题的方法。例如,如果你的任务完全围绕实体展开,可以提取相关实体及其相关属性。你可以将这种方法视为一种有损压缩,允许你向 LLM 提供更多上下文。这个中间步骤的另一个好处是,你将非结构化数据转换为结构化格式,这使得你能够在没有 LLM 的情况下做出明智的决策。下面的图示来自 Fei 等人[6]展示了这一任务的一个示例。

图示摘自 Fei 等人[6]

总结:将任务分解为较小的后续问题可以帮助将较大问题简化为更易管理的部分。你还可以利用这些较小的任务来解决与模型限制相关的瓶颈。

总结思考

这些是关于研究人员在 LLM 性能和效率的新前沿中探索的一些一般想法。这不是微调模型时需要考虑的所有事项的详尽列表,但在考虑这段旅程时是一个很好的起点。

进一步阅读,Hugging Face 关于训练 LLMs 的这篇文章非常有趣,是探索本地问题模仿模型的一个好起点。对LangChain有一个具体的理解也是极其有帮助的。尽管大部分库可以为你的用例重写,但主要好处在于,如果其他人为你编写代码,你可以更容易跟上研究进展!

再次总结要点:

  1. 根据任务的复杂性,试图用较弱的模型模仿 GPT 或任何复杂模型的输出可能会导致模型性能差。

  2. 通过动态示例加载进行上下文学习可能会达到与微调相同的效果,而不会带来额外的管理服务成本。

  3. 将任务分解为较小的后续问题可以帮助将较大问题简化为更易管理的部分。你还可以利用这些较小的任务来解决与模型限制相关的瓶颈。

感谢阅读这篇文章!我的主要兴趣领域是为用户创建个性化互动。请考虑互动文章并关注我在 Medium!如果你想联系我以讨论文章中的技术错误或保持联系,请通过 LinkedIn联系我。

参考文献

[1] Arnav Gudibande, Eric Wallace, Charlie Snell, Xinyang Geng, Hao Liu, Pieter Abbeel, Sergey Levine, & Dawn Song. (2023). 模仿专有 LLMs 的虚假承诺。

[2] Mukherjee, S., Mitra, A., Jawahar, G., Agarwal, S., Palangi, H., & Awadallah, A.(2023)。Orca:从 GPT-4* 复杂解释轨迹中逐步学习。arXiv: 计算与语言*。

[3] Damai Dai, Yutao Sun, Li Dong, Yaru Hao, Shuming Ma, Zhifang Sui, & Furu Wei.(2023)。为什么 GPT 能在上下文中学习?语言模型隐式地作为元优化器执行梯度下降。

[4] Simeng Sun, Yang Liu, Shuohang Wang, Chenguang Zhu, & Mohit Iyyer.(2023)。PEARL:引导大型语言模型规划和执行长文档上的动作。

[5] Habib, R..(2023)。根据 Sam Altman 的说法,OpenAI 的计划。

[6] Hao Fei, Fei Li, Chenliang Li, Shengqiong Wu, Jingye Li 和 Donghong Ji,(2022)。继承前人的智慧:一个用于统一基于方面的情感分析的多层级级联框架

超越数据科学的种种框架

原文:towardsdatascience.com/thinking-outside-data-sciences-many-boxes-d84f0e85a668?source=collection_archive---------5-----------------------#2023-05-11

TDS EditorsTowards Data Science TDS Editors

·

关注 发表于 Towards Data Science ·作为 Newsletter ·3 分钟阅读·2023 年 5 月 11 日

--

数据科学的两大支柱——基于统计的分析和代码——都带来了各种约束。如果你的查询结构错误,可能会搞乱整个流水线。如果应用公式不正确,你的测试结果可能不再反映现实。

在约束条件下工作并不一定感到僵化或限制,恰恰相反。一个灵活的数据专业人士在仍然在预定的一组参数内工作时,通常需要发挥他们的创造力。这在我们的日常工作流程中成立,在我们学习新技能的方式中成立,也在我们为成功的数据职业做准备的方式中成立。

我们本周突出的文章涵盖了非常广泛的领域,从构建推荐系统到为您的投资组合寻找项目创意。它们的共同点是对许多业内人士可能认为已解决的问题和挑战提供了全新的视角。如果“我们一直这样做”是你经常嗤之以鼻的句子,那么你可能会在本周的精选中找到合适的内容。

  • (重新)考虑仪表盘。在三年前宣布仪表盘的“死亡”后,Taylor Brownlow重新审视了这一普遍存在、偶尔无用、有时不可或缺的数据团队工具,提倡对仪表盘以及数据专业人员如何生成和传达有意义见解的更细致的看法:“改变我们的工作方式比采用新工具要困难得多。”

  • 生成推荐的全新方法。从电子商务网站到 Netflix,推荐系统多年来一直塑造着我们的在线体验。Amine Dadoun的新文章建议通过利用最新的知识图谱技术,来振兴这个广泛领域。

  • 退一步是最具创新性的解决方案。面对复杂的新项目,一些机器学习工程师可能急于尽快应用最先进的算法。正如Olga Chernytska在她的新系列文章开篇所解释的那样,他们真正应该做的是提前规划,以确保他们的解决方案既满足业务需求又符合技术要求。

图片由Jan Canty拍摄,来自Unsplash

  • 一些最有用的技能无法用代码行数来衡量。职位描述(以及招聘经理)通常倾向于可测量和易于评估的内容:X 年的这项技能,Y 年的那项技能。Abhi Sawhney的首篇 TDS 文章是对这种倾向的有益补救,突出了其他可以帮助你作为数据分析师脱颖而出的领域,从积极主动到同理心。

  • 是时候从“泰坦尼克号”上划走了。在竞争激烈的工作环境中,小细节可能会产生不同的影响——包括你作品集中的项目组合。Matt Chapman认为你应该把那些经过验证的数据集和过度分析留给其他求职者,并提供了几个你可以探索的有前景的方向。

这周我们还有一些阅读推荐,希望你们去看看!

  • 想要了解 ChatGPT 最近更新的隐私功能的动手概览,不要错过Andrea Valenzuela的最新文章。

  • 语言公平是大型语言模型的一个方面,这是从业者讨论不够充分的;幸运的是,Yennie Jun深入探讨了这个棘手的问题。

  • 为了成功地将意图传达给语言模型,Scott LundbergMarco Tulio Ribeiro坚称清晰语法的重要性。

  • 通过跟随Nazlı Alagöz的初学者友好指南,了解合成差分法的优缺点。

  • 我们很高兴启动了Paul Iusztin的新课程,该课程涉及设计、实施和部署机器学习系统,采用 MLOps 最佳实践。

感谢你支持我们的作者!如果你喜欢 TDS 上的文章,可以考虑成为 Medium 会员——它可以解锁我们的整个档案(以及 Medium 上的其他所有帖子)。

直到下一个变量,

TDS 编辑部

为什么以人为本的 AI 设计指南在制造业中使用时可能优雅地失败

原文:towardsdatascience.com/this-is-why-human-centred-ai-design-guidebooks-can-gracefully-fail-when-used-in-manufacturing-95ceae0aad21?source=collection_archive---------10-----------------------#2023-05-09

从一个使用案例中学习

Yuji YamamotoTowards Data Science Yuji Yamamoto

·

关注 发表于 数据科学前沿 ·9 分钟阅读·2023 年 5 月 9 日

--

图片由 UX Indonesia 提供,来源于 Unsplash

我们看到在各个社区对以人为本的 AI(HAI)的关注日益增加。HAI 的基本理念是将人类和人性置于设计 AI 驱动应用程序的中心。HAI 设计寻求实现人类与 AI 的共生——AI 辅助人类任务而不是取代它们,人类通过提供反馈来改进 AI。

像 Google、Microsoft、IBM、Apple 等大科技公司重视 HAI 理念,并开发了自己的 HAI 设计方法,并公开分享作为指南。例如,Google 的 PAIR 研究团队提供的People+AI 指南展示了如何组织和促进一系列研讨会,在这些研讨会上,不同领域的专家共同设计 AI 应用的功能和用户界面。它还提供了一系列在 HAI 设计过程中需要解决的问题和指导,如“应用程序的用户价值是什么?”以及“如何向用户解释预测结果?”。该指南进一步提供了各种实际应用 HAI 设计的示例用例,以激发设计参与者的灵感。Microsoft 分享了一种名为“HAX 工具包”的 HAI 方法。它提供了 PowerPoint 和 Excel 格式的设计指南和工作簿,目的是与 PAIR 的指南类似。

这些 HAI 方法的本质是相似的;它们允许多领域的人参与设计过程,并通过将用户体验、设计思维和负责任的 AI 的理论与实践融入统一的设计框架,促进捕捉和转化人们的需求为应用设计。

ThisisEngineering RAEng拍摄,照片来自Unsplash

好的,现在我来谈谈制造业:)。你可能认为制造业是一个可以轻松自动化的领域,但事实并非如此!即使在许多现代工厂中,许多技术熟练的人员仍在工作,并在开发、运行和改进工厂操作中扮演重要角色。因此,创造人类与机器之间的和谐至关重要。

那么,在制造业中整合 AI 技术时,为什么不使用 HAI 方法呢?这正是我们——一个拥有 AI、制造和用户体验专业知识的研究团队——的想法。我们与一家大型跨国制造商合作,试图开发并实施一种用于制造过程中的异常检测的机器学习模型。该模型检测来自关键制造设备中安装的传感器设备的数据中的异常模式。该模型的原型已经存在。我们使用了《People+AI》指南来帮助公司的 AI 项目。选择这种方法是因为它似乎是最全面且结构良好的方法。我们通过为期一天的研讨会使用了这种 HAI 方法,参与者包括约十名公司成员,涵盖了研发工程师、工艺工程师、数据科学家、技术人员和精益六西格玛专家等不同角色。

那么,使用这种方法的结果如何呢?嗯,我不会说它完全失败,但也不特别成功。总体而言,这种方法未能有效应对设计 AI 驱动的工业过程应用时复杂多面的挑战。研讨会的组织者(我们)和参与者感到他们必须同时处理来自不同角度的太多问题,导致了认知过载,并且体验混乱无序。

但我们从中学到了很多!我们意识到这种方法根本不适合制造业背景,需要对方法进行重大重构,以应对我们遇到的挑战。考虑到 HAI 方法之间的相似性,我们认为如果使用其他 HAI 方法,结果不会有太大不同。

让我们分享一下我们对这种方法在制造业中优雅地失败的反思。几个因素导致了这种失败,但在本文中,我挑选了三个重要的因素。希望这篇文章对那些有兴趣在工业环境中使用 AI 的人无论你的专业是什么,都能带来一些启发。

1. 工作流设计不是该方法的一个组成部分:

目前,科技公司和其他企业的 HAI 方法似乎主要针对帮助设计单一用户使用的应用程序,例如面向消费者的手机应用。在这种使用情况下,人机交互通常通过用户的屏幕、手指、眼睛和耳朵进行。指南似乎在设计这种交互时表现良好,使设计师能够探索不同的用户场景和体验,找到自动化和用户控制之间的平衡,管理对 AI 能力的期望等等。

另一方面,工业设施中的 AI 服务背景可能非常复杂。我们可以设想在制造厂使用异常检测的案例。这个应用程序通过放置在车间地板上的监视器屏幕显示传感器设备的健康状态,并在检测到异常时发出警报。应用程序的第一手用户是操作员。当然,操作员与应用程序之间的交互很重要,但事情并不止于此。当操作员收到警报时应该做什么或想做什么?这个人是否希望通过应用程序的帮助深入分析情况?或者他或她是否应该咨询上级或技术人员进行进一步的分析和决策?是否应该立即联系设备供应商?适当的行动是否取决于异常的严重性?行动是否取决于这些人的技能和知识?需要多少利益相关者参与决策?他们需要什么信息?如何在这些参与者之间共享信息?

如你所见,在制造环境中,初始事件——在此例中是发出警报——通常会触发一系列复杂的动作链,这些动作可能涉及组织内部或外部的多个人员。我们称这系列动作为工作流。我们已经了解到,手指-眼睛-屏幕的交互很难在没有工作流设计的情况下进行设计。因此,同时考虑这些设计或至少在开发过程中较早规划工作流设计是至关重要的,因为它们紧密相连。

HAI 方法是否支持这一点?不,工作流设计部分没有。在案例公司的研讨会上,设计参与者很高兴创建了关于异常状态和其他相关信息在车间地板上应如何显示的不同纸质原型。然而,由于他们对工作流如何展开了解有限,他们很快对哪些原型适合实际使用感到不确定。车间地板上的警报只是工作流的一个触发器。可能还有更多场景触发其他工作流,如假阴性、假阳性、传感器退化、传感器升级等。在没有适当的方法论支持的情况下,想象所有这些场景及其相应的工作流需要参与者付出大量的认知努力。

照片由Cristina Gottardi拍摄,来自Unsplash

2. 设计指南和问题引发了大量额外的询问:

正如我们在本文介绍部分讨论的那样,PAIR 指南手册与其他 HAI 方法一样,提供了一套在应用设计过程中需要考虑的问题和指南。我可以在这里展示一些更多的例子;“如何建立适当的信任水平,以便用户不会对 AI 结果给予过多或过少的信任?”,“应用程序如何接受用户的反馈以改进应用程序的行为?”

这些问题或指导无疑对我们在设计过程中深入处理关键设计问题大有裨益。同时,解决这些问题需要广泛的假设思考,特别是对于行为具有概率性的 AI 驱动应用程序。在开发过程中,应用程序的具体行为并不总是明确的。对于较简单的交互,如移动电话应用,假设思考可能仍可管理。然而,在公司的研讨会上,假设思考很快发展到了我们无法处理的程度。

在研讨会开始时,除了参与者愿意在操作中使用异常检测模型外,几乎没有决定。我们遵循了指南手册建议的设计过程,设计指南和问题似乎对这个过程有所帮助。然而,研讨会的参与者很快对哪些问题和指南比其他问题和指南更重要以及问题应该回答的深度或细节感到不确定。这些问题也紧密相连。

因此,回答这些问题变成了大量的猜测工作。以一个设计问题为例——如何建立用户对应用程序的信任。许多因素可以影响这一点,但至少它依赖于预测结果如何呈现给用户。展示的设计可能会受到模型性能的影响。性能将受到在开发过程中尚未完全了解的生产阶段数据的影响。正如我们之前讨论的,结果展示还依赖于工作流程。

如你所见,一个设计问题会引发一系列其他相互关联的问题,这些问题很难一一回答。一个答案依赖于另一个答案,而另一个答案又依赖于另一个只能部分回答的答案……难怪参与者很快就感到困惑和不知所措。一位参与者在最后说:“好吧,我们现在知道前方有座巨大的山,但我们仍然不确定如何攀登它。”

图片由 Luis Villasmil 提供,来源于 Unsplash

3. 整合收集信息的责任不明确:

HAI 方法帮助设计参与者生成设计 AI 驱动应用所需的大量信息。这些方法提供了各种工具,如创意卡片、设计问题、指南和工作簿,以协助生成和记录这些信息。

那么谁来整合所有这些信息呢?通过研讨会,显而易见,这个方法主要是从 UX 设计师的角度设计的,而设计师似乎是那个整合信息并将其转化为设计的人。

好吧,我们理解“以人为中心的 AI”这个短语在 HAI 方法中被强调,但它们对 UX 的偏向性较大。这种偏向可能不会在方法用于更简单的交互(如手机应用)时混淆人们。UX 设计师在设计这种应用的功能和界面方面经验丰富。

但是当这个方法用于工业过程,其中工作流设计是交互设计的关键和不可分割部分时情况如何?在这种多面向的使用案例中,UX 设计师是否仍应整合来自研讨会的信息?还是具有广泛和深入工业过程理解的项目负责人更适合这个任务?我们在没有明确理解这一问题的情况下开始了研讨会,这进一步使得研讨会(已经是一团糟!)变得复杂。

最终,从失败中学习并继续前进…

这三个因素已经足以使参与者感到不堪重负,并造成认知超负荷和混乱。我们只是带着对 HAI 方法在工业过程应用中的局限性不够了解的心态进入了研讨会。虽然这些方法为我们提供了坚实的基础,但我们发现需要进行重大修改以适应制造领域。

我们目前正在开发一种基于我们学习的新方法,并在公司进行测试。我们至少知道,工作流设计应该融入到这个方法中,并且该方法应该有效处理在设计过程中出现的一系列相互关联的“假设”问题。希望我们能在未来报告结果!! 😃.

本博文与我的同事 Kristian Sandström 和 Alvaro Aranda Munoz 一起撰写。谢谢!

关于状态保持机器学习、在线学习和智能机器学习模型再训练的思考

原文:towardsdatascience.com/thoughts-on-stateful-ml-online-learning-and-intelligent-ml-model-retraining-4e583728e8a1?source=collection_archive---------14-----------------------#2023-04-05

设计可扩展的在线和离线持续学习系统架构

Kyle GallatinTowards Data Science Kyle Gallatin

·

关注 发表在 Towards Data Science ·6 min read·2023 年 4 月 5 日

--

自从我读了 Chip Huyen 的实时机器学习:挑战与解决方案后,我一直在思考生产环境中机器学习的未来。短反馈循环、实时特性以及能够在线学习的状态保持机器学习模型部署需要一种与我今天所用的无状态机器学习模型部署截然不同的系统架构。

我在墨西哥科苏梅尔思考有状态的机器学习——图片来自作者

过去几个月,我一直在进行非正式用户研究、白板讨论和临时开发,以探讨真正有状态的机器学习系统可能是什么样的。大部分内容都概述了我的思维过程,并且我继续深入这个领域,发现有趣且独特的架构挑战。

定义

有状态(或连续)学习 涉及到更新模型参数,而不是从头开始再训练,以便于:

  • 缩短训练时间

  • 节省成本

  • 更频繁地更新模型

无状态与有状态的再训练——来自Chip Huyen的许可

在线学习 涉及实时从真实样本中学习,以便于:

  • 提升模型性能和反应性

  • 缓解因漂移/过时导致的性能问题

目前,大多数行业中的学习都是离线进行的。

智能模型再训练 通常指的是使用某些性能指标自动再训练模型,而不是按照固定的时间表进行,以便于:

  • 降低成本而不牺牲性能

目前,大多数行业中的模型都是使用 DAG 按照时间表再训练的。

来自自动模型再训练指南的智能再训练架构——由 Arize AI 授权

为在线学习设计 MVP

在上一篇文章中,我尝试运用基础工程原理来创建一个极其简单的在线学习架构。我的第一个想法是——将有状态的在线学习架构建模为有状态的网页应用程序。通过将“模型”视作数据库(其中预测为读取,增量训练会话为写入),我认为可以简化设计过程。

图片来自作者

在某种程度上,我确实做到了!通过使用在线学习库River,我构建了一个小型有状态的在线学习应用,这让我能够实时更新模型并提供预测。

Flask 应用程序在多个工作进程之间共享内存中的模型——图片来自作者

这种方法在编码时很酷很有趣——但在规模上存在一些根本性的问题:

  1. 无法横向扩展: 我们可以轻松地在单个应用程序的内存中共享一个模型——但这种方法在像 Kubernetes 这样的编排引擎中无法扩展到多个 pod。

  2. 混合应用职责: 我不知道(也不想成为第一个知道)关于尝试支持一个混合训练和服务的部署的注意事项。

  3. 预先引入复杂性: 在线学习是最积极主动的机器学习类型,但我们甚至还没有验证我们是否需要它。必须有一个更好的起点……

设计一种可扩展的架构

从现有标准开始——分布式模型训练。使用类似参数服务器的东西作为集中存储,同时多个工作者计算部分/分布式梯度……或其他什么……并在之后调整参数是相当常见的做法。

所以——我想尝试在实时模型服务部署的背景下思考这个问题,结果想出了最傻的架构。

一种没有意义的架构——作者提供的图片

分布式模型训练旨在加快训练过程。然而,在这种情况下,没有必要以分布式方式进行训练服务——保持训练去中心化会引入复杂性,并且在在线训练系统中没有实际用途。完全分开训练更有意义。

一种稍微有意义的架构——作者提供的图片

很好!有点。此时我不得不退后一步,因为我做了很多假设,可能有些过于前瞻性:

  1. 我们可能无法在接近实时的情况下获得真实情况。

  2. 持续在线训练可能不会比持续离线训练提供净收益,而且是一种过早的优化。

  3. 离线/在线学习也可能不是二元的——有些场景中我们可能需要/想要两者都具备!

一些合理的智能再训练、持续学习和在线学习架构

让我们从一个更简单的离线场景开始——我想使用某种机器学习可观察性系统,根据性能指标的退化自动再训练模型。在进行持续训练(且模型权重更新不会花费太长时间)的场景下,这在不显著影响业务的情况下是可行的。

智能再训练和持续在线学习——作者提供的图片

真棒——这是我今天画的第一个合理的东西!这个系统可能比无状态训练架构有更低的成本开销,并且对模型/数据的变化反应迅速。通过仅在需要时进行再训练,我们节省了大量的$,总体上也非常简单!

然而,这种架构有一个大问题……它远没有那么有趣!什么样的系统能兼具在线学习的所有反应性、连续学习的成本节约和在线学习的弹性呢?希望,像这样……

连续的在线学习 — 作者提供的图像

虽然还有很多细节我还没完全弄清楚,但这种架构有很多好处。它允许混合在线和离线学习(正如特征存储允许访问流式特征和离线计算的特征一样),对数据分布的变化或个别用户偏好(个性化系统(recsys))非常稳健,并且仍然允许我们集成 ML 可观测性(O11y)工具来不断测量数据分布和性能。

然而,尽管这可能是我迄今为止创建的最合理的图表,它仍然留有很多未解的问题:

  • 在在线系统中,我们如何/何时评估模型,以及使用什么数据?如果数据分布发生大幅变化,我们需要创建新的数据驱动方法和最佳实践,设计一个包含旧数据和最新数据的保留评估集。

  • 我们如何调和将训练过程分为批处理/离线和在线的 ML 模型?我们需要尝试新的技术和系统架构,以允许在像这样的系统中涉及大型 ML 模型的复杂计算操作。

  • 我们如何拉取/推送模型权重?按照一定的节奏?在某些事件中或根据某些指标的变化?这些架构决策中的每一个都可能对我们系统的性能产生重大影响——而且没有在线 A/B 测试或其他研究,将很难验证这些选择。

下一步

当然,我的下一步是开始构建这些东西,看看会发生什么。然而,我希望能从业内的任何人那里获得见解、想法和参与,以考虑一些前进的路径!

请通过 twitterLinkedIn 联系我,或报名参加我课程的下一期:Designing Production ML Systems this May

使用 NLP 进行线程总结

原文:towardsdatascience.com/thread-summarization-using-nlp-d5bed9bf0eb4

使用 POS 标记、NER 和情感分析进行抽取式总结

Arun JagotaTowards Data Science Arun Jagota

·发表于 Towards Data Science ·13 分钟阅读·2023 年 1 月 7 日

--

图片由 Holger 提供,来自 Pixabay

设想一个包含多个参与者的短信(或电子邮件)线程。下面是一个涉及两个人共同规划旅行的示例。每条消息之间由空行分隔。

Here is some lodging near Pismo beach: Cliffs Hotel, SeaCrest, 
Madonna Inn, Shell beach inn

Shell beach & Madonna - a bit pricey.

Check out Inn at the Pier.

Looks good.

What about food?

Coya Peruvian

Any seafood restaurants?

Ada's Fish House. Oyster Loft.

+1 on Oyster loft.

What about bars?

11 22, Boardroom

breweries?

Shell beach brewhouse

Check this lodge out: Wayfarer

Not close enough to where we are going.

反复的对话可能会产生大量的信息,尽管这些信息分散在不同的短信中。当这个线程在智能手机的短信应用上展开时,随着更多消息的持续流入,滚动浏览以保持跟进对话可能会是一个挑战。

如果有一个 NLP 机器人能够阅读线程中的所有消息,并将它们总结成一条新的消息,那岂不是很好?然后这条新消息可以作为新消息发布在同一个线程中。

本文解决了这个问题。

首先让我们观察这个线程中的重复模式,其中一些可能会推广到其他线程。

A 先生建议了一些特定的住宿地点。B 先生给出了一些意见,并建议了另一个地方。其中一人询问关于食物的问题。另一人回应了具体的餐馆。对话接着转到酒吧。提出了一些具体建议。随后是意见。接着是后续问题。然后是回应和意见。

NLP 方法

是否有现有的 NLP 方法适用于这个用例?从总体上看,有的。这被称为抽取式总结

让我们先从基本层面理解它。

The summary is represented as a suitable subset of the content in the text.

这种方法并不一定试图使总结具有可读性,而是旨在捕捉文本中的关键信息。

在深入了解抽取式总结之前,还有一种 NLP 总结方法叫做抽象式总结,值得考虑。

这种方法产生的总结是对原始文本的总结性改述。

提取式摘要似乎更适合我们的用例,因为我们的主要目标是创建一个线程文本的简化版本,并保留所有关键内容。不是改写。

此外,请注意,抽象摘要是一个更具挑战性的问题,因为它似乎涉及更深入的自然语言理解和生成。

在我们的设置中进行提取式摘要

让我们考虑以下基本方法,如[1]所述。

  1. 将文本标记化为句子。

  2. 将每个句子标记化为词语。

  3. 删除停用词。像isaanthetoforof这样的词,

  4. 以某种方式根据词频对每个句子的剩余文本进行评分。

在我们的线程设置中,我们将用“消息”替换“句子”。也就是说,将文本标记化为句子意味着将其标记化为消息。

这种方法在我们的线程中可能效果如何?

词语标记化确实有帮助。它有助于识别和删除停用词。

也就是说,稍后我们将看到一种不同的提取方法,完全避免删除停用词。

我们的线程已标记化

首先,我们将把线程标记化为消息序列,然后将每条消息标记化为词语序列。在 python 术语中,这只是一个列表的列表。

最重要的是,在这种表示中,我们没有保留发送特定消息的人的身份。

去除停用词的效果是否足够?

以下是去除停用词后的文本版本。在这样做时,我们必须决定什么是停用词,什么不是。我们遵循了常见的做法,这通常偏向保守。

要删除的停用词在/…/中标记。

Here /is/ some lodging near Pismo beach: Cliffs Hotel, SeaCrest, 
Madonna Inn, Shell beach inn. Shell beach & Madonna - /a/ bit pricey.
Check out Inn /at the/ Pier. Looks good. What about food? Coya Peruvian. 
Any seafood restaurants? Ada's Fish House. Oyster Loft. +1 on Oyster loft.
What about bars? 11 22, Boardroom breweries? Shell beach brewhouse 
Check this lodge out: Wayfarer Not close enough /to/ where we /are/ going.

斜体和粗体中的词是我们删除的停用词。显然,这帮助不大。此外,请注意,旅馆名称Inn at the Pier中的at the也被删除了。

朝着更好的解决方案

正如我们刚刚看到的,单独去除停用词是无效的。首先,它没有删除足够的词。其次,它有时会删除出现在实体名称中的停用词。

让我们更一般地考虑一下。如果有某种方法来确定哪些词应该删除,哪些词应该保留,那就太好了。有人可能会说,这不就是在重新描述问题吗?

是的。不过,以这种方式来看,打开了新的思维方式。

在我们的情况下,接下来我们介绍的自然语言处理方法——词性标注,将比单独去除停用词带来显著改善。事实上,它将消除对去除停用词的需求。

词性分析

一种广泛使用的自然语言处理方法是为文本中的每个词标记其词性类别。例如nounsverbsadjectives等。

那么我们为什么认为这会对我们的线程摘要需求有所帮助呢?这是因为我们认为一个词的词性将作为是否应该在摘要中保留该词的良好预测因素。我们期望删除动词、冠词和代词。我们期望保留名词。

让我们在示例中测试这个想法。

我们在parts-of-speech.info/上应用了词性标注服务于我们的线程文本。(所有消息被串联,消息边界以段落分隔符表示。)

下面是我们得到的结果。

颜色到词性标签的映射如下。

各种词的词性标签确实对总结很有用。我们希望保留所有名词,也许还包括形容词,因为四个中有两个携带明确的情感(价格,好)。

也就是说,虽然相对于仅仅去除停用词已有所改进,但仅仅基于词性标签的简单过滤可能还不够。原因如下。

首先,即使我们仅保留名词,仍然有太多的词语。

其次,就像我们在去除停用词时发生的情况一样,我们最终删除了旅馆名称Inn at the Pier中的at the。这不好。我们遮蔽了一个可能值得进一步研究的旅馆名称。

第三,形容词词性标签虽然提供了一些情感信号,但与情感的相关性并不足够强。

另一种方法

我们首先观察到,线程中的几乎所有显著信息都属于三种 NLP 类别之一:命名实体情感显著短语

如我们将很快看到的,能够区分这些将有助于改善我们的线程摘要器。

我们将从命名实体开始,因为这是该线程中的主要类别。

命名实体

在 NLP 中,任何短小的文本,通常是一到三个词,(i)是一个“事物”并且(ii)可以命名的,都称为命名实体。例子包括人名、国家名、餐厅名、酒店名等等。有关 NLP 中命名实体识别的详细帖子,请参见[2]。

拥有能够识别城市名称、餐厅名称和酒店名称的命名实体识别器,我们将能够更好地进行摘要。只要我们能够组装出足够丰富的城市名称、餐厅名称和酒店名称集合,就可以通过训练现成的命名实体识别器实现这一点。

下面是识别了上述命名实体并删除了其余文本后的摘要文本样式。我们还删除了段落分隔符,并在需要的地方添加了句号。(段落分隔符对摘要文本没有价值。)

Pismo beach: Cliffs Hotel, SeaCrest, Madonna Inn, Shell beach inn. Shell 
beach & Madonna - . Inn at the Pier. Coya Peruvian. Ada's Fish House. 
Oyster Loft. Oyster loft. Shell beach brewhouse. Wayfarer.

由于我们的文本摘要器进行命名实体识别,如果需要的话,我们可以利用这一点,通过标注命名实体来呈现摘要。像这样:

CITY: Pismo beach: 
HOTELS: Cliffs Hotel, SeaCrest, Madonna Inn, Shell beach inn. 
HOTELS: Shell beach & Madonna — . Inn at the Pier. 
RESTAURANTS: Coya Peruvian. Ada’s Fish House. Oyster Loft. Oyster loft. 
Shell beach brewhouse. 
HOTEL: Wayfarer.

还不错。不过,我不确定添加实体名称是否增加了价值,或者在这个线程中是否造成了杂乱。这确实告诉我们这是一种可能性。在较长的线程中,这可能效果更好。

不过,仍然有改进的空间。我们将改进分为以下几种类型,顺序不分先后。

  1. 牡蛎阁被提到两次。幸运的是,去重很容易(在这个具体的情况下),因为它已经被识别为一个命名实体。

  2. 情感没有被提取出来。例如看起来不错有点贵、+1 和离我们要去的地方不够近

  3. 显著短语,即标签,也没有被提取出来。具体而言,海鲜餐厅、酒吧、啤酒厂、旅馆(住宿)

关于 1)没有更多要说的。

情感

关于 2),这属于 NLP 情感分析的范畴。更具体地说,是基于方面的情感分析,即检测哪些情感适用于文本的哪些部分。与情感分类不同,情感分类是确定文本中的整体情感是积极、消极还是中性的任务。

详细的情感分析帖子请参见[3]。它详细介绍了基于方面的情感分析和情感分类,并提供了说明性例子。

话虽如此,在我们具体的讨论中,让我们尝试更好地理解我们真正需要什么,能够做出哪些假设,并考虑我们可能将它们推进多远。简而言之,让我们(i) 检查我们的具体用例,并(ii) 逐步推进。

首先,让我们将焦点缩小到我们想要提取的情感的消息和紧接在其后的消息。我们选择后者,因为消息中的情感可能在前一条消息中。我们已经在/…/中标注了携带情感的区域。

Here is some lodging near Pismo beach: Cliffs Hotel, SeaCrest, 
Madonna Inn, Shell beach inn. Shell beach & Madonna - /a bit pricey/.
Check out Inn at the Pier. /Looks good/.
…
Ada's Fish House. Oyster Loft.
/+1/ on Oyster loft.
…
Check this lodge out: Wayfarer. /Not close enough to where we are going/.

首先,让我们假设我们检测到了携带情感的术语。即使是基于词典的方法在这里也是一个很好的起点,尽管它的扩展性仍然是一个未解的问题。

我们的第一个想法是保持检测到的术语在总结中的位置不变。让我们看看最终结果是什么样的。与之前一样,携带情感的术语用/…/标记。

Pismo beach: Cliffs Hotel, SeaCrest, Madonna Inn, Shell beach inn. Shell 
beach & Madonna — /a bit pricey/. Inn at the Pier. /Looks good/. Coya 
Peruvian. Ada’s Fish House. Oyster Loft. /+1/ on Oyster loft. Shell beach 
brewhouse. Wayfarer. /Not close enough/

好的。

第一点,一个小点是,由于我们知道哪些文本块是携带情感的术语,我们可以将它们与实体视觉区分开来。这在/…/中有所说明。

第二点是我们可以更明确地将携带情感的术语与紧接其前的实体关联起来。当然,这样做有一定的风险。让我们看看结果会是什么样的。

Pismo beach: Cliffs Hotel, SeaCrest, Madonna Inn, Shell beach inn. 
Shell beach & Madonna — a bit pricey. Inn at the Pier (Looks good). 
Coya Peruvian. Ada’s Fish House. Oyster Loft (+1) on Oyster loft. 
Shell beach brewhouse. Wayfarer (Not close enough)

在这种情况下很好。

显著短语

现在让我们关注我们希望被检测并添加到总结中的显著短语。在我们的讨论中,这些是海鲜餐厅、酒吧、啤酒厂、旅馆(住宿)。再次,我们假设这些短语已被检测到,并查看包含它们的结果。这次我们用/…/标记检测到的显著短语。

lodging Pismo beach: Cliffs Hotel, SeaCrest, Madonna Inn, Shell beach inn. 
Shell beach & Madonna — a bit pricey. Inn at the Pier (Looks good). 
Coya Peruvian. /seafood restaurants/. Ada’s Fish House. Oyster Loft (+1) 
on Oyster loft. /bars/. /breweries/. Shell beach brewhouse. Wayfarer 
(Not close enough)

嗯。注入的显著短语没有像注入的情感那样增加太多价值。

差别在哪里?让我们分析一下。首先,添加情感丰富了总结,因为情感不能从周围的文本中预测。

在我们的线程中,检测到的突出的短语并没有提供太多信息。例如,读者可以看到接近住宿的实体是酒店,因此添加该词并不会丰富总结。同样的观点也适用于海鲜餐馆酒吧酿酒厂

用数据科学的语言,我们可以说,将情感添加到总结中获得的信息比添加突出的短语获得的信息要高。

第二点是,在文本消息的线程设置中,情感通常会集中在刚刚提到的实体上,无论是在同一消息中还是在之前的消息中。相比之下,突出的短语更为开放。在我们的线程中,突出的短语出现在问题中,这些问题的答案稍后会出现,可能会分散在不同的位置。

第三点是从读者的角度来看这些总结。这些读者通常是线程中的一些参与者。他们对线程中特定实体的其他人的情感将对他们很重要。

话虽如此,问题并非非黑即白,因为可以认为添加突出的短语确实增加了一些信息。考虑海鲜餐馆。我们现在知道对话中的某人对海鲜餐馆感兴趣。如果其他人对此建议有情感反馈,那么这一短语所传递的信息只会增加。

话虽如此,我们仍然可以说,相对而言,命名实体和情感比突出的短语更值得保留在总结中。我们可以这样做:最初保留突出的短语,但如果总结变得过长,则将其删除。

回顾

现在让我们回顾并反思 POS 标记与命名实体识别、情感分析和突出的短语提取的组合。

单独去除停用词非常粗略。根据词性决定去除哪些词和保留哪些词则更细致。用命名实体识别、情感分析和突出的短语提取的组合替代 POS 分析则更为精细。

因此,随着我们从最简单的方法进展到最复杂的方法,总结的质量会提高。当然,工作量也会增加。对于第三种方法,我们需要使用或训练合适的命名实体识别器。此外,还需要使用情感分析器,并可能进一步改进它。识别突出的短语是另一个问题。我们在本文中没有深入探讨这一点,因为在某个阶段,保留这些短语在总结中的理由减弱了。

出版后的更新

在我们示例中的线程中,一个之前遗漏的反复出现的主题是讨论各个相关人员的工作时间。

在我们的线程示例中,让我们将下面的内容作为前几个消息添加进去。

What days work for you all? For me, April 10–11, 17–18, or May 21–22 work.

None of these work for me. How about April 2–3?

...

Yup, Apr 2–3 works.

上述的“…”是一个指示,表示在两个包夹消息之间还有其他消息。

现在让我们致力于将上述消息中的关键信息整合到我们的总结中。

首先,我们希望扩展我们的命名实体识别器以识别日期。下面是相同的消息,我们希望识别为日期的部分已用粗体标出。

大家都能工作哪些天?对我来说,4 月 10–11 日17–18 日5 月 21–22 日都可以。这些日期对我来说都不行。4 月 2–3 日怎么样?是的,4 月 2–3 日可以。

识别日期实体在上述消息中并不难。字典基础、序列基础和模式基础机制的混合将是一个有效的开始,并可以随着时间的推移进行优化。

例如,模式<month> <num>-<num>将捕捉到上述一些日期。<month>来自字典,包括模糊条目如Apr,<num>可以通过正则表达式匹配一位或两位数字。

这将无法检测到17–184 月 10–11 日、17–18 日或 5 月 21–22 日中是一个日期。

将日期作为命名实体检测也提出了以下想法。我们可以将那些检测到日期实体的消息归入总结的一个单独部分,我们可以恰当地称之为日期

下面是一个增强了这些信息的总结版本。

Pismo beach: Cliffs Hotel, SeaCrest, Madonna Inn, Shell beach inn. Shell 
beach & Madonna - a bit pricey. Inn at the Pier (Looks good). Coya 
Peruvian. Ada's Fish House. Oyster Loft (+1) on Oyster loft. Shell beach 
brewhouse. Wayfarer (Not close enough)

Dates: For me, April 10–11, 17–18, or May 21–22 work. None of these work 
for me. How about April 2–3? Yup, Apr 2–3 works.

如果我们能够推断出4 月 2 日3 日适用于每个人,那就更好了。我们在这里不会追求这一点。

总结

在这篇文章中,我们讨论了构建一个总结文本消息的问题,该消息概述了特定对话线程中各个消息的内容。

我们取了一个现实的例子,并讨论了一些可能在这里有用的 NLP 方法。

我们观察到,去除停用词并基于词频评分是不够的。

从这里,我们进展到了使用文本中各种词汇的词性信息。这效果更好,但仍不够理想。

从这里我们进展到了使用命名实体识别和情感分析。在我们的线程中,这效果相当好,因为该线程充满了命名实体。我们能够识别这些命名实体并将其纳入总结中,并附上相关的情感。

最后,虽然我们看到命名实体及其附带的情感对于检测和添加到总结中是至关重要的,至少在我们的线程中,将此扩展到检测额外的显著短语并将其添加到总结中的价值较小。我们也解释了原因。

参考文献

  1. 使用 NLTK 在 Python 中进行提取式文本摘要

  2. NLP 中的命名实体识别。实际用例、模型、方法……

  3. NLP 中的文本情感分析。问题、用例和方法

用两行代码将你的 Python 程序多线程

原文:towardsdatascience.com/thread-your-python-program-with-two-lines-of-code-3b474407dbb8

通过同时做多件事来加速你的程序

Mike Huls Mike Huls Towards Data Science Mike Huls

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

--

更好地组织我们的线程(图片来自Karen PenrozUnsplash

当你的程序有很多涉及等待的任务时,你可以通过同时执行这些任务来加速程序,而不是一个一个地处理。比如在做早餐时,你不会等咖啡机完成后再煮鸡蛋。相反,你会启动咖啡机,同时给自己倒一杯橙汁,并加热平底锅以煎炒鸡蛋。

这篇文章向你展示如何做到这一点。最后你将能够安全地在两行代码中应用线程,并在程序中实现巨大的加速。让我们开始编程吧!

但首先……

本文将详细介绍如何通过对整列表参数应用相同的函数来应用线程。然后我们将查看如何以线程方式应用不同的函数。

[## Cython 入门:两步实现 30 倍速度提升

轻松的 Python 代码编译,打造飞快的应用程序

前往数据科学的文章链接

线程能解决我的问题吗?理解并发

确实在许多情况下,通过“同时做多件事”可以加速你的程序,但盲目地在各处应用线程并不是一个明智的解决方案。在 Python 中有两种多任务处理的方法:多进程和线程:

  • 线程并发方式运行代码:我们有一个活跃的 CPU,它在多个线程之间快速切换

  • 多进程并行的方式运行代码:我们有多个活动的 CPU 每个都运行自己的代码(请查看下面的文章)

## Applying Python multiprocessing in 2 lines of code

何时以及如何使用多个核心以更快地执行多次任务

## Applying Python multiprocessing in 2 lines of code

在线程处理时,你有一个执行所有任务的角色,通过在任务之间切换来同时完成它们。在早餐示例的上下文中:有一个角色(你)在咖啡机、锅和橙汁杯之间切换。

在多进程中,你激活多个任务,每个任务都被分配一个任务。在早餐的类比中,这就像把自己克隆两次,并给每个克隆分配一个单独的任务。虽然它比一个一个地运行任务要快得多,但多进程有更多的开销;克隆自己是一项很大的工作,只是为了让克隆在等待锅加热!

简而言之:多进程在需要计算大量数据时是最佳解决方案,线程则更适合需要等待的情况。

在这篇文章中,我们将专注于线程处理;如果你对多进程感兴趣,请查看下面的文章:

## Multi-tasking in Python: Speed up your program 10x by executing things simultaneously

逐步指南,以应用线程和进程来加速你的代码

## Multi-tasking in Python: Speed up your program 10x by executing things simultaneously

设置

对于这篇文章,我们假设一个旅行程序接收到一个需要验证的大型电子邮件地址列表。假设我们设置了一个 API,可以发送一个电子邮件地址,并根据电子邮件地址的有效性返回真/假。

最重要的是,我们需要发送请求并等待 API 响应。这是一个典型的可以多线程处理的任务:我们不需要额外的核心来加速计算;我们只需要一些额外的线程来同时发送多个电子邮件地址。

对于这篇文章,我们将使用这个电子邮件地址列表:

email_addresses = [
  'mikehuls42@gmail.com',
  'mike@mikehuls.com',
  'johndoe@some_email.com',
  'obviously_wrong@address',
  'otheraddress.com',
  'thisis@@wrong.too',
  'thisone_is@valid.com'
]

这将是我们的函数,它模拟将电子邮件地址发送到验证 API:

def send_email_address_to_validation_api(email_address:str):
  # We'll simulate the request to the validation API by just sleeping between 1 and 2 seconds
  sleep_time = random.random() + 1
  time.sleep(sleep_time)
  # Randomly return a true / false depending on the sleep_time
  return sleep_time > 1.5

## Docker for absolute beginners: the difference between an image and a container

了解 Docker 镜像和容器的区别 + 实用代码示例

towardsdatascience.com](/docker-for-absolute-beginners-the-difference-between-an-image-and-a-container-7e07d4c0c01d?source=post_page-----3b474407dbb8--------------------------------)

A. 非线程

首先,我们来看一下在不使用线程的情况下如何使用这个函数。

遍历电子邮件地址

我们将遍历我们的 7 个电子邮件地址列表,将每个值发送到 API;简单明了:

for email_address in email_addresses:
  is_valid = send_email_address_to_validation_api(email_address=email_address)
  # do other stuff with the email address and validity

这很容易理解,但它快吗?(剧透:不快)。由于我们依次验证每个 7 个电子邮件地址,每个地址需要 1 到 2 秒,因此总共需要 7 到 14 秒。我测得的时间是11.772 秒

## 用 OpenCV 摧毁《鸭子猎人》 — 初学者的图像分析

编写代码以打破每一个《鸭子猎人》的高分

towardsdatascience.com

使用映射函数

为了更好地理解下一部分,我们将使用 Python 的map函数重写上面的代码:

results: [bool] = map(send_email_address_to_validation_api, email_addresses)

上面的代码完全一样;它将函数映射到地址列表,这意味着它对email_addresses列表中的每个值执行该函数。

让我们将时间添加到基准测试中:

NON THREADED           11.772 secs

## 为什么 Python 如此慢以及如何加快速度

检查一下 Python 的瓶颈在哪里

towardsdatascience.com

B. 使用线程

在这一部分,我们检查 3 种不同的将线程应用到我们函数的方法。所有这些方法都使用线程池,可以通过以下方式导入:

from multiprocessing.pool import ThreadPool

将线程池视为等待任务的线程集合。线程池具有一个map函数,我们可以像在上面的非线程示例中一样使用它。一旦线程完成任务,它会返回到线程池中,等待另一个任务。

线程池允许我们通过提供对线程池中线程数量的限制,轻松且安全地使用线程

## Python 到 SQL — 安全、简单且快速的 UPSERT

使用 Python 进行闪电般快速的插入和/或更新

towardsdatascience.com

1. 线程池映射

我们将首先切换到线程池提供的map函数。

with ThreadPool(processes=10) as t_pool:
  results = t_pool.map(send_email_address_to_validation_api, email_addresses)

我们定义了一个最大进程数为 10 的线程池。由于这个原因,map 函数会同时启动所有对函数的调用。一旦所有工作线程完成,我们就可以评估结果,在这种情况下是1.901 秒

NON THREADED           11.772 secs
THREADED MAP            1.901 secs

## 创建并发布自己的 Python 包

关于如何使用 pip 安装你自制包的简短而简单的指南

towardsdatascience.com

2. 线程池 imap

在之前的示例中,我们不得不等待所有函数调用完成。如果我们使用 imap 而不是 map,情况就不同了。imap 函数返回一个迭代器,我们可以在结果可用时立即访问:

strt_time_t_imap = time.perf_counter()
with ThreadPool(processes=10) as t_pool:
  for res in t_pool.imap(send_email_address_to_validation_api, email_addresses):
    print(time.perf_counter() - strt_time_t_imap, 'seconds')

上述代码几乎完全相同。唯一的区别是添加了一些计时代码。此外,我们显然在第 3 行的 t_pool 上使用了 imap 函数。

如果我们查看打印结果,我们会看到:

1.4051628 seconds
1.4051628 seconds
1.7985222 seconds
1.7985749 seconds
1.7985749 seconds
1.7985957 seconds
1.7986305 seconds

imap 函数返回一个迭代器,我们可以在结果完成后立即访问。不过,这些结果是按顺序返回的。这意味着例如第二个电子邮件地址必须等待第一个;如果第二个电子邮件地址在 1.3 秒内完成,第一个电子邮件地址在 1.4 秒内完成;两者都在 1.4 秒后返回(正如你在上面的打印输出中看到的)。

尽管验证完整的电子邮件地址列表所需的时间与之前的示例大致相同,但我们可以更快地获取结果!第一个结果在1.4 秒后即可访问!

NON THREADED           11.772 secs
THREADED MAP            1.901 secs
THREADED IMAP           1.901 secs(first result accessible after 1.4  secs)

[## 针对绝对初学者的虚拟环境——什么是它以及如何创建一个(+ 示例)

深入了解 Python 虚拟环境、pip 和避免复杂的依赖关系

mikehuls.medium.com](https://mikehuls.medium.com/virtual-environments-for-absolute-beginners-what-is-it-and-how-to-create-one-examples-a48da8982d4b?source=post_page-----3b474407dbb8--------------------------------)

3. 线程池 imap_unordered

另一个改进:我们将返回无序的迭代器,而不是按顺序返回:

strt_time_t_imap = time.perf_counter()
with ThreadPool(processes=10) as t_pool:
  for res in t_pool.imap_unordered(send_email_address_to_validation_api, email_addresses):
    print(time.perf_counter() - strt_time_t_imap, res)

使用上述代码,我们可以在结果可用时立即访问。你也可以在打印输出中看到这一点:

1.0979514 seconds
1.2382307 seconds
1.3781070 seconds
1.4730333 seconds
1.7439070 seconds
1.7909826 seconds
1.9953354 seconds

最后的电子邮件地址可能在1.09 秒内完成并最先返回。这非常方便。

NON THREADED           11.772 secs
THREADED MAP            1.901 secs
THREADED IMAP           1.901 secs(first result accessible after 1.4  secs)
THREADED IMAP_UNORDERED 1.901 secs(first result accessible after 1.09 secs)

## 创建一个快速自动文档生成、可维护且易于使用的 Python API,只需 5 行代码

非经验丰富的开发者只需一个完整、有效、快速且安全的 API,这非常适合他们。

[towardsdatascience.com

4. 不同的函数

在前面的例子中,我们探讨了如何以线程方式应用相同的函数,但如果我们有多个函数呢?在下面的示例中,我们模拟加载一个网页。我们有不同的函数用于加载横幅、广告、帖子以及当然,还有吸引点击的内容:

def load_ad():
    time.sleep(1)
    return "ad loaded"
def load_clickbait():
    time.sleep(1.5)
    return "clickbait loaded"def load_banner():
    time.sleep(2)
    return "banner loaded"def load_posts():
    time.sleep(3)
    return "posts loaded"

如果我们顺序运行这些函数,我们的程序将需要大约 7.5 秒。我们可以通过稍微调整来使用线程池及其 mapimapimap_unordered 函数。请参见下面的 imap_unordered 示例:

with ThreadPool(processes=4) as t_pool:  # limit to 4 processes as we only need to execute 
  results = t_pool.imap_unordered(lambda x: x(), [load_ad, load_posts, load_banner, load_clickbait])

如你所见,我们将函数列表映射到一个 lambda 函数。函数列表由 lambda 函数执行(x 是每个函数的占位符,x() 将执行它)。以这种方式执行,渲染网页仅需 3.013 秒

## Git 入门:通过视频游戏了解 Git

对如何使用 git 的经典 RPG 类比获得直观的理解

[towardsdatascience.com

结论

使用线程池进行多线程处理是安全且易于应用的。总结:multiprocessing 库的 Pool 对象提供了三个函数。map 是 Python 内置 map 的并发版本。imap 函数返回一个有序迭代器,访问结果是阻塞的。imap_unordered 函数返回一个无序迭代器,使得可以在每个结果完成后立即访问,而无需等待另一个函数先完成。

我希望这篇文章的解释清晰明了,但如果不是,请告诉我我可以做什么进一步澄清。同时,查看我 其他文章,涉及各种编程相关主题:

编程愉快!

— Mike

附言:喜欢我做的事吗? 关注我!

[## 使用我的推荐链接加入 Medium — Mike Huls

阅读 Mike Huls 的每一个故事(以及 Medium 上其他成千上万位作家的故事)。你的会员费用直接支持 Mike…

mikehuls.medium.com

在生产中部署生成模型的三大挑战

原文:towardsdatascience.com/three-challenges-in-deploying-generative-models-in-production-8e4c0fcf63c3?source=collection_archive---------5-----------------------#2023-08-07

如何将大型语言模型和扩散模型部署到你的产品中,而不让用户感到恐惧。

Aliaksei Mikhailiuk数据科学前沿 Aliaksei Mikhailiuk

·

关注 发表于 数据科学前沿 · 9 分钟阅读 · 2023 年 8 月 7 日

--

图片由作者在 SDXL 1.0 中生成。

OpenAIGoogleMicrosoftMidjourneyStabilityAICharacterAI等公司——每个人都在竞相提供最佳的文本到文本、文本到图像、图像到图像和图像到文本模型解决方案。

原因很简单——这一领域提供了广阔的机会;毕竟,它不仅仅是娱乐,还有以前无法解锁的实用功能。从更好的搜索引擎到更令人印象深刻的个性化广告活动和友好的聊天机器人,如Snap 的 MyAI

尽管这一领域非常动态,许多模型检查点每天都在发布,但每个从事生成式 AI 的公司都在寻找解决方案来应对挑战。

在这里,我将讨论在生产中部署生成模型的主要挑战以及如何解决这些挑战。尽管有许多不同类型的生成模型,在这篇文章中,我将重点讨论最近在扩散模型和基于 GPT 的模型中的进展。然而,许多讨论的话题也适用于其他模型。

什么是生成式 AI?

生成式 AI 广泛描述了一组可以生成新内容的模型。广泛知名的生成对抗网络通过学习真实数据的分布,并从添加的噪声中生成变异来实现这一点。

近期生成式 AI 的繁荣源于模型在大规模上达到了人类水平的质量。解锁这一转变的原因很简单——我们现在只有足够的计算能力(因此是NVIDIA 股价飙升)来训练和维护足够容量的模型,以实现高质量的结果。当前的进步由两种基础架构驱动——变换器和扩散模型。

也许最近一年最重要的突破是 OpenAI 的 ChatGPT——一个基于文本的生成模型,最新的 ChatGPT-3.5 版本有 1750 亿个参数,具有足够的知识库来维持各种话题的对话。虽然 ChatGPT 是一个单模态模型,因为它只能支持文本,但多模态模型可以处理多种输入和输出,例如文本和图像。

图像到文本和文本到图像的多模态架构在一个由文本和图像概念共享的潜在空间中操作。通过在需要两个概念的任务(例如,图像标注)上训练来获得潜在空间,通过惩罚同一概念在两个不同模态中的潜在空间距离。一旦获得了这个潜在空间,它可以用于其他任务。

图像到文本模型的示例。图片由作者提供。

今年发布的著名生成模型有DALL·E/Stable-Diffusion(文本到图像/图像到图像)和BLIP(图像到文本实现)。DALL·E 模型以提示或图像作为输入,提示生成图像作为响应,而基于 BLIP 的模型可以回答关于图像内容的问题。

挑战与解决方案

不幸的是,机器学习没有免费的午餐,大规模生成模型在生产部署时遇到了一些挑战——大小和延迟、偏差和公平性以及生成结果的质量。

模型大小和延迟

模型大小趋势。数据来自P. Villalobos。图片由作者提供

先进的 GenAI 模型非常庞大。例如,文本到文本的Meta 的 LLaMA模型范围在 7 亿到 65 亿参数之间,而 ChatGPT-3.5 有 1750 亿参数。这些数字是合理的——在简化的世界里,经验法则是模型越大,训练所用的数据越多,质量也越好。

尽管文本到图像模型较小,但仍显著大于其生成对抗网络的前身——Stable Diffusion 1.5 检查点略低于 10 亿参数(占用超过三 GB 的空间),而 DALL·E 2.0 有 35 亿参数。很少有 GPU 能够拥有足够的内存来维持这些模型,通常你需要一整套设备来维护一个大型模型,这可能很快变得非常昂贵,更不用说将这些模型部署到移动设备上。

生成模型需要时间来产生输出。对于某些模型,延迟是由于其大小——在多个 GPU 上传播信号通过数十亿个参数即使需要时间,而对于其他模型,则是由于生成高质量结果的迭代性质。扩散模型在默认配置下需要 50 步来生成图像,减少步数会降低输出图像的质量。

解决方案: 将模型缩小通常有助于提高其速度 — 提炼、压缩和量化 模型也会减少延迟。高通为此铺平了道路,通过将稳定扩散模型压缩到可以在移动设备上部署的程度。最近,稳定扩散(tiny 和 small)的小型、提炼和更快速版本已被发布

特定模型的优化也有助于加快推理速度 — 对于扩散模型;可以生成低分辨率的输出,然后进行放大,或使用更少的步骤和不同的调度器,因为有些模型在较少的步骤下效果最好,而其他模型在更多迭代下会生成更高质量的结果。例如,Snap 最近展示了八个步骤就足以 使用稳定扩散 1.5 创建高质量的结果,并在训练时采用了各种优化。

例如,使用 NVIDIAs tensorrttorch.compile 编译模型,可以在最小的工程努力下大幅减少延迟。

## 边缘 — 在移动设备上部署深度学习应用

在受限设备上打破效率和准确性权衡的技术

towardsdatascience.com

偏见、公平性和安全性

你是否尝试过破解 ChatGPT?许多人成功揭露了偏见和公平性问题,值得称赞的是 OpenAI 做得很好 解决这些问题。如果没有大规模的修复,聊天机器人可能会通过传播有害和不安全的思想和行为而造成现实世界的问题。

破坏模型的示例见政治;例如,ChatGPT拒绝创作关于特朗普的诗歌,但会创作关于拜登的诗歌,特别是性别平等和职业——暗示某些职业适合男性,某些适合女性以及种族

与文本到文本模型类似,文本到图像和图像到文本模型也存在偏见和公平性问题。Stable Diffusion 2.1模型在生成医生和护士的图像时,通常生成的是白人男性作为医生和白人女性作为护士。有趣的是,偏见会根据提示中指定的国家而有所不同——例如,日本医生或巴西护士。

询问 Stable Diffusion 模型生成一位医生和一位护士的图像。图片由作者使用SD 2.1 接口生成。

玩弄BLIP 图像到文本模型,将一张超重人士和男女医生的图片输入时,我得到了一些带有评判和偏见的图像描述——“一个胖男人”,“一个男性医生”,“一位穿着实验室大褂、带着听诊器的女性”。

图片由作者使用 Stable Diffusion 2.1 生成,描述由 BLIP 模型生成,依次为[1]一个胖男人吃冰淇淋;[2]一位穿着白大褂和领带的男性医生;[3]一位穿着白色实验室大褂、脖子上挂着听诊器的女性。图片由作者使用SD 2.1 接口生成,并通过BLIP处理。

如何测试这个问题: 这是一个相当难以诊断的问题——在许多情况下,你需要知道要查找什么。拥有一个包含各种提示的独立基准数据集,其中可能会出现问题的情况、模板响应和我们将检测的每个答案的警告标志,以及来自不同背景和多个场景的人类数据集,存储关于图像中人物的所有可能属性,将会有所帮助。这些数据集需要包含数十万条记录,以获得可靠的统计数据。

解决方案:几乎所有的偏见、公平性和安全性问题都源于训练数据。我喜欢这样的类比:AI 模型是人性的镜像,加剧了我们所有的偏见。在干净、公正的数据上训练会大大改善结果。然而,即使有了这些,模型仍然会出错。

结果后处理和过滤是另一个可能的解决方案;例如,Stable Diffusion 在包含裸露图像的数据上进行训练,配有 NSFW 内容检测器以捕捉潜在问题。类似的过滤器可以应用于文本到文本模型的输出。

输出质量、相关性和正确性

生成模型在解释用户请求时可以非常有创意,虽然最近的大规模模型达到了人类水平的质量,但对于每种使用情况,它们不会开箱即用,需要额外的调整和提示工程。

在早期阶段,图像到文本和文本到文本模型的质量评估相对简单——毕竟,相较于无意义的内容,改进是显而易见的。高质量生成模型开始展现出更难以察觉的行为;例如,文本到文本模型可能变得含糊其辞,自信地输出错误和过时的信息。

扩散模型在其他方面表现出输出缺陷。典型的图像基础模型问题包括几何破损、变异的解剖结构、提示与图像结果不匹配、图像传输中的肤色和性别不匹配。自动化美学和现实性评估滞后,典型的度量指标,如 FID,无法捕捉这些变化。

从左到右:[1] 两个人拥抱;[2] 一个竖起大拇指的男人;[3] 一只狗在公园里跑步。一张由作者生成的来自Stable Diffusion 1.5的图像

如何测试问题: 测试生成模型的质量具有挑战性;毕竟,这些模型没有真实的标准——它们被设计用于提供新颖的输出。因此,到目前为止,没有一种度量标准能可靠地捕捉质量方面。最可靠的度量标准是人工评估。

与偏见和公平性评估一样,最好的方法是拥有一个大规模的提示和图像数据集来测试质量。随着文本到文本模型变得更加个性化并调整到每个用户,除了对话的连贯性、正确性和相关性,我们还希望评估它们记住关于对话的信息的能力。

解决方案: 许多质量问题可以归因于训练数据和模型的大小;它们可能还稍微有点小,不足以实现另一个质量飞跃(考虑 GPT-3.5 与 GPT-4)——当前的潜在空间是一个抽象概念,并不是为了存储精确信息而设计的。许多问题可以通过更好的提示工程解决——无论是文本到文本和文本到图像的提示增强,还是文本到图像模型的负面提示。

文本到图像和图像到图像的模型可以使用附加工具来提升质量——图像增强,无论是通过传统的深度学习方法还是基于扩散的细化器。像ControlNet这样的附加模块,与扩散架构正交,可以帮助对生成结果进行额外控制。针对特定应用进行微调的 Dreambooth 技术也有助于提升结果。调整额外参数,如调度器、CFG 和扩散步骤数,可能会大幅度影响质量。

总结

生成模型开启了新的应用范围,包括有趣的AI 镜头和商业用途,如更好的搜索引擎、共同助手和广告。同时,企业急于推出产品和消费者对新功能的兴奋,有时会使技术中的明显缺陷被忽视。

尽管有一种普遍的推动力,旨在通过发布大规模开源数据集、训练代码和评估结果来提高模型基准测试的透明度,但对大规模 AI 模型的严格监管也在不断推进。在理想的世界中,两者不会走向极端,而是相互促进,使 AI 更加安全和有趣。

喜欢这位作者?保持联系!

我是否遗漏了什么?请随时在LinkedInTwitter上留言、评论或直接给我发消息!

## 深度图像质量评估

深入探讨全参考图像质量评估。从主观图像质量实验到深度目标评估…

towardsdatascience.com ## 感知损失用于深度图像修复

从均方误差到 GAN——什么才是好的感知损失函数?

[towardsdatascience.com

本博客中的观点仅代表我个人,与 Snap 无关,也不代表 Snap 的立场。

三种你可能不知道的表示百分比的图表

原文:towardsdatascience.com/three-charts-to-represent-a-percentage-you-may-not-know-84cc7d5c62a3

数据可视化,Python

一个即用的 Python Altair 教程,用于构建表示百分比的图表

安杰利卡·洛·杜卡Towards Data Science 安杰利卡·洛·杜卡

·发表于 Towards Data Science ·5 分钟阅读·2023 年 8 月 23 日

--

Markus Winkler 拍摄的照片,来自 Unsplash

以视觉方式表示百分比可能会更吸引观众,并帮助他们更好地理解数据。有多种方式可以以视觉方式表示百分比。最直接的策略是大数字(BAN),如下图所示。

图片由作者提供

除了 BAN,还有其他方式可以以视觉方式表示百分比。在本文中,我们将重点介绍三种策略:

  • 饼图

  • 100% 堆叠图

  • 华夫图。

我们将使用 Python Altair 展示如何构建每种策略。Vega-Altair 库(简称 Altair)是一个基于 Vega 和 Vega-Lite 可视化语法的声明式 Python 统计可视化库。有关如何开始使用 Altair 的更多细节,你可以阅读官方的 Python Altair 文档

设置场景

为了展示每种图表的工作原理,我们将表示以下百分比:70%。为了表示这个值,你需要使用以下 Pandas DataFrame:

data = {
    'percentage': [0.7,0.3],
    'label'     : ['70%','30%'],
    'color'     : ['#81c01e','lightgray']
}

df = pd.DataFrame(data)

DataFrame 必须包含两个值:百分比(70%)及其互补值(30%)。此外,我们设置了与每个切片相关的颜色:#81c01e(绿色的一种)用于我们的值,浅灰色用于其他。

让我们开始通过饼图来表示这个 DataFrame。

饼图

饼图是一种圆形图表,以环形显示数据。它类似于饼图,但中心有一个空洞,从而创建了不同类别百分比的可视化表示。

写下以下代码以在 Altair 中构建一个基本的甜甜圈图:

import pandas as pd
import altair as alt

data = {
    'percentage': [0.7,0.3],
    'label'     : ['70%','30%'],
    'color'     : ['#81c01e','lightgray']
}

df = pd.DataFrame(data)

chart = alt.Chart(df).mark_arc(
    innerRadius=100,
    outerRadius=150
).encode( 
    theta='percentage',
    color=alt.Color('color', scale=None),
    tooltip='label'
).properties(
    width=300,
    height=300
)

chart.save('chart.html')

使用 innerRadiusouterRadius 设置甜甜圈图的内半径和外半径。

接下来,在甜甜圈图中添加文本,如下面的代码所示:

text = alt.Chart(df.head(1)).mark_text(
    align='center',
    baseline='middle',
    fontSize=60,
    fontWeight='bold',
    color='black'
).encode(
    text='label',
    color=alt.Color('color', scale=None)
).properties(
    width=300,
    height=300
) 

仅将 DataFrame 的第一行传递给图表,并使用 mark_text() 在 Altair 中绘制文本。同时,设置字体大小、粗细和颜色。

最后,结合这两个图表,如以下代码片段所示:

chart = (chart + text
).configure_view(
    strokeWidth=0
)

chart.save('donut-chart.html')

以下图示显示了结果图表:

作者提供的图片

100% 堆叠条形图

100% 堆叠条形图是一种将每个条形图堆叠到 100% 高度的堆叠条形图,用于显示每个类别的比例组成。

写下以下代码以在 Altair 中构建一个基本的 100% 堆叠条形图:

import pandas as pd
import altair as alt

data = {
    'percentage': [0.7,0.3],
    'label'     : ['70%','30%'],
    'color'     : ['#81c01e','lightgray']
}

df = pd.DataFrame(data)

chart = alt.Chart(df).mark_bar(
    size=40
).encode(
    x=alt.X('sum(percentage)', axis=None),
    color=alt.Color("color", scale=None)
).properties(
    width=300
)

接下来,在条形图中添加文本,如下面的代码所示:

text = alt.Chart(df.head(1)).mark_text(
    align='center',
    baseline='middle',
    fontSize=20,
    fontWeight='bold',
    color='lightgrey',
    dx=-30
).encode(
    text='label',
    x='percentage'
).properties(
    width=300
)

最后,结合这两个图表:

chart = (chart + text
).configure_view(
  strokeOpacity=0
)

chart.save('stacked-bar-chart.html')

以下图示显示了结果图表:

作者提供的图片

华夫图

华夫图使用方格来显示数据,类似于一堆华夫饼。

在构建华夫图之前,你必须设置 10x10 网格,如下面的代码所示:

import numpy as np
import pandas as pd
import altair as alt

# Compute x² + y² across a 2D grid
x, y = np.meshgrid(range(0, 10), range(0, 10))

ndex = 0
value = 70 # percentage

## calculate colors for each cell of the waffle chart
colors = []
for i in range(100,0,-1):
    if i <= np.round(value):
        colors.append(1)
    else:
        colors.append(0)

# Convert this grid to columnar data expected by Altair
df = pd.DataFrame({'x': x.ravel(),
                        'y': y.ravel(),
                        'z': colors})

接下来,使用 Altair 的 mark_rect() 标记方法构建你的华夫图:

chart = alt.Chart(df).mark_rect(
    size=5, 
    stroke='black'
).encode(
    x=alt.X('x:O', axis=None),
    y=alt.Y('y:O', axis=None),
    color=alt.condition(alt.datum.z == 0, 
                        alt.value('lightgrey'),
                        alt.value('#81c01e')
                        )
).properties(
    width=200,
    height=200
)

可选地,在华夫图下方添加文本:

text = alt.Chart(pd.DataFrame({'label' : [value/100]})).mark_text(
    align='center',
    baseline='middle',
    fontSize=30,
    fontWeight='bold',
    color='#81c01e'
).encode(
    text=alt.Text('label', format='.0%'),
).properties(
    width=200
)

最后,结合这两个图表并保存最终图表:

chart = chart & text

chart.save('waffle-chart.html')

以下图示显示了结果图表:

作者提供的图片

总结

恭喜你!你刚刚学会了在 Python Altair 中表示百分比的三种不同方法:

  • 通过 mar_arc() 标记方法绘制甜甜圈图

  • 通过 mark_bar() 标记方法绘制 100% 堆叠条形图

  • 通过 mark_rect() 标记方法绘制华夫图。

感谢阅读这篇博客文章!如果你有任何问题或意见,请留言或直接在 LinkedIn 上联系我。我很乐意与你联系!

你可以在我的书的 GitHub 仓库 中找到这个示例的完整代码,Data Storytelling with Generative AI using Python and Altair 的第六章。

相关文章

## 3 种在 Python Altair 中构建地理地图的方法

一项关于如何在 Python Altair 中构建三种不同地图的数据可视化教程:区划图、点密度图……

[towardsdatascience.com ## 3 种你可能不知道的时间序列可视化方法

一个关于如何在 Python 和 Altair 中可视化时间序列的即用教程

towardsdatascience.com

参考资料

在你离开之前…

要查看 Altair 的实际应用,请观看视频教程。

常见强化学习算法的三个根本性缺陷(及其修复方法)

原文:towardsdatascience.com/three-fundamental-flaws-in-common-reinforcement-learning-algorithms-and-how-to-fix-them-951160b7a207

对抗在日常强化学习算法中遇到的这些缺陷

Wouter van Heeswijk, PhDTowards Data Science Wouter van Heeswijk, PhD

·发布于 数据科学前沿 ·阅读时间 8 分钟·2023 年 1 月 30 日

--

图片由 Varvara Grabova 提供,来源于 Unsplash

像 Q 学习和 REINFORCE 这样的强化学习算法已经存在了几十年,其教科书中的实现仍然被广泛使用。不幸的是,它们存在一些根本性的缺陷,这大大增加了学习良好策略的难度。

本文讨论了经典强化学习算法的三个主要缺陷,并提出了克服这些缺陷的解决方案。

I. 选择被高估的动作

问题

大多数强化学习算法使用某种形式的价值函数来捕捉后续奖励,其中许多基于广为人知的 Q 学习算法。Q 学习的机制是选择带来最高期望值的动作。根据初始化情况,这一机制可能会在尝试的第一个动作上陷入困境,因此我们还以概率ϵ选择随机动作,通常设定为 0.05 左右。

在极限情况下,我们将无限次地探索每个动作,Q 值将收敛于真实值。然而,在实践中,我们处理的是有限样本和有偏的 Q 值。因此,Q 学习一贯选择具有高估值的动作

想象这样一个场景,我们玩两个完全相同的老丨虎丨机。机器 A 在早期迭代中给出的奖励高于平均水平,因此其 Q 值更高,我们持续玩 A。由于机器 B 仅偶尔被选择,我们需要很长时间才能发现 Q 值实际上是相同的。

更一般地说,价值函数总是会存在不完美之处,而强化学习往往会执行过高估值的动作。从某种意义上说,强化学习“奖励”了糟糕的估计,这显然不是一个理想的特性。

多臂老丨虎丨机问题清楚地展示了选择过高估值动作的影响[照片由Bangyu Wang提供,见Unsplash]

解决方案

Q 学习的问题可以追溯到使用相同观察进行采样和更新的实践。我们可以通过使用一个策略进行采样,另一个策略进行更新来解耦这些步骤。这正是双 Q 学习(Van Hasselt, 2010)所做的。

双 Q 学习使用一个网络来采样动作,但用另一个网络的输出更新 Q 值。这一过程将采样和学习解耦,并且克服了过度估计。[来源:Van Hasselt (2010)]

更一般地说,使用目标网络是一个良好的实践。目标网络是策略的周期性副本,用于生成我们训练的目标值(而不是使用完全相同的策略来生成观察和目标)。这种方法减少了目标与观察之间的相关性。

另一种解决角度是考虑我们 Q 值估计的不确定性。与其仅仅记录动作的期望值,我们还可以跟踪观察的方差,以指示我们可能偏离真实值的程度。使用不确定性边界和知识梯度是实现这一目标的两种方法。我们不仅考虑选择具有最高期望 Q 值的动作,还考虑从新观察中能学到多少。这种方法偏向于探索具有高不确定性的动作,同时仍然进行智能采样。

你应该知道的七种强化学习探索策略

纯探索和-利用,ϵ-贪婪,玻尔兹曼探索,乐观初始化,置信区间……

towardsdatascience.com

II. 糟糕的策略梯度更新

问题

策略梯度算法已经存在了几十年,并且是所有现代演员-评论员模型的根基。普通的策略梯度算法——例如 REINFORCE——依赖梯度来确定权重更新的方向。高奖励和高梯度的组合会产生强烈的更新信号。

传统的策略梯度更新函数,通过目标函数梯度 ∇_θJ(θ) 和步长 α 来更新策略权重 θ。

这个想法似乎很自然。如果奖励函数的斜率很陡,你就朝那个方向迈出大步。如果斜率很小,那就没有必要进行大幅更新。虽然这个逻辑可能很有说服力,但它也是根本有缺陷的。

左:超调行为的示例,进行了一次大幅策略更新但错过了奖励峰值。右:停滞行为的示例,被困在局部最优点,梯度接近 0。 [图像由作者提供]

梯度仅提供局部信息。它告诉我们斜率的陡峭程度,但不告诉我们在该方向上应走多远;我们可能会超调。此外,策略梯度并未考虑缺乏梯度信号可能会使我们陷入次优平稳点

更糟的是,我们无法通过强制权重更新在某个参数区域内来控制这种行为。例如,在下面的图中,相同幅度的权重更新对策略产生了非常不同的效果。

两个高斯策略更新的示例。尽管这两个更新在参数空间中的大小相同,但左侧的策略显然受到的影响要大得多,而右侧的影响则较小 [图像由作者提供]

解决方案

一个简单的开始是尝试各种学习算法。传统的随机梯度下降(SGD)算法只考虑一阶矩。现代学习算法(例如 ADAM)考虑了二阶矩,通常会显著提升性能。虽然不能完全解决问题,但性能提升可能会非常显著。

熵正则化是一种常见的方法来防止普通策略梯度算法的过早收敛。大致来说,强化学习中的熵是衡量动作选择不可预测性的指标。熵正则化为探索未知动作添加了奖励,当我们对系统了解较少时,这种奖励会更高:

强化学习中的熵度量

更复杂的策略梯度算法扩展还考虑了二阶导数,这提供了函数的局部灵敏度信息。在平稳区域,我们可以安全地大步前进而不会产生后果。在陡峭但弯曲的坡度上,我们则会更倾向于谨慎地前进。诸如 自然策略梯度 TRPO PPO 之类的算法考虑了更新的敏感性,无论是显式还是隐式地考虑了二阶导数。目前,PPO 是首选的策略梯度算法,在实现的便利性、速度和性能之间取得了良好的平衡。

自然策略梯度的权重更新方案。F(θ) 的 Fischer 矩阵包含关于局部灵敏度的信息,生成动态权重更新。

## 自然策略梯度在强化学习中的解释

传统的策略梯度方法固有地存在缺陷。自然梯度收敛更快、更好,形成了…

towardsdatascience.com ## 信赖区域策略优化(TRPO)解释

强化学习算法 TRPO 基于自然策略梯度算法,确保更新保持…

towardsdatascience.com ## 近端策略优化(PPO)解释

从 REINFORCE 到在连续控制中的首选算法的旅程

[towardsdatascience.com

III. 性能不足的离策略学习

问题

某些算法(例如,基于 Q 学习的算法)依赖于 离策略学习,这意味着 更新可能会使用与实际观察到的不同的动作。而在策略学习中需要一个元组 (s,a,r,s’,a’) — 实际上,就像其算法名称 SARSA 一样— 离策略学习使用已知的最佳动作 a 而不是 a’。因此,我们只存储 (s,a,r,s’) 用于权重更新,并且学习策略独立于代理的动作。

由于其设置,离策略学习可以通过从经验回放缓冲区中提取先前观察进行重用,这在生成观察(计算上)昂贵时尤其方便。我们只需将状态s’输入到我们的策略中以获得动作a,使用得到的值来更新 Q 值。状态从ss’的转移动态不需要重新计算。

不幸的是,即使在大型数据集上广泛训练离策略强化学习算法,当部署时,它通常表现得远不如预期。这是为什么呢?

问题归结为一个常见的统计学警告。假设是训练集代表真实数据集。当这种情况不成立时——通常是不成立的,因为更新的策略生成了不同的状态-动作对——策略就会拟合一个不反映代理最终操作环境的数据集。

解决方案

真实的离策略学习——例如,仅从静态数据集中学习良好的策略——在强化学习中可能从根本上是不可行的,因为更新策略不可避免地会改变观察到状态-动作对的概率。由于我们无法彻底探索搜索空间,我们不可避免地将值外推到未见的状态-动作对

最常见的解决方案是不要在完全静态的数据集上训练,而是不断丰富数据集,添加在新策略下生成的观察。也可以考虑移除旧的样本,这些样本不再代表最近策略生成的数据。

另一个解决方案是重要性采样,本质上是根据观察在当前策略下生成的可能性重新加权观察。对于每个观察,我们可以计算它在原始和当前策略下生成的概率的比率,使得来自相似策略的观察更可能被抽取。

重要性采样考虑原始策略和目标策略之间的相似性,以更高的概率选择在与当前策略相似的策略下生成的观察。

如果你继续在样本外使离策略算法表现不佳,可以考虑切换到在策略算法。特别是当生成新观察很便宜时,样本效率的损失可能会被增强的策略质量所弥补。

## 如何建模经验回放、批量学习和目标网络

关于使用 TensorFlow 2.0 进行稳定和成功的深度 Q 学习的三种基本技巧的快速教程

[towardsdatascience.com

摘要

这篇文章解决了传统强化学习算法中遇到的三个常见缺陷,以及应对这些缺陷的策略。

I. 高估的动作

问题:

  • 基于价值函数逼近的算法系统性地选择具有高估值的动作。

解决方案:

  • 使用目标网络减少目标和观察之间的相关性(例如,如在双重 Q 学习中)。

  • 在动作选择中纳入价值估计的不确定性(例如,不确定性边界、知识梯度)。

II. 策略梯度更新不良

问题:

  • 策略梯度算法常常执行不佳的更新步骤,例如,当陷入局部最优时采取小步,或当过度步骤时错过奖励峰值。

解决方案:

  • 使用包括例如 ADAM 的学习算法——它除了跟踪一阶梯度外,还跟踪动量——而不是标准的随机梯度下降。

  • 向奖励信号中添加熵奖励,鼓励对未知区域进行更多探索。

  • 部署包含二阶导数的算法(无论是显式还是隐式),如自然策略梯度、TRPO 或 PPO。

III. 离策略学习的表现不足

问题:

  • 回放缓冲区中的经验可能无法代表样本外的经验,从而导致值被错误地外推并且性能下降。

解决方案:

  • 更新回放缓冲区,添加新的经验并删除旧的经验。

  • 执行重要性采样以增加选择来自更接近目标策略的策略的经验的概率。

  • 切换到在线学习(如果采样观察很便宜)。

参考文献

问题 I: 高估的动作

Hasselt, H. (2010). 双重 Q 学习。神经信息处理系统进展23

Matiisen, Tambet (2015). 解密深度强化学习。计算神经科学实验室。取自 neuro.cs.ut.ee/demystifying-deep-reinforcement-learning/

问题 II: 策略梯度更新不良

Mahmood, A. R., Van Hasselt, H. P., & Sutton, R. S. (2014). 具有线性函数逼近的离策略学习的加权重要性采样。神经信息处理系统进展27

康奈尔大学计算优化开放教材。 (2021). ADAM。网址:optimization.cbe.cornell.edu/index.php?title=Adam

问题 III: 离策略学习的表现不足

Fujimoto, S., Meger, D., & Precup, D. (2019 年 5 月). 无探索的离策略深度强化学习。在 国际机器学习大会(第 2052–2062 页)。PMLR。

阈值化 — 使图像更清晰的方式 (CV-04)

原文:towardsdatascience.com/thresholding-a-way-to-make-images-more-visible-b3e314b5215c

通过阈值化从图像中提取更多信息

Md. ZubairTowards Data Science Md. Zubair

·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 4 月 26 日

--

图片由 Jonas Svidras 提供,来源于 Pixabay

动机

在现实世界中,我们并不总是处理百分之百清晰的图像。有时,图像会变得模糊、扭曲等等。从这些类型的图像中提取信息成为一个关键问题。这就是为什么透明、清晰和更具视觉吸引力的图像在获取全面信息方面扮演着重要角色。

左侧图像取自pxfuel,并且在创意共享许可下使用。右侧图像是应用阈值化后的生成结果。

阈值化后的图像更具视觉清晰度。除了图像,这种阈值化技术在数千种应用场景中可能会很有帮助。如果你读到文章的最后,你将掌握 如何使用、在哪里使用以及何时使用 图像阈值化的技能。

具体来说,图像阈值化将图像转换为二值图像,以提取更多信息。

目录

  1. 什么是图像阈值化?

  2. 全局阈值化与局部阈值化的区别

  3. 流行的阈值化技术和 Python 实现

什么是图像阈值化?

图像阈值化作用于灰度图像。这是一种将灰度图像分割成二值图像的方法 [1]。对于阈值化,某个特定的像素强度值被视为阈值值。所有大于或小于阈值的像素都被分配为最大或最小值。这将整个图像转换为二值图像。因为现在只有两种像素值。

假设我们想对像素强度值 123、50、180、200 应用图像阈值化。我们将阈值设置为 128。因此,所有大于 128 的值将变为最高的像素强度值 255,而小于 128 的值将变为 0。

左侧的图像是阈值化后的结果,右侧的图像是阈值化之前的图像(来自 Wikimedia Commons,公共领域许可)

看一下上面的两张图像。应用阈值化后,得到的图像完全是白色或黑色。相对高强度值的像素被转换成完全白色(强度值 255),而低强度值则变成纯黑色(强度值 0)。

全局阈值化和局部阈值化的区别

有许多阈值化技术可用。

相同的过程或阈值通常应用于整个图像。这种类型的阈值化被称为 全局阈值化

另一方面,局部/自适应阈值化在局部区域内工作。相同的阈值值不会应用于整个图像。我们可以对图像的不同部分应用不同的阈值值。

全局图像阈值化并不适用于所有情况。有时自适应阈值化更为合适。看看下面的图像。

图像来自于 OpenCV 文档

在原始图像中,光照在不同图像位置上有所变化。对整个图像应用相同的阈值将产生类似于全局阈值化图像的结果。自适应阈值化提供了更好的结果。

流行的阈值化技术和 Python 实现

我们将讨论 全局阈值化 技术——*简单阈值化和 Otsu 阈值化*。以及局部/自适应阈值化技术。

  • 简单阈值化

简单阈值化是一种全局阈值技术。在这种方法中,我们需要设置一个边界(阈值)强度值。阈值值用于转换新的像素强度值。看看下面的表格。

不同的简单阈值化技术(图像由作者提供)

看看上面的表格。在操作部分,pixel(x,y) 表示阈值化图像的更新后的特定强度值,而 src(x,y) 表示应用阈值化之前图像的原始强度值。

⭐ Python 实现️

为了演示目的,我使用了以下图像。

图像由 Picdream 提供,来源于 Pixabay

一些通用代码用于导入库和加载图像 —

阈值化始终作用于灰度图像。因此,我将图像从 BGR(OpenCV 通常以 BGR 格式读取图像)转换为灰度图像,代码为cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

二值阈值化

**OpenCV**库通过提供各种函数来实现计算机视觉技术,使我们的工作变得更加轻松。以下代码片段帮助我们实现二值阈值化。其中120是阈值,255是最大强度值。因此,所有低于120的强度值将被设为0,其余为255

输出会因不同的阈值而有所变化。

二值反向阈值化

这正好是二值化阈值的反向操作。我们将阈值设为120,最大值设为255。因此,所有低于120的强度值将为255(白色),其余为0(黑色)。请查看以下代码。

因此,在二值反向阈值的情况下,输出正好是二值阈值的相反结果。

截断阈值化

在我们的编码示例中,我们将阈值设为145,最大值设为255。因此,对于截断阈值化,所有低于145的值将被设为阈值145。否则,它将保持不变。

输出文本比之前的技术更为清晰可见。

阈值为零

在阈值为零的情况下,低于阈值的像素值将被设为零,否则保持不变。为了演示目的,我将阈值设置为 145,最大值设置为 255。代码如下所示。

阈值反转

这正好是阈值为零的相反操作。

大津法阈值化

大津法的阈值化与简单阈值化有所不同。它通过计算类间方差来设置阈值。有两个类别,即背景和前景像素。类间方差可以通过以下公式计算。

在这里,背景和前景类的平均权重分别用WbWf表示。类的均值权重用µbµf表示。我们需要计算所有像素强度值的类间方差。最终,选择类间方差最高的阈值。

为了演示目的,下面给出了一个示例。

3 x 3 图像(图像由作者提供)

在上图中,圆圈内的值是每个像素的强度值。像素强度为***0,1,2, 和 3***。强度值的频率如下所示。

在这里,我们将阈值设置为 2。所有小于 2 的值被视为背景,大于或等于 2 的值是前景。在上述图表中,红色条是背景频率,绿色条代表前景频率。

阈值计算如下所示。

阈值 2 的类间方差计算(图片作者提供)

其他阈值 0、1 和 3 的类间方差见下表。

图片由作者提供

最大的类间方差为 0.89,与阈值 2 相关。因此,根据 Otsu 方法,阈值为 2。

以下是对之前图像的 Python 实现。

这个结果非常棒,与之前的技术相比效果很好。让我们看看 Otsu 阈值处理的阈值。

自适应/局部阈值处理

在自适应/局部阈值处理的情况下,整个图像被分成一些分段,并对每个分段应用全局阈值。当光照均匀分布时,这种阈值处理更为可取。

演示灰度图像具有 6 个不同的分段(图片作者提供)

跟随灰度图像。方框是图像的分段。在自适应阈值处理的情况下,每个分段会被分开以应用全局阈值。因此,我们可以调整每个区域/分段的阈值。

文本图像上的自适应阈值处理实现如下所示。

结果也令人满意。

我们还可以将两个阈值输出进行融合以提取更多信息,如下所示。

结论

图像阈值处理是从模糊或不清晰的图像中提取信息的最佳方法之一。阈值处理技术不限于上述技术,但这些技术被广泛使用。选择何时使用哪种阈值处理技术完全依赖于你;我相信如果你对上述方法有清晰的概念,你将能够做出正确的决定。

参考文献

  1. 阈值处理(图像处理) — 维基百科

通过以下链接支持我的写作。

[mzh706.medium.com/membership](https://mzh706.medium.com/membership)

我计算机视觉系列的先前文章 —

[## NumPy 和 OpenCV 计算机视觉入门 (CV-01)]

使用 Python 开始你的计算机视觉编码

towardsdatascience.com ## 关于计算机视觉中色彩表示的全面指南 (CV-02)

颜色空间和颜色模型的详细解释

[towardsdatascience.com ## 最简单的图像融合指南 (CV-03)

最简单的图像融合和粘贴指南

[towardsdatascience.com

透过镜子,谷歌在眼睛中发现了什么

原文:towardsdatascience.com/through-the-looking-glass-and-what-google-find-there-in-the-eye-e7a836eb9571

| 计算机视觉 | 医疗 AI | CNN

或者,谷歌如何利用深度学习来诊断眼部照片中的疾病

Salvatore RaieliTowards Data Science Salvatore Raieli

·发表于Towards Data Science ·阅读时间 12 分钟·2023 年 3 月 30 日

--

图片由作者使用 OpenAI DALL-E 生成

谷歌最近发布了一篇科学论文,展示了人工智能模型如何通过一张简单的眼睛照片预测多个系统性生物标志物。

这如何运作?这些结果是如何得出的?这为何重要?我们将在本文中讨论这些问题。

眼睛中的隐藏宝藏

图片由v2osk提供,来自 Unsplash

疾病诊断通常需要昂贵仪器的检查,然后由受过训练的医疗专业人员进行解读。这并不总是可行。并非所有医院都有相同的仪器,有时还缺乏专家。

例如,糖尿病视网膜病变(DR)的诊断需要使用检查眼睛后部的眼底相机。这需要由资质高的人员进行分析。此检查还可以揭示其他状况,如心血管风险、贫血、慢性肾病及其他系统性参数。

曾经认为,眼底图像可以使用机器学习算法进行分析。然而,谷歌发布的一篇论文在 2017 年展示了外部眼睛照片能够诊断糖尿病视网膜病变并检测血糖控制不佳。

“糖尿病相关并发症可以通过使用专用相机拍摄眼底照片来诊断,这些照片可视化眼睛的后部区域。相反,使用标准消费级相机进行前眼成像可以揭示影响眼睑、结膜、角膜和晶状体的情况。” 图片来源:这里

在这项工作中,作者使用了来自加利福尼亚和其他队列的 145,832 名糖尿病患者的照片。作者然后使用了Inception V3(它之前已经在ImageNet上进行过训练)进行这项研究,结果显示:

我们的结果显示,眼睛的外部图像包含糖尿病相关视网膜病变、血糖控制不良和脂质升高的信号。(来源)

简而言之,Inception V3 当时在 ImageNet 上展示了最先进的性能(准确率 > 78.1%)。此外,Inception 比以前的模型计算效率更高。该模型通过使用并行结构(在同一个块中使用不同类型的卷积层)和积极的正则化来达到这些结果。在同一篇文章中,作者定义了一些原则,这些原则在接下来的几年里塑造了卷积神经网络(CNN)领域:

  • 避免表示瓶颈。表示大小应从输入到输出逐渐减少。

  • 高维表示在网络内部本地处理起来更容易。这显示出可以更快地训练网络。

  • 空间聚合可以更好地减少维度而不会丢失信息

  • 平衡网络的宽度和深度

“模型的高层次图示” 图片来源:这里

作者使用了经典的监督学习,实际上,他们使用了患者眼睛的图像作为是否存在疾病(糖尿病视网膜病、升高的血糖或升高的脂质)的基础真值。因此,训练出的模型在糖尿病视网膜病诊断方面显示出超过 80%的曲线下面积(AUC),而对于血糖和脂质则显示出突出的(但较低的)结果。

结果令人惊讶,因为通常这些系统参数可以从前眼得出,而这项初步研究表明,从外眼的照片中,通过深度学习也可以得出相同的结论。

此外,通过使用消融研究显著性图,作者也能更好地理解模型为何做出这些预测:

首先,消融分析表明,对于所有预测,图像的中心(瞳孔/晶状体、虹膜/角膜和结膜/巩膜)比图像的周边(例如眼睑)更为重要。其次,显著性分析同样表明,DLS 最受图像中心附近区域的影响。(来源

重要性图。图片来源:这里

消融研究:图像不同区域的重要性。图片来源:这里

这些结果表明,对于日益增长的糖尿病患者群体,一些参数可以在不需要专业医疗人员的情况下进行测量。此外,外眼的照片也可以使用简单相机拍摄获取。

虽然需要进一步工作来确定是否有额外的要求,例如光线、拍摄距离或角度、图像稳定性、镜头质量或传感器保真度,但我们希望通过外眼图像进行疾病检测的技术最终可以广泛提供给患者,无论是在诊所、药店还是在家中。(来源

无论如何,目前这些模型并不打算取代广泛筛查,而是用于标识哪些患者需要进一步筛查(这种方法比问卷调查更可靠)。

作者们继续评估模型,并引起了对潜在偏见和包含问题的关注。确实,生物医学领域人工智能模型最大的问题之一是,如果数据集不能代表总体人群,这可能会导致误导性结果。

我们的开发数据集覆盖了美国范围内多个不同地点,包括在 301 个糖尿病视网膜病筛查点拍摄的超过 300,000 张去标识化照片。我们的评估数据集包括来自 18 个美国州的 198 个地点的超过 95,000 张图像,其中包括主要为西班牙裔或拉丁裔患者的数据集、主要为黑人患者的数据集以及包括非糖尿病患者的数据集。我们对不同人口统计学和身体特征(如年龄、性别、种族和民族、白内障存在、瞳孔大小,甚至相机类型)的患者群体进行了广泛的亚组分析,并将这些变量作为协变量进行控制。(来源

眼睛,灵魂的镜子

图片由Caroline Veronez在 Unpslash 提供

Google 和其他研究人员也认为这种方法很有前途。因此,他们后来尝试将其扩展到其他标记和其他疾病。

到目前为止,作者已展示他们的模型能够有效诊断眼科疾病(糖尿病视网膜病)。另一方面,疾病种类繁多,诊断复杂(例如,找到合适的测试,昂贵的仪器并不总是存在等等)。因此,问题仍然是模型是否也能捕捉到眼睛图像中的其他疾病迹象。

我们可以将这种方法扩展到其他疾病吗?

毕竟,深度学习模型能够识别那些微妙的模式,而这些模式可能对非专家来说难以识别。考虑到这些假设,Google 研究人员决定测试是否可以在眼底图像中检测心血管风险因素。

心血管疾病是全球主要死亡原因,能够早期诊断可能拯救无数生命。此外,风险分层对于识别和管理高风险患者群体至关重要。通常,通过病史和不同测试(血糖和胆固醇水平的血液样本、年龄、性别、吸烟状态、血压和体重指数)获得的多个变量用于诊断和分层患者。有时所有必要的数据可能并不齐全(如一项综述研究所示)。

这项研究的作者展示了不仅可以预测一些患者特征(在某些数据未记录或缺失的情况下非常有用),例如BMI、年龄、性别和吸烟状态,还可以预测与心血管疾病相关的参数,例如收缩压(SBP)和舒张压(DBP)。

“左上角的图像是来自英国生物库数据集的彩色视网膜图像样本。其余图像显示的是相同的视网膜图像,但为黑白色。每个预测的软注意力热图以绿色覆盖,指示神经网络模型在进行图像预测时所使用的热图区域。” 图片来源:这里

作者们也使用了相同的 Inception V3 模型。此外,为了处理连续变量,作者们使用了分箱技术,基本上是将变量划分为不同的区间,使用不同的分割点(例如,<120,120–140,140–160 和 ≥160 作为 SBP)。

此外,作者使用了一种称为soft attention的技术来识别与特定特征相关的区域。简而言之,soft attention是一种考虑图像不同子区域并使用梯度下降反向传播的方法(无需在模型中实现注意力机制)。

soft-attention 可以帮助发现模型的预测错误。图像来源:这里

在另一项工作中,作者测试了贫血。一种影响超过 16 亿人的病症,需要监测血红蛋白浓度以进行诊断(这是一种侵入性测试,可能引起疼痛和感染风险)。

使用深度学习对贫血分类进行预测。图像来源:这里

在这种情况下,他们使用了Inception V4。这是一个比之前描述的模型更晚的版本(在这篇后续文章中,作者描述了通过添加残差连接可以改进 Inception V3 的架构)。Inception V4 展示了如何测试和使用不同类型的 Inception 块(其中有不同的卷积层,既有并行也有顺序)。

残差连接。图像来源:这里

在这项后续工作中,Google 展示了这种方法不限于分类(患者是否有贫血),还包括是否可以测量血红蛋白浓度(回归任务)。作者训练了一个用于分类的模型和一个用于回归任务的模型(Inception V4,预训练于 ImageNet)。

对于回归问题,作者仅使用了均方误差作为损失函数(而非交叉熵)。最终的预测是通过创建一个包含 10 个模型的集成(以相同方式训练)来实现的,并将这些模型的输出进行平均,以得到最终的预测结果。

“预测血红蛋白浓度。每个蓝点代表每位患者的测量血红蛋白浓度和预测值”。图片来源:这里

广阔景观的一瞥

图片由Bailey Zindel提供,来源于 Unsplash

到目前为止,研究人员观察到的通常是少量参数。通常,血液检测可以在一次检查中监测更多的参数。一个眼睛的照片模型能否估计一组系统性生物标志物?

这是谷歌今年测试并刚刚发布的内容:

[## 一种用于外眼照片中新型系统性生物标志物的深度学习模型:…

系统性疾病引起的眼部后遗症已被充分记录,并作为全球公认的基础…

www.thelancet.com](https://www.thelancet.com/journals/landig/article/PIIS2589-7500(23)00022-5/fulltext?source=post_page-----e7a836eb9571--------------------------------)

显然,这不是一项容易的任务,部分原因是当你想进行这样的分析时,存在发现虚假和错误结果的风险(也称为多重比较问题)。换句话说,同时进行的统计推断越多,发现错误推断的风险就越大。

虚假相关的示例。图片来源:这里

因此,作者首先将数据集分成两部分。他们在“开发数据集”上训练模型并进行分析,选择了九个最有前景的预测任务,并在测试数据集上评估了模型(他们仍然纠正了多重比较)。

他们首先收集了包含眼睛图像和相应实验室测试结果的数据集。然后,作者训练了一个卷积神经网络,该网络以外眼图像为输入,并预测临床和实验室测量结果。在这种情况下,这是一个多任务分类,每个任务都有一个预测头(以便可以使用交叉熵作为损失)。作者决定为每个任务选择阈值(与临床医生协商后选择)。

在这种情况下,作者使用了 2020 年发布的 Big Transfer(BiT)模型,该模型在训练时能够在各种其他数据集上很好地推广。简而言之,该模型与 ResNet 非常相似,不过在训练过程中使用了一些技巧,例如组归一化(GN)和权重标准化(WS)。你可以在这个GitHub 仓库中找到该模型。

预训练模型的转移性能超越了现有技术。图像来源:这里

超过了基线模型(对患者数据进行的逻辑回归)。虽然这些结果仍然不足以用于诊断应用,但与初步筛查工具一致(糖尿病预筛查)。

基线模型与深度学习模型的 AUC 比较。图像来源: 这里

在本研究及之前的研究中,作者使用了桌面相机(患者还使用了头托)获取图像,并在良好的照明条件下生成了高质量的图像。因此,作者尝试通过降低分辨率来查看模型是否有效。

作者注意到,即使将图像缩小到 150x150 像素,模式仍然对图像质量具有鲁棒性。

这个像素计数低于 0.1 百万像素,比典型的智能手机相机要小得多。(来源

输入图像分辨率的影响。 上图: 为本实验缩放到不同尺寸的样本图像。 下图: DLS 在图像尺寸下的性能比较”。图像来源:这里

此外,作者研究了图像的哪个部分对模型的预测目的重要。为此,作者在训练和评估期间遮蔽了几个区域(瞳孔或虹膜,将图像转换为黑白)。

结果表明,信息通常不仅仅局限于瞳孔或虹膜,并且颜色信息对大多数预测目标至少有一定的重要性(来源

实验遮蔽不同的图像区域或移除颜色 图像来源:这里

尽管这篇文章令人印象深刻,但仍然存在一些局限性。实际上,认为它可以在现实世界中使用仍然为时尚早。首先,照片是在最佳条件下获取的,我们需要验证在其他条件下获取的照片的准确性。

此外,这项工作中使用的数据集主要包括糖尿病患者,并没有充分代表一些重要的亚组——在考虑临床使用之前,需要对 DLS 的改进和评估进行更加针对性的数据收集,包括对更广泛人群和亚组的评估。(来源

分析思考

来自 Saif71.com 在 Unsplash 的图片

如本文所示,深度学习模型能够捕捉眼睛中的模式和信息,这些信息有时难以诊断。此外,诊断通常使用昂贵的工具和侵入性测试,并且需要经验丰富的人员。Google 多年来已经表明,可以通过眼睛的图像获取类似的信息。

未来,许多疾病可以通过简单的眼睛外部照片来诊断(或至少进行初步筛查)。这些照片可以通过手机摄像头简单地拍摄。此外,由于可以获得定量结果(如血红蛋白浓度),这些结果可以用于非侵入性的患者监测。

在技术方面,有趣的是像 Inception V3 这样的模型如何在第一次尝试时就取得了结果。这表明迁移学习和卷积网络的能力。此外,作者对模型进行了分类、回归和多任务分类的适配。

然而,还有几个情境是开放的。显然,Google 计划扩展数据集(正如他们在文章中所写的)。另一方面,作者使用了 CNN,也可以测试其他几种模型,如视觉变换器。此外,未来他们也可能会尝试使用语言模型作为输入,这些输入包括患者或医生的笔记(毕竟,未来是多模态的)。

另一方面,尽管这些应用可以用于没有装备医院的患者,但它也引发了伦理问题。如所见,模型还能够预测诸如年龄、性别、生活方式(吸烟/不吸烟)以及其他参数等敏感数据。这项技术也可能用于其他更具争议的应用。

无论如何,这些研究开启了非常有趣的应用领域。这不仅仅是 Google 在研究这样的模型。例如,其他团队已经显示,其他疾病如肝胆疾病可以通过眼睛识别,其他疾病也可能在不久的将来被识别。

如果你觉得这很有趣:

你可以查看我的其他文章,也可以订阅以便在我发布文章时获得通知,还可以通过LinkedIn联系我。

这是我 GitHub 仓库的链接,我计划在这里收集与机器学习、人工智能等相关的代码和许多资源。

[## GitHub - SalvatoreRa/tutorial: 关于机器学习、人工智能、数据科学的教程…

提供关于机器学习、人工智能、数据科学的教程,包括数学解释和可重用的代码(用 Python…

github.com

或许你对我最近的一篇文章感兴趣:

## 使语言模型更像人脑

语言模型和人脑在自然语言处理方面仍存在差距,激励人工智能填补这一差距

towardsdatascience.com ## 谷歌 Med-PaLM:人工智能临床医生

谷歌的新模型经过训练可以回答医学问题。怎么做的?

towardsdatascience.com ## 多模态思维链:解决多模态世界中的问题

世界不仅仅是文字:如何将思维链扩展到图像和文字?

towardsdatascience.com [## 稳定扩散填补医疗图像数据中的空白

一项新研究表明,稳定扩散可能有助于医学图像分析和稀有疾病。怎么做的?

在医疗图像数据中填补空白的稳定扩散

抛出一只猫到鸽子中间?用大型语言模型增强人类计算能力

原文:towardsdatascience.com/throwing-a-cat-among-the-pigeons-augmenting-human-computation-with-large-language-models-23910a6e026c?source=collection_archive---------19-----------------------#2023-07-21

生成性人工智能的时代提供了改善群体工作机会的可能性,而不一定是取代它

Ujwal GadirajuTowards Data Science Ujwal Gadiraju

·

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

--

照片由Steve Johnson拍摄,来源于Unsplash

我一直对词源学充满兴趣。通常,单词和短语如何获得我们熟悉的意义背后有一个引人入胜的故事。随着时代的变迁,词汇也在不断演变。机械土耳其人是 18 世纪匈牙利作家和发明家沃尔夫冈·冯·肯佩伦制造的一个下棋类人形机器。传说机械土耳其人在欧洲巡演,并在传说中的棋局中击败了拿破仑·波拿巴和本杰明·富兰克林等显赫人物。直到后来,真正的秘密才被揭示出来,原来是一个隐藏在地板下的真实人类棋手,在那里操控着机械人形的走棋。

这个故事启发了 2005 年推出的亚马逊机械土耳其人众包平台的命名。该平台旨在解决当时的替代方案无法解决的任务,并需要人工输入或智能。在这种背景下,“人工人工智能”的概念形成了,人类在机器能力之外提供智能。我们已经从那里走了很长一段路,达到了全新的“人工人工人工智能”概念的边缘。没错,你没看错,三个人工。在你试图用额头破解这个难题之前,让我们快速回顾一些记忆。

众包的早期日子

在他 2004 年出版的《群体的智慧》一书中,詹姆斯·苏罗维基探讨并综合了形成明智群体所需的特征——这种群体通常能比单一个体做出更好的决策。他指出,意见的多样性、判断的独立性和知识的分散性是实现这一目标的重要属性。2006 年,杰夫·豪在为《连线》杂志撰写的关于《众包的兴起》的文章中创造了众包这个词,它是“crowds”和“outsourcing”的合成词。他讨论了企业如何通过公开征集的方式,开始利用分布式在线社区的集体能力来完成某些任务。

亚马逊机械土耳其人在首次推出后蓬勃发展,几年内,全球数十万人通过在平台上完成任务找到了谋生的机会。这激发了全球范围内众包平台的增长,铸造并巩固了在线微任务众包工作的全新经济。研究人员和从业者开始依赖众包平台来完成各种任务,并证明了即使是高度复杂的任务也可以分解并众包。提出了系统和工具,以支持众包工作者有效完成任务。一些当代众包平台的显著例子包括 Toloka AI(“一个支持快速和可扩展 AI 发展的数据中心环境”)。

借助人类洞察力”) 和 Prolific(一个“进行研究或训练下一代人工智能”的平台)。

2009 年,ImageNet 的发布推动了整个机器学习领域。该数据集包含超过 320 万张图像,分为 12 个子树,并具有超过五千个同义词集,这是通过亚马逊机械土耳其工人众包进行的巨大数据收集努力[1]。这为多个计算机视觉任务提供了前所未有的进步机会,包括对象识别和图像分类。

我们不要忘记,这一进展伴随着一系列的试炼和磨难。许多人感受到了依赖易受认知和系统性偏见影响的人类生成数据所带来的危险的震颤。在 2013 年,一群在众包社区中著名的研究者写了一篇名为《众包工作的未来》的论文,反思了这一范式的现状以及需要立即解决的一系列挑战[2]。尽管取得了显著进展,但许多这些挑战在 10 年后的今天仍未得到解决。关于数据质量(例如,偏见传播)、平台上的权力不对称、低劣的小时工资、不公平的工作拒绝、隐形劳动、不健康的工作环境等问题已有充分记录。尽管有些人认为这是一个破碎的工作范式的脆弱之处,历史上却点缀着显著的成果,众包的力量无疑推动了技术进步的速度,只有少数人会预见到。

生成性人工智能的迷人时代

目前全球主流媒体陷入了关于生成性人工智能和大型语言模型民主化的广泛叙事中。许多生活将继续以预期和意想不到的方式受到人工智能的影响。而正是幕后辛勤工作的人工推动了这场人工智能革命。如果我们“审视人工智能的阴影,我们将发现推动它的人类”,正如玛丽·格雷和西德·苏里在《幽灵工作》中难以忘怀地指出的那样[3]。

夸大的预测和引人注目的标题将这一时代中人类的角色比作焦虑的鸽子,并将大型语言模型比作大胆的猫——隐喻中的猫打乱了队伍,使它们四散离去。但这个生成性人工智能新时代的开始对人类输入究竟意味着什么?在塑造未来技术方面,人类输入的需求是否已经在很大程度上被消除了?在本文的其余部分,我将论证答案是否定的,我们应预期的主要变化是在继续需要的人类输入的性质上。

我最近共同撰写了一篇研讨会论文,探讨了人类计算工作流程如何适应生成式 AI 模型的出现[4]。这项工作在今年早些时候于汉堡举办的顶级 HCI 会议 ACM CHI 2023 上的生成式 AI 研讨会上进行了展示。我们强调了大型语言模型(LLMs)在增强现有众包工作流程中可能发挥的作用,并讨论了如何对这些工作流程进行实证评估。

众包工作流程入门

众包工作流程是管理大规模任务如何被分解成较小任务以供众包工人完成的不同模式。众包驱动的文字处理器 Soylent 运用了Find-Fix-Verify工作流程,通过将任务分成生成和审查文本的阶段来生产高质量的文本。这使得“作家可以调用 Mechanical Turk 工人根据需要缩短、校对和编辑他们文档的部分[5]。” Iterate-and-Vote工作流程已被用于创建图像描述,在这个过程中,工人首先被要求写出图像的描述(例如,最终目的是帮助盲人)。随后,通过投票任务来确定最佳描述[6]。Map-Reduce工作流程则被提议用于“将工作分成可以并行完成的任务,映射任务到工人,并管理它们之间的依赖关系[7]。” 具有相同本质的工具,如CrowdWeaver,被提议用于管理复杂的工作流程,支持任务之间的数据共享,并提供监控工具和实时任务调整能力[8]。

利用 LLMs 提升众包工作流程

语言模型的出现不太可能使这些工作流程、框架和工具变得完全平凡。相反,众包社区独特地能够通过利用几十年的有效工作流程、人机交互方法和构建混合人类-人工智能系统的知识,来迎接 LLMs 带来的好处。

以人为本的技术开发视角专注于增强人们日常生活中的体验和提高人的能力。如果大语言模型(LLMs)确实可以帮助众包工人完成任务,它们应该以一种能够让工人更准确、更迅速地完成任务的方式被接纳和整合,或以某种方式改善他们的整体体验。

信息检索领域的研究者(这是我在过去十年中参与的一个社区)最近考虑了 LLMs 的普及对人类注释员在相关性判断中的角色意味着什么[9]。他们提出了一个人类与 LLMs 之间合作的谱系,以生成相关性判断(从人类判断到完全自动评估,类似于流行的自动化水平)。作者探讨了在辅助注释任务中引入 LLMs 的潜在好处,并将其与这样做的风险进行了权衡。显然,LLMs 可以降低创建评估集合的注释成本。然而,尚不清楚这些集合是否会系统性地与由人类创建的集合不同,以及这些文献会如何影响信息检索系统的评估,从而影响未来这些系统的设计。

除了在工作流程中支持个体写作或分类任务外,研究者们还在探索 LLMs 在辅助众包工作者中的应用。刘等人将 GPT-3 的生成能力与人类的评估能力结合,创建了一个新的自然语言推理数据集,该数据集在用作训练集时能产生更有效的模型[10]。类似地,其他人引入了一个‘生成注释助手’,以帮助生成动态对抗数据集合,显著提高了收集速率[11]。然而,关于 LLMs 如何提高众包工作流程的有效性以及如何全面评估这些工作流程,还有一些尚未完全理解的开放性问题。

前方有许多障碍吗?

与人类类似,LLMs 也可能受到偏见和不公平的影响。一方面,先前的研究表明,人类注释员在完成注释任务时容易受自身意见的影响,从而导致系统性偏见渗入结果数据集合[12]。其他研究者提出了用于对抗或报告注释过程中可能出现的认知偏见的检查清单[13]。另一方面,最近的研究揭示了 LLMs 中存在的歧视性立场和刻板印象偏见[14, 15]。

人类计算与众包研究社区(HCOMP)设计了多种有效的方法、接口、衡量标准和工具,以确保从众包工作者那里收集到高质量的数据。我们集体弄清楚如何在决策管道中集成 LLMs 的同时,提供这些质量相关的保证只是时间问题。

表面上,将大型语言模型(LLMs)整合进众包工作流程似乎相当简单。正如许多复杂系统解决方案的提议一样,说起来容易做起来难。众包涉及许多不同的利益相关者:希望收集大规模注释的任务请求者,愿意提供帮助以获得报酬的众包工人,提供基础设施并充当这些交易市场的平台,以及在下游开发或建设中间接使用产品或技术的最终用户。将 LLMs 纳入工作流程的影响可能以不同方式影响每个利益相关者。

如果众包工人能够通过在智能工作流程中利用 LLMs 变得更加高效,那么有可能在不增加成本的情况下完成更多工作。然而,还需要进一步的工作来更好地理解将 LLMs 纳入众包工作流程所带来的风险和奖励。考虑到可能需要的责任追究,谁将负责设计、开发和将 LLMs 整合到这些工作流程中呢?

历史上,众包工人通常被留给自己来提高生产力以及改善他们工作的环境和条件。难道现在不应该由众包平台和任务请求者共同负责,更好地理解如何配备基于 LLMs 的解决方案,以帮助工人成功完成任务,并提升和增强他们的工作体验吗?

人工人工人工智能与可期的未来

一项近期案例研究探讨了“人类”在文本总结任务中生成的众包数据在多大程度上确实是由人类生成的。作者发现证据表明,在他们对 Amazon Mechanical Turk 的研究中,超过 30%的众包工人已经开始依赖 LLMs [16]。尽管该研究仅报告了 44 名工人的这些见解,数字需谨慎对待,但这确实反映了越来越多的众包工人可能转向 LLM-based 解决方案的不可否认的前景,这些解决方案可以帮助他们提高生产力、最大化收入,并改善他们在众包市场中的时间。这就是“人工人工人工智能”概念的来源——众包工人可能利用 AI(LLMs 的帮助)提供所谓的“人类”输入。

:一张插图描绘了“人工人工人工智能”这一术语的出现,该术语源于[29]的 AI(1)到 AAI(2),最终到 AAAI(3)。来源:作者提供的图像。

需要进一步考虑 LLMs 的透明度和可解释性,与从人类那里获得的相比。当众包工人完成注释或其他需要决策的任务时,任务请求者可以通过后续问题提取有意义的理由。众包工人具备在需要时提供这些见解的能力。目前 LLMs 尚无法实现这一点。是的,存在模型可解释性的方法,但没有一种显示出与人类在沟通两端所能实现的效果相当的有效性。这种对 LLMs 的“黑箱”感知可能会对任务请求者和众包平台的采纳形成障碍,同时也阻碍了众包工人对这些工具的适当依赖。

人类与 LLMs?无限的可能性和一片引人入胜的问题海洋,只有少数闪烁的答案。利用这一技术进步来改善众包工作,更像是捕捉到风帆上的一阵风,而不是搅动一个黄蜂窝。让我们忙起来,因为当我们能够让人类居于中心舞台时,一个美好的未来在等待着我们。

参考文献

  1. Deng, J., Dong, W., Socher, R., Li, L. J., Li, K., & Fei-Fei, L. (2009 年 6 月)。Imagenet:大规模层次图像数据库。发表于 2009 年 IEEE 计算机视觉与模式识别会议论文集(第 248–255 页)。IEEE。

  2. Kittur, A., Nickerson, J.V., Bernstein, M., Gerber, E., Shaw, A., Zimmerman, J., Lease, M. 和 Horton, J., 2013 年 2 月。众包工作的未来。发表于 2013 年计算机支持协作工作会议论文集(第 1301–1318 页)。

  3. Gray, M. L., & Suri, S. (2019)。幽灵工作:如何阻止硅谷建立一个新的全球底层阶级。Eamon Dolan Books。

  4. Allen, G., He, G., Gadiraju, U. 提升!生成模型能为人类计算工作流程做些什么?发表于 ACM 国际计算系统人因会议(CHI 2023)的生成 AI 研讨会论文集

  5. Bernstein, Michael S., Greg Little, Robert C. Miller, Björn Hartmann, Mark S. Ackerman, David R. Karger, David Crowell, 和 Katrina Panovich. “Soylent:一个内部含有众包的文字处理器。” 发表于第 23 届年度 ACM 用户界面软件与技术研讨会论文集,第 313–322 页。2010 年。

  6. Little, G., Chilton, L. B., Goldman, M., & Miller, R. C. (2009 年 6 月)。Turkit:机械土耳其上迭代任务的工具。发表于 ACM SIGKDD 人类计算研讨会论文集(第 29–30 页)。

  7. Kittur, A., Smus, B., Khamkar, S., & Kraut, R. E. (2011 年 10 月)。Crowdforge:众包复杂工作。发表于第 24 届年度 ACM 用户界面软件与技术研讨会论文集(第 43–52 页)。

  8. Kittur, A., Khamkar, S., André, P. 和 Kraut, R., 2012 年 2 月。CrowdWeaver:可视化管理复杂的众包工作。发表于 ACM 2012 年计算机支持协作工作会议论文集(第 1033–1036 页)。

  9. Faggioli, G., Dietz, L., Clarke, C., Demartini, G., Hagen, M., Hauff, C., Kando, N., Kanoulas, E., Potthast, M., Stein, B. 和 Wachsmuth, H., 2023. 《对大型语言模型在相关性判断中的视角》。arXiv 预印本 arXiv:2304.09161

  10. Liu, Z., Roberts, R.A., Lal-Nag, M., Chen, X., Huang, R. 和 Tong, W., 2021. 《基于 AI 的语言模型在药物发现与开发中的应用》。Drug Discovery Today, 26(11), 第 2593–2607 页。

  11. Bartolo, M., Thrush, T., Riedel, S., Stenetorp, P., Jia, R. 和 Kiela, D., 2021. 《模型在循环中:利用生成式注释助手辅助众包工作者》。arXiv 预印本 arXiv:2112.09062

  12. Hube, C., Fetahu, B. 和 Gadiraju, U., 2019 年 5 月。《理解和减轻众包收集主观判断中的工作者偏见》。见于 2019 年 CHI 计算机系统人因会议论文集(第 1–12 页)。

  13. Draws, T., Rieger, A., Inel, O., Gadiraju, U., 和 Tintarev, N. (2021 年 10 月)。 《应对众包中的认知偏见检查表》。见于 AAAI 人类计算与众包会议论文集(第 9 卷,第 48–59 页)。

  14. Abid, A., Farooqi, M. 和 Zou, J., 2021 年 7 月。《大型语言模型中的持久反穆斯林偏见》。见于 2021 年 AAAI/ACM 人工智能、伦理与社会会议论文集(第 298–306 页)。

  15. Nadeem, M., Bethke, A. 和 Reddy, S., 2020. 《StereoSet:测量预训练语言模型中的刻板偏见》。arXiv 预印本 arXiv:2004.09456

  16. Veselovsky, V., Ribeiro, M. H., 和 West, R. (2023)。 《人工人工人工智能:众包工作者广泛使用大型语言模型进行文本生成任务》。arXiv 预印本 arXiv:2306.07899

TiDE:那个‘令人尴尬’的简单 MLP,击败了 Transformers

原文:towardsdatascience.com/tide-the-embarrassingly-simple-mlp-that-beats-transformers-7db77d588079

深入探讨 TiDE,使用 Darts 的实现,以及与 DeepAR 和 TFT(一个 Transformer 架构)的实际案例比较

Rafael GuedesTowards Data Science Rafael Guedes

·发布在 Towards Data Science ·9 分钟阅读·2023 年 12 月 8 日

--

随着各行业的不断发展,准确预测的重要性成为了一项不可妥协的资产,无论你是在电子商务、医疗保健、零售业,甚至是农业领域工作。能够预见未来并相应地制定计划,以克服未来的挑战,是使你在竞争中领先并在利润紧张、客户要求比以往任何时候都高的经济环境中蓬勃发展的关键。

Transformer 架构在过去几年里一直是 AI 的热门话题,特别是由于它们在自然语言处理(NLP)中的成功,chatGPT 就是最成功的用例之一,引起了所有人的关注,无论你是否是 AI 爱好者。然而,NLP 并不是唯一一个展示 Transformers 超越最先进解决方案的领域,在计算机视觉中,Stable Diffusion 及其变体也同样如此。

但 Transformers 能否超越最先进的时间序列模型?虽然已经付出了很多努力来开发用于时间序列预测的 Transformers,但似乎对于长期预测,简单的线性模型能够超越几种基于 Transformer 的方法。

在这篇文章中,我探讨了 TiDE,一个能够在长期预测中击败 Transformer 架构的简单深度学习模型。我还提供了使用 Python 的预测库 Darts 对 Walmart 数据集进行周销量预测的逐步实现。最后,我将 TiDE、DeepAR 和 TFT 在我公司实际案例中的表现进行了比较。

图 1:TiDE 是一种全新的预测模型,它是“简单到尴尬”的 MLP,旨在超越 Transformers (source)

一如既往,代码可以在 Github 上找到。

时间序列稠密编码器模型 (TiDE)

TiDE 是一种新颖的时间序列编码器-解码器模型,已显示出在长期预测中优于最先进的 Transformer 模型 [1]。它是一种多变量时间序列模型,能够使用静态协变量(例如产品品牌)和动态协变量(例如产品价格),这些协变量可以在预测范围内已知或未知。

与 Transformers 复杂的架构不同,TiDE 基于简单的编码器-解码器架构,仅使用多层感知机(MLP),没有任何注意力层。

编码器负责通过两个关键步骤将时间序列的过去和协变量映射到特征的稠密表示:

  • 特征投影,它减少了整个回溯和预测范围内动态协变量的维度。并且;

  • 稠密编码器,它接收特征投影的输出,与静态协变量和时间序列的过去连接,并将它们映射到嵌入中。

解码器接收来自编码器的嵌入,并通过两项操作将其转换为未来预测:

  • 稠密解码器,它将嵌入映射到预测范围内每个时间步的向量;以及

  • 时间解码器,它将稠密解码器的输出与该时间步的投影特征结合,以生成预测。

残差连接将回溯线性映射到与预测范围大小相同的向量,并将其添加到时间解码器的输出中,以生成最终预测。

图 2:TiDE 架构 (source)

如何在实践中使用 TiDE

本节介绍了使用 Walmart 的每周销售数据集(可在 kaggle(许可证 CC0:公共领域)上获取)逐步实现 TiDE 的过程,并借助名为 Darts 的包。

Darts 是一个用于预测和异常检测的 Python 库 [2],包含多个模型,如用于基线的简单模型、传统模型(如 ARIMA 或 Holt-Winters)、深度学习模型(如 TiDE 或 TFT)或基于树的模型(如 LightGBM 或随机森林)。它还支持单变量和多变量模型,其中一些模型提供概率预测解决方案。

训练数据集包含 2 年 8 个月的每周销售数据和 5 列:

  • Store — 存储编号和一个静态协变量

  • Dept — 部门编号和其他静态协变量

  • Date — 时间序列的时间索引,按周分布,将用于提取动态协变量,如周数和月份

  • Weekly_Sales — 目标变量

  • IsHoliday — 另一个动态协变量,用于识别某一周是否有假期

测试数据集具有相同的列,除了目标 (Weekly_Sales)

我们首先导入所需的库,并定义一些全局变量,如日期列、目标列、静态协变量、序列的频率以及要使用的缩放器:

# Libraries
import pandas as pd
import numpy as np
from darts import TimeSeries
from darts.models import TiDEModel
from darts.dataprocessing.transformers import Scaler
from darts.utils.timeseries_generation import datetime_attribute_timeseries
from darts.utils.likelihood_models import QuantileRegression
from darts.dataprocessing.transformers import StaticCovariatesTransformer

# Global Variables
TIME_COL = "Date"
TARGET = "Weekly_Sales"
STATIC_COV = ["Store", "Dept"]
FREQ = "W-FRI"
SCALER = Scaler()
TRANSFORMER = StaticCovariatesTransformer()

默认的缩放器是 MinMax Scaler,但我们可以使用来自 scikit-learn 的任何缩放器,只要它具备 fit()transform()inverse_transform() 方法。转换器也是如此,默认情况下是来自 scikit-learn 的 Label Encoder。

之后,我们加载训练数据集,并将 pandas 数据框转换为 TimeSeries,这是 Darts 所期望的格式。

我没有进行探索性数据分析,因为我的目标只是向你展示如何实现它,但我注意到了一些负值,这可能表示退货。然而,我将这些负值视为错误,并将其替换为 0。

我还使用了 fill_missing_dates 参数来添加缺失的周,并将这些日期也填充为 0。

# read train and test datasets and transform train dataset
train = pd.read_csv('data/train.csv')
train["Date"] = pd.to_datetime(train["Date"])
train[TARGET] = np.where(train[TARGET] < 0, 0, train[TARGET])
train_darts = TimeSeries.from_group_dataframe(
 df=train,
 group_cols=STATIC_COV,
 time_col=TIME_COL,
 value_cols=TARGET,
 static_cols=STATIC_COV,
 freq=FREQ,
 fill_missing_dates=True,
 fillna_value=0)

我们还加载了测试数据集,以便定义我们的预测范围以及预测范围中的假期。

# read test dataset and determine Forecast Horizon
test = pd.read_csv('data/test.csv')
test["Date"] = pd.to_datetime(test["Date"])
FORECAST_HORIZON = len(test['Date'].unique())

之后,对于训练集中的每个序列,我们创建动态协变量(周、月和一个识别特定周假期的二进制列):

# we get the holiday data that we have in both train and test dataset
holidays_df = pd.concat([train[["Date", "IsHoliday"]], test[["Date", "IsHoliday"]]]).drop_duplicates()
# convert bool to numeric
holidays_df["IsHoliday"] = holidays_df["IsHoliday"]*1
# create dynamic covariates for each series in the training darts
dynamic_covariates = []
for serie in train_darts:
    # add the month and week as a covariate
    covariate = datetime_attribute_timeseries(
        serie,
        attribute="month",
        one_hot=True,
        cyclic=False,
        add_length=FORECAST_HORIZON,
    )
    covariate = covariate.stack(
        datetime_attribute_timeseries(
            serie,
            attribute="week",
            one_hot=True,
            cyclic=False,
            add_length=FORECAST_HORIZON,
        )
    )

  # create holidays with dates for training and test
    holidays_serie = pd.merge(pd.DataFrame(covariate.time_index).rename(columns={'time':'Date'}), holidays_df, on='Date', how='left')
    covariate = covariate.stack(
                TimeSeries.from_dataframe(holidays_serie, time_col="Date", value_cols="IsHoliday", freq=FREQ)
            )
    dynamic_covariates.append(covariate)

现在,我们已经准备好所有数据,只需对数据进行缩放:

# scale covariates
dynamic_covariates_transformed = SCALER.fit_transform(dynamic_covariates)
# scale data
data_transformed = SCALER.fit_transform(train_darts)
# transform static covariates
data_transformed = TRANSFORMER.fit_transform(data_transformed)

最后,我们准备好进行预测了!

在我们的案例中,我们将预测接下来的 38 周的同一系列的每周销售额,不过你也可以预测不在训练集中的时间序列,只要它们具有相同的静态协变量。为此,你必须重复相同的数据准备过程。

TiDE_params = {
    "input_chunk_length": 4, # number of weeks to lookback
    "output_chunk_length": FORECAST_HORIZON,
    "num_encoder_layers": 1,
    "num_decoder_layers": 1,
    "decoder_output_dim": 1,
    "hidden_size": 15,
    "temporal_width_past": 4,
    "temporal_width_future": 4,
    "temporal_decoder_hidden": 26,
    "dropout": 0.1,
    "batch_size": 16,
    "n_epochs": 5,
    "likelihood": QuantileRegression(quantiles=[0.25, 0.5, 0.75]),
    "random_state": 42,
    "use_static_covariates": True,
    "optimizer_kwargs": {"lr": 1e-3},
    "use_reversible_instance_norm": False,
}

model = TiDEModel(**TiDE_params)
model.fit(data_transformed, future_covariates=dynamic_covariates_transformed, verbose=False)
pred = SCALER.inverse_transform(model.predict(n=FORECAST_HORIZON, series=data_transformed, future_covariates=dynamic_covariates_transformed, num_samples=50))

这里有一个商店 1 和部门 1 的预测示例,我们可以看到模型能够预测 2012 年黑色星期五和感恩节那周的激增,这是由于我们拥有的三个动态协变量(周、月以及识别某一周是否有假期的二进制列)。我们还可以看到,一年中有几个激增,可能来自折扣或营销活动,这可以通过动态协变量来处理,以改进我们的预测,这些数据也可以在 kaggle 上找到。

图 3:商店 1 和部门 1 的预测结果(图片由作者制作)

TiDE 与 DeepAR 的比较以及 TiDE 与 TFT 在实际应用中的比较

在我的公司,我们在 2022 年底部署了一个新的预测模型,旨在预测接下来 16 周内 264 个时间序列的订单量。

当时击败生产中模型的模型是 DeepAR,一种可在 Python 库 GluonTS [3] 中找到的深度学习架构。像 TiDE 一样,DeepAR 允许使用静态和动态协变量。

尽管 DeepAR 为我们提供了良好的结果,但它在较长时间范围(+8 周)内出现了我们称之为‘预测爆炸’的问题。从一周到另一周,仅增加一周的数据,模型就会给出与前一周完全不同的预测,预测的量高于正常水平。

尽管如此,我们制定了控制机制,并更改了一个特定的超参数(上下文长度,DeepAR 对此非常敏感),以避免此类情况,但最近发现这还不够,我们不得不每周密切监控结果,并调整其他超参数以获得合理的预测。

因此,我们决定开始新一轮研究,寻找一个更稳定可靠的模型,这时我们发现了 TiDE。我们对 TiDE 进行了超参数优化,包括使用哪些动态和静态协变量,哪些系列用于训练,哪些不用于训练等。然后我们在 2022 年 7 月到 2023 年 7 月的整整一年数据中,比较了优化后的 DeepAR 和 TiDE 模型在 26 个不同的截止日期上的表现。

结果显示,TiDE 不仅在短期和长期内优于 DeepAR(如图 4 所示),而且也解决了我们希望解决的‘预测爆炸’初始问题。

图 4:DeepAR 与 TiDE 在实际用例中的比较。用于比较的指标是 MSE,但由于保密原因在 y 轴上未显示(图像由作者制作)。

在我们的模型研究中,我们还将 TiDE 与 TFT [4](一种 Transformer 架构)进行了比较,以验证[1]中作者关于 TiDE 在长期预测中超越 Transformer 架构的说法。正如图 5 所示,TiDE 能够击败 TFT,特别是在长期预测(6 周以上)中。

图 5:TFT 与 TiDE 在实际用例中的比较(图像由作者制作)。

结论

Transformer 架构将是人类历史上下一次重大革命的基础。尽管它们在 NLP 和计算机视觉领域表现出色,但正如[1]中的作者所述,它们在长期预测方面无法超越更简单的模型。

在这篇文章中,我们将 TFT(一种 Transformer 架构)和 DeepAR 与 TiDE 进行了比较,验证了对于我们的用例,TiDE 能够超越这两个模型。

尽管 Transformer 架构足够强大以进行长期预测,但为什么谷歌还会开发一种新的非 Transformer 时间序列模型?TSMixer [5] 是谷歌开发的最新时间序列模型,它在 M5 竞赛中击败了包括 TFT 在内的 Transformers。

目前,更简单的模型似乎在预测方面更具优势,但让我们拭目以待未来的发展,看看 Transformers 是否能得到改进,以提供更好的长期结果!

保持联系: LinkedIn, Medium

参考文献

[1] Abhimanyu Das, Weihao Kong, Andrew Leach, Shaan Mathur, Rajat Sen, Rose Yu. 使用 TiDE 进行长期预测:时间序列密集编码器。arXiv:2304.08424, 2023

[2] Julien Herzen, Francesco Lässig, Samuele Giuliano Piazzetta, Thomas Neuer, Léo Tafti, Guillaume Raille, Tomas Van Pottelbergh, Marek Pasieka, Andrzej Skrodzki, Nicolas Huguenin, Maxime Dumonal, Jan Kościsz, Dennis Bader, Frédérick Gusset, Mounir Benheddi, Camila Williamson, Michal Kosinski, Matej Petrik, Gaël Grosch. Darts: 用户友好的现代机器学习时间序列,2022

[3] Alexander Alexandrov, Konstantinos Benidis, Michael Bohlke-Schneider, Valentin Flunkert, Jan Gasthaus, Tim Januschowski, Danielle C. Maddix, Syama Rangapuram, David Salinas, Jasper Schulz, Lorenzo Stella, Ali Caner Türkmen, Yuyang Wang. GluonTS: Python 中的概率时间序列模型。arXiv:1906.05264, 2019

[4] Bryan Lim, Sercan O. Arik, Nicolas Loeff, Tomas Pfister. 时间融合变压器用于可解释的多时间跨度时间序列预测。arXiv:1912.09363, 2019

[5] Si-An Chen, Chun-Liang Li, Nate Yoder, Sercan O. Arik, Tomas Pfister. TSMixer: 用于时间序列预测的全 MLP 架构。arXiv:2303.06053, 2023

整理数据集变化框架

原文:towardsdatascience.com/tidying-up-the-framework-of-dataset-shifts-cd9f922637b7?source=collection_archive---------16-----------------------#2023-07-18

回顾模型降级的原因

Valeria Fonseca Diaz Towards Data Science Valeria Fonseca Diaz

·

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

--

Marco Dalla Vecchia 合作,担任图像创作者

我们训练模型,并使用它们根据一组输入预测特定的结果。我们都知道这就是机器学习的游戏。我们对训练它们知道很多,以至于它们现在已经进化成了人工智能,这是有史以来最先进的智能水平。但在使用它们时,我们还没有那么远,我们继续探索和理解模型部署后每一个重要的方面。

所以今天,我们将讨论模型性能漂移(或简称模型漂移)的问题,也常被称为模型失败或模型退化。我们指的是我们的机器学习模型所提供的预测质量问题。不论是分类还是数值,我们关注的是该预测与真实类别或值之间的差距。当预测质量相对于我们部署模型时下降时,我们称之为模型性能漂移。你可能在文献中找到过其他术语,但在我们当前的讨论中,请关注 模型性能漂移 或简洁地说 模型漂移

我们所知道的

几个博客、书籍和许多论文已经探索并解释了模型漂移的核心概念,因此我们将首先进入当前的图景。我们主要将这些概念组织为 协变量偏移先验偏移条件偏移。后者也常被称为 概念漂移。这些偏移被认为是模型漂移的主要原因(记住,这是预测质量的下降)。总结定义如下:

  • 协变量偏移:P(X) 的分布发生变化,但 P(Y|X) 不一定发生变化。这意味着输入特征的分布发生了变化,这些偏移可能导致模型漂移。

  • 先验偏移:P(Y) 分布的变化。在这里,标签或数值输出变量的分布发生了变化。如果输出变量的概率分布发生偏移,当前模型对给定预测的确定性会大大降低,因此模型可能会出现漂移。

  • 条件偏移(即 概念漂移):条件分布 P(Y|X) 发生变化。这意味着,对于给定的输入,输出变量的概率发生了变化。根据我们目前的了解,这种偏移通常使我们很难保持预测质量。真的如此吗?

有许多来源列举了这些数据集偏移的发生实例。研究的一个核心机会是检测这些类型的偏移,而无需新的标签[1, 2, 3]。最近发布了有趣的指标,以无监督的方式监控模型的预测性能[2, 3]。这些指标确实受到数据集偏移不同概念的驱动,并且相当准确地反映了数据真实概率分布的变化。因此,我们将深入探讨这些偏移的理论。为什么?因为也许我们可以对这些定义进行一定的整理。通过整理,我们可能能够更轻松地前进,或者更清楚地理解整个框架。

为此,让我们回到一开始,慢慢推导这个故事。拿一杯咖啡,慢慢阅读,跟随我。或者,千万不要漂移!

真实模型与估计模型

我们训练的机器学习模型尝试让我们接近一个真实但未知的关系或函数,这个函数将特定输入 X 映射到输出 Y。我们自然区分真实的未知关系和估计的关系。然而,估计模型受限于真实未知模型的行为。也就是说,如果真实模型发生变化,而估计模型对这些变化不够稳健,那么估计模型的预测将会不够准确。

我们可以监控的性能涉及到估计函数,但模型漂移的原因在于真实模型的变化。

  • 真正的模型是什么? 真正的模型建立在所谓的条件分布 P(Y|X)上。这是给定输入的输出的概率分布。

  • 估计模型是什么? 这是一个函数ê(x),它专门估计 P(Y|X=x)的期望值。这个函数是与我们的机器学习模型相关的。

这是这些元素的视觉表示:

(图片由作者提供)

好的,现在我们澄清了这两个元素,我们准备好组织所谓的数据集偏移的想法,以及这些概念之间的联系。

新的安排

模型漂移的全球原因

我们的主要目标是理解我们估计模型的模型漂移的原因。因为我们已经理解了估计模型和条件概率分布之间的联系,我们可以在这里陈述我们之前已经知道的:我们估计模型漂移的全球原因是 P(Y|X)的变化。

基本而显然容易,但比我们想象的更为根本。我们假设我们的估计模型是对真实模型的良好反映。真实模型由 P(Y|X)支配。因此,如果 P(Y|X)发生变化,我们的估计模型很可能会漂移。我们需要注意我们在上述图中展示的推理路径。

我们之前已经知道这一点,那有什么新鲜的?新鲜的是我们现在将 P(Y|X)的变化称为全球原因,而不仅仅是一种原因。这将对其他原因施加一种层次结构。这种层次结构将帮助我们很好地定位关于其他原因的概念。

特定原因:全球原因的元素

知道全球原因在于 P(Y|X)的变化,自然而然地需要深入挖掘构成这一概率的元素。一旦我们识别了这些元素,我们将继续讨论模型漂移的原因。那么这些元素是什么呢?

我们一直都知道这一点。条件概率在理论上定义为 P(Y|X) = P(Y, X) / P(X),即联合概率除以 X 的边际概率。但我们可以再次展开联合概率,我们获得了几个世纪以来我们所熟知的神奇公式:

(图片由作者提供)

你已经看到我们要去哪里了吗?条件概率完全由三个元素定义:

  • P(X|Y):逆条件概率

  • P(Y):先验概率

  • P(X):协变量的边际概率

因为这三个元素定义了条件概率 P(Y|X),所以我们可以给出第二个陈述:如果 P(Y|X) 发生变化,这些变化来自于定义它的至少一个元素。换句话说,P(Y|X) 的变化由 P(X|Y)、P(Y) 或 P(X) 的任何变化定义。

也就是说,我们已经将当前知识中的其他元素定位为模型漂移的具体原因,而不仅仅是 P(Y|X) 的平行原因。

回到这篇文章的开头,我们列出了协变量漂移和先验漂移。我们注意到,还有另一个具体原因:逆条件分布 P(X|Y) 的变化。我们通常在谈论 P(Y) 的变化时会提到这个分布,好像我们一般在考虑从 Y 到 X 的逆关系 [1,4]。

新的概念层次结构

(图片来源于作者)

现在我们可以清楚地比较当前关于这些概念的思考和提出的层次结构。到目前为止,我们一直在通过识别不同的概率分布来讨论模型漂移的原因。已知这三种主要分布,P(X)、P(Y) 和 P(Y|X) 是我们 ML 模型预测质量漂移的主要原因。

我在这里提出的转折为这些概念施加了一个层次结构。在这个结构中,估计 X -> Y 关系的模型的漂移的全局原因是条件概率 P(Y|X) 的变化。这些 P(Y|X) 的变化可以来自于 P(X)、P(Y) 或 P(X|Y) 的变化。

列举一下这个层次结构的一些影响:

  • 我们可能会遇到 P(X) 发生变化的情况,但如果 P(Y) 和 P(X|Y) 也相应地发生变化,那么 P(Y|X) 保持不变。

  • 我们也可能遇到 P(X) 变化的情况,但如果 P(Y) 或 P(X|Y) 没有相应地变化,P(Y|X) 将会变化。如果你之前对此主题进行过一些思考,你可能已经看到在某些情况下,我们看到 X 的变化,并且这些变化似乎并不完全独立于 Y|X,所以最终 Y|X 也会发生变化。在这里,P(X) 是 P(Y|X) 变化的具体原因,而 P(Y|X) 反过来是我们模型漂移的全局原因。

  • 前两个陈述对于 P(Y) 也是正确的。

因为这三个具体原因可能会或可能不会独立变化,总体而言,P(Y|X) 的变化可以通过这些具体元素的变化来解释。可能是 P(X) 在这里稍微变化了一下,P(Y) 在那里稍微变化了一下,这两个变化也会导致 P(X|Y) 变化,最终导致 P(Y|X) 变化。

P(X) 和 P(Y|X) 不能被独立看待,P(X) 是 P(Y|X) 的一个原因

在这一切中,估计的 ML 模型在哪里?

好,现在我们知道所谓的协变量和先验转移是条件转移的原因,而不是与之并行。条件转移包括导致估计模型预测性能下降的具体原因。但估计模型实际上是决策边界或函数,而不是对实际概率的直接估计。那么这些原因对真实和估计的决策边界意味着什么呢?

让我们收集所有的部分,绘制连接所有元素的完整路径:

(图片由作者提供)

注意,我们的机器学习模型可以是解析的或数值的。此外,它可以是参数化或非参数化的表示。因此,最终,我们的机器学习模型是决策边界或回归函数的估计,这些是从预期的条件值中推导出来的。

这一事实对我们讨论的原因有重要的影响。虽然 P(X)、P(Y)和 P(X|Y)中发生的大多数变化将意味着 P(Y|X)和 E(Y|X)的变化,但并非所有这些变化都必然意味着真实决策边界或函数的变化。在这种情况下,如果估计的决策边界或函数最初是准确的,那么它将保持有效。看看下面这个例子:

(图片由作者提供)

  • 看到 P(Y)和 P(X)发生了变化。点的密度和位置导致了不同的概率分布。

  • 这些变化使得 P(Y|X)发生变化。

  • 然而,决策边界保持有效。

这里有一个重要的点。假设我们只关注 P(X)的变化,而没有真实标签的信息。我们想知道预测的准确性。如果 P(X)转向估计决策边界有很大不确定性的区域,预测可能是不准确的。因此,在协变量转移到决策边界的不确定区域的情况下,很可能也发生了条件转移。但我们无法知道决策边界是否发生了变化。在这种情况下,我们可以量化 P(X)的变化,这可能表明 P(Y|X)的变化,但我们无法知道决策边界或回归函数发生了什么。这是这个问题的表示:

既然我们说了这些,现在是时候再做一个声明了。当我们提到 P(Y|X)的变化时,我们讨论的是条件转移。我们所说的概念漂移可能特别指的是真实决策边界或回归函数的变化。下面是一个典型的条件转移示例,显示了决策边界的变化,但没有协变量或先验转移。实际上,这种变化来自于逆条件概率 P(X|Y)的变化。

(图片由作者提供)

对我们当前监控方法的影响

我们关心理解这些原因,以便开发方法来尽可能准确地监测我们的机器学习模型的性能。所提出的任何想法都不是对现有实际解决方案的坏消息。恰恰相反,借助这一新的概念层次,我们可能能够进一步推动检测模型性能下降原因的尝试。我们已有的方法和指标主要是在我们列出的不同概念的基础上提出的。然而,我们可能在度量指标的假设中混淆了概念。例如,我们可能将“无条件漂移”作为一种假设,而实际上它可能是“决策边界无变化”或“回归函数无变化”。我们需要继续思考这一点。

更多关于预测性能下降的信息

放大和缩小。我们已经深入探讨了框架,以思考预测性能下降的原因。但我们还有另一个维度来讨论这个话题,那就是预测性能漂移的类型。我们的模型因列出的原因而遭受痛苦,这些原因以不同的预测错位形式体现出来。我们主要发现了四种类型:偏差、斜率、方差和非线性漂移。查看这篇文章以了解这一硬币的另一面。

总结

我们在这篇文章中研究了模型性能下降的原因,并提出了基于我们之前已知概念的理论联系的框架。以下是要点:

  • 概率 P(Y|X)支配真实的决策边界或函数。

  • 估计的决策边界或函数被假定为对真实边界或函数的最佳近似。

  • 估计的决策边界或函数即是机器学习模型。

  • 机器学习模型可能会经历预测性能下降。

  • 这种退化是由 P(Y|X)的变化引起的。

  • P(Y|X)的变化是因为这些元素中的至少一个发生了变化:P(X)、P(Y)或 P(X|Y)。

  • P(X)和 P(Y)可以发生变化,而决策边界或回归函数不变。

一般的说法是:如果机器学习模型在漂移,那么 P(Y|X)也在变化。反之则不一定成立。

这一概念框架希望能成为机器学习预测性能下降这一关键主题的种子。尽管理论讨论本身非常令人愉快,但我相信这个联系将帮助我们进一步推进在实践中测量这些变化的目标,同时优化所需资源(样本和标签)。如果你有其他贡献,请加入讨论。

是什么导致你的模型在预测性能上出现漂移?

祝你思考愉快!

参考文献

[1] huyenchip.com/2022/02/07/data-distribution-shifts-and-monitoring.html

[2] www.sciencedirect.com/science/article/pii/S016974392300134X

[3]nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html#performance-estimation-deep-dive

[4] medium.com/towards-data-science/understanding-dataset-shift-f2a5a262a766

数据集转移框架的整理:示例

原文:towardsdatascience.com/tidying-up-the-framework-of-dataset-shifts-the-example-77807ee952f5?source=collection_archive---------4-----------------------#2023-09-01

条件概率如何随三个概率元素的函数变化

瓦莱里亚·冯塞卡·迪亚兹Towards Data Science 瓦莱里亚·冯塞卡·迪亚兹

·

关注 发表在 Towards Data Science ·7 min 阅读·2023 年 9 月 1 日

--

作者提供的图片

最近我讨论了模型性能下降的原因,即它们在我们训练和部署模型时预测质量下降的情况。在另一篇文章中,我提出了一种新的思考模型退化原因的方式。在这个框架中,所谓的条件概率成为全局原因。

条件概率本质上由三个我称之为具体原因的概率组成。这种概念重构最重要的学习是协变量偏移条件偏移不是两个独立或平行的概念。条件偏移可以作为协变量偏移的一个函数发生。

通过这种重构,我相信我们更容易思考原因,并且解释我们在应用中观察到的变化变得更加合乎逻辑。

这是机器学习模型的原因与模型性能的框架:

作者提供的图片。改编自 towardsdatascience.com/tidying-up-the-framework-of-dataset-shifts-cd9f922637b7

在这个框架中,我们可以清晰地看到将原因与我们估计模型的预测性能连接起来的路径。我们在统计学习中需要做的一个基本假设是,我们的模型是对真实模型(真实决策边界、真实回归函数等)的“良好”估计。“良好”可以有不同的含义,例如无偏估计、精确估计、完整估计、充分估计等。但为了简化和接下来的讨论,我们可以说它们在预测误差较小的意义上是好的。换句话说,我们假设它们能够代表真实模型。

在这个假设下,我们可以在概率P(X)、P(Y)、P(X|Y)中寻找估计模型退化的原因,并因此P(Y|X)

所以,我们今天要做的就是举例并演示不同的场景,看看P(Y|X)如何作为 3 个概率P(X|Y)P(X)P(Y)的函数变化。我们将使用二维空间中的少量点,并按拉普拉斯的方法计算这些样本点的概率。目的是消化模型退化的原因层次结构,将P(Y|X)作为全局原因,而其他三个作为具体原因。通过这种方式,我们可以理解,例如,潜在的协变量偏移有时可能是条件偏移的论据,而不是一个独立的偏移。

示例

我们今天课程中要绘制的案例是一个非常简单的。我们有两个协变量X1X2,输出Y是一个二元变量。这就是我们的模型空间的样子:

作者提供的图片

你可以看到,空间被划分为 4 个象限,而这个空间中的决策边界是交叉的。这意味着模型将样本分类为类别 1 如果它们位于第 1 象限和第 3 象限,否则分类为类别 0。为了这个练习的目的,我们将通过比较P(Y=1|X1>a)来逐步解析不同情况。这将是我们展示的条件概率。如果你在想为什么不考虑X2,这是为了简化练习。这不会影响我们想要理解的见解。

如果你仍然有些复杂的感觉,计算P(Y=1|X1>a) 相当于 P(Y=1|X1>a, -inf <X2 < inf),所以理论上,我们仍然考虑了X2

图片由作者提供

参考模型

所以,首先,我们计算我们的展示概率,得到的是1/2。在这里,我们的样本组在整个空间中相当均匀,先验概率也很均匀:

图片由作者提供

变化正在出现

  1. 一个额外的样本出现在右下角象限。所以我们首先要问的是:我们是否在讨论协变量变化?

是的,因为在X1>a的区域内的抽样比以前更多。所以,这只是一个协变量变化而不是条件变化吗?让我们看看。这里是用更新后的点集计算的所有相同概率(变化的概率用橙色标记):

图片由作者提供

我们在这里看到了什么?实际上,我们不仅仅获得了协变量变化,而且所有的概率都发生了变化。先验概率也发生了变化,因为协变量变化带来了一个新的类别 1 的点,使得这一类别的发生率大于类别 2。因此,逆概率 P(X1>a|Y=1) 正是因为先验变化而发生了变化。所有这些最终导致了一个条件变化,所以我们现在得到了P(Y=1|X1>a)=2/3,而不是1/2

这是一个思考泡泡。实际上是一个非常重要的思考泡泡。

随着抽样分布的变化,我们获得了所有在我们模型整体方案中起作用的概率的变化。然而,基于初始抽样存在的决策边界对这一变化仍然有效。

这是什么意思?

尽管我们获得了条件变化,但决策边界并没有必然退化。因为决策边界来自期望值,如果我们基于当前的变化计算这个值,边界可能保持不变,但条件概率不同。

2. 第一个象限中的样本不再存在。

因此,对于X1>a,情况保持不变。让我们看看展示的条件概率及其元素发生了什么变化。

图片由作者提供

直观上,因为在X1>a的情况下,条件概率保持不变。然而,当我们查看P(X1>a)时,我们得到2/3而不是与训练采样相比的1/2。所以在这里我们有一个协变量漂移没有 条件漂移

从数学角度来看,协变量概率如何在条件概率不变的情况下发生变化?这是因为P(Y=1)P(X1>a|Y=1)按照协变量概率的变化而变化。因此,补偿使条件概率保持不变。

有了这些变化,就像之前一样,决策边界保持有效。

3. 在不同象限中投放一些样本,同时决策边界保持有效。

这里有两个额外的组合。在一种情况下,先验保持不变,而其他两个概率发生了变化,但条件概率仍未变化。在第二种情况下, 逆概率与条件漂移相关。请查看下面的漂移。后者是一个相当重要的点,所以不要错过!

图片来源于作者

有了这些,我们现在对条件概率如何作为其他三种概率的函数发生变化有了相当扎实的理解。但最重要的是,我们也知道,并非所有的条件漂移都会使现有的决策边界失效。那么问题是什么呢?

概念漂移

在上一篇文章中,我还提出了一种更具体的定义概念漂移(或概念变化)的方法。提议是:

当决策边界或回归函数在相关概率发生变化时变得无效,我们称之为概念漂移

所以,关键在于,如果决策边界变得无效,肯定存在条件漂移。相反的情况,如我们在上一篇文章中讨论过,并且如上述示例所示,并不一定成立。

从实际角度来看,这可能并不是那么令人惊叹,因为这意味着为了真正了解是否存在概念漂移,我们可能被迫重新估计边界或函数。但至少从理论理解的角度来看,这同样引人入胜。

这是一个示例,我们有一个概念漂移,自然地伴随着一个条件漂移,但实际上没有协变量或先验漂移

图片来源于作者

这个组件分离有多酷?这里唯一改变的元素是逆概率,但与我们上面研究的先前偏移不同,这种逆概率的变化与决策边界的变化相关。现在,合法的决策边界仅仅是根据X1>a的分隔,抛弃了由X2所决定的边界。

我们学到了什么?

我们非常缓慢地走过了模型退化原因的分解过程。我们研究了概率元素的不同偏移及其与机器学习模型预测性能退化的关系。最重要的见解是:

  • 条件偏移是机器学习模型预测退化的全球原因。

  • 具体原因包括协变量偏移、先验偏移和逆概率偏移。

  • 在决策边界保持有效的情况下,我们可能会遇到许多不同的概率偏移情况。

  • 决策边界的变化会引起条件偏移,但反过来不一定成立!

  • 概念漂移可能更具体地与决策边界相关,而不是与整体条件概率分布相关。

这会带来什么?在这个定义层次结构的指导下重新组织我们的实际解决方案是我提出的最大建议。我们可能会找到许多当前问题的理想答案,关于如何监控我们的模型。

如果你目前正在使用这些定义进行模型性能监控,不要犹豫,分享你对这个框架的想法。

祝大家思维愉快!

Tidyverse 与 Base-R:如何为你选择最佳框架

原文:towardsdatascience.com/tidyverse-vs-base-r-how-to-choose-the-best-framework-for-you-29b702bdb384

R 编程最流行方法的优缺点

Rory SpantonTowards Data Science Rory Spanton

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

--

照片来自 Pexels 的 Chris Wynn

程序员是充满热情的人。他们会就自己喜欢的语言和框架展开热烈的辩论(也就是激烈的争吵),捍卫自己偏爱的做法免受批评。在 R 编程社区中,其中一个最大的争论点就是选择两个框架之一:Base-R 和 tidyverse。

Base-R 指的是 R 编程语言中自带的所有功能。tidyverse 是一个扩展 R 的包集合,拥有自己在数据分析方面的理念和立场。两者都非常受欢迎,人们对哪个更好争论不休。

Base-R 爱好者指责 tidyverse 用户不是“真正的程序员”的推文似乎成了年度惯例。这场争论有点激烈。

有些人对 R 编程非常认真。截图来自 Twitter(作者编辑)

从我的角度来看,这种对立被过分夸大了。我认为这两种方法只是不同的工具集,应该根据你的需求来选择使用。

在这篇文章中,我将考虑五个问题,以帮助你在 tidyverse 和 Base-R 之间做出选择。根据你的情况,我还会给出我对哪个框架更适合你的判断。

1. 哪个更容易使用?

就像木匠不会用黄油刀来修剪地板一样,当使用 R 时,你应该选择合适的工具。尽管 Base-R 和 tidyverse 提供了大致相同的功能,但在某些方面,一个方法要比另一个更容易使用。

例如,tidyverse 通常是快速且简单的数据操作的最佳选择。通过许多变量对数据集进行分组以创建汇总统计,使用像 dplyr 这样的包比使用 Base-R 函数要容易得多。

## 如何在 R 中分组数据

从初学者到高级用户,使用这些分组工作流

[towardsdatascience.com

然而,Base-R 更适合用于运行快速模拟等其他应用。根据你日常工作中 R 的使用情况,你的首选框架可能会有所变化。

在考虑可用性时,还值得考虑你的技能水平和编程背景。

初学者往往更喜欢 tidyverse,因为它比 Base-R 更易读。语法在函数之间一致,使得学习更容易,而且关键函数有描述性的名称,这使得阅读代码像是一组简单的指令。

话虽如此,一些经验丰富的程序员对此感到困惑,更喜欢 Base-R 的感觉。与 tidyverse 不同,Base-R 更加注重程序功能,这对来自其他语言的开发者来说更为熟悉。

2. 哪个更快?

在进行计算开销较大的操作时,执行时间非常重要。在许多情况下,Base-R 和 tidyverse 之间的速度差异非常大。

举个例子,当 Base-R 速度明显更快时,我们可以使用内置于 R 的 mtcars 数据集。执行一个简单的操作,比如筛选数据集以仅显示六缸的汽车,Base-R 的速度比 tidyverse 快超过 40 倍!

library(microbenchmark)
library(tidyverse)

results <- microbenchmark(mtcars %>% filter(cyl == 6), 
                          mtcars[mtcars$cyl == 6,])

summary(results) %>%
  as_tibble() %>%
  select(expression = expr, mean_execution_time = mean)

当然,tidyverse 版本对初学者来说更具可读性,并且有其他优点。但是,如果你运行的脚本中需要重复执行该筛选操作数百次,那么 40 倍的性能提升将非常实用。

尽管在很多情况下 Base-R 的速度比 tidyverse 更快,但有时情况也可能正好相反。虽然 Base-R 通常在速度上占优,但根据具体情况进行检查是值得的。

3. 我的合作者使用哪种工具?

尽管能够独立编写出色的代码很重要,但每个 R 用户的生活中都会遇到必须分享代码的时候。无论你是科学家、开发者还是数据分析师,让别人能够理解并使用你的代码至关重要。

在这里,你应该听取同事对 R 包的偏好。如果你合作的每个人都使用 tidyverse,那么考虑在某些情况下默认使用它以便于合作。同样地,如果他们都使用 Base-R。

与同事有共同的方法也有助于遇到问题或顽固的错误时。以个人经验为例,我在学习了 tidyverse 之后,与我以 tidyverse 为主的同事合作时要容易得多,尽管我在 R 学习的第二年才学会它。

这并不是说你必须根据合作伙伴的心情来限制自己使用 tidyverse 或 Base-R。即使我和我合作的大多数人默认使用 tidyverse,我偶尔也会为他们编写 Base-R 代码。不过,使用他们喜欢的方法作为基础是有帮助的。

4. 哪个社区更易于接触?

除了合作学习外,学习 R 最好的事情之一是它附带的在线社区。很多人和组织分享 R 的技巧和更新,这可以帮助你改进代码。

对于 tidyverse 和 Base-R 爱好者来说,社区精神总是充足的。#RStats 是获取社交媒体技巧的好地方。还有很多博客,包括 Medium 上的,提供 Base-R 和 tidyverse 的技巧。

## 如何快速学习 R 以进行数据科学

快速而免费地深入理解 R。

towardsdatascience.com

对于 tidyverse 爱好者来说,每周的 Tidy Tuesday 活动强调使用 tidyverse 包创建惊艳的可视化。R for Data Science 社区也源于 Hadley Wickham 合著的同名开创性书籍,他是 tidyverse 的共同创建者。

许多忠实的 Base-R 爱好者历史上曾在论坛上聚集。尽管许多人也在社交媒体上,但在 Twitter 和 Mastodon 等平台上,tidyverse 似乎有更多的社区存在。根据你在线上花费的时间,你可以对这两种方法有很多了解。

5. 哪个更适合软件开发?

虽然 tidyverse 很棒,但它在软件开发方面可能会有所不足。目前 tidyverse 中有超过 25 个包,每个包都需要自己的更新以保持最新。

如果你依赖这些技巧来编写自己的 R 包或其他软件,你的代码中可能会引入许多额外的依赖。虽然依赖额外的包不一定不好,但也不是理想的。

你的代码功能现在受其依赖的包的更新影响;这些更新是你无法控制的。依赖越多,重现你的环境以便他人运行你的代码就变得越困难。

如果你认真对待 R 开发并想向 CRAN 提交一个包,你会面临严格的依赖限制,这些限制是由于这些(和其他)原因。此情况下,tidyverse 包通常可能不适合。

相比之下,Base-R 不引入额外的依赖。问题解决。

你应该选择哪个:Base-R 还是 tidyverse?

综上所述,你应该选择哪一个——Base-R 还是 tidyverse?

两者都可以。

是的,这有点避重就轻。但说真的。了解这两种方法是扩展你的工具集并确保你能够处理 R 中所有类型任务的最佳方式。

话虽如此,许多程序员在日常工作中仍然专注于一种方法,并在需要时添加另一种方法的部分。以下是选择每种方法作为默认方法的一些理由。

如果你有以下情况,请将 tidyverse 设为你的默认方法:

  • 大多数你的工作涉及数据清理、可视化和常见统计

  • 你刚开始接触 R,觉得 Base-R 比 tidyverse 更容易阅读和理解

  • 大多数你的合作者和在线网络也在使用它

如果你有以下情况,请将 Base-R 设为你的默认方法:

  • 大多数你的工作涉及软件或包开发、先进的统计程序,或计算密集型操作

  • 你习惯使用与 Base-R 更相似的其他语言

  • 大多数你的合作者和在线网络也在使用它

这不是使用每个包的原因的详尽列表,但它们可以帮助你根据你的情况做出正确的选择。

作为心理学研究人员,我在进行大多数数据清理和简单分析时默认使用 tidyverse。然而,在进行更复杂的统计建模和模拟,或者当依赖性是个问题时,我会使用 Base-R。

最重要的是,我认为没有一种正确的方法。使用 tidyverse 并不会阻止你成为一个“真正的 R 程序员”,使用 Base-R 也不会阻止你编写整洁的代码。它们只是工具集,你可以用它们来做出很酷的 R 作品。

学习两者,混合搭配,使用适合工作的工具。

想要阅读我关于 R 编程、数据科学等的所有文章?请在 这个链接 注册 Medium 会员,获取我所有文章的完整访问权限以及 Medium 上的其他故事。这也直接帮助我,因为我可以从你的会员费中获得一小部分贡献,而你无需额外支付费用。

你还可以通过 订阅这里 直接将我所有的新文章送到你的收件箱。感谢阅读!

时间序列增强

原文:towardsdatascience.com/time-series-augmentations-16237134b29b?source=collection_archive---------0-----------------------#2023-10-19

一种简单但有效的增加时间序列数据量的方法

亚历山大·尼基廷Towards Data Science 亚历山大·尼基廷

·

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

--

这篇博客文章可以在 GitHub 上的 jupyter notebook找到。

增强已成为计算机视觉管道中不可或缺的组成部分。然而,它们在其他领域如时间序列中的受欢迎程度却没有达到相同的高度。在本教程中,我将深入探讨时间序列增强的世界,阐明其重要性,并提供使用强大的生成时间序列建模库 TSGM [5]的实际应用示例。

我们的起点是一个标记为(𝐗,𝐲)的数据集。在这里,𝐱ᵢ ∈ 𝐗 是多变量的(意味着每个时间点是一个多维特征向量)时间序列,而 y 是标签。预测标签 y 被称为下游任务。我们的目标是使用(𝐗,𝐲)生成额外的样本(𝐗,𝐲),这可以帮助我们更有效地解决下游任务(在预测性能或鲁棒性方面)。为了简化起见,我们在本教程中不会处理标签,但我们在这里描述的方法可以很容易地推广到有标签的情况,并且我们使用的软件实现可以通过向.generate方法添加额外参数来轻松扩展到监督情况(见下例)。

事不宜迟,让我们逐一考虑时间序列增强。

在 TSGM 中,所有的增强方法都整齐地组织在tsgm.models.augmentations中,你可以查看 TSGM documentation 提供的全面文档。

现在,让我们通过安装 tsgm 来启动编码示例:

pip install tsgm

接下来,我们导入 tsgm,并加载一个示例数据集。一个张量X现在包含 100 个长度为 64 的正弦时间序列,每个序列有 2 个特征。具有随机的偏移、频率和振幅(最大振幅为 20)。

# import the libraries
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import random
from tensorflow import keras
import tsgm
# and now generate the dataset
X = tsgm.utils.gen_sine_dataset(100, 64, 2, max_value=20)

抖动 / Gaussian 噪声

作为第一个增强方法,我们考虑抖动。

时间序列数据通过随机 Gaussian 噪声进行增强 (Wikipedia))

在 tsgm 中,Gaussian 噪声增强可以如下应用:

aug_model = tsgm.models.augmentations.GaussianNoise()
samples = aug_model.generate(X=X, n_samples=10, variance=0.2)

Gaussian 噪声增强的理念是,向时间序列中添加少量的抖动可能不会显著改变它,但会增加数据集中这种噪声样本的数量。这通常使下游模型对噪声样本更具鲁棒性或提高预测性能。

Gaussian 噪声的超参数及其添加方式(例如,Gaussian 噪声可能会在时间序列的末尾增加)是一个困难的问题,并且取决于特定的数据集和下游问题。通常值得进行实验,看看这些参数如何影响目标模型的性能。

在这里,我们提供了原始正弦数据集样本和增强样本的可视化。

原始时间序列和通过抖动生成的合成数据。

打乱特征

另一种时间序列增强的方法是简单地打乱特征。这种方法仅适用于特定的多变量时间序列,其中这些序列对所有或特定特征的排列是不变的。例如,它可以应用于每个特征表示来自各种传感器的相同独立测量的时间序列。

为了解释这种方法,假设有五个相同的传感器,标记为 S_1, S_2, S_3, S_4 和 S_5。为了说明,假设传感器 1 到 4 在旋转方面是可以互换的。那么,尝试在 S_1,…,S_5 传感器的旋转角度上进行特征旋转的数据增强是有意义的。

在这个例子中,有五个传感器,来自这些传感器的测量生成了五维时间序列数据。传感器 1 到 4 可以被任意旋转以生成新的合成样本(例如,1->2,2->3,3->4,4->1)。因此,通过对原始数据应用这些变换,可以生成新的合成样本。

与之前的例子类似,增强可以如下进行:

aug_model = tsgm.models.augmentations.Shuffle()
samples = aug_model.generate(X=X, n_samples=3)

这里,我们展示了一个具有 5 个特征的时间序列样本,以及一个增强样本,类似于上面的图像。

原始时间序列和通过特征洗牌生成的合成数据。

切片和洗牌

切片和洗牌增强 [3] 将时间序列切成片段并洗牌这些片段。对于在时间上表现出某种形式的不变性的时间序列,可以执行这种增强。例如,假设从可穿戴设备上测量的时间序列持续了几天。在这种情况下,一个好的策略是按天切割时间序列,并通过洗牌这些天来获得额外的样本。切片和洗牌增强在以下图像中可视化:

切片和洗牌示意图。

aug_model = tsgm.models.augmentations.SliceAndShuffle()
samples = aug_model.generate(X=X, n_samples=10, n_segments=3)

让我们查看增强样本和原始样本:

原始时间序列和通过切片和洗牌生成的合成数据。

幅度扭曲

幅度扭曲 [3] 通过将原始时间序列与立方样条曲线相乘来改变时间序列数据集中每个样本的幅度。这个过程会缩放时间序列的幅度,这在许多情况下是有益的,例如我们用正弦波 n_knots 数量的结点在随机幅度下分布的合成例子,其中 σ 由函数 .generate 中的参数 sigma 设置。

aug_model = tsgm.models.augmentations.MagnitudeWarping()
samples = aug_model.generate(X=X, n_samples=10, sigma=1)

这里是一个原始数据和通过 MagnitudeWarping 生成的增强样本的例子。

原始时间序列和通过幅度扭曲生成的合成数据。

窗口扭曲

在这种技术 [4] 中,时间序列数据中的选定窗口要么加速,要么减速。然后,将整个结果时间序列缩放回原始大小,以保持时间步长在原始长度。请参见下面的这种增强的例子:

这种增强可能是有益的,例如,在设备建模中。在这种应用中,传感器测量可以根据设备的使用方式改变变化速度。

在 tsgm 中,与往常一样,可以通过以下方式进行生成:

aug_model = tsgm.models.augmentations.WindowWarping()
samples = aug_model.generate(X=X, n_samples=10, scales=(0.5,), window_ratio=0.5)

下面可以找到一个生成的时间序列示例。

原始时间序列和通过窗口规整生成的合成数据。

动态时间规整重心平均(DTWBA)

动态时间规整重心平均(DTWBA)[2]是一种基于动态时间规整(DTW)的扩增方法。DTW 是一种测量时间序列相似性的方法。其思想是“同步”这些时间序列,如下图所示。

DTW 用于测量两个时间序列信号 sin(x)和 sin(2x)的相似性。DTW 测量通过白色线条显示。此外,还可视化了交叉相似性矩阵。

关于 DTW 计算的更多细节可以在rtavenar.github.io/blog/dtw.html中找到。

DTWBA 的过程如下:

1. 算法选择一个时间序列来初始化 DTWBA 结果。这个时间序列可以明确给出,也可以从数据集中随机选择。

2. 对于每一个N个时间序列,算法计算 DTW 距离和路径(路径是最小化距离的映射)。

3. 在计算所有N个 DTW 距离后,算法通过对所有找到的路径进行平均来更新 DTWBA 结果。

4. 算法重复步骤(2)和(3),直到 DTWBA 结果收敛。

参考实现可以在tslearn中找到,描述可以在[2]中找到。

在 tsgm 中,可以通过以下方式生成样本:

aug_model = tsgm.models.augmentations.DTWBarycentricAveraging()
initial_timeseries = random.sample(range(X.shape[0]), 10)
initial_timeseries = X[initial_timeseries]
samples = aug_model.generate(X=X, n_samples=10, initial_timeseries=initial_timeseries )

原始时间序列和通过 DTWBA 生成的合成数据。

使用生成机器学习模型进行扩增

另一种扩增方法是训练一个机器学习模型在历史数据上,并训练它生成新颖的合成样本。这是一种黑箱方法,因为很难解释新样本是如何生成的。在时间序列的情况下,可以应用几种方法;特别是,tsgm 拥有 VAE、GANs 和高斯过程。以下是使用 VAE 生成合成时间序列的示例:

n, n_ts, n_features = 1000, 24, 5
data = tsgm.utils.gen_sine_dataset(n, n_ts, n_features)
scaler = tsgm.utils.TSFeatureWiseScaler() 
scaled_data = scaler.fit_transform(data)
architecture = tsgm.models.zoo“vae_conv5”
encoder, decoder = architecture.encoder, architecture.decodervae = tsgm.models.cvae.BetaVAE(encoder, decoder)
vae.compile(optimizer=keras.optimizers.Adam())
vae.fit(scaled_data, epochs=1, batch_size=64)
samples = vae.generate(10)

结论

我们探索了几种合成时间序列生成方法。许多方法将归纳偏差引入模型,并在实际应用中非常有用。

如何选择?首先,分析你的问题是否包含不变性。它对随机噪声不变吗?对特征洗牌不变吗?

接下来,选择一组广泛的方法,并验证这些方法中的任何一种是否提高了下游问题的性能(tsgm 有下游性能指标)。然后,选择那些提供最大性能提升的扩增方法。

最后但同样重要的是,我感谢 Letizia Iannucci 和 Georgy Gritsenko 对于这篇文章写作的帮助和有益讨论。除非另有说明,所有图片均由作者提供。

这篇博客文章是 TSGM 项目的一部分,我们正在创建一个工具,通过增强和合成数据生成来提升时间序列管道。如果你觉得它有帮助,可以查看 我们的仓库 并考虑引用 关于 TSGM 的论文

@article{
  nikitin2023tsgm,
  title={TSGM: A Flexible Framework for Generative Modeling of Synthetic Time Series},
  author={Nikitin, Alexander and Iannucci, Letizia and Kaski, Samuel},
  journal={arXiv preprint arXiv:2305.11567},
  year={2023}
}

参考文献

[1] H. Sakoe 和 S. Chiba, “用于口语词汇识别的动态规划算法优化”。IEEE 声学、语音与信号处理学报, 26(1), 43–49 (1978)。

[2] F. Petitjean, A. Ketterlin & P. Gancarski. 动态时间规整的全局平均方法及其在聚类中的应用。模式识别,Elsevier,2011,第 44 卷,第 3 期,第 678–693 页。

[3] Um TT, Pfister FM, Pichler D, Endo S, Lang M, Hirche S,

Fietzek U, Kulic´ D (2017) 使用卷积神经网络对可穿戴传感器数据进行数据增强以监测帕金森病。发表于第 19 届 ACM 国际多模态交互会议论文集,第 216–220 页。

[4] Rashid, K.M. 和 Louis, J., 2019. Window-warping: 一种用于施工设备活动识别的 IMU 数据时间序列数据增强方法。发表于 ISARC. 国际自动化与施工机器人学会国际研讨会论文集(第 36 卷,第 651–657 页)。IAARC 出版社。

[5] Nikitin, A., Iannucci, L. 和 Kaski, S., 2023. TSGM: 一种用于合成时间序列生成建模的灵活框架。arXiv 预印本 arXiv:2305.11567Arxiv 链接

对跑步者疲劳检测的时间序列分类 — 一个教程

原文:towardsdatascience.com/time-series-classification-for-fatigue-detection-in-runners-a-tutorial-d649e8eb322f?source=collection_archive---------2-----------------------#2023-12-07

对跑步者可穿戴传感器数据进行的参与者间和参与者内分类的逐步演示

K BahavathyTowards Data Science K Bahavathy

·

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

--

图片由作者提供

使用可穿戴传感器收集的运行数据可以提供有关跑步者表现和整体技术的洞见。这些传感器提供的数据通常具有时间序列的特性。本教程讲解了一个疲劳检测任务,其中时间序列分类方法用于跑步数据集。在本教程中,时间序列数据以原始格式使用,而不是从时间序列中提取特征。这导致数据中增加了一个维度,因此使用传统向量格式的数据的传统机器学习算法效果不佳。因此,需要使用特定的时间序列算法。

数据包含跑步者在正常和疲劳状态下的运动捕捉数据。数据使用位于爱尔兰都柏林大学的惯性测量单元(IMU)收集。本教程中使用的数据可以在zenodo.org/records/7997851 找到。数据呈现一个二分类任务,我们试图预测‘疲劳’和‘非疲劳’之间的区别。在本教程中,我们使用专门的 Python 包,Scikit-learn;这是一个用于 Python 的机器学习工具包,以及sktime;这是一个专门为时间序列机器学习创建的库。

数据集包含多个数据通道。在这里,为了简化问题,我们将问题建模为单变量问题,因此仅使用一个数据通道。我们选择了幅度加速度信号,因为它是性能最佳的信号[1, 2]。幅度信号是每个方向分量平方和的平方根。

关于数据收集和处理的更多详细信息可以在以下文献中找到,[1, 2]。

总结一下,在本教程中:

  • 使用最先进的时间序列分类技术在可穿戴传感器收集的数据上执行时间序列分类任务。

  • 对跑步者疲劳检测中使用的参与者间模型(全球化)和参与者内模型(个性化)进行了比较。

分类任务的设置

首先,我们需要加载分析所需的数据。对于此评估,我们使用“Accel_mag_all.csv”中的数据。我们使用 pandas 加载数据。确保你已经从https://10.5281/zenodo.7997850 下载了此文件。

import pandas as pd

filename = "Accel_mag_all.csv"
data = pd.read_csv(filename, header = None)

需要从 sktime 和 sklearn 包中调用一些函数,因此我们在开始分析之前加载它们:

from sktime.transformations.panel.rocket import Rocket
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import RidgeClassifierCV, LogisticRegression, LogisticRegressionCV
from sklearn.model_selection import LeaveOneGroupOut

接下来,我们将标签和参与者编号分开。从这里开始,数据将通过数组来表示。

import numpy as np

X = data.iloc[:,2:].values

y =  data[1].values
participant_no =  data[0].values

对于这个任务,我们将使用 Rocket 变换结合岭回归分类器。Rocket 是一种用于时间序列分类的最先进技术[3]。Rocket 通过生成随机卷积核,然后沿时间序列进行卷积来生成特征图。然后,在此特征图上使用简单的线性分类器,例如岭回归分类器。可以创建一个管道,首先使用 Rocket 对数据进行变换,标准化特征,最后使用岭回归分类器进行分类。

rocket_pipeline_ridge = make_pipeline(
    Rocket(random_state=0), 
    StandardScaler(), 
    RidgeClassifierCV(alphas=np.logspace(-3, 3, 10))
)

全球化分类

在我们有来自多个参与者的数据的应用中,使用所有数据一起意味着一个参与者的数据可能同时出现在训练集和测试集中。为避免这种情况,通常进行留一法(LOSO)分析,即模型在所有参与者的基础上进行训练,但测试时只测试一个被排除的参与者。这对于每个参与者都重复此过程。这种方法可以测试模型在参与者之间泛化的能力。

logo = LeaveOneGroupOut()

logo.get_n_splits(X, y, participant_no)

Rocket_score_glob = []
for i, (train_index, test_index) in enumerate(logo.split(X, y, participant_no)):
    rocket_pipeline_ridge.fit(X[train_index], y[train_index])

    Rocket_score = rocket_pipeline_ridge.score(X[test_index],y[test_index])
    Rocket_score_glob = np.append(Rocket_score_glob, Rocket_score)

打印出上述结果的摘要:

print("Global Model Results")
print(f"mean accuracy: {np.mean(Rocket_score_glob)}")
print(f"standard deviation: {np.std(Rocket_score_glob)}")
print(f"minimum accuracy: {np.min(Rocket_score_glob)}")
print(f"maximum accuracy: {np.max(Rocket_score_glob)}")

上述代码的输出:

Global Model Results
mean accuracy: 0.5919805636306338
standard deviation: 0.10360659996594646
minimum accuracy: 0.4709480122324159
maximum accuracy: 0.8283582089552238

从这次 LOSO 分析中得到的准确率明显较低,有些数据集的结果甚至和随机猜测一样差。这表明,一个参与者的数据可能无法很好地泛化到另一个参与者。这是处理个人传感数据时常见的问题,因为运动技术和整体生理状况因人而异。此外,在此应用中,一个人如何应对疲劳可能与另一个人不同。让我们看看通过个性化模型是否可以提高性能。

个性化分类

在构建个性化模型时,预测是基于个人的数据进行的。在将时间序列数据分为训练集和测试集时,应以数据未被打乱的方式进行。为此,我们将每个类别分为单独的训练集和测试集,以保持训练集和测试集中每个类别的比例,同时保持数据的时间序列特性。使用跑步的前三分之一的数据来训练模型,以预测最后三分之一的数据。

Rocket_score_pers = []
for i, (train_index, test_index) in enumerate(logo.split(X, y, participant_no)):

    #print(f"Participant: {participant_no[test_index][0]}")
    label = y[test_index]
    X_S = X[test_index]

    # Identify the indices for each class
    class_0_indices = np.where(label == 'NF')[0]
    class_1_indices = np.where(label == 'F')[0]

    # Split each class into train and test using indexing
    class_0_split_index = int(0.66 * len(class_0_indices))
    class_1_split_index = int(0.66 * len(class_1_indices))

    X_train = np.concatenate((X_S[class_0_indices[:class_0_split_index]], X_S[class_1_indices[:class_1_split_index]]), axis=0)
    y_train = np.concatenate((label[class_0_indices[:class_0_split_index]], label[class_1_indices[:class_1_split_index]]), axis=0)

    X_test = np.concatenate((X_S[class_0_indices[class_0_split_index:]],X_S[class_1_indices[class_1_split_index:]]), axis=0)
    y_test = np.concatenate((label[class_0_indices[class_0_split_index:]], label[class_1_indices[class_1_split_index:]]), axis=0)

    rocket_pipeline_ridge.fit(X_train, y_train)

    Rocket_score_pers = np.append(Rocket_score_pers, rocket_pipeline_ridge.score(X_test,y_test))

打印出上述结果的摘要:

print("Personalised Model Results")
print(f"mean accuracy: {np.mean(Rocket_score_pers)}")
print(f"standard deviation: {np.std(Rocket_score_pers)}")
print(f"minimum accuracy: {np.min(Rocket_score_pers)}")
print(f"maximum accuracy: {np.max(Rocket_score_pers)}")

上述代码的输出:

Personalised Model Results
mean accuracy: 0.9517626092184379
standard deviation: 0.07750979452994386
minimum accuracy: 0.7037037037037037
maximum accuracy: 1.0

通过个性化模型,性能得到了显著提升。因此,在此应用中,显然从一个人到另一个人的泛化存在困难。

结论

为了对来自可穿戴传感器的时间序列数据进行分类,使用了最先进的技术 Rocket。这项分析表明,在这一领域中,个性化模型能带来更好的分类模型。

全球分类与每个参与者的个性化分类所获得的准确率

上图显示了使用个性化模型在性能上的显著提升,对于许多参与者,性能几乎翻倍。这种现象可能与个体间生理和跑步技巧的差异有关。从用户的角度来看,全球模型和个性化模型在不同的应用场景下都有其优点。例如,在需要监测个体用户运动技巧的临床环境中,个性化模型可能会很有用。然而,从单个个体收集足够的数据以进行准确预测可能是困难的,因此对于许多应用来说,全球模型将是理想的选择。

本教程中介绍的代码也可以在 github 上找到: github.com/bahavathyk/TSC_for_Fatigue_Detection

参考文献:

[1] B. Kathirgamanathan, T. Nguyen, G. Ifrim, B. Caulfield, P. Cunningham. 通过对可穿戴传感器数据进行时间序列分析来解释跑步者的疲劳,XKDD 2023: 第五届国际解释性知识发现数据挖掘研讨会,ECML PKDD,2023,xkdd2023.isti.cnr.it/papers/223.pdf

[2] B. Kathirgamanathan, B. Caulfield 和 P. Cunningham,“基于惯性测量单元的全球化运动分类模型,”2023 IEEE 第 19 届国际体感网络会议(BSN),波士顿,马萨诸塞州,美国,2023 年,页码 1–4,doi: 10.1109/BSN58485.2023.10331612。

[3] A. Dempster, F. Petitjean 和 G. I. Webb. ROCKET:使用随机卷积核进行极其快速和准确的时间序列分类。《数据挖掘与知识发现》,34(5):1454–1495,2020。

时间序列复杂性分析使用熵

原文:towardsdatascience.com/time-series-complexity-analysis-using-entropy-ec49a4aaff11?source=collection_archive---------2-----------------------#2023-09-04

这里有一种方法,可以用几行代码了解你的时间序列有多复杂

Piero PaialungaTowards Data Science Piero Paialunga

·

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

--

图片由作者提供,使用 Midjourney 生成

每个数据科学家都知道:解决机器学习问题的第一步是探索数据。

而且,这不仅仅是理解哪些特征可以帮助你解决问题。这实际上需要领域知识、大量的努力、很多询问和尝试去了解。这是一个必要的步骤,但在我看来,这是第二步。

第一步在某种程度上、形态上或形式上,基于对你数据的复杂性分析。他们是让你在总是一样的东西中寻找细节和模式,还是输出完全不同?他们想让你找到 0.0001 和 0.0002 之间的距离,还是找到 0 和 10 之间的距离?

让我更好地解释一下。

比如说,我是一个信号处理专家。我学习了傅里叶变换、chirplet 变换、小波变换、希尔伯特变换、时间序列预测、时间序列聚类、1D CNN、RNN,以及许多其他吓人的名字。

在时间序列领域,一个非常常见的问题是从一个输入(可能是另一个时间序列)到一个时间序列输出。例如:

  • 你有一个实验设置的属性,你想使用机器学习来模拟你的实验: 这实际上是我的博士论文,称为代理建模

  • 你有了股票市场到第 300 天的数据,你想预测第 301 天的情况:这非常著名,被称为时间序列预测

  • 你有一个非常脏或嘈杂的信号,你想清理它:这被称为编码器-解码器信号去噪,也非常著名。

在这些问题中,我惊讶地发现,首先看的通常是输出(而不是输入)时间序列

假设我在我的数据集中取一个随机时间序列。这个时间序列是一个温和且平滑的正弦和余弦组合吗?它是一个多项式函数吗?它是一个对数函数吗?还是一个我连名字都无法称呼的函数?

如果我再拿一个随机时间序列,它会如何变化?任务是基于从明显的基线中观察到的小变化,还是识别整个数据集中完全不同的行为?

用一句话来说,我们试图理解我们的任务有多复杂:我们在估计时间序列的复杂性。现在,“复杂”这个词对我们每个人的意义可能不同。

当我妻子向我展示她的解剖学课程时,我发现它们极其复杂,但对她来说,这只是另一个星期二 😃

好消息是,有一种更科学和独特的方式来描述复杂性:熵的概念

1. 1/0 时间序列的熵(理论)

让我们从一个非常简单的例子开始定义熵:一个只能有值 1 和 0 的时间序列。我知道这并不是我们习惯处理的时间序列类型,但你可以想象成每分钟你进入房间时翻一个硬币:如果是正面,你测得 1;如果是反面,你测得 0(或者相反,坦白说我并没有特别偏好 1 是正面……)。

作者制作的图像

现在,如果你考虑一下,当它在我们的大脑中没有真正的印象时,或者当它没有给你大量信息时,某些东西会更“复杂”。

我不再逗你了,我将给你这个该死的的方程:

方程 1

让我们分解一下:

  • X是我们的时间序列的领域,在我们的例子中,X =

  • p(x)是验证 X 中值 x 的概率

为什么里面有对数?这是什么意思?为什么有那个负号?

让我们通过示例来学习。

想象一下 X 为 0(尾部)的概率是 0,X 为 1(头部)的概率是 1. 这甚至不是真正的时间序列,因为它总是 1. 熵的值是多少?

现在,p(x=0)=0,因此第一个贡献是 0. p(x=1)=1,但 1 的对数是 0. 这意味着第二个贡献也是 0,因此熵确实是 0。

熵是 0 是什么意思?意味着时间序列完全不复杂,这很有意义,因为它看起来像这样:

图片来源于作者

这个时间序列没有“复杂性”,对吧?这就是为什么它的熵是 0。

如果我们知道 p(x=0)=p(x=1)=0.5,那么意味着 1 和 0(正面或反面)的概率完全相同。

图片来源于作者

这确实更复杂了,不是吗?

熵现在变成:

这个值高于 0. 这个值本身没有意义,但它是你可以得到的最高值。这意味着如果你将 p(x=0)更改为不同于 0.5 的值,熵会降低*。

图片来源于作者

*** 注意,当你改变 p(x=0)时,你也会改变 p(x=1),因为 p(x=1)=1-p(x=0)**

现在让我们思考一下我们的发现。

  • 当概率是 0 时,这意味着没有复杂性,因为我们已经知道了一切:你只有一个值。

  • 当概率是 0.0001 时,这意味着复杂性非常小,因为可能 x=0,但大多数时候 x 将等于 1

  • 当概率是 0.5 时,现在复杂性是最大的,因为你真的不知道接下来会发生什么:它可能是 1 或 0,概率相同

这就是我们认为的“复杂”的概念。在简单的 1/0 方式中,你可以通过回顾出现次数来找到概率,并获取熵。

2. 1/0 时间序列的熵(练习)

在我们的代码中,我们将使用Python,并且我们还将使用非常基础的库:

让我们写代码以找到相同的解决方案,但使用概率的“回顾性”,或者说,使用它们的频率定义:

其中:

  • x 是定义域中的一个值:在我们的例子中,x 只有 0 和 1 两种可能,因此 x 的值为 0 或 1。

  • n(x) 是时间序列中x出现的次数。

  • N 是我们时间序列的长度。

我们将找到 p(x=0)和 p(x=1),然后使用上面的方程 1……

好的,我再为你粘贴一次:

方程 1

Python中,你可以通过这段非常简单的代码来实现:

它有效吗?让我们测试一下!

让我们生成一个长度为 100 的时间序列,0 出现的概率为 0.5:

太棒了。所以我们得到了平衡的时间序列。虽然我们设置了 0.5 作为概率,但这并不意味着完全50 和 50,如你所见,这会给我们在估计概率时带来一些误差。这就是我们生活在的不完美世界 😃

计算理论熵的方程如下:

让我们看看理论熵和实际熵是否匹配:

太棒了!它们确实匹配!

现在让我们改变 p_0,看看它们是否继续匹配:

它们匹配的误差非常小,对吧?

有趣的是,如果我们这样做三次,增加时间序列的大小,误差将会越来越小。

在大小为 10k 后,我们基本上没有实际熵和预测熵之间的差异❤

3. 任意时间序列的熵

现在,如果我们仍然假设我们的时间序列具有离散值(0、1、2……),我们可以扩展熵的定义,适用于超过 2 个值的时间序列。

例如,让我们选择一个三值情况。所以我们的时间序列可以是 0、1 或 2。

让我们创建一个新的概率向量 p_0、p_1 和 p_2。为此,我们将生成 3 个 0 到 100 之间的随机数,并将它们存储在一个向量中,然后除以总和:

我们可以使用之前相同的方程(和相同的代码)来计算实际熵和预测熵。

让我们扩展熵的定义到实际熵的定义中:

这同样适用于仅有 0/1 的情况:

正如我们所看到的,即使在三值情况下,理论熵和预测熵也相匹配:

为了向你展示我没有作弊,我们可以看到它适用于各种情况。如果我们迭代地改变 p_vector(和时间序列),我们仍然会看到实际熵和预测熵匹配:

4. 结果

在这篇博客中,我们:

  • 反思在应用任何机器学习之前分析时间序列的复杂性。

  • 反思了时间序列的无序的概念。

  • 定义了的数学方程,并通过示例进行解释。

  • 在实践中应用了 0/1 时间序列和 0、1、2 时间序列,展示了理论定义如何与我们的计算近似相匹配。

现在,这种方法的问题(限制)是有时时间序列可能过于连续,以至于该方法无法工作。但不要惊慌!有一种连续熵的定义可以修正时间序列的熵。

我将在下一篇博客中讨论!

5. 结论

如果你喜欢这篇文章并且想了解更多关于机器学习的内容,或者你只是想问我一些问题,你可以:

A. 在Linkedin上关注我,我会在上面发布所有的故事。

订阅我的新闻通讯。它会让你了解新故事,并给你机会向我提问,获取所有可能的修正或解答。

成为推荐会员,这样你就不会有“每月最大故事数量”的限制,可以阅读我(以及其他数千名机器学习和数据科学顶级作者)关于最新技术的文章。

使用 sARIMA 和 Dash 进行时间序列数据分析

原文:towardsdatascience.com/time-series-data-analysis-with-sarima-and-dash-f4199c3fc092?source=collection_archive---------3-----------------------#2023-05-06

介绍一个 Dash 网页应用程序,指导时间序列数据集的分析,使用 sARIMA 模型 | 在线应用 | Git Hub

Gabriele AlbiniTowards Data Science Gabriele Albini

·

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

--

介绍

在处理时间序列数据集时,统计模型如 SARIMA 可以是理解数据组成部分(趋势、季节性和时间依赖性)的强大工具。

本文将简要介绍 sARIMA 模型,然后展示一个网页应用程序,该程序指导用户完成分析数据和拟合最佳模型以进行预测的步骤。该应用程序是使用 Plotly Dash 和 Python 构建的:

目录:

1. Sarima 模型的理论介绍

1.1 模型的基本构件

1.2 如何选择模型超参数:ACF 和 PACF

1.3 平稳性

2. 实用模板用于处理 Sarima 模型

2.1 绘制你的数据

2.2 将数据转换为平稳

2.3 使用 ACF 和 PACF 确定合适的模型超参数

2.4 执行模型网格搜索以识别最佳超参数

2.5 最终模型:拟合和预测

结论

1. Sarima 模型的理论介绍

1.1 模型的基本构件

要理解 sARIMA 模型是什么,我们首先介绍这些模型的基本构件。

sARIMA 是由不同子模型组成的(即我们用来表示时间序列数据的多项式),其缩写为:季节性(s)自回归(AR)积分(I)移动平均(MA):

  • AR:自回归组件,由超参数“p”控制,假设在时间“t”上的当前值可以表示为前“p”值的线性组合:

AR | 作者提供的图片

  • I:积分组件由超参数“d”表示,这是应用于数据的差分变换的阶数。差分是一种用于去除数据趋势的技术(即使数据相对于均值平稳,正如我们稍后会看到的),这有助于模型拟合数据,因为它隔离了趋势组件(我们使用 d=1 处理线性趋势,d=2 处理二次趋势,等等)。差分数据 d=1 意味着处理连续数据点之间的差异:

I | 作者提供的图片

  • MA:移动平均组件,由超参数“q”控制,假设在时间“t”上的当前值可以表示为一个常数项(通常是均值)加上前“q”点误差的线性组合:

MA | 作者提供的图片

  • 如果我们考虑到目前的组件,我们得到“ARIMA”,这是一个用于处理没有季节性的时间序列数据的模型家族的名称。sARIMA 模型是一种广泛应用于具有季节性的数据的模型,通过增加一个 S 组件:季节性组件,它由一组新的 AR、I、MA 组件组成,并带有季节滞后。换句话说,一旦确定了季节性并定义了其滞后(由超参数“m”表示——例如 m=12 意味着在每年的月度数据集中,我们会看到相同的行为),我们会创建一组新的 AR(P)、I(D)、MA(Q)组件,相对于季节滞后(m)(例如如果 D=1 且 m=12,这意味着我们对序列应用 1 阶差分,滞后为 12)。

总结一下,sARIMA 模型由 7 个超参数定义:3 个用于模型的非季节性部分,4 个用于季节性部分。它们表示为:

sARIMA (p,d,q) (P,D,Q)m

多亏了模型的灵活性,我们可以“关闭”数据中未体现的组件(即如果数据没有趋势或季节性,相应的参数可以设置为 0),仍然使用相同的模型框架来拟合数据。

另一方面,sARIMA 的局限性之一是这些模型只能捕捉到 1 种季节性。如果一个每日数据集具有年季节性和周季节性,我们需要选择其中较强的一种。

1.2 如何选择模型超参数:ACF 和 PACF

为了确定模型超参数,我们通常查看时间序列的自相关部分自相关;由于所有上述组件使用过去的数据来建模现在和未来的数据点,我们应该研究过去和现在的数据是如何相关的,并定义需要多少个过去的数据点来建模现在。

因此,自相关和部分自相关是两个广泛使用的函数:

  • ACF(自相关):描述时间序列及其滞后的相关性。所有数据点都与它们之前的滞后 1、滞后 2、滞后 3 等进行比较。结果的相关性绘制在直方图上。此图(也称为“自相关图”)用于可视化整个时间序列中保留了多少信息。ACF 帮助我们选择 sARIMA 模型,因为:

ACF 有助于确定 MA(q)超参数

  • PACF(部分自相关):描述时间序列及其滞后的部分相关性。与 ACF 不同,PACF 显示了一个点 X_t 和一个滞后之间的相关性,而这种相关性并未被与其他较低滞后的常见相关性所解释。换句话说,PACF 隔离了两个项之间的直接相关性。PACF 帮助我们选择 sARIMA 模型,因为:

PACF 有助于确定 AR(p)超参数

然而,在使用这些工具之前,我们需要提到 ACF 和 PACF 只能用于“平稳”的时间序列。

1.3 平稳性

一个(弱)平稳的时间序列是一个:

  • 均值是恒定的(即序列围绕一条水平线波动,没有正或负趋势)

  • 方差是恒定的(即没有季节性或均值偏离的变化)

当然,并不是所有的时间序列本身就是平稳的;然而,我们可以通过转换来使它们变得平稳。最常见的转换用于使时间序列平稳包括:

  • 自然对数:通过对每个数据点应用对数,我们通常能够使时间序列在方差方面变得平稳。

  • 差分:通过对时间序列进行差分,我们通常能够去除趋势,使时间序列在均值方面变得平稳。

在转换时间序列后,我们可以使用两个工具来确认它是否平稳:

  • Box-Cox图:这是一个滚动均值(x 轴)与滚动标准差(y 轴)(或分组点的均值与方差)的图。如果我们在图表中没有观察到任何特定趋势,并且在两个轴上都看到很少的变化,那么我们的数据就是平稳的。

  • 增广迪基–福勒检验(ADF):一种统计检验,我们尝试拒绝原假设,即时间序列是非平稳的。

一旦时间序列平稳,我们可以分析 ACF 和 PACF 模式,并找到 SARIMA 模型的超参数。

2. 使用 Sarima 模型的实用模板

确定适合我们数据的 sARIMA 模型包括一系列步骤,我们将在 AirPassenger 数据集上执行这些步骤(数据集可在此处获取)。

每一步大致对应于 Dash 网页应用程序的一个“页面”。

2.1 绘制数据图

创建你的原始数据的折线图:一些上述特征可以通过肉眼看到,特别是平稳性和季节性。

原始折线图 | 图片由作者提供

在上面的图表中,我们看到一个正向线性趋势和一个反复出现的季节性模式;考虑到我们有月度数据,我们可以假设季节性为年度(滞后 12)。数据不是平稳的。

2.2 转换数据以使其平稳

为了找到模型超参数,我们需要使用平稳的时间序列。因此,如果数据不是平稳的,我们需要对其进行转换:

  • 对数变换开始,以使数据相对于方差平稳(对数定义在正值上。因此,如果数据呈现负值或 0 值,请在每个数据点上添加一个常数)。

  • 应用差分以使数据相对于均值平稳。通常从差分阶数 1 和滞后 1 开始。如果数据仍然不平稳,尝试相对于季节性滞后进行差分(例如,如果我们有月度数据,则为 12)。 (使用反向顺序不会有差异)。

对于我们的数据集,我们需要执行以下步骤以使其完全平稳:

平稳转换 | 图片由作者提供

在每一步之后,通过查看 ADF 检验的 p 值和 Box-Cox 图,我们看到:

  • Box-Cox 图从任何趋势中逐渐变得干净,所有点越来越接近。

  • p 值逐渐下降。我们最终可以拒绝原假设。

平稳转换(2)| 图片由作者提供

2.3 使用 ACF 和 PACF 识别合适的模型超参数

在将数据转换为平稳的过程中,我们已经确定了 3 个参数:

  • 由于我们应用了差分,模型将包括差分组件。我们应用了 1 和 12 的差分:我们可以设置 d=1 和 D=1,并且 m=12(12 的季节性)。

对于其余参数,我们可以查看变换后的 ACF 和 PACF。

一般来说,我们可以应用以下规则

  • 我们有一个AR(p)过程如果:PACF 在某个滞后“p”处有显著峰值(之后没有显著峰值),并且 ACF 衰减或显示出正弦波行为(交替的正、负峰值)。

  • 我们有一个MA(q)过程如果:ACF 在某个滞后“q”处有显著峰值(之后没有显著峰值),并且 PACF 衰减或显示出正弦波行为(交替的正、负峰值)。

  • 季节性 AR(P)或 MA(Q)过程的情况下,我们会看到显著的峰值在季节性滞后处重复出现。

通过查看我们的示例,我们可以看到以下内容:

变换后的 ACF 和 PACF | 图片由作者提供

  • 与上述行为最接近的规则,建议使用“q”介于 1 到 3 之间的某些 MA(q)过程;我们在 12 处仍然有显著的峰值,这也可能暗示着 MA(Q)过程,其中 Q=1(因为 m=12)。

我们使用 ACF 和 PACF 来获取一系列超参数值,这些值将形成模型候选者。我们可以将这些不同的模型候选者与我们的数据进行比较,并选择表现最好的一个。

在示例中,我们的模型候选者似乎是:

  • SARIMA (p,d,q) (P,D,Q)m = (0, 1, 1) (0, 1, 1) 12

  • SARIMA (p,d,q) (P,D,Q)m = (0, 1, 3) (0, 1, 1) 12

2.4 执行模型网格搜索以识别最佳超参数

网格搜索可以用来比较多个模型候选者:我们将每个模型拟合到数据中,并选择表现最好的一个。

要设置网格搜索,我们需要:

  • 创建一个包含所有可能模型超参数组合的列表,给定每个超参数的值范围。

  • 拟合每个模型,并使用选择的 KPI 测量其性能。

  • 根据表现最佳的模型选择超参数。

在我们的案例中,我们将使用AIC(赤池信息量准则)分数来比较模型性能。这个 KPI 公式是拟合误差(准确性)与模型复杂性之间的权衡。一般来说,当复杂性过低时,误差较高,因为我们过度简化了模型拟合任务;相反,当复杂性过高时,由于过拟合,误差仍然较高。这两者之间的权衡将帮助我们识别“表现最佳”的模型。

实用说明:在拟合 sARIMA 模型时,我们需要使用带有对数变换的原始数据集(如果我们已应用),但我们不希望使用经过差分变换的数据

我们可以选择保留部分时间序列(通常是最新的 20%观察值)作为测试集。

在我们的示例中,基于以下超参数范围,最佳模型是:

模型网格搜索 | 作者图片

SARIMA (p,d,q) (P,D,Q)m = (0, 1, 1) (0, 1, 1) 12

2.5 最终模型:拟合与预测

我们最终可以对训练集、测试集以及任何未来的样本观察进行预测。最终的图表是:

最终模型 | 作者图片

为了确认我们捕捉了所有的相关性,我们可以绘制模型残差的 ACF 和 PACF:

在这种情况下,强季节性成分的一些信号仍然存在,但大多数剩余滞后项的相关性为 0。

结论

上述步骤应适用于任何可以通过 sARIMA 建模的数据集。总结:

1-绘制并探索你的数据

Dash 实时应用 | 作者图片

2-应用变换使数据平稳(关注左侧图表和 ADF 测试)

Dash 实时应用 | 作者图片

3-通过查看 ACF 和 PACF(右侧图表)来确定合适的超参数

Dash 实时应用 | 作者图片

4-执行网格搜索以选择最佳超参数

Dash 实时应用 | 作者图片

5-使用最佳模型进行拟合和预测

Dash 实时应用 | 作者图片

在本地下载应用程序,上传自己的数据集(通过替换数据文件夹中的.csv 文件)并尝试拟合最佳模型。

感谢阅读!

回到未来:使用马尔可夫转移矩阵分析时间序列数据

原文:towardsdatascience.com/time-series-data-markov-transition-matrices-7060771e362b?source=collection_archive---------1-----------------------#2023-02-01

概念概述及实际应用

Chinmay KakatkarTowards Data Science Chinmay Kakatkar

·

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

--

图片由 Oto GodfreyJustin Morton 提供,来自 Wikimedia Commons:根据 CC-BY-SA-4.0 许可自由使用

在本文中,我们将探讨如何通过使用马尔科夫转移矩阵重新构造时间序列数据,来获得有趣的描述性洞察以及优雅的预测、回溯和收敛分析方法。就像科幻经典《回到未来》中 Doc 改装的 DeLorean 时间机器一样,前进和后退时间。

注意: 以下章节中的所有方程式和图示均由本文作者创建。

基本构建块

E 定义组成时间序列数据的 k 个独特事件。例如,一个时间序列可能由以下三个基本且独特的事件组成,这些事件代表在离散时间步上绘制数据时观察到的路径轨迹类型:下跌平稳上涨。设 S 定义一个长度为 n(表示离散时间步)的序列,包含在 E 中定义的事件,代表部分或全部数据。例如,序列 [上涨下跌上涨平稳上涨] 代表五个时间步的数据。

现在可以定义一个维度为k²的马尔科夫转移矩阵 M,其中每个元素 M(i, j) 描述了从时间步 t 的事件 E(i) 转移到时间步 t+1 的事件 E(j) 的概率。换句话说,M(i, j) 表示在连续时间步之间转移的条件概率。从图论的角度看,事件 E(i)E(j) 可以被视作通过有向边 E(i)E(j) 连接的节点,如果 E(i) 在时间序列数据中被 E(j) 跟随;那么马尔科夫转移矩阵 M 本质上表示了图中节点所描绘事件的邻接矩阵(或共现矩阵)的归一化版本。

接下来,让我们看看这些基本构建块可以做些什么。

转移矩阵的应用:一个简单的例子

假设我们有以下涵盖 11 个连续时间步的原始时间序列数据:[1, 2, -2, -1, 0, 0, 2, 2, 1, 2, 3]。使用上述描述的路径轨迹简化视图,我们可以将数据转换为描述相邻时间步之间转移的以下 10 个事件序列:[上涨下跌上涨上涨平稳上涨平稳下跌上涨上涨]。

我们现在可以构造以下邻接矩阵,以捕捉事件序列中共现的模式:

元素 A(i, j) 表示在我们的事件序列中,在某个时间步 t 上发生事件 i 后,接着发生事件 j 在时间步 t+1 的次数;ij 分别是行和列索引。请注意,行表示事件的顺序为 upflatdown,从上到下,而列表示从左到右的顺序相同。例如,A 的左上角元素表示在给定的事件序列中,一个 up 事件后面跟随另一个 up 事件的次数为两次。A 的中心右侧元素表示在事件序列中,一个 flat 事件后面跟随一个 down 事件的次数为一次。依此类推。

我们可以通过行或列标准化矩阵 A 来得到转移矩阵。如果我们使用基于行的标准化,则元素 M(i, j) 将描述在时间步 t 给定事件 E(i) 的情况下,在时间步 t+1 看到事件 E(j) 的概率。因此,每一行中的概率应加起来等于 1。在我们的示例中,行标准化矩阵如下所示:

类似地,如果我们使用基于列的标准化,则元素 M(i, j) 将描述在时间步 t 给定事件 E(j) 的情况下,在时间步 t-1 发生事件 E(i) 的概率。现在每一列中的概率应加起来等于 1。在我们的示例中,列标准化矩阵如下所示:

请注意,行标准化的条件概率(名义上是向前看)可能与列标准化的条件概率(向后看)不同。

Python 代码示例

要尝试这些概念,这里有一些基本的 Python 代码,它捕捉了上述示例中的内容。

首先确保你已经安装了 Pandas 包:

pip install pandas==0.25.2

然后运行以下代码:

import pandas as pd

# Define helper functions
def get_transition_tuples(ls):
    ''' Converts a time series into a list of transition tuples
    '''
    return [(ls[i-1], ls[i]) for i in range(1, len(ls))]

def get_transition_event(tup):
    ''' Converts a tuple into a discrete transition event
    '''
    transition_event = 'flat'
    if tup[0] < tup[1]:
        transition_event = 'up'
    if tup[0] > tup[1]:
        transition_event = 'down'
    return transition_event

# Generate raw time series data
ls_raw_time_series = [1, 2, -2, -1, 0, 0, 2, 2, 1, 2, 3]

# Derive single-step state transition tuples
ls_transitions = get_transition_tuples(ls_raw_time_series)

# Convert raw time series data into discrete events
ls_events = [get_transition_event(tup) for tup in ls_transitions]
ls_event_transitions = get_transition_tuples(ls_events)

# Create an index (list) of unique event types
ls_index = ['up', 'flat', 'down']

# Initialize Markov transition matrix with zeros
df = pd.DataFrame(0, index=ls_index, columns=ls_index)

# Derive transition matrix (or co-occurrence matrix)
for i, j in ls_event_transitions:
    df[j][i] += 1  # Update j-th column and i-th row

''' Derive row-normalized transition matrix:
- Elements are normalized by row sum (fill NAs/NaNs with 0s)
- df.sum(axis=1) sums up each row, df.div(..., axis=0) then divides each column element
'''
df_rnorm = df.div(df.sum(axis=1), axis=0).fillna(0.00)

''' Derive column-normalized transition matrix:
- Elements are normalized by column sum (fill NAs/NaNs with 0s)
- df.sum(axis=0) sums up each col, df.div(..., axis=1) then divides each row element
'''
df_cnorm = df.div(df.sum(axis=0), axis=1).fillna(0.00)

这应该生成以下转移矩阵:

>>> df  # Transition matrix with raw event co-occurrences up    flat  down
up    2     2     1
flat  1     0     1
down  2     0     0 >>> df_rnorm  # Row-normalized transition matrix up    flat  down
up    0.4   0.4   0.2
flat  0.5   0.0   0.5
down  1.0   0.0   0.0 >>> df_cnorm  # Column-normalized transition matrix up    flat  down
up    0.4   1.0   0.5
flat  0.2   0.0   0.5
down  0.4   0.0   0.0

一种很好的可视化转移矩阵的方法是将它们描绘为有向加权图,使用像 GraphvizNetworkX 这样的绘图包。

我们将在这里使用 Graphviz,因此你需要安装该包以便跟随操作:

pip install graphviz==0.13.2

值得浏览简短而精炼的 官方安装指南,以确保你正确安装了该包,特别是对于可能需要执行一些额外安装步骤的 Windows 用户。

一旦 Graphviz 设置好了,创建一些用于绘图的辅助函数:

from graphviz import Digraph

# Define functions to visualize transition matrices as graphs

def get_df_edgelist(df, ls_index):
    ''' Derive an edge list with weight values
    '''
    edgelist = []
    for i in ls_index:
        for j in ls_index:
            edgelist.append([i, j, df[j][i]])
    return pd.DataFrame(edgelist, columns=['src', 'dst', 'weight'])

def edgelist_to_digraph(df_edgelist):
    ''' Convert an edge list into a weighted directed graph
    '''
    g = Digraph(format='jpeg')
    g.attr(rankdir='LR', size='30')
    g.attr('node', shape='circle')
    nodelist = []
    for _, row in df_edgelist.iterrows():
        node1, node2, weight = [str(item) for item in row]
        if node1 not in nodelist:
            g.node(node1, **{'width': '1', 'height': '1'})
            nodelist.append(node1)
        if node2 not in nodelist:
            g.node(node2, **{'width': '1', 'height': '1'})
            nodelist.append(node2)
        g.edge(node1, node2, label=weight)
    return g

def render_graph(fname, df, ls_index):
    ''' Render a visual graph and saves it to disk
    '''
    df_edgelist = get_df_edgelist(df, ls_index)
    g = edgelist_to_digraph(df_edgelist)
    g.render(fname, view=True)

现在你可以生成每个转移矩阵。默认情况下,输出的图形将存储在你的工作目录中。

# Generate graph of transition matrix (raw co-occurrences)
render_graph('adjmat', df, ls_index)

# Generate graph of row-normalized transition matrix
render_graph('transmat_rnorm', df_rnorm, ls_index)

# Generate graph of column-normalized transition matrix
render_graph('transmat_cnorm', df_cnorm, ls_index)

原始共现:

行标准化的转移概率:

列标准化的转移概率:

实际应用

描述性见解

我们可以用转移矩阵做的第一件最明显的事情是通过检查矩阵及其可视化图形表示来获得描述性见解。例如,利用我们前一节的示例输出,我们可以得到如下高层次的见解:

  • 在 9 种可能的事件转移中,有 3 种在我们的样本中从未发生过(平稳平稳下降下降下降平稳)。连续的平稳事件的低概率可能表明时间序列数据所跟踪的系统中的波动性。

  • 上升 事件是唯一一个有非零概率(0.4)连续发生的事件类型。事实上,这个转移概率在我们的数据中是最高的之一,可能指向数据背后的系统中的强化效应。

  • 在我们的案例中,基于行和列的归一化产生了不同的矩阵,尽管有些重叠。这告诉我们我们的时间序列在时间上本质上是不对称的,即我们观察到的模式在回顾或前瞻时会有所不同。

预测与回溯

通过将转移矩阵的副本连接起来,我们可以生成未来和过去的事件发生概率;这可以分别称为 预测回溯。这里的一个核心假设是“历史无关紧要”;无论我们选择哪个时间步 t 作为参考点,我们都假设转移矩阵为 t+1(如果是行归一化)和 t-1(如果是列归一化)提供相关的概率。结果是,我们可以使用转移矩阵从任何任意时间步进行预测和回溯。特别地,我们可以使用行归一化的转移矩阵进行预测,使用列归一化的转移矩阵进行回溯。

以上述示例中计算的矩阵为例,假设我们在时间步 t = 25 观察到一个 上升 事件,我们希望预测在时间步 t = 27 时最可能发生的事件是什么。通过检查行归一化转移矩阵的顶行,我们可以直接看到,在紧接着的时间步 t = 26,观察到 上升平稳下降 事件的概率分别为 0.4、0.4 和 0.2。为了推导时间步 t = 27(即距离我们参考点两个时间步)的类似事件概率,我们需要将转移矩阵自身相乘,如下所示:

注意观察事件概率相对于我们的参考时间步长的变化。例如,假设在t = 25 时出现一个上升事件,那么在t = 26 时观察到另一个上升事件的概率为 0.4(向未来进一步一步),并且在t = 27 时增加到 0.56(向未来两步)。与此同时,在t = 26 时观察到平稳事件的概率也为 0.4,但在t = 27 时减少到 0.16。关键是,矩阵乘法支持预测和回溯。通常来说,向前或向后推测事件发生的概率n步,我们可以分别计算该转移矩阵的 n 次幂的行规范化或列规范化。

转移矩阵也可以用于预测原始的基础时间序列数据。假设上升下降事件对时间序列数据造成了一单位的变化。现在假设时间序列从 1 到 2(一个上升事件)在t = 25 时发生,并且我们希望预测t = 26 和t = 27 时时间序列的进展。在上升事件之后,上升平稳事件在t = 26 时都有最高的发生概率(0.4)。因此,我们可以预测,在t = 26 时,时间序列很可能是[1, 2, 3]或[1, 2, 2],两者都有可能在t = 27 时产生两种更可能的情况:[1, 2, 3]可能变成[1, 2, 3, 4]或[1, 2, 3, 3](概率均为 0.4),而[1, 2, 2]可能变成[1, 2, 2, 3]或[1, 2, 2, 1](概率均为 0.5)。一般来说,我们期望所用于生成转移矩阵的数据集越大越丰富,能够捕捉更多事件链,因此每步的预测精度越高。

转移矩阵的乘法链导致了越来越复杂但完全可分解的原始事件转移概率的组合。这种可分解性可以帮助我们更深入地了解构成时间序列数据(或随机过程)的事件相互之间的关系。

收敛分析

连接转移矩阵的概念自然地引出了一个有趣的问题:转移矩阵 M 的概率会收敛吗?具体地说,是否存在一个稳定的转移矩阵M使得MM=M?如果是,那么lim(n → ∞)⁡Mⁿ = M,也就是说我们期望由矩阵乘法链⁡Mⁿ表示的马尔可夫过程在某个时间点收敛到一个稳定状态M;在这种情况下,该过程是收敛的并因此稳定。假设我们的转移矩阵是行规范化的,元素M(i, j)给出了事件i之后紧随事件j的稳定长期概率。然而,如果找不到稳定矩阵M*,那么该过程就不是收敛的也不是稳定的。

利用之前部分的运行示例,我们可以简要描述一下马尔可夫过程的收敛是如何通过分析得出的。

首先,让我们假设存在一个稳定的转移矩阵M,使得MM=M,并且M是行归一化的。由于我们知道M的样子,我们可以把矩阵乘法写成如下形式:

然后我们有下面的线性方程组:

如果这个方程组存在解(我们可以通过高斯消元等方法来检查),那么我们也可以得到一个收敛和稳定的转移矩阵。

包裹

一旦你掌握了这个技巧,使用马尔可夫转移矩阵来重构时间序列数据可以成为你数据科学工具箱中的一个有用部分。就像你通常用折线图来可视化时间序列数据以更好地把握整体趋势一样,转移矩阵提供了数据的一个互补表述,它高度压缩但在使用场景上非常多样化。当将其可视化为有向图时,转移矩阵已经可以用于获取高层次的描述性见解。当嵌入到更大的工作流程中时,转移矩阵可以成为预测和逆向预测更复杂方法的基础。此外,虽然我们在上述部分运行的简单例子将转移矩阵视为静态实体,但我们可以针对不同的时间间隔推导出不同的矩阵;这在分析显示数据中体现出明显的趋势逆转,反映在数据中具有显著 U 形或弯头形式模式的时间序列数据中尤为有用。显然,上面讨论的观点有几种可能的延伸,所以继续尝试吧——它们在你下一个数据科学项目中可能派上用场。

气候变化的时间序列:预测能源需求

原文:towardsdatascience.com/time-series-for-climate-change-forecasting-energy-demand-79f39c24c85e

如何利用时间序列分析和预测来应对气候变化

Vitor CerqueiraTowards Data Science 维托尔·塞尔凯拉

·发布于 Towards Data Science ·阅读时间 7 分钟·2023 年 5 月 2 日

--

图片由 Matthew Henry 提供,来源于 Unsplash

这是系列文章 气候变化的时间序列 的第四部分。文章列表:

  • 第一部分: 风能预测

  • 第二部分: 太阳辐射预测

  • 第三部分: 预测大型海浪

到目前为止,我们探讨了预测在将清洁能源源整合到电网中的重要性。

预测在能源系统的需求侧也发挥着关键作用。

平衡能源需求和供应

电力系统需要确保能源供应和需求之间的平衡。这一平衡对电网的可靠性至关重要。如果需求大于供应,会导致停电。当供应超过需求时,会出现多余的能源,往往被浪费掉。

电力系统使用预测模型来帮助预测能源需求。准确的需求预测有助于更高效地生产和使用能源。这直接影响到气候,因为它减少了浪费。

分析能源消耗在家庭中也很有价值。例如,个人可以检查哪些电器消耗更多的能源,并利用这些信息在高峰时段避免更高的费用。顺便提一句:据估计,约 8%的住宅电力需求来自待机功耗 [4]。

预测能源需求

预测能源需求是一个困难的问题。

能源消耗依赖于多个因素,其中一些可能不容易获得用于建模。例如,包括影响加热或制冷设备使用的天气和经济状况。天气的特征是高度变化的模式。这些模式使得预测天气对能源需求的影响程度变得困难。

能源需求数据在不同时间尺度上展示季节性模式,包括每日、每月或每年。例如,在冬季,能源需求因加热需求而增加。

按月分布的能源需求。图片由作者提供。

捕捉所有季节性效应在构建准确的能源需求预测模型中非常重要。

你怎么做呢?

实操:预测能源需求

在本文的其余部分,我们将开发一个能源需求预测模型。你将学到如何:

  • 使用自相关和数据可视化分析多个季节性效应;

  • 从日期和时间信息中提取特征,以处理多个季节性效应。

本教程中使用的完整代码可在 Github 上找到:

数据集

我们将使用一个代表美国肯塔基州每小时电力消耗(以兆瓦为单位)的数据集[1]。数据从 2013 年到 2018 年收集,总共有 45,344 个观测值。

这就是数据的样子:

美国肯塔基州的每小时能源需求时间序列。黄色线是每日平均需求。数据来源见参考文献 [1]。图片由作者提供。

可视化季节性

上述图表表明系列中存在年度规律模式。

另一种可视化季节性的方法是使用名为季节性图的图形:

每年的平均月能源需求的季节性图。图片由作者提供。

季节性图使得查看年度月度模式变得容易。例如,能源消耗在冬季和夏季增加,在春季和秋季减少。这可能与加热(冬季)或制冷(夏季)有关。

下图中的季节性子序列图对于分析数据在月份内部和跨月份的动态也很有帮助:

每年的平均月能源需求的季节性子序列图。图片由作者提供。

使用自相关函数(ACF)分析季节性

你还可以使用自相关来分析季节性。季节性时间序列在每个季节性滞后中会显示出更高的自相关性。

自相关图。波动表明每日季节性。图片由作者提供。

上图展示了高达 48 个滞后的自相关图。自相关显示了一个由每日季节性引起的振荡模式。

所以,除了年度规律性变化外,还有明显的每日季节性。特定小时观察到的值与前一天同一小时捕捉到的值相关。

自回归

作为起点,我们将开发一个用于预测电力消耗的自回归模型。你可以查看之前的文章了解这种建模的详细信息。

from sklearn.model_selection import train_test_split

from src.tde import time_delay_embedding

# Train / test split
train, test = train_test_split(series, test_size=0.2, shuffle=False)

# using past 12 observations as explanatory variables
N_LAGS = 12
# using the next 12 hours as the forecasting horizon
HORIZON = 12

# transforming time series into a tabular format for supervised learning
X_train, Y_train = time_delay_embedding(train, n_lags=N_LAGS, horizon=HORIZON, return_Xy=True)
X_test, Y_test = time_delay_embedding(test, n_lags=N_LAGS, horizon=HORIZON, return_Xy=True)

处理多重季节效应

大多数时间序列分解或预测方法设计为处理单一季节周期。

随着数据采集成本的降低,它使得以高采样频率(如每日或每小时)收集时间序列成为可能。高频时间序列提供了更多的数据,这是训练机器学习模型的重要因素。然而,这些数据也包含复杂的季节性模式,这可能很难建模。

那么,你如何处理多重季节效应?

我们将应用两个特征提取过程来完成这项工作:

  • 基于日期和时间数据提取特征;

  • 使用基于傅里叶项的三角函数表示。

基于日期和时间提取特征

建模多重季节模式的一个简单方法是总结每个时间步的日期和时间信息。你可以提取相关的周期,比如小时或一年中的天数,并将其作为解释变量。

库 sktime 提供了一个方便的类来实现这一点:

from sktime.transformations.series.date import DateTimeFeatures

hourly_feats = DateTimeFeatures(ts_freq='H',
                                keep_original_columns=False,
                                feature_scope='efficient')

dtime_train = hourly_feats.fit_transform(X_train)

这是输出的一个示例:

这些特征对于许多具有复杂季节性的时间序列可能是有效的方法。但是,它们可能会忽略时间的连续性。例如,假设你将月份信息编码为从 1(1 月)到 12(12 月)的整数:

将月份信息编码为整数。图像由作者提供。

在这种情况下,模型无法理解 12 月在 1 月之前。使用独热编码转换月份信息也会导致这个问题。

傅里叶项

傅里叶项是基于正弦和余弦波的周期性和确定性系列。这些项的平滑性使得时间连续性的建模成为可能。

使用傅里叶项编码月份信息。图像由作者提供。

这是一个用于提取傅里叶级数的 sklearn 兼容类:

from datetime import datetime

import numpy as np
import pandas as pd

class FourierTerms:

    def __init__(self, period: float, n_terms: int, prefix=''):
        self.period = period
        self.n_terms = n_terms
        self.prefix = prefix

    def transform(self, index: pd.DatetimeIndex, use_as_index: bool = True):
        t = np.array(
            (index - datetime(1970, 1, 1)).total_seconds().astype(float)
        ) / (3600 * 24.)

        fourier_x = np.column_stack([
            fun((2.0 * (i + 1) * np.pi * t / self.period))
            for i in range(self.n_terms)
            for fun in (np.sin, np.cos)
        ])

        col_names = [
            f'{self.prefix}{fun.__name__[0].upper()}{i}'
            for i in range(self.n_terms)
            for fun in (np.sin, np.cos)
        ]

        fourier_df = pd.DataFrame(fourier_x, columns=col_names)

        if use_as_index:
            fourier_df.index = index

        return fourier_df

主要输入是季节周期(例如,12 用于每月时间序列)、日期时间信息和项数。项数影响表示的平滑性。最佳项数取决于输入数据。

几种流行的方法使用傅里叶项来建模复杂的季节性。这些方法包括ProphetTBATSgreykite时间变化回归

傅里叶项作为解释性特征包括在内,如下所示:

fourier_daily = FourierTerms(n_terms=2, period=24, prefix='D_')
fourier_monthly = FourierTerms(n_terms=2, period=24 * 30.5, prefix='M_')
fourier_yearly = FourierTerms(n_terms=2, period=24 * 365, prefix='Y_')

dfourier_train = fourier_daily.transform(X_train.index)
mfourier_train = fourier_monthly.transform(X_train.index)
yfourier_train = fourier_yearly.transform(X_train.index)

feats_train = pd.concat([X_train, dtime_train, dfourier_train,
                         mfourier_train, yfourier_train],
                        axis=1)

model = RandomForestRegressor()
model.fit(feats_train, Y_train)

因此,自动回归与总结日期和时间数据的特征相结合。下面的特征重要性评分表明这些特征为模型提供了相关信息:

随机森林重要性评分。图片由作者提供。

就像预测能源生产的情况一样,能源需求模型在较长时间范围内的准确性会降低:

MASE 误差按预测时间范围。图片由作者提供。

主要结论

  • 能源需求预测与气候变化相关。它们使电力系统能够做出明智的决策,并将清洁能源来源整合到电网中;

  • 需求时间序列受到多个因素的影响,并具有复杂的季节性;

  • 你可以通过总结日期和时间信息来处理多个季节性模式。傅里叶项是实现这一目标的常用方法;

  • 能源需求在长期内难以预测,即超过几小时。提高长期预测的准确性对电力系统的效率至关重要。

感谢阅读,下次故事再见!

参考文献

[1] PJM 每小时能源消耗(许可证:CC0:公共领域)

[2] Rolnick, David, 等。“利用机器学习应对气候变化。” ACM 计算调查(CSUR)55.2(2022):1–96。

[3] MacKay, David. 可持续能源-没有热空气。UIT cambridge,2008 年。

气候变化时间序列:大型海洋波浪预测

原文:towardsdatascience.com/time-series-for-climate-change-forecasting-large-ocean-waves-78484536be36

如何使用时间序列分析和预测来应对气候变化

Vitor CerqueiraTowards Data Science Vitor Cerqueira

·发表在 Towards Data Science ·7 分钟阅读·2023 年 4 月 25 日

--

照片由 Silas Baisch 提供,来源于 Unsplash

这是系列气候变化时间序列的第三部分。文章列表:

  • 第一部分: 风能预测

  • 第二部分: 太阳辐射预测

海洋波浪能

波浪能转换器是从海洋波浪中获取能量的浮标。照片(浮标的照片,但不是 WEC 的照片)由 Emmy C 提供,来源于 Unsplash

海洋波浪是一个有前景的可再生能源来源。

为什么选择海洋波浪?

当谈到可再生能源时,人们通常会想到太阳能或风能。这些是最受欢迎的可再生能源来源。然而,由于其一致性,海洋波浪具有巨大潜力。

我们可以在约 90%的时间内从海洋波浪中获取能量。这个数字对于太阳能或风能来说大约是 20%-30%。详细信息请参见参考文献[1]。例如,太阳能技术仅在白天有效。

除了其一致性,波浪能也比上述两种替代能源更具预测性。海洋波浪能的主要限制是生产成本。目前,这些成本相对于太阳能或风能较高。

从波浪到电力

波浪能通过称为波浪能转换器(WECs)的设备转换为电力。这些设备是放置在海面上的浮标。

海洋波浪输出的能量取决于波浪的高度。这个量随时间变化。因此,预测波浪的高度是从海洋波浪中高效生产能量的关键任务。

波浪的高度是根据所谓的显著波高来量化的。该量值定义为最高三分之一波浪的平均波高,从波谷到波峰:

统计波浪分布。图片来源here(公有领域许可证)。

当波浪过大时——第二个动机

预测波浪高度对于除能源生产之外的其他因素也很重要。

预测即将到来的大浪有助于管理海洋操作的安全。准确的预测可以防止沿海灾害,并保护波能转换器。这些设备可能需要关闭以防止大浪造成损坏。

安全问题也与船只通行有关。一艘船只在移动时需要最低水深。大浪会减少水深,这可能无法达到最低要求。因此,预测可以提高船只移动的效率,从而降低成本,提高港口的可靠性。

总结来说,预测海洋波浪的高度很重要,原因有几个:

  • 估计能源生产以管理电网;

  • 管理海洋操作,包括船只通行。

教程:预测大浪

在本文的其余部分,我们将开发一个模型来预测即将到来的大浪。你将学习如何构建一个概率预测模型,该模型估计事件发生的可能性。

完整的代码可在 Github 上获得:

数据集

我们将使用一个来自爱尔兰海岸放置的智能浮标收集的实际数据集。除了其他信息,收集的数据包括显著波高,这是我们想要预测的变量。此数据在参考文献[2]的来源中提供。

下载数据后,你可以使用以下代码读取:

import pandas as pd

file = 'path_to_data/IrishSmartBuoy.csv'
# reading the data set
# skipping the second with skiprows
# parsing time column to datetime and setting it as index
data = pd.read_csv(file, skiprows=[1], parse_dates=['time'], index_col='time')
# defining the series and converting cm to meters
series = data['SignificantWaveHeight'] / 100
# resampling to hourly and taking the mean
series = series.resample('H').mean()

以下是数据的样子:

每小时显著波高时间序列。图片来源于作者

有几个时期的数据缺失。也许浮标正在维护。此外,还可以看到明显的季节性因素。波浪的高度在冬季通常较高。

时间序列的分布。图片来源于作者

数据分布显示出右偏态。重尾表示大浪,这对预测很重要。

关于如何定义大浪没有明确的共识。我们将其定义为至少 5 米高的波浪。这个阈值在上面的图中用黄色垂直线表示。

因此,目标是预测未来的波浪是否会超过 5 米。这个问题可以被框架化为超越概率预测任务。

超越概率预测概述

超越概率预测是预测时间序列在预定义的未来时间段内超越预定义阈值的概率的问题。

这个任务在极端值高度相关的领域是重要的。例如,在环境科学中,模型用于预测自然灾害如飓风或洪水的可能性。另一个例子是工程领域,专业人员使用模型来预测设备故障的可能性。

从机器学习的角度来看,主要挑战是缺乏关于超越事件的信息。根据定义,超越事件是稀有的。模型需要应对不平衡的类别分布,大多数观察值是非事件情况。

为什么用概率?

大浪的发生很难预测。因此,将预测结果以概率的形式呈现是有用的,以传达这些预测背后的不确定性和局限性。

一般来说,概率预测通过改善对每个可能行动相关风险的评估来帮助决策。

建立模型

我们可以使用自回归来处理这个任务。

目标是使用过去近期的海洋波高作为解释变量。目标是一个二元变量,表示是否会很快出现大浪。

from sklearn.model_selection import train_test_split

# https://github.com/vcerqueira/tsa4climate/tree/main/src
from src.tde import time_delay_embedding

# using past 24 observations as explanatory variables
N_LAGS = 24
# using the next 12 hours as the forecasting horizon
HORIZON = 12
# forecasting the probability of waves above 5 meters
THRESHOLD = 5

# leaving last 20% of observations for testing
train, test = train_test_split(series, test_size=0.2, shuffle=False)

# transforming time series into a tabular format for supervised learning
X_train, Y_train = time_delay_embedding(train, n_lags=N_LAGS, horizon=HORIZON, return_Xy=True)
X_test, Y_test = time_delay_embedding(test, n_lags=N_LAGS, horizon=HORIZON, return_Xy=True)

y_train = Y_train.apply(lambda x: (x > THRESHOLD).any(), axis=1).astype(int)
y_test = Y_test.apply(lambda x: (x > THRESHOLD).any(), axis=1).astype(int)

在这种情况下,我们将预测范围设置为 12 小时。因此,在每个时刻,我们的目标是预测在接下来的 12 小时内出现大浪的可能性。

目标变量的分布不平衡,大多数观察值指的是正常的波浪:

每个数据分区中按波浪类型(大/正常)的分布。图片作者提供。

由于目标变量是二元的,我们希望构建一个二元概率分类模型。对于这个案例研究,我们选择了随机森林。

from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score, roc_curve

model = RandomForestClassifier(max_depth=5)
model.fit(X_train, y_train)

probs = model.predict_proba(X_test)[:, 1]

roc_auc_score(y_test, probs)
fpr, tpr, thresholds = roc_curve(y_test, probs)

在训练模型后,我们可以使用.predict_proba方法获得概率预测。

二元概率预测可以使用 ROC(接收器操作特征)曲线来评估。其思想是绘制假阳性率(x 轴)与真正率(y 轴)在不同决策阈值下的关系。

这会生成如下的曲线:

预测模型的 ROC 曲线。图片作者提供。

曲线越接近左上角越好。对角线虚线是随机模型应该达到的水平。

ROC 曲线通常由其下方的面积(AUC)来总结。AUC 是用于评估二元概率分类器的指标,它量化了模型区分两类的能力。我们模型的 AUC 为 0.94,这是一个不错的分数。

关键要点

  • 海洋波浪是一个有前景的可再生能源来源;

  • 虽然这一来源比太阳能或风能更为稳定,但生产成本却阻碍了它的推广;

  • 大型海洋波浪对海事操作的安全构成了担忧,包括沿海灾害或船只的通行。此外,大波浪还可能损坏波浪能转换器;

  • 预测即将到来的大波浪是一项有用的任务。概率预测更为理想,因为这些提供了对决策至关重要的更多信息;

  • 预测模型的进步可以帮助加速海洋波浪能的采纳。

感谢阅读,下次故事见!

参考文献

[1] Drew, Benjamin, Andrew R. Plummer, and M. Necip Sahinkaya. “波浪能转换器技术综述。” (2009): 887–902。

[2] 爱尔兰波浪浮标(许可证:创意共享署名 4.0)

气候变化中的时间序列:风力发电预测

原文:towardsdatascience.com/time-series-for-climate-change-forecasting-wind-power-8ed6d653a255

如何利用时间序列分析和预测应对气候变化

Vitor CerqueiraTowards Data Science Vitor Cerqueira

·发布于 Towards Data Science ·阅读时间 6 分钟·2023 年 3 月 29 日

--

American Public Power Association 提供的照片,来源于 Unsplash

朝着清洁能源生产迈进

非可再生能源对我们的星球造成了沉重的生态足迹。这一问题促使了清洁能源的科学和技术进步,例如太阳能、风能和海洋波浪能。这些能源对环境友好,不像煤炭或石油。

延迟清洁能源广泛应用的原因之一是它们的不规则性。它们是高度可变的资源,这使得其行为难以预测。

因此,预测这些资源的条件是一个关键挑战。准确的预测对于高效生产清洁能源至关重要。

在本文中,我们将开发一个模型来预测风力发电。

风力发电

风力发电是日益普及的可再生能源之一。截至 2020 年,风力发电约占丹麦电力生产的 47%。其他国家也增加了电力网中的风力发电份额。

风力发电也有一些缺点。例如,风力涡轮机的视觉影响和噪音。此外,风力发电基础设施需要相当大的初期投资。

风力发电的电网整合也很困难。风力发电只能在风吹起时生成。这使得它成为一种间歇性和不可预测的能源。因此,它需要与其他替代能源配合使用。

风力突增

风力波动也是电力系统运营商面临的主要问题。这些是在短时间内(分钟到小时)风力发电的大幅变化。如果未及时检测,风力波动可能会影响电网的可靠性。

风力波动可以是向上或向下的变化。当发生突然的功率下降时,必须提高其他来源的能量来补偿损失。突然的上升变化可能促使操作员减少其他来源的输出,或者选择销售过剩的能源。

预测的作用

电力系统运营商依赖预测模型来预测风力条件。这些模型使运营商能够高效地平衡和整合多个能源来源。准确的预测对于电网的效率以及降低成本非常重要。

实践操作

在本文的其余部分,我们将构建一个预测风力发电的模型。目标是展示这个问题的挑战性以及未来的发展如何带来价值。

你可以在 Github 上找到这个项目的完整代码:

数据集

在这个教程中,我们将使用一个关于比利时风电场的公开数据集。

时间序列以 15 分钟为间隔从 2014 年到 2018 年收集。除了风力发电,我们还获取了关于已安装容量的信息(最大可能生成的电力):

风力发电时间序列(兆瓦),包括已安装的容量。数据来源见参考文献[1]。图片由作者提供。

随着新风机的加入,已安装容量随时间增加。因此,我们将风力发电量标准化为风电场的容量。这就形成了一种风力发电量占总容量百分比的度量。

风力发电占已安装容量的百分比。前四个月的样本。图片由作者提供。

构建预测模型

我们将使用机器学习算法构建一个预测模型。这个想法是应用一种叫做自回归的建模技术。自回归涉及使用最近的观察数据(滞后)来预测未来的观察数据。你可以在以前的文章中阅读更多关于自回归的内容。

首先,我们需要将时间序列转换为表格格式。这可以通过一种叫做时间延迟嵌入的滑动窗口方法完成:

from sklearn.model_selection import train_test_split

# src module here: https://github.com/vcerqueira/tsa4climate/tree/main/src
from src.tde import time_delay_embedding

# number of lags and forecasting horizon
N_LAGS, HORIZON = 24, 24

# leaving last 20% of observations for testing
train, test = train_test_split(series, test_size=0.2, shuffle=False)

# transforming time series into a tabular format for supervised learning
X_train, Y_train = time_delay_embedding(train, n_lags=N_LAGS, horizon=HORIZON, return_Xy=True)
X_test, Y_test = time_delay_embedding(test, n_lags=N_LAGS, horizon=HORIZON, return_Xy=True)

我们将滞后数和预测视野都设置为 24。在每个时间步骤中,我们希望模型使用过去 24 小时的数据来预测接下来 24 小时的波动高度。

这是训练解释变量和目标变量的样本:

第一和最后的解释变量与目标变量的样本

接下来,我们使用训练集选择一个模型。在本教程中,我们进行随机搜索以选择和优化回归算法。此外,我们还测试是否应包含特征提取。特定的特征提取过程基于滞后汇总统计

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import FunctionTransformer
from sklearn.model_selection import RandomizedSearchCV, TimeSeriesSplit

from src.model_selection import (MetaEstimator,
                                 search_space_with_feature_ext)

# Create a pipeline for hyperparameter optimization
# 'feature' contains different possibilities for feature extraction
# 'model' contains different regression algorithms and respective hyperparameters
pipeline = Pipeline([('feature', FunctionTransformer()),
                     ('model', MetaEstimator())])

# do random search optimization for model selection
search_mod = RandomizedSearchCV(estimator=pipeline,
                                param_distributions=search_space_with_feature_ext,
                                scoring='r2',
                                n_iter=30,
                                n_jobs=1,
                                refit=True,
                                verbose=2,
                                cv=TimeSeriesSplit(n_splits=3),
                                random_state=123)

search_mod.fit(X_train, Y_train)

print(search_mod.best_estimator_)
# Pipeline(steps=[('feature', FunctionTransformer()),
#                 ('model', RidgeCV(alphas=0.25))])

选择的模型是没有特征提取的岭回归。我们使用完整的训练集重新训练该模型,并在测试集上进行评估。

from sklearn.metrics import r2_score

# forecasting testing observations using the selected model
Y_hat_test = search_mod.predict(X_test)
Y_hat_test = pd.DataFrame(Y_hat_test, columns=Y_train.columns)

# evaluating the selected model over the forecasting horizon
r2_scores = {col: r2_score(y_true=Y_test[col], y_pred=Y_hat_test[col])
             for col in Y_hat_test}

结果揭示了两个主要挑战:

  • 在大范围内进行预测;

  • 预测极端观察值。

预测范围内的不确定性增加

众所周知,长期预测(即超过几个时间步)比短期预测更困难。风力预测也遵循这一趋势:

在预测范围内的预测性能(R²得分)。图片由作者提供。

预测下一小时的波浪功率是容易的。R2 得分约为 0.98——几乎完美。然而,当我们进行较长时间范围的预测时,性能会大幅下降。

长期预测(在这种情况下,超过几个小时)对有效平衡能源需求和供应非常重要。

预测极端值

显示实际值和预测值关系的散点图。图片由作者提供。

我们还需要模型在预测极端值时的准确性。在这种情况下,这些极端值代表高或低风力。这样的值很重要,因为它们可能对电网操作产生影响。

上图显示了一个预测值和实际值的散点图。红色虚线是理想情况,其中预测值与观察值相符。

大部分数据点围绕红线分布。但是,对于极端观察值,数据点偏离了红线。因此,模型有时未能预测极端值。

克服这两个局限性对于将风能整合到电力网中至关重要。

主要收获

  • 风力发电是越来越受欢迎的可再生能源;

  • 预测风力条件是估算来自此来源的能量的关键任务;

  • 这个预测任务可以通过基于自回归的模型来解决。

  • 预测风力具有两个挑战:低长期预测性能和极端观察值的低性能。

感谢阅读。这篇文章是关于气候变化时间序列系列帖子的第一篇。敬请关注更多内容!

参考文献

[1] 比利时风力发电风电场(许可证 CC BY 4.0

[2] Rolnick, David, 等. “利用机器学习应对气候变化。” ACM 计算机调查 (CSUR) 55.2 (2022): 1–96.

气候变化时间序列:起源-目的地需求预测

原文:towardsdatascience.com/time-series-for-climate-change-origin-destination-demand-forecasting-79c8fed6af89

挖掘浮动车数据以应对气候变化

Vitor CerqueiraTowards Data Science Vitor Cerqueira

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

--

图片来源: Denys NevozhaiUnsplash

这是《气候变化的时间序列》系列的第八部分。文章列表:

浮动车数据用于流动建模

挖掘浮动车数据是智能交通系统中的关键任务。浮动车数据指的是由配备 GPS 设备的车辆收集的数据,这些数据提供了关于车辆位置和速度的信息。

了解城市内部的流动模式是交通运输中的一项重要任务。例如,这有助于减少拥堵和整体交通活动。减少在交通中的时间意味着排放的温室气体减少。因此,准确的模型对气候变化有积极影响。

GPS 设备的普及产生了许多与移动性相关的数据集。但是,从 GPS 数据中学习是一个具有挑战性的问题。空间依赖性很复杂但非常重要。而且,还有时间依赖性,例如高峰时段。移动模式在工作日和非工作日也有所不同。

起始-目的地流量计数估计

旧金山的一次出租车行程。图片来源:作者

浮动车数据为移动建模提供了许多可能性。其中之一是起始-目的地(OD)流量计数问题。

OD 流量计数指的是在给定时间段内估计有多少辆车辆从一个子区域穿越到另一个子区域。这项任务有多个相关的原因。出租车公司可以根据特定区域的预期需求动态分配车队。

实操:预测旧金山的 OD 需求

在本文的其余部分,我们将预测旧金山的出租车乘客需求。我们将把这个问题作为 OD 流量计数任务来处理。

本教程中使用的完整代码可以在 Github 上找到:

数据集

我们将使用由美国加利福尼亚州旧金山的一家出租车车队收集的数据集。该数据集包含来自 536 辆出租车的 GPS 数据,时间跨度为 21 天。总共有 1.21 亿条 GPS 路径,分布在 464045 次行程中。有关更多细节,请参见参考文献 [1]。

数据集的样本。

在每个时间步长和每辆出租车上,我们都有其坐标信息和是否有乘客乘坐的信息。

问题定义

旧金山的一些出租车行程的结束位置。图片来源:作者。

我们的目标是根据起始点建模人们的去向。OD 流量计数估计可以分为四个子任务:

  1. 空间网格分解

  2. 起始-目的地对的选择

  3. 时间离散化

  4. 建模和预测

让我们逐个深入探讨每个问题。

空间网格分解

空间分解是 OD 流量计数估计的一个常见预处理步骤。其思想是将地图分割成网格单元,这些网格单元代表城市的一小部分。然后,我们可以统计每对网格单元之间有多少人穿越。

旧金山的两个示例网格单元。图片来源:作者。

在这个案例研究中,我们将城市地图分成 10000 个网格单元,如下所示:

import pandas as pd

from src.spatial import SpatialGridDecomposition, prune_coordinates

# reading the data set
trips_df = pd.read_csv('trips.csv', parse_dates=['time'])

# removing outliers from coordinates
trips_df = prune_coordinates(trips_df=trips_df, lhs_thr=0.01, rhs_thr=0.99)

# grid decomposition with 10000 cells
grid = SpatialGridDecomposition(n_cells=10000)
# setting bounding box
grid.set_bounding_box(lat=trips_df.latitude, lon=trips_df.longitude)
# grid decomposition
grid.grid_decomposition()

在上面的代码中,我们去除了异常地点。这些异常可能是由于 GPS 故障引起的。

获取最受欢迎的行程

在空间分解过程之后,我们获取了每次出租车行程的起点和终点,当它们被乘客占用时。

from src.spatial import ODFlowCounts

# getting origin and destination coordinates for each trip
df_group = trips_df.groupby(['cab', 'cab_trip_id'])
trip_points = df_group.apply(lambda x: ODFlowCounts.get_od_coordinates(x))
trip_points.reset_index(drop=True, inplace=True)

该想法是重建数据集,以包含以下信息:每次乘客旅行的起点、终点和起始时间戳。这些数据构成了我们的出发地-目的地 (OD) 流量计数模型的基础。

这些数据使我们能够计算从 A 单元格到 B 单元格的旅行次数:

# getting the origin and destination cell centroid
od_pairs = trip_points.apply(lambda x: ODFlowCounts.get_od_centroids(x, grid.centroid_df), axis=1)

为了简便起见,我们选择了旅行次数最多的前 50 个 OD 网格单元对。选择这个子集是可选的。然而,只有少量旅行的 OD 对在时间上会显示稀疏需求,这很难建模。此外,从车队管理的角度来看,需求低的旅行可能不太有用。

flow_count = od_pairs.value_counts().reset_index()
flow_count = flow_count.rename({0: 'count'}, axis=1)

top_od_pairs = flow_count.head(50)

时间离散化

在找到需求最高的 OD 对后,我们将其在时间上离散化。通过计算每小时每个给定顶级对的旅行次数来完成此操作。可以按如下方式进行:

# preparing data
trip_points = pd.concat([trip_points, od_pairs], axis=1)
trip_points = trip_points.sort_values('time_start')
trip_points.reset_index(drop=True, inplace=True)

# getting origin-destination cells for each trip, and origin start time
trip_starts = []
for i, pair in top_od_pairs.iterrows():

    origin_match = trip_points['origin'] == pair['origin']
    dest_match = trip_points['destination'] == pair['destination']

    od_trip_df = trip_points.loc[origin_match & dest_match, :]
    od_trip_df.loc[:, 'pair'] = i

    trip_starts.append(od_trip_df[['time_start', 'time_end', 'pair']])

trip_starts_df = pd.concat(trip_starts, axis=0).reset_index(drop=True)

# more data processing
od_count_series = {}
for pair, data in trip_starts_df.groupby('pair'):

    new_index = pd.date_range(
        start=data.time_start.values[0],
        end=data.time_end.values[-1],
        freq='H',
        tz='UTC'
    )

    od_trip_counts = pd.Series(0, index=new_index)
    for _, r in data.iterrows():
        dt = r['time_start'] - new_index
        dt_secs = dt.total_seconds()

        valid_idx = np.where(dt_secs >= 0)[0]
        idx = valid_idx[dt_secs[valid_idx].argmin()]

        od_trip_counts[new_index[idx]] += 1

    od_count_series[pair] = od_trip_counts.resample('H').mean()

od_df = pd.DataFrame(od_count_series)

这会产生一组时间序列,每个顶级 OD 对一个时间序列。以下是四个示例对的时间序列图:

四个示例出发地-目的地对的流量计数时间序列。图片由作者提供。

时间序列显示出日常季节性,这主要由高峰时段驱动。

预测

由时间离散化产生的时间序列集可用于预测。我们可以构建一个模型来预测相对于给定 OD 对的乘客旅行需求量。

下面是如何针对一个示例 OD 对进行操作的步骤:

from pmdarima.arima import auto_arima

# getting the first OD pair as example
series = od_df[0].dropna()

# fitting an ARIMA model
model = auto_arima(y=series, m=24)

上述内容,我们基于 ARIMA 构建了一个预测模型。该模型根据最近的需求预测下一小时的乘客需求。我们使用 ARIMA 方法是为了简便,但也可以使用其他方法,如深度学习

深入了解图神经网络

上述方法是一种简单而有效的解决 OD 流量计数问题的方法。但它将每个 OD 对视为一个独立的时间序列。

实际上,每对之间与相邻的 OD 对或周围道路相关联。因此,图神经网络在预测交通状况方面得到了越来越多的应用。道路网络被建模为图形,神经网络可以捕捉其中的复杂交互。你可以查看这个 Keras 示例来了解如何实现这种方法。

关键要点

  • 移动建模是智能交通系统中的一项重要任务;

  • OD 流量计数模型可以帮助减少城市交通,从而减少温室气体的排放;

  • 你可以通过基于空间分解和时间离散化的方法来解决 OD 流量计数问题。这将生成每个 OD 对的一组时间序列,可用于预测。

感谢阅读,下次故事见!

参考文献

[1] 旧金山,USA 的出租车移动轨迹数据集。(许可证 CC BY 4.0

[2] Moreira-Matias, Luís 等人。“使用高速 GPS 数据流进行时变 OD 矩阵估计。” 应用专家系统 44 (2016): 275–288。

气候变化的时间序列: 通过聚类减少食物浪费

原文:towardsdatascience.com/time-series-for-climate-change-reducing-food-waste-with-clustering-c2f067ffa907

使用时间序列聚类进行更好的需求预测

维托·塞尔奎拉数据科学的道路 维托·塞尔奎拉

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

--

图片由 卢克·迈克尔 提供,Unsplash

这是系列气候变化的时间序列的第七部分。文章列表:

  • 第一部分: 预测风力发电

  • 第二部分: 太阳辐射预测

  • 第三部分: 预测大洋波浪

  • 第四部分: 预测能源需求

  • 第五部分: 预测极端天气事件

  • 第六部分: 使用深度学习进行精准农业

减少食物浪费

改善供应链是减少我们生态足迹的另一个关键步骤。在发达国家,通常会有大量的消费品,如食品。这些过剩的物品需要大量的能源和资源,通常会被浪费。

减少过度生产是减少温室气体排放的重要里程碑。我们可以通过更好地了解需求来解决这个问题。

以食品为例。每年我们损失约 13 亿公吨食品[1]。当然,这并非所有都是剩余食品或与供应链有关的。一部分食品在生产或运输过程中丧失,例如由于冷藏条件差。然而,更好的需求预测模型可以对减少过度生产产生显著影响。

食品需求时间序列的聚类

图片由Diego Marín提供,来源于Unsplash

我们可以使用聚类分析来改进需求预测。

聚类涉及根据相似性对观察值进行分组。在这种情况下,每个观察值是代表某个产品销售的时间序列。一般而言,您可以使用时间序列聚类来:

  • 识别具有相似模式的时间序列,例如趋势或季节性;

  • 将时间序列划分为不同的组。这在时间序列数量较大时特别有用。

对于需求时间序列,聚类可以用来识别销售模式相似的产品。然后可以根据每个簇的特征量身定制预测模型。最终,这将带来更好的预测。

聚类需求时间序列对企业也很有价值。识别相似的产品有助于制定更好的营销或促销策略。

实践

在本文的其余部分,我们将对食品需求时间序列进行聚类分析。您将学习如何:

  • 使用特征提取总结一组时间序列;

  • 使用 K-Means 和层次方法进行时间序列聚类。

完整代码可在 Github 上获取:

数据集

我们将使用美国农业部收集的每周食品销售时间序列数据集。该数据集包含按产品类别和子类别划分的食品销售信息。时间序列按州划分,但我们将使用每个时期的全国总销售额。

下面是数据集的一个示例:

美国不同产品子类别的销售额(以百万美元计)

整体数据如下所示:

不同食品子类别的销售金额(百万美元)。图像由作者提供。

基于特征的时间序列聚类

我们将使用基于特征的时间序列聚类方法。这个过程包括两个主要步骤:

  1. 将每个时间序列总结为一组特征,例如平均值;

  2. 对特征集应用传统的聚类算法,例如 K-means。

我们逐步进行每个步骤。

使用tsfel进行特征提取

我们从提取一组统计数据开始,以总结每个时间序列。目标是将每个系列转换为一小组特征。

有几种时间序列特征提取工具。我们将使用tsfel,它相对于其他方法提供了竞争力的性能[3]。

这是如何使用tsfel的方法:

import pandas as pd
import tsfel

# get configuration
cfg = tsfel.get_features_by_domain()

# extract features for each food subcategory
features = {col: tsfel.time_series_features_extractor(cfg, data[col])
            for col in data}

features_df = pd.concat(features, axis=0)

这个过程会生成大量特征。其中一些可能是冗余的,因此我们进行特征选择过程。

下面,我们对特征集进行三项操作:

  • 归一化:将变量转换为 0–1 的值范围;

  • 方差选择:去除方差为 0 的变量;

  • 相关性选择:去除与其他现有变量有高相关性的变量。

from sklearn.preprocessing import MinMaxScaler
from sklearn.feature_selection import VarianceThreshold
from src.correlation_filter import correlation_filter

# normalizing the features
features_norm_df = pd.DataFrame(MinMaxScaler().fit_transform(features_df),
                                columns=features_df.columns)

# removing features with 0 variance
min_var = VarianceThreshold(threshold=0)
min_var.fit(features_norm_df)
features_norm_df = pd.DataFrame(min_var.transform(features_norm_df),
                                columns=min_var.get_feature_names_out())

# removing correlated features
features_norm_df = correlation_filter(features_norm_df, 0.9)
features_norm_df.index = data.columns

K 均值聚类

在预处理数据集后,我们准备对时间序列进行聚类。我们将每个序列总结为一小组无序特征。因此,我们可以使用任何传统的聚类算法。一个流行的选择是 K 均值。

使用 K 均值时,我们需要选择所需的聚类数量。除非我们有一些领域知识,否则这个参数没有明显的先验值。但是,我们可以采用数据驱动的方法来选择聚类数量。我们测试不同的值,并选择最佳值。

下面,我们测试最多 24 个聚类的 K 均值算法。然后,我们选择最大化轮廓系数的聚类数量。这个指标量化了获得的聚类的凝聚度。

from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

kmeans_parameters = {
    'init': 'k-means++',
    'n_init': 100,
    'max_iter': 50,
}

n_clusters = range(2, 25)
silhouette_coef = []
for k in n_clusters:
    kmeans = KMeans(n_clusters=k, **kmeans_parameters)
    kmeans.fit(features_norm_df)

    score = silhouette_score(features_norm_df, kmeans.labels_)

    silhouette_coef.append(score)

如下图所示,轮廓系数在 5 个聚类时最大。

最多 24 个聚类的轮廓系数。图片由作者提供。

我们可以绘制平行坐标图来理解每个聚类的特征。以下是一个包含三个特征的样本示例:

带有特征样本的平行坐标图。图片由作者提供。

我们还可以利用聚类的信息来改进需求预测模型。例如,通过为每个聚类构建一个模型。参考文献[5]中的论文是这种方法的一个很好的例子。

层次聚类

层次聚类是 K 均值的替代方法。它通过迭代地合并聚类对,形成类似树的结构。scipy库提供了这种方法的实现。

import scipy.cluster.hierarchy as shc

# hierarchical clustering using the ward method
clustering = shc.linkage(features_norm_df, method='ward')

# plotting the dendrogram
dend = shc.dendrogram(clustering,
                      labels=categories.values,
                      orientation='right',
                      leaf_font_size=7)

层次聚类模型的结果最好通过树状图进行可视化:

使用树状图可视化层次聚类的结果。图片由作者提供。

我们可以使用树状图来理解聚类的特征。例如,我们可以看到大多数罐装食品被分组(橙色)。橙子也与煎饼/蛋糕混合物聚类。这两者经常在早餐中一起出现。

关键要点

  • 发达国家有大量的食品资源盈余。这些盈余通常会被浪费。

  • 减少食物浪费可以通过减少温室气体排放对气候变化产生强烈影响;

  • 我们可以通过改进食品需求预测模型来实现这一点;

  • 对需求时间序列进行聚类可以改善涉及多个时间序列的预测模型。一种方法是为每个聚类训练一个模型。

  • 聚类也可以帮助理解数据集中不同群体的特征。

感谢阅读,下次故事见!

参考文献

[1] Jenny Gustavsson, Christel Cederberg, Ulf Sonesson, Robert Van Otterdijk, 和 Alexandre Meybeck. 2011. 全球食品损失和食品浪费. 联合国粮食及农业组织,罗马。

[2] 每周零售食品销售 由经济研究服务提供(许可证:公有领域)

[3] Henderson, Trent, 和 Ben D. Fulcher. “使用 theft 包的基于特征的时间序列分析。” arXiv 预印本 arXiv:2208.06146 (2022).

[4] Rolnick, David, 等. “用机器学习应对气候变化。” ACM Computing Surveys (CSUR) 55.2 (2022): 1–96.

[5] Kasun Bandara, Christoph Bergmeir, 和 Slawek Smyl. 利用递归神经网络对相似系列的时间序列数据库进行预测:一种聚类方法. 专家系统应用, 140:112896, 2020.

气候变化的时间序列:太阳辐射预测

原文:towardsdatascience.com/time-series-for-climate-change-solar-irradiance-forecasting-a972dac7418f

如何利用时间序列分析和预测应对气候变化

Vitor CerqueiraTowards Data Science Vitor Cerqueira

·发布于 Towards Data Science ·8 分钟阅读·2023 年 4 月 5 日

--

图片由 Andrey Grinkevich 提供,来源于 Unsplash

这是系列文章 气候变化的时间序列 的第二部分。文章列表:

  • 第一部分: 预测风能

太阳能系统

太阳能是一种越来越普及的清洁能源来源。

太阳光通过光伏设备转化为电能。由于这些设备不产生污染物,因此被认为是清洁能源的来源。除了环境效益外,太阳能因其低成本而具有吸引力。初期投资较大,但长期低成本是值得的。

生产的能源量取决于太阳辐射水平。然而,太阳条件可能会迅速变化。例如,云层可能会突然遮住太阳,降低光伏设备的效率。

因此,太阳能系统依赖于预测模型来预测太阳条件。像 风能预测的情况 一样,准确的预测直接影响这些系统的有效性。

超越能源生产

预测太阳辐射除了用于能源外,还有其他应用,例如:

  • 农业:农民可以利用预测来优化作物生产。例如,估算何时种植或收获作物,或优化灌溉系统;

  • 土木工程:预测太阳辐射对设计和建造建筑物也很有价值。预测可以用于最大化太阳辐射,从而减少取暖/制冷成本。预测还可以用于配置空调系统,这有助于建筑内能源的高效利用。

挑战及下一步

尽管其重要性,太阳条件高度可变且难以预测。这些条件依赖于几个气象因素,而这些信息有时无法获得。

在本文的其余部分,我们将开发一个太阳辐射预测模型。除此之外,你将学会如何:

  • 可视化多变量时间序列;

  • 转换多变量时间序列以进行监督学习;

  • 根据相关性和重要性评分进行特征选择。

教程:预测太阳辐射

本教程基于由美国农业部收集的数据集。你可以在参考文献[1]中查看更多详细信息。本教程的完整代码可在 Github 上获取:

数据是一个多变量时间序列:在每个时刻,观测值由多个变量组成。这些变量包括以下天气和水文变量:

  • 太阳辐射(每平方米瓦特);

  • 风向;

  • 积雪深度;

  • 风速;

  • 露点温度;

  • 降水量;

  • 蒸汽压力;

  • 相对湿度;

  • 气温。

该系列从 2007 年 10 月 1 日到 2013 年 10 月 1 日,按小时收集,总计 52,608 次观测。

下载数据后,我们可以使用 pandas 读取它:

import re
import pandas as pd
# src module available here: https://github.com/vcerqueira/tsa4climate/tree/main/src
from src.log import LogTransformation

# a sample here: https://github.com/vcerqueira/tsa4climate/tree/main/content/part_2/assets
assets = 'path_to_data_directory'

DATE_TIME_COLS = ['month', 'day', 'calendar_year', 'hour']
# we'll focus on the data collected at particular station called smf1
STATION = 'smf1'

COLUMNS_PER_FILE = \
    {'incoming_solar_final.csv': DATE_TIME_COLS + [f'{STATION}_sin_w/m2'],
     'wind_dir_raw.csv': DATE_TIME_COLS + [f'{STATION}_wd_deg'],
     'snow_depth_final.csv': DATE_TIME_COLS + [f'{STATION}_sd_mm'],
     'wind_speed_final.csv': DATE_TIME_COLS + [f'{STATION}_ws_m/s'],
     'dewpoint_final.csv': DATE_TIME_COLS + [f'{STATION}_dpt_C'],
     'precipitation_final.csv': DATE_TIME_COLS + [f'{STATION}_ppt_mm'],
     'vapor_pressure.csv': DATE_TIME_COLS + [f'{STATION}_vp_Pa'],
     'relative_humidity_final.csv': DATE_TIME_COLS + [f'{STATION}_rh'],
     'air_temp_final.csv': DATE_TIME_COLS + [f'{STATION}_ta_C'],
     }

data_series = {}
for file in COLUMNS_PER_FILE:
    file_data = pd.read_csv(f'{assets}/{file}')

    var_df = file_data[COLUMNS_PER_FILE[file]]

    var_df['datetime'] = \
        pd.to_datetime([f'{year}/{month}/{day} {hour}:00'
                        for year, month, day, hour in zip(var_df['calendar_year'],
                                                          var_df['month'],
                                                          var_df['day'],
                                                          var_df['hour'])])

    var_df = var_df.drop(DATE_TIME_COLS, axis=1)
    var_df = var_df.set_index('datetime')
    series = var_df.iloc[:, 0].sort_index()

    data_series[file] = series

mv_series = pd.concat(data_series, axis=1)
mv_series.columns = [re.sub('_final.csv|_raw.csv|.csv', '', x) for x in mv_series.columns]
mv_series.columns = [re.sub('_', ' ', x) for x in mv_series.columns]
mv_series.columns = [x.title() for x in mv_series.columns]

mv_series = mv_series.astype(float)

这段代码生成了以下数据集:

多变量时间序列样本

探索性数据分析

对数尺度的多变量时间序列图。为了可视化,系列被重新采样为每日频率。这是通过取每日平均值来完成的。图片由作者提供。

系列图表明有强烈的年度季节性。辐射水平在夏季达到峰值,其他变量也显示出类似的模式。除了季节性波动外,时间序列的水平在时间上是稳定的。

我们还可以单独可视化太阳辐射变量:

每日总太阳辐射。图片由作者提供。

除了明显的季节性,我们还可以发现一些围绕系列水平的下降峰值。这些情况需要及时预测,以便高效利用备用能源系统。

我们还可以分析每对变量之间的相关性:

显示成对相关性的热图。图片由作者提供。

太阳辐射与一些变量相关。例如,气温、相对湿度(负相关)或风速。

我们已经探讨了如何使用单变量时间序列在上一篇文章中建立预测模型。然而,相关性热图表明,将这些变量纳入模型可能会很有价值。

我们该如何做到这一点?

自回归分布滞后建模入门

自回归分布滞后(ARDL)是一种用于多变量时间序列的建模技术。

ARDL 是一种识别多个变量随时间关系的有用方法。它通过将自回归技术扩展到多变量数据来工作。系列中给定变量的未来值是基于其滞后值和其他变量的滞后值来建模的。

在这种情况下,我们希望基于多个因素的滞后预测太阳辐射,如气温或蒸汽压。

将数据转换为 ARDL 格式

应用 ARDL 方法涉及将时间序列转换为表格格式。这是通过应用时间延迟嵌入到每个变量,然后将结果连接成一个矩阵来完成的。可以使用以下函数来实现:

import pandas as pd

def mts_to_tabular(data: pd.DataFrame,
                   n_lags: int,
                   horizon: int,
                   return_Xy: bool = False,
                   drop_na: bool = True):
    """
    Time delay embedding with multivariate time series
    Time series for supervised learning

    :param data: multivariate time series as pd.DataFrame
    :param n_lags: number of past values to used as explanatory variables
    :param horizon: how many values to forecast
    :param return_Xy: whether to return the lags split from future observations

    :return: pd.DataFrame with reconstructed time series
    """

    # applying time delay embedding to each variable
    data_list = [time_delay_embedding(data[col], n_lags, horizon)
                 for col in data]

    # concatenating the results in a single dataframe
    df = pd.concat(data_list, axis=1)

    if drop_na:
        df = df.dropna()

    if not return_Xy:
        return df

    is_future = df.columns.str.contains('\+')

    X = df.iloc[:, ~is_future]
    Y = df.iloc[:, is_future]

    if Y.shape[1] == 1:
        Y = Y.iloc[:, 0]

    return X, Y

这个函数应用于数据如下:

from sklearn.model_selection import train_test_split

# target variable
TARGET = 'Solar Irradiance'
# number of lags for each variable
N_LAGS = 24
# forecasting horizon for solar irradiance
HORIZON = 48

# leaving the last 30% of observations for testing
train, test = train_test_split(mv_series, test_size=0.3, shuffle=False)

# transforming the time series into a tabular format
X_train, Y_train_all = mts_to_tabular(train, N_LAGS, HORIZON, return_Xy=True)
X_test, Y_test_all = mts_to_tabular(train, N_LAGS, HORIZON, return_Xy=True)

# subsetting the target variable
target_columns = Y_train_all.columns.str.contains(TARGET)
Y_train = Y_train_all.iloc[:, target_columns]
Y_test = Y_test_all.iloc[:, target_columns]

我们将预测视野设置为 48 小时。提前预测多个步骤对有效整合多种能源来源到电网中非常有价值。

很难事先确定应该包含多少个滞后。因此,每个变量的值设为 24。这导致总共有 216 个基于滞后的特征。

建立预测模型

在构建模型之前,我们基于日期和时间提取了 8 个额外特征。这些包括年份中的天数或小时等数据,这些数据对建模季节性很有用。

我们通过特征选择减少了解释变量的数量。首先,我们应用相关性过滤器,用于移除与任何其他解释变量相关性大于 95%的特征。然后,我们还基于随机森林的重要性分数应用递归特征消除(RFE)。在特征工程之后,我们使用随机森林训练模型。

我们利用 sklearn 的PipelineRandomSearchCV来优化不同步骤的参数:

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import FunctionTransformer
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import RandomizedSearchCV
from sktime.transformations.series.date import DateTimeFeatures

from src.holdout import Holdout

# including datetime information to model seasonality
hourly_feats = DateTimeFeatures(ts_freq='H',
                                keep_original_columns=True,
                                feature_scope='efficient')

# building a pipeline
pipeline = Pipeline([
    # feature extraction based on datetime
    ('extraction', hourly_feats),
    # removing correlated explanatory variables
    ('correlation_filter', FunctionTransformer(func=correlation_filter)),
    # applying feature selection based on recursive feature elimination
    ('select', RFE(estimator=RandomForestRegressor(max_depth=5), step=3)),
    # building a random forest model for forecasting
    ('model', RandomForestRegressor())]
)

# parameter grid for optimization
param_grid = {
    'extraction': ['passthrough', hourly_feats],
    'select__n_features_to_select': np.linspace(start=.1, stop=1, num=10),
    'model__n_estimators': [100, 200]
}

# optimizing the pipeline with random search
model = RandomizedSearchCV(estimator=pipeline,
                           param_distributions=param_grid,
                           scoring='neg_mean_squared_error',
                           n_iter=25,
                           n_jobs=5,
                           refit=True,
                           verbose=2,
                           cv=Holdout(n=X_train.shape[0]),
                           random_state=123)

# running random search
model.fit(X_train, Y_train)

# checking the selected model
model.best_estimator_
# Pipeline(steps=[('extraction',
#                  DateTimeFeatures(feature_scope='efficient', ts_freq='H')),
#                 ('correlation_filter',
#                  FunctionTransformer(func=<function correlation_filter at 0x28cccfb50>)),
#                 ('select',
#                  RFE(estimator=RandomForestRegressor(max_depth=5),
#                      n_features_to_select=0.9, step=3)),
#                 ('model', RandomForestRegressor(n_estimators=200))])

评估模型

我们使用随机搜索选择了一个模型,并结合了a validation split。现在,我们可以评估其在测试集上的预测性能。

# getting forecasts for the test set
forecasts = model.predict(X_test)
forecasts = pd.DataFrame(forecasts, columns=Y_test.columns)

选定的模型仅保留了原始 224 个解释变量中的 65 个。以下是前 20 个特征的重要性:

模型中每个特征的重要性。作者提供的图像。

特征中的一天小时和一年中的一天是排名前四的特征。这一结果突出了数据中季节性效应的强度。除了这些,一些变量的初始滞后期对模型也有用。

主要收获

  • 太阳能是一种相关的清洁能源,依赖于太阳辐射;

  • 预测太阳辐射是有效整合太阳能到电网中的一个重要方面;

  • 太阳辐射依赖于许多变量。这些变量难以建模或可能无法轻易获得;

  • 当这些变量可用时,它们表示一个多变量时间序列。这种类型的数据可以用 ARDL 技术建模;

  • 你可以通过特征选择过程来估计每个变量的滞后期数量。

感谢阅读,下次故事见!

参考文献

[1] 来自美国爱达荷州西南部四个以西部刺柏为主的实验流域的天气、雪和径流数据。 (许可证:美国公共领域)

[2] Rolnick, David, 等. “利用机器学习应对气候变化。” ACM 计算调查 (CSUR) 55.2 (2022): 1–96。

气候变化中的时间序列:使用深度学习进行精准农业

原文:towardsdatascience.com/time-series-for-climate-change-using-deep-learning-for-precision-agriculture-806878cab9c

如何使用时间序列分析和预测来应对气候变化

维托尔·塞尔奎拉数据科学前沿 维托尔·塞尔奎拉

·发布于 数据科学前沿 ·阅读时间 6 分钟·2023 年 5 月 29 日

--

图片来源: IrewoledeUnsplash

这是 气候变化中的时间序列 系列的第六部分。文章列表:

精准农业

精准农业旨在改进农业管理。以优化生产,同时节省资源并减少环境影响。

有多种技术致力于提高农业的可持续性。例如:

  • 智能灌溉系统:使用传感器优化灌溉过程;

  • 精准种植系统:优化种植过程,如种子间距;

  • 作物产量预测,帮助农民决定每个季节种植什么作物。

露点温度

图片来源: 西蒙·库兹涅佐夫Unsplash

在灌溉系统的情况下,过少的灌溉会增加植物的压力并降低作物产量。但过多的灌溉会导致过多的湿气,从而滋生害虫。一个最佳的灌溉过程可以节省大量水资源并保护其他资源。

影响灌溉的一个指标是露点温度。露点温度指示空气中的水分含量。这反过来影响灌溉过程。因此,预测露点温度有助于支持水资源规划。

露点温度的相关性延伸到水文学、气候学和农业等领域。例如,如果预测显示出过多湿气的可能性,可以主动采取害虫控制措施。预测还可以用来预见霜冻,这对植物有害。在这种情况下,农民可以采取预防措施来保护作物。

预测露点温度在能源管理方面也很重要。在高露点温度下,人们倾向于使用空调系统来应对高相对湿度水平。预测这些条件可以用于预测能源需求的增加。这有助于提高电网的效率。

实操:使用深度学习进行露点温度的时空预测

在本文的其余部分,我们将预测多个地点的露点温度。你将学习如何使用深度学习构建时空预测模型。

本教程的完整代码可以在 Github 上找到:

时空预测基础

时空数据是多变量时间序列的一个特例。这些数据集涉及在多个地点观察一个变量,例如露点温度。

这种类型的数据包含了时间依赖性和空间依赖性。特定地点收集的数据点与其滞后值以及附近地点的当前和过去滞后值相关。建模这两种依赖性对于每个地点获得更好的预测可能是重要的。

时空预测通常使用诸如 VAR(向量自回归)或 STAR(时空自回归)等技术。我们将使用 VAR 方法结合深度神经网络。

数据集

我们将使用由美国农业部收集的实际数据集。更多细节请参考文献[1]。该数据集包含了露点温度的信息,这一变量在 6 个附近的站点中被记录。

下载数据后,你可以使用以下代码进行读取:

import pandas as pd

DATE_TIME_COLS = ['month', 'day', 'calendar_year', 'hour', 'water_year']

# reading the data set
data = pd.read_csv(filepath)

# parsing the datetime column
data['datetime'] = \
    pd.to_datetime([f'{year}/{month}/{day} {hour}:00'
                    for year, month, day, hour in zip(data['calendar_year'],
                                                      data['month'],
                                                      data['day'],
                                                      data['hour'])])

data = data.drop(DATE_TIME_COLS, axis=1).set_index('datetime')
data.columns = data.columns.str.replace('_dpt_C', '')

数据的样貌如下:

不同地点的时间序列似乎是相关的。

VAR — 准备数据以进行监督学习

我们将使用 VAR 方法来准备数据以训练深度神经网络。VAR 方法旨在捕捉不同变量之间的时间依赖性。在这种情况下,变量代表在 6 个位置收集的露点温度。

我们可以通过使用滑动窗口将每个变量转换为矩阵格式,然后将结果合并来实现这一点。你可以查看a previous article获取更多关于这一过程的细节。

from sklearn.model_selection import train_test_split

from src.tde import transform_mv_series

N_LAGS, HORIZON = 12, 12

# number of stations
N_STATIONS = data.shape[1]

# leaving last 20% of observations for testing
train, test = train_test_split(data, test_size=0.2, shuffle=False)

# computing the average of each series in the training set
mean_by_location = train.mean()

# mean-scaling: dividing each series by its mean value
train_scaled = train / mean_by_location
test_scaled = test / mean_by_location

# transforming the data for supervised learning
X_train, Y_train = transform_mv_series(train_scaled, n_lags=N_LAGS, horizon=HORIZON)
X_test, Y_test = transform_mv_series(test_scaled, n_lags=N_LAGS, horizon=HORIZON)

然后,我们基于 keras 构建一个堆叠 LSTM 模型。LSTM(长短期记忆)是一种特殊的递归神经网络,可以捕捉时间依赖性。

from keras.models import Sequential
from keras.layers import Dense, Dropout, LSTM, RepeatVector, TimeDistributed

model = Sequential()
model.add(LSTM(64, activation='relu', input_shape=(N_LAGS, N_STATIONS)))
model.add(Dropout(.2))
model.add(RepeatVector(HORIZON))
model.add(LSTM(32, activation='relu', return_sequences=True))
model.add(Dropout(.2))
model.add(TimeDistributed(Dense(N_STATIONS)))

model.compile(optimizer='adam', loss='mse')

定义和编译模型后,我们可以如下训练它:

from keras.callbacks import ModelCheckpoint

# model checkpoint for saving the best model during training
model_checkpoint = ModelCheckpoint(
    filepath='best_model_weights.h5',
    save_weights_only=True,
    monitor='val_loss',
    mode='min',
    save_best_only=True)

# fitting the model
history = model.fit(X_train, Y_train,
                    epochs=25,
                    validation_split=0.2,
                    callbacks=[model_checkpoint])

训练后,我们可以加载由模型检查点回调保存的最佳权重:

# The best model weights are loaded into the model after training
model.load_weights('best_model_weights.h5')

# predictions on the test set
preds = model.predict_on_batch(X_test)

扩展模型

我们可以通过不同方式扩展这个模型。例如,包含其他气象信息作为解释变量。像露点温度这样的气象数据受到各种因素的影响。它们的包含可能是更好预测性能的关键。

测试其他神经网络配置也可能是有益的。我们应用了堆叠 LSTM,但其他方法也显示出有前景的预测性能。例如 N-BEATS、DeepAR 或 ES-RNN。

我们还可以包括空间信息,例如地理坐标。这样,模型可以改善不同位置之间空间依赖性的建模。

关键要点

  • 精准农业旨在优化农业过程。这种优化可以减少农业对环境的影响;

  • 露点温度是水文学中的一个关键指标。预测这个变量对各种活动都是有益的,例如灌溉;

  • 气象变量通常在几个站点中捕获,这导致了一个时空数据集;

  • 深度学习可以用于构建一个时空预测模型。我们使用包含来自六个不同位置的露点温度的数据集训练了一个 LSTM 神经网络;

  • 这个模型可以通过包括额外的解释变量、空间信息或更改网络架构来扩展。

感谢阅读,下一个故事见!

参考文献

[1] 来自美国爱达荷州西南部四个以西部刺柏为主的实验流域的天气、降雪和径流数据。(许可证:美国公共领域)

[2] Arikan, Bugrayhan Bickici 等。“北达科他州的露点时间序列预测。” 知识基础工程与科学 2.2 (2021): 24–34。

[3] Liakos, Konstantinos G., 等人。“农业中的机器学习:综述。” 传感器 18.8 (2018): 2674.

时间序列预测:深度学习与统计学——谁能赢?

原文:towardsdatascience.com/time-series-forecasting-deep-learning-vs-statistics-who-wins-c568389d02df

关于最终难题的全面指南

Nikos KafritsasTowards Data Science Nikos Kafritsas

·发表于Towards Data Science ·14 分钟阅读·2023 年 4 月 5 日

--

使用稳定扩散创建[1]

近年来,深度学习在 NLP 领域取得了显著进展。

时间序列,本质上也具有顺序性,提出了一个问题:如果我们将预训练变压器的全部能力应用于时间序列预测会发生什么?

然而,一些论文,如[2]和[3],对深度学习模型进行了详细审查。这些论文并没有呈现出完整的画面。即使在 NLP 案例中,有些人将 GPT 模型的突破归因于“更多的数据和计算能力”而不是“更好的 ML 研究”。

本文旨在澄清混淆并提供客观观点,使用来自学术界和行业的可靠数据和来源。具体内容包括:

  • 深度学习统计模型的优缺点。

  • 何时使用统计模型,何时使用深度学习。

  • 如何处理预测案例。

  • 如何通过选择适合你情况和数据集的最佳模型来节省时间和金钱。

让我们深入探讨。

我刚刚发布了AI Horizon Forecast这是一个专注于时间序列和创新 AI 研究的新闻简报。订阅这里以拓宽视野!

[## AutoGluon-TimeSeries:创建强大的集成预测 - 完整教程

亚马逊的时间序列预测框架应有尽有。

aihorizonforecast.substack.com](https://aihorizonforecast.substack.com/p/autogluon-timeseries-creating-powerful?source=post_page-----c568389d02df--------------------------------)

Makridakis 等人的论文[4]

我们不能讨论不同预测模型的进展,而不考虑从Makridakis 竞赛(M 竞赛)中获得的见解。

Makridakis 竞赛是一系列大规模挑战,展示了时间序列预测的最新进展。

最近,Makridakis 等人发表了一篇新论文,内容如下:

  • 总结了前五次 M 竞赛中的预测现状。

  • 提供了各种统计、机器学习和深度学习预测模型的广泛基准。

注: 我们将在本文后面讨论论文的局限性。

基准设置

传统上,Makridakis 及其同事会发布总结最后一次 M 竞赛结果的论文。

然而,作者首次在实验中引入了深度学习模型。为什么?

与 NLP 不同,直到 2018–2019 年,首批 DL 预测模型才足够成熟以挑战传统预测模型。实际上,在 2018 年的 M4 竞赛中,ML/DL 模型排名最后。

图 1: 2018 年 Makridakis 等人检查的八种统计和十种 ML 预测方法的预测准确率(sMAPE)。所有 ML 方法都排在最后。

现在,让我们来看看在论文中使用的 DL/ML 模型:

  • 多层感知机(MLP): 我们熟悉的前馈网络。

  • WaveNet: 一种自回归神经网络,结合了卷积层(2016 年)。

  • Transformer: 原始 Transformer,介绍于 2017 年。

  • DeepAR 亚马逊首个成功的自回归网络,结合了 LSTMs(2017 年)

注: 那项研究的深度学习模型已不再是 SOTA(最先进的技术)(更多内容请稍后)。另外,MLP 被认为是 ML 模型而不是“深度”模型。

基准的统计模型是ARIMAETS(指数平滑)—— 这些是广为人知且经过实践验证的模型。

此外:

  • ML/DL 模型首先通过超参数调优进行了微调。

  • 统计模型是以逐系列的方式训练的。相反,DL 模型是全球性的(一个模型训练于数据集的所有时间序列)。因此,它们利用了跨学习

  • 作者使用了集成方法:从深度学习模型创建了一个Ensemble-DL模型,另一个Ensemble-S由统计模型组成。集成方法是预测的中位数。

  • Ensemble-DL由 200 个模型组成,每个类别有 50 个模型:DeepAR、Transformer、WaveNet 和 MLP。

  • 研究使用了 M3 数据集:首先,作者测试了 1,045 个时间序列,然后是完整数据集(3,003 个序列)。

  • 作者使用 MASE均值绝对尺度误差)和 SMAPE均值绝对百分比误差)来测量预测准确性。这些误差度量常用于预测。

接下来,我们提供了从基准测试中获得的结果和结论的总结。

1. 深度学习模型更好

作者总结道,平均而言,DL 模型优于统计模型。结果显示在 图 2 中:

图 2: 所有模型的平均排名及 95% 置信区间,使用 sMAPE 进行排名。

Ensemble-DL 模型显然优于 Ensemble-S。此外,DeepAR 的结果与 Ensemble-S 非常相似。

有趣的是,图 2 显示虽然 Ensemble-DL 优于 Ensemble-S,但只有 DeepAR 击败了单独的统计模型。这是为什么呢?

我们将在文章后面回答这个问题。

2. 但是,深度学习模型是昂贵的

深度学习模型需要大量的训练时间(和资金)。这是预期之中的。结果显示在 图 3 中:

图 3: SMAPE 与计算时间的关系。ln(CT) 为零对应大约 1 分钟的计算时间,而 ln(CT) 为 2、4、6、8 和 10 分别对应大约 7 分钟、1 小时、7 小时、2 天和 15 天的计算时间。

计算差异是显著的。

因此,降低 10% 的预测误差需要额外大约 15 天的计算时间 (Ensemble-DL)。虽然这个数字看起来很庞大,但有一些因素需要考虑:

  1. 作者没有说明他们使用了什么类型的硬件。

  2. 他们也没有提到是否使用了任何并行化或训练优化。

  3. 如果在集成中使用较少的模型,Ensemble-DL 的计算时间可以显著减少。这在 图 4 中显示了:

图 4: Ensemble-DL 模型中模型数量与 SMAPE 的关系。

我之前提到过,Ensemble-DL 模型是 200 个 DL 模型的集成。

图 4 显示,75 个模型可以以只有 200 个模型三分之一的计算成本实现相当的准确性。如果使用更聪明的集成方法,这个数字可以进一步减少。

最后,当前论文并未探讨深度学习模型的迁移学习能力。我们将在后面讨论这一点。

3. 集成就是你需要的一切

集成的威力是不容置疑的 (图 2图 3)。

Ensemble-DLEnsemble-SL 都是表现最好的模型。其理念是,每个单独的模型在捕捉不同的时间动态方面都表现出色。结合它们的预测可以识别复杂的模式和进行准确的外推。

4. 短期预测与长期预测

作者调查了模型在短期和长期预测能力上的差异。

确实如此。

图 5展示了每个模型在每个预测期的准确性。例如,第 1 列显示了一步预测误差。类似地,第 18 列显示了第 18 步预测的误差。

图 5: 1045 个系列中每个模型的 sMAPE 误差——数值越低越好(点击这里查看完整图像

这里有三个关键观察点:

  • 首先,长期预测的准确性低于短期预测(这并不意外)。

  • 在前 4 个预测期内,统计模型胜出。 在此之后,深度学习模型开始变得更好,Ensemble-DL 胜出。

  • 具体来说,在第一个预测期内,Ensemble-S的准确率高出 8.1%。然而,在最后一个预测期,Ensemble-DL的准确率高出 8.5%。

如果你考虑这个问题,就会明白:

  • 统计模型是自回归的。随着预测期的增加,误差会累积。

  • 相对而言,深度学习模型是多输出模型。因此,它们的预测误差分布在整个预测序列中。

  • 唯一的深度学习自回归模型是 DeepAR。这就是为什么 DeepAR 在前几个预测期表现非常好,而其他深度学习模型表现相对较差的原因。

5. 深度学习模型在更多数据下是否有所改善?

在之前的实验中,作者只使用了 M3 数据集中的 1,045 个时间序列。

接下来,作者使用完整的数据集(3,003 个系列)重新进行了实验。他们还分析了每个预测期的预测损失。结果如图 6所示:

图 6: 3003 个系列中每个模型的 sMAPE 误差——数值越低越好(点击这里查看完整图像

现在,Ensemble-DLEnsemble-S之间的差距缩小了。统计模型在第一个预测期与深度学习模型相当,但之后,Ensemble-DL 超越了它们。

让我们进一步分析Ensemble-DLEnsemble-S之间的差异:

图 7: Ensemble-DL 相对于 Ensemble-S 的百分比改善(点击这里查看完整图像

随着预测步数的增加,深度学习模型优于统计集合模型。

6. 趋势和季节性分析

最后,作者调查了统计模型和深度学习模型如何处理重要的时间序列特征,如趋势和季节性。

为此,作者使用了[5]中的方法论。具体来说,他们拟合了一个多元线性回归模型,将 sMAPE 误差与 5 个关键时间序列特征相关联:预测能力(错误的随机性趋势季节性线性稳定性(决定数据正态性的最佳 Box-Cox 参数变换。结果如图 8所示:

图 8: 不同指标的线性回归系数 0。数值越低越好。

我们观察到:

  • 深度学习模型在嘈杂、趋势化非线性数据上表现更佳。

  • 统计模型更适用于季节性低方差的数据,以及线性关系的数据。

这些见解是非常宝贵的。

因此,在选择适合您的用例的模型之前,进行广泛的探索性数据分析(EDA)并了解数据的性质是至关重要的。

研究的局限性

这篇论文无疑是对当前时间序列预测领域状态的最佳研究之一,但它也存在一些局限性。让我们来看看这些局限性:

缺乏 ML 算法:树 / Boosted Trees

Boosted Trees 模型家族在时间序列预测问题中占有重要地位。

最受欢迎的模型有 XGBoost、LightGBM 和 CatBoost。此外,LightGBM 赢得了 M5 竞赛。

这些模型在表格型数据中表现出色。事实上,到今天为止,Boosted Trees 仍然是表格数据的最佳选择。然而,本文使用的 M3 数据集非常简单,因为它主要包含单变量序列。

在未来的研究中,将 Boosted Trees 添加到数据集中,特别是对于更复杂的数据集,将是一个很好的主意。

选择 M3 作为基准数据集

IJF 期刊主编 Rob Hyndman 教授说:“自 2000 年以来,M3 数据集一直用于测试预测方法;新提出的方法必须超越 M3 才能在 IJF 上发表。

然而,按现代标准,M3 数据集被认为是小型和简单的,因此不具备现代预测应用和实际场景的代表性。

当然,数据集的选择并不会减少研究的价值。然而,进行一次使用更大数据集的未来基准测试可能会提供有价值的见解。

深度学习模型还不是最先进的(SOTA)。

现在,是时候处理眼前的主要问题了。

该研究中的深度学习模型远未达到最先进水平。

该研究将亚马逊的 DeepAR 识别为在理论预测准确性方面最好的深度学习模型。因此,DeepAR 是唯一一个能够在单独层面上超越统计模型的模型。然而,DeepAR 模型现在已经超过 6 年了。

亚马逊随后发布了其改进版的 DeepAR,称为 Deep GPVAR。实际上,Deep GPVAR 也已过时——亚马逊最新的深度预测模型是 MQTransformer,它于 2020 年发布。

此外,其他强大的模型,如时序融合变换器(TFT)和N-BEATS(最近被 N-HITS 超越),在深度学习集成中也没有被使用。

因此,研究中使用的深度学习模型至少落后于当前技术水平的两代。不容置疑,当前一代深度预测模型将产生更好的结果。

预测并不是一切

准确性在预测中至关重要,但并不是唯一重要的因素。其他关键领域包括:

  • 不确定性量化

  • 预测可解释性

  • 零-shot 学习 / 元学习

  • 政策转变隔离

说到零-shot 学习,它是 AI 中最有前途的领域之一。

零-shot 学习是模型在没有专门训练的情况下,正确估计未见数据的能力。这种学习方法更好地反映了人类的认知。

所有的深度学习模型,包括 GPT 模型,都是基于这一原则的。

利用这一原则的首批广受好评的预测模型是N-BEATS / N-HITS。这些模型可以在庞大的时间序列数据集上进行训练,并在完全新的数据上进行预测,其准确性与模型专门训练过的数据相当。

零-shot 学习只是元学习的一个特定实例。自此以来,在时间序列上的元学习取得了进一步的进展。以M6 竞赛为例,其目标是检验数据科学预测与计量经济学是否能像传奇投资者(如沃伦·巴菲特)一样击败市场。获胜解决方案是一种新型架构,其中使用了神经网络和元学习

不幸的是,本研究并未探索深度学习模型在零-shot 学习设置中的竞争优势。

Nixtla 研究

Nixtla,一家在时间序列预测领域有前景的初创公司,最近发布了对 Makridakis 等人论文[4]的基准后续研究。

具体来说,Nixtla 团队添加了 2 个额外的模型:复杂指数平滑动态优化 Theta

这些模型的加入缩小了统计模型和深度学习模型之间的差距。此外,Nixtla 团队正确地指出了这两类模型在成本和资源需求上的显著差异。

确实,许多数据科学家被深度学习的过度炒作所误导,缺乏解决预测问题的正确方法。

我们将在下一部分进一步讨论这个问题。但在此之前,我们需要解决深度学习面临的批评。

深度学习受到批评

过去十年中,深度学习的发展是惊人的。至今尚无减缓的迹象。

然而,每一个威胁到现状的革命性突破通常都会遭遇怀疑和批评。以 GPT-4 为例:这一新发展在下一个十年威胁到 20%的美国工作岗位[6]。

深度学习和变换器在自然语言处理领域的主导地位是不容否认的。然而,面试中人们却提出类似这样的问题:

自然语言处理(NLP)的进步是归功于更好的研究,还是仅仅因为数据的增加和计算能力的提升?

在时间序列预测中,情况更为糟糕。要理解这一点的原因,你必须首先了解传统上是如何处理预测问题的。

在机器学习/深度学习广泛应用之前,预测完全是关于为数据集制定正确的转换。这包括使时间序列平稳,去除趋势和季节性,考虑波动性,并使用如 box-cox 变换等技术。所有这些方法都需要手动干预,并且对数学和时间序列有深刻理解。

随着机器学习的出现,时间序列算法变得更加自动化。你可以在几乎没有预处理的情况下直接应用这些算法(尽管额外的预处理和特征工程总是有帮助的)。如今,许多改进工作主要集中在超参数调优上。

因此,使用高级数学和统计学的人难以理解机器学习/深度学习算法能够超越传统统计模型的事实。而有趣的是,研究人员对一些深度学习概念真正有效的原因和机制却一无所知。

最近文献中的时间序列预测

就我所知,现有文献缺乏足够的证据来说明各种预测模型的优缺点。以下两篇论文是最相关的:

变换器模型对于时间序列预测是否有效?

一篇有趣的论文[2]展示了某些预测变换器模型的弱点。该论文举例说明了现代变换器模型中使用的位置信息编码方案如何未能捕捉时间序列的时间动态。这一点确实是正确的——自注意力机制是不变的。然而,论文未提及那些已经有效解决此问题的变换器模型。

例如,谷歌的 Temporal Fusion Transformer(TFT)使用编码器-解码器 LSTM 层来创建时间感知和上下文感知的嵌入。此外,TFT 使用了为时间序列问题适配的新颖注意力机制,以捕捉时间动态并提供可解释性。

类似地,亚马逊的 MQTransformer 使用其新颖的位置编码方案(上下文相关的季节性编码)和注意力机制(反馈感知注意力)。

我们真的需要深度学习模型来进行时间序列预测吗?

这篇论文 [3] 也很有趣,因为它比较了 统计提升树机器学习深度学习 类别的各种预测方法。

不幸的是,它未能达到其标题所述,因为在 12 个模型中表现最好的还是谷歌的 TFT,这是一种纯粹的深度学习模型。论文提到:

…… 表 5 中的结果强调了配置了滚动预测的 GBRT 的竞争力,但也显示了显著强大的基于变换器的模型,如 TFT [12],确实超越了提升回归树的表现。

总的来说,阅读复杂的预测论文和模型时要小心,特别是关于出版来源的部分。国际预测期刊 (IJF) 就是一个专注于预测的信誉良好的期刊的例子。

如何处理预测问题

这并不简单。每个数据集都是独特的,每个项目的目标也各不相同,使得预测变得具有挑战性。

然而,本文提供了一些可能对大多数方法有益的通用建议。

正如你从本文中了解到的,深度学习模型是预测项目中的一种新兴趋势,但它们仍处于早期阶段。尽管它们具有潜力,但也可能存在陷阱。

不建议立即将深度学习模型作为项目的首选。根据 Makridakis 等人和 Nixtla 的研究,最好从统计模型开始。3–4 个统计模型的集成可能比你预期的更强大。此外,尝试使用提升树,特别是如果你有表格数据的话。对于小型数据集(数千条数据),这些方法可能已足够。

深度学习模型可能提供额外的 3–10% 准确度提升。然而,训练这些模型可能耗时且昂贵。对于一些领域,如金融和零售,这额外的准确度提升可能更具价值,并使使用深度学习模型成为合理选择。更准确的产品销售预测或 ETF 的收盘价可能会转化为数千美元的增量收入。

另一方面,像 N-BEATS 和 N-HITS 这样的深度学习模型具有迁移学习能力。

如果构建了足够大的时间序列数据集,并且一个愿意的实体对这 2 个模型进行预训练并共享其参数,我们可以直接使用这些模型并实现顶尖的预测准确性(或先对我们的数据集进行少量微调)。

结束语

时间序列预测是数据科学的一个关键领域。

但与其他领域相比,它也被严重低估。Makridakis 等人[4]的论文为未来提供了一些有价值的见解,但仍有很多工作和研究需要完成。

此外,深度学习模型在预测中的应用仍未被广泛探索。

例如,多模态架构在深度学习中随处可见。这些架构利用多个领域来学习特定任务。例如,CLIP(由DALLE-2Stable Diffusion使用)结合了自然语言处理计算机视觉

基准 M3 数据集仅包含 3,003 个时间序列,每个序列最多 500 个观测值。相比之下,成功的 Deep GPVAR 预测模型包含平均 44K 个参数。相比之下,Facebook 的 LLaMA 语言 Transformer 模型的最小版本拥有 70 亿个参数,并在 1 万亿个令牌上进行训练。

因此,关于原始问题,没有明确的答案说明哪个模型是最好的,因为每个模型都有其优缺点。

相反,本文旨在提供所有必要的信息,帮助您选择最适合您项目或案例的模型。

感谢阅读!

[## AutoGluon-TimeSeries:创建强大的集成预测——完整教程

亚马逊的时间序列预测框架具备了一切。

aihorizonforecast.substack.com

参考文献

[1] 从 Stable Diffusion 创建,文本提示为“一个蓝青色时间序列抽象,闪亮的数字画,概念艺术”

[2] Ailing Zeng 等人 变压器在时间序列预测中是否有效?(2022 年 8 月)

[3] Shereen Elsayed 等人 我们真的需要深度学习模型来进行时间序列预测吗?(2021 年 10 月)

[4] Makridakis 等人 统计学、机器学习和深度学习预测方法:比较与前进方向 (2022 年 8 月)

[5] Kang 等人 可视化预测算法性能使用时间序列实例空间 (《国际预测学杂志》,2017 年)

[6] Eloundou 等人 GPTs 即 GPTs:大型语言模型对劳动市场潜在影响的早期观察(2023 年 3 月)

文章中使用的所有图像均来自[4]。 M3 数据集以及所有 M 数据集“可以在不需进一步许可的情况下自由使用”

基于深度学习的时间序列预测(LSTM-RNN)在 PyTorch 中的应用

原文:towardsdatascience.com/time-series-forecasting-with-deep-learning-in-pytorch-lstm-rnn-1ba339885f0c

一个关于使用 PyTorch 进行单变量时间序列深度学习预测的深入教程

Zain BaquarTowards Data Science Zain Baquar

·发布于 Towards Data Science ·阅读时间 12 分钟·2023 年 2 月 9 日

--

Unsplash: Maxim Hopman

介绍

信不信由你,人类在不断被动地预测事物——即使是最微小或看似琐碎的事情。过马路时,我们预测汽车会在哪里以安全地过马路,或者我们尝试预测球在我们试图接住它时会在哪里。我们不需要知道汽车的确切速度或影响球的风向才能完成这些任务——这些能力对我们来说或多或少是自然而然的。这些能力通过一系列事件得到调整,通过多年的经验和实践使我们能够应对我们生活中不可预测的现实。而当需要主动预测大规模现象,如天气或一年后的经济表现时,我们往往因为需要考虑的因素过多而失败。

这就是计算的力量所在——弥补我们无法将即使是最看似随机的事件与未来事件联系起来的不足。众所周知,计算机在执行特定任务时非常擅长——我们可以利用这一点来预测未来。

什么是“时间序列”?

时间序列是指在一段时间内发生的任何可量化的指标或事件。尽管这听起来很琐碎,但几乎任何东西都可以被认为是时间序列。比如一个月内每小时的平均心率,或者一年内每日的股票收盘价,或者某城市一年内每周的交通事故数量。记录这些信息在任何统一的时间段内被认为是时间序列。聪明的人会注意到,这些例子中都有一个频率(每天、每周、每小时等)和一个时间长度(一个月、一年、一天等)来描述事件发生的周期。

对于时间序列,指标是在我们观察指标的时间长度内以统一频率记录的。换句话说,每个记录之间的时间应该是相同的。

在本教程中,我们将探讨如何使用时间序列中的过去数据来预测未来可能发生的情况。

目标

算法的目标是能够接受一系列值,并预测序列中的下一个值。最简单的方法是使用自回归模型,然而,这已经被其他作者广泛讨论过,因此我们将专注于一种更深入的学习方法,即使用递归神经网络。我已将实现笔记本链接在此。本教程中使用的数据集曾在 Kaggle 比赛中使用,可以在这里找到。

数据准备

让我们看看一个示例时间序列。下面的图表显示了 2013 年至 2018 年间油价的一些数据。

作者提供的图像

这只是一个在日期轴上绘制单一数值序列的图表。下表显示了该时间序列的前 10 个条目。从日期列来看,很明显我们有每天频率的价格数据。

date        dcoilwtico
2013-01-01  NaN
2013-01-02  93.14
2013-01-03  92.97
2013-01-04  93.12
2013-01-07  93.20
2013-01-08  93.21
2013-01-09  93.08
2013-01-10  93.81
2013-01-11  93.60
2013-01-14  94.27

许多机器学习模型在标准化数据上表现更好。标准化数据的方式是将数据转换为每列的均值为 0,标准差为 1。下面的代码提供了一种使用scikit-learn库进行标准化的方法。

from sklearn.preprocessing import StandardScaler

# Fit scalers
scalers = {}
for x in df.columns:
  scalers[x] = StandardScaler().fit(df[x].values.reshape(-1, 1))

# Transform data via scalers
norm_df = df.copy()
for i, key in enumerate(scalers.keys()):
  norm = scalers[key].transform(norm_df.iloc[:, i].values.reshape(-1, 1))
  norm_df.iloc[:, i] = norm

我们还希望确保我们的数据具有统一的频率——在这个例子中,我们在这 5 年中每天都有油价数据,所以这很好地解决了问题。如果你的数据中不是这种情况,Pandas 提供了几种不同的方法来重新采样数据以适应统一的频率。

序列化

一旦实现了这一点,我们将使用时间序列生成固定长度的片段或序列。在记录这些序列时,我们还会记录序列之后发生的值。例如:假设我们有一个序列:[1, 2, 3, 4, 5, 6]。

通过选择序列长度为 3,我们可以生成以下序列及其相关目标:

[Sequence]: Target

[1, 2, 3] → 4

[2, 3, 4] → 5

[3, 4, 5] → 6

另一种看待这个问题的方法是,我们定义了回顾多少步以预测下一个值。我们将这个值称为 训练窗口,预测的值数目称为 预测窗口。在这个例子中,这些分别是 3 和 1。下面的函数详细说明了如何实现这一点。

# Defining a function that creates sequences and targets as shown above
def generate_sequences(df: pd.DataFrame, tw: int, pw: int, target_columns, drop_targets=False):
  '''
  df: Pandas DataFrame of the univariate time-series
  tw: Training Window - Integer defining how many steps to look back
  pw: Prediction Window - Integer defining how many steps forward to predict

  returns: dictionary of sequences and targets for all sequences
  '''
  data = dict() # Store results into a dictionary
  L = len(df)
  for i in range(L-tw):
    # Option to drop target from dataframe
    if drop_targets:
      df.drop(target_columns, axis=1, inplace=True)

    # Get current sequence  
    sequence = df[i:i+tw].values
    # Get values right after the current sequence
    target = df[i+tw:i+tw+pw][target_columns].values
    data[i] = {'sequence': sequence, 'target': target}
  return data

PyTorch 要求我们以以下方式将数据存储在 Dataset 类中:

class SequenceDataset(Dataset):

  def __init__(self, df):
    self.data = df

  def __getitem__(self, idx):
    sample = self.data[idx]
    return torch.Tensor(sample['sequence']), torch.Tensor(sample['target'])

  def __len__(self):
    return len(self.data)

然后,我们可以使用 PyTorch 的 DataLoader 来迭代数据。使用 DataLoader 的好处是它内部处理批次和洗牌,因此我们不必担心自己实现这些功能。

训练批次在以下代码执行后就准备好了:

# Here we are defining properties for our model

BATCH_SIZE = 16 # Training batch size
split = 0.8 # Train/Test Split ratio

sequences = generate_sequences(norm_df.dcoilwtico.to_frame(), sequence_len, nout, 'dcoilwtico')
dataset = SequenceDataset(sequences)

# Split the data according to our split ratio and load each subset into a
# separate DataLoader object
train_len = int(len(dataset)*split)
lens = [train_len, len(dataset)-train_len]
train_ds, test_ds = random_split(dataset, lens)
trainloader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, drop_last=True)
testloader = DataLoader(test_ds, batch_size=BATCH_SIZE, shuffle=True, drop_last=True)

在每次迭代中,DataLoader 将输出 16(批次大小)个序列及其相关目标,我们将这些序列传递给模型。

模型架构

以下类在 PyTorch 中定义了这一架构。我们将使用一个 LSTM 层,后面跟着一些密集层用于模型的回归部分,并且它们之间有丢弃层。模型将为每个训练输入输出一个单一值。

class LSTMForecaster(nn.Module):

  def __init__(self, n_features, n_hidden, n_outputs, sequence_len, n_lstm_layers=1, n_deep_layers=10, use_cuda=False, dropout=0.2):
    '''
    n_features: number of input features (1 for univariate forecasting)
    n_hidden: number of neurons in each hidden layer
    n_outputs: number of outputs to predict for each training example
    n_deep_layers: number of hidden dense layers after the lstm layer
    sequence_len: number of steps to look back at for prediction
    dropout: float (0 < dropout < 1) dropout ratio between dense layers
    '''
    super().__init__()

    self.n_lstm_layers = n_lstm_layers
    self.nhid = n_hidden
    self.use_cuda = use_cuda # set option for device selection

    # LSTM Layer
    self.lstm = nn.LSTM(n_features,
                        n_hidden,
                        num_layers=n_lstm_layers,
                        batch_first=True) # As we have transformed our data in this way

    # first dense after lstm
    self.fc1 = nn.Linear(n_hidden * sequence_len, n_hidden) 
    # Dropout layer 
    self.dropout = nn.Dropout(p=dropout)

    # Create fully connected layers (n_hidden x n_deep_layers)
    dnn_layers = []
    for i in range(n_deep_layers):
      # Last layer (n_hidden x n_outputs)
      if i == n_deep_layers - 1:
        dnn_layers.append(nn.ReLU())
        dnn_layers.append(nn.Linear(nhid, n_outputs))
      # All other layers (n_hidden x n_hidden) with dropout option
      else:
        dnn_layers.append(nn.ReLU())
        dnn_layers.append(nn.Linear(nhid, nhid))
        if dropout:
          dnn_layers.append(nn.Dropout(p=dropout))
    # compile DNN layers
    self.dnn = nn.Sequential(*dnn_layers)

  def forward(self, x):

    # Initialize hidden state
    hidden_state = torch.zeros(self.n_lstm_layers, x.shape[0], self.nhid)
    cell_state = torch.zeros(self.n_lstm_layers, x.shape[0], self.nhid)

    # move hidden state to device
    if self.use_cuda:
      hidden_state = hidden_state.to(device)
      cell_state = cell_state.to(device)

    self.hidden = (hidden_state, cell_state)

    # Forward Pass
    x, h = self.lstm(x, self.hidden) # LSTM
    x = self.dropout(x.contiguous().view(x.shape[0], -1)) # Flatten lstm out 
    x = self.fc1(x) # First Dense
    return self.dnn(x) # Pass forward through fully connected DNN.

这个类是一个即插即用的 Python 类,我构建它是为了能够动态构建任何大小的神经网络(此类型),基于我们选择的参数——因此请随意调整参数 n_hidden 和 n_deep_players,以添加或删除模型中的参数。更多参数意味着更多模型复杂性和更长的训练时间,所以一定要参考你的使用场景,以确定对数据最合适的参数设置。

作为一个任意选择,我们创建一个具有 5 层全连接层的长短期记忆(LSTM)模型,每层 50 个神经元,最终每个训练样本在每个批次中以单一输出值结束。在这里,sequence_len 指的是训练窗口,而 nout 定义了预测的步数;将 sequence_len 设置为 180 和 nout 设置为 1,意味着模型将回顾过去 180 天(半年),以预测明天会发生什么。

nhid = 50 # Number of nodes in the hidden layer
n_dnn_layers = 5 # Number of hidden fully connected layers
nout = 1 # Prediction Window
sequence_len = 180 # Training Window

# Number of features (since this is a univariate timeseries we'll set
# this to 1 -- multivariate analysis is coming in the future)
ninp = 1

# Device selection (CPU | GPU)
USE_CUDA = torch.cuda.is_available()
device = 'cuda' if USE_CUDA else 'cpu'

# Initialize the model
model = LSTMForecaster(ninp, nhid, nout, sequence_len, n_deep_layers=n_dnn_layers, use_cuda=USE_CUDA).to(device)

模型训练

定义了模型后,我们可以选择损失函数和优化器,设置学习率和训练轮数,并开始训练循环。由于这是一个回归问题(即我们尝试预测一个连续值),一个安全的选择是均方误差作为损失函数。这提供了一种稳健的方法来计算实际值与模型预测值之间的误差。计算公式如下:

图片摘自 Google。

优化器对象存储和计算了反向传播所需的所有梯度。

# Set learning rate and number of epochs to train over
lr = 4e-4
n_epochs = 20

# Initialize the loss function and optimizer
criterion = nn.MSELoss().to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=lr)

这是训练循环。在每次训练迭代中,我们将计算之前创建的训练集和验证集上的损失:

# Lists to store training and validation losses
t_losses, v_losses = [], []
# Loop over epochs
for epoch in range(n_epochs):
  train_loss, valid_loss = 0.0, 0.0

  # train step
  model.train()
  # Loop over train dataset
  for x, y in trainloader:
    optimizer.zero_grad()
    # move inputs to device
    x = x.to(device)
    y  = y.squeeze().to(device)
    # Forward Pass
    preds = model(x).squeeze()
    loss = criterion(preds, y) # compute batch loss
    train_loss += loss.item()
    loss.backward()
    optimizer.step()
  epoch_loss = train_loss / len(trainloader)
  t_losses.append(epoch_loss)

  # validation step
  model.eval()
  # Loop over validation dataset
  for x, y in testloader:
    with torch.no_grad():
      x, y = x.to(device), y.squeeze().to(device)
      preds = model(x).squeeze()
      error = criterion(preds, y)
    valid_loss += error.item()
  valid_loss = valid_loss / len(testloader)
  v_losses.append(valid_loss)

  print(f'{epoch} - train: {epoch_loss}, valid: {valid_loss}')
plot_losses(t_losses, v_losses)

训练循环的示例输出,显示每个时代的训练和验证损失。

现在模型已经训练完成,我们可以评估我们的预测。

推断

在这里,我们将简单地调用我们训练好的模型来预测未打乱的数据,并查看预测与真实观察值的差异。

def make_predictions_from_dataloader(model, unshuffled_dataloader):
  model.eval()
  predictions, actuals = [], []
  for x, y in unshuffled_dataloader:
    with torch.no_grad():
      p = model(x)
      predictions.append(p)
      actuals.append(y.squeeze())
  predictions = torch.cat(predictions).numpy()
  actuals = torch.cat(actuals).numpy()
  return predictions.squeeze(), actuals

历史上的归一化预测与实际油价。图片由作者提供。

初次尝试,我们的预测看起来还不错!而且我们验证损失与训练损失一样低,说明我们没有过拟合模型,因此模型可以被认为具有良好的泛化能力——这对任何预测系统都很重要。

有了对该时间段油价的相对合理估计,让我们看看能否用它来预测未来的情况。

预测

如果我们将历史定义为预测时刻之前的系列,算法则很简单:

  1. 从历史记录中获取最新有效序列(训练窗口长度)。

  2. 将最新的序列输入模型,并预测下一个值。

  3. 将预测值附加到历史记录中。

  4. 从第 1 步重复进行任意次数的迭代。

一个警告是,根据训练模型时选择的参数,预测得越远,模型越容易受到自身偏差的影响,开始预测均值。因此,如果不必要,我们不希望总是预测过远,因为这会降低预测的准确性。

这在下面的函数中实现:

 def one_step_forecast(model, history):
      '''
      model: PyTorch model object
      history: a sequence of values representing the latest values of the time 
      series, requirement -> len(history.shape) == 2

      outputs a single value which is the prediction of the next value in the
      sequence.
      '''
      model.cpu()
      model.eval()
      with torch.no_grad():
        pre = torch.Tensor(history).unsqueeze(0)
        pred = self.model(pre)
      return pred.detach().numpy().reshape(-1)

  def n_step_forecast(data: pd.DataFrame, target: str, tw: int, n: int, forecast_from: int=None, plot=False):
      '''
      n: integer defining how many steps to forecast
      forecast_from: integer defining which index to forecast from. None if
      you want to forecast from the end.
      plot: True if you want to output a plot of the forecast, False if not.
      '''
      history = data[target].copy().to_frame()

      # Create initial sequence input based on where in the series to forecast 
      # from.
      if forecast_from:
        pre = list(history[forecast_from - tw : forecast_from][target].values)
      else:
        pre = list(history[self.target])[-tw:]

      # Call one_step_forecast n times and append prediction to history
      for i, step in enumerate(range(n)):
        pre_ = np.array(pre[-tw:]).reshape(-1, 1)
        forecast = self.one_step_forecast(pre_).squeeze()
        pre.append(forecast)

      # The rest of this is just to add the forecast to the correct time of 
      # the history series
      res = history.copy()
      ls = [np.nan for i in range(len(history))]

      # Note: I have not handled the edge case where the start index + n is 
      # before the end of the dataset and crosses past it.
      if forecast_from:
        ls[forecast_from : forecast_from + n] = list(np.array(pre[-n:]))
        res['forecast'] = ls
        res.columns = ['actual', 'forecast']
      else:
        fc = ls + list(np.array(pre[-n:]))
        ls = ls + [np.nan for i in range(len(pre[-n:]))]
        ls[:len(history)] = history[self.target].values
        res = pd.DataFrame([ls, fc], index=['actual', 'forecast']).T
      return res

让我们尝试几个案例。

从系列中间的不同位置进行预测,以便将预测与实际发生的情况进行比较。由于我们编码的预测器可以从任何地方进行预测,并且可以预测任意合理的步数。红线表示预测。请注意,图中的 y 轴显示的是归一化价格。

从 2013 年第三季度预测 200 天。图片由作者提供。

从 2014/15 年年末预测 200 天。图片由作者提供。

从 2016 年第一季度预测 200 天。图片由作者提供。

从数据的最后一天预测 200 天。图片由作者提供。

这只是我们尝试的第一个模型配置!更多地实验架构和实现将使您的模型训练得更好,并更准确地进行预测。

结论

就这样!一个可以预测单变量时间序列中接下来会发生什么的模型。考虑到这种模型可以应用的各种方式和场景,真的很酷。是的,这篇文章仅处理了单变量时间序列,其中只有一个值序列。然而,也有方法可以使用多个测量不同事物的序列来进行预测。这被称为多变量时间序列预测,它主要只需要对模型架构进行一些调整,这些我将在未来的文章中进行介绍。

这种预测模型的真正魔力在于模型的 LSTM 层,它如何处理和记忆序列作为神经网络的递归层。有关不同类型神经网络的更多信息,我强烈推荐3blue1brown 的视频。他有一个很棒的系列,详细介绍了这些算法如何在内部工作,非常直观。

感谢阅读,确保查看我的其他文章!

参考资料:

时间序列数据 — www.kaggle.com/competitions/store-sales-time-series-forecasting/data?select=oil.csv

用 Facebook 的 Prophet 进行时间序列预测——第一部分

原文:towardsdatascience.com/time-series-forecasting-with-facebooks-prophet-in-10-minutes-958bd1caff3f

用 6 行代码构建一个有效模型

Guillaume WeingertnerTowards Data Science Guillaume Weingertner

·发表于Towards Data Science ·6 分钟阅读·2023 年 3 月 23 日

--

Prophet 的输出——作者插图

#1 动机

时间序列预测模型在商业决策过程中带来的附加值是不可否认的。我最近构建了一个模型,其影响如此之大,以至于我想在这里分享这种方法。

在尝试预测目标变量的未来值时,主要有两大类预测模型:

传统时间序列模型机器学习模型

时间序列模型——作者插图

  • 使用单变量时间序列模型时,基本思路是仅根据目标变量(即我们试图预测的变量)过去数据的趋势和季节性来预测未来值,而不考虑其他因素。多变量模型是其扩展,输入可以是多个时间序列。

  • 使用(监督)机器学习模型时,基本思路是利用其他解释变量,并建模它们与目标变量之间的关系来进行预测。

在这篇文章中,你猜对了,我们将重点关注传统方法,并尝试回答一个简单的问题:我们能否快速利用时间序列的过去值来预测未来值? 这个问题出现在许多领域:股市动向、公司销售、二氧化碳排放等等。

实际上,为了回答业务问题,你往往不是在寻找一种能够赢得机器学习挑战的最佳模型,而只是寻求一个足够好的估计,以便做出基于数据的决策。

这就是Facebook 的 Prophet的作用。它是一个现成的算法,易于设置,并且几乎不费力气就能获得不错的结果。

让我们看看如何开箱即用,并在几分钟内获得结果。

#2 商业问题

对于这个快速教程,我们将使用一个生成的数据集,代表一个虚构公司的销售——ABC 公司(你可以大致看出,原创性不是我的强项)。

简单声明:此数据集是虚构的,仅用于通过一个适合时间序列预测的示例来说明 Prophet 的使用。

首先,让我们导入必要的库来加载和绘制数据。

import pandas as pd
import datetime
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
from matplotlib.dates import MonthLocator, YearLocator, DateFormatter

df = pd.read_csv('sales_data.csv')
df['date'] = pd.to_datetime(df['date'])
df.head()

这就是我们的数据框的样子:虚构的 ABC 公司的每日销售数据:

虚构的数据集——图片由作者提供。

我们可以绘制它以突出显示两个感兴趣的点:

  • 明显的年度上升趋势——销售额随着时间的推移在增长。

  • 一年中的季节性——公司在上半年销售比下半年更多。

ABC 公司在时间上的销售——图片由作者提供

现在,这整个小操作的目标是对未来的销售进行预测——从 2023 年 1 月开始。

因为我们使用的是Prophet,一个单变量时间序列模型,我们只使用过去的销售数据(季节性和趋势)来预测未来的销售数据。

请注意,你可以将额外的时间序列作为模型的输入,实际上使Prophet成为一个多变量模型。

让我们看看这如何工作。

#3 设置模型

如果你之前从未使用过Prophet,你需要使用 pip 或 conda 安装它并导入:

#pip install prophet
#OR
#conda install -c conda-forge prophet

from prophet import Prophet

现在,让我们实际建立模型并进行预测。

如果你之前玩过机器学习模型,那么接下来的步骤应该很熟悉:

  • 将列重命名为‘ds’和‘y’。这对Prophet的工作至关重要。

  • 创建一个Prophet实例,并在过去的数据上训练/拟合模型。

  • 使用模型对未来数据进行预测。

# Rename the columns
df.columns = ['ds', 'y']

# Creating an instance of the Prophet class and training the model
m = Prophet() #instantiating a new Prophet object
model = m.fit(df) #build the model with the historical data

future = model.make_future_dataframe(periods=60, freq='D') #build the dataframe containing the predictions (and the historical data with the model fit)
forecast = model.predict(future) #The forecast object here is a new dataframe that includes a column yhat with the forecast

forecast[['ds', 'trend', 'weekly', 'yearly', 'yhat']].tail()

这就是预测的样子:

这是第一个开箱即用的 Prophet 模型的结果——yhat 是预测——图片由作者提供。

Facebook 的Prophet是一个加性回归模型,最终预测(yhat)是所有分量(趋势和季节性)分别建模的总和。

yhat(t) = g(t) + s(t) + h(t) + e(t),其中

  • g(t) = 随时间的增长(趋势)

  • s(t) = 季节性(每周、每年等)

  • h(t) = 假期(在我们的用例中尚未建模)

  • e(t) = 白噪声误差项(在我们的用例中尚未出现)

总共用了 6 行代码来建立一个初步的工作模型。

有两个图表值得关注,以便更清楚地了解结果:

第一个图是实际的预测随时间变化的图:

plot1 = model.plot(forecast, uncertainty=True)

Prophet 的输出——图片由作者提供

你可以大致看出这个图表上有 3 种不同的元素:

  1. 黑点代表我们实际的过去数据点。

  2. 蓝线是通过数据点拟合的模型,并将其外推到未来(在图表的最后 60 天)

  3. 浅蓝色区域是不确定性区间

第二个有趣的图表是模型组件的分解。这展示了我们刚刚提到的趋势和季节性模型。

plot2 = model.plot_components(forecast)

Prophet 的组件 — 作者提供的图片

目前,在这个非常基础的模型中,我们只有 3 个组件:

  • 趋势随着时间的推移而增长

  • 每周季节性显示周末销售较少

  • 年度季节性清晰地展示了两个销售量不同的时期

我们将在第二部分中看到,我们可以添加更多特征来提高模型的性能。

请注意,预测本质上是不确定的,但构建此模型有助于识别趋势或规划潜在情景,这无疑在决策过程中提供了附加价值。

#4 下一步是什么?

当然,这还不是一个令人满意的结果(还),因为预测的质量未知,仅仅基于这个基础的“黑箱”模型所输出的结果很难做出任何决策。

然而,这是一个很好的起点。接下来的步骤将包括:

  • 评估模型的性能

  • 理解黑箱和相关的超参数

  • 微调模型的超参数以提高其性能

敬请期待第二部分!

有用的资源

[1] 官方 Prophet 文档:facebook.github.io/prophet/

[2] Joos Korstanje 的《用 Python 进行高级预测:包括 LSTMs、Facebook Prophet 和 Amazon DeepAR 的最新模型》

[3] Aileen Nielsen 的《实用时间序列分析:使用统计和机器学习进行预测》

[4] 一个有用的网站,解释时间序列预测并快速了解 Prophet 的方程:otexts.com/fpp3/prophet.html

感谢你读到文章的最后。

关注以获取更多内容!

如果你有任何问题或意见,欢迎在下面留言,或通过* LinkedIn 与我联系!

用 Facebook 的 Prophet 进行时间序列预测,10 分钟 — 第二部分

原文:towardsdatascience.com/time-series-forecasting-with-facebooks-prophet-in-10-minutes-part-2-1f558ccc3e83

模型性能和超参数微调

Guillaume WeingertnerTowards Data Science Guillaume Weingertner

·发布于 Towards Data Science ·9 分钟阅读·2023 年 4 月 25 日

--

Prophet 的输出 — 作者图片

#1 在上一集…

在第一部分中,我们看到如何仅用 6 行代码快速建立一个工作的模型。请参阅下面的链接:

## 用 Facebook 的 Prophet 进行时间序列预测,10 分钟 — 第一部分

用 6 行代码建立一个工作的模型

towardsdatascience.com

但是我们真的可以信任这个模型输出的结果吗?

如果是这样,它的表现如何?我们能让它更好吗?

让我们看看如何评估模型的性能,添加假期/特殊事件(这些可能是时间序列的重要部分)到模型中,并调整其超参数以使其更准确、更可靠。

#2 训练-测试拆分

为了评估我们模型的性能,我们可以等待未来的值发生,并将其与模型预测的结果进行比较,或者我们可以使用一种非常常见的模型验证方法,即将历史数据拆分成两个不同的数据集:

  • 一个用于拟合模型的训练集

  • 一个用于评估模型的测试集

我们将 2023 年 11 月和 12 月作为测试数据,其余部分(即训练集)用于训练模型。

train = df.iloc[:1400,:]
test = df.iloc[1400:,:]

训练-测试拆分 — 作者图片

#3 性能指标

在这一部分中,让我们快速介绍一下我们将用来评估模型性能的指标。

我们先从几个快速定义开始:

  • yᵢ 是测试集中的实际数据点

  • ŷᵢ 是模型估计的点

  • ȳ 是时间序列测试集的均值

这三种指标(MAE、MSE 和 R 方)的组合应该能为我们提供对模型性能的足够估计。

1/ 平均绝对误差 (MAE):

我们计算观察值与预测值之间的平均绝对差。

平均绝对误差 — 图片作者

MAE 非常简单易于解释,并告诉你“平均而言模型偏离了 x 数量”。

与其他误差指标相比,MAE 对异常值的敏感度较低,这意味着异常值受到的惩罚较轻。

2/ 均方误差 (MSE):

我们计算观察值与预测值之间的平均平方差。

均方误差 — 图片作者

MSE 的单位与被预测数据的单位不同,因为它涉及误差的平方,使其比 MAE 更难解释。

然而,它对较大的误差给予更重的惩罚(由于平方操作),使得它对异常值或偏离实际值的预测更加敏感。

3/ R 方

决定系数是性能指标而非误差指标。它衡量回归模型中预测变量解释响应变量方差的比例。

R 方 — 图片作者

R 方值范围从 0 到 1,较高的值表示模型与数据的拟合更好。值为 1 表示回归模型解释了响应变量的所有变异,而值为 0 表示模型没有解释任何变异。

#4 模型的初步性能

正如我们在第一部分中所做的那样,让我们创建一个基本的、现成的模型。然而这次,我们将在训练集上拟合它,并在未见过的数据(即测试集)上测试其性能。

# Creating an instance of the Prophet class and training the model
m = Prophet() #instantiating a new Prophet object
model = m.fit(train) #build the model with the historical data

future = model.make_future_dataframe(periods=len(test), freq='D') #build the dataframe containing the predictions 
forecast = model.predict(future) #dataframe including a column yhat with the forecast

让我们在同一图表上绘制基本模型和测试集的实际值,以了解发生了什么:

基本模型(橙色)与实际值(蓝色)— 图片作者

对于一个 6 行模型,它的视觉效果还不错!然而,我们可以清楚地看到模型没有很好地捕捉到一些点(图上的异常值/峰值)。

为了计算 MAE、MSE 和 R 方以评估模型的性能,我们可以简单地从 scikit-learn 库中导入这些指标。另一种方法是从头计算,但我们这里可以偷懒。

from sklearn import metrics

r2_score = metrics.r2_score(list(test['y']), list(forecast.loc[1400:,'yhat']))
mae = metrics.mean_absolute_error(list(test['y']), list(forecast.loc[1400:,'yhat']))
mse = metrics.mean_squared_error(list(test['y']), list(forecast.loc[1400:,'yhat']))

print(f'r2_score : {r2_score}')
print(f'mae : {mae}')
print(f'mse : {mse}')

这是第一个基本模型的指标:

基本模型性能 — 图片作者

我们可以看出,这第一个模型在每次预测时平均偏差 $123(如 MAE 所示),R2 分数为 0.52。

目前 MSE 很难解释,但在与其他模型比较时会发挥作用。

#5 添加假期

根据业务案例,将假期纳入模型可能是相关的。在我们虚构公司的情况下,我们可以假设假期可能会对销售产生影响。

Prophet 文档清楚地解释了如何输入特殊假期以便模型能够考虑。

让我们将黑色星期五和圣诞前夕作为特殊事件添加进来……

black_friday = pd.DataFrame({
  'holiday': 'black_friday',
  'ds': pd.to_datetime(['2019-11-29', '2020-11-27', '2021-11-26', '2022-11-25']),
  'lower_window': -10,
  'upper_window': 1,
})
xmas_eve = pd.DataFrame({
  'holiday': 'xmas_eve',
  'ds': pd.to_datetime(['2019-12-24', '2020-12-24', '2021-12-24', '2022-12-24']),
  'lower_window': -5,
  'upper_window': 1,
})
holidays = pd.concat((black_friday, xmas_eve))

……以及将常规美国假期纳入我们的模型中。

并对这个第二个模型进行与之前相同的训练阶段。

# Creating an instance of the Prophet class and training the model
m2 = Prophet(holidays=holidays)
m2.add_country_holidays(country_name='US') #adding US holidays
model2 = m2.fit(train)

future2 = model2.make_future_dataframe(periods=len(test), freq='D')
forecast2 = model2.predict(future2)

让我们绘制这两个模型,以了解发生了什么:

模型比较 — 作者提供的图像

从视觉上我们可以看出,将假期纳入模型中产生了积极影响,因为异常值被考虑在内。让我们来验证一下。

r2_score = metrics.r2_score(list(test['y']), list(forecast2.loc[1400:,'yhat']))
mae = metrics.mean_absolute_error(list(test['y']), list(forecast2.loc[1400:,'yhat']))
mse = metrics.mean_squared_error(list(test['y']), list(forecast2.loc[1400:,'yhat']))

print(f'r2_score : {r2_score}')
print(f'mae : {mae}')
print(f'mse : {mse}')

基本模型 + 假期表现 — 作者提供的图像

将假期纳入我们的模型中是一个巨大的改进。

  • R2 分数从 0.52 上升到 0.69

  • MAE 从 $123 降至 $109,意味着每次预测的平均误差为 $109

  • 由于一些异常值很可能是节假日,现在模型对这些异常值的建模更为准确,因此 MSE 显著下降。

#6 微调模型的超参数

Prophet 的文档在解释我们应该调整哪些参数和哪些参数应该保持不变方面非常有帮助。

根据 Prophet 的开发人员[1],让我们关注最重要的 5 个参数:

1/ changepoint_prior_scale — 默认值 0.05

“它决定了趋势的灵活性,特别是趋势在趋势变化点的变化程度”

2/ seasonality_prior_scale — 默认值 10

“此参数控制季节性的灵活性”

3/ holidays_prior_scale — 默认值 10

“这控制了假期效应的灵活性”

4/ seasonality_mode — 默认值 ‘additive’

“选项有‘additive’ 或 ‘multiplicative’。默认值是 ‘additive’,但许多业务时间序列将具有乘法季节性。”

5/ changepoint_range — 默认值 0.8

“这是趋势允许变化的历史比例。”

这些参数对模型的输出有很大影响。问题是,它们是否设置在最佳值上——即会产生最低 MAE 或 MSE 和最接近 1 的 R-squared 的值?

这一部分最有趣的是我们如何调整这些参数。我们将使用网格搜索,这是一种在机器学习中非常常见的技术,用于微调模型的超参数。

思路很简单:我们创建一个包含广泛超参数组合的“网格”,为每个组合训练一个模型,并测试每一个以查看哪个模型表现最佳。

# Create the grid
changepoint_prior_scale = [0.01, 0.03, 0.05, 0.07] # default 0.05
seasonality_prior_scale = [1, 5, 10, 15] # default 10
holidays_prior_scale = [1, 5, 10, 15] # default 10
seasonality_mode = ['additive', 'multiplicative']
changepoint_range = [0.6, 0.7, 0.8, 0.9] # default 0.8

# Compute the total number of iterations
total_iter = len(changepoint_prior_scale)*len(seasonality_prior_scale)*len(holidays_prior_scale)*len(seasonality_mode)*len(changepoint_range)
print(f'Number of iterations : {total_iter}')

# Loop over the parameters, build and assess the models
grid_search_results = []
iteration = 1
for cps in changepoint_prior_scale:
    for sps in seasonality_prior_scale:
        for hps in holidays_prior_scale:
            for sm in seasonality_mode:
                for cr in changepoint_range:
                    m = Prophet(holidays=holidays, 
                                 changepoint_prior_scale = cps, 
                                 seasonality_prior_scale = sps, 
                                 holidays_prior_scale = hps, 
                                 seasonality_mode = sm, 
                                 changepoint_range = cr)
                    m.add_country_holidays(country_name='US')
                    model = m.fit(train)
                    future = model.make_future_dataframe(periods=len(test), freq='D')
                    forecast = model.predict(future) 

                    r2_score = metrics.r2_score(list(test['y']), list(forecast.loc[1400:,'yhat']))
                    mae = metrics.mean_absolute_error(list(test['y']), list(forecast.loc[1400:,'yhat']))
                    mse = metrics.mean_squared_error(list(test['y']), list(forecast.loc[1400:,'yhat']))

                    print(f'iteration : {iteration} / {total_iter} ')
                    print(f'r2_score : {r2_score}')
                    print(f'mae : {mae}')
                    print(f'mse : {mse}')

                    grid_search_results.append([iteration, cps, sps, hps, sm, cr, r2_score, mae, mse])
                    iteration += 1

# Store the results in a dataframe
grid_search_df = pd.DataFrame(grid_search_results, columns = ['iteration', 'cps', 'sps', 'hps', 'sm', 'cr', 'r2_score', 'mae', 'mse'])

当这一步完成后,我们需要做的就是选择具有最低 MAE、最低 MSE 或最接近 1 的 R-squared 的模型,具体取决于哪种标准最适合我们的业务问题。

对我来说,最重要的是在任何一天都不要出现较大的误差,而每天出现小的误差是可以接受的。这就是为什么我选择了具有最低均方误差(MSE)的模型。

grid_search_df.sort_values('mse', ascending = True).head()

超参数选择 — 作者图片

现在,让我们使用这些参数来构建我们最后的优化模型:

changepoint_prior_scale = 0.05 # default 0.05
seasonality_prior_scale = 1 # default 10
holidays_prior_scale = 5 # default 10
seasonality_mode = 'additive'
changepoint_range = 0.6 # default 0.8

# Creating an instance of the Prophet class and training the model
m3 = Prophet(holidays=holidays, 
            changepoint_prior_scale = changepoint_prior_scale,
            seasonality_prior_scale = seasonality_prior_scale,
            holidays_prior_scale = holidays_prior_scale,
            seasonality_mode = seasonality_mode,
            changepoint_range = changepoint_range)
m3.add_country_holidays(country_name='US')
model3 = m3.fit(train)

future3 = model3.make_future_dataframe(periods=len(test), freq='D')
forecast3 = model3.predict(future3)

下面图表中的绿色线显示了优化模型,验证了上面表格所强调的内容:这个最新模型的测试数据预测更加准确。

所有模型的比较 — 作者图片

#7 结论

该方法论(训练-测试分割、添加特殊事件、微调超参数)提供了一个基本框架,用于提升模型的准确性,如下表所示:

我们检查的 3 个指标上每个模型改进的总结

为了简化,我们没有进行任何交叉验证,这意味着我们只在一个测试数据集上评估了我们的模型。下一步是将交叉验证集成到我们的模型评估过程中。

另一个有趣的方面是将额外的回归变量添加到 Prophet 中——这是模型可以考虑的一个附加变量,以便进行预测。

有用的资源

[1] 官方 Prophet 文档:facebook.github.io/prophet/

[2] Joos Korstanje 的《使用最先进模型(包括 LSTM、Facebook 的 Prophet 和 Amazon 的 DeepAR)进行高级预测》

[3] Aileen Nielsen 的《实用时间序列分析:统计和机器学习预测》

[4] 解释时间序列预测并简要介绍 Prophet 方程的有用网站:otexts.com/fpp3/prophet.html

*感谢你阅读到文章的最后。

关注获取更多内容!

如果你有任何问题或意见,请随时在下方留言,或通过* LinkedIn 与我联系!

[## 通过我的推荐链接加入 Medium - Guillaume Weingertner

阅读 Guillaume Weingertner 和其他成千上万的 Medium 作者的每个故事。你的会员费直接...

guillaume-weingertner.medium.com](https://guillaume-weingertner.medium.com/membership?source=post_page-----1f558ccc3e83--------------------------------)

时间序列转换(及还原)变得简单

原文:towardsdatascience.com/time-series-transformations-and-reverting-made-easy-f4f768c18f63

探索时间序列的转换及如何在 Python 中使用 scalecast 进行还原

迈克尔·基思数据科学之道 迈克尔·基思

·发布于 数据科学之道 ·5 分钟阅读·2023 年 1 月 26 日

--

图片来源:作者

在预测时间序列数据时,平稳性是一个重要因素。某些模型——如 ARIMA、Holt-Winters、指数平滑等——专门用于时间序列,并不一定要求数据是平稳的。序列的平稳性指的是其随时间回到均值的倾向。非平稳序列会在数据集中引入趋势,这可能导致过度依赖这些趋势的函数,从而产生虚假的结果(看起来过于理想的结果)。

指数平滑模型平滑最近的趋势以应对非平稳性,而 ARIMA 会在积分因子(标准 ARIMA 顺序中的中间项)设置为大于 0 时对数据进行差分。其他时间序列模型以不同的方式处理平稳性。

然而,像 XGBoost 这样流行的机器学习模型并不考虑序列的平稳性,因为它并未开发来处理时间序列数据的特殊性。这并不一定意味着你不应该用它进行预测,但在这样做之前,处理潜在的非平稳性是一个好主意。这通常意味着应用一种转换——以某种方式操控原始值——以剥离趋势并使其更适合模型进行预测。有些人对进行转换持保留态度,因为这增加了数据准备的步骤,并使还原结果(以便预测产生可用的信息)变得困难。但情况不一定是这样的。

今天,我将概述如何在 Python 中轻松进行时间序列的转换、预测和还原。以下安装是必需的:

pip install --upgrade scalecast

基本语法

使用 scalecast 包应用转换的语法很简单。首先,我们导入必要的包并提取时间序列数据。我使用的是 Kaggle 上的航空公司数据集,具有开放数据库许可证。

import pandas as pd
import matplotlib.pyplot as plt
from scalecast.Forecaster import Forecaster
from scalecast.SeriesTransformer import SeriesTransformer

data = pd.read_csv('AirPassengers.csv')

然后我们创建一个 Forecaster 对象:

f = Forecaster(
    current_dates = data['Month'],
    y = data['#Passengers'],
    future_dates = 24,
)

使用 Forecaster 对象来提供一个 SeriesTransformer 对象:

transformer = SeriesTransformer(f)

就这样。现在,许多不同的转换,包括自定义转换,已经可供使用。这些转换包括一阶差分、二阶差分及以上、季节性差分、线性去趋势、多项式去趋势、对数去趋势、缩放、boxcox 转换等等。所有转换都是可堆叠且完全可还原的。例如,如果我想先应用季节性差分,然后去趋势我的系列,我可以这样做:

f = transformer.DiffTransform(12) # 12 periods is one seasonal difference for monthly data
f = transformer.DetrendTransform()

# plot results
f.plot();

作者提供的图片

然后我会使用 XGBoost 调用一个预测模型:

f.set_estimator('xgboost')
f.add_ar_terms(12)
f.manual_forecast(n_estimators=100,gamma=2)
f.plot();

要撤销已应用的转换,我会按照其转换对应函数的相反顺序调用还原函数:

f = transformer.DetrendRevert()
f = transformer.DiffRevert(12)
f.plot();

作者提供的图片

自动转换

有这么多转换选项,如果能基于可能最大化预测准确性的自定义推荐,岂不是很好?这也是 scalecast 的一个功能 (doc):

from scalecast.util import find_optimal_transformation
# default args below
transformer, reverter = find_optimal_transformation(
    f, # Forecaster object to try the transformations on
    estimator=None, # model used to evaluate each transformation, default mlr
    monitor='rmse', # out-of-sample metric to monitor
    test_length = None, # default is the fcst horizon in the Forecaster object
    train_length = None, # default is the max available
    num_test_sets = 1, # number of test sets to iterate through, final transformation based on best avg. metric
    space_between_sets = 1, # space between consectutive train sets
    lags='auto', # uses the length of the inferred seasonality
    try_order = ['detrend','seasonal_adj','boxcox','first_diff','first_seasonal_diff','scale'], # order of transformations to try
    boxcox_lambdas = [-0.5,0,0.5], # box-cox lambas
    detrend_kwargs = [{'loess': True},{'poly_order':1},{'poly_order':2}], # detrender transform kwargs (tries as many detrenders as the length of this list)
    scale_type = ['Scale','MinMax','RobustScale'], # scale transformers to try
    m = 'auto', # the seasonal length to try for the seasonal adjusters, accepts multiple
    model = 'add', # the model to use when seasonally adjusting
    # specific model kwargs also accepted
)
# see what it chose
reverter

这个函数按照给定的顺序循环许多可能的转换,并返回它认为能够提供最佳预测机会的转换集合,通过监控一个样本外度量来做出这个判断。从输出中我们可以看到,它选择了 boxcox 转换,随后是一次差分和一次季节性差分。

Reverter(
  reverters = [
    ('DiffRevert', 12),
    ('DiffRevert', 1),
    ('Revert', <function find_optimal_transformation.<locals>.boxcox_re at 0x0000029FA6900EE0>, {'lmbda': -0.5})
  ],
  base_transformer = Transformer(
  transformers = [
    ('Transform', <function find_optimal_transformation.<locals>.boxcox_tr at 0x0000029FA6E579D0>, {'lmbda': -0.5}),
    ('DiffTransform', 1),
    ('DiffTransform', 12)
  ]
)
)

返回的对象可以被输入到一个管道中,在那里应用其他自动机器学习方法:

from scalecast.Pipeline import Pipeline
from scalecast import GridGenerator

GridGenerator.get_example_grids()

transformer, reverter = find_optimal_transformation(f)
def forecaster(f):
    f.auto_Xvar_select()
    f.tune_test_forecast(
        ['elasticnet','xgboost'],
        cross_validate=True,
        limit_grid_size = .2,
    )

pipeline = Pipeline(
    steps = [
        ('Transform',transformer),
        ('Forecast',forecaster),
        ('Revert',reverter),
    ],
)

f = pipeline.fit_predict(f)
f.plot()
plt.title('Automated Forecasting with Transformations');

作者提供的图片

生成的代码易于阅读和维护。还可以进行更多的自定义,或者使用自动方法通常会得到不错的结果。

结论

今天,我概述了使用 scalecast 进行 Python 时间序列转换的方法。该方法扩展了自动机器学习技术,转换以及相应的还原函数被放入管道中以简化应用。感谢您的关注!确保在 GitHub 上为 scalecast 点赞。这里是本发布中使用的完整 代码

[## GitHub - mikekeith52/scalecast: 实践者的预测库

Scalecast 是一个轻量级的时间序列预测程序、封装器和结果容器,专为应用而构建。

github.com

时间旅行轻松掌握:Python Datetime 的全面指南

原文:towardsdatascience.com/time-travel-made-easy-a-comprehensive-guide-to-python-datetime-326dd1c57391?source=collection_archive---------2-----------------------#2023-05-04

可能是你对 Python Datetime 所需的一切

安德烈亚斯·卢基塔面向数据科学 安德烈亚斯·卢基塔

·

关注 发表在 面向数据科学 · 11 分钟阅读 · 2023 年 5 月 4 日

--

图片由 Zulfa Nazer 提供,来源于 Unsplash

处理具有日期和时间的数据可能会令人感到难以应对,尤其是当你不太熟悉datetime操作的细节时。许多术语,如DatetimeIndexTimestampTimedeltaTimezoneOffset,可能会让中级分析师感到困惑,难以理解和记忆。本指南将帮助你掌握datetime操作,并从数据中解锁强大的洞察。让我们开始吧!

Python 标准库中的**datetime**模块提供了可以处理日期、时间和时间间隔的类¹。该模块在数据分析中尤其重要,因为日期和时间通常是数据的关键组成部分,而准确地操作它们对于时间序列分析和金融建模等项目至关重要。通过使用datetime,分析师可以更好地理解数据中的时间趋势和模式,从而从数据集中获得更准确的洞察和预测。**datetime**模块下的 6 个类包括**date****time****datetime****timedelta****tzinfo****timezone**

内容目录

  • 带时区与无时区 **datetime** 对象

  • 协调世界时间 (UTC),时间,时区,偏移量

  • 属性和方法 **datetime** 对象,ISO 8601 标准

  • 格式化 **datetime** 对象,使用 **strftime**(format) 和将字符串解析为 datetime 对象,使用 **strptime**(format)

  • **timedelta** 对象

  • POSIX 时间戳

  • Pandas, **.dt** 访问器和 [**datetime64[ns]**](#18ee)

  • 将日期作为索引, **.resample(), .agg(), .transform()**方法

带时区与无时区datetime对象

简单来说,一个带时区的日期时间对象包含时区信息,使得特定日期和时间的时区变得明确¹。要创建一个带时区的日期时间对象,需要借助**pytz**模块将时区对象附加到日期时间对象上。

from datetime import datetime
import pytz

tz = pytz.timezone('Asia/Singapore')
dt = datetime(2023, 5, 4, 10, 30, 0, tzinfo=tz)

这将创建一个表示 2023 年 5 月 4 日新加坡时间上午 10:30 的带时区datetime对象。tzinfo参数指定了datetime对象的时区。打印dt将给出以下信息datetime.datetime(2023, 5, 4, 10, 30, **tzinfo**=<DstTzInfo ‘Asia/Singapore’ LMT+6:55:00 STD>)

另一方面,一个无时区的日期时间对象不包含时区信息。它确实表示日期和时间,但不清楚这些日期和时间指的是哪个时区¹。

from datetime import datetime

dt = datetime(2023, 5, 4, 10, 30, 0)

调用属性dt.tzinfodt.utcoffset将返回**None**

值得注意的是,感知的**datetime**对象内部始终是 UTC 时间,并在显示或用于计算时调整到指定的时区。这意味着你可以直接比较来自不同时区的感知**datetime**对象,因为它们内部都是以 UTC 时间表示的。通常情况下,最好在可能的情况下使用感知的**datetime**对象,特别是在处理来自不同时区的数据的应用中。

协调世界时(UTC)、时间、时区和偏移量

协调世界时(UTC)是全球用于调节时钟和时间的主要时间标准。1972 年之前,它被称为格林威治标准时间(GMT)²。UTC 是一个全球公认且协调的时间标准,对于国际通信、导航和科学研究至关重要。值得注意的是,UTC 不受夏令时的影响,使其成为时间相关活动的稳定参考点。相反,UTC 基于原子钟,并根据需要通过添加或减去闰秒来与地球的自转保持同步³。因此,UTC 时间在全球范围内保持一致,无论不同时间区的本地时间如何。

UTC 时间是明确的,不会重复。

时区:时区指的是地球上的一个区域,在该区域内所有时钟的偏移量与协调世界时(UTC)相同。它很重要,因为它影响世界各地的本地时间。

偏移量:偏移量指的是从协调世界时(UTC)中加上或减去的一段时间,以获得特定时区的本地时间。这一点很重要,因为它影响不同地区的本地时间。我们可以使用**timedelta**类从**datetime**模块来创建偏移量。

from datetime import timedelta

offset = timedelta(hours=1)
dt2 = dt + offset

打印 dt2 会给出以下信息 datetime.datetime(2023, 5, 4, **11**, 30, **tzinfo**=<DstTzInfo ‘Asia/Singapore’ LMT+6:55:00 STD>)。注意,在添加偏移量后,小时属性从 10 变为 1。

时间、时区和偏移量的相互作用在操控 Python 中的datetime时至关重要,因为它们决定了特定时区中的真实时间,包括夏令时的调整。

datetime 对象的属性和方法,ISO 8601 标准

datetime 类有几个常用的属性,它们在 datetime 操作中非常重要。这些属性包括 yearmonthdayhourminutesecondmicrosecondtzinfo。从我们上面的例子中,

print(dt.year) #2023
print(dt.month) #5 
print(dt.day) #4
print(dt.hour) #11
print(dt.minute) #30
print(dt.second) #0
print(dt.microsecond) #0
print(dt.tzinfo) #Asia/Singapore

一些基本的方法包括 date()time()replace()isoformat()isocalendar()strftime(format)。等等,ISO 格式到底是什么?

ISO 日历格式是一种全球使用的标准,旨在以易于计算机程序读取的格式表示日期和时间⁴。该格式包含特定的语法,日期使用四位数字表示年份,两位数字表示月份,两位数字表示日期(YYYY-MM-DD)。例如,2023 年 1 月 1 日表示为“2023–01–01”。此外,它还可以包含更复杂的信息,如时间和时区,如下面的代码所示。

print(dt.isoformat())
#return '2023-05-04T10:30:00+06:55'

print(dt.isocalendar()) 
#return tuple of datetime.IsoCalendarDate(year=2023, week=18, weekday=4)

datetime.fromisoformat("2023-01-05")
#return datetime.datetime(2023, 1, 5, 0, 0)

datetime.fromisoformat('2011-11-04T00:05:23')
#return datetime.datetime(2011, 11, 4, 0, 5, 23)

datetime.fromisoformat('2011-11-04 00:05:23.283')
#return datetime.datetime(2011, 11, 4, 0, 5, 23, 283000)

datetime.fromisoformat('2011-11-04 00:05:23.283+00:00')
#return datetime.datetime(2011, 11, 4, 0, 5, 23, 283000, tzinfo=datetime.timezone.utc)

datetime.fromisoformat('2011-11-04T00:05:23+04:00')
#return datetime.datetime(2011, 11, 4, 0, 5, 23, tzinfo=datetime.timezone(datetime.timedelta(seconds=14400))) 

让我们进一步深入最后一行代码。

  • 2011-11-04:日期组件,表示 2011 年 11 月 4 日。

  • T:一个分隔符字符,表示时间组件的开始。

  • 00:05:23:时间组件,表示 12:05:23am。

  • +04:00:时区偏移量组件,表示与协调世界时(UTC)相差 4 小时,方向为正(比 UTC 早)。

使用strftime(format)格式化datetime对象,以及使用**strptime**(format)解析字符串为datetime对象。

在 Python 中,你可以使用**strftime(format)**方法将datetime 对象转换为字符串。你只需提供一个字符串,告诉它你希望字符串的格式。相反,你也可以使用**strptime(input_string, input_format)**方法将字符串解析为 datetime 对象

格式字符串可以包含格式代码文字字符的组合。格式代码是由符号%表示的特殊字符序列,替换为datetime对象中的相应值。文字字符按原样包含在结果字符串中。以下是 Python 用户指南中常见格式代码的列表¹。

  • %Y:四位数字表示的年份。

  • %m:零填充的十进制数字表示的月份(01-12)。

  • %d:零填充的十进制数字表示的日期(01-31)。

  • %H:零填充的十进制数字表示的小时(00-23)。

  • %M:零填充的十进制数字表示的分钟(00-59)。

  • %S:零填充的十进制数字表示的秒(00-59)。

  • %a:缩写的星期几名称(周日、周一、周二等)。

  • %A:完整的星期几名称(星期日、星期一、星期二等)。

  • %b:缩写的月份名称(1 月、2 月、3 月等)。

  • %B:完整的月份名称(1 月、2 月、3 月等)。

  • %p:AM/PM 标识(AM 或 PM)。

from datetime import datetime
import pytz

tz = pytz.timezone('Asia/Singapore')
dt = datetime(2023, 5, 4, 10, 30, tzinfo=tz)

# Format as YYYY-MM-DD
# Output: 2023-05-04
print(dt.strftime('%Y-%m-%d'))

# Format as MM/DD/YYYY
# Output: 05/04/2023
print(dt.strftime('%m/%d/%Y'))   

# Format as Weekday, Month DD, YYYY HH:MM PM/AM
# Output: Thursday, May 04, 2023 10:30 AM
print(dt.strftime('%A, %B %d, %Y %I:%M %p'))

# .strptime() converts string to datetime object
# Output: 2023-05-04 20:30:45 with type datetime.datetime
dt_parsed = datetime.strptime("2023-05-04 20:30:45", '%Y-%m-%d %H:%M:%S')
print(dt_parsed)

# Another example of .strptime()
# Output: 2022-05-05 12:30:00
input_str = '5 May 2022 12:30 PM'
input_format = '%d %B %Y %I:%M %p'
dt = datetime.strptime(input_str, input_format)

timedelta对象

在 Python 中,**timedelta**对象表示两个日期或时间之间的持续时间或差异。它可以用于与datetime对象进行算术运算,例如加减时间间隔,或计算两个datetime对象之间的差异¹。

要创建一个 **timedelta** 对象,可以使用 **datetime.timedelta()** 构造函数,该函数接受一个或多个参数来指定持续时间。这些参数可以是整数或浮点数,表示天数、秒数、微秒数,或者它们的组合。例如,**timedelta(days=1, hours=3)** 创建了一个 **timedelta** 对象,表示一天和三小时。你可以对 **timedelta** 对象执行加法、减法、乘法和除法等算术操作,也可以使用比较运算符进行比较。例如,

from datetime import timedelta

delta_1 = timedelta(days=5, seconds=55, microseconds=555)
delta_2 = timedelta(days=1, seconds=11, microseconds=111)

print(delta_1.total_seconds()) #return 432055.000555
print(delta_1 - delta_2) #return 4 days, 0:00:44.000444
print(delta_1 > delta_2) #return True
print(delta_1 * 2) #return datetime.timedelta(days=10, seconds=110, microseconds=1110)
print(delta_1 / 2) #datetime.timedelta(days=2, seconds=43227, microseconds=500278)

POSIX 时间戳

POSIX 时间戳,也称为 Unix 时间戳或纪元时间戳,是一种以单一整数值表示时间的方法,这种方法便于比较和操作⁵。它在计算机系统和编程语言(如 Python)中被广泛使用。它表示从 1970 年 1 月 1 日 00:00:00 UTC 起的秒数,这被称为 Unix 纪元时间。由于它不受时区和夏令时 (DST) 的影响,因此在计算机系统中存储和操作日期和时间时非常有用。

from datetime import datetime

# create a datetime object for a specific date and time
dt1 = datetime(2023, 5, 4, 10, 30, 0)
dt2 = datetime(2023, 5, 4, 11, 30, 0)

# convert the datetime object to a POSIX timestamp
timestamp1 = dt1.timestamp()
timestamp2 = dt2.timestamp()

print(timestamp1)  # output: 1683171000.0
print(timestamp2)  # output: 1683174600.0
print(timestamp2 - timestamp1)  # output: 3600.0

请注意,时间戳之间的差异为 3600 秒,相当于一个小时的间隔。

类似地,我们也可以将 POSIX 时间戳转换为 datetime 对象。

from datetime import datetime

# create a POSIX timestamp for a specific date and time
timestamp = 1680658200.0

# convert the POSIX timestamp to a datetime object
dt = datetime.fromtimestamp(timestamp)

print(dt)  # output: 2023-04-05 08:30:00

Pandas,.dt 访问器和 datetime64[ns]

**datetime64[ns]** 数据类型是一种表示日期和时间的类型,其精度高达纳秒。它是 Python 中 NumPy 库的一部分,类似于 datetime 模块,但在处理大量日期和时间数据时表现更佳。这使得在处理大型数据集时更加高效,特别是与其他 NumPy 函数结合使用时,用于处理数组和矩阵。

在使用 Pandas 库时,datetime64[ns] 是一个非常好的数据类型,因为它允许我们访问强大的 dt 属性来处理 datetime 对象。注意,这个属性在 pd.Timestamp 对象中不可用,因此建议将数据类型转换为 datetime64[ns],以便更方便地操作 datetime 对象。让我们开始吧。我们将从 CSV 文件中导入包含 500 条记录的随机时间戳(包括两个城市:曼谷/新加坡)。默认情况下,数据类型将是 **str**。这一部分和代码风格受到《有效 Pandas》一书的启发⁶。

random_timestamp = pd.read_csv("random_timestamp.csv")
random_timestamp

供参考,曼谷的时间偏移为 GMT+7,而新加坡的时间偏移为 GMT+8。我们的目标是将所有时间转换为新加坡时间。处理 Pandas 中的 datetime 对象…

#Creating the offset
offset = np.where(random_timestamp.country == "Bangkok", "+07:00", "+08:00")
offset

#Convert data type using pd.to_datetime, groupby offset, convert to SG time
(pd
 .to_datetime(random_timestamp.timestamp)
 .groupby(offset)
 .transform(lambda s: s.dt.tz_localize(s.name)
                       .dt.tz_convert('Asia/Singapore'))
)

如果你对上一个方法的操作感到困惑,下面是详细说明:首先,datetime 系列或索引被假设为没有时区信息,即它不附加任何时区信息。

  1. .dt.tz_localize() 方法用于将时区附加到 datetime 系列或索引,这样它就会变成具有时区感知的。

  2. 该方法接受一个参数,即 datetime 系列或索引应该本地化到的时区。

  3. 一旦时区附加到 datetime 系列或索引上,你可以执行需要时区信息的 datetime 操作,例如将列转换为特定时区(即新加坡)。

左侧 vs 右侧输出: 未转换 vs 转换为特定时区

.dt 访问器允许我们检索 datetime 信息,如下所示

offset = np.where(random_timestamp.country == "Bangkok", "+07:00", "+08:00")
offset

(random_timestamp
 .assign(sg_time = (pd
                     .to_datetime(random_timestamp.timestamp)
                     .groupby(offset)
                     .transform(lambda s: s.dt.tz_localize(s.name)
                                           .dt.tz_convert('Asia/Singapore'))),
         sg_hour = lambda df_: df_.sg_time.dt.hour,
         sg_minute = lambda df_: df_.sg_time.dt.minute,
         sg_second = lambda df_: df_.sg_time.dt.second,
         sg_weekday = lambda df_: df_.sg_time.dt.weekday,
         sg_weekofyear = lambda df_: df_.sg_time.dt.isocalendar().week,
         sg_strftime = lambda df_: df_.sg_time.dt.strftime('%A, %B %d, %Y %I:%M %p'))
)

作为索引的日期,.resample(), .agg(), .transform() 方法

本节和编码风格受到《Effective Pandas⁶》一书的启发。假设我们有一个数据集,记录了一天中多个时间点的温度,如下所示。

我们希望对数据进行聚合,以找出一天中的最低、最高和平均温度。我们可以通过将 DataFrame 的索引设置为包含 **datetime** 对象的列,然后使用 **resample** 方法并指定聚合的频率来实现这一点。

(temp_record
 .set_index("timestamp")
 .resample('D')
 .agg(['min', 'max', 'mean'])
 .round(1)
)

.agg() 总结并缩减 DataFrame 的记录

如果我们希望保留行数而不是将其缩减为 7 天,我们可以使用方法 transform() 而不是 agg()。但是,请注意 transform() 不能接受一个聚合方法列表,而是限制为一次只能使用一个聚合方法。

(temp_record
 .set_index("timestamp")
 .resample('D')
 .transform('min')
 .round(1)
)

.transform() 保留了 DataFrame 的所有记录

后记

理解和操作 datetime 对象是任何数据分析师或科学家的关键技能。通过掌握各种类和方法,你可以从数据中解锁强大的洞察,并做出明智的决策。答应我,下次当你遇到带有日期和时间的数据集时,不要退缩——拥抱它,让 **datetime** 的魔力开始吧!

如果你从这篇文章中获得了一些有用的信息,请考虑在 Medium 上给我一个 关注。很简单,每周一篇文章,让你保持更新并保持领先!

你可以在 LinkedIn 上与我联系: https://www.linkedin.com/in/andreaslukita7/

参考资料:

  1. Python 文档: docs.python.org/3/library/datetime.html#timezone-objects

  2. 国家飓风中心和中太平洋飓风中心。什么是 UTC 或 GMT 时间? www.nhc.noaa.gov/aboututc.shtml#:~:text=Prior%20to%201972%2C%20this%20time,%22%20or%20%22Zulu%20Time%22.

  3. 国家标准与技术研究院。什么是 USNO 时间或 UTC(USNO)? www.nist.gov/pml/time-and-frequency-division/nist-time-frequently-asked-questions-faq#:~:text=USNO%20has%20an%20ensemble%20of,scale%20called%20UTC(USNO).

  4. 国际标准化组织。ISO 8601 日期和时间格式。 www.iso.org/iso-8601-date-and-time-format.html

  5. UNIX 时间。 unixtime.org/

  6. Matt Harrison 的《有效 Pandas》: store.metasnake.com/effective-pandas-book

TimeGPT: 第一个用于时间序列预测的基础模型

原文:towardsdatascience.com/timegpt-the-first-foundation-model-for-time-series-forecasting-bf0a75e63b3a

探索第一个生成式预训练预测模型,并在 Python 项目中应用它

Marco PeixeiroTowards Data Science Marco Peixeiro

·发布于 Towards Data Science ·阅读时间 12 分钟·2023 年 10 月 24 日

--

图片由 Boris Smokrovic 提供,来源于 Unsplash

时间序列预测领域正经历一个非常激动人心的时期。在过去三年里,我们见证了许多重要的贡献,比如 N-BEATSN-HiTSPatchTSTTimesNet

与此同时,大型语言模型(LLMs) 近年来获得了极大的关注,例如 ChatGPT,因为它们可以在没有进一步训练的情况下适应各种任务。

这引出了一个问题:基础模型是否可以像自然语言处理一样存在于时间序列中?一个在大量时间序列数据上预训练的大模型是否可以对未见数据做出准确预测?

通过 TimeGPT-1,由 Azul Garza 和 Max Mergenthaler-Canseco 提出,作者将 LLM 的技术和架构适配到预测领域,成功构建了第一个能够进行零样本推理的时间序列基础模型。

在这篇文章中,我们首先探讨了 TimeGPT 背后的架构以及模型的训练方式。然后,我们将其应用于一个预测项目,以评估其与其他先进方法(如 N-BEATS、N-HiTS 和 PatchTST)的表现。

欲了解更多细节,请务必阅读 原始论文

使用我的 免费时间序列速查表 学习最新的时间序列分析技术,全部用 Python 实现!获得统计和深度学习技术的实现,全都用 Python 和 TensorFlow!

开始吧!

探索 TimeGPT

如前所述,TimeGPT 是创建时间序列预测基础模型的首次尝试。

TimeGPT 如何被训练以对未见数据进行推断的示意图。图片来自 TimeGPT-1 的 Azul Garza 和 Max Mergenthaler-Canseco

从上图可以看出,TimeGPT 的总体思路是通过在来自不同领域的大量数据上训练模型,从而对未见数据进行零样本推断。

当然,这种方法依赖于 迁移学习,即模型利用在训练过程中获得的知识来解决新任务的能力。

现在,这只有在模型足够大且在大量数据上训练时才有效。

训练 TimeGPT

为此,作者在超过 1000 亿个数据点上训练了 TimeGPT,这些数据点都来自开源时间序列数据。数据集涵盖了广泛的领域,包括金融、经济和天气、网页流量、能源和销售。

请注意,作者未披露用于整理 1000 亿数据点的公共数据来源。

这种多样性对于基础模型的成功至关重要,因为它可以学习不同的时间模式,从而实现更好的泛化。

例如,我们可以预期天气数据会有日周期性(白天比夜晚更热)和年度季节性,而汽车交通数据则可能具有日周期性(白天路上的车更多)和周周期性(工作日路上的车更多)。

为了确保模型的稳健性和泛化能力,预处理保持在最低限度。实际上,仅填充了缺失值,其余数据保持原始状态。虽然作者没有具体说明数据填补的方法,但我怀疑使用了一些插值技术,如线性插值、样条插值或移动平均插值。

模型随后在多天的时间里进行了训练,在此期间对超参数和学习率进行了优化。虽然作者未透露训练所需的天数和 GPU 数量,但我们知道该模型是用 PyTorch 实现的,并使用了 Adam 优化器和学习率衰减策略。

TimeGPT 的架构

TimeGPT 利用基于 Google 和多伦多大学 2017 年开创性工作的自注意力机制的 Transformer 架构。

TimeGPT 的架构。输入序列以及外生变量被送入 Transformer 的编码器,解码器随后生成预测。图像来自 TimeGPT-1 的 Azul Garza 和 Max Mergenthaler-Canseco。

从上图中,我们可以看到 TimeGPT 使用了完整的编码器-解码器 Transformer 架构。

输入可以包括一段历史数据窗口以及外生数据,如特定事件或其他序列。

输入被送入模型的编码器部分。编码器内部的注意力机制然后学习来自输入的不同特性。这些信息随后被送入解码器,解码器利用学到的信息来生成预测。当然,预测序列会在达到用户设置的预测范围长度时结束。

重要的是要注意,作者在 TimeGPT 中实现了符合预测功能,使模型能够根据历史误差估计预测区间。

TimeGPT 的能力

考虑到 TimeGPT 是首次尝试构建时间序列的基础模型,它具备了广泛的功能。

首先,由于 TimeGPT 是一个预训练模型,这意味着我们可以在不专门在我们的数据上训练它的情况下生成预测。不过,也可以对模型进行微调以适应我们的数据。

其次,该模型支持外生变量来预测我们的目标,并且可以处理多变量预测任务。

最终,通过使用符合预测,TimeGPT 可以估计预测区间。这反过来允许模型执行异常检测。基本上,如果一个数据点落在 99% 置信区间之外,模型就会将其标记为异常。

请记住,所有这些任务都可以通过零样本推理或一些微调来实现,这对时间序列预测领域来说是一次根本性的范式转变。

现在我们对 TimeGPT 有了更扎实的理解,了解了它的工作原理和训练方式,让我们看看模型的实际表现。

使用 TimeGPT 进行预测

现在,让我们将 TimeGPT 应用于一个预测任务,并将其表现与其他模型进行比较。

请注意,在撰写本文时,TimeGPT 仅通过 API 访问,并处于封闭测试阶段。我提交了申请,并获得了为期两周的免费访问模型权限。要获取令牌并访问模型,你必须访问他们的 网站

如前所述,该模型在来自公开数据的 1000 亿数据点上进行训练。由于作者没有指定实际使用的数据集,我认为在已知的基准数据集上测试模型是不合理的,例如 ETTweather,因为模型在训练过程中可能已经见过这些数据。

因此,我编制并开源了我自己的数据集用于本文。

具体来说,我整理了从 2020 年 1 月 1 日到 2023 年 10 月 12 日我博客的每日浏览量。我还添加了两个外生变量:一个是标记新文章发布的日子,另一个是标记美国的假期日子,因为我的大多数观众都在美国。

数据集现已在 GitHub 上公开,并且最重要的是,我们确定 TimeGPT 没有在此数据上进行训练。

像往常一样,你可以在 GitHub 上访问完整的笔记本。

导入库并读取数据

自然的第一步是导入实验所需的库。

import pandas as pd
import numpy as np
import datetime
import matplotlib.pyplot as plt

from neuralforecast.core import NeuralForecast
from neuralforecast.models import NHITS, NBEATS, PatchTST

from neuralforecast.losses.numpy import mae, mse

from nixtlats import TimeGPT

%matplotlib inline

然后,为了访问 TimeGPT 模型,我们从文件中读取 API 密钥。请注意,我没有将 API 密钥分配给环境变量,因为访问仅限于两周。

with open("data/timegpt_api_key.txt", 'r') as file:
        API_KEY = file.read()

然后,我们可以读取数据。

df = pd.read_csv('data/medium_views_published_holidays.csv')
df['ds'] = pd.to_datetime(df['ds'])

df.head()

我们数据集的前五行。图片由作者提供。

从上图中,我们可以看到数据集的格式与我们使用其他来自 Nixtla 的开源库时相同。

我们有一个 unique_id 列来标记不同的时间序列,但在我们的情况下,我们只有一个序列。

y 代表我博客上的每日浏览量,published 是一个简单的标志,用于标记一天是否有新文章发布(1)或没有文章发布(0)。直观上,我们知道当发布新内容时,浏览量通常会在一段时间内增加。

最后,列 is_holiday 指示是否为美国假期。直观上,假期期间访问我博客的人较少。

现在,让我们可视化数据并寻找明显的模式。

published_dates = df[df['published'] == 1]

fig, ax = plt.subplots(figsize=(12,8))

ax.plot(df['ds'], df['y'])
ax.scatter(published_dates['ds'], published_dates['y'], marker='o', color='red', label='New article')
ax.set_xlabel('Day')
ax.set_ylabel('Total views')
ax.legend(loc='best')

fig.autofmt_xdate()

plt.tight_layout()

我博客上的每日浏览量。图片由作者提供。

从上图中,我们已经可以看到一些有趣的行为。首先,注意红点表示新发布的文章,它们几乎立即伴随着访问量的峰值。

我们还注意到 2021 年的活动较少,这在我博客的每日浏览量减少中体现出来。最后,在 2023 年,我们注意到在文章发布后访问量出现一些异常峰值。

放大数据后,我们还发现了明显的每周季节性。

我的博客每日浏览量。在这里,我们看到明显的每周季节性,周末访问人数较少。图片由作者提供。

从上图中,我们现在可以看到周末来博客的访客比工作日少。

了解了这些之后,让我们看看如何利用 TimeGPT 进行预测。

使用 TimeGPT 进行预测

首先,将数据集拆分为训练集和测试集。在这里,我会保留 168 个时间步作为测试集,这对应于 24 周的每日数据。

train = df[:-168]
test = df[-168:]

然后,我们使用七天的预测范围,因为我有兴趣预测一整周的每日浏览量。

目前,API 没有交叉验证的实现。因此,我们创建了自己的循环,每次生成七个预测,直到我们对整个测试集都有预测。

future_exog = test[['unique_id', 'ds', 'published', 'is_holiday']]

timegpt = TimeGPT(token=API_KEY)

timegpt_preds = []

for i in range(0, 162, 7):

    timegpt_preds_df = timegpt.forecast(
        df=df.iloc[:1213+i],
        X_df = future_exog[i:i+7],
        h=7,
        finetune_steps=10,
        id_col='unique_id',
        time_col='ds',
        target_col='y'
    )

    preds = timegpt_preds_df['TimeGPT']

    timegpt_preds.extend(preds)

在上面的代码块中,请注意我们必须传递外生变量的未来值。这是可以的,因为它们是静态变量。我们知道假期的未来日期,博客作者也知道他计划发布文章的时间。

同样注意,我们使用finetune_steps参数对 TimeGPT 进行微调以适应我们的数据。

一旦循环完成,我们可以将预测添加到测试集中。再次,TimeGPT 每次生成七个预测,直到获得 168 个预测,以便我们评估其预测下周每日浏览量的能力。

test['TimeGPT'] = timegpt_preds

test.head()

TimeGPT 的预测。图片由作者提供。

使用 N-BEATS、N-HiTS 和 PatchTST 进行预测

现在,让我们应用其他方法,看看是否专门在我们的数据集上训练这些模型可以产生更好的预测。

对于这个实验,如前所述,我们使用了 N-BEATS、N-HiTS 和 PatchTST。

horizon = 7

models = [NHITS(h=horizon,
               input_size=5*horizon,
               max_steps=50),
         NBEATS(h=horizon,
               input_size=5*horizon,
               max_steps=50),
         PatchTST(h=horizon,
                 input_size=5*horizon,
                 max_steps=50)]

然后,我们初始化NeuralForecast对象,并指定数据的频率,在这种情况下是每日。

nf = NeuralForecast(models=models, freq='D')

接着,我们在 24 个 7 时间步的窗口上进行交叉验证,以获得与 TimeGPT 使用的测试集对齐的预测。

preds_df = nf.cross_validation(
    df=df, 
    static_df=future_exog , 
    step_size=7, 
    n_windows=24
)

然后,我们可以将 TimeGPT 的预测简单地添加到这个新的preds_df DataFrame 中,以便拥有一个包含所有模型预测的单一 DataFrame。

preds_df['TimeGPT'] = test['TimeGPT']

包含所有模型预测的 DataFrame。图片由作者提供。

太好了!我们现在准备评估每个模型的性能。

评估

在测量性能指标之前,让我们可视化每个模型在测试集上的预测。

可视化每个模型的预测。图片由作者提供。

首先,我们看到每个模型之间有很多重叠。然而,我们确实注意到 N-HiTS 预测了两个在现实中未实现的峰值。此外,PatchTST 似乎经常低估预测。然而,TimeGPT 似乎总体上与实际数据重叠得相当好。

当然,评估每个模型性能的唯一方法是测量性能指标。在这里,我们使用均值绝对误差(MAE)和均方误差(MSE)。此外,我们将预测结果四舍五入为整数,因为在博客的日常访问者上下文中,小数没有意义。

preds_df = preds_df.round({
    'NHITS': 0,
    'NBEATS': 0,
    'PatchTST': 0,
    'TimeGPT': 0
})

data = {'N-HiTS': [mae(preds_df['NHITS'], preds_df['y']), mse(preds_df['NHITS'], preds_df['y'])],
       'N-BEATS': [mae(preds_df['NBEATS'], preds_df['y']), mse(preds_df['NBEATS'], preds_df['y'])],
       'PatchTST': [mae(preds_df['PatchTST'], preds_df['y']), mse(preds_df['PatchTST'], preds_df['y'])],
       'TimeGPT': [mae(preds_df['TimeGPT'], preds_df['y']), mse(preds_df['TimeGPT'], preds_df['y'])]}

metrics_df = pd.DataFrame(data=data)
metrics_df.index = ['mae', 'mse']

metrics_df.style.highlight_min(color='lightgreen', axis=1)

每个模型的性能指标。在这里,TimeGPT 是冠军模型,因为它实现了最低的 MAE 和 MSE。图片由作者提供。

从上图中,我们可以看到 TimeGPT 是冠军模型,因为它实现了最低的 MAE 和 MSE,紧随其后的是 N-BEATS、PatchTST 和 N-HiTS。

这是一个令人兴奋的结果,因为 TimeGPT 从未见过这个数据集,只进行了少量的微调。虽然这不是一个详尽的实验,但我相信它确实展示了基础模型在预测领域的潜力。

我对 TimeGPT 的个人看法

尽管我对 TimeGPT 的短期实验令人兴奋,但我必须指出,原始论文 在许多重要领域仍然模糊。

再次强调,我们不知道用于训练和测试模型的数据集,因此无法真正验证 TimeGPT 的性能结果,如下所示。

TimeGPT 的性能结果如 原始论文 所述,由 Azul Garza 和 Max Mergenthaler-Canseco 提供。

从上表中可以看出,TimeGPT 在月度和每周频率下表现最佳,N-HiTS 和 Temporal Fusion Transformer (TFT) 通常排名第 2 或第 3。再者,由于我们不知道使用了什么数据,我们无法验证这些指标。

关于模型的训练方式以及如何适应时间序列数据,也缺乏透明度。

我相信该模型旨在商业使用,这解释了为什么论文缺乏重现 TimeGPT 的细节。这并没有错,但论文缺乏可重现性对科学界来说是个问题。

尽管如此,我希望这能激发在时间序列领域基础模型的新工作和研究,并且我们最终能看到这些模型的开源版本,就像我们看到 LLM 的情况一样。

结论

TimeGPT 是第一个用于时间序列预测的基础模型。

它利用了 Transformer 架构,并在 1000 亿数据点上进行了预训练,以对新的未见数据进行零样本推理。

结合符合预测技术,该模型可以生成预测区间并执行异常检测,而无需在特定数据集上进行训练。

我仍然相信每个预测问题需要独特的方法,因此请确保测试 TimeGPT 以及其他模型。

感谢阅读!希望你喜欢这篇文章,并学到了新东西!

想要掌握时间序列预测吗?那就来看看我的课程Python 中的应用时间序列预测。这是唯一一个通过 Python 实现统计学、深度学习和最先进模型的课程,包含 16 个指导性的实践项目。

干杯 🍻

支持我

喜欢我的工作吗?通过请我喝咖啡来支持我,这是一种简单的鼓励方式,我可以享受一杯咖啡!如果你愿意,只需点击下面的按钮 👇

参考文献

TimeGPT-1 由 Azul Garza 和 Max Mergenthaler-Canseco 编写

气候变化时间序列:极端天气事件预测

原文:towardsdatascience.com/times-series-for-climate-change-forecasting-extreme-weather-events-335dc199fb6f

如何利用时间序列分析和预测应对气候变化

Vitor CerqueiraTowards Data Science Vitor Cerqueira

·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 5 月 15 日

--

图片由DESIGNECOLOGIST提供,来自Unsplash

这是《气候变化时间序列》系列的第五部分。文章列表:

极端天气事件

气候变化导致极端天气事件数量不断增加。这些事件对人类生命和基础设施构成了重大风险。

美国每年的冰雹和雷暴风事件数量。数据来源。图片由作者提供。

极端天气事件可能导致人员伤亡,并且成本高达数十亿美元。这些财务影响源于基础设施或农业资源的破坏。这些事件对受影响地区的社会经济发展有持久的影响。

各种极端天气事件的年度估算成本。数据来源。图片由作者提供。

极端天气事件包括多种天气现象。这些现象包括飓风、龙卷风、洪水、干旱、冰雹和热带风暴。下图显示了自 1950 年以来美国 20 种最常见的事件类型。

自 1950 年以来,美国 20 种最常见的极端天气事件类型。数据来源。图片作者提供。

准确预测的需求

准确的极端天气事件预测在我们适应气候变化影响中发挥了关键作用。

及时发布警报可以让人们撤离或保护自己及其资产。应急团队可以更有效地调动资源,减少事件的影响。

然而,预测极端天气事件是一项困难的任务。

一个问题是数据访问。极端天气事件是大气、海洋和陆地许多因素相互作用的结果。这些因素可能难以建模,或者可能不易获得。此外,极端天气事件本质上是稀有的。机器学习模型往往在数据集分布不平衡的情况下表现不佳——即,当某个类别稀少时

另一个困难是天气条件可能迅速变化。这意味着对未来有很大的不确定性。预测模型必须传达不确定性以便有效沟通。这个方面对政策制定者和公众来说都很重要。

应对这些挑战是减缓气候变化影响的关键一步。

实践:预测佛罗里达的极端天气事件

在本文的其余部分,我们将建立一个模型来预测美国佛罗里达的冰雹和雷暴事件。冰雹是由雷暴引起的冰冻降水。

本教程中使用的完整代码可以在 GitHub 上找到:

数据集

我们使用 NOAA 收集的数据集,描述了自 1950 年以来发生在美国的风暴事件[1]。数据包括以下信息:

  • 事件类型(例如冰雹、龙卷风);

  • 位置(坐标及相应州);

  • 日期和时间;

  • 估计的成本。

这里是关于佛罗里达风暴事件的数据样本:

风暴事件数据集的样本。图片作者提供。

风暴检测和追踪通常使用基于遥感的方法。但我们也可以基于气象数据建模极端天气事件。我们将使用由智能浮标捕获的气象数据来预测即将发生的冰雹或雷暴事件。这些数据也是由 NOAA 收集的[2]。

几个智能浮标被安置在佛罗里达海岸。截图来自这里

读取数据

我们可以直接从 NOAA 读取浮标数据,如下所示:

import re

import urllib3
import numpy as np
import pandas as pd

# Subset of stations on the coast of Florida
STATION_LIST = ['41009', 'SPGF1', 'VENF1',
                '42036', 'SAUF1', 'FWYF1',
                'LONF1', 'SMKF1']

# URL template
URL = 'https://www.ndbc.noaa.gov/view_text_file.php?filename={station_id}h{year}.txt.gz&dir=data/historical/stdmet/'

def read_buoy_remote(station_id: str, year: int):
    TIME_COLUMNS = ['YYYY', 'MM', 'DD', 'hh']

    # formatting the URL
    file_url = URL.format(station_id=station_id.lower(), year=year)

    http = urllib3.PoolManager()

    # get request
    response = http.request('GET', file_url)

    # decoding
    lines = response.data.decode().split('\n')

    # lots of data cleaning below
    data_list = []
    for line in lines:
        line = re.sub('\s+', ' ', line).strip()
        if line == '':
            continue
        line_data = line.split(' ')
        data_list.append(line_data)

    df = pd.DataFrame(data_list[2:], columns=data_list[0]).astype(float)
    df[(df == 99.0) | (df == 999.0)] = np.nan

    if 'BAR' in df.columns:
        df = df.rename({'BAR': 'PRES'}, axis=1)

    if '#YY' in df.columns:
        df = df.rename({'#YY': 'YYYY'}, axis=1)

    if 'mm' in df.columns:
        TIME_COLUMNS += ['mm']

    df[TIME_COLUMNS] = df[TIME_COLUMNS].astype(int)

    if 'mm' in df.columns:
        df['datetime'] = \
            pd.to_datetime([f'{year}/{month}/{day} {hour}:{minute}'
                            for year, month, day, hour, minute in zip(df['YYYY'],
                                                                      df['MM'],
                                                                      df['DD'],
                                                                      df['hh'],
                                                                      df['mm'])])

    else:
        df['datetime'] = \
            pd.to_datetime([f'{year}/{month}/{day} {hour}:00'
                            for year, month, day, hour in zip(df['YYYY'],
                                                              df['MM'],
                                                              df['DD'],
                                                              df['hh'])])

    df = df.drop(TIME_COLUMNS, axis=1)

    df.set_index('datetime', inplace=True)

    return df

我们从 8 个浮标中收集了以下变量:

  • 风速;

  • 波高;

  • 大气压力;

  • 水温;

  • 平均波周期。

这是一个浮标提供的数据样本:

来自智能浮标的气象数据样本。图片由作者提供。

建立数据集

我们需要合并这两个数据源以创建用于训练预测模型的数据集。

在每个时间步,我们从浮标获取最近的气象数据值。这些数据用作解释变量。目标变量是一个二元变量,表示在接下来的 12 小时内是否会发生冰雹或雷暴事件。

import numpy as np
import pandas as pd

from config import ASSETS, OUTPUTS

PART = 'Part 5'
assets = ASSETS[PART]

# focusing on hail and thunderstorm events
TARGET_EVENTS = ['Hail', 'Thunderstorm Wind']
# wind speed, wave height, pressure, water temp, avg wave period
METEOROLOGICAL_DATA = ['WSPD', 'WVHT', 'PRES', 'WTMP', 'APD']

# using past 4 hours as explanatory variables
N_LAGS = 4
# forecasting events in the next 12 hours
HORIZON = 12

# loading storm events data
storms = pd.read_csv(f'{assets}/storms_data.csv', index_col='storm_start')
storms.index = pd.to_datetime(storms.index)
hail_df = storms.loc[storms['EVENT_TYPE'].isin(TARGET_EVENTS), :]

# loading the meteorological data
buoys = pd.read_csv(f'{assets}/buoys.csv', index_col='datetime')
buoys.index = pd.to_datetime(buoys.index).tz_localize('UTC')
buoys['STATION'] = buoys['STATION'].astype(str)
# resampling the data to an hourly granularity
buoys_h = buoys.groupby('STATION').resample('H').mean()
# subsetting the variables
buoys_h = buoys_h[METEOROLOGICAL_DATA]
buoys_df = buoys_h.reset_index('STATION')

# getting all unique time steps
base_index = buoys_df.index.unique()

# getting list of stations
station_list = buoys_df['STATION'].unique().tolist()

X, y = [], []
# iterating over each time step
for i, dt in enumerate(base_index[N_LAGS + 1:]):

    features_by_station = []
    # iterating over each buoy station
    for station_id in station_list:

        # subsetting the data by station and time step (last n_lags observations)
        station_df = buoys_df.loc[buoys_df['STATION'] == station_id]
        station_df = station_df.drop('STATION', axis=1)
        station_df_i = station_df[:dt].tail(N_LAGS)

        if station_df_i.shape[0] < N_LAGS:
            break

        # transforming lags into features
        station_timestep_values = []
        for col in station_df_i:
            series = station_df_i[col]
            series.index = [f'{station_id}({series.name})-{i}'
                            for i in list(range(N_LAGS, 0, -1))]

            station_timestep_values.append(series)

        station_values = pd.concat(station_timestep_values, axis=0)

        features_by_station.append(station_values)

    if len(features_by_station) < 1:
        continue

    # combining features from all stations
    feature_set_i = pd.concat(features_by_station, axis=0)

    X.append(feature_set_i)

    # determining the target variable
    # whether an extreme weather events in the next HORIZON hours
    td = (hail_df.index - dt)
    td_hours = td / np.timedelta64(1, 'h')
    any_event_within = pd.Series(td_hours).between(0, HORIZON)

    y.append(any_event_within.any())

# combining all data points
X = pd.concat(X, axis=1).T
y = pd.Series(y).astype(int)

建立模型

从机器学习的角度来看,预测任务是一个稀有事件二元分类问题。此外,我们需要一个概率模型来有效传达不确定性。

我们将使用 LightGBM 算法来建立模型。其参数可以使用 Optuna 优化:

import optuna

import lightgbm as lgb
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split

# objective function for optuna
def objective(trial, X, y):

    train_x, valid_x, train_y, valid_y = \
        train_test_split(X, y, test_size=0.2, shuffle=False)

    dtrain = lgb.Dataset(train_x, label=train_y)

    param = {
        'objective': 'binary',
        'metric': 'binary_logloss',
        'verbosity': -1,
        'boosting_type': 'gbdt',
        'linear_tree': True,
        'lambda_l1': trial.suggest_float('lambda_l1', 1e-8, 10.0, log=True),
        'lambda_l2': trial.suggest_float('lambda_l2', 1e-8, 10.0, log=True),
        'num_leaves': trial.suggest_int('num_leaves', 2, 256),
        'feature_fraction': trial.suggest_float('feature_fraction', 0.4, 1.0),
        'bagging_fraction': trial.suggest_float('bagging_fraction', 0.4, 1.0),
        'bagging_freq': trial.suggest_int('bagging_freq', 1, 7),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
    }

    gbm = lgb.train(param, dtrain)
    preds = gbm.predict(valid_x)

    # optimizing for AUC
    auc = roc_auc_score(valid_y, preds)

    return auc

def optimize_params(X, y, n_trials: int):
    func = lambda trial: objective(trial, X, y)

    # auc should be maximized
    study = optuna.create_study(direction='maximize')
    study.optimize(func, n_trials=n_trials)

    # getting best parameter setup
    trial = study.best_trial

    return trial.params

# train/test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=False)

# optimization
params = optimize_params(X_train, y_train, n_trials=100)

# retraining with best parameters
dtrain = lgb.Dataset(X_train, label=y_train)
gbm = lgb.train(params, dtrain)

# inference
preds = gbm.predict(X_test)

这导致 AUC 分数为 0.78。

模型的 ROC 曲线,AUC 为 0.78。图片由作者提供。

AUC 分数表明我们可以基于浮标数据检测极端天气事件。我们可以做几件事来改进这个模型:

  • 更好地选择浮标。我选择了一些靠近海岸但彼此距离较远的浮标。然而,不同的浮标组合可能会更好;

  • 额外的气象变量。我们使用了从每个浮标捕获的 5 个变量,但其他变量也可能有价值。例如,来自卫星的云层覆盖信息。

主要收获

  • 极端天气事件对人类生命和基础设施构成了重大风险;

  • 气候变化与极端天气事件的增加有关。因此,预测极端天气事件对我们适应气候变化至关重要;

  • 我们可以基于智能浮标收集的气象数据来建模事件的发生;

  • 基于 LightGBM 的概率分类器可以以不错的 AUC 分数检测极端天气事件;

  • 更好的特征工程过程可能有助于改进这个模型。

感谢阅读,我们下个故事见!

参考文献

[1] 风暴事件数据库,数据来源于 www.ncdc.noaa.gov/stormevents/ftp.jsp (许可:公有领域)

[2] 国家数据浮标中心,数据来源于 www.ndbc.noaa.gov/ (许可:公有领域)

[3] McGovern, Amy, 等. “利用人工智能改善高影响天气的实时决策。” 美国气象学会公报 98.10 (2017): 2073–2090。

[4] Ramachandra, Vikas. “利用浮标数据和机器学习预测天气事件的严重性。” arXiv 预印本 arXiv:1911.09001 (2019)。

TimesNet:时间序列预测的最新进展

原文:towardsdatascience.com/timesnet-the-latest-advance-in-time-series-forecasting-745b69068c9c

了解 TimesNet 架构,并使用 Python 应用于预测任务

Marco PeixeiroTowards Data Science Marco Peixeiro

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

--

图片由Rachel Hisko提供,来源于Unsplash

在之前的文章中,我们探索了最新的最先进预测技术,分别是 2020 年发布的N-BEATS、2022 年的N-HiTS以及 2023 年 3 月的PatchTST。请注意,N-BEATS 和 N-HiTS 依赖于多层感知机架构,而 PatchTST 则利用了 Transformer 架构。

截至 2023 年 4 月,文献中发布了一种新模型,它在时间序列分析的多个任务中实现了最先进的结果,如预测、填补、分类和异常检测:TimesNet

TimesNet 由吴、胡、刘等人在他们的论文中提出:TimesNet:时间序列分析的时间 2D-变异建模

与之前的模型不同,它使用基于 CNN 的架构,在不同任务中实现了最先进的结果,使其成为时间序列分析的基础模型的优秀候选者。

在这篇文章中,我们探讨了 TimesNet 的架构和内部工作原理。接着,我们将模型应用于预测任务,并与 N-BEATS 和 N-HiTS 一起完成我们的小实验。

一如既往,更多详细信息请参阅原始论文

使用我的 免费时间序列备忘单 在 Python 中学习最新的时间序列分析技术!获取统计和深度学习技术的实现,全部在 Python 和 TensorFlow 中!

让我们开始吧!

探索 TimesNet

TimesNet 的动机来自于意识到许多现实生活中的时间序列展示了多周期性。这意味着在不同的周期中会发生变化。

例如,外部温度既有日周期也有年周期。通常,白天比夜晚热,夏天比冬天热。

现在,这些多个周期相互重叠并相互作用,使得单独分离和建模变得困难。

因此,TimesNet 的作者建议将序列重塑为二维空间,以建模周期内变化周期间变化

回到我们的天气示例,周期内变化将是温度在一天内的变化,而周期间变化将是温度从一天到另一日,或从一年到另一年的变化。

牢记这些,让我们深入探讨模型的架构。

TimesNet 的架构

让我们看看 TimesNet 的架构。

TimesNet 的架构。图像由 Haixu Wu, Tengge Hu, Yong Liu, Hang Zhou, Jianmin Wang 和 Mingsheng Long 提供,来自TimesNet: Temporal 2D-Variation Modeling For General Time Series Analysis

从上面的图中,我们可以看到 TimesNet 是由多个TimesBlock堆叠而成,并具有残差连接。

在每个 TimesBlock 中,我们可以看到序列首先经过快速傅里叶变换(FTT)以找到数据中的不同周期。然后,将其重塑为二维向量并发送到 Inception 块,在那里它学习和预测序列的二维表示。然后,这种深度表示必须通过自适应聚合重新变形回一维向量。

这里有很多内容需要了解,所以让我们更详细地介绍每个步骤。

捕捉多周期性

为了捕捉时间序列中多个周期的变化,作者建议将一维序列转换为二维空间,以同时建模周期内变化和周期间变化。

TimesNet 的架构。图像由 Haixu Wu, Tengge Hu, Yong Liu, Hang Zhou, Jianmin Wang 和 Mingsheng Long 提供,来自TimesNet: Temporal 2D-Variation Modeling For General Time Series Analysis

在上面的图中,我们可以看到模型如何在二维空间中表示变化。在红色矩形内,我们可以看到周期内变化,即数据在一个周期内的变化。然后,蓝色矩形包含周期间变化,即数据从一个周期到另一个周期的变化。

为了更好地理解这一点,假设我们有带有周周期的日数据。周期间变化指的是数据从周一到周二,再到周三等的变化情况。

然后,周期间变化指的是数据如何从第 1 周的周一变化到第 2 周的周一,从第 1 周的周二变化到第 2 周的周二。换句话说,它是同一相位在不同周期中的数据变化。

这些变化随后被表示在 2D 空间中,其中周期间变化是垂直的,而周期内变化是水平的。这使得模型能够更好地学习数据变化的表示。

虽然一维向量显示相邻点之间的变化,但这种 2D 表示法显示了相邻点和相邻周期之间的变化,提供了更完整的情况。

然而,仍有一个问题:我们如何找到序列中的周期?

确定数据中的显著周期

为了识别时间序列中的多个周期,模型应用了快速傅里叶变换(FTT)。

这是一种数学操作,将信号转换为频率和幅度的函数。

模型如何应用 FTT 以找到时间序列中的前k个显著周期。图片由 Haixu Wu, Tengge Hu, Yong Liu, Hang Zhou, Jianmin Wang 和 Mingsheng Long 提供,来自TimesNet: Temporal 2D-Variation Modeling For General Time Series Analysis

上图中,作者展示了 FTT 的应用方式。一旦我们得到了每个周期的频率和幅度,幅度最大的周期被认为是最相关的。

例如,这是对Etth1 数据集应用 FTT 的结果。

对 Etth1 数据集应用 FTT 的结果。我们可以看到,日周期和年周期是最显著的。图片由作者提供。

上图中的快速傅里叶变换使我们能够快速识别数据中的日周期和年周期,因为我们在这些周期上看到更高的幅度峰值。

一旦应用 FTT,用户可以设置参数k以选择前-k个最重要的周期,这些周期是幅度最大的。

TimesNet 随后为每个周期创建 2D 向量,并将其发送到 2D 卷积核,以捕捉时间变化。

在 TimesBlock 内部

一旦序列经过傅里叶变换,并为前-k周期创建了 2D 张量,数据将被发送到下图所示的 Inception 模块。

TimesNet 的架构。图片由 Haixu Wu, Tengge Hu, Yong Liu, Hang Zhou, Jianmin Wang 和 Mingsheng Long 提供,来自TimesNet: Temporal 2D-Variation Modeling For General Time Series Analysis

当然,请注意我们探索的所有步骤都是在 TimesBlock 内部进行的。

现在,数据的 2D 表示被送入Inception块。

Inception 模块是计算机视觉模型GoogLeNet的构建块,该模型于 2015 年发布。

Inception 模块的主要思想是通过保持数据稀疏来实现高效表示。这样,我们可以在技术上增加神经网络的大小,同时保持计算效率。

这是通过执行各种卷积和池化操作实现的,然后将所有内容连接在一起。在 TimesNet 的背景下,这就是 Inception 模块的样子。

TimesNet 中的 Inception 模块。图像由作者提供。

你可能会想知道为什么作者选择了一个视觉模型来处理时间序列数据。

对这个问题的简单回答是视觉模型在解析 2D 数据(如图像)方面特别擅长。

另一个好处是可以在 TimesNet 内部更换视觉骨干。虽然作者使用了 Inception 块,但可以更换为其他视觉模型骨干,因此 TimesNet 也可以从计算机视觉的进展中受益。

现在,将 Inception 模块从 TimesNet 与 GoogLeNet 中的 Inception 模块区分开来的一个元素是使用了自适应聚合

自适应聚合

要进行聚合,必须先将 2D 表示重塑为 1D 向量。

然后,使用自适应聚合,因为不同周期的幅度不同,这表明它们的重要性。

这就是为什么 FTT 的输出也会被送到 softmax 层,以便根据每个周期的相对重要性进行聚合。

聚合数据是单个 TimesBlock 的输出。然后,多个 TimesBlock 通过残差连接堆叠在一起,创建 TimesNet 模型。

现在我们了解了 TimesNet 模型是如何工作的,让我们在预测任务中测试它,并与 N-BEATS 和 N-HITS 进行比较。

使用 TimesNet 进行预测

现在,让我们在预测任务中应用 TimesNet 模型,并与 N-BEATS 和 N-HiTS 的表现进行比较。

对于这个小实验,我们使用了Etth1 数据集,该数据集在创作共用授权下发布。

这是一个在文献中广泛使用的时间序列预测的流行基准。它跟踪电力变压器的每小时油温,反映了设备的状况。

你可以在GitHub上访问数据集和代码。

导入库并读取数据

我们首先导入所需的库。在这里,我们使用了 Nixtla 提供的NeuralForecast的实现。

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

from neuralforecast.core import NeuralForecast
from neuralforecast.models import NHITS, NBEATS, TimesNet

from neuralforecast.losses.numpy import mae, mse

然后,我们可以读取我们的 CSV 文件。

df = pd.read_csv('data/etth1.csv')

df['ds'] = pd.to_datetime(df['ds'])

df.head()

Etth1 数据集的前五行。注意数据集已经具有 neuralforecast 期望的格式。图片由作者提供。

在上图中,请注意数据集已经具有 NeuralForecast 期望的格式。基本上,该包需要三列:

  • 一个标记为ds的日期列

  • 一个标记为unique_id的 ID 列来标记你的系列

  • 一个标记为y的值列

然后,我们可以绘制我们的系列。

fig, ax = plt.subplots()

ax.plot(df['y'])
ax.set_xlabel('Time')
ax.set_ylabel('Oil temperature')

fig.autofmt_xdate()
plt.tight_layout()

可视化 Etth1 数据集中的每小时油温。图片由作者提供。

现在,我们可以继续进行预测。

预测

对于我们的实验,我们使用 96 小时的预测范围,这是文献中长期预测的常见范围。

我们还保留两个 96 时间步的窗口来评估我们的模型。

首先,我们定义一个要用来进行预测任务的模型列表。再次,我们将使用 N-BEATS、N-HiTS 和 TimesNet。

我们将保持所有模型的默认参数,并将最大训练轮次限制为 50。请注意,默认情况下,TimesNet 将选择数据中最重要的前 5 个周期。

horizon = 96

models = [NHITS(h=horizon,
               input_size=2*horizon,
               max_steps=50),
         NBEATS(h=horizon,
               input_size=2*horizon,
               max_steps=50),
         TimesNet(h=horizon,
                 input_size=2*horizon,
                 max_steps=50)]

完成后,我们可以用我们的模型列表和数据的频率(即每小时)来实例化NeuralForecasts对象。

nf = NeuralForecast(models=models, freq='H')

然后,我们进行交叉验证,以获得预测和数据集的实际值。这样,我们可以评估每个模型的性能。

再次,我们使用两个 96 时间步的窗口进行评估。

preds_df = nf.cross_validation(df=df, step_size=horizon, n_windows=2)

预测 DataFrame 的前五行。图片由作者提供。

一旦模型训练完成并进行预测,我们会得到上述DataFrame。我们可以看到实际值以及之前指定的每个模型的预测结果。

我们也可以轻松地将预测与实际值进行可视化。

fig, ax = plt.subplots()

ax.plot(preds_df['y'], label='actual')
ax.plot(preds_df['NHITS'], label='N-HITS', ls='--')
ax.plot(preds_df['NBEATS'], label='N-BEATS', ls=':')
ax.plot(preds_df['TimesNet'], label='TimesNet', ls='-.')

ax.legend(loc='best')
ax.set_xlabel('Time steps')
ax.set_ylabel('Oil temperature')

fig.autofmt_xdate()
plt.tight_layout()

可视化每个模型在测试集上的预测。这里,所有模型都未能预测到测试集中观察到的下降。图片由作者提供。

在上图中,所有模型似乎都未能预测到测试集中观察到的油温下降。此外,N-BEATS 和 N-HiTS 捕捉到了一些在 TimesNet 的预测中未观察到的周期性模式。

仍然,我们需要通过计算 MSE 和 MAE 来评估模型,以确定哪个模型最好。

评估

现在,我们只需计算 MAE 和 MSE,以找出哪个模型表现最佳。

data = {'N-HiTS': [mae(preds_df['NHITS'], preds_df['y']), mse(preds_df['NHITS'], preds_df['y'])],
       'N-BEATS': [mae(preds_df['NBEATS'], preds_df['y']), mse(preds_df['NBEATS'], preds_df['y'])],
       'TimesNet': [mae(preds_df['TimesNet'], preds_df['y']), mse(preds_df['TimesNet'], preds_df['y'])]}

metrics_df = pd.DataFrame(data=data)
metrics_df.index = ['mae', 'mse']

metrics_df.style.highlight_min(color='lightgreen', axis=1)

每个模型在 Etth1 数据集上的长期预测任务的性能指标。图片由作者提供。

从上图可以看出,N-HiTS 达到了最低的 MAE,而 N-BEATS 达到了最低的 MSE。

不过,MAE 的差异为 0.002,MSE 的差异为 0.00025。由于 MSE 的差异非常小,特别是考虑到误差是平方的,我认为 N-HiTS 是这个任务的冠军模型。

所以,事实证明 TimesNet 并未达到最佳性能。不过,请记住,我们只是进行了一个简单的实验,没有进行超参数优化,只在一个数据集上,且仅在两个 96 时间步长的窗口上进行。

这真的旨在向你展示如何使用 TimesNet 和 NeuralForecast,以便你可以在解决下一个预测问题时有更多工具可用。

结论

TimesNet 是一个基于 CNN 的模型,利用 Inception 模块在许多时间序列分析任务上实现了最先进的性能,如预测、分类、填补和异常检测。

一如既往,每个预测问题都需要独特的方法和特定的模型,因此请确保测试 TimesNet 以及其他模型。

感谢阅读!希望你喜欢它,并学到了新的东西!

想掌握时间序列预测?那就看看我的课程Python 中的应用时间序列预测。这是唯一一个通过 16 个指导性动手项目使用 Python 实现统计、深度学习和最先进模型的课程。

干杯 🍻

支持我

喜欢我的工作吗?通过给我买杯咖啡来支持我,这是鼓励我的一种简单方式,我也能享受一杯咖啡!如果你愿意的话,只需点击下面的按钮 👇

参考文献

Haixu Wu, Tengge Hu, Yong Liu, Hang Zhou, Jianmin Wang 和 Mingsheng Long — TimesNet:用于一般时间序列分析的时序二维变化建模

Christian Szegedy, Wei Liu, Yangqing Jia, Pierre Sermanet, Scott Reed,

Dragomir Anguelov, Dumitru Erhan, Vincent Vanhoucke, Andrew Rabinovich — 深入卷积网络

Tiny Audio Diffusion:无需云计算的波形扩散

原文:towardsdatascience.com/tiny-audio-diffusion-ddc19e90af9b?source=collection_archive---------3-----------------------#2023-06-29

由 Stable Diffusion 生成

探索如何在消费者级笔记本电脑和 VRAM 小于 2GB 的 GPU 上训练模型并生成声音,通过音频波形扩散实现。

Christopher LandschootTowards Data Science Christopher Landschoot

·

关注 发表在 Towards Data Science · 11 分钟阅读 · 2023 年 6 月 29 日

--

背景

扩散模型目前非常流行,特别是自从 Stable Diffusion 在这个夏天掀起热潮之后。此后,已经发布了无数种扩散模型的变体和新模型,涵盖了各种不同的背景。虽然令人惊叹的视觉效果抢占了风头,但在生成音频方面也有了显著的进展。

在扩散和其他方法的推动下,生成音乐取得了许多最近的胜利,因为新的模型不断发布。OpenAI 在 2020 年发布的Jukebox让世界惊叹不已。然而,当 Google 在今年年初推出了卓越的MusicLM时,却让人们惊呼“让我来展示我的模型”。Meta 也紧随其后,上个月发布并开源了MusicGen。但大型机构并不是唯一参与者,独立研究者如Riffusion(Forsgren & Martiros)和Moûsai(Schneider 等)也做出了非常有趣的贡献。除此之外,过去几年还发布了许多其他模型,每个模型都有其优缺点。

扩散模型由于其非凡的创造力吸引了众多关注;这是许多其他机器学习(ML)领域所缺乏的。大多数 ML 模型被训练来执行特定任务,其成功可以通过正确与错误来衡量。但当我们进入艺术和音乐的领域时,如何优化模型以达到可能被认为是最佳的效果?当然,它可以学习重现著名的艺术作品或音乐,但没有新颖性是没有意义的。那么如何解决这个问题——将创造力注入到只懂得 1 和 0 的机器中呢?扩散是一种为这个难题提供优雅解决方案的方法。

扩散 — 从 10,000 英尺的视角

从本质上讲,ML 中的扩散只是从信号中添加或移除噪声的过程(想象一下旧电视的静态噪声)。正向扩散向信号添加噪声,而反向扩散则移除噪声。我们最熟悉的过程是反向扩散过程,在这个过程中,模型接收噪声,然后将其“去噪”成人类能够识别的东西(艺术、音乐、语音等)。这个过程可以通过多种方式进行操作,以服务于不同的目的。

扩散中的“创造力”来自于启动去噪过程的随机噪声。如果你每次都为模型提供不同的起点,以去噪成某种形式的艺术或音乐,这就模拟了创造力,因为输出结果总是独一无二的。

使用稳定扩散生成的图像

教授模型执行这种去噪过程的方法从最初的想法来看可能有点反直觉。模型实际上通过做完全相反的事情来学习去噪——不断地将噪声添加到干净的信号中,直到只剩下噪声。其思路是,如果模型能够学习如何预测每一步中添加的噪声,那么它也可以预测每一步中去除的噪声以进行反向过程。使这一切成为可能的关键因素是,添加/去除的噪声需要具有定义好的概率分布(通常是高斯分布),以便去噪/去噪步骤是可预测和可重复的。

这个过程涉及的细节远不止这些,但这应该能提供一个关于背后发生了什么的合理概念。如果你有兴趣了解更多关于扩散模型(数学公式、调度、潜在空间等)的内容,我建议阅读AssemblyAI 的这篇博客文章以及这些论文(DDPMImproving DDPMDDIMStable Diffusion)。

微小音频扩散

理解音频以进行机器学习

我对扩散的兴趣源于它在生成音频方面展示的潜力。传统上,为了训练机器学习算法,音频会被转换成频谱图,这基本上是声音能量随时间变化的热图。这是因为频谱图表示与图像相似,计算机在处理图像方面表现出色,而且与原始波形相比,数据大小大大减少。

声乐的示例频谱图

然而,这种转换带来了一些权衡,包括分辨率的降低和相位信息的丧失。音频信号的相位表示多个波形彼此之间的位置。这可以通过正弦函数和余弦函数之间的差异来展示。它们在幅度上表示完全相同的信号,唯一的区别是两者之间有一个 90°(π/2 弧度)的相位偏移。有关相位的更深入解释,请查看这个视频Akash Murthy提供。

sin与 cos 之间的 90°相位偏移

相位是一个始终具有挑战性的概念,即使对于从事音频工作的人也是如此,但它在创造声音的音色特质中发挥着关键作用。可以说,它不应该被如此轻易地丢弃。相位信息也可以像幅度一样在频谱图中表示(变换的复杂部分)。然而,结果是噪声较多,视觉上显得随机,使得模型很难从中学习到有用的信息。由于这一缺陷,最近有兴趣避免将音频转换为频谱图,而是将其保留为原始波形以训练模型。虽然这带来了自己的一系列挑战,但波形信号中包含了幅度和相位信息,为模型提供了一个更全面的声音学习视角。

主唱的波形示例

这是我对波形扩散兴趣的一个关键点,它在生成音频方面显示出了良好的前景。然而,波形是非常密集的信号,需要大量数据来表示人类可以听到的频率范围。例如,音乐行业标准采样率为 44.1kHz,这意味着需要 44,100 个样本来表示 1 秒钟的单声道音频。现在双倍考虑立体声播放。因此,大多数波形扩散模型(那些不利用潜在扩散或其他压缩方法的)需要高性能的 GPU(通常至少需要 16GB+ VRAM)来存储所有信息并进行训练。

动机

许多人没有高性能、大容量的 GPU,或者不愿意为个人项目支付租用云 GPU 的费用。发现自己处于这种情况,但仍然希望探索波形扩散模型,我决定开发一个可以在我微不足道的本地硬件上运行的波形扩散系统。

硬件设置

我使用的是 2017 年的 HP Spectre 笔记本电脑,配备了第 8 代 i7 处理器和 2GB VRAM 的 GeForce MX150 显卡——这并不算是训练机器学习模型的强大设备。我的目标是能够在这个系统上创建一个能够训练并生成高质量(44.1kHz)立体声输出的模型。

模型架构

我利用了 Archinet 的audio-diffusion-pytorch库来构建这个模型——感谢Flavio Schneider在处理这个他大部分构建的库时的帮助。

注意力 U-Net

基础模型架构由一个带有注意力块的 U-Net 组成,这在现代扩散模型中是标准配置。U-Net 是一个最初为图像(2D)分割开发的神经网络,但已经被改编为用于波形扩散的音频(1D)。U-Net 架构因其 U 形设计而得名。

U-Net(来源:U-Net: Convolutional Networks for Biomedical Image Segmentation (Ronneberger, et. al))*

与自编码器非常相似,U-Net 由编码器和解码器组成,还包含每一层的跳跃连接。这些跳跃连接是编码器和解码器对应层之间的直接连接,有助于将细粒度的细节从编码器传递到解码器。编码器负责捕捉输入信号的重要特征,而解码器负责生成新的音频样本。编码器逐渐减少输入音频的分辨率,在不同抽象层次上提取特征。然后解码器利用这些特征进行上采样,逐渐提高分辨率,以生成最终的音频样本。

注意力 U-Net(来源:注意力 U-Net: 学习如何查看胰腺Oktay, et al.))

这个 U-Net 还在较低层次包含自注意力块,这有助于保持输出的时间一致性。音频需要充分降采样,以维持在扩散过程中的采样效率,同时避免过载注意力块。模型利用了V-Diffusion,这是一种受到DDIM采样启发的扩散技术。

为了避免 GPU VRAM 耗尽,基础模型的训练数据长度需要较短。因此,我决定训练一次性鼓样本,因为它们的上下文长度固有地较短。经过多次迭代,确定基础模型长度为 32,768 个样本 @ 44.1kHz 立体声,相当于约 0.75 秒。这可能看起来特别短,但对于大多数鼓样本来说已经足够了。

变换

为了将音频降采样到足够适合注意力模块的程度,尝试了几种预处理变换。希望如果在训练模型之前能够在不丢失重要信息的情况下降采样音频数据,那么可以在不增加 GPU 内存负担的情况下最大化节点(神经元)和层数。

第一个尝试的变换是“补丁”版本。最初是为图像提出的,这一过程被调整为适用于我们的音频处理。输入音频样本按时间步长分组为块,然后转置为通道。该过程可以在 U-Net 的输出端逆转,将音频块恢复到其完整长度。然而,解块过程产生了别名问题,导致生成的音频中出现了不希望有的高频伪影。

第二种尝试的变换,由Schneider提出,被称为“学习变换”,它由单个卷积块组成,具有较大的卷积核尺寸和步幅,位于 U-Net 的开始和结束处。尝试了多种卷积核尺寸和步幅(16、32、64),并配合相应的模型变体来适当地降采样音频。然而,这仍然导致了生成音频中的混叠问题,尽管比打补丁变换少一些。

因此,我决定需要调整模型架构,以适应未经预处理变换的原始音频,从而产生足够质量的输出。

这需要扩展 U-Net 中的层数,以避免过快地降采样并丢失重要特征。在多次迭代后,最佳的架构仅在每层降采样 2 次。虽然这要求每层节点数量减少,但最终产生了最佳的结果。有关 U-Net 的确切层级、层数、节点、注意力特征等的详细信息,可以在 GitHub 上的tiny-audio-diffusion库中的配置文件中找到。

结论

预训练模型

我训练了 4 个独立的无条件模型,以生成踢鼓、军鼓、踩镲和打击乐(所有鼓声)。用于训练的数据集是我为音乐制作工作流程收集的小型免费单次样本(均为开源)。更大、更具多样性的数据集将提高每个模型生成输出的质量和多样性。这些模型根据每个数据集的大小进行了不同数量的步骤和周期训练。

预训练模型可以在Hugging Face上下载。请查看在Weights & Biases上记录的训练进度和输出样本。

结果

总体而言,尽管模型的规模较小,但输出质量仍然很高。然而,仍然存在一些轻微的高频“嘶嘶声”,这可能是由于模型的规模有限。这可以从下面波形中残留的少量噪音中看到。大多数生成的样本都很清晰,保持了瞬态和宽带音色特征。有时模型在样本的末尾添加额外的噪音,这可能是模型层数和节点数量限制的代价。

可以在这里收听来自模型的一些输出样本。每个模型的示例输出如下。

讨论

除了在本地硬件上探索波形扩散模型外,这个项目的重要目标之一是能够将这一机会分享给其他人。我希望为那些资源有限但希望尝试音频波形扩散的人提供一个简单的入口。因此,我将项目库结构化,提供了如何训练或微调自己的模型以及如何从Inference.ipynb笔记本生成新样本的逐步说明。

此外,我录制了一个Tutorial Video,展示了如何设置 Anaconda 环境,并演示了使用预训练模型生成独特样本的方法。

这是生成音频特别是扩散领域的激动人心的时刻。我在构建这个项目的过程中学到了很多,也进一步扩展了我对音频 AI 未来的乐观态度。我希望这个项目能对那些想探索音频 AI 世界的人有所帮助。

所有图片,除非另有说明,均由作者提供。

tiny-audio-diffusion 代码可以在这里找到: https://github.com/crlandsc/tiny-audio-diffusion

设置环境以使用 tiny-audio-diffusion 生成样本的教程视频:youtu.be/m6Eh2srtTro

我是一名专注于 AI/ML 和空间音频的音频科学家,同时也是一名终身音乐家。如果你对更多音频 AI 应用感兴趣,请参阅我最近的文章:音乐去混响

LinkedInGitHub上找到我,并通过这里的www.chrislandschoot.com跟进我当前的工作和研究。

SpotifyApple MusicYouTubeSoundCloud以及其他流媒体平台上以After August的名字找到我的音乐。

大型语言模型:TinyBERT — 为 NLP 蒸馏 BERT

原文:towardsdatascience.com/tinybert-1a928ba3082b?source=collection_archive---------1-----------------------#2023-10-21

解锁在 LLMs 中变换器蒸馏的力量

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·

关注 发布于 Towards Data Science ·8 分钟阅读·2023 年 10 月 21 日

--

介绍

最近几年,大型语言模型的演变迅速发展。BERT 成为最受欢迎且高效的模型之一,能够以高准确率解决广泛的 NLP 任务。BERT 之后,许多其他模型也相继出现,展示了同样出色的结果。

一个显而易见的趋势是,随着时间的推移,大型语言模型(LLMs)倾向于通过指数级增加其参数和训练数据的数量而变得更加复杂。深度学习研究表明,这种技术通常会带来更好的结果。不幸的是,机器学习领域已经遇到了一些关于 LLMs 的问题,而可扩展性已成为有效训练、存储和使用它们的主要障碍。

针对这个问题,专门制定了压缩 LLM 的方法。在本文中,我们将重点讨论 Transformer 提炼,这导致了一个名为 TinyBERT 的小型 BERT 版本的开发。此外,我们将了解 TinyBERT 的学习过程以及使 TinyBERT 如此强大的若干细节。本文基于官方的 TinyBERT 论文

主要观点

最近,我们已经讨论了 DistilBERT 中提炼的工作原理:简而言之,损失函数目标经过修改,以使学生和教师的预测相似。在 DistilBERT 中,损失函数比较了学生和教师的输出分布,并且还考虑了两个模型的输出嵌入(用于相似性损失)。

大型语言模型:DistilBERT — 更小、更快、更便宜、更轻量

解锁 BERT 压缩的秘密:最大效率的师生框架

[towardsdatascience.com

从表面上看,TinyBERT 中的提炼框架与 DistilBERT 没有太大变化:损失函数再次被修改,以使学生模仿教师。然而,在 TinyBERT 的情况下,它更进一步:损失函数不仅考虑了两个模型产生了什么,还考虑了预测是如何获得的。根据论文,TinyBERT 的损失函数由三个组成部分构成,涵盖了两个模型的不同方面:

  1. 嵌入层的输出

  2. 从 Transformer 层衍生的隐藏状态和注意力矩阵

3. 预测层输出的 logits

Transformer 提炼损失

比较两个模型的隐藏状态有什么意义? 包括隐藏状态和注意力的输出矩阵,使学生能够学习教师的隐藏层,从而构建类似于教师的层。这样,提炼出的模型不仅模仿原始模型的输出,还模仿其内部行为。

为什么复制教师的行为很重要? 研究人员声称,BERT 学到的注意力权重对于捕捉语言结构是有益的。因此,将其提炼到另一个模型中也为学生提供了更多获得语言知识的机会。

层映射

作为较小的 BERT 版本,TinyBERT 拥有更少的编码器层。我们定义 BERT 层的数量为 N,TinyBERT 的数量为 M。鉴于层数不同,计算提炼损失的方式并不明显。

为此,引入了一个特殊函数 n = g(m) 来定义哪个 BERT 层 n 用于将其知识蒸馏到 TinyBERT 的对应层 m。然后,选定的 BERT 层用于训练过程中的损失计算。

引入的函数 n = g(m) 有两个推理约束:

  • g(0) = 0。这意味着 BERT 中的嵌入层直接映射到 TinyBERT 中的嵌入层,这是有意义的。

  • g(M + 1) = N + 1。该方程表明 BERT 中的预测层映射到 TinyBERT 中的预测层。

对于所有其他 TinyBERT 层 1 ≤ m ≤ M,需要映射 n = g(m) 的对应函数值。现在,假设这种函数已定义。TinyBERT 的设置将在本文后续部分讨论。

Transformer 蒸馏

1. 嵌入层蒸馏

在原始输入传递到模型之前,它首先会被分词,然后映射到学习到的嵌入。这些嵌入被用作模型的第一层。所有可能的嵌入可以用矩阵的形式表示。为了比较学生和教师嵌入的差异,可以使用应用于它们各自嵌入矩阵 E 的标准回归度量。例如,transformer 蒸馏使用 MSE 作为回归度量。

由于学生和教师的嵌入矩阵大小不同,不能通过 MSE 元素级别地比较它们。这就是为什么学生嵌入矩阵要乘以一个可学习的权重矩阵 W,使得结果矩阵与教师嵌入矩阵具有相同的形状。

嵌入层蒸馏损失

由于学生和教师的嵌入空间不同,矩阵 W 在将学生的嵌入空间线性转换为教师的嵌入空间中也发挥着重要作用。

2. Transformer 层蒸馏

Transformer 层蒸馏损失可视化

2A. 注意力层蒸馏

从本质上讲,Transformer 中的多头注意力机制产生了包含丰富语言知识的多个注意力矩阵。通过从教师那里转移注意力权重,学生也可以理解重要的语言概念。为了实现这个想法,损失函数用于计算学生和教师注意力权重之间的差异。

在 TinyBERT 中,所有注意力层都被考虑,且每层的最终损失值等于所有头部的学生和教师注意力矩阵之间 MSE 值的总和。

注意力层蒸馏损失

用于注意力层蒸馏的注意力矩阵 A 是未经归一化的,而不是它们的 softmax 输出 softmax(A)。根据研究人员的说法,这种细微差别导致了更快的收敛和更好的性能。

2B. 隐藏层蒸馏

遵循捕捉丰富语言知识的思想,蒸馏也应用于变换器层的输出。

隐藏层蒸馏损失

权重矩阵 W 扮演着与嵌入层蒸馏中描述的相同角色。

3. 预测层蒸馏

最后,为了使学生能够再现教师的输出,考虑预测层损失。它包括计算两个模型预测的 logits 向量之间的交叉熵。

预测层蒸馏损失

有时,logits 被温度参数 T 除以,T 控制输出分布的平滑度。在 TinyBERT 中,温度 T 设置为 1。

损失方程

在 TinyBERT 中,根据其类型,每一层都有自己的损失函数。为了赋予某些层更多或更少的重要性,相应的损失值会乘以一个常数 a。最终损失函数等于所有 TinyBERT 层损失值的加权总和。

TinyBERT 中的损失函数

在众多实验中,结果显示,三种损失组件中,变换器层蒸馏损失对模型性能的影响最大。

训练

重要的是要注意,大多数 NLP 模型(包括 BERT)都是分两个阶段开发的:

  1. 模型在大规模数据集上预训练以获得语言结构的一般知识。

  2. 模型在另一个数据集上微调以解决特定的下游任务。

按照相同的范式,研究人员开发了一个框架,其中 TinyBERT 的学习过程也包括两个阶段。在这两个训练阶段中,Transformer 蒸馏用于将 BERT 知识转移到 TinyBERT 上。

  1. 通用蒸馏。TinyBERT 从预训练的 BERT(未微调)中获得丰富的一般语言结构知识,BERT 充当教师。通过使用较少的层和参数,经过此阶段后,TinyBERT 的表现通常不如 BERT。

  2. 任务特定蒸馏。这一次,经过微调的 BERT 充当教师角色。为了进一步提升性能,研究人员提出在训练数据集上应用数据增强方法。结果显示,在任务特定蒸馏后,TinyBERT 在性能上达到了与 BERT 相当的水平。

训练过程

数据增强

为任务特定蒸馏制定了一种特殊的数据增强技术。该技术包括从给定数据集中提取序列,并用两种方式之一替换一定比例的词:

  • 如果词被标记化为相同的词,那么这个词将由 BERT 模型预测,预测的词将替换序列中的原始词。

  • 如果单词被分割成几个子词,则这些子词将被替换为最相似的 GloVe 嵌入。

尽管模型大小大幅减少,但所描述的数据增强机制对 TinyBERT 的性能产生了很大影响,使其能够学习更多样化的例子。

增强示例

模型设置

TinyBERT 仅有 14.5M 参数,约为 BERT base 的 7.5 倍更小。其详细比较见下图:

BERT base 与 TinyBERT 比较

对于层映射,作者提出了一种 统一策略,根据该策略,层映射函数将每个 TinyBERT 层映射到每个第三个 BERT 层:g(m) = 3 * m。也研究了其他策略(如采用所有底层或顶层 BERT 层),但统一策略显示出最佳结果,这似乎是合乎逻辑的,因为它允许从不同的抽象层转移知识,使得转移的信息更加多样化。

不同的层映射策略。性能结果显示在 GLUE 数据集上。

说到训练过程,TinyBERT 在英语维基百科(2500M 词)上进行训练,并且大多数超参数与 BERT base 相同。

结论

Transformer 精简是自然语言处理中的一大步。考虑到基于 Transformer 的模型目前是机器学习中最强大的模型之一,我们可以通过应用 Transformer 精简来进一步珍惜它们。最好的例子之一是 TinyBERT,它是从 BERT base 压缩了 7.5 倍。

尽管参数减少如此之多,实验表明 TinyBERT 的性能与 BERT base 相当:TinyBERT 在 GLUE 基准测试中获得了 77.0% 的分数,与 BERT 的 79.5% 分数相差不远。显然,这是一个惊人的成就!最后,像量化或剪枝等其他流行的压缩技术也可以应用于 TinyBERT,使其变得更小。

资源

除非另有说明,否则所有图片均由作者提供

升级到 PyTorch 2.0 的技巧和窍门

原文:towardsdatascience.com/tips-and-tricks-for-upgrading-to-pytorch-2-3127db1d1f3d?source=collection_archive---------3-----------------------#2023-05-21

迁移到全新 “编译模式” 时需要注意的事项

Chaim RandTowards Data Science Chaim Rand

·

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

--

图片由 Mohamed Nohassi 提供,刊登在 Unsplash

作者

任何新的 AI 开发框架、AI 加速器或 AI 计算平台的发布,都可能带来运行时优化和成本降低,从而改善我们的 AI 开发生命周期。最近发布的 PyTorch 2.0 也不例外。PyTorch 2.x 的亮点之一是引入了torch.compile,据报道,PyTorch 2.x 可以显著加速训练和推理过程。与我们熟悉的 PyTorch 逐步执行模式不同,在该模式下每个 PyTorch 操作都是“急切地”运行,compile API 将你的模型转换为中间计算图(FX 图),然后将其编译成适合底层训练加速器的低级计算内核,使用如 内核融合乱序执行 等技术(有关更多详细信息,请参见这里)。

在这篇文章中,我们将展示这一令人兴奋的新特性及使用过程中可能遇到的一些问题和行为。你可能已经看到一些帖子,突出了使用 torch 编译的便捷性或性能提升效果。或者(像我一样),你可能在过去的两周里一直在与新 API 作斗争,试图让其在你的模型上良好运作。实际上,对于许多公共模型,只需用 torch.compile 调用包裹它们即可(如这里所报道)。然而,正如我们将看到的那样,有许多因素可能干扰图的编译和/或达成期望的性能改进。调整你的模型和/或成功达到最佳性能可能需要你重新设计项目或修改一些编码习惯。

在开始之前,我们应该提到几件事。我们在这篇文章中的意图是分享一些我们在适应 torch.compile API 过程中遇到的问题示例。这些示例绝非全面。你可能会遇到本文未提及的问题。还要记住,torch.compile 仍在积极开发中。我们所写的内容可能在你阅读时已经不再适用。务必保持最新,关注最新的发布和文档。

在 torch 编译中,存在许多创新技术,包括TorchDynamoFX GraphTorchInductorTriton等。虽然我们在此不会深入探讨这些不同的组件,但我们鼓励你从PyTorch 文档2022 年 PyTorch 大会或这篇有用的 TDS 帖子中了解它们。通常,对幕后发生的事情有一个好的理解可以帮助你弄清楚为什么模型没有编译成功以及如何解决这个问题。

本文绝不应被视为官方 PyTorch 文档的替代品(例如,这里)。本文也不应被视为对 PyTorch 相对于 TensorFlow(或其他 ML 训练框架)、编译模式相对于急切模式,或任何我们提到的工具、库或平台的认可。我发现所有框架都有其优缺点。我对任何特定框架没有强烈的偏好或热情。我的热情在于解决有趣的技术挑战——挑战越难越好——无论它们存在于何种平台或框架上。你可以说我对框架是中立的。尽管如此,请允许我对 PyTorch 和 TensorFlow 库如何随时间演变进行两个完全无关紧要的观察。可以跳过这些观察,直接回到正题。

TensorFlow 与 PyTorch 战争的两个完全无关紧要的观察

观察 1:在过去,当生活很简单时,PyTorch 和 TensorFlow 之间有明显的区别。PyTorch 使用急切执行模式,TensorFlow 使用图模式,大家都很满意,因为我们都知道自己在争论什么。但后来出现了 TensorFlow 2,它将急切执行作为默认执行模式,TensorFlow 变得有点像 PyTorch。现在,PyTorch 也推出了自己的图编译解决方案,变得有点像 TensorFlow。TensorFlow 与 PyTorch 的战争依然继续,但两者之间的差异正在慢慢消失。请参见这条推文,其中对 PyTorch 演变的评论我觉得很有趣。

观察 2:AI 开发是一项时尚的业务。与时尚行业类似,流行的 AI 模型、模型架构、学习算法、训练框架等都会随季节变化而变化。与时尚行业一样,AI 也有自己的出版物和会议,你可以通过这些途径跟上最新的趋势。直到几年前,我们大多数工作的模型都是用 TensorFlow 编写的。而人们对此不满。他们主要的两点抱怨是高层的 model.fit API 限制了他们的开发灵活性,以及图模式使他们无法进行调试。他们说:“我们必须转到 PyTorch”,因为“我们可以按照自己的方式构建模型并轻松调试”。几年的时间过去了,同样的人现在却在说:“我们必须适应 PyTorch Lightning(或其他高层 API),并且必须通过 torch.compile 加速训练”。要明确的是……我不是在评判。我只是想说,也许我们应该更加自我觉察。

回到实际内容

本文的其余部分组织成一系列关于如何开始使用 PyTorch 2 编译 API 的技巧以及你可能遇到的一些潜在问题。根据你项目的具体细节,将模型适配到 PyTorch 的图模式可能需要非同小可的努力。我们的希望是这篇文章能帮助你更好地评估这一努力,并决定采取最佳的步骤。

安装 PyTorch 2

PyTorch 安装文档 来看,安装 PyTorch 2 与安装其他版本的 PyTorch 没有什么不同。实际上,你可能会遇到一些问题。首先,PyTorch 2.0(截至本文撰写时)似乎需要 Python 3.8 或更高版本(见 这里)。希望你已经更新到最新的 Python 版本,这不会成为问题,但在不太可能(且不幸)的情况下你没有更新,这可能会成为你升级的另一个动机。此外,PyTorch 2 包含了一些之前版本中不存在的包依赖(最显著的是 pytorch-triton),这可能引入新的冲突。更有甚者,即使你成功构建了 PyTorch 2 环境,你可能会发现调用 torch.compile 会导致严重且完全无法解释的 段错误

节省麻烦的一种方法是使用一个预构建并经过验证的 PyTorch 2.0 Docker 镜像。在下面的示例中,我们将使用一个官方的 AWS Deep Learning Container 镜像,其中包含 PyTorch 2.0。具体来说,我们将使用763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-training:2.0.0-gpu-py310-cu118-ubuntu20.04-sagemaker 镜像,该镜像设计用于在 Amazon SageMaker 上的 GPU 实例进行训练,使用 Python 3.10 和 PyTorch 2.0。

向后兼容性

PyTorch 2 的一个好处是它完全向后兼容。因此,即使你选择继续使用 eager 执行模式而不使用 torch.compile,你仍然被强烈鼓励升级到 PyTorch 2.0 并从其他 新功能和增强 中受益。

玩具示例

让我们从一个图像分类模型的玩具示例开始。在下面的代码块中,我们使用 timm Python 包(版本 0.6.12)构建一个基本的 Vision Transformer (ViT) 模型,并在一个假数据集上训练 500 步。我们定义 use_compile 标志以控制是否进行模型编译(torch.compile),并定义 use_amp 以控制是否使用 自动混合精度 (AMP) 还是全精度 (FP)。

import time, os
import torch
from torch.utils.data import Dataset
from timm.models.vision_transformer import VisionTransformer

use_amp = True # toggle to enable/disable amp
use_compile = True # toggle to use eager/graph execution mode

# use a fake dataset (random data)
class FakeDataset(Dataset):
  def __len__(self):
    return 1000000

  def __getitem__(self, index):
    rand_image = torch.randn([3, 224, 224], dtype=torch.float32)
    label = torch.tensor(data=[index % 1000], dtype=torch.int64)
    return rand_image, label

def train():
  device = torch.cuda.current_device()
  dataset = FakeDataset()
  batch_size = 64

  # define an image classification model with a ViT backbone
  model = VisionTransformer()

  if use_compile:
    model = torch.compile(model)

  model.to(device)

  optimizer = torch.optim.Adam(model.parameters())
  data_loader = torch.utils.data.DataLoader(dataset,
                          batch_size=batch_size, num_workers=4)
  loss_function = torch.nn.CrossEntropyLoss()

  t0 = time.perf_counter()
  summ = 0
  count = 0

  for idx, (inputs, target) in enumerate(data_loader, start=1):
    inputs = inputs.to(device)
    targets = torch.squeeze(target.to(device), -1)

    optimizer.zero_grad()

    with torch.cuda.amp.autocast(
      enabled=use_amp,
      dtype=torch.bfloat16
    ):
      outputs = model(inputs)
      loss = loss_function(outputs, targets)

    loss.backward()
    optimizer.step()

    batch_time = time.perf_counter() - t0

    if idx > 10:  # skip first few steps
      summ += batch_time
      count += 1
    t0 = time.perf_counter()
    if idx > 500:
      break

  print(f'average step time: {summ/count}')

if __name__ == '__main__':
  train()

在下表中,我们展示了在使用ml.g5.xlarge实例类型和Amazon SageMaker 上运行训练脚本时的性能比较结果。模型编译的影响会因平台而异(例如,参见这里)。一般而言,现代服务器级 GPU 上的加速效果会更高。请记住,这些仅是您可能看到的结果类型示例。实际结果将高度依赖于项目的具体细节。

性能结果(按作者)

我们可以看到,使用AMP(28.6%)相较于使用 FP(4.5%),模型编译带来的性能提升显著。这是一个众所周知的差异(例如,参见这里)。如果您尚未使用 AMP 进行训练,您可能会发现从 FP 到 AMP 的过渡可以实现最显著的性能增益。我们还可以看到,在我们的模型案例中,性能提升伴随着 GPU 内存利用的轻微增加。

请注意,由于在编译图上实现分布式训练的方式,当扩展到多个 GPU 时,性能对比可能会发生变化。有关更多详细信息,请参见这里

高级编译选项

torch.compile API 包含多个选项,用于控制图的创建。这些选项使您能够针对您的特定模型进行精细调整编译,并可能进一步提升性能。下面的代码块包含了函数签名(来自这个source)。

def compile(model: Optional[Callable] = None, *,
            fullgraph: builtins.bool = False,
            dynamic: builtins.bool = False,
            backend: Union[str, Callable] = "inductor",
            mode: Union[str, None] = None,
            options: Optional[Dict[str, Union[str, builtins.int, builtins.bool]]] = None,
            disable: builtins.bool = False) -> Callable:
    """
    Optimizes given model/function using TorchDynamo and specified backend.

    Args:
       model (Callable): Module/function to optimize
       fullgraph (bool): Whether it is ok to break model into several subgraphs
       dynamic (bool): Use dynamic shape tracing
       backend (str or Callable): backend to be used
       mode (str): Can be either "default", "reduce-overhead" or "max-autotune"
       options (dict): A dictionary of options to pass to the backend.
       disable (bool): Turn torch.compile() into a no-op for testing
    """

编译模式:编译模式允许您选择减少编译所需开销(“reduce-overhead”)和最大化潜在性能提升(“max-autotune”)之间的权衡。有关更多详细信息,请参见这里

在下表中,我们比较了上述 ViT 模型在不同编译模式下的编译结果。

性能结果(作者提供)

我们可以看到编译模式的表现基本符合宣传,"reduce-overhead" 在额外内存利用的代价下减少了编译时间,而 "max-autotune" 在编译时间开销高的情况下实现了最大性能。

编译器后端:编译 API 允许您确定使用哪个后端将中间表示(IR)计算图( FX 图)转换为低级内核操作。这个选项对于调试图编译问题和更好地了解 torch.compile 的内部机制(如在这个有趣的示例中所示)非常有用。在大多数情况下(截至撰写本文时),默认的 TorchInductor 后端似乎提供了最佳的训练性能结果。请参见这里获取当前现有后端的列表,或者运行下面的代码查看您的环境中支持的后端。如果您愿意,也可以添加自己的后端 😃.

from torch import _dynamo
print(_dynamo.list_backends())

例如,通过修改上述代码以使用 nvprims-nvfuser后端,我们比急切模式获得了 13% 的性能提升(相比于默认后端的 28.6% 提升)。

强制单一图fullgraph标志是确保您没有任何不希望的图断裂的极其有用的控制。有关更多信息,请参见下文。

动态形状标志:截至撰写本文时,对具有动态形状的张量的编译支持仍然有限。编译具有动态形状的模型的常见副作用是过度重新编译,这可能显著增加开销并大幅减慢训练速度。如果您的模型确实包含动态形状,将dynamic标志设置为True将会带来更好的性能,特别是减少重新编译的次数。

性能分析

我们已经广泛讨论了(例如,这里)对训练性能进行分析的重要性,作为加速训练速度和降低成本的一种手段。我们用来分析 PyTorch 模型性能的关键工具之一是 PyTorch Profiler。PyTorch Profiler 允许我们评估和分析图编译如何优化训练步骤。在下面的代码块中,我们用 torch.profiler 包装了我们的训练循环,并为 TensorBoard 生成了结果。我们将输出保存在 SM_MODEL_DIR 中,该目录会在训练任务结束时自动上传到持久存储。

 out_path = os.path.join(os.environ.get('SM_MODEL_DIR','/tmp'),'profile')
  from torch.profiler import profile, ProfilerActivity
  with profile(
          activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
          schedule=torch.profiler.schedule(
            wait=20,
            warmup=5,
            active=10,
            repeat=1),
          on_trace_ready=torch.profiler.tensorboard_trace_handler(
                                                dir_name=out_path)

  ) as p:
    for idx, (inputs, target) in enumerate(data_loader, start=1):
      inputs = inputs.to(device)
      targets = torch.squeeze(target.to(device), -1)
      optimizer.zero_grad()

      with torch.cuda.amp.autocast(
        enabled=use_amp,
        dtype=torch.bfloat16
      ):
        outputs = model(inputs)
        loss = loss_function(outputs, targets)
      loss.backward()
      optimizer.step()
      p.step()

下图截取自 TensorBoard PyTorch Profiler 标签的 GPU 内核 视图。它提供了在编译模型试验的训练步骤中,运行在 GPU 上的内核的详细信息。

TensorBoard PyTorch Profiler 标签下的内核视图截图(作者提供)

通过将这些图表与急切执行运行的图表进行比较,我们可以看到图编译增加了 GPU 的 Tensor Cores 的使用率(从 51% 增加到 60%),并且引入了使用 Triton 开发的 GPU 内核。

诊断模型编译问题

PyTorch 编译仍在积极开发中(目前处于测试阶段),你在编译模型时遇到问题是完全有可能的。如果你运气好,你会得到一个有用的错误信息,并有一种简单(且合理)的解决办法。如果你运气不好,你可能需要更加努力地找出问题的根源,和/或得出结论:在目前的成熟度水平下,模型编译无法满足你的需求。

解决编译问题的主要资源是 TorchDynamo 故障排除页面,其中包含了调试工具的列表,并提供了 诊断错误 的逐步指南。不幸的是,撰写本文时,这些工具和技术似乎更多地面向 PyTorch 开发者,而非 PyTorch 用户。它们可以帮助找出编译问题的根本原因,提供一些关于如何绕过这些问题的提示,和/或将问题报告给 PyTorch。然而,你可能会发现它们在实际解决问题上并没有帮助。

在下面的代码块中,我们展示了一个简单的分布式模型,该模型包括对torch.distributed.all_reduce的调用。该模型在急切模式下按预期运行,但在图编译过程中(截至本文撰写时)失败,出现“属性错误”(torch.classes.c10d.ProcessGroup 没有名为 ‘shape’ 的字段)。通过将日志级别提高到INFO,我们发现错误发生在计算的“步骤 #3”中,即 TorchInductor。我们可以通过验证“急切”模式和“aot_eager”后端的编译成功来确认这一点。最后,我们可以创建一个最小的代码示例,使用PyTorch Minifier重现该错误。

import os, logging
import torch
from torch import _dynamo

# enable debug prints
torch._dynamo.config.log_level = logging.INFO
torch._dynamo.config.verbose=True

# uncomment to run minifier
# torch._dynamo.config.repro_after="aot"

def build_model():
  import torch.nn as nn
  import torch.nn.functional as F

  class DumbNet(nn.Module):
    def __init__(self):
      super().__init__()
      self.conv1 = nn.Conv2d(3, 6, 5)
      self.pool = nn.MaxPool2d(2, 2)
      self.fc1 = nn.Linear(1176, 10)

    def forward(self, x):
      x = self.pool(F.relu(self.conv1(x)))
      x = torch.flatten(x, 1)
      x = self.fc1(x)
      with torch.no_grad():
        sum_vals = torch.sum(x,0)
        # this is the problematic line of code
        torch.distributed.all_reduce(sum_vals)
      # add noise
      x = x + 0.1*sum_vals
      return x

  net = DumbNet()
  return net

def train():
  os.environ['MASTER_ADDR'] = os.environ.get('MASTER_ADDR',
                                             'localhost')
  os.environ['MASTER_PORT'] = os.environ.get('MASTER_PORT',
                                             str(2222))
  torch.distributed.init_process_group('nccl', rank=0,
                                         world_size=1)
  torch.cuda.set_device(0)
  device = torch.cuda.current_device()

  model = build_model()

  model = torch.compile(model)

  # replace with this to verfiy that error is not in TorchDynamo
  # model = torch.compile(model, 'eager')
  # replace with this to verfiy that error is not in AOTAutograd
  # model = torch.compile(model, 'aot_eager')

  model.to(device)

  rand_image = torch.randn([4, 3, 32, 32], dtype=torch.float32).to(device)

  model(rand_image)

if __name__ == '__main__':
  train()

遗憾的是,在我们的示例中,运行生成的 minifier_launcher.py 脚本会导致一个不同的属性错误(‘Repro’ 对象没有属性 ‘_tensor_constant0’),尽管整个过程很有趣,但记录的调试步骤在解决我们演示的编译问题时并没有太大帮助。

显然,我们希望你不会遇到任何编译问题。如果你遇到了,请知道:1. 你并不孤单 😃,2. 尽管它们可能与这里演示的问题不同,但按照故障排除指南中描述的步骤,可能会对它们的来源有所指示。

常见的图断裂

PyTorch 急切模式最受推崇的优势之一是能够将纯 Python 代码与 PyTorch 操作交错使用。不幸的是,(截至目前)当使用 torch.compile 时,这种自由度被显著限制。原因在于,某些 Python 操作会导致 TorchDynamo 将计算图拆分成多个组件,从而阻碍潜在的性能提升。你的目标应该是尽可能地减少图断裂。作为最佳实践,你可能会考虑在将模型迁移到 PyTorch 2 时使用fullgraph标志进行编译。这不仅会促使你移除任何导致图断裂的代码,还会教会你如何最佳地调整 PyTorch 开发习惯以适应图模式。然而,请注意,你需要禁用此标志以运行分布式代码,因为当前 GPU 之间的通信方式需要图断裂(例如,请参见此处)。或者,你可以使用torch._dynamo.explain 工具来分析图断裂,详细说明请参见这里

以下代码块展示了一个简单的模型,在其前向传递过程中有四个潜在的图断裂(截至目前)。在一个典型的 PyTorch 模型中,看到这些操作中的任何一个都不罕见。

import torch
from torch import _dynamo
import numpy as np

def build_model():
  import torch.nn as nn
  import torch.nn.functional as F

  class DumbNet(nn.Module):
    def __init__(self):
      super().__init__()
      self.conv1 = nn.Conv2d(3, 6, 5)
      self.pool = nn.MaxPool2d(2, 2)
      self.fc1 = nn.Linear(1176, 10)
      self.fc2 = nn.Linear(10, 10)
      self.fc3 = nn.Linear(10, 10)
      self.fc4 = nn.Linear(10, 10)
      self.d = {}

    def forward(self, x):
      x = self.pool(F.relu(self.conv1(x)))
      x = torch.flatten(x, 1)
      assert torch.all(x >= 0) # graph break
      x = self.fc1(x)
      self.d['fc1-out'] = x.sum().item() # graph break
      x = self.fc2(x)
      for k in np.arange(1): # graph break
        x = self.fc3(x)
      print(x)  # graph break
      x = self.fc4(x)
      return x

  net = DumbNet()
  return net

def train():
  model = build_model()
  rand_image = torch.randn([4, 3, 32, 32], dtype=torch.float32)
  explanation = torch._dynamo.explain(model, rand_image)
  print(explanation)

if __name__ == '__main__':
  train()

重要的是要强调,图断裂不会导致编译失败(除非设置了 fullgraph 标志)。因此,完全有可能你的模型在编译和运行时实际包含多个图断裂,这会导致性能下降。

训练问题排查

尽管成功编译模型是一项值得庆祝的成就,但这并不能保证训练一定成功。如上所述,运行在 GPU 上的低级内核在急切模式和图模式之间会有所不同。因此,某些高级操作可能会表现出不同的行为。特别是,你可能会发现急切模式下运行的操作在图模式下会失败(例如,我们遇到的这个 torch.argmin 失败)。或者,你可能会发现计算中的数值差异对你的训练产生影响。

更糟的是,图模式下的调试比急切模式下要困难得多。在急切模式中,每一行代码都是独立执行的,这使我们可以在代码的任何位置设置断点并评估当前张量值。另一方面,在图模式中,我们代码定义的模型在处理之前会经历多个转换,因此,您的断点可能不会被触发。

过去,我们扩展了图模式下调试的难点并提出了几种解决方法。当您遇到问题时,可以尝试以下两步方法。首先,恢复到急切模式,那里调试较少困难,并祈祷问题能够重现。如果没有,尝试在编译的计算图中评估感兴趣的中间张量,通过故意在模型中插入图断点来实现。您可以通过将模型明确地分成两个(或更多)部分并分别应用 torch.compile,或通过插入print和/或Tensor.numpy调用来生成图断点。根据您的操作方式,您甚至可能成功触发代码中的断点。然而,请记住,以这种方式拆分图形可能会修改低级操作的顺序,因此可能无法准确重现完全编译的图形执行。但它确实给您提供了更多的灵活性,以便深入探究问题。

如果您遇到编译模式和急切模式之间意外的差异,请参见准确性调试部分以及故障排除指南

将损失函数包含在图中

正如我们在上面的示例中所演示的,通过用 torch.compile 调用包装 PyTorch 模型(或函数)可以启用图执行模式。您可能已经观察到损失函数不在编译调用中,因此不在生成的图中。在许多情况下,包括我们展示的那些,损失函数是训练步骤中的一个相对小的部分,急切运行不会造成太多开销。然而,如果您有一个特别重的损失函数,您可以通过将其包含在编译的计算图中来进一步提升性能。例如,在下面的代码块中,我们定义了一个损失函数,用于(天真地)从一个大型 ViT 模型(具有 24 个 ViT 块)到一个较小的 ViT 模型(具有 12 个 ViT 块)进行模型蒸馏

import torch
from timm.models.vision_transformer import VisionTransformer

class ExpensiveLoss(torch.nn.Module):
  def __init__(self):
    super(ExpensiveLoss, self).__init__()
    self.expert_model = VisionTransformer(depth=24)
    if torch.cuda.is_available():
      self.expert_model.to(torch.cuda.current_device())
    self.mse_loss = torch.nn.MSELoss()

  def forward(self, input, outputs):
    expert_output = self.expert_model(input)
    return self.mse_loss(outputs, expert_output)

我们的实现包括一个在每个输入批次上调用大模型的损失函数。这是一个比上面提到的 CrossEntropyLoss 更加计算密集的损失函数,急切 运行它并不理想。

我们描述了解决这个问题的两种方法。第一种方法是将损失函数简单地包装在一个 torch.compile 调用中,如下所示:

loss_function = ExpensiveLoss()
compiled_loss = torch.compile(loss_function)

这种选项的缺点是,损失函数的编译图与模型的编译图不相交。第二种选项通过创建一个包含两者的包装模型并返回结果损失作为输出,来将模型和损失函数一起编译。此选项在下面的代码块中演示:

import time, os
import torch
from torch.utils.data import Dataset
from torch import nn
from timm.models.vision_transformer import VisionTransformer

# use a fake dataset (random data)
class FakeDataset(Dataset):
  def __len__(self):
    return 1000000

  def __getitem__(self, index):
    rand_image = torch.randn([3, 224, 224], dtype=torch.float32)
    label = torch.tensor(data=[index % 1000], dtype=torch.int64)
    return rand_image, label

# create a wrapper model for the ViT model and loss
class SuperModel(torch.nn.Module):
  def __init__(self):
    super(SuperModel, self).__init__()
    self.model = VisionTransformer()
    self.expert_model = VisionTransformer(depth=24 if torch.cuda.is_available() else 2)
    self.mse_loss = torch.nn.MSELoss()

  def forward(self, inputs):
    outputs = self.model(inputs)
    with torch.no_grad():
      expert_output = self.expert_model(inputs)
    return self.mse_loss(outputs, expert_output)

# a loss that simply passes through the model output
class PassthroughLoss(nn.Module):
  def __call__(self, model_output):
    return model_output

def train():
  device = torch.cuda.current_device()
  dataset = FakeDataset()
  batch_size = 64

  # create and compile the model
  model = SuperModel()
  model = torch.compile(model)

  model.to(device)

  optimizer = torch.optim.Adam(model.parameters())
  data_loader = torch.utils.data.DataLoader(dataset,
                          batch_size=batch_size, num_workers=4)

  loss_function = PassthroughLoss()

  t0 = time.perf_counter()
  summ = 0
  count = 0

  for idx, (inputs, target) in enumerate(data_loader, start=1):
    inputs = inputs.to(device)
    targets = torch.squeeze(target.to(device), -1)

    optimizer.zero_grad()

    with torch.cuda.amp.autocast(
      enabled=True,
      dtype=torch.bfloat16
    ):
      outputs = model(inputs)
      loss = loss_function(outputs)

    loss.backward()
    optimizer.step()

    batch_time = time.perf_counter() - t0

    if idx > 10:  # skip first few steps
      summ += batch_time
      count += 1
    t0 = time.perf_counter()
    if idx > 500:
      break

  print(f'average step time: {summ/count}')

if __name__ == '__main__':
  train()

这种方法的缺点是,当需要以 推理 模式运行模型时,内部模型需要从包装模型中提取出来。

在我们的案例中,两种选项都带来了大约 8% 的性能提升,展示了这种优化的重要性。当损失函数被急切运行时,总步长时间为 0.37 秒,而当损失函数被编译时,总步长时间为 0.34 秒。

动态形状

根据 文档 的报告,动态形状模型的编译支持有限(截至本文撰写时)。根据动态性的细节,动态模型可能会带来显著的性能开销,可能会引入图断裂和/或触发过多的 图重编译。图重编译发生在原始编译期间对模型所做的假设(称为 guards)被违反时。

torch.compile API 包含 动态 标志,用于指示编译器优化动态形状。然而,截至本文撰写时,这种优化的效果尚不明确。如果你在编译和优化动态图时遇到问题,可能需要等到支持水平成熟后再考虑使用这一功能。

摘要

PyTorch 2.0 编译模式具有显著提高训练和推理速度的潜力,因此可以实现显著的成本节约。然而,实现这一潜力所需的工作量可能差异很大。许多公共模型只需更改一行代码即可完成。其他模型,尤其是包含非标准操作、动态形状和/或大量交织的 Python 代码的模型,可能需要更多的努力。然而,现在可能是开始调整你的模型的最佳时机,因为编译模式似乎会长期存在。

在 Polars 中处理字符串的技巧与窍门

原文:towardsdatascience.com/tips-and-tricks-for-working-with-strings-in-polars-ec6bb74aeec2

从排序列名到拆分列

魏梦龙Towards Data Science 魏梦龙

·发布在 Towards Data Science ·阅读时长 9 分钟·2023 年 1 月 17 日

--

图片来源 Raphael SchallerUnsplash

在我过去关于 Polars 的文章中(medium.com/search?q=wei-meng+lee+polars),我深入探讨了如何开始使用 Polars,它的惰性计算模式如何帮助优化查询和提高处理大数据集的效率,以及如何利用它进行各种任务,如数据清理、数据分析和数据可视化。

我没有深入探讨的一个领域是字符串处理,这在处理数据框时是一个非常常见的话题。在本文中,我将介绍一些你可以在 Polars 中进行字符串处理时使用的技巧和方法。它们包括:

  • 排序 DataFrame 列

  • 计算字符串长度

  • 根据标题选择列

  • 使用正则表达式过滤行

  • 拆分字符串列

  • 替换字符串值

Polars 中的所有标题必须是字符串类型

在我们深入各种技巧和窍门之前,重要的是要记住,在 Polars 中,所有列标题都是字符串类型。请考虑以下示例:

import polars as pl
import numpy as npdf = pl.DataFrame(np.random.randint(0, 100, size=(10, 4)),  
                  columns=list('CDAB'))
df

上面的示例展示了一个具有四列的 Polars DataFrame。请注意,与 Pandas 不同,在 Pandas 中列标题可以是数字类型,而在 Polars 中,所有列标题必须是字符串类型。以下是不允许的:

df = pl.DataFrame(np.random.randint(0, 100, size=(10, 4)), 
                  columns=[1,2,3,4])  # error

如果你真的想使用数字作为列标题,你需要将它们转换为字符串:

df = pl.DataFrame(np.random.randint(0, 100, size=(10, 4)), 
                  columns=list('1234'))

列排序

要排序 Polars DataFrame 中的列,首先使用 Python 的 sorted() 函数对列名进行排序,然后使用 select() 函数重新排列列的顺序:

df.select(
    sorted(df.columns)
)

select() 函数返回一个新的 DataFrame,列的顺序已重新排列:

本文中的所有图像均由作者创建

sorted() 函数的默认排序顺序是字母顺序。要以相反的顺序进行排序,将 reverse 参数设置为 True

df.select(
    sorted(df.columns, reverse=True)
)

你可能会想使用方括号表示法进行排序(如 Pandas 中所示):

df[sorted(df.columns, reverse=True)]  # not recommended in Polars

但是,如果你打算将结果与其他 Polars 函数一起使用,这种方法不推荐,因为这种方法不能与延迟计算一起使用。

如果你只是想大致了解数据的样子,可以使用方括号表示法。

字符串长度计数

有时你需要计算特定列中字符串的长度。为此,我有一个名为 names.csv 的自制 CSV 文件:

name,age
Kristopher Ruch,23
Lakiesha Halton,45
Yun Frei,23
Sharyn Llanos,76
Lorretta Herren,21
Merrilee Akana,67
Boyd Gilman,89
Heidy Smalling,11
Leta Batalla,45
Siu Wayne,67
Sammie Gurule,23
Jayne Whetzel,11
Byron Doggett,67
Luke Alcazar,90
Petra Doutt,12
Tula Parkhurst,67
Davina Hess,26
Enda Cornelius,78
Merlyn Cora,89
Jeanett Hardeman,34

首先,将其加载到 Polars DataFrame 中:

import polars as pl
q = (
    pl.scan_csv('names.csv')    
)
q.collect()

你可以使用 lengths() 函数获取每个名称的长度,然后将其存储在名为 length_of_name 的新列中:

import polars as pl
q = (
    pl.scan_csv('names.csv')
    .select(
    [
        'name',
        'age',
        pl.col('name').str.lengths().alias('length_of_name'),        
    ])
)
q.collect()

基于标题选择列

在我之前关于 Polars 的文章中 (towardsdatascience.com/getting-started-with-the-polars-dataframe-library-6f9e1c014c5c),我展示了如何使用 select() 函数从数据框中选择列。让我们通过 Titanic 数据集详细了解一下。

数据来源:本节的数据来源于 www.kaggle.com/datasets/tedllh/titanic-train

许可 — 数据库内容许可(DbCL)v1.0 opendatacommons.org/licenses/dbcl/1-0/

加载 Titanic 数据集时,你会看到它有 12 列:

import polars as pl
q = (
    pl.scan_csv('Titanic_train.csv')
)
q.collect()

如果你只想检索 NameAge 列,将它们放入列表中并传递给 select() 函数:

import polars as pl
q = (
    pl.scan_csv('Titanic_train.csv')
    .select(
        ['Name','Age']
    )
)
q.collect()

如果你想要 所有 列,除了 PassengerId 列,可以使用 pl.exclude() 函数:

import polars as pl
q = (
    pl.scan_csv('Titanic_train.csv')
    .select(
        pl.exclude('PassengerId')
    )
)
q.collect()

exclude() 函数支持正则表达式。以下示例检索所有列,除了那些名称以“S”开头的列:

import polars as pl
q = (
    pl.scan_csv('Titanic_train.csv')
    .select(
        pl.exclude("^S.*$")   # exclude all columns that starts with S
    )
)
q.collect()

如果你想检索特定的列,可以使用 pl.col() 函数。以下示例检索所有以“S”开头的列:

import polars as pl
q = (
    pl.scan_csv('Titanic_train.csv')
    .select(
        pl.col('^S.*$')  # get all columns that starts with S
    )
)
q.collect()

使用正则表达式筛选行

除了 pl.col() 函数,contains() 函数也支持正则表达式。以下代码片段检索所有名称以“William”结尾的行:

import polars as pl
q = (
    pl.scan_csv('Titanic_train.csv')
    .filter(
        pl.col('Name').str.contains('William$')       
    )
    .select(
        [
            'Name'
        ]
    )
)
q.collect().to_pandas()

你也可以尝试以下表达式:

  • [Ww]illiam

  • (?i)illiam

  • ^William

你能弄清楚它们的作用吗?

这是一个挑战:你如何找到所有以“William”结尾的名字?

好吧,你可以使用正则表达式来做到这一点,但最简单的方法是使用is_not()函数来否定contains()函数中指定的条件:

import polars as pl
q = (
    pl.scan_csv('Titanic_train.csv')
    .filter(
        pl.col('Name').str.contains('William$').is_not()        
    )
    .select(
        [
            'Name'
        ]
    )
)
q.collect().to_pandas()

请注意,Polars 不支持正则表达式中的前瞻和回顾。

字符串列拆分

另一个关于字符串的流行活动是列拆分。现在让我们基于空格使用split()函数拆分name列(在names.csv文件中):

q = (
    pl.scan_csv('names.csv')
    .select(
    [
        'name',
        pl.col('name').str.split(' ').alias('splitname'),
        'age',
    ])
)

q.collect()

结果是一个名为split_name的新列,如下所示:

请注意,名字现在被拆分成了字符串列表。接下来你需要做的是将字符串列表转换为多个列,表示名字和姓氏。你可以使用with_column()pl.struct()函数来完成:

q = (
    pl.scan_csv('names.csv')
    .select(
    [
        'name',
        pl.col('name').str.split(' ').alias('split_name'),
        'age',
    ])
    .with_column(
        pl.struct(
            [
                pl.col('split_name').arr.get(i).alias(
                    'first_name' if i==0 else 'last_name') 
                for i in range(2)
            ]
        ).alias('split_name')
    )
)
q.collect()

with_column()函数返回一个更新了列的新 DataFrame。在这种情况下,我将使用pl.struct()函数更新split_name列,该函数遍历split_name列中的名字列表。将结构视为一组列。更新后的数据框现在如下所示:

值得注意的是,split_name列现在是struct类型。最后一步是使用unnest()函数将struct列拆分为单独的列:

q = (
    pl.scan_csv('names.csv')
    .select(
    [
        'name',
        pl.col('name').str.split(' ').alias('split_name'),
        'age',
    ])
    .with_column(
        pl.struct(
            [
                pl.col('split_name').arr.get(i).alias(
                    'first_name' if i==0 else 'last_name') 
                for i in range(2)
            ]
        ).alias('split_name')
    )
    .unnest('split_name')
)
q.collect()

最终结果如下:

现在让我们用 Titanic 数据集尝试另一个例子。特别是,我想关注Name列:

q = (
    pl.scan_csv('Titanic_train.csv')
)
q.collect()['Name'].to_pandas().unique()

这是Name列中唯一名字的快照:

'Braund, Mr. Owen Harris',
'Cumings, Mrs. John Bradley (Florence Briggs Thayer)',
'Heikkinen, Miss. Laina',
'Futrelle, Mrs. Jacques Heath (Lily May Peel)',
'Allen, Mr. William Henry', 'Moran, Mr. James',
'McCarthy, Mr. Timothy J', 'Palsson, Master. Gosta Leonard',
'Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)',
'Nasser, Mrs. Nicholas (Adele Achem)',
...

名字以以下格式保存:

姓氏,头衔。名字

理想情况下,你可以在split()函数中使用正则表达式。你可以使用以下正则表达式将Name列拆分为姓氏、头衔和名字:

‘([\’A-Za-z ()”//.-]+), ([A-Za-z]+). ([A-Za-z ()”//.-]*)’

这个正则表达式在我之前的文章中讨论过:

## 使用正则表达式(RegEx)进行特征工程(Pandas DataFrame)

探索如何使用正则表达式轻松操作你的字符串列

towardsdatascience.com

不幸的是,在写作时,Polars 中的split()函数不支持正则表达式。

所以策略是多次执行拆分:

这是第一次拆分:

q = (
    pl.scan_csv('Titanic_train.csv')
    .select(
    [
        pl.col('Name').str.split(r', ').alias('split_name'),
    ])
    .with_column(
        pl.struct(
            [
                pl.col('split_name').arr.get(i).alias(
                    'Last Name' if i==0 else 'First Name') 
                for i in range(2)
            ]
        ).alias('split_name')
    ).unnest('split_name')
)
q.collect()

结果现在显示姓氏被提取出来,接着是名字,其中包含标题和名字:

这是第二次分割:

q = (
    pl.scan_csv('Titanic_train.csv')
    .select(
    [
        pl.col('Name').str.split(r', ').alias('split_name'),
    ])
    .with_column(
        pl.struct(
            [
                pl.col('split_name').arr.get(i).alias(
                    'Last Name' if i==0 else 'First Name') 
                for i in range(2)
            ]
        ).alias('split_name')
    ).unnest('split_name')
    #---Second split---
    .select(
        [
            pl.exclude('First Name'), # get all columns except first name
            pl.col('First Name').str.split(r'. ').alias('split_name'),
        ]
    )
    .with_column(
        pl.struct(
            [
                pl.col('split_name').arr.get(i).alias(
                    'Title' if i==0 else 'First Name') 
                for i in range(2)
            ]
        ).alias('split_name')
    ).unnest('split_name')    
)
q.collect()

现在从名字中提取标题:

替换字符串值

替换 DataFrame 中的字符串值是你常常进行的另一个任务。在 Titanic 数据集中,乘客名称中有很多重复的标题。例如,对于女性乘客,使用的一些标题有 Ms、Miss、Mlle、Mlle 和 Mme。你通常会想将标题数量减少到一个更可管理的范围。

要在列中替换字符串,请使用 replace() 函数:

q = (
    pl.scan_csv('Titanic_train.csv')        
    .select(
    [
        pl.col('Name').str.replace('Mlle.','Miss.'),
    ])
)
q.collect()

上面的代码片段将 Name 列中所有的 “Mlle.” 替换为 “Miss.”。如果你想将多个标题替换为 “Miss.”,你可以使用正则表达式,如下所示:

q = (
    pl.scan_csv('Titanic_train.csv')        
    .select(
    [
        pl.col('Name').str.replace('Mlle.|Ms.|Mme.','Miss.'),
    ])
)
q.collect()

上面的代码片段将 “Mlle.”、 “Ms.” 和 “Mme.” 替换为 “Miss.”。

如果你喜欢阅读我的文章并且它对你的职业/学习有所帮助,请考虑注册成为 Medium 会员。每月 5 美元,它让你可以无限制地访问 Medium 上的所有文章(包括我的)。如果你使用以下链接注册,我将赚取少量佣金(对你没有额外费用)。你的支持意味着我将能花更多时间撰写像这样的文章。

[## 通过我的推荐链接加入 Medium - Wei-Meng Lee]

阅读 Wei-Meng Lee 的每一个故事(以及 Medium 上其他成千上万的作者)。你的会员费直接支持…

weimenglee.medium.com](https://weimenglee.medium.com/membership?source=post_page-----ec6bb74aeec2--------------------------------)

总结

在 Polars 中处理字符串类似于 Pandas。此外,如果你熟悉正则表达式,它一定会让你的工作变得更轻松。我在这篇文章中使用了相当多的函数,所以这里有一个方便的指南,供你在下一次处理 Polars 中的字符串时参考:

  • select() — 从 DataFrame 中选择你需要的列

  • with_column() — 返回一个更新了列的新 DataFrame

  • unnest() — 将一个结构体列分解为单独的列

  • str.lengths() — 返回字符串的长度

  • str.contains() — 检查字符串是否包含指定的字符串

  • str.split() — 基于指定的字符串分割字符串

  • str.replace() — 用另一个字符串替换字符串

  • pl.exclude() — 排除特定列

  • pl.col() — 包括特定的列

  • pl.struct() — 包含一组列的列

提高你 R 技能的技巧和窍门

原文:towardsdatascience.com/tips-and-tricks-to-improve-your-r-skills-b0f58006d0c1

学习如何编写高效的 R 代码

Janik 和 Patrick TinzTowards Data Science Janik 和 Patrick Tinz

·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 5 月 11 日

--

1234567890-=照片来源于 AltumCodeUnsplash

R 广泛用于商业和科学领域作为数据分析工具。该编程语言是数据驱动任务的基本工具。对于许多统计学家和数据科学家来说,R 是解决统计问题的首选。

数据科学家们通常处理大量数据和复杂的统计问题。内存和运行时间在这里扮演了重要角色。你需要编写高效的代码以实现最佳性能。在本文中,我们将介绍一些可以直接在下一个 R 项目中使用的技巧。

使用代码性能分析

数据科学家们经常希望优化他们的代码以提高运行速度。在某些情况下,你会依赖直觉尝试一些方法。这种方法的缺点是你可能优化了代码的错误部分,因此浪费了时间和精力。只有了解代码慢的部分,才能进行优化。解决方案是 代码性能分析。代码性能分析可以帮助你找到慢的代码部分!

Rprof() 是一个内置的代码性能分析工具。不幸的是,Rprof() 并不是很用户友好,因此我们不推荐直接使用它。我们推荐使用 profvis 包。Profvis 允许可视化来自 Rprof() 的代码性能数据。你可以通过 R 控制台使用以下命令来安装该包:

install.packages("profvis")

在下一步中,我们将通过一个示例进行代码性能分析。

library("profvis")

profvis({
  y <- 0
  for (i in 1:10000) {
    y <- c(y,i)
  }
})

如果你在 RStudio 中运行这段代码,你将得到以下输出。

火焰图(图片由作者提供)

在顶部,你可以看到你的 R 代码以及每行代码的内存和运行时间条形图。这种显示方式提供了代码中可能存在的问题的概览,但无法帮助你确定确切的原因。在内存列中,你可以看到每次调用分配的内存(右侧条形图)和释放的内存(左侧条形图)(以 MB 为单位)。时间列显示每行代码的运行时间(以 ms 为单位)。例如,你可以看到第 4 行的时间为 280 ms。

在底部,你可以看到带有完整调用栈的火焰图。该图提供了整个调用序列的概览。你可以将鼠标指针移动到单个调用上以获取更多信息。还可以注意到垃圾回收器()消耗了大量时间。为什么呢?在内存列中,你可以看到第 4 行的内存需求增加。第 4 行分配并释放了大量内存。每次迭代都会创建 y 的另一个副本,导致内存使用增加。请避免这种复制-修改的任务!

你还可以使用数据选项卡数据选项卡为你提供了所有调用的简洁概述,特别适合复杂的嵌套调用。

数据选项卡(图像由作者提供)

如果你想了解更多关于 provis 的信息,你可以访问 Github 页面。

向量化你的代码

也许你听说过向量化。那么它是什么呢?向量化不仅仅是避免使用 for() 循环。它更进一步。你需要从向量而不是标量的角度来思考。向量化对于加速 R 代码非常重要。向量化的函数使用用 C 编写的循环,而不是 R。C 中的循环开销更小,使其速度更快。向量化意味着找到在 C 中实现的与任务紧密匹配的现有 R 函数。函数 rowSums()colSums()rowMeans()colMeans() 对加速你的 R 代码非常有用。这些向量化矩阵函数总是比 apply() 函数更快。

为了测量运行时间,我们使用了 R 包 microbenchmark。在这个包中,所有表达式的评估都在 C 中完成,以最小化开销。作为输出,该包提供了统计指标的概述。你可以通过 R 控制台使用以下命令安装 microbenchmark 包:

install.packages("microbenchmark")

现在,我们将 apply() 函数的运行时间与 colMeans() 函数进行比较。以下代码示例演示了这一点。

install.packages("microbenchmark")
library("microbenchmark")

data.frame <- data.frame (a  = 1:10000, b = rnorm(10000))
microbenchmark(times=100, unit="ms", apply(data.frame, 2, mean), colMeans(data.frame))

# example console output:
# Unit: milliseconds
#                       expr      min        lq      mean    median        uq      max neval
# apply(data.frame, 2, mean) 0.439540 0.5171600 0.5695391 0.5310695 0.6166295 0.884585   100
#       colMeans(data.frame) 0.183741 0.1898915 0.2045514 0.1948790 0.2117390 0.287782   100

在这两种情况下,我们计算数据框每一列的均值。为了确保结果的可靠性,我们使用 microbenchmark 包进行了 100 次运行(times=10)。结果显示,colMeans() 函数大约快三倍。

如果你想了解更多关于向量化的知识,我们推荐在线书籍 R Advanced

矩阵与数据框

矩阵与数据框有一些相似之处。矩阵是一个二维对象。此外,一些函数的工作方式相同。不同之处在于:矩阵的所有元素必须具有相同的类型。矩阵常用于统计计算。例如,函数lm()会将输入数据内部转换为矩阵,然后进行计算。通常,矩阵的速度比数据框快。现在,我们来比较矩阵和数据框之间的运行时间差异。

library("microbenchmark")

matrix = matrix (c(1, 2, 3, 4), nrow = 2, ncol = 2, byrow = 1)
data.frame <- data.frame (a  = c(1, 3), b = c(2, 4))
microbenchmark(times=100, unit="ms", matrix[1,], data.frame[1,])

# example console output:
# Unit: milliseconds
#            expr      min        lq       mean    median       uq      max neval
#     matrix[1, ] 0.000499 0.0005750 0.00123873 0.0009255 0.001029 0.019359   100
# data.frame[1, ] 0.028408 0.0299015 0.03756505 0.0308530 0.032050 0.220701   100

我们使用 microbenchmark 包进行 100 次运行以获得有意义的统计评估。可以看出,矩阵访问第一行的速度比数据框快约 30 倍。这非常令人印象深刻! 矩阵明显更快,因此你应该优先使用矩阵而不是数据框。

is.na() 和 anyNA

你可能知道函数is.na()来检查向量是否包含缺失值。还有函数anyNA()来检查向量是否有任何缺失值。现在我们测试哪一个函数的运行时间更快。

library("microbenchmark")

x <- c(1, 2, NA, 4, 5, 6, 7) 
microbenchmark(times=100, unit="ms", anyNA(x), any(is.na(x)))
# example console output:
# Unit: milliseconds
#          expr      min       lq       mean   median       uq      max neval
#      anyNA(x) 0.000145 0.000149 0.00017247 0.000155 0.000182 0.000895   100
# any(is.na(x)) 0.000349 0.000362 0.00063562 0.000386 0.000393 0.022684   100

评估结果表明,anyNA() 的平均速度显著快于 is.na()。如果可能的话,你应该使用 anyNA()

if() … else() 与 ifelse()

if() ... else() 是标准控制流函数,而 ifelse() 更加用户友好。

Ifelse()按以下方案工作:

# test: condition, if_yes: condition true, if_no: condition false
ifelse(test, if_yes, if_no)

从许多程序员的角度来看,ifelse() 比多行的替代方案更易于理解。缺点是 ifelse() 在计算效率上不如 if() ... else()。以下基准测试表明,if() ... else() 的运行速度比 ifelse() 快 20 倍以上。

library("microbenchmark")

if.func <- function(x){
  for (i in 1:1000) {
    if (x < 0) {
      "negative"
    } else {
      "positive"
    }
  }
}
ifelse.func <- function(x){
  for (i in 1:1000) {
    ifelse(x < 0, "negative", "positive")
  }
}
microbenchmark(times=100, unit="ms", if.func(7), ifelse.func(7))

# example console output:
# Unit: milliseconds
#           expr      min       lq       mean   median        uq      max neval
#     if.func(7) 0.020694 0.020992 0.05181552 0.021463 0.0218635 3.000396   100
# ifelse.func(7) 1.040493 1.080493 1.27615668 1.163353 1.2308815 7.754153   100

在复杂循环中应避免使用 ifelse(),因为它会显著减慢你的程序速度。

并行计算

大多数计算机有多个处理器核心,可以并行处理任务。这个概念叫做并行计算。R 包 parallel 实现了 R 应用中的并行计算。该包在基本 R 中预安装。使用以下命令,你可以加载该包并查看你的计算机有多少个核心:

library("parallel")

no_of_cores = detectCores()
print(no_of_cores)

# example console output:
# [1] 8

并行数据处理非常适合蒙特卡罗模拟。每个核心独立地模拟模型的一个实现。最后,结果被汇总。以下示例基于在线书籍 Efficient R Programming。首先,我们需要安装 devtools 包。借助此包,我们可以从 GitHub 下载 efficient 包。你必须在 RStudio 控制台中输入以下命令:

install.packages("devtools")
library("devtools")

devtools::install_github("csgillespie/efficient", args = "--with-keep.source")

在 efficient 包中,有一个 snakes_ladders() 函数,它模拟了一场蛇梯棋游戏。我们将使用模拟来测量 sapply()parSapply() 函数的运行时间。parSapply()sapply() 的并行化变体。

library("parallel")
library("microbenchmark")
library("efficient")

N = 10⁴
cl = makeCluster(4)

microbenchmark(times=100, unit="ms", sapply(1:N, snakes_ladders), parSapply(cl, 1:N, snakes_ladders))
stopCluster(cl)

# example console output:
# Unit: milliseconds
#                               expr      min       lq     mean   median       uq      max neval
#        sapply(1:N, snakes_ladders) 3610.745 3794.694 4093.691 3957.686 4253.681 6405.910   100
# parSapply(cl, 1:N, snakes_ladders)  923.875 1028.075 1149.346 1096.950 1240.657 2140.989   100

评估显示,parSapply() 的模拟计算速度平均比 sapply() 函数快约 3.5 倍。哇! 你可以快速将这个技巧融入到你现有的 R 项目中。

R 语言与其他语言的接口

有时 R 会很慢。你使用各种技巧,但你的 R 代码仍然太慢。在这种情况下,你应该考虑用另一种编程语言重写你的代码。对于其他语言,R 提供了以 R 包形式的接口。例如,RcpprJava。编写 C++ 代码很简单,特别是如果你有软件工程背景。然后你可以在 R 中使用它。

首先,你需要使用以下命令安装 Rcpp:

install.packages("Rcpp")

以下示例展示了这种方法:

library("Rcpp")

cppFunction('
  double sub_cpp(double x, double y) {
    double value = x - y;
    return value;
  }
')

result <- sub_cpp(142.7, 42.7)
print(result)

# console output:
# [1] 100

C++ 是一种强大的编程语言,使其最适合于代码加速。对于非常复杂的计算,我们建议使用 C++ 代码。

结论

在这篇文章中,我们学习了如何分析 R 代码。provis 包支持你分析你的 R 代码。你可以使用 rowSums()colSums()rowMeans()colMeans() 等矢量化函数来加速你的程序。此外,如果可能的话,你应该优先使用矩阵而不是数据框。使用 anyNA() 而不是 is.na() 来检查向量是否有缺失值。通过使用 if() ... else() 而不是 ifelse() 来加速你的 R 代码。此外,你可以使用 parallel 包中的并行函数进行复杂的模拟。通过使用 Rcpp 包,你可以实现复杂代码段的最大性能。

有一些书籍用于学习 R。你将在以下找到我们认为非常适合学习高效 R 编程的三本书:

  • 高效 R 编程:更智能编程的实用指南

  • 动手编程 R:编写你自己的函数和模拟

  • R 数据科学:导入、整理、转换、可视化和建模数据

👉🏽 加入我们的免费每周 Magic AI 时事通讯,获取最新的 AI 更新!

👉🏽 你可以在我们的数字产品页面找到所有的免费资源!

免费订阅 以在我们发布新故事时获得通知:

[## 订阅邮件,以便在 Janik 和 Patrick Tinz 发布新内容时收到通知。

订阅邮件,以便在 Janik 和 Patrick Tinz 发布新内容时收到通知。如果你还没有 Medium 账户,注册时将会创建一个…

tinztwinspro.medium.com](https://tinztwinspro.medium.com/subscribe?source=post_page-----b0f58006d0c1--------------------------------)

了解更多关于我们的信息,请访问我们的关于页面。不要忘记关注我们的X。非常感谢你的阅读。如果你喜欢这篇文章,欢迎分享。祝你一天愉快!

通过我们的链接注册成为 Medium 会员,以阅读无限量的 Medium 故事。

学术研究出版技巧

原文:towardsdatascience.com/tips-for-scholarly-research-publication-bf96b1e3ad51?source=collection_archive---------9-----------------------#2023-04-10

如果你是一个尝试发表论文的研究生,请查看这个!

Ajay ShresthaTowards Data Science Ajay Shrestha

·

关注 发布于 Towards Data Science ·6 分钟阅读·2023 年 4 月 10 日

--

图片由 Lala Azizli 提供,来源于 Unsplash

本文的目标是与研究生分享一些关于在期刊和会议上发表论文的指南。它基于我作为博士生的过去经验。我的一篇 AI(深度学习)论文[1]最近在 Google Scholar 上获得了 1000 多次引用[2]。根据 Web of Science™数据库[引用影响力—3],只有约 0.026%的论文引用次数超过 1000 次。尽管看到论文发表后的影响力很高,但在论文被接受之前经历了许多反复尝试。这篇论文被多次拒绝,花了几年时间,并经历了多次修改才被接受。我尝试将我的经验提炼成一个指导过程。希望以下指南能帮助你在出版过程中。

问题

发布很困难,尤其是当你的学校要求提交到高影响力的期刊/会议时。大多数博士项目都有出版要求,这甚至可能成为拖延你毕业的原因。

目标

  1. 缩短发表时间

  2. 放大论文的影响力

指南

这里是一些我的经验,希望能帮助你减轻与出版相关的压力。举个例子,我认为启动公司和博士论文发表过程有一些共同点。两者初期都有不确定的未来,但最终都可能在各自领域产生新的贡献。就像创业一样,你的出版过程需要调查、愿景、策略、迭代和扩展。我的思路按以下顺序整理。

图 1:发表论文的 5 个里程碑 | 作者提供的图片

调查

阅读你尝试发表领域中的最新技术状态。记住,你是想对已有的工作进行补充。许多出版物是开放获取的,因此找到最新研究的论文不应是问题。此外,你的大学也应能提供必要的访问权限。Google Scholar 和 ResearchGate 是很好的开放资源。鉴于技术和科学研究的快速发展,我还会关注你研究领域的顶尖研究人员和公司,在 LinkedIn、Twitter 等类似网站上获取最新动态。如果适用,我还会探索统计数据和数据存储库进行探索性分析。关于调查的速度,推荐的数量从每周阅读 1 到 7 篇论文以准备文献综述,最终将 30 到 200 篇论文纳入/引用到你的综述中。

视野

调研后,制定你想要出版的主题愿景。一个创业公司从一个它打算解决的问题开始。创始人制定一个与他们和他们打算解决的问题相一致的愿景。为了帮助你找到匹配的方向,请从以下问题开始:

  1. 在你的研究领域/专业中有哪些未解决的问题?例如,这个 [4] 是一个关于各学科未解决问题的好参考列表。

  2. 在调研论文中讨论了一些活跃的研究领域是什么?

  3. 在你资格考试课程中,哪些课程让你最兴奋?

  4. 你的导师有哪些专业领域?

图 2:帮助制定出版愿景的维恩图 | 图片作者提供

尝试从上面维恩图中至少有三个重叠的区域选择一个主题。写一个摘要并获取你导师的反馈。确保它包括你的主要目标和论文的提纲。反馈越关键,此阶段需要的调整就会越多。

策略

在创业公司的初期阶段,其主要目标是实验、最大化学习,并找到市场真正需要的创新产品,即找到难以捉摸的产品-市场匹配。对你来说,那就是找到一个出版物,即论文-出版匹配。两者都需要策略。

在制定策略之前,请考虑以下问题:

  1. 你能投入多少时间和精力来进行出版工作?你将如何平衡其他承诺?

  2. 你的导师有多少时间来指导你?尝试安排一个定期的沟通会议。

  3. 你毕业所需的必要出版物的目标时间是什么?确保这一目标是现实的。

  4. 你是如何管理压力的?请注意,北美约 50%的博士生在获得学位之前会中途退学 [5]。你将需要一些生活技巧来完成博士学业。不出所料,三分之二的创业公司也失败了,即投资回报为负 [6]。

一旦你弄清楚这些问题并做了一些回顾,制定一个包含以下内容的出版策略:

  1. 论文主题/问题

  2. 论文类型、提纲和目标长度

  3. 目标出版物列表:

    • 按接受难度/影响因子排名

    • 包括他们的反馈时间(审稿周期)

    • 确保出版物符合你所在机构设定的所有标准

  4. 预期实现出版成功的目标日期

    • 计算n次迭代和修改论文的时间n

迭代和调整

一旦你和你的导师对策略/重点达成一致,开始你的研究,并尝试达到一个你可以展示的成果(也就是在创业术语中的最小可行产品或 MVP)。当你有了 MVP 论文时,首先提交给那些难度较高但审稿周期较短的期刊/会议。较短的审稿周期可以让你更快地迭代和吸收反馈,并在需要时更早地调整策略。

不要因为被拒绝而灰心丧气,应该利用批评(或严厉的)反馈对你的论文和/或策略进行大胆的修改。确保你在评估、回应并纳入评审人提供的反馈/差距。带有反馈的拒绝是一个祝福,是过程中的重要部分。把它看作是训练 AI 模型的过程。人工神经网络的权重根据标记/训练数据的反馈在训练过程中进行调整。正如下面图 3 所示,这些调整最初较大,随着接近论文发表的契合度(或 AI/ML 中的全局最小值)逐渐减小。

图 3:随机梯度下降,一种迭代的机器学习训练算法,用于达到全局/局部最小值(用+号标记)。论文发表契合度遵循类似的迭代 | 作者提供的图片

选择与你论文研究相符的期刊/会议,并仔细阅读提交指南。没有什么比等待几个月的反馈更糟糕的了,结果却发现你提交的论文因为不符合期刊/会议的主题或错过了重要的提交步骤而无法被考虑。如果适用,研究你所提交期刊/会议的过去论文并引用其中相关的工作会有帮助。另外,如果你的合著者(例如导师)在某些期刊上有过成功的论文,也可以尝试那些期刊。

规模

经过几次迭代,你将逐渐掌握接近发表的过程,因为拒绝的原因会变得不那么严重。达到那个阶段可能需要很长时间。请注意,你的第一篇论文可能是最困难的,并且需要最长的时间。

在机器学习中,有一种叫做迁移学习的技术[7],你可以将解决一个任务获得的知识应用到另一个相关任务中,所需的学习努力会减少。就像这样,你的第一篇论文中验证的所有学习将非常有助于加快你第二篇、第三篇以及随后的发表进程。

图 4:展示努力与发表(结果)的图表 | 作者提供的图片

更快的反馈周期和迭代是关键。利用这一点来达到必要的出版数量。坚持适合你的方法,并继续学习和调整流程,以便获得更有影响力的出版物。祝好运!

免责声明

这些建议基于我的学习经验,可能并不适用于每个人。

参考文献

  1. A. Shrestha 和 A. Mahmood,“深度学习算法与架构综述”,发表于 IEEE Access,卷 7,第 53040–53065 页,2019 年,doi: 10.1109/ACCESS.2019.2912200. (pdf 下载 链接

  2. 谷歌学术档案 — A. Shrestha

  3. 引用影响力: en.m.wikipedia.org/wiki/Citation_impact

  4. 未解决的问题 — en.wikipedia.org/wiki/Lists_of_unsolved_problems

  5. 博士学位完成率讨论 — www.wiley.com/en-us/network/publishing/research-publishing/trending-stories/improving-phd-completion-rates-where-should-we-start

  6. 为什么初创公司失败: hbr.org/2021/05/why-start-ups-fail

  7. AI/ML 中的迁移学习 — en.wikipedia.org/wiki/Transfer_learning

理解 R 中的正则表达式的提示

原文:towardsdatascience.com/tips-to-understand-regular-expressions-in-r-5d25be06f2a8

使用 stringR 获得更多的正则表达式知识

Gustavo SantosTowards Data Science Gustavo Santos

·发表于Towards Data Science ·8 分钟阅读·2023 年 1 月 18 日

--

照片由Jason Leung提供,来源于Unsplash

介绍

当一个项目涉及文本分析,比如情感分析、文本挖掘或你需要执行的其他分析任务时,通常会在某个时刻需要解析文本。这意味着需要提取文本的一部分或在文本中找到给定的模式,以便提取见解,例如。

如果我们处理的是简单的模式,比如一个单词或一个数字,那么在 R 中处理起来相对容易。

假设你有以下文本,并且你想找到单词random。我们可以使用许多简单的函数来执行这个任务。但首先加载library(stringr)。然后查看它的三个函数。

text <- "This is a random text. If you want to try to find a pattern here, 
let's say the numbers 1 or 2 or 3, you can use stringR."

# Find out if the word "random" is present
str_find(text, pattern='random')
[1] TRUE

# Extract the word "random"
str_extract(text, pattern= 'random')
[1] "random"

# Find where the pattern is located in the text
str_locate(text, 'random')
     start end
[1,]    11  16

现在,假设我们想找另一种类型的模式,非常具体的,比如句子中的任何数字,甚至更具体,字母和数字的组合。这就是正则表达式或regexp非常有用的地方。

正则表达式是一系列字符,帮助我们在文本中查找模式。

正则表达式 101

要编写你的第一个正则表达式,首先要理解这些模式可以由哪些元素组成。此外,记住始终将你的模式包裹在引号内,无论是单引号还是双引号。

开始吧。

旁注:*str_view_all*函数展示了由给定模式捕获的所有可能值,但它不会提取任何内容。这可以通过 stringR 库中的其他函数来完成。尽管如此,它是一个出色的工具,可以查看是否获得了正确的文本。

一个字符:方括号

如果你想查找一个字符,[] 将是你最好的朋友。当你写它们时,无论你放入什么,都表示你想要查找的那个字符。让我们通过实际示例来更好地理解这一点。

如果我们想查找在我们之前的文本中是否有字母k,这里是要使用的模式:[k]

str_locate(text, pattern= '[k]')
     start end
[1,]    81  81

是的,我们确实有一个字母k。它在位置 81 上。考虑到这仅对教育目的有用,我们再往上走一步。如果我们想知道我们的文本中是否包含字母 k、w 或 b 中的任意一个,我们可以使用这个模式:'[kwb]'

注意我们是这样写的:

  • 我们使用[]来告诉程序我们想要查找一个字符

  • 在方括号内,我们有我们要查找的模式,它表示kwb,哪个先出现就查找哪个。结果在第 31 个位置上是字母w

str_locate(text, pattern= '[kwb]')
     start end
[1,]    31  31

我们可以使用[]来表示其他许多模式。让我告诉你,这是我通常编写的正则表达式中最常用的模式之一。甚至还有一些预先创建的模式可以帮助我们在编写代码时。如果你点击这个链接,在 stringR 的速查表中你可以找到许多快速模式,如:

  • [:digit:] 用于查找句子中的所有数字。在我们的文本中:“1” “2” “3”

  • [:alpha:] 用于查找字母。

使用‘[:alpha]’在我们的文本中找到的内容。图片由作者提供。

  • [:punct:] 用于查找标点符号。在我们的文本中:"." "," "'" "," "."

我们还可以在方括号内使用值范围或区分大写或小写字母。例如,[a-z] 查找从 a 到 z 之间的一个小写字母,总是返回首先找到的内容。类似地,[A-Z] 做同样的事,但查找大写字母。

点号.意味着查找任何东西

正则表达式中,点号.表示任何字符。因此,如果我们创建一个类似'[Ii].'的模式并将其应用于我们的文本变量,我们就是在说我们要找的是一个字母I 或 i 后面跟着任何其他字符的模式。

str_view_all(text, '[Ii].')

这就是生成的图像。

字母 i 后面跟着任何其他字符。图片由作者提供。

查找某物的存在或缺失

正则表达式的另一个有趣部分是使用一些模式来查找空格、数字、字母的存在或缺失。通常,这会是一个双反斜杠后跟一个字母表示存在,字母的大写形式表示缺失。

  • \\s 表示空格,\\d 表示数字,\\w 表示单词,\\b 表示边界。

  • \\S 表示非空格,\\D 表示非数字,\\W 表示非单词。

# The new text variable
text2 <- "This is a random text 2\. Use stringR and Regexp."

# View all digits
str_view_all(text2, '\\d')

# View all EXCEPT digits
str_view_all(text2, '\\D')

# View all spaces
str_view_all(text2, '\\s')

# View all EXCEPT spaces
str_view_all(text2, '\\S')

# View all boundaries
str_view_all(text2, '\\b')

以下是相应的结果。

正则表达式的结果。图片由作者提供。

重要的是要注意,边界可以是任何包围模式的东西。想象一下你有一个 ID 号码如ID-3234。在这里,如果我们想仅使用这个正则表达式'\\b\\d+'提取数字,-将被识别为边界。

0、1 或更多次重复

+, * , ? , { }是你指示字符无或多次重复的方式。

  • ***** : 使用星号表示正则表达式可以发生零次或多次

  • ? : 问号表示正则表达式发生零次或一次

  • + : 使用加号表示正则表达式可以发生一次或多次

  • {2,4}: 大括号用于定制重复次数。这里,从 2 次到 4 次。

# Creating the text
text3 <- "This is a random text 3\. Hellooooo."

# Finds the l and one or more reps of o.
str_view(text3, '[l][o]+')
[OUT] looooo

# Finds exactly 3 reps of o.
str_view_all(text3, 'o{3}')
[OUT] ooo

# Finds exactly 2 to 5 reps of o.
str_view_all(text3, 'o{2,5}')
[OUT] ooooo

# One letter or the other: N or M?
text4 <- "nice or mice"
str_extract_all(text4, '[nm]ice')
[1] "nice" "mice"

使用除外符号^

我们可以在方括号内使用^作为除外的同义词。在我们的text2字符串中,我们可以随意说我们不想让模式捕获字母a, m, d, x, e。下面的正则表达式模式与^将移除方括号内列出的字母。

# Character NOT a, m, d, x or e
str_view_all(text2, '[^amdxe]')

代码返回下图。

我们正则表达式中的字母没有被选择。图像由作者提供。

之前和之后

要提取给定模式之前或之后的内容,这里是正则表达式。假设我们有一个text5 <- ‘Extract the ID 321’,我们想提取单词 ID 之前和之后的任何内容。

# Everything before ID
# . for any character
# + for one or more occurrences 
str_extract_all(text5, '.+(?=ID)')
[1] "Extract the "

# Everything after ID
str_extract(text5, '(?<=ID ).+')
[1] "321"

以…开始和结束

下一些代码是用来提取以某物开始或结束的模式。

#Text
text6 <- 'ID-1234'

# starts with ID
str_extract(text6, '^ID')
[1] "ID"

# ends with numbers
# \\d = digits
# + = one or more occurrences
# $ = ends with

str_extract(text6, '\\d+$')
[1] "1234"

分步构建

在学习了正则表达式的基础知识后,这里有一个好的提示来开始创建你自己的:分步构建。查看你想要创建的模式,思考它,然后开始编写每一部分,记住本文中学到的元素。

实践

模式:提取字母 A 后面跟着任何数量的数字。

  • 第一个元素是限定的字母 A。因此[A]将处理它。

  • 下一部分是任意数量的数字。所以\\d代表数字,+是一个或多个重复的符号。

  • 正则表达式 = '[A]\\d+'

text <- 'A234 B334 C434 A334 B345'
str_extract_all(text, pattern= '[A]\\d+')
[1] "A234" "A334"

模式:从书单中,如示例Romeo and Juliet by William Shakespeare (6389),提取书名、作者名和下载次数。

  • 要提取书名,我们必须获取直到单词by的所有内容。因此,我们使用.+表示任意字符出现一次或多次(?= by)直到by

  • 要提取作者名,我们必须获取单词by之后的所有内容。这里,(?<=by )是寻找by之后的部分,.+与前面的项目相同,[^(\\d)]+意味着^不( )包含数字。

  • 下载次数仅为数字\\d,出现一次或多次+

text <- "Romeo and Juliet by William Shakespeare (6389)
A Room with a View by E. M. Forster (5146)
Middlemarch by George Eliot (4913)
Little Women; Or, Meg, Jo, Beth, and Amy by Louisa May Alcott (4682)
Moby Dick; Or, The Whale by Herman Melville (4521)
The Enchanted April by Elizabeth Von Arnim (4490)
The Complete Works of William Shakespeare by William Shakespeare (4432)
The Blue Castle: a novel by L. M. Montgomery (4418)
Cranford by Elizabeth Cleghorn Gaskell (4276)
The Adventures of Ferdinand Count Fathom — Complete by T. Smollett (4257)"

# Extracting the names of the books to a data.frame
books = str_extract_all(text, pattern= '.+(?= by)')
# Extracting the names of the authors to a data.frame
authors = str_extract_all(text, pattern= '(?<=by ).+ [^(\\d)]+')
# Extracting the downloads
downloads = str_extract_all(text, pattern= '\\d+')

# As dataframe
gutenberg_top10 = data.frame(book = books,
                             author= authors,
                             downloads= downloads)
# Correct columns names
colnames(gutenberg_top10) <- c('book', 'author', 'downloads')

古腾堡提取前 10 本最下载的书籍的练习结果。图像由作者提供。

在你离开之前

好吧,这就是关于正则表达式的基础知识。我希望你能感受到它的强大。你可以使用正则表达式基本上找到任何东西。

我知道一开始可能看起来很吓人,但这只是一个练习的问题,你会开始对它更加自信。

你可以随时使用 Regex 101 网站 进行练习,参考资料。

如果你喜欢这些内容,请关注我的博客以获取更多信息。

[## Gustavo Santos - Medium

阅读 Gustavo Santos 在 Medium 上的文章。数据科学家。我从数据中提取见解,以帮助个人和公司…

gustavorsantos.medium.com](http://gustavorsantos.medium.com/?source=post_page-----5d25be06f2a8--------------------------------)

参考资料

[## 正则表达式 - 维基百科

正则表达式(缩写为 regex 或 regexp,有时称为有理表达式)是一系列…

en.wikipedia.org](https://en.wikipedia.org/wiki/Regular_expression?source=post_page-----5d25be06f2a8--------------------------------) [## 正则表达式

正则表达式是一种简洁且灵活的工具,用于描述字符串中的模式。本小册子描述了关键…

cran.r-project.org](https://cran.r-project.org/web/packages/stringr/vignettes/regular-expressions.html?source=post_page-----5d25be06f2a8--------------------------------) [## regex101:构建、测试和调试正则表达式

带有语法高亮、解释、PHP/PCRE、Python、GO、JavaScript 等的速查表的正则表达式测试器…

regex101.com](https://regex101.com/?source=post_page-----5d25be06f2a8--------------------------------) [## 简单、一致的常用字符串操作包装器

一套一致、简单且易于使用的封装在出色的 stringi 包周围的包装器。所有函数和参数…

stringr.tidyverse.org](https://stringr.tidyverse.org/?source=post_page-----5d25be06f2a8--------------------------------)

github.com/rstudio/cheatsheets/blob/main/strings.pdf

厌倦了二维码?自己制作一个标志性标记

原文:towardsdatascience.com/tired-of-qr-codes-build-you-own-fiducial-marker-aab81cce1f25?source=collection_archive---------7-----------------------#2023-11-04

二维码无处不在:想要创建一个更原创的解决方案吗?让我们制作自己的标志性标记,并学习如何检测和解码它。

Vincent VandenbusscheTowards Data Science Vincent Vandenbussche

·

关注 发布于Towards Data Science · 15 分钟阅读 · 2023 年 11 月 4 日

--

照片由Michael Dziedzic提供,发布于Unsplash

在这篇文章中,让我们学习如何制作一个新的标志性标记以及如何通过训练对象检测模型来检测它。然后,我们将学习如何使用图像处理技术解码我们的标记。

让我们将其分解为三个步骤:

  • 创建标志性标记

  • 在图像中检测标记

  • 解码标记

创建基准标记

目前已经存在很多用于计算机视觉的基准标记,最著名的是二维码。还有其他二维码,使用程度和鲁棒性各不相同,也可以使用。下面是一个不完全的代码列表。

一些最著名的基准标记及其名称和创建日期(来源 www.mdpi.com/1424-8220/21/16/5407,在 CC-BY 许可下)

如上图所示,基准标记可以有很大不同,但它们都有相同的目的:包含易于解码的信息。

什么是好的基准标记?

理想情况下,一个好的基准标记具有以下属性:

  • 易于检测:在能够解码标记之前,你必须能够准确地在图像中检测到它。

  • 易于解码:标记必须易于解码且没有任何歧义(即,解码后的标记产生唯一的值)

基于这些属性,现在让我们从现有的标记中构建我们自己的标记。

设计我们的基准标记

我个人喜欢 RUNE 标记(出于非常随意的理由):

  • 圆形和点的形状使其比方形标记更柔和

  • 这看起来非常明显,使得对象检测模型很可能容易检测到。

  • 它很容易自定义:我们可以调整每个圆圈上的点数以及圆圈的数量,以满足我们的需求和期望的美学。

但它在原始形式下并不完美:两个旋转的标记可能会导致相同或不同的解码结果。

这两个标签之间的唯一区别是 90°的旋转:它们无法被区分。图像作者提供。

为了减轻这个问题,我们将对标记添加一个条件:一个且只有一个扇区没有黑点,如下所示。

一个只有一行且没有黑点的标签,可以解决旋转问题。图像作者提供。

这样的标记可以很容易解码:假设每个扇区可以有三种可能的值:0、1 或 2,具体取决于三种可能的情况:

  • 一个小黑点:0

  • 一个大黑点:1

  • 两个点:2

一些扇区的表示:扇区 0 是唯一一个没有黑点的,而其他扇区总是至少有一个黑点。图像作者提供。

更一般地说,考虑一个具有 C 圆圈层的标记,一个扇区可以有最多 2ᶜ−1 种值(因为没有黑点的情况保留给扇区 0)。

最终,对于一个有 d+1 个点的标记,可能的组合数等于 (2ᶜ— 1)ᵈ。对于一个 2 个圆圈层和每个圆圈 20 个点的标签,这意味着 3¹⁹ ~ 11.6 亿个可能值。

构建我们的基准标记

下面我们解释一段用于生成随机基准标记图像的代码。

生成随机标签的方法概要。有关完整工作的代码链接,请参见文章末尾。

如你所见,第一步是生成一个随机值列表。考虑到C为圆圈层数和d+1为每层圆圈的点数,我们使用 numpy 生成一个在 0 到 2ᶜ−1 之间的d个值的列表。

基于这个随机值列表,我们计算点值:0 表示白色点,1 表示黑色点。最后,我们绘制最终标签,给定一个像素大小,并将输出保存为图像。当然,完整的代码库链接在文章末尾提供并记录。

我们选择了一个标记设计,并知道如何生成这样的标记。为了能够在实际条件下使用这样的标记,我们需要一个能够在图像中检测和解码这种标记的解决方案。这非常简单,分为两个步骤:

  • 使用目标检测检测标记

  • 解码检测到的标记

现在让我们进入这个管道的第一步。

检测标记

首先的步骤是检测给定图像中标记的存在和位置。为此,有许多目标检测模型。我们将在这里使用一个YOLOv8 模型,它非常容易训练和在生产环境中使用。

但在实际训练目标检测模型之前,我们需要数据:来自不同背景和环境的图像,包含来自不同缩放级别和视角的标签。

我们将生成和使用合成数据来训练模型,而不是收集和标记数据,这可能非常耗时。

生成数据

我们只需要两个要素来生成合成数据,以训练一个目标检测模型:

  • 各种免费使用的背景图像,例如可以从Unsplash获取。

  • 我们将随机生成的标记图像

有了这两个要素,我们所需要做的就是使用Albumentations进行一些增强,生成大量独特的合成图像及其相关标签。

下面提供了一段代码,允许生成图像,给定背景图像的路径和标记特征,如圆圈层数和每层圆圈中的点数。

生成合成数据的代码概要。有关完整工作的代码链接,请参见文章末尾。

这是一段相当长的代码,随意深入了解,但简单来说,它做了以下几件事:

  • 生成一个随机标签,图像边界是边界框标签。

  • 应用如仿射、透视或缩放等变换,感谢 Albumentations。

  • 随机将该标签插入到随机选择的背景图像中

  • 根据需要多次执行

使用这种方法,我们可以轻松生成足够大的数据集,包含数百或数千张图像。以下是一些创建的图像示例,带有红色边界框标签。

一些生成的图像,其中标签为红色边框。图像由作者提供。

正如我们所见,生成的图像相当多样,因为我们添加了背景和增广处理,如模糊和透视。

当然,我们不会对训练集和验证集使用相同的背景图像,以确保模型评估尽可能不受偏见影响。

一种允许在正确的文件夹中生成图像及其相关标签的 Python 脚本已在 GitHub 仓库中提供。

训练和评估模型

使用之前创建的数据集,我们现在可以在这些数据上训练一个目标检测模型。借助 YOLOv8 库,只需几行代码即可训练一个新模型。

YOLOv8 小模型在 100 个周期上的训练代码 gist。有关完整代码的链接见文章末尾。

正如我们所见,我们只需要实例化一个模型并在数据上进行训练。经过 100 个周期(如果在训练过程中遇到早停条件,则可以更少,比如我在这里大约 80 个周期后),我得到了约 0.5 的 mAP@50,如下面生成的结果所示。

YOLOv8 库生成的结果。结果的 mAP@50 约为 0.5。图像由作者提供。

尽管结果远未完美,但对于仅使用合成数据训练的数据集来说已经足够好。现在让我们用网络摄像头的实况测试这个模型。

为此,我们可以使用以下 gist 中的代码:

YOLOv8 模型在网络摄像头实况上的推理代码 gist。有关完整代码的链接见文章末尾。

这个代码相当直接:

  • 我们加载模型并获取网络摄像头的实况

  • 对于每张新图像,我们计算模型推理并显示任何检测到的边界框

  • 当按下 Escape 键时,我们停止实况

我用我手机上的标记图像运行了这段代码,正如我们在下面的图像中看到的那样,它效果非常好。

仅在合成数据上训练的 YOLO 模型的检测结果,在我的网络摄像头实况上测试。图像由作者提供。

虽然它在所有配置中并不能完美地检测标记,但对于仅使用合成数据训练的模型来说已经足够好。为了获得更好的结果,我相信可以稍微调整数据增广,当然真实的标记数据将会非常有帮助。

现在我们完成了管道的第一部分,让我们进入第二步:标签解码。

解码标记

我们现在拥有了生成和检测新基准标记的完全可用的代码。

一旦你可以在图像中检测到标签,下一步当然是解码它。让我们从经过我们之前训练的模型检测到的标记的裁剪图像开始。

来自我们目标检测模型的裁剪图像。让我们解码这个标记。

我开发了一个由以下步骤组成的解码算法:

  • 斑点检测以检测点

  • 外圆检测和椭圆拟合

  • 用于单应性计算的点选择

  • 单应性矩阵计算和图像展开

  • 最后,进行标记解码

主要思路如下:只要我能将检测到的标记与参考标记匹配(知道每圈的圆层数和每圈的点数),我就可以通过检查图像是白色还是黑色来相对容易地解码它。但为了做到这一点,我首先需要将图像展开以使其与参考标记匹配。

让我们一起回顾这些步骤。

检测点

首先任务是检测 YOLO 模型检测到的图像中的点。

从输入的裁剪图像中,我们将使用 OpenCV 应用以下图像处理列表:

  • 将图像转换为灰度图

  • 使用 Otsu 算法 二值化图像

  • 使用斑点检测器查找点

以下 gist 中的代码完成了这些操作:

从裁剪图像中检测点的代码。请查看文章末尾的链接以获取完整的代码。

如我们所见,为了最大化实际标记点的有效检测,设置了许多参数,如最小和最大面积,以及最小圆度。这些参数的微调花费了不少时间,但可以随意调整这些参数。

使用此代码处理我们的裁剪图像会得到以下的斑点检测结果。

输入裁剪图像上的斑点检测结果:点检测得很好。图像作者提供。

如我们所见,点检测得很好。下一步是检测外圆。

检测外圆

现在,我们需要检测最外层的圆圈(无论标签中的圆圈数量,这种解决方案都可以推广)。这将允许我们找到外圆上的点,以便我们最终展开图像。

要计算椭圆,我们所做的就是保留较大的点(在 OpenCV 中称为关键点),并从这些点拟合椭圆方程。这就是以下代码的功能:

代码允许从检测到的点计算椭圆方程。请注意,此代码始终计算一个中心估计,这在接下来的几个步骤中会很有用。

当我应用此代码并将检测到的点作为散点图展示,并展示拟合的椭圆时,得到如下结果:

检测到的斑点的散点图和最外圈的拟合椭圆。图像作者提供。

如我们所见,拟合的椭圆定义明确且与点的位置一致。请注意,由于我们在拟合椭圆,无论检测到的标记由于透视而变形程度如何,它都能正常工作。

现在我们需要找到实际上在这个椭圆上的点。这很简单:我们只需找到满足我们刚刚计算出的椭圆方程(带有给定阈值)的点位置即可。这是通过以下代码实现的:

用于返回椭圆上关键点的代码要点。请参见文章末尾的链接以获取完整代码。

现在我们知道点的位置,以及哪些点在最外圈上,我们可以使用这些点来计算单应性矩阵并解扭曲图像。

为单应性计算选择点

现在的目标是找到一些与参考图像匹配的点,以便计算单应性矩阵。

参考标签的图像,所有点都填充。作者提供的图像。

基于上面的参考图像,我们需要使用正确的单应性矩阵来解扭曲检测到的斑点。

为了计算单应性矩阵,我们可以简单地使用 OpenCV 函数findHomography。此函数需要参考图像和输入图像中至少 4 个匹配点作为输入,以便找到单应性矩阵。这个单应性矩阵将允许我们解扭曲检测到的图像,并与参考图像匹配。

从我们检测到的最外圈上的斑点来看,无法确定这些点在原始参考图像上的位置。因此,我们将选择最外圈中最近邻点的最长链条,以便与参考图像匹配。为此,有两个步骤:

  • 计算邻接矩阵,以便我们知道每个点的相邻点(如果有的话)

  • 从邻接矩阵中计算相邻点的最长链条

对于第一步,我们可以使用以下代码:

计算邻接矩阵的代码。请参见文章末尾的链接以获取完整代码。

这段代码将计算邻接矩阵,作为一个 Python 字典:对于最外圈上每个现有的点索引作为键,相应的值是找到的相邻点索引列表。

从这个邻接矩阵中,现在很容易找到最长的相邻点链条。为此,我使用了以下代码:

用于计算相邻点最长链条的代码要点。请参见文章末尾的链接以获取完整代码。

这段代码将高效地找到相邻点的最长链条,并返回它们的索引列表。

如果我们在这个输出中至少有 4 个点,我们可以理论上计算单应性矩阵。不幸的是,在大多数情况下,这不会非常准确,因为这些点几乎在同一条线上,无法准确计算单应性矩阵。为了解决这个问题,我们将添加一个额外的点:一个相对于中心对称放置的点:这将使单应性计算更准确。

我们可以用以下代码找到相对于中心的对称点(在进行椭圆拟合时计算得到):

找到对称点的代码,给定输入的最长链和椭圆上的所有关键点。请参见文章末尾的链接以获取完整的工作代码。

请注意,由于我们处于椭圆上,使用中心估计来找到给定点的对称点并不是 100%可靠的方法:它可能会输出错误的点。这一点在计算解码时我们会记住。

最终,我们得到以下图像中的结果,其中蓝色圆圈是最长链上的点,红色圆圈是预期的对称点(其中一个是最长链的一部分)。

点选择的结果。蓝色圆圈中的点是最长链上的点(除了最左侧的点)。红色圆圈中的点是检测到的对称点。中央的红点是估计的椭圆中心。图片由作者提供。

如我们所见,我们确实选择了 7 个相邻点的链,并选择了另一个点作为链中最左侧点的对称点。

解开图像

既然我们已经在输入图像中选择了一些点,接下来让我们在参考图像中找到匹配的点并计算单应性矩阵。为此,我们需要以下输入:

  • 裁剪图像中所选点的位置:这是我们刚刚计算的内容

  • 这些点在参考图像中的等效位置:需要计算,知道参考标记

要计算这些点的位置,我们将使用以下代码,允许计算点的位置。

用于生成参考点位置以计算单应性矩阵的代码要点。请参见文章末尾的链接以获取完整的工作代码。

请注意,我们通过一个名为symmetry_index_offset的参数增加了一个自由度:这将允许处理对称点计算中的可能错误,通过将偏移量添加到对称点的位置。

通过正确的点位置在裁剪图像和参考图像中,我们现在可以计算单应性矩阵并解开图像。为了确保我们在对称点上没有犯错误,我们将在[-2, 2]的范围内以 1 为步长进行计算,如下面的代码片段所示:

单应性矩阵计算和图像解开的代码要点。请参见文章末尾的链接以获取完整的工作代码。

我们在这里做的是用 OpenCV 的functionfindHomography计算单应性矩阵,然后用warpPerspective解开图像。我们对 5 个偏移值执行此操作,以便得到 5 张解开的图像。

结果图像如下:

结果解开的图像。只有-1 偏移量的图像正确解开。图片由作者提供。

正如我们所看到的,根据偏移量,未校正结果相差甚远。尽管通过视觉检查很容易理解 -1 的偏移量是正确的,但我们希望将这一检查自动化。我们将在下一步中处理这个问题:实际的标记解码。

解码标记

从给定的未校正图像开始,最后一步是解码标记。我们非常接近,这一步可能是最简单的。

我们需要做的就是检查每个预期点的位置,未校正图像的颜色。由于图像经过了 Otsu 二值化,这非常简单。我们只需检查预期点位置周围 3x3 像素区域内是否有黑色像素:如果有,则存在点;如果没有,则不存在点。

计算未校正图像的列表代码所使用的代码概要。请参阅文章末尾的完整代码链接。

这基本上是上面代码所做的。然后根据位置,我们分配一个值。这样,这个函数的输出就是一个值的列表。最后,我们寻找一个 -1 值(表示预期的区域没有黑点,请参考 设计我们的基准标记 部分以获取相关提醒),并重新排列数组,将其放在最后一个索引位置。

例如,以下是每个未校正图像计算出的代码:

  • 偏移量 -2: [0, 2, 0, -1, 1, -1, 0, 0, 0, 2, 2, 1, 2, 2, 0, 2, 2, 2, 0, -1]

  • 偏移量 -1: [2, 2, 2, 0, 2, 0, 1, 1, 1, 2, 2, 1, 2, 2, 0, 2, 2, 2, 0, -1]

  • 偏移量 0: [0, -1, 2, 2, 0, 0, -1, 0, 0, -1, 0, 1, 2, 2, 0, 2, 2, 2, 0, -1]

  • 偏移量 1: [-1, 2, 2, 2, 2, 0, -1, -1, 0, 0, 0, 0, 0, 2, 0, 2, 2, 2, 0, -1]

  • 偏移量 2: [-1, 2, 1, 2, 1, 0, 1, -1, -1, -1, -1, 0, 0, 0, 2, 2, 0, 0, 2, -1]

正如我们所见,只有一张图像在最后一个索引位置只有一个 -1 值:使用 -1 偏移量的未校正图像。这是我们已经很好地校正的图像(如视觉检查所见),能够实际解码标记。

由于这个代码列表对于每个可能的标记都是唯一的,你可以在这里停止,或者计算一个唯一的整数值。可以通过以下代码片段很容易地计算出唯一值:

计算解码最终值所使用的代码概要。请参阅文章末尾的完整代码链接。

在我们的例子中,这将返回 -1 对于所有错误校正的图像,并返回 377667386 对于实际标记。

就是这样,我们从输入图像一路走到了实际的唯一代码!现在让我们总结一下,并反思我们所做的工作中的局限性。

创建一个完整的流程

现在我们已经拥有所有的构建模块,我们只需要将它们组合起来,以获得一个漂亮的、自定义的基准标记解码器,它可以替代 QR 代码!

总结一下,以下是一个完整工作流程中的步骤:

  • 从输入图像中,使用对象检测来检测标记

  • 对于每个检测到的对象,裁剪图像并进行下一步

  • 使用 Otsu 二值化和斑点检测来检测点

  • 使用椭圆拟合找到最外层的点

  • 使用最近邻点和对称点的最长链计算同质变换矩阵

  • 使用同质变换矩阵去畸变图像

  • 使用参考图像解码标记

就这样!我不会让你自己编写所有代码,一切都可以在github 仓库中找到,还有一个预训练的目标检测模型。

你会在这个仓库中找到用于运行子步骤(例如生成合成图像、训练目标检测模型等)的 python 脚本,以及一个使用你的摄像头作为输入运行完整流程的 python 脚本,这样你就可以进行测试!

最后想法

希望你喜欢这篇文章并从中学到了东西!我个人非常喜欢这个项目,因为它结合了机器学习和传统的图像处理。

尽管如此,我开发的算法仍然有一些限制,我希望能克服它们。确实,并非所有标记都可以解码:

  • 外圈上没有超过 2 个相邻点的标记将无法正确解码

  • 对于没有对称点的标记,它会给出不可靠的结果,因为同质变换矩阵不准确。

另一个限制是有时在去畸变过程中,同质变换会镜像图像,导致列表代码被反转,从而最终解码的整数值不同。

如果你有任何想法来克服这些限制,欢迎给我发消息或提议拉取请求!

另一个话题是,这里的解码只给出一个整数值。你需要将这个整数值与应用中的任何相关内容(如链接、项目、图像等)匹配,以使其真正有用。我相信可以直接将这样的标记解码为 ASCII 字符列表,但我自己没有尝试过:再次强调,任何贡献都是非常欢迎的。

参考文献

原始 RUNE-Tag 论文:

F. Bergamasco, A. Albarelli, E. Rodolà 和 A. Torsello,"RUNE-Tag: 高精度的具有强遮挡恢复能力的基准标记",CVPR 2011,科罗拉多斯普林斯,美国,2011,第 113–120 页,doi: 10.1109/CVPR.2011.5995544。

原始 RUNE-Tag 仓库: github.com/artursg/RUNEtag

对你的数据工程师角色感到厌倦吗?

原文:towardsdatascience.com/tired-of-your-data-engineering-role-bb40b10275f9?source=collection_archive---------3-----------------------#2023-08-19

我是如何转型为数据分析工程师的

Madison SchottTowards Data Science Madison Schott

·

关注 发表在 Towards Data Science ·8 min read·2023 年 8 月 19 日

--

照片由 Campaign CreatorsUnsplash 提供

几年前,我曾经在职业生涯中感到不满。我在数据工程领域工作了三年,最初对科技世界的兴奋感已经消退。我开始意识到我对工作并不像我曾经希望的那样充满热情。

我认为,无论你身处何地,跟随自己的兴趣非常重要,以找出你真正想做的事情。这可能意味着追求那些让你快乐的工作之外的爱好,或者参与你已经工作的公司中的不同团队。

我记得在大学时我非常喜欢市场营销和业务方面的东西,所以我决定开始探索那方面的选择。我开始与每天使用数据解决业务问题的数据分析师交谈。他们就像是有更多业务曝光的数据工程师!

通过要求参与数据分析项目,我学会了使用dbt并进一步发展了我的 Python 技能。玩弄新技术帮助我看到,我仍然喜欢从事数据工作,只是需要使用正确的工具和解决正确的问题。最终,这促使我寻求一个与这些新发现的兴趣和技能集更匹配的不同角色。这个角色恰好就是数据分析工程师。

如何知道你应该过渡到数据分析工程

很多人害怕从数据工程师过渡到数据分析工程师,因为他们不知道时机是否合适。事实是,永远不会有“正确”的时间去做任何事情。然而,如果你感到没有挑战和满足感,你永远不会过早做出这个选择。

从数据工程到数据分析工程可能适合你如果你对数据本身感到好奇,而不是构建支持数据的产品。数据分析工程更以客户为导向,因为你使用客户的数据来回答关键业务问题。你更专注于增加收入和洞察,而不是构建事物。

这让我引出了第二点。如果你希望更紧密地联系业务,并希望做出推动公司增长的决策,数据分析工程可能适合你。作为数据工程师,你的任务由项目经理分配。你不一定能决定自己解决什么问题或认为需要优先处理什么。然而,在数据分析工程中,你可以。

我是如何发现数据分析工程的

说实话,当我意识到自己想要换数据角色时,我对数据分析工程一无所知。我认为我唯一能更接近业务的选项就是成为数据分析师。这正是我尝试做的。

我申请了很多数据分析师职位,但运气不佳。我缺乏分析师所需的深入业务经验,以及构建适当仪表盘的技能。我开始专注于那些结合了我已有技能和我想要学习技能的职位,而不是单纯关注职位名称。

最终,我偶然发现了分析工程师这一角色。这个角色要求掌握 SQL、Python、AWS、编排、dbt 和数据仓库等技能,而这些技能正是我作为数据工程师时所获得的。然而,它还要求具有使用现代数据堆栈工具的经验、与业务团队沟通的能力,以及一些基础的 BI 报告技能。

尽管我没有太多现代数据堆栈的经验,但我很幸运找到了一家相信我的热情和学习欲望的公司。有时候,如果匹配合适,公司愿意在没有所有要求的情况下雇用你!

在转向分析工程师之前需要发展的技能

在寻求分析工程师角色之前,我花了一些时间真正发展以下几个技能。这些是最重要的技能,会让你在其他候选人中脱颖而出。如果你专注于这三项技能,我相信你可以轻松地在工作中自学成为一名优秀的分析工程师所需的其他技能。

dbt

dbt,或称为数据构建工具,是一种真正为分析工程师开辟道路的数据转换工具。事实上,它背后的公司也是首创“分析工程师”这一名称的!虽然你不一定需要了解 dbt 才能成为一名分析工程师,但这是一项许多公司在招聘时寻找的技能。

dbt 是一个基于 SQL 的工具,所以如果你已经知道 SQL,学习起来相对简单。你需要熟悉设置 dbt 项目、数据建模最佳实践以及每种 dbt 数据模型的目的。我建议查看 dbt 样式指南,以了解在 dbt 中编写 SQL 代码的“应做”与“忌做”之处。这将帮助你学习当你加入团队时可能已经存在的标准。

dbt 还使用了一种称为 Jinja 的模板语言用于文档和工具内的功能。然而,dbt 称这些为宏,而不是函数。宏是更高级的功能,相当于函数。你可以使用它们在数据模型中自动化 SQL 输出。虽然这不是获得工作的必要条件,但这无疑是使你作为分析工程师的工作更轻松的有用技能。

商业沟通

这可能是作为数据工程师最难以发展和练习的技能。我们习惯于与其他工程师进行技术性对话,以至于忘记了用易于理解的方式解释事物。在与业务团队沟通时,你需要知道如何向非技术观众解释技术概念。越简单越好。你所沟通的听众几乎永远不会像你一样具备技术背景。

此外,你需要理解这些团队所使用的业务术语。你会不断听到不同的指标,如 CAC、MRR、NPM 和 ROI。确保你知道这些是什么意思!很可能,这些就是你构建数据模型的原因。你需要理解最终目标/指标以及计算这些指标的数据。

构建数据管道

幸运的是,作为一个数据工程师,你很可能已经具备了这个技能!构建数据管道是许多数据工程师的常见技能,这些技能在分析工程师中也同样适用。然而,在这个背景下,你需要知道如何协调不同的数据源与 dbt 模型。你需要能够处理来自多个不同源系统的依赖关系。

在分析工程中,常见的数据管道工具包括 Airflow、Prefect 和 Dagster。这些工具都是使用简单的 Python 构建的,Python 也是构建管道的重要技能。选择一个工具并熟悉它!一旦你学会了一个,你很可能也能学会其他的工具。

我希望在转型为分析工程师之前知道的事情

当然,回顾一下从数据工程师转型为分析工程师的过程,我有一些希望自己早知道的事情,这些事情会让我的旅程更加愉快。

你的数据工程技能每天仍然会被使用,并且非常有价值。

当你从数据工程师转型为分析工程师时,很容易认为你的整个角色和技能集都会发生变化。然而,分析工程师和数据工程师之间有很多重叠的部分!通常,你作为数据工程师学到的技能会成为你最大的超能力。你可能能做其他分析工程师做不到的事情,所以要充分利用这些技能!

我实际上认为,这会让你在申请职位时比其他候选人更具优势,特别是如果你正在寻求一个小公司里的数据职位,该公司刚刚开始组建数据团队。很可能,他们希望以最小的投入获得最大的回报,并且希望找到一个可以做各种事情的人。一个充满热情的数据工程师转变为分析工程师,正是适合这个工作的最佳人选!

一开始你不需要知道所有的东西。

很容易陷入需要一次性学习所有东西的状态。我需要知道如何在申请那个职位之前设置一个像 Airbyte 这样的开源数据连接器。我需要在申请那个职位之前有在 dbt 中构建自定义宏的经验。我不能申请那个职位,直到我掌握每种 SQL 窗口函数。

不要专注于你不知道的东西,而要关注你知道的!是什么让你独特?为什么有人会选择雇佣你而不是别人?思考那些在工作中无法学到的让你脱颖而出的事情。行业总在变化,这意味着总会有你不知道的东西。接受它!这就是分析工程的最佳部分之一。

当我开始作为分析工程师时,我根本不知道什么是维度建模。我实际上是在最初试验组织我们的数据仓库和建模数据时通过 dbt 的文档了解到的。最终,我学会了什么是维度建模,应用这些技术到我的工作中,并提高了我的数据建模技能。现在我可以和你谈论维度建模整整一天!

申请新角色从来不会太早。

如果你不开心,就要采取行动。你在不适合你的职业中呆得越久,就越错过学习和成长的机会。每次我换职业,都是因为我的工作不再给我带来挑战。如果我们不学习新事物,不成长为一个更好的人,我们就停滞不前。而这难道不是更糟糕的事情吗?

即使你觉得自己一开始没有合适的技能,通过探索你的选择,你将会学习到东西。浏览不同的角色,看看公司在寻找什么。职位发布中的技能有什么共同点?你是否认为分析工程角色中有你不喜欢的地方?查看职位描述是发现这些信息的好方法!

结论

回头看,我无法从数据工程到分析工程有更棒的过渡。我深知纯技术角色对我而言并不合适。我需要动手处理数据,利用数据解决客户和业务问题。

生活太短暂,不要留在你不完全享受的角色中!不要害怕探索其他可能的领域,无论是否是分析工程。最坏的情况是什么?如果你跳槽后发现这实际上不适合你,你总是可以回到之前的角色。但是,你永远不会知道除非你尝试!

想了解更多关于分析工程的信息,订阅我的免费每周通讯,我会分享学习资源、教程、最佳实践等。

查看我的第一本电子书,分析工程基础,这是一本关于入门分析工程角色的全面指南。

1 还是 0:图像分类中的像素攻击

原文:towardsdatascience.com/to-1-or-to-0-pixel-attacks-in-image-classification-ec323555a11a

探索对抗性机器学习的领域

Maham HaroonTowards Data Science Maham Haroon

·发表于Towards Data Science ·阅读时间 13 分钟·2023 年 11 月 23 日

--

图片由Pietro Jeng提供,来源于Unsplash

嗨,大家好!

今年,我参加了我的第一次Capture The Flag (CTF)比赛,这次经历可以说非常引人入胜。挑战,特别是那些涉及像素攻击的挑战,引起了我的注意,并成为了这篇文章的主要焦点。虽然我最初打算分享我在比赛中进行的一个简单的像素攻击,但这篇文章的目标也是深入探讨如何增强机器学习模型,以更好地抵御像比赛中遇到的像素攻击。

在我们深入理论之前,让我们通过一个引人入胜的场景来引起你的注意。

想象一下:我们的公司 MM Vigilant 致力于开发一款前沿的物体检测产品。概念简单而革命性——客户拍下所需物品的照片,几天后它就会送到客户家门口。作为幕后出色的数据科学家,你打造了终极的基于图像的物体分类模型。分类结果无可挑剔,模型评估指标一流,利益相关者也非常满意。模型投入生产,客户也很高兴——直到投诉接踵而至。

经调查,发现有人在图像到达分类器之前对其进行干扰。具体来说,每张钟表的图像都被恶意地分类为镜子。结果如何?任何期待钟表的人都会收到意外的镜子。这真是个意外的转折,不是吗?

我们在 MM Vigilant 的利益相关者对这种事故的发生感到既担忧又好奇,更重要的是,如何采取措施来防止它。

我们刚刚探讨的场景是一个假设情境——尽管图像篡改是一个非常可能的情况,特别是当模型存在漏洞时。

那么,让我们仔细看看这种图像操作的一个例子……

图像分类中的像素攻击

像素攻击,特别是在图像分类的背景下,旨在欺骗机器学习(ML)分类器,将图像分类为其他类别。虽然可以讽刺地认为,一个不佳的模型已经表现出这种行为,但这里的目标是击败最先进的模型。不用说,这些攻击有很多方法和动机,但这篇文章,限于范围,将重点关注黑箱、针对性像素攻击及其相关的预防措施。

让我们从直观上来理解这个概念。实际上,任何输入到神经网络的图像都经过每个像素的一系列数学运算来进行分类。改变一个像素,因此,会导致这些数学运算的结果发生变化,从而改变预测得分。这可以推断到这样一种程度,如果一个主要/“对分类至关重要”的像素被操控,它将对类别的预测得分产生足够大的影响,从而导致误分类,如下图所示。

图片来源于作者

在图像分类攻击领域,有两种知名的方法,取决于误分类的期望结果:

  1. 针对性攻击

  2. 未针对性攻击

针对性攻击

针对性像素攻击涉及一种有目的的转换,目标是将图像分类为特定类别。例如,想象一个故意尝试将熊分类为船或将苹果分类为考拉的行为。这些攻击有两个目标:最小化原始类别的得分,同时最大化目标类别的得分。

未针对性攻击

相反,未针对性像素攻击的前提是避免将图像分类为其原始类别。任务简化为最小化指定类别的预测得分。换句话说,目标是确保一只熊,例如,被分类为除了熊之外的任何东西

值得注意的是,每个针对性攻击都可以被认为是未针对性攻击,但反之则不一定成立。

除了攻击类型之外,还有两种不同的方法来实现这些攻击,具体取决于被攻击模型的可用性(传统/白盒方法)或仅有的结果分数(黑盒方法)。

传统攻击

在传统或白盒攻击中,模型通常是可用的。可以获取梯度信息并用于如 快速梯度符号方法(FGSM) 这样的攻击。这种方法通过沿梯度方向对输入数据进行小幅扰动,导致误分类,而不会显著改变图像的视觉外观。

可以在以下代码库中找到该方法的简单 GitHub 实现。

[## GitHub - ymerkli/fgsm-attack: 目标和非目标快速梯度符号方法的实现

目标和非目标快速梯度符号方法的实现 - GitHub - ymerkli/fgsm-attack: 实现了…

github.com](https://github.com/ymerkli/fgsm-attack/tree/master?source=post_page-----ec323555a11a--------------------------------)

黑盒攻击

黑盒攻击则完全依赖于模型预测。可以使用如 差分进化 这样的技术来执行这种类型的攻击。

差分进化是一种模拟自然选择的优化算法。它通过在迭代中创建和组合潜在解决方案,基于设定的标准从一个种群中选择表现最佳的解决方案。这种方法在探索解决方案空间方面效果显著,并且常用于对机器学习模型的对抗攻击。

鉴于我们的挑战集中在黑盒目标像素攻击上,让我们直接进入实现部分。

CTF 挑战

对于 CTF 挑战,其目标是将一张清晰的狼的图像误分类为格兰尼·史密斯苹果——向“小红帽”故事致敬。数据集中包含大约 1000 个类别,图像分辨率为 768x768 像素,超越了 MNIST、CIFAR 甚至 ImageNet 的分辨率,困难在于通过识别最少的像素数来欺骗模型以达到目标误分类。值得注意的是,尽管高分辨率图像复杂,但机器学习分类的本质,如上所述,在于任务的非直观性,将图像简化为一组值和一系列非常依赖这些个体值的数学运算。

在我们深入研究代码之前,让我们先看看我们狼的原始图像。难道它看起来不具有伪装成苹果的潜力吗?那绿色的眼睛、圆圆的脸以及绿色的背景——这些都是一个果味冒名顶替者的所有特征。

AI Village 提供,许可证 署名 4.0 国际 (CC BY 4.0)

在开始的旅程中,黑箱模型对“木狼”类别的初始评分约为 0.29,而对“青苹果”类别的评分为 0.0005。我最初考虑应用 scipy 的差分进化 方法。这种方法在涉及 CIFAR 和 ImageNet 数据集的像素攻击中已显示成功。差分进化技术涉及从 n 个随机样本开始,代表种群大小。在每一步,选择最好的后代,通过模型评分来确定,最终导致我们期望的结果。然而,鉴于时间限制和任务涉及仅更改单个图像的评分,我选择了更直接的策略。

方法

我首先将原始图像划分为逐渐更小的块,从 2x2 开始,一直到 16x16。针对目标青苹果(绿色),我逐一将块中的值更改为苹果绿色,并观察对木狼类别和青苹果类别分数的影响。然后,我手动选择了 2-3 个 16x16 的块,在这些块中应用了一种差分进化的方法。这意味着在该区域内随机选择的像素进行了大约 50-75 次迭代的单像素更改。

尽管我在给定的两天内无法准确定位到那个臭名昭著的单一像素,但我成功地进行了高度像素化的攻击,将狼的分类改变为青苹果,从而获得了三部分任务的两个子问题的标志。

现在我们有了背景信息,让我们跳入一些代码中,以便你能从这篇文章中学到一些东西。

Python 代码

我将其视为黑箱问题,当给定图像时,查询会提供所有类别的预测列表。预测按值排序,因此预测的类别是列表中的第一个值。

import requests
import base64
import cv2
import numpy as np
import matplotlib.pyplot as plt

def query(input_data):
    response = requests.post({link to get the blackbox score}, 
                              json={'data': input_data})
    return response.json()

get_scores函数以正确的格式将图像输入查询,并在大多数情况下以字典形式获得所需的结果。

 def get_scores(input_image):
    # Some preprocessing since the query accepted only bytes

    _, input_image = cv2.imencode('.png', input_image)  
    image_bytes = input_image.tobytes()
    input_data = base64.b64encode(image_bytes).decode()
    result = query(input_data)

    """
    the result is a json dict {} with the variable 'output' or 'flag', 
    the output consists of scores for 1000 classes of which two are timber
    wolf and granny smith. Initially the score for timber wolf is around 0.29
    and the score for granny smith id 0.0005
    """

    dict_score = {"timber wolf" : 0, "Granny Smith" : 0}

    try:
        print(result['flag'])
    except:
        pass

    # the scores in the output are always sorted so the first score 
    # is always the predicted score

    dict_score["predicted_class"] = result['output'][0][1]
    dict_score["predicted_score"] = result['output'][0][0]

    # next we get the scores for our wanted target and our original class
    count = 0
    for sublist in result['output']:
        score, class_name = sublist
        if class_name == "timber wolf":
            dict_score['timber wolf'] = score
            count+=1
        elif class_name == "Granny Smith":
            dict_score["Granny Smith"] = score
            if count ==1:
                break

    return dict_score

相关代码

核心思想是选择在苹果颜色的 RGB 范围内的像素,并测试约 50-75 个像素,以找到最大化“青苹果”类别分数并最小化“”类别分数的像素。我逐渐增加了选择区域的大小,并根据需要修改优化过程。例如,当“青苹果”类别的分数超过“”类别的分数时,我考虑所有增加“青苹果”类别分数的像素,只要它高于“”类别的分数,而不是专注于减少“”类别的分数,这显然加快了一些进程。

尽管没有找到那个难以捉摸的单一像素,我成功执行了高度像素化的攻击。

# Load your image
input_image = cv2.imread('/timber_wolf.png')
# Get the dimensions of the original image
image_height, image_width, _ = input_image.shape

# Define the size of the window (dxd)
# initially I had a large window size for testing purposes 
# to identify regions of high interest
window_size = 1 #image_height//64

# get the initial scores
scores = get_scores(input_image)

dict_pixels ={'pixels':[]}

best_score_tw =  scores['timber wolf'] #the current/best score for timber wolf
best_score_gs = scores['Granny Smith'] #the current/best score for granny smith

print(best_score_tw, best_score_gs)

max_iter = 75
iter_1=-1

pixel_count = -1 # number of pixels that have been changed
max_pixel_count = 40 # number of pixels we want to change

temp_image = input_image
rand_red_best, rand_green_best = (0, 0)
row_best, col_best = (0, 0)

while pixel_count < max_pixel_count:
  while iter_1 < max_iter:
    # although I did change the values from time to time
    row = np.random.randint(192,388) 
    col = np.random.randint(192,388)

    iter_1 +=1

    output_image = input_image.copy()

    left = row
    upper = col
    right = min(x + window_size, image_width)
    lower = min(y + window_size, image_height)

    # the pixel values for RGB were kept close to the color of the apple 
    rand_red = np.random.randint(0,153)
    rand_green = np.random.randint(170,255)
    rand_blue = 0#np.random.randint(0,255)
    output_image[upper:lower, left:right] = [rand_red, rand_green, rand_blue]
    scores = get_scores(output_image)
    # I actually also changed this a couple of times depending on where the output was

    #if (scores['timber wolf'] - scores['Granny Smith']) < min_score  :
    # initially I wanted pixels that bridged that gap between both classes the most.
    # Once granny smith score crossed the timberwolf score I only cared about increasing
    # score for granny smith class as long as timberwolf stayed below granny smith sclass
    if (best_score_tw > scores['timber wolf']) and (best_score_gs < scores['Granny Smith'])

        temp_image = output_image
        best_score_tw = scores['timber wolf']
        best_score_gs = scores['Granny Smith']
        rand_red_best = rand_red
        rand_green_best = rand_green
        min_diff =  scores['timber wolf'] - scores['Granny Smith']
        best_row, best_col = row, col

        print(iter_1, [rand_red,rand_green,0], ':', row,col, ";\n",min_diff,'\n')

  pixel_count += 1
  input_image = temp_image
  scores = get_scores(input_image)

  print(pixel_count,
        '\n', row, col, [rand_red_best, rand_green_best, 0],
        '\n', scores, '\n')

  dict_pixels['pixels'].append(([row_best,col_best],[rand_red_best,rand_green_best,0]))

np.save('/output_image.npy', input_image)
np.save('/pixel_data.npy', dict_pixels)

scores = get_scores(input_image)
best_score_tw =  scores['timber wolf']
best_score_gs = scores['Granny Smith']
print(best_score_tw, best_score_gs)

结果看起来像这样,它被分类为青苹果

原始图像由 AI Village 提供,许可 署名 4.0 国际 (CC BY 4.0)(由作者编辑)

放大结果

我的狼图像显然被篡改了,但这是一个高分辨率图像,攻击成功了。我相信如果再给点时间,可能会用更少的像素达到更好的欺骗效果。

像素化的狼被分类为青苹果。由 AI Village 提供,许可 署名 4.0 国际 (CC BY 4.0)(由作者编辑)

提醒一句…

在目睹了一个可能的像素攻击版本,这个版本仅根据预测分数和试验与错误导致了模型的错误分类之后,让我们进一步探讨如何避免这种情况。

当然,这里的目标不是鼓励执行像素攻击,除非这是对自己模型的抗性检查。探索对抗性机器学习实践的复杂性本质上是为了培养如何保护模型不受这些方法影响的意识。

所以让我们深入探讨如何避免这些情况…

像素攻击的可能弱点

像素攻击,特别是在黑箱设置中,已经涉及到大量的试验和错误,但各种策略可以进一步增强模型对这些攻击的鲁棒性。

1. 使用高分辨率图像

高分辨率图像更难被攻击,因为它们需要更多的资源和更高数量的改变特征/像素,因此更难以微妙地篡改。

图像由作者提供

澄清:例如,考虑一个来自 CIFAR 的 32x32 图像,它的像素较少,使其更容易受到篡改。相比之下,高分辨率图像由于像素数量更多,因此不易受到像素攻击。另一方面,这些图像虽然在隐蔽篡改时更具挑战,但在训练过程中可能会产生更高的计算成本。因此,需要在安全性和计算效率之间找到平衡。

提高接受结果的预测分数阈值

由于被攻击的图像具有较低的预测分数,可以利用分数阈值来检测潜在的对抗性攻击。

作者提供的图片

澄清:例如,设置一个阈值,低于该阈值的预测被视为不确定,这为防御对抗性攻击提供了额外的安全层。

再次值得指出的是,这是一个权衡,高阈值提升了信心,但可能会限制分类器的敏感度。找到合适的平衡对于避免拒绝有效预测同时抵御对抗性攻击至关重要。

考虑到 CNN 在关键应用中的鲁棒性

结果表明,尽管卷积神经网络(CNNs)并非免疫,但由于利用了空间层次结构,它们对这种对抗性攻击的敏感性较低。

作者提供的图片

澄清:简单来说,虽然平均模型将像素视为单独的输入,但卷积神经网络(CNN)通过内核窗口考虑预定义的关联,从而增强了对抗性操控的鲁棒性。

预测前的图像预处理

在将图像输入神经网络进行预测之前,应用一种稳健的预处理技术可能是值得的,从而限制黑箱攻击。

作者提供的图片

澄清:例如,图像压缩有助于减少篡改的影响,而计算机视觉算法可以识别图像中的失真或异常。此外,由于被操控的像素可能与原始图像的颜色或模式不完全匹配,因此可以应用插值技术。

安全的机器学习模型

上述方法虽然有效,但并非一刀切。最终,保护某个机器学习模型免受对抗性攻击包括在各种条件下(包括暴露于潜在对抗性输入)对模型进行严格的测试和验证。

决定添加多少安全性以及多频繁更新模型取决于其重要性和可能面临的威胁类型。但是,了解伦理考虑和理解可能的威胁有助于减少攻击风险。

总结……

虽然像素攻击或任何图像的操控对基于图像的人工智能系统确实是一个大问题,但我们也可以做很多事情来防范这些攻击。攻击者可以篡改单个像素来欺骗模型,使其犯错误,从而危害图像识别和安全系统等关键应用的可靠性。这不仅会导致安全漏洞,还会破坏客户和利益相关者的信任。

另一方面,机器学习从业者拥有工具来确保模型不容易受到这些攻击的影响。

在这篇文章中,我尝试探索了像素攻击,受到 CTF 挑战的启发,并深入研究了欺骗图像分类模型的一些复杂性。虽然狼确实变成了一个青苹果,但这需要大量的计算和反复试验,如果模型采取了一些预防措施,攻击将会失败。

我在下面列出了一些类似的方法资源,希望你能发现这些话题对保持模型安全有用。

资源

[## GitHub - Hyperparticle/one-pixel-attack-keras: Keras 实现的“单像素攻击”…

使用差分进化在 Cifar10 上进行“单像素攻击以欺骗深度神经网络”的 Keras 实现和…

github.com [## GitHub - max-andr/square-attack: Square Attack: 一个查询高效的黑箱对抗攻击,通过…

Square Attack: 一个查询高效的黑箱对抗攻击,通过随机搜索 [ECCV 2020] - GitHub …

github.com [## GitHub - kenny-co/procedural-advml: 无任务通用黑箱攻击计算机视觉…

无任务通用黑箱攻击计算机视觉神经网络通过过程噪声(CCS'19) - GitHub …

github.com

posted @ 2024-10-12 19:57  绝不原创的飞龙  阅读(231)  评论(0)    收藏  举报