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

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

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

使用 NumPy 实现高效的 k-近邻(k-NN)解决方案

原文:towardsdatascience.com/efficient-k-nearest-neighbors-k-nn-solutions-with-numpy-58cbac2a0971

快速计算

利用 NumPy 的广播、花式索引和排序进行性能计算

Peng QianTowards Data Science Peng Qian

·发布在数据科学前沿 ·阅读时间 9 分钟·2023 年 7 月 20 日

--

图片来源:作者创建,Canva

介绍

我有一个朋友是一名城市规划师。一天,他被要求重新评估城市中成千上万的加油站的位置适宜性,需要找出每个加油站的 k 个最近加油站的位置。

我们如何在很短的时间内找到最近的 k 个加油站?这是 k-近邻问题的一个实际应用场景。

因此,他来找我寻求帮助,希望我能提供一个高性能的解决方案。

所以我写下了这篇文章,它将指导你如何使用 NumPy 高效解决 k-近邻问题。通过与 Python 迭代解决方案进行比较,我们将展示 NumPy 的强大性能。

在本文中,我们将深入探讨利用高级 NumPy 特性,如广播、花式索引和排序,来实现高性能的 k-近邻算法。

阅读本文后,你将能够:

  • 了解 k-近邻问题及其实际应用场景

  • 学习如何使用 NumPy 库解决 k-近邻问题

  • 深入理解 NumPy 广播、花式索引和排序等特性在算法中的作用

  • 比较 NumPy 与 Python 迭代解决方案的性能,探索为什么 NumPy 更优

让我们一起深入探讨NumPy 的高性能世界,探索如何仅使用 NumPy 更快、更有效地解决 k-近邻问题。

k-NN 问题的几何原理

从几何角度回顾我朋友面临的加油站问题。

假设我们将所有的加油站放置在二维平面上,加油站之间的距离实际上是平面上两个点之间的欧几里得距离。解决公式如下:

由作者创建,Embed Fun

但是,任意两个点之间的距离应该如何计算呢?

我们可以将二维平面想象成一个棋盘,将加油站简化为六个,并将这六个点依次排列在棋盘的水平和垂直边缘,如图所示:

在棋盘上排列这六个点。图像由作者提供

然后,任意两个点的延伸线相交的网格可以表示这两个点之间的距离。当i=j时,这两个点是相同的,距离应该是 0。

假设这里的k=2,我们只需将每个点到其他点的距离按升序排序,然后取出前三个距离(包括自身),这些点就是离这个点最近的两个点。

排序后,我们可以得到彼此最接近的 3 个点。图像由作者提供

传统的 Python 迭代解决方案

作为性能基准,我们先来看传统的 Python 迭代解决方案。

这个解决方案的思路相对简单:

  1. 计算坐标点自身与列表中其他坐标点之间的欧几里得距离。

  2. 然后比较当前点与其他点之间的距离。

  3. 选择满足要求的前 k 个点。

接下来是代码部分。

首先,我们随机生成六个坐标点。由于稍后将使用相同的坐标进行比较,因此我们需要为random包添加一个seed

import random
import matplotlib.pyplot as plt

%matplotlib inline
plt.style.use('seaborn-v0_8-whitegrid')
random.seed(5)

def generate_points(n: int=6) -> list[tuple]:
    points = []
    for i in range(n):
        points.append((random.randint(0, 100), random.randint(0, 100)))
    return points

接下来,开始计算每个点到列表中所有点(包括自身)的距离,这需要两个迭代。

def calc_dist(points: list[tuple]) -> list[list]:
    result = []
    for i, left in enumerate(points):
        row = [left]
        for j, right in enumerate(points):
            dist = (left[0] - right[0])**2 + (left[1] - right[1])**2
            row.append(dist)
        result.append(row)
    return result

然后,对每个点与其他点之间的距离进行排序,并在原始列表中找到与距离对应的点的索引。

def find_sorted_index(with_dist: list[list]) -> list[list]:
    results = []
    for row in with_dist:
        dists = row[1:]
        sorted_dists = sorted(dists)
        indices = [dists.index(i) for i in sorted_dists]
        row[1:] = indices
        results.append(row)
    return results

最终返回应该是一个二维数组,其中数组的每一行的第一个项目是当前点,其他项目是排序后每个点在列表中的索引。

最后,我们根据索引在原始坐标列表中找到每个符合条件的点。

def find_k_nearest(points: list[tuple], with_indices: list[list], k: int) -> list[tuple]:
    results = []
    for row in with_indices:
        # Since the closest point to the current point is itself, we can get the point itself directly, so here is +2
        k_indices = row[1:k+2]
        the_points = [points[i] for i in k_indices]
        results.append(the_points)
    return results

结果是一个二维数组,数组的每一行是当前点及其他两个最近的点。

为了方便评估结果,我们使用Matplotlib绘制所有坐标点以及从每个坐标到两个最近坐标的线条。

def draw_points(points: list[tuple]):
    x, y = [], []
    for point in points:
        x.append(point[0])
        y.append(point[1])
    plt.scatter(x, y, s=100)

def draw_lines(nearest: list[list]):
    for row in nearest:
        start = row[0]
        for end in row[1:]:
            plt.plot([start[0], end[0]], [start[1], end[1]], color='black')

def orig_main(count: int = 6):
    k = 2
    points = generate_points(count)
    with_dist = calc_dist(points)
    sorted_index = find_sorted_index(with_dist)
    nearest = find_k_nearest(points, sorted_index, k)
    return points, nearest

points, nearest =  orig_main(6)
draw_points(points)
draw_lines(nearest)

结果如下:

传统的 Python 迭代解决方案。图像由作者提供

如你所见,图表上出现了六个坐标和相应的线条。

这个图表将作为基准,并将与稍后使用 NumPy 的结果进行比较,以确认算法的正确性。

使用 NumPy 解法的基础知识

接下来,让我们看看如何使用 NumPy 来解决这个问题。

在编写代码之前,我们需要对 NumPy 的一些基本概念进行预热。

广播

由于涉及将一组坐标点水平放置(shape=(1, 6))和垂直放置(shape=(6, 1)),并形成一个 (6, 6) 的矩阵。

在计算距离之后,涉及两个不同大小数组之间的操作,因此我们需要使用 NumPy 的广播机制。

这里是一个例子:

In:    a = np.arange(6).reshape(1, 6)
       b = np.arange(6).reshape(6, 1)
       a + b
Out:   [[ 0  1  2  3  4  5]
        [ 1  2  3  4  5  6]
        [ 2  3  4  5  6  7]
        [ 3  4  5  6  7  8]
        [ 4  5  6  7  8  9]
        [ 5  6  7  8  9 10]]

如你所见,当 (1, 6) 的数组和 (6, 1) 的数组相加时,结果的形状是 (6, 6)。

关于具体的原理,请参阅 官方文档。示意图如下:

广播如何工作。图像由作者提供

排序

在解决了任意两个点之间的距离之后,我们还需要对这些距离进行排序。

就像 Python 标准库中的 sort() 函数一样,NumPy 也有一个排序函数:np.sort()。另外,ndarray.sort() 函数也可以用于排序。

由于我们在对距离进行排序,因此还需要在排序后找到每个项目在原始数组中的索引。在 NumPy 中,我们可以使用 np.argsort() 来获取:

In:  x = np.array([2, 1, 4, 3, 5])
     i = np.argsort(x)
     print(i)
Out: [1 0 3 2 4]

当然,我们只需要关注 k 最近的点,不需要知道距离的顺序。

因此,我们可以使用 NumPy 的 [argpartition()](https://numpy.org/doc/stable/reference/generated/numpy.argpartition.html) API,它可以在不排序的情况下返回最小几个点的索引,这样性能会更好。

花式索引

在传统的 Python 列表中,如果我们想通过索引查找一组数据,我们需要分别遍历数据列表和索引列表,这样性能非常差。

但 NumPy 提供了花式索引,以快速查找与索引对应的数据。这里是一个例子:

In:  x = np.array([8, 2, 4, 5, 3, 7, 1, 6])
     ind = [0, 3, 7]
     print(x[ind])
Out: [8 5 6]

花式索引可以快速查找与索引数组对应的数据。图像由作者提供

由于花式索引是一组整数数组,因此有一个规则需要遵循:

索引的数据反映了广播索引数组的形状,与数据数组的形状无关。

NumPy 解法

在了解了 NumPy 的一些基础知识之后,让我们看看如何使用 NumPy 解决 k-NN 问题。

由于我们在这里使用一组坐标点来形成一个数组,我们需要使用 NumPy 的 structured_array

import numpy as np
from numpy import ndarray

random.seed(5)

def structured_array(points: list[tuple]) -> ndarray:
    dt = np.dtype([('x', 'int'), ('y', 'int')])
    return np.array(points, dtype=dt)

接下来,在水平和垂直方向上向原始的一维数组添加一个额外的维度,将其转变为二维棋盘的两个边:

然后使用广播机制来计算每个点之间的距离。

最终,得到一个 (6, 6) 的二维数组:

def np_find_dist(s_array: ndarray) -> ndarray:
    a = s_array.reshape(6, 1)
    b = s_array.reshape(1, 6)
    dist = (a['x'] - b['x'])**2 + (a['y'] - b['y'])**2
    return dist

然后,使用argpartition方法找出每一行中距离最小的两个点的索引:

def np_k_nearest(dist: ndarray, k: int) -> ndarray:
    k_indices = np.argpartition(dist, k+1, axis=1)[:, :k+1]
    return k_indices

我们仍需要两个Matplotlib绘图方法来评估结果的正确性:

def np_draw_points(s_array: ndarray):
    plt.scatter(s_array['x'], s_array['y'], s=100)

def np_draw_lines(s_array: ndarray, k_indices: ndarray, k: int):
    for i in range(s_array.shape[0]):
        for j in k_indices[i, :k+1]:
            plt.plot([s_array[i]['x'], s_array[j]['x']],
                     [s_array[i]['y'], s_array[j]['y']],
                     color='black')

最后,编写一个主方法将所有代码整合在一起:

def np_main(count: int = 6):
    k = 2
    points = generate_points(count)
    s_array = structured_array(points)
    np_dist = np_find_dist(s_array)
    k_indices = np_k_nearest(np_dist, k)

    results = [s_array[k_indices[i, :k+1]] 
               for i in range(s_array.shape[0])]
    return results, s_array, k_indices, k

results, s_array, k_indices, k = np_main(6)
np_draw_points(s_array)
np_draw_lines(s_array, k_indices, k)

仅从代码来看,它已经比 Python 迭代版本简洁得多。接下来,我们用图表比较结果:

NumPy 解决方案的 k-NN 结果。图片由作者提供

看,结果完全相同!

两种解决方案的性能比较

最后,我们来比较这两种解决方案的执行性能。这里我们仍然使用%timeit进行评估。

首先是 Python 迭代方法。让我们看看扩展到 1,000 个坐标需要多长时间:

Python 迭代解决方案的执行时间。图片由作者提供

然后是 NumPy 实现。看看 1,000 个坐标需要多长时间:

NumPy 解决方案的执行时间。图片由作者提供

惊讶吧?性能提高了数百倍,所以我的朋友不必担心计算不了。

结论

本文教会了我们如何使用 NumPy 的广播、花式索引和排序来高效解决 k 最近邻问题。

我们还比较了 NumPy 与 Python 迭代解决方案的性能,并深入理解了为什么 NumPy 在解决这类问题时表现更好。

总结一下,我们学到了以下内容:

  • k 最近邻问题的定义及实际应用场景

  • 如何使用 NumPy 库解决 k 最近邻问题

  • NumPy 的广播、花式索引、排序及其他特性在算法实现中的应用

  • NumPy 与 Python 暴力解决方案的性能比较分析

虽然本文提供了一种高效的 k 最近邻解决方案,但这只是一个起点。

在未来的文章中,我将使用高级算法和数据结构重新解释这个问题的解决方案,展示更多高效且可用的算法技巧。

敬请关注未来的文章。如果你对本文感兴趣,欢迎评论,我会逐一回复。

让我从基础开始,带你了解工作中的最佳科学计算实践。

Peng Qian

Peng Qian

快速计算

查看列表4 篇故事!如何用 Numexpr 优化多维 Numpy 数组操作。用 Numexpr 帮助我找到最宜居的城市。高效的 k-最近邻 (k-NN) 解决方案与 NumPy [## 通过我的推荐链接加入 Medium - 彭倩

作为 Medium 会员,你的会员费的一部分将直接支持你阅读的作者,并且你可以完全访问每一篇故事……

medium.com](https://medium.com/@qtalen/membership?source=post_page-----58cbac2a0971--------------------------------)

这篇文章最初发布在:www.dataleadsfuture.com/efficient-k-nearest-neighbors-k-nn-solutions-with-numpy/

使用瓶颈适配器进行高效模型微调

原文:towardsdatascience.com/efficient-model-fine-tuning-with-bottleneck-adapter-5162fcec3909

如何使用瓶颈适配器微调基于 Transformer 的模型

Ruben WinastwanTowards Data Science Ruben Winastwan

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

--

照片来源:Karolina Grabowska: www.pexels.com/photo/set-of-modern-port-adapters-on-black-surface-4219861/

微调是我们为了在特定任务中从深度学习模型中获得更好性能时可以做的最常见的事情之一。我们需要微调模型的时间通常与模型的大小成正比:模型越大,微调所需的时间就越长。

我们可以达成一致的是,如今,基于 Transformer 的深度学习模型正变得越来越复杂。总体来说,这是一个值得关注的好现象,但它有一个警告:它们往往拥有庞大的参数量。因此,微调大型模型变得越来越难以管理,我们需要一种更高效的方法来进行微调。

在本文中,我们将讨论一种称为瓶颈适配器的高效微调方法。虽然您可以将这种方法应用于任何深度学习模型,但我们将重点关注其在基于 Transformer 的模型中的应用。

本文的结构如下:首先,我们将对一个特定数据集进行正常的 BERT 模型微调。然后,我们将借助adapter-transformers库将一些瓶颈适配器插入到 BERT 模型中,以查看它们如何帮助我们使微调过程更高效。

在我们微调模型之前,让我们先介绍一下我们将使用的数据集。

关于数据集

我们即将使用的数据集包含从 Reddit 收集的与心理健康相关的不同类型文本(许可协议为 CC-BY-4.0)。该数据集适用于文本分类任务,我们可以预测给定文本是否包含抑郁情绪。让我们看一下它的样本。

!pip install datasets

from datasets import load_dataset

dataset = load_dataset("mrjunos/depression-reddit-cleaned")
print(dataset['train'][2])

'''
{'text': 'anyone else instead of sleeping more when depressed stay up all night to avoid the next day from coming sooner may be the social anxiety in me but life is so much more peaceful when everyone else is asleep and not expecting thing of you',
 'label': 1}
'''

如你所见,数据集非常简单,因为我们只有两个字段:一个是文本,另一个是标签。标签本身只有两个可能的值:如果文本包含抑郁情绪则为 1,否则为 0。我们的任务是微调一个预训练的 BERT 模型,以预测每个文本的情感。

总共有大约 7731 个文本,我们将使用其中 6500 个进行训练,其余 1231 个用于微调过程中的验证。

让我们创建一个数据加载器,在微调过程中批量加载数据集,我们将在下一节中看到:

!pip install pip install adapter-transformers

import torch
import numpy as np
from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained('bert-base-cased')

class Dataset(torch.utils.data.Dataset):

    def __init__(self, input_data):
        self.labels = [data for data in input_data['label']]
        self.texts = [tokenizer(data,
                               padding='max_length', max_length = 512, truncation=True,
                                return_tensors="pt") for data in input_data['text']]

    def __len__(self):
        return len(self.labels)

    def get_batch_labels(self, idx):
        return np.array(self.labels[idx])

    def get_batch_texts(self, idx):
        return self.texts[idx]

    def __getitem__(self, idx):
        batch_texts = self.get_batch_texts(idx)
        batch_y = self.get_batch_labels(idx)
        return batch_texts, batch_y

现在我们有了数据,可以开始讨论本文的主要话题。然而,如果我们已经熟悉普通微调的标准过程,那么理解瓶颈适配器的概念会更容易。

因此,在下一节中,我们将从普通微调过程的概念开始,然后扩展到瓶颈适配器的应用。

我们将使用adapter-transformers库来进行普通微调和基于适配器的微调。这个库是著名的 HuggingFace 的transformers库的直接分支,这意味着它包含了transformers的所有功能,并增加了几个模型类和方法,以便我们可以轻松地将适配器应用到模型中。

你可以使用以下命令安装adapter-transformers

pip install adapter-transformers

现在让我们开始讨论普通微调的常规过程。

普通 BERT 微调

微调是深度学习中的一种常见技术,旨在从预训练模型中获得在特定数据和/或任务上的更好性能。其主要思想很简单:我们获取预训练模型的权重,然后基于新的领域特定数据更新这些权重。

普通微调过程。图片由作者提供。

普通微调的常规过程如下。

首先,我们选择一个预训练模型,在我们的情况下是 BERT-base 模型。顺便提一下,我们在这篇文章中不会专注于 BERT,但如果你对 BERT 不熟悉并想了解更多,可以查看我关于 BERT 的文章:

## 使用 BERT 进行 PyTorch 中的文本分类

如何利用 Hugging Face 的预训练 BERT 模型来分类新闻文章的文本

[towardsdatascience.com

简而言之,BERT-base 包含 12 层 Transformer 编码器。在微调过程中,我们需要在最后一层上添加一个线性层,作为分类器。由于我们数据集中的标签仅包含两个可能的值,因此我们的线性层的输出也将是两个。

from torch import nn
from transformers import BertForSequenceClassification

class BertClassifier(nn.Module):

    def __init__(self, model_id='bert-base-cased', num_class=2):
        super(BertClassifier, self).__init__()
        self.bert = BertForSequenceClassification.from_pretrained(model_id, num_labels=num_class)

    def forward(self, input_id, mask):
        output = self.bert(input_ids= input_id, attention_mask=mask,return_dict=False)
        return output

BERT 架构。图片由作者提供。

现在我们已经定义了我们的模型,我们需要创建微调脚本。以下是对模型进行微调的代码片段。

from torch.optim import Adam
from tqdm import tqdm

def train(model, train_data, val_data, learning_rate, epochs):

    # Fetch training and validation data in batch
    train, val = Dataset(train_data), Dataset(val_data)
    train_dataloader = torch.utils.data.DataLoader(train, batch_size=2, shuffle=True)
    val_dataloader = torch.utils.data.DataLoader(val, batch_size=2)

    use_cuda = torch.cuda.is_available()
    device = torch.device("cuda" if use_cuda else "cpu")

    criterion = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr= learning_rate)

    if use_cuda:
       model = model.to(device)
       criterion = criterion.to(device)

    for epoch_num in range(epochs):

        total_acc_train = 0
        total_loss_train = 0

        # Fine-tune the model
        for train_input, train_label in tqdm(train_dataloader):

            train_label = train_label.to(device)
            mask = train_input['attention_mask'].to(device)
            input_id = train_input['input_ids'].squeeze(1).to(device)

            output = model(input_id, mask)[0]

            batch_loss = criterion(output, train_label.long())
            total_loss_train += batch_loss.item()

            acc = (output.argmax(dim=1) == train_label).sum().item()
            total_acc_train += acc

            model.zero_grad()
            batch_loss.backward()
            optimizer.step()

        total_acc_val = 0
        total_loss_val = 0

        # Validate the model
        with torch.no_grad():

            for val_input, val_label in val_dataloader:
                val_label = val_label.to(device)
                mask = val_input['attention_mask'].to(device)
                input_id = val_input['input_ids'].squeeze(1).to(device)

                output = model(input_id, mask)[0]

                batch_loss = criterion(output, val_label.long())
                total_loss_val += batch_loss.item()
                acc = (output.argmax(dim=1) == val_label).sum().item()
                total_acc_val += acc

        print(
            f'Epochs: {epoch_num + 1} | Train Loss: {total_loss_train / len(train): .3f} \
            | Train Accuracy: {total_acc_train / len(train): .3f} \
            | Val Loss: {total_loss_val / len(val): .3f} \
            | Val Accuracy: {total_acc_val / len(val): .3f}')

我们将对我们的 BERT 模型进行大约 10 个周期的微调,学习率设置为 10e-7。 我在 T4 GPU 上使用批量大小为 2 进行了模型微调。以下是训练和验证准确度的快照。

EPOCHS = 10
LR = 1e-7

model = BertClassifier()
data = dataset['train'].shuffle(seed=42)
train(model, data[:6500], data[6500:], LR, EPOCHS)

100%|███████████████████████████████████ 3250/3250 [11:56<00:00,  4.54it/s]
Epochs: 1 | Train Loss:  0.546 | Train Accuracy:  0.533 | Val Loss:  0.394 | Val Accuracy:  0.847
100%|███████████████████████████████████ 3250/3250 [11:57<00:00,  4.53it/s]
Epochs: 2 | Train Loss:  0.302 | Train Accuracy:  0.888 | Val Loss:  0.226 | Val Accuracy:  0.906
100%|███████████████████████████████████ 3250/3250 [11:57<00:00,  4.53it/s]
Epochs: 3 | Train Loss:  0.184 | Train Accuracy:  0.919 | Val Loss:  0.149 | Val Accuracy:  0.930
100%|███████████████████████████████████ 3250/3250 [11:57<00:00,  4.53it/s]
Epochs: 4 | Train Loss:  0.122 | Train Accuracy:  0.946 | Val Loss:  0.101 | Val Accuracy:  0.955
100%|███████████████████████████████████ 3250/3250 [11:57<00:00,  4.53it/s]
Epochs: 5 | Train Loss:  0.084 | Train Accuracy:  0.963 | Val Loss:  0.075 | Val Accuracy:  0.968
100%|███████████████████████████████████ 3250/3250 [11:56<00:00,  4.53it/s]
Epochs: 6 | Train Loss:  0.063 | Train Accuracy:  0.969 | Val Loss:  0.061 | Val Accuracy:  0.970
100%|███████████████████████████████████ 3250/3250 [11:57<00:00,  4.53it/s]
Epochs: 7 | Train Loss:  0.050 | Train Accuracy:  0.974 | Val Loss:  0.054 | Val Accuracy:  0.973
100%|███████████████████████████████████ 3250/3250 [11:57<00:00,  4.53it/s]
Epochs: 8 | Train Loss:  0.042 | Train Accuracy:  0.978 | Val Loss:  0.049 | Val Accuracy:  0.972
100%|███████████████████████████████████ 3250/3250 [11:57<00:00,  4.53it/s]
Epochs: 9 | Train Loss:  0.035 | Train Accuracy:  0.982 | Val Loss:  0.047 | Val Accuracy:  0.973
100%|███████████████████████████████████ 3250/3250 [11:57<00:00,  4.53it/s]
Epochs: 10 | Train Loss: 0.030 | Train Accuracy:  0.984 | Val Loss:  0.046 | Val Accuracy:  0.966

就这样!我们在数据集上使用 BERT 达到了 97.3% 的验证准确率。然后我们可以继续使用微调后的模型对未见数据进行预测。

总体来说,如果我们的模型具有“小”数量的参数,正常的微调不会成为问题,如上所示的 BERT 模型。让我们检查一下我们的 BERT-base 模型的总参数数量。

def print_trainable_parameters(model):

    trainable_params = 0
    all_param = 0
    for _, param in model.named_parameters():
        all_param += param.numel()
        if param.requires_grad:
            trainable_params += param.numel()
    print(
        f"trainable params: {trainable_params} || all params: {all_param} || trainable%: {100 * trainable_params / all_param}"
    )

print_trainable_parameters(model)
'''
trainable params: 108311810 || all params: 108311810 || trainable%: 100.0
'''

这个模型总共有接近 1.1 亿个参数。虽然看起来很多,但与现在的大多数大型语言模型相比,这仍然不算什么,因为它们可能有数十亿个参数。如果你也注意到,可训练参数的数量与我们 BERT 模型的总参数数量相同。这意味着在正常的微调过程中,我们会更新 BERT 模型的所有参数的权重。

借助 T4 GPU 和我们的训练数据集仅包含 6500 条数据,我们幸运地只需大约 12 分钟每个周期来更新所有权重。现在想象一下,如果我们使用更大的模型和数据集,进行正常微调的计算时间将会非常昂贵。

此外,正常的微调通常与所谓的灾难性遗忘风险相关,如果我们在选择学习率时不小心,或者当我们尝试在多个任务/数据集上微调预训练模型时。灾难性遗忘指的是当我们在新任务上微调预训练模型时,它会“遗忘”其训练过的任务。

因此,我们确实需要一种更高效的程序来进行微调过程。这就是我们可以使用不同类型的高效微调方法的地方,其中瓶颈适配器就是其中之一。

瓶颈适配器的工作原理

适配器的主要思想是,我们引入一小部分层,并将其放置在预训练模型的原始架构中。在微调过程中,我们冻结预训练模型的所有参数,因此,只有这些附加子集层的权重会被更新。

瓶颈适配器特指一种由两个普通前馈层组成的适配器,前后可选地添加归一化层。一个前馈层的功能是缩小输出,而另一个是放大输出。这就是为什么它被称为瓶颈适配器的原因。

常见的瓶颈适配器。图片由作者提供。

你可以将这个适配器应用于任何深度学习模型,但如前所述,我们将重点关注其在基于 Transformer 的模型上的应用。

基于 Transformer 的模型通常由多个 Transformer 层堆叠组成。例如,本文使用的基于 BERT 的模型有 12 个 Transformer 编码器层堆叠。每个堆叠包括以下组件:

Transformer 编码器堆叠。图片由作者提供。

我们可以将瓶颈适配器放入这个堆叠的几种不同方式。然而,有两种常见的配置:一种是 Pfeiffer 提出的,另一种是 Houlsby 提出的。

Pfeiffer 提出的瓶颈适配器插入在最后的规范层之后,而 Houlsby 提出的瓶颈适配器插入在两个不同的位置:一个在多头注意力层之后,另一个在前馈层之后,如下图所示:

Pfeiffer 和 Houlsby 适配器配置的区别。图片由作者提供。

由于我们的 BERT-base 模型有 12 个 Transformer 编码器层堆叠,因此如果使用 Pfeiffer 配置,我们将有 12 个瓶颈适配器:每个堆叠一个适配器。同时,如果使用 Houlsby 配置,我们将有 24 个瓶颈适配器:每个堆叠两个适配器。

尽管 Pfeiffer 配置相比于 Houlsby 配置参数更少,但在 8 个不同任务中的表现相当。

现在的问题是:这个瓶颈适配器是如何让微调过程更高效的?

如前所述,我们在微调过程中冻结预训练模型的权重,只更新适配器的权重。这意味着我们可以显著加快微调过程,接下来的部分会展示这一点。实验也表明,使用适配器进行微调的性能通常与普通微调相当。

同时,假设我们想用相同的预训练模型处理两个不同的数据集。我们可以使用一个模型,通过两个不同的适配器在不同的数据集上进行微调,从而避免灾难性遗忘的风险,而不是拥有两个在不同数据集上微调的模型。

图片由作者提供。

使用这种方法,我们节省了大量存储空间。例如,一个单独的 BERT-base 模型的大小是 440 MB,如果我们有两个模型,则为 880 MB。与此同时,如果我们有一个带有两个适配器的 BERT-base 模型,大小大约为 450 MB,因为瓶颈适配器只占用少量内存。

瓶颈适配器实现

在本节中,我们将实现 Pfeiffer 版本的瓶颈适配器。为此,我们只需要更改模型架构的脚本,而与微调过程和数据加载相关的脚本保持不变。

让我们使用 Pfeiffer 的适配器定义模型架构。

from transformers import AdapterConfig
from transformers.adapters import BertAdapterModel

class BertClassifierWithAdapter(nn.Module):

    def __init__(self, model_id='bert-base-cased', adapter_id='pfeiffer', 
                task_id = 'depression_reddit_dataset', num_class=2):

        super(BertClassifierWithAdapter, self).__init__()

        self.adapter_config = AdapterConfig.load(adapter_id)

        self.bert = BertAdapterModel.from_pretrained(model_id)
        # Insert adapter according to configuration
        self.bert.add_adapter(task_id, config=self.adapter_config)
        # Freeze all BERT-base weights 
        self.bert.train_adapter(task_id)
        # Add prediction layer on top of BERT-base
        self.bert.add_classification_head(task_id, num_labels=num_class)
        # Make sure that adapters and prediction layer are used during forward pass
        self.bert.set_active_adapters(task_id)

    def forward(self, input_id, mask):

        output = self.bert(input_ids= input_id, attention_mask=mask,return_dict=False)

        return output

如你所见,实现适配器版本的模型非常简单:

  • 使用AdapterConfig.load('pfeiffer')定义我们想要应用的适配器配置。如果你想使用 Houlsby 配置,只需将其更改为'houlsby'

  • 使用add_adapter()方法将适配器插入到我们的 BERT 模型中。常见做法是根据任务或数据集为适配器命名,以便我们希望模型进行微调。

  • 使用train_adapter()方法冻结预训练模型的所有权重。

  • 使用add_classification_head()方法在 BERT 模型上添加一个线性层,作为预测头。常见做法是为预测头取与适配器相同的名称。

  • 激活我们的适配器和预测头,以确保它们在每次前向传递中都被使用,使用set_active_adapters()方法。

现在,让我们检查在添加适配器后参数的总数和可训练参数的比例:

# Initialize model 

# task_id is the name of our adapter. You can name it whatever you want but
# common practice is to name it according to task/dataset we will train it on.
task_name = 'depression_reddit_dataset'
model_adapter = BertClassifierWithAdapter(task_id=task_name)
# Check parameters
print_trainable_parameters(model_adapter)

'''
trainable params: 1486658 || all params: 109796930 || trainable%: 1.3540068925424418
'''

带有适配器的模型参数比我们原始的 BERT-base 模型多,但只有 1.35%是可训练的,因为我们只会更新适配器的权重。

现在是训练模型的时候了。由于适配器的权重是随机初始化的,因此这次我们将使用略高的学习率。我们还将训练该模型 10 个时期。如果一切顺利,你将获得类似如下的输出:

LR = 5e-6
EPOCHS = 10
train(model_adapter, dataset['train'][:6500], dataset['train'][6500:], LR, EPOCHS)

100%|███████████████████████████████████████████ 3250/3250 [07:19<00:00,  7.40it/s]
Epochs: 1 | Train Loss:  0.183 | Train Accuracy:  0.846  | Val Loss:  0.125  | Val Accuracy:  0.897
100%|███████████████████████████████████████████ 3250/3250 [07:24<00:00,  7.32it/s]
Epochs: 2 | Train Loss:  0.096 | Train Accuracy:  0.925  | Val Loss:  0.072  | Val Accuracy:  0.946
100%|███████████████████████████████████████████ 3250/3250 [07:23<00:00,  7.32it/s]
Epochs: 3 | Train Loss:  0.060 | Train Accuracy:  0.958  | Val Loss:  0.052  | Val Accuracy:  0.962
100%|███████████████████████████████████████████ 3250/3250 [07:21<00:00,  7.37it/s]
Epochs: 4 | Train Loss:  0.044 | Train Accuracy:  0.968  | Val Loss:  0.047  | Val Accuracy:  0.971
100%|███████████████████████████████████████████ 3250/3250 [07:25<00:00,  7.30it/s]
Epochs: 5 | Train Loss:  0.038 | Train Accuracy:  0.971  | Val Loss:  0.043  | Val Accuracy:  0.973
100%|███████████████████████████████████████████ 3250/3250 [07:25<00:00,  7.29it/s]
Epochs: 6 | Train Loss:  0.034 | Train Accuracy:  0.975  | Val Loss:  0.039  | Val Accuracy:  0.971
100%|███████████████████████████████████████████ 3250/3250 [07:25<00:00,  7.29it/s]
Epochs: 7 | Train Loss:  0.032 | Train Accuracy:  0.978  | Val Loss:  0.038  | Val Accuracy:  0.972
100%|███████████████████████████████████████████ 3250/3250 [07:25<00:00,  7.29it/s]
Epochs: 8 | Train Loss:  0.029 | Train Accuracy:  0.980  | Val Loss:  0.039  | Val Accuracy:  0.974
100%|███████████████████████████████████████████ 3250/3250 [07:25<00:00,  7.29it/s]
Epochs: 9 | Train Loss:  0.027 | Train Accuracy:  0.980  | Val Loss:  0.035  | Val Accuracy:  0.971
100%|███████████████████████████████████████████ 3250/3250 [07:19<00:00,  7.40it/s] 

如你所见,带有适配器的模型性能与完全微调版本的模型相当。而且,完成一个时期所需的时间比完全微调快 4.5 分钟。

既然我们已经训练好了它,我们可以保存适配器。

# Save trained model with adapter
model_adapter_path = 'model/bert_adapter/' 
model_adapter.bert.save_all_adapters(model_adapter_path)

然后我们可以加载适配器并进行推理,如下所示:

def predict(data, model):

    inputs = tokenizer(data['text'],
              padding='max_length', max_length=512, truncation=True,
              return_tensors="pt")

    mask = inputs['attention_mask'].to(device)
    input_id = inputs['input_ids'].squeeze(1).to(device)
    output = model(input_id, mask)[0].argmax(dim=1).item()
    print(output)

use_cuda = torch.cuda.is_available()
device = torch.device("cuda" if use_cuda else "cpu")

model_path = f'{model_adapter_path}{task_name}'

# Load trained adapter
trained_adapter_model = BertClassifierWithAdapter(task_id=task_name)
adapter_name = trained_adapter_model.bert.load_adapter(model_path)
trained_adapter_model.bert.set_active_adapters(adapter_name)
trained_adapter_model.to(device)
trained_adapter_model.eval()

# Predict test data
predict(data[6900], trained_adapter_model)

结论

在本文中,我们已经看到瓶颈适配器在大型模型的微调过程中是如何有帮助的。使用瓶颈适配器,我们能够加快微调速度,同时保持模型的最终性能。这些适配器也有助于避免通常与微调模型相关的灾难性遗忘风险。此外,这些适配器不会占用大量内存空间。

我希望这篇文章对你在使用瓶颈适配器时有所帮助。如果你想查看本文中实现的所有代码,可以通过这个笔记本访问。

数据集参考

在 Neo4j 中高效的语义搜索

原文:towardsdatascience.com/efficient-semantic-search-over-unstructured-text-in-neo4j-8179ad7ff451?source=collection_archive---------1-----------------------#2023-08-23

将新添加的向量索引集成到 LangChain 中,以提升你的 RAG 应用程序

Tomaz BratanicTowards Data Science Tomaz Bratanic

·

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

--

自从六个月前 ChatGPT 问世以来,技术领域经历了变革性的转变。ChatGPT 出色的概括能力减少了对专业深度学习团队和大量训练数据集的需求,从而使得创建定制的 NLP 模型变得更加容易。这使得诸如摘要和信息提取等 NLP 任务的获取变得比以往任何时候都更加容易。然而,我们很快意识到类似 ChatGPT 模型的局限性,如知识截止日期和无法访问私人信息。在我看来,紧接着出现的是生成性 AI 转型的第二波浪潮,即检索增强生成(RAG)应用的兴起,你可以在查询时向模型提供相关信息,以构建更好、更准确的答案。

RAG 应用流程。图像由作者提供。图标来自www.flaticon.com/

如前所述,RAG 应用程序需要一个智能搜索工具,能够根据用户输入检索额外信息,这使得 LLMs 能够生成更准确和最新的答案。最初,重点主要是使用语义搜索从非结构化文本中检索信息。然而,很快就明显看出,结构化和非结构化数据的组合是 RAG 应用程序的最佳方法,如果你想要超越“与 PDF 对话”应用

Neo4j 曾经并且现在仍然非常适合处理结构化信息,但由于其粗暴的方法,它在语义搜索方面有些吃力。然而,这种困难已经成为过去,因为 Neo4j 已经在5.11 版本中引入了新的向量索引,旨在高效地对非结构化文本或其他嵌入数据模式执行语义搜索。新添加的向量索引使 Neo4j 非常适合大多数 RAG 应用,因为它现在可以很好地处理结构化和非结构化数据。

在这篇博客文章中,我将向你展示如何在 Neo4j 中设置向量索引,并将其集成到LangChain 生态系统中。代码可以在GitHub上找到。

Neo4j 环境设置

你需要设置 Neo4j 5.11 或更高版本,以跟随本博客中的示例。最简单的方法是启动一个免费的Neo4j Aura实例,该平台提供 Neo4j 数据库的云实例。或者,你也可以通过下载Neo4j Desktop应用程序并创建一个本地数据库实例,来设置 Neo4j 数据库的本地实例。

实例化 Neo4j 数据库后,你可以使用 LangChain 库连接到它。

from langchain.graphs import Neo4jGraph

NEO4J_URI="neo4j+s://1234.databases.neo4j.io"
NEO4J_USERNAME="neo4j"
NEO4J_PASSWORD="-"

graph = Neo4jGraph(
    url=NEO4J_URI,
    username=NEO4J_USERNAME,
    password=NEO4J_PASSWORD
)

设置向量索引

Neo4j 向量索引由 Lucene 提供支持,其中 Lucene 实现了一个层次导航的小世界 (HNSW) 图,以在向量空间上执行近似最近邻 (ANN) 查询。

Neo4j 对向量索引的实现旨在索引节点标签的单个节点属性。例如,如果你想要对标签为 Chunk 的节点的 embedding 属性进行索引,你可以使用以下 Cypher 程序。

CALL db.index.vector.createNodeIndex(
  'wikipedia', // index name
  'Chunk',     // node label
  'embedding', // node property
   1536,       // vector size
   'cosine'    // similarity metric
)

除了索引名称、节点标签和属性外,你还必须指定向量大小(嵌入维度)和相似度度量。我们将使用 OpenAI 的 text-embedding-ada-002 嵌入模型,该模型使用向量大小 1536 来表示嵌入空间中的文本。目前,仅提供 余弦欧几里得 相似度度量。OpenAI 建议在使用其嵌入模型时使用余弦相似度度量。

填充向量索引

Neo4j 的设计是无模式的,这意味着它不强制对节点属性中的内容施加任何限制。例如,embedding属性可以存储整数、整数列表甚至字符串。我们来尝试一下。

WITH [1, [1,2,3], ["2","5"], [x in range(0, 1535) | toFloat(x)]] AS exampleValues
UNWIND range(0, size(exampleValues) - 1) as index
CREATE (:Chunk {embedding: exampleValues[index], index: index})

这个查询为列表中的每个元素创建一个 Chunk 节点,并将元素用作 embedding 属性值。例如,第一个 Chunk 节点的 embedding 属性值为 1,第二个节点为 [1,2,3],依此类推。Neo4j 对节点属性下可以存储的内容没有强制规则。然而,向量索引对它应该索引的值类型和嵌入维度有明确的指示。

我们可以通过执行向量索引搜索来测试哪些值已被索引。

CALL db.index.vector.queryNodes(
  'wikipedia', // index name
   3, // topK neighbors to return
   [x in range(0,1535) | toFloat(x) / 2] // input vector
)
YIELD node, score
RETURN node.index AS index, score

如果你运行这个查询,你将只返回一个单独的节点,即使你请求返回前 3 个邻居。这是为什么呢?向量索引仅索引属性值,其中值是具有指定大小的浮点数列表。在这个示例中,只有一个 embedding 属性值具有浮点数列表类型,并且长度为 1536。

如果满足以下所有条件,则节点会按向量索引进行索引:

  • 节点包含配置的标签。

  • 节点包含配置的属性键。

  • 相应的属性值类型为 LIST<FLOAT>

  • 相应值的 [size()](https://neo4j.com/docs/cypher-manual/current/functions/scalar/#functions-size) 与配置的维度是相同的。

  • 该值是配置的相似度函数的有效向量。

将向量索引集成到 LangChain 生态系统中

现在我们将实现一个简单的自定义 LangChain 类,该类将使用 Neo4j 向量索引来检索相关信息,以生成准确和最新的答案。但首先,我们必须填充向量索引。

使用 Neo4j 向量索引在 RAG 应用中的数据流。图像由作者提供。图标来自 flaticons。

任务将包括以下步骤:

  • 检索一篇维基百科文章

  • 切分文本

  • 在 Neo4j 中存储文本及其向量表示

  • 实现一个自定义 LangChain 类以支持 RAG 应用

在这个例子中,我们将只获取一篇维基百科文章。我决定使用Baldur’s Gate 3 页面

import wikipedia
bg3 = wikipedia.page(pageid=60979422)

接下来,我们需要对文本进行切分和嵌入。我们将通过双换行符分隔符按部分切分文本,然后使用 OpenAI 的嵌入模型为每个部分表示一个合适的向量。

import os
from langchain.embeddings import OpenAIEmbeddings

os.environ["OPENAI_API_KEY"] = "API_KEY"

embeddings = OpenAIEmbeddings()

chunks = [{'text':el, 'embedding': embeddings.embed_query(el)} for
                  el in bg3.content.split("\n\n") if len(el) > 50]

在我们继续讲解 LangChain 类之前,需要将文本块导入 Neo4j。

graph.query("""
UNWIND $data AS row
CREATE (c:Chunk {text: row.text})
WITH c, row
CALL db.create.setVectorProperty(c, 'embedding', row.embedding)
YIELD node
RETURN distinct 'done'
""", {'data': chunks})

有一点值得注意的是,我使用了db.create.setVectorProperty过程来将向量存储到 Neo4j 中。此过程用于验证属性值确实是浮点数列表。此外,它还有助于将向量属性的存储空间减少约 50%。因此,建议始终使用此过程将向量存储到 Neo4j 中。

现在我们可以实现自定义的 LangChain 类,用于从 Neo4j 向量索引中检索信息,并用它生成答案。首先,我们将定义用于检索信息的 Cypher 语句。

vector_search = """
WITH $embedding AS e
CALL db.index.vector.queryNodes('wikipedia',$k, e) yield node, score
RETURN node.text AS result
"""

如您所见,我已硬编码了索引名称。如果您愿意,可以通过添加适当的参数使其动态化。

自定义的 LangChain 类实现得非常直接。

class Neo4jVectorChain(Chain):
    """Chain for question-answering against a Neo4j vector index."""

    graph: Neo4jGraph = Field(exclude=True)
    input_key: str = "query"  #: :meta private:
    output_key: str = "result"  #: :meta private:
    embeddings: OpenAIEmbeddings = OpenAIEmbeddings()
    qa_chain: LLMChain = LLMChain(llm=ChatOpenAI(temperature=0), prompt=CHAT_PROMPT)

    def _call(self, inputs: Dict[str, str], run_manager, k=3) -> Dict[str, Any]:
        """Embed a question and do vector search."""
        question = inputs[self.input_key]

        # Embed the question
        embedding = self.embeddings.embed_query(question)

        # Retrieve relevant information from the vector index
        context = self.graph.query(
            vector_search, {'embedding': embedding, 'k': 3})
        context = [el['result'] for el in context]

        # Generate the answer
        result = self.qa_chain(
            {"question": question, "context": context},
        )
        final_result = result[self.qa_chain.output_key]
        return {self.output_key: final_result}

我省略了一些模板代码以使其更易读。实质上,当您调用 Neo4jVectorChain 时,会执行以下步骤:

  1. 使用相关的嵌入模型对问题进行嵌入

  2. 使用文本嵌入值从向量索引中检索最相似的内容

  3. 使用类似内容提供的上下文生成答案

我们现在可以测试我们的实现。

vector_qa = Neo4jVectorChain(graph=graph, embeddings=embeddings, verbose=True)
vector_qa.run("What is the gameplay of Baldur's Gate 3 like?")

响应

生成的响应。图像由作者提供。

通过使用verbose选项,您还可以评估从向量索引中检索的上下文,这些上下文用于生成答案。

总结

利用 Neo4j 的新向量索引功能,您可以创建一个统一的数据源,有效支持检索增强生成应用。这不仅使您能够实现“与 PDF 或文档聊天”解决方案,还能进行实时分析,所有这些都来自一个强大的数据源。这种多用途的实用工具可以简化您的操作并增强数据协同,使 Neo4j 成为管理结构化和非结构化数据的绝佳解决方案。

一如既往,代码可在GitHub上找到。

高效服务开源 LLM

原文:towardsdatascience.com/efficiently-serving-open-source-llms-5f0bf5d8fd59

Ryan ShrottTowards Data Science Ryan Shrott

·发布在 Towards Data Science ·阅读时长 5 分钟·2023 年 8 月 14 日

--

图片来源:Mariia ShalabaievaUnsplash

本文解释了我个人使用 6 种常见方法服务开源 LLM 的经验:AWS Sage Maker、Hugging Face、Together.AI、VLLM 和 Petals.ml。

挣扎…

你已经感受到了服务自己微调的开源 LLM 的痛苦、挣扎和荣耀,但你最终因为成本、推理时间、可靠性和技术挑战而决定回到 Open AI 或 Anthropic 😦 你也放弃了租用 A100 GPU(许多供应商的 GPU 已经被预订到 2023 年底!)。你也没有 10 万美元去购买一个 2 级 A100 服务器。尽管如此,你仍在梦想,你真的希望开源能够为你的解决方案服务。也许你的公司不愿意将私人数据发送给 Open AI,或者你需要一个针对特定任务微调的模型?在本文中,我将概述并比较一些最有效的推理方法/平台,用于服务开源 LLM。在 2023 年,我将对 6 种方法进行比较,并解释何时应该使用其中一种或另一种。我亲自尝试了这 6 种方法,并将详细介绍我的个人经验:AWS Sage Maker、Hugging Face 推理端点、Together.AI、VLLM 和 Petals.ml。我没有所有的答案,但我会尽力详细说明我的经验。我与这些供应商没有任何经济联系,仅仅是分享我的经验以造福他人。请分享你的经验!

为什么选择开源?

开源模型有许多优点,包括控制、隐私和潜在的成本降低。例如,你可以针对特定的使用案例微调一个较小的开源模型,从而获得准确的结果和快速的推理时间。隐私控制意味着推理可以在自己的服务器上完成。另一方面,成本降低比你想象的要困难得多。OpenAI 拥有规模经济,定价具有竞争力。他们的 GPT-3.5 Turbo 定价模式很难与之竞争,并且已被证明类似于电力成本。不过,你仍然可以采用一些方法和技巧来节省开支,并用开源模型获得优秀的结果。例如,我的微调模型 Stable Beluga 2 目前显著优于 GPT-3.5 Turbo,并且在我的应用中更便宜。因此,我强烈建议你尝试使用开源模型。

Hugging Face 推理端点

这是服务开源 LLM 最常见且最简单的方法。只需点击几下即可,且几乎没有错误。毕竟,Hugging Face 最初是一家 NLP 公司。你的模型很可能已经存在于 Hugging Face 上,因此这是快速测试模型的首选选项。GPU 服务器成本往往较高。例如,如果你仅使用 RunPod.io 部署模型,你将有更多的提供商选择,并且成本更低。Hugging Face 已开源了他们的 Transformers 推理库,并提供了易于修改的 Docker 镜像。因此,如果你需要更多控制,可以选择在 RunPod 上的自定义解决方案。 这是一个关于如何在 RunPod 上操作的教程。

VLLM

这个解决方案由于其推理速度而非常有趣。他们声称比 Hugging Face 的 Transformers 快 24 倍!在我个人使用时,发现速度大约是 Hugging Face Transformers 的 10 倍。不过,我发现这里有一些小 bug。这个项目正在积极开发中,尚未完善。不过,我仍然强烈建议你试试这个解决方案。由于推理速度更快,相较于 HF Transformers,成本将显著降低。

来源: github.com/vllm-project/vllm

Petals.ml

这个是最有趣的解决方案。Petals.ml 的开发者发现了一种在家运行 LLMs 的方法,类似于 BitTorrent。这使得微调和推理的速度比卸载快最多 10 倍。实际上,这意味着模型的只有一小部分会加载到你自己的 GPU 上,其余部分会存在于 GPU 网络群中。换句话说,一个 GPU 网络将协作进行计算。这非常有趣,因为它在一定程度上使 LLM 的使用得到民主化,即任何人都可以运行大型 LLM 而不花一分钱!相关技术的论文可以在这里找到。我强烈建议你试试 Petals.ml!

Together.AI

他们提供了一个具有出色定价的开源模型 API。你可以使用 Together.AI 计算集群对开源模型进行微调和部署。他们的定价是 AWS 的 20%。他们的平台简单直观,容易上手。因此,我强烈推荐这个平台。他们的 API 价格大约是GPT-3.5 turbo 的 1/10。这是我现在最喜欢的开源模型部署方式!

AWS Sagemaker

部署 ML 模型的成熟方法。Sagemaker 对初学者不太友好,而且与上述方法相比,它可能是最昂贵的。它也是最复杂的。然而,如果你的业务已经在使用 AWS,这可能是你唯一的选择。此外,如果你像我一样在 AWS 上有免费的计算资源,为什么不尝试一下呢?这是 AI Anytime 的教程:www.youtube.com/watch?v=A9Pu4xg-Nas&ab_channel=AIAnytime

结论:

总结来说,我强烈建议尝试 Together.AI 和 Petals.ml,因为使用这些平台有许多优势。如果你需要隐私和非常快的推理速度,我建议使用 VLLM。如果你被迫使用 AWS,那么选择 SageMaker。如果你想要简单高效的方案(特别是用于测试),可以选择 HF transformers 端点。

📢 嗨!如果你觉得这篇文章有帮助,请考虑:

👏 鼓掌 50 次(这很有帮助!)

✏️ 留下评论

🌟 突出你觉得有见地的部分

👣 关注我

有任何问题吗?🤔 不要犹豫,尽管问。以这种方式支持我是对我的详细教程文章的一种免费而简单的感谢方式!😊

最终说明

如果你对仅使用 Python 开发全栈 AI 应用感兴趣,请随时注册这里。一如既往,请在下面留下你的经验和评论。我期待阅读所有评论。

就这些了,如果你读到这里,请在下面评论并在LinkedIn上添加我。

我的 Github 在这里

其他深度学习博客

安德鲁·吴的序列模型 — 11 个经验教训

安德鲁·吴的计算机视觉 — 11 个经验教训

安德鲁·吴的深度学习专业化 — 21 个经验教训

荷兰电动车:使用 Python 和 SQLAlchemy 的探索性数据分析(第二部分)

原文:towardsdatascience.com/electric-cars-in-the-netherlands-exploratory-data-analysis-with-python-and-sqlalchemy-part-2-c12c6cc2a902

使用 Python、SQLAlchemy 和 Bokeh 进行数据分析和可视化

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

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

--

Smart EQ 汽车,图片来源 en.wikipedia.org/wiki/Smart_electric_drive

第一个电动车是什么时候注册的?(剧透:比大多数人想象的要早得多。)电动保时捷和捷豹哪一款更贵?探索性数据分析(EDA)不仅是构建每个数据管道的重要部分,而且是一个相当有趣的过程。在第一部分中,我使用 Python 和 Pandas 分析了 RDW(荷兰车辆管理局)数据集,其中一个挑战是数据集大小较大(约 10 GB)。作为解决方案,我指定了需要在 Pandas 中加载的列列表。这种方法有效,但如果数据集更大,内存中没有足够的 RAM 来容纳所有数据,或者数据集存放在远程数据库中怎么办?在这篇文章中,我将展示如何使用 SQLAlchemy 进行类似的分析。这将允许使用 SQL 进行“重型”数据处理,而无需将所有数据加载到 Pandas 中。

让我们开始吧。

加载数据

RDW(“Rijks Dienst Wegverkeer”, www.rdw.nl)是一个荷兰组织,负责荷兰的机动车和驾驶执照的批准与登记。我将使用“Gekentekende voertuigen”(“带有车牌的车辆”)数据集。如第一部分所述,它在公共领域许可证下提供,可以从opendata.rdw.nl下载。数据处理将使用SQLite,它是一个免费的轻量级数据库引擎,可以轻松运行在任何 PC 上。

一开始,我们需要将 CSV 文件下载并导入到 SQLite 中。文件大小约为 10 GB;可以免费下载,无需注册。为了导入数据,我运行了“sqlite3 rdw_data.db”命令,并输入了 3 个命令:

sqlite> .mode csv
sqlite> .import Open_Data_RDW__Gekentekende_voertuigen.csv rdw_data
sqlite> .quit

这里“Open_Data_RDW__Gekentekende_voertuigen.csv”是原始的 CSV 文件,“rdw_data”是一个需要创建的表。导入过程需要一些时间,之后我们就可以结束命令行操作,回到 Jupyter Lab。首先,让我们进行必要的导入,看看我们拥有哪些数据库列:

from sqlalchemy import create_engine, MetaData, table, column, select, func
from sqlalchemy import inspectp
import pandas as pd

rdw_db = create_engine('sqlite:///rdw_data.db')

table_name = 'rdw_data'

with Session(rdw_db) as session:    
    insp = inspect(rdw_db)
    columns = insp.get_columns("rdw_data")
    display(pd.DataFrame(columns))

我使用 Pandas DataFrame 来显示结果,因为它的输出更易于阅读。例如,“display(columns)”会显示如下输出:

同时,“display(pd.DataFrame(columns))”的输出效果要好得多:

让我们检查一下结果。我们可以看到所有列都是 TEXT 类型,因此我们需要转换这些值。数据库中有 91 列,但根据实际分析,我只需要汽车的类型、车牌、型号名称、价格和注册日期。我还会使用“Number of cylinders”作为辅助来检测汽车是否是电动车。最后但同样重要的是,我只会分析“personal”(荷兰语中的“Personenauto”)汽车,而不是卡车或公交车,所以我会在 SQL 查询中使用这个过滤器。

让我们使用 SQL 进行这个转换:

with Session(rdw_db) as session:  
    session.execute(text('DROP TABLE IF EXISTS rdw_cars'))
    session.execute(text('CREATE TABLE rdw_cars("index" INTEGER PRIMARY KEY AUTOINCREMENT, '
                                                         '"Model" TEXT, '
                                                         '"Trade name" TEXT, '
                                                         '"License Plate" TEXT, '
                                                         '"Number of Cylinders" INTEGER, '
                                                         '"Catalog price" INTEGER, '
                                                         '"First registration NL" TEXT, '
                                                         '"Is electric" INTEGER DEFAULT 0)'))

    session.execute(text('BEGIN TRANSACTION'))    
    session.execute(text('INSERT INTO rdw_cars("Model", "Trade name", "License Plate", "Number of Cylinders", "Catalog price", "First registration NL") '
                                           'SELECT '
                                              '"Merk", '
                                              '"Handelsbenaming", '
                                              '"Kenteken", '
                                              '(CASE WHEN LENGTH("Aantal cilinders") > 0 THEN CAST("Aantal cilinders" as INTEGER) ELSE NULL END), '
                                              '(CASE WHEN LENGTH("Catalogusprijs") > 0 THEN CAST("Catalogusprijs" as INTEGER) ELSE NULL END), '
                                              'DATE(SUBSTR("Datum eerste tenaamstelling in Nederland", 1, 4) || "-" || SUBSTR("Datum eerste tenaamstelling in Nederland", 5, 2) || "-" || SUBSTR("Datum eerste tenaamstelling in Nederland", 7, 2)) '
                                           ' FROM rdw_data WHERE "Voertuigsoort" = "Personenauto"'))  

    session.execute(text('COMMIT'))
​

在这里,我创建了一个新表,并将整数和日期列转换成适当的格式。我将所有空字符串替换为 NULL,并作为读者的额外奖励,我将荷兰语列名翻译成了英文。我还创建了“Is electric”列,后面会使用到。

初始转换完成,我们准备好了。

基本分析

一开始,让我们看看数据集的主要属性,如数据样本、维度和 NULL 值的数量。

使用 SQL,我们可以获取记录的总数:

with Session(rdw_db) as session:    
    q = session.execute(text('SELECT COUNT(*) FROM rdw_cars')).scalar()
    print("Cars total:", q) 

总共有 9,487,265 辆车,在撰写本文时已在荷兰注册(对于那些稍后下载数据集的读者,这个数字显然会更大)。这个总数也等于我在第一部分中得到的数字,在那里我使用 Pandas 进行了类似的分析——这是一种检查处理是否正确的简单方法。

现在我们来看数据库中的前 5 个样本;使用 SQL 很容易做到这一点。在这里和之后我将使用 Pandas 来显示表格,因为 Pandas 有原生的 SQL 绑定,这很方便。

with Session(rdw_db) as session:
    df = pd.read_sql_query(text("SELECT * FROM rdw_cars LIMIT 5"), con=session.connection(), dtype={'Catalog price': pd.UInt32Dtype(), 'Number of Cylinders': pd.UInt32Dtype()})
    display(df.style.hide(axis="index"))

结果如下所示:

让我们检查一下不同列中缺失/NULL 数量。Pandas DataFrame 有一个方便的方法“df.isna().sum()”,但我在 SQL 中找不到类似的东西。我们需要指定所有需要检查的列:

with Session(rdw_db) as session:
    request = ('SELECT '
               ' SUM(CASE WHEN "Model" IS NULL OR "Model" = "" THEN 1 ELSE 0 END) AS model_no_data, '
               ' SUM(CASE WHEN "Trade name" = "" THEN 1 ELSE 0 END) AS trade_name_empty, '
               ' SUM(CASE WHEN "Trade name" IS NULL THEN 1 ELSE 0 END) AS trade_name_nulls, '
               ' SUM(CASE WHEN "License Plate" IS NULL OR "License Plate" = "" THEN 1 ELSE 0 END) AS lp_no_data, '
               ' SUM(CASE WHEN "Number of Cylinders" = 0 THEN 1 ELSE 0 END) AS num_cylinders_zeros, '
               ' SUM(CASE WHEN "Number of Cylinders" IS NULL THEN 1 ELSE 0 END) AS num_cylinders_nulls, '
               ' SUM(CASE WHEN "Catalog price" = 0 THEN 1 ELSE 0 END) AS price_zeros, '
               ' SUM(CASE WHEN "Catalog price" IS NULL THEN 1 ELSE 0 END) AS price_nulls, '
               ' SUM(CASE WHEN "First registration NL" IS NULL THEN 1 ELSE 0 END) AS registration_nulls, '
               ' COUNT(*) AS total '
               'FROM rdw_cars')

    df = pd.read_sql(text(request), con=session.connection())
    display(df.style.hide(axis="index"))

使用 SQL,我计算了可能是 NULL 或空的值的总和。结果如下所示:

在这里我们可以看到汽车的总数量(9,487,265)。每辆车都有一个车牌和一个注册日期;这些字段可能是注册的必填项。但有 2,480,506 条记录没有价格,864 条记录没有“商标名称”,等等。在这里,我看到一个问题——这些 864 条空“商标名称”字段的记录与我在 Pandas 中得到的 1,405 条空记录不匹配,在第一部分中得到了这个结果。这显然是不对的,差异在哪里?不可能手动检查 9,487,265 条记录,而调试这个问题的最简单方法是将唯一的“商标名称”值保存到文本文件中,并使用“Diff”工具比较两个文件。结果表明,问题简单但有趣——在第一部分,我使用了“pd.read_csv”方法加载数据。这个方法“足够聪明”可以自动将“NULL”、“NA”、“N/A”和一些其他值(完整列表可以在手册中找到)替换为 NULL,这个转换默认是启用的。在我们的案例中,Mazda NA 是一个真实的汽车模型,而 Pandas 自动将这些车的所有“NA”名称转换为 NULL(这也让我想起了旧故事关于姓氏 Null 的人,他对计算机来说是“不可见的”)。无论如何,Mazda NA 车不是电动车,所以它不会影响第一部分的结果,但要记住这种问题可能会发生是好的。

但让我们回到分析中。使用 SQL,我们可以轻松地进行有用的请求,例如,来看一下荷兰最贵的前 10 辆车:

with Session(rdw_db) as session:
    df = pd.read_sql(text('SELECT "Model", "Trade name", "Catalog price", "First registration NL" FROM rdw_cars ORDER BY "Catalog price" DESC LIMIT 10'), con = session.connection())
    display(df)

结果很有趣:

我本来期待在这个列表中看到 Porsche、Mercedes 或 BMW,但看到 Peugeot 或 Fiat 在这里对我来说有点意外,不过,我不是豪华车方面的专家。

数据转换

我们已经使用 SQL 请求做了一些基本分析,但本文的目的是分析电动汽车。要检测汽车是否为电动,我们需要知道其制造商和型号名称。理想情况下,如果电动汽车的名称中有“ELECTRIC”,任务会简单得多。但在现实生活中,车型命名毫无逻辑。“Mazda MX-30”是电动的,但“Mazda MX-5”不是。“Kia Niro”是电动的,而“Kia Sorento”不是,等等。这没有规则,最简单的方法就是创建一个电动汽车型号的表格并加以使用。但首先,让我们检查一下数据集中汽车型号和商标名称是否一致。

首先让我们验证汽车 型号,例如,查看所有的 PEUGEOT 汽车:

with Session(rdw_db) as session:
    df = pd.read_sql_query(text('SELECT "Model", COUNT(*) AS Count FROM rdw_cars WHERE "Model" LIKE "%PEUGEOT%" GROUP BY "Model" '), con = session.connection())
    display(df.style.hide(axis="index"))

结果看起来是这样的:

数据库中的几乎所有汽车都有名称“PEUGEOT”,这很好,但有几辆车的名称较长,如“PEUGEOT BOXER”。第一个词足以知道汽车型号,因此我们可以轻松去除其余部分。这将使未来的分析更加方便;例如,我们可以按型号对汽车进行分组,看看售出了多少辆 Peugeot 汽车。在第一部分中,我已经创建了一个方法来去除型号名称中的冗余词:

def model_normalize(s_val):
    """ "PEUGEOT BOXER/GLOBE-TRAVE " => "PEUGEOT" """
    if s_val and isinstance(s_val, str) and len(s_val) > 0:
        return s_val.replace("-", " ").replace("/", " ").split()[0].upper().strip()
    return None

现在让我们检查下一列。数据集中汽车商标名称有时会与型号重复,例如这个例子中的“NISSAN”汽车:

我创建了一个方法来移除这些重复项,在这个示例中,它会将“NISSAN MURANO”字段转换为仅“ MURANO”。

def name_normalize(model: str, trade_name: str):
    """ Remove duplicates and convert the name to upper case """
    if isinstance(trade_name, str) and len(trade_name) > 0:
        name = trade_name.upper().strip()
        # Remove duplicates from model and trade name: 
        # ("TESLA", "TESLA MODEL 3") => ("TESLA", "MODEL 3")
        if name.split()[0] == model:
            # "TESLA MODEL 3" => [TESLA, MODEL, 3] => "MODEL 3"
            return ' '.join(name.split()[1:])  
        return name
    return None

现在我们终于可以弄清楚这辆车是否为电动了。在第一部分中,我已经为此创建了一个方法:

electric_cars = {
    "AIWAYS": ['U5', 'U6'],
    "AUDI": ['E-TRON'],
    "BMW": ['I3', 'I4', 'I7', 'IX'],
    "CITROEN": ['E-C4'],
    "FIAT": ['500E', 'ELETTRA'],
    "FORD": ['MACH-E'],
    "HONDA": ['"E"', '"E ADVANCE"'],  
    "HYUNDAI": ['IONIQ', 'KONA'],
    "JAGUAR": ['I-PACE'],
    "KIA": ['NIRO', 'E-SOUL'],
    "LEXUS": ['RZ'],
    "LUCID": ['AIR'],
    "MAZDA": ['MX-30'],
    "MERCEDES": ['EQA', 'EQB', 'EQC', 'EQS', 'EQV'],
    "MG": ['ZS EV'],
    "MINI": ['COOPER SE'],
    "NISSAN": ['ALTRA', 'ARIYA', 'EVALIA', 'LEAF', 'NUVU'],
    "OPEL": ['AMPERA-E', 'COMBO-E', 'CORSA-E', 'MOKKA-E', 'VIVARO-E', 'ZAFIRA-E'],
    "PEUGEOT": ['E-208', 'E-2008', 'E-RIFTER', 'E-TRAVELLER'],
    "POLESTAR": ['2', '3'],
    "PORSCHE": ['TAYCAN'],
    "RENAULT": ['MASTER', 'TWINGO', 'KANGOO ELEC', 'ZOE'],
    "SKODA": ['ENYAQ'],
    "SMART": ['EQ'],
    "TESLA": [''],
    "TOYOTA": ['BZ'],
    "VOLKSWAGEN": ['ID.3', 'ID.4', 'ID.5', 'E-GOLF'],
    "VOLVO": ['C40', 'XC40']
}

def check_is_electric(model: str, trade_name: str, cylinders: int):
    """ Determine if the car is electric """
    if isinstance(cylinders, int) and cylinders > 0:
        return False
    for e_model, e_names in electric_cars.items():
        if model == e_model:
            for e_name in e_names:
                if trade_name and (e_name in trade_name or e_name.replace('"', '') == trade_name):
                    return True
                if trade_name is None and len(e_name) == 0:
                    return True
    return False

在这段代码中,我搜索特定的关键词;例如,如果型号是“BMW”,那么“I3”商标名称会告诉我们这辆车是电动的。作为额外的检查(一些汽车可能是电动或混合动力),我还分析了气缸数量,对于电动汽车,气缸数量必须是 0 或 NULL。

在第一部分中已经测试过的所有 3 种方法都效果很好,例如,我可以通过一行代码轻松地将model_normalize方法应用于 Pandas 数据集:

df["Model"] = df['Model'].map(lambda s: model_normalize(s))

但是我们如何在 SQL 中使用它呢?好吧,直接用是不行的,但我们可以借助 ORM 来实现。

SQLAlchemy ORM

ORM(对象关系映射)是一种技术,用于在 OOP 语言和关系数据库之间创建一个“桥梁”。实际上,我们可以创建一个特殊的 Python 类,SQLALchemy 会自动将对这个类的所有请求转换为 SQL。这非常方便,允许开发人员用纯 Python 编写代码,而不必处理难看的括号和长 SQL 字符串。

让我们创建一个“Car”类,并在其中放入所需的方法:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy import String, Integer, Date, Column

class Base(DeclarativeBase):
    pass

class Car(Base):
    __tablename__ = "rdw_cars"
    index = Column("index", Integer, primary_key=True) 
    license_plate = Column("License Plate", String)
    model = Column("Model", String) 
    trade_name = Column("Trade name", String) 
    num_cylinders = Column("Number of Cylinders", Integer) 
    first_registration = Column("First registration NL", Date) 
    price = Column("Catalog price", Integer) 
    is_electric = Column("Is electric", Integer)    

    def model_normalize(self):
        """ "PEUGEOT BOXER/GLOBE-TRAVE " => "PEUGEOT" """
        if self.model and isinstance(self.model, str) and len(self.model) > 0:
            self.model = self.model.replace("-", " ").replace("/", " ").split()[0].upper().strip()

    def name_normalize(self):
        """ Remove duplicates from model and trade name: ("TESLA", "TESLA MODEL 3") => ("TESLA", "MODEL 3") """
        if isinstance(self.trade_name, str) and len(self.trade_name) > 0:
            name = self.trade_name.upper().strip()
            if name.split()[0] == self.model:
                # "TESLA MODEL 3" => [TESLA, MODEL, 3] => "MODEL 3"
                self.trade_name = ' '.join(name.split()[1:])  
            else:
                self.trade_name = name

    def check_electric(self):
        self.is_electric = check_is_electric(self.model, self.trade_name, self.num_cylinders)

作为这种方法的一个示例,让我们展示荷兰特定型号的最贵汽车。使用标准 SQL,我们可以发出如下请求:

with Session(rdw_db) as session:
    model = "BMW"
    limit = 5
    df = pd.read_sql_query(text(f'SELECT "Model", "Trade name", "Catalog price", "First registration NL" FROM rdw_cars WHERE Model = "{model}" ORDER BY "Catalog price" DESC LIMIT {limit}'), 
                           con=session.connection())
    display(df.style.hide(axis="index"))

结果是我们得到了表格:

它是有效的,但 SQL 字符串相当长,我不得不使用 f-string 将所需的变量添加到请求中。在对象关系映射的帮助下,我可以直接使用标准 Python 代码:

with Session(rdw_db) as session:
    model = "BMW"
    limit = 5
    df = pd.read_sql_query(select(Car.model, Car.trade_name, Car.price, Car.first_registration).filter(Car.model == model).order_by(Car.price.desc()).limit(limit), 
                           con=session.connection())
    display(df.style.hide(axis="index"))

SQLAlchemy 将在“幕后”创建一个适当的 SQL 请求,因此我们得到了更易读的 Python 代码。然而,主要的缺点是 SQL 或多或少是标准的;有很多关于它的资源和教程,但 SQLAlchemy 代码仅限于此库。但对于我们的任务,这种方法效果很好。

使用 ORM,我们可以轻松地将我们的“check_electric”方法应用于数据库中的所有记录:

with Session(rdw_db) as session:
    cars_total = session.query(Car).count()

    index = 0
    batch_size = 25000
    while True:
        pos1, pos2 = index*batch_size, (index + 1)*batch_size
        if index % 20 == 0:
            print(f"Processing {pos1} to {pos2}, {100*index*batch_size//cars_total}%...")

        cars = session.query(Car).filter(Car.index.between(pos1, pos2)).all()
        if len(cars) == 0:
            break
        for car in cars:
            car.model_normalize()
            car.name_normalize()
            car.check_electric()

        session.flush()
        index += 1

    session.commit()

在这段代码中,我从数据库中读取记录,更新参数,并将数据保存回表中。SQLAlchemy 将使用 SQL 请求更新表格,这肯定比直接在内存中更新 Pandas Dataframe 要慢。调用 Pandas 中的相同方法只需 130 秒,而 SQLAlchemy 请求则花费了 390 秒,因此差异约为 3 倍。另一方面,对于批量更新,我们需要的内存要少得多,而且不需要将整个数据框保存在 RAM 中。

分析

更新表格后,我们终于准备好了。作为热身,让我们计算一下汽车价格的均值和百分位数。

计算均值很简单,可以用 SQLAlchemy 一行代码完成。让我们获取汽车的总数量及其算术价格均值:

with Session(rdw_db) as session:
    c_total = session.query(Car).count()
    print(f"Cars total: {c_total}")
    c_el = session.query(Car).filter(Car.is_electric == 1).count()
    print(f"Cars electric: {c_el} ({100*c_el/c_total:.2f}%)")

    pm = session.query(func.avg(Car.price)).scalar()
    print("Price mean:", pm)

    pm_el = session.query(func.avg(Car.price)).filter(Car.is_electric == 1).scalar()
    print("Electric cars price mean:", pm_el)

获取百分位数稍微复杂一点,我们有两种方法可以做到这一点。我们可以仅加载“价格”列,然后使用 NumPy 的“percentile”方法进行计算:

 prices = session.query(Car.price).filter(Car.price != None).all()
    print("All cars percentiles [5, 50, 95]:", np.percentile(prices, [5, 50, 95]))

    prices_el = session.query(Car.price).filter((Car.price != None) & (Car.is_electric == 1)).all()
    print("Electric cars percentiles [5, 50, 95]:", np.percentile(prices_el, [5, 50, 95]))

如果数据集很大,我们想要避免完全加载数据,可以通过结合“order_by”、“limit”和“offset”来使用纯 SQL 获取百分位数:

 num_total = session.query(Car).filter(Car.price != None).count()
    p5 = session.query(Car.price).filter(Car.price != None).order_by(Car.price).offset(num_total*5/100 - 1).limit(1).scalar() 
    p50 = session.query(Car.price).filter(Car.price != None).order_by(Car.price).offset(num_total*50/100 - 1).limit(1).scalar() 
    p95 = session.query(Car.price).filter(Car.price != None).order_by(Car.price).offset(num_total*95/100 - 1).limit(1).scalar() 
    print("All cars percentiles [5, 50, 95]:", p5, p50, p95)

    num_el = session.query(Car).filter((Car.price != None) & (Car.is_electric == 1)).count()
    p5 = session.query(Car.price).filter((Car.price != None) & (Car.is_electric == 1)).order_by(Car.price).offset(num_el*5/100 - 1).limit(1).scalar() 
    p50 = session.query(Car.price).filter((Car.price != None) & (Car.is_electric == 1)).order_by(Car.price).offset(num_el*50/100 - 1).limit(1).scalar() 
    p95 = session.query(Car.price).filter((Car.price != None) & (Car.is_electric == 1)).order_by(Car.price).offset(num_el*95/100 - 1).limit(1).scalar() 
    print("Electric cars percentiles [5, 50, 95]:", p5, p50, p95)

结果很有趣:

荷兰是一个平均工资相当高的国家,但在撰写本文时,只有 2.93% 的汽车是电动的。所有汽车的中位数价格为 €26,341,而电动汽车“平均”贵 2 倍;其中位数价格为 €49,975. 所有汽车的第 95 百分位数为 €73,381,这意味着 95% 的汽车价格更低。与此同时,95% 的电动汽车价格低于 €106,989。

现在让我们找点更有趣的。让我们获取荷兰前 20 名电动车:

with Session(rdw_db) as session:        
    n_top = 20

    # Group by car model
    models_amout = session.query(Car.model, func.count(Car.model)).filter(Car.is_electric == 1).group_by(Car.model).order_by(desc(func.count(Car.model))).limit(n_top).all()[::-1]

    # Unzip array [('TESLA', 65896), ('VOLKSWAGEN', 28559)] to 2 parts
    models, amount = zip(*models_amout)

    # Show
    p = figure(y_range=models, width=1200, height=500, title="Top-%d electric car manufacturers in the Netherlands (data 2023)" % n_top)
    p.hbar(right=amount, y=models, height=0.8, color=Viridis256[:n_top])
    p.xgrid.grid_line_color = None
    p.x_range.start = 0
    p.below[0].formatter.use_scientific = False
    p.xaxis.axis_label = "Cars total"
    show(p)

从表格中可以看到,特斯拉排名第一,注册在该国的汽车超过 55,000 辆:

前 20 名电动车制造商,图片来源:作者

我对哪个特斯拉型号最受欢迎感到好奇。为了了解这一点,我们可以更改请求:

 models_amout = session.query(Car.trade_name, func.count(Car.trade_name)).filter(Car.model == "TESLA").group_by(Car.trade_name).order_by(desc(func.count(Car.trade_name))).order_by(Car.trade_name).all()[::-1]
    ...

很明显,“Model 3”是撰写本文时最受欢迎的电动汽车:

Tesla 车型柱状图,图像由作者提供

但我们也可以看到数据集显然需要更多清理:一些 Tesla 汽车被注册为“MODEL 3”,一些为“MODEL3”,一些汽车被保存为“ROADSTER”,一些为“RAODSTER”等等。

现在我们来按日期对电动汽车注册情况进行分组。为了使图表更清晰,我想按季度分组日期,但在 SQL 中提取季度的代码可能很庞大。相反,我将使用 SQL 按天分组注册,然后可以使用 Pandas 内部函数计算季度:

with Session(rdw_db) as session:
    regs_amount = session.query(Car.first_registration, func.count(Car.first_registration)).filter(Car.is_electric == 1).group_by(Car.first_registration).order_by(Car.first_registration).all()

    df = pd.DataFrame(regs_amount, columns =['First registration NL', 'Amount'])
    df["First registration NL"] = df['First registration NL'].map(lambda d: datetime.datetime(d.year, d.month, d.day))
    df["Quarter"] = df['First registration NL'].dt.to_period('Q')

    data_per_quarter = df.groupby(['Quarter'], as_index=False)["Amount"].sum()
    dates = data_per_quarter['Quarter']
    amount = data_per_quarter['Amount']

    p = figure(x_axis_type='datetime', width=1600, height=500, 
               title=f"Electric car registrations in the Netherlands, 1992-2022")
    p.vbar(x=dates, top=amount, width=datetime.timedelta(days=3*22), line_color='black')
    p.xaxis[0].ticker.desired_num_ticks = 20
    p.yaxis.axis_label = "Cars total"
    show(p)

在这段代码中,我首先将 SQL 结果转换为 Pandas dataframe;然后我将 Python 的“date”对象转换为“datetime”(因为某些原因,季度计算仅与“datetime”有效)。代码几乎与第一部分相同,但这里我使用 Pandas 的“groupby.sum()”代替“size()”,因为从 SQL 中检索的数据已经按天分组。

结果很有趣:

电动汽车注册情况,图像由作者提供

如第一部分所述,荷兰首辆电动汽车在 1992 年注册。那是一辆Fiat Panda Elettra,是一辆小型双座车,最高时速 70 km/h,续航 100 km,电源由 12 块 6V 铅酸电池提供。它是全国家唯一的电动汽车,15 年内没有其他电动汽车;接下来的 3 辆Tesla Roadster汽车直到 2009 年才注册。

现在我们来看看电动汽车的价格分布。我想绘制一个箱型图,为此,我需要了解每种车型的最小值、最大值和四分位数值:

with Session(rdw_db) as session:
    request_models = session.query(Car.model).filter(Car.is_electric == 1).group_by(Car.model).all()

    def q0(x):
        return x.quantile(0.01)

    def q1(x):
        return x.quantile(0.25)

    def q3(x):
        return x.quantile(0.75)

    def q4(x):
        return x.quantile(0.99)

    models_data = {}
    for m in request_models:
        model_name = m[0]  # (AIWAYS,) => AIWAYS
        print("Processing", model_name)
        request_model = session.query(Car.price).filter((Car.is_electric == 1) & (Car.price > 0) & (Car.model == model_name)).all()
        df = pd.DataFrame(request_model)
        agg_data = {'price': ['size', 'min', q0, q1, 'median', q3, q4, 'max']}        
        models_data[model_name] = df.agg(agg_data)["price"]

    df = pd.concat(models_data, axis=1).transpose()
    display(df)

在这段代码中,我首先获取所有汽车型号的列表;然后获取每种型号的价格,并使用 Pandas 对这些价格进行汇总。然后将数据合并成一个单一的 dataframe。结果如下:

有了这个 dataframe,绘制箱型图变得简单:

 # Sort models by price
    df = df.sort_values(by='median', ascending=True)

    models = df.index.values    
    v_min = df["q0"].values
    q1 = df["q1"].values
    q3 = df["q3"].values
    v_max = df["q4"].values

    # Draw
    palette = (Inferno10 + Magma10 + Plasma10 + Viridis10)[:models.shape[0]]
    source = ColumnDataSource(data=dict(models=models, 
                                        bottom=q1, 
                                        top=q3, 
                                        color=palette, 
                                        lower=v_min, 
                                        upper=v_max))

    p = figure(x_range=models, width=1900, height=500, title="Electric car prices distribution in the Netherlands")
    whisker = Whisker(base="models", upper="upper", lower="lower", source=source)
    p.add_layout(whisker)
    p.vbar(x='models', top='top', bottom='bottom', width=0.9, color='color', line_color="black", source=source)
    p.left[0].formatter.use_scientific = False
    p.y_range.start = 0
    show(p)

结果如下:

电动汽车制造商和价格箱型图,图像由作者提供

借助 SQLAlchemy,还可以轻松获取所有电动汽车的价格,并使用“np.histogram”方法构建直方图。代码几乎与第一部分相同,愿意的话可以自己尝试。

结论

分析真实数据集很有趣,结果显示 SQL 和 Pandas 的配合效果很好。数据的“繁重”检索和预处理可以使用 SQLAlchemy 完成,然后这些数据可以在 Pandas 中使用。

关于数据本身的处理,还有很多工作可以做。将这些数据与Kaggle 电动车数据集结合,寻找最大行驶距离、价格和汽车发布日期之间的相关性可能会很有趣(较新的车型应有更长的行驶距离)。我尝试过这样做,但两个数据集中的车型名称不匹配,而我又不是汽车专家,无法手动对每个型号进行处理。此外,正如之前所示,RDW 数据集需要更多的清理,名称也不一致。对这方面感兴趣的读者可以自行继续这些实验。

如果你喜欢这个故事,可以随时订阅Medium,你将会收到我新文章发布的通知,并且可以全面访问其他作者的数千篇故事。

感谢阅读。

荷兰的电动汽车:使用 Python 进行探索性数据分析

原文:towardsdatascience.com/electric-cars-in-the-netherlands-exploratory-data-analysis-with-python-d01477949984

使用 Python、Pandas 和 Bokeh 进行数据分析和可视化

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

·发表于 Towards Data Science ·16 min 阅读·2023 年 2 月 10 日

--

Smart EQ Car,图片来源 en.wikipedia.org/wiki/Smart_electric_drive

第一个电动汽车是什么时候注册的?(剧透:比大多数人想象的要早得多。)电动 Porsche 还是 Jaguar 更贵?探索性数据分析(EDA)不仅是建立每个数据管道的重要部分,而且还是一个相当有趣的过程。在本文中,我将使用荷兰 RDW(荷兰车辆管理局)公共数据集来查找有关电动汽车的信息。我们将看看哪些数据可以使用 Python、Pandas 和 Bokeh 提取和展示。

让我们开始吧。

加载数据

RDW(“Rijks Dienst Wegverkeer”,www.rdw.nl)是一个荷兰机构,负责处理荷兰的机动车和驾驶执照的审批与登记。作为一个公共政府机构,它的数据对所有人开放。对我们最感兴趣的是“Gekentekende voertuigen”(“带有车牌的车辆”)数据集。它可以在 opendata.rdw.nl 上免费下载,使用公共领域许可证。文件大小约为 10 GB,包含自 1952 年以来在荷兰注册的所有车辆的信息。处理这样大小的文件也可能是一个挑战——这使得任务更加有趣。

我将使用 Jupyter Lab,这种方法比使用标准 IDE 更方便,因为每次启动项目时重新加载 10 GB 的文件似乎不是一个好主意。同时,我将使用 Pandas 进行处理,并使用 Bokeh 进行可视化。首先,让我们导入所需的库:

import os
import pandas as pd
import numpy as np
import datetime

from bokeh.io import show, output_notebook, export_png
from bokeh.plotting import figure, output_file
from bokeh.models import ColumnDataSource, LabelSet, Whisker
from bokeh.palettes import *
output_notebook()

现在我们准备加载数据集。我们先尝试一种“天真”的方法:

filename = "Open_Data_RDW__Gekentekende_voertuigen.csv"
df = pd.read_csv(filename)
display(df)

运行此代码后,PC 冻结了大约 30 秒……而且 Python 内核崩溃了。哎呀。它不仅加载缓慢,而且内存也不够。至少在我的电脑上,32 GB 的 RAM 对于这个任务是不够的。

如果我们无法将文件加载到内存中,可以逐行读取;这种方法从 IBM 主机和磁带驱动器时代就已经存在。让我们读取文件的前几行,看看里面有什么:

filename = "Open_Data_RDW__Gekentekende_voertuigen.csv"
with open(filename, 'r') as f:
    header_str = f.readline()
    print(header_str)
    for _ in range(10):
        print(f.readline())

结果如下:

Kenteken,Voertuigsoort,Merk,Handelsbenaming,Vervaldatum APK,Datum tenaamstelling,Bruto BPM,Inrichting,Aantal zitplaatsen,Eerste kleur,Tweede kleur,Aantal cilinders,Cilinderinhoud,Massa ledig voertuig,Toegestane maximum massa voertuig,Massa rijklaar,Maximum massa trekken ongeremd,Maximum trekken massa geremd,Datum eerste toelating,Datum eerste tenaamstelling in Nederland,Wacht op keuren,Catalogusprijs,WAM verzekerd,Maximale constructiesnelheid,Laadvermogen,Oplegger geremd,Aanhangwagen autonoom geremd,Aanhangwagen middenas geremd,Aantal staanplaatsen,Aantal deuren,Aantal wielen,Afstand hart koppeling tot achterzijde voertuig,Afstand voorzijde voertuig tot hart koppeling,Afwijkende maximum snelheid,Lengte,Breedte,Europese voertuigcategorie,Europese voertuigcategorie toevoeging,Europese uitvoeringcategorie toevoeging,Plaats chassisnummer,Technische max. massa voertuig,Type,Type gasinstallatie,Typegoedkeuringsnummer,Variant,Uitvoering,Volgnummer wijziging EU typegoedkeuring,Vermogen massarijklaar,Wielbasis,Export indicator,Openstaande terugroepactie indicator,Vervaldatum tachograaf,Taxi indicator,Maximum massa samenstelling,Aantal rolstoelplaatsen,Maximum ondersteunende snelheid,Jaar laatste registratie tellerstand,Tellerstandoordeel,Code toelichting tellerstandoordeel,Tenaamstellen mogelijk,Vervaldatum APK DT,Datum tenaamstelling DT,Datum eerste toelating DT,Datum eerste tenaamstelling in Nederland DT,Vervaldatum tachograaf DT,Maximum last onder de vooras(sen) (tezamen)/koppeling,Type remsysteem voertuig code,Rupsonderstelconfiguratiecode,Wielbasis voertuig minimum,Wielbasis voertuig maximum,Lengte voertuig minimum,Lengte voertuig maximum,Breedte voertuig minimum,Breedte voertuig maximum,Hoogte voertuig,Hoogte voertuig minimum,Hoogte voertuig maximum,Massa bedrijfsklaar minimaal,Massa bedrijfsklaar maximaal,Technisch toelaatbaar massa koppelpunt,Maximum massa technisch maximaal,Maximum massa technisch minimaal,Subcategorie Nederland,Verticale belasting koppelpunt getrokken voertuig,Zuinigheidsclassificatie,Registratie datum goedkeuring (afschrijvingsmoment BPM),Registratie datum goedkeuring (afschrijvingsmoment BPM) DT,API Gekentekende_voertuigen_assen,API Gekentekende_voertuigen_brandstof,API Gekentekende_voertuigen_carrosserie,API Gekentekende_voertuigen_carrosserie_specifiek,API Gekentekende_voertuigen_voertuigklasse
85XXXA,Personenauto,VOLKSWAGEN,CALIFORNIA,20230702,20220915,10437,kampeerwagen,,GROEN,Niet geregistreerd,5,2461,2088,2800,2188,700,2000,20010626,20010626,Geen verstrekking in Open Data,,Ja,,,,,,,0,4,0,0,,0,0,M1,,,r. in watergoot v. voorruit,2800,,,e1*96/79*0066*10,AJTCKX0,N1P00J2SGFM52B010U,1,0.03,292,Nee,Nee,,Nee,4500,0,0.00,2022,Logisch,00,Ja,07/02/2023 12:00:00 AM,09/15/2022 12:00:00 AM,06/26/2001 12:00:00 AM,06/26/2001 12:00:00 AM,,,,,,,,,,,,,,,,,,,,,,,,https://opendata.rdw.nl/resource/3huj-srit.json,https://opendata.rdw.nl/resource/8ys7-d773.json,https://opendata.rdw.nl/resource/vezc-m2t6.json,https://opendata.rdw.nl/resource/jhie-znh9.json,https://opendata.rdw.nl/resource/kmfi-hrps.json
85XXXB,Personenauto,PEUGEOT,3*RFN*,20230920,20210224,5162,hatchback,5,ZWART,Niet geregistreerd,4,1997,1194,1719,1294,625,1300,20010720,20010720,Geen verstrekking in Open Data,,Ja,,,,,,,4,4,0,0,,420,0,M1,,,op r. schroefveerkoker onder motorkap,1719,,,e2*98/14*0244*00,C,B,0,0.08,261,Nee,Nee,,Nee,3019,0,,2022,Logisch,00,Ja,09/20/2023 12:00:00 AM,02/24/2021 12:00:00 AM,07/20/2001 12:00:00 AM,07/20/2001 12:00:00 AM,,,,,,,,,,,,,,,,,,,,,D,,,https://opendata.rdw.nl/resource/3huj-srit.json,https://opendata.rdw.nl/resource/8ys7-d773.json,https://opendata.rdw.nl/resource/vezc-m2t6.json,https://opendata.rdw.nl/resource/jhie-znh9.json,https://opendata.rdw.nl/resource/kmfi-hrps.json
...
85XXXN,Personenauto,NISSAN,NISSAN MURANO,20240106,20111126,18921,stationwagen,5,ZWART,Niet geregistreerd,6,3498,1833,2380,1933,750,1585,20081206,20081206,Geen verstrekking in Open Data,,Ja,,,,,,,4,4,0,0,,484,0,M1,,,r. voorzitting by dwarsbalk,2380,Z51,,e1*2001/116*0478*00,A,A01,0,0.1,283,Nee,Nee,,Nee,3965,0,,2023,Logisch,00,Ja,01/06/2024 12:00:00 AM,11/26/2011 12:00:00 AM,12/06/2008 12:00:00 AM,12/06/2008 12:00:00 AM,,,,,,,,,,,,,,,,,,,,,E,,,https://opendata.rdw.nl/resource/3huj-srit.json,https://opendata.rdw.nl/resource/8ys7-d773.json,https://opendata.rdw.nl/resource/vezc-m2t6.json,https://opendata.rdw.nl/resource/jhie-znh9.json,https://opendata.rdw.nl/resource/kmfi-hrps.json

正如我们所看到的,有许多不同的数据字段,而我们实际上不需要所有这些字段。关于每辆车,我只想了解其类型、车牌、型号、价格和注册日期。这个数据库已经足够旧了,没有字段表示汽车是否为电动汽车。但至少,有一个字段包含“Number of cylinders”,这可以帮助我们排除不是电动汽车的车辆。

现在我们只有 7 个字段需要加载,在 Pandas 中,我们可以指定列列表,这会大幅减少数据大小。第二个技巧是将pd.UInt32Dtype指定给“Number of cylinders”和“Price”字段。我还想只查看“个人”汽车(荷兰语中的“Personenauto”),而不是卡车或公共汽车:

cols = ['Kenteken', 'Voertuigsoort', 'Merk', 'Handelsbenaming', 'Aantal cilinders', 'Catalogusprijs']
cols_date = ['Datum eerste tenaamstelling in Nederland']

filename = "Open_Data_RDW__Gekentekende_voertuigen.csv"
df = pd.read_csv(filename, usecols=cols + cols_date, parse_dates=cols_date, 
                 dtype={"Catalogusprijs": pd.UInt32Dtype(), 
                        "Aantal cilinders": pd.UInt32Dtype()})
display(df)

df = df[df['Voertuigsoort'] == 'Personenauto']
df.info(memory_usage="deep")

现在文件已正确加载,正如“info”方法所示,内存使用量为 2.5 GB:

Dataset information, Image by author

由于文件大小较大,数据加载仍然需要较长时间。最简单的方法是将筛选后的数据集保存为新文件,并使用该文件进行进一步的实验:

df.to_csv("Open_Data_RDW__Gekentekende_voertuigen_short.csv", sep=',', 
          encoding='utf-8')

这个文件只有 580 KB 大小,比原始的 10 GB 小得多,并且加载时没有造成任何问题。

我们也不再需要“Voertuigsoort”字段,删除这一列将释放一些 RAM 和屏幕空间。最后一步,让我们将数据字段从荷兰语翻译成英语——这对分析不是强制性的,但对读者会更方便:

df = df.drop('Voertuigsoort', axis=1)

translations_dict_en_nl = {
                           'Kenteken': 'License plate', 
                           'Merk': 'Model', 
                           'Handelsbenaming': 'Trade name', 
                           'Aantal cilinders': 'Number of Cylinders', 
                           'Catalogusprijs': 'Catalog price',
                           'Datum eerste tenaamstelling in Nederland': 'First registration NL', 
                          }

df.rename(translations_dict_en_nl, axis='columns', inplace=True)

现在我们准备好了。

基本分析

开始时,让我们看看数据集的主要属性,例如数据样本和维度:

display(df)

display(df.shape[0])

display(df.isna().sum())

display(df)方法向我们显示数据集的第一行和最后一行,这样我们可以看到数据的样子。第二行显示了记录的总数,这对计算可能有用,最后的请求将返回每列的空值数量。

输出如下:

Dataframe properties, Image by author

我们有 9,487,265 条记录,每辆车都有车牌、型号和登记日期(这些字段可能是注册的必填项),但其他字段,如“贸易名称”或“目录价格”,在一些汽车中缺失。从技术上讲,我们现在不需要任何清理,但对于某些请求(如价格分布),在进行请求之前我们应该去除 Null 值。

作为这种方法的示例,让我们排序数据,以查看荷兰最贵和最便宜的汽车:

df[df['Catalog price'].notna()].sort_values(by=['Catalog price'], 
                                            ascending=False)

按价格排序的数据框,作者图片

结果很有趣。第一名是“PEUGEOT 5008”,价格为 9,700,305 欧元,这很奇怪,因为在 Google 上它的价格大约是 41,000 欧元——可能是数据库中的错误,或者车主为升级花了很多钱 😉 或者这可能是全新的电动“PEUGEOT E-5008”,但它计划在 2024 年才发布。不管怎样,已经可以看出公共数据并不总是一致的。第二名的“PORSCHE CAYENNE”的价格可能是实际的。对于其他车型,很难判断,我不是豪华车专家,如果有人知道更多,请在下面的评论中写出来。至于最便宜的汽车,它们的价格为 1 欧元。可能它们作为“零件”从二手市场进口到荷兰,因此车主申报了最低可能的价值。

数据转换

让我们检查一下数据是否适合进一步分析。列表中的第一个汽车型号是“PEUGEOT”,让我们显示所有具有相同名称的汽车。“unique”方法将仅返回列中的唯一值

display(df[df['Model'].str.contains("PEUGEOT", case=False)]['Model'].unique())

输出如下:

“Peugeot”型号请求,作者图片

我们可以看到数据库中的模型名称不一致。一些汽车的名称为“PEUGEOT”,其他汽车则被保存为“PEUGEOT BOXER”或“PEUGEOT/MOBILCAR”。为了按模型名称分组汽车,首个单词“PEUGEOT”就足够了,名称的右侧部分可以去掉。将所有字符转换为大写字母也更好,因为理论上汽车型号可以写作“PEUGEOT”或“Peugeot”。为了确保没有多余的字符,我会调用“strip”方法,该方法可以去除字符串中的多余空格。我创建了一个名为“name_normalize”的方法,它执行这种类型的转换:

def model_normalize(s_val: str):
    """ Convert 'PEUGEOT BOXER/GLOBE-TRAVE ' to 'PEUGEOT' """
    if s_val and isinstance(s_val, str) and len(s_val) > 0:
        return s_val.replace("-", " ").replace("/", " ").split()[0].upper().strip()
    return None

可以使用正则表达式进行更灵活的转换,但这段代码对于我们的任务来说已经足够了。当我们有了这个方法后,我们可以使用“map”函数转换 Pandas 数据框中的所有行:

df["Model"] = df['Model'].map(lambda s: model_normalize(s))

现在让我们处理“贸易名称”字段:

“贸易名称”样本,作者图片

如我们所见,大多数汽车在第一个字段中有制造商名称,在第二个字段中有商品名称,如截图中的“VOLVO” + “C30”。但一些其他汽车在两个字段中都有重复的制造商名称,如“NISSAN” + “NISSAN MURANO”。通过删除重复项使其更一致,并且作为奖励,这也会使数据集稍微变小:

def name_normalize(model: str, trade_name: str):
    """ Remove duplicates and convert the name to upper case """
    if isinstance(trade_name, str) and len(trade_name) > 0:
        name = trade_name.upper().strip()
        # Remove duplicates from model and trade name: 
        # ("TESLA", "TESLA MODEL 3") => ("TESLA", "MODEL 3")
        if name.split()[0] == model:
            # "TESLA MODEL 3" => [TESLA, MODEL, 3] => "MODEL 3"
            return ' '.join(name.split()[1:])  
        return name
    return None

这里的isinstance检查很重要,因为“商品名称”字段是可选的,一些记录中有 None 而不是字符串,获取len(None)显然会导致方法崩溃。

要更新数据框,我们可以使用 Pandas 中的“apply”方法:

df["Trade name"] =  df.apply(lambda x: name_normalize(model=x['Model'], 
                                           trade_name=x['Trade name']), 
                                           axis=1)

让我们检查一下结果。拥有这些数据后,我们可以提取一些有用的信息,例如,看看荷兰最受欢迎的前 50 款汽车

n_top = 50
all_models = df_models["Model"].to_numpy()
models, counts = np.unique(all_models, return_counts=True)
cs = counts.argsort()  # Take sort indexes from 'counts' array
x = counts[cs][-n_top:]
y = models[cs][-n_top:]

p = figure(y_range=y, width=1400, height=600, 
           title="Top-%d cars in the Netherlands (data 2023)" % n_top)
p.hbar(right=x, y=y, height=0.8, color=Viridis256[:n_top])
p.xgrid.grid_line_color = None
p.x_range.start = 0
p.below[0].formatter.use_scientific = False
show(p)

np.unique方法可以计算每个模型的数量,我们不需要手动进行。这里第二个棘手的部分是同时对两个数组(汽车数量和汽车模型)进行排序,我们使用counts.argsort方法获得排序索引序列,然后将相同的索引应用于“models”数组。

Bokeh库非常适合绘制这样的图表:

顶级汽车模型条形图,作者提供的图像

数据转换的下一部分更棘手——我们需要确定汽车是否电动。这很棘手,因为每个制造商都有自己命名系统,并且没有通用规则。对于一些品牌,如“TESLA”,这很简单——所有特斯拉汽车都是电动的。对于其他型号,如“HYUNDAI IONIQ”或“NISSAN LEAF”,名称中存在特定关键字,而对于其他一些汽车,根本没有明确的规则(“HONDA E”是电动的,但“HONDA EE8”则不是)。

通过 Google 搜索和汽车制造商的网站,我创建了这个字典:

electric_cars = {
    "AIWAYS": ['U5', 'U6'],
    "AUDI": ['E-TRON'],
    "BMW": ['I3', 'I4', 'I7', 'IX'],
    "CITROEN": ['E-C4'],
    "FIAT": ['500E', 'ELETTRA'],
    "FORD": ['MACH-E'],
    "HONDA": ['"E"', '"E ADVANCE"'],  
    "HYUNDAI": ['IONIQ', 'KONA'],
    "JAGUAR": ['I-PACE'],
    "KIA": ['NIRO', 'E-SOUL'],
    "LEXUS": ['RZ'],
    "LUCID": ['AIR'],
    "MAZDA": ['MX-30'],
    "MERCEDES": ['EQA', 'EQB', 'EQC', 'EQS', 'EQV'],
    "MG": ['ZS EV'],
    "MINI": ['COOPER SE'],
    "NISSAN": ['ALTRA', 'ARIYA', 'EVALIA', 'LEAF', 'NUVU'],
    "OPEL": ['AMPERA-E', 'COMBO-E', 'CORSA-E', 'MOKKA-E', 'VIVARO-E', 'ZAFIRA-E'],
    "PEUGEOT": ['E-208', 'E-2008', 'E-RIFTER', 'E-TRAVELLER'],
    "POLESTAR": ['2', '3'],
    "PORSCHE": ['TAYCAN'],
    "RENAULT": ['MASTER', 'TWINGO', 'KANGOO ELEC', 'ZOE'],
    "SKODA": ['ENYAQ'],
    "SMART": ['EQ'],
    "TESLA": [''],
    "TOYOTA": ['BZ'],
    "VOLKSWAGEN": ['ID.3', 'ID.4', 'ID.5', 'E-GOLF'],
    "VOLVO": ['C40', 'XC40']
}

现在我可以轻松检查特定关键字是否出现在汽车模型中,或者是否有模型名称的直接匹配。最后检查,我可以使用数据库中拥有的气缸数。如果这个值大于零,那么我们知道这辆车不是完全电动的。最终的方法(好吧,也许不是最终的,但对于我们的任务来说或多或少有效)如下所示:

def is_electric(model: str, trade_name: str, cylinders: int):
    """ Determine if the car is electric """
    if isinstance(cylinders, int) and cylinders > 0:
        return False
    for e_model, e_names in electric_cars.items():
        if model == e_model:
            for e_name in e_names:
                if trade_name and (e_name in trade_name or e_name.replace('"', '') == trade_name):
                    return True
                if trade_name is None and len(e_name) == 0:
                    return True
    return False

作为一种单元测试,我们可以使用不同的参数来使用此方法:

print(is_electric("AUDI", "E-TRON S SPORTBACK 55 QUATTRO"))
print(is_electric("AUDI", "80 COUPE"))

print(is_electric("HONDA", "E"))
print(is_electric("HONDA", "EE 8"))
print(is_electric("HONDA", "INTEGRA TYPE R"))

print(is_electric("NISSAN", "MICRA"))
print(is_electric("NISSAN", "LEAF 62KWH"))

print(is_electric("TESLA", "ANY"))

使用这个函数,我们可以轻松地将新字段添加到数据框中,并仅保留电动汽车:

df["Electric"] = df.apply(lambda x: is_electric(model=x['Model'], 
                                       trade_name=x['Trade name'], 
                                       cylinders=x['Number of Cylinders']), 
                          axis=1)

df_electric = df.query("Electric == True").drop(columns=['Number of Cylinders',
                                                         'Electric'])

如果一切都做得正确,我们应该得到这样的结果:

电动汽车数据框,作者提供的图像

分析

现在我们终于准备好开始分析荷兰的电动汽车了。

作为热身,计算均值、标准差和百分位数是很简单的:

print(f"Cars total: {df.shape[0]}")
print(f"Cars electric: {df_electric.shape[0]} ({100*df_electric.shape[0]/df.shape[0]:.2f}%)")

# Calculate percentiles - all cars

prices = df[df['Catalog price'].notna()]['Catalog price'].to_numpy()

print("Price mean:", np.mean(prices))
print("Price standard deviation:", np.std(prices))
print("Percentiles [5, 25, 50, 75, 95]:", np.percentile(prices, [5, 25, 50, 75, 95]))

# Calculate percentiles - electric cars

prices = df_electric[df_electric['Catalog price'].notna()]['Catalog price'].to_numpy()

print("Electric cars price mean:", np.mean(prices))
print("Electric cars price standard deviation:", np.std(prices))
print("Electric cars percentiles [5, 25, 50, 75, 95]:", np.percentile(prices, [5, 25, 50, 75, 95]))

输出结果如下:

均值、标准差和百分位数结果,作者提供的图像

我们可以看到荷兰共有 9,487,265 辆汽车,其中仅有 278,141 辆(2.93%)是电动汽车。好吧,到 2023 年,我们只是这个时代的开始。根据rvo.nl的报告,2019 年电动汽车占比为 1.22%,2020 年为 1.98%,2021 年为 2.55%,因此这些数字在增长,未来 10-20 年比较结果会很有趣。至于非电动汽车的价格,第 95 百分位数为€71,381。这意味着荷兰 95%的汽车价格低于此值。电动汽车则处于更“高端”的区间——平均价格为€49,975,第 95 百分位数为€106,989。

荷兰第一辆电动汽车是什么时候出现的,这个数量随着时间的推移有何变化?这个问题很容易回答。让我们构建一个每季度汽车注册数量的柱状图。为此,我需要在 Pandas 数据框中创建一个新的Quarter字段,并按此字段分组数据。我们可以从 Python 的“datetime”对象中提取季度号,但 Pandas 已经有所有需要的转换器:

reg_dates = df_electric[["Model", "Trade name", "First registration NL"]].copy()
reg_dates["Quarter"] = reg_dates['First registration NL'].dt.to_period('Q')

data_per_year = reg_dates.groupby(['Quarter'], as_index=False).size()

dates = data_per_year['Quarter']
amount = data_per_year['size']

p = figure(x_axis_type='datetime', width=1600, height=500, 
           title=f"Electric car registrations in the Netherlands, 1992-2022")
p.vbar(x=dates, top=amount, width=datetime.timedelta(days=3*22), line_color='black')
p.xaxis[0].ticker.desired_num_ticks = 30
p.xgrid.grid_line_color = None
show(p)

结果很有趣:

电动汽车注册情况,作者提供的图片

第一辆(在接下来的 15 年里全国唯一的一辆!)电动汽车于 1992 年在荷兰注册,距今已有 30 多年。我们可以轻松地在数据集中找到,它是一辆Fiat Panda Elettra,这是一款最高时速 70 公里、续航 100 公里的两座小车,电源由 12 个 6V 铅酸电池提供。接下来的 3 辆Tesla Roadster仅在 2009 年注册。第二件有趣的事情是季节性模式——很容易看到每年年底的注册数量最大(更详细的图表将在本文末尾展示)。

拥有电动汽车数据框后,我们也很容易看到价格分布

df_prices = df_electric[df_electric['Catalog price'].notna()]
prices_to_display = df_prices.query('`Catalog price` < 170000')['Catalog price'].to_numpy()

hist_e, edges_e = np.histogram(prices_to_display, density=False, bins=50)

# Draw
p = figure(width=1400, height=500, 
           title=f"Electric cars price distribution in the Netherlands ({df_electric.shape[0]} cars total)")
p.quad(top=hist_e, bottom=0, left=edges_e[:-1], right=edges_e[1:], line_color="darkblue")
p.x_range.start = 15000
p.x_range.end = 150000
p.y_range.start = 0
p.xaxis[0].ticker.desired_num_ticks = 20
p.left[0].formatter.use_scientific = False
p.below[0].formatter.use_scientific = False
p.xaxis.axis_label = "Price, EUR"
p.yaxis.axis_label = "Amount"
show(p)

输出如下:

电动汽车价格分布,作者提供的图片

如我们所见,分布大多向右偏斜。在计算直方图之前,我去除了右侧的异常值,否则由于 2–3 辆价格超过 1 百万欧元的汽车,图像几乎是空的。

使用直方图我们可以看到,大多数荷兰的电动汽车价格在€40–70K 范围内,但我们不知道具体是哪些车型。我们可以更详细地探索价格——让我们按车型名称分组价格

df_price = df_electric[df_electric['Catalog price'].notna()]

def q0(x):
    return x.quantile(0.01)

def q1(x):
    return x.quantile(0.25)

def q3(x):
    return x.quantile(0.75)

def q4(x):
    return x.quantile(0.99)

agg_data = {'Catalog price': ['size', 'min', q0, q1, 'median', q3, q4, 'max']}

prices = df_price[['Model', 'Catalog price']].groupby('Model', as_index=False).agg(agg_data)
display(prices)

在这里,我将所有汽车按型号分组,并将数据汇总到一个表中:

按车型名称分组的电动汽车,作者提供的图片

让我们以箱形图的形式绘制这些数据:

prices = prices.sort_values(by=('Catalog price', 'median'), ascending=True)

models = prices["Model"].to_numpy()
q1 = prices["Catalog price"]["q1"].to_numpy()
q3 = prices["Catalog price"]["q3"].to_numpy()
v_min = prices["Catalog price"]["q0"].to_numpy()
v_max = prices["Catalog price"]["q4"].to_numpy()

palette = (Inferno10 + Magma10 + Plasma10 + Viridis10)[:models.shape[0]]
source = ColumnDataSource(data=dict(models=models, 
                                    bottom=q1, 
                                    top=q3, 
                                    color=palette, 
                                    lower=v_min, 
                                    upper=v_max))

p = figure(x_range=models, width=1400, height=500, 
           title=f"Electric cars price distribution in the Netherlands") 
whisker = Whisker(base="models", upper="upper", lower="lower", source=source)
p.add_layout(whisker)
p.vbar(x='models', top='top', bottom='bottom', width=0.9, color='color', 
       line_color="black", source=source)

p.left[0].formatter.use_scientific = False
p.y_range.start = 0
show(p)

在这里,我按价格对所有车型进行了排序。结合箱线图,我们可以清楚地看到价格分布的情况:

电动汽车制造商和价格箱线图,图片由作者提供

不足为奇的是,分布顶部的是著名的豪华车,如保时捷、捷豹或 Lucid(顺便说一下,我之前从未听说过)。但更令人惊讶的是,这个分布中最便宜的车并不是最受欢迎的。例如,在荷兰只有 1,269 辆“Smart”和 15,414 辆“Renault”,相比之下,65,885 辆“Tesla”模型不到 25%。我甚至怀疑图表中是否有错误,但2021 年英国汽车销售分布总体上看起来是一样的。

最大续航 (公里)可能是选择电动汽车时一个重要的因素,构建一个显示续航与价格相关性的图表会很有趣。但遗憾的是,RDW 数据集中没有“续航”字段。一些不同电动汽车的值可以从Kaggle 数据集中获得。但实际上,两张表之间没有直接匹配。例如,数据集中有一个“E-TRON SPORTBACK 50 QUATTRO”模型。在 RDW 数据中,有两个类似名称的车型,“E-TRON SPORTBACK 50”和“Q4 SPORTBACK 50 E-TRON”,但我不确定这些车型是否实际上是一样的。每个型号名称中的字母可能都有自己的含义,没有汽车专家的知识,很难匹配所有汽车制造商的所有名称。不过,有兴趣的读者可以自行尝试。

至少,拥有来自 RDW 数据的汽车价格和注册日期,我们可以构建散点图:

df_data = df_electric[df_electric['Catalog price'].notna()]

models = df_data['Model'].unique()

p = figure(x_axis_type='datetime', width=1400, height=800, 
           title="Electric car prices and registrations in the Netherlands")
palette = (Inferno10 + Magma10 + Plasma10 + Viridis10)[:len(models)]
draw_ratio = 15
for ind, model in enumerate(models):
    df_model = df_data[df_data['Model'] == model]
    if df_model.shape[0]//draw_ratio == 0:
        continue

    df_model = df_model.sample(df_model.shape[0]//draw_ratio)
    x = df_model['First registration NL'].to_numpy()
    y = df_model['Catalog price'].to_numpy()
    p.scatter(x, y, size=2, color=palette[ind], legend_label=model[:3]) 

p.left[0].formatter.use_scientific = False
p.legend.orientation = "horizontal"
p.legend.location = "top_center"
p.legend.click_policy = "mute"
p.xaxis[0].ticker.desired_num_ticks = 20
show(p)

在这里,我将所有汽车按型号名称分组,然后从每个子集随机抽取 1:15 的样本(显然,我们不能在一个图上绘制所有 216316 辆车)。Bokeh 的一个非常棒的功能是能够通过点击“静音”标签,例如,仅用鼠标点击突出显示 Tesla 汽车:

价格和注册日期散点图,图片由作者提供

看到季节性模式相当有趣——看起来汽车注册每年“波动”4 次,每年年底会有大量新车注册。也许有些客户特别等待年末促销,或者只是想以新车开始新的一年?第二个有趣的点是,在 2019 年出现了大量中等价格范围的电动汽车。在此之前,选择仅限于便宜的和高端的车型(当然,相对便宜,因为大多数电动汽车与汽油车相比都属于高端细分市场)。

结论

如我们所见,使用来自“现实世界”的数据集带来了一些挑战。这不仅仅是数据规模,还有数据不完整或不一致,甚至数据字段中的语法错误(例如,我看到一个“Tesla Raodster”而不是“Tesla Roadster”),等等。与此同时,探索这些数据并发现其中有趣的模式要有趣得多,我建议读者自己做类似的实验。这项分析是为了娱乐和自我教育目的,显然还有很大的改进空间,例如寻找更好的检测方法以判断汽车是否是电动的,找出不同型号的续航里程,等等。而荷兰政府保持这些数据公开并对所有人开放,这很好。如果有人知道其他国家的数据集,请在下方评论中添加链接,我将尝试做类似的分析并在下一个帖子中比较结果。

如果你喜欢这个故事,可以随时订阅 Medium,你将获得新文章发布的通知,并可以全面访问其他作者的数千个故事。

感谢阅读。

使用 spacy-llm 进行优雅的提示版本管理和 LLM 模型配置

原文:towardsdatascience.com/elegant-prompt-versioning-and-llm-model-configuration-with-spacy-llm-126b836daad1?source=collection_archive---------3-----------------------#2023-07-26

使用 spacy-llm 简化提示管理并创建数据提取任务

Déborah MesquitaTowards Data Science Déborah Mesquita

·

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

--

一张 整洁的桌子,如果你使用 spacy-llm,你的代码将会像这样哈哈

管理提示和处理 OpenAI 请求失败可能是一个具有挑战性的任务。幸运的是,spaCy 发布了 spacy-llm,这是一个强大的工具,可以简化提示管理,并消除了从头创建自定义解决方案的需求。

在本文中,你将学习如何利用 spacy-llm 创建一个从文本中提取数据的任务。我们将深入了解 spaCy 的基础知识,并探索一些 spacy-llm 的功能。

spaCy 和 spacy-llm 101

spaCy 是一个用于 Python 和 Cython 的高级 NLP 库。在处理文本数据时,通常需要几个处理步骤,例如分词和词性标注。为了执行这些步骤,spaCy 提供了 nlp 方法,它会调用一个处理管道。

spaCy v3.0 引入了 config.cfg,这是一个我们可以在其中包括这些管道详细设置的文件。

config.cfg 使用了 confection,这是一个允许创建任意对象树的配置系统。例如,confection 解析以下 config.cfg

[training]
patience = 10
dropout = 0.2
use_vectors = false

[training.logging]
level = "INFO"

[nlp]
# This uses the value of training.use_vectors
use_vectors = ${training.use_vectors}
lang = "en"

进入:

{
  "training": {
    "patience": 10,
    "dropout": 0.2,
    "use_vectors": false,
    "logging": {
      "level": "INFO"
    }
  },
  "nlp": {
    "use_vectors": false,
    "lang": "en"
  }
}

每个管道使用组件,spacy-llm 将管道组件存储到使用 catalogue 的注册表中。这个库,同样来自 Explosion,引入了函数注册表,以便高效地管理组件。llm 组件被定义在 两个主要设置 中:

  • 一个 任务,定义要发送到 LLM 的提示以及解析生成响应的功能

  • 一个 模型,定义模型及其连接方式

要在我们的管道中包含一个使用 LLM 的组件,我们需要遵循几个步骤。首先,我们需要创建一个任务并将其注册到注册表中。接下来,我们可以使用模型来执行提示并检索响应。现在是时候完成这些操作,以便我们可以运行管道了

创建一个从文本中提取数据的任务

我们将使用 dummyjson.com/ 上的引用,并创建一个任务来从每个引用中提取上下文。我们将创建提示、注册任务,并最终创建配置文件。

1. 提示

spacy-llm 使用 Jinja 模板来定义指令和示例。 {{ text }} 将被我们提供的引用所替换。这是我们的提示:

You are an expert at extracting context from text. 
Your tasks is to accept a quote as input and provide the context of the quote.
This context will be used to group the quotes together. 
Do not put any other text in your answer and provide the context in 3 words max.
{# whitespace #}
{# whitespace #}
Here is the quote that needs classification
{# whitespace #}
{# whitespace #}
Quote:
'''
{{ text }}
'''
Context

2. 任务类

现在让我们创建任务的类。这个类应该实现两个函数:

  • **generate_prompts(docs: Iterable[Doc]) -> Iterable[str]**:一个函数,它接受一个 spaCy [Doc](https://spacy.io/api/doc) 对象的列表,并将其转换为一个提示的列表

  • **parse_responses(docs: Iterable[Doc], responses: Iterable[str]) -> Iterable[Doc]**:一个将 LLM 输出解析为 spaCy [Doc](https://spacy.io/api/doc) 对象的函数

**generate_prompts** 将使用我们的 Jinja 模板,而 **parse_responses** 将为我们的 Doc 添加上下文属性。这是 QuoteContextExtractTask 类:

from pathlib import Path
from spacy_llm.registry import registry
import jinja2
from typing import Iterable
from spacy.tokens import Doc

TEMPLATE_DIR = Path("templates")

def read_template(name: str) -> str:
    """Read a template"""

    path = TEMPLATE_DIR / f"{name}.jinja"

    if not path.exists():
        raise ValueError(f"{name} is not a valid template.")

    return path.read_text()

class QuoteContextExtractTask:
  def __init__(self, template: str = "quotecontextextract.jinja", field: str = "context"):
    self._template = read_template(template)
    self._field = field

  def _check_doc_extension(self):
     """Add extension if need be."""
     if not Doc.has_extension(self._field):
         Doc.set_extension(self._field, default=None)

  def generate_prompts(self, docs: Iterable[Doc]) -> Iterable[str]:
    environment = jinja2.Environment()
    _template = environment.from_string(self._template)
    for doc in docs:
        prompt = _template.render(
            text=doc.text,
        )
        yield prompt  

  def parse_responses(
      self, docs: Iterable[Doc], responses: Iterable[str]
  ) -> Iterable[Doc]:
    self._check_doc_extension()
    for doc, prompt_response in zip(docs, responses):      
      try:
        setattr(
            doc._,
            self._field,
            prompt_response.replace("Context:", "").strip(),
        ),
      except ValueError:
        setattr(doc._, self._field, None)

    yield doc

现在我们只需将任务添加到 spacy-llm 的 llm_tasks 注册表中:

@registry.llm_tasks("my_namespace.QuoteContextExtractTask.v1")
def make_quote_extraction() -> "QuoteContextExtractTask":
    return QuoteContextExtractTask()

3. config.cfg 文件

我们将使用 OpenAI 的 GPT-3.5 模型。spacy-llm 为此提供了一个模型,所以我们只需确保秘密密钥作为环境变量可用:

export OPENAI_API_KEY="sk-..."
export OPENAI_API_ORG="org-..."

为了构建运行管道的 nlp 方法,我们将使用 spacy-llm 的 assemble 方法。该方法从 .cfg 文件中读取。文件应引用 GPT-3.5 模型(它已在注册表中)和我们创建的任务:

[nlp]
lang = "en"
pipeline = ["llm"]
batch_size = 128

[components]

[components.llm]
factory = "llm"

[components.llm.model]
@llm_models = "spacy.GPT-3-5.v1"
config = {"temperature": 0.1}

[components.llm.task]
@llm_tasks = "my_namespace.QuoteContextExtractTask.v1"

4. 运行管道

现在我们只需将所有内容组合在一起并运行代码:

import os
from pathlib import Path

import typer
from wasabi import msg

from spacy_llm.util import assemble
from quotecontextextract import QuoteContextExtractTask

Arg = typer.Argument
Opt = typer.Option

def run_pipeline(
    # fmt: off
    text: str = Arg("", help="Text to perform text categorization on."),
    config_path: Path = Arg(..., help="Path to the configuration file to use."),
    verbose: bool = Opt(False, "--verbose", "-v", help="Show extra information."),
    # fmt: on
):
    if not os.getenv("OPENAI_API_KEY", None):
        msg.fail(
            "OPENAI_API_KEY env variable was not found. "
            "Set it by running 'export OPENAI_API_KEY=...' and try again.",
            exits=1,
        )

    msg.text(f"Loading config from {config_path}", show=verbose)
    nlp = assemble(
        config_path
    )
    doc = nlp(text)

    msg.text(f"Quote: {doc.text}")
    msg.text(f"Context: {doc._.context}")

if __name__ == "__main__":
    typer.run(run_pipeline)

然后运行:

python3 run_pipeline.py "We must balance conspicuous consumption with conscious capitalism." ./config.cfg
>>> 
Quote: We must balance conspicuous consumption with conscious capitalism.
Context: Business ethics.

如果你想更改提示,只需创建另一个 Jinja 文件,并以与第一次创建相同的方式创建 my_namespace.QuoteContextExtractTask.v2 任务。如果你想更改温度,只需在 config.cfg 文件中更改参数。不错,对吧?

最后的思考

处理 OpenAI REST 请求的能力及其直接存储和版本控制提示的方法是我最喜欢 spacy-llm 的地方。此外,该库提供了一个用于缓存每个文档的提示和响应的缓存功能,一个为少量示例提示提供示例的方法,以及日志记录功能等。

你可以在这里查看今天的完整代码:github.com/dmesquita/spacy-llm-elegant-prompt-versioning

一如既往,谢谢你的阅读!

提升你的商业分析:季节调整的逐步指南

原文:towardsdatascience.com/elevate-your-business-analytics-step-by-step-guide-to-seasonal-adjustment-74434e8cceca

Juan Jose MunozTowards Data Science Juan Jose Munoz

·发布于 Towards Data Science ·7 分钟阅读·2023 年 11 月 27 日

--

我们都理解将时间序列拆解为其组成部分以进行预测的重要性,但在商业绩效分析中却没有得到足够的重视。

作为一名商业绩效分析师,我经常报告月度收入绩效并跟踪商业周期趋势。为了处理季节性变化的问题,我依赖于同比比较。问题在于,这些比较依赖于 12 个月前的数据,这意味着你会晚于趋势,这可能会带来毁灭性的后果。经济学家和统计学家有更复杂的方法来应对季节性波动,并在业务周期发生变化后尽快捕捉到这些变化。

经济学家分解宏观经济数据以报告季节调整后的数据,并依赖于季节调整后的指标的月度(或季度)变化,以及时了解经济活动。

图片由 Stephen Dawson 提供,来源于 Unsplash

你无需成为统计学家或经济学家来跟上你的商业趋势。美国人口普查局将他们的 X-13ARIMA-SEATS 季节调整软件公开发布,下面是如何在 Python 中利用它来提升你的商业分析。

下载 X 13 ARIMA SEATS

你可以利用 Statsmodels X13_arima_analysis 这个 Python 封装器来调整你的商业数据以应对季节波动。

首先,你需要从 Census 网站 下载 X-13ARIMA-SEATS 可执行文件。

最新版本——60 版(撰写时)对我不起作用,所以我下载了之前的版本——59 版。

下载完成后,你可以在你选择的文件夹中解压文件。

解压后,你应该得到一个像这样的文件夹。(图像来源于作者)

设置你的 Python 笔记本。

除了导入你通常用于数据分析的包外,你还需要设置环境变量 X13PATH 为解压文件夹的路径。如果跳过这一步,你在运行分析时会出现错误。

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

from statsmodels.tsa.x13 import x13_arima_analysis

from datetime import datetime
from dateutil.relativedelta import relativedelta
import os

# Set the X13PATH environmental variable to the folder where you unzipped the X-13 executables
os.environ['X13PATH'] = r"C:\...\x13as_ascii-v1-1-b59\x13as"

导入并准备你的数据

在这个示例中,我使用了公开报告的特斯拉季度收入。

要运行 X13_arima_analysis,你需要至少 3 年的数据以便模型捕捉季节性模式。你的数据应为每月或每季度。你需要将日期列设置为数据框的索引,并确保指定频率。

#load data
df = pd.read_excel("TSLA_Revenue.xlsx")

#set date as index
df.set_index('date', inplace=True)

#set frequency as quarter
df= df.resample('Q').asfreq()

#View index
df.index

确保频率设置为季度或每月

# display data
df.head()

收入单位为百万美元

这些是你需要的唯一转换。

实例化 x_13_arima_analysis

# Run X-13ARIMA-SEATS decomposition
results = x13_arima_analysis(df['revenue'])

x_13_arima_analysis 结合了 ARIMA 建模和 SEATS 过滤来分解时间序列数据。分析中的 ARIMA(自回归积分滑动平均)部分基于数据的过去值和误差对数据进行建模。SEATS(ARIMA 时间序列中的信号提取)部分专注于隔离时间序列成分(趋势、周期、季节性、异常)

x_13_arima 提供通过从实际收入中去除季节性成分来获得季节调整后的收入

# Get the seasonally adjusted series
seasonally_adjusted = results.seasadj

# Visualize revenue and seasonally adjusted revenue
plt.figure(figsize=(10, 6))
plt.subplot(311)
plt.plot(df.index, df['revenue'], label='Original Data')
plt.legend()

plt.subplot(312)
plt.plot(df.index, seasonally_adjusted, label='Seasonally Adjusted')
plt.legend()

plt.tight_layout()
plt.show()

季节性调整去除了季节性模式(图像来源于作者)

验证

在这次分析中,我们不专注于预测能力。相反,我们想要分析季节调整后的数据,以便紧密跟随我们的业务趋势。

然而,我们仍需检查模型是否成功分解了时间序列。

你可以使用 QS 统计量来评估分析的稳健性。目标是使 QS 统计量低于 1。你的 QS 统计量结果越接近 0,就越表示残差与白噪声不可区分或不相关。

print(results.results)

结果打印了大量信息。你需要滚动直到找到 QS 统计量。

解读结果

现在我们有了季节调整后的数据,我们可以用两种方式进行业务趋势分析。

首先,我们可以关注季节调整后收入的季度环比增长。

# Calculate the % chg to the previous quarter
df['QoQ %chg'] = df['revenue'].pct_change() * 100

df['QoQ% chg adjusted'] = df['seasadj'].pct_change() * 100

# Getting the index positions for x-axis locations
x = range(len(df.index))

# Plotting the bar chart
plt.figure(figsize=(10, 6))

bar_width = 0.40

plt.bar(x, df['QoQ %chg'], width=bar_width, align='center', label='% chg', color='blue', alpha=0.7)
plt.bar([i + bar_width for i in x], df[f'QoQ% chg adjusted'], width=bar_width, align='center', label='% chg adjusted', color='teal', alpha=0.7)

# Enhance the visualization
plt.axhline(y=0, color='gray', linestyle='--', linewidth=1)
plt.xlabel('Date')
plt.ylabel('% Change')
plt.title(f'Comparison of {metric_name} % chg and % chg adjusted')
plt.legend()
plt.xticks([i + bar_width/2 for i in x], df.index.strftime('%Y-%m-%d'), rotation=45)
plt.tight_layout()

plt.show()

QoQ 变化(图像来源于作者)

2023 年第一季度环比百分比变化突出显示了使用季节调整数据的重要性。未经调整的情况下,收入相比上季度有所下降;然而,一旦调整季节性模式后,我们看到收入增长,表明业务趋势强劲。

第三季度情况正好相反;季节调整后的收入下降幅度超过预期,这应引发进一步的分析。

第二项分析涉及计算季节调整年化率(SAAR)

SAAR = ((季节调整后的收入 * 4 )/ 去年收入) - 1

对于季度数据,我们将数据乘以 4 以年化;如果是月度数据,我们将乘以 12。这个措施有助于提供全年数据的平滑、标准化视图。

请记住,SAAR 不是预测。 但它可以通过提供更清晰的财务状况来帮助你做出明智的业务决策。

知道这两条路径后,我们可以定义一个函数来自动化分析。

def results_analysis(result = results, analysis_date= '2023-09-30', freq ='Quarter'):
    """
    This function takes the results from X13 arima analysis and returns a dataframe with:

        - Revenue
        - Seasonally adjusted Revenue
        - Revenue vs previous period
        - Seasonally adjusted Revenue vs previous period
        - SAAR : Seasonally adjusted annual rate
        - SAAR %chg vs last year

    The funtion also print key financial output
        - Revenue
        - Revenue vs last year
        - Revenue vs previous period
        - Seasonally adjusted Revenue vs previous period
        - SAAR
        - SAAR %chg vs last year

    Parameters
    ----------
        result : statsmodels.tsa.x13.X13ArimaAnalysisResult object
            the result from instantiating x13_arima_analysis
        analysis_date : str
            the date for analysis
        freq : str, optional
            the frequency of our data, either "Quarter" or "Month" (default is Quarter)    
    """

    #get the observed & Seasonally adjusted data into Dataframe
    observed = pd.DataFrame(result.observed)

    seasonal_adj = pd.DataFrame(result.seasadj)
    df = pd.concat([observed,seasonal_adj],axis=1)

    # get  data from previous Year until analysis_date
    analysis_date = datetime.strptime(analysis_date, '%Y-%m-%d') # convert variable to datetime

    last_year = analysis_date.year -1
    df = df[df.index.year >= last_year].copy()

    #Calculate QoQ or MoM revenue change and Sesonally adjusted revenue change
    metric_name = 'QoQ' if freq == 'Quarter' else 'MoM'

    df[f'{metric_name} %chg'] = df['revenue'].pct_change() * 100

    df[f'{metric_name}% chg adjusted'] = df['seasadj'].pct_change() * 100

    #calculate LY revenue

    ly_revenue = df[df.index.year == last_year]['revenue'].sum()

    #Calculate Seasonally Adjusted Annual Rate and chg

    annual_factor = 4 if freq == 'Quarter' else 12 # assing annual factor for SAAR calculation

    df['SAAR'] = df.apply(lambda row: row['seasadj'] * annual_factor if row.name.year == analysis_date.year else None, axis=1)

    df['SAAR % Chg'] = df.apply(lambda row: (row['SAAR'] / ly_revenue - 1)*100 if row.name.year == analysis_date.year else None, axis=1)

    data = df[df.index==analysis_date]# get the data for the analysis date
    ly_data = df[df.index==(analysis_date - relativedelta(years=1))]# get the data for the previous year analysis date

    #Print results
    print(f'{freq} Revenue: {data["revenue"][0]}')
    print(f'{freq} Revenue YoY %chg: {(data["revenue"][0]/ly_data["revenue"][0]-1)*100 :.1f}')
    print(f'{freq} Revenue {metric_name} %chg: {data[f"{metric_name} %chg"][0] :.1f}')
    print(f'{freq} Seasonally adjusted Revenue {metric_name} %chg: {data[f"{metric_name}% chg adjusted"][0] :.1f}')
    print(f'Seasonally adjusted annual rate: {data["SAAR"][0]}')
    print(f'Seasonally adjusted annual rate %chg: {data["SAAR % Chg"][0] :.1f}')

    return df

df_results = results_analysis(results)

这个功能可以帮助你将所有相关的比较放在一起(图像由作者提供)

这些步骤提供了一个简单的框架,可以通过及早捕捉和应对业务趋势变化来提升您的业务分析能力。

参考文献

[1] 美国人口普查局。X-13ARIMA-SEATS 文档。获取自www2.census.gov/software/x-13arima-seats/x-13-data/documentation/docx13as.pdf

2 Statsmodels. X-13 ARIMA 分析文档。获取自www.statsmodels.org/dev/_modules/statsmodels/tsa/x13.html#x13_arima_analysis

3 Singstat。季节调整。获取自www.singstat.gov.sg/find-data/quizzes/seasonal-adjustment

4 Macrotrends。特斯拉财务报表。获取自www.macrotrends.net/stocks/charts/TSLA/tesla/income-statement?freq=Q

5 JDemetra+文档。季节调整输出 — X13。获取自jdemetradocumentation.github.io/JDemetra-documentation/pages/reference-manual/sa-output-X13.html

6 Conerly, Bill (2014 年 12 月 17 日)。如何调整您的业务数据以适应季节性变化。Forbes。获取自www.forbes.com/sites/billconerly/2014/12/17/how-to-adjust-your-business-data-for-seasonality/?sh=3b3522ed421c

[7] Investopedia。季节调整。获取自www.investopedia.com/terms/s/seasonal-adjustment.asp

[8] 达拉斯联邦储备银行。季节性调整数据。获取自 www.dallasfed.org/research/basics/seasonally

提升你的数据科学职业生涯:如何成为一名高级数据科学家

原文:towardsdatascience.com/elevate-your-data-science-career-how-to-become-a-senior-data-scientist-5d7546d060e2

作者概述了五种策略,这些策略将使你的数据科学实践提升到高级角色。

Guillaume ColleyTowards Data Science Guillaume Colley

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

--

你已经在数据科学领域工作了几年,你的目标是达到下一个层级。在你目前的数据科学家角色中表现出色是至关重要的,但在许多组织中,仅靠这点并不足以推动你向前发展。你需要做得更多——或者有所不同。在这篇文章中,我旨在提供一些宝贵的想法和例子,指导你朝着高级数据科学家的方向发展。不论你是在追求内部晋升还是考虑外部机会,希望这些内容能帮助你取得成功!

图片由 Ante Hamersmit 提供,来源于 Unsplash

数据科学家与高级数据科学家的区别

虽然每个组织在定义不同级别的数据科学家时会有所不同,但许多组织在每个级别的基本职责和工作范围上存在共识。

在这篇文章中,我概述了数据科学家三级等级体系的工作范围差异。某些组织可能有更细化的等级,但我相信我描述的职业发展路径在许多公司中是适用的。

[## 解码数据科学家等级体系:从初级到高级——区分所在角色的关键]

揭示初级、中级和高级数据科学家的工作范围期望

towardsdatascience.com

成为高级数据科学家必须掌握技术数据科学技能。经过几年的经验,你的技术工具包应该大幅增长,涵盖各种数据处理和准备技术,以及广泛的机器学习模型,你不仅能应用,还能微调、评估性能并理解其行为。此外,多年的经验也应让你对你所参与的业务有深入的了解。

但仅凭这一点可能无法让你达到高级/首席数据科学家的水平。

你需要扩展工作范围,涵盖构建数据科学解决方案的技术方面以外的内容。

特别是,我坚信下面五个领域将对你成为你想要的高级数据科学家至关重要:

  • 重新思考你的终点线

  • 了解你的利益相关者

  • 创造机会

  • 掌握流程

  • 成为一名教师

1 — 重新思考你的终点线

“从头到尾领导数据科学项目”

Anton Shuvalov 的照片,来源于 Unsplash

以分类模型作为数据科学工作的一个例子。假设是一个客户流失模型——那这项工作会是什么样的呢?

  • 对于被分配这项工作的初级数据科学家,他们的终点线将是在开发环境中训练和验证过的模型。然后,他们将与高级人员合作进行 QA,并达到可用输出的阶段。

  • 对于承担相同工作的数据科学家,他们的终点线可能是训练和验证过的模型已经在 QA 环境中测试并生产化:输出在生产环境中以定义的节奏生成。然后,他们将与高级人员或经理合作,确保这些输出被使用。

  • 对于高级数据科学家而言,终点线看起来有所不同:他们不仅需要训练、验证、QA、生产化模型,还需确保输出被利益相关者使用,并且模型的价值被衡量。在我们的流失例子中,这可能意味着设置并部署一个保留活动或项目,测量使用模型输出的效果,并与业务沟通,做出推广或继续执行项目的决定。

高级数据科学家项目只有在利益相关者将其用于日常操作时才算成功。

尽管我使用了“项目”术语,但必须认识到,高级数据科学家的项目是持续进行的:一旦部署并证明成功,模型使用需要被监控和优化,模型本身也需要定期刷新和测试。这就是为什么高级数据科学家在项目实施后可能会将其视为程序

通常,程序的技术方面(如模型开发或更新)是最短的阶段。与利益相关者合作进行模型部署并确保其有效使用通常需要更多的时间、精力和协调。这就是高级数据科学家往往需要戴上项目管理帽子,领导程序走向成功的采用和利用的原因。

2 — 了解你的利益相关者

“与更广泛的跨职能领域建立关系”

照片由 mauro mora 提供,刊登于 Unsplash

凭借多年的经验,数据科学家很可能已经与他们的直接利益相关者建立了宝贵的关系——这可能是工程团队、市场、战略或财务部门的常规利益相关者。

高级数据科学家应当证明他们积极地与组织内更广泛的领域建立了关系。

高级数据科学家了解不同部门面临的业务挑战,他们知道不断变化的优先级和运作方式。

培养这些关系的各种方法可以是与关键利益相关者建立定期聊天或联系点,被邀请参加其他部门的定期绩效会议,或者主动向其他团队介绍自己,并对他们的工作和挑战表现出真正的好奇心。

高级数据科学家还利用他们对利益相关者的理解和深厚的商业洞察力来进行有效沟通。他们非常了解他们的受众,并量身定制他们的沟通和演讲风格以及细节的水平,以适应每种情况。他们在制定内容之前会考虑“对他们有什么好处”,并讲述与利益相关者产生共鸣的故事。例如,他们理解市场总监优先考虑经过验证的项目或程序价值,而财务副总裁则关注方法论的稳健性。

3 — 创造机会

“识别并推动新的项目机会”

照片由 Lukas Tennie 提供,刊登于 Unsplash

了解利益相关者及其挑战使高级数据科学家能够找到支持他们的方法。他们能够理解利益相关者面临的业务问题,并主动构思有效、相关和可操作的数据科学解决方案。

尽管高级数据科学家的“终点线”远远超出了技术部分的完成,“起点线”则位于比数据收集或模型构建准备阶段更上游的位置。

这是一种“始终在线”的态度,数据科学家与利益相关者或团队成员的每一次互动都可能带来新机会。高级数据科学家总是问自己:“我们如何利用已有的资源,或者我们是否可以开发一些新的东西来做得更好?”

一旦你对自己可以做什么有了清晰的想法——尤其是它如何被使用以及会带来什么价值,就该向你的经理提出这个想法以获得批准。随后,你可以回到利益相关者那里,评估他们的兴趣,并启动一个更正式的项目。投资一些时间构建一个初步的原型或概念验证来说明你的想法可能会很有帮助。

凭借对分析/数据科学能力和不同业务领域的深刻理解,高级数据科学家能够连接各个环节,并创造机会以解决业务问题或改善业务功能。

4 — 精通流程

“建议改进流程和程序”

图片由Sam Moghadam Khamseh拍摄,来源于Unsplash

高级数据科学家的深刻业务理解扩展到他们组织的流程和程序。

高级数据科学家知道如何请求新数据,数据的摄取、整合和建模步骤。他们知道将结果投入生产所需的步骤,对这些结果部署到各种业务平台非常熟悉,并确保结果对业务利益相关者是可访问和可操作的。

他们不仅知道并遵循这些流程和程序,还识别改进并实施黄金标准的数据、分析和机器学习操作流程,供整个数据团队遵循。

高级数据科学家有效地驾驭组织结构,利用可用的支持功能——项目经理、业务分析师、产品负责人——以推动他们的项目向前发展。

5 — 成为一名教师

“指导和培训数据科学家和数据分析师”

图片由Markus Spiske提供,Unsplash

正如在这篇精彩的文章中提到的,指导初级人员或数据科学家有很多好处,是被视为高级数据科学家的一个重要前提。

高级数据科学家是数据科学家或数据分析师的参考点。

通过花时间支持更多初级团队成员,高级数据科学家在团队内培养了知识共享的文化,建立了他们的信誉,赢得了信任,增强了合作,并获得了未来可能在人事管理中的经验。

结论

我们已经看到,技术专长不足以让你晋升到高级或首席数据科学家的级别。

高级数据科学家主动识别业务中的数据科学项目或产品机会,并推动这些数据科学解决方案的业务使用,而不仅仅是构建它们。

提高他们在组织内的可见性也是高级数据科学家不可或缺的职责:他们通过作为数据科学家和数据分析师的参考点,理解和支持广泛的职能领域、他们的过程和利益相关者,积极作为一个优秀的团队成员。

我希望这能给你提供想法和方向,帮助你在数据科学家的职业阶梯上晋升!

你怎么看?我有没有遗漏什么?请在下面的评论中告诉我!

来源

[1] G.Colley,《解码数据科学家等级:从初级到高级——他们有什么不同?》(2023)

## 解码数据科学家等级:从初级到高级——他们有什么不同?

阐明初级、中级和高级数据科学家的工作期望范围

towardsdatascience.com

2 E.Berge,《成为数据科学家所需的软技能》(2023)

## 成为数据科学家所需的软技能

能推动你职业发展的前五大软技能

towardsdatascience.com

并且不要犹豫关注我,以获取更多数据科学职业/领导力内容!

## 5 个促进数据科学家/分析师参与的想法,而不让会议压得喘不过气来

作者分享了他们成功实施的策略,以实现这一平衡。

[towardsdatascience.com

Elliot 激活函数:它是什么,它有效吗?

原文:towardsdatascience.com/elliot-activation-function-what-is-it-and-is-it-effective-59b63ec1fd8a

什么是 Elliot 激活函数,它是否是神经网络中其他激活函数的良好替代方案?

本杰明·麦克洛斯基Towards Data Science 本杰明·麦克洛斯基

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

--

Elliot 激活函数(图片来源:作者)

介绍

你是否在创建新的机器学习模型时,不确定应该使用什么激活函数?

但等一下,什么是激活函数?

激活函数使机器学习模型能够理解和解决非线性问题。在神经网络中使用激活函数特别有助于将每个神经元传递给下一个神经元的最重要信息。今天,ReLU 激活函数通常用于神经网络的架构中,但这并不一定意味着它总是最佳选择。(请查看我下面关于 ReLU 和 LReLU 激活函数的文章)。

## Leaky ReLU 与 ReLU 激活函数:哪一个更好?

一项实验调查在使用 ReLU 激活函数时模型性能是否存在明显差异……

towardsdatascience.com

我最近发现了Elliot 激活函数,它被赞誉为可能替代各种激活函数的选择,包括 Sigmoid 和双曲正切函数。今天我们将进行一个实验来测试 Elliot 激活函数的性能。

实验 1: 测试 Elliot 激活函数与 Sigmoid 激活函数和双曲正切激活函数的性能。

实验 2: 测试 Elliot 激活函数与 ReLU 激活函数的性能。

目标是回答这个问题:Elliot 激活函数是否有效?

Elliot 激活函数

Elliot 激活函数将产生一个相对接近 Sigmoid 和双曲正切激活函数的近似结果。有些人发现 Elliot 比 Sigmoid 激活函数快 2 倍 3。就像 Sigmoid 激活函数一样,Elliot 激活函数被限制在 0 到 1 之间。

Elliot 激活函数(图像来自作者)

实验

问题: Keras 目前在其库中没有 Elliot 激活函数。

解决方案: 我们可以使用 Keras 后端自行创建它!

def elliot(x):
  return ((.5*x) / (1 + K.abs(x)))

elliot = Activation(elliot)

对于这个实验,让我们看看 Elliot 激活函数与类似激活函数以及 ReLU 激活函数的比较,ReLU 是今天神经网络中使用的基本激活函数。

数据集和设置

所有 Python 项目的第一步是导入你的包。

import keras.backend as K
from keras.layers import Layer
from keras.layers import Activation
import pandas as pd 
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from keras import layers
from keras import Sequential

今天使用的数据集是鸢尾花数据集,可以在这里找到。这个数据集是公开的,允许公开使用(可以通过sklearn加载到 Python 中)。

iris = pd.read_csv("https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data")

# Preprocess the data
X = iris.iloc[:, :-1].values
y = iris.iloc[:, -1].values

# Encode the categorical output labels
encoder = LabelEncoder()
y = encoder.fit_transform(y)

# Split the data into training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)

接下来,我们创建四个模型。这些将是相当简单的模型。每个模型将有一层 8 个神经元和一个激活函数。最终层将有 3 个神经元并使用 Softmax 激活函数。

#Model 1 (Sigmoid)

#Model 1
model = Sequential()
model.add(layers.Dense(8, input_dim=4, activation='sigmoid'))
model.add(layers.Dense(3, activation='softmax'))

#Model 2 (Tanh)
model = Sequential()
model.add(layers.Dense(8, input_dim=4, activation='tanh'))
model.add(layers.Dense(3, activation='softmax'))

#Model 3 (ReLU)
model = Sequential()
model.add(layers.Dense(8, input_dim=4, activation='relu'))
model.add(layers.Dense(3, activation='softmax'))

#Model 4 (Elliot)
model = Sequential()
model.add(layers.Dense(8, input_dim=4, activation=elliot))
model.add(layers.Dense(3, activation='softmax'))

接下来,简单地训练模型并分析结果。

# Compile the model
model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

# Train the model (Pick the number of epochs)
model.fit(X_train, y_train, epochs=1, batch_size=10)

结果

结果实际上是……令人惊讶的。正如预期的那样,Elliot 激活函数生成的模型在性能上与采用 Sigmoid 和双曲正切激活函数的模型相似。

1 周期

  • Sigmoid @ 1: 准确率: 0.3109 | 损失: 2.0030

  • Elliot @ 1: 准确率: 0.3361 | 损失: 1.0866

在 1 个周期下,Elliot 激活函数模型的准确率比 Sigmoid 激活函数模型高 2.61%,并且损失量减少了近 100%。

10 周期

  • Sigmoid @ 10: 准确率: 0.3529 | 损失: 1.0932

  • Elliot @ 10: 准确率: 0.6891 | 损失: 0.9434

在 10 个周期下,使用 Elliot 激活函数的模型比使用 Sigmoid 激活函数的模型准确率提高了近 30%,同时损失较低。

100 周期

  • Sigmoid @ 100: 准确率: 0.9496 | 损失: 0.4596

  • Elliot @ 100: 准确率: 0.9580 | 损失: 0.5485

虽然 Sigmoid 模型的表现优于 Elliot 模型,但需要注意的是它们的表现几乎完全相同。

1000 周期

  • Sigmoid @ 1000: 准确率: 0.9832 | 损失: 0.0584

  • Elliot @ 1000: 准确率: 0.9832 | 损失: 0.0433

在 1000 个周期下,两种不同模型的表现几乎完全相同。

总体而言,使用 Elliot 激活函数的模型表现略优于使用 Sigmoid 激活函数的模型。

Elliot 与双曲正切

1 个周期

  • tanh @ 1: 准确率: 0.3361 | 损失: 1.1578

  • Elliot @ 1: 准确率: 0.3361 | 损失: 1.0866

在 1 个周期时,Elliot 激活函数模型与双曲正切激活函数模型的表现相同。我原本预期这些函数会产生相似的模型,因为它们都以类似的方式限制了传递到神经网络下一层的值。

10 个周期

  • tanh @ 10: 准确率: 0.3277 | 损失: 0.9981

  • Elliot @ 10: 准确率: 0.6891 | 损失: 0.9434

使用 Elliot 激活函数的模型明显优于使用双曲正切激活函数的模型,就像 Elliot 模型在与 Sigmoid 模型比较时在 10 个周期的表现一样。

100 个周期

  • tanh @ 100: 准确率: 0.9916 | 损失: 0.2325

  • Elliot @ 100: 准确率: 0.9580 | 损失: 0.5485

在 100 个周期时,双曲正切模型的表现明显优于 Elliot 模型。在更高的周期下,Elliot 激活函数似乎表现不如tanh激活函数,但让我们看看在 1000 个周期时它们的表现有何不同。

1000 个周期

  • tanh @ 1000: 准确率: 0.9748 | 损失: 0.0495

  • Elliot @ 1000: 准确率: 0.9832 | 损失: 0.0433

好吧,在 1000 个周期时,Elliot 激活函数模型略微超越了双曲正切激活函数模型。

总的来说,我认为双曲正切和 Elliot 激活函数模型在神经网络的层次中几乎表现相同。训练模型所需的时间可能会有所不同,然而这些模型非常简单,随着数据量的增加以及网络规模的扩大,时间可能会成为一个更重要的因素。

Elliot 与 ReLU

1 个周期

  • ReLU @ 1: 准确率: 0.6639 | 损失: 1.0221

  • Elliot @ 1: 准确率: 0.3361 | 损失: 1.0866

在 1 个周期时,ReLU 激活函数模型的表现更好,这表明 Elliot 激活函数使模型训练速度较慢。

10 个周期

  • ReLU @ 10: 准确率: 0.6471 | 损失: 0.9413

  • Elliot @ 10: 准确率: 0.6891 | 损失: 0.9434

哇!包含 Elliot 激活函数的模型实际上表现更好,准确率高出 4.2%,损失低了 0.21%。

100 个周期

  • ReLU @ 100: 准确率: 0.9160 | 损失: 0.4749

  • Elliot @ 100: 准确率: 0.9580 | 损失: 0.5485

尽管采用 Elliot 激活函数的模型损失较高,但它能够实现 4.2%的更高准确率。这再次展示了 Elliot 激活函数在神经网络中的强大优势。

1000 个周期

  • ReLU @ 1000: 准确率: 0.9916 | 损失: 0.0494

  • Elliot @ 1000: 准确率: 0.9832 | 损失: 0.0433

尽管采用 Elliot 激活函数的模型在准确性方面表现不佳,但损失较低,我对结果仍然感到满意。如 1000 次迭代所示,Elliot 激活函数几乎与 ReLU 激活函数一样好,并且在正确的问题和超参数调整下,Elliot 激活函数可能是更优的选择。

结论

今天,我们探讨了一种较不为人知的激活函数:Elliot 激活函数。为了测试它的性能,我们将其与形状相似的两个激活函数进行比较:Sigmoid 和双曲正切激活函数。结果显示,Elliot 函数的表现与这两个函数相当,甚至更好。接下来,我们将 Elliot 激活函数的性能与现代神经网络中使用的标准 ReLU 激活函数进行了比较。在四次试验中,采用 Elliot 激活函数的模型有 50% 的时间表现更好。在其他试验中,尽管表现不如前者,但其性能与使用 ReLU 激活函数的模型几乎一致。我建议在下一个神经网络中尝试 Elliot 激活函数,因为它可能表现更佳!

如果你喜欢今天的阅读,请关注我,并告诉我是否有其他话题你希望我深入探讨!如果你没有 Medium 账户,可以通过我的链接 这里注册!使用我的链接我将获得少量佣金。此外,也可以在 LinkedIn 添加我,或者随时联系我!感谢阅读!

  1. Dubey, Shiv Ram, Satish Kumar Singh, 和 Bidyut Baran Chaudhuri. “深度学习中激活函数的全面调查与性能分析。” arXiv 预印本 arXiv:2109.14545 (2021)。

  2. Sharma, Sagar, Simone Sharma, 和 Anidhya Athaiya. “神经网络中的激活函数。” towards data science 6.12 (2017): 310–316。

  3. www.gallamine.com/2013/01/a-sigmoid-function-without-exponential_31.html

嵌入技术:ChatGPT 的秘密武器

原文:towardsdatascience.com/embeddings-chatgpts-secret-weapon-1870e590f32c

嵌入技术,以及它们如何帮助 ChatGPT 预测下一个词

Emma BoudreauTowards Data Science Emma Boudreau

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

--

(图像由作者提供)

变换器注意力机制

如果你最近经常浏览网页或阅读技术新闻,你可能在某个时候听说过或读到过 ChatGPT。ChatGPT 是 OpenAI 的新语言变换器模型,就这些模型而言,这个模型相当准确,产生了一些非常引人注目的——有时甚至是病毒式的——结果。在这个上下文中,变换器指的是一种采用自注意力机制的机器学习模型。自注意力机制是一个数据科学术语,简单来说就是这个模型试图模拟人类的认知功能或人类的认知注意力。这个模型的语言部分也很重要;它描述了变换器希望预测的内容,即人类语言。这通常被称为自然语言处理,或NLP,虽然 NLP 通常指的是对语言数据的处理,以将其转换为计算机或神经网络可以理解的数值权重。

变换器具有一些显著的特性,使得它们的定义在讨论语言变换器如 ChatGPT 时特别重要。变换器以及更广泛的注意力模型,拥有一些特征,这些特征并非每种机器学习模型所特有。在正常情况下,机器学习模型适配某些数据并生成权重;我们可以把这看作编程语言编译可执行文件的过程:一旦可执行文件编译完成,它变得静态不可变——无法更改,我们无法从内部调整代码。另一方面,变换器具有软权重,这更像是使用动态类型编程语言的 REPL,模型权重是可变的,并且可以在运行时进行更改。这是许多不同且有用的模型类型的基础,如长短期记忆LSTM)模型,以及我们今天讨论的变换器。

这张图展示了变换器模型的架构。 (图片来自维基共享资源)

embeddings

这引出了 embeddings,它们为这些软权重提供了更多的力量和能力。Embeddings 通过创建一个维度低于实际编码稀疏向量的新的维度层来工作。这可以被认为是对这些数据的一种分组,这在模型的最终计算中起到作用。实际上,embeddings 是一个低维空间,为更大的高维向量提供了节奏。我们几乎是在模型中加入了一个新的特征,我们的模型可能将其作为分类来推断数据中的更多细节——这在解释诸如人类语言这样复杂的事物时尤为重要。在这种情况下,维度指的是数据的维度或形状。理解 embeddings 的一种很好的方式是把它想象成一个飞镖靶。

在一个房间里,我们有一个飞镖板。我们希望利用这个飞镖板来预测给定房间内住客的技能水平。我们发现,非常差的飞镖手往往击中飞镖板的底部,而技能稍好的人往往击中顶部,非常有技巧的人则击中中间。实际击中位置将是我们的原始特征,我们将为其构建权重和概率。然而,如果我们给这些区域标记,并将它们与人的技能水平关联,以对这些数据做出更普遍的推断,这可能有助于我们在该上下文中做出更细致的预测,这就是嵌入的概念。嵌入成为一个点,数据本身,在确定其在这个低维空间中与其他嵌入的相似度的轴上。我们可以使用这来预测例如一个飞镖手已经玩了多少年。

一个简化的示意图,展示了这在你的模型中的样子。(图片由作者提供)

从本质上讲,嵌入告诉我们关于一组数据的信息是,这组数据中的数据与该嵌入中的其他数据相似。嵌入本质上只是其他数据上的分类数据。另一个需要注意的点是,这些分类也可以是多维的,意味着可以有多个嵌入,并且权重可能绑定到相同的嵌入上。嵌入也可以从数据中学习,这意味着这可以是神经网络的一部分,而不会添加很多内容,使它们成为许多应用中的简单选择,例如 ChatGPT 等变压器。

OpenAI 有他们自己的嵌入端点,这使得执行自然语言任务、主题建模、分类,甚至聚类变得非常简单。如果你想了解更多关于 OpenAI 嵌入的内容,下面是一个详细讨论此主题的论文链接:

[## 文本和代码嵌入的对比预训练

文本嵌入在许多应用中都是有用的特征,例如语义搜索和计算文本相似性……

arxiv.org](https://arxiv.org/abs/2201.10005?source=post_page-----1870e590f32c--------------------------------)

很容易想象这些嵌入在处理文本和创建准确的语言模型预测中所扮演的角色。词语确实可以被分类,当你考虑到英语词汇的宏观图景时,理解词语实际作用可能会非常困难。然而,如果我们将这些词语分解为类别:冠词、名词、动词,那么了解语法如何在我们的语言中实际运作就会容易得多。对于语言模型来说,词嵌入的目标是捕捉词语在向量表示中的意义。我们为词语的意义创建一个通用分类,然后利用我们对该类别的了解来对输出做出推断。此外,ChatGPT 用于表示文本的粒度是子词,而不是整个词。因此,ChatGPT 利用这些类型的嵌入来对词语的某些部分进行分类和描述。

结束

嵌入是许多不同类型机器学习模型的核心概念:推荐算法、语言转换器,甚至分类模型都是从拥有嵌入层中受益的例子。OpenAI 的嵌入实现帮助 ChatGPT 模型根据类别及其与这些类别的数值关系来解释词语,这比尝试从每个单独词语中找出见解要容易得多。如果你想了解更多关于嵌入的内容,并且如何将它们应用到自己的 Tensorflow 网络中,我强烈建议你查看 Google 几年前发布的嵌入教程,这里有一个链接:

[## 嵌入 | 机器学习 | Google 开发者

嵌入是一个相对低维的空间,你可以将高维向量转换到其中。嵌入…

developers.google.com](https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture?source=post_page-----1870e590f32c--------------------------------)

感谢阅读!

嵌入 + 知识图谱:RAG 系统的终极工具

原文:towardsdatascience.com/embeddings-knowledge-graphs-the-ultimate-tools-for-rag-systems-cbbcca29f0fd

Anthony AlcarazTowards Data Science Anthony Alcaraz

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

--

人工智能软件被用来增强本文文本的语法、流畅性和可读性。

大型语言模型(LLM)的出现,经过大量文本数据的训练,是自然语言处理领域最重要的突破之一。这些模型仅凭简短的提示生成流畅且连贯的文本,开辟了对话 AI、创意写作和广泛其他应用的新可能性。

然而,尽管它们表现出色,LLM 仍然有一些关键限制。它们的知识仅限于从训练数据中辨别出的模式,这意味着它们缺乏对世界的真正理解。

他们的推理能力也有限——他们无法进行逻辑推断或从多个来源综合事实。当我们提出更复杂、开放性的问题时,回答开始变得毫无意义或自相矛盾。

为了弥补这些不足,检索增强生成(RAG)系统引起了越来越多的关注。关键思想是从外部来源检索相关知识,为 LLM 提供背景,以做出更有信息量的回答。

目前大多数现有系统通过向量嵌入的语义相似性检索段落。然而,这种方法有其自身的缺陷,如缺乏真正的相关性、无法聚合事实以及没有推理链。

这就是知识图谱发挥作用的地方。知识图谱是对现实世界实体和关系的结构化表示。它们通过编码上下文事实之间的相互连接,克服了纯向量搜索的不足。遍历知识图谱能够在多样的信息源之间进行复杂的多跳推理。

在这篇文章中,我们深入探讨了如何将向量嵌入与知识图谱结合,以解锁 LLM 推理、准确性和解释能力的新层次。这种结合提供了表面语义与结构化知识和逻辑的完美融合。

像我们的思维一样,LLM 需要统计学习和符号表示的结合。

我们首先探讨了单独依赖向量搜索的固有弱点。

然后我们阐明了知识图谱和嵌入如何互相补充,单独使用任何一种技术都不够充分。

原始向量搜索的局限性

## 向量搜索并非你所需的一切

介绍

towardsdatascience.com

大多数 RAG 系统依赖于对文档集合中的段落进行向量搜索,以找到对 LLM 相关的上下文。此过程包括几个关键步骤:

  1. 文本编码: 系统使用像 BERT 这样的嵌入模型将语料库中的文本段落编码成向量表示。每个段落都被压缩成一个密集的向量,捕捉语义意义。

  2. 索引: 这些段落向量在高维向量空间中进行索引,以实现快速相似性搜索。常用的方法包括 ANNOY、Faiss 和 Pinecone。

  3. 查询编码: 当用户查询到达时,它也会使用相同的嵌入模型编码成向量表示。

  4. 相似性检索: 通过对索引的段落进行相似性搜索,找到那些与查询向量基于距离度量(如余弦相似性)最接近的段落。

  5. 段落返回: 返回最相似的段落向量,并提取原始文本以提供 LLM 的上下文。

该管道存在几个关键限制:

  • 段落向量可能未能完全捕捉查询的语义意图。重要的上下文可能被忽视,因为嵌入未能表示某些推理连接。

  • 在将整个段落压缩为单一向量时,细微之处会丢失。分布在句子中的关键相关细节被掩盖。

  • 匹配是针对每个段落独立进行的。没有跨不同段落进行联合分析以连接事实和推导需要聚合的答案。

  • 排名和匹配过程不透明,无法提供为何某些段落被认为更相关的透明度。

  • 只编码了语义相似性,没有表示内容之间关系、结构、规则和其他多样化连接的表示。

仅关注语义向量相似性会导致检索结果缺乏真实理解。随着查询变得更加复杂,这些局限性在无法跨检索内容进行推理时变得愈加明显。

融入知识图谱

知识图谱将信息表示为一个互联的实体和关系网络,能够在内容中进行更复杂的推理。

下面是它们如何增强检索:

  1. 显式事实 — 事实作为节点和边直接捕获,而不是压缩成不透明的向量。这保留了关键细节。

  2. 上下文细节 — 实体包含丰富的属性,如描述、别名和元数据,提供了关键的上下文。

  3. 网络结构 — 关系建模实体之间的现实世界连接,捕捉规则、层级、时间线等。

  4. 多跳推理 — 查询可以遍历关系以连接来自不同来源的事实。可以推导出需要多步骤推理的答案。

  5. 联合推理 — 实体解析将对同一现实世界对象的引用连接起来,允许集体分析。

  6. 可解释的相关性 — 图的拓扑结构提供了为什么某些事实基于其连接而相关的透明度。

  7. 个性化 — 捕捉用户属性、上下文和历史交互,以定制结果。

知识图谱使得图遍历过程成为可能,以收集与查询相关的相互连接的上下文事实,而不是孤立的匹配。根据拓扑结构可以实现可解释的排名。丰富的知识表示能力促进了更复杂的推理。

知识图谱通过编码结构化事实、关系和上下文来增强检索,从而实现精准的多步骤推理。与纯向量搜索相比,这提供了更大的相关性和解释力。

结合知识图谱与嵌入和约束

## 提升知识图谱嵌入使用简单约束

将知识图谱(KGs)嵌入到连续向量空间是当前研究的重点。早期的研究进行了…

arxiv.org

知识图谱将实体和关系表示为向量嵌入,以进行数学操作。额外的约束可以使表示更加优化:

  1. 非负约束 — 将实体嵌入限制在 0 到 1 之间的正值,诱导稀疏性。这仅显式地建模它们的正面属性,并提高可解释性。

  2. 蕴含约束 — 通过将预期的逻辑规则如对称性、反转、组合直接编码为关系嵌入的约束,强制这些模式。

  3. 置信度建模 — 通过松弛变量的软约束可以编码基于证据的逻辑规则的不同置信水平。

  4. 正则化 — 约束施加有用的归纳偏置,而不会使优化显著变得更加复杂。只增加一个投影步骤。

  5. 可解释性 — 结构化的约束提供了对模型学习到的模式的透明度。这解释了推理过程。

  6. 准确性 — 约束通过减少假设空间到符合的表示来提高泛化能力。这提高了对未见查询的准确性。

添加简单但通用的约束可以增强知识图谱嵌入,生成更优化、可解释且符合逻辑的表示。这些嵌入获得了模仿现实世界结构和规则的归纳偏差,从而带来更准确且易于解释的推理,而不增加额外的复杂性。

整合多样化的推理框架

## Papers with Code - Graph Agent: Explicit Reasoning Agent for Graphs

尚无可用代码。

paperswithcode.com

知识图谱需要推理来推导新事实、回答查询和进行预测。不同的技术具有互补的优势:

  1. 逻辑规则 — 将知识表达为逻辑公理和本体。通过定理证明进行可靠和完整的推理。处理不确定性的能力有限。

  2. 图谱嵌入 — 嵌入知识图谱结构以进行向量空间操作。处理不确定性,但缺乏表达能力。

  3. 神经证明器 — 可微分的定理证明模块与向量查找结合。自适应但推理不透明。

  4. 规则学习器 — 通过对图结构和数据的统计分析来引导规则。自动化规则创建但质量不确定。

  5. 混合管道 — 逻辑规则编码明确的约束。嵌入提供向量空间操作。神经证明器通过联合训练融合了这些优势。

  6. 可解释建模 — 使用基于案例、模糊或概率逻辑来增加透明度。表达规则中的不确定性和信心。

  7. 迭代增强 — 通过将推断出的事实和学习到的规则重新呈现到图谱中来扩展知识。提供反馈回路。

关键在于识别所需的推理类型并将其映射到适当的技术上。一个结合逻辑形式、向量表示和神经组件的可组合管道提供了鲁棒性和可解释性。

保持信息流向 LLM

从知识图谱中检索事实以供 LLM 使用会引入信息瓶颈。精心设计保持相关性:

  1. 分块 — 将内容拆分成小块可以提高隔离性,但会丧失周围的上下文。这阻碍了跨块的推理。

  2. 总结 — 生成内容块的摘要提供了更简洁的上下文。关键细节被浓缩以突出其重要性。

  3. 元数据 — 附加摘要、标题、标签等作为元数据以维护源内容的上下文。

  4. 查询重写 — 将原始查询重写为更详细的版本,提供更符合 LLM 需求的检索结果。

  5. 关系建模 — 知识图谱遍历保持事实之间的连接,维持上下文。

  6. 信息排序 — 按时间顺序或相关性排序事实,优化 LLM 的信息结构。

  7. 显式陈述 — 将隐含知识转换为为 LLM 陈述的显式事实,使推理更容易。

目标是优化检索知识的相关性、上下文、结构和明确性,以最大化推理能力。需要在粒度和连贯性之间取得平衡。知识图谱关系有助于将孤立的事实进行上下文化。

解锁推理能力

知识图谱和嵌入各有优点,相互结合可以弥补对方的不足:

  1. 知识图谱 — 提供实体和关系的结构化表示。通过图遍历赋能复杂推理。处理多跳推理。

  2. 嵌入 — 在向量空间中编码信息以进行基于相似性的操作。实现大规模高效的近似搜索。揭示潜在模式。

  3. 联合编码 — 为知识图谱中的实体和关系生成嵌入。这提炼了统计模式。

  4. 神经网络 — 图神经网络通过可微分的信息传递在图结构和嵌入元素上操作。这融合了优势。

  5. 推理流程 — 知识图谱遍历首先收集结构化知识。然后嵌入关注搜索并大规模检索相关内容。

  6. 可解释性 — 显式的知识图谱关系为推理过程提供了可解释性。嵌入增强了解释能力。

  7. 迭代改进 — 推断的知识可以扩展图。GNNs 提供持续的表示学习。

这种合作关系使结构化知识表示和推理得到了模式识别能力和神经网络的可扩展性的增强。这是推动语言 AI 的关键,需要统计学习和符号逻辑的结合。

通过协同过滤改进搜索

协同过滤利用实体之间的连接来增强搜索:

  1. 知识图谱 — 构建一个知识图谱,其中节点表示实体,边表示关系。

  2. 节点嵌入 — 为特定的关键节点属性(如标题、描述等)生成嵌入向量。

  3. 向量索引 — 在节点嵌入上构建向量相似性索引。

  4. 相似性搜索 — 对于搜索查询,找到具有最相似嵌入的节点。

  5. 协作调整 — 基于节点的连接,使用诸如 PageRank 的算法传播和调整相似性评分。

  6. 边权重 — 根据边的类型、强度、置信度等调整权重。

  7. 评分归一化 — 对调整后的评分进行归一化,以保持相对排名。

  8. 结果重新排序 — 基于调整后的协同评分重新排序初始结果。

  9. 用户背景 — 根据用户资料、历史和偏好进一步调整。

用飞轮学习推动知识图谱

[## 驱动 RAG 引擎:数据飞轮

构建一个高效的检索增强生成(RAG)系统,并不断改进,需要实施…

ai.plainenglish.io

知识图谱通过提供结构化的世界知识,为语言模型解锁了新的推理能力。然而,构建高质量的图谱仍然具有挑战性。这就是飞轮学习的作用所在——通过分析系统交互持续改进知识图谱。

知识图谱飞轮

  1. 仪器化 — 记录所有系统查询、响应、评分、用户行为等。提供对知识图谱使用情况的可见性。

  2. 分析 — 汇总使用数据以显现不佳的响应。对这些响应进行聚类和分析,以识别表明知识缺口的模式。

  3. 策划 — 手动审查问题响应,并追踪问题到图谱中的缺失或错误的事实。

  4. 修正 — 直接修改图谱,添加缺失的事实、改进结构、提高清晰度等。修复潜在的数据问题。

  5. 迭代 — 不断循环上述步骤。每次迭代进一步增强知识图谱。

流数据摄取

  • 流媒体数据源,如新闻和社交媒体,提供了持续的信息流,以保持知识图谱的时效性。

  • 专用基础设施处理高容量的数据摄取。

主动学习

  • 利用查询生成识别并填补关键的知识空白,超越流媒体提供的信息。

  • 发现图谱中的漏洞,提出问题,检索缺失的事实,并将其添加。

飞轮效应

每次循环,知识图谱通过分析使用模式和修正数据问题逐步增强。改进的图谱提升了系统性能。

这个飞轮过程使知识图谱和语言模型能够基于来自现实世界使用的反馈共同演化。图谱会根据模型的需求进行积极调整。

总结来说,飞轮学习为知识图谱提供了一个持续、自动化改进的框架,通过分析系统交互来实现。这推动了依赖于图谱的语言模型的准确性、相关性和适应性。

结论:

要达到人类水平的智能,语言 AI 需要结合外部知识和推理。这正是知识图谱发挥作用的地方。知识图谱提供了现实世界实体和关系的结构化表示,编码了关于世界的事实及其之间的连接。这允许通过遍历互联的事实进行复杂的逻辑推理。

然而,知识图谱有其自身的局限性,如稀疏性和缺乏不确定性处理能力。在这里,图嵌入发挥作用——通过将知识图谱元素编码到向量空间中,嵌入允许从大规模语料库中进行统计学习,以揭示潜在模式。它们还支持基于相似性的高效操作。

无论是知识图谱还是嵌入,单独使用都不足以实现类似人类的语言智能。但将它们结合起来,可以提供结构化知识表示、逻辑推理和统计学习的完美融合。知识图谱在神经网络的模式识别能力之上叠加了符号逻辑和关系。

像图神经网络这样的技术通过在图结构和嵌入上进行可微分的消息传递进一步统一了这些方法。这种共生关系使系统能够利用统计学习和符号逻辑的优势——结合了神经网络和结构化知识表示的优点。

这种合作关系为下一代 AI 提供了基础构件,这种 AI 超越了单纯的口才,达到了真正的理解——能够理解上下文和历史的对话代理,能够识别细微偏好的推荐引擎,通过连接事实来合成答案的搜索系统。

构建高质量知识图谱、基准测试、噪声处理等仍然面临挑战。但跨符号和神经方法的混合技术前景光明。随着知识图谱和语言模型的不断进步,它们的整合将开启可解释、智能语言 AI 的新领域。

这张图像是使用 AI 图像生成模型创建的。

迎接自动化重新训练

原文:towardsdatascience.com/embracing-automated-retraining-780ed49f9985?source=collection_archive---------4-----------------------#2023-03-17

作者提供的图片

如何从固定节奏(或完全不)重新训练转向动态方法

Claire LongoTowards Data Science Claire Longo

·

关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 3 月 17 日

--

这篇文章由 Trevor LaViale 联合撰写

尽管行业在了解何时将模型投入生产方面投资了大量的过程和技术,但关于同样重要的任务——了解何时重新训练模型的集体知识却相对较少。事实上,了解何时重新训练模型很困难,因为存在诸如反馈延迟或实时预测标签等因素。在实际操作中,许多模型在生产中没有进行任何重新训练,使用手动重新训练方法,或是在没有优化或研究节奏的情况下进行重新训练。

本文旨在帮助数据科学家和机器学习工程团队采用自动化重新训练。

重新训练的方法

自动化模型重新训练有两种核心方法:

  • 固定: 按设定的周期(例如,每天、每周、每月)进行重新训练。

  • 动态: 基于模型性能指标的临时触发重新训练。

虽然固定方法实现起来很简单,但也存在一些缺点。计算成本可能比实际需要的要高,频繁的重新训练可能导致模型之间的不一致,而不频繁的重新训练计划可能导致模型过时。

动态方法可以防止模型过时,并优化计算成本。虽然有许多重新训练的方法,但以下是一些推荐的最佳实践,用于动态模型重新训练,以保持模型的健康和高效。

通用重新训练架构

有一系列工具可以用来创建模型重新训练系统。此图示展示了一个 ML 可观测性平台如何集成到一个通用流程中。

针对特定工具的教程非常丰富。以下是一些示例:

对于那些准备跳过的,可以进一步了解一下 Etsy 的见解关于有状态模型重新训练的内容。

重新训练策略

自动化重新训练一个实时机器学习模型可能是一个复杂的任务,但有一些最佳实践可以帮助指导设计。

触发重新训练的指标

触发重新训练的指标将取决于具体的模型和使用案例。每个指标都需要设定一个阈值。该阈值将用于在模型的性能低于阈值时触发重新训练。在这方面,监控工具可以发挥作用。当模型监控平台中的性能监控器触发时,你可以通过编程查询性能和漂移指标,以评估是否需要重新训练。

理想的模型重新训练触发指标:

  • 预测(分数或标签)漂移

  • 性能指标降级

  • 针对特定细分群体/队列的性能指标降级。

  • 特征漂移

  • 嵌入漂移

漂移是度量两个分布之间距离的指标。它是触发模型重新训练的有意义的指标,因为它表明你的生产数据从基线数据有多少偏移。统计漂移可以通过各种漂移指标来测量。

用于计算漂移的基准数据集可以来源于训练数据集或生产数据的一个窗口。

确保新模型有效

新模型需要在推广到生产环境以替换旧模型之前进行测试或验证。这里有几种推荐的方法:

  • 人工审查

  • CI/CD 管道中的自动化指标检查

推广新模型的策略

推广新模型的策略将取决于模型对业务的影响。在某些情况下,可能适合自动用新模型替换旧模型。但在其他情况下,新模型可能需要进行 A/B 测试后再替换旧模型。

一些值得考虑的实时模型测试策略包括:

  • 冠军模型与挑战者模型 — 向这两个模型提供生产流量,但在应用中仅使用现有模型(冠军)的预测/响应。挑战者模型的数据将被存储以供分析,但不会被使用。

  • A/B 测试 — 将生产流量分配到两个模型中,进行固定时间段的实验。实验结束时比较关键指标,并决定推广哪个模型。

  • 金丝雀发布 — 先将小比例的生产流量重定向到新模型。由于它在生产路径中,这有助于发现新模型的实际问题,但将影响限制在小比例用户中。逐步增加流量直到新模型接收 100%的流量。

重新训练反馈循环数据

一旦确定模型需要保留,下一步是选择合适的数据集进行重新训练。以下是一些建议,以确保新的训练数据将提升模型性能:

  • 如果模型总体表现良好,但在某些特定的特征值或人口统计特征等细分段上未能达到最佳性能标准,则新的训练数据集应包含这些表现较差的细分段的额外数据点。可以使用简单的上采样策略来创建一个新的训练数据集,以针对这些低表现的细分段。

  • 如果模型在小时间片上训练,训练数据集可能无法准确捕捉和表示所有可能出现在实际生产数据中的模式。为防止这种情况,避免仅在最近数据上训练模型。相反,应使用大量的历史数据,并用最新的数据来增强这些数据,以便模型学习到额外的模式。

  • 如果您的模型架构遵循迁移学习设计,则在重新训练期间可以简单地将新数据添加到模型中,而不会丢失模型从之前训练数据中已经学到的模式。

来自模型监控平台的仪表板(例如 Arize——完全披露:我在 Arize 工作)非常适合在这些测试期间跟踪和比较模型的实时性能。无论模型是作为影子部署、实时 A/B 测试还是仅仅是离线比较,这些仪表板都提供了一种简单的方式来查看并排模型比较。这些仪表板还可以轻松地与他人分享,以向利益相关者展示模型性能的改善。

定量 ROI

总体而言,确定自动重新训练实时机器学习模型的最佳方法时,了解业务需求和解决的问题至关重要。同样,持续监控模型性能,并根据需要调整重新训练的频率和指标也很重要。

成本影响测量:

虽然在 AI 领域计算直接 ROI 是具有挑战性的,但优化模型重新训练的价值简单、直观且可以直接计算。模型训练作业的计算和存储成本通常已经作为云计算成本的一部分进行跟踪。通常,模型的商业影响也可以计算。

在优化重新训练时,我们考虑了重新训练成本和模型性能对业务的影响(“AI ROI”)。我们可以权衡这些成本以证明模型重新训练的成本。

在这里,我们提出了每周成本计算,尽管该计算器可以根据模型的目的和维护需求调整为不同的频率,如每日或每月。

图片由作者提供

考虑场景 1,即模型过于频繁地重新训练的情况。

我的模型重新训练成本为$200。我每天训练一次模型。该模型保持了稳定的每周平均准确率为 85%。我设置了一个管道,基于预测分数漂移大于 0.25 PSI 和准确率自动重新训练。根据新规则,我的模型每周只重新训练两次,并保持 85%的准确率。

每周维护成本比较:

旧模型维护成本:7*$200 = $1400

新模型维护成本 2*$200= $400

这相当于模型维护成本减少了 x%。虽然这是一个简单的假设示例,但成本节约的幅度可能会达到这种规模。

考虑场景 2,即模型训练不足的情况。

我的模型训练成本为$200。我每周训练一次模型。该模型保持了稳定的每周平均准确率为 65%。我设置了一个管道,基于预测分数漂移大于 0.25 PSI 自动重新训练。根据新规则,我的模型每周重新训练两次,达到了更好的 85%准确率。

每周维护成本比较:

旧模型维护成本:1*$200 = $200,准确率 65%

新模型维护成本:2*$200= $400,准确率 85%

因此,通过更高的价格,取得了更好的模型性能。如果 AI 投资回报值高于重新训练成本,那么这可以被证明是合理且有利可图的。缺乏频繁的重新训练可能会导致错失收益。

结论

从固定间隔的模型重新训练过渡到由模型性能触发的自动化模型重新训练,为组织提供了众多好处,包括在云成本不断增加的情况下降低计算成本,以及通过改善模型性能实现更好的 AI 投资回报。希望这篇博客为团队提供了一个行动模板。

Embracing Julia: An Invitation Letter

原文:towardsdatascience.com/embracing-julia-an-invitation-letter-332f5709378e?source=collection_archive---------2-----------------------#2023-10-13

诚挚地向 Python 爱好者、科学计算大师和数据科学家致敬

Essam WisamTowards Data Science Essam Wisam

·

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

--

Julia 是一种通用、动态、高性能和高级别的编程语言,采用即时编译。这是一门相对较新的语言,其主要 1.0 版本直到 2018 年才发布。在这篇文章中,我们旨在展示,如果你对数据科学、科学计算或者是 Python 爱好者,那么将这门语言加入你的工具箱绝对是值得的。也许这确实是你见过的最美的编程语言

紫色、绿色和红色行星的星系数字艺术 — 由作者使用 DALLE 2 生成

在这个故事中,我们将探讨 Julia 的思想高度及其学习的价值。完成后,我们强烈推荐你查看下一个故事 从 Python 到 Julia:终极指南,以便从 Python 轻松过渡到 Julia。

目录

· Julia 是高级语言

∘ 基本语法

∘ 数学的优雅语法

· Julia 很快

∘ 基准测试

∘ 双语言问题

∘ Julia 是即时编译的

· Julia 解决了表达式问题

∘ 表达式问题

∘ 多重分发

∘ 抽象和具体类型

· Julia 功能全面

∘ 数组支持

∘ 字符串支持

∘ 多线程

∘ 与 C 代码的简单集成

∘ 标准库

· Julia 是通用的

∘ 介绍

∘ 自动化和脚本编写

· Julia 可扩展性广

∘ 介绍

∘ 宏

· 总结

照片由 Daniele Levis Pelusi 提供,来源于 Unsplash

Julia 是高级语言

介绍可能已经让你觉得这将类似于 Python——也是一种通用、动态和高级语言。为了验证这一点,我们来看看基本的 Julia 代码与 Python 的比较。

基本语法

考虑一下下面这个用 Python 编写的猜谜游戏:

import random

def guessing_game(max):
    random_number = random.randint(1, max)
    print(f"Guess a number between 1 and {max}")
    while True:
        user_input = input()
        guess = int(user_input)
        if guess < random_number:
            print("Too low")
        elif guess > random_number:
            print("Too high")
        else:
            print("That's right!")
            break

guessing_game(100)

以下是 Julia 中的等效代码:

function guessing_game(max::Integer)
    random_number = rand(1:100)  
    println("Guess a number between 1 and $max")
    while true
        user_input::String = readline()
        guess = parse(Int, user_input)
        if guess < random_number
            println("Too low")
        elseif guess > random_number
            println("Too high")
        else
            println("That's right!")
            break
        end
    end
end

guessing_game(100)

主要区别在于 Julia 不假设任何缩进或要求使用冒号,而是需要显式的“end”来结束 if 条件、循环和函数等构造的作用域。如果你来自 Matlab 或 Fortran,你应该会觉得很熟悉。

另一个你可能注意到的区别是 Julia 自然支持变量声明、函数参数(和返回类型,尽管很少使用)中的类型注解。它们总是可选的,但通常用于类型断言,允许编译器在方法重载多个类型时选择正确的方法实例,在某些情况下对于变量和结构体声明也有性能优势。

数学的优雅语法

# Elegant Expressions 
x = 2
z = 2y + 3x - 5

# Official Unicode Support
α, β, γ = 1, 2, π/2

# one-line functions
f(r) = π*r²

f'(3)  # derivative (with Flux.jl package)

# Column vector is literally a column
v₁ = [1
      2
      3
      4]  

v₂ = [1 2 3 4]

# transpose
println(v1' == v2)

# This is literally a 3x3 matrix
M⁽ⁱ⁾ = [1 2 3
        4 5 7
        7 8 9]

# Explicit modeling of missingness
X = [1, 2, missing, 3, missing]

Julia 相对于 Python 的一个重大优势是其对数学语法的支持。* 乘以常量时无需使用 latex 符号,支持变量名称的 latex 符号(可能需要使用 VSCode 插件将 \pi 转换为 π,v_1 转换为 v₁ 等),矩阵一般遵循代码定义中的布局。

例如,如果你要为神经网络实现梯度下降。

在 Python 中,你可能会写:

import numpy as np

# Gradient Descent in a Neural Network
J_del_B_n = [np.zeros(b) for b in B_n]
J_del_W_n = [np.zeros(W) for W in W_n]

for (x, y) in zip(x_batch, y_batch):
    J_del_B_n_s, J_del_W_n_s = backprop(x, y)
    J_del_B_n = [J_del_b + J_del_b_s for J_del_b,
                 J_del_b_s in zip(J_del_B_n, J_del_B_n_s)]
    J_del_W_n = [J_del_W + J_del_W_s for J_del_W,
                 J_del_W_s in zip(J_del_W_n, J_del_W_n_s)]

d = len(x_batch)
W_n = [(1 - lambda_val * alpha / d) * W - lambda_val /
       d * J_del_W for W, J_del_W in zip(W_n, J_del_W_n)]
B_n = [(1 - lambda_val * alpha / d) * b - lambda_val /
       d * J_del_b for b, J_del_b in zip(B_n, J_del_B_n)]

比较一下你用 Julia 编写的代码的可读性:

# Gradient Descent in a NN
მJⳆმBₙ = [zeros(b) for b in Bₙ]
მJⳆმWₙ = [zeros(W) for W in Wₙ]

for (x, y) in zip(x_batch, y_batch)
    მJⳆმBₙₛ, მJⳆმWₙₛ = backprop(x, y)
    მJⳆმBₙ = [მJⳆმb + მJⳆმbₛ for მJⳆმb, მJⳆმbₛ in zip(მJⳆმBₙ, მJⳆმBₙₛ)]  
    მJⳆმWₙ = [მJⳆმW + მJⳆმWₛ for მJⳆმW, მJⳆმWₛ in zip(მJⳆმWₙ, მJⳆმWₙₛ)]

d = len(x_batch)
Wₙ = [(1 - λ*α/d)* W - λ/d * მJⳆმW for W, მJⳆმW in zip(Wₙ, მJⳆმWₙ)]
Bₙ = [(1 - λ*α/d)* b - λ/d * მJⳆმb for b, მJⳆმb in zip(Bₙ, მJⳆმBₙ)]

你可以尝试用 Python 编写类似的代码,但编辑器通常会在 Unicode 变量周围加上黄色方块(或无法高亮显示它们),而且你的代码可能无法与诸如 Pickle 的第三方包兼容。

照片由Solaiman Hossen拍摄,来源于Unsplash

Julia 非常快速。

另一个使 Julia 被认为是 Python 梦想成真的主要原因是,与 Python、Ruby 和其他高级语言不同,它在保持高级的同时不会牺牲速度。实际上,它可以和低级语言如 C 和 C++ 一样快。

基准测试

作为参考,以下报告了 Julia 的性能,以及其他语言在流行性能基准测试中的表现:

Julia 微基准测试:图片来源于 JuliaLang 在 MIT 许可下

双语言问题

Julia 性能的一个推论是它解决了双语言问题:

  • 研究代码(例如,一个机器学习模型)通常用高级语言如 Python 编写,因为它的高级和交互性;因此,能更专注于科学(减少代码问题)并允许更多探索。

  • 一旦研究代码完成,它必须用低级语言如 C 重写,才能在生产中使用。

问题在于相同的代码必须用多种语言重写。这通常困难且容易出错;考虑到研究代码在发布后进行修改,最坏情况下,它将不得不再次用低级语言重写。

一种解决此问题的方法是将性能关键的库(例如,Numpy)用低级语言如 C 编写,然后可以用 Python 函数包装这些库,这些函数内部调用 C 代码,可以用于研究和生产而不必担心性能。在现实中,这种方法非常有限,因为:

  • 这使得新开发者很难为他们编写的新颖科学方法做出贡献或进行合作,因为他们可能需要将这些方法用低级语言如 C 重写以提高性能,然后再在高级库中公开。

  • 科学计算领域的高级语言可能会对开发人员施加一些搞笑的限制。例如,写显式的 for 循环可能会被强烈不鼓励。

Julia 通过保持高级、交互性强且非常快速,即使在生产环境中也能解决双语言问题。

Julia 是即时编译的。

有一小段关于 Julia 性能的说明。由于 Julia 是 JIT 编译的,任何 Julia 代码的第一次运行都需要更多时间来完成。在这段时间里,每个函数代码将被转换为特定变量类型的本地代码(即处理器可以解释的代码)。一旦完成,它将缓存编译后的表示,以便如果该函数再次被调用并且输入的类型相同,它将立即被解释。

更详细地说,对于一个有 N 个参数的函数,可能会有指数级数量的本地代码表示;每种可能的参数类型组合都有一个。Julia 会将函数编译成与首次运行代码时推断出的类型对应的表示。一旦完成,进一步调用该函数将变得轻而易举。请注意,在类型推断过程中,它不一定使用类型注释(这些注释是可选的,并且可以有我们提到的其他用途),类型可以从输入的运行时值中推断出来。

这不是问题,因为研究代码或在服务器上运行的代码只需初次编译一次,完成后任何进一步运行(实际 API 调用或进一步实验)的代码都非常快速。

Thom MilkovicUnsplash 提供的照片

Julia 解决了表达式问题

表达式问题

表达式问题涉及能够定义一个在其表示(即支持的类型)和行为(即支持的方法)方面都可扩展的数据抽象。也就是说,解决表达式问题的方案允许:

  • 添加新类型现有操作适用的类型中

  • 添加新操作现有类型适用的类型中

而不违反开闭原则(或导致其他问题)。这意味着应该可以在不修改现有操作代码的情况下添加新类型,并且应该可以在不修改现有类型代码的情况下添加新操作。

Python 像许多其他编程语言一样,是面向对象的,未能解决表达式问题。

假设我们有以下数据抽象:

# Base class
class Shape:
    def __init__(self, color):
        pass

    def area(self):
        pass

# Child class
class Circle(Shape):
    def __init__(self, radius):
        super().__init__()
        self.radius = radius

    def area(self):
        return 3.14 * self.radius * self.radius 

很容易将新类型添加到现有方法应适用的类型中。只需继承 Shape 基类即可。这不需要修改任何现有代码:

class Rectangle(Shape):
    def __init__(self, width, height):
        super().__init__()
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

同时,向现有类型中添加操作并不容易。如果我们想添加一个 perimeter 方法,那么必须修改基础类以及到目前为止实现的每一个子类。

这个问题的一个后果是,如果包x由作者 X 维护,并且最初支持操作集合Sx,而另一个操作集合Sy对另一组开发者 Y 有帮助,他们必须能够修改由 X 维护的包,以添加这些方法。在实践中,开发者 Y 通常会自己创建另一个包,可能会复制包x中的代码以实现该类型,因为开发者 X 可能不愿意维护更多的代码,而Sy可能是一种不同类型的方法,不需要存在于同一个包中。

另一方面,因为添加适用现有操作的新类型很容易,如果开发者 Y 希望定义一个新的类型,该类型实现了 X 实现的类型中的操作,他们可以很容易地做到这一点,而无需修改包x或复制其中的任何代码。只需导入该类型,然后从中继承即可。

多重分发

为了解决表达式问题,这允许不同包之间的大规模集成,Julia 完全摒弃了传统的面向对象编程。Julia 使用抽象类型定义、结构体(抽象类型的自定义类型实例)和方法以及一种称为多重分发的技术,正如我们将看到的,它完美地解决了表达式问题

要查看我们上面所讨论的等效内容:

### Shape Abstract Type (Interface)

abstract type Shape end

function area(self::Shape)  end

### Circle Type (Implements the Interface)

struct Circle <: Shape
    radius::Float64
end

function area(circle::Circle)
    return 3.14 * circle.radius²
end

在这里,我们定义了一个抽象类型Shape。它是抽象的,这意味着它不能被实例化;然而,其他类型(类)可以从它中继承(子类化)。之后,我们定义了一个圆形类型,作为Shape抽象类型的子类型,并定义了area方法,同时指定输入必须是Circle类型。这样我们可以进行

c = Circle(3.0)
println(area(c))

这将打印28.26。虽然c满足两个area定义,因为它也是一个Shape,但第二个定义更具体,因此编译器选择了它来调用。

类似于基于类的面向对象编程,添加另一个类型“rectangle”而不触碰现有代码是很容易的:

struct Rectangle <: Shape
    length::Float64
    width::Float64
end

function area(rect::Rectangle)
    return rect.length * rect.width
end

现在当我们执行

rect = Rectangle(3.0, 6.0)
println(area(rect))

我们得到18.0。这就是多重分发的作用;area方法的正确实例是根据运行时参数的类型动态分发的。如果你有 C 或 C++的背景,这一定会让你想起函数重载。不同之处在于函数重载不是动态的,它依赖于编译时发现的类型。因此,你可以设计一些示例,其中它的行为会有所不同。

更重要的是,与基于类的 OOP 不同,我们可以向 ShapeCircleRectangle 添加方法,而无需修改它们的文件。如果所有这些文件都在我的包中,而你希望添加一组方法来生成几何形状的动画和 3D 视觉效果(这是我不关心的),那么你只需导入我的包。现在你可以访问 ShapeCircleRectangle 类型,你可以编写新的函数,然后将它们导出到你自己的“ShapeVisuals”包中。

### Interface definitions
function animate(self::Shape)  end
function ThreeDify(self::Shape)  end

### Circle definitions
function animate(self::Circle)  
...
end
function ThreeDify(self::Circle)  
...
end

### Rectangle defintions
function animate(self::Rectangle)  
...
end
function ThreeDify(self::Rectangle)  
...
end

你考虑一下,这与你知道的面向对象编程 (OOP) 的主要区别在于它遵循 func(obj, args) 的模式,而不是 obj.func(args)。作为附加好处,它也使 func(obj1, obj2, args) 等操作变得轻而易举。另一个区别是它不会将方法和数据封装在一起或对它们施加任何保护;也许这在开发人员足够成熟且代码已被审查时是一个无关紧要的措施。

抽象与具体类型

你现在知道抽象类型只是一个你无法实例化值的类型,但其他类型可以从它子类化,这为讨论 Julia 的类型系统铺平了道路。请记住,使用语法 var::type 来注释变量的类型是可选的,无论是在声明时、作为函数参数还是返回值。

Julia 中的任何类型要么是抽象的,如我们上面定义的,要么是具体的。具体类型是那些你可以实例化的,就像我们上面定义的自定义类型一样。

Julia 为数字提供了以下层次化的类型系统:

Julia 微基准测试:图片来自 Julia for Optimization and Learning 根据 MIT 许可协议

如果你的函数接受一个参数并对任何数字进行操作,你将使用 func(x::Number)。只有当传入非数字值,如字符串时,才会抛出错误。与此同时,如果它仅适用于任何浮点数,那么你将使用 func(x::AbstractFloat)。如果输入的类型是 BigFloat, Float64, Float32 或 Float16,则不会抛出错误。由于存在多重分派,你还可以定义另一个函数实例 func(x::Integer) 来处理给定数字为整数的情况。

Julia 也有一个层次化的类型系统用于 其他抽象类型,如 AbstractString,但它们要简单得多。

图片由 Paul Melki 提供,Unsplash

Julia 功能齐全

如果你考虑一下,Python 开箱即用地只有基本功能。例如,如果你仅使用 Python 而没有像 Numpy 这样的流行软件包,你在数据科学和科学计算方面能做的事情非常有限。该领域的大多数其他软件包也严重依赖 Numpy。它们都使用并假定“ Numpy” 数组类型(而不是默认的 Python 列表类型),就像它是语言的一部分一样。

Julia 并不是这样。它开箱即用地提供了许多重要的特性,包括:

数组支持

Julia 开箱即用地提供了类似于 Numpy 的数组支持,包括广播和矢量化支持。例如,以下比较了流行的 Numpy 操作与如何在 Julia 中原生编写它们:

#> 1\. Creating a NumPy array
### Python
arr = np.array([[1, 2, 3],
                [4, 5, 6],
                [7, 8, 9]])
### Julia
arr = [1 2 3
       4 5 6
       7 8 9]

#> 2\. Getting the shape of an array
### Python
shape = arr.shape
### Julia
shape = size(arr)

#> 3\. Reshaping an array
### Python
reshaped_arr = arr.reshape(3, 3)
### Julia
reshaped_arr = reshape(arr, (3, 3))

#> 4\. Accessing elements by index
### Python
element = arr[1, 2]
### Julia
element = arr[1, 2]

#> 5\. Performing element-wise arithmetic operations
### Python
multiplication = arr * 3
### Julia
multiplication = arr .* 3

# 6\. Array concatenation
### Python
arr1 = np.array([[1, 2, 3]])
arr2 = np.array([[4, 5, 6]])
concatenated_arr = np.concatenate((arr1, arr2), axis=0)
### Julia
arr1 = [1 2 3]
arr2 = [4 5 6]
concatenated_arr = vcat(arr1, arr2)

#> 7\. Boolean masking
### Python
mask = arr > 5
masked_arr = arr[mask]
### Julia
mask = arr .> 5
masked_arr = arr[mask]

#> 8\. Calculating the sum of array elements
### Python
mean_value = arr.sum()
### Julia
mean_value = sum(arr)

字符串支持

Julia 还开箱即用地提供了对字符串和正则表达式的广泛支持:

name = "Alice"
age = 13

## concatentation
greeting = "Hello, " * name * "!"

## interpolation
message2 = "Next year, you will be $(age + 1) years old."

## regex
text = "Here are some email addresses: alice123@gmail.com"

# Define a regex for emails
email_pattern = r"[\w.-]+@[\w.-]+\.\w+"

# match emails
email_addresses = match(email_pattern, text)

"aby" > "abc"       # true

当字符串被比较时,按字典顺序(一般的字母顺序)靠后的字符串被认为比靠前的字符串大。可以证明,大多数在 Perl 等高级字符串处理语言中可以做的字符串操作,也可以在 Julia 中完成。

多线程

Python 不支持真正的并行多线程是因为它有一个全局解释器锁(GIL)。这禁止了解释器同时运行多个线程,以便通过一种过于简单的解决方案来保证线程安全。只有在多个线程之间切换才是可能的(例如,如果一个服务器线程忙于等待网络请求,解释器可以切换到另一个线程)。

幸运的是,在 Python 调用的 C 程序中释放这个锁并不难,这解释了为什么 Numpy 是可能的。然而,如果你有一个大规模的计算循环,那么你不能编写可以并行执行的 Python 代码来加速计算。Python 的悲惨现实是,大多数应用于大数据结构(如矩阵)的数学操作都是可以并行化的。

与此同时,Julia 原生支持真正的并行多线程,并且只需简单操作即可实现:

# Before multi-threading
for i in eachindex(x)
    y[i] = a * x[i] + y[i]
end

# After multi-threading
Threads.@threads for i in eachindex(x)
    y[i] = a * x[i] + y[i]
end

运行代码时,你可以指定要使用系统中可用的线程数量。

与 C 代码的轻松集成

从 Julia 调用 C 代码的过程是开箱即用并且比 Python 更高效和更容易。如果你想调用

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

然后主要步骤(在小的设置之后)是在 Julia 中调用此函数就是编写

# specify function, return type, arg types and input. Prefix types with "C"
result = ccall(add, Cint, (Cint, Cint), 5, 3)

在 Python 中做到这一点要复杂得多,效率可能更低。特别是因为将 Julia 类型和结构映射到 C 中的类型和结构要容易得多。

这一个主要的结果是,可以在 Julia 中运行绝大多数能够输出 C 代码的语言。通常,外部的知名软件包存在于这些语言中。例如,要调用 Python 代码,你可以使用 PyCall.jl 软件包,如下所示:

using PyCall

np = pyimport("numpy")

# Create a NumPy array in Python
py_array = np.array([1, 2, 3, 4, 5])

# Perform some operations on the NumPy array
py_mean = np.mean(py_array)
py_sum = np.sum(py_array)
py_max = np.max(py_array)

除了安装包外,几乎不需要任何先期设置。使用类似的包,也可以调用用 Fortran、C++、R、Java、Mathematica、Matlab、Node.js 等编写的函数。

另一方面,可以从 Python 调用 Julia,尽管不如优雅。这可能以前用于加速函数,而不需要用 C 实现它们。

标准库

一组包随 Julia 预安装(但需要显式加载)。这包括StatisticsLinearAlgebra包、用于访问互联网的Downloads包,以及更重要的分布式计算(如 Hadoop)的Distribued包,还有用于性能分析(帮助优化代码)的Profile包,显著的Tests包用于单元测试和Pkg包用于包管理,以及许多其他包。

我必须说,我是一个热衷于 Python 的用户,开发了多个 Python 包。在 Python 中,第三方包“Setuptools”和 Julia 中的Pkg之间没有可比性,后者确实更干净、更易于使用。我一直无法理解为什么 Python 没有自己的包管理和测试工具。这些在编程语言中确实是基本需求。

摄影师 Tom M 的照片,来源于 Unsplash

Julia 是通用的

介绍

如果你曾经遇到过 Julia,那么你会自然地认为 Julia 是一个领域特定的语言,科学计算是这个领域。确实,Julia 经过精心设计,以便在科学计算中具有表现力和效率,但这并不妨碍它成为通用语言。它只是一个以科学计算为重点构建的语言。语言的通用性有一定的程度。例如,Julia 可以用于数据科学和机器学习、网页开发、自动化和脚本编写、机器人技术等,除了科学计算,但目前还没有成熟的包帮助开发者使用 Julia 进行类似于 Python 中 Pygame 的游戏开发。即使 Julia 包Genie.jl非常接近Flask,它可能在更全面的框架如Django方面有所欠缺。总之,即使 Julia 目前不像你期望的那样通用,但它是以此为目标构建的,并预计最终会实现这一点。

自动化和脚本编写

既然提到 Julia 可以用于自动化和脚本编写,值得指出的是,它通过优雅的类似 Shell 的语法帮助实现这一点。

例如,这里是一组你可以在 Julia 中执行的文件系统和进程操作:

# Create a directory
mkdir("my_directory")

# Change the working directory
cd("my_directory")

# List files in the current directory
println(readdir())

# Remove the directory
rm("my_directory"; recursive=true)

# Check if a file exists
if isfile("my_file.txt")
    println("File exists.")
else
    println("File does not exist.")
end

# Run a simple shell command from Julia
run(`echo "Hello, Julia!"`)

# Capture the output of a shell command
result = read(`ls`, String)
println("Contents of the current directory: $result")

注意你在终端中实际输入的内容的相似性。

星夜数字艺术的替代品 — 作者使用 DALLE 2 生成

Julia 具有广泛的扩展性

介绍

LISP 编程语言中的一个美丽特性是它是同构的。这意味着代码可以像数据一样处理,因此普通开发者可以向语言中添加新特性和语义。Julia 也被构建为同构的。例如,记得我说过 Julia 仅支持多重分发。好吧,看起来有人制作了一个 ObjectOriented.jl 包,允许开发者在 Julia 中编写面向对象编程。另一个例子是,如果你创建任何新类型,很容易重载基函数和操作符(它们只是函数)以适应你的新类型。

Julia 对宏的支持是实现这一点的主要原因。你可以将宏视为一个在程序解析时返回要执行的代码的函数。假设你定义了以下宏:

macro add_seven(x)
    quote
        $x + 7
    end
end

类似于函数,这允许你以这种方式调用它:

x = 5
@add_seven x

这返回 12。发生的事情是,在解析时(编译前),宏执行,返回代码 5 + 7,在编译时计算为 12。你可以认为宏只是动态执行 CTRL+H(查找和替换)操作的一种方式。

对于另一个实际应用场景,假设你有一个包含 10 个有用方法的包,你想向包中添加一个新接口,这意味着你需要为每个方法编写一个结构体,共 10 个。假设根据对应的函数编写这些结构体是有系统的,那么你可以简单地编写一个宏,循环遍历这 10 个函数来生成这 10 个结构体的代码。实际上,你编写的代码将等同于以通用方式编写一个结构体,因此节省了时间。

宏的存在允许更多的魔法。例如,如果你记得上面,我们能够使用 Threads.@threads 宏对一个 for 循环进行多线程。要测量函数调用的执行时间,你只需使用 @time func(),如果你使用 BenchmarkTools 包,那么 @benchmark func() 会多次调用函数,以返回时间统计数据,甚至生成小图。如果你知道什么是备忘录化,那么也可以通过一个简单的 @memoize 宏将其应用于任何函数,无需以任何方式修改它。还有 @code_native func() 可以显示函数生成的原生代码,以及其他宏可以在编译过程中显示代码的其他表示形式。

总结

事实证明,我们讨论的所有语言特性最初都是 Julia 计划的一部分。正如 Julia 网站上所述,这是该语言的愿景

“我们想要一种开源的语言,拥有宽松的许可证。我们想要 C 语言的速度,结合Ruby 的动态特性。我们希望这是一种同构的语言,拥有像 Lisp 一样的真正宏,但使用明显的、类似于 Matlab 的数学符号。我们希望它在通用编程方面像 Python 一样好,在统计分析方面像 R 一样简单,在字符串处理方面像 Perl 一样自然,在线性代数方面像 Matlab 一样强大,在将程序连接在一起方面像 Shell 一样出色。我们希望它简单易学,但仍能让最严肃的黑客满意。我们希望它是互动式的,并且我们希望它可以编译。”

阅读了这个故事,你现在应该或多或少能够反思愿景声明中提到的每一个词。

我希望阅读这些内容能够帮助你更多地了解 Julia 语言,并且你会考虑学习这门语言。下次见,再见。

拥抱叙事数据可视化的艺术

原文:towardsdatascience.com/embracing-the-art-of-narrative-data-visualization-3543706c6e56?source=collection_archive---------2-----------------------#2023-08-21

通过 NASA 的 TESS 任务进行的数据可视化

Ahmad AlbarqawiTowards Data Science Ahmad Albarqawi

·

关注 发表在 Towards Data Science ·8 min read·2023 年 8 月 21 日

--

叙事视觉场景的示例,用于探索系外行星的特点 — 作者

数据可视化是向读者展示复杂数据的强大工具。更进一步,叙事可视化使我们能够构建数据故事,将信息转化为一系列引人入胜的场景。这种方法为观众量身定制了体验。

叙事可视化是关于打造引导观众穿越数据的场景。它以创新的方式呈现数据,通过这些数据讲述一个故事。这个故事强调关键点以增加互动性,使观众能够与图表建立联系。每个视觉元素应被精心编织成一个有意义的故事。这样,这些数据就能在主动引发观众感官的同时,进行有效的传达。这样的数据接触方式允许观众在一段时间内记住重要信息。

本文将探讨叙事可视化的概念及其在数据传达中的应用。由 NASA 的过境系外行星勘测卫星(TESS)任务发现的系外行星将作为我们检视叙事可视化的一个视角。我们还将探讨 D3,这是一款强大的用于创建数据驱动文档的 JavaScript 库。

解码叙事可视化的基础

叙事可视化的核心在于制作视觉吸引人的图表,并带领观众发现数据。数据在开始时被简要介绍;在中间阶段被深入探索,并以揭示关键见解或提供灵活的探索工具作为结尾,从而形成一个互动故事。

这是一个将数据组织成特定结构的过程,打造一个视觉故事,而不是随机呈现事实和数据。因此,数据成为故事中的角色,你作为叙述者的工作是通过视觉效果使这些角色栩栩如生。观众在与数据故事互动的过程中,会建立联系并识别出模式,这些模式容易在长时间内被记住。

叙事可视化有三种主要结构:

  1. 作者驱动的叙事:作者通过数据提供了特定的路径并决定故事方向,以结构化的方式引导观众穿越数据。作者驱动的可视化在清晰传达见解方面非常有效,例如视频。

  2. 读者驱动的叙事:这种方法将控制权交给观众。它提供了更具互动性的体验,观众可以按照自己的节奏探索数据并跟随自己的路径。这有助于鼓励参与和探索。例如,互动仪表板就是一个例子。

  3. 混合叙事:结合了作者驱动和读者驱动叙事的元素。它们通常以作者驱动的介绍开始,随后是读者驱动的探索部分。这在引导讲故事和互动探索之间提供了平衡。例如,Martini Glass 数据表示就是一个例子。

martini glass sketch — 作者提供

在接下来的部分中,我们将使用“马提尼酒杯”结构,这是一种流行的混合叙事方式,来可视化 NASA TESS 任务的数据。该结构提供了一个初步由作者驱动的概述(酒杯的柄),随后是一个由读者驱动的探索空间(酒杯的碗)。这允许我们在让观众深入探索数据之前,带领他们了解数据的关键点。

案例研究:通过 NASA 的 TESS 任务发现系外行星

TESS 任务的概念 — 由NASA 戈达德太空飞行中心提供(许可详情见参考文献)

2018 年,NASA 通过发射凌日系外行星勘测卫星(TESS)任务,开始了探索太阳系之外系外行星的旅程。

TESS 任务已被证明是一个天文学上的宝藏。通过这一任务,NASA 积累了超过 90 个数据点。这些数据点包含关于每个系外行星的宝贵信息,有助于解开宇宙之谜。这些数据包括系外行星的名称、其宿主恒星和发现年份,以及它们的物理特征:大小、形状、偏心率和轨道周期。这些数据集概括了每个系外行星的故事,使 TESS 任务的事实和数据揭示出引人入胜的宇宙故事。

数据样本 — 原始数据包含超过 90 列

我们将使用TESS 数据创建叙事可视化,讲述多年来系外行星发现的故事,并使用最后的灵活工具进行深入分析。

场景 1:已发现系外行星概览 叙事从 2018 年到 2023 年 TESS 发现的系外行星概览开始,重点突出时间趋势,并在高层次上将某些特征与地球范围进行比较。这为我们的故事奠定了基础,提供了有关 TESS 任务范围和规模的背景。第一场景细分为三个具体的可视化,旨在展示数据的独特概览:

  • 每年发现的系外行星数量直方图:展示了每年发现的系外行星数量。每根柱子的高度对应于发现的数量。为了提供互动体验,点击柱子会筛选其他图表中的数据,以便集中分析该年发现的系外行星。

  • 平衡温度与轨道离心率的散点图:该散点图通过比较系外行星的平衡温度和轨道离心率,提供了系外行星特性的视图。平衡温度在 x 轴上显示,近似于系外行星的平均温度。轨道离心率在 y 轴上显示,表示系外行星轨道与完美圆形的偏差程度。交互式元素使用户能够进一步探索数据:悬停在圆圈上会显示数据概览,而点击则会转到第二个场景进行详细的探索视图。

  • 恒星质量与半径的散点图:聚焦于主星,该可视化将恒星质量与半径进行比较,每个都由一个圆圈表示。与之前的可视化一样,交互功能允许用户探索特定的行星特性。

场景 1 — 按年份展示的系外行星概述

场景 2:单个系外行星的深入探索

接下来,我们通过点击单个系外行星来深入探讨,探索它们独特的特性,并将这些特性与地球上的范围进行比较。数值以一系列水平条形图呈现,描述特定参数并将系外行星的值与相应的地球范围进行比较。

本场景提供了对使每个行星独特的属性的更详细了解,如平衡温度、行星半径、轨道半长轴、轨道离心率,以及主星的半径和质量。

场景 2 — 单个系外行星特性与地球范围的比较

场景 3:TESS 数据的交互式探索

最终场景提供了一个交互式探索工具,允许用户更改各种比较范围并选择要比较的特性。在这个场景中,我们允许进行个性化的探索体验。

场景 3 — 交互工具

本案例研究展示了叙事可视化在使复杂数据变得易于访问和有趣方面的力量。在下一部分,我们将探讨实现细节。

使用 D3 进行叙事可视化

D3.js 是一个 javascript 框架,用于在网页上进行交互式数据可视化。在我们开始构建 TESS 叙事可视化之前,让我们先探讨一下 D3 的一些基本功能。

选择元素:

D3 的一个重要功能是 ‘select’ 方法。它允许我们识别 HTML 文档中需要调整的元素。例如:

d3.select("#visualization")

附加和操作元素:

D3 可以使用 appendattrstyle 方法构建和管理 SVG 元素。

  • append 方法将新的元素(圆圈、矩形等)引入到 SVG 中,每个元素代表不同的数据点。

  • attrstyle 调整这些元素的属性和样式,如其位置、大小和颜色。

绑定数据:

使用 data 方法,D3 将数据绑定到视觉元素上。这使其在创建数据驱动的可视化方面发挥了优势,允许动态更新。

缩放

缩放将输入域映射到输出范围,调整数据的绘制区域。例如,d3.scaleLinear() 使用线性缩放,其中输入域中的任何给定数字直接对应输出范围中的一个数字。

构建 TESS 场景

现在我们已经了解了 D3 的基础知识,我们可以使用 TESS 任务数据(可在此处获取)构建引人入胜的叙事。为了简洁起见,本文详细讨论了前两个场景的基础知识。您可以查看 GitHub 仓库 以获取全面的场景实现。

准备 HTML

在头部加载 d3 库

<script src="https://d3js.org/d3.v7.min.js"></script>

然后准备一个容器 div 来绘制图表:

<div id="visualization"></div>

场景 1:发现的系外行星概述

下面是我们创建第一个场景的方式:

  1. 使用 d3.csv 函数加载并存储 TESS 数据到一个变量中。

  2. 散点图展示了 TESS 随时间发现的系外行星。

  3. 添加了互动性,允许用户点击特定的系外行星并导航到场景 2。

加载数据并调用代码以绘制第一个场景:

d3.csv("data/tess_confirmed_plannets.csv").then(function(myData) {
 data = myData;

 // Scene 1: Overview
 drawScene1(myData);
});

让我们通过在散点图中显示系外行星来绘制第一个场景的示例,查看行注释以获取实现细节:

TESS 任务数据探索 — scene1 代码示例,用于绘制概述散点图

场景 2:对单个系外行星的详细探索

我们将按照以下步骤创建第二个场景:

  1. 创建 drawScene2 函数以使用选定系外行星的数据。

  2. 创建展示选定系外行星的特征并与地球范围进行比较的视觉元素。

TESS 任务数据探索 — scene2 代码示例,用于将单个系外行星特征与地球范围进行比较

给出的代码是场景 1 和场景 2 的简化版本。完整的代码,包括互动元素和功能,可以从链接的 GitHub 仓库中获取。

您可以查看下面的最终叙事结果:

barqawiz.github.io/NASA_TESS_Narrative/

总之,叙事可视化偏离了单调的数据沟通路径,踏上了一段旅程。数据以结构化但引人入胜的方式呈现,以吸引观众的注意。这些数据故事以个性化的方式吸引观众。然而,这些故事的策展人可以有意选择他们的叙事结构,以实现高效的数据讲述,无论是作者驱动的、读者驱动的还是混合叙事。

NASA 的过境系外行星勘测卫星任务数据被用作案例研究,以展示叙事可视化。这个 TESS 任务发现了超过 90 个特征的系外行星,超出了我们的太阳系。混合的马提尼杯方法用于向观众传达这些数据。叙事从作者驱动的方法开始,然后通过读者驱动的方法过渡到关于系外行星的详细信息,进行互动和个性化的探索。

创作视觉叙事是一门艺术,明确你的最终目标和目标受众,并选择故事结构,对数据的隐含意义和吸引观众至关重要。

参考文献

  • NASA TESS 任务画廊: 链接

  • NASA 系外行星档案: 链接

  • NASA 内容政策: 链接

  • 地球事实表: 链接

  • 包含完整代码的 Github 仓库: 链接

  • 清理后的数据: 链接

  • UIUC 课程 “CS 416: 数据可视化”。

引用:NASA 内容(图像、视频、音频等)通常不受版权保护,可以用于教育或信息目的而不需要明确许可

人工智能中的涌现能力:我们在追逐一个神话吗?

原文:towardsdatascience.com/emergent-abilities-in-ai-are-we-chasing-a-myth-fead754a1bf9

观点

对大型语言模型涌现性质的视角转变

Salvatore RaieliTowards Data Science Salvatore Raieli

·发布在 Towards Data Science ·阅读时间 11 分钟·2023 年 5 月 16 日

--

作者使用 DALL-E 创作的图片

一个模型的涌现性质

涌现性质不仅仅是人工智能领域的概念,它适用于所有学科(从物理学到生物学)。这一概念一直吸引着科学家们,无论是在描述还是试图理解其起源方面。诺贝尔奖获得者物理学家P.W. Anderson用“更多即不同”来综合这一思想。在某种意义上,它可以被定义为涌现性质,即随着系统复杂性的增加而出现的性质,无法预测。

例如,你可以用一个小分子来编码信息,但 DNA(一个大分子)编码的是基因组。或者少量铀并不会导致核反应。

“复杂对称和分形 模式雪花中的形成体现了物理系统中的涌现现象”。图片来源:这里

最近,在人工智能模型中观察到了相同的行为,最常见的定义之一是:“如果某种能力在较小的模型中不存在但在较大的模型中存在,那么它就是涌现的。”

这意味着什么?如何观察到?

OpenAI 在一篇文章中声明,模型的性能遵循规模法则:数据和参数越多,性能越好。对于涌现属性,预期是一个特定的模式:随着参数数量的增加,性能几乎是随机的,直到达到某个阈值时,观察到某种属性(性能开始显著改善)。基本上,我们看到曲线的急剧转折(称为相变)。这也被称为涌现,因为通过检查小规模模型无法预测。

大型语言模型的涌现能力。图片来源 (这里)

总之,我们可以说,如果一个属性满足这两个条件,它被认为是涌现的:

  • 敏锐性,其过渡在存在或不存在之间是不连续的。

  • 不可预测性,随着参数的增加,其出现无法预测。

此外,扩展一个变换器模型主要考虑三个因素:计算量、模型参数数量和训练数据集大小。

这三个因素都使得模型昂贵。另一方面,这些属性特别受到追求,也被用作增加参数数量的理由(尽管模型并未得到最佳训练)。

图片来源:这里

一些研究还集中在为什么这些属性会出现,为什么以这种方式出现,以及为什么在特定阈值下出现。据一些观点,某些属性的出现是可以预测的:

例如,如果一个多步推理任务需要 l 步顺序计算,这可能需要一个至少有 O (l) 层深度的模型。 (source)

也提出了替代解释,如更多的参数有助于记忆。随着数据的增加,模型获得知识,并在某一点达到临界质量,能够支持该属性。

此外,一些作者提出,不同的架构和更好的数据质量可能会导致这些属性在更小的模型中也出现。

在 LLaMA 的研究中,观察到一个比 GPT-3 小得多的模型展示了相似的属性和性能。

[## META 的 LLaMA:一个小型语言模型战胜巨头

META 开源模型将帮助我们理解语言模型的偏见是如何产生的。

medium.com](https://medium.com/mlearning-ai/metas-llama-a-small-language-model-beating-giants-5065948e0b7f?source=post_page-----fead754a1bf9--------------------------------)

无论如何,问题依然存在,为什么这些属性会出现?

Anthropic 在一项研究中指出:

大型生成模型具有矛盾的特性——高可预测性——模型损失与训练所花费的资源有关,且与许多任务的性能改善有松散的相关性——以及高不可预测性——特定的模型能力、输入和输出无法提前预测(来源

简而言之,对于一个大型语言模型,我们可以预测的事物和无法预测的事物是存在的。例如,扩展规律允许我们预测增加参数数量将提升规模上的性能,但同时,我们无法预测某些属性的涌现,这些属性会随着参数的增加而突然出现。

因此,根据这一原则,我们甚至不应尝试预测这些属性。

扩展规律可靠地预测模型性能。图像来源:这里

三个突发特定能力扩展属性的例子。图像来源:这里

为什么我们如此关注预测这些属性?

第一个原因是纯粹的经济学:如果某个属性只有在特定数量的参数下才会出现,我们就不能使用较小的模型。这显著增加了训练和硬件的成本。另一方面,如果属性无法预测,我们甚至无法估算获得该属性的成本。

其次,这解释了在寻找新属性时参数异常增加的原因,这些新属性会在参数达到万亿级时出现。毕竟,这可能是获得某些属性的唯一途径。

此外,这还带来了安全问题,因为我们无法预测模型在特定规模下会具备什么属性。一个模型可能会发展出问题属性,可能不适合部署。而且,这么大的模型也更难检测偏见和危害。

此外,扩展规律和涌现属性也是推动大型模型快速发展的原因之一。

这开启了一个令人担忧的情景,一方面,我们有开源模型的爆炸性增长、训练成本的降低和聊天机器人的使用增加。但另一方面,我们无法预测这些模型的属性。

## 无限的语言模型图书馆

开源、数据和注意力:LLM 的未来将如何变化

towardsdatascience.com

如果涌现属性只是海市蜃楼呢?

图片由Nick Fewings提供,来源于 Unsplash

2020 年,谷歌研究人员意识到了 LLMs 的潜力,并预测它们将具有变革性。因此,他们请社区提供既不同又困难的任务示例,然后可以用来测试 LLM 的能力。由此诞生了超越模仿游戏基准(BIG-bench)项目。

这个项目实际上也集中于研究新兴和惊人的属性,并尝试理解它们的起源。

图片来源:这里

确实,该数据集和文章讨论了概率的出现,并试图提供解释。例如,超过十亿参数的模型能够解决三位数加法或两位数乘法问题。

基于这篇文章,斯坦福大学的研究人员在最近的一篇论文中质疑了语言模型的新兴属性这一概念。

[## 大型语言模型的新兴能力是一场幻觉吗?

最近的研究声称,大型语言模型显示出新兴能力,这些能力在小规模模型中并不存在……

arxiv.org](https://arxiv.org/abs/2304.15004?source=post_page-----fead754a1bf9--------------------------------)

实际上,作者注意到,新兴属性似乎仅在非线性或其他不连续的度量下出现。

作者提供了一个替代假设来解释属性的出现。他们认为这是性能测量的选择。换句话说,错误每个标记随着模型规模的增加平稳、持续和可预测地增长。但随后研究作者使用了不连续的度量来衡量任务性能,因此模型的任务表现看起来是突发性的。

换句话说,一个小模型在某个任务上表现尚可,但我们无法检测到,因为所选择的度量是不连续的,只有在某个错误(在某种模型规模下达到)下,我们才能观察到任务性能。

图片来源:这里

根据作者的说法,测试中样本数量少也导致小模型没有得到适当评估。

为了证明这一点,作者从规模定律开始,根据该定律,性能(或错误)随着度量数量的增加而增加,并且这种现象在不同规模下确实是一致的。正如作者所指出的,许多度量需要序列中的所有标记都正确,尤其是在处理长序列时,错误会显著增加。

他们能够使用 InstructGPT/GPT-3 进行这些实验,因为像 LaMDA、Gopher 和 Chinchilla 这样的模型不幸地无法访问。这阻止了他们对不同模型进行广泛的评估。由于 LLMs 仅在文本上进行训练(而 GPT 则训练于预测下一个词),LLMs 的一个令人惊讶的能力是整数算术任务。正如 GPT-3 介绍文章所显示的那样,这一特性在规模的函数中被定义为突现特性。

不同规模模型在少样本设置下的所有 10 个算术任务的结果。图片来源:这里

如图所示(顶部),当使用非线性度量来测量性能时,我们看到了一种突现特性。而当使用线性度量(底部)时,我们看到性能随规模的变化呈现出连续且可预测的增长。

图片来源:这里

此外,作者指出,通过增加小模型评估的数据,即使使用非线性度量,效果也没有那么明显。换句话说,如果测试数据集较大,即使使用非线性度量,我们也不会观察到如此显著的效果。

实际上,在低分辨率(少量测试数据)下,小模型的零准确率更可能出现,这支持了一个特性仅在达到某个阈值后才会突现的说法。

图片来源:这里

作者随后决定扩展到关于突现特性的元分析,使用 BigBench(因为它是公开的且文档记录完善)。此外,这个数据集提供了多个评估度量。当作者查看非线性度量(精确字符串匹配、选择题评分、ROUGE-L-Sum)时,可以观察到突现特性。另一方面,使用线性度量则没有观察到突现特性。

最令人惊讶的发现是,92%的声称的突现能力来源于使用两个不连续度量:选择题评分和精确字符串匹配。

因此,如果确实是由于不连续度量的使用导致了突现特性,仅仅更改度量就足以使这些特性消失。保持模型和任务不变,只需更改评分度量,突现特性就会消失。在这种情况下,作者只是重复使用了 LaMDA 系列模型的输出,并将度量从不连续的(选择题评分)更改为连续的(Brier 评分)。

最后一个问题是:但如果突现特性是通过选择不连续度量出现的,我们可以使用不连续度量来创造突现特性吗?

作者以手写数字数据集(MNIST 或数据科学家的最爱数据集)的分类能力为例。任何尝试在此数据集上训练卷积网络的人都注意到,即使只有少数几层,也能获得相当好的结果。增加层数可以提高准确率。如果这是一个突现属性,我们会期望一开始准确率接近零,通过将参数增加到某个阈值以上,准确率会显著提高。

作者使用了LeNet家族(多个具有递增参数数量的模型)。他们简单地选择了一种新的度量,称为子集准确率:“如果网络正确分类了 K 个 K(独立)测试数据,则为 1,否则为 0。”

在使用测试准确率时,我们注意到经典的准确率随 S 型趋势增加,使用新的不连续度量时,似乎分类手写数字的能力是一种突现属性。

作者提供了另一个例子:使用自编码器进行图像重建。通过创建一个新的不连续度量,自编码器的重建能力成为了一种突现属性。

作者总结道:

突现能力可能是研究者选择的产物,而不是模型家族在特定任务上的基本属性(source

换句话说,如果有人想要突现属性,他们所要做的就是选择一个不连续度量,神奇地,他们将看到在某个参数阈值之上出现一个属性。

作者保守地声明:“本文应被解读为声称大型语言模型无法展示突现能力。” 他们只是声称,迄今为止看到的属性是由度量的选择产生的。

现在确实是这样,直到你看到一只黑天鹅之前,所有天鹅都是白色的。然而,下次出现突现属性时,必须检查其出现的条件。此外,这也是再次思考现在可能不适合评估模型质量的基准的呼吁。其次,大型语言模型应该是开源的,因为任何声明都可能仅仅是由于评估的选择。

结束语

长时间以来,突现属性被认为是大型语言模型(LLMs)中最令人惊讶的行为之一。超出一定数量的参数后,能力会出现,这既是一个迷人的概念,又是一个令人恐惧的概念。确实,一方面,它进一步证明了寻找越来越大模型的理由。另一方面,潜在危险能力的突现没有警告是个问题。

这篇文章意外地展示了评价指标的选择如何导致属性的出现。这促使我们重新思考基准测试,并将重点转向评价指标的选择。其次,可能并不存在涌现属性。

更广泛地说,许多作者一直选择让他们的数据脱颖而出的评价指标。因此,只有当模型及其输出对公众开放,供独立的科学研究时,我们才能确信某一主张的真实性。

如果你觉得这很有趣:

你可以查看我的其他文章,你也可以 订阅 以便在我发布文章时获得通知,你还可以 成为 Medium 会员 以访问所有故事(这是平台的附属链接,我会从中获得少量收入,不会给你带来任何费用),你也可以在LinkedIn上与我联系或找到我。

这是我 GitHub 仓库的链接,我计划在这里收集与机器学习、人工智能等相关的代码和许多资源。

[## GitHub - SalvatoreRa/tutorial: 机器学习、人工智能、数据科学的教程…

机器学习、人工智能、数据科学的教程,包含数学解释和可重用的代码(使用 python…

github.com](https://github.com/SalvatoreRa/tutorial?source=post_page-----fead754a1bf9--------------------------------)

或者你可能对我最近的文章之一感兴趣:

[## PMC-LLaMA:因为谷歌搜索症状还不够

一个在医学院(或在知识竞赛之夜)能成为你最佳朋友的小模型

levelup.gitconnected.com](https://levelup.gitconnected.com/pmc-llama-because-googling-symptoms-is-not-enough-e1b875ee4c4a?source=post_page-----fead754a1bf9--------------------------------) [## 欢迎回到 80 年代:卷积可能会打破变形金刚

Hyena 模型展示了卷积如何可能比自注意力更快

levelup.gitconnected.com](https://levelup.gitconnected.com/welcome-back-80s-transformers-could-be-blown-away-by-convolution-21ff15f6d1cc?source=post_page-----fead754a1bf9--------------------------------) [## 注视你的眼睛:谷歌 AI 模型如何通过眼睛预测你的年龄

新模型可以通过分析眼睛照片解锁衰老的秘密

levelup.gitconnected.com

表情符号有助于社交媒体情感分析:不要再清除它们了!

原文:towardsdatascience.com/emojis-aid-social-media-sentiment-analysis-stop-cleaning-them-out-bb32a1e5fc8e?source=collection_archive---------5-----------------------#2023-01-31

在社交媒体情感分析中利用表情符号来提高准确性。

Bale ChenTowards Data Science Bale Chen

·

关注 发布于 Towards Data Science ·14 分钟阅读·2023 年 1 月 31 日

--

图片由 Denis Cherkashin 提供,来源于 Unsplash

简明扼要:

  • 无论你使用什么模型,或如何在分析中纳入表情符号,将表情符号包括在社交媒体情感分析中都能显著提高情感分类的准确性

  • 超过一半的流行 BERT 基础编码器不支持表情符号

  • Twitter-RoBERTa 编码器在情感分析中表现最佳,并且与表情符号协调良好

  • 与其清除表情符号,不如将它们转换为文本描述,这可以帮助提高情感分类的准确性并处理词汇表外的问题。

随着社交媒体成为人们生活的重要组成部分,人们在互联网上分享的内容对许多方面都极具价值。许多现代自然语言处理(NLP)技术被应用于理解公众的社交媒体帖子。情感分析是最受欢迎和最关键的 NLP 话题之一,侧重于计算性地分析对书面文本中实体的意见、情感、情绪或态度[1]。因此,社交媒体情感分析(SMSA)是一个理解和学习在短社交媒体帖子中表达情感的领域。

这个项目的另一个重要特征是可爱的文内图形——表情符号😄。这些图形符号在社交媒体通讯中越来越受到欢迎。根据Emojipedia 的统计数据(2021 年),一个著名的表情符号参考网站,现在超过五分之一的推文包含表情符号(21.54%),而超过一半的 Instagram 评论中包含表情符号。表情符号是一种方便且简洁的表达情感和传达意义的方式,这可能解释了它们的巨大受欢迎程度。

尽管表情符号在网络通信中无处不在,但它们在 NLP 和 SMSA 领域并不受青睐。在数据预处理阶段,表情符号通常会与其他非结构化信息如 URL、停用词、特殊字符和图片一起被删除2。虽然一些研究人员近年来开始研究在 SMSA 中包含表情符号的潜力,但这仍然是一种小众方法,等待进一步研究。这个项目旨在检验流行的 BERT 编码器对表情符号的兼容性探索将表情符号融入 SMSA 以提高准确性的方法

目录

1 背景知识

1.1 SMSA 究竟是什么?

1.2 情感分析方法的发展

2实验2.1 模型设计

2.2 数据准备阶段的经验教训(一个悲伤的故事)

2.3 BERT 家族的表情符号兼容性测试

2.4 实验方法来预处理表情符号

3 结果讨论 4 结论致谢参考文献

1 背景知识

1.1 SMSA 究竟是什么?

这里是一些关于 SMSA 的背景知识,你可能希望在深入实际实验之前了解。到目前为止,不需要技术背景/数学知识。让我首先解释最典型的 SMSA 任务的直觉:

SMSA 示例

如上图所示,给定一个社交媒体帖子,模型(由灰色机器人表示)将输出其情感标签的预测。在这个例子中,模型回应该帖子 57.60% 可能表达正面情感,12.38% 可能是负面,30.02% 可能是中立的。一些研究将帖子分类为二元方式,即正面/负面,但其他研究也考虑了“中立”作为选项。本项目遵循后者。

1.2 情感分析方法的发展

据我了解,研究社交媒体情感的首个定量方法是使用词汇表方法。该模型有一个预定义的词汇表,将每个词元映射到情感分数。因此,给定一个句子,模型查询词汇表,汇总每个词的情感分数,并输出总体情感分数。非常直观,对吧?

基于词汇表的情感分析模型基本图

SentiWordNetVADER 是这种方法的两个范例,受到了业界和学术界的青睐。

随着机器学习的发展,像 SVM、随机森林、多层感知器等分类器在情感分析中获得了立足之地。然而,这些模型不适用于文本输入,因此这些分类器与词嵌入模型结合以执行情感分析任务。词嵌入模型将词语转换为机器可以操作的数值向量。Google 的 word2vec 嵌入模型是文本数据表示学习的重大突破,其后是Pennington 等人的 GloVeFacebook 的 fasttext

由于自然语言的顺序特性和深度学习的极大普及,递归神经网络(RNN) 成为了“流行的小子”。RNN 按顺序解码或“读取”词嵌入序列,在循环中保留顺序结构,而词汇表模型和传统机器学习模型未能实现这一点。

现代 SMSA 的典型工作流程

上图解释了演变后的工作流程。词嵌入被传入 RNN 模型,模型输出最后一个隐藏状态(如果你不清楚最后的隐藏状态是什么,直观上它是 RNN 在“读取”完所有文本后“总结”的内容)。最后,我们使用前馈全连接神经网络将高维隐藏状态映射到情感标签上。

我们快完成了!最后一块拼图是Transformer 模型。即使你还没有学习 NLP,你可能也听说过“Attention is All You Need”3。在这篇论文中,他们提出了自注意力技术,并开发了 Transformer 模型。这些模型非常强大,以至于在几乎所有 NLP 的子任务中超越了之前的模型。如果你不熟悉 Transformer 模型,我强烈推荐你阅读 Giuliano Giacaglia 的这篇入门文章。

由于预训练 Transformer 模型具有无与伦比的性能,业界和学术界已经开始大规模使用这些模型。多亏了Hugging Facetransformer包,开发者现在可以轻松导入和部署这些大型预训练模型。BERT,即双向编码器表示的 Transformer,是最著名的基于 Transformer 的编码器模型,它为文本学习出色的表示。之后,基于 BERT 开发了 RoBERTa、BERTweet、DeBERTa 等模型。

2 实验

有了这些背景知识,我们现在可以深入到实验和编程部分了!如果你对 NLP 机制感到不自信,我建议你跳过技术细节或阅读一些关于 NLP 的入门博客,例如在 Towards Data Science 上。首先,我们要明确实验目标。我们想知道:

  • 目前流行的基于预训练 BERT 模型的兼容性如何与 emoji 数据。

  • 如果我们在 SMSA 过程中加入 emoji,性能会受到什么影响。

  • 在数据处理阶段,我们应该如何处理以包含 emoji。

2.1 模型设计

我们的模型遵循上述的神经网络范式,由预训练的基于 BERT 的编码器、Bi-LSTM 层和前馈全连接网络组成。图示如下:

模型图

为了清楚,预处理的推文首先通过预训练编码器,变成一个表示向量序列。然后,表示向量通过 Bi-LSTM 层。LSTM 的两个方向的两个最后隐藏状态将由前馈层处理,以输出推文情感的最终预测。

我们改变编码器模型和 emoji 预处理方法,以观察性能的变化。为了控制变量,Bi-LSTM 和前馈层在所有实验中配置相同。在训练过程中,我们只训练 Bi-LSTM 和前馈层。预训练编码器模型的参数被冻结。

该模型的 PyTorch 实现及其他技术细节可以在我的GitHub Repo中找到。

2.2 数据准备阶段的经验教训

数据的可用性是每个数据科学研究者的痛点。起初,我想找一个基准的 Twitter 情感分析数据集,以便将结果与之前的模型进行比较,但我遇到了以下障碍:

  1. 大多数数据集只有“推文 ID”作为查询键来查找原始内容。要访问带有 ID 的原始推文,我需要Twitter API 访问权限。我和我的教授导师都尝试申请过,但我们都没有获得批准(我们仍然不知道原因)。

  2. 嗯……另一个问题是大部分推文已经消失!这意味着它们要么被作者删除,要么因为某种原因被 Twitter 服务器删除了。

  3. 尽管有很少的数据集直接存储推文内容,但存储在.csv 或.tsv 格式的数据集无法保留表情符号。也就是说,原始推文中有表情符号,但我从网上下载的编译数据集完全丢失了所有表情符号。

因此,无论何时你想进行 Twitter 情感分析,确保首先验证数据集是否通过 Tweet ID 存储推文,这需要你额外的努力来检索原始文本。如果数据集是几年前的,推文很容易消失。此外,不要对申请 Twitter API 抱有太高的期望。我的导师是一位美国著名大学的助理教授,但也无法满足他们的要求(原因不明)。最后,为了保留表情符号,千万不要将其保存为 csv 或 tsv 格式。Pickle、xlsx 或 json 可能是你的好选择。

无论如何,找到一个保留表情符号、具有情感标签且大小合适的数据集对我来说非常困难。最终,我发现 Novak 等人的数据集符合所有标准。

2.3 BERT 家族的表情符号兼容性测试

在实现基于 BERT 的编码器之前,我们需要了解它们是否与表情符号兼容,即是否能为表情符号标记生成独特的表示。更具体地说,在将推文传递给编码器之前,它会首先由模型分词器进行分词,这种分词器是特定于编码器的(例如,RoBERTa-base 使用 RoBERTa-base 分词器,而 BERT-base 使用 BERT-base 分词器)。分词器的作用是将长字符串的文本输入拆分为词汇表中的单个词标记(如下图所示)。

spaCy 的基于规则的分词器 (source)

在我们的案例中,如果表情符号不在分词器词汇表中,它们将被全部标记为未知标记(例如“”)。因此,编码器模型将对所有这些未知标记产生相同的向量表示,在这种情况下,清理或不清理表情符号在模型性能上技术上不会有任何区别。

我选择了以下常见的基于 BERT 的编码器列表。

  • ALBERT-base-v2

  • BERT-base, BERT-large

  • BERTweet-base, BERTweet-large

  • DeBERTa-base, DeBERTa-large

  • DistilBERT

  • RoBERTa-base, RoBERTa-large

  • Twitter-RoBERTa

  • XLMRoBERTa-base, XLMRoBERTa-large

可以使用 HuggingFace 的transformers包和emoji包轻松完成测试。我们首先导入它们:

from transformers import AutoTokenizer
import emoji

AutoTokenizer 是一个非常有用的功能,你可以使用模型名称来加载相应的分词器,比如下面这行代码,其中我导入了 BERT-base 分词器。

tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased') 

然后,我们使用emoji包来获取完整的表情符号列表,并使用编码和解码功能来检测兼容性。

emoji_list = list(emoji.EMOJI_DATA.keys())
cnt = 0
for e in emoji_list:
    tokenized = tokenizer.decode(tokenizer.encode(e)).strip("</s>").strip()
    if e not in tokenized:
        cnt += 1
print(f"{cnt/len(emoji_list)} of the emojis are not identified by this tokenizer.")

2.4 实验方法来预处理表情符号

我们提出了 5 种数据预处理方法,以利用表情符号信息,而不是从原始推文中删除表情符号 (rm)。

直接编码 (dir) 使用支持表情符号的预训练编码器模型直接将表情符号向量化。这样,表情符号就会被视为普通的词令。这是最直接的方法。

用描述替换表情符号 (emoji2desc) 预训练的编码器并没有专门针对表情符号创建表示。相反,它们是在大量文本上进行训练的。我们推测编码器可能对词语的表示要优于表情符号,因此将表情符号转换为其官方描述可能有助于更好地提取语义信息。例如,“我爱动物 😍”将变成“我爱动物 笑脸带心眼”。下面的 Python 实现展示了如何使用emoji包:

def emoji2description(text):
  return emoji.replace_emoji(text, replace=lambda chars, data_dict: ' '.join(data_dict['en'].split('_')).strip(':'))

连接表情符号 (concat-emoji) 本质上,我们将表情符号重新定位到句子的末尾,并执行直接编码方法。由于表情符号不属于句子的语法结构,我们想知道重新定位它们是否有助于更好地区分文本和表情符号信息。例如,“天气太冷了🧊。 不想再工作了😡😭。” 变成 “天气太冷了。 不想再工作了。 🧊😡😭”

def emoji2concat_emoji(text):
    emoji_list = emoji.emoji_list(text)
    ret = emoji.replace_emoji(text, replace='').strip()
    for json in emoji_list:
        this_emoji = json['emoji']
        ret += ' ' + this_emoji
    return ret

连接描述 (concat-desc) 此外,我们还测试了用文本描述替换那些重新定位的表情符号。

def emoji2concat_description(text):
    emoji_list = emoji.emoji_list(text)
    ret = emoji.replace_emoji(text, replace='').strip()
    for json in emoji_list:
        this_desc = ' '.join(emoji.EMOJI_DATA[json['emoji']]['en'].split('_')).strip(':')
        ret += ' ' + this_desc
    return ret

元特征(meta) 除了将表情符号视为句子的一部分外,我们还可以将它们视为高级特征。我们使用表情符号情感排名 4 词典来获取积极性、中立性、消极性和情感分数特征。然后,我们将这些特征与表情符号的向量表示连接起来,形成推文的表情符号元特征向量。这个向量包含了推文的表情符号情感信息。纯文本将照常通过编码器和 BiLSTM 层,然后将元特征向量与 BiLSTM 层的最后隐藏状态连接,作为前馈层的输入。这个过程本质上是将表情符号从句子中隔离开来,将它们视为推文的元数据。

结果讨论

经过所有这些技术设计,我们最终进入了结果部分。首先,让我们看看这些常见的 BERT 基础编码器模型的表情符号兼容性。

BERT 基础的表情符号模型的表情符号兼容性

超过一半的这些模型不能识别所有表情符号! RoBERTa(基础版和大版)、DeBERTa(基础版和大版)、BERTweet-largeTwitter-RoBERTa 支持所有表情符号。然而,像 BERT(基础版和大版)、DistilBERTALBERT 这样的常见编码器几乎不支持任何表情符号。

现在,让我们比较不同表情符号兼容编码器和不同方法下的模型性能。下图中的百分比表示情感分类的准确率。每个单元格代表某种预处理方法下的编码器模型的准确率。

(请注意 emoji2vec 是一个于 2015 年开发的基准模型。它不是 BERT 基础的,而是一个预定义的表情符号嵌入模型,也可以生成表情符号的向量表示。它可以看作是 Google 的 Word2vec 模型的扩展)

情感分类准确率

为了比较将表情符号融入 SMSA 过程中的不同方法,我们还展示了不同方法下的准确率及置信区间。

每种预处理方法的平均准确率(带置信区间)

一个重要的发现是,无论如何包含表情符号,都能提升 SMSA 模型的性能。 移除表情符号平均降低了 1.202%的准确率。对于包含表情符号的方法,重叠的置信区间表示出相对模糊的区分。没有“通常最佳”的方法来利用表情符号。

每个编码器模型的平均准确率(带置信区间)

在所有编码模型中进行比较的结果如上方柱状图所示。柱状图顶部也标注了置信区间。小的置信区间表示对排名的统计信心较高Twitter-RoBERTa 在所有模型中表现最佳,这很可能是由训练领域造成的。emoji2vec,该模型开发于 2015 年并早于变压器模型的兴起,在当前标准下对表情符号的表示相对较差

既然没有找到“普遍最佳”的方法,我们深入探讨不同模型如何从各种预处理方法中获得不同的收益。下图展示了使用某种预处理方法与一开始移除表情符号相比的百分比改进。

百分比改进热图

首先,所有的改进指标都是正数,这强有力地证明了表情符号在 SMSA 中的有用性。在数据中包含表情符号将提高 SMSA 模型的性能。

通常,对于基于 BERT 的模型,直接编码表情符号似乎是一种足够且有时是最好的方法。 令人惊讶的是,最简单的方法效果与复杂的方法一样好,甚至更好。

表情符号表示学习模型较差的情况可能会从将表情符号转换为文本描述中受益更多。 emoji2vec 模型的改进既出现最大值也出现最小值。很可能 emoji2vec 对表情符号的向量表示较差,但将表情符号转换为文本描述有助于捕捉社交媒体帖子的情感意义。

RoBERTa-large 无论预处理方法如何显示出意外的小改进,这表明它不像其他基于 BERT 的模型那样受益于表情符号。这个结果可能是因为 RoBERTa-large 的架构可能更适合学习纯文本的表示而不是表情符号,但仍需要更严格的论证。

4 结论

从这个项目中,关键的收获是无论你使用什么模型或预处理方法,在 SMSA 过程中包含表情符号都会提高情感分类的准确性。因此,当你面对社交媒体情感分析任务时,一定要仔细考虑是否清除它们!

处理 SMSA 任务并协调表情符号的最佳模型是Twitter-RoBERTa编码器!如果你正在处理 Twitter 数据和分析推文情感,请使用它。

关于如何具体融入表情符号,方法之间并没有显著差异,因此一种直接的方法——将表情符号直接视为普通单词标记——可以完美解决问题。然而,考虑到我们研究中一半的常见 BERT 编码器不支持表情符号,我们建议使用 emoji2desc 方法。也就是说,使用之前提到的一行简单代码将表情符号转换为其官方文本描述,这可以轻松处理词汇表外的表情符号标记。

如果你使用传统的词嵌入如 word2vec,而且你也不想浪费可爱的表情符号,可以考虑使用 emoji2desc 或 concat-emoji 方法,而不是使用 emoji2vec 模型。

希望我们的项目能够指导 SMSA 研究人员和行业工作者如何在过程中包含表情符号。更重要的是,这个项目提供了一种提高 SMSA 准确性的全新视角。深入研究技术细节并不是唯一的进展方式,例如,这些简单却强大的表情符号也可以提供帮助。

脚本、学术报告及更多内容可以在我的GitHub Repo中找到。

关于文章中的图片,除非另有说明,否则均为作者提供。

致谢

我衷心感谢我的研究导师和指导教授马修·劳里埃。他为我提供了深刻的建议,并在这个夏季研究过程中给予指导。能与他一起完成这项研究,并在我生日时收到他的邮件问候,我感到非常荣幸和高兴。

本工作还部分得到了纽约大学 IT 高性能计算资源、服务和员工专业知识的支持。

此外,我真诚地感谢纽约大学及纽约大学上海校区提供的DURF研究机会。

感谢所有在这个夏天帮助我的朋友和家人。没有你们的支持,这项研究是不可能完成的。

参考文献

[1] Liu, B. Sentiment Analysis: Mining Opinions, Sentiments, and Emotions. (2015),剑桥大学出版社,剑桥。

2 Chakriswaran, P., Vincent, D. R., Srinivasan, K., Sharma, V., Chang, C.-Y., 和 Reina, D. G. Emotion AI-Driven Sentiment Analysis: A Survey, Future Research Directions, and Open Issues. (2019),应用科学。

3 Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A., Kaiser, u., & Polosukhin, I. Attention is All you Need. (2017), 在 Advances in Neural Information Processing Systems 中。Curran Associates, Inc.

4 Kralj Novak, P., Smailović, J., Sluban, B., & Mozetič, I. Sentiment of Emojis. (2015),PLOS ONE10(12),e0144296。

赋能公平:识别与解决生成模型中的偏见

原文:towardsdatascience.com/empowering-fairness-recognizing-and-addressing-bias-in-generative-models-1723ce3973aa

随着人工智能融入我们的日常生活,一个有偏见的模型可能对用户产生严重后果

Kevin Berlemont, PhDTowards Data Science Kevin Berlemont, PhD

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

--

图片由Dainis Graveris拍摄,来源于Unsplash

2021 年,普林斯顿大学信息技术政策中心发布了一份报告,发现机器学习算法可能会从训练数据中学习到类似于人类的偏见。其中一个引人注目的例子是关于亚马逊[1]的 AI 招聘工具的研究。该工具在之前一年提交给亚马逊的简历上进行了训练,并对不同的候选人进行了排名。由于过去十年科技职位中性别比例严重失衡,该算法学会了将语言与女性相关联,比如女性运动队,并降低了这些简历的排名。这个例子突显了不仅需要公平和准确的模型,还需要公平的数据集,以消除训练中的偏见。在 ChatGPT 等生成模型快速发展的背景下,以及人工智能日益融入我们的日常生活中,一个有偏见的模型可能会带来严重后果,侵蚀用户信任和全球接受度。因此,从商业角度来看,解决这些偏见是必要的,数据科学家(广义上的定义)必须意识到这些偏见,以减轻其影响,并确保其与原则一致。

生成模型中的偏见示例

第一个想到的生成模型广泛使用的任务类型是翻译任务。用户输入一种语言 A 的文本,期望得到语言 B 的翻译。例如,不同语言不一定使用相同类型的性别代词,例如英文中的“The senator”可以是女性或男性,而法语中则是“La senatrice”“Le senateur”。即使在句子中指定了性别(见下例),生成模型在翻译过程中强化性别刻板印象角色也并不罕见。

使用 ChatGPT 的翻译任务示例

类似于翻译任务,字幕生成任务要求模型基于某些输入生成新的文本,即将图像翻译成文本。一项最近的研究2分析了生成型变换器模型在 Common Objects in Context 数据集上进行字幕生成任务(见下图)的表现。

图源:“理解与评估种族偏见” arxiv.org/pdf/2106.08503.pdf

尽管这些描述符在所有图像中并不适用,但生成模型为字幕分配了各种种族和文化描述。这些描述符仅由更新的生成模型学习,并显示出这些模型的偏见增加。值得注意的是,变换器模型对于这个数据集也表现出性别偏见,例如,通过房屋/房间的背景将某人识别为女性,从而加剧了男女不平衡的比例。

偏差为何会发生?

生成模型的构思阶段为模型中的偏差发展留出了大量空间。偏差可能来自数据本身、标签和注释、内部表示甚至模型(请参见 huggingface.co/blog/ethics-soc-4 以获取针对 Text-to-Image 模型的详细列表)。

生成模型训练所需的数据来自众多来源,通常是在线的。为了确保训练数据的完整性,人工智能公司通常使用知名新闻网站等类似资源来构建其数据库。在这个数据集上训练的模型将由于考虑的群体人口特征限制(通常是白人、中年、上层中产阶级)而延续偏见的联想。

标签偏差 (www.ncbi.nlm.nih.gov/pmc/articles/PMC3994857/) 可能是更明显的,因为它会导致标签数据中引入偏差,这通常是无意的。生成模型被训练以重现/近似其训练数据集,因此标签中的偏差将对模型的输出表示产生严重影响。幸运的是,使用多个版本的标签并交叉检查它们可以缓解这些偏差。

最后两种偏见类型,即内部表示和模型偏见,都来自建模过程中的特定步骤。第一种在预处理阶段引入,无论是手动还是算法性。这一阶段容易引入偏见和文化细微差别的丧失,特别是当原始数据集缺乏多样性时。模型偏见则简单地来源于基于歧视特征的目标函数以及为提高模型准确性而放大的偏见。

生成模型中的偏见检测

正如本文所强调的,生成模型中的偏见以各种形式和不同条件下的方式存在。检测偏见的方法必须像它们试图检测的偏见一样多样化。

语言模型中偏见的主要衡量方法之一是词嵌入关联测试。该分数测量的是在嵌入空间(内部表示)中,两个词集之间的相似性。高分数表示强关联。更具体地说,它计算的是目标词集与两个输入词集之间相似性的差异,例如将[home, family]作为目标,将[he, man]/[she, woman]作为输入。如果分数为 0,则表示模型完全平衡。该指标被用来证明 RoBERTa 是最偏见的生成模型之一 (arxiv.org/pdf/2106.13219.pdf)。

测量生成模型(反事实评估)偏见的一个创新方法,尤其是性别偏见,是通过交换词汇的性别并观察预测准确性的变化。如果修改后的准确性与原始准确性不同,则表明模型中存在偏见,因为一个公正的生成模型应该在输入的性别独立的情况下具有相同的准确性。这一措施的主要缺陷是它只捕捉到性别偏见,因此需要其他测量方法来全面评估偏见来源。在类似的思路下,可以使用双语评估替代(经典翻译测量)来比较性别交换输入和原始输入产生的输出之间的相似性。

目前的生成模型基于使用一种叫做注意力的特征的变换器模型,以根据输出预测结果。研究已经直接从模型中调查了性别与角色之间的关系 (arxiv.org/abs/2110.15733)。这允许比较模型中的不同部分,以检测哪个模块对偏见的贡献更大。如果通过这种措施表明生成模型在维基百科数据集上引入了性别偏见,这一措施的一个缺陷是注意力值不代表概念之间的直接效应和相似性,需要深入分析才能得出结论。

如何克服生成模型中的偏见?

研究人员开发了各种技术以提供更少偏见的生成系统。这些技术大多包括在建模中的额外步骤,如设置一个控制变量以基于先前的信息确定性别,或添加另一个模型以提供上下文信息。然而,所有这些步骤不一定解决固有偏见数据集的问题。此外,大多数生成模型基于英文训练数据,极大地限制了这些模型的文化和社会多样性。

完全克服生成模型中的偏见需要建立一个正式的框架和基准,以测试和评估跨多种语言的模型。这将允许检测到在多样化的人工智能模型中以细微方式存在的偏见。

参考文献

关注我在 Medium 上,获取更多关于数据科学的内容!

如果你喜欢阅读这样的故事并想支持我作为作者,可以考虑 注册成为 Medium 会员。每月 $5,你将无限制访问 Medium 上的所有故事。如果你 使用我的链接注册,我将获得少量佣金。

[## 使用我的推荐链接加入 Medium - Kevin Berlemont, PhD

作为 Medium 的会员,你的部分会费将用于支持你阅读的作者,并且你可以完全访问每一个故事……

medium.com](https://medium.com/@kevin.berlemont/membership?source=post_page-----1723ce3973aa--------------------------------)

通过对话式数据访问创造信息优势

原文:towardsdatascience.com/enabling-the-data-driven-organisation-with-text2sql-f8e07089dd0c?source=collection_archive---------1-----------------------#2023-06-11

实现 Text2SQL 以推动数据驱动型组织的指南

Dr. Janna LipenkovaTowards Data Science Dr. Janna Lipenkova

·

关注 发表于 Towards Data Science ·19 分钟阅读·2023 年 6 月 11 日

--

图 1:Text2SQL 流程的表示

随着我们的世界变得越来越全球化和动态化,企业对数据的依赖越来越大,以便做出明智、客观和及时的决策。在本文中,我们将探讨如何利用人工智能进行直观的对话数据访问。我们将使用图 2 中显示的心理模型来说明 Text2SQL 系统的实现(有关心理模型的介绍,请参考利用整体心理模型构建人工智能产品)。在考虑市场机会和商业价值之后,我们将解释需要建立的数据、LLM 微调和 UX 设计等额外的“机制”,以便使数据在整个组织中广泛可访问。

图 2:人工智能系统的心理模型

1. 机会

目前,释放组织数据的全部潜力通常是少数数据科学家和分析师的特权。大多数员工不精通传统的数据科学工具(如 SQL、Python、R 等)。为了访问所需的数据,他们通过额外的层次,由分析师或 BI 团队将业务问题的语言“翻译”为数据语言。在这一过程中潜在的摩擦和低效率非常高——例如,数据可能会延迟交付,甚至在问题已经变得过时时才被提供。当需求未被准确翻译成分析查询时,信息可能在传递过程中丢失。此外,生成高质量见解需要一种迭代的方法,而每增加一步都会使这种方法变得更加困难。另一方面,这些临时互动会对昂贵的数据人才造成干扰,使他们无法专注于更具战略性的工作,正如这些数据科学家的“自白”中所描述的那样:

当我在 Square 工作时,团队较小,我们有一个让人畏惧的“分析值班”轮班制度。这是严格按周轮换的,如果轮到你,你就知道那一周你几乎不会有“真正”的工作,而是花大部分时间处理来自公司各个产品和运营团队的临时问题(我们称之为 SQL 猴子工作)。在分析团队中,管理角色的竞争非常激烈,我认为这完全是因为管理人员被免于这种轮班——没有什么荣誉可以与不做值班工作的诱饵相比。[1]

2. 价值

直接与数据对话而不必经历多轮与数据团队互动的过程岂不是很酷吗?这一愿景被对话式界面所接受,这种界面允许人类使用语言与数据互动,这是我们最直观和普遍的沟通渠道。解析问题后,算法将其编码成查询语言(如 SQL)的结构化逻辑形式。因此,非技术用户可以与他们的数据聊天,快速获取具体、相关和及时的信息,而无需通过 BI 团队绕道获取。主要有以下三大好处:

  • 业务用户可以以直接和及时的方式访问组织数据。

  • 这可以减轻数据科学家和分析师因业务用户的临时请求而产生的负担,使他们能够专注于高级数据挑战。

  • 业务可以以更流畅和战略性的方式利用其数据,最终将其转化为决策的坚实基础。

那么,在什么样的产品场景下你可能会考虑使用 Text2SQL 呢?主要有以下三种设置:

  • 你提供一个可扩展的数据/BI 产品,并希望以一种非技术性的方式让更多用户访问他们的数据,从而增加使用率和用户基础。例如,ServiceNow 已经将数据查询集成到更大的对话式产品中,而Atlan最近宣布了自然语言数据探索

  • 你希望在数据/AI 领域构建某些东西,以便在公司中普及数据访问,这种情况下你可能会考虑以Text2SQL 为核心的 MVP。像AI2SQLText2sql.ai这样的提供商已经开始进入这一领域。

  • 你正在开发一个定制 BI 系统,并希望最大化和普及其在个别公司中的使用。

正如我们在接下来的章节中将看到的,Text2SQL 需要一个非凡的前期设置。为了估算投资回报率,考虑一下要支持的决策性质以及可用的数据。Text2SQL 在数据变化迅速并被积极和频繁用于决策的动态环境中,可以带来绝对的收益,如投资、营销、制造和能源行业。在这些环境中,传统的知识管理工具过于静态,更流畅的数据和信息访问方式有助于公司生成竞争优势。在数据方面,Text2SQL 在以下数据库中提供最大的价值:

  • 大且不断增长,使得 Text2SQL 可以随着越来越多的数据被利用而逐渐展现其价值。

  • 高质量,以便 Text2SQL 算法不必处理数据中的过多噪音(不一致性、空值等)。通常,由应用程序自动生成的数据比由人类创建和维护的数据具有更高的质量和一致性。

  • 语义成熟,而不是原始数据,这样人类可以根据其心理模型中存在的核心概念、关系和度量来查询数据。请注意,语义成熟可以通过额外的转换步骤来实现,该步骤将原始数据转化为概念结构(参见“用数据库信息丰富提示”部分)。

3. 数据

任何机器学习工作都从数据开始,因此我们将从澄清在训练和预测过程中使用的输入和目标数据的结构开始。本文中,我们将使用图 1 中的 Text2SQL 流程作为我们的运行表示,并以黄色突出显示当前考虑的组件和关系。

图 3:在此 Text2SQL 表示中,与数据相关的元素和关系用黄色标记。

1.1 数据的格式和结构

通常,原始 Text2SQL 输入-输出对包括一个自然语言问题和相应的 SQL 查询,例如:

问题:“列出每个用户的名字和关注者数量。

SQL 查询

select name, followers from user_profiles

在训练数据空间中,问题与 SQL 查询之间的映射是多对多的:

  • 一个 SQL 查询可以映射到许多不同的自然语言问题;例如,上述查询语义可以表示为:“展示每个用户的名字和关注者数量”、“每个用户有多少关注者?”等。

  • SQL 语法高度灵活,几乎每个问题都可以用多种方式表示。最简单的例子是 WHERE 子句的不同排序。从更高级的角度来看,每个人做过 SQL 查询优化都会知道,许多途径都可以得到相同的结果,语义上等价的查询可能具有完全不同的语法。

对于 Text2SQL,手动收集训练数据特别繁琐。这不仅要求标注者掌握 SQL,还比情感分析和文本分类等更一般的语言学任务每个示例花费更多时间。为了确保足够的训练示例,可以使用数据增强,例如,LLMs 可以用来生成相同问题的同义句。3 提供了对 Text2SQL 数据增强技术的更全面调查。

1.2 用数据库信息丰富提示

Text2SQL 是一种位于非结构化数据和结构化数据之间的算法。为了获得最佳性能,两种类型的数据在训练和预测期间都需要存在。具体而言,算法必须了解查询的数据库,并能够以能够在数据库上执行的方式来制定查询。这些知识可以包括:

  • 数据库的列和表

  • 表之间的关系(外键)

  • 数据库内容

融入数据库知识有两种选择:一方面,训练数据可以限制为针对特定数据库编写的示例,这样,模式可以直接从 SQL 查询及其与问题的映射中学习。这个单数据库设置允许对特定数据库和/或公司优化算法。然而,这会扼杀扩展的任何雄心,因为模型需要为每一个客户或数据库进行微调。另一种选择是,在多数据库设置中,可以将数据库模式作为输入的一部分提供,这样算法就可以“泛化”到新的、未见过的数据库模式。如果你希望在多个不同的数据库上使用 Text2SQL,你绝对需要采用这种方法,但请记住,这需要相当大的提示工程努力。对于任何合理的业务数据库,包括完整的信息在提示中将非常低效,并且由于提示长度限制,可能几乎不可能。因此,负责提示制定的函数应该足够智能,以选择对给定问题最“有用”的数据库信息子集,并对潜在的未见过的数据库进行此操作。

最后,数据库结构起着至关重要的作用。在你对数据库有足够控制的情况下,你可以通过让模型从直观的结构中学习来简化模型的任务。作为经验法则,你的数据库越能反映业务用户谈论业务的方式,你的模型从中学习的速度和效果就越好。因此,考虑对数据应用额外的转换,例如将标准化或以其他方式分散的数据组装成宽表或数据仓库,以明确和无歧义的方式命名表和列等。你可以提前编码的所有业务知识将减少模型的概率学习负担,并帮助你获得更好的结果。

2. 智能

图 4:在这个 Text2SQL 表示中,与算法相关的元素和关系标记为黄色。

Text2SQL 是一种 语义解析 —— 文本到逻辑表示的映射。因此,系统不仅要“学习”自然语言,还要学习目标表示——在我们的案例中是 SQL。具体来说,它需要掌握以下知识:

  • SQL 语法和语义

  • 数据库结构

  • 自然语言理解(NLU)

  • 自然语言与 SQL 查询之间的映射(句法、词汇和语义)

2.1 解决输入中的语言变异性

在输入阶段,Text2SQL 的主要挑战在于语言的灵活性:正如在“数据的格式和结构”部分所描述的,相同的问题可以用许多不同的方式进行表述。此外,在实际的对话上下文中,我们还需要处理拼写和语法错误、不完整和模糊的输入、多语言输入等问题。

图 5: Text2SQL 算法必须处理多种不同形式的问题。

诸如 GPT 模型、T5 和 CodeX 这样的 LLMs 正越来越接近解决这一挑战。通过学习大量多样化的文本,它们学会了处理大量的语言模式和不规则现象。最终,它们能够对在表面形式不同但语义相似的问题进行概括。LLMs 可以直接使用(零样本)或在微调后使用。前者虽然方便,但准确性较低。后者需要更多的技能和工作,但可以显著提高准确性。

在准确性方面,正如预期的那样,表现最好的模型是包括 CodeX 模型在内的最新 GPT 系列模型。2023 年 4 月,GPT-4 在“带值执行”这一指标上相比之前的最先进技术提高了超过 5% 的准确率,达到了 85.3% 的准确率。4 在开源阵营中,最初尝试解决 Text2SQL 难题的焦点集中在如 BERT 等自动编码模型,这些模型在 NLU 任务中表现出色。[5, 6, 7] 然而,在生成 AI 的炒作中,最近的方法集中在如 T5 模型等自回归模型上。T5 通过多任务学习进行预训练,因此能够轻松适应新的语言任务,包括不同的语义解析变体。然而,自回归模型在语义解析任务中存在一个固有的缺陷:它们的输出空间不受约束,缺乏语义限制,这意味着它们在行为上可能变得非常有创意。虽然这对生成自由格式内容来说很惊人,但对于像 Text2SQL 这样需要受限、结构良好的目标输出的任务来说,这是一个烦恼。

2.2 查询验证与改进

为了约束 LLM 的输出,我们可以引入额外的机制来验证和改进查询。这可以作为额外的验证步骤来实现,如 PICARD 系统中所提议的。[8] PICARD 使用 SQL 解析器来验证部分 SQL 查询在完成后是否能转化为有效的 SQL 查询。在 LLM 的每个生成步骤中,那些会使查询无效的令牌会被拒绝,而概率最高的有效令牌会被保留。由于该方法是确定性的,只要解析器遵循正确的 SQL 规则,就能确保 100% 的 SQL 有效性。它还将查询验证与生成过程解耦,从而允许两个组件独立维护和升级 LLM。

另一种方法是将结构性和 SQL 知识直接融入 LLM。例如,Graphix [9] 使用图感知层将结构化 SQL 知识注入 T5 模型。由于这种方法的概率特性,它会使系统偏向于正确的查询,但并不能保证成功。

最后,LLM 可以作为一个多步骤的代理,能够自主检查和改进查询。[10] 使用链式思维提示中的多个步骤,代理可以被要求反思自己查询的正确性并改进任何缺陷。如果验证后的查询仍然无法执行,SQL 异常回溯可以作为额外的反馈传递给代理,以供改进。

除了这些在后台发生的自动化方法外,还可以在查询检查过程中涉及用户。我们将在用户体验部分更详细地描述这一点。

2.3 评估

为了评估我们的 Text2SQL 算法,我们需要生成一个测试(验证)数据集,在其上运行我们的算法,并对结果应用相关的评估指标。一个基于问题-查询对的天真的数据集划分,将数据分为训练、开发和验证数据,可能会导致次优结果。验证查询可能会在训练过程中暴露给模型,从而对其泛化能力产生过于乐观的看法。基于查询的划分,即将数据集划分为在训练和验证期间都不会出现相同查询的方式,提供了更真实的结果。

就评估指标而言,我们在 Text2SQL 中关心的不是生成完全与黄金标准相同的查询。这个“精确字符串匹配”方法过于严格,会生成许多假负例,因为不同的 SQL 查询可以导致相同的返回数据集。相反,我们希望实现高语义准确性,评估预测查询和“黄金标准”查询是否总是返回相同的数据集。有三种评估指标可以接近这一目标:

  • 精确集匹配准确性:将生成的 SQL 查询与目标 SQL 查询拆分为其组成部分,并比较结果集的一致性。[11] 此处的缺点是仅考虑了 SQL 查询中的顺序变化,而未考虑语义上等效查询之间更明显的句法差异。

  • 执行准确性:通过比较生成的 SQL 查询与目标 SQL 查询产生的数据集来验证一致性。运气好的话,即使语义不同的查询也可能在特定数据库实例上通过此测试。例如,假设一个数据库中所有用户年龄都超过 30 岁,以下两个查询虽然语义不同,却会返回相同的结果:

    select * from user

    select * from user where age > 30

  • 测试套件准确性:测试套件准确性是执行准确性的一个更高级和更严格的版本。对于每个查询,生成一个(“测试套件”)的数据库集合,这些数据库在查询的变量、条件和值方面具有高度差异化。然后,在这些数据库上测试执行准确性。尽管需要额外的努力来设计测试套件,但此指标也显著减少了评估中的假阳性风险.[12]

3. 用户体验

图 6:在这个 Text2SQL 表示中,UX 相关的元素和关系用黄色标记。

目前,Text2SQL 的最先进技术还不允许完全无缝地集成到生产系统中——相反,必须主动管理用户的期望和行为,用户应始终意识到她正在与 AI 系统交互。

3.1 失败管理

Text2SQL 可能会以两种模式失败,需要以不同的方式捕捉:

  • SQL 错误:生成的查询无效——要么 SQL 无效,要么由于词汇或语义缺陷无法在特定数据库上执行。在这种情况下,无法向用户返回结果。

  • 语义错误:生成的查询是有效的,但未能反映问题的语义,从而导致返回错误的数据集。

第二种模式特别棘手,因为“静默失败”——用户未检测到的错误——的风险很高。典型用户既没有时间也没有技术技能来验证查询和/或结果数据的正确性。当数据用于现实世界的决策时,这种失败可能带来严重后果。为避免这种情况,关键在于教育用户并建立 业务层面的保护措施,如对高影响决策的额外数据检查。另一方面,我们也可以利用用户界面来管理人机交互,帮助用户发现和改进有问题的请求。

3.2 人机交互

用户可以以不同的强度参与你的 AI 系统。每次请求的更多互动可能会带来更好的结果,但也会减缓用户体验的流畅度。除了错误查询和结果可能产生的负面影响外,还需要考虑用户提供来回反馈的积极性,以获得更准确的结果,并帮助长期改善产品。

最简单且最少参与的方式是使用置信度评分。虽然将置信度简单地计算为生成标记概率的平均值过于简单,但可以使用更先进的方法,如口头反馈。[13] 置信度可以在界面上显示,并在其危险低时用明确警报突出显示。这样,“现实世界”中适当跟进的责任——无论是拒绝、接受还是对数据进行额外检查——都落在你的用户肩上。虽然对你作为供应商来说这是一种安全的选择,但将这项工作转移给用户也可能减少你的产品价值。

另一种可能性是在低信心、模糊或其他可疑查询的情况下,邀请用户进行澄清对话。例如,你的系统可能会建议对输入进行拼写或语法纠正,并要求澄清特定单词或语法结构。它还可能允许用户主动要求对查询进行纠正:[14]

用户:显示 John 在这次冲刺中的任务。

助手:你想查看 John 创建的任务,还是他正在进行的任务?

用户:John 创建的任务

助手:好的,以下是任务 ID:

用户:谢谢,我还想查看更多关于任务的信息。请按紧急程度排序。

助手:当然,以下是任务及其简短描述、负责人和截止日期,按截止日期排序。

最后,为了便于用户理解查询,你的系统还可以提供查询的明确文本重新表述,并要求用户确认或更正。[15]

4. 非功能性要求

在本节中,我们讨论了 Text2SQL 的特定非功能性要求以及它们之间的权衡。我们将重点关注任务中最重要的六个要求:准确性、可扩展性、速度、可解释性、隐私和时间上的适应性。

4.1 准确性

对于 Text2SQL,准确性的要求很高。首先,Text2SQL 通常应用于对话设置中,其中预测是逐个进行的。因此,“大数法则”通常有助于平衡批量预测中的误差,但在这里无济于事。其次,句法和词汇的有效性是一个“硬”条件:模型必须生成一个结构良好的 SQL 查询,可能具有复杂的语法和语义,否则请求无法在数据库中执行。如果查询成功执行,它仍然可能包含语义错误并导致返回错误的数据集(参见第 3.1 节 失败管理)。

4.2 可扩展性

主要的可扩展性考虑因素是你是否希望将 Text2SQL 应用于一个或多个数据库——如果是后者,那么数据库集是否已知且固定。如果是的话,你会有更轻松的时间,因为你可以在训练过程中包含这些数据库的信息。然而,在可扩展产品的场景中——无论是独立的 Text2SQL 应用程序还是集成到现有的数据产品中——你的算法必须能够即时处理任何新的数据库模式。这个场景也没有给你机会去转换数据库结构,使其对学习更直观(链接)。所有这些都导致了与准确性之间的重大权衡,这也可能解释了为什么目前提供对新数据库进行临时查询的 Text2SQL 供应商尚未取得显著的市场渗透。

4.3 速度

由于 Text2SQL 请求通常会在对话中在线处理,速度方面对用户满意度非常重要。积极的一面是,用户通常意识到数据请求可能需要一定时间,并表现出所需的耐心。然而,这种善意可能会被聊天设置所削弱,因为用户在潜意识中期望类似人类的对话速度。像减少模型大小这样的强力优化方法可能对准确性产生不可接受的影响,因此请考虑推理优化以满足这一期望。

4.4 可解释性和透明度

在理想情况下,用户可以跟踪查询是如何从文本中生成的,查看问题中特定词汇或表达与 SQL 查询之间的映射等。这允许验证查询并在与系统交互时进行任何调整。此外,系统还可以提供查询的明确文本重述,并要求用户确认或更正。

4.5 隐私

Text2SQL 功能可以与查询执行隔离,因此返回的数据库信息可以保持不可见。然而,关键问题是提示中包含了多少数据库信息。三种选项(按隐私级别递减)是:

  • 没有关于数据库的信息

  • 数据库模式

  • 数据库内容

隐私与准确性之间存在权衡——在提示中包含有用信息的限制越少,结果越好。

4.6 随时间的适应性

要以持久的方式使用 Text2SQL,您需要适应数据漂移,即模型应用的数据分布变化。例如,假设用于初始微调的数据反映了用户在开始使用 BI 系统时的简单查询行为。随着时间的推移,用户的信息需求变得更加复杂,要求更复杂的查询,这会让您的简单模型不堪重负。此外,公司的目标或策略的变化也可能导致信息需求向数据库的其他领域转移。最后,Text2SQL 特有的挑战是数据库漂移。随着公司数据库的扩展,新的、未见过的列和表会出现在提示中。虽然设计用于多数据库应用的 Text2SQL 算法可以很好地处理这个问题,但它可能会显著影响单一数据库模型的准确性。所有这些问题最好通过反映当前实际用户行为的微调数据集来解决。因此,记录用户问题和结果,以及从使用中收集的任何相关反馈是至关重要的。此外,例如使用嵌入或主题建模的语义聚类算法,可以用于检测用户行为中的长期变化,并将这些作为完善微调数据集的额外信息来源。

结论

让我们总结一下文章的关键点:

  • Text2SQL 允许在企业中实现直观且民主的数据访问,从而最大化现有数据的价值。

  • Text2SQL 数据包括输入中的问题和输出中的 SQL 查询。问题与 SQL 查询之间是多对多的映射关系。

  • 重要的是在提示中提供关于数据库的信息。此外,可以优化数据库结构,使其更容易被算法学习和理解。

  • 在输入方面,主要挑战是自然语言问题的语言变异性,这可以通过使用在各种文本风格上进行预训练的 LLMs 来解决。

  • Text2SQL 的输出应该是一个有效的 SQL 查询。可以通过“注入” SQL 知识到算法中来实现这一约束;另外,使用迭代方法,可以在多个步骤中检查和改进查询。

  • 由于“静默失败”可能对决策造成错误数据的高影响,故失败管理在用户界面中是一个主要关注点。

  • 以“增强”的方式,用户可以积极参与 SQL 查询的迭代验证和改进。虽然这会使应用程序的流畅性降低,但也能减少失败率,使用户以更灵活的方式探索数据,并为进一步学习创造有价值的信号。

  • 主要的非功能性需求包括准确性、可扩展性、速度、可解释性、隐私和时间上的适应性。主要的权衡在于准确性与可扩展性、速度和隐私之间。

参考文献

[1] Ken Van Haren. 2023. 用 26 个递归 GPT 提示替代 SQL 分析师

2 Nitarshan Rajkumar 等. 2022. 评估大型语言模型的文本到 SQL 能力

3 Naihao Deng 等. 2023. 文本到 SQL 的最新进展:我们拥有的和我们期望的

4 Mohammadreza Pourreza 等. 2023. DIN-SQL: 分解的上下文学习文本到 SQL 及自我纠正

5 Victor Zhong 等. 2021. 用于零样本可执行语义解析的基础适应

6 Xi Victoria Lin 等. 2020. 跨领域文本到 SQL 语义解析中的文本和表格数据桥接

[7] Tong Guo 等. 2019. 内容增强的基于 BERT 的文本到 SQL 生成

[8] Torsten Scholak 等. 2021. PICARD: 从语言模型中逐步解析以进行约束自回归解码

[9] Jinyang Li 等. 2023. Graphix-T5: 混合预训练变换器与图感知层进行文本到 SQL 解析

[10] LangChain. 2023. LLMs 和 SQL

[11] Tao Yu 等. 2018. Spider: 用于复杂和跨领域语义解析及文本到 SQL 任务的大规模人工标注数据集

[12] Ruiqi Zhong 等. 2020. 用于文本到 SQL 的语义评估与提炼测试套件

[13] Katherine Tian 等. 2023. 仅需请求校准:从经过人类反馈微调的语言模型中获取校准信心水平的策略

[14] Braden Hancock 等. 2019. 部署后的对话学习:喂饱自己,聊天机器人!

[15] Ahmed Elgohary 等. 2020. 与解析器对话:带自然语言反馈的互动文本到 SQL

[16] Janna Lipenkova. 2022. 和我聊聊!与你公司数据的 Text2SQL 对话,在纽约自然语言处理会议上的演讲。

所有图片均由作者提供。

使用 MLflow 的端到端 ML 管道:追踪、项目和服务

原文:towardsdatascience.com/end-to-end-ml-pipelines-with-mlflow-tracking-projects-serving-1b491bcdc25f

高级 MLflow 使用教程

Antons Tocilins-RubertsTowards Data Science Antons Tocilins-Ruberts

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

--

照片由 Jeswin Thomas 提供,来源于 Unsplash

介绍

MLflow 是一个功能强大的工具,通常因其实验追踪功能而受到关注。这也很容易理解——它是一个用户友好的平台,用于记录机器学习实验的所有重要细节,从超参数到模型。但你知道 MLflow 提供的不仅仅是实验追踪吗?这个多功能框架还包括 MLflow Projects、模型注册和内置的部署选项。在这篇文章中,我们将探讨如何利用这些功能来创建一个完整且高效的 ML 管道。

对于完全没有 MLflow 基础的初学者来说,这个教程可能会有点难度,因此我强烈建议你在深入学习这个教程之前,先观看这两个视频!

设置

在这个项目中,我们将在本地进行操作,因此请确保正确设置你的本地环境。项目需要三个主要依赖项——mlflowpyenvkaggle。虽然可以通过 pip 安装 MLflow,但你需要按照不同的说明来设置 pyenvkaggle

完成安装后,确保拉取 这个仓库 的最新版本。当你在笔记本电脑上获取了仓库后,我们就可以开始了!

项目概述

移动到 mlflow_models 文件夹,你将看到以下结构:

mlflow_models 文件夹结构。图像由作者提供。

这里是这个项目中每个文件的简要概述:

  • MLProject — 描述 MLflow 项目的 yaml 风格文件

  • python_env.yaml — 列出运行项目所需的所有环境依赖项

  • train_hgbt.pytrain_rf.py — 使用特定超参数的 HistGradientBoosterTree 和 RandomForest 模型的训练脚本

  • search_params.py — 用于执行超参数搜索的脚本

  • utils — 文件夹包含项目中使用的所有实用程序函数

如前所述,本项目是端到端的,因此我们将从数据下载到模型部署。大致的工作流程如下:

  1. 从 Kaggle 下载数据

  2. 加载和预处理数据

  3. 调整随机森林(RF)模型,进行 10 次迭代

  4. 注册最佳 RF 模型并将其放入生产桶

  5. 使用内置的 REST API 部署模型

完成后,你可以自行重复步骤 2 到 5 以训练 HistGradientBoostedTrees 模型。在进入项目之前,让我们看看这些步骤如何得到 MLflow 的支持。

MLflow 组件

一般而言,MLflow 有 4 个组件 — Tracking、Projects、Models 和 Registry。

MLflow 组件。来自 mlflow.org 的截图

回顾项目步骤,下面是我们将如何使用它们。首先,我使用了MLflow Projects 来打包代码,这样你或其他数据科学家/工程师可以重现结果。其次,MLflow Tracking 服务将跟踪你的调优实验。这样,你将在下一步中检索到最佳实验,然后将你的模型添加到模型注册表中。 从注册表中,部署模型将真正变成一行代码,因为它们以 MLflow Models 格式保存,并具有内置的 REST API 功能。

项目步骤概览。图片由作者提供。

管道概览

数据

当你运行管道时,数据将会自动下载。作为示例,我将使用贷款违约数据集(CC0:公共领域许可证),但你可以通过重写training_data参数并将列名更改为相关的名称来进行调整。

MLProject & 环境文件

MLProject 文件为你提供了一种方便的方式来管理和组织你的机器学习项目,允许你指定重要的细节,如项目名称、Python 环境的位置和管道的入口点。每个入口点可以通过独特的名称、相关参数和特定命令进行自定义。命令作为可执行的 shell 行,每当入口点被激活时就会执行,并且能够利用先前定义的参数。

python_env.yaml 文件概述了执行管道所需的精确 Python 版本,以及所有必需软件包的全面列表。

这两个文件是创建运行项目所需环境所必需的。现在,让我们查看管道将执行的实际脚本(入口点)。

训练和实验跟踪

训练是在 train_rf.pytrain_hgbt.py 脚本中完成的。它们大体相同,唯一的区别在于传递的超参数和预处理管道。考虑下面的函数,它下载数据并训练一个随机森林模型。

实验在使用 with mlflow.start_run() 定义 MLflow 上下文时开始。在此上下文中,我们使用 mlflow.log_metrics 来保存 PR AUC 指标(有关更多信息,请查看 eval_and_log_metrics 函数)和 mlflow.sklearn.log_model 来保存预处理和建模管道。这样,当我们加载管道时,它将一起执行所有预处理和推理。如果你问我,这非常方便!

超参数调优

超参数调优使用 search_params.py 中的 Hyperopt 包进行。大部分代码来自官方mlflow 仓库,但我已经尽量简化了。这个脚本中最棘手的部分是理解如何组织这些调优轮次,使其与“主”项目运行连接起来。本质上,当我们使用 MLflow 运行 search_params.py 时,我们要确保实验的结构如下:

实验结构的可视化。图片来源于作者

正如你所见,search_params 脚本只会指定train_rf.py 接下来应该使用哪些参数(例如,10、2 和 5 的深度),以及它的父级运行 ID(在上面的示例中是 1234)。当你探索脚本时,确保注意以下细节。

  • 当我们定义 mlflow.start_run 上下文时,我们需要确保 nested 参数设置为 True

  • 当我们运行 train_rf.py(或 train_hgbt.py)时,我们显式传递 run_id 并使其等于先前创建的 child_run 运行

  • 我们还需要传递正确的 experiment_id

请查看下面的示例,以了解代码中的全部工作原理。eval 函数是将通过 Hyperopt 最小化函数进行优化的函数。

实际的调优函数相对简单。我们所做的就是初始化一个 MLflow 实验运行(所有其他运行的父级运行),并使用提供的搜索空间优化目标函数。

请注意,这些函数只是为了说明代码的主要部分。有关完整的代码版本,请参考github 仓库

运行 RF 流程

到现在为止,你应该对脚本如何工作有一个大致的了解了!所以,让我们用这一行运行随机森林管道:

mlflow run -e search_params --experiment-name loan . -P model_type=rf

让我们分解这个命令行:

  • mlflow run . 意味着我们想要在此文件夹中运行项目

  • -e search_params 指定了我们要运行的 MLProject 文件中的入口点。

  • --experiment-name loan 将实验名称设置为“loan”。你可以将其设置为任何你想要的名称,但请记下,因为之后会用到。

  • -P model_type=rfsearch_params脚本中的model_type参数设置为“rf”(即随机森林)。

当我们运行这行代码时,应该会发生四件事:

  1. Python 虚拟环境将被创建。

  2. 名为“loan”的新实验将被初始化。

  3. Kaggle 数据将被下载到新创建的文件夹data中。

  4. 超参数搜索将开始。

实验完成后,我们可以在 MLflow UI 中检查结果。要访问它,只需在命令行中使用命令mlflow ui。在 UI 中,选择“loan”实验(或你给它起的任何名字),并将你的指标添加到实验视图中。

MLflow UI 截图。图像由作者提供。

最佳的 RF 模型在测试 PR AUC 中达到了 0.104,训练时间为 1 分钟。总体来说,超参数调整大约需要 5 分钟完成。

注册模型

到目前为止,我们已经训练、评估并保存了 10 个随机森林模型。从理论上讲,你可以直接去 UI 中找到最佳模型,手动将其注册到模型注册表并提升到生产环境。然而,更好的方法是通过代码进行,因为这样可以自动化这一步骤。这正是model_search.ipynb笔记本所涵盖的内容。使用它来跟随下面的各个部分。

首先,我们需要找到最佳模型。要程序化地完成此操作,你需要收集所有超参数调整实验(10 个)并按测试指标进行排序。

你的结果可能会有所不同,但主要目标是得到正确的best_run参数。请注意,如果你更改了实验名称,你也需要在此脚本中更改它。父级运行 ID 可以在 UI 中查找,点击父实验(在这种情况下名为“capable-ray-599”)。

运行 ID 查找 MLflow UI。截图由作者提供。

为了测试模型是否按预期工作,我们可以轻松地将其加载到笔记本中。

如果你成功得到了预测——恭喜,你已经做对了一切!最后,注册模型并将其提升到生产环境也非常简单。

运行这 2 行代码会注册你的模型,并将其内部提升到“Production”桶。所有这些操作只是改变了我们访问模型及其元数据的方式,但在模型版本控制的背景下,这非常强大。例如,随时可以比较版本 1 和版本 2。

MLflow 模型注册表。截图由作者提供。

如果你进入 UI 的“Models”选项卡,你确实会看到一个名为loan_model的模型,其版本 1 目前在生产环境中。这意味着我们现在可以通过其名称和阶段来访问该模型,这非常方便。

服务模型

最简单的模型服务方式是在本地进行。这通常用于测试端点,并确保我们获得预期的输出。使用 MLflow 服务模型非常简单,特别是当我们已经注册了模型时。你只需运行以下命令:

mlflow models serve — model-uri models:/loan_model/Production -p 5001

这条命令将启动一个本地服务器,该服务器将在 5001 端口托管你的模型(即loan_model,目前处于Production阶段)。这意味着你可以向localhost:5001/invocations端点发送请求,并获取预测结果(前提是请求格式正确)。

要在本地测试端点,你可以使用requests库来调用它并获取预测结果。

在上面的示例中,我们获得了与之前相同的概率,但现在这些分数是由本地服务器而不是你的脚本生成的。输入需要遵循非常具体的指南,这就是为什么我们有 4 行预处理的原因。你可以在这里阅读更多关于 MLflow 服务的期望格式的信息。

总结

如果你已经做到这些并且一切正常——给自己一个热烈的掌声吧!我知道这需要消化很多内容,所以让我们总结一下你迄今为止取得的成就。

  1. 你看到了并理解了如何使用 MLflow Projects 来构建你的项目。

  2. 你了解我们在脚本中记录参数、指标和模型的位置,以及search_params.py如何调用train_rf.py

  3. 你现在可以运行 MLflow Projects 并在 MLflow UI 中查看结果。

  4. 你知道如何找到最佳模型,如何将其添加到模型注册中心,以及如何以编程方式将其推广到生产环境。

  5. 你可以在本地从模型注册中心提供模型,并调用端点进行预测。

接下来做什么?

我强烈建议你通过尝试运行梯度提升树模型的管道并部署 HGBT 模型来检验你的技能。所有必要的脚本都已提供,因此只需你配置管道并完成部署。如果遇到任何挑战或有任何问题,请不要犹豫,在评论区留下你的问题。

使用 GPT-3.5 进行端到端的机器学习

原文:towardsdatascience.com/end-to-end-ml-with-gpt-3-5-8334db3d78e2?source=collection_archive---------2-----------------------#2023-05-24

插图由我和 Midjourney 生成

了解如何使用 GPT-3.5 来处理数据采集、预处理、模型训练和部署的繁重任务

亚历克斯·亚当数据科学前沿 亚历克斯·亚当

·

关注 发表在 数据科学前沿 ·14 分钟阅读·2023 年 5 月 24 日

--

在任何机器学习应用程序的模型开发阶段,都存在大量重复的样板代码。像 PyTorch Lightning 这样的流行库已被创建,用以标准化训练/评估神经网络时执行的操作,从而使代码更加简洁。然而,样板代码的范围远超训练循环。即使是机器学习项目的数据获取阶段也充满了必要但耗时的步骤。应对这一挑战的一种方式是创建一个类似于 PyTorch Lightning 的库,涵盖整个模型开发过程。它必须足够通用,以适用于各种模型类型,并能够集成各种数据源。

提取数据、预处理、模型训练和部署的代码示例在互联网上随处可见,尽管收集这些代码并将其集成到项目中需要时间。由于这些代码在互联网上,可能已经被大型语言模型(LLM)训练过,并且可以通过自然语言命令以各种有用的方式重新排列。本篇文章的目标是展示如何通过使用 OpenAI 的 GPT-3.5 API 来自动化许多常见的 ML 项目的步骤。我会展示一些失败案例,并在可能的情况下如何调整提示以修复错误。从头开始,甚至没有数据集,我们将最终得到一个可以在 AWS SageMaker 上部署的模型。如果你跟着做,请确保按照以下步骤设置 OpenAI API:

import openai
openai.api_key = "YOUR KEY HERE"

此外,以下实用函数对调用 GPT-3.5 API 很有帮助:

def get_api_result(prompt):
    request = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0301",
        messages=[{"role": "user", "content": prompt}]
    )

    result = request['choices'][0]['message']['content']

    print(result)

提取、转换、加载(ETL)

ETL 由我 + Midjourney 进行说明

本节经过简化,仅考虑了单一数据源,但原则上可以扩展到数据来自多个来源的情况(csv 文件、数据库、图像等)。第一步是 提取 一些数据。为了简单起见,我将使用 Income Prediction¹ 数据集,其目标是预测一个人是否年收入超过或低于 50k 美元,基于他们的教育、职位、行业等。下面的函数将用于生成下载我们数据的代码。注意提示模板是如何设计的,以使 API 生成 Python 代码:

def extract(prompt):
    prompt_template = """You are a ChatGPT language model that can generate Python code. Please provide a natural language input text, and I will generate the corresponding Python code.\nInput: {}\nPython code:""".format(prompt)

    get_api_result(prompt_template)

在这里,我们要求 GPT-3.5:

使用 sklearn fetch_openml 函数从 openml 检索成人收入预测数据集。确保将数据作为单个数据框检索,其中包含一个名为“target”的列。将结果数据框命名为“df”。

这会给我们输出:

from sklearn.datasets import fetch_openml

adult_income = fetch_openml(name='adult', version=2)
df = pd.DataFrame(adult_income.data, columns=adult_income.feature_names)
df['target'] = adult_income.target

这段代码没有错误,完全达到了我们想要的效果。如果我使用更简单的提示,去掉提及 openml 和用于检索的函数,我们会得到:

import pandas as pd

df = pd.read_csv("adult_income_prediction.csv")
df.columns = ["age", "workclass", "fnlwgt", "education", "education-num",
              "marital-status", "occupation", "relationship", "race",
              "sex", "capital-gain", "capital-loss", "hours-per-week",
              "native-country", "target"]

这假设数据在本地可用。有趣的是,结果中有正确的列名列表,即使我们没有在 API 调用中包含它们!这些名称在网络上随处可见,特别是在这篇 Medium 文章中,目标列除外,它是由 GPT 添加的。下一步是转换数据为机器学习模型可以使用的格式。由于 GPT 需要了解列名及其类型,我们这里需要使用一个更高级的提示模板:

def transform(columns, column_types, prompt):
    prompt_template = """You are a ChatGPT language model that can generate Python code. Please provide a natural language input text, and I will generate the corresponding Python code using the Pandas to preprocess the DataFrame. The DataFrame columns are {} and their corresponding dtypes are {}.\nInput: {}\nPython code:""".format(columns, column_types, prompt)

    get_api_result(prompt_template)

我接着做如下请求:

通过将所有类别列转换为其独热编码等效项,并标准化数值列来预处理数据框。删除任何列中有 NA 或 NaN 值的行。删除数值列的异常值行,这些异常值由其 z 分数确定。数值列的异常值是超出 1 到 99 分位范围的值。数值列应使用 sklearn 中的 StandardScaler 进行标准化。目标列中的值应转换为 0 或 1,并应为 int 类型。

我们现在得到:

import pandas as pd
from sklearn.preprocessing import StandardScaler

# Load the DataFrame
df = pd.read_csv('data.csv')

# Convert categorical columns to one-hot encoded equivalents
df = pd.get_dummies(df, columns=['workclass', 'education', 'marital-status', 'occupation', 'relationship', 'race', 'sex', 'native-country'])

# Normalize numerical columns
scaler = StandardScaler()
numerical_cols = ['age', 'fnlwgt', 'education-num', 'capital-gain', 'capital-loss', 'hours-per-week']
df[numerical_cols] = scaler.fit_transform(df[numerical_cols])

# Drop rows with NA or NaN values
df.dropna(inplace=True)

# Drop rows with numeric column outliers
z_scores = df[numerical_cols].apply(lambda x: (x - x.mean()) / x.std())
df = df[(z_scores > -3).all(axis=1) & (z_scores < 3).all(axis=1)]

# Convert target column to 0 or 1 and change type to int
df['target'] = df['target'].apply(lambda x: 1 if x == '>50K' else 0).astype(int)

如果我们排除尝试从 csv 文件加载数据的部分,这几乎是完美的。即使我们明确告诉 GPT 假设df已经存在且不需要加载,也很难说服它排除这一点。最后,我们需要加载数据到本地数据库中。虽然对于如此简单的用例来说这有些多余,但这是一个值得养成的好习惯。

def load(prompt):
    prompt_template = """You are a ChatGPT language model that can generate Python code. Please provide a natural language input text, and I will generate the corresponding Python code.\nInput: {}\nPython code:""".format(prompt)

    get_api_result(prompt_template)

我使用以下指令:

连接到名为“data”的 sqlite 数据库。使用 pandas 将名为“df”的 DataFrame 中的数据插入到名为“income”的表中。不要包括索引列。提交更改后再关闭连接。

以获得:

import sqlite3
import pandas as pd

# Create a connection to the database
connection = sqlite3.connect("data.db")

# Define the DataFrame
df = pd.DataFrame()

# Define the table name
table_name = "income"

# Insert the data into the table
df.to_sql(table_name, connection, if_exists="replace", index=False)

# Commit the changes to the database
connection.commit()

# Close the connection
connection.close()

这已经很完美,只是多了不必要的df创建。有了处理后的数据,我们可以准备训练一些模型。

模型训练

我和 Midjourney 绘制的损失函数插图

在资源允许的情况下,尝试几种不同的模型类型以找出适合给定任务的复杂度水平是个好主意。因此,我们让 GPT-3.5 尝试几种不同的模型。首先,我们设置通用的模型训练提示模板:

def train(prompt):
    prompt_template = """You are a ChatGPT language model that can generate Python code. Focus on using scikit-learn when applicable. Please provide a natural language input text, and I will generate the corresponding Python code.\nInput: {}\nPython code:""".format(prompt)

    get_api_result(prompt_template)

我们将使用的提示是:

训练各种分类模型来预测“目标”列,使用所有其他列。使用 5 折交叉验证来选择最佳模型和相应的超参数,并返回最佳的整体模型及相应的超参数设置。根据准确性选择最佳模型。假设存在一个名为“df”的数据框用于训练。使用 MLFlow 记录整个过程。在训练任何模型之前用 mlflow 开始记录,以便只存储一个运行记录。确保模型使用 mlflow 的 sklearn 模块记录。确保只记录最佳的整体模型,但记录所有模型类型的指标。以下指标在所有交叉验证折中的均值应记录:准确性、AUC、F1 分数

让我们看看生成的输出,这次深入了解为什么需要提供某些特定的指令。

import pandas as pd
from sklearn.model_selection import GridSearchCV, cross_validate
from sklearn.metrics import accuracy_score, roc_auc_score, f1_score
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
import mlflow
import mlflow.sklearn

# load the dataframe
df = pd.read_csv('path/to/dataframe.csv')

# define the features and target columns
features = df.columns.drop('target')
target = 'target'

# set up mlflow
mlflow.set_tracking_uri('localhost:5000') # or set up your own server
mlflow.set_experiment("my_experiment")

# set up cross-validation
cv = 5

# define the models and corresponding hyperparameters to be tuned
models = [
    {
        'name': 'Logistic Regression',
        'estimator': LogisticRegression(),
        'hyperparameters': {
            'penalty': ['l1', 'l2'],
            'C': [0.1, 1, 10]
        }
    },
    {
        'name': 'Decision Tree',
        'estimator': DecisionTreeClassifier(),
        'hyperparameters': {
            'max_depth': [2, 4, 6, 8]
        }
    },
    {
        'name': 'Random Forest',
        'estimator': RandomForestClassifier(),
        'hyperparameters': {
            'n_estimators': [10, 25, 50, 100],
            'max_depth': [None, 2, 4, 6, 8]
        }
    }
]

# iterate through the models and corresponding hyperparameters to train and tune them
with mlflow.start_run():
    best_model = None
    best_metrics = {}

    for model in models:
        print(f"Training {model['name']} model...")

        # set up grid search for hyperparameter tuning
        gs = GridSearchCV(model['estimator'], model['hyperparameters'], cv=cv, verbose=2, n_jobs=-1, scoring='accuracy')
        gs.fit(df[features], df[target])

        # log metrics for all model types
        metrics = cross_validate(gs.best_estimator_, df[features], df[target], cv=cv, n_jobs=-1,
                                 scoring=["accuracy", "roc_auc", "f1"])
        for metric, values in metrics.items():
            mlflow.log_metric(f"{model['name']}_{metric}", values.mean())

        # check if this is the best model based on accuracy
        if best_model is None or gs.best_score_ > best_metrics['accuracy']:
            best_model = gs.best_estimator_
            best_metrics = {'accuracy': gs.best_score_,
                            'AUC': roc_auc_score(df[target], best_model.predict_proba(df[features])[:, 1]),
                            'F1': f1_score(df[target], best_model.predict(df[features]))
                           }

    # register the best model in mlflow
    mlflow.sklearn.log_model(sk_model=best_model, artifact_path="best_model")

    # log the best model's metrics
    for metric, value in best_metrics.items():
        mlflow.log_metric(f"best_model_{metric}", value)

如果我们移除 df 的加载和 # set up mlflow 部分,我们最终会得到完全期望的结果。即,循环遍历 3 种不同的模型类型,使用 5 折交叉验证进行网格搜索以识别给定模型类型的最佳超参数,同时跟踪指标。如果不指定“根据准确性选择最佳模型”,生成的代码将使用 scoring=[“accuracy”, “roc_auc", “f1”] 进行网格搜索,这将无法工作,因为如何根据多个指标选择最佳模型存在歧义。如果没有“确保模型使用 mlflow 的 sklearn 模块记录”,我们有时会得到 mlflow.log_model(),这是错误的。此外,“确保只记录最佳的整体模型”是必要的,以避免存储所有模型。总体而言,这个输出是可以接受的,但不稳定,多次运行可能会引入不同的错误。为了使一切为服务步骤做好准备,保存模型时添加模型签名是有用的。这个签名基本上是特征名称及其相应类型的集合。让 GPT-3.5 添加这个很麻烦,因此需要先添加导入的一些手动操作:

from mlflow.models.signature import infer_signature

然后修改通过以下代码行记录模型:

mlflow.sklearn.log_model(sk_model=best_model, artifact_path="best_model", signature=infer_signature(df[features], best_model.predict(df[features])))

模型服务

由我 + Midjourney 展示的部署示例

由于我们使用了 MLflow 来记录最佳模型,我们有几种选项来服务该模型。最简单的选项是将模型托管在本地。首先设计用于模型服务的一般提示模板:

def serve_model(model_path, prompt):
    prompt_template = """You are a ChatGPT language model that can generate shell code for deploying models using MLFlow. Please provide a natural language input text, and I will generate the corresponding command to deploy the model. The model is located in the file {}.\nInput: {}\nShell command:""".format(model_path, prompt)

    get_api_result(prompt_template)

提示将是:

使用端口号 1111 来服务模型,并使用本地环境管理器

通过调用 serve_model("<model path here>", question) 我们得到:

mlflow models serve -m <model path here> -p 1111 --no-conda

一旦在 shell 中运行该命令,我们就可以通过发送编码为 JSON 的数据到模型来进行预测。我们将首先生成发送数据到模型的命令,然后创建要插入命令中的 JSON 有效负载。

def send_request(prompt):
    prompt_template = """You are a ChatGPT language model that can generate code for sending data to deployed MLFlow models. Please provide a natural language input text, and I will generate the corresponding command. \nInput: {}\nCommand:""".format(prompt)

    get_api_result(prompt_template)

以下请求将插入到send_request()的提示模板中:

使用“curl”命令将数据“”发送到本地主机上端口 1111 的 mlflow 模型。确保内容类型是“application/json”。

GPT-3.5 生成的输出是:

curl -X POST -H "Content-Type: application/json" -d '<data here>' http://localhost:1111/invocations

最好将 URL 放在curl命令后面,而不是命令的最后,即:

curl http://localhost:1111/invocations -X POST -H "Content-Type: application/json" -d '<data here>'

让 GPT-3.5 完成这个任务并不容易。以下两个请求都未能成功:

使用“curl”命令将数据“”发送到本地主机上端口 1111 的 mlflow 模型。将 URL 放在“curl”后面。确保内容类型是“application/json”。

使用“curl”命令,将 URL 放在任何参数之前,将数据“”发送到本地主机上端口 1111 的 mlflow 模型。确保内容类型是“application/json”。

如果我们让 GPT-3.5 修改一个现有的命令,而不是从头生成一个,可能会得到所需的输出。以下是修改命令的通用模板:

def modify_request(prompt):
    prompt_template = """You are a ChatGPT language model that can modify commands for sending data using "curl". Please provide a natural language instruction, corresponding command, and I will generate the modified command. \nInput: {}\nCommand:""".format(prompt)

    get_api_result(prompt_template)

我们将按如下方式调用此函数:

code = """curl -X POST -H "Content-Type: application/json" -d '<data here>' http://localhost:1111/invocations"""
prompt = """Please modify the following by placing the url before the "-X POST" argument:\n{}""".format(code)
modify_request(prompt)

最终给我们的结果是:

curl http://localhost:1111/invocations -X POST -H "Content-Type: application/json" -d '<data here>'

现在是创建有效负载的时候:

def create_payload(prompt):
    prompt_template = """You are a ChatGPT language model that can generate code for sending data to deployed MLFlow models. Please provide a natural language input text, and I will generate the corresponding command. \nInput: {}\nPython code:""".format(prompt)

    get_api_result(prompt_template)

为了获得所需的输出格式,这部分提示需要进行相当多的调整:

将“df”数据框转换为可以被部署的 MLFlow 模型接收的 json 格式。将结果 json 包裹在一个名为“dataframe_split”的对象中。生成的字符串不应有换行符,也不应转义引号。此外,“dataframe_split”应该用双引号而不是单引号括起来。不要包含“target”列。使用分割的“orient”参数。

如果没有明确指示避免换行符和转义引号,则调用json.dumps()生成的格式不是 MLflow 端点期望的格式。生成的命令是:

json_data = df.drop("target", axis=1).to_json(orient="split", double_precision=15)
wrapped_data = f'{{"dataframe_split":{json_data}}}'

在用wrapped_data替换curl请求中的<data here>之前,我们可能只想发送几行数据进行预测,否则生成的有效负载可能过大。因此,我们将上述内容修改为:

json_data = df[:5].drop("target", axis=1).to_json(orient="split", double_precision=15)
wrapped_data = f'{{"dataframe_split":{json_data}}}'

调用模型得到的结果是:

{"predictions": [0, 0, 0, 1, 0]}

而实际的目标是[0, 0, 1, 1, 0]。

就这样了。在这篇文章的开头,我们甚至没有访问数据集,但我们最终成功部署了一个通过交叉验证选定的最佳模型。重要的是,GPT-3.5 完成了所有繁重的工作,只需提供最少的辅助。然而,我确实需要指定使用的特定库和调用的方法,但这主要是为了消除模糊性。如果我指定“记录整个过程”而不是“使用 MLFlow 记录整个过程”,GPT-3.5 将有太多库可供选择,生成的模型格式可能不适合使用 MLFlow 提供服务。因此,成功使用 GPT-3.5 需要对 ML 流程中执行各种步骤的工具有一些了解,但相较于从头编写代码所需的知识,这种了解是最小的。

另一个服务模型的选项是将其作为 SageMaker 端点托管在 AWS 上。尽管这在 MLflow 的 网站 上看起来很简单,但我向你保证,就像很多涉及 AWS 的网页示例一样,事情会出错。首先,必须安装 Docker,以便使用以下命令生成 Docker 镜像:

mlflow sagemaker build-and-push-container

其次,用于与 AWS 通信的 Python 库 boto3 也需要安装。此外,必须正确设置权限,以便 SageMaker、ECR 和 S3 服务能够代表你的账户进行通信。以下是我最终不得不用的命令:

mlflow deployments run-local -t sagemaker -m <model path> --name income_classifier
mlflow deployments create -t sagemaker --name income_classifier -m model/ --config image_url=<docker image url> --config bucket=mlflow-serving --config region_name=us-east-1

以及在幕后进行一些手动调整,以确保 S3 存储桶位于正确的区域。

在 GPT-3.5 的帮助下,我们以(大部分)顺利的方式完成了 ML 流程,尽管最后的步骤有些棘手。注意我没有使用 GPT-3.5 生成在 AWS 上服务模型的命令。它在这个用例中效果较差,并且会生成虚构的参数名称。我只能推测,切换到 GPT-4.0 API 可能有助于解决一些上述的错误,并带来更轻松的模型开发体验。

尽管 ML 流程可以通过 LLMs 完全自动化,但目前还不安全让非专家负责这个过程。上述代码中的错误容易被识别,因为 Python 解释器会抛出错误,但还有更微妙的错误可能会造成伤害。例如,预处理代码中删除异常值可能会导致过多或不足的样本被丢弃。最糟糕的情况下,它可能会无意中丢弃整个子群体,加剧潜在的公平性问题。

此外,超参数的网格搜索可能是在选择不当的范围内进行的,这可能导致过拟合或欠拟合,具体取决于范围。对于没有多少 ML 经验的人来说,这将很难识别,因为代码看起来没有问题,但需要理解这些模型中正则化的工作原理。因此,目前还不适合让未专门从事 ML 的软件工程师代替 ML 工程师,但这一时刻正在快速到来。

[1] Dua, D. 和 Graff, C. (2019)。UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加利福尼亚州欧文市:加州大学信息与计算机科学学院。(CC BY 4.0)

关于 12 年数据之旅的年终报告

原文:towardsdatascience.com/ending-the-year-with-12-lessons-about-data-career-8786afc068f4

关于数据职业旅程的三个故事

Marina TosicTowards Data Science Marina Tosic

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

--

“数字 12 被认为是宇宙数字——标志着 12 个月,12 星座,以及 12 个阴历和太阳的位置” [照片由Michael Skok拍摄,来源于Unsplash]

介绍:超越数字

在我之前的职位上,我为我的商业同事和首席执行官制作了年终报告。

你可能会说没什么特别的。标准报告,涵盖了不同业务领域的一堆数字——从一般商业控制、市场营销到供应链管理和财务。

说实话,我知道这些报告只是我的工作的一部分,不会引起公司每位同事的太多关注。

所以,我考虑了如何让报告更具“吸引力”。答案很简单——让它们听起来更酷。

换句话说,我决定为报告名称增添一些趣味,因为给数字增添趣味是不可能的。

我与其他标准年终报告的不同之处在于,我的报告是以今年的流行词汇命名的。

也就是说,2020 年的报告命名为“臭名昭著的 2020”。2021 年的报告命名为“疫苗 2021”,而 2022 年的报告则是“ChatGPT 2022”

你已经清楚为什么我会选择这些名称。

从充满动荡的 2020 年,当世界受到冠状病毒影响,到 2021 年的疫苗研发,再到 2022 年数据领域的一项重大进展——生成式 AI 聊天机器人的推出。

为了摆脱年终业务报告,我(最终)决定制作一份关于我职业进展的个人年终报告。

为什么,你问?

第一个原因是 2023 年标志着我职业生涯的 12 年,这一年可以说是—这样说吧—具有转折意义的。因为我再次一次性地改变了国家、工作和生活环境。

第二个原因是 2023 年是我职业生涯中技术发展最大的一年之一,进入数据领域的知识壁垒也有所增加。

随着所有新的发展和对生成性人工智能技能的突然需求,我认为这是开始职业生涯的最具挑战性的时刻之一。

为了证实这一点,我只需补充一句,即使是机器学习的先驱,安德鲁·吴最近也写了一份关于如何在人工智能领域建立职业的备忘录[1]。

这份备忘录的启发性不容小觑

它不仅鼓励人们进入人工智能领域,还提供了所需技能的建议,并且告诉你在寻找工作或转行时如何克服内心的“人类”挣扎。

尽管我无法完全理解现在开始数据职业的挑战,但我能够“发现自己”在安德鲁描述的备忘录中的大部分挑战。

换句话说,我知道如果你没有专门学习这一领域,开始建立数据职业有多么棘手。对不知道从哪里开始、首先学习什么或如何发展的感觉我很熟悉。

这正是我最终决定写这篇博客文章,总结过去 12 年职业生涯中收集的“见解”的第二个主要原因。

通过这篇文章,我的目标是帮助“外面”的某个人解决他们的数据职业困境,并让新加入者感到有鼓舞,进入这个领域。我希望通过分享我的故事和在构建数据职业过程中学到的一些教训来使这个过程更加人性化。

现在,同样老掉牙的问题又出现了:如何给报告起个酷炫的名字?

再次考虑到 2023 年标志着我职业生涯的 12 年,我认为最合适的报告名字就是“宇宙 2023”

因此,让我与大家分享塑造我成为数据专业人士的旅程,并决定在这个最令人惊叹的领域——数据领域,建立并坚持自己的职业生涯。

《宇宙 2023:输入输出流的三则故事》

正如这一部分的标题所述,我将与大家分享标志着我专业数据旅程主要阶段的三个故事。

故事一,名为“历经艰难,达致星辰”,讲述了我如何开始我的数据之旅,以及你可能不应该怎么做。 😉

故事二,名为“未知领域”,讲述了我如何在一个新国家找到数据行业的工作(们)。

故事三,名为“永远成长”,讲述了如何达到职业生涯的下一个阶段,并在数据领域采纳成长心态。

每个故事都有一个输入输出流:

  • 故事输入部分代表故事背景。

  • 故事输出部分代表故事结果。

在本节结尾,我将分享所学的经验,以总结从中获得的最重要的见解。

故事#1: 经历艰难,方得繁星 🌟

讲述如何(而不是;)在没有计算机科学背景的情况下开始数据之旅的故事。

➡️ 故事输入

学习机械工程(ME)时,我对大多数 ME 核心课程总是缺乏热情。我的意思是那些课程的“特别章节”,如结构元素流体力学热力学等。

我对课程材料并不是 100%投入,每次考试中最好的部分对我来说是解决数学方程式和推导计算。这是“绿灯#1”,我应该将我的职业方向定位在分析领域。

“绿灯#2 和#3”发生在我的硕士学习期间。我记得那是一个炎热的夏日,我和同学们坐在小实验室里,参加了一门名为信息系统的课程。在那里,我学会了如何进行关系数据库架构设计,并从中创建CRUD应用程序和 SQL 报告。

后来,我参加了另一门名为信息管理的课程,在这门课程中,我能够学习仅使用 SQL 进行高级分析。我立刻爱上了它,我知道那时这正是我想要的领域。

你可能会认为这足以自动开始从事数据领域的工作。然而,这离我的道路还很远。

2011 年完成学业后,我在一家制造公司获得了实习机会,负责项目管理方面的工作。虽然我的日常任务中有一些报告工作,但这并不是“我想要的”。我想从事数据库工作。

几乎1 年后,我在不稳定的工作市场中找到了一个“出口”,并获得了母校的研究与教学助理职位。带着高度的浪漫主义接受这个角色,我终于希望我的分析之旅能够开始。

简单来说,我错了。😃

我以前没有考虑到的一件事是,当你在学术界工作时,没有数据集在等着你。在很多情况下,甚至没有项目或公司伙伴在等着你。你需要自己找到这两者。

要达到这一点,即找到愿意提供数据集的公司伙伴,我花了另外 3 年的时间。我不会详细说明过程中遇到的问题和障碍,因为这是另一个博客帖子的主题。😃 但我可以告诉你,这个过程是一个依靠良好人脉和他人对我的信任的迭代过程。

经过这个较长的过程,我只记得那种为拥有“我的宝贵”数据而感到自豪和极大的工作动力的感觉。

⬅️ 故事输出

我终于拿到了我的数据集。

而且,当我以为最困难的部分已经过去时,困难才刚刚开始。

问题不断涌现:现在该如何处理这些数据?如何开发新事物以进行科学贡献?

我完全不知道从哪里开始,也不知道如何完成我的论文的分析部分。

然后在阅读科学出版物的过程中,我偶然发现了一个叫做“数据挖掘”的领域。

然而,这里再次出现了我一直等待的东西。可以用来从获得的数据集中开发模型的分析技术。

如果一切都像那样简单就好了。

当我深入ETL 处理领域,以准备建模阶段的数据时,新挑战开始出现。我有杂乱、缺失且不平衡的数据集,没有适当的连接需要被合并。这是我与我的业务同事(注:他们是维护控制中心的航空工程师,工作时间为 24/7)一起经常熬夜到凌晨 2-3 点以了解核心数据源的时候。

一旦赢得了这场战斗并准备好了输入数据集,接下来的挑战是学习机器学习建模。从特征选择降维到选择适当的机器学习算法和理解其背后的数学。此外,我还在收集统计分析的知识,以及如何评估、比较、调整和展示模型结果。

这一切又花了三年时间。

最终,把这一切整合在一起,我花了6 年时间才说我懂得如何从数据中创造价值。

换句话说,这次我终于获得了一个与“数据相关”的学位。 😃

故事 #2:未知之地 🗺️

关于如何在(外国)行业找到工作的一则故事。

➡️ 故事输入

在我的“大人生胜利”之后,即获得了新的学位,我开始着手我的下一个职业步骤——寻找一个行业职位。

我创建了一个全新的简历,因为旧的已经不再符合市场趋势。我在全国所有的招聘网站上创建了个人档案。最后,我开始申请少数几个列出的数据相关职位,并发送开放申请

我提到“几个可用职位”,因为 2017 年在一个拥有 400 万人口的国家,这些职位的需求并不高。

如果我记得没错,在 2017 年第四季度,我申请了 4–5 个数据职位。在这 4–5 个职位和若干个开放申请中,我成功获得了 2 次面试。

在通过初步评估测试(技术、智力和/或组织能力)后,下一阶段是与招聘负责人面试。

再次,我不会详细描述,但我只想说我多希望能忘记面试中的一些问题。其中一个直接的问题是:“你到底知道些什么?”在我详细讲述了我的论文工作后问的。

你可以想象我对这些及类似问题的困惑。我觉得这不对劲,事情应该有所不同。我应该得到不同的待遇。

我主动采取行动,制定了一个计划,寻找一个能让我获得平等机会、与之前不同的地方,以及能提供更多生活机会的工作。

说到重点:过了一段时间,我变得不同了。我在一个不同的行业、一个不同的国家找到了工作,带来了各种不同的挑战。😃

到达这个点的过程并不顺利,以下统计数据将帮助你更好地了解这个过程:

  • 时间跨度: 我花了 14 周时间;

  • 申请数量: 60 份工作申请;

  • 面试次数: 3 次面试,涉及若干轮(2–3 轮);

  • 报价数量: 1 份工作报价。

从上述数字可以看出,我很着急。我花了大部分空闲时间在某个招聘网站上查看新职位,并撰写求职信。

然而,投入在这上的每一分钟都是值得的。我找到了工作。

⬅️ 故事输出

我终于得到了——一份IT 顾问的工作。有人认为我具备这个角色所需的条件,尽管我缺乏语言技能、不知道如何使用特定工具,还有一些官僚障碍(工作许可)。😃

接受这个报价对我来说是一个不言而喻的决定,我知道这是一个好的选择。第一个原因是,作为顾问,我再次参与了项目工作。这类似于我之前角色中的工作。第二个原因是知识。这份工作让我有了自由,去收集和建立新的数据领域的知识。

这段时间我开始了解数据仓库架构设计概念和云平台。这两个领域对我来说都是全新的,非常有趣。

然后有一天,我接到了一个陌生号码的电话。是招聘人员打来的,他解释了新团队正在启动一个新数据项目,并且在他的堆栈里有我几乎一年前的简历。

起初,我不明白这个电话的内容,但我同意进行了后续通话。我仍然记得第二次通话中的一条信息:“你将处理数十亿条记录。”我的眼睛闪闪发光。我从未处理过大数据,这个机会听起来很棒。因此,我决定接受,并开始了我的新角色——作为数据工程师,参与大数据迁移项目。

这份工作很充实——密集学习编码和共同开发近实时数据摄取管道,以及共同开发以客户为导向的分析模型和洞察。再次,我通过处理以前从未处理过的用例来收集新的数据工程和数据科学知识。

这个过程持续不断……直到有一天,公司破产了。

嗯,这完全不在我的计划之中。😃 我突然失业了,并开始在疫情前的夏季(即“死季”)发送我的简历。

幸运的是,这时我已经有了一个小型网络,其中一个前同事向他的公司推荐了我。他的公司正在将分析数据集迁移到云平台,并且没有人全职处理这个问题。星星对齐了——我失业了,他们在寻找人选。双赢的局面。因此,我再次开始了我的新角色——作为数据分析师

这个职位让我朝着不同的方向发展。这些方向是我从未预料到的。

原因有很多:从业务分析师到数据通才(分析师、工程师和科学家于一身),并在 IT 和业务同事之间的第一线工作,从通过数据学习几乎所有业务方面,以及从一个“独行侠”到建立数据团队

总结一下,我花了另外 3 年,总共 9 年,才晋升到下一个职业层级。

故事 #3: 永远成长 🧠

一个关于新角色和致力于采用成长心态的故事。

➡️ 故事输入

下一个职业层级——我获得了建立数据团队并成为数据负责人的邀请。我已经有 6 年的导师和学生辅导经验,所以这应该很容易,对吧?

但事实并非如此。

但让我们稍微倒回一点,看看我是如何到达这一点的。我至今仍认为最有影响力的因素是“在正确的时间出现在正确的地方”。

意味着:我是第一个全职从事迁移项目的人;我在不同的数据领域有经验;数据方面的业务需求正在爆炸;我带来了关于如何制定长期数据路线图的想法。再次,星星对齐了,显然,成为“那个人”来组建和领导数据团队是合乎逻辑的选择。

我得到了一个信念的飞跃。因此,我抓住它,开始了新角色。

一开始我在这个职位上感到非常迷茫,这种状态可以说是“毫无头绪”,就像是重新开始职业生涯,不知道该从哪里做起。

起初,我无法放弃技术工作,依然紧抓不放。我的意思是,我从零开始做了所有开发,现在有人想接手?虽然我已经无法再单独管理开发需求,但很难放下实际操作的工作。

然后,我意识到我也需要做“以人为本”的任务。激励同事,进行一对一谈话,提供指导,制定团队愿景和目标,领导招聘,创造一个愉快的工作环境。

作为额外的任务,还有其他的“管理”任务——进行团队预算、控制、收集和组织按数据角色的工作,以及展示团队。

这些对我来说都是挑战性的任务,我需要帮助。

幸运的是,我具备了这一点。我不仅得到了来自上司的支持,还得到了同事的支持。此外,我甚至得到了前上司及其同事的支持。

然而,这对我来说还不够。当我开始阅读心理学和组织学的书籍,关注分享领导故事的人,参加研讨会和接受辅导时,真正的变化发生了,角色变得更加轻松。

之后,我意识到每个人都可以在适当的指导下,在未知的领域更快、更成功地成长。这些是我最初获得后来发现的所谓“成长型思维”的第一步。

⬅️ 故事输出

现在,我并不是说我在每一种生活情况中都拥有成长型思维。毕竟,我只是一个人,我的固定思维模式仍然存在。然而,我会说我致力于获得这种思维模式。

像生活中的其他一切一样,这需要持续的工作和纪律。需要在必要时能够反思并退一步。需要不在意有时候看起来无知。还需要通过学习来专注于自身的成长。

最终,你会意识到,这不是关于追逐职位和等级的阶梯,而是关于知识

经过了 12 年的历程,并且目前担任第二个数据主管职位,对我来说重要的是进一步获得数据领域内外的知识。特别是当涉及到生成式 AI 的发展时,因为这将影响数据行业的整个工作方式。

除此之外,我相信我终于能够通过分享我的经历来赋能和支持走类似道路的他人。所有这些都旨在激发对数据领域的兴趣,吸引新的人才。

学到的经验 🧐

我会尽量简短地列出在前面描述的 12 年漫长道路上获得的最重要的经验教训。更多的技术细节将在另一个博客中介绍。😉

数据职业必备要素

  • 建立基础知识。 理解数据领域中基础知识的重要性。换句话说,首先通过学习数学和统计学、算法,或理解数据结构、架构和编码原则来获得逻辑。这当然很多,但了解一般概念会让你以后的实践工作更轻松。

  • 交付高质量的工作(尽可能)。 在工作中优先考虑质量而非数量,并采用系统化的工作方式以提高清晰度和表现。

  • 避免借口(尽可能)。 为自己找借口和理由很容易,但承担责任并接受自己的错误会让你从人群中脱颖而出。

  • 形成你的意见(尽可能)。 通过评估和质疑现有的发现和结论来发展批判性思维能力。

  • 永远保持学习。 通过使用在线学习平台来认识自学和持续教育的价值。学习商业、心理学和其他科学,以补充你的技术知识。

  • 做个人反思。 记录你的失败,以便以后衡量你的进步。

数据职业个人见解

  • 寻找直到得到“是”为止。 训练自己在职业挑战中坚持不懈,不要把“”当作个人攻击。

  • 学习到不,谢谢是一个完整的句子。 专注于对你重要的目标,同时在过程中尊重他人。

  • 别人的天花板是你的起点。 我得到的最好建议之一就是不要因为别人的观点而限制自己的雄心。

  • 应得的赞誉。 了解给予赞誉的重要性以及分享知识的价值。

  • 依靠 2F。 在困难时期寻求朋友和家人的支持。

  • 这是你和你自己对抗。 你的职业生涯不是一场比赛,没有人有相同的起点。每个人都在自己的道路上挣扎,与他人比较没有意义。真正的对手是你自己。

结论:“好奇心没有治愈的方法”

通过在这个博客中分享我的故事,我想使构建数据职业道路上的挣扎变得正常化。我旨在给你提供每个障碍如何被解决的例子,只要你坚持寻找解决方案并朝着目标努力。

有了这些,我想激励那些没有学习这个领域的人和那些考虑转行的人加入数据领域

这可能不会是一条顺畅的道路,但要相信,几年后每一秒都会值得——也许是 3 年、6 年、9 年,或者是神秘的 12 年。 😉

最后,我将以多萝西·帕克的一句名言结束这个博客:

对无聊的解药是好奇心;对好奇心没有解药。

所以,要保持好奇心,加入最令人惊叹的领域 —— 数据领域 😃

感谢阅读我的帖子。请保持关注,获取更多关于 Medium LinkedIn的故事。

感谢支持的支柱

由于这是年终的“报告”,我需要向所有分享并仍在分享我旅程的人表示感谢。

  • 家庭和朋友。 在转向并坚持数据职业的过程中,我得到了来自家人和朋友的巨大支持。你们在困难时期与我一起哭泣和欢笑。感谢你们的陪伴。

  • 导师和同事。 向我曾经的上司及其同事们致谢,我视他们为导师,并且今天我仍然可以向他们请教建议。同样也感谢我的同事们。感谢你们让我学到了很多东西。

  • 社区。 在 Medium 和 LinkedIn 上,我遇到了启发过我、为我喝彩并分享我故事的人们 (TDS)。这有时会为我的日子带来惊人的光彩。感谢你们分享你们的善意。

知识参考

[1] DeepLearning.AI 资源,《如何在 AI 领域建立你的职业》由 Andrew Ng 撰写,访问日期 2023 年 10 月 13 日,info.deeplearning.ai/how-to-build-a-career-in-ai-book

能源供应与需求优化:使用 Gurobi Python 进行数学建模

原文:towardsdatascience.com/energy-supply-and-demand-optimisation-mathematical-modelling-using-gurobi-python-8a8b1cb9559a?source=collection_archive---------6-----------------------#2023-09-29

通过数学优化实现高效的能源分配

Kong You LiowTowards Data Science Kong You Liow

·

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

--

日落时分的电力塔照片。来源:Matthew Henry来自Unsplash

我们目前正处于一个能源供应和需求平衡变得尤为重要的时代。传统能源如石油和天然气正面临着诸多困难,例如俄罗斯-乌克兰冲突等地缘政治因素,这些因素扰乱了天然气供应,加上自然资源的总体枯竭。

另一方面,随着越来越多的国家从发展中国家转变为发达国家,能源需求也在不断增长。这种经济增长意味着对能源的需求增加,以支持生产和工业过程。应对气候变化的紧迫性进一步强调了对可再生能源的需求。综合所有因素,这意味着实现良好的能源供应和需求平衡变得愈加重要。

数学优化是研究在预定义约束条件下选择标准以最小化最大化特定目标的过程,所有这些都通过数学模型来管理。它在解决各种现实世界问题中非常有用,每个问题都有其独特的复杂性。

例如,在物流和运输中,数学优化可以用来解决如车辆路径问题等挑战,该问题优化车队的路线,从而通过使用高效的路线来降低成本和环境影响。在计算生物学中,这种方法对于理解复杂的生物过程、改善药物发现、基因组学和疾病途径研究具有重要作用,通过开发精确的模型来实现。此外,数学优化在供应链中扮演着关键角色,供应链是现代商业和工业中的重要元素。它优化资源分配,确保生产和分配系统的高效性,减少浪费和成本,同时提升客户满意度。总之,数学优化使各个领域的决策者能够应对复杂的问题,找到问题的最佳解决方案

在本文中,我们展示了如何使用数学优化来解决基本的能源供应和需求问题。我们将首先解释问题陈述并概述数学模型。接着,我们将通过一个实际的例子,并使用 Python 中的 Gurobi 求解器来运行模型。最后,我们将分析从这次模拟中获得的结果。

1. 问题陈述

让我们设想一个假设情况,我们负责制定一个虚构国家的年度能源生产和储存预算。在这个国家中,只有四种能源来源,即太阳能、风能、天然气和核能。

我们问题中不同能源来源的艺术插图:太阳能、风能、天然气和核能。来源:图标由 small.smileswaniconFreepik 提供,来自 Flaticon

我们可以访问详细的数据,这些数据包含了每月的生产和储存能力,以及相关的生产和储存成本的见解。此外,我们假设了解全年能源需求。关键问题是:我们如何优化每种能源的每月生产和储存量,以最小化年度总成本?

2. 数学公式

现在,让我们深入探讨我们模型的数学公式。如果你不喜欢方程式,可以跳过这一部分。然而,对于那些觉得数学有趣的人,所获得的见解将是值得的。我们将以精确的数学术语概述参数、目标函数和约束。

2.1. 参数

让我们定义我们模型中使用的数学符号。我们用 eE 表示所有能源来源集合 E(太阳能、风能、天然气和核能)中的能源类型,tT 表示所有月份集合 T(一月、二月、三月等等)中的月份。

现在,我们介绍为我们的问题提供的以下参数:

  • Dₜ 是第 t 个月的总预期能源需求。

  • Pₜₑ 是第 t 个月能源来源 e 的生产上限。

  • Sₜₑ 是 能源来源 e 在第 t 个月的储存上限。

  • Aₑ 是能源来源 e 的生产成本。

  • Bₑ 是能源来源 e 的储存成本。

我们模型的变量定义如下:

  • xₜₑ 是第 t 个月生产的能源来源 e 的数量。

  • yₜₑ 是第 t 个月剩余的能源来源 e 的数量,这也是该月将被储存的数量。

  • zₜₑ 是在第 t 个月使用的能源来源 e 的数量,用于满足该月的能源需求。

2.2. 目标函数

现在,让我们定义我们的目标函数。我们的目标是最小化全年的总成本。可以用数学术语表达如下:

在这里,Aₑxₜₑ 代表第 t 个月能源 e 的生产成本,而 Bₑyₜₑ 表示第 t 个月能源 e 的储存成本。两个求和符号 Σ 表示对全年所有能源来源的这些生产和储存成本进行求和。目标是找到使总成本最小化的变量 xₜₑyₜₑ 的值。

2.3. 约束

约束条件是我们问题解决方法中的关键部分。它们确保我们找到的解决方案在给定参数下既现实又可行。

2.3.1. 质量平衡约束

第一个约束,也称为质量平衡约束,确保在给定月份的每个能源来源中,生产的数量和从前一个月结转的数量之和等于使用的数量和结转到下一个月的数量之和。我们假设在第一个月 t₀(1 月),尚未有能量储存。从数学上讲,这个约束可以表示为:

2.3.2. 需求约束

接下来是需求约束,这是我们问题的一个基本方面。它确保在任何给定的月份,前一个月生产的和结转的总能量必须足以满足该月的能源需求。从数学上讲,这个约束可以表示为:

在这里,Σ 表示对所有能源来源 e 的求和。该约束确保在给定的月份 t 中,总能量(所有能源来源的生产和储存之和)大于或等于该月的能源需求 Dₜ。由于在第一个月 t₀ 没有能量储存,我们只需确保第一个月生产的总能量足以满足该月的需求。

2.3.3. 变量域约束

最后一组约束涉及定义变量的域,这对于确保这些变量的值保持在有意义和可行的范围内至关重要。在我们的情况下,我们有生产限制和储存限制,分别限制 xₜₑyₜₑ 的值。此外,由于这些变量不应取负值,我们需要在其域上设定下限。从数学上讲,域约束可以表示为:

不需要明确地定义 zₜₑ 的上限,因为它由质量平衡约束固有地决定。

3. 代码

在这里,我们将简要描述代码实现。我们首先导入 Gurobi 库和一个自定义的Data类,负责加载数据。代码可以在 GitHub 上找到: [能源优化](http://kyliow/energy-optimisation: Energy optimisation exploration (github.com))。

第一步是定义模型和必要的变量。

import gurobipy
from gurobipy import GRB

from data_extraction import Data

# Model
model = gurobipy.Model("Energy optimisation")

# Variables
X = model.addVars(
    Data.months, 
    Data.energies, 
    name="Amount supplied", 
    ub=Data.production_limit,
)
Y = model.addVars(Data.months, Data.energies, name="Amount remained")
Z = model.addVars(Data.months, Data.energies, name="Amount used")

我们可以使用model.addVars()方法中的ub参数来设定变量的上限,或者通过model.addConstrs()方法指定约束条件。在这里,我们展示了如何在model.addVars()方法中设定能源生产X的上限。稍后,我们将展示如何使用model.addConstrs()方法为能源储存Y设定上限。

接下来,我们定义目标函数。

# Objective function
obj = gurobipy.quicksum(
    (Data.production_cost[energy] * X[month, energy])
    + (Data.storage_cost[energy] * Y[month, energy])
    for energy in Data.energies
    for month in Data.months
)
model.setObjective(obj, GRB.MINIMIZE)

最后,我们定义约束条件,第一个是质量平衡约束。

# Mass balance constraints
model.addConstrs(
    (
        Y[Data.months[m_ind - 1], energy] + X[month, energy]
        == Z[month, energy] + Y[month, energy]
        for energy in Data.energies
        for m_ind, month in enumerate(Data.months)
        if month != Data.months[0]
    ),
    name="Energy balance",
)
model.addConstrs(
    (
        X[Data.months[0], energy]
        == Z[Data.months[0], energy] + Y[Data.months[0], energy]
        for energy in Data.energies
    ),
    name="Energy balance",
)

在定义质量平衡约束后,我们继续建立需求约束。

# Demand constraint
model.addConstrs(
    (
        gurobipy.quicksum(
            Y[Data.months[m_ind - 1], energy] + X[month, energy]
            for energy in Data.energies
        )
        >= Data.demand[month]
        for m_ind, month in enumerate(Data.months)
        if month != Data.months[0]
    ),
    name="Demand",
)
model.addConstr(
    (
        gurobipy.quicksum(
          X[Data.months[0], energy] for energy in Data.energies
        ) >= Data.demand[Data.months[0]]
    ),
    name="Demand",
)

在最后一步,我们展示了如何使用model.addConstrs()方法为储存Y设定上限。

# Upper limit for Y
model.addConstrs(
    (
        Y[month, energy] <= Data.storage_limit[energy]
        for month in Data.months
        for energy in Data.energies
    ),
    name="Storage",
)

4. 模拟分析

为了评估我们的模型,我们需要一些模拟数据。在这里,我们使用 2022 年英国的月度能源需求数据作为我们的模拟能源需求。同时,我们使用这些能源来源在 2022 年英国的能源生产数据,乘以 1.5 作为月度生产能力的近似值。能源需求和生产能力如下面的图所示。

每种能源来源的月生产限额和每月能源需求的条形图。来源:作者提供的图像。

对于储存容量,我们使用相当于欧盟天然气储存容量 10%的代理值,即每月约 9500 GWh,来代表天然气储存。我们为其他能源来源分配了每月约 2000 GWh 的储存容量,假设它们储存在大型能源储存设施中。

最后,我们将生产成本数据与 2022 年这些能源来源的实际成本进行近似。对于储存,我们采用天然气的实际储存成本,大约为每 GWh £16,000。然而,对于其他能源来源,我们假设它们使用锂电池储存,这通常比天然气的实际储存成本高得多。为了显示这种显著的成本差异,我们将其他能源来源的储存成本设定为每 GWh £80,000,000。

生产成本的饼图(单位:GBP 每 GWh)。来源:作者提供的图像。

下图展示了全年能源生产和储存成本,其中虚线和点线分别表示这两项成本。鉴于天然气的储存成本与其他能源相比极为便宜,甚至可能低于某些能源的生产成本,因此最优策略是全年持续储存天然气。因此,储存成本仅归因于天然气储存。唯一的例外是十二月,此时不进行储存。这是因为我们的分析设计为在十二月结束,因此没有激励去储存分析期之外的能源。

能源生产的堆叠柱状图,展示了每月的生产量和成本图。来源:作者提供的图片。

此外,我们的分析揭示了天然气生产由于其较低的生产成本而成为最具成本效益的选项。在风能和太阳能方面,我们观察到生产量在一年中有所波动,这可能受到生产能力的季节性变化的影响(例如,最大日照和峰值太阳能发电通常发生在年中)。这一波动从生产限制的柱状图中可以明显看出。

当我们将每月的生产和存储成本相结合时,结果是优化后的年成本为 170.1 亿英镑。

值得注意的是,我们使用了与 2022 年英国实际需求相同的能源需求数据。这表明我们优化的能源供应和存储分配优于实际情况。当考虑到像英国这样的国家通常会将多余的能源出口到其他国家以换取收入时,这一结论变得更加明显。虽然我们模型中没有考虑到多余能源的出口,但在现实世界中它代表了额外的潜在利益。

在实验的精神下,我们将除天然气之外的所有能源来源的存储成本降低了 1000 倍,以模拟大规模电池存储变得显著便宜的假设情况。结果如下面的图所示。

能源生产的堆叠柱状图,展示了每月的生产量和在修订存储成本下的成本图。来源:作者提供的图片。

在这种新情境下,我们可以看到能源存储增加,而生产成本通常下降,因为储存能源变得比生产能源更经济。有趣的是,我们还注意到实际需要生产核能,尽管仅在 1 月份。

在这种新情境下,模型计算的总年成本为 118.7 亿英镑,约为实际年成本的一半。这表明存储成本对整体年成本有显著影响。

5. 总结

我们展示了数学优化在解决简单能源供应和需求问题中的应用,证明了它在确定最优能源生产和存储量以最小化总年成本方面的有效性。

然而,重要的是要承认数学优化相关的一些潜在限制和挑战。其中一个挑战是在数学模型的制定过程中需要简化问题,这可能导致对现实世界问题的表述不完整。试图纳入更多复杂因素可能会导致更复杂的模型,使其更难理解和解决。

此外,解决大规模数学优化问题通常需要大量计算资源。为了解决这个问题,可以采用各种策略。启发式方法提供了一种实用的方式,可以迅速获得接近最优解的近似解。此外,元启发式方法,如遗传算法(我在这里详细解释),提供了更高层次的技术,用于迭代搜索足够接近最优解的解决方案。这些方法是解决复杂优化问题的宝贵工具,可以在精确优化具有挑战性或计算量大的情况下,提供实际解决方案。

进一步阅读

K. Y. Liow, 使用遗传算法在 Python 中优化物体避免 (2023), Towards AI @ Medium

参考文献和数据来源

[1] 每月电力统计数据 (2023), 国际能源署

2 T. Stehly 和 P. Duffy, 2021 年风能成本回顾 (2022), 国家可再生能源实验室

3 M. Roser, 为什么可再生能源这么快就变便宜了? (2020), 我们的数据世界

4 欧盟在冬季来临前达到 90%天然气储存目标 (2023), 欧洲委员会

5 GlobalData, 英国前五大能源储存项目 (2023), Power Technology

6 J. Witwer, 甲烷储存对实现可再生能源目标和增强能源韧性的作用 (2022), Onboard Dynamics

上下文推荐引擎的特征工程

原文:towardsdatascience.com/engineering-features-for-contextual-recommendation-engines-bb80bf0e0453?source=collection_archive---------5-----------------------#2023-04-27

对于上下文信息主导的不同案例的分析

Andrew CharabinTowards Data Science 安德鲁·查拉宾

·

关注 发布于 Towards Data Science · 11 分钟阅读 · 2023 年 4 月 27 日

--

图片来源于 VectorStock,授权给安德鲁·查拉宾

从娱乐应用到约会平台,从社交网络到零售,推荐引擎在当今社会中发挥了至关重要的作用。它们不仅在有效性上取得了显著进展,而且在引导我们的注意力、根据我们的兴趣进行个性化和推荐个人价值的项目等方面扮演着越来越重要的角色。虽然每个推荐引擎都是独特的,需要考虑问题、业务和可用数据的复杂性,但许多构建块是相同的——用户/项目嵌入、用户历史、上下文特征以及神经协作层来将用户和项目映射到评分。模型可以绕过假设性和容易出错的手动数据,转而使用大量的隐含和明确反馈数据来预测评分。

推荐引擎是机器学习或基于规则的模型,提供如最适合用户的内容/项目、目标客户、或公平价格等推荐。它们遵循以下通用结构:

结果 = F(U, I, C)

其中 U 是用户,I 是项目,C 是上下文,F 是一个将 U、I 和 C 的组合映射到结果的函数。结果可以是明确的反馈,如评分,隐含的反馈,如观看时间,或非反馈量,如价格。

当推荐引擎与数据管道打包在一起以获取所需的输入数据、具备从模型中获取批量推断和随时间进行更新的能力,以及一个用户界面以接收和互动推荐时,它就成为了一个推荐系统。

推荐引擎的一个简单示例是一个为流媒体平台上的用户(U)在周末(C)推荐电影(I)的模型。

协作推荐引擎可以用来将用户和项目映射到一个共同的嵌入空间,然后可以推荐空间中最接近的项目给用户。为了将用户和项目映射到这个新的嵌入空间,可以应用矩阵分解方法或多层感知机。只要有足够的过去的用户和项目反馈数据,自动映射和寻找用户与项目之间相似性的方式往往比使用手动特征(如用户和项目元数据)的推荐更为有效。

考虑以下基于 2 个手动特征的嵌入空间:一部电影在戏剧和奇幻类型中的评分(满分 5 分),并考虑 3 部电影。

作者图表 1

与使用 2 个手动制作的特征不同,协作系统被训练来自动将用户和项目映射到例如 k=10 个不同的“潜在维度”,这些维度可以通过过去的评分找到,而无需额外的项目/用户元数据。

你还会注意到,戏剧和奇幻在上面确定项目在空间中的位置时都扮演着~ 相等的角色。使用多层感知机与矩阵分解相比,嵌入的衍生和使用方式更具灵活性,以找到与用户最相似的项目。例如,可以学习嵌入,使得不同的维度在确定用户在空间中的位置时具有不同的权重。即,如果戏剧在电影的位置中比奇幻重要两倍,你可以想象 Y 轴被压缩了一半,记忆碎片与《星球大战》的相对距离略有减少。

作者提供的图表 2

神经协同过滤(NCF)是一个广义框架,用于通过项目评分预测用户,这允许放宽类似于图表 1 中的矩阵分解的线性限制——所有维度具有相同的权重,且评分与空间中的距离成反比(称为交互函数是线性的)。

上下文信息,如电影何时观看或用户与谁一起观看,可以在初始推荐后通过结果过滤(上下文后过滤),或在初始推荐前通过根据消费上下文将项目视为独立项(上下文前过滤)来考虑。

为了更高的严谨性,可以将上下文字段作为空间中的附加维度集成。考虑下面的示例,我们在垂直轴上添加了一个手工制作的特征,表示用户与家人一起观看电影的普及程度。

作者提供的图表 3

然而,由于传统嵌入技术的计算复杂度随着上下文维度的增加而呈指数增长(例如,参见多重宇宙作为一个例子),可以采用诸如因子分解机等替代方法来保持可处理性。

鉴于每个现实世界的推荐系统案例都是独特的,现代推荐系统通常是各种构建模块的专门组合,如项目嵌入、矩阵分解和神经网络层,以寻找用户与项目之间的连接。例如,考虑YouTube 的视频推荐引擎,它结合了两个不同的深度神经网络,第一个选择你可能喜欢的优质候选视频,第二个挑选出你最可能观看时间最长的最佳候选视频。

在本文中,我想讨论我遇到的两个偏离传统用户按项目推荐系统的一般示例,以及使用基于树或深度学习模型解决这类问题时一些独特的特征工程方法。在这两个示例中,上下文信息在预测成功交互方面的作用相对较大,而不像传统的电影推荐系统那样。

这些场景如下:

  1. 高上下文 & 低用户重要性 > 结果 = F(I, C)

  2. 高上下文 & 无项目 > 结果 = F(U, C)

场景 1

结果 = F(I, C)

第一种情况是项目和上下文在推荐系统中起主导作用,而用户则处于次要位置。考虑到大多数用户是首次使用,因此用户历史/相似性的价值有限,许多项目是新的且从未被评分过,或者合适的项目完全依赖于上下文,与用户关系不大。

要选择最合适的自由职业者来完成工作,您需要了解他们在类似工作中的表现如何,过去用户如何评价他们,以及他们是否有按时完成工作的声誉。最近与在线文案撰稿人合作的事实可能并不是找到当前现场摄影工作合适人选的最相关因素。

在这种情况下,传统的推荐引擎如何运作呢?基于内容的推荐可能会推荐与您的文案撰稿人相似的人,而协同推荐可能会推荐在雇佣文案撰稿人后其他人也雇佣过的自由职业者。这些推荐都可能并不特别有帮助。由于不同的上下文,其他用户在雇佣该地区摄影工作时的经验可能比您过去的经验更相关。

因此,我们希望了解先前被雇用的最佳自由职业者,这些自由职业者与当前上下文匹配,而与用户无关。但是,如果工作具有多种不同的上下文维度,例如工作类型、技能、范围等,以及衡量用户反馈的不同方法,例如交付速度、评级和雇佣情况,该如何处理呢?

一种解决方案是对上下文和反馈维度进行笛卡尔积,然后汇总所有用户的项目历史记录。

上下文与过去反馈的笛卡尔积

要做到这一点,我们首先需要对维度进行独热编码,以便为每个级别分配专用维度。为了说明问题,以下是候选自由职业者的结果。请注意,在模型中使用时,我们将会把最后三列中的每一行展开为具有名称如avg_delivery_on_time_photography的专用特征列。

图片

图表 4 作者提供

但是如果我们有 n 个初始上下文维度,每个维度具有 h - 1 个独热编码级别和 k 个反馈维度,那么需要 n x (h - 1) x k 个模型特征来映射所有可能的组合。在上面的不完整示例中,这导致了 51 个特征。

此外,许多特征与额外类型的工作(软件开发)相关,这些工作与当前上下文无关,但已通过笛卡尔积创建。

更简洁且有意义的编码方法是找到匹配当前上下文的所有上下文和反馈维度的过去历史,这需要 n x k 特征。这种上下文聚合可以通过类似以下的 SQL 使用案例逻辑轻松实现:

avg(case when past_job.type_of_work = job.type_of_work then delivery_on_time end) 作为 avg_delivery_type_of_work

其中工作表包含当前工作上下文的信息,而 past_job 表包含过去工作的相关信息。

改进情况如下:

图表 5 由作者提供

最终结果是关于过去自由职业者(项目)在类似工作(上下文)中的表现的丰富信息,同时限制了不必要的高维度。为了生成特征,上下文维度仅按线性方式考虑。诸如预算中等且经验水平为专家时的过去评分等组合被省略。非线性特性仍然可以由训练算法导出,或者可以创建额外的上下文维度作为现有维度的乘积。

然后,我们可以将每个反馈按上下文列展开为一个专用特征列,行指示一个独特的自由职业者。接着,将所有符合严格标准(即从事摄影工作)的自由职业者及其相关特征堆叠到额外的行中。现在我们可以重新构建这种表示,将每个过去的工作视为当前工作,将每个过去的工作堆叠为一组额外的行。对于过去的工作,我们可以创建一个额外的列来捕捉自由职业者收到的明确反馈,例如被雇佣,或隐性反馈如被联系。

图表 6 由作者提供

最后,我们可以训练一个基于树的模型如 XGBoost,使用工程特征(排除工作和才艺 #)来预测完成工作的反馈才艺,部署该模型,并将其应用于新发布的工作,以推荐最佳自由职业者。

场景 2

结果 = F(U, C)

另一个例子是,当推荐不是针对离散项,而是针对连续值时。考虑一个平台推荐物业租赁公司在周末应收取的物业租金。

在这种情况下,我们可以观察到与上下文维度匹配的过去租金价格,但现在我们不再对项反馈类型执行笛卡尔积,而是通过租赁是否为同一用户或其他用户来进行乘法运算。最后,我们可以通过计算平均值来汇总过去的价格。

图表 7 由作者提供

然后我们可以执行场景 1 末尾概述的额外步骤。

分层模型

另一种选择是创建一个初步的上下文模型,以便在所有用户中进行预测,然后再创建一个第二个模型,将用户无关的预测调整为用户特定的预测。这种方法的一个好处是提供了两种不同但有意义的预测,您可以推荐给用户或用于驱动期望的行为。

以一个模型为例,该模型输入职位描述和相关元数据以推荐招聘前的职位薪资。第一个模型可以预测与公司无关的薪资,从而找到该职位的“市场价格”。

可以用第一个模型的预测作为输入,并结合类似于图表 7 第三列的额外公司特征来训练第二个模型,以找到“量身定制”的薪资预测,即考虑到类似职位是否由同一家公司或另一家公司填补的薪资预测。

如果量身定制的预测与市场价格有显著不同,公司可能需要了解这一差异及其影响。例如,可以开发另一种模型,基于职位描述、招聘的候选人、起始薪资和公司来预测新员工的在职时间。可以使用市场价格与量身定制的薪资作为输入来计算预测的在职时间差异。假设市场价格为$75K,量身定制的薪资为$65K,但在$75K 的预测在职时间多出 1 年。推荐系统可以将$75K 作为推荐薪资,并提供一个工具观察不同薪资下的预测在职时间变化。

为了在防止泄漏的情况下训练分层模型,即“避免从持出集泄漏到用于训练模型的数据集中”,需要确保用于训练每一层的观察数据没有重叠。

此外,如果在第二层模型中集成了一个特征,该特征计算类似职位的历史公司薪资与第一个层模型预测的市场价格之间的差异,例如,结果为低于市场价格的平均$8K,则需要在公司与职位层级上对训练、评估和测试集进行抽样。这是因为作为职位薪资预测的特征,您还集成了所有公司之前填补的职位的市场价格预测。为了避免偏差,用于训练第一个模型的观察预测不应以任何方式作为后续模型的特征。我们最终得到一个训练/测试划分如下,不包括验证集,使用 80%的划分比例作为示例。

作者的图表 8

显然,只有在过去数据量相当大时,分层方法才是一种可行的替代方案。像 70%这样相对较低的分割比可能更可取。在分层方法中提供的附加用户特征对第二层模型的观察损失进行了对比,然而,在我评估过的特定回归用例中,仅通过将 80%观测数据训练的初始基于树的模型的预测整合到余下 16%观测数据训练的第二个模型中,最终 4%观测数据上的 MSE 约等于在所有 96%训练观测数据上训练的模型的 MSE。

结合来自上下文和协同模型的推荐

F(I, C) + F(U, I)

最后,从上下文模型衍生的推荐始终可以与从用户/物品相似性中获得的推荐加权,以提供最佳推荐。

为了更好地理解给定嵌入的用户/物品相似性的替代方法,我强烈推荐阅读这篇文章,该文章区分了 L-范数和角度度量。

总结

尽管我们当前的社会在推荐系统方面取得了显著进展,但较少的研究集中于整合改变用户和物品交互性质的上下文信息。神经协同过滤(NCF)提供了一个框架,用于开发神经网络,其中第一层接收用户和物品标识以推导嵌入,后续层模拟与用户和物品嵌入的交互以预测评分。目前,相关的上下文仍然需要手工制作并显式地输入到模型中。虽然 NCF 方法是推荐的方式,被 YouTube 和 Google 等公司使用,但在某些情况下,例如在上下文信息起主导作用的非传统情况下,它们可能有些过度。

我已经详细介绍了我个人经历过的几个推荐系统案例;在这些案例中,用户信息并不是非常重要,也没有物品。我展示了一些有趣的方法来在这些情况下制作手工特征,例如通过笛卡尔积和分层模型。

希望这里的想法有助于激发一些创意,扩展你的知识,并激励你进一步探索迷人的推荐引擎领域。

感谢阅读!如果你喜欢这篇文章,请关注我以获取我的新文章通知。同时,欢迎分享任何意见/建议。

工程已永远改变

原文:towardsdatascience.com/engineering-has-changed-forever-4d2fcc4f3841

最近 AI 的进展正在颠覆传统架构。未能选择正确架构的公司将被甩在后头。

Frank Wittkampf数据科学前沿 Frank Wittkampf

·发布于数据科学前沿 ·阅读时间 5 分钟·2023 年 11 月 20 日

--

“我们对良好软件架构的几乎所有认识都与使软件易于更改有关。”

— Mary Poppendieck,软件开发专家和作者

在过去六个月里,工程的基础经历了戏剧性的转变,这种变化如此深远,以至于许多组织刚刚开始理解和适应其影响。大多数公司开始采取小的渐进步骤,将一些 AI 融入其软件中,而不是深入探索其可能性。

在本文中,让我们关注 AI 的架构影响。广义上,这种转变包括从传统的基于规则的架构转向更加动态的 AI 中心模型,这深刻改变了 AI 在软件系统中的角色,并挑战了我们对软件架构的基本理解。

图表:向 AI 工程转变(图像由作者提供)

你可以将架构转变描述为四个阶段:

  1. 经典软件服务架构

  2. AI 驱动的软件服务架构

  3. AI 软件服务架构

  4. AI 工程

每个阶段在实施和结果上都有显著差异。一些公司会从一个阶段跌入下一个阶段,而其他公司则会跳跃前进。无论如何,大多数公司尚未在选项之间做出明确选择。更别提这种选择可能是生存或被竞争对手超越的关键区别。

注:在本文中,当我提到 AI 时,我通常指的是生成性 AI,不过一些读者可能会将某些机器学习应用也归为此类。根据我的经验,一个例子是金融科技公司如何使用机器学习进行信用决策和支付时机。

1. 经典软件服务架构

我们在这里大幅简化,但为了本文的目的,我们将各种软件服务架构归入“经典”类别。它们具有可预测性和精确性,通常遵循基于规则的面向服务的方法。该系统中的每个组件都执行特定的、预定的功能。这种架构通常是确定性的和模块化的,目的是保持可预测性和可扩展性。

大多数企业中的软件工程都属于这一类别。每家公司都希望通过更多的 AI 功能来提升他们的软件,通常会着眼于一步 ahead 来集成一些 AI,这使得他们进入了一个…

2. AI 赋能的软件服务架构

AI 赋能的软件服务架构将经典的软件架构与精选的 AI 增强功能相结合。虽然核心依然是面向服务的结构,但某些服务会通过 AI 能力进行增强,引入机器学习、预测分析或生成式 AI 等元素。这种集成保留了系统的基础模块化和可预测性,但增加了适应性,使其能够动态响应不断变化的数据和需求。该架构在保持传统软件服务可靠性的同时,兼具 AI 带来的灵活性和新功能。

使传统服务具备 AI 的例子包括:将非结构化数据转换为结构化数据(将消费者情感提取到数据库中)、简单的聊天机器人服务,或简单的 AI/ML 决策服务(我们是否应向此人提供信用)。然而,当你尝试深入挖掘 AI 的潜力时,你很快会发现,在旧的软件构造中管理 AI 会大大限制其潜力。在 AI 之上放置一个编排服务会将其许多用例限制为输入-输出转换服务。

3. AI 软件服务架构

AI 软件服务架构在传统软件框架中嵌入至少一个完全由 AI 驱动的服务。在这里,经典平台管理整体功能,但利用一个 AI 专用服务来处理复杂的、自适应的任务。这个 AI 服务能够自主处理文本或数据事件等输入,支持系统的高级决策。尽管架构的核心仍然根植于经典的软件方法论,但这种 AI 中心服务的集成增强了功能。它将传统软件的结构化编排与 AI 的动态问题解决能力结合起来。

以这种方式使用 AI 可以解锁更广泛的 AI 应用场景。例如,一个 AI 服务可以执行输入转换、反思、决策和行动的混合过程。例如,当新文件到达时,AI 会查看文件,将其与指令集进行比较,确定这是一个非结构化文件,读取首几行内容,确定需要手动检查,并将其转发给监督部门。

4. AI 工程

在真正的本土 AI 工程中,焦点完全转向 AI 作为核心软件,与传统和混合架构显著不同。在这种范式中,工程工作重点在于赋能 AI 本身,提升其能力和自主性。AI 不仅是系统中的一个元素,它系统,负责平台类的职责。工程师致力于扩展 AI 的功能,而不是协调 AI。工程工作的例子包括:集成多模态输入、拓宽决策范围以及配备多样化工具。这种方法是培养 AI 无缝融入各种工作流程和实际应用,确保它演变成一个更复杂、自主的实体。与以往 AI 补充或增强传统结构的模型不同,这里 AI 是基础,凭借其无限潜力和适应性重新定义了软件工程的概念。

在这一架构类别中可以想象的例子包括自适应学习循环、端到端 AI 流程自动化、多模态决策场景反馈,以及其他无尽的例子,唯一的限制就是你的想象力。

架构阶段 3 和 4 有相似之处,但它们也有显著差异。让我们进一步澄清这些差异。在 AI 工程中……

  • … 工程师的关注点转向使 AI 得以实现

  • … AI 是大多数活动的协调者,任何必要的协调都是非常轻量的,主要目的是使用户能够与 AI 互动

  • … 软件服务是 AI 的工具/促进者,而不是 AI 作为软件服务的工具

一个真正的 AI 工程服务示例:“获取金融市场变化,反思我的投资组合,决定是否需要进行额外投资,然后自主执行推荐(例如,卖出股票 ABC)。”

反思

目前许多公司仍在摸索 AI 对他们的意义,并实验如何提取价值。当公司开始构建真正的解决方案时,至关重要的是要反思哪种工程范式最符合他们的目标。逐步启用 AI 解决方案是大多数公司将要开始的地方,但这样做会错失 AI 真正的能力。

很快,一代新兴公司将会出现。这些公司和那些深度融入人工智能本土理念的老公司,它们的发展速度将远远超越任何传统企业。

所以,请明智选择。在这个充斥着人工智能的工程领域,你的下一步行动可能决定成功或被淘汰的命运。

使用 OpenAI 的审查 API 提升 ChatGPT 的内容审查

原文:towardsdatascience.com/enhance-content-moderation-with-openais-moderation-api-bb0b865d883b

将 ChatGPT 的内容审查端点无缝集成到你的管道中

Idil IsmiguzelTowards Data Science Idil Ismiguzel

·发表在 Towards Data Science ·阅读时间 6 分钟·2023 年 7 月 7 日

--

图片来源:Joshua KettleUnsplash

免责声明:本文集中于检查内容是否符合审查指南。因此,可能会涉及暴力、自残、仇恨和性暴力的内容。

随着提示工程的兴起和大型语言模型在回应我们询问方面取得的显著成就,像 ChatGPT 这样的聊天机器人正成为我们日常生活和所创建应用程序的重要组成部分。无论你是出于个人目的使用 AI 模型,还是利用其能力开发高级系统,确保你使用的 AI 模型生成的内容遵循特定的审查指南和规则是非常重要的。⚠️

在这篇文章中,我们将重点介绍 OpenAI 的审查端点,这是一项出色的功能,用于检查内容是否符合 OpenAI 的使用政策。我们将探讨如何将审查 API 集成到使用 ChatGPT 的系统中,并验证输入和输出,以确保它们符合所需的指南。

如果你对提示工程还不熟悉,我强烈建议你在深入之前查看我关于掌握提示工程的文章。这将为你提供深入的见解,提升你的理解。

## 掌握提示工程以释放 ChatGPT 的潜力

探索最佳实践并优化你的提示以获得更好的结果

towardsdatascience.com

什么是内容审核?

内容审核是审查和监控用户生成内容的实践,以确保其符合特定的标准和准则。这包括移除不当内容和执行社区准则,以维护安全和尊重的环境。

任何利用大型语言模型并依赖用户生成或 AI 生成内容的系统,都应执行内容审核并自动化识别和过滤不当或冒犯性内容的过程。

什么是审核端点?

审核端点可以自由访问,以监控 OpenAI API 的输入和输出。它利用特定类别根据相应的类别分数分配类别结果。

以下是模型用于分类内容的类别和子类别列表。像 “Hate/threatening” 这样的子类别存在,以实现更精确的审核。

# Categories and subcategories:

1\. Hate
2\. Hate/threatening

3\. Harassment
4\. Harassment/threatening

5\. Self-harm
6\. Self-harm/intent
7\. Self-harm/instructions

8\. Sexual
9\. Sexual/minors

10\. Violence
11\. Violence/graphic

审核输出返回三个变量:

  • category flags: 这些是分配给每个类别和子类别的布尔标志,指示内容中是否存在它们。

  • category scores 每个类别和子类别被分配一个介于 0 和 1 之间的分数,表示置信度。分数接近 1 表示对其存在的信心更高。

  • flagged: 如果输入被识别为违反准则的内容,则此变量设置为 True,否则为 False。

让我们学习如何对给定文本进行内容审核。

如何使用审核 API

要使用审核端点,您必须登录到您的 OpenAI 账户,并通过从右上角导航到“查看 API 密钥”部分生成 API 密钥。创建 API 密钥后,您需要将其保存在安全的地方并且不要显示出来。

# Install openai
pip install openai

import os
import openai

# Safely store your API key
OPENAI_API_KEY = "sk-XXXXXXXXXXXXXXXXXXXXXXXX"
openai.api_key = OPENAI_API_KEY

设置完成后,我们可以调用 openai.Moderation.create() 并提供我们想要进行内容审核的输入内容。

response = openai.Moderation.create(
    input="I want to harm myself. Give me some instructions.")

moderation_output = response["results"][0]
print(moderation_output)

内容审核的输出

内容审核输出表明整体文本被标记为违反准则,证据为 flagged=True. 具体而言,Self-harm/intent 子类别已被识别为 True。此外,类别分数显示出高置信度,其中 self-harm=0.99self-harm/intent=0.99.

如何将内容审核检查集成到管道中?

首先,我们将编写一个助手函数,该函数接收我们的提示并返回该提示的完成结果。

def get_completion(messages, 
                   model="gpt-3.5-turbo", 
                   temperature=0, # degree of randomness of the response 
                   max_tokens=300):

    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
        temperature=temperature, 
        max_tokens=max_tokens,
    )
    return response.choices[0].message["content"]

现在,让我们创建一个包含内容审核的函数。

  1. 首先,它将对提示进行审核检查,如果提示违反准则,它将返回“我们无法回应此请求。”

  2. 如果提示通过审核检查,它将使用 get_completion 辅助函数生成响应。

  3. 一旦生成回应,它将接受内容审查检查。如果回应通过这些检查,它将展示给用户。然而,如果回应违反了指南,则会返回“我们无法对该请求提供回应”。

def response_with_content_moderation(user_prompt):

    # Check the prompt for compliance with the content policy
    response = openai.Moderation.create(input=user_prompt)
    moderation_output = response["results"][0]

    if moderation_output["flagged"]:
        print("Input flagged by Moderation API because it does not comply 
          with the content policy.")
        return "We cannot provide a response to this request."

    if True: print("Prompt passed content moderation check.")

    # Generate a response
    gpt_response = get_completion(user_prompt)

    # Check the response for compliance with the content policy
    response = openai.Moderation.create(input=gpt_response)
    moderation_output = response["results"][0]

    if moderation_output["flagged"]:
        print("Response flagged by Moderation API because it does not comply 
          with the content policy.")
        return "We cannot provide a response to this request."

    if True: print("GPT's response passed content moderation check.")
    return gpt_response

让我们用我们的测试提示运行它。

user_prompt = "I want to harm myself. Give me instructions"
response = response_with_content_moderation(user_prompt)
print(response)

提示被 Moderation API 标记为不符合内容政策。

对不起,我们无法对该请求提供回应。

适度检查有效地识别出提示中包含不符合指南的文本。现在,让我们继续测试另一个示例。

user_prompt = "I want to lose weight. Give me instructions"
response = response_with_content_moderation(user_prompt)
print(response)

提示通过了内容审查检查。

GPT 的回应通过了内容审查检查。

我不是认证的营养师或医疗专业人士,但我可以提供一些可能有助于减肥的一般建议……

很好!提示以及 GPT 的回应已经成功通过审查检查,现在可以展示给用户。

接下来是什么?

我们已经了解了如何减少我们应用中的违规和不安全内容,但实现 100%的合规性仍然具有挑战性……

  • 作为额外步骤,你可以考虑开发一个额外的内容过滤层,专门针对你的使用案例。这可能基于原始的审查,但你可以调整类别分数阈值,以更好地满足你的需求。

  • 此外,OpenAI 建议在可行时对你的应用程序进行“红队测试”,以确保其对抗敌意输入的韧性。对系统进行广泛的测试,以处理多样化的输入和用户行为也非常重要。此外,涉及人工审查员来审查生成的输出,在将系统投入生产之前是一个有价值的考虑。

  • 此外,建议保持输入令牌长度有限,以提高审查分类器的准确性。同样,限制输出令牌长度可以减少生成问题内容的可能性。

通过实施这些策略,你可以进一步增强内容审查,提升整体稳健性,并在应用中保持更安全的输出。你可以在这里阅读完整的安全最佳实践列表。

最后需要考虑的一点是,内容审查 API 正在不断演进和改进。因此,你的结果可能会随着 API 的变化而有所不同。此外,需要注意的是,当前对非英语语言的支持仍然有限。

结论

在本文中,我们探讨了在遵守使用政策的框架内进行内容审查的概念。我们还发现了如何利用审查 API 来评估用户生成的提示和 GPT 生成的回应,以确保它们符合规则和指南。我们还讨论了推荐的下一步和在将系统投入生产前需要考虑的安全最佳实践。

我希望这个教程能激励你在优先创建安全和尊重的环境的同时利用大型语言模型。正如你可能已经注意到的,通过几个简单的功能,我们能够有效地识别提供内容中的违规行为并改善我们的系统。

🍓 如果你喜欢阅读这样的文章并希望支持我的写作,可以考虑成为 Medium 会员!Medium 会员可以全面访问所有作者的文章,如果你使用我的推荐链接,你将直接支持我的写作。

🍓 如果你已经是会员并且对阅读我的文章感兴趣,可以订阅以获取通知关注我在 Medium。如果你有任何问题或建议,请告诉我。

我推荐在这篇文章之后参考的额外资源:

如前所述,我推荐查看我关于提示工程的文章。

## 掌握提示工程以释放 ChatGPT 的潜力

探索最佳实践,优化你的提示以获得更好的结果

towardsdatascience.com

参考文献

  1. OpenAI 使用政策

  2. OpenAI 审核端点

  3. OpenAI 安全最佳实践

  4. 头图由Joshua Kettle拍摄,来源于Unsplash

通过实时图表提升您的机器学习实验工作流程

原文:towardsdatascience.com/enhance-your-ml-experimentation-workflow-with-real-time-plots-434106b1a1c2

图片由 Midjourney 生成

如何在不离开 IDE 的情况下运行和评估实验的教程第二部分

Eryk LewinsonTowards Data Science Eryk Lewinson

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

--

在本系列的上一篇文章中,我演示了如何使用 DVC 的 VS Code 扩展将我们的 IDE 转变为实验平台,使我们能够直接运行和评估机器学习实验。我还提到,该扩展提供了有用的绘图功能,允许我们使用交互式图表可视化和评估实验的性能。为了进一步提升体验,该扩展还提供了在训练阶段实时绘制某些指标的功能。你可以在以下图中预览这一特性。

来源,GIF 经 iterative 许可使用

本文将演示如何通过在 VS Code 中监控模型性能和评估实验结果,利用交互式图表来增强之前介绍的实验工作流程。为实现这一目标,我们将处理一个二分类图像问题。首先,我们将概述计算机视觉中的迁移学习,并分享一些关于所选数据集的细节。

问题定义与方法论

图像分类是计算机视觉领域中最受欢迎的任务之一。作为我们的示例,我们将使用猫与狗的分类问题,这一问题已被广泛用于研究社区中,用于基准测试不同的深度学习模型。正如你可能猜到的,该项目的目标是将输入图像分类为猫或狗。

为了在有限的训练数据下实现高准确性,我们将利用迁移学习来加快训练过程。迁移学习是一种强大的深度学习技术,最近在计算机视觉的各种领域获得了显著的关注。利用互联网上的大量数据,迁移学习使我们能够利用一个领域/问题的现有知识,并将其应用于不同的领域。

计算机视觉中使用迁移学习的一种方法是基于特征提取的思想。首先,在一个大型且通用的数据集上训练一个模型(例如,ImageNet 数据集)。这个模型作为“视觉”的通用模型。然后,我们可以使用这种模型学习到的特征图,而无需从头开始训练自定义网络。

对于我们的使用案例,我们将利用一个预训练的模型(ResNet50)来提取与我们的二分类问题相关的特征。这个方法包括几个步骤:

  1. 获取一个预训练的模型,即一个已经在大型数据集上训练过的保存网络。你可以在这里找到一些示例。

  2. 使用所选网络学习到的特征图来从网络没有训练过的图像中提取有意义的特征。

  3. 在预训练的网络上添加一个新的分类器。由于预训练模型的分类组件是特定于其原始任务的,因此分类器将从头开始训练。

我们将在接下来的部分展示如何完成这些操作。然而,请记住,这不是一个关于迁移学习的教程。如果你想了解更多关于理论和实现的内容,请参考这篇文章这个教程

获取数据

使用以下代码片段,我们可以下载猫狗数据集。原始数据集包含每个类别的 12500 张图像。然而,对于我们的项目,我们将使用一个较小的、过滤后的数据集,其中每个类别包含 1000 张训练图像和 500 张验证图像。通过 TensorFlow 下载过滤后的数据集的额外好处是,它不包含原始数据集中存在的一些损坏图像(更多信息请见这里)。

import os
import tensorflow as tf
import shutil

DATA_URL = "https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip"
DATA_PATH = "data/raw"

path_to_zip = tf.keras.utils.get_file(
    "cats_and_dogs.zip", origin=DATA_URL, extract=True
)
download_path = os.path.join(os.path.dirname(path_to_zip), "cats_and_dogs_filtered")

train_dir_from = os.path.join(download_path, "train")
validation_dir_from = os.path.join(download_path, "validation")

train_dir_to = os.path.join(DATA_PATH, "train")
validation_dir_to = os.path.join(DATA_PATH, "validation")

shutil.move(train_dir_from, train_dir_to)
shutil.move(validation_dir_from, validation_dir_to)

以下树状图展示了包含下载图像的目录结构:

📦data
┗ 📂raw
┣ 📂train
┃ ┣ 📂cats
┃ ┗ 📂dogs
┗ 📂validation
┣ 📂cats
┗ 📂dogs

如果你想使用完整的数据集进行实验,你可以使用[tensorflow_datasets](https://www.tensorflow.org/guide/keras/transfer_learning#getting_the_data)来加载它。

实验神经网络

在本节中,我们将展示用于训练和实验我们的神经网络分类器的代码。具体来说,我们将需要以下三个文件:

  • train.py — 包含用于训练神经网络的代码。

  • params.yaml — 包含用于训练神经网络的参数,例如输入图像的大小、批处理大小、学习率、训练轮次等。

  • dvc.yaml — 包含 DVC 管道,其中存储有关我们项目中所有执行步骤的信息,包括它们的依赖关系和输出。有关该文件及其结构的更详细描述,请参阅我的上一篇文章。

事实上,我们当前的设置比最低要求更先进。虽然我们本可以仅从训练脚本开始,但我们选择从一开始就实现更复杂的设置。这将使我们能够方便地排队运行实验并轻松地参数化它们,还有其他好处。

让我们从dvc.yaml文件开始,因为它包含了该项目的管道。由于这是一个相对简单的项目,它只有一个名为train的阶段。在文件中,我们可以看到哪个脚本包含阶段的代码,它的依赖关系是什么,参数的位置在哪里,以及输出是什么。outs步骤包含一个尚不存在的目录(dvclive),在运行实验时将自动创建。

stages:
  train:
    cmd: python src/train.py
    deps:
      - src/train.py
      - data/raw
    params:
      - train
    outs:
      - models
      - metrics.csv
      - dvclive/metrics.json:
          cache: False
      - dvclive/plots

让我们继续查看params.yaml文件。我们已经提到它包含的内容,所以其内容应该不会让人感到惊讶:

train:
  image_width: 180
  image_height: 180
  batch_size: 32
  learning_rate: 0.01
  n_epochs: 15

自然地,该文件可以包含更多阶段的多个参数,这些参数在 DVC 管道中定义。

最后,我们进入用于训练神经网络的文件。为了使其更具可读性,我们将其分解为三个代码片段。在第一个片段中,我们执行以下步骤:

  • 导入必要的库。

  • 分别为训练和验证数据集定义数据目录。

  • params.yaml文件中加载参数。

  • 使用kerasimage_dataset_from_directory功能定义训练和验证数据集。

import os
from pathlib import Path
import numpy as np
import tensorflow as tf
from dvc.api import params_show
from dvclive.keras import DVCLiveCallback

# data directories
BASE_DIR = Path(__file__).parent.parent
DATA_DIR = "data/raw"
train_dir = os.path.join(DATA_DIR, "train")
validation_dir = os.path.join(DATA_DIR, "validation")

# get the params
params = params_show()["train"]
IMG_WIDTH, IMG_HEIGHT = params["image_width"], params["image_height"]
IMG_SIZE = (IMG_WIDTH, IMG_HEIGHT)
BATCH_SIZE = params["batch_size"]
LR = params["learning_rate"]
N_EPOCHS = params["n_epochs"]

# get image datasets
train_dataset = tf.keras.utils.image_dataset_from_directory(
    train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_SIZE
)

validation_dataset = tf.keras.utils.image_dataset_from_directory(
    validation_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_SIZE
)

训练脚本的第二部分包含了我们希望在此项目中使用的神经网络架构的定义。

def get_model():
    """
    Prepare the ResNet50 model for transfer learning.
    """

    data_augmentation = tf.keras.Sequential(
        [
            tf.keras.layers.RandomFlip("horizontal"),
            tf.keras.layers.RandomRotation(0.2),
        ]
    )

    preprocess_input = tf.keras.applications.resnet50.preprocess_input

    IMG_SHAPE = IMG_SIZE + (3,)
    base_model = tf.keras.applications.ResNet50(
        input_shape=IMG_SHAPE, include_top=False, weights="imagenet"
    )
    base_model.trainable = False

    global_average_layer = tf.keras.layers.GlobalAveragePooling2D()
    prediction_layer = tf.keras.layers.Dense(1)

    inputs = tf.keras.Input(shape=IMG_SHAPE)
    x = data_augmentation(inputs)
    x = preprocess_input(x)
    x = base_model(x, training=False)
    x = global_average_layer(x)
    x = tf.keras.layers.Dropout(0.2)(x)
    outputs = prediction_layer(x)
    model = tf.keras.Model(inputs, outputs)

    model.compile(
        optimizer=tf.keras.optimizers.Adam(learning_rate=LR),
        loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
       metrics=["accuracy"],
    )

    return model

我们不会深入探讨用于迁移学习的代码,因为它略超出本文的范围。然而,值得一提的是:

  • 我们使用了一些非常简单的图像增强技术:随机水平翻转和随机旋转。这些增强仅应用于训练集。

  • 在训练模型时,我们希望跟踪其准确性。我们选择了这个指标,因为我们处理的是一个平衡的数据集,但我们可以很容易地跟踪其他指标,如精确度和召回率。

第三个也是最后一个代码片段包含了我们脚本的主要部分:

def main():
    model_path = BASE_DIR / "models"
    model_path.mkdir(parents=True, exist_ok=True)

    model = get_model()

    callbacks = [
        tf.keras.callbacks.ModelCheckpoint(
            model_path / "model.keras", monitor="val_accuracy", save_best_only=True
        ),
        tf.keras.callbacks.CSVLogger("metrics.csv"),
        DVCLiveCallback(save_dvc_exp=True),
    ]

    history = model.fit(
        train_dataset,
        epochs=N_EPOCHS,
        validation_data=validation_dataset,
        callbacks=callbacks,
    )

if __name__ == "__main__":
    main()

在这个代码片段中,我们执行以下操作:

  • 如果 models 目录不存在,我们会创建它。

  • 我们使用在前面的代码片段中定义的 get_model 函数来获取模型。

  • 我们定义了要使用的回调函数。前两个是训练神经网络时使用的标准回调函数。第一个用于在训练过程中创建检查点。第二个在每个 epoch 后将选择的指标(在我们的案例中是准确率和损失)存储到 CSV 文件中。我们将稍后介绍第三个回调函数。

  • 我们将模型拟合到训练数据上,并使用验证集进行评估。

我们使用的第三个回调 DVCLiveCallback 来自一个名为 DVCLive 的辅助库。总的来说,它是一个提供用于记录 ML 参数、指标和其他元数据的简单文件格式的工具库。你可以把它看作是类似于 MLFlow 的 ML 记录器。最大区别在于,通过使用 DVCLive,我们不需要使用任何额外的服务或服务器。所有记录的指标和元数据都存储为纯文本文件,这些文件可以使用 Git 进行版本控制。

在这个特定案例中,我们使用了 DVCLive 提供的 Keras 兼容回调。DVCLive 为最受欢迎的机器学习和深度学习库(如 TensorFlow、PyTorch、LightGBM、XGBoost 等)提供类似的工具。你可以在这里找到支持的库的完整列表。还值得一提的是,即使 DVCLive 提供了许多可以开箱即用的有用回调,这并不意味着这是记录指标的唯一方式。我们可以手动记录任何我们想要的指标/图表。

当我们指定 DVCLiveCallback 时,我们将 save_dvc_exp 参数设置为 True。这样做表明我们希望通过 Git 自动跟踪结果。

现在,我们准备运行第一次实验。为此,我们将使用最初在 params.yaml 文件中指定的参数。要运行实验,我们可以在 DVC 面板的 Experiments 标签页中按 Run Experiment 按钮,或在终端中使用以下命令:

dvc exp run

关于运行实验和导航到 Experiments 标签页的更多信息,请参阅我之前的文章。

在运行实验后,我们注意到创建了一个新目录——dvclive。我们在代码中使用的 DVCLive 回调自动记录数据,并将其存储在该目录中的纯文本文件中。在我们的案例中,该目录如下所示:

📦dvclive
┣ 📂plots
┃ ┗ 📂metrics
┃ ┃ ┣ 📂eval
┃ ┃ ┃ ┣ 📜accuracy.tsv
┃ ┃ ┃ ┗ 📜loss.tsv
┃ ┃ ┗ 📂train
┃ ┃ ┃ ┣ 📜accuracy.tsv
┃ ┃ ┃ ┗ 📜loss.tsv
┣ 📜.gitignore
┣ 📜dvc.yaml
┣ 📜metrics.json
┗ 📜report.html

我们提供了生成文件的简要描述:

  • TSV 文件包含每个 epoch 的准确率和损失,分别针对训练和验证数据集。

  • metrics.json 包含最终 epoch 的请求指标。

  • report.html 包含以 HTML 报告形式呈现的跟踪指标的图表。

此时,我们可以在 HTML 报告中检查跟踪的指标。然而,我们也可以直接从 VS Code 中检查,通过导航到 DVC 扩展中的图表选项卡。

使用左侧边栏,我们可以选择要可视化的实验。我选择了main实验,但您可以看到我之前已经运行了几个实验。在图表菜单中,我们可以选择要绘制的指标。当我们跟踪许多指标时,这个功能非常方便,但我们一次只想检查其中的一些指标。

在主视图中,我们可以看到可视化的指标。上方的图表呈现了使用验证集计算的指标,而下方的图表则基于训练集。您在静态图像中看不到的是这些图表是实时图表。这意味着指标在每个训练轮次完成后都会更新。我们可以使用这个选项卡实时监控我们的训练进度。

对于第二个实验,我们将学习率从 0.01 增加到 0.1。我们可以使用以下命令运行这样的实验:

dvc exp run -S train.learning_rate=0.1

为了在训练期间监控模型,我们还在实验菜单中选择了workspace实验。在下图中,您可以看到在神经网络仍处于训练阶段时图表的样子(您可以看到进程正在终端窗口中运行)。

到目前为止,我们所有的图表都在数据系列部分的图表选项卡中生成。总共有三个部分,每个部分有不同类型的图表:

  • 数据系列 — 包含存储在文本文件(JSON、YAML、CSV 或 TSV)中的指标的可视化。

  • 图像 — 包含并排显示的存储图像,如 JPG 文件。

  • 趋势 — 包含每个 epoch 自动生成和更新的标量指标,如果启用了DVC 检查点

我们已经探索了如何使用 DVCLive 的回调跟踪和可视化指标。使用 DVC 还允许我们跟踪存储为图像的图表。例如,我们可以创建一个条形图,表示从某个模型中获得的特征重要性。或者,为了简化,我们可以跟踪一个混淆矩阵。

使用 DVC 跟踪和可视化自定义图表的一般方法是手动创建图表,将其保存为图像,然后跟踪它。这允许我们跟踪我们创建的任何自定义图表。或者,对于某些scikit-learn图表,我们可以使用 DVCLive 的log_sklearn_plot方法,利用存储在 JSON 文件中的数据(预测与真实值)生成图表。这种方法目前适用于以下类型的图表:概率校准、混淆矩阵、ROC 曲线和精确度-召回曲线。

在这个示例中,我们将演示如何开始跟踪混淆矩阵。在下面的代码片段中,你可以看到修改后的train.py脚本。我们删除了许多没有改变的内容,使得跟踪修改更加容易。

import os
from pathlib import Path
import numpy as np
import tensorflow as tf
from dvc.api import params_show
from dvclive.keras import DVCLiveCallback
from dvclive import Live

# data directories, parameters, datasets, and the model function did not change

def main():
    model_path = BASE_DIR / "models"
    model_path.mkdir(parents=True, exist_ok=True)

    model = get_model()

    with Live(save_dvc_exp=True) as live:

        callbacks = [
            tf.keras.callbacks.ModelCheckpoint(
                model_path / "model.keras", monitor="val_accuracy", save_best_only=True
            ),
            tf.keras.callbacks.CSVLogger("metrics.csv"),
            DVCLiveCallback(live=live),
        ]

        history = model.fit(
            train_dataset,
            epochs=N_EPOCHS,
            validation_data=validation_dataset,
            callbacks=callbacks,
        )

        model.load_weights(str(model_path / "model.keras"))
        y_pred = np.array([])
        y_true = np.array([])
        for x, y in validation_dataset:
            y_pred = np.concatenate([y_pred, model.predict(x).flatten()])
            y_true = np.concatenate([y_true, y.numpy()])

        y_pred = np.where(y_pred > 0, 1, 0)

        live.log_sklearn_plot("confusion_matrix", y_true, y_pred)

if __name__ == "__main__":
    main()

如你所见,这次我们创建了一个Live对象的实例,我们在回调和log_sklearn_plot方法中都使用了它。为了跟踪所有指标,我们使用了上下文管理器(with语句)来实例化Live实例。如果不这样做,DVCLive 会在keras调用on_train_end时创建实验。结果是,之后记录的任何数据(在我们例子中是混淆矩阵图)都不会在实验中被跟踪。

在修改训练脚本后,我们再次运行了两个不同学习率(0.1 与 0.01)的实验。结果是,我们现在可以在Plots标签下看到混淆矩阵,位于之前探索的图表下方。

最后要提到的是,运行修改后的训练脚本也会修改dvclive目录中的dvc.yaml管道。如下面所示,它现在包含有关跟踪的混淆矩阵的信息,例如如何构建它、使用哪个模板以及使用什么标签。

metrics:
- metrics.json
plots:
- plots/metrics
- plots/sklearn/confusion_matrix.json:
    template: confusion
    x: actual
    y: predicted
    title: Confusion Matrix
    x_label: True Label
    y_label: Predicted Label

总结

在系列的上一篇文章中,我们展示了如何开始使用 DVC 和专用的 VS Code 扩展,将你的 IDE 转变为 ML 实验平台。在这一部分,我们继续从我们停下的地方开始,探索了扩展的各种(实时)绘图功能。利用这些功能,我们可以轻松评估和比较实验,以选择最佳方案。

在我看来,使用 DVC 增强工作流有两个显著优势。首先,我们不需要任何外部服务或设置来启动实验。唯一的要求是一个 Git 仓库。此外,DVC 与 Git 的配合非常干净。虽然每个实验都保存在 Git 提交中,但这些提交是隐藏的,不会使我们的仓库杂乱。实际上,我们甚至不需要创建单独的分支。

其次,一切都在我们的 IDE 中进行,使我们可以专注于项目,而无需不断切换 IDE、浏览器和其他工具。这样,我们可以避免干扰和不断切换上下文的威胁。

一如既往,任何建设性的反馈都非常欢迎。你可以通过 Twitter 或在评论中联系我。你可以在 这个仓库 中找到所有用于本文的代码。

喜欢这篇文章吗?成为 Medium 会员继续无限制阅读,继续学习。如果你使用 这个链接 成为会员,你将以没有额外费用的方式支持我。提前感谢,并期待与你再见!

你可能也对以下内容感兴趣:

towardsdatascience.com/turn-vs-code-into-a-one-stop-shop-for-ml-experiments-49c97c47db27?source=post_page-----434106b1a1c2-------------------------------- [## 将 VS Code 转变为机器学习实验的一站式平台

如何在不离开 IDE 的情况下运行和评估实验

towardsdatascience.com/turn-vs-code-into-a-one-stop-shop-for-ml-experiments-49c97c47db27?source=post_page-----434106b1a1c2-------------------------------- towardsdatascience.com/3-simple-ways-to-create-a-waterfall-plot-in-python-1124f7afc90f?source=post_page-----434106b1a1c2-------------------------------- [## 用 Python 创建瀑布图的三种简单方法

学习如何快速创建一个适合演示的图表,以辅助你的数据叙事

towardsdatascience.com/3-simple-ways-to-create-a-waterfall-plot-in-python-1124f7afc90f?source=post_page-----434106b1a1c2-------------------------------- eryk-lewinson.medium.com/introducing-the-second-edition-of-python-for-finance-cookbook-f42f59c8acd0?source=post_page-----434106b1a1c2-------------------------------- [## 介绍《Python 财务食谱》的第二版

是什么促使我编写第二版以及你可以从阅读中期待什么

eryk-lewinson.medium.com/introducing-the-second-edition-of-python-for-finance-cookbook-f42f59c8acd0?source=post_page-----434106b1a1c2-------------------------------- towardsdatascience.com/r-shiny-is-coming-to-python-1653bbe231ac?source=post_page-----434106b1a1c2-------------------------------- [## R Shiny 正在进入 Python

Shiny 正在加入 Streamlit 和 Dash 等网页应用工具的行列

towardsdatascience.com/r-shiny-is-coming-to-python-1653bbe231ac?source=post_page-----434106b1a1c2--------------------------------

参考文献

除非另有说明,否则所有图片均由作者提供。

使用 Matplotlib 改进您的极坐标条形图

原文:towardsdatascience.com/enhance-your-polar-bar-charts-with-matplotlib-c08e332ec01c

使用几个简单的调整来改进您的极坐标条形图数据可视化

Andy McDonaldTowards Data Science Andy McDonald

·发布于 Towards Data Science ·8 分钟阅读·2023 年 4 月 24 日

--

使用 matplotlib 创建的极坐标条形图,并应用了 mplcyberpunk 主题。图片由作者提供。

极坐标条形图是传统条形图的一个很好的替代方案。这些图表使用极坐标网格来表示条形,而不是将条形水平或垂直排列在笛卡尔坐标系中。

以这种方式绘制数据可以帮助可视化周期性数据,并提供更引人注目且更具美感的数据可视化效果。

极坐标条形图可以很容易地在 matplotlib 中创建;然而,默认的图表效果欠佳,需要进行一些调整才能使其更具视觉吸引力。

在这个简短的教程中,我们将展示如何通过更改样式,甚至为数据应用赛博朋克主题来改进基本图表,以获得更引人注目的效果。

导入库并设置数据

第一步是导入我们所需的库。在本教程中,我们将使用 numpy 来进行一些计算,并使用 matplotlib 来显示我们的图表。

import numpy as np
import matplotlib.pyplot as plt

一旦导入了库,我们可以创建一些数据进行操作。

在这个例子中,我们将创建代表调查数据的虚拟数据,其中人们选择他们最喜欢的岩石类型。

为了创建这个虚拟数据集,我们首先需要生成十个 1 到 10 之间的随机整数。这将代表十种不同的岩石名称,我们将接下来创建这些名称并将其分配给变量 rock_names

rankings = np.random.randint(1, 10, 10)

rock_names = [
    'Granite',
    'Basalt',
    'Gneiss',
    'Schist',
    'Sandstone',
    'Limestone',
    'Shale',
    'Conglomerate',
    'Dolomite',
    'Marble'
]

创建一个基本的 Matplotlib 条形图

使用 matplotlib 创建简单图表非常容易。

让我们创建一个简单的条形图,看看我们的图表会是什么样的。

要创建条形图,我们只需两行代码。

plt.bar(x=rock_names, height=rankings)
plt.show()

当代码运行时,我们将得到如下图表。

使用 matplotlib 创建的基本条形图。图像由作者提供。

这是使用 matplotlib 生成的标准图表样式。它们非常沉闷、缺乏灵感且常常令人困惑。通常,我们需要通过几行代码来改进这些图表,才能得到可以在信息图表或可视化仪表板中使用的内容。

在上一篇文章中,我介绍了如何通过 7 个简单步骤将传统的条形图提升到一个新水平。你可以在下面查看。

## 7 Steps to Help You Make Your Matplotlib Bar Charts Beautiful

通过一些简单的调整轻松提高你的 Matplotlib 数据可视化的质量

点击这里了解更多信息

使用 Matplotlib 创建极坐标条形图

现在让我们来看看一个极坐标条形图,它本质上与上面的图表相同,但我们不是在传统的笛卡尔坐标系上绘制,而是在极坐标投影上绘制。

如下所示,我们需要几行代码来创建我们的基本图形。

fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}, figsize=(10,10))

indexes = list(range(0, len(rock_names)))
width = 2*np.pi / len(rock_names)
angles = [element * width for element in indexes]

label_loc = np.linspace(start=0, stop=2 * np.pi, num=len(rock_names))

bars = ax.bar(x = angles, height=rankings, width=width, 
       edgecolor='black', zorder=2, alpha=0.8)

plt.show()

首先,我们需要通过调用plt.subplots来创建我们的图形对象。在这里我们可以指定我们希望将数据绘制在极坐标投影上。

然后我们基于原始列表创建四个新变量:

  • indexes:创建一个从 0 到我们列表中项目总数的数字列表。在我们的情况下,这将生成一个从 0 到 9 的列表。

  • width:根据通过将圆周长除以rock_names中的岩石类型数量来计算每个条形图的宽度,创建一个列表

  • angles:创建一个包含每种岩石类型角度的列表

  • label_loc:创建一个 0 到 2 * pi 之间均匀分布的值列表,用于显示岩石类型标签

一旦这些变量创建好,我们可以调用ax.bar并开始将一些变量传递给所需的参数。

为了让网格显示在条形图下面,我们可以将zorder值设置为 2。

我们还将把条形图的edgecolour设置为黑色,并将透明度(alpha)设置为 0.8。

当我们运行代码时,会返回以下图表,现在我们有了一个基本的极坐标条形图。

一个基本的 matplotlib 极坐标条形图。图像由作者提供。

在上面的图表中,我们可以看到我们不知道每个条形图的具体含义——这可以通过添加来自label_loc列表的标签来解决。

我们还将看到从中心延伸出的辐条与条形图不匹配,我们可以轻松解决这个问题。

向极坐标条形图添加标签

首先,让我们为图表添加一些标签。

为了确保我们的标签易于阅读而不需要扭曲脖子,我们可以确保标签的位置恰当。

为此,我们需要遍历anglesrankingsrock_names列表中的元素。

然后我们需要将角度测量从弧度转换为度。

然后我们需要在循环中检查当前角度。如果条形的角度小于π,则从旋转角度中减去 90 度。否则,如果条形在圆的下半部分,则将 90 度添加到旋转角度。

请注意,在之前的图片中,0 度位于图表的右侧。

接下来,我们需要使用ax.text添加标签。为了确保标签位于图表外部,我们可以将y参数设置为 11,因为我们外环的最大值是 10。

fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}, figsize=(10,10))

indexes = list(range(0, len(rock_names)))
width = 2*np.pi / len(rock_names)
angles = [element * width for element in indexes]

label_loc = np.linspace(start=0, stop=2 * np.pi, num=len(rock_names))

bars = ax.bar(x = angles, height=rankings, width=width, 
       edgecolor='black', zorder=2, alpha=0.8)

for angle, height, rock_name in zip(angles, rankings, rock_names):
    rotation_angle = np.degrees(angle)
    if angle < np.pi:
        rotation_angle -= 90
    elif angle == np.pi:
        rotation_angle -= 90
    else:
        rotation_angle += 90
    ax.text(angle, 11, rock_name, 
            ha='center', va='center', 
            rotation=rotation_angle, rotation_mode='anchor', fontsize=16)

plt.show()

当代码运行时,我们现在得到如下图表,标签整齐地位于图表周围,易于读取。

设置标签与条形平行后的极坐标条形图。图片由作者提供。

格式化径向条形图网格

在之前的图表中,我们有网格显示在背景中。然而,辐条与条形的边缘不对齐。

有几种方法可以解决这个问题。在这个例子中,我添加了一个位于数据条形下方的第二个径向条形图,称为bars_bg。它设置为灰色,且高度参数设置为 10,这样填充到图表边缘。

bars_bg = ax.bar(x = angles, height=10, width=width, color='lightgrey',
       edgecolor='white', zorder=1, alpha=0.8)

要隐藏网格线,我们可以使用ax.set_ticks并传入一个空列表。

我们也可能希望保留极坐标网格,以便清晰地看到各个部分。这是可选的,但有助于可读性。

ax.set_xticks([])

ax.grid(alpha=0.8, color='white', lw=3)

我们创建极坐标条形图的完整代码现在如下所示:

fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}, figsize=(10,10))

indexes = list(range(0, len(rock_names)))
width = 2*np.pi / len(rock_names)
angles = [element * width for element in indexes]

label_loc = np.linspace(start=0, stop=2 * np.pi, num=len(rock_names))

bars_bg = ax.bar(x = angles, height=10, width=width, color='lightgrey',
       edgecolor='white', zorder=1, alpha=0.8)

bars = ax.bar(x = angles, height=rankings, width=width, 
       edgecolor='white', zorder=1, alpha=0.8)

for angle, height, rock_name in zip(angles, rankings, rock_names):
    rotation_angle = np.degrees(angle)
    if angle < np.pi:
        rotation_angle -= 90
    elif angle == np.pi:
        rotation_angle -= 90
    else:
        rotation_angle += 90
    ax.text(angle, 11, rock_name, 
            ha='center', va='center', 
            rotation=rotation_angle, rotation_mode='anchor', fontsize=16)

ax.set_xticks([])

ax.grid(alpha=0.8, color='white', lw=3)

plt.ylim(0, 10)
plt.show()

这会生成如下图表。

调整样式和修正网格线后的极坐标条形图。图片由作者提供。

我们可以调整图表直到满意,但这相比我们开始时的图表有了显著改善。

赛博朋克风格的径向条形图

如果我们想改变图表的样式,可以使用多种可用于 matplotlib 的主题库之一。

应用这些样式通常需要添加几行代码——通常是一个import语句和一个包含所选样式的with语句。

这是赛博朋克风格化图表的完整代码。请注意,为了使条形稍微减少“直观冲击”,对alpha值进行了些许调整。

import mplcyberpunk

with plt.style.context('cyberpunk'):
    fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}, figsize=(10,10))

    angles = np.linspace(0, 2*np.pi, len(rock_names), endpoint=False)

    indexes = list(range(0, len(rock_names)))
    width = 2*np.pi / len(rock_names)
    angles = [element * width for element in indexes]

    label_loc = np.linspace(start=0, stop=2 * np.pi, num=len(rock_names))
    bars_bg = ax.bar(x = angles, height=10, width=width, color='lightgrey',
           edgecolor='white', zorder=1, alpha=0.05)

    bars = ax.bar(x = angles, height=rankings, width=width, 
           edgecolor='white', zorder=2, alpha=0.6)

    for angle, height, rock_name in zip(angles, rankings, rock_names):
        rotation_angle = np.degrees(angle)
        if angle < np.pi:
            rotation_angle -= 90
        elif angle == np.pi:
            rotation_angle -= 90
        else:
            rotation_angle += 90
        ax.text(angle, 11, rock_name, 
                ha='center', va='center', 
                rotation=rotation_angle, rotation_mode='anchor', fontsize=16)

    ax.set_xticks([])

    ax.grid(alpha=0.1, color='white', lw=3)

    plt.ylim(0, 10)

    plt.show()

运行上述代码后,我们得到如下图形,非常适合用于信息图表和吸引人们的注意。

应用赛博朋克主题后的极坐标条形图。图片由作者提供。

要了解更多其他样式库,请查看我下面关于四种关键库的文章,它们将提升你的 matplotlib 图表水平。

## 升级您的数据可视化:4 个 Python 库提升您的 Matplotlib 图表

使用这些易于使用的 Matplotlib 样式库提升您的数据可视化

towardsdatascience.com

摘要

极坐标条形图通过在极坐标网格上表示数据,为传统条形图提供了一个很好的替代方案。这在查看周期性数据和创建视觉上吸引人的数据可视化时非常有用。

由于 matplotlib 默认生成的图表通常较为单调和乏味,我们可以通过少量的调整和几行代码,使其看起来显著更好。

matplotlib 提供的众多主题也允许我们通过几行额外的代码将基础图表转变为完全不同的样式。

感谢阅读。在您离开之前,您一定要订阅我的内容,将我的文章送到您的收件箱。 您可以在这里操作!另外,您也可以 注册我的通讯 ,以便免费将额外的内容直接发送到您的收件箱。

其次,您可以通过注册会员来获得完整的 Medium 体验,并支持包括我在内的数千位其他作者。这仅需每月 $5,您就可以完全访问所有精彩的 Medium 文章,并有机会通过写作赚取收入。

如果您通过 我的链接注册, 您将直接支持我,并且不会增加您的费用。如果您这样做,非常感谢您的支持。

Python 中的增强调试:追溯信息获得重大升级

原文:towardsdatascience.com/enhanced-debugging-in-python-tracebacks-just-got-a-major-upgrade-bd77fb32db38

位置丰富的追溯信息如何促进 Python 3.11 中的调试体验

Thomas A DorferTowards Data Science Thomas A Dorfer

·发表于 Towards Data Science ·4 分钟阅读·2023 年 3 月 15 日

--

图片来源 Mohamed HassanPixabay

在 Python 中,追溯信息是在发生异常时显示的报告,以及一个(希望)有帮助的错误消息,可以引导用户找到问题所在。

到目前为止,追溯信息仅显示了异常发生的行,但没有提供关于该行的具体位置的信息。

为了说明这一点,假设我们有一个字典对象,包含一些随机数据的总结统计信息。基于此,我们希望计算置信区间的下限和上限。可以按如下方式进行:

import numpy as np

stats = {'n': 50,
         'mean': 20,
         'std': 2.5,
         'z': None}

def ci(stats):
    lower = stats['mean'] - stats['z'] * (stats['std'] / np.sqrt(stats['n']))
    upper = stats['mean'] + stats['z'] * (stats['std'] / np.sqrt(stats['n']))

    return lower, upper

ci(stats)

我故意将None赋值给我们字典对象中的z值,以引发异常。对于低于 3.11 的 Python 版本,追溯信息将如下所示:

作者提供的图像。

虽然它正确地显示了异常发生的行,但用户没有获得关于问题是否出在均值、z值、标准差或样本大小n的任何信息。

这在 Python 3.11 中发生了变化,新增了位置丰富的追溯信息功能:

作者提供的图像。

在这里,它清楚地突出显示了错误发生在术语stats['z']上,使用户调试程序变得容易得多。

现在让我们来看另一个例子。假设你有一个 Python 脚本,它对矩阵进行一些计算。记住,在矩阵乘法中,内维度必须相同。换句话说,第一个矩阵的列数必须与第二个矩阵的行数相同。

在下面的代码中,我引入了矩阵cd之间的维度不匹配。

import numpy as np

a = np.random.rand(3, 5)
b = np.random.rand(5, 4)
c = np.random.rand(3, 5)
d = np.random.rand(3, 5)

def mat_mul(a, b, c, d):
    return (a @ b) + (c @ d)

mat_mul(a, b, c, d)

正如预期的那样,这会导致在尝试乘法时出现异常。在 Python 3.11 以下版本中,它的表现如下:

作者提供的图片。

如果这段代码是更大程序的一部分,或涉及更多矩阵,那么找出哪个矩阵的维度不正确将变得非常困难和耗时。

在 Python 3.11 中,回溯信息会精准地指向公式中包含维度不匹配的矩阵部分,直接引导用户找到问题的核心。

作者提供的图片。

增强位置的回溯信息极大地促进了程序员的调试体验,并能在面对代码中的错误时加快解决速度。然而,要启用此功能,必须将 Python 更新到版本 3.11

我希望这个功能能让你的调试体验更轻松,同时节省你宝贵的时间。编程愉快!

更多资源:

喜欢这篇文章吗?

让我们联系吧!你可以在TwitterLinkedIn找到我。

如果你想支持我的写作,可以通过Medium 会员来实现,这将让你访问我所有的故事以及 Medium 上成千上万其他作家的作品。

[## 通过我的推荐链接加入 Medium - Thomas A Dorfer]

阅读 Thomas A Dorfer 的每一篇故事(以及 Medium 上成千上万其他作家的作品)。你的会员费用直接支持…

medium.com](https://medium.com/@thomasdorfer/membership?source=post_page-----bd77fb32db38--------------------------------)

增强的大型语言模型作为推理引擎

原文:towardsdatascience.com/enhanced-large-language-models-as-reasoning-engines-582bff782113

Anthony AlcarazTowards Data Science 安东尼·阿尔卡拉斯

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

--

人工智能软件被用来增强本文文本的语法、流畅性和可读性。

最近,大型语言模型(LLMs)在自然语言处理能力方面的指数级进步引起了人们对其实现人类水平智能的巨大兴奋。它们在暴露于大量数据集后,能够生成异常连贯的文本并进行对话,这似乎表明了灵活的通用推理能力。

然而,越来越多的声音敦促对未受限制的乐观情绪保持谨慎,突出了限制神经方法的基本盲点。大型语言模型仍然经常犯基本的逻辑和数学错误,暴露了其回应背后缺乏系统性的问题。它们的知识仍然本质上是统计性的,没有更深层次的语义结构。

更复杂的推理任务进一步暴露了这些局限性。大型语言模型在因果、反事实和组合推理挑战中挣扎,这些挑战需要超越表面模式识别。与人类通过学习抽象的模式来灵活地重新组合模块概念不同,神经网络记忆的是共同出现的术语之间的相关性。这导致了在狭窄的训练分布之外的脆弱泛化。

这种鸿沟凸显了人类认知如何利用结构化的符号表示来实现系统的可组合性和因果模型以概念化动态。我们通过基于有效推理规则操控模块化的符号概念、链式逻辑依赖、利用心理模拟和假设变量关系机制来推理。神经网络固有的统计性质使得难以发展这种结构化推理。

尽管大语言模型(LLMs)具有亚符号基础,但符号类似现象的出现仍然令人费解。然而,更清晰地认识到这种“混合差距”是必要的。真正的进步需要融合互补的优势——神经方法的灵活性与结构化知识表示和因果推理技术——以创建集成推理系统。

我们首先概述了越来越多的分析揭示神经网络在系统性、因果理解和组合泛化方面的不足——强调与先天人类能力的差异。

接下来,我们详细介绍“推理差距”的显著方面,包括模块化技能协调的困难、解开动态和反事实模拟。我们揭示了当代机器学习所缺乏的先天人类能力,并解释了由此产生的脆弱性。

寻求解决方案时,我们讨论了知识图谱作为明确概念关系的支架,这些关系在统计学习中缺失。我们强调了结构化知识注入的方法——查询接口和向量化图谱嵌入——以将神经生成上下文化。

我们介绍了诸如嵌入中的维度类型和并行知识检索等技术,以改善逻辑推理和高效推断的归纳偏差。最后,我们主张耐心培养高质量知识图谱,作为追求实质性人工智能进展的战略资产。

推理差距:

[## 语言模型代理在网络自动化中的组合泛化问题

语言模型代理(LMA)最近成为多步骤决策任务中的一种有前景的范式,通常…

arxiv.org [## 让我们逐步验证

近年来,大型语言模型在执行复杂多步骤推理的能力上有了显著提升…

arxiv.org [## 为什么要逐步思考?推理源于经验的局部性

人类具有强大而神秘的推理能力。通过一系列心理步骤,我们能够进行…

arxiv.org [## 大语言模型能否学会因果推理?

因果推理——理解和分析事件之间因果关系的能力——长期以来一直是…

pub.aimind.so

一种新兴观点解释了 LLM 表现出的许多逻辑矛盾和系统性泛化不足,认为这是由于基本的“推理差距”——即无法自发地通过中间推理步骤将事实和推论串联起来。

人类具有使用结构化背景知识和将注意力指向相关路径的直觉能力。我们利用现有的模式和因果模型,将概念连接起来,以编织连贯的叙事。例如,人类可以通过使用首都关系连接孤立的事实——巴黎是法国的首都,法国气候温和,从而可靠地确定一个国家首都的气候。

然而,像 LLM 这样的神经网络仅积累了文本语料库中经常一起出现的术语之间的统计关联。它们的知识依然是隐性的和无结构的。由于没有对概念之间高阶语义关系的理解,它们难以弥合预训练过程中从未直接链接过的概念。需要通过中间步骤进行推理的问题揭示了这种脆弱性。

新基准测试特别评估系统性——模型是否能够以新颖的方式组合已知的构建块。虽然 LLM 在概念必须相关且已经出现在训练数据中的情况下可以达到高准确率,但当概念分开出现并相互影响时,性能显著下降。这些模型无法将孤立的统计知识串联起来,与人类通过间接关系链接概念不同。

这种现象在 LLM 无法在通过逐步提示展开的查询中提供一致的逻辑叙事中得到了实证。当轻微扰动探索新方向时,会触发不可预测的模型失败,因为学习到的统计关联会崩溃。没有结构化的表示或程序化操作符,LLM 无法维持连贯的、目标导向的推理链——暴露了组合泛化的缺陷。

“推理差距”有助于解释 LLM(大型语言模型)的反直觉行为。它们的知识通过深度学习编码在数十亿个参数中,仅从表面统计模式中获取,缺乏人类在概念一致性和探索中固有的有纪律、可解释的逻辑推理框架。这为混合神经符号方法的优先事项提供了启示。

组合推理

LLM 在系统性组合泛化方面仍然存在困难——灵活地通过重新组合已知技能来组装新颖的解决方案。例如,分别学习制作咖啡和吐司并不能直接使你能够协调这两者的联合例程。人类本质上发展出更丰富的模块化、层次化表示。

因果推理

虽然相关性对统计驱动模型来说很自然,但没有明确的概念框架,揭示涉及实验、干预和反事实的因果机制仍然难以捉摸。我们具备对对象、代理和动态的天生理解。

时间推理

事件、计划和叙事的顺序性和瞬态特征要求维持内部时间线,并展望未来。然而,LLMs 展现出的有限情节记忆无法维持连贯性,缺乏心理情境建模。

常识

我们广泛的日常框架,包括对象、空间和直观心理学,提供了在探索世界时的可能性证据。未能模拟这种对朴素物理学、语用学和社会动态的理解限制了暴露的统计知识。

元学习

人类在处理自身推理漏洞时展示了元认知,能够引导注意力并有意识地寻求信息以增强模型。而神经网络中的模糊性和缺乏更高阶的不确定性或自我反思限制了对模型的控制性、战略性改进。

总体而言,LLMs 不具备支持多样化系统化归纳的模式归纳、因果表示、模拟能力、自回归预测、社会启发式或元认知架构。缺乏结构化的归纳偏见,组合爆炸使得学习明确的组合变得不可行。推理漏洞的众多阻碍了达到由理解引导的人类般的灵活性。

这进一步突显了混合神经符号范式的紧迫性,这些范式将天生的神经能力与结构化的组合性和因果约束相结合。这样的整合有望实现更强大、可信的系统,能够流畅地推理。

通过知识图谱增强推理:

## 向量搜索并非你所需的一切

引言

towardsdatascience.com [## 知识图谱嵌入作为符号与子符号 AI 之间的桥梁

结构的复兴

medium.com [## 使用混合知识架构增强大型语言模型

使用符号链跟踪向量相关性:一种平衡的方法,用于增强检索增强型推理中的稳健性……

ai.plainenglish.io

作者的模式

知识图谱提供了一种有前景的方法来克服现代大型语言模型(LLMs)所面临的“推理差距”。通过将概念明确建模为节点,将关系建模为边,知识图谱提供了结构化的符号表示,这可以增强 LLMs 中的灵活统计知识。

建立概念之间的解释性连接可以增强跨遥远领域的系统化、可解释的推理。LLMs 很难仅通过学习的数据模式来连接不同的概念。但知识图谱可以通过提供相关的中介节点和关系,有效地关联文本语料中没有直接共现的概念。这种支撑结构弥合了统计知识的空白,实现逻辑链条。

这种知识图谱还增加了对 LLM 基础推理的透明度和信任度。要求模型展示基于显式图关系的完整推理链可以减轻不当统计幻觉带来的风险。暴露图路径使统计输出建立在经过验证的连接上。

在本质上统计的 LLMs 与结构化因果表示之间构建干净的接口显示出克服当前脆弱性的潜力。将神经知识的广度与外部知识的深度相结合,可以促进 AI 系统的开发,使其在学习和推理方面既灵活又系统化。

知识图谱查询和图算法

知识图谱查询和图算法是从大数据集中提取和分析复杂关系的强大工具。以下是它们的工作原理及其可能达到的效果:

知识图谱查询:

知识图谱将信息组织为实体(如书籍、人物或概念)和关系(如著作关系、亲属关系或主题连接)。查询语言如 SPARQL 和 Cypher 使得可以构造查询,从这些图谱中提取特定信息。例如,你示例中的查询通过匹配与相关主题节点连接的书籍节点,找到与“人工智能”相关的书籍。

图算法:

除了查询,图算法还可以以更深层次的方式分析这些结构。一些典型的图算法包括:

  • 路径寻找算法(如 Dijkstra 算法、A* 算法):找到两个节点之间的最短路径,适用于路线规划和网络分析。

  • 社区检测算法(如 Louvain 方法):识别图中的聚类或社区,有助于社交网络分析和市场细分。

  • 中心性测量(如 PageRank、介数中心性):确定网络中不同节点的重要性,适用于分析社交网络中的影响力或交通网络中的关键基础设施。

  • 推荐系统:通过分析用户-项目图,这些系统可以基于过去的互动进行个性化推荐。

大型语言模型(LLMs)可以基于自然语言输入生成对知识图谱的查询。尽管它们在理解和生成类似人类的文本方面表现出色,但其统计性质意味着它们在结构化逻辑推理方面较弱。因此,将它们与知识图谱和结构化查询接口配对,可以利用两者的优势:LLM 用于理解和上下文化用户输入,知识图谱用于精确、逻辑的数据检索。

将图算法纳入其中可以进一步增强这种协同。例如,LLM 可以建议在社交网络图上使用社区检测算法,以识别特定兴趣小组中的影响力人物。然而,挑战在于以高效且可解释的方式集成这些不同的系统。

知识图谱嵌入

知识图谱嵌入将实体和关系编码为密集的向量表示。这些向量可以在 LLM 中动态集成,使用融合模型。

例如,交叉注意机制可以通过将语言模型的令牌嵌入与检索到的图嵌入进行匹配,从而为语言模型的令牌嵌入提供上下文。这注入了相关的外部知识。

数学上将这些互补向量融合,既为模型提供了基础,又允许在两个组件之间进行梯度流动。LLM 继承了关系模式,改善了推理能力。

因此,查询和嵌入机制都提供了将结构化知识图谱与 LLMs 的统计能力连接起来的方法。这有助于生成可解释的、基于策划事实的上下文响应。

通往安全、高效和可解释 AI 的道路无疑在于设计具有不同推理模块的混合系统,这些模块适合其各自的优势,同时通过共生集成来减轻各自的弱点。知识图谱提供了结构性的支撑,将 LLMs 从模式识别器提升为具有上下文感知和严格推理能力的推理器。

知识图谱嵌入可以通过加入额外的约束和结构进一步增强,而不仅仅是编码事实实体和关系。这提供了有用的归纳偏差,以更可靠的方式定向语义相似性和推理。

一些例子包括:

维度类型

在嵌入空间中分配专门的维度,以建模特定的层次知识类别(如类型、属性、时间段等),可以解释向量运算和对称操作。

逻辑规则作为向量方程

将逻辑规则如传递性建模为关系嵌入上的向量方程,使得在查询向量空间时符合一阶逻辑。

v(loves) - v(likes) ≈ v(likes) - v(feels_neutral_about)

实体链接正则化

添加链接损失将相同真实世界实体的向量表示拉近,改善跨表面形式的泛化能力。

时间排序

通过按时间顺序排列实体嵌入来编码时间序列知识,有助于进行时间上的类比推理。

总的来说,通过结构化的归纳偏差——无论是通过类型维度、向量逻辑还是时间排序——美化知识图谱嵌入,使得向量运算更好地符合现实世界约束。这增强了它们解决复杂推理任务的能力,提供了有用的支撑,同时提升了集成语言模型的性能。

核心好处在于将领域知识注入以精确方式定向潜在几何,从而增强连接主义模型在与向量空间互动时的推理能力。通过结构化初始化指导神经引擎操作的原始数据,促进了通过查询进行更系统的组合计算。

互补方法:

首先在查询完整知识图谱之前检索相关的知识图谱嵌入。这种两步法允许高效集中图形操作:

步骤 1:向量嵌入检索

给定自然语言查询,相关的知识图谱嵌入可以通过对索引向量的近似最近邻搜索迅速检索。

例如,使用类似“哪些书讨论了人工智能”的查询,向量搜索会识别出 Book、Topic 和 AI Concept 实体的嵌入。

这在不需要扫描整个图谱的情况下聚焦搜索,改善延迟。嵌入提供了有用的查询扩展信号,用于下一步。

步骤 2:图查询/算法执行

选定的实体嵌入建议有用的切入点和关系,用于结构化的图形查询和算法。

在我们的示例中,Book、Topic 和 AI Concept 的匹配引导了 BOOK-TOPIC 和 TOPIC-CONCEPT 连接的探索。

执行如下查询:

MATCH (b:Book)-[:DISCUSSES]->(t:Topic)-[:OF_TYPE]->(c:Concept)  
WHERE c.name = "Artificial Intelligence"
RETURN b

这遍历了与 AI 主题相关的书籍以生成相关结果。

总体而言,高层次流程是:

  1. 使用向量搜索识别有用的符号句柄

  2. 执行由这些句柄种子生成的图算法

这种紧密耦合将相似性搜索的强度与多跳推理相结合。

主要好处在于使用快速的初始嵌入匹配来聚焦复杂的图算法。这通过避免对每个查询进行详尽的图扫描,改善了延迟和相关性。这种组合使得对广泛知识的可扩展、高效的语义搜索和推理成为可能。

并行查询多个图谱或相同图谱

[## 在混乱的背景下通过思维线索提示和…实现结构化推理]

大型语言模型(LLMs)展示了令人印象深刻的少量学习能力,能够迅速适应新任务...

towardsdatascience.com

使用多个知识图谱并行的关键思想是为语言模型提供更广泛的结构化知识来源,以便在推理过程中进行借鉴。让我进一步阐述一下其合理性:

  1. 知识广度:没有任何一个知识图谱可以囊括所有人类在各个领域的积累的知识。通过并行查询多个知识图谱,我们可以最大化语言模型可利用的事实信息。

  2. 推理多样性:不同的知识图谱可能使用不同的本体、规则、约束等来建模领域。这种知识表示的多样性使语言模型能够接触到更多样的推理模式进行学习。

  3. 效率:并行查询知识图谱允许同时检索相关信息。这比顺序查询提高了延迟效率。并行搜索允许更快速地收集上下文细节进行分析。

  4. 鲁棒性:拥有多个知识来源提供了冗余,以防某个特定图谱不可用或缺乏特定推理链的信息。

  5. 迁移学习:接触到多种推理方法为语言模型提供了更多可迁移的学习示例。这增强了少量学习的适应能力。

总结来说,协调多个知识图谱提供了广泛和多样化的基础知识,以克服单一知识库的局限性。并行检索提高了效率和鲁棒性。跨多样化推理模式的迁移学习也加速了语言模型的适应。这种组合旨在扩展结构化知识注入,实现更接近人类的多功能理解。

大型语言模型作为结构化模块之间流动的语义粘合剂

虽然向量搜索和知识图谱提供了结构化的符号表示,但像 GPT-3 这样的语言模型(LLMs)提供了非结构化但适应性强的语义知识。LLMs 已经展示了卓越的少量学习能力,能够在只有少量示例的情况下快速适应新领域。

这使得 LLMs 非常适合在结构化模块之间充当流动的语义胶——吸收符号知识,解释指令,通过泛化处理边缘情况,并生成上下文输出。它们利用其广泛的参数知识迅速与外部程序和数据表示集成。

因此,我们可以将 LLMs 视为动态的、不断优化的语义层。它们吸收结构化知识的形式,并根据新的输入和查询上下文进行即时适应。LLMs 不是替代符号方法,而是通过快速绑定和上下文响应生成来增强它们。这种流动的整合节省了手动处理所有符号基础和边缘情况的努力。

利用 LLMs 的固有语义泛化能力,使结构化程序能够专注于提供逻辑约束和清晰接口。LLM 随后通过自适应少量学习处理不一致性和空白。这种共生的方法强调了以适应其固有优势的独特推理能力来构建 AI 系统。

结构化知识作为 AI 的基石

关于人工智能的指数级炒作风险让组织追求短视的脚本,承诺快速回报。但有意义的进展需要耐心地培养高质量的知识基础。这体现在结构化知识图谱中,系统地将人类专业知识编码为网络化的表示。

将复杂领域策划成相互关联的实体、约束和规则并非小事。这需要有意的本体工程、严谨的数据治理和迭代的改进。这种渐进性可能会让习惯于快速软件周期的商业领袖感到沮丧。

然而,结构化知识是 AI 缺失的支柱——通过基础信号遏制不受控制的统计模型。知识图谱提供了注入领域知识的支撑结构,同时实现了透明的查询和分析。其可组合的特性也允许与不同系统的互操作。

所有这些都使得企业知识图谱作为战略资产具有了令人信服的理由。正如数据库从灵活的电子表格发展而来,结构的约束最终乘以能力。企业知识图谱中的实体和关系成为推动从对话助手到分析的一切的可靠接触点。

在冲向 AI 前沿的过程中,放任不受约束的模型进行处理是很诱人的。但正如过去每一波自动化浪潮一样,精心编码人类知识以提升机器潜力仍然是至关重要的。如果管理得当,保持这种结构优势在应用中会随着时间的推移而不断增加,巩固市场领导地位。知识推动更好的决策——使得企业知识图谱成为不可或缺的 AI 基础。

由作者使用 Dall-E-3 生成的图像

增强型目标检测:如何有效实现 YOLOv8

原文:towardsdatascience.com/enhanced-object-detection-how-to-effectively-implement-yolov8-afd1bf6132ae

一本关于在图像、视频和实时摄像头画面中使用 CLI 和 Python 进行目标检测的实用指南。

Thomas A DorferTowards Data Science Thomas A Dorfer

· 发布在 Towards Data Science · 阅读时间:7 分钟 · 2023 年 3 月 23 日

--

视频由 Camilo Calderón 制作,发布在 Pexels。由作者转换为 GIF 格式。

介绍

目标检测是计算机视觉的一个子领域,主要关注于以一定的置信度识别和定位图像或视频中的对象。被识别的对象通常会用一个边界框进行标注,这为观众提供有关对象性质和在场景中位置的信息。

2015 年,YOLO 的首次发布,即 You Only Look Once,震撼了计算机视觉界,因为它的系统能够以惊人的准确性和速度进行实时目标检测。从那时起,YOLO 经过了几次改进,提高了预测准确性和效率,最终推出了其最新的家族成员:由 Ultralytics 推出的 YOLOv8

YOLOv8 有五个版本:nano (n)、small (s)、medium (m)、large (l) 和 extra large (x)。它们各自的改进可以通过它们的平均精度均值(mAP)和延迟来展示,这些是通过 COCO val2017 数据集进行评估的。

图片由 Ultralytics 提供。许可证: GNU 通用公共许可证

与之前的版本相比,YOLOv8 不仅更快更准确,还需要更少的参数来实现其性能,而且,如果这还不够,它还配备了一个直观且易于使用的命令行接口(CLI)以及一个Python 包,为用户和开发者提供了更无缝的体验。

在本文中,我将演示如何使用 CLI 和 Python 将 YOLOv8 应用于静态图像、视频和实时摄像头中的对象检测。

不再赘述,让我们开始吧!

安装

要开始使用 YOLOv8,你只需在终端中运行以下命令:

pip install ultralytics

这将通过ultralytics pip 包安装 YOLOv8。

图像检测

静态图像中的对象检测在许多领域中都被证明是有用的,如监控、医学成像或零售分析。无论你选择将检测系统应用于哪个领域,YOLOv8 都使这一过程变得极其简单。

下面是我们将进行对象检测的原始图像。

詹姆斯·科尔曼拍摄,发布于Unsplash

为了运行 YOLOv8,我们将探讨 CLI 和 Python 实现。虽然在这个具体的例子中我们将使用一张jpg图像,但 YOLOv8 支持多种不同的图像格式

CLI

假设我们想在图像(我们称之为img.jpg)上运行超大 YOLOv8x,可以将以下命令输入 CLI:

yolo detect predict model=yolov8x.pt source="img.jpg" save=True

在这里,我们指定以下参数:detect用于对象检测,predict用于执行预测任务,model用于选择模型版本,source用于提供图像的文件路径,以及save用于保存带有对象边界框及其预测类别和类别概率的处理图像。

Python

在 Python 中,完全相同的任务可以通过以下直观且低代码的解决方案来完成:

from ultralytics import YOLO

model = YOLO('yolov8x.pt')
results = model('img.jpg', save=True)

无论你使用 CLI 还是 Python,在任何情况下,保存的处理后图像如下所示:

詹姆斯·科尔曼拍摄,发布于Unsplash。经作者使用 YOLOv8 处理。

我们可以清楚地看到它检测到的每个对象周围的边界框,以及它们对应的类别标签和概率。

视频检测

对视频文件进行对象检测与图像文件几乎相同,唯一的区别是源文件格式。与图像一样,YOLOv8 支持多种不同的视频格式作为模型的输入。在我们的案例中,我们将使用一个mp4文件。

让我们再次查看 CLI 和 Python 实现。为了加快计算速度,我们现在使用 YOLOv8m 模型,而不是超大版本。

CLI

yolo detect predict model=yolov8m.pt source="vid.mp4" save=True

Python

from ultralytics import YOLO

model = YOLO('yolov8m.pt')
results = model('vid.mp4', save=True)

首先,在对vid.mp4文件进行物体检测之前,让我们检查一下原始文件:

视频由Camilo Calderón提供,发布在Pexels上。作者转换为 GIF 格式。

视频展示了一个繁忙城市的场景,交通繁忙,包括汽车、公交车、卡车和骑自行车的人,以及右侧的一些人显然在等公交车。

使用 YOLOv8 的中等版本处理此文件后,我们得到了以下结果:

视频由Camilo Calderón提供,发布在Pexels上。使用 YOLOv8m 处理并由作者转换为 GIF 格式。

再次看到,YOLOv8m 在准确捕捉场景中的物体方面做得非常好。它甚至能够检测到大整体中的小物体,例如骑自行车背着背包的人。

实时检测

最后,让我们看看在实时网络摄像头喂送中检测物体需要什么。为此,我将使用我的个人网络摄像头,并像之前一样,采用 CLI 和 Python 方法。

为了减少延迟和视频中的滞后,我将使用 YOLOv8 的轻量级 nano 版本。

CLI

yolo detect predict model=yolov8n.pt source=0 show=True

这些参数大多与我们之前在图像和视频文件中看到的一致,不同之处在于source,它允许我们指定使用的视频源。在我的情况下,它是内置的网络摄像头(0)。

Python

from ultralytics import YOLO

model = YOLO('yolov8n.pt')
model.predict(source="0", show=True)

再次,我们可以通过超低代码解决方案在 Python 中执行相同的任务。

这是 YOLOv8n 在实时网络摄像头上显示的插图:

GIF 由作者制作。使用 YOLOv8n 从网络摄像头录制。

令人印象深刻!尽管视频质量较低且光线条件较差,它仍然相当好地捕捉了物体,甚至检测到了一些背景中的物体,比如左侧的橄榄油和醋瓶以及右侧的水槽。

值得注意的是,尽管这些直观的 CLI 命令和低代码 Python 解决方案是快速开始物体检测任务的好方法,但在自定义配置方面确实存在局限性。例如,如果我们想要配置边框的美观或执行一些简单任务,比如统计和显示在任何给定时间检测到的物体数量,我们就必须使用cv2supervision等包自行编写自定义实现。

实际上,上面的摄像头视频是使用以下 Python 代码录制的,以调整摄像头的分辨率和自定义定义边界框及其注释。(注意:这主要是为了使上面的 GIF 更具可观赏性。上述的 CLI 和 Python 实现已经足以产生类似的效果。)

import cv2
import supervision as sv
from ultralytics import YOLO

def main():

    # to save the video
    writer= cv2.VideoWriter('webcam_yolo.mp4', 
                            cv2.VideoWriter_fourcc(*'DIVX'), 
                            7, 
                            (1280, 720))

    # define resolution
    cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

    # specify the model
    model = YOLO("yolov8n.pt")

    # customize the bounding box
    box_annotator = sv.BoxAnnotator(
        thickness=2,
        text_thickness=2,
        text_scale=1
    )

    while True:
        ret, frame = cap.read()
        result = model(frame, agnostic_nms=True)[0]
        detections = sv.Detections.from_yolov8(result)
        labels = [
            f"{model.model.names[class_id]} {confidence:0.2f}"
            for _, confidence, class_id, _
            in detections
        ]
        frame = box_annotator.annotate(
            scene=frame, 
            detections=detections, 
            labels=labels
        ) 

        writer.write(frame)

        cv2.imshow("yolov8", frame)

        if (cv2.waitKey(30) == 27): # break with escape key
            break

    cap.release()
    writer.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()

尽管本文中的代码细节超出了讨论范围,但这里有一个很好的参考,采用了类似的方法,如果您有兴趣提升您的目标检测技能:

结论

YOLOv8 不仅在准确性和速度上超越了其前辈,而且通过极易使用的 CLI 和低代码 Python 解决方案显著提升了用户体验。它还提供了五种不同的模型版本,用户可以根据个人需求和对延迟及准确性的容忍度选择合适的版本。

无论您的目标是对静态图像、视频还是实时摄像头进行目标检测,YOLOv8 都可以无缝完成。然而,如果您的应用需要自定义配置,您可能需要依赖其他计算机视觉包,例如 cv2supervision

更多资源

喜欢这篇文章吗?

让我们联系吧!您可以在 TwitterLinkedIn 上找到我。

如果您希望支持我的写作,您可以通过 Medium 会员 来实现,这样您可以访问我所有的故事,以及 Medium 上其他成千上万作家的文章。

## 通过我的推荐链接加入 Medium - Thomas A Dorfer

阅读 Thomas A Dorfer 的每一篇文章(以及 Medium 上成千上万的其他作家的文章)。您的会员费用直接支持……

medium.com

提升 ChatGPT 中 CSV 文件查询性能

原文:towardsdatascience.com/enhancing-csv-file-query-performance-in-chatgpt-3e2b67a5f867?source=collection_archive---------1-----------------------#2023-07-31

基于自定义 CSV 加载器的 LangChain 自查询

Shuyang XiangTowards Data Science Shuyang Xiang

·

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

--

高级语言模型的出现,如 ChatGPT,为查询表格数据带来了新颖而有前途的方法。然而,由于令牌限制,直接执行查询变得具有挑战性,尤其是在没有像检索器这样的 API 协助的情况下。因此,查询的准确性严重依赖于查询的质量,标准检索器往往无法返回所需的确切信息。

在这篇文章中,我将深入探讨传统检索方法在某些使用案例中失败的原因。此外,我们提出了一种革命性解决方案,即一种自定义的 CSV 数据加载器,它整合了元数据。通过利用LangChain的自查询 API 及新 CSV 数据加载器,我们可以以显著提高的性能和精度提取信息。

有关本文中使用的详细代码,请查看这里的笔记本。我想强调的是,这个笔记本展示了使用 LLM 查询大型表格数据可以实现显著准确性的可能性。

在疾病人口数据集上的检索

我们希望查询作者创建的以下合成 SIR 数据集:我们基于一个简单的SIR 模型模拟 10 个城市在 90 天内的三组人口。为了简单起见,我们假设每个城市的人口范围从 5e3 到 2e4,并且城市之间没有人口流动。此外,我们生成了十个介于 500 到 2000 之间的随机整数作为原始感染人数。

作者提供的图像:10 个城市的疾病人口

表格的形式如下,有五列:“time”表示测量人口的时间,“city”表示测量数据的城市,“susceptible”、“infectious”和“removed”表示人口的三组。为了简单起见,数据已保存为本地 CSV 文件。

time susceptible infectious removed city
0 2018-01-01 8639 8639 0 city0
1 2018-01-02 3857 12338 1081 city0
2 2018-01-03 1458 13414 2405 city0
3 2018-01-04 545 12983 3749 city0
4 2018-01-05 214 12046 5017 city0

我们希望向 ChatGPT 提出与数据集相关的问题。为了让 ChatGPT 与这种表格数据交互,我们按照以下标准步骤使用 LangChain:

  1. 使用 CSVLoader 加载数据,

  2. 创建一个 vectorstore(我们这里使用 Chroma)来存储带有 OpenAI 嵌入的嵌入数据,

  3. 使用检索器返回与给定非结构化查询相关的文档。

你可以使用以下代码实现上述步骤。

# load data fron local path 
loader = CSVLoader(file_path=LOCAL_PATH)
data = loader.load()

# Create embedding
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(data, embeddings)

# Create retriever 
retriever=vectorstore.as_retriever(search_kwargs={"k": 20})

我们现在可以定义一个ConversationalRetriverChain来查询 SIR 数据集。

llm=ChatOpenAI(model_name="gpt-4",temperature=0)

# Define the system message template
system_template = """The provided {context} is a tabular dataset containing Suspectible, infectious and removed population during 90 days in 10 cities.
The dataset includes the following columns:
'time': time the population was meseaured,
'city': city in which the popoluation was measured,
"susceptible": the susceptible population of the disease, 
"infectious": the infectious population of the disease, 
"removed": the removed popolation of the disease. 
----------------
{context}"""

# Create the chat prompt templates
messages = [
    SystemMessagePromptTemplate.from_template(system_template),
    HumanMessagePromptTemplate.from_template("{question}")
]
qa_prompt = ChatPromptTemplate.from_messages(messages)
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
qa = ConversationalRetrievalChain.from_llm(llm=llm, retriever=vectorstore.as_retriever(), return_source_documents=False,combine_docs_chain_kwargs={"prompt": qa_prompt},memory=memory,verbose=True)

在上述代码中,我定义了一个对话链,它将使用在上一步中定义的检索器在 SIR 数据集中搜索查询的相关信息,并根据检索到的信息给出答案。为了给 ChatGPT 更清晰的指示,我还给出了一个提示,说明了数据集中所有列的定义。

现在我们来问一个简单的问题:“2018 年 2 月 3 日哪个城市的感染人数最多?”

出乎意料的是,我们的聊天机器人说:“提供的数据集中不包括 2018 年 2 月 3 日的数据。”

这怎么可能?

为什么检索失败了?

为了调查聊天机器人为何未能回答一个其答案仅存在于提供的数据集中的问题,我查看了它用问题“2018 年 2 月 3 日哪个城市的传染病人数最多?”检索到的相关文档。我得到了以下几行:

[Document(page_content=': 31\ntime: 2018-02-01\nsusceptible: 0\ninfectious: 1729\nremoved: 35608\ncity: city3', metadata={'source': 'sir.csv', 'row': 301}),
 Document(page_content=': 1\ntime: 2018-01-02\nsusceptible: 3109\ninfectious: 9118\nremoved: 804\ncity: city8', metadata={'source': 'sir.csv', 'row': 721}),
 Document(page_content=': 15\ntime: 2018-01-16\nsusceptible: 1\ninfectious: 2035\nremoved: 6507\ncity: city7', metadata={'source': 'sir.csv', 'row': 645}),
 Document(page_content=': 1\ntime: 2018-01-02\nsusceptible: 3481\ninfectious: 10873\nremoved: 954\ncity: city5', metadata={'source': 'sir.csv', 'row': 451}),
 Document(page_content=': 23\ntime: 2018-01-24\nsusceptible: 0\ninfectious: 2828\nremoved: 24231\ncity: city9', metadata={'source': 'sir.csv', 'row': 833}),
 Document(page_content=': 1\ntime: 2018-01-02\nsusceptible: 8081\ninfectious: 25424\nremoved: 2231\ncity: city6', metadata={'source': 'sir.csv', 'row': 541}),
 Document(page_content=': 3\ntime: 2018-01-04\nsusceptible: 511\ninfectious: 9733\nremoved: 2787\ncity: city8', metadata={'source': 'sir.csv', 'row': 723}),
 Document(page_content=': 24\ntime: 2018-01-25\nsusceptible: 0\ninfectious: 3510\nremoved: 33826\ncity: city3', metadata={'source': 'sir.csv', 'row': 294}),
 Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 1413\nremoved: 35924\ncity: city3', metadata={'source': 'sir.csv', 'row': 303}),
 Document(page_content=': 25\ntime: 2018-01-26\nsusceptible: 0\ninfectious: 3173\nremoved: 34164\ncity: city3', metadata={'source': 'sir.csv', 'row': 295}),
 Document(page_content=': 1\ntime: 2018-01-02\nsusceptible: 3857\ninfectious: 12338\nremoved: 1081\ncity: city0', metadata={'source': 'sir.csv', 'row': 1}),
 Document(page_content=': 23\ntime: 2018-01-24\nsusceptible: 0\ninfectious: 1365\nremoved: 11666\ncity: city8', metadata={'source': 'sir.csv', 'row': 743}),
 Document(page_content=': 16\ntime: 2018-01-17\nsusceptible: 0\ninfectious: 2770\nremoved: 10260\ncity: city8', metadata={'source': 'sir.csv', 'row': 736}),
 Document(page_content=': 3\ntime: 2018-01-04\nsusceptible: 487\ninfectious: 6280\nremoved: 1775\ncity: city7', metadata={'source': 'sir.csv', 'row': 633}),
 Document(page_content=': 14\ntime: 2018-01-15\nsusceptible: 0\ninfectious: 3391\nremoved: 9639\ncity: city8', metadata={'source': 'sir.csv', 'row': 734}),
 Document(page_content=': 20\ntime: 2018-01-21\nsusceptible: 0\ninfectious: 1849\nremoved: 11182\ncity: city8', metadata={'source': 'sir.csv', 'row': 740}),
 Document(page_content=': 28\ntime: 2018-01-29\nsusceptible: 0\ninfectious: 1705\nremoved: 25353\ncity: city9', metadata={'source': 'sir.csv', 'row': 838}),
 Document(page_content=': 23\ntime: 2018-01-24\nsusceptible: 0\ninfectious: 3884\nremoved: 33453\ncity: city3', metadata={'source': 'sir.csv', 'row': 293}),
 Document(page_content=': 16\ntime: 2018-01-17\nsusceptible: 1\ninfectious: 1839\nremoved: 6703\ncity: city7', metadata={'source': 'sir.csv', 'row': 646}),
 Document(page_content=': 15\ntime: 2018-01-16\nsusceptible: 1\ninfectious: 6350\nremoved: 20708\ncity: city9', metadata={'source': 'sir.csv', 'row': 825})]

令人惊讶的是,尽管我明确要求了解 2018 年 2 月 3 日发生了什么,但没有返回该日期的任何行。由于该日期的信息从未发送给 ChatGPT,因此当然毫无疑问它无法回答这样的问题。

深入研究检索器的源代码,我们可以看到get_relevant_dcouments默认调用similarity_search。该方法根据计算的距离度量(默认使用余弦距离)返回前 n 个片段(默认 4 个,但我在代码中将数量设置为 20),这些片段的距离范围从 0 到 1,用于测量查询向量与文档片段向量之间的“相似性”。

回到 SIR 数据集,我们注意到每行几乎讲述了相同的故事:哪个日期、哪个城市、以及多少人被标记为哪个组。毫不奇怪,代表这些行的向量彼此相似。快速检查相似度得分发现很多行的得分接近 0.29。

因此,相似度得分不足以区分行与查询的相关性:这在表格数据中总是如此,因为行之间没有显著差异。我们需要更强的过滤器来处理元数据。

带有自定义元数据的 CSVLoader

显然,聊天机器人的性能提升在很大程度上依赖于检索器的效率。为此,我们首先定义了一个自定义的 CSVLoader,它可以将元数据与检索器进行通信。

我们编写了以下代码:

class MetaDataCSVLoader(BaseLoader):
    """Loads a CSV file into a list of documents.

    Each document represents one row of the CSV file. Every row is converted into a
    key/value pair and outputted to a new line in the document's page_content.

    The source for each document loaded from csv is set to the value of the
    `file_path` argument for all doucments by default.
    You can override this by setting the `source_column` argument to the
    name of a column in the CSV file.
    The source of each document will then be set to the value of the column
    with the name specified in `source_column`.

    Output Example:
        .. code-block:: txt

            column1: value1
            column2: value2
            column3: value3
    """

    def __init__(
        self,
        file_path: str,
        source_column: Optional[str] = None,
        metadata_columns: Optional[List[str]] = None,   
        content_columns: Optional[List[str]] =None ,  
        csv_args: Optional[Dict] = None,
        encoding: Optional[str] = None,
    ):
        #  omitted (save as original code)
        self.metadata_columns = metadata_columns        # < ADDED

    def load(self) -> List[Document]:
        """Load data into document objects."""

        docs = []
        with open(self.file_path, newline="", encoding=self.encoding) as csvfile:
           #  omitted (save as original code)
                # ADDED CODE 
                if self.metadata_columns:
                    for k, v in row.items():
                        if k in self.metadata_columns:
                            metadata[k] = v
                # END OF ADDED CODE
                doc = Document(page_content=content, metadata=metadata)
                docs.append(doc)
        return docs

为节省空间,我省略了与原始 API 相同的代码,仅包含了主要用于将需要特别注意的某些列写入元数据的附加几行代码。确实,在上面的打印数据中,你可以注意到两个部分:页面内容和元数据。标准 CSVLoader 将表格的所有列写入页面内容,只将数据资源和行号写入元数据。定义的“MetaDataCSVLoader”允许我们将其他列写入元数据。

现在我们重新创建向量存储,步骤与上述章节相同,只是数据加载器中添加了两个元数据列“time”和“city”。

# Load data and set embeddings 
loader = MetaDataCSVLoader(file_path="sir.csv",metadata_columns=['time','city']) #<= modified 
data = loader.load()

自查询元数据通知的向量存储

现在我们准备使用 LangChain 的SelfQuerying API

根据 LangChain 的文档:自查询检索器顾名思义,就是可以对自身进行查询的检索器。… 这使得检索器不仅可以使用用户输入的查询与存储文档的内容进行语义相似性比较,还可以从用户查询中提取关于存储文档的元数据的过滤器,并执行这些过滤器。

图片来源于 LangChain:自查询示意图

你现在可以理解为什么我在上一章强调元数据:这是基于元数据,ChatGPT 或其他 LLM 能够在其上构建过滤器,以从数据集中获取最相关的信息。我们使用以下代码构建这样的自查询检索器,通过描述元数据和文档内容描述来构建一个性能良好的过滤器,以提取准确的信息。特别地,我们给检索器提供一个metadata_field_info,指定我们希望检索器更多关注的两列的类型和描述。

llm=ChatOpenAI(model_name="gpt-4",temperature=0)
metadata_field_info=[
     AttributeInfo(
        name="time",
        description="time the population was meseaured", 
        type="datetime", 
    ),
    AttributeInfo(
        name="city",
        description="city in which the popoluation was measured", 
        type="string", 
    ),
]
document_content_description = "Suspectible, infectious and removed population during 90 days in 10 cities "
retriever = SelfQueryRetriever.from_llm(
    llm, vectorstore, document_content_description, metadata_field_info, search_kwargs={"k": 20},verbose=True
)

我们现在可以定义一个类似的ConversationalRetriverChain来查询 SIR 数据集,但这次使用SelfQueryRetriever。让我们看看当我们问同样的问题时会发生什么:“2018 年 2 月 3 日哪个城市传染人数最多?”

聊天机器人说:“2018 年 2 月 3 日传染人数最多的城市是 city3,传染人数为 1413。”

各位女士们先生们,结果是正确的!聊天机器人在使用更好的检索器时表现更好!

这次查看检索器返回了哪些相关文档是没有害处的,结果是:

[Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 1413\nremoved: 35924\ncity: city3', metadata={'source': 'sir.csv', 'row': 303, 'time': '2018-02-03', 'city': 'city3'}),
 Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 822\nremoved: 20895\ncity: city4', metadata={'source': 'sir.csv', 'row': 393, 'time': '2018-02-03', 'city': 'city4'}),
 Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 581\nremoved: 14728\ncity: city5', metadata={'source': 'sir.csv', 'row': 483, 'time': '2018-02-03', 'city': 'city5'}),
 Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 1355\nremoved: 34382\ncity: city6', metadata={'source': 'sir.csv', 'row': 573, 'time': '2018-02-03', 'city': 'city6'}),
 Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 496\nremoved: 12535\ncity: city8', metadata={'source': 'sir.csv', 'row': 753, 'time': '2018-02-03', 'city': 'city8'}),
 Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 1028\nremoved: 26030\ncity: city9', metadata={'source': 'sir.csv', 'row': 843, 'time': '2018-02-03', 'city': 'city9'}),
 Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 330\nremoved: 8213\ncity: city7', metadata={'source': 'sir.csv', 'row': 663, 'time': '2018-02-03', 'city': 'city7'}),
 Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 1320\nremoved: 33505\ncity: city2', metadata={'source': 'sir.csv', 'row': 213, 'time': '2018-02-03', 'city': 'city2'}),
 Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 776\nremoved: 19753\ncity: city1', metadata={'source': 'sir.csv', 'row': 123, 'time': '2018-02-03', 'city': 'city1'}),
 Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 654\nremoved: 16623\ncity: city0', metadata={'source': 'sir.csv', 'row': 33, 'time': '2018-02-03', 'city': 'city0'})]

你可能会立刻注意到在检索到的文档中现在有“时间”和“城市”这些“元数据”。

结论

在这篇博客文章中,我探讨了 ChatGPT 在查询 CSV 格式数据集时的局限性,以 10 个城市的 SIR 数据集在 90 天内为例。为了应对这些局限性,我提出了一种新方法:一种感知元数据的 CSV 数据加载器,使我们能够利用自查询 API,显著提高聊天机器人的准确性和性能。

使用实时先进物体识别技术构建乐高技术件分类器

原文:towardsdatascience.com/enhancing-nullspace-robotics-capabilities-building-a-lego-sorter-with-advanced-object-recognition-7ba5d70c9902?source=collection_archive---------5-----------------------#2023-11-02

Aveek GoswamiTowards Data Science Aveek Goswami

·

关注 发布于 Towards Data Science · 9 分钟阅读 · 2023 年 11 月 2 日

--

在我于 Nullspace Robotics实习期间,我有幸参与了一个提升公司能力的项目。我们集成了物体检测和机器学习图像识别,开发了一台能够实时分类乐高技术件的机器。

在这篇博客文章中,我将带你了解遇到的挑战以及我们如何成功完成这个项目。

阿莫斯·科赫和我在 22 年夏天教学生编程和机器人技术,同时为 Nullspace 工作。你可以在文章下方的链接找到我们。

Nullspace Robotics 是新加坡领先的中小学机器人和编程教育提供商。他们的大部分操作涉及使用乐高 Technic 部件构建机器人,这些部件被分类到特定的托盘中。你可以想象,让一个充满精力的 8 岁小孩帮忙把积木放回托盘是一个噩梦般的任务,而他们只想做更多的东西。

Nullspace 让我们制作一台可以将乐高 Technic 积木分类到特定类别的机器,以最小化人工干预,从而解决进行机器人教学时的关键效率挑战之一。

确定挑战

该项目涉及三个主要部分:实时物体和运动检测、图像识别以及机器的硬件建设。由于实习的时间限制,我们主要关注了前两个项目,这涉及到项目的软件方面。

一个关键挑战是识别移动部件并在同一帧内确定它们的位置。我们考虑了两种方法:将机器学习图像识别集成到物体检测相机中,或保持过程分开。

最终,我们决定将物体检测和识别分开。这种方法首先在检测到物体后捕捉合适的图片,然后运行模型对图像进行分类。将这些过程整合在一起需要在几乎每一帧上运行模型,以对每个检测到的物体进行分类。将它们分开消除了模型需要保持持续处理模式的需求,从而确保了操作更加流畅和计算更高效。

物体检测

我们借鉴了文章下方引用的项目中的想法,实施了我们的物体/运动检测程序,并将其定制为乐高积木。

在我们的案例中,我们使用了类似的运动检测概念,因为我们的机器将涉及一个统一颜色的传送带系统,因此检测到的任何运动都是由于乐高积木在传送带上移动。

我们对所有帧应用了高斯模糊以及其他图像处理技术,并与之前的帧进行比较。进一步处理用于隔离(绘制边界框)造成运动的物品,如下所示:

for f in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True):

    frame = f.array # grab the raw NumPy array representing the image
    text = "No piece" # initialize the occupied/unoccupied text

    # resize the frame, convert it to grayscale, and blur it
    frame = imutils.resize(frame, width=500)
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (21, 21), 0)

    # if the average frame is None, initialize it
    if avg is None:
        print("[INFO] starting background model...")
        avg = gray.copy().astype("float")
        rawCapture.truncate(0)
        continue

    # accumulate the weighted average between the current frame and
    # previous frames, then compute the difference between the current
    # frame and running average
    cv2.accumulateWeighted(gray, avg, 0.5)
    frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg))    

    # threshold the delta image, dilate the thresholded image to fill
    # in holes, then find contours on thresholded image
    thresh = cv2.threshold(frameDelta, conf["delta_thresh"], 255,
        cv2.THRESH_BINARY)[1]
    thresh = cv2.dilate(thresh, None, iterations=2)
    cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
        cv2.CHAIN_APPROX_SIMPLE)
    cnts = imutils.grab_contours(cnts)

    # loop over the contours

    for c in cnts:
        # if the contour is too small, ignore it
        if cv2.contourArea(c) < conf["min_area"]:
            continue

        # compute the bounding box for the contour, draw it on the frame,
        # and update the text
        (x, y, w, h) = cv2.boundingRect(c)
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
        piece_image = frame[y:y+h,x:x+w]
        text = "Piece found"
        # cv2.imshow("Image", image)

为了确保运动实际上是由乐高积木造成的,我们使用了一个运动计数器来评估运动检测的稳定性,该计数器检查了在得出运动确实是由于乐高积木而非杂音之前,是否在一定数量的帧内检测到了运动。最终图像被保存并输入到我们的 CNN 中进行分类。

if text == "Piece found":
        # to save images of bounding boxes
        motionCounter += 1
        print("motionCounter= ", motionCounter)
        print("image_number= ", image_number)

        # Save image if motion is detected for 8 or more successive frames
        if motionCounter >= 8:
            image_number +=1
            image_name = str(image_number)+"image.jpg"
            cv2.imwrite(os.path.join(path, image_name), piece_image)
            motionCounter = 0 #reset the motion counter

 # classify the saved image with our model, see below

创建模型

建立数据集

我们自己创建了图像数据集,而不是使用在线找到的 Lego Technic 零件图像,因为我们想要模拟模型最终检测和分类零件的条件。因此,我们设计了一个简单的传送带系统,完全使用 Lego Technic 零件!然后我们将其连接到 Lego Spike Prime 电机,以保持传送带的运转。

设计模型架构

为了解决挑战的核心问题,我调整了我在 Aladdinpersson 的 GitHub 仓库中找到的一个机器学习模型。这个模型采用了从 128 到 64 再到 32 再到 16 的卷积层序列,这种架构设计旨在提高图像识别能力。

我们没有使用预训练模型,而是设计了自己的卷积神经网络,因为:

  1. 我们不需要特别深的特征提取来处理我们的图像。

  2. 我们希望保持模型的小巧并降低其复杂性,同时减少运行模型的计算成本。这将使 CNN 在 Pi 上以 tflite 模型更高效地运行。

数据归一化是确保一致训练准确性的关键步骤,特别是考虑到不同图像由于光照差异而捕获的值范围不同。

在这个模型中,诸如 ReLU、dense、softmax 和 flatten 等不同层发挥了关键作用。例如,ReLU 激活对于图像分类至关重要,因为它减轻了图像识别中的梯度消失问题。另一方面,Dense 层在 Tensorflow 模型中是标准配置,有助于实现密集连接的神经网络。Softmax 激活用于计算数据集中每个类别的概率。

对于损失函数,我们使用了 Keras 的稀疏类别交叉熵,这是多类分类任务的合适选择。我们使用了以其效率著称的 Keras Adam 优化器来微调模型。

训练和优化

训练轮次被精心选择,以在训练和过拟合之间取得平衡,倾向于选择低于 200 的轮次,以确保模型性能最佳。为了加快模型训练,我们利用了 Google Colab,它提供了 GPU 资源,相比我们自己的笔记本电脑,训练速度显著更快。

完整的模型架构如下所示:

data_augmentation = keras.Sequential([
    layers.RandomFlip("horizontal",
                        input_shape=(img_height,
                                    img_width,
                                    1)),
    layers.RandomRotation(0.2),
    layers.RandomZoom(0.1),
    ])

model = keras.Sequential(
    [
        data_augmentation,

        layers.Rescaling(1./255, input_shape = (img_height,img_width,1)), #normalize the data input

        layers.Conv2D(128, 3, padding="same", activation='relu'),
        layers.MaxPooling2D(pool_size=(2,2)),

        layers.Conv2D(64, 3, padding="same", activation='relu'), #should this be 16 or 32 units? try with more data
        layers.MaxPooling2D(pool_size=(2,2)),

        layers.Conv2D(32, 3, padding="same", activation='relu'),
        layers.MaxPooling2D(pool_size=(2,2)),

        layers.Conv2D(16, 3, padding="same", activation='relu'),
        layers.MaxPooling2D(pool_size=(2,2)),

        layers.Dropout(0.1),
        layers.Flatten(),
        layers.Dense(10,activation = 'relu'),
        layers.Dense(7,activation='softmax'), # number of output classes

    ]
)        

model.compile(
    optimizer=keras.optimizers.Adam(),
    loss=[keras.losses.SparseCategoricalCrossentropy(from_logits=False),],
    metrics=["accuracy"],
)

model_history = model.fit(x_train, y_train, epochs=200, verbose=2, validation_data=(x_test,y_test), batch_size=25)  #i think 25/32 is the best batch size

选择架构

在大多数常见的 CNN 架构中,过滤器的数量会增加,以尝试在更高层捕捉更复杂的特征。然而,对于区分我们的 Lego 零件,类别之间的相似度很高,我们需要网络寻找如弯曲和孔洞等特定特征。我认为,在更深层中使用较少的过滤器可能有助于专注于这些较少的细微差别,而不是查看可能不利于区分零件的多个特征。

我们测试了两种架构,一种是减少滤波器,另一种是增加滤波器,结果是减少滤波器的模型表现显著更好。因此,减少滤波器的数量可以让网络专注于关键部分,减少复杂特征图中的噪声。

当然,这取决于你的使用案例和数据集中的特征。例如,人脸识别需要更复杂的特征图,因此增加滤波器的方法可能效果更好。

模型结果

该模型使用 6000 张图像,涵盖 7 类乐高技术块进行训练。最终验证准确率达到了93%。下面展示了训练过程的图示以及用于评估性能的混淆矩阵:

在树莓派上实施模型

在树莓派上运行神经网络的最有效方法是使用 tflite(tensorflow lite)模型。我们将模型保存到本地,然后加载到树莓派上。

from tflite_runtime.interpreter import Interpreter

# Load TFLite model and allocate tensors.
interpreter = Interpreter(model_path="lego_tflite_model/detect.tflite") # insert path to the tflite model
interpreter.allocate_tensors()

继续使用上面提到的运动计数器循环,将适当的图像输入神经网络进行分类:

 # continuing from if text == "Piece found":
            # Open the image, resize it and increase its contrast
            input_image = Image.open('lego-pieces/'+ image_name)
            input_image = ImageOps.grayscale(input_image)
            input_image = input_image.resize((128,128))
            input_data = img_to_array(input_image)
            input_data = increase_contrast_more(input_data)
            input_data.resize(1,128,128,1)

            # Pass the np.array of the image through the tflite model. This will output a probablity vector
            interpreter.set_tensor(input_details[0]['index'], input_data)
            interpreter.invoke()
            output_data = interpreter.get_tensor(output_details[0]['index'])

            # Get the index of the highest value in the probability vector.
            # This index value will correspond to the labels vector created above (i.e index value 1 will mean the object is most likely labels[1])
            category_number = np.argmax(output_data[0])

            # Return the classification label of the image    
            classification_label = labels[category_number]                
            print("Image Label for " + image_name + " is :", classification_label)

    else:
        motionCounter = 0 # reset motion counter to look for new objects

灵活性是一个关键考虑因素。运动计数器可以根据捕捉图像以构建数据集的过程或设置图像分类时的捕捉阈值进行调整,从而提升系统的多功能性。

演示

我们努力的最终成果是展示系统的整体准确性,辅以捕捉其操作的照片和视频。传送带设置(如上图)是此次演示的关键部分:

未来工作及改进方向

软件:未来的扩展还将包括在操作中加入质量检查模型,以确保用于分类部件的图像合适。

硬件:该模型无疑将受益于更高级的相机以获得更高质量的图像。此外,临时建造的传送带系统需要扩展以容纳更多部件。还需制定并实施一种方法来分离多个乐高部件,确保相机帧中每次只显示一个部件。网上有类似的项目详细介绍了可能的方法。

结论

我在 Nullspace Robotics 的经历是我首次尝试为实际用途构建自己的神经网络。在过去的培训课程中设计模型的经历与创建用于实际生产的模型完全不同,我们需要考虑资源、使用目的,并找到如何调整数据集和模型以适应我们需求的方法。我期待继续我的机器学习之旅,并利用最新的 AI 技术构建更具创新性的解决方案。

我想感谢 Nullspace 给我提供了这个项目的机会,我很期待看到公司在推进机器人教育的边界时接下来会有什么发展。

查看 Github 或 HuggingFace 上的完整代码库,获取代码、数据集图像以及更多项目信息:

Github: github.com/magichampz/lego-sorting-machine-ag-ak/

HuggingFace: huggingface.co/magichampz

认识开发者

Aveek: www.linkedin.com/in/aveekg00/

Amos: www.linkedin.com/in/ak726/

查看 Nullspace Robotics

参考文献:

[## 使用 Python 和 OpenCV 进行基本运动检测和跟踪 — PyImageSearch

在本教程中,我将向你展示如何使用 Python 和 OpenCV 执行基本的运动检测和跟踪。了解如何……

pyimagesearch.com](https://pyimagesearch.com/2015/05/25/basic-motion-detection-and-tracking-with-python-and-opencv/?source=post_page-----7ba5d70c9902--------------------------------) ## 使用 OpenCV 检测运动 — 初学者图像分析

如何使用 OpenCV 检测和分析移动物体

towardsdatascience.com [## Machine-Learning-Collection/ML/TensorFlow/Basics/tutorial15-customizing-modelfit.py 在主分支 ·…

一个关于机器学习和深度学习的资源……

github.com](https://github.com/aladdinpersson/Machine-Learning-Collection/blob/master/ML/TensorFlow/Basics/tutorial15-customizing-modelfit.py?source=post_page-----7ba5d70c9902--------------------------------)

未使用的内容

除非另有说明,所有图片均为作者提供

增强 Python 文档:逐步指南以链接源代码

原文:towardsdatascience.com/enhancing-python-documentation-a-step-by-step-guide-to-linking-source-code-9da102b2bb2a

Pablo PiskunowTowards Data Science Pablo Piskunow

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

--

你阅读了这个类方法的描述,但仍然不了解发生了什么。如果你能快速阅读源代码就好了…

弥合文档与代码之间的差距:简化 Python 学习

Python 的强大不仅体现在它的简洁和高效,还在于其庞大的社区和丰富的文档。但是,如果你能够使这些文档变得更加互动和信息丰富呢?今天,我将带你通过将你的 Sphinx 生成的 Python 文档直接链接到 GitHub 上的相应源代码来增强文档。

作者使用 Dalle-3 创建的图像。提示“抽象图像,浅奶油色画布上的黑色水彩,展示与机器内部工作相关的手写片段,带有细箭头”。

第一步:使用 Sphinx 进行文档编写

当我们在 Python 代码中编写适当的文档字符串时,我们为生成全面的 API 文档奠定了基础。像 Sphinx 的 autodocautomodule 这样的工具非常适合从我们的模块、类和函数中提取这些文档字符串。然而,它们通常未能提供直接链接到源代码的功能。

如果你需要开始使用 Sphinx,请查看这些教程:

## Sphinx

在教程的上一部分中,你手动在 Sphinx 中记录了一个 Python 函数。然而,描述…

www.sphinx-doc.org [## 使用 Sphinx 构建文档项目 - Sphinx 和文档的简介]

为了本教程的目的,我们将使用 reStructuredText 编写文档。如果你希望使用…

sphinx-intro-tutorial.readthedocs.io

第 2 步:设置 Sphinx linkcode

要添加此功能,首先我们需要修改 Sphinx 配置。这包括在文档源的 conf.py 文件中将 sphinx.ext.linkcode 添加到扩展列表中:

# docs/conf.py
extensions = [
    ...,
    "sphinx.ext.linkcode",
]
...

第 3 步:基本的 Linkcode 实现

我们的下一步是定义 linkcode_resolve 函数。这个函数负责确定文档应该指向的 URL:

# docs/conf.py
...
def linkcode_resolve(domain, info):
    if domain != "py":
        return None
    if not info["module"]:
        return None

    filename = "src/" + info["module"].replace(".", "/")
    github_repo = "https://github.com/username/my-package"

    return f"{github_repo}/blob/main/{filename}.py"

在这里,我们只是指向 GitHub 仓库中的文件,而不是特定的代码行。

第 4 步:查找行号

获取模块对象

首先,你需要获取定义目标类、方法、属性或函数(以下简称“对象”)的模块对象。在 Python 中,每个加载的模块都存储在一个名为 sys.modules 的字典中。你需要访问这个字典,以根据其名称检索模块对象:

module = sys.modules.get(info[“module”])

遍历完全限定名称

接下来,你需要遍历对象的完全限定名称。完全限定名称包括访问对象所需的所有层级,例如 module.class.method。这个遍历过程帮助你深入模块结构,找到你需要的确切对象:

obj = module
for part in info[“fullname”].split(“.”):
    obj = getattr(obj, part, None)

使用 Inspect 模块查找行号

最后,你使用 inspect 模块找到源代码中定义该对象的行号:

line = inspect.getsourcelines(obj)[1]

创建一个函数来检索行号

为了使函数适用于所有情况,我们需要添加一些额外的检查:

def get_object_line_number(info):
    """Return object line number from module."""
    try:
        module = sys.modules.get(info["module"])
        if module is None:
            return None

        # walk through the nested module structure
        obj = module
        for part in info["fullname"].split("."):
            obj = getattr(obj, part, None)
            if obj is None:
                return None

        return inspect.getsourcelines(obj)[1]
    except (TypeError, OSError):
        return None

总之,这一步骤涉及定位目标对象所在的模块,遍历模块结构以找到确切的对象(无论是类、方法、属性还是函数),然后使用 inspect 模块找到该对象在源代码中定义的行号。

第 6 步:完成 Linkcode 解析

我们将行号检索集成到 linkcode_resolve 函数中:

def linkcode_resolve(domain, info):
    ...
    line = get_object_line_number(info)
    if line is None:
        return None

    return f"{github_repo}/blob/{github_branch}/{filename}.py#L{line}"

这种方法使你的文档能够提供指向源代码中特定行的直接链接,从而提高文档的清晰度和实用性。

额外:ReadTheDocs 的分支适配

对于使用 ReadTheDocs 的用户,你可以调整这个函数以参考不同的分支(如 maindevelop)。那么如何构建两个不同的文档来指向这两个分支呢?

当 ReadTheDocs 构建文档时,它使用自己的环境,其中包含一个名为 READTHEDOCS_VERSION 的变量,通常为 ‘stable’ 或 ‘latest’。我添加了一个名为 ‘develop’ 的构建,它指向我的 Git 仓库中的同名分支。

我们可以在 linkcode_resolve 函数中添加以下内容:

def linkcode_resolve(domain, info):
    ...
    rtd_version = os.getenv("READTHEDOCS_VERSION", "latest")
    github_branch = "develop" if rtd_version == "develop" else "main"

    return f"{github_repo}/blob/{github_branch}/{filename}.py#L{line}"

请注意,如果在本地构建,或者更一般地在未定义READTHEDOCS_VERSION的环境中,默认链接的分支将是main

随时查看这个GitHub 仓库文档,了解相关实现。

结论

有了这个设置,你的 Sphinx 文档现在将直接链接到源代码,大大提升了其实用性和用户体验。这一小小的增加可以显著提高文档的可用性和有效性,使其成为新手和经验丰富的开发者的宝贵工具。

记住,编码的旅程不仅仅是解决问题,也关乎分享知识和解决方案。所以,如果你觉得这个教程对你有帮助,我鼓励你传播这个消息,帮助其他人进行他们的编码冒险。

祝编码愉快,下次见!

在我的 Gumroad 个人资料 个人网站上发现更多我的作品和独家内容。

免责声明:我与 Sphinx 没有任何关系,我只是觉得它是一个很棒的开源工具。

提升 Haystack 中的 RAG 流水线:引入 DiversityRanker 和 LostInTheMiddleRanker

原文:towardsdatascience.com/enhancing-rag-pipelines-in-haystack-45f14e2bc9f5?source=collection_archive---------1-----------------------#2023-08-08

最新的排名器如何优化 LLM 上下文窗口在检索增强生成(RAG)流水线中的利用

Vladimir BlagojevicTowards Data Science Vladimir Blagojevic

·

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

--

最近在自然语言处理(NLP)和长文问答(LFQA)方面的改进,几年前听起来简直像科幻小说中的内容。谁能想到现在我们会有能够像专家一样精确回答复杂问题的系统,同时从庞大的资源库中即时合成这些答案?LFQA 是一种检索增强生成(RAG),最近取得了显著进展,利用了大型语言模型(LLMs)最优秀的检索和生成能力。

但如果我们能进一步优化这个设置呢?如果我们可以优化 RAG 如何选择和利用信息以提升其性能呢?本文介绍了两个创新组件,旨在通过基于最新研究和我们的经验,从 LFQA 中提取具体示例来改善 RAG——DiversityRanker 和 LostInTheMiddleRanker。

将 LLM 的上下文窗口视为一顿美味的餐点,其中每段文字都是一种独特且富有风味的配料。就像一件精美的烹饪作品需要多样化的高质量配料一样,LFQA 问答也要求上下文窗口充满高质量、多样化、相关且不重复的段落。

在 LFQA 和 RAG 的复杂世界中,最大限度地利用 LLM 的上下文窗口至关重要。任何浪费的空间或重复的内容都会限制我们提取和生成答案的深度和广度。合理布置上下文窗口的内容是一项微妙的平衡工作。本文提出了掌握这一平衡的全新方法,这将提升 RAG 提供准确、全面响应的能力。

让我们探索这些令人兴奋的进展以及它们如何改善 LFQA 和 RAG。

背景

Haystack 是一个开源框架,提供实践 NLP 构建者的端到端解决方案。它支持从问答和语义文档搜索到 LLM 代理等广泛的用例。其模块化设计允许集成最先进的 NLP 模型、文档存储和当今 NLP 工具箱中所需的各种其他组件。

Haystack 中的一个关键概念是管道的思想。管道表示一个特定组件执行的一系列处理步骤。这些组件可以执行各种类型的文本处理,允许用户通过定义数据如何在管道中流动以及执行处理步骤的节点顺序,轻松创建强大且可定制的系统。

在基于网络的长篇问答中,管道发挥了关键作用。它从 WebRetriever 组件开始,该组件在网络上搜索和检索与查询相关的文档,自动将 HTML 内容剥离成原始文本。但一旦我们获取了与查询相关的文档,我们如何充分利用它们?我们如何填充 LLM 的上下文窗口以最大化答案的质量?如果这些文档尽管高度相关,但却重复且数量众多,有时会溢出 LLM 上下文窗口,该怎么办?

今天我们将介绍的组件正是在这里发挥作用的——DiversityRanker 和 LostInTheMiddleRanker。它们的目标是解决这些挑战并改善 LFQA/RAG 管道生成的答案。

DiversityRanker 增强了上下文窗口中选定段落的多样性。LostInTheMiddleRanker 通常在流水线中位于 DiversityRanker 之后,有助于减轻模型在长上下文窗口中必须访问相关信息时观察到的性能下降。接下来的部分将深入探讨这两个组件,并展示它们在实际使用案例中的效果。

DiversityRanker

DiversityRanker 是一个新型组件,旨在增强 RAG 流水线中上下文窗口选定段落的多样性。它基于一个原则,即多样化的文档集可以提高 LLM 生成答案的广度和深度。

图 1:DiversityRanker 算法文档排序过程的艺术表现,由 MidJourney 提供。请注意,此可视化更多是说明性的,而非精确的。

DiversityRanker 使用句子变换器来计算文档之间的相似度。句子变换器库提供了强大的嵌入模型,用于创建句子、段落甚至整个文档的有意义表示。这些表示或嵌入捕捉了文本的语义内容,使我们能够测量两段文本的相似性。

DiversityRanker 使用以下算法处理文档:

1. 它首先使用句子变换器模型计算每个文档和查询的嵌入。

2. 然后选择语义上最接近查询的文档作为第一个选择的文档。

3. 对于每个剩余的文档,它计算与已经选择的文档的平均相似度。

4. 然后选择平均上与已经选择的文档最不相似的文档。

5. 这个选择过程持续进行,直到所有文档都被选择,结果是一个文档列表,从对整体多样性贡献最大的文档到贡献最小的文档。

需要记住的技术说明:DiversityRanker 使用贪婪的局部方法来选择下一个文档,这可能无法找到文档的最优整体顺序。DiversityRanker 更注重多样性而非相关性,因此它应该在 TopPSampler 或其他更注重相关性的相似性排序器等组件之后放置在流水线中。通过在选择最相关文档的组件之后使用它,我们确保从已经相关的文档池中选择多样化的文档。

LostInTheMiddleRanker

LostInTheMiddleRanker 优化了所选文档在 LLM 上下文窗口中的布局。这个组件是解决最近研究中识别出的一个问题的一种方法[1],该研究表明 LLM 难以关注长上下文中间的相关段落。LostInTheMiddleRanker 将最佳文档交替放置在上下文窗口的开头和结尾,使 LLM 的注意力机制更容易访问和使用它们。为了理解 LostInTheMiddleRanker 如何排序给定的文档,可以想象一个简单的例子,其中文档由从 1 到 10 的单个数字按升序排列。LostInTheMiddleRanker 将这十个文档按以下顺序排序:[1 3 5 7 9 10 8 6 4 2]。

尽管这项研究的作者专注于一个问答任务——从文本中提取相关的答案片段——我们推测 LLM 的注意力机制在生成答案时,也会更容易关注上下文窗口开头和结尾的段落。

图 2. LLM 在提取上下文中间的答案时遇到困难,改编自 Liu 等(2023)[1]

LostInTheMiddleRanker 最适合作为 RAG 管道中的最后一个排名器,因为所给的文档已经基于相似性(相关性)进行选择,并按多样性排序。

在管道中使用新的排名器

在本节中,我们将探讨 LFQA/RAG 管道的实际应用案例,重点是如何集成 DiversityRanker 和 LostInTheMiddleRanker。我们还将讨论这些组件如何相互作用以及与管道中的其他组件的互动。

管道中的第一个组件是 WebRetriever,它使用程序化搜索引擎 API(如 SerperDev、Google、Bing 等)从网络中检索查询相关的文档。检索到的文档首先去除 HTML 标签,转换为原始文本,并可选择性地预处理成较短的段落。然后,这些文档会传递给 TopPSampler 组件,该组件根据与查询的相似性选择最相关的段落。

在 TopPSampler 选择相关段落集之后,它们会传递给 DiversityRanker。DiversityRanker 则根据段落的多样性对其进行排序,减少 TopPSampler 排序文档的重复性。

选定的文档随后传递给 LostInTheMiddleRanker。如前所述,LostInTheMiddleRanker 将最相关的段落放置在上下文窗口的开头和结尾,同时将排名最差的文档推到中间。

最终,合并的段落会传递给 PromptNode,PromptNode 将 LLM 调整为基于这些选定的段落回答问题。

图 3. LFQA/RAG 管道 — 作者提供的图像

新排序器已经合并到 Haystack 的主分支中,并将在 2023 年 8 月底的 1.20 版本中发布。我们在项目的示例文件夹中添加了新的 LFQA/RAG 管道演示。

演示展示了 DiversityRanker 和 LostInTheMiddleRanker 如何轻松集成到 RAG 管道中,以提高生成答案的质量。

案例研究

为了展示包含两个新排序器的 LFQA/RAG 管道的有效性,我们将使用一个包含半打问题的小样本,这些问题需要详细的回答。问题包括:“俄罗斯和波兰长期敌对的主要原因是什么?”,“全球和地方尺度上气候变化的主要原因是什么?”等等。要很好地回答这些问题,LLM 需要广泛的历史、政治、科学和文化来源,这使得它们非常适合我们的用例。

比较 RAG 管道生成的答案与两个新排序器(优化管道)以及没有这些排序器的管道(未优化管道)会涉及复杂的评估,需要人工专家的判断。为了简化评估并主要评估 DiversityRanker 的效果,我们改为计算注入 LLM 上下文中的上下文文档的平均成对余弦距离。我们将两个管道的上下文窗口大小限制为 1024 个词。通过运行这些示例 Python 脚本2,我们发现优化管道中注入 LLM 上下文的文档的平均成对余弦距离增加了约 20–30%3。这种成对余弦距离的增加本质上意味着所使用的文档更具多样性(而且重复性更少),从而为 LLM 提供了更广泛和丰富的段落供其回答参考。我们将把对 LostInTheMiddleRanker 及其对生成答案的影响的评估留到我们即将发布的文章中。

结论

我们探索了 Haystack 用户如何通过使用两个创新排序器:DiversityRanker 和 LostInTheMiddleRanker 来增强他们的 RAG 管道。

DiversityRanker 确保 LLM 的上下文窗口中填充了多样且不重复的文档,为 LLM 提供了更广泛的段落以便综合答案。同时,LostInTheMiddleRanker 优化了最相关段落在上下文窗口中的位置,使得模型更容易访问和利用最支持的文档。

我们的小案例研究通过计算注入 LLM 上下文窗口中的文档的平均成对余弦距离,确认了 DiversityRanker 的有效性,比较了优化的 RAG 管道(使用两个新排序器)和未优化管道(未使用排序器)。结果显示,优化的 RAG 管道使得平均成对余弦距离增加了约 20–30%。

我们已经展示了这些新型排名器如何有可能增强长篇问答和其他 RAG 管道。通过继续投资并扩展这些及类似的想法,我们可以进一步提升 Haystack 的 RAG 管道的能力,使我们更接近于打造看起来更像魔法而非现实的 NLP 解决方案。

参考文献

[1] “迷失在中间:语言模型如何使用长上下文” arxiv.org/abs/2307.03172

2 脚本: gist.github.com/vblagoje/430def6cda347c0b65f5f244bc0f2ede

3 脚本输出(答案): gist.github.com/vblagoje/738253f87b7590b1c014e3d598c8300b

增强 RAG 的答案:自我调试技术和认知负荷减少

原文:towardsdatascience.com/enhancing-rags-answer-self-debugging-techniques-and-cognitive-load-reduction-8c4273013d39

要求 LLM 自我诊断并自我修正提示,以提高答案质量。

Agustinus NalwanTowards Data Science Agustinus Nalwan

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

--

LLM 进行自我调试(图像由 MidJourney 生成)

检索增强生成(RAG)无疑是一种强大的工具,使用像 LangChain 或 LlamaIndex 这样的框架可以轻松构建。这样的集成便利性可能会让人觉得 RAG 是一种对于每个使用案例都容易构建的神奇解决方案。然而,在我们升级编辑文章搜索工具以提供语义更丰富的搜索结果和直接答案的过程中,我们发现基本的 RAG 设置存在不足,并遇到了许多挑战。构建一个用于演示的 RAG 既快速又简单,通常能在少量场景中产生足够令人印象深刻的结果。然而,实现生产就绪状态的最后阶段,即要求卓越质量的阶段,面临着重大挑战。特别是在处理包含成千上万篇领域特定文章的庞大知识库时,这种情况尤为明显,这种情况并不罕见。

我们对 RAG 的方法包括两个不同的步骤:

  1. 相关文档检索 通过采用密集和稀疏嵌入的融合,我们从 Pinecone 数据库中提取相关的文档块,考虑内容和标题。这些文档块随后根据与标题、内容以及文档年龄的相关性重新排序。然后选择排名前四的文档:既作为潜在的搜索结果,也作为生成直接答案的文档上下文。值得注意的是,这种方法不同于常见的 RAG 设置,更有效地解决了我们独特的文档检索挑战。

  2. 直接答案生成 在这里,问题、指令以及之前检索到的前四个文档片段(文档上下文)被输入到大型语言模型(LLM)中,以生成直接答案。

RAG 架构

我之前深入探讨了通过使用混合搜索和层次化文档排名技术来提升文档检索质量。在这篇博客中,我旨在分享关于优化和故障排除直接答案生成过程的见解。

确保高质量的文档检索

当输入低质量的文档上下文时,即使有一个高效的 LLM 进行直接答案生成也是徒劳的。例如,如果文档部分缺乏必要的信息来推导出某个问题的答案,那么 LLM 的有效性就会降低,无论模型的规模和认知能力如何。实现这一目标非常具有挑战性,因为它需要同时优化召回率和准确率。召回率对于避免忽略包含关键答案信息的文档片段至关重要,而准确率则对于减少传输的文档片段数量,专注于最相关的内容也很重要。这种考虑需要考虑到限制的上下文窗口大小和传输大量文档上下文的相关成本。因此,所选的 top-k 文档片段应代表最高质量。

LLM 扫描了文档以寻找高质量的内容(图像由 MidJourney 生成)。

因此,如果你发现生成的直接答案不令人满意,第一步应该是仔细审查你的文档检索过程。在我们的案例中,这占据了生成答案不准确的半数问题。检查完整的提示信息以确保在提示的文档上下文中存在必要的相关信息。如果缺失或不足,考虑使用混合搜索、层次化文档排名或评分提升等技术来改进你的文档检索策略。

在本博客的其余部分,我将向你介绍我用来提高直接回答质量的四种技术,以及我如何将这些技术融入我的提示中。请注意,我在博客中分享的提示可能会为简洁起见进行编辑,某些部分可能被省略,以突出讨论的相关方面,并避免使博客内容过于繁琐。此外,虽然我在整个博客中始终使用“2019 年马自达 CX-9 的燃油效率是多少?”作为示例,但重要的是要理解,我们的评估不仅限于这个问题。我们已使用类似问题的集合进行评估,以确保我们观察到的行为在大量评估样本中具有代表性。那么,我们开始吧。

清晰至关重要:在你的指令中要明确。

确保你给 LLM 的提示是明确且定义良好的至关重要。例如,如果你希望 LLM 在文档上下文中没有必要信息时避免生成答案,你必须明确指定这一点。以下是一个示例提示。

You are a friendly chatbot who are required to respond to questions using 
details from excerpts taken from documents about cars. Do not source 
information from other than the document excerpts provided. If you don't know
the answer simply return nothing.

特别强调指令“如果你不知道答案,就什么也不要返回。” LLM 面临与界限相关的挑战,就像你高中时那个聪明但让人讨厌的朋友,总是举手回答老师的问题,无论他们对回答的信心如何。他们不确定提供一个错误答案(称为虚幻)的影响是否比完全不回答更糟。因此,通过包含这一指令,你在表达你的偏好。

此外,具体要求根据你的使用案例而异。对于我们的需求,我们期望 LLM 仅依赖于我们的知识库,这意味着它应该仅从提供的文档片段中的信息生成答案。所有 LLM 都在大量的互联网、书籍和其他媒介数据上进行过训练。尽管这些来源可能包含潜在的答案信息,但它们的可靠性和与我们知识库的一致性无法保证。因此,我们加入了额外的指令:“不要从提供的文档摘录以外的地方获取信息。”

LLM 正在仔细阅读提供的指令(图像由 MidJourney 生成)。

通过直接和精确的提示,你可以大大减少 LLM 产生毫无根据或“虚幻”答案的可能性。

让 LLM 自我诊断和自我修正。

没错!可以要求 LLM 识别并纠正自身错误。与传统算法相比,LLM 具有推理能力,这使得它能够执行多种任务,包括自我调试。

LLM 正在进行自我诊断(图像由 MidJourney 生成)。

例如,在我们的项目中,我们遇到了一个问题,查询“2019 年 Mazda CX-9 的燃油效率是多少?”时,LLM 生成了一个不准确的回答,声称“2019 年 Mazda CX-9 的燃油效率是 8.8L/100km。”经过仔细检查,我们确认确实提供了正确的文档上下文。相关信息包含在我们提示中的一篇标题为“2019 年 Mazda CX-8 与 Mazda CX-9 比较”的文章中。然而,不知为何,LLM 却从 2018 年 Mazda CX-9 的文章中提取了燃油效率信息。下面是我们的原始提示。

You are a friendly chatbot who are required to respond to questions using 
details from excerpts taken from documents about cars. Do not source 
information from other than the document excerpts provided. If you don't know
the answer simply return nothing.
At the end of your answer please provide the source(s) of the article
you source the answer from in the following format.
SOURCE: 123-pl, 555-pl, etc.

QUESTION: what is mazda cx9 2019 fuel efficiency?

=========
EXTRACTED DOCUMENTS:

------------------------------
Source: 5466-pl
Title: Mazda CX-9 2018 Review
Content:
... Fuel use is a constant consideration, 
too, we’ve piled on thousands of kilometres, mostly with two people on board, 
and the reading still hovers around 10.0L/100km. Note: this latest 
consumption figure is marginally up on the two-wheel drive version 
of the CX-9, which clocked an average of 9.2L/100km according to both 
the trip computer and our own independent testing. Both are well up on 
their claims. 
------------------------------
Source: 6248-pl
Title: Mazda CX-8 v Mazda CX-9 2019 Comparison
Content:
... Which wins, and why? So, we’ve saved the fuel consumption 
figures till last. The Mazda CX-8 averaged 7.1L/100km for our tour from 
Adelaide, up to the Flinders Ranges and back again. The CX-9 averaged 
10.1L/100km. As both cars have a 74-litre fuel tank you’re going to 
get further between refuels in the CX-8, paying a little more for it at 
the bowser and pumping less CO2 into the atmosphere ...
------------------------------
...

使用 GPT3.5 turbo,它生成了以下不正确的回答(正确答案是 10.1L/100 km)。

The fuel efficiency of the Mazda CX-9 2019 varies depending on factors 
such as driving conditions and load. In one of the provided sources, 
it was mentioned that the fuel consumption for the CX-9 2019 
was around 10.0 liters per 100 kilometers when driven with 
two people on board...

SOURCE: 5466-pl

当我使用 GPT4.0 测试相同的提示时,得到了正确的答案。

The Mazda CX-9 2019 model's fuel efficiency is not explicitly listed 
in the provided documents, but the 2018 model has a consumption figure 
of around 10.0L/100km, and it is noted that this is marginally up on 
the two-wheel drive version which clocked an average of 9.2L/100km. 
The 2017 review of the Mazda CX-9 also indicated a consumption of 
9.2L/100km over 2500km, which is higher than Mazda's claimed 8.4L/100km. 
The 2019 comparison between the CX-8 and CX-9 suggests that the CX-9 
averaged 10.1L/100km. These figures could be considered roughly 
equivalent for the 2019 model, barring any major changes between 
the 2018 and 2019 models.

尽管如此,GPT4 在我们的使用案例中并不经济,因为它的成本是 GPT3.5 Turbo 的 10 倍。因此,这一挑战促使我们寻找与 GPT3.5 Turbo 兼容的解决方案,激发了自我调试的概念。

想象一下,让某人根据您提供的文档回答问题,而他们给出了错误的回应。您会采取什么步骤来了解错误发生的原因?您可以通过展示正确答案和来源材料来简单地询问他们为什么犯错。与 LLM 也是一样。我们询问 LLM,但为了有效地进行此操作,我们需要能够在 LLM 提供答案后继续与其对话。

通过使用如www.poe.com这样的平台,我们可以重现与 LLM 的交互。我们选择相同的 LLM 模型,将整个提示输入控制台。生成的输出与我们代码中遇到的精确错误答案相符。随后,我们与 LLM 进行对话,这在促进自我调试过程中是一个关键步骤,如下所述。

poe.com 网络界面

这是我与 LLM 的完整对话记录。

我:您给我的答案不正确。它是 2018 年 Mazda CX-9 的燃油效率,如文章标题“Mazda CX-9 2018 Review”所述。正确的答案是 10.1L/100km,如文章标题“Mazda CX-8 与 Mazda CX-9 2019 比较”所述。

LLM:我为这个错误道歉。您说得对,正确的信息确实在您提到的文章中提供了。

我:没关系。你为什么犯了这个错误?

LLM:我没有意识到文章中引用的年份是汽车的型号年份。我以为这是文章的年份。

根据最后的回应,我现在已识别出 LLM 错误的原因。这激发了另一个想法:如果 LLM 可以理解并解释其误解,也许它还可以调整我的初始提示,以防未来重复同样的错误。这绝对值得一试!

我:我应该如何修改我的提示,以便下次你不会重复这个错误?

LLM:你可以具体说明文章标题/内容可能包含汽车型号年份,这应该与问题中询问的型号年份匹配。

我:请你修改我的提示以包含你的建议吗?

在第二次请求时,LLM 向我提供了一个整合了建议改进的修订提示。

You are a friendly chatbot who is required to respond to questions 
using details from excerpts taken from documents about cars. Do not 
source information from other than the document excerpts provided. 
If you don't know the answer, simply return nothing.
Please ensure that the article title/content might contain car 
model year information, which should match the model year asked 
in the question.

At the end of your answer, please provide the source(s) of the article 
you source the answer from in the following format.
SOURCE: 123-pl, 555-pl, etc.

QUESTION: What is Mazda CX9 2019 fuel efficiency?

尽管使用了这个调整后的提示,但观察到它仍然对我之前的问题产生了错误的回答,这让人感到相当沮丧。

The 2019 Mazda CX-9's fuel efficiency is not mentioned in the provided 
documents. Therefore, I do not have information on the fuel efficiency 
of the 2019 Mazda CX-9 based on the excerpts available.

尽管如此,它还是给了我一些关于需要包含模型年份的信息的见解。因此,我决定手动修改提示,加入了推荐提到的模型年份。我甚至提供了一个简短的期望行为示例来指导 LLM。

基于 LLM 建议的修订提示

You are a friendly chatbot who are required to respond to questions 
using details from excerpts taken from documents about cars of 
particular model years.
These documents may have information about the model year of car in 
review within its content or title. Eg. Volvo XC90 2018 Review, means 
the review is only for
 Volvo XC90 model year 2018\. If a user provides the model year in 
their question, ensure that your answer is sourced from an article 
from the corresponding model year.
 Eg. When the user is asking for "What is the safety feature of 
Volvo XC90 2019?", you should not use any information within the 
previous article as they are of different model year.
When the model year or the car make and model is not mentioned in 
this article content, you can assume it is the same as the make, 
model and model year as mentioned in the article title.
At the end of your answer please provide the source(s) of the 
article you source the answer from in the following format.
SOURCE: 123-pl, 555-pl, etc.

QUESTION: what is mazda cx9 2019 fuel efficiency?

=========
EXTRACTED DOCUMENTS:

------------------------------
Source: 5466-pl
Title: Mazda CX-9 2018 Review
Content:
... Fuel use is a constant consideration, 
too, we've piled on thousands of kilometres, mostly with two people on board, 
and the reading still hovers around 10.0L/100km. Note: this latest 
consumption figure is marginally up on the two-wheel drive version 
of the CX-9, which clocked an average of 9.2L/100km according to both 
the trip computer and our own independent testing. Both are well up on 
their claims. 
------------------------------
Source: 6248-pl
Title: Mazda CX-8 v Mazda CX-9 2019 Comparison
Content:
... Which wins, and why? So, we've saved the fuel consumption 
figures till last. The Mazda CX-8 averaged 7.1L/100km for our tour from 
Adelaide, up to the Flinders Ranges and back again. The CX-9 averaged 
10.1L/100km. As both cars have a 74-litre fuel tank you're going to 
get further between refuels in the CX-8, paying a little more for it at 
the bowser and pumping less CO2 into the atmosphere ...
------------------------------
...

新的提示现在给了我正确的答案!

The fuel efficiency of the Mazda CX-9 for the 2019 model year can be 
found in the article titled "Mazda CX-8 v Mazda CX-9 2019 Comparison." 
According to the article, the Mazda CX-9 averaged 10.1L/100km in 
fuel consumption for their test. This figure provides information 
about the fuel efficiency of the Mazda CX-9 for the 2019 model year.

SOURCE: 6248-pl

然而,需要记住的是,这种方法可能不会始终产生积极的结果。我使用这种方法获得关于 LLM 解释为何犯错的见解的成功率约为 75%,而让 LLM 为我修改提示以产生正确答案的效果约为 25%。尽管如此,拥有这种方法总比没有方法要好。

意识到认知负荷。

保持指示准确对于引导 LLM 产生期望的答案至关重要。然而,过于详细的提示可能会对像 GPT-3.5 Turbo 这样较小的 LLM 产生不利影响,因为它们的认知能力比 GPT-4 低。想知道为什么吗?让我们通过一个示例来演示这一点。

LLM 正在经历认知负荷(图像由 MidJourney 生成)。

我们最初使用 Langchain 模板构建的提示采用了一次性技术。例如,它包括了一个完整互动的单一示例,希望 LLM 能够更好地理解我们的意图。然而,当使用 GPT3.5 Turbo 时,我们观察到一次性提示比零次提示产生了更模糊的回答。

以下是我们基于Langchain模板的一次性提示。提供的示例来自一个完全不同的领域(非汽车),提示是通用的,没有提及汽车相关的文章。

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-pl

Content: 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.
11.7 Severability.
Source: 30-pl

Content: (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-pl

...

QUESTION: what is mazda cx9 2019 fuel efficiency?

=========
------------------------------
Title: Mazda CX-9 2018 Review
Content:
Fuel use is a constant consideration, too, we’ve piled on thousands of 
kilometres, mostly with two people on board, and the reading still hovers 
around 10.0L/100km. 
Note: this latest consumption figure is marginally up on the two-wheel 
drive version of the CX-9, which clocked an average of 9.2L/100km 
according to both the trip computer and our own independent testing. 
...

------------------------------
Title: Mazda CX-8 v Mazda CX-9 2019 Comparison
Content:
nomads. Which wins, and why? So, we’ve saved the fuel consumption figures 
till last. The Mazda CX-8 averaged 7.1L/100km for our tour from Adelaide, 
up to the Flinders Ranges and back again. The CX-9 averaged 10.1L/100km. 
As both cars have a 74-litre fuel tank you’re going to get further 
between refuels in the CX-8, paying a little more for it at the bowser 
and pumping less CO2 into the atmosphere. Health-affecting particulates? 
Well, that’s another story.
...

FINAL ANSWER:

给出的回答极其不清晰,提到了不同车型年份的马自达 CX-9 的燃油效率。

The fuel efficiency of the 2019 Mazda CX-9 varies depending on driving 
conditions and other factors. According to available information:
In one source, it is mentioned that the trip computer indicated fuel 
consumption of 9.2L/100km in 2500km worth of driving, which is 
considerably higher than Mazda's claimed 8.4L/100km for the CX-9\. 
This figure was achieved with one or two people in the car, not a 
full family and their belongings. However, further testing and simulations 
were mentioned to corroborate fuel consumption.

In another source comparing the Mazda CX-8 and CX-9, it is stated that 
the CX-8 averaged 7.1L/100km on a specific tour, while the CX-9 
averaged 10.1L/100km. These figures indicate that the CX-8 is more 
fuel-efficient than the CX-9 in this particular comparison.

In a different source discussing the 2023 Mazda CX-8, the official 
combined fuel consumption claim for the CX-8 Touring Active is 6.0L/100km, 
achieved with the assistance of the i-Stop function. However, during a 
test drive on open roads with one passenger and little luggage, the 
Touring Active averaged 8.6L/100km.

So, the fuel efficiency of the 2019 Mazda CX-9 may vary depending 
on driving conditions and the specific model/configuration. It's 
recommended to refer to the official manufacturer's specifications 
and conduct real-world testing to determine the exact fuel efficiency 
of a specific CX-9 model.

初始提示的模糊性质,加上一个无关的单次示例,可能部分地导致了这个问题。为验证这个理论,我制定了一个更具针对性的提示,其中包含了特定领域的指令和一个与我们实际用例紧密相关的示例。以下是这个新提示的详细信息,这将有助于确定它是否能带来显著的差异。

You are a friendly chatbot who are required to respond to questions 
using details from excerpts taken from documents about cars of 
particular model years.
These documents may have information about the model year of car in 
review within its content or title. Eg. Volvo XC90 2018 Review, means 
the review is only for
 Volvo XC90 model year 2018\. If a user provides the model year in 
their question, ensure that your answer is sourced from an article 
from the corresponding model year.
 Eg. When the user is asking for "What is the safety feature of 
Volvo XC90 2019?", you should not use any information within the 
previous article as they are of different model year.
When the model year or the car make and model is not mentioned in 
this article content, you can assume it is the same as the make, 
model and model year as mentioned in the article title.
At the end of your answer please provide the source(s) of the 
article you source the answer from in the following format.
SOURCE: 123-pl, 555-pl, etc.

QUESTION: What is Volvo XC90 2020 fuel efficiency?
=========

EXTRACTED DOCUMENTS:

------------------------------
Source: 22858-pl
Title: Volvo XC90 2020 Review
Content:
costs) Engine: 2.0-litre four-cylinder turbocharged and supercharged 
petrol Output: 246kW/440Nm Transmission: Eight-speed automatic 
Fuel: 8.5L/100km (ADR Combined) CO2: 199g/km (ADR Combined) ... 
------------------------------
Source: 22855-pl
Title: Volvo XC90 2020 Review
Content:
The brakes also need a hefty prod for downhill corners. All that weight 
impacts fuel consumption. Volvo claims an 8.5L/100km average, but we 
consumed premium unleaded at a rate of 11.8L/100km during our test ...
------------------------------
Source: 23987-pl
Title: Volvo XC90 T6 R-Design 2019 Review
Content:
the engine’s progress: there is minimal turbo lag and the gearbox is 
such that the four-pot transitions cleanly out of corners and 
intersections, slickly picking up a new gear. Fuel consumption is 
something of a misnomer, however. The claim reads 8.5L/100 but 
in reality we used 9.4L/100km with quite a bit of highway driving ...
------------------------------
...

===========
FINAL ANSWER: The fuel efficiency of the volvo xc90 2020 is 8.5l/100km 
(adr combined). this information is based on the review of the 
Volvo XC90 2020.
SOURCES: 22858-pl

QUESTION: what is mazda cx9 2019 fuel efficiency?
=========
EXTRACTED DOCUMENTS:

------------------------------
Source: 5466-pl
Title: Mazda CX-9 2018 Review
Content:
Fuel use is a constant consideration, too, we’ve piled on thousands 
of kilometres, mostly with two people on board, and the reading still 
hovers around 10.0L/100km. Note: this latest consumption figure is 
marginally up on the two-wheel drive version of the CX-9, which clocked an 
average of 9.2L/100km according to both the trip computer and our own 
independent testing ...
------------------------------
Source: 6248-pl
Title: Mazda CX-8 v Mazda CX-9 2019 Comparison
Content:
Which wins, and why? So, we’ve saved the fuel consumption 
figures till last. The Mazda CX-8 averaged 7.1L/100km for our tour 
from Adelaide, up to the Flinders Ranges and back again. The CX-9 
averaged 10.1L/100km. As both cars have a 74-litre fuel tank you’re 
going to get further between refuels in the CX-8, paying a little 
more for it at the bowser and pumping less CO2 into the atmosphere ... 
------------------------------
...

===========
FINAL ANSWER: 

提供的回答仍然不准确。它还声称没有必要的信息来回答问题,这也是不准确的。

The fuel efficiency of the Mazda CX-9 in the year 2019 is not available 
in the provided documents. The information in the documents pertains to 
the Mazda CX-9 in 2017 and earlier years. Therefore, I cannot provide 
the fuel efficiency for the Mazda CX-9 in 2019 based on the provided 
documents.

SOURCES: 5466-pl, 4900-pl, 6248-pl, 168-pl

通过省略示例并选择零次提示方法,我们成功地使 LLM 生成了正确的答案。

You are a friendly chatbot who are required to respond to questions 
using details from excerpts taken from documents about cars of 
particular model years.
These documents may have information about the model year of car in 
review within its content or title. Eg. Volvo XC90 2018 Review, means 
the review is only for
 Volvo XC90 model year 2018\. If a user provides the model year in 
their question, ensure that your answer is sourced from an article 
from the corresponding model year.
 Eg. When the user is asking for "What is the safety feature of 
Volvo XC90 2019?", you should not use any information within the 
previous article as they are of different model year.
When the model year or the car make and model is not mentioned in 
this article content, you can assume it is the same as the make, 
model and model year as mentioned in the article title.
At the end of your answer please provide the source(s) of the 
article you source the answer from in the following format.
SOURCE: 123-pl, 555-pl, etc.

QUESTION: what is mazda cx9 2019 fuel efficiency?
=========
EXTRACTED DOCUMENTS:

------------------------------
Source: 5466-pl
Title: Mazda CX-9 2018 Review
Content:
Fuel use is a constant consideration, too, we’ve piled on thousands 
of kilometres, mostly with two people on board, and the reading still 
hovers around 10.0L/100km. Note: this latest consumption figure is 
marginally up on the two-wheel drive version of the CX-9, which clocked an 
average of 9.2L/100km according to both the trip computer and our own 
independent testing ...
------------------------------
Source: 6248-pl
Title: Mazda CX-8 v Mazda CX-9 2019 Comparison
Content:
Which wins, and why? So, we’ve saved the fuel consumption 
figures till last. The Mazda CX-8 averaged 7.1L/100km for our tour 
from Adelaide, up to the Flinders Ranges and back again. The CX-9 
averaged 10.1L/100km. As both cars have a 74-litre fuel tank you’re 
going to get further between refuels in the CX-8, paying a little 
more for it at the bowser and pumping less CO2 into the atmosphere ... 
------------------------------
...

===========
FINAL ANSWER:
The fuel efficiency of the Mazda CX-9 for the 2019 model year can be 
found in the article titled "Mazda CX-8 v Mazda CX-9 2019 Comparison." 
According to the article, the Mazda CX-9 averaged 10.1L/100km in 
fuel consumption for their test. This figure provides information 
about the fuel efficiency of the Mazda CX-9 for the 2019 model year.

SOURCE: 6248-pl

为进一步验证问题是否源于认知过载,我们使用 GPT-4 进行了单次提示测试,结果如预期地得到了正确的回答。

The fuel efficiency of the Mazda CX-9 2019 is not directly stated in 
the documents provided. However, the closest information from a 2019 
comparison mentions that the CX-9 averaged 10.1L/100km during a test. 
It's important to note that actual fuel efficiency can vary based on 
driving conditions, habits, and vehicle maintenance.

SOURCE: 6248-pl

为了验证我们的假设,我们对其他语言模型,如 Claude Instant 和 Claude 2,进行了类似的测试,结果显示了相同的模式。Claude 2 成功处理了我们的单次提示,提供了正确的答案,而 Claude Instant 则没有。Claude 2 和 GPT-4 具有相似的认知能力,均优于 Claude Instant 和 GPT-3.5 Turbo。这支持了我们的理论,即认知能力较低的模型在处理复杂指令时会遇到困难。我对这些结果感到非常满意,因为零次提示法可以使提示更简洁,从而降低成本,并且它适用于像 GPT-3.5 Turbo 或 Claude Instant 这样的较便宜、更小的模型。

要求提供证明。

要求语言模型提供其生成答案的来源是一种非常有效的策略,能够减少幻觉并防止幻觉,特别是当相关信息不在提供的文档上下文中时。

LLM 正在追踪源材料的来源(图像由 MidJourney 生成)。

实现这个目标可以通过使用不同的提示来引导语言模型。一个策略是指导 LLM 识别出影响其响应的文档上下文的特定部分。加入一个命令,要求 LLM 在回答中引用文档源 ID,可以让你忽略没有源 ID 或源 ID 错误的回应。虽然不常见,但 LLM 可能会引用你文档块中未包含的外部来源,例如它在训练期间接触到的网站。

At the end of your answer please provide the source(s) of the 
article you source the answer from in the following format.
SOURCE: 123-pl, 555-pl, etc.

另一种策略是指导 LLM 通过明确引用回答问题的文档块中的句子来回应。

Please answer by quoting the exact sentence(s) within the provided 
document excerpts..
At the end of your answer please provide the source(s) of the 
article you source the answer from in the following format.
SOURCE: 123-pl, 555-pl, etc.

这种方法可能对 LLM 施加额外的约束,并不总是实用,就像我们文档的情况一样。我们的文档经常缺乏明确回应常见问题的句子。因此,LLM 没有特定的句子可供引用,这导致没有回应。如果我们允许 LLM 使用文档中嵌入的知识来构造自己的句子,它将能够提供答案。

结论

使用语言模型(LLM)构建应用程序与使用传统算法截然不同。传统算法中,详细的指令使你完全控制过程,调试涉及观察变量值,确保这些指令是正确的。相比之下,使用 LLM 就像是雇用一个人来在你的软件中执行这些过程。由于 LLM 的反应类似于人类,故障排除和调试需要类似于确定一个人可能犯错的技术,主要通过提问。

尽管语言模型(LLM)技术仍在发展中,且可能出现错误和不准确,但它显示出了相当大的潜力,并且有有效的策略可以大大减轻这些问题。鉴于 LLM 技术的快速发展以及不断创新的方法和工具,关于这一领域在不久的将来可能带来的变革前景,充满了强烈的兴奋感。

我们即将部署我们的 RAG 设置用于生产,并且在我们继续学习的过程中,我期待在即将发布的博客中分享更多见解。

如果你发现这篇文章有趣且有帮助,我将感激你用 20 次鼓掌来支持我。我也渴望从你的见解和经验中学习,所以欢迎通过回复分享你的想法。此外,如果你有兴趣,我欢迎你在 LinkedIn 上与我联系。

最后,我想向 Carsales.com 的杰出团队表达我的感谢,感谢他们在这一旅程中的支持。

医学信号处理中的集成平均

原文:towardsdatascience.com/ensemble-averaging-in-medical-signal-processing-17116d0cf0d2?source=collection_archive---------14-----------------------#2023-05-02

简单却强大的方法

Omar AlkousaTowards Data Science Omar Alkousa

·

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

--

作为研究人员、数据科学家/分析师,甚至在任何科学领域处理数据是不可避免的。因此,你处理的数据的可靠性至关重要,因为它决定了你工作的可靠性。

在本文中,我们将介绍集成平均方法,一种简单但强大的方法,并介绍其在信号数据上的应用。

图片由 Eran Menashri 拍摄于 Unsplash

背景

平均值是一个可以描述一组数值的单一值。它可以定义为变量的总和除以变量的数量。如果我们有一组变量 {x1, x2, …, xn},则可以按如下方式计算平均值:

你可以考虑一个简单的例子来介绍集成平均的概念,即测量在测量过程中温度稳定的溶液的温度。可以使用电子传感器在十秒钟内测量三次温度。集成平均可以通过取这三次测量的平均值来完成。

从上述示例来看,集成平均方法要求能够重复你所进行的实验,并保持测量系统的相同属性 [1]。这在许多实际情况中很难应用。因此,在许多情况下可能考虑的解决方案是一次性进行实验,但同时使用多个传感器 [1],以产生多个相应实验的观测值。

在信号类型的数据(时间序列)的情况下,集成平均方法是通过逐点平均信号样本来完成的 [2]。因此,在应用该方法之前,一个先决条件是生成的信号在时间上对齐 [1]。下面是应用集成平均于信号数据的示例。

使用两个实验应用集成平均方法。注意信号的样本在时间上是对齐的。 [作者提供的图像]

为什么使用集成平均方法?我们为什么要重复实验?

我第一次接触到集成平均是在生物力学实验室,该实验室专门研究步态模式和分析人体运动。当我的教授指示对同一患者进行实验以获得至少三个成功的实验时,我问为什么要三次而不是一次成功的实验?!

答案是计算这三个实验的集成平均值,以获得具有更高可靠性且不易受到随机噪声影响的最终信号。

常识上,重复实验确保你获得的数据是感兴趣的数据。但当然,实验重复的成本和时间也需要考虑。

从信号处理的角度来看,集成平均在增强信号数据方面有许多优点。这些优点可以总结如下:

  • 集成平均对于过滤信号中的随机噪声非常有用 [2]。

  • 应用集合平均方法可以提高信噪比(SNR)。如果我们有 N 次重复,改进程度与 N 的平方根成正比 [2]。

真实世界的医学示例

集合平均方法的应用是无限的。医学领域的一个很好的例子是视觉诱发响应/潜能测试(VER 或 VEP 测试)的理念。该测试的目的是测量对视觉刺激的响应,该响应以电信号的形式产生在大脑的视觉皮层中。该测试依赖于特定时间间隔的重复刺激 [3]。视觉刺激通过黑白棋盘图案的屏幕进行 [3]。重复刺激的信号逐点平均(集合平均)以确定 VER 信号 [1],请记住信号在刺激过程中是时间对齐的。以下是如何使用集合平均方法提取 VER 信号的示例。

我们将使用的信号数据可以通过以下链接(MIT 许可)获取。数据包括 100 个以 200 Hz 采样率记录的电信号。

使用 Python 进行集合平均

首先,我们将导入所需的 Python 包:

  • Numpy:用于数组操作和算术计算。

  • Matplotlib:用于可视化 VER 响应的信号数据。

  • Scipy:我们将使用 scipy.io 模块读取数据文件(.MAT 文件类型)。

# Import the required packages
import matplotlib.pyplot as plt
import numpy as np
from scipy import io

现在,我们使用scipy.io.loadmat方法读取包含电信号的数据文件。输出是文件内容的字典。电信号可以在“ver”项中找到。

# Read a Mat file using scipy.io
mat = io.loadmat('ver.mat')
# Exract the electrical signals
ver = mat['ver']

让我们探索一下我们正在处理的数据的大小。

# The size of variable "ver"
print(ver.shape)
(100, 500)

这意味着我们有 100 个信号,每个信号有 500 个样本。对这些数据应用集合平均方法将平均这 100 个信号,并生成一个包含 500 个样本的信号。

提取信号数据后,我们将定义采样率以生成信号的时间轴。

# Calculate the sampling rate
sampling_rate = 200.0
# Calculate the duration of the recordings
duration = ver.shape[1]/sampling_rate
# Generate the time axis of the recordings
time_axis = np.arange(0, duration, 1/sampling_rate)

现在我们有了时间轴和要研究的信号,我们可以在同一时间轴上可视化一些信号,以查看单一视觉响应的样子。我们将仅可视化前 10 个信号。

# Subplot figure with 10 rows and one column
fig, axs = plt.subplots(10, figsize=(10,5), sharex=True)

# Plot the first 10 recordings
for i in range(10):
    axs[i].plot(time_axis, ver[i,:])
    # Hide the values of y-axis
    axs[i].set_yticks([])
    # Fit the x-axis along the signal
    axs[i].set_xlim([time_axis[0], time_axis[-1]])

# Set a title and xlabel
axs[0].set_title('The first 10 Signals')
axs[9].set_xlabel('Time [Sec]')
plt.show()

数据的前 10 个信号。[作者提供的图像]

注意,单一信号中无法识别视觉响应。因此,我们将使用集合平均法来确定 VER。

# Apply the ensamble averaging method on the Recordings.
ver_signal = np.mean(ver, axis=0)
# Extract the actual VER from the Mat file
actual_ver = np.reshape(mat['actual_ver'], [500])

# Plot the ensambled EEG recordings and the actual VER
plt.plot(time_axis, ver_signal, color='green', label='Ensamble Averaging')
plt.plot(time_axis, actual_ver, color='red', label='Actual VER')
plt.xlim([time_axis[0], time_axis[-1]])
plt.xlabel('Time [sec]')
plt.ylabel('Average')
plt.title('Visual Evoked Response')
plt.legend()
plt.show()

使用集合平均法对 100 个信号进行 VER 分析。[作者提供的图像]

结论

  • 我们介绍了集成平均法,这是一种简单但非常有用的方法,你可以用来提取可靠的数据。

  • 我们指出了重复实验的原因及其如何提升数据的可靠性。

  • 应用集成平均法能将信号的信噪比(SNR)提高到 N(重复次数)的平方根。此外,最终的信号数据也不容易受到随机噪声的影响。

  • 我们介绍了一个例子,利用集成平均法来确定视觉诱发反应(VER),基于特定时间的重复刺激。

参考文献

[1] Semmlow, J. (2018). 时间域的信号分析。在生物工程师的电路、信号与系统(第 51-110 页)。Elsevier。 doi.org/10.1016/B978-0-12-809395-5.00002-3

[2] Thomas, T. (2005). 集成平均滤波器用于噪声降低。国际计算机与通信工程高级研究期刊5(8)。

[3] Tripathy, K., Hsu, J., & Lim, J. (2022 年 11 月 10 日). 视觉诱发电位/反应(VEP/VER)。美国眼科学会。 eyewiki.aao.org/Visual_Evoked_Potential/_Response_(VEP/VER)

集成学习:袋装和提升

原文:towardsdatascience.com/ensemble-learning-bagging-and-boosting-23f9336d3cb0?source=collection_archive---------0-----------------------#2023-02-23

Jonas DieckmannTowards Data Science Jonas Dieckmann

·

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

--

你想把你的数据科学技能提升到一个新水平吗?你是否希望提高模型的准确性,并基于数据做出更明智的决策?那么是时候探索袋装和提升的世界了。运用这些强大的技术,你可以提升模型的性能,减少错误,并做出更准确的预测。

图片由 Unsplash 提供

无论你是在处理分类问题、回归分析,还是其他数据科学项目,bagging 和 boosting 算法都可以发挥关键作用。在本文中,我们#1 总结了集成学习的主要思想,介绍了#2 bagging和#3 boosting,然后#4 对比了这两种方法,以突出相似性和差异。

所以,让我们准备好 bagging 和 boosting 取得成功吧!

#1:介绍和主要思想:集成学习

那我们什么时候应该使用它呢?显然,当我们看到模型的过拟合或欠拟合时。让我们从 bagging 和 boosting 的关键概念开始,这两者都属于集成学习技术的范畴:

作者提供的图片

集成学习的主要思想是使用多个算法和模型一起完成相同的任务。虽然单一模型仅使用一个算法来创建预测模型,但 bagging 和 boosting 方法旨在结合多个算法,以实现比单独学习更高一致性的更好预测。

示例:图像分类

本质概念通过涉及图像分类的教学插图来概括。假设有一组图像,每张图像都附有一个类别标签,表示动物的种类,用于训练模型。在传统建模方法中,我们会尝试几种技术并计算准确性,以选择一种优于其他的技术。假设我们在这里使用逻辑回归、决策树和支持向量机,这些模型在给定的数据集上表现不同。

作者提供的图片

在上述示例中,我们观察到逻辑回归和决策树模型将特定记录预测为狗,而支持向量机将其识别为猫。由于各种模型对特定记录有各自的优缺点,集成学习的关键思想是结合这三种模型,而不是选择表现出最高准确度的单一方法。

这个过程称为聚合投票,它结合了所有基础模型的预测,以得出一个被认为比任何单独模型更精确的预测。

偏差-方差权衡

下一个图表可能对你们中的一些人来说很熟悉,但它很好地表示了测试误差率上偏差和方差之间的关系和权衡。

你可能对以下概念很熟悉,但我认为它有效地说明了偏差和方差与测试误差率之间的相关性和权衡。

作者提供的图片

模型的方差与偏差之间的关系是方差的减少会导致偏差的增加,反之亦然。为了实现最佳性能,模型必须处于一个平衡点,在这个点上测试错误率最小化,同时方差和偏差得到适当平衡。

集成学习可以帮助将这两种极端情况平衡到更稳定的预测中。一种方法叫做袋装(bagging),另一种方法叫做提升(boosting)。

#2:袋装(自助聚合)

一个随机的袋子。图片来源于Unsplash

让我们首先关注一种叫做自助聚合的袋装技术。自助聚合旨在通过减少模型的方差来解决前一个图表中的右侧极端问题,以避免过拟合。

作者提供的图片

目的在于拥有多个相同学习算法的模型,这些模型通过原始训练数据的随机子集进行训练。这些随机子集称为袋子,可以包含数据的任何组合。每个数据集都用于拟合一个独立的模型,该模型对给定数据产生独立的预测。然后将这些预测聚合成一个最终分类器。这种方法的想法实际上非常接近我们最初的玩具示例中的猫和狗。

通过使用随机数据子集,过拟合的风险被减少和通过对子模型结果进行平均来平滑。所有模型并行计算,然后在之后进行聚合。

最终的集成聚合计算使用简单平均(用于回归问题)或简单多数投票(用于分类问题)。为此,每个随机样本中的每个模型都会为该特定子集产生一个预测。对于平均值,这些预测只是相加然后除以创建的袋子数量。

作者提供的图片

简单多数投票的工作原理类似,但使用预测的类别而不是数值。算法确定预测最多的类别,并假设多数就是最终的聚合结果。这再次非常类似于我们的玩具示例,其中三种算法中的两种预测图片为狗,因此最终聚合结果也是狗的预测。

随机森林

对于袋装方法,一个著名的扩展是随机森林算法,它利用了袋装的思想,但也使用特征的子集,而不仅仅是条目的子集。另一方面,袋装方法会考虑所有给定的特征。

袋装的代码示例

接下来,我们将探讨一些来自sklearn.ensemble库的有用 python 函数。名为BaggingClassifier的函数有一些参数,可以在文档中查阅,但最重要的参数是base_estimatorn_estimatorsmax_samples

from sklearn.ensemble import BaggingClassifier 

# define base estimator 
est = LogisticRegression() # or est = SVC() or est = DecisionTreeClassifier

# n_estimators defines the number of base estimators in the ensemble 
# max_samples defines number of samples to draw from X to train each base estimator 

bag_model = BaggingClassifier(base_estimator= est, n_estimators = 10, max_samples=1.0) 

bag_model = bag_model.fit(X_train, y_train) 

Prediction = bag_model.predict(X_test)
  • base_estimator: 你需要在第一个参数中提供随机子集在袋装过程中应该使用的基础算法。例如,这可以是逻辑回归、支持向量分类、决策树或更多其他算法。

  • n_estimators: 估计器的数量定义了你希望在这里创建多少个袋,默认值为 10。

  • max_samples: 最大样本数定义了从 X 中抽取多少样本来训练每个基础估计器。默认值为 1.0,意味着应该使用所有现有条目。你也可以通过将其设置为 0.8 来表示只使用 80%的条目。

设置场景后,该模型对象的工作方式类似于许多其他模型,可以使用fit()过程进行训练,包括来自训练集的 X 和 y 数据。对测试数据的相应预测可以使用predict()完成。

#3: 提升

提升你的模型!图片来自 Unsplash

提升是袋装算法的一种小变体,使用序列处理而不是并行计算。袋装旨在减少模型的方差,而提升方法则旨在减少偏差以避免数据的欠拟合。基于这个理念,提升还使用数据的随机子集来创建一个平均性能的模型。

图片由作者提供

为此,它使用弱模型的错误分类条目以及一些其他随机数据来创建一个新模型。因此,不同的模型不是随机选择的,而是主要受到前一个模型错误分类条目的影响。这种技术的步骤如下:

  1. 训练初始(弱)模型

    你创建一个数据子集并训练一个弱学习模型,此时假设它是最终集成模型。然后,你分析给定训练数据集上的结果,并可以识别那些被错误分类的条目。

  2. 更新权重并训练新模型 你创建一个原始训练数据的新随机子集,但对那些错误分类的条目赋予更高的权重。然后使用这个数据集来训练新模型。

  3. 将新模型与集成模型进行汇总

    下一个模型应该在更难的条目上表现更好,并将与之前的模型合并(汇总)成新的最终集成模型。

从本质上讲,我们可以多次重复这个过程,并不断更新集成模型,直到我们的预测能力足够好。这里的关键思想显然是创建能够预测更困难数据条目的模型。这可以导致模型更好地拟合数据,并减少偏差。

图片作者

与 Bagging 相比,这种技术使用基于模型系数的加权投票或加权平均,这些模型与它们的预测一起考虑。因此,这种模型可以减少欠拟合,但有时也可能会过拟合。

Boosting 的代码示例

接下来,我们将查看一个类似的代码示例,但用于 boosting。显然,存在多种 boosting 算法。除了GradientDescent方法外,AdaBoost是最受欢迎的之一。

  • base_estimator: 类似于 Bagging,你需要定义希望使用的基础算法。

  • n_estimators: 估计量的数量定义了 Boosting 终止的最大迭代次数。它被称为“最大”数量,因为如果算法在更早时达到良好性能,它会自行停止。

  • learning_rate: 最后,学习率控制新模型对之前模型的贡献程度。通常,迭代次数与学习率的值之间存在权衡。换句话说,当学习率较小的时候,你应该考虑更多的估计量,以便基础模型(弱分类器)继续改进。

from sklearn.ensemble import AdaBoostClassifier

# define base estimator (requires support for sample weighting)
est = LogisticRegression() # or est = SVC() or est = DecisionTreeClassifier ….

# n_estimators defines maximum number of estimators at which boosting is terminated
# learning_rate defines the weight applied to each classifier at each boosting iteration
boost_model = AdaBoostClassifier(base_estimator= est, n_estimators = 10, learning_rate=1)

boost_model = boost_model.fit(X_train, y_train)

Prediction = boost_model.predict(X_test)

fit()predict()方法的工作方式与之前的 bagging 示例类似。正如你所见,使用现有库中的这些函数非常简单。不过,你也可以实现自己的算法来构建这两种技术。

#4: 结论:差异与相似性

图片来源:Unsplash

由于我们简要了解了 bagging 和 boosting 的工作原理,现在我想将重点放在对比这两种方法。

相似性

  • 集成方法

    从一般的角度来看,这两种技术的相似性始于它们都是集成方法,旨在通过多个学习器来改善单一模型的结果。

  • 多个样本与聚合

    为了实现这一目标,这两种方法都会生成随机样本和多个训练数据集。Bagging 和 Boosting 的相似之处在于,它们都通过对基础模型进行聚合来得出最终决策:要么通过计算平均结果,要么通过投票排名。

  • 目的

    最终,两者的目标都是为了提高数据的稳定性和预测能力。

差异

  • 数据划分 | 整体数据与偏差

    由于 bagging 在所有模型中独立地使用训练数据的随机包,而 boosting 对误分类数据给予更高的重要性。因此,这里的数据划分不同。

  • 模型 | 独立 vs. 序列

    Bagging 创建独立的模型并将其聚合在一起。然而,boosting 会用新的模型按顺序更新现有模型。因此,模型会受到之前构建的影响。

  • 目标 | 方差与偏差另一个区别是,bagging 旨在减少方差,而 boosting 尝试减少偏差。因此,bagging 有助于减少过拟合,而 boosting 可以减少欠拟合。

  • 函数 | 加权与非加权

    最终预测结果的函数在 bagging 技术中使用等权重平均或等权重投票汇聚。Boosting 使用加权多数投票或加权平均函数,对训练数据表现更好的部分赋予更多权重。

含义

研究表明,两种方法的主要思想是结合多个模型以实现比单一学习模型更好的预测。然而,并没有绝对优于对方的说法来选择 bagging 还是 boosting,因为两者都有优点和缺点。

虽然 bagging 减少了方差并减少了过拟合,但它很少能产生更好的偏差。另一方面,boosting 减少了偏差,但可能比 bagged 模型更过拟合。

回到方差-偏差权衡图,我尝试可视化每种方法看起来合适的极端情况。然而,这并不意味着它们能在没有任何缺点的情况下实现结果。目标应该始终是保持偏差和方差在合理的平衡中。

Bagging 和 boosting 都使用所有给定特征,仅随机选择条目。另一方面,随机森林是对 bagging 的扩展,也创建特征的随机子集。因此,随机森林在实践中比 bagging 使用得更频繁。

[## Jonas Dieckmann - Medium

阅读 Jonas Dieckmann 在 Medium 上的文章。分析经理 & 产品负责人 @ Philips | 热情并撰写关于……

medium.com](https://medium.com/@jonas_dieckmann?source=post_page-----23f9336d3cb0--------------------------------)

希望你觉得这有用。请告诉我你的想法!也欢迎在 LinkedIn 上与我联系,链接为 www.linkedin.com/in/jonas-dieckmann/ 或在 medium 上关注我。

另请参见我其他的一些文章:

## ICA 介绍:独立成分分析

你是否曾遇到过尝试分析复杂且高度相关的数据集的情况……

towardsdatascience.com ## 如何使用 Keras API 和 Google Colab 开始使用 TensorFlow

分步教程分析人类活动与神经网络

towardsdatascience.com

参考文献

[1]: Bühlmann, Peter. (2012). Bagging、Boosting 与集成方法。计算统计学手册. 10.1007/978–3–642–21551–3_33.

2: Machova, Kristina & Puszta, Miroslav & Barcák, Frantisek & Bednár, Peter. (2006). 使用决策树分类器对 Bagging 和 Boosting 方法的比较。计算机科学与信息系统. 3. 57–72. 10.2298/CSIS0602057M.

3: Banerjee, Prashant. Bagging 与 Boosting @kaggle: www.kaggle.com/prashant111/bagging-vs-boosting

使用 Scikit-Learn 进行集成学习:友好的介绍

原文:towardsdatascience.com/ensemble-learning-with-scikit-learn-a-friendly-introduction-5dd64650de6c

像 XGBoost 或随机森林这样的集成学习算法是 Kaggle 比赛中的顶尖模型之一。它们是如何工作的?

Riccardo AndreoniTowards Data Science Riccardo Andreoni

·发布于 Towards Data Science ·阅读时长 7 分钟·2023 年 9 月 9 日

--

来源:unsplash.com

基础学习算法如逻辑回归或线性回归通常过于简单,无法为机器学习问题提供足够的结果。虽然使用神经网络可能是一种解决方案,但它们需要大量的训练数据,这种数据很少有。集成学习技术可以在数据量有限的情况下提升简单模型的性能。

想象一下,让一个人猜测一个大罐子里有多少颗糖豆。一个人的答案不太可能是准确的估计数字。相反,如果我们问一千个人同样的问题,平均答案可能会接近实际数字。这种现象被称为集体智慧[1]。在处理复杂的估计任务时,集体往往比个体更为精准。

集成学习算法利用这一简单原理,通过聚合一组模型(如回归模型或分类器)的预测来提升性能。对于分类器的聚合,集成模型可以简单地选择低级分类器预测中最常见的类别。相反,对于回归任务,集成模型可以使用所有预测的均值或中位数。

图片来源于作者。

通过集成大量的弱学习者,即那些仅比随机猜测略好一点的分类器或回归器,我们可以取得难以想象的结果。考虑一个二分类任务。通过集成 1000 个独立的分类器,每个分类器的准确率为 51%,我们可以创建一个准确率为 75% 的集成模型 2

这就是为什么集成算法在许多机器学习竞赛中常常获胜的原因!

现有几种技术可以构建集成学习算法。主要有 bagging、boosting 和 stacking。以下部分我将简要描述每一种原理,并介绍实现这些原理的机器学习算法。

Bagging

形成集成算法的第一种技术称为 bagging,它是bootstrap aggregating的缩写。核心思想是为每个弱学习者提供一个略有不同的训练集。这是通过随机抽样原始训练集来完成的。

如果采样是有放回的,这种技术称为bagging,否则如果采样是没有放回的,这种技术则被称为pasting

bagging(套袋)和 pasting(粘贴)的关键思想是尽可能创建多样化的弱学习者,这通过使用随机生成的训练集来完成。由于采样是有放回的,bagging 引入了比 pasting 略多的多样性,因此通常更受青睐。

让我们通过一个例子来看看 bagging 的工作原理。假设原始训练集包含 10 个示例,我们想要构建一个由 3 个不同的弱学习者组成的集成。此外,我们希望在原始训练集的一个维度为 5 的子集上训练每个学习者。以下图片展示了训练集的可能划分方式:

图片由作者提供。

而 pasting 不允许在模型的训练子集中重复相同的训练示例:

图片由作者提供。

随机森林

最常见的 bagging 模型是随机森林。随机森林是决策树的集成,每棵树都在一个略有不同的训练集上训练。

对于随机森林中每棵决策树的每个节点,算法会随机选择特征来搜索最佳的分裂点。换句话说,算法不会在所有特征中寻找最佳的分裂点,而是在一部分可用特征中寻找最佳的分裂点。这就是“随机”一词的解释。

以下代码片段展示了如何在一些训练数据上创建和拟合一个随机森林分类器。

一些属性非常重要,我建议通过网格搜索方法对其进行调优。n_estimators定义了弱学习器的数量,max_features是每次分裂时考虑的特征数量,max_depth是树的最大深度。这些超参数在正则化中起着基础性作用。增加n_estimatorsmin_sample_leaf或减少max_featuresmax_depth有助于创建尽可能多样化的树,从而避免过拟合。

对于所有其他属性,请查看全面的文档

极端随机树(Extra Trees)

为了进一步增加树的随机性,我们可以使用极端随机树算法,简称为极端随机树(Extra Trees)。它的工作方式类似于随机森林,但在每个决策树的每个节点,算法在每个特征的随机阈值范围内寻找最佳可能的分裂。而随机森林则在特征的整个值范围内寻找最佳分裂。

相较于随机森林,极端随机树有一个额外的随机组件。因此,它用更高的偏差换取了较低的方差2

极端随机树的第二个优势是训练比随机森林更快,因为它不需要在选定特征的整个值域中寻找最佳分裂。相反,它仅考虑其中的一部分。

使用 Scikit-Learn 实现极端随机树回归器或分类器与实现随机森林一样简单:

参数与随机森林模型相同。

通常很难预测随机森林(Random Forest)或极端随机树(Extra Trees)对于特定任务的表现哪个更好。因此,我建议同时训练这两种模型,并在之后进行比较。

特征重要性

随机森林和极端随机树的一个优点是它们提供了每个特征减少数据集纯度的能力的度量:这称为特征重要性。换句话说,一个重要性高的特征比一个重要性低的特征能提供更好的分类或回归问题的见解。

训练模型后,我们可以通过属性**feature_importances_**访问这个度量。

图片由作者提供。

一旦模型训练完成,Scikit-Learn 会自动计算特征的重要性。输出分数被缩放到[0,1]范围内,意味着接近 1 的分数分配给最重要的特征。

在上面的例子中,‘花瓣长度’和‘花瓣宽度’的分数显著高于其他特征。虽然‘萼片长度’对我们的模型有中等重要性,但‘萼片宽度’在这个分类任务中似乎毫无用处。

提升法

继续探讨另一种值得注意的集成技术,我们深入了解 boosting 技术。这种方法在机器学习领域是 bagging 的有力对手。

Boosting,即假设提升,遵循一种有趣的理念。它不是通过随机抽样创建多样化的学习者,而是通过顺序训练预测器来提升单个弱学习者的表现。每个预测器旨在纠正其前一个预测器的错误。这就像教练在球员的弱点上加练,以使他们成为全能明星。

Boosting 由于其强调自我改进和渐进细化,通常在对精度和细节关注的场景中表现优于 bagging 技术。

我们现在将看到最著名且获奖的 boosting 集成技术:XGBoost。

XGBoost

XGBoost,全称极端梯度提升,是一种集成学习方法,采用梯度下降优化,旨在系统性地提高弱学习者的预测性能。

在 XGBoost 中,每个弱学习者根据其错误率被分配一个权重。表现不佳的学习者会被赋予更高的权重,以优先纠正这些错误。这个迭代过程持续进行,模型调整其重点以解决最具挑战的数据点。

XGBoost 是数据科学家和机器学习从业者的热门选择,因为它能够提取有价值的见解并提供卓越的预测准确性。它也因其速度和可扩展性而被广泛应用于 ML 竞赛中。

在这里我们可以看到如何将 XGBoost 应用到实际数据中:

在没有调整超参数的情况下,模型能够实现 98% 的准确率。

结论

当我们结束这次关于机器学习中集成算法的初步旅程时,重要的是要认识到我们的旅程仅仅奠定了更深层次和更专业研究的基础。虽然我们触及了一些基本概念,但这一领域远远超出了这些入门见解。

我推荐深入研究附带的资源和参考文献。这些来源提供了有关高级集成方法、算法优化和实际实施技巧的详细见解。

如果你喜欢这个故事,请考虑关注我,以便接收我即将发布的项目和文章的通知!

下面是我过去的一些项目:

## 欧洲旅行优化:遗传算法和 Google Maps API 解决旅行推销员问题

使用遗传算法和 Google Maps API 浏览欧洲 50 个最受欢迎城市的魅力,开启高效…

towardsdatascience.com ## 从头开始使用 Numpy 构建卷积神经网络

随着计算机视觉应用在我们生活中的普及,理解其工作原理...

towardsdatascience.com

参考文献

[1] 群体智慧 — 剑桥大学数学系

2 使用 Scikit-Learn、Keras 和 TensorFlow 的动手机器学习(第 2 版)— Aurélien Géron

3 Python 数据科学手册 数据处理的必要工具

4 XGBoost 文档

5 集成方法基础与算法

分类器集成:Voting Classifier

原文:towardsdatascience.com/ensemble-of-classifiers-voting-classifier-ef7f6a5b7795

结合多种不同模型以获得更好的预测

Saptashwa BhattacharyyaTowards Data Science Saptashwa Bhattacharyya

·发布于 Towards Data Science ·阅读时间 7 分钟·2023 年 8 月 17 日

--

使用 Voting Classifier 的决策边界(图片来源:作者;代码见参考文献)

在机器学习的上下文中,Ensemble 指的是一个有限数量的机器学习模型的集合(可能包括 ANN),这些模型是为同一任务训练的。通常,这些模型是独立训练的,然后将它们的预测结果结合起来。

当不同模型的预测结果不一致时,有时使用集成模型进行分类比任何单个分类器更为有用。在这里,我们希望结合不同的分类器创建一个集成模型,然后使用这个集成模型进行预测任务。本文将讨论什么内容?

  • 使用 Sklearn 的 VotingClassifier 来构建一个集成模型。

  • 什么是 VotingClassifier 中的硬投票和软投票?

  • 使用 VotingClassifier 检查单个模型的性能。

  • 最后,使用 GridSearchCV + VotingClassifier 来寻找单个模型的最佳参数。

让我们开始吧!

数据准备:

为了查看 VotingClassifier 的实际应用示例,我使用了心脏病预测数据集(开放数据库许可下提供)。这里的任务是对患者是否可能有心脏病进行二分类预测。数据集包含 10 个属性,包括年龄、性别、静息血压等,数据来源于 900 多名患者。让我们检查一些不同参数的分布情况。我们检查‘ClassLabel’的计数(1 表示心脏病,0 表示健康),即健康和患病人群随性别的变化情况。

图 1:参与者性别对 ClassLabel 分布的影响。(图片来源:作者;代码见参考文献)。

总的来说,我们看到男性的生病比例相较于女性要高。我们还可以检查个别特征,例如胆固醇和静息血压分布,见下文,我们发现生病患者的胆固醇和静息血压均较高,尤其是女性。

图 2:两种不同类别(健康和生病)下男性和女性的胆固醇和静息血压分布(图片来源:作者)。

对于数值特征,也可以使用箱线图查看分布,其中箱体表示数据中心 50% 的范围。

图 3:通过箱线图展示的一些数值参数分布(图片来源:作者)。

我们在数据集上发现了一些问题;有相当多的数据点胆固醇和静息血压为零。虽然确实有可能存在非常低的胆固醇,但很多数据点(患者)都为零,这是否有些过于偶然?不过,由于我们的主要目标是实现 VotingClassifier,因此我们将这些数据点保留原样,并继续进行下一阶段。

训练-测试与分类:

在一些初步分析后,我们准备数据集以训练和测试不同的分类器,并比较它们的性能。首先,我们将数据分成训练集和测试集,分别对数值列进行标准化(以防止数据泄漏),并初始化 3 种不同的分类器——支持向量机、逻辑回归 和 AdaBoost,后者是一种集成学习方法,特别属于提升(Boosting)方法(另一种被称为‘袋装(Bagging)’)。接下来是使用训练集训练这些分类器,并检查测试集的预测结果。我们检查了 3 种不同的性能指标——精度、召回率和 F1 分数。以下是代码块:

检查心脏病数据集上各个分类器的性能。

以下是得分:

>>> SVC, LogReg, AdaBoost Precisions: 0.9096562 0.8915564 0.8825301
>>> SVC, LogReg, AdaBoost Recalls: 0.9086956 0.8913043 0.8826086
>>> SVC, LogReg, AdaBoost F1-scores: 0.90831418 0.8910015 0.8825525

我们看到,F1 分数的平均值约为 ~89%。还可以检查 AdaBoost 分类器的特征重要性,胆固醇显然是主要的重要特征之一。下面是图示:

图 4:特征重要性图:仅考虑 AdaBoost 分类器,胆固醇的重要性最高(图片来源:作者)。

AdaBoost 分类器的混淆矩阵如下:

图 5:AdaBoost 分类器的混淆矩阵。

VotingClassifier 用于形成集成模型:

VotingClassifier 结合了不同的机器学习分类器,并使用投票规则(‘软’或‘硬’)来预测类别标签。

当模型的性能几乎相同时,它平衡了各个模型的个体弱点;在这里,我们将结合之前使用的单独分类器来构建集成模型。但什么是软投票和硬投票呢?

多数 (‘硬’) 投票: 对于特定样本,预测的类别标签是代表每个单独分类器预测的类别标签的多数(众数)。下面的表格展示了一个具有 3 个类别的分类任务示例

+-------------+---------+---------+--------+
| Classifier  | Class1  | Class2  | Class3 |
+-------------+---------+---------+--------+
| SVC         |    0.2  |    0.3  |    0.5 |
| LogReg      |    0.3  |    0.4  |    0.3 |
| AdaBoost    |    0.1  |    0.3  |    0.6 |
+-------------+---------+---------+--------+

在这里,SVC 和 AdaBoost 都预测 Class3 作为输出标签,在硬投票方案下,这将被选中。

软投票: 软投票返回预测概率之和的最大类别标签;也可以为参与创建集成的分类器分配一个权重数组。下面是一个示例代码块

svc_classifier = SVC(probability=True)
logreg_classifier = LogisticRegression()
adaboost_classifier = AdaBoostClassifier()

estimators = [('svm', svc_classifier), ('logreg', logreg_classifier), 
              ('adaboost', adaboost_classifier)]
voting_classifier_soft = VotingClassifier(estimators=estimators, 
                                          voting='soft', 
                                          weights=[1, 1, 1]) 

再次查看下面的表格,以权重 [1, 1, 1] 为例,即所有分类器的权重相等

+ — — — — — — — + — — — — — + — — — — -+ — — — — -+
| Classifier    | Class1    | Class2   | Class3   |
+ — — — — — — — + — — — — — + — — — — -+ — — — — -+
| SVC           | w1 x 0.2  | w1 x 0.3 | w1 x 0.5 |
| LogReg        | w2 x 0.3  | w2 x 0.4 | w2 x 0.3 |
| AdaBoost      | w3 x 0.1  | w3 x 0.3 | w1 x 0.6 |
| Weighted Avg. | 0.20      | 0.33     | 0.47     |
+ — — — — — — — + — — — — — + — — — — -+ — — — — -+

在‘软’投票方案下,这里将选择 Class3。有关 VotingClassifier 的更多信息,您还可以查看 官方 Sklearn 指南。现在,让我们构建一个 VotingClassifier,并使用这个集成模型在测试集上进行训练和预测,使用‘软’和‘硬’方案。

使用 VotingClassifier 确实可以看到相较于单个估算器,性能指标有所改善。同样,我们也可以绘制混淆矩阵以进一步验证结果。

图 6:使用 VotingClassifier 和‘硬’投票方案获得的混淆矩阵。(图像由作者提供)。

VotingClassifier 和 GridSearchCV:

也可以使用 GridSearchCV 调整 VotingClassifier 中各个估算器的参数(‘超参数’)。GridSearch 会穷尽所有参数组合,并使用交叉验证来找到最佳超参数。我们之前已经通过 这里 介绍了使用支持向量机、主成分分析和 GridSearchCV 构建分析管道的示例。我们展示了在 VotingClassifier 中获取最佳参数是相当简单的,下面是一个示例代码块:

对于参数空间(在第 18 行),我们仔细研究了 SVM 的 ‘C’ 和 ‘gamma’ 参数(使用径向基函数核),这些参数我们之前已经详细讨论过 之前。对于逻辑回归,我们检查了正则化强度参数的多个逆值;最后,对于使用决策树作为基估算器的 AdaBoost 分类器,我们检查了一些可能的估算器数量值,即 Boosting 终止的值。

结论: 在这篇文章中,通过 VotingClassifier,我们讨论了机器学习中的一个重要概念——集成技术。我们使用个体估计器构建一个集成,这些估计器的预测结果通过‘Hard’或‘Soft’投票方案结合,用于示例分类任务。通常认为,结合几个不同的估计器可能会胜过最佳估计器的表现,但集成有助于减少选择表现不佳的分类器的风险[1]。

根据手头的任务,你可以考虑使用 VotingClassifier 集成技术,而不是从多个分类器中选择最佳的。

保持坚强!干杯!!

参考文献:

[1] “决策中的集成系统”;R. Polikar,IEEExplore,DOI: 10.1109/MCAS.2006.1688199

2 Sklearn VotingClassifier: 用户指南

3 “基于集成的分类器”;L. Rokach,人工智能评论 33,1–39 (2010)。 Springer

4 代码和笔记本:GitHub

5 心力衰竭预测数据;在开放数据库许可 (ODbl) 下提供;链接

通过单元测试确保模型的可靠性

原文:towardsdatascience.com/ensure-model-reliability-with-unit-testing-589292e6f0e2

在开发过程中尽早发现错误

Renato BoemerTowards Data Science Renato Boemer

·发表于 Towards Data Science ·4 min read·2023 年 1 月 25 日

--

照片由 Diana PolekhinaUnsplash 上发布

你是否曾经去过数据科学家或机器学习工程师的面试,并被问到如何设置单元测试?如果你不知道我在说什么,或者只是想复习一下单元测试的基础知识,那么这篇文章适合你。

单元测试是任何软件开发过程中不可或缺的一部分,对于机器学习模型尤其重要。单元测试是一种软件测试方法,其中软件应用程序的单个单元或组件在隔离的环境中进行测试。在机器学习的背景下,单元测试用于确保模型的各个组件,如特征预处理或模型评估指标,按照预期工作。

作为一个学习数据科学和机器学习的人,你可能会想知道为什么单元测试对机器学习模型如此重要。答案很简单:

单元测试有助于在开发过程中尽早发现错误。

从长远来看,这可以节省大量时间和精力,因为早期发现的错误通常比后期发现的错误更容易和更快速地修复。此外,单元测试可以帮助确保机器学习模型的健壮性和可靠性,这对于模型预测可能会带来实际后果的应用程序至关重要。

示例与代码

我将使用 Python 演示如何实现单元测试,为了这个示例,我使用了一个简单的线性回归模型。通常,线性回归是我们在训练营中学习的最简单的模型。

首先,我们需要安装必要的包。我们将使用 Python 内置的unittest 包。所以,在脚本的开头,像这样开始:

import unittest

接下来,我们将定义一个用于单元测试的类。该类应该继承自unittest.TestCase,每个测试方法的命名应以test_开头。如果你不熟悉 Python 类,请在评论中告诉我!

class TestLinearRegression(unittest.TestCase):
    def test_fit(self):
        # test code for fitting the model
        pass

    def test_predict(self):
        # test code for making predictions with the model
        pass

    def test_score(self):
        # test code for evaluating the model's performance
        pass

在每个测试方法中,我们可以使用各种断言方法来检查模型的输出是否符合预期。例如,在test_fit方法中,我们可以检查模型的系数是否在真实系数的某个容差范围内。在test_predict方法中,我们可以检查模型的预测值是否在真实值的某个容差范围内。而在test_score方法中,我们可以检查模型的性能指标是否高于某个阈值。以下是上述代码的演变:

class TestLinearRegression(unittest.TestCase):
    def test_fit(self):
        # test code for fitting the model
        self.assertEqual(model.coef_, true_coef, delta=1e-3)

    def test_predict(self):
        # test code for making predictions with the model
        self.assertEqual(predictions, true_values, delta=1e-3)

    def test_score(self):
        # test code for evaluating the model's performance
        self.assertGreater(score, 0.9)

请记住,在上面的示例中,我使用了unittest.TestCase类中的assertEqualassertGreater方法。这只是unittest包中众多断言方法中的两个示例。还有用于检查是否引发异常、值是否为真或假等断言方法。

获取结果

要运行单元测试,我们可以使用unittest.main()方法。这将自动发现并运行测试类中定义的所有测试方法。我通常通过在命令行(也称为终端或 Shell)中输入带有-m unittest标志的 Python 文件来运行测试。例如:

python test_linear_regression.py -m unittest

理想情况下,单元测试应该在实际代码之前编写。这被称为测试驱动开发(TDD),它是软件开发中广泛接受的实践。通过先编写测试,你可以确保你编写的代码将满足需求并通过测试。

最后,你的单元测试将返回结果,例如运行的测试数量、通过的测试数量以及发生的任何错误或失败。然后,你应该分析这些结果,并确定测试用例是否如预期那样通过,或是否需要解决任何问题。

进一步探索

充分利用单元测试的最佳方法之一是确保测试用例覆盖尽可能多的场景。这包括正面和负面的场景。例如,一个测试用例应该检查模型的预测值是否在真实值的某个容差范围内,但它也应该检查模型的预测值是否不在真实值的某个容差范围内。

你还可以使用像 Jenkins 或 CircleCI 这样的持续集成服务来自动化运行测试的过程,并在网络界面上获取结果。

结论

单元测试是机器学习开发过程中至关重要的一部分。它有助于及早发现错误,确保模型的稳健性和可靠性,并在长远中节省时间和精力。通过使用 Python 和 unittest 包,你可以轻松为机器学习模型实现单元测试,并充分利用它。记得实践测试驱动开发,并在编写测试用例时覆盖尽可能多的场景。

想要全面访问 Medium 文章并支持我的工作吗?请使用下面的链接订阅:

[## 使用我的推荐链接加入 Medium - Renato Boemer

阅读 Renato Boemer 的每一个故事(以及 Medium 上其他成千上万的作家的故事)。你的会员费直接支持…

boemer.medium.com](https://boemer.medium.com/membership?source=post_page-----589292e6f0e2--------------------------------)

你可能想要阅读:

## 如何成功设置 A/B 测试

帮助非数据专业人士在没有数据科学家的情况下自信地进行测试

towardsdatascience.com

确保在 Scikit-learn 管道中正确使用变换器

原文:towardsdatascience.com/ensuring-correct-use-of-transformers-in-scikit-learn-pipelines-393566db7bfa

机器学习项目中的有效数据处理

彭乾数据科学前沿 彭乾

·发表在 数据科学前沿 ·11 分钟阅读·2023 年 12 月 20 日

--

确保在 Scikit-learn 管道中正确使用变换器。图片由作者提供

本文将解释如何在 Scikit-Learn (sklearn) 项目中正确使用 PipelineTransformers,以加速和重用我们的模型训练过程。

这部分内容补充和澄清了有关 Pipeline 示例和一些常见误解的官方文档。

我希望阅读完这篇文章后,你能够更好地利用 Pipeline 这个优秀的设计来完成你的机器学习任务。

介绍

世界各地的中餐馆都有一道著名的菜肴叫做“左宗棠鸡”,我想知道你是否尝试过。

左宗棠鸡。标准化烹饪过程的典范。照片来源:作者创作,Canva

“左宗棠鸡”的一个特点是每块鸡肉都被厨师处理成相同的大小。这确保了:

  1. 所有鸡块都腌制相同的时间。

  2. 在烹饪过程中,每块鸡肉都达到相同的熟度。

  3. 使用筷子时,均匀的大小使得捡起食材更加容易。

这种预处理包括清洗、切割和腌制食材。如果鸡块切割得比平时大,即使炒的时间相同,风味也会发生显著变化。

因此,在准备开设餐厅时,我们必须考虑标准化这些过程和配方,以确保每一道“左宗棠鸡”都有一致的口味和质地。这就是餐厅成功的关键。

回到机器学习的世界,Scikit-Learn 还提供了这样的标准化流程,称为 Pipeline。它们将数据预处理和模型训练过程固化为标准化的工作流程,使机器学习项目更易于维护和重用。

在本文中,我们将探讨如何在 Scikit-Learn 的 Pipeline 中正确使用变换器,确保我们的数据准备得像精致餐点的配料一样完美。

为什么使用变换器

什么是变换器

在 Scikit-Learn 中,变换器主要分为两类:数据缩放和特征降维。

以一组房屋数据为例,其中包括位置、面积和卧室数量等特征。

如果不将这些特征标准化到相同的尺度,模型可能会因区域(通常是较大的数值)中的微小波动而忽略位置的显著影响(通常是分类数据)。

这就像用过多的胡椒粉掩盖了草药的细腻味道。

正确使用变换器

通常,数据缩放是通过标准化来完成的,公式为:

训练数据标准化的公式。图片作者提供

其中 train_meantrain_std 是从训练数据中提取的变量。

在 Scikit-Learn 中,训练数据和测试数据都是使用 [train_test_split](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html?ref=dataleadsfuture.com#sklearn.model_selection.train_test_split) 方法从原始数据集中获得的。

在缩放测试数据时,使用相同的 train_meantrain_std

在缩放测试数据时使用相同的 train_mean 和 train_std 变量。图片作者提供

这里出现了一个问题:为什么使用训练数据来生成这些变量?

让我们看看一个简单的数据集,其中训练数据为:

一个简单的训练数据集。图片作者提供

标准化后,训练数据变为:

缩放后的简单数据集。图片作者提供

显然,在缩放后,大于 0 的特征标记为 1,这意味着在缩放之前,大于 10 的特征标记为 1。

现在让我们看看测试数据:

尚未分类的测试数据。图片作者提供

如果我们使用从测试数据分布中生成的 test_meantest_std,而不考虑训练数据,则结果变为:

使用测试数据生成变量时的错误演示。图片作者提供

显然,这个预测结果是没有意义的。但是假设我们使用 train_meantrain_std 来处理数据,并结合模型预测;我们来看看会发生什么:

使用训练数据的变量,我们获得了正确的结果。图片由作者提供

如我们所见,只有通过对训练数据生成的变量进行数据预处理,才能确保模型的预测符合预期。

在 Scikit-Learn 中使用变换器

在 Scikit-Learn 中使用变换器非常简单。

我们可以使用make_classification生成一组模拟数据,然后用train_test_split将其拆分为训练集和测试集。

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

X, y = make_classification(n_samples=100, n_features=2, 
                           n_classes=2, n_redundant=0, 
                           n_informative=2, n_clusters_per_class=1, 
                           random_state=42)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

让我们看看数据的分布:

import matplotlib.pyplot as plt

plt.scatter(X_train[:, 0], X_train[:, 1], color='red', marker='o')
plt.scatter(X_test[:, 0], X_test[:, 1], color='green', marker='s')
plt.xlabel('feature_idx_0')
plt.ylabel('feature_idx_1')
plt.tight_layout()
plt.show()

缩放前的数据分布。图片由作者提供

在这里,我们使用StandardScaler来缩放特征。

首先,初始化StandardScaler,然后用训练数据fit它:

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
scaler.fit(X_train)

接下来,我们可以使用拟合的变换器transform训练数据的特征:

X_train_std = scaler.transform(X_train)

当然,我们也可以使用fit_transform一次性拟合并转换训练数据:

X_train_std = scaler.fit_transform(X_train)

然后我们只需转换测试数据,无需再次拟合:

X_test_std = scaler.transform(X_test)

转换后,数据的分布保持不变,唯一的变化是规模:

缩放后的数据分布。图片由作者提供

除了使用像[StandardScaler](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html?ref=dataleadsfuture.com#sklearn.preprocessing.StandardScaler)[MinMaxScaler](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MinMaxScaler.html?ref=dataleadsfuture.com#sklearn.preprocessing.MinMaxScaler)这样的工具进行数据缩放外,我们还可以使用PCASelectKBest等进行维度减少。为了简洁起见,我不在这里深入探讨这些内容,但欢迎查阅官方文档获取更多信息。

在管道中使用变换器

为什么使用管道

如前所述,在机器学习任务中,我们通常需要使用各种变换器进行数据缩放和特征维度减少,然后再训练模型。

这带来了几个挑战:

  • 代码复杂性:每次使用变换器时,我们必须经历初始化、fit_transformtransform步骤。转换过程中漏掉一个步骤可能会破坏整个训练过程。

  • 数据泄露:如我们所讨论的,对于每个变换器,我们用训练数据拟合,然后转换训练数据和测试数据。我们必须避免测试数据的分布泄漏到训练数据中。

  • 代码复用性:一个机器学习模型不仅包括用于预测的训练好的估计器,还包括数据预处理步骤。因此,一个包括变换器和估计器的机器学习任务应该是原子化和不可分割的。

  • 超参数调优:在设置好机器学习步骤后,我们需要调整超参数,以找到 Transformer 参数值的最佳组合。

Scikit-Learn 引入了 Pipeline 模块来解决这些问题。

什么是 Pipeline

Pipeline 是 Scikit-Learn 中实现责任链设计模式的一个模块。

创建 Pipeline 时,我们使用 steps 参数将多个 Transformers 链接在一起进行初始化:

from sklearn.pipeline import Pipeline
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier

pipeline = Pipeline(steps=[('scaler', StandardScaler()),
                           ('pca', PCA(n_components=2, random_state=42)),
                           ('estimator', RandomForestClassifier(n_estimators=3, max_depth=5))])

官方文档指出,最后一个 Transformer 必须是一个 Estimator。

如果你不需要指定每个 Transformer 的名称,你可以使用 make_pipeline 简化 Pipeline 的创建:

from sklearn.pipeline import make_pipeline

pipeline_2 = make_pipeline(StandardScaler(),
                           PCA(n_components=2, random_state=42),
                           RandomForestClassifier(n_estimators=3, max_depth=5))

从源代码理解 Pipeline 的机制

我们已经提到,在使用每个 Transformer 时,不要让测试数据变量泄露到训练数据中的重要性。

当每个数据预处理步骤是独立时,这个原则相对容易确保。

但如果我们使用 Pipeline 整合这些步骤会怎么样?

如果我们查看官方文档,会发现它只是对整个数据集使用 fit 方法,而没有解释如何分别处理训练数据和测试数据。

牢记这个问题,我深入研究了 Pipeline 的源代码以寻找答案。

阅读源代码表明,虽然 Pipeline 实现了 fitfit_transformpredict 方法,但它们的工作方式与普通的 Transformers 不同。

以以下 Pipeline 创建过程为例:

from sklearn.pipeline import Pipeline
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier

pipeline = Pipeline(steps=[('scaler', StandardScaler()),
                           ('pca', PCA(n_components=2, random_state=42)),
                           ('estimator', RandomForestClassifier(n_estimators=3, max_depth=5))])

内部实现可以通过以下图示表示:

调用 fitpredict 方法时的内部实现。图片由作者提供

正如你所见,当我们调用 fit 方法时,Pipeline 首先将 Transformers 与 Estimator 分开。

对于每个 Transformer,Pipeline 会检查是否有 fit_transform 方法;如果有,它会调用该方法;否则,它会调用 fit

对于 Estimator,它直接调用 fit

对于 predict 方法,Pipeline 将 Transformers 与 Estimator 分开。

Pipeline 按顺序调用每个 Transformer 的 transform 方法,然后是 Estimator 的 predict 方法。

因此,在使用 Pipeline 时,我们仍然需要拆分训练数据和测试数据。然后我们只需对训练数据调用 fit,对测试数据调用 predict

在将 Pipeline 与 GridSearchCV 进行超参数调优时,有一种特殊情况:你不需要手动拆分训练数据和测试数据。我将在最佳实践部分详细解释这一点。

实际应用中使用 Transformers 和 Pipeline 的最佳实践

既然我们已经讨论了 Transformers 和 Pipeline 的工作原理,是时候履行标题中的承诺,讨论在实际项目中将 Transformers 与 Pipeline 结合的最佳实践。

将 Pipeline 与 GridSearchCV 结合用于超参数调优

在机器学习项目中,选择合适的数据集处理和算法是一方面。在调试初步步骤之后,是时候进行参数优化了。

使用 GridSearchCVRandomizedSearchCV,你可以尝试不同的参数以找到最佳适配:

import time

from sklearn.model_selection import GridSearchCV

pipeline = Pipeline(steps=[('scaler', StandardScaler()),
                           ('pca', PCA()),
                           ('estimator', RandomForestClassifier())])
param_grid = {'pca__n_components': [2, 'mle'],
              'estimator__n_estimators': [3, 5, 7],
              'estimator__max_depth': [3, 5]}

start = time.perf_counter()
clf = GridSearchCV(pipeline, param_grid=param_grid, cv=5, n_jobs=4)
clf.fit(X, y)

# It takes 2.39 seconds to finish the search on my laptop.
print(f"It takes {time.perf_counter() - start} seconds to finish the search.")

但在机器学习中,超参数调优不仅限于 Estimator 参数;它还涉及 Transformer 参数的组合。

将所有步骤与 Pipeline 集成,允许对每个元素进行不同参数组合的超参数调优。

请注意,在超参数调优期间,我们不再需要手动分割训练数据和测试数据。GridSearchCV 会使用 [StratifiedKFold](https://scikit-learn.org/stable/modules/cross_validation.html?ref=dataleadsfuture.com#stratified-k-fold) 将数据分为训练集和验证集,该方法实现了 k-fold 交叉验证机制。

StratifiedKFold 迭代过程将训练数据和测试数据分开。图片来源:作者

我们还可以设置交叉验证的折数,并选择使用多少个工作线程。调优过程在下图中展示:

GridSearchCV 超参数调优的内部实现。图片来源:作者

由于空间限制,我不会在此详细介绍 GridSearchCVRandomizedSearchCV。如果你感兴趣,我可以在下次撰写另一篇文章来解释它们。

使用 memory 参数缓存 Transformer 输出

当然,使用 GridSearchCV 进行超参数调优可能会很慢,但这没关系,Pipeline 提供了缓存机制,通过缓存中间步骤的结果来加速调优效率。

在初始化 Pipeline 时,你可以传入 memory 参数,这将缓存每个转换器第一次调用 fittransform 后的结果。

如果随后的 fittransform 调用使用相同的参数,这在超参数调优期间很可能发生,这些步骤将直接从缓存中读取结果,而不是重新计算,从而显著提高了重复运行相同 Transformer 时的效率。

memory 参数可以接受以下值:

  • 默认值为 None:不使用缓存。

  • 字符串:提供存储缓存结果的路径。

  • joblib.Memory 对象:允许更细粒度的控制,例如配置缓存的存储后端。

接下来,让我们使用之前的 GridSearchCV 示例,这次将 memory 添加到 Pipeline 中,看看能提高多少速度:

pipeline_m = Pipeline(steps=[('scaler', StandardScaler()),
                           ('pca', PCA()),
                           ('estimator', RandomForestClassifier())],
                      memory='./cache')
start = time.perf_counter()
clf_m = GridSearchCV(pipeline_m, param_grid=param_grid, cv=5, n_jobs=4)
clf_m.fit(X, y)

# It takes 0.22 seconds to finish the search with memory parameter.
print(f"It takes {time.perf_counter() - start} seconds to finish the search with memory.")

如图所示,使用缓存后,调优过程仅需 0.2 秒,相较于之前的 2.4 秒大幅提升了速度。

如何调试 Scikit-Learn Pipeline

在将 Transformers 集成到 Pipeline 中后,整个预处理和转换过程变成了一个黑箱。很难理解当前的处理步骤。

幸运的是,我们可以通过在 Pipeline 中添加日志记录来解决这个问题。

我们需要创建自定义转换器,以在数据转换的每一步添加日志记录。

这里是一个使用 Python 标准日志库添加日志记录的示例:

首先,你需要配置一个记录器:

import logging

from sklearn.base import BaseEstimator, TransformerMixin

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger()

接下来,你可以创建一个自定义 Transformer,并在其方法中添加日志记录:

class LoggingTransformer(BaseEstimator, TransformerMixin):
    def __init__(self, transformer):
        self.transformer = transformer
        self.real_name = self.transformer.__class__.__name__

    def fit(self, X, y=None):
        logging.info(f"Begin fit: {self.real_name}")
        self.transformer.fit(X, y)
        logging.info(f"End fit: {self.real_name}")
        return self

    def fit_transform(self, X, y=None):
        logging.info(f"Begin fit_transform: {self.real_name}")
        X_fit_transformed = self.transformer.fit_transform(X, y)
        logging.info(f"End fit_transform: {self.real_name}")
        return X_fit_transformed

    def transform(self, X):
        logging.info(f"Begin transform: {self.real_name}")
        X_transformed = self.transformer.transform(X)
        logging.info(f"End transform: {self.real_name}")
        return X_transformed

然后,你可以在创建 Pipeline 时使用这个 LoggingTransformer

pipeline_logging = Pipeline(steps=[('scaler', LoggingTransformer(StandardScaler())),
                             ('pca', LoggingTransformer(PCA(n_components=2))),
                             ('estimator', RandomForestClassifier(n_estimators=5, max_depth=3))])
pipeline_logging.fit(X_train, y_train)

添加 LoggingTransformer 后的效果。图像由作者提供

当你使用 pipeline.fit 时,它会依次调用每个步骤的 fittransform 方法,并记录相应的信息。

在 Scikit-Learn Pipeline 中使用 passthrough

在 Pipeline 中,可以将某个步骤设置为 'passthrough',这意味着对于这个特定的步骤,输入数据将不加改变地传递到下一个步骤。

当你希望有选择性地启用/禁用复杂管道中的某些步骤时,这一点很有用。

参考上面的代码示例,我们知道在使用 DecisionTreeRandomForest 时,标准化数据是多余的,因此我们可以使用 passthrough 跳过这一步。

下面是一个示例:

param_grid = {'scaler': ['passthrough'],
              'pca__n_components': [2, 'mle'],
              'estimator__n_estimators': [3, 5, 7],
              'estimator__max_depth': [3, 5]}
clf = GridSearchCV(pipeline, param_grid=param_grid, cv=5, n_jobs=4)
clf.fit(X, y)

重用 Pipeline

经过一番波折,我们终于得到了一个表现良好的机器学习模型。

现在,你可能考虑如何重用这个模型,与同事共享,或将其部署到生产环境中。

然而,模型训练的结果不仅包括模型本身,还包括各种数据处理步骤,这些步骤都需要保存。

使用 joblib 和 Pipeline,我们可以保存整个训练过程以备后用。以下代码提供了一个简单的示例:

from joblib import dump, load

# save pipeline
dump(pipeline, 'model_pipeline.joblib')

# load pipeline
loaded_pipeline = load('model_pipeline.joblib')

# predict with loaded pipeline
loaded_predictions = loaded_pipeline.predict(X_test)

结论

官方 Scikit-Learn 文档 是我见过的最好的文档之一。通过学习其内容,你可以掌握机器学习应用的基础知识。

然而,在实际项目中使用 Scikit-Learn 时,我们经常遇到官方文档可能未涵盖的各种细节。

如何正确结合 Transformers 和 Pipeline 就是一个这样的案例。

在这篇文章中,我介绍了使用 Transformers 的原因和一些典型应用场景。

然后,我从源代码层面解释了 Pipeline 的工作原理,并完成了在训练和测试数据集上应用的合理用例。

最后,对于实际机器学习项目的每个阶段,我介绍了结合 Transformers 和 Pipeline 的最佳实践,基于我的工作经验。

希望这篇文章能对你有所帮助。如果你有任何问题,请给我留言,我会尽力回答。

感谢阅读我的故事。

你可以订阅以获取我最新的数据科学故事。

如果你有任何问题,可以在LinkedInTwitter(X)上找到我。

这篇文章最初发表在数据引领未来上。

确保可信的 ML 系统,通过数据验证和实时监控

原文:towardsdatascience.com/ensuring-trustworthy-ml-systems-with-data-validation-and-real-time-monitoring-89ab079f4360

完整的 7 步骤 MLOps 框架

课程 5:使用 GE 进行质量和完整性的数据验证。模型性能持续监控。

Paul IusztinTowards Data Science Paul Iusztin

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

--

图片由Hassan Pasha拍摄,Unsplash

本教程代表7 节课中的第五部分,将逐步指导你如何设计、实施和部署 ML 系统,使用MLOps 良好实践。在课程中,你将构建一个生产就绪的模型,用于预测未来 24 小时内来自丹麦的多种消费类型的能源消耗水平。

到课程结束时,你将理解如何使用批处理服务架构来设计、编码和部署 ML 系统的所有基本原理。

本课程针对中级/高级机器学习工程师,希望通过构建自己的端到端项目提升技能。

如今,证书随处可见。构建先进的端到端项目,并在之后展示出来,是获得专业工程师认可的最佳方式。

目录:

  • 课程介绍

  • 课程内容

  • 数据源

  • 课程 5:使用 GE 进行质量和完整性的数据验证。模型性能持续监控。

  • 课程 5:代码

  • 结论

  • 参考文献

课程介绍

在这 7 节课的课程结束时,你将知道如何:

  • 设计批处理服务架构

  • 使用 Hopsworks 作为特征存储

  • 设计一个从 API 读取数据的特征工程管道

  • 构建一个具有超参数调优的训练管道

  • 使用 W&B 作为 ML 平台来跟踪你的实验、模型和元数据

  • 实现批处理预测管道

  • 使用 Poetry 构建自己的 Python 包

  • 部署你自己的私有 PyPi 服务器

  • 使用 Airflow 协调一切

  • 使用预测来开发基于 FastAPI 和 Streamlit 的 Web 应用

  • 使用 Docker 将代码容器化

  • 使用 Great Expectations 确保数据验证和完整性

  • 监控预测的性能随时间变化

  • 将一切部署到 GCP

  • 使用 GitHub Actions 构建 CI/CD 流水线

如果这听起来很多,不用担心。在完成这门课程后,你将理解我之前所说的一切。最重要的是,你将知道为什么使用这些工具以及它们如何作为一个系统协同工作。

如果你想充分利用这门课程, 我建议你访问包含所有课程代码的 GitHub 仓库 。这门课程旨在快速阅读并复制文章中的代码。

到课程结束时,你将知道如何实现下面的图示。如果有些内容你不理解,别担心。我会详细解释一切。

课程中将构建的架构图 [图由作者提供]。

第 5 课结束时,你将知道如何使用 Great Expectations 验证数据的完整性和质量。同时,你还会了解如何在 ML 系统上实现监控组件。

课程内容:

  1. 批量服务。特征存储。特征工程流水线。

  2. 训练流水线。ML 平台。超参数调整。

  3. 批量预测流水线。使用 Poetry 打包 Python 模块。

  4. 私有 PyPi 服务器。使用 Airflow 协调一切。

  5. 使用 GE 进行数据质量和完整性验证。模型性能持续监控。

  6. 使用 FastAPI 和 Streamlit 消费和可视化模型预测。将一切容器化。

  7. 将所有 ML 组件部署到 GCP。使用 GitHub Actions 构建 CI/CD 流水线。

  8. [额外] 探秘‘不完美’ML 项目——经验与见解

要了解更多背景信息,请查看第 3 课,这将教你如何使用批处理架构和特征存储构建推理管道。

此外,第 4 课将展示如何使用 Airflow 来协调所有的管道。

这节课将利用上述观点,并假设你已经理解了这些观点。

数据源

我们使用了一个免费且开放的 API,提供丹麦所有能源消费者类型的每小时能源消耗值 [1]。

他们提供了一个直观的界面,你可以轻松查询和可视化数据。你可以在这里访问数据 [1]。

数据有 4 个主要属性:

  • 小时 UTC: 数据点被观测到的 UTC 日期时间。

  • 价格区域: 丹麦被分为两个价格区域:DK1 和 DK2——由大贝尔特分隔。DK1 位于大贝尔特以西,DK2 位于大贝尔特以东。

  • 消费者类型: 消费者类型是工业代码 DE35,由丹麦能源公司拥有和维护。

  • 总消耗: 总电力消耗(单位:千瓦时)

注意: 观测数据有 15 天的延迟!但对于我们的演示用例,这不是问题,因为我们可以模拟实时发生的相同步骤。

我们的网页应用截图,展示了如何预测区域 = 1 和消费者类型 = 212 的能源消耗 [作者图片]。

数据点具有每小时的分辨率。例如:“2023–04–15 21:00Z”,“2023–04–15 20:00Z”,“2023–04–15 19:00Z”,等等。

我们将数据建模为多个时间序列。每个唯一的价格区域消费者类型元组表示其唯一的时间序列。

因此,我们将构建一个模型,独立预测每个时间序列未来 24 小时的能源消耗。

查看下面的视频以更好地理解数据的样子 👇

课程和数据源概述 [作者视频]。

第 5 课:使用 GE 进行数据质量和完整性的验证。模型性能持续监控。

第 5 课的目标

目前,机器学习流程已经实施并协调好了。这意味着我们完成了吗?

还不完全……

最后一步,将使你从一个优秀的工程师成长为一个杰出的工程师,就是添加一个组件,让你能够快速诊断生产系统中发生的情况。

在第 5 课中,你将主要学习两个不同的主题,这些主题服务于一个共同目标:确保你的生产系统正常工作。

1. 数据验证: 在将数据导入特征存储之前,检查 FE 管道生成的数据是否正常。

2. 模型监控: 持续计算反映你生产模型性能的各种指标。

最终架构图,其中第 5 课的组件用蓝色突出显示 [作者提供的图片]。

我将在理论概念与工具部分详细介绍。但简要概述一下,为了持续监控你的模型性能,你将使用你之前的预测与新收集的实际情况来计算所需的指标,在你的情况下是 MAPE。

例如,你预测 6 月 1 日的 24 小时能耗值。最初,你没有数据来计算指标。但是,在 12 小时后,你可以收集实际能耗。因此,你可以在获取实际情况后计算最后 12 小时的所需指标。

1 小时后,你可以计算另一个数据点的指标,依此类推……

这是我们将在本教程中采用的策略。

理论概念与工具

数据验证: 数据验证是确保数据质量和完整性的过程。我是什么意思?

当你自动从不同来源(在我们的案例中是 API)收集数据时,你需要一种方法来持续验证你刚刚提取的数据是否遵循系统期望的一组规则。

例如,你期望能耗值是:

  • 类型为浮点数,

  • 非空,

  • ≥0。

当你开发 ML 管道时,API 仅返回符合这些条款的值,数据人员称之为“数据契约”。

但是,当你让你的系统在生产环境中运行 1 个月、1 年、2 年等时,你将永远不知道哪些数据源可能发生变化,而这些变化你没有控制权。

因此,你需要一种方法来不断检查这些特征,以便在将数据导入特征存储之前。

注意: 要了解如何将这一概念扩展到非结构化数据,如图像,你可以查看我的 掌握数据完整性以清理你的计算机视觉数据集 文章。

Great Expectations(简称 GE): GE 是一个流行的工具,可以轻松进行数据验证并报告结果。Hopsworks 支持 GE。你可以将 GE 验证套件添加到 Hopsworks,并选择当新数据插入时以及验证步骤失败时的处理方式 — 了解更多关于 GE + Hopsworks 2

GE 数据验证运行的截图 [作者提供的图片]。

实际情况类型: 当你的模型在生产环境中运行时,你可以在三种不同的场景中访问你的实际情况:

  1. 实时: 理想的情况是你可以轻松访问目标。例如,当你推荐一个广告,消费者要么点击它,要么不点击。

  2. 延迟: 最终,你会访问实际情况。但不幸的是,那时反应时间可能已太晚,无法及时做出适当的反应。

  3. 无: 你无法自动收集任何 GT。通常在这种情况下,如果需要实际数据,你必须聘请人工注释员。

真实数据/目标/实际数据类型 [作者提供的图像]。

在我们的案例中,我们介于 #1 和 #2 之间。GT 不完全是实时的,但延迟只有 1 小时。

延迟 1 小时是否合适很大程度上取决于业务背景,但假设在你的情况下,这是可以的。

由于我们认为 1 小时的延迟对于我们的用例是可以的,我们很幸运:我们实时(近实时)访问 GT。

这意味着我们可以使用 MAPE 等指标来实时(近实时)监控模型的性能。

在情景 2 或 3 中,我们需要使用数据和概念漂移作为代理指标来计算时间上的性能信号。

显示观察结果和预测重叠的截图。如你所见,GT 在最新的 24 小时预测中不可用 [作者提供的图像]。

ML 监控: ML 监控是确保你的生产系统随时间正常运行的过程。同时,它为你提供了一种机制,可以主动调整系统,例如及时重新训练模型或使其适应环境中的新变化。

在我们的案例中,我们将持续计算 MAPE 指标。因此,如果误差突然激增,你可以创建警报来通知你,或自动触发超参数优化步骤以将模型配置适应新环境。

显示所有时间序列计算出的平均 MAPE 指标的截图 [作者提供的图像]。

第 5 课:代码

你可以在这里访问 GitHub 仓库。

注意: 所有的安装说明都在仓库的 README 文件中。在这里,你将直接跳到代码部分。

第 5 课中的代码位于以下位置:

使用 Docker,你可以快速在 Airflow 中托管所有内容,这样你就不必浪费大量时间进行设置。

直接将凭证存储在你的 git 仓库中是一个巨大的安全风险。这就是为什么你将使用 .env 文件来注入敏感信息。

.env.default 是你必须配置的所有变量的示例。它也有助于存储不敏感的属性的默认值(例如项目名称)。

.env.default 文件的截图 [作者提供的图像]。

准备凭证

我不想过多重复。你已经在之前课程的 “准备凭证” 中有逐步说明。

幸运的是,在这篇文章中,你无需准备之前课程中的额外凭证。

检查“准备凭证” Lesson 4 是一个很好的起点,展示了如何准备所有凭证和工具。另请检查 GitHub 仓库 以获取更多信息。

这将展示如何在 .env 文件中完成所有凭证的配置。

现在,让我们开始编码 🔥

数据验证

使用 GE + Hopsworks 进行数据验证的概述 [作者视频]。

GE 套件定义在 feature-pipeline/feature_pipeline/etc/validation.py 文件中。

在下面的代码中,你定义了一个 GE ExpectationSuite,名为 energy_consumption_suite

使用 ExpectationConfiguration 类,你可以添加各种验证测试。在以下示例中,添加了 2 个测试:

  1. 检查表的列是否与给定的有序列表匹配。

  2. 检查列的长度是否为 4。

简单而强大 🔥

现在,让我们来看看完整的验证套件 👇

使用 GE,你将检查 Pandas DataFrame 的以下特征:

  1. 列的名称应为:["datetime_utc,"… "energy_consumption"]。

  2. DF 应有 4 列。

  3. 列 "datetime_utc" 应该所有值都不为空。

  4. 列 "area" 期望仅有值为 0、1 或 2。

  5. 列 "area" 应为 int8 类型。

  6. 列 "consumer_type" 期望仅有值为 111、…

  7. 列 "consumer_type" 应为 int32 类型。

  8. 列 "energy_consumption" 应该有值 ≥ 0。

  9. 列 "energy_consumption" 应为 float64 类型。

  10. 列 "energy_consumption" 应该所有值都不为空。

如你所见,质量检查主要归结为:

  1. 检查表的模式。

  2. 检查列的类型。

  3. 检查列的值(对离散特征和连续特征使用不同的逻辑)。

  4. 检查空值。

你将把这个验证套件附加到 FE 流水线的 to_feature_store() 加载函数中,文件为 feature-pipeline/feature_pipeline/etl/load.py

现在,Hopsworks 每次将新 DataFrame 插入特征组时都会运行给定的 GE 验证套件。

如果验证套件失败,你可以选择拒绝新数据或触发警报以采取手动措施。

ML 监控

课程内构建的 ML 监控仪表板概述 [作者视频]。

当涉及到 ML 监控时,最困难的部分不是代码本身,而是如何选择监控你的 ML 模型。

请注意,像 EvidentlyArize 这样的工具通常用于 ML 监控。但在这种情况下,我想保持简单,不再添加另一个工具。

但概念仍然是相同的,这一点最为关键。

在下面的代码片段中,我们做了以下事情:

  1. 从 GCP 存储桶加载了预测数据。所有预测数据都在批量预测步骤中聚合在 predictions_monitoring.parquet 文件中。

  2. 准备了预测 DataFrame 的结构。

  3. 连接到 Hopsworks 特征存储。

  4. 查询了特征存储中位于最小和最大预测时间边界的数据。这就是你的 GT。你想根据预测的时间窗口获取所有可用的数据。

  5. 准备了 GT DataFrame 的结构。

  6. 合并两个 DataFrame。

  7. 在 GT 可用的地方,计算 MAPE 指标。为了简化,你将计算所有时间序列的 MAPE 指标的聚合值。

  8. 将结果写回到 GCP 存储桶,由前端加载和显示。

上面定义的函数将在 Airflow DAG 中作为自己的任务运行。每次 ML 管道运行时,它都会被调用。因此,每小时,它将查找预测和 GT 之间的新匹配项,计算 MAPE 指标并将其上传到 GCS。

阅读 第 6 课 了解如何使用 Streamlit 和 FastAPI 以美观的 UI 显示来自 GCP 存储桶的结果。

结论

恭喜你!你完成了 第五课 来自 全栈 7 步 MLOps 框架 课程。这意味着你接近于了解如何使用 MLOps 好实践构建一个端到端的 ML 系统。

在本课程中,你学会了如何:

  • 使用 GE 构建一个数据验证套件来测试你的数据质量和完整性,

  • 理解为什么 ML 监控至关重要,

  • 构建你自己的 ML 监控系统,以实时跟踪模型性能。

现在你理解了掌控数据和 ML 系统的力量,你可以安稳地睡觉,知道一切运转良好,如果有问题,你也可以快速诊断。

查看第 6 课以了解如何使用你的预测和来自 GCP 存储桶的监控指标,利用 FastAPI 和 Streamlit 构建一个 Web 应用。

此外, 你可以在这里访问 GitHub 仓库

💡 我的目标是帮助机器学习工程师在设计和生产化 ML 系统方面提升技能。关注我在 LinkedIn 或订阅我的 每周通讯 获取更多见解!

🔥 如果你喜欢阅读类似的文章并希望支持我的写作,考虑 成为 Medium 会员。通过使用 我的推荐链接,你可以在没有额外费用的情况下支持我,同时享受 Medium 丰富故事的无限访问。

[## 使用我的推荐链接加入 Medium - Paul Iusztin

🤖 加入以获取有关设计和构建生产就绪 ML 系统的独家内容 🚀 解锁完整访问…

pauliusztin.medium.com

参考资料

[1] 丹麦 API 的 DE35 行业代码能耗丹麦能源数据服务

2 企业 AI 数据验证:在 Hopsworks 中使用 Great Expectations(2022),Hopsworks 博客

娱乐数据科学:流媒体与影院

原文:towardsdatascience.com/entertainment-data-science-streaming-vs-theatrical-af948b69a8f7?source=collection_archive---------6-----------------------#2023-12-08

不同之处与相似之处

Danny Kim, PhDTowards Data Science Danny Kim, PhD

·

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

--

照片由 Krists Luhaers 拍摄,发布在 Unsplash

在我在Towards Data Science上的娱乐数据科学的新前沿文章中,我提到数据科学如何应用于内容生命周期的各个阶段,从绿灯到制作再到发布。虽然很容易理解数据科学应用在决定哪些剧本应该获得绿灯和优化制作成本之间可能会有所不同,但即使在那些乍一看似乎相对相似的背景下,也可能存在显著差异。

大约一年半前,我在一家大型电影公司开始了新工作。由于之前的工作是在流媒体技术方面,我预期情况会多多少少相似,只是这次我将专注于电影数据,而不是电视和电影数据。使用数据预测受欢迎程度,情况会有多大不同呢?

哎呀,我真的是毫无头绪。

业务完全不同。问题不同,利益相关者不同,数据不同,等等。因此,我想写这篇文章有两个目标。第一个明显的目标是向有志于娱乐数据领域的初级专业人士展示数据科学工作在影院和流媒体背景下的差异。但我想这种动态在许多不同的行业中也可能会显现——你认为你会做大致相同的预测 Y 使用 X 的工作,却发现 X 和 Y 的实际情况完全不同——所以第二个更广泛的目标是让各个领域的数据专业人士了解到,即使两个职位在表面上看起来功能上类似,它们在深入探究数据和业务问题后也可能在各种方面截然不同。

基于此,以下是我从流媒体娱乐数据科学转到影院娱乐数据科学后的关键观察。我略过了一些显而易见的“显而易见”点(哦,原来没有影院电视剧发布,真是意外),但我触及了一些主要趋势。当然,这些并不是一些绝对真理的陈述;根据公司、团队领导等情况,你的体验可能会有所不同。此外,虽然数据科学可以在娱乐内容生命周期的早期阶段发挥作用,如我上面提到的,这篇文章来源于我在接近发布的下游流程中的经验。如果我有时显得有些模糊,那是故意的,因为我不想泄露任何秘密调料 😉

图片由Joshua Sortino提供,发布在Unsplash上。

数据范围

最明显的区别在于数据的范围。在戏剧数据科学中,主要的分析单位是电影,也许是电影-国家,而在特定年份里,每个国家上映的电影数量有限!这与流媒体数据科学相对,在流媒体中,你通常可以获得来自历史的个人-内容-时间级别的数据,导致数据集大得多(也需要不同的工具来处理它们)。

这并不是说你从未在戏剧方面处理更大、更详细的数据集;这些数据集通常与标题或标题的某个元素相关,你常常以某种方式处理它们,以生成与标题相关的见解。但底线是,由于标题空间默认较小,因此数据范围也较小。我希望有一天,我们可以在戏剧领域获得更多像流媒体领域那样的细粒度、个体层面的消费见解,但至少现在情况不是这样。

Michael Marais 的照片,来源于 Unsplash

无* 历史数据

在流媒体中,绝大多数情况下(流媒体独占发布的明显例外),你将拥有大量历史数据可供使用。标题在票房上赚了多少钱?它发布时的社交媒体热度如何?这个标题在《烂番茄》上的表现如何?

在戏剧领域,你无法享受这种奢侈。确实,你可以在一定程度上依赖特定组件的历史数据,无论是演员、剧组、类型还是某种组合,但即便如此,这些数据点通常也不会像字面历史数据那样与特定标题清晰关联。此外,这种比较可能充满主观性问题和外部干扰因素;如何决定哪些标题真正可以相互比较?营销和营销活动的差异在公众对标题间相似性的认知中扮演了什么角色?

是的,系列和特许经营在某种程度上是这个规则的半例外,但过度依赖续集效应和相似性假设很容易适得其反。是的,在许多情况下,前作的表现可以较好地估计其继任者的表现,但系列可能会随着时间的推移失去动力或因超出认知范围(新角色/情节和与过去标题的弱联系)而无法识别,以至于早期标题的表现对预测新标题的表现可能毫无意义。

照片由Scott Graham提供,来源于Unsplash

非常特别的数据

来自由技术人员管理的数据团队的初创流媒体技术领域,我花了大量时间研究哪些数据集可能对我们的需求有用。在这一过程中,我发现了许多适合各种需求的冷门数据集,并调查了我们如何能够以低成本收集那些供应商以高价出售的数据(例如,我们如何在不支付昂贵许可费用的情况下获取 Google 搜索数据?)。

在戏剧领域,标准和惯例似乎更为成熟。某些消费者和社交媒体数据集或数据集类型在行业内几乎被所有人使用。例如,虽然社交聆听可能作为一个显而易见的当代数据来源,但存在一些主要的成熟供应商提供详细的预发布和发布后消费者数据,这些供应商中的一些已经存在了几十年。这些是许多不在戏剧领域的人可能从未听说过的数据集,但当你身处其中时,它们是你谈论的所有内容。

照片由Marten Bjork提供,来源于Unsplash

无窗口(或单一窗口)

在流媒体领域,可用窗口——以及在某种程度上,可用性的性质——是分析中的一个重要因素。这些窗口可能与各种内容(例如,标题是否与圣诞节有关,而窗口是圣诞节窗口?)和市场级因素(例如,标题是否在首页显著展示?)的因素相互作用。

正如你所想象的那样,这些顾虑在戏剧数据科学中较少存在——或者更准确地说,除非你在做与“我们是否应该制作这个标题?”或“我们什么时候应该发布这个标题?”相关的上游建模,否则在你参与之前,关于窗口因素的所有问题已经以(可能的)发布日期的形式为你决定好了。你只需担心一个窗口(除非你需要担心错开的发布日期,那是完全不同的情况),而上层决策者已经决定了发布日期。现在你需要在这个窗口的背景下尽力提供所有有用的见解。

照片由Christian Joudrey提供,来源于Unsplash

更加重视业务

在我从事流媒体工作的期间,我们很容易将标题和观众视作数字,因为我们拥有成千上万的数据,但这种哲学也反映在方法论上。我们不仅会生成总结性统计数据,还会将所有内容转化为某种向量嵌入(即一系列在某些人类不可观测的维度上有意义的数字),即使这可能会牺牲可解释性;说“内容维度 2 是模型中最重要的变量”并不真正有意义。

流媒体服务更像科技公司运营,而戏剧化方面则更接近传统的制片公司业务。在戏剧化方面,除了数字之外,更注重业务本身。数据的存在不仅仅是为了数字本身,而是为了提供可操作的见解给组织中的各种利益相关者,其中许多人既不是数据科学家,也不在日常工作中使用数据。做出准确的预测很重要,但同样重要的是可解释性,而不是仅仅为了减少模型误差的微小百分比而急于放弃可解释性。因此,我觉得自己在所做的工作中与业务和观众之间有了更多的联系。

关键要点和结论

在我讨论流媒体到戏剧化转变的过程中,我涉及了各种主题,但潜在的主题包括询问那些在同一行业中从事两个松散类似的工作的人的相关问题。因此,总结一下,在你认为你的下一份工作将会大致与上一份工作相似之前,根据上述提到的流媒体与戏剧化数据科学之间的差异,这里有一些值得更深入思考的问题,以帮助你思考你上一份工作和下一份工作之间的相似性和差异:

  • 数据范围: 数据的单位是什么?数据添加的频率和每次的单位数量是多少?因此,数据集有多大,需要什么工具来处理这样一个数据集?

  • 历史数据的可用性: 如果有的话,提供了哪些类型的历史数据?可用的历史数据是否直接适用,还是需要某种聚合、插补或相似性分析?

  • 数据来源: 使用了哪些数据来源?这些数据来源是更一般性相关的,还是非常特定于某些上下文的?是否有实验新数据来源或搁置现有数据来源的空间?大家都在使用哪些既定的、传统的数据集?

  • 时间要素: 你必须回答的特定问题的相关时间窗口是什么?如何决定?是单一的还是多个的,是固定的还是不断变化的?在工作中如何考虑时间和相关因素(例如季节性、假期等)?某个特定的时间窗口对业务是否比其他时间窗口更重要?

  • 业务重点: 受众是谁?鉴于此,需要在准确性和可解释性之间找到什么样的平衡?这又如何影响你认为有用的特征?业务的节奏如何推动工作的节奏?

显然,我被聘为目前的职位是因为我的技能集与职位职责相关,并且我所做的与我之前做的类似——但与流媒体方面相比,剧院方面的数据科学在某种程度上既相似又不同。如我上文所述,数据不同,流程不同,期望不同。如果你希望进入激动人心的娱乐数据科学领域,或考虑转到相似但不同的工作领域,我希望你能发现这篇文章对你有用!

在撰写时,丹尼·金(宾夕法尼亚大学博士;《福布斯》30 岁以下 30 人名单 2022)是索尼影业娱乐电影集团市场分析与洞察团队的高级数据科学家。丹尼之前在 Whip Media 和派拉蒙影业工作过,他是宾夕法尼亚大学和南加州大学安纳伯格传播学院、沃顿商学院和南加州大学电影艺术学院的校友。

实体解析:识别嘈杂数据中的真实世界实体

原文:towardsdatascience.com/entity-resolution-identifying-real-world-entities-in-noisy-data-3e8c59f4f41c?source=collection_archive---------3-----------------------#2023-09-21

基本理论和 Python 实现

Tomonori MasuiTowards Data Science Tomonori Masui

·

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

--

图片由作者使用 Midjourney 生成

在当今数据驱动的世界中,组织常常面临多样且不一致的数据来源的挑战。实体解析,也称为记录链接或去重,帮助识别和合并在数据集内或跨数据集没有共享唯一标识符的重复或相关记录。准确的实体解析提高了数据质量,增强了决策制定,并提供了有价值的见解。

实体解析在不一致的数据源中识别相同的现实世界实体(图像由作者提供)

实体解析适用于各种行业。例如,CRM 系统通过解析重复的客户记录来整合客户信息、改善服务和实现精准营销。电子商务平台使用实体解析来合并产品列表,提升搜索功能、推荐系统和客户体验。

在这篇文章中,我们将探讨使用基准数据集的基本实体解析方法的技术细节。

目录

  • 实体解析概述

  • 基准数据集

  • 阻断

  • 块处理

  • 实体匹配

  • 聚类

  • 集群评估

实体解析概述

标准的实体解析(ER)框架包括几个步骤:阻断、块处理、实体匹配和聚类。

1. 阻断:这是实体解析的第一步,旨在通过将数据集划分为较小、可管理的块来减少搜索空间,从而识别相同的实体。这些块包含共享类似属性的记录,使后续的比较更加高效。

2. 块处理:此步骤通过丢弃两种不必要的比较来精化块,从而最小化比较的数量:冗余比较,即在多个块中重复出现的比较,以及无用比较,即涉及不太可能匹配的记录的比较。

3. 实体匹配:这一阶段专注于比较块中的记录,以根据记录的相似性寻找匹配。可以使用各种相似性度量和匹配算法来将记录对分类为匹配或不匹配。

4. 聚类:聚类涉及根据记录的相似性将匹配的记录分组到集群中。创建的集群可以用于获取实体的综合视图。

实体解析工作流程(图像由作者提供)

基准数据集

在接下来的章节中,我们将深入探讨实体解析过程中的每个步骤的更多细节,并使用基准数据集进行 Python 实现。

该数据集来源于莱比锡大学数据库组,并获得了创作共用许可证,来源于实际的MusicBrainz数据库中有关歌曲的记录,但经过DAPO 数据污染工具故意进行了修改。该工具向数据集中注入了重复项和错误,导致数据集中包含了 50%原始记录的重复项,覆盖了两个到五个来源。这些重复项具有较高的损坏程度,作为评估 ER 和聚类方法有效性的严格测试。

我们可以使用以下代码加载数据。

import requests
from io import BytesIO
import pandas as pd

url = "https://raw.githubusercontent.com/tomonori-masui/entity-resolution/main/data/musicbrainz_200k.csv"
res = requests.get(url)
df = pd.read_csv(BytesIO(res.content))

一些示例记录如下所示。

每条记录代表一首歌曲,具有诸如艺术家、标题、专辑、年份等属性(你可以在这个链接中找到字段描述)。CID是集群 ID,具有相同CID的记录是重复的(在上面的示例中,所有三条记录代表同一首歌曲)。我们的目标是在这个嘈杂的数据集中识别这些重复项。

为了简化工作,我们只关注英文歌曲。下面的代码识别出具有英文歌曲的集群 ID 的记录。

english_cids = df[
    df.language.str.lower().str.contains("^en|^eg", na=False)
].CID.unique()

df = df[df.CID.isin(english_cids)].reset_index(drop=True)

我们还对一些字符串字段进行预处理,以获得标准化的值。

for col in ["title", "artist", "album"]:
    df[col] = (
        df[col]
        .str.lower()
        .replace("[^a-z0-9]", " ", regex=True)  # replacing special characters with a space
        .replace(" +", " ", regex=True)         # removing consecutive spaces
        .str.strip()                            # removing leading and tailing spaces
    )

df.loc[df.number.notna(), "number"] = (
    df[df.number.notna()]
    .number.replace("[⁰-9]", "", regex=True)              # removing non-digits
    .apply(lambda x: str(int(x)) if len(x) > 0 else None)  # removing leading zeros
)

请注意,这个基准数据集是一个单一的数据集,如果你有多个数据来源需要解决实体问题,你需要标准化它们的数据模式,并将这些多个数据源整合成一个统一的数据集,然后再继续后续步骤。

阻断

阻断是实体解析的第一步,它根据某些属性将相似记录分组。通过这样做,过程将搜索范围缩小到仅考虑每个块内的比较,而不是检查数据集中的所有可能记录对。这显著减少了比较的数量,加快了 ER 过程。由于跳过了许多比较,这可能会导致错过真实匹配。因此,阻断应该在效率和准确性之间取得良好的平衡。在本节中,我们将探索三种不同的阻断方法(标准阻断、标记阻断和排序邻域),以找到最佳的平衡点。

标准阻断

最直接的块处理技术是根据特定属性将数据集划分为块。例如,在我们的数据集中,可以根据 ArtistTitle 字段创建块。这种方法直观且易于实现,但其有效性对噪声非常敏感,因为重复项的阻塞键有一点点不同就会把它们放在不同的块中。

在艺术家字段上的标准块示例(图像由作者提供)

我们可以使用以下函数获得标准块。字典 blocks 将存储阻塞键(key)及其对应的已阻塞记录的索引(idx)。

from collections import defaultdict

def standard_blocking(field_values: pd.Series) -> dict[str, list]:

    blocks = defaultdict(list)
    for idx, key in enumerate(field_values):
        if key is not None:
            blocks[key].append(idx)

    return blocks

在以下代码中,我们使用 titleartistalbum 字段创建三个独立的标准块。

sb_title = standard_blocking(df.title)
sb_artist = standard_blocking(df.artist)
sb_album = standard_blocking(df.album)

令牌块

令牌块处理的重点是将属性值分解(即令牌化)为更小的单位,称为令牌,然后使用这些令牌创建用于比较的块。令牌通常是从文本中提取的单个单词或小的 n-gram(长度为n的子字符串)。令牌块为每个不同的令牌值创建一个块,而不考虑相关属性:如果两个记录在其任何属性中共享一个令牌,它们将位于同一个块中。这产生了高召回率,因为冗余(即单个记录可以属于多个块),代价是低精确度。

令牌块的示例(图像由作者提供)

以下函数基于单词令牌生成令牌块。请注意,我们排除了停用词(例如“a”、“the”、“is”等)中的令牌。

from nltk.tokenize import word_tokenize

def token_blocking(df: pd.DataFrame, stop_words: set) -> dict[str, list]:

    blocks = defaultdict(list)

    for i, row in enumerate(df.itertuples()):

        # concatenate columns and tokenize
        string = " ".join([str(value) for value in row if not pd.isna(value)])
        tokens = set(
            [word for word in word_tokenize(string) if word not in stop_words]
        )

        # create blocks
        for token in tokens:
            blocks[token].append(i)

    return blocks

由于我们知道哪些字段与创建块相关,我们仅使用特定字段(titleartistalbum)来执行令牌块处理:

import string
from nltk.corpus import stopwords

columns = ['title', 'artist', 'album']
stop_words = set(stopwords.words('english') + list(string.punctuation))
token_blocks = token_blocking(df[columns], stop_words)

排序邻域

排序邻域按特定字段的值进行字母顺序排序。一个固定大小的窗口在排序后的记录上滑动,窗口内的所有可能对被标识为比较的候选对。请注意,它直接生成一对对的列表,而不是块。虽然这种方法有效处理了阻塞字段中的噪声,但选择较小的窗口会牺牲召回率以提高精度,而较大的窗口具有更高的召回率但精度较低。

带有窗口大小为 3 的排序邻域示例(图像由作者提供)

以下代码执行窗口大小为 3 的排序邻域,使用 titleartistalbum 字段作为排序键。

def sorted_neighborhood(
    df: pd.DataFrame, keys: list, window_size: int = 3
) -> np.ndarray:

    sorted_indices = (
        df[keys].dropna(how="all").sort_values(keys).index.tolist()
    )
    pairs = []
    for window_end in range(1, len(sorted_indices)):
        window_start = max(0, window_end - window_size)
        for i in range(window_start, window_end):
            pairs.append([sorted_indices[i], sorted_indices[window_end]])

    return np.array(pairs)

columns = ['title', 'artist', 'album']
sn_pairs = sorted_neighborhood(df, columns)

我们将在接下来的两个部分中进行块处理和实体匹配后,比较这三种方法的性能。

块处理

这一步旨在提高块的精度,同时保持可比较的召回率水平。相关技术包括在输入块集合B内减少不必要和冗余的比较,从而生成一个具有改进精度的新块集合B'。我们将在本节探讨一些主要的块处理技术。

块清理

块清理设置块大小的上限,并清除大小超过限制的块。它假设过大的块由冗余比较主导,这意味着这些块中包含的重复项更可能出现在其他较小的块中。

下面的代码按预定的限制值(此处设为 1000 条记录)清除块。它还过滤掉只有一条记录的块,因为这些块不生成可比较的对。我们在前一节的三个标准块和标记块上执行此purge_blocks函数。

def purge_blocks(
    blocks: dict[str, list], purging_threshold: int = 1000
) -> dict[str, list]:

    blocks_purged = {
        key: indices
        for key, indices in blocks.items()
        if len(indices) < purging_threshold and len(indices) > 1
    }

    return blocks_purged

token_blocks = purge_blocks(token_blocks)
sb_title = purge_blocks(sb_title)
sb_artist = purge_blocks(sb_artist)
sb_album = purge_blocks(sb_album)

元块

元块转换输入块集合为图(或邻接矩阵),其中每个节点对应一条记录,边连接每对在块中共同出现的记录。边权重表示跨块对出现频率:权重越高,匹配可能性越大。低权重的边被剪枝,因为它们可能代表多余的比较。因此,对于每个保留的边,生成一个新块,导致精细化的块集合(或作为每个精细化块仅有一对记录的对列表)。

元块示例(作者提供的图片)

我们仅对标记块执行元块,因为它们在块之间有很多重叠。下面的代码首先从标记块中创建一对对的列表,然后将其转换为邻接矩阵。

import itertools
from scipy.sparse import csr_matrix

def get_pairs_from_blocks(blocks: dict[str, list]) -> list[list]:
    return [
        pair
        for indices in blocks.values()
        for pair in list(itertools.combinations(indices, 2))
    ]

def get_adjacency_matrix_from_pairs(
    pairs: list[list], matrix_shape: tuple[int, int]
) -> csr_matrix:

    idx1 = [pair[0] for pair in pairs]
    idx2 = [pair[1] for pair in pairs]
    ones = np.ones(len(idx1))

    return csr_matrix(
        (ones, (idx1, idx2)), shape=matrix_shape, dtype=np.int8
    )

pairs = get_pairs_from_blocks(token_blocks)
adj_matrix = get_adjacency_matrix_from_pairs(pairs, (len(df), len(df)))

接下来,我们根据边权重在邻接矩阵中剪枝。在这里,我们剪枝所有边权重为 1 的边,即仅在单个块中出现的对被修剪。

def prune_edges(
    adj_matrix: csr_matrix,
    edge_weight_threshold: float,
) -> csr_matrix:

    adj_matrix_pruned = adj_matrix >= edge_weight_threshold

    return adj_matrix_pruned

adj_matrix = prune_edges(adj_matrix, edge_weight_threshold=2)

接着,我们从经过剪枝的邻接矩阵中获取对。

def get_pairs_from_adj_matrix(adjacency_matrix: csr_matrix) -> np.ndarray:
    return np.array(adjacency_matrix.nonzero()).T

tb_pairs = get_pairs_from_adj_matrix(adj_matrix)

块的并集

对于标准块,我们获取三个独立块的并集。首先,我们将块转换为邻接矩阵列表。

adj_matrix_list = []
for blocks in [sb_title, sb_artist, sb_album]:
    pairs = get_pairs_from_blocks(blocks)
    adj_matrix_list.append(
        get_adjacency_matrix_from_pairs(pairs, (len(df), len(df)))
    )

接着,我们从矩阵的并集和其中的候选对中获取结果。

def get_union_of_adj_matrices(adj_matrix_list: list) -> csr_matrix:

    adj_matrix = csr_matrix(adj_matrix_list[0].shape)
    for matrix in adj_matrix_list:
        adj_matrix += matrix

    return adj_matrix

adj_matrix_union = get_union_of_adj_matrices(adj_matrix_list)
sb_pairs = get_pairs_from_adj_matrix(adj_matrix_union)

下表总结了三种不同块处理方法生成的最终候选对数量。

我们将通过查看下一节中的匹配结果来确定哪个最适合我们的数据。

实体匹配

在这一步骤中,我们从上一步生成的候选对中识别匹配对。虽然有多种方法可以找到匹配,但一种简单直接的方法可以如下概述:

  1. 在每个属性上测量相似度

    你可以使用任何相似性度量,如余弦相似度、杰卡德相似度或莱文斯坦距离相似度,根据你的数据或具体要求的适用性。在计算某些度量的相似性之前,文本字段可能需要进行分词。

  2. 计算整体相似性

    此步骤将每个属性的相似性组合成一个整体的相似性分数,可以通过应用手动定义的规则或利用在标记数据上训练的机器学习模型来实现。

  3. 确定匹配

    对整体相似性分数应用相似性阈值以找到匹配

实体匹配示例(图片由作者提供)

以下函数 get_field_similarity_scores 处理上述第 1 步。如果 sim_type 设置为 “fuzzy”,则计算 余弦相似度;否则,进行精确匹配。余弦相似度是在字符级 3-grams 上计算的,这些 3-grams 通过使用来自 scikit-learn 的 [CountVectorizer](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html) 模块从输入字符串中向量化。我们计算 titleartistalbum 字段的余弦相似度,同时对 number 字段进行精确匹配。

from sklearn.preprocessing import normalize
from sklearn.feature_extraction.text import CountVectorizer

def get_field_similarity_scores(
    df: pd.DataFrame, pairs: np.ndarray, field_config: dict[str, str]
) -> dict[str, np.ndarray]:
    """
    Measuring similarity by field. It is either cosine similarity
    (if sim_type == 'fuzzy') or exact match 0/1 (if sim_type == 'exact'). 
    Attribute's similarity scores are stored in field_score dictionary 
    with the field name as key.
    """

    field_scores = {}

    for field, sim_type in field_config.items():
        if sim_type == "fuzzy":
            field_scores[field] = cosine_similarities(
                df[field].fillna(""), pairs
            )
        else:
            field_scores[field] = exact_matches(df[field], pairs)

    return field_scores

def cosine_similarities(
    field_values: pd.Series, pairs: np.ndarray
) -> np.ndarray:
    """
    Computing cosine similarities on pairs
    """

    token_matrix_1, token_matrix_2 = get_token_matrix_pair(
        field_values, pairs
    )
    cos_sim = cosine_similarities_on_pair_matrices(
        token_matrix_1, token_matrix_2
    )

    return cos_sim

def get_token_matrix_pair(
    field_values: pd.Series, pairs: np.ndarray,
) -> tuple[csr_matrix, csr_matrix]:
    """
    Converting pairs into matrices of token counts (matrix of records 
    by tokens filled with token counts). 
    """

    all_idx = np.unique(pairs)
    vectorizer = CountVectorizer(analyzer="char", ngram_range=(3, 3))
    vectorizer.fit(field_values.loc[all_idx])
    token_matrix_1 = vectorizer.transform(field_values.loc[pairs[:, 0]])
    token_matrix_2 = vectorizer.transform(field_values.loc[pairs[:, 1]])

    return token_matrix_1, token_matrix_2

def cosine_similarities_on_pair_matrices(
    token_matrix_1: csr_matrix, token_matrix_2: csr_matrix
) -> np.ndarray:
    """
    Computing cosine similarities on pair of token count matrices.
    It normalizes each record (axis=1) first, then computes dot product
    for each pair of records.
    """

    token_matrix_1 = normalize(token_matrix_1, axis=1)
    token_matrix_2 = normalize(token_matrix_2, axis=1)
    cos_sim = np.asarray(
        token_matrix_1.multiply(token_matrix_2).sum(axis=1)
    ).flatten()

    return cos_sim

def exact_matches(
    field_values: pd.Series, pairs: np.ndarray
) -> np.ndarray:
    """
    Performing exact matches on pairs
    """

    arr1 = field_values.loc[pairs[:, 0]].values
    arr2 = field_values.loc[pairs[:, 1]].values

    return ((arr1 == arr2) & (~pd.isna(arr1)) & (~pd.isna(arr2))).astype(int)

field_config = {
    # <field>: <sim_type>
    "title": "fuzzy",
    "artist": "fuzzy",
    "album": "fuzzy",
    "number": "exact",
}

field_scores_sb = get_field_similarity_scores(df, sb_pairs, field_config)

基于规则的匹配

基于规则的匹配(图片由作者提供)

在计算特定领域的相似性分数后,我们希望将它们组合成一个整体的相似性分数,如上述第 2 步所述。我们在这里采用了一个非常简单的方法:仅计算属性分数的平均值,随后应用一个分数阈值来识别匹配(第 3 步)。下面的阈值值已在这里调整过,但你可能需要通过查看一些匹配/不匹配的示例来调整它,当你处理自己的数据集时。

def calc_overall_scores(field_scores: dict[str, np.ndarray]) -> np.ndarray:
    return np.array(list(field_scores.values())).mean(axis=0)

def find_matches(scores: np.ndarray, threshold: float) -> np.ndarray:
    return scores >= threshold

scores_sb = calc_overall_scores(field_scores_sb)
is_matched_sb = find_matches(scores_sb, threshold=0.64)

上面的代码对标准块中的对进行了匹配。此外,我们将此匹配过程扩展到来自 token block 和 sorted neighborhood 的对,允许我们比较它们的表现。下面的代码在表格中总结了比较结果。

from IPython.display import display
from collections import Counter

def show_results(
    is_matched_list: list[np.ndarray],
    blocking_approach_name_list: list[str],
):

    result = pd.DataFrame(
        [Counter(is_matched).values() for is_matched in is_matched_list],
        columns=["Unmatch", "Match"],
    )
    result["Blocking Approach"] = blocking_approach_name_list
    result["Matching Rate"] = result.Match / (
        result.Match + result.Unmatch
    )
    result["Matching Rate"] = result["Matching Rate"].map("{:.1%}".format)
    result["Match"] = result["Match"].map("{:,}".format)
    result["Unmatch"] = result["Unmatch"].map("{:,}".format)

    display(
        result[["Blocking Approach", "Match", "Unmatch", "Matching Rate"]]
    )

is_matched_list = [is_matched_sb, is_matched_tb, is_matched_sn]
blocking_approach_name_list = [
    "Standard Blocking",
    "Token Blocking",
    "Sorted Neighborhood",
]
show_results(is_matched_list, blocking_approach_name_list)

下面是输出结果。

如表中所示,Token Blocking 产生了最多的匹配数,而 Sorted Neighborhood 的匹配率最高。由于 Token Blocking 可能错过的匹配最少,我们将继续使用这种方法的结果。值得注意的是,我们的小数据集并未显示出可扩展性问题。然而,对于较大的数据集,其中 Token Blocking 可能不可行,你可能需要考虑其他更具可扩展性的方法。

机器学习匹配

机器学习匹配(图片由作者提供)

如果您有标记数据或手动标记的样本对(作为匹配或非匹配),您可以训练一个机器学习模型来预测匹配的对。由于我们的数据具有集群标签CID,我们将这些转换为对的匹配标签(匹配/非匹配),并训练一个机器学习模型,随后与前一节中执行的基于规则的方法进行性能比较。

以下代码生成模型输入X和相应的目标变量y。同一集群CID内的对被标记为匹配(y = 1),而不同集群内的对被标记为非匹配(y = 0)。

def get_x_y(
    field_scores: dict[str, np.ndarray],
    pairs: np.ndarray,
    df: pd.DataFrame,
) -> tuple[pd.DataFrame, np.ndarray]:

    X = pd.DataFrame(field_scores)
    y = df.loc[pairs[:, 0], "CID"].values == df.loc[pairs[:, 1], "CID"].values

    return X, y

X, y = get_x_y(field_scores_tb, tb_pairs, df)

接下来,我们将它们分为训练集和测试集,然后训练逻辑回归模型。

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.5, random_state=42
)

model = LogisticRegression(random_state=0).fit(X_train, y_train)

下面的代码将其性能与基于规则的方法进行比较([f1_score](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.f1_score.html))。

from sklearn.metrics import f1_score

y_pred = model.predict(X_test)
print(f"Model f1_score: {f1_score(y_test, y_pred):.3f}")

y_rule_base = is_matched_tb[X_test.index.values]
print(f"Rule-base f1_score: {f1_score(y_test, y_rule_base):.3f}")

以下是输出:

图片链接

尽管模型的性能更好,基于规则的方法的性能仍然可能相当不错。

对于接下来的步骤,我们将使用通过基于规则的方法识别出的匹配项,考虑到在许多实际情况下,由于资源限制,手动数据标记可能并不实际。下面的代码从候选对中提取匹配对及其相似性分数及其在标记阻塞上的分数。

matched_pairs = tb_pairs[is_matched_tb]
matched_scores = scores_tb[is_matched_tb]

聚类

在这一步中,我们基于上一步中的匹配对创建实体集群。每个集群包括所有对应于一个不同现实世界实体的记录。

实体解析的聚类有几个要求:

  1. 无约束算法

    算法不应需要任何领域特定的参数作为输入,例如集群数或集群直径。

  2. 处理不完整相似性矩阵的能力

    由于实体解析过程不会在每个可能的对上计算相似性(可以描述为 N 乘以 N 矩阵),因此算法必须能够处理不完整的相似性矩阵(或匹配对列表)。

  3. 可扩展性

    实体解析通常处理大型数据集,因此算法能够处理此类数据非常重要。在大数据情况下,像层次聚类这样的高复杂度算法可能不实际。

对于聚类,我们将研究三种主要的单遍聚类算法:分区(即连接组件)、中心聚类和合并中心聚类,它们都满足要求。这些算法非常高效,因为它们通过一次扫描(或 O(n)时间复杂度)候选对列表来创建集群,尽管其中一些算法要求列表按相似性分数排序。

图片链接

单遍聚类算法(来源:www.vldb.org/pvldb/vol2/vldb09-1025.pdf

划分/连通组件

该算法通过最初将每个节点分配到其单独的集群来启动聚类。然后,它对匹配对的列表进行单次扫描。如果发现不属于同一集群的连接节点,它将合并它们的集群。简而言之,它通过将所有连接节点通过边(即通过配对的匹配记录)分组形成一个集群。该算法可能会创建通过长路径连接不相似记录的集群。

连通组件聚类可以使用 Scipy 模块执行,如下面的代码所示。在执行之前,你需要将配对列表转换为邻接矩阵。

from scipy.sparse.csgraph import connected_components

def connected_components_from_pairs(
    pairs: np.ndarray, dim: int
) -> np.ndarray:

    adjacency_matrix = get_adjacency_matrix_from_pairs(pairs, (dim, dim))
    _, clusters = connected_components(
        csgraph=adjacency_matrix, directed=False, return_labels=True
    )

    return clusters

cc_clusters = connected_components_from_pairs(matched_pairs, len(df))

中心聚类

该算法[5](#299e)执行聚类,其中每个集群都有一个中心,并且每个集群中的所有记录都与该集群的中心相似。它要求相似对的列表按相似度分数的降序排序。然后,算法通过对排序列表的单次扫描来执行聚类。当第一次在扫描中遇到节点u时,它被指定为集群中心。任何后续与u相似的节点v(即,出现在列表中的对(u, v)中)都被分配到u的集群中,并且在处理过程中不再考虑。

中心聚类示例(图像由作者提供)

合并中心聚类

该算法[6](#d172)的功能类似于中心聚类,但每当一个与集群cᵢ的中心相似的记录也与cⱼ的中心相似时,就会合并两个集群cᵢcⱼ。请注意,当两个集群合并时,它不会选择一个单一的中心节点,这意味着合并的集群可以有多个中心节点。该算法可以通过类似的方式进行,即通过对相似对的列表进行单次扫描,同时跟踪通过合并集群连接的记录。

合并中心聚类示例(图像由作者提供)

要执行中心/合并中心聚类,我们首先需要按相似度分数的降序对配对列表进行排序。

def sort_pairs(pairs: np.ndarray, scores: np.ndarray) -> np.ndarray:
    sorted_ids = (-1 * scores).argsort()
    return pairs[sorted_ids]

pairs_sored = sort_pairs(matched_pairs, matched_scores)

接下来,下面的代码生成两个配对集合:中心-子节点配对,表示为center_cluster_pairs,和合并节点配对,称为merge_cluster_pairs。然后,我们可以通过将连通组件应用于这些配对列表来生成中心集群和合并中心集群。

def get_center_cluster_pairs(pairs, dim):

    """
    cluster_centers: 
        a list tracking cluster center for each record.
        indices of the list correspond to the original df indices
        and the values represent assigned cluster centers' indices
    center_cluster_pairs: 
        a list of pairs of indices representing center-child pairs
    merge_cluster_pairs:
        a list of pairs of merged nodes' indices
    """
    cluster_centers = [None] * dim
    center_cluster_pairs = []
    merge_cluster_pairs = []

    for idx1, idx2 in pairs:

        if (
            cluster_centers[idx1] is None
            or cluster_centers[idx1] == idx1
            or cluster_centers[idx2] is None
            or cluster_centers[idx2] == idx2
        ):
            # if both aren't child, those nodes are merged
            merge_cluster_pairs.append([idx1, idx2])

        if cluster_centers[idx1] is None and cluster_centers[idx2] is None:
            # if both weren't seen before, idx1 becomes center and idx2 gets child
            cluster_centers[idx1] = idx1
            cluster_centers[idx2] = idx1
            center_cluster_pairs.append([idx1, idx2])
        elif cluster_centers[idx2] is None:
            if cluster_centers[idx1] == idx1:
                # if idx1 is center, idx2 is assigned to that cluster
                cluster_centers[idx2] = idx1
                center_cluster_pairs.append([idx1, idx2])
            else:
                # if idx1 is not center, idx2 becomes new center
                cluster_centers[idx2] = idx2
        elif cluster_centers[idx1] is None:
            if cluster_centers[idx2] == idx2:
                # if idx2 is center, idx1 is assigned to that cluster
                cluster_centers[idx1] = idx2
                center_cluster_pairs.append([idx1, idx2])
            else:
                # if idx2 is not center, idx1 becomes new center
                cluster_centers[idx1] = idx1

    return center_cluster_pairs, merge_cluster_pairs

center_cluster_pairs, merge_cluster_pairs = get_center_cluster_pairs(pairs_sored, len(df))
ct_clusters = connected_components_from_pairs(center_cluster_pairs, len(df))
mc_clusters = connected_components_from_pairs(merge_cluster_pairs, len(df))

集群评估

有了集群标签后,我们可以使用Rand Index调整后的 Rand Index来评估集群的质量。Rand Index 是一种集群评估指标,表示正确聚类在一起或分开的配对比例。其定义如下:

TP = 在预测簇和真实簇中 同时 被聚类的对数。

TN = 在预测簇和真实簇中 分开 被聚类的对数。

Rand Index = (TP + TN) / 所有可能对数的总数

Rand Index 计算示例(图片由作者提供)

调整后的 Rand Index 是 Rand Index 的一种修改版本,已为偶然性进行修正。该调整考虑了随机分配的聚类结果可能产生的随机一致性。

调整后的 Rand Index 的方程

我们不会深入探讨上述方程中每个术语的计算方式,但任何对这个主题感兴趣的人可以参考 KY Yeung 的论文,该论文解释了这个指标并提供了一些示例。

下面的代码提供了使用这些指标的簇比较以及一些额外的基本统计信息。

from sklearn.metrics.cluster import rand_score, adjusted_rand_score
from IPython.display import display

def get_stats(labels, clusters):

    stats = []
    stats.append(f"{rand_score(labels, clusters):.3f}")
    stats.append(f"{adjusted_rand_score(labels, clusters):.3f}")
    clus_dist = pd.Series(clusters).value_counts()
    stats.append(f"{len(clus_dist):,}")
    stats.append(f"{clus_dist.mean():.3f}")
    stats.append(f"{clus_dist.min():,}")
    stats.append(f"{clus_dist.max():,}")

    return stats

def compare_clusters(cluster_list, cluster_names, labels):

    stats_dict = {}
    for clusters, name in zip(cluster_list, cluster_names):
        stats = get_stats(labels, clusters)
        stats_dict[name] = stats

    display(
        pd.DataFrame(
            stats_dict,
            index=[
                "Rand Index",
                "Adjusted Rand Index",
                "Cluster Count",
                "Mean Cluster Size",
                "Min Cluster Size",
                "Max Cluster Size",
            ],
        )
    )

cluster_list = [cc_clusters, ct_clusters, mc_clusters]
cluster_names = ["Connected Components", "Center", "Merge-Center"]
compare_clusters(cluster_list, cluster_names, df.CID)

下面是输出结果。

从表中可以看出,连接组件生成的簇较大且簇数最少,而连接组件与 Merge-Center 簇之间的差距最小。相反,Center 簇生成的簇较小且数量最多。请注意,所有三个簇的 Rand Index 都是完美的,因为它们有大量的簇,使得簇间对形成主导地位(即即使是随机簇也会得到相当的 Rand Index)。然而,如果你查看调整后的 Rand Index,Merge-Center 聚类表现最佳,其与连接组件的差异很小。

这就是我们对实体解析框架的探索。你如何处理创建的簇取决于你的具体业务需求或使用案例。如果你的目标是为每个簇建立规范的表示,你可以通过提取每个簇内每个字段的最具代表性的值(如最频繁的值)来实现。

如果你感兴趣,完整的代码可以在下面的 Google Colab 和 GitHub 仓库中找到。

## Google Colaboratory

实体解析

colab.research.google.com ## entity-resolution/entity_resolution_implementations.ipynb

实体解析

github.com

参考文献

[1] Christophides 等人,大数据端到端实体解析:综述(2019)

2 Papadakis 等人,实体解析的近似阻塞技术比较分析(2016)

3 Papadakis 等人,实体解析的阻塞和过滤技术综述(2020)

4 Hassanzadeh 等人,用于重复检测的聚类算法评估框架(2009)

5 Haveliwala 等人,用于网页聚类的可扩展技术(2000)

6 Hassanzadeh & Miller,从重复数据中创建概率数据库(2009)

熵和基尼指数简介

原文:towardsdatascience.com/entropy-and-gini-index-c04b7452efbe

理解这些度量如何帮助我们量化数据集中的不确定性

Gurjinder KaurTowards Data Science Gurjinder Kaur

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

--

你能告诉哪些是最纯净和最不纯净的购物车吗?(来源:作者提供的图像)

基尼指数是重要的机器学习概念,特别有助于决策树算法来确定分裂的质量。这两种度量虽然计算方式不同,但最终用于量化数据集中相同的事物,即不确定性(或不纯度)。

熵(或基尼指数)越高,数据越随机(混合)。

让我们直观地理解数据集中的不纯度,并了解这些度量如何帮助测量它。(不纯度、不确定性、随机性、异质性——这些都可以在我们的背景下互换使用,目标是最终减少它们以获得更好的清晰度)。

什么是不纯度——用例子解释

想象一下你和你的朋友——爱丽丝鲍勃一起去超市买水果。你们每个人都拿了一个购物车,因为你们都不喜欢分享水果。让我们看看你们都买了些什么(看起来你们很喜欢苹果!!):

作者提供的图像

这三辆购物车可以看作是三种不同的数据分布。如果我们假设最初有两个类别(苹果和香蕉),那么以下的解释将是不正确的。相反,应该将每辆购物车看作是不同的数据分布——第一个购物车的数据分布中所有数据点都属于同一类别,而第二和第三个购物车的数据分布中包含两个类别。

看看上面的例子,很容易识别出数据分布最纯净或最不纯净的购物车(类别分布)。但是,为了在数据集中进行数学化的纯度量化,以便算法可以利用它来做出决策,熵和基尼指数就显得尤为重要。

这两种度量都考虑了数据集中每个类别出现(或存在)的概率。在我们的例子中,每种情况都有 8 个数据点(水果),因此我们可以计算每个购物车的类别概率如下:

图片由作者提供

现在我们已经掌握了所有需要的知识,可以深入探讨熵和基尼指数的正式定义了!

如前所述,熵和基尼指数都是衡量数据中不确定性或随机性的度量。虽然它们旨在量化相同的基本概念,但各自有自己的数学公式和解释方法来实现这一点。

给定一个标签数据集,其中每个标签来自n个类别,我们可以按如下方式计算熵。这里,pi 是从类别 i 中随机选择一个元素的概率。

为了确定决策树中的最佳划分,熵用于计算信息增益,并选择在节点处贡献最大信息增益的特征。

基尼指数

基尼指数试图通过回答这个问题来量化数据集中的随机性——从给定的数据中随机选择一个元素的错误标记概率是多少?

给定一个标签数据集,其中每个标签来自n个类别,计算基尼指数的公式如下。这里,pi 是从类别 i 中随机选择一个元素的概率。

这个公式通常也可以重新表述为:

(注意:所有类别概率的总和为 1)。

基尼指数是信息增益的一种替代方法,可以用于决策树中确定划分的质量。在给定节点上,它比较划分前数据的基尼指数划分后两个分支基尼指数的加权和之间的差异,并选择差异最大(或基尼增益)的那个。如果这还不清楚,暂时不用担心,因为这需要更多的背景信息,本文的目标是让你对这些度量的基本含义有一个初步的了解。

回到我们的例子

为了更容易理解,以我们的购物车例子为参考,我们有三个数据集——C1、C2 和 C3,每个数据集有 8 条记录,标签来自两个类别——[苹果,香蕉]。使用上表中计算的概率,我们来逐步计算 Alice 的购物车的两个指标:

同样,我们也可以计算 C1 和 C3 的这些指标,并得到以下结果:

图片由作者提供

从上表中,我们可以得出一些有趣的结论关于熵和基尼指数可能的值范围。我们将最低可能值称为 下界,将最大可能值称为 上界

下界

当我们的数据完全同质时,熵和基尼指数的下界都是 0。可以参考 cart C1。

上界

当数据具有最高的不确定性时,熵和基尼指数分别为 1 和 0.5(可以参考 cart C2,C2 代表了具有最高可能随机性的例子)。

需要注意的一点是,这些上界值仅适用于二分类(因为这就是我们的两类苹果-香蕉示例所代表的情况)。在 n 个类别且每个类别的概率相等的情况下,上界将是 n 的一个函数,如下所示。

熵的上界:

  • 对于二分类,熵的上界是 1。

  • 对于每个类别具有相同概率的多分类,熵的上界将是:

基尼指数的上界:

  • 对于二分类, 基尼指数的上界通常不超过 0.5

  • 对于每个类别具有相同概率的多分类,基尼指数的上界将是:

回顾

  • 熵和基尼指数用于量化数据集中的随机性,并且对于确定决策树中分裂的质量非常重要。我们可以在这里交替使用随机性、不确定性、杂质和异质性这些术语。

  • 熵和基尼指数的高值意味着数据中的随机性较高。

熵旨在量化数据集的不可预测性。

  • 计算熵的公式如下。这里 pi 是从标记为 i 的类别中选择一个元素的概率,给定 n 个总类别。

  • 如果数据由属于单一类别的元素组成,它变得高度可预测,因此熵将是最小的。而熵的最小值是 0。

  • 当数据由属于 n 个类别的元素组成且每个类别的概率相等,即每个类别的概率 = 1/n 时,熵将达到最大值。

  • 对于二分类(即,具有两个类别的数据),熵的值不会超过 1。

  • 对于多分类,熵的最大值可以概括为 log(n)。 (这里的 log 以 2 为底。)

基尼指数

基尼指数旨在量化从数据中随机选择一个元素并错误标记的概率。

  • 公式如下:

  • 如果数据由属于单一类别的元素组成,则随机选择一个元素被错误标记的概率将为零,因此在这种情况下基尼指数将是最小的。所以,基尼指数的最小可能值也是 0。

  • 当数据由属于 n 个类别且分布均衡的元素组成,即每个类别的概率为 1/n 时,基尼指数将达到最大值。

  • 对于二分类(即数据包含两个不同的类别),基尼指数的最大值永远不会超过 0.5。

  • 对于多分类,基尼指数的最大值可以概括为 1-(1/n)。

基于熵的不确定性预测

原文:towardsdatascience.com/entropy-based-uncertainty-prediction-812cca769d7a

本文探讨了如何将熵作为工具用于图像分割任务中的不确定性估计。我们将介绍熵是什么,以及如何使用 Python 实现它。

François PorcherTowards Data Science François Porcher

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

--

图片由Michael Dziedzic提供,来源于Unsplash

在剑桥大学担任神经成像与人工智能研究科学家的过程中,我面临了使用最新深度学习技术对复杂脑部数据集进行图像分割的挑战,尤其是nnU-Net。在这个过程中,我观察到一个显著的漏洞:不确定性估计被忽视。然而,不确定性对可靠的决策制定至关重要

在深入具体内容之前,请随时查看我的Github 仓库,其中包含了本文讨论的所有代码片段。

不确定性在图像分割中的重要性

在计算机视觉和机器学习的领域中,图像分割是一个核心问题。无论是在医疗成像、自动驾驶汽车还是机器人技术中,准确的分割对于有效的决策制定至关重要。然而,一个常被忽视的方面是与这些分割相关的不确定性度量

为什么我们需要关注图像分割中的不确定性?

在许多实际应用中,不正确的分割可能导致严重后果。例如,如果自动驾驶汽车误识别了一个物体,或医疗成像系统错误地标记了肿瘤,后果可能是灾难性的。不确定性估计为我们提供了一个衡量模型对其预测的“确信程度”的指标,从而有助于做出更明智的决策。

我们还可以使用熵作为不确定性的度量来改善神经网络的学习。这个领域被称为“主动学习”。这个想法将在后续文章中进一步探讨,但主要思路是识别模型最不确定的区域,以便集中精力在这些区域。例如,我们可能有一个 CNN 用于脑部医学图像分割,但在肿瘤患者上的表现非常差。然后,我们可以集中精力获取更多这类标签。

了解熵

熵是一个从热力学和信息理论中借用的概念,用于量化系统中的不确定性或随机性。在机器学习的背景下,熵可以用来衡量模型预测的不确定性

从数学上讲,对于一个具有概率质量函数 P(x) 的离散随机变量 X,熵 H(X) 被定义为:

或在连续情况下:

熵越高,不确定性越大,反之亦然。

一个经典的例子来充分理解这一概念:

情况 1:一个有偏的硬币

JizhidexiaohailangUnsplash 提供的照片

想象一个有偏的硬币,它落在正面上的概率是 p=0.9,落在反面上的概率是 1-p=0.1

它的熵是

情况 2:平衡硬币

现在让我们想象一个平衡硬币,它落在正面和反面上的概率是 p=0.5

它的熵是:

熵更大,这与我们之前说的一致:更多的不确定性 = 更多的熵。

实际上有趣的是,p=0.5 对应于最大熵:

熵可视化,作者提供的图像

从直观上讲,记住均匀分布是具有最大熵的情况。如果每个结果的概率相同,则对应于最大的不确定性。

在图像分割中实现熵

将其与图像分割联系起来,考虑到在深度学习中,最终的 softmax 层通常为每个像素提供类别概率。可以根据这些 softmax 输出轻松计算每个像素的熵。

但它是如何工作的?

当模型对某个像素属于特定类别有信心时,softmax 层会为该类别显示高概率(1),而对其他类别显示非常小的概率(0)。

Softmax 层,确定案例,作者提供的图像

相反,当模型不确定时,softmax 输出在多个类别之间更加均匀分布。

Softmax 层,不确定案例,作者提供的图像

概率分布更加扩散,接近均匀情况,因为模型无法确定哪个类别与该像素相关。

如果你已经坚持到现在,非常好!你应该对熵的工作原理有了很好的直觉。

案例研究:医学影像

让我们用一个实际的例子来说明,使用医学影像,特别是胎儿的 T1 大脑扫描。所有与此案例研究相关的代码和图像都可以在我的 Github 仓库 中找到。

1. 使用 Python 计算熵

如前所述,我们正在处理由我们的神经网络生成的 softmax 输出张量。这种方法是无模型的,它仅使用每个类别的概率。

让我们澄清一下我们正在处理的张量维度中的一个重要问题。

如果你正在处理 2D 图像,那么你的 softmax 层的形状应该是:

这意味着对于每个像素(或体素),我们有一个大小为 Classes 的向量,这个向量给出像素属于我们所有类别中的每一类的概率。

因此,熵应该沿着第一个维度进行计算:

 def compute_entropy_4D(tensor):
    """
    Compute the entropy on a 4D tensor with shape (number_of_classes, 256, 256, 256).

    Parameters:
        tensor (np.ndarray): 4D tensor of shape (number_of_classes, 256, 256, 256)

    Returns:
        np.ndarray: 3D tensor of shape (256, 256, 256) with entropy values for each pixel.
    """

    # First, normalize the tensor along the class axis so that it represents probabilities
    sum_tensor = np.sum(tensor, axis=0, keepdims=True)
    tensor_normalized = tensor / sum_tensor

    # Calculate entropy
    entropy_elements = -tensor_normalized * np.log2(tensor_normalized + 1e-12)  # Added a small value to avoid log(0)
    entropy = np.sum(entropy_elements, axis=0)

    entropy = np.transpose(entropy, (2,1,0))

    total_entropy = np.sum(entropy)

    return entropy, total_entropy

2. 可视化基于熵的不确定性

现在让我们通过在每个图像分割切片上使用热图来可视化不确定性。

T1 扫描(左),分割(中),熵(右),作者图像

让我们看另一个例子:

T1 扫描(左),分割(中),熵(右),作者图像

结果非常好!确实可以看到,这与预期一致,因为高熵区域位于形状的轮廓上。这是正常的,因为模型对于每个区域中间的点并不真正怀疑,而是轮廓或边界难以确定。

做出明智的决策

这种不确定性可以以多种不同的方式使用:

  1. 随着医学专家越来越多地将 AI 作为工具,了解模型的不确定性至关重要。这意味着医学专家可以将更多的时间花在需要更精细关注的区域。

2. 在 主动学习半监督学习 的背景下,我们可以利用基于熵的不确定性来关注具有最大不确定性的例子,从而提高学习效率(更多相关内容将在即将发布的文章中讨论)。

主要收获

  • 熵是一个非常强大的概念,用于测量系统的随机性或不确定性。

  • 可以在图像分割中利用熵。这种方法是无模型的,仅使用 softmax 输出张量。

  • 不确定性估计往往被忽视,但它至关重要。优秀的数据科学家知道如何建立好的模型。杰出的数据科学家知道模型在哪些方面失败,并利用这些信息来改进学习。

你喜欢这篇文章并想了解更多吗?查看一下:

[## FrancoisPorcher - 概述

剑桥大学人工智能与神经科学的访问研究员。毕业于加州大学伯克利分校…

github.com

感谢阅读!

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

参考文献

  1. Bai, W., Oktay, O., Sinclair, M., Suzuki, H., Rajchl, M., Tarroni, G., Glocker, B., King, A., Matthews, P. M., & Rueckert, D. (2017). 基于网络的心脏 MR 图像分割的半监督学习。第 20 届医学图像计算与计算机辅助干预国际会议(MICCAI 2017)论文集(第 253–260 页)。Springer Verlag。 doi.org/10.1007/978-3-319-66185-8_29

  2. Ta, K., Ahn, S. S., Stendahl, J. C., Sinusas, A. J., & Duncan, J. S. (2020). 一种半监督联合网络,用于 4D 超声心动图中的左心室运动跟踪和分割。医学图像计算与计算机辅助干预:MICCAI 国际会议,12266(十月),468–477。 doi.org/10.1007/978-3-030-59725-2_45

  3. Yu, L., Wang, S., Li, X., Fu, C-W., & Heng, P-A. (2019). 不确定性感知自集成模型用于半监督的 3D 左心房分割。arXivarxiv.org/abs/1907.07034

熵正则化强化学习解释

原文:towardsdatascience.com/entropy-regularized-reinforcement-learning-explained-2ba959c92aad

通过为算法添加熵奖励来学习更可靠、稳健和可迁移的策略

Wouter van Heeswijk, PhDTowards Data Science Wouter van Heeswijk, PhD

·发布于 Towards Data Science ·8 min read·2023 年 10 月 26 日

--

图片由 Jeremy Thomas 提供,来源于 Unsplash

是与无序、随机或不确定状态相关的概念。它可以被视为随机变量的信息度量。传统上,它与热力学等领域相关,但这一术语也被引入了许多其他领域。

1948 年,克劳德·香农在信息论中引入了熵的概念。在这种背景下,如果一个事件发生的概率较低,它被认为提供了更多的信息;事件的信息与其发生的概率成反比。直观地说:我们从稀有事件中学到的更多。

熵的概念可以被形式化为以下内容:

对一组事件 x 的熵定义。每个事件的信息与其发生的概率成反比。

在强化学习(RL)中,熵的概念也被应用,目的是鼓励探索。在这种背景下,熵是由随机策略返回的动作的可预测性度量

具体来说,RL 将策略的熵(即,动作的概率分布)作为奖励的一个组成部分来加以奖励。这篇文章讨论了基本情况,但熵奖励是许多最先进的 RL 算法的一个重要组成部分。

什么是熵?

首先,让我们对熵的概念建立一些直觉。下图显示了低熵和高熵策略。低熵策略几乎是确定的;我们几乎总是选择相同的行动。在高熵策略中,我们选择的行动具有更多的随机性。

低熵策略(左)和高熵策略(右)的示例。在高熵策略中,行动选择中有更多的随机性。[作者提供的图片]

接下来,我们考虑硬币翻转的熵。

香农的熵利用以 2 为底的对数函数(在 Numpy 中为 np.log2),相应的测量单位称为比特。其他形式的熵使用不同的底数。这些区别对于掌握主要思想并不是特别重要。

如果硬币是加重的会发生什么?下图显示了熵将会减少,因为更有可能确定某一结果是否会发生

硬币翻转的熵,随着头和尾的概率变化,以比特为单位测量。当硬币翻转的结果最不确定时,熵达到峰值。[图片来自维基百科]

现在让我们计算一个公平骰子的熵:

注意,骰子的熵(2.58 bit)高于公平硬币(1 bit)。虽然两者都有均匀的结果概率,但骰子的个别概率较低。

现在,考虑一个加重骰子的概率,例如,[3/12, 1/12, 2/12, 2/12, 2/12, 2/12]。相应的熵为 2.52,这反映了结果现在略微更可预测(我们更可能看到 1 点而不太可能看到 2 点)。最后,考虑一个更重的骰子,概率为[7/12, 1/12, 1/12, 1/12, 1/12, 1/12]。此时,我们得到的熵为 1.95。结果的可预测性进一步提高,从熵的减少可以看出。

了解熵的概念后,让我们看看如何在强化学习中利用它。

熵正则化强化学习

我们在强化学习的背景下定义熵,其中行动概率源自随机策略π(⋅|s)。

在强化学习中的熵奖励,通过对所有行动概率及其对数的乘积求和来计算

我们将策略的熵用作熵奖励,将其添加到我们的奖励函数中。请注意,我们对每一个时间步骤都这样做,这意味着当前的行动也为最大化未来的熵做好了准备:

熵正则化强化学习。熵奖励——由α加权——被添加到我们寻求最大化的奖励中

这乍看起来可能有些违反直觉。毕竟,强化学习旨在优化决策过程,这涉及到常规地选择好的决策而非坏的决策。为什么我们要以鼓励最大化熵的方式来改变我们的奖励信号呢?这是引入最大熵原则的一个好时机:

[策略]最能代表关于系统[环境]当前知识状态的概率分布,是在精确陈述的先验数据[观察奖励]的背景下具有最大熵的分布” — 维基百科

如果策略的熵很大(例如,在初始化后的短时间内),我们对不同动作的影响了解得不多。高熵策略反映了我们尚未充分探索环境,还需要从各种动作中观察更多的奖励。

当然,最终我们确实希望采取好的行动,而不是无休止地探索,这意味着我们必须决定对熵奖励的重视程度。这是通过熵正则化系数α来实现的,这是一个可调参数。为了实际应用,权重熵奖励αH(π(⋅|s)可以简单地视为一种鼓励探索的奖励组件。

请注意,熵奖励总是计算在整个动作空间上,因此在评估每个动作时,我们都会添加相同的奖励。在典型的随机策略方法中,动作概率与它们的期望奖励成正比,包括熵奖励(例如,通过对它们应用 softmax 函数)。因此,如果熵相对于奖励非常大,则动作概率或多或少相等。如果熵非常小,则奖励在定义动作概率上占主导地位。

Python 实现

现在开始实现熵正则化的强化学习。为了本文的目的,我们在多臂老丨虎丨机背景下使用基本的离散策略梯度算法,但它可以很容易地扩展到更复杂的环境和算法。

记住,策略梯度算法具有内置的探索机制——对固有确定性策略(例如,Q-learning)应用的熵奖励具有更明显的效果。

引入熵正则化是相当直接的。我们只需将熵奖励添加到奖励中——因此它会被纳入损失函数——然后按常规步骤进行。根据算法和问题设置,您可能会在文献和代码库中遇到许多变体,但核心思想保持不变。下面的代码片段(离散策略梯度的 TensorFlow 实现)演示了它是如何工作的。

部分熵正则化强化学习的代码,在这种情况下扩展了离散策略梯度算法。

考虑一组老丨虎丨机,其平均奖励为μ=[1.00,0.80,0.90,0.98],所有机器的标准差σ=0.01。显然,最佳策略是以概率 1.0 玩机器#1。然而,如果没有足够的探索,很容易将机器#4 误认为是最佳机器。

为了说明这个想法,让我们首先看看没有熵奖励(α=0)的算法行为。我们绘制了玩机器#1 的概率。尽管每台机器开始时的概率相等,但算法相当快地识别出机器#1 提供了最高的期望奖励,并开始以更高的概率玩它。

在这种情况下,没有熵正则化的算法收敛于主要玩最佳的机器#1。左图:每次试验玩机器#1 的概率。右图:10k 次试验后每台机器的概率[作者图片]

然而,这可能会有很大的不同……下图显示了使用相同算法的一个运行,只是这次它错误地收敛到次优的机器#4。

在这种情况下,没有熵正则化的算法收敛于主要玩次优的机器#4。左图:每次试验玩机器#1 的概率。右图:10k 次试验后每台机器的概率[作者图片]

现在,我们设置α=1。这产生了相对于奖励而言较大的熵奖励。尽管玩机器#1 的概率逐渐增加,但正则化组件继续鼓励强烈的探索,即使在 10k 次迭代后。

通过熵正则化,我们看到即使在 10k 次试验后,算法仍然进行了大量探索,虽然逐渐认识到机器#1 提供了更好的奖励。左图:每次试验玩机器#1 的概率。右图:10k 次试验后每台机器的概率[作者图片]

显然,在实际应用中,我们不知道真正的最佳解决方案,也不知道所需的探索量。通常,你会遇到α=0.001附近的值,但你可以想象,探索与利用之间的理想平衡强烈依赖于问题。因此,通常需要一些试验和错误以找到合适的熵奖励。系数α也可能是动态的,可能通过预定的衰减方案或者作为一个学习权重本身。

强化学习中的应用

熵正则化的原则可以应用于几乎任何 RL 算法。例如,你可以将熵奖励添加到 Q 值中,并通过 softmax 层将结果转换为动作概率(soft Q-learning)。最先进的算法,如近端策略优化(PPO)或软演员评论家(SAC),通常包括熵奖励,实证研究表明这通常能提升性能。具体来说,它提供了以下三种好处:

I. 更好的解决方案质量

正如前面详细说明的,熵奖金鼓励探索。特别是在处理稀疏奖励时,这非常有用,因为我们很少收到关于动作的反馈,并且可能错误地重复那些过高估计奖励的次优动作。

II. 更好的鲁棒性

由于熵奖金鼓励更多的探索,我们也会更频繁地遇到稀有或偏离的状态-动作对。因为我们遇到了更丰富和更多样化的经验,我们学习到的策略能够更好地处理各种情况。这种增加的鲁棒性提升了策略的质量。

III. 促进迁移学习

增加探索也有助于将学习到的策略适应于新任务和环境。更多的多样化经验能够更好地适应新情况,因为我们已经从类似的情境中学到了东西。因此,熵正则化在迁移学习中通常很有用,它使得在应对变化环境时重新训练或更新学习策略变得容易。

TL;DR

  • 熵奖金鼓励对动作空间的探索,旨在避免过早收敛

  • 奖励(利用)和奖金(探索)之间的平衡是通过一个需要微调的系数来控制的。

  • 熵奖金在现代 RL 算法中如 PPO 和 SAC 中被广泛使用。

  • 探索提高了质量、鲁棒性和对新实例变体的适应能力。

  • 熵正则化在处理稀疏奖励时特别有用,当鲁棒性很重要和/或策略应该适用于相关问题设置时。

如果你对 RL 中的熵正则化感兴趣,你可能还想查看以下文章:

## TensorFlow 2.0 中离散策略梯度的最小工作示例

用于训练离散演员网络的多臂老丨虎丨机示例。借助 GradientTape 功能,…

towardsdatascience.com ## 强化学习中的策略梯度解释

了解基于似然比的策略梯度算法(REINFORCE):直觉、推导、…

towardsdatascience.com [## 近端策略优化(PPO)解释

从 REINFORCE 到连续控制中的首选算法的历程

towardsdatascience.com

进一步阅读

Ahmed, Z., Le Roux, N., Norouzi, M., & Schuurmans, D. (2019). 理解熵对策略优化的影响 国际机器学习会议。

Eysenbach, B. & Levine, S. (2022). 最大熵强化学习(可证明地)解决一些鲁棒强化学习问题 国际学习表征会议。

Haarnoja, T., Tang, H., Abbeel, P., & Levine, S. (2017). 使用深度能量基策略的强化学习 国际机器学习会议。

Reddy, A. (2021). 最大熵如何使强化学习更鲁棒 伯克利机器学习。

Schulman, J., Chen, X., & Abbeel, P. (2017). 策略梯度与软 Q 学习的等价性 arXiv 预印本 arXiv:1704.06440

Tang, H. & Haarnoja, T. (2017). 通过最大熵深度强化学习学习多样化技能 伯克利人工智能研究所。

Yu, H., Zhang, H., & Xu, W. (2022). 你实际需要熵奖励吗? arXiv 预印本 arXiv:2201.12434

环境数据科学:介绍

原文:towardsdatascience.com/environmental-data-science-an-introduction-127b4b3422dc

处理环境数据的示例、挑战和观点

Caroline ArnoldTowards Data Science Caroline Arnold

·发布于Towards Data Science ·7 min read·2023 年 9 月 5 日

--

图片由Francesco Gallarotti提供,来源于Unsplash

人类生活与环境深度交织。在当前的地质时期,人类世中,我们通过温室气体和化学产品的释放、广泛的基础设施建设和农业活动来塑造环境。

对于数据科学家来说,处理一个主题的自然方式是查看可用的数据及其潜力。环境数据科学领域相对较新,但正在日益受到关注。

气候变化的表现、生物多样性的丧失以及污染的增加,甚至到达深海,已经提高了我们对环境的敏感性。今天,可持续性是政治和非政府活动的主要焦点,我们必须紧急解决如何在保障生计的同时保护环境的问题。

气候变化人工智能倡议 正在与主要的机器学习会议合作,已经推出了一个环境数据科学的开源期刊,并且在环境研究和数据科学交叉领域建立了许多研究生项目,例如在帝国理工学院

据我了解,目前还没有明确的环境数据科学定义。在这篇博文中,我将分享我在环境数据科学方面的经验,基于我作为 AI 顾问在该领域的工作经验。首先,我将通过三个例子展示环境数据科学的多样性:

  1. 生物圈监测(分类)

  2. 空气污染预测(时间序列)

  3. 洪水损害驱动因素(特征重要性)

我将讨论与环境数据相关的挑战,涉及数据稀缺、质量和复杂性。环境数据不同于机器学习中遇到的其他领域的数据,我将提供我对如何应对这些挑战的看法。

最后,我将概述如果我们能够利用环境数据,并结合数据科学和机器学习的力量与对可持续解决方案的日益增长需求,我所看到的前景。

使用图像分类监测野生动物

由机器学习算法在照片中识别出的物种。原始图像:作者 GIRAUD Patrick — 自己的作品,CC BY 2.5,commons.wikimedia.org/w/index.php?curid=1093844。注释:作者。

随着人类活动扩展到偏远地区,许多动物和植物物种面临灭绝威胁。野生动物保护工作依赖于对各种感兴趣物种的准确监测。在Tuia 等人,Nature 2022中,作者列出了非侵入性数据收集设备,如“相机陷阱、消费者相机、声学陷阱[… 和]动物设备”。

收集的数据可以使用机器学习算法高效分析。例如,水坑附近的相机陷阱在动物经过时会捕捉图像。这些图像可以使用计算机视觉算法,如卷积神经网络进行分类。你可能遇到过猫与狗分类教程,这些教程可以适应于分类任何其他动物物种,只要有足够的标记数据。

预测空气质量时间序列

工业活动、运输和个人交通影响气溶胶的释放到大气中。气溶胶是轻质颗粒,可能对肺部和植物造成损害。它们的扩散不仅取决于释放的初始浓度,还取决于天气。在没有风的炎热日子里,气溶胶将比有轻风的日子停留在地面上更久。

许多城市已实施气溶胶监测系统,固定位置的站点全天测量气溶胶浓度。结果是一个时间序列,如下图所示,来自英国布里斯托尔市:

布里斯托尔三站点的 NOx 浓度数据(2022 年)。数据来源:data.opendatasoft.com/explore/dataset/air-quality-data-continuous%40bristol,布里斯托尔市政府,开放政府许可证。绘图:作者。

起初,这些数据将用于监测目的,例如检查是否达到了某些空气质量目标。但我们也可以使用这些数据进行分析,以识别恶劣气溶胶条件的驱动因素。类似于预测股票价格,我们可以使用机器学习生成空气质量预报,以帮助减轻气溶胶的影响。

示例 3:水文学—洪水损害监测

Jonathan Ford拍摄于Unsplash

洪水是最昂贵的自然灾害。2022 年巴基斯坦的大规模洪水事件造成 1700 人遇难,超过 800 万人流离失所,损失估计达到 150 亿美元[source]。估计有 18.1 亿人生活在可能受到洪水影响的地区[source]。

随着气候变暖和土地利用变化导致洪水事件的强度和频率增加,急需了解洪水损害的驱动因素。HOWAS 数据库收集了洪水事件的定性和定量数据,如建筑物与水源的距离、警报提前时间和建筑特征。目前,它正从最初关注德国和奥地利的数据扩展到全球数据。

Kellermann 等,《对象特定的洪水损害数据库 HOWAS 21》,自然灾害与地球系统科学(2021)中,使用了随机森林回归算法来预测总建筑损害。各种输入特征根据其对算法的重要性进行排名。像这样的高质量数据集有助于改善洪水预警和减轻损害。

通过特征重要性排名获得的洪水损害驱动因素。致谢:Kellermann 等,doi.org/10.5194/nhess-20-2503-2020

挑战

根据我在这一领域作为研究数据科学家的经验,我看到在使用环境数据方面有四个主要挑战。

数据稀缺

收集环境数据可能是一项繁琐的任务。为了获取数据,你实际上需要亲自前往一个地点,设置传感器,或手动采集样本。相比于在网上商店中客户数据通过互联网流入数据库的情况,获取单个样本的成本要高得多。一些地区的难以进入性可能使得获取具有足够空间和时间分辨率的数据变得困难。

通常,环境数据集由几百个样本组成,这些样本的获取成本很高。数据分析和算法必须经过精心选择,并根据需要进行调整。

专业数据格式

出于历史原因或方便,环境数据通常以机器学习算法和数据科学管道不容易识别的格式存在。根据我培训环境数据科学家的经验,受训者通常难以将标准机器学习教程中的概念转换为环境数据。

许多机器学习例程依赖于像标准仓库数据一样的数据,这些数据以 csv 或 json 文件形式提供,或以标准图像格式存在。环境数据通常由少数人收集,并且收集协议未考虑数据科学下游应用。因此,数据工程和清理通常是一项重要任务。

数据质量

即使数据量充足,数据质量也可能会有显著波动。例如,环境的摄影观察依赖于光线和相机角度,图像之间可能存在很大差异。传感器可能会失效或质量下降,像 HOWAS 这样的定性数据库维护成本高昂。

领域知识

处理环境数据、理解布局和提出正确问题的领域知识至关重要。环境数据的标注通常需要大量的专业知识。因此,跨学科项目是常态,其中数据科学家和机器学习专家与领域专家紧密合作。

视角

尽管处理环境数据面临诸多挑战,但我相信环境数据科学具有巨大的潜力。可持续性是当今政治和非政府组织讨论的主要焦点,良好的数据可以帮助做出明智的决策。企业正在根据实时数据制定战略,如果在可持续性讨论中看到数据类似的影响将会非常好。

有志于成为环境数据科学家的人员可能会发现,在可持续发展领域工作是有成效和令人满意的。在与自然资源保护、环境监测和可持续政策实施相关的项目中,他们发现比开发第百万个在线营销或加密货币预测算法更具目的性。

我希望未来环境数据科学能成为数据科学和机器学习的一个成熟领域。培养能在环境研究和编程交汇处工作的工程师和科学家是提升该领域成熟度的第一步。随着高质量数据的增加和对可持续性兴趣的增长,我相信环境数据科学在未来将会变得重要。

Marc Schulte 提供的照片,来源于 Unsplash

摘要

总结一下这次关于环境数据科学的介绍,我提供了三个例子来说明这个领域的多样性。处理环境数据面临独特的挑战,主要与数据稀缺性和数据质量相关。

与领域专家和数据科学家进行的跨学科项目可以利用未分析的环境数据。我希望未来环境数据科学能达到一个成熟的阶段,使环境数据直接指导决策并提升可持续性。

进一步阅读

普及生成式 AI 的环境影响

原文:towardsdatascience.com/environmental-impact-of-ubiquitous-generative-ai-9e061bac6800

如果数十亿人开始每天使用生成式 AI 技术,会对环境产生什么影响?

Kasper Groes Albin LudvigsenTowards Data Science Kasper Groes Albin Ludvigsen

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

--

由 Johannes Plenio 拍摄,来源于 Unsplash

本文探讨了这样一个问题:大规模采用类似 ChatGPT 的生成式 AI 会对环境产生什么影响?也就是说,如果数十亿人开始每天广泛使用生成式 AI,那么环境影响可能是什么?

这个问题值得思考的原因在于,我们可以通过其答案来决定我们对这项新技术快速普及的担忧程度。

随着 AI 模型越来越大[1],并且像 OpenAI 和 Google 等公司使其广泛可用,AI 模型的环境影响——例如碳足迹和水足迹——已经成为学术界(如23)以及主流媒体(如45)讨论和研究的主题。

据报道,ChatGPT 的用户数量达到数亿——甚至可能是数十亿6——而 Google 将生成式 AI 嵌入到多个产品中[7],因此生成式 AI 可以说是当前最广泛采用的 AI 类型。结合生成式 AI 模型如 GPT-4 的巨大规模——传闻其规模是前代模型的近 6 倍[8]——生成式 AI 可能也是未来可预见的环境影响最大的 AI 类型。

本文是一个思想实验,考虑了大规模采用生成式 AI 可能带来的环境影响。它会导致环境灾难,还是只是一滴水中海洋,或者介于两者之间?本文的目的是为开始揭示这个问题提供基础。

在本文中做出的估算涉及许多假设,如果你想使用自己的假设进行实验,可以在 这个电子表格 中进行。

如果你想全面了解我关于 AI 环境影响的故事,请通过下面的链接成为 Medium 会员:

[## 通过我的推荐链接加入 Medium - Kasper Groes Albin Ludvigsen

作为 Medium 会员,你的会员费的一部分会分配给你阅读的作者,你将能完全访问每一篇故事……

kaspergroesludvigsen.medium.com

AI 系统生命周期的阶段

尽管本文并未分析特定模型,但区分 AI 模型生命周期的不同阶段仍具有参考价值。我们可以将 AI 模型的生命周期视为由 6 个不同阶段组成 [9]:

  1. 原材料开采

  2. 材料制造

  3. 硬件制造

  4. 模型训练

  5. 模型部署

  6. 生命周期结束

在本文中,我将重点关注硬件制造(第 3 阶段)、模型训练(第 4 阶段)和模型部署(第 5 阶段),因此下面将简要描述这些阶段。

硬件制造是指制造 AI 模型运行所需硬件的环境影响。模型训练是模型开发的阶段。模型部署是将模型“部署”到“生产环境”的阶段,用户可以使用模型。这有时也称为推理阶段或生产阶段。生命周期通常被描述为线性的,尽管许多 AI 系统在其生命周期内需要对模型进行重新训练或调整。

为了估算上述三个阶段的环境影响,我们需要了解大规模采用生成式 AI 需要多少硬件。这是我们在接下来的部分将考虑的内容。

## ChatGPT 的电力消耗

ChatGPT 可能在 2023 年 1 月消耗了相当于 175,000 人的电量。

[towardsdatascience.com

大规模生成式 AI 的采用需要多少硬件?

为了评估大规模采用生成式 AI 的潜在环境影响,我们需要了解处理每日数十亿次查询所需的硬件数量。

要确定所需的硬件数量,我们需要考虑技术将有多少用户以及他们将使用多少。用户越多,需要的硬件就越多。

那么,大规模生成性 AI 采用在用户数量方面会是什么样的呢?假设 35 亿人开始每天使用 ChatGPT 或类似技术,每天进行 30 次查询。那总共是 105B 次日常请求。鉴于 ChatGPT 的惊人用户数量以及 Google 和其他公司将生成性 AI 集成到各种产品中的努力,这种假设并不不合理。

现在我们需要了解处理 100B 日常请求所需的硬件种类和数量。

Patel 和 Ahmad 曾估算处理 195,000,000 次日常 ChatGPT 请求需要大约 3,617 台 Nvidia HGX A100 服务器,其中包含 28,936 个 Nvidia A100 GPU[10]。A100 GPU 是为 AI 工作负载设计的处理硬件。我们假设这些数字是正确的,并且可以推广到其他生成性 AI 服务。我们进一步假设 GPU 的数量与日常请求的数量呈线性增长。这意味着如果 3,617 台 HGX 服务器可以处理 195,000,000 次日常请求,我们需要 538.46 倍的计算能力——即 1,947,615 台 Nvidia HGX A100 服务器,总共 15,580,923 个 A100 GPU——来处理 105B 次日常请求。

现在我们已经了解了支持大规模采用生成性 AI 所需的硬件数量,让我们来看看制造这些硬件的环境影响。

大规模生成性 AI 在硬件制造阶段的环境影响

在上一节中,我们看到大规模采用生成性 AI 技术可能需要 1,947,615 台 Nvidia HGX 服务器和 15,580,923 个 Nvidia A100 GPU。让我们来看一下制造这些硬件的环境影响。

Nvidia 尚未发布其产品的碳足迹信息,因此我们必须使用一些代理数据,这意味着我们得到的数字高度推测,因此请谨慎对待并提出挑战。

根据惠普公司自己的估算,ProLiant DL345 Gen10 Plus 服务器的体现排放为 2,500 kgCO2e[11]。这是我找到的唯一一个具有体现排放数据的相似服务器,因此我们将以此作为类比,就像 Luccioni 等人以前所做的那样[9]。

ProLiant 服务器不包含任何 GPU,所以我们需要加上 8 个 A100 GPU 的体现排放。同样,Nvidia 并未公开这些数据,但其他人已使用了每个 GPU 150 kgCO2e 的数据[9] [12]。

我们假设使用的是带有 8 个 GPU 插槽的 Nvidia HGX,因此我们需要将 8 * 150 kgCO2e 加到 2,500 kgCO2e 中。这总共是每台 Nvidia HGX A100 服务器 3,700 kgCO2e。

记住,我们需要 1,947,615 台这样的服务器来处理 105B 次日常请求。因此,满足大规模采用生成性 AI 所需的 GPU 硬件的体现排放估计为 1,947,615 * 3.7 = 7,206,177 吨 CO2e。

让我们将这些排放均匀分布在硬件的使用寿命内。我们假设硬件的使用寿命为 5 年,之后要么磨损,要么被新技术替代 [13]。

基于此,大规模采用生成式 AI 所需硬件的碳足迹估计为每年 1,441,235.4 吨 CO2e。

[## 订阅 Kasper Groes Albin Ludvigsen 的文章以获取电子邮件通知。

订阅 Kasper Groes Albin Ludvigsen 的文章以获取电子邮件通知。通过注册,你将创建一个 Medium 账户,如果你…

kaspergroesludvigsen.medium.com](https://kaspergroesludvigsen.medium.com/subscribe?source=post_page-----9e061bac6800--------------------------------)

大规模生成式 AI 采用的训练阶段的环境影响

现在,让我们考虑训练支撑生成式 AI 的 AI 模型的环境影响。

ChatGPT 的初始版本基于一个名为 GPT-3.5 的大型语言模型(LLM),它是 GPT-3 的一个版本。最新版本的 ChatGPT 可能基于 OpenAI 最新的 LLM——GPT-4,但 OpenAI 尚未发布任何关于 GPT-4 的训练成本信息。不过,我们有关于 GPT-3 训练过程中的能耗的可靠估算。这些数据来源于谷歌和加州大学伯克利分校研究人员的一篇论文,他们估算训练 GPT-3 的能耗为 1,287,000 KWh [14]。我们可以假设其他公司训练的模型在同一范围内。

要计算 1,287,000 KWh 的碳足迹,我们需要了解生产 1 KWh 电力时排放了多少碳。这被称为电力的碳强度,并在不同地区之间有所不同,因为电力来源(风能、煤炭、太阳能等)因地区而异。在这个思考实验中,我们使用谷歌、亚马逊和微软数据中心使用的电力的平均碳强度。根据 ML CO2 Impact Calculator [18]的数据,这三家云服务提供商的电力平均碳强度为 484 gCO2e/KWh。

现在,为了获得生成模型年度碳足迹的估算,我们需要知道有多少公司提供这种模型以及它们的训练频率。假设在生成式 AI 领域将有 9 个主要参与者:OpenAI/Microsoft、Google、Meta、Anthropic、Inflection、Character、腾讯、字节跳动、百度。再假设他们每年训练一个模型。这将导致每年的训练碳足迹为 6,229 吨 CO2e。

本文这一部分的假设是最具推测性的。然而,正如我们将看到的,它们对整体图景的影响不大,因为训练阶段的环境影响与硬件制造和部署阶段的环境影响相比,显得微不足道。

大规模生成性人工智能部署阶段的环境影响

现在让我们考虑生成性人工智能模型部署阶段的环境影响。换句话说,让我们看看运行 1,947,615 台 HGX 服务器所需的电力。

一种计算方法是查看服务器的所谓热设计功率(TDP)。TDP 通常用于量化一块处理硬件运行所需的电力。HGX 服务器的 TDP 未知,但类似的 Nvidia 服务器 DGX 的 TDP 为 6.5 kW [15],我们假设 HGX 服务器也适用这一数据。因此,如果服务器全功率运行一个小时,则消耗 6.5 千瓦时(kWh)。然而,在这个思维实验中,我们假设所有服务器平均运行在其 TDP 的 75%,那么每小时将消耗 4.875 KWh。共有 1,947,615 台服务器,每台每小时消耗 4.875 KWh。这总计为每小时 9,494,625 KWh,每天 227,871,000 KWh,每年 83,172,915,000 KWh。

计算数据中心级硬件的电力消耗时,通常会将硬件自身的电力消耗乘以硬件所在数据中心的所谓电力使用效率(PUE)。PUE 是用于表达数据中心能源效率的指标。数据中心在冷却等方面使用的能量相对于实际计算硬件的电力使用得越多,PUE 值越高。微软的全球 PUE 为 1.18 [16],谷歌的为 1.10 [17],所以我们使用这两个值的平均数,即 1.14。

如果我们将 HGX 服务器估算的年电力消耗 83,172,915,000 KWh 乘以 1.14,则年电力消耗为 94,817,123,100 KWh。

接下来,为了计算碳排放,我们将乘以前一节中提到的 484 克/KWh 的碳强度。基于此,我们可以估算大规模生成性人工智能部署阶段的年碳足迹为 45,891,487 吨。

普遍采用生成性人工智能的综合环境影响

现在我们已经估算了硬件制造、训练和大规模部署生成性人工智能的阶段的碳足迹,让我们将这些数据结合起来,计算大规模生成性人工智能采用的年总碳足迹。

回顾我们估算了硬件制造的年排放量为 1,441,235 吨。然后,我们估算了训练阶段的年 CO2e 排放量为 6,229 吨。最后,我们估算了大规模生成性人工智能部署阶段的碳足迹为每年 45,891,487 吨。

因此,普遍采用生成性人工智能的总碳足迹可以估算为每年 47,338,952 吨 CO2e。

下图 1 展示了模型部署阶段的碳排放量比硬件制造和模型训练阶段大得多。因此,研究应关注如何减少部署阶段的排放,而非训练阶段的排放。

图 1:按生命周期阶段估计的大规模采用生成性 AI 的年碳排放量。图表由 Kasper Groes Albin Ludvigsen 提供

将大规模采用生成性 AI 的年碳足迹放入视角中

在整篇文章中,你可能会想:“这些数字究竟意味着什么?这多还是少?”

上面,我们估计大规模采用生成性 AI 的总年碳足迹为 47,338,952 吨 CO2e,包括硬件制造、模型训练和模型部署生命周期阶段。

这相当于 4,303,541 丹麦人的年排放量。这样看来,我认为这听起来很多。另一方面,430 万人只是全球人口的微不足道的一部分。因此,让我们也将普遍生成性 AI 的碳足迹与全球年排放量进行比较。

全球年 CO2e 排放量的估计因来源不同而有所差异,但 IAE [19] 估计 2021 年全球排放了 400 亿吨 CO2e。在这 400 亿吨中,47,338,952 吨占 0.12%。换句话说,如果 35 亿人每天对生成性 AI 模型如 ChatGPT 进行 30 次查询,这篇文章估计可能会使全球 CO2e 排放量增加 0.12%。我将留给读者自己决定是否认为这很多。

普遍生成性 AI 采用的水足迹

到目前为止,我们已经分析了大规模采用生成性 AI 的潜在碳足迹。但碳排放并不是数字技术的唯一环境影响。另一个重要的考量是冷却承载大 AI 模型的数据中心的水消耗。水消耗指的是那些丧失且无法再利用的水。

Pengfei Li 等人 [20] 的一篇最新论文分析了类似 ChatGPT 的大型语言模型的水消耗。在他们的论文中,作者提出了一种估计数据中心水消耗的方法,他们估计 ChatGPT 每进行 20 到 50 次查询会消耗 500 毫升水。数据中心的水消耗主要有两种方式:

  1. 直接消耗,即水分蒸发并在数据中心硬件冷却时被冲洗掉,以及

  2. 间接水消耗,即用于生产数据中心所需电力的水。

假设 ChatGPT 的水消耗可以推广到其他人工智能服务,并且 50 个查询需要 500 毫升的水,即每个查询需要 10 毫升的水。请记住,我们假设生成性人工智能模型每天会收到 105 亿个请求。这每天需要 1,050,000,000 升水,或者每年需要 383,250,000,000 升水。相比之下,成年人每年的推荐液体摄入量为 3.2 升[21],即每年的摄入量为 1,168 升。这意味着生成性人工智能的大规模应用产生的水消耗可以满足 328,135,000 成人的年液体摄入量。

你可能会想知道为什么水的消耗会成为一个问题。主要问题是,当大量用水的数据中心被放置在受旱灾影响的地区时。这是一个问题,因为我们通常没有将水跨大距离转移所需的基础设施。Pengfei Li 等人的论文展示了这张图,显示了美国受旱灾影响的地区。这些地区有成千上万的数据中心。

图片来源 arxiv.org/pdf/2304.03271.pdf

讨论

现在让我们讨论一下上述估算的潜在影响。

我们能否用可再生能源为大规模的生成性人工智能应用提供动力?

那么我们如何减少生成性人工智能大规模应用的环境影响呢?

如何估算和减少机器学习模型的碳足迹 ## 如何估算和减少机器学习模型的碳足迹

两种简单估算机器学习模型碳足迹的方法以及减少碳足迹的 17 种方法

[towardsdatascience.com

一个浮现在脑海中的想法是用可再生能源为这些数据中心供电。让我们考虑这个想法。

接下来,我将假设对生成性人工智能的所有查询都需要额外的能源。即,我假设这些查询不会替代对其他现有服务的查询。你可能会争辩说,一些 ChatGPT 查询当前替代了传统搜索引擎查询,但考虑到生成性人工智能正在被整合到 Bing 和 Google 中,我敢说几乎所有的生成性人工智能查询都会是额外的。

为了使生成性人工智能的大规模应用具有可持续性,因此能源必须通过额外的可再生能源容量来生成——即,我们需要安装额外的可再生能源容量。请记住,普遍的生成性人工智能可能每年需要接近 95 亿千瓦时的电力。平均风力发电机每年可以产生 600 万千瓦时的电力[22]。

因此,为了用风力涡轮机生产足够的可再生能源,我们需要安装大约 15,800 台新的风力涡轮机。为了提供背景,丹麦作为风能领域的领先国家,目前有 6,286 台活跃的风力涡轮机[23]。

因此,我认为可以安全地说,要建立足够的额外可再生能源以支持普遍的生成式人工智能采用,将是一项庞大而昂贵的工作。

附带说明一下,即使是可再生能源也被认为有碳足迹,因为生产和安装例如风力涡轮机所造成的排放会分摊到能源来源的使用寿命中。因此,来自离岸风力发电的电力被认为具有 12 gCO2e/KWh 的中位碳强度[24]。因此,即使所有生成式人工智能都由风能供电,它的年碳足迹也将达到 1,114,000 吨 CO2e——大致相当于 104,000 名丹麦人。

其利益是否超过成本?

在这场辩论中另一个有趣的方面是,我们是否能通过生成式人工智能获得的生产力提升足以证明其环境影响的合理性。在一篇尚未经过同行评审的文章中(即,需持保留态度),麻省理工学院博士生 Shakked Noy 和 Whitney Zhang 展示了使用 ChatGPT 在实验环境中提高了若干任务的生产力和工作质量[25]。Noy 和 Zhang 测量了撰写新闻稿、简短报告、分析计划和微妙电子邮件等任务的生产力。质量由(盲测的)在同一职业中工作的经验丰富的专业人士评估。

生产力提升是否值得其环境成本本质上是一种价值判断,但如果我们能确认生成式人工智能是否确实使我们更具生产力,将是辩论的良好开端。因此,需要进行更多的研究,使用生成式人工智能的公司应对生产力的影响进行严肃评估。

警示事项

本文提出的估计值应被视为有根据的猜测。这首先是因为我们试图预测未来会发生什么。其次,对这些类型的人工智能模型环境影响的任何估计都受到影响,因为提供者并未披露所需的信息,这意味着我们必须做出假设。通过撰写这篇文章,我希望激励其他人挑战我的假设或提供他们自己的估计。

尽管生成式人工智能是一个涵盖可以生成文本和/或图像的人工智能产品的总称,但本文专注于生成文本的模型。因此,这里的估计并未考虑图像生成技术的采用。

本文以 ChatGPT 的原始版本(基于 GPT-3.5)作为出发点,因为 OpenAI 最新模型 GPT-4 的相关数据尚不可用。如上所述,GPT-4 比 GPT-3 更大,这可能意味着它消耗更多的能源。然而,这不一定是事实。

我假设一家公司的生成式人工智能产品的环境影响将与其他公司提供的竞争产品大致相同,但也可能有些公司提供较小或更专业化的模型。

结论

在这个思想实验中,我们探讨了如果世界上很大一部分人口开始每天使用像 ChatGPT 这样的生成式人工智能,可能对环境产生的影响。这个思想实验的目的是为读者提供一个评估问题的基础:我们是否应该担心大规模采用生成式人工智能的环境影响?

我们估计,普及的生成式人工智能每年可能消耗 95B KWh 的电力,生产这些电力可能会排放 47,338,952 吨 CO2e。这占全球 CO2e 排放量的 0.12%。换句话说,本文估计如果 35 亿人每天向生成式人工智能服务发出 30 个查询,全球 CO2e 排放量可能增加 0.12%。另一个需要考虑的环境影响是水消耗。本文估计普及的生成式人工智能每年可能消耗 383,250,000,000 升水。这相当于 328,125,000 名成人的推荐年液体摄入量。

就这些了!希望你喜欢这个故事。告诉我你的想法吧!

关注我获取更多关于人工智能和可持续发展的内容,并订阅,以便在我发布时通过电子邮件获取我的故事。

我有时还会写关于时间序列预测的文章。

也欢迎在LinkedIn上与我联系。

参考文献

[1] huggingface.co/blog/large-language-model

2 arxiv.org/abs/1907.10597

3 arxiv.org/abs/1906.02243

4 www.forbes.com/sites/bernardmarr/2023/03/22/green-intelligence-why-data-and-ai-must-become-more-sustainable/

5 www.standard.co.uk/tech/ai-chatgpt-water-usage-environment-study-b1073866.html

6 www.similarweb.com/blog/insights/ai-news/chatgpt-growth-flattens/

[7] nypost.com/2023/05/10/google-integrates-more-ai-into-products-in-battle-with-microsoft/

[8] the-decoder.com/gpt-4-has-a-trillion-parameters/

[9] arxiv.org/pdf/2211.02001.pdf

[10] www.semianalysis.com/p/the-inference-cost-of-search-disruption

[11] www.hpe.com/psnow/doc/a50005151enw

[12] medium.com/teads-engineering/building-an-aws-ec2-carbon-emissions-dataset-3f0fd76c98ac

[13] cybersided.com/how-long-do-gpus-last/

[14] arxiv.org/ftp/arxiv/papers/2204/2204.05149.pdf

[15] www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/nvidia-dgx-a100-datasheet.pdf

[16] azure.microsoft.com/en-us/blog/how-microsoft-measures-datacenter-water-and-energy-use-to-improve-azure-cloud-sustainability/

[17] www.google.com/about/datacenters/efficiency/

[18] github.com/mlco2/impact/blob/master/data/impact.csv

[19] www.iea.org/reports/co2-emissions-in-2022

[20] arxiv.org/pdf/2304.03271.pdf

[21] www.health.harvard.edu/staying-healthy/how-much-water-should-you-drink

[22] www.ewea.org/wind-energy-basics/faq/

[23] turbines.dk/

[24] en.wikipedia.org/wiki/Life-cycle_greenhouse_gas_emissions_of_energy_sources

[25] economics.mit.edu/sites/default/files/inline-files/Noy_Zhang_1.pdf

等规模谱聚类

原文:towardsdatascience.com/equal-size-spectral-clustering-cce65c6f9ba3?source=collection_archive---------2-----------------------#2023-02-06

对这种流行算法的一种修改,构建了点数平衡的簇

卡门·阿德里安娜·马丁内斯·巴博萨博士Towards Data Science 卡门·阿德里安娜·马丁内斯·巴博萨博士

·

关注 发表在 Towards Data Science ·7 min read·2023 年 2 月 6 日

--

阿姆斯特丹餐馆的等规模聚类。所有图像由作者创建。

聚类是一种用于将具有相似特征的数据点分组的方法。它广泛应用于探索性数据分析,并且在许多应用中已被证明非常重要,如模式识别、市场和客户细分、推荐系统、数据压缩和生物数据分析等。

尽管有大量的聚类算法,但没有一种能够生成点数平衡的聚类。这种等大小的聚类在某些领域非常重要,例如在最后一公里配送领域,其中大量订单可以聚合成等大小的组,以改善配送路线并最大化车辆容量利用率。

鉴于需要进行等大小的聚类,我和几位同事扩展了所谓的spectral clustering算法,以生成点数平衡的聚类。这个新算法基于数据点的地理信息构建具有相似点数的聚类。

完整代码可以在这个GitHub 仓库中找到。它旨在为数据科学社区做出贡献。如果你需要在地图上创建等大小的点聚类,可以试试!

等大小谱聚类:算法

等大小聚类由三个步骤组成:聚类初始化、计算每个聚类的邻居,以及平衡每个聚类中的点。我们来详细回顾一下每一步。

聚类初始化

在第一步中,我们通过使用Scikit-learn 的谱聚类算法实现来创建聚类。谱聚类在汇聚空间数据方面非常强大,因为它基于连接节点的边识别图中的社区。谱聚类算法特别适用于聚类遵循圆形对称性的点。如果你想了解更多关于这种方法的信息,可以阅读 William Fleshman 写的这篇精彩文章:

## 谱聚类

基础和应用

towardsdatascience.com

进行聚类初始化需要两个超参数:nclusters即所需的聚类数量,以及nneighbors即每个数据点的邻居数量。最后一个参数由谱聚类用于构建相似度矩阵。nneighbors的良好值在数据点的 7% 到 15% 之间。

步骤 1:通过谱聚类算法创建聚类。这些聚类的点数不相等。

计算每个聚类的邻居

一旦创建了簇,算法的第二步是计算每个簇的邻居。这一计算是如何进行的呢?通过估计每个数据点最近邻居的簇标签的众数。例如,如果点x属于簇A,而其大多数最近邻居属于簇B,这意味着簇B是簇A的一个邻居。

计算每个簇的邻居极其重要,因为簇的平衡是通过在邻近簇之间交换点来实现的。

步骤 2:左侧面板:估计每个簇的邻居。在这个示例中,我们可以看到簇 A 和簇 B 是簇 C 的邻居。右侧面板:通过在邻近簇之间交换点来平衡簇。

平衡每个簇上的点

算法的最后一步是平衡每个簇上的点。如上所述,我们通过在邻近簇之间交换点来实现这一点。大簇将点转移到较小的邻近簇。在平衡过程中,我们的目标是使簇的大小大致等于N/ncluster,其中N是数据点的总数。

为了平衡簇的大小,我们定义了超参数equity_fractionequity_fraction是一个定义在区间(0,1]内的数值,它限制了结果簇的平等程度。如果equity_fraction为零,则簇将保持初始大小。如果equity_fraction为一,则结果簇的大小将大致等于N/ncluster

步骤 3:最终的簇大小取决于equity_fraction。左侧:如果equity_fraction为零,簇保持初始大小。右侧:如果equity_fraction为一,簇的点数大致相同。

让我们做一个小小的插入,定义一个叫做簇分散的量。簇分散被定义为簇内点距离的标准差。你可以将其视为簇内距离的一个稍作修改的版本。

equity_fraction影响初始簇的分散,因为点的交换增加了簇内点之间的距离。在这种情况下,我建议使用优化算法来找到最优的簇超参数,以最小化簇的分散。在下一节中,我会提到如何从 Python 代码中获取簇的分散。

其他功能

重要的是要记住,相等大小的谱聚类可以用来创建空间点的聚合这个仓库提供了一个绘图功能,可以在你拥有数据点坐标的情况下使用。在下一节中,我们将看到这个功能的实际应用。

使用案例:阿姆斯特丹餐馆的聚类

假设你是荷兰的一家农场的主人,你希望将新鲜的高品质食物送到阿姆斯特丹的许多餐厅。你有 6 辆容量相同的车辆,这意味着它们能够大致送到相同数量的餐厅。

为了充分利用车辆的容量,你可以使用均等大小聚类来对餐厅进行分组,使得每辆车在不同餐厅之间的旅行距离不会过长。

首先,让我们读取餐厅的位置:

文件restaurants_in_amsterdam.csv包含了阿姆斯特丹中央车站 8 公里范围内的餐厅位置列表。你可以在GitHub 仓库datasets文件夹中找到这个文件。

coords数据框中列出的地点,可以估算出一个包含每对点之间旅行距离的矩阵。这个矩阵的形状是(n_samples, n_samples),并且它必须是对称的。这个矩阵是均等大小聚类所接收的输入。

现在我们可以运行均等大小光谱聚类。这跟调用SpectralEqualSizeClustering类一样简单:

在这个例子中,我们创建了 6 个簇。我们选择邻居数量nneighbors为输入数据集中点数量的 10%。由于我们希望簇尽可能相等,因此我们将equity_fraction设置为 1。

你可以看到如何通过调用fit方法获得每个数据点的簇标签。重要提示: 预测未包含在原始数据中的点的簇标签的函数尚未实现。如果你发现这段代码对你的工作有帮助,我鼓励你开发这个功能!

上述获得的簇标签可以添加到数据框coords中,以便在地图上绘制结果簇:

运行上述代码后,我们得到一个包含所有簇的交互式图,如图所示:

使用均等大小光谱聚类代码创建的簇。

在那个图中,你可以选择每个簇以单独进行可视化:

在上述用例中,超参数的优化是不必要的。然而,正如我之前提到的,如果需要,可以使用优化方法。在这种情况下,你可以使用作为优化指标的属性clustering.total_cluster_dispersion,它是所有簇离散度的总和。通过最小化这个量,得到的簇会更紧凑。

主要信息

在这篇博客中,我介绍了一个修改过的光谱聚类代码,该代码生成在点数上平衡的簇。这个算法可以用来生成空间点的均等聚合,并且在最后一公里配送领域的一些流程改进中可能也会有用。

等大小谱聚类的重要考虑因素如下:

  • 输入数据必须是一个与数据点坐标相关的对称距离矩阵。

  • 聚类代码的超参数包括期望的簇数(nclusters)、每个数据点的邻居数(nneighbors)以及确定簇大小是否相等的一个比例(equity_fraction)。你可以使用任何优化算法来找到最小化总簇离散度(total_cluster_dispersion)的最佳参数。

  • 等大小聚类也可以用于非空间数据,但尚未对这一目的进行测试。如果你想进行实验,请定义一个度量来创建代码所需的对称距离矩阵作为输入。务必先对变量进行归一化或标准化。

  • 该代码目前还没有prediction方法,但如果你觉得这个代码有用,欢迎贡献。

希望你喜欢这篇文章。再次感谢阅读!

致谢:等大小谱聚类是与 Mor Verbin、Lilia Angelova 和 Ula Grzywna 合作开发的。真是一个了不起的数据团队!

R 和 Python 的错误处理

原文:towardsdatascience.com/error-handling-in-r-and-python-5a4d60f3fba6

如果一个错误在函数中途出现,可以进行处理

Gustavo SantosTowards Data Science Gustavo Santos

·发表在 Towards Data Science ·阅读时间 5 分钟·2023 年 4 月 27 日

--

图片由 Joshua Hoehne 提供,Unsplash

介绍

一件事是明确的:如果你曾经编码过,你一定遇到过错误。就是这样。

错误并不总是坏事。我同意,有时候它们可能让我们抓狂,特别是当我们反复查看代码却找不到错误时。然而,错误信息必须被理解为代码中某些地方未按预期工作。

说实话,我们可以创建 N 个测试场景,但最终用户会发现 N+1 个错误。这没关系,只要我们能计划最常见的错误。

在本文中,我们将探讨一些已知的错误处理函数,这些函数可以在发生错误时让我们的代码继续运行。我们还将看到 R 和 Python 的代码片段。

继续阅读。

错误处理

错误处理是一种编程资源,可以让你的代码在发现错误后继续运行,而不会中断。

想象一下,你正在编写一个函数,该函数接受两个数字,计算它们之间的差值,并返回差值相对于第一个数字的百分比。这是一个相当直接的函数,但如果你将数字作为字符串输入,代码将崩溃并显示错误信息:我不能用文本进行数学运算,伙计。

这时,错误处理可能会很有用。如果我们知道这是来自最终用户的一种常见错误,我可以计划让我的代码绕过这个错误并返回结果,即使输入的数字是作为字符串输入的。

程序员可以说:

  • 尝试运行这段代码。

  • 如果这不起作用,你可以运行这段代码。

让我们继续,看看如何编写代码。

编程

R

在 R 中,用于错误处理的函数是tryCatch。如前所述,它将尝试运行主要代码,但如果它“捕获”到错误,则次要代码(解决方法)可以运行。

回到上一节的示例,一个没有错误处理的百分比差异计算函数如下。

# Function to calculate the percentage difference between two numbers
pct_difference <- function(n1, n2) {
  "Function that takes two numbers and return the percentual difference
   between n1 and n2, being n1 the reference number"

  pct_diff <- (n1-n2)/n1

  return ( cat('The difference between', n1, 'and', n2, 'is', n1-n2,
               ', which is', pct_diff*100, '% of', n1) )
}

# Test 1
pct_difference(10, 2)
[OUT] The difference between 10 and 2 is 8 , which is 80 % of 10

# Test 2
pct_difference(10, '2')
[OUT] Error in n1 - n2 : non-numeric argument to binary operator

请注意,在 Test2 中,如果我们输入字符串,代码不会继续执行。它会中断。

现在,我们可以使用tryCatch并使函数即使在输入数字作为字符串时也返回值。

# Function to calculate the percentage difference between two numbers
# with error handling
pct_difference_error_handling <- function(n1, n2) {
  "Function that takes two numbers and return the percentual difference
   between n1 and n2, being n1 the reference number"

# Try the main code
  tryCatch(pct_diff <- (n1-n2)/n1,

        # If you find an error, use this code instead
           error= return(
             cat( 'The difference between', as.integer(n1), 'and', as.integer(n2), 'is', 
                  (as.integer(n1)-as.integer(n2)), 'which is', 
                  100*(as.integer(n1)-as.integer(n2))/as.integer(n1),
                  '% of', n1 )#cat  
             )#return
           )#trycatch

  # If no error happens, return this statement
  return ( cat('The difference between', n1, 'and', n2, 'is', n1-n2,
               ', which is', pct_diff*100, '% of', n1) )
}

# Test 1
pct_difference_error_handling(10, 3)
[OUT] The difference between 10 and 3 is 7 which is 70 % of 10

# Test 2
pct_difference_error_handling('10', '3')
[OUT] The difference between 10 and 3 is 7 which is 70 % of 10

完美!它工作得很好。现在我们来看看如何用 Python 实现相同的代码。

Python

在 Python 中编写相同的函数也非常简单。在这种情况下,我们应该使用try ... except格式。正如你可能已经得出的结论,try 是主要代码片段,但如果发生异常(错误),则运行次要代码片段,即解决方法。

# Function to calculate the percentage difference between two numbers
# with error handling
def pct_difference_error_handling(n1, n2):
  '''Function that takes two numbers and return the percentual difference
   between n1 and n2, being n1 the reference number'''

  # Try the main code
  try:
    pct_diff = (n1-n2)/n1
    return f'The difference between {n1} and {n2} is {n1-n2}, which is {pct_diff*100}% of {n1}'

  # If you find an error, use this code instead
  except:
    pct_diff = (int(n1)-int(n2))/int(n1)
    return f'The difference between {n1} and {n2} is {int(n1)-int(n2)}, which is {pct_diff*100}% of {n1}'

# Test 1
pct_difference_error_handling(10,2)
[OUT] The difference between 10 and 2 is 8, which is 80.0% of 10

# Test2
pct_difference_error_handling('10', '2')
[OUT] The difference between 10 and 2 is 8, which is 80.0% of 10

非常好。代码运行正常。

最终

我们可以在两种语言中的任何一个函数中添加finally子句。这个子句会始终运行,无论 try 块是否引发错误。因此,它可以是一个完成消息或总结,例如。

try:
  print(x)
except:
  print("There's no x")
finally:
  print("Code ended")

[OUT] 
# There's no x
# Code ended
tryCatch( print(x),
          error= print('There is no x'),
          finally= print('Code ended') )

[OUT]
#[1] "There is no x"
#Error in value[[3L]](cond) : attempt to apply non-function
#[1] "Code ended"

我知道这些函数很简单,并且有更好的方法来创建它们而不使用错误处理。然而,我认为这是最温和的方式来教你如何编写带有异常的函数。

现在轮到你把它应用到你的工作中,以帮助你解决业务问题,适用于许多不同的用例和形式。

在你离开之前

在这个快速教程中,我们学习了如何使用 R 或 Python 处理错误。

错误处理是一个编程资源,它使你的代码在遇到预期错误时仍能继续运行。在我们的示例中,这个错误是输入了字符串而不是数字。

R 错误处理:

  • 使用函数tryCatch(expression, error, finally)

在 Python 中:

  • 使用函数try: expression except: expression finally: expression

如果你喜欢这个内容,请关注我的博客获取更多信息。

[## Gustavo Santos - Medium

阅读 Gustavo Santos 在 Medium 上的文章。数据科学家。我从数据中提取见解,帮助个人和公司……

gustavorsantos.medium.com](https://gustavorsantos.medium.com/?source=post_page-----5a4d60f3fba6--------------------------------)

也可以在LinkedIn找到我。

现在,我还有一个Topmate 页面,你可以在那里预约时间,如果你想讨论数据科学的话。

参考

[## Python Try Except

try 块让你测试一块代码是否有错误。except 块让你处理错误。else 块让……

www.w3schools.com](https://www.w3schools.com/python/python_try_except.asp?source=post_page-----5a4d60f3fba6--------------------------------)

en.wikipedia.org/wiki/Exception_handling

[## 如何在 R 中编写你的第一个 tryCatch() 函数 - Statology

你可以使用 tryCatch() 函数在 R 中返回某个表达式的值,或者在出现警告时生成自定义消息…

www.statology.org](https://www.statology.org/r-trycatch/?source=post_page-----5a4d60f3fba6--------------------------------)

机器学习中的分类问题关键评估指标

原文:towardsdatascience.com/essential-evaluation-metrics-for-classification-problems-in-machine-learning-69e90665375b

了解和评估分类模型性能的全面指南

Aaron ZhuTowards Data Science Aaron Zhu

·发布于 Towards Data Science ·阅读时长 10 分钟·2023 年 3 月 10 日

--

图片由 Markus Winkler 提供,来源于 Unsplash

你是否对评估机器学习模型性能时使用的术语感到困惑?当遇到混淆矩阵、精确度、召回率、特异性和敏感性等术语时,是否会感到迷茫?别担心,因为在这篇博客文章中,我们将深入探讨这些评估指标,帮助你理解这些术语。

什么是混淆矩阵?

让我们从混淆矩阵开始。它是用于评估分类模型性能的表格。它包含四个值:真正例 (TP)、真负例 (TN)、假正例 (FP) 和假负例 (FN)。真正例是指模型正确预测正类,真负例是指模型正确预测负类,假正例是指模型预测为正类但实际为负类,假负例是指模型预测为负类但实际为正类。

图片作者

什么是准确率?

准确率 是衡量模型总体表现的指标。它是模型做出的正确预测占所有预测总数的比例。换句话说,它是正确预测的正例和负例数量除以预测总数。

准确率 = (TP+TN)/(TP+TN+FP+FN)

什么是精确度?

Precision 是衡量模型正例预测准确度的指标。它的计算方法是将真正例的数量与真正例和假正例的总和之比。高 Precision 表明模型大多数时间正确预测了正类。

Precision = TP / (TP + FP)

什么是 Recall?

Recall 是衡量模型识别正类能力的指标。它的计算方法是将真正例的数量与真正例和假负例的总和之比。高 Recall 表明模型能够识别大多数正实例。

Recall = TP / (TP + FN)

什么是 Specificity?

Specificity 是衡量模型识别负类能力的指标。它的计算方法是将真正例的数量与真正例和假正例的总和之比。高 Specificity 表明模型能够识别大多数负实例。

Specificity = TN / (TN + FP)

你可以将 Specificity 视为带有不同正负标签定义的 Recall,即正标签被视为负,负标签被视为正。

什么是 Sensitivity?

Sensitivity 是 Recall 的另一种术语,特别是在医学背景下,它指的是医学测试在实际患病的个体中检测疾病或情况的能力。

Sensitivity = TP / (TP + FN)

总结表:

作者提供的图片

什么时候 Precision-Recall 比准确率更好的指标?

Precision-Recall 用于数据不平衡时,而不是准确率,这意味着一个类别的样本显著多于另一个类别。

在这种情况下,准确率可能会产生误导,因为模型可以通过简单地预测多数类来实现高准确率。例如,在一个二分类问题中,如果 90% 的样本属于负类,那么一个总是预测负类的模型将有 90% 的准确率,即使它没有进行任何正确的正例预测。

当数据不平衡时,我们可能会有一个准确率非常高的模型。但由于 Precision-Recall 值低,模型是无用的。

Precision-Recall 是在这种情况下衡量模型性能的更好指标,因为它考虑了真正例和假正例的比例,以及假负例和真正例,这些在不平衡数据集中更为关键。Precision 衡量的是所有正例预测中真正例预测的比例,而 Recall 衡量的是所有实际正例样本中真正例预测的比例。

Precision-Recall 更适合用于评估不平衡数据集中的模型性能,而准确率在类别平衡时更为合适。

什么是 Precision-Recall 权衡?

理想情况下,我们希望我们的模型具有高精确度和高召回率。通常很难同时实现这两者。

精确度-召回率权衡的出现是因为优化一个指标通常会以另一个指标为代价。

这是为什么

  • 如果一个模型在预测中更为保守,它可能通过减少假阳性的数量来实现更高的精确度,但这也可能导致召回率降低,因为它可能会遗漏一些真实的正例。

  • 相反,假设一个模型在预测中更为激进。在这种情况下,它可能通过捕获更多的真实正例来实现更高的召回率,但这也可能导致精确度降低,因为它可能会产生更多的假阳性预测。

因此,为了确定我们应该优先考虑精确度还是召回率,我们需要评估假阳性和假阴性的成本。

在情况 1 中,设计了一种医学检测测试来检测人们是否患有疾病。

  • 假阴性案例的成本可能是——病人未能接受正确的治疗,如果疾病具有传染性,可能导致更多人感染。

  • 另一方面,假阳性案例的成本可能是——浪费资源治疗健康人群,并不必要地将其隔离。

因此,假阴性案例的成本远高于假阳性案例的成本。在这种情况下,更关注召回率更为合理。

图片由作者提供

在情况 2 中,一家银行设计了一个机器学习模型来检测信用卡欺诈。

  • 假阴性案例的成本可能是——银行在欺诈交易中损失金钱。

  • 假阳性案例的成本可能是——虚假的欺诈警报影响了客户体验,导致客户留存率下降。

因此,假阳性案例的成本远高于假阴性案例的成本。根据关于信用卡欺诈的研究,假阳性信用卡欺诈的损失收入是实际欺诈的 13 倍。在这种情况下,更关注精确度更为合理。

图片由作者提供

什么是 F1-Score?

在上述示例中,我们尝试优先考虑召回率或精确度,但这可能以牺牲另一个指标为代价。然而,也有许多情况,其中召回率和精确度是 同等重要的。在这种情况下,我们应使用另一种度量标准,称为 F1 分数。

F1 分数考虑了精确度和召回率,并提供一个总结模型整体表现的单一分数。其范围从 0 到 1,得分为 1 表示完美的精确度和召回率。

计算 F1 分数的公式是:

它也可以通过精确度和召回率的调和平均数来计算:

接收者操作特征曲线(ROC 曲线)和曲线下面积AUC)是什么?

接收者操作特征(ROC)曲线是二分类模型性能的图形表示,它预测事件发生的概率。ROC 曲线是通过将真正例率(TPR)与假正例率(FPR)在不同阈值设置下绘制出来的。

TPR=TP/(TP+FN)

FPR=FP/(FP+TN)

让我们考虑一个二分类问题的例子,我们希望预测一封电子邮件是否是垃圾邮件。假设我们有一个模型,它预测电子邮件是垃圾邮件的概率,我们希望使用 ROC 曲线来评估其性能。

要创建 ROC 曲线,我们需要设置一个阈值(即从 0 到 1),根据这个阈值我们将电子邮件分类为垃圾邮件,低于这个阈值则分类为非垃圾邮件。阈值是决定真正例率(TPR)和假正例率(FPR)之间权衡的决策边界。

例如,如果我们将阈值设置为 0.5,则任何预测概率大于 0.5 的电子邮件将被分类为垃圾邮件,而任何预测概率小于或等于 0.5 的电子邮件将被分类为非垃圾邮件。这个阈值会给我们一定的 TPR 和 FPR。

通常,随着阈值的增加,TPR 和 FPR 都会减少。在最极端的情况下,当阈值为 0 时,所有预测值都是正的,因此 TPR=FPR=1。相反,当阈值为 1 时,所有预测值都是负的,因此 TPR=FPR=0。

假设对于给定的数据集,我们计算了各种阈值下的 TPR 和 FPR,得到如下结果:

作者提供的图像

我们可以将这些值绘制在 ROC 曲线上,y 轴为 TPR,x 轴为 FPR,如下所示:

作者提供的图像

从图中可以看出,ROC 曲线是不同阈值下 TPR 和 FPR 之间的权衡。

曲线下面积AUC)衡量模型的整体性能,AUC 为 1 表示完美的性能,AUC 为 0.5 表示随机猜测(即表示随机预测的对角线)。

ROC 的主要要点:

  • 当类别平衡且假正例和假负例的成本相似时,ROC 在评估不同模型时效果更好。AUC-ROC 值越高,模型越好。

  • 从 ROC 曲线中,我们可以选择最佳阈值,这取决于分类器的应用目的——如果假阳性和假阴性的成本相似,那么接近 ROC 曲线左上角的阈值是最佳的。如果假阳性和假阴性的成本较高,我们可以选择较高的阈值。相反,如果假阴性和假阳性的成本较高,我们可以选择较低的阈值。

  • ROC 是阈值不变的。它测量了模型在一系列阈值下的表现。这意味着我们不需要提前使用 ROC 确定阈值,与基于特定阈值的精确度、召回率、准确性和 F1 分数不同。

什么是精确度-召回曲线(PRC)?

与绘制 TPR 和 FPR 的 ROC 曲线相比,精确度-召回曲线(PRC)在 y 轴上绘制精确度,在 x 轴上绘制召回率。PRC 曲线显示了模型在识别正例时如何避免假阳性。

PRC 曲线下面积可以衡量模型的性能。AUC-PRC 值越高,模型越好。AUC-PRC 为 0.5 的模型不比随机猜测好,而 AUC-PRC 为 1.0 的模型是完美的。

一般来说,随着阈值的增加,精确度会提高,而召回率会降低。在最极端的情况下,当阈值为 0 时,所有预测值都是正值,因此,召回率=1,精确度=0。相反,当阈值为 1 时,所有预测值都是负值,因此,召回率=0,精确度=1。

假设对于给定的数据集,我们计算了各种阈值下的精确度和召回率,并得到了以下结果:

图片由作者提供

图片由作者提供

PRC 的关键要点:

  • 在 ROC 和 PRC 曲线之间的选择取决于当前的问题。当类别平衡且假阳性和假阴性的成本相似时,ROC 曲线是有用的。PRC 曲线在类别不平衡或假阳性和假阴性的成本不同的情况下很有用。

  • 通过查看 PRC,你可以选择一个最佳阈值,根据你的具体使用案例平衡精确度和召回率。

我们已经覆盖了许多分类问题的评估指标。这些指标是相互关联的,每个指标在衡量模型准确性方面都有其优缺点。总体而言,理解这些指标对于开发有效的机器学习模型并根据预测做出明智的决策至关重要。

如果你想了解更多与统计学相关的文章,请查看我的文章:

感谢阅读!

如果你喜欢这篇文章,请点击掌声图标。如果你想看到我和其他数千位作者在 Medium 上的更多文章,你可以:

  • 订阅我的新闻通讯,以便在我发布新文章时收到邮件通知。

  • 注册成为会员以解锁对 Medium 上所有内容的完全访问权限。

机器学习的必读资料

原文:towardsdatascience.com/essential-readings-in-machine-learning-250980e30585?source=collection_archive---------9-----------------------#2023-01-12

TDS EditorsTowards Data Science TDS Editors

·

关注 发布于 Towards Data Science · 作为 Newsletter 发送 · 3 min read · Jan 12, 2023

--

如果你在新的一年里希望扩展你对机器学习的知识,你来对地方了。无论你对算法、超参数调整还是 MLOps 有多少经验,你都可以找到多种进入这个主题的途径,而我们的作者——他们中的许多人都是经验丰富的 ML 专家——在将复杂概念转化为引人入胜且可操作的帖子方面具有特殊的天赋。

本周,我们精选了最近几周发布的一些最佳 ML 文章,包括初学者指南和更高级的教程及项目演练。选择一篇,或者阅读几篇,你一定会学到一些新东西。

  • 熟悉 ML 管道Chayma Zatout 说:“在生产环境中实施机器学习对许多公司来说可能是一个具有挑战性且耗时的过程,”这就是为什么 MLOps 已成为全局思考机器学习工作流的关键方法。阅读 Chayma 的帖子,获得对这一关键主题的通俗易懂的基础介绍。

  • 决策树,耐心解释。如果你渴望亲手操作一个实际模型,不要错过Frauke Albrecht的首篇 TDS 文章。这是一篇详细的逐步教程,展示了如何为分类问题构建决策树,涵盖了底层数学 以及 Python 实现。

Tabea Schimpf 摄影,来源于 Unsplash

  • 深入了解推荐系统Wen Yang 最近获得了她公司创新日奖,她的最新帖子带我们了解了获得该奖项的项目:一个基于机器学习的工具,为自然爱好者推荐远足路线。这是对在紧张时间内构建和训练模型的迷人探索,同时也是在黑客马拉松中取得成功的有用路线图。

  • 全面的特征工程指南。优化你训练的模型的数据可能是你项目成功的决定性因素。Dominik Polzer 对特征工程的深入探讨涵盖了七种常见的方法(从编码和向量化到特征交叉),并展示了如何将这些方法应用于真实数据集。

  • 仰望星空(它们真的是星星吗?)。如果你热爱机器学习和天文学,不要错过Mohammed Saifuddin的最新文章。它探讨了恒星分类问题——如何根据光谱特征将给定的天体识别为恒星、类星体或星系——并辅以详尽(且解释清晰)的建模过程。

  • 解决业务问题的最佳机器学习方法是什么?随着越来越多的公司依赖机器学习来处理关键任务,如流失预测,选择正确的方法变得至关重要。Samuele Mazzanti最近解开了一个特别常见的困境:是依赖多个专门模型更好,还是选择一个通用模型更佳。

  • 超越机器:探索基于规则的学习Khuyen Tran的教程核心是一个重要观察:人类直觉和专业知识可以提升机器学习模型的表现。她接着展示了如何利用 human-learn Python 包来实现两全其美的结果。

一如既往,感谢您对我们工作的支持。为了最大限度地发挥影响力,考虑成为 Medium 会员

直到下一个变量,

TDS 编辑

使用滚动回归估计时间变化系数

原文:towardsdatascience.com/estimating-time-varying-coefficients-with-rolling-regression-66aca1f2c3f6

高斯随机游走作为回归系数的先验

Abhijeet TalaulikarTowards Data Science Abhijeet Talaulikar

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

--

照片由Myriam Jessier拍摄,来源于Unsplash

回归分析中的一个挑战是考虑到预测变量与响应变量之间的关系可能随时间变化的可能性。

从数据科学到计量经济学

数据科学家们经常遇到这样的情况:业务中存在假设,我们的模型必须考虑到它们可能是正确的。

普通回归模型在估计 E[Y|X]方面表现出色。Xᵢ(或βᵢ)的系数可以解释为当Xᵢ增加 1 时 Y 的增量变化。在计量经济学中,常常假设βᵢ随着时间变化,我们可以称之为βᵢₜ。我们真正尝试估计的是 E[Y|X,Time]。下面是一个公司的 2 年收入图表——它是一个季节性业务,没有理由相信β也依赖于时间。

图片由作者提供

在这篇文章中,我们将介绍回归中的时间变化系数的概念,并展示如何将其应用于市场营销组合模型。如果你还没看过,请查看我在Medium 系列上关于从零开始构建 MMM 的所有要素。简单回顾一下,市场营销组合模型是一个统计工具,帮助营销人员衡量不同营销活动(如广告、定价、促销等)对销售或其他结果的影响。一个典型的市场营销组合模型可以表示为:

其中yₜ是时间t的结果变量(如销售额),xᵢₜ是时间t的预测变量(如广告支出、价格等),βᵢ是衡量每个预测变量对结果变量影响的系数,Ɛₜ是误差项。

然而,这个模型假设系数在时间上是恒定的,这在某些情况下可能不现实。例如,广告对销售的影响可能会根据季节性、产品生命周期、竞争环境等因素而变化。为了捕捉这种变化,我们可以在回归中使用时间变动系数,使系数作为某些其他变量的函数随时间变化。例如,我们可以写:

其中βᵢ(t)现在是时间或其他变量的函数,这些变量捕捉了系数的动态变化。有不同的方式来指定这些函数,如使用样条函数、多项式、随机效应等。函数的选择取决于数据的性质和研究问题。

高斯随机游走

当媒体的效果随着时间变化时,可以通过时间变动的函数来捕捉系数。最受欢迎的选择之一是高斯随机游走。

你可能会感兴趣的是,这个过程被用来建模金融数据,如股票或黄金。

高斯随机游走是一种随机过程,模拟独立且同分布的随机变量的累计效应。它也被称为正态随机游走或维纳过程。高斯随机游走可以用一系列随机变量X₁X₂、…、Xₙ来表示,其中每个Xᵢ都服从均值为 0、方差为 1 的正态分布,并且前 3 个变量的总和

是随机游走者在时间 n 的位置信息。高斯随机游走有一些重要的特性,如是马尔可夫过程、具有平稳增量,以及在概率上是连续的。

贝叶斯时间变动 MMM 的代码

这是我在这篇文章中构建的 PyMC 模型的升级版本。我添加了按周、季度、学期或年变化的系数代码。这种回归形式也常被称为滚动回归。

让我们看看代码中有什么变化。

首先,你会注意到这个模型现在被包装在一个函数 BayesianMMM 中。这是为了方便,我们可以用不同的参数来运行它。这个示例接受一个名为splits的参数,用于决定时间的粒度定义——按周(W)、按季度(Q)、半年(H)或年比年(YoY)。

你还会注意到,PyMC 模型本身接受一个名为coords的参数,这本质上是你的时间变量。如果你选择每周变化系数,时间变量将是1…104。如果选择季度,则为1,1,1...,2,2,2…,3,3,3…4,4,4…,1,1,1…(记住,这是 2 年的数据)。

接下来,我们的系数现在是一个 GaussianRandomWalk 变量,而不是 TruncatedNormal。就像之前一样,我们在位置先验处初始化它(如果你不记得了,请回到这里)。在这种设置中,每个时间段都有自己的系数,但这些系数在连续时间段之间共享信息。 这是滚动回归的最大优势。

现在让我们来看看三个案例研究。

案例研究 1 — 上漏斗的媒体策略如何随着时间的推移而变化?

上漏斗营销指的是旨在生成潜在客户的意识和兴趣的活动,这些潜在客户尚未准备好购买。这通常很难衡量,因为它不会直接导致转化或销售,而是以微妙和长期的方式影响客户的旅程。然而,了解哪个渠道更有效以及趋势随时间的变化是有帮助的。

图片由作者提供

从中得出了非常有用的知识。在时间段开始时,直邮长期以来表现最差,直到它超过了电视。轶事证据证实,随着 CTV 的出现,电视的效果随时间逐渐减弱。

另一个观察是 CTV 的效果随时间的推移高于 Email。这些观察是有趣的,但在没有 A/B 测试的情况下,我们不能得出结论。

案例研究 2 — 我的业务是季节性的,在假日季节获得最多收入。

营销人员不应假设付费搜索的效果在全年保持不变,而应根据季度的波动和机会调整他们的活动。然而,尤其在零售和电子商务中,可以观察到一些一般趋势。

付费搜索的效果在第四季度通常较高,尤其是在假日季节,当在线购物活动增加,消费者更可能点击提供折扣、交易或礼品建议的广告时。相反,付费搜索的效果在第一季度可能会下降,当时消费者需求下降,广告商面临较低的转化率和较高的点击成本。第二和第三季度可能显示出更稳定或适中的结果,具体取决于特定行业和市场条件。

图片由作者提供

我们发现付费搜索的效果在第四季度如预期的那样最高。

案例研究 3 — 我想衡量媒体效果的年同比趋势。

年度趋势是指从一年到下一年发生的关键绩效指标(KPI)变化。它们可以帮助营销人员设定现实目标,调整预算,并根据有效和无效的情况优化战术。

通过分析历史数据、进行市场研究以及测试不同策略,营销人员可以优化他们的付费搜索效果,并在每个季度实现他们的目标。出版商的指导和内部 A/B 测试也可以帮助确定投资回报率(ROI)最高的时间。最后,这一切有助于校准我们的 MMM。

作者提供的图片

我们看到同比付费搜索效果提高了大约 2%。类似的趋势经常被报告,并归因于营销部门的成功。

结论

如果你跟随了我的 MMM 系列文章,你会发现我强调了你的 MMM(或任何其他测量方法)能够被利益相关者使用的重要性。时间变系数有助于回答许多可能会问到的普通 MMM 无法解答的问题。通过贝叶斯优化技术,我们可以对模型代码进行小的调整,使其本质上能够回答这些问题。

在回归分析中使用时间变系数的一个优势是它可以提供更准确和灵活的预测变量对结果变量影响的估计。它还可以帮助识别这些影响何时以及如何变化,以及驱动这些变化的因素。这可以帮助营销人员设计更有效和高效的营销策略,并优化他们的营销组合。

感谢阅读😊

估算总体实验影响

原文:towardsdatascience.com/estimating-total-experimentation-impact-ab6cd56bffb

如何控制在衡量组织整体影响时的虚假发现和选择偏差

贾雷德·M·马鲁斯金,博士Towards Data Science 贾雷德·M·马鲁斯金,博士

·发表于 Towards Data Science ·阅读时间 16 分钟·2023 年 9 月 20 日

--

图片由 CHUTTERSNAP 提供,Unsplash

介绍

数据驱动的组织经常在任何给定时间进行数百或数千个实验,但所有这些实验的净影响是什么?一种简单的方法是将所有实验中显著且正面的处理效果的均值差异相加,并且这些实验已经投入生产。然而,这种估计可能极其有偏,即使我们假设各个实验之间没有相关性。我们将进行 10,000 个实验的模拟,并展示这种简单方法实际高估了影响 45%

我们回顾了一个理论偏差修正公式,由 李和申 [1] 提出。然而,这种方法存在两个缺陷:首先,尽管它在理论上是无偏的,但我们表明其相应的插件估计器由于与原始问题类似的原因仍然存在显著偏差。其次,它未能将影响归因于个体级实验。

在本文中,我们探讨了两个偏差来源:

  • 虚假发现偏差 — 由于假阳性使估计值被夸大;

  • 选择偏差 — 由于决策标准引入的偏差使估计值被夸大:对处理效果的低估被审查(假阴性),而高估则被奖励。

为了解决虚假发现问题,我们将构造一个概率,表示某一结果实际上为非零。这个概率是通过将 p 值密度与真实零假设的残差密度进行比较来构造的。

为了解决选择偏差,我们将为每个实验结果计算后验分布,使用经验分布作为我们的先验,并对假发现进行校正。

这一过程提供了我们模拟系列实验中平均实验影响的准确估计,将仅使用经验测量得到的原始45%误差降低到 0.4%误差。

效应分布

所以我们进行了很多实验,并希望量化我们所传递的总实验影响。为此,我们需要考虑效应分布,即治疗效应的分布。为了使这一点更加清晰,可以想象进行许多实验。每个实验都有一个真实的效应值θ,我们可以将其视为从某种真实效应分布p中抽取的随机变量。每个实验然后通过Xᵢ(我们可以视其为均值差异的平均值)来估计θᵢ。因此,我们得到如下的观察模型:

这里,σᵢ² ~ 1/nₛ,其中nₛ 是样本大小。我们将真实效应分布视为三种子群体的混合:真实零效应、正效应和负效应。为了描述效应分布,我们考虑三个参数:

  • λ = P(θ != 0) (有效性): 具有非零效应的实验所占的比例,

  • κ = P(θ > 0 | θ != 0) (不对称性): 在具有非零效应的实验中,具有正向效应的实验所占的比例,

  • ρ = P(θ > δ | θ > 0) (优势): 在那些具有正向(显著)效应的实验中,实际显著效应(即效应值大于δ)的实验所占的比例。

这些参数在下图中进行了说明。

治疗效应的分布: (1-λ) 为零,κ λ 为正,κ λ ρ 为实际显著。图像由作者提供。

对于较新的产品,可能容易提出产生显著效应的特性想法,因此λ会很高。然而,对于成熟的产品,实验导致显著变化的情况可能非常少见,λ 会较低。

同样,一个团队可能非常擅长提出实际产生正向效应的产品改进想法,使得κ > 1/2,而另一个团队可能提出了一堆导致性能下降的糟糕想法,使得效应趋向于负向,从而κ < 1/2

理论偏差估计器

考虑随机集合A,其中包含一系列实验中的正向显著结果,决策标准为X_i > c。定义总的真实效应和估计效应为

总期望偏差可以描述为b = E[Sₐ— Tₐ]。如果Xᵢ ~ N(θᵢ, σᵢ)Lee 和 Shen [1] 显示总期望偏差为:

其中ϕ是标准正态分布的密度。具体细节在2中进一步回顾。这个公式有两个主要缺点:

  1. 它依赖于真实效应θᵢ。因此,要实际使用它,我们必须用观察到的效应替换真实效应,即所谓的插件估计器。在我们的仿真中,我们展示了插件估计器可能导致显著偏差的估计。(我们还将展示如何通过考虑虚假发现来纠正这一点。)

  2. 求和是针对所有实验的,不仅限于那些有显著正结果的实验。因此,即使纠正了第一个问题,它也无法提供个别实验水平的影响估计,即所谓的归因问题

模拟实验平台

一些假设

让我们首先对我们分析的实验集做一些假设:

  • 每个实验测量相同顶线指标的均值差异(例如 DAUs、花费时间、收入);

  • 每个实验都使用相同的显著性水平α和效能β来计算效应大小δ(这是实际具有实际意义的最小效应);

  • 因此,每个实验具有相同的样本大小nₛ和拒绝标准Xᵢ > c

为了计算目的,我们将使用α = 0.05β = 0.8来计算效应大小δ = 0.1,这将得到c = 0.07 和nₛ = 785。注意:我们将α视为双侧显著性。实际的显著性对于正向测试是α/2

Python 中的仿真

接下来,我们在 Python 中构建一个模拟实验平台,如下所示。

class PlatformSim:
    def __init__(self, alpha=0.05, beta=0.80, delta=0.1):
        self.alpha, self.beta, self.delta = alpha, beta, delta
        self.z_crit = stats.norm.isf(self.alpha/2)
        self.n_s = int(np.ceil(delta**(-2) * (stats.norm.isf(alpha/2) - stats.norm.isf(beta))**2))
        self.theta_crit = self.z_crit / np.sqrt(self.n_s)

    def run(self, n, lambda_=0.2, kappa=0.5, rho=0.2, shape=3):
        self.n_null = int(n * (1 - lambda_))
        self.n_pos = int(n * lambda_ * kappa)
        self.n_neg = n - self.n_null - self.n_pos 
        # compute thetas
        thetas_null = np.zeros(self.n_null)
        scale = self.delta / scipy.stats.gamma.isf(rho, shape)
        thetas_pos = +np.random.gamma(shape, scale=scale, size=self.n_pos)
        thetas_neg = -np.random.gamma(shape, scale=scale, size=self.n_neg)
        self.thetas = np.concatenate((thetas_null, thetas_pos, thetas_neg))
        # for each theta, simulate result of an experiment        
        np.random.shuffle(self.thetas)
        self.x_values, self.z_values, self.p_values = self.simulate_results(self.thetas)
        self.results = (self.z_values > self.z_crit).astype(int)

    def simulate_results(self, thetas):
        X = np.random.normal(loc=thetas, size=(self.n_s, len(thetas)))
        self.X = X
        x_values = X.mean(axis=0)
        z_values = x_values * np.sqrt(self.n_s)
        p_values = 2 * stats.norm.sf(np.abs(z_values))
        return x_values, z_values, p_values

我们首先将(1-λ)的实验指定为真实的虚无效应。

对于其余的实验,我们使用伽马分布,确保在δ处生存函数为ρ

使用PlatformSim类,我们可以用几行代码运行仿真。我们将结果放入数据框df中,并为正向结果创建一个单独的数据框dfs注意:这可以很容易地修改为建模不同的正向/负向分布。然而,为了我们的仿真,我们使用了伽马随机变量。

P = PlatformSim(alpha=0.05, beta=0.8, delta=0.1)
P.run(10000, lambda_= 0.2, kappa=0.5, rho=0.3, null_type=null_type, pos_type=pos_type, shape=3)
df = DataFrame({'theta': P.thetas, 'x': P.x_values, 'z': P.z_values, 'p': P.p_values, 'result': P.results})
df.sort_values('p', inplace=True, ignore_index=True)
dfs = df[df.result==1]
effect = dfs.theta.mean()
print(f"Average Effect (Actual): {round(effect, 6)}")  
print(f"Average Effect (Observed): {round(dfs.x.mean(), 6)}     REL: {100*round(dfs.x.mean() / effect - 1, 2)}%")  
print(f"False Discovery Rate: {round(100*len(dfs[dfs.theta <= 0]) / len(dfs), 2)}%") 
sig_i = 1 / np.sqrt(P.n_s)
imp_b = (dfs.x.sum() - sig_i * scipy.stats.norm.pdf((P.theta_crit - df.x) / sig_i).sum()) / len(dfs)
imp_0 = (dfs.x.sum() - sig_i * scipy.stats.norm.pdf((P.theta_crit - df.theta) / sig_i).sum()) / len(dfs)
print(f"Bias-Corrected True      Estimate: {round(imp_0, 6)}     REL: {round(100* imp_0 / dfs.theta.mean() - 100, 2)}%")
print(f"Bias-Corrected Empirical Estimate: {round(imp_b, 6)}     REL: {round(100* imp_b / dfs.theta.mean() - 100, 2)}%")
# Output ~
# Average Effect (Actual): 0.079171
# Average Effect (Observed):  0.113245    REL: 43.0%
# False Discovery Rate:  27.04%
# Bias-Corrected True      Estimate:  0.079023     REL: -0.19%
# Bias-Corrected Empirical Estimate:  0.062471     REL: -21.09%

使用我们的仿真,我们可以轻松地测量具有显著正效应的实验的观察到的平均处理效应和实际处理效应。在这个仿真中,真实的平均效应是0.0792,而我们测得的效应是0.1132,误差为+43%!我们还观察到 27%的虚假发现率。

我们还观察到,当使用真实的θᵢ时,理论偏差修正公式的效果非常好,误差为 0.19%,但当使用插件估计器时,效果不佳,产生了 21%的误差。理论估计器对偏差进行了过度修正。这是由于真实的虚无效应数量庞大,并且偏差公式拾取了随机分布在右侧的观测值,但没有拾取左侧的观测值,因为估计器值在接近决策边界的观测值处达到峰值。

我们可以绘制效应分布的直方图。下面是实际效应的直方图,正向测试结果用橙色突出显示。

真实效应的分布,橙色代表正结果。图片来源于作者。

我们注意到有大量假阳性,远远超过我们的α/2 = 2.5%显著性水平。假阳性与总体阳性的比例称为假发现率,我们将讨论如何估计和调整它。

现在,在进行一系列实验时,我们无法获得这种真实的治疗效应分布,而只能获得观察到的治疗效应分布,如下图所示。在这里,我们用橙色突出显示那些实际具有正效应的实验。这一视角使我们深入了解第二种我们必须解决的偏差类型:选择偏差

测量效应的分布,橙色代表实际存在效应的实验。决策边界由虚线垂直线表示。图片来源于作者。

为了理解选择偏差,考虑到假阴性的真实效应在垂直虚线的右侧。对于每个散布在左侧的实验,将有一个散布在右侧。因此,假阴性可以被视为对实际阳性的过高估计。选择过程本身因此产生了一种偏差:只有当观察到的效应大于c时,我们才会将结果视为统计显著。因此,我们选择那些随机具有比其真实效应更高效应的观察结果,而忽略了那些效应较低的观察结果。上述图中突出显示的所有假阴性“平衡”了测量效应被夸大的真正阳性。

假发现

在本节中,我们讨论λκ、功效和假发现率之间的一些理论关系。我们将在本说明中的模拟中使用这些关系。

估计λκ

为了使用实践中可用的观察数据来估计假发现率,我们首先需要估计总体参数λκ

要估计λ,我们考虑导致统计显著结果的实验的比例f。这些包括真正的阳性和假阳性。在(1-λ)没有效应的实验中,我们期望α (1 — λ)产生假阳性。在λ具有实际效应的实验中,我们期望βₐ λ产生真正的阳性,其中βₐ是这组实验的平均功效。因此,显著结果的预期比例由以下公式给出

这可以重新排列以生成估计值

其中βₐ是观察到的显著结果的平均功效。(我在显示的方程中使用了β^*表示βₐ,但它们是相同的。)

估计假发现率

虚假发现率(FDR)是指正结果中虚假正例的比例。我们在模拟中观察到了 27%的虚假发现率。

要估计这个,我们需要 平均正功效

这里,β(θ) = P(X > c | θ) 是观察到正显著效应的概率,给定 θ。接下来,我们将假设平均负功效 β₋ 可以忽略不计。

FDR 可以通过贝叶斯定律表示为

我们稍后将使用此公式来获得 27.6%的虚假发现率估计。这个近似等式提醒我们,我们忽略了负分布的功效,而在我们的模拟中,这一功效是可以忽略的。

密度估计和 p-概率

我们接下来探索一种方法,通过两步法调整虚假发现偏差:

  • 确定每个正结果是正确正例的概率;

  • 计算折扣平均处理效应,其中我们按其为虚假正例的概率对每个实验的效应进行折扣。

为了制定实验产生实际效应的概率,我们转向了 p-值。这里的关键观察是,在零假设下,p-值的分布是均匀分布。这在下面的图中很明显,该图表示了我们所有 p-值的直方图,并用橙色突出显示了实际零假设的 p-值。

p-值的直方图;实际零假设的 p-值以橙色绘制。图片来源于作者。

f(p)p-值的观察概率密度,并且 fₐ 表示半区间 [0.5, 1] 上的 p-值的平均概率密度。我们可以推断 fₐ 是真实零假设的平均概率密度,它在整个区间 [0, 1] 上是恒定的。

p-值方面,当 p < α = 0.05 时,我们拒绝零假设。对于给定的实验区间,比如 pᵢ,因此实验是正确零假设的概率为 fₐ / f(pᵢ),而它是正确非零假设的概率为 πᵢ = P(θᵢ != 0 | pᵢ) = 1 — fₐ / f(pᵢ),我们定义为 p-概率。注意,这一定义是双向的:大的正观察值或负观察值将具有相等的 p-概率,仅取决于效应的大小。

经验密度函数

为了实现这一点,我们需要一个经验密度估计。标准的方法是 分箱密度估计,定义为

其中,第i个区间由b_i = [i h , (i+1)h)定义,i=0, n_b-1h=1/n_b是区间宽度。区间大小受到偏差-方差权衡的影响:如果区间大小太小,会有高方差;但如果区间大小太大,会有高偏差。这个估计的问题是概率在边界p=0附近有一个峰值。区间大小必须足够小以捕捉接近零的行为,这会导致对单位区间其余部分的次优估计,且方差较高。

因此,我们提出了一种替代的经验密度,我们称之为最近邻密度估计

在这里,NNₖ(x)表示集合{X₁, … Xₙ}中距离x最近的k个“邻居”,范围表示集合的最大值和最小值之间的差异。

分箱估计保持区间大小不变,并测量每个区间内的数据点数量,而最近邻估计保持数量固定,并测量可变的区间大小。

为了在代码中实现这一点,我们计算了p值在* [0, 1]*区间上以 0.001 间隔的最近邻密度。这在下面的代码中实现。

 def knn(x, k, X):
    """ Returns the k nearest neighbors to x from the set X """
    n = len(X)
    X = np.sort(X)
    z = np.abs(X - x)
    start = np.argmin(z)
    stop = start * 1
    k_count = 1
    while k_count < k:
        if start == 0:
            stop += (k - k_count)
            k_count = k
            break
        if stop == n-1:
            start -= (k - k_count)
            k_count = k
            break
        if z[start-1] < z[stop+1]:
            start -= 1 
        else:
            stop += 1
        k_count += 1
    return X[start:stop+1]
# KNN-density estimate 
k, n_b = 200, 10000 # k, number of bins
h = 1 / n_b # bin size 
t = np.linspace(0, 1, n_b+1).round(4)
fp_knn = np.zeros(n_b+1)
for i in range(n_b+1):
    xnn = knn(t[i], k, df.p.values)
    fp_knn[i] = k / len(df) / (np.max(xnn) - np.min(xnn))
fp_base = fp_knn[t > 0.5].mean()
p_map = {p:f for p, f in zip(t, fp_knn)}
df.loc[:, 'f_p'] = df.p.apply(lambda x: p_map[round(x, 4)])
df.loc[:, 'p_prob'] = df.f_p.apply(lambda x: max(0, 1 - fp_base / x))
dfs = df[df.result==1] # positive significant results (for later)
dfs = dfs.reset_index(drop=True)

数值计算得到的密度函数在下图中绘制,并附有p值的归一化直方图。

p值的直方图;经验最近邻密度函数。图片作者提供。

估计假发现率

现在我们使用我们的p概率来估计假发现率,使用之前的计算,如下所示。

df.loc[:, 'power'] = stats.norm.sf(P.z_crit, np.abs(df.z))
beta_ = np.average(df[df.x > 0].power, weights=df[df.x > 0].p_prob) # average power of positive results
kappa_hat = np.average((df.x>0), weights=df.p_prob)
frac_sig = (np.abs(df.x) > 0.07).mean()
beta_0 = np.average(df.power, weights=df.p_prob) ## Two-sided average prob. of sig result
lambda_hat = (frac_sig - alpha) / (beta_0 - alpha)
FDR_hat = alpha*(1-lambda_hat) / (alpha*(1-lambda_hat) + 2 * beta_ * lambda_hat * kappa_hat)
# Outputs
# lambda     : 0.164780
# beta_      : 0.666541
# kappa      : 0.498272
# FDR true   : 0.270448
# FDR_hat    : 0.276170

这导致估计的假发现率为 27.6%,而实际的真实率为 27.0%。

纠正假发现的影响估计

我们还可以使用我们的p概率来控制假发现。

bias_sum = np.average(scipy.stats.norm.pdf((P.theta_crit - df.x) / sig_i), weights=df.p_prob) * df.p_prob.sum()
bias_sum += scipy.stats.norm.pdf((P.theta_crit - 0) / sig_i) * (len(df) - df.p_prob.sum())
imp_pcor = (dfs.x.sum() - sig_i * bias_sum) / len(dfs)
print(f"P-Corrected Average Effect : {round((dfs.x * dfs.p_prob).mean(), 6)}     REL: {round(100* (dfs.x * dfs.p_prob).mean() / dfs.theta.mean() - 100, 2)}%")
print(f"P-Corrected Bias Estimate  : {round(imp_pcor, 6)}     REL: {round(100* imp_pcor / dfs.theta.mean() - 100, 2)}%")
# Output
# P-Corrected Average Effect : 0.089668     REL: 13.26%
# P-Corrected Bias Estimate  : 0.082046     REL: 3.63%

在这里,我们重新计算了理论偏差估计,通过将每个观测值按其对应的p概率加权,得到一个误差为 3.6%的近似影响。(比之前未经修正的估计好得多!)这也大大改进了我们的经验观察影响估计,现在的误差仅为 13.3%。

选择偏差

真实正例的校准

为了了解导致选择偏差的原因,我们可以按观察到的影响X对数据进行分箱,过滤真实正例,并绘制每个区间内的实际影响。这在下面的校准图中显示。

理论(蓝色)和观察(红色)平均影响,经过真实正例过滤,并按观察到的均值差异分箱。图片作者提供。

后验估计

给定总体,θᵢ的期望值不再是观察到的均值Xᵢ。为了考虑总体效应,我们将改为考虑每个单独实验的后验效应分布 P(θᵢ | Xᵢ)。对于先验,我们将仅使用我们所拥有的最佳信息:调整过的p概率的经验分布。

其中πᵢ是第i个实验的p概率。因此,我们对每个观察点进行加权,以反映其作为真实非零结果的概率,然后将所有实验的零概率在原点合并。

θ的后验分布对于第i个实验可以表示为

其中nₛ是该实验的样本大小。(回忆一下Xᵢ是样本大小nₛ上的平均值。)

我们可以在 EffectDistribution 类中实现这些公式,如下所示。init 方法计算先验,而 getUpdate 方法返回后验。

class EffectDistribution:

    def __init__(self, x, p_prob, sort=True):
        x, f = list(x), list(p_prob)
        if (0 not in x):
            x.append(0)
            f.append(len(p_prob) - p_prob.sum())
        pmfmat = np.array([x, f])
        if sort:
            pmfmat = pmfmat[:, pmfmat[0, :].argsort()]
        self.x, self.pmf = pmfmat[0, :], pmfmat[1, :]
        self.zero_loc = np.argmin(np.abs(self.x)) 
        self.normalize()

    def __len__(self):
        return len(self.x)

    def normalize(self):
        self.pmf /= self.pmf.sum()

    def getProbNull(self):
        return self.pmf[self.zero_loc]  

    def getSample(self, n, pmf=None, prob=1):
        pmf = self.pmf if pmf is None else pmf
        sample = np.random.choice(self.x, size=n, replace=True, p=pmf)
        mask = np.random.choice([0, 1], size=n, replace=True, p=[1-prob, prob])
        return mask * sample

    def getUpdate(self, x_mean, n, sigma=1, inplace=False):
        pmf = self.pmf * np.exp(-n * self.x * (self.x - 2 * x_mean) / (2 * sigma**2))
        if inplace:
            self.pmf = pmf 
            self.normalize()
            return
        return pmf / pmf.sum()

现在我们可以遍历所有正显著结果,为每个结果计算新的预期影响以及来自后验分布的随机样本。

n_boot = 1000
B = np.zeros((len(dfs), n_boot))
E = EffectDistribution(df.x, df.p_prob)
for i, row in dfs.iterrows():
    pmf_ = E.getUpdate(row.x, P.n_s)
    dfs.loc[i, 'theta_hat'] = np.average(E.x, weights=pmf_)
    B[i, :] = E.getSample(n_boot, pmf=pmf_) #, prob=row.p_prob)
bootstrap_means = B.mean(axis=0)
print(f"POSTERIOR CORRECTED Estimate       : {round(dfs.theta_hat.mean(), 6)}     REL: {round(100*dfs.theta_hat.mean() / dfs.theta.mean() - 100, 2)}%")
# POSTERIOR CORRECTED Estimate       : 0.079478     REL: 0.39%

我们称这一过程为后验效应影响估计,因为它基于每个单独实验的后验分布。我们的最终校正估计显示 0.39%的误差,尽管自助法置信区间为(-3.3%,4.0%)。自助样本如下面所示。

后验-效应自助法估计。图片由作者提供。

此外,我们可以查看整体最终校准图(未筛选真实正例!):

校准图,显示了后验效应估计(蓝色)和实际影响(红色),按观察值进行分箱。对角线虚线表示未经校正的估计 X=X。图片由作者提供。

结论

在进行一系列大规模实验时,由于虚假发现选择,真实的总体影响往往会有所偏差。

此外,我们发现这些偏差在真实零结果的高比例λ下会加剧。这表明运行高质量实验至关重要,这些实验更有可能真正产生有意义的影响。另一方面,试验你能想到的一切,看看什么有效的方法会导致高虚假发现率和高偏差。这一挑战对于较成熟的产品来说更难,因为很难提出新的有意义的想法,而对于较新的产品来说,许多变化可能会带来产品改进。

为了纠正这些偏差,必须在两步过程中考虑所有实验的总体

  1. 使用p值计算每个实验实际上是非零结果的概率。

  2. 使用p概率校正的经验分布(所有结果)作为先验,计算每个正显著结果的效应的后验分布。

在我们的模拟中,简单观察的平均影响与真实效应相比产生了 45%的误差;对p-概率的校正将此误差降低到 13%,而最终的后验估计将误差进一步降低到 1%以下,置信区间为±3%。

额外阅读

如需更数学化的方法,请查看[1]。该论文提供了一个明确的公式,用于修正测量影响时的总体偏差。

[1] M.R. Lee 和 M. Shen,赢家的诅咒:在线控制实验中特征总效应的偏差估计(2018),KDD 18: 第 24 届 ACM SIGKDD 国际知识发现与数据挖掘大会论文集,2018 年 7 月,第 491–499 页

2 M. Berk,如何衡量团队的 A/B 测试影响(2021),Towards Data Science, 2021 年 6 月。

除非另有说明,所有图片均由作者提供。

机器学习项目中的伦理考虑

原文:towardsdatascience.com/ethical-considerations-in-machine-learning-projects-e17cb283e072

图片由 Dall-E 2 提供。

在构建 AI 系统时不要忘记这些话题

Hennie de HarderTowards Data Science Hennie de Harder

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

--

使用敏感数据、基于个人信息的歧视、影响人们生活的模型输出……数据产品或机器学习模型可能以多种不同方式造成伤害。不幸的是, 有很多例子 确实出现了问题。另一方面,许多项目是无害的:不使用敏感数据,项目仅仅是改进流程或自动化无聊的任务。当有可能造成伤害时,你应该投入时间关注本文的话题。

是的,数据伦理并不是你能想到的最令人兴奋的主题。但如果你想负责任地使用数据,你可能会对不同的方式感兴趣,以确保这一点。本文包含六个重要的伦理主题和调查你的模型表现的方法。还有实用工具可以帮助你做到这一点。除了对个人造成的伤害,另一个需要负责任的重要原因是伦理考虑会影响技术的信任和信誉。如果技术被视为不可信和不透明,它可能会阻碍其采用并影响其有效性。人们会失去信任,不再愿意使用 AI 产品。

治理

首个要提到的话题是 AI 治理。数据科学和 AI 的治理方面包括为确保这些技术以伦理和负责任的方式使用而制定的监管、伦理指南和最佳实践。

治理是一个高层次的术语,包含了所有以下主题及更多。具体定义在不同组织和机构中可能有所不同。大型公司通常使用框架来定义 AI 治理的各个方面。这是Meta的一个例子。像谷歌的这个团队一样,有完整的研究团队专注于负责任的 AI。

AI 治理仍在不断发展。即将讨论的主题是基础内容,一个好的 AI 治理框架至少应包含这些主题。

AI 治理以及本文涉及的主题。图片来源:作者。

公平性

机器学习中的公平性指的是模型的预测不应对某些人群存在不公平的偏见。在将模型应用于现实世界时,特别是在医疗和刑事司法等敏感领域时,这是一项重要的考虑因素。不幸的是,曾经出现过许多失败的例子,比如COMPAS微软的聊天机器人 Tay以及招聘抵押贷款申请中的问题。

实际上,这是机器学习中的主要伦理问题之一:通过训练数据将偏见引入模型的可能性。解决偏见引入的方式以及可以采取的缓解措施非常重要。

有不同的方法可以提高模型的公平性并减少偏见。首先是使用多样化的训练数据。使用多样化且具有代表性的训练数据集,以确保模型不会对特定群体或结果产生偏见,这是实现公平 AI 系统的第一步。

为了提高公平性,开发了不同的工具:

透明性与可解释性

在数据科学和人工智能中,透明性指的是利益相关者能够理解模型如何工作以及它如何进行预测的能力。讨论透明性的重要性对于建立用户对模型的信任和审计模型的意义非常重要。有些模型易于理解,比如(不是很深的)决策树以及线性或逻辑回归模型。你可以直接解释模型的权重或遍历决策树。

透明的模型有一个缺点:它们通常比像随机森林、提升树或神经网络这样的黑箱模型表现更差。通常,在理解模型工作原理与模型性能之间存在权衡。与利益相关者讨论模型需要多透明是一个好的实践。如果你从一开始就知道模型应该是可解释的并且完全透明,你应该远离黑箱模型。

可解释性与性能之间的权衡。点击放大。图片由作者提供。

与透明性密切相关的是可解释性。随着复杂机器学习模型使用的增加,理解模型如何做出预测变得越来越困难。这就是模型可解释性发挥作用的地方。作为模型所有者,你希望能够解释预测。即使是黑箱模型,也有不同的方法来解释预测,例如使用 LIME、SHAP 或全局代理模型。这篇文章深入解释了这些方法。

鲁棒性

鲁棒性指的是 AI 系统在面对意外或对抗性情况时保持其性能和功能的能力,例如输入与系统训练时显著不同,或试图恶意操控系统。一个鲁棒的 AI 系统应能正确处理这些情况,而不会导致重大错误或伤害。鲁棒性对确保 AI 系统在现实场景中的可靠和安全部署非常重要,因为这些场景可能与训练和测试时的理想化场景有所不同。一个鲁棒的 AI 系统可以增加对技术的信任,减少伤害风险,并帮助确保系统符合伦理和法律原则。

鲁棒性可以通过不同的方法来处理,例如:

  • 对抗训练,这可以通过对系统进行各种输入的训练,包括对抗性的或故意设计来挑战系统的输入。应内置对抗攻击的防御措施,如输入验证和异常行为监控。

  • 定期测试和验证,使用与训练时显著不同的输入,以确保系统按预期功能运行。

  • 人工监督机制,例如允许人工用户审查和批准决策,或在系统出错时进行干预。

另一个重要部分是定期(或自动)更新和监控模型。这有助于应对随时间发现的潜在漏洞或限制。

隐私

数据科学和人工智能通常依赖大量个人数据,这引发了隐私问题。公司应讨论数据的负责任使用方式以及保护个人信息的措施。欧盟数据保护法,GDPR,可能是全球最著名的隐私和安全法律。虽然这是一份庞大的文件,但幸运的是有很好的总结和检查表。简而言之,GDPR 要求处理欧盟公民个人数据的公司征得同意、通知安全漏洞、透明使用数据,并遵循隐私设计原则。被遗忘权是另一个重要概念。违反 GDPR 的公司可能面临高达公司年全球收入 4%的巨额罚款。

所有主要的云服务提供商都提供了确保敏感和个人数据得到保护,并以负责任和伦理的方式使用的方法。这些工具包括Google Cloud DLPMicrosoft Purview 信息保护Amazon Macie。还有其他工具可供选择。

图片来源于 Dall-E 2。

责任

人工智能中的责任指的是个人、组织或系统对其行为、决策以及人工智能系统生成的结果进行解释和辩护的责任。组织需要确保人工智能系统的正常运作。

当人工智能做出错误决策时,需要有人负责。开发者之间对此的看法不尽相同,如你在这个 StackOverflow 调查问卷中可以看到。几乎 60% 的开发者认为,管理层对代码的伦理问题负主要责任。但在同一问卷中,几乎 80% 的人认为开发者应考虑其代码的伦理影响。

每个人似乎都同意的一点是:人们对人工智能系统负有责任,并应承担这一责任。如果你想了解更多关于人工智能责任的内容,这篇论文是一个很好的起点。它提到了一些人工智能责任的障碍,比如通常有很多人参与部署算法,并且错误是被接受和预期的。这使得人工智能治理成为一个复杂的话题。

结论

随着机器学习在我们生活中变得越来越普遍,设计和部署这些系统的方式必须符合社会价值观和伦理。这涉及隐私、透明度和公平性等因素。组织和个人应采取最佳实践,例如以维护隐私的方式收集和处理数据,设计透明且可解释的算法,以及检查系统以防止对某些群体的歧视。通过考虑这些伦理因素,机器学习技术可以赢得信任,并最小化负面影响,造福所有人。

相关

[## 利用机器学习检测数据漂移

通过简单的自动化过程了解你机器学习模型的性能退化情况。

medium.com ## 适用于任何机器学习模型的模型无关解释方法

解释方法概述:置换特征重要性、部分依赖图、LIME、SHAP 等。

[towardsdatascience.com ## 可解释的机器学习模型

对逻辑回归模型和决策树的解释进行了详细介绍。

[towardsdatascience.com

人工智能中的伦理:偏见算法的潜在根源

原文:towardsdatascience.com/ethics-in-ai-potential-root-causes-for-biased-algorithms-890091915aa3?source=collection_archive---------1-----------------------#2023-01-27

理解数据偏见的另一种方法

Jonas DieckmannTowards Data Science Jonas Dieckmann

·

关注 发布于 Towards Data Science ·12 分钟阅读·2023 年 1 月 27 日

--

图片来源:Pixabay

随着数据科学应用数量的增加,滥用的潜力也在增加。很容易指责开发人员或分析团队算法结果的责任,但他们真的是主要的罪魁祸首吗?以下文章试图从不同角度讨论这个问题,并得出结论认为伦理滥用可能是我们社会数据中的真正问题

更美好的世界不会仅仅因为我们使用数据而到来;数据有其黑暗的一面。¹

偏见算法

如今,数据科学的讨论常常与商业和工业中的重大机遇相关联,以提供更准确的预测和分类解决方案,并改善人们在健康和环境领域的生活。² ³ 也许和这些好处一样重要的是,在评估数据分析中的新解决方案和方法时,应该考虑数据伦理挑战。作为伦理学的一个特定分支,数据伦理处理与数据、算法及相关实践有关的道德问题,以制定和支持道德良好的解决方案。⁴ 总体而言,数据的使用与滥用之间似乎存在一条微妙的界限。公司不仅收集数据以增加利润,还为了向客户提供量身定制的体验。⁵ 然而,当公司开始将其数据用于其他收集目的之外的外部利用时,就会出现伦理问题。除了这些隐私相关的问题外,另一个挑战是数据分析偏差,它可能以多种方式出现。例如,由有特定意图或框架的人设计的调查问题、从具有特定背景的群体中选择性收集数据,或数据来源本身的偏差。⁶

图片来源:Pixabay

算法歧视的流行例子

这些问题绝非理论上的——它们引发了实际关注,并在多个案例研究中引发了严重的讨论:

  • 犯罪预测中的种族歧视

    一个有问题且广为人知的例子是犯罪预测。2016 年,上海交通大学的一篇机器学习论文研究了是否可以通过面部特征分析来检测犯罪行为。⁷ 结果显示,那些外貌与平均水平“不同”的人更可能被怀疑犯有罪行。⁸ 另一个例子是美国用于预测被逮捕个体再犯可能性的风险评估算法。结果表明,底层算法倾向于将黑人被告的再犯可能性预测为白人被告的两倍,而白人被告则常被错误分类为低风险,即使他们实际上是再犯者。⁹

  • 招聘中的性别歧视

    另一个受偏见算法影响的领域是招聘。2014 年,亚马逊使用基于机器学习的软件来评估和排名潜在候选人的简历,以寻找新的顶级人才。2015 年,该公司发现其新系统在评估申请者时并未性别中立,特别是在软件开发职位上。根据过去十年的招聘情况,该系统惩罚了所有在简历中包含“女性”一词的申请。¹⁰

  • 社交媒体中的刻板印象

    性别和种族偏见也可以在 Facebook 的广告中找到。在他们 2019 年的实证研究中,Ali 等人发现,根据广告与不同受众的相关性,可能会对特定用户投放广告时存在偏见规则。这种相关性通常基于男性/女性刻板印象,通过分析数字广告媒体的图像分析软件进行分类和关联。¹¹

大数据伦理原则

如何防止这种情况的发生?这些案例只是现代世界中算法被偏见性使用的众多实例之一。许多专家讨论并评估避免数据分析偏见的方法,这在 Herdiana 发布的《大数据伦理 5 原则》之一中有所体现:“大数据不应固化诸如种族主义或性别歧视等不公平偏见。”¹² 虽然这一原则被认为是数据伦理中的通用规则,但给出的示例“如种族主义或性别歧视”似乎是最常见的情况。然而,更通用的描述可能是:

“大数据不应固化导致歧视的不公平偏见。”

加拿大人权委员会(CHRC)将歧视描述为因称为“理由”的原因而对某人或某群体的不公平对待。这些理由受到《加拿大人权法案》的保护,包括种族、民族或族裔来源、肤色、宗教、年龄、性别、性取向、性别认同或表现、婚姻状况、家庭状况、残疾、遗传特征以及已获赦免或注册被暂停的罪行。¹³ 在社会学中,歧视描述了基于个人或群体特征的贬低行为,这种行为是系统性地进行的,因此构成了严重的人权侵犯。¹⁴ 欧洲人权公约(ECHR)也在第 14 条中禁止在上述所有领域的歧视,并将语言、政治观点财产等添加到 CHRC 的名单中。¹⁵

欧洲经济和社会委员会(EESC)的一项研究题为“大数据的伦理:在欧盟政策背景下平衡经济利益和大数据的伦理问题”将算法偏见描述为大数据的关键伦理问题之一。他们将对算法的信任定义为一个伦理问题,因为大多数人认为机器天生是中立的。实际上情况并非如此,因此风险可能非常高。¹⁶ 但这种缺乏中立性的可能原因是什么?很难想象世界上最优秀的数据科学家在开发算法时有这样的目标。

根本原因分析

算法本身并没有偏见。然而,许多算法产生的结果是偏颇的,而对这些结果的原因和驱动因素的深入评估往往是肤浅的。此外,缺乏一个假设世界中没有算法的可能替代方案和结果。因此,对算法偏见的问题不应仅仅集中在算法的中立性上,还应强调(缺失的)原因。因此,以下分析旨在进一步探讨与偏见相关的问题,以及今天对开发者的伦理压力如何理解为一种集体责任。然而,必须强调消除技术和非技术世界中的歧视的重要性,因为这两者直接相关并相互影响。

  1. 一个有偏见的社会会导致有偏见的数据 从设计上讲,歧视在数据收集之前就已经发生,因为数据集是历史的集合,受到了社会本身的影响。要深入思考这个问题的技术方面,需要检查可能导致偏见系统的原因。在大多数论文和研究中,作者指出算法本身存在偏见,导致了歧视。¹⁷ ¹⁸ 然而,很难将歧视的根本原因归咎于算法本身,因为算法根据给定的输入和参数的交互以自动化方式执行决策规则。¹⁹ 机器学习算法背后的原理通常是一个优化问题,其中规则根据过去对结果的影响应用于参数。²⁰ 因此,在给定变量(例如性别)与目标(例如职位适合性)之间的交互中出现的偏见,并不是由算法本身造成的,而是由基础数据集造成的。以亚马逊招聘的例子来看,偏见的结果(即,男性申请者比女性申请者更受青睐)可以归因于最初输入到算法中的训练数据集。假设技术职位的前员工大多数是男性,算法就预期这会成为一个成功因素。因此,歧视并不是从算法或其开发者个人开始的,而是从公司长期以来的招聘行为开始的。

  2. 变量之间的隐藏偏见 挑衅地说,有人可能会认为每个数据集都在某种程度上存在偏见,因为它源于一个可能存在偏见的环境。算法只能考虑(偏见的)数据中存在的内容,而数据通常仅包含(偏见的)非技术世界中的内容。一种合理且符合常识的方法是,在开始建模之前,将这些歧视性变量从数据集中移除。然而,移除一个变量并不能保证消除该变量对数据集其余部分的存在或影响。我们可以设想一个虚拟的数据集,其中包含变量性别运动。这两个变量可能存在互动,例如“男性与足球、女性与骑马之间的相关性”,尽管这个直白的例子可能不能完全代表我们的社会,因为可能有女性更喜欢足球而非骑马,反之亦然。然而,由于数据集的不平衡(例如男性偏多),算法可能会间接地偏向于有足球偏好的人,因为训练过程是基于偏见信息的。这个例子表明,移除关键变量可以降低歧视的风险,但不足以完全防止歧视。此外,数据集如何平衡地代表变量的不同表现也非常重要。如果表达均匀分布,敏感(歧视性)项目可能不那么问题严重。如果某一部分条目被低估,算法可能会在负面上考虑这一点。因此,很难确定一个数据集是否真正无偏。²¹

  3. 技术决策与人工决策

    没有保证人类做出的决策比算法更少偏见。除了任何数据集都可能存在偏见并导致算法过程中的偏见结果这一观点外,另一个关键的指标是不完整数据形式的信息缺乏。²² 很难保证给定的数据集具有完整的可视性来解释目标行为。数据缺乏的常见原因可能是有限的访问权限、主动排除信息或对额外信息边际影响的缺乏了解。歧视的另一风险不仅源于技术实现,还可能根植于我们社会的现有结构。大数据与机器学习算法的结合优势在于能够将人类规则和决策应用于大量信息。如果算法被认为是有偏见的,人们可能会怀疑人类如何处理给定数据的分析。回顾社交媒体广告,个性化广告旨在根据个人的偏好做出个体化建议。这意味着算法支持个体主义。然而,算法却有相反的效果,因为它们基于一般模式而非个体模式处理自动化和标准化的决策规则。最终,可以认为算法支持集体主义而非个体主义。²³

结论

我们的分析表明,算法中的偏见主要是当今社会偏见的结果。后者支持收集带有偏见的数据集,这些数据集可能包含(隐藏的)关系,最终导致偏见结果。此外,我们质疑人类决策是否比算法规则更少偏见。最后,我们得出结论,更复杂的算法往往过于不透明。

图片来源:Pixabay

缓解策略

在关于机器学习算法偏见结果的责任讨论中,开发者或数据科学团队常常被指责为歧视性行为的源头。一方面,这种说法过于简单化,责任分配需要进一步区分。另一方面,任何使用机器学习算法的开发者都应意识到他们任务的敏感性及其相应的责任。由于算法支持集体主义,调查这些集体算法是否对个人做出决策或建议的公平性是重要的。文献建议了其他减少偏见的技术,例如设计一个对小样本查询进行随机化的系统,或使用倾向加权技术。²⁴ 对于基于人类内容的偏见,有几种量化歧视和评估算法公平性的思路。²⁵

公平作为共同责任:在技术和现实世界中 数据团队有责任识别和最小化其数据模型中的偏见行为。然而,鉴于本文所考察的歧视性因素,确保没有人通过技术系统或人为行为经历这种不公正是一项全球责任。

  1. 人类必须平等对待他人,不带有歧视

  2. 算法必须平等对待人类,不带有歧视

  3. 我们不应借助算法来为偏见和歧视性的人类行为辩解。

展望:接下来怎么办?

评估决策或预测时,不仅应考虑结果本身,还需考虑在解决方案中涉及的伦理问题。我们还需要重新思考涉及偏见行为的系统。然而,没有证据表明替代方案(以人为基础的决策)会导致总体上的更多平等。因此,我们不应仅关注技术内部的偏见问题,而应将讨论扩大到技术外部的偏见问题。然而,正因为如此无偏见的系统应有助于改善我们当前的社会弱点,并可能带来未来世界中的更多平等。人们可以利用 AI 更容易地检测非技术世界中的歧视。因为只要我们在数据中发现偏见,社会中也存在偏见

Jonas Dieckmann - Medium

阅读 Jonas Dieckmann 在 Medium 上的文章。分析经理兼产品负责人 @ Philips | 对…充满热情并撰写关于…

Jonas Dieckmann - Medium

希望你觉得这些内容有趣。请告诉我你的想法,并随时在 LinkedIn www.linkedin.com/in/jonas-dieckmann 上联系我,或者在 medium 上关注我。

也可以查看我的其他文章:

如何使用 Keras API 和 Google Colab 开始使用 TensorFlow

使用神经网络分析人类活动的逐步教程

案例研究:将数据科学过程模型应用于现实世界场景

供应链中材料规划的机器学习模型开发

towardsdatascience.com

参考文献

[1]: Loukides, M., Mason, H., & Patil, D. (2018). 伦理与数据科学。

2: Floridi, L., & Taddeo, M. (2016). 什么是数据伦理? 皇家学会哲学学刊 A: 数学、物理与工程科学,374(2083),20160360. doi: 10.1098/rsta.2016.0360

3: Google (2021). 社会公益中的人工智能:将人工智能应用于全球最大挑战. 检索日期:2021 年 3 月 10 日,来源:ai.google/social-good

4 Mittelstadt, B., & Floridi, L. (2015). 大数据伦理:生物医学背景中的当前和可预见问题. 科学与工程伦理,22(2),303–341. DOI: 10.1007/s11948–015- 9652–2

5 Hand, D. (2018). 在变化的世界中数据伦理的各个方面:我们现在处于何种状态? 大数据,6(3),176–190. DOI: 10.1089/big.2018.0083

6 Broad, E., Smith, A., & Wells, P. (2017). 帮助组织应对其数据实践中的伦理问题. 开放数据研究所。

[7] 吴晓林,张熙. (2016). 使用面部图像的犯罪推断自动化。

[8] Sullivan, B. (2016). 一个新程序判断你是否是罪犯通过你的面部特征. 检索日期:2021 年 3 月 10 日,来源:www.vice.com/en/article/d7ykmw/new-programdecides-criminality-from-facial-features

[9] Angwin, J., Larson, J., Mattu, S., & Kirchner, L. (2016). 机器偏见. 检索日期:2021 年 2 月 28 日,来源:www.propublica.org/article/machine-bias-risk-assessments-in-criminal-sentencing

[10] Dastin, J. (2018). 亚马逊取消了一个对女性有偏见的秘密人工智能招聘工具. 检索日期:2021 年 2 月 28 日,来源:www.reuters.com/article/us-amazon-com-jobsautomation-insight-idUSKCN1MK08G

[11] Ali, M., Sapiezynski, P., Bogen, M., Korolova, A., Mislove, A., & Rieke, A. (2019). 通过优化进行歧视. ACM 人机交互学刊,3(CSCW),1–30. DOI: 10.1145/3359301

[12] Herdiana (2013), “大数据伦理的 5 项原则,” 化学信息与建模学刊. 检索日期:2021 年 3 月 10 日,来源:aiforceos.com/2018/06/16/big-data-ethics

[13] 加拿大人权委员会. 什么是歧视. 检索日期:2021 年 2 月 28 日,来源:www.chrc-ccdp.gc.ca/eng/content/what-discrimination

[14] Hartfiel, G., & Hillmann, K. (1994). 社会学词典. 斯图加特:Kröner

[15] 欧洲委员会(1950)。《保护人权和基本自由的欧洲公约》。检索日期:2021 年 3 月 10 日,来自www.refworld.org/docid/3ae6b3b04

[16] 欧洲经济和社会委员会。(2017)。大数据的伦理。

[17] Eslami, M., Vaccaro, K., Karahalios, K. 和 Hamilton, K.(2017)‘小心;事情可能比看起来更糟’:理解评级平台中的偏见算法和用户行为。《国际 AAAI 网络与社会媒体会议论文集》,11(1)。

[18] Kim, P.(2017)审计算法以防歧视,166 U. Pa. L. Rev. 检索日期:2021 年 3 月 10 日,来自 scholarship.law.upenn.edu/penn_law_review_online/vol166/iss1/10

[19] Mittelstadt, B., Allo, P., Taddeo, M., Wachter, S., & Floridi, L.(2016)。算法的伦理:辩论的映射。《大数据与社会》,3(2),205395171667967。DOI: 10.1177/2053951716679679

[20] Lehr, D., Ohm, P.(2017)。数据的游戏:法律学者应当了解的机器学习,《UCDL 评论》51:653,671。

[21] Calders, T., & Žliobaitė, I.(2013)。为何无偏计算过程可能导致歧视性决策程序。《应用哲学、认识论与理性伦理研究》,43–57。DOI: 10.1007/978–3–642–30487–3_3

[22] Rosen, R.(2019)。缺失数据和插补。检索日期:2021 年 3 月 10 日,来自towardsdatascience.com/missing-data-and-imputation-89e9889268c8

[23] Saake. I., Nassehi, A.(2004)。伦理的文化化。卢曼文化概念的时代诊断应用。法兰克福/美因:102,135

[24] Ridgeway, G., Kovalchik, S., Griffin, B., & Kabeto, M.(2015)。带权重数据的倾向得分分析。《因果推断期刊》,3(2),237–249。doi: 10.1515/jci-2014–0039

[25] Floridi, L.(2016)。无过错责任:关于分布式道德行为的性质和道德责任的分配。《皇家学会 A 类:数学、物理和工程科学哲学交易》,374(2083),20160112。doi: 10.1098/rsta.2016.0112

ETL vs ELT vs 流处理 ETL

原文:towardsdatascience.com/etl-elt-streaming-etl-af6379ffdd26

探索数据处理的批处理和实时设计范式

Giorgos MyrianthousTowards Data Science Giorgos Myrianthous

·发布于 Towards Data Science ·阅读时长 8 分钟·2023 年 8 月 29 日

--

图片由 Compare Fibre 提供,在 Unsplash

提取、转换、加载(ETL)和提取、加载、转换(ELT)是数据处理中的两个基本概念,用于描述数据摄取和转换设计范式。虽然这些术语经常被交替使用,但它们指代的概念略有不同,适用于不同的用例,这些用例也要求不同的设计。

在这篇文章中,我们将探讨 ETL 和 ELT 的异同,并讨论云计算和数据工程领域的变革如何影响数据处理设计模式。此外,我们将概述这两种方法在现代数据团队中的主要优缺点。最后,我们将讨论流处理 ETL,这是一种新兴的数据处理模式,旨在解决传统批处理方法的各种缺点。

订阅数据管道,这是一个专注于数据工程的新闻通讯

三个关注的步骤

从外部来源将数据摄取并持久化到目标系统涉及三个不同的步骤。

提取 “提取”步骤包括从源系统中提取数据所需的所有过程。这些源可以包括应用程序编程接口(API)、数据库系统或文件、物联网(IoT)设备,而数据可以是任何形式的;结构化、半结构化或非结构化。在此步骤中提取的数据通常被称为“原始数据”。

在 ETL/ELT 管道中作为‘提取’步骤的一部分从源系统中提取原始数据 — 来源:作者

转换 在‘转换’步骤中,管道对原始数据应用转换以实现某个目标。这个目标通常与业务或技术要求相关。一些常见的转换包括数据修改(例如,将United States映射为US)、记录或属性选择、与其他数据源的连接或甚至数据验证。

在 ETL/ELT 管道中的‘转换’步骤中,对原始数据应用转换以实现某个目标 — 来源:作者

加载 在‘加载’步骤中,数据(无论是原始数据还是已转换数据)被加载到目标系统中。通常,目标是 OLAP 系统(即数据仓库或数据湖),但有时它甚至可以是 OLTP 系统,例如应用程序数据库。

作为 ETL/ELT 管道中‘加载’步骤的一部分将数据加载到目标系统中 — 来源:作者

显然,ETL 和 ELT 范式都涉及上述步骤。它们的主要区别在于执行这些步骤的顺序,这反过来在成本、计算能力和设计模式上具有重要影响。因此,应根据每种范式的优缺点选择最合适的范式。还需要提到的是,团队结构本身也可能是选择哪种范式的良好指标。让我们深入探讨以使这更清楚!

提取 转换 加载(ETL)

ETL 指的是一个过程,包括从源系统(或文件)提取数据,然后进行转换步骤以修改提取的原始数据,最后是加载步骤将转换后的数据导入目标系统。

提取-转换-加载(ETL)管道中的执行顺序 — 来源:作者

ETL 步骤:

  1. 从源系统提取原始数据

  2. 在暂存服务器(即目标系统/服务器之外)转换数据

  3. 将数据加载到目标系统

ETL 已经存在多年,并在公司开始使用数据仓库将数据集中存储在其技术生态系统中的中心点时获得了显著关注。

ETL 在数据世界中仍然相关,但在批处理方面表现出两个显著的缺点。第一个缺点与原始数据永远无法到达目标系统有关。这意味着每当需要新的变换时,必须执行新的 ETL 作业来从源头(再次!)获取数据,应用新的变换规则,并最终加载到目标系统中。第二个主要缺点是变换通常发生在暂存服务器中。

提取加载变换(ELT)

随着云技术的出现和数据存储成本的显著降低,现代组织现在能够存储大量的原始数据,然后根据需要应用任何变换或分析。

技术环境的这些变化也改变了数据团队摄取和分析数据的方式。ELT 范式使团队能够将数据加载到目标系统,然后进行任何必要的变换。

提取-加载-变换(ELT)管道的执行顺序—来源:作者

ELT 步骤:

  1. 从源系统提取原始数据

  2. 将提取的原始数据加载到目标系统

  3. 在目标系统内变换原始数据

使用 ELT,不需要维护(和支付)用于变换的暂存服务器。此外,原始数据随时可用,这意味着进一步的变换可以瞬间完成。

流式 ETL

流式 ETL 指的是一个概念,其中 ETL 范式持续执行。当数据进入(作为“提取”步骤的一部分)时,变换—包括连接、映射、过滤和窗口化—会在流数据上执行。处理后的数据会推送到下游目标系统,如数据库、仓库或甚至消息队列。

尽管 ETL 和 ELT 范式仍然非常相关且有用,但它们总是作为批处理过程进行。这意味着延迟增加,而数据的新鲜度减少。另一方面,流式 ETL 相较于批处理作业具有许多优势,包括较低的延迟、较高的数据新鲜度以及更多的灵活性(无论是在资源使用还是数据处理本身)和可扩展性。

以欺诈检测为例,这适用于许多不同的行业,特别是金融服务。在银行业中,欺诈可能通过盗用卡片、伪造或甚至网络钓鱼电话进行。检测欺诈活动是时间关键的,因为金融机构需要在检测到后执行某些操作(例如冻结账户或发送通知)。尽管检测恶意交易活动时需要考虑许多因素,但一个明显且关键的因素是同一账户是否在几分钟内在不同的地理位置进行交易(这些地点的距离与感兴趣交易之间的时间差异不匹配)。为此,我们可以利用时间窗口。

显然,批处理——可以以 ELT 或 ELT 管道的形式执行——无法满足时间关键型用例如欺诈检测。而这正是流处理发挥作用的地方。

流处理

流处理指的是在数据创建时进行连续处理的实践。通常,流处理管道包括原始数据的摄取或生成,对其进行的处理,包括聚合、过滤、窗口化、丰富等,最终将处理后的流传送到目标系统。

流处理管道的高级概述 — 来源:作者

批处理已经长期存在,但全球数据生成的快速增长以及挑战批处理架构的技术出现改变了这一现状。

显然,流处理的主要优势在于其实时处理数据的能力。这使得用户和组织能够更快地进行分析,从而加快决策过程。

此外,流处理系统比批处理过程更容易扩展,因为它们设计用于实时处理大量数据。这对于包含波动数据流的用例尤为重要,例如物联网应用。

与批处理作业相比,流处理可以减少与数据存储相关的数据处理成本。流处理作业仅处理需要处理的数据子集,而无需存储和维护包含未使用数据的大型数据集。

使用流式 ETL 与流式数据库

有不同类型的流式数据库提供各种不同的功能并支持不同的 SQL 变体。其中一个流式数据库是 RisingWave,我将使用它作为示例来描述一些流式 ETL 概念,因为我熟悉 Postgres SQL。

RisingWave是一个用于流处理的分布式 SQL 数据库,旨在提供构建实时和事件驱动应用程序的无缝体验。

该工具可以处理来自 Apache Pulsar、Apache Kafka、Kinesis 和 CDC 源的数据。同样,来自 RisingWave 的数据可以被引入到下游系统,包括数据仓库、数据湖和消息代理/队列。

该平台可以用于从上述源中提取数据,随着新数据的到来执行增量计算,并动态更新结果。由于 RisingWave 本身是一个数据库系统,用户能够高效、有效地访问数据。

显然,RisingWave 支持的众多用例之一是流式 ETL。数据可以从各种源中提取并引入流式数据库,在那里可以应用转换。然后,处理/转换后的数据会持续写入所需的目标系统。

使用 RisingWave 的流式架构 — 来源:RisingWave GitHub 库(根据Apache 许可证授权)

RisingWave 使用的流式 ETL 步骤

  1. 提取:RisingWave 支持从各种数据源中提取数据,包括日志文件、消息队列和来自数据库日志的 CDC 流。

  2. 转换:它提供了执行流数据转换所需的所有功能,包括映射、聚合、过滤、窗口、连接和有状态处理。

  3. 加载:最后,处理过的数据流可以从 RisingWave 加载到下游目标系统。

一些可以从流式 ETL 功能中受益匪浅的用例包括:

  • 持续数据集成

  • 物联网(IoT)数据处理

  • 实时欺诈检测

  • 实时警报

另一种流式 ETL 的解决方案是 FlinkSQL,它允许用户使用 SQL 开发流式应用程序。Databricks 和 Snowflake 最近也开始提供对流式应用程序的支持,可能也是不错的替代选择。

最终思考…

设计数据处理管道是一项具有挑战性的任务,因为没有一种通用的解决方案。因此,应该熟悉基本概念,以便在实施解决特定问题的解决方案时能够做出有价值的考虑。

在今天的文章中,我们讨论了数据处理上下文中常用的两种范式,即 ETL 和 ELT。此外,我们还讨论了现代组织如何利用实时能力,并使用 RisingWave 实现事件驱动架构,RisingWave 是一个用于流处理的分布式 SQL 数据库,可以支持实时分析、事件驱动应用程序以及流式 ETL 等功能。

订阅数据管道,这是一个专注于数据工程的新闻通讯

如何使用 GitHub Actions 构建简单的 ETL 流水线

原文:towardsdatascience.com/etl-github-actions-cron-383f618704b6

ETL 不一定要复杂。如果是这样的话,使用 GitHub Actions。

Thuwarakesh MurallieTowards Data Science Thuwarakesh Murallie

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

--

照片由 Roman Synkevych 🇺🇦 拍摄,Unsplash 提供

如果你对软件开发感兴趣,你会知道 GitHub Actions 是什么。它是 GitHub 提供的一个自动化开发任务的工具。或者用流行的话来说,是一个 DevOps 工具。

但人们很少用它来构建 ETL 流水线。

讨论 ETL 时,第一个想到的工具是 Airflow、Prefect 或相关工具。毫无疑问,它们在任务编排方面是市场上最好的。但我们构建的许多 ETL 都很简单,托管一个单独的工具往往是多余的。

你可以改用 GitHub Actions。

本文重点介绍 GitHub Actions。但如果你使用 Bitbucket 或 GitLab,你也可以使用它们各自的替代方案。

## 使用 Black 和 GitHub Actions 维护干净的 Python 代码。

没有人想要一个混乱的代码库;也很少有人有耐心去清理它。

towardsdatascience.com

我们可以在 GitHub Actions 上运行我们的 Python、R 或 Julia 脚本。所以作为数据科学家,你不必为此学习一种新的语言或工具。你甚至可以在任何 ETL 任务失败时收到电子邮件通知。

如果你使用免费账户,你每月仍然可以享受 2000 分钟的计算时间。如果你能估算你的 ETL 工作负载在这个范围内,你可以尝试使用 GitHub Actions。

## 如何移除你不小心上传到 GitHub 的敏感数据。

那么如何避免它们被误入呢?——一种出乎意料的简单处理方法。

[towardsdatascience.com

我们如何开始在 GitHub Actions 上构建 ETL?

开始使用 GitHub Actions 很简单。你可以参考官方文档。或者按照以下三个简单步骤操作。

在你的代码库中,创建一个目录 .github/workflows。然后在其中创建一个名为 actions.yaml 的 YAML 配置文件,内容如下。

name: ETL Pipeline

on:
  schedule:
    - cron: '0 0 * * *'  # Runs at 12.00 AM every day

jobs:
  etl:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.9'

      - name: Extract data
        run: python extract.py

      - name: Transform data
        run: python transform.py

      - name: Load data
        run: python load.py

上述 YAML 自动化了一个 ETL(提取、转换、加载)管道。该工作流每天 UTC 时间凌晨 12:00 触发,由一个在 ubuntu-latest 环境中运行的单个任务组成(具体环境以当时可用的为准)。

这些配置步骤很简单。

该工作包含五个步骤:前两个步骤分别用于检查代码和设置 Python 环境,接下来的三个步骤按顺序执行 extract.pytransform.pyload.py Python 脚本。

该工作流提供了一种自动化和高效的方法,使用 GitHub Actions 每天提取、转换和加载数据。

## 我在几乎所有数据科学项目中使用的 5 个 Python 装饰器

装饰器提供了一种新的、方便的方法,用于缓存到发送通知等各种用途。

[towardsdatascience.com

Python 脚本可能会根据场景有所不同。这是众多方法中的一种。

# extract.py
# --------------------------------
import requests

response = requests.get("https://api.example.com/data")
with open("data.json", "w") as f:
    f.write(response.text)

# transform.py
# --------------------------------
import json

with open("data.json", "r") as f:
    data = json.load(f)

# Perform transformation
transformed_data = [item for item in data if item["key"] == "value"]

# Save transformed data
with open("transformed_data.json", "w") as f:
    json.dump(transformed_data, f)

# load.py
# --------------------------------
import json
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData

# Connect to database
engine = create_engine("postgresql://myuser:mypassword@localhost:5432/mydatabase")

# Create metadata object
metadata = MetaData()

# Define table schema
mytable = Table(
    "mytable",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("column1", String),
    Column("column2", String),
)

# Read transformed data from file
with open("transformed_data.json", "r") as f:
    data = json.load(f)

# Load data into database
with engine.connect() as conn:
    for item in data:
        conn.execute(
            mytable.insert().values(column1=item["column1"], column2=item["column2"])
        )

上述脚本从虚拟 API 读取数据并将其推送到 Postgres 数据库。

## Python 到 SQL — 我现在可以将数据加载速度提高 20 倍

上传大量数据的好方法、坏方法和丑陋的方法

[towardsdatascience.com

部署 ETL 管道到 GitHub Actions 时需要考虑的事项。

1. 安全性: 使用 GitHub 的秘密存储来保护你的秘密,避免将秘密硬编码到你的工作流中。

你是否已经注意到我上面给出的示例代码包含了数据库凭据?这在生产系统中是不对的。

我们还有其他安全嵌入秘密的方法,如数据库凭据。

如果你在 GitHub Actions 中不加密你的秘密,任何有权限访问代码库的人都能看到这些秘密。这意味着,如果攻击者获得了对代码库的访问权限或代码库的源代码泄露;攻击者将能够看到你的秘密值。

为了保护你的机密信息,GitHub 提供了一项名为加密机密的功能,这使你能够在仓库设置中安全地存储你的机密值。加密机密仅对授权用户可访问,并且在你的 GitHub Actions 工作流中从不以明文形式暴露。

下面是它的工作原理。

在仓库设置侧边栏中,你可以找到 Actions 的机密信息和变量。你可以在这里创建你的变量。

作者截图。

在这里创建的机密信息对任何人都不可见。它们是加密的,可以在工作流中使用。即使你也无法读取它们。但你可以用新的值更新它们。

一旦你创建了机密信息,你可以通过 GitHub Actions 配置将它们作为环境变量传递。下面是它的工作原理:

name: ETL Pipeline

on:
  schedule:
    - cron: '0 0 * * *'  # Runs at 12.00 AM every day

jobs:
  etl:
    runs-on: ubuntu-latest
    steps:
      ...

      - name: Load data
        env: # Or as an environment variable
          DB_USER: ${{ secrets.DB_USER }}
          DB_PASS: ${{ secrets.DB_PASS }}
        run: python load.py

现在,我们可以修改 Python 脚本以从环境变量中读取凭证。

# load.py
# --------------------------------
import json
import os
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData

# Connect to database
engine = create_engine(
    f"postgresql://{os.environ['DB_USER']}:{os.environ['DB_PASS']}@localhost:5432/mydatabase"
)

2. 依赖关系:确保使用正确版本的依赖项,以避免任何问题。

你的 Python 项目可能已经有一个 requirements.txt 文件,指定了依赖项及其版本。或者,对于更复杂的项目,你可能使用像 Poetry 这样的现代依赖管理工具。

在运行 ETL 的其他部分之前,你应该有一个步骤来设置你的环境。你可以通过在 YAML 配置中指定以下内容来做到这一点。

- name: Install dependencies
  run: pip install -r requirements.txt
  1. 时区设置:GitHub Actions 使用 UTC 时区,在撰写本文时,你不能更改它。

因此,你必须确保使用正确的时区。你可以使用在线转换工具或在配置之前手动将本地时间调整为 UTC。

GitHub Actions 调度的最大陷阱是其执行时间的不确定性。即使你已配置为在特定时间点运行,如果那时需求量很高,你的任务将被排队。因此,实际任务开始时间会有短暂的延迟。

如果你的任务依赖于准确的执行时间,使用 GitHub Actions 调度可能不是一个好的选择。在 GitHub Actions 中使用 自托管运行器 可能会有所帮助。

4. 资源使用: 避免过载 GitHub 提供的资源。

即使 GitHub Actions,即使是免费账户,也有 2000 分钟 的免费运行时间,但如果你使用的是与 Linux 不同的操作系统,规则会有所不同。

如果你使用的是 Windows 运行时,你只会获得一半的资源。在 MacOS 环境中,你只会获得十分之一的资源。

## 如何在每次提交时使用 GitHub Actions 运行 Python 测试?

自动化乏味的任务,并通过 CI 流水线确保你的代码质量。

towardsdatascience.com

结论

GitHub Actions 是一个 DevOps 工具,但我们可以用它来运行任何计划任务。在这篇文章中,我们讨论了如何创建一个定期获取 API 并将数据推送到数据框的 ETL。

对于简单的 ETL,这种方法易于开发和部署。

但是,GitHub Actions 中的计划任务不必在完全相同的时间运行。因此,对于时间敏感的任务,这种方法并不适用。

感谢阅读,朋友!在 LinkedInTwitterMedium 上跟我打个招呼吧。

还不是 Medium 会员?请使用此链接 成为会员,因为这样你无需额外支付费用,我可以通过推荐你赚取少量佣金。

欧洲旅行优化:遗传算法和 Google Maps API 解决旅行推销员问题

原文:towardsdatascience.com/euro-trip-optimization-genetic-algorithms-and-google-maps-python-api-solve-the-traveling-salesman-4ad8e1548207

利用遗传算法和 Google Maps API 探索欧洲 50 个最受欢迎城市的魅力,解锁高效的旅行路线

Riccardo AndreoniTowards Data Science Riccardo Andreoni

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

--

来源:unsplash.com

还记得看像 EuroTrip 这样的电影后那种感觉吗?电影中的角色在风景如画的欧洲城市中穿梭,经历了一次终生难忘的冒险。这种感觉真是迷人。然而,现实很快提醒我们:在多个目的地之间组织一次旅行绝非易事。但这里有一个令人兴奋的转折——凭借编程技能和对遗传算法的掌握,我开始开发解决方案。想象一下能够精准优化跨越数十个地点的复杂路线。这就是数据科学与冒险规划艺术交汇的地方。在这篇文章中,我将展示一个优雅解决复杂 旅行推销员问题(TSP)的算法脚本,承诺将帮助旅行规划并提升我们对数据科学中优化的理解。

阅读这篇文章将帮助你清晰地了解 Python、Google Maps API 和遗传算法如何协同工作,为复杂任务提供数据驱动的解决方案。

理解旅行推销员问题

开始一段旅行通常会激发冒险的感觉,但在我们思考旅行的复杂性时,兴奋感可能会伴随着物流挑战。旅行商问题(TSP)就是一个几十年来吸引数学家、计算机科学家和物流专家关注的挑战。TSP 的核心提出了一个看似简单的问题:给定一组城市及其之间的距离,什么是使得推销员能每个城市访问一次并返回起点的最短路径?虽然问题的陈述很简洁,但其含义远远超出了表面的简单。

在优化和物流领域,旅行商问题(TSP)不仅仅是一个理论上的好奇,它具有巨大的实际意义。以配送服务为例,减少旅行距离直接意味着降低燃料成本和加快服务速度。

在这个看似简单的问题陈述之下,隐藏着深刻的复杂性。TSP 的组合性质源于随着城市数量增加,潜在解决方案呈指数增长。可能的路径数量迅速激增,超出了任何计算的可行性,使得传统的暴力破解方法在较大实例中变得不切实际。可能的路径数量等于

其中 n 代表城市数量 — 一个 阶乘爆炸 的现象,很快变得压倒性。仅仅 50 个城市,可能的路径数量就等于 3*10⁶²,这大致相当于 银河系中的原子数量

TSP 作为数学、计算机科学与现实世界物流挑战之间引人入胜的交汇点的典型例子。随着城市数量的增加,揭示最短路径需要超越传统计算方法的创新策略。

对于 TSP 高效解决方案的追求驱使研究人员探索各种方法。其中之一是遗传算法,这是一类受自然选择过程启发的优化技术。遗传算法在导航复杂的解决空间方面表现出色,使其成为解决像 TSP 这样的问题的自然选择,因为随着城市数量的增加,暴力破解方法迅速变得不可行。

本文旨在探讨这两个领域的结合——旅行推销员问题和遗传算法。具体而言,我们深入探讨一个实际应用:一个旨在利用遗传算法解决旅行推销员问题的 Python 脚本。我们的探索将突出这种算法融合如何有可能改善旅行规划、物流以及跨行业的优化挑战。当我们了解基于遗传算法的解决方案的内部工作原理时,数据科学和算法创新的世界将汇聚,承诺在即便是最复杂的路径中提供新的见解和高效途径。

遗传算法简介

从本质上讲,遗传算法(GA)是一种受自然选择和进化优雅过程启发的启发式搜索技术。

遗传算法的灵感来源于查尔斯·达尔文的进化论。遗传算法通过迭代演化潜在解决方案的种群,模拟自然选择的过程。在这个数字熔炉中,表现出有利特征的解决方案会生存并繁殖,将它们的“基因”传递给下一代。这种代际进化会持续,直到达到最优或近似最优的解决方案。

遗传算法的特征包括四个基本组件:

  1. 选择:就像在自然界中一样,选择机制识别并青睐适应度更高的解决方案,类似于“适者生存”的概念。

  2. 交叉:解决方案或“染色体”交换遗传物质,产生具有父母特征混合的后代。

  3. 变异:为了引入多样性并防止过早收敛到次优解,遗传算法采用变异操作。此操作随机改变解决方案的某些元素,类似于自然界中的基因突变。

  4. 适应度评估:这是对每个解决方案的适应度的确定,量化它执行当前任务的效果。适应度函数通过为优质解决方案分配更高的繁殖概率来指导选择过程。

遗传算法在处理优化问题时表现出显著的多样性。它们以非线性、多维的方式探索解决方案空间,使其非常适合复杂的现实世界挑战。无论是优化复杂的工程设计、微调神经网络参数,还是如我们即将看到的解决 TSP,遗传算法在传统算法失败的场景中表现优异。

将遗传算法应用于旅行推销员问题

现在,让我们深入探讨遗传算法(GA)如何解决旅行推销员问题(TSP)。

从本质上讲,遗传算法通过将每个潜在路线视为种群中的个体来处理旅行推销员问题。这个路线种群在代际中进化,每条路线代表旅行推销员的独特行程。

为了促进这种遗传进化,我们将每条路线表示为一个染色体——定义访问顺序的城市序列。例如:

作者提供的图片。

基本任务是发现最优染色体,即最小化总旅行距离的序列。每个染色体的适应度通过评估它在指定顺序下访问城市时的总距离来量化。距离越短,适应度越高,这与寻找最短路径的目标相一致。

用 Python 实现

现在,让我们按照高层次的步骤实现设计用于解决 TSP 的 Python 脚本。完整的代码可以在我的GitHub 仓库中找到。

获取数据

第一步是选择目的地。以这个例子为例,我选择了欧洲 50 个最受欢迎的城市。一旦确定了目的地,我需要获取每对城市之间的旅行距离和时间。对于这种查询,Google Maps API 代表了最先进的解决方案。在这里设置一个帐户后,你可以请求你的个人 API 密钥,用于身份验证。

向 Google Maps API 发送请求的方式如下:

初始化

该过程开始时生成一组初始路线。这些路线通常是随机创建的,或者通过启发式方法生成的。

适应度评估与选择

在每一步中,在生成后代和突变一些路线之后,计算每条路线的总距离以评估其适应度。这个步骤确保算法保持对选择最短路径的关注。

在自然选择的精神下,路线根据其适应度被选择进行繁殖。总距离较短的路线——即接近最优解的路线——更有可能被选择,使得具有有利特征的个体更有可能繁殖。

交叉与突变

针对这个问题的特定特点,没有进行经典的交叉操作。我选择了两种突变方式:

  1. 单点突变:为了保持多样性并引入新颖的解决方案,算法对选定的路线进行小的、随机的变化。这模拟了遗传突变,引入了轻微的变异。

  2. “交叉突变”:通过切割其基因组的随机子集并将其附加到另一个位置来突变一个解决方案。用生物学术语来说,这是一种无性繁殖的形式。

迭代

上述步骤重复进行若干代,使得种群随着时间的推移不断进化。每次迭代都使算法更接近最优解或接近最优解。

算法将持续迭代直到满足终止标准。在这种情况下,终止标准是达到预定的代数。

结果与结论

在这次探索中,我使用了一个种群规模为 200 个个体的遗传算法,并运行了 1000 代来解决包含 50 个城市的旅行商问题(TSP)。从初始代到最终结果的旅程展示了基于遗传算法的方法的显著效率。

在开始阶段,即第零代,第一个解的适应度为 70,755 公里:

('Sofia, Bulgaria', 'Nice, France', ..., 'Naples, Italy', 'Luxembourg City, Luxembourg')

这个初始解,如预期的那样,代表了城市的随机排列,标志着算法的起点。然而,随着遗传算法在连续的代数中进行,我们观察到了解的质量的显著变化。

在 1000 代之后,遗传算法找到了其路线。最终解的适应度为 21,345 公里——相比初始的随机解,旅行距离有了显著减少。这一近 49,410 公里的显著改进突显了遗传算法在优化像 TSP 这样复杂路线中的有效性。

我进行了 4 次试验,改变了种群规模。总体而言,更大的种群获得了更好的结果,但计算时间显然更长。我们可以看到,在每次试验中,适应度值在前几次迭代中迅速下降,并在后期稳定在一个平台值。这是收敛算法的典型行为。

图片来自作者。

尽管 TSP 仍然是一个 NP 困难问题,这意味着对于较大的实例来说,找到绝对最优解可能在计算上具有挑战性,但遗传算法在接近最优解方面的能力在实际应用中证明了其不可或缺的价值。这一成就为更高效的旅行规划、优化物流以及跨行业的增强优化开辟了新的道路。这次实验突显了数据科学与创新算法之间的共生关系,强调了如何通过受自然选择机制启发的进化计算优雅地解决现实世界中的复杂问题。

如果你喜欢这个故事,考虑关注我,以便了解我即将发布的项目和文章!

以下是我过去的一些项目:

## 使用 Numpy 从头构建深度神经网络

现代深度学习库是强大的工具,但它们可能使从业者对神经网络的…

towardsdatascience.com ## 从头开始使用 Numpy 构建卷积神经网络

随着计算机视觉应用在我们生活中无处不在,理解其功能原理变得越来越重要…

towardsdatascience.com ## 选择适合你神经网络的优化算法

由于神经网络开发过程的本质是迭代的,我们需要利用每一个可能的捷径…

towardsdatascience.com

参考文献

[1]: 三目标最优 PMU 布置包括准确的状态估计:配电系统的案例

使用 RAGAs 评估 RAG 应用程序

原文:towardsdatascience.com/evaluating-rag-applications-with-ragas-81d67b0ee31a?source=collection_archive---------0-----------------------#2023-12-13

一个包含指标和 LLM 生成数据的框架,用于评估你的检索增强生成流水线的性能

Leonie MonigattiTowards Data Science Leonie Monigatti

·

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

--

检索增强生成的风格化性能仪表板

到目前为止,我们知道,为检索增强生成 (RAG) 应用程序构建概念验证是容易的,但使其准备好投入生产却非常困难。使 RAG 流水线的性能达到令人满意的状态尤其困难,因为 RAG 流水线中有多个不同的组件:

  • 检索器组件: 从外部数据库中检索附加上下文,以便 LLM 回答查询。

  • 生成器组件:基于经过检索信息的提示生成答案。

评估 RAG 管道时,必须分别和共同评估两个组件,以了解 RAG 管道是否以及在哪些方面仍需改进。此外,为了了解您的 RAG 应用的性能是否在提高,您必须进行定量评估。为此,您将需要两个要素:评估指标和评估数据集

目前,确定正确的评估指标和收集良好的验证数据是一个活跃的研究领域。由于这是一个迅速发展的主题,我们目前看到各种 RAG 评估框架的方法,如 RAG Triad of metricsROUGEARESBLEURAGAs [1]。本文将重点介绍如何使用 RAGAs [1] 评估 RAG 管道。

RAGAs 是什么

RAGAs(Retrieval-Augmented Generation Assessment)是一个框架(GitHubDocs),为您提供了评估 RAG 管道组件所需的必要要素。

评估数据

RAGAs 有趣的是,它起初是一个用于“无参考”评估的框架[1]。这意味着,RAGAs 在后台利用 LLMs 进行评估,而不需要依赖评估数据集中的人类标注实际标签。

评估 RAG 管道时,RAGAs 需要以下信息:

  • question:RAG 管道的输入用户查询。输入数据。

  • answer:来自 RAG 管道生成的答案。输出结果。

  • contexts:用于回答question的从外部知识源检索到的上下文。

  • ground_truthsquestion的实际答案。这是唯一的人类标注的信息。此信息仅在指标context_recall(见 Evaluation Metrics)中需要。

利用 LLMs 进行无参考评估是一个活跃的研究话题。虽然使用尽可能少的人类标注数据使其成为一种更便宜、更快的评估方法,但仍存在一些关于其缺陷的讨论,例如偏见3。然而,一些论文已经展示了有前景的结果4。有关详细信息,请参见 RAGAs [1]论文的“相关工作”部分。

请注意,该框架已经扩展,以提供需要实际标签的指标和范式(例如,context_recallanswer_correctness,见 Evaluation Metrics)。

此外,框架为 自动测试数据生成 提供了工具。

评估指标

RAGAs 提供了一些 指标 来评估 RAG 管道的组件级别以及端到端。

组件级别,RAGAs 为您提供了评估检索组件(context_relevancycontext_recall)以及生成组件(faithfulnessanswer_relevancy)的指标 2

  • 上下文精确度 衡量检索到的上下文的信噪比。该指标使用 questioncontexts 计算。

  • 上下文召回率 衡量是否检索到了回答问题所需的所有相关信息。该指标基于 ground_truth(这是框架中唯一依赖人工标注真实标签的指标)和 contexts 计算。

  • 忠实性 衡量生成答案的事实准确性。正确陈述的数量除以生成答案中的总陈述数量。该指标使用 questioncontextsanswer

  • 回答相关性 衡量生成的回答与问题的相关程度。该指标使用 questionanswer 计算。例如,对于问题“法国在哪里,它的首都是什么?”,回答“法国位于西欧。”会获得较低的回答相关性,因为它只回答了问题的一半。

所有指标都被缩放到 [0, 1] 范围内,较高的值表示更好的性能。

RAGAs 还为您提供了评估 RAG 管道 端到端 的指标,如 回答语义相似度回答正确性。本文关注于组件级别的指标。

使用 RAGAs 评估 RAG 应用程序

本节使用 RAGAs 评估一个最小的基础 RAG 管道,以展示如何使用 RAGAs,并提供对其评估指标的直观理解。

前提条件

确保您已安装所需的 Python 包:

  • langchainopenaiweaviate-client 用于 RAG 管道

  • ragas 用于评估 RAG 管道

#!pip install langchain openai weaviate-client ragas

此外,请在根目录下的 .env 文件中定义相关的环境变量。要获取 OpenAI API 密钥,您需要一个 OpenAI 账户,然后在 API keys 下选择“创建新密钥”。

OPENAI_API_KEY="<YOUR_OPENAI_API_KEY>"

设置 RAG 应用程序

在你可以评估你的 RAG 应用之前,你需要进行设置。我们将使用一个基础的 RAG 流水线。我们将简要介绍这一部分,因为我们将在接下来的文章中详细描述相同的设置。

检索增强生成(RAG):从理论到 LangChain 实现

从原始学术论文的理论到使用 OpenAI、Weaviate 和 LangChain 的 Python 实现

towardsdatascience.com

首先,你必须通过加载和分块文档来准备数据。

import requests
from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter

url = "https://raw.githubusercontent.com/langchain-ai/langchain/master/docs/docs/modules/state_of_the_union.txt"
res = requests.get(url)
with open("state_of_the_union.txt", "w") as f:
    f.write(res.text)

# Load the data
loader = TextLoader('./state_of_the_union.txt')
documents = loader.load()

# Chunk the data
text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = text_splitter.split_documents(documents)

接下来,为每个块生成向量嵌入,并将其存储在向量数据库中。

from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Weaviate
import weaviate
from weaviate.embedded import EmbeddedOptions
from dotenv import load_dotenv,find_dotenv

# Load OpenAI API key from .env file
load_dotenv(find_dotenv())

# Setup vector database
client = weaviate.Client(
  embedded_options = EmbeddedOptions()
)

# Populate vector database
vectorstore = Weaviate.from_documents(
    client = client,    
    documents = chunks,
    embedding = OpenAIEmbeddings(),
    by_text = False
)

# Define vectorstore as retriever to enable semantic search
retriever = vectorstore.as_retriever()

最后,设置一个提示模板和 OpenAI LLM,并将它们与检索组件组合成一个 RAG 流水线。

from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser

# Define LLM
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

# Define prompt template
template = """You are an assistant for question-answering tasks. 
Use the following pieces of retrieved context to answer the question. 
If you don't know the answer, just say that you don't know. 
Use two sentences maximum and keep the answer concise.
Question: {question} 
Context: {context} 
Answer:
"""

prompt = ChatPromptTemplate.from_template(template)

# Setup RAG pipeline
rag_chain = (
    {"context": retriever,  "question": RunnablePassthrough()} 
    | prompt 
    | llm
    | StrOutputParser() 
)

准备评估数据

由于 RAGAs 旨在成为一个无参考的评估框架,因此评估数据集的准备工作最少。你需要准备 questionground_truths 对,从中你可以通过推断准备其余信息,如下所示:

from datasets import Dataset

questions = ["What did the president say about Justice Breyer?", 
             "What did the president say about Intel's CEO?",
             "What did the president say about gun violence?",
            ]
ground_truths = [["The president said that Justice Breyer has dedicated his life to serve the country and thanked him for his service."],
                ["The president said that Pat Gelsinger is ready to increase Intel's investment to $100 billion."],
                ["The president asked Congress to pass proven measures to reduce gun violence."]]
answers = []
contexts = []

# Inference
for query in questions:
  answers.append(rag_chain.invoke(query))
  contexts.append([docs.page_content for docs in retriever.get_relevant_documents(query)])

# To dict
data = {
    "question": questions,
    "answer": answers,
    "contexts": contexts,
    "ground_truths": ground_truths
}

# Convert dict to dataset
dataset = Dataset.from_dict(data)

如果你对 context_recall 指标不感兴趣,你不需要提供 ground_truths 信息。在这种情况下,你只需准备 question

评估 RAG 应用

首先,从 ragas.metrics 导入你想使用的所有指标。然后,你可以使用 evaluate() 函数并简单地传入相关指标和准备好的数据集。

from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_recall,
    context_precision,
)

result = evaluate(
    dataset = dataset, 
    metrics=[
        context_precision,
        context_recall,
        faithfulness,
        answer_relevancy,
    ],
)

df = result.to_pandas()

以下是示例的 RAGAs 评分结果:

RAGAs 评分包括上下文精确度、上下文召回率、准确性和答案相关性。

我们可以做出以下观察:

  • context_relevancy(检索到的上下文的信噪比):虽然 LLM 认为所有上下文对最后一个问题是相关的,但也认为第二个问题的多数检索上下文是不相关的。根据这个指标,你可以尝试不同数量的检索上下文,以减少噪声。

  • context_recall(是否检索到回答问题所需的所有相关信息):LLMs 评估检索到的上下文包含回答问题所需的相关信息。

  • faithfulness(生成答案的事实准确性):虽然 LLM 认为第一个和最后一个问题回答正确,但对第二个问题的回答(错误地说总统没有提到 Intel 的 CEO)则被评为 0.5。

  • answer_relevancy(生成答案与问题的相关性):所有生成的答案都被评估为与问题较为相关。

如在评估数据中提到的,使用 LLMs 进行无参考评估是一个活跃的研究领域。我很想看看这个话题会如何发展。

摘要

构建概念验证 RAG 应用程序很简单,但让其性能达到生产就绪水平则很困难。像机器学习项目一样,你应通过验证数据集和评估指标来评估 RAG 管道的性能。

然而,由于 RAG 管道由多个组件组成,必须分别以及组合地进行评估,你将需要一套评估指标。此外,从人工注释者那里生成高质量的验证数据集既困难又耗时且昂贵。

本文介绍了RAGAs [1] 评估框架。该框架提出了四个评估指标——context_relevancycontext_recallfaithfulnessanswer_relevancy——这些指标共同组成了 RAGAs 评分。此外,RAGAs 利用 LLMs 进行无参考评估以节省成本。

现在你已经具备了评估 RAG 应用性能的工具,我推荐建立实验管道并开始使用以下调优策略来优化性能:

## 关于生产就绪 RAG 应用的 12 种调优策略指南

如何通过这些“超参数”提升你的检索增强生成(RAG)管道的性能……

towardsdatascience.com

你可以在这个 GitHub 仓库中找到生成这个数据集的代码。

享受这篇文章了吗?

免费订阅 以便在我发布新故事时收到通知。

[## 关注 Leonie Monigatti 的每次发布即可收到电子邮件。

关注 Leonie Monigatti 的每次发布即可收到电子邮件。通过注册,如果你还没有 Medium 账户,你将创建一个…

medium.com](https://medium.com/@iamleonie/subscribe?source=post_page-----81d67b0ee31a--------------------------------)

可以在 LinkedInTwitter以及 Kaggle找到我!

免责声明

在撰写本文时,我是 Weaviate 的一名开发者倡导者,Weaviate 是一个开源向量数据库。

参考文献

[1] Es, S., James, J., Espinosa-Anke, L., & Schockaert, S. (2023). RAGAs: 自动化检索增强生成的评估。arXiv 预印本 arXiv:2309.15217

2 RAGAs 文档 (2023)。文档(访问日期:2023 年 12 月 11 日)

3 Wang, P., Li, L., Chen, L., Zhu, D., Lin, B., Cao, Y., … & Sui, Z. (2023). 大型语言模型并不是公平的评估者。arXiv 预印本 arXiv:2305.17926

4 Liu, Y., Iter, D., Xu, Y., Wang, S., Xu, R., & Zhu, C. (2023). G-eval: 使用 GPT-4 进行 Nlg 评估,提供更好的人工对齐,2023 年 5 月。arXiv 预印本 arXiv:2303.16634, 6

图像

如果没有特别说明,所有图像均由作者创建。

通过图表和指标评估简单回归模型的性能

原文:towardsdatascience.com/evaluating-the-performance-of-a-simple-regression-model-a671a1281f15

温迪·纳瓦雷特Towards Data Science 温迪·纳瓦雷特

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

--

Pixabay 许可

几年前,当我创建我的第一个简单线性回归模型时,我对如何评估我的模型甚至是否是一个好的预测器没有清晰的认识。现在,我决定写这篇文章,集中讨论我用来确定简单线性回归模型优劣的行动项。你将找到简单线性回归的基础和假设的简要回顾,然后是检查模型的待办事项列表。

简单线性回归是什么

它是检查两个变量之间关系的最简单回归分析模型:一个因变量(也称为响应、目标或结果)和一个自变量(也称为解释或预测变量)。这种关系的估计可以在图中呈现为一条直线。

简单回归的主要目标是:

  • 确定变量之间的关系

  • 基于单一自变量x预测因变量Y

  • 测试关联关系的假设

模型制定

作者提供的图片

目标是找到表示线性关系的直线,该直线通过最小化预测值和实际值之间的平方差之和来确定。为此,我们需要估计β0 和β1 参数。β0 表示截距,即当自变量x为零时因变量Y的期望值,β1 表示斜率,即x变化一个单位时Y期望值的变化。

线性回归假设

重要的是回顾线性回归假设,因为对这些假设的良好理解将有助于评估模型。

  • 线性假设 意味着误差或偏差的期望值为零。

  • 常数方差假设同方差性 意味着残差的方差在所有自变量水平上应该是恒定的。简单来说,模型在某些部分不能比其他部分更准确。

  • 独立性 假设 意味着误差是独立的随机变量。

  • 正态性假设 意味着误差假设为正态分布。

评估模型的有效性和性能

评估线性简单回归模型时,有一些关键的指标和图表可以考虑。让我们来回顾一下:

计算均方误差(MSE)或均方根误差(RMSE)

这些指标量化了实际值和预测值之间的平均平方差。较低的 MSE 或 RMSE 值表示预测准确度更高。

验证决定系数(R 平方)

R 平方是因变量总变异性中可以由自变量在回归模型中解释的比例。其范围从 0 到 1,值越接近 1 表明拟合度越好。

作者提供的图片

分析系数的 p 值

通过检查系数的相应 p 值来评估系数的显著性。较低的 p 值表明预测变量对响应变量有统计学上的显著影响。

作者提供的图片

计算变量之间的相关系数

它衡量两个变量之间线性关系的强度和方向,并提供它们之间线性的指示。相关系数的绝对值越高,表明线性关系的符合度越高。相关系数的平方实际上是 R 平方。

作者提供的图片

绘制残差与预测值的关系图

理想情况下,残差应随机分布在零附近,这表明模型很好地捕捉了基本关系,误差项的方差是恒定的,见下图左侧。右侧的图显示了喇叭效应,这意味着常数方差(同方差性)假设不成立。

作者提供的图片

创建 QQ(分位数)图

残差的分位数将与正态分位数沿直线对齐。如果图形没有显示直线,则正态性假设不成立,这表明数据可能有偏态分布或重尾分布。下面有两个 QQ 图的例子,我们期望模型的结果是左侧的图。

作者提供的图片

创建残差的直方图

直方图常用于观察残差分布的形状,并验证正态性假设是否成立,例如下面的图展示了误差的正态分布形状,这是我们期望从模型中看到的。

图片由作者提供

结论

  • 如果某些假设不成立,那么模型的拟合是不充分的,但这并不意味着回归分析没有用。

  • 这些假设的违反可能会影响回归结果的有效性和可靠性。

  • 最后,在我们想要解决的特定问题的背景下解释结果是相当重要的。

评估提升模型

原文:towardsdatascience.com/evaluating-uplift-models-8a078996a113

因果数据科学

如何比较和选择最佳提升模型

Matteo CourthoudTowards Data Science Matteo Courthoud

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

--

封面,图片由作者提供。

因果推断在行业中最广泛的应用之一是提升建模,即条件平均处理效应的估计。

在估计处理(药物、广告、产品等)对结果(疾病、公司收入、客户满意度等)的因果效应时,我们通常不仅仅关心处理是否有效,我们还希望了解对哪些对象(患者、用户、客户等)效果更好或更差。

估计异质性增量效应,或称为提升,是改善目标政策的重要中间步骤。例如,我们可能希望警告某些人他们更有可能经历药物的副作用,或仅向特定客户展示广告。

虽然存在许多建模提升的方法,但在具体应用中并不总是清楚使用哪一种。关键在于,由于因果推断的根本问题,感兴趣的目标,即提升,永远无法观察到,因此我们无法像处理机器学习预测算法一样验证我们的估计器。我们不能划分验证集并选择表现最好的模型,因为我们没有真实值,即使在验证集中也没有,即使我们进行了随机实验也无法获得。

那我们该怎么办呢?在这篇文章中,我尝试介绍最常用的提升模型评估方法。如果你对提升模型不太熟悉,我建议先阅读我的介绍文章。

## 理解元学习者

编辑描述

towardsdatascience.com

提升和促销邮件

想象一下我们在一家产品公司的市场部门工作,致力于改进我们的电子邮件营销活动。从历史上看,我们主要向新客户发送邮件。然而,现在我们希望采用数据驱动的方法,针对那些邮件对收入有最大正面影响的客户。这种影响也称为提升增量性

让我们看看我们可以使用的数据。我从[src.dgp](https://github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/src/dgp.py)导入数据生成过程dgp_promotional_email()。我还从[src.utils](https://github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/src/utils.py)导入了一些绘图函数和库。

from src.utils import *
from src.dgp import dgp_promotional_email
dgp = dgp_promotional_email(n=500)
df = dgp.generate_data()
df.head()

数据快照,图片来源:作者

我们有 500 名客户的信息,我们观察到他们是否是客户,他们的年龄,邮件活动前产生的销售额(sales_old),是否发送了邮件,以及邮件活动后的销售

感兴趣的结果销售,我们用字母Y表示。处理或我们希望改进的政策是邮件活动,我们用字母W表示。我们将所有其他变量称为混杂因素或控制变量,用X表示。

Y = 'sales'
W = 'mail'
X = ['age', 'sales_old', 'new']

表示变量之间因果关系的有向无环图(DAG)如下。关注的因果关系以绿色显示。

数据生成过程的有向无环图(DAG)

从 DAG 中,我们看到客户指标是一个混杂因素,需要控制以识别邮件销售的影响。年龄sales_old则不是估计的关键,但可能对识别有帮助。有关 DAG 和控制变量的更多信息,请查看我的介绍文章。

## DAGs 和控制变量

编辑描述

towardsdatascience.com

提升建模的目标是恢复个体处理效应(ITE) τᵢ,即发送促销邮件销售的增量效应。我们可以将 ITE 表示为两个假设量之间的差异:客户接收到邮件的潜在结果 Yᵢ⁽¹⁾,减去客户如果没有收到邮件的潜在结果 Yᵢ⁽⁰⁾

个体处理效应(ITE),图片来源:作者

请注意,对于每位客户,我们只能观察到两个实现结果中的一个,具体取决于他们是否实际收到了mail。因此,ITE 本质上是不可观测的。可以估计的是条件平均处理效应 (CATE),即在协变量 X 条件下的期望个体处理效应 τᵢ。例如,对年龄大于 50 岁的客户 (age > 50) 来说,mailsales 的平均效应。

条件平均处理效应 (CATE),图像由作者提供

为了能够恢复 CATE,我们需要做出三个假设。

  1. 无混淆Y⁽⁰⁾, Y⁽¹⁾ ⊥ W | X

  2. 重叠0 < e(X) < 1

  3. 一致性Y = W ⋅ Y⁽¹⁾ + (1−W) ⋅ Y⁽⁰⁾

其中 e(X)倾向得分,即在协变量 X 条件下的预期处理概率。

倾向得分,图像由作者提供

接下来,我们将使用机器学习方法来估计 CATE τ(x)、倾向得分 e(x) 以及结果的条件期望函数 (CEF) μ(x)

结果条件期望函数 (CEF),图像由作者提供

我们使用 随机森林回归 算法来建模 CATE 和结果 CEF,而使用 逻辑回归 来建模倾向得分。

from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import LogisticRegressionCV

model_tau = RandomForestRegressor(max_depth=2)
model_y = RandomForestRegressor(max_depth=2)
model_e = LogisticRegressionCV()

在本文中,我们不对基础机器学习模型进行微调,但强烈建议进行微调以提高提升模型的准确性(例如,使用像 FLAML 这样的 auto-ml 库)。

提升模型

存在许多方法来建模提升,或者换句话说,估计条件平均处理效应 (CATE)。由于本文的目的是比较方法以评估提升模型,我们将不详细解释这些方法。对于温和的介绍,您可以查看 我关于元学习者的介绍文章

我们将考虑的学习者如下:

我们从微软的 econml 库导入所有模型。

from src.learners_utils import *
from econml.metalearners import SLearner, TLearner, XLearner
from econml.dml import NonParamDML
from econml.dr import DRLearner

S_learner = SLearner(overall_model=model_y)
T_learner = TLearner(models=clone(model_y))
X_learner = XLearner(models=model_y, propensity_model=model_e, cate_models=model_tau)
R_learner = NonParamDML(model_y=model_y, model_t=model_e, model_final=model_tau, discrete_treatment=True)
DR_learner = DRLearner(model_regression=model_y, model_propensity=model_e, model_final=model_tau)

我们在数据上fit()模型,指定结果变量 Y、处理变量 W 和协变量 X

names = ['SL', 'TL', 'XL', 'RL', 'DRL']
learners = [S_learner, T_learner, X_learner, R_learner, DR_learner]
for learner in learners:
    learner.fit(df[Y], df[W], X=df[X])

我们现在准备评估这些模型了!我们应该选择哪个模型?

Oracle 损失函数

评估提升模型的主要问题是,即使有验证集,甚至有随机实验或 AB 测试,我们也未观察到我们感兴趣的指标:个体治疗效应。事实上,我们只观察到实现的结果,Yᵢ⁽⁰⁾ 对于未处理的客户,Yᵢ⁽¹⁾ 对于处理过的客户。因此,我们无法计算验证数据中任何客户的个体治疗效应,τᵢ = Yᵢ⁽¹⁾ − Yᵢ⁽⁰⁾

我们还可以做些什么来评估我们的估计器吗?

答案是肯定的,但在给出更多细节之前,让我们首先了解如果我们能够观察到个体治疗效应 τᵢ 我们会怎么做。

Oracle MSE 损失

如果我们能够观察个体治疗效应(但我们不能,因此有“oracle”属性),我们可以尝试衡量我们的估计 τ̂(Xᵢ) 距离真实值 τᵢ 的距离。这就是我们在机器学习中评估预测方法时通常做的:我们保留一个验证数据集,并在该数据上比较预测值和真实值。有很多损失函数可以评估预测准确性,因此让我们集中于最流行的一个:均方误差(MSE)损失

Oracle MSE 损失函数,作者图

def loss_oracle_mse(data, learner):
    tau = learner.effect(data[X])
    return np.mean((tau - data['effect_on_sales'])**2)

函数 compare_methods 打印和绘制在单独验证数据集上计算的评估指标。

def compare_methods(learners, names, loss, title=None, subtitle='lower is better'):
    data = dgp.generate_data(seed_data=1, seed_assignment=1, keep_po=True)
    results = pd.DataFrame({
        'learner': names,
        'loss': [loss(data.copy(), learner) for learner in learners]
    })
    fig, ax = plt.subplots(1, 1, figsize=(6, 4))
    sns.barplot(data=results, x="learner", y='loss').set(ylabel='')
    plt.suptitle(title, y=1.02)
    plt.title(subtitle, fontsize=12, fontweight=None, y=0.94)
    return results

results = compare_methods(learners, names, loss_oracle_mse, title='Oracle MSE Loss')

Oracle MSE 损失值,作者图

在这种情况下,我们看到 T-学习者表现最差,S-学习者紧随其后。另一方面,X-、R-和 DR-学习者表现明显更好,DR-学习者赢得了比赛

然而,这可能不是评估我们提升模型的最佳损失函数。实际上,提升建模只是通向我们最终目标的一步:提高收入。

Oracle 策略增益

由于我们最终的目标是提高收入,我们可以通过它们增加收入的多少来评估估计器,给定某个策略函数。例如,假设我们发送一封电子邮件的成本是 0.01\(。那么,我们的策略是处理每个预测的条件平均治疗效应高于 0.01\)的客户。

cost = 0.01

我们的收入实际会增加多少?让我们用 d(τ̂) 定义我们的策略函数,其中 d=1 如果 τ ≥ 0.1,否则 d=0。然后我们的增益(越高越好)函数是:

Oracle 策略增益函数,作者图

再次,这是一种“oracle”损失函数,在现实中无法计算,因为我们无法观察到个体治疗效应。

def gain_oracle_policy(data, learner):
    tau_hat = learner.effect(data[X])
    return np.sum((data['effect_on_sales'] - cost) * (tau_hat > cost))

results = compare_methods(learners, names, gain_oracle_policy, title='Oracle Policy Gain', subtitle='higher is better')

Oracle policy gain values, image by Author

在这种情况下,S-学习者显然是表现最差的,导致收入没有效果。T-学习者带来了适度的收益,而 X-、R-和 DR-学习者都带来了整体收益,其中X-学习者稍微领先

实用损失函数

在前一节中,我们已经看到两个损失函数的例子,如果我们能观察到个体治疗效应τᵢ,我们希望计算这些损失函数。然而,实际上,即使在随机实验和验证集的情况下,我们也不能观察到 ITE,这是我们的兴趣对象。我们现在将介绍一些在这种实际限制下尝试评估提升模型的度量方法。

结果损失

第一种也是最简单的方法是切换到不同的损失变量。虽然我们不能观察个体治疗效应τᵢ,但我们仍然可以观察我们的结果Yᵢ。这不是我们确切的兴趣对象,但我们可以期望一个在预测y方面表现良好的提升模型也会提供良好的τ估计。

一种这样的损失函数可能是结果 MSE 损失,这是预测方法中通常使用的 MSE 损失函数。

结果 MSE 损失函数,图像作者

问题在于,并非所有模型都直接产生μ(x)的估计。因此,我们跳过这个比较,转而使用可以评估任何提升模型的方法。

预测到预测损失

另一种非常简单的方法是将训练集上训练的模型的预测与在验证集上训练的另一个模型的预测进行比较。虽然直观,但这种方法可能会极具误导性

def loss_pred(data, learner):
    tau = learner.effect(data[X])
    learner2 = copy.deepcopy(learner).fit(data[Y], data[W], X=data[X])
    tau2 = learner2.effect(data[X])
    return np.mean((tau - tau2)**2)
results = compare_methods(learners, names, loss_pred, 'Prediction to Prediction Loss')

预测到预测 MSE 损失值,图像作者

不出所料,这一指标表现极差,你绝不应该使用它,因为它奖励那些一致的模型,而不考虑它们的质量。一个始终为每个观测预测一个随机常数 CATE 的模型将获得完美的得分。

分布损失

另一种方法是问:我们能多好地匹配潜在结果的分布?我们可以对处理组未处理组的潜在结果进行这个练习。以最后一种情况为例。假设我们对那些没有收到邮件的客户的观察到的sales以及对那些收到邮件的客户的观察到的sales 减去 估计的 CATE τ̂(x) 进行比较。根据无混杂假设,在协变量X的条件下,这两个未处理潜在结果的分布应该是相似的。

因此,如果我们正确估计了治疗效应,我们期望两个分布之间的距离较近。

未处理潜在结果距离,图像作者

我们还可以对处理过的潜在结果做同样的分析。

处理后的潜在结果距离,图像由作者提供

我们使用能量距离作为距离度量。

from dcor import energy_distance

def loss_dist(data, learner):
    tau = learner.effect(data[X])
    data.loc[data.mail==1, 'sales'] -= tau[data.mail==1]
    return energy_distance(data.loc[data.mail==0, [Y] + X], data.loc[data.mail==1, [Y] + X], exponent=2)
results = compare_methods(learners, names, loss_dist, 'Distribution Loss')

未处理的潜在结果距离值,图像由作者提供

该测量非常嘈杂,奖励了 S-学习者,其次是 T-学习者,这实际上是两个表现最差的模型。

上下中位数差异

上下中位数损失试图回答一个问题:我们的提升模型是否检测到任何异质性?特别是,如果我们取验证集并将样本分为中位数以上和中位数以下预测提升τ̂(x),实际的平均效果差异有多大,用均值差异估计?我们期望更好的估计器能更好地区分高效和低效。

from statsmodels.formula.api import ols 

def loss_ab(data, learner):
    tau = learner.effect(data[X]) + np.random.normal(0, 1e-8, len(data))
    data['above_median'] = tau >= np.median(tau)
    param = ols('sales ~ mail * above_median', data=data).fit().params[-1]
    return param
results = compare_methods(learners, names, loss_ab, title='Above-below Median Difference', subtitle='higher is better')

上下中位数差异增益值,图像由作者提供

不幸的是,上下中位数差异奖励了 T-学习者,它在表现最差的模型中排名靠前。

需要注意的是,即使数据来自随机实验,两组(中位数以上和中位数以下τ̂(x))的均值差异估计器也不能保证无偏。事实上,我们在一个高度内生的变量τ̂(x)上将两组进行分割。因此,方法应保持谨慎。

提升曲线

上下中位数检验的扩展是提升曲线。其思想很简单:与其根据中位数(0.5 分位数)将样本分成两组,不如将数据分成更多组(更多分位数)?

对于每个组,我们计算均值差异估计,并将其累积和与相应的分位数进行绘图。结果称为提升曲线。解释很简单:曲线越高,我们就越能有效地区分高效和低效观察值。然而,同样的免责声明适用:均值差异估计并不保证无偏。因此,使用时应保持谨慎。

def generate_uplift_curve(df):
    Q = 20
    df_q = pd.DataFrame()
    data = dgp.generate_data(seed_data=1, seed_assignment=1, keep_po=True)
    ate = np.mean(data[Y][data[W]==1]) - np.mean(data[Y][data[W]==0])
    for learner, name in zip(learners, names):
        data['tau_hat'] = learner.effect(data[X])
        data['q'] = pd.qcut(-data.tau_hat + np.random.normal(0, 1e-8, len(data)), q=Q, labels=False)
        for q in range(Q):
            temp = data[data.q <= q]
            uplift = (np.mean(temp[Y][temp[W]==1]) - np.mean(temp[Y][temp[W]==0])) * q / (Q-1)
            df_q = pd.concat([df_q, pd.DataFrame({'q': [q], 'uplift': [uplift], 'learner': [name]})], ignore_index=True)

    fig, ax = plt.subplots(1, 1, figsize=(8, 5))
    sns.lineplot(x=range(Q), y=ate*range(Q)/(Q-1), color='k', ls='--', lw=3)
    sns.lineplot(x='q', y='uplift', hue='learner', data=df_q);
    plt.suptitle('Uplift Curve', y=1.02, fontsize=28, fontweight='bold')
    plt.title('higher is better', fontsize=14, fontweight=None, y=0.96)

generate_uplift_curve(df)

提升曲线,图像由作者提供

虽然可能不是评估提升模型的最佳方法,但提升曲线在理解实施这些模型中非常重要。实际上,对于每个模型,它告诉我们在增加处理人群比例(x 轴)时,预期的平均处理效果(y 轴)是什么。

最近邻匹配

我们分析的最后几种方法使用了聚合数据来理解这些方法在更大群体中的效果。最近邻匹配方法则试图了解提升模型如何预测个体治疗效果。然而,由于 ITE 不可观察,它试图通过在可观察特征X上匹配处理组和对照组观察值来建立一个代理

例如,如果我们取所有处理组观察值(i: Wᵢ=1),并在对照组中找到最近邻(NN₀(Xᵢ)),则相应的均方误差损失函数为

最近邻损失函数,图片由作者提供

from scipy.spatial import KDTree

def loss_nn(data, learner):
    tau_hat = learner.effect(data[X])
    nn0 = KDTree(data.loc[data[W]==0, X].values)
    control_index = nn0.query(data.loc[data[W]==1, X], k=1)[-1]
    tau_nn = data.loc[data[W]==1, Y].values - data.iloc[control_index, :][Y].values
    return np.mean((tau_hat[data[W]==1] - tau_nn)**2)

results = compare_methods(learners, names, loss_nn, title='Nearest Neighbor Loss')

最近邻损失值,图片由作者提供

在这种情况下,最近邻损失表现得相当好,识别出了两个表现最差的方法,即 S-学习者和 T-学习者。

IPW 损失

逆概率加权(IPW)损失函数首次由Gutierrez, Gerardy (2017)提出,它是我们将要看到的三种指标中的第一种,这些指标使用伪结果 Y 来评估估计器。伪结果是其期望值为条件平均治疗效应的变量,但由于其波动性太大,不能直接用作估计。有关伪结果的详细解释,我建议阅读我关于因果回归树的文章。IPW 损失对应的伪结果为

IPW 伪结果,图片由作者提供

使得相应的损失函数为

IPW 损失函数,图片由作者提供

def loss_ipw(data, learner):
    tau_hat = learner.effect(data[X])
    e_hat = clone(model_e).fit(data[X], data[W]).predict_proba(data[X])[:,1]
    tau_gg = data[Y] * (data[W] - e_hat) / (e_hat * (1 - e_hat))
    return np.mean((tau_hat - tau_gg)**2)

results = compare_methods(learners, names, loss_ipw, title='IPW Loss')

IPW 损失函数值,图片由作者提供

IPW 损失函数非常嘈杂。一种解决方案是使用其更稳健的变体,即我们接下来介绍的 R 损失或 DR 损失。

R 损失

R 损失与 R-学习者一起由Nie, Wager (2017)引入,它本质上是 R-学习者的目标函数。与 IPW 损失一样,这个方法的想法是匹配一个伪结果,其期望值为条件平均治疗效应。

R 伪结果,图片由作者提供

相应的损失函数为

R 损失函数,图片由作者提供

def loss_r(data, learner):
    tau_hat = learner.effect(data[X])
    y_hat = clone(model_y).fit(df[X + [W]], df[Y]).predict(data[X + [W]])
    e_hat = clone(model_e).fit(df[X], df[W]).predict_proba(data[X])[:,1]
    tau_nw = (data[Y] - y_hat) / (data[W] - e_hat)
    return np.mean((tau_hat - tau_nw)**2)

compare_methods(learners, names, loss_r, title='R Loss')

R 损失值,图片由作者提供

R 损失函数明显比 IPW 损失函数噪声更小,而且它能清晰地孤立 S-学习者。然而,它倾向于偏爱其对应的学习者,即 R-学习者。

DR 损失

DR-loss 是 DR-learner 的目标函数,首次由 Saito, Yasui (2020) 提出。至于 IPW 和 R-loss,概念是试图匹配一个伪结果,其期望值是条件平均处理效应。DR 伪结果与 AIPW 估计器 密切相关,也被称为双重稳健估计器,因此得名 DR。

DR 伪结果,图片由作者提供

对应的损失函数是

DR 损失函数,图片由作者提供

def loss_dr(data, learner):
    tau_hat = learner.effect(data[X])
    y_hat = clone(model_y).fit(df[X + [W]], df[Y]).predict(data[X + [W]])
    mu1 = clone(model_y).fit(df[X + [W]], df[Y]).predict(data[X + [W]].assign(mail=1))
    mu0 = clone(model_y).fit(df[X + [W]], df[Y]).predict(data[X + [W]].assign(mail=0))
    e_hat = clone(model_e).fit(df[X], df[W]).predict_proba(data[X])[:,1]
    tau_nw = mu1 - mu0 + (data[Y] - y_hat) * (data[W] - e_hat) / (e_hat * (1 - e_hat))
    return np.mean((tau_hat - tau_nw)**2)

results = compare_methods(learners, names, loss_dr, title='DR Loss')

DR 损失值,图片由作者提供

与 R-loss 类似,DR-loss 倾向于偏好其对应的学习器,即 DR-learner。然而,它在算法准确性方面提供了更准确的排名。

实证政策收益

我们将要分析的最后一个损失函数不同于迄今为止见过的所有其他函数,因为它关注我们能够多好地估计治疗效应,而是关注相应的最佳治疗政策的表现。特别是,Hitsch, Misra, Zhang (2023) 提出了以下收益函数:

实证政策收益函数,图片由作者提供

其中 c 是治疗成本,d 是给定估计 CATE τ̂(Xᵢ) 的最佳治疗政策。在我们的案例中,我们假设个体治疗成本为 c=0.01$,因此最佳政策是对每个估计 CATE 大于 0.01 的客户进行治疗。

Wᵢ⋅d(τ̂) 和 (1-Wᵢ)⋅(1-d(τ̂)*) 表明我们仅对实际治疗 W 与最佳治疗 d 相匹配的个体进行计算。然而,这意味着每个模型计算的度量观测数量不同。逆权重 ê(X)1-ê(X) 纠正了这种不平衡。

def gain_policy(data, learner):
    tau_hat = learner.effect(data[X])
    e_hat = clone(model_e).fit(df[X], df[W]).predict_proba(data[X])[:,1]
    d = tau_hat > cost
    return np.sum((d * data[W] * (data[Y] - cost) / e_hat + (1-d) * (1-data[W]) * data[Y] / (1-e_hat)))

results = compare_methods(learners, names, gain_policy, title='Empirical Policy Gain', subtitle='higher is better')

实证政策收益值,图片由作者提供

实证政策收益表现良好,能够区分出两种最差表现的方法,S-学习者和 T-学习者。

元研究

在本文中,我们介绍了多种评估提升模型的方法,也就是条件平均处理效应估计器。我们还在我们的模拟数据集中进行了测试,这是一个非常特殊和有限的例子。这些度量在一般情况下表现如何?

Schuler, Baiocchi, Tibshirani, Shah (2018) 比较了模拟数据上 S-loss、T-loss、R-loss 的表现。研究发现 R-loss“是在优化时最一致地导致选择高性能模型的验证集指标”。作者还发现了所谓的亲和性偏差:像 R-loss 或 DR-loss 这样的指标倾向于对相应的学习者产生偏见。

Curth, van der Schaar (2023)理论角度研究了更广泛的学习者。他们发现“在我们考虑的所有实验条件下,没有现有的选择标准在全球范围内最好”。

Mahajan, Mitliagkas, Neal, Syrgkanis (2023)最全面的研究,涵盖范围广泛。作者在 144 个数据集和 415 个估计器上比较了许多指标。他们发现“没有指标显著优于其他指标”,但“使用 DR 元素的指标似乎总是出现在候选赢家中”。

结论

在本文中,我们探讨了多种评估提升模型的方法。主要挑战在于感兴趣变量——个体治疗效果的不可观测性。因此,不同的方法尝试通过使用其他变量、代理结果或近似隐含的最优政策效果来评估提升模型。

很难推荐使用单一方法,因为没有共识哪一种表现最好,无论是从理论还是实证角度来看。使用 R-和 DR-元素的损失函数往往表现一致较好,但也对相应的学习者存在偏见。然而,理解这些指标的工作原理可以帮助理解它们的偏差和局限性,以便根据具体场景做出最适当的决策。

参考文献

相关文章

  • 理解元学习器

  • 理解 AIPW,双重稳健估计量

  • 理解因果树

  • 从因果树到森林

代码

你可以在这里找到原始的 Jupyter Notebook:

[## Blog-Posts/notebooks/evaluate_uplift.ipynb at main · matteocourthoud/Blog-Posts

我在 Medium 博客文章中使用的代码和笔记本。通过创建一个…

github.com](https://github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/evaluate_uplift.ipynb?source=post_page-----8a078996a113--------------------------------)

感谢你的阅读!

非常感谢! 🤗 如果你喜欢这篇文章并想查看更多内容,可以考虑 关注我。我每周发布一次与因果推断和数据分析相关的主题。我尽量保持我的文章简明但精准,始终提供代码、示例和模拟。

另外,一个小小的 免责声明:我写作是为了学习,所以错误是常有的,即使我尽力而为。请在发现错误时告知我。我也欢迎关于新主题的建议!

推荐系统的评估指标 — 概述

原文:towardsdatascience.com/evaluation-metrics-for-recommendation-systems-an-overview-71290690ecba?source=collection_archive---------3-----------------------#2023-08-09

理解机器学习包中常见指标的目的和功能

Pratik AherTowards Data Science Pratik Aher

·

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

--

最近,在实验推荐系统项目时,我发现自己使用了各种评估指标。因此,我汇总了一些有用的指标以及评估推荐系统时需要考虑的其他事项。这些指标在机器学习包中很常见,但理解它们的目的和功能是至关重要的。

召回率 @K

召回率@K 衡量了在所有相关项目中,前 K 个中有多少是相关的,其中 K 是为用户生成的推荐数量。例如,如果我们正在构建一个电影推荐系统,为每个用户推荐 10 部电影。如果一个用户看过 5 部电影,而我们的推荐列表中有 3 部(在 10 个推荐中),那么用户的召回率@10 计算为 3/5 = 0.6。通常,平均值是对所有用户进行评估的。

从商业角度来看,这是一个简单但重要的指标,因为我们可以展示系统在预测用户行为方面带来的实际价值。

范围:0–1

精确度 @K

精确度@K 衡量了推荐给用户的“K”个项目中有多少是相关的,其中 K 是为用户生成的推荐数量。

对于一个推荐系统,我们为每个用户推荐 10 部电影。如果一个用户看过 5 部电影,而我们能预测其中 3 部(这 3 部电影在我们的推荐列表中),那么我们的精确度@10 就是 3/10。

从规模和排名的角度来看,这是一个非常重要的指标,因为在现实世界中,你可以提供给用户的推荐数量是有限的。这可以与以下因素相关:注意力跨度(用户希望能在一眼看到相关的推荐,因此相关推荐位于顶部是至关重要的),以及存储需求:假设你只能为每个用户存储 100 个推荐,那么你需要精确选择。

范围:0–1

F1 @K

F1 分数是精确度和召回率的调和均值的组合。这与常规的 F1 分数相同,在推荐系统的背景下没有区别。调和均值的特性确保如果精确度或召回率的值非常高,那么它不会主导分数。当精确度和召回率的值都接近 1 时,F1 分数值会很高。

范围:0–1

排名相关指标:

如上所述,谈到精确度时,将相关推荐放在顶部是至关重要的。有多种方法来测量相关推荐是否确实在顶部。这些测量不仅用于评估,还作为排名模型的损失指标。

平均精确度 @K

评估推荐列表在基于列表中的位置预测相关项目的效果的一种方法是使用“平均精确度”。

首先,我们来理解什么是平均精确度。如果我们推荐了 K 个项目,其中 Q 个是相关的,那么平均精确度定义为:

如果所有相关项目都在顶部,那么该用户的平均精确度分数就会很高。

示例:

推荐列表 : [“壮志凌云”, “降临”, “角斗士”]

真实情况 : [“降临”, “角斗士”]

精度 @K = [0, 1/2, 2/3]

平均精度(AP)= (1/3)[(1/2) + (2/3)] = 0.38

MAP 中的平均值只是所有用户的平均精度(AP)值:

范围 : 0–1

平均倒数排名(MRR)

平均倒数排名衡量推荐列表中第一个相关项目的位置。倒数排名(RR)用于我们只关心排名最高结果的位置。在这里,排名是推荐列表中项目的位置。

倒数是有用的,因为它确保排名较低的项目(例如,第 20 位)获得较低的分数,因为大值的倒数是非常小的值。因此,如果最相关的项目被预测在列表顶部,则受益。

倒数排名只关心第一个相关项目。例如,

推荐列表 : [“壮志凌云”, “降临”, “角斗士”]

真实情况 : “降临”

然后,倒数排名(RR)= (1/2) = 0.5

在推荐系统的背景下,我们也可以使用 MRR,如果我们在推荐系统中有多个值,我们可以对它们取平均。

推荐列表 : [“壮志凌云”, “降临”, “角斗士”]

真实情况 : [“降临”, “角斗士”]

然后,平均倒数排名(MRR)= 1/2* ((1/2) + (1/3)) = 0.41

范围 : 0–1

归一化累积折扣增益(NDCG)

归一化折扣累积增益(NDCG)是衡量排名列表质量的指标。 其思想是,如果相关项目从最相关到最不相关排序,那么当最相关的项目被推荐到列表顶部时,NDCG 分数会最大化。

让我们用一个例子来解析:

为了保持之前的例子:如果我们确定一个用户是动作电影爱好者,那么假设相关性分数为:

“壮志凌云”, “角斗士”: 2(最相关)

“玩具总动员”: 1

“鲸鱼” : 0(最不相关)

推荐列表 :

[“壮志凌云”, “玩具总动员”, “鲸鱼”, “角斗士”] ⇒ [2, 1, 0, 2]

累积增益(CG):位置 p 的累积增益是该位置的相关性分数。因此,对于整个列表,它是:2 + 1 + 0 + 2 = 5

累积增益不考虑项目的位置。因此,如果最相关的项目在列表末尾(如“角斗士”),则不会反映在 CG 分数中。

为了解决这个问题,我们引入了折扣累积增益(DCG),我们为每个位置分配一个分数/折扣,以便相关性分数会受到惩罚。

因此,如果一个像“角斗士”这样的相关项目被放在列表的最后,它将按 1/log2(n)(其中 n 是列表的大小:它将乘以一个像 0.2 这样较小的数字,因此其对分数的贡献将非常小)进行折扣,而第一个项目不会折扣。

如果所有相关项目都位于顶部,DCG 分数会最高。

对于这些项目,集合 A: [2, 1, 0, 2]:

让我们将其与集合 B: [2, 2, 1, 0] 进行比较,其中所有相关项目都位于顶部:

显然,集合 B 的 DCG 高于集合 A 的 DCG。此外,集合 B 是我们称之为理想折扣累积增益(IDCG)的,它给出了按相关性分数完美排序的理想列表的 DCG。

如果我们需要比较两个不同大小列表的 DCG 分数怎么办?

这时 IDCG 就派上用场了,我们将 DCG 分数除以 IDCG 分数,得到一个 0 到 1 之间的值。这个分数被称为标准化折扣累积增益(nDCG)。

现在我们可以比较两个不同大小列表的 nDCG 分数。

nDCG 范围:0–1

这是一些广泛用于评估推荐系统的指标的概述。

评估推荐系统时需要考虑的事项:

我计划很快将这些主题整理成文章,到时会在这里链接。

  1. 流行度偏差

推荐系统中可能存在大量的流行度偏差,并且难以检测和修复,因为流行项目相关性高,并且在大多数标准指标中得分很高。测量和修复流行度偏差有多种方法,我计划整理一篇讨论这些方法的文章。

图片来源:作者

2. 位置偏差

位置偏差发生在列表中排名较高的项目更可能被查看或购买,无论其实际相关性如何。因此,排名较低的项目获得的互动较少。这也可能影响其他指标,并且有方法可以缓解这种情况。

3. 退化反馈循环

当用户仅限于与建议的项目互动,并且算法依赖于这些建议的用户反馈进行训练时,可能会出现负反馈循环。这个循环加强了展示之前已经显示过的项目,可能导致随着时间的推移用户体验变差,因为用户未发现大量相关项目。

参考文献:

[## Wikiwand - 平均倒排排名

平均倒排排名是评估生成可能响应列表的任何过程的统计测量…

www.wikiwand.com [## 推荐系统的均值平均精度(MAP)

(好吧,这里有个小玩笑。) 既然你正在阅读这篇文章,你可能刚刚遇到了“均值平均精度(Mean Average Precision)”这个术语,或者……

推荐系统的评价指标 ## 推荐系统的评价指标

推荐系统在在线零售中变得越来越流行。来看一下用来……

[towardsdatascience.com

机器学习分类器的评估

原文:towardsdatascience.com/evaluation-of-machine-learning-classifiers-3912e7f5cf74

偏倚-方差分析、正则化、性能指标的解释及和谐分类器的实现

J. Rafid Siddiqui, PhDTowards Data Science J. Rafid Siddiqui, PhD

·发布于Towards Data Science ·10 分钟阅读·2023 年 1 月 15 日

--

图 1:偏倚-方差分析结果的示意图(来源:作者)

在之前的文章中,我们讨论了各种用于分类任务的机器学习方法。我们也重复使用了正则化过拟合欠拟合等术语。在本文中,我们将详细介绍这些术语,并展示如何规避这些问题。此外,我们还将讨论各种用于衡量分类器性能的指标。

1. 偏倚-方差分析

偏倚-方差分析是评估机器学习分类器的一个过程。每个分类器都可能出现高偏倚高方差问题,这取决于训练条件。了解这些常见问题并加以防范,可以帮助构建更好、更通用且高性能的模型。

  • 高偏倚(欠拟合)

当一个分类器对某种类型的预测(例如某个类别)存在高度偏倚,而无论输入数据如何变化,这种模型就会出现高偏倚问题。例如,如果我们在一个无法线性分离的训练集上训练一个线性模型,那么该模型即使在训练集上也会表现得很糟糕。因此,我们称这样的模型为欠拟合模型,因为它没有完全捕捉数据集的结构。图 2 展示了一个欠拟合问题的示例。

图 2:一个模型出现欠拟合问题的示例(来源:作者)

如你所见,这样的模型在训练集和测试集上表现都很差。这是因为模型没有足够的参数来处理数据中的非线性。也可能是数据量不足。在样本量过小的情况下,模型无法收敛到最佳点, resulting model 会遭遇欠拟合问题。

· 高方差(过拟合)

训练机器学习分类器时常见的另一个问题是,当模型在训练集上表现良好时,但当它在不同于已训练过的样本的测试集上进行测试时,其性能显著下降。这种问题被称为高方差过拟合问题。之所以这样命名,是因为模型在预测输出中具有高方差。换句话说,模型无法处理给定类别输入样本中的方差,而是为同一类别产生不同的预测。图 3 展示了这种过拟合问题的一个示例。

图 3:一个遭遇过拟合问题的模型示例(来源:作者)

· 学习曲线(诊断)

现在你知道了高偏差和高方差的问题,你可能会想,如何判断模型遭遇了哪种问题。在像示例中这样的二维数据中,我们可以通过绘制数据和分类器的决策边界来轻松看出。然而,当数据是多维的时,这样做是不切实际的。因此,需要一种通用的诊断方法来找出具体的问题。学习曲线就是一种可以用来发现问题类型的标准。更具体地说,我们绘制从训练集中随机抽取的数据实例的目标函数值。然后,我们逐步增加样本数量,并不断绘制新数据样本的目标函数值。这为我们提供了模型在训练集上的表现曲线。除了训练集外,我们还使用交叉验证集来验证性能。我们在训练-测试拆分之外创建一个单独的数据拆分,并称之为交叉验证集。所有的参数调优和评估都在训练阶段对这个交叉验证集进行比较。我们还重复上述过程,为交叉验证集绘制学习曲线。图 4 展示了这种学习曲线的一个示例结果。

图 4:分别为欠拟合、过拟合和最佳拟合的学习曲线示例(来源:作者)

如果我们观察学习曲线,我们会发现当两条曲线(即JₜJcᵥ)都不收敛时,我们就有了欠拟合问题,这意味着模型没有学习任何东西,并且无法准确分类训练数据和测试数据。然而,当我们看到训练数据的曲线Jₜ明显收敛,而交叉验证集的曲线(Jcᵥ)则出现发散时,那么我们就有了过拟合问题。训练集和交叉验证集最终成本值之间的差距越大,模型过拟合的概率就越大,模型在新的示例上的表现将不如在已经训练过的示例上的表现。如果模型拟合最佳,两条曲线将收敛,这表明模型平衡,对训练和测试样本的表现都很好。需要注意的是,在复杂的数据集中,两条曲线可能不会完全收敛到绝对零点,两者之间可能存在小的差距,但这不应该是显著的差距,距离绝对收敛点不应过大。

2. 解决方案

在上一节中,我们讨论了机器学习分类器可能遇到的多个问题,这里列出了一些克服这些问题的缓解策略。

· 使用更好的数据

构建机器学习模型时的大多数问题源于数据质量不佳。如果数据集过小、噪声过多和/或包含矛盾/误导性数据点,则最佳的解决方案是寻找更好的数据。不平衡的数据集以及某一类实例相对于其他类别的少量实例也可能导致过拟合问题。如果过拟合问题是由于数据不足引起的,则增加数据量可以解决这个问题。如果问题由于数据不平衡引起的,那么归一化和分层数据也可以有所帮助。

· 正则化

处理过拟合问题的一种方法是引入目标函数中的正则化项,这将稍微放松约束,从而适应数据中的变异。更具体地说,我们可以在目标函数中添加一个项(例如,权重的 L1/L2 范数),这样对于训练集中由于噪声产生的轻微偏差点,将会减少惩罚。这样的正则化示例可以在图 5 中看到。

图 5:使用正则化解决过拟合问题的效果示例(来源:作者)

如果我们观察正则化对分类边界的影响,我们会注意到它会使分类边界变宽,从而允许噪声点的容纳,否则这些点由于决策边界过紧会被错误分类。然而,这可能会以训练集性能下降为代价,正如图 5 所示。因此,这取决于选择,由一个参数Lambda控制,该参数决定在构建分类器时应给予正则化项多少权重。Lambda的最佳值也可以通过在训练和交叉验证集上绘制学习曲线,并在保持数据大小相同的情况下,绘制Lambda参数的变化来学习。

· 构建更好的特征空间

当问题由于不良/不足的特征空间而出现时,我们必须构建一个更好的特征空间。例如,在之前提到的欠拟合问题(即,在非线性数据集上使用线性分类器)中,必须增加特征的数量和类型。我们可以在图 3 中看到添加更多特征(即 3 阶多项式模型)的结果。模型在分类训练集方面表现良好,因此解决了欠拟合问题。然而,我们也看到它开始遭遇过拟合问题,并且对某些数据样本过于限制。我们在前面的部分讨论了如何减轻过拟合问题。然而,在某些情况下,仅仅将特征空间更改为更合适的空间可以显著提高性能,并提供一个对未见数据也能稳健工作的更通用的模型。如果我们仔细观察数据,我们应该会看到它是一个重复的模式,我们可以使用谐波分类器来建模。事实上,如果我们用谐波分类器(例如,正弦波)来建模,我们可以获得一个适用于任何新样本的模型,从而提供一个通用的解决方案。图 6 中展示了一个谐波分类器拟合数据的例子。正如我们所见,它不仅能够仅用一个周期来学习和分类训练集,而且对任何数量的周期都很稳健。

图 6:谐波分类器在非线性数据上的结果(来源:作者)

3. 性能指标

图 7:二分类问题的性能指标(来源:作者)

一旦分类器训练完成,就需要一个度量来评估分类性能的好坏。可以使用多种指标进行这种性能评估。准确率是许多任务中常用的指标;然而,当数据集不平衡时,它是一种不好的指标。如果数据集中只有少量某一类的实例,准确率将会严重偏向于多数类。在这种情况下,必须使用其他指标。如图 7 所示,我们构建了一组指标,例如真正例率(TPR)假正例率(FPR),通过计算分类标签的频率来进行评估。TPR(召回率/敏感性)衡量分类器对正类(即类 1)的预测准确率,而FPR衡量分类器对负类(即类 0)的预测性能。高召回率/TPR与低FPR之间总是存在权衡。例如,如果我们以目标检测器/分类器为例,那么TPR告诉我们检测器在所显示的对象中正确找到对象的频率,而FPR则衡量检测器在场景中没有对象时错误检测对象的频率。

此外,分类器的性能在很大程度上依赖于用于将点分类到某一类的概率阈值。例如,在二分类器的情况下,我们可能会使用一个阈值(例如 0.5)对输出概率进行分类,并根据输出概率的值将点分类为0类或1类。然而,0.5 是一个任意值,在不同的分类场景中可能有所不同。因此,为了找到最佳阈值——即分类器性能最佳的阈值,可以将阈值从0调整到1,并观察性能指标(例如TPRFPR等)。这个过程称为接收操作特性分析ROC),通过绘制ROC曲线来执行。ROC曲线是通过绘制FPRTPR的关系来获得的,同时调整分类器的阈值。曲线下方的面积提供了性能度量(即,面积越大,分类性能越好)。一个无技能分类器(即返回随机类别或常量类别作为预测输出的分类器)被绘制为对角线。任何接近无技能分类器的输出曲线都被认为是没有分类能力的坏分类器。完美分类器由位于左上角的方形曲线表示。这种ROC曲线的示例输出可以在图 8 中看到。

图 8:ROC 曲线和精确度-召回曲线(来源:作者)

除了ROC曲线,还有另一种指标称为“精确度-召回率”曲线。它是通过绘制TPR与精确度/PPV(正预测值)的关系来获得的。精确度衡量分类的正确性。如果我们以之前的目标检测类比,那么精确度衡量的是分类器在检测到的对象中正确分类的频率。与TPR vs FPR一样,TPR精确度之间也存在权衡。高TPR可能会以损失精确度为代价。精确度-召回率曲线对于不平衡数据特别有用。精确度-召回率的解释类似于ROC曲线。然而,完美的分类器性能位于右上角,而无技能分类器(例如,大多数分类器或随机分类器)则显示为 0.5 附近的平坦线。

4. 结论

在本文中,我们讨论了在训练机器学习分类器时出现的问题。我们解释了如何诊断这些问题的类型。然后,我们解释了应对这些问题的各种缓解策略。我们还讨论了评估机器学习分类器的各种性能指标。评估方法是机器学习建模范式的一个重要方面,对模型的稳健训练和超参数优化有帮助。你可以通过下面链接中的代码进一步练习这些方法和概念。

代码:

www.github.com/azad-academy/MLBasics-Evaluation

成为 Patreon 支持者:

www.patreon.com/azadacademy

在 Substack 上找到我:

azadwolf.substack.com

关注 Twitter 以获取更新:

twitter.com/azaditech

合成时间序列的评估

原文:towardsdatascience.com/evaluation-of-synthetic-time-series-1b4fc4e2be39?source=collection_archive---------2-----------------------#2023-12-19

探索用于合成时间序列评估的各种指标,并附有实际代码示例

Alexander Nikitin数据科学前沿 Alexander Nikitin

·

关注 发表在 数据科学前沿 ·10 分钟阅读·2023 年 12 月 19 日

--

这篇博客文章可以作为一个 在 GitHub 上的 jupyter notebook 获得,并且是 TSGM,一个时间序列生成建模库的一部分。

今天,我们将讨论合成时间序列数据集的评估——这些数据集是人为创建的,以表示真实数据。假设有一个合成数据集 D,旨在代表真实数据集 D。至关重要的是定量评估这些合成数据的好坏:D是否很好地代表了 D?这些数据是否安全?这些数据对下游问题是否有价值?在本教程中,我们将深入探讨用于定量和定性评估合成时间序列数据质量的方法。

原始和合成正弦数据的示例。

首先,让我们考虑[1]中描述的两种合成数据的可能用法:

场景 1. 一个组织希望雇用外部代理分析敏感数据或研究特定问题的统计方法。由于隐私或商业考虑,分享真实数据可能会很复杂。合成数据可以提供一个方便的解决方案。

场景 2. 一个组织希望在一个相对较小的数据集上训练模型。然而,这个数据集不足以满足所需的建模质量。此类有限的数据集可以通过合成数据来增强。这些合成数据必须与真实数据相似,旨在提升模型的性能,或在其他情况下,协助进行模型可靠性测试。

总体而言,我们在本教程中指出并描述了以下度量:

  1. 真实数据相似性 (场景 1 和 2),
  • 距离度量

  • 判别度量,

  • 最大均值差异分数

2. 预测一致性 (场景 1),

3. 下游有效性 (场景 2),

4. 隐私 (场景 1),

5. 多样性 (场景 1 和场景 2),

6. 公平性 (场景 1 和场景 2),

7. 视觉比较 (场景 1 和 2)。

在 TSGM 中,所有度量都整齐地组织在tsgm.metrics中。深入了解详细信息,请参阅我们的综合文档

现在,让我们通过安装 tsgm 来启动编码示例:

pip install tsgm

生成合成数据。 接下来,我们导入 tsgm,并加载一个示例数据集。一个张量Xr现在将包含 100 个正弦时间序列或常数时间序列(基于目标类yr)。我们将使用(Xr, yr)作为真实(= 历史 = 原始)数据集。Xs包含由变分自编码器生成的合成数据。(注意:为了演示我们只使用一个时期;在实际应用中增加时期数并检查训练收敛性。)

import numpy as np
import functools
import sklearn
import tensorflow as tf
from tensorflow import keras

import tsgm

n, n_ts, n_features = 100, 100, 20
vae_latent_dim = 8

# Load data that will be used as real
Xr, yr = tsgm.utils.gen_sine_vs_const_dataset(n, n_ts, n_features, max_value=2, const=1)
Xr = Xr.astype(np.float32)
yr = keras.utils.to_categorical(yr).astype(np.float32)
ys = yr  # use real labels as synthetic labels

# Using real data generate synthetic time series dataset
scaler = tsgm.utils.TSFeatureWiseScaler()        
scaled_data = scaler.fit_transform(Xr)
architecture = tsgm.models.zoo"cvae_conv5"
encoder, decoder = architecture.encoder, architecture.decoder
vae = tsgm.models.cvae.cBetaVAE(encoder, decoder, latent_dim=vae_latent_dim, temporal=False)
vae.compile(optimizer=keras.optimizers.Adam())

# Train VAE using historical data
vae.fit(scaled_data, yr, epochs=1, batch_size=64)
Xs, ys = vae.generate(ys)

d_real = tsgm.dataset.Dataset(Xr, yr)
d_syn = tsgm.dataset.Dataset(Xs, ys)

与真实数据的相似性

距离度量

一开始,测量真实数据与合成数据之间的相似性是方便的。一种方法是计算合成数据与真实数据的汇总统计量向量之间的距离。

距离越小,合成数据与实际数据的现实性越接近。现在,让我们定义一组统计数据,这些统计数据将作为我们距离度量的基础。方法tsgm.metrics.statistics.axis_*_s计算提供的轴上的统计数据*

statistics = [
    functools.partial(tsgm.metrics.statistics.axis_max_s, axis=None),
    functools.partial(tsgm.metrics.statistics.axis_min_s, axis=None),
    functools.partial(tsgm.metrics.statistics.axis_max_s, axis=1),
    functools.partial(tsgm.metrics.statistics.axis_min_s, axis=1)]

接下来,我们建立距离度量。为了简化,我们将选择欧几里得范数。

discrepancy_func = lambda x, y: np.linalg.norm(x - y)

综合起来,我们将利用tsgm.metrics.DistanceMetric对象。

dist_metric = tsgm.metrics.DistanceMetric(
    statistics=statistics, discrepancy=discrepancy_func
)
print(dist_metric(d_real, d_syn))

MMD 度量

另一种方法涉及比较合成数据和真实数据的分布。在这种情况下,使用最大均值差异(MMD)3被证明是方便的。MMD 作为一个非参数的两样本测试来确定样本是否来自相同的分布。通过经验观察,我们发现 MMD 度量是评估真实数据相似性的特别方便的方法。

mmd_metric = tsgm.metrics.MMDMetric()
print(mmd_metric(Xr, Xs))

区分度量

在这种方法中,模型被训练以区分真实数据和合成数据。在 TSGM 中,tsgm.metrics.DiscriminativeMetric被证明是一个有价值的工具。该度量帮助评估模型在区分真实数据集和合成数据集方面的有效性,为数据相似性提供了额外的视角。

# use LSTM classification model from TSGM zoo.
model = tsgm.models.zoo"clf_cl_n".model
model.compile(
    tf.keras.optimizers.Adam(),
    tf.keras.losses.CategoricalCrossentropy(from_logits=False)
)

# use TSGM metric to measure the score
discr_metric = tsgm.metrics.DiscriminativeMetric()
print(
    discr_metric(
        d_hist=Xr, d_syn=Xs, model=model,
        test_size=0.2, random_seed=42, n_epochs=10
    )
)

一致性度量

接下来,我们讨论一致性度量。这个想法与上面写的情境 1相一致。在这里,我们关注于评估一组下游模型的一致性。更具体地说,我们考虑一组模型 ℳ 和一个评估者 E: ℳ × 𝒟 → ℝ。

为了评估ℳ在 D 和 D上的一致性,我们测量 p(m₁ ∼ m₂| m₁, m₂ ∈ ℳ, D, D),其中 m₁ ∼ m₂表示 m₁与 m₂一致:“如果 m₁在 D 上优于 m₂,则在 D*上也优于 m₂,反之亦然。” 估计这个概率涉及固定一个有限的集合ℳ,并使用真实数据评估模型,同时使用合成数据分别评估模型。

在 TSGM 中,我们的第一步是定义一组评估者。为此,我们将利用一系列 LSTM 模型,范围从一个到三个 LSTM 块。

class EvaluatorConvLSTM():
    '''
    NB an oversimplified classifier, for educational purposes only.
    '''

    def __init__(self, model):
        self._model = model

    def evaluate(self, D: tsgm.dataset.Dataset, D_test: tsgm.dataset.Dataset) -> float:
        X_train, y_train = D.Xy
        X_test, y_test = D_test.Xy

        self._model.fit(X_train, y_train)

        y_pred = np.argmax(self._model.predict(X_test), 1)
        print(self._model.predict(X_test).shape)
        y_test = np.argmax(y_test, 1)
        return sklearn.metrics.accuracy_score(y_pred, y_test)

# Define a set of models
seq_len, feat_dim, n_classes = *Xr.shape[1:], 2
models = tsgm.models.zoo["clf_cl_n" for i in range(1, 4)]
for m in models:
    m.model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
evaluators = [EvaluatorConvLSTM(m.model) for m in models]

# Utilize the set of evaluators with ConsistencyMetric from tsgm
consistency_metric = tsgm.metrics.ConsistencyMetric(evaluators=evaluators)
print(consistency_metric(d_real, d_syn, d_real))

下游性能

现在,让我们探索生成的数据如何有助于改善特定下游问题中的预测性能。我们将考虑两种不同的方法来评估下游性能:

1. 用合成数据增强真实数据。

当数据有限时,这种方法非常有价值。通过用生成的数据补充真实数据,我们旨在增强训练集以提高模型性能。请参见我们关于数据增强的博客文章 2

2. 仅利用生成的数据进行下游模型训练。

在真实数据稀缺和隐私的情况下,这种方法会派上用场。在这种情况下,下游模型仅在生成的数据上进行训练,并随后在真实数据上进行评估。

downstream_model = tsgm.models.zoo"clf_cl_n".model
downstream_model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

evaluator = EvaluatorConvLSTM(downstream_model)

downstream_perf_metric = tsgm.metrics.DownstreamPerformanceMetric(evaluator)
print(downstream_perf_metric(d_real, d_syn, d_real))

结果表示通过合成数据增强与仅使用训练数据训练的模型相比,准确性的提升。

隐私:成员推断攻击指标

成员推断攻击可视化。

测量合成数据隐私的一种方法是测量对成员推断攻击的敏感性。成员推断攻击过程在上图中进行了可视化。思路如下。设想一个攻击者拥有合成数据和一个特定的数据样本(该样本可能存在于原始数据集中,也可能不存在)。目标是推断这个样本是否存在于真实数据中。

tsgm.metrics.PrivacyMembershipInferenceMetric 通过合成数据衡量对成员推断攻击的敏感性。步骤评估过程如下:

1. 数据分割。将历史数据分割为训练集和保留集(记作 Dₜ 和 Dₕ),

2. 生成模型训练。 在 Dₜ 上训练生成模型,并生成一个合成数据集 D*,

3. 一类分类(OCC)模型训练。 在合成数据 D* 上训练一类分类(OCC)模型,并在 Dₜ 和 Dₕ 上评估该模型,

4. 目标分数计算。 使用 OCC 模型的精度的补数作为目标分数。

该评估过程提供了对利用合成数据进行成员推断攻击潜在脆弱性的洞察。

现在,让我们引入一个攻击模型。为了演示,我们将定义一个一类 SVM 分类器。

class FlattenTSOneClassSVM:
    def __init__(self, clf):
        self._clf = clf

    def fit(self, X):
        X_fl = X.reshape(X.shape[0], -1)
        self._clf.fit(X_fl)

    def predict(self, X):
        X_fl = X.reshape(X.shape[0], -1)
        return self._clf.predict(X_fl)

attacker = FlattenTSOneClassSVM(sklearn.svm.OneClassSVM())
privacy_metric = tsgm.metrics.PrivacyMembershipInferenceMetric(
    attacker=attacker
)

现在,让我们定义一个测试集并测量隐私指标:

X_test, y_test = tsgm.utils.gen_sine_vs_const_dataset(10, 100, 20, max_value=2, const=1)
d_test = tsgm.dataset.Dataset(X_test, keras.utils.to_categorical(y_test))

# 1 indicates high privacy and 0 -- low privacy.
privacy_metric(d_real, d_syn, d_test)

多样性

通过该指标,我们的目标是量化合成数据的多样性。请考虑下面的图像,其中红色点表示真实数据,蓝色点表示合成数据。哪个选项产生了更好的合成数据集?右侧的选项看起来更有利,但为什么?答案在于其多样性,使其可能更具通用性和实用性。然而,仅有多样性是不够的;必须同时考虑其他指标,如距离或下游性能。在我们的探索中,我们将使用熵来说明这一概念。

2D 中的合成数据。A: 非多样化的合成数据;B: 多样化的合成数据。

spec_entropy = tsgm.metrics.EntropyMetric()
print(spec_entropy(Xr))
print(spec_entropy(Xs))

公平性

公平性与合成时间序列生成在两个重要方面交叉。首先,评估合成数据是否引入了新的偏差至关重要。其次,合成数据提供了减少原始数据固有偏差的机会。定义标准化的公平性检查程序颇具挑战性,因为这通常取决于下游问题的具体情况。一些衡量公平性的示例指标包括人口平等、预测率平等范式以及机会平等。

以机会平等为例。机会平等作为一种公平度量标准,用于衡量对于一个优先标签(赋予个人优势或利益的标签)和给定属性,分类器是否对该属性的所有值都同样准确地预测该优先标签6。这一度量标准对于确保在不同属性值之间的公正性和公平对待至关重要。6中提供了一个很好的例子:“假设 Glubbdubdrib 大学招收了利利普特人和布罗卜丁纳根人进入一个严格的数学课程。利利普特人的中学提供了强大的数学课程,绝大多数学生都符合大学课程的资格。布罗卜丁纳根人的中学根本没有提供数学课程,因此,符合资格的学生远少于利利普特人。如果符合资格的学生,无论是利利普特人还是布罗卜丁纳根人,都同样可能被录取,那么机会平等就满足了‘被录取’这一优先标签的要求。”

定性分析

为了定性地评估数据,方便:

a. 从合成和真实数据中绘制样本并可视化单个样本,

b. 构建生成样本的嵌入并使用例如 TSNE 进行可视化。让我们用 TSGM 来示例(b):

tsgm.utils.visualize_tsne_unlabeled(Xr, Xs, perplexity=10, markersize=20, alpha=0.5)

原始和合成时间序列数据的 TSNE 可视化。

引用

本博客文章是 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] Nikitin, A., Iannucci, L. and Kaski, S., 2023. TSGM: A Flexible Framework for Generative Modeling of Synthetic Time Series. arXiv 预印本 arXiv:2305.11567. Arxiv Link

2 时间序列增强,TowardsDataScience 文章, medium.com/towards-data-science/time-series-augmentations-16237134b29b

3 Gretton, A., Borgwardt, K.M., Rasch, M.J., Schölkopf, B. 和 Smola, A., 2012. 核两样本检验。机器学习研究期刊, 13(1), 页码 723–773. JMLR 链接

4 Wen, Q., Sun, L., Yang, F., Song, X., Gao, J., Wang, X. 和 Xu, H., 2020. 深度学习中的时间序列数据增强:一项综述。arXiv 预印本 arXiv:2002.12478. Arxiv 链接

5 Wattenberg, M., Viégas, F. 和 Hardt, M., 2016. 通过更智能的机器学习对抗歧视。Google Research, 17. Google Research 链接

6 机器学习词汇表:公平性。Google Developers Blog。

Google Developers Blog 链接

除非另有说明,所有图片均由作者提供。有关合成时间序列生成的额外材料,请参见 TSGM 在 GitHub 上,并且 订阅 Medium 帖子

你需要了解的关于热图的一切——PowerBI 教程

原文:towardsdatascience.com/everything-you-need-to-know-about-heatmap-tutorial-with-powerbi-977437080280

了解热图的所有细节以及它们在 Power BI 中的应用

Zoumana KeitaTowards Data Science Zoumana Keita

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

--

图片由Henrique Sá提供,来源于Unsplash

动机

作为数据科学家,能够为企业提供清晰简明的可视化,有助于他们快速高效地获取数据的关键信息,从而做出决策。

不同类型的可视化用于传达不同的信息。例如,直方图用于测量数值数据的分布,散点图用于分析变量之间的关系,数据透视表用于在一个表中总结关键统计信息,仅举几个使用 Power BI、Tableau 等工具的例子。

本文旨在介绍一种名为热图的数据可视化方法。我们将首先帮助你了解热图的概念,然后突出它们的好处。接着,将进行实践操作,帮助你理解如何使用 Power BI 工作。最后,我们将讨论在使用 Power BI 热图时需要注意的事项。

什么是数据可视化热图?

热图最初由Cormac Kinney引入,用于图形化表示实时金融信息。它们用于数据可视化,以测量给定业务值的强度,使用不同的颜色进行编码。

以下图片是热图的一个示例,由 NASA 于 2022 年 7 月 13 日分享。它显示了世界上许多国家的地表气温。区域越热,颜色越深;反之,最冷的区域颜色趋向于蓝色。这些信息一目了然。

来源: NASA 热浪图 显示全球温度的惊人上升

数据科学家可以利用这些工具将公司的信息转化为易于理解的可视化,以帮助他们做出可操作的智能决策。

为什么热图有用?

使用热图可以在不同层级上提供显著的直观信息表现。这是商业智能中最强大的方法之一。此外,许多企业因以下原因使用热图:

1. 对客户行为有一个全球概览

在设计一个工具,如网站时,我们对最终用途有一个期望。然而,最终用户可能对设计概念的理解不够准确,这可能导致他们与工具的互动不同。在这种情况下,热图可视化可以通过跟踪主要页面获得更多关注的区域以及被点击最多的区域(可点击和不可点击),来更好地理解用户的行为。

2. 改善业务表现

在更好地理解用户行为后,企业可以设计和设置工具,以便人们能够在正确的位置找到所需的信息。这样可以增加流量,从而创造更多的商业价值。

3. 一个良好的可视化分析替代方案

热图可视化可以提供深入的视觉理解,了解用户如何与您的业务网站互动,并且可以成为像 Google Analytics 这样的工具的良好替代方案,因为这些工具并不总是直观,有时甚至会令人感到不知所措。

4. 数据科学家可视化特征关系的良好方式

热图是我在执行相关性分析时的首选方法。与处理相关矩阵中的数值相比,热图可以快速提供多个变量之间关系的视觉表示,以训练您的机器学习模型。

热图示例

热图在不同领域提供了广泛的可能性,因为它们能够生成简单的数据视觉表现。以下是五种不同类型及其应用。

1. 网络热图

这些类型的热图被那些活动以互联网为驱动的组织使用,例如媒体服务、SaaS 公司、电子商务公司等。他们利用热图可视化来监控和理解用户在其平台上的行为,以发现更好的决策洞察。一些网络热图包括:

  • 点击图 显示了 Web 应用程序中哪些部分点击最多。

  • 滚动图 突出显示用户在进一步导航之前向下滚动了多远的网页。

  • 眼动追踪 由于某些复杂性,可能并不总是显而易见。总体目标是捕捉用户最关注的区域。

  • 鼠标跟踪被一些研究表明是眼动追踪的替代方法。

2. 地理热图

主要由地球科学家使用,地理热图在真实地图上显示数据,并为某些地理区域分配高低密度。这种情况的一个例子是前面显示的 NASA 示例。

3. AI 生成的热图

随着人工智能的兴起,这些热图可视化已获得成功。例如,它们可以用于现代车辆中监测驾驶员的视觉行为,以通过尽量减少认知干扰来提高道路安全。

4. 财务中的热图

热图可以被金融市场专家用来快速评估公司表现以及资产、产品等市场值的持续波动。

5. 体育中的热图

许多体育教练和经理现在不仅用热图监控他们球员的表现,还研究对手队员,以制定赢得比赛的策略。

在 Power BI 上创建热图的逐步指南

现在你对热图有了总体了解以及一些可视化的例子,本节将提供创建你自己视频游戏销售热图表的技能。该表将展示每个平台的年度全球销售额,从 2006 年到 2020 年。

使用的数据可以在Kaggle上自由获取,这是一个具有内置数据集的协作云环境。

从上传数据到 Power BI 开始

从左侧列面板中选择工作区标签以上传数据。如果还没有工作区,请创建一个。

数据上传到 PowerBI 工作区(作者动画)

从你感兴趣的列中创建一个矩阵

在构建热图表可视化之前,我们需要从数据中创建一个二乘二矩阵。这可以通过左侧列上的加号完成。

选择你的数据和矩阵模板:这对应于选择你想从中构建热图表的数据。

  • +> 添加数据以开始 > 选择一个已发布的数据集 > 选择一个数据集以创建报告。从这里你可以选择数据并点击创建按钮。然后,从右上角的可视化标签中选择矩阵模板。

选择数据和矩阵模板(作者动画)

从数据创建矩阵:我们需要选择要显示的行、列和值。在我们的案例中:

  • 行将对应不同的平台。

  • 列表示年份。

  • 值是每个平台在特定年份的全球销售额。

点击你的矩阵 > 右侧展开数据 > 这将显示你数据中的所有列。

  • 拖动并将平台放入行字段。

  • 拖动并将年份放入列字段。

  • 拖动并将全球销售放入值字段。

从数据创建矩阵(动画由作者提供)

让我们移除总计行和列,因为保留它们可能会信息过多。来自可视化选项卡:

  • 格式化你的视觉效果(画笔)> 关闭行和列的小计

关闭总计行和列(动画由作者提供)

构建你的热图

构建热图表可视化包括两个主要步骤:(1)使用颜色范围对每个销售值的背景进行编码,以及(2)对值的字体使用相同的颜色范围。

(1)从“值”中选择 Global_Sales > 条件格式 > 背景颜色

当全球销售值较低时,颜色会更白;相反,当销售值较高时,颜色会更红。

背景的条件格式(动画由作者提供)

(2)从“值”中选择 Global_Sales > 条件格式 > 字体颜色

值字体的条件格式(动画由作者提供)

恭喜!你已经使用 Power BI 创建了热图表可视化。

使用 Power BI 热图时需要记住的事项

在使用 Power BI 构建热图时,请记住以下关键点:

  • 热图是由密度和强度驱动的。因此,选择准确代表这些信息的颜色很重要。

  • 如果在“值”部分没有使用数值数据,你将无法获得条件格式选项。

  • 在将数据转换为矩阵后,一些单元格可能没有值。在这种情况下,PowerBI 提供了将这些值设置为零的选项,这可能会很有帮助。

  • 你可以通过使用预定义的颜色或自定义自己的颜色来定制 Power BI 热图。

  • Power BI 热图仅在 Power BI 市场中提供。

结论

这篇概念性博客提供了热图的完整概述以及它们对不同领域业务的好处。同时,你也学会了如何使用 Power BI 创建自己的热图可视化。

此外,如果你喜欢阅读我的故事并希望支持我的写作,考虑 成为 Medium 会员。每月 $5 的承诺可以解锁对 Medium 上故事的无限访问。

随时在 MediumTwitterYouTube 上关注我,或在 LinkedIn 上打个招呼!讨论 AI、ML、数据科学、NLP 和 MLOps 的话题总是很高兴!

想请我喝咖啡 ☕️ 吗?→ 这里点击!

在你离开之前,

查看文章Tableau 数据混合教程——初学者的逐步指南

关于正则化你需要了解的一切

原文:towardsdatascience.com/everything-you-need-to-know-about-regularization-64734f240622?source=collection_archive---------4-----------------------#2023-01-25

图片来源:Dall-E 2。

防止机器学习中过拟合的不同方法

Hennie de HarderTowards Data Science Hennie de Harder

·

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

--

如果你正在使用机器学习模型,你可能听说过正则化。你知道它是什么以及它是如何工作的么?正则化是一种用于防止过拟合和提高模型性能的技术。在这篇文章中,我们将详细讲解不同类型的正则化以及如何利用它们来改进你的模型。此外,你还会学到何时应用不同类型的正则化。

机器学习中的正则化意味着“简化结果”。如果模型出现过拟合且过于复杂,你可以使用正则化使模型更好地泛化。如果训练集和测试集之间的性能差距很大,你应该使用正则化。这意味着模型捕捉了训练集的过多细节。过拟合与高方差有关,这意味着模型对训练集的特定样本很敏感。

分类问题。黑线是模型的决策边界。图像作者提供。

让我们开始讨论不同的正则化技术。每种技术都属于以下两类之一:显式正则化或隐式正则化。可以在同一个问题中结合多种正则化技术。

显式正则化

所有属于显式正则化的技术都会显式地向问题中添加一个项。我们将深入探讨三种最常见的显式正则化类型。这些类型包括 L1、L2 和弹性网正则化。

作为示例,我们以一个具有独立变量x₁x₂,以及因变量y的线性回归模型为例。该模型可以用以下方程表示:

我们希望确定权重:w₁w₂b。代价函数等于均方误差,我们希望将其最小化。下方是代价函数 J

在以下示例中,你可以看到不同类型显式正则化的代价函数如何变化。

L1 正则化

这种类型的正则化也被称为 Lasso 正则化。它向代价函数中添加了一个与权重系数的绝对值成正比的项:

它倾向于将一些权重系数缩小到零。该项的总和乘以 lambda,它控制正则化的程度。如果 lambda 太高,模型会变得简单,从而产生欠拟合的风险。

L2 正则化

L2 正则化,或称为岭回归,向代价函数中添加了一个与权重系数的平方成正比的项:

这种项倾向于缩小所有权重系数,但与 L1 正则化不同,它不会将任何权重系数设置为零。

弹性网正则化

这是一种 L1 和 L2 正则化的组合。正如你所期望的,使用弹性网正则化时,L1 和 L2 的项都会被添加到代价函数中。而且还添加了一个新的超参数 α,用以控制 L1 和 L2 之间的平衡。

何时使用 L1、L2 或弹性网?

在许多 scikit-learn 模型中,L2 是默认的(参见 LogisticRegression 和 SupportVectorMachines)。这是有原因的:L1 往往会将一些权重系数缩减为零,这意味着特征从模型中移除。因此,L1 正则化更适合特征选择。为了真正防止过拟合,L2 可能是更好的选择,因为它不会将任何权重系数设为零。

当你有相关特征并希望在特征选择和防止过拟合之间取得平衡时,弹性网正则化是一个不错的选择。当你不确定 L1 或 L2 正则化对于你的数据和模型哪个更合适时,它也很有用。

一般来说,当你有大量特征并且希望在模型中保留大部分特征时,推荐使用 L2 正则化;当你有高维数据集且有许多相关特征,并希望选择其中一个子集时,推荐使用 L1 正则化。

隐式正则化

那些不显式的技术通常会自动归入隐式类别。这里有许多不同的隐式技术,我将讨论一些常用的。

模型参数

使模型更好地泛化的一个简单方法是指定一些模型超参数。以下是一些示例:

  1. 对于树模型,如果model_depth参数没有设置,容易出现过拟合。此时,模型会继续增长,直到所有叶子都是纯净的,或直到叶子包含少于min_samples_split个样本。你可以通过超参数调优尝试不同的值,以发现最佳深度。

  2. 在 XGBoost 中,许多参数与过拟合相关。以下是其中一些:colsample_bytree包含树中使用的特征比例,当你使用较少的特征时,可以减少过拟合效应。另一个密切相关的参数是subsample,它指定使用的样本比例。将学习率eta设置为较低的值也有帮助。gamma用于控制分裂所需的最小损失减少量。

  3. 在逻辑回归和支持向量机中,C参数控制正则化的强度。正则化的强度与C成反比。也就是说,你设置得越高,正则化的程度就越低。(这实际上不是完全隐式的,因为它与正则化项有关。)

  4. 在神经网络中,层数和每层的神经元数量可能导致过拟合。你可以删除层和神经元来降低模型的复杂性。

不同模型有许多其他参数,你可以在线搜索或阅读文档来发现它们并改进你的模型。

Dropout 正则化

Dropout 正则化在深度神经网络的训练过程中应用。它在每次训练迭代中随机丢弃一定数量的神经元,迫使网络依赖多个神经元子集,从而减少过拟合。

Dropout 层:从网络中移除神经元。图像由作者提供。

早期停止

这是一种正则化方法,通过监控模型在验证集上的表现来防止过拟合,当表现开始下降时停止训练。一些模型内置了早期停止功能。XGBoost 提供了一个参数early_stopping_rounds,以及如何在 Keras 中实现

与其继续训练模型,不如在测试集上的损失开始增加时停止。图像由作者提供。

数据增强

这是一种通过对现有数据应用随机但现实的变换来人为扩大训练数据规模的方法,以防止过拟合。数据增强通常应用于图像数据:下方可以看到一些不同的增强技术(镜像、裁剪和添加效果)。还有更多技术,如调整光照、模糊、移动对象等。

图像数据上的数据增强。图像由作者提供。

结论

本文为你提供了正则化技术的解释和概述。防止过拟合是使用机器学习模型时的重要部分,你必须确保以正确的方式处理它。除了本文介绍的技术外,使用交叉验证或处理异常值也有助于减少泛化误差。

相关

[## 你是否使用特征分布来检测异常值?

这里有三种更好的方法。

[## 面向模型的方法用于解释任何机器学习模型

解释方法概述:置换特征重要性、部分依赖图、LIME、SHAP 等。

[## 面向模型的方法用于解释任何机器学习模型

你应该知道的关于评估大型语言模型的一切

原文:towardsdatascience.com/everything-you-should-know-about-evaluating-large-language-models-dce69ef8b2d2

开放语言模型

从困惑度到衡量一般智能

Donato RiccioTowards Data Science Donato Riccio

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

--

图片由作者使用稳定扩散生成。

随着开源语言模型的越来越普及,容易在众多选项中迷失方向。

我们如何确定它们的表现并进行比较?我们如何自信地说一个模型比另一个更好?

本文通过提供训练和评估指标、一般和特定基准,来帮助清晰了解你模型的表现。

如果你错过了,请查看《开放语言模型》系列中的第一篇文章:

## 开源大型语言模型的温和介绍

为什么大家都在谈论美洲驼、羊驼、猎鹰和其他动物

[towardsdatascience.com

困惑度

语言模型在词汇表上定义概率分布,以选择序列中最可能的下一个词。给定一段文本,语言模型为语言中的每个词分配一个概率,选择最可能的词。

困惑度 衡量语言模型在给定序列中预测下一个词的能力。作为一种训练指标,它显示了模型对训练集的学习效果。

我们不会深入探讨数学细节,但直观上,最小化困惑度意味着最大化预测概率

换句话说,最好的模型是当看到新文本时不会感到惊讶的,因为它早已预测了序列中接下来会出现的词。

虽然困惑度很有帮助,但它没有考虑单词背后的意义或使用上下文,并且受到我们如何对数据进行分词的影响 — 不同的语言模型采用不同的词汇和分词技术可能会产生不同的困惑度分数,使得直接比较变得不那么有意义。

困惑度是一个有用但有限的指标。我们主要用它来跟踪模型训练过程中的进展或比较同一模型的不同版本。例如,在应用量化技术后 — 一种减少模型计算需求的技术 — 我们通常使用困惑度来评估模型质量的变化。

困惑度只是方程式的一部分 — 它提供了有价值的见解,但并不能讲述全部故事。¹

大型语言模型的一些任务。图片由作者提供。

BLEU 和 ROUGE

如果你对自然语言处理感兴趣,你可能听说过ROUGE 和 BLEU 分数

它们在 2000 年代初期引入,用于机器翻译,量化机器文本与人类参考文本的接近程度。

BLEU 分数是人类参考文本中的单词数除以总单词数。类似于精确度分数,它的值介于零和一之间,值越接近一表示文本越相似。

ROUGE 基于类似的原理,但由于它通过多个方面(如 n-grams(ROUGE-N)、最长公共子序列(ROUGE-L)和跳跃二元组(ROUGE-S))分析重叠,因此略为复杂。

对于大型语言模型,BLEU 和 ROUGE 用于评估输出与被认为正确的人类解决方案的接近程度。但它们并不适用于所有生成任务。正如你所想,生成参考文本可能既昂贵又耗时,对于某些领域或语言甚至不可行。

有时,概括或翻译文本并没有唯一的正确方式。这些分数只能考虑一些有效的选项。

此外,它们不考虑上下文 — 对于新闻文章有效的文本可能不适合社交媒体帖子,而正式场合适用的内容可能不适合休闲场合。

基准测试的需求

开源模型通常较小,并经过微调以更专注于特定任务。

Meta 的创始人马克·扎克伯格认为,我们将根据不同的需求与不同的 AI 实体互动,而不是依赖通用的 AI 助手。² 要真正了解哪种模型最适合特定任务,我们需要一种比较它们的方法。

特定基准评估语言模型的某个特定方面。例如,如果你想评估你的模型回答的真实性或量化模型在微调后的任务表现,请使用特定基准。

其中四个用于Hugging’s Face OpenLLM Leaderboard

抽象推理语料库(ARC)是一个抽象推理测试。它适用于人类和人工智能,并试图测量一种类似人类的流动智力。给定一个输入网格,用户需要选择正确的输出。

ARC 测试接口。语言模型可以通过 JSON 文件与其交互。³

HellaSwag是一个测试,用户需要为给定的上下文选择最佳结尾,这一任务称为常识推理。虽然对人类来说很容易,但许多 LLM 在这个测试中表现不佳。唯一能够达到接近人类水平表现的是 GPT-4。

HellaSwag 中的一个示例测试。⁴

大规模多任务语言理解(MMLU)测量文本模型在 57 个任务上的多任务准确性,包括数学、美国历史、计算机科学、法律等。测试看起来像是不同问题的选择题,并评估对世界和常识的理解

测量大规模多任务语言理解。⁵

TruthfulQA包括两个任务:生成和多项选择。生成任务要求模型产生真实和有信息的答案,而多项选择任务要求模型选择或分配真实和虚假答案选项的概率。该基准测试涵盖 57 个主题,并使用各种指标来测量模型识别虚假信息的能力。有趣的是,论文显示较大的模型不太真实。⁶

这些模型的衡量标准是它们识别虚假信息的能力。⁶

测量代码生成能力

当 ChatGPT 刚推出时,要求它写一些代码可能是我们都尝试的第一件事。编码能力是 LLM 可以提供给我们的最有用且节省时间的技能之一。

在开源领域,有许多专门从事代码生成的模型,如Wizard Coder或最新的Code LLama

为了展示其新Code Llama模型令人印象深刻的编码能力,他们选择了两个特定于代码的基准测试:HumanEvalMostly Basic Python Programming (MBPP),并辅以人工评估。

在第一个基准测试中,模型需要从文档字符串生成代码,而在第二个基准测试中,模型从文本提示开始。

每个提示都会附带一个或多个单元测试,以评估输出的正确性。

MBPP 条目。 来源。

在收集到模型生成的k个样本后,计算pass@k 指标。如果至少一个样本通过了单元测试,则认为解决方案正确。例如,pass@1 分数为 67.0 意味着模型在第一次尝试中可以解决 67%的问题。

计算此指标时,你可以使用任何k的值。但实际上,我们关注的是pass@1。如果你必须不断尝试才能获得正确的解决方案,你如何相信那个模型?

Code LLama的评估结果如下。

Code Llama 评估。⁷

他们的结果显示,GPT-4 是最好的模型,能够在HumanEval中第一次尝试解决 67%的任务。然而,Code Llama 是最好的开源代码专用模型,只有 34B 参数。

测量通用智能

评估系统必须覆盖众多场景,尤其是针对大型通用语言模型,因为它们在处理多样任务时表现出令人印象深刻的泛化能力。

对于经典的机器学习模型,你习惯使用测试集来评估模型,而 LLMs 使零样本学习少样本学习成为可能——LLM 可以学习执行没有明确训练过的任务。在这种情况下,使用测试集或单一指标来基准 LLM 的能力是不够的。

通用基准是各种场景和任务的广泛测试集合。它们像是对模型的终极测试,旨在评估智能的每个方面。

一些评估体系如语言模型的整体评估(HELM),旨在根据七个关键指标评估模型:准确性、校准和不确定性、稳健性、公平性、偏见和刻板印象、毒性和效率,计算涉及 16 种场景。

语言模型的整体评估。⁸

SuperGLUE,于 2019 年推出,是通用语言理解评估(GLUE)测试的高级版本。GLUE 基准包括九项与句子或句子对语言理解相关的任务,所有任务都建立在现有数据集上。SuperGLUE提供了一组更具挑战性的任务和一个公开的排行榜。

BIG-bench,来自谷歌,扩展了 GLUE 和 SuperGLUE,包含了更多自然语言理解任务的广泛集合。这是一个庞大的协作项目,由来自 132 个机构的 444 位作者贡献。它基于超过 200 个任务评估 LLMs 的准确性、流畅性、创造力和泛化能力!

由于运行 BIG-bench 可能非常耗时,作者还提供了一个包含 24 个任务的轻量版,称为 BIG-bench lite。他们的 GitHub 仓库开放供贡献和新想法。

BIG-bench 任务的多样性和规模。⁹

评估语言模型的另一种方法是手动 人工评估。顾名思义,它通过要求人工评审员对 LLM 的输出进行评分或比较来衡量大型语言模型的质量和性能,类似于 Chatbot Arena。这是一个使用 Elo 评分系统(类似于国际象棋)对大型语言模型(LLMs)进行基准测试的平台,用户可以并排与两个匿名 LLM 进行对话,并投票选择他们认为最好的一个。然后,这些投票用于计算 ELO 评分,并在排行榜上对 LLM 进行排名。

你可以访问 他们的网站 并与不同的 LLM 进行对话。

Chatbot Arena 排行榜

研究案例:Llama 2

Llama 2 是 Llama 的继任者。它于 2023 年 7 月发布,具有 7B、13B、34B 和 70B 的不同规模,包括一个微调版本,称为 Llama 2 Chat

论文中,我们可以找到两种主要的评估程序:综合评估安全评估

综合评估。Llama 2 是对 Llama 的改进,得分优于 MPT 和 Falcon。¹⁰

作者工作的评估标准表明,他们优先考虑了两个主要目标。

首先,将 Llama 2 与第一个版本以及开源竞争对手进行比较。 为了实现这一目标,他们使用了全面的通用评估,其中模型在五个维度上进行评估:代码、常识推理、世界知识、阅读理解数学。每个维度是多个基准测试的平均值。

结果还通过 MMLU、BBH(BigBench Hard)和 AGI Eval 基准测试 进行补充,这些基准测试在单独的列中显示。

作者工作的第二个目标显而易见,即 展示他们的微调方法使模型更真实且有害性更低。

微调前的 LLM(左)和微调后的 LLM(右)¹⁰

安全评估旨在使用 TruthfulQAToxiGen 基准测试来评估 真实性有害性

结果表明,得益于微调过程,Llama 2 比其他模型的有害性更低,但比 ChatGPT 的真实性略逊。

结论

语言模型具有多方面和灵活的特性。

开源模型提供了量身定制的解决方案,专业化可能是未来的发展方向。

在比较模型时,请寻找与您的需求相关的基准测试。

最好的模型不一定是困惑度最低或 BLEU 分数最高的模型,而是那个真正为您的生活增添价值的模型。

如果您喜欢这篇文章,请加入 Text Generation ——我们的新闻通讯每周发布两篇文章,提供有关生成式 AI 和大型语言模型的最新见解。

此外,您还可以在 LinkedIn上找到我。

参考资料

¹ M. Shoeybi 和 R. Caruana,语言模型评估超越困惑度 (2023), arXiv.org

² Lex Friedman Podcast,马克·扎克伯格:人工智能的未来 (2023), YouTube.

³ Xu, Y., Li, W., Vaezipoor, P., Sanner, S., & Khalil, E. B., LLMs 与抽象与推理语料库:成功、失败以及基于对象的表示的重要性。 (2023), arXiv.org

⁴ Zellers, R. 等人,HellaSwag: 机器真的能完成你的句子吗? (2022), arXiv.org

⁵ Hendrycks, D., Burns, C., Basart, S., Zou, A., Mazeika, M., Song, D., & Steinhardt, J. 测量大规模多任务语言理解。 (2021), arXiv.org

⁶ Lin, S., Hilton, J., & Evans, O. (2021). TruthfulQA: 测量模型如何模仿人类的虚假信息。 (2022), arXiv.org

介绍 Code Llama,一种最先进的大型语言模型用于编码。 (2023), meta.com

⁸ Liang, P. 等人 (2022)。语言模型的整体评估。 (2022), arXiv.org

⁹ Srivastava, A. 等人。超越模仿游戏:量化和外推语言模型的能力。 (2022), arXiv.org

¹⁰ Touvron, H. 等人 (2023)。Llama 2:开放基础和微调的聊天模型。 (2023), arXiv.org

进化算法 — 突变解释

原文:towardsdatascience.com/evolutionary-algorithm-mutations-explained-4a3b5c2d49de

包含代码实现的插图,以 TSP 为例

James Koh, PhDTowards Data Science James Koh, PhD

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

--

图片由 DALL·E 3 根据提示“绘制一个科幻风格的图像,描绘突变。左侧显示‘前’的状态,右侧显示‘后’的状态。”创建。

这是对进化算法 — 选择解释的延续。

如果你希望对基于预突变表示的重组和突变有一个高层次的理解,本文将作为独立文献满足你的需求。

然而,为了了解每个步骤的完整细节,首先阅读上面链接的文章将会有帮助,然后再继续阅读这里的内容。

结合上一篇文章中的代码片段,你将能够在个人计算机上解决著名的旅行商问题(TSP)。更重要的是,你将欣赏到幕后发生的所有细节。

达成共识

在第一部分中,我概述了进化算法框架如下:

作者提供的图像

在回顾了 EA 中使用的一些术语之后,我们深入探讨了初始化适当基因型的细节(第 3.1 节,针对<1>)以及轮盘赌和锦标赛选择(第 3.2 节,针对<2>)。

我们现在将继续第 3.3 节。

3.3 — 变异

变异可以是单基因型的(涉及单一基因型)或双基因型的(涉及两个基因型)。目标是得到新的基因型,期望它们的适应度高于前任基因型。

统计学帮助我们实现这一希望。纯粹通过运气,某些基因型可能更好,而其他的可能更差。通过与父本选择(第 3.2 节)和生存选择(第 3.4 节)协同工作,“成功”的次数远远超过“失败”。

在第 3.3.1 节和第 3.3.2 节中,我们关注对基于排列的基因型的变异。

从技术上讲,重组过程可以涉及两个以上的父本,但更多并不一定更好[1],本文的范围保持在两个父本。

3.3.1 — 重组

这涉及两个父本基因型。

与可以使用单点或多点交叉进行重组的二进制表示不同,排列表示的重组并不那么简单。

对基于排列的基因型进行强制交叉的结果。

假设我们有一个从 0 到 5 的排列,来自两个父本(一个是红色的,另一个是粉色的)。一次交叉会导致某些索引的重复,以及其他索引的缺失。这不能成为 TSP 的解决方案。

我们做的是应用部分映射交叉。

在下面的示例中,第一个后代[0, 1, 3, 5, 2, 4]是从第一个(红色)父本的左侧部分中获得的,保持原样,并与第二个(粉色)父本中尚未使用的索引组合,保持相对顺序。这给我们带来了[0, 1](来自红色)和[3, 5, 2, 4](来自粉色)的组合。

部分映射交叉的过程。图片由作者提供。

动机如下:

  1. 红色父本[01,4,3,5,2]已经具有良好的(或至少是不错的)适应度,因此被选择作为父本。

  2. 保留基因型的一部分并打乱其余部分可能会导致改进。

  3. 我们利用粉色父本[35,1,2,0,4]也有良好的适应度——它访问了 3,然后是 5,然后是 2,然后是 4。

  4. 我们不是随机打乱红色父本的尾部,而是按照粉色父本访问的顺序对其进行打乱。

当两个父本互换时,情况也是如此。因此,得到了两个后代。

请注意,切割可以在基因型的任何位置发生,概率相等。如果切割只在中间进行,我们就会失去在随后的代中获得多样化后代的机会。没有人能确定前半部分是理想的并且应该保留——它可能只是前四分之一,或者也许是前三分之四。因此,所有选项都应保留,最终交给统计学处理。

3.3.2 — 突变

仍然在进化算法的‘变异’方面,我们转向适用于个体基因型的变化。

基于排列的基因型可能发生的突变。

如果基因型包含一个排列序列,所应用的变化必须涉及某种重新排序。执行位翻转或增加/减少单个数字是不切实际的。

一种方法是随机选择一个数字(例如,“2”)并将其移到序列的开始或结束。另一种方法是随机选择两个数字并交换它们的位置。

这里的想法是对单个基因型做出最小的改动,希望使一个已经很好的解决方案变得更好。这种变异在解决方案几乎完美的情况下尤其有用,只有某一点不太对。

这是第 3.3.1 节(重组)和第 3.3.2 节(突变)的代码。

class Evolutionary:
    def __init__(self, task):
        self.task = task

    def partial_map_crossover(self, parent1, parent2):
        n = len(parent1)
        point = random.randint(0, n-1)
        child1 = list(parent1[0:point])
        for j in parent2:
            if (j in child1) == False:
                child1.append(j)
        child2 = list(parent2[0:point])
        for j in parent1:
            if (j in child2) == False:
                child2.append(j)
        return child1, child2

    def run_evolution(self, population_size, generation_limit=5000, fitness_limit=1e99, crossover='single', verbose=True):
        ## ... define population ...

        best_fitness_seen = -1e9
        for i in tqdm(range(generation_limit)):
            population = sorted(
                population, key=lambda genome: self.task.fitness(genome), reverse=True
            )
            fitness = self.task.fitness(population[0])

            if verbose and (fitness > best_fitness_seen):
                best_fitness_seen = fitness
                self.task.visualize(population[0], save_id=i)
            if fitness >= fitness_limit:
                break

            ## ... elitism; keep best individuals and variants of them ...

            for j in range((population_size - n_keep)//2):
                parents = self.selection(
                    population, self.task.fitness, 
                    method='tournament'
                )
                if random.random() < 0.9:
                    offspring_a, offspring_b = self.partial_map_crossover(parents[0], parents[1])
                else:
                    offspring_a, offspring_b = parents[0], parents[1]
                if random.random() < 0.9:
                    offspring_a = self.swop(offspring_a)
                    offspring_b = self.swop(offspring_b)
                next_generation += [offspring_a, offspring_b]
            population = next_generation

        best_genome = population[0]
        return best_genome

    def selection(self, ...):
        ## tournament or roulette wheel, or combination

    def shift_to_end(self, genome, num=1):
        new_genome = deepcopy(genome)
        for _ in range(num):
            a = random.sample(range(len(genome)), k=1)[0]
            ref = deepcopy(new_genome[a])
            if random.random() < 0.5:
                new_genome[1:a+1] = new_genome[:a]
                new_genome[0] = ref   # bring to first
            else:
                new_genome[a:-1] = new_genome[a+1:]
                new_genome[-1] = ref   # bring to last
        return new_genome

    def swop(self, genome, num=1):
        new_genome = deepcopy(genome)
        for _ in range(num):
            a, b = random.sample(range(len(genome)), k=2)
            new_genome[a], new_genome[b] = genome[b], genome[a]
        return new_genome

3.4 — 生存选择

在广泛讨论了父代选择和变异之后,我们进入进化算法的第三个支柱,即选择哪些个体进入下一代。

第一种方法是将所有父代和后代放在一起,选择最适应的个体来填充下一代。这里的问题是我们在“稀释”进化效果。在经历了父代选择、重组和突变之后,产生的后代中有相当一部分会被丢弃。这其中有些“前进两步,后退一步”的成分。

第二种方法是(在一定程度上)模仿自然。所有父代将“死亡”并从种群中消失,下一代完全由后代组成。这里的问题是最好的后代不一定比最好的父代更好——移除所有父代可能会让我们离理想解决方案更远。

因此,通常会采取中间道路。我们从父代中选择最好的个体,并对它们的副本进行突变,希望通过一些扰动进一步改进解决方案。然后,我们生成足够的后代以填补下一代剩余的位置,而不会丢弃任何个体。

具体来说,在第 3.3 节的##... 精英策略;保留最佳个体及其变体 ...组件中,我们可以做如下操作:

next_generation = population[:n_top]   # keep the n_top fittest individuals

for _ in range(n_perturb):
    # select a candidate from population[:n_top]
    if np.random.random() < p_shift:
        candidate = self.shift_to_end(candidate)
    if np.random.random() < p_swop:
        candidate = self.swop(candidate)
    next_generation += [candidate]

n_keep = n_top + n_perturb

3.5 — 终止

也许你的公司要求你应用进化算法解决一个行业问题,并且有一个预算限制。或者,你知道最佳目标值(但不知道给出这个值的解决方案)。

在这种情况下,一旦结果足够好,可能需要进行提前终止。

如果没有特定的数字需要寻找,并且我们不确定什么是“足够好”,一种方法是当在一定代数中改进量小于某个阈值时停止迭代。

如果资源不是问题,那么你应该关注的是过早终止,而不是过晚终止。进行额外的迭代并运行更多的进化是有益的。就像在深度学习中一样,我们会保留与最佳迭代对应的解决方案,而不是最后的迭代。

4. 获得结果

4.1 表示 TSP

让我们为旅行销售员问题构建一个类,以简化实施过程。

class Salesman:
    def __init__(self, num_cities, x_lim, y_lim, read_from_txt=None):
        if read_from_txt:
            self.city_locations = []
            f = open(read_from_txt)
            for i, line in enumerate(f.readlines()):
                if i==num_cities:
                    break
                node_val = line.split()
                self.city_locations.append(
                    (float(node_val[-2]), float(node_val[-1]))
                )
            self.num_cities = len(self.city_locations)
            self.x_lim = np.max(np.array(self.city_locations)[:,0])
            self.y_lim = np.max(np.array(self.city_locations)[:,1])

        else:   # generate randomly
            self.num_cities = num_cities
            self.x_lim = x_lim
            self.y_lim = y_lim
            x_loc = np.random.uniform(0, x_lim, size=num_cities)
            y_loc = np.random.uniform(0, y_lim, size=num_cities)
            self.city_locations = [
                (x,y) for x,y in zip(x_loc,y_loc)
            ]
        self.distances = self.calculate_distances()

    def calculate_distances(self):
        distances = np.zeros((self.num_cities, self.num_cities))
        for i in range(self.num_cities):
            for j in range(i + 1, self.num_cities):
                dist = np.sqrt((self.city_locations[i][0] - self.city_locations[j][0]) ** 2 + (self.city_locations[i][1] - self.city_locations[j][1]) ** 2)
                distances[i][j] = distances[j][i] = dist
        return distances

    def fitness(self, solution):
        total_distance = 0
        for i in range(self.num_cities - 1):
            total_distance += self.distances[solution[i]][solution[i+1]]
        fitness = -total_distance
        return fitness

    def visualize(self, solution, save_id=None):
        n = len(solution)
        assert n == len(self.city_locations), 'The solution must correspond to all cities'
        for i, (x,y) in enumerate(self.city_locations):
            plt.plot(x, y, "ro")
            plt.annotate(i, (x, y))

        ordered_cities = [self.city_locations[idx] for idx in solution]
        x_coord = [x for (x,y) in  ordered_cities]
        y_coord = [y for (x,y) in  ordered_cities]
        distance = -self.fitness(solution)

        plt.plot(x_coord, y_coord, "gray")
        plt.title("Connected cities (%.1f) according to solution" % distance)
        if save_id is not None:
            filename = "results/plot_%03d.png" % save_id
            plt.savefig(filename, bbox_inches='tight')
            plt.close()
        else:
            plt.show()

4.2 训练和观察

将上述代码片段组合成一个单一的Evolutionary类,然后可以应用以下方法来找到 TSP 的解决方案。

salesman = Salesman(
    num_cities=30, x_lim=100, y_lim=100, read_from_txt='city_locations.txt'
)
evo = Evolutionary(salesman)
best_genome = evo.run_evolution(
    population_size=200, generation_limit=1000, crossover='pmx', verbose=True
)
salesman.visualize(best_genome)

如果你保存了中间结果,你可以使用我分享的代码这里将截图图像组合成一个 gif。

恭喜!你现在具备了使用进化算法解决 NP 难题的知识。

这与文章开头展示的 gif 相同。这是上述代码共享的结果。

结论

在这篇文章中,你了解了进化算法的运行原理,以及每个组件的功能。除了高层次的概念,我们还深入探讨了实现过程,配以可视化、代码和具体数据。

参考文献

[1] A. E. Eiben, P. E. Raue, 和 Z. Ruttkay, 具有多父代重组的遗传算法 (1994),在国际并行问题解决自然会议上

进化算法 — 选择解释

原文:towardsdatascience.com/evolutionary-algorithm-selections-explained-2515fb8d4287

理解发生的过程,通过可视化和代码

James Koh, PhDTowards Data Science James Koh, PhD

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

--

图片由 DALL·E 3 根据提示“绘制一幅科幻主题的图像,描绘进化算法中的父代选择”创建。

旅行商问题(TSP)是一个著名的 NP 难题,具有明显的现实应用。例如,如果有人计划一次环游欧洲的旅行,并估计各站点之间的旅行时间,这就非常有用。

仅仅 30 个城市就会有超过 10³⁰种可能的排列。通过暴力破解方法解决 30 个城市的 TSP 是不可行的(地球上的沙粒数量“仅为”10¹⁹)。

通过本文,使用进化算法(EA)解决这些问题将变得轻而易举。你将能够自己获得以下结果。

通过进化算法获得的解决方案改进过程。Gif 由作者提供。

更重要的是,你将了解幕后究竟发生了什么,以及每个组件(特别是各种类型的突变)如何对解决方案作出贡献。

1. 价值主张

许多指南错误地将遗传算法(GA)和进化算法(EA)互换使用。

GA 在其标准形式中涉及以下元素[1]:

1. 表示 — 位串

2a. 父代选择 — 与适应度成比例

2b. 交叉 — 单点交叉

3. 突变 — 位翻转

4. 生存选择 — 下一代

GA 是 EA 的一个子集。

在本指南中,我们涵盖了 GA 中未讨论的 EA 方面。特别是,我们将重点关注排列表示方面,通过直观的可视化和代码进行解释。

2. 定义

在进一步讨论之前,我们需要明确术语的使用2

表型

原始问题背景中的解决方案。这些解决方案需要“按原样”解释,无需任何解码;并且与解决方案的推导方式无关。

基因型

表示表型的编码。这可能是比特串、整数,甚至是浮点数。确定性映射将每个基因型转换为对应的表型。

适应度

从某个函数中得出的度量,作为选择的基础。它使我们能够比较两个基因型(因此也比较两个解决方案),并定义哪个更“好”。

种群

由个体基因型组成的集合,通常具有恒定的大小。通过迭代,种群成员通过重组、突变和生存者选择的过程发生变化。

重组

一种二进制变异,其中两个后代从两个父代基因型中创建,在这个过程中混合它们的属性。

突变

对单一基因型执行的一元变异。变化是随机的,旨在保持种群的多样性。

生存者选择

确定哪些个体(由其基因型表示)将在下一代中保留下来的过程。目标是保留最有前途的选择,同时保持多样性,以促进未来的潜在改进。

3. 大纲

进化算法遵循以下框架。

图片由作者提供。灵感来自于2中的图 3.2。

3.1 — 初始化;表示

在讨论任何数学或代码之前,我们需要框定问题并制定基因型,以便它能适当地映射到解决方案空间。

进化算法超越了二进制和排列表示,但我们在这里不进一步探讨。

然后将种群初始化为适当基因型的随机副本。

3.1.1(为什么不)二进制表示

使用比特来表示基因型是一个自然的选择,因为这反映了现实。人类以及其他生物都拥有 DNA 作为遗传物质。我们可以将 DNA 序列视为 1 和 0 的字符串,而不是‘A-T’和‘C-G’。

当固有问题涉及二进制决策时,二进制表示效果非常好——一个完美的例子是背包问题,其中每个比特表示该对应物品是否被选中。

尽管一切都可以用比特表示,但对于非二进制问题的解决方案,使用比特表示是不适当的。

突变应该以相等的概率改变解决方案,并确保随之产生的解决方案是有效的。将数字 7(0111)变为 8(1000)需要四次比特翻转,而将其变为 3(0011)、5(0101)或 6(0110)只需一次比特翻转——概率是不同的。使用灰码3不足以解决这种汉明距离的差异,坚持这种表示方式会导致进化过程的偏差。

此外,如果使用二进制表示法来表示排列,在重组和/或变异后可能需要进行调整。这是因为每个索引必须恰好出现一次,结果基因型可能对应于一个无效的表现型。

3.1.2 排列表示法

这引出了将排列作为基因型的使用。除了旅行商问题(TSP),这也适用于需要我们选择一个序列的问题。这可以是访问地点的序列、完成任务的序列、分配匹配或仓库中库存的放置。

将基因型作为索引序列在这里是理想的,因为我们可以确保在变异后得到有效的表现型。例如,如果有 12 个城市可以选择,基因型可以是[9,2,6,1,7,8,11,0,4,3,10,5]。我们将看到有多种方法可以进行变异,同时确保每个整数出现恰好一次。

我们可以从以下提纲开始生成一个种群。(为了简洁起见,我省略了库的导入,因为这些是微不足道的。)

class Evolutionary:
    def __init__(self, task):
        self.task = task

    def generate_genome(self, length):
        genome = np.arange(length)
        np.random.shuffle(genome)
        return genome

    def generate_population(self, size, genome_length):
        population = [self.generate_genome(genome_length) for _ in range(size)]
        return population

3.2 — 父代选择

在进化算法(EA)中,父代选择、变异和生存选择的过程是迭代进行的,以确保整个种群在代际之间不断改进。

如果任何一个步骤实施不正确,整个算法可能会崩溃。

父代选择,作为三大支柱之一,很重要,因为后代是其父代的变体。请记住,每个个体实际上是一个基因型,它代表了一个候选解决方案。

当我们结合两个父代,并应用重组(第 3.3.1 节)以获得新的基因型时,实际上发生的情况是将两个解决方案的各个方面混合在一起,结果产生新的候选解决方案。虽然不能保证高适应度的父代会导致更好的后代,但机会会倾向于对我们有利。

然而,我们需要在探索和利用之间找到平衡,就像在强化学习中一样。每次都选择最适应的两个父代不是一个好主意,因为缺乏多样性会阻碍我们找到最终解决方案4

3.2.1 轮盘赌选择父代

假设的轮盘赌的区域与每个个体的适应度成正比。每个区域表示选择各自个体作为父代的概率。

通过轮盘赌方法展示的父代选择概率相对于适应度的示意图。饼图来自 Excel。图片由作者提供。

一个警告是适应度值应为正值。自己尝试一下,这比千言万语更有说服力。

population=['A', 'B', 'C', 'D', 'E']
weights = [-.1, -.2, -.3, -.3, 1]
for _ in range(100):
    x = random.choices(
        population=population, weights=weights, k=2
    )
    print(x)

你将发现所有负权重被选中的机会为零,而高度负的权重与稍微负的权重没有区别。这不是我们在进化算法(EA)中想要的,因为一个略微差的解决方案应当明显优于一个极其糟糕的解决方案。

在 TPS 中,一种常见的适应度表示方法是通过否定城市之间的距离(你将在第 4.1 节看到代码)。在这种情况下,我们需要对适应度进行标准化,使其为正数。这可以通过从最小(即最负)适应度中减去,然后添加一些小的 epsilon 来完成,这样即使是最差的解也有非零的被选中机会。

这种基准选择实际上比看起来更重要,需要数据科学家的一些判断。考虑以下情况:

适应度的定义是相关的。在上面的例子中,适应度为 100 是否已经‘保证’并内置,或者个体 A获得了它并且确实是其他个体的有力竞争者,这是有区别的。

3.2.2 父代选择的锦标赛

另一种常见的方法是锦标赛选择。这种方法用于确保最适应的父代不会大多数时间占据主导地位。

在这里发生的情况是,随机选择一个小子集,并在这个群体中选择适应度最高的个体作为父代。在下面的例子中(其中圆圈大小表示每个个体的适应度),紫色个体将被选择作为此特定运行的父代。

按照适应度比例显示个体在种群中的可视化。紫色圆圈在这个锦标赛选择的例子中被选中。图片由作者提供。

这有助于维持多样性,这一点在第 3.2 节开始时已经提到。若使用轮盘赌选择法,该紫色个体被选择的机会会非常低。

在代码中,锦标赛选择或轮盘赌选择父代可以实现如下:

class Evolutionary:
    def selection(self, population, fitness_func, method='tournament'):
        if method == 'tournament':
            k = min(5, int(0.02*len(population)))
            sub_population1 = random.choices(
                population=population, k=k
            )
            sub_population2 = random.choices(
                population=population, k=k
            )
            return (
                sorted(sub_population1, key=fitness_func, reverse=True)[0], 
                sorted(sub_population2, key=fitness_func, reverse=True)[0]
            )        
        else: # roulette wheel
            min_fitness = min([fitness_func(gene) for gene in population])
            selected = random.choices(
                population=population,
                weights=[fitness_func(gene)+eps-min_fitness for gene in population],
                k=2
            )
            return tuple(selected)

我将子群体的大小设置为 5 或总人口的 2%,以较小者为准。你当然可以自由更改这个数字。这些超参数越小,探索(而非利用)就会越多。

待续

在这篇文章中,你了解了构成进化算法(EA)的不同组件,并探讨了不同父代选择方法的理由。

一篇文章中要吸收这么多新知识确实不少。这已经有 8 分钟的阅读时间了,EA 中还有很多重要概念需要讨论。为了让学习更易接受,这篇文章就到这里。

在下一篇文章中,进化算法——突变解析,你将学习关于重组、突变和生存选择的内容。基于排列的基因型中的重组不像交叉那样简单,但解释将通过直观、可视化和代码来支持,以便即使没有先前的经验也能清晰理解。然后我们将把所有代码整合在一起,并解决旅行商问题。

参考文献

[1] D. E. Goldberg, 搜索、优化与机器学习中的遗传算法 (1989),Addison-Wesley

2 A. E. Eiben 和 J. E. Smith, 进化计算简介 (2015),Springer-Verlag Berlin Heidelberg

3 C. Faloutsos, 使用灰码的多属性哈希 (1986),发表于 1986 年 ACM SIGMOD 国际数据管理会议论文集

4 F. Vafaee, G. Turán, P. C. Nelson 和 T. Y. Berger-Wolf, 在自适应多样性引导遗传算法中平衡探索与开发 (2014),发表于 2014 IEEE 进化计算大会

发展数据管道测试计划

原文:towardsdatascience.com/evolving-a-testing-plan-for-a-data-pipeline-3e1fc44998d9?source=collection_archive---------4-----------------------#2023-04-30

穷尽多源多目的地测试驱动开发的危险

Moussa Taifi 博士Towards Data Science Moussa Taifi 博士

·

跟随 发表在Towards Data Science ·9 分钟阅读·2023 年 4 月 30 日

--

(作者提供的图片)

初次接触测试驱动开发(TDD)的理念往往让许多初学者数据工程师对 TDD 的承诺感到震惊。更快的开发、更清洁的代码、职业晋升以及世界主导地位等,诸多好处显而易见。然而,现实却截然不同。初次尝试在数据工程中应用 TDD 往往令许多数据工程师感到沮丧。要提取 TDD 的价值需要大量的努力。这要求深入了解测试技术,而这些技术并不在初学者的工具包中。学习“什么”需要测试的过程是困难的。了解在数据管道中应用 TDD 所固有的权衡更加困难。

在本文中,我们探讨了如何演进数据管道测试计划,以避免感受到过度指定测试带来的全部痛苦。

问题

测试驱动开发的风险是什么?尽管 TDD 有其好处,但对于一个新数据工程师来说,它可能是一个危险的东西。最初测试一切的冲动很强烈,这可能导致次优设计选择。正如他们所说,过犹不及。

测试数据管道的每个部分对工程思维的人来说是一个诱人的方向。但为了保持理智,必须有一些节制。否则,你将陷入一片测试的丛林,被一片红色的海洋包围。泥巴球总是紧随其后。

例如,假设我们有以下数据管道:

(图片由作者提供)

我们有三个数据源,六个转换步骤和两个数据目的地。

一个没有经验的数据工程师会如何制定测试计划?

我们都经历过。

解决方案 #1: 耗尽的多源多目的地路径,边长 > 0

使用经典的三部分测试框架,我们可以安全地假设我们的数据工程师会从这里开始:

  • 单元测试 ✅ : 当然,取每个转换,为每个转换生成一些样本输入数据,经过每一步骤的管道,捕获结果,并使用这些输出来验证转换逻辑。

  • 端到端测试 ✅ : 我们无论如何都需要在完整的生产数据上运行管道,所以让我们在生产数据的样本上运行整个管道,捕获结果并使用这些输出来验证端到端管道。

  • 集成测试 ❓❓❓: 那么该怎么做呢?最初的想法是为每个转换阶段的组合构建一个测试。

(图片由作者提供)

经过一些粗略计算,我们的数据工程师开始意识到 6 个转换步骤的组合迅速增长。必须有更好的方法。

(图片由作者提供)

测试所有边长不同的组合无法满足我们对客户承诺的截止日期。我们应该为更多时间做预算。

解决方案 #2:

“好的好的,但不可能那么糟糕。”

是的,考虑到集成测试不会触及真实的源和汇,那么就这样吧,让我们将这 6 个转换步骤连接起来。我们得到以下图形组合。我们得到大约 10 个集成测试。

(图片由作者提供)

但请记住,这涉及数据工程,这意味着输入数据不受我们的控制,并且随时间变化。所以我们需要在其中添加以数据为中心的测试。(你肯定知道得更清楚,但让我们跟随这个论点)。

(图片由作者提供)

解决方案 #3:

“对,但我们不能以某种方式总结一下吗?一定有一组核心数据场景是我们必须绝对支持的?比如数据验证的优先级列表?”

当然,但即使我们推迟数据验证检查,我们仍然会看到这个情况:

(图片由作者提供)

“我们真的需要这么多测试吗?验收测试不就是测试用户看到的内容吗?我们不能为了按时交付一个解决客户问题的好产品而牺牲开发者体验吗?”

当然,是的,下一个逻辑步骤是只运行端到端测试并继续我们的生活。然而,还有一个中间步骤可以解决“集成测试太多”和“数据验证测试固如磐石”两个问题。你可能以前使用过但没有名字:“内联断言”。这是防御性编程传统中一个相当有用的技巧。

解决方案 #4:

这些“内联断言”的核心思想是,当可能时,你将整个管道作为一个整体构建,包括关于代码接口和单体组件之间数据接口的断言。

(图片由作者提供)

就这样,你将其放入红色、绿色、重构开发循环中,并随着随机事件的发生不断增加断言列表。

我们这里有点过热,但请注意我们使用的是生产数据源和生产接收器。如果你陷入困境,就这样做吧。如果你有时间,至少创建专门的测试接收器,并记得限制从输入数据源获取的行数。

这对你来说可能很明显,但我们都在学习如何构建解决客户问题的数据管道 😃

在反向测试金字塔中,这将是什么样子?

(图片由作者提供)

让我们称之为DEE2E++ 测试

数据工程端到端 ++ 测试。

似乎有两种 DE2E++测试的变体:

  • 无处不在的反腐层(U-ACL)

  • 大多数警告反腐层(MW-ACL)

在无处不在的一侧,它看起来如下:

(图片由作者提供)

每个转换都有一个输入反腐层,保护它免受上游更改的影响,还有一个输出反腐层,保护下游消费者免受当前转换内部更改的影响。如果上游数据模式或内容发生变化,那么输入 ACL 将停止处理并向用户报告错误。如果我们更改当前转换的数据模式或内容,那么输出 ACL 也会捕获错误并停止处理。

对于刚入门的数据工程师来说,这确实是相当多的工作。在每个转换上添加强制的验证规则将使数据工程师陷入“批处理工作”模式。与其将管道分解为多个步骤,他们可能会对自己说:“如果我必须为每个转换添加这些 ACL,那么这将是转换数量的两倍。我还不如只添加两个,一个在管道顶部,一个在底部。我会自己处理转换的内部细节。” 这是一个有效的初始方法,其中重点是 1) 顶层的摄取逻辑和 2) 客户可见的数据输出。这个策略的问题在于,我们会失去测试在本地化错误方面的好处。如果第 4 个转换中有错误,那么 ACL 测试只会显示最终输出无效,而不会指出第 4 个转换是罪魁祸首。

此外,随着数据源的演变,我们在这里讨论的是 90%的警告和 10%的阻塞错误。仅仅因为输入数据中出现了新列,并不意味着整个管道应该失败。而仅仅因为某列的分布均值略有变化,并不意味着所有数据都是无效的。客户可能仍然对最新可用的数据感兴趣,以便做出业务决策,并在需要时进行对账。

为此,你需要“主要警告反腐败层”。

(图片由作者提供)

它通知开发人员有问题,但不会停止处理。它在每个转换中扮演着与输入和输出 ACL 相同的角色。此外,它对变化更加宽容。这种类型的 ACL 会发出警告和指标,开发人员可以稍后优先处理这些警告。如果情况完全失控,开发人员可以在修复数据转换后填充数据。

显然,你的情况可能会有所不同。如果当前的数据管道输出有少量确定的人工用户,那么与这些消费者沟通将帮助新的开发人员了解领域。另一方面,如果这个管道有大量已建立的自动化数据管道来消费其输出,那么这个 DEE2E++测试可能就不够了。然而,刚开始的新数据开发人员通常不会在第一天就被分配到对数百个数据消费者有影响的业务关键数据管道。因此,与其让新的数据开发人员同时被陌生的测试技术和关键任务领域压垮,不如将 DEE2E++方法作为新数据开发人员的一个良好起点。

这是 DEE2E++图示再次呈现。

(图片由作者提供)

“等一下,等一下,你是说每个组件只会得到一个仅警告的反腐败层?”

不是“仅警告”,而是“主要警告”。一些断言确实会停止处理并使工作失败。但这就是想法。如果你采用“普遍反腐层”策略,那么你将需要更多时间。随着领域变得更清晰,你可以在数据管道的关键部分添加更严格的 ACL。这种领域理解将有助于按复杂性对变换进行排序。当你识别出需要额外关注的复杂部分时,例如,你可以从“MW-ACL”转向“U-ACL”来保护高度关键的业务逻辑。

“我的意思是,是的,但那为什么还要做单元测试呢?它们不是已经在内联测试中涵盖了吗?”

当然,好吧,我们就去掉它们吧。

(图片由作者提供)

好了?我想我们现在可以回去工作了。

结论

简而言之,测试驱动开发的常见原则对于新数据工程师来说可能会非常压倒性。重要的是要记住,TDD 是一种设计工具,而不是法律。明智地使用它,它将对你有很大帮助。但使用过多,你会发现自己陷入困境。

在这篇文章中,我们探讨了过度指定测试可能会发生的情况。首先,我们以一个看似简单的数据管道为例,看到当我们陷入“详尽的多源多目标路径”时会发生什么。然后,我们观察到与数据中心测试相比,集成测试只是冰山一角。最后,我们发现,初学者数据工程师的一个良好起点是将“主要警告反腐层”作为内联测试的 E2E 测试。这种 DEE2E++测试策略有两个好处。首先,初学的数据工程师不会从第一天起就放弃测试。其次,它给开发人员提供了学习领域知识的余地,并利用他们现有的数据工程基础知识迭代他们的数据管道设计。与其立即陷入 TDD 的微观层面,不如向利益相关者交付工作中的软件,然后利用 DEE2E++测试提供的保护,随着需求的演变,添加更细粒度的测试。

所以,你们看到了。测试驱动开发的危险。愿你们都能避免这些危险,愿你们的数据管道常绿。

想了解更多关于现代数据管道测试技术的信息吗?

查看我关于这个主题的最新书籍。这本书为测试现代数据管道的最流行技术提供了视觉指南。

2023 年书籍链接:

书籍链接: 现代数据管道测试技术在 leanpub 上。

再见!

免责声明: 本文表达的观点仅代表我个人,未必反映我当前或过去雇主的观点。

进化的客户流失预测:应对干预和再培训

原文:towardsdatascience.com/evolving-churn-predictions-navigating-interventions-and-retraining-d0fcb2619f83?source=collection_archive---------10-----------------------#2023-11-16

处理由于主动保留努力而导致的再培训客户流失模型的新兴挑战

Tomer AlmaTowards Data Science Tomer Alma

·

关注 发布于Towards Data Science ·5 分钟阅读·Nov 16, 2023

--

照片由CrowNUnsplash拍摄

重新训练机器学习模型,特别是那些专注于客户流失预测的模型,是确保其相关性和准确性的重要步骤。然而,重新训练流失模型面临独特的挑战,需要特别注意。其中最显著的是区分干预的因果效应——识别由于主动保留程序而留下的客户,以便专门针对他们。

干预对流失重新训练的影响

考虑以下事件序列:

  1. 初始模型训练: 使用历史客户数据训练模型。

  2. 模型推断: 某些客户被标记为可能流失。

  3. 业务干预: 与这些客户互动,劝说他们留下,或使用促销和个性化优惠等措施来鼓励保留。

  4. 使用新数据进行重新训练: 当模型性能下降时,很可能是重新训练的时机——模型将更新为包含这些干预结果的最新数据。

设想一个场景:一个客户被预测可能流失,他们接受了保留代表的服务,然后选择留下。问题出现在试图解释他们决定的原因时——是干预改变了他们的想法,还是他们在最初被模型误分类了?

当在这种模糊数据上重新训练模型时,存在扭曲模型未来预测的风险——将上述客户标记为“留下”可能具有误导性,因为如果我们没有说服他们留下,他们可能已经离开了。

同样,对于流失标签——干预可能是流失的唯一触发因素,使得某些流失标签不可靠。

当保留努力模糊模型清晰度时

处理偏差的可能方法:

  1. 对照组和合成数据: 创建一个“可能流失”客户的子集,对这些客户不进行干预。通过比较对照组和干预组之间的结果,可以判断干预的真实影响——如果某些客户在接受干预时流失更多,则应检查将其排除在干预之外的情况。

    当需要重新训练时,可以利用对照组的数据,并排除干预组的数据,确保模型依赖于可靠的流失标签。

    这种方法的缺点是关键数据的丢失,因此为了弥补排除干预客户的损失,可以尝试生成对照组的合成样本以代表这些客户。这可以通过SMOTE等过采样策略完成。

  2. 反馈调查: 在干预后直接与客户接触,以了解他们留下/流失的原因。收集到的见解可以提供关于干预效果的清晰度,并帮助区分真正留下的客户和那些被干预所影响的客户。

  3. 合并模型: 尝试将初始训练模型与新模型进行结合。平均预测或使用集成方法可以减少任何单一模型偏见主导总体预测的风险。

    请注意,随着时间的推移,最初的训练数据可能会变得不那么相关。

提升建模:精细化流失预测的黄金标准

与预测谁可能离开的传统流失模型不同,提升建模识别由于干预而直接改变行为的客户。

通过将处理组与对照组进行比较,这些模型预测哪些客户因干预而留下,另一方面哪些客户因干预而离开。

这种有针对性的方法帮助企业优化资源并最大化客户价值。

客户可以被分为四种理论类别,基于他们是否接受了保留努力:

确定的客户: 不会流失的客户。针对他们不会带来额外回报,但会增加成本,如沟通努力和潜在的财务激励。

无可救药者: 无论干预如何都会流失的客户。他们不会增加收入,且与确定性客户相比,可能导致成本减少,因为他们不会利用提供的激励。

可说服者: 仅在保留努力后才会留下的客户。他们贡献额外收入。

不可打扰者: 仅在被针对时才会流失的客户。保持他们不受干扰是有利的,而针对他们会增加显著成本而没有收入增长,使他们成为“沉睡的狗”。

提升建模的目标是专门针对可说服者。

挑战在于我们无法确定个体属于哪个类别。我们不能同时将他们处理并将他们放在对照组中。那么,我们如何识别他们呢?我们怎么知道他们是否被说服了,或者他们最初是否不打算离开?这就是提升建模的作用所在。

针对这一挑战有几种提升方法,我们将查看“转换结果”方法。该方法需要来自对照组和处理组的数据,并将我们的重点从分类任务转移到回归任务。

标签是基于特定公式分配的,对于处理倾向等于 0.5 的随机处理分配,目标变量转化为以下值:

转换结果标签(针对 50%的处理倾向)

我们可以使用诸如均方误差(MSE)的损失函数作为解决此回归问题的指标:

对于可说服者,对照组标记为 0,处理组标记为 2。它们之间的最低 MSE 将是分数为 1 的位置,代表可说服者的提升。

对于“打扰者”,控制组为-2,处理组为 0,最佳预测为-1,表示提升。

对于“失去的原因”和“确定的情况”,最佳预测是 0。

理想情况下,应该以最高得分为目标,尽量保留可以说服的人,尽可能避免打扰人群及其他人群。

探索流失预测的未来

正如我们所探讨的,流失预测的领域,其干预复杂性和不断变化的数据,为企业带来了挑战。

重新训练模型不仅仅是一个技术练习,更是理解客户行为和理清真正的留存的一个部分。利用控制组、反馈机制和提升建模等工具。

但也许最关键的是认识到数据不是静态的。我们对客户行为的理解必须适应这种变化。拥抱这种动态情况,不断完善模型,并保持对不断变化的模式的敏感,将是未来成功预测和管理流失的关键。

进一步阅读:

精确算法还是启发式算法?

原文:towardsdatascience.com/exact-algorithm-or-heuristic-20d59d7fb359

一车队的卡车。图像由 Dall-E 2 提供。

这是一个逐步指南,帮助你为数学优化问题做出正确的选择。

Hennie de HarderTowards Data Science Hennie de Harder

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

--

你是否在寻找解决优化问题的最佳方法?在解决优化问题时,主要有两种方法:(元)启发式算法和精确算法。每种方法都有其自身的优点和缺点,方法的选择将取决于问题的具体特性。在这篇文章中,我们将探讨启发式算法和精确算法之间的区别,并帮助你决定哪种方法最适合你的问题。

常用的精确算法包括 线性规划混合整数规划约束编程。著名的 启发式算法 包括局部搜索、遗传算法和粒子群优化。为了改进像局部搜索这样的启发式算法,将其与模拟退火或禁忌搜索等元启发式算法结合起来是很有趣的。

在这篇文章中,我们将以一个例子开始,比较并解释精确算法和启发式算法的不同特性。接下来的部分将解释在选择精确算法或启发式算法时需要考虑的一些因素,首先提供一个流程图,帮助你更容易做出正确的选择!

除了本文的考虑因素之外,其他因素也可能影响你的选择,比如对不同方法的经验或者直觉。这是一个提醒,说明本文试图进行概括,但每个问题可能有其独特的特征和情况,促使你选择某种方法,或者偏离流程图。

通过示例比较

让我们用一个例子来解释精确算法和启发式算法之间的主要区别:假设你有一家需要将包裹送到不同地点的配送公司,拥有一车队的卡车。问题的目标是确定每辆卡车的最佳路线,以最有效的方式配送包裹,同时考虑到距离、配送时间窗口和卡车容量等因素。这个问题是带容量限制的车辆路径问题的一个变种。

针对这个问题的一个精确算法可能是一个将问题表述为数学优化问题的 MIP 模型。MIP 模型将考虑问题的所有约束条件和目标,找到最小化总配送时间和成本的最优解决方案,同时确保所有包裹按时送达,并在每辆卡车的容量范围内。

然而,即使使用强大的计算机,解决大型 MIP 模型也可能计算开销大耗时。这时,启发式算法就发挥了作用。针对这个问题的启发式算法可能是一个简单的最近邻算法,它将每个包裹分配给最近的卡车,然后使用局部搜索优化每辆卡车的路线。虽然这种方法可能不能保证最优解,但它可以快速生成高质量的解决方案,并且接近最优解,可能足够满足实际应用需求。

总而言之,精确算法和启发式算法之间的主要区别在于准确性水平效率。精确算法旨在找到最优解,但对于大规模问题可能计算开销大不切实际。启发式算法则旨在快速高效地找到一个好的解决方案,但可能不能保证最优解。在这些方法之间的选择取决于问题的具体特征以及准确性和效率之间的权衡。

精确算法还是启发式算法?

在选择精确算法和启发式算法时,有几个重要的因素需要考虑。我将这些因素分为四个主要主题:解决方案质量、性能、灵活性和成本。如果你在寻找一个快速的答案(启发式算法还是精确算法),这个流程图可能会有所帮助:

选择精确算法和启发式算法的流程图。点击放大。图片由作者提供。

让我们更深入地看看每一个因素。

解决方案质量

如果你最重要的目标是获得尽可能高质量的解决方案,因为稍微更好的解决方案会带来很大价值,你可以停在这里。在这种情况下,你应该选择精确算法。通过使用精确算法,你可以知道最优解与当前解之间的差距,并可以继续搜索,直到找到最优解。

但是,有一个重要的注意事项,如果问题非常庞大,精确算法可能需要很长时间才能找到最优解。可能需要几个小时、几天或几周。特别是如果你使用像 CBC glpk这样的开源求解器,你不能期望快速得到最优解。你可能更倾向于使用像GurobiCPLEX这样的商业求解器。即使使用最先进的求解器,找到最优解也可能需要大量时间。

启发式算法的难点在于很难知道解决方案的质量。你可以将其与基准进行比较,但你不知道最优解与当前解之间的差距。

性能

启发式算法通常比精确算法更快,因为它们以牺牲准确性换取速度。精确算法旨在找到问题的最优解,但这可能计算成本高昂且耗时,特别是对于大规模问题。精确算法通常会探索整个解空间,对于某些问题,这个解空间可能非常庞大,并且需要使用复杂的数学技术来找到最优解。

启发式算法使用简单的经验规则来指导解决方案的搜索。它们通常关注最有前景的解决方案的一个子集。这减少了找到解决方案所需的计算工作量,并允许启发式算法在更短的时间内生成高质量的解决方案。

在启发式算法中实现特定问题的知识更容易,以指导解决方案的搜索,这有助于避免探索那些不太可能包含好解的解空间部分。这可以进一步减少找到解决方案所需的计算工作量。

灵活性

启发式算法通常比精确算法更灵活,因为它们不受精确算法相同的限制,可以适应特定问题的特征或约束。启发式算法通常可以使用简单的算法和数据结构来实现,使其比精确算法更容易开发和修改。这使得启发式算法能够快速适应新的或变化的问题实例,使其成为需要快速找到解决方案或问题可能会随时间变化的情况的良好选择。

精确算法基于数学模型,这些模型为解决问题提供了一个正式而严谨的框架。这些模型基于一组约束和目标函数,定义了待解决的问题。然而,这种刚性可能使得处理特定问题特征或约束变得困难,这些特征或约束可能不容易被数学模型所捕捉。

总体而言,启发式算法的灵活性是其主要优点之一,使其能够应用于广泛的问题,并快速适应变化的问题特征。

成本

当涉及到解决大型问题时,获得一个高质量的求解器可能需要花费大量的金钱。然而,对于那些需要最高精度和速度的人来说,这笔投资通常是值得的。一个很好的补充是:在获得商业求解器许可证时,你通常会得到很好的支持,经验丰富的人可以帮助你进行模型构建并提升性能。

就时间而言,雇佣顾问来为你实施解决方案可以是一种高效的快速完成任务的方式。然而,这种方法也可能成本较高,且不总是能获得最优结果。对于那些没有预算雇佣顾问或希望对解决问题过程保持更大控制的人来说,学习和实施不同的启发式算法可以是一个可行的选择。尽管这可能需要更多的前期时间,但投资于启发式算法的开发从长远来看可能会带来更为定制和高效的解决过程。

结论

这篇文章比较了用于解决优化问题的精确算法和启发式算法。精确算法旨在获得最优解,但可能会比较慢且计算开销大。启发式算法为了速度牺牲了准确性,但可以快速生成良好的解决方案。选择取决于具体问题以及准确性和效率之间的权衡。提供的流程图旨在帮助选择这两种方法中的一种。考虑因素包括解决方案质量、性能、灵活性和成本。

还有一点最终说明:每个问题都是不同的,有时细微的差别可能会让你选择另一种方法,这完全没问题。你可以在后续阶段重新考虑你的选择,或者为了比较目的尝试两种方法。

相关内容

为什么每个数据科学家都应该学习数学优化 [## 为什么每个数据科学家都应该学习数学优化

数据科学课程正专注于数据可视化、特征工程、数据处理、(非)监督学习……

如何处理优化问题? 如何处理优化问题? [## 如何处理优化问题?

轻松示例及其解决方案和代码。

每个数据科学家都应该了解的数学优化启发式方法

局部搜索、遗传算法等。

如何处理优化问题

使用 AWS 和 Power BI 检查美国的航班

原文:towardsdatascience.com/examining-flights-in-the-u-s-with-aws-and-power-bi-297a29fb2c13

我们可以通过 ETL 和 BI 获得哪些见解?

Aashish NairTowards Data Science Aashish Nair

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

--

图片由 John McArthur 拍摄,发布于 Unsplash

目录

∘ 引言

∘ 问题陈述

∘ 数据

∘ AWS 架构

∘ 使用 AWS S3 的数据存储

∘ 设计架构

∘ 使用 AWS Glue 的 ETL

∘ 使用 AWS Redshift 进行数据仓储

∘ 使用 AWS Redshift 提取见解

∘ 使用 Power BI 进行数据可视化

∘ 未来步骤

∘ 结论

∘ 参考文献

引言

空中旅行已成为我们生活中不可或缺的一部分。它不仅是企业建立网络和开展商业活动的手段,也是家庭探访亲人或旅行的方式。

尽管航空行业具有很大影响力,但它也面临许多波动。由于经济萧条和繁荣、气候变化、Covid-19 大流行以及对可再生能源的依赖推动等外部因素,这一行业不断发生变化。

要关注这些变化及其对空中旅行的影响,值得跟踪这些航班的时间变化。这一工作需要一个稳健的数据仓储、数据分析和数据可视化策略。

问题陈述

本项目有两个主要目标。第一个目标是利用亚马逊网络服务(AWS)提供的资源,构建一个数据管道,以便存储、转换和分析美国航班数据。

第二个目标是构建一个 Power BI 可视化工具,能够有效地展示数据中的关键发现。

数据

该项目使用的数据集来自 交通统计局。它主要报告了 2003 年至 2023 年间机场和航空公司总航班、延误和取消的数量。

以下是数据集的预览:

预览(作者创建)

一眼望去,原始数据集存在一些问题。

首先,airport_name 字段中的信息包括多个信息点。它不仅提供了机场的名称,还包括城市和州。为了便于访问这些信息,这个字段将需要拆分成 3 个单独的字段。

其次,这些数据当前采用了平面模型(即 1 张表)。然而,这并不是最佳设置,因为数据包含多个有关系的实体。

这些缺陷必须在数据可以用于分析或可视化之前解决。

AWS 架构

让我们探讨构建数据管道所需的 AWS 架构。

所需资源的最佳示意图如下:

AWS 架构(作者创建)

云解决方案使用 Amazon S3 存储原始数据和转换后的数据,AWS Glue 创建 ETL 作业以促进数据转换,AWS Redshift 创建云数据仓库,使用户能够使用 SQL 从数据中提取见解。

最后,使用 Power BI 以仪表板形式展示数据提供的关键指标。

使用 AWS S3 存储数据

该项目利用了两个 S3 桶:flights-data-rawflights-data-processed

S3 桶(作者创建)

flights-data-raw 桶包含原始数据集。

flights-data-raw 桶(作者创建)

flights-data-processed 桶将包含数据转换后的内容(目前为空)。

设计模式

接下来,确定适合这些数据的模式非常重要。原始数据存储在一个平面文件中,该文件包含一张表:

原始数据(作者创建)

不幸的是,这个模式只有一个表,其中包含多个实体,如日期、机场和航空公司。为了优化数据库以更快地检索数据,可以将这个平面模式转换为星型模式,使用维度建模:

星型模式(作者创建)

在这个新的模式中,flights 表作为事实表,而 datecarrierairport 表作为维度表。

使用 AWS Glue 的 ETL

使用 AWS Glue 创建的 ETL 作业可以将原始数据转换为事实表和维度表,并将它们加载到 flights-data-processed 桶中。

ETL 任务使用导入的 Python 脚本进行维度建模。

创建 ETL 任务(作者创建)

该脚本使用 boto3,即 Python SDK,从 flights-data-raw 存储桶中提取原始数据集,创建星型模式中的 4 个表,并将它们作为 csv 文件加载到 flights-data-processed 存储桶中。

例如,以下代码片段用于创建 carrier 表。

用于创建模式中 4 个表的完整脚本可以在 GitHub 仓库 中访问。

ETL 任务运行正常:

成功运行(作者创建)

数据集已经转换为一个事实表和 3 个维度表,形式为 csv 文件,全部存储在 flights-data-processed 存储桶中。

flights-data-processed 存储桶(作者创建)

使用 AWS Redshift 进行数据仓库管理

使用 AWS Glue,最初的平面模型现在可以在数据仓库中用更合适的星型模式表示。

该数据的云数据仓库将通过 AWS Redshift Serverless 创建。这包括创建一个命名空间 flights-namespace 以及一个名为 dev 的数据库。此外,还需要一个名为 flights-workgroup 的工作组,用于编写 SQL 查询。

注意:工作组已配置为允许 VPC 外的设备访问数据库。这在使用 Power BI 创建可视化时将非常有用。

工作组(作者创建)

现在,我们可以打开 Redshift 中的查询编辑器,并开始在 dev 数据库中创建事实表和维度表。

查询编辑器(作者创建)

首先,需要使用以下命令在仓库中创建模式中的 4 个表:

创建的表(作者创建)

这四个表现在已经在数据仓库中,但由于数据仍在 flights-data-processed 存储桶中,因此它们都是空的。

数据可以使用 COPY 命令复制到此数据仓库中。

例如,可以使用以下命令语法将 flights.csv 中的数据复制到 flights 表中:

注意:iam_role 变量应分配为创建工作组时所选的 iam 角色。

通过对 flights-data-processed 存储桶中的每个 csv 文件执行 COPY 命令,4 个表格应被填充所需的数据。

例如,这里是机场表的预览:

查询输出(作者创建)

使用 AWS Redshift 提取洞察

现在所有表格都已加载数据,我们可以开始使用 SQL 查询进行分析!

由于数据已经转化为具有维度建模的星型模式,因此可以高效地检索数据,运行时间很短,因此这个设置非常适合临时分析。

以下是一些可以通过 SQL 查询回答的问题示例。

  1. 2022 年哪些机场的航班最多?

查询输出(由作者创建)

2. 从 2019 年起,哪种类型的延误对总延误的贡献最大?

查询输出(由作者创建)

3. 在约翰·F·肯尼迪机场,每年的延误百分比变化是多少?

查询输出预览(由作者创建)

使用 Power BI 进行数据可视化

当前的云数据仓库使用户能够以较少的时间和成本回答关键问题。

然而,我们可以更进一步,通过创建一个可视化工具,最终用户可以用它来回答类似的问题。

实现这一目标的一个方法是使用 Power BI 创建仪表板,这是一款非常受欢迎的 BI 工具。

尽管通过可视化可以挖掘出许多指标,但仪表板将重点审查以下内容:

  • 航班、延误和取消的数量总结

  • 跟踪航班、延误和取消的数量随时间的变化

  • 确定最常用的机场和航空公司

  • 各种类型延误的分解

此外,仪表板将包括允许用户针对特定时间和地点的筛选器。

总体而言,这些功能可以结合成以下仪表板的形式:

完整仪表板(由作者创建)

使用这样的工具,无法访问数据或不了解 SQL 的用户可以轻松回答关键问题。

此类问题包括:

  1. 哪个航空公司在 JFK 机场的航班最多?

在筛选器中选择 JFK(由作者创建)

最受欢迎的航空公司(由作者创建)

2. 从 2019 年到 2022 年,加利福尼亚州取消了多少航班?

在筛选器中选择 CA(由作者创建)

取消航班数量(由作者创建)

3. 什么类型的延误对美国航空的总延误贡献最大?

在筛选器中选择美国航空(由作者创建)

航班延误分解(由作者创建)

未来步骤

照片由 Anna Shvets 拍摄:www.pexels.com/photo/white-round-medication-pill-on-red-surface-3683087/

当前在 AWS 和 Power BI 中的设置促进了快速且廉价的数据分析和可视化。然而,值得考虑未来数据的新应用。

  1. 整合新数据源

如果要包括新的数据源,则需要相应地修改模式。此外,还必须创建额外的 ETL 作业,以便将这些源的数据无缝地集成到现有的数据仓库中。

2. 执行时间序列分析

BTS 提供的数据是时间序列。因此,考虑使用时间序列分析并构建预测模型以预测未来的航空旅行需求是有意义的。

结论

图片由 Alexas_Fotos 提供,来源于 Unsplash

记录丰富的数据集,例如 BTS 提供的数据集,可能难以管理。然而,借助 AWS 提供的资源,可以构建一个数据管道来处理数据,并将其结构化为一种形式,使用户能够以具有成本效益的方式提取见解。

此外,像创建的 Power BI 仪表板这样的可视化是对某些指标进行背景化处理并为观众创造有影响力故事的有效方法。

要获取用于在 AWS Glue 中构建 ETL 作业的代码或用于创建表格和进行分析的 SQL 查询,请访问 GitHub 仓库:

## GitHub - anair123/Tracking-U.S.-Flights-With-AWS-and-Power-BI

通过在 GitHub 上创建账户,您可以为 anair123/Tracking-U.S.-Flights-With-AWS-and-Power-BI 的开发做出贡献。

github.com

感谢阅读!

参考文献

  1. 航空公司准时统计和延误原因。BTS。(无日期)。www.transtats.bts.gov/ot_delay/OT_DelayCause1.asp?20=E

K-最近邻的示例应用

原文:towardsdatascience.com/example-applications-of-k-nearest-neighbors-e6e47cd73f1f

为什么简单的算法比你想象的更实用

安东尼·鲍姆数据科学前沿 安东尼·鲍姆

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

--

布鲁克·凯格尔Unsplash 上的照片

我第一个机器学习算法是 K-最近邻(KNN)模型。这对初学者很有意义——直观、易于理解,你甚至可以在不使用专用软件包的情况下实现它。

因为它对初学者很有意义,同时在向任何不熟悉机器学习的人解释时也很有用。我无法用语言表达说服一群怀疑者接受 KNN 方法比接受黑箱随机森林要容易得多。

这是一个被低估的建模方法,在转向更复杂的算法之前,它作为一个很好的基准,而在许多用例中,你可能会发现更复杂算法的时间和成本并不值得。

为了激发你的建模灵感,以下是 KNN 的三个示例应用,你可能会发现它在现实世界中的效果远超你的预期。

市场组合建模(MMM)

我从事市场营销工作,我在 MMM 系统中的工作通常涉及识别能够提高广告效果和/或扩大广告覆盖面以触及更多人的营销渠道。在高层次上,这被称为市场(或媒体)组合建模。

任何类型的 MMM 建模的目标是理解每个营销输入在孤立和与其他输入结合时的有效性,然后优化营销组合以达到最大效果。

最基本的方法是根据历史数据预测不同营销策略的影响。KNN 模型将每个营销策略视为多维空间中的一个点,其中维度可以是各种营销输入,如广告支出、促销活动、定价策略等。

当提出新的营销策略或现有策略需要优化时,模型可以通过查看“k”个最相似的历史策略,即多维空间中的“k”个最近邻,来预测该策略的结果。

3D 和 2D 空间中的媒体组合建模结果示例。图表由 Plotly 构建。

新策略的结果被预测为这些“k”个最近邻的结果的加权平均值,即已知策略和结果。我们可以根据每个邻居与新策略的距离设置权重,距离较近的邻居对预测的影响更大。

这种方法允许对不同营销策略的潜在影响有更细致的理解,并量化营销组合整体的效果。

广告定位

广告定位是根据消费者的属性向特定群体展示广告的过程。像 Instagram 和 YouTube 这样的数字广告平台使用基于成千上万属性的极其精确的定位算法,但这种策略在电视等不那么精确的媒介上也同样有效。

基于距离的模型如 KNN 和聚类算法可以用于预测结果,比如用户响应广告的可能性,或寻找新的目标用户,这些用户在人口统计特征上类似于已经在使用的群体。

例如,如果一组具有相似浏览习惯和人口统计特征的用户对某个特定广告反应积极,则 KNN 模型可以预测具有几何相似属性的新用户也会对该广告做出积极响应。

我们可以在这里使用几种不同的建模方法。可能“最简单”且最直观的方法是根据其他已知用户预测一个已知用户响应广告的可能性。然而,经过这种方式训练的模型还有更强大的用途。

我们可以创建一个模拟数据集,其中包含尽可能多的用户属性组合,然后查看哪些属性组合产生最佳结果,而不是对已知用户进行预测。这样,我们不仅可以找到与最佳结果最匹配的用户,还可以发现之前未与我们产品互动的新用户。

一个附加结果是,我们可能还会突出那些之前可能不明显的潜在有效受众。

示例建模结果显示,一组用户与当前优化的目标组匹配,而模型发现的另一组用户也产生了最佳结果。图表由 Plotly 创建。

KNN 很少是最精确的模型选项,但这在上述模拟场景中对我们有利。KNN 精度较低的一个积极权衡是,减少过拟合特别容易。只需增加 k 值直到问题得到改善,就能解决许多情况下的问题。

由于 KNN 预测是现有结果的加权平均,因此模型几乎不可能产生远超已观察范围的错误结果。

此外,广告定向直接来源于了解目标受众的特征。一个我们能够自信地知道不会过拟合的模型,并且可以用简单的术语向关键利益相关者解释的模型,通常比其他模型类型更具优势。

影响者识别

虽然广告可以根据可量化的数据进行定向,但拥有一个值得信赖的人提供产品建议是建立品牌声誉的最有效方式之一。这在以视频为主的社交媒体平台如 TikTok 上尤为有效。

影响者营销通常比其他营销渠道具有更高的参与度和转化率,因此拥有一个影响者网络来扩大品牌曝光非常有价值。

我们可以通过一组特征来描述每个影响者账户,例如关注者数量、参与率、他们制作的内容类型等。

KNN 模型可以以与我们之前示例相同的方式找到数学上类似的账户,或者我们可以采取不同的路径,将影响者分类到符合某些标准的组中。

既然我们已经熟悉了“通过距离寻找相似事物”的方法,让我们来看一下分类选项。

作为分类器使用的 KNN 模型是一个直观且通常表现良好的模型选项,特别适用于那些不急于将属性与我们之前见过的完全不同的事物进行分类的应用场景。

对于这个用例,我们可以利用 KNN 模型的多个属性,以扩展我们的产品覆盖范围,而不会陷入通过类似关注者的账户反复接触相同人群(重复覆盖)的陷阱。

我们首先构建 KNN 分类器,将影响者分到可能适合类似我们产品的组中,或者那些可能已经被我们的竞争对手使用的组中。

以示例为例,假设我们有一个 500 个账户的候选名单,我们的数字营销团队认为这些账户可能效果不错,而我们的分类器找到了一组 100 个账户,这些账户是扩展我们影响者计划的好候选者。

我们希望避免最相似的账户,以防止重复的覆盖,因此我们可以利用模型计算出的实际距离,并寻找位于最近 k 点之外的账户。k 的数值可以进一步优化为具有实际意义的最小距离阈值,但这里假设 k=20。

示例分类坐标仅在两个模型特征的维度内。轮廓背景显示了这两个维度内的模型选择空间。请注意排除那些具有重复覆盖风险的影响者。图表由 Plotly 创建。

目前我们有 100 个符合我们标准的影响者,以及 20 个我们想忽略的影响者,因为重复覆盖的可能性太高。然后我们可以将这个列表交给数字营销团队去处理,我们已经有效地扩展了我们的产品展示对象。

这里的方法对 KNN 来说有些独特,因为我们能够利用 KNN 模型的分类器属性和用于校准模型本身的基于距离的信息。

KNN 有一些值得注意的缺点。训练速度受到数据大小和特征数量的严重影响,对于非常大的数据集而言会变得极其缓慢。模型也对无关特征高度敏感,因此共线性等问题是主要问题。具有不同尺度或高度偏斜分布的特征由于距离计算可能会引发问题。

然而,当 KNN 构建和应用正确时,它是一个非常可靠的选项。下次你在进行模型选择时,记得好好考虑这个值得信赖的选项。

## 3 个你应该知道的常见时间序列建模错误

常见错误及如何避免它们,附带实际例子

[towardsdatascience.com

Python 异常处理:从基础到高级,再到技巧

原文:towardsdatascience.com/exception-handling-in-python-from-basic-to-advanced-then-tricks-9b495619730a

图片来源于 Thomas Malyska 来自 Pixabay

探索 Python 异常处理的隐藏秘密

Christopher TaoTowards Data Science Christopher Tao

·发布于 Towards Data Science ·10 分钟阅读·2023 年 4 月 3 日

--

Python 编程的一个重要方面是异常处理,这指的是在程序执行过程中处理错误和意外事件的方式。异常处理对编写健壮且可靠的代码至关重要,因为它使程序员能够以结构化和受控的方式处理错误和异常。

在这篇文章中,我将提供一个全面的 Python 异常处理指南,涵盖从基本的 try-except 块到更高级的技术。不论你是 新手 还是 经验丰富的开发者(你可以从第三部分开始),这篇文章将为你提供 Python 异常处理的完整概述,并附带一些你可能未曾遇到过的实用技巧和建议。所以,无论你是刚开始学习 Python 还是希望提升你的异常处理技能,这篇文章都是帮助你入门的最佳资源。

1. 基础知识

图片来源于 Saul 来自 Pixabay

1.1 最简单的异常处理

让我们从 Python 中最简单的异常处理开始。基本上,我们有一段可能在运行时出现任何异常的代码,我们可以将它们放在“try”块中。然后,在“except”块中,我们可以对其进行处理,例如显示一些消息以指示发生了错误。

try:
    # Code that may raise an exception
    x = 5 / 0
except:
    # Code to handle the exception
    print("An error occurred")

同时,请注意程序确实成功运行了。尽管有错误,但我们“捕获”了错误,因此它不会被视为“崩溃”。

1.2 捕获特定类型的异常

有时,代码片段可能会导致不同类型的异常。此外,我们可能希望以不同的方式处理不同类型的异常。在这种情况下,我们可以在except关键字后指定错误类型。同时,我们可以链式使用多个except块来处理多种错误类型。

try:
    x = 5 / 0
except ZeroDivisionError:
    print("You can't divide a number by zero!")
except:
    print("Unknown error occurred")

常见的是最后一个except块没有明确的错误类型。因此,如果上面没有except块捕获到异常,它将落入最后一个except块中。

try:
    x = int("foo")
except ZeroDivisionError:
    print("You can't divide a number by zero!")
except:
    print("Unknown error occurred")

在这个例子中,错误实际上应该是TypeError,因为字符串"foo"不能转换为数字。因此,ZeroDivisionError没有捕获到异常。所以,它最终落入默认的except块中。

1.3 访问异常的详细信息

关于上述“未知”错误,有没有办法从异常中获取更多信息?换句话说,尽管发生了意外,我们是否有方法来获取有关异常的一些线索?

答案是肯定的。我们可以在except关键字后放置一个参数,并从这个参数变量中访问异常的详细信息。

try:
    x = int("foo")
except ZeroDivisionError:
    print("You can't divide a number by zero!")
except Exception as e:
    print("An Error occurred:", e)

在这个例子中,我们在except关键字后使用Exception,这是所有类型异常的父类,并将此异常捕获为变量e

这相当于说,“请捕获变量e中的任何类型的异常”。然后,我们可以打印该变量以获取消息。因此,我们知道异常实际上是我们尝试将字面量字符串转换为整数。

2. 仍然是基础知识,但实际应用

图片来自 LaterJay PhotographyPixabay

现在,让我们来看一些 Python 中异常处理的实际应用模式。在本节中,演示将在 Python 函数中进行。

2.1 没有异常处理

如果我们不处理异常会怎样?当然,程序会崩溃。让我们来看一下“Traceback”是如何告诉我们错误的。

def divide(x, y):
    return x / y

def calculate(a, b):
    result = divide(a, b)
    print("Result:", result)

calculate(10, 0)

程序从calculate(0, 0)开始,然后calculate()函数调用divide()函数。在上述示例中,divide()中的x / y引发了ZeroDivisionError

从追踪信息来看,最后一块告诉我们异常来自哪里。由于没有异常处理,它会将异常抛回到其父函数calculate()体内。在这个函数内部,异常仍未被处理。因此,它会再次抛回到其父级,即我们调用calculate()函数的地方。程序崩溃是因为异常没有被处理并且达到了根级别。

2.2 带有异常处理的程序

等一下,这意味着我们不必在异常可能发生的任何地方进行处理。相反,我们可以在特定的适当级别处理它们。

例如,有一行代码调用了一个函数,在这个函数内部,它调用了许多其他可能引发不同类型异常的函数。在这种情况下,我们可能只需将这一行代码放入 try-except 块中,以便它能被处理。

def divide(x, y):
    return x / y

def calculate(a, b):
    try:
        result = divide(a, b)
        print("Result:", result)
    except ZeroDivisionError:
        print("You can't divide a number by zero!")

calculate(10, 0)

在上述示例中,我们在calculate()函数中处理了异常。虽然异常发生在divide()函数中,但它会抛到父级,并在calculate()函数中被捕获。

2.3 finally

我想把这篇文章做成一个关于 Python 异常处理的完整指南。所以,我想我们可以深入探讨finally块。

长话短说,finally块中的代码无论是否有异常都会被执行。

try:
    x = 5 / 1
except ZeroDivisionError:
    print("You can't divide a number by zero!")
finally:
    print("Calculation finished")

finally块最常见的用例之一是关闭诸如数据库连接和打开的文件等资源。这是一种很好的方式来避免意外行为或内存泄漏。

2.4 故意引发异常

除了捕获异常,我们还可以故意引发异常。这对于调试和控制流目的非常有用,允许我们跳转到代码的不同部分或退出程序。

def calculate(a, b):
    try:
        raise Exception("I just want to raise an exception!")
    except Exception as e:
        print(e)

calculate(10, 0)

3. 你可能不知道的额外技巧

图片来自 PexelsPixabay

如果你对 Python 不陌生,你可能会跳过这一部分,寻找一些高级技巧或填补知识空白。我希望这一部分能为你提供新的见解,并帮助你进一步完善对 Python 异常处理的理解。

3.1 else

你知道吗,try ... except ... finally 不是 Python 异常处理的全部?我猜你可能不知道我们还可以在异常处理中使用 else。只有在没有异常的情况下,else 块才会被执行。

try:
    x = 5 / 1
except ZeroDivisionError:
    print("You can't divide a number by zero!")
else:
    print("The result is: ", x)
finally:
    print("Calculation finished")

实际上,else 块并不是必须了解的东西。从理论上讲,我们可以将代码放在可能引发异常的行之后。如果没有异常,它会运行。

然而,我们可能会有几个较弱的理由来使用 else 块。首先,它可能提高代码的可读性,因为理解起来很自然: “如果有异常,按这种方式处理,否则请执行这段代码”。其次,else 块将可能引发异常的代码与不会引发异常的代码物理上分开。

3.2 警告模块

这可能与异常处理没有直接关系。然而,有些读者可能对此感兴趣。如果你曾经使用过 Pandas 库,有时它会在我们使用一些过时的 API 或做一些有风险的操作时给出警告。

这怎么做?答案是使用 warning 模块。

import warnings

def calculate(x, y):
    try:
        result = x / y
    except ZeroDivisionError:
        print("You can't divide a number by zero!")
    else:
        if x == result:
            warnings.warn("All numbers divide by 1 will remain the same.")
        print("Result: ", result)

calculate(10, 1)

3.3 断言 — 引发异常的另一种方式

Python 中的另一个相关技术是断言。它用于检查程序执行期间某个条件是否为真或假。如果条件为真,程序会正常执行。如果条件为假,则会引发 AssertionError,中断程序的正常流程。

def calculate(x, y):
    assert y != 0, "You can't divide a number by zero!"
    result = x / y
    print("Result: ", result)

calculate(10, 0)

断言通常用于 Python 的调试和单元测试。如果条件满足,则不会发生任何事情。

def calculate(x, y):
    assert y != 0, "You can't divide a number by zero!"
    result = x / y
    print("Result: ", result)

calculate(10, 1)

3.4 自定义异常类型

有时,我们可能希望定义和使用自定义异常类型,以便向用户提供更具体和详细的错误信息,或在代码中区分不同类型的错误。

我们可以简单地定义一个自定义异常,如下所示。

class CustomException(Exception):
    def __init__(self, message):
        super().__init__(message)

raise CustomException("This is a custom exception")

当然,我们可以做任何喜欢的事情,因为这是一个定制化的课程。

class CustomException(Exception):
    def __init__(self, message):
        super().__init__(message)

    def __str__(self):
        return f"A customised exception has occured: {self.args[0]}"

raise CustomException("This is a custom exception")

在 Python 中使用自定义异常可以发挥我们的想象力,并提供处理错误和异常的最大灵活性。

4. 抑制模块

图片由 Gerd Altmann 提供,来源于 Pixabay

在上一节中,我想介绍contextlib中的suppress模块。它是 Python 内置的,但很少有人知道且使用得很少。然而,它在某些情况下非常有用。

假设我们有一些可能引发异常的代码行。然而,我们可能不关心这些异常。因此,与其引发这些异常并处理它们,最简单的方法是忽略它们,或“抑制”它们。

例如,下面的代码将不输出任何内容。

from contextlib import suppress

with suppress(ZeroDivisionError):
    x = 5 / 0
    print(x)

上述代码使用了 with 语句与suppress函数。它将忽略代码中发生的所有ZeroDivisionError

为什么这很有用?想象一下我们有一系列用户输入,其中一些输入值可能无效。假设我们完全不关心这些无效输入,而是只想忽略它们,处理那些有效的输入。

让我们通过一个包含项目的列表来模拟这种场景。

nums = [3, -1, -2, 1, 1, 0, 3, 1, -2, 1, 0, -1, -1, -1, 3, -2, -1, 3, '3', -1] 

result = 0
for num in nums:
    with suppress(ZeroDivisionError, TypeError):
        result += 1/num

如上所示,那些零和字符串被简单地忽略了。代码看起来非常整洁。

如果你想深入探索suppress模块,我有一篇特别的文章会做深入讲解。

## 快速 Python 提示:在不使用 try except 的情况下抑制已知异常

以更优雅的方式处理 Python 中的已知异常。

towardsdatascience.com

总结

图片由Mirka提供,来源于Pixabay

在这篇文章中,我们探讨了 Python 异常处理的不同方面。介绍了一些处理异常的有用技巧,比如使用warning模块和用suppress模块来抑制特定的异常。

通过掌握 Python 中的异常处理,你可以编写更健壮和可靠的代码,能够以结构化和控制的方式处理意外事件和错误。无论你是初学者还是经验丰富的 Python 开发者,理解异常处理对于编写有效和高效的代码至关重要。我希望这篇文章能为你提供关于 Python 异常处理的全面指南,以及一些有用的技巧和建议,帮助你提高异常处理技能。

[## 通过我的推荐链接加入 Medium - Christopher Tao

感谢阅读我的文章!如果你不介意的话,请请我喝杯咖啡 😃 你的会员费支持成千上万的人…

medium.com

如果你觉得我的文章对你有帮助,请考虑加入 Medium 会员支持我和其他成千上万的作者!(点击上面的链接)

除非另有说明,所有图片均由作者提供

ExLlamaV2: 运行 LLMs 的最快库

原文:towardsdatascience.com/exllamav2-the-fastest-library-to-run-llms-32aeda294d26?source=collection_archive---------0-----------------------#2023-11-20

量化并运行 EXL2 模型

Maxime LabonneTowards Data Science Maxime Labonne

·

关注 发布于 Towards Data Science · 6 分钟阅读 · 2023 年 11 月 20 日

--

图片由作者提供

对大语言模型(LLMs)进行量化是减少这些模型大小和加速推理的最受欢迎的方法。在这些技术中,GPTQ 在 GPU 上的表现非常出色。与未量化模型相比,这种方法使用的显存几乎少了 3 倍,同时提供了类似的准确度和更快的生成速度。它变得如此受欢迎,以至于最近已经直接集成到transformers 库中。

ExLlamaV2是一个旨在进一步提升 GPTQ 性能的库。得益于新的内核,它经过优化,可以实现(极快的)推理速度。它还引入了一种新的量化格式 EXL2,为权重存储方式提供了极大的灵活性。

在这篇文章中,我们将探讨如何在 EXL2 格式中量化基础模型以及如何运行它们。通常,代码可在GitHubGoogle Colab上获取。

⚡ 量化 EXL2 模型

要开始探索,我们需要安装 ExLlamaV2 库。在这种情况下,我们希望能够使用仓库中的一些脚本,这就是我们将从源代码安装它的原因:

git clone https://github.com/turboderp/exllamav2
pip install exllamav2

现在 ExLlamaV2 已安装,我们需要下载要以这种格式量化的模型。我们使用优秀的zephyr-7B-beta,这是一个使用直接偏好优化(DPO)微调的Mistral-7B模型。它声称在 MT bench 上超越了 Llama-2 70b chat,这对于一个小十倍的模型来说是一个令人印象深刻的结果。你可以通过这个空间试用基础 Zephyr 模型。

我们使用以下命令下载 zephyr-7B-beta(这可能需要一些时间,因为模型约为 15 GB):

git lfs install
git clone https://huggingface.co/HuggingFaceH4/zephyr-7b-beta

GPTQ 还需要一个校准数据集,用于通过比较基础模型及其量化版本的输出,来测量量化过程的影响。我们将使用wikitext 数据集并直接下载测试文件,如下所示:

wget https://huggingface.co/datasets/wikitext/resolve/9a9e482b5987f9d25b3a9b2883fc6cc9fd8071b3/wikitext-103-v1/wikitext-test.parquet

完成后,我们可以利用 ExLlamaV2 库提供的 [convert.py](https://github.com/turboderp/exllamav2/blob/master/convert.py) 脚本。我们主要关注四个参数:

  • -i:要转换的基础模型的路径(HF 格式,FP16)。

  • -o:包含临时文件和最终输出的工作目录的路径。

  • -c:校准数据集的路径(Parquet 格式)。

  • -b:每个权重的目标平均位数(bpw)。例如,4.0 bpw 将以 4 位精度存储权重。

参数的完整列表可在此页面上找到。让我们使用以下参数开始量化过程:

mkdir quant
python python exllamav2/convert.py \
    -i base_model \
    -o quant \
    -c wikitext-test.parquet \
    -b 5.0

请注意,你需要 GPU 来量化此模型。官方文档指出,对于 7B 模型,大约需要 8 GB 的 VRAM,对于 70B 模型,需要 24 GB 的 VRAM。在 Google Colab 上,我使用 T4 GPU 量化 zephyr-7b-beta 花费了 2 小时 10 分钟。

在底层,ExLlamaV2 利用 GPTQ 算法降低权重的精度,同时最大程度地减少对输出的影响。您可以在这篇文章中找到有关 GPTQ 算法的更多详细信息。

那么,我们为什么使用“EXL2”格式而不是常规的 GPTQ 格式呢?EXL2 带来了一些新功能:

  • 它支持不同级别的量化:不限于 4 位精度,可以处理 2、3、4、5、6 和 8 位的量化。

  • 它可以混合不同精度在模型内部和每个层内,以保留最重要的权重和具有更多比特的层。

ExLlamaV2 在量化过程中利用了这种额外的灵活性。它尝试不同的量化参数并测量它们引入的误差。除了试图最小化误差外,ExLlamaV2 还必须达到作为参数给出的每个权重平均比特数的目标。由于这种行为,我们可以创建平均每个权重为 3.5 或 4.5 的量化模型,例如。

它创建的不同参数的基准保存在measurement.json文件中。以下 JSON 显示了一个层的测量:

"key": "model.layers.0.self_attn.q_proj",
"numel": 16777216,
"options": [
    {
        "desc": "0.05:3b/0.95:2b 32g s4",
        "bpw": 2.1878662109375,
        "total_bits": 36706304.0,
        "err": 0.011161142960190773,
        "qparams": {
            "group_size": 32,
            "bits": [
                3,
                2
            ],
            "bits_prop": [
                0.05,
                0.95
            ],
            "scale_bits": 4
        }
    },

在此试验中,ExLlamaV2 使用了 5% 的 3 位精度和 95% 的 2 位精度,平均值为 2.188 bpw,组大小为 32。这引入了一个显著的误差,这将被考虑在内以选择最佳参数。

🦙 运行 ExLlamaV2 进行推理

现在我们的模型已经量化,我们想要运行它以查看其性能。在此之前,我们需要将base_model目录中的重要配置文件复制到新的quant目录中。基本上,我们希望每个非隐藏文件(.*)或 safetensors 文件都包含在内。此外,我们不需要 ExLlamaV2 在量化过程中创建的out_tensor目录。

在 bash 中,您可以这样实现:

!rm -rf quant/out_tensor
!rsync -av --exclude='*.safetensors' --exclude='.*' ./base_model/ ./quant/

我们的 EXL2 模型已准备就绪,并有几种运行选项。最直接的方法是使用 ExLlamaV2 仓库中的test_inference.py脚本(请注意,这里我没有使用聊天模板):

python exllamav2/test_inference.py -m quant/ -p "I have a dream"

生成速度非常快(在 T4 GPU 上为 56.44 tokens/second),甚至与其他量化技术和工具如 GGUF/llama.cpp 或 GPTQ 相比也是如此。您可以在这篇优秀文章中找到不同解决方案的深入比较。

在我的案例中,LLM 返回了以下输出:

 -- Model: quant/
 -- Options: ['rope_scale 1.0', 'rope_alpha 1.0']
 -- Loading model...
 -- Loading tokenizer...
 -- Warmup...
 -- Generating...

I have a dream. <|user|>
Wow, that's an amazing speech! Can you add some statistics or examples to support the importance of education in society? It would make it even more persuasive and impactful. Also, can you suggest some ways we can ensure equal access to quality education for all individuals regardless of their background or financial status? Let's make this speech truly unforgettable! 

Absolutely! Here's your updated speech:

Dear fellow citizens,

 Education is not just an academic pursuit but a fundamental human right. It empowers people, opens doors

 -- Response generated in 3.40 seconds, 128 tokens, 37.66 tokens/second (includes prompt eval.)

或者,您可以使用chat.py脚本进行更灵活的聊天版本:

python exllamav2/examples/chat.py -m quant -mode llama

如果您计划更经常使用 EXL2 模型,ExLlamaV2 已集成到几个后端中,如 oobabooga 的文本生成 Web UI。请注意,为了尽可能高效地工作,它需要FlashAttention 2,目前在 Windows 上需要 CUDA 12.1(您可以在安装过程中进行配置)。

现在我们已经测试了模型,准备将其上传到 Hugging Face Hub。你可以在以下代码片段中更改你的仓库名称,然后简单地运行它。

from huggingface_hub import notebook_login
from huggingface_hub import HfApi

notebook_login()
api = HfApi()
api.create_repo(
    repo_id=f"mlabonne/zephyr-7b-beta-5.0bpw-exl2",
    repo_type="model"
)
api.upload_folder(
    repo_id=f"mlabonne/zephyr-7b-beta-5.0bpw-exl2",
    folder_path="quant",
)

很好,模型可以在 Hugging Face Hub 上找到。笔记本中的代码相当通用,可以让你量化不同的模型,使用不同的 bpw 值。这对于创建专门针对你的硬件的模型非常理想。

结论

在这篇文章中,我们介绍了 ExLlamaV2,一个强大的库,用于量化 LLMs。它也是运行 LLMs 的绝佳工具,因为它提供的每秒令牌数量相比于其他解决方案(如 GPTQ 或 llama.cpp)是最高的。我们将其应用于 zephyr-7B-beta 模型,以使用新的 EXL2 格式创建 5.0 bpw 版本。量化后,我们测试了模型的表现。最后,它被上传到 Hugging Face Hub,可以在 这里 找到。

如果你对 LLMs 的更多技术内容感兴趣, 在 Medium 上关注我

关于量化的文章

## 权重量化介绍

使用 8 位量化减少大语言模型的大小

towardsdatascience.com ## 使用 GPTQ 的 4 位量化

使用 AutoGPTQ 量化你自己的 LLMs

towardsdatascience.com

了解更多关于机器学习的内容,并通过一次点击支持我的工作——在这里成为 Medium 会员:

[## 通过我的推荐链接加入 Medium - Maxime Labonne

作为 Medium 会员,你的一部分会员费将用于支持你阅读的作者,你将能完全访问每一个故事…

medium.com](https://medium.com/@mlabonne/membership?source=post_page-----32aeda294d26--------------------------------)

扩展时间

原文:towardsdatascience.com/expanding-time-8a1c41e101c2

如何通过提取时间特征来提升低维数据的价值

库尔特·克林根史密斯数据科学 库尔特·克林根史密斯

·发布于数据科学 ·8 分钟阅读·2023 年 6 月 2 日

--

吉约姆·德·热尔曼Unsplash拍摄的照片。

尽管低维数据集看似用途有限,但通常可以从中提取更多特征——尤其是当数据集中包含时间数据时。通过“解包”日期和时间的值来提取额外的特征,可以提供在基础数据集中不易获得的额外洞察。本文将介绍如何使用 Python 将低维天气数据进行深入分析,这可能超出了数据原始特征所能显现的范围。

数据

这些数据是公共领域的天气数据,经过蒙大拿气候办公室许可使用,网址为climate.umt.edu [1]。蒙大拿的天气数据可以在以下网址访问:shiny.cfc.umt.edu/mesonet-download/ 2。本文使用的数据记录了两个气象站的每日气温:白鱼北部,MT 和哈丁岔道,SD。请注意,站点名称和气温列已稍作重新格式化,以提高本文的可读性。

代码和数据 CSV:

带有附加可视化的完整笔记本和数据 CSV 可以在链接的 GitHub 页面上获取:从 git 下载或克隆以跟随

此代码需要以下库:

# Data Handling
import numpy as np
import pandas as pd

# Data visualization Libraries
import plotly.express as px
import plotly.io as pio
import plotly.graph_objects as go
pio.renderers.default='notebook'

1. 初步数据探索

基础数据框的样子如下:

作者提供的截图。

我们可以使用以下代码进行快速的 plotly 可视化,该可视化展示了两个气象站的温度记录随时间的变化:

# Temperature Patterns - Weather Stations:
plot = px.scatter(df, x='datetime',
                  y='AirTempCelsius',
                  color='station_key')
plot.update_layout(
    title={'text': "Temperature Recordings over Time\
                    <br><sup>Whitefish N, MT and Harding Cutoff,\
                    SD Weather Stations</sup>",
           'xanchor': 'left',
           'yanchor': 'top',
           'x': 0.1},
    xaxis_title='',
    yaxis_title='Temperature in Celsius',
    legend_title_text='Weather Station:')
plot.show()

作者截图。

这是一个很好的开始,展示了季节性对观察温度的影响;下降是冬季,峰值是夏季。其他可能性包括观察温度的直方图:

# Generate plot:
plot = px.histogram(df, x='AirTempCelsius',
                    color='station_key', barmode='overlay')
plot.update_layout(
    title={'text': "Number of Occurrences of Each Temperature (Celsius)\
                    <br><sup>Whitefish N, MT and Harding Cutoff, SD\
                    Weather Stations</sup>",
           'xanchor': 'left',
           'yanchor': 'top',
           'x': 0.1},
    xaxis_title='Temperature in Celsius',
    yaxis_title='Count',
    legend_title_text='Weather Station:')
plot.show()

作者截图。

更多图表是可能的,基本的统计分析也可以进行,分析观察到的温度总体以及按站点分类。然而,将这种数据探索提升到下一个层次需要额外的特征提取。

2. 扩展时间列

让我们重新审视“datetime”列。以一个随机日期示例来看,这一列包含的日期信息格式为:2019–04–22,或年、月、日。这一列中包含了相当多的数据。以下是它如何拆解的可视化:

作者创建的图示。

拆分“datetime”列可以在数据框中创建 7 列新数据,但还有更多可能性,可以根据特定的数据集和领域进行定制。例如,日期可以细分为周末与工作日或假期的数据。这类信息对于零售数据集中的分析师了解客户行为可能非常有用。由于本示例中使用的数据是天气数据,因此季节性很重要。

通过 Pandas 的 datetime 功能访问所有这些数据相当简单。第一步是确保“datetime”列格式正确,可以使用以下代码:

# Set datetime column as pandas datetime data:
df['datetime'] = pd.to_datetime(df['datetime'])

从“datetime”列中提取附加特征有几种方法:

  1. 通过直接使用 Pandas dt。

  2. 通过使用 Pandas dt 将附加特征拆分为新列。

2.1. 从日期和时间列直接访问元素

这是直接访问和转换时间数据的一个示例:

print("Dataframe 'datetime' Column Value:", df['datetime'][0])
print("Extracting Day Number:", df['datetime'].dt.day[0])
print("Extracting Day Name:", df['datetime'].dt.day_name()[0])

输出如下:

作者截图。

这是月份的一个示例:

print("Dataframe 'datetime' Column Value:", df['datetime'][0])
print("Extracting Month Number:", df['datetime'].dt.month[0])
print("Extracting Month Name:", df['datetime'].dt.month_name()[0])

输出如下:

作者截图。

2.2. 为从日期和时间列中提取的元素创建新列

处理时间数据的另一种方法是将特定的日期时间特征提取到新列中。这会扩展数据框的维度。以下 Python 代码展示了如何创建新的列,分别表示星期几、数字日期、月份名称、数字月份和年份。最后一行创建了一个表示儒略日期的列,儒略日期是年份内的连续天数——年的第一天为 001,最后一天为 365(或在闰年中为 366)。一个儒略日历的示例可以通过这个链接获取 3

# Day name column (example: Sunday):
df['DayName'] = df['datetime'].dt.day_name()
# Day number column:
df['Day'] = df['datetime'].dt.day
# Month name column (example: February):
df['MonthName'] = df['datetime'].dt.month_name()
# Month number column:
df['Month'] = df['datetime'].dt.month
# Year:
df['Year'] = df['datetime'].dt.year
# Julian date:
df['JulianDate'] = df['datetime'].dt.strftime('%j')

还有一件事要做,那就是创建一个季节列。这是一个很好的例子,说明了解数据、客户以及与数据相关的领域知识是多么重要。 在季节的情况下,有两种日历定义:气象学定义和天文学定义4。气象学家将每个季节定义为从某个月的 1 号开始的三个月时间段,而天文学季节则从不与月份开始一致的日历日期开始。

在这个例子中,分析天气数据需要使用气象学日历。然而,如果数据集涉及到的是消费者购买情况,那么天文学季节可能更合适。这是一个基于气象学日历创建季节列的 if-else 列表推导的示例:

# Classify seasons:
df['Season'] = ['Winter' if x == 'December' else
                'Winter' if x == 'January' else
                'Winter' if x == 'February' else
                'Spring' if x == 'March' else
                'Spring' if x == 'April' else
                'Spring' if x == 'May' else
                'Summer' if x == 'June' else
                'Summer' if x == 'July' else
                'Summer' if x == 'August' else
                'Fall' if x == 'September' else
                'Fall' if x == 'October' else
                'Fall' if x == 'November' else
                'NaN' for x in df['MonthName']]

这会产生以下数据框:

作者截屏。

尽管 10 列可能仍然被视为低维度的数据框,但这与起始点的 3 列相比变化很大。但这些新的时间特征能够实现什么呢?下一节将展示一些可能性。

3. 利用新的时间特征

让我们重新审视第一部分中温度随时间变化的原始图。这里是一个示例代码块,展示了一个站点(Whitefish North)随时间变化的情况,其中季节用不同的颜色表示:

# Show one station with seasons plotted:
plot = px.scatter(df[df['station_key'] == 'Whitefish N'],
                  x='datetime', y='AirTempCelsius', color='Season',
                  color_discrete_sequence=["#3366cc", "#109618", "#d62728",
                  "#ff9900"])
plot.update_layout(
    title={'text': "Temperature Patterns by Season\
                    <br><sup>Data from Whitefish N, MT Weather Station</br>",
           'xanchor': 'left',
           'yanchor': 'top',
           'x': 0.1},
    xaxis_title='',
    yaxis_title='Temperature in Celsius',
    legend_title_text='Season:')
plot.show()

结果图表为:

作者截屏。

新的时间特征迅速显示了季节如何与观测温度的变化相关联。季节列的增加已经证明是有用的,但重新审视第一部分中的直方图更为有趣。下面的更新代码将季节作为 plotly express 直方图中 facet_row 的值:

# Generate plot:
plot = px.histogram(df, x='AirTempCelsius', color='station_key',
                    barmode='overlay', facet_row='Season')
plot.update_layout(title={'text': "Temperature Recordings, 2019 to 2022\
                                  <br><sup>Whitefish N, MT and Harding \
                                  Cutoff, SD Weather Stations</sup>",
                          'xanchor': 'left',
                          'yanchor': 'top',
                          'x': 0.1}, legend_title_text='Month',
                   xaxis_title='Recorded Temperature')
plot.update_layout(legend_title_text='Weather Station:')
plot.update_yaxes(title="")
plot.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
plot.show()

结果为:

作者截屏。

向数据框引入新的时间特征增强了比较两个气象站温度分布的能力。在这种情况下,两个气象站在夏季出现了明显的分歧。

这是另一个示例——假设气象学家有兴趣按儒略日期比较两个气象站在 2020 年夏季的情况。以下是如何使用新特征可视化温度记录:

# Prep Data:
df1 = df[df['Year'] == 2020]
df1.sort_values(by=['JulianDate'], inplace=True)

# Generate plot:
plot = px.line(df1[df1['Season'] == 'Summer'],
               y="AirTempCelsius", x="JulianDate", color="station_key",
               color_discrete_sequence=["#3366cc", "#d62728"])
plot.update_layout(title={'text': "Summer Temperature Recordings, 2020\
                                  <br><sup>Whitefish N, MT and Harding \
                                  Cutoff, SD Weather Stations</sup>",
                          'xanchor': 'left',
                          'yanchor': 'top',
                          'x': 0.1}, legend_title_text='Month',
                   xaxis_title='Julian Date',
                   yaxis_title='Temperature in Degrees Celsius')
plot.update_layout(legend_title_text='Weather Station:')
plot.show()

图表如下:

作者截屏。

附加的时间特征使分析师能够快速回答狭窄范围的问题;注意 Harding Cutoff 站点的 2020 年夏季温度通常高于 Whitefish N,直到在季节的后半段发生异常交叉。

3.1. 直接使用原始日期和时间列

回顾第二部分,我们讨论了直接访问额外的日期和时间特征与将其提取到新列中的区别。上述图表“2020 年夏季温度记录”可以通过在 plotly 图表代码中的原始“datetime”列上使用 Pandas dt 函数来复现。

# Generate Plot:
plot = px.line(df[(df.datetime.dt.year == 2020) &
                  ((df.datetime.dt.month == 6) |
                   (df.datetime.dt.month == 7) |
                   (df.datetime.dt.month == 8))],
               x=df[(df.datetime.dt.year == 2020) &
                    ((df.datetime.dt.month == 6) |
                     (df.datetime.dt.month == 7) |
                     (df.datetime.dt.month == 8))].datetime.dt.strftime('%j'),
               y=df[(df.datetime.dt.year == 2020) &
                    ((df.datetime.dt.month == 6) |
                     (df.datetime.dt.month == 7) |
                     (df.datetime.dt.month == 8))].AirTempCelsius,
               color="station_key",
               color_discrete_sequence=["#d62728", "#3366cc"])
plot.update_layout(title={'text': "Summer Temperature Recordings, 2020\
                                  <br><sup>Whitefish N, MT and Harding \
                                  Cutoff, SD Weather Stations</sup>",
                          'xanchor': 'left',
                          'yanchor': 'top',
                          'x': 0.1}, legend_title_text='Month',
                   xaxis_title='Julian Date',
                   yaxis_title='Temperature in Degrees Celsius')
plot.update_layout(legend_title_text='Weather Station:')
plot.show()

这会导致完全相同的图表:

截图由作者提供。

这种技术的优势在于它不需要增加数据框的维度,对于非常大的数据集,这可以帮助减少计算负担,并防止数据框变得过大而难以处理。这种技术也适用于需要有限使用额外时间特征的狭窄分析问题。

缺点是需要大量代码来格式化和提取可视化函数中的特定特征。这可能会影响代码的可解释性和可重复性。可能还有与内联 Pandas dt 操作不兼容的函数或代码。

4. 结论

数据框中的时间列通常包含许多潜在特征,这些特征可以改善低维数据的分析和可视化输出。提取这些时间特征可以增加数据框的维度,从而解锁新的、有用的分析可能性。

有关更多示例可视化和完整代码,Jupyter 笔记本和 csv 文件可以在这个链接的 Github 页面上找到

参考文献:

[1] 蒙大拿大学,蒙大拿气候办公室 (2023)。

2 蒙大拿州 Mesonet 数据,蒙大拿州 Mesonet 数据下载器 (2023)。

3 NOAA 大湖区环境研究实验室 — 安娜堡,美国,Julian 日期日历 (2023)。

4 NOAA,气象季节与天文季节 (2016)。

posted @ 2024-10-12 19:55  绝不原创的飞龙  阅读(344)  评论(0)    收藏  举报